akm-cli 0.9.2 → 0.9.4
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 +110 -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/preparation.js +8 -1
- package/dist/commands/lint/index.js +3 -7
- package/dist/commands/tasks/tasks-cli.js +28 -3
- package/dist/commands/tasks/tasks.js +29 -1
- package/dist/core/adapter/adapters/akm-adapter.js +21 -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.js +19 -3
- package/dist/core/extra-params.js +115 -1
- package/dist/indexer/lookup/adapter-concept-owner.js +6 -89
- package/dist/indexer/search/db-search.js +6 -0
- package/dist/indexer/walk/matchers.js +0 -22
- package/dist/output/shapes/helpers.js +19 -1
- package/dist/output/shapes/passthrough.js +1 -0
- 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 +12698 -12530
- package/dist/scripts/akm-migrate.js +12698 -12530
- package/dist/storage/repositories/proposals-repository.js +33 -1
- package/dist/storage/repositories/task-history-repository.js +22 -10
- package/dist/tasks/run/run-native-task.js +28 -1
- package/dist/tasks/run/task-history.js +23 -3
- package/dist/tasks/scheduler-sync-preview.js +43 -0
- package/dist/tasks/scheduler-sync.js +1 -0
- 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 +500 -0
- package/dist/tasks/source/task-to-v4.js +467 -0
- package/docs/reference/cli.md +8 -3
- package/package.json +4 -4
|
@@ -1,7 +1,7 @@
|
|
|
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
|
-
import { directoryMatcher, extensionMatcher, parentDirHintMatcher, smartMdMatcher
|
|
4
|
+
import { directoryMatcher, extensionMatcher, parentDirHintMatcher, smartMdMatcher } from "../../indexer/walk/matchers.js";
|
|
5
5
|
/**
|
|
6
6
|
* The four builtin matchers, in registration order. The array index IS the
|
|
7
7
|
* registration index `runMatchers` uses for tie-breaking. (The `wiki` matcher
|
|
@@ -46,22 +46,3 @@ export function recognizeMatch(file) {
|
|
|
46
46
|
}
|
|
47
47
|
return winningMatch(hits);
|
|
48
48
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Every AKM matcher winner possible from path fields alone. The three
|
|
51
|
-
* path-only matchers run exactly as production does; each possible result of
|
|
52
|
-
* the shared smart-Markdown fact table is then arbitrated at its real index.
|
|
53
|
-
*/
|
|
54
|
-
export function recognizePathCandidateMatches(file) {
|
|
55
|
-
const fileContext = file;
|
|
56
|
-
const pathHits = [extensionMatcher, directoryMatcher, parentDirHintMatcher].flatMap((matcher, index) => {
|
|
57
|
-
const result = matcher(fileContext);
|
|
58
|
-
return result ? [{ result, index }] : [];
|
|
59
|
-
});
|
|
60
|
-
const smartCandidates = smartMdPathCandidates(file);
|
|
61
|
-
const variants = smartCandidates.length > 0 ? smartCandidates : [undefined];
|
|
62
|
-
const winners = variants.flatMap((smart) => {
|
|
63
|
-
const winner = winningMatch(smart ? [...pathHits, { result: smart, index: 3 }] : [...pathHits]);
|
|
64
|
-
return winner ? [winner] : [];
|
|
65
|
-
});
|
|
66
|
-
return [...new Map(winners.map((winner) => [`${winner.type}\0${winner.renderer}`, winner])).values()];
|
|
67
|
-
}
|
|
@@ -151,10 +151,10 @@ const BUILTIN_PLACEMENT_SPECS = {
|
|
|
151
151
|
isRelevantFile: (fileName) => path.extname(fileName).toLowerCase() === ".yml",
|
|
152
152
|
toCanonicalName: (typeRoot, filePath) => {
|
|
153
153
|
const rel = toPosix(path.relative(typeRoot, filePath));
|
|
154
|
-
return rel.endsWith(".yml") ? rel.slice(0, -4) : rel;
|
|
154
|
+
return rel.toLowerCase().endsWith(".yml") ? rel.slice(0, -4) : rel;
|
|
155
155
|
},
|
|
156
156
|
toAssetPath: (typeRoot, name) => {
|
|
157
|
-
const withExt = name.endsWith(".yml") ? name : `${name}.yml`;
|
|
157
|
+
const withExt = name.toLowerCase().endsWith(".yml") ? name : `${name}.yml`;
|
|
158
158
|
return path.join(typeRoot, withExt);
|
|
159
159
|
},
|
|
160
160
|
},
|
|
@@ -241,3 +241,22 @@ export function assetPathForName(assetType, typeRoot, name) {
|
|
|
241
241
|
throw new Error(`Unknown asset type: "${assetType}"`);
|
|
242
242
|
return spec.toAssetPath(typeRoot, name);
|
|
243
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Every physical path spelling that could own `name`, closed-form (#857).
|
|
246
|
+
* `toAssetPath` is a function, so it can only pick ONE spelling — but `env`'s
|
|
247
|
+
* "default" alias is genuinely dual-owned: both `<dir>/.env` and
|
|
248
|
+
* `<dir>/default.env` derive the same canonical name (`toCanonicalName`
|
|
249
|
+
* above), so a physical-owner lookup must consider both without reading
|
|
250
|
+
* either file. Every other placement type has exactly one inverse spelling.
|
|
251
|
+
*/
|
|
252
|
+
export function assetPathCandidatesForName(assetType, typeRoot, name) {
|
|
253
|
+
const primary = assetPathForName(assetType, typeRoot, name);
|
|
254
|
+
if (assetType !== "env")
|
|
255
|
+
return [primary];
|
|
256
|
+
const base = name === "default" ? "" : name.endsWith("/default") ? name.slice(0, -"default".length) : undefined;
|
|
257
|
+
if (base === undefined)
|
|
258
|
+
return [primary];
|
|
259
|
+
const dotForm = path.join(typeRoot, base, ".env");
|
|
260
|
+
const namedForm = path.join(typeRoot, base, "default.env");
|
|
261
|
+
return [...new Set([primary, dotForm, namedForm])];
|
|
262
|
+
}
|
package/dist/core/common.js
CHANGED
|
@@ -5,7 +5,7 @@ import crypto from "node:crypto";
|
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { ConfigError } from "./errors.js";
|
|
8
|
-
import { getConfigPath, getDefaultStashDir } from "./paths.js";
|
|
8
|
+
import { getConfigPath, getDefaultStashDir, getRegistryCacheDir, getRegistryIndexCacheDir } from "./paths.js";
|
|
9
9
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
10
10
|
// Moved to the platform leaf so paths.ts can use it without a common↔paths
|
|
11
11
|
// cycle (chunk-8 WI-8.6, DoD 11); re-exported here for the existing surface.
|
|
@@ -382,6 +382,26 @@ export function isContainedRelativePath(value) {
|
|
|
382
382
|
export function isWithin(candidate, root) {
|
|
383
383
|
return isContainedResolvedPath(safeRealpath(candidate), safeRealpath(root));
|
|
384
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* True when `filePath` sits inside akm's OWN resolved registry-cache
|
|
387
|
+
* directories (`<cache>/registry`, `<cache>/registry-index` —
|
|
388
|
+
* {@link getRegistryCacheDir}/{@link getRegistryIndexCacheDir}), the
|
|
389
|
+
* read-only installed-source/registry-index copies that lint (and `--fix`)
|
|
390
|
+
* must never touch.
|
|
391
|
+
*
|
|
392
|
+
* This is the single source of truth for that exclusion — it replaces what
|
|
393
|
+
* used to be three independent unanchored substring checks
|
|
394
|
+
* (`posixPath.includes("/.cache/") || posixPath.includes("/registry/")`).
|
|
395
|
+
* That check matched ANY path merely containing the literal text `.cache` or
|
|
396
|
+
* `registry` anywhere in it — a normal XDG `~/.cache/...` user bundle, or a
|
|
397
|
+
* CI workspace checked out under a `.cache`-named directory, tripped it and
|
|
398
|
+
* silently got zero lint findings. Using {@link isWithin} (realpath +
|
|
399
|
+
* containment, not a string search) fixes that while still excluding the
|
|
400
|
+
* real cache content the check was meant to skip.
|
|
401
|
+
*/
|
|
402
|
+
export function isAkmRegistryCachePath(filePath) {
|
|
403
|
+
return isWithin(filePath, getRegistryCacheDir()) || isWithin(filePath, getRegistryIndexCacheDir());
|
|
404
|
+
}
|
|
385
405
|
/**
|
|
386
406
|
* {@link isWithin} for callers that must not block the event loop (e.g. the
|
|
387
407
|
* workflow exec dispatch path, which runs once per fan-out unit). Same
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { ConfigError } from "../errors.js";
|
|
7
|
+
import { liftLegacyEngineExtraParams } from "../extra-params.js";
|
|
7
8
|
import { acquireConfigLock, backupExistingConfig, parseConfigText, readConfigText, withConfigLock, writeConfigAtomic, } from "./config-io.js";
|
|
8
9
|
import { AkmConfigSchema, CURRENT_CONFIG_VERSION } from "./config-schema.js";
|
|
9
10
|
import { bundlesToSourceEntries } from "./config-sources.js";
|
|
@@ -141,14 +142,29 @@ export function acquireConfigReadFence() {
|
|
|
141
142
|
* canonical shape before defaults are merged.
|
|
142
143
|
*/
|
|
143
144
|
export function parseAndValidateConfigText(text, sourcePath) {
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
145
|
+
const parsedRaw = parseConfigText(text, sourcePath);
|
|
146
|
+
if (parsedRaw.configVersion !== CURRENT_CONFIG_VERSION) {
|
|
146
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.");
|
|
147
148
|
}
|
|
149
|
+
// #852 (following #815): lift legacy `extraParams` keys — e.g.
|
|
150
|
+
// `reasoning_effort`, a documented 0.9.1 workaround — onto the first-class
|
|
151
|
+
// engine field they now shadow, before the protected-key check in
|
|
152
|
+
// `ExtraParamsSchema` gets a chance to hard-reject them. In-memory only;
|
|
153
|
+
// never written back to the file.
|
|
154
|
+
const { config: raw, lifted, conflicts } = liftLegacyEngineExtraParams(parsedRaw);
|
|
155
|
+
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
156
|
+
if (conflicts.length > 0) {
|
|
157
|
+
const lines = conflicts
|
|
158
|
+
.map((c) => ` - engines.${c.engine}.extraParams.${c.key} (${JSON.stringify(c.extraParamsValue)}) conflicts with engines.${c.engine}.${c.field} (${JSON.stringify(c.fieldValue)})`)
|
|
159
|
+
.join("\n");
|
|
160
|
+
throw new ConfigError(`Invalid config${where}: extraParams and the first-class field disagree:\n${lines}\n\nEach extraParams key above has a first-class equivalent and akm will not guess which value you meant — remove the extraParams entry once the field carries the value you want.`, "INVALID_CONFIG_FILE");
|
|
161
|
+
}
|
|
162
|
+
if (lifted.length > 0) {
|
|
163
|
+
warn(`Config${where} uses deprecated extraParams keys with first-class equivalents; treating them as the first-class fields for this run (not written back to the file):\n - ${lifted.join("\n - ")}`);
|
|
164
|
+
}
|
|
148
165
|
const parsed = AkmConfigSchema.safeParse(raw);
|
|
149
166
|
if (!parsed.success) {
|
|
150
167
|
const lines = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
151
|
-
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
152
168
|
throw new ConfigError(`Invalid config${where}:\n${lines}`, "INVALID_CONFIG_FILE");
|
|
153
169
|
}
|
|
154
170
|
const merged = deepMergeConfig(DEFAULT_CONFIG, parsed.data);
|
|
@@ -25,6 +25,46 @@ export const EXTRA_PARAMS_CREDENTIAL_KEYS = [
|
|
|
25
25
|
];
|
|
26
26
|
const PROTECTED_TOP_LEVEL_KEYS = new Set(EXTRA_PARAMS_PROTECTED_TOP_LEVEL_KEYS);
|
|
27
27
|
const CREDENTIAL_KEYS = new Set(EXTRA_PARAMS_CREDENTIAL_KEYS);
|
|
28
|
+
// ── Protected-key remedies (#852) ───────────────────────────────────────────
|
|
29
|
+
//
|
|
30
|
+
// `EXTRA_PARAMS_PROTECTED_TOP_LEVEL_KEYS` rightly stops a provider extra from
|
|
31
|
+
// shadowing an AKM-managed field, but naming the rule without naming the fix
|
|
32
|
+
// leaves the reader stuck (#852). Every protected key gets a one-line remedy
|
|
33
|
+
// baked into its issue message; keys with a genuine scalar first-class field
|
|
34
|
+
// are also eligible for the automatic config-load lift below.
|
|
35
|
+
/** normalizeExtraParamKey(key) -> the first-class engine field it shadows. */
|
|
36
|
+
const LEGACY_EXTRA_PARAMS_FIELD = {
|
|
37
|
+
model: "model",
|
|
38
|
+
temperature: "temperature",
|
|
39
|
+
maxtokens: "maxTokens",
|
|
40
|
+
enablethinking: "enableThinking",
|
|
41
|
+
reasoningeffort: "reasoningEffort",
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Subset of {@link LEGACY_EXTRA_PARAMS_FIELD} that {@link liftLegacyEngineExtraParams}
|
|
45
|
+
* will move onto the first-class field automatically. `model` is deliberately
|
|
46
|
+
* excluded: unlike the others it was never a "no first-class field yet"
|
|
47
|
+
* workaround (`model` has always been required on an LLM engine), so a
|
|
48
|
+
* mismatch is far more likely a genuine mistake than a stale 0.9.1 config —
|
|
49
|
+
* it stays a hard rejection rather than being silently reinterpreted.
|
|
50
|
+
*/
|
|
51
|
+
const LIFTABLE_EXTRA_PARAMS_KEYS = new Set(["temperature", "maxtokens", "enablethinking", "reasoningeffort"]);
|
|
52
|
+
/** Remedies for protected keys with no scalar first-class field to lift onto. */
|
|
53
|
+
const EXTRA_PARAMS_NO_FIELD_REMEDY = {
|
|
54
|
+
messages: "AKM builds the request messages internally — remove it from extraParams",
|
|
55
|
+
responseformat: "AKM controls the response format internally — remove it from extraParams",
|
|
56
|
+
stream: "AKM controls streaming internally — remove it from extraParams",
|
|
57
|
+
streamoptions: "AKM controls streaming internally — remove it from extraParams",
|
|
58
|
+
chattemplatekwargs: "set engines.<name>.enableThinking instead of chat_template_kwargs.enable_thinking",
|
|
59
|
+
};
|
|
60
|
+
function protectedKeyRemedy(normalized) {
|
|
61
|
+
const field = LEGACY_EXTRA_PARAMS_FIELD[normalized];
|
|
62
|
+
if (field) {
|
|
63
|
+
const note = normalized === "reasoningeffort" ? " (moved to a first-class field in 0.9.2)" : "";
|
|
64
|
+
return `set engines.<name>.${field} instead${note}`;
|
|
65
|
+
}
|
|
66
|
+
return EXTRA_PARAMS_NO_FIELD_REMEDY[normalized];
|
|
67
|
+
}
|
|
28
68
|
export function normalizeExtraParamKey(key) {
|
|
29
69
|
return key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
30
70
|
}
|
|
@@ -57,7 +97,11 @@ export function validateExtraParams(value) {
|
|
|
57
97
|
for (const [key, child] of Object.entries(entry)) {
|
|
58
98
|
const normalized = normalizeExtraParamKey(key);
|
|
59
99
|
if (path.length === 0 && PROTECTED_TOP_LEVEL_KEYS.has(normalized)) {
|
|
60
|
-
|
|
100
|
+
const remedy = protectedKeyRemedy(normalized);
|
|
101
|
+
issues.push({
|
|
102
|
+
path: [key],
|
|
103
|
+
message: remedy ? `${key} is protected by AKM — ${remedy}.` : `${key} is protected by AKM`,
|
|
104
|
+
});
|
|
61
105
|
}
|
|
62
106
|
if (CREDENTIAL_KEYS.has(normalized)) {
|
|
63
107
|
issues.push({ path: [...path, key], message: `${key} cannot carry credentials` });
|
|
@@ -72,3 +116,73 @@ export function formatExtraParamsIssue(label, issue) {
|
|
|
72
116
|
const suffix = issue.path.map((part) => (typeof part === "number" ? `[${part}]` : `.${part}`)).join("");
|
|
73
117
|
return `${label}${suffix} ${issue.message}`;
|
|
74
118
|
}
|
|
119
|
+
function isPlainObject(value) {
|
|
120
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Lift legacy `extraParams` keys onto their first-class engine field before
|
|
124
|
+
* schema validation runs, so a 0.9.1-shaped config using (e.g.)
|
|
125
|
+
* `extraParams.reasoning_effort` keeps loading now that `reasoningEffort` is
|
|
126
|
+
* a first-class — and therefore protected — field (#852, following #815).
|
|
127
|
+
*
|
|
128
|
+
* In-memory only: this never rewrites the config file. Callers should warn
|
|
129
|
+
* using the returned `lifted` descriptions so the user knows to update the
|
|
130
|
+
* file by hand, and reject using `conflicts` rather than silently preferring
|
|
131
|
+
* either value.
|
|
132
|
+
*/
|
|
133
|
+
export function liftLegacyEngineExtraParams(raw) {
|
|
134
|
+
const lifted = [];
|
|
135
|
+
const conflicts = [];
|
|
136
|
+
const rawEngines = raw.engines;
|
|
137
|
+
if (!isPlainObject(rawEngines)) {
|
|
138
|
+
return { config: raw, lifted, conflicts };
|
|
139
|
+
}
|
|
140
|
+
const engines = {};
|
|
141
|
+
let anyEngineChanged = false;
|
|
142
|
+
for (const [name, engineValue] of Object.entries(rawEngines)) {
|
|
143
|
+
if (!isPlainObject(engineValue) || !isPlainObject(engineValue.extraParams)) {
|
|
144
|
+
engines[name] = engineValue;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const engine = { ...engineValue };
|
|
148
|
+
const extraParams = { ...engineValue.extraParams };
|
|
149
|
+
let engineChanged = false;
|
|
150
|
+
for (const [rawKey, value] of Object.entries(engineValue.extraParams)) {
|
|
151
|
+
const normalized = normalizeExtraParamKey(rawKey);
|
|
152
|
+
if (!LIFTABLE_EXTRA_PARAMS_KEYS.has(normalized))
|
|
153
|
+
continue;
|
|
154
|
+
const field = LEGACY_EXTRA_PARAMS_FIELD[normalized];
|
|
155
|
+
if (!field)
|
|
156
|
+
continue;
|
|
157
|
+
const existing = engine[field];
|
|
158
|
+
if (existing !== undefined && existing !== value) {
|
|
159
|
+
conflicts.push({ engine: name, key: rawKey, field, extraParamsValue: value, fieldValue: existing });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
delete extraParams[rawKey];
|
|
163
|
+
engineChanged = true;
|
|
164
|
+
if (existing === value) {
|
|
165
|
+
lifted.push(`engines.${name}.extraParams.${rawKey} is redundant — engines.${name}.${field} is already set to the same value; dropped the extraParams entry`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
engine[field] = value;
|
|
169
|
+
lifted.push(`engines.${name}.extraParams.${rawKey} -> engines.${name}.${field}`);
|
|
170
|
+
}
|
|
171
|
+
if (!engineChanged) {
|
|
172
|
+
engines[name] = engineValue;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
anyEngineChanged = true;
|
|
176
|
+
if (Object.keys(extraParams).length > 0) {
|
|
177
|
+
engine.extraParams = extraParams;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
delete engine.extraParams;
|
|
181
|
+
}
|
|
182
|
+
engines[name] = engine;
|
|
183
|
+
}
|
|
184
|
+
if (!anyEngineChanged) {
|
|
185
|
+
return { config: raw, lifted, conflicts };
|
|
186
|
+
}
|
|
187
|
+
return { config: { ...raw, engines }, lifted, conflicts };
|
|
188
|
+
}
|
|
@@ -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
|
]
|
|
@@ -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();
|
|
@@ -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;
|
|
@@ -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) {
|