@llman-sdd/core 0.3.1 → 0.5.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.
Files changed (107) hide show
  1. package/package.json +2 -1
  2. package/src/archive/freeze.ts +86 -18
  3. package/src/archive/frozenCard.ts +105 -0
  4. package/src/archive/sevenzip.ts +15 -13
  5. package/src/change/closeOutHarness.ts +29 -0
  6. package/src/change/collect.ts +140 -0
  7. package/src/change/frontmatter.ts +48 -6
  8. package/src/change/id.ts +2 -6
  9. package/src/change/lifecycle.ts +285 -86
  10. package/src/change/nextId.ts +63 -2
  11. package/src/change/resolve.ts +2 -2
  12. package/src/change/tasks.ts +59 -0
  13. package/src/config/changeId.ts +14 -12
  14. package/src/config/load.ts +14 -0
  15. package/src/config/schema.ts +4 -41
  16. package/src/config/surface.ts +6 -36
  17. package/src/context/indexStore.ts +7 -3
  18. package/src/context/retrieve.ts +8 -10
  19. package/src/context/tree.ts +28 -24
  20. package/src/git/spawnGit.ts +90 -2
  21. package/src/index.ts +67 -59
  22. package/src/init/defaultConfig.ts +1 -5
  23. package/src/init/init.ts +19 -4
  24. package/src/ports.ts +1 -7
  25. package/src/project/migrateNotes.ts +104 -0
  26. package/src/render/machine.ts +30 -0
  27. package/src/report/collect.ts +11 -127
  28. package/src/report/graph/analysis.ts +152 -0
  29. package/src/report/graph/deps.ts +30 -0
  30. package/src/report/graph/graphData.ts +53 -0
  31. package/src/report/graph/nodes.ts +130 -0
  32. package/src/report/graph/render.ts +83 -0
  33. package/src/report/graph/types.ts +47 -0
  34. package/src/report/graph.ts +9 -381
  35. package/src/report/show.ts +20 -22
  36. package/src/report/specHelpers.ts +42 -21
  37. package/src/report/specs.ts +23 -25
  38. package/src/review/review.ts +45 -30
  39. package/src/spec/authoring.ts +91 -63
  40. package/src/spec/ir.ts +43 -15
  41. package/src/spec/migrateNative.ts +167 -0
  42. package/src/spec/parser.ts +73 -77
  43. package/src/spec/reqRegistry.ts +8 -9
  44. package/src/templates/embedded.ts +10 -16
  45. package/src/templates/engine.ts +10 -5
  46. package/src/templates/locale.ts +1 -1
  47. package/src/templates/skills.ts +4 -5
  48. package/src/validation/changeCheck.ts +128 -105
  49. package/src/validation/harness.ts +161 -0
  50. package/src/validation/staleness.ts +9 -5
  51. package/src/validation/validate.ts +60 -88
  52. package/templates/en/skills/llman-sdd-apply-cycle.md +20 -28
  53. package/templates/en/skills/llman-sdd-apply.md +58 -76
  54. package/templates/en/skills/llman-sdd-arch-review.md +12 -19
  55. package/templates/en/skills/llman-sdd-archive.md +27 -42
  56. package/templates/en/skills/llman-sdd-continue.md +17 -24
  57. package/templates/en/skills/llman-sdd-draft.md +17 -28
  58. package/templates/en/skills/llman-sdd-explore.md +29 -43
  59. package/templates/en/skills/llman-sdd-ff.md +12 -17
  60. package/templates/en/skills/llman-sdd-graph.md +14 -32
  61. package/templates/en/skills/llman-sdd-propose.md +48 -63
  62. package/templates/en/skills/llman-sdd-quick.md +12 -27
  63. package/templates/en/skills/llman-sdd-research.md +13 -24
  64. package/templates/en/skills/llman-sdd-specs-compact.md +14 -39
  65. package/templates/en/skills/llman-sdd-validate.md +11 -15
  66. package/templates/en/skills/llman-sdd-verify.md +23 -44
  67. package/templates/en/skills/llman-sdd-wayfinder.md +18 -22
  68. package/templates/en/units/skills/cli-footer.md +2 -0
  69. package/templates/en/units/skills/git-native-flow-brief.md +7 -6
  70. package/templates/en/units/skills/git-native-flow.md +21 -11
  71. package/templates/en/units/skills/human-readable-summary.md +2 -3
  72. package/templates/en/units/skills/stage-guard.md +7 -7
  73. package/templates/en/units/skills/structured-protocol.md +5 -8
  74. package/templates/en/units/skills/validation-hints.md +10 -14
  75. package/templates/en/units/spec/feature-contract.md +27 -16
  76. package/templates/en/units/workflow/archive-freeze-guidance.md +6 -3
  77. package/templates/zh-Hans/skills/llman-sdd-apply-cycle.md +23 -31
  78. package/templates/zh-Hans/skills/llman-sdd-apply.md +63 -81
  79. package/templates/zh-Hans/skills/llman-sdd-arch-review.md +21 -28
  80. package/templates/zh-Hans/skills/llman-sdd-archive.md +29 -44
  81. package/templates/zh-Hans/skills/llman-sdd-continue.md +17 -24
  82. package/templates/zh-Hans/skills/llman-sdd-draft.md +18 -29
  83. package/templates/zh-Hans/skills/llman-sdd-explore.md +34 -48
  84. package/templates/zh-Hans/skills/llman-sdd-ff.md +13 -18
  85. package/templates/zh-Hans/skills/llman-sdd-graph.md +16 -34
  86. package/templates/zh-Hans/skills/llman-sdd-propose.md +51 -65
  87. package/templates/zh-Hans/skills/llman-sdd-quick.md +15 -30
  88. package/templates/zh-Hans/skills/llman-sdd-research.md +17 -28
  89. package/templates/zh-Hans/skills/llman-sdd-specs-compact.md +15 -40
  90. package/templates/zh-Hans/skills/llman-sdd-validate.md +11 -15
  91. package/templates/zh-Hans/skills/llman-sdd-verify.md +26 -47
  92. package/templates/zh-Hans/skills/llman-sdd-wayfinder.md +25 -29
  93. package/templates/zh-Hans/units/skills/cli-footer.md +2 -0
  94. package/templates/zh-Hans/units/skills/git-native-flow-brief.md +7 -6
  95. package/templates/zh-Hans/units/skills/git-native-flow.md +22 -12
  96. package/templates/zh-Hans/units/skills/human-readable-summary.md +4 -5
  97. package/templates/zh-Hans/units/skills/stage-guard.md +9 -9
  98. package/templates/zh-Hans/units/skills/structured-protocol.md +5 -8
  99. package/templates/zh-Hans/units/skills/validation-hints.md +10 -14
  100. package/templates/zh-Hans/units/spec/feature-contract.md +25 -16
  101. package/templates/zh-Hans/units/workflow/archive-freeze-guidance.md +6 -2
  102. package/templates/en/skills/llman-sdd-onboard.md +0 -34
  103. package/templates/en/skills/llman-sdd-show.md +0 -24
  104. package/templates/en/units/migrate-prompt.md +0 -28
  105. package/templates/zh-Hans/skills/llman-sdd-onboard.md +0 -34
  106. package/templates/zh-Hans/skills/llman-sdd-show.md +0 -24
  107. package/templates/zh-Hans/units/migrate-prompt.md +0 -28
package/src/index.ts CHANGED
@@ -1,34 +1,12 @@
1
- // 版本 SSOT 是包描述文件(发布产物)/git tag(二进制,经 LLMAN_SDD_VERSION 注入)。
2
- // 运行时从所在包的 package.json 读取,避免双处维护。单文件二进制内
3
- // import.meta.url 指向 $bunfs 虚拟路径,package.json 不存在——此处必须容错,
4
- // 版本由构建期 define 注入,回退值不会对外暴露。
5
- import { readFileSync } from 'node:fs';
6
- import { fileURLToPath } from 'node:url';
7
-
8
- function readPackageVersion(): string {
9
- try {
10
- const pkg = JSON.parse(
11
- readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8'),
12
- ) as { version: string };
13
- return pkg.version;
14
- } catch {
15
- return '0.0.0';
16
- }
17
- }
18
- export const VERSION = readPackageVersion();
19
-
20
1
  export * from './ports.ts';
21
2
 
22
3
  export {
23
4
  EXTRA_SKILLS,
24
5
  archiveSchema,
25
6
  bddSchema,
26
- bindingSchema,
27
7
  changeIdSchema,
28
- scenarioAttrsBindingSchema,
29
8
  sddConfigSchema,
30
9
  sddSchema,
31
- tagsBindingSchema,
32
10
  type SddConfig,
33
11
  type SddConfigInput,
34
12
  } from './config/schema.ts';
@@ -39,20 +17,17 @@ export {
39
17
  nextUniqueNumber,
40
18
  renderChangeIdTemplate,
41
19
  } from './config/changeId.ts';
42
- export {
43
- ExtraSkillsError,
44
- renderConfigOverview,
45
- setExtraSkills,
46
- skillsJson,
47
- } from './config/surface.ts';
20
+ export { renderConfigOverview, skillsJson } from './config/surface.ts';
48
21
 
49
22
  export type {
50
23
  CapabilityDoc,
51
24
  CapabilityHeader,
52
- ScenarioClassification,
25
+ RuleIR,
53
26
  ScenarioIR,
27
+ ScenarioStep,
54
28
  SpecStructuralError,
55
29
  } from './spec/ir.ts';
30
+ export { specIdOf } from './spec/ir.ts';
56
31
  export {
57
32
  SpecParseError,
58
33
  localeToGherkinLang,
@@ -60,10 +35,23 @@ export {
60
35
  parseFeatureSource,
61
36
  } from './spec/parser.ts';
62
37
  export { buildReqRegistry, type ReqRegistry, type RegistryDuplicate } from './spec/reqRegistry.ts';
38
+ export {
39
+ analyzeLegacy,
40
+ hasNativeRules,
41
+ migrateNativeSource,
42
+ type MigrateBlock,
43
+ type MigrateResult,
44
+ } from './spec/migrateNative.ts';
45
+ export {
46
+ MIGRATE_KINDS,
47
+ isMigrateKind,
48
+ migrateNoteFor,
49
+ migrateOverviewFor,
50
+ type MigrateKind,
51
+ } from './project/migrateNotes.ts';
63
52
  export {
64
53
  addReq,
65
54
  addScenario,
66
- allReqIds,
67
55
  AuthoringError,
68
56
  planDedupe,
69
57
  resolveReq,
@@ -79,6 +67,7 @@ export type {
79
67
  ValidationReport,
80
68
  } from './validation/validate.ts';
81
69
  export { validateAllSpecs, validateCapability, applyStrict } from './validation/validate.ts';
70
+ export { buildDuplicatesFor, formatTotals } from './validation/validate.ts';
82
71
  export { splitVerb } from './config/changeId.ts';
83
72
  export {
84
73
  evaluateStaleness,
@@ -88,8 +77,7 @@ export {
88
77
  export {
89
78
  checkChangeDoc,
90
79
  validateChange,
91
- expandRunCommand,
92
- hasPlaceholders,
80
+ checkGlobalChangeIdUniqueness,
93
81
  STAGE_ORDER,
94
82
  type ChangeCheckConfig,
95
83
  type ChangeCheckInput,
@@ -99,19 +87,44 @@ export {
99
87
  type StageGate,
100
88
  } from './validation/changeCheck.ts';
101
89
  export { discoverSpecs, type DiscoveryIo } from './validation/discover.ts';
90
+ export {
91
+ expandRunCommand,
92
+ runHarnessForSpecs,
93
+ type HarnessGate,
94
+ type HarnessRunner,
95
+ type HarnessRunOutcome,
96
+ type HarnessTarget,
97
+ } from './validation/harness.ts';
102
98
  export type { ChangeBinding } from './change/frontmatter.ts';
103
- export { readBinding, writeBinding } from './change/frontmatter.ts';
99
+ export {
100
+ readBinding,
101
+ writeBinding,
102
+ extractFrontmatter,
103
+ readNeedsSpecsChange,
104
+ } from './change/frontmatter.ts';
104
105
  export {
105
106
  GitError,
106
107
  defaultBranch,
107
108
  isCleanTree,
108
109
  makeSpawnGit,
109
110
  currentBranch,
110
- revParseHead,
111
+ worktreeList,
112
+ probeMainCheckout,
113
+ nonMainCheckoutWarning,
111
114
  type GitLike,
115
+ type WorktreeEntry,
116
+ type MainCheckoutProbe,
112
117
  } from './git/spawnGit.ts';
113
- export { DRAFT_PROPOSAL_TEMPLATE, deriveChangeId, isLegalChangeId } from './change/id.ts';
114
- export { extractUniqueNumber, harvestUniqueNumbers } from './change/nextId.ts';
118
+ export { DRAFT_PROPOSAL_TEMPLATE, deriveChangeId } from './change/id.ts';
119
+ export { parseTaskCheckboxes, type ParsedTaskCheckboxes } from './change/tasks.ts';
120
+ export { CLOSE_OUT_TASK_HINT, closeOutTaskLines, isCloseOutTaskTitle } from './change/tasks.ts';
121
+ export { decideCloseOutHarness, type CloseOutHarnessDecision } from './change/closeOutHarness.ts';
122
+ export {
123
+ extractUniqueNumber,
124
+ collectNumbers,
125
+ harvestUniqueNumbers,
126
+ harvestAcrossWorktrees,
127
+ } from './change/nextId.ts';
115
128
  export { ChangeIdResolveError, resolveChangeId, type ResolvedChangeId } from './change/resolve.ts';
116
129
  export {
117
130
  CHANGES_DIR,
@@ -130,16 +143,12 @@ export {
130
143
  export { normalizeLocale, localeFallbacks } from './templates/locale.ts';
131
144
  export {
132
145
  MAX_UNIT_NESTING_DEPTH,
133
- MissingUnitError,
134
146
  renderTemplate,
135
147
  renderWithUnits,
136
148
  type UnitRegistry,
137
149
  } from './templates/engine.ts';
138
150
  export {
139
- DEFAULT_SKILL_FILES,
140
151
  ETHICS_KEYS,
141
- OPTIONAL_SKILL_FILES,
142
- UNIT_FILES,
143
152
  buildTemplateVars,
144
153
  effectiveRunCommand,
145
154
  enforceEthicsGovernance,
@@ -165,27 +174,23 @@ export {
165
174
  } from './init/init.ts';
166
175
  export {
167
176
  collectChanges,
168
- renderChangesList,
169
- renderChangesJson,
170
177
  statusFor,
171
178
  stageFor,
172
- relativeTime,
173
- statusHuman,
179
+ countTasks,
180
+ firstH1,
174
181
  type ChangeFsIo,
175
182
  type ChangeStatus,
176
183
  type ChangeSummary,
177
- } from './report/collect.ts';
178
- export { graphMermaid, type GraphFsIo } from './report/graph.ts';
184
+ } from './change/collect.ts';
185
+ export { renderChangesJson, renderChangesList } from './report/collect.ts';
186
+ export { graphData, graphMermaid, type GraphDataIr, type GraphFsIo } from './report/graph.ts';
187
+ export { parseDeps } from './report/graph.ts';
188
+ export { renderMachine, type MachineFormat } from './render/machine.ts';
179
189
  export { showChangeJson, type ShowDeps, type ShowFsIo } from './report/show.ts';
180
- export {
181
- nextReqId,
182
- scaffoldSpec,
183
- skeletonContent,
184
- type SpecHelperIo,
185
- } from './report/specHelpers.ts';
190
+ export { nextReqId, scaffoldSpec, type SpecHelperIo } from './report/specHelpers.ts';
186
191
  export {
187
192
  collectSpecs,
188
- morphologyOfScenarios,
193
+ morphologyOf,
189
194
  renderSpecsJson,
190
195
  renderSpecsList,
191
196
  type SpecMorphology,
@@ -193,10 +198,10 @@ export {
193
198
  } from './report/specs.ts';
194
199
  export {
195
200
  SevenZipError,
196
- embeddedWasmBinary,
197
201
  makeWasmSevenZip,
198
202
  resolveEmbeddedWasmB64,
199
203
  type SevenZipPort,
204
+ type WasmSevenZipDeps,
200
205
  } from './archive/sevenzip.ts';
201
206
  export {
202
207
  ARCHIVE_DIR_REL,
@@ -210,18 +215,23 @@ export {
210
215
  type FreezeRunResult,
211
216
  type ThawResult,
212
217
  } from './archive/freeze.ts';
218
+ export {
219
+ FROZEN_CARD_EXT,
220
+ composeFrozenCard,
221
+ frozenCardIdOf,
222
+ frozenCardName,
223
+ isFrozenCard,
224
+ } from './archive/frozenCard.ts';
213
225
  export {
214
226
  buildReview,
215
227
  renderReviewHtml,
228
+ specRelFor,
216
229
  type ReviewInput,
217
230
  type ReviewKind,
218
231
  type ReviewResult,
219
232
  type ReviewSignal,
220
- type TagBinding,
221
233
  } from './review/review.ts';
222
234
  export {
223
- TREE_VERSION,
224
- buildDocs,
225
235
  buildTreeIndex,
226
236
  computeSpecHash,
227
237
  type HashIo,
@@ -236,7 +246,6 @@ export {
236
246
  REBUILD_LOCK_REL,
237
247
  TREE_JSON_REL,
238
248
  checkIndexFreshness,
239
- loadTree,
240
249
  loadTreeWithAutoRebuild,
241
250
  parseLock,
242
251
  rebuildIndex,
@@ -247,7 +256,6 @@ export {
247
256
  type RebuildResult,
248
257
  } from './context/indexStore.ts';
249
258
  export {
250
- MAX_TOOL_ROUNDS,
251
259
  resolveChatConfig,
252
260
  runContextRetrieval,
253
261
  unavailableResult,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Default llmanspec/config.yaml templates, extracted verbatim from v1
2
+ * Default llmanspec/config.yaml templates, extracted verbatim from predecessor
3
3
  * (crates/llman-sdd/src/sdd/project/config.rs DEFAULT_CONFIG_EN/ZH_HANS).
4
4
  */
5
5
  export const LLMANSPEC_SCHEMA_URL =
@@ -26,8 +26,6 @@ locale: en
26
26
  # BDD integration (optional, uncomment to enable)
27
27
  # bdd:
28
28
  # framework: pytest-bdd
29
- # feature_dir: tests/features/
30
- # # default_language: en
31
29
  # # Filtered runners: include {feature_*} so validate --all/--specs runs per capability.
32
30
  # # run_command: "pytest {feature_dir} -k {feature_name} -v"
33
31
  # # Project-wide runners (no placeholders): validate --all/--specs runs the suite once (batch-once).
@@ -56,8 +54,6 @@ locale: zh-Hans
56
54
  # BDD 集成(可选,取消注释以启用)
57
55
  # bdd:
58
56
  # framework: pytest-bdd
59
- # feature_dir: tests/features/
60
- # # default_language: zh-CN
61
57
  # # 过滤型 runner:写 {feature_*},validate --all/--specs 按 capability 分别执行。
62
58
  # # run_command: "pytest {feature_dir} -k {feature_name} -v"
63
59
  # # 项目级 runner(无占位符):validate --all/--specs 整批只跑一次(batch-once)。
package/src/init/init.ts CHANGED
@@ -31,19 +31,34 @@ export { effectiveRunCommand } from '../templates/skills.ts';
31
31
  const MARKER_START = '<!-- LLMANSPEC:START -->';
32
32
  const MARKER_END = '<!-- LLMANSPEC:END -->';
33
33
 
34
- /** Marker-block update: replace block body if present, else prepend the block. */
34
+ /**
35
+ * Marker-block update: replace block body if present, else prepend the block.
36
+ * Formatter-friendly (0.0.78 parity): exactly one blank line after START and
37
+ * after END; on the END boundary an existing blank line is preserved verbatim
38
+ * and a missing one is inserted, so repeated updates never accumulate blanks.
39
+ */
35
40
  export function updateFileWithMarkers(content: string, body: string): string {
36
41
  if (content === '') {
37
- return `${MARKER_START}\n${body}\n${MARKER_END}\n`;
42
+ return `${MARKER_START}\n\n${body}\n${MARKER_END}\n`;
38
43
  }
39
44
  const start = content.indexOf(MARKER_START);
40
45
  const end = content.indexOf(MARKER_END);
41
46
  if (start !== -1 && end !== -1) {
42
47
  if (end < start)
43
48
  throw new Error('Invalid marker state: end marker appears before start marker.');
44
- return `${content.slice(0, start)}${MARKER_START}\n${body}\n${MARKER_END}${content.slice(end + MARKER_END.length)}`;
49
+ const suffix = content.slice(end + MARKER_END.length);
50
+ return `${content.slice(0, start)}${MARKER_START}\n\n${body}\n${MARKER_END}${separatorAfterEndMarker(suffix)}`;
45
51
  }
46
- return `${MARKER_START}\n${body}\n${MARKER_END}\n${content}`;
52
+ return `${MARKER_START}\n\n${body}\n${MARKER_END}\n\n${content}`;
53
+ }
54
+
55
+ /** END-boundary suffix, guaranteed blank-line-separated from following content. */
56
+ function separatorAfterEndMarker(suffix: string): string {
57
+ const br = /^\r?\n/u.exec(suffix);
58
+ if (br === null) return suffix === '' ? '\n' : `\n\n${suffix}`;
59
+ const rest = suffix.slice(br[0].length);
60
+ if (rest === '' || /^\r?\n/u.test(rest)) return suffix;
61
+ return `${br[0]}${suffix}`;
47
62
  }
48
63
 
49
64
  export const TEMPLATES_ROOT = join(import.meta.dirname, '..', '..', 'templates');
package/src/ports.ts CHANGED
@@ -5,16 +5,10 @@
5
5
  * `node:fs`, `Bun.$`, or prompt libraries; runtimes wire adapters in.
6
6
  */
7
7
 
8
- /** Interaction port. v1 ships an @inquirer/prompts adapter; a future ink TUI
8
+ /** Interaction port. predecessor ships an @inquirer/prompts adapter; a future ink TUI
9
9
  * ships its own adapter (the two must never run in the same process). */
10
10
  export interface PromptDriver {
11
11
  select<T extends string>(message: string, choices: readonly T[]): Promise<T>;
12
12
  multiselect<T extends string>(message: string, choices: readonly T[]): Promise<T[]>;
13
13
  confirm(message: string): Promise<boolean>;
14
14
  }
15
-
16
- /** Template rendering port (nunjucks adapter planned: autoescape off,
17
- * minijinja-compatible trim semantics, `unit()` registered via addGlobal). */
18
- export interface TemplateEngine {
19
- render(template: string, context: Record<string, unknown>): string;
20
- }
@@ -0,0 +1,104 @@
1
+ // Collaboration notes for `project migrate` — the predecessor (Rust ≤0.0.78) shipped two
2
+ // one-shot migrations (toon2features / specs-flatten); this tool deliberately ships
3
+ // none (peripheral-commands r35: the command outputs an explanation and never
4
+ // migrates). Porting the implementations was surveyed on 2026-09-23 and
5
+ // declined: specs-flatten fights this repo's directory-style coexistence direction,
6
+ // and toon2features has no confirmed audience. Instead, `--kind <x>` prints
7
+ // the equivalent manual-migration guidance so old command forms get useful
8
+ // stdout instead of an unknown-option error.
9
+
10
+ export const MIGRATE_KINDS = ['toon2features', 'specs-flatten'] as const;
11
+
12
+ export type MigrateKind = (typeof MIGRATE_KINDS)[number];
13
+
14
+ export function isMigrateKind(value: string): value is MigrateKind {
15
+ return (MIGRATE_KINDS as readonly string[]).includes(value);
16
+ }
17
+
18
+ const isZh = (locale: string): boolean => locale.trim().toLowerCase().startsWith('zh');
19
+
20
+ const TOON2FEATURES_ZH = `llman-sdd project migrate --kind toon2features — 遗留 spec.toon → 单轨 .feature 协作说明
21
+
22
+ 背景:v1(Rust ≤0.0.78)以 specs/<cap>/spec.toon(表格形态)与散落 .feature 双轨存规格;v2 只读单轨 .feature(specs/<cap>.feature 或目录式 <cap>/<cap>.feature),spec.toon 会被静默忽略。本命令不执行迁移,以下为手工迁移指引。
23
+
24
+ Agent 该做什么:
25
+ 1. 逐 capability 确认真的存在 spec.toon(无则跳过)。
26
+ 2. 读 spec.toon 表格并转换:requirements 表每行 → 在 <cap>.feature 写一个 @req:<id> + 规则: 块(id 进块头标签,statement 全文放描述);scenarios 表每行:有 GWT(假如/当/那么)且 req_id 已定义 → 作为嵌套 场景: 挂回该规则;无 GWT 或 req 未定义 → 在 proposal/design 记录理由后舍弃,不可静默丢弃。
27
+ 3. 目标 <cap>.feature 已存在时勿覆盖——人工合并两侧内容后再删除 spec.toon。
28
+ 4. 迁移后运行 llman-sdd validate --specs --strict 与项目 BDD 套件,全绿为收口标准。
29
+
30
+ 人类该做什么:
31
+ - 审阅 diff 与 req 语义;把 # scope: 指向该规范管辖的真实源码目录(scope 承载 staleness 扫描,自引用无意义)。
32
+
33
+ 陷阱:
34
+ - spec.toon 的引号转义存在两种历史风格("" 翻倍与 \\" 反斜杠),手工读表须逐一确认。
35
+ - 任何一行无法解析都停下来人工判定,勿凭猜测整表转换。`;
36
+
37
+ const TOON2FEATURES_EN = `llman-sdd project migrate --kind toon2features — legacy spec.toon → single-track .feature collaboration notes
38
+
39
+ Background: v1 (Rust ≤0.0.78) stored specs dual-track — specs/<cap>/spec.toon (table form) plus scattered .feature files; v2 reads single-track .feature only (specs/<cap>.feature or directory-style <cap>/<cap>.feature) and silently ignores spec.toon. This command performs no migration; the notes below are the manual procedure.
40
+
41
+ What the agent does:
42
+ 1. Per capability, confirm a spec.toon actually exists (skip otherwise).
43
+ 2. Read the spec.toon tables and convert: each requirements row → a @req:<id> 规则: block in <cap>.feature (id in the block header tag, statement verbatim in the description); each scenarios row: GWT-bearing (Given/When/Then) with a defined req_id → a nested 场景: under that rule; no GWT or undefined req_id → record the justification in proposal/design and drop — never silently.
44
+ 3. Never overwrite an existing <cap>.feature — merge both sides by hand, then delete spec.toon.
45
+ 4. After migration run llman-sdd validate --specs --strict and the project BDD suite; all-green is the bar.
46
+
47
+ What the human does:
48
+ - Review the diff and the req semantics; point # scope: at the real source directory the spec governs (scope drives staleness scanning; self-references are useless).
49
+
50
+ Pitfalls:
51
+ - spec.toon quoting has two historical escaping styles ("" doubling and \\" backslash); verify cell by cell.
52
+ - Any unparseable row means stop and judge by hand — never convert a whole table on guesswork.`;
53
+
54
+ const SPECS_FLATTEN_ZH = `llman-sdd project migrate --kind specs-flatten — 目录式 specs 归一为扁平 协作说明
55
+
56
+ 背景:v1 曾把 capability 放在 specs/<cap>/<cap>.feature 目录;v2 两种布局都能读,但扁平 specs/<cap>.feature 是唯一写入形态(spec add-* / skeleton 按扁平拼写),归一属可选整理。本命令不执行迁移,以下为手工指引。
57
+
58
+ Agent 该做什么:
59
+ 1. 仅处理「目录内只有一个 <cap>.feature、无其他文件」的纯目录;目标 specs/<cap>.feature 已存在(冲突)、目录内有多个 .feature、或有附属文件 → 报告并跳过,勿强迁。
60
+ 2. 用 git mv specs/<cap>/<cap>.feature specs/<cap>.feature 保留历史,然后删除空目录。
61
+ 3. 检查该文件 # scope: 是否自引用自身路径,是则改写为真实源码目录。
62
+ 4. 迁移后运行 llman-sdd validate --specs 确认无 stale 警告。
63
+
64
+ 人类该做什么:
65
+ - 审阅 git diff(move 保历史);确认 scope 指向真实管辖面。`;
66
+
67
+ const SPECS_FLATTEN_EN = `llman-sdd project migrate --kind specs-flatten — directory-style specs → flat collaboration notes
68
+
69
+ Background: v1 placed capabilities in specs/<cap>/<cap>.feature directories; v2 reads both layouts, but flat specs/<cap>.feature is the only write shape (spec add-* / skeleton spell flat paths), so flattening is optional housekeeping. This command performs no migration; the notes below are the manual procedure.
70
+
71
+ What the agent does:
72
+ 1. Only handle pure directories (exactly one <cap>.feature inside, nothing else); if the target specs/<cap>.feature already exists (conflict), the directory holds multiple .feature files, or there are auxiliary entries → report and skip, never force.
73
+ 2. git mv specs/<cap>/<cap>.feature specs/<cap>.feature to preserve history, then remove the emptied directory.
74
+ 3. Check the file's # scope: for a self-referential path and rewrite it to the real source directory.
75
+ 4. After migration run llman-sdd validate --specs and confirm no stale warning remains.
76
+
77
+ What the human does:
78
+ - Review the git diff (moves keep history); confirm scope points at the real governed surface.`;
79
+
80
+ const OVERVIEW_ZH =
81
+ 'legacy 迁移实现不随本工具提供;v2 直接读取既有 llmanspec 布局(config.yaml / specs/*.feature / changes/),零迁移可读。\n' +
82
+ '两种历史迁移的协作说明:--kind toon2features | --kind specs-flatten。';
83
+
84
+ const OVERVIEW_EN =
85
+ 'Legacy migration implementations are not shipped; v2 reads existing llmanspec layouts directly (config.yaml / specs/*.feature / changes/), zero-migration readable.\n' +
86
+ 'Collaboration notes for the two historical migrations: --kind toon2features | --kind specs-flatten.';
87
+
88
+ /** stdout body for `project migrate --kind <kind>`; null when the kind is unknown. */
89
+ export function migrateNoteFor(kind: string, locale: string): string | null {
90
+ const zh = isZh(locale);
91
+ switch (kind) {
92
+ case 'toon2features':
93
+ return zh ? TOON2FEATURES_ZH : TOON2FEATURES_EN;
94
+ case 'specs-flatten':
95
+ return zh ? SPECS_FLATTEN_ZH : SPECS_FLATTEN_EN;
96
+ default:
97
+ return null;
98
+ }
99
+ }
100
+
101
+ /** stdout body for bare `project migrate` (no --kind). */
102
+ export function migrateOverviewFor(locale: string): string {
103
+ return isZh(locale) ? OVERVIEW_ZH : OVERVIEW_EN;
104
+ }
@@ -0,0 +1,30 @@
1
+ import { encode } from '@toon-format/toon';
2
+
3
+ /**
4
+ * Machine-readable render targets shared by report commands (add-render-layer).
5
+ * Each command owns one canonical IR (exactly the payload its `--json` emits);
6
+ * every machine format renders from that single object, so formats can never
7
+ * drift the way hand-written per-command JSON did. `human` prose is
8
+ * deliberately out of scope — it stays per-command.
9
+ */
10
+ export type MachineFormat = 'json' | 'compact-json' | 'toon';
11
+
12
+ /**
13
+ * Render `ir` in the requested format. The returned string carries no trailing
14
+ * newline (callers `console.log` it, matching the pre-renderer byte layout).
15
+ *
16
+ * - `json`: 2-space pretty — byte-identical to the historical `--json` output.
17
+ * - `compact-json`: true minified single line (normalizes the historical
18
+ * strip-newlines / indent-0 variants; same data, contract still "one line").
19
+ * - `toon`: TOON predecessor via the official encoder (comma delimiter default).
20
+ */
21
+ export function renderMachine(ir: unknown, format: MachineFormat): string {
22
+ switch (format) {
23
+ case 'compact-json':
24
+ return JSON.stringify(ir);
25
+ case 'toon':
26
+ return encode(ir);
27
+ case 'json':
28
+ return JSON.stringify(ir, null, 2);
29
+ }
30
+ }
@@ -1,128 +1,10 @@
1
+ import { type ChangeSummary, statusFor } from '../change/collect.ts';
1
2
  /**
2
- * Change collection + rendering (peripheral-commands capability, r20).
3
- * Port of v1 commands/list.rs shapes. Pure — IO injected.
3
+ * Change list rendering (peripheral-commands capability, r20): presentation
4
+ * only — domain scan/stage derivation lives in change/collect.ts; this module
5
+ * renders what it produces. Pure — no IO.
4
6
  */
5
- import { readBinding } from '../change/frontmatter.ts';
6
- import { CHANGES_DIR } from '../change/lifecycle.ts';
7
-
8
- export interface ChangeFsIo {
9
- exists(path: string): boolean;
10
- readText(path: string): string;
11
- listDir(path: string): string[];
12
- isDirectory(path: string): boolean;
13
- mtimeMs(path: string): number;
14
- }
15
-
16
- export interface ChangeSummary {
17
- name: string;
18
- path: string;
19
- title: string;
20
- stage: 'draft' | 'designed' | 'planned' | 'full';
21
- hasBinding: boolean;
22
- completedTasks: number;
23
- totalTasks: number;
24
- lastModified: Date;
25
- idleDays: number;
26
- }
27
-
28
- export type ChangeStatus = 'no-tasks' | 'complete' | 'in-progress';
29
-
30
- export function statusFor(total: number, completed: number): ChangeStatus {
31
- if (total === 0) return 'no-tasks';
32
- return completed >= total ? 'complete' : 'in-progress';
33
- }
34
-
35
- export function stageFor(
36
- hasDesign: boolean,
37
- hasTasks: boolean,
38
- hasBinding: boolean,
39
- ): ChangeSummary['stage'] {
40
- // Monotonic v1 parity (r34): design.md gates designed, tasks.md only
41
- // upgrades on top of design, binding only upgrades the complete set.
42
- if (hasDesign && hasTasks && hasBinding) return 'full';
43
- if (hasDesign && hasTasks) return 'planned';
44
- if (hasDesign) return 'designed';
45
- return 'draft';
46
- }
47
-
48
- export function countTasks(tasksMd: string): { completed: number; total: number } {
49
- let completed = 0;
50
- let total = 0;
51
- for (const line of tasksMd.split('\n')) {
52
- const m = line.match(/^\s*-\s+\[( |x|X)\]/u);
53
- if (m) {
54
- total += 1;
55
- if (m[1] !== ' ') completed += 1;
56
- }
57
- }
58
- return { completed, total };
59
- }
60
-
61
- export function firstH1(md: string): string {
62
- for (const line of md.split('\n')) {
63
- const m = line.match(/^#\s+(.*)$/u);
64
- if (m?.[1]) return m[1].trim();
65
- }
66
- return '';
67
- }
68
-
69
- export function collectChanges(
70
- io: ChangeFsIo,
71
- root: string,
72
- now: Date,
73
- opts: { maxScanDepth?: number } = {},
74
- ): ChangeSummary[] {
75
- const changesDir = `${root}/${CHANGES_DIR}`;
76
- if (!io.exists(changesDir) || !io.isDirectory(changesDir)) return [];
77
- const maxDepth = opts.maxScanDepth ?? 8;
78
- const out: ChangeSummary[] = [];
79
- // r58: recursive, depth-limited proposal discovery (v1 --max-scan-depth parity)
80
- const visit = (dir: string, depth: number): void => {
81
- if (depth > maxDepth) return;
82
- for (const name of io.listDir(dir).toSorted()) {
83
- if (name === 'archive' || name.startsWith('.')) continue;
84
- const child = `${dir}/${name}`;
85
- if (!io.isDirectory(child)) continue;
86
- if (io.exists(`${child}/proposal.md`)) {
87
- readChangeDir(child, name);
88
- } else {
89
- visit(child, depth + 1);
90
- }
91
- }
92
- };
93
- const readChangeDir = (dir: string, name: string): void => {
94
- const proposal = `${dir}/proposal.md`;
95
- const hasDesign = io.exists(`${dir}/design.md`);
96
- const hasTasks = io.exists(`${dir}/tasks.md`);
97
- const hasBinding = readBinding(io.readText(proposal)) !== null;
98
- const { completed, total } = hasTasks
99
- ? countTasks(io.readText(`${dir}/tasks.md`))
100
- : { completed: 0, total: 0 };
101
-
102
- // lastModified = newest mtime across the change dir
103
- let latest = io.mtimeMs(proposal);
104
- for (const f of [hasDesign ? `${dir}/design.md` : null, hasTasks ? `${dir}/tasks.md` : null]) {
105
- if (f) latest = Math.max(latest, io.mtimeMs(f));
106
- }
107
- const lastModified = new Date(latest);
108
- const idleDays = Math.floor((now.getTime() - lastModified.getTime()) / 86_400_000);
109
-
110
- out.push({
111
- name,
112
- path: name,
113
- title: firstH1(io.readText(proposal)),
114
- stage: stageFor(hasDesign, hasTasks, hasBinding),
115
- hasBinding,
116
- completedTasks: completed,
117
- totalTasks: total,
118
- lastModified,
119
- idleDays,
120
- });
121
- };
122
- visit(changesDir, 1);
123
- // v1 lists newest-first
124
- return out.toSorted((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
125
- }
7
+ import { renderMachine } from '../render/machine.ts';
126
8
 
127
9
  export function statusHuman(c: ChangeSummary): string {
128
10
  const s = statusFor(c.totalTasks, c.completedTasks);
@@ -160,8 +42,11 @@ export function renderChangesList(changes: readonly ChangeSummary[], now: Date):
160
42
  return lines;
161
43
  }
162
44
 
163
- export function renderChangesJson(changes: readonly ChangeSummary[]): string {
164
- return JSON.stringify(
45
+ export function renderChangesJson(
46
+ changes: readonly ChangeSummary[],
47
+ mode: 'json' | 'compact-json' | 'toon' = 'json',
48
+ ): string {
49
+ return renderMachine(
165
50
  {
166
51
  changes: changes.map((c) => ({
167
52
  name: c.name,
@@ -174,7 +59,6 @@ export function renderChangesJson(changes: readonly ChangeSummary[]): string {
174
59
  status: statusFor(c.totalTasks, c.completedTasks),
175
60
  })),
176
61
  },
177
- null,
178
- 2,
62
+ mode,
179
63
  );
180
64
  }