@openprd/cli 0.1.9 → 0.1.11

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 (138) hide show
  1. package/.openprd/benchmarks/index.md +16 -0
  2. package/.openprd/benchmarks/sources.yaml +53 -1
  3. package/.openprd/config.yaml +10 -0
  4. package/.openprd/design/README.md +47 -0
  5. package/.openprd/design/README_EN.md +25 -0
  6. package/.openprd/design/active/asset-spec.md +15 -0
  7. package/.openprd/design/active/direction-plan.md +7 -0
  8. package/.openprd/design/active/facts-sheet.md +13 -0
  9. package/.openprd/design/active/image-preflight.md +9 -0
  10. package/.openprd/design/active/selected-direction.md +11 -0
  11. package/.openprd/design/anti-slop.md +16 -0
  12. package/.openprd/design/assets/dark-mesh.svg +14 -0
  13. package/.openprd/design/assets/paper-grid.svg +14 -0
  14. package/.openprd/design/assets/surface-presets.md +21 -0
  15. package/.openprd/design/assets/workbench-grid.svg +16 -0
  16. package/.openprd/design/checklists/ui-quality-gate.md +22 -0
  17. package/.openprd/design/components/component-catalog.md +42 -0
  18. package/.openprd/design/layouts/layout-catalog.json +45 -0
  19. package/.openprd/design/lenses/frontend-lenses.md +31 -0
  20. package/.openprd/design/recipes/content-experience.md +26 -0
  21. package/.openprd/design/recipes/operational-tool.md +25 -0
  22. package/.openprd/design/recipes/product-launch.md +26 -0
  23. package/.openprd/design/templates/README.md +36 -0
  24. package/.openprd/design/templates/README_EN.md +31 -0
  25. package/.openprd/design/templates/content-home.html +415 -0
  26. package/.openprd/design/templates/ops-dashboard.html +340 -0
  27. package/.openprd/design/templates/product-launch.html +360 -0
  28. package/.openprd/design/themes/theme-catalog.json +77 -0
  29. package/.openprd/engagements/active/prd.md +111 -100
  30. package/.openprd/schema/prd.schema.yaml +25 -0
  31. package/.openprd/templates/base/intake.md +17 -0
  32. package/.openprd/templates/base/prd.md +31 -6
  33. package/AGENTS.md +10 -7
  34. package/README.md +26 -28
  35. package/README_EN.md +3 -3
  36. package/package.json +41 -2
  37. package/scripts/openprd-codex-isolated-worker.mjs +799 -0
  38. package/skills/openprd-benchmark-router/SKILL.md +5 -0
  39. package/skills/openprd-frontend-design/SKILL.md +161 -0
  40. package/skills/openprd-frontend-design/references/design-asset-contract.md +34 -0
  41. package/skills/openprd-frontend-design/references/direction-engine.md +46 -0
  42. package/skills/openprd-harness/SKILL.md +37 -20
  43. package/skills/openprd-learning-review/SKILL.md +2 -1
  44. package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +1 -1
  45. package/skills/openprd-quality/SKILL.md +5 -3
  46. package/skills/openprd-requirement-intake/SKILL.md +27 -5
  47. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -3
  48. package/skills/openprd-requirement-intake/references/routing-rubric.md +9 -5
  49. package/skills/openprd-requirement-intake/references/startup-validation-lens.md +108 -0
  50. package/skills/openprd-router/SKILL.md +13 -5
  51. package/skills/openprd-shared/SKILL.md +38 -25
  52. package/src/agent-integration.js +120 -69
  53. package/src/brainstorm-artifacts.js +1805 -0
  54. package/src/brainstorm-presentation.js +233 -0
  55. package/src/brainstorm.js +766 -0
  56. package/src/cli/args.js +51 -3
  57. package/src/cli/doctor-print.js +34 -8
  58. package/src/cli/print.js +6 -0
  59. package/src/cli/quality-print.js +79 -3
  60. package/src/cli/run-print.js +43 -1
  61. package/src/cli/shared-print.js +33 -2
  62. package/src/cli/workflow-print.js +21 -0
  63. package/src/codex-hook-runner-template.mjs +838 -58
  64. package/src/design-starter-support.js +462 -0
  65. package/src/design-starter.js +1207 -0
  66. package/src/diagram-core.js +384 -151
  67. package/src/html-artifacts.js +403 -50
  68. package/src/knowledge.js +291 -32
  69. package/src/learning-review.js +3 -3
  70. package/src/loop.js +839 -101
  71. package/src/openprd.js +86 -1
  72. package/src/openspec/change-validate.js +1 -0
  73. package/src/prd-core.js +119 -2
  74. package/src/quality-html-artifact.js +30 -5
  75. package/src/quality-learning.js +50 -6
  76. package/src/quality.js +22 -12
  77. package/src/run-harness.js +191 -29
  78. package/src/self-update.js +301 -4
  79. package/src/standards.js +46 -5
  80. package/src/visual-prepare.js +940 -0
  81. package/src/workspace-core.js +175 -0
  82. package/src/workspace-workflow.js +307 -7
  83. package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +0 -14
  84. package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +0 -14
  85. package/.openprd/discovery/config.json +0 -35
  86. package/.openprd/engagements/active/flows.md +0 -35
  87. package/.openprd/engagements/active/handoff.md +0 -16
  88. package/.openprd/engagements/active/review.html +0 -61
  89. package/.openprd/engagements/active/roles.md +0 -22
  90. package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +0 -23
  91. package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +0 -18
  92. package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +0 -18
  93. package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +0 -18
  94. package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +0 -18
  95. package/.openprd/exports/.gitkeep +0 -0
  96. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +0 -78
  97. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +0 -129
  98. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +0 -41
  99. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +0 -14
  100. package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +0 -49
  101. package/.openprd/knowledge/index.json +0 -47
  102. package/.openprd/reviews/v0001.html +0 -1322
  103. package/.openprd/reviews/v0002.html +0 -1150
  104. package/.openprd/reviews/v0003.html +0 -1150
  105. package/.openprd/reviews/v0004.html +0 -1150
  106. package/.openprd/reviews/v0005.html +0 -1150
  107. package/.openprd/sessions/.gitkeep +0 -0
  108. package/.openprd/state/changes.json +0 -27
  109. package/.openprd/state/current.json +0 -505
  110. package/.openprd/state/release-ledger.json +0 -387
  111. package/.openprd/state/version-index.json +0 -67
  112. package/.openprd/state/versions/.gitkeep +0 -0
  113. package/.openprd/state/versions/v0001.json +0 -121
  114. package/.openprd/state/versions/v0001.md +0 -161
  115. package/.openprd/state/versions/v0002.json +0 -264
  116. package/.openprd/state/versions/v0002.md +0 -183
  117. package/.openprd/state/versions/v0003.json +0 -269
  118. package/.openprd/state/versions/v0003.md +0 -188
  119. package/.openprd/state/versions/v0004.json +0 -274
  120. package/.openprd/state/versions/v0004.md +0 -193
  121. package/.openprd/state/versions/v0005.json +0 -299
  122. package/.openprd/state/versions/v0005.md +0 -189
  123. package/docs/assets/openprd-capability-overview-en.png +0 -0
  124. package/docs/assets/openprd-capability-overview-zh.png +0 -0
  125. package/docs/assets/openprd-learning-html.png +0 -0
  126. package/docs/assets/openprd-quality-html.png +0 -0
  127. package/docs/assets/openprd-requirement-routing-en.png +0 -0
  128. package/docs/assets/openprd-requirement-routing-en.svg +0 -102
  129. package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
  130. package/docs/assets/openprd-requirement-routing-zh.png +0 -0
  131. package/docs/assets/openprd-requirement-routing-zh.svg +0 -102
  132. package/docs/assets/openprd-review-html.png +0 -0
  133. package/docs/assets/openprd-scenario-overview.png +0 -0
  134. package/docs/assets/openprd-scenario-overview.svg +0 -114
  135. package/docs/assets/openprd-self-evolving-mechanisms-en.png +0 -0
  136. package/docs/assets/openprd-self-evolving-mechanisms-zh.png +0 -0
  137. package/docs/assets/openprd-visual-compare-case-study-en.png +0 -0
  138. package/docs/assets/openprd-visual-compare-case-study-zh.png +0 -0
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { readWorkspaceRegistry } from './workspace-registry.js';
5
6
 
