@cortexkit/aft-bridge 0.57.1 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/dist/bash-hints.d.ts +22 -3
  2. package/dist/bash-hints.d.ts.map +1 -1
  3. package/dist/bash-hints.js +28 -9
  4. package/dist/bash-hints.js.map +1 -1
  5. package/dist/bash-host-fallback.js +1 -1
  6. package/dist/bash-host-fallback.js.map +1 -1
  7. package/dist/binary-identity.d.ts +109 -0
  8. package/dist/binary-identity.d.ts.map +1 -0
  9. package/dist/binary-identity.js +295 -0
  10. package/dist/binary-identity.js.map +1 -0
  11. package/dist/bridge.d.ts +10 -1
  12. package/dist/bridge.d.ts.map +1 -1
  13. package/dist/bridge.js +50 -14
  14. package/dist/bridge.js.map +1 -1
  15. package/dist/child-process.d.ts +37 -0
  16. package/dist/child-process.d.ts.map +1 -0
  17. package/dist/child-process.js +62 -0
  18. package/dist/child-process.js.map +1 -0
  19. package/dist/coerce.d.ts +2 -0
  20. package/dist/coerce.d.ts.map +1 -1
  21. package/dist/coerce.js +29 -0
  22. package/dist/coerce.js.map +1 -1
  23. package/dist/config-error.d.ts +71 -0
  24. package/dist/config-error.d.ts.map +1 -0
  25. package/dist/config-error.js +112 -0
  26. package/dist/config-error.js.map +1 -0
  27. package/dist/downloader.d.ts +4 -2
  28. package/dist/downloader.d.ts.map +1 -1
  29. package/dist/downloader.js +41 -20
  30. package/dist/downloader.js.map +1 -1
  31. package/dist/durable-log.d.ts +7 -0
  32. package/dist/durable-log.d.ts.map +1 -1
  33. package/dist/durable-log.js +47 -4
  34. package/dist/durable-log.js.map +1 -1
  35. package/dist/error-contract.d.ts +19 -0
  36. package/dist/error-contract.d.ts.map +1 -1
  37. package/dist/error-contract.js +24 -0
  38. package/dist/error-contract.js.map +1 -1
  39. package/dist/feature-config.d.ts +192 -0
  40. package/dist/feature-config.d.ts.map +1 -0
  41. package/dist/feature-config.js +657 -0
  42. package/dist/feature-config.js.map +1 -0
  43. package/dist/index.d.ts +17 -7
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +17 -6
  46. package/dist/index.js.map +1 -1
  47. package/dist/message-detach.d.ts +25 -0
  48. package/dist/message-detach.d.ts.map +1 -0
  49. package/dist/message-detach.js +34 -0
  50. package/dist/message-detach.js.map +1 -0
  51. package/dist/migration-notices.d.ts +29 -0
  52. package/dist/migration-notices.d.ts.map +1 -0
  53. package/dist/migration-notices.js +114 -0
  54. package/dist/migration-notices.js.map +1 -0
  55. package/dist/migration.d.ts +1 -1
  56. package/dist/migration.d.ts.map +1 -1
  57. package/dist/migration.js +1 -1
  58. package/dist/migration.js.map +1 -1
  59. package/dist/npm-resolver.d.ts +1 -1
  60. package/dist/npm-resolver.d.ts.map +1 -1
  61. package/dist/npm-resolver.js +1 -1
  62. package/dist/npm-resolver.js.map +1 -1
  63. package/dist/onnx-probe.d.ts +44 -0
  64. package/dist/onnx-probe.d.ts.map +1 -0
  65. package/dist/onnx-probe.js +167 -0
  66. package/dist/onnx-probe.js.map +1 -0
  67. package/dist/onnx-runtime.d.ts +19 -4
  68. package/dist/onnx-runtime.d.ts.map +1 -1
  69. package/dist/onnx-runtime.js +146 -34
  70. package/dist/onnx-runtime.js.map +1 -1
  71. package/dist/path-lookup.d.ts +22 -0
  72. package/dist/path-lookup.d.ts.map +1 -0
  73. package/dist/path-lookup.js +51 -0
  74. package/dist/path-lookup.js.map +1 -0
  75. package/dist/resolver.d.ts +47 -21
  76. package/dist/resolver.d.ts.map +1 -1
  77. package/dist/resolver.js +254 -159
  78. package/dist/resolver.js.map +1 -1
  79. package/dist/revivable-transport.d.ts.map +1 -1
  80. package/dist/revivable-transport.js +12 -0
  81. package/dist/revivable-transport.js.map +1 -1
  82. package/dist/semantic-status.d.ts +10 -1
  83. package/dist/semantic-status.d.ts.map +1 -1
  84. package/dist/semantic-status.js +50 -2
  85. package/dist/semantic-status.js.map +1 -1
  86. package/dist/storage-paths.d.ts +9 -0
  87. package/dist/storage-paths.d.ts.map +1 -1
  88. package/dist/storage-paths.js +18 -1
  89. package/dist/storage-paths.js.map +1 -1
  90. package/dist/subc-transport.d.ts +60 -1
  91. package/dist/subc-transport.d.ts.map +1 -1
  92. package/dist/subc-transport.js +382 -69
  93. package/dist/subc-transport.js.map +1 -1
  94. package/dist/tar-executable.js +1 -1
  95. package/dist/tar-executable.js.map +1 -1
  96. package/dist/transport-factory.d.ts +14 -2
  97. package/dist/transport-factory.d.ts.map +1 -1
  98. package/dist/transport-factory.js +24 -16
  99. package/dist/transport-factory.js.map +1 -1
  100. package/dist/version-probe.d.ts +33 -0
  101. package/dist/version-probe.d.ts.map +1 -0
  102. package/dist/version-probe.js +108 -0
  103. package/dist/version-probe.js.map +1 -0
  104. package/package.json +2 -2
  105. package/dist/bash-timeout.d.ts +0 -19
  106. package/dist/bash-timeout.d.ts.map +0 -1
  107. package/dist/bash-timeout.js +0 -26
  108. package/dist/bash-timeout.js.map +0 -1
