Repository navigation
Commit c1f311f
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)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DVSVpxVzADN1RG8iaqN34ff)_
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.
<details><summary>Ablations (each mutation written by
scripts/ablation-replace.mjs, proven on disk, then restored with git
diff HEAD empty)</summary>
| 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.
</details>
## 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.
<details><summary>52 gates, exit codes at f8d3de9</summary>
```text
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
```
</details>
Size: 26 files, +518 / −123 against the merge base (641 changed lines),
under the 3,000-line threshold.
## Human Minimum
- [x] 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.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; repo-wide `pnpm lint` is left to CI.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. `@objectstack/scenarios` is `private: true` and the cards are
docs, so nothing publishes: `skip-changeset`. Both declaration lines are
above.
- [x] 6. `packages/spec` not touched.
- [x] 7. No hand edits to generated or release-owned files.
- [x] 8. Title, body and comments are in English; the 维护者速读 section is
the seat's Chinese summary.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016WV4cyKDwJet4nwPKBn5KP
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a77c2e9 commit c1f311f
26 files changed
Lines changed: 520 additions & 123 deletions
File tree
- docs/qa/scenarios
- packages/qa/scenarios
- src
- runner
- score
- test
- runner
- score
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
40 | 40 | | |
41 | | - | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
42 | 44 | | |
43 | 45 | | |
44 | 46 | | |
| |||
153 | 155 | | |
154 | 156 | | |
155 | 157 | | |
156 | | - | |
| 158 | + | |
157 | 159 | | |
158 | 160 | | |
159 | 161 | | |
| |||
164 | 166 | | |
165 | 167 | | |
166 | 168 | | |
167 | | - | |
168 | | - | |
| 169 | + | |
| 170 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
3 | 4 | | |
4 | 5 | | |
5 | 6 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
3 | 4 | | |
4 | 5 | | |
5 | 6 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
3 | 4 | | |
4 | 5 | | |
5 | 6 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| |||
0 commit comments