@vibecompass/vibecompass 0.12.0 → 0.13.1

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 CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.13.1 - 2026-09-11
6
+
7
+ - Add strict backward compatibility for intentional legacy recovery checkpoints
8
+ at root `sessions/YYYY-MM-DD.md` paths with an exact
9
+ `# Recovery checkpoint — YYYY-MM-DD — <title>` H1; project them with their
10
+ matching `session_date`, title, and
11
+ `session_number: null` without inventing numbered-session chronology.
12
+ - Preserve fail-closed behavior for malformed, mismatched, untitled, or nested
13
+ recovery checkpoints and all existing numbered-session behavior, with focused
14
+ parser, manifest, read-model, and session-lifecycle regression coverage.
15
+
16
+ ## 0.13.0 - 2026-07-11
17
+
18
+ - Architecture docs are now explicitly current-state contracts (D-292/D-293):
19
+ generated context/README/workflow guidance, the docs-update recommendations,
20
+ close-session guidance, and the docs-review prompt all state that updates
21
+ fold into existing sections in place — work/ship chronology goes to session
22
+ notes, decisions record accepted choices, and the doc keeps the durable
23
+ plan, unresolved next steps, and rollout state.
24
+ - New advisory changelog-shape detection: the shared scanner emits one
25
+ aggregated `architecture-changelog-smell` warning per doc (conjunctive
26
+ line-local signals; fenced/inline code, comments, and blockquotes masked),
27
+ and `docs-review --apply-output` emits the matching
28
+ `changelog_shaped_architecture_doc` warning. Optional frontmatter
29
+ `content_mode: chronological-ledger` suppresses only this advisory;
30
+ explicit unknown/non-string values warn. Advisory-only — never gates
31
+ close-session or read-model projection.
32
+ - `docs-update` affected docs now carry a self-describing
33
+ `size: { byteLength, softLimitBytes, exceedsSoftLimit }` advisory (the
34
+ 12000-byte soft budget is hoisted from docs-review into the shared
35
+ scanner); a session-scoped size advisory renders in the plan instead of a
36
+ standing status warning.
37
+ - Local docs-review prompt is v8; local parser is `docs-review-parser-v2`
38
+ (new warning codes). `detectChangelogShapedLines` and
39
+ `ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES` are exported as public API for
40
+ hosted-parser parity consumers.
41
+
5
42
  ## 0.12.0 - 2026-07-10
6
43
 
7
44
  - New `vibecompass bootstrap --bundle <file>` materializes a complete local
package/README.md CHANGED
@@ -294,12 +294,21 @@ import {
294
294
  scanProjectMemory,
295
295
  loadProjectReadModel,
296
296
  getProjectContext,
297
+ detectChangelogShapedLines,
298
+ ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES,
297
299
  } from "@vibecompass/vibecompass";
