Skip to content

feat(scenarios): cards carry an app code; every scenario app is built in the demo+code namespace and a bare item name fails its build - #30837

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

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

domain:devx#1 · os-elon-musk · session_016WV4cyKDwJet4nwPKBn5KP

Fixes #30795
Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:每张场景卡新增 code(2–8 个小写字母,全库唯一,加载器拒收缺失、畸形、重复);运行器把每个场景应用固定放进 demo+code 命名空间(如 democrm);构建提示要求每个元数据项都带前缀,评分器把每个不带前缀的非对象项记为一条 Build 失败,这样的构建不会成为模板或画廊候选。
  • 为什么改:画廊需要稳定、可读、互不冲突的名字。此前命名空间随脚手架而变,且裸名在两个画廊应用共用一个环境时会撞名。
  • 风险与回滚:只动私有、不发布的 @objectstack/scenarios 包和场景卡 JSON 及两份 README;回滚即 revert。
  • 先看:① packages/qa/scenarios/src/runner/orchestrate.ts 的 template 模式(宿主名改为 hostCODE,如 hostcrm,安装前先取环境基线再扣除);② src/score/scenario-naming.ts(裸名判定覆盖哪些集合);③ src/loader.ts 的跨卡 code 去重。
  • 两分钟验证:pnpm --filter @objectstack/scenarios exec vitest run test/runner/orchestrate.test.ts,应看到 9 个测试全绿,其中一个证明「带裸名视图和仪表盘的构建 Build 不通过、template 模式被跳过」。
  • 需要你做的:无,席位落地。

What changes

1. A card code (schema.ts, loader.ts). Every card now carries code: 2–8 lowercase letters, no digit, no underscore. The schema refuses a missing or malformed code. Uniqueness is a property of the library, so loadScenarios judges it across the directory: every card that shares a code is refused at code, naming the other file(s), even when that card is also refused for another fault. loadScenarioFile, given one file, cannot judge it, and its docblock says so. scenarioNamespace(card) (schema.ts) is the one place that spells the namespace: demo + code.

Every card under docs/qa/scenarios/ has a code:

Card code Namespace
asset_lifecycle asset demoasset
crm_account_with_contacts crm democrm
invoice_with_line_items invoice demoinvoice
leave_balances leave demoleave
order_to_cash order demoorder
project_with_tasks project demoproject
requisition_to_receipt purchase demopurchase
service_desk helpdesk demohelpdesk
travel_expense travel demotravel

The longest namespace is 12 characters, inside create-objectstack's 13-character scaffold cap.

2. The demo+code namespace in both modes (runner/orchestrate.ts, runner/namespace.ts, runner/cli.ts).

  • create-objectstack derives the namespace from the project name, so scratch mode scaffolds the project as demo+code (democrm), in a directory of its own per card and mode. Before the build, the runner checks the scaffolded config declares that namespace. A project declaring any other namespace is skipped with the reason and never read against it.
  • Template mode installs the verified build. That build is the card's own scratch build, which declares demo+code, or a supplied template, which is skipped with the reason unless it declares demo+code. The unmanaged copy keeps that namespace, so the app scored in template mode is in demo+code too. The blank host project is scaffolded as host+code (hostcrm). See the first acceptance note for why it cannot be demo+code.

3. Every item is prefixed (runner/prompts.ts, score/scenario-naming.ts, score/scenario-tags.ts, runner/scorecard.ts).

  • Every authoring prompt (build, iterate, template iterate) states the rule: name every metadata item with the namespace prefix, not only objects. The data contract names each persona's permission set with the prefix too (democrm_sdr, was sdr), and the runner grants that name.
  • gradeItemNames(artifact, namespace) reads every manifest collection the spec lists in PLURAL_TO_SINGULAR except objects, through resolveStackCollection, so a multi-package artifact is judged too. Each item whose name does not start with the namespace and an underscore is one failing check, bare-name:TYPE:NAME. A clean build adds no check, so the rule never pads a score.
  • scoreCardArtifact passes the card's namespace, so ScenarioArtifactScore.naming carries these checks and counts them in passed / total. buildPassed also requires naming to be clean. So a build with one bare item is never verified: no template install and no gallery candidate. The scorecard lists each bare item as a finding (build: bare item name view all_accounts (…)), and the run record carries the checks as Build evidence.

