akm-cli 0.9.1 → 0.9.2-alpha.2
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 +103 -28
- package/README.md +3 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/assets/hints/cli-hints-full.md +14 -9
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/models.json +35 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
- package/dist/assets/tasks/core/extract.yml +6 -5
- package/dist/assets/tasks/core/improve.yml +6 -5
- package/dist/assets/tasks/core/index-refresh.yml +6 -5
- package/dist/assets/tasks/core/sync.yml +6 -5
- package/dist/assets/tasks/core/version-check.yml +6 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
- package/dist/cli/confirm.js +2 -2
- package/dist/cli/parse-args.js +3 -24
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli.js +11 -9
- package/dist/commands/agent/agent-dispatch.js +55 -89
- package/dist/commands/agent/contribute-cli.js +12 -45
- package/dist/commands/command/builtin-action.js +32 -0
- package/dist/commands/command/command-cli.js +99 -0
- package/dist/commands/command/command-execution.js +308 -0
- package/dist/commands/command/execution-source-loader.js +176 -0
- package/dist/commands/command/portable-template.js +60 -0
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env.js +4 -2
- package/dist/commands/feedback-cli.js +1 -1
- package/dist/commands/health/checks.js +241 -29
- package/dist/commands/health/html-report.js +0 -14
- package/dist/commands/health/report-view-model.js +0 -1
- package/dist/commands/health/surfaces.js +6 -7
- package/dist/commands/health/types.js +0 -2
- package/dist/commands/health.js +63 -18
- package/dist/commands/improve/collapse-detector.js +5 -6
- package/dist/commands/improve/consolidate.js +251 -214
- package/dist/commands/improve/distill/promote-memory.js +71 -34
- package/dist/commands/improve/distill/quality-gate.js +17 -5
- package/dist/commands/improve/distill.js +232 -155
- package/dist/commands/improve/eligibility.js +112 -79
- package/dist/commands/improve/execution.js +57 -0
- package/dist/commands/improve/extract-cli.js +5 -5
- package/dist/commands/improve/extract-prompt.js +64 -22
- package/dist/commands/improve/extract.js +608 -360
- package/dist/commands/improve/improve-strategies.js +43 -14
- package/dist/commands/improve/improve.js +249 -29
- package/dist/commands/improve/loop-stages.js +11 -17
- package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
- package/dist/commands/improve/outcome-loop.js +22 -38
- package/dist/commands/improve/planner.js +134 -0
- package/dist/commands/improve/preparation.js +730 -409
- package/dist/commands/improve/reflect.js +386 -223
- package/dist/commands/improve/run-context.js +3 -4
- package/dist/commands/improve/salience.js +6 -58
- package/dist/commands/improve/session-asset.js +12 -12
- package/dist/commands/lint/index.js +101 -29
- package/dist/commands/migrate-cli.js +11 -69
- package/dist/commands/migration-tool.js +6 -9
- package/dist/commands/models-cli.js +27 -0
- package/dist/commands/proposal/drain.js +258 -186
- package/dist/commands/proposal/proposal-cli.js +32 -10
- package/dist/commands/proposal/proposal.js +2 -5
- package/dist/commands/proposal/propose.js +192 -172
- package/dist/commands/proposal/repository.js +54 -91
- package/dist/commands/proposal/validators/proposal-validators.js +9 -7
- package/dist/commands/read/curate.js +53 -22
- package/dist/commands/read/registry-search.js +25 -9
- package/dist/commands/read/remember-cli.js +14 -2
- package/dist/commands/read/search.js +10 -4
- package/dist/commands/read/show.js +139 -153
- package/dist/commands/registry-cli.js +16 -7
- package/dist/commands/remember.js +33 -18
- package/dist/commands/sources/add-cli.js +19 -178
- package/dist/commands/sources/bundle-cli.js +15 -3
- package/dist/commands/sources/dangerous-env-audit.js +135 -0
- package/dist/commands/sources/info.js +2 -1
- package/dist/commands/sources/installed-stashes.js +901 -177
- package/dist/commands/sources/schema-repair.js +174 -95
- package/dist/commands/sources/self-update.js +30 -74
- package/dist/commands/sources/source-add.js +3 -5
- package/dist/commands/sources/sources-cli.js +2 -15
- package/dist/commands/sources/update-transaction.js +220 -0
- package/dist/commands/tasks/tasks-cli.js +3 -3
- package/dist/commands/tasks/tasks.js +736 -317
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
- package/dist/core/adapter/adapters/akm-adapter.js +85 -35
- package/dist/core/adapter/adapters/akm-lint.js +54 -39
- package/dist/core/adapter/adapters/akm-metadata.js +45 -45
- package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
- package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
- package/dist/core/adapter/adapters/index.js +0 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
- package/dist/core/adapter/adapters/okf-adapter.js +4 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
- package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
- package/dist/core/adapter/execution-source.js +308 -0
- package/dist/core/adapter/recognize-match.js +36 -13
- package/dist/core/adapter/registry.js +0 -9
- package/dist/core/asset/stash-meta.js +94 -4
- package/dist/core/common.js +6 -11
- package/dist/core/config/config-io.js +3 -3
- package/dist/core/config/config-schema.js +18 -40
- package/dist/core/config/config-sources.js +11 -21
- package/dist/core/config/config-walker.js +31 -13
- package/dist/core/config/config.js +23 -26
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/index-config.js +0 -27
- package/dist/core/config/schema/primitives.js +1 -23
- package/dist/core/config/schema/sources-bundles.js +13 -16
- package/dist/core/errors.js +2 -0
- package/dist/core/events.js +68 -32
- package/dist/core/extra-params.js +1 -0
- package/dist/core/improve-result.js +315 -0
- package/dist/core/lesson-lint.js +0 -6
- package/dist/core/maintenance-barrier.js +4 -4
- package/dist/core/network-policy.js +152 -0
- package/dist/core/paths.js +1 -1
- package/dist/core/recognition-util.js +4 -4
- package/dist/core/registry-url.js +456 -0
- package/dist/core/state/migrations.js +161 -47
- package/dist/core/state-db.js +453 -80
- package/dist/core/system-error.js +32 -0
- package/dist/core/time.js +2 -12
- package/dist/core/write-source.js +0 -18
- package/dist/execution/directory-identity.js +52 -0
- package/dist/execution/executable-identity.js +107 -0
- package/dist/execution/guarded-source.js +398 -0
- package/dist/execution/json.js +95 -0
- package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
- package/dist/execution/record.js +55 -0
- package/dist/execution/resolved-request.js +730 -0
- package/dist/execution/source.js +320 -0
- package/dist/indexer/bundle-identity-guard.js +5 -4
- package/dist/indexer/db/graph-db.js +33 -0
- package/dist/indexer/graph/graph-boost.js +3 -4
- package/dist/indexer/graph/graph-extraction.js +562 -373
- package/dist/indexer/index-written-assets.js +78 -39
- package/dist/indexer/indexer.js +471 -432
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/materialize-embeddings.js +155 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/passes/metadata.js +263 -118
- package/dist/indexer/scan/doc-to-entry.js +7 -10
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +156 -50
- package/dist/indexer/search/fts-query.js +40 -40
- package/dist/indexer/search/ranking.js +36 -1
- package/dist/indexer/search/search-attribution.js +3 -1
- package/dist/indexer/search/search-fields.js +23 -14
- package/dist/indexer/search/search-hit-enrichers.js +1 -1
- package/dist/indexer/search/search-source.js +7 -16
- package/dist/indexer/search/semantic-status.js +10 -1
- package/dist/indexer/usage/show-usage.js +105 -0
- package/dist/indexer/usage/usage-events.js +7 -2
- package/dist/indexer/walk/matchers.js +40 -10
- package/dist/indexer/walk/path-resolver.js +5 -2
- package/dist/indexer/walk/walker.js +20 -2
- package/dist/integrations/agent/builder-shared.js +3 -6
- package/dist/integrations/agent/conversation-fallback.js +16 -0
- package/dist/integrations/agent/engine-resolution.js +87 -87
- package/dist/integrations/agent/execution-cascade.js +566 -0
- package/dist/integrations/agent/execution-definitions.js +211 -0
- package/dist/integrations/agent/execution-lowering.js +811 -0
- package/dist/integrations/agent/execution-preparation.js +67 -0
- package/dist/integrations/agent/index.js +0 -2
- package/dist/integrations/agent/inline-execution.js +74 -0
- package/dist/integrations/agent/model-map.js +515 -0
- package/dist/integrations/agent/persona-fallback.js +30 -0
- package/dist/integrations/agent/request-lowering.js +186 -0
- package/dist/integrations/agent/runner-dispatch.js +230 -37
- package/dist/integrations/agent/runner.js +12 -83
- package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
- package/dist/integrations/harnesses/claude/index.js +1 -5
- package/dist/integrations/harnesses/claude/session-log.js +3 -33
- package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +4 -44
- package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
- package/dist/integrations/harnesses/opencode/index.js +0 -2
- package/dist/integrations/harnesses/opencode/session-log.js +14 -204
- package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
- package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/shared.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -3
- package/dist/integrations/lockfile.js +82 -79
- package/dist/integrations/session-logs/index.js +6 -17
- package/dist/integrations/session-logs/provider-base.js +1 -29
- package/dist/llm/client.js +10 -5
- package/dist/llm/embedder.js +6 -7
- package/dist/llm/embedders/local.js +37 -88
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +75 -50
- package/dist/llm/index-passes.js +43 -5
- package/dist/llm/memory-infer.js +8 -6
- package/dist/llm/metadata-enhance.js +5 -3
- package/dist/llm/structured-call.js +122 -25
- package/dist/output/format-exempt.js +1 -1
- package/dist/output/render-registry.js +0 -16
- package/dist/output/renderers.js +12 -7
- package/dist/output/shapes/curate.js +1 -0
- package/dist/output/shapes/helpers.js +10 -2
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/command-format.js +31 -33
- package/dist/output/text/health-format.js +1 -29
- package/dist/output/text/migrate.js +6 -56
- package/dist/output/text/proposal-format.js +16 -1
- package/dist/output/text/workflow-format.js +16 -0
- package/dist/registry/network.js +279 -0
- package/dist/registry/pinned-request-helper.js +247 -0
- package/dist/registry/pinned-transport.js +717 -0
- package/dist/registry/providers/skills-sh.js +18 -6
- package/dist/registry/providers/static-index.js +20 -7
- package/dist/registry/resolve.js +53 -28
- package/dist/scripts/akm-migrate-node.js +19334 -52269
- package/dist/scripts/akm-migrate.js +19270 -51612
- package/dist/setup/registry-stash-loader.js +64 -20
- package/dist/setup/semantic-assets.js +9 -34
- package/dist/setup/setup.js +12 -30
- package/dist/setup/source-identity.js +17 -0
- package/dist/setup/steps/sources.js +36 -15
- package/dist/setup/steps/tasks.js +39 -11
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/providers/npm.js +2 -2
- package/dist/sources/providers/provider-utils.js +4 -3
- package/dist/sources/providers/website.js +11 -7
- package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
- package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
- package/dist/sources/website-url.js +73 -0
- package/dist/storage/engines/sqlite-migrations.js +81 -26
- package/dist/storage/managed-db.js +27 -24
- package/dist/storage/repositories/events-repository.js +3 -0
- package/dist/storage/repositories/index-connection.js +42 -10
- package/dist/storage/repositories/index-entries-repository.js +203 -229
- package/dist/storage/repositories/index-entry-mapper.js +8 -12
- package/dist/storage/repositories/index-entry-schema.js +255 -0
- package/dist/storage/repositories/index-fts-repository.js +64 -71
- package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
- package/dist/storage/repositories/index-meta-repository.js +0 -11
- package/dist/storage/repositories/index-schema.js +74 -350
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +56 -7
- package/dist/storage/repositories/proposals-repository.js +4 -127
- package/dist/storage/repositories/registry-cache.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +20 -40
- package/dist/storage/repositories/workflow-runs-repository.js +228 -129
- package/dist/storage/sqlite-read-snapshot.js +148 -0
- package/dist/tasks/backends/cron.js +170 -42
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +787 -202
- package/dist/tasks/backends/schtasks.js +282 -83
- package/dist/tasks/embedded.js +7 -7
- package/dist/tasks/frozen-script.js +50 -0
- package/dist/tasks/resolve-akm-bin.js +5 -1
- package/dist/tasks/runner.js +239 -251
- package/dist/tasks/runtime-v3.js +281 -0
- package/dist/tasks/scheduler-binding.js +272 -0
- package/dist/tasks/scheduler-invocation.js +57 -43
- package/dist/tasks/scheduler-sync.js +654 -0
- package/dist/tasks/source-v3.js +752 -0
- package/dist/tasks/standalone-script-entry.js +5 -0
- package/dist/tasks/task-id.js +29 -0
- package/dist/workflows/authoring/authoring.js +15 -32
- package/dist/workflows/exec/dispatch-redaction.js +14 -8
- package/dist/workflows/exec/exec-unit.js +7 -28
- package/dist/workflows/exec/frozen-judge.js +57 -89
- package/dist/workflows/exec/lowering-notices.js +23 -0
- package/dist/workflows/exec/native-executor.js +301 -458
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +26 -32
- package/dist/workflows/exec/step-work.js +105 -109
- package/dist/workflows/exec/unit-dispatch.js +103 -27
- package/dist/workflows/exec/unit-writer.js +3 -3
- package/dist/workflows/exec/worktree.js +2 -2
- package/dist/workflows/ir/compile.js +86 -72
- package/dist/workflows/ir/environment-v4.js +328 -0
- package/dist/workflows/ir/freeze-v4.js +122 -0
- package/dist/workflows/ir/plan-hash.js +13 -7
- package/dist/workflows/ir/schema-v4.js +525 -0
- package/dist/workflows/ir/schema.js +25 -284
- package/dist/workflows/ir/source-freeze-v4.js +506 -0
- package/dist/workflows/parser.js +27 -24
- package/dist/workflows/program/schema.js +1 -2
- package/dist/workflows/renderer.js +42 -29
- package/dist/workflows/resource-limits.js +4 -5
- package/dist/workflows/runtime/agent-identity.js +11 -13
- package/dist/workflows/runtime/plan-classifier.js +8 -8
- package/dist/workflows/runtime/runs.js +27 -43
- package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
- package/dist/workflows/source-files.js +373 -0
- package/dist/workflows/source-ir/compile.js +196 -0
- package/dist/workflows/source-ir/github-yaml.js +577 -0
- package/dist/workflows/source-ir/ordering.js +38 -0
- package/dist/workflows/source-ir/program.js +50 -0
- package/dist/workflows/source-ir/result.js +26 -0
- package/dist/workflows/source-ir/schema.js +772 -0
- package/dist/workflows/source-ir/semantics.js +242 -0
- package/dist/workflows/source-ir/uses.js +14 -0
- package/docs/README.md +2 -0
- package/docs/migration/README.md +3 -1
- package/docs/migration/release-notes/0.9.2.md +55 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/migration/v0.8-to-v0.9.md +76 -1077
- package/docs/migration/v0.9.0-troubleshooting.md +104 -516
- package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +230 -98
- package/docs/reference/configuration.md +159 -36
- package/docs/reference/data-and-telemetry.md +19 -1
- package/docs/reference/supported-formats.md +23 -3
- package/docs/reference/tasks.md +182 -0
- package/docs/reference/workflow-schema.md +91 -40
- package/docs/reference/workflows.md +33 -6
- package/package.json +10 -6
- package/schemas/akm-config.json +372 -224
- package/schemas/akm-task.json +324 -80
- package/schemas/akm-workflow.json +6 -9
- package/dist/core/migration-operation.js +0 -75
- package/dist/integrations/agent/model-aliases.js +0 -74
- package/dist/tasks/parser.js +0 -380
- package/dist/tasks/schema.js +0 -123
- package/dist/tasks/validator.js +0 -80
- package/dist/workflows/ir/freeze.js +0 -320
- package/dist/workflows/runtime/document-cache.js +0 -13
|
@@ -18,95 +18,76 @@ import { getStateDbPath, withStateDb } from "../../core/state-db.js";
|
|
|
18
18
|
import { warn } from "../../core/warn.js";
|
|
19
19
|
import { buildSearchText } from "../../indexer/search/search-fields.js";
|
|
20
20
|
import { ENTRY_COLUMNS, rowToIndexedEntry } from "./index-entry-mapper.js";
|
|
21
|
+
import { deleteFtsEntries, replaceFtsEntry } from "./index-fts-repository.js";
|
|
21
22
|
import { SQLITE_CHUNK_SIZE } from "./index-sql.js";
|
|
22
23
|
import { deleteEntryVectors, isVecAvailable } from "./index-vec-repository.js";
|
|
23
24
|
// ── Entry operations ────────────────────────────────────────────────────────
|
|
24
25
|
/**
|
|
25
|
-
* Insert or update
|
|
26
|
+
* Insert or update one canonical entry and all synchronously derived search
|
|
27
|
+
* state. Returns the stable row id.
|
|
26
28
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
29
|
+
* The entries row, FTS projection, and stale-vector invalidation commit as one
|
|
30
|
+
* SQLite transaction. Callers therefore cannot publish an entry and forget a
|
|
31
|
+
* second FTS maintenance step.
|
|
30
32
|
*/
|
|
31
|
-
export function upsertEntry(db,
|
|
33
|
+
export function upsertEntry(db, filePath, entry, searchText, provenance, contentHash) {
|
|
32
34
|
// Hot path during indexing — cache prepared statements per database
|
|
33
35
|
// connection so we don't pay the SQL parse/compile cost on every call.
|
|
34
36
|
const stmts = getUpsertStmts(db);
|
|
35
|
-
const previous = (provenance?.itemRef
|
|
36
|
-
? stmts.findByItemRef.get(provenance.itemRef)
|
|
37
|
-
: undefined) ?? stmts.findByEntryKey.get(entryKey);
|
|
38
37
|
// Phase 5A / Advantage D5: surface derived memory parent ref into the
|
|
39
38
|
// dedicated `derived_from` column so retrieval-time lookup (parent→child)
|
|
40
39
|
// does not have to scan + JSON-decode every memory row.
|
|
41
40
|
const derivedFrom = typeof entry.derivedFrom === "string" && entry.derivedFrom.trim() ? entry.derivedFrom.trim() : null;
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// token) unconditionally; `item_ref`/bundle/component/concept/adapter come
|
|
45
|
-
// from the write-boundary derivation when available (NULL otherwise, healed
|
|
46
|
-
// by the next full index). `content_hash` (F4a M-core-2) is `doc.hash` from
|
|
47
|
-
// the diff-persist writer; a NULL passed here PRESERVES any existing hash (the
|
|
48
|
-
// ON CONFLICT COALESCE below) so the LLM-enrichment re-upsert cannot wipe it.
|
|
41
|
+
// `content_hash` is optional on the LLM-enrichment re-upsert; a missing hash
|
|
42
|
+
// preserves the scan writer's current value.
|
|
49
43
|
const apply = () => {
|
|
50
|
-
const
|
|
44
|
+
const previous = stmts.findByItemRef.get(provenance.itemRef);
|
|
45
|
+
const result = stmts.upsert.get(provenance.itemRef, provenance.bundleId, provenance.componentId, provenance.conceptId, provenance.adapterId, entry.type, filePath, contentHash ?? null, JSON.stringify(entry), searchText, derivedFrom);
|
|
51
46
|
if (!result)
|
|
52
|
-
throw new Error("upsertEntry:
|
|
47
|
+
throw new Error("upsertEntry: item_ref not found after upsert");
|
|
53
48
|
if (previous?.id === result.id && previous.search_text !== searchText)
|
|
54
49
|
deleteEntryVectors(db, result.id);
|
|
55
|
-
|
|
56
|
-
// only revisits entries that actually changed. INSERT OR IGNORE is
|
|
57
|
-
// idempotent across multiple upserts of the same row.
|
|
58
|
-
stmts.markDirty.run(result.id);
|
|
50
|
+
replaceFtsEntry(db, result.id, entry);
|
|
59
51
|
return result.id;
|
|
60
52
|
};
|
|
61
|
-
|
|
53
|
+
// Always enter the driver's transaction wrapper. Both supported SQLite
|
|
54
|
+
// drivers lower a transaction opened inside another transaction to a
|
|
55
|
+
// savepoint, so a caller that catches this mutation's error cannot commit a
|
|
56
|
+
// partial entries row through its outer transaction.
|
|
57
|
+
return db.transaction(apply)();
|
|
62
58
|
}
|
|
63
59
|
const upsertStmtsByDb = new WeakMap();
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// update `entry_key` when adapter ownership changes its
|
|
67
|
-
// spelling so directory pruning retains the upserted row.
|
|
68
|
-
// `content_hash` COALESCEs so a NULL passed by the LLM-enhance re-upsert cannot
|
|
69
|
-
// wipe a previously-persisted hash.
|
|
60
|
+
// item_ref is the sole durable conflict target. `content_hash` COALESCEs so a
|
|
61
|
+
// metadata-only enrichment pass cannot wipe a scan hash.
|
|
70
62
|
const UPSERT_SET_CLAUSE = `SET
|
|
71
|
-
entry_key = excluded.entry_key,
|
|
72
|
-
dir_path = excluded.dir_path,
|
|
73
|
-
file_path = excluded.file_path,
|
|
74
|
-
stash_dir = excluded.stash_dir,
|
|
75
|
-
entry_json = excluded.entry_json,
|
|
76
|
-
search_text = excluded.search_text,
|
|
77
|
-
entry_type = excluded.entry_type,
|
|
78
|
-
derived_from = excluded.derived_from,
|
|
79
|
-
item_ref = excluded.item_ref,
|
|
80
63
|
bundle_id = excluded.bundle_id,
|
|
81
64
|
component_id = excluded.component_id,
|
|
82
65
|
concept_id = excluded.concept_id,
|
|
83
66
|
adapter_id = excluded.adapter_id,
|
|
84
67
|
type = excluded.type,
|
|
68
|
+
file_path = excluded.file_path,
|
|
69
|
+
document_json = excluded.document_json,
|
|
70
|
+
search_text = excluded.search_text,
|
|
71
|
+
derived_from = excluded.derived_from,
|
|
85
72
|
content_hash = COALESCE(excluded.content_hash, content_hash)`;
|
|
86
73
|
function getUpsertStmts(db) {
|
|
87
74
|
const existing = upsertStmtsByDb.get(db);
|
|
88
75
|
if (existing)
|
|
89
76
|
return existing;
|
|
90
|
-
// Durable identity is the primary conflict target. `entry_key` remains the
|
|
91
|
-
// internal conflict key for low-level entries without provenance.
|
|
92
|
-
const conflictClause = `ON CONFLICT(item_ref) DO UPDATE ${UPSERT_SET_CLAUSE}
|
|
93
|
-
ON CONFLICT(entry_key) DO UPDATE ${UPSERT_SET_CLAUSE}`;
|
|
94
77
|
const stmts = {
|
|
95
78
|
// RETURNING id handles ON CONFLICT DO UPDATE correctly — no second
|
|
96
79
|
// SELECT round-trip needed (last_insert_rowid() is unreliable for
|
|
97
80
|
// ON CONFLICT). Use `.get()` so a single row comes back.
|
|
98
81
|
upsert: db.prepare(`
|
|
99
82
|
INSERT INTO entries (
|
|
100
|
-
|
|
101
|
-
|
|
83
|
+
item_ref, bundle_id, component_id, concept_id, adapter_id, type,
|
|
84
|
+
file_path, content_hash, document_json, search_text, derived_from
|
|
102
85
|
)
|
|
103
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
104
|
-
${
|
|
86
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
87
|
+
ON CONFLICT(item_ref) DO UPDATE ${UPSERT_SET_CLAUSE}
|
|
105
88
|
RETURNING id
|
|
106
89
|
`),
|
|
107
|
-
|
|
108
|
-
findByItemRef: db.prepare("SELECT id, search_text FROM entries WHERE item_ref = ? ORDER BY id ASC LIMIT 1"),
|
|
109
|
-
findByEntryKey: db.prepare("SELECT id, search_text FROM entries WHERE entry_key = ? LIMIT 1"),
|
|
90
|
+
findByItemRef: db.prepare("SELECT id, search_text FROM entries WHERE item_ref = ?"),
|
|
110
91
|
};
|
|
111
92
|
upsertStmtsByDb.set(db, stmts);
|
|
112
93
|
return stmts;
|
|
@@ -121,10 +102,10 @@ function getUpsertStmts(db) {
|
|
|
121
102
|
* ordering keeps results deterministic). Returns `null` when no derived
|
|
122
103
|
* child has been indexed for this parent.
|
|
123
104
|
*/
|
|
124
|
-
export function getDerivedForParent(db, parentRef,
|
|
105
|
+
export function getDerivedForParent(db, parentRef, bundleId) {
|
|
125
106
|
if (!parentRef)
|
|
126
107
|
return null;
|
|
127
|
-
const sourceScope =
|
|
108
|
+
const sourceScope = bundleId ? "AND bundle_id = ?" : "";
|
|
128
109
|
const row = db
|
|
129
110
|
.prepare(`SELECT ${ENTRY_COLUMNS}
|
|
130
111
|
FROM entries
|
|
@@ -132,7 +113,7 @@ export function getDerivedForParent(db, parentRef, stashDir) {
|
|
|
132
113
|
${sourceScope}
|
|
133
114
|
ORDER BY id DESC
|
|
134
115
|
LIMIT 1`)
|
|
135
|
-
.get(parentRef, ...(
|
|
116
|
+
.get(parentRef, ...(bundleId ? [bundleId] : []));
|
|
136
117
|
if (!row)
|
|
137
118
|
return null;
|
|
138
119
|
return rowToIndexedEntry(row, "getDerivedForParent");
|
|
@@ -143,10 +124,9 @@ export function getDerivedForParent(db, parentRef, stashDir) {
|
|
|
143
124
|
*
|
|
144
125
|
* Used by the derived-twin belief inheritance in search ranking: a `.derived`
|
|
145
126
|
* twin has no belief state of its own, so it inherits its base memory's
|
|
146
|
-
* demoting state (contradicted/superseded/…) at search time. A twin's
|
|
147
|
-
* `
|
|
148
|
-
*
|
|
149
|
-
* found by stripping that suffix — no ref/prefix reconstruction. Returns a map
|
|
127
|
+
* demoting state (contradicted/superseded/…) at search time. A twin's durable
|
|
128
|
+
* `item_ref` is exactly its base ref plus the `.derived` suffix, so the base is
|
|
129
|
+
* found by stripping that suffix. Returns a map
|
|
150
130
|
* of twin id → base beliefState for bases that carry a non-empty state.
|
|
151
131
|
* Best-effort: any query error (e.g. legacy DB) yields no inheritance rather
|
|
152
132
|
* than failing the search.
|
|
@@ -163,14 +143,14 @@ export function getBaseBeliefStatesForDerivedTwins(db, twinIds) {
|
|
|
163
143
|
const placeholders = chunk.map(() => "?").join(",");
|
|
164
144
|
bestEffort(() => {
|
|
165
145
|
const rows = db
|
|
166
|
-
.prepare(`SELECT twin.id AS twin_id, json_extract(base.
|
|
146
|
+
.prepare(`SELECT twin.id AS twin_id, json_extract(base.document_json, '$.beliefState') AS belief
|
|
167
147
|
FROM entries twin
|
|
168
148
|
JOIN entries base
|
|
169
|
-
ON base.
|
|
149
|
+
ON base.type = 'memory'
|
|
170
150
|
AND base.item_ref = substr(twin.item_ref, 1, length(twin.item_ref) - length('.derived'))
|
|
171
151
|
WHERE twin.id IN (${placeholders})
|
|
172
152
|
AND twin.item_ref LIKE '%.derived'
|
|
173
|
-
AND json_extract(base.
|
|
153
|
+
AND json_extract(base.document_json, '$.beliefState') IS NOT NULL`)
|
|
174
154
|
.all(...chunk);
|
|
175
155
|
for (const r of rows) {
|
|
176
156
|
if (typeof r.belief === "string" && r.belief.trim().length > 0)
|
|
@@ -181,18 +161,18 @@ export function getBaseBeliefStatesForDerivedTwins(db, twinIds) {
|
|
|
181
161
|
return out;
|
|
182
162
|
}
|
|
183
163
|
/**
|
|
184
|
-
*
|
|
164
|
+
* Re-key an entries row in place for the opt-in source-maintenance script.
|
|
185
165
|
*
|
|
186
166
|
* The row id is preserved on purpose — `utility_scores`,
|
|
187
167
|
* `utility_scores_scoped`, and `embeddings` are keyed by `entry_id`, so an
|
|
188
|
-
* UPDATE (rather than a delete + insert under the new `
|
|
168
|
+
* UPDATE (rather than a delete + insert under the new `item_ref`) is what
|
|
189
169
|
* keeps the asset's accumulated usage-ranking history attached across a
|
|
190
170
|
* rename. (`asset_salience` / `asset_outcome` live in state.db keyed by
|
|
191
171
|
* `asset_ref` TEXT and are re-keyed separately by `akm mv` — see
|
|
192
|
-
*
|
|
172
|
+
* the state rekey helper.) `document_json.name` (and `filename`, when
|
|
193
173
|
* present) is patched and `search_text` rebuilt so search reflects the new
|
|
194
|
-
* name
|
|
195
|
-
*
|
|
174
|
+
* name. Its FTS projection and stale vector are updated in the same
|
|
175
|
+
* transaction as the canonical identity.
|
|
196
176
|
*
|
|
197
177
|
* Bundle-qualified `usage_events.entry_ref` rows for the old conceptId are
|
|
198
178
|
* rewritten to the new item ref. Without this, events keep the old
|
|
@@ -203,7 +183,7 @@ export function getBaseBeliefStatesForDerivedTwins(db, twinIds) {
|
|
|
203
183
|
* never adopts them (live asset's history wins, matching the stale-row
|
|
204
184
|
* eviction below).
|
|
205
185
|
*
|
|
206
|
-
* A stale row already occupying
|
|
186
|
+
* A stale row already occupying the new item ref (the caller has verified no
|
|
207
187
|
* FILE exists at the target, so such a row can only be a leftover for a
|
|
208
188
|
* deleted file) is evicted first — through {@link deleteRelatedRows}, so its
|
|
209
189
|
* child rows (embeddings, entries_vec, utility scores, usage events) go with
|
|
@@ -211,44 +191,48 @@ export function getBaseBeliefStatesForDerivedTwins(db, twinIds) {
|
|
|
211
191
|
* FK under `PRAGMA foreign_keys = ON` and roll back the whole re-key.
|
|
212
192
|
* The moved row keeps its id.
|
|
213
193
|
*
|
|
214
|
-
* Returns the surviving row id, or `null` when no row matches
|
|
194
|
+
* Returns the surviving row id, or `null` when no row matches the old item ref
|
|
215
195
|
* (nothing indexed under the old name — the caller falls open and the next
|
|
216
196
|
* full `akm index` picks the file up as a fresh entry).
|
|
217
197
|
*/
|
|
218
198
|
export function rekeyEntryInPlace(db, opts) {
|
|
219
199
|
const oldItemRef = `${opts.sourceName}//${opts.oldRef}`;
|
|
220
200
|
const row = db
|
|
221
|
-
.prepare("SELECT id,
|
|
201
|
+
.prepare("SELECT id, file_path, document_json, search_text, type FROM entries WHERE item_ref = ?")
|
|
222
202
|
.get(oldItemRef);
|
|
223
203
|
if (!row)
|
|
224
204
|
return null;
|
|
225
|
-
|
|
226
|
-
|
|
205
|
+
const sourceRoot = path.resolve(opts.sourceRoot);
|
|
206
|
+
const currentPath = path.resolve(row.file_path);
|
|
207
|
+
if (currentPath !== sourceRoot && !currentPath.startsWith(`${sourceRoot}${path.sep}`)) {
|
|
208
|
+
throw new Error(`Refusing to re-key entry ${oldItemRef}: source root does not match.`);
|
|
227
209
|
}
|
|
228
|
-
// Patch the JSON payload. On corrupt
|
|
210
|
+
// Patch the JSON payload. On corrupt document_json still re-key identity/path so
|
|
229
211
|
// the utility history survives; the next full index heals the JSON.
|
|
230
|
-
let
|
|
212
|
+
let documentJson = row.document_json;
|
|
231
213
|
let searchText = row.search_text;
|
|
214
|
+
let document;
|
|
232
215
|
try {
|
|
233
|
-
const entry = JSON.parse(row.
|
|
216
|
+
const entry = JSON.parse(row.document_json);
|
|
234
217
|
entry.name = opts.newName;
|
|
235
218
|
if (typeof entry.filename === "string")
|
|
236
219
|
entry.filename = path.basename(opts.newFilePath);
|
|
237
220
|
if (opts.newDerivedFrom !== undefined)
|
|
238
221
|
entry.derivedFrom = opts.newDerivedFrom;
|
|
239
|
-
|
|
222
|
+
documentJson = JSON.stringify(entry);
|
|
240
223
|
searchText = buildSearchText(entry);
|
|
224
|
+
document = entry;
|
|
241
225
|
}
|
|
242
226
|
catch {
|
|
243
|
-
/* corrupt
|
|
227
|
+
/* corrupt document_json — identity/path-only re-key */
|
|
244
228
|
}
|
|
245
|
-
const expectedNewRef = conceptIdFromTypeName(row.
|
|
229
|
+
const expectedNewRef = conceptIdFromTypeName(row.type, opts.newName);
|
|
246
230
|
if (opts.newRef !== expectedNewRef) {
|
|
247
|
-
throw new Error(`Refusing to re-key entry ${
|
|
231
|
+
throw new Error(`Refusing to re-key entry ${oldItemRef}: target ref does not match the entry type and name.`);
|
|
248
232
|
}
|
|
249
233
|
const newItemRef = `${opts.sourceName}//${opts.newRef}`;
|
|
250
234
|
db.transaction(() => {
|
|
251
|
-
const stale = db.prepare("SELECT id FROM entries WHERE
|
|
235
|
+
const stale = db.prepare("SELECT id FROM entries WHERE item_ref = ?").get(newItemRef);
|
|
252
236
|
if (stale && stale.id !== row.id) {
|
|
253
237
|
// Full child-row cleanup (embeddings, entries_vec, utility scores,
|
|
254
238
|
// usage events, FTS + dirty marks) BEFORE the entries delete: the
|
|
@@ -258,11 +242,16 @@ export function rekeyEntryInPlace(db, opts) {
|
|
|
258
242
|
deleteRelatedRows(db, [{ id: stale.id }]);
|
|
259
243
|
db.prepare("DELETE FROM entries WHERE id = ?").run(stale.id);
|
|
260
244
|
}
|
|
261
|
-
db.prepare("UPDATE entries SET
|
|
245
|
+
db.prepare("UPDATE entries SET file_path = ?, document_json = ?, search_text = ?, item_ref = ?, concept_id = ? WHERE id = ?").run(opts.newFilePath, documentJson, searchText, newItemRef, opts.newRef, row.id);
|
|
262
246
|
if (opts.newDerivedFrom !== undefined) {
|
|
263
247
|
db.prepare("UPDATE entries SET derived_from = ? WHERE id = ?").run(opts.newDerivedFrom, row.id);
|
|
264
248
|
}
|
|
265
|
-
|
|
249
|
+
if (row.search_text !== searchText)
|
|
250
|
+
deleteEntryVectors(db, row.id);
|
|
251
|
+
if (document)
|
|
252
|
+
replaceFtsEntry(db, row.id, document);
|
|
253
|
+
else
|
|
254
|
+
deleteFtsEntries(db, [row.id]);
|
|
266
255
|
})();
|
|
267
256
|
// Re-point usage history at the new ref. Chunk-8 WI-8.3: usage_events lives in
|
|
268
257
|
// state.db now, so this is a SEPARATE cross-DB transaction (best-effort — the
|
|
@@ -347,68 +336,65 @@ export function getPositiveFeedbackCountsByIds(ids) {
|
|
|
347
336
|
}, "positive feedback counts are best-effort");
|
|
348
337
|
return result;
|
|
349
338
|
}
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
339
|
+
function rowsInDirectory(db, dirPath, bundleId) {
|
|
340
|
+
const rows = db
|
|
341
|
+
.prepare(`SELECT id, item_ref, file_path FROM entries${bundleId ? " WHERE bundle_id = ?" : ""}`)
|
|
342
|
+
.all(...(bundleId ? [bundleId] : []));
|
|
343
|
+
const resolvedDir = path.resolve(dirPath);
|
|
344
|
+
return rows.filter((row) => path.dirname(path.resolve(row.file_path)) === resolvedDir);
|
|
345
|
+
}
|
|
346
|
+
function deleteEntryRows(db, rows, options = {}) {
|
|
347
|
+
if (rows.length === 0)
|
|
348
|
+
return [];
|
|
349
|
+
deleteRelatedRows(db, rows, options);
|
|
350
|
+
for (let i = 0; i < rows.length; i += SQLITE_CHUNK_SIZE) {
|
|
351
|
+
const chunk = rows.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
352
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
353
|
+
db.prepare(`DELETE FROM entries WHERE id IN (${placeholders})`).run(...chunk.map((row) => row.id));
|
|
354
|
+
}
|
|
355
|
+
return rows.map((row) => row.id);
|
|
356
356
|
}
|
|
357
357
|
export function deleteEntriesByDir(db, dirPath) {
|
|
358
|
-
|
|
358
|
+
db.transaction(() => deleteEntryRows(db, rowsInDirectory(db, dirPath)))();
|
|
359
359
|
}
|
|
360
|
-
export function
|
|
361
|
-
return db.transaction(() =>
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
db.prepare("
|
|
367
|
-
|
|
360
|
+
export function deleteEntriesByDirAndBundle(db, dirPath, bundleId, options = {}) {
|
|
361
|
+
return db.transaction(() => deleteEntryRows(db, rowsInDirectory(db, dirPath, bundleId), options))();
|
|
362
|
+
}
|
|
363
|
+
/** Delete every entry and child row belonging to one canonical bundle. */
|
|
364
|
+
export function deleteEntriesByBundle(db, bundleId) {
|
|
365
|
+
db.transaction(() => {
|
|
366
|
+
const rows = db.prepare("SELECT id FROM entries WHERE bundle_id = ?").all(bundleId);
|
|
367
|
+
deleteEntryRows(db, rows);
|
|
368
368
|
})();
|
|
369
369
|
}
|
|
370
370
|
/**
|
|
371
|
-
* Delete
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
* which is orthogonal to the ref grammar and identifies rows regardless of
|
|
375
|
-
* whether their `item_ref` is populated or NULL. No item_ref predicate is
|
|
376
|
-
* needed (or correct) here; both grammars' rows are removed by location.
|
|
371
|
+
* Delete the complete regenerable entry generation through the same child-row
|
|
372
|
+
* authority used by targeted deletes. The caller may retain cross-database
|
|
373
|
+
* usage events so the finalize pass can relink them to the new row ids.
|
|
377
374
|
*/
|
|
378
|
-
export function
|
|
379
|
-
|
|
375
|
+
export function deleteAllEntries(db, options = {}) {
|
|
376
|
+
return db.transaction(() => {
|
|
377
|
+
const rows = db.prepare("SELECT id FROM entries").all();
|
|
378
|
+
return deleteEntryRows(db, rows, options);
|
|
379
|
+
})();
|
|
380
380
|
}
|
|
381
381
|
/**
|
|
382
|
-
* Diff-persist orphan delete
|
|
383
|
-
*
|
|
384
|
-
* `entry_key` is NOT in `keepKeys` — the rows for files that vanished from a
|
|
385
|
-
* rescanned directory.
|
|
382
|
+
* Diff-persist orphan delete: remove every entry under `dirPath` whose durable
|
|
383
|
+
* `item_ref` is not in `keepRefs`.
|
|
386
384
|
*
|
|
387
385
|
* Replaces the old per-dir `deleteEntriesByDir` + full re-insert: the caller
|
|
388
|
-
* upserts the current file set
|
|
386
|
+
* upserts the current file set first (ON CONFLICT preserving `entries.id`, so
|
|
389
387
|
* embeddings / utility / usage stay attached to unchanged rows), then calls this
|
|
390
|
-
* to prune only the
|
|
388
|
+
* to prune only the departed rows. The net row-state for the directory is identical
|
|
391
389
|
* to delete-then-reinsert; the win is that unchanged rows keep their id.
|
|
392
390
|
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
* one another's rows.
|
|
391
|
+
* Both directory- and bundle-scoped so overlapping physical roots cannot
|
|
392
|
+
* prune one another's rows.
|
|
396
393
|
*/
|
|
397
|
-
export function
|
|
394
|
+
export function deleteEntriesByDirExceptRefs(db, dirPath, bundleId, keepRefs, options = {}) {
|
|
398
395
|
return db.transaction(() => {
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
.all(dirPath, stashDir);
|
|
402
|
-
const doomed = rows.filter((r) => !keepKeys.has(r.entry_key));
|
|
403
|
-
if (doomed.length === 0)
|
|
404
|
-
return [];
|
|
405
|
-
deleteRelatedRows(db, doomed, options);
|
|
406
|
-
for (let i = 0; i < doomed.length; i += SQLITE_CHUNK_SIZE) {
|
|
407
|
-
const chunk = doomed.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
408
|
-
const placeholders = chunk.map(() => "?").join(",");
|
|
409
|
-
db.prepare(`DELETE FROM entries WHERE id IN (${placeholders})`).run(...chunk.map((r) => r.id));
|
|
410
|
-
}
|
|
411
|
-
return doomed.map((row) => row.id);
|
|
396
|
+
const doomed = rowsInDirectory(db, dirPath, bundleId).filter((row) => !keepRefs.has(row.item_ref));
|
|
397
|
+
return deleteEntryRows(db, doomed, options);
|
|
412
398
|
})();
|
|
413
399
|
}
|
|
414
400
|
function deleteRelatedRows(db, ids, options = {}) {
|
|
@@ -416,15 +402,9 @@ function deleteRelatedRows(db, ids, options = {}) {
|
|
|
416
402
|
return;
|
|
417
403
|
const numericIds = ids.map((r) => r.id);
|
|
418
404
|
const vecAvail = isVecAvailable(db);
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
|
|
422
|
-
for (let i = 0; i < numericIds.length; i += SQLITE_CHUNK_SIZE) {
|
|
423
|
-
const chunk = numericIds.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
424
|
-
const placeholders = chunk.map(() => "?").join(",");
|
|
425
|
-
bestEffort(() => db.prepare(`DELETE FROM entries_fts WHERE entry_id IN (${placeholders})`).run(...chunk), "fts table may not exist on a brand-new db");
|
|
426
|
-
bestEffort(() => db.prepare(`DELETE FROM entries_fts_dirty WHERE entry_id IN (${placeholders})`).run(...chunk), "fts dirty table is created lazily by upsertEntry");
|
|
427
|
-
}
|
|
405
|
+
// FTS is part of the canonical mutation boundary, not a caller-maintained
|
|
406
|
+
// dirty queue. Delete it before the parent row inside this transaction.
|
|
407
|
+
deleteFtsEntries(db, numericIds);
|
|
428
408
|
// Process in chunks to stay within SQLITE_MAX_VARIABLE_NUMBER
|
|
429
409
|
for (let i = 0; i < numericIds.length; i += SQLITE_CHUNK_SIZE) {
|
|
430
410
|
const chunk = numericIds.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
@@ -437,43 +417,40 @@ function deleteRelatedRows(db, ids, options = {}) {
|
|
|
437
417
|
bestEffort(() => db.prepare(`DELETE FROM utility_scores WHERE entry_id IN (${placeholders})`).run(...chunk), "delete utility_scores for entries");
|
|
438
418
|
bestEffort(() => db.prepare(`DELETE FROM utility_scores_scoped WHERE entry_id IN (${placeholders})`).run(...chunk), "delete utility_scores_scoped for entries");
|
|
439
419
|
}
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
|
|
444
|
-
deleteUsageEventsByEntryIds(numericIds);
|
|
445
|
-
// #624-P1: graph_files is NO LONGER keyed on entries.id, so deleting an
|
|
446
|
-
// entries row must NOT wipe the extracted graph (that is the whole point —
|
|
447
|
-
// the graph survives a reindex when body_hash is unchanged). We therefore do
|
|
448
|
-
// NOT delete graph_files here. We DO, however, recompute graph_meta counts
|
|
449
|
-
// for the stash roots touched by the deleted entries so the summary numbers
|
|
450
|
-
// stay consistent with the live child rows (the counts are derived, and the
|
|
451
|
-
// entries delete may have changed which files are considered/indexed).
|
|
452
|
-
//
|
|
453
|
-
// Resolve the affected stash roots from the entries rows BEFORE deletion.
|
|
454
|
-
const affectedStashRoots = new Set();
|
|
420
|
+
// Graph rows are independently keyed and intentionally survive entry
|
|
421
|
+
// deletion. Resolve their owning roots through the canonical physical path
|
|
422
|
+
// before entries disappear, then refresh the derived summary counts.
|
|
423
|
+
const affectedGraphRoots = new Set();
|
|
455
424
|
for (let i = 0; i < numericIds.length; i += SQLITE_CHUNK_SIZE) {
|
|
456
425
|
const chunk = numericIds.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
457
426
|
const placeholders = chunk.map(() => "?").join(",");
|
|
458
427
|
bestEffort(() => {
|
|
459
428
|
const rows = db
|
|
460
|
-
.prepare(`SELECT DISTINCT
|
|
429
|
+
.prepare(`SELECT DISTINCT gf.stash_root
|
|
430
|
+
FROM graph_files gf
|
|
431
|
+
JOIN entries e ON e.file_path = gf.file_path
|
|
432
|
+
WHERE e.id IN (${placeholders})`)
|
|
461
433
|
.all(...chunk);
|
|
462
|
-
for (const row of rows)
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
}, "resolve stash roots for graph_meta recompute");
|
|
434
|
+
for (const row of rows)
|
|
435
|
+
affectedGraphRoots.add(row.stash_root);
|
|
436
|
+
}, "resolve graph roots for graph_meta recompute");
|
|
467
437
|
}
|
|
468
|
-
for (const stashRoot of
|
|
438
|
+
for (const stashRoot of affectedGraphRoots) {
|
|
469
439
|
bestEffort(() => db
|
|
470
440
|
.prepare(`UPDATE graph_meta
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
441
|
+
SET extracted_files = (SELECT COUNT(*) FROM graph_files WHERE stash_root = ?),
|
|
442
|
+
entity_count = (SELECT COUNT(*) FROM graph_file_entities WHERE stash_root = ?),
|
|
443
|
+
relation_count = (SELECT COUNT(*) FROM graph_file_relations WHERE stash_root = ?)
|
|
444
|
+
WHERE stash_root = ?`)
|
|
475
445
|
.run(stashRoot, stashRoot, stashRoot, stashRoot), "sync graph_meta counts after entries delete");
|
|
476
446
|
}
|
|
447
|
+
// usage_events lives in state.db, outside this transaction. Index persistence
|
|
448
|
+
// disables this cleanup and runs it only after its index.db transaction
|
|
449
|
+
// commits; standalone delete callers retain the immediate behavior.
|
|
450
|
+
if (options.cleanupUsageEvents !== false)
|
|
451
|
+
deleteUsageEventsByEntryIds(numericIds);
|
|
452
|
+
// graph_files is keyed by its own stash_root/file_path/body_hash identity,
|
|
453
|
+
// so deleting an entry row intentionally leaves extracted graph data intact.
|
|
477
454
|
}
|
|
478
455
|
export function deleteUsageEventsByEntryIds(entryIds) {
|
|
479
456
|
if (entryIds.length === 0 || !fs.existsSync(getStateDbPath()))
|
|
@@ -490,10 +467,10 @@ export function deleteUsageEventsByEntryIds(entryIds) {
|
|
|
490
467
|
}
|
|
491
468
|
/**
|
|
492
469
|
* Delete entries by their primary key IDs, along with all related rows
|
|
493
|
-
* (embeddings, entries_vec, entries_fts,
|
|
470
|
+
* (embeddings, entries_vec, entries_fts, utility scores, usage_events).
|
|
494
471
|
*
|
|
495
|
-
* Used by
|
|
496
|
-
* no longer exist
|
|
472
|
+
* Used by explicit `--clean` reconciliation before embeddings and final
|
|
473
|
+
* verification to remove stale entries whose source files no longer exist.
|
|
497
474
|
*/
|
|
498
475
|
export function deleteEntriesByIds(db, ids) {
|
|
499
476
|
if (ids.length === 0)
|
|
@@ -525,11 +502,11 @@ export function getAllEntries(db, entryType, excludeTypes) {
|
|
|
525
502
|
// list skips the clause (never `NOT IN ()`).
|
|
526
503
|
const excludes = excludeTypes && excludeTypes.length > 0 ? excludeTypes : [];
|
|
527
504
|
if (entryType && entryType !== "any") {
|
|
528
|
-
sql = `SELECT ${ENTRY_COLUMNS} FROM entries WHERE
|
|
505
|
+
sql = `SELECT ${ENTRY_COLUMNS} FROM entries WHERE type = ?`;
|
|
529
506
|
params = [entryType];
|
|
530
507
|
}
|
|
531
508
|
else if (excludes.length > 0) {
|
|
532
|
-
sql = `SELECT ${ENTRY_COLUMNS} FROM entries WHERE
|
|
509
|
+
sql = `SELECT ${ENTRY_COLUMNS} FROM entries WHERE type NOT IN (${excludes.map(() => "?").join(", ")})`;
|
|
533
510
|
params = [...excludes];
|
|
534
511
|
}
|
|
535
512
|
else {
|
|
@@ -542,11 +519,10 @@ export function getAllEntries(db, entryType, excludeTypes) {
|
|
|
542
519
|
/**
|
|
543
520
|
* Resolve a single `entries.id` from a new-grammar `[bundle//]conceptId` ref,
|
|
544
521
|
* keying on the canonical stored `item_ref` (ref-grammar decision D-R1/D-R4).
|
|
545
|
-
* The optional `
|
|
546
|
-
* match to one source root.
|
|
522
|
+
* The optional `bundleId` scopes the match to one indexed bundle.
|
|
547
523
|
*/
|
|
548
|
-
export function findEntryIdByRef(db, ref,
|
|
549
|
-
return findEntryIdByBundleRef(db, ref,
|
|
524
|
+
export function findEntryIdByRef(db, ref, bundleId) {
|
|
525
|
+
return findEntryIdByBundleRef(db, ref, bundleId);
|
|
550
526
|
}
|
|
551
527
|
/** `name` plus its `.md`-toggled sibling — the markdown ext-keep/strip ambiguity. */
|
|
552
528
|
function withMdVariants(name) {
|
|
@@ -556,12 +532,12 @@ function withMdVariants(name) {
|
|
|
556
532
|
* Current (`[bundle//]conceptId`) id lookup: match `item_ref` exactly when
|
|
557
533
|
* bundle-qualified or by `//conceptId` suffix when short.
|
|
558
534
|
*/
|
|
559
|
-
function findEntryIdByBundleRef(db, ref,
|
|
535
|
+
function findEntryIdByBundleRef(db, ref, bundleId) {
|
|
560
536
|
const parsed = parseBundleRef(ref);
|
|
561
537
|
const conceptVariants = withMdVariants(parsed.conceptId);
|
|
562
538
|
// item_ref is the canonical stored spelling post-flip.
|
|
563
539
|
for (const conceptId of conceptVariants) {
|
|
564
|
-
const id = matchIdByItemRef(db, parsed.bundle, conceptId,
|
|
540
|
+
const id = matchIdByItemRef(db, parsed.bundle, conceptId, bundleId);
|
|
565
541
|
if (id !== undefined)
|
|
566
542
|
return id;
|
|
567
543
|
}
|
|
@@ -591,24 +567,23 @@ function findEntryIdByBundleRef(db, ref, stashDir) {
|
|
|
591
567
|
* `resolveRef`'s job; this DB helper takes no config handle). The exact-bundle
|
|
592
568
|
* arm is single-row under the UNIQUE `item_ref` index.
|
|
593
569
|
*/
|
|
594
|
-
function matchIdByItemRef(db, bundle, conceptId,
|
|
595
|
-
const scope =
|
|
570
|
+
function matchIdByItemRef(db, bundle, conceptId, bundleId) {
|
|
571
|
+
const scope = bundleId ? "AND bundle_id = ?" : "";
|
|
596
572
|
if (bundle !== undefined) {
|
|
597
573
|
const itemRef = `${bundle}//${conceptId}`;
|
|
598
574
|
const row = db
|
|
599
575
|
.prepare(`SELECT id FROM entries WHERE item_ref = ? ${scope} ORDER BY id ASC LIMIT 1`)
|
|
600
|
-
.get(itemRef, ...(
|
|
576
|
+
.get(itemRef, ...(bundleId ? [bundleId] : []));
|
|
601
577
|
return row?.id;
|
|
602
578
|
}
|
|
603
579
|
const suffix = `//${conceptId}`;
|
|
604
580
|
const row = db
|
|
605
581
|
.prepare(`SELECT id FROM entries
|
|
606
|
-
WHERE item_ref
|
|
607
|
-
AND substr(item_ref, length(item_ref) - length(?) + 1) = ?
|
|
582
|
+
WHERE substr(item_ref, length(item_ref) - length(?) + 1) = ?
|
|
608
583
|
${scope}
|
|
609
584
|
ORDER BY id ASC
|
|
610
585
|
LIMIT 1`)
|
|
611
|
-
.get(suffix, suffix, ...(
|
|
586
|
+
.get(suffix, suffix, ...(bundleId ? [bundleId] : []));
|
|
612
587
|
return row?.id;
|
|
613
588
|
}
|
|
614
589
|
export function getEntryCount(db) {
|
|
@@ -616,15 +591,12 @@ export function getEntryCount(db) {
|
|
|
616
591
|
return row.cnt;
|
|
617
592
|
}
|
|
618
593
|
/**
|
|
619
|
-
* Per-asset-type entry counts (keyed by `
|
|
594
|
+
* Per-asset-type entry counts (keyed by `type`, e.g. "skill",
|
|
620
595
|
* "knowledge", "memory"). Used by `akm info` to break down the aggregate
|
|
621
|
-
* `indexStats.entryCount` (R-057).
|
|
622
|
-
* omitted rather than surfaced under a synthetic key.
|
|
596
|
+
* `indexStats.entryCount` (R-057).
|
|
623
597
|
*/
|
|
624
598
|
export function getEntryCountByType(db) {
|
|
625
|
-
const rows = db
|
|
626
|
-
.prepare("SELECT entry_type AS type, COUNT(*) AS cnt FROM entries WHERE entry_type IS NOT NULL GROUP BY entry_type")
|
|
627
|
-
.all();
|
|
599
|
+
const rows = db.prepare("SELECT type, COUNT(*) AS cnt FROM entries GROUP BY type").all();
|
|
628
600
|
const out = {};
|
|
629
601
|
for (const row of rows) {
|
|
630
602
|
out[row.type] = row.cnt;
|
|
@@ -636,41 +608,44 @@ export function getEmbeddableEntryCount(db) {
|
|
|
636
608
|
}
|
|
637
609
|
export function getEntryById(db, id) {
|
|
638
610
|
const row = db
|
|
639
|
-
.prepare("SELECT file_path,
|
|
611
|
+
.prepare("SELECT file_path, document_json, item_ref, bundle_id, component_id, concept_id, adapter_id FROM entries WHERE id = ?")
|
|
640
612
|
.get(id);
|
|
641
613
|
if (!row)
|
|
642
614
|
return undefined;
|
|
643
615
|
// Guard against corrupt JSON
|
|
644
616
|
let entry;
|
|
645
617
|
try {
|
|
646
|
-
entry = JSON.parse(row.
|
|
618
|
+
entry = JSON.parse(row.document_json);
|
|
647
619
|
}
|
|
648
620
|
catch {
|
|
649
|
-
warn(`[db] getEntryById: skipping entry id=${id} — corrupt
|
|
621
|
+
warn(`[db] getEntryById: skipping entry id=${id} — corrupt document_json`);
|
|
650
622
|
return undefined;
|
|
651
623
|
}
|
|
652
624
|
return {
|
|
653
625
|
filePath: row.file_path,
|
|
654
|
-
stashDir: row.stash_dir,
|
|
655
626
|
entry,
|
|
656
627
|
itemRef: row.item_ref,
|
|
657
628
|
bundleId: row.bundle_id,
|
|
629
|
+
componentId: row.component_id,
|
|
658
630
|
conceptId: row.concept_id,
|
|
631
|
+
adapterId: row.adapter_id,
|
|
659
632
|
};
|
|
660
633
|
}
|
|
661
634
|
export function getEntriesByDir(db, dirPath) {
|
|
662
|
-
const
|
|
635
|
+
const ids = new Set(rowsInDirectory(db, dirPath).map((row) => row.id));
|
|
636
|
+
const rows = db.prepare(`SELECT ${ENTRY_COLUMNS} FROM entries`).all().filter((row) => ids.has(row.id));
|
|
663
637
|
return parseEntryRows(rows, "getEntriesByDir");
|
|
664
638
|
}
|
|
665
|
-
/** Return every directory previously indexed for one
|
|
666
|
-
export function
|
|
667
|
-
const rows = db.prepare("SELECT
|
|
668
|
-
return rows.map((row) => row.
|
|
639
|
+
/** Return every directory previously indexed for one canonical bundle. */
|
|
640
|
+
export function getIndexedDirPathsByBundleId(db, bundleId) {
|
|
641
|
+
const rows = db.prepare("SELECT file_path FROM entries WHERE bundle_id = ?").all(bundleId);
|
|
642
|
+
return [...new Set(rows.map((row) => path.dirname(row.file_path)))];
|
|
669
643
|
}
|
|
670
|
-
/** Return every persisted
|
|
671
|
-
export function
|
|
672
|
-
const
|
|
673
|
-
|
|
644
|
+
/** Return every persisted bundle owner for one physical directory. */
|
|
645
|
+
export function getIndexedBundleIdsByDir(db, dirPath) {
|
|
646
|
+
const ids = new Set(rowsInDirectory(db, dirPath).map((row) => row.id));
|
|
647
|
+
const rows = db.prepare("SELECT id, bundle_id FROM entries").all();
|
|
648
|
+
return [...new Set(rows.filter((row) => ids.has(row.id)).map((row) => row.bundle_id))];
|
|
674
649
|
}
|
|
675
650
|
/**
|
|
676
651
|
* Resolve a single `entries.id` by exact `file_path` (the canonical on-disk
|
|
@@ -704,7 +679,7 @@ export function getIndexedFilePaths(db) {
|
|
|
704
679
|
*
|
|
705
680
|
* Lifted verbatim (WS5) from the inline `SELECT file_path FROM entries WHERE
|
|
706
681
|
* id = ?` in commands/feedback-cli.ts. Unlike {@link getEntryById}, this does
|
|
707
|
-
* NOT parse `
|
|
682
|
+
* NOT parse `document_json`, so a row with corrupt JSON still yields its path —
|
|
708
683
|
* preserving feedback-cli's pre-extraction behaviour byte-for-byte.
|
|
709
684
|
*/
|
|
710
685
|
export function getEntryFilePathById(db, id) {
|
|
@@ -712,8 +687,9 @@ export function getEntryFilePathById(db, id) {
|
|
|
712
687
|
return row?.file_path;
|
|
713
688
|
}
|
|
714
689
|
/**
|
|
715
|
-
* Fetch every `(file_path,
|
|
716
|
-
*
|
|
690
|
+
* Fetch every `(file_path, document_json)` row whose path is under a source
|
|
691
|
+
* root. The path containment filter is applied in JS so SQLite LIKE wildcard
|
|
692
|
+
* characters in filesystem paths cannot widen ownership.
|
|
717
693
|
*
|
|
718
694
|
* Lifted verbatim (WS5) from the inline query in commands/graph.ts'
|
|
719
695
|
* `buildRefByPath`. The full result set is materialised with `.all()` before
|
|
@@ -721,27 +697,13 @@ export function getEntryFilePathById(db, id) {
|
|
|
721
697
|
* connection-lifetime rule). JSON parsing stays with the caller, unchanged.
|
|
722
698
|
*/
|
|
723
699
|
export function getEntryRefRowsForStashRoot(db, stashRoot) {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
.
|
|
700
|
+
const root = path.resolve(stashRoot);
|
|
701
|
+
return db.prepare("SELECT file_path, document_json FROM entries").all().filter((row) => {
|
|
702
|
+
const file = path.resolve(row.file_path);
|
|
703
|
+
return file === root || file.startsWith(`${root}${path.sep}`);
|
|
704
|
+
});
|
|
727
705
|
}
|
|
728
706
|
// ── Indexer-phase helpers (moved from indexer.ts) ────────────────────────────
|
|
729
|
-
/**
|
|
730
|
-
* Upsert a workflow document record for an indexed entry.
|
|
731
|
-
* Persists the parsed workflow AST as JSON alongside a FNV-1a hash of the
|
|
732
|
-
* source content for future incremental fast-paths.
|
|
733
|
-
*/
|
|
734
|
-
export function upsertWorkflowDocument(db, entryId, doc, content) {
|
|
735
|
-
const sourceHash = computeSourceHash(content);
|
|
736
|
-
db.prepare(`INSERT INTO workflow_documents (entry_id, schema_version, document_json, source_path, source_hash, updated_at)
|
|
737
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
738
|
-
ON CONFLICT(entry_id) DO UPDATE SET
|
|
739
|
-
schema_version = excluded.schema_version,
|
|
740
|
-
document_json = excluded.document_json,
|
|
741
|
-
source_path = excluded.source_path,
|
|
742
|
-
source_hash = excluded.source_hash,
|
|
743
|
-
updated_at = excluded.updated_at`).run(entryId, doc.schemaVersion, JSON.stringify(doc), doc.source.path, sourceHash, new Date().toISOString());
|
|
744
|
-
}
|
|
745
707
|
/**
|
|
746
708
|
* Compute a cheap FNV-1a hash of a buffer for source-identity tracking.
|
|
747
709
|
* Not security-sensitive; used as an incremental fast-path skip key.
|
|
@@ -791,8 +753,7 @@ export function getEntryByRef(db, ref) {
|
|
|
791
753
|
/**
|
|
792
754
|
* The fully-qualified `item_ref` (`<bundle>//<conceptId>`, the durable stored
|
|
793
755
|
* spelling — spec §11.1 D-R3) for an entry `id`, or `null` when the row is gone
|
|
794
|
-
*
|
|
795
|
-
* full index). The usage-event / salience / feedback writers derive the durable
|
|
756
|
+
* The usage-event / salience / feedback writers derive the durable
|
|
796
757
|
* key from this so a stored key is always the resolved entry's canonical ref,
|
|
797
758
|
* never raw input (D-R3: durable keys are never derived from input).
|
|
798
759
|
*/
|
|
@@ -823,7 +784,15 @@ function resolveUsageEventEntryId(db, ref) {
|
|
|
823
784
|
* distinct linked entry_ids in usage_events is small — and the re-resolution
|
|
824
785
|
* reads `entries` from `indexDb`.
|
|
825
786
|
*/
|
|
826
|
-
|
|
787
|
+
function qualifiedUsageEventsTable(stateSchema) {
|
|
788
|
+
if (stateSchema === undefined)
|
|
789
|
+
return "usage_events";
|
|
790
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(stateSchema))
|
|
791
|
+
throw new Error("Invalid attached state schema name.");
|
|
792
|
+
return `"${stateSchema}".usage_events`;
|
|
793
|
+
}
|
|
794
|
+
export function relinkUsageEvents(indexDb, stateDb, options = {}) {
|
|
795
|
+
const usageEvents = qualifiedUsageEventsTable(options.stateSchema);
|
|
827
796
|
bestEffort(() => {
|
|
828
797
|
// Step 1: null out stale entry_ids (entry was deleted, re-keyed, etc).
|
|
829
798
|
// Leaving them in place would let `recomputeUtilityScores` aggregate by an
|
|
@@ -831,23 +800,28 @@ export function relinkUsageEvents(indexDb, stateDb, _options = {}) {
|
|
|
831
800
|
// on the utility_scores INSERT and roll back the entire finalize
|
|
832
801
|
// transaction. Nulled rows can be re-resolved by step 2 below; events whose
|
|
833
802
|
// entry is permanently gone simply stay null and age out via retention.
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
803
|
+
const linkedRows = stateDb
|
|
804
|
+
.prepare(`SELECT DISTINCT entry_id AS id, entry_ref AS ref FROM ${usageEvents} WHERE entry_id IS NOT NULL`)
|
|
805
|
+
.all();
|
|
806
|
+
const entryIdentity = indexDb.prepare("SELECT item_ref AS itemRef FROM entries WHERE id = ?");
|
|
807
|
+
const staleLinks = linkedRows.filter(({ id, ref }) => {
|
|
808
|
+
const live = entryIdentity.get(id);
|
|
809
|
+
return live == null || (ref !== null && live.itemRef !== ref);
|
|
810
|
+
});
|
|
811
|
+
if (staleLinks.length > 0) {
|
|
812
|
+
const nullOut = stateDb.prepare(`UPDATE ${usageEvents} SET entry_id = NULL WHERE entry_id = ? AND entry_ref IS ?`);
|
|
839
813
|
const nullTx = stateDb.transaction(() => {
|
|
840
|
-
for (const id of
|
|
841
|
-
nullOut.run(id);
|
|
814
|
+
for (const { id, ref } of staleLinks)
|
|
815
|
+
nullOut.run(id, ref);
|
|
842
816
|
});
|
|
843
817
|
nullTx();
|
|
844
818
|
}
|
|
845
819
|
// Step 2: re-resolve each fully-qualified ref. Bare rows are not current
|
|
846
820
|
// durable identities and remain detached.
|
|
847
821
|
const refs = stateDb
|
|
848
|
-
.prepare(
|
|
822
|
+
.prepare(`SELECT DISTINCT entry_ref AS ref FROM ${usageEvents} WHERE entry_id IS NULL AND entry_ref IS NOT NULL`)
|
|
849
823
|
.all();
|
|
850
|
-
const update = stateDb.prepare(
|
|
824
|
+
const update = stateDb.prepare(`UPDATE ${usageEvents} SET entry_id = ? WHERE entry_ref = ? AND entry_id IS NULL`);
|
|
851
825
|
const relinkTx = stateDb.transaction(() => {
|
|
852
826
|
for (const { ref } of refs) {
|
|
853
827
|
let id;
|