Skip to content

feat: add ownership ingestion core - #78

Merged
Aliferous3 merged 2 commits into
mainfrom
feat/ownership-ingestion-core
Oct 18, 2026
Merged

Aliferous3 merged 2 commits into
mainfrom
feat/ownership-ingestion-core

Conversation

@Aliferous3

Copy link
Copy Markdown
Owner

Summary

Phase 1C-A — the deterministic, privacy-preserving ingestion core for Code Ownership Over Time. Pure modules under lib/ownership/ plus a hard-off feature gate. No runtime wiring, no persistence, no production callers — production keeps emitting exactly zero ownership rows. Implements the contract from docs/ownership-data-privacy-spec.md against the schema deployed by migration 012.

Modules

  • gate.mjs — OWNERSHIP_INGEST_ENABLED must be the literal string true and the repository's internal uuid must appear in OWNERSHIP_CANARY_REPOSITORY_IDS. Every other state (absent/malformed/empty/not-listed) is disabled. No truthiness, wildcards, names, or logins.
  • queries.mjs — production-audited GraphQL builders promoted from the spike: history(path:) (metadata-only: committedDate + author{user{id login}} — no oid, no whole-commit stats), batched blame capped at the measured 50-alias ceiling (built but uncalled), and tree entries (name path type). assertOwnershipQuerySafe statically rejects forbidden fields (text/patch/message/email/name/committer/files/additions/deletions/…) and non-{id login} user selections.
  • scopes.mjs — path normalization (canonical /, traversal/malformed rejection, ≤200 chars), repo + directory scope candidates only (file paths never durable), sha256 scope hashes (constant repo sentinel, dir-hash domain separation, deliberately not rename-stable), in-memory exclusion of vendor/generated/build/dependency classes + submodule gitlinks by entry type, deterministic ≤4-depth / ≤500-dir cap with overflow returned for later cardinality_cap marking.
  • contributors.mjs — opaque repo-scoped sha256 contributor_key derived from repository id + GitHub node id (login renames don't move it; same person ≠ same key across repos), single unlinked bucket for author.user == null (never pseudonymized), other aggregate bucket, conservative [bot]-suffix heuristic, human-metric basis = linked ∧ !is_bot.
  • history-reducer.mjs — pure UTC-month aggregation: per-contributor touch_commits/touch_share, scope totals (total_touch_commits, human_contributor_count computed pre-fold over the full transient set, unlinked_touch_share), deterministic Top-25 + Other folding (touches desc, key asc), durable-identity minimization (overflow-only identities get no contributor candidate), numeric(5,4) share stability (clamped [0,1], no NaN/−0, no forced sum-to-1).

Safety envelope

  • Gate semantics tested exhaustively (flag literal-only, allowlist uuid-only, disabled ⇒ OwnershipDisabledError).
  • Dormancy guards evolved, not deleted: the core may exist, but nothing outside lib/ownership/ may import it, reference ownership tables, or read the gate env; the core itself may import only node: builtins + siblings (no db/octokit/supabase/fs), may not console-log, and may not contain persistence-shaped calls.
  • .env.example documents the gate off (OWNERSHIP_INGEST_ENABLED=false, empty allowlist); placeholder test now permits false but never true.
  • Internal spec status corrected: schema foundation is live, tables are empty, ingestion/UI are not, public trust record unamended.

Explicitly NOT in this PR

No ownership DB writes, no ownership_sync_state rows, no runners/callers, no cron/webhook/dashboard wiring, no OWNERSHIP_ENABLED, no HEAD-blame reducer or cursors, no UI, no GitHub permission changes, no production config changes. migrations/012_ownership.sql untouched.

Test plan

  • npm test — 608 tests, 605 pass, 0 fail, 3 skipped
  • npm run typecheck — pass
  • npm run build — pass
  • npm audit --audit-level=moderate — 0 vulnerabilities
  • npm run db:drill — PASS on Postgres 17 (docker, disposable)
  • Diff audit — changes limited to lib/ownership/*, ownership tests, .env.example, secret inventory metadata, internal spec status; no api/src/migrations/vercel.json changes
  • Production DB untouched — all verification local/disposable

Generated with Devin

@vercel

vercel Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
dev-ledger Ready Ready Preview Oct 18, 2026 4:50pm UTC

Deterministic, privacy-preserving ingestion core for Code Ownership
Over Time — pure modules under lib/ownership/ plus a hard-off gate.
No runtime wiring, no persistence, no production callers: production
keeps emitting exactly zero ownership rows.

- gate.mjs: OWNERSHIP_INGEST_ENABLED literal-true + internal-uuid
  canary allowlist; everything else disabled by construction
- queries.mjs: audited metadata-only GraphQL builders (history(path:),
  50-alias blame ceiling, tree entries) with a static field audit that
  rejects source/identity-creep selections
- scopes.mjs: path normalization, repo+dir scope candidates only
  (never file paths), in-memory vendor/generated/build/submodule
  exclusions, deterministic <=4-depth / <=500-scope cap
- contributors.mjs: repository-scoped sha256 contributor keys on the
  GraphQL node id, single unlinked bucket, conservative [bot] heuristic
- history-reducer.mjs: pure UTC-month aggregation — touch shares,
  pre-fold human counts, Top-25 + Other folding, durable-identity
  minimization, numeric(5,4) share stability

Dormancy tests evolved: the core may exist but nothing may call it,
write to ownership tables, or read the gate env outside gate.mjs.
.env.example documents the gate off-by-default; SecurityPrivacyPage
unchanged.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…uery

Review corrections for the ingestion core:

- classifyAuthorIdentity: 'unlinked' now means ONLY author.user == null.
  A present-but-malformed user object (missing/empty/wrong-typed id or
  login) throws OwnershipIdentityError carrying no identifying values —
  malformed provider data can never silently become unresolved
  attribution. The reducer propagates it with no partial result.
- scopes.mjs gains a conservative binary-blob classifier (images, A/V,
  archives, compiled objects, fonts, PDFs, binary documents). Directory
  candidates now derive ONLY from eligible descendant blobs: a dir of
  only binary or excluded blobs produces no scope; 'tree' entries alone
  create nothing; submodules still excluded by entry type.
- treeEntriesQuery removed — tree enumeration is measured only through
  the REST recursive git tree endpoint; the GraphQL route was not part
  of the spike evidence.
- spec §9.1: explicit persistence-handoff blocker for Phase 1C-B —
  no page-local folding, full-set human counts, no durable overflow
  identities, atomic cursor+aggregate checkpoints required before any
  ownership writes.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@Aliferous3
Aliferous3 merged commit 67e5065 into main Oct 18, 2026
5 checks passed

This branch was successfully deployed

1 active deployment
Preview — cb0a6fa1 Deployed Oct 18, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant