Repository navigation
feat(scenarios): a gallery exporter writes verified, generic scenario apps to a local directory in the layout of objectstack-ai/gallery - #30930
Conversation
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
…spec's binding, and the first shot's page is visited once unshot Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
📓 Docs Drift CheckThis PR changes 1 package(s): 49 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 0 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 41fa412d8f583717d5bec7badeb02bcb76e16a1f && git checkout 41fa412d8f583717d5bec7badeb02bcb76e16a1f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 33043bd8a2b34ed2efdc188f38173c1c44ca3f36 30360bf2c66a057d02fe1e8dee157d425953f7e6 && git checkout -B drift-repro 33043bd8a2b34ed2efdc188f38173c1c44ca3f36 && git merge --no-ff 30360bf2c66a057d02fe1e8dee157d425953f7e6
node scripts/docs-audit/affected-docs.mjs --json 33043bd8a2b34ed2efdc188f38173c1c44ca3f36
|
…asured turn The scorecard counts a turn that rests on a refused seed row in neither passed nor total, so its half can still read `pass`. The exporter now reads every Run half's questions and operations and refuses the app when any of them is marked not measured, naming each such turn. A previous export of that app is removed like any other no-longer-passing app. Pinned by tests: a refused seed row (crm contact ct4) keeps every half verdict at `pass` and still fails judgeRecord; an operation marked not measured is its own reason; exportGallery refuses the app and removes its earlier export. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
|
domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8 Contract reviewServed-tier: Inputs: card #30349 body and all 10 comments (the claim 6683477231 and the two dev reports read as claims), follow-up #30932, PR #30930 body, labels and file list, the net diff ① Derived judgmentsGallery rule 1 — only scenarios that passed every half. Gallery rule 2 — nothing customer-derived. Gallery rule 3 — no secret, no local path. Persona API keys never reach the record ( Gallery rule 3 — the ② The transcript. ③ Re-export. Each app is written whole to ④ Screenshots. ⑤ The landing. Check-runs on c78ea5a: 68 runs, all ② Semver levelNone owed. ③ Boundary flags
④ Design judgmentAgainst the gallery's rules (AGENTS.md: exporter-only writes to Must-fix
Should-fix
Implemented-by: VERDICT: FAIL |
…ed pages, seed tally, empty runs, shots kept M1. With --work, the record still names the work directory of the machine that ran it (evidenceDir, every build tail and error text), and only --work was redacted. Every directory that names the run (the record's evidenceDir, --work, and the real path of each that exists here) is now replaced by $WORK in run-record.json, scorecard.json/.md, transcript.json/.md and the README, longest first and only at a path-name boundary, so /tmp never rewrites /tmpfoo. S1. A shot whose navigation answers no response or a status outside 2xx (304 counts as shown), or whose page draws one of the Console's own not-found views, is refused: skipped with the reason, its file removed, never exported as proof. S2. seedApp tallies into the half's own seed again, so an insert that throws leaves the partial inserted/refused tally in the record next to the error, as on main. S3. A runs directory with no scenario record, over an index that lists apps, throws (the CLI exits non-zero) and touches nothing. S4. An export without screenshots never replaces an app whose previous export had them: the old directory and entry are kept and the summary reports it as kept. An app that no longer passes is still removed. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
|
domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8 Contract reviewServed-tier: ① Derived judgmentsM1 — S1 — failed pages, was should-fix. Fixed, and the markers hold on the released Console. S2 — seed tally on a throw, was should-fix. Fixed. S3 — empty runs over a non-empty index, was should-fix. Fixed. S4 — Anything else touched. PR body vs tree. The live body (head 30360bf) describes round 3 in the maintainer brief, the Check-runs on 30360bf: every run ② Semver levelUnchanged: none owed, private unpublished package, ③ Boundary flags
④ Design judgmentThe delta closes the one rule the round-2 tree broke: with Must-fix: none. Should-fix: none. (Notes above: the Implemented-by: VERDICT: PASS |
|
domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8 席位核对 — ACCEPT ·
|
Requested by 田騏 · project thread
Part of #30349
Clause-②: no
Metadata-protocol: none
维护者速读
export-gallery:读本地跑分器写下的跑分记录和项目目录,把每一半都通过的场景应用按 objectstack-ai/gallery 的布局写到本地目录:应用源码、跑分记录与计分卡、每个角色的问数问答记录(Markdown 加 JSON),以及 Playwright 对启动后应用截的列表、看板/日历(卡片打了标签才截)、记录页和仪表盘截图,外加一份经 zod 校验、排序稳定的index.json。重导会先删旧目录;不再通过的场景从目录和索引里移除。@objectstack/scenarios(QA 工具),只写本地目录,不向 gallery 仓库写任何东西。卡片目前没有「通用」标记,所以导出器现在拒绝所有卡片,画廊保持为空,直到你决定标记字段。回滚即还原本 PR。Part of #30349。--work时,记录里原机器的工作目录(evidenceDir)和--work两者都替换成$WORK,输出里不再出现任何一方的绝对路径;页面没真正显示(非 2xx,或 Console 自己的「未找到」视图)的截图被拒绝并报告;种子写入中途抛错时,已写入/被拒的部分计数留在记录里(与 main 一致);--runs里没有任何跑分记录而旧索引列着应用时拒绝执行、什么都不删;--no-screenshots不会用无截图的条目替换原本有截图的条目,旧目录和条目保留,摘要里标为 kept。packages/qa/scenarios/src/export/generic.ts(只导出通用卡的守卫);packages/qa/scenarios/src/export/pass.ts(「每一半都通过」怎么从记录里读);packages/qa/scenarios/src/export/exporter.ts(删除与替换的规则)。pnpm --filter @objectstack/scenarios exec vitest run test/export,预期 44 个用例全部通过,其中 "generic only" 用例断言默认守卫拒绝开放库全部 9 张卡。write_path_options里选一条写入 gallery 的路径,并决定卡片的「通用」标记字段。What changes
A gallery exporter in
@objectstack/scenarios(src/export/, CLIpnpm --filter @objectstack/scenarios export-gallery --runs DIR --out DIR, in the style ofsrc/runner/cli.ts). It reads the run records and project directories the local runner writes and writes verified apps to--outin the layout of objectstack-ai/gallery. ⛔ It writes nothing to objectstack-ai/gallery and adds no workflow; the write path is a design item for the maintainer (thewrite_path_optionsin the report comment).Which record, and "passed every half" (
pass.ts). The newest record of each card decides (bydate). Its per-half verdicts are read from the record'sresults, the rowsrunRecordwrites, and nothing is re-scored. A half passes only onpass:partial,fail,blocked,not-runand a missing row are not a pass. The record must hold scratch mode, because that is the project the gallery ships. Scratch mode owes all six halves (Build, Run-问数, Run-operations, iterate Build, iterate Run-问数, iterate Run-operations), so a run with--no-iterate-rundoes not pass. Template mode, when present, owes its three iterate halves. Its first Build and Run rows arenot-runby construction and are not asked. ⛔ A record with any not-measured turn does not pass, even when every half readspass(round 2, decision 4). The scorecard counts a turn that rests on a refused seed row in neither passed nor total, so its half can still readpass.judgeRecordtherefore reads every Run half the record holds (scratch Run, both modes' iterate Run) and names each question or operation markednotMeasuredas a reason.Generic only (
generic.ts). A card is exported only when it is explicitly marked generic. The card schema has no such marker:provenanceis free prose, and the open library's directory is not a marker because the loader reads any directory, the private overlay included. SoNO_GENERIC_MARKER, the guard the CLI uses, refuses every card, and the CLI has no flag to pass another guard. The exporter takes the guard as a parameter so its tests can run the export path with a double.schema.tsandloader.tsare untouched (#30846 is in flight there).The
apps/SCENARIO/layout (exporter.tsAPP_LAYOUT):README.md(title, requirement, the verdict of each owed half, the scorecard table, links),source/(the authored metadata source: the allow-listobjectstack.config.ts,objectstack.manifest.json,package.json,tsconfig.jsonand thesrc/tree; nevernode_modules,dist,.agents/.claudeskills,.envor scaffold boilerplate; a symbolic link refuses the export),run-record.json(the record as read, with every work directory that names the run replaced by$WORK, see below),scorecard.jsonandscorecard.md,transcript.jsonandtranscript.md, andscreenshots/KIND.png.No local path (
record.tsredactWorkDir,exporter.tsworkDirsOf). A record names the work directory of the machine that ran it:evidenceDir, every build command tail, any error text.--work DIRnames where the projects are read on this machine. Both, and the real path of each that exists here, are replaced by$WORKinrun-record.json,scorecard.json,scorecard.md,transcript.json,transcript.mdandREADME.md. The longest is replaced first, and only at a path-name boundary, so/tmpnever rewrites/tmpfooor/x/tmp. The authoredsource/files are copied as written.The transcript (
transcript.ts). The record keeps each turn's grade but not its reply. The reply is in the run's recording (run-scenarios --record F, passed as--recording F). Every recorded reply is graded again withgradeCardAnswer, exactly as the runner grades it, and a grade that differs from the record's refuses the export. A recording from another run therefore never reaches the gallery as this run's answers. An asked turn with no recorded reply also refuses the export.Screenshots (
screenshots.ts). This is a separate step that--no-screenshotsskips. It boots the scratch project with the runner's ownbootDev, seeds it with the runner's own seed loop, and signs in through the Console's sign-in door.planShotschooses the views from the card and the build alone:records-forms.kanban-drag-persistence), showing a view of that type the build declares, read through the spec'sderiveViewContainerObjectandexpandViewContainer;A tagged view the build lacks, or a refused seed row, is a skipped shot with its reason. A page the Console did not show is never proof (
pageRefusal): a navigation that answers no response or a status outside 2xx (a 304 revalidation counts as shown), or a page that draws one of the Console's own not-found views (CONSOLE_NOT_FOUND: theobject-not-found,object-package-disabled,record-page-not-foundandform-not-foundtest ids, and the router'sPage not foundtitle), is a skipped shot with the reason, and its file is removed. A screenshot step that fails refuses the app instead of exporting it without its proof.Re-export replaces (
exporter.ts). Each app is written whole into a staging directory and swapped in after the old directory is deleted. The exporter owns what the previousindex.jsonlists. An app it listed that does not export this time is deleted and leaves the index, and the summary says why. That covers an app whose newest record no longer passes, an app whose card is gone or not generic, and an app whose project is gone. A directory the index never listed is left alone and named. An invalid previous index stops the export before anything is touched. So does a runs directory with no scenario record while the previous index lists apps: that is a wrong--runs, and the CLI exits non-zero. An export with--no-screenshotsnever replaces an app whose previous export had screenshots: the old directory and entry are kept and the summary lists the app askept. An app that no longer passes is still removed.index.json(manifest.ts, zod, strict at every level, validated before it is written). It holdsschemaVersion, plus one entry per app, sorted by id. Each entry hasid,code,namespace,path,function(the card'scategory),title(enfrom the card'stitle;zhisnullbecause the card schema has no Chinese title),summary(prompt.short),capabilities,screenshots,qa(questions,turns,personas),operations,framework(versionis the@objectstack/specpin of the generated project;runneris the record'senv.framework),runDateandrun. There is no export timestamp, so a re-export with no change is a no-op diff.Runner touch-ups, no behaviour change.
orchestrate.ts: the seed loop moves out ofrunHalfinto an exportedseedApp(the screenshot step reuses it), which tallies into the half's ownseedso an insert that throws still leaves the partial tally in the record, as on main, andmodeDir(card, mode)names theCARD-scratchdirectory the exporter finds the project in.runner/cli.ts:ADMINandfindUpare exported.package.json: the./exportentry, theexport-galleryscript, andplaywright-core1.63.0, the version already in the lockfile for dogfood and service-print. The lockfile change is the three-line importer entry.Tests
test/export/exporter.test.ts(28 tests) runs on a realrunCardrun of the realcrm_account_with_contactscard, in both modes. Its fixture (test/export/fixture.ts) writes real project directories under a temp work directory with in-memory app doubles, wrapped in the runner's ownRecordingDriver. It pins:node_modules,.agents,.claude,.objectstack,.env,README.mdandDockerfile;$WORKredaction;apps/and the index, while an unowned directory is kept;exportGalleryand end to end through the CLI over the real card directory;workDir, a record whose strings name another machine's directory: neither that directory, norworkDir, nor its real path appears in any file under--out, and the redaction's path-name boundary;exportGalleryand the CLI, with the tree unchanged, while an empty runs directory with no index writes an empty index;--no-screenshotskeeping an app that had shots, byte for byte, and reporting it askept, while a newer failing record still removes it;ct4) keeps every half atpassand still does not pass, with one reason per not-measured question and operation in each Run half; an operation marked not measured is its own reason; andexportGalleryrefuses the app and removes its earlier export fromapps/and the index.test/export/screenshots.test.ts(16 tests) tests view selection with a double:os buildwrites;captureScreenshotswith boot, harness and browser doubles runs boot, then seed, then shots, then close, then stop;pageRefusalon 2xx, 304, no response, other statuses and the Console's not-found views, andcaptureScreenshotsskipping a 500 page and a not-found page with their reasons, their files removed.test/runner/orchestrate.test.ts(one more test): a seed insert that throws mid-loop leaves the partial tally (2 inserted, 1 refused) in the half next to its error. The same test fails against the round-2seedApp.Acceptance notes
index.jsonand every text file are stable; a re-export with screenshots rewrites the PNGs.apps/README.mdsays it "is replaced when the first export lands". The gallery's AGENTS.md lists it as hand-written, so the exporter does not write it. The layout above is what it should describe.main(the first-run checklist changeset).Verification
Real boot reading: create-objectstack 18.3.0, a hand-written fixture app, Chromium
No model was run, so no generated app exists here. Instead, a project was scaffolded with
npx create-objectstack@latest democrm(18.3.0). A fixture app was written into it by hand: account and contact objects, a kanban list view, a dataset and dashboard, and appdemocrm_crm.os buildpassed. The project was placed atWORK/crm-account-with-contacts-scratch/democrm, andexportGalleryran with the realcaptureScreenshots(bootDev,HttpApp,playwrightBrowseron/opt/pw-browsers/chromium), using the crm card with a kanban tag added. Result: exported, four shots, none skipped —/_console/apps/democrm_crm/democrm_account,…/democrm_account/view/democrm_account.board,…/democrm_account/record/ac1,…/dashboard/democrm_overview. The PNGs show the seeded grid, the kanban grouped by industry, the Acme record page with its two contacts, and the dashboard with 3 accounts by industry. The first reading found the currency dialog over the list shot; the warm-up visit was added and the reading repeated clean. A first attempt from inside vitest got401on the admin sign-in, because vitest'sNODE_ENV=testreachedobjectstack dev, so no dev admin was seeded. The CLI runs under tsx without it. This reading is a one-off and is not a permanent test.Gates (local subset, derived by dispatch-gates for the final diff)
Head
30360bf2c6(round 3).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 64 commands, the same list as rounds 1 and 2; each exit code is captured before any pipe.--ranreconciliation:✓ dispatch-gates --ran: 64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED (a DERIVED zero — all 64 recorded an exit code and none of them is 3).Package:
pnpm --filter @objectstack/scenariostsc --noEmitexit 0;vitest runexit 0,Test Files 20 passed (20),Tests 290 passed (290).test:repois not run locally; it belongs to CI.🤖 Generated with Claude Code
https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8