cabloy 5.1.249 → 5.1.250

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.
Files changed (115) hide show
  1. package/.cabloy-version +1 -1
  2. package/.github/workflows/create-cabloy.yml +50 -3
  3. package/CHANGELOG.md +21 -0
  4. package/package.json +1 -1
  5. package/repo-agent-governance/managed-assets.json +12 -12
  6. package/repo-agent-governance/skills/cabloy-spec-execution/references/status-and-evidence.md +1 -1
  7. package/repo-agent-governance/skills/cabloy-spec-generation/references/canonical-spec-input.md +1 -1
  8. package/repo-agent-governance/skills/cabloy-spec-generation/references/repo-specs-document-set.md +2 -2
  9. package/repo-agent-governance/skills/cabloy-spec-generation/references/traceability-and-status-rules.md +1 -1
  10. package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.mjs +73 -22
  11. package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.test.mjs +98 -7
  12. package/repo-docs/ai/playbook-spec-execution.md +1 -1
  13. package/repo-docs/backend/crud-workflow.md +92 -1
  14. package/repo-docs/backend/dto-guide.md +16 -1
  15. package/repo-docs/backend/dto-infer-generation.md +23 -5
  16. package/repo-docs/backend/orm-aggregate-group-guide.md +32 -0
  17. package/repo-docs/backend/validation-guide.md +38 -2
  18. package/repo-e2e/specs/cabloy-basic.spec.ts +9 -2
  19. package/vona/packages-cli/cli/package.json +1 -1
  20. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudAnalysis/aggregate/src/dto/{{resourceName}}AggregateRes.tsx_ +11 -0
  21. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudAnalysis/common/src/lib/{{resourceName}}Analysis.ts_ +14 -0
  22. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudAnalysis/common/test/{{resourceName}}Analysis.test.ts_ +81 -0
  23. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudAnalysis/group/src/dto/{{resourceName}}GroupResItem.tsx_ +11 -0
  24. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/controller/{{resourceName}}.ts_ +18 -1
  25. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}SelectReq.tsx_ +2 -0
  26. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/service/{{resourceName}}.ts_ +14 -1
  27. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/test/{{resourceName}}.test.ts_ +4 -0
  28. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/3-en-us.ts +2 -0
  29. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/4-zh-cn.ts +2 -0
  30. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/controller/{{resourceName}}.ts_ +18 -1
  31. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}SelectReq.tsx_ +2 -0
  32. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/service/{{resourceName}}.ts_ +14 -1
  33. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/test/{{resourceName}}.test.ts_ +1 -1
  34. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/3-en-us.ts +2 -0
  35. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/4-zh-cn.ts +2 -0
  36. package/vona/packages-cli/cli-set-api/package.json +1 -1
  37. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.tools.crudBasic.ts +2 -0
  38. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.tools.crudStart.ts +2 -0
  39. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.tools.masterDetail.ts +9 -1
  40. package/vona/packages-cli/cli-set-api/src/lib/crudAnalysis.ts +11 -0
  41. package/vona/packages-cli/cli-set-api/test/crudAnalysis.test.ts +385 -0
  42. package/vona/packages-utils/babel-plugin-vona-bean-module/package.json +1 -1
  43. package/vona/packages-vona/vona/package.json +1 -1
  44. package/vona/packages-vona/vona-core/package.json +1 -1
  45. package/vona/packages-vona/vona-mock/package.json +1 -1
  46. package/vona/pnpm-lock.yaml +7 -7
  47. package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +18 -0
  48. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +2 -0
  49. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +2 -0
  50. package/vona/src/suite/a-training/modules/training-student/src/controller/student.ts +28 -1
  51. package/vona/src/suite/a-training/modules/training-student/src/dto/studentAggregateRes.tsx +11 -0
  52. package/vona/src/suite/a-training/modules/training-student/src/dto/studentGroupResItem.tsx +11 -0
  53. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectReq.tsx +2 -0
  54. package/vona/src/suite/a-training/modules/training-student/src/lib/studentAnalysis.ts +12 -0
  55. package/vona/src/suite/a-training/modules/training-student/src/service/student.ts +22 -1
  56. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +4 -0
  57. package/vona/src/suite/a-training/modules/training-student/test/studentAnalysis.test.ts +422 -0
  58. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  59. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  60. package/vona/src/suite-vendor/a-captcha/modules/a-captcha/package.json +1 -1
  61. package/vona/src/suite-vendor/a-captcha/package.json +1 -1
  62. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  63. package/vona/src/suite-vendor/a-vona/modules/a-hmr/package.json +1 -1
  64. package/vona/src/suite-vendor/a-vona/modules/a-locale/package.json +1 -1
  65. package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
  66. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/package.json +1 -1
  67. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/utils.ts +1 -1
  68. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  69. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/.metadata/index.ts +26 -26
  70. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/aggregateResult.ts +53 -0
  71. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/analysis/controls.ts +163 -0
  72. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/analysis/execute.ts +49 -0
  73. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/analysis/index.ts +3 -0
  74. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/analysis/policy.ts +140 -0
  75. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_cache.ts +4 -2
  76. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_crud_inner.ts +8 -3
  77. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dto.ts +3 -0
  78. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoAggregate.ts +4 -1
  79. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoAnalysis.ts +34 -0
  80. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/index.ts +1 -0
  81. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/types/analysis.ts +137 -0
  82. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/types/index.ts +1 -0
  83. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/types/relationsAggr.ts +1 -1
  84. package/vona/src/suite-vendor/a-vona/modules/a-orm/test/aggregateResult.test.ts +81 -0
  85. package/vona/src/suite-vendor/a-vona/modules/a-orm/test/analysis.test.ts +208 -0
  86. package/vona/src/suite-vendor/a-vona/modules/a-orm/test/analysis.types.test.ts +112 -0
  87. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  88. package/vona/src/suite-vendor/a-vona/modules/a-queue/package.json +1 -1
  89. package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/package.json +1 -1
  90. package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
  91. package/vona/src/suite-vendor/a-vona/modules/a-web/package.json +1 -1
  92. package/vona/src/suite-vendor/a-vona/modules/a-web/src/.metadata/index.ts +40 -40
  93. package/vona/src/suite-vendor/a-vona/modules/a-web/src/bean/pipe.filter.ts +44 -3
  94. package/vona/src/suite-vendor/a-vona/modules/a-web/src/lib/analysisFilter.ts +69 -0
  95. package/vona/src/suite-vendor/a-vona/modules/a-web/src/lib/decorator/arguments.ts +6 -4
  96. package/vona/src/suite-vendor/a-vona/modules/a-web/src/lib/index.ts +1 -0
  97. package/vona/src/suite-vendor/a-vona/modules/a-web/src/types/dto.ts +2 -1
  98. package/vona/src/suite-vendor/a-vona/modules/a-web/src/types/filter.ts +7 -2
  99. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  100. package/zova/packages-cli/cli/package.json +2 -2
  101. package/zova/packages-cli/cli-set-front/package.json +2 -2
  102. package/zova/packages-utils/zova-jsx/package.json +2 -2
  103. package/zova/packages-utils/zova-vite/package.json +1 -1
  104. package/zova/packages-zova/zova/package.json +3 -3
  105. package/zova/packages-zova/zova-core/package.json +1 -1
  106. package/zova/pnpm-lock.yaml +11 -11
  107. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +16 -2
  108. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +201 -7
  109. package/zova/src/suite/a-training/modules/training-student/src/api/trainingStudent.ts +42 -0
  110. package/zova/src/suite/a-training/modules/training-student/src/apiSchema/trainingStudent.ts +10 -0
  111. package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
  112. package/zova/src/suite-vendor/a-zova/modules/a-ssrserver/package.json +1 -1
  113. package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
  114. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  115. package/zova/src/suite-vendor/a-zova/package.json +5 -5
