You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
api.enableSearch and metadata.maskObjectFields are live REST config keys that no schema in packages/spec declares — read through as any, and stripped by any parse of their own config object #11983
Found while implementing #11637 (making the REST server parse config.api instead of casting to it). Filed, not fixed — the repair is a packages/spec declaration, which is domain:spec's single-owner surface and outside #11637's declared scope.
This is the mirror image of #11637: that card is a key the spec declares and the runtime never enforces; this is a key the runtime honours and the spec never declares.
What was measured
On origin/main @ 7899f5745.
RestServer.normalizeConfig reads two keys through as any because they have no declared seat:
enableOpenApiis declared (packages/spec/src/api/rest-server.zod.ts), so its as any is stale residue — harmless, but it is what makes the line beside it look equally safe.
enableSearch is not. git grep -c enableSearch origin/main -- packages/spec/src → zero hits (reverse-checked with enableDiscovery, 1 hit in the same file, and projectResolution, 3 hits across the directory). It is nonetheless live: NormalizedRestServerConfig.api.enableSearch is typed, and registerRoutes / the discovery capability block both read it, so enableSearch: false really does turn the search surface off today.
metadata.maskObjectFields is the same shape, and the code says so in place: "MetadataEndpointsConfigSchema lives in packages/spec and giving this key a declared seat there is a separate change" (ADR-0106 D8).
Why it is not merely cosmetic
RestApiConfigSchema is not .strict(), and a non-strict z.object()strips what it does not declare. Measured directly:
RestApiConfigSchema.parse({ version: 'v1', enableSearch: false })
→ the returned object has no `enableSearch` property at all
So the moment anything parses this config and consumes the result, a deployment that turned search off silently gets it back on — no error, no warning, nothing to grep. That is the ADR-0104 silent-strip class the tombstones in packages/spec/src/shared/retired-key.ts exist to prevent, and it is a live trap rather than a hypothetical: #11637 had to make its parse validation-only, discarding the parsed value and normalizing from the raw input, specifically to avoid tripping it. That workaround is a cost this undeclared key imposes on every future parse at that seam.
There is also the authoring face: RestApiConfig is z.input<typeof RestApiConfigSchema>, so a TypeScript author writing api: { enableSearch: false } against the declared type gets an excess-property error today and has to cast — which is why every in-repo caller passes as any.
Not prejudged
Declare both keys in RestApiConfigSchema / MetadataEndpointsConfigSchema with the defaults the runtime already applies (enableSearch: z.boolean().default(true), and the ADR-0106 D8 default for maskObjectFields), then delete the three as any reads. Declared = enforced, and the seam's parse can consume its own output.
Retire enableSearch under ADR-0049 enforce-or-remove if the deployment-wide search opt-out is not a capability worth keeping — it would need a tombstone, since deleting an honoured key silently is the failure above.
Either way enableOpenApi's stale as any should go with it: it is declared, and the cast is what makes its undeclared neighbour look normal.
Claim: PM loop round R8, wave 1
Session: session_012xGvxcwPRTJfA7RfjXEYA4
Branch: claude/issue-11983-declare-live-rest-config-keys
Worktree: objectstack-issue-11983
Domain: domain:spec
File surface: packages/spec/src/api/rest-server.zod.ts + the metadata-endpoints config schema in the same package (declare api.enableSearch, metadata.maskObjectFields with the runtime's current defaults), spec authorable baselines if gates demand, packages/rest/src/rest-server.ts (delete the three as any reads at :3065/:3066/:3101), config-shape tests, one changeset (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: claude-fable-5 — clause-② content limb (widens the declared config surface); dispatch-gates @ f93df4d: no path-derived tier mandate, clause-② suspect line flags packages/spec/src/api/rest-server.zod.ts ⇢ packages/spec/src/**
Clause-②: yes
Serial constraints cleared: in-flight #12573 (cli lane) holds packages/rest TEST files + test-typecheck-debt.json, and its dispatch order explicitly forbids its dev from editing rest-server.ts — surfaces disjoint by construction; this card in turn must not touch that ledger, its 12 listed test files, or src/rest.test.ts (fenced separately by open PR #12421). No spec-lane in-flight siblings. H17 on-hold trigger files (#8345/#8346: field.zod.ts / view.zod.ts / currency data) — no intersection.
Dev claim (executing seat): running as a mode:subagent dev inside PM session session_012xGvxcwPRTJfA7RfjXEYA4 (R8 wave 1).
Branch: claude/issue-11983-declare-live-rest-config-keys (pushed; this branch is my distinguishing identity)
Worktree: objectstack-issue-11983 @ base f93df4d
Scope per claim comment 5428472607: declare api.enableSearch + metadata.maskObjectFields in packages/spec with the runtime's current defaults, delete the three as any reads in packages/rest/src/rest-server.ts, config-shape tests, one changeset. Not touching packages/rest/test-typecheck-debt.json, its 12 listed test files, or packages/rest/src/rest.test.ts.
{
"issue": 11983,
"status": "done",
"branch": "claude/issue-11983-declare-live-rest-config-keys",
"pr": "https://github.com/objectstack-ai/objectstack/pull/12605",
"premise_still_valid": true,
"summary": "Declared api.enableSearch in RestApiConfigSchema (z.boolean().default(true), describe states the runtime meaning) and deleted all three as-any config reads in packages/rest normalizeConfig; NormalizedRestServerConfig.api.enableSearch is now a required boolean and the two now-dead '?? true' read sites were simplified. Premise partially overtaken: metadata.maskObjectFields was ALREADY declared on main (MetadataEndpointsConfigSchema, default true, ADR-0106 D8, with pin tests) before this run — only its rest-side cast was stale — so this PR declares one key, not two. The card's flip measurement is pinned: parse of enableSearch:false now RETURNS the key. Stale prose in the #11637 pin file and in assertDeclaredApiConfig's doc updated to the declared reality; the parse there stays validation-only for the omitted keys (requireAuth #3963, projectResolution #11999). Draft PR per Clause-2; no ready-flip, no auto-merge, no labels touched.",
"tests": "All at final head b3357c6e: spec build + check:generated all 14 artifact gates green (regenerated authorable-surface/api.json, authorable-defaults/api.json, references/api/rest-server.mdx via gen:docs); spec typecheck green (check:test-typecheck: OK); spec src/api/rest-server.test.ts 53 passed incl. the three new [#11983] pins (verbose listing captured); rest typecheck green (debt ledger unchanged, 13 files / 37 errors held); rest targeted tests (discovery-search-capability-agreement, rest-config-parse-not-cast, discovery-per-request-protocol, meta-object-fls) 123 passed; gates check:authz-resolver, check:route-envelope, check:spec-parsed-alias, check:nul-bytes, check:where-matcher + check:query-options-erasure (baseline verified against dfebfc8), check:engine-double-contract, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset all green, each read from the gate's own verdict line with exit captured before any pipe. Reverse A (predicted red): zod file reverted to origin/main from committed state, mutation confirmed on disk (grep count 0), exactly the 2 parse pins red / 51 green, restored from HEAD with empty git diff HEAD. Reverse B (predicted red): typed read mutated to enableSearchX (confirmed on disk), tsc red TS2551 suggesting enableSearch — proves rest reads the REBUILT spec d.ts — restored from HEAD, tree clean. No ablation-through-dist legs (spec tests import source; rest reads freshly rebuilt dist, proven by reverse B). Gate derivation: dispatch-gates.mjs --repo objectstack-ai/objectstack, no hand-fed paths, derived at my commit; dispatched families all run; full-farm convergence is CI's per dispatch.",
"mcp_calls": 7,
"gates_run": "spec check:generated (14/14), spec typecheck, rest typecheck, spec+rest targeted vitest, check:authz-resolver, check:route-envelope, check:spec-parsed-alias, check:nul-bytes, check:where-matcher, check:query-options-erasure, check:engine-double-contract, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset — all green at b3357c6e",
"files_changed": [".changeset/declare-live-rest-config-keys.md", "content/docs/references/api/rest-server.mdx", "packages/rest/src/rest-config-parse-not-cast.test.ts", "packages/rest/src/rest-server.ts", "packages/spec/authorable-defaults/api.json", "packages/spec/authorable-surface/api.json", "packages/spec/src/api/rest-server.test.ts", "packages/spec/src/api/rest-server.zod.ts"],
"deviations": ["maskObjectFields declaration found already landed on main — PR declares enableSearch only and deletes all three casts (premise half-overtaken, core defect still live)", "beyond the three named casts: tightened NormalizedRestServerConfig.api.enableSearch to required boolean, dropped the two now-dead '?? true' reads (enableSearch and enableOpenApi read sites), same file, zero behavior change", "edited packages/rest/src/rest-config-parse-not-cast.test.ts (prose/title only, assertion kept) — within the claimed config-shape-tests surface, not debt-fenced; declared here as a file-surface increment", "changeset bump-class precedent for the landed maskObjectFields declaration unfindable (history grafted to one squash commit) — patch used as dispatched", "rebased onto origin/main dfebfc8 before PR (4 upstream commits, no overlap, clean); union re-run at final head"],
"open_questions": [],
"out_of_scope_findings": []
}
ACCEPT — reviewer of record: spec seat R8, session_012xGvxcwPRTJfA7RfjXEYA4. PR: #12605 (draft, head b3357c6e).
Checked against the PR itself, not the report narrative: 8 files match the claim plus three declared increments (normalized enableSearch tightened to required boolean with the two dead ?? true reads dropped — behavior-neutral by construction since the default materializes in normalizeConfig; rest-config-parse-not-cast.test.ts prose-only edit keeps its assertion and is outside the #12573 fence; base rebased to dfebfc8). Spot-checks: the flip pin asserts hasOwnProperty on the parsed output (the card's exact measured trap, now inverted); the discovery capability block reads the same normalized field as the mount; authorable-surface/api.json + authorable-defaults/api.json each gain exactly one row; changeset is patch × 2 packages with the silent-strip history. Reverse verification A (schema revert → exactly 2 pins red) and B (typed-read mutation → TS2551 naming enableSearch, proving rest typechecks against the rebuilt spec d.ts) both ran in the predicted direction with disk-confirmed mutations and hash-proven restores. Premise deviation accepted as measured: maskObjectFields was already declared on main (its cast was stale residue) — the card's second half was landed by someone else, correctly not re-done.
Clause-② enqueue gate: path limb fires (packages/spec/src/api/rest-server.zod.ts); dispatch tier was claude-fable-5 = CONTRACT_REVIEW_TIER (dispatch-gates.mjs:3813), so the tier condition is satisfied. Landing path: ready → merge queue once EVERY check on the head is green (not just required); CI convergence is this seat's watch.
Activity
os-zhuang commentedon Aug 26, 2026
Claim: PM loop round R8, wave 1
Session:
session_012xGvxcwPRTJfA7RfjXEYA4Branch:
claude/issue-11983-declare-live-rest-config-keysWorktree:
objectstack-issue-11983Domain:
domain:specFile surface:
packages/spec/src/api/rest-server.zod.ts+ the metadata-endpoints config schema in the same package (declareapi.enableSearch,metadata.maskObjectFieldswith the runtime's current defaults), spec authorable baselines if gates demand,packages/rest/src/rest-server.ts(delete the threeas anyreads at :3065/:3066/:3101), config-shape tests, one changeset (stop on breach; explain in the report)Container & model: M,
mode:subagent,model: claude-fable-5— clause-② content limb (widens the declared config surface); dispatch-gates @f93df4d: no path-derived tier mandate, clause-② suspect line flagspackages/spec/src/api/rest-server.zod.ts⇢packages/spec/src/**Clause-②: yes
Serial constraints cleared: in-flight #12573 (cli lane) holds
packages/restTEST files +test-typecheck-debt.json, and its dispatch order explicitly forbids its dev from editingrest-server.ts— surfaces disjoint by construction; this card in turn must not touch that ledger, its 12 listed test files, orsrc/rest.test.ts(fenced separately by open PR #12421). No spec-lane in-flight siblings. H17 on-hold trigger files (#8345/#8346:field.zod.ts/view.zod.ts/ currency data) — no intersection.Generated by Claude Code
os-zhuang commentedon Aug 26, 2026
Dev claim (executing seat): running as a
mode:subagentdev inside PM sessionsession_012xGvxcwPRTJfA7RfjXEYA4(R8 wave 1).Branch:
claude/issue-11983-declare-live-rest-config-keys(pushed; this branch is my distinguishing identity)Worktree:
objectstack-issue-11983@ basef93df4dScope per claim comment 5428472607: declare
api.enableSearch+metadata.maskObjectFieldsinpackages/specwith the runtime's current defaults, delete the threeas anyreads inpackages/rest/src/rest-server.ts, config-shape tests, one changeset. Not touchingpackages/rest/test-typecheck-debt.json, its 12 listed test files, orpackages/rest/src/rest.test.ts.Generated by Claude Code
Generated by Claude Code
fix(spec,rest): declare api.enableSearch, delete the three as-any con…
os-zhuang commentedon Aug 26, 2026
os-dev-report
{ "issue": 11983, "status": "done", "branch": "claude/issue-11983-declare-live-rest-config-keys", "pr": "https://github.com/objectstack-ai/objectstack/pull/12605", "premise_still_valid": true, "summary": "Declared api.enableSearch in RestApiConfigSchema (z.boolean().default(true), describe states the runtime meaning) and deleted all three as-any config reads in packages/rest normalizeConfig; NormalizedRestServerConfig.api.enableSearch is now a required boolean and the two now-dead '?? true' read sites were simplified. Premise partially overtaken: metadata.maskObjectFields was ALREADY declared on main (MetadataEndpointsConfigSchema, default true, ADR-0106 D8, with pin tests) before this run — only its rest-side cast was stale — so this PR declares one key, not two. The card's flip measurement is pinned: parse of enableSearch:false now RETURNS the key. Stale prose in the #11637 pin file and in assertDeclaredApiConfig's doc updated to the declared reality; the parse there stays validation-only for the omitted keys (requireAuth #3963, projectResolution #11999). Draft PR per Clause-2; no ready-flip, no auto-merge, no labels touched.", "tests": "All at final head b3357c6e: spec build + check:generated all 14 artifact gates green (regenerated authorable-surface/api.json, authorable-defaults/api.json, references/api/rest-server.mdx via gen:docs); spec typecheck green (check:test-typecheck: OK); spec src/api/rest-server.test.ts 53 passed incl. the three new [#11983] pins (verbose listing captured); rest typecheck green (debt ledger unchanged, 13 files / 37 errors held); rest targeted tests (discovery-search-capability-agreement, rest-config-parse-not-cast, discovery-per-request-protocol, meta-object-fls) 123 passed; gates check:authz-resolver, check:route-envelope, check:spec-parsed-alias, check:nul-bytes, check:where-matcher + check:query-options-erasure (baseline verified against dfebfc8), check:engine-double-contract, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset all green, each read from the gate's own verdict line with exit captured before any pipe. Reverse A (predicted red): zod file reverted to origin/main from committed state, mutation confirmed on disk (grep count 0), exactly the 2 parse pins red / 51 green, restored from HEAD with empty git diff HEAD. Reverse B (predicted red): typed read mutated to enableSearchX (confirmed on disk), tsc red TS2551 suggesting enableSearch — proves rest reads the REBUILT spec d.ts — restored from HEAD, tree clean. No ablation-through-dist legs (spec tests import source; rest reads freshly rebuilt dist, proven by reverse B). Gate derivation: dispatch-gates.mjs --repo objectstack-ai/objectstack, no hand-fed paths, derived at my commit; dispatched families all run; full-farm convergence is CI's per dispatch.", "mcp_calls": 7, "gates_run": "spec check:generated (14/14), spec typecheck, rest typecheck, spec+rest targeted vitest, check:authz-resolver, check:route-envelope, check:spec-parsed-alias, check:nul-bytes, check:where-matcher, check:query-options-erasure, check:engine-double-contract, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset — all green at b3357c6e", "files_changed": [".changeset/declare-live-rest-config-keys.md", "content/docs/references/api/rest-server.mdx", "packages/rest/src/rest-config-parse-not-cast.test.ts", "packages/rest/src/rest-server.ts", "packages/spec/authorable-defaults/api.json", "packages/spec/authorable-surface/api.json", "packages/spec/src/api/rest-server.test.ts", "packages/spec/src/api/rest-server.zod.ts"], "deviations": ["maskObjectFields declaration found already landed on main — PR declares enableSearch only and deletes all three casts (premise half-overtaken, core defect still live)", "beyond the three named casts: tightened NormalizedRestServerConfig.api.enableSearch to required boolean, dropped the two now-dead '?? true' reads (enableSearch and enableOpenApi read sites), same file, zero behavior change", "edited packages/rest/src/rest-config-parse-not-cast.test.ts (prose/title only, assertion kept) — within the claimed config-shape-tests surface, not debt-fenced; declared here as a file-surface increment", "changeset bump-class precedent for the landed maskObjectFields declaration unfindable (history grafted to one squash commit) — patch used as dispatched", "rebased onto origin/main dfebfc8 before PR (4 upstream commits, no overlap, clean); union re-run at final head"], "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
os-zhuang commentedon Aug 26, 2026
ACCEPT — reviewer of record: spec seat R8,
session_012xGvxcwPRTJfA7RfjXEYA4. PR: #12605 (draft, headb3357c6e).Checked against the PR itself, not the report narrative: 8 files match the claim plus three declared increments (normalized
enableSearchtightened to required boolean with the two dead?? truereads dropped — behavior-neutral by construction since the default materializes innormalizeConfig;rest-config-parse-not-cast.test.tsprose-only edit keeps its assertion and is outside the #12573 fence; base rebased todfebfc8). Spot-checks: the flip pin assertshasOwnPropertyon the parsed output (the card's exact measured trap, now inverted); the discovery capability block reads the same normalized field as the mount;authorable-surface/api.json+authorable-defaults/api.jsoneach gain exactly one row; changeset is patch × 2 packages with the silent-strip history. Reverse verification A (schema revert → exactly 2 pins red) and B (typed-read mutation → TS2551 namingenableSearch, proving rest typechecks against the rebuilt spec d.ts) both ran in the predicted direction with disk-confirmed mutations and hash-proven restores. Premise deviation accepted as measured:maskObjectFieldswas already declared on main (its cast was stale residue) — the card's second half was landed by someone else, correctly not re-done.Clause-② enqueue gate: path limb fires (
packages/spec/src/api/rest-server.zod.ts); dispatch tier wasclaude-fable-5=CONTRACT_REVIEW_TIER(dispatch-gates.mjs:3813), so the tier condition is satisfied. Landing path: ready → merge queue once EVERY check on the head is green (not just required); CI convergence is this seat's watch.Generated by Claude Code
fix(spec,rest): declare api.enableSearch, delete the three as-any con…
fix(rest): consume the parsed `api` sub-config instead of discarding it
fix(rest): consume the parsed `api` sub-config instead of discarding it
fix(rest): consume the parsed `api` sub-config so `RestApiConfigSchem…
test(spec): the second api/ file group's test titles state each cited…