Skip to content

feat(spec): error-code provenance gate + four adjudicated ledger rows - #14091

Merged
os-support-ai merged 3 commits into
mainfrom
claude/issue-13353-error-code-provenance
Sep 1, 2026
Merged

feat(spec): error-code provenance gate + four adjudicated ledger rows#14091
os-support-ai merged 3 commits into
mainfrom
claude/issue-13353-error-code-provenance

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes #13353

Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1 (dev seat; durable attribution — the footer form is create-only).

Clause-②: no — provenance rows change no accept or reject verdict: the registered union is proven byte-identical (248 codes before and after, measured on both trees), ErrorCode parses exactly what it did, and the ledger's admission rules are untouched. The new exports (PROVENANCE_WAIVERS, ProvenanceWaiverSchema, ProvenanceWaiver) are additive data tables read by the new gate. The path limb (packages/spec/src/**) binds regardless, so needs:contract-review is on this PR and the card.

What this delivers — the gate first, the rows as its adjudicated data

Per the triage on #13353 (comment 5478816074, binding): the gate is the deliverable, the rows are the easy half.

1. check:error-code-provenance (packages/spec/scripts/check-error-code-provenance.ts, wired as a spec package script + one lint.yml step): every stamp site of a ledger-REGISTERED code in packages/ non-test source must be listed under the stamping package's own owner key, or carry a recorded PROVENANCE_WAIVERS entry. Scan method is the card's own (three patterns: object-literal code: stamp, .code = assignment, *_CODE constant initializer), sharing the source-text idiom and the comment mask of check:dispatcher-error-vocabulary — populations are disjoint by construction (that gate reports codes OUTSIDE the registered union; this one reports registered codes stamped by a non-listing package). Declared blindness is printed on every run and inherits the sibling gate's: constants not named *_CODE, helper/shorthand indirections, templates, class fields. Waivers are held live in three directions: registeredUnder must still list the code; the waived package must still NOT list it; the scan must still find a site for the pair.

2. The waiver mechanism lives in the ledger file itself, matching the STANDARD_SYNONYM_WAIVERS idiom (schema + recorded reason + stale-guard), because the triage's predicted condition held: the FIRST adjudication below proved a genuine door case, and the two ledger-recorded counter-examples (UPDATE_ID_MISMATCH, FLOW_DISABLED family) needed machine-readable records for the gate to be green at all. 8 entries, all live.

3. Four adjudicated provenance rows (of the card's five candidates). Union unchanged; each row's comment in the ledger records the wire path.

Per-row adjudication (site read, wire path traced, verdict + reason)

Adjudication order per triage: service-datasource first, because its verdict decides the gate design. Line numbers are from origin/main at 8dc22d6 (the worktree base).

1. @objectstack/service-datasource / EXTERNAL_IMPORT_ERROR — DOOR CASE, waiver, NO row. Site: external-datasource-service.ts:333, importNameRefusedError sets err.code/err.status. Wire path traced: importObject has exactly one door in the tree — rest's POST .../external/tables/:remote/import (packages/rest/src/external-datasource-routes.ts, the route's catch), which stamps EXTERNAL_IMPORT_ERROR itself via sendError(res, 400, ...) for EVERY importObject throw and never reads the producer's code; service-datasource/admin-routes.ts does not serve importObject at all (grep: zero call sites outside rest). The rest module's own header records the decision: the code is registered under rest "because a refused federated import is specific to it". The producer's stamp is the declaration shape (resolveThrownHttpError reads it) agreeing with the door by construction — not a second wire emitter. This is exactly the FLOW_DISABLED/UPDATE_ID_MISMATCH class, so per the triage this verdict forced the waiver mechanism into the gate's design.

