duaer-spec 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,9 +28,12 @@ For new behavior, fixes, refactors that change product behavior, or architecture
28
28
  3. Do **not** wait for `/duaer-do` or any other skill invocation.
29
29
  4. Do **not** say “please run `/duaer-specify`” — you do it.
30
30
  5. Do **not** dump a methodology tutorial unless they ask how Duaer works.
31
- 6. Merge to **`develop`** when done (hotfix: **`main`**, then back-merge
31
+ 6. **Break down to atomic, monitorable tasks:** each `tasks.md` item is one
32
+ independently verifiable atom with a `- [ ]` / `- [x]` checkbox; mark done
33
+ as you go so progress is visible. Do not leave only coarse mega-tasks.
34
+ 7. Merge to **`develop`** when done (hotfix: **`main`**, then back-merge
32
35
  **`develop`**). Promote to **`main`** only if the user asks to go online.
33
- 7. **Handoff (mandatory):** stop processes bound to `.worktree/<id>/`, remove
36
+ 8. **Handoff (mandatory):** stop processes bound to `.worktree/<id>/`, remove
34
37
  the worktree, then restart services from the primary checkout on `develop`
35
38
  using `.duaer/handoff.json` / `duaer handoff [--run]`. Never leave the user
36
39
  on a dead worktree server.
@@ -32,9 +32,12 @@ For new behavior, fixes, refactors that change product behavior, or architecture
32
32
  3. Do **not** wait for `/duaer-do` or any other skill invocation.
33
33
  4. Do **not** say “please run `/duaer-specify`” — you do it.
34
34
  5. Do **not** dump a methodology tutorial unless they ask how Duaer works.
35
- 6. Merge to **`develop`** when done (hotfix: **`main`**, then back-merge
35
+ 6. **Break down to atomic, monitorable tasks:** each `tasks.md` item is one
36
+ independently verifiable atom with a `- [ ]` / `- [x]` checkbox; mark done
37
+ as you go so progress is visible. Do not leave only coarse mega-tasks.
38
+ 7. Merge to **`develop`** when done (hotfix: **`main`**, then back-merge
36
39
  **`develop`**). Promote to **`main`** only if the user asks to go online.
37
- 7. **Handoff (mandatory):** stop processes bound to `.worktree/<id>/`, remove
40
+ 8. **Handoff (mandatory):** stop processes bound to `.worktree/<id>/`, remove
38
41
  the worktree, then restart services from the primary checkout on `develop`
39
42
  using `.duaer/handoff.json` / `duaer handoff [--run]`. Never leave the user
40
43
  on a dead worktree server.
@@ -54,7 +54,13 @@ Do **not** lecture about methodology. Do **not** ask them to run commands.
54
54
  - Deep ambiguity after a full Spec still uses `duaer-clarify` (up to 5
55
55
  questions). Understand is the **lightweight front door** on every ask.
56
56
 
57
- 3. **Break down** — If `tasks.md` missing, write a short checklist from the Spec.
57
+ 3. **Break down** — If `tasks.md` missing, write a checklist from the Spec.
58
+ **Atomic + monitorable (mandatory):**
59
+ - Split until each item is **one** independently verifiable acceptance /
60
+ deliverable atom (not a mega-phase).
61
+ - Every item is a markdown checkbox (`- [ ]` / `- [x]`) so progress can be
62
+ polled; check off immediately when that atom is done.
63
+ - Prefer one checkbox per Acceptance line; no artificial upper count.
58
64
  Skip a heavy plan unless architecture or contracts change.
59
65
 
60
66
  4. **Work** — Implement open tasks; mark `[x]` as you go. No scope beyond Spec.
@@ -139,6 +139,14 @@ The tasks.md should be immediately executable - each task must be specific enoug
139
139
 
140
140
  **CRITICAL**: Tasks MUST be organized by user story to enable independent implementation and testing.
141
141
 
142
+ **Atomic + monitorable (NON-NEGOTIABLE)**:
143
+ - Every task is **one** independently verifiable atom (one acceptance point,
144
+ one file-scoped change, or one verification slice) — not a bundled mega-task.
145
+ - Every task is a markdown checkbox (`- [ ]` / `- [x]`) so progress is
146
+ monitorable; agents mark `[x]` as each atom completes.
147
+ - Prefer one task per Acceptance / independent-test criterion. No artificial
148
+ upper count; refine if still too coarse.
149
+
142
150
  **Verification is DEFAULT**: For each user story, generate verification tasks
143
151
  from `.duaer/memory/testing.md` (L0–L3 as the risk table requires). Do **not**
144
152
  omit tests just because the Spec did not say “TDD”. Only skip a level when the
@@ -1,5 +1,5 @@
1
1
  {
2
- "featureDir": ".duaer/specs/202-release-021",
3
- "slug": "202-release-021",
4
- "branch": "chore/release-021"
2
+ "featureDir": ".duaer/specs/205-release-022",
3
+ "slug": "205-release-022",
4
+ "updatedAt": "2026-09-19T07:35:00.000Z"
5
5
  }
@@ -41,3 +41,16 @@ cannot cover the path.
41
41
  **Job handoff (not a git lock):** the agent reports accept/open itself.
42
42
  Default policy `coach` — do not claim "done" while the active job is unfinished.
43
43
  Do not send the human to run `duaer status` for routine work.
44
+
45
+ ### V. Atomic verifiable tasks; progress must be monitorable (NON-NEGOTIABLE)
46
+
47
+ When breaking down work (`tasks.md`, kickoff task pool, or mid-job re-split):
48
+
49
+ 1. **Atomic + verifiable:** each task covers **one** independently checkable
50
+ acceptance point or deliverable slice. Do not bundle unrelated acceptance
51
+ into a mega-task.
52
+ 2. **Monitorable progress:** every task is a markdown checkbox
53
+ (`- [ ]` / `- [x]`). Mark `[x]` as soon as that atom is done so FDE / humans
54
+ can see progress. Do not replace checkboxes with prose-only status.
55
+ 3. Prefer splitting Acceptance lines (and Goal slices) over inventing coarse
56
+ phases. There is **no** artificial upper count; refine if still too coarse.
@@ -0,0 +1,18 @@
1
+ {
2
+ "status": "accepted",
3
+ "acceptedAt": "2026-09-19T04:20:00.000Z",
4
+ "summary": "0.21.0 shipped: develop+main pushed, tag v0.21.0, GitHub Release, npm publish success.",
5
+ "verification": {
6
+ "commands": [
7
+ "git push origin develop main v0.21.0",
8
+ "gh release create v0.21.0",
9
+ "gh run watch 35420973848 → success",
10
+ "npm view duaer-spec version → 0.21.0"
11
+ ]
12
+ },
13
+ "preview": {
14
+ "url": "https://www.npmjs.com/package/duaer-spec/v/0.21.0",
15
+ "label": "npm 0.21.0"
16
+ },
17
+ "release": "https://github.com/fujiezee/duaer-spec/releases/tag/v0.21.0"
18
+ }
@@ -1,5 +1,5 @@
1
1
  # Tasks — 202-release-021
2
2
 
3
3
  - [x] Brief
4
- - [ ] Bump 0.21.0 + CHANGELOG
5
- - [ ] Promote / push / release / npm
4
+ - [x] Bump 0.21.0 + CHANGELOG
5
+ - [x] Promote / push / release / npm
@@ -0,0 +1,13 @@
1
+ {
2
+ "status": "accepted",
3
+ "acceptedAt": "2026-09-19T04:50:00.000Z",
4
+ "verification": {
5
+ "levels": ["L0", "L1"],
6
+ "commands": [
7
+ "node --check bin/live-modules.mjs bin/live-progress.mjs web/live-dev/task-graph.mjs web/live-dev/acceptance-lines.mjs",
8
+ "npm test (165 pass; atomic pool + preview split covered)"
9
+ ],
10
+ "result": "pass"
11
+ },
12
+ "summary": "Atomic verifiable tasks with checkbox-monitorable progress: constitution, skills, kickoff pool + preview graph split acceptance lines."
13
+ }
@@ -0,0 +1,44 @@
1
+ # Feature Specification: Atomic verifiable tasks with monitorable progress
2
+
3
+ **Feature Branch**: `feat/atomic-tasks`
4
+
5
+ ## Goal
6
+
7
+ When decomposing work (Break down / kickoff task pool / `tasks.md`), every
8
+ task MUST be an **atomic, independently verifiable** unit, and every task's
9
+ progress MUST be **monitorable** via a markdown checkbox that the FDE desk
10
+ (and agents) can poll.
11
+
12
+ ## Why
13
+
14
+ Coarse mega-tasks hide progress and block wave orchestration. Acceptance
15
+ bundled into one line cannot be checked off piecemeal.
16
+
17
+ ## In scope
18
+
19
+ - Constitution + duaer-do / duaer-tasks / template wording for the rule
20
+ - Kickoff `buildTaskPoolFromModules` splits module acceptance into one
21
+ checkbox task per acceptance line (plus impl / verify / shared steps)
22
+ - Preview task graph uses the same atomic split so path matches pool
23
+ - `tasks.md` footer / dispatch prompt reinforce: checkbox = progress signal
24
+ - Tests + E2E catalog row
25
+
26
+ ## Out of scope
27
+
28
+ - Changing orchestration wave algorithm beyond richer ready sets
29
+ - Dedicated cloud / npm release
30
+
31
+ ## Acceptance
32
+
33
+ 1. Constitution (or equivalent always-on rule) states: decompose to atomic
34
+ verifiable tasks; each task progress monitorable via `- [ ]` / `- [x]`
35
+ 2. Module with multi-line acceptance yields one task per acceptance line in
36
+ the task pool / `tasks.md`
37
+ 3. Preview pool for the same modules has matching atomic acceptance tasks
38
+ 4. Unit tests cover the split; E2E catalog mentions the rule
39
+ 5. Agents following duaer-do Break down must not leave only coarse mega-tasks
40
+
41
+ ## Assumptions
42
+
43
+ - Prefer splitting on acceptance lines / `;` / `;` (existing
44
+ `splitAcceptanceLines`) rather than LLM rephrasing at kickoff
@@ -0,0 +1,8 @@
1
+ # Tasks
2
+
3
+ - [x] T001 Add constitution + duaer-do / duaer-tasks / template rule
4
+ - [x] T002 Split acceptance into atomic tasks in live-modules pool
5
+ - [x] T003 Mirror atomic split in task-graph preview pool
6
+ - [x] T004 Strengthen tasks.md footer + dispatch prompt wording
7
+ - [x] T005 Unit tests + E2E catalog
8
+ - [x] T006 Stamp delivery accepted
@@ -0,0 +1,14 @@
1
+ {
2
+ "status": "accepted",
3
+ "acceptedAt": "2026-09-19T07:40:00.000Z",
4
+ "verification": {
5
+ "levels": ["L0", "L1", "L3"],
6
+ "commands": [
7
+ "node --check web/live-dev/i18n.js web/live-dev/i18n-ja.js web/live-dev/app.js bin/live-deliverables.mjs",
8
+ "npm test (168 pass incl. live-i18n-ja)",
9
+ "npm run test:live"
10
+ ],
11
+ "result": "pass"
12
+ },
13
+ "summary": "Added Japanese (ja) live desk locale with full catalog parity, language select, and deliverables lang=ja."
14
+ }
@@ -0,0 +1,34 @@
1
+ # Feature Specification: Live desk Japanese locale (ja)
2
+
3
+ **Feature Branch**: `feat/live-i18n-ja`
4
+
5
+ ## Goal
6
+
7
+ Add **Japanese (`ja`)** as a third FDE live-desk language beside zh-CN and en.
8
+ Users can switch to 日本語; reload keeps the choice; desk copy updates.
9
+
10
+ ## In scope
11
+
12
+ - `LOCALES` includes `ja`; full catalog for all i18n keys
13
+ - Language `<select>` option; `detectLocale` for `ja*` navigator
14
+ - Deliverables `lang=ja` + date/`html lang` handling
15
+ - app.js list join / toLocaleString for ja
16
+ - task-graph Archify locale passthrough
17
+ - E2E catalog + unit/smoke markers; CHANGELOG
18
+
19
+ ## Out of scope
20
+
21
+ - Translating README / intro site / worker-models.md into Japanese
22
+ - Other locales beyond `ja`
23
+
24
+ ## Acceptance
25
+
26
+ 1. Language select shows 日本語 (`ja`); switching updates visible desk copy
27
+ 2. Reload keeps `ja` via localStorage
28
+ 3. Every key present in `en` exists in `ja` (no missing-key gaps in catalog parity test)
29
+ 4. Deliverables HTML accepts `lang=ja`
30
+ 5. `npm test` / required live smoke still pass
31
+
32
+ ## Assumptions
33
+
34
+ - Default remains zh-CN when unset; `ja` browser language can auto-detect to ja
@@ -0,0 +1,7 @@
1
+ # Tasks
2
+
3
+ - [x] T001 Add ja catalog + LOCALES/detectLocale/setLocale wiring
4
+ - [x] T002 Language select option in index.html
5
+ - [x] T003 app.js / deliverables / task-graph ja handling
6
+ - [x] T004 Tests + E2E + docs + CHANGELOG
7
+ - [x] T005 Stamp delivery accepted
@@ -0,0 +1,12 @@
1
+ # 205 — Release 0.22.0
2
+
3
+ ## Goal
4
+
5
+ Ship 0.22.0: bump version + CHANGELOG, promote develop → main, push,
6
+ GitHub Release `v0.22.0`, trigger npm Trusted Publishing.
7
+
8
+ ## Acceptance
9
+
10
+ 1. `package.json` is `0.22.0`; CHANGELOG has `0.22.0` section (Unreleased cleared).
11
+ 2. `develop` and `main` pushed with release commits.
12
+ 3. GitHub Release `v0.22.0` published; npm shows `duaer-spec@0.22.0`.
@@ -0,0 +1,6 @@
1
+ # Tasks
2
+
3
+ - [x] T001 Bump package.json to 0.22.0 and move CHANGELOG Unreleased
4
+ - [x] T002 Merge release branch to develop
5
+ - [ ] T003 Promote develop → main, push, tag, gh release, npm
6
+ - [ ] T004 Stamp delivery accepted
@@ -15,6 +15,10 @@ TDD-style tests-before-code remain encouraged when useful.
15
15
 
16
16
  **Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
17
17
 
18
+ **Atomic + monitorable**: Each checkbox is one independently verifiable atom.
19
+ Progress is only via `- [ ]` / `- [x]` — no prose-only status. Split Acceptance
20
+ into one task per criterion; no artificial upper count.
21
+
18
22
  ## Format: `[ID] [P?] [Story] Description`
19
23
 
20
24
  - **[P]**: Can run in parallel (different files, no dependencies)
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.22.0 — 2026-09-19
6
+
7
+ ### Feat: Japanese (ja) live desk locale
8
+
9
+ - Language select adds 日本語; catalog covers all en keys
10
+ - Deliverables API accepts `lang=ja`; dates / HTML `lang` use ja-JP
11
+
12
+ ### Feat: atomic verifiable tasks with monitorable progress
13
+
14
+ - Constitution + duaer-do / duaer-tasks: decompose to one independently
15
+ verifiable atom per checkbox; progress only via `- [ ]` / `- [x]`
16
+ - Kickoff task pool and preview graph split multi-line acceptance into
17
+ one task per criterion so FDE can monitor and release waves
18
+
5
19
  ## 0.21.0 — 2026-09-19
6
20
 
7
21
  ### Feat: click architecture diagram for fullscreen present view
@@ -3625,7 +3625,7 @@ Brief: ${featureDir}
3625
3625
  3. 按 .duaer/memory/testing.md(若有)做风险验证
3626
3626
  4. 每完成 tasks.md 中的一步,立刻把该行改成 - [x](Duaer-spec FDE 靠此显示细粒度进度与编排放行)
3627
3627
  4a. 只做当前编排波次已放行的任务;未满足 dependsOn / 未放行的任务不要开工;本波勾完后:若还有后续波次则静默停等编排器 continue(不要写 Job not accepted yet);若已无后续波次则继续完成 stamp accepted
3628
- 4b. 拆任务:每个勾选项只覆盖一个可独立验收的功能点;不要把多项验收揉进同一条;不要人为限制条数(不必卡在 12 条内)。若仍偏粗,先按 Acceptance 扩成「一条功能一勾选」(仍用 T00x),保存后再做;小步勾选,不要攒到最后一次勾完
3628
+ 4b. 拆任务(强制):必须拆到原子任务且每条可独立验证;每个勾选项只覆盖一个验收点;进度只能用 tasks.md - [ ]/- [x] 监控(立刻勾选,不要攒到最后);不要把多项验收揉进同一条;不要人为限制条数。若仍偏粗,先按 Acceptance 扩成「一条验收一勾选」(仍用 T00x),保存后再做
3629
3629
  5. 对照 Acceptance 全部满足后,才 stamp ${path.join(featureDir, "delivery.json")} 为 accepted——tasks.md 全部勾完还不够,必须 stamp;禁止停在 Job not accepted yet
3630
3630
  5b. 交付前必须更新产品仓 README(说明文档):与本次交付一致——做什么、模块/验收要点、如何运行或打开;需求变了就改 README,不要只改代码。英文 README 不得出现中文;若项目是中文说明则用 README.zh-CN.md(或项目既有约定),可夹英文术语
3631
3631
  6. 必须在 delivery.json 写入 preview.url(满意交付的必填证据):必须是可打开的成品入口——HTTP 服务用 http://localhost:…;静态页用 index.html 等 HTML。禁止把 docs/**/*.md 等说明文档当作 preview.url——不要因「没有页面」而省略
@@ -4243,7 +4243,7 @@ ${restated.keep}
4243
4243
  0. 本轮 Revision 已在 Duaer-spec FDE 自动验收通过。直接改;不要进入 Confirming intent;不要让用户从多个风格/方向选项里再选一次;不要反复确认需求
4244
4244
  1. 只做本轮 Revision ${revN} 范围,不要重做无关功能
4245
4245
  2. 立刻把 tasks.md 里 R${revN}-* 勾成 - [x](Duaer-spec FDE 靠此显示细粒度进度)
4246
- 2b. 拆任务:每个 R${revN}-* 只覆盖一个可独立验收的改动;不要把多项验收揉进同一条;不要人为限制条数。若仍偏粗,先按本轮 acceptance 扩成「一条改动一勾选」(仍用 R${revN}-*),保存后再做;小步勾选
4246
+ 2b. 拆任务(强制):必须拆到原子任务且每条可独立验证;每个 R${revN}-* 只覆盖一个改动;进度用 - [ ]/- [x] 立刻勾选监控;不要把多项验收揉进同一条;不要人为限制条数。若仍偏粗,先按本轮 acceptance 扩成「一条改动一勾选」(仍用 R${revN}-*),保存后再做
4247
4247
  3. 对照本轮 Revision acceptance 全部满足后,才 stamp delivery.json 为 accepted,并必须更新 preview.url 为可打开的成品入口(http://localhost:… 或 HTML 页面,必填;禁止用 docs/**/*.md 当预览);若是服务须先启动并可打开。禁止以 Job not accepted yet 收尾
4248
4248
  3b. 交付前必须更新产品仓 README(说明文档)以反映本轮改动后的行为/用法;需求变了就改 README,不要只改代码。英文 README 不得出现中文;中文说明用 README.zh-CN.md(或项目既有约定)
4249
4249
  4. 按 testing.md 做风险验证(若有)
@@ -6578,10 +6578,13 @@ async function handleApi(req, res) {
6578
6578
  send(res, 400, { error: "path required", code: "EMPTY_PATH" });
6579
6579
  return;
6580
6580
  }
6581
+ const langRaw = String(url.searchParams.get("lang") || "").toLowerCase();
6581
6582
  const lang =
6582
- String(url.searchParams.get("lang") || "").toLowerCase() === "en"
6583
+ langRaw === "en" || langRaw.startsWith("en")
6583
6584
  ? "en"
6584
- : "zh";
6585
+ : langRaw === "ja" || langRaw.startsWith("ja")
6586
+ ? "ja"
6587
+ : "zh";
6585
6588
  const asJson = String(url.searchParams.get("format") || "") === "json";
6586
6589
  const session = readProjectChat(liveRoot(), projectPath);
6587
6590
  let job = null;
@@ -15,12 +15,25 @@ function esc(s) {
15
15
  .replace(/"/g, "&quot;");
16
16
  }
17
17
 
18
+ function normalizeDeliverablesLang(lang) {
19
+ const l = String(lang || "").toLowerCase();
20
+ if (l === "en" || l.startsWith("en")) return "en";
21
+ if (l === "ja" || l.startsWith("ja")) return "ja";
22
+ return "zh";
23
+ }
24
+
25
+ function pickLang(lang, map) {
26
+ const l = normalizeDeliverablesLang(lang);
27
+ return map[l] ?? map.zh;
28
+ }
29
+
18
30
  function fmtAt(iso, lang) {
19
- if (!iso) return lang === "en" ? "—" : "—";
31
+ if (!iso) return "—";
20
32
  try {
21
33
  const d = new Date(iso);
22
34
  if (Number.isNaN(d.getTime())) return String(iso).slice(0, 19);
23
- return d.toLocaleString(lang === "en" ? "en-US" : "zh-CN", {
35
+ const tag = pickLang(lang, { en: "en-US", ja: "ja-JP", zh: "zh-CN" });
36
+ return d.toLocaleString(tag, {
24
37
  year: "numeric",
25
38
  month: "2-digit",
26
39
  day: "2-digit",
@@ -279,7 +292,7 @@ function renderTaskPoolHtml(body, L) {
279
292
  * @param {{ job?: object|null, lang?: string, projectTitle?: string }} [opts]
280
293
  */
281
294
  export function buildDeliverablesModel(session, opts = {}) {
282
- const lang = opts.lang === "en" ? "en" : "zh";
295
+ const lang = normalizeDeliverablesLang(opts.lang);
283
296
  const s = session && typeof session === "object" ? session : {};
284
297
  const modules = Array.isArray(s.modules) ? s.modules : [];
285
298
  const confirmed = modules.filter((m) => m.status === "confirmed");
@@ -294,7 +307,7 @@ export function buildDeliverablesModel(session, opts = {}) {
294
307
  if (allReq || confirmed.length) {
295
308
  reqVersions.push({
296
309
  id: "v0",
297
- label: lang === "en" ? "Initial" : "初版",
310
+ label: pickLang(lang, { en: "Initial", ja: "初版", zh: "初版" }),
298
311
  at: s.updatedAt || null,
299
312
  modules: (allReq ? modules : confirmed).map((m) => ({
300
313
  id: m.id,
@@ -309,8 +322,11 @@ export function buildDeliverablesModel(session, opts = {}) {
309
322
  if (rev < 1) continue;
310
323
  reqVersions.push({
311
324
  id: `r${rev}`,
312
- label:
313
- lang === "en" ? `Revision ${rev}` : `第 ${rev} 次改进`,
325
+ label: pickLang(lang, {
326
+ en: `Revision ${rev}`,
327
+ ja: `改訂 ${rev}`,
328
+ zh: `第 ${rev} 次改进`,
329
+ }),
314
330
  at: entry.at || s.updatedAt || null,
315
331
  revise: {
316
332
  goal: entry.goal || "",
@@ -324,19 +340,27 @@ export function buildDeliverablesModel(session, opts = {}) {
324
340
  const stages = [
325
341
  {
326
342
  id: "requirements",
327
- title: lang === "en" ? "Requirements" : "需求",
343
+ title: pickLang(lang, { en: "Requirements", ja: "要件", zh: "需求" }),
328
344
  status: allReq ? "done" : confirmed.length ? "partial" : "empty",
329
345
  artifacts: [
330
346
  {
331
347
  id: "req-doc",
332
- title: lang === "en" ? "Requirements document" : "需求文档",
348
+ title: pickLang(lang, {
349
+ en: "Requirements document",
350
+ ja: "要件ドキュメント",
351
+ zh: "需求文档",
352
+ }),
333
353
  kind: "timeline",
334
354
  ready: reqVersions.length > 0,
335
355
  versions: reqVersions,
336
356
  },
337
357
  {
338
358
  id: "req-confirm",
339
- title: lang === "en" ? "Requirements confirmation" : "需求确认书",
359
+ title: pickLang(lang, {
360
+ en: "Requirements confirmation",
361
+ ja: "要件確認書",
362
+ zh: "需求确认书",
363
+ }),
340
364
  kind: "confirmations",
341
365
  ready: confirmed.length > 0,
342
366
  confirmations: confirmed.map((m) => ({
@@ -350,12 +374,16 @@ export function buildDeliverablesModel(session, opts = {}) {
350
374
  },
351
375
  {
352
376
  id: "architecture",
353
- title: lang === "en" ? "Architecture" : "架构",
377
+ title: pickLang(lang, { en: "Architecture", ja: "アーキテクチャ", zh: "架构" }),
354
378
  status: arch.confirmed ? "done" : arch.url ? "partial" : "empty",
355
379
  artifacts: [
356
380
  {
357
381
  id: "arch-diagram",
358
- title: lang === "en" ? "Architecture diagram" : "架构图",
382
+ title: pickLang(lang, {
383
+ en: "Architecture diagram",
384
+ ja: "アーキテクチャ図",
385
+ zh: "架构图",
386
+ }),
359
387
  kind: "architecture",
360
388
  ready: Boolean(arch.url),
361
389
  url: arch.url || null,
@@ -364,34 +392,48 @@ export function buildDeliverablesModel(session, opts = {}) {
364
392
  },
365
393
  {
366
394
  id: "arch-confirm",
367
- title: lang === "en" ? "Architecture confirmation" : "架构确认",
395
+ title: pickLang(lang, {
396
+ en: "Architecture confirmation",
397
+ ja: "アーキテクチャ確認",
398
+ zh: "架构确认",
399
+ }),
368
400
  kind: "note",
369
401
  ready: Boolean(arch.confirmed),
370
402
  body: arch.confirmed
371
- ? lang === "en"
372
- ? "Architecture confirmed on the desk."
373
- : "已在台面确认架构。"
403
+ ? pickLang(lang, {
404
+ en: "Architecture confirmed on the desk.",
405
+ ja: "デスクでアーキテクチャを確認済みです。",
406
+ zh: "已在台面确认架构。",
407
+ })
374
408
  : "",
375
409
  },
376
410
  ],
377
411
  },
378
412
  {
379
413
  id: "kickoff",
380
- title: lang === "en" ? "Kickoff & implementation" : "开工与实现",
414
+ title: pickLang(lang, {
415
+ en: "Kickoff & implementation",
416
+ ja: "キックオフと実装",
417
+ zh: "开工与实现",
418
+ }),
381
419
  status: s.jobId || s.taskPool ? "done" : "empty",
382
420
  artifacts: [
383
421
  {
384
422
  id: "brief",
385
- title: lang === "en" ? "Brief / job" : "Brief / 工单",
423
+ title: pickLang(lang, {
424
+ en: "Brief / job",
425
+ ja: "Brief / ジョブ",
426
+ zh: "Brief / 工单",
427
+ }),
386
428
  kind: "note",
387
429
  ready: Boolean(s.jobId),
388
430
  body: s.jobId
389
- ? `${lang === "en" ? "Job" : "工单"}: ${s.jobId}`
431
+ ? `${pickLang(lang, { en: "Job", ja: "ジョブ", zh: "工单" })}: ${s.jobId}`
390
432
  : "",
391
433
  },
392
434
  {
393
435
  id: "task-pool",
394
- title: lang === "en" ? "Task pool" : "任务池",
436
+ title: pickLang(lang, { en: "Task pool", ja: "タスクプール", zh: "任务池" }),
395
437
  kind: "taskpool",
396
438
  ready: Boolean(s.taskPool),
397
439
  body: s.taskPool
@@ -399,16 +441,18 @@ export function buildDeliverablesModel(session, opts = {}) {
399
441
  : "",
400
442
  meta:
401
443
  Number(s.workerCount) > 1
402
- ? lang === "en"
403
- ? `Workers: ${s.workerCount}`
404
- : `数字员工:${s.workerCount}`
444
+ ? pickLang(lang, {
445
+ en: `Workers: ${s.workerCount}`,
446
+ ja: `デジタル従業員:${s.workerCount}`,
447
+ zh: `数字员工:${s.workerCount}`,
448
+ })
405
449
  : "",
406
450
  },
407
451
  ],
408
452
  },
409
453
  {
410
454
  id: "delivery",
411
- title: lang === "en" ? "Delivery" : "交付",
455
+ title: pickLang(lang, { en: "Delivery", ja: "デリバリー", zh: "交付" }),
412
456
  status:
413
457
  delivery?.status === "accepted" || job?.jobStatus === "accepted"
414
458
  ? "done"
@@ -418,22 +462,26 @@ export function buildDeliverablesModel(session, opts = {}) {
418
462
  artifacts: [
419
463
  {
420
464
  id: "delivery-stamp",
421
- title: lang === "en" ? "Delivery stamp" : "交付戳",
465
+ title: pickLang(lang, {
466
+ en: "Delivery stamp",
467
+ ja: "デリバリースタンプ",
468
+ zh: "交付戳",
469
+ }),
422
470
  kind: "note",
423
471
  ready: Boolean(delivery || job?.jobStatus),
424
472
  body: delivery
425
- ? `${lang === "en" ? "Status" : "状态"}: ${delivery.status || "—"}${
473
+ ? `${pickLang(lang, { en: "Status", ja: "状態", zh: "状态" })}: ${delivery.status || "—"}${
426
474
  delivery.acceptedAt
427
- ? `\n${lang === "en" ? "Accepted" : "验收"}: ${delivery.acceptedAt}`
475
+ ? `\n${pickLang(lang, { en: "Accepted", ja: "受入", zh: "验收" })}: ${delivery.acceptedAt}`
428
476
  : ""
429
477
  }`
430
478
  : job?.jobStatus
431
- ? `${lang === "en" ? "Job status" : "工单状态"}: ${job.jobStatus}`
479
+ ? `${pickLang(lang, { en: "Job status", ja: "ジョブ状態", zh: "工单状态" })}: ${job.jobStatus}`
432
480
  : "",
433
481
  },
434
482
  {
435
483
  id: "preview",
436
- title: lang === "en" ? "Preview" : "预览",
484
+ title: pickLang(lang, { en: "Preview", ja: "プレビュー", zh: "预览" }),
437
485
  kind: "link",
438
486
  ready: Boolean(preview?.url),
439
487
  url: preview?.url || null,
@@ -445,14 +493,15 @@ export function buildDeliverablesModel(session, opts = {}) {
445
493
  if (reviseCards.length || s.reviseLocked || s.lastRevision) {
446
494
  stages.push({
447
495
  id: "revise",
448
- title: lang === "en" ? "Revisions" : "改进",
496
+ title: pickLang(lang, { en: "Revisions", ja: "改訂", zh: "改进" }),
449
497
  status: reviseCards.length ? "done" : "partial",
450
498
  artifacts: reviseCards.map((entry) => ({
451
499
  id: `revise-${entry.revision}`,
452
- title:
453
- lang === "en"
454
- ? `Revision ${entry.revision} plan`
455
- : `第 ${entry.revision} 版改进方案`,
500
+ title: pickLang(lang, {
501
+ en: `Revision ${entry.revision} plan`,
502
+ ja: `改訂 ${entry.revision} プラン`,
503
+ zh: `第 ${entry.revision} 版改进方案`,
504
+ }),
456
505
  kind: "card",
457
506
  ready: true,
458
507
  card: {
@@ -474,7 +523,7 @@ export function buildDeliverablesModel(session, opts = {}) {
474
523
  projectTitle:
475
524
  String(opts.projectTitle || "").trim() ||
476
525
  path.basename(String(s.projectPath || "")) ||
477
- (lang === "en" ? "Project" : "项目"),
526
+ pickLang(lang, { en: "Project", ja: "プロジェクト", zh: "项目" }),
478
527
  updatedAt: s.updatedAt || null,
479
528
  stages,
480
529
  };
@@ -512,6 +561,37 @@ function labelsFor(lang) {
512
561
  jump: "View in requirements",
513
562
  };
514
563
  }
564
+ if (lang === "ja") {
565
+ return {
566
+ brand: "プロジェクト成果物",
567
+ pageTitle: "成果物ドシエ",
568
+ subtitle: "各段階で何ができたかを整理した記録",
569
+ generated: "生成日時",
570
+ project: "プロジェクト",
571
+ emptyStage: "まだ成果物はありません",
572
+ emptyCard: "(空)",
573
+ goal: "目的",
574
+ out: "対象外",
575
+ accept: "受入基準",
576
+ assume: "前提",
577
+ statusDone: "完了",
578
+ statusPartial: "進行中",
579
+ statusEmpty: "未着手",
580
+ timeline: "バージョンタイムライン",
581
+ toc: "目次",
582
+ artifacts: "成果物",
583
+ openArch: "図を開く",
584
+ openLink: "開く",
585
+ confirmed: "確認済み",
586
+ draft: "下書き",
587
+ module: "モジュール",
588
+ depends: "依存",
589
+ taskId: "ID",
590
+ taskTitle: "タスク",
591
+ confirmRegistry: "確認レジストリ",
592
+ jump: "要件本文を見る",
593
+ };
594
+ }
515
595
  return {
516
596
  brand: "项目交付物",
517
597
  pageTitle: "交付物档案",
@@ -631,7 +711,7 @@ function renderArtifact(art, L, lang) {
631
711
  * @param {ReturnType<typeof buildDeliverablesModel>} model
632
712
  */
633
713
  export function renderDeliverablesHtml(model) {
634
- const lang = model.lang === "en" ? "en" : "zh";
714
+ const lang = normalizeDeliverablesLang(model.lang);
635
715
  const L = labelsFor(lang);
636
716
  const stages = model.stages || [];
637
717
  const tocHtml = `<nav class="toc" aria-label="${esc(L.toc)}">
@@ -673,7 +753,7 @@ export function renderDeliverablesHtml(model) {
673
753
  .join("\n");
674
754
 
675
755
  return `<!DOCTYPE html>
676
- <html lang="${lang === "en" ? "en" : "zh-CN"}">
756
+ <html lang="${pickLang(lang, { en: "en", ja: "ja", zh: "zh-CN" })}">
677
757
  <head>
678
758
  <meta charset="utf-8" />
679
759
  <meta name="viewport" content="width=device-width, initial-scale=1" />