zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh) - #121

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-117-zh-demo-seed
Sep 2, 2026
Merged

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh)#121
os-warren merged 1 commit into
mainfrom
claude/issue-117-zh-demo-seed

Conversation

@os-warren

@os-warrenos-warren commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Closes#117

The Chinese UI has been done for a while — nav, columns, statuses, progress phrases and form help all render in zh-CN. Every record underneath it was still English, so a Chinese evaluator saw a Chinese frame around English content. pnpm demo:zh now opens the same worked example with 安岭集团, its people, its duty catalog and six months of its history all in Chinese.

What this is, mechanically

DULY_DEMO_LOCALE (unset = en, zh-CN) chooses the fixture's language, read at compile time exactly as DULY_DEMO_SEED is and for the same reason — the seed is baked into dist/objectstack.json, so both are compile-time decisions and both boots pass --compile. An unrecognised spelling (zh_CN, zh-Hans) is a hard error, not a silent fallback to English: a fallback brings the demo up in English, working perfectly, and nobody finds out until it is on a screen in front of a customer.

One fixture in two languages, not two fixtures. The arrays stay authored in English and are mapped through t() once per section; src/data/demo-zh.ts holds every human-readable string keyed by its English original. A parallel demo-org.zh.ts / demo-catalog.zh.ts would duplicate the structure as well as the prose — two catalogs, two cadence sets, two review-state distributions — and they would drift invisibly. A Chinese demo with 19 catalog items where the English one has 20 is not a bug anybody spots on a screen.

Keying on the English string also reduces "is it translated?" to a set comparison a test can make in both directions, which is what pins item 6: rewording an English line orphans its dictionary entry and goes red.

Machine values are deliberately untouched — unit codes, period_keys, select values, timezones, frequencies are identical in both locales, and the zh-CN translations bundle is what renders the select values. Mailboxes stay ASCII pinyin (chen.zhiyuan@ardenline.example) while display names are Chinese: sys_user.email is an identifier that gets typed, pasted and matched on.

Item 5 — the admin persona, measured

The card said to measure this rather than assume it, and the measurement is the reason the rename ships.