6
7
  export const DEFAULT_SELF_UPDATE_SOURCE = '@openprd/cli@latest';
7
8
 
@@ -20,6 +21,139 @@ export function formatCommand(command, args = []) {
20
21
  return [command, ...args].map(shellQuote).join(' ');
21
22
  }
22
23
 
24
+ function normalizeVersionText(value) {
25
+ const raw = String(value ?? '').trim();
26
+ if (!raw) {
27
+ return null;
28
+ }
29
+ return raw.startsWith('v') ? raw.slice(1) : raw;
30
+ }
31
+
32
+ function parseSemverParts(value) {
33
+ const normalized = normalizeVersionText(value);
34
+ const match = normalized?.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/u);
35
+ if (!match) {
36
+ return null;
37
+ }
38
+ return {
39
+ major: Number.parseInt(match[1], 10),
40
+ minor: Number.parseInt(match[2], 10),
41
+ patch: Number.parseInt(match[3], 10),
42
+ prerelease: match[4] ?? null,
43
+ build: match[5] ?? null,
44
+ };
45
+ }
46
+
47
+ function comparePrereleaseIdentifiers(left, right) {
48
+ const leftParts = String(left ?? '').split('.');
49
+ const rightParts = String(right ?? '').split('.');
50
+ const max = Math.max(leftParts.length, rightParts.length);
51
+ for (let index = 0; index < max; index += 1) {
52
+ const leftPart = leftParts[index];
53
+ const rightPart = rightParts[index];
54
+ if (leftPart === undefined) {
55
+ return -1;
56
+ }
57
+ if (rightPart === undefined) {
58
+ return 1;
59
+ }
60
+ const leftNumeric = /^\d+$/u.test(leftPart);
61
+ const rightNumeric = /^\d+$/u.test(rightPart);
62
+ if (leftNumeric && rightNumeric) {
63
+ const diff = Number.parseInt(leftPart, 10) - Number.parseInt(rightPart, 10);
64
+ if (diff !== 0) {
65
+ return diff > 0 ? 1 : -1;
66
+ }
67
+ continue;
68
+ }
69
+ if (leftNumeric !== rightNumeric) {
70
+ return leftNumeric ? -1 : 1;
71
+ }
72
+ const diff = leftPart.localeCompare(rightPart);
73
+ if (diff !== 0) {
74
+ return diff > 0 ? 1 : -1;
75
+ }
76
+ }
77
+ return 0;
78
+ }
79
+
80
+ export function compareVersions(left, right) {
81
+ const leftParts = parseSemverParts(left);
82
+ const rightParts = parseSemverParts(right);
83
+ if (!leftParts || !rightParts) {
84
+ return null;
85
+ }
86
+ for (const field of ['major', 'minor', 'patch']) {
87
+ if (leftParts[field] !== rightParts[field]) {
88
+ return leftParts[field] > rightParts[field] ? 1 : -1;
89
+ }
90
+ }
91
+ if (!leftParts.prerelease && !rightParts.prerelease) {
92
+ return 0;
93
+ }
94
+ if (!leftParts.prerelease) {
95
+ return 1;
96
+ }
97
+ if (!rightParts.prerelease) {
98
+ return -1;
99
+ }
100
+ return comparePrereleaseIdentifiers(leftParts.prerelease, rightParts.prerelease);
101
+ }
102
+
103
+ function packageNameFromSource(source, fallback = '@openprd/cli') {
104
+ const text = String(source ?? '').trim();
105
+ if (!text) {
106
+ return fallback;
107
+ }
108
+ const scopedMatch = text.match(/^(@[^/]+\/[^@]+)(?:@.+)?$/u);
109
+ if (scopedMatch) {
110
+ return scopedMatch[1];
111
+ }
112
+ const unscopedMatch = text.match(/^([^@]+)(?:@.+)?$/u);
113
+ return unscopedMatch?.[1] ?? fallback;
114
+ }
115
+
116
+ function parseVersionOutput(text) {
117
+ const raw = String(text ?? '').trim();
118
+ if (!raw) {
119
+ return null;
120
+ }
121
+ try {
122
+ const parsed = JSON.parse(raw);
123
+ if (typeof parsed === 'string') {
124
+ return normalizeVersionText(parsed);
125
+ }
126
+ if (Array.isArray(parsed)) {
127
+ const first = parsed.find((item) => typeof item === 'string' && item.trim());
128
+ return first ? normalizeVersionText(first) : null;
129
+ }
130
+ } catch {
131
+ // Fall back to plain text parsing.
132
+ }
133
+ const firstLine = raw.split(/\r?\n/u).map((line) => line.trim()).find(Boolean);
134
+ return firstLine ? normalizeVersionText(firstLine) : null;
135
+ }
136
+
137
+ function versionComparisonLabel(currentVersion, targetVersion) {
138
+ const comparison = compareVersions(currentVersion, targetVersion);
139
+ if (comparison === null) {
140
+ return 'unknown';
141
+ }
142
+ if (comparison < 0) {
143
+ return 'behind';
144
+ }
145
+ if (comparison > 0) {
146
+ return 'ahead';
147
+ }
148
+ return 'same';
149
+ }
150
+
151
+ function refreshCandidateNote(workspaceRoot) {
152
+ return /(^|\/)(archive|archives|归档)(\/|$)/iu.test(String(workspaceRoot ?? ''))
153
+ ? '归档项目,建议先确认'
154
+ : '可直接处理';
155
+ }
156
+
23
157
  async function readPackageInfo(packageRoot = DEFAULT_PACKAGE_ROOT) {
24
158
  const packageJsonPath = path.join(packageRoot, 'package.json');
25
159
  const raw = await fs.readFile(packageJsonPath, 'utf8');
@@ -101,6 +235,109 @@ async function resolveOpenPrdExecutable(deps = {}) {
101
235
  };
102
236
  }