2. @objectstack/plugin-webhooks / INVALID_REQUEST — ROW (new owner key). Site: webhook-outbox-plugin.ts:399. Wire path: the plugin mounts POST /api/v1/webhooks/redeliver DIRECTLY on the raw Hono app (registerAdminRoutes) and writes the refusal envelope itself — the door IS the stamping package, mounted whenever the http-server service is up (a live server; the #8035 shape). Same raw-app plugin-route door the ledger already accepted for UNIQUE_SCOPE_CONFIRMATION_REQUIRED. Sibling refusals on the route are standard-catalog members needing no row.

3. @objectstack/cloud-connection / FORBIDDEN — ROW (existing key). Site: marketplace-install-local-plugin.ts:1722 (base-tree 1719; drift is line-only), requireInstallCapability answers 403 by c.json on the plugin's own four install/uninstall/reseed/purge routes. Door = producer. The spelling is the #8211-waived synonym: the waiver admits the (code, shadows) pair regardless of package, so the admission gate stays green; the waiver's recorded reason gained one clause naming this row, and the reverse pin in error-code-ledger.test.ts now lists four packages.

4. @objectstack/cli / ENVIRONMENT_NOT_FOUND — ROW (new owner key). Site: commands/serve.ts:4798, the unknown-hostname guard's JSON limb answers 404 with the code by the CLI's OWN middleware on a serving host. Door = producer. Second emitter of a code cloud-connection already registers — provenance, not identity, per the ledger header.

5. @objectstack/trigger-api / INVALID_REQUEST — ROW (existing key). Sites: api-trigger.ts:195 and :199, handleRequest's malformed-body 400s. Wire path: the same package's plugin.ts serves the returned body verbatim (c.json(out.body, out.status)) on the raw-app hooks route — the handler whose sibling refusals (ENQUEUE_FAILED, INVALID_SIGNATURE) this key already registers. Door = producer.

Tree drift since the card's scan (base 881f8d8), triaged the same way: service-automation / FLOW_INPUT_SCHEMA_INVALID (engine.ts — the #10025 producer has since landed; the runtime row's comment already records "registered HERE, not under the engine's package" — waiver), second FLOW_DISABLED/FLOW_NO_START_NODE sites in retryExecution (same pairs, covered by the same waivers), @objectstack/spec / ITEM_LOCKED (metadata-protection.zod.ts shared lock evaluators; the protocol layer is the registered emitter — waiver), and @objectstack/types / VALIDATION_FAILED (validation-failure.ts, the #8016 shared constructor; served under the emitting doors' own registrations — waiver).

Out of scope, per triage: the @objectstack/client / UPLOAD_SESSION_EXPIRED client-synthesised hit. #13353 remains the record of that open ledger-scope question; here it is a waiver entry whose reason states the question is unruled — deliberately NOT a sixth row, and no new card was filed (the triage already routed it: a separate card only if the spec seat wants it answered).

Edits beyond the claim's file surface, each ordered by a gate

  • packages/spec/src/api/error-code-ledger.test.ts — the reverse-direction pin enumerates the packages registering FORBIDDEN; the adjudicated row adds cloud-connection to that list (admission RULES untouched).
  • packages/spec/src/type-alias-convention.pin.test.tscheck:spec-parsed-alias (ADR-0122) went red on the new schema and its own failure text ordered the fix: Iso865 isomorphism pin (input equals infer — four constrained strings, no default/transform), count 835 to 836 with the receipt comment.
  • packages/spec/scripts/check-generated.ts — the reconciliation ledger requires every new check: script classified; added as NO_GENERATOR (pure source audit, nothing to regenerate).
  • .github/workflows/lint.ymlflagged loudly per dispatch: one added step (Error-code provenance guard, beside its sibling gate, deliberately in the job with no paths filter). The workflows touch is unavoidable, verified three ways: no aggregation auto-runs new spec package check scripts (each has an explicit step; check:generated deliberately does NOT run NO_GENERATOR audits), and hosting the repo-wide scan in the spec vitest suite is structurally unsound (turbo hashes per-package inputs, so a cached spec test run stays green on exactly the PR that adds an unlisted stamper elsewhere — the check:cross-package-test-inputs failure class). If enqueue hits token-permission trouble, this file is why.
  • Regenerated artifacts (via check:generated --fix, only the proved-stale set): api-surface, export-origins, declaration-map, docs references, strictness-ledger counts, authorable-surface/json-schema manifests — the diff is exactly the three new exports plus one counted z.object.

Verification (union pass at head da672bb, after the final commit)

  • pnpm --filter @objectstack/spec check:generated — "All 15 generated artifacts are up to date."
  • pnpm --filter @objectstack/spec check:error-code-provenance — self-test "3 patterns and every waiver direction pinned"; real run "OK — every registered-code stamp site is listed under its own owner key or carries a recorded waiver (8 waiver(s), all live)". Scan population at this head: 1959 files, 300 registered-code stamp sites — 285 listed, 15 waived, 0 violations.
  • pnpm check:dispatcher-error-vocabulary at da672bb — "OK — 52 unregistered code-stamping site(s), all classified; 1 awaiting a ledger entry (spec: register the dispatcher conformance gate's reported error codes in ERROR_CODE_LEDGER (spec half of the #8087 ruling) #8846)" (stays green, as the triage required).
  • Full spec suite (at 7e76f29; da672bb changes only one test file, re-run below): "Test Files 448 passed | 1 skipped; Tests 12036 passed | 1 skipped". Directly-affected files re-run at da672bb: 4 files, 43 tests passed.
  • pnpm --filter @objectstack/spec typecheck exit 0 (scripts program covers both new files — proven by listFiles, 2 hits); check:test-typecheck "OK" after the pin edit.
  • Reverse verification, committed-state ablation, both legs through the real gate: removing the plugin-webhooks row reddened it naming exactly webhook-outbox-plugin.ts:399; removing the EXTERNAL_IMPORT_ERROR waiver reddened it naming exactly external-datasource-service.ts:333; both mutations proven on disk by anchored grep counts (8 to 7, 4 to 3), both restores proven by empty git diff HEAD plus blob hash equal to HEAD; restored tree green. Direction observed: red, as predicted.
  • Also green: check:error-code-casing, check:cross-package-test-inputs, check-comment-mask-adoption, check-self-test-wired, check-self-test-workflow-commands, check-ci-filter-parity, check:required-contexts, check:pnpm-filter-targets, check:test-source-alias, check:changeset-gate-self-tests plus the four changeset gates individually, check:merge-driver, check:published-files (the script does not ship; the ledger rows do, via src/**/*.zod.ts), check:type-check-coverage, check:spec-parsed-alias ("836 pinned isomorphic ... OK"), nul-bytes, and the docs/pm families derived by dispatch-gates.mjs. check-test-completeness and check:type-check-debt exit 3 = NOT MEASURED by their own declared contract (they need a CI turbo log / a re-measure input) — recorded as such, not as green or red.

Changeset: patch for @objectstack/spec (rows + additive exports ship in the package; the gate script does not). Not breaking, so no ADR-0087 marker is owed — check-adr-0087-registration green.

Generated by Claude Code


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 19 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json def3b7c43425a5a7b3ea14afaacb50b4e9439491.

1 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/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 — 128 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 def3b7c43425a5a7b3ea14afaacb50b4e9439491packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6c82d5a915df19d1dde5ec9ee7f4cbab8d2ab30c — the merge of head da672bb561d2e5dba9631eeca4d2a2835a9cbbbf into base def3b7c43425a5a7b3ea14afaacb50b4e9439491, 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 6c82d5a915df19d1dde5ec9ee7f4cbab8d2ab30c && git checkout 6c82d5a915df19d1dde5ec9ee7f4cbab8d2ab30c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin def3b7c43425a5a7b3ea14afaacb50b4e9439491 da672bb561d2e5dba9631eeca4d2a2835a9cbbbf && git checkout -B drift-repro def3b7c43425a5a7b3ea14afaacb50b4e9439491 && git merge --no-ff da672bb561d2e5dba9631eeca4d2a2835a9cbbbf

node scripts/docs-audit/affected-docs.mjs --json def3b7c43425a5a7b3ea14afaacb50b4e9439491

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

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

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

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

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

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

    @objectstack/runtime:test:  FAIL  src/batch-row-http-status-real-driver.integration.test.ts > [#8570] a batch row carries the status its producer DECLARED — real driver > the REAL validator: the card'
      ↳ 失败原因: @objectstack/runtime:test: Error: Test timed out in 5000ms.
    @objectstack/runtime:test:  FAIL  src/package-duplicate-adopt-org-scope.integration.test.ts > #7819 tier 2 — duplicatePackage must copy the source’s env-wide rows > the premise, measured: a no-org pub
      ↳ 失败原因: @objectstack/runtime:test: Error: Test timed out in 5000ms.
    @objectstack/runtime:test:  FAIL  src/package-revert-commit-attribution-org-scope.integration.test.ts > #7860 — a revert commit is attributed to what it reverted, not to who asked > an org caller reve
      ↳ 失败原因: @objectstack/runtime:test: Error: Test timed out in 5000ms.
    @objectstack/runtime:test:  FAIL  src/package-revert-commit-attribution-org-scope.integration.test.ts > #7860 — a revert commit is attributed to what it reverted, not to who asked > an org caller reve
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    @objectstack/runtime:test:  FAIL  src/package-revert-commit-attribution-org-scope.integration.test.ts > #7860 — a revert commit is attributed to what it reverted, not to who asked > mirror — the no-or
      ↳ 失败原因: @objectstack/runtime:test: AssertionError: expected [ …(2) ] to deeply equal []
    @objectstack/runtime:test:  FAIL  src/package-revert-commit-attribution-org-scope.integration.test.ts > #7860 — a revert commit is attributed to what it reverted, not to who asked > mirror — the no-or
      ↳ 失败原因: @objectstack/runtime:test: Error: Test timed out in 5000ms.
    @objectstack/runtime:test:  FAIL  src/standalone-stack.test.ts > createStandaloneStack — default datasource declared, built via the shared factory (ADR-0062 D1) > file: → declares driver "sqlite" with
      ↳ 失败原因: @objectstack/runtime:test: Error: Test timed out in 60000ms.
    @objectstack/runtime:test:  FAIL  src/sandbox/undeclared-field-write-driver-split.integration.test.ts > #4271 an undeclared field written by an L2 body — the real runtime split > SQL driver (better-sq
      ↳ 失败原因: @objectstack/runtime:test: Error: Test timed out in 30000ms.
    @objectstack/runtime:test:  FAIL  src/sandbox/undeclared-field-write-driver-split.integration.test.ts > #4271 an undeclared field written by an L2 body — the real runtime split > SQL driver (better-sq
      ↳ 失败原因: @objectstack/runtime:test: AssertionError: expected [ …(2) ] to deeply equal []
    

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

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

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

  • src/batch-row-http-status-real-driver.integration.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • src/package-duplicate-adopt-org-scope.integration.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • src/package-revert-commit-attribution-org-scope.integration.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • src/standalone-stack.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • src/sandbox/undeclared-field-write-driver-split.integration.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

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

分诊清单:

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

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

Copy link
Copy Markdown
Collaborator Author

Queue-failure triage (run 33472997997, Test Core (4/6)): standing down as not-this-PR's, re-queueing once.

  • Every failing test lives in @objectstack/runtime — a package this 17-file diff does not touch, and with no mechanism to reach: the ledger rows are union-invariant (248 codes before/after, proven in the PR body), the new script is a lint-side audit, and the workflow touch adds one lint step.
  • 7 of 9 failures are plain timeouts (5s/30s/60s budgets). The two AssertionError: expected [ …(2) ] to deeply equal [] hits are both in files where sibling tests timed out mid-run — the leftover-rows contamination shape of an interrupted teardown, not an independent behavioral signal.
  • Signature history per the triage bot: first occurrence for these five files in the 24h window, against 10 unrelated queue-build failures in the same window.

Per the merge-queue triage checklist (branch 2/3): one re-queue now. If the rebuild fails again with the same runtime-timeout signature, the second failure is treated as real and gets root-caused, not re-queued.


Generated by Claude Code

Merged via the queue into main with commit 3795c5f Sep 1, 2026
42 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-13353-error-code-provenance branch September 1, 2026 05:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] error-code ledger: five more packages stamp a registered code with no provenance row — the #13254 class, swept

2 participants