Measured on @objectstack/* 17.2.0, live, on a clean database with nothing else holding it:

StepResult
PATCH /api/v1/data/sys_user/{id} with {"name":"演示管理员"} as the signed-in admin200, name reads back 演示管理员
Sign in again, same credentials200, user 演示管理员, email admin@objectos.ai
pnpm demo:zh on a fresh DB1/2 done — admin account ready, renamed to 演示管理员 for this locale.
Seed's natural-key match13 sys_user rows, not 14 — the seed matched the renamed row and skipped it
The demo account's own screens26 tasks owned by the signed-in admin, 6 of them open/in_progress → My week is populated
Restart on the same database1/2 done — admin account ready. (no rename — idempotent), sign-in 200, every row count identical

So the rename lands, sign-in survives it, and the replay is a no-op. Dev Admin stays in English.

The failure this guards is specific and silent, which is why scripts/demo.mjsfails loudly rather than warning: if the rename did not happen, the seed would find no 演示管理员 row, insert a fourteenth user, and hand the demo account's duties, tasks, assignment and work log to a person nobody can log in as — leaving My week, My duties, Sent by me and Work log empty on the first screen an evaluator opens, with the seed reporting success.

The rename can only happen in the priming boot: plugin-auth's maybeSeedDevAdmin does not take a name, and the seed's sys_user row for the admin carries its natural key and nothing else on purpose (anything more turns the loader's no-op skip into an UPDATE against a live credential-bearing account).

Item 3 — readable position codes

plant_compliance_officerPlant compliance officer / 厂区合规专员, and the two siblings. The column is free text the app never parsed — it was only ever being read by people, who do not write their own job title that way in either language. Still matched verbatim by the sync and apply actions, so Plant compliance officer and plant compliance officer remain two different positions.

No test was keyed on POSITIONS; the ones using plant_compliance_officer (catalog-instantiate, catalog-action-placement, catalog-engine-facade) supply their own literals and are untouched. The sync action's placeholder was deliberately not changed — it is mirrored in the zh-CN translations bundle, which this card excludes — filed as #120 along with samples/catalog-items.csv, which still carries the old spelling.

Tests

pnpm test 868 passed / 35 files (baseline on origin/main was 833 / 32), all four gates green at e05dcfb.

  • test/seed-locale.test.ts boots a real kernel per locale with the declarative seeder running, and reads the rows back — not the fixture arrays, because half of these strings are natural keys and the failure a translation causes is a reference that resolves to nothing, not prose that reads oddly. Asserts: two independent databases (no Chinese duty in the English DB and vice versa — without which "same counts" would be trivially true of a doubly-seeded store); equal row counts across all 8 objects; equal status / caliber / review-state / frequency / period_key distributions; a Han character in every one of the 395 zh-CN display strings, and none in the English ones; ASCII mailboxes on the reserved domain; every catalog reference an invented internal document (《…》第N条, and never GB/ISO/国标); and the English unit, user, catalog, position, duty, assignment and log names pinned literally — not toMatchSnapshot(), which -u updates without anybody reading the diff.
  • test/demo-locale.test.ts pins the switch (accepted spellings, and that zh_CN throws rather than answering in English) and holds the dictionary to the fixture in both directions.
  • test/demo-script.test.ts holds scripts/demo.mjs's admin name to the fixture's ADMIN. They are two copies of one fact — plain .mjs that runs before anything compiles, and TypeScript baked into the artifact — and they cannot be one constant.

Both new guards were reverse-verified

A guard that has never been observed failing is indistinguishable from one that cannot fail, so each was ablated on a committed tree, with the mutation confirmed on disk by grepping for the removed and the injected text, and restored by an EXIT INT TERM trap.

Ablation A — one translation replaced by its English original (向承运方催了三张缺失的转移联单 → the English string; confirmed on disk: removed=0, injected=1). Both guards went red, and the seed-locale failure named the actual field:

FAIL test/seed-locale.test.ts > every display string a Chinese boot writes carries a Han character
AssertionError: expected [ [ 'duly_log_entry.subject', …(1) ] ] to deeply equal []
FAIL test/demo-locale.test.ts > translates every one of them into something actually Chinese

Ablation B — an English fixture line reworded (Chased the carrier for three missing transfer notes…for the missing transfer notes; confirmed on disk: original=0, reworded=1). Red three ways, which is the item-6 pin working:

Error: demo fixture: no zh-CN translation for "Chased the carrier for the missing transfer notes"
FAIL > every string the fixture asks for has a zh-CN translation
FAIL > and every translation is asked for by the fixture

No rebuild leg was needed: vitest reads the TypeScript source, and both suites point artifactPath at a path that cannot exist so a stale dist/objectstack.json cannot be what is under test.

Live boot evidence — pnpm demo:zh, clean database, port 4321

 Duly demo — two steps, then the server is yours.
1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready, renamed to 演示管理员 for this locale.
2/2 starting Duly with the demo organisation loaded…
POST /api/v1/auth/sign-in/email → 200, name 演示管理员
GET /api/v1/data/duly_task?limit=5 → 200, total 183
- 班前安全讲话记录—河畔厂区 | open | 2026-09 | late_after 2026-09-13
- 产线安全巡查—B线 | done | 2026-05 | late_after 2026-05-05
- 产线安全巡查—河畔厂区 | open | 2026-08 | late_after 2026-08-05
- 交接班记录—A线 | done | 2026-W33 | late_after 2026-08-10
- 每月厂区运行手记 | done | 2026-03 | late_after 2026-03-30
units 安岭集团/ARD · 北门厂区/NGP · 河畔厂区/RVP · 集团总部/CEN
北门生产部/NGP-OPS · 北门质量部/NGP-QA
positions 厂区合规专员 · 班组长 · 质量技术员
catalog 承包商入场培训复训 / 为仍持有通行证的每一位承包商重做一次入场培训… / 《厂区安全标准 SS-15》第3条
note 夜班还有两人没参加,另安排一次补讲。
skip 整个周期 A 线都在大修停机——没有产线可巡。
log 与监管联络人的例行通话 | category meeting | visibility manager

Row counts, both locales, both boots — identical throughout:

objectcount
sys_user13
sys_business_unit6
sys_business_unit_member12
duly_catalog_item20
duly_duty31
duly_assignment2
duly_task183
duly_log_entry15

And pnpm demo on a clean database is unchanged — 183 tasks, 13 users, Dev Admin, English subjects, with the readable position codes item 3 asked for. That control was re-taken: an earlier reading was contaminated by a stray server (setsid re-parents, so $! is not the process-group leader and the first teardown missed four servers). All servers were killed by process group; none survive.

Notes for review

  • The card says "21 catalog items"; the fixture has 20 (the 21 is samples/catalog-items.csv, which carries one extra row, Daily line start-up check). All 20 are translated.
  • The card names test/seed.test.ts for the per-locale boot. It is in test/seed-locale.test.ts instead: seed.test.ts boots one kernel from static imports, and a second locale needs vi.resetModules() plus dynamic imports, so a separate file is both cleaner and parallelisable. seed.test.ts is untouched and still green.
  • Nothing was changed in objectstack.config.ts, the objects, the views or the translations bundle.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p

The Chinese UI has been done for a while — nav, columns, statuses, progress
phrases and form help all render in zh-CN. Every RECORD underneath it was
still English, so a Chinese evaluator saw a Chinese frame around English
content, which reads as "not localised" however good the chrome is.
`DULY_DEMO_LOCALE` (unset = `en`, `zh-CN`) now chooses the fixture's language,
read at compile time exactly as `DULY_DEMO_SEED` is and for the same reason —
the seed is baked into `dist/objectstack.json`. `pnpm demo:zh` is `pnpm demo`
with it set, and `scripts/demo.mjs` passes it to BOTH boots.
One fixture, two languages, not two fixtures. The arrays stay authored in
English and are mapped through `t()` once per section; `src/data/demo-zh.ts`
holds every human-readable string keyed by its English original. That keeps
one org chart, one history planner and one set of invariants — a parallel
zh fixture would drift, and a Chinese demo with 19 catalog items where the
English one has 20 is not a bug anybody spots on a screen. It also makes "is
it translated?" a set comparison a test can make, in both directions.
Machine values are deliberately untouched: unit codes, period keys, select
values, timezones and frequencies are identical in both locales. Mailboxes
stay ASCII pinyin on the reserved `.example` domain while display names are
Chinese.
Position codes are now readable in both languages — `Plant compliance
officer` / `厂区合规专员` rather than `plant_compliance_officer`. The column
is free text the app never parsed; it was only ever being read by people.
The admin persona is MEASURED, not assumed (`@objectstack/*` 17.2.0): the
priming boot PATCHes `sys_user.name` to `演示管理员` before the seed runs,
signing in with the same credentials afterwards still returns 200, and the
seed replays as a no-op against the renamed row. It fails loudly if the
rename does not land, because the alternative is a fourteenth user owning
the demo account's work and four empty screens on first boot.
Tests: `test/seed-locale.test.ts` boots a kernel per locale and asserts equal
row counts, equal status/caliber/period distributions, a Han character in
every zh-CN display string, ASCII mailboxes, invented-only references, and
the English names and subjects pinned literally. `test/demo-locale.test.ts`
pins the switch and holds the dictionary to the fixture in both directions —
which is what stops this card quietly rewording the English demo.
`test/demo-script.test.ts` holds `scripts/demo.mjs` to the fixture's `ADMIN`.
Closes#117
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 2, 2026 10:39
@os-warren
os-warren merged commit 5c5a8a0 into mainSep 2, 2026
1 check passed
Sign up for freeto 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.

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh)

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh) - #121

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-117-zh-demo-seed
Sep 2, 2026
Merged

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh)#121
os-warren merged 1 commit into
mainfrom
claude/issue-117-zh-demo-seed

Conversation

@os-warren

@os-warrenos-warren commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Closes#117

The Chinese UI has been done for a while — nav, columns, statuses, progress phrases and form help all render in zh-CN. Every record underneath it was still English, so a Chinese evaluator saw a Chinese frame around English content. pnpm demo:zh now opens the same worked example with 安岭集团, its people, its duty catalog and six months of its history all in Chinese.

What this is, mechanically

DULY_DEMO_LOCALE (unset = en, zh-CN) chooses the fixture's language, read at compile time exactly as DULY_DEMO_SEED is and for the same reason — the seed is baked into dist/objectstack.json, so both are compile-time decisions and both boots pass --compile. An unrecognised spelling (zh_CN, zh-Hans) is a hard error, not a silent fallback to English: a fallback brings the demo up in English, working perfectly, and nobody finds out until it is on a screen in front of a customer.

One fixture in two languages, not two fixtures. The arrays stay authored in English and are mapped through t() once per section; src/data/demo-zh.ts holds every human-readable string keyed by its English original. A parallel demo-org.zh.ts / demo-catalog.zh.ts would duplicate the structure as well as the prose — two catalogs, two cadence sets, two review-state distributions — and they would drift invisibly. A Chinese demo with 19 catalog items where the English one has 20 is not a bug anybody spots on a screen.

Keying on the English string also reduces "is it translated?" to a set comparison a test can make in both directions, which is what pins item 6: rewording an English line orphans its dictionary entry and goes red.

Machine values are deliberately untouched — unit codes, period_keys, select values, timezones, frequencies are identical in both locales, and the zh-CN translations bundle is what renders the select values. Mailboxes stay ASCII pinyin (chen.zhiyuan@ardenline.example) while display names are Chinese: sys_user.email is an identifier that gets typed, pasted and matched on.

Item 5 — the admin persona, measured

The card said to measure this rather than assume it, and the measurement is the reason the rename ships.

Measured on @objectstack/* 17.2.0, live, on a clean database with nothing else holding it:

StepResult
PATCH /api/v1/data/sys_user/{id} with {"name":"演示管理员"} as the signed-in admin200, name reads back 演示管理员
Sign in again, same credentials200, user 演示管理员, email admin@objectos.ai
pnpm demo:zh on a fresh DB1/2 done — admin account ready, renamed to 演示管理员 for this locale.
Seed's natural-key match13 sys_user rows, not 14 — the seed matched the renamed row and skipped it
The demo account's own screens26 tasks owned by the signed-in admin, 6 of them open/in_progress → My week is populated
Restart on the same database1/2 done — admin account ready. (no rename — idempotent), sign-in 200, every row count identical

So the rename lands, sign-in survives it, and the replay is a no-op. Dev Admin stays in English.

The failure this guards is specific and silent, which is why scripts/demo.mjsfails loudly rather than warning: if the rename did not happen, the seed would find no 演示管理员 row, insert a fourteenth user, and hand the demo account's duties, tasks, assignment and work log to a person nobody can log in as — leaving My week, My duties, Sent by me and Work log empty on the first screen an evaluator opens, with the seed reporting success.

The rename can only happen in the priming boot: plugin-auth's maybeSeedDevAdmin does not take a name, and the seed's sys_user row for the admin carries its natural key and nothing else on purpose (anything more turns the loader's no-op skip into an UPDATE against a live credential-bearing account).

Item 3 — readable position codes

plant_compliance_officerPlant compliance officer / 厂区合规专员, and the two siblings. The column is free text the app never parsed — it was only ever being read by people, who do not write their own job title that way in either language. Still matched verbatim by the sync and apply actions, so Plant compliance officer and plant compliance officer remain two different positions.

No test was keyed on POSITIONS; the ones using plant_compliance_officer (catalog-instantiate, catalog-action-placement, catalog-engine-facade) supply their own literals and are untouched. The sync action's placeholder was deliberately not changed — it is mirrored in the zh-CN translations bundle, which this card excludes — filed as #120 along with samples/catalog-items.csv, which still carries the old spelling.

Tests

pnpm test 868 passed / 35 files (baseline on origin/main was 833 / 32), all four gates green at e05dcfb.

  • test/seed-locale.test.ts boots a real kernel per locale with the declarative seeder running, and reads the rows back — not the fixture arrays, because half of these strings are natural keys and the failure a translation causes is a reference that resolves to nothing, not prose that reads oddly. Asserts: two independent databases (no Chinese duty in the English DB and vice versa — without which "same counts" would be trivially true of a doubly-seeded store); equal row counts across all 8 objects; equal status / caliber / review-state / frequency / period_key distributions; a Han character in every one of the 395 zh-CN display strings, and none in the English ones; ASCII mailboxes on the reserved domain; every catalog reference an invented internal document (《…》第N条, and never GB/ISO/国标); and the English unit, user, catalog, position, duty, assignment and log names pinned literally — not toMatchSnapshot(), which -u updates without anybody reading the diff.
  • test/demo-locale.test.ts pins the switch (accepted spellings, and that zh_CN throws rather than answering in English) and holds the dictionary to the fixture in both directions.
  • test/demo-script.test.ts holds scripts/demo.mjs's admin name to the fixture's ADMIN. They are two copies of one fact — plain .mjs that runs before anything compiles, and TypeScript baked into the artifact — and they cannot be one constant.

Both new guards were reverse-verified

A guard that has never been observed failing is indistinguishable from one that cannot fail, so each was ablated on a committed tree, with the mutation confirmed on disk by grepping for the removed and the injected text, and restored by an EXIT INT TERM trap.

Ablation A — one translation replaced by its English original (向承运方催了三张缺失的转移联单 → the English string; confirmed on disk: removed=0, injected=1). Both guards went red, and the seed-locale failure named the actual field:

FAIL test/seed-locale.test.ts > every display string a Chinese boot writes carries a Han character
AssertionError: expected [ [ 'duly_log_entry.subject', …(1) ] ] to deeply equal []
FAIL test/demo-locale.test.ts > translates every one of them into something actually Chinese

Ablation B — an English fixture line reworded (Chased the carrier for three missing transfer notes…for the missing transfer notes; confirmed on disk: original=0, reworded=1). Red three ways, which is the item-6 pin working:

Error: demo fixture: no zh-CN translation for "Chased the carrier for the missing transfer notes"
FAIL > every string the fixture asks for has a zh-CN translation
FAIL > and every translation is asked for by the fixture

No rebuild leg was needed: vitest reads the TypeScript source, and both suites point artifactPath at a path that cannot exist so a stale dist/objectstack.json cannot be what is under test.

Live boot evidence — pnpm demo:zh, clean database, port 4321

 Duly demo — two steps, then the server is yours.
1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready, renamed to 演示管理员 for this locale.
2/2 starting Duly with the demo organisation loaded…
POST /api/v1/auth/sign-in/email → 200, name 演示管理员
GET /api/v1/data/duly_task?limit=5 → 200, total 183
- 班前安全讲话记录—河畔厂区 | open | 2026-09 | late_after 2026-09-13
- 产线安全巡查—B线 | done | 2026-05 | late_after 2026-05-05
- 产线安全巡查—河畔厂区 | open | 2026-08 | late_after 2026-08-05
- 交接班记录—A线 | done | 2026-W33 | late_after 2026-08-10
- 每月厂区运行手记 | done | 2026-03 | late_after 2026-03-30
units 安岭集团/ARD · 北门厂区/NGP · 河畔厂区/RVP · 集团总部/CEN
北门生产部/NGP-OPS · 北门质量部/NGP-QA
positions 厂区合规专员 · 班组长 · 质量技术员
catalog 承包商入场培训复训 / 为仍持有通行证的每一位承包商重做一次入场培训… / 《厂区安全标准 SS-15》第3条
note 夜班还有两人没参加,另安排一次补讲。
skip 整个周期 A 线都在大修停机——没有产线可巡。
log 与监管联络人的例行通话 | category meeting | visibility manager

Row counts, both locales, both boots — identical throughout:

objectcount
sys_user13
sys_business_unit6
sys_business_unit_member12
duly_catalog_item20
duly_duty31
duly_assignment2
duly_task183
duly_log_entry15

And pnpm demo on a clean database is unchanged — 183 tasks, 13 users, Dev Admin, English subjects, with the readable position codes item 3 asked for. That control was re-taken: an earlier reading was contaminated by a stray server (setsid re-parents, so $! is not the process-group leader and the first teardown missed four servers). All servers were killed by process group; none survive.

Notes for review

  • The card says "21 catalog items"; the fixture has 20 (the 21 is samples/catalog-items.csv, which carries one extra row, Daily line start-up check). All 20 are translated.
  • The card names test/seed.test.ts for the per-locale boot. It is in test/seed-locale.test.ts instead: seed.test.ts boots one kernel from static imports, and a second locale needs vi.resetModules() plus dynamic imports, so a separate file is both cleaner and parallelisable. seed.test.ts is untouched and still green.
  • Nothing was changed in objectstack.config.ts, the objects, the views or the translations bundle.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p

The Chinese UI has been done for a while — nav, columns, statuses, progress
phrases and form help all render in zh-CN. Every RECORD underneath it was
still English, so a Chinese evaluator saw a Chinese frame around English
content, which reads as "not localised" however good the chrome is.
`DULY_DEMO_LOCALE` (unset = `en`, `zh-CN`) now chooses the fixture's language,
read at compile time exactly as `DULY_DEMO_SEED` is and for the same reason —
the seed is baked into `dist/objectstack.json`. `pnpm demo:zh` is `pnpm demo`
with it set, and `scripts/demo.mjs` passes it to BOTH boots.
One fixture, two languages, not two fixtures. The arrays stay authored in
English and are mapped through `t()` once per section; `src/data/demo-zh.ts`
holds every human-readable string keyed by its English original. That keeps
one org chart, one history planner and one set of invariants — a parallel
zh fixture would drift, and a Chinese demo with 19 catalog items where the
English one has 20 is not a bug anybody spots on a screen. It also makes "is
it translated?" a set comparison a test can make, in both directions.
Machine values are deliberately untouched: unit codes, period keys, select
values, timezones and frequencies are identical in both locales. Mailboxes
stay ASCII pinyin on the reserved `.example` domain while display names are
Chinese.
Position codes are now readable in both languages — `Plant compliance
officer` / `厂区合规专员` rather than `plant_compliance_officer`. The column
is free text the app never parsed; it was only ever being read by people.
The admin persona is MEASURED, not assumed (`@objectstack/*` 17.2.0): the
priming boot PATCHes `sys_user.name` to `演示管理员` before the seed runs,
signing in with the same credentials afterwards still returns 200, and the
seed replays as a no-op against the renamed row. It fails loudly if the
rename does not land, because the alternative is a fourteenth user owning
the demo account's work and four empty screens on first boot.
Tests: `test/seed-locale.test.ts` boots a kernel per locale and asserts equal
row counts, equal status/caliber/period distributions, a Han character in
every zh-CN display string, ASCII mailboxes, invented-only references, and
the English names and subjects pinned literally. `test/demo-locale.test.ts`
pins the switch and holds the dictionary to the fixture in both directions —
which is what stops this card quietly rewording the English demo.
`test/demo-script.test.ts` holds `scripts/demo.mjs` to the fixture's `ADMIN`.
Closes#117
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 2, 2026 10:39
@os-warren
os-warren merged commit 5c5a8a0 into mainSep 2, 2026
1 check passed
Sign up for freeto 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.

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh)

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh) - #121

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-117-zh-demo-seed
Sep 2, 2026
Merged

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh)#121
os-warren merged 1 commit into
mainfrom
claude/issue-117-zh-demo-seed

Conversation

@os-warren

@os-warrenos-warren commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Closes#117

The Chinese UI has been done for a while — nav, columns, statuses, progress phrases and form help all render in zh-CN. Every record underneath it was still English, so a Chinese evaluator saw a Chinese frame around English content. pnpm demo:zh now opens the same worked example with 安岭集团, its people, its duty catalog and six months of its history all in Chinese.

What this is, mechanically

DULY_DEMO_LOCALE (unset = en, zh-CN) chooses the fixture's language, read at compile time exactly as DULY_DEMO_SEED is and for the same reason — the seed is baked into dist/objectstack.json, so both are compile-time decisions and both boots pass --compile. An unrecognised spelling (zh_CN, zh-Hans) is a hard error, not a silent fallback to English: a fallback brings the demo up in English, working perfectly, and nobody finds out until it is on a screen in front of a customer.

One fixture in two languages, not two fixtures. The arrays stay authored in English and are mapped through t() once per section; src/data/demo-zh.ts holds every human-readable string keyed by its English original. A parallel demo-org.zh.ts / demo-catalog.zh.ts would duplicate the structure as well as the prose — two catalogs, two cadence sets, two review-state distributions — and they would drift invisibly. A Chinese demo with 19 catalog items where the English one has 20 is not a bug anybody spots on a screen.

Keying on the English string also reduces "is it translated?" to a set comparison a test can make in both directions, which is what pins item 6: rewording an English line orphans its dictionary entry and goes red.

Machine values are deliberately untouched — unit codes, period_keys, select values, timezones, frequencies are identical in both locales, and the zh-CN translations bundle is what renders the select values. Mailboxes stay ASCII pinyin (chen.zhiyuan@ardenline.example) while display names are Chinese: sys_user.email is an identifier that gets typed, pasted and matched on.

Item 5 — the admin persona, measured

The card said to measure this rather than assume it, and the measurement is the reason the rename ships.

Measured on @objectstack/* 17.2.0, live, on a clean database with nothing else holding it:

StepResult
PATCH /api/v1/data/sys_user/{id} with {"name":"演示管理员"} as the signed-in admin200, name reads back 演示管理员
Sign in again, same credentials200, user 演示管理员, email admin@objectos.ai
pnpm demo:zh on a fresh DB1/2 done — admin account ready, renamed to 演示管理员 for this locale.
Seed's natural-key match13 sys_user rows, not 14 — the seed matched the renamed row and skipped it
The demo account's own screens26 tasks owned by the signed-in admin, 6 of them open/in_progress → My week is populated
Restart on the same database1/2 done — admin account ready. (no rename — idempotent), sign-in 200, every row count identical

So the rename lands, sign-in survives it, and the replay is a no-op. Dev Admin stays in English.

The failure this guards is specific and silent, which is why scripts/demo.mjsfails loudly rather than warning: if the rename did not happen, the seed would find no 演示管理员 row, insert a fourteenth user, and hand the demo account's duties, tasks, assignment and work log to a person nobody can log in as — leaving My week, My duties, Sent by me and Work log empty on the first screen an evaluator opens, with the seed reporting success.

The rename can only happen in the priming boot: plugin-auth's maybeSeedDevAdmin does not take a name, and the seed's sys_user row for the admin carries its natural key and nothing else on purpose (anything more turns the loader's no-op skip into an UPDATE against a live credential-bearing account).

Item 3 — readable position codes

plant_compliance_officerPlant compliance officer / 厂区合规专员, and the two siblings. The column is free text the app never parsed — it was only ever being read by people, who do not write their own job title that way in either language. Still matched verbatim by the sync and apply actions, so Plant compliance officer and plant compliance officer remain two different positions.

No test was keyed on POSITIONS; the ones using plant_compliance_officer (catalog-instantiate, catalog-action-placement, catalog-engine-facade) supply their own literals and are untouched. The sync action's placeholder was deliberately not changed — it is mirrored in the zh-CN translations bundle, which this card excludes — filed as #120 along with samples/catalog-items.csv, which still carries the old spelling.

Tests

pnpm test 868 passed / 35 files (baseline on origin/main was 833 / 32), all four gates green at e05dcfb.

  • test/seed-locale.test.ts boots a real kernel per locale with the declarative seeder running, and reads the rows back — not the fixture arrays, because half of these strings are natural keys and the failure a translation causes is a reference that resolves to nothing, not prose that reads oddly. Asserts: two independent databases (no Chinese duty in the English DB and vice versa — without which "same counts" would be trivially true of a doubly-seeded store); equal row counts across all 8 objects; equal status / caliber / review-state / frequency / period_key distributions; a Han character in every one of the 395 zh-CN display strings, and none in the English ones; ASCII mailboxes on the reserved domain; every catalog reference an invented internal document (《…》第N条, and never GB/ISO/国标); and the English unit, user, catalog, position, duty, assignment and log names pinned literally — not toMatchSnapshot(), which -u updates without anybody reading the diff.
  • test/demo-locale.test.ts pins the switch (accepted spellings, and that zh_CN throws rather than answering in English) and holds the dictionary to the fixture in both directions.
  • test/demo-script.test.ts holds scripts/demo.mjs's admin name to the fixture's ADMIN. They are two copies of one fact — plain .mjs that runs before anything compiles, and TypeScript baked into the artifact — and they cannot be one constant.

Both new guards were reverse-verified

A guard that has never been observed failing is indistinguishable from one that cannot fail, so each was ablated on a committed tree, with the mutation confirmed on disk by grepping for the removed and the injected text, and restored by an EXIT INT TERM trap.

Ablation A — one translation replaced by its English original (向承运方催了三张缺失的转移联单 → the English string; confirmed on disk: removed=0, injected=1). Both guards went red, and the seed-locale failure named the actual field:

FAIL test/seed-locale.test.ts > every display string a Chinese boot writes carries a Han character
AssertionError: expected [ [ 'duly_log_entry.subject', …(1) ] ] to deeply equal []
FAIL test/demo-locale.test.ts > translates every one of them into something actually Chinese

Ablation B — an English fixture line reworded (Chased the carrier for three missing transfer notes…for the missing transfer notes; confirmed on disk: original=0, reworded=1). Red three ways, which is the item-6 pin working:

Error: demo fixture: no zh-CN translation for "Chased the carrier for the missing transfer notes"
FAIL > every string the fixture asks for has a zh-CN translation
FAIL > and every translation is asked for by the fixture

No rebuild leg was needed: vitest reads the TypeScript source, and both suites point artifactPath at a path that cannot exist so a stale dist/objectstack.json cannot be what is under test.

Live boot evidence — pnpm demo:zh, clean database, port 4321

 Duly demo — two steps, then the server is yours.
1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready, renamed to 演示管理员 for this locale.
2/2 starting Duly with the demo organisation loaded…
POST /api/v1/auth/sign-in/email → 200, name 演示管理员
GET /api/v1/data/duly_task?limit=5 → 200, total 183
- 班前安全讲话记录—河畔厂区 | open | 2026-09 | late_after 2026-09-13
- 产线安全巡查—B线 | done | 2026-05 | late_after 2026-05-05
- 产线安全巡查—河畔厂区 | open | 2026-08 | late_after 2026-08-05
- 交接班记录—A线 | done | 2026-W33 | late_after 2026-08-10
- 每月厂区运行手记 | done | 2026-03 | late_after 2026-03-30
units 安岭集团/ARD · 北门厂区/NGP · 河畔厂区/RVP · 集团总部/CEN
北门生产部/NGP-OPS · 北门质量部/NGP-QA
positions 厂区合规专员 · 班组长 · 质量技术员
catalog 承包商入场培训复训 / 为仍持有通行证的每一位承包商重做一次入场培训… / 《厂区安全标准 SS-15》第3条
note 夜班还有两人没参加,另安排一次补讲。
skip 整个周期 A 线都在大修停机——没有产线可巡。
log 与监管联络人的例行通话 | category meeting | visibility manager

Row counts, both locales, both boots — identical throughout:

objectcount
sys_user13
sys_business_unit6
sys_business_unit_member12
duly_catalog_item20
duly_duty31
duly_assignment2
duly_task183
duly_log_entry15

And pnpm demo on a clean database is unchanged — 183 tasks, 13 users, Dev Admin, English subjects, with the readable position codes item 3 asked for. That control was re-taken: an earlier reading was contaminated by a stray server (setsid re-parents, so $! is not the process-group leader and the first teardown missed four servers). All servers were killed by process group; none survive.

Notes for review

  • The card says "21 catalog items"; the fixture has 20 (the 21 is samples/catalog-items.csv, which carries one extra row, Daily line start-up check). All 20 are translated.
  • The card names test/seed.test.ts for the per-locale boot. It is in test/seed-locale.test.ts instead: seed.test.ts boots one kernel from static imports, and a second locale needs vi.resetModules() plus dynamic imports, so a separate file is both cleaner and parallelisable. seed.test.ts is untouched and still green.
  • Nothing was changed in objectstack.config.ts, the objects, the views or the translations bundle.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p

The Chinese UI has been done for a while — nav, columns, statuses, progress
phrases and form help all render in zh-CN. Every RECORD underneath it was
still English, so a Chinese evaluator saw a Chinese frame around English
content, which reads as "not localised" however good the chrome is.
`DULY_DEMO_LOCALE` (unset = `en`, `zh-CN`) now chooses the fixture's language,
read at compile time exactly as `DULY_DEMO_SEED` is and for the same reason —
the seed is baked into `dist/objectstack.json`. `pnpm demo:zh` is `pnpm demo`
with it set, and `scripts/demo.mjs` passes it to BOTH boots.
One fixture, two languages, not two fixtures. The arrays stay authored in
English and are mapped through `t()` once per section; `src/data/demo-zh.ts`
holds every human-readable string keyed by its English original. That keeps
one org chart, one history planner and one set of invariants — a parallel
zh fixture would drift, and a Chinese demo with 19 catalog items where the
English one has 20 is not a bug anybody spots on a screen. It also makes "is
it translated?" a set comparison a test can make, in both directions.
Machine values are deliberately untouched: unit codes, period keys, select
values, timezones and frequencies are identical in both locales. Mailboxes
stay ASCII pinyin on the reserved `.example` domain while display names are
Chinese.
Position codes are now readable in both languages — `Plant compliance
officer` / `厂区合规专员` rather than `plant_compliance_officer`. The column
is free text the app never parsed; it was only ever being read by people.
The admin persona is MEASURED, not assumed (`@objectstack/*` 17.2.0): the
priming boot PATCHes `sys_user.name` to `演示管理员` before the seed runs,
signing in with the same credentials afterwards still returns 200, and the
seed replays as a no-op against the renamed row. It fails loudly if the
rename does not land, because the alternative is a fourteenth user owning
the demo account's work and four empty screens on first boot.
Tests: `test/seed-locale.test.ts` boots a kernel per locale and asserts equal
row counts, equal status/caliber/period distributions, a Han character in
every zh-CN display string, ASCII mailboxes, invented-only references, and
the English names and subjects pinned literally. `test/demo-locale.test.ts`
pins the switch and holds the dictionary to the fixture in both directions —
which is what stops this card quietly rewording the English demo.
`test/demo-script.test.ts` holds `scripts/demo.mjs` to the fixture's `ADMIN`.
Closes#117
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 2, 2026 10:39
@os-warren
os-warren merged commit 5c5a8a0 into mainSep 2, 2026
1 check passed
Sign up for freeto 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.

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh)

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh) - #121

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-117-zh-demo-seed
Sep 2, 2026
Merged

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh)#121
os-warren merged 1 commit into
mainfrom
claude/issue-117-zh-demo-seed

Conversation

@os-warren

@os-warrenos-warren commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Closes#117

The Chinese UI has been done for a while — nav, columns, statuses, progress phrases and form help all render in zh-CN. Every record underneath it was still English, so a Chinese evaluator saw a Chinese frame around English content. pnpm demo:zh now opens the same worked example with 安岭集团, its people, its duty catalog and six months of its history all in Chinese.

What this is, mechanically

DULY_DEMO_LOCALE (unset = en, zh-CN) chooses the fixture's language, read at compile time exactly as DULY_DEMO_SEED is and for the same reason — the seed is baked into dist/objectstack.json, so both are compile-time decisions and both boots pass --compile. An unrecognised spelling (zh_CN, zh-Hans) is a hard error, not a silent fallback to English: a fallback brings the demo up in English, working perfectly, and nobody finds out until it is on a screen in front of a customer.

One fixture in two languages, not two fixtures. The arrays stay authored in English and are mapped through t() once per section; src/data/demo-zh.ts holds every human-readable string keyed by its English original. A parallel demo-org.zh.ts / demo-catalog.zh.ts would duplicate the structure as well as the prose — two catalogs, two cadence sets, two review-state distributions — and they would drift invisibly. A Chinese demo with 19 catalog items where the English one has 20 is not a bug anybody spots on a screen.

Keying on the English string also reduces "is it translated?" to a set comparison a test can make in both directions, which is what pins item 6: rewording an English line orphans its dictionary entry and goes red.

Machine values are deliberately untouched — unit codes, period_keys, select values, timezones, frequencies are identical in both locales, and the zh-CN translations bundle is what renders the select values. Mailboxes stay ASCII pinyin (chen.zhiyuan@ardenline.example) while display names are Chinese: sys_user.email is an identifier that gets typed, pasted and matched on.

Item 5 — the admin persona, measured

The card said to measure this rather than assume it, and the measurement is the reason the rename ships.

Measured on @objectstack/* 17.2.0, live, on a clean database with nothing else holding it:

StepResult
PATCH /api/v1/data/sys_user/{id} with {"name":"演示管理员"} as the signed-in admin200, name reads back 演示管理员
Sign in again, same credentials200, user 演示管理员, email admin@objectos.ai
pnpm demo:zh on a fresh DB1/2 done — admin account ready, renamed to 演示管理员 for this locale.
Seed's natural-key match13 sys_user rows, not 14 — the seed matched the renamed row and skipped it
The demo account's own screens26 tasks owned by the signed-in admin, 6 of them open/in_progress → My week is populated
Restart on the same database1/2 done — admin account ready. (no rename — idempotent), sign-in 200, every row count identical

So the rename lands, sign-in survives it, and the replay is a no-op. Dev Admin stays in English.

The failure this guards is specific and silent, which is why scripts/demo.mjsfails loudly rather than warning: if the rename did not happen, the seed would find no 演示管理员 row, insert a fourteenth user, and hand the demo account's duties, tasks, assignment and work log to a person nobody can log in as — leaving My week, My duties, Sent by me and Work log empty on the first screen an evaluator opens, with the seed reporting success.

The rename can only happen in the priming boot: plugin-auth's maybeSeedDevAdmin does not take a name, and the seed's sys_user row for the admin carries its natural key and nothing else on purpose (anything more turns the loader's no-op skip into an UPDATE against a live credential-bearing account).

Item 3 — readable position codes

plant_compliance_officerPlant compliance officer / 厂区合规专员, and the two siblings. The column is free text the app never parsed — it was only ever being read by people, who do not write their own job title that way in either language. Still matched verbatim by the sync and apply actions, so Plant compliance officer and plant compliance officer remain two different positions.

No test was keyed on POSITIONS; the ones using plant_compliance_officer (catalog-instantiate, catalog-action-placement, catalog-engine-facade) supply their own literals and are untouched. The sync action's placeholder was deliberately not changed — it is mirrored in the zh-CN translations bundle, which this card excludes — filed as #120 along with samples/catalog-items.csv, which still carries the old spelling.

Tests

pnpm test 868 passed / 35 files (baseline on origin/main was 833 / 32), all four gates green at e05dcfb.

  • test/seed-locale.test.ts boots a real kernel per locale with the declarative seeder running, and reads the rows back — not the fixture arrays, because half of these strings are natural keys and the failure a translation causes is a reference that resolves to nothing, not prose that reads oddly. Asserts: two independent databases (no Chinese duty in the English DB and vice versa — without which "same counts" would be trivially true of a doubly-seeded store); equal row counts across all 8 objects; equal status / caliber / review-state / frequency / period_key distributions; a Han character in every one of the 395 zh-CN display strings, and none in the English ones; ASCII mailboxes on the reserved domain; every catalog reference an invented internal document (《…》第N条, and never GB/ISO/国标); and the English unit, user, catalog, position, duty, assignment and log names pinned literally — not toMatchSnapshot(), which -u updates without anybody reading the diff.
  • test/demo-locale.test.ts pins the switch (accepted spellings, and that zh_CN throws rather than answering in English) and holds the dictionary to the fixture in both directions.
  • test/demo-script.test.ts holds scripts/demo.mjs's admin name to the fixture's ADMIN. They are two copies of one fact — plain .mjs that runs before anything compiles, and TypeScript baked into the artifact — and they cannot be one constant.

Both new guards were reverse-verified

A guard that has never been observed failing is indistinguishable from one that cannot fail, so each was ablated on a committed tree, with the mutation confirmed on disk by grepping for the removed and the injected text, and restored by an EXIT INT TERM trap.

Ablation A — one translation replaced by its English original (向承运方催了三张缺失的转移联单 → the English string; confirmed on disk: removed=0, injected=1). Both guards went red, and the seed-locale failure named the actual field:

FAIL test/seed-locale.test.ts > every display string a Chinese boot writes carries a Han character
AssertionError: expected [ [ 'duly_log_entry.subject', …(1) ] ] to deeply equal []
FAIL test/demo-locale.test.ts > translates every one of them into something actually Chinese

Ablation B — an English fixture line reworded (Chased the carrier for three missing transfer notes…for the missing transfer notes; confirmed on disk: original=0, reworded=1). Red three ways, which is the item-6 pin working:

Error: demo fixture: no zh-CN translation for "Chased the carrier for the missing transfer notes"
FAIL > every string the fixture asks for has a zh-CN translation
FAIL > and every translation is asked for by the fixture

No rebuild leg was needed: vitest reads the TypeScript source, and both suites point artifactPath at a path that cannot exist so a stale dist/objectstack.json cannot be what is under test.

Live boot evidence — pnpm demo:zh, clean database, port 4321

 Duly demo — two steps, then the server is yours.
1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready, renamed to 演示管理员 for this locale.
2/2 starting Duly with the demo organisation loaded…
POST /api/v1/auth/sign-in/email → 200, name 演示管理员
GET /api/v1/data/duly_task?limit=5 → 200, total 183
- 班前安全讲话记录—河畔厂区 | open | 2026-09 | late_after 2026-09-13
- 产线安全巡查—B线 | done | 2026-05 | late_after 2026-05-05
- 产线安全巡查—河畔厂区 | open | 2026-08 | late_after 2026-08-05
- 交接班记录—A线 | done | 2026-W33 | late_after 2026-08-10
- 每月厂区运行手记 | done | 2026-03 | late_after 2026-03-30
units 安岭集团/ARD · 北门厂区/NGP · 河畔厂区/RVP · 集团总部/CEN
北门生产部/NGP-OPS · 北门质量部/NGP-QA
positions 厂区合规专员 · 班组长 · 质量技术员
catalog 承包商入场培训复训 / 为仍持有通行证的每一位承包商重做一次入场培训… / 《厂区安全标准 SS-15》第3条
note 夜班还有两人没参加,另安排一次补讲。
skip 整个周期 A 线都在大修停机——没有产线可巡。
log 与监管联络人的例行通话 | category meeting | visibility manager

Row counts, both locales, both boots — identical throughout:

objectcount
sys_user13
sys_business_unit6
sys_business_unit_member12
duly_catalog_item20
duly_duty31
duly_assignment2
duly_task183
duly_log_entry15

And pnpm demo on a clean database is unchanged — 183 tasks, 13 users, Dev Admin, English subjects, with the readable position codes item 3 asked for. That control was re-taken: an earlier reading was contaminated by a stray server (setsid re-parents, so $! is not the process-group leader and the first teardown missed four servers). All servers were killed by process group; none survive.

Notes for review

  • The card says "21 catalog items"; the fixture has 20 (the 21 is samples/catalog-items.csv, which carries one extra row, Daily line start-up check). All 20 are translated.
  • The card names test/seed.test.ts for the per-locale boot. It is in test/seed-locale.test.ts instead: seed.test.ts boots one kernel from static imports, and a second locale needs vi.resetModules() plus dynamic imports, so a separate file is both cleaner and parallelisable. seed.test.ts is untouched and still green.
  • Nothing was changed in objectstack.config.ts, the objects, the views or the translations bundle.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p

The Chinese UI has been done for a while — nav, columns, statuses, progress
phrases and form help all render in zh-CN. Every RECORD underneath it was
still English, so a Chinese evaluator saw a Chinese frame around English
content, which reads as "not localised" however good the chrome is.
`DULY_DEMO_LOCALE` (unset = `en`, `zh-CN`) now chooses the fixture's language,
read at compile time exactly as `DULY_DEMO_SEED` is and for the same reason —
the seed is baked into `dist/objectstack.json`. `pnpm demo:zh` is `pnpm demo`
with it set, and `scripts/demo.mjs` passes it to BOTH boots.
One fixture, two languages, not two fixtures. The arrays stay authored in
English and are mapped through `t()` once per section; `src/data/demo-zh.ts`
holds every human-readable string keyed by its English original. That keeps
one org chart, one history planner and one set of invariants — a parallel
zh fixture would drift, and a Chinese demo with 19 catalog items where the
English one has 20 is not a bug anybody spots on a screen. It also makes "is
it translated?" a set comparison a test can make, in both directions.
Machine values are deliberately untouched: unit codes, period keys, select
values, timezones and frequencies are identical in both locales. Mailboxes
stay ASCII pinyin on the reserved `.example` domain while display names are
Chinese.
Position codes are now readable in both languages — `Plant compliance
officer` / `厂区合规专员` rather than `plant_compliance_officer`. The column
is free text the app never parsed; it was only ever being read by people.
The admin persona is MEASURED, not assumed (`@objectstack/*` 17.2.0): the
priming boot PATCHes `sys_user.name` to `演示管理员` before the seed runs,
signing in with the same credentials afterwards still returns 200, and the
seed replays as a no-op against the renamed row. It fails loudly if the
rename does not land, because the alternative is a fourteenth user owning
the demo account's work and four empty screens on first boot.
Tests: `test/seed-locale.test.ts` boots a kernel per locale and asserts equal
row counts, equal status/caliber/period distributions, a Han character in
every zh-CN display string, ASCII mailboxes, invented-only references, and
the English names and subjects pinned literally. `test/demo-locale.test.ts`
pins the switch and holds the dictionary to the fixture in both directions —
which is what stops this card quietly rewording the English demo.
`test/demo-script.test.ts` holds `scripts/demo.mjs` to the fixture's `ADMIN`.
Closes#117
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 2, 2026 10:39
@os-warren
os-warren merged commit 5c5a8a0 into mainSep 2, 2026
1 check passed
Sign up for freeto 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.

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh)

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh) - #121

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-117-zh-demo-seed
Sep 2, 2026
Merged

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh)#121
os-warren merged 1 commit into
mainfrom
claude/issue-117-zh-demo-seed

Conversation

@os-warren

@os-warrenos-warren commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Closes#117

The Chinese UI has been done for a while — nav, columns, statuses, progress phrases and form help all render in zh-CN. Every record underneath it was still English, so a Chinese evaluator saw a Chinese frame around English content. pnpm demo:zh now opens the same worked example with 安岭集团, its people, its duty catalog and six months of its history all in Chinese.

What this is, mechanically

DULY_DEMO_LOCALE (unset = en, zh-CN) chooses the fixture's language, read at compile time exactly as DULY_DEMO_SEED is and for the same reason — the seed is baked into dist/objectstack.json, so both are compile-time decisions and both boots pass --compile. An unrecognised spelling (zh_CN, zh-Hans) is a hard error, not a silent fallback to English: a fallback brings the demo up in English, working perfectly, and nobody finds out until it is on a screen in front of a customer.

One fixture in two languages, not two fixtures. The arrays stay authored in English and are mapped through t() once per section; src/data/demo-zh.ts holds every human-readable string keyed by its English original. A parallel demo-org.zh.ts / demo-catalog.zh.ts would duplicate the structure as well as the prose — two catalogs, two cadence sets, two review-state distributions — and they would drift invisibly. A Chinese demo with 19 catalog items where the English one has 20 is not a bug anybody spots on a screen.

Keying on the English string also reduces "is it translated?" to a set comparison a test can make in both directions, which is what pins item 6: rewording an English line orphans its dictionary entry and goes red.

Machine values are deliberately untouched — unit codes, period_keys, select values, timezones, frequencies are identical in both locales, and the zh-CN translations bundle is what renders the select values. Mailboxes stay ASCII pinyin (chen.zhiyuan@ardenline.example) while display names are Chinese: sys_user.email is an identifier that gets typed, pasted and matched on.

Item 5 — the admin persona, measured

The card said to measure this rather than assume it, and the measurement is the reason the rename ships.

Measured on @objectstack/* 17.2.0, live, on a clean database with nothing else holding it:

StepResult
PATCH /api/v1/data/sys_user/{id} with {"name":"演示管理员"} as the signed-in admin200, name reads back 演示管理员
Sign in again, same credentials200, user 演示管理员, email admin@objectos.ai
pnpm demo:zh on a fresh DB1/2 done — admin account ready, renamed to 演示管理员 for this locale.
Seed's natural-key match13 sys_user rows, not 14 — the seed matched the renamed row and skipped it
The demo account's own screens26 tasks owned by the signed-in admin, 6 of them open/in_progress → My week is populated
Restart on the same database1/2 done — admin account ready. (no rename — idempotent), sign-in 200, every row count identical

So the rename lands, sign-in survives it, and the replay is a no-op. Dev Admin stays in English.

The failure this guards is specific and silent, which is why scripts/demo.mjsfails loudly rather than warning: if the rename did not happen, the seed would find no 演示管理员 row, insert a fourteenth user, and hand the demo account's duties, tasks, assignment and work log to a person nobody can log in as — leaving My week, My duties, Sent by me and Work log empty on the first screen an evaluator opens, with the seed reporting success.

The rename can only happen in the priming boot: plugin-auth's maybeSeedDevAdmin does not take a name, and the seed's sys_user row for the admin carries its natural key and nothing else on purpose (anything more turns the loader's no-op skip into an UPDATE against a live credential-bearing account).

Item 3 — readable position codes

plant_compliance_officerPlant compliance officer / 厂区合规专员, and the two siblings. The column is free text the app never parsed — it was only ever being read by people, who do not write their own job title that way in either language. Still matched verbatim by the sync and apply actions, so Plant compliance officer and plant compliance officer remain two different positions.

No test was keyed on POSITIONS; the ones using plant_compliance_officer (catalog-instantiate, catalog-action-placement, catalog-engine-facade) supply their own literals and are untouched. The sync action's placeholder was deliberately not changed — it is mirrored in the zh-CN translations bundle, which this card excludes — filed as #120 along with samples/catalog-items.csv, which still carries the old spelling.

Tests

pnpm test 868 passed / 35 files (baseline on origin/main was 833 / 32), all four gates green at e05dcfb.

  • test/seed-locale.test.ts boots a real kernel per locale with the declarative seeder running, and reads the rows back — not the fixture arrays, because half of these strings are natural keys and the failure a translation causes is a reference that resolves to nothing, not prose that reads oddly. Asserts: two independent databases (no Chinese duty in the English DB and vice versa — without which "same counts" would be trivially true of a doubly-seeded store); equal row counts across all 8 objects; equal status / caliber / review-state / frequency / period_key distributions; a Han character in every one of the 395 zh-CN display strings, and none in the English ones; ASCII mailboxes on the reserved domain; every catalog reference an invented internal document (《…》第N条, and never GB/ISO/国标); and the English unit, user, catalog, position, duty, assignment and log names pinned literally — not toMatchSnapshot(), which -u updates without anybody reading the diff.
  • test/demo-locale.test.ts pins the switch (accepted spellings, and that zh_CN throws rather than answering in English) and holds the dictionary to the fixture in both directions.
  • test/demo-script.test.ts holds scripts/demo.mjs's admin name to the fixture's ADMIN. They are two copies of one fact — plain .mjs that runs before anything compiles, and TypeScript baked into the artifact — and they cannot be one constant.

Both new guards were reverse-verified

A guard that has never been observed failing is indistinguishable from one that cannot fail, so each was ablated on a committed tree, with the mutation confirmed on disk by grepping for the removed and the injected text, and restored by an EXIT INT TERM trap.

Ablation A — one translation replaced by its English original (向承运方催了三张缺失的转移联单 → the English string; confirmed on disk: removed=0, injected=1). Both guards went red, and the seed-locale failure named the actual field:

FAIL test/seed-locale.test.ts > every display string a Chinese boot writes carries a Han character
AssertionError: expected [ [ 'duly_log_entry.subject', …(1) ] ] to deeply equal []
FAIL test/demo-locale.test.ts > translates every one of them into something actually Chinese

Ablation B — an English fixture line reworded (Chased the carrier for three missing transfer notes…for the missing transfer notes; confirmed on disk: original=0, reworded=1). Red three ways, which is the item-6 pin working:

Error: demo fixture: no zh-CN translation for "Chased the carrier for the missing transfer notes"
FAIL > every string the fixture asks for has a zh-CN translation
FAIL > and every translation is asked for by the fixture

No rebuild leg was needed: vitest reads the TypeScript source, and both suites point artifactPath at a path that cannot exist so a stale dist/objectstack.json cannot be what is under test.

Live boot evidence — pnpm demo:zh, clean database, port 4321

 Duly demo — two steps, then the server is yours.
1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready, renamed to 演示管理员 for this locale.
2/2 starting Duly with the demo organisation loaded…
POST /api/v1/auth/sign-in/email → 200, name 演示管理员
GET /api/v1/data/duly_task?limit=5 → 200, total 183
- 班前安全讲话记录—河畔厂区 | open | 2026-09 | late_after 2026-09-13
- 产线安全巡查—B线 | done | 2026-05 | late_after 2026-05-05
- 产线安全巡查—河畔厂区 | open | 2026-08 | late_after 2026-08-05
- 交接班记录—A线 | done | 2026-W33 | late_after 2026-08-10
- 每月厂区运行手记 | done | 2026-03 | late_after 2026-03-30
units 安岭集团/ARD · 北门厂区/NGP · 河畔厂区/RVP · 集团总部/CEN
北门生产部/NGP-OPS · 北门质量部/NGP-QA
positions 厂区合规专员 · 班组长 · 质量技术员
catalog 承包商入场培训复训 / 为仍持有通行证的每一位承包商重做一次入场培训… / 《厂区安全标准 SS-15》第3条
note 夜班还有两人没参加,另安排一次补讲。
skip 整个周期 A 线都在大修停机——没有产线可巡。
log 与监管联络人的例行通话 | category meeting | visibility manager

Row counts, both locales, both boots — identical throughout:

objectcount
sys_user13
sys_business_unit6
sys_business_unit_member12
duly_catalog_item20
duly_duty31
duly_assignment2
duly_task183
duly_log_entry15

And pnpm demo on a clean database is unchanged — 183 tasks, 13 users, Dev Admin, English subjects, with the readable position codes item 3 asked for. That control was re-taken: an earlier reading was contaminated by a stray server (setsid re-parents, so $! is not the process-group leader and the first teardown missed four servers). All servers were killed by process group; none survive.

Notes for review

  • The card says "21 catalog items"; the fixture has 20 (the 21 is samples/catalog-items.csv, which carries one extra row, Daily line start-up check). All 20 are translated.
  • The card names test/seed.test.ts for the per-locale boot. It is in test/seed-locale.test.ts instead: seed.test.ts boots one kernel from static imports, and a second locale needs vi.resetModules() plus dynamic imports, so a separate file is both cleaner and parallelisable. seed.test.ts is untouched and still green.
  • Nothing was changed in objectstack.config.ts, the objects, the views or the translations bundle.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p

The Chinese UI has been done for a while — nav, columns, statuses, progress
phrases and form help all render in zh-CN. Every RECORD underneath it was
still English, so a Chinese evaluator saw a Chinese frame around English
content, which reads as "not localised" however good the chrome is.
`DULY_DEMO_LOCALE` (unset = `en`, `zh-CN`) now chooses the fixture's language,
read at compile time exactly as `DULY_DEMO_SEED` is and for the same reason —
the seed is baked into `dist/objectstack.json`. `pnpm demo:zh` is `pnpm demo`
with it set, and `scripts/demo.mjs` passes it to BOTH boots.
One fixture, two languages, not two fixtures. The arrays stay authored in
English and are mapped through `t()` once per section; `src/data/demo-zh.ts`
holds every human-readable string keyed by its English original. That keeps
one org chart, one history planner and one set of invariants — a parallel
zh fixture would drift, and a Chinese demo with 19 catalog items where the
English one has 20 is not a bug anybody spots on a screen. It also makes "is
it translated?" a set comparison a test can make, in both directions.
Machine values are deliberately untouched: unit codes, period keys, select
values, timezones and frequencies are identical in both locales. Mailboxes
stay ASCII pinyin on the reserved `.example` domain while display names are
Chinese.
Position codes are now readable in both languages — `Plant compliance
officer` / `厂区合规专员` rather than `plant_compliance_officer`. The column
is free text the app never parsed; it was only ever being read by people.
The admin persona is MEASURED, not assumed (`@objectstack/*` 17.2.0): the
priming boot PATCHes `sys_user.name` to `演示管理员` before the seed runs,
signing in with the same credentials afterwards still returns 200, and the
seed replays as a no-op against the renamed row. It fails loudly if the
rename does not land, because the alternative is a fourteenth user owning
the demo account's work and four empty screens on first boot.
Tests: `test/seed-locale.test.ts` boots a kernel per locale and asserts equal
row counts, equal status/caliber/period distributions, a Han character in
every zh-CN display string, ASCII mailboxes, invented-only references, and
the English names and subjects pinned literally. `test/demo-locale.test.ts`
pins the switch and holds the dictionary to the fixture in both directions —
which is what stops this card quietly rewording the English demo.
`test/demo-script.test.ts` holds `scripts/demo.mjs` to the fixture's `ADMIN`.
Closes#117
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 2, 2026 10:39
@os-warren
os-warren merged commit 5c5a8a0 into mainSep 2, 2026
1 check passed
Sign up for freeto 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.

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh)

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh) - #121

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-117-zh-demo-seed
Sep 2, 2026
Merged

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh)#121
os-warren merged 1 commit into
mainfrom
claude/issue-117-zh-demo-seed

Conversation

@os-warren

@os-warrenos-warren commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Closes#117

The Chinese UI has been done for a while — nav, columns, statuses, progress phrases and form help all render in zh-CN. Every record underneath it was still English, so a Chinese evaluator saw a Chinese frame around English content. pnpm demo:zh now opens the same worked example with 安岭集团, its people, its duty catalog and six months of its history all in Chinese.

What this is, mechanically

DULY_DEMO_LOCALE (unset = en, zh-CN) chooses the fixture's language, read at compile time exactly as DULY_DEMO_SEED is and for the same reason — the seed is baked into dist/objectstack.json, so both are compile-time decisions and both boots pass --compile. An unrecognised spelling (zh_CN, zh-Hans) is a hard error, not a silent fallback to English: a fallback brings the demo up in English, working perfectly, and nobody finds out until it is on a screen in front of a customer.

One fixture in two languages, not two fixtures. The arrays stay authored in English and are mapped through t() once per section; src/data/demo-zh.ts holds every human-readable string keyed by its English original. A parallel demo-org.zh.ts / demo-catalog.zh.ts would duplicate the structure as well as the prose — two catalogs, two cadence sets, two review-state distributions — and they would drift invisibly. A Chinese demo with 19 catalog items where the English one has 20 is not a bug anybody spots on a screen.

Keying on the English string also reduces "is it translated?" to a set comparison a test can make in both directions, which is what pins item 6: rewording an English line orphans its dictionary entry and goes red.

Machine values are deliberately untouched — unit codes, period_keys, select values, timezones, frequencies are identical in both locales, and the zh-CN translations bundle is what renders the select values. Mailboxes stay ASCII pinyin (chen.zhiyuan@ardenline.example) while display names are Chinese: sys_user.email is an identifier that gets typed, pasted and matched on.

Item 5 — the admin persona, measured

The card said to measure this rather than assume it, and the measurement is the reason the rename ships.

Measured on @objectstack/* 17.2.0, live, on a clean database with nothing else holding it:

StepResult
PATCH /api/v1/data/sys_user/{id} with {"name":"演示管理员"} as the signed-in admin200, name reads back 演示管理员
Sign in again, same credentials200, user 演示管理员, email admin@objectos.ai
pnpm demo:zh on a fresh DB1/2 done — admin account ready, renamed to 演示管理员 for this locale.
Seed's natural-key match13 sys_user rows, not 14 — the seed matched the renamed row and skipped it
The demo account's own screens26 tasks owned by the signed-in admin, 6 of them open/in_progress → My week is populated
Restart on the same database1/2 done — admin account ready. (no rename — idempotent), sign-in 200, every row count identical

So the rename lands, sign-in survives it, and the replay is a no-op. Dev Admin stays in English.

The failure this guards is specific and silent, which is why scripts/demo.mjsfails loudly rather than warning: if the rename did not happen, the seed would find no 演示管理员 row, insert a fourteenth user, and hand the demo account's duties, tasks, assignment and work log to a person nobody can log in as — leaving My week, My duties, Sent by me and Work log empty on the first screen an evaluator opens, with the seed reporting success.

The rename can only happen in the priming boot: plugin-auth's maybeSeedDevAdmin does not take a name, and the seed's sys_user row for the admin carries its natural key and nothing else on purpose (anything more turns the loader's no-op skip into an UPDATE against a live credential-bearing account).

Item 3 — readable position codes

plant_compliance_officerPlant compliance officer / 厂区合规专员, and the two siblings. The column is free text the app never parsed — it was only ever being read by people, who do not write their own job title that way in either language. Still matched verbatim by the sync and apply actions, so Plant compliance officer and plant compliance officer remain two different positions.

No test was keyed on POSITIONS; the ones using plant_compliance_officer (catalog-instantiate, catalog-action-placement, catalog-engine-facade) supply their own literals and are untouched. The sync action's placeholder was deliberately not changed — it is mirrored in the zh-CN translations bundle, which this card excludes — filed as #120 along with samples/catalog-items.csv, which still carries the old spelling.

Tests

pnpm test 868 passed / 35 files (baseline on origin/main was 833 / 32), all four gates green at e05dcfb.

  • test/seed-locale.test.ts boots a real kernel per locale with the declarative seeder running, and reads the rows back — not the fixture arrays, because half of these strings are natural keys and the failure a translation causes is a reference that resolves to nothing, not prose that reads oddly. Asserts: two independent databases (no Chinese duty in the English DB and vice versa — without which "same counts" would be trivially true of a doubly-seeded store); equal row counts across all 8 objects; equal status / caliber / review-state / frequency / period_key distributions; a Han character in every one of the 395 zh-CN display strings, and none in the English ones; ASCII mailboxes on the reserved domain; every catalog reference an invented internal document (《…》第N条, and never GB/ISO/国标); and the English unit, user, catalog, position, duty, assignment and log names pinned literally — not toMatchSnapshot(), which -u updates without anybody reading the diff.
  • test/demo-locale.test.ts pins the switch (accepted spellings, and that zh_CN throws rather than answering in English) and holds the dictionary to the fixture in both directions.
  • test/demo-script.test.ts holds scripts/demo.mjs's admin name to the fixture's ADMIN. They are two copies of one fact — plain .mjs that runs before anything compiles, and TypeScript baked into the artifact — and they cannot be one constant.

Both new guards were reverse-verified

A guard that has never been observed failing is indistinguishable from one that cannot fail, so each was ablated on a committed tree, with the mutation confirmed on disk by grepping for the removed and the injected text, and restored by an EXIT INT TERM trap.

Ablation A — one translation replaced by its English original (向承运方催了三张缺失的转移联单 → the English string; confirmed on disk: removed=0, injected=1). Both guards went red, and the seed-locale failure named the actual field:

FAIL test/seed-locale.test.ts > every display string a Chinese boot writes carries a Han character
AssertionError: expected [ [ 'duly_log_entry.subject', …(1) ] ] to deeply equal []
FAIL test/demo-locale.test.ts > translates every one of them into something actually Chinese

Ablation B — an English fixture line reworded (Chased the carrier for three missing transfer notes…for the missing transfer notes; confirmed on disk: original=0, reworded=1). Red three ways, which is the item-6 pin working:

Error: demo fixture: no zh-CN translation for "Chased the carrier for the missing transfer notes"
FAIL > every string the fixture asks for has a zh-CN translation
FAIL > and every translation is asked for by the fixture

No rebuild leg was needed: vitest reads the TypeScript source, and both suites point artifactPath at a path that cannot exist so a stale dist/objectstack.json cannot be what is under test.

Live boot evidence — pnpm demo:zh, clean database, port 4321

 Duly demo — two steps, then the server is yours.
1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready, renamed to 演示管理员 for this locale.
2/2 starting Duly with the demo organisation loaded…
POST /api/v1/auth/sign-in/email → 200, name 演示管理员
GET /api/v1/data/duly_task?limit=5 → 200, total 183
- 班前安全讲话记录—河畔厂区 | open | 2026-09 | late_after 2026-09-13
- 产线安全巡查—B线 | done | 2026-05 | late_after 2026-05-05
- 产线安全巡查—河畔厂区 | open | 2026-08 | late_after 2026-08-05
- 交接班记录—A线 | done | 2026-W33 | late_after 2026-08-10
- 每月厂区运行手记 | done | 2026-03 | late_after 2026-03-30
units 安岭集团/ARD · 北门厂区/NGP · 河畔厂区/RVP · 集团总部/CEN
北门生产部/NGP-OPS · 北门质量部/NGP-QA
positions 厂区合规专员 · 班组长 · 质量技术员
catalog 承包商入场培训复训 / 为仍持有通行证的每一位承包商重做一次入场培训… / 《厂区安全标准 SS-15》第3条
note 夜班还有两人没参加,另安排一次补讲。
skip 整个周期 A 线都在大修停机——没有产线可巡。
log 与监管联络人的例行通话 | category meeting | visibility manager

Row counts, both locales, both boots — identical throughout:

objectcount
sys_user13
sys_business_unit6
sys_business_unit_member12
duly_catalog_item20
duly_duty31
duly_assignment2
duly_task183
duly_log_entry15

And pnpm demo on a clean database is unchanged — 183 tasks, 13 users, Dev Admin, English subjects, with the readable position codes item 3 asked for. That control was re-taken: an earlier reading was contaminated by a stray server (setsid re-parents, so $! is not the process-group leader and the first teardown missed four servers). All servers were killed by process group; none survive.

Notes for review

  • The card says "21 catalog items"; the fixture has 20 (the 21 is samples/catalog-items.csv, which carries one extra row, Daily line start-up check). All 20 are translated.
  • The card names test/seed.test.ts for the per-locale boot. It is in test/seed-locale.test.ts instead: seed.test.ts boots one kernel from static imports, and a second locale needs vi.resetModules() plus dynamic imports, so a separate file is both cleaner and parallelisable. seed.test.ts is untouched and still green.
  • Nothing was changed in objectstack.config.ts, the objects, the views or the translations bundle.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p

The Chinese UI has been done for a while — nav, columns, statuses, progress
phrases and form help all render in zh-CN. Every RECORD underneath it was
still English, so a Chinese evaluator saw a Chinese frame around English
content, which reads as "not localised" however good the chrome is.
`DULY_DEMO_LOCALE` (unset = `en`, `zh-CN`) now chooses the fixture's language,
read at compile time exactly as `DULY_DEMO_SEED` is and for the same reason —
the seed is baked into `dist/objectstack.json`. `pnpm demo:zh` is `pnpm demo`
with it set, and `scripts/demo.mjs` passes it to BOTH boots.
One fixture, two languages, not two fixtures. The arrays stay authored in
English and are mapped through `t()` once per section; `src/data/demo-zh.ts`
holds every human-readable string keyed by its English original. That keeps
one org chart, one history planner and one set of invariants — a parallel
zh fixture would drift, and a Chinese demo with 19 catalog items where the
English one has 20 is not a bug anybody spots on a screen. It also makes "is
it translated?" a set comparison a test can make, in both directions.
Machine values are deliberately untouched: unit codes, period keys, select
values, timezones and frequencies are identical in both locales. Mailboxes
stay ASCII pinyin on the reserved `.example` domain while display names are
Chinese.
Position codes are now readable in both languages — `Plant compliance
officer` / `厂区合规专员` rather than `plant_compliance_officer`. The column
is free text the app never parsed; it was only ever being read by people.
The admin persona is MEASURED, not assumed (`@objectstack/*` 17.2.0): the
priming boot PATCHes `sys_user.name` to `演示管理员` before the seed runs,
signing in with the same credentials afterwards still returns 200, and the
seed replays as a no-op against the renamed row. It fails loudly if the
rename does not land, because the alternative is a fourteenth user owning
the demo account's work and four empty screens on first boot.
Tests: `test/seed-locale.test.ts` boots a kernel per locale and asserts equal
row counts, equal status/caliber/period distributions, a Han character in
every zh-CN display string, ASCII mailboxes, invented-only references, and
the English names and subjects pinned literally. `test/demo-locale.test.ts`
pins the switch and holds the dictionary to the fixture in both directions —
which is what stops this card quietly rewording the English demo.
`test/demo-script.test.ts` holds `scripts/demo.mjs` to the fixture's `ADMIN`.
Closes#117
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 2, 2026 10:39
@os-warren
os-warren merged commit 5c5a8a0 into mainSep 2, 2026
1 check passed
Sign up for freeto 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.

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh)

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh) - #121

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-117-zh-demo-seed
Sep 2, 2026
Merged

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh)#121
os-warren merged 1 commit into
mainfrom
claude/issue-117-zh-demo-seed

Conversation

@os-warren

@os-warrenos-warren commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Closes#117

The Chinese UI has been done for a while — nav, columns, statuses, progress phrases and form help all render in zh-CN. Every record underneath it was still English, so a Chinese evaluator saw a Chinese frame around English content. pnpm demo:zh now opens the same worked example with 安岭集团, its people, its duty catalog and six months of its history all in Chinese.

What this is, mechanically

DULY_DEMO_LOCALE (unset = en, zh-CN) chooses the fixture's language, read at compile time exactly as DULY_DEMO_SEED is and for the same reason — the seed is baked into dist/objectstack.json, so both are compile-time decisions and both boots pass --compile. An unrecognised spelling (zh_CN, zh-Hans) is a hard error, not a silent fallback to English: a fallback brings the demo up in English, working perfectly, and nobody finds out until it is on a screen in front of a customer.

One fixture in two languages, not two fixtures. The arrays stay authored in English and are mapped through t() once per section; src/data/demo-zh.ts holds every human-readable string keyed by its English original. A parallel demo-org.zh.ts / demo-catalog.zh.ts would duplicate the structure as well as the prose — two catalogs, two cadence sets, two review-state distributions — and they would drift invisibly. A Chinese demo with 19 catalog items where the English one has 20 is not a bug anybody spots on a screen.

Keying on the English string also reduces "is it translated?" to a set comparison a test can make in both directions, which is what pins item 6: rewording an English line orphans its dictionary entry and goes red.

Machine values are deliberately untouched — unit codes, period_keys, select values, timezones, frequencies are identical in both locales, and the zh-CN translations bundle is what renders the select values. Mailboxes stay ASCII pinyin (chen.zhiyuan@ardenline.example) while display names are Chinese: sys_user.email is an identifier that gets typed, pasted and matched on.

Item 5 — the admin persona, measured

The card said to measure this rather than assume it, and the measurement is the reason the rename ships.

Measured on @objectstack/* 17.2.0, live, on a clean database with nothing else holding it:

StepResult
PATCH /api/v1/data/sys_user/{id} with {"name":"演示管理员"} as the signed-in admin200, name reads back 演示管理员
Sign in again, same credentials200, user 演示管理员, email admin@objectos.ai
pnpm demo:zh on a fresh DB1/2 done — admin account ready, renamed to 演示管理员 for this locale.
Seed's natural-key match13 sys_user rows, not 14 — the seed matched the renamed row and skipped it
The demo account's own screens26 tasks owned by the signed-in admin, 6 of them open/in_progress → My week is populated
Restart on the same database1/2 done — admin account ready. (no rename — idempotent), sign-in 200, every row count identical

So the rename lands, sign-in survives it, and the replay is a no-op. Dev Admin stays in English.

The failure this guards is specific and silent, which is why scripts/demo.mjsfails loudly rather than warning: if the rename did not happen, the seed would find no 演示管理员 row, insert a fourteenth user, and hand the demo account's duties, tasks, assignment and work log to a person nobody can log in as — leaving My week, My duties, Sent by me and Work log empty on the first screen an evaluator opens, with the seed reporting success.

The rename can only happen in the priming boot: plugin-auth's maybeSeedDevAdmin does not take a name, and the seed's sys_user row for the admin carries its natural key and nothing else on purpose (anything more turns the loader's no-op skip into an UPDATE against a live credential-bearing account).

Item 3 — readable position codes

plant_compliance_officerPlant compliance officer / 厂区合规专员, and the two siblings. The column is free text the app never parsed — it was only ever being read by people, who do not write their own job title that way in either language. Still matched verbatim by the sync and apply actions, so Plant compliance officer and plant compliance officer remain two different positions.

No test was keyed on POSITIONS; the ones using plant_compliance_officer (catalog-instantiate, catalog-action-placement, catalog-engine-facade) supply their own literals and are untouched. The sync action's placeholder was deliberately not changed — it is mirrored in the zh-CN translations bundle, which this card excludes — filed as #120 along with samples/catalog-items.csv, which still carries the old spelling.

Tests

pnpm test 868 passed / 35 files (baseline on origin/main was 833 / 32), all four gates green at e05dcfb.

  • test/seed-locale.test.ts boots a real kernel per locale with the declarative seeder running, and reads the rows back — not the fixture arrays, because half of these strings are natural keys and the failure a translation causes is a reference that resolves to nothing, not prose that reads oddly. Asserts: two independent databases (no Chinese duty in the English DB and vice versa — without which "same counts" would be trivially true of a doubly-seeded store); equal row counts across all 8 objects; equal status / caliber / review-state / frequency / period_key distributions; a Han character in every one of the 395 zh-CN display strings, and none in the English ones; ASCII mailboxes on the reserved domain; every catalog reference an invented internal document (《…》第N条, and never GB/ISO/国标); and the English unit, user, catalog, position, duty, assignment and log names pinned literally — not toMatchSnapshot(), which -u updates without anybody reading the diff.
  • test/demo-locale.test.ts pins the switch (accepted spellings, and that zh_CN throws rather than answering in English) and holds the dictionary to the fixture in both directions.
  • test/demo-script.test.ts holds scripts/demo.mjs's admin name to the fixture's ADMIN. They are two copies of one fact — plain .mjs that runs before anything compiles, and TypeScript baked into the artifact — and they cannot be one constant.

Both new guards were reverse-verified

A guard that has never been observed failing is indistinguishable from one that cannot fail, so each was ablated on a committed tree, with the mutation confirmed on disk by grepping for the removed and the injected text, and restored by an EXIT INT TERM trap.

Ablation A — one translation replaced by its English original (向承运方催了三张缺失的转移联单 → the English string; confirmed on disk: removed=0, injected=1). Both guards went red, and the seed-locale failure named the actual field:

FAIL test/seed-locale.test.ts > every display string a Chinese boot writes carries a Han character
AssertionError: expected [ [ 'duly_log_entry.subject', …(1) ] ] to deeply equal []
FAIL test/demo-locale.test.ts > translates every one of them into something actually Chinese

Ablation B — an English fixture line reworded (Chased the carrier for three missing transfer notes…for the missing transfer notes; confirmed on disk: original=0, reworded=1). Red three ways, which is the item-6 pin working:

Error: demo fixture: no zh-CN translation for "Chased the carrier for the missing transfer notes"
FAIL > every string the fixture asks for has a zh-CN translation
FAIL > and every translation is asked for by the fixture

No rebuild leg was needed: vitest reads the TypeScript source, and both suites point artifactPath at a path that cannot exist so a stale dist/objectstack.json cannot be what is under test.

Live boot evidence — pnpm demo:zh, clean database, port 4321

 Duly demo — two steps, then the server is yours.
1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready, renamed to 演示管理员 for this locale.
2/2 starting Duly with the demo organisation loaded…
POST /api/v1/auth/sign-in/email → 200, name 演示管理员
GET /api/v1/data/duly_task?limit=5 → 200, total 183
- 班前安全讲话记录—河畔厂区 | open | 2026-09 | late_after 2026-09-13
- 产线安全巡查—B线 | done | 2026-05 | late_after 2026-05-05
- 产线安全巡查—河畔厂区 | open | 2026-08 | late_after 2026-08-05
- 交接班记录—A线 | done | 2026-W33 | late_after 2026-08-10
- 每月厂区运行手记 | done | 2026-03 | late_after 2026-03-30
units 安岭集团/ARD · 北门厂区/NGP · 河畔厂区/RVP · 集团总部/CEN
北门生产部/NGP-OPS · 北门质量部/NGP-QA
positions 厂区合规专员 · 班组长 · 质量技术员
catalog 承包商入场培训复训 / 为仍持有通行证的每一位承包商重做一次入场培训… / 《厂区安全标准 SS-15》第3条
note 夜班还有两人没参加,另安排一次补讲。
skip 整个周期 A 线都在大修停机——没有产线可巡。
log 与监管联络人的例行通话 | category meeting | visibility manager

Row counts, both locales, both boots — identical throughout:

objectcount
sys_user13
sys_business_unit6
sys_business_unit_member12
duly_catalog_item20
duly_duty31
duly_assignment2
duly_task183
duly_log_entry15

And pnpm demo on a clean database is unchanged — 183 tasks, 13 users, Dev Admin, English subjects, with the readable position codes item 3 asked for. That control was re-taken: an earlier reading was contaminated by a stray server (setsid re-parents, so $! is not the process-group leader and the first teardown missed four servers). All servers were killed by process group; none survive.

Notes for review

  • The card says "21 catalog items"; the fixture has 20 (the 21 is samples/catalog-items.csv, which carries one extra row, Daily line start-up check). All 20 are translated.
  • The card names test/seed.test.ts for the per-locale boot. It is in test/seed-locale.test.ts instead: seed.test.ts boots one kernel from static imports, and a second locale needs vi.resetModules() plus dynamic imports, so a separate file is both cleaner and parallelisable. seed.test.ts is untouched and still green.
  • Nothing was changed in objectstack.config.ts, the objects, the views or the translations bundle.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p

The Chinese UI has been done for a while — nav, columns, statuses, progress
phrases and form help all render in zh-CN. Every RECORD underneath it was
still English, so a Chinese evaluator saw a Chinese frame around English
content, which reads as "not localised" however good the chrome is.
`DULY_DEMO_LOCALE` (unset = `en`, `zh-CN`) now chooses the fixture's language,
read at compile time exactly as `DULY_DEMO_SEED` is and for the same reason —
the seed is baked into `dist/objectstack.json`. `pnpm demo:zh` is `pnpm demo`
with it set, and `scripts/demo.mjs` passes it to BOTH boots.
One fixture, two languages, not two fixtures. The arrays stay authored in
English and are mapped through `t()` once per section; `src/data/demo-zh.ts`
holds every human-readable string keyed by its English original. That keeps
one org chart, one history planner and one set of invariants — a parallel
zh fixture would drift, and a Chinese demo with 19 catalog items where the
English one has 20 is not a bug anybody spots on a screen. It also makes "is
it translated?" a set comparison a test can make, in both directions.
Machine values are deliberately untouched: unit codes, period keys, select
values, timezones and frequencies are identical in both locales. Mailboxes
stay ASCII pinyin on the reserved `.example` domain while display names are
Chinese.
Position codes are now readable in both languages — `Plant compliance
officer` / `厂区合规专员` rather than `plant_compliance_officer`. The column
is free text the app never parsed; it was only ever being read by people.
The admin persona is MEASURED, not assumed (`@objectstack/*` 17.2.0): the
priming boot PATCHes `sys_user.name` to `演示管理员` before the seed runs,
signing in with the same credentials afterwards still returns 200, and the
seed replays as a no-op against the renamed row. It fails loudly if the
rename does not land, because the alternative is a fourteenth user owning
the demo account's work and four empty screens on first boot.
Tests: `test/seed-locale.test.ts` boots a kernel per locale and asserts equal
row counts, equal status/caliber/period distributions, a Han character in
every zh-CN display string, ASCII mailboxes, invented-only references, and
the English names and subjects pinned literally. `test/demo-locale.test.ts`
pins the switch and holds the dictionary to the fixture in both directions —
which is what stops this card quietly rewording the English demo.
`test/demo-script.test.ts` holds `scripts/demo.mjs` to the fixture's `ADMIN`.
Closes#117
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 2, 2026 10:39
@os-warren
os-warren merged commit 5c5a8a0 into mainSep 2, 2026
1 check passed
Sign up for freeto 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.

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh)

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh) - #121

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-117-zh-demo-seed
Sep 2, 2026
Merged

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh)#121
os-warren merged 1 commit into
mainfrom
claude/issue-117-zh-demo-seed

Conversation

@os-warren

@os-warrenos-warren commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Closes#117

The Chinese UI has been done for a while — nav, columns, statuses, progress phrases and form help all render in zh-CN. Every record underneath it was still English, so a Chinese evaluator saw a Chinese frame around English content. pnpm demo:zh now opens the same worked example with 安岭集团, its people, its duty catalog and six months of its history all in Chinese.

What this is, mechanically

DULY_DEMO_LOCALE (unset = en, zh-CN) chooses the fixture's language, read at compile time exactly as DULY_DEMO_SEED is and for the same reason — the seed is baked into dist/objectstack.json, so both are compile-time decisions and both boots pass --compile. An unrecognised spelling (zh_CN, zh-Hans) is a hard error, not a silent fallback to English: a fallback brings the demo up in English, working perfectly, and nobody finds out until it is on a screen in front of a customer.

One fixture in two languages, not two fixtures. The arrays stay authored in English and are mapped through t() once per section; src/data/demo-zh.ts holds every human-readable string keyed by its English original. A parallel demo-org.zh.ts / demo-catalog.zh.ts would duplicate the structure as well as the prose — two catalogs, two cadence sets, two review-state distributions — and they would drift invisibly. A Chinese demo with 19 catalog items where the English one has 20 is not a bug anybody spots on a screen.

Keying on the English string also reduces "is it translated?" to a set comparison a test can make in both directions, which is what pins item 6: rewording an English line orphans its dictionary entry and goes red.

Machine values are deliberately untouched — unit codes, period_keys, select values, timezones, frequencies are identical in both locales, and the zh-CN translations bundle is what renders the select values. Mailboxes stay ASCII pinyin (chen.zhiyuan@ardenline.example) while display names are Chinese: sys_user.email is an identifier that gets typed, pasted and matched on.

Item 5 — the admin persona, measured

The card said to measure this rather than assume it, and the measurement is the reason the rename ships.

Measured on @objectstack/* 17.2.0, live, on a clean database with nothing else holding it:

StepResult
PATCH /api/v1/data/sys_user/{id} with {"name":"演示管理员"} as the signed-in admin200, name reads back 演示管理员
Sign in again, same credentials200, user 演示管理员, email admin@objectos.ai
pnpm demo:zh on a fresh DB1/2 done — admin account ready, renamed to 演示管理员 for this locale.
Seed's natural-key match13 sys_user rows, not 14 — the seed matched the renamed row and skipped it
The demo account's own screens26 tasks owned by the signed-in admin, 6 of them open/in_progress → My week is populated
Restart on the same database1/2 done — admin account ready. (no rename — idempotent), sign-in 200, every row count identical

So the rename lands, sign-in survives it, and the replay is a no-op. Dev Admin stays in English.

The failure this guards is specific and silent, which is why scripts/demo.mjsfails loudly rather than warning: if the rename did not happen, the seed would find no 演示管理员 row, insert a fourteenth user, and hand the demo account's duties, tasks, assignment and work log to a person nobody can log in as — leaving My week, My duties, Sent by me and Work log empty on the first screen an evaluator opens, with the seed reporting success.

The rename can only happen in the priming boot: plugin-auth's maybeSeedDevAdmin does not take a name, and the seed's sys_user row for the admin carries its natural key and nothing else on purpose (anything more turns the loader's no-op skip into an UPDATE against a live credential-bearing account).

Item 3 — readable position codes

plant_compliance_officerPlant compliance officer / 厂区合规专员, and the two siblings. The column is free text the app never parsed — it was only ever being read by people, who do not write their own job title that way in either language. Still matched verbatim by the sync and apply actions, so Plant compliance officer and plant compliance officer remain two different positions.

No test was keyed on POSITIONS; the ones using plant_compliance_officer (catalog-instantiate, catalog-action-placement, catalog-engine-facade) supply their own literals and are untouched. The sync action's placeholder was deliberately not changed — it is mirrored in the zh-CN translations bundle, which this card excludes — filed as #120 along with samples/catalog-items.csv, which still carries the old spelling.

Tests

pnpm test 868 passed / 35 files (baseline on origin/main was 833 / 32), all four gates green at e05dcfb.

  • test/seed-locale.test.ts boots a real kernel per locale with the declarative seeder running, and reads the rows back — not the fixture arrays, because half of these strings are natural keys and the failure a translation causes is a reference that resolves to nothing, not prose that reads oddly. Asserts: two independent databases (no Chinese duty in the English DB and vice versa — without which "same counts" would be trivially true of a doubly-seeded store); equal row counts across all 8 objects; equal status / caliber / review-state / frequency / period_key distributions; a Han character in every one of the 395 zh-CN display strings, and none in the English ones; ASCII mailboxes on the reserved domain; every catalog reference an invented internal document (《…》第N条, and never GB/ISO/国标); and the English unit, user, catalog, position, duty, assignment and log names pinned literally — not toMatchSnapshot(), which -u updates without anybody reading the diff.
  • test/demo-locale.test.ts pins the switch (accepted spellings, and that zh_CN throws rather than answering in English) and holds the dictionary to the fixture in both directions.
  • test/demo-script.test.ts holds scripts/demo.mjs's admin name to the fixture's ADMIN. They are two copies of one fact — plain .mjs that runs before anything compiles, and TypeScript baked into the artifact — and they cannot be one constant.

Both new guards were reverse-verified

A guard that has never been observed failing is indistinguishable from one that cannot fail, so each was ablated on a committed tree, with the mutation confirmed on disk by grepping for the removed and the injected text, and restored by an EXIT INT TERM trap.

Ablation A — one translation replaced by its English original (向承运方催了三张缺失的转移联单 → the English string; confirmed on disk: removed=0, injected=1). Both guards went red, and the seed-locale failure named the actual field:

FAIL test/seed-locale.test.ts > every display string a Chinese boot writes carries a Han character
AssertionError: expected [ [ 'duly_log_entry.subject', …(1) ] ] to deeply equal []
FAIL test/demo-locale.test.ts > translates every one of them into something actually Chinese

Ablation B — an English fixture line reworded (Chased the carrier for three missing transfer notes…for the missing transfer notes; confirmed on disk: original=0, reworded=1). Red three ways, which is the item-6 pin working:

Error: demo fixture: no zh-CN translation for "Chased the carrier for the missing transfer notes"
FAIL > every string the fixture asks for has a zh-CN translation
FAIL > and every translation is asked for by the fixture

No rebuild leg was needed: vitest reads the TypeScript source, and both suites point artifactPath at a path that cannot exist so a stale dist/objectstack.json cannot be what is under test.

Live boot evidence — pnpm demo:zh, clean database, port 4321

 Duly demo — two steps, then the server is yours.
1/2 preparing an admin account (quiet, a few seconds)…
1/2 done — admin account ready, renamed to 演示管理员 for this locale.
2/2 starting Duly with the demo organisation loaded…
POST /api/v1/auth/sign-in/email → 200, name 演示管理员
GET /api/v1/data/duly_task?limit=5 → 200, total 183
- 班前安全讲话记录—河畔厂区 | open | 2026-09 | late_after 2026-09-13
- 产线安全巡查—B线 | done | 2026-05 | late_after 2026-05-05
- 产线安全巡查—河畔厂区 | open | 2026-08 | late_after 2026-08-05
- 交接班记录—A线 | done | 2026-W33 | late_after 2026-08-10
- 每月厂区运行手记 | done | 2026-03 | late_after 2026-03-30
units 安岭集团/ARD · 北门厂区/NGP · 河畔厂区/RVP · 集团总部/CEN
北门生产部/NGP-OPS · 北门质量部/NGP-QA
positions 厂区合规专员 · 班组长 · 质量技术员
catalog 承包商入场培训复训 / 为仍持有通行证的每一位承包商重做一次入场培训… / 《厂区安全标准 SS-15》第3条
note 夜班还有两人没参加,另安排一次补讲。
skip 整个周期 A 线都在大修停机——没有产线可巡。
log 与监管联络人的例行通话 | category meeting | visibility manager

Row counts, both locales, both boots — identical throughout:

objectcount
sys_user13
sys_business_unit6
sys_business_unit_member12
duly_catalog_item20
duly_duty31
duly_assignment2
duly_task183
duly_log_entry15

And pnpm demo on a clean database is unchanged — 183 tasks, 13 users, Dev Admin, English subjects, with the readable position codes item 3 asked for. That control was re-taken: an earlier reading was contaminated by a stray server (setsid re-parents, so $! is not the process-group leader and the first teardown missed four servers). All servers were killed by process group; none survive.

Notes for review

  • The card says "21 catalog items"; the fixture has 20 (the 21 is samples/catalog-items.csv, which carries one extra row, Daily line start-up check). All 20 are translated.
  • The card names test/seed.test.ts for the per-locale boot. It is in test/seed-locale.test.ts instead: seed.test.ts boots one kernel from static imports, and a second locale needs vi.resetModules() plus dynamic imports, so a separate file is both cleaner and parallelisable. seed.test.ts is untouched and still green.
  • Nothing was changed in objectstack.config.ts, the objects, the views or the translations bundle.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p

The Chinese UI has been done for a while — nav, columns, statuses, progress
phrases and form help all render in zh-CN. Every RECORD underneath it was
still English, so a Chinese evaluator saw a Chinese frame around English
content, which reads as "not localised" however good the chrome is.
`DULY_DEMO_LOCALE` (unset = `en`, `zh-CN`) now chooses the fixture's language,
read at compile time exactly as `DULY_DEMO_SEED` is and for the same reason —
the seed is baked into `dist/objectstack.json`. `pnpm demo:zh` is `pnpm demo`
with it set, and `scripts/demo.mjs` passes it to BOTH boots.
One fixture, two languages, not two fixtures. The arrays stay authored in
English and are mapped through `t()` once per section; `src/data/demo-zh.ts`
holds every human-readable string keyed by its English original. That keeps
one org chart, one history planner and one set of invariants — a parallel
zh fixture would drift, and a Chinese demo with 19 catalog items where the
English one has 20 is not a bug anybody spots on a screen. It also makes "is
it translated?" a set comparison a test can make, in both directions.
Machine values are deliberately untouched: unit codes, period keys, select
values, timezones and frequencies are identical in both locales. Mailboxes
stay ASCII pinyin on the reserved `.example` domain while display names are
Chinese.
Position codes are now readable in both languages — `Plant compliance
officer` / `厂区合规专员` rather than `plant_compliance_officer`. The column
is free text the app never parsed; it was only ever being read by people.
The admin persona is MEASURED, not assumed (`@objectstack/*` 17.2.0): the
priming boot PATCHes `sys_user.name` to `演示管理员` before the seed runs,
signing in with the same credentials afterwards still returns 200, and the
seed replays as a no-op against the renamed row. It fails loudly if the
rename does not land, because the alternative is a fourteenth user owning
the demo account's work and four empty screens on first boot.
Tests: `test/seed-locale.test.ts` boots a kernel per locale and asserts equal
row counts, equal status/caliber/period distributions, a Han character in
every zh-CN display string, ASCII mailboxes, invented-only references, and
the English names and subjects pinned literally. `test/demo-locale.test.ts`
pins the switch and holds the dictionary to the fixture in both directions —
which is what stops this card quietly rewording the English demo.
`test/demo-script.test.ts` holds `scripts/demo.mjs` to the fixture's `ADMIN`.
Closes#117
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 2, 2026 10:39
@os-warren
os-warren merged commit 5c5a8a0 into mainSep 2, 2026
1 check passed
Sign up for freeto 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.

zh-CN demo dataset: a fully Chinese Ardenline for Chinese-locale demos (pnpm demo:zh)

1 participant

@os-warren