akm-cli 0.9.10 → 0.9.12
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 +226 -0
- package/STABILITY.md +28 -15
- package/dist/assets/hints/cli-hints-full.md +1 -1
- package/dist/assets/improve-strategies/consolidate.json +1 -1
- package/dist/assets/improve-strategies/default.json +1 -1
- package/dist/assets/improve-strategies/thorough.json +1 -2
- package/dist/cli/shared.js +16 -4
- package/dist/cli.js +15 -13
- package/dist/commands/agent/agent-dispatch.js +8 -0
- package/dist/commands/command/execution-source-loader.js +25 -22
- package/dist/commands/command/portable-template.js +4 -26
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env-binding.js +10 -3
- package/dist/commands/env/env-cli.js +7 -0
- package/dist/commands/env/secret-cli.js +15 -4
- package/dist/commands/health/checks.js +209 -78
- package/dist/commands/health/improve-metrics.js +12 -0
- package/dist/commands/health.js +16 -4
- package/dist/commands/improve/distill/quality-gate.js +15 -7
- package/dist/commands/improve/distill.js +28 -12
- package/dist/commands/improve/eval-cases.js +9 -2
- package/dist/commands/improve/execution.js +1 -2
- package/dist/commands/improve/extract.js +82 -56
- package/dist/commands/improve/improve-strategies.js +26 -8
- package/dist/commands/improve/improve.js +32 -4
- package/dist/commands/improve/loop-stages.js +13 -3
- package/dist/commands/improve/preparation.js +9 -6
- package/dist/commands/improve/reflect.js +61 -77
- package/dist/commands/lint/index.js +3 -1
- package/dist/commands/migrate-cli.js +6 -4
- package/dist/commands/proposal/drain-policies.js +22 -2
- package/dist/commands/proposal/repository.js +4 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +23 -2
- package/dist/commands/proposal/validators/proposals.js +10 -19
- package/dist/commands/read/show.js +42 -31
- package/dist/commands/registry-cli.js +4 -2
- package/dist/commands/sources/init.js +4 -8
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-clone.js +5 -7
- package/dist/commands/sources/sources-cli.js +3 -5
- package/dist/commands/tasks/tasks-cli.js +36 -12
- package/dist/commands/tasks/tasks.js +38 -35
- package/dist/commands/tasks/validate.js +186 -0
- package/dist/commands/url-checker.js +75 -16
- package/dist/commands/workflow-cli.js +17 -15
- package/dist/core/activation-policy.js +31 -3
- package/dist/core/adapter/execution-source.js +39 -11
- package/dist/core/asset/stash-meta.js +7 -41
- package/dist/core/bundle-id.js +7 -1
- package/dist/core/common.js +8 -17
- package/dist/core/config/config-schema.js +3 -23
- package/dist/core/config/config-walker.js +50 -4
- package/dist/core/config/config.js +21 -5
- package/dist/core/config/legacy-source-shape-shim.js +79 -0
- package/dist/core/config/schema/embedding.js +2 -2
- package/dist/core/config/schema/engines.js +19 -2
- package/dist/core/config/schema/index-config.js +19 -21
- package/dist/core/config/schema/primitives.js +21 -10
- package/dist/core/config/schema/sources-bundles.js +1 -6
- package/dist/core/errors.js +2 -3
- package/dist/core/improve-result.js +8 -0
- package/dist/core/improve-types.js +17 -0
- package/dist/core/json-schema.js +1 -11
- package/dist/core/maintenance-barrier.js +17 -2
- package/dist/core/paths.js +124 -15
- package/dist/core/state/migrations.js +28 -0
- package/dist/core/state-db.js +28 -1
- package/dist/core/write-source.js +6 -6
- package/dist/indexer/bundle-identity-guard.js +3 -0
- package/dist/indexer/ensure-index.js +5 -0
- package/dist/indexer/indexer.js +11 -3
- package/dist/indexer/lookup/adapter-concept-owner.js +14 -3
- package/dist/indexer/passes/metadata.js +16 -5
- package/dist/indexer/search/search-fields.js +1 -30
- package/dist/indexer/search/search-source.js +3 -2
- package/dist/integrations/agent/engine-resolution.js +107 -4
- package/dist/integrations/agent/execution-lowering.js +15 -2
- package/dist/integrations/agent/model-map.js +16 -10
- package/dist/integrations/agent/prompts.js +13 -6
- package/dist/integrations/agent/runner-dispatch.js +16 -3
- package/dist/integrations/agent/runner.js +2 -0
- package/dist/integrations/lockfile.js +22 -7
- package/dist/llm/client.js +16 -0
- package/dist/llm/index-passes.js +3 -2
- package/dist/output/shapes/passthrough.js +10 -3
- package/dist/output/shapes.js +5 -3
- package/dist/output/text/workflow-format.js +8 -1
- package/dist/scripts/akm-migrate-node.js +2028 -1613
- package/dist/scripts/akm-migrate.js +2027 -1612
- package/dist/setup/setup.js +14 -21
- package/dist/sources/include.js +150 -20
- package/dist/sources/providers/git-install.js +14 -12
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/snapshot-fetchers/website-ingest.js +54 -16
- package/dist/sources/website-url.js +12 -4
- package/dist/storage/engines/sqlite-migrations.js +40 -10
- package/dist/storage/like-pattern.js +7 -0
- package/dist/storage/repositories/extract-sessions-repository.js +23 -0
- package/dist/storage/repositories/index-connection.js +27 -10
- package/dist/storage/repositories/index-entry-schema.js +19 -2
- package/dist/storage/repositories/index-schema.js +30 -9
- package/dist/storage/repositories/proposals-repository.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +14 -7
- package/dist/storage/repositories/workflow-runs-repository.js +15 -1
- package/dist/storage/sqlite-read-snapshot.js +11 -9
- package/dist/tasks/backends/cron.js +34 -5
- package/dist/tasks/backends/launchd.js +23 -26
- package/dist/tasks/backends/schtasks.js +50 -3
- package/dist/tasks/frozen-script.js +2 -0
- package/dist/tasks/prepare/prepare.js +2 -7
- package/dist/tasks/prepare/script-capture.js +38 -6
- package/dist/tasks/schedule.js +154 -13
- package/dist/tasks/scheduler-sync.js +51 -25
- package/dist/tasks/source/task-source-v3-frozen.js +0 -1
- package/dist/tasks/source/task-source-v4.js +0 -1
- package/dist/workflows/exec/child-workflow.js +2 -3
- package/dist/workflows/exec/dispatch-redaction.js +21 -7
- package/dist/workflows/exec/exec-unit.js +3 -4
- package/dist/workflows/exec/run-workflow.js +19 -10
- package/dist/workflows/exec/step-work.js +35 -56
- package/dist/workflows/freeze/resolve-steps.js +19 -11
- package/dist/workflows/freeze/source-freeze.js +7 -0
- package/dist/workflows/freeze/targets/child-workflow.js +12 -18
- package/dist/workflows/freeze/targets/command.js +14 -2
- package/dist/workflows/ir/environment-v4.js +4 -2
- package/dist/workflows/ir/freeze-v4.js +2 -5
- package/dist/workflows/ir/plan-hash.js +0 -3
- package/dist/workflows/ir/schema-v4.js +14 -9
- package/dist/workflows/ir/schema.js +1 -3
- package/dist/workflows/resource-limits.js +35 -48
- package/dist/workflows/runtime/plan-classifier.js +89 -41
- package/dist/workflows/runtime/run-outputs.js +1 -21
- package/dist/workflows/runtime/runs.js +76 -150
- package/dist/workflows/source-files.js +28 -54
- package/dist/workflows/source-ir/program.js +2 -2
- package/dist/workflows/source-ir/semantics.js +5 -23
- package/docs/integration/bundling-akm.md +1 -1
- package/docs/migration/v0.8-to-v0.9.md +32 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +20 -0
- package/docs/reference/cli.md +105 -22
- package/docs/reference/configuration.md +12 -2
- package/docs/reference/data-and-telemetry.md +1 -1
- package/docs/reference/tasks.md +8 -0
- package/package.json +1 -1
- package/schemas/akm-config.json +11 -8
package/dist/core/json-schema.js
CHANGED
|
@@ -61,15 +61,9 @@ import { isRecord } from "./common.js";
|
|
|
61
61
|
* same schema tree, so they share one bound.
|
|
62
62
|
*/
|
|
63
63
|
const MAX_DEFINITION_DEPTH = 64;
|
|
64
|
-
/** Total (schema node × value node) visits one {@link validateJsonSchemaSubset} call may make. */
|
|
65
|
-
const MAX_VALIDATION_NODES = 100_000;
|
|
66
64
|
export function validateJsonSchemaSubset(value, schema, options) {
|
|
67
65
|
const errors = [];
|
|
68
|
-
|
|
69
|
-
validateNode(value, schema, "$", { errors, budget, depth: 0, redactValues: options?.redactValues ?? false });
|
|
70
|
-
if (budget.nodes < 0) {
|
|
71
|
-
errors.push(`$: schema evaluation exceeded the limit of ${MAX_VALIDATION_NODES} checks and was stopped`);
|
|
72
|
-
}
|
|
66
|
+
validateNode(value, schema, "$", { errors, depth: 0, redactValues: options?.redactValues ?? false });
|
|
73
67
|
return errors;
|
|
74
68
|
}
|
|
75
69
|
/** Human-readable list of the keywords {@link validateJsonSchemaSubset} enforces (for error messages). */
|
|
@@ -372,10 +366,6 @@ function validateNode(value, schema, path, ctx) {
|
|
|
372
366
|
errors.push(`${path}: schema nesting exceeds the depth limit of ${MAX_DEFINITION_DEPTH}`);
|
|
373
367
|
return;
|
|
374
368
|
}
|
|
375
|
-
// Fail CLOSED: a truncated evaluation never returns "valid" — the counter
|
|
376
|
-
// going negative is what the wrapper turns into a top-level error.
|
|
377
|
-
if (--ctx.budget.nodes < 0)
|
|
378
|
-
return;
|
|
379
369
|
const actual = typeOf(value);
|
|
380
370
|
const declared = schema.type;
|
|
381
371
|
if (typeof declared === "string" || Array.isArray(declared)) {
|
|
@@ -10,6 +10,20 @@ import { ConfigError } from "./errors.js";
|
|
|
10
10
|
import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync } from "./file-lock.js";
|
|
11
11
|
import { getMaintenanceBarrierPath } from "./paths.js";
|
|
12
12
|
const heldBarrierContext = new AsyncLocalStorage();
|
|
13
|
+
/**
|
|
14
|
+
* The barrier is meant to be held only for the short critical section that
|
|
15
|
+
* registers one lock/lease/activity — a process that still holds it past
|
|
16
|
+
* this age is wedged (crashed mid-section, deadlocked, killed without
|
|
17
|
+
* cleanup), not doing legitimate long-running work. Without an age bound, a
|
|
18
|
+
* probe only reclaims a lock whose holder PID has verifiably died; a wedged
|
|
19
|
+
* — but still-alive — holder (or a PID a container/namespace boundary
|
|
20
|
+
* reused, making `isProcessAlive` see the wrong process as live) locked
|
|
21
|
+
* every other akm invocation out of ANY maintenance registration forever,
|
|
22
|
+
* with no recovery but killing the holder by hand. 5 minutes matches the
|
|
23
|
+
* stale-lock window already used for the improve extract-session lock
|
|
24
|
+
* (`commands/improve/extract.ts`).
|
|
25
|
+
*/
|
|
26
|
+
const MAINTENANCE_BARRIER_STALE_AFTER_MS = 5 * 60 * 1000;
|
|
13
27
|
/**
|
|
14
28
|
* Serialize the short critical section that creates each long-lived AKM lock,
|
|
15
29
|
* lease, or state activity. The operation keeps its own ownership record; this
|
|
@@ -23,7 +37,7 @@ export function tryAcquireMaintenanceBarrier() {
|
|
|
23
37
|
if (ownership) {
|
|
24
38
|
return () => releaseLock(ownership);
|
|
25
39
|
}
|
|
26
|
-
const probe = probeLock(lockPath);
|
|
40
|
+
const probe = probeLock(lockPath, { staleAfterMs: MAINTENANCE_BARRIER_STALE_AFTER_MS });
|
|
27
41
|
if (probe.state !== "stale" || !reclaimStaleLock(lockPath, probe))
|
|
28
42
|
return undefined;
|
|
29
43
|
}
|
|
@@ -33,7 +47,8 @@ export function acquireMaintenanceBarrier() {
|
|
|
33
47
|
const release = tryAcquireMaintenanceBarrier();
|
|
34
48
|
if (release)
|
|
35
49
|
return release;
|
|
36
|
-
throw new ConfigError(`AKM maintenance is in progress (barrier ${getMaintenanceBarrierPath()}); retry after it completes
|
|
50
|
+
throw new ConfigError(`AKM maintenance is in progress (barrier ${getMaintenanceBarrierPath()}); retry after it completes. ` +
|
|
51
|
+
`A sentinel older than ${MAINTENANCE_BARRIER_STALE_AFTER_MS / 60_000} minute(s) is reclaimed automatically on the next attempt.`, "INVALID_CONFIG_FILE");
|
|
37
52
|
}
|
|
38
53
|
export function withMaintenanceStartBarrier(run) {
|
|
39
54
|
if (heldBarrierContext.getStore()?.active)
|
package/dist/core/paths.js
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import os from "node:os";
|
|
12
12
|
import path from "node:path";
|
|
13
|
+
import { shortHash } from "./bundle-id.js";
|
|
13
14
|
import { ConfigError } from "./errors.js";
|
|
14
15
|
import { IS_WINDOWS } from "./platform.js";
|
|
16
|
+
import { warnOnce } from "./warn.js";
|
|
15
17
|
/**
|
|
16
18
|
* Returns true when the current process appears to be running under
|
|
17
19
|
* `bun test` (either via the BUN_TEST sentinel Bun sets on the test
|
|
@@ -257,6 +259,117 @@ export function getRegistryCacheDir() {
|
|
|
257
259
|
export function getRegistryIndexCacheDir() {
|
|
258
260
|
return path.join(getCacheDir(), "registry-index");
|
|
259
261
|
}
|
|
262
|
+
// ── State directory ──────────────────────────────────────────────────────────
|
|
263
|
+
/**
|
|
264
|
+
* Returns the XDG state directory for akm (`~/.local/state/akm` on
|
|
265
|
+
* Linux/macOS, `%LOCALAPPDATA%\akm\state` on Windows).
|
|
266
|
+
*
|
|
267
|
+
* Per-machine runtime state that is neither durable application data
|
|
268
|
+
* (`$DATA`), a purely regenerable cache (`$CACHE`), nor state that must
|
|
269
|
+
* travel with a bundle's own content (`$STASH/.akm`; see "What May Live in
|
|
270
|
+
* $STASH/.akm" in docs/architecture/internals/storage-locations.md).
|
|
271
|
+
*
|
|
272
|
+
* Env overrides (in priority order):
|
|
273
|
+
* AKM_STATE_DIR — point to any directory
|
|
274
|
+
* XDG_STATE_HOME — (Linux/macOS) override the XDG base; akm subdir is appended
|
|
275
|
+
*/
|
|
276
|
+
export function getStateDir(env = process.env, platform = process.platform) {
|
|
277
|
+
const override = env.AKM_STATE_DIR?.trim();
|
|
278
|
+
if (override)
|
|
279
|
+
return override;
|
|
280
|
+
if (platform === "win32") {
|
|
281
|
+
const localAppData = env.LOCALAPPDATA?.trim();
|
|
282
|
+
if (localAppData)
|
|
283
|
+
return path.join(localAppData, "akm", "state");
|
|
284
|
+
const userProfile = env.USERPROFILE?.trim();
|
|
285
|
+
if (userProfile)
|
|
286
|
+
return path.join(userProfile, "AppData", "Local", "akm", "state");
|
|
287
|
+
const appData = env.APPDATA?.trim();
|
|
288
|
+
if (!appData) {
|
|
289
|
+
throw new ConfigError("Unable to determine state directory. Set LOCALAPPDATA, USERPROFILE, or APPDATA.", "CONFIG_DIR_UNRESOLVABLE");
|
|
290
|
+
}
|
|
291
|
+
return path.join(appData, "..", "Local", "akm", "state");
|
|
292
|
+
}
|
|
293
|
+
const xdgStateHome = env.XDG_STATE_HOME?.trim();
|
|
294
|
+
if (xdgStateHome)
|
|
295
|
+
return path.join(xdgStateHome, "akm");
|
|
296
|
+
const home = env.HOME?.trim();
|
|
297
|
+
if (!home)
|
|
298
|
+
return homelessFallbackDir("akm-state");
|
|
299
|
+
return path.join(home, ".local", "state", "akm");
|
|
300
|
+
}
|
|
301
|
+
// ── Per-stash state under $STATE / $CACHE (itlackey/akm#890) ─────────────────
|
|
302
|
+
/**
|
|
303
|
+
* Deterministic, filesystem-safe key for machine-local state that belongs to
|
|
304
|
+
* one stash but must not live under `$STASH` itself. Namespaces `$STATE`/
|
|
305
|
+
* `$CACHE` writers by the resolved absolute stash directory — the filesystem
|
|
306
|
+
* counterpart of how `state.db`'s `proposals` table keys per-stash rows by
|
|
307
|
+
* its `stash_dir` column — so two stashes on one machine never collide.
|
|
308
|
+
*
|
|
309
|
+
* Reuses {@link shortHash} (bundle-id.ts) rather than growing a second
|
|
310
|
+
* sha256-truncated-hex helper. (The other existing option,
|
|
311
|
+
* `getCurrentWorkflowScopeKey`'s `dir:v1:<sha256>` in
|
|
312
|
+
* `src/workflows/authoring/scope-key.ts`, is a state.db column value for a
|
|
313
|
+
* different subsystem — full untruncated hex plus a `dir:v1:` prefix, which
|
|
314
|
+
* is neither filesystem-safe as a directory segment on Windows (`:`) nor
|
|
315
|
+
* short, so it is not a fit here.) `shortHash` trims to 8 hex chars (32
|
|
316
|
+
* bits) — fine at its original call site because a collision only ever
|
|
317
|
+
* shortens a batch-unique slug suffix that a retry loop immediately
|
|
318
|
+
* re-disambiguates. Here there is no such retry loop, but a machine
|
|
319
|
+
* realistically has, at most, a handful to a few dozen distinct stash
|
|
320
|
+
* directories ever registered — 32 bits of digest space makes an accidental
|
|
321
|
+
* collision between two of them astronomically unlikely.
|
|
322
|
+
*/
|
|
323
|
+
export function getStashStateKey(stashDir) {
|
|
324
|
+
const resolved = path.resolve(stashDir).replace(/\\/g, "/");
|
|
325
|
+
const normalized = IS_WINDOWS ? resolved.toLowerCase() : resolved;
|
|
326
|
+
return shortHash(normalized);
|
|
327
|
+
}
|
|
328
|
+
function stashScopedDir(base, stashDir) {
|
|
329
|
+
return path.join(base, getStashStateKey(stashDir));
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* `$STATE/improve/distill-rejected/<stash>/` — lessons that failed the
|
|
333
|
+
* distill quality gate. Moved out of `$STASH/.akm/distill-rejected/`
|
|
334
|
+
* (itlackey/akm#890): nothing reads it to resolve bundle content, so it does
|
|
335
|
+
* not meet the "must travel with the content" rule.
|
|
336
|
+
*/
|
|
337
|
+
export function getDistillRejectedDir(stashDir) {
|
|
338
|
+
return stashScopedDir(path.join(getStateDir(), "improve", "distill-rejected"), stashDir);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* `$STATE/improve/eval-cases/<stash>/` — regression eval cases captured from
|
|
342
|
+
* rejected distill/proposal output. Moved out of `$STASH/.akm/eval-cases/`
|
|
343
|
+
* (itlackey/akm#890).
|
|
344
|
+
*/
|
|
345
|
+
export function getEvalCasesDir(stashDir) {
|
|
346
|
+
return stashScopedDir(path.join(getStateDir(), "improve", "eval-cases"), stashDir);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* `$STATE/improve/measurement/verdicts/<stash>/` — `akm-eval-proactive-verdict`
|
|
350
|
+
* reports. Moved out of `$STASH/.akm/measurement/verdicts/` (itlackey/akm#890);
|
|
351
|
+
* the pilot treatment file at `$STASH/.akm/measurement/` is manually-authored
|
|
352
|
+
* measurement input and stays put.
|
|
353
|
+
*/
|
|
354
|
+
export function getMeasurementVerdictsDir(stashDir) {
|
|
355
|
+
return stashScopedDir(path.join(getStateDir(), "improve", "measurement", "verdicts"), stashDir);
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* `$CACHE/index/unresolved-sources/<stash>/` — synthetic placeholder path for
|
|
359
|
+
* a configured source whose content root did not resolve this run. Never
|
|
360
|
+
* written to disk; only used as a stable, reportable `SearchSource.path`.
|
|
361
|
+
* Moved out of `$STASH/.akm/unresolved-sources/` (itlackey/akm#890).
|
|
362
|
+
*/
|
|
363
|
+
export function getUnresolvedSourcesDir(stashDir) {
|
|
364
|
+
return stashScopedDir(path.join(getCacheDir(), "index", "unresolved-sources"), stashDir);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* `$STATE/locks/<stash>/` — per-stash operational lock files for the improve
|
|
368
|
+
* pipeline (e.g. `improve.lock`). Moved out of `$STASH/.akm/` (itlackey/akm#890).
|
|
369
|
+
*/
|
|
370
|
+
export function getStashLocksDir(stashDir) {
|
|
371
|
+
return stashScopedDir(path.join(getStateDir(), "locks"), stashDir);
|
|
372
|
+
}
|
|
260
373
|
// ── Scheduled-task runtime directories (logs + history) ──────────────────────
|
|
261
374
|
export function getTaskLogDir() {
|
|
262
375
|
return path.join(getCacheDir(), "tasks", "logs");
|
|
@@ -312,7 +425,6 @@ export function assertSafeStashDir(stashDir, env = process.env) {
|
|
|
312
425
|
const SYSTEM_ROOTS = new Set([
|
|
313
426
|
"/etc",
|
|
314
427
|
"/var",
|
|
315
|
-
"/var/tmp",
|
|
316
428
|
"/usr",
|
|
317
429
|
"/usr/local",
|
|
318
430
|
"/opt",
|
|
@@ -336,6 +448,9 @@ export function assertSafeStashDir(stashDir, env = process.env) {
|
|
|
336
448
|
if (SYSTEM_ROOTS.has(resolved)) {
|
|
337
449
|
throw new ConfigError(`Refusing stashDir at system path (${resolved}). Pick a path inside your home directory.`, "UNSAFE_STASH_DIR");
|
|
338
450
|
}
|
|
451
|
+
if (resolved === "/var/tmp") {
|
|
452
|
+
warnOnce("stash-dir:var-tmp", `Stash directory is at ${resolved}, a shared scratch directory system cleanup jobs may periodically empty; using it as configured.`);
|
|
453
|
+
}
|
|
339
454
|
// User home — exact match only. Subdirs (~/akm, ~/work/stash) are fine.
|
|
340
455
|
// Check BOTH the env-controlled home and the OS-reported home, so the
|
|
341
456
|
// refusal can't be bypassed by unsetting HOME, and so it still fires
|
|
@@ -353,27 +468,21 @@ export function assertSafeStashDir(stashDir, env = process.env) {
|
|
|
353
468
|
catch {
|
|
354
469
|
// os.homedir() can throw on misconfigured systems; ignore.
|
|
355
470
|
}
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
".local",
|
|
359
|
-
".cache",
|
|
360
|
-
".ssh",
|
|
361
|
-
".gnupg",
|
|
362
|
-
".aws",
|
|
363
|
-
".kube",
|
|
364
|
-
".docker",
|
|
365
|
-
"Documents",
|
|
366
|
-
"Downloads",
|
|
367
|
-
"AppData",
|
|
368
|
-
];
|
|
471
|
+
const CREDENTIAL_USER_PARENTS = [".config", ".local", ".cache", ".ssh", ".gnupg", ".aws", ".kube", ".docker"];
|
|
472
|
+
const PLAIN_USER_DATA_PARENTS = ["Documents", "Downloads", "AppData"];
|
|
369
473
|
for (const home of candidateHomes) {
|
|
370
474
|
if (resolved === home) {
|
|
371
475
|
throw new ConfigError(`Refusing stashDir at your home directory (${resolved}). Pick a subdirectory like ~/akm.`, "UNSAFE_STASH_DIR");
|
|
372
476
|
}
|
|
373
|
-
for (const sub of
|
|
477
|
+
for (const sub of CREDENTIAL_USER_PARENTS) {
|
|
374
478
|
if (resolved === path.join(home, sub)) {
|
|
375
479
|
throw new ConfigError(`Refusing stashDir at sensitive user directory (${resolved}). Pick a subdirectory or a dedicated workspace.`, "UNSAFE_STASH_DIR");
|
|
376
480
|
}
|
|
377
481
|
}
|
|
482
|
+
for (const sub of PLAIN_USER_DATA_PARENTS) {
|
|
483
|
+
if (resolved === path.join(home, sub)) {
|
|
484
|
+
warnOnce(`stash-dir:plain-user-data:${sub}`, `Stash directory is at ${resolved}, your ${sub} folder; using it as configured, though it is usually a large, unrelated-content directory to index.`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
378
487
|
}
|
|
379
488
|
}
|
|
@@ -37,6 +37,7 @@ export const STATE_MIGRATION_SAFETY_BY_ID = Object.freeze({
|
|
|
37
37
|
"024-workflow-run-outputs": "additive",
|
|
38
38
|
"025-task-history-vocabulary-backfill": "data-preserving-rebuild",
|
|
39
39
|
"026-proposals-strip-legacy-fragment-refs": "data-preserving-rebuild",
|
|
40
|
+
"027-extract-sessions-seen-harness-rename": "data-preserving-rebuild",
|
|
40
41
|
});
|
|
41
42
|
export const STATE_MIGRATIONS = [
|
|
42
43
|
// ── Migration 001 — initial schema ──────────────────────────────────────────
|
|
@@ -1149,6 +1150,33 @@ export const STATE_MIGRATIONS = [
|
|
|
1149
1150
|
WHERE ref LIKE '%#%';
|
|
1150
1151
|
`,
|
|
1151
1152
|
},
|
|
1153
|
+
// ── Migration 027 — claude-code -> claude harness rename (#915) ──
|
|
1154
|
+
//
|
|
1155
|
+
// The 0.9.2 rename shipped without a data migration, so rows written under
|
|
1156
|
+
// the old key were invisible to every reader keyed on "claude".
|
|
1157
|
+
//
|
|
1158
|
+
// `extract_sessions_seen` has PRIMARY KEY (harness, session_id), so a
|
|
1159
|
+
// session already recorded under "claude" (written by a run that really
|
|
1160
|
+
// happened post-rename) collides with its "claude-code" counterpart on
|
|
1161
|
+
// rename. `UPDATE OR IGNORE` keeps the newer "claude" row exactly as it
|
|
1162
|
+
// was — the older row's outcome is superseded, not more correct — and the
|
|
1163
|
+
// trailing DELETE drops that now-unreachable duplicate so the "claude-code"
|
|
1164
|
+
// key space is fully empty afterwards, not just mostly-migrated.
|
|
1165
|
+
//
|
|
1166
|
+
// `workflow_runs.agent_harness` (migration 020) has no uniqueness
|
|
1167
|
+
// constraint on that column, so a plain UPDATE is sufficient there. Both
|
|
1168
|
+
// statements are kept in one migration so the rename is a single ledger
|
|
1169
|
+
// event. `improve_runs.result_json` is deliberately untouched: the harness
|
|
1170
|
+
// name embedded in those JSON blobs is reporting data about a past run, not
|
|
1171
|
+
// a lookup key, so rewriting it would not fix anything a reader depends on.
|
|
1172
|
+
{
|
|
1173
|
+
id: "027-extract-sessions-seen-harness-rename",
|
|
1174
|
+
up: `
|
|
1175
|
+
UPDATE OR IGNORE extract_sessions_seen SET harness = 'claude' WHERE harness = 'claude-code';
|
|
1176
|
+
DELETE FROM extract_sessions_seen WHERE harness = 'claude-code';
|
|
1177
|
+
UPDATE workflow_runs SET agent_harness = 'claude' WHERE agent_harness = 'claude-code';
|
|
1178
|
+
`,
|
|
1179
|
+
},
|
|
1152
1180
|
];
|
|
1153
1181
|
assertMigrationRegistry(STATE_MIGRATIONS);
|
|
1154
1182
|
function assertStateMigrationSafetyRegistry() {
|
package/dist/core/state-db.js
CHANGED
|
@@ -69,9 +69,11 @@ import { sleepSync } from "../runtime.js";
|
|
|
69
69
|
import { openDatabase } from "../storage/database.js";
|
|
70
70
|
import { assertMigrationLedger } from "../storage/engines/sqlite-migrations.js";
|
|
71
71
|
import { openManagedDatabase, withManagedDb } from "../storage/managed-db.js";
|
|
72
|
+
import { pkgVersion } from "../version.js";
|
|
72
73
|
import { acquireMaintenanceActivitySync } from "./maintenance-barrier.js";
|
|
73
74
|
import { getDataDir } from "./paths.js";
|
|
74
75
|
import { runMigrations, STATE_MIGRATIONS } from "./state/migrations.js";
|
|
76
|
+
import { warnOnce } from "./warn.js";
|
|
75
77
|
// ── Path helper ──────────────────────────────────────────────────────────────
|
|
76
78
|
/**
|
|
77
79
|
* Default path: `<dataDir>/state.db`.
|
|
@@ -370,6 +372,25 @@ function createHistoricalStateSafetyCopy(source, migrationId) {
|
|
|
370
372
|
* matches the value used in openDatabase() for index.db; 5 s proved too
|
|
371
373
|
* narrow when a post-inference reindex overlapped a parallel event write.
|
|
372
374
|
*/
|
|
375
|
+
/**
|
|
376
|
+
* Tell the operator once when state.db was migrated by a newer akm than the
|
|
377
|
+
* one running. The open proceeds: every migration this binary knows is already
|
|
378
|
+
* applied, so it reads and writes the tables it knows. Commands that depend on
|
|
379
|
+
* something a later migration changed may still report less than the truth,
|
|
380
|
+
* which is why this is said out loud rather than swallowed.
|
|
381
|
+
*/
|
|
382
|
+
function warnNewerStateLedger(ledger) {
|
|
383
|
+
if (ledger.status !== "newer")
|
|
384
|
+
return;
|
|
385
|
+
warnOnce("state-db-newer-ledger", `[state.db] This akm (v${pkgVersion}) is older than the state database: ${ledger.detail}. ` +
|
|
386
|
+
"Continuing with the schema this version knows; upgrade akm if its output looks incomplete.");
|
|
387
|
+
}
|
|
388
|
+
function unversionedDatabaseHasNoTables(db) {
|
|
389
|
+
const tables = db
|
|
390
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != ?")
|
|
391
|
+
.get("schema_migrations");
|
|
392
|
+
return !tables;
|
|
393
|
+
}
|
|
373
394
|
export function openStateDatabase(dbPath, options) {
|
|
374
395
|
const canonicalPath = getStateDbPath();
|
|
375
396
|
const resolvedPath = dbPath ?? canonicalPath;
|
|
@@ -392,6 +413,7 @@ export function openStateDatabase(dbPath, options) {
|
|
|
392
413
|
let existingSource;
|
|
393
414
|
let openedDb;
|
|
394
415
|
let existingUnversionedDatabase = false;
|
|
416
|
+
let treatUnversionedAsFresh = false;
|
|
395
417
|
let stateSafetyCopyCreated = false;
|
|
396
418
|
try {
|
|
397
419
|
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
@@ -404,7 +426,12 @@ export function openStateDatabase(dbPath, options) {
|
|
|
404
426
|
try {
|
|
405
427
|
preflight.exec("PRAGMA busy_timeout = 30000");
|
|
406
428
|
const ledger = assertMigrationLedger(preflight, STATE_MIGRATIONS);
|
|
429
|
+
warnNewerStateLedger(ledger);
|
|
407
430
|
existingUnversionedDatabase = ledger.migrationIds.length === 0;
|
|
431
|
+
if (existingUnversionedDatabase && unversionedDatabaseHasNoTables(preflight)) {
|
|
432
|
+
existingUnversionedDatabase = false;
|
|
433
|
+
treatUnversionedAsFresh = true;
|
|
434
|
+
}
|
|
408
435
|
if (existingUnversionedDatabase && !options?.allowHistoricalDestructiveStateUpgrade) {
|
|
409
436
|
throw new Error("Refusing to migrate an existing unversioned state.db during an ordinary managed open. " +
|
|
410
437
|
"Run `akm upgrade` (or `akm migrate apply`) to create a verified snapshot before migration 001 " +
|
|
@@ -421,7 +448,7 @@ export function openStateDatabase(dbPath, options) {
|
|
|
421
448
|
pragmas: { dataDir: path.dirname(resolvedPath) },
|
|
422
449
|
init: (db) => {
|
|
423
450
|
runMigrations(db, {
|
|
424
|
-
freshDatabase: !!freshReservation,
|
|
451
|
+
freshDatabase: !!freshReservation || treatUnversionedAsFresh,
|
|
425
452
|
existingUnversionedDatabase,
|
|
426
453
|
allowHistoricalDestructiveStateUpgrade: options?.allowHistoricalDestructiveStateUpgrade,
|
|
427
454
|
beforeExistingUnversionedStateMigration: options?.allowHistoricalDestructiveStateUpgrade
|
|
@@ -37,7 +37,7 @@ import { existingFileMode, isWithin, resolveStashDir, writeFileAtomic } from "./
|
|
|
37
37
|
import { bundleContentRoot, resolveConfiguredSources } from "./config/config.js";
|
|
38
38
|
import { ConfigError, UsageError } from "./errors.js";
|
|
39
39
|
import { sanitizeCommitMessage } from "./git-message.js";
|
|
40
|
-
import { warn } from "./warn.js";
|
|
40
|
+
import { warn, warnOnce } from "./warn.js";
|
|
41
41
|
import { recordWrittenPath } from "./write-provenance.js";
|
|
42
42
|
/**
|
|
43
43
|
* Source kinds that the loader is allowed to mark `writable: true`. Anything
|
|
@@ -872,10 +872,10 @@ export function prepareWriteTargetForMutation(target, options = {}) {
|
|
|
872
872
|
}
|
|
873
873
|
const upstream = inspectGitUpstream(repoPath);
|
|
874
874
|
if (upstream.behind > 0) {
|
|
875
|
-
|
|
875
|
+
warnOnce(`write-source:git-behind:${realRepoPath}`, `Writable Git target "${target.source.name}" is ${upstream.behind} commit(s) behind ${upstream.upstream}; writing anyway. Run \`akm bundle update ${target.source.name}\` to catch up.`);
|
|
876
876
|
}
|
|
877
|
-
if (upstream.ahead > 0
|
|
878
|
-
|
|
877
|
+
if (upstream.ahead > 0) {
|
|
878
|
+
warnOnce(`write-source:git-ahead:${realRepoPath}`, `Writable Git target "${target.source.name}" has ${upstream.ahead} unpushed commit(s); writing another on top. Push or reconcile them when convenient.`);
|
|
879
879
|
}
|
|
880
880
|
return {
|
|
881
881
|
...target,
|
|
@@ -997,7 +997,7 @@ const WINDOWS_RESERVED_DEVICE_NAMES = new Set([
|
|
|
997
997
|
function resolveAssetFilePath(source, ref) {
|
|
998
998
|
const basename = path.posix.basename(ref.name.replaceAll("\\", "/")).replace(/\.md$/i, "").toLowerCase();
|
|
999
999
|
if (basename === "index" || basename === "log") {
|
|
1000
|
-
|
|
1000
|
+
warnOnce(`write-source:reserved-basename:${basename}`, `Concept name "${basename}" collides with a reserved word some tooling treats specially; writing it anyway.`);
|
|
1001
1001
|
}
|
|
1002
1002
|
// Windows resolves these names as DEVICES no matter the directory or the
|
|
1003
1003
|
// extension, so `CON.md` is not a file — a write goes to the console and a
|
|
@@ -1005,7 +1005,7 @@ function resolveAssetFilePath(source, ref) {
|
|
|
1005
1005
|
// portable: an asset authored on Linux must not become unopenable when the
|
|
1006
1006
|
// same bundle is used on Windows.
|
|
1007
1007
|
if (WINDOWS_RESERVED_DEVICE_NAMES.has(basename)) {
|
|
1008
|
-
|
|
1008
|
+
warnOnce(`write-source:windows-device-name:${basename}`, `Asset name "${basename}" is a reserved Windows device name; writing it anyway, but this bundle will not be portable to Windows.`);
|
|
1009
1009
|
}
|
|
1010
1010
|
const typeDir = stashDirFor(ref.type);
|
|
1011
1011
|
if (!typeDir) {
|
|
@@ -33,6 +33,7 @@ import { rethrowIfTestIsolationError } from "../core/errors.js";
|
|
|
33
33
|
import { getDbPath } from "../core/paths.js";
|
|
34
34
|
import { warn } from "../core/warn.js";
|
|
35
35
|
import { closeDatabase, openReadonlyExistingDatabase } from "../storage/repositories/index-connection.js";
|
|
36
|
+
import { isCanonicalIndexGeneration } from "../storage/repositories/index-entry-schema.js";
|
|
36
37
|
let guardSettled = false;
|
|
37
38
|
/** TEST-ONLY: re-arm the once-per-process guard between cases. */
|
|
38
39
|
export function resetBundleIdentityGuardForTests() {
|
|
@@ -47,6 +48,8 @@ function indexBundlePrefixes(dbPath) {
|
|
|
47
48
|
db = openReadonlyExistingDatabase(dbPath);
|
|
48
49
|
if (!db)
|
|
49
50
|
return undefined;
|
|
51
|
+
if (!isCanonicalIndexGeneration(db))
|
|
52
|
+
return undefined;
|
|
50
53
|
return db
|
|
51
54
|
.prepare("SELECT DISTINCT bundle_id AS b FROM entries WHERE bundle_id IS NOT NULL AND bundle_id != ''")
|
|
52
55
|
.all().map((row) => row.b);
|
|
@@ -26,6 +26,7 @@ import { classifyPathAccess } from "../core/path-access.js";
|
|
|
26
26
|
import { getDbPath } from "../core/paths.js";
|
|
27
27
|
import { assertIndexPathReadable, closeDatabase, openExistingDatabase } from "../storage/repositories/index-connection.js";
|
|
28
28
|
import { getEntryCount, getIndexedFilePaths } from "../storage/repositories/index-entries-repository.js";
|
|
29
|
+
import { isCanonicalIndexGeneration } from "../storage/repositories/index-entry-schema.js";
|
|
29
30
|
import { getMeta } from "../storage/repositories/index-meta-repository.js";
|
|
30
31
|
import { warnOnBundleRenameDrift } from "./bundle-identity-guard.js";
|
|
31
32
|
function getIndexableFiles(root, spec) {
|
|
@@ -118,6 +119,8 @@ export function isIndexStale(stashDir) {
|
|
|
118
119
|
let db;
|
|
119
120
|
try {
|
|
120
121
|
db = openExistingDatabase(dbPath);
|
|
122
|
+
if (!isCanonicalIndexGeneration(db))
|
|
123
|
+
return true;
|
|
121
124
|
const entryCount = getEntryCount(db);
|
|
122
125
|
if (entryCount === 0)
|
|
123
126
|
return true;
|
|
@@ -164,6 +167,8 @@ function indexCanServeStash(stashDir) {
|
|
|
164
167
|
let db;
|
|
165
168
|
try {
|
|
166
169
|
db = openExistingDatabase(dbPath);
|
|
170
|
+
if (!isCanonicalIndexGeneration(db))
|
|
171
|
+
return false;
|
|
167
172
|
if (getEntryCount(db) === 0)
|
|
168
173
|
return false;
|
|
169
174
|
const storedStashDir = getMeta(db, "stashDir");
|
package/dist/indexer/indexer.js
CHANGED
|
@@ -1770,9 +1770,17 @@ async function lookupBundleRefWithResolutionUsing(ref, openLookupDatabase) {
|
|
|
1770
1770
|
const entry = readLookupEntry(db, id, ref.conceptId, source.path);
|
|
1771
1771
|
if (entry) {
|
|
1772
1772
|
if (owner.workflowSource) {
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1773
|
+
try {
|
|
1774
|
+
assertIndexedWorkflowSourceIdentity(inputRef, entry.filePath, owner.workflowSource);
|
|
1775
|
+
if (entry.adapterId !== adapterId) {
|
|
1776
|
+
throw new WorkflowSourceIdentityError(inputRef, entry.filePath, owner.path);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
catch (error) {
|
|
1780
|
+
if (!(error instanceof WorkflowSourceIdentityError))
|
|
1781
|
+
throw error;
|
|
1782
|
+
warn(`${error.message} Falling back to the physical owner.`);
|
|
1783
|
+
return { entry: null, owner, ...(indexError === undefined ? {} : { indexError }) };
|
|
1776
1784
|
}
|
|
1777
1785
|
}
|
|
1778
1786
|
else if (entry.adapterId !== adapterId || !indexedPathMatchesOwner(entry.filePath, owner)) {
|
|
@@ -7,6 +7,7 @@ import { adapterForId } from "../../core/adapter/registry.js";
|
|
|
7
7
|
import { compareCodePoints, hasErrnoCode, isWithin } from "../../core/common.js";
|
|
8
8
|
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
9
9
|
import { canonicalizeWorkflowName } from "../../core/recognition-util.js";
|
|
10
|
+
import { warnOnce } from "../../core/warn.js";
|
|
10
11
|
import { resolveUniqueWorkflowSource, workflowNameForConceptId, } from "../../workflows/source-files.js";
|
|
11
12
|
import { buildFileContext } from "../walk/file-context.js";
|
|
12
13
|
const CONTENT_READ_REQUIRED = Symbol("adapter ownership probe requires content");
|
|
@@ -123,7 +124,7 @@ function claimsWithoutContent(adapter, component, owner) {
|
|
|
123
124
|
* adapter supplies its own read placements and is probed with a byte-denying
|
|
124
125
|
* FileContext so path-level abstention remains authoritative.
|
|
125
126
|
*/
|
|
126
|
-
export function resolveAdapterConceptOwner(sourcePath, adapterId, conceptId) {
|
|
127
|
+
export function resolveAdapterConceptOwner(sourcePath, adapterId, conceptId, options) {
|
|
127
128
|
const adapter = adapterForId(adapterId);
|
|
128
129
|
const normalized = normalizedConceptId(conceptId);
|
|
129
130
|
if (!adapter || !normalized)
|
|
@@ -185,9 +186,19 @@ export function resolveAdapterConceptOwner(sourcePath, adapterId, conceptId) {
|
|
|
185
186
|
if (claimsWithoutContent(adapter, component, candidate))
|
|
186
187
|
ownersByIdentity.set(identity, candidate);
|
|
187
188
|
}
|
|
188
|
-
const owners = [...ownersByIdentity.values()]
|
|
189
|
+
const owners = [...ownersByIdentity.values()]
|
|
190
|
+
.filter((owner) => owner.conceptId === resolutionConceptId)
|
|
191
|
+
.sort((left, right) => compareCodePoints(left.path, right.path));
|
|
189
192
|
if (owners.length > 1) {
|
|
190
|
-
|
|
193
|
+
if (options?.mode !== "read") {
|
|
194
|
+
throw new AdapterConceptCollisionError(adapterId, resolutionConceptId, owners.map((owner) => path.relative(sourcePath, owner.path).replaceAll("\\", "/")));
|
|
195
|
+
}
|
|
196
|
+
const [winner, ...losers] = owners;
|
|
197
|
+
warnOnce(`adapter-concept-collision:${adapterId}:${resolutionConceptId}`, `Adapter "${adapterId}" has multiple physical owners for "${resolutionConceptId}": ` +
|
|
198
|
+
`${owners.map((owner) => path.relative(sourcePath, owner.path).replaceAll("\\", "/")).join(", ")}. ` +
|
|
199
|
+
`Reading "${path.relative(sourcePath, winner.path).replaceAll("\\", "/")}" and ignoring ` +
|
|
200
|
+
`${losers.map((owner) => path.relative(sourcePath, owner.path).replaceAll("\\", "/")).join(", ")}.`);
|
|
201
|
+
return winner;
|
|
191
202
|
}
|
|
192
203
|
return owners[0];
|
|
193
204
|
}
|
|
@@ -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
|
|
9
|
+
import { isVerbose, warn } from "../../core/warn.js";
|
|
10
10
|
export const SCOPE_KEYS = ["user", "agent", "run", "channel"];
|
|
11
11
|
// ── Quality semantics (v1 spec §4.2) ────────────────────────────────────────
|
|
12
12
|
/**
|
|
@@ -835,10 +835,17 @@ export function isEnrichmentComplete(entry) {
|
|
|
835
835
|
// ── Native Markdown search projection ──────────────────────────────────────
|
|
836
836
|
/**
|
|
837
837
|
* Maximum native Markdown prose carried by the low-weight `content` field.
|
|
838
|
-
*
|
|
839
|
-
*
|
|
838
|
+
*
|
|
839
|
+
* Raised far past any real authored document:
|
|
840
|
+
* this used to sit at 16_384 chars, tight enough that ordinary long-form
|
|
841
|
+
* skills/knowledge docs lost their tail from both FTS and the embedding
|
|
842
|
+
* input with no visible signal (the cut was reported via `warnVerbose`,
|
|
843
|
+
* silent unless `AKM_VERBOSE` was set). The remaining bound exists only to
|
|
844
|
+
* stop a truly pathological single file (a committed data dump, a decompressed
|
|
845
|
+
* log) from ballooning index size — not to shave real content — so a caller
|
|
846
|
+
* that hits it is always told, unconditionally.
|
|
840
847
|
*/
|
|
841
|
-
export const MARKDOWN_CONTENT_MAX_CHARS =
|
|
848
|
+
export const MARKDOWN_CONTENT_MAX_CHARS = 1_000_000;
|
|
842
849
|
/**
|
|
843
850
|
* Locate a leading nested frontmatter block in a body: up to three blank
|
|
844
851
|
* lines, then a `---` line, closed by a later `---` line. Mirrors the
|
|
@@ -1220,7 +1227,11 @@ export function applyPreContributorFields(entry, file, ctx, pkgMeta) {
|
|
|
1220
1227
|
entry.content = contentProjection;
|
|
1221
1228
|
if (truncationInfo.truncated) {
|
|
1222
1229
|
entry.contentTruncated = true;
|
|
1223
|
-
warnVerbose
|
|
1230
|
+
// Unconditional, not warnVerbose: this bound now sits far past any
|
|
1231
|
+
// real document, so tripping it means
|
|
1232
|
+
// something unusual is in the bundle and the operator should see
|
|
1233
|
+
// it without having to pass --verbose.
|
|
1234
|
+
warn(`${file}: indexed content truncated to ${MARKDOWN_CONTENT_MAX_CHARS} chars`);
|
|
1224
1235
|
}
|
|
1225
1236
|
}
|
|
1226
1237
|
}
|
|
@@ -1,18 +1,6 @@
|
|
|
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";
|
|
14
|
-
/** Structured metadata plus bounded body text supplied to embedding providers. */
|
|
15
|
-
export const SEARCH_TEXT_MAX_CHARS = 8_192;
|
|
16
4
|
/**
|
|
17
5
|
* Return per-field search text for multi-column FTS5 indexing.
|
|
18
6
|
*
|
|
@@ -90,25 +78,8 @@ export function buildSearchText(entry) {
|
|
|
90
78
|
const structured = [fields.name, fields.description, fields.tags, fields.hints]
|
|
91
79
|
.filter((field) => field.length > 0)
|
|
92
80
|
.join(" ");
|
|
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)`);
|
|
95
|
-
return truncateUnicodeSafe(structured, SEARCH_TEXT_MAX_CHARS);
|
|
96
|
-
}
|
|
97
81
|
if (!fields.content)
|
|
98
82
|
return structured;
|
|
99
83
|
const separator = structured ? " " : "";
|
|
100
|
-
|
|
101
|
-
if (fields.content.length > remaining) {
|
|
102
|
-
warnVerbose(`${entry.ref ?? entry.name}: search text truncated to ${SEARCH_TEXT_MAX_CHARS} chars`);
|
|
103
|
-
}
|
|
104
|
-
return `${structured}${separator}${truncateUnicodeSafe(fields.content, remaining)}`;
|
|
105
|
-
}
|
|
106
|
-
function truncateUnicodeSafe(text, maxChars) {
|
|
107
|
-
if (text.length <= maxChars)
|
|
108
|
-
return text;
|
|
109
|
-
let cut = text.slice(0, maxChars);
|
|
110
|
-
const lastCode = cut.charCodeAt(cut.length - 1);
|
|
111
|
-
if (lastCode >= 0xd800 && lastCode <= 0xdbff)
|
|
112
|
-
cut = cut.slice(0, -1);
|
|
113
|
-
return cut.trimEnd();
|
|
84
|
+
return `${structured}${separator}${fields.content}`;
|
|
114
85
|
}
|
|
@@ -5,6 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { isWithin, resolveStashDir } from "../../core/common.js";
|
|
7
7
|
import { bundleComponentConfig, bundlesToSourceEntries, getSources, loadConfig } from "../../core/config/config.js";
|
|
8
|
+
import { getUnresolvedSourcesDir } from "../../core/paths.js";
|
|
8
9
|
import { resolveGitContentRoot, resolveWritable } from "../../core/write-source.js";
|
|
9
10
|
import { lockContentRootFor } from "../../integrations/lockfile.js";
|
|
10
11
|
import { resolveSourceProviderFactory } from "../../sources/provider-factory.js";
|
|
@@ -86,14 +87,14 @@ export function resolveSourceEntries(overrideStashDir, existingConfig) {
|
|
|
86
87
|
const component = bundleComponentConfig(config.bundles?.[entry.name ?? ""]);
|
|
87
88
|
const contentRoot = resolveEntryContentDir(entry);
|
|
88
89
|
if (contentRoot == null) {
|
|
89
|
-
const unresolvedPath = path.join(implicitStashDir ?? process.cwd(),
|
|
90
|
+
const unresolvedPath = path.join(getUnresolvedSourcesDir(implicitStashDir ?? process.cwd()), entry.name ?? entry.type);
|
|
90
91
|
addSource(unresolvedPath, entry.name, component?.writable ?? resolveWritable(entry), entry.type, component?.adapter, true);
|
|
91
92
|
continue;
|
|
92
93
|
}
|
|
93
94
|
const dir = path.resolve(contentRoot, component?.root ?? ".");
|
|
94
95
|
if (!isWithin(dir, contentRoot)) {
|
|
95
96
|
warn(`Warning: component root "${component?.root}" escapes bundle "${entry.name}"; skipping source.`);
|
|
96
|
-
const unresolvedPath = path.join(contentRoot,
|
|
97
|
+
const unresolvedPath = path.join(getUnresolvedSourcesDir(contentRoot), entry.name ?? entry.type);
|
|
97
98
|
addSource(unresolvedPath, entry.name, component?.writable ?? resolveWritable(entry), entry.type, component?.adapter, true);
|
|
98
99
|
continue;
|
|
99
100
|
}
|