@vibecompass/vibecompass 0.10.0 → 0.10.2

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.
@@ -6,15 +6,27 @@ import { sha256Text } from './hash.js';
6
6
  import { writeStateManifest } from './manifest.js';
7
7
  import { scanProjectMemory } from './project-memory.js';
8
8
  import { parseSimpleYaml } from './simple-yaml.js';
9
+ import {
10
+ buildDocsReviewSourceInventory,
11
+ docsReviewSourceInventoryPath,
12
+ readDocsReviewSourceInventory,
13
+ reconcileCoverageWithSourceInventory,
14
+ summarizeSourceInventory,
15
+ writeDocsReviewSourceInventory,
16
+ } from './source-inventory.js';
9
17
  import { readSyncCursor, resolveSyncBinding } from './sync-binding.js';
10
18
  import { PACKAGE_VERSION } from './version.js';
11
19
 
12
- const DOCS_REVIEW_PROMPT_VERSION = 'VibeCompass Docs Review Prompt v5';
20
+ const DOCS_REVIEW_PROMPT_VERSION = 'VibeCompass Docs Review Prompt v6';
21
+ const DOCS_REVIEW_PARSER_VERSION = 'docs-review-parser-v1';
22
+ const DOCS_REVIEW_DOCUMENTATION_PLAN_PROJECTION_VERSION = 'docs-review-documentation-plan-v1';
23
+ const DOCUMENTATION_PLAN_STATE_VERSION = 1;
13
24
  const ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES = 12000;
14
25
  const COVERAGE_PLAN_OUTPUT_VERSION = 1;
15
26
  const COVERAGE_PLAN_STATUSES = new Set(['accepted', 'deferred', 'missing']);
16
27
  const COVERAGE_LEVELS = new Set(['comprehensive', 'partial', 'initial', 'missing']);
17
28
  const ANCHOR_ACTIONS = new Set(['reuse', 'update', 'split', 'merge', 'defer', 'replace', 'new']);
29
+ const DOCUMENTATION_PLAN_RUN_SCOPES = new Set(['baseline', 'deepening']);
18
30
  const REBUILD_STALE_POLICIES = new Set(['keep', 'archive']);
19
31
  const PROJECT_MAP_PATH = 'architecture/overview/project-shape.md';
20
32
 
@@ -93,6 +105,12 @@ export async function preflightDocsReview(options = {}, environment = {}) {
93
105
  anthropicEnvVar: options.anthropicEnvVar ?? 'ANTHROPIC_API_KEY',
94
106
  syncTarget: options.syncTarget ?? null,
95
107
  });
