Repository navigation
The demo opens on an empty screen: owner_id is NULL on all 120 seeded contracts, so 我的合同 › Launched by Me is blank for every audience #26
Description
Found by card 07 (#23, PR #25) the moment the app shell existed to see it in. Out of scope there — the view is correct; something upstream of it leaves the column empty. Filed rather than ridden.
Milestone M3. This is a demo-quality card, not a correctness one, and for a product whose whole premise is being sellable it is worth more than most of the correctness backlog.
⚠️ This body was rewritten on 2026-09-07. The first version gave the cause as "a declarative seed cannot name a user." That is false, and the file says so two lines from the one it blames — see the comment below. The diagnosis section is now the open question rather than an answer.
The symptom
DESIGN.md §05 puts 我的合同 first, and its first item is Launched by Me. That is the screen a buyer's demo lands on, for every one of the five audiences.
It renders No matching records. For all five. On a database with 120 contracts in it.
What is actually known
owner_id is NULL on all 120 seeded contracts. Everything past that sentence is the card's job to establish.
Proven not to be a view bug — card 07's positive control on the same view, in one run:
my_contracts 0 → 1 after launching one contract as the signed-in user
all_contracts 120 → 121 same run, same session
A contract created through the UI gets owner_id stamped and appears immediately. The filter, the view and the gate all work. There is nothing for them to match.
The two claims in the source that do not survive contact
src/data/contract.seed.ts:77 explains the omission:
// `owner_id` is left to the platform: a seed cannot name a user that
// does not exist when it runs, and the runtime claims ownerless seeded
// rows for the dev admin once that account is minted (measured).
legal_owner: contract.hasLegalOwner ? DEMO_USER : null,Both halves are contradicted:
- "a seed cannot name a user" — the very next line names one.
legal_owner: DEMO_USERresolves againstsys_user.name, and it works, becausescripts/demo.mjssequences two boots precisely so that it can: the priming boot mints the account, the demo boot seeds against it. That handover already carries three user references —clm_review.reviewer,clm_obligation.ownerandclm_contract.legal_owner, on this same object. So the mechanism exists, is used, and is used one field away. - "the runtime claims ownerless seeded rows for the dev admin … (measured)" — if that were true
owner_idwould hold the dev admin, not NULL. Card 07 measured NULL on all 120. Whatever was measured, it was not this; the annotation is carrying more authority than its evidence.
owner_id itself is declared, system: true, readonly: false, a lookup('sys_user'), and its own comment says "the security middleware stamps the acting user on any insert that leaves it empty." A seed insert evidently is not such an insert — which is the thing to establish first.
What this card has to settle, in order
- Why is it NULL? Does the security middleware skip system/seed writes, or does the seed loader bypass the middleware entirely? Measure it; do not infer it. This answer decides everything below.
- Can the fixture simply set it, the way it already sets
legal_owner—owner_id: <a user name>resolved by the same two-boot handover? If yes, this card is a few lines insrc/data/contract.seed.tsand the post-seed step is unnecessary. Try this before building anything. - Only if 2 fails, add the post-seed stamping step, following the pattern
scripts/demo.mjsalready documents at length. - Correct the two false comments at
src/data/contract.seed.ts:77regardless of which route is taken. A comment marked(measured)that is not is worse than no comment — it is the shape this repo's whole verification discipline exists to prevent, and the next author will trust it exactly as I did.
Spread it, do not dump it
Whatever the route: do not put all 120 on the dev admin. 我的合同 would then be correct for one account and empty for the other four — the same defect with a smaller blast radius. Spread the owners the way the fixture spreads everything else, per §10's persona mix, so every audience's first screen has rows on it. State the per-persona counts you expect before you measure them.
Constraints
- The 820-row total and both locales' provable identity must not change. The fixture's row counts must not become conditional on a step succeeding — if the stamp fails, it fails loudly rather than silently changing what
pnpm demoproduced. - ⛔
AGENTS.md: no seeded number that looks computed. The owner is a realsys_userid or the field stays NULL. - ⛔
DESIGN.md§10 forbids seeding users. The fixture references accounts; it does not create them.
The other empty first screen
The Versions tab is empty on every contract too — #19: clm_contract_version holds 0 of §10's 300 rows, because a seed cannot mint a sys_file id. That one is a genuine "the seed cannot produce this value" case and it is still waiting on a decision, so ⛔ it is not this card's to fix. Noted because the two land on the same screens and it would be easy to conflate them — and because #19's constraint is real in a way this card's turned out not to be.
Third consequence, already on #19: a seeded draft cannot be submitted through the UI, because the draft → submitted guard wants a version.
Related
#19 (versions — genuinely blocked, and awaiting a decision) · #18 / PR #21 (the fixture and the two-boot handover) · #23 / PR #25 (where this surfaced).
Activity
hotlong commentedon Sep 7, 2026
Correction — I filed this card with the wrong cause, and the body above is rewritten. Recording what happened, because the mistake is the same one the card is now about.
I wrote that 我的合同 is empty because "a declarative seed cannot name a user", and I wrote it as established fact. I took it from
src/data/contract.seed.ts:77, which says exactly that, and I did not check it — despite spending this evening insisting that every other claim on this repo be measured rather than inferred.It is false, and the refutation is on the next line of the same file:
The seed names a user, on this object, one field away from the one it says cannot be named.
scripts/demo.mjsexists to make that possible and documents it at length — the priming boot mints the account, the demo boot seeds against it, and the handover carriesclm_review.reviewer,clm_obligation.ownerandclm_contract.legal_owner.The second half of the comment is contradicted too: "the runtime claims ownerless seeded rows for the dev admin once that account is minted (measured)". If that held,
owner_idwould be the dev admin. Card 07 measured NULL on all 120. Whatever produced the(measured)annotation, it was not this.I caught it only because I opened
scripts/demo.mjsto write a dispatch that would have told an agent to build the two-boot stamping step — the step that may well be unnecessary, since the fixture might just setowner_idthe way it already setslegal_owner. So the practical damage was one card sent down the wrong road, and it stopped there.What changes in the card: the diagnosis section is now the first thing to establish rather than a premise, step 2 is "try the one-line fixture route before building anything", and step 4 adds fixing the two false comments regardless of route.
No dispatch has gone out against this card. It goes out against the rewritten body.
The general lesson is the card's own: a comment marked
(measured)is not a measurement, and a source comment is not primary evidence about runtime behaviour — it is a claim by an earlier author, with the same standing as any other claim.Generated by Claude Code
hotlong commentedon Sep 7, 2026
Claim: PM loop round R9 — first M3 card, dispatched ahead of cards 09–14 because it is what a buyer sees first. Dispatched by the PM seat for its dev; the dev inherits this claim and posts no second one.
Session:
session_01KcrVDXSptwDukFsHPHPR1VBranch:
claude/issue-26-demo-contract-ownersWorktree:
hotclm-issue-26(dedicated, offorigin/main@7b42d28)File surface:
src/data/contract.seed.ts·scripts/demo.mjs(only if the fixture route fails) ·src/data/plan*.tsif the persona spread needs itContainer & model:
M,mode:subagent,model: opusDispatched against the rewritten body (2026-09-07), not the original — the original's stated cause was wrong and the correction comment above says how. Nothing else is in flight.
Cards 09–14 are not dispatched: card 09 needs #6, #10 and #14 answered first, and dispatching it now would send an agent into three walls it has no mandate to climb.
Generated by Claude Code
hotlong commentedon Sep 7, 2026
os-dev-report
{ "issue": 26, "status": "done", "branch": "claude/issue-26-demo-contract-owners", "pr": "https://github.com/objectstack-ai/hotclm/pull/27", "premise_still_valid": true, "summary": "Step 2 won: the fixture sets `owner_id` directly and no post-seed stamping step was built. `src/data/keys.ts` deals the 40 counterparties across DESIGN.md §10's three 业务承办 account names (43 / 43 / 34) and `contract.seed.ts` writes `owner_id: ownerOf(contract)`. Step 1, measured rather than inferred: the security middleware does not skip the seed by accident — it stands aside for system writes at its first line (`if (opCtx.context?.isSystem) return next()`) and a seed insert carries no acting user to stamp; and `claimSeedOwnership` IS a real platform routine, but it runs only on the boot that promotes the first human to platform admin, which under `pnpm demo` is the priming boot, before a single contract exists (single boot, demo on: `adminPromoted: true, ownershipClaimed: 260`, all 120 owned; `pnpm demo`'s second boot: `adminPromoted: false, reason: \"already_have_admin\"`, all 120 NULL). The two-boot handover that rescued the 60 reviews is exactly what turned the claim off, so the `(measured)` annotation was true once and false since #21. Both false claims are replaced with that measurement. The dev admin is deliberately NOT one of the three owners: it holds no `clm_*` permission set, so `GET /api/v1/meta/app/clm` serves it `navigation: []` and it can never open 我的合同 — contracts parked there would belong to the one account that cannot see them. The assignee field was set by the PM's dispatch as expected and was never written by me.", "tests": "pnpm validate exit 0 (`✓ Validation passed (834ms)`) · pnpm lint exit 0 (`5 suggestion(s)`, the same five approval-approver notes as main) · pnpm typecheck exit 0. Row counts: `pnpm demo` on a fresh DB, counted from SQLite = 820 (9+30+6+40+120+0+60+25+30+200+300), unchanged and unconditional — there is no step for the counts to depend on. Locales: both artifacts compiled and compared record-for-record — `owner sequence en === zh: true`, tally in both `{BR1: 43, BR2: 43, BR3: 34}`, `total seeded rows en/zh: 820 820`; the deal reads only `contract.partyIndex` and never a locale bundle. Counts predicted BEFORE measurement (43/43/34) and confirmed in the artifact, in SQLite and on the wire. Browser (Chromium at /opt/pw-browsers/chromium-1194, `/_console/apps/clm`), after creating the accounts in Setup and re-running `pnpm demo` (the upsert handover): signed in as `Business Requester 1` → `43 records`, grouped by status with `Active 16`, wire `filter=[[\"owner_id\",\"equals\",\"ugAfw…\"]] → total=43`; as `Business Requester 2` → `total=43`; negative control `Legal Counsel One` → `No matching records`, `total=0`, while being served the same nav group and the same view, so the emptiness is ownership and not access; second control `Business Requester 3` account deliberately never created, its 34 rows stayed NULL through both runs. Post-handover SQLite: `[{name:null,c:34},{Business Requester 1:43},{Business Requester 2:43}]`, TOTAL clm_ rows = 820. Boot: one `⚠ Boot diagnostics` warning, `[Seeder] Inline seed exceeded 8000ms budget`, IDENTICAL on origin/main @ 7b42d28 and on this branch — the card's 'zero warnings' does not reproduce in this container on main either, and nothing on this branch adds one. Step-2 probe: throwaway edit reverted with `git checkout HEAD --` and verified byte-identical against its pre-edit `git hash-object` (bacdb0919f084f24c994d5e1cb09fe721978c648 both sides), `git diff HEAD` empty.", "mcp_calls": "5 — issue get, issue get_comments, PR #25 get, create_pull_request, this comment (+1 for the terminal report is not an MCP call)", "open_questions": [ { "question": "Should `pnpm demo` mint the three 業務承办 accounts (and grant them `clm_requester`) so the demo spreads out of the box? Today it cannot: `pnpm demo` produces exactly one account (measured), that account is the dev admin, and the dev admin is served `navigation: []` for the whole CLM app — so on a stock demo NO account can open 我的合同 at all, with or without this card. The fixture now names the owners; only the accounts are missing.", "options": [ "A — leave it as shipped: the fixture names the accounts, the operator creates them in Setup and re-runs `pnpm demo` (documented in the README and printed by `pnpm demo`). §10 untouched, no invented personas, but the first screen stays empty until the operator acts.", "B — `scripts/demo.mjs` creates the three accounts in the priming boot and grants each `clm_requester`. The demo then works on `pnpm demo` alone. §10's 用户不可种子 constrains the FIXTURE, and the script is not the fixture — but it is still account creation, and it invents emails/passwords, which is product surface.", "C — decide #11 question 1 the other way (drop `requiredPermissions` from `group_my_contracts`), so the dev admin sees 我的合同 too, and give the dev admin a share of the book. Cheapest for a stock demo, but it re-opens a gating decision this card has no mandate over." ], "recommendation": "A as shipped, then B as a small follow-up card if the maintainer wants `pnpm demo` alone to be sellable. B is ~40 lines against endpoints that already exist (`POST /api/v1/auth/admin/create-user` + a `sys_user_permission_set` insert — both used in this card's verification), but it decides product content (account names, emails, whether the demo ships credentials for five personas), which is the maintainer's, not mine. C should not be bundled into either: it is #11's decision." } ], "out_of_scope_findings": [ "noted, not filed: `src/data/index.ts`'s note that the seed loader 'runs BEFORE the first user account exists' is true only of a single boot — under `pnpm demo` the priming boot mints the account first, which is the whole point of that script. The sentence is a leftover of the pre-#21 world, like the one this card fixed; harmless (its conclusion about `clm_contract_version` stands for a different reason) but it is the same shape of stale claim.", "noted, not filed: `scripts/demo.mjs`'s header states the seed's last write landed at 18:59:54.118Z and the account at 18:59:54.931Z — 0.8s — while `plan-contracts.ts` cites the same measurement as '~1.8s'. One of the two numbers is wrong; neither changes any conclusion.", "noted, not filed: the console shell requests `GET /api/v1/data/sys_activity` for its Recent panel and a `clm_requester` account is refused with 403 PERMISSION_DENIED, logged to the browser console on every page. Platform-side (the shell asks for something a normal member may not read), not this app's metadata, and cosmetic — but it is a red console error on the demo's main screen for every non-admin audience.", "noted, not filed: `pnpm demo` on this container prints one boot warning (`Inline seed exceeded 8000ms budget`) on `main` as well as on this branch. The card and PR #25 both record a silent boot, so the 8s budget is being missed under parallel-agent load rather than on merit — worth knowing before the next card treats a warning-free boot as a regression test." ] }Generated by Claude Code
hotlong commentedon Sep 7, 2026
Review: ACCEPT — PR #27, head
ef6a492, CI green. 4 files, +112/−8, data and documentation only. Merging.The diagnosis is right, and I checked it against the platform's own source rather than the report
This is the claim the whole card turns on, and it is a strong one, so I read
@objectstack/plugin-security@17.3.0's bundle myself:claimSeedOwnership(ql, target.id, { logger })is called atdist/index.js:8621— after the promotion block.if (!walled && humanUnscopedHolders.length > 0) return { seeded, adminPromoted: false, reason: "already_have_admin", … }.if (opCtx?.context?.isSystem) return next();— the middleware stands aside for system writes, so the "stamps the acting user" branch never sees a seed insert.Which confirms the mechanism exactly as reported:
pnpm demo's priming boot promotes the admin with the demo off, so there is not one contract to claim; the demo boot then exits atalready_have_admin.⇒ The two-boot handover that rescued the 60 reviews in #21 is what turned contract ownership off. A fix that silently broke something else, invisible because both symptoms are the same sentence: the column is empty. The
(measured)annotation was not fabricated — it was true, and then #21 landed. That is a better lesson than the one I filed this card with, and the replacement comments now carry it.The rest, verified
43 / 43 / 34predicted before measurement and confirmed in the artifact, in SQLite and on the wire · 820 rows unchanged and unconditional · both locales identical record-for-record, and structurally so, since the deal reads onlycontract.partyIndex· the negative control is the right one —Legal Counsel Oneholdsclm_requester, is served the same group and opens the same view, and still sees nothing, so the emptiness is ownership, not access · the step-2 probe was reverted and checked byte-identical bygit hash-object, not by an exit code.Excluding the dev admin from the deal is the correct call and the reasoning is the same control PR #25 recorded: it is served
navigation: [], so contracts parked there would belong to the one account that can never open the screen they are for.A correction to my own dispatch, on the boot standard
I told this card that
mainboots with zero⚠ Boot diagnosticsand therefore "any warning after your change is yours." The report showsmainprinting[Seeder] Inline seed exceeded 8000ms budgetin this container, and it is right — my silent boot was on a--seed-admindatabase with no demo data, and this one is apnpm demoboot with 820 inline rows under parallel-agent load. Both measurements are true of different boots.So the standard needs its condition attached, and future cards should read it as: a boot warning that is not also on
mainunder the same seed and the same load is yours. The report handled it exactly that way — baseline and branch on the same tree — which is why it is a note and not a defect.What this card does not finish, and why that is right
Out of the box
pnpm demostill leaves the contracts ownerless, because the three accounts do not exist. The fixture names them; only the accounts are missing. That last step is account creation, not seeding, and it decides product content — so the agent declining to take it was correct.Filed as its own decision card, coupled with #11 question 1, because the two answers interact: if 我的合同 stops being gated on
clm_requester.access, the dev admin can see the group, and a single-bootclaimSeedOwnershipalready owns all 120 to it — no new accounts at all.Carried forward
Three stale claims of the same shape as the one this card fixed, all noted and none ridden:
src/data/index.ts's "the seed loader runs before the first user account exists" (true of a single boot only, pre-#21 leftover); a 0.8s vs ~1.8s disagreement betweenscripts/demo.mjsandplan-contracts.tsabout the same measurement; and the console shell'ssys_activity403, which is platform-side but now shows a red console error on the demo's main screen for every non-admin audience.Generated by Claude Code
hotlong commentedon Sep 7, 2026
Correction, measured after the merge: the unresolved owner name is NOT silent. It logs 120
ERRORlines on every stockpnpm demo.Card 10 (#29 / PR #30) reported 120 errors on
mainwhile establishing its boot baseline. I re-ran it myself onmain@fb549e3, fresh database, and it is right:× 120, then a
WARN [Seeder] ✗ Deferred reference unresolved after pass 2 … and 100 more error(s)summary. Row count is unaffected — 820,owner_idNULL on all 120 — so this card's count promise holds and the merge stands.How both of us missed it, which is the part worth keeping
This card's report said the probe rows "landed NULL silently — no error, no boot warning." I repeated that here, in the merge commit and to the maintainer.
I made the identical mistake re-checking it just now: I booted, waited for
Server is ready, grepped, and got 0 errors. The log was clean because the seeder had not run yet — the boot printsand then finishes the 820 rows after the ready banner. My first measurement was taken 75 seconds too early. The errors appeared at 23:52:22; the banner was at 23:51:07.
⇒ A log grepped before the background seeder finishes is indistinguishable from a clean log. That is this repo's recurring failure shape in yet another costume, and it now has a name: the ready banner is not the end of the boot. Every future card comparing boot output must wait for seed completion, not for readiness.
The platform's own message is excellent and says the thing we kept saying to each other — "every row counter looks healthy while the relationship is MISSING." It was there the whole time.
What changes
Nothing in this card's code; the diagnosis, the deal and the counts are all still right. What changes is #28: option A is no longer "the first screen stays empty until the operator acts" but "the first screen stays empty AND every stock
pnpm demologs 120 errors, permanently." Posted there with the measurement.Note the error text prescribes exactly the remedy
DESIGN.md§10 forbids — "Add the missingsys_userrecord to the seed" — which is the same fork #28 asks the maintainer to settle.Generated by Claude Code
fix(data): deal `legal_owner` to the two legal counsel, and stop deal…
seed: deal clm_obligation.owner to the desks that perform it, not the…