package/.cabloy-version CHANGED
@@ -1 +1 @@
1
- 5.1.249
1
+ 5.1.250
@@ -96,13 +96,60 @@ jobs:
96
96
  $log = Join-Path $redisDirectory 'redis.log'
97
97
  if (Test-Path $log) { Get-Content $log }
98
98
  throw 'Redis did not become ready'
99
+ - name: wait for published Cabloy package
100
+ shell: bash
101
+ run: |
102
+ node --input-type=module <<'NODE'
103
+ import { appendFileSync, readFileSync } from 'node:fs';
104
+
105
+ const expectedVersion = JSON.parse(readFileSync('package.json', 'utf8')).version;
106
+ const latestUrl = 'https://registry.npmjs.org/cabloy/latest';
107
+ const deadline = Date.now() + 20 * 60 * 1000;
108
+ let lastStatus = 'not checked';
109
+
110
+ while (Date.now() < deadline) {
111
+ try {
112
+ const response = await fetch(latestUrl, { cache: 'no-store' });
113
+ lastStatus = `${response.status} ${response.statusText}`;
114
+ if (response.ok) {
115
+ const metadata = await response.json();
116
+ if (metadata.version === expectedVersion) break;
117
+ lastStatus = `latest is ${metadata.version ?? 'unknown'}`;
118
+ }
119
+ } catch (error) {
120
+ lastStatus = error instanceof Error ? error.message : String(error);
121
+ }
122
+ console.log(`Waiting for cabloy@${expectedVersion} to become npm latest (${lastStatus})...`);
123
+ await new Promise(resolve => setTimeout(resolve, 10_000));
124
+ }
125
+
126
+ if (Date.now() >= deadline) {
127
+ throw new Error(`cabloy@${expectedVersion} was not npm latest within 20 minutes (last status: ${lastStatus})`);
128
+ }
129
+
130
+ appendFileSync(process.env.GITHUB_ENV, `CABLOY_VERSION=${expectedVersion}\n`);
131
+ NODE
99
132
  - name: create project
100
133
  run: npm create cabloy@latest cabloy-app
101
134
  - name: verify project initialization
102
135
  working-directory: cabloy-app
103
- run: >-
104
- node --input-type=module -e
105
- "import { existsSync } from 'node:fs'; const required = ['.cabloy-version', 'package.json', 'vona/package.json', 'zova/package.json', 'node_modules', 'vona/node_modules', 'zova/node_modules', 'zova/dist']; const missing = required.filter(file => !existsSync(file)); if (missing.length) { console.error('Project initialization is incomplete:', missing.join(', ')); process.exit(1); }"
136
+ shell: bash
137
+ run: |
138
+ node --input-type=module <<'NODE'
139
+ import { existsSync, readFileSync } from 'node:fs';
140
+
141
+ const required = ['.cabloy-version', 'package.json', 'vona/package.json', 'zova/package.json', 'node_modules', 'vona/node_modules', 'zova/node_modules', 'zova/dist'];
142
+ const missing = required.filter(file => !existsSync(file));
143
+ const packageVersion = JSON.parse(readFileSync('package.json', 'utf8')).version;
144
+ if (packageVersion !== process.env.CABLOY_VERSION) {
145
+ console.error(`Expected cabloy@${process.env.CABLOY_VERSION}, got cabloy@${packageVersion}`);
146
+ process.exit(1);
147
+ }
148
+ if (missing.length) {
149
+ console.error('Project initialization is incomplete:', missing.join(', '));
150
+ process.exit(1);
151
+ }
152
+ NODE
106
153
  - name: reset database
107
154
  working-directory: cabloy-app
108
155
  run: npm run db:reset
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.250
4
+
5
+ ### Features
6
+
7
+ - Add Japanese (`ja-jp`) and Traditional Chinese (`zh-tw`) locales.
8
+ - Introduce locale providers and locale specifications.
9
+ - Add grouping and aggregation capabilities.
10
+
11
+ ### Bug Fixes
12
+
13
+ - Distinguish planning closure from unfinished work breakdown structure (WBS) scope.
14
+ - Correct target decorator DTO resolution for OpenAPI.
15
+ - Keep Cabloy Locale delivery confined to its worktree.
16
+
17
+ ### Improvements
18
+
19
+ - Refactor CLI CRUD functionality.
20
+ - Enhance grouping and aggregation.
21
+ - Clarify locale provider examples and metadata commands.
22
+ - Update locale documentation and synchronize OpenAPI guidance.
23
+
3
24
  ## 5.1.249