298
300
  ```
299
301
 
300
302
  Use the JavaScript API when building tools that need to inspect or manage a
301
303
  local VibeCompass project-memory root.
302
304
 
305
+ `detectChangelogShapedLines(body)` takes architecture-doc body text (frontmatter
306
+ already stripped) and returns the changelog-shaped lines it matched — the same
307
+ D-292 advisory detector the scanner and `docs-review --apply-output` use, shared
308
+ so external validators (for example the hosted proposal parser) stay in parity.
309
+ `ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES` is the byte-valued soft size budget
310
+ (12000) both docs-review apply warnings and the docs-update size advisory read.
311
+
303
312
  ## More Documentation
304
313
 
305
314
  - Developer guide: https://vibecompass.dev/developers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecompass/vibecompass",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "description": "Local-first project-memory core for VibeCompass",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,7 +4,11 @@ import { createInterface } from 'node:readline/promises';
4
4
  import { parseFrontmatter } from './frontmatter.js';
5
5
  import { sha256Text } from './hash.js';
6
6
  import { writeStateManifest } from './manifest.js';
7
- import { scanProjectMemory } from './project-memory.js';
7
+ import {
8
+ ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES,
9
+ detectChangelogShapedLines,
10
+ scanProjectMemory,
11
+ } from './project-memory.js';
8
12
  import { parseSimpleYaml } from './simple-yaml.js';
9
13
  import {
10
14
  buildDocsReviewSourceInventory,
@@ -20,12 +24,11 @@ import { withMemoryRootLock } from './serialization.js';
20
24
  import { listDecisionFileNames, readNextDecisionId } from './decisions.js';
21
25
  import { looksLikeGroupedDecisionIndex } from './decision-index.js';
22
26
 
23
- const DOCS_REVIEW_PROMPT_VERSION = 'VibeCompass Docs Review Prompt v7';
24
- const DOCS_REVIEW_PARSER_VERSION = 'docs-review-parser-v1';
27
+ const DOCS_REVIEW_PROMPT_VERSION = 'VibeCompass Docs Review Prompt v8';
28
+ const DOCS_REVIEW_PARSER_VERSION = 'docs-review-parser-v2';
25
29
  const DOCS_REVIEW_ACCEPTED_OUTPUT_CONTRACT_VERSION = 'docs-review-accepted-output-v7';
26
30
  const DOCS_REVIEW_DOCUMENTATION_PLAN_PROJECTION_VERSION = 'docs-review-documentation-plan-v1';
27
31
  const DOCUMENTATION_PLAN_STATE_VERSION = 1;
28
- const ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES = 12000;
29
32
  const COVERAGE_PLAN_OUTPUT_VERSION = 1;
30
33
  const COVERAGE_PLAN_STATUSES = new Set(['accepted', 'deferred', 'missing']);
31
34
  const COVERAGE_LEVELS = new Set(['comprehensive', 'partial', 'initial', 'missing']);
@@ -1496,6 +1499,24 @@ function createArchitectureDocQualityWarnings(blocks) {
1496
1499
  ) {
1497
1500
  warnings.push(`missing_repo_scope: ${block.path} should declare repo or repos in frontmatter when implementation-scoped.`);
1498
1501
  }
1502
+
1503
+ let contentMode = null;
1504
+ if (Object.hasOwn(data, 'content_mode')) {
1505
+ const rawContentMode = data.content_mode;
1506
+ contentMode = typeof rawContentMode === 'string' ? rawContentMode.trim() : null;
1507
+ if (contentMode !== 'chronological-ledger') {
1508
+ const rendered = typeof rawContentMode === 'string' ? `"${rawContentMode}"` : JSON.stringify(rawContentMode) ?? 'undefined';
1509
+ warnings.push(`unknown_content_mode: ${block.path} declares content_mode ${rendered}; the recognized value is "chronological-ledger" (absence means current-state).`);
1510
+ contentMode = null;
1511
+ }
1512
+ }
1513
+ if (contentMode !== 'chronological-ledger') {
1514
+ const changelogLines = detectChangelogShapedLines(body);
1515
+ if (changelogLines.length > 0) {
1516
+ const example = changelogLines[0].length > 80 ? `${changelogLines[0].slice(0, 77)}...` : changelogLines[0];
1517
+ warnings.push(`changelog_shaped_architecture_doc: ${block.path} contains ${changelogLines.length} changelog-shaped line(s) (e.g. "${example}"); architecture docs are current-state contracts (D-292) — fold the content into the existing sections and move work/ship chronology to the session note; decisions record accepted choices (D-293). Intentionally dated ledgers/reports declare content_mode: chronological-ledger.`);
1518
+ }
1519
+ }
1499
1520
  }
1500
1521
 
1501
1522
  return warnings;
@@ -2333,7 +2354,7 @@ function renderAnchorContext(anchors) {
2333
2354
  '',
2334
2355
  'Required prior-doc classifications for Stage 2:',
2335
2356
  '- `reuse`: keep path/frontmatter identity and update only if needed',
2336
- '- `update`: keep path/frontmatter identity but refresh content or coverage',
2357
+ '- `update`: keep path/frontmatter identity but refresh content or coverage by rewriting the affected sections in place — deepening or audit findings fold into the current contract, never an appended dated section',
2337
2358
  '- `split`: replace one broad prior doc with multiple focused docs',
2338
2359
  '- `merge`: combine overlapping prior docs',
2339
2360
  '- `defer`: leave prior area documented but out of the accepted generation scope',
@@ -2532,7 +2553,7 @@ function renderReviewPrompt(options) {
2532
2553
  '- Propose the smallest useful architecture-doc set that will let future agents understand the project and retrieve targeted context.',
2533
2554
  '- 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.',
2534
2555
  '- 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.',
2535
- '- Keep baseline docs breadth-first and compact. Use `deepening` only for scoped follow-up docs that should not block the first project-understanding baseline.',
2556
+ '- Keep baseline docs breadth-first and compact. Use `deepening` only for scoped follow-up docs that should not block the first project-understanding baseline. Deepening output still rewrites existing docs in place or creates new focused docs; it never appends dated audit/session sections to an existing doc.',
2536
2557
  '- Prioritize entry points, user-flow/gating relationships, data ownership, external integrations, jobs/async boundaries, auth/security boundaries, and test surfaces.',
2537
2558
  '- 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.',
2538
2559
  '- 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.',
@@ -2550,6 +2571,7 @@ function renderReviewPrompt(options) {
2550
2571
  '- Ensure `architecture/overview/project-shape.md` includes compact backbone sections for topology, core feature inventory, user journey map, project systems map, and coverage/quality summary.',
2551
2572
  '- Ensure `architecture/overview/project-shape.md` includes exactly one `vibecompass-project-map version=1` JSON fence that lists journey-facing features, supported feature relationships, and minimal optional derived `systems[]` metadata.',
2552
2573
  '- Keep docs concise and retrieval-oriented: explain contracts, flows, invariants, and ownership; do not rewrite source code or produce file-by-file walkthroughs.',
2574
+ '- Write every doc as a current-state contract (D-292): when updating an existing doc, rewrite the affected sections in place or propose a new focused doc — never append dated session/audit sections (for example "Audit deepening — YYYY-MM-DD"). Keep the durable plan, unresolved next steps, and material rollout state in the doc; work/ship chronology belongs in session notes, and decision entries record accepted choices (D-293). Intentionally dated ledgers/reports declare frontmatter `content_mode: chronological-ledger`.',
2553
2575
  `- 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.`,
2554
2576
  '- Include only the most important involved files; prefer representative entry points and owned contracts over exhaustive file lists.',
2555
2577
  '- 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.',
@@ -5,7 +5,7 @@ import { promisify } from 'node:util';
5
5
  import { parseFrontmatter } from './frontmatter.js';
6
6
  import { readCurrentRepoHead, resolveRepoSourceDir } from './git-binding.js';
7
7
  import { resolveLaneMarkerContext, resolveLaneSelection } from './lane-marker.js';
8
- import { scanProjectMemory } from './project-memory.js';
8
+ import { ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES, scanProjectMemory } from './project-memory.js';
9
9
  import { parseSimpleYaml } from './simple-yaml.js';
10
10
 
11
11
  const execFileAsync = promisify(execFile);
@@ -140,6 +140,9 @@ export function renderDocsUpdatePlan(plan) {
140
140
  if (doc.qualityWarnings.length > 0) {
141
141
  lines.push(` - quality warnings: ${doc.qualityWarnings.map((warning) => warning.code).join(', ')}`);
142
142
  }
143
+ if (doc.size?.exceedsSoftLimit) {
144
+ lines.push(` - size advisory: ${doc.size.byteLength} bytes exceeds the ${doc.size.softLimitBytes}-byte soft budget; this session is about to edit the doc — split it into focused component docs or trim low-value detail while folding in changes.`);
145
+ }
143
146
  }
144
147
  }
145
148
 
@@ -817,6 +820,7 @@ function summarizeArchitectureDocument(document) {
817
820
  feature: normalizeOptionalString(data.feature),
818
821
  component: normalizeOptionalString(data.component),
819
822
  involvedFiles: extractInvolvedFiles(frontmatter.body),
823
+ byteLength: document.byteLength,
820
824
  warnings: document.warnings,
821
825
  };
822
826
  }
@@ -853,6 +857,14 @@ function findAffectedArchitectureDocs(docs, delta) {
853
857
  path: doc.path,
854
858
  reasons: Array.from(new Set(reasons)),
855
859
  qualityWarnings: doc.warnings,
860
+ // Session-scoped size advisory (D-292): surfaced only for docs the
861
+ // session is about to touch, deliberately not a standing status
862
+ // check. Self-describing so every renderer reads the same values.
863
+ size: {
864
+ byteLength: typeof doc.byteLength === 'number' ? doc.byteLength : null,
865
+ softLimitBytes: ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES,
866
+ exceedsSoftLimit: typeof doc.byteLength === 'number' && doc.byteLength > ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES,
867
+ },
856
868
  };
857
869
  })
858
870
  .filter((doc) => doc.reasons.length > 0)
@@ -906,9 +918,9 @@ function buildRecommendations(options) {
906
918
  const recommendations = [];
907
919
 
908
920
  if (options.affectedArchitectureDocs.length > 0) {
909
- recommendations.push('Review and update the affected architecture docs listed above, then close with --architecture-docs updated.');
921
+ recommendations.push('Fold the session changes into the affected architecture docs as current-state contracts — rewrite sections in place, keep the durable plan/next steps/rollout state in the doc, and move work/ship chronology to the session note (D-292, D-293) — then close with --architecture-docs updated.');
910
922
  } else if (options.changedFiles.length > 0 && options.affectedArchitectureDocs.length === 0) {
911
- recommendations.push('No matching architecture doc was found for the session delta; create a focused doc, mark docs not-needed with evidence, or defer explicitly.');
923
+ recommendations.push('No matching architecture doc was found for the session delta; create a focused doc written as a current-state contract, mark docs not-needed with evidence, or defer explicitly.');
912
924
  } else {
913
925
  recommendations.push('No architecture doc update is indicated by the current delta; close with --architecture-docs not-needed if that remains true.');
914
926
  }
package/src/index.js CHANGED
@@ -15,7 +15,11 @@ export {
15
15
  pullPreviewProjectMemory,
16
16
  pushProjectMemory,
17
17
  } from './sync.js';
18
- export { scanProjectMemory } from './project-memory.js';
18
+ export {
19
+ ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES,
20
+ detectChangelogShapedLines,
21
+ scanProjectMemory,
22
+ } from './project-memory.js';
19
23
  export {
20
24
  closeProjectSession,
21
25
  listProjectSessions,
@@ -5,6 +5,15 @@ import { sha256Text } from './hash.js';
5
5
  import { parseSimpleYaml } from './simple-yaml.js';
6
6
 
7
7
  const PROJECT_FILE = 'project.yaml';
8
+ // Shared soft size budget for architecture docs (docs-review apply and the
9
+ // docs-update affected-doc advisory read the same threshold).
10
+ export const ARCHITECTURE_DOC_SOFT_SIZE_LIMIT_BYTES = 12000;
11
+ const KNOWN_ARCHITECTURE_CONTENT_MODES = new Set(['chronological-ledger']);
12
+ const ISO_DATE_PATTERN = /\b\d{4}-\d{2}-\d{2}\b/;
13
+ const HEADING_CHRONOLOGY_CUES = /\b(audit deepening|updated|fixes|fixed|shipped|done|completed|activation gate)\b/i;
14
+ // ATX heading start: 1-6 hashes followed by space, tab, or end of line
15
+ // (CommonMark permits bare `#`..`######`); deliberately not broad \s.
16
+ const ATX_HEADING_START = /^#{1,6}([ \t]|$)/;
8
17
  const NON_CANONICAL_DECISION_FILES = new Set(['EXAMPLE.md', 'INDEX.md', 'README.md']);
