akm-cli 0.9.6 → 0.9.8-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +408 -0
- package/dist/assets/hints/cli-hints-full.md +3 -3
- package/dist/assets/improve-strategies/catchup.json +40 -11
- package/dist/assets/improve-strategies/thorough.json +45 -7
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +2 -2
- package/dist/commands/agent/contribute-cli.js +11 -0
- package/dist/commands/env/env.js +2 -21
- package/dist/commands/health/checks.js +0 -25
- package/dist/commands/health/improve-metrics.js +8 -34
- package/dist/commands/health/windows.js +0 -4
- package/dist/commands/health.js +1 -35
- package/dist/commands/improve/consolidate/eligibility.js +11 -5
- package/dist/commands/improve/extract.js +36 -32
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/improve/improve-strategies.js +0 -4
- package/dist/commands/improve/memory/memory-belief.js +15 -5
- package/dist/commands/improve/memory/memory-contradiction-detect.js +16 -25
- package/dist/commands/improve/memory/memory-improve.js +9 -20
- package/dist/commands/improve/preparation.js +53 -37
- package/dist/commands/improve/reflect.js +14 -18
- package/dist/commands/lint/base-linter.js +182 -19
- package/dist/commands/lint/index.js +21 -9
- package/dist/commands/migrate/config-extra-params.js +61 -0
- package/dist/commands/migrate/dead-residue.js +113 -0
- package/dist/commands/migrate/stale-txn.js +49 -0
- package/dist/commands/migrate-cli.js +42 -1
- package/dist/commands/proposal/proposal.js +1 -21
- package/dist/commands/proposal/repository.js +0 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -6
- package/dist/commands/read/curate.js +51 -18
- package/dist/commands/read/search-cli.js +24 -1
- package/dist/commands/read/show.js +2 -1
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/self-update.js +38 -1
- package/dist/commands/sources/sources-cli.js +17 -1
- package/dist/commands/tasks/tasks.js +0 -8
- package/dist/commands/url-checker.js +20 -28
- package/dist/core/adapter/adapters/agent-skills-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-adapter.js +2 -3
- package/dist/core/adapter/adapters/akm-lint.js +2 -20
- package/dist/core/adapter/adapters/akm-task-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +2 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +2 -22
- package/dist/core/adapter/adapters/generic-files-adapter.js +3 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +2 -6
- package/dist/core/adapter/adapters/okf-adapter.js +7 -18
- package/dist/core/adapter/adapters/shared.js +2 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +3 -9
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +2 -6
- package/dist/core/adapter/validate-context.js +1 -3
- package/dist/core/asset/asset-placement.js +14 -5
- package/dist/core/asset/frontmatter.js +212 -0
- package/dist/core/asset/memory-archive.js +97 -0
- package/dist/core/common.js +66 -2
- package/dist/core/config/config-walker.js +6 -10
- package/dist/core/config/config.js +11 -8
- package/dist/core/config/engine-semantics.js +0 -2
- package/dist/core/extra-params.js +17 -13
- package/dist/core/improve-result.js +1 -3
- package/dist/core/json-schema.js +9 -11
- package/dist/core/state/migrations.js +52 -2
- package/dist/core/state-db.js +2 -1
- package/dist/execution/executable-identity.js +1 -3
- package/dist/execution/guarded-source.js +1 -6
- package/dist/indexer/bundle-identity-guard.js +6 -1
- package/dist/indexer/db/graph-db.js +139 -154
- package/dist/indexer/ensure-index.js +11 -19
- package/dist/indexer/graph/graph-boost.js +23 -34
- package/dist/indexer/graph/graph-extraction.js +12 -2
- package/dist/indexer/indexer.js +1 -1
- package/dist/indexer/lookup/adapter-concept-owner.js +12 -9
- package/dist/indexer/passes/memory-inference.js +7 -2
- package/dist/indexer/scan/drain-dir.js +2 -4
- package/dist/indexer/search/db-search.js +3 -3
- package/dist/indexer/search/fts-query.js +10 -15
- package/dist/indexer/search/search-source.js +0 -13
- package/dist/indexer/usage/usage-events.js +9 -1
- package/dist/indexer/walk/walker.js +11 -6
- package/dist/integrations/harnesses/claude/result-extractor.js +30 -15
- package/dist/integrations/harnesses/codex/result-extractor.js +43 -6
- package/dist/integrations/harnesses/copilot/result-extractor.js +39 -12
- package/dist/integrations/harnesses/gemini/result-extractor.js +40 -12
- package/dist/integrations/harnesses/index.js +0 -4
- package/dist/integrations/harnesses/openhands/result-extractor.js +51 -19
- package/dist/integrations/harnesses/pi/result-extractor.js +45 -12
- package/dist/integrations/lockfile.js +0 -14
- package/dist/integrations/session-logs/index.js +0 -81
- package/dist/llm/client.js +0 -12
- package/dist/llm/memory-infer.js +1 -3
- package/dist/llm/usage-telemetry.js +1 -3
- package/dist/registry/create-provider-registry.js +4 -0
- package/dist/registry/factory.js +4 -0
- package/dist/registry/resolve.js +23 -8
- package/dist/runtime.js +0 -12
- package/dist/scripts/akm-migrate-node.js +155 -251
- package/dist/scripts/akm-migrate.js +155 -251
- package/dist/setup/setup.js +6 -12
- package/dist/sources/providers/git-install.js +7 -2
- package/dist/sources/providers/tar-utils.js +1 -7
- package/dist/sources/snapshot-fetchers/content-extract.js +0 -17
- package/dist/sources/snapshot-fetchers/website-ingest.js +126 -0
- package/dist/storage/managed-db.js +20 -7
- package/dist/storage/repositories/events-repository.js +0 -81
- package/dist/storage/repositories/index-connection.js +45 -3
- package/dist/storage/repositories/index-entries-repository.js +0 -17
- package/dist/storage/repositories/index-fts-repository.js +24 -30
- package/dist/storage/repositories/index-utility-repository.js +0 -57
- package/dist/storage/repositories/index-vec-repository.js +25 -27
- package/dist/storage/repositories/task-history-repository.js +9 -3
- package/dist/tasks/backends/cron.js +49 -9
- package/dist/tasks/backends/launchd.js +8 -18
- package/dist/tasks/resolve-akm-bin.js +17 -2
- package/dist/tasks/run/task-history.js +21 -31
- package/dist/tasks/scheduler-invocation.js +8 -1
- package/dist/tasks/scheduler-sync.js +1 -6
- package/dist/tasks/source/bounded-document.js +1 -14
- package/dist/tasks/source/parse-task-source.js +23 -9
- package/dist/workflows/exec/child-workflow.js +1 -1
- package/dist/workflows/exec/native-executor.js +2 -2
- package/dist/workflows/exec/step-work.js +5 -17
- package/dist/workflows/exec/worktree.js +40 -6
- package/dist/workflows/freeze/task-bindings.js +2 -4
- package/dist/workflows/ir/compile.js +3 -14
- package/dist/workflows/ir/schema-v4.js +4 -6
- package/dist/workflows/ir/schema.js +2 -5
- package/dist/workflows/parser.js +23 -26
- package/dist/workflows/source-files.js +8 -13
- package/dist/workflows/source-ir/schema.js +1 -19
- package/docs/migration/v0.9.0-troubleshooting.md +12 -2
- package/docs/reference/cli.md +7 -1
- package/docs/reference/workflow-schema.md +24 -0
- package/package.json +2 -5
- package/dist/assets/improve-strategies/frequent.json +0 -15
- package/dist/assets/improve-strategies/memory-focus.json +0 -15
- package/dist/workflows/source-ir/compare.js +0 -17
|
@@ -23,19 +23,13 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import path from "node:path";
|
|
25
25
|
import { parseFrontmatter } from "../../asset/frontmatter.js";
|
|
26
|
+
import { toPosix } from "../../common.js";
|
|
26
27
|
import { SCRIPT_EXTENSIONS } from "../../recognition-util.js";
|
|
27
|
-
import { hashContent, nonEmptyString, readTags, runBaseValidateChecks } from "./shared.js";
|
|
28
|
+
import { hashContent, nonEmptyString, RESERVED_FILES, readTags, runBaseValidateChecks } from "./shared.js";
|
|
28
29
|
/** A generic-files bundle is single-component; its one component is `main`. */
|
|
29
30
|
const COMPONENT_ID = "main";
|
|
30
31
|
/** Markdown / plain-text extensions classified as `document`. */
|
|
31
32
|
const DOCUMENT_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".text"]);
|
|
32
|
-
/** OKF reserved structural files (D-R6) — excluded from the catch-all, case-insensitive. */
|
|
33
|
-
const RESERVED_FILES = new Set(["index.md", "log.md"]);
|
|
34
|
-
/** Upper bound on the bounded `content` FTS field (mirrors okf-adapter). */
|
|
35
|
-
const MAX_CONTENT_CHARS = 100_000;
|
|
36
|
-
function toPosix(p) {
|
|
37
|
-
return p.replace(/\\/g, "/");
|
|
38
|
-
}
|
|
39
33
|
function isReserved(base) {
|
|
40
34
|
return RESERVED_FILES.has(base.toLowerCase());
|
|
41
35
|
}
|
|
@@ -69,7 +63,7 @@ function recognize(c, file) {
|
|
|
69
63
|
adapterId: "generic-files",
|
|
70
64
|
type,
|
|
71
65
|
name,
|
|
72
|
-
content: body
|
|
66
|
+
content: body,
|
|
73
67
|
};
|
|
74
68
|
if (type !== "script")
|
|
75
69
|
doc.ownsPresentation = true;
|
|
@@ -73,6 +73,7 @@ import fs from "node:fs";
|
|
|
73
73
|
import path from "node:path";
|
|
74
74
|
import { parse as parseYaml } from "yaml";
|
|
75
75
|
import { parseFrontmatter, parseFrontmatterBlock } from "../../asset/frontmatter.js";
|
|
76
|
+
import { toPosix } from "../../common.js";
|
|
76
77
|
import { hashContent, nonEmptyString } from "./shared.js";
|
|
77
78
|
/** A wiki is a single-component bundle; its one component is conventionally `main` (recognition golden). */
|
|
78
79
|
const WIKI_COMPONENT_ID = "main";
|
|
@@ -86,12 +87,7 @@ const RESERVED_ROOT_FILES = new Set(["schema.md", "index.md", "log.md"]);
|
|
|
86
87
|
const RAW_SUBDIR = "raw";
|
|
87
88
|
/** Content subdirectory holding agent-authored pages. */
|
|
88
89
|
const PAGES_SUBDIR = "pages";
|
|
89
|
-
/** Upper bound on the bounded `content` FTS field (mirrors okf-adapter). */
|
|
90
|
-
const MAX_CONTENT_CHARS = 100_000;
|
|
91
90
|
/** POSIX-normalize separators. */
|
|
92
|
-
function toPosix(p) {
|
|
93
|
-
return p.replace(/\\/g, "/");
|
|
94
|
-
}
|
|
95
91
|
/** conceptId = component-root-relative path minus `.md`. */
|
|
96
92
|
function conceptIdOf(relPath) {
|
|
97
93
|
return toPosix(relPath).replace(/\.md$/i, "");
|
|
@@ -280,7 +276,7 @@ function recognize(c, file) {
|
|
|
280
276
|
ownsPresentation: true,
|
|
281
277
|
type: isRaw ? WIKI_SOURCE_TYPE : (fm.pageKind ?? DEFAULT_PAGE_KIND),
|
|
282
278
|
name: lastSegment,
|
|
283
|
-
content: body
|
|
279
|
+
content: body,
|
|
284
280
|
};
|
|
285
281
|
if (fm.description !== undefined)
|
|
286
282
|
doc.description = fm.description;
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
import fs from "node:fs";
|
|
32
32
|
import path from "node:path";
|
|
33
33
|
import { parseFrontmatter } from "../../asset/frontmatter.js";
|
|
34
|
-
import {
|
|
34
|
+
import { isRecord, toPosix } from "../../common.js";
|
|
35
|
+
import { hashContent, nonEmptyString, RESERVED_FILES, readTags, runBaseValidateChecks } from "./shared.js";
|
|
35
36
|
/** v0.2 frontmatter keys consumed into first-class fields below (§0.1) — excluded from the generic `documentJson` extras fold alongside the v0.1 five, so nothing is duplicated between a first-class field and the opaque extras bag. */
|
|
36
37
|
const CONSUMED_FRONTMATTER_KEYS = [
|
|
37
38
|
"type",
|
|
@@ -46,17 +47,13 @@ const CONSUMED_FRONTMATTER_KEYS = [
|
|
|
46
47
|
"stale_after",
|
|
47
48
|
"okf_version",
|
|
48
49
|
];
|
|
49
|
-
/** True for a plain (non-null, non-array) object — the shape every v0.2 mapping (`generated`, one `verified`/`sources` entry) must have. */
|
|
50
|
-
function isPlainObject(value) {
|
|
51
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
52
|
-
}
|
|
53
50
|
/**
|
|
54
51
|
* Parse one `verified:` actor mapping (`{by, at?}`). Tolerant: a missing/blank
|
|
55
52
|
* `by` yields `undefined` (the entry is dropped, never rejecting the document
|
|
56
53
|
* — OKF conformance leniency); `at` is independently optional.
|
|
57
54
|
*/
|
|
58
55
|
function parseActorMapping(value) {
|
|
59
|
-
if (!
|
|
56
|
+
if (!isRecord(value))
|
|
60
57
|
return undefined;
|
|
61
58
|
const by = nonEmptyString(value.by);
|
|
62
59
|
if (by === undefined)
|
|
@@ -96,7 +93,7 @@ function parseOkfSources(value) {
|
|
|
96
93
|
return undefined;
|
|
97
94
|
const out = [];
|
|
98
95
|
for (const item of value) {
|
|
99
|
-
if (!
|
|
96
|
+
if (!isRecord(item))
|
|
100
97
|
continue;
|
|
101
98
|
const resource = nonEmptyString(item.resource);
|
|
102
99
|
if (resource === undefined)
|
|
@@ -125,15 +122,7 @@ function parseOkfSources(value) {
|
|
|
125
122
|
function parseLifecycleStatus(value) {
|
|
126
123
|
return value === "draft" || value === "stable" || value === "deprecated" ? value : undefined;
|
|
127
124
|
}
|
|
128
|
-
/**
|
|
129
|
-
const RESERVED_FILES = new Set(["index.md", "log.md"]);
|
|
130
|
-
/** Upper bound on the bounded `content` FTS field (§3: "content: FTS 1 (bounded)"). Small fixtures are never truncated. */
|
|
131
|
-
const MAX_CONTENT_CHARS = 100_000;
|
|
132
|
-
/** POSIX-normalize separators without importing a cycle-participant helper. */
|
|
133
|
-
function toPosix(p) {
|
|
134
|
-
return p.replace(/\\/g, "/");
|
|
135
|
-
}
|
|
136
|
-
/** True when `name` (a bare file name) is a reserved OKF file, case-insensitively. */
|
|
125
|
+
/** True when `name` (a bare file name) is a reserved OKF file, case-insensitively (§5, OKF §1.4). */
|
|
137
126
|
function isReservedFileName(name) {
|
|
138
127
|
return RESERVED_FILES.has(name.toLowerCase());
|
|
139
128
|
}
|
|
@@ -237,7 +226,7 @@ function recognize(c, file) {
|
|
|
237
226
|
// `generated.at` — v0.2's replacement for `timestamp` — takes precedence;
|
|
238
227
|
// `timestamp` remains a fully valid fallback (the v0.2-permitted legacy
|
|
239
228
|
// reading, not merely tolerated). Both stay fully optional (never rejects).
|
|
240
|
-
const generatedMapping =
|
|
229
|
+
const generatedMapping = isRecord(data.generated) ? data.generated : undefined;
|
|
241
230
|
const generatedAt = generatedMapping ? nonEmptyString(generatedMapping.at) : undefined;
|
|
242
231
|
const generatedBy = generatedMapping ? nonEmptyString(generatedMapping.by) : undefined;
|
|
243
232
|
const legacyTimestamp = nonEmptyString(data.timestamp);
|
|
@@ -272,7 +261,7 @@ function recognize(c, file) {
|
|
|
272
261
|
ownsPresentation: true,
|
|
273
262
|
type,
|
|
274
263
|
name,
|
|
275
|
-
content: body
|
|
264
|
+
content: body,
|
|
276
265
|
};
|
|
277
266
|
if (description !== undefined)
|
|
278
267
|
doc.description = description;
|
|
@@ -69,6 +69,8 @@ export function readTags(value) {
|
|
|
69
69
|
const tags = value.filter((tag) => typeof tag === "string" && tag.trim().length > 0);
|
|
70
70
|
return tags.length > 0 ? tags : undefined;
|
|
71
71
|
}
|
|
72
|
+
/** Reserved files (case-insensitive) every concrete adapter recognizes but never indexes as a concept. */
|
|
73
|
+
export const RESERVED_FILES = new Set(["index.md", "log.md"]);
|
|
72
74
|
// ── Base validate checks (port of `BaseLinter.runBaseChecks`) ────────────────
|
|
73
75
|
function checkMissingUpdated(data, frontmatterText) {
|
|
74
76
|
return frontmatterText !== null && !("updated" in data);
|
|
@@ -49,21 +49,15 @@
|
|
|
49
49
|
import path from "node:path";
|
|
50
50
|
import { createAdapterExtensions, } from "../../../execution/source.js";
|
|
51
51
|
import { parseFrontmatter } from "../../asset/frontmatter.js";
|
|
52
|
+
import { toPosix } from "../../common.js";
|
|
52
53
|
import { executionDefaultsFromFrontmatter, renderMarkdownExecutionSource } from "../execution-source.js";
|
|
53
54
|
import { skillDirectoryDiagnostics } from "./akm-lint.js";
|
|
54
|
-
import { hashContent, nonEmptyString, readTags, runBaseValidateChecks } from "./shared.js";
|
|
55
|
-
/** OKF reserved structural files (D-R6) — excluded at every depth, case-insensitive. */
|
|
56
|
-
const RESERVED_FILES = new Set(["index.md", "log.md"]);
|
|
57
|
-
/** Upper bound on the bounded `content` FTS field (mirrors okf-adapter). */
|
|
58
|
-
const MAX_CONTENT_CHARS = 100_000;
|
|
55
|
+
import { hashContent, nonEmptyString, RESERVED_FILES, readTags, runBaseValidateChecks } from "./shared.js";
|
|
59
56
|
/** The canonical (plural) subdir spellings writes normalize to (open-question-6). */
|
|
60
57
|
const CANONICAL_COMMAND_DIR = "commands";
|
|
61
58
|
const CANONICAL_AGENT_DIR = "agents";
|
|
62
59
|
const CANONICAL_SKILL_DIR = "skills";
|
|
63
60
|
const SKILL_MANIFEST = "SKILL.md";
|
|
64
|
-
function toPosix(p) {
|
|
65
|
-
return p.replace(/\\/g, "/");
|
|
66
|
-
}
|
|
67
61
|
function isReserved(base) {
|
|
68
62
|
return RESERVED_FILES.has(base.toLowerCase());
|
|
69
63
|
}
|
|
@@ -122,7 +116,7 @@ export function recognizeToolDir(layout, c, file) {
|
|
|
122
116
|
adapterId: layout.adapterId,
|
|
123
117
|
type: cls.type,
|
|
124
118
|
name,
|
|
125
|
-
content: body
|
|
119
|
+
content: body,
|
|
126
120
|
};
|
|
127
121
|
if (cls.type === "instruction")
|
|
128
122
|
doc.ownsPresentation = true;
|
|
@@ -28,6 +28,7 @@ import fs from "node:fs";
|
|
|
28
28
|
import path from "node:path";
|
|
29
29
|
import { parse as parseYaml } from "yaml";
|
|
30
30
|
import { parseFrontmatter, parseFrontmatterBlock } from "../../asset/frontmatter.js";
|
|
31
|
+
import { toPosix } from "../../common.js";
|
|
31
32
|
import { hashContent, nonEmptyString, runBaseValidateChecks } from "./shared.js";
|
|
32
33
|
/** A snapshot bundle is single-component; its one component is `main`. */
|
|
33
34
|
const COMPONENT_ID = "main";
|
|
@@ -37,11 +38,6 @@ const PAGES_PREFIX = "stash/knowledge/";
|
|
|
37
38
|
const MANIFEST_FILE = "manifest.json";
|
|
38
39
|
/** The tag the snapshot writer stamps on every crawled page. */
|
|
39
40
|
const WEBSITE_TAG = "website";
|
|
40
|
-
/** Upper bound on the bounded `content` FTS field (mirrors okf-adapter). */
|
|
41
|
-
const MAX_CONTENT_CHARS = 100_000;
|
|
42
|
-
function toPosix(p) {
|
|
43
|
-
return p.replace(/\\/g, "/");
|
|
44
|
-
}
|
|
45
41
|
function parseSnapshotFrontmatter(raw) {
|
|
46
42
|
const out = { tags: [] };
|
|
47
43
|
const block = parseFrontmatterBlock(raw);
|
|
@@ -98,7 +94,7 @@ function recognize(c, file) {
|
|
|
98
94
|
ownsPresentation: true,
|
|
99
95
|
type: "website",
|
|
100
96
|
name,
|
|
101
|
-
content: body
|
|
97
|
+
content: body,
|
|
102
98
|
};
|
|
103
99
|
if (fm.description !== undefined)
|
|
104
100
|
doc.description = fm.description;
|
|
@@ -48,9 +48,7 @@ import fs from "node:fs";
|
|
|
48
48
|
import path from "node:path";
|
|
49
49
|
import { assetPathForName, stashDirFor } from "../asset/asset-placement.js";
|
|
50
50
|
import { typeNameFromConceptId } from "../asset/resolve-ref.js";
|
|
51
|
-
|
|
52
|
-
return p.replace(/\\/g, "/");
|
|
53
|
-
}
|
|
51
|
+
import { toPosix } from "../common.js";
|
|
54
52
|
/** Build the overlay map, keyed by POSIX path relative to `root`. */
|
|
55
53
|
function buildOverlay(root, changes) {
|
|
56
54
|
const overlay = new Map();
|
|
@@ -25,10 +25,8 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import fs from "node:fs";
|
|
27
27
|
import path from "node:path";
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
return input.replace(/\\/g, "/");
|
|
31
|
-
}
|
|
28
|
+
import { toPosix } from "../common.js";
|
|
29
|
+
import { DERIVED_SUFFIX, SCRIPT_EXTENSIONS, WORKFLOW_EXTENSIONS } from "../recognition-util.js";
|
|
32
30
|
const workflowSpec = {
|
|
33
31
|
isRelevantFile: (fileName) => WORKFLOW_EXTENSIONS.includes(path.extname(fileName).toLowerCase()),
|
|
34
32
|
toCanonicalName: (typeRoot, filePath) => {
|
|
@@ -247,10 +245,21 @@ export function assetPathForName(assetType, typeRoot, name) {
|
|
|
247
245
|
* "default" alias is genuinely dual-owned: both `<dir>/.env` and
|
|
248
246
|
* `<dir>/default.env` derive the same canonical name (`toCanonicalName`
|
|
249
247
|
* above), so a physical-owner lookup must consider both without reading
|
|
250
|
-
* either file.
|
|
248
|
+
* either file. `memory` has a second, analogous duality (#882): a ref to
|
|
249
|
+
* `<name>` may own either `<name>.md` or the LLM-inferred `<name>.derived.md`
|
|
250
|
+
* twin — `.derived` is a provenance marker on the SAME identity, not part of
|
|
251
|
+
* the name (see `resolveParentRef`/`isDerivedMemory` in
|
|
252
|
+
* `commands/improve/memory/derived-ref.ts`, and the belief-edge identity
|
|
253
|
+
* channel's own `memory:<name>.derived` refs). The plain `.md` file wins when
|
|
254
|
+
* both exist, so it stays `primary` — first in the returned list — and every
|
|
255
|
+
* caller here already prefers the first candidate that exists on disk. Every
|
|
256
|
+
* other placement type has exactly one inverse spelling.
|
|
251
257
|
*/
|
|
252
258
|
export function assetPathCandidatesForName(assetType, typeRoot, name) {
|
|
253
259
|
const primary = assetPathForName(assetType, typeRoot, name);
|
|
260
|
+
if (assetType === "memory" && !name.endsWith(DERIVED_SUFFIX)) {
|
|
261
|
+
return [primary, assetPathForName(assetType, typeRoot, `${name}${DERIVED_SUFFIX}`)];
|
|
262
|
+
}
|
|
254
263
|
if (assetType !== "env")
|
|
255
264
|
return [primary];
|
|
256
265
|
const base = name === "default" ? "" : name.endsWith("/default") ? name.slice(0, -"default".length) : undefined;
|
|
@@ -207,6 +207,218 @@ export function spliceFrontmatterLine(raw, line) {
|
|
|
207
207
|
lines.splice(closeIdx, 0, line);
|
|
208
208
|
return lines.join("\n");
|
|
209
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Strip one layer of matching quotes — frontmatter list items are often quoted
|
|
212
|
+
* refs. Written as an explicit char compare rather than a backreference regex
|
|
213
|
+
* on purpose: `scripts/lint-repository-sql.ts`'s comment/string stripper has no
|
|
214
|
+
* regex-literal awareness, so a literal holding an ODD number of quote
|
|
215
|
+
* characters desyncs its state machine and corrupts every match after it.
|
|
216
|
+
*/
|
|
217
|
+
function unquote(value) {
|
|
218
|
+
const trimmed = value.trim();
|
|
219
|
+
if (trimmed.length < 2)
|
|
220
|
+
return trimmed;
|
|
221
|
+
const first = trimmed[0];
|
|
222
|
+
if ((first === '"' || first === "'") && trimmed[trimmed.length - 1] === first)
|
|
223
|
+
return trimmed.slice(1, -1);
|
|
224
|
+
return trimmed;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Remove specific VALUES from one frontmatter list key, preserving every other
|
|
228
|
+
* byte — the counterpart to {@link spliceFrontmatterLine} for the
|
|
229
|
+
* `akm lint --prune-dangling-edges` repair (#884).
|
|
230
|
+
*
|
|
231
|
+
* Handles the three spellings a belief channel appears in: a block sequence
|
|
232
|
+
* (`contradictedBy:\n - a`), an inline flow (`contradictedBy: [a, b]`), and a
|
|
233
|
+
* bare scalar (`contradictedBy: a`). When every value under the key is removed
|
|
234
|
+
* the key itself goes too — an empty `contradictedBy: []` is not the same
|
|
235
|
+
* assertion as no edge at all.
|
|
236
|
+
*
|
|
237
|
+
* Returns the rewritten source, or `null` when `raw` has no well-formed
|
|
238
|
+
* frontmatter block or nothing matched, so the caller can leave the file
|
|
239
|
+
* untouched and report the finding unfixed. Deliberately source-preserving:
|
|
240
|
+
* these are user-authored memories, and a repair must not silently reformat
|
|
241
|
+
* the frontmatter it was not asked to touch.
|
|
242
|
+
*/
|
|
243
|
+
export function removeFrontmatterListValues(raw, key, values) {
|
|
244
|
+
const remove = new Set(values.map((v) => unquote(v)));
|
|
245
|
+
if (remove.size === 0)
|
|
246
|
+
return null;
|
|
247
|
+
const lines = raw.split(/\r?\n/);
|
|
248
|
+
if (lines[0]?.trim() !== "---")
|
|
249
|
+
return null;
|
|
250
|
+
const closeIdx = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
|
|
251
|
+
if (closeIdx === -1)
|
|
252
|
+
return null;
|
|
253
|
+
const out = [];
|
|
254
|
+
let changed = false;
|
|
255
|
+
let index = 0;
|
|
256
|
+
while (index < lines.length) {
|
|
257
|
+
const line = lines[index];
|
|
258
|
+
if (index === 0 || index >= closeIdx) {
|
|
259
|
+
out.push(line);
|
|
260
|
+
index += 1;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
264
|
+
if (kv === null || kv[1] !== key) {
|
|
265
|
+
out.push(line);
|
|
266
|
+
index += 1;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const rest = kv[2].trim();
|
|
270
|
+
// Inline flow: contradictedBy: [a, b]
|
|
271
|
+
const flow = rest.match(/^\[(.*)\]$/);
|
|
272
|
+
if (flow !== null) {
|
|
273
|
+
const kept = flow[1]
|
|
274
|
+
.split(",")
|
|
275
|
+
.map((item) => item.trim())
|
|
276
|
+
.filter(Boolean)
|
|
277
|
+
.filter((item) => !remove.has(unquote(item)));
|
|
278
|
+
const original = flow[1].split(",").filter((s) => s.trim().length > 0).length;
|
|
279
|
+
if (kept.length !== original) {
|
|
280
|
+
changed = true;
|
|
281
|
+
if (kept.length > 0)
|
|
282
|
+
out.push(`${key}: [${kept.join(", ")}]`);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
out.push(line);
|
|
286
|
+
}
|
|
287
|
+
index += 1;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
// Bare scalar: contradictedBy: a
|
|
291
|
+
if (rest !== "") {
|
|
292
|
+
if (remove.has(unquote(rest)))
|
|
293
|
+
changed = true;
|
|
294
|
+
else
|
|
295
|
+
out.push(line);
|
|
296
|
+
index += 1;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
// Block sequence: the key line, then ` - value` items.
|
|
300
|
+
const header = line;
|
|
301
|
+
const items = [];
|
|
302
|
+
let cursor = index + 1;
|
|
303
|
+
while (cursor < closeIdx) {
|
|
304
|
+
const itemMatch = lines[cursor].match(/^\s+-\s*(.*)$/);
|
|
305
|
+
if (itemMatch === null)
|
|
306
|
+
break;
|
|
307
|
+
items.push(lines[cursor]);
|
|
308
|
+
cursor += 1;
|
|
309
|
+
}
|
|
310
|
+
const kept = items.filter((item) => !remove.has(unquote(item.replace(/^\s*-\s*/, ""))));
|
|
311
|
+
if (kept.length !== items.length) {
|
|
312
|
+
changed = true;
|
|
313
|
+
if (kept.length > 0) {
|
|
314
|
+
out.push(header);
|
|
315
|
+
out.push(...kept);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
out.push(header);
|
|
320
|
+
out.push(...items);
|
|
321
|
+
}
|
|
322
|
+
index = cursor;
|
|
323
|
+
}
|
|
324
|
+
return changed ? out.join("\n") : null;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Rewrite specific VALUES in one frontmatter list key to new spellings,
|
|
328
|
+
* preserving every other byte — the rename counterpart to
|
|
329
|
+
* {@link removeFrontmatterListValues}, used by `akm lint --fix` to migrate
|
|
330
|
+
* retired `type:slug` xref values (`xrefs:`/`supersededBy:`/`contradictedBy:`)
|
|
331
|
+
* to their conceptId form once resolution confirms the rewritten spelling
|
|
332
|
+
* still points at a real asset.
|
|
333
|
+
*
|
|
334
|
+
* Handles the same three spellings a belief channel appears in: a block
|
|
335
|
+
* sequence, an inline flow, and a bare scalar. `replacements` maps an
|
|
336
|
+
* unquoted OLD value to its NEW value; a value not present in the map is left
|
|
337
|
+
* untouched byte-for-byte (including any quoting it had).
|
|
338
|
+
*
|
|
339
|
+
* Returns the rewritten source, or `null` when `raw` has no well-formed
|
|
340
|
+
* frontmatter block or nothing matched, so the caller can leave the file
|
|
341
|
+
* untouched. Deliberately source-preserving, like its sibling.
|
|
342
|
+
*/
|
|
343
|
+
export function rewriteFrontmatterListValue(raw, key, replacements) {
|
|
344
|
+
if (replacements.size === 0)
|
|
345
|
+
return null;
|
|
346
|
+
const lines = raw.split(/\r?\n/);
|
|
347
|
+
if (lines[0]?.trim() !== "---")
|
|
348
|
+
return null;
|
|
349
|
+
const closeIdx = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
|
|
350
|
+
if (closeIdx === -1)
|
|
351
|
+
return null;
|
|
352
|
+
const out = [];
|
|
353
|
+
let changed = false;
|
|
354
|
+
let index = 0;
|
|
355
|
+
while (index < lines.length) {
|
|
356
|
+
const line = lines[index];
|
|
357
|
+
if (index === 0 || index >= closeIdx) {
|
|
358
|
+
out.push(line);
|
|
359
|
+
index += 1;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
363
|
+
if (kv === null || kv[1] !== key) {
|
|
364
|
+
out.push(line);
|
|
365
|
+
index += 1;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const rest = kv[2].trim();
|
|
369
|
+
// Inline flow: xrefs: [a, b]
|
|
370
|
+
const flow = rest.match(/^\[(.*)\]$/);
|
|
371
|
+
if (flow !== null) {
|
|
372
|
+
const items = flow[1].split(",").map((item) => item.trim());
|
|
373
|
+
const nextItems = items.map((item) => {
|
|
374
|
+
if (!item)
|
|
375
|
+
return item;
|
|
376
|
+
const replacement = replacements.get(unquote(item));
|
|
377
|
+
if (replacement === undefined)
|
|
378
|
+
return item;
|
|
379
|
+
changed = true;
|
|
380
|
+
return replacement;
|
|
381
|
+
});
|
|
382
|
+
out.push(items.length > 0 && items[0] !== "" ? `${kv[1]}: [${nextItems.join(", ")}]` : line);
|
|
383
|
+
index += 1;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
// Bare scalar: xrefs: a
|
|
387
|
+
if (rest !== "") {
|
|
388
|
+
const replacement = replacements.get(unquote(rest));
|
|
389
|
+
if (replacement === undefined) {
|
|
390
|
+
out.push(line);
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
changed = true;
|
|
394
|
+
out.push(`${kv[1]}: ${replacement}`);
|
|
395
|
+
}
|
|
396
|
+
index += 1;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
// Block sequence: the key line, then ` - value` items.
|
|
400
|
+
out.push(line);
|
|
401
|
+
let cursor = index + 1;
|
|
402
|
+
while (cursor < closeIdx) {
|
|
403
|
+
const itemLine = lines[cursor];
|
|
404
|
+
const itemMatch = itemLine.match(/^(\s*-\s*)(.*)$/);
|
|
405
|
+
if (itemMatch === null)
|
|
406
|
+
break;
|
|
407
|
+
const [, prefix, value] = itemMatch;
|
|
408
|
+
const replacement = replacements.get(unquote(value));
|
|
409
|
+
if (replacement === undefined) {
|
|
410
|
+
out.push(itemLine);
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
changed = true;
|
|
414
|
+
out.push(`${prefix}${replacement}`);
|
|
415
|
+
}
|
|
416
|
+
cursor += 1;
|
|
417
|
+
}
|
|
418
|
+
index = cursor;
|
|
419
|
+
}
|
|
420
|
+
return changed ? out.join("\n") : null;
|
|
421
|
+
}
|
|
210
422
|
/**
|
|
211
423
|
* Parse a YAML scalar value (string, boolean, or number).
|
|
212
424
|
*
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
/**
|
|
5
|
+
* The memory-cleanup archive as a REF-RESOLUTION surface (#884).
|
|
6
|
+
*
|
|
7
|
+
* `analyzeMemoryCleanup`'s prune (`commands/improve/memory/memory-improve.ts`
|
|
8
|
+
* `#archiveMemory`) does not delete a memory: it `rename`s the file under
|
|
9
|
+
* `.akm/memory-cleanup/archive/<stamp>-<ref>/<originalPath>` and writes a
|
|
10
|
+
* sibling `cleanup.md` audit asset carrying `ref` / `originalPath` /
|
|
11
|
+
* `archivedPath`. The bytes and the identity both survive — but the ref stops
|
|
12
|
+
* resolving at its ORIGINAL location, so every inbound belief edge
|
|
13
|
+
* (`contradictedBy` / `supersededBy`) pointing at the pruned memory becomes a
|
|
14
|
+
* `missing-ref` the moment #882 made those channels validatable.
|
|
15
|
+
*
|
|
16
|
+
* That is the #884 defect, and the archive already holds everything needed to
|
|
17
|
+
* fix it: the audit record IS a tombstone. This module reads those tombstones
|
|
18
|
+
* so ref resolution can answer "archived" instead of "missing". Resolution
|
|
19
|
+
* stays non-destructive — pruning never rewrites an unrelated memory's
|
|
20
|
+
* frontmatter, and the contradiction an edge records is preserved rather than
|
|
21
|
+
* erased (the concern #884 raised against a bare edge-scrub).
|
|
22
|
+
*
|
|
23
|
+
* A ref whose target has NO tombstone and no file is genuinely dangling — it
|
|
24
|
+
* was removed by something other than prune (a hand `git rm`, an older
|
|
25
|
+
* release). Those stay reported; clearing them mutates user data and so is
|
|
26
|
+
* gated behind `akm lint --prune-dangling-edges`.
|
|
27
|
+
*/
|
|
28
|
+
import fs from "node:fs";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
import { parseFrontmatter } from "./frontmatter.js";
|
|
31
|
+
/** Stash-relative root the prune path archives into. Must match `memory-improve.ts#createArchiveDir`. */
|
|
32
|
+
export const MEMORY_ARCHIVE_REL = ".akm/memory-cleanup/archive";
|
|
33
|
+
/** Filename of the per-archive audit asset written alongside the archived memory. */
|
|
34
|
+
const AUDIT_FILENAME = "cleanup.md";
|
|
35
|
+
/**
|
|
36
|
+
* Cache keyed by stash root. A lint sweep resolves thousands of refs against a
|
|
37
|
+
* directory that only the prune path ever writes, so the scan runs once per
|
|
38
|
+
* root instead of once per missing ref.
|
|
39
|
+
*/
|
|
40
|
+
const cache = new Map();
|
|
41
|
+
/** @internal Drop the memoized scans — process-global state needs a reset seam for tests (#785). */
|
|
42
|
+
export function resetMemoryArchiveCache() {
|
|
43
|
+
cache.clear();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Every stash-relative `originalPath` archived under `root`, i.e. the set of
|
|
47
|
+
* paths that USED to hold a memory and now hold a tombstone instead.
|
|
48
|
+
*
|
|
49
|
+
* Unreadable or malformed audit records are skipped rather than thrown: a
|
|
50
|
+
* corrupt tombstone must degrade to "this ref is missing" (the pre-#884
|
|
51
|
+
* answer), never break the whole lint sweep.
|
|
52
|
+
*/
|
|
53
|
+
export function archivedOriginalPaths(root) {
|
|
54
|
+
const cached = cache.get(root);
|
|
55
|
+
if (cached !== undefined)
|
|
56
|
+
return cached;
|
|
57
|
+
const paths = new Set();
|
|
58
|
+
const archiveRoot = path.join(root, MEMORY_ARCHIVE_REL);
|
|
59
|
+
let entries;
|
|
60
|
+
try {
|
|
61
|
+
entries = fs.readdirSync(archiveRoot, { withFileTypes: true });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
cache.set(root, paths); // no archive dir — nothing was ever pruned here
|
|
65
|
+
return paths;
|
|
66
|
+
}
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (!entry.isDirectory())
|
|
69
|
+
continue;
|
|
70
|
+
let raw;
|
|
71
|
+
try {
|
|
72
|
+
raw = fs.readFileSync(path.join(archiveRoot, entry.name, AUDIT_FILENAME), "utf8");
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
let originalPath;
|
|
78
|
+
try {
|
|
79
|
+
originalPath = parseFrontmatter(raw).data.originalPath;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (typeof originalPath === "string" && originalPath.trim().length > 0) {
|
|
85
|
+
paths.add(originalPath.trim().replace(/\\/g, "/"));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
cache.set(root, paths);
|
|
89
|
+
return paths;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* True when `relPath` (stash-relative, POSIX) names a memory that prune
|
|
93
|
+
* archived — the ref resolves to a tombstone rather than to nothing.
|
|
94
|
+
*/
|
|
95
|
+
export function isArchivedRelPath(relPath, root) {
|
|
96
|
+
return archivedOriginalPaths(root).has(relPath.replace(/\\/g, "/"));
|
|
97
|
+
}
|
package/dist/core/common.js
CHANGED
|
@@ -277,8 +277,14 @@ function isValidDirectory(dir) {
|
|
|
277
277
|
try {
|
|
278
278
|
return fs.statSync(dir).isDirectory();
|
|
279
279
|
}
|
|
280
|
-
catch {
|
|
281
|
-
|
|
280
|
+
catch (error) {
|
|
281
|
+
// Genuinely absent — the caller's "not found" fallback applies. Any other
|
|
282
|
+
// stat failure (e.g. EACCES) is not "doesn't exist"; treating it as such
|
|
283
|
+
// produced the wrong message ("Run akm bundle create") for a directory
|
|
284
|
+
// that exists but cannot be read.
|
|
285
|
+
if (hasErrnoCode(error, "ENOENT"))
|
|
286
|
+
return false;
|
|
287
|
+
throw new ConfigError(`Unable to read bundle directory at "${dir}".`, "STASH_DIR_UNREADABLE");
|
|
282
288
|
}
|
|
283
289
|
}
|
|
284
290
|
/**
|
|
@@ -356,6 +362,10 @@ function readStashDirFromConfig() {
|
|
|
356
362
|
export function toPosix(input) {
|
|
357
363
|
return input.replace(/\\/g, "/");
|
|
358
364
|
}
|
|
365
|
+
/** Locale-independent code-point ordering — a stable `Array.prototype.sort` comparator for strings (paths, names, ids). */
|
|
366
|
+
export function compareCodePoints(left, right) {
|
|
367
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
368
|
+
}
|
|
359
369
|
export function hasErrnoCode(error, code) {
|
|
360
370
|
if (typeof error !== "object" || error === null || !("code" in error))
|
|
361
371
|
return false;
|
|
@@ -873,7 +883,61 @@ export function asNonEmptyString(value) {
|
|
|
873
883
|
const trimmed = value.trim();
|
|
874
884
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
875
885
|
}
|
|
886
|
+
// ── env-file assignment scanning ─────────────────────────────────────────────
|
|
887
|
+
/** Matches a `KEY=value` assignment line, capturing only the key. */
|
|
888
|
+
export const ENV_ASSIGN_LINE_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
|
|
889
|
+
/** Scan lines and return KEY names in file order, without duplicates. */
|
|
890
|
+
export function scanEnvKeyNames(text) {
|
|
891
|
+
const keys = [];
|
|
892
|
+
const seen = new Set();
|
|
893
|
+
for (const line of text.split(/\r?\n/)) {
|
|
894
|
+
const m = line.match(ENV_ASSIGN_LINE_RE);
|
|
895
|
+
if (!m)
|
|
896
|
+
continue;
|
|
897
|
+
const key = m[1];
|
|
898
|
+
if (!key)
|
|
899
|
+
continue;
|
|
900
|
+
if (seen.has(key))
|
|
901
|
+
continue;
|
|
902
|
+
seen.add(key);
|
|
903
|
+
keys.push(key);
|
|
904
|
+
}
|
|
905
|
+
return keys;
|
|
906
|
+
}
|
|
907
|
+
/** True when `value` contains no lone/unpaired UTF-16 surrogate (every surrogate is part of a valid pair). */
|
|
908
|
+
export function wellFormedUnicode(value) {
|
|
909
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
910
|
+
const code = value.charCodeAt(index);
|
|
911
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
912
|
+
const next = value.charCodeAt(index + 1);
|
|
913
|
+
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
914
|
+
return false;
|
|
915
|
+
index += 1;
|
|
916
|
+
}
|
|
917
|
+
else if (code >= 0xdc00 && code <= 0xdfff)
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
876
922
|
// ── Generic data utilities ───────────────────────────────────────────────────
|
|
923
|
+
/**
|
|
924
|
+
* Narrow an unknown value to a plain-ish record: an `object` that is neither
|
|
925
|
+
* `null` nor an `Array`. Does not distinguish a literal `{}` from a `Date`,
|
|
926
|
+
* `Map`, or class instance — callers that need that distinction use a
|
|
927
|
+
* stricter predicate instead.
|
|
928
|
+
*/
|
|
929
|
+
export function isRecord(value) {
|
|
930
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
931
|
+
}
|
|
932
|
+
/** `JSON.parse` that returns `undefined` instead of throwing. */
|
|
933
|
+
export function tryParseJson(raw) {
|
|
934
|
+
try {
|
|
935
|
+
return JSON.parse(raw);
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
return undefined;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
877
941
|
/**
|
|
878
942
|
* Coerce an unknown value to a filtered, trimmed string array.
|
|
879
943
|
* Non-strings and empty/whitespace-only entries are dropped.
|