Skip to content

spec(api): make ApiEndpoint.target optional; publish gate holds the flow requirement - #11290

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-10338-endpoint-target-optional
Aug 23, 2026
Merged

spec(api): make ApiEndpoint.target optional; publish gate holds the flow requirement#11290
os-sam merged 2 commits into
mainfrom
claude/issue-10338-endpoint-target-optional

Conversation

@claude

@claudeclaudeBot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes#10338

Implements the maintainer ruling of 2026-08-23 (issue comment, verbatim 「其他同意你的意见」 adopting recommendation A): ApiEndpoint.target becomes optional in the vocabulary; the publish gate requires it for type: 'flow' — an object_operation author 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:171target: z.string(), required, no .optional()/.default(). Confirmed.
  • packages/spec/src/api/endpoint-publish-gate.tstargetGate — requires target for flow only (if (!endpoint.target)), never reads it for object_operation. Confirmed.
  • Executor/OpenAPI split — packages/runtime/src/endpoint-executor.tsplanEndpointTarget reads endpoint.target only in the flow branch; packages/rest/src/openapi-endpoints.ts likewise (if (!endpoint.target) on the flow branch only). Confirmed. Every consumer is a truthiness check, so undefined behaves 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:

  • Scripted parse of every in-tree module-exported ApiEndpoint declaration through the rebuilt schema: 4 parsed, 0 rejected (examples/app-showcase allApis ×2, qa/dogfood endpoint-policy-fixture ×2 — all carrying string targets except the swept task-feed example).
  • Full @objectstack/spec suite (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 missing target"), 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

  • Vocabulary (endpoint.zod.ts): target is .optional(); the .describe() now states the per-type truth — REQUIRED at publish for type: 'flow', UNREAD for object_operation (do not write it there).
  • Publish gate (endpoint-publish-gate.ts): unchanged by design — targetGate already refuses !endpoint.target for flow, which now also covers the newly-expressible omitted key. New pins:
    • gate refuses a flow endpoint that OMITS target, at issue path apis.0.target, message "names no target flow" (apis-publish-gates.test.ts);
    • gate ACCEPTS an object_operation endpoint with no target (apis-publish-gates.test.ts — the pin that goes red if required-ness is restored);
    • vocabulary parses an object_operation without the key (endpoint.test.ts).
  • code+status pinned case (ruling clause ②): the publish gate's EndpointGateIssue is 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 (planEndpointTargetunsupportedAnswer), so the pin asserting bothcode and status is there: a flow endpoint with target omitted answers 501 with error.code === 'NOT_IMPLEMENTED', message "names no target flow", conformant envelope, nothing delegated (endpoint-executor.test.ts).
  • Old-contract fixture replaced (metadata-type-api-registration.test.ts): the "refuses a body missing target" 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 — at objectParams for object_operation, at target for flow.
  • Corpus sweep — every in-tree object_operation declaration teaching the dead key:
    • examples/app-showcase/src/system/apis/index.ts — dropped target: 'showcase_task' (comment explains).
    • examples/app-showcase/test/gap-fill.test.ts — the object-existence check now reads objectParams.object (what the executor delegates on) instead of the unread target.
    • content/docs/api/declarative-endpoints.mdx — example drops the key; the "target is 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 lists target as optional with the new description.
    • Left in place, deliberately: packages/qa/dogfood/test/fixtures/endpoint-policy-fixture.ts still writes target on two object_operation fixtures — they parse fine and are outside this card's declared file surface (packages/qa); they are fixtures, not shipped teaching. skills/objectstack-api/SKILL.md also still teaches the key — skills are a governed surface outside this card's file surface; filed separately (see report).
  • Changeset: .changeset/lazy-pugs-shake.md, @objectstack/spec minor. Non-breaking (widening), so no ADR-0087 disposition marker is required; check-adr-0087-registration green.

Reverse verification (predicted directions stated first)

  1. Prediction: restoring required-ness turns the new acceptance pins red. Mutation: re-add required target in endpoint.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 Zod invalid_type at apis.0.target instead 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 --absent preflight was inconclusive — the marker target: z.string().optional().describe is 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.
  2. Prediction: ablating the flow-target gate check turns the flow-refusal pins red (gate test), while the runtime 501 pin stays green (it pins the runtime counterpart, not the gate). Mutation: short-circuit if (!endpoint.target) in targetGate, 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 501 code+status pin exercises the runtime counterpart, not the gate. Unique marker false && !endpoint.target: 1 src occurrence during mutation, 0 after restore. Dist note: the mutation-leg preflight found the marker only in sourcemaps — esbuild constant-folds false && … 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 --absent preflight passed over all 209 built files.

Both legs: mutation proven on disk (anchored grep of the mutated text), rebuilt via pnpm --filter @objectstack/spec build with scripts/ablation-dist-preflight.mjs marker checks on mutation AND restore legs, restore via git checkout from 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 commit 9ff4da688 then committed verbatim).

  • @objectstack/spec full 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/runtime full suite: 184 files / 2711 tests passed (Tests 2711 passed (2711)).
  • @objectstack/rest full suite: 138 files / 2201 tests passed.
  • @objectstack/metadata targeted (publish-endpoint-gate, endpoint-matcher, match-endpoint, stored-envelope): 4 files / 103 tests passed; @objectstack/metadata-protocol targeted: 2 files / 73 tests passed.
  • @objectstack/example-showcase full suite (edited gap-fill included): 25 files / 367 tests passed.
  • turbo run typecheck over spec/runtime/rest/metadata/metadata-protocol/example-showcase: 64 tasks successful (each named package's tsc --noEmit echoed).
  • 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").
  • Derived gate families (dispatch-gates at 9ff4da688, --repo asserted, 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-coverage OK, check:type-check-debt --re-measure OK (none above recorded), check:skill-examples ✅ 227 prose examples, check-dev-prereqs ✓ after full ./packages/* build.
  • Zero-migration script: 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

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 2 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/declarative-endpoints.mdx(via ApiEndpointSchema (symbol), object_operation (literal))
  • content/docs/getting-started/quick-reference.mdx(via object_operation (literal))
  • content/docs/protocol/kernel/http-protocol.mdx(via ApiEndpointSchema (symbol), object_operation (literal))
What this run could not see

Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 764dbbccbd00bbf61936bf128be524e8d8ad1bedpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 7c63d692d1850bacbc19d4c48e7b15ca7f239f7e — the merge of head 9ff4da688eeae4f27aa672a7e600da86b56f758b into base 764dbbccbd00bbf61936bf128be524e8d8ad1bed, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 764dbbccbd00bbf61936bf128be524e8d8ad1bed → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 23, 2026
@os-sam
os-sam marked this pull request as ready for review August 23, 2026 09:26
@os-sam
os-sam added this pull request to the merge queueAug 23, 2026
Merged via the queue into main with commit d2619fdAug 23, 2026
55 of 59 checks passed
@os-sam
os-sam deleted the claude/issue-10338-endpoint-target-optional branch August 23, 2026 09:44
os-zhuang pushed a commit that referenced this pull request Aug 24, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationteststooling

Projects

None yet

2 participants

@os-sam@claude