9
18
  const NON_CANONICAL_SESSION_FILES = new Set(['wip.md', 'handoff.md', 'README.md']);
10
19
  const NON_CANONICAL_ARCHITECTURE_FILES = new Set(['README.md']);
@@ -462,6 +471,24 @@ function parseArchitectureDocument(options) {
462
471
  );
463
472
  }
464
473
 
474
+ // Only an absent content_mode key means current-state; any explicit value
475
+ // that is not the exact string "chronological-ledger" warns (D-292).
476
+ let contentMode = null;
477
+ if (Object.hasOwn(data, 'content_mode')) {
478
+ const rawContentMode = data.content_mode;
479
+ contentMode = typeof rawContentMode === 'string' ? rawContentMode.trim() : null;
480
+ if (!contentMode || !KNOWN_ARCHITECTURE_CONTENT_MODES.has(contentMode)) {
481
+ const rendered = typeof rawContentMode === 'string' ? `"${rawContentMode}"` : JSON.stringify(rawContentMode) ?? 'undefined';
482
+ warnings.push(
483
+ createWarning(
484
+ 'architecture-unknown-content-mode',
485
+ `Unknown architecture content_mode ${rendered}; the recognized value is "chronological-ledger" (absence means current-state).`,
486
+ ),
487
+ );
488
+ contentMode = null;
489
+ }
490
+ }
491
+
465
492
  const sections = extractLevelTwoSections(frontmatter.body);