103
237
 
238
+ async function inspectPublishedVersion(packageInfo, options = {}, deps = {}) {
239
+ const runCommand = deps.runCommand ?? runProcess;
240
+ const packageName = packageInfo.name ?? packageNameFromSource(options.source ?? DEFAULT_SELF_UPDATE_SOURCE);
241
+ const command = 'npm';
242
+ const args = ['view', packageName, 'version', '--json'];
243
+ const result = await runCommand(command, args, {
244
+ cwd: options.cwd ?? packageInfo.packageRoot ?? process.cwd(),
245
+ env: options.env ?? process.env,
246
+ });
247
+ const publishedVersion = result.ok ? parseVersionOutput(result.stdout) : null;
248
+ const errors = [];
249
+ if (!result.ok) {
250
+ errors.push(result.stderr.trim() || `npm view failed with exit code ${result.exitCode}.`);
251
+ } else if (!publishedVersion) {
252
+ errors.push('Unable to parse the published npm version for @openprd/cli.');
253
+ }
254
+ const comparison = versionComparisonLabel(packageInfo.version, publishedVersion);
255
+ return {
256
+ ok: errors.length === 0,
257
+ packageName,
258
+ currentVersion: normalizeVersionText(packageInfo.version),
259
+ publishedVersion,
260
+ updateAvailable: comparison === 'behind',
261
+ comparison,
262
+ command: {
263
+ command,
264
+ args,
265
+ display: formatCommand(command, args),
266
+ },
267
+ result,
268
+ errors,
269
+ };
270
+ }
271
+
272
+ async function resolveExecutableVersion(executable, options = {}, deps = {}) {
273
+ const runCommand = deps.runCommand ?? runProcess;
274
+ const result = await runCommand(executable, ['--version'], {
275
+ cwd: options.cwd ?? process.cwd(),
276
+ env: options.env ?? process.env,
277
+ });
278
+ const version = result.ok ? parseVersionOutput(result.stdout || result.stderr) : null;
279
+ const errors = [];
280
+ if (!result.ok) {
281
+ errors.push(result.stderr.trim() || `Version check failed with exit code ${result.exitCode}.`);
282
+ } else if (!version) {
283
+ errors.push('Unable to parse the installed OpenPrd version after update.');
284
+ }
285
+ return {
286
+ ok: errors.length === 0,
287
+ executable,
288
+ version,
289
+ command: {
290
+ command: executable,
291
+ args: ['--version'],
292
+ display: formatCommand(executable, ['--version']),
293
+ },
294
+ result,
295
+ errors,
296
+ };
297
+ }
298
+
299
+ async function collectRefreshCandidates(targetVersion, options = {}, deps = {}) {
300
+ const normalizedTargetVersion = normalizeVersionText(targetVersion);
301
+ if (!normalizedTargetVersion) {
302
+ return {
303
+ ok: true,
304
+ targetVersion: null,
305
+ registryPath: null,
306
+ staleCount: 0,
307
+ total: 0,
308
+ projects: [],
309
+ };
310
+ }
311
+ const registry = await (deps.readWorkspaceRegistry ?? readWorkspaceRegistry)({
312
+ openprdHome: options.openprdHome,
313
+ });
314
+ const projects = registry.entries
315
+ .filter((entry) => {
316
+ if (!entry.openprdVersion) {
317
+ return false;
318
+ }
319
+ return compareVersions(entry.openprdVersion, normalizedTargetVersion) === -1;
320
+ })
321
+ .map((entry) => ({
322
+ workspaceRoot: entry.workspaceRoot,
323
+ workspaceName: entry.workspaceName ?? path.basename(entry.workspaceRoot),
324
+ currentVersion: normalizeVersionText(entry.openprdVersion),
325
+ targetVersion: normalizedTargetVersion,
326
+ suggestedAction: '刷新 OpenPrD',
327
+ note: refreshCandidateNote(entry.workspaceRoot),
328
+ }))
329
+ .sort((left, right) => left.workspaceRoot.localeCompare(right.workspaceRoot));
330
+
331
+ return {
332
+ ok: true,
333
+ targetVersion: normalizedTargetVersion,
334
+ registryPath: registry.registryPath,
335
+ staleCount: registry.staleEntries.length,
336
+ total: projects.length,
337
+ projects,
338
+ };
339
+ }
340
+
104
341
  function buildSelfUpdatePlan(options = {}) {
105
342
  const source = options.source ?? DEFAULT_SELF_UPDATE_SOURCE;
106
343
  const command = options.packageManager ?? 'npm';
@@ -164,9 +401,41 @@ export function createSelfUpdateWorkspace(deps = {}) {
164
401
  const runCommand = deps.runCommand ?? runProcess;
165
402
  const packageRoot = deps.packageRoot ?? DEFAULT_PACKAGE_ROOT;
166
403
 
404
+ async function checkSelfUpdateWorkspace(options = {}) {
405
+ const packageInfo = await (deps.readPackageInfo ?? readPackageInfo)(packageRoot);
406
+ const versionCheck = await inspectPublishedVersion(packageInfo, options, {
407
+ ...deps,
408
+ runCommand,
409
+ });
410
+ const refreshTargetVersion = versionCheck.updateAvailable
411
+ ? versionCheck.publishedVersion
412
+ : packageInfo.version;
413
+ const refreshCandidates = await collectRefreshCandidates(refreshTargetVersion, options, deps);
414
+ return {
415
+ ok: versionCheck.ok,
416
+ action: 'self-update-check',
417
+ checkOnly: true,
418
+ source: 'npm-published-version',
419
+ package: packageInfo,
420
+ currentVersion: normalizeVersionText(packageInfo.version),
421
+ publishedVersion: versionCheck.publishedVersion,
422
+ updateAvailable: versionCheck.updateAvailable,
423
+ comparison: versionCheck.comparison,
424
+ versionCheck,
425
+ refreshCandidates,
426
+ errors: [...versionCheck.errors],
427
+ nextActions: versionCheck.updateAvailable
428
+ ? ['Run `openprd self-update` to install the published CLI version.']
429
+ : [],
430
+ };
431
+ }
432
+
167
433
  async function selfUpdateWorkspace(options = {}) {
168
434
  const packageInfo = await (deps.readPackageInfo ?? readPackageInfo)(packageRoot);
169
435
  const localCheckout = await (deps.isLocalSourceCheckout ?? isLocalSourceCheckout)(packageRoot);
436
+ if (options.check) {
437
+ return checkSelfUpdateWorkspace(options);
438
+ }
170
439
  const plan = buildSelfUpdatePlan(options);
171
440
  const base = {
172
441
  ok: true,
@@ -178,6 +447,15 @@ export function createSelfUpdateWorkspace(deps = {}) {
178
447
  installCommand: plan.install,
179
448
  result: null,
180
449
  resolvedExecutable: null,
450
+ installedVersion: null,
451
+ refreshCandidates: {
452
+ ok: true,
453
+ targetVersion: null,
454
+ registryPath: null,
455
+ staleCount: 0,
456
+ total: 0,
457
+ projects: [],
458
+ },
181
459
  errors: [],
182
460
  nextActions: [],
183
461
  };
@@ -213,19 +491,36 @@ export function createSelfUpdateWorkspace(deps = {}) {
213
491
  const resolvedExecutable = installResult.ok
214
492
  ? await resolveOpenPrdExecutable(deps)
215
493
  : null;
494
+ const installedVersion = installResult.ok && resolvedExecutable?.ok
495
+ ? await resolveExecutableVersion(resolvedExecutable.executable, options, {
496
+ ...deps,
497
+ runCommand,
498
+ })
499
+ : null;
500
+ const refreshCandidates = installedVersion?.ok
501
+ ? await collectRefreshCandidates(installedVersion.version, options, deps)
502
+ : base.refreshCandidates;
216
503
 
217
504
  return {
218
505
  ...base,
219
- ok: installResult.ok && (resolvedExecutable?.ok ?? false),
506
+ ok: installResult.ok && (resolvedExecutable?.ok ?? false) && (installedVersion?.ok ?? false),
220
507
  result: installResult,
221
508
  resolvedExecutable,
509
+ installedVersion,
510
+ refreshCandidates,
222
511
  errors: [
223
512
  ...(installResult.ok ? [] : [installResult.stderr.trim() || `Self-update command failed with exit code ${installResult.exitCode}.`]),
224
513
  ...(installResult.ok && !resolvedExecutable?.ok ? [resolvedExecutable?.error ?? 'Unable to resolve updated openprd executable.'] : []),
514
+ ...(installedVersion?.errors ?? []),
515
+ ],
516
+ nextActions: [
517
+ ...(installResult.ok && !resolvedExecutable?.ok
518
+ ? ['Check that the global npm bin directory is on PATH, then run openprd update <project>.']
519
+ : []),
520
+ ...((refreshCandidates?.total ?? 0) > 0
521
+ ? ['If you want to refresh older workspaces too, review the refreshCandidates list before running fleet or per-project updates.']
522
+ : []),
225
523
  ],
226
- nextActions: installResult.ok && !resolvedExecutable?.ok
227
- ? ['Check that the global npm bin directory is on PATH, then run openprd update <project>.']
228
- : [],
229
524
  };
230
525
  }
231
526
 
@@ -318,6 +613,7 @@ export function createSelfUpdateWorkspace(deps = {}) {
318
613
  }
319
614
 
320
615
  return {
616
+ checkSelfUpdateWorkspace,
321
617
  selfUpdateWorkspace,
322
618
  upgradeWorkspace,
323
619
  };
@@ -325,5 +621,6 @@ export function createSelfUpdateWorkspace(deps = {}) {
325
621
 
326
622
  const defaultWorkspace = createSelfUpdateWorkspace();
327
623
 
624
+ export const checkSelfUpdateWorkspace = defaultWorkspace.checkSelfUpdateWorkspace;
328
625
  export const selfUpdateWorkspace = defaultWorkspace.selfUpdateWorkspace;
329
626
  export const upgradeWorkspace = defaultWorkspace.upgradeWorkspace;
package/src/standards.js CHANGED
@@ -370,9 +370,36 @@ function validateTextSections(relativePath, text, sections, errors) {
370
370
  }
371
371
  }
372
372
 
373
- function sourceManualReadmeName(projectRoot, dirPath) {
374
- const moduleName = path.basename(projectRoot).replace(/[^a-zA-Z0-9_-]+/g, '-');
375
- const folderName = path.basename(dirPath).replace(/[^a-zA-Z0-9_-]+/g, '-');
373
+ function normalizeModuleName(raw, fallback = 'project') {
374
+ const candidate = String(raw ?? '').trim().split('/').filter(Boolean).pop() ?? '';
375
+ const normalized = candidate.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
376
+ return normalized || fallback;
377
+ }
378
+
379
+ async function resolveSourceManualModuleName(projectRoot, config = {}, fallbackModuleName = '') {
380
+ const configuredName = typeof config?.folderManual?.moduleName === 'string'
381
+ ? config.folderManual.moduleName.trim()
382
+ : '';
383
+ if (configuredName) {
384
+ return normalizeModuleName(configuredName, normalizeModuleName(path.basename(projectRoot)));
385
+ }
386
+
387
+ const packageJson = await readJson(cjoin(projectRoot, 'package.json')).catch(() => null);
388
+ const packageName = typeof packageJson?.name === 'string' ? packageJson.name.trim() : '';
389
+ if (packageName) {
390
+ return normalizeModuleName(packageName, normalizeModuleName(path.basename(projectRoot)));
391
+ }
392
+
393
+ const fallbackName = typeof fallbackModuleName === 'string' ? fallbackModuleName.trim() : '';
394
+ if (fallbackName) {
395
+ return normalizeModuleName(fallbackName, normalizeModuleName(path.basename(projectRoot)));
396
+ }
397
+
398
+ return normalizeModuleName(path.basename(projectRoot));
399
+ }
400
+
401
+ function sourceManualReadmeName(moduleName, dirPath) {
402
+ const folderName = path.basename(dirPath).replace(/[^a-zA-Z0-9_-]+/g, '-') || moduleName;
376
403
  return `${moduleName}_${folderName}_README.md`;
377
404
  }
378
405
 
@@ -602,6 +629,11 @@ async function validateSourceManuals(projectRoot, errors, checks, options = {})
602
629
  const sourceFiles = await collectSourceFiles(projectRoot, projectRoot, [], options.ignorePatterns, discovery);
603
630
  const filesMissingManualRaw = [];
604
631
  const foldersMissingManualRaw = [];
632
+ const moduleName = await resolveSourceManualModuleName(
633
+ projectRoot,
634
+ options.config ?? {},
635
+ options.folderManualModuleName,
636
+ );
605
637
 
606
638
  for (const filePath of sourceFiles) {
607
639
  const relativePath = path.relative(projectRoot, filePath);
@@ -613,7 +645,8 @@ async function validateSourceManuals(projectRoot, errors, checks, options = {})
613
645
 
614
646
  const sourceDirsRaw = new Set(sourceFiles.map((filePath) => path.dirname(filePath)));
615
647
  for (const dirPath of sourceDirsRaw) {
616
- const expectedPath = cjoin(dirPath, sourceManualReadmeName(projectRoot, dirPath));
648
+ const folderLabel = dirPath === projectRoot ? moduleName : null;
649
+ const expectedPath = cjoin(dirPath, sourceManualReadmeName(moduleName, folderLabel ?? dirPath));
617
650
  const relativePath = path.relative(projectRoot, expectedPath);
618
651
  const text = await readText(expectedPath).catch(() => null);
619
652
  if (!text || !hasAllManualSections(text)) {
@@ -785,6 +818,9 @@ export async function checkStandardsWorkspace(projectRoot, options = {}) {
785
818
  if (config.docsRoot !== 'docs/basic') {
786
819
  errors.push(`${OPENPRD_STANDARDS_CONFIG} docsRoot must be docs/basic.`);
787
820
  }
821
+ if (config?.folderManual?.moduleName != null && (typeof config.folderManual.moduleName !== 'string' || !config.folderManual.moduleName.trim())) {
822
+ errors.push(`${OPENPRD_STANDARDS_CONFIG} folderManual.moduleName must be a non-empty string when provided.`);
823
+ }
788
824
  validateDevelopmentStandardsConfig(config, errors);
789
825
  validateGrowthConfig(config, errors);
790
826
  for (const externalPath of normalizeStringList(config?.externalReferences?.paths)) {
@@ -847,7 +883,12 @@ export async function checkStandardsWorkspace(projectRoot, options = {}) {
847
883
  && config?.fileManual?.enabled !== false
848
884
  && config?.folderManual?.enabled !== false;
849
885
  const manualReport = enforceSourceManuals
850
- ? await validateSourceManuals(projectRoot, errors, checks, { ignorePatterns, externalReferencePaths })
886
+ ? await validateSourceManuals(projectRoot, errors, checks, {
887
+ ignorePatterns,
888
+ externalReferencePaths,
889
+ config,
890
+ folderManualModuleName: options.folderManualModuleName,
891
+ })
851
892
  : {
852
893
  ignorePatterns,
853
894
  externalReferencePaths,