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.
- package/CHANGELOG.md +100 -0
- package/README.md +8 -10
- package/SECURITY.md +7 -99
- package/STABILITY.md +57 -98
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -2
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +11 -17
- package/dist/cli.js +32 -41
- package/dist/commands/feedback-cli.js +5 -4
- package/dist/commands/health.js +15 -16
- package/dist/commands/proposal/proposal-cli.js +1 -1
- package/dist/commands/read/registry-search.js +4 -1
- package/dist/commands/read/search.js +6 -6
- package/dist/commands/sources/self-update.js +9 -5
- package/dist/commands/tasks/tasks.js +12 -13
- package/dist/core/adapter/adapters/agent-skills-adapter.js +7 -7
- package/dist/core/adapter/adapters/akm-task-adapter.js +1 -1
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/adapters/claude-adapter.js +1 -1
- package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +1 -1
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +1 -1
- package/dist/core/adapter/adapters/opencode-adapter.js +1 -1
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -1
- package/dist/core/bundle-rename.js +17 -7
- package/dist/indexer/bundle-identity-guard.js +6 -6
- package/dist/indexer/index-written-assets.js +3 -3
- package/dist/indexer/indexer.js +12 -8
- package/dist/indexer/materialize-embeddings.js +12 -2
- package/dist/indexer/passes/metadata.js +103 -46
- package/dist/indexer/search/ranking.js +7 -3
- package/dist/indexer/search/search-fields.js +1 -1
- package/dist/indexer/usage/usage-events.js +30 -8
- package/dist/indexer/walk/walker.js +1 -1
- package/dist/output/shapes/helpers.js +3 -3
- package/dist/output/text/command-format.js +5 -6
- package/dist/scripts/akm-migrate-node.js +31 -3
- package/dist/scripts/akm-migrate.js +31 -3
- package/dist/sources/providers/git-provider.js +2 -4
- package/dist/sources/providers/provider-utils.js +8 -13
- package/dist/sources/snapshot-fetchers/website-ingest.js +6 -7
- package/dist/storage/repositories/events-repository.js +15 -0
- package/dist/storage/repositories/improve-ledger-repository.js +13 -0
- package/dist/storage/repositories/index-entries-repository.js +43 -5
- package/dist/storage/repositories/outcome-repository.js +12 -0
- package/dist/storage/repositories/proposals-repository.js +5 -5
- package/dist/storage/repositories/salience-repository.js +13 -0
- package/dist/storage/repositories/task-history-repository.js +8 -7
- package/dist/storage/repositories/workflow-runs-repository.js +10 -8
- package/docs/README.md +24 -60
- package/docs/agents/README.md +3 -8
- package/docs/architecture/README.md +18 -20
- package/docs/architecture/adapters.md +40 -43
- package/docs/architecture/architecture.md +171 -151
- package/docs/architecture/improvement.md +95 -91
- package/docs/architecture/internals/classification.md +101 -119
- package/docs/architecture/internals/fresh-host-rebuild-runbook.md +33 -35
- package/docs/architecture/internals/health-advisories.md +59 -46
- package/docs/architecture/internals/improve-workflow.md +166 -693
- package/docs/architecture/internals/indexing.md +65 -465
- package/docs/architecture/internals/storage-locations.md +301 -848
- package/docs/architecture/persisted-data-compat.md +6 -6
- package/docs/architecture/specs/okf-support.md +67 -95
- package/docs/architecture/specs/ref.md +67 -125
- package/docs/architecture/testing/testing-workflow.md +107 -167
- package/docs/architecture/workflow-engine.md +34 -33
- package/docs/guides/README.md +5 -5
- package/docs/guides/author-bundles.md +72 -98
- package/docs/guides/author-workflows.md +11 -18
- package/docs/guides/bundles.md +91 -123
- package/docs/guides/capture-knowledge.md +10 -9
- package/docs/guides/concepts.md +45 -30
- package/docs/guides/discover-and-load.md +39 -32
- package/docs/guides/getting-started.md +24 -24
- package/docs/guides/improve-the-library.md +98 -80
- package/docs/guides/recipes/headless-install.md +4 -4
- package/docs/guides/recipes/website-source.md +9 -5
- package/docs/guides/run-workflows.md +10 -11
- package/docs/guides/scheduling.md +11 -17
- package/docs/guides/use-with-any-agent.md +15 -14
- package/docs/guides/wikis.md +6 -9
- package/docs/integration/bundling-akm.md +21 -26
- package/docs/maintainers/README.md +2 -2
- package/docs/maintainers/local-development.md +3 -16
- package/docs/migration/README.md +2 -1
- package/docs/migration/release-notes/0.10.0.md +167 -22
- package/docs/migration/release-notes/README.md +2 -2
- package/docs/migration/v0.9-to-v0.10.md +47 -7
- package/docs/product-surface.md +10 -8
- package/docs/reference/README.md +26 -15
- package/docs/reference/asset-types.md +11 -12
- package/docs/reference/bundle-types.md +6 -5
- package/docs/reference/cli.md +52 -28
- package/docs/reference/configuration.md +46 -60
- package/docs/reference/data-and-telemetry.md +204 -421
- package/docs/reference/env-and-secrets.md +80 -83
- package/docs/reference/memory.md +1 -1
- package/docs/reference/refs.md +23 -36
- package/docs/reference/registry.md +138 -348
- package/docs/reference/supported-formats.md +8 -8
- package/docs/reference/tasks.md +13 -19
- package/docs/reference/website-sources.md +23 -15
- package/docs/reference/workflow-schema.md +28 -47
- package/docs/reference/workflows.md +33 -37
- 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
|
-
*
|
|
73
|
-
*
|
|
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'
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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...)"
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
158
|
-
// adapter's directoryList()
|
|
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
|
|
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
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
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
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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 &&
|
|
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 = `${
|
|
508
|
-
const refs = db.prepare(`SELECT COUNT(*) AS n FROM proposals WHERE ref
|
|
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 = `${
|
|
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
|
|
529
|
-
.run(newBundleId, oldBundleId,
|
|
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 = `${
|
|
212
|
-
return db
|
|
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
|
|
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 = `${
|
|
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
|
|
225
|
-
.run(newBundleId, oldBundleId,
|
|
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
|
}
|