466
493
  for (const section of RECOMMENDED_ARCHITECTURE_SECTIONS) {
467
494
  if (!sections.has(section.toLowerCase())) {
@@ -508,6 +535,21 @@ function parseArchitectureDocument(options) {
508
535
  );
509
536
  }
510
537
 
538
+ // D-292: architecture docs are current-state contracts; one aggregated
539
+ // advisory per doc, suppressed only by content_mode: chronological-ledger.
540
+ if (contentMode !== 'chronological-ledger') {
541
+ const changelogLines = detectChangelogShapedLines(frontmatter.body);
542
+ if (changelogLines.length > 0) {
543
+ const example = changelogLines[0].length > 80 ? `${changelogLines[0].slice(0, 77)}...` : changelogLines[0];
544
+ warnings.push(
545
+ createWarning(
546
+ 'architecture-changelog-smell',
547
+ `Architecture doc contains ${changelogLines.length} changelog-shaped line(s) (e.g. "${example}"). Architecture docs are current-state contracts (D-292): fold the change into the existing sections, keep the durable plan and rollout state in the doc, and move work/ship chronology to the session note — decisions record accepted choices (D-293). Intentionally dated ledgers/reports can declare content_mode: chronological-ledger.`,
548
+ ),
549
+ );
550
+ }
551
+ }
552
+
511
553
  return {
512
554
  kind: 'architecture',
513
555
  extracted: {
@@ -515,6 +557,7 @@ function parseArchitectureDocument(options) {
515
557
  feature: typeof data.feature === 'string' ? data.feature : null,
516
558
  component: typeof data.component === 'string' ? data.component : null,
517
559
  status: typeof data.status === 'string' ? data.status : null,
560
+ content_mode: contentMode,
518
561
  repo_ids: repoIds,
519
562
  },
520
563
  warnings,
@@ -622,11 +665,34 @@ function parseSessionDocument(options) {
622
665
  const h1Match = options.content.match(/^#\s+Session(?:\s+—\s+(.+))?$/m);
623
666
 
624
667
  if (!filenameMatch && !h1Match) {
668
+ const recoveryCheckpoint = parseRecoveryCheckpointIdentity(options);
669
+ if (recoveryCheckpoint.candidate) {
670
+ if (recoveryCheckpoint.error) {
671
+ errors.push(recoveryCheckpoint.error);
672
+ return { kind: 'session', extracted: null, warnings, errors };
673
+ }
674
+
675
+ warnings.push(
676
+ createWarning(
677
+ 'session-filename-mismatch',
678
+ 'Recovery checkpoint uses the supported legacy sessions/YYYY-MM-DD.md filename instead of the recommended YYYY-MM-DD-N-display-title.md pattern.',
679
+ ),
680
+ );
681
+ appendMissingSessionSectionWarnings(options.content, warnings);
682
+
683
+ return {
684
+ kind: 'session',
685
+ extracted: recoveryCheckpoint.extracted,
686
+ warnings,
687
+ errors,
688
+ };
689
+ }
690
+
625
691
  errors.push(
626
692
  createError(
627
693
  options.relativePath,
628
694
  'session-unrecognized',
629
- 'Session note must use the session filename pattern or a "# Session" heading.',
695
+ 'Session note must use the session filename pattern, a "# Session" heading, or the strict legacy recovery-checkpoint form.',
630
696
  ),
631
697
  );
632
698
  return { kind: 'session', extracted: null, warnings, errors };
@@ -641,14 +707,7 @@ function parseSessionDocument(options) {
641
707
  );
642
708
  }
643
709
 
644
- const sections = extractLevelTwoSections(options.content);
645
- for (const section of RECOMMENDED_SESSION_SECTIONS) {
646
- if (!sections.has(section.toLowerCase())) {
647
- warnings.push(
648
- createWarning('session-missing-section', `Session note is missing recommended section "## ${section}".`),
649
- );
650
- }
651
- }
710
+ appendMissingSessionSectionWarnings(options.content, warnings);
652
711
 
653
712
  const title = extractSessionTitle(options.content, filenameMatch);
654
713
 
@@ -664,6 +723,98 @@ function parseSessionDocument(options) {
664
723
  };
665
724
  }
666
725
 
726
+ function parseRecoveryCheckpointIdentity(options) {
727
+ const filenameMatch = options.relativePath.match(/^sessions\/(\d{4}-\d{2}-\d{2})\.md$/);
728
+ const firstLine = options.content.split(/\r?\n/, 1)[0];
729
+ const headingCandidate = /^# Recovery checkpoint(?:\s|$)/.test(firstLine);
730
+ const candidate = Boolean(filenameMatch) || headingCandidate;
731
+
732
+ if (!candidate) {
733
+ return { candidate: false, extracted: null, error: null };
734
+ }
735
+
736
+ if (!filenameMatch) {
737
+ return {
738
+ candidate: true,
739
+ extracted: null,
740
+ error: createError(
741
+ options.relativePath,
742
+ 'session-recovery-checkpoint-path-invalid',
743
+ 'Legacy recovery checkpoints must be root session files named sessions/YYYY-MM-DD.md.',
744
+ ),
745
+ };
746
+ }
747
+
748
+ const headingMatch = firstLine.match(
749
+ /^# Recovery checkpoint — (\d{4}-\d{2}-\d{2}) — (\S(?:.*\S)?)$/,
750
+ );
751
+ if (!headingMatch) {
752
+ return {
753
+ candidate: true,
754
+ extracted: null,
755
+ error: createError(
756
+ options.relativePath,
757
+ 'session-recovery-checkpoint-heading-invalid',
758
+ 'Legacy recovery checkpoints require "# Recovery checkpoint — YYYY-MM-DD — <nonempty title>".',
759
+ ),
760
+ };
761
+ }
762
+
763
+ const filenameDate = filenameMatch[1];
764
+ const headingDate = headingMatch[1];
765
+ if (!isValidIsoCalendarDate(filenameDate) || !isValidIsoCalendarDate(headingDate)) {
766
+ return {
767
+ candidate: true,
768
+ extracted: null,
769
+ error: createError(
770
+ options.relativePath,
771
+ 'session-recovery-checkpoint-date-invalid',
772
+ 'Legacy recovery checkpoint dates must be valid ISO calendar dates.',
773
+ ),
774
+ };
775
+ }
776
+
777
+ if (filenameDate !== headingDate) {
778
+ return {
779
+ candidate: true,
780
+ extracted: null,
781
+ error: createError(
782
+ options.relativePath,
783
+ 'session-recovery-checkpoint-date-mismatch',
784
+ `Legacy recovery checkpoint filename date ${filenameDate} does not match heading date ${headingDate}.`,
785
+ ),
786
+ };
787
+ }
788
+
789
+ return {
790
+ candidate: true,
791
+ extracted: {
792
+ title: headingMatch[2].trim(),
793
+ session_date: filenameDate,
794
+ // A checkpoint records continuity inside an open lane; assigning a
795
+ // number would invent a close-session chronology that never occurred.
796
+ session_number: null,
797
+ },
798
+ error: null,
799
+ };
800
+ }
801
+
802
+ function appendMissingSessionSectionWarnings(content, warnings) {
803
+ const sections = extractLevelTwoSections(content);
804
+ for (const section of RECOMMENDED_SESSION_SECTIONS) {
805
+ if (!sections.has(section.toLowerCase())) {
806
+ warnings.push(
807
+ createWarning('session-missing-section', `Session note is missing recommended section "## ${section}".`),
808
+ );
809
+ }
810
+ }
811
+ }
812
+
813
+ function isValidIsoCalendarDate(value) {
814
+ const parsed = new Date(`${value}T00:00:00.000Z`);
815
+ return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
816
+ }
817
+
667
818
  function extractDecisionEntries(content) {
668
819
  const matches = [...content.matchAll(/^###\s+D-(\d{3,})\b.*$/gm)];
669
820
 
@@ -717,6 +868,385 @@ function extractLevelTwoSections(content) {
717
868
  return sections;
718
869
  }
719
870
 
871
+ // D-292 changelog-shape detector, shared by the scanner and the docs-review
872
+ // apply validator. Signals are deliberately conjunctive and line-local so
873
+ // legitimate content never trips it: bare dates, Track/Phase plan headings,
874
+ // lane/session vocabulary, completion words in prose, and strikethrough alone
875
+ // must not trigger. Frontmatter is expected to be stripped by the caller;
876
+ // fenced code, inline code, and HTML comments are masked here because dates
877
+ // inside artifact names (migration filenames, example blocks) are legitimate.
878
+ export function detectChangelogShapedLines(body) {
879
+ // Block-aware masking approximating CommonMark, deliberately short of a
880
+ // full tokenizer. Modeled exactly: fenced code (character + run length,
881
+ // whitespace-only closers within 3 columns relative to the containing
882
+ // block), 4+ column indented code with tab-stop arithmetic (never
883
+ // interrupting an open paragraph), block-level HTML comments, ATX headings
884
+ // interrupting paragraphs, single-level list containers
885
+ // (marker-width/padding-derived content indent, lazy item-paragraph
886
+ // continuation until a blank or block interrupter, container exit with
887
+ // line reprocessing, list-contained fences and comments, and the
888
+ // paragraph-interruption rule: only non-empty bullets or ordered markers
889
+ // with start value 1 interrupt), and source-ordered escape-aware inline
890
+ // masking of code spans and comments. Known approximations, guarded by the
891
+ // checked-in nine-doc calibration fixture matrix plus the separately rerun
892
+ // three-root live calibration: nested containers, setext headings,
893
+ // thematic-break paragraph boundaries, and lazy blockquote continuation.
894
+ const hits = [];
895
+ // fence/htmlComment carry `container` when opened inside a list item;
896
+ // `container` alone means an active list item with no open child block.
897
+ // `container.paragraphOpen` tracks whether the item has an open paragraph
898
+ // eligible for lazy continuation by under-indented lines.
899
+ let fence = null;
900
+ let htmlComment = null;
901
+ let container = null;
902
+ let paragraph = [];
903
+
904
+ const evaluateLeadIn = (maskedLine, rawLine) => {
905
+ const trimmed = maskedLine.trim();
906
+ if (trimmed === '') {
907
+ return;
908
+ }
909
+ // Blockquoted lines are quoted examples, not the doc's own lead-ins.
910
+ const isListItem = /^([-*+]|\d+[.)])\s/.test(trimmed);
911
+ const isBlockquote = trimmed.startsWith('>');
912
+ if (
913
+ !isListItem &&
914
+ !isBlockquote &&
915
+ trimmed.endsWith(':') &&
916
+ trimmed.length <= 160 &&
917
+ ISO_DATE_PATTERN.test(trimmed) &&
918
+ /\bupdated\b/i.test(trimmed)
919
+ ) {
920
+ hits.push(rawLine.trim());
921
+ }
922
+ };
923
+
924
+ const flushParagraph = () => {
925
+ if (paragraph.length === 0) {
926
+ return;
927
+ }
928
+ const maskedLines = maskInlineConstructs(paragraph.map((entry) => entry.raw).join('\n')).split('\n');
929
+ for (let index = 0; index < paragraph.length; index += 1) {
930
+ evaluateLeadIn(maskedLines[index], paragraph[index].raw);
931
+ }
932
+ paragraph = [];
933
+ };
934
+
935
+ const openContainerBlock = (rest) => {
936
+ const opener = /^(`{3,}|~{3,})(.*)$/.exec(rest);
937
+ if (opener && !(opener[1][0] === '`' && opener[2].includes('`'))) {
938
+ fence = { char: opener[1][0], length: opener[1].length, container };
939
+ return true;
940
+ }
941
+ if (rest.startsWith('<!--') && !rest.slice(4).includes('-->')) {
942
+ htmlComment = { container };
943
+ return true;
944
+ }
945
+ return false;
946
+ };
947
+
948
+ // Lines at valid root position (columns 0-3) that start a new block and
949
+ // therefore end lazy paragraph continuation. Any valid list marker —
950
+ // empty or not, of any type — closes the current item here: marker type
951
+ // only decides whether the reprocessed line joins the old list or starts
952
+ // a new one. The empty/start-number paragraph-interruption limits apply
953
+ // only where a document-level paragraph itself is open.
954
+ const interruptsLazyContinuation = (content) => {
955
+ if (ATX_HEADING_START.test(content) || content.startsWith('>') || content.startsWith('<!--')) {
956
+ return true;
957
+ }
958
+ const opener = /^(`{3,}|~{3,})(.*)$/.exec(content);
959
+ if (opener && !(opener[1][0] === '`' && opener[2].includes('`'))) {
960
+ return true;
961
+ }
962
+ return /^([-*+]|\d{1,9}[.)])([ \t]+\S|[ \t]*$)/.test(content);
963
+ };
964
+
965
+ // Whether a container content line is item PARAGRAPH text (eligible for
966
+ // lazy continuation) as opposed to a non-persistent child block: an ATX
967
+ // heading (with content or bare), blockquote, complete one-line HTML
968
+ // comment, or nested list marker occupies the line without leaving a
969
+ // paragraph open.
970
+ const startsContainerParagraph = (rest) => {
971
+ if (ATX_HEADING_START.test(rest) || rest.startsWith('>') || rest.startsWith('<!--')) {
972
+ return false;
973
+ }
974
+ return !/^([-*+]|\d{1,9}[.)])([ \t]+\S|[ \t]*$)/.test(rest);
975
+ };
976
+
977
+ // Normalize CRLF and lone CR at ingress: the exported helper's contract is
978
+ // "frontmatter-stripped body text", and end-anchored block predicates
979
+ // (bare ATX, fence closers, bare markers) break on trailing \r.
980
+ const lines = (body ?? '').split(/\r\n?|\n/);
981
+ let index = 0;
982
+ while (index < lines.length) {
983
+ const rawLine = lines[index];
984
+ const blank = rawLine.trim() === '';
985
+ const indent = expandedIndentColumns(rawLine);
986
+
987
+ if (fence) {
988
+ if (blank) {
989
+ index += 1;
990
+ continue;
991
+ }
992
+ if (fence.container && indent < fence.container.contentIndent) {
993
+ // Container exit ends the child fence with its enclosing item; the
994
+ // exiting line is reprocessed at document level.
995
+ fence = null;
996
+ container = null;
997
+ continue;
998
+ }
999
+ const baseIndent = fence.container ? fence.container.contentIndent : 0;
1000
+ const closer = /^[ \t]*(`{3,}|~{3,})[ \t]*$/.exec(rawLine);
1001
+ if (
1002
+ closer &&
1003
+ closer[1][0] === fence.char &&
1004
+ closer[1].length >= fence.length &&
1005
+ indent - baseIndent <= 3
1006
+ ) {
1007
+ fence = null;
1008
+ }
1009
+ index += 1;
1010
+ continue;
1011
+ }
1012
+
1013
+ if (htmlComment) {
1014
+ if (!blank && htmlComment.container && indent < htmlComment.container.contentIndent) {
1015
+ htmlComment = null;
1016
+ container = null;
1017
+ continue;
1018
+ }
1019
+ if (rawLine.includes('-->')) {
1020
+ htmlComment = null;
1021
+ }
1022
+ index += 1;
1023
+ continue;
1024
+ }
1025
+
1026
+ if (container) {
1027
+ if (blank) {
1028
+ container.paragraphOpen = false;
1029
+ index += 1;
1030
+ continue;
1031
+ }
1032
+ if (indent < container.contentIndent) {
1033
+ // A lazy continuation of the item's open paragraph stays inside the
1034
+ // item (item text is non-signal); a valid root block start (columns
1035
+ // 0-3 only — at 4+ columns nothing can start a root block, and
1036
+ // indented code cannot interrupt the open paragraph) exits the
1037
+ // container and is reprocessed at document level.
1038
+ if (
1039
+ container.paragraphOpen &&
1040
+ (indent >= 4 || !interruptsLazyContinuation(rawLine.trimStart()))
1041
+ ) {
1042
+ index += 1;
1043
+ continue;
1044
+ }
1045
+ container = null;
1046
+ continue;
1047
+ }
1048
+ // Container content: only child fences/comments matter for masking;
1049
+ // item text (including nested lists, an out-of-scope approximation)
1050
+ // never produces signals itself. 4+ relative columns is indented code
1051
+ // within the item, not a fence opener. Non-persistent child blocks
1052
+ // (headings, one-line comments, blockquotes, nested markers) close the
1053
+ // item paragraph rather than opening one.
1054
+ if (indent - container.contentIndent <= 3) {
1055
+ const rest = rawLine.trimStart();
1056
+ container.paragraphOpen = openContainerBlock(rest) ? false : startsContainerParagraph(rest);
1057
+ }
1058
+ index += 1;
1059
+ continue;
1060
+ }
1061
+
1062
+ const content = rawLine.trimStart();
1063
+
1064
+ if (blank) {
1065
+ flushParagraph();
1066
+ index += 1;
1067
+ continue;
1068
+ }
1069
+ if (indent >= 4) {
1070
+ // Indented code cannot interrupt an open paragraph (CommonMark): an
1071
+ // indented line after prose is paragraph continuation text.
1072
+ if (paragraph.length > 0) {
1073
+ paragraph.push({ raw: rawLine });
1074
+ }
1075
+ index += 1;
1076
+ continue;
1077
+ }
1078
+
1079
+ const listMarker = /^([-*+]|\d{1,9}[.)])(?:([ \t]+)(.*)|[ \t]*)$/.exec(content);
1080
+ if (listMarker) {
1081
+ const rest = listMarker[3] ?? '';
1082
+ const orderedDigits = /^(\d{1,9})[.)]$/.exec(listMarker[1]);
1083
+ // Interrupting an open paragraph requires a non-empty item, and for
1084
+ // ordered lists a start VALUE of 1 (so `01.` may interrupt while `2.`
1085
+ // may not).
1086
+ const canInterrupt =
1087
+ rest.trim() !== '' && (!orderedDigits || Number.parseInt(orderedDigits[1], 10) === 1);
1088
+ if (paragraph.length > 0 && !canInterrupt) {
1089
+ paragraph.push({ raw: rawLine });
1090
+ index += 1;
1091
+ continue;
1092
+ }
1093
+ flushParagraph();
1094
+ const markerWidth = listMarker[1].length;
1095
+ let paddingColumns = 0;
1096
+ if (listMarker[2]) {
1097
+ let column = indent + markerWidth;
1098
+ for (const character of listMarker[2]) {
1099
+ column += character === '\t' ? 4 - (column % 4) : 1;
1100
+ }
1101
+ paddingColumns = column - indent - markerWidth;
1102
+ }
1103
+ const contentIndent =
1104
+ rest === '' || paddingColumns === 0 || paddingColumns > 4
1105
+ ? indent + markerWidth + 1
1106
+ : indent + markerWidth + paddingColumns;
1107
+ container = { contentIndent, paragraphOpen: false };
1108
+ if (rest !== '') {
1109
+ // Residual logical indentation of the marker line's own content: with
1110
+ // more than four padding columns, one column is list padding and the
1111
+ // remainder is a child INDENTED CODE block (CommonMark), so no item
1112
+ // paragraph opens and the text is never a fence/comment/heading.
1113
+ const restRelativeIndent = indent + markerWidth + paddingColumns - contentIndent;
1114
+ if (restRelativeIndent >= 4) {
1115
+ container.paragraphOpen = false;
1116
+ } else {
1117
+ container.paragraphOpen = openContainerBlock(rest) ? false : startsContainerParagraph(rest);
1118
+ }
1119
+ }
1120
+ index += 1;
1121
+ continue;
1122
+ }
1123
+
1124
+ const opener = /^(`{3,}|~{3,})(.*)$/.exec(content);
1125
+ if (opener && !(opener[1][0] === '`' && opener[2].includes('`'))) {
1126
+ flushParagraph();
1127
+ fence = { char: opener[1][0], length: opener[1].length, container: null };
1128
+ index += 1;
1129
+ continue;
1130
+ }
1131
+
1132
+ if (content.startsWith('<!--')) {
1133
+ flushParagraph();
1134
+ if (!content.slice(4).includes('-->')) {
1135
+ htmlComment = { container: null };
1136
+ }
1137
+ index += 1;
1138
+ continue;
1139
+ }
1140
+
1141
+ if (ATX_HEADING_START.test(content)) {
1142
+ flushParagraph();
1143
+ if (/^#{2,3}\s/.test(content)) {
1144
+ const masked = maskInlineConstructs(content);
1145
+ if (ISO_DATE_PATTERN.test(masked) && HEADING_CHRONOLOGY_CUES.test(masked)) {
1146
+ hits.push(rawLine.trim());
1147
+ }
1148
+ }
1149
+ index += 1;
1150
+ continue;
1151
+ }
1152
+
1153
+ paragraph.push({ raw: rawLine });
1154
+ index += 1;
1155
+ }
1156
+ flushParagraph();
1157
+
1158
+ return hits;
1159
+ }
1160
+
1161
+ // Source-ordered inline masking: scans left to right so the first valid
1162
+ // construct wins (a comment containing a backtick masks before that backtick
1163
+ // can pair with later code), backslash-escaped backticks are not code
1164
+ // delimiters, an unmatched backtick run or unterminated `<!--` is literal
1165
+ // text, and closer runs inside an open code span count regardless of
1166
+ // backslashes (escapes are literal inside code). Masked spans preserve
1167
+ // newlines so paragraph line mapping stays exact.
1168
+ function maskInlineConstructs(text) {
1169
+ let result = '';
1170
+ let position = 0;
1171
+
1172
+ while (position < text.length) {
1173
+ const character = text[position];
1174
+
1175
+ if (character === '\\' && position + 1 < text.length) {
1176
+ result += text.slice(position, position + 2);
1177
+ position += 2;
1178
+ continue;
1179
+ }
1180
+
1181
+ if (character === '`') {
1182
+ let runEnd = position;
1183
+ while (runEnd < text.length && text[runEnd] === '`') {
1184
+ runEnd += 1;
1185
+ }
1186
+ const runLength = runEnd - position;
1187
+ let closerStart = -1;
1188
+ let scan = runEnd;
1189
+ while (scan < text.length) {
1190
+ if (text[scan] === '`') {
1191
+ let scanEnd = scan;
1192
+ while (scanEnd < text.length && text[scanEnd] === '`') {
1193
+ scanEnd += 1;
1194
+ }
1195
+ if (scanEnd - scan === runLength) {
1196
+ closerStart = scan;
1197
+ break;
1198
+ }
1199
+ scan = scanEnd;
1200
+ } else {
1201
+ scan += 1;
1202
+ }
1203
+ }
1204
+ if (closerStart !== -1) {
1205
+ const spanEnd = closerStart + runLength;
1206
+ result += text.slice(position, spanEnd).replace(/[^\n]/g, ' ');
1207
+ position = spanEnd;
1208
+ } else {
1209
+ result += text.slice(position, runEnd);
1210
+ position = runEnd;
1211
+ }
1212
+ continue;
1213
+ }
1214
+
1215
+ if (text.startsWith('<!--', position)) {
1216
+ const commentEnd = text.indexOf('-->', position + 4);
1217
+ if (commentEnd !== -1) {
1218
+ const spanEnd = commentEnd + 3;
1219
+ result += text.slice(position, spanEnd).replace(/[^\n]/g, ' ');
1220
+ position = spanEnd;
1221
+ } else {
1222
+ result += '<!--';
1223
+ position += 4;
1224
+ }
1225
+ continue;
1226
+ }
1227
+
1228
+ result += character;
1229
+ position += 1;
1230
+ }
1231
+
1232
+ return result;
1233
+ }
1234
+
1235
+ // CommonMark column arithmetic: a tab advances to the next 4-column stop.
1236
+ function expandedIndentColumns(line) {
1237
+ let columns = 0;
1238
+ for (const character of line) {
1239
+ if (character === ' ') {
1240
+ columns += 1;
1241
+ } else if (character === '\t') {
1242
+ columns += 4 - (columns % 4);
1243
+ } else {
1244
+ break;
1245
+ }
1246
+ }
1247
+ return columns;
1248
+ }
1249
+
720
1250
  function extractSessionTitle(content, filenameMatch) {
721
1251
  const h1WithNumber = content.match(/^#\s+Session\s+—\s+\d{4}-\d{2}-\d{2}-\d+\s+—\s+(.+)$/m);
722
1252
  if (h1WithNumber) {
package/src/scaffold.js CHANGED
@@ -215,7 +215,7 @@ This workspace uses VibeCompass project memory rooted at \`${rootRelativePath}\`
215
215
 
216
216
  ## Canonical files
217
217
  - \`${rootRelativePath}/project.yaml\` — machine-oriented project metadata
218
- - \`${rootRelativePath}/architecture/\` — canonical architecture docs
218
+ - \`${rootRelativePath}/architecture/\` — canonical architecture docs; the mutable current-state layer, rewritten in place as contracts change (D-292)
219
219
  - \`${rootRelativePath}/decisions/\` — append-only decision log for this project-memory root
220
220
  - \`${rootRelativePath}/sessions/\` — finalized session notes
221
221
 
@@ -240,6 +240,7 @@ This workspace uses VibeCompass project memory rooted at \`${rootRelativePath}\`
240
240
  - Every lane gets a per-lane runtime assignment at start (D-282): a lane port and temp dir recorded in \`session.yaml\` and exported with \`eval "$(vibecompass lane-env)"\` (includes conventional \`PORT\`/\`TMPDIR\` aliases), so parallel lanes never fight over dev-server ports or temp paths. Defaults are configurable under \`project.yaml\` \`runtime:\`; \`close-session\` removes the lane temp dir under guards.
241
241
  - Finalized sessions are append-only notes named \`${rootRelativePath}/sessions/YYYY-MM-DD-N-title.md\`; multiple sessions on the same day increment \`N\`.
242
242
  - Decisions remain append-only and independent from session notes. A session note may reference decisions, but the decision entry in \`${rootRelativePath}/decisions/\` is the durable decision record for this root.
243
+ - Architecture docs are the opposite of the append-only surfaces (D-292): they are current-state contracts, rewritten in place. Fold changes into the existing sections and keep the durable plan, unresolved next steps, and material rollout state in the doc. Work/ship chronology belongs in finalized session notes; decision entries record accepted choices and rationale; lane scratch holds only transient execution/review state, and anything still pending at close moves to the session note's next steps and, when durable, into architecture (D-293). Intentionally dated ledger/report docs may declare frontmatter \`content_mode: chronological-ledger\`.
243
244
 
244
245
  ## Repos in scope
245
246
  ${repos}
@@ -320,7 +321,7 @@ During the session:
320
321
  - keep \`handoff.md\` current after substantive work blocks
321
322
  - run \`eval "$(vibecompass lane-env)"\` in a lane shell before starting dev servers or build tools so the lane's assigned port and temp dir are used (D-282); do not hardcode ports in lane work
322
323
  - run \`vibecompass docs-update --session <lane-id>\` whenever you need an ad hoc targeted documentation-maintenance plan for the current session delta
323
- - after substantive feature work, confirm affected architecture docs and decisions still match the implementation; if not, update them while the context is fresh
324
+ - after substantive feature work, confirm affected architecture docs and decisions still match the implementation; if not, update them while the context is fresh — fold the changes into the doc's current-state sections (rewrite in place; no dated "update" sections, lane names in headings, or completed-task chronology; D-292)
324
325
  - use \`vibecompass list-sessions\` and \`vibecompass switch-session <lane-id>\` to inspect or change the current lane
325
326
  - use \`address review\` when reviewer feedback lands so the builder resolves it from the selected lane's latest \`wip.md\` / \`handoff.md\`
326
327
  - 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
@@ -338,12 +339,12 @@ If \`vibecompass start-session\` reports stale scratch files, read the existing
338
339
  At session close:
339
340
  - prefer running \`vibecompass close-session --session <lane-id> --title "..." --completed "..." --architecture-docs updated|not-needed|deferred --decision-log updated|not-needed|deferred --session-maintenance updated|not-needed|deferred --next-step "..."\`; \`vibecompass end-session\` is a supported alias
340
341
  - close-session prints the same targeted docs-update plan — including the pre-close staleness set (new decisions since lane start, stale base revisions, newer finalized notes touching this lane's scope, claim overlap with other active lanes) — before the document-maintenance checkpoint; use it to decide whether affected \`architecture/\`, \`decisions/\`, and active-session scratch/final note inputs are updated, not-needed, or deferred
341
- - document-maintenance checkpoint statuses are required before close-session writes the finalized note; the package validates the status values, while the closer owns semantic doc authorship
342
+ - document-maintenance checkpoint statuses are required before close-session writes the finalized note; the package validates the status values, while the closer owns semantic doc authorship — architecture docs keep current behavior plus the durable plan, unresolved next steps, and material rollout state; session notes keep work chronology and close-out next steps; decisions keep accepted choices and rationale (D-292, D-293)
342
343
  - follow the stored close-session defaults from \`${rootRelativePath}/project.yaml\`
343
344
  - finalize the lane-local \`wip.md\` into \`${rootRelativePath}/sessions/YYYY-MM-DD-N-title.md\`; the permanent note distills decisions, completions, blockers, and next steps rather than preserving the full \`## Review log\`
344
345
  - if a granular reviewer trail must remain durable, summarize it explicitly in the session note inputs or create a separate finalized session note before close-session deletes lane scratch files
345
346
  - close-session deletes the closed lane directory under \`${rootRelativePath}/sessions/active/<lane-id>/\` and cleans up provisioned worktrees, the lane marker, the container, and the lane temp dir when they are safely removable; leftover pieces come with printed guidance
346
- - refresh any affected architecture/decision docs
347
+ - refresh any affected architecture/decision docs by folding changes into their current-state contract prose — do not append session chronology; distill anything still pending from lane scratch into the session note's next steps and, when durable, into the docs (D-292, D-293)
347
348
  - for local-primary roots with hosted sync configured, run \`vibecompass push --root ${rootRelativePath}\` after canonical docs/session files are finalized when the hosted dashboard should reflect the session; pass \`--sync-target <name>\` when using a non-default or named target
348
349
  - for hosted-only projects, there is no local authoritative push; confirm hosted dashboard/proposal/Understanding state was updated or record the refresh/apply work as deferred
349
350
 
@@ -523,6 +524,7 @@ architecture/<domain-slug>/<feature-slug>/<component>.md
523
524
  - Retrieval guidance should say when to consult the doc, what it does not cover, and which related docs or decisions matter.
524
525
  - Details should distinguish confirmed behavior from known gaps or open follow-up.
525
526
  - Involved files should use concrete \`repo:path\` references that match the affected implementation surface.
527
+ - Docs are mutable current-state contracts (D-292): fold updates into the existing sections by rewriting them in place, keeping the durable plan, unresolved next steps, and material rollout state in the doc. Dated session headings, lane names, and completed-task chronology belong in session notes; decision entries record accepted choices and rationale (D-293). Docs whose artifact itself is dated (migration ledgers, incident/upstream reports) may declare frontmatter \`content_mode: chronological-ledger\` to suppress the changelog-shape advisory.
526
528
 
527
529
  Each component doc is canonical. This README is only a convenience guide.
528
530
  `;
@@ -635,7 +637,7 @@ function renderWorkflowDefaults(workflow) {
635
637
  const lines = [
636
638
  `- reviewer handback: ${describeReviewerHandback(workflow)}`,
637
639
  '- close-session: require document-maintenance checkpoint statuses for architecture docs, decision log, and session handoff/scratch',
638
- '- close-session: refresh any relevant architecture docs before finalizing the session',
640
+ '- close-session: refresh any relevant architecture docs before finalizing the session, folding changes into their current-state contract prose (D-292)',
639
641
  '- close-session: refresh any relevant decision files before finalizing the session',
640
642
  ];
641
643
 
package/src/workflow.js CHANGED
@@ -83,6 +83,9 @@ export function buildCloseSessionGuidance(workflowSettings, options = {}) {
83
83
 
84
84
  guidance.push('Reviewer handback stays in the selected sessions/active/<lane-id>/wip.md and handoff.md before close-session runs.');
85
85
  guidance.push('Close-session requires document-maintenance checkpoint statuses for architecture docs, decision log, and session handoff/scratch: updated, not-needed, or deferred.');
86
+ // D-292/D-293: unconditional — the authorship rule holds even when the
87
+ // refresh-architecture-docs workflow step is disabled.
88
+ guidance.push('Architecture docs keep current behavior plus the durable plan, unresolved next steps, and material rollout state — fold changes in place, never dated session sections; session notes keep work chronology and close-out next steps; decisions keep accepted choices and rationale; transient lane scratch drains into the session note at close (D-292, D-293).');
86
89
 
87
90
  if (workflowSettings.closeSession.refreshArchitectureDocs) {
88
91
  guidance.push('Refresh any relevant architecture docs before finalizing the session.');
@@ -122,7 +122,7 @@ The first pass is breadth-first: establish an honest project denominator, compac
122
122
  3. Run \`vibecompass docs-review --guided --llm <tool-name> --model <model-name>\` to record the review marker and print the canonical docs-review contract. Use the current AI tool name and model when known.
123
123
  4. Present the evidence inventory, completeness inventory with accepted/deferred/missing accounting, topology, chosen taxonomy axis, prior-doc reuse/update/split/merge/defer/replace classifications, coverage plan, backbone outputs (core feature inventory, user journey map, project systems map, coverage/quality summary), proposed architecture docs, blindspots, token-budget risks, quality report, and any decision candidates before generating accepted output. At this gate, ask whether to apply the compact baseline now, adjust the plan, or spend a follow-up pass deepening specific high-risk areas. If you stop here for user approval, state clearly that no architecture docs have been applied yet and do not label proposed areas as accepted.
124
124
  5. Draft only output the user accepts. After approval, the accepted plan must be fenced as \`vibecompass-coverage-plan version=1\` and should preserve each area's \`run_scope\` as \`baseline\` or \`deepening\`. Accepted architecture docs must be fenced as \`vibecompass-architecture-doc path=architecture/domain/feature/component.md\` blocks and must include required frontmatter: \`domain\`, \`feature\`, \`component\`, and \`status\`. Feature docs may include compact prose-only surface matrices; surface/platform ownership docs stay under the existing \`architecture/platform/<surface>/<component>.md\` shape rather than a new top-level systems tree. The accepted \`architecture/overview/project-shape.md\` doc should include compact backbone sections and exactly one \`vibecompass-project-map version=1\` JSON block with journey-facing features, entry points, evidence-backed relationships, and minimal optional derived \`systems[]\` metadata. Accepted local decisions may be fenced as \`vibecompass-decision-recommendation target=decisions/domain.md\` blocks without D-numbers.
125
- 6. Keep each generated architecture doc focused and retrieval-oriented. Soft budget: under 12000 bytes per doc unless the extra detail is necessary and called out in review metadata or retrieval guidance.
125
+ 6. Keep each generated architecture doc focused and retrieval-oriented. Soft budget: under 12000 bytes per doc unless the extra detail is necessary and called out in review metadata or retrieval guidance. Write every doc as a current-state contract (D-292): updates rewrite the affected sections in place or create a new focused doc — never append dated session/audit sections. Keep the durable plan and rollout state in the doc; work/ship chronology belongs in session notes, and decision entries record accepted choices (D-293).
126
126
  7. Write the accepted fenced blocks verbatim to \`${outputPath}\`.
127
127
  8. Run \`${applyCommand}\` to validate and apply the accepted blocks. If \`vibecompass\` is not installed on PATH, run \`${npxApplyCommand}\` instead. Only report docs-review as applied after this command succeeds. If accepted docs were written manually outside the output file, run \`vibecompass docs-review --root ${rootRelativePath} --complete\` after confirming the marker should be completed.
128
128
  9. Run \`vibecompass status\` to confirm the docs-review marker is complete and not drifted. Report the applied architecture paths, coverage projection, baseline/deepening split, decision recommendations, parser warnings such as oversized docs or missing review metadata, and any blindspots or deepening candidates documented in the accepted docs-review output.