4
25
 
5
26
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.249",
3
+ "version": "5.1.250",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -186,7 +186,7 @@
186
186
  {
187
187
  "adapter": "codex",
188
188
  "category": "skill",
189
- "sha256": "9ec7a5dd8b7c1a5d561a96562ee754c148e1d14db953d3629075af89e38613d3",
189
+ "sha256": "a7375a23f4112d16407e867334781856367d041a956cb0009244881b3f88dcff",
190
190
  "source": "skills/cabloy-spec-execution/references/status-and-evidence.md",
191
191
  "target": ".agents/skills/cabloy-spec-execution/references/status-and-evidence.md"
192
192
  },
@@ -221,7 +221,7 @@
221
221
  {
222
222
  "adapter": "codex",
223
223
  "category": "skill",
224
- "sha256": "f4051409e04e5557a3d45415b8111e6a70b7bfa013b52ee75553ace29d28531c",
224
+ "sha256": "6cb948d8c061c1abf96045f3edf5a24319c2cc22d9f1f6c5f1811b6face0333c",
225
225
  "source": "skills/cabloy-spec-generation/references/canonical-spec-input.md",
226
226
  "target": ".agents/skills/cabloy-spec-generation/references/canonical-spec-input.md"
227
227
  },
@@ -235,14 +235,14 @@
235
235
  {
236
236
  "adapter": "codex",
237
237
  "category": "skill",
238
- "sha256": "17aaee04e0db0bce9b09d72eb55181d062b0024974ed72014681be1c8490a6a1",
238
+ "sha256": "0a8c64f3b0c055790d9fb2b267954dbbd0c2473a2ea0e5b2c8be2348a9956459",
239
239
  "source": "skills/cabloy-spec-generation/references/repo-specs-document-set.md",
240
240
  "target": ".agents/skills/cabloy-spec-generation/references/repo-specs-document-set.md"
241
241
  },
242
242
  {
243
243
  "adapter": "codex",
244
244
  "category": "skill",
245
- "sha256": "6066944449f7072dbecfde7a79ceb16ebf75777b00c28ddc996b90d8dcdf2145",
245
+ "sha256": "6698717c1e20519fa13c2961de7e863fe5c2ff9f2f420f3f289f96a9533a190e",
246
246
  "source": "skills/cabloy-spec-generation/references/traceability-and-status-rules.md",
247
247
  "target": ".agents/skills/cabloy-spec-generation/references/traceability-and-status-rules.md"
248
248
  },
@@ -522,7 +522,7 @@
522
522
  {
523
523
  "adapter": "claude",
524
524
  "category": "skill",
525
- "sha256": "9ec7a5dd8b7c1a5d561a96562ee754c148e1d14db953d3629075af89e38613d3",
525
+ "sha256": "a7375a23f4112d16407e867334781856367d041a956cb0009244881b3f88dcff",
526
526
  "source": "skills/cabloy-spec-execution/references/status-and-evidence.md",
527
527
  "target": ".claude/skills/cabloy-spec-execution/references/status-and-evidence.md"
528
528
  },
@@ -557,7 +557,7 @@
557
557
  {
558
558
  "adapter": "claude",
559
559
  "category": "skill",
560
- "sha256": "f4051409e04e5557a3d45415b8111e6a70b7bfa013b52ee75553ace29d28531c",
560
+ "sha256": "6cb948d8c061c1abf96045f3edf5a24319c2cc22d9f1f6c5f1811b6face0333c",
561
561
  "source": "skills/cabloy-spec-generation/references/canonical-spec-input.md",
562
562
  "target": ".claude/skills/cabloy-spec-generation/references/canonical-spec-input.md"
563
563
  },
@@ -571,14 +571,14 @@
571
571
  {
572
572
  "adapter": "claude",
573
573
  "category": "skill",
574
- "sha256": "17aaee04e0db0bce9b09d72eb55181d062b0024974ed72014681be1c8490a6a1",
574
+ "sha256": "0a8c64f3b0c055790d9fb2b267954dbbd0c2473a2ea0e5b2c8be2348a9956459",
575
575
  "source": "skills/cabloy-spec-generation/references/repo-specs-document-set.md",
576
576
  "target": ".claude/skills/cabloy-spec-generation/references/repo-specs-document-set.md"
577
577
  },
578
578
  {
579
579
  "adapter": "claude",
580
580
  "category": "skill",
581
- "sha256": "6066944449f7072dbecfde7a79ceb16ebf75777b00c28ddc996b90d8dcdf2145",
581
+ "sha256": "6698717c1e20519fa13c2961de7e863fe5c2ff9f2f420f3f289f96a9533a190e",
582
582
  "source": "skills/cabloy-spec-generation/references/traceability-and-status-rules.md",
583
583
  "target": ".claude/skills/cabloy-spec-generation/references/traceability-and-status-rules.md"
584
584
  },
@@ -872,7 +872,7 @@
872
872
  {
873
873
  "adapter": "cursor",
874
874
  "category": "skill",
875
- "sha256": "9ec7a5dd8b7c1a5d561a96562ee754c148e1d14db953d3629075af89e38613d3",
875
+ "sha256": "a7375a23f4112d16407e867334781856367d041a956cb0009244881b3f88dcff",
876
876
  "source": "skills/cabloy-spec-execution/references/status-and-evidence.md",
877
877
  "target": ".cursor/skills/cabloy-spec-execution/references/status-and-evidence.md"
878
878
  },
