Skip to content

pnpm demo prints its "now create the three requesters" instruction ~290 lines and 120 ERROR lines before the ready banner, so nobody reads it - #51

Merged
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-49-demo-banner-order
Sep 10, 2026
Merged

zhuangjianguo merged 4 commits into
mainfrom
claude/issue-49-demo-banner-order

Conversation

@zhuangjianguo

@zhuangjianguo zhuangjianguo commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #49

What changed

scripts/demo.mjs only. The operator instruction — create the three business-requester accounts, run again — used to print inline, immediately before startDemo() handed the terminal to the demo boot. It now prints after that boot has come to rest, and it says what the seed's owner_id errors are while the same reader is still looking at them.

The defect, measured (not repeated from the card)

Clean-database pnpm demo runs (rm -rf .objectstack dist first), captured to a file. Base origin/main @ a7b7db5; branch at 405d67f:

measured on main on this branch
operator instruction at line 12 167
✓ Server is ready at line 147 136
terminal comes to rest (Press Ctrl+C to stop) 169 159
boot output after the instruction 157 lines 0 lines
total lines captured 172 195
ERROR lines in the boot 2 (sys_oauth_resource UNIQUE, unrelated to the seed) same 2
ERROR [SeedLoader] lines 0 in two runs, one held 150s past the banner 0 in a 30s window; 120 in a 90s window

The card's magnitudes are different from mine and the report on #49 says why. The defect is confirmed either way: 157 lines of boot output stood between the instruction and the resting point. The cause the card names is confirmed straight from the seeded database — every clm_contract row landed with owner_id NULL — and from the loader's own error text: Deferred reference UNRESOLVED after pass 2 — clm_contract.owner_id stays NULL … no such sys_user row exists, once per contract row.

The measurement that shaped the fix

The ready banner is where the boot comes to rest. On a first boot it is not always where the seed does:

07:03:46.862  WARN [Seeder] Inline seed exceeded 8000ms budget … continuing in background
07:03:48      ✓ Server is ready · Press Ctrl+C to stop · (this note)
07:05:09.768  ERROR [SeedLoader] … 120 lines, from the background continuation
07:05:09.827  WARN [Seeder] Seed loading completed with … dropped record(s) and … error(s)

