akm-cli 0.10.261016-beta → 0.10.261017-beta

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 (104) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +8 -10
  3. package/SECURITY.md +7 -99
  4. package/STABILITY.md +57 -98
  5. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -2
  6. package/dist/assets/stash-skeleton/facts/conventions/organization.md +11 -17
  7. package/dist/cli.js +32 -41
  8. package/dist/commands/feedback-cli.js +5 -4
  9. package/dist/commands/health.js +15 -16
  10. package/dist/commands/proposal/proposal-cli.js +1 -1
  11. package/dist/commands/read/registry-search.js +4 -1
  12. package/dist/commands/read/search.js +6 -6
  13. package/dist/commands/sources/self-update.js +9 -5
  14. package/dist/commands/tasks/tasks.js +12 -13
  15. package/dist/core/adapter/adapters/agent-skills-adapter.js +7 -7
  16. package/dist/core/adapter/adapters/akm-task-adapter.js +1 -1
  17. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  18. package/dist/core/adapter/adapters/claude-adapter.js +1 -1
  19. package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
  20. package/dist/core/adapter/adapters/generic-files-adapter.js +1 -1
  21. package/dist/core/adapter/adapters/llm-wiki-adapter.js +1 -1
  22. package/dist/core/adapter/adapters/opencode-adapter.js +1 -1
  23. package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -1
  24. package/dist/core/bundle-rename.js +17 -7
  25. package/dist/indexer/bundle-identity-guard.js +6 -6
  26. package/dist/indexer/index-written-assets.js +3 -3
  27. package/dist/indexer/indexer.js +12 -8
  28. package/dist/indexer/materialize-embeddings.js +12 -2
  29. package/dist/indexer/passes/metadata.js +103 -46
  30. package/dist/indexer/search/ranking.js +7 -3
  31. package/dist/indexer/search/search-fields.js +1 -1
  32. package/dist/indexer/usage/usage-events.js +30 -8
  33. package/dist/indexer/walk/walker.js +1 -1
  34. package/dist/output/shapes/helpers.js +3 -3
  35. package/dist/output/text/command-format.js +5 -6
  36. package/dist/scripts/akm-migrate-node.js +31 -3
  37. package/dist/scripts/akm-migrate.js +31 -3
  38. package/dist/sources/providers/git-provider.js +2 -4
  39. package/dist/sources/providers/provider-utils.js +8 -13
  40. package/dist/sources/snapshot-fetchers/website-ingest.js +6 -7
  41. package/dist/storage/repositories/events-repository.js +15 -0
  42. package/dist/storage/repositories/improve-ledger-repository.js +13 -0
  43. package/dist/storage/repositories/index-entries-repository.js +43 -5
  44. package/dist/storage/repositories/outcome-repository.js +12 -0
  45. package/dist/storage/repositories/proposals-repository.js +5 -5
  46. package/dist/storage/repositories/salience-repository.js +13 -0
  47. package/dist/storage/repositories/task-history-repository.js +8 -7
  48. package/dist/storage/repositories/workflow-runs-repository.js +10 -8
  49. package/docs/README.md +24 -60
  50. package/docs/agents/README.md +3 -8
  51. package/docs/architecture/README.md +18 -20
  52. package/docs/architecture/adapters.md +40 -43
  53. package/docs/architecture/architecture.md +171 -151
  54. package/docs/architecture/improvement.md +95 -91
  55. package/docs/architecture/internals/classification.md +101 -119
  56. package/docs/architecture/internals/fresh-host-rebuild-runbook.md +33 -35
  57. package/docs/architecture/internals/health-advisories.md +59 -46
  58. package/docs/architecture/internals/improve-workflow.md +166 -693
  59. package/docs/architecture/internals/indexing.md +65 -465
  60. package/docs/architecture/internals/storage-locations.md +301 -848
  61. package/docs/architecture/persisted-data-compat.md +6 -6
  62. package/docs/architecture/specs/okf-support.md +67 -95
  63. package/docs/architecture/specs/ref.md +67 -125
  64. package/docs/architecture/testing/testing-workflow.md +107 -167
  65. package/docs/architecture/workflow-engine.md +34 -33
  66. package/docs/guides/README.md +5 -5
  67. package/docs/guides/author-bundles.md +72 -98
  68. package/docs/guides/author-workflows.md +11 -18
  69. package/docs/guides/bundles.md +91 -123
  70. package/docs/guides/capture-knowledge.md +10 -9
  71. package/docs/guides/concepts.md +45 -30
  72. package/docs/guides/discover-and-load.md +39 -32
  73. package/docs/guides/getting-started.md +24 -24
  74. package/docs/guides/improve-the-library.md +98 -80
  75. package/docs/guides/recipes/headless-install.md +4 -4
  76. package/docs/guides/recipes/website-source.md +9 -5
  77. package/docs/guides/run-workflows.md +10 -11
  78. package/docs/guides/scheduling.md +11 -17
  79. package/docs/guides/use-with-any-agent.md +15 -14
  80. package/docs/guides/wikis.md +6 -9
  81. package/docs/integration/bundling-akm.md +21 -26
  82. package/docs/maintainers/README.md +2 -2
  83. package/docs/maintainers/local-development.md +3 -16
  84. package/docs/migration/README.md +2 -1
  85. package/docs/migration/release-notes/0.10.0.md +167 -22
  86. package/docs/migration/release-notes/README.md +2 -2
  87. package/docs/migration/v0.9-to-v0.10.md +47 -7
  88. package/docs/product-surface.md +10 -8
  89. package/docs/reference/README.md +26 -15
  90. package/docs/reference/asset-types.md +11 -12
  91. package/docs/reference/bundle-types.md +6 -5
  92. package/docs/reference/cli.md +52 -28
  93. package/docs/reference/configuration.md +46 -60
  94. package/docs/reference/data-and-telemetry.md +204 -421
  95. package/docs/reference/env-and-secrets.md +80 -83
  96. package/docs/reference/memory.md +1 -1
  97. package/docs/reference/refs.md +23 -36
  98. package/docs/reference/registry.md +138 -348
  99. package/docs/reference/supported-formats.md +8 -8
  100. package/docs/reference/tasks.md +13 -19
  101. package/docs/reference/website-sources.md +23 -15
  102. package/docs/reference/workflow-schema.md +28 -47
  103. package/docs/reference/workflows.md +33 -37
  104. package/package.json +1 -1
