@vibecompass/vibecompass 0.5.0 → 0.7.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 +18 -0
- package/README.md +80 -680
- package/package.json +4 -2
- package/src/cli.js +50 -1
- package/src/docs-review.js +669 -13
- package/src/status.js +41 -1
- package/src/workflows/registry.js +142 -0
package/src/docs-review.js
CHANGED
|
@@ -1,10 +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';
|
|
7
8
|
|
|
9
|
+
const DOCS_REVIEW_PROMPT_VERSION = 'VibeCompass Docs Review Prompt v2';
|
|
10
|
+
const ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES = 12000;
|
|
11
|
+
const COVERAGE_PLAN_OUTPUT_VERSION = 1;
|
|
12
|
+
const COVERAGE_PLAN_STATUSES = new Set(['accepted', 'deferred', 'missing']);
|
|
13
|
+
const COVERAGE_LEVELS = new Set(['comprehensive', 'partial', 'initial', 'missing']);
|
|
14
|
+
const REBUILD_STALE_POLICIES = new Set(['keep', 'archive']);
|
|
15
|
+
|
|
8
16
|
export async function preflightDocsReview(options = {}, environment = {}) {
|
|
9
17
|
validateDocsReviewMode(options);
|
|
10
18
|
const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
@@ -18,6 +26,18 @@ export async function preflightDocsReview(options = {}, environment = {}) {
|
|
|
18
26
|
outputPath: options.outputPath,
|
|
19
27
|
llm: options.llm,
|
|
20
28
|
model: options.model,
|
|
29
|
+
refreshIndex: options.refreshIndex,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (options.rebuild) {
|
|
34
|
+
return rebuildDocsReview({
|
|
35
|
+
rootDir,
|
|
36
|
+
statePath,
|
|
37
|
+
apply: options.apply,
|
|
38
|
+
dryRun: options.dryRun,
|
|
39
|
+
scopePath: options.scopePath,
|
|
40
|
+
stalePolicy: options.stalePolicy,
|
|
21
41
|
});
|
|
22
42
|
}
|
|
23
43
|
|
|
@@ -180,8 +200,10 @@ async function applyDocsReviewOutput(options) {
|
|
|
180
200
|
: path.join(path.dirname(options.statePath), 'docs-review-output.md');
|
|
181
201
|
const source = await readFile(outputPath, 'utf8');
|
|
182
202
|
const blocks = parseArchitectureDocBlocks(source);
|
|
203
|
+
const coverageBlocks = parseCoveragePlanBlocks(source);
|
|
204
|
+
const decisionRecommendationBlocks = parseDecisionRecommendationBlocks(source);
|
|
183
205
|
|
|
184
|
-
if (blocks.length === 0) {
|
|
206
|
+
if (blocks.length === 0 && coverageBlocks.length === 0 && decisionRecommendationBlocks.length === 0) {
|
|
185
207
|
const malformedFenceCount = countMalformedArchitectureDocFences(source);
|
|
186
208
|
if (malformedFenceCount > 0) {
|
|
187
209
|
throw new Error(
|
|
@@ -189,12 +211,35 @@ async function applyDocsReviewOutput(options) {
|
|
|
189
211
|
);
|
|
190
212
|
}
|
|
191
213
|
|
|
214
|
+
const malformedCoveragePlanFenceCount = countMalformedCoveragePlanFences(source);
|
|
215
|
+
if (malformedCoveragePlanFenceCount > 0) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Malformed coverage-plan fence found in ${outputPath}. Expected fence openings like \`\`\`vibecompass-coverage-plan version=1\`.`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const malformedDecisionRecommendationFenceCount = countMalformedDecisionRecommendationFences(source);
|
|
222
|
+
if (malformedDecisionRecommendationFenceCount > 0) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`Malformed decision recommendation fence found in ${outputPath}. Expected fence openings like \`\`\`vibecompass-decision-recommendation target=decisions/domain.md\`.`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
192
228
|
throw new Error(
|
|
193
|
-
`No accepted
|
|
229
|
+
`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\`.`,
|
|
194
230
|
);
|
|
195
231
|
}
|
|
196
232
|
|
|
197
233
|
validateArchitectureDocBlocks(blocks);
|
|
234
|
+
const coverageProjection = buildCoverageProjection(coverageBlocks, {
|
|
235
|
+
outputPath,
|
|
236
|
+
outputHash: sha256Text(source),
|
|
237
|
+
});
|
|
238
|
+
const decisionRecommendations = validateDecisionRecommendationBlocks(decisionRecommendationBlocks);
|
|
239
|
+
const warnings = [
|
|
240
|
+
...createArchitectureDocSizeWarnings(blocks),
|
|
241
|
+
...createArchitectureDocQualityWarnings(blocks),
|
|
242
|
+
];
|
|
198
243
|
|
|
199
244
|
const applied = [];
|
|
200
245
|
for (const block of blocks) {
|
|
@@ -205,8 +250,31 @@ async function applyDocsReviewOutput(options) {
|
|
|
205
250
|
applied.push({ path: block.path, status: existed ? 'overwritten' : 'created' });
|
|
206
251
|
}
|
|
207
252
|
|
|
253
|
+
let appliedCoverage = null;
|
|
254
|
+
if (coverageProjection) {
|
|
255
|
+
const coveragePath = path.join(path.dirname(options.statePath), 'docs-review-coverage.json');
|
|
256
|
+
await writeFile(coveragePath, `${JSON.stringify(coverageProjection, null, 2)}\n`, 'utf8');
|
|
257
|
+
appliedCoverage = {
|
|
258
|
+
path: coveragePath,
|
|
259
|
+
area_count: coverageProjection.area_count,
|
|
260
|
+
coverage_score: coverageProjection.coverage_score,
|
|
261
|
+
statuses: coverageProjection.statuses,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const appliedDecisionCandidates = decisionRecommendations.length > 0
|
|
266
|
+
? await applyDecisionRecommendations({
|
|
267
|
+
rootDir: options.rootDir,
|
|
268
|
+
recommendations: decisionRecommendations,
|
|
269
|
+
refreshIndex: options.refreshIndex,
|
|
270
|
+
})
|
|
271
|
+
: { applied: [], warnings: [] };
|
|
272
|
+
warnings.push(...appliedDecisionCandidates.warnings);
|
|
273
|
+
|
|
274
|
+
const manifestResult = await writeStateManifest(options.rootDir);
|
|
208
275
|
const llm = normalizeOptionalString(options.llm) ?? current.llm ?? null;
|
|
209
276
|
const model = normalizeOptionalString(options.model) ?? current.model ?? null;
|
|
277
|
+
const appliedAt = new Date().toISOString();
|
|
210
278
|
const marker = {
|
|
211
279
|
...current,
|
|
212
280
|
status: 'completed',
|
|
@@ -214,10 +282,16 @@ async function applyDocsReviewOutput(options) {
|
|
|
214
282
|
...(model ? { model } : {}),
|
|
215
283
|
applied: {
|
|
216
284
|
output_path: outputPath,
|
|
285
|
+
output_hash: sha256Text(source),
|
|
286
|
+
local_root_revision: manifestResult.manifest.canonical.local_root_revision,
|
|
287
|
+
manifest_hash: manifestResult.manifest.canonical.manifest_hash,
|
|
217
288
|
architecture_docs: applied,
|
|
218
|
-
|
|
289
|
+
...(appliedCoverage ? { coverage: appliedCoverage } : {}),
|
|
290
|
+
...(appliedDecisionCandidates.applied.length > 0 ? { decision_candidates: appliedDecisionCandidates.applied } : {}),
|
|
291
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
292
|
+
applied_at: appliedAt,
|
|
219
293
|
},
|
|
220
|
-
completed_at:
|
|
294
|
+
completed_at: appliedAt,
|
|
221
295
|
};
|
|
222
296
|
|
|
223
297
|
await writeDocsReviewMarker(options.statePath, marker);
|
|
@@ -232,8 +306,12 @@ async function applyDocsReviewOutput(options) {
|
|
|
232
306
|
hosted: current.hosted ?? null,
|
|
233
307
|
applied: marker.applied,
|
|
234
308
|
reviewPrompt: null,
|
|
235
|
-
warnings
|
|
236
|
-
message:
|
|
309
|
+
warnings,
|
|
310
|
+
message: renderApplyOutputMessage({
|
|
311
|
+
architectureCount: applied.length,
|
|
312
|
+
coverageApplied: Boolean(appliedCoverage),
|
|
313
|
+
decisionCount: appliedDecisionCandidates.applied.length,
|
|
314
|
+
}),
|
|
237
315
|
};
|
|
238
316
|
}
|
|
239
317
|
|
|
@@ -335,6 +413,92 @@ async function applyDecisionArtifact(options) {
|
|
|
335
413
|
};
|
|
336
414
|
}
|
|
337
415
|
|
|
416
|
+
async function rebuildDocsReview(options) {
|
|
417
|
+
const scopePath = normalizeRebuildScopePath(options.scopePath);
|
|
418
|
+
const stalePolicy = normalizeRebuildStalePolicy(options.stalePolicy);
|
|
419
|
+
const apply = Boolean(options.apply);
|
|
420
|
+
const architectureDocs = await listArchitectureDocs(options.rootDir, scopePath);
|
|
421
|
+
const requestedAt = new Date();
|
|
422
|
+
const archiveRoot = stalePolicy === 'archive'
|
|
423
|
+
? `state/docs-review-archive/${formatArchiveTimestamp(requestedAt)}`
|
|
424
|
+
: null;
|
|
425
|
+
const rebuildEntries = architectureDocs.map((docPath) => ({
|
|
426
|
+
path: docPath,
|
|
427
|
+
action: stalePolicy === 'archive' ? 'archive' : 'keep',
|
|
428
|
+
...(archiveRoot ? { archive_path: `${archiveRoot}/${docPath}` } : {}),
|
|
429
|
+
}));
|
|
430
|
+
|
|
431
|
+
if (!apply) {
|
|
432
|
+
return {
|
|
433
|
+
rootDir: options.rootDir,
|
|
434
|
+
statePath: options.statePath,
|
|
435
|
+
status: 'rebuild-preview',
|
|
436
|
+
recorded: false,
|
|
437
|
+
runtime: null,
|
|
438
|
+
hosted: null,
|
|
439
|
+
applied: null,
|
|
440
|
+
reviewPrompt: null,
|
|
441
|
+
rebuild: {
|
|
442
|
+
scope_path: scopePath,
|
|
443
|
+
stale_policy: stalePolicy,
|
|
444
|
+
architecture_docs: rebuildEntries,
|
|
445
|
+
architecture_doc_count: rebuildEntries.length,
|
|
446
|
+
archive_root: archiveRoot,
|
|
447
|
+
},
|
|
448
|
+
warnings: [],
|
|
449
|
+
message: 'Docs-review rebuild preview complete. Re-run with --apply to record the rebuild marker and perform the selected stale-doc action.',
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
for (const entry of rebuildEntries) {
|
|
454
|
+
if (entry.action !== 'archive') {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const sourcePath = path.join(options.rootDir, entry.path);
|
|
458
|
+
const targetPath = path.join(options.rootDir, entry.archive_path);
|
|
459
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
460
|
+
await rename(sourcePath, targetPath);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const manifestResult = await writeStateManifest(options.rootDir);
|
|
464
|
+
const appliedAt = new Date().toISOString();
|
|
465
|
+
const marker = {
|
|
466
|
+
status: 'rebuild-ready',
|
|
467
|
+
requested_at: requestedAt.toISOString(),
|
|
468
|
+
runtime: {
|
|
469
|
+
provider: 'local',
|
|
470
|
+
operation: 'rebuild',
|
|
471
|
+
},
|
|
472
|
+
rebuild: {
|
|
473
|
+
scope_path: scopePath,
|
|
474
|
+
stale_policy: stalePolicy,
|
|
475
|
+
architecture_docs: rebuildEntries,
|
|
476
|
+
architecture_doc_count: rebuildEntries.length,
|
|
477
|
+
...(archiveRoot ? { archive_root: path.join(options.rootDir, archiveRoot) } : {}),
|
|
478
|
+
local_root_revision: manifestResult.manifest.canonical.local_root_revision,
|
|
479
|
+
manifest_hash: manifestResult.manifest.canonical.manifest_hash,
|
|
480
|
+
applied_at: appliedAt,
|
|
481
|
+
},
|
|
482
|
+
completed_at: null,
|
|
483
|
+
};
|
|
484
|
+
await writeDocsReviewMarker(options.statePath, marker);
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
rootDir: options.rootDir,
|
|
488
|
+
statePath: options.statePath,
|
|
489
|
+
status: 'rebuild-ready',
|
|
490
|
+
runtime: marker.runtime,
|
|
491
|
+
hosted: null,
|
|
492
|
+
applied: null,
|
|
493
|
+
reviewPrompt: null,
|
|
494
|
+
rebuild: marker.rebuild,
|
|
495
|
+
warnings: [],
|
|
496
|
+
message: stalePolicy === 'archive'
|
|
497
|
+
? 'Archived selected architecture docs and recorded a rebuild marker. Run docs-review again, then apply accepted output.'
|
|
498
|
+
: 'Recorded a rebuild marker without moving architecture docs. Run docs-review again, then apply accepted output.',
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
338
502
|
async function readDocsReviewMarker(statePath) {
|
|
339
503
|
try {
|
|
340
504
|
return JSON.parse(await readFile(statePath, 'utf8'));
|
|
@@ -355,12 +519,28 @@ function validateDocsReviewMode(options) {
|
|
|
355
519
|
options.runLocalAnthropic ? '--run-local-anthropic' : null,
|
|
356
520
|
options.applyOutput ? '--apply-output' : null,
|
|
357
521
|
options.applyDecisionArtifact ? '--apply-decision-artifact' : null,
|
|
522
|
+
options.rebuild ? '--rebuild' : null,
|
|
358
523
|
options.complete ? '--complete' : null,
|
|
359
524
|
].filter(Boolean);
|
|
360
525
|
|
|
361
526
|
if (enabled.length > 1) {
|
|
362
527
|
throw new Error(`docs-review accepts only one execution mode at a time. Conflicting flags: ${enabled.join(', ')}.`);
|
|
363
528
|
}
|
|
529
|
+
if (options.apply && !options.rebuild) {
|
|
530
|
+
throw new Error('docs-review --apply is only valid with --rebuild.');
|
|
531
|
+
}
|
|
532
|
+
if (options.dryRun && !options.rebuild) {
|
|
533
|
+
throw new Error('docs-review --dry-run is only valid with --rebuild.');
|
|
534
|
+
}
|
|
535
|
+
if (options.stalePolicy && !options.rebuild) {
|
|
536
|
+
throw new Error('docs-review --stale-policy is only valid with --rebuild.');
|
|
537
|
+
}
|
|
538
|
+
if (options.scopePath && !options.rebuild) {
|
|
539
|
+
throw new Error('docs-review --path is only valid with --rebuild.');
|
|
540
|
+
}
|
|
541
|
+
if (options.apply && options.dryRun) {
|
|
542
|
+
throw new Error('docs-review --rebuild accepts only one of --dry-run or --apply.');
|
|
543
|
+
}
|
|
364
544
|
}
|
|
365
545
|
|
|
366
546
|
async function promptForReviewConfig(options, environment) {
|
|
@@ -619,6 +799,36 @@ function parseArchitectureDocBlocks(source) {
|
|
|
619
799
|
return blocks;
|
|
620
800
|
}
|
|
621
801
|
|
|
802
|
+
function parseCoveragePlanBlocks(source) {
|
|
803
|
+
const blocks = [];
|
|
804
|
+
const pattern = /^```vibecompass-coverage-plan\s+version=1\s*\n([\s\S]*?)^```[ \t]*$/gm;
|
|
805
|
+
let match;
|
|
806
|
+
|
|
807
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
808
|
+
blocks.push({
|
|
809
|
+
version: COVERAGE_PLAN_OUTPUT_VERSION,
|
|
810
|
+
content: match[1].replace(/\n$/, ''),
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return blocks;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function parseDecisionRecommendationBlocks(source) {
|
|
818
|
+
const blocks = [];
|
|
819
|
+
const pattern = /^```vibecompass-decision-recommendation\s+target=([^\s`]+)\s*\n([\s\S]*?)^```[ \t]*$/gm;
|
|
820
|
+
let match;
|
|
821
|
+
|
|
822
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
823
|
+
blocks.push({
|
|
824
|
+
targetPath: normalizeDecisionTargetPath(match[1]),
|
|
825
|
+
content: match[2].replace(/\n$/, ''),
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return blocks;
|
|
830
|
+
}
|
|
831
|
+
|
|
622
832
|
function countMalformedArchitectureDocFences(source) {
|
|
623
833
|
const fencePattern = /^```vibecompass-architecture-doc\b.*$/gm;
|
|
624
834
|
const acceptedOpeningPattern = /^```vibecompass-architecture-doc\s+path=[^\s`]+\s*$/;
|
|
@@ -634,6 +844,36 @@ function countMalformedArchitectureDocFences(source) {
|
|
|
634
844
|
return count;
|
|
635
845
|
}
|
|
636
846
|
|
|
847
|
+
function countMalformedCoveragePlanFences(source) {
|
|
848
|
+
const fencePattern = /^```vibecompass-coverage-plan\b.*$/gm;
|
|
849
|
+
const acceptedOpeningPattern = /^```vibecompass-coverage-plan\s+version=1\s*$/;
|
|
850
|
+
let count = 0;
|
|
851
|
+
let match;
|
|
852
|
+
|
|
853
|
+
while ((match = fencePattern.exec(source)) !== null) {
|
|
854
|
+
if (!acceptedOpeningPattern.test(match[0])) {
|
|
855
|
+
count += 1;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return count;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function countMalformedDecisionRecommendationFences(source) {
|
|
863
|
+
const fencePattern = /^```vibecompass-decision-recommendation\b.*$/gm;
|
|
864
|
+
const acceptedOpeningPattern = /^```vibecompass-decision-recommendation\s+target=[^\s`]+\s*$/;
|
|
865
|
+
let count = 0;
|
|
866
|
+
let match;
|
|
867
|
+
|
|
868
|
+
while ((match = fencePattern.exec(source)) !== null) {
|
|
869
|
+
if (!acceptedOpeningPattern.test(match[0])) {
|
|
870
|
+
count += 1;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return count;
|
|
875
|
+
}
|
|
876
|
+
|
|
637
877
|
function validateArchitectureDocBlocks(blocks) {
|
|
638
878
|
const seenPaths = new Set();
|
|
639
879
|
for (const block of blocks) {
|
|
@@ -645,6 +885,115 @@ function validateArchitectureDocBlocks(blocks) {
|
|
|
645
885
|
}
|
|
646
886
|
}
|
|
647
887
|
|
|
888
|
+
function buildCoverageProjection(blocks, options) {
|
|
889
|
+
if (blocks.length === 0) {
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
if (blocks.length > 1) {
|
|
893
|
+
throw new Error('Accepted docs-review output contains multiple coverage-plan blocks. Include at most one `vibecompass-coverage-plan version=1` block.');
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
let plan;
|
|
897
|
+
try {
|
|
898
|
+
plan = JSON.parse(blocks[0].content);
|
|
899
|
+
} catch (error) {
|
|
900
|
+
throw new Error(`Accepted coverage plan must contain valid JSON. ${error instanceof Error ? error.message : ''}`.trim());
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
|
|
904
|
+
throw new Error('Accepted coverage plan must contain a JSON object.');
|
|
905
|
+
}
|
|
906
|
+
if (!Array.isArray(plan.areas)) {
|
|
907
|
+
throw new Error('Accepted coverage plan requires an "areas" array.');
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const ids = new Set();
|
|
911
|
+
const areas = plan.areas.map((area, index) => normalizeCoverageArea(area, index, ids));
|
|
912
|
+
const statuses = countBy(areas, 'status');
|
|
913
|
+
const coverage_levels = countBy(areas, 'coverage');
|
|
914
|
+
const denominator = areas.length;
|
|
915
|
+
const acceptedCount = statuses.accepted ?? 0;
|
|
916
|
+
|
|
917
|
+
return {
|
|
918
|
+
version: COVERAGE_PLAN_OUTPUT_VERSION,
|
|
919
|
+
source: {
|
|
920
|
+
output_path: options.outputPath,
|
|
921
|
+
output_hash: options.outputHash,
|
|
922
|
+
},
|
|
923
|
+
projected_at: new Date().toISOString(),
|
|
924
|
+
summary: normalizeOptionalString(plan.summary),
|
|
925
|
+
area_count: areas.length,
|
|
926
|
+
coverage_score: denominator === 0 ? null : Number((acceptedCount / denominator).toFixed(4)),
|
|
927
|
+
statuses,
|
|
928
|
+
coverage_levels,
|
|
929
|
+
areas,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function normalizeCoverageArea(area, index, ids) {
|
|
934
|
+
if (!area || typeof area !== 'object' || Array.isArray(area)) {
|
|
935
|
+
throw new Error(`Coverage plan area at index ${index} must be an object.`);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const id = normalizeOptionalString(area.id);
|
|
939
|
+
if (!id) {
|
|
940
|
+
throw new Error(`Coverage plan area at index ${index} requires non-empty "id".`);
|
|
941
|
+
}
|
|
942
|
+
if (ids.has(id)) {
|
|
943
|
+
throw new Error(`Coverage plan contains duplicate area id: ${id}`);
|
|
944
|
+
}
|
|
945
|
+
ids.add(id);
|
|
946
|
+
|
|
947
|
+
const status = normalizeOptionalString(area.status);
|
|
948
|
+
if (!COVERAGE_PLAN_STATUSES.has(status)) {
|
|
949
|
+
throw new Error(`Coverage plan area "${id}" requires status to be accepted, deferred, or missing.`);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const coverage = normalizeOptionalString(area.coverage);
|
|
953
|
+
if (!COVERAGE_LEVELS.has(coverage)) {
|
|
954
|
+
throw new Error(`Coverage plan area "${id}" requires coverage to be comprehensive, partial, initial, or missing.`);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const proposedPath = normalizeOptionalString(area.proposed_path);
|
|
958
|
+
if (proposedPath) {
|
|
959
|
+
const normalizedPath = normalizeBlockPath(proposedPath);
|
|
960
|
+
if (
|
|
961
|
+
!normalizedPath.startsWith('architecture/') ||
|
|
962
|
+
!normalizedPath.endsWith('.md') ||
|
|
963
|
+
path.posix.isAbsolute(normalizedPath) ||
|
|
964
|
+
normalizedPath.split('/').includes('..')
|
|
965
|
+
) {
|
|
966
|
+
throw new Error(`Coverage plan area "${id}" has invalid proposed_path: ${proposedPath}`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
return {
|
|
971
|
+
id,
|
|
972
|
+
domain: normalizeOptionalString(area.domain),
|
|
973
|
+
feature: normalizeOptionalString(area.feature),
|
|
974
|
+
component: normalizeOptionalString(area.component),
|
|
975
|
+
status,
|
|
976
|
+
coverage,
|
|
977
|
+
...(proposedPath ? { proposed_path: normalizeBlockPath(proposedPath) } : {}),
|
|
978
|
+
evidence: normalizeStringArray(area.evidence),
|
|
979
|
+
blindspots: normalizeStringArray(area.blindspots),
|
|
980
|
+
...(normalizeOptionalString(area.reason) ? { reason: normalizeOptionalString(area.reason) } : {}),
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function countBy(items, key) {
|
|
985
|
+
return items.reduce((counts, item) => {
|
|
986
|
+
counts[item[key]] = (counts[item[key]] ?? 0) + 1;
|
|
987
|
+
return counts;
|
|
988
|
+
}, {});
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function normalizeStringArray(value) {
|
|
992
|
+
return Array.isArray(value)
|
|
993
|
+
? value.filter((item) => typeof item === 'string').map((item) => item.trim()).filter(Boolean)
|
|
994
|
+
: [];
|
|
995
|
+
}
|
|
996
|
+
|
|
648
997
|
function normalizeBlockPath(value) {
|
|
649
998
|
return String(value ?? '').trim().replaceAll('\\', '/');
|
|
650
999
|
}
|
|
@@ -679,6 +1028,62 @@ function validateArchitectureDocBlock(block) {
|
|
|
679
1028
|
}
|
|
680
1029
|
}
|
|
681
1030
|
|
|
1031
|
+
function createArchitectureDocSizeWarnings(blocks) {
|
|
1032
|
+
return blocks
|
|
1033
|
+
.map((block) => {
|
|
1034
|
+
const byteLength = Buffer.byteLength(ensureTrailingNewline(block.content), 'utf8');
|
|
1035
|
+
if (byteLength <= ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES) {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
return `oversized_architecture_doc: ${block.path} is ${byteLength} bytes; soft budget is ${ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES} bytes. Consider compacting the doc to preserve future LLM context budget.`;
|
|
1040
|
+
})
|
|
1041
|
+
.filter(Boolean);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function createArchitectureDocQualityWarnings(blocks) {
|
|
1045
|
+
const warnings = [];
|
|
1046
|
+
|
|
1047
|
+
for (const block of blocks) {
|
|
1048
|
+
const frontmatter = parseFrontmatter(block.content, { sourceName: block.path });
|
|
1049
|
+
const body = frontmatter.body ?? '';
|
|
1050
|
+
const sections = extractLevelTwoSections(body);
|
|
1051
|
+
const data = frontmatter.data ?? {};
|
|
1052
|
+
|
|
1053
|
+
if (!sections.has('review metadata')) {
|
|
1054
|
+
warnings.push(`missing_review_metadata: ${block.path} is missing "## Review metadata".`);
|
|
1055
|
+
}
|
|
1056
|
+
if (!/^\s*-\s*Evidence:\s+\S/im.test(body)) {
|
|
1057
|
+
warnings.push(`missing_evidence_metadata: ${block.path} is missing review metadata "Evidence".`);
|
|
1058
|
+
}
|
|
1059
|
+
if (!/^\s*-\s*Blindspots:\s+\S/im.test(body)) {
|
|
1060
|
+
warnings.push(`missing_blindspots_metadata: ${block.path} is missing review metadata "Blindspots".`);
|
|
1061
|
+
}
|
|
1062
|
+
if (!sections.has('retrieval guidance')) {
|
|
1063
|
+
warnings.push(`missing_retrieval_guidance: ${block.path} is missing "## Retrieval guidance".`);
|
|
1064
|
+
}
|
|
1065
|
+
if (!sections.has('involved files')) {
|
|
1066
|
+
warnings.push(`missing_involved_files: ${block.path} is missing "## Involved files".`);
|
|
1067
|
+
}
|
|
1068
|
+
if (
|
|
1069
|
+
block.path !== 'architecture/overview/project-shape.md' &&
|
|
1070
|
+
data.repo === undefined &&
|
|
1071
|
+
data.repos === undefined
|
|
1072
|
+
) {
|
|
1073
|
+
warnings.push(`missing_repo_scope: ${block.path} should declare repo or repos in frontmatter when implementation-scoped.`);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
return warnings;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function extractLevelTwoSections(markdown) {
|
|
1081
|
+
return new Set(
|
|
1082
|
+
[...String(markdown ?? '').matchAll(/^##\s+(.+)$/gm)]
|
|
1083
|
+
.map((match) => match[1].trim().toLowerCase()),
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
682
1087
|
async function submitHostedDocsReview(options) {
|
|
683
1088
|
if (options.runtime.provider !== 'hosted') {
|
|
684
1089
|
throw new Error('docs-review --submit-hosted requires a hosted sync binding in project.yaml.');
|
|
@@ -884,6 +1289,83 @@ async function pathExists(targetPath) {
|
|
|
884
1289
|
}
|
|
885
1290
|
}
|
|
886
1291
|
|
|
1292
|
+
async function listArchitectureDocs(rootDir, scopePath) {
|
|
1293
|
+
const scopeAbsolutePath = path.join(rootDir, scopePath);
|
|
1294
|
+
|
|
1295
|
+
if (!await pathExists(scopeAbsolutePath)) {
|
|
1296
|
+
return [];
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const docs = [];
|
|
1300
|
+
await collectArchitectureDocs({
|
|
1301
|
+
rootDir,
|
|
1302
|
+
currentPath: scopeAbsolutePath,
|
|
1303
|
+
docs,
|
|
1304
|
+
});
|
|
1305
|
+
return docs.sort();
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
async function collectArchitectureDocs(options) {
|
|
1309
|
+
const entries = await readdir(options.currentPath, { withFileTypes: true });
|
|
1310
|
+
for (const entry of entries) {
|
|
1311
|
+
const absolutePath = path.join(options.currentPath, entry.name);
|
|
1312
|
+
if (entry.isDirectory()) {
|
|
1313
|
+
await collectArchitectureDocs({
|
|
1314
|
+
rootDir: options.rootDir,
|
|
1315
|
+
currentPath: absolutePath,
|
|
1316
|
+
docs: options.docs,
|
|
1317
|
+
});
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) {
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
const relativePath = normalizeBlockPath(path.relative(options.rootDir, absolutePath));
|
|
1325
|
+
if (isProtectedArchitectureDoc(relativePath)) {
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
options.docs.push(relativePath);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function isProtectedArchitectureDoc(relativePath) {
|
|
1333
|
+
return relativePath === 'architecture/README.md' ||
|
|
1334
|
+
relativePath === 'architecture/overview/project-shape.md';
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function normalizeRebuildScopePath(value) {
|
|
1338
|
+
const normalized = normalizeBlockPath(value ?? 'architecture');
|
|
1339
|
+
if (
|
|
1340
|
+
normalized !== 'architecture' &&
|
|
1341
|
+
!normalized.startsWith('architecture/')
|
|
1342
|
+
) {
|
|
1343
|
+
throw new Error(`docs-review --rebuild --path must point under architecture/. Invalid path: ${value}`);
|
|
1344
|
+
}
|
|
1345
|
+
if (
|
|
1346
|
+
path.posix.isAbsolute(normalized) ||
|
|
1347
|
+
normalized.split('/').some((part) => part === '..' || part === '') ||
|
|
1348
|
+
normalized.endsWith('.md')
|
|
1349
|
+
) {
|
|
1350
|
+
throw new Error(`docs-review --rebuild --path must be a relative architecture directory. Invalid path: ${value}`);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
return normalized;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function normalizeRebuildStalePolicy(value) {
|
|
1357
|
+
const normalized = normalizeOptionalString(value) ?? 'keep';
|
|
1358
|
+
if (!REBUILD_STALE_POLICIES.has(normalized)) {
|
|
1359
|
+
throw new Error('docs-review --rebuild --stale-policy must be keep or archive.');
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
return normalized;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function formatArchiveTimestamp(date) {
|
|
1366
|
+
return date.toISOString().replaceAll(':', '-').replaceAll('.', '-');
|
|
1367
|
+
}
|
|
1368
|
+
|
|
887
1369
|
function normalizeOptionalString(value) {
|
|
888
1370
|
if (typeof value !== 'string') {
|
|
889
1371
|
return null;
|
|
@@ -919,6 +1401,106 @@ function normalizeDecisionTargetPath(value) {
|
|
|
919
1401
|
return normalized;
|
|
920
1402
|
}
|
|
921
1403
|
|
|
1404
|
+
function validateDecisionRecommendationBlocks(blocks) {
|
|
1405
|
+
return blocks.map((block, index) => {
|
|
1406
|
+
const fields = parseDecisionRecommendationFields(block.content);
|
|
1407
|
+
const title = normalizeOptionalString(fields.title);
|
|
1408
|
+
const decision = normalizeOptionalString(fields.decision);
|
|
1409
|
+
const rationale = normalizeOptionalString(fields.rationale);
|
|
1410
|
+
const context = normalizeOptionalString(fields.context);
|
|
1411
|
+
|
|
1412
|
+
if (!title) {
|
|
1413
|
+
throw new Error(`Decision recommendation ${index + 1} must include a non-empty Title.`);
|
|
1414
|
+
}
|
|
1415
|
+
if (!decision) {
|
|
1416
|
+
throw new Error(`Decision recommendation ${index + 1} must include non-empty Decision content.`);
|
|
1417
|
+
}
|
|
1418
|
+
if (!rationale) {
|
|
1419
|
+
throw new Error(`Decision recommendation ${index + 1} must include a non-empty Rationale.`);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
return {
|
|
1423
|
+
targetPath: block.targetPath,
|
|
1424
|
+
title,
|
|
1425
|
+
decision,
|
|
1426
|
+
rationale,
|
|
1427
|
+
context,
|
|
1428
|
+
};
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function parseDecisionRecommendationFields(content) {
|
|
1433
|
+
const fields = {};
|
|
1434
|
+
let currentKey = null;
|
|
1435
|
+
|
|
1436
|
+
for (const line of String(content ?? '').split(/\r?\n/)) {
|
|
1437
|
+
const labelMatch = line.match(/^\*\*(Title|Decision|Rationale|Context):\*\*\s*(.*)$/i);
|
|
1438
|
+
if (labelMatch) {
|
|
1439
|
+
currentKey = labelMatch[1].toLowerCase();
|
|
1440
|
+
fields[currentKey] = labelMatch[2].trim();
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
if (currentKey && line.trim()) {
|
|
1445
|
+
fields[currentKey] = `${fields[currentKey] ? `${fields[currentKey]}\n` : ''}${line.trim()}`;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
return fields;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
async function applyDecisionRecommendations(options) {
|
|
1453
|
+
const applied = [];
|
|
1454
|
+
for (const recommendation of options.recommendations) {
|
|
1455
|
+
const nextDecisionId = await readNextDecisionId(options.rootDir);
|
|
1456
|
+
const targetFilePath = path.join(options.rootDir, recommendation.targetPath);
|
|
1457
|
+
await mkdir(path.dirname(targetFilePath), { recursive: true });
|
|
1458
|
+
|
|
1459
|
+
let existingContent = '';
|
|
1460
|
+
try {
|
|
1461
|
+
existingContent = await readFile(targetFilePath, 'utf8');
|
|
1462
|
+
} catch (error) {
|
|
1463
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
1464
|
+
existingContent = `# ${path.basename(recommendation.targetPath, '.md')} decisions\n`;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const entry = renderDecisionEntry({
|
|
1468
|
+
decisionId: nextDecisionId,
|
|
1469
|
+
title: recommendation.title,
|
|
1470
|
+
decision: recommendation.decision,
|
|
1471
|
+
rationale: recommendation.rationale,
|
|
1472
|
+
context: recommendation.context,
|
|
1473
|
+
});
|
|
1474
|
+
await writeFile(targetFilePath, `${existingContent.trimEnd()}\n\n${entry}`, 'utf8');
|
|
1475
|
+
applied.push({
|
|
1476
|
+
decision_id: nextDecisionId,
|
|
1477
|
+
target_path: recommendation.targetPath,
|
|
1478
|
+
title: recommendation.title,
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
if (applied.length === 0) {
|
|
1483
|
+
return { applied, warnings: [] };
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const indexResult = options.refreshIndex
|
|
1487
|
+
? await refreshDecisionIndex(options.rootDir)
|
|
1488
|
+
: {
|
|
1489
|
+
refreshed: false,
|
|
1490
|
+
warnings: [
|
|
1491
|
+
'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.',
|
|
1492
|
+
],
|
|
1493
|
+
};
|
|
1494
|
+
|
|
1495
|
+
return {
|
|
1496
|
+
applied: applied.map((item) => ({
|
|
1497
|
+
...item,
|
|
1498
|
+
refreshed_index: indexResult.refreshed,
|
|
1499
|
+
})),
|
|
1500
|
+
warnings: indexResult.warnings,
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
|
|
922
1504
|
async function readNextDecisionId(rootDir) {
|
|
923
1505
|
const decisionsDir = path.join(rootDir, 'decisions');
|
|
924
1506
|
let max = 0;
|
|
@@ -1079,11 +1661,26 @@ function renderDocsReviewMessage(options) {
|
|
|
1079
1661
|
return 'Docs-review preflight passed. Run the generated architecture-review prompt in the selected LLM, then record completion after applying accepted docs changes.';
|
|
1080
1662
|
}
|
|
1081
1663
|
|
|
1664
|
+
function renderApplyOutputMessage(options) {
|
|
1665
|
+
const parts = [];
|
|
1666
|
+
if (options.architectureCount > 0) {
|
|
1667
|
+
parts.push(`${options.architectureCount} accepted architecture doc${options.architectureCount === 1 ? '' : 's'}`);
|
|
1668
|
+
}
|
|
1669
|
+
if (options.coverageApplied) {
|
|
1670
|
+
parts.push('coverage plan');
|
|
1671
|
+
}
|
|
1672
|
+
if (options.decisionCount > 0) {
|
|
1673
|
+
parts.push(`${options.decisionCount} decision recommendation${options.decisionCount === 1 ? '' : 's'}`);
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
return `Applied ${parts.length > 0 ? parts.join(', ') : 'accepted docs-review output'} and completed the docs-review marker.`;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1082
1679
|
function renderReviewPrompt(options) {
|
|
1083
1680
|
return [
|
|
1084
|
-
|
|
1681
|
+
`# ${DOCS_REVIEW_PROMPT_VERSION}`,
|
|
1085
1682
|
'',
|
|
1086
|
-
'Run a
|
|
1683
|
+
'Run a staged VibeCompass architecture documentation review for the project-memory root below. Follow this prompt exactly; do not invent a different document structure.',
|
|
1087
1684
|
'',
|
|
1088
1685
|
'## Review Context',
|
|
1089
1686
|
`- Project memory root: ${options.rootDir}`,
|
|
@@ -1096,13 +1693,37 @@ function renderReviewPrompt(options) {
|
|
|
1096
1693
|
'1. Read `project.yaml`.',
|
|
1097
1694
|
'2. Read `context.md` if present.',
|
|
1098
1695
|
'3. Read all canonical files under `architecture/`, `decisions/`, and finalized `sessions/`.',
|
|
1099
|
-
'4. Inspect
|
|
1696
|
+
'4. Inspect declared repositories before making implementation claims.',
|
|
1697
|
+
'',
|
|
1698
|
+
'## Staged Review Protocol',
|
|
1699
|
+
'Stage 1 — Evidence inventory:',
|
|
1700
|
+
'- Build a compact repo inventory from file paths, manifests/config files, route/job/test directories, and existing architecture frontmatter before reading large file bodies.',
|
|
1701
|
+
'- Mine finalized session notes for decision candidates and product constraints, but cap proposed decisions to the five highest-value architecture choices.',
|
|
1702
|
+
'- Identify candidate domains, features, components, and ownership boundaries with concrete `repo:path` evidence.',
|
|
1703
|
+
'- Do not fetch or summarize broad source bodies until the coverage plan says a file is needed.',
|
|
1704
|
+
'',
|
|
1705
|
+
'Stage 2 — Coverage plan:',
|
|
1706
|
+
'- Propose the smallest useful architecture-doc set that will let future agents understand the project and retrieve targeted context.',
|
|
1707
|
+
'- Prioritize entry points, data ownership, external integrations, jobs/async boundaries, auth/security boundaries, and test surfaces.',
|
|
1708
|
+
'- Mark each proposed doc as `comprehensive`, `partial`, or `initial`, with the reason and blindspots.',
|
|
1709
|
+
'- Emit the accepted plan as one `vibecompass-coverage-plan version=1` JSON fence before architecture-doc blocks.',
|
|
1710
|
+
'- Ask for user acceptance before emitting fenced architecture-doc blocks.',
|
|
1711
|
+
'',
|
|
1712
|
+
'Stage 3 — Bounded doc generation:',
|
|
1713
|
+
'- Generate only accepted docs, one complete fenced block per architecture path.',
|
|
1714
|
+
'- Keep docs concise and retrieval-oriented: explain contracts, flows, invariants, and ownership; do not rewrite source code or produce file-by-file walkthroughs.',
|
|
1715
|
+
`- Soft size budget: keep each generated architecture doc under ${ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES} bytes unless the extra detail is necessary and called out in Blindspots or Retrieval guidance.`,
|
|
1716
|
+
'- Include only the most important involved files; prefer representative entry points and owned contracts over exhaustive file lists.',
|
|
1717
|
+
'',
|
|
1718
|
+
'Stage 4 — Apply and verify:',
|
|
1719
|
+
'- After accepted blocks are written to `state/docs-review-output.md`, run `vibecompass docs-review --apply-output`.',
|
|
1720
|
+
'- Surface any parser warnings, especially oversized docs, so the user can decide whether to compact or accept the output.',
|
|
1100
1721
|
'',
|
|
1101
1722
|
'## Architecture Doc Contract',
|
|
1102
1723
|
'When creating or updating architecture docs, use the existing VibeCompass structure:',
|
|
1103
1724
|
'- path: `architecture/<domain-slug>/<feature-slug>/<component-slug>.md` for component docs',
|
|
1104
1725
|
'- frontmatter fields: `domain`, `feature`, `component`, `status`, plus `repo` or `repos` when implementation-scoped',
|
|
1105
|
-
'- body sections: `## Description`, `## Review metadata`, `## Details`, `## Next steps`, `## Involved files`',
|
|
1726
|
+
'- body sections: `## Description`, `## Review metadata`, `## Details`, `## Retrieval guidance`, `## Next steps`, `## Involved files`',
|
|
1106
1727
|
'',
|
|
1107
1728
|
'Use this exact Review metadata sub-header in every generated architecture doc:',
|
|
1108
1729
|
'',
|
|
@@ -1113,6 +1734,8 @@ function renderReviewPrompt(options) {
|
|
|
1113
1734
|
'- Confidence: high | medium | low',
|
|
1114
1735
|
'- Coverage: comprehensive | partial | initial',
|
|
1115
1736
|
'- Evidence: repo:path references inspected before making implementation claims',
|
|
1737
|
+
'- Retrieval scope: when future agents should load this doc',
|
|
1738
|
+
`- Token posture: compact unless the doc intentionally exceeds ${ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES} bytes`,
|
|
1116
1739
|
'- Blindspots: explicit list, or "None identified" only when evidence is comprehensive',
|
|
1117
1740
|
'',
|
|
1118
1741
|
'## Review Rules',
|
|
@@ -1124,21 +1747,54 @@ function renderReviewPrompt(options) {
|
|
|
1124
1747
|
'6. Put uncertainty in `Blindspots`; do not fill gaps with guesses.',
|
|
1125
1748
|
'7. Do not append real `D-NNN` decisions without explicit user acceptance. Propose candidate decisions separately.',
|
|
1126
1749
|
'8. Do not edit `decisions/INDEX.md` directly unless a canonical decision file was explicitly accepted and appended.',
|
|
1750
|
+
'9. Optimize for future retrieval: project-level docs stay compact; component docs carry focused context for a specific area.',
|
|
1127
1751
|
'',
|
|
1128
1752
|
'## Output Contract',
|
|
1129
1753
|
'First provide a concise findings summary with:',
|
|
1130
1754
|
'- mapped areas',
|
|
1131
1755
|
'- gaps/blindspots',
|
|
1132
|
-
'-
|
|
1756
|
+
'- evidence inventory summary',
|
|
1757
|
+
'- coverage plan and proposed architecture docs',
|
|
1758
|
+
'- token-budget risks or oversized-doc candidates',
|
|
1133
1759
|
'- proposed decisions, if any',
|
|
1134
1760
|
'',
|
|
1761
|
+
'Then output one machine-readable coverage-plan block for the accepted plan:',
|
|
1762
|
+
'',
|
|
1763
|
+
'```vibecompass-coverage-plan version=1',
|
|
1764
|
+
'{',
|
|
1765
|
+
' "summary": "Short coverage plan summary",',
|
|
1766
|
+
' "areas": [',
|
|
1767
|
+
' {',
|
|
1768
|
+
' "id": "stable-area-id",',
|
|
1769
|
+
' "domain": "Domain",',
|
|
1770
|
+
' "feature": "Feature",',
|
|
1771
|
+
' "component": "Component",',
|
|
1772
|
+
' "status": "accepted | deferred | missing",',
|
|
1773
|
+
' "coverage": "comprehensive | partial | initial | missing",',
|
|
1774
|
+
' "proposed_path": "architecture/domain/feature/component.md",',
|
|
1775
|
+
' "evidence": ["repo:path"],',
|
|
1776
|
+
' "blindspots": ["explicit blindspot"]',
|
|
1777
|
+
' }',
|
|
1778
|
+
' ]',
|
|
1779
|
+
'}',
|
|
1780
|
+
'```',
|
|
1781
|
+
'',
|
|
1135
1782
|
'Then, for each architecture doc the user accepts, output one fenced block exactly like this:',
|
|
1136
1783
|
'',
|
|
1137
1784
|
'```vibecompass-architecture-doc path=architecture/domain/feature/component.md',
|
|
1138
1785
|
'<complete markdown file content>',
|
|
1139
1786
|
'```',
|
|
1140
1787
|
'',
|
|
1141
|
-
'
|
|
1788
|
+
'If the review surfaced accepted decision candidates, output each candidate as a separate block without assigning a D-number:',
|
|
1789
|
+
'',
|
|
1790
|
+
'```vibecompass-decision-recommendation target=decisions/cross-cutting.md',
|
|
1791
|
+
'**Title:** Short decision title',
|
|
1792
|
+
'**Decision:** Accepted decision statement to append after local review.',
|
|
1793
|
+
'**Rationale:** Why this choice matters.',
|
|
1794
|
+
'**Context:** Optional supporting context.',
|
|
1795
|
+
'```',
|
|
1796
|
+
'',
|
|
1797
|
+
'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`.',
|
|
1142
1798
|
].join('\n');
|
|
1143
1799
|
}
|
|
1144
1800
|
|