Skip to content

[finding] the authorable-key deletion gate's reachability BFS reports integration/DataSyncConfig unreachable from the metadata-type roots — it is two hops from the connector root, so #4650 proof 2 waives a tombstone for a key a stack manifest really authors #17356

Description

@os-bill

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

  1. On a branch off main, delete the schedule key from DataSyncConfigSchema in packages/spec/src/integration/connector.zod.ts (no retiredKey(), no registry entry).
  2. Delete the line "integration/DataSyncConfig:schedule" from packages/spec/authorable-surface/integration.json.
  3. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions