Skip to content

fix(cli): a define* helper's ZodError at config load is reported as one located refusal, not a raw JSON array - #29087

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-vukoe2
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-vukoe2

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #28956

Clause-②: no — CLI output text only. No schema, export or accepted input changes; a refused config is refused with the same exit code as before.
Metadata-protocol: none

维护者速读

  • 改了什么:配置加载时 defineView 等 define* helper 抛出的 ZodError,现在打印成一条带定位的拒绝(文件:行:列 · helper · 路径 · zod 原话),不再是 40 多行 JSON 数组。os validate、os build、os lint、os dev、os serve 一致。
  • 为什么改:新手第一次写视图(字符串列和对象列混用)就撞上这个错误,npm run validate 只给 JSON,不说哪个文件、哪个视图,人和 coding agent 都没法自我纠正。
  • 风险与回滚:只改 CLI 的输出文字,schema 和接受集不变,退出码不变;语法错误、缺失 import、helper 自己的文字拒绝、defineStack 的拒绝原样不动。回滚 = revert 本 PR。
  • 先看:packages/cli/src/utils/config-load-refusal.ts(定位逻辑,locateConfigLoadZodError 只在栈和 source map 都对得上时才给文件);packages/cli/src/utils/config.ts 的 bundleConfigModule;packages/cli/src/commands/serve.ts 一行改动。
  • 两分钟验证:pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 test/config-load-zod-refusal.test.ts,预期 9/9 通过,用例里能看到 src/views/ticket.view.ts:5:28 · defineView validation failed (1 issue):。
  • 需要你做的:无,席位落地。

What changes

