akm-cli 0.9.7 → 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.
Files changed (117) hide show
  1. package/CHANGELOG.md +218 -0
  2. package/dist/commands/env/env.js +2 -21
  3. package/dist/commands/health/checks.js +0 -25
  4. package/dist/commands/health/improve-metrics.js +8 -34
  5. package/dist/commands/health/windows.js +0 -4
  6. package/dist/commands/health.js +1 -35
  7. package/dist/commands/improve/consolidate/eligibility.js +11 -5
  8. package/dist/commands/improve/extract.js +36 -32
  9. package/dist/commands/improve/memory/memory-belief.js +15 -5
  10. package/dist/commands/improve/memory/memory-contradiction-detect.js +16 -25
  11. package/dist/commands/improve/memory/memory-improve.js +7 -19
  12. package/dist/commands/improve/preparation.js +52 -36
  13. package/dist/commands/improve/reflect.js +13 -17
  14. package/dist/commands/lint/base-linter.js +42 -2
  15. package/dist/commands/lint/index.js +4 -5
  16. package/dist/commands/migrate/config-extra-params.js +61 -0
  17. package/dist/commands/migrate/dead-residue.js +113 -0
  18. package/dist/commands/migrate/stale-txn.js +49 -0
  19. package/dist/commands/migrate-cli.js +42 -1
  20. package/dist/commands/proposal/proposal.js +1 -21
  21. package/dist/commands/proposal/repository.js +0 -4
  22. package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -6
  23. package/dist/commands/read/curate.js +4 -18
  24. package/dist/commands/read/show.js +2 -1
  25. package/dist/commands/sources/installed-stashes.js +5 -1
  26. package/dist/commands/sources/self-update.js +38 -1
  27. package/dist/commands/sources/sources-cli.js +17 -1
  28. package/dist/commands/tasks/tasks.js +0 -8
  29. package/dist/commands/url-checker.js +20 -28
  30. package/dist/core/adapter/adapters/agent-skills-adapter.js +2 -6
  31. package/dist/core/adapter/adapters/akm-adapter.js +2 -3
  32. package/dist/core/adapter/adapters/akm-lint.js +2 -20
  33. package/dist/core/adapter/adapters/akm-task-adapter.js +2 -6
  34. package/dist/core/adapter/adapters/akm-workflow-adapter.js +2 -6
  35. package/dist/core/adapter/adapters/dotenv-adapter.js +2 -22
  36. package/dist/core/adapter/adapters/generic-files-adapter.js +3 -9
  37. package/dist/core/adapter/adapters/llm-wiki-adapter.js +2 -6
  38. package/dist/core/adapter/adapters/okf-adapter.js +7 -18
  39. package/dist/core/adapter/adapters/shared.js +2 -0
  40. package/dist/core/adapter/adapters/tool-dir-shared.js +3 -9
  41. package/dist/core/adapter/adapters/website-snapshot-adapter.js +2 -6
  42. package/dist/core/adapter/validate-context.js +1 -3
  43. package/dist/core/asset/asset-placement.js +1 -3
  44. package/dist/core/asset/frontmatter.js +96 -0
  45. package/dist/core/common.js +66 -2
  46. package/dist/core/config/config-walker.js +6 -10
  47. package/dist/core/config/config.js +11 -8
  48. package/dist/core/extra-params.js +17 -13
  49. package/dist/core/improve-result.js +1 -3
  50. package/dist/core/json-schema.js +9 -11
  51. package/dist/core/state/migrations.js +52 -2
  52. package/dist/core/state-db.js +2 -1
  53. package/dist/execution/executable-identity.js +1 -3
  54. package/dist/execution/guarded-source.js +1 -6
  55. package/dist/indexer/bundle-identity-guard.js +6 -1
  56. package/dist/indexer/db/graph-db.js +139 -154
  57. package/dist/indexer/ensure-index.js +11 -19
  58. package/dist/indexer/graph/graph-boost.js +23 -34
  59. package/dist/indexer/graph/graph-extraction.js +12 -2
  60. package/dist/indexer/indexer.js +1 -1
  61. package/dist/indexer/lookup/adapter-concept-owner.js +12 -9
  62. package/dist/indexer/passes/memory-inference.js +7 -2
  63. package/dist/indexer/scan/drain-dir.js +2 -4
  64. package/dist/indexer/search/db-search.js +3 -3
  65. package/dist/indexer/search/fts-query.js +10 -15
  66. package/dist/indexer/search/search-source.js +0 -13
  67. package/dist/indexer/usage/usage-events.js +9 -1
  68. package/dist/indexer/walk/walker.js +11 -6
  69. package/dist/integrations/harnesses/claude/result-extractor.js +30 -15
  70. package/dist/integrations/harnesses/codex/result-extractor.js +43 -6
  71. package/dist/integrations/harnesses/copilot/result-extractor.js +39 -12
  72. package/dist/integrations/harnesses/gemini/result-extractor.js +40 -12
  73. package/dist/integrations/harnesses/index.js +0 -4
  74. package/dist/integrations/harnesses/openhands/result-extractor.js +51 -19
  75. package/dist/integrations/harnesses/pi/result-extractor.js +45 -12
  76. package/dist/integrations/lockfile.js +0 -14
  77. package/dist/integrations/session-logs/index.js +0 -81
  78. package/dist/llm/client.js +0 -12
  79. package/dist/llm/memory-infer.js +1 -3
  80. package/dist/llm/usage-telemetry.js +1 -3
  81. package/dist/registry/create-provider-registry.js +4 -0
  82. package/dist/registry/factory.js +4 -0
  83. package/dist/registry/resolve.js +23 -8
  84. package/dist/runtime.js +0 -12
  85. package/dist/scripts/akm-migrate-node.js +142 -244
  86. package/dist/scripts/akm-migrate.js +142 -244
  87. package/dist/setup/setup.js +6 -12
  88. package/dist/sources/providers/git-install.js +7 -2
  89. package/dist/sources/providers/tar-utils.js +1 -7
  90. package/dist/sources/snapshot-fetchers/content-extract.js +0 -17
  91. package/dist/storage/managed-db.js +20 -7
  92. package/dist/storage/repositories/events-repository.js +0 -81
  93. package/dist/storage/repositories/index-entries-repository.js +0 -17
  94. package/dist/storage/repositories/index-fts-repository.js +24 -30
  95. package/dist/storage/repositories/index-utility-repository.js +0 -57
  96. package/dist/storage/repositories/index-vec-repository.js +25 -27
  97. package/dist/storage/repositories/task-history-repository.js +9 -3
  98. package/dist/tasks/backends/launchd.js +8 -18
  99. package/dist/tasks/run/task-history.js +21 -31
  100. package/dist/tasks/scheduler-sync.js +1 -6
  101. package/dist/tasks/source/bounded-document.js +1 -14
  102. package/dist/workflows/exec/child-workflow.js +1 -1
  103. package/dist/workflows/exec/native-executor.js +2 -2
  104. package/dist/workflows/exec/step-work.js +5 -17
  105. package/dist/workflows/exec/worktree.js +40 -6
  106. package/dist/workflows/freeze/task-bindings.js +2 -4
  107. package/dist/workflows/ir/compile.js +3 -14
  108. package/dist/workflows/ir/schema-v4.js +4 -6
  109. package/dist/workflows/ir/schema.js +2 -5
  110. package/dist/workflows/parser.js +23 -26
  111. package/dist/workflows/source-files.js +8 -13
  112. package/dist/workflows/source-ir/schema.js +1 -19
  113. package/docs/migration/v0.9.0-troubleshooting.md +12 -2
  114. package/docs/reference/cli.md +2 -0
  115. package/docs/reference/workflow-schema.md +24 -0
  116. package/package.json +2 -5
  117. package/dist/workflows/source-ir/compare.js +0 -17
