Skip to content

fix(spec): widen AnalyticsResultResponseSchema and TriggerFlowResponseSchema data to producer-contract parity - #13184

Merged
os-elon merged 3 commits into
mainfrom
claude/issue-13078-response-schema-contract-parity
Aug 29, 2026
Merged

fix(spec): widen AnalyticsResultResponseSchema and TriggerFlowResponseSchema data to producer-contract parity#13184
os-elon merged 3 commits into
mainfrom
claude/issue-13078-response-schema-contract-parity

Conversation

@os-trump

@os-trumpos-trump commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Fixes#13078

Two packages/spec route response schemas declared a data that was a strict subset of the
contract their route relays. This brings each one up to parity with its producer’s declared
return — the ruled route, not the projection-docblock alternative.

Continuation of a dispatch that terminated mid-work on a host-side quota 429; the predecessor’s
commit is carried forward rather than restarted. Session:
https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4

Why parity, not a projection note

The #6442 precedent is recorded in analytics.zod.ts itself, on the sibling schema it was ruled
for: when the TS contract and the runtime already agree and the schema is the lone outlier, the
schema moves
. Both missing member sets here are served in reality, so a deliberate-projection
note would have documented a refusal of reads the wire really carries — the same thing the #6442
route was rejected for.

What changed

AnalyticsResultResponseSchema.data now declares everything AnalyticsResult
(contracts/analytics-service.ts) declares — fields[].label / format / currency /
percentScale (the ADR-0053 renderer chains) and totals (the ADR-0021 marginal-aggregate
channel). POST /analytics/query ends deps.success(await analyticsService.query(body, ctx)),
so that contract IS the body under data.

TriggerFlowResponseSchema.data now declares everything AutomationResult
(contracts/automation-service.ts) declares — the paused third state of the #9378 / #9510 trigger
contract (status / runId / screen), the closed code classification, the friendly terminal
messages and the run summary. Both trigger routes end deps.success(result) with that same
contract.

The screen payload’s two nested shapes are declared module-local (screenFieldSpecShape,
screenSpecShape) rather than exported: ScreenSpec / ScreenFieldSpec on the contract are
already THE names for those shapes, so a second exported name would be the permanent synonym
ADR-0122 D3 forbids and a new dual-source export. This follows the cubeMetaMemberShape treatment
already in analytics.zod.ts.

Zero wire change. This is an accept-set widening: every payload that parsed before still
parses. What changes is that the served keys these schemas used to silently strip — a paused run’s
runId and screen, a measure’s label — now survive a parse.

The export gap the card names

AnalyticsResultResponse had no exported type at all, so a consumer could not name the route’s
response even once the schema said the right thing. It is now exported exactly as every sibling in
the file is (z.input plus a Parsedz.infer), and the shrink-only import-surface ratchet entry
api/AnalyticsResultResponse — no type export is discharged by tooling
(build-docs.ts --update-import-baseline), because a stale line stays available to excuse the next
missing export.

protocol.zod.ts keeps its module-local z.infer alias deliberately, and it is NOT the export gap:
that alias sits in a block of four (AnalyticsQueryRequest, AnalyticsResultResponse,
GetAnalyticsMetaRequest, GetAnalyticsMetaResponse) that type the AnalyticsProtocol interface
with post-parse shapes, and the first of them coexists with an exported AnalyticsQueryRequest
in analytics.zod.ts already. Rewiring it would change that interface’s declared types from parsed
to input — a different change, out of this card’s scope.

Pins, both directions

  • Compile-time parity, exported so it is really compiled: AnalyticsResultMatchesContract in
    spec/api/analytics.test.ts and TriggerFlowDataMatchesContract in
    spec/api/automation-api.zod.test.ts bind each schema’s data member to its contract with a
    type-identity assert. Narrow either side alone and it goes red.
  • Runtime preservation, which is the half that matters for a stripping schema: realistic payloads
    including the paused-flow status / runId / screen triple, an object-form screen pause, the
    renderer-chain field metadata and the grand-total totals entry.
  • Negative pins matching what this file family’s strictness posture actually does: a percentScale
    / status / code outside its closed vocabulary is rejected, and so is a screen missing its
    required nodeId.
  • Reverse verification, direction predicted before running: renaming totals to totalsMUTANT in
    the schema (mutation confirmed on disk by anchor grep counts 1 to 0 and 0 to 1, plus a changed
    blob hash) turns check:test-typecheckred at src/api/analytics.test.ts with 4 type errors,
    the parity pin. Restored by git checkout HEAD -- PATH, proven by the restored blob hash matching
    the HEAD blob exactly and git diff HEAD empty.

