akm-cli 0.9.3 → 0.9.5
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 +233 -1
- package/README.md +1 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/cli.js +5 -5
- package/dist/commands/health/improve-metrics.js +17 -0
- package/dist/commands/health/windows.js +2 -2
- package/dist/commands/health.js +2 -2
- package/dist/commands/improve/anti-collapse.js +4 -91
- package/dist/commands/improve/preparation.js +8 -1
- package/dist/commands/lint/index.js +3 -7
- package/dist/commands/proposal/validators/proposal-validators.js +12 -0
- package/dist/commands/read/search.js +14 -24
- package/dist/commands/tasks/tasks-cli.js +81 -3
- package/dist/commands/tasks/tasks.js +117 -2
- package/dist/core/adapter/adapters/akm-adapter.js +23 -14
- package/dist/core/adapter/adapters/akm-lint.js +3 -2
- package/dist/core/adapter/adapters/akm-task-adapter.js +9 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +13 -11
- package/dist/core/adapter/recognize-match.js +1 -20
- package/dist/core/asset/asset-placement.js +21 -2
- package/dist/core/common.js +21 -1
- package/dist/core/config/config-version-shim.js +101 -0
- package/dist/core/config/config.js +6 -6
- package/dist/core/improve-result.js +35 -14
- package/dist/execution/guarded-source.js +0 -10
- package/dist/indexer/lookup/adapter-concept-owner.js +6 -89
- package/dist/indexer/passes/metadata.js +12 -4
- package/dist/indexer/scan/doc-to-entry.js +2 -0
- package/dist/indexer/search/db-search.js +6 -0
- package/dist/indexer/search/search-fields.js +16 -1
- package/dist/indexer/walk/matchers.js +0 -22
- package/dist/output/shapes/helpers.js +19 -1
- package/dist/output/shapes/passthrough.js +18 -5
- package/dist/output/text/command-format.js +4 -0
- package/dist/registry/pinned-request-helper.js +2 -2
- package/dist/registry/pinned-transport.js +6 -6
- package/dist/scripts/akm-migrate-node.js +12678 -12601
- package/dist/scripts/akm-migrate.js +12678 -12601
- package/dist/storage/repositories/proposals-repository.js +65 -7
- package/dist/storage/repositories/task-history-repository.js +22 -10
- package/dist/tasks/run/task-history.js +23 -3
- package/dist/tasks/scheduler-binding.js +15 -5
- package/dist/tasks/scheduler-sync-preview.js +45 -0
- package/dist/tasks/scheduler-sync.js +77 -41
- package/dist/tasks/source/bounded-document.js +1 -1
- package/dist/tasks/source/parse-task-source.js +77 -11
- package/dist/tasks/source/task-source-v3-frozen.js +428 -0
- package/dist/tasks/source/task-to-v3.js +512 -0
- package/dist/tasks/source/task-to-v4.js +457 -0
- package/docs/reference/cli.md +33 -6
- package/docs/reference/configuration.md +27 -6
- package/docs/reference/tasks.md +10 -0
- package/package.json +4 -4
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* The `configVersion` read shim (#863).
|
|
6
|
+
*
|
|
7
|
+
* `configVersion` used to be a hard `z.literal(CURRENT_CONFIG_VERSION)` gate:
|
|
8
|
+
* every value other than the exact current string threw
|
|
9
|
+
* `UNSUPPORTED_CONFIG_VERSION` for every command, since every akm invocation
|
|
10
|
+
* loads config first. That is the same shape of break #858/#859 (proposal
|
|
11
|
+
* rows), the `task_history` `metadataVersion` gate, and the task-source v2/v3
|
|
12
|
+
* gate each caused in 0.9.x — a version gate with no read shim — except
|
|
13
|
+
* `configVersion`'s blast radius is the whole CLI rather than one subsystem.
|
|
14
|
+
*
|
|
15
|
+
* This module mirrors the in-tree template for that fix,
|
|
16
|
+
* `src/tasks/source/parse-task-source.ts`'s v2/v3 -> v4 shim: a known-old
|
|
17
|
+
* version routes through a pure, in-memory upgrade function to the current
|
|
18
|
+
* shape, with a one-line stderr deprecation warning; the result is never
|
|
19
|
+
* written back to disk (the on-disk rewrite already happens for free — every
|
|
20
|
+
* `saveConfig`/`mutateConfig` write forces `configVersion` to
|
|
21
|
+
* {@link CURRENT_CONFIG_VERSION}, so the very next `akm config set` or any
|
|
22
|
+
* other mutating command silences the warning permanently). A version that is
|
|
23
|
+
* neither current nor a known old version — including anything NEWER than
|
|
24
|
+
* current — still fails closed with `UNSUPPORTED_CONFIG_VERSION`:
|
|
25
|
+
* forward-incompatibility is a real hazard (an older binary must not guess at
|
|
26
|
+
* a newer, unknown shape) and this shim does not soften that.
|
|
27
|
+
*
|
|
28
|
+
* IMPORTANT — as of this writing, `"0.9.0"` is the only `configVersion` akm
|
|
29
|
+
* has ever shipped; there is no real prior release to shim. `"0.0.1"` below
|
|
30
|
+
* is a SYNTHETIC placeholder entry that exists solely to stand up and
|
|
31
|
+
* exercise this mechanism — the known-versions list, the dispatch table, the
|
|
32
|
+
* warn-once-and-upgrade behavior, the fail-closed behavior for anything
|
|
33
|
+
* else — before a real bump ever needs it (see
|
|
34
|
+
* `tests/integration/config-version-shim.test.ts` and the
|
|
35
|
+
* `previous-release-corpus.test.ts` fixture). When the first genuine
|
|
36
|
+
* `configVersion` bump ships, add its real old shape as its own entry the
|
|
37
|
+
* same way and delete the synthetic `"0.0.1"` entry (and this paragraph) in
|
|
38
|
+
* the same change.
|
|
39
|
+
*/
|
|
40
|
+
import { ConfigError } from "../errors.js";
|
|
41
|
+
import { warn } from "../warn.js";
|
|
42
|
+
import { CURRENT_CONFIG_VERSION } from "./schema/primitives.js";
|
|
43
|
+
/**
|
|
44
|
+
* Every `configVersion` this binary can still READ, other than
|
|
45
|
+
* {@link CURRENT_CONFIG_VERSION} itself — each with an in-memory upgrade
|
|
46
|
+
* function in {@link CONFIG_VERSION_UPGRADES}. Anything not in this list (and
|
|
47
|
+
* not equal to current) fails closed.
|
|
48
|
+
*/
|
|
49
|
+
export const KNOWN_OLD_CONFIG_VERSIONS = ["0.0.1"];
|
|
50
|
+
function isKnownOldConfigVersion(value) {
|
|
51
|
+
return typeof value === "string" && KNOWN_OLD_CONFIG_VERSIONS.includes(value);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* SYNTHETIC 0.0.1 -> 0.9.0 upgrade (placeholder — see module doc). Per this
|
|
55
|
+
* placeholder, 0.0.1 kept the default LLM engine name at the config root as
|
|
56
|
+
* `defaultEngine`; 0.9.0 moved it under `defaults.llmEngine`. Pure function:
|
|
57
|
+
* takes the raw parsed JSON object, returns a new raw object with the 0.9.0
|
|
58
|
+
* shape. Never touches disk.
|
|
59
|
+
*/
|
|
60
|
+
function upgradeFrom080(raw) {
|
|
61
|
+
const { defaultEngine, defaults, ...rest } = raw;
|
|
62
|
+
if (typeof defaultEngine !== "string" || defaultEngine.length === 0) {
|
|
63
|
+
return { ...rest, ...(defaults !== undefined ? { defaults } : {}), configVersion: CURRENT_CONFIG_VERSION };
|
|
64
|
+
}
|
|
65
|
+
const existingDefaults = defaults !== null && typeof defaults === "object" ? defaults : {};
|
|
66
|
+
return {
|
|
67
|
+
...rest,
|
|
68
|
+
configVersion: CURRENT_CONFIG_VERSION,
|
|
69
|
+
// An explicit `defaults.llmEngine` already present in the raw 0.0.1
|
|
70
|
+
// document (should never happen for a real 0.0.1 file, but a malformed
|
|
71
|
+
// one is possible) wins over the root-level field being migrated in.
|
|
72
|
+
defaults: { llmEngine: defaultEngine, ...existingDefaults },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const CONFIG_VERSION_UPGRADES = {
|
|
76
|
+
"0.0.1": upgradeFrom080,
|
|
77
|
+
};
|
|
78
|
+
function unsupportedConfigVersionError(rawVersion, sourcePath) {
|
|
79
|
+
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
80
|
+
const supported = [CURRENT_CONFIG_VERSION, ...KNOWN_OLD_CONFIG_VERSIONS].map((v) => `"${v}"`).join(", ");
|
|
81
|
+
return new ConfigError(`Unsupported configVersion${where}: got ${JSON.stringify(rawVersion)}, expected one of ${supported}.`, "UNSUPPORTED_CONFIG_VERSION", "Recreate engines and improve.strategies manually for AKM 0.9.0; profile-based configuration is not translated automatically.");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Route a raw parsed config object through the version shim before schema
|
|
85
|
+
* validation. Returns a raw object whose `configVersion` is
|
|
86
|
+
* {@link CURRENT_CONFIG_VERSION} — either unchanged (already current),
|
|
87
|
+
* in-memory-upgraded (a known old version, with a one-line stderr warning),
|
|
88
|
+
* or this throws `UNSUPPORTED_CONFIG_VERSION` (unknown, newer, missing, or
|
|
89
|
+
* malformed).
|
|
90
|
+
*/
|
|
91
|
+
export function upgradeConfigVersion(raw, sourcePath) {
|
|
92
|
+
const version = raw.configVersion;
|
|
93
|
+
if (version === CURRENT_CONFIG_VERSION)
|
|
94
|
+
return raw;
|
|
95
|
+
if (isKnownOldConfigVersion(version)) {
|
|
96
|
+
const upgraded = CONFIG_VERSION_UPGRADES[version](raw);
|
|
97
|
+
warn(`Config${sourcePath ? ` at ${sourcePath}` : ""} uses configVersion "${version}" — auto-upgraded to ${CURRENT_CONFIG_VERSION} in memory; the next config write (e.g. \`akm config set\`) persists this and silences the warning.`);
|
|
98
|
+
return upgraded;
|
|
99
|
+
}
|
|
100
|
+
throw unsupportedConfigVersionError(version, sourcePath);
|
|
101
|
+
}
|
|
@@ -8,6 +8,7 @@ import { liftLegacyEngineExtraParams } from "../extra-params.js";
|
|
|
8
8
|
import { acquireConfigLock, backupExistingConfig, parseConfigText, readConfigText, withConfigLock, writeConfigAtomic, } from "./config-io.js";
|
|
9
9
|
import { AkmConfigSchema, CURRENT_CONFIG_VERSION } from "./config-schema.js";
|
|
10
10
|
import { bundlesToSourceEntries } from "./config-sources.js";
|
|
11
|
+
import { upgradeConfigVersion } from "./config-version-shim.js";
|
|
11
12
|
import { deepMergeConfig } from "./deep-merge.js";
|
|
12
13
|
export { stripJsonComments } from "./config-io.js";
|
|
13
14
|
import { getConfigPath } from "../paths.js";
|
|
@@ -138,14 +139,13 @@ export function acquireConfigReadFence() {
|
|
|
138
139
|
* Parse raw config text and validate via Zod.
|
|
139
140
|
* ({@link AkmConfigSchema}). Returns the merged-with-defaults AkmConfig.
|
|
140
141
|
*
|
|
141
|
-
* The schema accepts only the current config version
|
|
142
|
-
*
|
|
142
|
+
* The schema accepts only the current config version. A known older version
|
|
143
|
+
* is auto-upgraded in memory first (see `./config-version-shim`); anything
|
|
144
|
+
* else — including anything newer — is rejected before the canonical shape
|
|
145
|
+
* is validated.
|
|
143
146
|
*/
|
|
144
147
|
export function parseAndValidateConfigText(text, sourcePath) {
|
|
145
|
-
const parsedRaw = parseConfigText(text, sourcePath);
|
|
146
|
-
if (parsedRaw.configVersion !== CURRENT_CONFIG_VERSION) {
|
|
147
|
-
throw new ConfigError(`Unsupported configVersion${sourcePath ? ` at ${sourcePath}` : ""}: expected "${CURRENT_CONFIG_VERSION}".`, "UNSUPPORTED_CONFIG_VERSION", "Recreate engines and improve.strategies manually for AKM 0.9.0; profile-based configuration is not translated automatically.");
|
|
148
|
-
}
|
|
148
|
+
const parsedRaw = upgradeConfigVersion(parseConfigText(text, sourcePath), sourcePath);
|
|
149
149
|
// #852 (following #815): lift legacy `extraParams` keys — e.g.
|
|
150
150
|
// `reasoning_effort`, a documented 0.9.1 workaround — onto the first-class
|
|
151
151
|
// engine field they now shadow, before the protected-key check in
|
|
@@ -456,20 +456,24 @@ function validateCommon(value) {
|
|
|
456
456
|
}
|
|
457
457
|
}
|
|
458
458
|
}
|
|
459
|
-
/**
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
459
|
+
/**
|
|
460
|
+
* Per-`schemaVersion` decoders for the persisted `improve_runs.result_json`
|
|
461
|
+
* contract, keyed by the literal `schemaVersion` value each accepts.
|
|
462
|
+
*
|
|
463
|
+
* #866 item 5 / read-shim policy (same shape as the version router in
|
|
464
|
+
* src/tasks/source/parse-task-source.ts, which this mirrors): every
|
|
465
|
+
* `schemaVersion` this build understands gets its own entry here, so
|
|
466
|
+
* decoding never regresses to "reject every row" the day the version bumps
|
|
467
|
+
* — it only stops accepting the ONE version that changed shape. All 1,539
|
|
468
|
+
* live rows today are v2; when a v3 shape is introduced, add a `3` entry
|
|
469
|
+
* (and, if v2 rows must keep reading under the newer runtime, keep this `2`
|
|
470
|
+
* entry as-is — every caller already skip-and-counts a decode failure, so a
|
|
471
|
+
* genuinely unreadable old row degrades to "excluded from metrics", never a
|
|
472
|
+
* crash). Do not remove the `2` entry when adding `3` unless the schema
|
|
473
|
+
* bump is known to be a strict superset callers can validate with it.
|
|
474
|
+
*/
|
|
475
|
+
const SCHEMA_DECODERS = {
|
|
476
|
+
2: (parsed) => {
|
|
473
477
|
requireExactFields(parsed, V2_FIELDS);
|
|
474
478
|
validateCommon(parsed);
|
|
475
479
|
if (typeof parsed.strategy !== "string" || parsed.strategy.length === 0) {
|
|
@@ -482,6 +486,23 @@ export function decodeImproveResult(input) {
|
|
|
482
486
|
envelope: parsed,
|
|
483
487
|
strategy: parsed.strategy,
|
|
484
488
|
};
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
/** Decode the persisted public result contract. */
|
|
492
|
+
export function decodeImproveResult(input) {
|
|
493
|
+
let parsed = input;
|
|
494
|
+
if (typeof input === "string") {
|
|
495
|
+
try {
|
|
496
|
+
parsed = JSON.parse(input);
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
fail("not valid JSON");
|
|
500
|
+
}
|
|
485
501
|
}
|
|
502
|
+
if (!isRecord(parsed))
|
|
503
|
+
fail("root must be an object");
|
|
504
|
+
const decoder = typeof parsed.schemaVersion === "number" ? SCHEMA_DECODERS[parsed.schemaVersion] : undefined;
|
|
505
|
+
if (decoder)
|
|
506
|
+
return decoder(parsed);
|
|
486
507
|
fail(`unsupported schemaVersion: ${String(parsed.schemaVersion)}`);
|
|
487
508
|
}
|
|
@@ -7,7 +7,6 @@ import path from "node:path";
|
|
|
7
7
|
import { parseFrontmatter } from "../core/asset/frontmatter.js";
|
|
8
8
|
import { UsageError } from "../core/errors.js";
|
|
9
9
|
import { createExecutionSourceIdentity } from "./source.js";
|
|
10
|
-
export const DEFAULT_GUARDED_SOURCE_MAX_BYTES = 1024 * 1024;
|
|
11
10
|
function errorMessage(cause) {
|
|
12
11
|
return cause instanceof Error ? cause.message : String(cause);
|
|
13
12
|
}
|
|
@@ -71,10 +70,6 @@ function captureRecord(sourcePathInput, containmentRootInput, options = {}) {
|
|
|
71
70
|
const sourcePath = path.resolve(sourcePathInput);
|
|
72
71
|
const { containmentRoot, containmentRealPath, containmentStat } = requireContainmentRoot(containmentRootInput);
|
|
73
72
|
const lexicalRelative = containedRelative(containmentRoot, sourcePath, false);
|
|
74
|
-
const maxBytes = options.maxBytes ?? DEFAULT_GUARDED_SOURCE_MAX_BYTES;
|
|
75
|
-
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
|
|
76
|
-
throw new UsageError("Guarded source byte limit must be a non-negative safe integer.", "INVALID_FLAG_VALUE");
|
|
77
|
-
}
|
|
78
73
|
const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
79
74
|
let descriptor;
|
|
80
75
|
try {
|
|
@@ -83,9 +78,6 @@ function captureRecord(sourcePathInput, containmentRootInput, options = {}) {
|
|
|
83
78
|
if (!before.isFile()) {
|
|
84
79
|
throw new UsageError(`${sourcePath} is not a regular guarded source file.`, "INVALID_FLAG_VALUE");
|
|
85
80
|
}
|
|
86
|
-
if (before.size > BigInt(maxBytes)) {
|
|
87
|
-
throw new UsageError(`${sourcePath} exceeds the guarded source size limit (1 MiB; ${maxBytes} bytes).`, "INVALID_FLAG_VALUE");
|
|
88
|
-
}
|
|
89
81
|
const bytes = fs.readFileSync(descriptor);
|
|
90
82
|
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
91
83
|
if (!sameBigIntStat(before, after) || BigInt(bytes.byteLength) !== before.size) {
|
|
@@ -127,7 +119,6 @@ function captureRecord(sourcePathInput, containmentRootInput, options = {}) {
|
|
|
127
119
|
...(options.identity ? { identity: options.identity } : {}),
|
|
128
120
|
}),
|
|
129
121
|
stat: numberStat(before),
|
|
130
|
-
maxBytes,
|
|
131
122
|
};
|
|
132
123
|
}
|
|
133
124
|
catch (cause) {
|
|
@@ -402,7 +393,6 @@ export class GuardedExecutionSourceCollector {
|
|
|
402
393
|
try {
|
|
403
394
|
current = captureRecord(record.source.sourcePath, record.source.containmentRoot, {
|
|
404
395
|
authored: record.source.authored,
|
|
405
|
-
maxBytes: record.maxBytes,
|
|
406
396
|
...(record.source.identity ? { identity: record.source.identity } : {}),
|
|
407
397
|
}).source;
|
|
408
398
|
}
|
|
@@ -10,10 +10,6 @@ import { canonicalizeWorkflowName } from "../../core/recognition-util.js";
|
|
|
10
10
|
import { resolveUniqueWorkflowSource, workflowNameForConceptId, } from "../../workflows/source-files.js";
|
|
11
11
|
import { buildFileContext } from "../walk/file-context.js";
|
|
12
12
|
const CONTENT_READ_REQUIRED = Symbol("adapter ownership probe requires content");
|
|
13
|
-
const OWNER_SCAN_MAX_DIRECTORIES = 4_096;
|
|
14
|
-
const OWNER_SCAN_MAX_FILES = 16_384;
|
|
15
|
-
const OWNER_SCAN_SKIP_DIRECTORIES = new Set([".git", "node_modules", "bin", ".cache"]);
|
|
16
|
-
const OWNER_SCAN_SKIP_FILES = new Set([".stash.json", ".gitignore", ".gitattributes"]);
|
|
17
13
|
export class AdapterConceptOwnershipError extends UsageError {
|
|
18
14
|
}
|
|
19
15
|
export class AdapterConceptCollisionError extends AdapterConceptOwnershipError {
|
|
@@ -24,13 +20,6 @@ export class AdapterConceptCollisionError extends AdapterConceptOwnershipError {
|
|
|
24
20
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
25
21
|
}
|
|
26
22
|
}
|
|
27
|
-
export class AdapterConceptScanError extends AdapterConceptOwnershipError {
|
|
28
|
-
constructor(adapterId, sourcePath, detail) {
|
|
29
|
-
super(`Adapter "${adapterId}" could not safely enumerate ${sourcePath}: ${detail}.`, "INVALID_FLAG_VALUE");
|
|
30
|
-
this.name = "AdapterConceptScanError";
|
|
31
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
23
|
function normalizedConceptId(conceptId) {
|
|
35
24
|
const normalized = conceptId.replaceAll("\\", "/");
|
|
36
25
|
if (!normalized ||
|
|
@@ -128,79 +117,6 @@ function claimsWithoutContent(adapter, component, owner) {
|
|
|
128
117
|
throw error;
|
|
129
118
|
}
|
|
130
119
|
}
|
|
131
|
-
function adapterPathContext(root, authoredPath) {
|
|
132
|
-
const file = buildFileContext(root, authoredPath);
|
|
133
|
-
return {
|
|
134
|
-
absPath: file.absPath,
|
|
135
|
-
relPath: file.relPath,
|
|
136
|
-
ext: file.ext,
|
|
137
|
-
fileName: file.fileName,
|
|
138
|
-
parentDir: file.parentDir,
|
|
139
|
-
parentDirAbs: file.parentDirAbs,
|
|
140
|
-
ancestorDirs: file.ancestorDirs,
|
|
141
|
-
stashRoot: file.stashRoot,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
function scanRegularAuthoredPaths(sourcePath, realRoot, adapterId) {
|
|
145
|
-
const paths = [];
|
|
146
|
-
const stack = [path.resolve(sourcePath)];
|
|
147
|
-
let directories = 0;
|
|
148
|
-
let files = 0;
|
|
149
|
-
while (stack.length > 0) {
|
|
150
|
-
const current = stack.pop();
|
|
151
|
-
if (!current)
|
|
152
|
-
continue;
|
|
153
|
-
directories++;
|
|
154
|
-
if (directories > OWNER_SCAN_MAX_DIRECTORIES) {
|
|
155
|
-
throw new AdapterConceptScanError(adapterId, sourcePath, `directory limit ${OWNER_SCAN_MAX_DIRECTORIES} exceeded`);
|
|
156
|
-
}
|
|
157
|
-
let entries;
|
|
158
|
-
try {
|
|
159
|
-
entries = fs
|
|
160
|
-
.readdirSync(current, { withFileTypes: true })
|
|
161
|
-
.sort((left, right) => comparePaths(left.name, right.name));
|
|
162
|
-
}
|
|
163
|
-
catch (error) {
|
|
164
|
-
throw new AdapterConceptScanError(adapterId, sourcePath, `cannot read ${current}: ${String(error)}`);
|
|
165
|
-
}
|
|
166
|
-
for (let index = entries.length - 1; index >= 0; index--) {
|
|
167
|
-
const entry = entries[index];
|
|
168
|
-
if (!entry)
|
|
169
|
-
continue;
|
|
170
|
-
const authoredPath = path.join(current, entry.name);
|
|
171
|
-
if (entry.isSymbolicLink())
|
|
172
|
-
continue;
|
|
173
|
-
if (entry.isDirectory()) {
|
|
174
|
-
if (OWNER_SCAN_SKIP_DIRECTORIES.has(entry.name) || entry.name.startsWith("."))
|
|
175
|
-
continue;
|
|
176
|
-
stack.push(authoredPath);
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
if (!entry.isFile() || OWNER_SCAN_SKIP_FILES.has(entry.name))
|
|
180
|
-
continue;
|
|
181
|
-
files++;
|
|
182
|
-
if (files > OWNER_SCAN_MAX_FILES) {
|
|
183
|
-
throw new AdapterConceptScanError(adapterId, sourcePath, `file limit ${OWNER_SCAN_MAX_FILES} exceeded`);
|
|
184
|
-
}
|
|
185
|
-
let realPath;
|
|
186
|
-
try {
|
|
187
|
-
realPath = fs.realpathSync(authoredPath);
|
|
188
|
-
}
|
|
189
|
-
catch {
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (isWithin(realPath, realRoot))
|
|
193
|
-
paths.push(authoredPath);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return paths.sort(comparePaths);
|
|
197
|
-
}
|
|
198
|
-
function scannedReadCandidates(adapter, component, sourcePath, realRoot, conceptId) {
|
|
199
|
-
const recognizePathCandidates = adapter.recognizePathCandidates;
|
|
200
|
-
if (!recognizePathCandidates)
|
|
201
|
-
return [];
|
|
202
|
-
return scanRegularAuthoredPaths(sourcePath, realRoot, adapter.id).flatMap((authoredPath) => recognizePathCandidates(component, adapterPathContext(sourcePath, authoredPath)).flatMap((candidateConceptId) => candidateConceptId === conceptId ? [{ path: authoredPath, conceptId: candidateConceptId }] : []));
|
|
203
|
-
}
|
|
204
120
|
/**
|
|
205
121
|
* Resolve one adapter/component's exact physical concept owner without reading
|
|
206
122
|
* authored bytes. Native workflow arbitration is reused verbatim; every other
|
|
@@ -240,11 +156,12 @@ export function resolveAdapterConceptOwner(sourcePath, adapterId, conceptId) {
|
|
|
240
156
|
ownersByIdentity.set(`${path.resolve(workflowOwner.path)}\0${resolutionConceptId}`, workflowOwner);
|
|
241
157
|
}
|
|
242
158
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
159
|
+
// Closed-form candidate set (#857): `readCandidates` inverts the conceptId
|
|
160
|
+
// directly into every physical spelling that could own it (no bundle walk
|
|
161
|
+
// — see each adapter's own doc comment for its candidate classes), and
|
|
162
|
+
// `candidateSpellings` adds case-only siblings via one `readdirSync` of
|
|
163
|
+
// each candidate's own parent directory.
|
|
164
|
+
const spellings = (adapter.readCandidates?.(component, resolutionConceptId) ?? []).flatMap((candidate) => candidateSpellings(candidate, sourcePath, realRoot));
|
|
248
165
|
const inspected = [
|
|
249
166
|
...new Map(spellings.map((candidate) => [`${path.resolve(candidate.path)}\0${candidate.conceptId}`, candidate])).values(),
|
|
250
167
|
]
|
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
7
7
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
8
8
|
import { asNonEmptyString } from "../../core/common.js";
|
|
9
|
-
import { isVerbose, warn } from "../../core/warn.js";
|
|
9
|
+
import { isVerbose, warn, warnVerbose } from "../../core/warn.js";
|
|
10
10
|
export const SCOPE_KEYS = ["user", "agent", "run", "channel"];
|
|
11
11
|
// ── Quality semantics (v1 spec §4.2) ────────────────────────────────────────
|
|
12
12
|
/**
|
|
@@ -1119,7 +1119,7 @@ function stripMarkdownLinkDestinations(text, nesting = 0) {
|
|
|
1119
1119
|
* secret/env/session bytes; that policy is enforced at the adapter metadata
|
|
1120
1120
|
* boundary below.
|
|
1121
1121
|
*/
|
|
1122
|
-
export function projectMarkdownContent(body) {
|
|
1122
|
+
export function projectMarkdownContent(body, truncationInfo) {
|
|
1123
1123
|
const lines = body.split(/\r?\n/);
|
|
1124
1124
|
const innerBlock = findInnerFrontmatterBlock(lines);
|
|
1125
1125
|
const start = innerBlock && isFrontmatterShaped(lines, innerBlock) ? innerBlock.close + 1 : 0;
|
|
@@ -1168,6 +1168,8 @@ export function projectMarkdownContent(body) {
|
|
|
1168
1168
|
const text = projected.join(" ").replace(/\s+/g, " ").trim();
|
|
1169
1169
|
if (!text)
|
|
1170
1170
|
return undefined;
|
|
1171
|
+
if (truncationInfo)
|
|
1172
|
+
truncationInfo.truncated = text.length > MARKDOWN_CONTENT_MAX_CHARS;
|
|
1171
1173
|
return truncateUnicodeSafe(text, MARKDOWN_CONTENT_MAX_CHARS);
|
|
1172
1174
|
}
|
|
1173
1175
|
// ── Metadata Generation ─────────────────────────────────────────────────────
|
|
@@ -1212,9 +1214,15 @@ export function applyPreContributorFields(entry, file, ctx, pkgMeta) {
|
|
|
1212
1214
|
// Native Markdown has one bounded low-weight body projection. Sensitive
|
|
1213
1215
|
// types and raw session/checkpoint material never cross this boundary.
|
|
1214
1216
|
if (entry.type !== "env" && entry.type !== "session" && !hasSessionMemoryMarker(parsed.data, parsed.content)) {
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1217
|
+
const truncationInfo = { truncated: false };
|
|
1218
|
+
const contentProjection = projectMarkdownContent(parsed.content, truncationInfo);
|
|
1219
|
+
if (contentProjection) {
|
|
1217
1220
|
entry.content = contentProjection;
|
|
1221
|
+
if (truncationInfo.truncated) {
|
|
1222
|
+
entry.contentTruncated = true;
|
|
1223
|
+
warnVerbose(`${file}: indexed content truncated to ${MARKDOWN_CONTENT_MAX_CHARS} chars`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1218
1226
|
}
|
|
1219
1227
|
// Extract parameters from template placeholders ($1, $ARGUMENTS, {{named}})
|
|
1220
1228
|
if (entry.type === "command") {
|
|
@@ -58,6 +58,8 @@ export function indexDocumentToStashEntry(doc) {
|
|
|
58
58
|
entry.tags = doc.tags;
|
|
59
59
|
if (doc.content !== undefined)
|
|
60
60
|
entry.content = doc.content;
|
|
61
|
+
if (doc.contentTruncated !== undefined)
|
|
62
|
+
entry.contentTruncated = doc.contentTruncated;
|
|
61
63
|
if (doc.ownsPresentation !== undefined)
|
|
62
64
|
entry.ownsPresentation = doc.ownsPresentation;
|
|
63
65
|
if (doc.updated !== undefined)
|
|
@@ -785,6 +785,10 @@ export async function buildDbHit(input) {
|
|
|
785
785
|
...(input.entry.beliefState ? { beliefState: input.entry.beliefState } : {}),
|
|
786
786
|
...(input.entry.currentBeliefRefs ? { currentBeliefRefs: input.entry.currentBeliefRefs } : {}),
|
|
787
787
|
...(graphHit ? { graph: { entities: graphHit.entities, relations: graphHit.relations } } : {}),
|
|
788
|
+
// Which stage of the progressive AND->OR lexical ladder produced this
|
|
789
|
+
// hit. Omitted when the hit has no FTS component (pure-semantic hybrid
|
|
790
|
+
// contribution).
|
|
791
|
+
...(input.lexicalMatch ? { matchStage: input.lexicalMatch } : {}),
|
|
788
792
|
};
|
|
789
793
|
attachDbHitAttribution(hit, input);
|
|
790
794
|
if (input.entry.derivedFrom) {
|
|
@@ -825,6 +829,8 @@ rankingMode, qualityBoost, confidenceBoost, utilityBoosted, graphBoost, lexicalM
|
|
|
825
829
|
];
|
|
826
830
|
if (lexicalMatch === "relaxed")
|
|
827
831
|
reasons.push("lexical recovery after strict query returned no hits");
|
|
832
|
+
if (lexicalMatch === "prefix")
|
|
833
|
+
reasons.push("prefix match after strict query returned no hits");
|
|
828
834
|
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
829
835
|
const queryLower = query.toLowerCase().trim();
|
|
830
836
|
const name = entry.name.toLowerCase();
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Per-field search text extraction for FTS5 indexing.
|
|
6
|
+
*
|
|
7
|
+
* Extracted from indexer.ts to break the circular dependency:
|
|
8
|
+
* db.ts -> indexer.ts -> db.ts
|
|
9
|
+
*
|
|
10
|
+
* This module imports only from metadata.ts (for the IndexDocument type),
|
|
11
|
+
* so it can be safely imported by both db.ts and indexer.ts.
|
|
12
|
+
*/
|
|
13
|
+
import { warnVerbose } from "../../core/warn.js";
|
|
4
14
|
/** Structured metadata plus bounded body text supplied to embedding providers. */
|
|
5
15
|
export const SEARCH_TEXT_MAX_CHARS = 8_192;
|
|
6
16
|
/**
|
|
@@ -80,12 +90,17 @@ export function buildSearchText(entry) {
|
|
|
80
90
|
const structured = [fields.name, fields.description, fields.tags, fields.hints]
|
|
81
91
|
.filter((field) => field.length > 0)
|
|
82
92
|
.join(" ");
|
|
83
|
-
if (structured.length >= SEARCH_TEXT_MAX_CHARS)
|
|
93
|
+
if (structured.length >= SEARCH_TEXT_MAX_CHARS) {
|
|
94
|
+
warnVerbose(`${entry.ref ?? entry.name}: search text truncated to ${SEARCH_TEXT_MAX_CHARS} chars (content dropped entirely)`);
|
|
84
95
|
return truncateUnicodeSafe(structured, SEARCH_TEXT_MAX_CHARS);
|
|
96
|
+
}
|
|
85
97
|
if (!fields.content)
|
|
86
98
|
return structured;
|
|
87
99
|
const separator = structured ? " " : "";
|
|
88
100
|
const remaining = SEARCH_TEXT_MAX_CHARS - structured.length - separator.length;
|
|
101
|
+
if (fields.content.length > remaining) {
|
|
102
|
+
warnVerbose(`${entry.ref ?? entry.name}: search text truncated to ${SEARCH_TEXT_MAX_CHARS} chars`);
|
|
103
|
+
}
|
|
89
104
|
return `${structured}${separator}${truncateUnicodeSafe(fields.content, remaining)}`;
|
|
90
105
|
}
|
|
91
106
|
function truncateUnicodeSafe(text, maxChars) {
|
|
@@ -270,28 +270,6 @@ function matchResultForFact(fact) {
|
|
|
270
270
|
...(fact.meta ? { meta: fact.meta } : {}),
|
|
271
271
|
};
|
|
272
272
|
}
|
|
273
|
-
/**
|
|
274
|
-
* Every smart-Markdown result possible from path fields alone. The actual
|
|
275
|
-
* classifier above returns only facts from this shared table, so owner
|
|
276
|
-
* discovery can conservatively model a byte request without reading bytes.
|
|
277
|
-
*/
|
|
278
|
-
export function smartMdPathCandidates(ctx) {
|
|
279
|
-
if (ctx.ext !== ".md" || ctx.ancestorDirs.includes("secrets"))
|
|
280
|
-
return [];
|
|
281
|
-
const facts = isTypedDirDocFile(ctx.fileName)
|
|
282
|
-
? [SMART_MD_FACTS.knowledge]
|
|
283
|
-
: [
|
|
284
|
-
SMART_MD_FACTS.workflow,
|
|
285
|
-
SMART_MD_FACTS.toolsAgent,
|
|
286
|
-
SMART_MD_FACTS.command,
|
|
287
|
-
SMART_MD_FACTS.modelAgent,
|
|
288
|
-
SMART_MD_FACTS.knowledge,
|
|
289
|
-
];
|
|
290
|
-
return facts.flatMap((fact) => {
|
|
291
|
-
const result = matchResultForFact(fact);
|
|
292
|
-
return result ? [result] : [];
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
273
|
// ---------------------------------------------------------------------------
|
|
296
274
|
// Public matchers (API unchanged)
|
|
297
275
|
// ---------------------------------------------------------------------------
|
|
@@ -275,7 +275,20 @@ export function shapeSearchHit(hit, detail) {
|
|
|
275
275
|
// visible without forcing callers up to `--detail full`. Optional
|
|
276
276
|
// `quality` (v1 spec §4.2) is also surfaced when present so callers
|
|
277
277
|
// can see why a `proposed` entry showed up under `--include-proposed`.
|
|
278
|
-
|
|
278
|
+
// `matchStage` (issue #856) surfaces which stage of the progressive
|
|
279
|
+
// AND->OR lexical ladder produced the hit; cheap compact enum, worth
|
|
280
|
+
// showing without requiring `--detail full`.
|
|
281
|
+
const shaped = capDescription(pickFields(hit, [
|
|
282
|
+
"type",
|
|
283
|
+
"name",
|
|
284
|
+
"description",
|
|
285
|
+
"action",
|
|
286
|
+
"score",
|
|
287
|
+
"estimatedTokens",
|
|
288
|
+
"warnings",
|
|
289
|
+
"quality",
|
|
290
|
+
"matchStage",
|
|
291
|
+
]), NORMAL_DESCRIPTION_LIMIT);
|
|
279
292
|
if (Array.isArray(hit.keys) && hit.keys.length > 0)
|
|
280
293
|
shaped.keys = hit.keys;
|
|
281
294
|
return shaped;
|
|
@@ -296,6 +309,11 @@ export function shapeSearchHitForAgent(hit) {
|
|
|
296
309
|
"score",
|
|
297
310
|
"estimatedTokens",
|
|
298
311
|
"keys",
|
|
312
|
+
// Issue #856: which stage of the progressive AND->OR lexical ladder
|
|
313
|
+
// produced this hit. Agents need this to gauge how much to trust a
|
|
314
|
+
// hit (a strict-AND match is stronger signal than an OR-fallback
|
|
315
|
+
// recovery match) without going to `--detail full`.
|
|
316
|
+
"matchStage",
|
|
299
317
|
]);
|
|
300
318
|
if (picked.editable !== false)
|
|
301
319
|
delete picked.editHint;
|
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
// #484: stamp schemaVersion + shape discriminator on passthrough envelopes so
|
|
5
5
|
// third-party consumers can pin a schema version and dispatch on shape uniformly.
|
|
6
6
|
// Idempotent — never overwrites an existing schemaVersion or shape field.
|
|
7
|
+
//
|
|
8
|
+
// Builds a shallow copy rather than mutating `result` in place: several
|
|
9
|
+
// command results (e.g. `akm task sync --dry-run`'s `SchedulerPlanPreview`,
|
|
10
|
+
// see src/tasks/scheduler-sync-preview.ts) are deliberately `Object.freeze`d
|
|
11
|
+
// by their producer as an immutability guarantee, and an in-place `obj.shape
|
|
12
|
+
// = …` assignment throws ("Attempting to define property on object that is
|
|
13
|
+
// not extensible") the moment it hits one. Copying tolerates both frozen and
|
|
14
|
+
// mutable inputs uniformly, and `output()` never uses the result's identity
|
|
15
|
+
// past this call, so a copy is safe here.
|
|
7
16
|
function makeStampHandler(command) {
|
|
8
17
|
return (result) => {
|
|
9
18
|
if (result === null || result === undefined)
|
|
@@ -11,11 +20,13 @@ function makeStampHandler(command) {
|
|
|
11
20
|
if (typeof result !== "object" || Array.isArray(result))
|
|
12
21
|
return result;
|
|
13
22
|
const obj = result;
|
|
14
|
-
if (obj.shape
|
|
15
|
-
obj
|
|
16
|
-
|
|
17
|
-
obj
|
|
18
|
-
|
|
23
|
+
if (obj.shape !== undefined && obj.schemaVersion !== undefined)
|
|
24
|
+
return obj;
|
|
25
|
+
return {
|
|
26
|
+
...obj,
|
|
27
|
+
shape: obj.shape ?? command,
|
|
28
|
+
schemaVersion: obj.schemaVersion ?? 1,
|
|
29
|
+
};
|
|
19
30
|
};
|
|
20
31
|
}
|
|
21
32
|
const PASSTHROUGH_COMMANDS = [
|
|
@@ -55,8 +66,10 @@ const PASSTHROUGH_COMMANDS = [
|
|
|
55
66
|
"task-doctor",
|
|
56
67
|
"task-explain",
|
|
57
68
|
"task-history",
|
|
69
|
+
"task-prune",
|
|
58
70
|
"task-run",
|
|
59
71
|
"task-sync",
|
|
72
|
+
"task-sync-dry-run",
|
|
60
73
|
"update",
|
|
61
74
|
"upgrade",
|
|
62
75
|
"workflow-abandon",
|
|
@@ -270,6 +270,10 @@ export function formatSearchPlain(r, detail) {
|
|
|
270
270
|
// Optional v1 spec §4.2 quality marker (e.g. "curated" / "proposed").
|
|
271
271
|
if (typeof hit.quality === "string" && hit.quality)
|
|
272
272
|
lines.push(` quality: ${hit.quality}`);
|
|
273
|
+
// Issue #856: which stage of the progressive AND->OR lexical ladder
|
|
274
|
+
// produced this hit ("exact" | "prefix" | "relaxed").
|
|
275
|
+
if (typeof hit.matchStage === "string" && hit.matchStage)
|
|
276
|
+
lines.push(` matchStage: ${hit.matchStage}`);
|
|
273
277
|
// Surface optional hit-level warnings (v1 spec §4.2).
|
|
274
278
|
if (Array.isArray(hit.warnings) && hit.warnings.length > 0) {
|
|
275
279
|
lines.push(` warnings: ${hit.warnings.join("; ")}`);
|
|
@@ -117,8 +117,8 @@ export async function nodePinnedRequestHelperMain(readRequest, http, https, isIP
|
|
|
117
117
|
};
|
|
118
118
|
try {
|
|
119
119
|
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
120
|
-
if (!Number.isInteger(nodeMajor) || nodeMajor <
|
|
121
|
-
throw new Error(`The pinned registry helper requires Node.js >=
|
|
120
|
+
if (!Number.isInteger(nodeMajor) || nodeMajor < 22) {
|
|
121
|
+
throw new Error(`The pinned registry helper requires Node.js >= 22; found ${process.versions.node}`);
|
|
122
122
|
}
|
|
123
123
|
const { addressFamily, bodyPresent, headers, initialBody, iterator, method, pinnedAddress, timeoutMs, url } = await readRequest(isIP);
|
|
124
124
|
const hostname = url.hostname.startsWith("[") && url.hostname.endsWith("]") ? url.hostname.slice(1, -1) : url.hostname;
|
|
@@ -233,7 +233,7 @@ async function requestWithNodeHelper(url, address, prepared, init, timeoutMs, ex
|
|
|
233
233
|
}
|
|
234
234
|
function resolveNodeExecutable(override) {
|
|
235
235
|
if (override === null) {
|
|
236
|
-
throw new RegistryPinnedTransportError("Pinned registry networking under Bun requires Node.js >=
|
|
236
|
+
throw new RegistryPinnedTransportError("Pinned registry networking under Bun requires Node.js >= 22 on PATH; no Node executable is available");
|
|
237
237
|
}
|
|
238
238
|
const candidates = override === undefined
|
|
239
239
|
? (process.env.PATH ?? "")
|
|
@@ -254,10 +254,10 @@ function resolveNodeExecutable(override) {
|
|
|
254
254
|
return resolved;
|
|
255
255
|
}
|
|
256
256
|
catch {
|
|
257
|
-
// Try the next PATH entry. The helper itself enforces Node >=
|
|
257
|
+
// Try the next PATH entry. The helper itself enforces Node >= 22.
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
|
-
throw new RegistryPinnedTransportError("Pinned registry networking under Bun requires Node.js >=
|
|
260
|
+
throw new RegistryPinnedTransportError("Pinned registry networking under Bun requires Node.js >= 22 on PATH. Install Node.js or run akm with Node.");
|
|
261
261
|
}
|
|
262
262
|
function isSupportedNodeExecutable(executable) {
|
|
263
263
|
const result = spawnSync(executable, ["--version"], {
|
|
@@ -272,14 +272,14 @@ function isSupportedNodeExecutable(executable) {
|
|
|
272
272
|
return false;
|
|
273
273
|
const match = /^v(\d+)\./.exec(result.stdout.trim());
|
|
274
274
|
const major = match?.[1];
|
|
275
|
-
return major !== undefined && Number.parseInt(major, 10) >=
|
|
275
|
+
return major !== undefined && Number.parseInt(major, 10) >= 22;
|
|
276
276
|
}
|
|
277
277
|
function assertNodeRuntimeVersion() {
|
|
278
278
|
if (process.versions.bun)
|
|
279
279
|
return;
|
|
280
280
|
const major = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
281
|
-
if (!Number.isInteger(major) || major <
|
|
282
|
-
throw new RegistryPinnedTransportError(`Pinned registry networking requires Node.js >=
|
|
281
|
+
if (!Number.isInteger(major) || major < 22) {
|
|
282
|
+
throw new RegistryPinnedTransportError(`Pinned registry networking requires Node.js >= 22; found ${process.versions.node}`);
|
|
283
283
|
}
|
|
284
284
|
}
|
|
285
285
|
function createHelperArtifacts(ca) {
|