@@ -0,0 +1,657 @@
1
+ /**
2
+ * Feature-based configuration policy shared by the OpenCode and Pi plugins.
3
+ *
4
+ * Mirrors `crates/aft/src/feature_config.rs`: the literal tool inventory, the
5
+ * one-release migration policy for retired keys, the per-block translation of
6
+ * those keys into canonical `disabled_tools` / `indexes` / `github` leaves,
7
+ * validation of a resolved configuration, and the migration-notice projection
8
+ * digest. The config parity fixtures and the shared notice-projection fixture
9
+ * (`crates/aft/tests/fixtures/feature_config/notice_projection.json`) keep the
10
+ * two implementations equal.
11
+ */
12
+ import { createHash } from "node:crypto";
13
+ /** Every agent-visible tool name a registration adapter may publish (sorted). */
14
+ export const CANONICAL_TOOLS = [
15
+ "aft_callgraph",
16
+ "aft_conflicts",
17
+ "aft_delete",
18
+ "aft_import",
19
+ "aft_inspect",
20
+ "aft_move",
21
+ "aft_outline",
22
+ "aft_safety",
23
+ "aft_search",
24
+ "aft_zoom",
25
+ "apply_patch",
26
+ "ast_grep_replace",
27
+ "ast_grep_search",
28
+ "bash",
29
+ "bash_kill",
30
+ "bash_status",
31
+ "bash_watch",
32
+ "bash_write",
33
+ "edit",
34
+ "glob",
35
+ "grep",
36
+ "read",
37
+ "write",
38
+ ];
39
+ /** The seven host tool slots AFT takes over. */
40
+ export const HOST_TOOL_NAMES = [
41
+ "apply_patch",
42
+ "bash",
43
+ "edit",
44
+ "glob",
45
+ "grep",
46
+ "read",
47
+ "write",
48
+ ];
49
+ /** Disables applied when the user base makes no registration choice. */
50
+ export const DEFAULT_DISABLED_TOOLS = ["aft_delete", "aft_move"];
51
+ /** Historical prefixed tool names accepted in disabled lists during the window. */
52
+ export const LEGACY_TOOL_ALIASES = {
53
+ aft_read: "read",
54
+ aft_write: "write",
55
+ aft_edit: "edit",
56
+ aft_apply_patch: "apply_patch",
57
+ aft_grep: "grep",
58
+ aft_glob: "glob",
59
+ aft_bash: "bash",
60
+ };
61
+ /** Names the historical bash registration gate removed (canonical names). */
62
+ export const BASH_GATE_DISABLES = [
63
+ "bash",
64
+ "bash_kill",
65
+ "bash_status",
66
+ "bash_watch",
67
+ "bash_write",
68
+ ];
69
+ export const MIGRATION_POLICY_ID = "feature-config-v1";
70
+ export const POLICY_INTRODUCED_MINOR = [0, 58];
71
+ export const POLICY_REJECT_FROM_MINOR = [0, 59];
72
+ /** Retired config paths and the replacement named in their rejection. */
73
+ export const RETIRED_PATHS = [
74
+ ["tool_surface", "disabled_tools"],
75
+ ["hoist_builtin_tools", "disabled_tools"],
76
+ ["enabled", "disabled_tools"],
77
+ ["search_index", "indexes.trigram"],
78
+ ["experimental_search_index", "indexes.trigram"],
79
+ ["semantic_search", "indexes.semantic"],
80
+ ["experimental_semantic_search", "indexes.semantic"],
81
+ ["callgraph_store", "indexes.callgraph"],
82
+ ["github.enabled", "github.read,github.write,github.shim"],
83
+ ];
84
+ /** Index leaf, immediate legacy key and optional experimental alias. */
85
+ export const INDEX_INPUTS = [
86
+ ["trigram", "search_index", "experimental_search_index"],
87
+ ["semantic", "semantic_search", "experimental_semantic_search"],
88
+ ["callgraph", "callgraph_store", undefined],
89
+ ];
90
+ /**
91
+ * Registration adapters that cannot register some canonical tools because the
92
+ * adapter has no implementation for them. The registered set on an adapter is
93
+ * the canonical inventory minus resolved disables minus this list; setup
94
+ * reports these rows as unavailable with reason `no_implementation_on_harness`.
95
+ */
96
+ export const ADAPTER_UNIMPLEMENTED_TOOLS = {
97
+ "opencode-v1": [],
98
+ "opencode-v2": [],
99
+ pi: ["apply_patch", "glob"],
100
+ omp: ["apply_patch", "glob"],
101
+ };
102
+ export function isKnownTool(name) {
103
+ return CANONICAL_TOOLS.includes(name);
104
+ }
105
+ export function isProjectProtectedTool(name) {
106
+ return name === "aft_safety" || HOST_TOOL_NAMES.includes(name);
107
+ }
108
+ /** Literal disabled set a legacy `tool_surface` value translates to. */
109
+ export function surfaceDisables(surface) {
110
+ switch (surface) {
111
+ case "all":
112
+ return [];
113
+ case "recommended":
114
+ return ["aft_callgraph", "aft_delete", "aft_move"];
115
+ case "minimal":
116
+ return CANONICAL_TOOLS.filter((name) => name !== "aft_outline" && name !== "aft_zoom" && name !== "aft_safety");
117
+ default:
118
+ return undefined;
119
+ }
120
+ }
121
+ function parseMinor(version) {
122
+ const match = /^v?(\d+)\.(\d+)/.exec(version.trim());
123
+ if (!match)
124
+ return undefined;
125
+ return [Number(match[1]), Number(match[2])];
126
+ }
127
+ /** Policy phase for a package version (major/minor only; patch ignored). */
128
+ export function policyPhaseForVersion(version) {
129
+ const minor = parseMinor(version);
130
+ if (!minor)
131
+ return "window";
132
+ const [major, min] = minor;
133
+ const [rejectMajor, rejectMinor] = POLICY_REJECT_FROM_MINOR;
134
+ return major > rejectMajor || (major === rejectMajor && min >= rejectMinor)
135
+ ? "rejecting"
136
+ : "window";
137
+ }
138
+ function isRecord(value) {
139
+ return typeof value === "object" && value !== null && !Array.isArray(value);
140
+ }
141
+ function removed(old, replacement) {
142
+ return `removed_config_key:${old}:use:${replacement}`;
143
+ }
144
+ function hasPath(map, path) {
145
+ const dot = path.indexOf(".");
146
+ if (dot < 0)
147
+ return Object.hasOwn(map, path);
148
+ const container = map[path.slice(0, dot)];
149
+ return isRecord(container) && Object.hasOwn(container, path.slice(dot + 1));
150
+ }
151
+ function falseRuntimeGates(map) {
152
+ const gates = [];
153
+ if (isRecord(map.backup) && map.backup.enabled === false)
154
+ gates.push("backup.enabled");
155
+ if (isRecord(map.inspect) && map.inspect.enabled === false)
156
+ gates.push("inspect.enabled");
157
+ if (map.bash === false)
158
+ gates.push("bash");
159
+ else if (isRecord(map.bash) && map.bash.enabled === false)
160
+ gates.push("bash.enabled");
161
+ return gates;
162
+ }
163
+ export function sortedUnique(names) {
164
+ return [...new Set(names)].sort();
165
+ }
166
+ function translateBlock(map,
167
+ // True only for the user file's base block, the one place the absent-base
168
+ // default disables may be added.
169
+ userBase, phase, blockLabel, out) {
170
+ if (Object.hasOwn(map, "gh_read"))
171
+ out.errors.push(removed("gh_read", "github.read"));
172
+ if (isRecord(map.gh_shim) && Object.hasOwn(map.gh_shim, "enabled")) {
173
+ out.errors.push(removed("gh_shim", "github.shim"));
174
+ }
175
+ let explicitList;
176
+ const rawList = map.disabled_tools;
177
+ if (Array.isArray(rawList) && rawList.every((entry) => typeof entry === "string")) {
178
+ explicitList = rawList.map((name) => {
179
+ const host = LEGACY_TOOL_ALIASES[name];
180
+ if (host === undefined)
181
+ return name;
182
+ out.legacyInput = true;
183
+ if (phase === "rejecting")
184
+ out.errors.push(removed(name, host));
185
+ return host;
186
+ });
187
+ }
188
+ const suppliedPaths = RETIRED_PATHS.map(([path]) => path).filter((path) => hasPath(map, path));
189
+ if (suppliedPaths.length > 0)
190
+ out.legacyInput = true;
191
+ const gates = falseRuntimeGates(map);
192
+ if (phase === "rejecting") {
193
+ for (const path of suppliedPaths) {
194
+ const replacement = RETIRED_PATHS.find(([old]) => old === path)?.[1] ?? "disabled_tools";
195
+ out.errors.push(removed(path, replacement));
196
+ }
197
+ if (gates.length > 0 && explicitList === undefined) {
198
+ out.warnings.push({
199
+ code: "legacy_runtime_gate_runtime_only",
200
+ key: blockLabel,
201
+ message: `${gates.join(", ")} now restricts runtime behavior only and no longer removes tool registrations; list tools in disabled_tools to unregister them`,
202
+ });
203
+ }
204
+ return;
205
+ }
206
+ if (explicitList !== undefined)
207
+ map.disabled_tools = explicitList;
208
+ for (const [leaf, legacy, experimental] of INDEX_INPUTS) {
209
+ const indexes = isRecord(map.indexes) ? map.indexes : undefined;
210
+ const canonical = typeof indexes?.[leaf] === "boolean" ? indexes[leaf] : undefined;
211
+ const inputs = [];
212
+ if (Object.hasOwn(map, legacy)) {
213
+ inputs.push([legacy, map[legacy]]);
214
+ delete map[legacy];
215
+ }
216
+ if (experimental !== undefined && Object.hasOwn(map, experimental)) {
217
+ inputs.push([experimental, map[experimental]]);
218
+ delete map[experimental];
219
+ }
220
+ let chosen;
221
+ for (const [key, value] of inputs) {
222
+ if (typeof value !== "boolean") {
223
+ out.warnings.push({
224
+ code: "invalid_legacy_config",
225
+ key,
226
+ message: `Ignoring non-boolean ${key}; use indexes.${leaf}`,
227
+ });
228
+ continue;
229
+ }
230
+ if (canonical !== undefined) {
231
+ if (canonical !== value) {
232
+ out.warnings.push({
233
+ code: "superseded_legacy_config",
234
+ key,
235
+ message: `${key}=${value} is ignored because indexes.${leaf}=${canonical} is set`,
236
+ });
237
+ }
238
+ }
239
+ else if (chosen !== undefined) {
240
+ if (chosen !== value) {
241
+ out.warnings.push({
242
+ code: "superseded_legacy_config",
243
+ key,
244
+ message: `${key}=${value} is ignored because ${legacy}=${chosen} takes precedence`,
245
+ });
246
+ }
247
+ }
248
+ else {
249
+ chosen = value;
250
+ }
251
+ }
252
+ if (canonical === undefined && chosen !== undefined) {
253
+ if (!Object.hasOwn(map, "indexes"))
254
+ map.indexes = {};
255
+ if (isRecord(map.indexes))
256
+ map.indexes[leaf] = chosen;
257
+ }
258
+ }
259
+ if (isRecord(map.github) && Object.hasOwn(map.github, "enabled")) {
260
+ const github = map.github;
261
+ const master = github.enabled;
262
+ delete github.enabled;
263
+ if (master === false) {
264
+ for (const leaf of ["read", "write", "shim"]) {
265
+ if (!Object.hasOwn(github, leaf))
266
+ github[leaf] = false;
267
+ else if (github[leaf] === true) {
268
+ out.warnings.push({
269
+ code: "superseded_legacy_config",
270
+ key: "github.enabled",
271
+ message: `github.enabled=false is ignored for github.${leaf} because github.${leaf}=true is set`,
272
+ });
273
+ }
274
+ }
275
+ }
276
+ }
277
+ const surface = map.tool_surface;
278
+ const hasSurface = Object.hasOwn(map, "tool_surface");
279
+ const hoist = map.hoist_builtin_tools;
280
+ const enabled = map.enabled;
281
+ delete map.tool_surface;
282
+ delete map.hoist_builtin_tools;
283
+ delete map.enabled;
284
+ const generated = new Set();
285
+ let explicitSurface = false;
286
+ if (hasSurface) {
287
+ const names = surfaceDisables(surface);
288
+ if (names === undefined) {
289
+ out.warnings.push({
290
+ code: "invalid_legacy_config",
291
+ key: "tool_surface",
292
+ message: "Ignoring unknown tool_surface value; use disabled_tools",
293
+ });
294
+ }
295
+ else {
296
+ explicitSurface = true;
297
+ for (const name of names)
298
+ generated.add(name);
299
+ }
300
+ }
301
+ if (hoist === false)
302
+ for (const name of HOST_TOOL_NAMES)
303
+ generated.add(name);
304
+ if (enabled === false) {
305
+ for (const name of CANONICAL_TOOLS)
306
+ generated.add(name);
307
+ out.retiredEnabledFalse = true;
308
+ out.warnings.push({
309
+ code: "legacy_enabled_false_indexes_still_build",
310
+ key: blockLabel === "base" ? "enabled" : `${blockLabel}.enabled`,
311
+ message: RETIRED_ENABLED_FALSE_INDEXES_NOTE,
312
+ });
313
+ }
314
+ for (const gate of gates) {
315
+ if (gate === "backup.enabled")
316
+ generated.add("aft_safety");
317
+ else if (gate === "inspect.enabled")
318
+ generated.add("aft_inspect");
319
+ else
320
+ for (const name of BASH_GATE_DISABLES)
321
+ generated.add(name);
322
+ }
323
+ if (explicitList !== undefined) {
324
+ if (explicitSurface || generated.size > 0) {
325
+ out.warnings.push({
326
+ code: "superseded_legacy_config",
327
+ key: blockLabel,
328
+ message: "disabled_tools is set explicitly, so legacy tool_surface/hoist_builtin_tools/enabled and runtime gates do not change registration",
329
+ once: true,
330
+ });
331
+ }
332
+ return;
333
+ }
334
+ let list;
335
+ if (userBase) {
336
+ if (explicitSurface)
337
+ list = sortedUnique(generated);
338
+ else if (generated.size > 0)
339
+ list = sortedUnique([...generated, ...DEFAULT_DISABLED_TOOLS]);
340
+ }
341
+ else if (generated.size > 0) {
342
+ list = sortedUnique(generated);
343
+ }
344
+ if (list !== undefined)
345
+ map.disabled_tools = list;
346
+ if (gates.length > 0) {
347
+ out.warnings.push({
348
+ code: "legacy_runtime_gate_requires_fix",
349
+ key: blockLabel,
350
+ message: `${gates.join(", ")} still removes tool registrations during this release only; run \`npx @cortexkit/aft doctor --fix\` to record the choice in disabled_tools`,
351
+ });
352
+ }
353
+ }
354
+ /**
355
+ * What a retired `enabled: false` no longer does. The translation only hides
356
+ * tools; it never stops indexing, so a user who relied on it to keep AFT out
357
+ * of a repository must also switch the indexes off.
358
+ */
359
+ export const RETIRED_ENABLED_FALSE_INDEXES_NOTE = "enabled: false no longer turns AFT off: it is translated to disabling every tool, but the trigram, semantic and callgraph indexes still build. To keep AFT from indexing this repository, also set indexes.trigram, indexes.semantic and indexes.callgraph to false.";
360
+ /** The once-per-identity migration notice for a config file that used retired keys. */
361
+ export function legacyConfigNoticeMessage(configPath, translation) {
362
+ const base = `AFT config ${configPath} uses retired keys (tool_surface, hoist_builtin_tools, enabled, search_index, semantic_search, callgraph_store, github.enabled or aft_-prefixed tool names). They are translated for this release and rejected from v0.59; run \`npx @cortexkit/aft doctor --fix\` to migrate.`;
363
+ return translation.retiredEnabledFalse ? `${base} ${RETIRED_ENABLED_FALSE_INDEXES_NOTE}` : base;
364
+ }
365
+ /**
366
+ * Translate or reject every block (base plus each `harnesses.<id>`) in place.
367
+ *
368
+ * Only the user file's base block receives the absent-base default
369
+ * (`aft_move`/`aft_delete`) when its legacy keys generate disables: that
370
+ * default applies once, at user-base resolution. A project file's base block
371
+ * contributes only the names its own legacy keys imply, so a legacy key in a
372
+ * repository can never re-disable tools the user enabled.
373
+ */
374
+ export function translateConfigDocument(map, phase, tier) {
375
+ const out = {
376
+ errors: [],
377
+ warnings: [],
378
+ legacyInput: false,
379
+ retiredEnabledFalse: false,
380
+ };
381
+ translateBlock(map, tier === "user", phase, "base", out);
382
+ if (isRecord(map.harnesses)) {
383
+ for (const [name, block] of Object.entries(map.harnesses)) {
384
+ if (isRecord(block))
385
+ translateBlock(block, false, phase, `harnesses.${name}`, out);
386
+ }
387
+ }
388
+ out.errors = sortedUnique(out.errors);
389
+ return out;
390
+ }
391
+ /** Distinct unknown names in a resolved disabled list, sorted. */
392
+ export function unknownDisabledTools(disabled) {
393
+ return sortedUnique(disabled.filter((name) => !isKnownTool(name)));
394
+ }
395
+ /**
396
+ * Validate a resolved configuration object. Missing fields are never replaced
397
+ * by defaults here; missing containers are reported instead of their children
398
+ * and errors are sorted by path.
399
+ */
400
+ export function validateResolvedConfig(value) {
401
+ if (!isRecord(value))
402
+ return ["invalid_resolved_config:type:$"];
403
+ const errors = [];
404
+ const disabled = value.disabled_tools;
405
+ if (disabled === undefined)
406
+ errors.push("invalid_resolved_config:missing:disabled_tools");
407
+ else if (!Array.isArray(disabled) || !disabled.every((entry) => typeof entry === "string")) {
408
+ errors.push("invalid_resolved_config:type:disabled_tools");
409
+ }
410
+ const indexes = value.indexes;
411
+ if (indexes === undefined)
412
+ errors.push("invalid_resolved_config:missing:indexes");
413
+ else if (!isRecord(indexes))
414
+ errors.push("invalid_resolved_config:type:indexes");
415
+ else {
416
+ for (const leaf of ["callgraph", "semantic", "trigram"]) {
417
+ if (indexes[leaf] === undefined)
418
+ errors.push(`invalid_resolved_config:missing:indexes.${leaf}`);
419
+ else if (typeof indexes[leaf] !== "boolean") {
420
+ errors.push(`invalid_resolved_config:type:indexes.${leaf}`);
421
+ }
422
+ }
423
+ }
424
+ const path = (error) => error.slice(error.lastIndexOf(":") + 1);
425
+ return errors.sort((left, right) => path(left) < path(right) ? -1 : path(left) > path(right) ? 1 : 0);
426
+ }
427
+ const ABSENT = { absent: true };
428
+ function projectBlock(block) {
429
+ const legacyPaths = {};
430
+ for (const [path] of RETIRED_PATHS) {
431
+ const dot = path.indexOf(".");
432
+ if (dot < 0) {
433
+ if (Object.hasOwn(block, path))
434
+ legacyPaths[path] = block[path];
435
+ }
436
+ else {
437
+ const container = block[path.slice(0, dot)];
438
+ const leaf = path.slice(dot + 1);
439
+ if (isRecord(container) && Object.hasOwn(container, leaf))
440
+ legacyPaths[path] = container[leaf];
441
+ }
442
+ }
443
+ const gates = {};
444
+ if (isRecord(block.backup) && Object.hasOwn(block.backup, "enabled")) {
445
+ gates["backup.enabled"] = block.backup.enabled;
446
+ }
447
+ if (isRecord(block.inspect) && Object.hasOwn(block.inspect, "enabled")) {
448
+ gates["inspect.enabled"] = block.inspect.enabled;
449
+ }
450
+ if (typeof block.bash === "boolean")
451
+ gates.bash = block.bash;
452
+ else if (isRecord(block.bash) && Object.hasOwn(block.bash, "enabled")) {
453
+ gates["bash.enabled"] = block.bash.enabled;
454
+ }
455
+ let aliases = [];
456
+ let disabled = ABSENT;
457
+ if (Array.isArray(block.disabled_tools)) {
458
+ const names = block.disabled_tools.filter((entry) => typeof entry === "string");
459
+ aliases = sortedUnique(names.filter((name) => LEGACY_TOOL_ALIASES[name] !== undefined));
460
+ disabled = sortedUnique(names);
461
+ }
462
+ else if (Object.hasOwn(block, "disabled_tools")) {
463
+ disabled = block.disabled_tools;
464
+ }
465
+ const github = {};
466
+ for (const leaf of ["read", "shim", "write"]) {
467
+ github[leaf] =
468
+ isRecord(block.github) && Object.hasOwn(block.github, leaf) ? block.github[leaf] : ABSENT;
469
+ }
470
+ const indexes = {};
471
+ for (const [leaf, legacy, experimental] of INDEX_INPUTS) {
472
+ const inputs = {
473
+ canonical: isRecord(block.indexes) && Object.hasOwn(block.indexes, leaf)
474
+ ? block.indexes[leaf]
475
+ : ABSENT,
476
+ [legacy]: Object.hasOwn(block, legacy) ? block[legacy] : ABSENT,
477
+ };
478
+ if (experimental !== undefined) {
479
+ inputs[experimental] = Object.hasOwn(block, experimental) ? block[experimental] : ABSENT;
480
+ }
481
+ indexes[leaf] = inputs;
482
+ }
483
+ return {
484
+ legacy_paths: legacyPaths,
485
+ runtime_gates: gates,
486
+ legacy_disabled_entries: aliases,
487
+ disabled_tools: disabled,
488
+ github,
489
+ indexes,
490
+ };
491
+ }
492
+ /**
493
+ * Build the `notice_projection_v1` object for one raw (untranslated) config
494
+ * document. Only translation-relevant inputs are projected, so comments,
495
+ * formatting, key order and unrelated settings never change it.
496
+ */
497
+ export function noticeProjection(raw) {
498
+ const blocks = {};
499
+ if (raw !== undefined) {
500
+ blocks.base = projectBlock(raw);
501
+ if (isRecord(raw.harnesses)) {
502
+ for (const [name, block] of Object.entries(raw.harnesses)) {
503
+ if (isRecord(block))
504
+ blocks[`harnesses.${name}`] = projectBlock(block);
505
+ }
506
+ }
507
+ }
508
+ return { projection: "notice_projection_v1", file_absent: raw === undefined, blocks };
509
+ }
510
+ /** Compact JSON with object keys sorted at every level. */
511
+ export function canonicalJson(value) {
512
+ if (Array.isArray(value))
513
+ return `[${value.map(canonicalJson).join(",")}]`;
514
+ if (isRecord(value)) {
515
+ return `{${Object.keys(value)
516
+ .sort()
517
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
518
+ .join(",")}}`;
519
+ }
520
+ return JSON.stringify(value);
521
+ }
522
+ /** SHA-256 hex digest of a canonical projection. */
523
+ export function noticeDigest(projection) {
524
+ return createHash("sha256").update(canonicalJson(projection), "utf8").digest("hex");
525
+ }
526
+ /**
527
+ * One-time notice shown when the semantic index is on only because indexes now
528
+ * default on. The text follows the feature-config spec, except that commands
529
+ * are spelled as `npx @cortexkit/aft …`: users reach this notice through the
530
+ * plugin, with no `aft` command on their PATH.
531
+ */
532
+ export const SEMANTIC_COST_NOTICE = "AFT indexes now default on; the local semantic backend may download an ONNX runtime and model and use CPU. Run npx @cortexkit/aft setup to change indexes.semantic; if legacy configuration is rejected, run npx @cortexkit/aft doctor --fix first.";
533
+ /**
534
+ * Identity of the semantic cost notice. It is a constant rather than a digest
535
+ * of the config file, so the notice is delivered once per user config path and
536
+ * unrelated config edits never bring it back.
537
+ */
538
+ export const SEMANTIC_COST_NOTICE_DIGEST = noticeDigest({ notice: "semantic_default_cost_v1" });
539
+ /**
540
+ * True when a raw config document supplies any semantic index input (the
541
+ * canonical `indexes.semantic` leaf or a legacy/experimental alias) in its base
542
+ * block or in the block of `activeHarness`. A supplied value, true or false,
543
+ * means the user chose the setting, so the default-on cost notice does not
544
+ * apply.
545
+ */
546
+ export function suppliesSemanticIndexInput(raw, activeHarness) {
547
+ if (raw === undefined)
548
+ return false;
549
+ const [leaf, immediate, experimental] = INDEX_INPUTS.find(([name]) => name === "semantic") ?? [
550
+ "semantic",
551
+ "semantic_search",
552
+ "experimental_semantic_search",
553
+ ];
554
+ const blockSupplies = (block) => {
555
+ if (!isRecord(block))
556
+ return false;
557
+ if (isRecord(block.indexes) && block.indexes[leaf] !== undefined)
558
+ return true;
559
+ if (block[immediate] !== undefined)
560
+ return true;
561
+ return experimental !== undefined && block[experimental] !== undefined;
562
+ };
563
+ const harnessBlock = isRecord(raw.harnesses) ? raw.harnesses[activeHarness] : undefined;
564
+ return blockSupplies(raw) || blockSupplies(harnessBlock);
565
+ }
566
+ /**
567
+ * The semantic cost notice for one load, or null when it does not apply. It is
568
+ * a migration notice: it applies only to an existing config file that relied
569
+ * on the old default, where the semantic index is now effectively on because
570
+ * of the new default. That means a config file was loaded, no loaded tier
571
+ * supplied a semantic input, and no non-local embedding backend is configured
572
+ * (a remote backend downloads nothing). A fresh install has no file that ever
573
+ * relied on the old default, so it gets no notice.
574
+ */
575
+ export function semanticCostNotice(options) {
576
+ if (!options.configFileLoaded)
577
+ return null;
578
+ if (!options.semanticEffective || options.semanticInputSupplied)
579
+ return null;
580
+ if (options.semanticBackend !== undefined && options.semanticBackend !== "fastembed")
581
+ return null;
582
+ return {
583
+ configPath: options.userConfigPath,
584
+ digest: SEMANTIC_COST_NOTICE_DIGEST,
585
+ message: SEMANTIC_COST_NOTICE,
586
+ };
587
+ }
588
+ export const DEFAULT_INDEXES = {
589
+ trigram: true,
590
+ semantic: true,
591
+ callgraph: true,
592
+ };
593
+ /** Whole-load rejection: the candidate config must not be used at all. */
594
+ export class ConfigRejectedError extends Error {
595
+ errors;
596
+ constructor(errors, source) {
597
+ super(`AFT configuration${source ? ` at ${source}` : ""} was rejected: ${errors.join(", ")}. ` +
598
+ "Run `npx @cortexkit/aft doctor --fix` to migrate removed keys.");
599
+ this.name = "ConfigRejectedError";
600
+ this.errors = errors;
601
+ }
602
+ }
603
+ /**
604
+ * Union two raw disabled lists while preserving presence: an explicit empty
605
+ * list on either side yields an explicit (possibly empty) sorted result.
606
+ */
607
+ export function unionDisabledTools(base, override) {
608
+ if (override === undefined)
609
+ return base === undefined ? undefined : [...base];
610
+ return sortedUnique([...(base ?? []), ...override]);
611
+ }
612
+ /**
613
+ * Merge index switches. Trusted overrides (user harness block) replace each
614
+ * supplied leaf; restricted overrides (project base or harness) can only turn
615
+ * a leaf off.
616
+ */
617
+ export function mergeIndexes(base, override, restricted) {
618
+ if (override === undefined)
619
+ return base;
620
+ const merged = { ...base };
621
+ for (const leaf of ["trigram", "semantic", "callgraph"]) {
622
+ const value = override[leaf];
623
+ if (value === false)
624
+ merged[leaf] = false;
625
+ else if (value === true && !restricted)
626
+ merged[leaf] = true;
627
+ }
628
+ return merged;
629
+ }
630
+ /** Resolve raw index switches onto their defaults. */
631
+ export function resolveIndexes(raw) {
632
+ return {
633
+ trigram: raw?.trigram ?? DEFAULT_INDEXES.trigram,
634
+ semantic: raw?.semantic ?? DEFAULT_INDEXES.semantic,
635
+ callgraph: raw?.callgraph ?? DEFAULT_INDEXES.callgraph,
636
+ };
637
+ }
638
+ /**
639
+ * Split a project disabled list into accepted names and ignored protected
640
+ * slots (aft_safety and the seven host tools).
641
+ */
642
+ export function partitionProjectDisables(names) {
643
+ if (names === undefined)
644
+ return { accepted: undefined, ignored: [] };
645
+ return {
646
+ accepted: names.filter((name) => !isProjectProtectedTool(name)),
647
+ ignored: sortedUnique(names.filter(isProjectProtectedTool)),
648
+ };
649
+ }
650
+ /** The resolved disabled list of a finalized config; absence is a bug, not []. */
651
+ export function requireResolvedDisabledTools(config) {
652
+ if (config.disabled_tools === undefined) {
653
+ throw new ConfigRejectedError(["invalid_resolved_config:missing:disabled_tools"]);
654
+ }
655
+ return config.disabled_tools;
656
+ }
657
+ //# sourceMappingURL=feature-config.js.map