@@ -907,7 +907,7 @@
907
907
  {
908
908
  "adapter": "cursor",
909
909
  "category": "skill",
910
- "sha256": "f4051409e04e5557a3d45415b8111e6a70b7bfa013b52ee75553ace29d28531c",
910
+ "sha256": "6cb948d8c061c1abf96045f3edf5a24319c2cc22d9f1f6c5f1811b6face0333c",
911
911
  "source": "skills/cabloy-spec-generation/references/canonical-spec-input.md",
912
912
  "target": ".cursor/skills/cabloy-spec-generation/references/canonical-spec-input.md"
913
913
  },
@@ -921,14 +921,14 @@
921
921
  {
922
922
  "adapter": "cursor",
923
923
  "category": "skill",
924
- "sha256": "17aaee04e0db0bce9b09d72eb55181d062b0024974ed72014681be1c8490a6a1",
924
+ "sha256": "0a8c64f3b0c055790d9fb2b267954dbbd0c2473a2ea0e5b2c8be2348a9956459",
925
925
  "source": "skills/cabloy-spec-generation/references/repo-specs-document-set.md",
926
926
  "target": ".cursor/skills/cabloy-spec-generation/references/repo-specs-document-set.md"
927
927
  },
928
928
  {
929
929
  "adapter": "cursor",
930
930
  "category": "skill",
931
- "sha256": "6066944449f7072dbecfde7a79ceb16ebf75777b00c28ddc996b90d8dcdf2145",
931
+ "sha256": "6698717c1e20519fa13c2961de7e863fe5c2ff9f2f420f3f289f96a9533a190e",
932
932
  "source": "skills/cabloy-spec-generation/references/traceability-and-status-rules.md",
933
933
  "target": ".cursor/skills/cabloy-spec-generation/references/traceability-and-status-rules.md"
934
934
  },
@@ -102,7 +102,7 @@ The normal path is:
102
102
  not-started -> in-progress -> implementation-complete -> verified
103
103
  ```
104
104
 
105
- A genuinely documentary/design-only task can instead follow `not-started -> in-progress -> planning-complete`, but only when its formal WBS declaration contains `Completion mode: planning-only.`, its own checks have revision-scoped documentary evidence, and a named reviewer explicitly records the planning-closure disposition. `authority-only` traceability is not this opt-in. `planning-complete` satisfies a predecessor dependency edge but never grants successor execution approval, accepts another ADR, completes source, or passes a linked prospective runtime ATP. The Gantt shows its status separately; verified totals and the burndown's active-minus-verified remainder still count only `verified`. `implementation-complete`, `waived` and `deferred` are not completed prerequisites. `blocked`, `waived`, and `deferred` are explicit states, not shortcuts to completion. Mark an implementation task `in-progress` only after approved implementation or verification actually begins. A generated file, successful scaffold, passing typecheck, code review, manual walkthrough, screenshot, or unrelated broad test pass cannot by itself produce `verified`.
105
+ A genuinely documentary/design-only task can instead follow `not-started -> in-progress -> planning-complete`, but only when its formal WBS declaration contains `Completion mode: planning-only.`, its own checks have revision-scoped documentary evidence, and a named reviewer explicitly records the planning-closure disposition. `authority-only` traceability is not this opt-in. `planning-complete` satisfies a predecessor dependency edge but never grants successor execution approval, accepts another ADR, completes source, or passes a linked prospective runtime ATP. Charts distinguish its documentary closure from verified acceptance. Verified totals count only `verified`; the burndown subtracts verified and eligible planning-complete counts from active scope for its unfinished remainder. Deferred scope is excluded; all other active statuses remain unfinished. `implementation-complete`, `waived` and `deferred` are not completed prerequisites. `blocked`, `waived`, and `deferred` are explicit states, not shortcuts to completion. Mark an implementation task `in-progress` only after approved implementation or verification actually begins. A generated file, successful scaffold, passing typecheck, code review, manual walkthrough, screenshot, or unrelated broad test pass cannot by itself produce `verified`.
106
106
 
107
107
  A task may be `implementation-complete` when source work is complete but an ATP, browser check, paired build, release gate, redaction decision, or durable artifact remains. A phase can be verified without implying that a separately defined integration or release phase is verified.
108
108
 
@@ -111,7 +111,7 @@ Setup, Procedure, Expected result, Minimum proof, and Traceability must each be
111
111
  | `WBS-DEMO-10-01` | `not-started` | None; execution has not begun. | Confirm the bounded execution dossier. |
112
112
  ```
113
113
 
114
- Resolve columns by header names (`WBS ID` and `Status`), never fixed cell positions. Additional/reordered columns are allowed. Require one row for every formal WBS task when constructing the complete chart model. Preserve the established status vocabulary and explain blockers/waivers precisely. `planning-complete` is valid only for a task declaring `Completion mode: planning-only.` in its WBS body; it satisfies a predecessor dependency edge but does not grant successor execution approval, count as verified, or reduce the verified-based burndown remainder.
114
+ Resolve columns by header names (`WBS ID` and `Status`), never fixed cell positions. Additional/reordered columns are allowed. Require one row for every formal WBS task when constructing the complete chart model. Preserve the established status vocabulary and explain blockers/waivers precisely. `planning-complete` is valid only for a task declaring `Completion mode: planning-only.` in its WBS body; it satisfies a predecessor dependency edge but does not grant successor execution approval or count as verified. The burndown reports verified and eligible planning-complete counts separately; remaining = active scope − verified − eligible planning-complete. Deferred scope is separate; all other active statuses remain unfinished.
115
115
 
116
116
  ## Three gates, not one success signal
117
117
 
@@ -41,7 +41,7 @@ When records disagree, update authority first, then downstream mappings and deri
41
41
 
42
42
  Generate/check charts only with complete supported README/WBS/ATP/progress inputs. Regenerate after `pdp-wbs.md`, `test-plan.md`, `progress.md`, or README title/language changes. An incomplete lightweight/legacy set reports missing chart inputs rather than inventing business definitions to make the generator run.
43
43
 
