peaks-loop 4.0.51 → 4.0.52

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/cli/commands/baseline-commands.js +11 -1
  5. package/dist/cli/commands/codegraph-command-runtime.d.ts +28 -0
  6. package/dist/cli/commands/codegraph-command-runtime.js +72 -0
  7. package/dist/cli/commands/codegraph-commands.d.ts +2 -11
  8. package/dist/cli/commands/codegraph-commands.js +173 -228
  9. package/dist/cli/commands/codegraph-status-command.d.ts +22 -0
  10. package/dist/cli/commands/codegraph-status-command.js +299 -0
  11. package/dist/cli/commands/core/memory-command.js +6 -2
  12. package/dist/cli/commands/job-commands.js +121 -30
  13. package/dist/cli/commands/project-commands.js +13 -3
  14. package/dist/cli/commands/request-commands.js +19 -8
  15. package/dist/cli/commands/slice-commands.js +2 -2
  16. package/dist/services/artifacts/artifact-prerequisites.js +23 -1
  17. package/dist/services/codegraph/codegraph-autorefresh.d.ts +16 -0
  18. package/dist/services/codegraph/codegraph-autorefresh.js +51 -5
  19. package/dist/services/codegraph/codegraph-config-repair-writer.d.ts +88 -0
  20. package/dist/services/codegraph/codegraph-config-repair-writer.js +322 -0
  21. package/dist/services/codegraph/codegraph-exclude-integrity.d.ts +20 -2
  22. package/dist/services/codegraph/codegraph-exclude-integrity.js +24 -3
  23. package/dist/services/codegraph/codegraph-exclude-reconciler.d.ts +23 -2
  24. package/dist/services/codegraph/codegraph-exclude-reconciler.js +123 -12
  25. package/dist/services/codegraph/codegraph-exclude-repair.d.ts +109 -55
  26. package/dist/services/codegraph/codegraph-exclude-repair.js +249 -195
  27. package/dist/services/codegraph/codegraph-include-reconciler.d.ts +10 -0
  28. package/dist/services/codegraph/codegraph-include-reconciler.js +160 -0
  29. package/dist/services/codegraph/codegraph-index-integrity.d.ts +268 -0
  30. package/dist/services/codegraph/codegraph-index-integrity.js +471 -0
  31. package/dist/services/codegraph/codegraph-service.d.ts +54 -0
  32. package/dist/services/codegraph/codegraph-service.js +84 -1
  33. package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.js +19 -4
  34. package/dist/services/doctor/doctor-service/checks/codegraph-index-integrity.d.ts +54 -0
  35. package/dist/services/doctor/doctor-service/checks/codegraph-index-integrity.js +151 -0
  36. package/dist/services/doctor/doctor-service/checks/l3-orphan-sessions.js +10 -10
  37. package/dist/services/doctor/doctor-service/plugin-registry.js +2 -0
  38. package/dist/services/doctor/doctor-service/types.d.ts +25 -0
  39. package/dist/services/memory/project-memory-service/index/kind-dispatch.js +48 -13
  40. package/dist/services/memory/project-memory-service/index.d.ts +5 -3
  41. package/dist/services/memory/project-memory-service/index.js +2 -2
  42. package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +15 -1
  43. package/dist/services/memory/project-memory-service/parsers/frontmatter.js +34 -6
  44. package/dist/services/memory/project-memory-service/parsers/markdown-pure.d.ts +27 -1
  45. package/dist/services/memory/project-memory-service/parsers/markdown-pure.js +92 -7
  46. package/dist/services/memory/project-memory-service/types.d.ts +86 -0
  47. package/dist/services/slice/slice-check-types.d.ts +1 -1
  48. package/dist/services/workspace/runtime-layout.d.ts +91 -0
  49. package/dist/services/workspace/runtime-layout.js +148 -0
  50. package/dist/services/workspace/workspace-claude-settings-materializer.js +14 -0
  51. package/package.json +6 -6
  52. package/scripts/clean-dist.mjs +15 -3
  53. package/scripts/sync-version.mjs +26 -4
  54. package/skills/bee/peaks-prd/SKILL.md +1 -1
  55. package/skills/bee/peaks-qa/references/qa-skill-presence.md +1 -1
  56. package/skills/bee/peaks-rd/references/skill-presence-and-title.md +1 -1
  57. package/skills/bee/peaks-sc/SKILL.md +1 -1
  58. package/skills/bee/peaks-txt/SKILL.md +3 -3
  59. package/skills/peaks-code/SKILL.md +1 -1
  60. package/skills/peaks-code/references/project-memory-loading.md +19 -1
  61. package/skills/peaks-code/references/step-11-memory-sediment.md +1 -1
@@ -30,8 +30,23 @@ import { inspectCodegraphExcludeIntegrity, isCodegraphExcludeConfigPresent } fro
30
30
  const CHECK_ID = 'capability:codegraph-exclude-integrity';
31
31
  /** How many rules / offending files the message names before eliding. */
32
32
  const MAX_NAMED = 5;
