@fonlan/dsh-gitmemo 0.1.5 → 0.3.0

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/lib/mem.d.ts CHANGED
@@ -1,106 +1,315 @@
1
+ /** Schema version stored in `.gitmemo-format`. */
2
+ export declare const GITMEMO_FORMAT_VERSION = "2";
3
+ /** Format marker file tracked in the .mem repo (content = schema version). */
4
+ export declare const FORMAT_FILE = ".gitmemo-format";
5
+ /** Entry directory inside the .mem repo. */
6
+ export declare const ENTRY_DIR = "entries";
7
+ /** Sibling lock file (usable before `.mem` exists). */
8
+ export declare const LOCK_FILE_NAME = ".mem.gitmemo.lock";
9
+ /** Sibling journal used to recover an interrupted migration directory swap. */
10
+ export declare const MIGRATION_JOURNAL_NAME = ".mem.gitmemo-migration.json";
11
+ /** Max entry content size. */
12
+ export declare const MAX_CONTENT_BYTES: number;
13
+ /** Error type thrown by every gitmemo operation. */
14
+ export declare class GitMemoError extends Error {
15
+ }
1
16
  /** Per-invocation engine options. */
2
17
  export interface GitMemoConfig {
3
- /** Name of the memory repository directory at the project root. Default ".mem". */
4
- memDirName?: string;
5
- /** Maximum number of search hits returned per call. Default 20. */
18
+ /** Max search hits returned per call (page size). Default 20. */
6
19
  searchLimit?: number;
7
- /** Whether `.mem` branch follows the project branch on write. Default true. */
8
- branchAlign?: boolean;
9
20
  /** Per git subprocess timeout in milliseconds. Default 60000. */
10
21
  gitTimeoutMs?: number;
11
- /** Cap on git-log batches scanned by one search call. Default 200. */
12
- maxSearchBatches?: number;
22
+ /** Cross-process lock wait timeout in milliseconds. Default 30000. */
23
+ lockTimeoutMs?: number;
24
+ /** @deprecated parsed for compatibility only — the memory dir is fixed at `<projectRoot>/.mem`. */
25
+ memDirName?: string;
26
+ /** @deprecated parsed for compatibility only — `.mem` always stays on main. */
27
+ branchAlign?: boolean;
28
+ /** @deprecated parsed for compatibility only — session-start recent seeding was removed. */
29
+ recentContextLimit?: number;
13
30
  }
14
- /** One search hit, formatted `hash|title|date` like the original script. */
31
+ /** One scored search hit. */
15
32
  export interface SearchHit {
16
33
  hash: string;
17
34
  title: string;
18
35
  date: string;
36
+ summary: string;
37
+ keywords: string[];
38
+ score: number;
39
+ matched_keywords: string[];
40
+ }
41
+ export interface SearchOutput {
42
+ snapshot: string;
43
+ total: number;
44
+ next_skip: number | null;
45
+ results: SearchHit[];
46
+ legacy?: boolean;
47
+ warning?: string;
48
+ diagnostics?: string[];
19
49
  }
