Skip to content

fix(objectql): MetadataFacade object writes now reach the map its reads use (#6725) - #7211

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-6725-facade-object-write-read
Aug 10, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-6725-facade-object-write-read

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #6725.

The defect

MetadataFacade.register('object', …) wrote through SchemaRegistry.registerItem, which stores into the generic metadata map. Every one of the facade's object reads resolves from objectContributors, which only registerObject populates:

member route reads
getObject(name) registry.getObject objectContributors
get('object', name), exists('object', name) registry.getItem → special-cases the object type back to getObject objectContributors
list('object'), listNames('object'), listObjects() registry.listItems → special-cases to getAllObjects objectContributors

So an object written through the public facade was readable back through none of them — register resolved successfully and every subsequent read answered undefined / [].

IMetadataService (@objectstack/spec/contracts) declares getObject(name) ≡ get('object', name), and its own conformance test round-trips a register('object', …) through both members. This was a shipped contract that could not work. Dormant in-tree only because nothing on main installs a MetadataFacade into the metadata slot — but the class is exported from this package's root and core entrypoints, so a downstream host that installs it got the split, including ObjectQL's own bridgeObjectsToMetadataService, whose "already registered?" probe would never answer and so would re-register the full object set on every boot.

Shape chosen, and the blast radius measured for it

The card priced three dispositions and pre-ruled none. Measurement:

  • Who calls facade.register('object', …) today? In-tree: only this package's own test file. new MetadataFacade(...) appears nowhere on main outside the two objectql test files. As published API: any host occupying the metadata slot — and the two in-tree occupants (MetadataManager, createMemoryMetadata) both round-trip correctly, so the facade is the odd one out, not the reference.
  • What does registerObject do that registerItem does not? System-field injection, better-auth apiMethods reconciliation, ADR-0079 primary-title designation, __search companion provisioning, the ADR-0029 single-owner guard, contributor merge + priority sort, merge-cache invalidation, and the _objectRevision bump.
  • Is routing objects through registerItem itself the bug? No — and this is the part that decides the shape. registerItem's docblock does say "non-object metadata", but SchemaRegistry.unregisterObject's header (A deleted runtime object is still served by SchemaRegistry.getObject — the registry heal reaches the metadata map but never objectContributors #6808) states the actual invariant: "a runtime-authored object is written into TWO places (metadata['object'] via registerItem and objectContributors via registerObject)". The one in-tree precedent for this exact write, MetadataProtocol.applyObjectRegistryMutation, does both, with packageId || 'sys_metadata'. The facade was performing half of a documented two-place write.

So: the write now performs both halves, rather than moving to one of them.

Rejected, with reasons:

Details that are load-bearing

  • The contributor gets a copy. applyProtection stamps _packageId / _provenance in place, and applySystemFields returns its input unchanged on the no-injection path (systemFields: false, managedBy: 'better-auth', sys_*). A shared reference would therefore have written a synthetic package id onto the generic-map entry — exactly what the provenance pin forbids. Pinned by a new test that registers a systemFields: false object, i.e. the aliasing path.
  • Provenance. A package-less object registers under the 'sys_metadata' sentinel with _provenance: 'org' — both of which getArtifactItem / isArtifactBacked exclude, so it cannot read as code-shipped (the cloud#970 misclassification). Without the explicit 'org', applyProtection would default the copy to _provenance: 'package'. An object carrying a real _packageId registers under it and keeps 'package'.
  • Ordering. The contributor write runs first, because it is the half that can refuse (ADR-0029 single-owner). A refused registration now writes nothing at all instead of re-opening the split from the other side.
  • Both spellings. 'object' and 'objects' are both special-cased on the read side, so both are covered on the write side.

What happened to the provenance test

"never invents a synthetic package id for object registrations" is unchanged and still in place, still reading (registry as any).metadata.get('object') directly. That direct read remains the right instrument: the pin is about the stored document, and the object reads answer the contributor copy — which now exists and deliberately does carry the sentinel. Reading it through get('object', …) would have silently retargeted the assertion. A comment on the test now says so, and a new sibling test pins the aliasing hazard the copy exists to prevent.

Scope note: unregister

unregister('object', name) now removes both halves too. This is not scope creep — without it the fix would have introduced #6808's shape from the other side: a removal that empties only the generic map leaves getObject, which the data plane dispatches on, serving a deleted object for the life of the process. registry.unregisterObject(name) (the #6808 verb) is idempotent and refuses, per ADR-0029, an object still extended by another package.

Behaviour changes a caller can observe

  • The six read members now answer a facade-registered object, with the runtime-effective shape the contract promises.
  • register('object', …) can now throw where it previously succeeded and did nothing: claiming an object another package owns is refused (ADR-0029).
  • unregister('object', …) can throw for an object still extended by another package.

Tests

packages/objectql/src/metadata-facade.test.ts — 11 new cases, 14 total: round-trip through getObject and get (reference-identical, with an anti-vacuity toBeDefined); the enumeration members; the plural spelling; the runtime-effective shape on a multi-tenant registry; sentinel-not-artifact and real-package-id provenance; idempotent re-registration; the ADR-0029 refusal writing nothing; removal from both places; idempotent removal of an absent object.

Reverse-verified — pre-fix metadata-facade.ts restored under the new tests, red/green predicted per case before running. See the comment below.

Refs #6725, #6505 / PR #6723, #6853, #6808, ADR-0010, ADR-0029.


Generated by Claude Code

…ds use (#6725)

`MetadataFacade.register('object', …)` wrote through
`SchemaRegistry.registerItem`, into the generic `metadata` map. Every one of
the facade's object reads resolves from `objectContributors`, which only
`registerObject` populates: `getObject` goes straight there; `get('object', …)`
and `exists` go via `registry.getItem`, which special-cases the object type back
to `getObject`; `list`/`listNames` go via `registry.listItems`, which
special-cases to `getAllObjects`. So an object written through the public facade
was readable back through none of them — `register` resolved and every read
answered `undefined`.

`IMetadataService` declares `getObject(name)` ≡ `get('object', name)` and its
own conformance test round-trips a `register('object', …)` through both members,
so this was a shipped contract that could not work. Dormant in-tree only because
nothing on `main` installs a `MetadataFacade` into the `metadata` slot.

The write now performs both halves of the two-place object write the registry
documents (`SchemaRegistry.unregisterObject`'s header; the in-tree precedent is
`MetadataProtocol.applyObjectRegistryMutation`): `registerObject` for the
contributor entry the reads resolve, plus the existing `registerItem` for the
stored document. Both type spellings are covered, since both are special-cased
on the read side.

The contributor gets a COPY: `applyProtection` stamps in place and
`applySystemFields` returns its input unchanged when there is nothing to inject,
so a shared reference would have leaked a synthetic package id onto the stored
document — what the "never invents a synthetic package id" pin forbids. That pin
keeps its direct read of the generic map, because the stored document is what it
was written to guard. A package-less object registers under the `'sys_metadata'`
sentinel with `_provenance: 'org'`, so it cannot read as code-shipped.

`unregister('object', …)` removes both halves too. Without that the fix would
have re-opened #6808 from the other side: a removal that empties only the
generic map leaves `getObject` — what the data plane dispatches on — serving a
deleted object for the life of the process.

Refs #6725, #6505, PR #6723, #6808, ADR-0010, ADR-0029.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141cZum72My2vskaQSoQ1tZ
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 2:49am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

15 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/objectql)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/system-context.mdx (via packages/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)

⛔ 1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Reverse-verification

Pre-fix packages/objectql/src/metadata-facade.ts restored from origin/main under the post-fix tests. Red/green predicted per case before running: 4 green — the three original provenance cases (they exercise the non-object path, or the stored document, neither of which the pre-fix source handles differently) and "unregistering an object nothing registered stays a no-op" (pre-fix unregisterItem warns and returns). 10 red — every round-trip case, the new aliasing pin (which dies on getObject(...) being undefined before it can compare anything), and the ADR-0029 case (pre-fix register resolves instead of throwing).

Measured:

 ❯ src/metadata-facade.test.ts (14 tests | 10 failed)
     × keeps the stored document unstamped even when the contributor copy is stamped
     × reads a registered object back through BOTH getObject and get
     × reads it back through the enumeration members too
     × closes the same split for the plural `objects` spelling
     × serves the runtime-effective object, as the contract says it does
     × registers a package-less object under the sentinel, not as an artifact
     × registers a package-stamped object under its own package id
     × re-registering the same object replaces it rather than accumulating owners
     × refuses to claim an object another package owns, and writes nothing
     × unregisters an object out of BOTH places it was written into
      Tests  10 failed | 4 passed (14)

Same 10, same 4. No deviations from the prediction. Fix restored → 14/14 pass.

Tests

  • packages/objectql full suite: 167 files, 2917 tests, all passing.
  • pnpm --filter @objectstack/objectql typecheck: clean.

Gates

Enumerated fresh from origin/main: 64 in lint.yml, 70 across all workflows. 48 of the 64 are root scripts; the other 16 run under pnpm --filter <pkg> (15 on @objectstack/spec, 1 on @objectstack/lint).

All 48 root gates: PASS. Three (check:app-nav-i18n, check:i18n, check:i18n-coverage) and check:type-check-debt first reported "PREREQUISITE NOT MET — the workspace packages are not built", which measures nothing; re-run after turbo run build --filter='./packages/*' --filter='./packages/*/*' (the same build lint.yml does before those steps) they pass. Notably green: check:meta-type-normalized, check:engine-double-contract, check:type-check-coverage, check:type-check-debt, check:empty-changeset, check:startup-registry-verdict, check:slot-lookup, check:service-providers, check:init-service-contract, check:tenant-chokepoint.

This diff does not reach packages/spec, so the spec-gate suite and the #6017 cross-seat declaration are not prerequisites here. Four spec gates were run anyway to rule out collateral and baseline drift — check:generated --reconcile-only, check:spec-changes, check:export-origins, check:api-surface — all PASS, so no generated artifact is stale against this base (origin/main @ 55da611).

One note on the enumeration: check:strictness-ledger and check:variant-docs appear in the lint.yml count only because they are named inside a comment there. Their real steps live in spec-liveness-check.yml, both @objectstack/spec-filtered. The count of 64 is right; two of its members are not lint.yml steps.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 10, 2026 03:10
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit 1507ba3 Aug 10, 2026
26 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6725-facade-object-write-read branch August 10, 2026 03:30
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…ip across every shipped implementation (objectstack-ai#7223) (objectstack-ai#7371)

`register(type, name, data)` and `get(type, name)` are the contract's first
two CRUD members, and the round-trip between them was exercised in exactly
one place — `packages/spec/src/contracts/metadata-service.test.ts`, against a
hand-rolled `Map`-of-`Map`s double written inside the test itself. No shipped
implementation was held to it, which is the hole objectstack-ai#6725 fell through: a shipped,
exported `IMetadataService` could not perform its own most basic round-trip
while the full objectql suite and all 64 `lint.yml` gates stayed green.

Adds `METADATA_ROUNDTRIP_CASES` (`@objectstack/spec/contracts`) — 15 cases,
one table, a thin driver per implementation, the shape
`data/filter-logic-conformance.ts` already uses for filter backends — plus the
two drivers that run it:

  - the contract's own reference double, in `packages/spec` (the dependency
    root, which can see no implementation);
  - every implementation this repo ships, in `packages/objectql` (the only
    package that can see all three at once): `MetadataManager` with and
    without a writable loader, `createMemoryMetadata`, `MetadataFacade`.

The pre-existing Map double in `metadata-service.test.ts` is untouched — it
pins the contract's type surface and its own inline round-trip, independent
of any implementation.

No shipped behaviour changes. Three cases get different answers from
`MetadataFacade` than from the other implementations and the reference double;
each is pinned as measured under a `// DIVERGENCE` marker and filed as its own
card rather than reconciled here.

Verified the suite is not vacuous by re-introducing the objectstack-ai#6725 split locally
(dropping the contributor write from `MetadataFacade.registerObjectBothPlaces`):
four rows go red, including the plain object round-trip.

Refs objectstack-ai#7223, objectstack-ai#6725, PR objectstack-ai#7211, objectstack-ai#6745.


Claude-Session: https://claude.ai/code/session_0193R6tMZqgrdFrCSnaogFc4

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…acts/ to the commits and ADRs that decided them (stage 1) (objectstack-ai#20326)

Part of objectstack-ai#20234
Clause-②: no

## What changed

This is stage 1 of the staged sweep, covering
`packages/spec/src/kernel/**` and `packages/spec/src/contracts/**` and
nothing else. Later stages cover the other areas, so this PR carries
`Part of`.

Every comment or docblock site in these two areas that cited a tracker
number answering 404 has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123): **160 sites on 151 lines in 45 files, covering 47
numbers (152 lines rewritten)**. Each rewritten line now cites the
object this repository controls that decided the matter:

- an ADR or ruling record where one exists;
- otherwise the commit in `origin/main` history that decided it.

Each line also says in its own words what that object decided. Where
nothing answers, the sentence keeps its reason in words and the number
is gone: that happened for four numbers.

Only comments changed. Every file keeps its line count (152 lines out,
152 in), so no line citation into these files moves. No code token moves
(see the guard below). String literals carrying a dead number are
tokens: 30 such sites are left as they were and listed below.

**No citation number is added.** Every tracker number on an added line
was already on the line it replaces. That includes the two PR numbers
now standing beside their shas as convenience links: PR objectstack-ai#6900 beside
`b5404f496`, and PR objectstack-ai#7211 beside `1507ba356`.

## Census: this stage's two areas, before and after

**Instrument.** This is PR objectstack-ai#20226's instrument: REST `GET
/repos/objectstack-ai/objectstack/issues/N` without following redirects,
over every distinct in-repo number cited in the two areas. The
population is:
- bare `#N`, `objectstack#N`, `framework#N`, and the `pre-#N` /
`post-#N` spellings, with N of 100 or more;
- excluding the ordinal heads the citation gate declares (Prime
Directive, `PD`, decision `batch`) and `summon`.

**Controls.** The lit controls were `objectstack-ai#16862`, `objectstack-ai#16847` and `objectstack-ai#17698`. The
dead controls were `objectstack-ai#16714`, `objectstack-ai#16715` and `objectstack-ai#16697`. They were probed at
the start, after every 100 numbers and at the end: 6 checkpoints per
run. They read 18 of 18 lit (200) and 18 of 18 dead (404) in both runs.

| reading | tree | numbers probed | 200 | 404 | 301 or other | dead
sites | kernel | contracts | lines | files | dead numbers |
|---|---|---|---|---|---|---|---|---|---|---|---|
| before | base `6a6a17b62`, probed 2026-09-27T20:14Z to 20:16Z | 445 |
398 | 47 | 0 | **190** | 106 | 84 | 180 | 45 | 47 |
| after | head (probe at `eda5c6b27`, 2026-09-27T21:24Z to 21:26Z;
source identical at the final head) | 415 | 398 | 17 | 0 | **30** | 18 |
12 | 29 | 14 | 17 |

**Before, by class.**
- 73 non-test docblock sites and 26 non-test line comments.
- 22 test docblock sites and 39 test line comments.
- 28 test string sites (describe and it titles, one assertion message).
- 2 non-test strings.

**After.** Only the 30 string sites remain. There are 0 comment sites.
The head probe found no number newly dead since the base probe.

PR objectstack-ai#20226's area table read kernel 105 and contracts 81 at an earlier
base. This census reads 106 and 84 because it also counts the `pre-#N` /
`post-#N` spelling: 3 dead sites.

## Per-number table

The site counts give comments rewritten and strings left. The kind
column says what each line now cites:
- commit: the commit whose diff made the decision the line describes
(read in each diff, not only in the subject);
- ADR: the recorded decision;
- dropped: the reason is kept in words and the number removed.

| number | sites / files | rewritten / left (string) | anchor | kind |
|---|---|---|---|---|
| `objectstack-ai#5970` | 2/1 | 2/0 | `97e7e3caa`: `ActionSchema.visible` gains the
boolean arm | commit |
| `objectstack-ai#6083` | 1/1 | 1/0 | ADR-0122 phase 2, `53068c130`: find and findOne
pinned to the parsed state | commit (ADR named) |
| `objectstack-ai#6206` | 12/5 | 12/0 | `d7e0b4212`: maintainer ruling 2026-08-07,
enforcement takes the full envelope with no per-site subset. One site
cites `8e13ca876`, the route half that restored the five dropped fields.
Two name the ruling in words where the same block already cites the sha
| commit |
| `objectstack-ai#6216` | 2/2 | 2/0 | `f586f1a89`: one ExecutionContext assembler,
closed field-set pin | commit |
| `objectstack-ai#6300` | 2/1 | 2/0 | `74155c735`: find and findOne accept the author
state | commit |
| `objectstack-ai#6361` | 1/1 | 1/0 | `90bbf2510`: notification-list `cursor` retired
on both halves | commit |
| `objectstack-ai#6362` | 3/2 | 3/0 | `b5404f496` (PR objectstack-ai#6900 beside it): `connector`
keeps the ADR-0010 envelope | commit |
| `objectstack-ai#6363` | 3/1 | 3/0 | `17d095413`: `unreadCount` counts the whole
inbox, not the window | commit |
| `objectstack-ai#6483` | 6/1 | 6/0 | ADR-0005 whitelist, executed by `ee58392e1`:
nine unratified `allowOrgOverride: true` rolled back | commit (ADR
named) |
| `objectstack-ai#6511` (a PR) | 5/1 | 5/0 | `d7e0b4212`, its squash commit | commit |
| `objectstack-ai#6523` | 7/3 | 6/1 | `aa4b90d9a`: sharing and approval enforcement
take the full ExecutionContext | commit |
| `objectstack-ai#6640` | 2/2 | 2/0 | `2ab1257c9`: `preserveAudit` is UPDATE-only,
with a loud INSERT warn | commit |
| `objectstack-ai#6723` (a PR) | 1/1 | 1/0 | `8ad609c69`, its squash commit:
getObject's declared answer | commit |
| `objectstack-ai#6725` | 6/2 | 4/2 | `1507ba356` (PR objectstack-ai#7211 beside it): facade object
writes reach the map its reads use | commit |
| `objectstack-ai#6745` | 2/1 | 2/0 | `7a5ef0008`: the getObject-equals-get
conformance pin | commit |
| `objectstack-ai#8715` | 3/3 | 3/0 | `2c86fe3ea`: the retirement pin form over
`export-origins/` | commit |
| `objectstack-ai#8794`, `objectstack-ai#8836` | 1/1 each | 1/0 each | `1850ebbb0`: measured the
filter-reuse invariant and pinned it | commit |
| `objectstack-ai#10194` | 7/2 | 6/1 | `2306a765c`: theme and analytics_cube bound at
the /meta door | commit |
| `objectstack-ai#10238` | 1/1 | 1/0 | none: whether cube authoring is live end to end
is still its own measurement (`559041d39` leaves it open) | dropped |
| `objectstack-ai#10338` | 2/1 | 1/1 | `d2619fd0c`: `ApiEndpoint.target` optional, the
publish gate holds the requirement | commit |
| `objectstack-ai#10485` | 5/2 | 5/0 | `35ad101bc`: `themes` carrier and ThemeSchema
retired | commit |
| `objectstack-ai#10627` | 3/2 | 3/0 | `be21955ba`, whose message records that
controlled census | commit |
| `objectstack-ai#10724` | 16/5 | 15/1 | `be21955ba`: nine dead `contributes` members
tombstoned | commit |
| `objectstack-ai#10726` | 6/3 | 4/2 | `bc56e1881`: `contributes.routes` retired,
ruled Option B | commit |
| `objectstack-ai#10812` | 2/2 | 2/0 | none: the cloud leg's clean close is kept as
its date (2026-08-24, which `be21955ba` records) | dropped |
| `objectstack-ai#11071` | 1/1 | 1/0 | `50fb191dc`: `os generate` file names derived
from the registry, with the parity pin | commit |
| `objectstack-ai#11330` | 2/1 | 1/1 | `a9ee98992`: trust-tier text states
publish-gate-only enforcement | commit |
| `objectstack-ai#11331` | 2/1 | 2/0 | none: an open "tracked on" pointer, and the
enforce leg is unbuilt. The lines now say so | dropped |
| `objectstack-ai#11332` | 9/2 | 8/1 | `dce5cd4f0`: three dead manifest containers
retired | commit |
| `objectstack-ai#11333` | 1/1 | 1/0 | `aaacf1d5c`: the commit that corrected the
permissions half | commit |
| `objectstack-ai#11350` | 1/1 | 1/0 | `ece4dad31`, which records the 2026-08-23
entry-nameability ruling | commit |
| `objectstack-ai#11504` | 1/1 | 1/0 | `f90e82024`: `FLOW_INPUT_SCHEMA_INVALID`
registered | commit |
| `objectstack-ai#11741` | 5/2 | 4/1 | `b706af987`: `SendEmailInput.organizationId` |
commit |
| `objectstack-ai#11846` | 11/3 | 7/4 | `0c2334f6c`: preview mode retired | commit |
| `objectstack-ai#12010` | 5/2 | 3/2 | none: that `ConnectionEngineLike` inventory is
described in words | dropped |
| `objectstack-ai#12165` | 1/1 | 1/0 | `b307bfd2a`: the glob-discovery disposition
recorded beside `filePatterns` | commit |
| `objectstack-ai#12248` | 19/4 | 15/4 | `8425c17cc`: the five ruled engine members
adopted, getObject typed | commit |
| `objectstack-ai#13135` | 9/7 | 8/1 | `9e0ba21a1`: paper customization protocol
retired. ADR-0126 section 6 wall 4 stays cited where the line had it |
commit (ADR named) |
| `objectstack-ai#13608` | 2/1 | 2/0 | `fc9ba76a5`: eligibility held at redemption |
commit |
| `objectstack-ai#14143` | 2/2 | 2/0 | `f19475c0a`: the handler-face
`ctx.recordLoadDenied` signal | commit |
| `objectstack-ai#14192` | 4/1 | 0/4 | none rewritten: test titles only | (strings) |
| `objectstack-ai#14722` | 2/1 | 2/0 | `23c72be3c`: pinned the measurement that
refuted that card's premise | commit |
| `objectstack-ai#16559` | 2/1 | 1/1 | `c7aca0dce`: ResumeFailureReport declared once
| commit |
| `objectstack-ai#16786` | 3/2 | 2/1 | `6059b29c0`: `updateById` declares its answer |
commit |
| `objectstack-ai#17147` | 5/2 | 3/2 | `aaacf1d5c`: the granted permission set is
registered and refuses nothing | commit |
| `objectstack-ai#18335` | 1/1 | 1/0 | ADR-0090 D10's 2026-09-16 note: an API key is a
credential | ADR |

Every cited sha resolves to exactly one commit, and every one is an
ancestor of `origin/main` (38 shas, each `merge-base --is-ancestor` exit
0).

## The 30 string sites left as tokens

- **Test titles and one assertion message (28 sites).**
- contracts: `data-engine.test.ts` (4), `objectql-engine.test.ts` (2),
`email-service.test.ts`, `resume-failure-report.pin.test.ts`,
`scoped-context.test.ts` and `sharing-service.test.ts` (1 each).
- kernel: `manifest-unknown-keys.test.ts` (4), `manifest.test.ts` (4),
`preview-mode-retirement.test.ts` (4, one of them an assertion message),
`plugin-runtime-tier-truthful-text.test.ts` (3), and 1 each in
`metadata-customization-retirement.test.ts`,
`metadata-type-api-registration.test.ts` and
`metadata-type-schemas.test.ts`.
- **Non-test strings (2 sites).** Two `why:` strings on rows of the
exported `METADATA_ROUNDTRIP_CASES` table in
`contracts/metadata-service-roundtrip-conformance.ts` (lines 194 and
202). They ship in the package as data. No runtime path prints them to
an author: both test drivers title each case by its `id`. So they are
not author-shown text in the form D sense, and they are not rewritten
here.

## Mechanical guard: no code token moves

The check is a comments-stripped token comparison, base `6a6a17b62`
against the head. It uses the TypeScript parser's leaf tokens, so
template literals are scanned in context, and it excludes JSDoc nodes.
It ran over all 45 touched `.ts` files.

- Real run: 60,827 base tokens, **0 files with a token change** (exit
0).
- Comment-insertion control: 0 changes, as expected (exit 0).
- Positive control (a declaration inserted): 1 file flagged (exit 1).
- Positive control (one digit changed inside a test-title string): 1
file flagged (exit 1).

## Changeset

This change ships bytes, so a `patch` changeset for `@objectstack/spec`
is included; it says only that provenance comments were re-anchored.

Measured on the built package: rewritten docblocks reach
`dist/**/*.d.ts`. For example, `8425c17cc`, `17d095413`, `aa4b90d9a` and
`fc9ba76a5` each appear in 1 declaration file, and the positive control,
a pre-existing `notification-service` docblock sentence, appears in
`dist/contracts/index.d.ts`. Rewritten comments also reach the bundled
`.js`: `be21955ba` appears in 20 files. The `src/**/*.zod.ts` sources
ship verbatim through `files[]`.

## Gates (head `ebbea0b6e`)

- **Citation judging pass, run as CI runs it:** `pnpm
check:issue-citations && node scripts/check-issue-citations.mjs` exits
0. It judged 23 citations: 21 resolve and 2 resolve as pull requests
(the two convenience links).
- **Doc authoring:** `pnpm check:doc-authoring` exits 0.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` derived 86 families, and all 86 exit
0. `--ran` reports 86 run, 0 NOT MEASURED, 0 unrun, and exits 0. Five of
them (`check:doc-formula-expressions`, `check:dual-build-cjs-loads`,
`check:i18n`, `check:lean-entry-closure`, `check:type-check-debt`) first
exited 3, PREREQUISITE NOT MET. They exited 0 after a full `turbo run
build` of `./packages/*` (71 tasks, exit 0, under the shared verify
lock).
- **Build, tests, typecheck:**
  - `pnpm --filter @objectstack/spec build` exits 0.
- `vitest run src/kernel src/contracts` in `packages/spec`: 99 files and
1,608 tests pass. That covers all 24 touched test files and every test
here that reads contract source text.
- `pnpm --filter @objectstack/spec typecheck` exits 0, including
`check:test-typecheck`.

## Acceptance notes

- **Base.** The branch is 5 commits behind `origin/main` (`4e0f72e8d`).
None of those commits touches `kernel/`, `contracts/` or any file here,
so there was no merge.
- **No author-shown string in scope.** Nothing in these two areas is
form D's; the migration-entry fields belong to objectstack-ai#20233.
- **Instrument note, not filed.** The citation gate reads `pre-#N` and
`post-#N` as a cross-repository qualifier (`pre-`), so it never judges
them. In these two areas that is 18 sites at base, 3 of them dead. This
census counted them by hand.
- **`framework#N` is NOT MEASURED as a repository.**
`objectstack-ai/framework` answers 403 to this session's REST gate. The
five numbers cited that way resolve 200 in this repository. None of them
is in the dead set.
- **Seat 2's pointer.** The 21 `objectstack-ai#13003` notes in
`packages/spec/liveness/permission.json` are outside this stage, as the
claim records.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…nd ADRs that decided them (stage 3 of objectstack-ai#20595) (objectstack-ai#21268)

Part of objectstack-ai#20595
Clause-②: no

## What changed

Stage 3 of the `domain:engine` lane of the dead-citation sweep:
`packages/objectql/**`, comment and docblock prose only, per the claim
(`5941871762`). Stages 1 and 2 (`packages/metadata-protocol`) landed as
`a7d9768ec` and `d150c3039`; objectstack-ai#20595 stays open for the next stage
(`driver-sql`).

Every comment or docblock site in the package that cited a tracker
number answering 404 is rewritten in ruling C+D's form C (record
`5749154545` on objectstack-ai#19123): the ADR when one records the decision,
otherwise the commit in this repository's history that made it. That is
**279 sites on 275 lines in 67 files, covering 70 numbers**:

- **136 census sites** (136 lines, 17 files under `src/`): the whole
`allocated-but-absent` population of the gate's own census in this
package at the base, the slash-joined `plugin.ts` `objectstack-ai#10629` from the
post-landing census (`5923084795`, now at `:1543`) included;
- **1 site in `vitest.config.ts`** (`:61`, `objectstack-ai#17853`): outside the census
glob, inside the claimed surface;
- **142 test-comment sites** (138 lines, 49 test files), which the
census defers. They carry 51 numbers: the census itself reads 36 of them
as dead elsewhere in the repository, and never judges the other 15 (they
stand only in test files here), which the board and a single read each
settle.

**Anchors: 66 numbers by commit, 4 by ADR, 0 by words alone.** 45
numbers reuse the anchor another lane or stage already measured for
them, 23 were measured here, and 2 are split between a reused and a
measured commit (see the table). Two depart from another lane's anchor
for a stated reason (`objectstack-ai#10629`, `objectstack-ai#10243`, under Wordings to check).

Only comments changed. Every file keeps its line count (275 lines out,
275 in, plus the changeset), so no line citation into any of them moves.
No code token moves (the guard below). **No citation number is added**:
on every changed line, the numbers on the new text are a subset of those
on the old, and the diff-scoped gate judged the 14 citations left on
changed lines: 13 resolve and 1 is a declared cross-repo reference.

**A `patch` changeset**: 54 of the 137 rewritten non-test lines are in
the published `dist` (the `.d.ts` keeps JSDoc on exported members, and
esbuild keeps some comments in the JS), and `dist` is not byte-identical
with the base text (see Changeset).

## Census: `objectql`, before and after

**Instrument (A1).** The gate's own `node
scripts/check-issue-citations.mjs --census --json`, read-only and
unchanged. The count is its `allocated-but-absent` findings under
`packages/objectql/`.

| reading | tree | board | whole-repo `allocated-but-absent` | sites |
lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `4727fcb22`, run 22:38:04Z to 22:41:42Z | enumerated,
191 pages, frontier objectstack-ai#21252, 19,073 records (newest number read before
and after the run: objectstack-ai#21252) | 592 | **136** | 136 | 17 | 50 |
| after | `8d6465457`, run 23:19:32Z to 23:22:54Z | enumerated, 191
pages, frontier objectstack-ai#21261, 19,082 records (newest before and after: objectstack-ai#21261)
| 456 | **0** | 0 | 0 | 0 |

The whole-repo drop is 136, and the two finding sets differ by exactly
the 136 rows of this package, removed; none was added. `resolves`
(34,638), `resolves-as-pull-request` (2,095) and `cross-repo-unjudged`
(1,144) did not move. The card's 135 was taken at `f11b5f20a2` with the
older extractor; the base here reads 136, the difference being
`plugin.ts` `objectstack-ai#10629`. The same census at the first base `d150c3039`
(before a fast-forward to `4727fcb22`, which touched no `objectql` file)
read the identical 136 rows. The head's only later commit is a merge of
`main` that touches no file under `packages/objectql` (`git diff
8d64654 1e08958 -- packages/objectql` is empty).

**Supplementary instrument, the whole package.** The census reads
neither test files nor strings nor files outside `src`. A second reading
runs the gate's own exported `extractCitations` (whole-file and
comment-prose projections) and `namesThisRepository` over every tracked
file in the package (430 `.ts`, 6 `.json`, 2 `.md`, `LICENSE`), and
classifies each citation with the gate's `classifyCitation` against one
board enumerated by the gate's `enumerateBoard` (191 pages, frontier
objectstack-ai#21252, 19,073 records, 22:43:50Z). Every one of the 70 numbers in the
population was then read on its own over the issues endpoint: **all 70
answer 404**, and the lit controls `objectstack-ai#5286` and `objectstack-ai#12624` answer 200.

| reading | citations | dead | src comment | test comment | test string
| changelog |
|---|---|---|---|---|---|---|
| before, `4727fcb22` | 9,338 | **388** | 137 | 142 | 43 | 66 |
| after, head | 9,059 | **109** | 0 | 0 | 43 | 66 |

`src comment` includes `vitest.config.ts`. The drop of 279 citations is
exactly the rewritten sites, and the live counts did not move (non-test
comment: 2,971 resolve, 80 as pull requests; test comment: 2,700 and
122). A third, raw reading (every `#` followed by 2 to 6 digits,
whatever surrounds it) counts 9,504 before and 9,225 after: the same
drop of 279.

**Comment ids.** Six distinct comment-id citations stand on 11 lines in
this package (`5237739551`, `5434929046`, `5791803339`, `5805782503`,
`5865053231`, `5865693155`). Each was read over the issue-comments
endpoint and each answers 200 (control `5941871762`, 200), so none is in
the population.

## Per-number table

`src` counts census sites (plus `vitest.config.ts` for `objectstack-ai#17853`), `test`
counts test-comment sites. Every sha below matches exactly one commit
(`git rev-parse --disambiguate`, count 1) and is an ancestor of the base
(`git merge-base --is-ancestor`, exit 0 for all 67 shas; the clone is
not shallow, 15,432 commits at the base). The message or the diff of
each one names the number it replaces, with one exception, `objectstack-ai#10629`,
explained under Wordings to check. Where a sentence credits a ruling, a
measurement or a note to the number, the anchor's own message or diff
carries it (checked per site; the ones that needed a reworded sentence
are listed below). `source` says whether another lane or stage already
used this anchor for this number (`reused`) or it was measured here
(`measured`).

| number | src | test | anchor | kind | source | what it decided |
|---|---|---|---|---|---|---|
| `objectstack-ai#6037` | 2 | 1 | `18189983d` | commit | reused | validate-only data
operation — DataProtocol.validateData |
| `objectstack-ai#6083` | 3 | 2 | `53068c130` | commit | reused | ADR-0122 phase 2 —
flip bare names to parsed semantics |
| `objectstack-ai#6241` | 0 | 1 | `83a3b1f2e` | commit | reused | normalize the
`:type` segment once per handler so the plural spelling cannot skip the
§6.7 audience gate |
| `objectstack-ai#6300` | 5 | 4 | `74155c735` | commit | reused |
IDataEngine.find/findOne accept the author state — engine fills
SortNode.order's declared default |
| `objectstack-ai#6311` | 0 | 1 | `59b794f71` | commit | measured | narrow
`HookContext.api` from `z.unknown()` to the minimal `IScopedContext` |
| `objectstack-ai#6478` | 0 | 1 | `474f131cf` | commit | reused | roll `flow`'s
`allowOrgOverride` back to `false` per ADR-0005's original call, the
write path refusing loudly |
| `objectstack-ai#6483` | 0 | 9 | `ee58392e1` | commit | reused | enforce the ADR-0005
whitelist: nine unratified `allowOrgOverride: true` flags rolled back to
`false` (its message records the 2026-08-08 ruling) |
| `objectstack-ai#6573` | 5 | 6 | `708431313` | commit | measured | `registerHook`
refuses an empty `object` target and a self-cancelling scope |
| `objectstack-ai#6723` | 0 | 4 | `8ad609c69` | commit | reused | declare what
IMetadataService.getObject answers with |
| `objectstack-ai#6725` | 3 | 7 | `1507ba356` | commit | reused | MetadataFacade
object writes now reach the map its reads use |
| `objectstack-ai#6745` | 0 | 4 | `7a5ef0008` | commit | reused | pin getObject(n) =
get('object', n) across all three IMetadataService implementations |
| `objectstack-ai#8454` | 1 | 0 | `427344c26` | commit | measured | the object catalog
loses to an explicitly-set scalar |
| `objectstack-ai#8460` | 7 | 0 | ADR-0029 D9.2a | ADR | reused | ADR-0029 D9.2a, the
2026-08-13 amendment: an extender's scalar yields to a diverged base (it
names the number; executed as `01a7337fc`) |
| `objectstack-ai#8648` | 1 | 4 | `e5eeb499c` | commit | reused | pin the SEARCH-axis
remedy agreement, and correct the three comments that claimed
word-identity |
| `objectstack-ai#8672` | 6 | 4 | `ff08691e6` | commit | measured | a system-context
insert resolves the install's organization, or is refused — the runtime
producer of the autonumber fork |
| `objectstack-ai#8818` | 0 | 1 | `fd6bdf89f` | commit | reused | saveMetaItem's
missing-item refusal declares 400 INVALID_REQUEST instead of answering
500 |
| `objectstack-ai#8823` | 10 | 4 | `4dfa369a9` | commit | measured | drop the caller
value MySQL inlines in its duplicate-entry diagnostic |
| `objectstack-ai#9030` | 2 | 1 | `27a567dd8` | commit | measured | teach the
internal-leak predicate MySQL's three error templates |
| `objectstack-ai#10062` | 2 | 0 | `fa5d137ab` | commit | reused | gate undeclared
workspace imports in published src |
| `objectstack-ai#10091` | 1 | 0 | `da891e0ef` | commit | reused | gate sys_attachment
beforeUpdate with the uploader-or-parent-editor rule |
| `objectstack-ai#10165` | 2 | 1 | `801296050` | commit | reused | lifecycle
ttl.onlyWhen row filter with the canonical null predicate |
| `objectstack-ai#10194` | 0 | 3 | `2306a765c` | commit | reused | validate theme /
analytics_cube at the /meta write door via UNREGISTERED_KIND_SCHEMAS |
| `objectstack-ai#10243` | 1 | 1 | ADR-0126 §7.2 | ADR | measured | ADR-0126 §7.2: the
durable ledger row replaces the process-local `flowEnabled` map,
retiring the env-wide toggle leak's mechanism (it names the number) |
| `objectstack-ai#10290` | 4 | 1 | `2570ab05c` | commit | measured | the primary key
is never a `__search` companion source |
| `objectstack-ai#10347` | 5 | 3 | `530c1df65` | commit | reused | the Archiver
honours a declared `ttl` instead of archiving by `created_at` age alone
|
| `objectstack-ai#10485` | 0 | 2 | `35ad101bc` | commit | reused | retire the `themes`
carrier key and ThemeSchema — `app.branding` is the one colour surface |
| `objectstack-ai#10527` | 1 | 0 | `5649efbf9` | commit | reused | refuse a diverging
retention + ttl + archive lifecycle triple at parse time |
| `objectstack-ai#10528` | 4 | 1 | `7d483e1e5` | commit | measured | the Archiver
resolves its window through P4 governance |
| `objectstack-ai#10629` | 1 | 2 | `199ec4712` | commit | measured | bind federated
objects whatever the boot order, and report what could not be bound |
| `objectstack-ai#10643` | 1 | 0 | `5649efbf9` | commit | measured | refuse a
diverging retention + ttl + archive lifecycle triple at parse time |
| `objectstack-ai#10729` | 1 | 1 | `10485009a` | commit | measured | log a contributed
kind by its declared `id` |
| `objectstack-ai#11065` | 1 | 0 | `20950404c` | commit | reused | count a boolean
aggregand as 1/0 in avg and sum |
| `objectstack-ai#11311` | 1 | 0 | `1272f0a6b` | commit | reused | promote
resolveRecordOrganizationField to the shared platform-row resolver:
approvals and automation runs stamp the subject record's organization |
| `objectstack-ai#11427` | 5 | 0 | `c3c72a4bc` | commit | reused | hydrate a
tombstoned sys_file that still has a live holder |
| `objectstack-ai#11674` | 0 | 4 | `9a884c6e4` + `1cba33f16` | commit | reused | seed
pass 2 writes back by the internal id captured at insert time, healing
keyless datasets / warn at load time when a seed defers a required
column, and document the ordering constraint at the four pointer-pair
sites |
| `objectstack-ai#12194` | 0 | 2 | `311433f6b` | commit | reused | Declare the
metadata item-name grammar in spec and refuse it loudly at the publish
door |
| `objectstack-ai#13178` | 2 | 1 | `f087c376f` + `e49d98896` | commit |
reused+measured | scope the sys_file / sys_upload_session update and
delete doors to the acting organization / cut the tenant-audit control's
scope by the object's tenancy, not the caller's flag |
| `objectstack-ai#13197` | 5 | 11 | `56c093c4d` | commit | reused | enforce
field-level `unique` so a colliding write is refused, not landed |
| `objectstack-ai#13273` | 1 | 4 | `3a86a65e7` | commit | reused | pick the `find`
failure log level from the cause — "the table is not provisioned yet" is
not "the read failed" |
| `objectstack-ai#13644` | 3 | 2 | `34ce8e7db` | commit | reused | declare
ctx.referentialFieldClear on HookContextSchema, populate every set_null
cleanup write, and carry it across the QuickJS sandbox boundary |
| `objectstack-ai#13657` | 4 | 4 | `b003cf2e8` | commit | reused | Refuse an
undeclared field a before-hook writes — the post-hook half of the
declared-field door, one envelope on every driver |
| `objectstack-ai#14163` | 0 | 1 | ADR-0130 D3 + `1dcb995f2` | ADR | measured |
ADR-0130 D3: the gate relaxation and the object-name uniqueness check
are one change; `1dcb995f2` landed it, and its changeset names the
number |
| `objectstack-ai#14345` | 0 | 1 | `e89fa9233` | commit | measured | declare
aggregate? on IDataDriver with the signature the engine calls |
| `objectstack-ai#14390` | 6 | 1 | `9d7f7259f` | commit | reused | `update` answers a
driver unique violation with the `DUPLICATE_RECORD` envelope, on every
driver |
| `objectstack-ai#14399` | 3 | 8 | `3c1bbd2a8` | commit | measured | derive a view
container's object through the shared helper, so the row's own `name` is
LAST at every SOURCE registrar |
| `objectstack-ai#14422` | 0 | 1 | `dc7c226b9` | commit | reused | give the
standalone-action owner-key ladder one spelling |
| `objectstack-ai#14423` | 11 | 2 | `a56baa2bd` | commit | reused | the action audit
reads the store key and asks the plane by name, and listNames gains
loadMany fault parity |
| `objectstack-ai#14472` | 1 | 1 | `00ff228fe` | commit | measured | decide the
insert-side runtime-owned strip by hook-write provenance |
| `objectstack-ai#14474` | 1 | 3 | `df657d9df` | commit | reused | carry an ADR-0112
envelope on the install-time namespace conflict refusal |
| `objectstack-ai#14484` | 1 | 0 | `3f64fe6c6` | commit | reused | stamp
organization_id on every sys_record_share write, backfill the stranded
rows, admit the object to the tenancy ledger |
| `objectstack-ai#14535` | 0 | 1 | `1aba3159a` | commit | measured | declare the
recorded-by fixture's lookup with the canonical `reference` key |
| `objectstack-ai#14666` | 1 | 6 | `d0ee598e6` | commit | measured | refuse a view
container whose `name` disagrees with its derived object key |
| `objectstack-ai#14667` | 0 | 1 | `dc7c226b9` | commit | reused | give the
standalone-action owner-key ladder one spelling |
| `objectstack-ai#14680` | 1 | 1 | `3bd9b3498` | commit | measured | a leaf
`/view-container` subpath keeps objectql's lean ADR-0076 closure free of
the manager, chokidar, glob and js-yaml |
| `objectstack-ai#14683` | 0 | 4 | `96326040f` | commit | reused | apply the
allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are
scoped per type |
| `objectstack-ai#14723` | 1 | 0 | `65846bc46` | commit | reused | a batch/import ROW
reports a unique-constraint refusal as `UNIQUE_VIOLATION`, the route's
one wire spelling |
| `objectstack-ai#14770` | 0 | 6 | `d5cbb44f3` | commit | reused | gate
`getMetaItem`'s overlay read on the metadata registry |
| `objectstack-ai#14878` | 0 | 2 | `29db3cd2a` | commit | reused | widen the
deleted-member absence pin from one file to the tree |
| `objectstack-ai#14957` | 1 | 0 | `26144c204` | commit | measured | Derive and gate
the platform-object tenancy census |
| `objectstack-ai#15041` | 2 | 1 | ADR-0104, 2026-09-05 addendum | ADR | reused |
ADR-0104's 2026-09-05 addendum: the media column holds the bare
`sys_file` id; its execution order puts the driver card at step 2 (it
names the number) |
| `objectstack-ai#15094` | 1 | 0 | `901773b21` | commit | measured |
check-react-page-adapter-contract names its class by shape, not by the
records spelling, and re-anchors its citation |
| `objectstack-ai#16608` | 4 | 0 | `a016f08b8` | commit | reused | evaluate the
insert-side RLS `check` on the row that will be stored, after
`beforeInsert` |
| `objectstack-ai#16711` | 1 | 0 | `7862fb711` | commit | measured | object-definition
parameters declare the keys they are read for, plus a gate that sees
subclass overrides |
| `objectstack-ai#16729` | 1 | 0 | `0f38ab084` | commit | measured | an explicit
tenancy opt-out survives a partial `syncSchema` re-registration |
| `objectstack-ai#16783` | 1 | 1 | `854639b31` | commit | reused | `findOne`, `update`
and `delete` declare what they answer, and their hook seams are guarded
|
| `objectstack-ai#16786` | 3 | 2 | `5c8f5af50` + `6059b29c0` | commit |
measured+reused | `ObjectRepository` declares the `findOne` / `update`
shapes it already published / declare
IScopedObjectRepository.updateById's answer — the record or null, not
any |
| `objectstack-ai#16805` | 2 | 0 | `a016f08b8` | commit | reused | evaluate the
insert-side RLS `check` on the row that will be stored, after
`beforeInsert` |
| `objectstack-ai#17195` | 1 | 0 | `d2c1d1980` | commit | reused | beforeUpdate
receives the persist image; the caller submission moves to ctx.submitted
|
| `objectstack-ai#17219` | 3 | 2 | `706ad0fcc` | commit | reused | name the withheld
read-only key when a hook faults reaching through it |
| `objectstack-ai#17853` | 1 | 0 | `08f5f0e5a` | commit | reused | make a vitest
filter that selects no test file say so |

## Wordings to check

Most rewrites swap a tag in place (`[#N]` to `[commit SHA]`, `(#N)` to
`(commit SHA)`, `#N's X` to `commit SHA's X`, `PR #N` to its squash
commit), the form the landed stages use. These say more than the tag:

- **`objectstack-ai#10629`, three sites** (`plugin.ts:1543`,
`skip-schema-sync-registers-object-metadata.test.ts:22`, `:153`): 「the
same ruling objectstack-ai#7737/objectstack-ai#10629 made for federated objects」 became 「the same
ruling objectstack-ai#7737 made for federated objects (commit 199ec47)」. `199ec4712`
is objectstack-ai#7737's fix, and its message states the ruling the sites paraphrase:
`OS_SKIP_SCHEMA_SYNC` is a DDL flag while the federated binding is
DDL-free, and the binding is reconciled on `kernel:ready`. Its message
and diff name objectstack-ai#7737, **not objectstack-ai#10629**: the only commit that names objectstack-ai#10629
as its own (`13a6cb4ad`, the runtime lane's anchor for it) is an
expected-log-noise capture, a different subject, and `a037f7cbd`, which
wrote the 「objectstack-ai#7737/objectstack-ai#10629 ruling」 phrase, records nothing objectstack-ai#10629 added. So
the dead number is dropped and the live objectstack-ai#7737 carries the citation,
beside the commit that decided it. The same pair stands in `driver-sql`
(`sql-driver.ts`), the next stage.
- **`objectstack-ai#10243`, two sites** (`action-activation.ts:185`,
`action-activation.test.ts:23`): 「the objectstack-ai#10243 mechanism ADR-0126 retires」
became 「the env-wide toggle leak's mechanism ADR-0126 §7.2 retires」.
ADR-0126 §7.2 names that mechanism (the process-local `flowEnabled` map)
and the number (「the objectstack-ai#10243 leak's mechanism」), so ruling C's ADR rung
applies; the dogfood lane anchored a similar sentence to `02b41232d`,
the measurement commit.
- **`objectstack-ai#8672`, ten sites**: objectstack-ai#8672 was an observation that no commit fixed.
`ff08691e6` is the first in-repo record of its reasoning: its diff
quotes 「an org-less row is defensible for `sys_permission_set`」 and
names objectstack-ai#8672 five times. So 「objectstack-ai#8672's reasoning」 became 「commit
ff08691's (recorded) reasoning」, and 「objectstack-ai#8672 measured this primitive」
(`system-write-organization.test.ts:347`) became 「The card commit
ff08691 cites measured this primitive」.
`system-write-organization.test.ts:117` quotes what the file used to
read, 「platform namespace ⇒ deliberately org-less (objectstack-ai#8672)」; the quoted
number is elided to 「(…)」 rather than re-spelled, so the quote stays
true.
- **`objectstack-ai#13178`'s census figure** (`engine.ts:5450`,
`tenancy-by-object-classification.test.ts:26`): 「objectstack-ai#13178 census measured
… 135 of 175」 became 「census cited in commit e49d988's message
measured … 135 of 175」. That message carries the figure; see Acceptance
notes for what `4ecafc78b` records about it.
`platform-object-tenancy.ts:144` (the `sys_upload_session` update
writer) takes `f087c376f`, the service-storage lane's anchor.
- **`objectstack-ai#15094`** (`find-hook-result-shape.ts:30`): 「the ~70 … normalizer
limbs the objectstack-ai#15094 census counted」 became 「… limbs a census counted
(commit 901773b records its band)」. `901773b21` wrote the header of
`scripts/check-react-page-adapter-contract.mjs`, which records that
census (104 blocks at `ca46f8f12`) and a re-measure band; it does not
restate 「~70」, so the sentence points at the band and claims nothing
more.
- **`objectstack-ai#16805`** (`engine.ts:13014`, `:13218`): 「the contract review of PR
objectstack-ai#16805 measured」 became 「the contract review commit a016f08 records
measured」. `a016f08b8` is that pull request's squash, and its message
records the review's finding.
- **`objectstack-ai#16786`**: the `objectql` half (`engine.ts:18230`, `:18264`, two
tests) takes `5c8f5af50`, the commit that declared `ObjectRepository`'s
`findOne` / `update` shapes; `engine.ts:18272`'s 「stays open on objectstack-ai#16786」
became 「(its spec half: commit 6059b29)」, the spec lane's anchor,
since that half has landed.
- **`objectstack-ai#14163`** (`registry-ownership-refusal-envelope.test.ts:11`):
`(ADR-0130 D3, commit 1dcb995)`. The cited thing is the install-time
object-name check; ADR-0130 D3 decides it, and `1dcb995f2` landed it
with a changeset naming objectstack-ai#14163.
- **`objectstack-ai#8460`** (seven `registry.ts` sites): ADR-0029 D9.2a, the
2026-08-13 amendment, which names objectstack-ai#8460; the dogfood and spec lanes used
the same. `:2508`'s 「every shape objectstack-ai#8460 measured」 became 「every shape the
ADR-0029 D9.2a amendment records」.
- **`objectstack-ai#15041`** (`engine.ts:7164`, `:9860`, one test): 「the ruling on
objectstack-ai#15041 step 2」 became 「sequencing step 2 of ADR-0104's 2026-09-05
addendum」, the cli lane's spelling of the same record.
- **`objectstack-ai#6241`** (`metadata-service-roundtrip-conformance.test.ts:41`):
「the gate's header carries objectstack-ai#3984/objectstack-ai#5881/objectstack-ai#6241」 became 「… carries objectstack-ai#3984,
objectstack-ai#5881 and the third bypass, fixed in commit 83a3b1f」, because that
header (`scripts/`, another lane's surface) still carries the number
itself.
- **Tense, where the anchor is past**: 「the phantom read objectstack-ai#14770 removes」
became 「… commit d5cbb44 removed」 (`protocol-meta.test.ts:100`) and
「the resurrection objectstack-ai#14683 closes」 became 「… commit 9632604 closed」
(`:169`), as stage 1 wrote it. `:181`'s backticked `` `objectstack-ai#14770` `` became
plain 「commit d5cbb44」; its message records the four raw-org callers
the line says it measured.
- **Card antecedents**: 「the whole subject of objectstack-ai#14423」 became 「the whole
subject of the card commit a56baa2 closed」
(`action-governance.ts:406`); 「the C4 cell objectstack-ai#14423's ruling left open」
became 「the C4 cell commit a56baa2 left open」
(`plugin-governance-scoped-metadata.test.ts:5`; that commit pins C4 as
「a BOUNDARY, not a defect」); 「route 3 of objectstack-ai#8648」 became 「route 3 of the
card commit e5eeb49 fixed」
(`query-expression-conformance.test.ts:1070`, a line that commit wrote);
「filed objectstack-ai#6573」 / 「objectstack-ai#6573's ruling」 became 「filed the card commit 7084313
closed」 / 「The decision commit 7084313 records」
(`hook-exclude-objects.test.ts:501`, `:502`); 「(objectstack-ai#11674, the card's
"Second, NOT measured" question)」 became 「(the "Second, NOT measured"
question on the card commit 9a884c6 fixed)」; 「measurement (objectstack-ai#13644)」
became 「measurement (on the card commit 34ce8e7 closed)」.
- **Other single rewrites**: 「and objectstack-ai#6573 is why」 became 「and commit
7084313 says why」 (`engine.ts:2433`; its message gives the reason);
「question at this line (objectstack-ai#10527), since decided」 became 「question at this
line, since decided by that commit」 (`lifecycle-service.ts:1294`, where
`:1293` now cites `5649efbf9`); 「objectstack-ai#14680 is what they cost」 became 「the
leak commit 3bd9b34 closed is what they cost」
(`core-boundary.ratchet.test.ts:52`); 「The objectstack-ai#10165 acceptance criterion」
became 「The acceptance criterion behind commit 8012960」.
- **Reference lists**
(`metadata-service-getobject-equivalence.test.ts:51`,
`metadata-service-roundtrip-conformance.test.ts:68`,
`metadata-facade.test.ts:101`): the dead numbers became their commits,
in the spec lane's 「commits SHA (what), …」 form; the live numbers in
those lists stay, `PR objectstack-ai#7211` beside its `1507ba356`.
- No line was reflowed, so some are longer than their block's wrap
(`eslint.config.mjs` declares no line-length rule, and a reflow would
move neighbouring lines and every line citation into the file).

## Sites left

- **In comments (src, test, `vitest.config.ts`): none.**
- **String literals: 43 test-string sites, 25 numbers, 23 files**
(describe and `it` titles, assertion arguments): `objectstack-ai#14422` 4, `objectstack-ai#13273` 3,
`objectstack-ai#13657` 3, `objectstack-ai#17219` 3, `objectstack-ai#11674` 3, `objectstack-ai#6573` 3, `objectstack-ai#14423` 2, `objectstack-ai#10165` 2,
`objectstack-ai#14535` 2, `objectstack-ai#10290` 2, `objectstack-ai#8672` 2, and 14 more once each. Every one of
the 25 is in this stage's population, so the table above holds an anchor
for each. Non-test strings carry none. Strings are outside this stage's
surface.
- **Outside `src`:** the release-owned `CHANGELOG.md` names dead numbers
on 66 sites (44 numbers); left. `test-typecheck-debt.json`,
`tsconfig.test.json` and `tsconfig.scripts.json` cite only live numbers.

## Mechanical guard: no code token moves

The guard (stage 2's) compares, base `4727fcb22` against the working
tree, over all 67 touched files, with TypeScript 6.0.3:

- **Reading 1**: the parser's leaf nodes, from a `forEachChild` walk.
Comments are trivia there, and JSDoc is never visited.
- **Reading 2**: the full token stream in parser context, from a
`getChildren` walk, JSDoc nodes skipped. String, template and numeric
literals are compared in full on both readings.

Results:

- Real run at the head: 298,707 base tokens, **0 files with a token
change** (exit 0).
- Comment control (「The defaulting」 to 「The DEFAULTING」 on
`engine.ts:5`): 0 files changed (exit 0).
- Positive control, an identifier (`ARCHIVE_BATCH_SIZE` to
`ARCHIVE_BATCH_SIZEX`, `lifecycle-service.ts`): DIFFER on both readings
(exit 1).
- Positive control, a string literal (`'[value redacted]'` to `'[value
redactedX]'`, `driver-fault-redaction.ts`): DIFFER on both readings
(exit 1).
- Positive control, a numeric literal (`ARCHIVE_MAX_BATCHES_PER_SWEEP =
20` to `21`): DIFFER on both readings (exit 1).

Each mutation went through `scripts/ablation-replace.mjs` (wrap mode)
under a shell trap that restores by absolute path from `HEAD`. Each
landed (anchor 1 to 0, blob changed), and each restore was proven equal
to its `HEAD` blob (`71f6c9268aeb`, `34b1dd7989b8`, `9594cdd593c8`),
with `git diff HEAD` empty and a clean tree afterwards.

## Changeset: `patch` (`dist` measured)

`files[]` is `dist`, `README.md` and `CHANGELOG.md`, and the package is
not private. The dependency closure was built first (`turbo run build
--filter='@objectstack/objectql^...'`, 13 tasks). Then the package's own
`build` (tsup plus `check-dts-emitted`) ran three times in one script
under the shared verify lock (VERDICT command-exit 0):

- **Leg 1**, at `9d6a0a8d6`: 14 `dist` files hashed. Of the 137
rewritten non-test lines, 54 appear verbatim in `dist`: 25 from
`engine.ts`, 7 from `action-governance.ts`, 5 from
`platform-object-tenancy.ts`, and 17 from ten other files; in
`index.d.ts` / `index.d.mts`, the shared `util-*.d.ts` chunk, and
`index.js` / `index.mjs` / `core.js` / `core.mjs`.
- **Leg 2**, the base text put back in the 18 non-test files (18 of 18
proven equal to their base blob): 10 of the 14 files differ from leg 1
(`core` and `index` in `.d.ts`, `.d.mts`, `.js`, `.mjs`, and the
`util-*` chunk's two `.d` files).
- **Leg 3**, after the proven restore (18 of 18 equal to their `HEAD`
blob, `git diff HEAD` empty): all 14 files are byte-identical to leg 1,
so the build is deterministic and the difference is the rewrite.

So the rewrite ships, and
`.changeset/20595-objectql-provenance-anchors.md` declares a `patch` for
`@objectstack/objectql`, comment text only, with the claim's `Clause-②:
no` line.

## Gates (head `1e0895870d`)

- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at `1e0895870d` (68 paths against
merge base `8dea55d31`; no stale-tree warning) derived 68 commands. All
68 ran, each exit code captured before any pipe: 68 exit 0. `--ran`
reports 「68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN」 (a derived zero)
and exits 0. A full `turbo run build` over `./packages/*` and
`./packages/*/*` ran first under the verify lock (71 of 71 tasks,
VERDICT command-exit 0), so no gate read an unbuilt workspace.
- **Named in the dispatch:** `node scripts/check-issue-citations.mjs`
exits 0 (「every citation this change adds resolves (or is a declared
cross-repo reference)」, 14 judged across 17 files); `pnpm
check:issue-citations` exits 0 (self-test); `pnpm check:doc-authoring`
exits 0 (the sibling-package prose-id baseline holds, no growth); `pnpm
check:nul-bytes` exits 0, and a raw scan of the 68 changed files for
control bytes finds none.
- **Tests and typecheck, under the verify lock, at `1e0895870d`:** `pnpm
--filter @objectstack/objectql test`: 360 test files and 7,082 tests
pass. `pnpm --filter @objectstack/objectql typecheck` exits 0; its
`check:test-typecheck` step compiles all 361 tracked test files under
`tsconfig.test.json` (`tsc --listFiles`), the ledger holding (40 files,
234 errors, 65 pinned signatures).
- **Lint, as a proven narrowing:** eslint with inline config disabled,
over the 67 touched `.ts` files plus `dist/index.js` as the control: 68
results, 0 errors and 1 warning, the control's ignore notice; none of
the 67 is reported ignored. `eslint.config.mjs` never enables type-aware
linting (its lines 327-328 say so), so a comment edit cannot move the
verdict on an untouched file. The repo-wide `pnpm lint` is CI's run.

## Acceptance notes

- **Base.** The branch was cut at `d150c3039`, fast-forwarded to
`4727fcb22` before any edit (both census readings of this package
agree), and merged with `main` once at `8dea55d31` before the gates;
that merge touched no file under `packages/objectql` and neither
`check-issue-citations.mjs` nor `dispatch-gates.mjs`. Tests, typecheck
and gates ran on the merged head.
- **The `135 of 175` figure.** `engine.ts:5450` and
`tenancy-by-object-classification.test.ts:26` state that census figure
as measured. `4ecafc78b`, which re-derived the tenant-audit census as an
in-tree artifact after objectstack-ai#13178 became unreachable, records that 「the
135/77% "silenced by the isSystem guard" figure has no surviving
corroboration and is not reproduced」. This stage moves the citation to
the commit whose message carries the figure and leaves the claim as
written; whether those two sentences should say so is outside a citation
sweep.
- **A board enumeration came back short, at exit 0.** One run of the
gate's own `enumerateBoard`, between two full ones, returned 91 pages,
9,000 records and frontier objectstack-ai#9389, and raised nothing; the runs either
side of it read 191 pages and frontier objectstack-ai#21252. Its only guard is a
zero-record check. This stage discarded that reading and re-enumerated;
nothing above rests on it. Noted only.
- **Wording only:** no line without a number was changed, except the
lines whose antecedent was the number itself, listed above.

---
_Generated by [Claude
Code](https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants