@llman-sdd/core 0.3.0 → 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.
- package/package.json +2 -1
- package/src/archive/freeze.ts +86 -18
- package/src/archive/frozenCard.ts +105 -0
- package/src/archive/sevenzip.ts +15 -13
- package/src/change/closeOutHarness.ts +29 -0
- package/src/change/collect.ts +140 -0
- package/src/change/frontmatter.ts +48 -6
- package/src/change/id.ts +2 -6
- package/src/change/lifecycle.ts +285 -86
- package/src/change/nextId.ts +63 -2
- package/src/change/resolve.ts +2 -2
- package/src/change/tasks.ts +59 -0
- package/src/config/changeId.ts +14 -12
- package/src/config/load.ts +14 -0
- package/src/config/schema.ts +4 -41
- package/src/config/surface.ts +6 -36
- package/src/context/indexStore.ts +7 -3
- package/src/context/retrieve.ts +8 -10
- package/src/context/tree.ts +28 -24
- package/src/git/spawnGit.ts +90 -2
- package/src/index.ts +67 -59
- package/src/init/defaultConfig.ts +1 -5
- package/src/init/init.ts +19 -4
- package/src/ports.ts +1 -7
- package/src/project/migrateNotes.ts +104 -0
- package/src/render/machine.ts +30 -0
- package/src/report/collect.ts +11 -127
- package/src/report/graph/analysis.ts +152 -0
- package/src/report/graph/deps.ts +30 -0
- package/src/report/graph/graphData.ts +53 -0
- package/src/report/graph/nodes.ts +130 -0
- package/src/report/graph/render.ts +83 -0
- package/src/report/graph/types.ts +47 -0
- package/src/report/graph.ts +9 -381
- package/src/report/show.ts +20 -22
- package/src/report/specHelpers.ts +42 -21
- package/src/report/specs.ts +23 -25
- package/src/review/review.ts +45 -30
- package/src/spec/authoring.ts +91 -63
- package/src/spec/ir.ts +43 -15
- package/src/spec/migrateNative.ts +167 -0
- package/src/spec/parser.ts +73 -77
- package/src/spec/reqRegistry.ts +8 -9
- package/src/templates/embedded.ts +10 -16
- package/src/templates/engine.ts +10 -5
- package/src/templates/locale.ts +1 -1
- package/src/templates/skills.ts +4 -5
- package/src/validation/changeCheck.ts +128 -105
- package/src/validation/harness.ts +161 -0
- package/src/validation/staleness.ts +9 -5
- package/src/validation/validate.ts +60 -88
- package/templates/en/skills/llman-sdd-apply-cycle.md +20 -28
- package/templates/en/skills/llman-sdd-apply.md +58 -76
- package/templates/en/skills/llman-sdd-arch-review.md +12 -19
- package/templates/en/skills/llman-sdd-archive.md +27 -42
- package/templates/en/skills/llman-sdd-continue.md +17 -24
- package/templates/en/skills/llman-sdd-draft.md +17 -28
- package/templates/en/skills/llman-sdd-explore.md +29 -43
- package/templates/en/skills/llman-sdd-ff.md +12 -17
- package/templates/en/skills/llman-sdd-graph.md +14 -32
- package/templates/en/skills/llman-sdd-propose.md +48 -63
- package/templates/en/skills/llman-sdd-quick.md +12 -27
- package/templates/en/skills/llman-sdd-research.md +13 -24
- package/templates/en/skills/llman-sdd-specs-compact.md +14 -39
- package/templates/en/skills/llman-sdd-validate.md +11 -15
- package/templates/en/skills/llman-sdd-verify.md +23 -44
- package/templates/en/skills/llman-sdd-wayfinder.md +18 -22
- package/templates/en/units/skills/cli-footer.md +2 -0
- package/templates/en/units/skills/git-native-flow-brief.md +7 -6
- package/templates/en/units/skills/git-native-flow.md +21 -11
- package/templates/en/units/skills/human-readable-summary.md +2 -3
- package/templates/en/units/skills/stage-guard.md +7 -7
- package/templates/en/units/skills/structured-protocol.md +5 -8
- package/templates/en/units/skills/validation-hints.md +10 -14
- package/templates/en/units/spec/feature-contract.md +27 -16
- package/templates/en/units/workflow/archive-freeze-guidance.md +6 -3
- package/templates/zh-Hans/skills/llman-sdd-apply-cycle.md +23 -31
- package/templates/zh-Hans/skills/llman-sdd-apply.md +63 -81
- package/templates/zh-Hans/skills/llman-sdd-arch-review.md +21 -28
- package/templates/zh-Hans/skills/llman-sdd-archive.md +29 -44
- package/templates/zh-Hans/skills/llman-sdd-continue.md +17 -24
- package/templates/zh-Hans/skills/llman-sdd-draft.md +18 -29
- package/templates/zh-Hans/skills/llman-sdd-explore.md +34 -48
- package/templates/zh-Hans/skills/llman-sdd-ff.md +13 -18
- package/templates/zh-Hans/skills/llman-sdd-graph.md +16 -34
- package/templates/zh-Hans/skills/llman-sdd-propose.md +51 -65
- package/templates/zh-Hans/skills/llman-sdd-quick.md +15 -30
- package/templates/zh-Hans/skills/llman-sdd-research.md +17 -28
- package/templates/zh-Hans/skills/llman-sdd-specs-compact.md +15 -40
- package/templates/zh-Hans/skills/llman-sdd-validate.md +11 -15
- package/templates/zh-Hans/skills/llman-sdd-verify.md +26 -47
- package/templates/zh-Hans/skills/llman-sdd-wayfinder.md +25 -29
- package/templates/zh-Hans/units/skills/cli-footer.md +2 -0
- package/templates/zh-Hans/units/skills/git-native-flow-brief.md +7 -6
- package/templates/zh-Hans/units/skills/git-native-flow.md +22 -12
- package/templates/zh-Hans/units/skills/human-readable-summary.md +4 -5
- package/templates/zh-Hans/units/skills/stage-guard.md +9 -9
- package/templates/zh-Hans/units/skills/structured-protocol.md +5 -8
- package/templates/zh-Hans/units/skills/validation-hints.md +10 -14
- package/templates/zh-Hans/units/spec/feature-contract.md +25 -16
- package/templates/zh-Hans/units/workflow/archive-freeze-guidance.md +6 -2
- package/templates/en/skills/llman-sdd-onboard.md +0 -34
- package/templates/en/skills/llman-sdd-show.md +0 -24
- package/templates/en/units/migrate-prompt.md +0 -28
- package/templates/zh-Hans/skills/llman-sdd-onboard.md +0 -34
- package/templates/zh-Hans/skills/llman-sdd-show.md +0 -24
- 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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
114
|
-
export {
|
|
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
|
-
|
|
173
|
-
|
|
179
|
+
countTasks,
|
|
180
|
+
firstH1,
|
|
174
181
|
type ChangeFsIo,
|
|
175
182
|
type ChangeStatus,
|
|
176
183
|
type ChangeSummary,
|
|
177
|
-
} from './
|
|
178
|
-
export {
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|
package/src/report/collect.ts
CHANGED
|
@@ -1,128 +1,10 @@
|
|
|
1
|
+
import { type ChangeSummary, statusFor } from '../change/collect.ts';
|
|
1
2
|
/**
|
|
2
|
-
* Change
|
|
3
|
-
*
|
|
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 {
|
|
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(
|
|
164
|
-
|
|
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
|
-
|
|
178
|
-
2,
|
|
62
|
+
mode,
|
|
179
63
|
);
|
|
180
64
|
}
|