feat(spec): offer agent.structuredOutput on the agent form and drop its stale not-enforced-yet ledger row - #21398
Conversation
…stale omit row The reconciliation ledger excused `agent.structuredOutput` as declared but not enforced. The key is enforced now (liveness `live`), and the ledger's own rule hands the offer decision to the enforcement. Studio's composite control carries every child of the block, the free-form JSON Schema record included (raw JSON editor), so the key is offered as a composite beside the other agent blocks and the row goes. Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
…e structuredOutput row `pnpm i18n:extract` adds the agent form's new `structuredOutput` row (label and help text) to the four metadata-form bundles. The zh-CN, ja-JP and es-ES leaves are authored, not left as copies of the English source, and the re-run extract drops their source-hash entries by itself. Changeset: spec minor (a new form offer), platform-objects patch. Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
…he structuredOutput row The per-locale positive control counts the metadata-form labels authored in each of zh-CN, ja-JP and es-ES. The agent form's new row adds one, so the measured count is 660 (was 659) in every locale. Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
The console pin moved on main after the reading; the renderer files the reading cites are unchanged across the move, so the changeset names the renderer rather than one sha. Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 138 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 447015d16eaca22294645530ba650739e2474492 && git checkout 447015d16eaca22294645530ba650739e2474492
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ecb6ca0258176466767588a6805363387c5777a6 5870ff91d74be07f621380dbfb0c17b9c85c466a && git checkout -B drift-repro ecb6ca0258176466767588a6805363387c5777a6 && git merge --no-ff 5870ff91d74be07f621380dbfb0c17b9c85c466a
node scripts/docs-audit/affected-docs.mjs --json ecb6ca0258176466767588a6805363387c5777a6 |
Contract reviewServed-tier: Inputs: card #21374 (body; comments 5948883894 triage, 5949271559 claim, 5951402104 os-dev-report), PR #21398 (body, 8-file list, net diff against ① Derived judgments
② Semver level
③ Boundary flags
Dev deviations, each answered:
Out-of-scope findings (four, noted not filed), judged:
Check-runs on the head, read 2026-10-02T11:38Z: 23 success, 3 skipped ( Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #21374
Clause-②: no
The form reconciliation ledger kept
agent.structuredOutputunoffered on a reason that stopped being true: the key is enforced and gradedlive. This PR deletes that row and decides the offer on a measurement, as triage5948883894directed. The measurement says the Studiocompositecontrol carries the block, including its free-form JSON Schema record, so the key is offered.What changes
packages/spec/src/system/metadata-form-zod-reconciliation.test.tsagent/structuredOutputomitrow (base lines 408 to 414) is deleted. The gate's own rule (lines 307 to 309) gives this decision to the enforcement. No other line changes, and nothing new checks a row'swhytext.packages/spec/src/ai/agent.form.ts{ field: 'structuredOutput', type: 'composite', helpText }. It is spelled likememoryandguardrails, with no hand-writtenfields, so Studio derives the sub-rows from the served JSON Schema.packages/platform-objects/src/apps/translations/*.metadata-forms.generated.tspnpm i18n:extract. Two new leaves per locale: the row's label and help text. Thezh-CN,ja-JPandes-ESleaves are authored, not left as copies of the English source. After the second extract, no locale'ssource-hashes.generated.tsholds an entry for them, so those three files are not in the diff.packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.tsexpected 660 to be 659) before the pin was edited..changeset/21374-agent-structured-output-form-offer.md@objectstack/specminor (a new form offer),@objectstack/platform-objectspatch (two catalog leaves). Neither is breaking.The measurement (triage item 2)
Where it was read: objectui at the
.objectui-shapin31971ff1e28f, from a local clone withgit show.mainmoved the pin to89cad75d5570while this was in flight.SchemaForm.tsxandwidgets.tsxare byte-identical between the two pins (git diff --statis empty), andmerge-base --is-ancestor 31971ff1e28f 89cad75d5570exits 0. So the same reading holds at the pin this branch now carries.What the control is given: the served node, measured with the emitter's own code.
z.toJSONSchemawas run withmarkErasedAuthoringInput, thenstripUnauthorableProperties: the stepstoJsonSchemaSafeinmetadata-protocoltakes.agenttakes the output arm (24 top-level keys), andstructuredOutputis served inline, with no$ref:format,fallbackFormattype: string,enum: [json_object, json_schema]schematype: object,propertyNames: {type: string},additionalProperties: {}, and nopropertiesstrict,retryOnValidationFailuretype: booleanmaxRetriestype: integer,minimum: 0transformPipelinetype: array,items: {type: string, enum: [trim, parse_json, validate]}How a
compositerow renders (packages/app-shell/src/views/metadata-admin/, at the pin):SchemaForm.tsxresolveFieldFace(line 837):fieldSpec.type === 'composite'gives{ kind: 'composite' }.FieldControl(lines 2081 to 2099): the row has nofields, soderivePropertyNames(schema)(line 3376) lists every property of the served node.CompositeField(line 2518) renders oneFieldRowper property, with the child node frompickSubSchema(schema, 'composite', name)(line 2487). A child edit writesonChange({ ...obj, [field]: v }), and an untouched child is never written.resolveFieldWidget, thenresolveFieldFace:schema, the open record:inferWidget(line 519) answersobject-fieldsfortype: object, and no name detector matches.object-fieldsis not a key ofWIDGETS(widgets.tsxline 2934).isObjectFormis false (noproperties), the node is not an object-row array, and the name is not inKNOWN_PASSTHROUGH_WIDGETS(line 230). So the face is{ kind: 'raw-json', hint: 'object-fields' }, which isRawJsonEditor(line 3218). The editor showsJSON.stringify(value, null, 2). Each edit runsJSON.parseand passes the parsed value up. Text that does not parse showsInvalid JSONand passes nothing, and empty text passesundefined.transformPipeline, the enum array:inferWidget(line 510) answersmultiselect, a registered widget, soMultiSelectWidgetrenders it (widgets.tsxline 1396). Its options areitems.enum.toggle()(lines 1428 to 1443) rebuilds the selection asoptions.map(o => o.value).filter(v => set.has(v)): declared enum order, duplicates collapsed, and an empty selection passed up asundefined.formatandfallbackFormatare selects overenum.strictandretryOnValidationFailureare switches, andmaxRetriesis a number input.Round-trip verdicts ("carries" means view it, edit it and save it back unchanged):
schema: carries. The stored record is shown whole as JSON. An untouched value is never re-emitted, so it saves back byte-equal. An edited value saves exactly the JSON that was typed. The parse still refuses an untyped subschema at its path, and that refusal is unchanged.transformPipeline: carries the step set. An untouched value saves back unchanged. A toggle writes the steps in declared enum order (trim,parse_json,validate) with duplicates collapsed, so this control cannot author another order. Every pipeline written in this repo is already in enum order (git grep transformPipeline: the agent tests and the conversion fixtures). This repo cannot read whether the cloud runtime honours another order, so this PR makes no claim about it.action.ai.outputSchemacomes from the sameaiJsonSchemaSlotfactory. It is served with an identical node and is already offered through the action form'sairow, where it reaches the same raw-JSON face. OfferingstructuredOutputadds no new face.Verification
All runs are on this branch. Gates and suites are at the merged head
5870ff91d7(origin/main69a12a0952merged in, with a fullturbo run build --filter='./packages/**'of 71 tasks first).Ablation of the pin. Two legs, both written with
scripts/ablation-replace.mjs(anchor hit, then blob restored equal toHEADandgit diff HEADempty), run at77dedb2c86. The suite waspnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/system/metadata-form-zod-reconciliation.test.ts, which imports the form registry from source, so no build sits between the edit and the test.agent.(root): accepted by the Zod but unauthorable in the form … expected [ 'structuredOutput' ] to deeply equal [].agent.(root).structuredOutput: the form offers it now — drop the ledger entry. The first attempt at leg B was a no-op: the tool refused it because the replacement contained its own anchor, so no test ran. The retry used an anchor the replacement does not contain.HEAD:f37667ab25a0(form) and2350eaed10be(test).Suites:
pnpm --filter @objectstack/platform-objects testTest Files 59 passed (59)·Tests 949 passed (949)pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2(whole package)Test Files 647 passed (647)·Tests 18407 passed | 1 todo (18408)pnpm --filter @objectstack/spec --filter @objectstack/platform-objects run typecheckDonepnpm --filter @objectstack/spec check:generated✓ All 15 generated artifacts are up to dateGates:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 87 commands against merge base69a12a095, with no stale-tree warning. Each was run, and its exit code was recorded before any pipe.--ranreconciliation:87 derived, 87 run, 0 NOT-MEASURED, 0 UNRUN. This includespnpm check:i18n(platform-objects in sync (11 bundle(s))),pnpm check:doc-authoring,pnpm check:nul-bytes, the changeset gates (check-adr-0087-registration: no declared-breaking changeset;check-changeset-no-major: nomajor) andpnpm check:i18n-coverage(621 baselined untranslated string(s), none new, run in addition to the derived list).Semver (A5):
Clause-②: no.AgentSchema's accept set does not move, andcheck:authorable-surfaceandcheck:api-surfaceare green with no regeneration. The form definition ships in@objectstack/spec'sdist, and the catalogs ship in@objectstack/platform-objects'sdist. Both new leaves were found in the builtplatform-objectsdist. So both packages publish a change, and this is a changeset, notskip-changeset.Acceptance notes
Observations only. None meets a filing class here.
schemachild's hint. It shows objectui's announced fallback line,widget object-fields — falling back to JSON until a custom renderer is registered.The action form'sai.outputSchemashows the same line today. Polish on the objectui side.prettify(name), help text from eachdescribe()), so they read in English in every locale.memory's andguardrails' sub-rows read the same way: a schema-derived composite has no catalog key for its children.agent.form.ts.planningnames "strategy, max iterations, replan" (the schema declares onlymaxIterations).memorynames "short-term" (removed, with a refusal and guidance on the key). Nobody owns them yet, so they are noted, not filed.memoryandlifecycleare offered as composites while their describes say[EXPERIMENTAL — not enforced], the reverse of this ledger's not-enforced-yet discipline. That discipline is a reconciliation-ledger convention, not a published contract, and this card does not touch them.Generated by Claude Code