44
- The implementation charts are generated records, not authorities. The Gantt reads formal WBS phases/tasks/dependencies and progress status, with ATP labels only when they resolve in `test-plan.md`; absent authoritative dates or estimates must be shown as relative/illustrative order. The burndown reads active/deferred scope and verified status; without immutable dated snapshots it must be a scope-count reference rather than a calendar trend or forecast. Neither chart may add scope, dependencies, evidence, or completion claims. Both SVGs use the README-derived language consistently, including visible labels, accessibility text, and metadata.
44
+ The implementation charts are generated records, not authorities. The Gantt reads formal WBS phases/tasks/dependencies and progress status, with ATP labels only when they resolve in `test-plan.md`; absent authoritative dates or estimates must be shown as relative/illustrative order. The burndown reads active/deferred scope, verified acceptance, and eligible planning-complete closure separately; without immutable dated snapshots it must be a scope-count reference rather than a calendar trend or forecast. Neither chart may add scope, dependencies, evidence, or completion claims. Both SVGs use the README-derived language consistently, including visible labels, accessibility text, and metadata.
45
45
 
46
46
  ## README template contract
47
47
 
@@ -201,7 +201,7 @@ Initialize a new suite with statuses such as `not-started`, `deferred`, or expli
201
201
  | `waived` | A temporary exception with owner, reason, and expiry; expiry makes it a release blocker. |
202
202
  | `deferred` | Explicitly postponed scope, not completed scope. |
203
203
 
204
- A `planning-complete` predecessor satisfies its dependency edge, not the next task's design/approval gates; keep a blocked successor blocked. Verified/remaining burndown counts still count only `verified` as complete. Progress must remain a derived register. It may point to evidence but must not become a second requirements document. Include a decision-register entry for each material site strategy and show strategy/identifier deferral as a blocker only on affected frontend/site WBS branches; source reading, planning, or a selected strategy alone is not implementation evidence.
204
+ A `planning-complete` predecessor satisfies its dependency edge, not the next task's design/approval gates; keep a blocked successor blocked. Verified totals count only `verified`; the burndown separately counts eligible `planning-complete` and calculates remaining = active scope − verified − eligible planning-complete. Deferred scope is separate; all other active statuses remain unfinished. Progress must remain a derived register. It may point to evidence but must not become a second requirements document. Include a decision-register entry for each material site strategy and show strategy/identifier deferral as a blocker only on affected frontend/site WBS branches; source reading, planning, or a selected strategy alone is not implementation evidence.
205
205
 
206
206
  ## ADR 0001 template contract
207
207
 
@@ -84,7 +84,7 @@ Downstream records do not silently override authority. Charts cannot authorize s
84
84
  | `waived` | A temporary exception explicitly approved with owner, reason, and expiry. |
85
85
  | `deferred` | Explicitly postponed scope; it is not complete or verified. |
86
86
 
87
- For a newly created plan, initialize delivery rows as `not-started`, `deferred`, or `blocked` as appropriate. Creating Markdown files never makes a task `planning-complete`, implementation `implementation-complete`, or ATP `verified`. Only a formally declared `Completion mode: planning-only.` task with documentary acceptance checks, revision-scoped evidence and an explicit named-reviewer closure disposition may reach `planning-complete`; `authority-only` traceability does not establish this eligibility. A planning-complete predecessor satisfies its WBS dependency edge, but blocked/deferred/waived successors stay gated and require their own decisions and approvals. `implementation-complete`, `waived` and `deferred` do not satisfy predecessor completion. Only `verified` contributes to verified totals and the verified-based burndown remainder.
87
+ For a newly created plan, initialize delivery rows as `not-started`, `deferred`, or `blocked` as appropriate. Creating Markdown files never makes a task `planning-complete`, implementation `implementation-complete`, or ATP `verified`. Only a formally declared `Completion mode: planning-only.` task with documentary acceptance checks, revision-scoped evidence and an explicit named-reviewer closure disposition may reach `planning-complete`; `authority-only` traceability does not establish this eligibility. A planning-complete predecessor satisfies its WBS dependency edge, but blocked/deferred/waived successors stay gated and require their own decisions and approvals. `implementation-complete`, `waived` and `deferred` do not satisfy predecessor completion. Only `verified` contributes to verified totals. The burndown separately reports eligible `planning-complete` closure and subtracts both from active scope to calculate unfinished work; deferred scope is excluded, and all other active statuses remain unfinished.
88
88
 
89
89
  ### README and ADR decision-status consistency
90
90
 
