Repository navigation
Dogfood pass on M3-complete main: drive the whole contract lifecycle in a browser and report what a real user hits #45
Description
Maintainer request, 2026-09-09: 「使用浏览器测试,并提出优化改进意见。」
⛔ Observation only. This card produces NO code and NO pull request. Its deliverable is a report: what the browser actually did, and where the product hurts. The PM turns that into cards. ⛔ Do not fix anything you find, however tempting or small — a fix inside an observation pass is an unreviewed change with no card behind it.
Why now
main @ a7b7db5 is the first tree where M1 · M2 · M3 are all complete. The reminder layer landed hours ago (src/flows/ went 3 → 11). Nothing has ever been driven end-to-end on a tree where the whole lifecycle exists — every prior browser pass verified one card's surface. This is also the pre-read for card 14, whose acceptance is 「陌生人 clone 一条命令跑起」.
Two passes, and the first one matters more than it looks
Pass 1 — 开箱, as a stranger. ⛔ Do NOT perform the README operator setup.
Clone-equivalent state, pnpm install, pnpm demo, open the browser, sign in as the seeded dev admin, and stop there. Report exactly what a first-time evaluator sees: which navigation groups appear, which are empty, what the first screen says, and what — if anything — tells them what to do next.
This is the measurement behind decision cards #28 and #11. Prior runs established that without the operator setup (three requester accounts, clm_requester grants, dev admin into clm_admin) the app serves navigation: [] and 我的合同 is blank. Re-measure it on this tree rather than assuming it — M3 added an inbox and eleven flows since, and the dev admin's position may now resolve differently.
Report it as a first-run experience, not as a defect list: what would a buyer conclude in the first sixty seconds?
Pass 2 — the full lifecycle, after the operator setup
Do the README setup, then drive a contract all the way through as the right audience at each step — intake → review/deviation → approval ladder → signature and execution formalities → activation → obligations and payments → renewal or expiry → archive. Use the five audience groups (requester, legal, finance, records, admin) rather than doing everything as admin: a screen that only works for an admin is not a working screen.
Then open the inbox and the three dashboards, and do one full pass in zh-CN.
Known suspects — verify, ⛔ do not assume
Each of these is on the record from earlier runs. Say for each whether it still reproduces on this tree:
navigation: []without the operator setup ([Decision]pnpm demo开箱即用时没有任何账号能打开「我的合同」——要不要让脚本建账号,与 #11 第一问耦合 #28, [Decision]DESIGN.md§04 makesclm_requesterevery employee's default, but a set that grantsclm_requester.accesscannot bind toeveryone#11) — the pass-1 measurement above.- 21 fields declared with no consumer ([Decision] 17.4 的新规则指出 21 个字段声明了却没人读——展品该给它们消费者,还是删掉声明 #38) — 17.4's new
field-no-consumersrule reportsclm_party.address/bank_account/contact_phone,clm_signature.envelope_id/signers,clm_review.internal_noteand more. Which of them show up as a visible hole in a screen a user actually opens? That is the reading [Decision] 17.4 的新规则指出 21 个字段声明了却没人读——展品该给它们消费者,还是删掉声明 #38's decision needs and does not have. - The backfill action is invisible to legal ([Decision] §06 F16 说补录动作「档案岗**或**法务可用」,但实现只能给档案岗——权限是 AND,没有两边都持有的能力 #43) — records + admin only, because the permission gate is an AND. Confirm by signing in as legal.
- F3's notifications reach only half their targets — the card-09 run measured 10 contracts over 30 days and 5 notified, because only 5 carry a
legal_owner; the rest took a "nobody to tell" edge. Is a silent no-recipient path acceptable to a user who expects to be nudged? pnpm demostops being re-runnable once the jobs have run (pnpm demostops being re-runnable once the daily jobs have run: the seed re-asserts statuses the state machines refuse to go back to #41) — reproduce it and describe what the operator sees.- Chart category labels render English in a
zh-CNconsole on same-object dimensions, and view descriptions are declared, walked, counted as covered, and not rendered — both routed upstream from the card-11 run. Confirm on 17.4.
What "优化改进意见" means here — the actual deliverable
⛔ Not a bug list. For each thing you report, say what a user was trying to do and what got in their way, then propose the smallest change that would fix it. Rank by how much it costs the product, not by how easy it is to fix.
Specifically wanted:
- The first-sixty-seconds problem. If a buyer's first screen is empty or confusing, that outranks every other finding.
- Dead ends — any screen where the obvious next action is missing, disabled, or refuses without saying why.
- Places the product lies — a label, count, description or empty state that does not match what the data actually says. This repo has been bitten repeatedly by annotations that were true once; the same class exists in the UI.
- Anything an evaluator would have to be told rather than being able to see.
- Where a platform limitation is the cause, say so and name it — those go upstream, ⛔ not into this app.
Reporting
- Screenshots for anything visual. Console and network readings for anything behavioural —
GET … → 200 {"total":0}beats "the list looked empty". - ⛔ Do not report a "missing / broken / unreachable" finding from a post-navigation DOM read alone. Confirm with a screenshot or the server's own metadata response first — a React hydration artefact reads exactly like a P0 and is not one.
- Two known-pre-existing platform ERROR lines appear on any boot after the first (
objectstack#17175,objectstack#17176). Those are not yours. Anything else in the boot log is worth reporting. - Note the tree you measured (
a7b7db5) and the@objectstack/*version (17.4.0) on every reading.
Environment notes
/home/user/hotclm exists but sits on a stale branch with no node_modules; the container has restarted twice this session, so assume nothing about local state. Chromium is pre-installed at /opt/pw-browsers/chromium-1194/chrome-linux/chrome — ⛔ never run playwright install. Work in your own worktree off origin/main @ a7b7db5, and pick a non-default port.
Activity
zhuangjianguo commentedon Sep 9, 2026
Claim: PM loop round 3 (seat
repo:hotclm). Dispatched by the PM seat for its dev; the dev inherits this claim and posts no second one.Session:
session_01R3n3GGzobdegM4HUzah1iRBranch: none — ⛔ this card produces no commits. A worktree is needed only to boot the app; nothing is committed to it and no PR is opened.
Worktree:
hotclm-dogfood-45(dedicated, offorigin/main@a7b7db5)Domain:
repo:hotclm(single-lane repo — nodomain:*axis)File surface: ⛔ none. Read-only observation. The only writes are to a local gitignored database and a scratch directory. Any urge to edit a source file is a finding to report, not a change to make.
Container & model:
L,mode:subagent,model: default judgement tier— the card is judgement-heavy (it asks what hurts a user, not whether a gate is green), so ⛔ not the floor tier.Clause-②: no — an observation pass writes nothing and changes no acceptance set or published surface.
Thread-read: none (no comments on this card at claim time, 2026-09-09T20:4xZ)
Serial constraints cleared: nothing in flight. Zero
pm:dispatchedcards besides this one, zero devs running. One open PR — #40 (DESIGN.md, awaiting the maintainer's merge) — which cannot conflict: this card touches no files at all.origin/main@a7b7db5, read 2026-09-09T20:4xZ.Why this is dispatched rather than done at the seat: booting an objectstack app end-to-end (install → seed → two boots → a full-lifecycle browser drive across five audiences → a
zh-CNpass) is long-running work, and this container has already been restarted twice mid-task today. A dev's worktree survives a restart; a seat's half-finished browser session does not. The judgement half — turning findings into ranked improvement proposals and cards — stays with the seat, per 「PM 永不写代码」 and because ⛔ deciding what the product should do is not a dev's call.Generated by Claude Code
claude commentedon Sep 10, 2026
os-dev-report
Dogfood pass on
main@a7b7db5,@objectstack/*17.4.0, better-sqlite3, port 3987, Chromium 141 headless.No code, no commits, no PR. Detached worktree
hotclm-dogfood-45, removed at the end.Every reading below is from a running app; screenshots and server responses are quoted inline.
1. Pass 1 — the first sixty seconds, as a stranger
pnpm install(9.8s, cache warm) ·pnpm demoon an empty.objectstack/· browser · sign in. Nothing else.What a buyer sees. The login page is branded ObjectOS, not HotCLM, with a friendly yellow box handing over
admin@objectos.ai / admin123. Signing in lands on/_console/apps/clm?package=app.objectstack.hotclm— the HotCLM app,isDefault: true— and the app renders an entirely blank left sidebar and, in the content area, the platform's Studio metadata console: "Your control center for modeling data, designing experiences, and automating the platform", with Quick actions "New Object / New View / New Page / New Dashboard / New Flow / New Action".There is no mention of a contract anywhere on that screen. Nothing says what to do next.
Authoritative reading, not a hydration artefact — the sidebar DOM node measures
innerText.length = 0, and:The
setupapp in the same response carries a full navigation tree, so the platform serves navigation fine — it is HotCLM's fiveclm_*.accessgates that empty it. The app declares 5 groups / 20 items; the dev admin holds none of the five capabilities, so 0 render.The data is all there. 120 contracts, 200 obligations, 300 payment instalments, 60 reviews, 30 signatures, 25 deviations, 40 counterparties, 30 clauses, 9 contract types, 6 approval rules.
clm_contract_version0 andsys_approval_request0, exactly as the README documents.What a buyer would conclude. "This is a metadata modelling tool. I was told it was a contract system, and I cannot find a single contract." The product has 820 rows of a beautifully built demo and shows the evaluator none of it. Everything needed to fix this is already true — the app is correct, the data is correct, the gate is correct — but the one screen that decides the sale is the platform's developer console.
Three more things a first-run evaluator meets:
pnpm demoprints its own instructions, then buries them. The script helpfully explains at log line ~11 that three business-requester accounts must be created. Then roughly 290 more lines scroll past — including 120ERRORlines — before✓ Server is ready. The one instruction the evaluator needs is far up the scrollback.✓ Server is readyand⚠ Boot diagnostics — 2 warnings, and then the backgrounded seeder emits 120ERROR [SeedLoader] Deferred reference UNRESOLVED after pass 2 — clm_contract.owner_id stays NULL …. The seed is deliberately backgrounded ([Seeder] Inline seed exceeded 8000ms budget … continuing in background), so the "all good" banner is printed before the seed has finished.WARN [Seeder] Seed loading completed with 120 dropped record(s) and 120 error(s)with{"inserted":820,"errored":120,"referencesDropped":0}. Zero records were dropped (clm_contracttotal = 120) and 120 references were dropped (owner_id = nullmatches 120). Both counters are wrong in opposite directions. Platform-layer.field-no-consumers, 5approval-approvers-may-resolve-empty).README.mdstill reads "Status: M0 — scaffold and configuration domain" on a tree where M1·M2·M3 are complete.Screenshots:
01-landing,02-after-signin.2. Pass 2 — the lifecycle walk
2.1 The operator setup, done as written
Setup → Users → Create User, ten times; then permission sets and positions. Roughly 21 console operations before the first screen has anything on it. Two things stood out on the way:
POST /api/v1/auth/admin/create-user {"generatePassword":false,…} → 200. There is nothing on screen to save. (08-after-create-user)Requesterreturns four rows (Finance, Legal, Records, Requester) because the search matches descriptions — the operator has to read the API-name column.2.2 ⛔ The documented operator setup does not work: positions grant nothing
The README's table tells the operator to assign seven positions. I assigned them, restarted the server, and measured:
GET /api/v1/meta/app?id=clmclm_legal_counselnavigation: []clm_records_managernavigation: []clm_finance_controllernavigation: []clm_adminnavigation: []sys_user_permission_set→clm_legalnavigation: 1— Legal DeskIdentical users, identical
sys_user_positionrows; the only difference is the direct set grant. The platform's own explain endpoint gives the verdict, as Legal Counsel One:All 13
sys_position_permission_setrows exist and are correct (clm_legal_counsel → clm_legalamong them; a duplicate insert is refused409 UNIQUE_VIOLATION). They are simply not reflected in the principal.2.3 ⛔ The same root cause, second half: sharing never materialises
Even with the permission sets granted directly — navigation then correct for all five audiences — row access stays empty:
(123/46 include 3 orphan drafts my own failed intake runs left. Config objects are
public_read, hence the last four columns.)GET /api/v1/data/sys_record_share?top=1&count=true → 200 {"total":0}— zero grants exist. I ran the remedysrc/sharing/contract.sharing.tsitself documents:The criteria match perfectly (123 records). The position recipient expands to zero users, although two accounts hold
clm_legal_counseland the explain endpoint lists that position on the principal.One root cause, two symptoms:
sys_user_positionholders are not reachable from a position, so (a)sys_position_permission_setbindings never resolve →navigation: [], and (b) sharing rules with a position recipient expand to 0 → zero rows. This is upstream — report toobjectstack-ai/objectstack, do not patch this app. Untested hypothesis worth handing over: every catalog row (sys_position,sys_permission_set,sys_position_permission_set,sys_sharing_rule) carriesorganization_id: nullwhilesys_user_positionrows carryorganization_id: org_mtur8hy89tlw2z8y.Boot 1 also logged
[sharing-rule] a bulk write touched more rows than can be recomputed inline — every rule grant on this object was revoked and is being re-granted in the backgroundforclm_contract. The re-grant never landed.2.4 The legal desk, as Legal Counsel One
All seven Legal Desk screens load and are correctly labelled. All seven that read contracts are empty.
33-legal-all-contracts)33-legal-expiry-calendar)public_read.GET /api/v1/data/sys_activity → 403 PERMISSION_DENIED, logged as a console error and invisible in the UI.2.5 ⛔ Intake — three dead ends on the product's primary action
(a) The "Contract type" picker does not render. The sidebar's
Launch a Contractopens thelaunch_contractparam dialog. BothContract type *andCounterpartyrender as bare text inputs —{role:null, aria-autocomplete:null, readOnly:false}, 0[role=option], 0[role=listbox], and zeroGET /api/v1/data/clm_contract_typeon the wire. TypingMasterand confirming produces:The action declares
{ field: 'contract_type', required: true }andclm_contract.contract_typeistype: 'lookup'withlookupFilters. The flow's own header comment records this as measured on 17.3.0: "the launch_contract ACTION's param dialog, whose field-backed lookup params render the real record pickers." That measurement is no longer true on 17.4.0. Pasting the real idWWS8I0YzPhm8EdS5makes the flow proceed normally, so the value is the only thing missing. Positive control: the object-form screen later in the same flow does render a proper record picker for itsContract *lookup — so lookups are fine in object forms; it is the action-param dialog that degrades one to a text box. (17-contract-type-open,19-confirm-attempt)(b) The flow dialog is taller than the viewport and cannot scroll. At 1500x950 the "Contract details" screen measures
height 1162, top -106, bottom 1056, vh 950, withoverflow-y: visible,scrollHeight == clientHeight,body { overflow-y: hidden }and no scrollable ancestor. The Submit button sits aty = 991— 41px below the viewport. Playwright: "element is visible, enabled and stable … element is outside of the viewport", 32 retries. At 1500x1400 the same dialog submits fine. On a 1080p laptop the NDA intake cannot be completed. (23-dialog-overflow-950)(c) The file widget never uploads, so version 1 cannot be saved. Step 4 is an object form on
clm_contract_versionwith a requiredFile. Two independent paths:setInputFiles→ Save & Continue →POST /api/v1/data/clm_contract_version → 400, body sent{"file":{"name":"nda-v1.pdf","size":193,"mime_type":"application/pdf","url":"blob:http://localhost:3987/101ab3a5-…"}}, inline red error "File has an invalid file value: Invalid input: expected string, received object".filechooserevent via "or click to browse" → the widget showsnda-v1.pdf (0.2 KB)and fires zero network requests. It makes an object URL and never uploads.The engine wants a
sys_fileid string; the console posts a browser blob object. No contract can be submitted through the UI, because the submission guard requires a first version. The README already notes a seeded draft cannot be submitted and directs you to drive a new contract instead — that path is blocked too. Side effect: the flow creates theclm_contractbefore the version, so each failed attempt leaves an orphan draft. (29-after-version-save,30-filechooser-upload)2.6 The record page and the lifecycle buttons — the strongest part
The contract record is genuinely good: a lifecycle stepper, seven tabs, grouped sections with "Show N empty fields" progressive disclosure, roll-ups, typed badges. (
34-contract-record-admin)The transition buttons work and are status-gated correctly:
Obligations & Paymentsshows 3 obligations with kind badges and an overdue date in red, plus a 5-row payment schedule with Paid/Planned, actual dates and invoice numbers.Signing & Archiveshows the archive card withEXECUTED AT,SIGNED AT,EXECUTION FORMALITIES: Countersigned copy returned,BACKFILLED ✓. Honest and complete.Note the practical gap:
in_review → in_approvalis unreachable by anyone. It needsclm_legal.access, and the only holders are legal, who can see zero rows.2.7 ⛔ A correct business refusal is a 500, and is invisible
A requester clicks Submit on their own draft:
and on a blocked counterparty,
"…threw: Error: Counterparty Delta Ridge Holdings is blocked; a contract with a blocked party cannot be submitted."The messages are excellent. Two things are wrong: they are served as HTTP 500
INTERNAL_ERRORprefixed withhook '…' threw: Error:, and nothing is shown to the user — 0 toast elements, no inline error, the record still reads Draft. Confirmed by screenshot, not DOM alone. (38-submit-500,39-submit-nonblocked)The same product does toast a flow refusal (2.5a). So one refusal path tells you and the other says nothing.
2.8 Dashboards — the best thing in the repo
All three load with real numbers and unusually honest descriptions: "Each bar is its own currency — no FX conversion exists in this app", "Not an average duration — see the PR", "a fixed threshold, not the per-type breach". Legal Workbench 6/12/10/1 and 本月已批准 2 (↓75%); Executive: value by currency, 12 expiring within 90 days, 19 high-risk, routes 11/41/23/4; Finance: 逾期金额 2,176,250, 12 overdue instalments, a top-10 unsettled table, planned-vs-settled by month.
One defect, on the widget the legal team looks at first: "Pipeline by Stage" renders as a funnel that widens at Signing and Active and spills outside its card as a bowtie — because a contract book normally has more late-stage rows than early-stage ones, which is the one shape a funnel mark cannot draw. (
40-dash-legal-workbench)2.9 Inbox and notifications
The inbox works and reads "You're all caught up". It is empty because nothing has ever been delivered:
sys_notification_receiptandsys_inbox_messagewere both0until I triggered the jobs by hand. Afterwards, all 12 receipts carryuser_id= the dev admin. No other account has ever been notified.2.10 One full
zh-CNpassThe bundle is excellent — navigation, object labels, view tabs, dashboard titles and descriptions, KPI units, empty states, even 预览版 / 相对方 / 补录已签合同. Two gaps, both in §3 suspect 6 below. The locale switch is also well hidden: avatar → Language (a row that opens only on click, not hover) → 中文(中国). (
47-zh-contract-list,48-zh-dash-legal-workbench)3. The six known suspects
1.
navigation: []without the operator setup (#28, #11) — STILL REPRODUCES, and it is worse than recorded.GET /api/v1/meta/app?id=clm → 200 {"navigation":[]}for the dev admin, exactly as the README documents by design. What is new: it also reproduces for every account set up the way the README instructs, because positions grant nothing (§2.2). The measurement #28/#11 need is that the fix is not only "give the dev admin a set" — it is that the documented remedy is currently inert.2. 21 fields declared with no consumer (#38) — REPRODUCES at build time; but essentially none is a visible hole. All 21 warnings still fire. In the browser, the console's auto-generated record detail page renders every one of them, grouped into sections, populated from real data:
clm_party.legal_representative / address / contact_phone / screened_atrender on the Counterparty page;bank_name+bank_accountrender once the collapsed Banking section is opened ("Rheinische Kreditbank","DE89 3704 0044 1000 4384"— all 40 parties carry both);clm_clause.standard_textandfallback_textrender in full;clm_contract_type.descriptionrenders;clm_contract_version.notesis an editable field on the intake object-form screen. Data behind them:review.comments60,review.internal_note35,signature.envelope_id28,deviation.deviation_text25,party.risk_note3. The only pair that reads as a hole isclm_contract_type.template_file/template_placeholders, which render as—and have no writer either. So the rule is measuring declared consumers (view column, form section, page binding, flow, dataset) and is correct on its own terms; "no declared consumer" is not "the user cannot see it". (49-rec-clm_party,50-party-banking-expanded,51-type-sections-expanded)3. The backfill action is invisible to legal (#43) — STILL REPRODUCES. Contract list toolbar, measured per audience: LEGAL
["New","Import","Launch Contract"]; RECORDS and ADMIN additionally carry"Backfill Executed Contract"; REQUESTER same as legal.src/actions/contract-backfill.actions.tsalready documents why (§06 F16 wants records OR legal,requiredPermissionsis an AND, so it gates onexecute_contract). Unchanged on 17.4.0.4. F3's notifications reach only half their targets — REPRODUCES exactly, and the rest of the reminder layer is worse. Node-level summaries from
POST /api/v1/automation/FLOW/trigger:The 10-selected / 5-notified shape is exactly as recorded. Cause confirmed: 12 contracts
in_review, 6 with alegal_owner, 6 without; 54 of 120 contracts carry nolegal_ownerat all. Answering the card's question — is a silent no-recipient path acceptable? — no, and the reason is sharper than "half": four of the six reminder flows select work and then notify nobody, with no error, no counter surfaced to a user, and no trace anywhere a person looks. A user who expects to be nudged is not nudged and is not told they will not be.5.
pnpm demostops being re-runnable once the jobs have run (#41) — DOES NOT REPRODUCE on this tree. Three re-runs on a live database. Boot 4 was run afterpayment_overduehad moved 18 instalments tooverdue(count 12 → 30). Result:Seeds: app.objectstack.hotclm 820 rows, 0 ERROR lines, no seeder warnings — and afterwardspayment_plan overduewas back to 12. The seed silently reverted the 18 instalments the job had moved, with no state-machine refusal. The expiration half was not exercised:expiration_sweep selected 0, so I cannot speak toactive → expired. Two notes for #41's decision: its premise did not hold for the payment path here, and the behaviour I did see — a re-seed silently undoing a job's work — is its own problem, differently shaped from the one the card describes.6. Chart labels and view descriptions — BOTH REPRODUCE on 17.4.0.
(a) Chart category labels render English in a
zh-CNconsole, on same-object dimensions: 法务工作台 各阶段合同数 showsActive / Approved / Draft / In Approval / In Review / Signing / Submitted(clm_contract.status); 管理层看板 已签合同额(按方向) showsPurchase / Sales(clm_contract.direction); 生效合同额(按币种) showsEUR — Euro,GBP — Pound…,USD — US Do…. Not universal — 财务看板 renders 计划 / 采购 / 销售 and 已结清 / 计划 in Chinese. A second defect rides along: inzh-CNthe funnel is ordered alphabetically by the English label, so a "pipeline by stage" chart is no longer in stage order.(b) View descriptions: they are rendered, but not localised. In the Chinese console the contract list shows
"Contracts I launched, grouped by where each one has got to."surrounded by Chinese, whilesrc/views/contract.view.ts:89declaresdescription: { en: '…', 'zh-CN': '我发起的合同,按各自进行到哪一步分组。' }. Ten views declare a bilingual description; the console serves theenbranch.4. Ranked improvement proposals
Ranked by cost to the product.
P0-1 — The documented setup produces a dead app. A buyer follows the README exactly and still gets an empty screen for seven of ten accounts. The position → permission-set and position → sharing-recipient expansions both resolve to nothing. Smallest fix: file the upstream bug against
objectstack-ai/objectstackwith thesecurity/explainoutput and theexpandedUsers: 0evaluate result; in this repo, change the README's setup table to grant the permission sets directly (Setup → Users → Permission Sets → Grant permission set), which is measurably the only path that works today, and keep the positions as a second step for sharing once upstream lands.P0-2 — A contract cannot be created through the UI. A requester tries to launch a contract and cannot choose a type, cannot reach Submit on a 1080p screen, and cannot attach version 1. All three are platform-console defects, all three sit on the product's primary action. Smallest fix: three upstream issues — action-param dialog must render field-backed lookups as record pickers; the flow/action dialog needs
max-heightplus an internal scroll region; the file widget must upload and hand back asys_fileid. In this repo, today: nothing to patch, but the flow's header comment recording the 17.3.0 picker measurement should be re-measured and annotated, since it is the kind of(measured)note this repo has been bitten by before.P0-3 — The first screen tells a buyer nothing. An evaluator signs in and sees a metadata Studio. Smallest fix that is entirely in this repo: give the
clmapp one always-visible navigation entry with norequiredPermissions— atype: 'page'welcome/orientation item — so an ungated user lands on something that says what HotCLM is and links to the operator setup. One nav item, no permission model change. It convertsnavigation: []from a dead end into a first screen. (This is the one line #11's decision moves; the welcome page is the option that does not need #11 answered.)P1-4 — The product refuses in silence. A requester clicks the only button their contract offers and nothing happens. The hook messages are already well written; they are thrown away. Smallest fix: upstream, action failures need to surface as a toast the way flow failures already do, and a hook refusal should carry a 4xx business code rather than 500
INTERNAL_ERRORwith ahook '…' threw:prefix. Worth filing as one issue with both halves, because the good message already exists and only the transport is wrong.P1-5 — Empty states that contradict the data. A lawyer opens All Contracts and is told to create their first record, next to 123 rows they cannot see. Smallest fix: upstream, distinguish "no rows matched" from "no rows visible to you" and word the second one accordingly. Cheap and high-value — this is the class the card calls "places the product lies", and it is the difference between "the product is empty" and "your access is not set up".
P1-6 — The reminder layer nudges nobody. Four of six flows select work and notify no one. The
legal_owner-shaped hole is a data problem the demo could fix and a product problem the app should decide on. Smallest fix in this repo: the demo dataset should give every contract alegal_owner(54 of 120 have none), so the reminder layer demonstrates itself; separately, a no-recipient sweep should record that it had nobody to tell — a counter on the run summary is already there (skipped), it just never reaches a person.P2-7 —
pnpm demo's first run reads as a failure. 120 red ERROR lines after a green "Server is ready". The condition is expected and documented; the presentation is not. Smallest fix: the seeder's unresolved-reference lines for a reference the fixture knows cannot resolve yet should be one WARN summarising the count, not 120 ERRORs — and the "create the three requesters" hint should be re-printed after the ready banner, where it will actually be read.P2-8 — Small honesty and polish items. README still says M0 · Studio says "All healthy" over 26 author-time warnings · the seeder's
dropped/referencesDroppedcounters are both wrong · the "User Created" dialog asks you to copy a password it does not show · lookups render as raw ids in related lists (position_mtur8hz641l67zpxin Setup → Users → Positions;bsZ9czV…in the obligations Owner column) · the Pipeline funnel mark is wrong for this data shape and loses stage order inzh-CN· view descriptions ignore theirzh-CNbranch.Things an evaluator would have to be told
Worth naming, since the card asks: that the app is empty because the dev admin holds no
clm_*set; that the three requester accounts must be named exactly; thatpnpm demomust be run a second time; that 42 contracts sit pastin_approvalwith no approval request behind them; thatversion_countreads an honest 0 because a seed cannot mint a file. Every one of these is written down truthfully in the README — and none of them is visible from inside the running product. That is the gap card 14's 「陌生人 clone 一条命令跑起」 has to close.5. What worked, plainly
The data model, the audience model, the record page, the three dashboards and the
zh-CNbundle are all genuinely good, and the descriptions throughout refuse to overclaim in a way that is rare. Once the permission sets are granted directly, every audience's navigation renders exactly the §05 table, group for group. Nothing in this report is a defect of design; the expensive ones are all a layer below the app, and the app's own honesty rules are visibly holding.Environment: worktree
hotclm-dogfood-45(detached ata7b7db5), port 3987, fourpnpm demoboots, Chromium 141 headless at/opt/pw-browsers/chromium-1194. Pre-existing platform pair (no such table: _objectstack_sequences,UNIQUE constraint failed: sys_oauth_resource.identifier) observed on boots 1–3 and attributed as the card says. No file in the repository was modified; the only writes were to the local gitignored database.Generated by Claude Code
zhuangjianguo commentedon Sep 10, 2026
Review: ACCEPT — with a headline retraction that the seat caught at dedupe, before anything was filed upstream.
⛔ Read this before acting on the report comment above. Its §2.2, §2.3, the row-visibility matrix, suspect 1's extension, proposal P0-1 and open question 1 are retracted. The rest stands, and most of what matters is in the rest.
What was wrong, and how it was caught
The report's headline was "the README's documented operator setup does not work" — positions granting nothing, sharing never materialising, four audiences rendering correct navigation over zero rows, "one upstream root cause, two symptoms". That was about to become a new
objectstackcard.It did not, because the pre-filing dedupe search hit
objectstack#16712, whose title carries "RE-CUT: the originally reported resolution-path defect is disproved", and the closed#13419, which records that permission-set resolution binds by position NAME. The report's ownsecurity/explainoutput showed an id inprincipal.positions. That mismatch was the whole finding.Asked directly, the run confirmed it against its own saved output:
{"user_id":"…","position":"position_mtur8hz5jp7ptm3r"}, from aname → idmap. Every one returned 201 with no diagnostic.principal.positions = ["org_member", "position_mtur8hz5jp7ptm3r", "everyone"]— the two platform-seeded memberships appear as names; only the row the run wrote appears as an id. The list is populated verbatim fromsys_user_position.position.recipient position:clm_legal_counsel,clm_legal_head, …). Matching those against an id-spelled row yieldsexpandedUsers: 0— the expected result, not a defect.README.md:72says 「create accounts in Setup → Users and assign the seven positions」 and names them by name. The id was chosen by analogy withsys_user_permission_set.permission_set_idafter a metadata read was truncated before it reached thepositionfield's definition — andpositionis the one field on that row without an_idsuffix.The run volunteered all of this, listed precisely what to retract, and named its own process error: a working positive control (a direct set grant) was read as isolating the mechanism when it equally isolated its own payload. That is the right instinct arriving one step late, and saying so is worth more than the finding would have been.
Why this is still an ACCEPT
The card asked for observation and judgement about a user's experience, ⛔ not for a defect list, and it delivered that. Nothing that matters was lost to the retraction, because none of the surviving findings rests on those rows:
navigation: []for the dev admin was measured before any setup existed.500refusal were all measured asrequester1, who reached them through a direct permission-set grant and record ownership — no position involved.40+ screenshots, four boots, server-side readings throughout, and the one case where a post-navigation DOM read looked like a P0 was correctly disproved with a screenshot — which is the discipline the card demanded.
What survives from the retracted half, and where it goes
Two smaller, real things:
sys_user_position.positionwith201and no diagnostic. That is plausibly alreadyobjectstack#16712's territory; the seat is adding this evidence there rather than filing a duplicate.README.mdnames the positions but never says the assignment binds by name. An operator following it through the UI is fine; anyone automating it has nothing to go on. App-side, and the seat is carding it.{"user_id":"…","position":"clm_legal_counsel"}, re-runsecurity/explain, see whetherpermissionSetsgainsclm_legal— was correctly not run rather than rebuilding a torn-down world for one row. It goes to a fresh dispatch. Until it is run, ⛔ nobody may state that positions do or do not work on this version.Card status
This card stays open as the carrier for the findings the seat has not yet carded. ⛔ It is not closed on ACCEPT: closing it would strand roughly a dozen named observations with no home, and this repo's own discipline is that a finding without a carrier is a finding that dies.
Teardown verified by the run: servers and Chromium stopped by recorded PID,
git statusempty beforegit worktree remove(exit 0), the siblinghotclm-issue-39worktree untouched, and no repository file modified — which is what an observation card should look like.Generated by Claude Code
fix(dashboards): draw Pipeline by Stage as a ranked bar, not a funnel (…
zhuangjianguo commentedon Sep 10, 2026
Correction to this pass's record: one finding did not reproduce
This card's report described the
Pipeline by Stagewidget on 法务工作台 as "a bowtie spilling outside its card". #48 was cut from that, and its dev re-measured the claim rather than inheriting it. The spilling half is false:main@2807b3bwith console 17.4.0, the funnel's ink sat 25px inside the card on every side at 1440 / 1280 / 1024 / 820px viewport widths, withscrollWidth === clientWidthat each — no horizontal overflow at any width.2b74c4f): band widths 115 · 138 · 138 · 92 · 69 · 692 · 692px, i.e. narrow-wide-narrow then eleven times wider, because a funnel encodes monotonic decline over a distribution that does not decline (draft 10 · submitted 6 · in_review 12 · in_approval 8 · approved 4 · signing 6 · active 60).Recorded here rather than left in the successor card, because this card is the observation record and an unretracted claim in it is what the next pass would inherit.
The lesson is this card's own golden rule pointing at itself. It says: ⛔ do not report a "missing / broken / unreachable" finding from a post-navigation DOM read alone; confirm against a screenshot or the server's own response. The spilling claim was a visual read of a screenshot in which a full-width trapezoid looks like it is breaching its container — and the geometry says it was not. Same failure mode, different sense: the report is what an eye concluded, not what a measurement said. A layout claim needs
getBoundingClientRect/scrollWidth, exactly the way a data claim needs the network response.Nothing else in this pass's report is affected — the bowtie, the
navigation: []re-measurement, the label-locale findings and the rest stand as written. #48 is closed, #57 is merged, and the platform half is upstream as objectstack-ai/objectstack#17344.Generated by Claude Code
zhuangjianguo commentedon Sep 10, 2026
Closing — the report is delivered and every finding is routed
This card's deliverable was a report, not code, and the report is in. Where each finding went:
Fixed and merged
Pipeline by Stagerenders as a bowtie2b74c4f2d63324pnpm demo's operator banner asserted a census it could not know2807b3bclm_contract.legal_ownerwas the dev admin on every contractd34b225payment_overdue/active → terminatedcontradiction this pass raised against PR #42's invariantFiled and in flight or queued
clm_obligation.owneris the dev admin on all 200 seeded rows #52 —clm_obligation.owneris the dev admin on all 200 rows: F10's receipts are the only ones still addressed to an account that cannot act on them. Dispatched.Pipeline by Stageis 83% one bar —activeis a terminal state on a chart whose own rule excludes terminal states #59 —Pipeline by Stagespends 83% of its width onactive, a terminal state the widget's own rule would exclude. Surfaced by 「Pipeline by Stage」 uses a funnel mark on data whose late stages outnumber the early ones, so it renders as a bowtie spilling outside its card #48's measurements, not by this pass directly. Dispatched.scripts/demo.mjsstill tells the operator thatclm_contract.legal_ownerresolves against the dev admin — PR for #47 made that false #54 —scripts/demo.mjsstill enumerateslegal_owneramong the dev-admin references. Dispatched.DESIGN.mdserialization).Routed upstream
options.stageOrderis declared for every chart type, documented for a chart type that does not exist, and silently dropped in every non-enlocale objectstack#17344 —options.stageOrder: funnel-only, documents a chart type that does not exist, and label-matched so the authored order is silently dropped outsideen. This pass's "chart category labels render English in azh-CNconsole" observation turned out to have a consequence nobody had connected to it: it does not merely leave labels untranslated, it discards authored ordering metadata. That is why the upstream issue exists rather than a comment on the closed #5076.Sitting in the decision box, awaiting the maintainer
Five of this card's "known suspects" were confirmed to still reproduce and are decisions rather than defects: #28 (
navigation: []out of the box) · #11 (clm_requestercannot bind toeveryone) · #38 (21 fields with no consumer) · #43 (backfill invisible to legal — the permission gate is an AND) · #41 (pnpm demostops being re-runnable once the jobs have run). Plus #53, which this pass produced. Each is its own card with its own four-axis analysis; none of them needs this card to stay open.One claim retracted
The "spilling outside its card" half of the funnel finding did not reproduce — see the correction above. The bowtie half did and is fixed.
Closing as completed. A future dogfood pass gets a fresh card against the tree it measures; this one's tree (
a7b7db5) is six commits behind.Generated by Claude Code