108
+ const sourceInventory = await buildDocsReviewSourceInventory(project, {
109
+ rootDir,
110
+ cwd,
111
+ sourceRootOverrides: options.sourceRootOverrides,
112
+ });
113
+ await writeDocsReviewSourceInventory(rootDir, sourceInventory);
96
114
  const reviewPrompt = renderReviewPrompt({
97
115
  project,
98
116
  rootDir,
@@ -100,6 +118,7 @@ export async function preflightDocsReview(options = {}, environment = {}) {
100
118
  model: reviewConfig.model,
101
119
  executionMode: promptExecutionMode,
102
120
  anchors: await buildDocsReviewAnchorContext(rootDir),
121
+ sourceInventory,
103
122
  });
104
123
 
105
124
  const status = options.submitHosted
@@ -138,6 +157,7 @@ export async function preflightDocsReview(options = {}, environment = {}) {
138
157
  llm: reviewConfig.llm,
139
158
  model: reviewConfig.model,
140
159
  runtime,
160
+ source_inventory: summarizeSourceInventory(sourceInventory),
141
161
  ...(hosted ? { hosted } : {}),
142
162
  ...(localReview ? { local_review: localReview } : {}),
143
163
  completed_at: null,
@@ -154,8 +174,10 @@ export async function preflightDocsReview(options = {}, environment = {}) {
154
174
  status,
155
175
  hosted,
156
176
  localReview,
177
+ sourceInventory,
157
178
  reviewPrompt,
158
179
  warnings: [
180
+ ...(sourceInventory.warnings ?? []).map((warning) => warning.message ?? `${warning.code}: ${warning.repo_id ?? warning.path ?? ''}`.trim()),
159
181
  ...(localReview?.truncated ? [`Local ${localReview.provider} docs-review stopped at max_tokens; saved output may be partial.`] : []),
160
182
  ],
161
183
  message: renderDocsReviewMessage(options),
@@ -242,14 +264,40 @@ async function applyDocsReviewOutput(options) {
242
264
  }
243
265
 
244
266
  validateArchitectureDocBlocks(blocks);
267
+ const sourceInventory = await readDocsReviewSourceInventory(options.rootDir);
245
268
  const coverageProjection = buildCoverageProjection(coverageBlocks, {
246
269
  outputPath,
247
270
  outputHash: sha256Text(source),
271
+ sourceInventory,
248
272
  });
249
273
  const anchorContext = coverageProjection ? await buildDocsReviewAnchorContext(options.rootDir) : null;
274
+ const sourceInventoryReconciliation = reconcileCoverageWithSourceInventory(coverageProjection, sourceInventory);
275
+ if (coverageProjection && sourceInventory) {
276
+ coverageProjection.source_inventory_summary = summarizeSourceInventory(sourceInventory);
277
+ coverageProjection.reconciliation_summary = sourceInventoryReconciliation
278
+ ? {
279
+ scanned_count: sourceInventoryReconciliation.scanned_count,
280
+ declared_count: sourceInventoryReconciliation.declared_count,
281
+ accounted_count: sourceInventoryReconciliation.accounted_count,
282
+ unaccounted_ids: sourceInventoryReconciliation.unaccounted_ids,
283
+ unknown_declared_ids: sourceInventoryReconciliation.unknown_declared_ids,
284
+ source_unavailable_repo_ids: sourceInventoryReconciliation.source_unavailable_repo_ids,
285
+ }
286
+ : null;
287
+ coverageProjection.warnings = sourceInventoryReconciliation?.warnings ?? [];
288
+ }
289
+ const documentationPlanProjection = coverageProjection
290
+ ? buildDocumentationPlanProjection(coverageProjection, sourceInventory, {
291
+ generatedAt: options.generatedAt,
292
+ })
293
+ : null;
294
+ if (coverageProjection && documentationPlanProjection) {
295
+ coverageProjection.documentation_plan_summary = summarizeDocumentationPlan(documentationPlanProjection);
296
+ }
250
297
  const decisionRecommendations = validateDecisionRecommendationBlocks(decisionRecommendationBlocks);
251
298
  const warnings = [
252
299
  ...createCoveragePlanQualityWarnings(coverageProjection, anchorContext),
300
+ ...(sourceInventoryReconciliation?.warnings ?? []).map((warning) => `${warning.code}: ${warning.message}`),
253
301
  ...createArchitectureDocSizeWarnings(blocks),
254
302
  ...createArchitectureDocQualityWarnings(blocks),
255
303
  ];
@@ -264,6 +312,7 @@ async function applyDocsReviewOutput(options) {
264
312
  }
265
313
 
266
314
  let appliedCoverage = null;
315
+ let appliedDocumentationPlan = null;
267
316
  if (coverageProjection) {
268
317
  const coveragePath = path.join(path.dirname(options.statePath), 'docs-review-coverage.json');
269
318
  await writeFile(coveragePath, `${JSON.stringify(coverageProjection, null, 2)}\n`, 'utf8');
@@ -274,6 +323,16 @@ async function applyDocsReviewOutput(options) {
274
323
  statuses: coverageProjection.statuses,
275
324
  };
276
325
  }
326
+ if (documentationPlanProjection) {
327
+ const documentationPlanPath = path.join(path.dirname(options.statePath), 'docs-review-documentation-plan.json');
328
+ await writeFile(documentationPlanPath, `${JSON.stringify(documentationPlanProjection, null, 2)}\n`, 'utf8');
329
+ appliedDocumentationPlan = {
330
+ path: documentationPlanPath,
331
+ item_count: documentationPlanProjection.summary.item_count,
332
+ by_status: documentationPlanProjection.summary.by_status,
333
+ by_run_scope: documentationPlanProjection.summary.by_run_scope,
334
+ };
335
+ }
277
336
 
278
337
  const appliedDecisionCandidates = decisionRecommendations.length > 0
279
338
  ? await applyDecisionRecommendations({
@@ -300,6 +359,7 @@ async function applyDocsReviewOutput(options) {
300
359
  manifest_hash: manifestResult.manifest.canonical.manifest_hash,
301
360
  architecture_docs: applied,
302
361
  ...(appliedCoverage ? { coverage: appliedCoverage } : {}),
362
+ ...(appliedDocumentationPlan ? { documentation_plan: appliedDocumentationPlan } : {}),
303
363
  ...(appliedDecisionCandidates.applied.length > 0 ? { decision_candidates: appliedDecisionCandidates.applied } : {}),
304
364
  ...(warnings.length > 0 ? { warnings } : {}),
305
365
  applied_at: appliedAt,
@@ -921,13 +981,24 @@ function buildCoverageProjection(blocks, options) {
921
981
 
922
982
  const ids = new Set();
923
983
  const areas = plan.areas.map((area, index) => normalizeCoverageArea(area, index, ids));
984
+ const areaById = new Map(areas.map((area) => [area.id, area]));
985
+ const hasCompletenessInventory = Object.prototype.hasOwnProperty.call(plan, 'completeness_inventory');
986
+ const completenessInventory = normalizeCompletenessInventory(plan.completeness_inventory, areaById);
924
987
  const statuses = countBy(areas, 'status');
925
988
  const coverage_levels = countBy(areas, 'coverage');
926
- const denominator = areas.length;
927
- const acceptedCount = statuses.accepted ?? 0;
989
+ const inventory_statuses = countBy(completenessInventory, 'status');
928
990
 
929
991
  return {
930
992
  version: COVERAGE_PLAN_OUTPUT_VERSION,
993
+ producer: {
994
+ package_version: PACKAGE_VERSION,
995
+ prompt_version: DOCS_REVIEW_PROMPT_VERSION,
996
+ parser_version: DOCS_REVIEW_PARSER_VERSION,
997
+ coverage_projection_version: COVERAGE_PLAN_OUTPUT_VERSION,
998
+ ...(options.sourceInventory?.producer?.scanner_version
999
+ ? { scanner_version: options.sourceInventory.producer.scanner_version }
1000
+ : {}),
1001
+ },
931
1002
  source: {
932
1003
  output_path: options.outputPath,
933
1004
  output_hash: options.outputHash,
@@ -937,13 +1008,107 @@ function buildCoverageProjection(blocks, options) {
937
1008
  ...(normalizeOptionalString(plan.topology) ? { topology: normalizeOptionalString(plan.topology) } : {}),
938
1009
  ...(normalizeCoverageTaxonomy(plan.taxonomy) ? { taxonomy: normalizeCoverageTaxonomy(plan.taxonomy) } : {}),
939
1010
  area_count: areas.length,
940
- coverage_score: denominator === 0 ? null : Number((acceptedCount / denominator).toFixed(4)),
1011
+ ...(hasCompletenessInventory ? {
1012
+ completeness_inventory: completenessInventory,
1013
+ inventory_count: completenessInventory.length,
1014
+ inventory_statuses,
1015
+ } : {}),
1016
+ score_basis: hasCompletenessInventory ? 'model_declared_inventory' : 'area_statuses',
1017
+ coverage_score: calculateCoverageScore({ areas, statuses, completenessInventory, hasCompletenessInventory }),
941
1018
  statuses,
942
1019
  coverage_levels,
943
1020
  areas,
944
1021
  };
945
1022
  }
946
1023
 
1024
+ function buildDocumentationPlanProjection(coverageProjection, sourceInventory, options = {}) {
1025
+ const generatedAt = options.generatedAt instanceof Date ? options.generatedAt : new Date();
1026
+ const inventoryIdsByArea = new Map();
1027
+ for (const item of coverageProjection.completeness_inventory ?? []) {
1028
+ for (const areaId of item.coverage_area_ids ?? []) {
1029
+ const ids = inventoryIdsByArea.get(areaId) ?? [];
1030
+ ids.push(item.id);
1031
+ inventoryIdsByArea.set(areaId, ids);
1032
+ }
1033
+ }
1034
+
1035
+ const items = coverageProjection.areas.map((area) => {
1036
+ const linkedInventoryIds = uniqueStrings([
1037
+ ...(area.linked_inventory_ids ?? []),
1038
+ ...(inventoryIdsByArea.get(area.id) ?? []),
1039
+ ]);
1040
+ return {
1041
+ id: area.id,
1042
+ title: area.title ?? buildCoverageAreaTitle(area),
1043
+ status: area.status,
1044
+ run_scope: area.run_scope ?? 'baseline',
1045
+ expected_coverage: area.coverage,
1046
+ ...(area.proposed_path ? { target_path: area.proposed_path } : {}),
1047
+ ...(area.purpose ? { purpose: area.purpose } : {}),
1048
+ ...(area.parent ? { parent: area.parent } : {}),
1049
+ ...(linkedInventoryIds.length > 0 ? { linked_inventory_ids: linkedInventoryIds } : {}),
1050
+ evidence: area.evidence ?? [],
1051
+ blindspots: area.blindspots ?? [],
1052
+ ...(area.anchor_action ? { anchor_action: area.anchor_action } : {}),
1053
+ ...(area.anchor_paths?.length > 0 ? { anchor_paths: area.anchor_paths } : {}),
1054
+ ...(area.anchor_reason ? { anchor_reason: area.anchor_reason } : {}),
1055
+ };
1056
+ });
1057
+
1058
+ return {
1059
+ version: DOCUMENTATION_PLAN_STATE_VERSION,
1060
+ producer: {
1061
+ package_version: PACKAGE_VERSION,
1062
+ prompt_version: DOCS_REVIEW_PROMPT_VERSION,
1063
+ parser_version: DOCS_REVIEW_PARSER_VERSION,
1064
+ documentation_plan_projection_version: DOCS_REVIEW_DOCUMENTATION_PLAN_PROJECTION_VERSION,
1065
+ coverage_projection_version: coverageProjection.version,
1066
+ ...(sourceInventory?.producer?.scanner_version
1067
+ ? { scanner_version: sourceInventory.producer.scanner_version }
1068
+ : {}),
1069
+ generated_at: generatedAt.toISOString(),
1070
+ },
1071
+ source: coverageProjection.source,
1072
+ summary: {
1073
+ item_count: items.length,
1074
+ by_status: countBy(items, 'status'),
1075
+ by_run_scope: countBy(items, 'run_scope'),
1076
+ linked_inventory_item_count: new Set(items.flatMap((item) => item.linked_inventory_ids ?? [])).size,
1077
+ unlinked_item_count: items.filter((item) => !item.linked_inventory_ids || item.linked_inventory_ids.length === 0).length,
1078
+ },
1079
+ ...(sourceInventory ? { source_inventory_summary: summarizeSourceInventory(sourceInventory) } : {}),
1080
+ items,
1081
+ };
1082
+ }
1083
+
1084
+ function summarizeDocumentationPlan(documentationPlan) {
1085
+ return {
1086
+ path: 'state/docs-review-documentation-plan.json',
1087
+ projection_version: documentationPlan.producer.documentation_plan_projection_version,
1088
+ item_count: documentationPlan.summary.item_count,
1089
+ by_status: documentationPlan.summary.by_status,
1090
+ by_run_scope: documentationPlan.summary.by_run_scope,
1091
+ linked_inventory_item_count: documentationPlan.summary.linked_inventory_item_count,
1092
+ unlinked_item_count: documentationPlan.summary.unlinked_item_count,
1093
+ };
1094
+ }
1095
+
1096
+ function buildCoverageAreaTitle(area) {
1097
+ return [area.domain, area.feature, area.component].filter(Boolean).join(' / ') || area.id;
1098
+ }
1099
+
1100
+ function calculateCoverageScore({ areas, statuses, completenessInventory, hasCompletenessInventory }) {
1101
+ if (hasCompletenessInventory) {
1102
+ const denominator = completenessInventory.length;
1103
+ const acceptedCount = completenessInventory.filter((item) => item.status === 'accepted').length;
1104
+ return denominator === 0 ? null : Number((acceptedCount / denominator).toFixed(4));
1105
+ }
1106
+
1107
+ const denominator = areas.length;
1108
+ const acceptedCount = statuses.accepted ?? 0;
1109
+ return denominator === 0 ? null : Number((acceptedCount / denominator).toFixed(4));
1110
+ }
1111
+
947
1112
  function normalizeCoverageTaxonomy(value) {
948
1113
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
949
1114
  return null;
@@ -961,6 +1126,73 @@ function normalizeCoverageTaxonomy(value) {
961
1126
  };
962
1127
  }
963
1128
 
1129
+ function normalizeCompletenessInventory(value, areaById) {
1130
+ if (value === undefined || value === null) {
1131
+ return [];
1132
+ }
1133
+ if (!Array.isArray(value)) {
1134
+ throw new Error('Coverage plan completeness_inventory must be an array when present.');
1135
+ }
1136
+
1137
+ const ids = new Set();
1138
+ const unknownCoverageAreaReferences = [];
1139
+ const normalized = value.map((item, index) => normalizeCompletenessInventoryItem(
1140
+ item,
1141
+ index,
1142
+ ids,
1143
+ areaById,
1144
+ unknownCoverageAreaReferences,
1145
+ ));
1146
+ if (unknownCoverageAreaReferences.length > 0) {
1147
+ throw new Error(formatUnknownCoverageAreaReferences(unknownCoverageAreaReferences));
1148
+ }
1149
+ return normalized;
1150
+ }
1151
+
1152
+ function normalizeCompletenessInventoryItem(item, index, ids, areaById, unknownCoverageAreaReferences) {
1153
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
1154
+ throw new Error(`Coverage plan completeness_inventory item at index ${index} must be an object.`);
1155
+ }
1156
+
1157
+ const id = normalizeOptionalString(item.id);
1158
+ if (!id) {
1159
+ throw new Error(`Coverage plan completeness_inventory item at index ${index} requires non-empty "id".`);
1160
+ }
1161
+ if (ids.has(id)) {
1162
+ throw new Error(`Coverage plan completeness_inventory contains duplicate item id: ${id}`);
1163
+ }
1164
+ ids.add(id);
1165
+
1166
+ const status = normalizeOptionalString(item.status);
1167
+ if (!COVERAGE_PLAN_STATUSES.has(status)) {
1168
+ throw new Error(`Coverage plan completeness_inventory item "${id}" requires status to be accepted, deferred, or missing.`);
1169
+ }
1170
+
1171
+ const coverageAreaIds = normalizeStringArray(item.coverage_area_ids);
1172
+ for (const areaId of coverageAreaIds) {
1173
+ if (!areaById.has(areaId)) {
1174
+ unknownCoverageAreaReferences.push({ inventoryId: id, areaId });
1175
+ }
1176
+ }
1177
+
1178
+ return {
1179
+ id,
1180
+ ...(normalizeOptionalString(item.kind) ? { kind: normalizeOptionalString(item.kind) } : {}),
1181
+ ...(normalizeOptionalString(item.label) ? { label: normalizeOptionalString(item.label) } : {}),
1182
+ status,
1183
+ ...(coverageAreaIds.length > 0 ? { coverage_area_ids: coverageAreaIds } : {}),
1184
+ evidence: normalizeStringArray(item.evidence),
1185
+ blindspots: normalizeStringArray(item.blindspots),
1186
+ };
1187
+ }
1188
+
1189
+ function formatUnknownCoverageAreaReferences(references) {
1190
+ const formatted = references
1191
+ .map((reference) => `"${reference.inventoryId}" -> "${reference.areaId}"`)
1192
+ .join(', ');
1193
+ return `Coverage plan completeness_inventory references unknown coverage area ids: ${formatted}`;
1194
+ }
1195
+
964
1196
  function normalizeCoverageArea(area, index, ids) {
965
1197
  if (!area || typeof area !== 'object' || Array.isArray(area)) {
966
1198
  throw new Error(`Coverage plan area at index ${index} must be an object.`);
@@ -1002,6 +1234,10 @@ function normalizeCoverageArea(area, index, ids) {
1002
1234
  if (anchorAction && !ANCHOR_ACTIONS.has(anchorAction)) {
1003
1235
  throw new Error(`Coverage plan area "${id}" has invalid anchor_action: ${anchorAction}`);
1004
1236
  }
1237
+ const runScope = normalizeOptionalString(area.run_scope);
1238
+ if (runScope && !DOCUMENTATION_PLAN_RUN_SCOPES.has(runScope)) {
1239
+ throw new Error(`Coverage plan area "${id}" has invalid run_scope: ${runScope}`);
1240
+ }
1005
1241
  const anchorPaths = normalizeStringArray(area.anchor_paths).map(normalizeBlockPath);
1006
1242
  for (const anchorPath of anchorPaths) {
1007
1243
  if (
@@ -1021,7 +1257,14 @@ function normalizeCoverageArea(area, index, ids) {
1021
1257
  component: normalizeOptionalString(area.component),
1022
1258
  status,
1023
1259
  coverage,
1260
+ ...(normalizeOptionalString(area.title) ? { title: normalizeOptionalString(area.title) } : {}),
1261
+ ...(normalizeOptionalString(area.purpose) ? { purpose: normalizeOptionalString(area.purpose) } : {}),
1262
+ ...(normalizeOptionalString(area.parent) ? { parent: normalizeOptionalString(area.parent) } : {}),
1263
+ ...(runScope ? { run_scope: runScope } : {}),
1024
1264
  ...(proposedPath ? { proposed_path: normalizeBlockPath(proposedPath) } : {}),
1265
+ ...(normalizeStringArray(area.linked_inventory_ids).length > 0
1266
+ ? { linked_inventory_ids: normalizeStringArray(area.linked_inventory_ids) }
1267
+ : {}),
1025
1268
  evidence: normalizeStringArray(area.evidence),
1026
1269
  blindspots: normalizeStringArray(area.blindspots),
1027
1270
  ...(normalizeOptionalString(area.reason) ? { reason: normalizeOptionalString(area.reason) } : {}),
@@ -1044,6 +1287,10 @@ function normalizeStringArray(value) {
1044
1287
  : [];
1045
1288
  }
1046
1289
 
1290
+ function uniqueStrings(values) {
1291
+ return [...new Set(values.filter((value) => typeof value === 'string' && value.trim() !== '').map((value) => value.trim()))];
1292
+ }
1293
+
1047
1294
  function normalizeBlockPath(value) {
1048
1295
  return String(value ?? '').trim().replaceAll('\\', '/');
1049
1296
  }
@@ -1134,15 +1381,30 @@ function createCoveragePlanQualityWarnings(coverageProjection, anchors) {
1134
1381
  return [];
1135
1382
  }
1136
1383
 
1384
+ const warnings = [];
1385
+ const hasModernCoverageMetadata = Boolean(coverageProjection.topology || coverageProjection.taxonomy);
1386
+ const hasCompletenessInventory = Array.isArray(coverageProjection.completeness_inventory);
1387
+ if (hasModernCoverageMetadata && !hasCompletenessInventory) {
1388
+ warnings.push('missing_completeness_inventory: coverage plan should enumerate discovered subsystems so deferred or missing areas cannot be silently omitted.');
1389
+ } else if (hasCompletenessInventory) {
1390
+ for (const item of coverageProjection.completeness_inventory) {
1391
+ if (item.status === 'accepted' && (!Array.isArray(item.coverage_area_ids) || item.coverage_area_ids.length === 0)) {
1392
+ warnings.push(`accepted_inventory_without_area: completeness_inventory item "${item.id}" is accepted but does not reference coverage_area_ids.`);
1393
+ }
1394
+ if (item.status !== 'accepted' && (!Array.isArray(item.blindspots) || item.blindspots.length === 0)) {
1395
+ warnings.push(`incomplete_inventory_blindspot: completeness_inventory item "${item.id}" is ${item.status} but does not explain the gap in blindspots.`);
1396
+ }
1397
+ }
1398
+ }
1399
+
1137
1400
  const hasPriorAnchors =
1138
1401
  (anchors?.architectureDocs?.length ?? 0) > 0 ||
1139
1402
  (anchors?.projectMapFeatures?.length ?? 0) > 0 ||
1140
1403
  Boolean(anchors?.coverage);
1141
1404
  if (!hasPriorAnchors) {
1142
- return [];
1405
+ return warnings;
1143
1406
  }
1144
1407
 
1145
- const warnings = [];
1146
1408
  for (const area of coverageProjection.areas) {
1147
1409
  if (!area.anchor_action) {
1148
1410
  warnings.push(`missing_anchor_action: coverage area "${area.id}" should classify prior docs or project-map identities when re-review anchors exist.`);
@@ -1950,6 +2212,55 @@ function renderAnchorContext(anchors) {
1950
2212
  return lines.join('\n');
1951
2213
  }
1952
2214
 
2215
+ function renderSourceInventoryContext(inventory) {
2216
+ const lines = [
2217
+ '## Package Source Inventory',
2218
+ 'Use this package-scanned inventory as the source-evidence denominator for coverage planning. Account for each scanned item as accepted, deferred, or missing in `completeness_inventory` when evidence is sufficient.',
2219
+ '',
2220
+ ];
2221
+
2222
+ if (!inventory) {
2223
+ lines.push('No package source inventory was available.');
2224
+ return lines.join('\n');
2225
+ }
2226
+
2227
+ const itemCount = Array.isArray(inventory.items) ? inventory.items.length : 0;
2228
+ lines.push(`Scanner: ${inventory.producer?.scanner_version ?? 'unknown'}; items=${itemCount}; warnings=${inventory.warnings?.length ?? 0}.`);
2229
+ if (Array.isArray(inventory.source_roots) && inventory.source_roots.length > 0) {
2230
+ lines.push('Source roots:');
2231
+ for (const sourceRoot of inventory.source_roots) {
2232
+ const details = [
2233
+ `kind=${sourceRoot.kind}`,
2234
+ `status=${sourceRoot.status}`,
2235
+ sourceRoot.repo_root_path ? `repo_root_path=${sourceRoot.repo_root_path}` : null,
2236
+ ].filter(Boolean).join('; ');
2237
+ lines.push(`- ${sourceRoot.repo_id}: ${details}`);
2238
+ }
2239
+ }
2240
+
2241
+ if (Array.isArray(inventory.warnings) && inventory.warnings.length > 0) {
2242
+ lines.push('Source inventory warnings:');
2243
+ for (const warning of inventory.warnings.slice(0, 20)) {
2244
+ lines.push(`- ${warning.code}: ${warning.message}`);
2245
+ }
2246
+ }
2247
+
2248
+ if (itemCount > 0) {
2249
+ lines.push('Scanned inventory items:');
2250
+ for (const item of inventory.items.slice(0, 80)) {
2251
+ const evidence = item.evidence?.slice(0, 3).map((entry) => entry.path).join(', ');
2252
+ lines.push(`- ${item.id} (${item.kind}, ${item.confidence}): ${item.label}${evidence ? `; evidence=${evidence}` : ''}`);
2253
+ }
2254
+ if (itemCount > 80) {
2255
+ lines.push(`- ... ${itemCount - 80} additional scanned inventory items omitted from prompt summary.`);
2256
+ }
2257
+ } else {
2258
+ lines.push('Scanned inventory items: none. If source roots are unavailable, say coverage is not source-backed for those repos.');
2259
+ }
2260
+
2261
+ return lines.join('\n');
2262
+ }
2263
+
1953
2264
  function renderReviewPrompt(options) {
1954
2265
  const outputPath = path.join(options.rootDir, 'state', 'docs-review-output.md');
1955
2266
  const applyCommand = `vibecompass docs-review --root ${shellQuote(options.rootDir)} --apply-output --output ${shellQuote(outputPath)}`;
@@ -2021,22 +2332,31 @@ function renderReviewPrompt(options) {
2021
2332
  '',
2022
2333
  renderAnchorContext(options.anchors),
2023
2334
  '',
2335
+ renderSourceInventoryContext(options.sourceInventory),
2336
+ '',
2024
2337
  '## Staged Review Protocol',
2025
2338
  'Stage 1 — Evidence inventory:',
2026
2339
  '- Build a compact repo inventory from file paths, manifests/config files, route/job/test directories, and existing architecture frontmatter before reading large file bodies.',
2027
2340
  '- Identify the project topology before choosing docs: single-repo/monolith, multi-repo, multi-surface, package/CLI, workers/services, mobile app, or mixed. State the topology in the coverage plan.',
2341
+ '- Build a completeness inventory of discovered apps/repos, routes/screens, API endpoints, tables/storage, jobs/cron/tasks, auth/session flows, external integrations, platform surfaces, and product features. Keep it subsystem-level, not a raw file dump.',
2028
2342
  '- Mine finalized session notes for decision candidates and product constraints, but cap proposed decisions to the five highest-value architecture choices.',
2029
2343
  '- Identify candidate domains, features, components, ownership boundaries, user journey entry points, feature-to-feature relationships, and system-layer connections with concrete `repo:path` evidence.',
2344
+ '- Separate product features from runtime surfaces. Web, mobile, backend/API, database/storage, jobs, external integrations, and deployment/runtime are surfaces or systems that participate in features; do not treat them as competing feature categories unless the project itself is a platform/tooling project.',
2030
2345
  '- Do not fetch or summarize broad source bodies until the coverage plan says a file is needed.',
2031
2346
  '',
2032
2347
  'Stage 2 — Coverage plan:',
2033
2348
  '- Propose the smallest useful architecture-doc set that will let future agents understand the project and retrieve targeted context.',
2034
2349
  '- The minimum useful set must cover: user journey, project/system map (frontend/backend/API/data/integration structure and connections), and summaries of the core journey-facing features.',
2350
+ '- Treat Stage 2 as a documentation-plan gate: each proposed doc needs a title, target path, purpose, parent/backbone grouping, linked inventory ids, evidence set, expected coverage level, prior-anchor action, and `baseline` or `deepening` run scope.',
2351
+ '- Keep baseline docs breadth-first and compact. Use `deepening` only for scoped follow-up docs that should not block the first project-understanding baseline.',
2035
2352
  '- Prioritize entry points, user-flow/gating relationships, data ownership, external integrations, jobs/async boundaries, auth/security boundaries, and test surfaces.',
2353
+ '- Account for each completeness-inventory item in the coverage plan as accepted, deferred, or missing. Do not inflate coverage by omitting large discovered subsystems; honest deferred/missing entries are expected when the first pass cannot cover everything.',
2354
+ '- Before generating prose, self-check that every `completeness_inventory[].coverage_area_ids[]` value points to an `areas[].id` in the same accepted plan. Fix all dangling ids together at the plan gate.',
2036
2355
  '- Choose one primary taxonomy axis from topology evidence and keep it stable: single-repo/monolith may default domain-first; multi-repo or multi-surface projects may use platform/repo-first or domain-first with per-surface components. Do not mix taxonomy axes at the same tier unless you explicitly propose and justify a restructuring.',
2037
2356
  '- For every prior architecture doc or project-map feature identity in Re-review Anchors, classify the plan action as `reuse`, `update`, `split`, `merge`, `defer`, or `replace`. Use `new` only for areas not represented by the prior tree.',
2038
2357
  '- Every `split`, `merge`, or `replace` action requires an evidence-backed anchor reason before prose generation. No unexplained slug rename is allowed.',
2039
2358
  '- Treat the accepted Stage-2 plan as the taxonomy and feature-list freeze for this docs-review run.',
2359
+ '- The user can revise the Stage-2 plan by merging, splitting, demoting, deferring, or requesting deepening before prose generation.',
2040
2360
  '- Mark each proposed doc as `comprehensive`, `partial`, or `initial`, with the reason and blindspots.',
2041
2361
  ...coveragePlanStageLines,
2042
2362
  '',
@@ -2047,6 +2367,7 @@ function renderReviewPrompt(options) {
2047
2367
  '- Keep docs concise and retrieval-oriented: explain contracts, flows, invariants, and ownership; do not rewrite source code or produce file-by-file walkthroughs.',
2048
2368
  `- 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.`,
2049
2369
  '- Include only the most important involved files; prefer representative entry points and owned contracts over exhaustive file lists.',
2370
+ '- Do not put transient apply-state instructions inside architecture docs, such as "this artifact has not been applied". Apply state belongs in the chat response and package marker, not canonical docs.',
2050
2371
  '',
2051
2372
  'Stage 4 — Apply and verify:',
2052
2373
  stage4WriteLine,
@@ -2058,6 +2379,8 @@ function renderReviewPrompt(options) {
2058
2379
  '- frontmatter fields: `domain`, `feature`, `component`, `status`, plus `repo` or `repos` when implementation-scoped',
2059
2380
  '- body sections: `## Description`, `## Review metadata`, `## Details`, `## Retrieval guidance`, `## Next steps`, `## Involved files`',
2060
2381
  '- overview docs may add compact peer sections for Stage 1 backbone outputs when useful: `## Topology`, `## Core feature inventory`, `## User journey map`, `## Project systems map`, and `## Coverage and quality summary`',
2382
+ '- feature docs may add a compact `## Surface matrix` prose table when multiple surfaces participate. Keep it evidence-sized: include participating surfaces with concrete `repo:path` evidence and explicit N/A rows only when an absence is important to retrieval. Do not emit a new machine-readable surface block.',
2383
+ '- surface/platform ownership docs should stay inside the existing contract, typically as `architecture/platform/<surface>/<component>.md`; do not introduce a top-level `systems/` tree.',
2061
2384
  '',
2062
2385
  'Use this exact Review metadata sub-header in every generated architecture doc:',
2063
2386
  '',
@@ -2121,6 +2444,7 @@ function renderReviewPrompt(options) {
2121
2444
  '',
2122
2445
  'Only include relationships supported by evidence. Prefer `navigates_to` and `gates` for the main user journey; use `depends_on` and `writes_to` for supporting system relationships. If useful journey links cannot be inferred, state that as a coverage gap rather than inventing links.',
2123
2446
  '`systems[]` is optional derived overview metadata, not a new canonical taxonomy, relationship schema, or projection source. Include it only when evidence identifies runtime/deployable units or external systems; treat `owns_features` as documentation-only grouping, put user-journey links in `relationships[]`, and record uncertain systems or system-to-system connections as blindspots.',
2447
+ 'Per-feature surface matrices are derived prose. `systems[].owns_features` remains the only machine-readable system-to-feature hint in this release; do not duplicate that graph in another parsed block.',
2124
2448
  '',
2125
2449
  '## Output Contract',
2126
2450
  'First provide a concise findings summary with:',
@@ -2129,10 +2453,11 @@ function renderReviewPrompt(options) {
2129
2453
  '- user journey and project/system map coverage',
2130
2454
  '- gaps/blindspots',
2131
2455
  '- evidence inventory summary',
2456
+ '- completeness inventory summary with accepted/deferred/missing accounting',
2132
2457
  '- topology and chosen taxonomy axis',
2133
2458
  '- coverage plan and proposed architecture docs',
2134
2459
  '- prior-doc anchor classifications and reasons',
2135
- '- coverage/quality report, including missing/generic evidence, unmapped core features, oversized-doc candidates, and unresolved blindspots',
2460
+ '- coverage/quality report, including missing/generic evidence, unmapped core features, oversized-doc candidates, unresolved blindspots, and a note that the score is over the evidence/completeness inventory when present, not the number of doc files',
2136
2461
  '- token-budget risks or oversized-doc candidates',
2137
2462
  '- proposed decisions, if any',
2138
2463
  '',
@@ -2146,15 +2471,31 @@ function renderReviewPrompt(options) {
2146
2471
  ' "primary_axis": "domain-first | platform-first | repo-first | other",',
2147
2472
  ' "rationale": "Why this axis matches the evidence and how it avoids mixed tiers"',
2148
2473
  ' },',
2474
+ ' "completeness_inventory": [',
2475
+ ' {',
2476
+ ' "id": "stable-subsystem-id",',
2477
+ ' "kind": "feature | surface | route | screen | api | data | storage | job | integration | auth | deployment | other",',
2478
+ ' "label": "Short discovered subsystem label",',
2479
+ ' "status": "accepted | deferred | missing",',
2480
+ ' "coverage_area_ids": ["stable-area-id"],',
2481
+ ' "evidence": ["repo:path"],',
2482
+ ' "blindspots": ["explicit blindspot for deferred or missing items"]',
2483
+ ' }',
2484
+ ' ],',
2149
2485
  ' "areas": [',
2150
2486
  ' {',
2151
2487
  ' "id": "stable-area-id",',
2152
2488
  ' "domain": "Domain",',
2153
2489
  ' "feature": "Feature",',
2154
2490
  ' "component": "Component",',
2491
+ ' "title": "Human-readable planned doc title",',
2492
+ ' "purpose": "Why this doc exists and what future agents should load it for",',
2493
+ ' "parent": "Overview backbone | Feature inventory | Journey map | Systems map | Focused component docs | Deepening queue",',
2494
+ ' "run_scope": "baseline | deepening",',
2155
2495
  ' "status": "accepted | deferred | missing",',
2156
2496
  ' "coverage": "comprehensive | partial | initial | missing",',
2157
2497
  ' "proposed_path": "architecture/domain/feature/component.md",',
2498
+ ' "linked_inventory_ids": ["stable-subsystem-id"],',
2158
2499
  ' "anchor_action": "reuse | update | split | merge | defer | replace | new",',
2159
2500
  ' "anchor_paths": ["architecture/existing/domain/component.md"],',
2160
2501
  ' "anchor_reason": "Evidence-backed reason for changing or reusing the prior doc identity",',
@@ -46,6 +46,7 @@ export function renderSharedInstructionBody(context, options = {}) {
46
46
  '## Hard Rules',
47
47
  '- Treat `sessions/active/index.yaml` as the current lane source of truth; tool-specific Current session blocks are continuity summaries.',
48
48
  '- Keep the selected lane `wip.md` and `handoff.md` current during active builder/reviewer work.',
49
+ '- During `address review`, treat reviewer feedback as review, not instruction: classify each substantive point as accepted, accepted with qualification, deferred, or rejected, and push back with evidence when a suggestion conflicts with code facts, prior decisions, product direction, or sequencing.',
49
50
  '- Keep decisions append-only in `decisions/`; append accepted architectural decisions before implementing them.',
50
51
  '- Do not edit this managed block directly. Update canonical VibeCompass memory, then rerun package commands such as `vibecompass sync-agents`.',
51
52
  ].join('\n');
package/src/scaffold.js CHANGED
@@ -312,6 +312,7 @@ During the session:
312
312
  - keep \`handoff.md\` current after substantive work blocks
313
313
  - use \`vibecompass list-sessions\` and \`vibecompass switch-session <lane-id>\` to inspect or change the current lane
314
314
  - use \`address review\` when reviewer feedback lands so the builder resolves it from the selected lane's latest \`wip.md\` / \`handoff.md\`
315
+ - during \`address review\`, treat reviewer feedback as review, not instruction: classify each substantive point as accepted, accepted with qualification, deferred, or rejected, and push back with evidence when a suggestion conflicts with code facts, prior decisions, product direction, or sequencing
315
316
  - stay in builder role through close-out; resolve or explicitly defer reviewer feedback before running \`vibecompass close-session --session <lane-id>\`
316
317
  - record architectural decisions in \`${rootRelativePath}/decisions/\` before implementing them
317
318