minnimemory 1.0.0-beta.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/LICENSE +39 -0
- package/README.md +824 -0
- package/dist/bench.d.ts +98 -0
- package/dist/bench.js +142 -0
- package/dist/benchReport.d.ts +12 -0
- package/dist/benchReport.js +128 -0
- package/dist/bounds.d.ts +40 -0
- package/dist/bounds.js +44 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +503 -0
- package/dist/compile.d.ts +187 -0
- package/dist/compile.js +516 -0
- package/dist/discover.d.ts +125 -0
- package/dist/discover.js +520 -0
- package/dist/doctor.d.ts +9 -0
- package/dist/doctor.js +67 -0
- package/dist/episodic.d.ts +47 -0
- package/dist/episodic.js +130 -0
- package/dist/hook.d.ts +45 -0
- package/dist/hook.js +104 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +18 -0
- package/dist/init.d.ts +125 -0
- package/dist/init.js +475 -0
- package/dist/instructions.d.ts +60 -0
- package/dist/instructions.js +270 -0
- package/dist/mcp.d.ts +109 -0
- package/dist/mcp.js +252 -0
- package/dist/mcpServer.d.ts +136 -0
- package/dist/mcpServer.js +997 -0
- package/dist/paths.d.ts +25 -0
- package/dist/paths.js +47 -0
- package/dist/recall.d.ts +113 -0
- package/dist/recall.js +256 -0
- package/dist/recallDir.d.ts +50 -0
- package/dist/recallDir.js +187 -0
- package/dist/reorganize.d.ts +62 -0
- package/dist/reorganize.js +216 -0
- package/dist/report.d.ts +16 -0
- package/dist/report.js +204 -0
- package/dist/router.d.ts +141 -0
- package/dist/router.js +314 -0
- package/dist/rules.d.ts +32 -0
- package/dist/rules.js +651 -0
- package/dist/scan.d.ts +110 -0
- package/dist/scan.js +173 -0
- package/dist/text.d.ts +158 -0
- package/dist/text.js +395 -0
- package/dist/tokenizer.d.ts +26 -0
- package/dist/tokenizer.js +69 -0
- package/dist/types.d.ts +156 -0
- package/dist/types.js +17 -0
- package/dist/version.d.ts +7 -0
- package/dist/version.js +7 -0
- package/dist/writeProtocol.d.ts +19 -0
- package/dist/writeProtocol.js +45 -0
- package/examples/CLAUDE.md +75 -0
- package/examples/README.md +7 -0
- package/package.json +52 -0
package/dist/scan.d.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured, whole-directory inventory of a memory setup for an agent to reorganize.
|
|
3
|
+
*
|
|
4
|
+
* This is the deterministic half of "AI-scanned reorganization" (project_minnimemory memory,
|
|
5
|
+
* 2026-09-03): scan.ts computes facts - frontmatter, section boundaries, volatility evidence,
|
|
6
|
+
* candidate routing keywords, cross-file duplicate blocks - the same primitives doctor/init
|
|
7
|
+
* already use. It never classifies, merges, or judges; it hands a calling agent everything it
|
|
8
|
+
* would otherwise have to re-derive by reading every file by hand. The agent (not this module,
|
|
9
|
+
* not an API call - see the locked "no LLM in init/doctor" decision) does the O1-O5/CoALA
|
|
10
|
+
* judgment and produces a ReorganizePlan for reorganize.ts to apply.
|
|
11
|
+
*/
|
|
12
|
+
import { type MemoryKind } from "./episodic.js";
|
|
13
|
+
import type { WorkspaceShape } from "./types.js";
|
|
14
|
+
export interface Frontmatter {
|
|
15
|
+
name?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
/** metadata.type - the auto-memory category (user/feedback/project/reference) when present */
|
|
18
|
+
type?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface SectionScan {
|
|
21
|
+
heading: string;
|
|
22
|
+
level: number;
|
|
23
|
+
startLine: number;
|
|
24
|
+
endLine: number;
|
|
25
|
+
tokens: number;
|
|
26
|
+
volatile: boolean;
|
|
27
|
+
volatilityReasons: string[];
|
|
28
|
+
/** O2: semantic, episodic or procedural, decided per section */
|
|
29
|
+
kind: MemoryKind;
|
|
30
|
+
keywords: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface FileScan {
|
|
33
|
+
rel: string;
|
|
34
|
+
tokens: number;
|
|
35
|
+
isList: boolean;
|
|
36
|
+
alwaysLoaded: boolean;
|
|
37
|
+
frontmatter?: Frontmatter;
|
|
38
|
+
sections: SectionScan[];
|
|
39
|
+
}
|
|
40
|
+
export interface DuplicateScan {
|
|
41
|
+
/** short preview, not the full block - keep the scan itself cheap to hold in context */
|
|
42
|
+
preview: string;
|
|
43
|
+
tokensWastedPerExtraCopy: number;
|
|
44
|
+
occurrences: {
|
|
45
|
+
file: string;
|
|
46
|
+
startLine: number;
|
|
47
|
+
endLine: number;
|
|
48
|
+
}[];
|
|
49
|
+
}
|
|
50
|
+
export interface MemoryScan {
|
|
51
|
+
root: string;
|
|
52
|
+
shape: WorkspaceShape;
|
|
53
|
+
/** `@path` imports outside the root that were not followed; see discover.resolveImports */
|
|
54
|
+
skippedImports?: string[];
|
|
55
|
+
totalTokens: number;
|
|
56
|
+
alwaysLoadedTokens: number;
|
|
57
|
+
files: FileScan[];
|
|
58
|
+
duplicates: DuplicateScan[];
|
|
59
|
+
}
|
|
60
|
+
export interface FileScanSummary {
|
|
61
|
+
rel: string;
|
|
62
|
+
tokens: number;
|
|
63
|
+
isList: boolean;
|
|
64
|
+
alwaysLoaded: boolean;
|
|
65
|
+
frontmatter?: Frontmatter;
|
|
66
|
+
sectionCount: number;
|
|
67
|
+
volatileSectionCount: number;
|
|
68
|
+
kinds: Record<MemoryKind, number>;
|
|
69
|
+
}
|
|
70
|
+
export interface MemoryScanSummary {
|
|
71
|
+
root: string;
|
|
72
|
+
shape: WorkspaceShape;
|
|
73
|
+
skippedImports?: string[];
|
|
74
|
+
totalTokens: number;
|
|
75
|
+
alwaysLoadedTokens: number;
|
|
76
|
+
files: FileScanSummary[];
|
|
77
|
+
duplicates: {
|
|
78
|
+
groups: number;
|
|
79
|
+
tokensWasted: number;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The rollup a caller reaches for first: counts and per-file totals, not every section's
|
|
84
|
+
* heading, keywords and volatility reasons (the `full` shape below). 2026-09-13 audit round 2,
|
|
85
|
+
* D4 - `scan`'s default output on a real memory directory of any size was mostly detail nobody
|
|
86
|
+
* asked for yet; `detail: "full"` still returns the original shape when a caller actually wants
|
|
87
|
+
* every section.
|
|
88
|
+
*/
|
|
89
|
+
export declare function summarize(scan: MemoryScan): MemoryScanSummary;
|
|
90
|
+
/**
|
|
91
|
+
* Restrict a scan to exactly the named files (matched against `rel`), recomputing the token
|
|
92
|
+
* totals over just that subset so they describe what is actually returned. A name that matches
|
|
93
|
+
* nothing is reported back in `unknownFiles` rather than silently dropped. `duplicates` are left
|
|
94
|
+
* as computed over the whole workspace: a block duplicated between a kept file and a filtered-out
|
|
95
|
+
* one is still worth knowing about, and pretending the other copy does not exist would hide
|
|
96
|
+
* exactly the fact this field exists to surface.
|
|
97
|
+
*/
|
|
98
|
+
export declare function filterScanFiles(scan: MemoryScan, files: string[] | undefined): {
|
|
99
|
+
scan: MemoryScan;
|
|
100
|
+
unknownFiles: string[];
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Scan any target discover() can resolve - a bare memory directory, a Claude Code auto-memory
|
|
104
|
+
* folder (directly or via a project root), a host-file repo - into a full structured inventory.
|
|
105
|
+
* Read-only, no writes, safe to call as often as needed while drafting a plan.
|
|
106
|
+
*/
|
|
107
|
+
export declare function scanMemoryDir(target: string, options?: {
|
|
108
|
+
followExternalImports?: boolean;
|
|
109
|
+
includeAutoMemory?: boolean;
|
|
110
|
+
}): MemoryScan;
|
package/dist/scan.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured, whole-directory inventory of a memory setup for an agent to reorganize.
|
|
3
|
+
*
|
|
4
|
+
* This is the deterministic half of "AI-scanned reorganization" (project_minnimemory memory,
|
|
5
|
+
* 2026-09-03): scan.ts computes facts - frontmatter, section boundaries, volatility evidence,
|
|
6
|
+
* candidate routing keywords, cross-file duplicate blocks - the same primitives doctor/init
|
|
7
|
+
* already use. It never classifies, merges, or judges; it hands a calling agent everything it
|
|
8
|
+
* would otherwise have to re-derive by reading every file by hand. The agent (not this module,
|
|
9
|
+
* not an API call - see the locked "no LLM in init/doctor" decision) does the O1-O5/CoALA
|
|
10
|
+
* judgment and produces a ReorganizePlan for reorganize.ts to apply.
|
|
11
|
+
*/
|
|
12
|
+
import { discover } from "./discover.js";
|
|
13
|
+
import { classifyKind } from "./episodic.js";
|
|
14
|
+
import { findDuplicateBlocks } from "./rules.js";
|
|
15
|
+
import { keywords as extractKeywords, sectionVolatilityReasons, sections, splitFrontmatter } from "./text.js";
|
|
16
|
+
import { estimateTokens } from "./tokenizer.js";
|
|
17
|
+
/**
|
|
18
|
+
* The rollup a caller reaches for first: counts and per-file totals, not every section's
|
|
19
|
+
* heading, keywords and volatility reasons (the `full` shape below). 2026-09-13 audit round 2,
|
|
20
|
+
* D4 - `scan`'s default output on a real memory directory of any size was mostly detail nobody
|
|
21
|
+
* asked for yet; `detail: "full"` still returns the original shape when a caller actually wants
|
|
22
|
+
* every section.
|
|
23
|
+
*/
|
|
24
|
+
export function summarize(scan) {
|
|
25
|
+
const files = scan.files.map((f) => {
|
|
26
|
+
const kinds = { semantic: 0, episodic: 0, procedural: 0 };
|
|
27
|
+
let volatileSectionCount = 0;
|
|
28
|
+
for (const s of f.sections) {
|
|
29
|
+
kinds[s.kind]++;
|
|
30
|
+
if (s.volatile)
|
|
31
|
+
volatileSectionCount++;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
rel: f.rel,
|
|
35
|
+
tokens: f.tokens,
|
|
36
|
+
isList: f.isList,
|
|
37
|
+
alwaysLoaded: f.alwaysLoaded,
|
|
38
|
+
...(f.frontmatter ? { frontmatter: f.frontmatter } : {}),
|
|
39
|
+
sectionCount: f.sections.length,
|
|
40
|
+
volatileSectionCount,
|
|
41
|
+
kinds,
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
const tokensWasted = scan.duplicates.reduce((n, d) => n + d.tokensWastedPerExtraCopy * Math.max(0, d.occurrences.length - 1), 0);
|
|
45
|
+
return {
|
|
46
|
+
root: scan.root,
|
|
47
|
+
shape: scan.shape,
|
|
48
|
+
...(scan.skippedImports?.length ? { skippedImports: scan.skippedImports } : {}),
|
|
49
|
+
totalTokens: scan.totalTokens,
|
|
50
|
+
alwaysLoadedTokens: scan.alwaysLoadedTokens,
|
|
51
|
+
files,
|
|
52
|
+
duplicates: { groups: scan.duplicates.length, tokensWasted },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Restrict a scan to exactly the named files (matched against `rel`), recomputing the token
|
|
57
|
+
* totals over just that subset so they describe what is actually returned. A name that matches
|
|
58
|
+
* nothing is reported back in `unknownFiles` rather than silently dropped. `duplicates` are left
|
|
59
|
+
* as computed over the whole workspace: a block duplicated between a kept file and a filtered-out
|
|
60
|
+
* one is still worth knowing about, and pretending the other copy does not exist would hide
|
|
61
|
+
* exactly the fact this field exists to surface.
|
|
62
|
+
*/
|
|
63
|
+
export function filterScanFiles(scan, files) {
|
|
64
|
+
if (!files || files.length === 0)
|
|
65
|
+
return { scan, unknownFiles: [] };
|
|
66
|
+
const wanted = new Set(files);
|
|
67
|
+
const kept = scan.files.filter((f) => wanted.has(f.rel));
|
|
68
|
+
const keptRels = new Set(kept.map((f) => f.rel));
|
|
69
|
+
const unknownFiles = files.filter((f) => !keptRels.has(f));
|
|
70
|
+
const totalTokens = kept.reduce((n, f) => n + f.tokens, 0);
|
|
71
|
+
const alwaysLoadedTokens = kept.filter((f) => f.alwaysLoaded).reduce((n, f) => n + f.tokens, 0);
|
|
72
|
+
return {
|
|
73
|
+
scan: { ...scan, files: kept, totalTokens, alwaysLoadedTokens },
|
|
74
|
+
unknownFiles,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A minimal, deliberately non-general YAML reader for exactly the frontmatter shape this
|
|
79
|
+
* project's memory files use: flat `key: value` lines plus one level of nesting under
|
|
80
|
+
* `metadata:`. Not a YAML parser - a real one is a dependency this offline-first tool does not
|
|
81
|
+
* need for three known keys.
|
|
82
|
+
*/
|
|
83
|
+
function parseFrontmatter(lines) {
|
|
84
|
+
if (lines.length === 0)
|
|
85
|
+
return undefined;
|
|
86
|
+
const body = lines.slice(1, -1); // drop the two `---` fence lines
|
|
87
|
+
const out = {};
|
|
88
|
+
let inMetadata = false;
|
|
89
|
+
const unquote = (v) => v.trim().replace(/^["']|["']$/g, "");
|
|
90
|
+
for (const raw of body) {
|
|
91
|
+
const indented = /^\s+/.test(raw) && raw.trim() !== "";
|
|
92
|
+
if (!indented)
|
|
93
|
+
inMetadata = /^metadata\s*:/.test(raw.trim());
|
|
94
|
+
const m = /^\s*([\w.-]+)\s*:\s*(.*)$/.exec(raw);
|
|
95
|
+
if (!m)
|
|
96
|
+
continue;
|
|
97
|
+
const key = m[1] ?? "";
|
|
98
|
+
const value = unquote(m[2] ?? "");
|
|
99
|
+
if (!indented && key === "name" && value)
|
|
100
|
+
out.name = value;
|
|
101
|
+
else if (!indented && key === "description" && value)
|
|
102
|
+
out.description = value;
|
|
103
|
+
else if (indented && inMetadata && key === "type" && value)
|
|
104
|
+
out.type = value;
|
|
105
|
+
}
|
|
106
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
107
|
+
}
|
|
108
|
+
function scanFile(file, isList) {
|
|
109
|
+
const { frontmatter: fmLines, bodyStartLine } = splitFrontmatter(file.lines);
|
|
110
|
+
const bodyLines = file.lines.slice(bodyStartLine - 1);
|
|
111
|
+
const sectionScans = sections(bodyLines).map((s) => {
|
|
112
|
+
const sectionLines = bodyLines.slice(s.startLine - 1, s.endLine);
|
|
113
|
+
const reasons = sectionVolatilityReasons(sectionLines);
|
|
114
|
+
const bodyOnly = sectionLines.slice(1).join("\n");
|
|
115
|
+
return {
|
|
116
|
+
heading: s.heading,
|
|
117
|
+
level: s.level,
|
|
118
|
+
// offset back into the whole-file line numbering, since sections() above only saw bodyLines
|
|
119
|
+
startLine: s.startLine + bodyStartLine - 1,
|
|
120
|
+
endLine: s.endLine + bodyStartLine - 1,
|
|
121
|
+
tokens: estimateTokens(sectionLines.join("\n")),
|
|
122
|
+
volatile: reasons.length > 0,
|
|
123
|
+
volatilityReasons: reasons,
|
|
124
|
+
kind: classifyKind(s.heading, sectionLines.slice(1)),
|
|
125
|
+
keywords: extractKeywords(s.heading, bodyOnly),
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
rel: file.rel,
|
|
130
|
+
tokens: file.tokens,
|
|
131
|
+
isList,
|
|
132
|
+
alwaysLoaded: file.alwaysLoaded,
|
|
133
|
+
frontmatter: parseFrontmatter(fmLines),
|
|
134
|
+
sections: sectionScans,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const PREVIEW_LEN = 160;
|
|
138
|
+
/**
|
|
139
|
+
* Scan any target discover() can resolve - a bare memory directory, a Claude Code auto-memory
|
|
140
|
+
* folder (directly or via a project root), a host-file repo - into a full structured inventory.
|
|
141
|
+
* Read-only, no writes, safe to call as often as needed while drafting a plan.
|
|
142
|
+
*/
|
|
143
|
+
export function scanMemoryDir(target, options = {}) {
|
|
144
|
+
const ws = discover(target, {
|
|
145
|
+
followExternalImports: options.followExternalImports === true,
|
|
146
|
+
includeAutoMemory: options.includeAutoMemory !== false,
|
|
147
|
+
});
|
|
148
|
+
const files = ws.files.map((f) => scanFile(f, f === ws.onDemandList || f === ws.autoMemoryList));
|
|
149
|
+
const duplicates = findDuplicateBlocks(ws.files).map(({ occurrences, wastedTokens }) => {
|
|
150
|
+
const first = occurrences[0];
|
|
151
|
+
const text = first ? first.block.text : "";
|
|
152
|
+
return {
|
|
153
|
+
preview: text.length > PREVIEW_LEN ? `${text.slice(0, PREVIEW_LEN)}…` : text,
|
|
154
|
+
tokensWastedPerExtraCopy: occurrences.length > 1 ? Math.round(wastedTokens / (occurrences.length - 1)) : 0,
|
|
155
|
+
occurrences: occurrences.map((o) => ({
|
|
156
|
+
file: o.file.rel,
|
|
157
|
+
startLine: o.block.startLine,
|
|
158
|
+
endLine: o.block.endLine,
|
|
159
|
+
})),
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
const totalTokens = ws.files.reduce((n, f) => n + f.tokens, 0);
|
|
163
|
+
const alwaysLoadedTokens = ws.files.filter((f) => f.alwaysLoaded).reduce((n, f) => n + f.tokens, 0);
|
|
164
|
+
return {
|
|
165
|
+
root: ws.root,
|
|
166
|
+
shape: ws.shape,
|
|
167
|
+
...(ws.skippedImports?.length ? { skippedImports: ws.skippedImports } : {}),
|
|
168
|
+
totalTokens,
|
|
169
|
+
alwaysLoadedTokens,
|
|
170
|
+
files,
|
|
171
|
+
duplicates,
|
|
172
|
+
};
|
|
173
|
+
}
|
package/dist/text.d.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text primitives shared by the rule engine and the compiler.
|
|
3
|
+
*/
|
|
4
|
+
export interface Section {
|
|
5
|
+
/** heading text with the hashes stripped */
|
|
6
|
+
heading: string;
|
|
7
|
+
/** 1 for #, 2 for ##, and so on. 0 for the synthetic preamble section. */
|
|
8
|
+
level: number;
|
|
9
|
+
/** 1-indexed, inclusive */
|
|
10
|
+
startLine: number;
|
|
11
|
+
endLine: number;
|
|
12
|
+
}
|
|
13
|
+
export interface Block {
|
|
14
|
+
text: string;
|
|
15
|
+
startLine: number;
|
|
16
|
+
endLine: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Split lines into heading-delimited sections. Lines inside fenced code blocks are never
|
|
20
|
+
* headings: a shell comment like `# Machine-readable output` in a bash example is not a title.
|
|
21
|
+
* Found by the 2026-09-02 sweep, where one real CLAUDE.md gained a phantom H1 mid-file and
|
|
22
|
+
* half its content was duplicated into AlwaysOnMemory as "preamble".
|
|
23
|
+
*/
|
|
24
|
+
export declare function sections(lines: string[]): Section[];
|
|
25
|
+
/** Split lines into blank-line-delimited blocks. */
|
|
26
|
+
export declare function blocks(lines: string[]): Block[];
|
|
27
|
+
/** Normalise a block so cosmetic differences do not hide a duplicate. */
|
|
28
|
+
export declare function normalise(text: string): string;
|
|
29
|
+
/** Filesystem-safe, stable slug for a heading. */
|
|
30
|
+
export declare function slugify(heading: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Volatility: does this content change often, so that editing it would invalidate the cached
|
|
33
|
+
* always-loaded prefix?
|
|
34
|
+
*
|
|
35
|
+
* This is scored on evidence, not fired by a single word. The 2026-09-02 sweep over 23 real
|
|
36
|
+
* CLAUDE.md/AGENTS.md files from public repos showed that one-word matching flagged stable
|
|
37
|
+
* sections constantly: a version number inside a URL, "the next step" in a coding standard,
|
|
38
|
+
* "regenerate the changelog" in commit guidance, "pending activity" in a debugging note. Those
|
|
39
|
+
* are prose, not status. What actually marks status is a status heading, dated log entries,
|
|
40
|
+
* checklists, or several status phrases together.
|
|
41
|
+
*
|
|
42
|
+
* Scoring (a section is volatile at VOLATILITY_THRESHOLD or above):
|
|
43
|
+
* status heading ("Current status", "Changelog", "Roadmap", "Open items"...) +2
|
|
44
|
+
* list item that starts with a date (a log entry) +1 each
|
|
45
|
+
* strong status phrase ("as of 2026", "in progress", "blocked on") +1 each
|
|
46
|
+
* date anywhere else in a line +0.5 each
|
|
47
|
+
* weak status word ("currently", "pending", "todo", "roadmap" in prose) +0.5 each
|
|
48
|
+
* checkbox item +0.25 each, at most +1
|
|
49
|
+
* version string +0.25 each
|
|
50
|
+
* Inline code spans and URLs are removed from a line before matching, so a version in a link
|
|
51
|
+
* or a `TODO` in a code sample never counts.
|
|
52
|
+
*/
|
|
53
|
+
export declare const VOLATILITY_THRESHOLD = 2;
|
|
54
|
+
/**
|
|
55
|
+
* Interface rule O7, the granularity clause: a section above this many tokens carries
|
|
56
|
+
* subheadings so that retrieval has a unit smaller than the whole. `doctor` flags a flat
|
|
57
|
+
* section over it (MM009); `init` splits a section over it at its subheadings into separate
|
|
58
|
+
* OnDemandMemory files, so the agent-driven route (an agent reading a whole file the
|
|
59
|
+
* OnDemandMemory list named) gets the same granularity `recall` gets from in-file sections.
|
|
60
|
+
*/
|
|
61
|
+
export declare const GRANULARITY_TOKENS = 800;
|
|
62
|
+
/**
|
|
63
|
+
* A bullet (list marker or numbered item) whose first few words carry a date: a changelog or
|
|
64
|
+
* session-log entry. The single shared definition (2026-09-13 audit round 2, D7a) - episodic.ts,
|
|
65
|
+
* router.ts and rules.ts's MM009 each grew their own slightly different copy (one required a full
|
|
66
|
+
* day, one skipped numbered lists, one had an explicit checkbox branch this pattern's lazy
|
|
67
|
+
* 0-24-character window already covers). This is the widest of the three: numbered lists
|
|
68
|
+
* (`1.`/`1)`), a month-only date, and up to 24 characters (a checkbox, a bold marker, ...) before
|
|
69
|
+
* the date all count.
|
|
70
|
+
*/
|
|
71
|
+
export declare const RE_DATED_BULLET: RegExp;
|
|
72
|
+
/**
|
|
73
|
+
* Is this body of lines "episodic": mostly a list of dated entries (a changelog, a session log)?
|
|
74
|
+
* At least 3 dated bullets, and at least half of all bullets dated. The one rule every episodic-
|
|
75
|
+
* detection call site in the codebase now shares, so "how many dated entries make a section a
|
|
76
|
+
* changelog" has one answer instead of three.
|
|
77
|
+
*/
|
|
78
|
+
export declare function isEpisodicBody(lines: string[]): boolean;
|
|
79
|
+
export interface Volatility {
|
|
80
|
+
score: number;
|
|
81
|
+
/** distinct kinds of evidence found, in a fixed order, for messages */
|
|
82
|
+
reasons: string[];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Score a run of lines. Pass the heading line as `heading` when the lines are a section body;
|
|
86
|
+
* pass an empty string for a headingless block. A heading with no body is never volatile: a
|
|
87
|
+
* title cannot be edited often when there is nothing under it to edit.
|
|
88
|
+
*/
|
|
89
|
+
export declare function volatility(heading: string, bodyLines: string[]): Volatility;
|
|
90
|
+
/** Reasons a headingless block is volatile; empty when it is not. */
|
|
91
|
+
export declare function volatilityReasons(text: string): string[];
|
|
92
|
+
/** Reasons a heading-led section is volatile; empty when it is not. */
|
|
93
|
+
export declare function sectionVolatilityReasons(sectionLines: string[]): string[];
|
|
94
|
+
/** Delimiters around the generated regions of a stub: the OnDemandMemory list and the instruction block. */
|
|
95
|
+
export declare const LIST_START = "<!-- minnimemory:ondemand-list -->";
|
|
96
|
+
export declare const LIST_END = "<!-- /minnimemory:ondemand-list -->";
|
|
97
|
+
export declare const INSTRUCTIONS_START = "<!-- minnimemory:instructions -->";
|
|
98
|
+
export declare const INSTRUCTIONS_END = "<!-- /minnimemory:instructions -->";
|
|
99
|
+
/**
|
|
100
|
+
* Locate one generated region. Only a single well-formed pair counts: the first opening marker
|
|
101
|
+
* that is followed by a closing marker. An unclosed opener, a second pair, or a closer with no
|
|
102
|
+
* opener is ignored, so a marker pasted into prose cannot hide the rest of a file from the
|
|
103
|
+
* rules (security audit 2026-09-02, finding 5). Returns 0-based inclusive line indices.
|
|
104
|
+
*/
|
|
105
|
+
export declare function generatedRegion(lines: string[], start: string, end: string): {
|
|
106
|
+
from: number;
|
|
107
|
+
to: number;
|
|
108
|
+
} | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Blank out the generated regions, line for line so line numbers are preserved. The
|
|
111
|
+
* OnDemandMemory list is a routing manifest: its trigger words ("changelog", "open items") are
|
|
112
|
+
* labels for OnDemandMemory files, not status, and it only changes when a file is added or
|
|
113
|
+
* removed. The instruction block is fixed text. Auditing either as prose would make `doctor`
|
|
114
|
+
* flag `init`'s own output.
|
|
115
|
+
*/
|
|
116
|
+
export declare function stripGeneratedRegions(lines: string[]): string[];
|
|
117
|
+
/** Kept for callers that only know about the OnDemandMemory list region. */
|
|
118
|
+
export declare const stripGeneratedList: typeof stripGeneratedRegions;
|
|
119
|
+
/**
|
|
120
|
+
* The inverse of `stripGeneratedRegions`: blank every line outside one marked region, keeping
|
|
121
|
+
* the region's own lines verbatim at their real position. Line numbers stay meaningful for a
|
|
122
|
+
* region carved out of a larger always-loaded file (compile.ts's merged AlwaysOnMemory.md holds
|
|
123
|
+
* always-on prose and the OnDemandMemory list in one file; this is how a caller gets just the
|
|
124
|
+
* list's lines back without losing the line numbers a finding reports against the real file).
|
|
125
|
+
*/
|
|
126
|
+
export declare function regionOnly(lines: string[], start: string, end: string): string[];
|
|
127
|
+
/**
|
|
128
|
+
* Leading YAML frontmatter: a `---` line first, closed by the next `---` line. Returned as the
|
|
129
|
+
* verbatim lines plus the 1-indexed line number where the document body begins. Frontmatter has
|
|
130
|
+
* to stay first in a file to remain frontmatter, so the compiler carries it through untouched
|
|
131
|
+
* instead of treating it as preamble prose.
|
|
132
|
+
*/
|
|
133
|
+
export declare function splitFrontmatter(lines: string[]): {
|
|
134
|
+
frontmatter: string[];
|
|
135
|
+
bodyStartLine: number;
|
|
136
|
+
};
|
|
137
|
+
export declare const STOPWORDS: Set<string>;
|
|
138
|
+
/** A code token's raw frequency inside a fenced block, relative to a comment or prose word. */
|
|
139
|
+
export declare const CODE_WEIGHT = 0.2;
|
|
140
|
+
/**
|
|
141
|
+
* Split body text into (fragment, weight) pieces for keyword extraction: prose and `#` comment
|
|
142
|
+
* text keep weight 1, the command/flag/tool-name text around a comment inside a fenced code
|
|
143
|
+
* block is down-weighted, so a repeated tool name doesn't drown out a rare-but-specific comment
|
|
144
|
+
* word under raw frequency. Found live (2026-09-09): an OnDemandMemory file whose only mentions of "UAC" and
|
|
145
|
+
* "window" were in comments lost to "npm"/"venv"/"pester" appearing many times across unrelated
|
|
146
|
+
* commands sharing one section, so a question phrased the way a user actually asks it ("run
|
|
147
|
+
* without a UAC prompt") matched no trigger at all. Shared by `keywords()` below and
|
|
148
|
+
* `router.ts`'s `selectTriggers()`, the two places that rank candidate routing words this way.
|
|
149
|
+
*/
|
|
150
|
+
export declare function weightedFragments(body: string): {
|
|
151
|
+
text: string;
|
|
152
|
+
weight: number;
|
|
153
|
+
}[];
|
|
154
|
+
/**
|
|
155
|
+
* Pull candidate routing keywords out of a piece of text.
|
|
156
|
+
* Frequency-ranked, stopword-filtered, and biased toward terms in the heading.
|
|
157
|
+
*/
|
|
158
|
+
export declare function keywords(heading: string, body: string, limit?: number): string[];
|