refactor(spec)!: api is code-only — withdraw a runtime create door the endpoint matcher could never read (#5488) - #7040
Conversation
…the matcher could never read (#5488) `DEFAULT_METADATA_TYPE_REGISTRY`'s `api` entry declared `allowRuntimeCreate: true` and the runtime never honoured it: `PUT /api/v1/meta/api/{name}` answered 200 "Saved" and the declared route then 404'd forever, with no `EXCLUDED` log line — the endpoint was never in the matcher's index at all. `matchEndpoint` reads `MetadataManager.listForIndex('api')` (the manager's registry plus its filesystem/memory loaders); a runtime write lands in `sys_metadata`, in neither. ADR-0049 remove side, per the maintainer ruling of 2026-08-07T16:59Z (Option B). The flip makes `api` code-only, so the existing #5086 inlet refuses with `NOT_CREATABLE` / 403 before persistence, in draft mode as well as active. Deliberate, recorded retirements that come with it: - `gateApiDraftsForPublish` (PR #5279) and its 9 tests — it gated a promotion into a state the matcher can never read, and no `api` draft can now exist. - the `api` create seed — a form whose save can only 403. - the decision block at metadata-plugin.zod.ts is rewritten as a RECORDED OVERTURN, keeping its three original bullets verbatim. The two #5271 tripwire pins are replaced by retirement pins; every rejection case asserts `code` AND `status`. The artifact route (`**/*.api.ts` → `publishPackage`) is untouched. Closes #5311 (subsumed per the 2026-08-09 triage designation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
… entry (#5488) `check:generated` proved exactly two artifacts stale — the two the new `api-runtime-create-withdrawn` SemanticMigration feeds. Regenerated with `check:generated --fix` (only the proven-stale ones, never the whole set). The four ratchets (api-surface, authorable-surface, json-schema.manifest, api-surface-signatures) are byte-identical, which is the CORRECT reading for this shape: the change is a boolean value in a platform constant array, not an enum narrowing and not a def deletion, so no authorable key and no export moved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Merge-lap onto current main. One conflict, in the shared ADR-0087 retirement registry (`packages/spec/src/migrations/registry.ts`): main's `import-run-automations-declared-default-corrected` (#6190) and this branch's `api-runtime-create-withdrawn` (#5488) both append a SemanticMigration into step 17's array, sharing the brace above and below. Resolved UNION-KEEP — neither entry may be lost (#6526). Recounted from the merged file: 43 step-17 semantic entries, with both #42 (`api-runtime-create-withdrawn`) and #43 (`import-run-automations-declared-default-corrected`) present. The two generated artifacts were NOT text-merged (the os-regen merge driver refuses to): `spec-changes.json` and `docs/protocol-upgrade-guide.md` were regenerated from the merged tree, and both entries are present in each. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
📓 Docs Drift CheckThis PR changes 2 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
#7040 (#5488) appended `api-runtime-create-withdrawn` to step17.semantic while this branch appended `plugin-manifest-loading-retired`; the conflict was purely additive and BOTH are kept. Recounted from the file (#6526): RETIRED_KEYS_BY_MAJOR[17] = 12 entries, RETIRED_DEFS_BY_MAJOR[17] = 45 — every sibling's registrations survive. Generated artifacts regenerated from the merged tree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
Fixes #5488
Closes #5311 (subsumed per the 2026-08-09 triage designation)
DEFAULT_METADATA_TYPE_REGISTRY'sapientry declaredallowRuntimeCreate: trueand theruntime never honoured it. This flips it to
false, which makesapicode-only, andlets the existing #5086 inlet refuse the write loudly. ADR-0049 remove side.
…and no
[EndpointMatcher] … EXCLUDEDline anywhere in the boot log: the endpoint was notgated out, it was never in the index at all.
IMetadataService.matchEndpoint→EndpointMatcher→MetadataManager.listForIndex('api')reads the manager's own registryplus its registered loaders (
["filesystem","memory"]on dev/serve). A runtime write landsin
sys_metadata, which is in neither.Ruling this implements (verbatim, 2026-08-07T16:59Z)
Lane and shape are the 2026-08-09T08:37:07Z triage designation:
domain:specowns it, onecoordinated PR, because a spec-first split is mechanically unsound — the inlet derives from
the spec constant, ignores
mode, and rejects drafts too.gateApiDraftsForPublish— a recorded overturn, not a silent deletegateApiDraftsForPublish(metadata-protocol/src/protocol.ts) landed two days beforethis PR, in PR #5279, as #5206 step 2 / ADR-0121's publish gate on
publishPackageDrafts. It is removed here on purpose, with its nine tests and thePUBLISH_DRAFTS_NAMESPACE_REMEDYstring only it appended.Why it can go: it judged whether an
apidraft was fit to be promoted draft→active insys_metadata. No such row is ever served — that is this issue. It was a correct verdictabout a state with no consumer.
Why it must go rather than sit unreached: with
apicode-only, the #5086 inlet refusesbefore persistence and before the draft/publish branch (it does not read
mode), so noapidraft can be authored at all and the gate could never again see one. Leaving it wouldleave unreachable code asserting a rule about a row that cannot exist — a phantom check.
The reasoning is recorded in place at the old call site, not only here. The three
bullets of the original "CODE-ONLY was considered and rejected" decision block in
metadata-plugin.zod.tsare likewise kept verbatim with what became of each, so thereversal is auditable rather than silently contradicted. Its second bullet predicted this
exact mechanism, and was correct.
What still judges endpoints, unchanged:
validateApiEndpointDeclarations/identityFreeEndpointGateFailureon the route that actually serves — the stack schema,publishPackage(#5189), and again at load inbuildEndpointIndex(PR #5203). ADR-0121's"publish REJECTS" ruling is intact on that route.
Full file surface (exception-path condition 1)
packages/spec/src/kernel/metadata-plugin.zod.tsapientry →allowRuntimeCreate: false; decision block rewritten as a RECORDED OVERTURN; ADR-0088 admission clause 2 correctedpackages/spec/src/kernel/metadata-type-api-registration.test.tsfilePatternspin so the 403 can prescribe)packages/spec/src/kernel/metadata-type-schemas.tsapientrypackages/spec/src/kernel/metadata-create-seeds.tsapicreate seed removed (a pre-filled form whose save can only 403)packages/spec/src/kernel/metadata-create-seeds.test.tsapijoinsKNOWN_UNSEEDED, oncapability's precedentpackages/spec/src/migrations/registry.tsSemanticMigrationapi-runtime-create-withdrawnin step 17packages/spec/spec-changes.json,docs/protocol-upgrade-guide.mdpackages/metadata-protocol/src/protocol.tsgateApiDraftsForPublishretired + call-site note; orphanedspec/apiimport and remedy constant removedpackages/metadata-protocol/src/protocol-publish-drafts-endpoint-gate.test.tscodeANDstatus)packages/metadata-protocol/src/protocol.code-only-types.test.tsapiprobe added; derived code-only set nowagent, api, capability, jobpackages/objectql/src/protocol-meta.test.tspackages/objectql/src/sys-metadata-repository.test.tspackages/objectql/src/metadata-validation-sweep.test.ts.changeset/api-runtime-create-withdrawn.mdThe declared two-package surface was short by one package
packages/objectqlderives from the same registry constant —sys-metadata-repository.tshas its own
RUNTIME_CREATE_ALLOWED_TYPES, andprotocol-meta.test.tspins theapiwrite door. Swept in the #6218 direction (by the RULE's consumers, not by the edited
package:
'…@objectstack/spec'+'…@objectstack/metadata-protocol'), which is how itsurfaced. This is inside the change's completion scope — those tests go red on the flag flip
alone — so they are fixed here rather than filed.
A fourth objectql site needed the opposite treatment:
metadata-validation-sweep.test.tssweeps
DEFAULT_METADATA_TYPE_REGISTRY.filter(e => e.allowRuntimeCreate), soapileavesthat suite silently and its fixture would have sat there looking like coverage while
asserting nothing. The fixture is removed deliberately, with the reason in place.
Measured blast radius vs the 2026-08-07 prediction
Prediction on record: 23 red — spec 2 + metadata-protocol 21. Re-measured by flipping
only the one-line flag on top of
origin/main:packages/specpackages/metadata-protocolgateApiDraftsForPublish's 9 + 12 auto-enrolled code-only casespackages/objectqlThe three objectql reds:
accepts a spec-valid api item,refuses a spec-INVALID api item with 422,put accepts statically-registered api with intent=runtime-only (#5271).Verification
All commands under the shared verification lock,
NODE_OPTIONS=--max-old-space-size=4096.pnpm --filter @objectstack/spec testpnpm --filter @objectstack/metadata-protocol testpnpm --filter @objectstack/objectql test@objectstack/metadata-protocolhad no
diston disk, so 56 files failed to resolve the package and 0 tests ran whilethe summary said "56 failed". That is the §9 stale/missing-artefact trap wearing the costume
of a real red. Re-run after building
'@objectstack/objectql^...'; the number above is fromthe valid run, and the script now asserts the dist is present before believing any result.
Reverse verification — direction predicted before running
Recorded in advance: ordinary RED, not one of the inverted shapes (#5046's
more-diagnostics, #5009's canonical-first inversion), because the new pins assert a refusal
that exists only while the flag is
false. Config C keeps the whole change and flips theflag back to
true:packages/spec— 2 red: both new retirement pins.packages/metadata-protocol— 6 red: all 5 new refusal pins + the derived code-onlyset assertion (
agent, api, capability, joblosesapi).Predicted set, predicted direction, no surprises.
Generated artifacts — expectation recorded before reading the instrument
This change is neither shape the retirement playbook tabulates: not an enum-value
narrowing, not a whole-def deletion. It is a boolean value in a platform constant array,
so no Zod def changes and no authorable key moves. Predicted: the four ratchets byte-identical,
and
spec-changes.json+ the upgrade guide the ONLY things that must move.check:generatedreported exactly 2 of 10 stale:Regenerated with
check:generated --fix(only the two proven stale, never the whole set).A zero reading on the four ratchets is the correct result for this shape, not a sign the
generators were skipped — the self-evidencing signal is the opposite one, and it fired.
ADR-0087 disposition
Registered as a
SemanticMigration, not aMetadataConversion: there is no authored sourceto rewrite.
allowRuntimeCreateis a platform registry value, and the artifact route isbyte-for-byte unaffected — what changed is a runtime HTTP verdict, the
BatchOptions.validateOnlydisposition.The earlier assessment checked
protocol.tsonly, and that part holds: #6973's +157 is theorg-scoped-write refusal path, disjoint from the inlet/gate regions here. But #6973 also
touches two files this PR touches:
packages/objectql/src/protocol-meta.test.ts— INTERSECTS. fix(metadata-protocol): refuse an org-scoped write of a type that has no per-org channel (#6190) #6973 edits the twoapicases at ~L1657-1690 (removing
organizationId: 'org_alpha'). Those are the exact twocases this PR rewrites to
403 NOT_CREATABLE.packages/metadata-protocol/src/protocol.code-only-types.test.ts— touched by both, butdisjoint hunks, and fix(metadata-protocol): refuse an org-scoped write of a type that has no per-org channel (#6190) #6973 does not touch
CODE_ONLY_TYPES.The intersection is benign and composable, in the same direction: #6973's own comment
says an org-scoped
apiwrite "is refused BEFORE the schema is consulted"; after this PRapiis refused regardless of org scope. Resolution on the merge lap keeps #6973's env-widespelling and this PR's 403 expectation. #6973 lands first; this PR merge-laps over it and
re-runs
metadata-protocol+objectqlon the merged tree before it goes ready.Notes
in
content/docs/**orskills/**ever taughtPUT /api/v1/meta/api/:name. Everyendpoint-authoring doc teaches
apis:on the stack artifact — the route that alwaysserved, and the route this change leaves untouched.
apirowsa deployment already has in
sys_metadatafrom/meta/apiand/openapi.json.deleteMetaItemstays ungated so those rows can be cleaned up.OS_METADATA_WRITABLE=apiremains the single operator escape hatch; it unlocks the write only — the endpoint
still will not be served, which is why it is a diagnostic and not a workaround.
Generated by Claude Code