akm-cli 0.7.5 → 0.8.0-rc2
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/.github/CHANGELOG.md +1 -1
- package/dist/cli/parse-args.js +43 -0
- package/dist/cli.js +853 -479
- package/dist/commands/agent-dispatch.js +102 -0
- package/dist/commands/agent-support.js +62 -0
- package/dist/commands/config-cli.js +68 -84
- package/dist/commands/consolidate.js +823 -0
- package/dist/commands/distill-promotion-policy.js +658 -0
- package/dist/commands/distill.js +244 -52
- package/dist/commands/eval-cases.js +40 -0
- package/dist/commands/events.js +2 -23
- package/dist/commands/graph.js +222 -0
- package/dist/commands/health.js +376 -0
- package/dist/commands/help/help-accept.md +9 -0
- package/dist/commands/help/help-improve.md +53 -0
- package/dist/commands/help/help-proposals.md +15 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +8 -0
- package/dist/commands/history.js +3 -30
- package/dist/commands/improve.js +1170 -0
- package/dist/commands/info.js +2 -2
- package/dist/commands/init.js +2 -2
- package/dist/commands/install-audit.js +5 -1
- package/dist/commands/installed-stashes.js +118 -138
- package/dist/commands/knowledge.js +133 -0
- package/dist/commands/lint/agent-linter.js +46 -0
- package/dist/commands/lint/base-linter.js +285 -0
- package/dist/commands/lint/command-linter.js +46 -0
- package/dist/commands/lint/default-linter.js +13 -0
- package/dist/commands/lint/index.js +107 -0
- package/dist/commands/lint/knowledge-linter.js +13 -0
- package/dist/commands/lint/memory-linter.js +58 -0
- package/dist/commands/lint/registry.js +33 -0
- package/dist/commands/lint/skill-linter.js +42 -0
- package/dist/commands/lint/task-linter.js +47 -0
- package/dist/commands/lint/types.js +1 -0
- package/dist/commands/lint/workflow-linter.js +53 -0
- package/dist/commands/lint.js +1 -0
- package/dist/commands/proposal.js +8 -7
- package/dist/commands/propose.js +78 -28
- package/dist/commands/reflect.js +143 -35
- package/dist/commands/registry-search.js +2 -2
- package/dist/commands/remember.js +54 -0
- package/dist/commands/schema-repair.js +130 -0
- package/dist/commands/search.js +21 -5
- package/dist/commands/show.js +121 -17
- package/dist/commands/source-add.js +10 -10
- package/dist/commands/source-manage.js +11 -19
- package/dist/commands/tasks.js +385 -0
- package/dist/commands/url-checker.js +39 -0
- package/dist/commands/vault.js +8 -26
- package/dist/core/action-contributors.js +25 -0
- package/dist/core/asset-ref.js +4 -0
- package/dist/core/asset-registry.js +4 -16
- package/dist/core/asset-spec.js +10 -0
- package/dist/core/common.js +94 -0
- package/dist/core/concurrent.js +22 -0
- package/dist/core/config.js +222 -128
- package/dist/core/events.js +73 -126
- package/dist/core/frontmatter.js +3 -1
- package/dist/core/markdown.js +17 -0
- package/dist/core/memory-improve.js +678 -0
- package/dist/core/parse.js +155 -0
- package/dist/core/paths.js +101 -3
- package/dist/core/proposal-validators.js +61 -0
- package/dist/core/proposals.js +49 -38
- package/dist/core/state-db.js +775 -0
- package/dist/core/time.js +51 -0
- package/dist/core/warn.js +59 -1
- package/dist/indexer/db-search.js +52 -238
- package/dist/indexer/db.js +378 -1
- package/dist/indexer/ensure-index.js +61 -0
- package/dist/indexer/graph-boost.js +247 -94
- package/dist/indexer/graph-db.js +201 -0
- package/dist/indexer/graph-dedup.js +99 -0
- package/dist/indexer/graph-extraction.js +409 -76
- package/dist/indexer/index-context.js +10 -0
- package/dist/indexer/indexer.js +442 -290
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/match-contributors.js +141 -0
- package/dist/indexer/matchers.js +24 -190
- package/dist/indexer/memory-inference.js +63 -29
- package/dist/indexer/metadata-contributors.js +26 -0
- package/dist/indexer/metadata.js +194 -175
- package/dist/indexer/path-resolver.js +89 -0
- package/dist/indexer/ranking-contributors.js +204 -0
- package/dist/indexer/ranking.js +74 -0
- package/dist/indexer/search-hit-enrichers.js +22 -0
- package/dist/indexer/search-source.js +24 -9
- package/dist/indexer/semantic-status.js +2 -16
- package/dist/indexer/walker.js +25 -0
- package/dist/integrations/agent/config.js +175 -3
- package/dist/integrations/agent/index.js +3 -1
- package/dist/integrations/agent/pipeline.js +39 -0
- package/dist/integrations/agent/profiles.js +67 -5
- package/dist/integrations/agent/prompts.js +77 -72
- package/dist/integrations/agent/runners.js +31 -0
- package/dist/integrations/agent/sdk-runner.js +120 -0
- package/dist/integrations/agent/spawn.js +71 -16
- package/dist/integrations/lockfile.js +10 -18
- package/dist/integrations/session-logs/index.js +65 -0
- package/dist/integrations/session-logs/providers/claude-code.js +56 -0
- package/dist/integrations/session-logs/providers/opencode.js +52 -0
- package/dist/integrations/session-logs/types.js +1 -0
- package/dist/llm/call-ai.js +74 -0
- package/dist/llm/client.js +61 -122
- package/dist/llm/feature-gate.js +27 -16
- package/dist/llm/graph-extract.js +297 -62
- package/dist/llm/memory-infer.js +49 -71
- package/dist/llm/metadata-enhance.js +39 -22
- package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
- package/dist/output/cli-hints-full.md +277 -0
- package/dist/output/cli-hints-short.md +65 -0
- package/dist/output/cli-hints.js +2 -318
- package/dist/output/renderers.js +190 -123
- package/dist/output/shapes.js +33 -0
- package/dist/output/text.js +239 -2
- package/dist/registry/providers/skills-sh.js +61 -49
- package/dist/registry/providers/static-index.js +44 -48
- package/dist/setup/setup.js +510 -11
- package/dist/sources/provider-factory.js +2 -1
- package/dist/sources/providers/git.js +2 -2
- package/dist/sources/website-ingest.js +4 -0
- package/dist/tasks/backends/cron.js +200 -0
- package/dist/tasks/backends/exec-utils.js +25 -0
- package/dist/tasks/backends/index.js +32 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +184 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +212 -0
- package/dist/tasks/parser.js +198 -0
- package/dist/tasks/resolveAkmBin.js +84 -0
- package/dist/tasks/runner.js +432 -0
- package/dist/tasks/schedule.js +208 -0
- package/dist/tasks/schema.js +13 -0
- package/dist/tasks/validator.js +59 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +12 -0
- package/dist/wiki/wiki.js +10 -61
- package/dist/workflows/authoring.js +5 -25
- package/dist/workflows/renderer.js +8 -3
- package/dist/workflows/runs.js +59 -91
- package/dist/workflows/validator.js +1 -1
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +3 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.8.0.md +43 -0
- package/package.json +3 -2
- package/dist/templates/wiki-templates.js +0 -100
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { stringify as yamlStringify } from "yaml";
|
|
4
|
+
import { makeAssetRef, parseAssetRef } from "./asset-ref";
|
|
5
|
+
import { firstString, groupBy, stringArray } from "./common";
|
|
6
|
+
import { parseFrontmatter } from "./frontmatter";
|
|
7
|
+
const DERIVED_SUFFIX = ".derived";
|
|
8
|
+
export function analyzeMemoryCleanup(stashDir, options = {}) {
|
|
9
|
+
const records = collectDerivedMemories(stashDir, options.parentRef);
|
|
10
|
+
const byRef = new Map(records.map((record) => [record.ref, record]));
|
|
11
|
+
const byParent = groupBy(records, (record) => record.parentRef);
|
|
12
|
+
const planned = new Map();
|
|
13
|
+
const contradictionCandidates = [];
|
|
14
|
+
const beliefTransitions = new Map();
|
|
15
|
+
const planPrune = (record, reason, survivorRef) => {
|
|
16
|
+
const existing = planned.get(record.ref);
|
|
17
|
+
if (existing)
|
|
18
|
+
return existing;
|
|
19
|
+
const next = {
|
|
20
|
+
ref: record.ref,
|
|
21
|
+
parentRef: record.parentRef,
|
|
22
|
+
reason,
|
|
23
|
+
...(survivorRef ? { survivorRef } : {}),
|
|
24
|
+
filePath: record.filePath,
|
|
25
|
+
};
|
|
26
|
+
planned.set(record.ref, next);
|
|
27
|
+
return next;
|
|
28
|
+
};
|
|
29
|
+
const planBeliefTransition = (record, toState, reason, currentBeliefRefs = []) => {
|
|
30
|
+
const normalizedRefs = [...new Set(currentBeliefRefs)].sort();
|
|
31
|
+
const metadataChanged = !sameStringArray(record.currentBeliefRefs, normalizedRefs) ||
|
|
32
|
+
(toState === "contradicted"
|
|
33
|
+
? !sameStringArray(record.contradictedBy, normalizedRefs)
|
|
34
|
+
: record.contradictedBy.length > 0);
|
|
35
|
+
if (record.beliefState === toState && !metadataChanged)
|
|
36
|
+
return;
|
|
37
|
+
const existing = beliefTransitions.get(record.ref);
|
|
38
|
+
if (existing)
|
|
39
|
+
return existing;
|
|
40
|
+
const next = {
|
|
41
|
+
ref: record.ref,
|
|
42
|
+
parentRef: record.parentRef,
|
|
43
|
+
fromState: record.beliefState,
|
|
44
|
+
toState,
|
|
45
|
+
reason,
|
|
46
|
+
...(normalizedRefs[0] ? { relatedRef: normalizedRefs[0] } : {}),
|
|
47
|
+
...(normalizedRefs.length > 0 ? { relatedRefs: normalizedRefs, currentBeliefRefs: normalizedRefs } : {}),
|
|
48
|
+
};
|
|
49
|
+
beliefTransitions.set(record.ref, next);
|
|
50
|
+
return next;
|
|
51
|
+
};
|
|
52
|
+
for (const record of records) {
|
|
53
|
+
const supersededTarget = firstExistingRef(record.supersededBy, byRef, record.ref);
|
|
54
|
+
if (supersededTarget) {
|
|
55
|
+
planPrune(record, "superseded-derived", supersededTarget);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (record.obsolete) {
|
|
59
|
+
planPrune(record, "obsolete-derived");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const excludedRefs = new Set(planned.keys());
|
|
63
|
+
for (const family of byParent.values()) {
|
|
64
|
+
const activeFamily = family.filter((record) => !excludedRefs.has(record.ref));
|
|
65
|
+
const resolution = resolveFamilyContradictions(activeFamily);
|
|
66
|
+
for (const candidate of resolution.contradictionCandidates) {
|
|
67
|
+
contradictionCandidates.push(candidate);
|
|
68
|
+
}
|
|
69
|
+
for (const transition of resolution.transitions) {
|
|
70
|
+
const record = byRef.get(transition.ref);
|
|
71
|
+
if (!record)
|
|
72
|
+
continue;
|
|
73
|
+
planBeliefTransition(record, transition.toState, transition.reason, transition.currentBeliefRefs ?? []);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const excludedForDuplicateDetection = new Set([
|
|
77
|
+
...planned.keys(),
|
|
78
|
+
...contradictionCandidates.map((candidate) => candidate.ref),
|
|
79
|
+
]);
|
|
80
|
+
for (const family of byParent.values()) {
|
|
81
|
+
const active = family.filter((record) => !excludedForDuplicateDetection.has(record.ref));
|
|
82
|
+
const byFingerprint = groupBy(active, (record) => record.fingerprint);
|
|
83
|
+
for (const duplicates of byFingerprint.values()) {
|
|
84
|
+
if (duplicates.length < 2)
|
|
85
|
+
continue;
|
|
86
|
+
const [survivor, ...rest] = sortRecordsForSurvival(duplicates);
|
|
87
|
+
for (const duplicate of rest) {
|
|
88
|
+
planPrune(duplicate, "duplicate-derived", survivor.ref);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const consolidationCandidates = [];
|
|
93
|
+
const excludedForConsolidation = new Set([
|
|
94
|
+
...planned.keys(),
|
|
95
|
+
...contradictionCandidates.map((candidate) => candidate.ref),
|
|
96
|
+
]);
|
|
97
|
+
for (const [parentRef, family] of byParent.entries()) {
|
|
98
|
+
const active = family.filter((record) => !excludedForConsolidation.has(record.ref));
|
|
99
|
+
if (active.length < 2)
|
|
100
|
+
continue;
|
|
101
|
+
const bySignal = groupBy(active.filter((record) => record.signalKey !== undefined), (record) => record.signalKey);
|
|
102
|
+
for (const [signal, signalRecords] of bySignal.entries()) {
|
|
103
|
+
if (signalRecords.length < 2)
|
|
104
|
+
continue;
|
|
105
|
+
const ordered = sortRecordsForSurvival(signalRecords);
|
|
106
|
+
consolidationCandidates.push({
|
|
107
|
+
parentRef,
|
|
108
|
+
signal,
|
|
109
|
+
refs: ordered.map((record) => record.ref),
|
|
110
|
+
suggestedSurvivorRef: ordered[0].ref,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const RELATIVE_DATE_RE = /\b(yesterday|last week|last month|last year|\d+ days? ago|\d+ weeks? ago|\d+ months? ago)\b/gi;
|
|
115
|
+
const relativeDateCandidates = [];
|
|
116
|
+
for (const record of records) {
|
|
117
|
+
const matches = record.body.match(RELATIVE_DATE_RE);
|
|
118
|
+
if (matches && matches.length > 0) {
|
|
119
|
+
relativeDateCandidates.push({
|
|
120
|
+
ref: record.ref,
|
|
121
|
+
filePath: record.filePath,
|
|
122
|
+
matches: [...new Set(matches.map((m) => m.toLowerCase()))],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
analyzedDerived: records.length,
|
|
128
|
+
pruneCandidates: [...planned.values()]
|
|
129
|
+
.map(({ filePath: _filePath, ...candidate }) => candidate)
|
|
130
|
+
.sort(compareCandidates),
|
|
131
|
+
contradictionCandidates: contradictionCandidates.sort(compareContradictionCandidates),
|
|
132
|
+
beliefStateTransitions: [...beliefTransitions.values()].sort(compareBeliefTransitions),
|
|
133
|
+
consolidationCandidates: consolidationCandidates.sort(compareConsolidationCandidates),
|
|
134
|
+
relativeDateCandidates,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
export function applyMemoryCleanup(stashDir, plan) {
|
|
138
|
+
const records = collectDerivedMemories(stashDir);
|
|
139
|
+
const fileByRef = new Map(records.map((record) => [record.ref, record.filePath]));
|
|
140
|
+
const archived = [];
|
|
141
|
+
const appliedBeliefTransitions = [];
|
|
142
|
+
const warnings = [];
|
|
143
|
+
for (const transition of plan.beliefStateTransitions) {
|
|
144
|
+
const filePath = fileByRef.get(transition.ref);
|
|
145
|
+
if (!filePath)
|
|
146
|
+
continue;
|
|
147
|
+
try {
|
|
148
|
+
persistBeliefStateTransition(filePath, transition);
|
|
149
|
+
appliedBeliefTransitions.push(transition);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
warnings.push(formatApplyWarning("belief-transition", transition.ref, error));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
let transitionLogPath;
|
|
156
|
+
if (appliedBeliefTransitions.length > 0) {
|
|
157
|
+
try {
|
|
158
|
+
transitionLogPath = appendBeliefStateTransitionLog(stashDir, appliedBeliefTransitions);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
warnings.push(formatApplyWarning("transition-log", "memory-cleanup", error));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const candidate of plan.pruneCandidates) {
|
|
165
|
+
const filePath = fileByRef.get(candidate.ref);
|
|
166
|
+
if (!filePath)
|
|
167
|
+
continue;
|
|
168
|
+
try {
|
|
169
|
+
archived.push(archiveCleanupCandidate(stashDir, candidate, filePath));
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
warnings.push(formatApplyWarning("archive", candidate.ref, error));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
archived.sort((a, b) => a.ref.localeCompare(b.ref));
|
|
176
|
+
appliedBeliefTransitions.sort(compareBeliefTransitions);
|
|
177
|
+
return {
|
|
178
|
+
archived,
|
|
179
|
+
beliefStateTransitions: appliedBeliefTransitions,
|
|
180
|
+
...(transitionLogPath ? { transitionLogPath: path.relative(stashDir, transitionLogPath).replace(/\\/g, "/") } : {}),
|
|
181
|
+
...(transitionLogPath ? { transitionLogEntries: appliedBeliefTransitions.length } : {}),
|
|
182
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function formatApplyWarning(stage, ref, error) {
|
|
186
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
187
|
+
return `${stage} failed for ${ref}: ${detail}`;
|
|
188
|
+
}
|
|
189
|
+
function resolveFamilyContradictions(family) {
|
|
190
|
+
if (family.length === 0)
|
|
191
|
+
return { contradictionCandidates: [], transitions: [] };
|
|
192
|
+
const familyRefSet = new Set(family.map((record) => record.ref));
|
|
193
|
+
const edges = new Map();
|
|
194
|
+
let edgeCount = 0;
|
|
195
|
+
for (const record of family) {
|
|
196
|
+
const targets = [
|
|
197
|
+
...new Set(record.contradictedBy.filter((ref) => ref !== record.ref && familyRefSet.has(ref))),
|
|
198
|
+
].sort();
|
|
199
|
+
edges.set(record.ref, targets);
|
|
200
|
+
edgeCount += targets.length;
|
|
201
|
+
}
|
|
202
|
+
if (edgeCount === 0) {
|
|
203
|
+
return {
|
|
204
|
+
contradictionCandidates: [],
|
|
205
|
+
transitions: family
|
|
206
|
+
.filter((record) => record.beliefState !== "active" || record.contradictedBy.length > 0 || record.currentBeliefRefs.length > 0)
|
|
207
|
+
.map((record) => ({
|
|
208
|
+
ref: record.ref,
|
|
209
|
+
parentRef: record.parentRef,
|
|
210
|
+
fromState: record.beliefState,
|
|
211
|
+
toState: "active",
|
|
212
|
+
reason: "belief-refresh",
|
|
213
|
+
})),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const { components, componentIndexByRef } = stronglyConnectedComponents(family.map((record) => record.ref), edges);
|
|
217
|
+
const outgoingComponents = new Map();
|
|
218
|
+
for (let index = 0; index < components.length; index += 1) {
|
|
219
|
+
outgoingComponents.set(index, new Set());
|
|
220
|
+
}
|
|
221
|
+
for (const [ref, targets] of edges.entries()) {
|
|
222
|
+
const fromIndex = componentIndexByRef.get(ref);
|
|
223
|
+
if (fromIndex === undefined)
|
|
224
|
+
continue;
|
|
225
|
+
for (const target of targets) {
|
|
226
|
+
const toIndex = componentIndexByRef.get(target);
|
|
227
|
+
if (toIndex === undefined || toIndex === fromIndex)
|
|
228
|
+
continue;
|
|
229
|
+
outgoingComponents.get(fromIndex)?.add(toIndex);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const sinkComponents = new Set();
|
|
233
|
+
for (const [index, outgoing] of outgoingComponents.entries()) {
|
|
234
|
+
if (outgoing.size === 0)
|
|
235
|
+
sinkComponents.add(index);
|
|
236
|
+
}
|
|
237
|
+
const reachableSinkRefsMemo = new Map();
|
|
238
|
+
const reachableSinkRefsForComponent = (index) => {
|
|
239
|
+
const memoized = reachableSinkRefsMemo.get(index);
|
|
240
|
+
if (memoized)
|
|
241
|
+
return memoized;
|
|
242
|
+
const outgoing = outgoingComponents.get(index);
|
|
243
|
+
if (!outgoing || outgoing.size === 0) {
|
|
244
|
+
const refs = [...components[index]].sort();
|
|
245
|
+
reachableSinkRefsMemo.set(index, refs);
|
|
246
|
+
return refs;
|
|
247
|
+
}
|
|
248
|
+
const refs = new Set();
|
|
249
|
+
for (const nextIndex of outgoing) {
|
|
250
|
+
for (const ref of reachableSinkRefsForComponent(nextIndex))
|
|
251
|
+
refs.add(ref);
|
|
252
|
+
}
|
|
253
|
+
const resolved = [...refs].sort();
|
|
254
|
+
reachableSinkRefsMemo.set(index, resolved);
|
|
255
|
+
return resolved;
|
|
256
|
+
};
|
|
257
|
+
const contradictionCandidates = [];
|
|
258
|
+
const transitions = [];
|
|
259
|
+
for (const record of family) {
|
|
260
|
+
const componentIndex = componentIndexByRef.get(record.ref);
|
|
261
|
+
if (componentIndex === undefined)
|
|
262
|
+
continue;
|
|
263
|
+
const isCurrentComponent = sinkComponents.has(componentIndex);
|
|
264
|
+
const currentRefs = reachableSinkRefsForComponent(componentIndex);
|
|
265
|
+
if (!isCurrentComponent) {
|
|
266
|
+
contradictionCandidates.push({
|
|
267
|
+
ref: record.ref,
|
|
268
|
+
parentRef: record.parentRef,
|
|
269
|
+
reason: "contradicted-derived",
|
|
270
|
+
contradictedByRef: currentRefs[0],
|
|
271
|
+
contradictedByRefs: currentRefs,
|
|
272
|
+
currentBeliefRefs: currentRefs,
|
|
273
|
+
});
|
|
274
|
+
if (record.beliefState !== "contradicted" ||
|
|
275
|
+
!sameStringArray(record.contradictedBy, currentRefs) ||
|
|
276
|
+
!sameStringArray(record.currentBeliefRefs, currentRefs)) {
|
|
277
|
+
transitions.push({
|
|
278
|
+
ref: record.ref,
|
|
279
|
+
parentRef: record.parentRef,
|
|
280
|
+
fromState: record.beliefState,
|
|
281
|
+
toState: "contradicted",
|
|
282
|
+
reason: "contradicted-derived",
|
|
283
|
+
relatedRef: currentRefs[0],
|
|
284
|
+
relatedRefs: currentRefs,
|
|
285
|
+
currentBeliefRefs: currentRefs,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const componentRefs = [...components[componentIndex]].sort();
|
|
291
|
+
const peerCurrentRefs = componentRefs.filter((ref) => ref !== record.ref);
|
|
292
|
+
if (record.beliefState !== "active" ||
|
|
293
|
+
record.contradictedBy.length > 0 ||
|
|
294
|
+
!sameStringArray(record.currentBeliefRefs, peerCurrentRefs)) {
|
|
295
|
+
transitions.push({
|
|
296
|
+
ref: record.ref,
|
|
297
|
+
parentRef: record.parentRef,
|
|
298
|
+
fromState: record.beliefState,
|
|
299
|
+
toState: "active",
|
|
300
|
+
reason: "belief-refresh",
|
|
301
|
+
...(peerCurrentRefs[0] ? { relatedRef: peerCurrentRefs[0], relatedRefs: peerCurrentRefs } : {}),
|
|
302
|
+
...(peerCurrentRefs.length > 0 ? { currentBeliefRefs: peerCurrentRefs } : {}),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
contradictionCandidates: contradictionCandidates.sort(compareContradictionCandidates),
|
|
308
|
+
transitions: transitions.sort(compareBeliefTransitions),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function stronglyConnectedComponents(refs, edges) {
|
|
312
|
+
let index = 0;
|
|
313
|
+
const indices = new Map();
|
|
314
|
+
const lowLinks = new Map();
|
|
315
|
+
const stack = [];
|
|
316
|
+
const onStack = new Set();
|
|
317
|
+
const components = [];
|
|
318
|
+
const visit = (ref) => {
|
|
319
|
+
indices.set(ref, index);
|
|
320
|
+
lowLinks.set(ref, index);
|
|
321
|
+
index += 1;
|
|
322
|
+
stack.push(ref);
|
|
323
|
+
onStack.add(ref);
|
|
324
|
+
for (const target of edges.get(ref) ?? []) {
|
|
325
|
+
if (!indices.has(target)) {
|
|
326
|
+
visit(target);
|
|
327
|
+
lowLinks.set(ref, Math.min(lowLinks.get(ref) ?? 0, lowLinks.get(target) ?? 0));
|
|
328
|
+
}
|
|
329
|
+
else if (onStack.has(target)) {
|
|
330
|
+
lowLinks.set(ref, Math.min(lowLinks.get(ref) ?? 0, indices.get(target) ?? 0));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if ((lowLinks.get(ref) ?? -1) !== (indices.get(ref) ?? -2))
|
|
334
|
+
return;
|
|
335
|
+
const component = [];
|
|
336
|
+
while (stack.length > 0) {
|
|
337
|
+
const member = stack.pop();
|
|
338
|
+
onStack.delete(member);
|
|
339
|
+
component.push(member);
|
|
340
|
+
if (member === ref)
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
components.push(component.sort());
|
|
344
|
+
};
|
|
345
|
+
for (const ref of refs) {
|
|
346
|
+
if (!indices.has(ref))
|
|
347
|
+
visit(ref);
|
|
348
|
+
}
|
|
349
|
+
const componentIndexByRef = new Map();
|
|
350
|
+
for (let componentIndex = 0; componentIndex < components.length; componentIndex += 1) {
|
|
351
|
+
for (const ref of components[componentIndex]) {
|
|
352
|
+
componentIndexByRef.set(ref, componentIndex);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return { components, componentIndexByRef };
|
|
356
|
+
}
|
|
357
|
+
function archiveCleanupCandidate(stashDir, candidate, filePath) {
|
|
358
|
+
const archivedAt = new Date().toISOString();
|
|
359
|
+
const originalPath = path.relative(stashDir, filePath).replace(/\\/g, "/");
|
|
360
|
+
const archiveDir = createArchiveDir(stashDir, candidate.ref, archivedAt);
|
|
361
|
+
const archivedPath = path.join(archiveDir, originalPath);
|
|
362
|
+
fs.mkdirSync(path.dirname(archivedPath), { recursive: true });
|
|
363
|
+
fs.renameSync(filePath, archivedPath);
|
|
364
|
+
const archiveRef = path.relative(stashDir, archivedPath).replace(/\\/g, "/");
|
|
365
|
+
const auditPath = path.join(archiveDir, "cleanup.md");
|
|
366
|
+
const auditRef = path.relative(stashDir, auditPath).replace(/\\/g, "/");
|
|
367
|
+
const auditFrontmatter = yamlStringify({
|
|
368
|
+
schemaVersion: 1,
|
|
369
|
+
kind: "memory-cleanup-archive",
|
|
370
|
+
archivedAt,
|
|
371
|
+
beliefState: "archived",
|
|
372
|
+
previousBeliefState: priorBeliefStateForArchive(candidate),
|
|
373
|
+
ref: candidate.ref,
|
|
374
|
+
parentRef: candidate.parentRef,
|
|
375
|
+
reason: candidate.reason,
|
|
376
|
+
...(candidate.survivorRef ? { survivorRef: candidate.survivorRef } : {}),
|
|
377
|
+
originalPath,
|
|
378
|
+
archivedPath: archiveRef,
|
|
379
|
+
}).trimEnd();
|
|
380
|
+
fs.writeFileSync(auditPath, `---\n${auditFrontmatter}\n---\n\nArchived derived memory for recoverable cleanup.\n`, "utf8");
|
|
381
|
+
return {
|
|
382
|
+
ref: candidate.ref,
|
|
383
|
+
parentRef: candidate.parentRef,
|
|
384
|
+
reason: candidate.reason,
|
|
385
|
+
beliefState: "archived",
|
|
386
|
+
previousBeliefState: priorBeliefStateForArchive(candidate),
|
|
387
|
+
...(candidate.survivorRef ? { survivorRef: candidate.survivorRef } : {}),
|
|
388
|
+
originalPath,
|
|
389
|
+
archivedPath: archiveRef,
|
|
390
|
+
auditPath: auditRef,
|
|
391
|
+
archivedAt,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function persistBeliefStateTransition(filePath, transition) {
|
|
395
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
396
|
+
const parsed = parseFrontmatter(raw);
|
|
397
|
+
const nextFrontmatter = {
|
|
398
|
+
...parsed.data,
|
|
399
|
+
beliefState: transition.toState,
|
|
400
|
+
};
|
|
401
|
+
const currentBeliefRefs = [...new Set(transition.currentBeliefRefs ?? [])].sort();
|
|
402
|
+
if (transition.toState === "contradicted") {
|
|
403
|
+
nextFrontmatter.contradictedBy = [...currentBeliefRefs];
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
delete nextFrontmatter.contradictedBy;
|
|
407
|
+
if (parsed.data.supersededBy !== undefined && refArray(parsed.data.supersededBy).length === 0) {
|
|
408
|
+
delete nextFrontmatter.supersededBy;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (currentBeliefRefs.length > 0)
|
|
412
|
+
nextFrontmatter.currentBeliefRefs = [...currentBeliefRefs];
|
|
413
|
+
else
|
|
414
|
+
delete nextFrontmatter.currentBeliefRefs;
|
|
415
|
+
const frontmatter = yamlStringify(nextFrontmatter).trimEnd();
|
|
416
|
+
const body = parsed.content.replace(/^\n+/, "");
|
|
417
|
+
fs.writeFileSync(filePath, `---\n${frontmatter}\n---\n\n${body}`, "utf8");
|
|
418
|
+
}
|
|
419
|
+
function appendBeliefStateTransitionLog(stashDir, transitions) {
|
|
420
|
+
const logDir = path.join(stashDir, ".akm", "memory-cleanup");
|
|
421
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
422
|
+
const logPath = path.join(logDir, "belief-transitions.jsonl");
|
|
423
|
+
const appliedAt = new Date().toISOString();
|
|
424
|
+
const lines = transitions
|
|
425
|
+
.map((transition) => JSON.stringify({
|
|
426
|
+
appliedAt,
|
|
427
|
+
ref: transition.ref,
|
|
428
|
+
parentRef: transition.parentRef,
|
|
429
|
+
fromState: transition.fromState,
|
|
430
|
+
toState: transition.toState,
|
|
431
|
+
reason: transition.reason,
|
|
432
|
+
...(transition.relatedRef ? { relatedRef: transition.relatedRef } : {}),
|
|
433
|
+
...(transition.relatedRefs ? { relatedRefs: transition.relatedRefs } : {}),
|
|
434
|
+
...(transition.currentBeliefRefs ? { currentBeliefRefs: transition.currentBeliefRefs } : {}),
|
|
435
|
+
}))
|
|
436
|
+
.join("\n");
|
|
437
|
+
fs.appendFileSync(logPath, `${lines}\n`, "utf8");
|
|
438
|
+
return logPath;
|
|
439
|
+
}
|
|
440
|
+
function priorBeliefStateForArchive(candidate) {
|
|
441
|
+
if (candidate.reason === "superseded-derived")
|
|
442
|
+
return "superseded";
|
|
443
|
+
return "active";
|
|
444
|
+
}
|
|
445
|
+
function createArchiveDir(stashDir, ref, archivedAt) {
|
|
446
|
+
const baseName = `${archivedAt.replace(/[:.]/g, "-")}-${sanitizeRef(ref)}`;
|
|
447
|
+
const root = path.join(stashDir, ".akm", "memory-cleanup", "archive");
|
|
448
|
+
fs.mkdirSync(root, { recursive: true });
|
|
449
|
+
let attempt = 0;
|
|
450
|
+
while (true) {
|
|
451
|
+
const candidate = path.join(root, attempt === 0 ? baseName : `${baseName}-${attempt}`);
|
|
452
|
+
if (!fs.existsSync(candidate)) {
|
|
453
|
+
fs.mkdirSync(candidate, { recursive: true });
|
|
454
|
+
return candidate;
|
|
455
|
+
}
|
|
456
|
+
attempt += 1;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function sanitizeRef(ref) {
|
|
460
|
+
return ref.replace(/[^a-z0-9._-]+/gi, "-");
|
|
461
|
+
}
|
|
462
|
+
function collectDerivedMemories(stashDir, parentRefFilter) {
|
|
463
|
+
const memoriesDir = path.join(stashDir, "memories");
|
|
464
|
+
if (!fs.existsSync(memoriesDir))
|
|
465
|
+
return [];
|
|
466
|
+
const records = [];
|
|
467
|
+
for (const filePath of walkMarkdownFiles(memoriesDir)) {
|
|
468
|
+
const name = toMemoryName(memoriesDir, filePath);
|
|
469
|
+
if (!name)
|
|
470
|
+
continue;
|
|
471
|
+
let raw;
|
|
472
|
+
try {
|
|
473
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const parsed = parseFrontmatter(raw);
|
|
479
|
+
const parentRef = resolveParentRef(name, parsed.data);
|
|
480
|
+
if (!parentRef)
|
|
481
|
+
continue;
|
|
482
|
+
if (parentRefFilter && parentRef !== parentRefFilter)
|
|
483
|
+
continue;
|
|
484
|
+
if (!isDerivedMemory(name, parsed.data))
|
|
485
|
+
continue;
|
|
486
|
+
const title = firstString(parsed.data.title) ?? extractHeading(parsed.content) ?? "";
|
|
487
|
+
const description = firstString(parsed.data.description) ?? "";
|
|
488
|
+
const tags = stringArray(parsed.data.tags);
|
|
489
|
+
const searchHints = stringArray(parsed.data.searchHints);
|
|
490
|
+
const body = parsed.content.trim();
|
|
491
|
+
const signalKey = normalizeSignal(firstNonEmpty([title, description, searchHints[0]]));
|
|
492
|
+
records.push({
|
|
493
|
+
ref: makeAssetRef("memory", name),
|
|
494
|
+
name,
|
|
495
|
+
filePath,
|
|
496
|
+
parentRef,
|
|
497
|
+
title,
|
|
498
|
+
description,
|
|
499
|
+
tags,
|
|
500
|
+
searchHints,
|
|
501
|
+
body,
|
|
502
|
+
canonicalName: name === `${parentRef.slice("memory:".length)}${DERIVED_SUFFIX}`,
|
|
503
|
+
signalScore: computeSignalScore(title, description, tags, searchHints, body),
|
|
504
|
+
fingerprint: buildFingerprint(title, description, tags, searchHints, body),
|
|
505
|
+
...(signalKey ? { signalKey } : {}),
|
|
506
|
+
supersededBy: refArray(parsed.data.supersededBy),
|
|
507
|
+
contradictedBy: refArray(parsed.data.contradictedBy),
|
|
508
|
+
currentBeliefRefs: refArray(parsed.data.currentBeliefRefs),
|
|
509
|
+
obsolete: parsed.data.obsolete === true || parsed.data.retracted === true,
|
|
510
|
+
beliefState: resolveBeliefState(parsed.data),
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
return records.sort(compareRecords);
|
|
514
|
+
}
|
|
515
|
+
function resolveBeliefState(frontmatter) {
|
|
516
|
+
const explicit = firstString(frontmatter.beliefState);
|
|
517
|
+
if (explicit === "active" || explicit === "superseded" || explicit === "contradicted") {
|
|
518
|
+
return explicit;
|
|
519
|
+
}
|
|
520
|
+
return "active";
|
|
521
|
+
}
|
|
522
|
+
function isDerivedMemory(name, frontmatter) {
|
|
523
|
+
return frontmatter.inferred === true || name.endsWith(DERIVED_SUFFIX);
|
|
524
|
+
}
|
|
525
|
+
function resolveParentRef(name, frontmatter) {
|
|
526
|
+
const fromSource = parseMemoryRef(firstString(frontmatter.source));
|
|
527
|
+
if (fromSource)
|
|
528
|
+
return fromSource;
|
|
529
|
+
const derivedFrom = firstString(frontmatter.derivedFrom);
|
|
530
|
+
if (derivedFrom)
|
|
531
|
+
return makeAssetRef("memory", derivedFrom);
|
|
532
|
+
if (name.endsWith(DERIVED_SUFFIX)) {
|
|
533
|
+
return makeAssetRef("memory", name.slice(0, -DERIVED_SUFFIX.length));
|
|
534
|
+
}
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
function refArray(value) {
|
|
538
|
+
if (typeof value === "string") {
|
|
539
|
+
const parsed = parseMemoryRef(value);
|
|
540
|
+
return parsed ? [parsed] : [];
|
|
541
|
+
}
|
|
542
|
+
if (!Array.isArray(value))
|
|
543
|
+
return [];
|
|
544
|
+
const refs = new Set();
|
|
545
|
+
for (const item of value) {
|
|
546
|
+
if (typeof item !== "string")
|
|
547
|
+
continue;
|
|
548
|
+
const parsed = parseMemoryRef(item);
|
|
549
|
+
if (parsed)
|
|
550
|
+
refs.add(parsed);
|
|
551
|
+
}
|
|
552
|
+
return [...refs].sort();
|
|
553
|
+
}
|
|
554
|
+
function parseMemoryRef(value) {
|
|
555
|
+
if (!value)
|
|
556
|
+
return undefined;
|
|
557
|
+
try {
|
|
558
|
+
const parsed = parseAssetRef(value.trim());
|
|
559
|
+
if (parsed.type !== "memory")
|
|
560
|
+
return undefined;
|
|
561
|
+
return makeAssetRef(parsed.type, parsed.name);
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function buildFingerprint(title, description, tags, searchHints, body) {
|
|
568
|
+
return JSON.stringify({
|
|
569
|
+
title: normalizeSignal(title),
|
|
570
|
+
description: normalizeSignal(description),
|
|
571
|
+
tags: normalizeList(tags),
|
|
572
|
+
searchHints: normalizeList(searchHints),
|
|
573
|
+
body: normalizeBody(body),
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
function normalizeBody(value) {
|
|
577
|
+
return value
|
|
578
|
+
.replace(/^#+\s+/gm, "")
|
|
579
|
+
.replace(/[`*_>#-]+/g, " ")
|
|
580
|
+
.toLowerCase()
|
|
581
|
+
.replace(/\s+/g, " ")
|
|
582
|
+
.trim();
|
|
583
|
+
}
|
|
584
|
+
function normalizeSignal(value) {
|
|
585
|
+
if (!value)
|
|
586
|
+
return undefined;
|
|
587
|
+
const normalized = value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
588
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
589
|
+
}
|
|
590
|
+
function normalizeList(values) {
|
|
591
|
+
return [
|
|
592
|
+
...new Set(values.map((value) => normalizeSignal(value)).filter((value) => value !== undefined)),
|
|
593
|
+
].sort();
|
|
594
|
+
}
|
|
595
|
+
function computeSignalScore(title, description, tags, searchHints, body) {
|
|
596
|
+
return [title, description, body].join("\n").trim().length + tags.length * 25 + searchHints.length * 10;
|
|
597
|
+
}
|
|
598
|
+
function sortRecordsForSurvival(records) {
|
|
599
|
+
return [...records].sort((a, b) => {
|
|
600
|
+
if (a.canonicalName !== b.canonicalName)
|
|
601
|
+
return a.canonicalName ? -1 : 1;
|
|
602
|
+
if (a.signalScore !== b.signalScore)
|
|
603
|
+
return b.signalScore - a.signalScore;
|
|
604
|
+
return compareRecords(a, b);
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
function compareRecords(a, b) {
|
|
608
|
+
return a.ref.localeCompare(b.ref);
|
|
609
|
+
}
|
|
610
|
+
function compareCandidates(a, b) {
|
|
611
|
+
return a.ref.localeCompare(b.ref);
|
|
612
|
+
}
|
|
613
|
+
function compareContradictionCandidates(a, b) {
|
|
614
|
+
return a.ref.localeCompare(b.ref);
|
|
615
|
+
}
|
|
616
|
+
function compareBeliefTransitions(a, b) {
|
|
617
|
+
return a.ref.localeCompare(b.ref);
|
|
618
|
+
}
|
|
619
|
+
function compareConsolidationCandidates(a, b) {
|
|
620
|
+
return a.parentRef.localeCompare(b.parentRef) || a.signal.localeCompare(b.signal);
|
|
621
|
+
}
|
|
622
|
+
function firstExistingRef(refs, byRef, selfRef) {
|
|
623
|
+
for (const ref of refs) {
|
|
624
|
+
if (ref === selfRef)
|
|
625
|
+
continue;
|
|
626
|
+
if (byRef.has(ref))
|
|
627
|
+
return ref;
|
|
628
|
+
}
|
|
629
|
+
return undefined;
|
|
630
|
+
}
|
|
631
|
+
function sameStringArray(a, b) {
|
|
632
|
+
if (a.length !== b.length)
|
|
633
|
+
return false;
|
|
634
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
635
|
+
if (a[index] !== b[index])
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
function extractHeading(content) {
|
|
641
|
+
for (const line of content.split(/\r?\n/)) {
|
|
642
|
+
const match = line.match(/^#\s+(.+)$/);
|
|
643
|
+
if (match?.[1])
|
|
644
|
+
return match[1].trim();
|
|
645
|
+
}
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
|
648
|
+
function firstNonEmpty(values) {
|
|
649
|
+
for (const value of values) {
|
|
650
|
+
if (value && value.trim().length > 0)
|
|
651
|
+
return value;
|
|
652
|
+
}
|
|
653
|
+
return undefined;
|
|
654
|
+
}
|
|
655
|
+
function* walkMarkdownFiles(root) {
|
|
656
|
+
let entries;
|
|
657
|
+
try {
|
|
658
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
for (const entry of entries) {
|
|
664
|
+
const full = path.join(root, entry.name);
|
|
665
|
+
if (entry.isDirectory()) {
|
|
666
|
+
yield* walkMarkdownFiles(full);
|
|
667
|
+
}
|
|
668
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
669
|
+
yield full;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function toMemoryName(memoriesDir, filePath) {
|
|
674
|
+
const rel = path.relative(memoriesDir, filePath);
|
|
675
|
+
if (!rel || rel.startsWith(".."))
|
|
676
|
+
return undefined;
|
|
677
|
+
return rel.replace(/\\/g, "/").replace(/\.md$/i, "");
|
|
678
|
+
}
|