@@ -69,20 +69,28 @@ export function insertUsageEventStrict(db, event) {
69
69
  // ── Query ────────────────────────────────────────────────────────────────────
70
70
  /**
71
71
  * User feedback counts for many assets at once, keyed by durable `entry_ref`.
72
- * Refs with no user feedback are absent from the map. Read in one query per
73
- * chunk of refs.
72
+ * A negative that carried an exact fix does not count once an accepted
73
+ * `feedback` proposal for the asset exists at or after it (the rule `akm
74
+ * improve` uses for a fix already acted on); reverting that proposal makes it
75
+ * count again. Refs with no user feedback are absent from the map. Read in one
76
+ * query per chunk of refs.
74
77
  */
75
78
  export function countFeedbackSignalsByRef(db, refs) {
76
79
  const counts = new Map();
77
80
  for (let i = 0; i < refs.length; i += SQLITE_CHUNK_SIZE) {
78
81
  const chunk = refs.slice(i, i + SQLITE_CHUNK_SIZE);
79
82
  const rows = db
80
- .prepare(`SELECT entry_ref,
81
- SUM(CASE WHEN signal = 'positive' THEN 1 ELSE 0 END) AS pos,
82
- SUM(CASE WHEN signal = 'negative' THEN 1 ELSE 0 END) AS neg
83
- FROM usage_events
84
- WHERE event_type = 'feedback' AND source = 'user' AND entry_ref IN (${chunk.map(() => "?").join(",")})
85
- GROUP BY entry_ref`)
83
+ .prepare(`SELECT u.entry_ref,
84
+ SUM(CASE WHEN u.signal = 'positive' THEN 1 ELSE 0 END) AS pos,
85
+ SUM(CASE WHEN u.signal = 'negative' AND NOT (
86
+ json_extract(u.metadata, '$.fix') IS NOT NULL AND EXISTS (
87
+ SELECT 1 FROM proposals p
88
+ WHERE p.ref = u.entry_ref AND p.source = 'feedback' AND p.status = 'accepted'
89
+ AND julianday(p.created_at) >= julianday(u.created_at))
90
+ ) THEN 1 ELSE 0 END) AS neg
91
+ FROM usage_events u
92
+ WHERE u.event_type = 'feedback' AND u.source = 'user' AND u.entry_ref IN (${chunk.map(() => "?").join(",")})
93
+ GROUP BY u.entry_ref`)
86
94
  .all(...chunk);
87
95
  for (const row of rows)
88
96
  counts.set(row.entry_ref, { pos: row.pos, neg: row.neg });
@@ -150,3 +158,17 @@ export function purgeOldUsageEvents(db, retentionDays) {
150
158
  /* Table may not exist yet */
151
159
  }
152
160
  }
161
+ /**
162
+ * Rewrite every `usage_events.entry_ref` naming `oldBundleId` to `newBundleId`
163
+ * (`akm bundle rename`). Search ranking counts feedback by `entry_ref`, so an
164
+ * event left under the old prefix stops counting for the renamed bundle's
165
+ * assets. `entry_id` is untouched: the rename keeps every entry's row id. A
166
+ * legacy unqualified ref is not matched by the `<old>//` prefix and is left alone.
167
+ * Returns the number of rows rewritten.
168
+ */
169
+ export function renameUsageEventRefs(db, oldBundleId, newBundleId) {
170
+ const prefix = `${oldBundleId}//`;
171
+ return Number(db
172
+ .prepare(`UPDATE usage_events SET entry_ref = ? || substr(entry_ref, length(?) + 1) WHERE substr(entry_ref, 1, length(?)) = ?`)
173
+ .run(newBundleId, oldBundleId, prefix, prefix).changes);
174
+ }
@@ -112,7 +112,7 @@ function walkStashGit(stashRoot, options) {
112
112
  ? ignoredResult.stdout.toString("utf8")
113
113
  : String(ignoredResult.stdout ?? "");
114
114
  // Data-hygiene filename skips: the legacy metadata sidecar (never indexed as
115
- // content — Chunk-8 folds it into the bundle format) plus git dot-files.
115
+ // content) plus git dot-files.
116
116
  const SKIP_FILES = new Set([".stash.json", ".gitignore", ".gitattributes"]);
117
117
  const stdout = Buffer.isBuffer(result.stdout) ? result.stdout.toString("utf8") : String(result.stdout ?? "");
118
118
  const files = [...stdout.split("\0"), ...ignoredStdout.split("\0")]
@@ -71,7 +71,7 @@ export function shapeProposalEntry(entry, detail) {
71
71
  if (detail === "normal") {
72
72
  // `confidence` and `gateDecision` (#577) explain why a proposal is pending,
73
73
  // so they are projected at `normal` for `akm proposal list/show` when present.
74
- // `retirement` (alpha.9) is the same kind of field for a consolidate
74
+ // `retirement` is the same kind of field for a consolidate
75
75
  // `retire` proposal: without it, "list/show" carries no reason a reviewer
76
76
  // could act on (its `payload.content` is empty by design — a retire
77
77
  // proposal deletes, it does not write).
@@ -278,7 +278,7 @@ export function shapeSearchHit(hit, detail) {
278
278
  }
279
279
  // Stash hit (local or remote)
280
280
  // `ref` is included at `brief` so agents can run `akm show <ref>` without
281
- // needing --detail full or --detail agent (REC-03).
281
+ // needing --detail full or --detail agent.
282
282
  if (detail === "brief") {
283
283
  return pickFields(hit, ["type", "name", "ref", "action", "estimatedTokens", "keys"]);
284
284
  }
@@ -376,7 +376,7 @@ export function shapeShowOutput(result, detail, shape = "human") {
376
376
  const base = pickFields(result, [
377
377
  "type",
378
378
  "name",
379
- // ref is always projected, same as path/editable below (R-020).
379
+ // ref is always projected, same as path/editable below.
380
380
  "ref",
381
381
  "origin",
382
382
  "action",
@@ -7,8 +7,7 @@
7
7
  * events/history, search/curate, and the stash-lifecycle verbs (init,
8
8
  * index, list, add, remove, update, upgrade, clone).
9
9
  *
10
- * Split out of `helpers.ts` (formerly 1418 lines / 59 fns) as its own
11
- * sibling module: unlike `show`/`workflow`/`proposal` (each a stateful,
10
+ * Unlike `show`/`workflow`/`proposal` (each a stateful,
12
11
  * multi-branch renderer with its own module), these are flat
13
12
  * field-extraction formatters with no shared state.
14
13
  */
@@ -308,7 +307,7 @@ export function formatSearchPlain(r, detail) {
308
307
  if (parts.length > 0)
309
308
  lines.push(`timing: ${parts.join(", ")}`);
310
309
  }
311
- // REC-02: When stash hits exist, tell the agent the next required step so it
310
+ // When stash hits exist, tell the agent the next required step so it
312
311
  // doesn't skip `akm show` and write from training memory instead.
313
312
  if (hits.length >= 1) {
314
313
  // Prefer skill/command/agent type hits for the "Next:" ref — knowledge docs are
@@ -517,7 +516,7 @@ export function formatAddPlain(r) {
517
516
  // writes a desired locator to config — nothing is synced until a later
518
517
  // `akm bundle update`. Force-fitting the declarative shape's `added`/`entry`/`message`
519
518
  // fields into the "Installed <ref> (N scanned...)" wording produced
520
- // "Installed undefined (0 directories scanned...)" (R-014) because it
519
+ // "Installed undefined (0 directories scanned...)" because it
521
520
  // implied a sync that never happened. `AddResponse` always carries an
522
521
  // `index` block (even when zero entries were scanned); `SourceAddResult`
523
522
  // never does — that is the reliable shape discriminator used below to
@@ -630,11 +629,11 @@ export function formatBundleRenamePlain(r) {
630
629
  return lines.join("\n");
631
630
  }
632
631
  export function formatUpdatePlain(r) {
633
- // R-015: `processed` alone conflated three cases into one empty array —
632
+ // `processed` alone would conflate three cases into one empty array —
634
633
  // a true no-op (--all with nothing configured), a source this call flatly
635
634
  // never looked at (a plain source under --all), and a plain git/website
636
635
  // source that WAS just synced successfully but has no `UpdateResultItem`
637
- // shape to report (no version/lock to diff). All three rendered as the
636
+ // shape to report (no version/lock to diff). All three would render as the
638
637
  // same misleading "nothing to update". `plainSynced` and `skipped` (see
639
638
  // sources/types.ts) cover the other two so this can report accurately.
640
639
  const processed = r.processed;
@@ -34568,6 +34568,27 @@ function isMarkdownFenceClosing(line, fence) {
34568
34568
  return false;
34569
34569
  return /^[\t ]*$/.test(line.slice(cursor));
34570
34570
  }
34571
+ function fencedCodeIdentifiers(line) {
34572
+ const cleaned = line.replace(/(["'`])((?:\\.|(?!\1).)*)\1(\s*:)?/g, (match, _quote, body, colon, offset) => {
34573
+ if (colon)
34574
+ return /^\s*["'`]/.test(line.slice(offset + match.length)) ? ` ${body} ` : ` ${body}: `;
34575
+ return /^\/[\w./-]+$/.test(body) ? ` ${body} ` : " ";
34576
+ }).replace(/([A-Za-z_][A-Za-z0-9_.-]*\s*[:=]\s*)((?:[Bb]earer|[Bb]asic|[Tt]oken|[Dd]igest)\s+\S+|\S+)/g, (_match, name, value) => /^[!(]*[A-Za-z_][\w$]*(?:\.[A-Za-z_][\w$]*)+[()\]}]*[,;]?$/.test(value) ? `${name} ${value}` : name).replace(/(?<![\w.-])[A-Za-z0-9+/=]{20,}(?![\w.-])/g, (run) => /[a-z]/.test(run) && /[A-Z]/.test(run) && /\d/.test(run) ? " " : run);
34577
+ const words = [];
34578
+ for (let token of cleaned.match(/--?[A-Za-z][A-Za-z0-9-]*|[A-Za-z_][A-Za-z0-9_.-]*/g) ?? []) {
34579
+ token = token.replace(/[.-]+$/, "");
34580
+ const bare = token.replace(/^-+/, "");
34581
+ if (bare.length < 2)
34582
+ continue;
34583
+ if (bare.length >= TOKEN_LIKE_MIN_LENGTH && /\d/.test(bare))
34584
+ continue;
34585
+ words.push(token);
34586
+ const parts = bare.split(/[_.-]|(?<=[a-z0-9])(?=[A-Z])/).filter((part) => part.length > 1);
34587
+ if (parts.length > 1)
34588
+ words.push(...parts);
34589
+ }
34590
+ return words.join(" ");
34591
+ }
34571
34592
  function stripMarkdownHtmlComments(line, state) {
34572
34593
  let cursor = 0;
34573
34594
  let visible = "";
@@ -34733,6 +34754,7 @@ function projectMarkdownContent(body, truncationInfo) {
34733
34754
  const innerBlock = findInnerFrontmatterBlock(lines);
34734
34755
  const start = innerBlock && isFrontmatterShaped(lines, innerBlock) ? innerBlock.close + 1 : 0;
34735
34756
  const projected = [];
34757
+ const fenced = [];
34736
34758
  forEachVisibleMarkdownLine(lines, start, (_index, visible) => {
34737
34759
  let trimmed = visible.trim();
34738
34760
  if (!trimmed || /^(-{3,}|\*{3,}|_{3,}|=+)$/.test(trimmed))
@@ -34750,15 +34772,19 @@ function projectMarkdownContent(body, truncationInfo) {
34750
34772
  trimmed = trimmed.replace(/\s+/g, " ").trim();
34751
34773
  if (trimmed)
34752
34774
  projected.push(trimmed);
34775
+ }, (_index, fencedLine) => {
34776
+ const identifiers = fencedCodeIdentifiers(fencedLine);
34777
+ if (identifiers)
34778
+ fenced.push(identifiers);
34753
34779
  });
34754
- const text2 = projected.join(" ").replace(/\s+/g, " ").trim();
34780
+ const text2 = [...projected, ...fenced].join(" ").replace(/\s+/g, " ").trim();
34755
34781
  if (!text2)
34756
34782
  return;
34757
34783
  if (truncationInfo)
34758
34784
  truncationInfo.truncated = text2.length > MARKDOWN_CONTENT_MAX_CHARS;
34759
34785
  return truncateUnicodeSafe(text2, MARKDOWN_CONTENT_MAX_CHARS);
34760
34786
  }
34761
- function forEachVisibleMarkdownLine(lines, start, visit3) {
34787
+ function forEachVisibleMarkdownLine(lines, start, visit3, visitFenced) {
34762
34788
  let fence;
34763
34789
  const htmlComment = { inComment: false };
34764
34790
  for (let index = start;index < lines.length; index += 1) {
@@ -34766,6 +34792,8 @@ function forEachVisibleMarkdownLine(lines, start, visit3) {
34766
34792
  if (fence) {
34767
34793
  if (isMarkdownFenceClosing(rawLine, fence))
34768
34794
  fence = undefined;
34795
+ else
34796
+ visitFenced?.(index, rawLine);
34769
34797
  continue;
34770
34798
  }
34771
34799
  const leadingFence = htmlComment.inComment ? undefined : parseMarkdownFenceOpening(rawLine);
@@ -34936,7 +34964,7 @@ function extractDirTagsFromName(name) {
34936
34964
  }
34937
34965
  return Array.from(tags);
34938
34966
  }
34939
- var SCOPE_KEYS, KNOWN_QUALITY_VALUES, warnedUnknownQualityValues, COMMENT_TAGS, MARKDOWN_CONTENT_MAX_CHARS = 1e6, MAX_MARKDOWN_LINK_NESTING = 32, markdownFragmentContentByEntry, markdownFragmentProjectionEntries;
34967
+ var SCOPE_KEYS, KNOWN_QUALITY_VALUES, warnedUnknownQualityValues, COMMENT_TAGS, MARKDOWN_CONTENT_MAX_CHARS = 1e6, TOKEN_LIKE_MIN_LENGTH = 20, MAX_MARKDOWN_LINK_NESTING = 32, markdownFragmentContentByEntry, markdownFragmentProjectionEntries;
34940
34968
  var init_metadata = __esm(() => {
34941
34969
  init_asset_ref();
34942
34970
  init_frontmatter();
@@ -34567,6 +34567,27 @@ function isMarkdownFenceClosing(line, fence) {
34567
34567
  return false;
34568
34568
  return /^[\t ]*$/.test(line.slice(cursor));
34569
34569
  }
34570
+ function fencedCodeIdentifiers(line) {
34571
+ const cleaned = line.replace(/(["'`])((?:\\.|(?!\1).)*)\1(\s*:)?/g, (match, _quote, body, colon, offset) => {
34572
+ if (colon)
34573
+ return /^\s*["'`]/.test(line.slice(offset + match.length)) ? ` ${body} ` : ` ${body}: `;
34574
+ return /^\/[\w./-]+$/.test(body) ? ` ${body} ` : " ";
34575
+ }).replace(/([A-Za-z_][A-Za-z0-9_.-]*\s*[:=]\s*)((?:[Bb]earer|[Bb]asic|[Tt]oken|[Dd]igest)\s+\S+|\S+)/g, (_match, name, value) => /^[!(]*[A-Za-z_][\w$]*(?:\.[A-Za-z_][\w$]*)+[()\]}]*[,;]?$/.test(value) ? `${name} ${value}` : name).replace(/(?<![\w.-])[A-Za-z0-9+/=]{20,}(?![\w.-])/g, (run) => /[a-z]/.test(run) && /[A-Z]/.test(run) && /\d/.test(run) ? " " : run);
34576
+ const words = [];
34577
+ for (let token of cleaned.match(/--?[A-Za-z][A-Za-z0-9-]*|[A-Za-z_][A-Za-z0-9_.-]*/g) ?? []) {
34578
+ token = token.replace(/[.-]+$/, "");
34579
+ const bare = token.replace(/^-+/, "");
34580
+ if (bare.length < 2)
34581
+ continue;
34582
+ if (bare.length >= TOKEN_LIKE_MIN_LENGTH && /\d/.test(bare))
34583
+ continue;
34584
+ words.push(token);
34585
+ const parts = bare.split(/[_.-]|(?<=[a-z0-9])(?=[A-Z])/).filter((part) => part.length > 1);
34586
+ if (parts.length > 1)
34587
+ words.push(...parts);
34588
+ }
34589
+ return words.join(" ");
34590
+ }
34570
34591
  function stripMarkdownHtmlComments(line, state) {
34571
34592
  let cursor = 0;
34572
34593
  let visible = "";
@@ -34732,6 +34753,7 @@ function projectMarkdownContent(body, truncationInfo) {
34732
34753
  const innerBlock = findInnerFrontmatterBlock(lines);
34733
34754
  const start = innerBlock && isFrontmatterShaped(lines, innerBlock) ? innerBlock.close + 1 : 0;
34734
34755
  const projected = [];
34756
+ const fenced = [];
34735
34757
  forEachVisibleMarkdownLine(lines, start, (_index, visible) => {
34736
34758
  let trimmed = visible.trim();
34737
34759
  if (!trimmed || /^(-{3,}|\*{3,}|_{3,}|=+)$/.test(trimmed))
@@ -34749,15 +34771,19 @@ function projectMarkdownContent(body, truncationInfo) {
34749
34771
  trimmed = trimmed.replace(/\s+/g, " ").trim();
34750
34772
  if (trimmed)
34751
34773
  projected.push(trimmed);
34774
+ }, (_index, fencedLine) => {
34775
+ const identifiers = fencedCodeIdentifiers(fencedLine);
34776
+ if (identifiers)
34777
+ fenced.push(identifiers);
34752
34778
  });
34753
- const text2 = projected.join(" ").replace(/\s+/g, " ").trim();
34779
+ const text2 = [...projected, ...fenced].join(" ").replace(/\s+/g, " ").trim();
34754
34780
  if (!text2)
34755
34781
  return;
34756
34782
  if (truncationInfo)
34757
34783
  truncationInfo.truncated = text2.length > MARKDOWN_CONTENT_MAX_CHARS;
34758
34784
  return truncateUnicodeSafe(text2, MARKDOWN_CONTENT_MAX_CHARS);
34759
34785
  }
34760
- function forEachVisibleMarkdownLine(lines, start, visit3) {
34786
+ function forEachVisibleMarkdownLine(lines, start, visit3, visitFenced) {
34761
34787
  let fence;
34762
34788
  const htmlComment = { inComment: false };
34763
34789
  for (let index = start;index < lines.length; index += 1) {
@@ -34765,6 +34791,8 @@ function forEachVisibleMarkdownLine(lines, start, visit3) {
34765
34791
  if (fence) {
34766
34792
  if (isMarkdownFenceClosing(rawLine, fence))
34767
34793
  fence = undefined;
34794
+ else
34795
+ visitFenced?.(index, rawLine);
34768
34796
  continue;
34769
34797
  }
34770
34798
  const leadingFence = htmlComment.inComment ? undefined : parseMarkdownFenceOpening(rawLine);
@@ -34935,7 +34963,7 @@ function extractDirTagsFromName(name) {
34935
34963
  }
34936
34964
  return Array.from(tags);
34937
34965
  }
34938
- var SCOPE_KEYS, KNOWN_QUALITY_VALUES, warnedUnknownQualityValues, COMMENT_TAGS, MARKDOWN_CONTENT_MAX_CHARS = 1e6, MAX_MARKDOWN_LINK_NESTING = 32, markdownFragmentContentByEntry, markdownFragmentProjectionEntries;
34966
+ var SCOPE_KEYS, KNOWN_QUALITY_VALUES, warnedUnknownQualityValues, COMMENT_TAGS, MARKDOWN_CONTENT_MAX_CHARS = 1e6, TOKEN_LIKE_MIN_LENGTH = 20, MAX_MARKDOWN_LINK_NESTING = 32, markdownFragmentContentByEntry, markdownFragmentProjectionEntries;
34939
34967
  var init_metadata = __esm(() => {
34940
34968
  init_asset_ref();
34941
34969
  init_frontmatter();
@@ -154,10 +154,8 @@ function hasExtractedRepo(repoDir) {
154
154
  try {
155
155
  if (!fs.statSync(repoDir).isDirectory())
156
156
  return false;
157
- // WI-3.1: the "any type dir present" test is now sourced from the `akm`
158
- // adapter's directoryList() — behavior-identical to the placement
159
- // stash-subdir names, with `stashDirNames()` kept live as the fallback. The
160
- // content/ subdir check above is preserved verbatim.
157
+ // The "any type dir present" test is sourced from the `akm`
158
+ // adapter's directoryList(), with `stashDirNames()` as the fallback.
161
159
  const ownedDirs = akmAdapter.directoryList?.({ id: "akm", adapter: "akm", root: repoDir, writable: false }) ?? stashDirNames();
162
160
  return ownedDirs.some((dirName) => fs.existsSync(path.join(repoDir, dirName)));
163
161
  }
@@ -37,22 +37,16 @@ function looksLikeAnyBundleRoot(root) {
37
37
  }
38
38
  /**
39
39
  * Find the directory inside `extractedDir` that should be treated as the
40
- * bundle/stash root. Probes the top-level dir via the ordered §1.2 registry
40
+ * bundle/stash root. Probes the top-level dir via the ordered registry
41
41
  * probe; if nothing claims it, BFS for the shallowest nested candidate.
42
42
  */
43
43
  export function detectStashRoot(extractedDir) {
44
44
  const root = path.resolve(extractedDir);
45
- // WI-3.1 → wired to the static registry: the top-level root check now routes
46
- // through the ordered §1.2 `looksLikeRoot` probe over ALL built-in adapters
47
- // (`registry.getAdapters()`), not the single hardcoded `akm` probe. This is
48
- // BEHAVIOR-IDENTICAL for akm roots — `akm.looksLikeRoot` reproduces the former
49
- // `.stash`-marker + immediate-stash-subdir (`hasStashDirs`) checks VERBATIM
50
- // (akm-adapter.looksLikeRoot) and `akm` is in the probe set — so those two
51
- // local checks are now subsumed and were removed. It additionally recognizes
52
- // NON-akm bundle roots (okf root index / llm-wiki / `.claude` / …) at the top
53
- // level, which the single-adapter probe missed but the old final `return root`
54
- // fallback also happened to yield. The shallowest-BFS nested-layout discovery
55
- // is unchanged.
45
+ // The top-level root check routes through the ordered `looksLikeRoot` probe
46
+ // over ALL built-in adapters (`registry.getAdapters()`). `akm.looksLikeRoot`
47
+ // covers the `.stash` marker and immediate stash subdirs; the other adapters
48
+ // recognize NON-akm bundle roots (okf root index / llm-wiki / `.claude` / …).
49
+ // Otherwise fall through to the shallowest-BFS nested-layout discovery.
56
50
  if (looksLikeAnyBundleRoot(root)) {
57
51
  return root;
58
52
  }
@@ -161,7 +155,8 @@ function countStashDirs(dirPath) {
161
155
  * (scripts/, skills/, etc.), so nested layouts like `project/my-stash/scripts/`
162
156
  * are discovered even without a `.stash` marker.
163
157
  *
164
- * Skips `root` itself since the caller already checked it via `hasStashDirs`.
158
+ * Skips `root` itself since the caller already checked it via
159
+ * `looksLikeAnyBundleRoot`.
165
160
  */
166
161
  const BFS_MAX_DEPTH = 5;
167
162
  function findShallowestStashRoot(root) {
@@ -90,7 +90,7 @@ export async function ensureWebsiteMirror(config, options) {
90
90
  crawlTimeoutMs: resolveCrawlTimeoutMs(config.options?.crawlTimeoutMs),
91
91
  resolveSecret: options?.resolveSecret,
92
92
  // As-supplied, pre-normalization start URL (see crawlWebsite's
93
- // `rawStartUrl` doc comment): threaded through purely for the C-02
93
+ // `rawStartUrl` doc comment): threaded through purely for the start-URL
94
94
  // robots.txt check, which must see the trailing slash the user
95
95
  // actually typed before `normalizeSiteUrl` strips it.
96
96
  rawStartUrl: rawUrl,
@@ -461,7 +461,7 @@ async function resolveCrawlRobotsDecision(robots, normalizedUrl, rawUrl) {
461
461
  /**
462
462
  * Fail fast, before any page fetch, when the start URL itself is
463
463
  * off-limits per an actually-parsed robots.txt rule. A 5xx on robots.txt
464
- * itself no longer reaches this as `disallowAll`: `loadRobotsTxt` reports it the same way a
464
+ * itself does not reach this as `disallowAll`: `loadRobotsTxt` reports it the same way a
465
465
  * 4xx is reported (`ALLOW_ALL_RULES`), with its own warning, rather than
466
466
  * treating the site's own transient error as a Disallow. The check below
467
467
  * stays as defense-in-depth for a `RobotsPolicy` built over some other
@@ -594,7 +594,7 @@ async function crawlWebsite(startUrl, options) {
594
594
  }
595
595
  // Counts actual `fetchWebsitePage` invocations (regardless of outcome) so
596
596
  // Crawl-delay pacing skips the first fetch and never charges a delay slot
597
- // to a URL that robots.txt skipped without ever being fetched (C-11).
597
+ // to a URL that robots.txt skipped without ever being fetched.
598
598
  let fetchAttempts = 0;
599
599
  // URLs pushed back because their Crawl-delay would not fit in the remaining
600
600
  // budget, and URLs that ran out of retries entirely. Reported at the end so
@@ -827,8 +827,7 @@ async function fetchWebsiteResponse(pageUrl, redirectCount = 0, options) {
827
827
  // origin is the literal string "null", and re-resolving against it
828
828
  // throws an unhandled TypeError that aborts the whole crawl. Refuse
829
829
  // the hop outright instead of ever handing such a URL to the policy —
830
- // `fetchWebsitePage` maps this to a graceful skip. Regression
831
- // introduced by a67412c, which fell back to the raw `nextUrl` here.
830
+ // `fetchWebsitePage` maps this to a graceful skip.
832
831
  warnVerbose("[akm] website crawl: skipping redirect to %s (not an http(s) URL)", nextUrl);
833
832
  throw new RobotsDisallowedRedirectError(nextUrl);
834
833
  }
@@ -990,7 +989,7 @@ export function parseLlmsTxtLinks(text, baseUrl) {
990
989
  * content legitimately contains `##` headings too — so recovered page
991
990
  * boundaries can't be trusted, whereas per-page fetches through the existing
992
991
  * pipeline are cheap, bounded by this author-curated list, and produce
993
- * cleanly addressable assets. See the issue's "alternatives considered".
992
+ * cleanly addressable assets.
994
993
  */
995
994
  async function fetchLlmsManifest(start, robots, options) {
996
995
  const manifestUrl = new URL("/llms.txt", start.origin).toString();
@@ -1043,7 +1042,7 @@ function normalizeCrawlUrl(rawUrl) {
1043
1042
  }
1044
1043
  }
1045
1044
  /**
1046
- * D-R6: `index.md`/`log.md` are OKF reserved structural filenames at every
1045
+ * `index.md`/`log.md` are OKF reserved structural filenames at every
1047
1046
  * depth — no adapter indexes them, so a crawled page must never land on one.
1048
1047
  * Same remap convention as the content migration (`index.md` →
1049
1048
  * `index-content.md`). Segments are already lowercased by slugifySegment.
@@ -147,3 +147,18 @@ export function purgeOldEvents(db, retentionDays = 90) {
147
147
  const changes = result.changes ?? 0;
148
148
  return typeof changes === "bigint" ? Number(changes) : changes;
149
149
  }
150
+ /**
151
+ * Rewrite every `events.ref` naming `oldBundleId` to `newBundleId` (`akm bundle
152
+ * rename`). Improve reads an asset's feedback events by this ref, and
153
+ * `insertEventOnce` finds a journal event by it under the proposal or workflow
154
+ * ref the rename also rewrites, so an event left under the old prefix stops
155
+ * counting for the renamed bundle. A legacy unqualified ref is not matched by
156
+ * the `<old>//` prefix and is left alone; refs inside `metadata_json` are
157
+ * history and stay as written. Returns the number of rows rewritten.
158
+ */
159
+ export function renameEventRefs(db, oldBundleId, newBundleId) {
160
+ const prefix = `${oldBundleId}//`;
161
+ return Number(db
162
+ .prepare(`UPDATE events SET ref = ? || substr(ref, length(?) + 1) WHERE substr(ref, 1, length(?)) = ?`)
163
+ .run(newBundleId, oldBundleId, prefix, prefix).changes);
164
+ }
@@ -269,3 +269,16 @@ export function listImproveLedgerRows(db, stashDir, sources) {
269
269
  .all(stashDir, ...(sources && sources.length > 0 ? sources : []));
270
270
  return rows.map((row) => toRow(withHash ? row : { ...row, content_hash: null }));
271
271
  }
272
+ /**
273
+ * Rewrite every `improve_ledger.ref` naming `oldBundleId` to `newBundleId`
274
+ * (`akm bundle rename`), so improve keeps the renamed bundle's attempts and
275
+ * back-off windows next to its feedback events. A row already at the new key
276
+ * can only be an orphan of a removed bundle by that name; the renamed bundle's
277
+ * row replaces it. Returns the number of rows rewritten.
278
+ */
279
+ export function renameImproveLedgerRefs(db, oldBundleId, newBundleId) {
280
+ const prefix = `${oldBundleId}//`;
281
+ return Number(db
282
+ .prepare(`UPDATE OR REPLACE improve_ledger SET ref = ? || substr(ref, length(?) + 1) WHERE substr(ref, 1, length(?)) = ?`)
283
+ .run(newBundleId, oldBundleId, prefix, prefix).changes);
284
+ }
@@ -18,6 +18,7 @@ import { getStateDbPath, withStateDb } from "../../core/state-db.js";
18
18
  import { warn } from "../../core/warn.js";
19
19
  import { getMarkdownFragmentContent, hasMarkdownFragmentContent, } from "../../indexer/passes/metadata.js";
20
20
  import { buildSearchText } from "../../indexer/search/search-fields.js";
21
+ import { capEmbeddingText, DEFAULT_MAX_INPUT_TOKENS } from "../../llm/embedders/remote.js";
21
22
  import { sha256Hex } from "../../runtime.js";
22
23
  import { ENTRY_COLUMNS, rowToIndexedEntry } from "./index-entry-mapper.js";
23
24
  import { deleteFtsEntries, replaceFtsEntry } from "./index-fts-repository.js";
@@ -26,13 +27,21 @@ import { SQLITE_CHUNK_SIZE } from "./index-sql.js";
26
27
  import { deleteEntryVectors } from "./index-vec-repository.js";
27
28
  // ── Entry operations ────────────────────────────────────────────────────────
28
29
  /**
29
- * Hash of the text an entry's vector is embedded from (`buildSearchText`),
30
- * stored as `entries.embed_hash`: a change deletes the entry's vector, and the
31
- * next embedding pass re-embeds it.
30
+ * Hash of an entry's whole search text (`buildSearchText`), stored as
31
+ * `entries.embed_hash`: text that hashes the same cannot change what the
32
+ * entry's vector is embedded from.
32
33
  */
33
34
  function embedHash(entry) {
34
35
  return sha256Hex(buildSearchText(entry));
35
36
  }
37
+ /**
38
+ * The text an entry's vector is embedded from: its search text cut to the
39
+ * embedding cap (`embedding.maxInputTokens`). Text past the cut never reaches
40
+ * the embedder, so a change there leaves the vector as it is.
41
+ */
42
+ function embeddedInput(entry, maxInputTokens) {
43
+ return capEmbeddingText(buildSearchText(entry), maxInputTokens).text;
44
+ }
36
45
  /**
37
46
  * Insert or update one canonical entry and all synchronously derived search
38
47
  * state. Returns the stable row id.
@@ -40,8 +49,13 @@ function embedHash(entry) {
40
49
  * The entries row, FTS projection, declared links, and stale-vector
41
50
  * invalidation commit as one SQLite transaction. Callers therefore cannot
42
51
  * publish an entry and forget a second FTS or links maintenance step.
52
+ *
53
+ * The entry's vector is deleted when the text it is embedded from changes,
54
+ * that is when the search text cut to `maxInputTokens` (the embedding cap,
55
+ * `embedding.maxInputTokens`) differs from the cut of the stored document. A
56
+ * change past the cut rewrites the keyword rows and keeps the vector.
43
57
  */
44
- export function upsertEntry(db, filePath, entry, provenance, contentHash) {
58
+ export function upsertEntry(db, filePath, entry, provenance, contentHash, maxInputTokens = DEFAULT_MAX_INPUT_TOKENS) {
45
59
  // Hot path during indexing — cache prepared statements per database
46
60
  // connection so we don't pay the SQL parse/compile cost on every call.
47
61
  const stmts = getUpsertStmts(db);
@@ -54,10 +68,14 @@ export function upsertEntry(db, filePath, entry, provenance, contentHash) {
54
68
  // writer's current value.
55
69
  const apply = () => {
56
70
  const previous = stmts.findByItemRef.get(provenance.itemRef);
71
+ // Decided before the upsert overwrites the stored document it compares against.
72
+ const vectorStale = previous != null &&
73
+ previous.embed_hash !== hash &&
74
+ !storedDocumentEmbedsAs(stmts, previous, embeddedInput(entry, maxInputTokens), maxInputTokens);
57
75
  const result = stmts.upsert.get(provenance.itemRef, provenance.bundleId, provenance.componentId, provenance.conceptId, provenance.adapterId, entry.type, filePath, contentHash ?? null, JSON.stringify(entry), derivedFrom, hash);
58
76
  if (!result)
59
77
  throw new Error("upsertEntry: item_ref not found after upsert");
60
- if (previous?.id === result.id && previous.embed_hash !== hash)
78
+ if (previous?.id === result.id && vectorStale)
61
79
  deleteEntryVectors(db, result.id);
62
80
  replaceFtsEntry(db, result.id, entry, hasMarkdownFragmentContent(entry) ? (getMarkdownFragmentContent(entry) ?? null) : undefined);
63
81
  replaceEntryLinks(db, result.id, entry, provenance);
@@ -69,6 +87,25 @@ export function upsertEntry(db, filePath, entry, provenance, contentHash) {
69
87
  // partial entries row through its outer transaction.
70
88
  return db.transaction(apply)();
71
89
  }
90
+ /**
91
+ * Whether the stored document of `previous` embeds as `input`. The stored
92
+ * document is what the entry's current vector was embedded from, so the
93
+ * previous input is derived from it and needs no column of its own. The stored
94
+ * hash vouches for that: a document that cannot be read, or whose search text
95
+ * no longer hashes to `embed_hash`, has no known input, and its vector goes.
96
+ */
97
+ function storedDocumentEmbedsAs(stmts, previous, input, maxInputTokens) {
98
+ const row = stmts.documentById.get(previous.id);
99
+ if (!row)
100
+ return false;
101
+ try {
102
+ const text = buildSearchText(JSON.parse(row.document_json));
103
+ return sha256Hex(text) === previous.embed_hash && capEmbeddingText(text, maxInputTokens).text === input;
104
+ }
105
+ catch {
106
+ return false;
107
+ }
108
+ }
72
109
  const upsertStmtsByDb = new WeakMap();
73
110
  // item_ref is the sole durable conflict target. `content_hash` COALESCEs so
74
111
  // an upsert that omits it (`contentHash` is optional) cannot wipe a scan hash.
@@ -101,6 +138,7 @@ function getUpsertStmts(db) {
101
138
  RETURNING id
102
139
  `),
103
140
  findByItemRef: db.prepare("SELECT id, embed_hash FROM entries WHERE item_ref = ?"),
141
+ documentById: db.prepare("SELECT document_json FROM entries WHERE id = ?"),
104
142
  };
105
143
  upsertStmtsByDb.set(db, stmts);
106
144
  return stmts;
@@ -118,3 +118,15 @@ export function countAssetOutcomeMissing(db) {
118
118
  const row = db.prepare(`SELECT COUNT(*) AS n FROM asset_outcome WHERE missing_since IS NOT NULL`).get();
119
119
  return row.n;
120
120
  }
121
+ /**
122
+ * Rewrite every `asset_outcome.asset_ref` naming `oldBundleId` to
123
+ * `newBundleId` (`akm bundle rename`). Same prefix match and orphan-replacing
124
+ * collision policy as `renameAssetSalienceRefs`. Returns the number of rows
125
+ * rewritten.
126
+ */
127
+ export function renameAssetOutcomeRefs(db, oldBundleId, newBundleId) {
128
+ const prefix = `${oldBundleId}//`;
129
+ return Number(db
130
+ .prepare(`UPDATE OR REPLACE asset_outcome SET asset_ref = ? || substr(asset_ref, length(?) + 1) WHERE substr(asset_ref, 1, length(?)) = ?`)
131
+ .run(newBundleId, oldBundleId, prefix, prefix).changes);
132
+ }
@@ -504,8 +504,8 @@ export function listStateProposalIdsByPrefix(db, stashDir, idPrefix) {
504
504
  * rename --dry-run`: the same two `WHERE` predicates, read-only.
505
505
  */
506
506
  export function countProposalsForBundleRename(db, oldBundleId) {
507
- const prefix = `${escapeLikePattern(oldBundleId)}//`;
508
- const refs = db.prepare(`SELECT COUNT(*) AS n FROM proposals WHERE ref LIKE ? ESCAPE '\\'`).get(`${prefix}%`).n;
507
+ const prefix = `${oldBundleId}//`;
508
+ const refs = db.prepare(`SELECT COUNT(*) AS n FROM proposals WHERE substr(ref, 1, length(?)) = ?`).get(prefix, prefix).n;
509
509
  const targets = db
510
510
  .prepare(`SELECT COUNT(*) AS n FROM proposals WHERE json_extract(metadata_json, '$.proposedTarget.source') = ?`)
511
511
  .get(oldBundleId).n;
@@ -522,11 +522,11 @@ export function countProposalsForBundleRename(db, oldBundleId) {
522
522
  * rewritten.
523
523
  */
524
524
  export function renameProposalsBundleRef(db, oldBundleId, newBundleId) {
525
- const prefix = `${escapeLikePattern(oldBundleId)}//`;
525
+ const prefix = `${oldBundleId}//`;
526
526
  return db.transaction(() => {
527
527
  const refs = Number(db
528
- .prepare(`UPDATE proposals SET ref = ? || substr(ref, length(?) + 1) WHERE ref LIKE ? ESCAPE '\\'`)
529
- .run(newBundleId, oldBundleId, `${prefix}%`).changes);
528
+ .prepare(`UPDATE proposals SET ref = ? || substr(ref, length(?) + 1) WHERE substr(ref, 1, length(?)) = ?`)
529
+ .run(newBundleId, oldBundleId, prefix, prefix).changes);
530
530
  const targets = Number(db
531
531
  .prepare(`UPDATE proposals SET metadata_json = json_set(metadata_json, '$.proposedTarget.source', ?)
532
532
  WHERE json_extract(metadata_json, '$.proposedTarget.source') = ?`)
@@ -153,3 +153,16 @@ export function countAssetSalienceMissing(db) {
153
153
  const row = db.prepare(`SELECT COUNT(*) AS n FROM asset_salience WHERE missing_since IS NOT NULL`).get();
154
154
  return row.n;
155
155
  }
156
+ /**
157
+ * Rewrite every `asset_salience.asset_ref` naming `oldBundleId` to
158
+ * `newBundleId` (`akm bundle rename`). A legacy unqualified ref is not matched
159
+ * by the `<old>//` prefix and is left alone. A row already at the new ref can only
160
+ * be an orphan of a removed bundle by that name; the renamed bundle's row
161
+ * replaces it. Returns the number of rows rewritten.
162
+ */
163
+ export function renameAssetSalienceRefs(db, oldBundleId, newBundleId) {
164
+ const prefix = `${oldBundleId}//`;
165
+ return Number(db
166
+ .prepare(`UPDATE OR REPLACE asset_salience SET asset_ref = ? || substr(asset_ref, length(?) + 1) WHERE substr(asset_ref, 1, length(?)) = ?`)
167
+ .run(newBundleId, oldBundleId, prefix, prefix).changes);
168
+ }
@@ -11,7 +11,6 @@
11
11
  */
12
12
  import { isRecord } from "../../core/common.js";
13
13
  import { warnOnce } from "../../core/warn.js";
14
- import { escapeLikePattern } from "../like-pattern.js";
15
14
  function metadataError(message) {
16
15
  throw new Error(`invalid task_history metadata_json: ${message}`);
17
16
  }
@@ -208,19 +207,21 @@ export function queryCompletedTaskIntervals(db, since, until) {
208
207
  * rename --dry-run`: the same `WHERE` predicate, read-only.
209
208
  */
210
209
  export function countTaskHistoryTargetRefs(db, oldBundleId) {
211
- const prefix = `${escapeLikePattern(oldBundleId)}//`;
212
- return db.prepare(`SELECT COUNT(*) AS n FROM task_history WHERE target_ref LIKE ? ESCAPE '\\'`).get(`${prefix}%`).n;
210
+ const prefix = `${oldBundleId}//`;
211
+ return db
212
+ .prepare(`SELECT COUNT(*) AS n FROM task_history WHERE substr(target_ref, 1, length(?)) = ?`)
213
+ .get(prefix, prefix).n;
213
214
  }
214
215
  /**
215
216
  * Rewrite every `task_history.target_ref` naming `oldBundleId` (a workflow
216
217
  * run's fully-qualified `<bundle>//conceptId` target) to `newBundleId`
217
218
  * (`akm bundle rename`, D6). A row whose `target_ref` is unset or not
218
- * bundle-qualified is not matched by the `LIKE` prefix and is left alone.
219
+ * bundle-qualified is not matched by the `<old>//` prefix and is left alone.
219
220
  * Returns the number of rows rewritten.
220
221
  */
221
222
  export function renameTaskHistoryTargetRefs(db, oldBundleId, newBundleId) {
222
- const prefix = `${escapeLikePattern(oldBundleId)}//`;
223
+ const prefix = `${oldBundleId}//`;
223
224
  return Number(db
224
- .prepare(`UPDATE task_history SET target_ref = ? || substr(target_ref, length(?) + 1) WHERE target_ref LIKE ? ESCAPE '\\'`)
225
- .run(newBundleId, oldBundleId, `${prefix}%`).changes);
225
+ .prepare(`UPDATE task_history SET target_ref = ? || substr(target_ref, length(?) + 1) WHERE substr(target_ref, 1, length(?)) = ?`)
226
+ .run(newBundleId, oldBundleId, prefix, prefix).changes);
226
227
  }