What is wrong
computeSurfaceReachability() in packages/spec/scripts/build-schemas.ts is the whole of check (c) proof 2 (#4650, 2026-08-02 ruling): a deleted authorable-surface/ baseline line is legitimate when "the def is not reachable from the metadata-type roots (no metadata document is ever parsed by it), so its entry was over-collection and there is no author to tombstone for."
For integration/DataSyncConfig that answer is measurably false. The def sits two hops from a root the same file enumerates, and the runtime really does parse metadata documents against it.
The function's own docblock names this direction as the dangerous one:
Judged conservatively: one shared entry marks the whole def reachable — a false "reachable" demands a tombstone too many, a false "unreachable" would waive one silently.
This is that false "unreachable", in the tree, today.
The two readings, side by side
Reading A — what the gate says. pnpm --filter @objectstack/spec gen:schema on a branch that bare-deletes integration/DataSyncConfig:schedule, exit 0, printing:
ℹ️ 4 baseline deletion(s) since cca1dc0bfb1e carry their own proof (#4650):
- integration/DataSyncConfig:schedule — def not reachable from the 26 metadata-type roots
(BUILTIN_METADATA_TYPE_SCHEMAS + EXTRA_METADATA_TYPE_SCHEMAS overlay; BFS over this
build's in-memory Zod graph): an over-collected entry, never parsed against a
metadata document.
Reading B — what the graph actually holds. A tsx probe over the same modules the build imports, run with OS_EAGER_SCHEMAS=1 exactly as gen:schema does:
roots: 26
connector root === DeclarativeConnectorEntrySchema: true
connector root type tag: object
unwrap trail: object
shape keys sample: name,label,type,description,icon,authentication,provider,providerConfig
has syncConfig prop: true
syncConfig unwrap: optional -> object
syncConfig inner === DataSyncConfigSchema: true
So: getMetadataTypeSchema('connector') (an EXTRA_METADATA_TYPE_SCHEMAS entry, one of the 26 roots the message counts) resolves to an object whose syncConfig property, unwrapped through one optional, is the same instance as the exported DataSyncConfigSchema. reachableVia('integration/DataSyncConfig') should answer root-graph; it answers null.
Reading C — the runtime agrees with B, not A. Pin tests in packages/spec/src/cron-typed-positions-retirement.test.ts parse a connector through three doors and observe syncConfig being processed by that def each time: the bare ConnectorSchema, DeclarativeConnectorEntrySchema, and ObjectStackSchema with connectors: [...] — i.e. a real defineStack manifest, and PUT /api/v1/meta/connector/:name behind getMetadataTypeSchema. A metadata document IS parsed by this def.
Why it matters
Proof 2 exists to waive the tombstone requirement only where no author can receive a prescription. Where it answers falsely, the gate lets an authorable key be deleted from a non-strict schema with no tombstone and no ADR-0087 entry — which is precisely the silent-strip class ADR-0104 and #3733 name: the author keeps writing the key, gets a clean parse, and the setting never takes effect. Every gate stays green, repo-wide, for exactly the shape check (a) refuses in its own failure text.
The blast radius is not one key: any def reachable only through the same shape of edge is equally invisible, so the waiver is available to every future spec retirement that touches one.
Repro
- On a branch off
main, delete the schedule key from DataSyncConfigSchema in packages/spec/src/integration/connector.zod.ts (no retiredKey(), no registry entry).
- Delete the line
"integration/DataSyncConfig:schedule" from packages/spec/authorable-surface/integration.json.
pnpm --filter @objectstack/spec gen:schema — exits 0 and prints the proof-2 line quoted above.
Observed on objectstack-ai/objectstack at 1504dd1d77e9b0e0c53dd6fb4251c0874e1e5b7e (branch claude/issue-16320-retire-cron-typed-positions), against merge base cca1dc0bfb1e380442670843606d6a0c7762f6ba.
Where to look
packages/spec/scripts/build-schemas.ts:
computeSurfaceReachability() — the BFS, zodChildSchemas(node) and the bridged map;
zodByDefKey — whether the instance it holds for integration/DataSyncConfig is the instance the connector shape holds (the probe shows the EXPORTED schema is; the def-key map may hold another);
zodShapeOf(node) for the connector root and for the optional wrapper on syncConfig.
Not diagnosed here on purpose — two independent candidates (an edge the child-walk does not follow, or a def-key map keyed on a different instance) and only one of them is the fault. This card names the contradiction and the reading that settles it.
Not in this card's scope
⛔ Nothing here argues that the four deletions on the branch above should be reverted; that route is a maintainer ruling and stands on its own. What is asked here is that proof 2 stop answering null for a def a manifest parses.
Suggested acceptance
- a
--self-test case that fails when a def reachable through an optional-wrapped property of an EXTRA_METADATA_TYPE_SCHEMAS root reads null;
reachableVia('integration/DataSyncConfig') answers root-graph on main;
- the fix does not turn the conservatism around: a genuinely unreachable def still reads
null, pinned with its own case.
Generated by Claude Code
What is wrong
computeSurfaceReachability()inpackages/spec/scripts/build-schemas.tsis the whole of check (c) proof 2 (#4650, 2026-08-02 ruling): a deletedauthorable-surface/baseline line is legitimate when "the def is not reachable from the metadata-type roots (no metadata document is ever parsed by it), so its entry was over-collection and there is no author to tombstone for."For
integration/DataSyncConfigthat answer is measurably false. The def sits two hops from a root the same file enumerates, and the runtime really does parse metadata documents against it.The function's own docblock names this direction as the dangerous one:
This is that false "unreachable", in the tree, today.
The two readings, side by side
Reading A — what the gate says.
pnpm --filter @objectstack/spec gen:schemaon a branch that bare-deletesintegration/DataSyncConfig:schedule, exit 0, printing:Reading B — what the graph actually holds. A
tsxprobe over the same modules the build imports, run withOS_EAGER_SCHEMAS=1exactly asgen:schemadoes:So:
getMetadataTypeSchema('connector')(anEXTRA_METADATA_TYPE_SCHEMASentry, one of the 26 roots the message counts) resolves to an object whosesyncConfigproperty, unwrapped through oneoptional, is the same instance as the exportedDataSyncConfigSchema.reachableVia('integration/DataSyncConfig')should answerroot-graph; it answersnull.Reading C — the runtime agrees with B, not A. Pin tests in
packages/spec/src/cron-typed-positions-retirement.test.tsparse a connector through three doors and observesyncConfigbeing processed by that def each time: the bareConnectorSchema,DeclarativeConnectorEntrySchema, andObjectStackSchemawithconnectors: [...]— i.e. a realdefineStackmanifest, andPUT /api/v1/meta/connector/:namebehindgetMetadataTypeSchema. A metadata document IS parsed by this def.Why it matters
Proof 2 exists to waive the tombstone requirement only where no author can receive a prescription. Where it answers falsely, the gate lets an authorable key be deleted from a non-strict schema with no tombstone and no ADR-0087 entry — which is precisely the silent-strip class ADR-0104 and #3733 name: the author keeps writing the key, gets a clean parse, and the setting never takes effect. Every gate stays green, repo-wide, for exactly the shape check (a) refuses in its own failure text.
The blast radius is not one key: any def reachable only through the same shape of edge is equally invisible, so the waiver is available to every future spec retirement that touches one.
Repro
main, delete theschedulekey fromDataSyncConfigSchemainpackages/spec/src/integration/connector.zod.ts(noretiredKey(), no registry entry)."integration/DataSyncConfig:schedule"frompackages/spec/authorable-surface/integration.json.pnpm --filter @objectstack/spec gen:schema— exits 0 and prints the proof-2 line quoted above.Observed on
objectstack-ai/objectstackat1504dd1d77e9b0e0c53dd6fb4251c0874e1e5b7e(branchclaude/issue-16320-retire-cron-typed-positions), against merge basecca1dc0bfb1e380442670843606d6a0c7762f6ba.Where to look
packages/spec/scripts/build-schemas.ts:computeSurfaceReachability()— the BFS,zodChildSchemas(node)and thebridgedmap;zodByDefKey— whether the instance it holds forintegration/DataSyncConfigis the instance the connector shape holds (the probe shows the EXPORTED schema is; the def-key map may hold another);zodShapeOf(node)for theconnectorroot and for theoptionalwrapper onsyncConfig.Not diagnosed here on purpose — two independent candidates (an edge the child-walk does not follow, or a def-key map keyed on a different instance) and only one of them is the fault. This card names the contradiction and the reading that settles it.
Not in this card's scope
⛔ Nothing here argues that the four deletions on the branch above should be reverted; that route is a maintainer ruling and stands on its own. What is asked here is that proof 2 stop answering
nullfor a def a manifest parses.Suggested acceptance
--self-testcase that fails when a def reachable through anoptional-wrapped property of anEXTRA_METADATA_TYPE_SCHEMASroot readsnull;reachableVia('integration/DataSyncConfig')answersroot-graphonmain;null, pinned with its own case.Generated by Claude Code