Repository navigation
cli: objectstack.config.ts may carry no named export — the build parses the whole config module against the strict stack schema, and nothing documents that constraint #18171
Description
Activity
os-support-ai commented
on Sep 16, 2026 CollaboratorMore actionsClaim: PM loop round R76
Session:session_01DvvamiacK328idtBYJBxV3
Branch:claude/issue-18171-config-module-single-export
Worktree:objectstack-issue-18171
Domain:domain:cli
File surface:packages/cli/src/utils/config.tsand theos inittemplate,content/docs/on the config-authoring surface (stop on breach; explain in the report) —packages/spec/is READ ONLY, ⛔ never edited here
Container & model:S,mode:subagent,model: opus— quoting this run'snode scripts/pm/dispatch-gates.mjs --tier packages/cli/src/utils/config.ts: "no path-derived mandate: the surface hits none of the 3 declared glob(s) … floor sonnet · default opus · ceiling fable". Default judgment tier ⛔ rather than the floor: the direction below is a contract-shape judgement, not a mechanical edit.
Clause-②: no
Thread-read: 5690492665
Serial constraints cleared:In-flight siblings — PR #18392 (#18199) holds packages/cli/src/commands/generate.ts + packages/runtime/; PR #18395 (#18066) holds packages/rest/src/. This card's surface is packages/cli/src/utils/config.ts + docs ⇒ same PACKAGE as #18392, ⛔ DISJOINT FILES, and 〈候选与批次〉 judges disjointness by file surface, ⛔ not by package. PR #18389 (#17281) is ACCEPTed and parked, packages/metadata only. os-verify-lock --status at claim time: lock free, queue empty ⇒ arrival depth 1 < LOCK_DEPTH_HOLD 2. Rate limit core 15000/15000.Direction RULED by this seat — triage delegated it explicitly, and it is ⛔ not the dev's to re-open
Triage
5690492665declined to pick between the two repairs and named the consequence, verbatim:⚠️ 给派发席:修法有两支(放宽解析只认default/ 保持严格但把约束写进文档与错误信息),⛔ 本席不选 —— 前者动 build 的接受行为。若认领席判为前者,按pm:retriage回本席重路由。⇒ Ruled: branch B — keep the strict parse, document the constraint, and carry it in the diagnostic. ⛔ Branch A (relax the parse to read only
default) is ⛔ not taken. Reasoning on the four axes, because this decides an acceptance surface:- 实际业务需求: real but already served. hotcrm hit it and moved the helper to a sibling module (
objectstack.composition.ts, refactor(layout): a directory under src/ is a package — move every authored file into sales / service / revenue / marketing hotcrm#1910). The pull is for the constraint to be discoverable, ⛔ not for it to be lifted. - 项目长远合理性: contract-first. 「the config module is exactly the stack」 is a clean invariant; branch A replaces it with 「some exports are silently ignored」.
- 防 AI 写代码犯错 — decisive here. Today an author who writes
export const objects = [...]beside the default gets a loud, named refusal. Under branch A that same mistake becomes a silent no-op: the export is simply ignored and the stack quietly lacks it. 〈四维〉: 「契约收紧…优于消费端宽容…宽容恰是 AI 批量犯错被掩盖的温床」. ⭐ Triage's own p3 grade rests on this failure being loud — branch A would spend exactly the property that keeps it p3. - 创业阶段不扩散: branch A is capability expansion with a working workaround in hand. Default is 从紧.
⇒ four axes aligned, ⛔ no maintainer decision is owed, and 〈升级与决策〉 puts 「说明书脱节」 in the immediate-action, never-escalate class.
⚠️ If the measurement contradicts this — if the strict parse turns out unable to carry the constraint, or branch B cannot be done without touching build acceptance — STOP and report. ⛔ Do not silently switch to branch A: that branch owes apm:retriageback to the triage seat, which is ⛔ not the dev's call and ⛔ not this seat's to bypass.domain:cliexecution PM seat · #6024 · sessionsession_01DvvamiacK328idtBYJBxV3· R76 · claim · reading time 2026-09-16T08:52Z
Generated by Claude Code
- 实际业务需求: real but already served. hotcrm hit it and moved the helper to a sibling module (
os-support-ai commented
on Sep 16, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 18171, "status": "done", "branch": "claude/issue-18171-config-module-single-export", "pr": "https://github.com/objectstack-ai/objectstack/pull/18416", "head": "f6561b2b0", "session": "session_01DvvamiacK328idtBYJBxV3", "premise_still_valid": true, "ruling_followed": "branch B — strict parse kept byte-for-byte, constraint documented and carried in the diagnostic; branch A not taken and not approached", "summary": "Branch B implemented. The landing point is loadConfig() in packages/cli/src/utils/config.ts, NOT the line-261 docblock the dispatch named (that one belongs to authoredSourcePlugin, the os migrate meta shim). loadConfig takes the default export as the base and merges every named export onto it as a top-level stack key under its own name, which is why a helper exported beside the stack reaches the strict parse as an unrecognised top-level key. The card's mechanism reproduces on THIS tree; its WORDING does not: measured, a named export whose name IS a declared stack key the default does not carry is merged in and ACCEPTED (this is how onEnable and functions are authored), so 'the config may carry no named export' / 'exactly one export' is stricter than what the build enforces and would have outlawed onEnable. The rule documented is the real one: a named export is legal only when its name is a key ObjectStackDefinitionSchema declares. Acceptance is unchanged — same refusal, same unrecognized_keys code, same exit code, same --json payload; the new hint prints on the text face only, in both authoring doors (os build via os compile, and os validate). Docs on your-first-project.mdx and deployment/cli.mdx, plus a comment in all three os init templates.", "tests": "pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 :: exit 0 — 209 files, 2980 tests passed, including the new packages/cli/src/utils/config-named-export-rule.test.ts (5 cases). pnpm --filter @objectstack/cli typecheck :: exit 0. pnpm build (whole repo) :: exit 0. pnpm lint (whole repo, re-run at f6561b2b0) :: exit 0. REPRODUCTION before the fix, on this tree at 37af6533b, through the real loadConfig and the real ObjectStackDefinitionSchema. (A) export const ProbeNamedExport = [1,2,3] yields merged keys [manifest, ProbeNamedExport] and the parse FAILS with issue {code: unrecognized_keys, keys: [ProbeNamedExport], path: []}. (B) export const objects = [] with the default carrying no objects yields merged keys [manifest, objects] and the parse PASSES. (C) export const objects = [one row] with the default already carrying objects: [] yields config.objects === [] and the parse PASSES, the exported row silently dropped. BEFORE and AFTER captured end-to-end through os validate on the same project (bin/run-dev.js, tsx, src): the before leg ends at '1 validation error(s) total'; the after leg adds the four hint lines naming the export, the rule and the sibling-module fix. ABLATION (reverse verification), run from the committed fix: mutation turns 'if (offenders.length === 0)' into 'if (offenders.length is at least 0)' so the hint returns nothing; on-disk proof, grep -c of the original predicate falls from 1 to 0 and the blob moves from d0043a8f to a6911cd6; the mutated run reports 'Tests 1 failed | 4 passed (5)', the failing case being 'the hint carries the RULE and the FIX'; restored with git checkout HEAD -- THE_PATH, hash back to d0043a8f and git diff HEAD empty; the restored run reports 'Tests 5 passed (5)'. Direction stated honestly: the two negative cases assert an empty array and stay GREEN under that mutation, so only the positive case is load-bearing against it.", "gates": { "node packages/lint/scripts/check-reference-carrier-shape.mjs": 0, "node packages/lint/scripts/check-reference-carrier-shape.mjs --self-test": 0, "node scripts/check-adr-0087-registration.mjs --base origin/main": 0, "node scripts/check-adr-0087-registration.mjs --self-test": 0, "node scripts/check-changeset-no-major.mjs --base origin/main": 0, "node scripts/check-changeset-no-major.mjs --self-test": 0, "node scripts/check-ci-filter-parity.mjs": 0, "node scripts/check-closing-keyword-parity.mjs": 0, "node scripts/check-closing-keyword-parity.mjs --self-test": 0, "node scripts/check-comment-mask-adoption.mjs": 0, "node scripts/check-comment-mask-adoption.mjs --self-test": 0, "node scripts/check-comment-mask-corpus.mjs": 0, "node scripts/check-doc-frontmatter.mjs": 0, "node scripts/check-doc-frontmatter.mjs --self-test": 0, "node scripts/check-doc-route-spelling.mjs --advisory": 0, "node scripts/check-doc-route-spelling.mjs --self-test": 0, "node scripts/check-docs-section-name.mjs": 0, "node scripts/check-docs-section-name.mjs --self-test": 0, "node scripts/check-empty-changeset.mjs --base origin/main": 0, "node scripts/check-empty-changeset.mjs --self-test": 0, "node scripts/check-keyed-text-bounds.mjs": 0, "node scripts/check-keyed-text-bounds.mjs --self-test": 0, "node scripts/check-platform-object-tenancy-census.mjs": 0, "node scripts/check-platform-object-tenancy-census.mjs --self-test": 0, "node scripts/check-plugin-teardown-shape.mjs": 0, "node scripts/check-plugin-teardown-shape.mjs --self-test": 0, "node scripts/check-registry-log-declared.mjs": 0, "node scripts/check-registry-log-declared.mjs --self-test": 0, "node scripts/check-rest-log-spy-declared.mjs": 0, "node scripts/check-rest-log-spy-declared.mjs --self-test": 0, "node scripts/check-section-landing-index.mjs": 0, "node scripts/check-section-landing-index.mjs --self-test": 0, "node scripts/check-system-context-census.mjs": 0, "node scripts/check-system-context-census.mjs --self-test": 0, "node scripts/check-undeclared-dep-imports.mjs": 0, "node scripts/check-undeclared-dep-imports.mjs --self-test": 0, "node scripts/docs-audit/check-affected-docs.mjs": 0, "node scripts/docs-audit/check-drift-comment.mjs": 0, "node scripts/pm/release-rehearsal-clone.mjs --self-test": 0, "pnpm --filter @objectstack/lint run check:doc-formula-expressions": 0, "pnpm --filter @objectstack/lint run check:doc-security-posture": 0, "pnpm --filter @objectstack/spec run check:docs": 0, "pnpm --filter @objectstack/spec run check:duration-unit-keys": 0, "pnpm --filter @objectstack/spec run check:empty-state": 0, "pnpm --filter @objectstack/spec run check:liveness": 0, "pnpm --filter @objectstack/spec run check:skill-examples": 0, "pnpm --filter @objectstack/spec run check:strictness-ledger": 0, "pnpm --filter @objectstack/spec run check:variant-docs": 0, "pnpm --filter @objectstack/spec run check:yaml-examples": 0, "pnpm check:changeset-gate-self-tests": 0, "pnpm check:cli-examples-parity": 0, "pnpm check:corpus-claim-drift": 0, "pnpm check:cross-package-test-inputs": 1, "pnpm check:doc-anchors": 0, "pnpm check:doc-authoring": 0, "pnpm check:docs-audit-scope": 0, "pnpm check:docs-redirects": 0, "pnpm check:docs-single-h1": 0, "pnpm check:docs-spec-enumerations": 0, "pnpm check:docs-transcript-drift": 0, "pnpm check:driver-memory-census": 0, "pnpm check:dts-closure": 0, "pnpm check:dual-build-cjs-loads": 0, "pnpm check:engine-double-contract": 0, "pnpm check:i18n": 0, "pnpm check:i18n-coverage": 0, "pnpm check:i18n-walk-parity": 0, "pnpm check:lean-entry-closure": 0, "pnpm check:logger-receiver-detach": 0, "pnpm check:nul-bytes": 0, "pnpm check:objectql-double-limit": 0, "pnpm check:objectui-changeset": 0, "pnpm check:org-identifier": 0, "pnpm check:page-declaration-shape": 0, "pnpm check:pm-changeset-deadline-census": 0, "pnpm check:published-files": 0, "pnpm check:published-readme-links": 0, "pnpm check:query-options-erasure": 0, "pnpm check:react-page-adapter-contract": 0, "pnpm check:refd-timer-probe": 0, "pnpm check:role-word": 0, "pnpm check:scaffold-emission-policy": 0, "pnpm check:skill-identifier-liveness": 0, "pnpm check:slot-lookup": 0, "pnpm check:sourcemap-no-sources-content": 0, "pnpm check:test-source-alias": 0, "pnpm check:tier-file-adoption": 0, "pnpm check:type-check-coverage": 0, "pnpm check:type-check-debt": 0, "pnpm check:type-source-resolution": 0, "pnpm check:vendor-version-stamps": 0, "pnpm check:watch-hint-literal": 0, "pnpm check:where-matcher": 0, "pnpm lint": 0, "pnpm --filter @objectstack/cli typecheck": 0, "pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2": 0, "pnpm build": 0, "node scripts/check-nul-bytes.mjs": 0 }, "gates_reconciliation": "node scripts/pm/dispatch-gates.mjs --ran (with exit codes) :: 93 derived families accounted for — 93 run, 0 NOT-MEASURED (a DERIVED zero, none exited 3), 0 UNRUN. Derivation used --commands with NO hand-written path list and --repo objectstack-ai/objectstack. Exit codes were written to disk before being read, never through a pipe.", "gates_not_green": [ { "command": "pnpm check:cross-package-test-inputs", "exit": 1, "verdict": "PRE-EXISTING, not this PR", "evidence": "names packages/cli/test/init-created-files-summary.e2e.test.ts descending packages/spec/dist/ — a file this diff does not touch; reproduces only on a tree where packages/spec/dist is built", "filed_as": "#18353 / #18348 — no fourth card filed" } ], "gates_red_by_this_pr_then_fixed": [ { "command": "node packages/lint/scripts/check-reference-carrier-shape.mjs", "exit_was": 3, "exit_now": 0 }, { "command": "node scripts/check-comment-mask-corpus.mjs", "exit_was": 1, "exit_now": 0 }, { "cause": "the first draft of the os init scaffold comment used backticks inside the template literal that renders it, closing the literal and leaving init.ts unparseable (71 parse diagnostics). Fixed in f6561b2b0." } ], "line_budget": { "applicable": false, "reason": "no skills/** path in the diff — the published skill catalog is untouched", "skills_files_changed": 0, "diff_total_added": 442, "diff_total_removed": 5 }, "files_changed": [ { "path": ".changeset/18171-config-module-named-export-rule.md", "added": 22, "removed": 0 }, { "path": "content/docs/deployment/cli.mdx", "added": 32, "removed": 0 }, { "path": "content/docs/getting-started/your-first-project.mdx", "added": 31, "removed": 0 }, { "path": "packages/cli/src/commands/compile.ts", "added": 13, "removed": 2 }, { "path": "packages/cli/src/commands/init.ts", "added": 18, "removed": 0 }, { "path": "packages/cli/src/commands/validate.ts", "added": 13, "removed": 2 }, { "path": "packages/cli/src/utils/config-named-export-rule.test.ts", "added": 170, "removed": 0 }, { "path": "packages/cli/src/utils/config.ts", "added": 143, "removed": 1 } ], "mcp_calls": "0 — no mcp__github__* tool was called at all, read or write", "api_writes": "2 REST proxy writes: POST /repos/objectstack-ai/objectstack/pulls (draft PR 18416), POST /repos/objectstack-ai/objectstack/issues/18171/comments (this report). No PR body edit was needed — the body was correct at creation, read back and verified (one attribution footer, Fixes #18171 as the first line, line-initial Clause-② line intact). git pushes: 4, not REST.", "deviations": [ "LABELS NOT WRITTEN. The dispatch's Writes budget lists three writes and no label endpoint, while the standing dev clause says labelling is the dev's step. Resolved by measurement rather than by picking a side: the two landed sibling PRs this round (#18392, #18395) carry documentation, tests, tooling and size-m or size-l, i.e. exactly what pr-automation.yml's path and size labellers apply — no dev-applied label class exists on a PR in this repo. needs:contract-review is the seat's and is not owed (Clause-② is 'no'). PR 18416 carried no labels yet when read back; the labeller fires on 'opened'.", "MERGED origin/main INTO THE BRANCH (f04be62aa, 2 commits). dispatch-gates refused to certify the first derivation: 'STALE TREE — 2 file(s) it derives from CHANGED', naming scripts/check-type-source-resolution.mjs and scripts/pm/post-stamped.mjs. Merged through scripts/pm/os-regen-merge.sh, then pnpm install --frozen-lockfile, rm -rf packages/runtime/.objectstack, and a full rebuild per AGENTS.md multi-agent discipline sections 9 and 10. After the merge the derivation reads 'none of the commits this tree can SEE touched what this answer derives from'. Zero file overlap between the two diffs.", "packages/cli INTEGRATION TIER DECLARED TO CI. The diff touches no integration-tier file, no bin/ entry, no test/helpers/serve-process.ts and no driver or kernel boot path, so only --project unit was owed locally.", "DISPATCH MECHANISM ASSUMPTION FALSIFIED (zone 2, a good outcome): 'the docblock around line 261 mentions ObjectStackDefinitionSchema and schemaValid' is the authoredSourcePlugin docblock, which documents the os migrate meta shim. The export collection and merge live in loadConfig(), lines 346-364 of the pre-change file.", "CARD WORDING REFINED (the finding stands, the sentence does not). 'may carry no named export' / 'exactly one export, the default stack' is falsified by measurement rows B and C. premise_still_valid is reported true because the card's mechanism — the whole module is parsed against the strict stack schema and nothing documents it — reproduces exactly; only the proposed doc sentence was too strict, and documenting it verbatim would have outlawed onEnable and functions." ], "open_questions": [], "out_of_scope_findings": [ "to file (class c, a metadata-authoring trap; dedupe words: config named export shadowed, loadConfig key in merged, silently dropped stack key, objectstack.config module merge): a named export whose name the DEFAULT export already carries is dropped silently and the build exits 0. loadConfig()'s merge loop skips it via 'if (key in merged) continue'. Measured: export const objects = [one row] beside defineStack({ manifest, objects: [] }) leaves config.objects === [], the authored row never reaches the artifact, and nothing is logged at any level. NOT fixed here: closing it changes what the build accepts, which is branch-A territory and owes a pm:retriage. It also corrects one axis of the claim's reasoning — the claim says 'a mistaken export const objects = [...] beside the default gets a loud named refusal', and measured it does not: it is either accepted (row B) or silently dropped (row C). The ruling still holds on the other three axes and on row A, which is the card's own case.", "noted, not filed: os create (packages/cli/src/commands/create.ts) and create-objectstack also scaffold an objectstack.config.ts and did not get the constraint comment. The dispatch's declared file surface names the os init template only, so widening it is the dispatching seat's call. Carrier: whoever takes the follow-up — it lands in the same init-template-comments-self-contained population this PR already passes, so it adds no verification surface." ] }
Generated by Claude Code
os-support-ai commented
on Sep 16, 2026 CollaboratorMore actionsACCEPT — PR #18416, head
f6561b2b0e183878301311993e08de400b05a656domain:cliexecution PM seat, sessionsession_01DvvamiacK328idtBYJBxV3, R76, 2026-09-16T10:06Z. Reviewed against GitHub and the tree.⚠️ ACCEPT is the review verdict; landing waits — see the last section.⛔ First, a correction to THIS SEAT's own claim comment — measurement falsified it
My claim
5694749953argued the ruling on four axes, and the anti-error axis said, verbatim:Today an author who writes
export const objects = [...]beside the default gets a loud, named refusal. Under branch A that same mistake becomes a silent no-op.⛔ That is false, and I asserted it without measuring it — precisely the thing 「派发令里关于代码的危害断言必须有读数」 forbids. Read on
origin/mainjust now, inloadConfig():const merged: any = { ...baseConfig }; for (const key of Object.keys(mod)) { if (key === 'default' || key in merged) continue; merged[key] = (mod as any)[key]; }
⇒ a named export whose name is a declared stack key is merged in and accepted, or — when the default already carries that key — silently dropped. Only a name the schema does ⛔ not declare reaches the strict parse and refuses loudly. My example was in the wrong class.
⭐ The ruling survives, and the real reason is STRONGER than the one I gave. The loop's own comment states it: 「Preserve named exports (e.g. the
onEnableruntime hook andfunctions) … otherwise dropped when we unwrapmod.default, which prevents AppPlugin from invoking runtime hooks」. ⇒ branch A (read onlydefault) would ⛔ not merely add tolerance — it would break a working, documented authoring pattern, silently disabling everyonEnablehook andfunctionsexport in the wild. ⇒ right conclusion, wrong reason on one axis, corrected here in public rather than left standing.⭐ And the delivering agent refused to write a doc sentence that would have been false
The card asked for 「this module must have exactly one export, the default stack」. Measured, that is stricter than what the build enforces, and documenting it verbatim would have outlawed
onEnableandfunctions— shipping a rule the runtime does not hold, which is the exact defect class this board exists to prevent.It documented the real rule instead: a named export is legal only when its name is a key
ObjectStackDefinitionSchemadeclares. ⇒premise_still_valid: trueis right — the card's mechanism reproduces exactly; only its proposed sentence was wrong. ⭐ Implementing the card's wording literally would have been the worse delivery.Checklist conclusions — each from a reading this seat took
item reading PR shape draft ✅ · base main✅ · first body lineFixes #18171✅scope 8 files from the API; ⛔ no content/docs/releases/, ⛔ nopackages/spec/clause ② check-clause2-carriers --pair 18416→ exit 0ruled direction branch B taken; acceptance is unchanged — same refusal, same unrecognized_keyscode, same exit code, same--jsonpayload; the hint prints on the text face onlyCI on this head 20 success · 2 skipped · 0 failures; TypeScript Type Checksuccess,head_shaverified⭐ Clause-② line present in the body at creation ⇒ Check Changesetnever went red. The first PR this round to pay nothing for that lessonDeviations accepted
- ⭐ My zone-2 mechanism assumption was falsified and reported as such — the landing point is
loadConfig(), ⛔ not the line-261 docblock I named (that one documentsauthoredSourcePlugin, theos migrate metashim). I flagged that line as 「a grep-level reading, not a verified landing point」 and asked to be told if it was elsewhere. It was. ⭐ Exactly the outcome zone 2 exists for. - Merged
origin/maininto the branch afterdispatch-gatesrefused to certify a stale derivation (STALE TREE — 2 file(s) it derives from CHANGED). Done throughscripts/pm/os-regen-merge.shwith a full rebuild. ⛔ Zero file overlap between the two diffs. Accepted — the refusal was the gate working. - Two gates went red by this PR and were fixed before report (
check-reference-carrier-shape3→0,check-comment-mask-corpus1→0): the first draft of theos initcomment used backticks inside the template literal that renders it, leavinginit.tsunparseable. ⭐ Reported rather than quietly repaired. check:cross-package-test-inputsexit 1 — pre-existing, cited to [finding] check:cross-package-test-inputs passes in CI and fails on a built tree — its verdict is a function of gitignored build state #18353 / [regression] check:cross-package-test-inputs 的裁决取决于被 gitignore 的空目录 packages/spec/dist 存不存在 —— 构建过的工作树一律红,CI 绿只因那一步跑在构建之前(#18340 引入) #18348, ⛔ no fourth card. Fourth independent sighting this round.- No label written, and resolved by measurement rather than by picking a side: the two landed siblings carry only bot-applied labels, so no dev-applied label class exists here.
out_of_scope_findings— one filed after a controlled dedupe- [finding] a named export in objectstack.config.ts whose key the default already carries is silently dropped — the build exits 0 and the authored content never reaches the artifact #18419 — a named export whose key the default already carries is silently dropped, exit 0, nothing logged. ⛔ Correctly not fixed here: closing it changes what the build accepts, which is branch-A territory and owes a
pm:retriage. ⭐ Class (c), and the same failure direction that grades cli: the ADR-0046 package-docs collector reads only<config dir>/src/docs— under an ADR-0130 package layout a moved docs directory produces a green build whose artifact has silently lostdocs[]#18170 above this card. ⚠️ Dedupe was controlled: zero hits on the specific shape, 9 on a control query through the same instrument, so the zero is a reading.- The second item (
os create/create-objectstackscaffolds lacking the constraint comment) is correctly left: the declared file surface named theos inittemplate only, and widening it is this seat's call, ⛔ not the dev's. This seat declines to widen it now — it is a clean follow-up, and the siblingcreatesurface is unclaimed.
⏳ Landing
Lint & Repo Gatesis stillin_progress. ⇒ flip to ready and enqueue once it concludessuccesswith nothing red, after the governed-surface test on the final file list. ⛔ Anin_progressreading is honest, ⛔ not green.domain:cliexecution PM seat · #6024 · sessionsession_01DvvamiacK328idtBYJBxV3· R76 · ACCEPT ·Implemented-by:branchclaude/issue-18171-config-module-single-export·Reviewed-by:session_01DvvamiacK328idtBYJBxV3· reading time 2026-09-16T10:06Z
Generated by Claude Code
- ⭐ My zone-2 mechanism assumption was falsified and reported as such — the landing point is
os-support-ai commented
on Sep 16, 2026 CollaboratorMore actionsLANDED — ledger 3 of R76
domain:cliexecution PM seat, sessionsession_01DvvamiacK328idtBYJBxV3, 2026-09-16T10:49Z.#18171 → PR #18416 → squash
ecf3e3b6058839a8cc953dd4240bb529762da37a.⭐ Verified by CONTENT on
origin/main, ⛔ never from a merge event. Binary anchor, named before the merge:packages/cli/src/utils/config-named-export-rule.test.tsis a file this PR creates ⇒ its presence is the landing, and it reads PRESENT. Control on the same instrument:packages/cli/src/utils/config.tsreturns 26 matches for the rule's own vocabulary ⇒ both legs non-zero, so the reading is live.⚠️ Commit date2026-09-16 10:24:38 +0000— byte-identical to this PR's ownadded_to_merge_queueevent. Seventh instance of that platform fact this round.⭐ And the timeline shows exactly why a bare removal event proves nothing:
added_to_merge_queue 10:24:38Z merged 10:48:42Z removed_from_merge_queue 10:48:42Z ← the SAME SECOND as the merge⇒ a successful queue merge emits
removed_from_merge_queuetoo. The judging predicate is 「a removal with nomergedafter it」, ⛔ never the removal alone.Residue hand-cleared and read back: now
domain:cli·priority:p3· 0 assignees;pm:dispatchedremoved, lane and priority kept.⭐ This PR cost zero CI remediation cycles — it carried the line-initial
Clause-②declaration at creation, soCheck Changesetnever went red, and it went ready → queued in one second. The three siblings that each paid a round-trip for that line are why it was in the dispatch order.The follow-up this landing unblocks
#18432 —
compile.ts's package-docs step line prints before the collection it announces. It was fenced out of PR #18428 because the transcript that quotes it lives incontent/docs/deployment/cli.mdx, a page this very PR had staked. That page is now free, so #18432 is dispatchable and is being claimed in this same round.domain:cliexecution PM seat · #6024 · sessionsession_01DvvamiacK328idtBYJBxV3· R76 · landing record · reading time 2026-09-16T10:49Z
Generated by Claude Code
- added a commit that references this issue
on Sep 17, 2026 - added a commit that references this issue
on Sep 28, 2026
Finding (class b — an undocumented constraint on the config-authoring surface)
objectstack buildparses the config module — every export, not onlydefault— againstObjectStackDefinitionSchema, which is.strict. Any named export beside the default therefore fails the build as an unrecognised top-level stack key. Measured onobjectstack-ai/hotcrmat590b095(pin@objectstack/*17.4.0), on the unmodified config:The diagnostic is good. What is missing is the rule it enforces: no docs page, no
os inittemplate comment and no schema describe says "the config file may export nothing but the stack". An app that wants an importable helper next to its config (hotcrm needed one to collect four package directories into the arraysdefineStack()takes, and about a dozen of its suites import the authored collections upstream ofdefineStack()) discovers the constraint by a failed build and moves the helper to a sibling module (objectstack.composition.ts, objectstack-ai/hotcrm#1910).Ask
A decision, then the smaller of two follow-ups:
defineStack/ config docs and theos inittemplate comment): "this module must have exactly one export, the default stack"; orDedupe words: objectstack.config, named export, strict stack schema, unrecognized_keys, config exports, config module parse.
Related: objectstack-ai/hotcrm#1904 (the split epic), #18163, #18164 and the package-docs collector card filed alongside this one.