akm-cli 0.10.261007-beta → 0.10.261009-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 +72 -0
- package/STABILITY.md +10 -4
- package/dist/api.d.ts +6 -0
- package/dist/api.js +12 -1
- package/dist/assets/prompts/consolidate-pair-check.md +2 -0
- package/dist/assets/prompts/consolidate-pair-same-age.md +1 -0
- package/dist/assets/prompts/consolidate-pair-terms.md +6 -0
- package/dist/assets/prompts/consolidate-pair.md +4 -12
- package/dist/commands/health.js +1 -2
- package/dist/commands/improve/consolidate/pair-pass.js +59 -31
- package/dist/commands/improve/extract.js +18 -12
- package/dist/commands/improve/memory/memory-belief.js +2 -2
- package/dist/commands/proposal/repository.js +44 -13
- package/dist/commands/read/curate.js +77 -18
- package/dist/commands/read/search-cli.js +25 -11
- package/dist/commands/read/search.js +3 -3
- package/dist/commands/read/show.js +47 -1
- package/dist/core/adapter/adapters/akm-adapter.js +1 -0
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +3 -5
- package/dist/core/adapter/adapters/okf-adapter.js +3 -16
- package/dist/core/asset/markdown-link-targets.js +43 -0
- package/dist/core/common.js +17 -2
- package/dist/core/config/config-io.js +14 -4
- package/dist/indexer/passes/metadata.js +17 -0
- package/dist/indexer/scan/doc-to-entry.js +1 -0
- package/dist/indexer/search/db-search.js +16 -7
- package/dist/indexer/search/search-hit-enrichers.js +3 -3
- package/dist/output/shapes/curate.js +3 -1
- package/dist/output/shapes/helpers.js +6 -0
- package/dist/output/text/show-format.js +7 -0
- package/dist/scripts/akm-migrate-node.js +1824 -324
- package/dist/scripts/akm-migrate.js +1824 -324
- package/dist/storage/repositories/index-entries-repository.js +16 -0
- package/dist/storage/repositories/index-fts-repository.js +28 -6
- package/dist/storage/repositories/index-vec-repository.js +19 -3
- package/docs/architecture/0.10-process-rules.md +4 -4
- package/docs/architecture/architecture.md +4 -0
- package/docs/architecture/persisted-data-compat.md +1 -1
- package/docs/reference/api.md +19 -1
- package/docs/reference/cli.md +56 -4
- package/docs/reference/data-and-telemetry.md +1 -1
- package/docs/reference/memory.md +7 -6
- package/package.json +2 -1
|
@@ -24,7 +24,7 @@ import { appendEvent } from "../../core/events.js";
|
|
|
24
24
|
import { nonTaskInput } from "../../core/non-task-input.js";
|
|
25
25
|
import { redactCredentialPatterns } from "../../core/redaction.js";
|
|
26
26
|
import { withStateDbTelemetry } from "../../core/state-db.js";
|
|
27
|
-
import { searchHitContent } from "../../indexer/search/db-search.js";
|
|
27
|
+
import { matchBeliefFilter, searchHitContent } from "../../indexer/search/db-search.js";
|
|
28
28
|
import { copySearchHitAttribution, getSearchHitAttribution, usageEventAttributionMetadata, } from "../../indexer/search/search-attribution.js";
|
|
29
29
|
import { insertUsageEvent } from "../../indexer/usage/usage-events.js";
|
|
30
30
|
import { estimateTokenCount } from "../../llm/embedders/remote.js";
|
|
@@ -32,9 +32,11 @@ import { isLlmFeatureEnabled, tryLlmFeature } from "../../llm/feature-gate.js";
|
|
|
32
32
|
import { rerankDocuments } from "../../llm/rerank-client.js";
|
|
33
33
|
import { truncateDescription } from "../../output/shapes/helpers.js";
|
|
34
34
|
import { TELEMETRY_BUSY_TIMEOUT_MS, withIndexDb } from "../../storage/repositories/index-db.js";
|
|
35
|
-
import { findEntryIdByRef, getItemRefById } from "../../storage/repositories/index-entries-repository.js";
|
|
36
|
-
import { akmSearch, parseSearchSource } from "./search.js";
|
|
35
|
+
import { findEntryIdByRef, getEntryIdByFilePath, getItemRefById, } from "../../storage/repositories/index-entries-repository.js";
|
|
36
|
+
import { akmSearch, normalizeScopeFilters, parseBeliefFilterMode, parseSearchSource } from "./search.js";
|
|
37
37
|
import { akmShowUnified } from "./show.js";
|
|
38
|
+
// Keep the effective policy with the result through packing without changing JSON.
|
|
39
|
+
const recallPolicies = new WeakMap();
|
|
38
40
|
const DEFAULT_CURATE_LIMIT = 4;
|
|
39
41
|
const MAX_CURATE_SUPPORT_REFS = 2;
|
|
40
42
|
/** Fused candidates the reranker reorders when `search.curateRerank.topN` is unset. */
|
|
@@ -103,6 +105,12 @@ export async function akmCurate(options) {
|
|
|
103
105
|
if (!trimmedQuery) {
|
|
104
106
|
throw new UsageError('A curation query is required. Usage: akm curate "<task or prompt>" [--type <type>] [--limit <n>]', "MISSING_REQUIRED_ARGUMENT");
|
|
105
107
|
}
|
|
108
|
+
const recall = {
|
|
109
|
+
type: options.type,
|
|
110
|
+
source: options.source ?? "local",
|
|
111
|
+
filters: normalizeScopeFilters(options.filters),
|
|
112
|
+
belief: parseBeliefFilterMode(options.belief ?? "current"),
|
|
113
|
+
};
|
|
106
114
|
const nonTask = nonTaskInput(trimmedQuery);
|
|
107
115
|
if (nonTask) {
|
|
108
116
|
const abstained = {
|
|
@@ -125,15 +133,18 @@ export async function akmCurate(options) {
|
|
|
125
133
|
// An enabled reranker reorders the top fused candidates, not just the final `limit`.
|
|
126
134
|
limit: Math.max(limit, rerankTopN(loadConfig())),
|
|
127
135
|
source,
|
|
136
|
+
filters: recall.filters,
|
|
137
|
+
belief: recall.belief,
|
|
128
138
|
skipLogging: true,
|
|
129
139
|
}));
|
|
130
|
-
const result = await curateSearchResults(options.query, searchResponse, limit, options.type, options.eventSource);
|
|
140
|
+
const result = await curateSearchResults(options.query, searchResponse, limit, options.type, options.eventSource, options.searchResponse ? undefined : recall);
|
|
141
|
+
recallPolicies.set(result, recall);
|
|
131
142
|
if (!options.skipLogging) {
|
|
132
143
|
logCurateEvent(options.query, result, options.eventSource, options.attributionProjection);
|
|
133
144
|
}
|
|
134
145
|
return result;
|
|
135
146
|
}
|
|
136
|
-
export async function curateSearchResults(query, result, limit, selectedType, eventSource) {
|
|
147
|
+
export async function curateSearchResults(query, result, limit, selectedType, eventSource, recall) {
|
|
137
148
|
const allStashHits = result.hits.filter((hit) => hit.type !== "registry");
|
|
138
149
|
const registryHits = result.registryHits ?? [];
|
|
139
150
|
// F3/R-018: `--type` NARROWS the candidate pool. The caller's search
|
|
@@ -145,9 +156,18 @@ export async function curateSearchResults(query, result, limit, selectedType, ev
|
|
|
145
156
|
// F4/R-019: respect `--limit` for registry fill instead of hard-capping it
|
|
146
157
|
// at a bare literal 2 — the remaining slots after stash hits ARE the cap.
|
|
147
158
|
const selectedRegistryHits = selectedStashHits.length >= limit ? [] : registryHits.slice(0, limit - selectedStashHits.length);
|
|
148
|
-
const selectedRefs = new Set(selectedStashHits.
|
|
159
|
+
const selectedRefs = new Set(selectedStashHits.flatMap((hit) => {
|
|
160
|
+
if (!recall)
|
|
161
|
+
return [hit.ref];
|
|
162
|
+
try {
|
|
163
|
+
return [hit.ref, evidenceRef(hit)];
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return [hit.ref]; // no longer indexed: enrichCuratedStashHit drops it
|
|
167
|
+
}
|
|
168
|
+
}));
|
|
149
169
|
const items = [
|
|
150
|
-
...(await Promise.all(selectedStashHits.map((hit) => enrichCuratedStashHit(query, hit, selectedRefs, eventSource)))),
|
|
170
|
+
...(await Promise.all(selectedStashHits.map((hit) => enrichCuratedStashHit(query, hit, selectedRefs, eventSource, recall)))).filter((item) => item !== undefined),
|
|
151
171
|
...selectedRegistryHits.map((hit) => buildCuratedRegistryItem(query, hit)),
|
|
152
172
|
].slice(0, limit);
|
|
153
173
|
return {
|
|
@@ -181,15 +201,24 @@ export async function packCuratedHits(result, budgetTokens) {
|
|
|
181
201
|
for (const item of stashItems) {
|
|
182
202
|
let shown;
|
|
183
203
|
try {
|
|
184
|
-
shown = await akmShowUnified({
|
|
204
|
+
shown = await akmShowUnified({
|
|
205
|
+
ref: evidenceRef(item),
|
|
206
|
+
skipLogging: true,
|
|
207
|
+
recall: recallPolicies.get(result) ?? { belief: "current" },
|
|
208
|
+
});
|
|
185
209
|
}
|
|
186
210
|
catch {
|
|
187
211
|
continue;
|
|
188
212
|
}
|
|
213
|
+
const historical = matchBeliefFilter(shown.beliefState, "historical");
|
|
214
|
+
const context = {
|
|
215
|
+
...(shown.beliefState ? { beliefState: shown.beliefState } : {}),
|
|
216
|
+
...(historical ? { reason: "Historical context; not current supporting authority." } : {}),
|
|
217
|
+
};
|
|
189
218
|
const content = shown.content ?? shown.template ?? shown.prompt ?? "";
|
|
190
219
|
const tokens = estimateTokenCount(content);
|
|
191
220
|
if (used + tokens <= budgetTokens) {
|
|
192
|
-
packed.push({ ref: item.ref, tokens, content });
|
|
221
|
+
packed.push({ ref: item.ref, tokens, content, ...context });
|
|
193
222
|
used += tokens;
|
|
194
223
|
continue;
|
|
195
224
|
}
|
|
@@ -197,7 +226,7 @@ export async function packCuratedHits(result, budgetTokens) {
|
|
|
197
226
|
const remaining = budgetTokens - used;
|
|
198
227
|
if (remaining > 0) {
|
|
199
228
|
const truncated = content.slice(0, remaining * 4);
|
|
200
|
-
packed.push({ ref: item.ref, tokens: estimateTokenCount(truncated), content: truncated });
|
|
229
|
+
packed.push({ ref: item.ref, tokens: estimateTokenCount(truncated), content: truncated, ...context });
|
|
201
230
|
used += estimateTokenCount(truncated);
|
|
202
231
|
}
|
|
203
232
|
}
|
|
@@ -205,22 +234,38 @@ export async function packCuratedHits(result, budgetTokens) {
|
|
|
205
234
|
}
|
|
206
235
|
return { query: result.query, budget: budgetTokens, tokens: used, items: packed };
|
|
207
236
|
}
|
|
208
|
-
|
|
237
|
+
function evidenceRef(item) {
|
|
238
|
+
return withIndexDb((db) => {
|
|
239
|
+
const id = getEntryIdByFilePath(db, item.path);
|
|
240
|
+
const ref = id === undefined ? undefined : getItemRefById(db, id);
|
|
241
|
+
if (!ref) {
|
|
242
|
+
if (item.path)
|
|
243
|
+
throw new UsageError(`Indexed asset no longer available: ${item.ref}`);
|
|
244
|
+
return item.ref;
|
|
245
|
+
}
|
|
246
|
+
const fragment = parseBundleRef(item.ref).fragment;
|
|
247
|
+
return fragment ? `${ref}#${fragment}` : ref;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
async function enrichCuratedStashHit(query, hit, selectedRefs, eventSource, recall) {
|
|
209
251
|
let shown;
|
|
210
252
|
try {
|
|
211
|
-
shown = await akmShowUnified({ ref: hit.ref, eventSource, skipLogging: true });
|
|
253
|
+
shown = await akmShowUnified({ ref: recall ? evidenceRef(hit) : hit.ref, eventSource, skipLogging: true, recall });
|
|
212
254
|
}
|
|
213
255
|
catch {
|
|
256
|
+
if (recall)
|
|
257
|
+
return undefined;
|
|
214
258
|
shown = undefined;
|
|
215
259
|
}
|
|
260
|
+
const ref = shown?.selectedRef ?? shown?.ref ?? hit.ref;
|
|
216
261
|
const description = shown?.description ?? hit.description;
|
|
217
262
|
const preview = buildCuratedPreview(shown, hit);
|
|
218
|
-
const supportRefs = buildCurateSupportRefs(shown?.links, selectedRefs,
|
|
263
|
+
const supportRefs = await buildCurateSupportRefs(shown?.links, selectedRefs, ref, recall);
|
|
219
264
|
const item = {
|
|
220
265
|
source: "local",
|
|
221
266
|
type: shown?.type ?? hit.type,
|
|
222
267
|
name: shown?.name ?? hit.name,
|
|
223
|
-
ref
|
|
268
|
+
ref,
|
|
224
269
|
path: shown?.path ?? hit.path,
|
|
225
270
|
editable: shown?.editable ?? hit.editable ?? false,
|
|
226
271
|
...((shown?.editable ?? hit.editable ?? false) === false
|
|
@@ -232,8 +277,9 @@ async function enrichCuratedStashHit(query, hit, selectedRefs, eventSource) {
|
|
|
232
277
|
...(shown?.parameters?.length ? { parameters: shown.parameters } : {}),
|
|
233
278
|
...(shown?.run ? { run: shown.run } : {}),
|
|
234
279
|
...(supportRefs.length > 0 ? { supportRefs } : {}),
|
|
235
|
-
followUp: `akm show ${
|
|
236
|
-
|
|
280
|
+
followUp: `akm show ${ref}`,
|
|
281
|
+
...(shown?.beliefState ? { beliefState: shown.beliefState } : {}),
|
|
282
|
+
reason: `${matchBeliefFilter(shown?.beliefState, "historical") ? "Historical context. " : ""}${buildCuratedReason(query, shown?.type ?? hit.type)}`,
|
|
237
283
|
...(hit.score !== undefined ? { score: hit.score } : {}),
|
|
238
284
|
};
|
|
239
285
|
copySearchHitAttribution(hit, item, item.description);
|
|
@@ -333,7 +379,7 @@ function rerankDocumentTexts(hits) {
|
|
|
333
379
|
* it, in the order `akm show` lists them. The LLM entity graph's `related`
|
|
334
380
|
* list no longer feeds them.
|
|
335
381
|
*/
|
|
336
|
-
function buildCurateSupportRefs(links, selectedRefs, ownerRef) {
|
|
382
|
+
async function buildCurateSupportRefs(links, selectedRefs, ownerRef, recall) {
|
|
337
383
|
const supportRefs = [];
|
|
338
384
|
for (const [part, direction] of [
|
|
339
385
|
["outgoing", "from"],
|
|
@@ -343,11 +389,24 @@ function buildCurateSupportRefs(links, selectedRefs, ownerRef) {
|
|
|
343
389
|
for (const ref of group.refs) {
|
|
344
390
|
if (ref === ownerRef || selectedRefs.has(ref) || supportRefs.some((existing) => existing.ref === ref))
|
|
345
391
|
continue;
|
|
392
|
+
let shown;
|
|
393
|
+
if (recall) {
|
|
394
|
+
try {
|
|
395
|
+
// A support ref is a link, not a hit: `--type` narrows the hits only.
|
|
396
|
+
const { type: _type, ...linkRecall } = recall;
|
|
397
|
+
shown = await akmShowUnified({ ref, recall: linkRecall, skipLogging: true });
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const historical = matchBeliefFilter(shown?.beliefState, "historical");
|
|
346
404
|
const type = supportRefType(ref);
|
|
347
405
|
supportRefs.push({
|
|
348
406
|
ref,
|
|
349
407
|
...(type ? { type } : {}),
|
|
350
|
-
|
|
408
|
+
...(shown?.beliefState ? { beliefState: shown.beliefState } : {}),
|
|
409
|
+
reason: `${historical ? "Historical context; " : ""}Declared link (${kind}) ${direction} this asset.`,
|
|
351
410
|
});
|
|
352
411
|
if (supportRefs.length >= MAX_CURATE_SUPPORT_REFS)
|
|
353
412
|
return supportRefs;
|
|
@@ -24,6 +24,14 @@ import { deliverRendered } from "../../output/html-render.js";
|
|
|
24
24
|
import { akmCurate, packCuratedHits } from "./curate.js";
|
|
25
25
|
import { akmSearch, parseBeliefFilterMode, parseScopeFilterFlags, parseSearchSource } from "./search.js";
|
|
26
26
|
import { akmShowUnified } from "./show.js";
|
|
27
|
+
const scopeFilterArg = {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "Scope filter (repeatable): --filter user=<id> --filter agent=<id> --filter run=<id> --filter channel=<name>. Narrows results without changing ranking.",
|
|
30
|
+
};
|
|
31
|
+
const beliefArg = {
|
|
32
|
+
type: "string",
|
|
33
|
+
description: "Belief filter on any asset: all|current|historical. current keeps active/asserted and unflagged assets; historical keeps deprecated/contradicted/superseded/archived assets. Labels do not establish factual truth.",
|
|
34
|
+
};
|
|
27
35
|
export const searchCommand = defineJsonCommand({
|
|
28
36
|
meta: { name: "search", description: "Search the bundle" },
|
|
29
37
|
args: {
|
|
@@ -44,20 +52,13 @@ export const searchCommand = defineJsonCommand({
|
|
|
44
52
|
description: "Include asset-level search results (only meaningful with --from registry|all)",
|
|
45
53
|
default: false,
|
|
46
54
|
},
|
|
47
|
-
filter:
|
|
48
|
-
type: "string",
|
|
49
|
-
description: "Scope filter (repeatable): --filter user=<id> --filter agent=<id> --filter run=<id> --filter channel=<name>. Narrows results without changing ranking.",
|
|
50
|
-
},
|
|
55
|
+
filter: scopeFilterArg,
|
|
51
56
|
"include-proposed": {
|
|
52
57
|
type: "boolean",
|
|
53
58
|
description: 'Include entries with quality:"proposed" in the result set. Excluded by default.',
|
|
54
59
|
default: false,
|
|
55
60
|
},
|
|
56
|
-
belief: {
|
|
57
|
-
type: "string",
|
|
58
|
-
description: "Memory belief filter: all|current|historical. current keeps active memory beliefs; historical keeps contradicted/superseded/archived memory beliefs.",
|
|
59
|
-
default: "all",
|
|
60
|
-
},
|
|
61
|
+
belief: { ...beliefArg, default: "current" },
|
|
61
62
|
"include-sessions": {
|
|
62
63
|
type: "boolean",
|
|
63
64
|
description: "Include session assets (excluded from default search results via config.search.defaultExcludeTypes).",
|
|
@@ -110,6 +111,8 @@ export const curateCommand = defineJsonCommand({
|
|
|
110
111
|
},
|
|
111
112
|
limit: { type: "string", description: "Maximum number of curated results", default: "4" },
|
|
112
113
|
from: { type: "string", description: "Search source (local|registry|all)", default: "local" },
|
|
114
|
+
filter: scopeFilterArg,
|
|
115
|
+
belief: { ...beliefArg, default: "current" },
|
|
113
116
|
pack: {
|
|
114
117
|
type: "string",
|
|
115
118
|
description: "Pack the ranked stash hits' full content into a single token-budgeted blob instead of returning refs " +
|
|
@@ -123,7 +126,14 @@ export const curateCommand = defineJsonCommand({
|
|
|
123
126
|
async run({ args }) {
|
|
124
127
|
const packBudget = parsePositiveIntFlag(args.pack ?? undefined, "--pack");
|
|
125
128
|
const outputMode = getOutputMode();
|
|
126
|
-
const curated = await runCurate({
|
|
129
|
+
const curated = await runCurate({
|
|
130
|
+
query: args.query,
|
|
131
|
+
type: args.type,
|
|
132
|
+
limit: args.limit,
|
|
133
|
+
from: args.from,
|
|
134
|
+
filters: parseAllFlagValues("--filter"),
|
|
135
|
+
belief: args.belief,
|
|
136
|
+
}, outputMode);
|
|
127
137
|
if (packBudget !== undefined) {
|
|
128
138
|
const packed = await packCuratedHits(curated, packBudget);
|
|
129
139
|
deliverRendered(outputMode.format === "text" ? formatCuratePackText(packed) : JSON.stringify(packed.items, null, 2), outputMode.outputPath);
|
|
@@ -149,6 +159,8 @@ export async function runCurate(args, outputMode) {
|
|
|
149
159
|
type: args.type,
|
|
150
160
|
limit: limitParsed && limitParsed > 0 ? limitParsed : 4,
|
|
151
161
|
source: parseSearchSource(args.from ?? "local"),
|
|
162
|
+
filters: parseScopeFilterFlags(args.filters ?? [], "--filter"),
|
|
163
|
+
belief: parseBeliefFilterMode(args.belief ?? "current"),
|
|
152
164
|
eventSource: resolveUsageEventSource(),
|
|
153
165
|
attributionProjection: outputMode.shape === "agent" ? "agent" : outputMode.detail,
|
|
154
166
|
});
|
|
@@ -158,7 +170,9 @@ function formatCuratePackText(packed) {
|
|
|
158
170
|
if (packed.items.length === 0) {
|
|
159
171
|
return `No packed content for "${packed.query}" (budget ${packed.budget} tokens).`;
|
|
160
172
|
}
|
|
161
|
-
return packed.items
|
|
173
|
+
return packed.items
|
|
174
|
+
.map((item) => `## ${item.ref}\n\n${item.reason ? `${item.reason}\n\n` : ""}${item.content}`)
|
|
175
|
+
.join("\n\n");
|
|
162
176
|
}
|
|
163
177
|
/**
|
|
164
178
|
* Reject any positional after the ref: `akm show` takes one ref, and a stray
|
|
@@ -103,7 +103,7 @@ export async function akmSearch(input) {
|
|
|
103
103
|
const sources = allSources;
|
|
104
104
|
const filters = normalizeScopeFilters(input.filters);
|
|
105
105
|
const includeProposed = input.includeProposed === true;
|
|
106
|
-
const belief = input.belief ?? "
|
|
106
|
+
const belief = parseBeliefFilterMode(input.belief ?? "current");
|
|
107
107
|
const localResult = source === "registry"
|
|
108
108
|
? undefined
|
|
109
109
|
: await searchLocal({
|
|
@@ -118,7 +118,7 @@ export async function akmSearch(input) {
|
|
|
118
118
|
beliefFilter: belief,
|
|
119
119
|
// When `--from <name>` narrowed the source list above, propagate
|
|
120
120
|
// that intent down to the database layer so FTS/vector hits from
|
|
121
|
-
// sources outside the narrowed set
|
|
121
|
+
// sources outside the narrowed set cannot consume candidate budgets.
|
|
122
122
|
// Without this, the index (which spans every configured source)
|
|
123
123
|
// would leak hits from sources the caller did not request.
|
|
124
124
|
restrictToSources: namedSourceName !== undefined,
|
|
@@ -384,7 +384,7 @@ export function parseBeliefFilterMode(value) {
|
|
|
384
384
|
* `undefined` when nothing meaningful remains, so callers don't pay for an
|
|
385
385
|
* empty-filter post-walk in `searchLocal`.
|
|
386
386
|
*/
|
|
387
|
-
function normalizeScopeFilters(raw) {
|
|
387
|
+
export function normalizeScopeFilters(raw) {
|
|
388
388
|
if (!raw)
|
|
389
389
|
return undefined;
|
|
390
390
|
const out = {};
|
|
@@ -34,13 +34,15 @@ import { SCRIPT_EXTENSIONS } from "../../core/recognition-util.js";
|
|
|
34
34
|
import { presentationFor } from "../../core/type-presentation.js";
|
|
35
35
|
import { warn, warnOnce } from "../../core/warn.js";
|
|
36
36
|
import { lookupBundleRef, lookupBundleRefWithResolution, lookupBundleRefWithResolutionReadonly, } from "../../indexer/indexer.js";
|
|
37
|
-
import { projectMarkdownFragmentContent } from "../../indexer/passes/metadata.js";
|
|
37
|
+
import { applyCuratedFrontmatter, applyProvenanceFrontmatter, projectMarkdownFragmentContent, } from "../../indexer/passes/metadata.js";
|
|
38
38
|
import { ensurePrimaryIndexForRead, resolveReadSources } from "../../indexer/read-preflight.js";
|
|
39
|
+
import { applyEntryFilters } from "../../indexer/search/db-search.js";
|
|
39
40
|
import { buildEditHint, findSourceForPath, isEditable, resolveSourceEntries, } from "../../indexer/search/search-source.js";
|
|
40
41
|
import { recentShowCount, recordShowUsage } from "../../indexer/usage/show-usage.js";
|
|
41
42
|
import { buildFileContext, buildRenderContext, getRenderer, } from "../../indexer/walk/file-context.js";
|
|
42
43
|
import { resolveSourcesForOrigin } from "../../registry/origin-resolve.js";
|
|
43
44
|
import { withIndexDb } from "../../storage/repositories/index-db.js";
|
|
45
|
+
import { getEntryById, getEntryIdByFilePath } from "../../storage/repositories/index-entries-repository.js";
|
|
44
46
|
import { getIndexedMarkdownFragment } from "../../storage/repositories/index-fts-repository.js";
|
|
45
47
|
import { readEntryLinks } from "../../storage/repositories/index-links-repository.js";
|
|
46
48
|
import { getCurrentWorkflowScopeKey } from "../../workflows/authoring/scope-key.js";
|
|
@@ -258,6 +260,34 @@ export async function showLocal(input) {
|
|
|
258
260
|
"Run `akm bundle create` to create the stash directory, or check `akm bundle list` for configured paths.");
|
|
259
261
|
}
|
|
260
262
|
const fileCtx = buildFileContext(sourceStashDir, assetPath);
|
|
263
|
+
let beliefState;
|
|
264
|
+
if (input.recall) {
|
|
265
|
+
const policy = input.recall;
|
|
266
|
+
const sources = policy.source && !["local", "all"].includes(policy.source)
|
|
267
|
+
? resolveSourcesForOrigin(policy.source, allSources)
|
|
268
|
+
: allSources;
|
|
269
|
+
const eligible = withIndexDb((db) => {
|
|
270
|
+
const id = getEntryIdByFilePath(db, assetPath);
|
|
271
|
+
const row = id === undefined ? undefined : getEntryById(db, id);
|
|
272
|
+
if (!row || id === undefined)
|
|
273
|
+
return false;
|
|
274
|
+
// Source-live show must not restore a belief/scope changed after indexing.
|
|
275
|
+
const entry = { ...row.entry, scope: undefined };
|
|
276
|
+
applyCuratedFrontmatter(entry, parseFrontmatter(fileCtx.content()).data);
|
|
277
|
+
const allowed = applyEntryFilters([{ ...row, id, entry }], {
|
|
278
|
+
db,
|
|
279
|
+
sources,
|
|
280
|
+
restrictToSources: true,
|
|
281
|
+
filters: policy.filters,
|
|
282
|
+
includeProposed: false,
|
|
283
|
+
beliefFilter: policy.belief ?? "current",
|
|
284
|
+
});
|
|
285
|
+
beliefState = entry.beliefState;
|
|
286
|
+
return allowed.length > 0 && (!policy.type || policy.type === "any" || entry.type === policy.type);
|
|
287
|
+
}, { readOnly: input.readOnly === true });
|
|
288
|
+
if (!eligible)
|
|
289
|
+
throw new NotFoundError(`Asset not found for recall filters: ${input.ref}`);
|
|
290
|
+
}
|
|
261
291
|
const presentedName = indexedEntry.name;
|
|
262
292
|
const opaqueFragmentSelector = parsed.fragment?.startsWith("akm-fragment-") === true;
|
|
263
293
|
// Friendly heading selectors retain the source-live exact response contract.
|
|
@@ -302,6 +332,8 @@ export async function showLocal(input) {
|
|
|
302
332
|
catch (error) {
|
|
303
333
|
throwIndexedPathNotFound(error, input.ref);
|
|
304
334
|
}
|
|
335
|
+
if (beliefState)
|
|
336
|
+
response.beliefState = beliefState;
|
|
305
337
|
response.type = indexedEntry.type;
|
|
306
338
|
response.name = indexedEntry.name;
|
|
307
339
|
const isPrimaryStash = source?.isDefault === true;
|
|
@@ -364,8 +396,22 @@ export async function showLocal(input) {
|
|
|
364
396
|
delete response.toolPolicy;
|
|
365
397
|
}
|
|
366
398
|
const editable = isEditable(assetPath, config, allSources);
|
|
399
|
+
// Evidence must describe the same revision as the rendered claim: ordinary
|
|
400
|
+
// Markdown show is source-live, while indexed fragment show is a snapshot.
|
|
401
|
+
let evidenceMetadata;
|
|
402
|
+
if (["memory", "lesson", "knowledge"].includes(response.type)) {
|
|
403
|
+
evidenceMetadata = indexedFragment ? indexedEntry.document : { name: presentedName, type: response.type };
|
|
404
|
+
if (!indexedFragment && evidenceMetadata) {
|
|
405
|
+
const frontmatter = parseFrontmatter(fileCtx.content()).data;
|
|
406
|
+
applyCuratedFrontmatter(evidenceMetadata, frontmatter);
|
|
407
|
+
applyProvenanceFrontmatter(evidenceMetadata, frontmatter);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
367
410
|
const fullResponse = {
|
|
368
411
|
...response,
|
|
412
|
+
...(evidenceMetadata?.evidence ? { evidence: evidenceMetadata.evidence } : {}),
|
|
413
|
+
...(evidenceMetadata?.evidenceSources ? { evidenceSources: evidenceMetadata.evidenceSources } : {}),
|
|
414
|
+
...(evidenceMetadata?.evidence && evidenceMetadata.provenance ? { provenance: evidenceMetadata.provenance } : {}),
|
|
369
415
|
ref: canonicalRef,
|
|
370
416
|
origin: source?.registryId ?? null,
|
|
371
417
|
editable,
|
|
@@ -192,6 +192,7 @@ const DOCUMENT_JSON_CARRIED_FIELDS = [
|
|
|
192
192
|
"wikiRole",
|
|
193
193
|
"sources",
|
|
194
194
|
"evidenceSources",
|
|
195
|
+
"evidence",
|
|
195
196
|
// #935: a workflow's step targets and a task's target, read as declared links.
|
|
196
197
|
"uses",
|
|
197
198
|
// D2 (#730): the OKF v0.2 provenance `promoteProposal` stamps onto AKM-native
|
|
@@ -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 { markdownLinkTargets } from "../../asset/markdown-link-targets.js";
|
|
76
77
|
import { toPosix } from "../../common.js";
|
|
77
78
|
import { hashContent, nonEmptyString } from "./shared.js";
|
|
78
79
|
/** A wiki is a single-component bundle; its one component is conventionally `main` (recognition golden). */
|
|
@@ -169,13 +170,10 @@ function resolveXref(xref, bundleId) {
|
|
|
169
170
|
*/
|
|
170
171
|
function resolveBodyLinks(body, fileRelPath) {
|
|
171
172
|
const dir = path.posix.dirname(toPosix(fileRelPath));
|
|
172
|
-
const linkRe = /\[[^\]]*\]\(([^)]+)\)/g;
|
|
173
173
|
const out = [];
|
|
174
174
|
const seen = new Set();
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
while ((match = linkRe.exec(body)) !== null) {
|
|
178
|
-
let target = match[1].trim();
|
|
175
|
+
for (const link of markdownLinkTargets(body, false)) {
|
|
176
|
+
let target = link.trim();
|
|
179
177
|
const wsIdx = target.search(/\s/);
|
|
180
178
|
if (wsIdx >= 0)
|
|
181
179
|
target = target.slice(0, wsIdx);
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import fs from "node:fs";
|
|
32
32
|
import path from "node:path";
|
|
33
33
|
import { parseFrontmatter } from "../../asset/frontmatter.js";
|
|
34
|
+
import { markdownLinkTargets } from "../../asset/markdown-link-targets.js";
|
|
34
35
|
import { isRecord, toPosix } from "../../common.js";
|
|
35
36
|
import { hashContent, nonEmptyString, RESERVED_FILES, readTags, runBaseValidateChecks } from "./shared.js";
|
|
36
37
|
/** 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. */
|
|
@@ -142,24 +143,10 @@ function isReservedFileName(name) {
|
|
|
142
143
|
*/
|
|
143
144
|
export function resolveOkfLinks(body, fileRelPath) {
|
|
144
145
|
const dir = path.posix.dirname(toPosix(fileRelPath));
|
|
145
|
-
const definitions = new Map();
|
|
146
|
-
for (const match of body.matchAll(/^\s*\[([^\]]+)\]:\s*(\S+)/gm)) {
|
|
147
|
-
definitions.set(match[1].trim().toLowerCase(), match[2]);
|
|
148
|
-
}
|
|
149
|
-
const candidates = [];
|
|
150
|
-
for (const match of body.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) {
|
|
151
|
-
candidates.push({ index: match.index, target: match[1] });
|
|
152
|
-
}
|
|
153
|
-
for (const match of body.matchAll(/(?<!!)\[[^\]]*\]\[([^\]]+)\]/g)) {
|
|
154
|
-
const target = definitions.get(match[1].trim().toLowerCase());
|
|
155
|
-
if (target)
|
|
156
|
-
candidates.push({ index: match.index, target });
|
|
157
|
-
}
|
|
158
|
-
candidates.sort((a, b) => a.index - b.index);
|
|
159
146
|
const out = [];
|
|
160
147
|
const seen = new Set();
|
|
161
|
-
for (const
|
|
162
|
-
let target =
|
|
148
|
+
for (const link of markdownLinkTargets(body, true)) {
|
|
149
|
+
let target = link.trim();
|
|
163
150
|
// Drop an optional markdown link title: `[x](/a.md "Title")`.
|
|
164
151
|
const wsIdx = target.search(/\s/);
|
|
165
152
|
if (wsIdx >= 0)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { Marked } from "marked";
|
|
5
|
+
// A private instance keeps unrelated consumers' global Marked options out of indexing.
|
|
6
|
+
const markdown = new Marked({ gfm: true });
|
|
7
|
+
/**
|
|
8
|
+
* Actual document links, in authored order. The tokenizer owns code regions,
|
|
9
|
+
* escapes and nested containers; the adapter still owns its supported syntax
|
|
10
|
+
* and path resolution. In particular, shortcut/collapsed references and wiki
|
|
11
|
+
* links are not new indexed syntax. Inline images retain their existing treatment.
|
|
12
|
+
*/
|
|
13
|
+
export function markdownLinkTargets(body, references) {
|
|
14
|
+
const tokens = markdown.lexer(body);
|
|
15
|
+
const definitions = new Map();
|
|
16
|
+
if (references) {
|
|
17
|
+
markdown.walkTokens(tokens, (token) => {
|
|
18
|
+
if (token.type !== "def")
|
|
19
|
+
return;
|
|
20
|
+
const definition = /^\s*\[([^\]]+)\]:\s*(\S+)/.exec(token.raw);
|
|
21
|
+
if (definition)
|
|
22
|
+
definitions.set(definition[1].trim().toLowerCase(), definition[2]);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const targets = [];
|
|
26
|
+
markdown.walkTokens(tokens, (token) => {
|
|
27
|
+
if (token.type !== "link" && token.type !== "image")
|
|
28
|
+
return;
|
|
29
|
+
const inline = /^!?\[[\s\S]*\]\(/.test(token.raw);
|
|
30
|
+
if (inline) {
|
|
31
|
+
// Angle-wrapped destinations were outside the adapters' existing subset.
|
|
32
|
+
const angled = /\]\(\s*<[^>]*>(?:\s+["'(][\s\S]*["')])?\s*\)$/.test(token.raw);
|
|
33
|
+
if (!angled)
|
|
34
|
+
targets.push(token.href);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const reference = references && token.type === "link" ? /^\[[\s\S]*\]\[([^\]]+)\]$/.exec(token.raw) : null;
|
|
38
|
+
const target = reference ? definitions.get(reference[1].trim().toLowerCase()) : undefined;
|
|
39
|
+
if (target)
|
|
40
|
+
targets.push(target);
|
|
41
|
+
});
|
|
42
|
+
return targets;
|
|
43
|
+
}
|
package/dist/core/common.js
CHANGED
|
@@ -165,8 +165,15 @@ export function writeFileAtomic(target, content, mode) {
|
|
|
165
165
|
try {
|
|
166
166
|
fd = fs.openSync(tmp, "wx", fileMode);
|
|
167
167
|
tempOwned = true;
|
|
168
|
-
if (process.platform !== "win32")
|
|
169
|
-
|
|
168
|
+
if (process.platform !== "win32") {
|
|
169
|
+
try {
|
|
170
|
+
fs.fchmodSync(fd, fileMode);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (!isChmodUnsupported(error))
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
170
177
|
let offset = 0;
|
|
171
178
|
while (offset < data.byteLength) {
|
|
172
179
|
const written = fs.writeSync(fd, data, offset, data.byteLength - offset);
|
|
@@ -361,6 +368,14 @@ export function hasErrnoCode(error, code) {
|
|
|
361
368
|
return false;
|
|
362
369
|
return error.code === code;
|
|
363
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* True when a chmod failed because the filesystem has no settable POSIX modes
|
|
373
|
+
* (CIFS/SMB such as an Azure Files share, some FUSE mounts). The mount options
|
|
374
|
+
* decide the mode there, so callers keep going with whatever mode the file got.
|
|
375
|
+
*/
|
|
376
|
+
export function isChmodUnsupported(error) {
|
|
377
|
+
return hasErrnoCode(error, "EPERM") || hasErrnoCode(error, "ENOTSUP") || hasErrnoCode(error, "EINVAL");
|
|
378
|
+
}
|
|
364
379
|
/**
|
|
365
380
|
* True when `value` is a RELATIVE path that cannot leave its base directory:
|
|
366
381
|
* no absolute form (POSIX `/`, Windows `\` or a `C:` drive prefix), no `~`
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import fs from "node:fs";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { sleepSync } from "../../runtime.js";
|
|
18
|
-
import { readTextFile, stripJsonComments, writeFileAtomic } from "../common.js";
|
|
18
|
+
import { isChmodUnsupported, readTextFile, stripJsonComments, writeFileAtomic } from "../common.js";
|
|
19
19
|
import { ConfigError } from "../errors.js";
|
|
20
20
|
import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync } from "../file-lock.js";
|
|
21
21
|
import { getCacheDir, getConfigDir } from "../paths.js";
|
|
@@ -88,7 +88,7 @@ export function backupExistingConfig(configPath, now = new Date()) {
|
|
|
88
88
|
// env.ts/secret.ts convention — so no other local user can traverse in during
|
|
89
89
|
// the copy→chmod window. chmod again to tighten a dir from an older version.
|
|
90
90
|
fs.mkdirSync(backupDir, { recursive: true, mode: 0o700 });
|
|
91
|
-
|
|
91
|
+
chmodIfSupported(backupDir, 0o700);
|
|
92
92
|
const timestamp = now.toISOString().replace(/[.:]/g, "-");
|
|
93
93
|
let sequence = 0;
|
|
94
94
|
let timestamped;
|
|
@@ -109,11 +109,21 @@ export function backupExistingConfig(configPath, now = new Date()) {
|
|
|
109
109
|
// 08-F4: a config backup carries the same sensitive fields as the live config
|
|
110
110
|
// (endpoints, tokens). `copyFileSync` inherits the source's (often 0644) mode,
|
|
111
111
|
// so tighten the backups to owner-only — mirrors the env-cli 0600 write floor.
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
chmodIfSupported(timestamped, 0o600);
|
|
113
|
+
chmodIfSupported(latest, 0o600);
|
|
114
114
|
pruneOldBackups(backupDir);
|
|
115
115
|
return { timestamped, latest };
|
|
116
116
|
}
|
|
117
|
+
/** chmod, skipped on a filesystem without POSIX modes (CIFS), where the mount sets them. */
|
|
118
|
+
function chmodIfSupported(target, mode) {
|
|
119
|
+
try {
|
|
120
|
+
fs.chmodSync(target, mode);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (!isChmodUnsupported(error))
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
117
127
|
function pruneOldBackups(backupDir) {
|
|
118
128
|
pruneToNewest(backupDir, MAX_CONFIG_BACKUPS, (entry) => entry.isFile() && isTimestampedConfigBackup(entry.name));
|
|
119
129
|
}
|
|
@@ -165,6 +165,8 @@ export function validateStashEntry(entry) {
|
|
|
165
165
|
const evidenceSources = normalizeNonEmptyStringList(e.evidenceSources);
|
|
166
166
|
if (evidenceSources)
|
|
167
167
|
result.evidenceSources = evidenceSources;
|
|
168
|
+
if (typeof e.evidence === "string" && e.evidence.trim().length > 0)
|
|
169
|
+
result.evidence = e.evidence;
|
|
168
170
|
if (typeof e.derivedFrom === "string" && e.derivedFrom.trim().length > 0) {
|
|
169
171
|
result.derivedFrom = e.derivedFrom.trim();
|
|
170
172
|
}
|
|
@@ -242,6 +244,19 @@ export function applyScopeFrontmatter(entry, fmData) {
|
|
|
242
244
|
if (scope)
|
|
243
245
|
entry.scope = scope;
|
|
244
246
|
}
|
|
247
|
+
/** The same normalized tuple the index projects, including legacy nested scope. */
|
|
248
|
+
export function scopeFromFrontmatter(fm) {
|
|
249
|
+
const entry = { type: "memory", name: "scope" };
|
|
250
|
+
if (typeof fm.scope === "object" && fm.scope !== null && !Array.isArray(fm.scope)) {
|
|
251
|
+
entry.scope = normalizeScopeObject(fm.scope);
|
|
252
|
+
}
|
|
253
|
+
applyScopeFrontmatter(entry, fm);
|
|
254
|
+
return entry.scope ?? {};
|
|
255
|
+
}
|
|
256
|
+
/** Exact maintenance equality: absence is a value, never a wildcard. */
|
|
257
|
+
export function sameScope(a, b) {
|
|
258
|
+
return SCOPE_KEYS.every((key) => a[key] === b[key]);
|
|
259
|
+
}
|
|
245
260
|
function normalizeIntent(value) {
|
|
246
261
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
247
262
|
return undefined;
|
|
@@ -367,6 +382,8 @@ export function applyCuratedFrontmatter(entry, fmData) {
|
|
|
367
382
|
const evidenceSources = normalizeNonEmptyStringList(fmData.evidenceSources);
|
|
368
383
|
if (evidenceSources)
|
|
369
384
|
entry.evidenceSources = evidenceSources;
|
|
385
|
+
if (typeof fmData.evidence === "string" && fmData.evidence.trim().length > 0)
|
|
386
|
+
entry.evidence = fmData.evidence;
|
|
370
387
|
// Phase 5A / Advantage D5: capture parent ref for derived memories.
|
|
371
388
|
// Memory-inference writes `source: "memories/<parent>"` and `inferred: true`
|
|
372
389
|
// (and a derived child name suffix `.derived`). We mirror that source ref into
|
|
@@ -138,6 +138,7 @@ export function indexDocumentToStashEntry(doc) {
|
|
|
138
138
|
entry.wikiRole = dj.wikiRole;
|
|
139
139
|
assignStringList(entry, "sources", dj.sources);
|
|
140
140
|
assignStringList(entry, "evidenceSources", dj.evidenceSources);
|
|
141
|
+
assignString(entry, "evidence", dj.evidence);
|
|
141
142
|
assignStringList(entry, "uses", dj.uses);
|
|
142
143
|
// D2 (#730): OKF v0.2 provenance promoteProposal stamps onto AKM-native
|
|
143
144
|
// writes, carried via DOCUMENT_JSON_CARRIED_FIELDS (akm-adapter.ts) —
|