20
- export interface WriteOptions {
21
- /** Short "[module] action + object" title (required). */
50
+ export interface SearchOptions {
51
+ skip?: number;
52
+ /** Snapshot returned by the first page — pass back unchanged for stable pagination. */
53
+ snapshot?: string;
54
+ /** @deprecated legacy `mode` — recall is always OR now. */
55
+ mode?: "and" | "or" | "auto";
56
+ }
57
+ export interface WriteInput {
58
+ /** Short "[module] action + object" title (single line, 1-200 chars). */
59
+ title: string;
60
+ /** 1-3 sentence summary (multi-line allowed, <= 4000 chars). */
61
+ summary: string;
62
+ /** 2-12 keywords (1-64 chars each); include 中英文 synonyms. Legacy writes may pass fewer. */
63
+ keywords: string[];
64
+ /** Memory markdown body (engine generates front matter). */
65
+ content: string;
66
+ related_branches?: string[];
67
+ related_paths?: string[];
68
+ }
69
+ /** One-version compatibility shape accepted by the engine, not model-facing tools. */
70
+ export interface LegacyWriteInput {
22
71
  title: string;
23
- /** Entry markdown; provide exactly one of content / contentFile / file. */
72
+ summary?: string;
73
+ keywords?: string[];
24
74
  content?: string;
25
- /** Path to a file holding the entry markdown (temp file or an existing .mem/entries path). */
26
- contentFile?: string;
27
- /** Existing entry path to commit in place: `entries/foo.md`, `.mem/entries/foo.md`, or an absolute path under .mem. */
28
- file?: string;
29
- /** Optional single-line commit body. */
75
+ related_branches?: string[];
76
+ related_paths?: string[];
30
77
  body?: string;
31
- /** Optional path to a file holding the commit body. */
32
- bodyFile?: string;
78
+ body_file?: string;
79
+ content_file?: string;
80
+ /** Old in-place updates conflict with immutable entries and are always rejected. */
81
+ file?: string;
33
82
  }
34
83
  export interface WriteResult {
35
84
  hash: string;
36
85
  file: string;
86
+ legacy?: boolean;
37
87
  }
38
- export interface SearchResult {
39
- mode: "and" | "or";
40
- results: SearchHit[];
88
+ export interface ReadResult {
89
+ hash: string;
90
+ file: string;
91
+ content: string;
92
+ legacy?: boolean;
93
+ }
94
+ export interface DeleteInput {
95
+ commit_hash: string;
96
+ reason: string;
97
+ }
98
+ export interface ReplaceInput extends WriteInput {
99
+ commit_hash: string;
100
+ }
101
+ /** Parsed structured commit message. */
102
+ export interface ParsedCommitMessage {
103
+ subject: string;
104
+ summary: string;
105
+ type: "add" | "replace" | "delete" | "unknown";
106
+ keywords: string[];
107
+ digest?: string;
108
+ deletes?: string;
109
+ replaces?: string;
110
+ legacy: boolean;
111
+ }
112
+ interface LogRecord {
113
+ hash: string;
114
+ committerTime: number;
115
+ subject: string;
116
+ body: string;
117
+ added: string[];
118
+ deleted: string[];
119
+ message: ParsedCommitMessage;
120
+ }
121
+ interface TransactionJournal {
122
+ operationId: string;
123
+ operation: string;
124
+ phase: string;
125
+ baseHead: string;
126
+ add: string[];
127
+ delete: string[];
128
+ digest?: string;
129
+ createdAt: string;
130
+ }
131
+ export interface MigrationSwapJournal {
132
+ operationId: string;
133
+ sourceDir: string;
134
+ targetDir: string;
135
+ oldDir: string;
136
+ tmpDir: string;
137
+ phase: "prepared" | "old-renamed" | "new-installed";
138
+ createdAt: string;
139
+ }
140
+ interface CodeContext {
141
+ branch: string;
142
+ commit: string;
143
+ }
144
+ interface GitOptions {
145
+ input?: string;
146
+ env?: NodeJS.ProcessEnv;
147
+ }
148
+ /** Run one git subprocess (internal; exported for migrate/cli). */
149
+ export declare function git(dir: string, args: string[], gitTimeoutMs: number, opts?: GitOptions): Promise<string>;
150
+ /**
151
+ * Commit a message via stdin, retrying once with a built-in identity when the
152
+ * ambient git configuration has no user.name/user.email (fresh machines,
153
+ * CI, tests). The retry only adds env defaults — it never overrides an
154
+ * explicitly configured identity.
155
+ */
156
+ export declare function commitWithIdentity(memDir: string, message: string, gitTimeoutMs: number, opts?: GitOptions): Promise<string>;
157
+ /** `git init` with an explicit `main` branch (falls back for old git). */
158
+ export declare function gitInitMain(dir: string, gitTimeoutMs: number): Promise<void>;
159
+ /**
160
+ * Resolve the code project root for a session working directory (plan 3.1):
161
+ * explicit config > git work-tree root > session cwd.
162
+ */
163
+ export declare function resolveProjectRoot(cwd: string, gitTimeoutMs?: number): Promise<string>;
164
+ /**
165
+ * Query/write normalization contract: NFKC + Unicode default case folding +
166
+ * whitespace collapsing to single ASCII spaces. Both query keywords and the
167
+ * `GitMemo-Search-Text` projections use this exact function, so the first
168
+ * grep stage never depends on the platform locale.
169
+ */
170
+ export declare function normalizeText(input: string): string;
171
+ export declare function normalizeKeyword(input: string): string;
172
+ export interface ValidatedWriteInput {
173
+ title: string;
174
+ summary: string;
175
+ keywords: string[];
176
+ content: string;
177
+ related_branches: string[];
178
+ related_paths: string[];
41
179
  }
42
- /** True when a path points inside (or at) the memory entries directory. */
43
- declare function isEntriesPath(memRoot: string, full: string): boolean;
180
+ /**
181
+ * Validate a write/replace payload (plan 5.3). `legacy` relaxes the keyword
182
+ * minimum to 0 (legacy adapter and migration entries); everything else is
183
+ * enforced identically.
184
+ */
185
+ export declare function validateWriteInput(input: WriteInput, legacy?: boolean): ValidatedWriteInput;
186
+ /**
187
+ * Stable digest over the semantic payload only: title + summary +
188
+ * normalized+sorted keywords + content. Write time, code branch/SHA and
189
+ * related branches/paths are excluded, so the same conclusion never produces
190
+ * an exact duplicate because of when/where it was written, and a withdrawn
191
+ * conclusion can be re-written later.
192
+ */
193
+ export declare function computeDigest(title: string, summary: string, keywords: string[], content: string): string;
194
+ /** UTC millisecond timestamp for filenames: `20260823T113612.123Z`. */
195
+ export declare function utcMs(date: Date): string;
196
+ /** ISO date for a committer timestamp; falls back to the epoch for out-of-range values. */
197
+ export declare function safeIsoDate(committerTime: number): string;
198
+ export declare function slugifyTitle(title: string): string;
199
+ /**
200
+ * Create an entry file under entries/ with an exclusive create; on collision
201
+ * append a numeric suffix. Never overwrites an existing file.
202
+ */
203
+ export declare function createEntryFile(memDir: string, baseName: string, content: string): Promise<string>;
204
+ /** Build the full entry markdown (engine-generated front matter + body). */
205
+ export declare function buildEntryMarkdown(input: ValidatedWriteInput, ctx: CodeContext & {
206
+ relatedBranches: string[];
207
+ }, digest: string, date: Date): string;
208
+ /** ADD commit message (plan 4.1). */
209
+ export declare function buildAddMessage(title: string, summary: string, keywords: string[], digest: string, legacy?: boolean): string;
210
+ /** DELETE commit message (plan 4.2). */
211
+ export declare function buildDeleteMessage(entryName: string, reason: string, deletesHash: string): string;
212
+ /** REPLACE commit message (plan 4.3). */
213
+ export declare function buildReplaceMessage(title: string, summary: string, keywords: string[], digest: string, replacesHash: string): string;
214
+ /**
215
+ * Parse a commit message into logical search fields. The trailer block is the
216
+ * contiguous run at the end; everything between subject and the block is the
217
+ * summary. Malformed input degrades to `type: "unknown"` (legacy commits).
218
+ */
219
+ export declare function parseCommitMessage(raw: string): ParsedCommitMessage;
220
+ /**
221
+ * Parse the NUL-delimited log stream
222
+ * (`--format=%x00%H%x00%ct%x00%s%x00%B%x00 --name-status --no-renames`).
223
+ * Every commit contributes exactly five NUL-separated tokens followed by the
224
+ * status tail, so records align rigidly regardless of newlines in bodies
225
+ * (git itself forbids NUL inside commit messages).
226
+ */
227
+ export declare function parseLogRecords(output: string): LogRecord[];
228
+ export declare function writeMigrationSwapJournal(root: string, journal: MigrationSwapJournal): Promise<void>;
229
+ /**
230
+ * Recover the narrow two-rename migration swap window before any operation is
231
+ * allowed to auto-initialize `.mem`. This prevents a crash between renames
232
+ * from being mistaken for a genuinely fresh project.
233
+ */
234
+ export declare function recoverMigrationSwap(root: string, gitTimeoutMs: number): Promise<void>;
235
+ /** Detect the repo format from `.gitmemo-format` at the given snapshot. */
236
+ export declare function repoFormat(memDir: string, gitTimeoutMs: number, snapshot?: string): Promise<"new" | "legacy">;
237
+ /** Resolve a ref/hash to a full commit hash or throw. */
238
+ export declare function resolveCommit(memDir: string, ref: string, gitTimeoutMs: number): Promise<string>;
239
+ /** Entry files added by one commit (single ADD/REPLACE mapping). */
240
+ export declare function addedFilesOf(memDir: string, hash: string, gitTimeoutMs: number): Promise<string[]>;
241
+ /**
242
+ * Cross-process transaction lock (plan 8.1): exclusive create of a sibling
243
+ * lock file, backed by an in-process mutex on the same canonical key. Used by
244
+ * init/write/delete/replace/migrate; search/read take it briefly to resolve
245
+ * their immutable snapshot (read barrier) and release before scanning.
246
+ */
247
+ export declare function withLock<T>(root: string, opts: {
248
+ lockTimeoutMs: number;
249
+ }, operation: string, fn: () => Promise<T>): Promise<T>;
250
+ /**
251
+ * Strict journal recovery (plan 8.3): only when HEAD still equals the
252
+ * journal's base and the worktree/index changes are exactly the planned
253
+ * paths is the rollback performed; anything else aborts with repair guidance.
254
+ */
255
+ export declare function rollbackJournal(memDir: string, journal: TransactionJournal, gitTimeoutMs: number): Promise<void>;
256
+ /**
257
+ * Initialize a new-format `.mem` repo (plan 8.4): build the whole repo in a
258
+ * sibling temp directory (init commit carries `.gitmemo-format` + `.gitkeep`
259
+ * on `main`), then atomically rename it into place. Never converts an
260
+ * existing legacy repo. Idempotent for already-initialized repos.
261
+ */
262
+ export declare function ensureInit(root: string, memDir: string, gitTimeoutMs: number): Promise<void>;
44
263
  /**
45
264
  * Git-backed long-term memory engine operating on one project root.
265
+ * Every operation auto-initializes a new-format repo; legacy repos are
266
+ * read-only for search/read until migrated via the CLI.
46
267
  */
47
268
  export declare class GitMemo {
48
269
  readonly root: string;
49
270
  readonly memDir: string;
50
271
  private readonly config;
51
272
  constructor(root: string, config?: GitMemoConfig);
52
- /** The current branch of a git work tree, "main" when detached/unavailable. */
53
- private currentBranch;
54
- private projectBranch;
55
- private memBranch;
273
+ /** Initialize the memory repo when missing (idempotent). */
274
+ init(): Promise<string>;
56
275
  /**
57
- * Initialize the memory repo when missing (git init + .gitkeep commit).
58
- * Idempotent; every other operation auto-initializes.
276
+ * Search past memories (plan 6): fixed-string OR grep recall over commit
277
+ * messages only, active-entry filtering, field-level matching and scoring,
278
+ * snapshot-stable pagination, in-process LRU.
279
+ *
280
+ * @param keywords - 1-12 keywords (legacy callers may pass a comma-separated string).
281
+ * @param options.skip - pagination offset.
282
+ * @param options.snapshot - pass back the snapshot from the first page.
59
283
  */
60
- init(): Promise<string>;
61
- /** Align the memory repo branch with the project branch (create if needed). */
62
- private syncBranch;
284
+ search(keywords: string[] | string, options?: SearchOptions): Promise<SearchOutput>;
285
+ private computeSearch;
63
286
  /**
64
- * Search past memories.
65
- * @param keywords - comma-separated keywords.
66
- * @param skip - pagination offset (default 0).
67
- * @param mode - "and" (strict), "or" (broad), or "auto" (AND first, OR fallback).
287
+ * Read one memory entry by create/replace commit hash. Historical entries
288
+ * (deleted or replaced) stay readable for audit; `mem_read` accepts legacy
289
+ * hashes too.
68
290
  */
69
- search(keywords: string, skip?: number, mode?: "and" | "or" | "auto"): Promise<SearchResult>;
70
- private runSearchMode;
291
+ read(commitHash: string): Promise<ReadResult>;
71
292
  /**
72
- * List the most recent memory entries across all branches, newest first.
73
- * Used to seed a new session's system prompt with a compact overview.
74
- * @param limit - maximum number of entries to return (0 returns none).
293
+ * Write a memory entry (plan 5.3). Entries are immutable: each write
294
+ * creates a brand-new file `entries/<utc-ms>-<digest-prefix>-<slug>.md`
295
+ * with one ADD commit. `legacy = true` relaxes the keyword minimum to 0
296
+ * (compatibility adapter + migration reimports).
75
297
  */
76
- recent(limit: number): Promise<SearchHit[]>;
77
- /** Active (non-deleted) entry files at HEAD. */
78
- private activeEntries;
79
- /** Shared git-log scan: keyword-grep optional, active-entry filtering, skip/limit. */
80
- private scanLog;
298
+ write(input: WriteInput | LegacyWriteInput, legacy?: boolean): Promise<WriteResult>;
81
299
  /**
82
- * Synchronous variant of {@link recent} for the session-start injection
83
- * path: blocks until the memory repo is scanned so the system-prompt
84
- * context can be registered before the first request assembles.
300
+ * Delete a memory entry (plan 5.4). Only withdraws the ACTIVE entry of a
301
+ * create/replace hash; stale hashes are rejected.
85
302
  */
86
- recentSync(limit: number): SearchHit[];
87
- private acceptRecord;
88
- /** Resolve the entry file touched by a commit (first file under entries/). */
89
- private entryFileFromCommit;
90
- /** Read one memory entry by commit hash. */
91
- read(commitHash: string): Promise<{
303
+ delete(input: DeleteInput): Promise<{
92
304
  file: string;
93
- content: string;
94
305
  }>;
95
306
  /**
96
- * Write a memory entry.
97
- * Mirrors mem.sh write: commit a markdown entry under entries/ with an
98
- * optional body, aligning the .mem branch with the project branch first.
307
+ * Replace an active entry (plan 5.5): one transaction, one commit deletes
308
+ * the old file and adds the new one (never delete-then-write). The active
309
+ * digest dedupe excludes the entry being replaced, so replacing with the
310
+ * same digest but updated non-digest metadata (related branches/paths) is
311
+ * legal; colliding with any OTHER active entry is rejected.
99
312
  */
100
- write(options: WriteOptions): Promise<WriteResult>;
101
- /** Delete a memory entry by commit hash. */
102
- delete(commitHash: string): Promise<string>;
313
+ replace(input: ReplaceInput): Promise<WriteResult>;
103
314
  }
104
- /** Initialize the memory repo when missing. */
105
- export declare function ensureInit(memDir: string, gitTimeoutMs?: number): Promise<void>;
106
- export { isEntriesPath };
315
+ export {};