33
- function defaultProbe() {
34
- const projectRoot = process.cwd();
33
+ /**
34
+ * 2026-09-17 — `projectRoot` is the doctor's resolved L3 root, NOT
35
+ * `process.cwd()`. Same defect as the sibling `codegraph-index-integrity`
36
+ * probe (see its note): with `projectRootResolver` injected, every other
37
+ * check inspected the caller's root while this one inspected the operator's
38
+ * checkout, so its verdict depended on which repository you ran the doctor
39
+ * in. This one is read-only — it reads `.codegraph/config.json` and
40
+ * `git ls-files`, never the sqlite index — so unlike its sibling it did not
41
+ * also materialise `-shm`/`-wal` sidecars; the wrong-root defect was the
42
+ * same either way.
43
+ */
44
+ function defaultProbe(projectRoot) {
45
+ // An unresolved root is not a project — see the sibling probe's note: a
46
+ // relative lookup here would resolve against `cwd` and inspect the
47
+ // operator's checkout.
48
+ if (projectRoot.length === 0)
49
+ return null;
35
50
  // No config → codegraph was never initialized here, so no exclude
36
51
  // list is in play and there is nothing to report.
37
52
  return isCodegraphExcludeConfigPresent(projectRoot)
@@ -48,8 +63,8 @@ function renderGapMessage(excludedTrackedCount, trackedSourceCount, rulesToRemov
48
63
  const elidedFiles = violations.length > MAX_NAMED ? `; … (+${violations.length - MAX_NAMED})` : '';
49
64
  return `codegraph index is incomplete: ${excludedTrackedCount} of ${trackedSourceCount} tracked source files are blocked by ${rulesToRemove.length} exclude rule(s) [${namedRules}${elidedRules}]. Blocked: ${namedFiles}${elidedFiles}. Run \`peaks codegraph repair-exclude --project <root>\` to drop them and rebuild the index.`;
50
65
  }
51
- function run({ options }) {
52
- const probe = options.codegraphIntegrityProbe ?? defaultProbe;
66
+ function run({ options, resolvedL3Root }) {
67
+ const probe = options.codegraphIntegrityProbe ?? (() => defaultProbe(resolvedL3Root));
53
68
  let report;
54
69
  try {
55
70
  report = probe();
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Check: codegraph index integrity (`capability:codegraph-index-integrity`).
3
+ *
4
+ * The third codegraph check, and the one that stops `[OK]` from being a
5
+ * self-consistency assertion:
6
+ * - `capability:codegraph` answers "is upstream resolvable";
7
+ * - `capability:codegraph-exclude-integrity` answers "do the `exclude`
8
+ * rules drop tracked files";
9
+ * - this one answers "does the INDEX ITSELF cover the repository" —
10
+ * files the extractor supports that `include` never admitted, and rows
11
+ * the index still holds for paths that are gone.
12
+ *
13
+ * Why the exclude gate structurally cannot answer it: that gate runs the
14
+ * `include` filter FIRST and reconciles `exclude` against the survivors, so
15
+ * a file dropped by `include` is not "a tracked source file" as far as it is
16
+ * concerned; and it never reads the index at all, so dead rows are invisible
17
+ * to it. Both defects were present while `peaks codegraph status` printed
18
+ * `[OK] Index is up to date`.
19
+ *
20
+ * Read-only by construction — it consumes the same read-only inspector
21
+ * `peaks codegraph status` gates on. It never writes `.codegraph/config.json`
22
+ * and never invokes the upstream binary.
23
+ *
24
+ * Failure posture:
25
+ * - codegraph not initialized in the inspected root (no
26
+ * `.codegraph/codegraph.db`) → `ok: true`; there is no index to be
27
+ * incomplete or stale, and a fresh clone must not fail the doctor.
28
+ * - a confirmed gap on either axis → `ok: false`. ADVISORY by default
29
+ * (`severity: 'warning'`, the doctor exit code is left alone), and
30
+ * blocking (no severity tag) when the project opts in with
31
+ * `PEAKS_CODEGRAPH_INDEX_STRICT=1` — the user's option C decision.
32
+ * The finding is `ok: false` either way; only its severity moves.
33
+ * - could not evaluate (not a git work tree, missing/malformed config,
34
+ * unreadable index) → `ok: false, severity: 'warning'` so the doctor
35
+ * reports the blind spot without flipping the exit code on an
36
+ * unrelated failure. Kept non-blocking in BOTH modes: "could not
37
+ * evaluate" is not a finding about the project, and it is
38
+ * distinguishable from the `ok: true` "covers the repository"
39
+ * verdict by its `ok` value.
40
+ *
41
+ * R12-2 — the partial collapse that remains, stated rather than implied:
42
+ * this branch and the advisory `gap` branch below BOTH emit `ok: false`
43
+ * with `severity: 'warning'`, and `DoctorCheck` has no third field, so in
44
+ * the doctor envelope they are separated by message text alone. Not fixed,
45
+ * deliberately: the shape is legacy (`runDoctor` compatibility is a
46
+ * stated constraint), neither state is `ok: true` — so the invariant that
47
+ * a non-measurement may never read as "verified clean" holds — and in the
48
+ * mode where they are collapsed neither state moves the exit code.
49
+ * Under `PEAKS_CODEGRAPH_INDEX_STRICT=1` they ARE separated in the
50
+ * machine fields: the advisory `gap` branch drops its tag, this one keeps
51
+ * it. Pinned by the "(severity policy)" and "(unevaluable)" tests.
52
+ */
53
+ import type { DoctorCheckPlugin } from '../types.js';
54
+ export declare const check: DoctorCheckPlugin;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Check: codegraph index integrity (`capability:codegraph-index-integrity`).
3
+ *
4
+ * The third codegraph check, and the one that stops `[OK]` from being a
5
+ * self-consistency assertion:
6
+ * - `capability:codegraph` answers "is upstream resolvable";
7
+ * - `capability:codegraph-exclude-integrity` answers "do the `exclude`
8
+ * rules drop tracked files";
9
+ * - this one answers "does the INDEX ITSELF cover the repository" —
10
+ * files the extractor supports that `include` never admitted, and rows
11
+ * the index still holds for paths that are gone.
12
+ *
13
+ * Why the exclude gate structurally cannot answer it: that gate runs the
14
+ * `include` filter FIRST and reconciles `exclude` against the survivors, so
15
+ * a file dropped by `include` is not "a tracked source file" as far as it is
16
+ * concerned; and it never reads the index at all, so dead rows are invisible
17
+ * to it. Both defects were present while `peaks codegraph status` printed
18
+ * `[OK] Index is up to date`.
19
+ *
20
+ * Read-only by construction — it consumes the same read-only inspector
21
+ * `peaks codegraph status` gates on. It never writes `.codegraph/config.json`
22
+ * and never invokes the upstream binary.
23
+ *
24
+ * Failure posture:
25
+ * - codegraph not initialized in the inspected root (no
26
+ * `.codegraph/codegraph.db`) → `ok: true`; there is no index to be
27
+ * incomplete or stale, and a fresh clone must not fail the doctor.
28
+ * - a confirmed gap on either axis → `ok: false`. ADVISORY by default
29
+ * (`severity: 'warning'`, the doctor exit code is left alone), and
30
+ * blocking (no severity tag) when the project opts in with
31
+ * `PEAKS_CODEGRAPH_INDEX_STRICT=1` — the user's option C decision.
32
+ * The finding is `ok: false` either way; only its severity moves.
33
+ * - could not evaluate (not a git work tree, missing/malformed config,
34
+ * unreadable index) → `ok: false, severity: 'warning'` so the doctor
35
+ * reports the blind spot without flipping the exit code on an
36
+ * unrelated failure. Kept non-blocking in BOTH modes: "could not
37
+ * evaluate" is not a finding about the project, and it is
38
+ * distinguishable from the `ok: true` "covers the repository"
39
+ * verdict by its `ok` value.
40
+ *
41
+ * R12-2 — the partial collapse that remains, stated rather than implied:
42
+ * this branch and the advisory `gap` branch below BOTH emit `ok: false`
43
+ * with `severity: 'warning'`, and `DoctorCheck` has no third field, so in
44
+ * the doctor envelope they are separated by message text alone. Not fixed,
45
+ * deliberately: the shape is legacy (`runDoctor` compatibility is a
46
+ * stated constraint), neither state is `ok: true` — so the invariant that
47
+ * a non-measurement may never read as "verified clean" holds — and in the
48
+ * mode where they are collapsed neither state moves the exit code.
49
+ * Under `PEAKS_CODEGRAPH_INDEX_STRICT=1` they ARE separated in the
50
+ * machine fields: the advisory `gap` branch drops its tag, this one keeps
51
+ * it. Pinned by the "(severity policy)" and "(unevaluable)" tests.
52
+ */
53
+ import { getErrorMessage } from 'peaks-loop-shared/result';
54
+ import { isCodegraphInitialized } from '../../../codegraph/codegraph-service.js';
55
+ import { CODEGRAPH_INDEX_STRICT_ENV_VAR, CODEGRAPH_REPAIR_INDEX_COMMAND, inspectCodegraphIndexIntegrity, isCodegraphIndexStrictMode } from '../../../codegraph/codegraph-index-integrity.js';
56
+ const CHECK_ID = 'capability:codegraph-index-integrity';
57
+ /** How many offending paths the message names per axis before eliding. */
58
+ const MAX_NAMED = 5;
59
+ function elide(paths) {
60
+ const named = paths.slice(0, MAX_NAMED).join(', ');
61
+ return paths.length > MAX_NAMED ? `${named}, … (+${paths.length - MAX_NAMED})` : named;
62
+ }
63
+ /**
64
+ * 2026-09-17 — `projectRoot` is the doctor's resolved L3 root, NOT
65
+ * `process.cwd()`. It used to read `process.cwd()`, which is a different
66
+ * thing from the root the rest of the doctor was pointed at: a caller that
67
+ * injects `projectRootResolver` (e.g.
68
+ * `tests/unit/doctor/doctor-exit-code-warn-only.test.ts`) got every other
69
+ * check aimed at its temp root while THIS one opened the operator's real
70
+ * `.codegraph/codegraph.db`. Opening that database materialises its
71
+ * `-shm`/`-wal` sidecars, so a unit run mutated the operator's checkout and
72
+ * the check's result depended on which repository you happened to run it
73
+ * in. Every other L3 check reads `resolvedL3Root` from `DoctorContext`;
74
+ * this one now does too.
75
+ */
76
+ function defaultProbe(projectRoot) {
77
+ // An unresolved root is not a project. It must NOT fall through to a
78
+ // relative lookup (`join('', '.codegraph', …)` resolves against `cwd`),
79
+ // because that fallback IS the defect this signature was changed to close:
80
+ // a caller that never resolved a root would silently inspect the operator's
81
+ // checkout. `runDoctor` always resolves one; only a hand-built test context
82
+ // can pass ''.
83
+ if (projectRoot.length === 0)
84
+ return null;
85
+ // No index → nothing to be incomplete or stale.
86
+ return isCodegraphInitialized(projectRoot) ? inspectCodegraphIndexIntegrity(projectRoot) : null;
87
+ }
88
+ function renderGapMessage(includeGap, admittedTrackedCount, trackedSourceCount, deadRows, indexedFileCount) {
89
+ const parts = [];
90
+ if (includeGap.length > 0) {
91
+ parts.push(`${includeGap.length} of ${trackedSourceCount} extractor-supported tracked file(s) are not admitted by the config's include globs (admitted: ${admittedTrackedCount}) [${elide(includeGap)}]`);
92
+ }
93
+ if (deadRows.length > 0) {
94
+ parts.push(`${deadRows.length} of ${indexedFileCount} indexed file(s) are gone from disk [${elide(deadRows)}]`);
95
+ }
96
+ return `codegraph index does not cover the repository: ${parts.join('; ')}.`;
97
+ }
98
+ // The remediation clause, in both modes. Naming the command is the
99
+ // slice-002 half of slice-001's design decision 4 (which deliberately named
100
+ // none while none existed); the command name is a shared constant, so this
101
+ // message cannot drift from the CLI surface it points at.
102
+ function remediation() {
103
+ return `Run \`${CODEGRAPH_REPAIR_INDEX_COMMAND}\` to add the missing include pattern(s) and rebuild the index without the stale rows.`;
104
+ }
105
+ function run({ options, resolvedL3Root }) {
106
+ const probe = options.codegraphIndexIntegrityProbe ?? (() => defaultProbe(resolvedL3Root));
107
+ const strict = isCodegraphIndexStrictMode();
108
+ let report;
109
+ try {
110
+ report = probe();
111
+ }
112
+ catch (error) {
113
+ return [{
114
+ id: CHECK_ID,
115
+ ok: false,
116
+ severity: 'warning',
117
+ message: `codegraph index integrity could not be evaluated: ${getErrorMessage(error)}`
118
+ }];
119
+ }
120
+ if (report === null) {
121
+ return [{
122
+ id: CHECK_ID,
123
+ ok: true,
124
+ message: 'codegraph is not initialized in this project (no .codegraph/codegraph.db); there is no index to be incomplete or stale'
125
+ }];
126
+ }
127
+ if (!report.gap) {
128
+ return [{
129
+ id: CHECK_ID,
130
+ ok: true,
131
+ message: `codegraph index covers the repository (${report.admittedTrackedCount} extractor-supported tracked file(s) admitted, ${report.indexedFileCount} indexed row(s), none stale)`
132
+ }];
133
+ }
134
+ const gapMessage = renderGapMessage(report.includeGap, report.admittedTrackedCount, report.trackedSourceCount, report.deadRows, report.indexedFileCount);
135
+ // Option C: advisory by default, blocking on opt-in. `ok: false` in both
136
+ // modes — the finding is reported either way; only `severity` (and with
137
+ // it the doctor exit code) moves. The advisory suffix is the discovery
138
+ // path for the switch, so an operator who wants blocking is told how.
139
+ return strict
140
+ ? [{ id: CHECK_ID, ok: false, message: `${gapMessage} ${remediation()}` }]
141
+ : [{
142
+ id: CHECK_ID,
143
+ ok: false,
144
+ severity: 'warning',
145
+ message: `${gapMessage} ${remediation()} Advisory: set ${CODEGRAPH_INDEX_STRICT_ENV_VAR}=1 to make this blocking.`
146
+ }];
147
+ }
148
+ export const check = {
149
+ name: 'codegraph-index-integrity',
150
+ run
151
+ };
@@ -18,16 +18,16 @@
18
18
  import { existsSync, readdirSync } from 'node:fs';
19
19
  import { join } from 'node:path';
20
20
  import { getErrorMessage } from 'peaks-loop-shared/result';
21
- /**
22
- * Canonical system subdirs that intentionally live under
23
- * `.peaks/_runtime/` and must NOT be flagged as orphan sessions.
24
- *
25
- * `change/` is the routing target for change-id reviewable
26
- * artifacts per F3 audit-p1. Adding a new entry here requires
27
- * also updating `RUNTIME_SYSTEM_SUBDIRS_DOC` in the comments
28
- * below so the next maintainer knows why each entry is listed.
29
- */
30
- const RUNTIME_SYSTEM_SUBDIRS = new Set(['change']);
21
+ import { RUNTIME_SYSTEM_SUBDIRS } from '../../../workspace/runtime-layout.js';
22
+ // The exclude-list moved to `src/services/workspace/runtime-layout.ts`.
23
+ // It is imported above, not re-declared here: the local literal
24
+ // (`new Set(['change'])`) had already drifted — `callers/` is a designed
25
+ // location written by `caller-binding-service.ts` and was never added, so
26
+ // this check reported `4 orphan session(s) …: callers, cli, unknown-sid, x`
27
+ // and `peaks doctor` exited 1 on a clean workspace, permanently.
28
+ // `tests/unit/workspace/runtime-layout-drift-guard.test.ts` now fails when
29
+ // the code writes a `.peaks/_runtime/` child the registry does not know, so
30
+ // the set can no longer drift silently.
31
31
  function run({ resolvedL3Root, isValidSessionId }) {
32
32
  try {
33
33
  const runtimeDir = join(resolvedL3Root, '.peaks/_runtime');
@@ -43,6 +43,7 @@ import { check as statuslineInstall } from './checks/statusline-install.js';
43
43
  import { check as statuslineRuntime } from './checks/statusline-runtime.js';
44
44
  import { check as codegraphCapability } from './checks/codegraph-capability.js';
45
45
  import { check as codegraphExcludeIntegrity } from './checks/codegraph-exclude-integrity.js';
46
+ import { check as codegraphIndexIntegrity } from './checks/codegraph-index-integrity.js';
46
47
  import { check as distSourceVersion } from './checks/dist-source-version.js';
47
48
  import { check as multiBinaryDrift } from './checks/multi-binary-drift.js';
48
49
  import { check as workspaceLayout } from './checks/workspace-layout.js';
@@ -72,6 +73,7 @@ export const PLUGINS = [
72
73
  statuslineRuntime, // id "statusline:runtime"
73
74
  codegraphCapability, // id "capability:codegraph"
74
75
  codegraphExcludeIntegrity, // id "capability:codegraph-exclude-integrity"
76
+ codegraphIndexIntegrity, // id "capability:codegraph-index-integrity"
75
77
  distSourceVersion, // id "build:dist-version-matches-source"
76
78
  multiBinaryDrift, // id "build:multi-binary-drift"
77
79
  workspaceLayout, // id "build:workspace-layout-canonical"
@@ -94,6 +94,23 @@ export type CodegraphExcludeIntegrityProbe = {
94
94
  readonly matchedRule: string;
95
95
  }[];
96
96
  };
97
+ /**
98
+ * Structural shape of the codegraph index-integrity report the
99
+ * `capability:codegraph-index-integrity` check gates on. Declared
100
+ * structurally (rather than imported from the codegraph service) to
101
+ * keep this type module dependency-free — the default probe returns a
102
+ * `CodegraphIndexIntegrityReport`, which is assignable here.
103
+ */
104
+ export type CodegraphIndexIntegrityProbe = {
105
+ readonly gap: boolean;
106
+ readonly trackedSourceCount: number;
107
+ readonly admittedTrackedCount: number;
108
+ /** Class ① — extractor-supported tracked files `include` does not admit. */
109
+ readonly includeGap: readonly string[];
110
+ readonly indexedFileCount: number;
111
+ /** Class ② — index rows whose path is gone from disk. */
112
+ readonly deadRows: readonly string[];
113
+ };
97
114
  export type DistVersionComparison = {
98
115
  dist: string | null;
99
116
  source: string;
@@ -256,6 +273,14 @@ export type DoctorOptions = {
256
273
  * and reported as a non-blocking warning.
257
274
  */
258
275
  codegraphIntegrityProbe?: () => CodegraphExcludeIntegrityProbe | null;
276
+ /**
277
+ * Optional override for the `capability:codegraph-index-integrity`
278
+ * check. Returns the report, or `null` when codegraph is not
279
+ * initialized in the inspected root (no index to inspect). When
280
+ * omitted, the check inspects `process.cwd()`. Throwing is allowed
281
+ * and reported as a non-blocking warning.
282
+ */
283
+ codegraphIndexIntegrityProbe?: () => CodegraphIndexIntegrityProbe | null;
259
284
  skillPresenceProbe?: () => DoctorSkillPresence | null;
260
285
  skillPresenceFreshnessThresholdMs?: number;
261
286
  statusLineInstalledProbe?: () => boolean;
@@ -27,7 +27,7 @@ import { copyFileSync, mkdirSync, readFileSync } from 'node:fs';
27
27
  import { dirname, join, relative } from 'node:path';
28
28
  import { isInsidePath, resolveInputPath, stablePath, stableRealPath } from '../../../../shared/path-utils.js';
29
29
  import { renderMemoryFile, slugify } from '../parsers/frontmatter.js';
30
- import { extractStableProjectMemories, summarizeBackupResult, summarizeExtractResult } from '../parsers/markdown-pure.js';
30
+ import { extractStableProjectMemoriesWithDiagnostics, summarizeBackupResult, summarizeExtractResult } from '../parsers/markdown-pure.js';
31
31
  import { assertInsideProject, assertSafeProjectMemoryDir, assertSafeSessionDir, normalizeRoot, realPathOrThrow } from '../store/paths.js';
32
32
  import { assertSafeMemoryFileContent, writeNewFile } from '../store/atomic-write.js';
33
33
  import { generateMemoryIndexFile, readStoredMemoryNames } from './ranking.js';
@@ -35,11 +35,18 @@ import { listMarkdownFiles } from './search.js';
35
35
  export function createProjectMemoryExtractPlan(options) {
36
36
  const projectRoot = normalizeRoot(options.projectRoot);
37
37
  const primaryMemoryDir = assertSafeProjectMemoryDir(projectRoot);
38
- const extractedMemories = options.artifactPaths.flatMap((artifactPath) => {
38
+ const extractedMemories = [];
39
+ const droppedBlocks = [];
40
+ for (const artifactPath of options.artifactPaths) {
39
41
  const safeArtifactPath = assertInsideProject(artifactPath, projectRoot);
40
42
  const relativeArtifactPath = relative(projectRoot, safeArtifactPath).replaceAll('\\', '/');
41
- return extractStableProjectMemories(readFileSync(safeArtifactPath, 'utf8'), relativeArtifactPath);
42
- }).sort((left, right) => slugify(left.title).localeCompare(slugify(right.title)));
43
+ const extracted = extractStableProjectMemoriesWithDiagnostics(readFileSync(safeArtifactPath, 'utf8'), relativeArtifactPath);
44
+ // Push order + the sort below are the pre-existing ordering contract:
45
+ // artifacts in argument order, memories sorted by slug.
46
+ extractedMemories.push(...extracted.memories);
47
+ droppedBlocks.push(...extracted.dropped);
48
+ }
49
+ extractedMemories.sort((left, right) => slugify(left.title).localeCompare(slugify(right.title)));
43
50
  const slugCounts = new Map();
44
51
  for (const memory of extractedMemories) {
45
52
  const slug = slugify(memory.title);
@@ -60,7 +67,8 @@ export function createProjectMemoryExtractPlan(options) {
60
67
  primaryMemoryDir,
61
68
  backupPolicy: 'project-memory-primary-artifact-backup',
62
69
  extractedMemories,
63
- plannedWrites
70
+ plannedWrites,
71
+ droppedBlocks
64
72
  };
65
73
  }
66
74
  export function executeProjectMemoryExtract(options) {
@@ -177,22 +185,45 @@ export function extractSessionMemories(options) {
177
185
  scannedFiles: 0,
178
186
  extractedCount: 0,
179
187
  writtenFiles: [],
180
- updatedIndex: false
188
+ updatedIndex: false,
189
+ droppedBlocks: [],
190
+ scanFailures: []
181
191
  };
182
192
  }
183
193
  throw error;
184
194
  }
185
195
  const scannedFiles = listMarkdownFiles(sessionDir, { maxDepth: 6, skipDotfiles: true });
186
196
  const allExtracted = [];
197
+ // Symmetric with `createProjectMemoryExtractPlan`, which has reported the
198
+ // rejected blocks since M2. This path used to call the diagnostic-free
199
+ // projection, so a session handoff whose blocks were all malformed returned
200
+ // `extractedCount: 0` with nothing anywhere saying a block had been found.
201
+ const droppedBlocks = [];
202
+ const scanFailures = [];
187
203
  for (const filePath of scannedFiles) {
204
+ const relativePath = relative(projectRoot, filePath).replaceAll('\\', '/');
188
205
  try {
189
206
  const content = readFileSync(filePath, 'utf8');
190
- const relativePath = relative(projectRoot, filePath).replaceAll('\\', '/');
191
- const extracted = extractStableProjectMemories(content, relativePath);
192
- allExtracted.push(...extracted);
207
+ const extracted = extractStableProjectMemoriesWithDiagnostics(content, relativePath);
208
+ allExtracted.push(...extracted.memories);
209
+ droppedBlocks.push(...extracted.dropped);
193
210
  }
194
- catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
195
- // skip unreadable files
211
+ catch (error) {
212
+ // G2 resolution for this site: this was a bare `catch {}` carrying the
213
+ // repo-wide G2 grace marker that `scripts/lint/silent-warning-detector.mjs`
214
+ // reads (anti-pattern #1, `empty-catch`). The throw is no longer
215
+ // swallowed — the file is named in the same `warnings` channel as the
216
+ // block drops, which is what the grace period was deferring, so the
217
+ // marker is gone from this line. Still non-fatal: one unreadable artifact
218
+ // must not abort the scan of the rest, so the catch keeps its control
219
+ // flow and only gains a channel.
220
+ //
221
+ // `relativePath` is computed above the `try` so the catch can name the
222
+ // file. That is safe rather than convenient: `path.relative` is pure
223
+ // string arithmetic on two already-validated absolute path strings and
224
+ // does not throw for them, so hoisting it cannot turn a path-join problem
225
+ // into a fatal error that the old swallow would have absorbed.
226
+ scanFailures.push({ file: relativePath, detail: error instanceof Error ? error.message : String(error) });
196
227
  }
197
228
  }
198
229
  if (allExtracted.length === 0) {
@@ -205,7 +236,9 @@ export function extractSessionMemories(options) {
205
236
  scannedFiles: scannedFiles.length,
206
237
  extractedCount: 0,
207
238
  writtenFiles: [],
208
- updatedIndex: false
239
+ updatedIndex: false,
240
+ droppedBlocks,
241
+ scanFailures
209
242
  };
210
243
  }
211
244
  const slugCounts = new Map();
@@ -248,6 +281,8 @@ export function extractSessionMemories(options) {
248
281
  scannedFiles: scannedFiles.length,
249
282
  extractedCount: allExtracted.length,
250
283
  writtenFiles,
251
- updatedIndex: apply && writtenFiles.length > 0
284
+ updatedIndex: apply && writtenFiles.length > 0,
285
+ droppedBlocks,
286
+ scanFailures
252
287
  };
253
288
  }
@@ -1,8 +1,10 @@
1
- export type { BackupPlanOptions, ExtractedProjectMemory, MemoryKindTier, ExtractPlanOptions, ExtractSessionMemoriesOptions, ExtractSessionMemoriesResult, MemoryIndex, MemoryIndexEntry, ProjectMemoryBackupPlan, ProjectMemoryBackupResult, ProjectMemoryBackupSummary, ProjectMemoryCopy, ProjectMemoryExtractPlan, ProjectMemoryExtractResult, ProjectMemoryExtractSummary, ProjectMemoryKind, ProjectMemoryReadResult, ProjectMemoryShowResult, ProjectMemoryWrite, StoredProjectMemory } from './types.js';
1
+ export type { BackupPlanOptions, ExtractedProjectMemory, MemoryBlockDrop, MemoryBlockDropReason, MemoryBlockParse, MemoryKindTier, ExtractPlanOptions, ExtractSessionMemoriesOptions, ExtractSessionMemoriesResult, MemoryIndex, MemoryIndexEntry, ProjectMemoryBackupPlan, ProjectMemoryBackupResult, ProjectMemoryBackupSummary, ProjectMemoryCopy, ProjectMemoryExtractPlan, ProjectMemoryExtractResult, ProjectMemoryExtractSummary, ProjectMemoryKind, ProjectMemoryReadResult, ProjectMemoryShowResult, ProjectMemoryWrite, StoredProjectMemory } from './types.js';
2
2
  export { VALID_PROJECT_MEMORY_KINDS } from './parsers/frontmatter.js';
3
3
  export { HOT_MEMORY_KINDS, MEMORY_KIND_TIER, PROJECT_MEMORY_KINDS, WARM_MEMORY_KINDS } from './types.js';
4
- export { summarizeBackupResult, summarizeExtractResult, summarizeMemoryBody, extractStableProjectMemories, END_MARKER, START_MARKER } from './parsers/markdown-pure.js';
5
- export { parseBlock, parseMemoryFrontmatter, parseStoredMemoryFile, renderMemoryFile, resolveMemoryKind, resolveMemoryName, slugify } from './parsers/frontmatter.js';
4
+ export { describeMemoryBlockDrops, describeSessionScanFailures, summarizeBackupResult, summarizeExtractResult, summarizeMemoryBody, extractStableProjectMemories, extractStableProjectMemoriesWithDiagnostics, END_MARKER, START_MARKER } from './parsers/markdown-pure.js';
5
+ export type { SessionScanFailure } from './types.js';
6
+ export type { ExtractedMemoryBlocks } from './parsers/markdown-pure.js';
7
+ export { parseBlock, parseBlockResult, parseMemoryFrontmatter, parseStoredMemoryFile, renderMemoryFile, resolveMemoryKind, resolveMemoryName, slugify } from './parsers/frontmatter.js';
6
8
  export type { MemoryKindResolution, MemoryKindSource, MemoryNameResolution, MemoryNameSource, ParsedMemoryFrontmatter } from './parsers/frontmatter.js';
7
9
  export { assertInsideProject, assertSafeProjectMemoryDir, assertSafeSessionDir, normalizeRealRoot, normalizeRoot, realPathOrThrow, resolveProjectPath, safeRealpath } from './store/paths.js';
8
10
  export { assertSafeMemory, assertSafeMemoryFileContent, hasSensitiveMemoryContent, writeNewFile } from './store/atomic-write.js';
@@ -14,9 +14,9 @@ export { VALID_PROJECT_MEMORY_KINDS } from './parsers/frontmatter.js';
14
14
  // Canonical kind vocabulary + hot/warm tier map (slice E)
15
15
  export { HOT_MEMORY_KINDS, MEMORY_KIND_TIER, PROJECT_MEMORY_KINDS, WARM_MEMORY_KINDS } from './types.js';
16
16
  // Pure markdown helpers
17
- export { summarizeBackupResult, summarizeExtractResult, summarizeMemoryBody, extractStableProjectMemories, END_MARKER, START_MARKER } from './parsers/markdown-pure.js';
17
+ export { describeMemoryBlockDrops, describeSessionScanFailures, summarizeBackupResult, summarizeExtractResult, summarizeMemoryBody, extractStableProjectMemories, extractStableProjectMemoriesWithDiagnostics, END_MARKER, START_MARKER } from './parsers/markdown-pure.js';
18
18
  // Frontmatter parser + renderer
19
- export { parseBlock, parseMemoryFrontmatter, parseStoredMemoryFile, renderMemoryFile, resolveMemoryKind, resolveMemoryName, slugify } from './parsers/frontmatter.js';
19
+ export { parseBlock, parseBlockResult, parseMemoryFrontmatter, parseStoredMemoryFile, renderMemoryFile, resolveMemoryKind, resolveMemoryName, slugify } from './parsers/frontmatter.js';
20
20
  // Store: path safety + sensitive content
21
21
  export { assertInsideProject, assertSafeProjectMemoryDir, assertSafeSessionDir, normalizeRealRoot, normalizeRoot, realPathOrThrow, resolveProjectPath, safeRealpath } from './store/paths.js';
22
22
  export { assertSafeMemory, assertSafeMemoryFileContent, hasSensitiveMemoryContent, writeNewFile } from './store/atomic-write.js';
@@ -1,4 +1,4 @@
1
- import type { ExtractedProjectMemory, ProjectMemoryKind, StoredProjectMemory } from '../types.js';
1
+ import type { ExtractedProjectMemory, MemoryBlockParse, ProjectMemoryKind, StoredProjectMemory } from '../types.js';
2
2
  /** Accepted-kind set, derived from the canonical `PROJECT_MEMORY_KINDS`
3
3
  * tuple so the parser cannot drift from the union type / tier map. */
4
4
  export declare const VALID_MEMORY_KINDS: ReadonlySet<ProjectMemoryKind>;
@@ -6,6 +6,20 @@ export declare const VALID_MEMORY_KINDS: ReadonlySet<ProjectMemoryKind>;
6
6
  * set (CLI help text, `--kind` validation) without duplicating the literal. */
7
7
  export declare const VALID_PROJECT_MEMORY_KINDS: readonly ProjectMemoryKind[];
8
8
  export declare function slugify(title: string): string;
9
+ /**
10
+ * Single implementation of the extract-path block parse. Returns a
11
+ * discriminated result so the caller can say WHY a found block was rejected.
12
+ *
13
+ * The precondition order mirrors the original combined `if` exactly
14
+ * (separator → title → kind present → kind valid → body non-empty), so the
15
+ * accepted set is bit-for-bit what it always was; only the explanation is new.
16
+ */
17
+ export declare function parseBlockResult(block: string, sourceArtifact: string): MemoryBlockParse;
18
+ /**
19
+ * `null`-on-failure projection of `parseBlockResult`. Kept because callers
20
+ * (and the parser's own contract) consume a plain nullable value; both views
21
+ * come from the one implementation above, so they cannot diverge.
22
+ */
9
23
  export declare function parseBlock(block: string, sourceArtifact: string): ExtractedProjectMemory | null;
10
24
  export declare function renderMemoryFile(memory: ExtractedProjectMemory): string;
11
25
  /**
@@ -31,11 +31,20 @@ export function slugify(title) {
31
31
  const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
32
32
  return slug.length > 0 ? slug : 'project-memory';
33
33
  }
34
- export function parseBlock(block, sourceArtifact) {
34
+ /**
35
+ * Single implementation of the extract-path block parse. Returns a
36
+ * discriminated result so the caller can say WHY a found block was rejected.
37
+ *
38
+ * The precondition order mirrors the original combined `if` exactly
39
+ * (separator → title → kind present → kind valid → body non-empty), so the
40
+ * accepted set is bit-for-bit what it always was; only the explanation is new.
41
+ */
42
+ export function parseBlockResult(block, sourceArtifact) {
35
43
  const normalizedBlock = block.replace(/\r\n/g, '\n');
36
44
  const separatorIndex = normalizedBlock.indexOf('\n---\n');
37
- if (separatorIndex < 0)
38
- return null;
45
+ if (separatorIndex < 0) {
46
+ return { ok: false, reason: 'missing-separator', detail: "block header is not followed by a '---' separator line" };
47
+ }
39
48
  const header = normalizedBlock.slice(0, separatorIndex).trim();
40
49
  const body = normalizedBlock.slice(separatorIndex + '\n---\n'.length).trim();
41
50
  const fields = new Map();
@@ -49,9 +58,28 @@ export function parseBlock(block, sourceArtifact) {
49
58
  }
50
59
  const title = fields.get('title')?.trim();
51
60
  const kind = fields.get('kind')?.trim();
52
- if (!title || !kind || !VALID_MEMORY_KINDS.has(kind) || body.length === 0)
53
- return null;
54
- return { title, kind, body, sourceArtifact };
61
+ if (!title) {
62
+ return { ok: false, reason: 'missing-title', detail: "block header has no non-empty 'title:' field" };
63
+ }
64
+ if (!kind) {
65
+ return { ok: false, reason: 'missing-kind', detail: "block header has no non-empty 'kind:' field" };
66
+ }
67
+ if (!VALID_MEMORY_KINDS.has(kind)) {
68
+ return { ok: false, reason: 'unknown-kind', detail: `block declares kind '${kind}', which is not an accepted memory kind` };
69
+ }
70
+ if (body.length === 0) {
71
+ return { ok: false, reason: 'empty-body', detail: 'block body is empty' };
72
+ }
73
+ return { ok: true, memory: { title, kind, body, sourceArtifact } };
74
+ }
75
+ /**
76
+ * `null`-on-failure projection of `parseBlockResult`. Kept because callers
77
+ * (and the parser's own contract) consume a plain nullable value; both views
78
+ * come from the one implementation above, so they cannot diverge.
79
+ */
80
+ export function parseBlock(block, sourceArtifact) {
81
+ const parsed = parseBlockResult(block, sourceArtifact);
82
+ return parsed.ok ? parsed.memory : null;
55
83
  }
56
84
  export function renderMemoryFile(memory) {
57
85
  const name = slugify(memory.title);
@@ -1,7 +1,33 @@
1
- import type { ExtractedProjectMemory, ProjectMemoryBackupResult, ProjectMemoryBackupSummary, ProjectMemoryExtractResult, ProjectMemoryExtractSummary } from '../types.js';
1
+ import type { ExtractedProjectMemory, MemoryBlockDrop, ProjectMemoryBackupResult, ProjectMemoryBackupSummary, ProjectMemoryExtractResult, ProjectMemoryExtractSummary, SessionScanFailure } from '../types.js';
2
2
  export declare const START_MARKER = "<!-- peaks-memory:start -->";
3
3
  export declare const END_MARKER = "<!-- peaks-memory:end -->";
4
4
  export declare function summarizeMemoryBody(body: string): string;
5
+ /** Extracted memories plus the blocks that were found and rejected. */
6
+ export type ExtractedMemoryBlocks = {
7
+ memories: ExtractedProjectMemory[];
8
+ dropped: MemoryBlockDrop[];
9
+ };
10
+ /**
11
+ * Same scan as `extractStableProjectMemories`, but also reports the blocks
12
+ * that were found between the markers and rejected by the parser.
13
+ *
14
+ * `extractStableProjectMemories` is the `.memories` projection of this, so the
15
+ * extracted set is identical by construction — the diagnostics cannot change
16
+ * which blocks are accepted.
17
+ */
18
+ export declare function extractStableProjectMemoriesWithDiagnostics(content: string, sourceArtifact: string): ExtractedMemoryBlocks;
5
19
  export declare function extractStableProjectMemories(content: string, sourceArtifact: string): ExtractedProjectMemory[];
20
+ /**
21
+ * Render one warning line per rejected block, for the CLI envelope's
22
+ * `warnings` channel. `unknown-kind` additionally names the accepted
23
+ * vocabulary, because that is the failure whose remedy is a value change.
24
+ */
25
+ export declare function describeMemoryBlockDrops(dropped: ReadonlyArray<MemoryBlockDrop>): string[];
26
+ /**
27
+ * Render one warning line per session artifact that could not be read. The
28
+ * sibling of `describeMemoryBlockDrops`, for the failure one level coarser: a
29
+ * whole file that never yielded blocks at all.
30
+ */
31
+ export declare function describeSessionScanFailures(failures: ReadonlyArray<SessionScanFailure>): string[];
6
32
  export declare function summarizeExtractResult(result: ProjectMemoryExtractResult): ProjectMemoryExtractSummary;
7
33
  export declare function summarizeBackupResult(result: ProjectMemoryBackupResult): ProjectMemoryBackupSummary;