@@ -563,9 +563,6 @@ export function listProposalsReadOnly(stashDir, options = {}, ctx) {
563
563
  }
564
564
  });
565
565
  }
566
- catch {
567
- return [];
568
- }
569
566
  finally {
570
567
  db?.close();
571
568
  }
@@ -1013,7 +1010,6 @@ async function finalizeProposalTransaction(txn, target, proposal, ctx) {
1013
1010
  paths: [commitPath],
1014
1011
  snapshots: p.gitSnapshots ?? {},
1015
1012
  onCommitRecorded: (commit) => {
1016
- // biome-ignore lint/style/noNonNullAssertion: publishWriteTargetTransaction throws when absent
1017
1013
  const publication = p.gitPublication;
1018
1014
  if (publication.commit !== commit) {
1019
1015
  publication.commit = commit;
@@ -190,12 +190,6 @@ export function hasHotCaptureMode(frontmatter) {
190
190
  return frontmatter?.captureMode === "hot";
191
191
  }
192
192
  // ── Consolidate merge size gate ──────────────────────────────────────────────
193
- /**
194
- * Ratio lower-bound for merged body vs. the larger source body.
195
- * Lower than reflect (0.5) because deduplication is expected — two memories
196
- * with 80-90% overlap legitimately compress to well under 50% of the larger.
197
- */
198
- export const MERGE_SHRINK_RATIO_MIN = 0.3;
199
193
  // ── Reflect size gate ────────────────────────────────────────────────────────
200
194
  /** Ratio lower-bound: proposed body must be at least this fraction of source. */
201
195
  export const REFLECT_SHRINK_RATIO_MIN = 0.5;
@@ -52,7 +52,9 @@ export const CURATE_SEARCH_LIMIT_MULTIPLIER = 4;
52
52
  export const MIN_CURATE_SEARCH_LIMIT = 12;
53
53
  const DEFAULT_CURATE_LIMIT = 4;
54
54
  const CURATE_CLOSE_SCORE_BAND = 0.12;
55
- const CURATE_TAIL_SCORE_FLOOR = 0.35;
55
+ // Used by `shouldRunCurateFallback` to judge whether the initial search
56
+ // already returned strong results (a different purpose from ranking/keeping
57
+ // hits — see that function for its own rationale).
56
58
  const CURATE_RELATIVE_SCORE_FLOOR = 0.7;
57
59
  const CURATE_FALLBACK_TOP_SCORE_THRESHOLD = 0.8;
58
60
  const CURATE_FALLBACK_STRONG_SCORE_FLOOR = 0.35;
@@ -577,11 +579,6 @@ function compareCurateHits(a, b) {
577
579
  return rawDiff;
578
580
  return a.originalIndex - b.originalIndex;
579
581
  }
580
- function passesCurateScoreFloor(hit, leaderScore) {
581
- if (leaderScore === undefined)
582
- return true;
583
- return hit.rawScore >= Math.max(CURATE_TAIL_SCORE_FLOOR, leaderScore * CURATE_RELATIVE_SCORE_FLOOR);
584
- }
585
582
  function isNarrowReferenceFamilyQuery(query, family) {
586
583
  if (!family || family.role !== "reference")
587
584
  return false;
@@ -602,19 +599,8 @@ function selectCuratedStashHits(query, hits, limit) {
602
599
  const ranked = collapsed.hits
603
600
  .map(({ hit, originalIndex }) => annotateCurateHit(query, hit, originalIndex, intent))
604
601
  .sort(compareCurateHits);
605
- const selected = [];
606
602
  const supportRefsByRef = collapsed.supportRefsByRef;
607
- let leaderScore;
608
- for (const candidate of ranked) {
609
- if (!passesCurateScoreFloor(candidate, leaderScore))
610
- continue;
611
- selected.push(candidate);
612
- if (leaderScore === undefined)
613
- leaderScore = candidate.rawScore;
614
- if (selected.length >= limit)
615
- break;
616
- }
617
- return { selected: selected.map((entry) => entry.hit), supportRefsByRef };
603
+ return { selected: ranked.slice(0, limit).map((entry) => entry.hit), supportRefsByRef };
618
604
  }
619
605
  function collapseCurateFamilies(query, hits) {
620
606
  const passthrough = [];
@@ -27,7 +27,7 @@ import { displayRef, typeNameFromConceptId } from "../../core/asset/resolve-ref.
27
27
  import { META_DIR, parseMetaRef, readMetaFile } from "../../core/asset/stash-meta.js";
28
28
  import { asNonEmptyString, isWithin } from "../../core/common.js";
29
29
  import { getIndexPassConfig, loadConfig } from "../../core/config/config.js";
30
- import { NotFoundError, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
30
+ import { NotFoundError, rethrowIfDataDirUnreadable, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
31
31
  import { appendEvent } from "../../core/events.js";
32
32
  import { SCRIPT_EXTENSIONS } from "../../core/recognition-util.js";
33
33
  import { presentationFor } from "../../core/type-presentation.js";
@@ -292,6 +292,7 @@ export async function showLocal(input) {
292
292
  }
293
293
  catch (err) {
294
294
  rethrowIfTestIsolationError(err);
295
+ rethrowIfDataDirUnreadable(err);
295
296
  return { total: 0, hits: [] };
296
297
  }
297
298
  })(),
@@ -47,7 +47,11 @@ function canonicalJson(value) {
47
47
  if (value && typeof value === "object") {
48
48
  const entries = Object.entries(value)
49
49
  .filter(([, entry]) => entry !== undefined)
50
- .sort(([left], [right]) => left.localeCompare(right));
50
+ // Codepoint order, NOT localeCompare: this string feeds sha256Hex for the
51
+ // bundle-audit generation, and localeCompare is ICU/locale-dependent — the
52
+ // same object could hash differently on two machines. Matches the other
53
+ // canonicalJson implementations (workflows/ir/plan-hash.ts).
54
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
51
55
  return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
52
56
  }
53
57
  return JSON.stringify(value);
@@ -333,7 +333,7 @@ function runPostUpgradeTasks(akmBin, opts, upgradeState) {
333
333
  ok: false,
334
334
  skipped: opts.skip,
335
335
  message: `Upgrade completed, but the state schema was not prepared (${detail}). ` +
336
- "Preserve state.db and run `akm upgrade --force` before other AKM commands.",
336
+ "Preserve state.db and run `akm upgrade --state-only` before other AKM commands (the binary is already current).",
337
337
  };
338
338
  }
339
339
  const stateNote = stateUpgrade.safetyCopyPath ? ` Historical state safety copy: ${stateUpgrade.safetyCopyPath}.` : "";
@@ -520,3 +520,40 @@ export function getPackageManagerUpgradeCommand(installMethod, packageName = get
520
520
  }
521
521
  return undefined;
522
522
  }
523
+ /**
524
+ * Apply pending historical destructive state.db migrations WITHOUT installing a
525
+ * new akm — the body of `akm upgrade --state-only`.
526
+ *
527
+ * Migrations flagged `historical-destructive` are refused during an ordinary
528
+ * managed open: they need a verified sibling safety copy taken under the
529
+ * migration writer lock, and that is deliberate, so an unattended `akm index`
530
+ * can never quietly drop operator state.
531
+ *
532
+ * The bug this fixes is not the guard but its reachability (#895). The only
533
+ * code path that set `allowHistoricalDestructiveStateUpgrade` ran as a
534
+ * POST-INSTALL step of a real upgrade, so it sat behind an npm install. Where
535
+ * akm is installed globally by an image and the runtime user is unprivileged,
536
+ * that install fails EACCES and throws long before the migration is reached —
537
+ * leaving the documented remedy impossible to run and `akm index --full`
538
+ * permanently blocked. Nothing about the migration itself needs the network,
539
+ * root, or a new binary; it is local, offline, and already verified.
540
+ *
541
+ * The safety copy is NOT skipped here. This changes only who may ask for the
542
+ * migration, never what it does.
543
+ */
544
+ export function upgradeStateOnly(currentVersion, dependencies) {
545
+ const upgradeState = dependencies?.upgradeHistoricalStateDatabase ?? upgradeHistoricalStateDatabase;
546
+ const result = upgradeState();
547
+ return {
548
+ currentVersion,
549
+ newVersion: currentVersion,
550
+ upgraded: false,
551
+ installMethod: detectInstallMethod(),
552
+ message: result.upgraded
553
+ ? `Applied pending state.db migrations. Safety copy: ${result.safetyCopyPath}`
554
+ : "state.db is already current; no migration was needed",
555
+ stateUpgrade: result.safetyCopyPath
556
+ ? { applied: result.upgraded, safetyCopyPath: result.safetyCopyPath }
557
+ : { applied: result.upgraded },
558
+ };
559
+ }
@@ -33,7 +33,7 @@ import { UsageError } from "../../core/errors.js";
33
33
  import { appendEvent } from "../../core/events.js";
34
34
  import { resolveWritableOverride, saveGitStash } from "../../sources/providers/git.js";
35
35
  import { pkgVersion } from "../../version.js";
36
- import { checkForUpdate, performUpgrade } from "./self-update.js";
36
+ import { checkForUpdate, performUpgrade, upgradeStateOnly } from "./self-update.js";
37
37
  import { akmClone } from "./source-clone.js";
38
38
  export const upgradeCommand = defineJsonCommand({
39
39
  meta: { name: "upgrade", description: "Upgrade akm to the latest release" },
@@ -45,8 +45,24 @@ export const upgradeCommand = defineJsonCommand({
45
45
  description: "Skip the post-upgrade index rebuild",
46
46
  default: false,
47
47
  },
48
+ "state-only": {
49
+ type: "boolean",
50
+ description: "Apply pending state.db migrations without installing a new akm",
51
+ default: false,
52
+ },
48
53
  },
49
54
  async run({ args }) {
55
+ // Applying a historical destructive state migration used to be reachable
56
+ // ONLY as a post-install step of a real upgrade, so an install akm cannot
57
+ // rewrite -- a global npm install owned by root, an image that ships the
58
+ // CLI -- had no route to it at all: the npm step fails EACCES and throws
59
+ // long before the migration runs (#895). The migration is a local,
60
+ // offline, already-verified operation; it does not need the network or a
61
+ // new binary, and coupling it to one was the bug.
62
+ if (args["state-only"]) {
63
+ output("upgrade", upgradeStateOnly(pkgVersion));
64
+ return;
65
+ }
50
66
  const check = await checkForUpdate(pkgVersion);
51
67
  if (args.check) {
52
68
  output("upgrade", check);
@@ -875,14 +875,6 @@ function warnIneligibleRebind(runtime, explicitRebind, warnings) {
875
875
  return;
876
876
  warnings.push(`--rebind bound scheduled tasks to an ineligible ${runtime.kind ?? "unknown"} invocation (${runtime.binding.join(" ")}); scheduled runs will invoke a mutable, unproven binary. Install akm via \`npm install --global akm-cli\` or a standalone release, then re-run \`akm task sync --rebind\`.`);
877
877
  }
878
- export function prepareSchedulerRuntime(explicitRebind, operation, deps = {}) {
879
- const invocation = (deps.resolveInvocation ?? resolveAkmInvocation)();
880
- if (!invocation.eligible && !explicitRebind) {
881
- throw new UsageError(`Refusing to ${operation} from an ineligible ${invocation.kind ?? "unknown"} invocation (${invocation.argv.join(" ")}).`, "INVALID_FLAG_VALUE", "npm-global ownership could not be verified. Run `npm install --global akm-cli` and use that launcher, use a standalone installation, or explicitly repeat the operation with --rebind.");
882
- }
883
- const contextPath = (deps.writeDescriptor ?? writeSchedulerContextDescriptor)(schedulerContextDescriptor());
884
- return { binding: invocation.argv, contextPath, eligible: invocation.eligible, kind: invocation.kind };
885
- }
886
878
  function groupInstalledBindings(entries, invocation) {
887
879
  const groups = new Map();
888
880
  for (const entry of entries) {
@@ -2,40 +2,32 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  const URL_RE = /https?:\/\/[^\s"'<>)\]]+/g;
5
- const TIMEOUT_MS = 5000;
6
- const MAX_URLS = 20;
5
+ /**
6
+ * Check every URL in `entries` and report the ones that are dead.
7
+ *
8
+ * No cap, no per-entry slice, no ceiling option. There used to be a
9
+ * `MAX_URLS = 20` plus an undocumented `slice(0, 3)` per entry, so this
10
+ * examined at most twenty links in a bundle holding thousands and reported
11
+ * success. The first attempt at fixing that kept the cap and layered a
12
+ * coverage report, a warning, and an override on top — a constraint plus more
13
+ * code to explain the constraint. Both are gone. It checks what you asked it
14
+ * to check.
15
+ *
16
+ * A request that fails, times out, or cannot resolve surfaces as a `DeadUrl`
17
+ * rather than being swallowed, so a network problem is visible instead of
18
+ * looking like a clean bill of health.
19
+ */
7
20
  export async function checkDeadUrls(_stashDir, entries) {
8
- const urlsToCheck = [];
9
- for (const entry of entries) {
10
- if (urlsToCheck.length >= MAX_URLS)
11
- break;
12
- const matches = entry.body.match(URL_RE) ?? [];
13
- for (const url of matches.slice(0, 3)) {
14
- urlsToCheck.push({ ref: entry.ref, url });
15
- if (urlsToCheck.length >= MAX_URLS)
16
- break;
17
- }
18
- }
21
+ const urlsToCheck = entries.flatMap((entry) => (entry.body.match(URL_RE) ?? []).map((url) => ({ ref: entry.ref, url })));
19
22
  const results = [];
20
23
  await Promise.allSettled(urlsToCheck.map(async ({ ref, url }) => {
21
24
  try {
22
- const controller = new AbortController();
23
- const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
24
- const res = await fetch(url, {
25
- method: "HEAD",
26
- signal: controller.signal,
27
- redirect: "follow",
28
- });
29
- clearTimeout(timer);
30
- if (res.status >= 400) {
25
+ const res = await fetch(url, { method: "HEAD", redirect: "follow" });
26
+ if (res.status >= 400)
31
27
  results.push({ ref, url, status: res.status });
32
- }
33
28
  }
34
- catch (e) {
35
- if (e.name === "AbortError") {
36
- results.push({ ref, url, status: "timeout" });
37
- }
38
- // network errors (ENOTFOUND etc.) — skip, don't report as dead
29
+ catch {
30
+ results.push({ ref, url, status: "error" });
39
31
  }
40
32
  }));
41
33
  return results;
@@ -44,6 +44,7 @@
44
44
  import fs from "node:fs";
45
45
  import path from "node:path";
46
46
  import { parseFrontmatter } from "../../asset/frontmatter.js";
47
+ import { toPosix } from "../../common.js";
47
48
  import { hashContent, nonEmptyString, readTags } from "./shared.js";
48
49
  /** A skills collection is a single-component bundle; its one component is `main`. */
49
50
  const COMPONENT_ID = "main";
@@ -56,11 +57,6 @@ const DESCRIPTION_MAX = 1024;
56
57
  const RESERVED_NAME_WORDS = ["anthropic", "claude"];
57
58
  /** name charset/shape rule (spec §4.5). */
58
59
  const NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
59
- /** Upper bound on the bounded `content` FTS field (mirrors okf-adapter). */
60
- const MAX_CONTENT_CHARS = 100_000;
61
- function toPosix(p) {
62
- return p.replace(/\\/g, "/");
63
- }
64
60
  /** conceptId + package-dir name for a `<name>/SKILL.md` file, or null when the file is not a skill manifest. */
65
61
  function skillPackage(relPath) {
66
62
  const posix = toPosix(relPath);
@@ -95,7 +91,7 @@ function recognize(c, file) {
95
91
  adapterId: "agent-skills",
96
92
  type: "skill",
97
93
  name,
98
- content: body.length > MAX_CONTENT_CHARS ? body.slice(0, MAX_CONTENT_CHARS) : body,
94
+ content: body,
99
95
  };
100
96
  if (description !== undefined)
101
97
  doc.description = description;
@@ -88,7 +88,7 @@ import { executionDefaultsFromFrontmatter, renderMarkdownExecutionSource } from
88
88
  import { recognizeMatch } from "../recognize-match.js";
89
89
  import { perTypeValidateChecks, skillDirectoryDiagnostics, workflowYamlSourceDiagnostics } from "./akm-lint.js";
90
90
  import { applyFoldedMetadata, foldRecognizedMetadata } from "./akm-metadata.js";
91
- import { hashContent, runBaseValidateChecks } from "./shared.js";
91
+ import { hashContent, RESERVED_FILES, runBaseValidateChecks } from "./shared.js";
92
92
  // `recognizeMatch` + the builtin matcher list moved to the cycle-free leaf
93
93
  // `../recognize-match` (Chunk 5 M-b) so both this adapter AND the indexer
94
94
  // metadata pass import it without closing a metadata ↔ adapter cycle. Re-exported
@@ -99,12 +99,11 @@ export { recognizeMatch } from "../recognize-match.js";
99
99
  * OKF reserved structural files (ref-grammar decision D-R6, spec §5.1 /
100
100
  * adapter-spec §5.1/§6): `index.md` (directory listing) and `log.md` (update
101
101
  * history) are bundle structure at EVERY depth — never a concept document. The
102
- * `okf` / `llm-wiki` adapters already exclude them (`okf-adapter.ts`
102
+ * `okf` / `llm-wiki` adapters already exclude them (`shared.ts`'s
103
103
  * `RESERVED_FILES`); this brings the `akm` adapter into conformance so a
104
104
  * `knowledge/index.md` never classifies as a `knowledge` item. Case-insensitive,
105
105
  * matched on the bare filename so the exclusion holds at any depth.
106
106
  */
107
- const RESERVED_FILES = new Set(["index.md", "log.md"]);
108
107
  /** True when `name` (a bare file name) is an OKF reserved file, case-insensitively (D-R6). */
109
108
  function isReservedFileName(name) {
110
109
  return RESERVED_FILES.has(name.toLowerCase());
@@ -57,7 +57,7 @@ import { taskSourceErrorDetail } from "../../../tasks/source-v3.js";
57
57
  import { compileWorkflowPlan } from "../../../workflows/ir/compile.js";
58
58
  import { compileWorkflowSource } from "../../../workflows/source-ir/compile.js";
59
59
  import { conceptIdForStashFile } from "../../asset/resolve-ref.js";
60
- import { isAkmRegistryCachePath } from "../../common.js";
60
+ import { isAkmRegistryCachePath, scanEnvKeyNames } from "../../common.js";
61
61
  /** Recommended `category` values for facts — `commands/lint/fact-linter.ts:9`. */
62
62
  const KNOWN_CATEGORIES = new Set(["personal", "team", "project", "convention", "meta"]);
63
63
  /** Placeholder markers a workflow stub carries — `commands/lint/workflow-linter.ts:10`. */
@@ -118,24 +118,6 @@ export function nameOrTypeDiagnostics(relPath, data, frontmatter, allowedTypes)
118
118
  return [];
119
119
  }
120
120
  // ── env dangerous-key scan (content-based port of env-key-rules.ts) ──────────
121
- /** Matches a KEY=value assignment line, capturing only the key — `commands/env/env.ts:62`. */
122
- const ASSIGN_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
123
- /** Port of `commands/env/env.ts#scanKeys` (`:65-77`) — content-based (overlay `raw`, not disk). */
124
- function scanKeys(text) {
125
- const keys = [];
126
- const seen = new Set();
127
- for (const line of text.split(/\r?\n/)) {
128
- const m = line.match(ASSIGN_RE);
129
- if (!m)
130
- continue;
131
- const key = m[1];
132
- if (seen.has(key))
133
- continue;
134
- seen.add(key);
135
- keys.push(key);
136
- }
137
- return keys;
138
- }
139
121
  /** Port of `env-key-rules.ts#collectSuppressedKeys` (`:144-164`) — content-based. */
140
122
  function collectSuppressedKeys(raw) {
141
123
  const suppressed = new Set();
@@ -171,7 +153,7 @@ export function dangerousEnvKeyDiagnostics(type, relPath, raw) {
171
153
  return []; // NARROWNESS: collectEnvFiles only visits *.env
172
154
  // `relPath` is already stash-root-relative, so "." IS the stash root here.
173
155
  const ref = conceptIdForStashFile(type, ".", relPath);
174
- const keys = scanKeys(raw);
156
+ const keys = scanEnvKeyNames(raw);
175
157
  const suppressed = collectSuppressedKeys(raw);
176
158
  const diagnostics = [];
177
159
  for (const key of keys) {
@@ -35,16 +35,12 @@ import fs from "node:fs";
35
35
  import path from "node:path";
36
36
  import { parseTaskSource } from "../../../tasks/source/parse-task-source.js";
37
37
  import { TASK_EXTENSION, TASK_NEAR_MISS_EXTENSION, taskExtensionDetail, taskSourceErrorDetail, } from "../../../tasks/source-v3.js";
38
+ import { toPosix } from "../../common.js";
38
39
  import { hashContent } from "./shared.js";
39
40
  /** A native task bundle is single-component; its one component is `main`. */
40
41
  const COMPONENT_ID = "main";
41
42
  /** The task YAML extension (spec §6 task row). */
42
43
  const TASK_EXT = TASK_EXTENSION;
43
- /** Upper bound on the bounded `content` FTS field (mirrors okf-adapter). */
44
- const MAX_CONTENT_CHARS = 100_000;
45
- function toPosix(p) {
46
- return p.replace(/\\/g, "/");
47
- }
48
44
  function recognize(c, file) {
49
45
  if (file.ext !== TASK_EXT)
50
46
  return null;
@@ -61,7 +57,7 @@ function recognize(c, file) {
61
57
  adapterId: "akm-task",
62
58
  type: "task",
63
59
  name,
64
- content: raw.length > MAX_CONTENT_CHARS ? raw.slice(0, MAX_CONTENT_CHARS) : raw,
60
+ content: raw,
65
61
  };
66
62
  }
67
63
  async function validate(c, changes, ctx) {
@@ -23,17 +23,13 @@ import fs from "node:fs";
23
23
  import path from "node:path";
24
24
  import { looksLikeGithubWorkflowSource } from "../../../workflows/source-ir/compile.js";
25
25
  import { parseFrontmatter } from "../../asset/frontmatter.js";
26
+ import { toPosix } from "../../common.js";
26
27
  import { perTypeValidateChecks, workflowYamlSourceDiagnostics } from "./akm-lint.js";
27
28
  import { hashContent, nonEmptyString, readTags, runBaseValidateChecks } from "./shared.js";
28
29
  /** A native workflow bundle is single-component; its one component is `main`. */
29
30
  const COMPONENT_ID = "main";
30
31
  /** The two authoritative workflow source formats. `.yaml` is deliberately not accepted. */
31
32
  const WORKFLOW_EXTS = new Set([".md", ".yml"]);
32
- /** Upper bound on the bounded `content` FTS field (mirrors okf-adapter). */
33
- const MAX_CONTENT_CHARS = 100_000;
34
- function toPosix(p) {
35
- return p.replace(/\\/g, "/");
36
- }
37
33
  /** Strip the recognized workflow extension from a component-root-relative path → conceptId. */
38
34
  function conceptIdOf(relPath) {
39
35
  return toPosix(relPath).replace(/\.(?:md|yml)$/i, "");
@@ -78,7 +74,7 @@ function recognize(c, file) {
78
74
  adapterId: "akm-workflow",
79
75
  type: "workflow",
80
76
  name,
81
- content: body.length > MAX_CONTENT_CHARS ? body.slice(0, MAX_CONTENT_CHARS) : body,
77
+ content: body,
82
78
  };
83
79
  if (description !== undefined)
84
80
  doc.description = description;
@@ -30,6 +30,7 @@
30
30
  import fs from "node:fs";
31
31
  import path from "node:path";
32
32
  import { assetPathCandidatesForName, assetPathForName, typeForStashDir } from "../../asset/asset-placement.js";
33
+ import { scanEnvKeyNames, toPosix } from "../../common.js";
33
34
  import { dangerousEnvKeyDiagnostics } from "./akm-lint.js";
34
35
  import { hashContent } from "./shared.js";
35
36
  /** A dotenv bundle is single-component; its one component is `main`. */
@@ -40,11 +41,6 @@ const ENV_DIR = "env";
40
41
  const SECRETS_DIR = "secrets";
41
42
  /** Non-secret marker suffixes under `secrets/` (spec §6 secret row). */
42
43
  const SECRET_SKIP_SUFFIXES = [".lock", ".sensitive"];
43
- /** Matches a KEY=value assignment line, capturing only the key (mirrors akm-lint scanKeys). */
44
- const ASSIGN_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
45
- function toPosix(p) {
46
- return p.replace(/\\/g, "/");
47
- }
48
44
  /** Classify a component-root-relative file as env / secret, or null (abstain). */
49
45
  function classify(relPath) {
50
46
  const posix = toPosix(relPath);
@@ -80,22 +76,6 @@ function hasSensitiveMarker(absPath, type) {
80
76
  const marker = type === "env" ? absPath.replace(/\.env$/i, ".sensitive") : `${absPath}.sensitive`;
81
77
  return marker !== absPath && fs.existsSync(marker);
82
78
  }
83
- /** Extract KEY NAMES (never values) from an env file's raw content, first-appearance order, deduped. */
84
- function scanKeyNames(raw) {
85
- const keys = [];
86
- const seen = new Set();
87
- for (const line of raw.split(/\r?\n/)) {
88
- const m = line.match(ASSIGN_RE);
89
- if (!m)
90
- continue;
91
- const key = m[1];
92
- if (seen.has(key))
93
- continue;
94
- seen.add(key);
95
- keys.push(key);
96
- }
97
- return keys;
98
- }
99
79
  function conceptIdForPath(type, relativePath) {
100
80
  const posix = toPosix(relativePath);
101
81
  if (type === "secret")
@@ -115,7 +95,7 @@ function recognize(c, file) {
115
95
  // env: strip `.env`; surface KEY NAMES only (never values/comments/content).
116
96
  const conceptId = conceptIdForPath(type, posix);
117
97
  const name = (conceptId.split("/").pop() ?? conceptId) || "default";
118
- const keys = scanKeyNames(raw);
98
+ const keys = scanEnvKeyNames(raw);
119
99
  const doc = {
120
100
  ref: `${c.id}//${conceptId}`,
121
101
  bundle: c.id,
@@ -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.length > MAX_CONTENT_CHARS ? body.slice(0, MAX_CONTENT_CHARS) : 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.length > MAX_CONTENT_CHARS ? body.slice(0, MAX_CONTENT_CHARS) : 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 { hashContent, nonEmptyString, readTags, runBaseValidateChecks } from "./shared.js";
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 (!isPlainObject(value))
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 (!isPlainObject(item))
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
- /** Reserved OKF files (case-insensitive) recognized, never indexed as concepts (§5, OKF §1.4). */
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 = isPlainObject(data.generated) ? data.generated : undefined;
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.length > MAX_CONTENT_CHARS ? body.slice(0, MAX_CONTENT_CHARS) : 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);