The #12104 client bindings are NOT retargeted

client.analytics.query and client.automation.trigger still return the producer contracts
(BaseResponse intersected with the contract). Only the docblocks and the measurement comments are
refreshed, because they asserted a defect that no longer exists. Two @ts-expect-error pins in
return-type-precision.test.ts had to be re-judged rather than re-spelled: their suppressions would
now be UNUSED (TS2578) precisely because the gap they pinned is closed, so they are replaced by the
stronger equality — the data member of TriggerFlowResponse equals AutomationResult. The pin
file still passes: Test Files 2 passed (2) · Tests 9 passed (9).

Changeset

@objectstack/specminor — an additive widening of published response schemas plus two new
exported types is a feature-level surface addition, not a fix to behaviour; nothing narrows, so it
is not major. @objectstack/clientpatch — comments and docblocks only. check:changeset-no-major
and check:empty-changeset both green.

Verification

All gate results below were measured on 9ae99605f, the branch head, with exit codes captured
before any pipe. Gate family derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
with no hand-fed paths: 39 matched families, plus the convention-triggered ones for the
adds-or-edits-a-test-file kind.

  • Full workspace build, pnpm exec turbo run build --concurrency=2 over ./packages/* and
    ./packages/*/*Tasks: 70 successful, 70 total.
  • pnpm --filter @objectstack/spec exec vitest run on the two spec pin files —
    Test Files 2 passed (2) · Tests 68 passed (68).
  • pnpm --filter @objectstack/client exec vitest run on the two client pin files —
    Test Files 2 passed (2) · Tests 9 passed (9).
  • typecheck and check:test-typecheck green for both packages. The pin files are proven inside
    the checked program: tsc -p tsconfig.test.json --listFiles names both, and none of the 263
    frozen TEST_DEBT errors are in files this PR touches.
  • pnpm --filter @objectstack/spec check:generated — 14 of 14 artifacts current after
    regeneration; the regenerated artifacts (reference docs, strictness ledger api/ 444 to 448,
    import-surface baseline) are in the diff as their own commit.
  • pnpm check:type-check-debt31 ledger entr(ies) re-measured in 243.4s, 1570 raw tsc error(s) total, none above its recorded number. surplus: none.
  • pnpm lint (repo-wide eslint . --no-inline-config) — exit 0, so no narrowing was needed.
  • 36 of the 39 derived gate families ran green. Three returned PREREQUISITE NOT MET on an unbuilt
    workspace and were re-run green after the full build (check:dual-build-cjs-loads,
    check:skill-examples, check-dev-prereqs.mjs). Two are not measurable in this container and are
    recorded as NOT MEASURED rather than as passes: scripts/pm/check-half-states.mjs (exit 3, no
    GitHub credential — a PM-board sweep unrelated to this diff) and scripts/check-test-completeness.mjs
    (a CI-only invocation that needs a turbo test log argument).

Landing posture

Implementation tier was opus under the recorded quota-exhaustion exemption, with
needs:contract-review as the compensating control.

Clause-② applies on both legs, so this PR stays draft and is parked for the review chain — not
marked ready, not auto-merged, not enqueued.

