@vibecompass/vibecompass 0.13.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,17 @@
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
+
5
16
  ## 0.13.0 - 2026-07-11
6
17
 
7
18
  - Architecture docs are now explicitly current-state contracts (D-292/D-293):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecompass/vibecompass",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Local-first project-memory core for VibeCompass",
5
5
  "type": "module",
6
6
  "bin": {
@@ -665,11 +665,34 @@ function parseSessionDocument(options) {
665
665
  const h1Match = options.content.match(/^#\s+Session(?:\s+—\s+(.+))?$/m);
666
666
 
667
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
+
668
691
  errors.push(
669
692
  createError(
670
693
  options.relativePath,
671
694
  'session-unrecognized',
672
- '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.',
673
696
  ),
674
697
  );
675
698
  return { kind: 'session', extracted: null, warnings, errors };
@@ -684,14 +707,7 @@ function parseSessionDocument(options) {
684
707
  );
685
708
  }
686
709
 
687
- const sections = extractLevelTwoSections(options.content);
688
- for (const section of RECOMMENDED_SESSION_SECTIONS) {
689
- if (!sections.has(section.toLowerCase())) {
690
- warnings.push(
691
- createWarning('session-missing-section', `Session note is missing recommended section "## ${section}".`),
692
- );
693
- }
694
- }
710
+ appendMissingSessionSectionWarnings(options.content, warnings);
695
711
 
696
712
  const title = extractSessionTitle(options.content, filenameMatch);
697
713
 
@@ -707,6 +723,98 @@ function parseSessionDocument(options) {
707
723
  };
708
724
  }
709
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
+
710
818
  function extractDecisionEntries(content) {
711
819
  const matches = [...content.matchAll(/^###\s+D-(\d{3,})\b.*$/gm)];
712
820