4. Namespace-relative reading kept (ruling A on #30292), now always against demo+code. Objects are read as before. Because every item is prefixed, the other names a card states are read the same way: expected dashboards, every nav target, agent action names, and permission sets. Otherwise the scorer would require a bare dashboard:overview and also report it as a finding. Fields, row ids, persona ids and truths are unchanged.

Tests

pnpm --filter @objectstack/scenarios typecheck && pnpm --filter @objectstack/scenarios test at f8d3de9a: typecheck exit 0 (the tsconfig includes test/**), Test Files 18 passed (18), Tests 238 passed (238), VERDICT command-exit 0.

  • refusals.test.ts: a missing code, an underscore code, and a nine-letter code (each one refusal at code). Two cards sharing crm are both refused, each naming the other, with a control where distinct codes load. A shared code is also named on a card refused for another fault.
  • test/score/entry.test.ts: bare permission:sdr and view:all_accounts are two failing checks with exact details, objects and prefixed items add none, and the total grows by 2 while passed does not. A multi-package artifact's bare dashboard is found under packages[].
  • test/runner/units.test.ts: namespacedScenario prefixes objects, agent action names, dashboards and nav targets, and is idempotent. permissionSetName and templateHostName are tested. Every authoring prompt states the rule, and the contract names democrm_partner_user and democrm_delete_contact. withoutItems keeps only what the install added.
  • test/runner/orchestrate.test.ts: the crm card end to end. The scaffolds are crm-account-with-contacts-scratch/democrm and crm-account-with-contacts-template/hostcrm, and the seed lands on democrm_*. The grants are democrm_sdr and democrm_partner_user. The installed copy declares democrm, and the template build is not degraded although the fake environment serves setup and admin_full_access unprefixed. A scaffold declaring crm_account_w, and a supplied template declaring crm, are each skipped. A build with a bare view and a bare dashboard passes every expect check but is degraded, lists one finding per item, and template mode is skipped.
Ablations (each mutation written by scripts/ablation-replace.mjs, proven on disk, then restored with git diff HEAD empty)
Mutation Expected red Observed
loader: if (false && others.length > 0) (no duplicate-code refusal) the two shared-code tests 2 failed / 45 passed
scorer: if (true) continue; (no bare-name check) entry naming ×2 + orchestrate bare-name 3 failed / 15 passed
buildPassed ignores naming orchestrate bare-name (template not skipped) 1 failed / 8 passed
templateHostName returns the app's namespace units host test + orchestrate scaffold list 2 failed / 34 passed
template mode scores after without the baseline subtracted orchestrate clean template run (bare-name:permission:admin_full_access) 1 failed / 8 passed

The package is source-only (exports point at src/), so no rebuild sits between a mutation and the test run.

Acceptance notes

  • Deviation: the template-mode host is host+code (hostcrm), not demo+code. Measured live on create-objectstack@18.3.0 with objectstack dev --fresh. Installing a democrm package unmanaged into a host scaffolded as democrm answers 409 RESOURCE_CONFLICT: "The namespace 'democrm' of package 'com.example.democrm' is already owned by 'com.example.democrm'. The unmanaged copy keeps the source namespace, so it cannot sit beside a package that owns it". The same install into a host scaffolded as hostcrm answers 200, targetPackageId: com.example.democrm-copy, namespace: democrm. The refusal is step 6 of the import verb in packages/metadata-protocol/src/protocol-package-commits.ts. So the scored app is in demo+code in both modes, and only the host is renamed.
  • Template-mode scoring now subtracts what the environment served before the install. Measured on the same blank env: /api/v1/meta/* serves unprefixed platform items (app setup, dashboard system_overview, permission sets such as admin_full_access, positions, datasource default). Without the subtraction, every template build would carry bare-name findings for items that are not the app's. The subtraction also keeps the platform setup app's nav out of the nav-resolves: checks, which already read every app in the environment's metadata before this change. os build of a blank project emits only the project's own items (measured: democrm_note plus 3 plugins), so scratch mode needs no subtraction.
  • Permission sets, agent actions and dashboards are read namespace-relative too (see What changes §4). These are names the card states, and leaving them bare would contradict the prefix rule.
  • Card count: docs/qa/scenarios/ holds 9 cards on c17719d. The 10 in the order counted README.md.
  • code placement: the line right after id in each card.
  • Out of scope, per the card: install-time re-namespacing and the platform-wide bare-name question (decision card). Objects are not judged by the naming check, because defineStack already refuses an unprefixed object.

Verification

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (exit 0) derived 52 families for this change set at f8d3de9a (base c17719dd). All 52 ran at f8d3de9a. 51 exited 0 on the first pass. pnpm check:dual-build-cjs-loads first exited 3 (prerequisite: built output); after turbo run build --filter='@objectstack/dogfood^...' --filter='@objectstack/runtime^...' it exited 0 (116 require entry points across 71 packages load). dispatch-gates.mjs --ran on the record: exit 0, "52 derived famil(ies) accounted for — 52 run, 0 NOT-MEASURED".

Head note: this draft opens before #30802 lands, by the seat's call, so CI runs early. #30802 edits the same loader, refusals.test.ts and three cards. After it merges, this branch merges origin/main, re-runs the package suite and the gates on the new head, and the seat gets that head.

52 gates, exit codes at f8d3de9
node scripts/check-adr-links.mjs :: exit 0
node scripts/check-adr-links.mjs --self-test :: 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-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
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:ci-filter-parity :: 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:dual-build-cjs-loads :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: 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: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: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

Size: 26 files, +518 / −123 against the merge base (641 changed lines), under the 3,000-line threshold.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (packages/spec is untouched; the card schema is the private package's Zod schema.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; repo-wide pnpm lint is left to CI.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. @objectstack/scenarios is private: true and the cards are docs, so nothing publishes: skip-changeset. Both declaration lines are above.
  • 6. packages/spec not touched.
  • 7. No hand edits to generated or release-owned files.
  • 8. Title, body and comments are in English; the 维护者速读 section is the seat's Chinese summary.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cyKDwJet4nwPKBn5KP

… in the demo+code namespace and a bare item name fails its build

- schema/loader: a required `code` (2-8 lowercase letters); the loader refuses a
  missing or malformed code, and every card sharing one across the library.
- every card under docs/qa/scenarios gets a code.
- runner: scratch mode scaffolds the project as demo+code; template mode installs
  a template declaring that namespace into a host named host+code (the unmanaged
  copy keeps its namespace and the import refuses one the host owns).
- prompts state the every-item-prefixed rule; persona permission sets, agent
  actions and expected dashboards are read namespace-relative too.
- scorer: one failing Build check per bare non-object item name; a build with
  one is never verified. Template mode scores the installed app, not the
  environment's own items.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cyKDwJet4nwPKBn5KP
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Nov 6, 2026
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests labels Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/scenarios, touching 34 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/qa/scenarios/README.md, packages/qa/scenarios/src/runner/cli.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/data-api.mdx (via /:object/export (route, bridged from symbol templateMode — its route source's handler names it))
  • content/docs/concepts/metadata-lifecycle.mdx (via /:object/export (route, bridged from symbol templateMode — its route source's handler names it))
  • content/docs/data-modeling/schema-design.mdx (via /:object/export (route, bridged from symbol templateMode — its route source's handler names it))
  • content/docs/deployment/cli.mdx (via /:object/export (route, bridged from symbol templateMode — its route source's handler names it))
  • content/docs/permissions/permission-sets.mdx (via /:object/export (route, bridged from symbol templateMode — its route source's handler names it))
  • content/docs/protocol/objectui/actions.mdx (via /:object/export (route, bridged from symbol templateMode — its route source's handler names it))
  • content/docs/ui/actions.mdx (via /:object/export (route, bridged from symbol templateMode — its route source's handler names it))

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

  • content/docs/releases/v17/17-5.mdx (via /:object/export (route, bridged from symbol templateMode — its route source's handler names it))
  • content/docs/releases/v17/17-6.mdx (via /:object/export (route, bridged from symbol templateMode — its route source's handler names it))
  • content/docs/releases/v18/18-3.mdx (via /:object/export (route, bridged from symbol templateMode — its route source's handler names it))

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
  • 2 changed file(s) yielded no anchor (packages/qa/scenarios/README.md, packages/qa/scenarios/src/runner/cli.ts) — 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 63 of 222 client-bound route-ledger rows — the other 159 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 159: 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; 104 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 — 0 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 25e1c1ad8bec483748d9a7e36adf6676aa8df895 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 25e1c1ad8bec483748d9a7e36adf6676aa8df895

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

…rror-code casing guard does not read it as an emission

`check:error-code-casing` (ADR-0112 D1) reads a `code: 'lowercase'` pair
in an object literal as an error.code emission. The twin card's app code
`crmtwin` is a namespace code, not an error code, so the test now assigns
it the way the refusal cases already do. The guard is unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cyKDwJet4nwPKBn5KP
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#1 · os-elon-musk · session_016WV4cymvo9YoyLwKCXXBNw

席位核对 · ACCEPT · #30795 · 2026-11-06T06:46Z

Head-sha: bea14b472850ce018c6b8ce68c9ccf0ad97920a6

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 skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scenario cards carry an app code; the runner and gallery use the demo<code> namespace and refuse a bare non-object item name

2 participants