Uh oh!
There was an error while loading. Please reload this page.
spec(api): make ApiEndpoint.target optional; publish gate holds the flow requirement - #11290
Conversation
…irement (#10338) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RadETjNRLALFLhFA3xehZP
…ngeset (#10338) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RadETjNRLALFLhFA3xehZP
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not seeCoarse fallback — 126 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 7c63d692d1850bacbc19d4c48e7b15ca7f239f7e && git checkout 7c63d692d1850bacbc19d4c48e7b15ca7f239f7e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 764dbbccbd00bbf61936bf128be524e8d8ad1bed 9ff4da688eeae4f27aa672a7e600da86b56f758b && git checkout -B drift-repro 764dbbccbd00bbf61936bf128be524e8d8ad1bed && git merge --no-ff 9ff4da688eeae4f27aa672a7e600da86b56f758b
node scripts/docs-audit/affected-docs.mjs --json 764dbbccbd00bbf61936bf128be524e8d8ad1bed
|
Uh oh!
There was an error while loading. Please reload this page.
…ect_operation example (#11291) `ApiEndpoint.target` became optional in #10338 (landed as #11290): it is required at publish only for `type: 'flow'` and is UNREAD for `type: 'object_operation'`, which is addressed by `objectParams.object` / `.operation`. Nothing checks a `target` written beside them against `objectParams.object`, so the `leadFeed` example was teaching a dead string — the exact AI-authoring trap #10338 removed. The in-tree examples and the protocol/getting-started docs were swept in #11290; the published skill was out of that card's file surface. The example now omits the key. No explanatory note was added: the published skills token ratchet had zero headroom on this file (ceiling 6348, file 6348), and the card made that note conditional on budget. The gate-description lines (~212-214) already state the per-type rule ("an `object_operation` needs both `objectParams` halves; a `flow` needs a `target`") and stay unchanged. Lower the file's ratchet ceiling 6348 -> 6342 to lock in the saving, per the ratchet's own documented discipline ("a ceiling may be LOWERED by any PR that shrinks its file ... always legitimate and encouraged"). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_5213b871-5164-5bc3-8874-28b336bbcd40
Fixes#10338
Implements the maintainer ruling of 2026-08-23 (issue comment, verbatim 「其他同意你的意见」 adopting recommendation A):
ApiEndpoint.targetbecomesoptionalin the vocabulary; the publish gate requires it fortype: 'flow'— anobject_operationauthor stops writing a dead string. The ruling is the authorization to edit the #5040 §0-frozen vocabulary for this key.Premise readings (re-verified on origin/main at 064d484)
packages/spec/src/api/endpoint.zod.ts:171—target: z.string(), required, no.optional()/.default(). Confirmed.packages/spec/src/api/endpoint-publish-gate.tstargetGate— requirestargetforflowonly (if (!endpoint.target)), never reads it forobject_operation. Confirmed.packages/runtime/src/endpoint-executor.tsplanEndpointTargetreadsendpoint.targetonly in theflowbranch;packages/rest/src/openapi-endpoints.tslikewise (if (!endpoint.target)on the flow branch only). Confirmed. Every consumer is a truthiness check, soundefinedbehaves exactly as''did.Zero-migration measurement (ruling premise clause)
Optional-izing
z.string()→z.string().optional()is a pure widening: the accepted-value set strictly grows, so no previously valid value can be rejected. Measured, not just argued:ApiEndpointdeclaration through the rebuilt schema: 4 parsed, 0 rejected (examples/app-showcaseallApis×2, qa/dogfoodendpoint-policy-fixture×2 — all carrying stringtargets except the swept task-feed example).@objectstack/specsuite (all inline endpoint fixtures included): 11112/11113 passed on first run; the single failure was a pin of the OLD required-ness (metadata-type-api-registration.test.ts"refuses a body missingtarget"), replaced per fixture triage below. 108/108 green on the three touched files after.No stored-row corpus exists in-tree; stored rows carry string
targets written under the required-era schema, which the widened schema accepts by construction. Premise holds — no migration, no ADR-0087 entry (nothing an author could write is removed; the change is acceptance-widening plus a gate that already existed).What changed
endpoint.zod.ts):targetis.optional(); the.describe()now states the per-type truth — REQUIRED at publish fortype: 'flow', UNREAD forobject_operation(do not write it there).endpoint-publish-gate.ts): unchanged by design —targetGatealready refuses!endpoint.targetforflow, which now also covers the newly-expressible omitted key. New pins:target, at issue pathapis.0.target, message "names no target flow" (apis-publish-gates.test.ts);object_operationendpoint with notarget(apis-publish-gates.test.ts— the pin that goes red if required-ness is restored);object_operationwithout the key (endpoint.test.ts).code+statuspinned case (ruling clause ②): the publish gate'sEndpointGateIssueis issue-shaped (path + message) by contract — no ADR-0112 envelope exists at the spec layer. The envelope for this refusal lives at the gate's declared runtime counterpart (planEndpointTarget→unsupportedAnswer), so the pin asserting bothcodeandstatusis there: a flow endpoint withtargetomitted answers 501 witherror.code === 'NOT_IMPLEMENTED', message "names no target flow", conformant envelope, nothing delegated (endpoint-executor.test.ts).metadata-type-api-registration.test.ts): the "refuses a body missingtarget" pin asserted the vocabulary-level refusal this ruling removes. Replaced with the new-contract pin: a headless body parses at the shape door and is refused by the GATE — atobjectParamsforobject_operation, attargetforflow.object_operationdeclaration teaching the dead key:examples/app-showcase/src/system/apis/index.ts— droppedtarget: 'showcase_task'(comment explains).examples/app-showcase/test/gap-fill.test.ts— the object-existence check now readsobjectParams.object(what the executor delegates on) instead of the unreadtarget.content/docs/api/declarative-endpoints.mdx— example drops the key; the "targetis required on every entry" paragraph rewritten to the per-type contract.content/docs/protocol/kernel/http-protocol.mdx,content/docs/getting-started/quick-reference.mdx— examples drop the key with a one-line comment.content/docs/references/api/endpoint.mdx— regenerated (gen:schema && gen:docs), now liststargetas optional with the new description.packages/qa/dogfood/test/fixtures/endpoint-policy-fixture.tsstill writestargeton twoobject_operationfixtures — they parse fine and are outside this card's declared file surface (packages/qa); they are fixtures, not shipped teaching.skills/objectstack-api/SKILL.mdalso still teaches the key — skills are a governed surface outside this card's file surface; filed separately (see report)..changeset/lazy-pugs-shake.md,@objectstack/specminor. Non-breaking (widening), so no ADR-0087 disposition marker is required;check-adr-0087-registrationgreen.Reverse verification (predicted directions stated first)
targetinendpoint.zod.ts(z.string()), rebuild spec, run the three spec test files. Observed: 4 pins red, exactly the predicted set —parses an object_operation endpoint that omits target(endpoint.test.ts), the gate acceptance pin, the gate omit-refusal pin (red because the refusal became a Zodinvalid_typeatapis.0.targetinstead of the gate's message), and the headless-body pin — 104/108 others green. Disk proof: 0 occurrences of the.optional()spelling after mutation, 1 after restore. Honesty note: the dist--absentpreflight was inconclusive — the markertarget: z.string().optional().describeis NOT unique in spec (ui/action.zod, automation/state-machine.zod, api/odata.zod also spell it) — but the measured surface (spec's own tests) imports the schema by relative src path, so the mutation provably reached it; both legs were rebuilt regardless.if (!endpoint.target)intargetGate, rebuild, run. Observed: 4 gate pins red as predicted — the empty-target pin, the omit-target pin, the three-rejections aggregate (one FEWER rejection: the diagnostics-shrink direction), and the flow half of the headless pin — while the runtime endpoint-executor suite stayed GREEN 50/50, demonstrating the 501code+statuspin exercises the runtime counterpart, not the gate. Unique markerfalse && !endpoint.target: 1 src occurrence during mutation, 0 after restore. Dist note: the mutation-leg preflight found the marker only in sourcemaps — esbuild constant-foldsfalse && …out of executable output — so the dist proof for that leg rests on the sourcemaps + rebuild plus the src resolution of the measured tests; the restore-leg--absentpreflight passed over all 209 built files.Both legs: mutation proven on disk (anchored grep of the mutated text), rebuilt via
pnpm --filter @objectstack/spec buildwithscripts/ablation-dist-preflight.mjsmarker checks on mutation AND restore legs, restore viagit checkoutfrom the committed state.Verification
All local verification below ran on the final tree; final commit
9ff4da688(the last two runs' logs and the gate re-derivation cite it; earlier suite runs executed on a byte-identical tree whose only uncommitted files were the ones commit9ff4da688then committed verbatim).@objectstack/specfull suite: first run 11112/11113 (the 1 red was the old required-target pin, replaced); after replacement the three touched files: 108/108 (Test Files 3 passed).@objectstack/runtimefull suite: 184 files / 2711 tests passed (Tests 2711 passed (2711)).@objectstack/restfull suite: 138 files / 2201 tests passed.@objectstack/metadatatargeted (publish-endpoint-gate, endpoint-matcher, match-endpoint, stored-envelope): 4 files / 103 tests passed;@objectstack/metadata-protocoltargeted: 2 files / 73 tests passed.@objectstack/example-showcasefull suite (edited gap-fill included): 25 files / 367 tests passed.turbo run typecheckover spec/runtime/rest/metadata/metadata-protocol/example-showcase: 64 tasks successful (each named package'stsc --noEmitechoed).pnpm --filter @objectstack/spec check:generated: 1 stale artifact (check:docs), regenerated via--fix; all others up to date (its own report line: "1 of 14 artifact(s) stale").9ff4da688,--repoasserted, set unchanged from first derivation): doc gates (anchors/authoring/audit-scope/redirects/frontmatter/security-posture via lint filter runs at CI),check:quick-reference-counts,check:role-word,check:published-readme-links,check:merge-driver,check:objectui-changeset,check:examples-live-imports,check:changeset-gate-self-tests,check-adr-0087-registration,check-changeset-no-major,check-empty-changeset,check-ci-filter-parity,check-plugin-teardown-shape,check-doc-frontmatter,check-nul-bytes,docs-audit/check-affected-docs,check-cross-package-test-inputs,check:spec-parsed-alias,check:slot-lookup,check:test-source-alias,check:type-source-resolution,check:published-files,check:query-options-erasure,check:engine-double-contract,check:where-matcher,check:empty-state,check:variant-docs,check:liveness+check:strictness-ledger(inside check:generated) — all exit 0. Convention-triggered:check:type-check-coverageOK,check:type-check-debt --re-measureOK (none above recorded),check:skill-examples✅ 227 prose examples,check-dev-prereqs✓ after full./packages/*build.MEASUREMENT: 4 parsed, 0 rejected of 4 in-tree declarations.Not run locally (CI-owned): repo-wide
pnpm lint(eslint sweep) and the full farm — deliberate narrowing per the seat's standing verification-scope rule; CI runs the farm on the PR.Generated by Claude Code