defineView, defineAction and the other strict helpers call Schema.parse and throw the ZodError as is. On the normal load path nothing wrapped it, and each command's catch-all printed error.message, which for a ZodError is its issues as a JSON array.

  • One seam. bundleConfigModule in packages/cli/src/utils/config.ts is now the one place the config module graph is bundled and evaluated. loadConfig (validate, build, lint, dev's compile step, info, diff, generate, doctor, verify, i18n, migrate meta) and os serve's own load both go through it. os serve had its own bundleRequire call and printed the same raw array, so it is routed through the seam too (one line in serve.ts).
  • The refusal. A raw ZodError thrown while the module evaluates is rethrown as a plain ConfigLoadRefusalError whose message is FILE:LINE:COL · HELPER validation failed (N issues): followed by the block formatZodError already renders (the same formatter the os migrate meta shim's __refusal and the defineStack validation failed block use). There is no second formatter. The ZodError rides as cause. Because it is a plain Error, no catch-all changed: the text face prints it where it printed the array, and --json copies it into error. No code is added.
  • The location. bundle-require evaluates one esbuild bundle with an inline source map. Its text is read just before the import (bundle-require deletes the file afterwards). The innermost stack frame inside the bundle is the authored call; the map turns it back into the authored file, line and column. The frame directly above it names the helper, kept only when it is a define* function and the authored line spells that name. With --enable-source-maps the frame is already authored and is matched against the map's sources.
  • Never a wrong file. No bundle frame on the stack, no map entry on the frame's own generated line, or a mapped file not on disk: the refusal names the config entry and says the stack does not say which module threw.
  • Every other load failure passes through untouched: a syntax error, a missing import, a helper's own prose refusal (defineView: the container defines no views …), defineStack's formatted refusal.

Card output, before and after (os validate on the card's project, a defineView with columns: [{ field: 'subject', link: true }, 'status', 'priority'] in src/views/ticket.view.ts, re-exported through src/views/index.ts):

before (main 0be247ce):
  → Loading configuration...

  ✗ [
  {
    "code": "invalid_union",
    "errors": [
  … 40-odd lines of JSON, exit 1

after:
  → Loading configuration...

  ✗ src/views/ticket.view.ts:5:28 · defineView validation failed (1 issue):

  ✗ list.columns: Invalid input
    ✗ list.columns.0: Invalid input: expected string, received object

Tests

New packages/cli/test/config-load-zod-refusal.test.ts (unit tier, in-process, src imports):

  • os validate --json: exit 1, error names src/views/ticket.view.ts:5:28, defineView, list.columns and the branch issue; it does not start with [.
  • os validate text face: no ✗ [ line, no "code": "invalid_union".
  • os build --json: the same located head, and the same error as os validate; exit code unchanged.
  • A refused call that is not a define* helper (ObjectSchema.create with a bad name) is located (objectstack.config.ts:3:N · Validation failed (1 issue):) without naming a helper.
  • A stack with no bundle frame names the config entry with the "stack does not say which module" words, never a guessed module.
  • Controls: a clean config validates; a missing import, a syntax error and a helper's prose refusal each keep exactly the message bundle-require throws.

Acceptance notes

  • The union branch words are the formatter's. For the card's mixed columns, the existing branch ranking (union-branch-policy.ts) picks the string[] branch, so the line reads list.columns.0: Invalid input: expected string, received object. It is located and correctable, but it is not the card's suggested "mix of strings and column objects; use one form". That wording would need a union message in packages/spec (a spec change with contract review), which the order told this card not to make.
  • Helper naming is define* only. ObjectSchema.create, Field.* and other non-define* calls get the file, line and column, and the default Validation failed header.
  • Not routed: os create's utils/scaffold-validate.ts has its own bundleRequire over the template it just generated, which is not authored input.
  • File surface beyond the claim: the claim named config.ts and the validate.ts catch-all. validate.ts is not touched. Added: utils/config-load-refusal.ts (new) and one line in commands/serve.ts, because os serve loads the config without loadConfig and printed the same array.
  • Overlap re-read: open PRs touching packages/cli/src/utils/config.ts, commands/serve.ts: none. fix(cli)!: os validate and os build refuse a flow whose region the automation engine refuses to register #29043 touches commands/compile.ts and commands/validate.ts (not touched here); fix(cli): os migrate meta --json says when no migration step ran #29045 touches migrate/meta.ts (not touched here).

Verification

Real CLI, red on main 0be247c and green on this branch (validate, build, lint, dev, serve)

Red, built CLI from origin/main 0be247c, card project:

  • os validate: exit 1, ✗ [ then the array (45 lines of output).
  • os build: exit 2, same array.
  • os lint: exit 1, same array.
  • os dev -p 38956: exit 1, the compile step prints the same array.
  • os serve -p 38958: exit 1, Loading objectstack.config.ts... then the same array.

Green, this branch:

  • os validate: exit 1, ✗ src/views/ticket.view.ts:5:28 · defineView validation failed (1 issue): and the two issue lines.
  • os build: exit 2 (unchanged), same block.
  • os lint: exit 1, same block.
  • os dev: exit 1, same block then ✗ Compile failed — fix errors above before starting dev server.
  • os serve: exit 1, same block.
  • os validate --json: "error": "src/views/ticket.view.ts:5:28 · defineView validation failed (1 issue):\n\n ✗ list.columns: Invalid input\n ✗ list.columns.0: Invalid input: expected string, received object".
  • NODE_OPTIONS=--enable-source-maps os validate: same located head.
  • Run from src/ with os validate ../objectstack.config.ts: views/ticket.view.ts:5:28 · … (relative to the working directory).

Controls on this branch: a syntax-error config prints esbuild's Build failed with 1 error: … Expected identifier but found end of file; a missing import prints Could not resolve "./src/not-there"; a clean config validates (✓ Validation passed); a bad manifest.id still prints ✗ defineStack validation failed (1 issue):; os migrate meta --from 17 on the card project still prints the shim's [authored-source] defineView(): the current schema refuses this artifact … defineView validation failed (1 issue): warning and exits 0.

Ablation (src-level; the test imports src, no dist hop)

node scripts/ablation-replace.mjs --file packages/cli/src/utils/config.ts --anchor ' if (isRawZodError(error)) throw configLoadRefusal(error, absolutePath, bundle, cwd);' --delete -- pnpm exec vitest run test/config-load-zod-refusal.test.ts:

  • ok mutation landed: anchor 1 -> 0, blob 82be1e8df47c -> f63bad0a3d26
  • Tests 4 failed | 5 passed (9): the four seam cases (validate --json, validate text face, build --json, the non-define* locate). The renderer's unit case and the four controls stay green.
  • ok restored: blob == HEAD (82be1e8df47c) and git diff HEAD is empty
Local gates

Head 5af532cc.

  • cli unit tier (vitest run --project unit): Test Files 369 passed (369), Tests 5205 passed (5205). The integration tier is declared to CI (no spawn entry or integration file touched).
  • cli typecheck (tsc --noEmit + check:test-typecheck): exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands: 63 derived families, every one run with its exit code captured before any pipe: 63 × exit 0. --ran: 63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED (a DERIVED zero).
  • check:i18n-coverage first answered exit 3 (PREREQUISITE: @objectstack/connector-slack / service-print had no build output); after building the ./examples/*^... closure it answered exit 0 (OK (14 config(s), 426 baselined untranslated string(s), none new)).
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:cli-test-child-env :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:i18n :: exit 0
pnpm check:i18n-walk-parity :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:route-envelope :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
pnpm check:i18n-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0

Generated by Claude Code

A define* helper's Schema.parse throws its ZodError as is, and every
command that loads the config printed error.message: the issues as a JSON
array, with no file and no helper. The config module now loads through
bundleConfigModule, which reads the evaluated bundle's inline source map
to locate the authored call and renders the issues with formatZodError.
os serve's own config load goes through the same seam.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5mch1VryTyQsMG5U86
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/automation/hook-bodies.mdx (via loadConfig (symbol, a top-level function))

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

  • content/docs/releases/v17/17-2.mdx (via loadConfig (symbol, a top-level function))

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
  • 1 anchor(s) matched too much of the corpus to be a work list: os serve (command, 44 pages)
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 101 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 35 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 d1f3ac05038cffc28b9f8bcbe2116afe1b5f1a2e → packageMentionDocs.

Which tree this was computed on

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

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-11-02T07:18Z · head 5af532cc · claimed by seat 2 on #28956 (comment 6570646800)

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape:
  2. Contract review: none is owed. The declaration is Clause-②: no, and no spec or governed path is touched. The seat read the hand-back (os-dev-report 6571317004):
    • There is one seam, bundleConfigModule in utils/config.ts, used by loadConfig and by os serve's own load.
    • A config-load ZodError is rethrown as a located refusal (FILE:LINE:COL · defineView validation failed), followed by the existing formatZodError block. There is no second formatter.
    • Exit codes are unchanged.
    • The wider surface (config-load-refusal.ts and one line in serve.ts) is accepted, because os serve printed the same raw array.
    • Acceptance note, accepted: the union line reads the existing branch ranking ("expected string, received object"), not the card's "use one form" wording, which would need a spec change.
  3. Evidence: reported by the dev, and not re-run by the seat.
    • Red on main for validate, build, lint, dev and serve; green on all five.
    • Ablation: removing the seam reds exactly its 4 cases.
    • cli unit: 369 files / 5205 tests.
    • Typecheck: green.
    • Derived gates: 63 of 63 exit 0.
  4. Gates: CI on this head was still running at the time of this ACCEPT, with 0 red. Auto-merge waits for it, and the merge queue re-runs CI against current main.
  5. Landing: pr_ready + automerge_enable through the relay.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 07:20
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 07:20
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 87d6bae Nov 2, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-vukoe2 branch November 2, 2026 08:38
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

Landing record · PR #29087

domain:server seat 2 · session_01ADHkmQxwoGb5fSnrFkpxmt · 2026-11-02T09:08Z · card #28956

Reading Probe Result
Merged PR #29087 state merged, merge commit 87d6baef34
On main git merge-base --is-ancestor 87d6baef34 origin/main yes
Queue refs git ls-remote origin refs/heads/gh-readonly-queue/* matching 29087 0

Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

1 participant