@fingerskier/augment 0.3.1 → 0.4.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/dist/cli.d.ts +9 -6
- package/dist/cli.js +9 -43
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +7 -0
- package/dist/constants.js.map +1 -1
- package/dist/daemon/client.d.ts +4 -2
- package/dist/daemon/client.js +12 -5
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/http.js +93 -28
- package/dist/daemon/http.js.map +1 -1
- package/dist/db.d.ts +1 -0
- package/dist/db.js +35 -4
- package/dist/db.js.map +1 -1
- package/dist/install.d.ts +2 -0
- package/dist/install.js +68 -5
- package/dist/install.js.map +1 -1
- package/dist/mcp/apps.js +0 -16
- package/dist/mcp/apps.js.map +1 -1
- package/dist/mcp/insights.d.ts +4 -6
- package/dist/mcp/insights.js +4 -11
- package/dist/mcp/insights.js.map +1 -1
- package/dist/mcp/server.js +43 -23
- package/dist/mcp/server.js.map +1 -1
- package/dist/memory/files.js +11 -0
- package/dist/memory/files.js.map +1 -1
- package/dist/memory/git.d.ts +5 -1
- package/dist/memory/git.js +7 -3
- package/dist/memory/git.js.map +1 -1
- package/dist/pi/extension.d.ts +4 -2
- package/dist/pi/extension.js +29 -47
- package/dist/pi/extension.js.map +1 -1
- package/dist/proposals.d.ts +42 -0
- package/dist/proposals.js +179 -0
- package/dist/proposals.js.map +1 -0
- package/dist/recall-log.d.ts +21 -0
- package/dist/recall-log.js +24 -0
- package/dist/recall-log.js.map +1 -0
- package/dist/recall.d.ts +1 -1
- package/dist/service.d.ts +63 -0
- package/dist/service.js +204 -3
- package/dist/service.js.map +1 -1
- package/dist/types.d.ts +26 -2
- package/docs/FEATURES.md +98 -27
- package/integrations/claude/skills/augment-context/SKILL.md +74 -0
- package/integrations/claude/skills/augment-sleep/SKILL.md +26 -0
- package/package.json +14 -14
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
import { normalizeKind, normalizeLinkType } from "./memory/files.js";
|
|
5
|
+
import { safeSegment } from "./memory/project.js";
|
|
6
|
+
/** sleep -> `<stateDir>/sleeps`; dream-memory | dream-link -> `<stateDir>/dreams`. */
|
|
7
|
+
export function proposalsDir(stateDir, type) {
|
|
8
|
+
return path.join(stateDir, type === "sleep" ? "sleeps" : "dreams");
|
|
9
|
+
}
|
|
10
|
+
export async function writeProposal(stateDir, fm, body) {
|
|
11
|
+
const dir = proposalsDir(stateDir, fm.type);
|
|
12
|
+
await mkdir(dir, { recursive: true });
|
|
13
|
+
const now = new Date().toISOString();
|
|
14
|
+
// Conditionally spread optional keys so an unset field is omitted from the
|
|
15
|
+
// object entirely (and thus from the written YAML) instead of being
|
|
16
|
+
// written as an explicit `null` -- mirrors normalizeFrontmatter's
|
|
17
|
+
// consolidated_into/derived_from handling in memory/files.ts.
|
|
18
|
+
const frontmatter = {
|
|
19
|
+
type: fm.type,
|
|
20
|
+
project: fm.project,
|
|
21
|
+
status: "proposed",
|
|
22
|
+
...(fm.kind !== undefined ? { kind: fm.kind } : {}),
|
|
23
|
+
...(fm.name !== undefined ? { name: fm.name } : {}),
|
|
24
|
+
derived_from: fm.derived_from,
|
|
25
|
+
...(fm.link !== undefined ? { link: fm.link } : {}),
|
|
26
|
+
...(fm.rationale !== undefined ? { rationale: fm.rationale } : {}),
|
|
27
|
+
created_at: now,
|
|
28
|
+
updated_at: now,
|
|
29
|
+
};
|
|
30
|
+
const stamp = timestampSegment(new Date());
|
|
31
|
+
const base = `${stamp}-${safeSegment(fm.name ?? "link")}`;
|
|
32
|
+
const filename = await uniqueFilename(dir, base);
|
|
33
|
+
const trimmedBody = body.trim();
|
|
34
|
+
await writeFile(path.join(dir, filename), matter.stringify(`${trimmedBody}\n`, frontmatter), "utf8");
|
|
35
|
+
return { frontmatter, body: trimmedBody, filename };
|
|
36
|
+
}
|
|
37
|
+
export async function readProposal(stateDir, type, filename) {
|
|
38
|
+
assertSafeFilename(filename);
|
|
39
|
+
const dir = proposalsDir(stateDir, type);
|
|
40
|
+
const raw = await readFile(path.join(dir, filename), "utf8");
|
|
41
|
+
const parsed = matter(raw);
|
|
42
|
+
return {
|
|
43
|
+
frontmatter: normalizeProposalFrontmatter(parsed.data),
|
|
44
|
+
body: parsed.content.trim(),
|
|
45
|
+
filename,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export async function listProposals(stateDir, type, status) {
|
|
49
|
+
const dir = proposalsDir(stateDir, type);
|
|
50
|
+
let entries;
|
|
51
|
+
try {
|
|
52
|
+
entries = await readdir(dir);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code === "ENOENT") {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
const filenames = entries.filter((entry) => entry.endsWith(".md")).sort();
|
|
61
|
+
const proposals = [];
|
|
62
|
+
for (const filename of filenames) {
|
|
63
|
+
try {
|
|
64
|
+
const proposal = await readProposal(stateDir, type, filename);
|
|
65
|
+
if (!status || proposal.frontmatter.status === status) {
|
|
66
|
+
proposals.push(proposal);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.warn(`Skipping unparseable proposal ${path.join(dir, filename)}: ${error.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return proposals;
|
|
74
|
+
}
|
|
75
|
+
export async function setProposalStatus(stateDir, type, filename, status) {
|
|
76
|
+
const existing = await readProposal(stateDir, type, filename);
|
|
77
|
+
const frontmatter = {
|
|
78
|
+
...existing.frontmatter,
|
|
79
|
+
status,
|
|
80
|
+
updated_at: new Date().toISOString(),
|
|
81
|
+
};
|
|
82
|
+
const dir = proposalsDir(stateDir, type);
|
|
83
|
+
await writeFile(path.join(dir, filename), matter.stringify(`${existing.body}\n`, frontmatter), "utf8");
|
|
84
|
+
return { frontmatter, body: existing.body, filename };
|
|
85
|
+
}
|
|
86
|
+
export function renderProposalsText(proposals, type) {
|
|
87
|
+
if (proposals.length === 0) {
|
|
88
|
+
return `No ${type} proposals.\n`;
|
|
89
|
+
}
|
|
90
|
+
let out = `${type} proposals (${proposals.length}):\n`;
|
|
91
|
+
proposals.forEach((proposal, index) => {
|
|
92
|
+
const fm = proposal.frontmatter;
|
|
93
|
+
const subject = fm.link
|
|
94
|
+
? `link ${fm.link.from} -> ${fm.link.to} (${fm.link.type})`
|
|
95
|
+
: `${fm.kind ?? "?"} ${fm.name ?? "?"}`;
|
|
96
|
+
out += `\n[${index + 1}] ${proposal.filename} status:${fm.status} project:${fm.project}\n`;
|
|
97
|
+
out += ` ${subject}\n`;
|
|
98
|
+
out += ` derived_from: ${fm.derived_from.length > 0 ? fm.derived_from.join(", ") : "(none)"}\n`;
|
|
99
|
+
if (fm.rationale) {
|
|
100
|
+
out += ` rationale: ${fm.rationale}\n`;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
// yyyymmdd-hhmmss in UTC, so lexical filename sort matches chronological order
|
|
106
|
+
// regardless of the host machine's local timezone.
|
|
107
|
+
function timestampSegment(date) {
|
|
108
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
109
|
+
const y = date.getUTCFullYear();
|
|
110
|
+
const mo = pad(date.getUTCMonth() + 1);
|
|
111
|
+
const d = pad(date.getUTCDate());
|
|
112
|
+
const h = pad(date.getUTCHours());
|
|
113
|
+
const mi = pad(date.getUTCMinutes());
|
|
114
|
+
const s = pad(date.getUTCSeconds());
|
|
115
|
+
return `${y}${mo}${d}-${h}${mi}${s}`;
|
|
116
|
+
}
|
|
117
|
+
async function uniqueFilename(dir, base) {
|
|
118
|
+
let candidate = `${base}.md`;
|
|
119
|
+
let attempt = 2;
|
|
120
|
+
while (await pathExists(path.join(dir, candidate))) {
|
|
121
|
+
candidate = `${base}-${attempt}.md`;
|
|
122
|
+
attempt += 1;
|
|
123
|
+
}
|
|
124
|
+
return candidate;
|
|
125
|
+
}
|
|
126
|
+
async function pathExists(absolutePath) {
|
|
127
|
+
try {
|
|
128
|
+
await stat(absolutePath);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Security: filenames come from callers (including MCP tool args in later
|
|
136
|
+
// tasks) and are joined directly onto proposalsDir(). Reject anything that
|
|
137
|
+
// could escape that directory -- a path separator (either slash, since this
|
|
138
|
+
// repo runs on Windows too) or a bare "." / ".." segment.
|
|
139
|
+
function assertSafeFilename(filename) {
|
|
140
|
+
if (!filename || /[\\/]/.test(filename) || filename === "." || filename === "..") {
|
|
141
|
+
throw new Error(`Invalid proposal filename: ${filename}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Defensive re-parse of raw YAML frontmatter data. Proposals are
|
|
145
|
+
// user-editable on disk, and gray-matter's underlying YAML parser will turn
|
|
146
|
+
// an unquoted ISO-looking date into a native Date object rather than a
|
|
147
|
+
// string -- coerce dates back to strings the same way
|
|
148
|
+
// memory/files.ts#normalizeFrontmatter does. kind/link.type are revalidated
|
|
149
|
+
// through the existing normalizeKind/normalizeLinkType so a hand-edited typo
|
|
150
|
+
// fails loudly instead of silently corrupting the frontmatter.
|
|
151
|
+
function normalizeProposalFrontmatter(data) {
|
|
152
|
+
const frontmatter = {
|
|
153
|
+
type: data.type,
|
|
154
|
+
project: String(data.project ?? ""),
|
|
155
|
+
status: data.status,
|
|
156
|
+
derived_from: Array.isArray(data.derived_from) ? data.derived_from.map(String) : [],
|
|
157
|
+
created_at: String(data.created_at ?? ""),
|
|
158
|
+
updated_at: String(data.updated_at ?? ""),
|
|
159
|
+
};
|
|
160
|
+
if (data.kind !== undefined) {
|
|
161
|
+
frontmatter.kind = normalizeKind(String(data.kind));
|
|
162
|
+
}
|
|
163
|
+
if (data.name !== undefined) {
|
|
164
|
+
frontmatter.name = String(data.name);
|
|
165
|
+
}
|
|
166
|
+
if (data.link && typeof data.link === "object") {
|
|
167
|
+
const link = data.link;
|
|
168
|
+
frontmatter.link = {
|
|
169
|
+
from: String(link.from ?? ""),
|
|
170
|
+
to: String(link.to ?? ""),
|
|
171
|
+
type: normalizeLinkType(String(link.type)),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (data.rationale !== undefined) {
|
|
175
|
+
frontmatter.rationale = String(data.rationale);
|
|
176
|
+
}
|
|
177
|
+
return frontmatter;
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=proposals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proposals.js","sourceRoot":"","sources":["../src/proposals.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passive per-search recall log. Successor to the Phase-2 dogfood tally
|
|
3
|
+
* (removed per GH issue #3): the human-rating surface is gone, but the
|
|
4
|
+
* per-search event log stays — it is the instrument that caught the
|
|
5
|
+
* 2026-07-10 memoryRoot split-brain. Best-effort by design: a failed append
|
|
6
|
+
* must never fail the search that triggered it.
|
|
7
|
+
*/
|
|
8
|
+
export declare const RECALL_LOG_MAX_BYTES = 5000000;
|
|
9
|
+
/** One automatically-logged injection event (what search actually returned). */
|
|
10
|
+
export interface RecallEvent {
|
|
11
|
+
ts: string;
|
|
12
|
+
surface: string;
|
|
13
|
+
project?: string;
|
|
14
|
+
query: string;
|
|
15
|
+
abstained: boolean;
|
|
16
|
+
result_count: number;
|
|
17
|
+
top_score?: number;
|
|
18
|
+
paths: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare function eventsPath(stateDir: string): string;
|
|
21
|
+
export declare function appendRecallEvent(stateDir: string, event: RecallEvent): Promise<void>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { appendFile, mkdir, rename, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Passive per-search recall log. Successor to the Phase-2 dogfood tally
|
|
5
|
+
* (removed per GH issue #3): the human-rating surface is gone, but the
|
|
6
|
+
* per-search event log stays — it is the instrument that caught the
|
|
7
|
+
* 2026-07-10 memoryRoot split-brain. Best-effort by design: a failed append
|
|
8
|
+
* must never fail the search that triggered it.
|
|
9
|
+
*/
|
|
10
|
+
export const RECALL_LOG_MAX_BYTES = 5_000_000;
|
|
11
|
+
export function eventsPath(stateDir) {
|
|
12
|
+
return path.join(stateDir, "recall", "events.jsonl");
|
|
13
|
+
}
|
|
14
|
+
export async function appendRecallEvent(stateDir, event) {
|
|
15
|
+
const file = eventsPath(stateDir);
|
|
16
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
17
|
+
// Single-slot rotation: the log is diagnostic, not a record of account.
|
|
18
|
+
const size = await stat(file).then((s) => s.size).catch(() => 0);
|
|
19
|
+
if (size > RECALL_LOG_MAX_BYTES) {
|
|
20
|
+
await rename(file, `${file}.1`).catch(() => { });
|
|
21
|
+
}
|
|
22
|
+
await appendFile(file, `${JSON.stringify(event)}\n`, "utf8");
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=recall-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recall-log.js","sourceRoot":"","sources":["../src/recall-log.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,oBAAoB,GAAG,SAAS,CAAC;AAc9C,MAAM,UAAU,UAAU,CAAC,QAAgB;IACzC,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,QAAgB,EAAE,KAAkB;IAC1E,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAClC,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrD,wEAAwE;IACxE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;IACjE,IAAI,IAAI,GAAG,oBAAoB,EAAE,CAAC;QAChC,MAAM,MAAM,CAAC,IAAI,EAAE,GAAG,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAClD,CAAC;IACD,MAAM,UAAU,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC/D,CAAC"}
|
package/dist/recall.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface RecallOptions {
|
|
|
11
11
|
maxChars?: number;
|
|
12
12
|
/** Minimum relevance score (0..1); 0 disables the floor. */
|
|
13
13
|
minScore?: number;
|
|
14
|
-
/** Injection surface tag
|
|
14
|
+
/** Injection surface tag recorded in the per-search recall log (see src/recall-log.ts). */
|
|
15
15
|
surface?: string;
|
|
16
16
|
}
|
|
17
17
|
/** The slice of the daemon client `recall` depends on — kept narrow so tests can inject a fake. */
|
package/dist/service.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AugmentConfig } from "./config.js";
|
|
2
2
|
import { AugmentDatabase } from "./db.js";
|
|
3
3
|
import type { EmbeddingProvider } from "./embeddings.js";
|
|
4
|
+
import { type Proposal, type ProposalStatus } from "./proposals.js";
|
|
4
5
|
import { type SleepCluster } from "./sleep.js";
|
|
5
6
|
import type { LinkRecord, MemoryRecord, ProjectGraph, ProjectRecord, SearchInput, SearchResult, UpsertInput } from "./types.js";
|
|
6
7
|
export interface InitProjectInput {
|
|
@@ -28,6 +29,26 @@ export interface SleepCandidatesOutput {
|
|
|
28
29
|
clusters: SleepCluster[];
|
|
29
30
|
eligible: number;
|
|
30
31
|
}
|
|
32
|
+
/** Draft a sleep consolidation: >=2 WORK sources fold into one new SPEC/ARCH.
|
|
33
|
+
* The proposal is written OUTSIDE the corpus for review; nothing is consolidated
|
|
34
|
+
* until {@link AugmentService.sleepApply}. */
|
|
35
|
+
export interface SleepProposeInput {
|
|
36
|
+
project_name: string;
|
|
37
|
+
kind: "SPEC" | "ARCH";
|
|
38
|
+
name: string;
|
|
39
|
+
content: string;
|
|
40
|
+
derived_from: string[];
|
|
41
|
+
rationale?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface SleepApplyInput {
|
|
44
|
+
filename: string;
|
|
45
|
+
}
|
|
46
|
+
export interface SleepApplyOutput {
|
|
47
|
+
memory: MemoryRecord;
|
|
48
|
+
archived: string[];
|
|
49
|
+
proposal: Proposal;
|
|
50
|
+
text: string;
|
|
51
|
+
}
|
|
31
52
|
export declare class AugmentService {
|
|
32
53
|
private readonly config;
|
|
33
54
|
private readonly embeddingProvider;
|
|
@@ -112,6 +133,48 @@ export declare class AugmentService {
|
|
|
112
133
|
* behind the Phase-2 go/no-go and does not exist.
|
|
113
134
|
*/
|
|
114
135
|
sleepCandidates(input?: SleepCandidatesInput, now?: number): Promise<SleepCandidatesOutput>;
|
|
136
|
+
/**
|
|
137
|
+
* Draft a sleep consolidation as a review artifact OUTSIDE the corpus. Writes
|
|
138
|
+
* nothing to the memory store: the corpus is only mutated by {@link sleepApply}
|
|
139
|
+
* on an explicitly approved proposal. Validates the sources up front (same
|
|
140
|
+
* checks apply re-runs against the live corpus) so an impossible proposal fails
|
|
141
|
+
* loudly at draft time.
|
|
142
|
+
*/
|
|
143
|
+
sleepPropose(input: SleepProposeInput): Promise<{
|
|
144
|
+
proposal: Proposal;
|
|
145
|
+
text: string;
|
|
146
|
+
}>;
|
|
147
|
+
sleepProposals(status?: ProposalStatus): Promise<{
|
|
148
|
+
proposals: Proposal[];
|
|
149
|
+
text: string;
|
|
150
|
+
}>;
|
|
151
|
+
/**
|
|
152
|
+
* Apply an approved sleep proposal — the ONLY corpus mutation in the flow. The
|
|
153
|
+
* sequence is a contract: read the proposal fresh from disk (user edits count),
|
|
154
|
+
* require it still be `proposed`, re-validate against the LIVE corpus (drafts
|
|
155
|
+
* sit while the corpus moves underneath them), upsert the consolidated record
|
|
156
|
+
* (provenance threaded into frontmatter), archive each source in place (body
|
|
157
|
+
* untouched, verbatim), then flip the proposal to `applied`. Does NOT commit
|
|
158
|
+
* git: the route commits the whole pass as one revertible commit.
|
|
159
|
+
*/
|
|
160
|
+
sleepApply(input: SleepApplyInput): Promise<SleepApplyOutput>;
|
|
161
|
+
private sleepApplyCore;
|
|
162
|
+
sleepReject(filename: string): Promise<{
|
|
163
|
+
proposal: Proposal;
|
|
164
|
+
text: string;
|
|
165
|
+
}>;
|
|
166
|
+
/**
|
|
167
|
+
* Shared sleep validation, run at propose time and re-run against the live
|
|
168
|
+
* corpus at apply time. Every source must resolve, be a WORK memory in
|
|
169
|
+
* `projectName` (single-project constraint), and be unarchived; there must be
|
|
170
|
+
* >=2 of them; and the consolidation target must not already exist. Every
|
|
171
|
+
* violation names the offending path.
|
|
172
|
+
*/
|
|
173
|
+
private validateSleepSources;
|
|
174
|
+
/** Rewrite one source's frontmatter to archived + consolidated_into, leaving
|
|
175
|
+
* the body verbatim, and reindex it. Mirrors {@link scrubInboundLinks}. Returns
|
|
176
|
+
* the source's relative path. */
|
|
177
|
+
private archiveSource;
|
|
115
178
|
private findSimilarMemory;
|
|
116
179
|
private ensureEmbedding;
|
|
117
180
|
}
|
package/dist/service.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { DEFAULT_MEMORY_HARD_LIMIT_CHARS, DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_MAX_CHARS, DEFAULT_SEARCH_MIN_SCORE, SEMANTIC_COSINE_BASELINE, SEMANTIC_COSINE_MATCH, SLEEP_MAX_CLUSTERS, SLEEP_MIN_AGE_DAYS, SLEEP_MIN_COSINE, } from "./constants.js";
|
|
3
|
+
import { ARCHIVED_SEARCH_WEIGHT, DEFAULT_MEMORY_HARD_LIMIT_CHARS, DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_MAX_CHARS, DEFAULT_SEARCH_MIN_SCORE, SEMANTIC_COSINE_BASELINE, SEMANTIC_COSINE_MATCH, SLEEP_MAX_CLUSTERS, SLEEP_MIN_AGE_DAYS, SLEEP_MIN_COSINE, } from "./constants.js";
|
|
4
4
|
import { ensureConfigDirs } from "./config.js";
|
|
5
5
|
import { AugmentDatabase } from "./db.js";
|
|
6
6
|
import { TransformersEmbeddingProvider } from "./embeddings.js";
|
|
7
7
|
import { deleteMemoryFile, normalizeKind, normalizeLinkType, normalizeRelativePath, memoryRelativePath, readMemory, writeMemory } from "./memory/files.js";
|
|
8
8
|
import { inferProjectName } from "./memory/project.js";
|
|
9
9
|
import { findMemoryFiles } from "./memory/scan.js";
|
|
10
|
+
import { listProposals, readProposal, renderProposalsText, setProposalStatus, writeProposal, } from "./proposals.js";
|
|
10
11
|
import { clusterSleepCandidates, renderSleepCandidatesText } from "./sleep.js";
|
|
11
12
|
import { clamp01, cosineSimilarity, lexicalScoreWith, tokens } from "./util/text.js";
|
|
12
13
|
/** How long a self-write marker stays claimable. Long enough to cover the watcher's
|
|
@@ -204,7 +205,9 @@ export class AugmentService {
|
|
|
204
205
|
// memory at its deterministic path instead. Both versions stay
|
|
205
206
|
// recoverable from the store and the prior body never pollutes the new
|
|
206
207
|
// memory's embedding/lexical document.
|
|
207
|
-
if (
|
|
208
|
+
if (!input.skip_similarity_fold &&
|
|
209
|
+
similar &&
|
|
210
|
+
lexicalScoreWith(new Set(tokens(similar.body)), input.content) >= 1) {
|
|
208
211
|
existing = similar;
|
|
209
212
|
relativePath = similar.relative_path;
|
|
210
213
|
action = "similar-updated";
|
|
@@ -220,6 +223,9 @@ export class AugmentService {
|
|
|
220
223
|
links: existing.links,
|
|
221
224
|
created_at: existing.created_at,
|
|
222
225
|
updated_at: now,
|
|
226
|
+
archived: existing.archived,
|
|
227
|
+
consolidated_into: existing.consolidated_into,
|
|
228
|
+
derived_from: input.derived_from ?? existing.derived_from,
|
|
223
229
|
}
|
|
224
230
|
: {
|
|
225
231
|
project: project.name,
|
|
@@ -229,6 +235,7 @@ export class AugmentService {
|
|
|
229
235
|
links: [],
|
|
230
236
|
created_at: now,
|
|
231
237
|
updated_at: now,
|
|
238
|
+
derived_from: input.derived_from,
|
|
232
239
|
};
|
|
233
240
|
await writeMemory(this.config.memoryRoot, relativePath, normalizeMemoryFrontmatter(metadata), input.content);
|
|
234
241
|
this.recordSelfWrite(relativePath);
|
|
@@ -306,6 +313,9 @@ export class AugmentService {
|
|
|
306
313
|
links: memory.links.filter((link) => link.to !== targetPath),
|
|
307
314
|
created_at: memory.created_at,
|
|
308
315
|
updated_at: new Date().toISOString(),
|
|
316
|
+
archived: memory.archived,
|
|
317
|
+
consolidated_into: memory.consolidated_into,
|
|
318
|
+
derived_from: memory.derived_from,
|
|
309
319
|
});
|
|
310
320
|
await writeMemory(this.config.memoryRoot, memory.relative_path, frontmatter, memory.body);
|
|
311
321
|
this.recordSelfWrite(memory.relative_path);
|
|
@@ -353,6 +363,9 @@ export class AugmentService {
|
|
|
353
363
|
links: mergeFrontmatterLink(from.links, { to: to.relative_path, type }),
|
|
354
364
|
created_at: from.created_at,
|
|
355
365
|
updated_at: new Date().toISOString(),
|
|
366
|
+
archived: from.archived,
|
|
367
|
+
consolidated_into: from.consolidated_into,
|
|
368
|
+
derived_from: from.derived_from,
|
|
356
369
|
});
|
|
357
370
|
await writeMemory(this.config.memoryRoot, from.relative_path, frontmatter, from.body);
|
|
358
371
|
this.recordSelfWrite(from.relative_path);
|
|
@@ -396,6 +409,9 @@ export class AugmentService {
|
|
|
396
409
|
links: from.links.filter((candidate) => !(candidate.to === link.to_relative_path && candidate.type === link.type)),
|
|
397
410
|
created_at: from.created_at,
|
|
398
411
|
updated_at: new Date().toISOString(),
|
|
412
|
+
archived: from.archived,
|
|
413
|
+
consolidated_into: from.consolidated_into,
|
|
414
|
+
derived_from: from.derived_from,
|
|
399
415
|
});
|
|
400
416
|
await writeMemory(this.config.memoryRoot, from.relative_path, frontmatter, from.body);
|
|
401
417
|
this.recordSelfWrite(from.relative_path);
|
|
@@ -462,7 +478,15 @@ export class AugmentService {
|
|
|
462
478
|
const semanticScore = queryEmbedding ? normalizedCosine(queryEmbedding, embeddings.get(memory.id)) : 0;
|
|
463
479
|
const lexScore = lexicalScoreWith(queryTokens, searchDocument(memory));
|
|
464
480
|
const metadataScore = metadataScoreFor(input.project_name, memory);
|
|
465
|
-
|
|
481
|
+
let score = clamp01(semanticScore * 0.55 + lexScore * 0.25 + metadataScore * 0.1);
|
|
482
|
+
// Down-weight (never exclude) an archived memory's own relevance, after
|
|
483
|
+
// the blend and before the relevance floor below -- so a down-weighted
|
|
484
|
+
// score CAN fall under the floor and be abstained on, but read() still
|
|
485
|
+
// returns its full body verbatim (archival is a search-time concern
|
|
486
|
+
// only). See ARCHIVED_SEARCH_WEIGHT in constants.ts.
|
|
487
|
+
if (memory.archived) {
|
|
488
|
+
score = clamp01(score * ARCHIVED_SEARCH_WEIGHT);
|
|
489
|
+
}
|
|
466
490
|
direct.set(memory.id, {
|
|
467
491
|
memory,
|
|
468
492
|
score,
|
|
@@ -513,6 +537,7 @@ export class AugmentService {
|
|
|
513
537
|
const snapshot = await this.corpus();
|
|
514
538
|
const cutoff = now - minAgeDays * 86_400_000;
|
|
515
539
|
const eligible = snapshot.memories.filter((memory) => memory.kind === "WORK" &&
|
|
540
|
+
!memory.archived && // a consolidated source shouldn't be re-consolidated
|
|
516
541
|
(!input.project_name || memory.project_name === input.project_name) &&
|
|
517
542
|
Date.parse(memory.updated_at) <= cutoff);
|
|
518
543
|
const clusters = clusterSleepCandidates({
|
|
@@ -527,6 +552,169 @@ export class AugmentService {
|
|
|
527
552
|
text: renderSleepCandidatesText(clusters, eligible.length, minCosine, minAgeDays),
|
|
528
553
|
};
|
|
529
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* Draft a sleep consolidation as a review artifact OUTSIDE the corpus. Writes
|
|
557
|
+
* nothing to the memory store: the corpus is only mutated by {@link sleepApply}
|
|
558
|
+
* on an explicitly approved proposal. Validates the sources up front (same
|
|
559
|
+
* checks apply re-runs against the live corpus) so an impossible proposal fails
|
|
560
|
+
* loudly at draft time.
|
|
561
|
+
*/
|
|
562
|
+
async sleepPropose(input) {
|
|
563
|
+
await this.init();
|
|
564
|
+
const kind = normalizeKind(input.kind);
|
|
565
|
+
await this.validateSleepSources(input.project_name, kind, input.name, input.derived_from);
|
|
566
|
+
const proposal = await writeProposal(this.config.stateDir, {
|
|
567
|
+
type: "sleep",
|
|
568
|
+
project: input.project_name,
|
|
569
|
+
kind,
|
|
570
|
+
name: input.name,
|
|
571
|
+
derived_from: input.derived_from,
|
|
572
|
+
...(input.rationale !== undefined ? { rationale: input.rationale } : {}),
|
|
573
|
+
}, input.content);
|
|
574
|
+
return { proposal, text: renderProposalsText([proposal], "sleep") };
|
|
575
|
+
}
|
|
576
|
+
async sleepProposals(status) {
|
|
577
|
+
await this.init();
|
|
578
|
+
const proposals = await listProposals(this.config.stateDir, "sleep", status);
|
|
579
|
+
return { proposals, text: renderProposalsText(proposals, "sleep") };
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Apply an approved sleep proposal — the ONLY corpus mutation in the flow. The
|
|
583
|
+
* sequence is a contract: read the proposal fresh from disk (user edits count),
|
|
584
|
+
* require it still be `proposed`, re-validate against the LIVE corpus (drafts
|
|
585
|
+
* sit while the corpus moves underneath them), upsert the consolidated record
|
|
586
|
+
* (provenance threaded into frontmatter), archive each source in place (body
|
|
587
|
+
* untouched, verbatim), then flip the proposal to `applied`. Does NOT commit
|
|
588
|
+
* git: the route commits the whole pass as one revertible commit.
|
|
589
|
+
*/
|
|
590
|
+
async sleepApply(input) {
|
|
591
|
+
try {
|
|
592
|
+
return await this.sleepApplyCore(input);
|
|
593
|
+
}
|
|
594
|
+
finally {
|
|
595
|
+
this.invalidateCorpusSnapshot();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async sleepApplyCore(input) {
|
|
599
|
+
await this.init();
|
|
600
|
+
// 1. Read the proposal fresh from disk so hand-edits (body, sources) count.
|
|
601
|
+
const proposal = await readProposal(this.config.stateDir, "sleep", input.filename);
|
|
602
|
+
const fm = proposal.frontmatter;
|
|
603
|
+
// 2. Only a still-proposed draft may apply; a second apply is rejected here.
|
|
604
|
+
if (fm.status !== "proposed") {
|
|
605
|
+
throw new Error(`Sleep proposal ${input.filename} is already ${fm.status}, not proposed (already applied?)`);
|
|
606
|
+
}
|
|
607
|
+
if (!fm.kind) {
|
|
608
|
+
throw new Error(`Sleep proposal ${input.filename} is missing a target kind`);
|
|
609
|
+
}
|
|
610
|
+
if (!fm.name) {
|
|
611
|
+
throw new Error(`Sleep proposal ${input.filename} is missing a target name`);
|
|
612
|
+
}
|
|
613
|
+
const kind = normalizeKind(fm.kind);
|
|
614
|
+
const name = fm.name;
|
|
615
|
+
const projectName = fm.project;
|
|
616
|
+
const derivedFrom = fm.derived_from;
|
|
617
|
+
// 3. Re-validate against the live corpus (sources may have moved since draft).
|
|
618
|
+
await this.validateSleepSources(projectName, kind, name, derivedFrom);
|
|
619
|
+
const project = await this.db.getProjectByName(projectName);
|
|
620
|
+
if (!project) {
|
|
621
|
+
throw new Error(`Sleep proposal project not found in corpus: ${projectName}`);
|
|
622
|
+
}
|
|
623
|
+
// 4. Write the consolidated record through the public upsert (snapshot
|
|
624
|
+
// invalidation + provenance handled there). skip_similarity_fold pins the
|
|
625
|
+
// write to the approved target's deterministic path: the user approved
|
|
626
|
+
// THIS identity (validateSleepSources above already guaranteed it's
|
|
627
|
+
// free), so upsertCore must not divert it into some other lexically-
|
|
628
|
+
// similar same-kind record it happens to subsume.
|
|
629
|
+
const upserted = await this.upsert({
|
|
630
|
+
project_id: project.id,
|
|
631
|
+
kind,
|
|
632
|
+
name,
|
|
633
|
+
content: proposal.body,
|
|
634
|
+
derived_from: derivedFrom,
|
|
635
|
+
skip_similarity_fold: true,
|
|
636
|
+
});
|
|
637
|
+
const expectedTargetPath = memoryRelativePath(projectName, kind, name);
|
|
638
|
+
if (upserted.memory.relative_path !== expectedTargetPath) {
|
|
639
|
+
throw new Error(`Sleep apply diverted from the approved target ${expectedTargetPath} to ${upserted.memory.relative_path}; aborting before archiving any source`);
|
|
640
|
+
}
|
|
641
|
+
const consolidatedInto = upserted.memory.relative_path;
|
|
642
|
+
// 5. Archive each source in place, pointing it at the consolidated record.
|
|
643
|
+
const archived = [];
|
|
644
|
+
for (const sourcePath of derivedFrom) {
|
|
645
|
+
archived.push(await this.archiveSource(sourcePath, consolidatedInto));
|
|
646
|
+
}
|
|
647
|
+
// 6. Flip the proposal to applied (outside the corpus).
|
|
648
|
+
const appliedProposal = await setProposalStatus(this.config.stateDir, "sleep", input.filename, "applied");
|
|
649
|
+
return {
|
|
650
|
+
memory: upserted.memory,
|
|
651
|
+
archived,
|
|
652
|
+
proposal: appliedProposal,
|
|
653
|
+
text: `applied sleep ${input.filename}: ${derivedFrom.length} WORK -> ${kind}/${name} (${projectName})`,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
async sleepReject(filename) {
|
|
657
|
+
await this.init();
|
|
658
|
+
const proposal = await setProposalStatus(this.config.stateDir, "sleep", filename, "rejected");
|
|
659
|
+
return { proposal, text: renderProposalsText([proposal], "sleep") };
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Shared sleep validation, run at propose time and re-run against the live
|
|
663
|
+
* corpus at apply time. Every source must resolve, be a WORK memory in
|
|
664
|
+
* `projectName` (single-project constraint), and be unarchived; there must be
|
|
665
|
+
* >=2 of them; and the consolidation target must not already exist. Every
|
|
666
|
+
* violation names the offending path.
|
|
667
|
+
*/
|
|
668
|
+
async validateSleepSources(projectName, kind, name, derivedFrom) {
|
|
669
|
+
if (derivedFrom.length < 2) {
|
|
670
|
+
throw new Error(`A sleep consolidation needs at least 2 WORK sources; got ${derivedFrom.length}`);
|
|
671
|
+
}
|
|
672
|
+
for (const sourcePath of derivedFrom) {
|
|
673
|
+
const source = await this.db.getMemoryByPath(normalizeRelativePath(sourcePath));
|
|
674
|
+
if (!source) {
|
|
675
|
+
throw new Error(`Sleep source not found in corpus: ${sourcePath}`);
|
|
676
|
+
}
|
|
677
|
+
if (source.kind !== "WORK") {
|
|
678
|
+
throw new Error(`Sleep source must be a WORK memory: ${sourcePath} is ${source.kind}`);
|
|
679
|
+
}
|
|
680
|
+
if (source.project_name !== projectName) {
|
|
681
|
+
throw new Error(`Sleep source ${sourcePath} belongs to ${source.project_name}, not ${projectName}`);
|
|
682
|
+
}
|
|
683
|
+
if (source.archived) {
|
|
684
|
+
throw new Error(`Sleep source is already archived: ${sourcePath}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const targetPath = memoryRelativePath(projectName, kind, name);
|
|
688
|
+
if (await this.db.getMemoryByPath(targetPath)) {
|
|
689
|
+
throw new Error(`Sleep target already exists in corpus: ${targetPath}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
/** Rewrite one source's frontmatter to archived + consolidated_into, leaving
|
|
693
|
+
* the body verbatim, and reindex it. Mirrors {@link scrubInboundLinks}. Returns
|
|
694
|
+
* the source's relative path. */
|
|
695
|
+
async archiveSource(sourcePath, consolidatedInto) {
|
|
696
|
+
const memory = await this.db.getMemoryByPath(normalizeRelativePath(sourcePath));
|
|
697
|
+
if (!memory) {
|
|
698
|
+
throw new Error(`Sleep source not found in corpus: ${sourcePath}`);
|
|
699
|
+
}
|
|
700
|
+
const frontmatter = normalizeMemoryFrontmatter({
|
|
701
|
+
project: memory.project_name,
|
|
702
|
+
kind: memory.kind,
|
|
703
|
+
name: memory.name,
|
|
704
|
+
tags: memory.tags,
|
|
705
|
+
links: memory.links,
|
|
706
|
+
created_at: memory.created_at,
|
|
707
|
+
updated_at: new Date().toISOString(),
|
|
708
|
+
archived: true,
|
|
709
|
+
consolidated_into: consolidatedInto,
|
|
710
|
+
derived_from: memory.derived_from,
|
|
711
|
+
});
|
|
712
|
+
await writeMemory(this.config.memoryRoot, memory.relative_path, frontmatter, memory.body);
|
|
713
|
+
this.recordSelfWrite(memory.relative_path);
|
|
714
|
+
const parsed = await readMemory(this.config.memoryRoot, memory.relative_path);
|
|
715
|
+
await this.db.upsertMemory(parsed);
|
|
716
|
+
return memory.relative_path;
|
|
717
|
+
}
|
|
530
718
|
async findSimilarMemory(projectId, kind, content) {
|
|
531
719
|
const candidates = await this.db.listMemoriesForProject(projectId, kind);
|
|
532
720
|
const contentTokens = new Set(tokens(content));
|
|
@@ -557,6 +745,19 @@ function normalizeMemoryFrontmatter(input) {
|
|
|
557
745
|
links: input.links ?? [],
|
|
558
746
|
created_at: input.created_at,
|
|
559
747
|
updated_at: input.updated_at,
|
|
748
|
+
// Every corpus-mutating write reconstructs frontmatter through this
|
|
749
|
+
// function. Keys are included ONLY when set so an ordinary (never
|
|
750
|
+
// archived) memory never gains "archived: false" churn -- but every
|
|
751
|
+
// caller MUST thread archived/consolidated_into/derived_from through from
|
|
752
|
+
// the existing record, or a write that has nothing to do with archival
|
|
753
|
+
// state (link/unlink/scrub) would silently strip it.
|
|
754
|
+
...(input.archived === true ? { archived: true } : {}),
|
|
755
|
+
...(typeof input.consolidated_into === "string"
|
|
756
|
+
? { consolidated_into: normalizeRelativePath(input.consolidated_into) }
|
|
757
|
+
: {}),
|
|
758
|
+
...(Array.isArray(input.derived_from)
|
|
759
|
+
? { derived_from: input.derived_from.map((entry) => normalizeRelativePath(entry)) }
|
|
760
|
+
: {}),
|
|
560
761
|
};
|
|
561
762
|
}
|
|
562
763
|
function mergeFrontmatterLink(links, link) {
|