@vibecompass/vibecompass 0.6.0 → 0.8.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/CHANGELOG.md +14 -0
- package/README.md +95 -691
- package/package.json +1 -1
- package/src/cli.js +170 -4
- package/src/docs-review.js +641 -24
- package/src/init.js +139 -1
- package/src/setup.js +1 -0
- package/src/status.js +55 -1
- package/src/sync-binding.js +307 -0
- package/src/sync.js +31 -24
- package/src/workflows/registry.js +9 -5
package/src/docs-review.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { access, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { access, mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createInterface } from 'node:readline/promises';
|
|
4
4
|
import { parseFrontmatter } from './frontmatter.js';
|
|
5
|
+
import { sha256Text } from './hash.js';
|
|
5
6
|
import { writeStateManifest } from './manifest.js';
|
|
6
7
|
import { parseSimpleYaml } from './simple-yaml.js';
|
|
8
|
+
import { readSyncCursor, resolveSyncBinding } from './sync-binding.js';
|
|
7
9
|
|
|
8
10
|
const DOCS_REVIEW_PROMPT_VERSION = 'VibeCompass Docs Review Prompt v2';
|
|
9
11
|
const ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES = 12000;
|
|
12
|
+
const COVERAGE_PLAN_OUTPUT_VERSION = 1;
|
|
13
|
+
const COVERAGE_PLAN_STATUSES = new Set(['accepted', 'deferred', 'missing']);
|
|
14
|
+
const COVERAGE_LEVELS = new Set(['comprehensive', 'partial', 'initial', 'missing']);
|
|
15
|
+
const REBUILD_STALE_POLICIES = new Set(['keep', 'archive']);
|
|
10
16
|
|
|
11
17
|
export async function preflightDocsReview(options = {}, environment = {}) {
|
|
12
18
|
validateDocsReviewMode(options);
|
|
@@ -21,6 +27,18 @@ export async function preflightDocsReview(options = {}, environment = {}) {
|
|
|
21
27
|
outputPath: options.outputPath,
|
|
22
28
|
llm: options.llm,
|
|
23
29
|
model: options.model,
|
|
30
|
+
refreshIndex: options.refreshIndex,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (options.rebuild) {
|
|
35
|
+
return rebuildDocsReview({
|
|
36
|
+
rootDir,
|
|
37
|
+
statePath,
|
|
38
|
+
apply: options.apply,
|
|
39
|
+
dryRun: options.dryRun,
|
|
40
|
+
scopePath: options.scopePath,
|
|
41
|
+
stalePolicy: options.stalePolicy,
|
|
24
42
|
});
|
|
25
43
|
}
|
|
26
44
|
|
|
@@ -66,6 +84,7 @@ export async function preflightDocsReview(options = {}, environment = {}) {
|
|
|
66
84
|
localProvider,
|
|
67
85
|
runLocal: shouldRunLocal(options),
|
|
68
86
|
anthropicEnvVar: options.anthropicEnvVar ?? 'ANTHROPIC_API_KEY',
|
|
87
|
+
syncTarget: options.syncTarget ?? null,
|
|
69
88
|
});
|
|
70
89
|
const reviewPrompt = renderReviewPrompt({
|
|
71
90
|
project,
|
|
@@ -183,8 +202,10 @@ async function applyDocsReviewOutput(options) {
|
|
|
183
202
|
: path.join(path.dirname(options.statePath), 'docs-review-output.md');
|
|
184
203
|
const source = await readFile(outputPath, 'utf8');
|
|
185
204
|
const blocks = parseArchitectureDocBlocks(source);
|
|
205
|
+
const coverageBlocks = parseCoveragePlanBlocks(source);
|
|
206
|
+
const decisionRecommendationBlocks = parseDecisionRecommendationBlocks(source);
|
|
186
207
|
|
|
187
|
-
if (blocks.length === 0) {
|
|
208
|
+
if (blocks.length === 0 && coverageBlocks.length === 0 && decisionRecommendationBlocks.length === 0) {
|
|
188
209
|
const malformedFenceCount = countMalformedArchitectureDocFences(source);
|
|
189
210
|
if (malformedFenceCount > 0) {
|
|
190
211
|
throw new Error(
|
|
@@ -192,13 +213,35 @@ async function applyDocsReviewOutput(options) {
|
|
|
192
213
|
);
|
|
193
214
|
}
|
|
194
215
|
|
|
216
|
+
const malformedCoveragePlanFenceCount = countMalformedCoveragePlanFences(source);
|
|
217
|
+
if (malformedCoveragePlanFenceCount > 0) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`Malformed coverage-plan fence found in ${outputPath}. Expected fence openings like \`\`\`vibecompass-coverage-plan version=1\`.`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const malformedDecisionRecommendationFenceCount = countMalformedDecisionRecommendationFences(source);
|
|
224
|
+
if (malformedDecisionRecommendationFenceCount > 0) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`Malformed decision recommendation fence found in ${outputPath}. Expected fence openings like \`\`\`vibecompass-decision-recommendation target=decisions/domain.md\`.`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
195
230
|
throw new Error(
|
|
196
|
-
`No accepted
|
|
231
|
+
`No accepted docs-review output blocks found in ${outputPath}. Expected fenced blocks like \`\`\`vibecompass-architecture-doc path=architecture/domain/feature/component.md\`, \`\`\`vibecompass-coverage-plan version=1\`, or \`\`\`vibecompass-decision-recommendation target=decisions/domain.md\`.`,
|
|
197
232
|
);
|
|
198
233
|
}
|
|
199
234
|
|
|
200
235
|
validateArchitectureDocBlocks(blocks);
|
|
201
|
-
const
|
|
236
|
+
const coverageProjection = buildCoverageProjection(coverageBlocks, {
|
|
237
|
+
outputPath,
|
|
238
|
+
outputHash: sha256Text(source),
|
|
239
|
+
});
|
|
240
|
+
const decisionRecommendations = validateDecisionRecommendationBlocks(decisionRecommendationBlocks);
|
|
241
|
+
const warnings = [
|
|
242
|
+
...createArchitectureDocSizeWarnings(blocks),
|
|
243
|
+
...createArchitectureDocQualityWarnings(blocks),
|
|
244
|
+
];
|
|
202
245
|
|
|
203
246
|
const applied = [];
|
|
204
247
|
for (const block of blocks) {
|
|
@@ -209,8 +252,31 @@ async function applyDocsReviewOutput(options) {
|
|
|
209
252
|
applied.push({ path: block.path, status: existed ? 'overwritten' : 'created' });
|
|
210
253
|
}
|
|
211
254
|
|
|
255
|
+
let appliedCoverage = null;
|
|
256
|
+
if (coverageProjection) {
|
|
257
|
+
const coveragePath = path.join(path.dirname(options.statePath), 'docs-review-coverage.json');
|
|
258
|
+
await writeFile(coveragePath, `${JSON.stringify(coverageProjection, null, 2)}\n`, 'utf8');
|
|
259
|
+
appliedCoverage = {
|
|
260
|
+
path: coveragePath,
|
|
261
|
+
area_count: coverageProjection.area_count,
|
|
262
|
+
coverage_score: coverageProjection.coverage_score,
|
|
263
|
+
statuses: coverageProjection.statuses,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const appliedDecisionCandidates = decisionRecommendations.length > 0
|
|
268
|
+
? await applyDecisionRecommendations({
|
|
269
|
+
rootDir: options.rootDir,
|
|
270
|
+
recommendations: decisionRecommendations,
|
|
271
|
+
refreshIndex: options.refreshIndex,
|
|
272
|
+
})
|
|
273
|
+
: { applied: [], warnings: [] };
|
|
274
|
+
warnings.push(...appliedDecisionCandidates.warnings);
|
|
275
|
+
|
|
276
|
+
const manifestResult = await writeStateManifest(options.rootDir);
|
|
212
277
|
const llm = normalizeOptionalString(options.llm) ?? current.llm ?? null;
|
|
213
278
|
const model = normalizeOptionalString(options.model) ?? current.model ?? null;
|
|
279
|
+
const appliedAt = new Date().toISOString();
|
|
214
280
|
const marker = {
|
|
215
281
|
...current,
|
|
216
282
|
status: 'completed',
|
|
@@ -218,11 +284,16 @@ async function applyDocsReviewOutput(options) {
|
|
|
218
284
|
...(model ? { model } : {}),
|
|
219
285
|
applied: {
|
|
220
286
|
output_path: outputPath,
|
|
287
|
+
output_hash: sha256Text(source),
|
|
288
|
+
local_root_revision: manifestResult.manifest.canonical.local_root_revision,
|
|
289
|
+
manifest_hash: manifestResult.manifest.canonical.manifest_hash,
|
|
221
290
|
architecture_docs: applied,
|
|
291
|
+
...(appliedCoverage ? { coverage: appliedCoverage } : {}),
|
|
292
|
+
...(appliedDecisionCandidates.applied.length > 0 ? { decision_candidates: appliedDecisionCandidates.applied } : {}),
|
|
222
293
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
223
|
-
applied_at:
|
|
294
|
+
applied_at: appliedAt,
|
|
224
295
|
},
|
|
225
|
-
completed_at:
|
|
296
|
+
completed_at: appliedAt,
|
|
226
297
|
};
|
|
227
298
|
|
|
228
299
|
await writeDocsReviewMarker(options.statePath, marker);
|
|
@@ -238,7 +309,11 @@ async function applyDocsReviewOutput(options) {
|
|
|
238
309
|
applied: marker.applied,
|
|
239
310
|
reviewPrompt: null,
|
|
240
311
|
warnings,
|
|
241
|
-
message:
|
|
312
|
+
message: renderApplyOutputMessage({
|
|
313
|
+
architectureCount: applied.length,
|
|
314
|
+
coverageApplied: Boolean(appliedCoverage),
|
|
315
|
+
decisionCount: appliedDecisionCandidates.applied.length,
|
|
316
|
+
}),
|
|
242
317
|
};
|
|
243
318
|
}
|
|
244
319
|
|
|
@@ -340,6 +415,92 @@ async function applyDecisionArtifact(options) {
|
|
|
340
415
|
};
|
|
341
416
|
}
|
|
342
417
|
|
|
418
|
+
async function rebuildDocsReview(options) {
|
|
419
|
+
const scopePath = normalizeRebuildScopePath(options.scopePath);
|
|
420
|
+
const stalePolicy = normalizeRebuildStalePolicy(options.stalePolicy);
|
|
421
|
+
const apply = Boolean(options.apply);
|
|
422
|
+
const architectureDocs = await listArchitectureDocs(options.rootDir, scopePath);
|
|
423
|
+
const requestedAt = new Date();
|
|
424
|
+
const archiveRoot = stalePolicy === 'archive'
|
|
425
|
+
? `state/docs-review-archive/${formatArchiveTimestamp(requestedAt)}`
|
|
426
|
+
: null;
|
|
427
|
+
const rebuildEntries = architectureDocs.map((docPath) => ({
|
|
428
|
+
path: docPath,
|
|
429
|
+
action: stalePolicy === 'archive' ? 'archive' : 'keep',
|
|
430
|
+
...(archiveRoot ? { archive_path: `${archiveRoot}/${docPath}` } : {}),
|
|
431
|
+
}));
|
|
432
|
+
|
|
433
|
+
if (!apply) {
|
|
434
|
+
return {
|
|
435
|
+
rootDir: options.rootDir,
|
|
436
|
+
statePath: options.statePath,
|
|
437
|
+
status: 'rebuild-preview',
|
|
438
|
+
recorded: false,
|
|
439
|
+
runtime: null,
|
|
440
|
+
hosted: null,
|
|
441
|
+
applied: null,
|
|
442
|
+
reviewPrompt: null,
|
|
443
|
+
rebuild: {
|
|
444
|
+
scope_path: scopePath,
|
|
445
|
+
stale_policy: stalePolicy,
|
|
446
|
+
architecture_docs: rebuildEntries,
|
|
447
|
+
architecture_doc_count: rebuildEntries.length,
|
|
448
|
+
archive_root: archiveRoot,
|
|
449
|
+
},
|
|
450
|
+
warnings: [],
|
|
451
|
+
message: 'Docs-review rebuild preview complete. Re-run with --apply to record the rebuild marker and perform the selected stale-doc action.',
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
for (const entry of rebuildEntries) {
|
|
456
|
+
if (entry.action !== 'archive') {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const sourcePath = path.join(options.rootDir, entry.path);
|
|
460
|
+
const targetPath = path.join(options.rootDir, entry.archive_path);
|
|
461
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
462
|
+
await rename(sourcePath, targetPath);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const manifestResult = await writeStateManifest(options.rootDir);
|
|
466
|
+
const appliedAt = new Date().toISOString();
|
|
467
|
+
const marker = {
|
|
468
|
+
status: 'rebuild-ready',
|
|
469
|
+
requested_at: requestedAt.toISOString(),
|
|
470
|
+
runtime: {
|
|
471
|
+
provider: 'local',
|
|
472
|
+
operation: 'rebuild',
|
|
473
|
+
},
|
|
474
|
+
rebuild: {
|
|
475
|
+
scope_path: scopePath,
|
|
476
|
+
stale_policy: stalePolicy,
|
|
477
|
+
architecture_docs: rebuildEntries,
|
|
478
|
+
architecture_doc_count: rebuildEntries.length,
|
|
479
|
+
...(archiveRoot ? { archive_root: path.join(options.rootDir, archiveRoot) } : {}),
|
|
480
|
+
local_root_revision: manifestResult.manifest.canonical.local_root_revision,
|
|
481
|
+
manifest_hash: manifestResult.manifest.canonical.manifest_hash,
|
|
482
|
+
applied_at: appliedAt,
|
|
483
|
+
},
|
|
484
|
+
completed_at: null,
|
|
485
|
+
};
|
|
486
|
+
await writeDocsReviewMarker(options.statePath, marker);
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
rootDir: options.rootDir,
|
|
490
|
+
statePath: options.statePath,
|
|
491
|
+
status: 'rebuild-ready',
|
|
492
|
+
runtime: marker.runtime,
|
|
493
|
+
hosted: null,
|
|
494
|
+
applied: null,
|
|
495
|
+
reviewPrompt: null,
|
|
496
|
+
rebuild: marker.rebuild,
|
|
497
|
+
warnings: [],
|
|
498
|
+
message: stalePolicy === 'archive'
|
|
499
|
+
? 'Archived selected architecture docs and recorded a rebuild marker. Run docs-review again, then apply accepted output.'
|
|
500
|
+
: 'Recorded a rebuild marker without moving architecture docs. Run docs-review again, then apply accepted output.',
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
343
504
|
async function readDocsReviewMarker(statePath) {
|
|
344
505
|
try {
|
|
345
506
|
return JSON.parse(await readFile(statePath, 'utf8'));
|
|
@@ -360,12 +521,28 @@ function validateDocsReviewMode(options) {
|
|
|
360
521
|
options.runLocalAnthropic ? '--run-local-anthropic' : null,
|
|
361
522
|
options.applyOutput ? '--apply-output' : null,
|
|
362
523
|
options.applyDecisionArtifact ? '--apply-decision-artifact' : null,
|
|
524
|
+
options.rebuild ? '--rebuild' : null,
|
|
363
525
|
options.complete ? '--complete' : null,
|
|
364
526
|
].filter(Boolean);
|
|
365
527
|
|
|
366
528
|
if (enabled.length > 1) {
|
|
367
529
|
throw new Error(`docs-review accepts only one execution mode at a time. Conflicting flags: ${enabled.join(', ')}.`);
|
|
368
530
|
}
|
|
531
|
+
if (options.apply && !options.rebuild) {
|
|
532
|
+
throw new Error('docs-review --apply is only valid with --rebuild.');
|
|
533
|
+
}
|
|
534
|
+
if (options.dryRun && !options.rebuild) {
|
|
535
|
+
throw new Error('docs-review --dry-run is only valid with --rebuild.');
|
|
536
|
+
}
|
|
537
|
+
if (options.stalePolicy && !options.rebuild) {
|
|
538
|
+
throw new Error('docs-review --stale-policy is only valid with --rebuild.');
|
|
539
|
+
}
|
|
540
|
+
if (options.scopePath && !options.rebuild) {
|
|
541
|
+
throw new Error('docs-review --path is only valid with --rebuild.');
|
|
542
|
+
}
|
|
543
|
+
if (options.apply && options.dryRun) {
|
|
544
|
+
throw new Error('docs-review --rebuild accepts only one of --dry-run or --apply.');
|
|
545
|
+
}
|
|
369
546
|
}
|
|
370
547
|
|
|
371
548
|
async function promptForReviewConfig(options, environment) {
|
|
@@ -442,11 +619,10 @@ function resolveDocsReviewRuntime(project, options) {
|
|
|
442
619
|
localProvider &&
|
|
443
620
|
typeof options.env?.[localProvider.credentialEnvVar] === 'string' &&
|
|
444
621
|
options.env[localProvider.credentialEnvVar].trim() !== '';
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
typeof project.sync.project_id === 'string';
|
|
622
|
+
// Throws on an explicitly requested unknown/incomplete --sync-target so a
|
|
623
|
+
// typo can never fall back to a different environment (D-236).
|
|
624
|
+
const hostedBinding = resolveSyncBinding(project, options.syncTarget ?? null);
|
|
625
|
+
const hasHostedBinding = Boolean(hostedBinding);
|
|
450
626
|
|
|
451
627
|
if (mode === 'local-only') {
|
|
452
628
|
if (options.runLocal && !localProvider) {
|
|
@@ -488,11 +664,10 @@ function resolveDocsReviewRuntime(project, options) {
|
|
|
488
664
|
if (hasHostedBinding) {
|
|
489
665
|
return {
|
|
490
666
|
provider: 'hosted',
|
|
491
|
-
api_url:
|
|
492
|
-
project_id:
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
: {}),
|
|
667
|
+
api_url: hostedBinding.apiUrl,
|
|
668
|
+
project_id: hostedBinding.projectId,
|
|
669
|
+
credential_env_var: hostedBinding.credentialEnvVar,
|
|
670
|
+
...(hostedBinding.target ? { sync_target: hostedBinding.target } : {}),
|
|
496
671
|
};
|
|
497
672
|
}
|
|
498
673
|
|
|
@@ -524,11 +699,10 @@ function resolveDocsReviewRuntime(project, options) {
|
|
|
524
699
|
|
|
525
700
|
return {
|
|
526
701
|
provider: 'hosted',
|
|
527
|
-
api_url:
|
|
528
|
-
project_id:
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
: {}),
|
|
702
|
+
api_url: hostedBinding.apiUrl,
|
|
703
|
+
project_id: hostedBinding.projectId,
|
|
704
|
+
credential_env_var: hostedBinding.credentialEnvVar,
|
|
705
|
+
...(hostedBinding.target ? { sync_target: hostedBinding.target } : {}),
|
|
532
706
|
};
|
|
533
707
|
}
|
|
534
708
|
|
|
@@ -624,6 +798,36 @@ function parseArchitectureDocBlocks(source) {
|
|
|
624
798
|
return blocks;
|
|
625
799
|
}
|
|
626
800
|
|
|
801
|
+
function parseCoveragePlanBlocks(source) {
|
|
802
|
+
const blocks = [];
|
|
803
|
+
const pattern = /^```vibecompass-coverage-plan\s+version=1\s*\n([\s\S]*?)^```[ \t]*$/gm;
|
|
804
|
+
let match;
|
|
805
|
+
|
|
806
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
807
|
+
blocks.push({
|
|
808
|
+
version: COVERAGE_PLAN_OUTPUT_VERSION,
|
|
809
|
+
content: match[1].replace(/\n$/, ''),
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return blocks;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function parseDecisionRecommendationBlocks(source) {
|
|
817
|
+
const blocks = [];
|
|
818
|
+
const pattern = /^```vibecompass-decision-recommendation\s+target=([^\s`]+)\s*\n([\s\S]*?)^```[ \t]*$/gm;
|
|
819
|
+
let match;
|
|
820
|
+
|
|
821
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
822
|
+
blocks.push({
|
|
823
|
+
targetPath: normalizeDecisionTargetPath(match[1]),
|
|
824
|
+
content: match[2].replace(/\n$/, ''),
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return blocks;
|
|
829
|
+
}
|
|
830
|
+
|
|
627
831
|
function countMalformedArchitectureDocFences(source) {
|
|
628
832
|
const fencePattern = /^```vibecompass-architecture-doc\b.*$/gm;
|
|
629
833
|
const acceptedOpeningPattern = /^```vibecompass-architecture-doc\s+path=[^\s`]+\s*$/;
|
|
@@ -639,6 +843,36 @@ function countMalformedArchitectureDocFences(source) {
|
|
|
639
843
|
return count;
|
|
640
844
|
}
|
|
641
845
|
|
|
846
|
+
function countMalformedCoveragePlanFences(source) {
|
|
847
|
+
const fencePattern = /^```vibecompass-coverage-plan\b.*$/gm;
|
|
848
|
+
const acceptedOpeningPattern = /^```vibecompass-coverage-plan\s+version=1\s*$/;
|
|
849
|
+
let count = 0;
|
|
850
|
+
let match;
|
|
851
|
+
|
|
852
|
+
while ((match = fencePattern.exec(source)) !== null) {
|
|
853
|
+
if (!acceptedOpeningPattern.test(match[0])) {
|
|
854
|
+
count += 1;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return count;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function countMalformedDecisionRecommendationFences(source) {
|
|
862
|
+
const fencePattern = /^```vibecompass-decision-recommendation\b.*$/gm;
|
|
863
|
+
const acceptedOpeningPattern = /^```vibecompass-decision-recommendation\s+target=[^\s`]+\s*$/;
|
|
864
|
+
let count = 0;
|
|
865
|
+
let match;
|
|
866
|
+
|
|
867
|
+
while ((match = fencePattern.exec(source)) !== null) {
|
|
868
|
+
if (!acceptedOpeningPattern.test(match[0])) {
|
|
869
|
+
count += 1;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return count;
|
|
874
|
+
}
|
|
875
|
+
|
|
642
876
|
function validateArchitectureDocBlocks(blocks) {
|
|
643
877
|
const seenPaths = new Set();
|
|
644
878
|
for (const block of blocks) {
|
|
@@ -650,6 +884,115 @@ function validateArchitectureDocBlocks(blocks) {
|
|
|
650
884
|
}
|
|
651
885
|
}
|
|
652
886
|
|
|
887
|
+
function buildCoverageProjection(blocks, options) {
|
|
888
|
+
if (blocks.length === 0) {
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
if (blocks.length > 1) {
|
|
892
|
+
throw new Error('Accepted docs-review output contains multiple coverage-plan blocks. Include at most one `vibecompass-coverage-plan version=1` block.');
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
let plan;
|
|
896
|
+
try {
|
|
897
|
+
plan = JSON.parse(blocks[0].content);
|
|
898
|
+
} catch (error) {
|
|
899
|
+
throw new Error(`Accepted coverage plan must contain valid JSON. ${error instanceof Error ? error.message : ''}`.trim());
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
|
|
903
|
+
throw new Error('Accepted coverage plan must contain a JSON object.');
|
|
904
|
+
}
|
|
905
|
+
if (!Array.isArray(plan.areas)) {
|
|
906
|
+
throw new Error('Accepted coverage plan requires an "areas" array.');
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const ids = new Set();
|
|
910
|
+
const areas = plan.areas.map((area, index) => normalizeCoverageArea(area, index, ids));
|
|
911
|
+
const statuses = countBy(areas, 'status');
|
|
912
|
+
const coverage_levels = countBy(areas, 'coverage');
|
|
913
|
+
const denominator = areas.length;
|
|
914
|
+
const acceptedCount = statuses.accepted ?? 0;
|
|
915
|
+
|
|
916
|
+
return {
|
|
917
|
+
version: COVERAGE_PLAN_OUTPUT_VERSION,
|
|
918
|
+
source: {
|
|
919
|
+
output_path: options.outputPath,
|
|
920
|
+
output_hash: options.outputHash,
|
|
921
|
+
},
|
|
922
|
+
projected_at: new Date().toISOString(),
|
|
923
|
+
summary: normalizeOptionalString(plan.summary),
|
|
924
|
+
area_count: areas.length,
|
|
925
|
+
coverage_score: denominator === 0 ? null : Number((acceptedCount / denominator).toFixed(4)),
|
|
926
|
+
statuses,
|
|
927
|
+
coverage_levels,
|
|
928
|
+
areas,
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function normalizeCoverageArea(area, index, ids) {
|
|
933
|
+
if (!area || typeof area !== 'object' || Array.isArray(area)) {
|
|
934
|
+
throw new Error(`Coverage plan area at index ${index} must be an object.`);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const id = normalizeOptionalString(area.id);
|
|
938
|
+
if (!id) {
|
|
939
|
+
throw new Error(`Coverage plan area at index ${index} requires non-empty "id".`);
|
|
940
|
+
}
|
|
941
|
+
if (ids.has(id)) {
|
|
942
|
+
throw new Error(`Coverage plan contains duplicate area id: ${id}`);
|
|
943
|
+
}
|
|
944
|
+
ids.add(id);
|
|
945
|
+
|
|
946
|
+
const status = normalizeOptionalString(area.status);
|
|
947
|
+
if (!COVERAGE_PLAN_STATUSES.has(status)) {
|
|
948
|
+
throw new Error(`Coverage plan area "${id}" requires status to be accepted, deferred, or missing.`);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const coverage = normalizeOptionalString(area.coverage);
|
|
952
|
+
if (!COVERAGE_LEVELS.has(coverage)) {
|
|
953
|
+
throw new Error(`Coverage plan area "${id}" requires coverage to be comprehensive, partial, initial, or missing.`);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const proposedPath = normalizeOptionalString(area.proposed_path);
|
|
957
|
+
if (proposedPath) {
|
|
958
|
+
const normalizedPath = normalizeBlockPath(proposedPath);
|
|
959
|
+
if (
|
|
960
|
+
!normalizedPath.startsWith('architecture/') ||
|
|
961
|
+
!normalizedPath.endsWith('.md') ||
|
|
962
|
+
path.posix.isAbsolute(normalizedPath) ||
|
|
963
|
+
normalizedPath.split('/').includes('..')
|
|
964
|
+
) {
|
|
965
|
+
throw new Error(`Coverage plan area "${id}" has invalid proposed_path: ${proposedPath}`);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return {
|
|
970
|
+
id,
|
|
971
|
+
domain: normalizeOptionalString(area.domain),
|
|
972
|
+
feature: normalizeOptionalString(area.feature),
|
|
973
|
+
component: normalizeOptionalString(area.component),
|
|
974
|
+
status,
|
|
975
|
+
coverage,
|
|
976
|
+
...(proposedPath ? { proposed_path: normalizeBlockPath(proposedPath) } : {}),
|
|
977
|
+
evidence: normalizeStringArray(area.evidence),
|
|
978
|
+
blindspots: normalizeStringArray(area.blindspots),
|
|
979
|
+
...(normalizeOptionalString(area.reason) ? { reason: normalizeOptionalString(area.reason) } : {}),
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function countBy(items, key) {
|
|
984
|
+
return items.reduce((counts, item) => {
|
|
985
|
+
counts[item[key]] = (counts[item[key]] ?? 0) + 1;
|
|
986
|
+
return counts;
|
|
987
|
+
}, {});
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function normalizeStringArray(value) {
|
|
991
|
+
return Array.isArray(value)
|
|
992
|
+
? value.filter((item) => typeof item === 'string').map((item) => item.trim()).filter(Boolean)
|
|
993
|
+
: [];
|
|
994
|
+
}
|
|
995
|
+
|
|
653
996
|
function normalizeBlockPath(value) {
|
|
654
997
|
return String(value ?? '').trim().replaceAll('\\', '/');
|
|
655
998
|
}
|
|
@@ -697,6 +1040,49 @@ function createArchitectureDocSizeWarnings(blocks) {
|
|
|
697
1040
|
.filter(Boolean);
|
|
698
1041
|
}
|
|
699
1042
|
|
|
1043
|
+
function createArchitectureDocQualityWarnings(blocks) {
|
|
1044
|
+
const warnings = [];
|
|
1045
|
+
|
|
1046
|
+
for (const block of blocks) {
|
|
1047
|
+
const frontmatter = parseFrontmatter(block.content, { sourceName: block.path });
|
|
1048
|
+
const body = frontmatter.body ?? '';
|
|
1049
|
+
const sections = extractLevelTwoSections(body);
|
|
1050
|
+
const data = frontmatter.data ?? {};
|
|
1051
|
+
|
|
1052
|
+
if (!sections.has('review metadata')) {
|
|
1053
|
+
warnings.push(`missing_review_metadata: ${block.path} is missing "## Review metadata".`);
|
|
1054
|
+
}
|
|
1055
|
+
if (!/^\s*-\s*Evidence:\s+\S/im.test(body)) {
|
|
1056
|
+
warnings.push(`missing_evidence_metadata: ${block.path} is missing review metadata "Evidence".`);
|
|
1057
|
+
}
|
|
1058
|
+
if (!/^\s*-\s*Blindspots:\s+\S/im.test(body)) {
|
|
1059
|
+
warnings.push(`missing_blindspots_metadata: ${block.path} is missing review metadata "Blindspots".`);
|
|
1060
|
+
}
|
|
1061
|
+
if (!sections.has('retrieval guidance')) {
|
|
1062
|
+
warnings.push(`missing_retrieval_guidance: ${block.path} is missing "## Retrieval guidance".`);
|
|
1063
|
+
}
|
|
1064
|
+
if (!sections.has('involved files')) {
|
|
1065
|
+
warnings.push(`missing_involved_files: ${block.path} is missing "## Involved files".`);
|
|
1066
|
+
}
|
|
1067
|
+
if (
|
|
1068
|
+
block.path !== 'architecture/overview/project-shape.md' &&
|
|
1069
|
+
data.repo === undefined &&
|
|
1070
|
+
data.repos === undefined
|
|
1071
|
+
) {
|
|
1072
|
+
warnings.push(`missing_repo_scope: ${block.path} should declare repo or repos in frontmatter when implementation-scoped.`);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
return warnings;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function extractLevelTwoSections(markdown) {
|
|
1080
|
+
return new Set(
|
|
1081
|
+
[...String(markdown ?? '').matchAll(/^##\s+(.+)$/gm)]
|
|
1082
|
+
.map((match) => match[1].trim().toLowerCase()),
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
700
1086
|
async function submitHostedDocsReview(options) {
|
|
701
1087
|
if (options.runtime.provider !== 'hosted') {
|
|
702
1088
|
throw new Error('docs-review --submit-hosted requires a hosted sync binding in project.yaml.');
|
|
@@ -720,8 +1106,15 @@ async function submitHostedDocsReview(options) {
|
|
|
720
1106
|
`api/sync/projects/${encodeURIComponent(options.runtime.project_id)}/docs-review`,
|
|
721
1107
|
ensureTrailingSlash(options.runtime.api_url),
|
|
722
1108
|
);
|
|
1109
|
+
// D-237: the baseline must come from the submitted target's own cursor so a
|
|
1110
|
+
// dev revision can never be sent as another environment's baseline.
|
|
1111
|
+
const submitCursor = readSyncCursor(options.manifest?.manifest?.sync, {
|
|
1112
|
+
target: options.runtime.sync_target ?? null,
|
|
1113
|
+
apiUrl: options.runtime.api_url,
|
|
1114
|
+
projectId: options.runtime.project_id,
|
|
1115
|
+
});
|
|
723
1116
|
const baselineRemoteRevisionId = normalizeUuid(
|
|
724
|
-
|
|
1117
|
+
submitCursor.last_successful_remote_revision,
|
|
725
1118
|
);
|
|
726
1119
|
const response = await options.fetch(endpoint.href, {
|
|
727
1120
|
method: 'POST',
|
|
@@ -902,6 +1295,83 @@ async function pathExists(targetPath) {
|
|
|
902
1295
|
}
|
|
903
1296
|
}
|
|
904
1297
|
|
|
1298
|
+
async function listArchitectureDocs(rootDir, scopePath) {
|
|
1299
|
+
const scopeAbsolutePath = path.join(rootDir, scopePath);
|
|
1300
|
+
|
|
1301
|
+
if (!await pathExists(scopeAbsolutePath)) {
|
|
1302
|
+
return [];
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const docs = [];
|
|
1306
|
+
await collectArchitectureDocs({
|
|
1307
|
+
rootDir,
|
|
1308
|
+
currentPath: scopeAbsolutePath,
|
|
1309
|
+
docs,
|
|
1310
|
+
});
|
|
1311
|
+
return docs.sort();
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
async function collectArchitectureDocs(options) {
|
|
1315
|
+
const entries = await readdir(options.currentPath, { withFileTypes: true });
|
|
1316
|
+
for (const entry of entries) {
|
|
1317
|
+
const absolutePath = path.join(options.currentPath, entry.name);
|
|
1318
|
+
if (entry.isDirectory()) {
|
|
1319
|
+
await collectArchitectureDocs({
|
|
1320
|
+
rootDir: options.rootDir,
|
|
1321
|
+
currentPath: absolutePath,
|
|
1322
|
+
docs: options.docs,
|
|
1323
|
+
});
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) {
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
const relativePath = normalizeBlockPath(path.relative(options.rootDir, absolutePath));
|
|
1331
|
+
if (isProtectedArchitectureDoc(relativePath)) {
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
options.docs.push(relativePath);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function isProtectedArchitectureDoc(relativePath) {
|
|
1339
|
+
return relativePath === 'architecture/README.md' ||
|
|
1340
|
+
relativePath === 'architecture/overview/project-shape.md';
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function normalizeRebuildScopePath(value) {
|
|
1344
|
+
const normalized = normalizeBlockPath(value ?? 'architecture');
|
|
1345
|
+
if (
|
|
1346
|
+
normalized !== 'architecture' &&
|
|
1347
|
+
!normalized.startsWith('architecture/')
|
|
1348
|
+
) {
|
|
1349
|
+
throw new Error(`docs-review --rebuild --path must point under architecture/. Invalid path: ${value}`);
|
|
1350
|
+
}
|
|
1351
|
+
if (
|
|
1352
|
+
path.posix.isAbsolute(normalized) ||
|
|
1353
|
+
normalized.split('/').some((part) => part === '..' || part === '') ||
|
|
1354
|
+
normalized.endsWith('.md')
|
|
1355
|
+
) {
|
|
1356
|
+
throw new Error(`docs-review --rebuild --path must be a relative architecture directory. Invalid path: ${value}`);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
return normalized;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function normalizeRebuildStalePolicy(value) {
|
|
1363
|
+
const normalized = normalizeOptionalString(value) ?? 'keep';
|
|
1364
|
+
if (!REBUILD_STALE_POLICIES.has(normalized)) {
|
|
1365
|
+
throw new Error('docs-review --rebuild --stale-policy must be keep or archive.');
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
return normalized;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function formatArchiveTimestamp(date) {
|
|
1372
|
+
return date.toISOString().replaceAll(':', '-').replaceAll('.', '-');
|
|
1373
|
+
}
|
|
1374
|
+
|
|
905
1375
|
function normalizeOptionalString(value) {
|
|
906
1376
|
if (typeof value !== 'string') {
|
|
907
1377
|
return null;
|
|
@@ -937,6 +1407,106 @@ function normalizeDecisionTargetPath(value) {
|
|
|
937
1407
|
return normalized;
|
|
938
1408
|
}
|
|
939
1409
|
|
|
1410
|
+
function validateDecisionRecommendationBlocks(blocks) {
|
|
1411
|
+
return blocks.map((block, index) => {
|
|
1412
|
+
const fields = parseDecisionRecommendationFields(block.content);
|
|
1413
|
+
const title = normalizeOptionalString(fields.title);
|
|
1414
|
+
const decision = normalizeOptionalString(fields.decision);
|
|
1415
|
+
const rationale = normalizeOptionalString(fields.rationale);
|
|
1416
|
+
const context = normalizeOptionalString(fields.context);
|
|
1417
|
+
|
|
1418
|
+
if (!title) {
|
|
1419
|
+
throw new Error(`Decision recommendation ${index + 1} must include a non-empty Title.`);
|
|
1420
|
+
}
|
|
1421
|
+
if (!decision) {
|
|
1422
|
+
throw new Error(`Decision recommendation ${index + 1} must include non-empty Decision content.`);
|
|
1423
|
+
}
|
|
1424
|
+
if (!rationale) {
|
|
1425
|
+
throw new Error(`Decision recommendation ${index + 1} must include a non-empty Rationale.`);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
return {
|
|
1429
|
+
targetPath: block.targetPath,
|
|
1430
|
+
title,
|
|
1431
|
+
decision,
|
|
1432
|
+
rationale,
|
|
1433
|
+
context,
|
|
1434
|
+
};
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function parseDecisionRecommendationFields(content) {
|
|
1439
|
+
const fields = {};
|
|
1440
|
+
let currentKey = null;
|
|
1441
|
+
|
|
1442
|
+
for (const line of String(content ?? '').split(/\r?\n/)) {
|
|
1443
|
+
const labelMatch = line.match(/^\*\*(Title|Decision|Rationale|Context):\*\*\s*(.*)$/i);
|
|
1444
|
+
if (labelMatch) {
|
|
1445
|
+
currentKey = labelMatch[1].toLowerCase();
|
|
1446
|
+
fields[currentKey] = labelMatch[2].trim();
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
if (currentKey && line.trim()) {
|
|
1451
|
+
fields[currentKey] = `${fields[currentKey] ? `${fields[currentKey]}\n` : ''}${line.trim()}`;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
return fields;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
async function applyDecisionRecommendations(options) {
|
|
1459
|
+
const applied = [];
|
|
1460
|
+
for (const recommendation of options.recommendations) {
|
|
1461
|
+
const nextDecisionId = await readNextDecisionId(options.rootDir);
|
|
1462
|
+
const targetFilePath = path.join(options.rootDir, recommendation.targetPath);
|
|
1463
|
+
await mkdir(path.dirname(targetFilePath), { recursive: true });
|
|
1464
|
+
|
|
1465
|
+
let existingContent = '';
|
|
1466
|
+
try {
|
|
1467
|
+
existingContent = await readFile(targetFilePath, 'utf8');
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
1470
|
+
existingContent = `# ${path.basename(recommendation.targetPath, '.md')} decisions\n`;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
const entry = renderDecisionEntry({
|
|
1474
|
+
decisionId: nextDecisionId,
|
|
1475
|
+
title: recommendation.title,
|
|
1476
|
+
decision: recommendation.decision,
|
|
1477
|
+
rationale: recommendation.rationale,
|
|
1478
|
+
context: recommendation.context,
|
|
1479
|
+
});
|
|
1480
|
+
await writeFile(targetFilePath, `${existingContent.trimEnd()}\n\n${entry}`, 'utf8');
|
|
1481
|
+
applied.push({
|
|
1482
|
+
decision_id: nextDecisionId,
|
|
1483
|
+
target_path: recommendation.targetPath,
|
|
1484
|
+
title: recommendation.title,
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
if (applied.length === 0) {
|
|
1489
|
+
return { applied, warnings: [] };
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
const indexResult = options.refreshIndex
|
|
1493
|
+
? await refreshDecisionIndex(options.rootDir)
|
|
1494
|
+
: {
|
|
1495
|
+
refreshed: false,
|
|
1496
|
+
warnings: [
|
|
1497
|
+
'decisions/INDEX.md was not refreshed after applying docs-review decision recommendations. Run again with --refresh-index if this root uses the package-generated flat index, or update the project-specific index manually.',
|
|
1498
|
+
],
|
|
1499
|
+
};
|
|
1500
|
+
|
|
1501
|
+
return {
|
|
1502
|
+
applied: applied.map((item) => ({
|
|
1503
|
+
...item,
|
|
1504
|
+
refreshed_index: indexResult.refreshed,
|
|
1505
|
+
})),
|
|
1506
|
+
warnings: indexResult.warnings,
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
|
|
940
1510
|
async function readNextDecisionId(rootDir) {
|
|
941
1511
|
const decisionsDir = path.join(rootDir, 'decisions');
|
|
942
1512
|
let max = 0;
|
|
@@ -1097,6 +1667,21 @@ function renderDocsReviewMessage(options) {
|
|
|
1097
1667
|
return 'Docs-review preflight passed. Run the generated architecture-review prompt in the selected LLM, then record completion after applying accepted docs changes.';
|
|
1098
1668
|
}
|
|
1099
1669
|
|
|
1670
|
+
function renderApplyOutputMessage(options) {
|
|
1671
|
+
const parts = [];
|
|
1672
|
+
if (options.architectureCount > 0) {
|
|
1673
|
+
parts.push(`${options.architectureCount} accepted architecture doc${options.architectureCount === 1 ? '' : 's'}`);
|
|
1674
|
+
}
|
|
1675
|
+
if (options.coverageApplied) {
|
|
1676
|
+
parts.push('coverage plan');
|
|
1677
|
+
}
|
|
1678
|
+
if (options.decisionCount > 0) {
|
|
1679
|
+
parts.push(`${options.decisionCount} decision recommendation${options.decisionCount === 1 ? '' : 's'}`);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
return `Applied ${parts.length > 0 ? parts.join(', ') : 'accepted docs-review output'} and completed the docs-review marker.`;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1100
1685
|
function renderReviewPrompt(options) {
|
|
1101
1686
|
return [
|
|
1102
1687
|
`# ${DOCS_REVIEW_PROMPT_VERSION}`,
|
|
@@ -1119,6 +1704,7 @@ function renderReviewPrompt(options) {
|
|
|
1119
1704
|
'## Staged Review Protocol',
|
|
1120
1705
|
'Stage 1 — Evidence inventory:',
|
|
1121
1706
|
'- Build a compact repo inventory from file paths, manifests/config files, route/job/test directories, and existing architecture frontmatter before reading large file bodies.',
|
|
1707
|
+
'- Mine finalized session notes for decision candidates and product constraints, but cap proposed decisions to the five highest-value architecture choices.',
|
|
1122
1708
|
'- Identify candidate domains, features, components, and ownership boundaries with concrete `repo:path` evidence.',
|
|
1123
1709
|
'- Do not fetch or summarize broad source bodies until the coverage plan says a file is needed.',
|
|
1124
1710
|
'',
|
|
@@ -1126,6 +1712,7 @@ function renderReviewPrompt(options) {
|
|
|
1126
1712
|
'- Propose the smallest useful architecture-doc set that will let future agents understand the project and retrieve targeted context.',
|
|
1127
1713
|
'- Prioritize entry points, data ownership, external integrations, jobs/async boundaries, auth/security boundaries, and test surfaces.',
|
|
1128
1714
|
'- Mark each proposed doc as `comprehensive`, `partial`, or `initial`, with the reason and blindspots.',
|
|
1715
|
+
'- Emit the accepted plan as one `vibecompass-coverage-plan version=1` JSON fence before architecture-doc blocks.',
|
|
1129
1716
|
'- Ask for user acceptance before emitting fenced architecture-doc blocks.',
|
|
1130
1717
|
'',
|
|
1131
1718
|
'Stage 3 — Bounded doc generation:',
|
|
@@ -1177,13 +1764,43 @@ function renderReviewPrompt(options) {
|
|
|
1177
1764
|
'- token-budget risks or oversized-doc candidates',
|
|
1178
1765
|
'- proposed decisions, if any',
|
|
1179
1766
|
'',
|
|
1767
|
+
'Then output one machine-readable coverage-plan block for the accepted plan:',
|
|
1768
|
+
'',
|
|
1769
|
+
'```vibecompass-coverage-plan version=1',
|
|
1770
|
+
'{',
|
|
1771
|
+
' "summary": "Short coverage plan summary",',
|
|
1772
|
+
' "areas": [',
|
|
1773
|
+
' {',
|
|
1774
|
+
' "id": "stable-area-id",',
|
|
1775
|
+
' "domain": "Domain",',
|
|
1776
|
+
' "feature": "Feature",',
|
|
1777
|
+
' "component": "Component",',
|
|
1778
|
+
' "status": "accepted | deferred | missing",',
|
|
1779
|
+
' "coverage": "comprehensive | partial | initial | missing",',
|
|
1780
|
+
' "proposed_path": "architecture/domain/feature/component.md",',
|
|
1781
|
+
' "evidence": ["repo:path"],',
|
|
1782
|
+
' "blindspots": ["explicit blindspot"]',
|
|
1783
|
+
' }',
|
|
1784
|
+
' ]',
|
|
1785
|
+
'}',
|
|
1786
|
+
'```',
|
|
1787
|
+
'',
|
|
1180
1788
|
'Then, for each architecture doc the user accepts, output one fenced block exactly like this:',
|
|
1181
1789
|
'',
|
|
1182
1790
|
'```vibecompass-architecture-doc path=architecture/domain/feature/component.md',
|
|
1183
1791
|
'<complete markdown file content>',
|
|
1184
1792
|
'```',
|
|
1185
1793
|
'',
|
|
1186
|
-
'
|
|
1794
|
+
'If the review surfaced accepted decision candidates, output each candidate as a separate block without assigning a D-number:',
|
|
1795
|
+
'',
|
|
1796
|
+
'```vibecompass-decision-recommendation target=decisions/cross-cutting.md',
|
|
1797
|
+
'**Title:** Short decision title',
|
|
1798
|
+
'**Decision:** Accepted decision statement to append after local review.',
|
|
1799
|
+
'**Rationale:** Why this choice matters.',
|
|
1800
|
+
'**Context:** Optional supporting context.',
|
|
1801
|
+
'```',
|
|
1802
|
+
'',
|
|
1803
|
+
'Only include accepted coverage, architecture, and decision-recommendation blocks. After accepted output is applied, `vibecompass docs-review --apply-output` records completion in `state/docs-review.json`.',
|
|
1187
1804
|
].join('\n');
|
|
1188
1805
|
}
|
|
1189
1806
|
|