82 seconds after the banner. The same command on main, held open for 150s past its banner, never emitted those lines at all while seeding the same corpus — same code, same fixture, two different clocks. Nothing the script can observe tells the cases apart (the WARN, the errors and the summary are all in the child's inherited stream), so the note does not claim the errors are above it. It names the clock that puts them above — seed inside its inline budget, which is what the dogfood pass saw — and the one that puts them below, and it is the frame either way. It is not printed twice to cover both.

Adjudicated by the seat: the strict ordering is a platform gap (nothing an app can observe says the boot has come to rest), filed upstream by the seat and not closed here. No authenticated poll, no pipe.

What the note does NOT print: a census

Earlier revisions of this note printed the counts I had measured — the number of ERROR lines, the per-object row totals, the figure in the loader's summary. Those are out. Every one was true on the boot it was measured on and none is defended by anything, since no gate reads printed prose: #47, in flight in this same batch, deals legal_owner across the seeded corpus and would falsify the error-line count in silence; #41 would move the row census. A stale census printed to an operator as reassurance is the demo telling a stranger a false number about its own health.

So the note states the shape, which is true at any count: one error line per contract whose requester account does not exist yet, every seeded row present with only owner_id NULL, and the loader's summary contradicting the database quoted as its own accounting rather than as a figure. The one number the file still prints is derived at runtime (Math.min(log.length, LOG_TAIL_LINES)), which is the pattern. A three-line rule in the note's doc block says why, so the census does not come back.

How the note reaches the resting point

stdio: 'inherit' is kept and the boot's own output is untouched — not filtered, not reformatted, not dimmed. Piping the child would give an exact "it stopped printing" signal at the cost of the child's TTY, and a boot behind a pipe loses its colour: the seed's ERROR lines would have arrived dimmed by the very edit meant to explain them. So readiness is a TCP probe on the port os dev will bind — resolved the way os dev resolves it (--port, then OS_PORT, then PORT, then 3000) — plus a 2.5s settle for the 22-line banner tail. Both blind cases degrade to the pre-fix behaviour (a note printed mid-stream), never to a lost note; a boot that dies gets no note at all, because os dev has already said why.

Before / after, as a reader experiences it

Before (origin/main, last lines on screen — the instruction is 157 lines above this):

  ⚠ Boot diagnostics — 1 warning logged during startup:
    WARN [Seeder] Inline seed exceeded 8000ms budget for app.objectstack.hotclm; continuing in background…
    run with --log-level debug to watch the boot stream live

  Press Ctrl+C to stop

After (this branch at 405d67f, last lines on screen, copied from the capture):

  ────────────────────────────────────────────────────────────────────────
   Before you open the app — one setup step, and one thing about the log
  ────────────────────────────────────────────────────────────────────────

  1. The seeded contracts have no owner yet. Every contract is launched by
     one of the three business requesters DESIGN.md §10 asks you to create,
     and no seed may create a user (§10) — so until those accounts exist,
     `owner_id` is NULL on every contract row and 我的合同 stays empty. Add
     them in Setup → Users and run this again; the README names them and
     says who gets what.

  2. The `ERROR [SeedLoader]` lines are that same missing owner: one per
     contract whose requester account does not exist yet, expected on a
     first boot, and nothing is lost to them. The loader defers `owner_id`,
     finds no such account on its last pass, writes the row anyway and
     leaves that one column NULL — every seeded row is in the database and
     only its owner is missing. Step 1 is what fills it in.

     The loader then signs off with a summary that counts those rows as
     dropped while they sit in the database: its own accounting, not the
     state of your data, and upstream as objectstack#17177. And where those
     lines fall relative to this note is a clock, not a verdict — above it
     when the seed finished inside its inline budget, up to a couple of
     minutes below it when the boot said `WARN [Seeder] … continuing in
     background`.

Gates — exit codes captured before any pipe

Re-run on the reworked tree (405d67f), each gate redirected to a file first and $? read after, so no exit code passes through a pipe:

GATE validate       EXIT=0
GATE lint           EXIT=0
GATE typecheck      EXIT=0
GATE lint:i18n-gate EXIT=0

Plus node --check scripts/demo.mjs → 0, and a control-character self-scan of the file (grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]') → no matches.

Acceptance notes

🤖 Generated with Claude Code

https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR

…rest

`pnpm demo` printed the "create the three business requesters" instruction
inline, just before handing the terminal to the demo boot. Measured on
main @ a7b7db5, one clean-database run: the note was log line 12 of 172,
`✓ Server is ready` was line 147, and the last line was 172 — the one
instruction that decides whether the app has anything in it scrolled 160
lines out of sight before the terminal stopped moving.

Print it after the boot instead, and say what the seed's `owner_id` errors
are while the same reader is still looking at them: one per contract row,
expected on a first boot, row written with `owner_id` NULL, upstream cause
objectstack#17177. The errors themselves are untouched — the boot keeps its
inherited TTY and its colour, which is why the ready signal is a port probe
and not a pipe over the child's output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR
Measured on this branch: the ready banner is where the BOOT comes to rest,
but not always the SEED. On a contended box the inline seed overran its
8000ms budget, the boot printed `WARN [Seeder] … continuing in background`,
and the 120 `ERROR [SeedLoader]` lines arrived from that continuation 82s
AFTER the banner — and so after the note. A second run never emitted them
inside a 150s window while seeding the same 820 rows.

The note therefore no longer claims the errors are "above" it. It names the
clock that puts them above (seed inside its inline budget) and the one that
puts them below (the background continuation), and it stays the frame either
way. It also carries the number that actually reassures: the loader signs off
with `120 dropped record(s)` while all 820 rows are in the database —
measured, and the contradiction objectstack#17177 is about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR
The comment credited a second branch run for a window that was measured on
`main`. Same seed path either way, but the claim names a measurement and so
has to name the right one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR
The note printed measured counts — the number of ERROR lines, the row
totals per object, the figure the loader's summary quotes. Every one was
true on the boot it was measured on and none is defended by anything: no
gate reads printed prose, so a card that deals owners differently or
changes what the fixture seeds falsifies them in silence. #47 is in flight
doing exactly that. Printed to an operator as reassurance, a stale census
is the demo telling a stranger a false number about its own health.

State the shape instead, which is true at any count: one error line per
contract whose requester account does not exist yet, every seeded row
present with only owner_id NULL, and the loader's summary contradicting
the database quoted as its own accounting rather than as a figure. Also
drops the platform's 8s seed budget from the text — the boot prints the
real number in its own WARN line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pnpm demo prints its "now create the three requesters" instruction ~290 lines and 120 ERROR lines before the ready banner, so nobody reads it

2 participants