…eSchema data to producer-contract parity
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4
…surface baseline
Tooling output of the schema widening, not hand-written:
- `gen:docs` re-renders the analytics/automation-api reference pages with the
newly declared members and the now-importable `AnalyticsResultResponse`.
- `gen:strictness-ledger` moves the `api/` unknown-key site count 444 -> 448.
- `--update-import-baseline` discharges the shrink-only ratchet entry
`api/AnalyticsResultResponse — no type export`: the gap the card names is
closed, and a stale line would stay available to excuse the next one.
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Aug 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/client, @objectstack/spec, touching 10 documentable anchor(s). ⚠️3 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/docs-import-surface.baseline.json, packages/spec/export-origins/api.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/client-sdk.mdx(via automation.execute (sdk), automation.trigger (sdk))
  • content/docs/api/data-api.mdx(via AnalyticsResultResponseSchema (symbol))
  • content/docs/api/declarative-endpoints.mdx(via /automation/:name/trigger (route))
  • content/docs/api/plugin-endpoints.mdx(via /automation/:name/trigger (route), /automation/trigger/:name (route))
  • content/docs/automation/flows.mdx(via automation.execute (sdk), /automation/:name/trigger (route))
  • content/docs/protocol/kernel/http-protocol.mdx(via /automation/:name/trigger (route))
  • content/docs/ui/actions.mdx(via /automation/:name/trigger (route))
What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/docs-import-surface.baseline.json, packages/spec/export-origins/api.json) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 129 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 dc729f3585830b8c71550245e6d883afbfcea94epackageMentionDocs.

Which tree this was computed on

This run read content/docs from f03f418d3c51e758e6af51ed0a973d9dabf3933d — the merge of head 9ae99605fc02392cf3989cfa57826fc7e2cdc984 into base dc729f3585830b8c71550245e6d883afbfcea94e, 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 f03f418d3c51e758e6af51ed0a973d9dabf3933d && git checkout f03f418d3c51e758e6af51ed0a973d9dabf3933d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dc729f3585830b8c71550245e6d883afbfcea94e 9ae99605fc02392cf3989cfa57826fc7e2cdc984 && git checkout -B drift-repro dc729f3585830b8c71550245e6d883afbfcea94e && git merge --no-ff 9ae99605fc02392cf3989cfa57826fc7e2cdc984
node scripts/docs-audit/affected-docs.mjs --json dc729f3585830b8c71550245e6d883afbfcea94e

⚠️ 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 dc729f3585830b8c71550245e6d883afbfcea94e → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-trumpClaude

Copy link
Copy Markdown
CollaboratorAuthor

Attribution + a platform reading, because the body cannot carry the first one right now.

Measured on this PR, 2026-08-29, three writes each read back from the rendered page and from the API:

  1. create_pull_request through the MCP GitHub server HTML-escapes apostrophes and double quotes in the body it stores — the first version of this body read back with producer&#39;s in the stored bytes, and the page rendered that entity literally. Fix: author PR bodies with typographic apostrophes and quotes, or avoid them; do not assume what you sent is what is stored.
  2. Every subsequent update_pull_request PATCH strips the trailing attribution footer — the thematic break plus the italic Generated by line — from the stored body. Reproduced twice: with the footer directly after the break, and with a blank line between them. The rest of the body survived byte-for-byte both times, so this is not truncation; it is the trailing block specifically.
  3. A comment body does not get either treatment — apostrophes and the footer both survive verbatim here, which is why the attribution is being recorded in this comment.

Consequence for anyone editing this PR body later: the session URL is written into the body prose (under the opening paragraph) as the durable copy, because the footer form cannot survive an edit. Do not try to re-paste the footer into the body — it will be removed again.


Generated by Claude Code

@os-trumpClaude

Copy link
Copy Markdown
CollaboratorAuthor

Standing down on this failure — it is not this PR's (domain:spec seat, session_01LpRNHxWZgSUgVnFT9mQQo4).

Landing posture is unchanged and independent of this: the PR is DRAFT and parked for contract review (see the parking note on #13078). Green here hands the reviewer a clean PR; it does not authorize this seat to land it.


Generated by Claude Code

@os-elon
os-elon added this pull request to the merge queueAug 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33248906874 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL test/serve-publishes-bound-port.e2e.test.ts > #13062 `os serve --port 0` — the request that can never be the answer > announces the BOUND port on all three channels, and ↳ 失败原因: @objectstack/cli:test: Error: ENOENT: no such file or directory, open '/tmp/os-bound-port-home-Wsho5b/runtime.env_local.json'
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 18 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit e22158fAug 29, 2026
59 of 61 checks passed
@os-elon
os-elon deleted the claude/issue-13078-response-schema-contract-parity branch August 29, 2026 12:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

3 participants

@os-trump@os-elon@claude