@leonarto/spec-embryo 0.1.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/README.md +156 -0
- package/package.json +48 -0
- package/src/backends/base.ts +18 -0
- package/src/backends/deterministic.ts +105 -0
- package/src/backends/index.ts +26 -0
- package/src/backends/prompt.ts +169 -0
- package/src/backends/subprocess.ts +198 -0
- package/src/cli.ts +111 -0
- package/src/commands/agents.ts +16 -0
- package/src/commands/current.ts +95 -0
- package/src/commands/doctor.ts +12 -0
- package/src/commands/handoff.ts +64 -0
- package/src/commands/init.ts +101 -0
- package/src/commands/reshape.ts +20 -0
- package/src/commands/resume.ts +19 -0
- package/src/commands/spec.ts +108 -0
- package/src/commands/status.ts +98 -0
- package/src/commands/task.ts +190 -0
- package/src/commands/ui.ts +35 -0
- package/src/domain.ts +357 -0
- package/src/engine.ts +290 -0
- package/src/frontmatter.ts +83 -0
- package/src/index.ts +75 -0
- package/src/paths.ts +32 -0
- package/src/repository.ts +807 -0
- package/src/services/adoption.ts +169 -0
- package/src/services/agents.ts +191 -0
- package/src/services/dashboard.ts +776 -0
- package/src/services/details.ts +453 -0
- package/src/services/doctor.ts +452 -0
- package/src/services/layout.ts +420 -0
- package/src/services/spec-answer-evaluation.ts +103 -0
- package/src/services/spec-import.ts +217 -0
- package/src/services/spec-questions.ts +343 -0
- package/src/services/ui.ts +34 -0
- package/src/storage.ts +57 -0
- package/src/templates.ts +270 -0
- package/tsconfig.json +17 -0
- package/web/package.json +24 -0
- package/web/src/app.css +83 -0
- package/web/src/app.d.ts +6 -0
- package/web/src/app.html +11 -0
- package/web/src/lib/components/AnalysisFilters.svelte +293 -0
- package/web/src/lib/components/DocumentBody.svelte +100 -0
- package/web/src/lib/components/MultiSelectDropdown.svelte +280 -0
- package/web/src/lib/components/SelectDropdown.svelte +265 -0
- package/web/src/lib/server/project-root.ts +34 -0
- package/web/src/lib/task-board.ts +20 -0
- package/web/src/routes/+layout.server.ts +57 -0
- package/web/src/routes/+layout.svelte +421 -0
- package/web/src/routes/+layout.ts +1 -0
- package/web/src/routes/+page.svelte +530 -0
- package/web/src/routes/specs/+page.svelte +416 -0
- package/web/src/routes/specs/[specId]/+page.server.ts +81 -0
- package/web/src/routes/specs/[specId]/+page.svelte +675 -0
- package/web/src/routes/tasks/+page.svelte +341 -0
- package/web/src/routes/tasks/[taskId]/+page.server.ts +12 -0
- package/web/src/routes/tasks/[taskId]/+page.svelte +431 -0
- package/web/src/routes/timeline/+page.svelte +1093 -0
- package/web/svelte.config.js +10 -0
- package/web/tsconfig.json +9 -0
- package/web/vite.config.ts +11 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type {
|
|
3
|
+
CurrentStateDocument,
|
|
4
|
+
HandoffDocument,
|
|
5
|
+
ProjectConfig,
|
|
6
|
+
SpecDocument,
|
|
7
|
+
TaskDocument,
|
|
8
|
+
} from "../domain.ts";
|
|
9
|
+
import {
|
|
10
|
+
parseCurrentStateDocument,
|
|
11
|
+
parseHandoffDocument,
|
|
12
|
+
parseProjectConfig,
|
|
13
|
+
parseSpecDocument,
|
|
14
|
+
parseTaskDocument,
|
|
15
|
+
} from "../domain.ts";
|
|
16
|
+
import { parseTomlFrontmatter } from "../frontmatter.ts";
|
|
17
|
+
import { createDefaultConfig, readProjectConfig } from "../repository.ts";
|
|
18
|
+
import { resolveProjectPaths } from "../paths.ts";
|
|
19
|
+
import { buildAdoptionGuidance } from "./adoption.ts";
|
|
20
|
+
import { inspectAdoptionState, type AdoptionInspection } from "./layout.ts";
|
|
21
|
+
import { listMarkdownFiles, pathExists, readUtf8 } from "../storage.ts";
|
|
22
|
+
|
|
23
|
+
export type DoctorSeverity = "blocking" | "warning" | "info";
|
|
24
|
+
export type DoctorOverallStatus = "healthy" | "needs-attention" | "blocked";
|
|
25
|
+
|
|
26
|
+
export interface DoctorFinding {
|
|
27
|
+
severity: DoctorSeverity;
|
|
28
|
+
code: string;
|
|
29
|
+
message: string;
|
|
30
|
+
path?: string;
|
|
31
|
+
hint?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DoctorSummary {
|
|
35
|
+
overall: DoctorOverallStatus;
|
|
36
|
+
blockingCount: number;
|
|
37
|
+
warningCount: number;
|
|
38
|
+
infoCount: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DoctorReport {
|
|
42
|
+
summary: DoctorSummary;
|
|
43
|
+
adoption: AdoptionInspection;
|
|
44
|
+
config: {
|
|
45
|
+
exists: boolean;
|
|
46
|
+
valid: boolean;
|
|
47
|
+
memoryDir?: string;
|
|
48
|
+
};
|
|
49
|
+
counts: {
|
|
50
|
+
specs: number;
|
|
51
|
+
tasks: number;
|
|
52
|
+
handoffs: number;
|
|
53
|
+
};
|
|
54
|
+
findings: DoctorFinding[];
|
|
55
|
+
guidance: ReturnType<typeof buildAdoptionGuidance>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface SafeParseResult<T> {
|
|
59
|
+
documents: T[];
|
|
60
|
+
findings: DoctorFinding[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function pushFinding(
|
|
64
|
+
findings: DoctorFinding[],
|
|
65
|
+
severity: DoctorSeverity,
|
|
66
|
+
code: string,
|
|
67
|
+
message: string,
|
|
68
|
+
path?: string,
|
|
69
|
+
hint?: string,
|
|
70
|
+
): void {
|
|
71
|
+
findings.push({ severity, code, message, path, hint });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function safeParseCollection<T>(
|
|
75
|
+
dirPath: string,
|
|
76
|
+
parser: (filePath: string, attributes: unknown, body: string) => T,
|
|
77
|
+
options: {
|
|
78
|
+
missingDirSeverity: DoctorSeverity;
|
|
79
|
+
missingDirCode: string;
|
|
80
|
+
missingDirMessage: string;
|
|
81
|
+
missingDirHint?: string;
|
|
82
|
+
},
|
|
83
|
+
): Promise<SafeParseResult<T>> {
|
|
84
|
+
const findings: DoctorFinding[] = [];
|
|
85
|
+
|
|
86
|
+
if (!(await pathExists(dirPath))) {
|
|
87
|
+
pushFinding(
|
|
88
|
+
findings,
|
|
89
|
+
options.missingDirSeverity,
|
|
90
|
+
options.missingDirCode,
|
|
91
|
+
options.missingDirMessage,
|
|
92
|
+
dirPath,
|
|
93
|
+
options.missingDirHint,
|
|
94
|
+
);
|
|
95
|
+
return { documents: [], findings };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const files = await listMarkdownFiles(dirPath);
|
|
99
|
+
const documents: T[] = [];
|
|
100
|
+
|
|
101
|
+
for (const filePath of files) {
|
|
102
|
+
try {
|
|
103
|
+
const raw = await readUtf8(filePath);
|
|
104
|
+
const { attributes, body } = parseTomlFrontmatter(raw);
|
|
105
|
+
documents.push(parser(filePath, attributes, body));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
108
|
+
pushFinding(findings, "blocking", "parse-error", message, filePath);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { documents, findings };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildSummary(findings: DoctorFinding[]): DoctorSummary {
|
|
116
|
+
const blockingCount = findings.filter((finding) => finding.severity === "blocking").length;
|
|
117
|
+
const warningCount = findings.filter((finding) => finding.severity === "warning").length;
|
|
118
|
+
const infoCount = findings.filter((finding) => finding.severity === "info").length;
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
overall: blockingCount > 0 ? "blocked" : warningCount > 0 ? "needs-attention" : "healthy",
|
|
122
|
+
blockingCount,
|
|
123
|
+
warningCount,
|
|
124
|
+
infoCount,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function validateSpecTaskLinks(specs: SpecDocument[], tasks: TaskDocument[]): DoctorFinding[] {
|
|
129
|
+
const findings: DoctorFinding[] = [];
|
|
130
|
+
const specIds = new Set(specs.map((spec) => spec.id));
|
|
131
|
+
const taskIds = new Set(tasks.map((task) => task.id));
|
|
132
|
+
|
|
133
|
+
for (const spec of specs) {
|
|
134
|
+
for (const taskId of spec.taskIds) {
|
|
135
|
+
if (!taskIds.has(taskId)) {
|
|
136
|
+
pushFinding(
|
|
137
|
+
findings,
|
|
138
|
+
"warning",
|
|
139
|
+
"missing-task-link",
|
|
140
|
+
`${spec.id} references missing task ${taskId}.`,
|
|
141
|
+
spec.filePath,
|
|
142
|
+
"Either create the missing task or remove the stale task_ids backlink.",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const task of tasks) {
|
|
149
|
+
for (const specId of task.specIds) {
|
|
150
|
+
if (!specIds.has(specId)) {
|
|
151
|
+
pushFinding(
|
|
152
|
+
findings,
|
|
153
|
+
"warning",
|
|
154
|
+
"missing-spec-link",
|
|
155
|
+
`${task.id} references missing spec ${specId}.`,
|
|
156
|
+
task.filePath,
|
|
157
|
+
"Either create the missing spec or remove the stale spec_ids reference.",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const dependencyId of task.dependsOn) {
|
|
163
|
+
if (!taskIds.has(dependencyId)) {
|
|
164
|
+
pushFinding(
|
|
165
|
+
findings,
|
|
166
|
+
"warning",
|
|
167
|
+
"missing-task-dependency",
|
|
168
|
+
`${task.id} depends on missing task ${dependencyId}.`,
|
|
169
|
+
task.filePath,
|
|
170
|
+
"Fix the depends_on list so ready/blocker derivation stays trustworthy.",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const blockedById of task.blockedBy) {
|
|
176
|
+
if (!taskIds.has(blockedById)) {
|
|
177
|
+
pushFinding(
|
|
178
|
+
findings,
|
|
179
|
+
"warning",
|
|
180
|
+
"missing-task-blocker",
|
|
181
|
+
`${task.id} is blocked by missing task ${blockedById}.`,
|
|
182
|
+
task.filePath,
|
|
183
|
+
"Fix the blocked_by list so manual blocker tracking stays trustworthy.",
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return findings;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function validateCurrentStateLinks(currentState: CurrentStateDocument | undefined, specs: SpecDocument[], tasks: TaskDocument[]): DoctorFinding[] {
|
|
193
|
+
if (!currentState) {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const findings: DoctorFinding[] = [];
|
|
198
|
+
const specIds = new Set(specs.map((spec) => spec.id));
|
|
199
|
+
const taskIds = new Set(tasks.map((task) => task.id));
|
|
200
|
+
|
|
201
|
+
for (const specId of currentState.activeSpecIds) {
|
|
202
|
+
if (!specIds.has(specId)) {
|
|
203
|
+
pushFinding(
|
|
204
|
+
findings,
|
|
205
|
+
"warning",
|
|
206
|
+
"missing-current-spec",
|
|
207
|
+
`Current state references missing active spec ${specId}.`,
|
|
208
|
+
currentState.filePath,
|
|
209
|
+
"Update current.md so active_spec_ids only includes real specs.",
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const taskId of [...currentState.activeTaskIds, ...currentState.blockedTaskIds]) {
|
|
215
|
+
if (!taskIds.has(taskId)) {
|
|
216
|
+
pushFinding(
|
|
217
|
+
findings,
|
|
218
|
+
"warning",
|
|
219
|
+
"missing-current-task",
|
|
220
|
+
`Current state references missing task ${taskId}.`,
|
|
221
|
+
currentState.filePath,
|
|
222
|
+
"Update current.md so active_task_ids and blocked_task_ids only include real tasks.",
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return findings;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function buildDoctorReport(rootDir: string): Promise<DoctorReport> {
|
|
231
|
+
const configFile = join(rootDir, ".specpm", "config.toml");
|
|
232
|
+
const configExists = await pathExists(configFile);
|
|
233
|
+
const findings: DoctorFinding[] = [];
|
|
234
|
+
let config: ProjectConfig | undefined;
|
|
235
|
+
|
|
236
|
+
if (configExists) {
|
|
237
|
+
try {
|
|
238
|
+
config = await readProjectConfig(rootDir);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
241
|
+
pushFinding(
|
|
242
|
+
findings,
|
|
243
|
+
"blocking",
|
|
244
|
+
"invalid-config",
|
|
245
|
+
`Config could not be parsed: ${message}`,
|
|
246
|
+
configFile,
|
|
247
|
+
"Repair .specpm/config.toml before running normal Spec Embryo workflows.",
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
pushFinding(
|
|
252
|
+
findings,
|
|
253
|
+
"warning",
|
|
254
|
+
"missing-config",
|
|
255
|
+
"No .specpm/config.toml exists yet.",
|
|
256
|
+
configFile,
|
|
257
|
+
"Run `spm init` to adopt this repo into the canonical Spec Embryo layout.",
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const adoption = await inspectAdoptionState(rootDir, { config });
|
|
262
|
+
for (const finding of adoption.findings) {
|
|
263
|
+
pushFinding(findings, "info", "adoption-inspection", finding);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const effectiveConfig = config ?? createDefaultConfig(rootDir);
|
|
267
|
+
const paths = resolveProjectPaths(rootDir, effectiveConfig);
|
|
268
|
+
let currentState: CurrentStateDocument | undefined;
|
|
269
|
+
|
|
270
|
+
if (!config && adoption.state === "partially-adopted") {
|
|
271
|
+
pushFinding(
|
|
272
|
+
findings,
|
|
273
|
+
"warning",
|
|
274
|
+
"partial-adoption",
|
|
275
|
+
"The repo shows Spec Embryo signals without a valid config, so the managed layout is not fully trustworthy yet.",
|
|
276
|
+
undefined,
|
|
277
|
+
"Run `spm init` to complete adoption, then confirm whether a migration toward the canonical layout is wanted.",
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!(await pathExists(paths.stateFile))) {
|
|
282
|
+
if (config) {
|
|
283
|
+
pushFinding(
|
|
284
|
+
findings,
|
|
285
|
+
"blocking",
|
|
286
|
+
"missing-current-state",
|
|
287
|
+
"The configured current state file is missing.",
|
|
288
|
+
paths.stateFile,
|
|
289
|
+
"Run `spm init --force` only if you intentionally want to reseed missing files, or recreate current.md manually.",
|
|
290
|
+
);
|
|
291
|
+
} else if (adoption.state === "partially-adopted") {
|
|
292
|
+
pushFinding(
|
|
293
|
+
findings,
|
|
294
|
+
"warning",
|
|
295
|
+
"missing-current-state",
|
|
296
|
+
"A partially adopted repo is missing its current state file.",
|
|
297
|
+
paths.stateFile,
|
|
298
|
+
"Complete adoption with `spm init` so current.md exists before relying on resume or status flows.",
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
try {
|
|
303
|
+
const raw = await readUtf8(paths.stateFile);
|
|
304
|
+
const { attributes, body } = parseTomlFrontmatter(raw);
|
|
305
|
+
currentState = parseCurrentStateDocument(paths.stateFile, attributes, body);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
308
|
+
pushFinding(
|
|
309
|
+
findings,
|
|
310
|
+
config ? "blocking" : "warning",
|
|
311
|
+
"invalid-current-state",
|
|
312
|
+
`Current state could not be parsed: ${message}`,
|
|
313
|
+
paths.stateFile,
|
|
314
|
+
"Repair the current state frontmatter so status and resume flows can load it safely.",
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const specsResult = await safeParseCollection(paths.specsDir, parseSpecDocument, {
|
|
320
|
+
missingDirSeverity: config ? "blocking" : "warning",
|
|
321
|
+
missingDirCode: "missing-specs-dir",
|
|
322
|
+
missingDirMessage: "The configured specs directory is missing.",
|
|
323
|
+
missingDirHint: "Create the managed specs directory or complete adoption with `spm init`.",
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const tasksResult = await safeParseCollection(paths.tasksDir, parseTaskDocument, {
|
|
327
|
+
missingDirSeverity: config ? "blocking" : "warning",
|
|
328
|
+
missingDirCode: "missing-tasks-dir",
|
|
329
|
+
missingDirMessage: "The configured tasks directory is missing.",
|
|
330
|
+
missingDirHint: "Create the managed tasks directory or complete adoption with `spm init`.",
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const handoffsResult = await safeParseCollection(paths.handoffsDir, parseHandoffDocument, {
|
|
334
|
+
missingDirSeverity: config ? "warning" : "info",
|
|
335
|
+
missingDirCode: "missing-handoffs-dir",
|
|
336
|
+
missingDirMessage: "The configured handoffs directory is missing.",
|
|
337
|
+
missingDirHint: "Create the managed handoffs directory if you want durable interruption checkpoints.",
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
findings.push(...specsResult.findings, ...tasksResult.findings, ...handoffsResult.findings);
|
|
341
|
+
|
|
342
|
+
if (config && !(await pathExists(paths.skillsDir))) {
|
|
343
|
+
pushFinding(
|
|
344
|
+
findings,
|
|
345
|
+
"warning",
|
|
346
|
+
"missing-skills-dir",
|
|
347
|
+
"The configured skills directory is missing.",
|
|
348
|
+
paths.skillsDir,
|
|
349
|
+
"Create the managed skills directory if repo-local agent guidance should live there.",
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
findings.push(...validateSpecTaskLinks(specsResult.documents, tasksResult.documents));
|
|
354
|
+
findings.push(...validateCurrentStateLinks(currentState, specsResult.documents, tasksResult.documents));
|
|
355
|
+
|
|
356
|
+
if (config && specsResult.documents.length === 0) {
|
|
357
|
+
pushFinding(
|
|
358
|
+
findings,
|
|
359
|
+
"warning",
|
|
360
|
+
"empty-specs",
|
|
361
|
+
"No spec documents were found in the configured specs directory.",
|
|
362
|
+
paths.specsDir,
|
|
363
|
+
"Create or import at least one spec so linked work has a durable planning anchor.",
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (config && tasksResult.documents.length === 0) {
|
|
368
|
+
pushFinding(
|
|
369
|
+
findings,
|
|
370
|
+
"warning",
|
|
371
|
+
"empty-tasks",
|
|
372
|
+
"No task documents were found in the configured tasks directory.",
|
|
373
|
+
paths.tasksDir,
|
|
374
|
+
"Create linked tasks or import legacy execution notes so the workflow becomes actionable.",
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const guidance = buildAdoptionGuidance({
|
|
379
|
+
adoption,
|
|
380
|
+
memoryDir: config?.memoryDir ?? adoption.layoutChoice.memoryDir ?? "docs/spm",
|
|
381
|
+
doctorFindings: findings,
|
|
382
|
+
commandContext: "doctor",
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
summary: buildSummary(findings),
|
|
387
|
+
adoption,
|
|
388
|
+
config: {
|
|
389
|
+
exists: configExists,
|
|
390
|
+
valid: Boolean(config),
|
|
391
|
+
memoryDir: config?.memoryDir,
|
|
392
|
+
},
|
|
393
|
+
counts: {
|
|
394
|
+
specs: specsResult.documents.length,
|
|
395
|
+
tasks: tasksResult.documents.length,
|
|
396
|
+
handoffs: handoffsResult.documents.length,
|
|
397
|
+
},
|
|
398
|
+
findings,
|
|
399
|
+
guidance,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function renderDoctorReport(report: DoctorReport): string {
|
|
404
|
+
const lines = [
|
|
405
|
+
"Spec Embryo doctor",
|
|
406
|
+
`Status: ${report.summary.overall}`,
|
|
407
|
+
`Adoption state: ${report.adoption.state}`,
|
|
408
|
+
`Config: ${report.config.valid ? `valid (${report.config.memoryDir})` : report.config.exists ? "invalid" : "missing"}`,
|
|
409
|
+
`Counts: specs ${report.counts.specs} | tasks ${report.counts.tasks} | handoffs ${report.counts.handoffs}`,
|
|
410
|
+
"",
|
|
411
|
+
`Findings: blocking ${report.summary.blockingCount} | warnings ${report.summary.warningCount} | info ${report.summary.infoCount}`,
|
|
412
|
+
];
|
|
413
|
+
|
|
414
|
+
for (const severity of ["blocking", "warning", "info"] as const) {
|
|
415
|
+
const matches = report.findings.filter((finding) => finding.severity === severity);
|
|
416
|
+
if (matches.length === 0) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
lines.push("", `${severity[0]!.toUpperCase()}${severity.slice(1)} findings:`);
|
|
421
|
+
for (const finding of matches) {
|
|
422
|
+
const pathLabel = finding.path ? ` [${finding.path}]` : "";
|
|
423
|
+
lines.push(`- (${finding.code}) ${finding.message}${pathLabel}`);
|
|
424
|
+
if (finding.hint) {
|
|
425
|
+
lines.push(` hint: ${finding.hint}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
lines.push(
|
|
431
|
+
"",
|
|
432
|
+
"Adoption summary:",
|
|
433
|
+
`- ${report.guidance.overview}`,
|
|
434
|
+
"",
|
|
435
|
+
"What was found:",
|
|
436
|
+
...report.guidance.whatFound.map((item) => `- ${item}`),
|
|
437
|
+
"",
|
|
438
|
+
"What changed:",
|
|
439
|
+
...report.guidance.whatChanged.map((item) => `- ${item}`),
|
|
440
|
+
"",
|
|
441
|
+
"What still needs migration or follow-up:",
|
|
442
|
+
...report.guidance.remainingWork.map((item) => `- ${item}`),
|
|
443
|
+
"",
|
|
444
|
+
"Recommended next commands:",
|
|
445
|
+
...report.guidance.recommendedCommands.map((item) => `- ${item.command} — ${item.reason}`),
|
|
446
|
+
"",
|
|
447
|
+
"First-week sequence:",
|
|
448
|
+
...report.guidance.firstWeekSequence.map((item, index) => `${index + 1}. ${item}`),
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
return lines.join("\n");
|
|
452
|
+
}
|