akm-cli 0.7.5 → 0.8.0-rc.11
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 → CHANGELOG.md} +192 -2
- package/README.md +22 -6
- package/SECURITY.md +93 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +133 -0
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2569 -1449
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +110 -0
- package/dist/commands/agent-support.js +68 -0
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +130 -534
- package/dist/commands/consolidate.js +2122 -0
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +660 -0
- package/dist/commands/distill.js +1075 -77
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +43 -0
- package/dist/commands/events.js +5 -23
- package/dist/commands/extract-cli.js +127 -0
- package/dist/commands/extract-prompt.js +204 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/feedback-cli.js +331 -0
- package/dist/commands/graph.js +477 -0
- package/dist/commands/health.js +1302 -0
- package/dist/commands/help/help-accept.md +12 -0
- package/dist/commands/help/help-improve.md +69 -0
- package/dist/commands/help/help-proposals.md +18 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +11 -0
- package/dist/commands/history.js +54 -46
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +217 -0
- package/dist/commands/improve-profiles.js +166 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +2373 -0
- package/dist/commands/info.js +5 -2
- package/dist/commands/init.js +50 -2
- package/dist/commands/installed-stashes.js +102 -139
- package/dist/commands/knowledge.js +136 -0
- package/dist/commands/lint/agent-linter.js +49 -0
- package/dist/commands/lint/base-linter.js +479 -0
- package/dist/commands/lint/command-linter.js +49 -0
- package/dist/commands/lint/default-linter.js +16 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +196 -0
- package/dist/commands/lint/knowledge-linter.js +16 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +61 -0
- package/dist/commands/lint/registry.js +36 -0
- package/dist/commands/lint/skill-linter.js +45 -0
- package/dist/commands/lint/task-linter.js +50 -0
- package/dist/commands/lint/types.js +4 -0
- package/dist/commands/lint/workflow-linter.js +56 -0
- package/dist/commands/lint.js +4 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal.js +67 -12
- package/dist/commands/propose.js +86 -31
- package/dist/commands/reflect.js +1091 -73
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +5 -2
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +69 -6
- package/dist/commands/schema-repair.js +203 -0
- package/dist/commands/search.js +115 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +148 -25
- package/dist/commands/source-add.js +17 -45
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +14 -19
- package/dist/commands/tasks.js +437 -0
- package/dist/commands/url-checker.js +42 -0
- package/dist/core/action-contributors.js +28 -0
- package/dist/core/asset-ref.js +17 -2
- package/dist/core/asset-registry.js +12 -17
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +67 -1
- package/dist/core/common.js +182 -0
- package/dist/core/concurrent.js +25 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +534 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +364 -981
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +91 -138
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -8
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +20 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +806 -0
- package/dist/core/parse.js +158 -0
- package/dist/core/paths.js +280 -14
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +69 -0
- package/dist/core/proposals.js +512 -42
- package/dist/core/state-db.js +1068 -0
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +54 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +64 -1
- package/dist/core/write-source.js +3 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +178 -256
- package/dist/indexer/db.js +975 -103
- package/dist/indexer/ensure-index.js +64 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +376 -101
- package/dist/indexer/graph-db.js +391 -0
- package/dist/indexer/graph-dedup.js +95 -0
- package/dist/indexer/graph-extraction.js +550 -124
- package/dist/indexer/index-context.js +4 -0
- package/dist/indexer/indexer.js +523 -301
- package/dist/indexer/llm-cache.js +52 -0
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +167 -160
- package/dist/indexer/memory-inference.js +152 -74
- package/dist/indexer/metadata-contributors.js +29 -0
- package/dist/indexer/metadata.js +275 -196
- package/dist/indexer/path-resolver.js +92 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +331 -0
- package/dist/indexer/ranking.js +81 -0
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +111 -0
- package/dist/indexer/search-source.js +44 -10
- package/dist/indexer/semantic-status.js +6 -17
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +28 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +122 -230
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +7 -13
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +70 -5
- package/dist/integrations/agent/prompts.js +214 -80
- package/dist/integrations/agent/runner.js +151 -0
- package/dist/integrations/agent/sdk-runner.js +126 -0
- package/dist/integrations/agent/spawn.js +118 -23
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +32 -69
- package/dist/integrations/session-logs/index.js +69 -0
- package/dist/integrations/session-logs/inline-refs.js +35 -0
- package/dist/integrations/session-logs/pre-filter.js +152 -0
- package/dist/integrations/session-logs/providers/claude-code.js +282 -0
- package/dist/integrations/session-logs/providers/opencode.js +258 -0
- package/dist/integrations/session-logs/types.js +4 -0
- package/dist/llm/call-ai.js +62 -0
- package/dist/llm/client.js +77 -124
- package/dist/llm/embedder.js +20 -29
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +42 -1
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +95 -48
- package/dist/llm/graph-extract.js +676 -70
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +77 -71
- package/dist/llm/metadata-enhance.js +42 -29
- package/dist/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/output/cli-hints-full.md +292 -0
- package/dist/output/cli-hints-short.md +66 -0
- package/dist/output/cli-hints.js +7 -320
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +300 -257
- package/dist/output/shapes/curate.js +56 -0
- package/dist/output/shapes/distill.js +10 -0
- package/dist/output/shapes/env-list.js +19 -0
- package/dist/output/shapes/events.js +11 -0
- package/dist/output/shapes/helpers.js +424 -0
- package/dist/output/shapes/history.js +7 -0
- package/dist/output/shapes/passthrough.js +102 -0
- package/dist/output/shapes/proposal-accept.js +7 -0
- package/dist/output/shapes/proposal-diff.js +7 -0
- package/dist/output/shapes/proposal-list.js +7 -0
- package/dist/output/shapes/proposal-producer.js +11 -0
- package/dist/output/shapes/proposal-reject.js +7 -0
- package/dist/output/shapes/proposal-show.js +7 -0
- package/dist/output/shapes/registry-search.js +6 -0
- package/dist/output/shapes/registry.js +30 -0
- package/dist/output/shapes/search.js +6 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes/show.js +6 -0
- package/dist/output/shapes/vault-list.js +19 -0
- package/dist/output/shapes.js +51 -516
- package/dist/output/text/add.js +6 -0
- package/dist/output/text/clone.js +6 -0
- package/dist/output/text/config.js +6 -0
- package/dist/output/text/curate.js +6 -0
- package/dist/output/text/distill.js +7 -0
- package/dist/output/text/enable-disable.js +7 -0
- package/dist/output/text/events.js +10 -0
- package/dist/output/text/feedback.js +6 -0
- package/dist/output/text/helpers.js +1039 -0
- package/dist/output/text/history.js +7 -0
- package/dist/output/text/import.js +6 -0
- package/dist/output/text/index.js +6 -0
- package/dist/output/text/info.js +6 -0
- package/dist/output/text/init.js +6 -0
- package/dist/output/text/list.js +6 -0
- package/dist/output/text/proposal-producer.js +8 -0
- package/dist/output/text/proposal.js +11 -0
- package/dist/output/text/registry-commands.js +11 -0
- package/dist/output/text/registry.js +30 -0
- package/dist/output/text/remember.js +6 -0
- package/dist/output/text/remove.js +6 -0
- package/dist/output/text/save.js +6 -0
- package/dist/output/text/search.js +6 -0
- package/dist/output/text/show.js +6 -0
- package/dist/output/text/update.js +6 -0
- package/dist/output/text/upgrade.js +6 -0
- package/dist/output/text/vault.js +16 -0
- package/dist/output/text/wiki.js +15 -0
- package/dist/output/text/workflow.js +14 -0
- package/dist/output/text.js +44 -1092
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +4 -1
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +71 -50
- package/dist/registry/providers/static-index.js +53 -48
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17750 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +775 -37
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +5 -12
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +138 -21
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +7 -0
- package/dist/tasks/backends/cron.js +203 -0
- package/dist/tasks/backends/exec-utils.js +28 -0
- package/dist/tasks/backends/index.js +24 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +187 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +215 -0
- package/dist/tasks/parser.js +211 -0
- package/dist/tasks/resolveAkmBin.js +87 -0
- package/dist/tasks/runner.js +458 -0
- package/dist/tasks/schedule.js +227 -0
- package/dist/tasks/schema.js +15 -0
- package/dist/tasks/validator.js +62 -0
- package/dist/version.js +3 -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 +15 -0
- package/dist/wiki/wiki.js +13 -61
- package/dist/workflows/authoring.js +8 -25
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +11 -3
- package/dist/workflows/runs.js +77 -92
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +4 -8
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +10 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +48 -0
- package/docs/migration/v0.7-to-v0.8.md +1307 -0
- package/package.json +30 -12
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -381
- package/dist/commands/vault.js +0 -328
- package/dist/templates/wiki-templates.js +0 -100
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { makeAssetRef, parseAssetRef } from "./asset-ref";
|
|
7
|
+
import { assembleAsset } from "./asset-serialize";
|
|
8
|
+
import { firstString, groupBy, stringArray } from "./common";
|
|
9
|
+
import { parseFrontmatter } from "./frontmatter";
|
|
10
|
+
const DERIVED_SUFFIX = ".derived";
|
|
11
|
+
export function analyzeMemoryCleanup(stashDir, options = {}) {
|
|
12
|
+
const records = collectDerivedMemories(stashDir, options.parentRef);
|
|
13
|
+
const byRef = new Map(records.map((record) => [record.ref, record]));
|
|
14
|
+
const byParent = groupBy(records, (record) => record.parentRef);
|
|
15
|
+
const planned = new Map();
|
|
16
|
+
const contradictionCandidates = [];
|
|
17
|
+
const beliefTransitions = new Map();
|
|
18
|
+
const planPrune = (record, reason, survivorRef) => {
|
|
19
|
+
const existing = planned.get(record.ref);
|
|
20
|
+
if (existing)
|
|
21
|
+
return existing;
|
|
22
|
+
const next = {
|
|
23
|
+
ref: record.ref,
|
|
24
|
+
parentRef: record.parentRef,
|
|
25
|
+
reason,
|
|
26
|
+
...(survivorRef ? { survivorRef } : {}),
|
|
27
|
+
filePath: record.filePath,
|
|
28
|
+
};
|
|
29
|
+
planned.set(record.ref, next);
|
|
30
|
+
return next;
|
|
31
|
+
};
|
|
32
|
+
const planBeliefTransition = (record, toState, reason, currentBeliefRefs = []) => {
|
|
33
|
+
const normalizedRefs = [...new Set(currentBeliefRefs)].sort();
|
|
34
|
+
const metadataChanged = !sameStringArray(record.currentBeliefRefs, normalizedRefs) ||
|
|
35
|
+
(toState === "contradicted"
|
|
36
|
+
? !sameStringArray(record.contradictedBy, normalizedRefs)
|
|
37
|
+
: record.contradictedBy.length > 0);
|
|
38
|
+
if (record.beliefState === toState && !metadataChanged)
|
|
39
|
+
return;
|
|
40
|
+
const existing = beliefTransitions.get(record.ref);
|
|
41
|
+
if (existing)
|
|
42
|
+
return existing;
|
|
43
|
+
const next = {
|
|
44
|
+
ref: record.ref,
|
|
45
|
+
parentRef: record.parentRef,
|
|
46
|
+
fromState: record.beliefState,
|
|
47
|
+
toState,
|
|
48
|
+
reason,
|
|
49
|
+
...(normalizedRefs[0] ? { relatedRef: normalizedRefs[0] } : {}),
|
|
50
|
+
...(normalizedRefs.length > 0 ? { relatedRefs: normalizedRefs, currentBeliefRefs: normalizedRefs } : {}),
|
|
51
|
+
};
|
|
52
|
+
beliefTransitions.set(record.ref, next);
|
|
53
|
+
return next;
|
|
54
|
+
};
|
|
55
|
+
for (const record of records) {
|
|
56
|
+
const supersededTarget = firstExistingRef(record.supersededBy, byRef, record.ref);
|
|
57
|
+
if (supersededTarget) {
|
|
58
|
+
planPrune(record, "superseded-derived", supersededTarget);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (record.obsolete) {
|
|
62
|
+
planPrune(record, "obsolete-derived");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const excludedRefs = new Set(planned.keys());
|
|
66
|
+
for (const family of byParent.values()) {
|
|
67
|
+
const activeFamily = family.filter((record) => !excludedRefs.has(record.ref));
|
|
68
|
+
const resolution = resolveFamilyContradictions(activeFamily);
|
|
69
|
+
for (const candidate of resolution.contradictionCandidates) {
|
|
70
|
+
contradictionCandidates.push(candidate);
|
|
71
|
+
}
|
|
72
|
+
for (const transition of resolution.transitions) {
|
|
73
|
+
const record = byRef.get(transition.ref);
|
|
74
|
+
if (!record)
|
|
75
|
+
continue;
|
|
76
|
+
planBeliefTransition(record, transition.toState, transition.reason, transition.currentBeliefRefs ?? []);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const excludedForDuplicateDetection = new Set([
|
|
80
|
+
...planned.keys(),
|
|
81
|
+
...contradictionCandidates.map((candidate) => candidate.ref),
|
|
82
|
+
]);
|
|
83
|
+
for (const family of byParent.values()) {
|
|
84
|
+
const active = family.filter((record) => !excludedForDuplicateDetection.has(record.ref));
|
|
85
|
+
const byFingerprint = groupBy(active, (record) => record.fingerprint);
|
|
86
|
+
for (const duplicates of byFingerprint.values()) {
|
|
87
|
+
if (duplicates.length < 2)
|
|
88
|
+
continue;
|
|
89
|
+
const [survivor, ...rest] = sortRecordsForSurvival(duplicates);
|
|
90
|
+
for (const duplicate of rest) {
|
|
91
|
+
planPrune(duplicate, "duplicate-derived", survivor.ref);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const consolidationCandidates = [];
|
|
96
|
+
const excludedForConsolidation = new Set([
|
|
97
|
+
...planned.keys(),
|
|
98
|
+
...contradictionCandidates.map((candidate) => candidate.ref),
|
|
99
|
+
]);
|
|
100
|
+
for (const [parentRef, family] of byParent.entries()) {
|
|
101
|
+
const active = family.filter((record) => !excludedForConsolidation.has(record.ref));
|
|
102
|
+
if (active.length < 2)
|
|
103
|
+
continue;
|
|
104
|
+
const bySignal = groupBy(active.filter((record) => record.signalKey !== undefined), (record) => record.signalKey);
|
|
105
|
+
for (const [signal, signalRecords] of bySignal.entries()) {
|
|
106
|
+
if (signalRecords.length < 2)
|
|
107
|
+
continue;
|
|
108
|
+
const ordered = sortRecordsForSurvival(signalRecords);
|
|
109
|
+
consolidationCandidates.push({
|
|
110
|
+
parentRef,
|
|
111
|
+
signal,
|
|
112
|
+
refs: ordered.map((record) => record.ref),
|
|
113
|
+
suggestedSurvivorRef: ordered[0].ref,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const RELATIVE_DATE_RE = /\b(yesterday|last week|last month|last year|\d+ days? ago|\d+ weeks? ago|\d+ months? ago)\b/gi;
|
|
118
|
+
const relativeDateCandidates = [];
|
|
119
|
+
for (const record of records) {
|
|
120
|
+
const matches = record.body.match(RELATIVE_DATE_RE);
|
|
121
|
+
if (matches && matches.length > 0) {
|
|
122
|
+
relativeDateCandidates.push({
|
|
123
|
+
ref: record.ref,
|
|
124
|
+
filePath: record.filePath,
|
|
125
|
+
matches: [...new Set(matches.map((m) => m.toLowerCase()))],
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
analyzedDerived: records.length,
|
|
131
|
+
pruneCandidates: [...planned.values()]
|
|
132
|
+
.map(({ filePath: _filePath, ...candidate }) => candidate)
|
|
133
|
+
.sort(compareCandidates),
|
|
134
|
+
contradictionCandidates: contradictionCandidates.sort(compareContradictionCandidates),
|
|
135
|
+
beliefStateTransitions: [...beliefTransitions.values()].sort(compareBeliefTransitions),
|
|
136
|
+
consolidationCandidates: consolidationCandidates.sort(compareConsolidationCandidates),
|
|
137
|
+
relativeDateCandidates,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export function applyMemoryCleanup(stashDir, plan) {
|
|
141
|
+
const records = collectDerivedMemories(stashDir);
|
|
142
|
+
const fileByRef = new Map(records.map((record) => [record.ref, record.filePath]));
|
|
143
|
+
const archived = [];
|
|
144
|
+
const appliedBeliefTransitions = [];
|
|
145
|
+
const warnings = [];
|
|
146
|
+
for (const transition of plan.beliefStateTransitions) {
|
|
147
|
+
const filePath = fileByRef.get(transition.ref);
|
|
148
|
+
if (!filePath)
|
|
149
|
+
continue;
|
|
150
|
+
try {
|
|
151
|
+
persistBeliefStateTransition(filePath, transition);
|
|
152
|
+
appliedBeliefTransitions.push(transition);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
warnings.push(formatApplyWarning("belief-transition", transition.ref, error));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
let transitionLogPath;
|
|
159
|
+
if (appliedBeliefTransitions.length > 0) {
|
|
160
|
+
try {
|
|
161
|
+
transitionLogPath = appendBeliefStateTransitionLog(stashDir, appliedBeliefTransitions);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
warnings.push(formatApplyWarning("transition-log", "memory-cleanup", error));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const candidate of plan.pruneCandidates) {
|
|
168
|
+
const filePath = fileByRef.get(candidate.ref);
|
|
169
|
+
if (!filePath)
|
|
170
|
+
continue;
|
|
171
|
+
try {
|
|
172
|
+
archived.push(archiveCleanupCandidate(stashDir, candidate, filePath));
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
warnings.push(formatApplyWarning("archive", candidate.ref, error));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// M-5 / #396: Resolve relative dates for flagged candidates.
|
|
179
|
+
// Anchor: use the file's `createdAt` frontmatter field, or fall back to the
|
|
180
|
+
// file's mtime. Graphiti arXiv:2501.13956, HeidelTime (Strötgen & Gertz 2010).
|
|
181
|
+
let relativeDatesResolved = 0;
|
|
182
|
+
for (const candidate of plan.relativeDateCandidates) {
|
|
183
|
+
try {
|
|
184
|
+
const raw = fs.readFileSync(candidate.filePath, "utf8");
|
|
185
|
+
const fm = parseFrontmatter(raw);
|
|
186
|
+
const createdAtStr = fm.data.createdAt;
|
|
187
|
+
let referenceDate;
|
|
188
|
+
if (createdAtStr) {
|
|
189
|
+
const parsed = new Date(createdAtStr);
|
|
190
|
+
referenceDate = Number.isNaN(parsed.getTime()) ? new Date(fs.statSync(candidate.filePath).mtimeMs) : parsed;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
referenceDate = new Date(fs.statSync(candidate.filePath).mtimeMs);
|
|
194
|
+
}
|
|
195
|
+
const resolvedBody = resolveRelativeDates(fm.content ?? "", referenceDate);
|
|
196
|
+
if (resolvedBody === (fm.content ?? ""))
|
|
197
|
+
continue; // no change
|
|
198
|
+
const newContent = assembleAsset(fm.data, resolvedBody);
|
|
199
|
+
fs.writeFileSync(candidate.filePath, newContent, "utf8");
|
|
200
|
+
relativeDatesResolved++;
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
warnings.push(formatApplyWarning("relative-date-resolve", candidate.ref, error));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
archived.sort((a, b) => a.ref.localeCompare(b.ref));
|
|
207
|
+
appliedBeliefTransitions.sort(compareBeliefTransitions);
|
|
208
|
+
return {
|
|
209
|
+
archived,
|
|
210
|
+
beliefStateTransitions: appliedBeliefTransitions,
|
|
211
|
+
...(transitionLogPath ? { transitionLogPath: path.relative(stashDir, transitionLogPath).replace(/\\/g, "/") } : {}),
|
|
212
|
+
...(transitionLogPath ? { transitionLogEntries: appliedBeliefTransitions.length } : {}),
|
|
213
|
+
...(relativeDatesResolved > 0 ? { relativeDatesResolved } : {}),
|
|
214
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function formatApplyWarning(stage, ref, error) {
|
|
218
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
219
|
+
return `${stage} failed for ${ref}: ${detail}`;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* M-5 / #396: Resolve relative date expressions to absolute ISO dates.
|
|
223
|
+
*
|
|
224
|
+
* Uses `referenceDate` as the anchor point (Graphiti arXiv:2501.13956,
|
|
225
|
+
* HeidelTime Strötgen & Gertz 2010 — document creation time as reference).
|
|
226
|
+
* Replaces patterns like "yesterday", "3 days ago", "last week" with their
|
|
227
|
+
* ISO 8601 date string (YYYY-MM-DD).
|
|
228
|
+
*
|
|
229
|
+
* Returns the rewritten string; returns the original if no matches.
|
|
230
|
+
*/
|
|
231
|
+
function resolveRelativeDates(text, referenceDate) {
|
|
232
|
+
const RELATIVE_DATE_RE = /\b(yesterday|last week|last month|last year|\d+ days? ago|\d+ weeks? ago|\d+ months? ago)\b/gi;
|
|
233
|
+
return text.replace(RELATIVE_DATE_RE, (match) => {
|
|
234
|
+
const lower = match.toLowerCase().trim();
|
|
235
|
+
const d = new Date(referenceDate);
|
|
236
|
+
if (lower === "yesterday") {
|
|
237
|
+
d.setDate(d.getDate() - 1);
|
|
238
|
+
}
|
|
239
|
+
else if (lower === "last week") {
|
|
240
|
+
d.setDate(d.getDate() - 7);
|
|
241
|
+
}
|
|
242
|
+
else if (lower === "last month") {
|
|
243
|
+
d.setMonth(d.getMonth() - 1);
|
|
244
|
+
}
|
|
245
|
+
else if (lower === "last year") {
|
|
246
|
+
d.setFullYear(d.getFullYear() - 1);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
const numMatch = lower.match(/^(\d+)\s+(day|week|month)s?\s+ago$/);
|
|
250
|
+
if (numMatch) {
|
|
251
|
+
const n = Number.parseInt(numMatch[1] ?? "0", 10);
|
|
252
|
+
const unit = numMatch[2] ?? "day";
|
|
253
|
+
if (unit === "day")
|
|
254
|
+
d.setDate(d.getDate() - n);
|
|
255
|
+
else if (unit === "week")
|
|
256
|
+
d.setDate(d.getDate() - n * 7);
|
|
257
|
+
else if (unit === "month")
|
|
258
|
+
d.setMonth(d.getMonth() - n);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
return match; // unrecognized pattern — leave as-is
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return d.toISOString().slice(0, 10); // YYYY-MM-DD
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
function resolveFamilyContradictions(family) {
|
|
268
|
+
if (family.length === 0)
|
|
269
|
+
return { contradictionCandidates: [], transitions: [] };
|
|
270
|
+
const familyRefSet = new Set(family.map((record) => record.ref));
|
|
271
|
+
const edges = new Map();
|
|
272
|
+
let edgeCount = 0;
|
|
273
|
+
for (const record of family) {
|
|
274
|
+
const targets = [
|
|
275
|
+
...new Set(record.contradictedBy.filter((ref) => ref !== record.ref && familyRefSet.has(ref))),
|
|
276
|
+
].sort();
|
|
277
|
+
edges.set(record.ref, targets);
|
|
278
|
+
edgeCount += targets.length;
|
|
279
|
+
}
|
|
280
|
+
if (edgeCount === 0) {
|
|
281
|
+
return {
|
|
282
|
+
contradictionCandidates: [],
|
|
283
|
+
transitions: family
|
|
284
|
+
.filter((record) => {
|
|
285
|
+
// `deprecated` is a frozen historical state — never refresh it to active.
|
|
286
|
+
// (`superseded` is intentionally still refreshable to preserve pre-Phase-1A behavior.)
|
|
287
|
+
if (isFrozenHistoricalBeliefState(record.beliefState))
|
|
288
|
+
return false;
|
|
289
|
+
// `active` and `asserted` are both "current/believed" states. Only emit a
|
|
290
|
+
// refresh if there is something to clear (contradictions / currentBeliefRefs)
|
|
291
|
+
// or the state is something else entirely (e.g. lingering `contradicted`).
|
|
292
|
+
if (isActiveLikeBeliefState(record.beliefState)) {
|
|
293
|
+
return record.contradictedBy.length > 0 || record.currentBeliefRefs.length > 0;
|
|
294
|
+
}
|
|
295
|
+
return true;
|
|
296
|
+
})
|
|
297
|
+
.map((record) => ({
|
|
298
|
+
ref: record.ref,
|
|
299
|
+
parentRef: record.parentRef,
|
|
300
|
+
fromState: record.beliefState,
|
|
301
|
+
// Preserve `asserted` authority; otherwise refresh to plain `active`.
|
|
302
|
+
toState: record.beliefState === "asserted" ? "asserted" : "active",
|
|
303
|
+
reason: "belief-refresh",
|
|
304
|
+
})),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const { components, componentIndexByRef } = stronglyConnectedComponents(family.map((record) => record.ref), edges);
|
|
308
|
+
const outgoingComponents = new Map();
|
|
309
|
+
for (let index = 0; index < components.length; index += 1) {
|
|
310
|
+
outgoingComponents.set(index, new Set());
|
|
311
|
+
}
|
|
312
|
+
for (const [ref, targets] of edges.entries()) {
|
|
313
|
+
const fromIndex = componentIndexByRef.get(ref);
|
|
314
|
+
if (fromIndex === undefined)
|
|
315
|
+
continue;
|
|
316
|
+
for (const target of targets) {
|
|
317
|
+
const toIndex = componentIndexByRef.get(target);
|
|
318
|
+
if (toIndex === undefined || toIndex === fromIndex)
|
|
319
|
+
continue;
|
|
320
|
+
outgoingComponents.get(fromIndex)?.add(toIndex);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const sinkComponents = new Set();
|
|
324
|
+
for (const [index, outgoing] of outgoingComponents.entries()) {
|
|
325
|
+
if (outgoing.size === 0)
|
|
326
|
+
sinkComponents.add(index);
|
|
327
|
+
}
|
|
328
|
+
const reachableSinkRefsMemo = new Map();
|
|
329
|
+
const reachableSinkRefsForComponent = (index) => {
|
|
330
|
+
const memoized = reachableSinkRefsMemo.get(index);
|
|
331
|
+
if (memoized)
|
|
332
|
+
return memoized;
|
|
333
|
+
const outgoing = outgoingComponents.get(index);
|
|
334
|
+
if (!outgoing || outgoing.size === 0) {
|
|
335
|
+
const refs = [...components[index]].sort();
|
|
336
|
+
reachableSinkRefsMemo.set(index, refs);
|
|
337
|
+
return refs;
|
|
338
|
+
}
|
|
339
|
+
const refs = new Set();
|
|
340
|
+
for (const nextIndex of outgoing) {
|
|
341
|
+
for (const ref of reachableSinkRefsForComponent(nextIndex))
|
|
342
|
+
refs.add(ref);
|
|
343
|
+
}
|
|
344
|
+
const resolved = [...refs].sort();
|
|
345
|
+
reachableSinkRefsMemo.set(index, resolved);
|
|
346
|
+
return resolved;
|
|
347
|
+
};
|
|
348
|
+
const contradictionCandidates = [];
|
|
349
|
+
const transitions = [];
|
|
350
|
+
for (const record of family) {
|
|
351
|
+
const componentIndex = componentIndexByRef.get(record.ref);
|
|
352
|
+
if (componentIndex === undefined)
|
|
353
|
+
continue;
|
|
354
|
+
const isCurrentComponent = sinkComponents.has(componentIndex);
|
|
355
|
+
const currentRefs = reachableSinkRefsForComponent(componentIndex);
|
|
356
|
+
if (!isCurrentComponent) {
|
|
357
|
+
contradictionCandidates.push({
|
|
358
|
+
ref: record.ref,
|
|
359
|
+
parentRef: record.parentRef,
|
|
360
|
+
reason: "contradicted-derived",
|
|
361
|
+
contradictedByRef: currentRefs[0],
|
|
362
|
+
contradictedByRefs: currentRefs,
|
|
363
|
+
currentBeliefRefs: currentRefs,
|
|
364
|
+
});
|
|
365
|
+
if (record.beliefState !== "contradicted" ||
|
|
366
|
+
!sameStringArray(record.contradictedBy, currentRefs) ||
|
|
367
|
+
!sameStringArray(record.currentBeliefRefs, currentRefs)) {
|
|
368
|
+
transitions.push({
|
|
369
|
+
ref: record.ref,
|
|
370
|
+
parentRef: record.parentRef,
|
|
371
|
+
fromState: record.beliefState,
|
|
372
|
+
toState: "contradicted",
|
|
373
|
+
reason: "contradicted-derived",
|
|
374
|
+
relatedRef: currentRefs[0],
|
|
375
|
+
relatedRefs: currentRefs,
|
|
376
|
+
currentBeliefRefs: currentRefs,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const componentRefs = [...components[componentIndex]].sort();
|
|
382
|
+
const peerCurrentRefs = componentRefs.filter((ref) => ref !== record.ref);
|
|
383
|
+
// `deprecated` is a frozen historical state — never refresh to active.
|
|
384
|
+
// (`superseded` is intentionally still refreshable to preserve pre-Phase-1A behavior.)
|
|
385
|
+
if (isFrozenHistoricalBeliefState(record.beliefState)) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
// For `active` / `asserted` records, only refresh when something changes.
|
|
389
|
+
// For everything else (e.g. lingering `contradicted`) always refresh.
|
|
390
|
+
const isActiveLike = isActiveLikeBeliefState(record.beliefState);
|
|
391
|
+
const needsRefresh = !isActiveLike || record.contradictedBy.length > 0 || !sameStringArray(record.currentBeliefRefs, peerCurrentRefs);
|
|
392
|
+
if (needsRefresh) {
|
|
393
|
+
transitions.push({
|
|
394
|
+
ref: record.ref,
|
|
395
|
+
parentRef: record.parentRef,
|
|
396
|
+
fromState: record.beliefState,
|
|
397
|
+
// Preserve `asserted` authority; otherwise refresh to plain `active`.
|
|
398
|
+
toState: record.beliefState === "asserted" ? "asserted" : "active",
|
|
399
|
+
reason: "belief-refresh",
|
|
400
|
+
...(peerCurrentRefs[0] ? { relatedRef: peerCurrentRefs[0], relatedRefs: peerCurrentRefs } : {}),
|
|
401
|
+
...(peerCurrentRefs.length > 0 ? { currentBeliefRefs: peerCurrentRefs } : {}),
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
contradictionCandidates: contradictionCandidates.sort(compareContradictionCandidates),
|
|
407
|
+
transitions: transitions.sort(compareBeliefTransitions),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function stronglyConnectedComponents(refs, edges) {
|
|
411
|
+
let index = 0;
|
|
412
|
+
const indices = new Map();
|
|
413
|
+
const lowLinks = new Map();
|
|
414
|
+
const stack = [];
|
|
415
|
+
const onStack = new Set();
|
|
416
|
+
const components = [];
|
|
417
|
+
const visit = (ref) => {
|
|
418
|
+
indices.set(ref, index);
|
|
419
|
+
lowLinks.set(ref, index);
|
|
420
|
+
index += 1;
|
|
421
|
+
stack.push(ref);
|
|
422
|
+
onStack.add(ref);
|
|
423
|
+
for (const target of edges.get(ref) ?? []) {
|
|
424
|
+
if (!indices.has(target)) {
|
|
425
|
+
visit(target);
|
|
426
|
+
lowLinks.set(ref, Math.min(lowLinks.get(ref) ?? 0, lowLinks.get(target) ?? 0));
|
|
427
|
+
}
|
|
428
|
+
else if (onStack.has(target)) {
|
|
429
|
+
lowLinks.set(ref, Math.min(lowLinks.get(ref) ?? 0, indices.get(target) ?? 0));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if ((lowLinks.get(ref) ?? -1) !== (indices.get(ref) ?? -2))
|
|
433
|
+
return;
|
|
434
|
+
const component = [];
|
|
435
|
+
while (stack.length > 0) {
|
|
436
|
+
const member = stack.pop();
|
|
437
|
+
onStack.delete(member);
|
|
438
|
+
component.push(member);
|
|
439
|
+
if (member === ref)
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
components.push(component.sort());
|
|
443
|
+
};
|
|
444
|
+
for (const ref of refs) {
|
|
445
|
+
if (!indices.has(ref))
|
|
446
|
+
visit(ref);
|
|
447
|
+
}
|
|
448
|
+
const componentIndexByRef = new Map();
|
|
449
|
+
for (let componentIndex = 0; componentIndex < components.length; componentIndex += 1) {
|
|
450
|
+
for (const ref of components[componentIndex]) {
|
|
451
|
+
componentIndexByRef.set(ref, componentIndex);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return { components, componentIndexByRef };
|
|
455
|
+
}
|
|
456
|
+
function archiveCleanupCandidate(stashDir, candidate, filePath) {
|
|
457
|
+
const archivedAt = new Date().toISOString();
|
|
458
|
+
const originalPath = path.relative(stashDir, filePath).replace(/\\/g, "/");
|
|
459
|
+
const archiveDir = createArchiveDir(stashDir, candidate.ref, archivedAt);
|
|
460
|
+
const archivedPath = path.join(archiveDir, originalPath);
|
|
461
|
+
fs.mkdirSync(path.dirname(archivedPath), { recursive: true });
|
|
462
|
+
fs.renameSync(filePath, archivedPath);
|
|
463
|
+
const archiveRef = path.relative(stashDir, archivedPath).replace(/\\/g, "/");
|
|
464
|
+
const auditPath = path.join(archiveDir, "cleanup.md");
|
|
465
|
+
const auditRef = path.relative(stashDir, auditPath).replace(/\\/g, "/");
|
|
466
|
+
const auditAsset = assembleAsset({
|
|
467
|
+
schemaVersion: 1,
|
|
468
|
+
kind: "memory-cleanup-archive",
|
|
469
|
+
archivedAt,
|
|
470
|
+
beliefState: "archived",
|
|
471
|
+
previousBeliefState: priorBeliefStateForArchive(candidate),
|
|
472
|
+
ref: candidate.ref,
|
|
473
|
+
parentRef: candidate.parentRef,
|
|
474
|
+
reason: candidate.reason,
|
|
475
|
+
...(candidate.survivorRef ? { survivorRef: candidate.survivorRef } : {}),
|
|
476
|
+
originalPath,
|
|
477
|
+
archivedPath: archiveRef,
|
|
478
|
+
}, "Archived derived memory for recoverable cleanup.\n");
|
|
479
|
+
fs.writeFileSync(auditPath, auditAsset, "utf8");
|
|
480
|
+
return {
|
|
481
|
+
ref: candidate.ref,
|
|
482
|
+
parentRef: candidate.parentRef,
|
|
483
|
+
reason: candidate.reason,
|
|
484
|
+
beliefState: "archived",
|
|
485
|
+
previousBeliefState: priorBeliefStateForArchive(candidate),
|
|
486
|
+
...(candidate.survivorRef ? { survivorRef: candidate.survivorRef } : {}),
|
|
487
|
+
originalPath,
|
|
488
|
+
archivedPath: archiveRef,
|
|
489
|
+
auditPath: auditRef,
|
|
490
|
+
archivedAt,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function persistBeliefStateTransition(filePath, transition) {
|
|
494
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
495
|
+
const parsed = parseFrontmatter(raw);
|
|
496
|
+
const nextFrontmatter = {
|
|
497
|
+
...parsed.data,
|
|
498
|
+
beliefState: transition.toState,
|
|
499
|
+
};
|
|
500
|
+
const currentBeliefRefs = [...new Set(transition.currentBeliefRefs ?? [])].sort();
|
|
501
|
+
if (transition.toState === "contradicted") {
|
|
502
|
+
nextFrontmatter.contradictedBy = [...currentBeliefRefs];
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
delete nextFrontmatter.contradictedBy;
|
|
506
|
+
if (parsed.data.supersededBy !== undefined && refArray(parsed.data.supersededBy).length === 0) {
|
|
507
|
+
delete nextFrontmatter.supersededBy;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (currentBeliefRefs.length > 0)
|
|
511
|
+
nextFrontmatter.currentBeliefRefs = [...currentBeliefRefs];
|
|
512
|
+
else
|
|
513
|
+
delete nextFrontmatter.currentBeliefRefs;
|
|
514
|
+
fs.writeFileSync(filePath, assembleAsset(nextFrontmatter, parsed.content), "utf8");
|
|
515
|
+
}
|
|
516
|
+
function appendBeliefStateTransitionLog(stashDir, transitions) {
|
|
517
|
+
const logDir = path.join(stashDir, ".akm", "memory-cleanup");
|
|
518
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
519
|
+
const logPath = path.join(logDir, "belief-transitions.jsonl");
|
|
520
|
+
const appliedAt = new Date().toISOString();
|
|
521
|
+
const lines = transitions
|
|
522
|
+
.map((transition) => JSON.stringify({
|
|
523
|
+
appliedAt,
|
|
524
|
+
ref: transition.ref,
|
|
525
|
+
parentRef: transition.parentRef,
|
|
526
|
+
fromState: transition.fromState,
|
|
527
|
+
toState: transition.toState,
|
|
528
|
+
reason: transition.reason,
|
|
529
|
+
...(transition.relatedRef ? { relatedRef: transition.relatedRef } : {}),
|
|
530
|
+
...(transition.relatedRefs ? { relatedRefs: transition.relatedRefs } : {}),
|
|
531
|
+
...(transition.currentBeliefRefs ? { currentBeliefRefs: transition.currentBeliefRefs } : {}),
|
|
532
|
+
}))
|
|
533
|
+
.join("\n");
|
|
534
|
+
fs.appendFileSync(logPath, `${lines}\n`, "utf8");
|
|
535
|
+
return logPath;
|
|
536
|
+
}
|
|
537
|
+
function priorBeliefStateForArchive(candidate) {
|
|
538
|
+
if (candidate.reason === "superseded-derived")
|
|
539
|
+
return "superseded";
|
|
540
|
+
return "active";
|
|
541
|
+
}
|
|
542
|
+
function createArchiveDir(stashDir, ref, archivedAt) {
|
|
543
|
+
const baseName = `${archivedAt.replace(/[:.]/g, "-")}-${sanitizeRef(ref)}`;
|
|
544
|
+
const root = path.join(stashDir, ".akm", "memory-cleanup", "archive");
|
|
545
|
+
fs.mkdirSync(root, { recursive: true });
|
|
546
|
+
let attempt = 0;
|
|
547
|
+
while (true) {
|
|
548
|
+
const candidate = path.join(root, attempt === 0 ? baseName : `${baseName}-${attempt}`);
|
|
549
|
+
if (!fs.existsSync(candidate)) {
|
|
550
|
+
fs.mkdirSync(candidate, { recursive: true });
|
|
551
|
+
return candidate;
|
|
552
|
+
}
|
|
553
|
+
attempt += 1;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function sanitizeRef(ref) {
|
|
557
|
+
return ref.replace(/[^a-z0-9._-]+/gi, "-");
|
|
558
|
+
}
|
|
559
|
+
function collectDerivedMemories(stashDir, parentRefFilter) {
|
|
560
|
+
const memoriesDir = path.join(stashDir, "memories");
|
|
561
|
+
if (!fs.existsSync(memoriesDir))
|
|
562
|
+
return [];
|
|
563
|
+
const records = [];
|
|
564
|
+
for (const filePath of walkMarkdownFiles(memoriesDir)) {
|
|
565
|
+
const name = toMemoryName(memoriesDir, filePath);
|
|
566
|
+
if (!name)
|
|
567
|
+
continue;
|
|
568
|
+
let raw;
|
|
569
|
+
try {
|
|
570
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
const parsed = parseFrontmatter(raw);
|
|
576
|
+
const parentRef = resolveParentRef(name, parsed.data);
|
|
577
|
+
if (!parentRef)
|
|
578
|
+
continue;
|
|
579
|
+
if (parentRefFilter && parentRef !== parentRefFilter)
|
|
580
|
+
continue;
|
|
581
|
+
if (!isDerivedMemory(name, parsed.data))
|
|
582
|
+
continue;
|
|
583
|
+
const title = firstString(parsed.data.title) ?? extractHeading(parsed.content) ?? "";
|
|
584
|
+
const description = firstString(parsed.data.description) ?? "";
|
|
585
|
+
const tags = stringArray(parsed.data.tags);
|
|
586
|
+
const searchHints = stringArray(parsed.data.searchHints);
|
|
587
|
+
const body = parsed.content.trim();
|
|
588
|
+
const signalKey = normalizeSignal(firstNonEmpty([title, description, searchHints[0]]));
|
|
589
|
+
records.push({
|
|
590
|
+
ref: makeAssetRef("memory", name),
|
|
591
|
+
name,
|
|
592
|
+
filePath,
|
|
593
|
+
parentRef,
|
|
594
|
+
title,
|
|
595
|
+
description,
|
|
596
|
+
tags,
|
|
597
|
+
searchHints,
|
|
598
|
+
body,
|
|
599
|
+
canonicalName: name === `${parentRef.slice("memory:".length)}${DERIVED_SUFFIX}`,
|
|
600
|
+
signalScore: computeSignalScore(title, description, tags, searchHints, body),
|
|
601
|
+
fingerprint: buildFingerprint(title, description, tags, searchHints, body),
|
|
602
|
+
...(signalKey ? { signalKey } : {}),
|
|
603
|
+
supersededBy: refArray(parsed.data.supersededBy),
|
|
604
|
+
contradictedBy: refArray(parsed.data.contradictedBy),
|
|
605
|
+
currentBeliefRefs: refArray(parsed.data.currentBeliefRefs),
|
|
606
|
+
obsolete: parsed.data.obsolete === true || parsed.data.retracted === true,
|
|
607
|
+
beliefState: resolveBeliefState(parsed.data),
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
return records.sort(compareRecords);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* `active` and `asserted` are both "currently believed" states. `asserted` carries
|
|
614
|
+
* stronger user-explicit authority (set by the hot-path `akm remember`) but for
|
|
615
|
+
* state-machine purposes (contradiction resolution, refresh logic) they are
|
|
616
|
+
* equivalent.
|
|
617
|
+
*/
|
|
618
|
+
function isActiveLikeBeliefState(state) {
|
|
619
|
+
return state === "active" || state === "asserted";
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* `deprecated` is a frozen historical state introduced in Phase 1A. Once
|
|
623
|
+
* recorded, the contradiction-resolution pass must not refresh it back to
|
|
624
|
+
* active. (`contradicted` is also historical but it *is* updated by the
|
|
625
|
+
* contradiction resolver, so it is treated separately.)
|
|
626
|
+
*
|
|
627
|
+
* Note: `superseded` is deliberately NOT included here. Pre-Phase-1A,
|
|
628
|
+
* `superseded` records were refreshed to `active` by the belief-refresh
|
|
629
|
+
* pass (the old guard was `record.beliefState !== "active"`). In practice
|
|
630
|
+
* most `superseded` records are pruned earlier via `supersededBy` metadata
|
|
631
|
+
* in `analyzeMemoryCleanup`, so they never reach belief refresh — but a
|
|
632
|
+
* record marked `beliefState: superseded` without `supersededBy` metadata
|
|
633
|
+
* was previously refreshable. Preserving that behavior here avoids a
|
|
634
|
+
* surprise regression; only `deprecated` is the new frozen state.
|
|
635
|
+
*/
|
|
636
|
+
function isFrozenHistoricalBeliefState(state) {
|
|
637
|
+
return state === "deprecated";
|
|
638
|
+
}
|
|
639
|
+
function resolveBeliefState(frontmatter) {
|
|
640
|
+
const explicit = firstString(frontmatter.beliefState);
|
|
641
|
+
if (explicit === "active" ||
|
|
642
|
+
explicit === "asserted" ||
|
|
643
|
+
explicit === "deprecated" ||
|
|
644
|
+
explicit === "superseded" ||
|
|
645
|
+
explicit === "contradicted") {
|
|
646
|
+
return explicit;
|
|
647
|
+
}
|
|
648
|
+
return "active";
|
|
649
|
+
}
|
|
650
|
+
function isDerivedMemory(name, frontmatter) {
|
|
651
|
+
return frontmatter.inferred === true || name.endsWith(DERIVED_SUFFIX);
|
|
652
|
+
}
|
|
653
|
+
function resolveParentRef(name, frontmatter) {
|
|
654
|
+
const fromSource = parseMemoryRef(firstString(frontmatter.source));
|
|
655
|
+
if (fromSource)
|
|
656
|
+
return fromSource;
|
|
657
|
+
const derivedFrom = firstString(frontmatter.derivedFrom);
|
|
658
|
+
if (derivedFrom)
|
|
659
|
+
return makeAssetRef("memory", derivedFrom);
|
|
660
|
+
if (name.endsWith(DERIVED_SUFFIX)) {
|
|
661
|
+
return makeAssetRef("memory", name.slice(0, -DERIVED_SUFFIX.length));
|
|
662
|
+
}
|
|
663
|
+
return undefined;
|
|
664
|
+
}
|
|
665
|
+
function refArray(value) {
|
|
666
|
+
if (typeof value === "string") {
|
|
667
|
+
const parsed = parseMemoryRef(value);
|
|
668
|
+
return parsed ? [parsed] : [];
|
|
669
|
+
}
|
|
670
|
+
if (!Array.isArray(value))
|
|
671
|
+
return [];
|
|
672
|
+
const refs = new Set();
|
|
673
|
+
for (const item of value) {
|
|
674
|
+
if (typeof item !== "string")
|
|
675
|
+
continue;
|
|
676
|
+
const parsed = parseMemoryRef(item);
|
|
677
|
+
if (parsed)
|
|
678
|
+
refs.add(parsed);
|
|
679
|
+
}
|
|
680
|
+
return [...refs].sort();
|
|
681
|
+
}
|
|
682
|
+
function parseMemoryRef(value) {
|
|
683
|
+
if (!value)
|
|
684
|
+
return undefined;
|
|
685
|
+
try {
|
|
686
|
+
const parsed = parseAssetRef(value.trim());
|
|
687
|
+
if (parsed.type !== "memory")
|
|
688
|
+
return undefined;
|
|
689
|
+
return makeAssetRef(parsed.type, parsed.name);
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
return undefined;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
function buildFingerprint(title, description, tags, searchHints, body) {
|
|
696
|
+
return JSON.stringify({
|
|
697
|
+
title: normalizeSignal(title),
|
|
698
|
+
description: normalizeSignal(description),
|
|
699
|
+
tags: normalizeList(tags),
|
|
700
|
+
searchHints: normalizeList(searchHints),
|
|
701
|
+
body: normalizeBody(body),
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
function normalizeBody(value) {
|
|
705
|
+
return value
|
|
706
|
+
.replace(/^#+\s+/gm, "")
|
|
707
|
+
.replace(/[`*_>#-]+/g, " ")
|
|
708
|
+
.toLowerCase()
|
|
709
|
+
.replace(/\s+/g, " ")
|
|
710
|
+
.trim();
|
|
711
|
+
}
|
|
712
|
+
function normalizeSignal(value) {
|
|
713
|
+
if (!value)
|
|
714
|
+
return undefined;
|
|
715
|
+
const normalized = value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
716
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
717
|
+
}
|
|
718
|
+
function normalizeList(values) {
|
|
719
|
+
return [
|
|
720
|
+
...new Set(values.map((value) => normalizeSignal(value)).filter((value) => value !== undefined)),
|
|
721
|
+
].sort();
|
|
722
|
+
}
|
|
723
|
+
function computeSignalScore(title, description, tags, searchHints, body) {
|
|
724
|
+
return [title, description, body].join("\n").trim().length + tags.length * 25 + searchHints.length * 10;
|
|
725
|
+
}
|
|
726
|
+
function sortRecordsForSurvival(records) {
|
|
727
|
+
return [...records].sort((a, b) => {
|
|
728
|
+
if (a.canonicalName !== b.canonicalName)
|
|
729
|
+
return a.canonicalName ? -1 : 1;
|
|
730
|
+
if (a.signalScore !== b.signalScore)
|
|
731
|
+
return b.signalScore - a.signalScore;
|
|
732
|
+
return compareRecords(a, b);
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
function compareRecords(a, b) {
|
|
736
|
+
return a.ref.localeCompare(b.ref);
|
|
737
|
+
}
|
|
738
|
+
function compareCandidates(a, b) {
|
|
739
|
+
return a.ref.localeCompare(b.ref);
|
|
740
|
+
}
|
|
741
|
+
function compareContradictionCandidates(a, b) {
|
|
742
|
+
return a.ref.localeCompare(b.ref);
|
|
743
|
+
}
|
|
744
|
+
function compareBeliefTransitions(a, b) {
|
|
745
|
+
return a.ref.localeCompare(b.ref);
|
|
746
|
+
}
|
|
747
|
+
function compareConsolidationCandidates(a, b) {
|
|
748
|
+
return a.parentRef.localeCompare(b.parentRef) || a.signal.localeCompare(b.signal);
|
|
749
|
+
}
|
|
750
|
+
function firstExistingRef(refs, byRef, selfRef) {
|
|
751
|
+
for (const ref of refs) {
|
|
752
|
+
if (ref === selfRef)
|
|
753
|
+
continue;
|
|
754
|
+
if (byRef.has(ref))
|
|
755
|
+
return ref;
|
|
756
|
+
}
|
|
757
|
+
return undefined;
|
|
758
|
+
}
|
|
759
|
+
function sameStringArray(a, b) {
|
|
760
|
+
if (a.length !== b.length)
|
|
761
|
+
return false;
|
|
762
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
763
|
+
if (a[index] !== b[index])
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
function extractHeading(content) {
|
|
769
|
+
for (const line of content.split(/\r?\n/)) {
|
|
770
|
+
const match = line.match(/^#\s+(.+)$/);
|
|
771
|
+
if (match?.[1])
|
|
772
|
+
return match[1].trim();
|
|
773
|
+
}
|
|
774
|
+
return undefined;
|
|
775
|
+
}
|
|
776
|
+
function firstNonEmpty(values) {
|
|
777
|
+
for (const value of values) {
|
|
778
|
+
if (value && value.trim().length > 0)
|
|
779
|
+
return value;
|
|
780
|
+
}
|
|
781
|
+
return undefined;
|
|
782
|
+
}
|
|
783
|
+
function* walkMarkdownFiles(root) {
|
|
784
|
+
let entries;
|
|
785
|
+
try {
|
|
786
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
for (const entry of entries) {
|
|
792
|
+
const full = path.join(root, entry.name);
|
|
793
|
+
if (entry.isDirectory()) {
|
|
794
|
+
yield* walkMarkdownFiles(full);
|
|
795
|
+
}
|
|
796
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
797
|
+
yield full;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function toMemoryName(memoriesDir, filePath) {
|
|
802
|
+
const rel = path.relative(memoriesDir, filePath);
|
|
803
|
+
if (!rel || rel.startsWith(".."))
|
|
804
|
+
return undefined;
|
|
805
|
+
return rel.replace(/\\/g, "/").replace(/\.md$/i, "");
|
|
806
|
+
}
|