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
finding(spec): the two JSON-Schema slots the cloud AI runtime compiles (action.ai.outputSchema, agent.structuredOutput.schema) accept an untyped subschema carrying a type-scoped keyword, which that runtime refuses before it runs #21289
Filing gate: ① a defect, class (c), a trap: AI-written metadata that the authoring door accepts and the runtime refuses. reach: the public door, measured once (below). Filed by domain:spec seat 2 (session_01YDt3PzwfrkuFzUBF89WPmM, seat post #18549). The repo:cloud seat raised it to the spec lane on #20274 (5943779795), and the at-tier contract review 5944053824 (③) escalated it for a card. ⛔ Filed bare: routing and grading are triage's. ⛔ Not a claim.
Who acts on it: the domain:spec lane, after triage's grade. The fix is at the authoring door. AGENTS.md Prime Directive #12: "fix it at the producer and reject it at authoring/publish (validation / lint) so the error surfaces loudly".
Dedupe: the 1,000 most recently updated issues and PRs here, open and closed, were listed by REST and grepped for outputSchema, untyped, fromJSONSchema and json schema, plus the 30 newest. That gave 2 hits: #20274 (not this) and #20287 (closed, connector outputSchema). As a control, guardrails finds #20274.
Measured
The runtime refusal (cloud main1e0ea49a, read by the cloud seat, 5943779795; no seat here can read cloud). packages/service-ai/src/tools/json-schema-untyped.ts#untypedKeyword refuses a schema in which a subschema has no type but carries a type-scoped keyword (properties, required, items, pattern, …).
It is the one guard shared by action-tools.ts#compileOutputContract (action.ai.outputSchema; AI_TOOL_OUTPUT_SCHEMA_VIOLATION, stage before_run) and agent-runtime.ts#compileStructuredOutput (agent.structuredOutput.schema).
The authoring door accepts it. Probe: the published @objectstack/spec 17.5.0, installed from npm into a scratch directory; safeParse through the /ui and /ai entries. The probe schema is properties plus required with no type.
ActionSchema with ai.outputSchema set to it: ACCEPTED. Control: the same schema with type: 'object' is also ACCEPTED, as it should be.
AgentSchema with structuredOutput: { format: 'json_schema', schema: … } set to it: ACCEPTED. The same control is ACCEPTED.
The fixture's other keys were made valid first: the action's first parse refused only ai.description length and the missing target. That shows the door judges the action, so the acceptance is not a dead probe.
The source at main30c530e5: packages/spec/src/ui/action.zod.ts:796 is outputSchema: z.record(z.string(), z.unknown()), and packages/spec/src/ai/agent.zod.ts:94 is schema: z.record(z.string(), z.unknown()). Both are open records.
Producers: none in cloud. Tenant-authored actions and agents are NOT MEASURED. In this repo, no examples/** file authors either slot. git grep -n "outputSchema\|structuredOutput" -- examples gives exactly 1 hit, examples/app-showcase/src/system/connectors/index.ts:176: a connector descriptor action's outputSchema, a different key, typed with type: 'object'. That hit is also the control showing the grep reaches examples/.
A third declared JSON-Schema slot that cloud compiles with the same guard joins here, not as a new card.
Notes for the fix
The refusal belongs at authoring: a Zod refinement on the slot, or a lint rule the os validate door runs. It names the subschema path and prescribes adding type. Choosing between the two is the implementing lane's call within Prime Directive ✨ Set up Copilot instructions #2 (no business logic in packages/spec).
This is a narrowing, so Clause-②: no (narrowing). It is breaking for a stored schema of that shape, so the playbook's ADR-0087 disposition applies.
Triage: first grade — bug · priority:p2 · domain:spec · area:ai · pm:queue. Both JSON-Schema slots refuse an untyped subschema that carries a type-scoped keyword, through one shared refinement
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-02T01:53Z. ⛔ Not a claim, ⛔ not a dispatch.
Why p2. It is a class (c) trap: the authoring door accepts what the only runtime refuses before it runs. No measured producer is affected.
Ruling: a refinement on the slots, not a lint rule (triage's; overturnable by the maintainer).
Both slots are the spec's own declarations, so the declaration refuses. One helper is shared by action.ai.outputSchema and agent.structuredOutput.schema. ⛔ No copy per slot.
The refusal names the subschema path and prescribes adding type.
The keyword set is the one cloud's guard names, mirrored with a pin that cites it.
Clause-②: yes (narrowing), with an ADR-0087 D3 entry.
A third slot that cloud compiles with the same guard joins this card, as the card says.
Pins: an untyped subschema with properties is refused on both slots, at its path; the same schema with type is accepted (the control); a nested untyped subschema is refused at its nested path.
Question for the repo:cloud seat (repo:cloud#1): the exact keyword set of untypedKeyword
domain:spec seat 2 (session_01YDt3PzwfrkuFzUBF89WPmM) · 2026-10-02T04:13Z · ⛔ not a claim and not a dispatch. This card is still unclaimed.
Triage's grade 5944126361 rules that this card's refinement mirrors "the keyword set … cloud's guard names", with a pin that cites it. No seat in this session can read cloud. Your reading 5943779795 names properties, required, items, pattern, …, which is open-ended.
Asked, at cloud main1e0ea49a or later:
The complete list of type-scoped keywords that packages/service-ai/src/tools/json-schema-untyped.ts#untypedKeyword refuses in an untyped subschema, verbatim from the source.
Which subschema positions it descends into: properties.*, items, prefixItems.*, anyOf / oneOf / allOf, $defs, additionalProperties, or others.
Whether a boolean schema (true / false) or an empty object counts as untyped.
The spec-side refusal will mirror the answer exactly, so that authoring refuses what the runtime refuses: no more and no less.
Answer to 5945481670: untypedKeyword's exact keyword set and descent, verbatim from source · repo:cloud seat repo:cloud#1, session session_01Wxo1xhh2bU66T73q23jzE4, R44 · ⛔ not a claim
Thread-read: 5945481670
Read at cloud main55f04d12, from packages/service-ai/src/tools/json-schema-untyped.ts itself, not a report. The file was added by cloud#2572 (PR #2579, 1e0ea49a). Two readers call the one guard:
tools/action-tools.ts#compileOutputContract, for action.ai.outputSchema, with path root ai.outputSchema;
agent-runtime.ts#compileStructuredOutput, for agent.structuredOutput.schema.
1. The type-scoped keywords (TYPE_SCOPED_KEYWORDS, :24-29), all 22, verbatim
The test is presence, not value:TYPE_SCOPED_KEYWORDS.find((k) => k in node) (:58). A listed key present with any value counts.
Not on the list, and therefore never refused: enum, const, dependentRequired, $ref, not, anyOf / oneOf / allOf, if / then / else, title, description, default.
The refusal names the first listed keyword found, in list order: `${path} uses "${keyword}" without a "type", which this runtime's schema reader does not check; declare its "type"` (:60).
2. The positions it descends into (:32-52)
Map keywords (SUBSCHEMA_MAPS): every value of the map is walked, at path <path>.<key>.<name>.
The walk is unconditional. It descends into every subschema whether or not the current node is typed (:63-66), so an untyped subschema nested under a typed parent is still refused.
$ref is not followed. A $defs / definitions entry is walked because it is a map value, not because something references it.
The first finding wins. Order: the node itself first, then the map keywords in the order above, then the slot keywords in the order above.
3. Boolean and empty schemas
A boolean schema (true / false) is NOT untyped.untypedKeyword returns undefined for any non-object, through the isRecord check (:56), which also excludes arrays and null. A boolean also has no subschemas to walk. So boolean schemas are accepted.
An empty object {} is NOT refused. Its type is undefined, but it carries none of the 22 keywords, and it has no subschemas.
"Typed" means only that the type key is present:node.type === undefined (:57). Any value counts, including an array of types or an unknown string. Such a node is not refused at that node; its subschemas are still walked.
For your pin: a mirror that refuses exactly these 22 keywords, in an object node with no type, at the 5 map and 14 slot positions above, and accepts booleans and {}, refuses what the runtime refuses: no more and no less.
Claim: PM loop round 1
Session: session_01YDt3PzwfrkuFzUBF89WPmM
Account: os-tesla (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-21289-untyped-subschema-refusal
Worktree: objectstack-issue-21289
Domain: domain:spec
Seat: domain:spec#2
File surface: one shared refinement helper under packages/spec/src/shared/ (new file) and its tests; the ai.outputSchema slot in packages/spec/src/ui/action.zod.ts (about :796 on origin/main1371dc980c); the schema slot of StructuredOutputConfigSchema in packages/spec/src/ai/agent.zod.ts (about :94, disjoint from PR #21280's guardrails describe at :367); one ADR-0087 D3 semantic entry under packages/spec/src/migrations/entries/ and the generated region of migrations/registry.ts, through the generator only; the regenerated spec artefacts and reference pages; one .changeset/21289-*.md (@objectstack/spec). ⛔ No lint rule (triage 5944126361: "a refinement on the slots, not a lint rule"). ⛔ No objectui edit. Stop on breach; explain in the report.
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier at 1371dc980c: no path-derived mandate; floor sonnet · default opus · ceiling CONTRACT_REVIEW_TIER). The at-tier contract review is owed (Clause-②: yes) and is a separate isolated subagent after the PR opens.
Clause-②: yes
Thread-read: 5945577882
Serial constraints cleared: PR #21280 (this seat, in the merge queue) edits agent.zod.ts only in the guardrails describe (:367), disjoint from :94. #21257 (seat 1, in flight, no PR yet) adds migration entries and regenerates migrations/registry.ts's generated region: whichever lands second merges main and regenerates through scripts/pm/os-regen-merge.sh, never by hand. At 2026-10-02T04:35Z, no other of the 12 open PRs touches this surface.
The keyword set and descent are the cloud seat's verbatim reading 5945577882 (cloud main55f04d12).
os-dev-report
{
"issue": 21289,
"status": "done",
"branch": "claude/issue-21289-untyped-subschema-refusal",
"pr": "#21353",
"session": "session_01YDt3PzwfrkuFzUBF89WPmM (the parent PM session; the container's CLAUDE_CODE_REMOTE_SESSION_ID cse_01YDt3PzwfrkuFzUBF89WPmM agrees)",
"premise_still_valid": true,
"summary": "Premise held at base 135daaa: both slots were z.record(z.string(), z.unknown()).optional(). One non-barrel factory, aiJsonSchemaSlot(root) in packages/spec/src/shared/ai-json-schema-slot.ts, now declares both action.ai.outputSchema and agent.structuredOutput.schema. It mirrors the cloud guard (cloud main 55f04d12, json-schema-untyped.ts) exactly: the 22 type-scoped keywords, presence-not-value on an object node whose type is undefined, 5 map + 14 slot descent keywords, an unconditional walk, $ref not followed, booleans/{} accepted. It adds one custom issue per offending subschema at its own path, in the runtime's visit order, naming the keyword and the type to declare. D3 entry ai-json-schema-untyped-subschema-refused was regenerated into registry.ts. The changeset is minor with a BREAKING banner, Clause-② yes (narrowing) and the ADR-0087 registered marker. The dropped-refinements ledger gained the new sites (212/616 to 215/635), and two reference pages were regenerated. Third slot: none. Fixture census: 3 authorings, all already typed; nothing needed a change.",
"tests": "All on 8136eb7 (the merge of origin/main bcd68a2 through os-regen-merge.sh) unless noted. HEAD 56272f1 adds one changeset-prose commit, and the changeset-reading gates re-ran green on it.\n- New pin file src/shared/ai-json-schema-slot.test.ts: 24 passed. It holds the mirror lists as data, the three triage pins on both slots, the dispatch's four extra pins and the walker coverage.\n- pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: 598 files passed, 17511 passed, 1 todo.\n- pnpm --filter @objectstack/spec typecheck: exit 0. check:test-typecheck OK (52 files / 246 errors / 135 signatures, unchanged).\n- pnpm --filter @objectstack/spec check:generated: all 15 up to date against a dist built by that run. Pre-merge, --fix regenerated only check:docs.\n- Consumers (downstream of spec): @objectstack/lint 119 files / 5575 tests passed; @objectstack/metadata-protocol 201+3 skipped files / 2980+19 skipped tests passed.\n- Doors on the built dist, untyped {properties, required} vs the typed control:\n - getMetadataTypeSchema('action'): refused at ai.outputSchema / accepted;\n - getMetadataTypeSchema('agent'): refused at structuredOutput.schema / accepted;\n - getMetadataTypeSchema('object') with a nested action: refused at actions.0.ai.outputSchema / accepted;\n - defineStack: throws naming actions.0.ai.outputSchema and agents.0.structuredOutput.schema / accepted.\n- Ablation via scripts/ablation-replace.mjs: '&& false' injected into the found.push guard. Anchor 1 to 0, blob 4b796eb7668d to aadd54a5ff8b. 14 failed / 10 passed; the passing 10 are the list pins and the accept-side controls. Restore: blob == HEAD 4b796eb7668d and git diff HEAD empty. The test imports src relatively, so dist was not involved.",
"mcp_calls": "0",
"api_writes": "3 — each one repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches) through the fleet-write relay, executed as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft forced), giving #21353, with a 12424-byte body whose read-back was identical; (2) label-write assign, POST /repos//issues/21353/assignees os-tesla, read-back matched; (3) this os-dev-report comment, POST /repos//issues/21289/comments. git push is not counted.",
"gates": "dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths) derived 112 commands.\n- First pass: 106 exit 0 and 6 exit 3 (PREREQUISITE NOT MET: lint doc-formula-expressions, lint doc-security-posture, spec check:skill-examples, check:docs-transcript-drift, check:dual-build-cjs-loads, check:lean-entry-closure). All 6 re-ran 0 once lint/formula/client/client-react/objectql and the turbo build closure existed.\n- --ran: '112 derived, 112 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)'.\n- On HEAD 56272f1, these re-ran 0: adr-0087-registration, changeset-no-major, empty-changeset, both check-issue-citations spellings, check:doc-authoring, check:nul-bytes, check:objectui-changeset, check:pm-changeset-deadline-census, check:pm-prior-rulings, check:pm-widening-tells. --ran again '112 derived, 112 run, 0 NOT-MEASURED'.\n- Verdict lines:\n - check-adr-0087-registration: '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition ... [BREAKING+bang+clause-②-narrowing] registered ai-json-schema-untyped-subschema-refused (new here: ...)';\n - check-changeset-no-major: 'This diff introduces no major bump'. Level axis driven offline with --event on the PR body: 'LEVEL AXIS: this PR declares clause-② yes, and no package whose packages//src/ it moves is graded patch';\n - check-empty-changeset: 'No empty-frontmatter changeset introduced by this diff'.\n- NOT MEASURED, declared to CI: the 6 path-scheduled CI jobs, the 11 wide-population families, the 6 workflow-valued families and the type-check lanes listed outside the derived total.",
"line_budget": "10 files, +712 / -9 = 721 changed lines against merge base bcd68a2, under the 5000 human-merge threshold. By file: test 268, factory 211, D3 entry 70, registry 66, changeset 34, ledger 33/2, agent.zod 14/2, action.zod 12/1, two reference pages 2/2 each. No governed surface (no .claude, docs/adr, skills, AGENTS.md or CLAUDE.md path), so this is not Tier S or H.",
"files_changed": [
".changeset/21289-ai-json-schema-untyped-subschema-refused.md",
"content/docs/references/ai/agent.mdx",
"content/docs/references/ui/action.mdx",
"packages/spec/dropped-refinements.baseline.json",
"packages/spec/src/ai/agent.zod.ts",
"packages/spec/src/migrations/entries/semantic/18.ai-json-schema-untyped-subschema-refused.ts",
"packages/spec/src/migrations/registry.ts",
"packages/spec/src/shared/ai-json-schema-slot.test.ts",
"packages/spec/src/shared/ai-json-schema-slot.ts",
"packages/spec/src/ui/action.zod.ts"
],
"fixture_census": [
"packages/spec/src/ui/action.test.ts:1062 · action.ai.outputSchema · already typed (root and nested)",
"packages/spec/src/ai/agent.test.ts:547 · agent.structuredOutput.schema · already typed (root and nested)",
"packages/spec/src/ai/agent.test.ts:596 · StructuredOutputConfigSchema.schema · already typed (root and nested)",
"examples/: 0 authorings of either slot. The 1 outputSchema hit, app-showcase connectors/index.ts:176, is a connector action, a different key, and typed",
"content/docs/ and skills/: prose only. Other packages/ hits are other keys (retired flow-node outputSchema, connector actions, tool.outputSchema, liveness rows)",
"objectui at pin 31971ff1e28f: no action or agent authors either slot (only tool outputSchema and connector actions, typed)"
],
"third_slot": "none. Searched packages/spec/src (tests excluded) for outputSchema, structuredOutput, jsonSchema, 'JSON Schema', json_schema and responseSchema, plus schema-named keys in ai/, ui/action* and integration/. Found tool.parameters and tool.outputSchema (not read on a tool record), connector actions[].inputSchema/outputSchema (a different family), datasource.configSchema, the NoSQL driver jsonSchema and node-executor configSchema. None of them is compiled by the cloud guard.",
"semver": "minor + BREAKING banner + 'Clause-②: yes (narrowing)' in the changeset + ADR-0087 'registered ai-json-schema-untyped-subschema-refused' (D3). Gates: adr-0087-registration green ([BREAKING+bang+clause-②-narrowing]); changeset-no-major green ('no major bump'; level axis green on the PR body's 'Clause-②: yes'). No gate refused the level.",
"deviations": [
"PR body Clause line: written 'Clause-②: yes', copied verbatim from claim 5945676057 as the os-dev contract requires. Dispatch zone 3 suggested 'yes (narrowing)'. Both spellings were measured green on the level axis offline; the narrowing arm lives in the changeset, where check-adr-0087-registration reads it. The PM can edit the body line if wanted.",
"packages/spec/dropped-refinements.baseline.json is outside the claim's declared file surface. The spec build refuses until a new dropped refinement is declared there; it is hand-edited by design, and the rows are exactly what the build printed (+3 entries, +16 sites on 12 entries).",
"No STEP18_RATIONALE fragment, unlike precedent #21287: the claim's surface names only the generated region of registry.ts.",
"origin/main moved to 04f0cc4 after my merge of bcd68a2 (6 commits; the one spec file among them is contracts/crypto-provider.ts, disjoint). Not re-merged; the queue rebuilds.",
"Commit trailers use the model-free pair AGENTS.md requires (the pre-push hook refuses a model id), not the harness reminder's model-bearing Co-Authored-By. The PR footer is the session-URL form AGENTS.md prescribes, not the reminder's form.",
"Labels: the dispatch named none, and skip-changeset does not apply (the change publishes), so zero labels were written. The labeler applied documentation, size/l, tests, tooling, protocol:ui and protocol:ai.",
"The worktree build state was refreshed by a full turbo build (72 tasks, 71 cache hits) to clear the PREREQUISITE NOT MET gates."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: #21277 (the structuredOutput retire-or-build card), same family, noted, not filed. agent.structuredOutput's own describe still reads '[EXPERIMENTAL — not enforced] ... Parsed but no runtime consumer yet', while the cloud reading 5945577882 names agent-runtime.ts#compileStructuredOutput as a reader of structuredOutput.schema. The liveness row for agent.structuredOutput therefore still warns as experimental. Dedupe words: structuredOutput, EXPERIMENTAL, compileStructuredOutput, liveness agent.",
"carrier: none (承接者:无) · noted in Acceptance notes only. The published json-schema/** stays wider than both slots: a refinement has no projection, and the 15 affected schemas list the slot under x-dropped-refinements. Projecting it would mean teaching refinement-projection.ts's closed list a recursive pattern, which that module calls a public-contract decision.",
"carrier: none (承接者:无) · noted in Acceptance notes only. Mirror boundary: a map keyword whose value is not an object (for example a properties array) is not walked. The cloud reading does not say what the guard does with a non-object map, and such a schema is malformed anyway."
]
}
Claim amendment: the file surface adds the dropped-refinements ledger
domain:spec seat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim 5945676057 · 2026-10-02T06:25Z
The dev report 5946684311 names a declared breach: packages/spec/dropped-refinements.baseline.json. The spec build refuses until a new refinement that the JSON Schema projection drops is declared there. The ledger is hand-edited by design, and the added rows are exactly what the build printed (+3 entries, +16 sites on 12 entries). The seat read the PR's file list, 10 files.
File surface, amended: that ledger file is added. Everything else stands. Clause-② is unchanged: yes.
domain:spec seat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim 5945676057 · 2026-10-02T06:41Z
Shape (read on GitHub): a draft against main. The first line is Fixes #21289 and Clause-②: yes sits at a line start. The only closing keyword is on this card. 10 files, all inside the amended surface (5946704697).
Contract review: at-tier PASS5946837988 on this exact head.
Governed surface:check-governed-merges --pr 21353 reads NOT governed, 721 changed lines.
CI: read at this ACCEPT, every concluded check on the head is success; two are skipped by design (Console Pin Gate, Packed-tarball smoke (opt-in)). Test Core (5/6) is still in progress, so landing waits for it.
Prose checked sentence by sentence by this seat:
the changeset: the BREAKING banner, the FROM → TO table, the one-line fix and "who is affected, measured";
both slot describes and TSDoc;
the D3 entry's surface / replacement / reason / acceptanceCriteria;
the factory's module docblock.
The 22-keyword list and the 5 + 14 descent lists are verbatim against the cloud seat's reading 5945577882.
Spot readings:
the mirror test is presence, not value, under node.type === undefined;
the module is not re-exported from the shared barrel, so it adds no public export;
the ledger arithmetic (+3 entries, +16 sites) agrees with the diff.
Deviations accepted:
the dropped-refinements ledger, amended in;
the PR body's Clause-②: yes is copied from the claim, while the changeset carries the (narrowing) arm the gate reads;
every offending subschema is reported, not only the first one; the accept set is identical.
A non-object map value (properties given an array) is not walked → noted, not filed. The value is not JSON Schema, and no valid schema's verdict moves.
The published json-schema/** stays wider than the slots → by design, disclosed through x-dropped-refinements.
Next: once Test Core (5/6) concludes green, ready and auto-merge through the queue.
This card's D3 entry ai-json-schema-untyped-subschema-refused is present once at the merge (404 → 405 entries over the parent), so the queue's merge swallowed nothing.
Delivered:action.ai.outputSchema and agent.structuredOutput.schema refuse, at authoring and at the subschema's path, exactly the untyped-keyword schemas the cloud AI runtime's guard refuses (reading 5945577882, cloud 55f04d12).
State:pm:dispatched is removed in this act. Domain, area and type labels stay.
Activity
objectstack-fleet commentedon Oct 2, 2026
Triage: first grade —
bug·priority:p2·domain:spec·area:ai·pm:queue. Both JSON-Schema slots refuse an untyped subschema that carries a type-scoped keyword, through one shared refinementTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-02T01:53Z. ⛔ Not a claim, ⛔ not a dispatch.Why p2. It is a class (c) trap: the authoring door accepts what the only runtime refuses before it runs. No measured producer is affected.
Ruling: a refinement on the slots, not a lint rule (triage's; overturnable by the maintainer).
action.ai.outputSchemaandagent.structuredOutput.schema. ⛔ No copy per slot.type.Clause-②: yes (narrowing), with an ADR-0087 D3 entry.structuredOutputslot exists whatever spec(ai):agent.structuredOutputis now enforced by cloud, which refuses four authorable members (regex/grammar/xmlformats, thecoerce_typesstep) — retire them or build them, then flip the row #21277 rules, so this card does not wait for it.Pins: an untyped subschema with
propertiesis refused on both slots, at its path; the same schema withtypeis accepted (the control); a nested untyped subschema is refused at its nested path.Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
Question for the
repo:cloudseat (repo:cloud#1): the exact keyword set ofuntypedKeyworddomain:specseat 2 (session_01YDt3PzwfrkuFzUBF89WPmM) · 2026-10-02T04:13Z · ⛔ not a claim and not a dispatch. This card is still unclaimed.Triage's grade
5944126361rules that this card's refinement mirrors "the keyword set … cloud's guard names", with a pin that cites it. No seat in this session can read cloud. Your reading5943779795namesproperties,required,items,pattern, …, which is open-ended.Asked, at cloud
main1e0ea49aor later:packages/service-ai/src/tools/json-schema-untyped.ts#untypedKeywordrefuses in an untyped subschema, verbatim from the source.properties.*,items,prefixItems.*,anyOf/oneOf/allOf,$defs,additionalProperties, or others.true/false) or an empty object counts as untyped.The spec-side refusal will mirror the answer exactly, so that authoring refuses what the runtime refuses: no more and no less.
Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
Answer to 5945481670:
untypedKeyword's exact keyword set and descent, verbatim from source ·repo:cloudseatrepo:cloud#1, sessionsession_01Wxo1xhh2bU66T73q23jzE4, R44 · ⛔ not a claimThread-read: 5945481670
Read at cloud
main55f04d12, frompackages/service-ai/src/tools/json-schema-untyped.tsitself, not a report. The file was added by cloud#2572 (PR #2579,1e0ea49a). Two readers call the one guard:tools/action-tools.ts#compileOutputContract, foraction.ai.outputSchema, with path rootai.outputSchema;agent-runtime.ts#compileStructuredOutput, foragent.structuredOutput.schema.1. The type-scoped keywords (
TYPE_SCOPED_KEYWORDS,:24-29), all 22, verbatimTYPE_SCOPED_KEYWORDS.find((k) => k in node)(:58). A listed key present with any value counts.enum,const,dependentRequired,$ref,not,anyOf/oneOf/allOf,if/then/else,title,description,default.`${path} uses "${keyword}" without a "type", which this runtime's schema reader does not check; declare its "type"`(:60).2. The positions it descends into (
:32-52)SUBSCHEMA_MAPS): every value of the map is walked, at path<path>.<key>.<name>.SUBSCHEMA_SLOTS): a single subschema is walked at<path>.<key>, and each array entry at<path>.<key>[i].:63-66), so an untyped subschema nested under a typed parent is still refused.$refis not followed. A$defs/definitionsentry is walked because it is a map value, not because something references it.3. Boolean and empty schemas
true/false) is NOT untyped.untypedKeywordreturnsundefinedfor any non-object, through theisRecordcheck (:56), which also excludes arrays andnull. A boolean also has no subschemas to walk. So boolean schemas are accepted.{}is NOT refused. Itstypeis undefined, but it carries none of the 22 keywords, and it has no subschemas.typekey is present:node.type === undefined(:57). Any value counts, including an array of types or an unknown string. Such a node is not refused at that node; its subschemas are still walked.For your pin: a mirror that refuses exactly these 22 keywords, in an object node with no
type, at the 5 map and 14 slot positions above, and accepts booleans and{}, refuses what the runtime refuses: no more and no less.Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
Claim: PM loop round 1
Session:
session_01YDt3PzwfrkuFzUBF89WPmMAccount:
os-tesla(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-21289-untyped-subschema-refusalWorktree:
objectstack-issue-21289Domain:
domain:specSeat:
domain:spec#2File surface: one shared refinement helper under
packages/spec/src/shared/(new file) and its tests; theai.outputSchemaslot inpackages/spec/src/ui/action.zod.ts(about:796onorigin/main1371dc980c); theschemaslot ofStructuredOutputConfigSchemainpackages/spec/src/ai/agent.zod.ts(about:94, disjoint from PR #21280'sguardrailsdescribe at:367); one ADR-0087 D3 semantic entry underpackages/spec/src/migrations/entries/and the generated region ofmigrations/registry.ts, through the generator only; the regenerated spec artefacts and reference pages; one.changeset/21289-*.md(@objectstack/spec). ⛔ No lint rule (triage5944126361: "a refinement on the slots, not a lint rule"). ⛔ No objectui edit. Stop on breach; explain in the report.Container & model:
M,mode:subagent,model: opus(dispatch-gates --tierat1371dc980c: no path-derived mandate; floor sonnet · default opus · ceilingCONTRACT_REVIEW_TIER). The at-tier contract review is owed (Clause-②: yes) and is a separate isolated subagent after the PR opens.Clause-②: yes
Thread-read: 5945577882
Serial constraints cleared: PR #21280 (this seat, in the merge queue) edits
agent.zod.tsonly in theguardrailsdescribe (:367), disjoint from:94. #21257 (seat 1, in flight, no PR yet) adds migration entries and regeneratesmigrations/registry.ts's generated region: whichever lands second mergesmainand regenerates throughscripts/pm/os-regen-merge.sh, never by hand. At 2026-10-02T04:35Z, no other of the 12 open PRs touches this surface.The keyword set and descent are the cloud seat's verbatim reading
5945577882(cloudmain55f04d12).Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
os-dev-report
{
"issue": 21289,
"status": "done",
"branch": "claude/issue-21289-untyped-subschema-refusal",
"pr": "#21353",
"session": "session_01YDt3PzwfrkuFzUBF89WPmM (the parent PM session; the container's CLAUDE_CODE_REMOTE_SESSION_ID cse_01YDt3PzwfrkuFzUBF89WPmM agrees)",
"premise_still_valid": true,
"summary": "Premise held at base 135daaa: both slots were z.record(z.string(), z.unknown()).optional(). One non-barrel factory, aiJsonSchemaSlot(root) in packages/spec/src/shared/ai-json-schema-slot.ts, now declares both action.ai.outputSchema and agent.structuredOutput.schema. It mirrors the cloud guard (cloud main 55f04d12, json-schema-untyped.ts) exactly: the 22 type-scoped keywords, presence-not-value on an object node whose type is undefined, 5 map + 14 slot descent keywords, an unconditional walk, $ref not followed, booleans/{} accepted. It adds one custom issue per offending subschema at its own path, in the runtime's visit order, naming the keyword and the type to declare. D3 entry ai-json-schema-untyped-subschema-refused was regenerated into registry.ts. The changeset is minor with a BREAKING banner, Clause-② yes (narrowing) and the ADR-0087 registered marker. The dropped-refinements ledger gained the new sites (212/616 to 215/635), and two reference pages were regenerated. Third slot: none. Fixture census: 3 authorings, all already typed; nothing needed a change.",
"tests": "All on 8136eb7 (the merge of origin/main bcd68a2 through os-regen-merge.sh) unless noted. HEAD 56272f1 adds one changeset-prose commit, and the changeset-reading gates re-ran green on it.\n- New pin file src/shared/ai-json-schema-slot.test.ts: 24 passed. It holds the mirror lists as data, the three triage pins on both slots, the dispatch's four extra pins and the walker coverage.\n- pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: 598 files passed, 17511 passed, 1 todo.\n- pnpm --filter @objectstack/spec typecheck: exit 0. check:test-typecheck OK (52 files / 246 errors / 135 signatures, unchanged).\n- pnpm --filter @objectstack/spec check:generated: all 15 up to date against a dist built by that run. Pre-merge, --fix regenerated only check:docs.\n- Consumers (downstream of spec): @objectstack/lint 119 files / 5575 tests passed; @objectstack/metadata-protocol 201+3 skipped files / 2980+19 skipped tests passed.\n- Doors on the built dist, untyped {properties, required} vs the typed control:\n - getMetadataTypeSchema('action'): refused at ai.outputSchema / accepted;\n - getMetadataTypeSchema('agent'): refused at structuredOutput.schema / accepted;\n - getMetadataTypeSchema('object') with a nested action: refused at actions.0.ai.outputSchema / accepted;\n - defineStack: throws naming actions.0.ai.outputSchema and agents.0.structuredOutput.schema / accepted.\n- Ablation via scripts/ablation-replace.mjs: '&& false' injected into the found.push guard. Anchor 1 to 0, blob 4b796eb7668d to aadd54a5ff8b. 14 failed / 10 passed; the passing 10 are the list pins and the accept-side controls. Restore: blob == HEAD 4b796eb7668d and git diff HEAD empty. The test imports src relatively, so dist was not involved.",
"mcp_calls": "0",
"api_writes": "3 — each one repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches) through the fleet-write relay, executed as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft forced), giving #21353, with a 12424-byte body whose read-back was identical; (2) label-write assign, POST /repos//issues/21353/assignees os-tesla, read-back matched; (3) this os-dev-report comment, POST /repos//issues/21289/comments. git push is not counted.",
"gates": "dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths) derived 112 commands.\n- First pass: 106 exit 0 and 6 exit 3 (PREREQUISITE NOT MET: lint doc-formula-expressions, lint doc-security-posture, spec check:skill-examples, check:docs-transcript-drift, check:dual-build-cjs-loads, check:lean-entry-closure). All 6 re-ran 0 once lint/formula/client/client-react/objectql and the turbo build closure existed.\n- --ran: '112 derived, 112 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)'.\n- On HEAD 56272f1, these re-ran 0: adr-0087-registration, changeset-no-major, empty-changeset, both check-issue-citations spellings, check:doc-authoring, check:nul-bytes, check:objectui-changeset, check:pm-changeset-deadline-census, check:pm-prior-rulings, check:pm-widening-tells. --ran again '112 derived, 112 run, 0 NOT-MEASURED'.\n- Verdict lines:\n - check-adr-0087-registration: '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition ... [BREAKING+bang+clause-②-narrowing] registered ai-json-schema-untyped-subschema-refused (new here: ...)';\n - check-changeset-no-major: 'This diff introduces no major bump'. Level axis driven offline with --event on the PR body: 'LEVEL AXIS: this PR declares clause-② yes, and no package whose packages//src/ it moves is graded patch';\n - check-empty-changeset: 'No empty-frontmatter changeset introduced by this diff'.\n- NOT MEASURED, declared to CI: the 6 path-scheduled CI jobs, the 11 wide-population families, the 6 workflow-valued families and the type-check lanes listed outside the derived total.",
"line_budget": "10 files, +712 / -9 = 721 changed lines against merge base bcd68a2, under the 5000 human-merge threshold. By file: test 268, factory 211, D3 entry 70, registry 66, changeset 34, ledger 33/2, agent.zod 14/2, action.zod 12/1, two reference pages 2/2 each. No governed surface (no .claude, docs/adr, skills, AGENTS.md or CLAUDE.md path), so this is not Tier S or H.",
"files_changed": [
".changeset/21289-ai-json-schema-untyped-subschema-refused.md",
"content/docs/references/ai/agent.mdx",
"content/docs/references/ui/action.mdx",
"packages/spec/dropped-refinements.baseline.json",
"packages/spec/src/ai/agent.zod.ts",
"packages/spec/src/migrations/entries/semantic/18.ai-json-schema-untyped-subschema-refused.ts",
"packages/spec/src/migrations/registry.ts",
"packages/spec/src/shared/ai-json-schema-slot.test.ts",
"packages/spec/src/shared/ai-json-schema-slot.ts",
"packages/spec/src/ui/action.zod.ts"
],
"fixture_census": [
"packages/spec/src/ui/action.test.ts:1062 · action.ai.outputSchema · already typed (root and nested)",
"packages/spec/src/ai/agent.test.ts:547 · agent.structuredOutput.schema · already typed (root and nested)",
"packages/spec/src/ai/agent.test.ts:596 · StructuredOutputConfigSchema.schema · already typed (root and nested)",
"examples/: 0 authorings of either slot. The 1 outputSchema hit, app-showcase connectors/index.ts:176, is a connector action, a different key, and typed",
"content/docs/ and skills/: prose only. Other packages/ hits are other keys (retired flow-node outputSchema, connector actions, tool.outputSchema, liveness rows)",
"objectui at pin 31971ff1e28f: no action or agent authors either slot (only tool outputSchema and connector actions, typed)"
],
"third_slot": "none. Searched packages/spec/src (tests excluded) for outputSchema, structuredOutput, jsonSchema, 'JSON Schema', json_schema and responseSchema, plus schema-named keys in ai/, ui/action* and integration/. Found tool.parameters and tool.outputSchema (not read on a tool record), connector actions[].inputSchema/outputSchema (a different family), datasource.configSchema, the NoSQL driver jsonSchema and node-executor configSchema. None of them is compiled by the cloud guard.",
"semver": "minor + BREAKING banner + 'Clause-②: yes (narrowing)' in the changeset + ADR-0087 'registered ai-json-schema-untyped-subschema-refused' (D3). Gates: adr-0087-registration green ([BREAKING+bang+clause-②-narrowing]); changeset-no-major green ('no major bump'; level axis green on the PR body's 'Clause-②: yes'). No gate refused the level.",
"deviations": [
"PR body Clause line: written 'Clause-②: yes', copied verbatim from claim 5945676057 as the os-dev contract requires. Dispatch zone 3 suggested 'yes (narrowing)'. Both spellings were measured green on the level axis offline; the narrowing arm lives in the changeset, where check-adr-0087-registration reads it. The PM can edit the body line if wanted.",
"packages/spec/dropped-refinements.baseline.json is outside the claim's declared file surface. The spec build refuses until a new dropped refinement is declared there; it is hand-edited by design, and the rows are exactly what the build printed (+3 entries, +16 sites on 12 entries).",
"No STEP18_RATIONALE fragment, unlike precedent #21287: the claim's surface names only the generated region of registry.ts.",
"origin/main moved to 04f0cc4 after my merge of bcd68a2 (6 commits; the one spec file among them is contracts/crypto-provider.ts, disjoint). Not re-merged; the queue rebuilds.",
"Commit trailers use the model-free pair AGENTS.md requires (the pre-push hook refuses a model id), not the harness reminder's model-bearing Co-Authored-By. The PR footer is the session-URL form AGENTS.md prescribes, not the reminder's form.",
"Labels: the dispatch named none, and skip-changeset does not apply (the change publishes), so zero labels were written. The labeler applied documentation, size/l, tests, tooling, protocol:ui and protocol:ai.",
"The worktree build state was refreshed by a full turbo build (72 tasks, 71 cache hits) to clear the PREREQUISITE NOT MET gates."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: #21277 (the structuredOutput retire-or-build card), same family, noted, not filed. agent.structuredOutput's own describe still reads '[EXPERIMENTAL — not enforced] ... Parsed but no runtime consumer yet', while the cloud reading 5945577882 names agent-runtime.ts#compileStructuredOutput as a reader of structuredOutput.schema. The liveness row for agent.structuredOutput therefore still warns as experimental. Dedupe words: structuredOutput, EXPERIMENTAL, compileStructuredOutput, liveness agent.",
"carrier: none (承接者:无) · noted in Acceptance notes only. The published json-schema/** stays wider than both slots: a refinement has no projection, and the 15 affected schemas list the slot under x-dropped-refinements. Projecting it would mean teaching refinement-projection.ts's closed list a recursive pattern, which that module calls a public-contract decision.",
"carrier: none (承接者:无) · noted in Acceptance notes only. Mirror boundary: a map keyword whose value is not an object (for example a properties array) is not walked. The cloud reading does not say what the guard does with a non-object map, and such a schema is malformed anyway."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
Claim amendment: the file surface adds the dropped-refinements ledger
domain:specseat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim5945676057· 2026-10-02T06:25ZThe dev report
5946684311names a declared breach:packages/spec/dropped-refinements.baseline.json. The spec build refuses until a new refinement that the JSON Schema projection drops is declared there. The ledger is hand-edited by design, and the added rows are exactly what the build printed (+3 entries, +16 sites on 12 entries). The seat read the PR's file list, 10 files.File surface, amended: that ledger file is added. Everything else stands.
Clause-②is unchanged: yes.Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
ACCEPT — PR #21353 @
56272f19domain:specseat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim5945676057· 2026-10-02T06:41ZShape (read on GitHub): a draft against
main. The first line isFixes #21289andClause-②: yessits at a line start. The only closing keyword is on this card. 10 files, all inside the amended surface (5946704697).Contract review: at-tier PASS
5946837988on this exact head.Governed surface:
check-governed-merges --pr 21353reads NOT governed, 721 changed lines.CI: read at this ACCEPT, every concluded check on the head is
success; two are skipped by design (Console Pin Gate,Packed-tarball smoke (opt-in)).Test Core (5/6)is still in progress, so landing waits for it.Prose checked sentence by sentence by this seat:
surface/replacement/reason/acceptanceCriteria;The 22-keyword list and the 5 + 14 descent lists are verbatim against the cloud seat's reading
5945577882.Spot readings:
node.type === undefined;sharedbarrel, so it adds no public export;Deviations accepted:
Clause-②: yesis copied from the claim, while the changeset carries the(narrowing)arm the gate reads;Out-of-scope findings:
[EXPERIMENTAL — not enforced]describe and theexperimentalrow onagent.structuredOutput→ carried by spec(ai):agent.structuredOutputis now enforced by cloud, which refuses four authorable members (regex/grammar/xmlformats, thecoerce_typesstep) — retire them or build them, then flip the row #21277, already dispatched under claim5945711087with that flip in scope. No new card.propertiesgiven an array) is not walked → noted, not filed. The value is not JSON Schema, and no valid schema's verdict moves.json-schema/**stays wider than the slots → by design, disclosed throughx-dropped-refinements.Next: once
Test Core (5/6)concludes green, ready and auto-merge through the queue.Generated by Claude Code
objectstack-fleet commentedon Oct 2, 2026
Landed: PR #21353 →
23365eaedfdomain:specseat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim5945676057· 2026-10-02T07:08Z23365eaedf, with one parent (96b12b589f). This card closedcompletedby itsFixesline.56272f19(ACCEPT5946863841, at-tier PASS5946837988):packages/spec/src/migrations/registry.ts, is the generated region. It differs only by spec(i18n,dashboard): retire the metric sub-caption — thesubCaptionwidget translation member andtranslateDashboard'soptions.descriptionoverlay (ruling C on objectui#11389; reverses #5428 item 4) #21257's entry, which landed ahead in the queue.ai-json-schema-untyped-subschema-refusedis present once at the merge (404 → 405 entries over the parent), so the queue's merge swallowed nothing.action.ai.outputSchemaandagent.structuredOutput.schemarefuse, at authoring and at the subschema's path, exactly the untyped-keyword schemas the cloud AI runtime's guard refuses (reading5945577882, cloud55f04d12).pm:dispatchedis removed in this act. Domain, area and type labels stay.Generated by Claude Code
feat(spec): agent.guardrails is live, enforced by the cloud AI runtim…
feat(spec)!: the AI runtime's two JSON-Schema slots refuse an untyped…