feat(rest): parse crud / metadata / batch / routes at construction and consume the parse (#11984) - #14395
Conversation
…ork in progress) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…ume the parse Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…rmalize-config-parse-siblings
…origin/main Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 4 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c2f44b5c755a046ab02c5afb75bfd81cf3556a69 && git checkout c2f44b5c755a046ab02c5afb75bfd81cf3556a69
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7da4cc2ba75d8a3a959a83a4a796ac7775bfb50b 02fd466d5cc024c162723015f73fff695ccfdede && git checkout -B drift-repro 7da4cc2ba75d8a3a959a83a4a796ac7775bfb50b && git merge --no-ff 02fd466d5cc024c162723015f73fff695ccfdede
node scripts/docs-audit/affected-docs.mjs --json 7da4cc2ba75d8a3a959a83a4a796ac7775bfb50b
|
…rmalize-config-parse-siblings
…n/main `content/docs/permissions/system-context.mdx` is a generated artifact anchored by source line numbers. `origin/main` added 24 lines to `packages/rest/src/rest-server.ts` and moved anchors in `objectql/src/engine.ts`, `plugin-auth`, `metadata-protocol` and `objectql/src/registry.ts`, so the plain merge left the page stale: `check:system-context-census` reported 16 problems over 145 anchors and 109 census sites (8 `site-without-a-row`, 6 `anchor-is-not-a-read-site`, 2 `ledger-row-unused` — all pure line rot). `pnpm gen:system-context-census` re-anchored 10 citations; the gate now reads `OK -- 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read`. This is the regeneration half of `scripts/pm/os-regen-merge.sh` (its step 4), discharging the deferral the pre-commit hook recorded for the merge commit. No source, test or changeset content is touched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WXyGTWPbbreqXow7Z2pZCk
…t review named (#11984) The isolated contract reviewer (comment 5507350669 §4) and the director's reconciliation adopt as required prose three refusals the changeset left under its generic "wrong type" bullet: an explicit `null` at a declared key (the cast-era `??` chain read it as absent and defaulted it; zod's `.default()` fills `undefined` only), a `crud.patterns` entry missing its required `path`, and a sub-object that is not an object at all. Every bullet is a measurement, not a transcription: a throwaway driver constructed the REAL `RestServer` with each shape and the refusal text quoted here is what it printed, with an empty config as the discriminating control (the driver is not committed). The partial-`operations` bullet and the Migration sentence now cite #14365, so the later `z.partialRecord` widening is traceable from the refusal it reverses. Changeset prose only — no source, test or other file changed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WXyGTWPbbreqXow7Z2pZCk
|
Landing provenance — director seat (summon #9, session Generated by Claude Code |
Fixes #11984
What
RestServer.normalizeConfignow runs the DECLARED contract of its four sibling sub-objects —crud,metadata,batch,routes— at construction, and builds the normalized config from the parsed output. #11637 parsedapialone (one narrowing in front of contract review rather than five); this is the other four, one narrowing later, on the same door.buildDeclaredSubConfigSchemas:apiwith itsrequireAuth.omit(), the four siblings whole) and oneparseDeclaredSubConfig(name, schema, value, rationale?).assertDeclaredApiConfignow delegates to it, keeping its exact message and theversion-only mount rationale.RestServerConfigSchema(whole-config) is deliberately NOT parsed, so theopenApi31retiredKey()tombstone stays unexecuted — the warn-and-ignore posture 把 public 从"全局开关的副产品"升级为声明式能力,然后删掉 api.requireAuth 开关 #3963 chose forapi.requireAuth, per the card's triage ruling. Pinned.normalizeConfigreads is one its schema declares (key diff empty for all four — table below), none carries a tombstone, so nothing a non-strict parse could strip is anything the runtime honours. Defaults are unchanged and now come from the schema alone.apikeepsRestApiConfigSchemaconstrainsapi.versionwith a regex the REST server never runs — the seam casts instead of parsing, soapi.version: ''is accepted and mounts the whole API at/api//#11637's validate-only shape (filed as [finding]RestServer.normalizeConfigstill discards the parsedapioutput — its??chain duplicatesRestApiConfigSchema's defaults key for key, and the validate-only reason has expired #14366).Consume / validate-only decision per sub-object (key diff)
normalizeConfigreadscrudoperations.{create,read,update,delete,list},patterns,dataPrefix,objectParamStylemetadataprefix,enableCache,cacheTtl,maskObjectFields,endpoints.{types,items,item,schema}maskObjectFieldssince #11983)batchmaxBatchSize,enableBatchEndpoint,operations.{createMany,updateMany,deleteMany,upsertMany},defaultAtomicroutesincludeObjects,excludeObjects,nameTransform,overridesNewly refused / deliberately kept
Refused (all pinned through the real
RestServerconstruction and, in §B, the real pluginstart()):batch.maxBatchSizeoutside1..1000or non-integer;routes.nameTransformandcrud.objectParamStyleoutside their enums; non-integermetadata.cacheTtl; any declared key with the wrong type;crud.patternskeyed by a non-CRUD operation or carrying a non-HTTPmethod; a PARTIALroutes.overrides.OBJECT.operations(zod 4 reads that enum-keyedz.recordwith a required value as exhaustive — the input type already demanded all five).Kept, because the schema declares no such rule: a negative
metadata.cacheTtl(.int()only — the card guessed this one wrong); unknown keys inside a sub-object (non-strictz.object()s strip, as before); the retiredopenApi31(ignore posture);apiexactly as #11637 / #12450 left it.Blast radius, measured per sub-object on
origin/main@08e49496f(re-checked after mergingd62f990a98)new RestServer(orcreateRestApiPlugin((277 sites). Scanning everycrud:/metadata:/batch:/routes:block in those files for the sub-object's own declared keys: 0 sites outsiderest-server.tsitself and this PR's pin file pass any of the four sub-objects. (routes: { data: '', ... }fixtures arediscovery.routespayloads; the 90metadata: {blocks are endpoint / plugin metadata.) Positive control:rest-server.ts's ownNormalizedRestServerConfigandnormalizeConfigblocks hit.packages/spec/src/api/rest-server.test.ts:382/386(maxBatchSize: 0/2000, the schema's own refusal pins) hits:maxBatchSize: 24 lines, 3 out-of-range literals — the two schema-test pins above (never construct a server) andtracing.test.ts:413(1024, a different schema's key).nameTransform: 6 lines, 0 unknown values.objectParamStyle: 6 lines, 0 unknown values.cacheTtl: 69 lines, 1 non-integer literal (packages/runtime/src/endpoint-policy.test.ts:331,30.7— the declarative endpoint policy'scacheTtl,plugin-rest-api.zod.ts/ endpoint schemas, notMetadataEndpointsConfig).@objectstack/restsuite on this change —168 files / 2828 tests passedatabb61564c6. No fixture changed.Red-first
New pin file
packages/rest/src/rest-sub-config-parse-not-cast.test.tsrun against the unmodified tree (08e49496f+ test file only):13 failed | 13 passed (26)— every §A refusal and both §B plugin rejections red, every §C/§D guard green. After the change:43 passed (43)across this file and #11637'srest-config-parse-not-cast.test.ts.Changeset
.changeset/rest-sub-configs-parsed-not-cast.md,@objectstack/rest: minor— the level #11637'srest-api-config-parsed-not-cast.mdand #12450'srest-project-resolution-parsed.mdset for this door (BREAKING accept-set tightening, refused shapes enumerated,adr-0087: not-required (no-migration-prescription)).needs:contract-reviewper the card's Clause-② ruling.Also in this diff
content/docs/permissions/system-context.mdx— the system-context census page is a GENERATED artifact anchored byrest-server.tsline numbers; inserting the schema table shifted them,check:system-context-censuswent red, and the page was regenerated (pnpm gen:system-context-census), once before and once after mergingorigin/main(the os-regen driver deferral discharged in its own commit). The second regeneration (741d9c2d9, after mergingorigin/main@00ff228fe0) had--fixREFUSE ZERO files — the sentencecheck-system-context-censusline 117 asks the repairing PR to state — while rewriting 21 anchors across 16 rows; re-running--fixfrom the merge commit's own page reproduces the committed page byte for byte (blob087009f37f), so the commit is a pure re-anchor and not a population change.Out of scope, filed
CrudEndpointsConfigSchema.patternsis an exhaustive enum-keyedz.record— the input type demands all five operations and a partial parses to explicitundefinedentries (wantsz.partialRecord) #14365 —CrudEndpointsConfigSchema.patterns/RouteGenerationConfigSchema.overrides.*.operationsare exhaustive enum-keyed records under zod 4 (type demands all five keys; a partialpatternsparses to explicitundefinedentries) — spec seat, wantsz.partialRecord.RestServer.normalizeConfigstill discards the parsedapioutput — its??chain duplicatesRestApiConfigSchema's defaults key for key, and the validate-only reason has expired #14366 —[finding]consume the parsedapioutput too; its??chain duplicates the schema's defaults key for key.RestServerConfigkeys are normalized byRestServerand read by nothing —routes.*entirely,crud.patterns/objectParamStyle,metadata.cacheTtl/endpoints.schema,batch.defaultAtomic/operations.upsertMany(ADR-0049 enforce-or-remove candidates) #14369 —[finding]ten declaredRestServerConfigkeys (all ofroutes.*,crud.patterns/objectParamStyle,metadata.cacheTtl/endpoints.schema,batch.defaultAtomic/operations.upsertMany) are normalized and read by nothing — ADR-0049 enforce-or-remove candidates.metadata.cacheTtl's missing lower bound is noted there.Verification
Code head
02fd466d5— the tree that PASSed contract review (4fe663ec90), plus a merge oforigin/main@00ff228fe0, the census regeneration (741d9c2d9) and one changeset-prose commit enumerating the three newly-refused shapes the review named. Nothing underpackages/has changed since4fe663ec90: the three later commits touch only the generated census page and.changeset/rest-sub-configs-parsed-not-cast.md.pnpm exec turbo run build --filter='@objectstack/rest^...'thenpnpm --filter @objectstack/rest typecheck(tsc +check:test-typecheck, ledger unchanged, no new debt, noas anyin the new file) — exit 0 ate9fddb90bb.RestApiConfigSchemaconstrainsapi.versionwith a regex the REST server never runs — the seam casts instead of parsing, soapi.version: ''is accepted and mounts the whole API at/api//#11637's file + the upstreamrest-data-create-address-unknown-key.test.ts—3 files / 45 tests passedate9fddb90bb.@objectstack/restsuite —168/168 files, 2828/2828 testsatabb61564c6(the merge brought nopackages/restsource change; its one new test is in the run above).pnpm lint— exit 0 atabb61564c6; the sweep at head is in the report comment (shared verify lock).node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsate9fddb90bb= 61 families; 56 exit 0; 5 NOT MEASURED locally by the gates' own prerequisite exits (check-test-completenessneeds a CI test log,check-half-statesneeds repo-scoped egress,check:skill-examples/check:dual-build-cjs-loads/check:type-check-debtneed the whole workspace built);--ranreconciliation: 61 derived, 61 run, 0 unrun.Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Generated by Claude Code