Skip to content

Publish refusals: declare failed[].issues + seedApplied.issues, then trim error to a headline - #10895

Merged
os-elon merged 2 commits into
mainfrom
claude/issue-10524-publish-error-headline
Aug 21, 2026
Merged

Publish refusals: declare failed[].issues + seedApplied.issues, then trim error to a headline#10895
os-elon merged 2 commits into
mainfrom
claude/issue-10524-publish-error-headline

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#10524

Clause-②: yes — step 1 lands on packages/spec/src/api/protocol.zod.ts (contract-review tier, fable).

What changed — declare-then-trim, in that order

Step 1 (declare, spec — additive):

  • PublishPackageDraftsResponseSchema.failed[] elements declare issues[] as z.array(RuntimeAuthoringIssueSchema).optional() — the key the producer has emitted since [finding] metadata-protocol's batch verbs still put caught error text on client-facing payloads — the 8 producers option C did not reach #8333 (...(Array.isArray(e?.issues) ? { issues: e.issues } : {}) on the causal element) but which every declared parse stripped and the SDK type could not see. Declared measured: the real emitted element is the six-key RuntimeAuthoringIssueSchema (rule/path/where/message/hint/severity), not the card's { path, message } guess — declaring the guess would have stripped rule/hint at the parse door.
  • seedApplied declares issues[] ({ path, message, code? }, the zodIssuesToMetadataIssues entry — a zod-parse vocabulary, deliberately not the six-key rule shape). Without this, trimming site 2's message would have deleted the author's per-key detail from the wire entirely: both catches that surface seedRequestValidationError onto seedApplied kept only the message. Both catches now thread e.issues beside the headline (in-batch applySeedBodies, guarded by declaresClientRefusal; route-level fallback in packages/runtime/src/domains/packages.tsissues is structured authoring feedback only the declared 422 attaches, per the A raw driver code (SQLITE_ERROR) still reaches client-facing payloads through the batch verbs' code limb — the sibling field #8333 did not cover #8441 measurement, so nothing undeclared routes around the withhold).

Step 2 (trim, producers):

The four sites, measured at 4d7c564 — which moved and which did not

#siteverdict
1runtime-authoring-gate.ts INVALID_METADATA/422trimmed — every wire face its message lands on (single-item 422 details.issues, failed[].issues) now carries the same array structurally
2protocol.tsseedRequestValidationErrortrimmed + threaded — its issues were dropped at both catches before; now declared and threaded, so the trim loses nothing
3saveMetaItem DESTRUCTIVE_CHANGE/409untouched, argued out — different refusal class (force-confirm flow, remedy-bearing message, its own {code, field?, message} vocabulary). Filed as #10886
4saveMetaItem spec-validation INVALID_METADATA/422 (protocol.ts ~:13306) — a fourth site beyond the dispatch table, found by measurementtrial-trimmed, then deliberately reverted — see below. Filed as #10888

The site-4 reversal is a prediction that missed, reported rather than hidden. I first included it under envelope-family consistency; three #8333 GUARD pins went red and they were right: duplicatePackage's failed[].error quotes that message without threading issues (a message-only face), and the pins hold the author's prescription ("Unrecognized key(s) on this view container…", the defineView( spelling) to the string. Trimming there before those faces declare a structured channel violates the very declare-then-trim order this card enforces. The revert carries an in-code comment naming the measurement; #10888 carries the follow-up with the face inventory as step 1.

Evidence — three-way taxonomy, each control named

All runs at commit a77d87d (source tree; the follow-up commit 425890b adds only the changeset and two regenerated artifacts — no source), through the real producers: the broken-CEL approval flow staged as a draft and refused at the batch promote, and the malformed seed body (seeds.0.mode) refused at the loader-request parse.

1. Defect control — red on the pre-fix tree.packages/objectql/src/publish-package-drafts-response-conformance.test.ts "error is a headline: it restates none of the prose issues[] carries". Pre-fix leg (producers + schema restored from origin/main, spec + metadata-protocol dist rebuilt before the read): red with expected '[invalid_metadata] flow/leave_approva…' not to contain 'expression approver does not parse as…' — the card's duplication, measured directly. Post-fix: green. No ablation used where the real defect was measurable.

2. Preserved-behaviour controls — falsified by mutating the fix. "a consumer rendering only error still learns what failed, where, and how many" (headline keeps flow/leave_approval, the located path, [approval-expression-invalid], and the leading count — the triage acceptance criterion, which is not string equality with today). Mutation run: dropped the locators from the headline (void locators; // MUTATION), rebuilt metadata-protocol, re-ran — exactly this case went red (Expected: "flows[0].nodes[1].config.approvers[0].value"), 14/15 others stayed green, defect control included. Restored and proven on disk: git hash-object == git rev-parse HEAD:path, git diff --exit-code 0, porcelain empty; rebuilt; 15/15 green.

3. Control whose subject does not exist pre-fix — the step-1 declaration. "a causal failed[] entry carries issues[] and the declared parse strips none of it" (deep-equal through PublishPackageDraftsResponseSchema.parse, plus the six-key element pin). Falsified by ablating only the two issues declarations from the schema, producers untouched:

  • ablation leg: spec rebuilt, scripts/ablation-dist-preflight.mjs @objectstack/spec 'never "no problems"' --absent → marker absent from all 189 built files; objectql case red via expected undefined to deeply equal [ { …(6) } ] (the strip mechanism, exactly); runtime seedApplied case red the same way; the other 14 + 5 cases stayed green (producers unchanged — the ablation moved only the declaration).
  • restore leg: file restored (hash == HEAD, diff-exit-0, porcelain empty), spec rebuilt, preflight marker present again, 15/15 + 6/6 green.
    Both legs rebuilt before every read — objectql and metadata-protocol tests resolve @objectstack/spec and @objectstack/metadata-protocol through dist/ per KNOWN_UNALIASED_TEST_IMPORTS; runtime tests alias spec to src but read metadata-protocol through dist, so it was rebuilt for those legs too.

Named non-effects, predicted before the runs, all held:advisories untouched (objectql advisory + byte-stability cases green throughout); SaveMetaItemResponseSchema byte-identical-when-empty #4717 pins green (spec + objectql full suites); DESTRUCTIVE_CHANGE message byte-unchanged (metadata-protocol full suite green); BATCH_ABORTED siblings carry no issues (new pin, green on both the pre-fix and post-fix legs); the pre-flight refusal face unchanged (existing refused-batch conformance case green).

New pins

Fixture triage (every moved pin, individually judged)

pintreatment
batch-verb-driver-text / batch-verb-driver-code broken-CEL GUARDs ("does not parse as CEL" in error)prose assertion moved to issues[0].message, headline now pinned to keep path + rule id + not restate the prose — the #8333 guarded property (which field of which draft survives the withhold) is preserved and strengthened
publish-drafts-closure "absent dataset" (no_such_dataset_xyz in error)dataset name asserted on the finding's message; error keeps the widget-dataset-unknown locator and is pinned not to restate
save-union-issues / P10 spec-validation prose pinsunchanged — the site-4 revert keeps them green as-is (their evidence is what forced the revert)
rest-4xx-truncation, http-dispatcher, meta-put-falsy-body, permission-set-projectionunchanged — synthesized fixtures pinning transport properties, not this producer

Verification summary (real outputs in the session log)

  • Full suites at a77d87d: spec 11056 passed (415 files; the one initial red was the strictness-ledger artifact, regenerated below), metadata-protocol 1788 passed, objectql 4007 passed, runtime 2679 passed; targeted rest (12) and plugin-security (53) green.
  • pnpm typecheck green for spec / objectql / runtime; @objectstack/metadata-protocol has no typecheck script by design (measured DEBT-ledger entry) — covered by its DTS build and the check:type-check-debt --re-measure run below.
  • pnpm --filter @objectstack/spec check:generated → 2 stale (check:docs, check:strictness-ledger, both pure projections of the schema edit: one row 406→407, one type cell) → --fix regenerated exactly those → re-check green.
  • Gate union at head 425890b (node scripts/pm/dispatch-gates.mjs, no path args — 13 committed paths, working tree clean): all 33 path-matched families + 5 convention-triggered (query-options-erasure, type-check-coverage, type-check-debt after the full turbo closure build, engine-double-contract, where-matcher) + check:nul-bytes — every gate exit 0, each verdict read from the gate's own line with exits captured before any pipe. check:dev-prereqs red once on the unbuilt-worktree precondition ("The workspace is not built — 1 unmet precondition"), green after turbo run build (70/70).

Out-of-scope findings

Changeset

.changeset/publish-error-headline-issues.md — spec minor (additive declared keys), metadata-protocol + runtime patch (producer fix + threading). Not declared-breaking: messages are not contract (the machine channels are code and issues[]), no migration to register — check:adr-0087-registration green on exactly that reading.


Generated by Claude Code

…rides the declared issues[] channel
Declare-then-trim (#10524): PublishPackageDraftsResponseSchema's failed[]
element declares the issues[] the producer has emitted since #8333, and
seedApplied declares the seed refusal's issues[]; then the #4463 gate's 422
message and seedRequestValidationError's message shrink to one-sentence
headlines (count + path [rule|code] locators) instead of restating the prose
the structured channel carries.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/runtime, @objectstack/spec, touching 8 documentable anchor(s).

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

  • content/docs/concepts/metadata-lifecycle.mdx(via ObjectStackProtocolImplementation (symbol), saveMetaItem (symbol))
  • content/docs/deployment/validating-metadata.mdx(via saveMetaItem (symbol))
  • content/docs/kernel/services-checklist.mdx(via saveMetaItem (symbol))
  • content/docs/permissions/authorization.mdx(via saveMetaItem (symbol))

2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx(via ObjectStackProtocolImplementation (symbol))
  • content/docs/releases/v17.mdx(via ObjectStackProtocolImplementation (symbol), saveMetaItem (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 132 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 9faa9bc51d7f6844dafa1e48ccc1d408f7193e20packageMentionDocs.

Which tree this was computed on

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

⚠️ 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 9faa9bc51d7f6844dafa1e48ccc1d408f7193e20 → 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 21, 2026
@os-elon
os-elon marked this pull request as ready for review August 21, 2026 16:16
@os-elon
os-elon enabled auto-merge August 21, 2026 16:16
@os-elon
os-elon added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit a79bd35Aug 21, 2026
33 checks passed
@os-elon
os-elon deleted the claude/issue-10524-publish-error-headline branch August 21, 2026 16:40
@github-actions

Copy link
Copy Markdown
Contributor

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

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

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

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 5.90s
    

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

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

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

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

分诊清单:

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

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

os-elon pushed a commit that referenced this pull request Aug 22, 2026
… and pin the sole carrier
#10886. `saveMetaItem`'s Phase 3a-destructive 409 renders its findings into
the message AND attaches the same array as `err.issues`, so a console that
renders both channels shows every finding twice — the render-then-attach shape
#10524 trimmed on the publish refusals.
The card's own first step is the face inventory, and the inventory says DO NOT
TRIM. Enumerated from every caller of `saveMetaItem` in the repo, then filtered
by the gate's own predicate (`!force`, folded type `object`/`field`, an existing
item under the target name, a non-empty diff), four of the seven callers cannot
reach the gate at all — three pass a literal non-object `type`, one passes
`force: true`. Of the three that can:
- the two `@objectstack/rest` `PUT /meta` doors and the `@objectstack/runtime`
dispatcher door all resolve through `resolveThrownHttpError`, so `issues`
reaches the wire structurally (a top-level `issues` on the REST body,
`details.issues` on the dispatcher envelope) — a message trim would lose
nothing there;
- `duplicatePackage`'s `failed[].error` is a SOLE CARRIER. It reports per-item
failures as DATA on a 200 (`POST /packages/:id/duplicate`), so no HTTP
boundary is involved and `details.issues` never exists; the array is typed
inline as `{ type, name, error }`; and unlike `publishPackageDrafts` — whose
`failed[]` #10895 could extend because it has a response schema —
`duplicatePackage` has none in `packages/spec` at all.
So the declare half of declare-then-trim is not done for this refusal, and the
trim is refused. Declaring a channel on that face is a `packages/spec` change
and is deliberately out of this card's scope; reported to the PM instead.
Reaching that face was measured, not argued, and the obvious attempt misleads: a
duplicate re-namespaces objects, so the target name usually does not exist, the
gate is skipped, and the copy fails the author-time gate instead. The gate fires
on the duplicate-again workflow, where the target namespace already holds the
renamed object.
No behaviour change: one new pin file plus a comment recording the verdict at
the throw site, so the next author is told which measurement they are standing
on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 23, 2026
… and pin the sole carrier (objectstack-ai#11016)
objectstack-ai#10886. `saveMetaItem`'s Phase 3a-destructive 409 renders its findings into
the message AND attaches the same array as `err.issues`, so a console that
renders both channels shows every finding twice — the render-then-attach shape
objectstack-ai#10524 trimmed on the publish refusals.
The card's own first step is the face inventory, and the inventory says DO NOT
TRIM. Enumerated from every caller of `saveMetaItem` in the repo, then filtered
by the gate's own predicate (`!force`, folded type `object`/`field`, an existing
item under the target name, a non-empty diff), four of the seven callers cannot
reach the gate at all — three pass a literal non-object `type`, one passes
`force: true`. Of the three that can:
- the two `@objectstack/rest` `PUT /meta` doors and the `@objectstack/runtime`
dispatcher door all resolve through `resolveThrownHttpError`, so `issues`
reaches the wire structurally (a top-level `issues` on the REST body,
`details.issues` on the dispatcher envelope) — a message trim would lose
nothing there;
- `duplicatePackage`'s `failed[].error` is a SOLE CARRIER. It reports per-item
failures as DATA on a 200 (`POST /packages/:id/duplicate`), so no HTTP
boundary is involved and `details.issues` never exists; the array is typed
inline as `{ type, name, error }`; and unlike `publishPackageDrafts` — whose
`failed[]` objectstack-ai#10895 could extend because it has a response schema —
`duplicatePackage` has none in `packages/spec` at all.
So the declare half of declare-then-trim is not done for this refusal, and the
trim is refused. Declaring a channel on that face is a `packages/spec` change
and is deliberately out of this card's scope; reported to the PM instead.
Reaching that face was measured, not argued, and the obvious attempt misleads: a
duplicate re-namespaces objects, so the target name usually does not exist, the
gate is skipped, and the copy fails the author-time gate instead. The gate fires
on the duplicate-again workflow, where the target namespace already holds the
renamed object.
No behaviour change: one new pin file plus a comment recording the verdict at
the throw site, so the next author is told which measurement they are standing
on.
Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
Co-authored-by: Claude <noreply@anthropic.com>
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

Development

Successfully merging this pull request may close these issues.

A publish failure's error string inlines the same validation prose that issues[] already carries, so every console renders each finding twice

2 participants

@os-elon@claude