@@ -49,9 +49,14 @@ const COPY = {
49
49
  'Dependency readiness is not execution approval; descriptive gates still require review.',
50
50
  ganttDescription: (count, reviewed) =>
51
51
  `${count} formal WBS tasks. Current derived status reviewed ${reviewed}.`,
52
- burndownDescription: (total, verified, remaining) =>
53
- `${total} active WBS items; ${verified} verified; ${remaining} remaining.`,
54
- phaseSummary: (number, verified, total) => `Phase ${number}: ${verified} / ${total} verified`,
52
+ burndownDescription: (total, verified, planningComplete, remaining) =>
53
+ `${total} active WBS items; ${verified} verified; ${planningComplete} planning complete; ${remaining} remaining.`,
54
+ phaseSummary: (number, total, verified, planningComplete, remaining) =>
55
+ `Phase ${number}: ${total} active; ${verified} verified; ${planningComplete} planning complete; ${remaining} remaining`,
56
+ planningComplete: 'planning complete',
57
+ completed: 'Completed',
58
+ completionCounts: (verified, planningComplete) =>
59
+ `${verified} verified · ${planningComplete} planning complete`,
55
60
  statuses: {
56
61
  'not-started': 'Not started',
57
62
  'in-progress': 'In progress',
@@ -65,14 +70,14 @@ const COPY = {
65
70
  source: 'Sources',
66
71
  reviewed: 'Last reviewed',
67
72
  scopeReference: 'Scope-count reference',
68
- notTime: 'Verified WBS items in recorded scope — not time',
73
+ notTime: 'Completed WBS items (verified + planning complete) — not time',
69
74
  remainingAxis: 'WBS items remaining',
70
75
  logicalBaseline: 'Logical recorded-scope baseline',
71
76
  currentSnapshot: 'Current snapshot',
72
77
  remainingShort: 'remaining',
73
78
  emptyScope: 'No active WBS items remain in scope.',
74
79
  noHistory: 'No observed intermediate status points are available.',
75
- formula: 'remaining = active scope − verified',
80
+ formula: 'remaining = active scope − verified − planning complete',
76
81
  phaseScope: 'Scope by phase',
77
82
  active: 'Active WBS items',
78
83
  deferred: 'Deferred',
@@ -107,9 +112,14 @@ const COPY = {
107
112
  candidateCaveat: '依赖就绪不代表实施批准;描述性门槛仍需审查。',
108
113
  ganttDescription: (count, reviewed) =>
109
114
  `${count} 项正式 WBS 工作。当前派生状态的审查日期:${reviewed}。`,
110
- burndownDescription: (total, verified, remaining) =>
111
- `${total} 项活跃 WBS 工作;${verified} 项已核验;${remaining} 项剩余。`,
112
- phaseSummary: (number, verified, total) => `阶段 ${number}:${verified} / ${total} 项已核验`,
115
+ burndownDescription: (total, verified, planningComplete, remaining) =>
116
+ `${total} 项活跃 WBS 工作;${verified} 项已核验;${planningComplete} 项规划完成;${remaining} 项剩余。`,
117
+ phaseSummary: (number, total, verified, planningComplete, remaining) =>
118
+ `阶段 ${number}:${total} 项活跃;${verified} 项已核验;${planningComplete} 项规划完成;${remaining} 项剩余`,
119
+ planningComplete: '规划完成',
120
+ completed: '已完成',
121
+ completionCounts: (verified, planningComplete) =>
122
+ `${verified} 已核验 · ${planningComplete} 规划完成`,
113
123
  statuses: {
114
124
  'not-started': '未开始',
115
125
  'in-progress': '进行中',
@@ -123,14 +133,14 @@ const COPY = {
123
133
  source: '来源',
124
134
  reviewed: '最后审查日期',
125
135
  scopeReference: '范围计数参考',
126
- notTime: '已核验的记录范围 WBS 项——不是时间',
136
+ notTime: '已完成 WBS 项(已核验 + 规划完成)——不是时间',
127
137
  remainingAxis: '剩余 WBS 项',
128
138
  logicalBaseline: '逻辑上的记录范围基线',
129
139
  currentSnapshot: '当前快照',
130
140
  remainingShort: '剩余',
131
141
  emptyScope: '当前范围内没有活跃 WBS 项。',
132
142
  noHistory: '没有可用的中间历史状态点。',
133
- formula: '剩余 = 活跃范围 − 已核验',
143
+ formula: '剩余 = 活跃范围 − 已核验 − 规划完成',
134
144
  phaseScope: '按阶段统计范围',
135
145
  active: '活跃 WBS 项',
136
146
  deferred: '已延期',
@@ -236,6 +246,15 @@ function languageFromReadme(readme) {
236
246
  return cjk > latin / 3 ? 'zh' : 'en';
237
247
  }
238
248
 
249
+ function completionCounts(tasks) {
250
+ const verified = tasks.filter(task => task.status === 'verified').length;
251
+ const planningComplete = tasks.filter(
252
+ task => task.status === 'planning-complete' && isPlanningCompleteEligible(task),
253
+ ).length;
254
+ const completed = verified + planningComplete;
255
+ return { verified, planningComplete, completed, remaining: tasks.length - completed };
256
+ }
257
+
239
258
  export function createChartModel({ readme, wbs, progress, testPlan }) {
240
259
  const language = languageFromReadme(readme);
241
260
  const atpIds = parseAtpIds(testPlan);
@@ -275,7 +294,7 @@ export function createChartModel({ readme, wbs, progress, testPlan }) {
275
294
  }
276
295
  const activeTasks = tasks.filter(task => !task.deferred && task.status !== 'deferred');
277
296
  const deferredTasks = tasks.filter(task => !activeTasks.includes(task));
278
- const verified = activeTasks.filter(task => task.status === 'verified').length;
297
+ const counts = completionCounts(activeTasks);
279
298
  return {
280
299
  language,
281
300
  copy: COPY[language],
@@ -284,7 +303,7 @@ export function createChartModel({ readme, wbs, progress, testPlan }) {
284
303
  activeTasks,
285
304
  deferredTasks,
286
305
  expandedEdges,
287
- verified,
306
+ ...counts,
288
307
  reviewed: reviewed === 'not recorded' ? COPY[language].notRecorded : reviewed,
289
308
  };
290
309
  }
@@ -441,9 +460,19 @@ export function renderGantt(model, suiteName) {
441
460
  }
442
461
 
443
462
  export function renderBurndown(model, suiteName) {
444
- const { copy: t, phases, activeTasks, deferredTasks, verified, reviewed, language } = model;
463
+ const {
464
+ copy: t,
465
+ phases,
466
+ activeTasks,
467
+ deferredTasks,
468
+ verified,
469
+ planningComplete,
470
+ completed,
471
+ remaining,
472
+ reviewed,
473
+ language,
474
+ } = model;
445
475
  const total = activeTasks.length;
446
- const remaining = total - verified;
447
476
  const phaseBottom = 836 + Math.max(0, phases.length - 1) * 28;
448
477
  const footerY = Math.max(976, phaseBottom + 50);
449
478
  const height = footerY + 74;
@@ -451,7 +480,7 @@ export function renderBurndown(model, suiteName) {
451
480
  const x1 = 910;
452
481
  const y0 = 410;
453
482
  const y1 = 650;
454
- const completion = total ? verified / total : 0;
483
+ const completion = total ? completed / total : 0;
455
484
  const currentX = x0 + completion * (x1 - x0);
456
485
  const currentY = y0 + completion * (y1 - y0);
457
486
  const currentMarker =
@@ -461,10 +490,31 @@ export function renderBurndown(model, suiteName) {
461
490
  const phaseRows = phases
462
491
  .map((phase, index) => {
463
492
  const phaseTasks = phase.tasks.filter(task => !task.deferred && task.status !== 'deferred');
464
- const phaseVerified = phaseTasks.filter(task => task.status === 'verified').length;
493
+ const counts = completionCounts(phaseTasks);
494
+ const summary = t.phaseSummary(
495
+ phase.number,
496
+ phaseTasks.length,
497
+ counts.verified,
498
+ counts.planningComplete,
499
+ counts.remaining,
500
+ );
501
+ const status = !phaseTasks.length
502
+ ? 'deferred'
503
+ : counts.remaining
504
+ ? 'in-progress'
505
+ : counts.planningComplete
506
+ ? 'planning-complete'
507
+ : 'verified';
508
+ const label = !phaseTasks.length
509
+ ? t.emptyScope
510
+ : counts.remaining
511
+ ? `${counts.completed}/${phaseTasks.length}`
512
+ : counts.planningComplete && counts.verified
513
+ ? t.completed
514
+ : statusLabel(status, language);
465
515
  const y = 832 + index * 28;
466
- const width = phaseTasks.length ? (phaseVerified / phaseTasks.length) * 210 : 0;
467
- return `<g class="row" tabindex="0" aria-label="${escapeXml(t.phaseSummary(phase.number, phaseVerified, phaseTasks.length))}"><title>${escapeXml(`${t.phaseSummary(phase.number, phaseVerified, phaseTasks.length)} · ${phase.title}`)}</title><text class="task" x="96" y="${y}">${escapeXml(`${t.phase} ${phase.number} · ${phase.title}`)}</text><text class="num" x="730" y="${y}" text-anchor="end">${phaseVerified} / ${phaseTasks.length}</text><text class="num" x="850" y="${y}" text-anchor="end">${phaseTasks.length - phaseVerified}</text><rect x="930" y="${y - 11}" width="210" height="12" rx="6" fill="var(--blue-wash)"/><rect x="930" y="${y - 11}" width="${width}" height="12" rx="6" fill="var(--aqua)"/>${statusIcon(1215, y - 5, phaseVerified === phaseTasks.length ? 'verified' : 'in-progress')}<text class="small" x="1228" y="${y}">${escapeXml(phaseVerified === phaseTasks.length ? statusLabel('verified', language) : `${phaseVerified}/${phaseTasks.length}`)}</text></g>`;
516
+ const width = phaseTasks.length ? (counts.completed / phaseTasks.length) * 210 : 0;
517
+ return `<g class="row" tabindex="0" aria-label="${escapeXml(summary)}"><title>${escapeXml(`${summary} · ${phase.title}`)}</title><text class="task" x="96" y="${y}">${escapeXml(`${t.phase} ${phase.number} · ${phase.title}`)}</text><text class="num" x="650" y="${y}" text-anchor="end">${counts.verified}</text><text class="num" x="750" y="${y}" text-anchor="end">${counts.planningComplete}</text><text class="num" x="850" y="${y}" text-anchor="end">${counts.remaining}</text><rect x="930" y="${y - 11}" width="210" height="12" rx="6" fill="var(--blue-wash)"/><rect x="930" y="${y - 11}" width="${width}" height="12" rx="6" fill="var(--aqua)"/>${statusIcon(1215, y - 5, status)}<text class="small" x="1228" y="${y}">${escapeXml(label)}</text></g>`;
468
518
  })
469
519
  .join('\n');
470
520
  let explanationY = 412;
@@ -478,19 +528,20 @@ export function renderBurndown(model, suiteName) {
478
528
  .join('');
479
529
  const body = `<rect class="canvas" width="1600" height="${height}"/><rect class="card" x="32" y="28" width="1536" height="${height - 56}" rx="14"/>
480
530
  <text class="title" x="72" y="84">${escapeXml(`${suiteName} ${t.burndownTitle}`)}</text><text class="subtitle" x="72" y="110">${escapeXml(t.burndownSubtitle)}</text><text class="small" x="1500" y="84" text-anchor="end">${escapeXml(`${t.reviewed}: ${reviewed}`)}</text>
481
- <rect x="72" y="142" width="1428" height="116" rx="8" fill="var(--aqua-wash)"/><text class="heading" x="96" y="170">${escapeXml(t.current)}</text><text x="96" y="231" class="title" style="font-size:52px">${remaining}</text><text class="heading" x="155" y="215">${escapeXml(t.remaining)}</text><text class="caption" x="155" y="237">${escapeXml(`${verified} / ${total} ${t.verified}`)}</text><text class="caption" x="700" y="205">${escapeXml(t.approved)}</text><text class="heading" x="700" y="230">${total}</text><text class="caption" x="1030" y="205">${escapeXml(t.deferred)}</text><text class="heading" x="1030" y="230">${deferredTasks.length}</text>
531
+ <rect x="72" y="142" width="1428" height="116" rx="8" fill="var(--aqua-wash)"/><text class="heading" x="96" y="170">${escapeXml(t.current)}</text><text x="96" y="231" class="title" style="font-size:52px">${remaining}</text><text class="heading" x="155" y="215">${escapeXml(t.remaining)}</text><text class="caption" x="155" y="237">${escapeXml(t.completionCounts(verified, planningComplete))}</text><text class="caption" x="700" y="205">${escapeXml(t.approved)}</text><text class="heading" x="700" y="230">${total}</text><text class="caption" x="1030" y="205">${escapeXml(t.deferred)}</text><text class="heading" x="1030" y="230">${deferredTasks.length}</text>
482
532
  <text class="heading" x="72" y="300">${escapeXml(t.scopeReference)}</text><text class="caption" x="72" y="321">${escapeXml(t.noHistory)}</text><rect class="card" x="72" y="340" width="940" height="390" rx="9"/><text class="small" x="116" y="372">${escapeXml(t.remainingAxis)}</text><text class="small" x="550" y="704" text-anchor="middle">${escapeXml(t.notTime)}</text>
483
533
  <g class="grid"><line x1="${x0}" y1="${y0}" x2="${x1}" y2="${y0}"/><line x1="${x0}" y1="${y0 + 80}" x2="${x1}" y2="${y0 + 80}"/><line x1="${x0}" y1="${y0 + 160}" x2="${x1}" y2="${y0 + 160}"/><line x1="${x0}" y1="${y1}" x2="${x1}" y2="${y1}"/><line x1="${x0}" y1="${y0}" x2="${x0}" y2="${y1}"/><line x1="${x1}" y1="${y0}" x2="${x1}" y2="${y1}"/></g><line class="axis" x1="${x0}" y1="${y1}" x2="${x1}" y2="${y1}"/><line class="axis" x1="${x0}" y1="${y0}" x2="${x0}" y2="${y1}"/>
484
- <line x1="${x0}" y1="${y0}" x2="${currentX.toFixed(1)}" y2="${currentY.toFixed(1)}" stroke="var(--blue)" stroke-width="2"/><line x1="${currentX.toFixed(1)}" y1="${currentY.toFixed(1)}" x2="${x1}" y2="${y1}" stroke="var(--blue)" stroke-width="2" stroke-dasharray="6 5"/><circle cx="${x0}" cy="${y0}" r="7" fill="var(--blue)" stroke="var(--surface)" stroke-width="2"/><text class="heading" x="${x0 + 22}" y="${y0 + 18}">${escapeXml(t.logicalBaseline)}</text><text class="caption" x="${x0 + 22}" y="${y0 + 37}">${total} ${escapeXml(t.remainingShort)}</text>${currentMarker}<text class="heading" x="${Math.max(x0 + 150, currentX - 22)}" y="${Math.max(y0 + 60, currentY - 49)}" text-anchor="end">${escapeXml(t.currentSnapshot)}</text><text class="caption" x="${Math.max(x0 + 150, currentX - 22)}" y="${Math.max(y0 + 79, currentY - 30)}" text-anchor="end">${escapeXml(`${remaining} ${t.remainingShort} · ${verified} ${t.verified}`)}</text>
534
+ <line x1="${x0}" y1="${y0}" x2="${currentX.toFixed(1)}" y2="${currentY.toFixed(1)}" stroke="var(--blue)" stroke-width="2"/><line x1="${currentX.toFixed(1)}" y1="${currentY.toFixed(1)}" x2="${x1}" y2="${y1}" stroke="var(--blue)" stroke-width="2" stroke-dasharray="6 5"/><circle cx="${x0}" cy="${y0}" r="7" fill="var(--blue)" stroke="var(--surface)" stroke-width="2"/><text class="heading" x="${x0 + 22}" y="${y0 + 18}">${escapeXml(t.logicalBaseline)}</text><text class="caption" x="${x0 + 22}" y="${y0 + 37}">${total} ${escapeXml(t.remainingShort)}</text>${currentMarker}<text class="heading" x="${Math.max(x0 + 150, currentX - 22)}" y="${Math.max(y0 + 60, currentY - 49)}" text-anchor="end">${escapeXml(t.currentSnapshot)}</text><text class="caption" x="${Math.max(x0 + 150, currentX - 22)}" y="${Math.max(y0 + 79, currentY - 30)}" text-anchor="end">${escapeXml(`${remaining} ${t.remainingShort} · ${t.completionCounts(verified, planningComplete)}`)}</text>
485
535
  <rect x="1040" y="340" width="460" height="390" rx="9" fill="var(--blue-wash)"/><text class="heading" x="1070" y="378">${escapeXml(t.howRead)}</text>${explanation}
486
- <rect class="card" x="72" y="758" width="1428" height="${phaseBottom + 24 - 758}" rx="9"/><text class="heading" x="96" y="786">${escapeXml(t.phaseScope)}</text><text class="small" x="96" y="800">${escapeXml(`${t.active}: ${total}; ${t.verified}: ${verified}; ${t.remaining}: ${remaining}`)}</text><text class="small" x="730" y="786" text-anchor="end">${escapeXml(t.verified)}</text><text class="small" x="850" y="786" text-anchor="end">${escapeXml(t.remaining)}</text><text class="small" x="930" y="786">${escapeXml(t.completion)}</text>${phaseRows}
536
+ <rect class="card" x="72" y="758" width="1428" height="${phaseBottom + 24 - 758}" rx="9"/><text class="heading" x="96" y="786">${escapeXml(t.phaseScope)}</text><text class="small" x="96" y="800">${escapeXml(`${t.active}: ${total}; ${t.verified}: ${verified}; ${t.planningComplete}: ${planningComplete}; ${t.remaining}: ${remaining}`)}</text><text class="small" x="650" y="786" text-anchor="end">${escapeXml(t.verified)}</text><text class="small" x="750" y="786" text-anchor="end">${escapeXml(t.planningComplete)}</text><text class="small" x="850" y="786" text-anchor="end">${escapeXml(t.remaining)}</text><text class="small" x="930" y="786">${escapeXml(t.completion)}</text>${phaseRows}
487
537
  <line class="grid" x1="72" y1="${height - 74}" x2="1500" y2="${height - 74}"/><text class="small" x="72" y="${height - 48}">${escapeXml(`${t.source}: pdp-wbs.md · progress.md (${t.reviewed}: ${reviewed})`)}</text><text class="small" x="1500" y="${height - 48}" text-anchor="end">${escapeXml(t.caveat)}</text>`;
488
538
  return svgDocument(
489
539
  `${suiteName} ${t.burndownTitle}`,
490
- `${t.burndownTitle}. ${t.burndownDescription(total, verified, remaining)} ${t.noHistory}`,
540
+ `${t.burndownTitle}. ${t.burndownDescription(total, verified, planningComplete, remaining)} ${t.noHistory}`,
491
541
  body,
492
542
  height,
493
543
  t.metadata,
544
+ planningComplete > 0,
494
545
  );
495
546
  }
496
547