Uh oh!
There was an error while loading. Please reload this page.
Publish refusals: declare failed[].issues + seedApplied.issues, then trim error to a headline - #10895
Conversation
…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
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
📓 Docs Drift CheckThis PR changes 3 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 132 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 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
|
Uh oh!
There was an error while loading. Please reload this page.
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32502814268 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
… 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
… 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>
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 declareissues[]asz.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-keyRuntimeAuthoringIssueSchema(rule/path/where/message/hint/severity), not the card's{ path, message }guess — declaring the guess would have strippedrule/hintat the parse door.seedApplieddeclaresissues[]({ path, message, code? }, thezodIssuesToMetadataIssuesentry — 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 surfaceseedRequestValidationErrorontoseedAppliedkept only the message. Both catches now threade.issuesbeside the headline (in-batchapplySeedBodies, guarded bydeclaresClientRefusal; route-level fallback inpackages/runtime/src/domains/packages.ts—issuesis structured authoring feedback only the declared 422 attaches, per the A raw drivercode(SQLITE_ERROR) still reaches client-facing payloads through the batch verbs'codelimb — the sibling field #8333 did not cover #8441 measurement, so nothing undeclared routes around the withhold).Step 2 (trim, producers):
runtime-authoring-gate.ts— the card's captured string: the 422 message is now[invalid_metadata] type/name failed author-time validation: N issue(s) — path [rule]; ...(up to 3 locators). The full prose (detail) still goes where no structured channel exists: theOS_ALLOW_UNLINTED_METADATA_WRITESoperator warn ([runtime/metadata] 作者时规则只存在于 CLI:Studio/REST/MCP 的运行时授权面是第四扇门,26 条规则一条不跑——#4409 修完后最大的敞口 #4463 acceptance), unchanged.seedRequestValidationError— same headline grammar withpath [zod-code]locators, via a sharedmetadataIssueHeadlinehelper. Kept separate from the gate's composer on purpose: rule ids and zod codes are different vocabularies.(+N more)tail's decided fate: replaced by the leading total count, which subsumes it.The four sites, measured at 4d7c564 — which moved and which did not
runtime-authoring-gate.tsINVALID_METADATA/422details.issues,failed[].issues) now carries the same array structurallyprotocol.tsseedRequestValidationErrorsaveMetaItemDESTRUCTIVE_CHANGE/409{code, field?, message}vocabulary). Filed as #10886saveMetaItemspec-validation INVALID_METADATA/422 (protocol.ts ~:13306) — a fourth site beyond the dispatch table, found by measurementThe 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'sfailed[].errorquotes that message without threadingissues(a message-only face), and the pins hold the author's prescription ("Unrecognized key(s) on this view container…", thedefineView(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 withexpected '[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-code0, 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 twoissuesdeclarations from the schema, producers untouched:scripts/ablation-dist-preflight.mjs @objectstack/spec 'never "no problems"' --absent→ marker absent from all 189 built files; objectql case red viaexpected 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).Both legs rebuilt before every read — objectql and metadata-protocol tests resolve
@objectstack/specand@objectstack/metadata-protocolthroughdist/perKNOWN_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:
advisoriesuntouched (objectql advisory + byte-stability cases green throughout);SaveMetaItemResponseSchemabyte-identical-when-empty #4717 pins green (spec + objectql full suites); DESTRUCTIVE_CHANGE message byte-unchanged (metadata-protocol full suite green); BATCH_ABORTED siblings carry noissues(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
packages/spec/src/api/protocol.test.ts— declaration pins for both keys (carry-through, optional-without-fabrication, element-by-reference refused-not-narrowed, non-array refused), house style of the publishPackageDrafts still discards the runtime authoring gate's per-draft advisories — Studio's "publish whole app" reports none of them #9343/[P2/P3] 运行时授权门的 advisory findings 要进响应并被 Studio 渲染 + docs「the gate」补第四列(#4463 收尾) #4717 blocks.packages/objectql/src/publish-package-drafts-response-conformance.test.ts— the four cases above against the real protocol; the missing causal-entry conformance case is precisely why the key rode the wire undeclared.packages/runtime/src/domains/packages-publish-drafts-response-conformance.test.ts— the route-level seed refusal through the real route catch: headline + declaredissueson the wire, unstripped.packages/metadata-protocol/src/protocol.batch-verb-driver-text.test.ts§4 — the in-batchapplySeedBodiesthreading.Fixture triage (every moved pin, individually judged)
error)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 strengthenedno_such_dataset_xyzinerror)errorkeeps thewidget-dataset-unknownlocator and is pinned not to restateVerification summary (real outputs in the session log)
pnpm typecheckgreen for spec / objectql / runtime;@objectstack/metadata-protocolhas no typecheck script by design (measured DEBT-ledger entry) — covered by its DTS build and thecheck:type-check-debt --re-measurerun 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) →--fixregenerated exactly those → re-check green.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-debtafter 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-prereqsred once on the unbuilt-worktree precondition ("The workspace is not built — 1 unmet precondition"), green afterturbo run build(70/70).Out-of-scope findings
issuescarries — the remaining duplication of the #10524 family, different refusal class #10886 — site 3's DESTRUCTIVE_CHANGE duplication (different refusal class, its own vocabulary; the message-only-face inventory is the prerequisite).issues[]on the envelope, but message-only faces (duplicatePackagefailed[].error) forbid the #10524 trim until they declare a structured channel #10888 — site 4's spec-validation duplication, with the trial-trim measurement (message-only faces such asduplicatePackage.failed[].errormust declare a structured channel first). Neither is addressed here; both remain open.Changeset
.changeset/publish-error-headline-issues.md— specminor(additive declared keys), metadata-protocol + runtimepatch(producer fix + threading). Not declared-breaking: messages are not contract (the machine channels arecodeandissues[]), no migration to register —check:adr-0087-registrationgreen on exactly that reading.Generated by Claude Code