akm-cli 0.9.17-alpha.2 → 0.9.17-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +756 -0
- package/dist/akm +94 -196
- package/dist/cli/shared.js +6 -2
- package/dist/cli.js +22 -9
- package/dist/commands/agent/agent-dispatch.js +1 -1
- package/dist/commands/command/command-execution.js +24 -62
- package/dist/commands/feedback-cli.js +0 -1
- package/dist/commands/health/accept-rate.js +2 -2
- package/dist/commands/health/checks.js +30 -75
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +0 -38
- package/dist/commands/health/improve-metrics.js +123 -562
- package/dist/commands/health/plugin-staleness.js +53 -3
- package/dist/commands/health/renderers.js +12 -4
- package/dist/commands/health/report-view-model.js +11 -106
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -73
- package/dist/commands/health.js +122 -143
- package/dist/commands/improve/consolidate/chunking.js +25 -100
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +538 -1075
- package/dist/commands/improve/content-hash.js +16 -24
- package/dist/commands/improve/distill/content-repair.js +18 -100
- package/dist/commands/improve/distill-guards.js +20 -81
- package/dist/commands/improve/distill-promotion-policy.js +23 -243
- package/dist/commands/improve/distill.js +608 -1075
- package/dist/commands/improve/eligibility.js +126 -400
- package/dist/commands/improve/execution.js +3 -5
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +29 -166
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +12 -7
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +443 -1063
- package/dist/commands/improve/ledger.js +114 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +459 -1172
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +14 -118
- package/dist/commands/improve/memory/memory-improve.js +4 -3
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -10
- package/dist/commands/improve/preparation.js +851 -2339
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +621 -1367
- package/dist/commands/improve/salience.js +46 -232
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +323 -0
- package/dist/commands/proposal/drain.js +251 -644
- package/dist/commands/proposal/proposal-cli.js +3 -18
- package/dist/commands/proposal/proposal-types.js +20 -41
- package/dist/commands/proposal/proposal.js +1 -2
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +502 -1487
- package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
- package/dist/commands/proposal/validators/proposal-validators.js +1 -1
- package/dist/commands/proposal/validators/proposals.js +13 -89
- package/dist/commands/read/curate.js +63 -413
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +2 -13
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +7 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +2 -11
- package/dist/commands/sources/installed-stashes.js +197 -746
- package/dist/commands/sources/schema-repair.js +98 -129
- package/dist/commands/sources/source-add.js +62 -12
- package/dist/commands/sources/stash-cli.js +1 -1
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +9 -8
- package/dist/commands/tasks/tasks.js +326 -930
- package/dist/commands/tasks/validate.js +42 -21
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +0 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +11 -12
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +291 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +1 -33
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +163 -68
- package/dist/core/config/legacy-source-shape-shim.js +38 -9
- package/dist/core/config/schema/embedding.js +20 -5
- package/dist/core/config/schema/engines.js +5 -0
- package/dist/core/config/schema/execution.js +1 -1
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/improve-processes.js +21 -95
- package/dist/core/config/schema/improve.js +4 -42
- package/dist/core/config/schema/scheduler.js +12 -12
- package/dist/core/config/schema/search.js +6 -22
- package/dist/core/env-secret-ref.js +0 -1
- package/dist/core/errors.js +8 -9
- package/dist/core/file-lock.js +76 -173
- package/dist/core/logs-db.js +2 -2
- package/dist/core/paths.js +0 -27
- package/dist/core/redaction.js +109 -2
- package/dist/core/run-lock.js +2 -5
- package/dist/core/spawn-env.js +1 -1
- package/dist/core/state/migrations.js +108 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +293 -1012
- package/dist/execution/input-contract.js +1 -1
- package/dist/execution/resolved-request.js +135 -689
- package/dist/execution/source.js +63 -257
- package/dist/execution/target-ref.js +1 -1
- package/dist/indexer/bundle-identity-guard.js +2 -2
- package/dist/indexer/db/graph-db.js +106 -46
- package/dist/indexer/ensure-index.js +44 -85
- package/dist/indexer/graph/graph-extraction.js +340 -562
- package/dist/indexer/graph/graph-related.js +130 -0
- package/dist/indexer/index-rebuild-lock.js +3 -11
- package/dist/indexer/index-writer-lock.js +8 -17
- package/dist/indexer/index-written-assets.js +139 -151
- package/dist/indexer/indexer.js +524 -846
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +81 -90
- package/dist/indexer/passes/metadata.js +132 -200
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +1 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +181 -590
- package/dist/indexer/search/fts-query.js +30 -41
- package/dist/indexer/search/ranking.js +28 -154
- package/dist/indexer/search/search-attribution.js +12 -32
- package/dist/indexer/search/search-fields.js +11 -15
- package/dist/indexer/search/search-hit-enrichers.js +54 -85
- package/dist/indexer/search/search-source.js +1 -4
- package/dist/indexer/usage/usage-events.js +2 -7
- package/dist/integrations/agent/engine-fallback.js +23 -40
- package/dist/integrations/agent/engine-resolution.js +93 -183
- package/dist/integrations/agent/execution.js +507 -0
- package/dist/integrations/agent/model-map.js +28 -156
- package/dist/integrations/agent/request-lowering.js +66 -141
- package/dist/integrations/agent/runner-dispatch.js +143 -321
- package/dist/integrations/agent/runner.js +54 -14
- package/dist/integrations/lockfile.js +53 -101
- package/dist/llm/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +10 -15
- package/dist/llm/graph-extract.js +3 -12
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/metadata-enhance.js +1 -2
- package/dist/llm/structured-call.js +5 -24
- package/dist/output/generic-render.js +23 -11
- package/dist/output/html-render.js +13 -10
- package/dist/output/render-registry.js +3 -32
- package/dist/output/shapes/helpers.js +2 -34
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +60 -23
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +1 -2
- package/dist/output/text/workflow-format.js +0 -32
- package/dist/output/text.js +2 -0
- package/dist/registry/factory.js +4 -19
- package/dist/registry/network.js +66 -220
- package/dist/registry/providers/index.js +0 -2
- package/dist/registry/providers/skills-sh.js +3 -14
- package/dist/registry/providers/static-index.js +24 -26
- package/dist/registry/resolve.js +55 -131
- package/dist/scripts/akm-migrate-node.js +43937 -93313
- package/dist/scripts/akm-migrate.js +43697 -93071
- package/dist/setup/registry-stash-loader.js +4 -13
- package/dist/setup/semantic-assets.js +3 -44
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/tasks.js +25 -15
- package/dist/sources/provider-factory.js +17 -18
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +7 -1
- package/dist/sources/providers/git-provider.js +0 -3
- package/dist/sources/providers/git-stash.js +0 -17
- package/dist/sources/providers/npm.js +2 -4
- package/dist/sources/providers/provider-utils.js +5 -10
- package/dist/sources/providers/website.js +0 -2
- package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
- package/dist/sources/website-url.js +2 -2
- package/dist/storage/database.js +9 -35
- package/dist/storage/repositories/improve-ledger-repository.js +168 -0
- package/dist/storage/repositories/index-connection.js +34 -70
- package/dist/storage/repositories/index-entries-repository.js +69 -111
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +83 -269
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +192 -220
- package/dist/storage/repositories/index-utility-repository.js +8 -29
- package/dist/storage/repositories/index-vec-repository.js +133 -414
- package/dist/storage/repositories/outcome-repository.js +2 -1
- package/dist/storage/repositories/proposals-repository.js +35 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- package/dist/storage/repositories/task-history-repository.js +26 -4
- package/dist/storage/repositories/workflow-runs-repository.js +53 -244
- package/dist/storage/sqlite-migrations.js +136 -0
- package/dist/storage/sqlite-pragmas.js +11 -9
- package/dist/storage/sqlite-transaction.js +170 -0
- package/dist/storage/state-db-integrity.js +34 -27
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +129 -277
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +125 -745
- package/dist/tasks/backends/schtasks.js +101 -620
- package/dist/tasks/prepare/prepare-support.js +5 -15
- package/dist/tasks/prepare/prepare.js +0 -2
- package/dist/tasks/resolve-akm-bin.js +20 -79
- package/dist/tasks/run/attempt-lifecycle.js +0 -1
- package/dist/tasks/scheduler-binding.js +18 -238
- package/dist/tasks/scheduler-invocation.js +52 -52
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +363 -679
- package/dist/tasks/source/parse-task-source.js +160 -10
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +2 -2
- package/dist/workflows/authoring/authoring.js +3 -12
- package/dist/workflows/compile.js +211 -0
- package/dist/workflows/concurrency-policy.js +13 -74
- package/dist/workflows/exec/child-invocation.js +3 -17
- package/dist/workflows/exec/child-workflow.js +32 -141
- package/dist/workflows/exec/dispatch-redaction.js +13 -53
- package/dist/workflows/exec/environment.js +98 -0
- package/dist/workflows/exec/exec-unit.js +33 -140
- package/dist/workflows/exec/frozen-judge.js +7 -59
- package/dist/workflows/exec/native-executor.js +82 -341
- package/dist/workflows/exec/param-secrets.js +29 -47
- package/dist/workflows/exec/run-workflow.js +154 -387
- package/dist/workflows/exec/scheduler.js +9 -36
- package/dist/workflows/exec/step-work.js +127 -430
- package/dist/workflows/exec/unit-dispatch.js +11 -63
- package/dist/workflows/exec/unit-writer.js +8 -52
- package/dist/workflows/exec/worktree.js +39 -273
- package/dist/workflows/freeze/child-output-references.js +4 -15
- package/dist/workflows/freeze/environment.js +99 -92
- package/dist/workflows/freeze/freeze.js +172 -0
- package/dist/workflows/freeze/step-values.js +19 -21
- package/dist/workflows/freeze/targets/child-workflow.js +23 -92
- package/dist/workflows/freeze/targets/command.js +10 -33
- package/dist/workflows/freeze/targets/script.js +5 -12
- package/dist/workflows/freeze/targets/shell.js +3 -6
- package/dist/workflows/freeze/targets/task.js +25 -80
- package/dist/workflows/freeze/task-bindings.js +20 -67
- package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
- package/dist/workflows/ir/params.js +6 -51
- package/dist/workflows/ir/plan-hash.js +2 -34
- package/dist/workflows/parser.js +140 -43
- package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
- package/dist/workflows/renderer.js +36 -69
- package/dist/workflows/resource-limits.js +12 -120
- package/dist/workflows/runtime/agent-identity.js +8 -40
- package/dist/workflows/runtime/run-outputs.js +3 -6
- package/dist/workflows/runtime/run-plan.js +316 -0
- package/dist/workflows/runtime/runs.js +48 -200
- package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
- package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/integration/bundling-akm.md +49 -42
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.17.md +41 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
- package/docs/reference/cli.md +182 -125
- package/docs/reference/configuration.md +49 -56
- package/docs/reference/data-and-telemetry.md +19 -20
- package/docs/reference/tasks.md +86 -38
- package/docs/reference/workflow-schema.md +14 -18
- package/docs/reference/workflows.md +6 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +87 -406
- package/dist/commands/health/advisories.js +0 -150
- package/dist/commands/health/metrics.js +0 -329
- package/dist/commands/health/surfaces.js +0 -102
- package/dist/commands/improve/anti-collapse.js +0 -83
- package/dist/commands/improve/collapse-detector.js +0 -432
- package/dist/commands/improve/consolidate/eligibility.js +0 -48
- package/dist/commands/improve/consolidate/merge.js +0 -146
- package/dist/commands/improve/distill/promote-memory.js +0 -329
- package/dist/commands/improve/distill/quality-gate.js +0 -500
- package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
- package/dist/commands/improve/proposal-envelope.js +0 -31
- package/dist/commands/improve/run-context.js +0 -123
- package/dist/commands/improve/shared.js +0 -21
- package/dist/commands/improve/source-identity.js +0 -28
- package/dist/commands/improve/triage.js +0 -96
- package/dist/commands/proposal/drain-policies.js +0 -151
- package/dist/commands/sources/update-transaction.js +0 -220
- package/dist/core/action-contributors.js +0 -28
- package/dist/core/config/config-version-shim.js +0 -101
- package/dist/core/config/retired-experimental-keys-shim.js +0 -62
- package/dist/core/fs-txn.js +0 -405
- package/dist/core/lexical-score.js +0 -25
- package/dist/core/maintenance-barrier.js +0 -167
- package/dist/execution/executable-identity.js +0 -105
- package/dist/execution/guarded-source.js +0 -427
- package/dist/indexer/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- package/dist/indexer/walk/project-context.js +0 -192
- package/dist/integrations/agent/execution-cascade.js +0 -566
- package/dist/integrations/agent/execution-definitions.js +0 -202
- package/dist/integrations/agent/execution-lowering.js +0 -841
- package/dist/integrations/agent/execution-preparation.js +0 -98
- package/dist/integrations/agent/inline-execution.js +0 -74
- package/dist/registry/create-provider-registry.js +0 -29
- package/dist/registry/pinned-request-helper.js +0 -247
- package/dist/registry/pinned-transport.js +0 -717
- package/dist/sources/providers/index.js +0 -14
- package/dist/storage/engines/sqlite-migrations.js +0 -271
- package/dist/storage/repositories/canaries-repository.js +0 -107
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
- package/dist/storage/repositories/registry-cache.js +0 -113
- package/dist/tasks/scheduler-sync-preview.js +0 -52
- package/dist/workflows/freeze/resolve-steps.js +0 -86
- package/dist/workflows/freeze/source-freeze.js +0 -64
- package/dist/workflows/ir/compile.js +0 -321
- package/dist/workflows/ir/environment-v4.js +0 -330
- package/dist/workflows/ir/freeze-v4.js +0 -153
- package/dist/workflows/ir/schema-v4.js +0 -745
- package/dist/workflows/ir/schema.js +0 -354
- package/dist/workflows/program/schema.js +0 -78
- package/dist/workflows/runtime/checkin.js +0 -57
- package/dist/workflows/runtime/plan-classifier.js +0 -196
- package/dist/workflows/runtime/unit-checkin.js +0 -45
- package/dist/workflows/runtime/unit-phases.js +0 -20
- package/dist/workflows/schema.js +0 -4
- package/dist/workflows/source-ir/compile.js +0 -200
- package/dist/workflows/source-ir/program.js +0 -50
- package/dist/workflows/source-ir/result.js +0 -26
- package/dist/workflows/source-ir/schema.js +0 -786
- package/dist/workflows/source-ir/triggers.js +0 -79
- package/dist/workflows/source-ir/uses.js +0 -40
- package/dist/workflows/validator.js +0 -60
package/dist/execution/source.js
CHANGED
|
@@ -1,102 +1,38 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
import {
|
|
5
|
-
import { cloneExecutionJson, cloneExecutionJsonObject, } from "./json.js";
|
|
6
|
-
import { assertSnapshotKeys, snapshotStrictRecord } from "./record.js";
|
|
4
|
+
import { cloneExecutionJson, cloneExecutionJsonObject } from "./json.js";
|
|
7
5
|
export const EXECUTION_SOURCE_SCHEMA_VERSION = 1;
|
|
8
6
|
/** Current internal adapter identifiers are lowercase kebab-case registry keys. */
|
|
9
7
|
export const EXECUTION_ADAPTER_ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
10
|
-
const CONTROL_OR_LINE_SEPARATOR_PATTERN = /[\p{Cc}\p{Zl}\p{Zp}]/u;
|
|
11
|
-
const FORMAT_CHARACTER_PATTERN = /\p{Cf}/u;
|
|
12
|
-
const WINDOWS_DRIVE_PREFIX_PATTERN = /^[A-Za-z]:/;
|
|
13
|
-
const RESERVED_EXTENSION_OWNERS = new Set(["__proto__", "constructor", "prototype", "tostring"]);
|
|
14
|
-
const renderedSourceBrand = Symbol("akm.adapter-rendered-execution-source");
|
|
15
|
-
const renderedSourceInstances = new WeakSet();
|
|
16
8
|
function requireRecord(value, path) {
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
function assertOnlyKeys(value, allowed, path) {
|
|
20
|
-
assertSnapshotKeys(value, allowed, path);
|
|
21
|
-
}
|
|
22
|
-
function validateExtensionOwner(owner) {
|
|
23
|
-
const normalized = owner.toLowerCase();
|
|
24
|
-
if (!EXECUTION_ADAPTER_ID_PATTERN.test(owner) || RESERVED_EXTENSION_OWNERS.has(normalized)) {
|
|
25
|
-
throw new TypeError(`extension owner must be a canonical, non-reserved adapter identifier: ${owner}`);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
function frozenNullPrototypeMap(entries) {
|
|
29
|
-
const out = Object.create(null);
|
|
30
|
-
for (const [key, value] of entries) {
|
|
31
|
-
Object.defineProperty(out, key, { value, enumerable: true, configurable: false, writable: false });
|
|
9
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
10
|
+
throw new TypeError(`${path} must be an object`);
|
|
32
11
|
}
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
function cloneExtensionEntry(value, index) {
|
|
36
|
-
const path = `extension entry ${index}`;
|
|
37
|
-
const cloned = cloneExecutionJson(value, path);
|
|
38
|
-
if (!Array.isArray(cloned) || cloned.length !== 2) {
|
|
39
|
-
throw new TypeError(`${path} must be a two-element [owner, values] array`);
|
|
40
|
-
}
|
|
41
|
-
const [owner, values] = cloned;
|
|
42
|
-
if (typeof owner !== "string")
|
|
43
|
-
throw new TypeError(`${path} owner must be a string`);
|
|
44
|
-
if (values === null || Array.isArray(values) || typeof values !== "object") {
|
|
45
|
-
throw new TypeError(`${path} values must be a JSON object`);
|
|
46
|
-
}
|
|
47
|
-
return [owner, values];
|
|
12
|
+
return value;
|
|
48
13
|
}
|
|
49
14
|
export function createAdapterExtensions(first, second, ...rest) {
|
|
50
|
-
const
|
|
15
|
+
const entries = typeof first === "string"
|
|
51
16
|
? [[first, second]]
|
|
52
17
|
: [first, ...(second === undefined ? [] : [second]), ...rest];
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
validateExtensionOwner(owner);
|
|
58
|
-
if (seen.has(owner))
|
|
59
|
-
throw new TypeError(`duplicate extension owner: ${owner}`);
|
|
60
|
-
seen.add(owner);
|
|
61
|
-
cloned.push([owner, cloneExecutionJsonObject(values, `extensions.${owner}`)]);
|
|
62
|
-
}
|
|
63
|
-
return frozenNullPrototypeMap(cloned);
|
|
18
|
+
const out = {};
|
|
19
|
+
for (const [owner, values] of entries)
|
|
20
|
+
out[owner] = cloneExecutionJsonObject(values, `extensions.${owner}`);
|
|
21
|
+
return Object.freeze(out);
|
|
64
22
|
}
|
|
65
23
|
export function cloneAdapterExtensions(value, path) {
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
return first ? createAdapterExtensions(first, ...rest) : frozenNullPrototypeMap([]);
|
|
70
|
-
}
|
|
71
|
-
function requireCanonicalString(value, path) {
|
|
72
|
-
if (typeof value !== "string" ||
|
|
73
|
-
value.length === 0 ||
|
|
74
|
-
!isWellFormedUnicode(value) ||
|
|
75
|
-
value.normalize("NFC") !== value) {
|
|
76
|
-
throw new TypeError(`${path} must be a non-empty NFC string`);
|
|
77
|
-
}
|
|
78
|
-
if (hasUnsafeIdentityCharacter(value)) {
|
|
79
|
-
throw new TypeError(`${path} must not contain Unicode control or dangerous format characters`);
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const [owner, fields] of Object.entries(requireRecord(value, path))) {
|
|
26
|
+
out[owner] = cloneExecutionJsonObject(fields, `${path}.${owner}`);
|
|
80
27
|
}
|
|
81
|
-
return
|
|
28
|
+
return Object.freeze(out);
|
|
82
29
|
}
|
|
83
|
-
/**
|
|
84
|
-
* Validate a selector/layer identifier that may cross the durable execution
|
|
85
|
-
* boundary. Keep this shared with the cascade planner so resume cannot admit a
|
|
86
|
-
* selector the live planner would reject.
|
|
87
|
-
*/
|
|
30
|
+
/** A selector or layer identifier that may cross the durable execution boundary. */
|
|
88
31
|
export function requireStableExecutionSelector(value, path) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
selector = requireCanonicalString(value, path);
|
|
32
|
+
if (typeof value !== "string" || value.length === 0 || value.trim() !== value) {
|
|
33
|
+
throw new TypeError(`${path} must be a non-empty stable identifier`);
|
|
92
34
|
}
|
|
93
|
-
|
|
94
|
-
throw new TypeError(`${path} must be a non-empty stable NFC identifier`, { cause });
|
|
95
|
-
}
|
|
96
|
-
if (selector.length > 512 || selector.trim() !== selector) {
|
|
97
|
-
throw new TypeError(`${path} must be a non-empty stable NFC identifier`);
|
|
98
|
-
}
|
|
99
|
-
return selector;
|
|
35
|
+
return value;
|
|
100
36
|
}
|
|
101
37
|
export function isPortableExecutionAgentSelector(value) {
|
|
102
38
|
return /^(?:[^/]+\/\/)?agents\/[^/]/.test(value);
|
|
@@ -104,118 +40,61 @@ export function isPortableExecutionAgentSelector(value) {
|
|
|
104
40
|
export function executionPersonaMatchesSelector(selector, personaRef) {
|
|
105
41
|
return selector.includes("//") ? selector === personaRef : personaRef.endsWith(`//${selector}`);
|
|
106
42
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
function isWellFormedUnicode(value) {
|
|
119
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
120
|
-
const code = value.charCodeAt(index);
|
|
121
|
-
if (code >= 0xd800 && code <= 0xdbff) {
|
|
122
|
-
const next = value.charCodeAt(index + 1);
|
|
123
|
-
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
124
|
-
return false;
|
|
125
|
-
index += 1;
|
|
126
|
-
}
|
|
127
|
-
else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return true;
|
|
132
|
-
}
|
|
133
|
-
function validateCanonicalIdentity(input, path) {
|
|
134
|
-
assertOnlyKeys(input, ["ref", "bundle", "adapter", "file", "hash"], path);
|
|
135
|
-
const ref = requireCanonicalString(input.ref, `${path}.ref`);
|
|
136
|
-
const bundle = requireCanonicalString(input.bundle, `${path}.bundle`);
|
|
137
|
-
const adapter = requireCanonicalString(input.adapter, `${path}.adapter`);
|
|
138
|
-
const file = requireCanonicalString(input.file, `${path}.file`);
|
|
139
|
-
const hash = requireCanonicalString(input.hash, `${path}.hash`);
|
|
140
|
-
let parsed;
|
|
141
|
-
try {
|
|
142
|
-
parsed = parseBundleRef(ref);
|
|
143
|
-
}
|
|
144
|
-
catch (cause) {
|
|
145
|
-
throw new TypeError(`${path}.ref is not a canonical bundle ref`, { cause });
|
|
146
|
-
}
|
|
147
|
-
if (parsed.bundle === undefined ||
|
|
148
|
-
parsed.bundle !== bundle ||
|
|
149
|
-
parsed.fragment !== undefined ||
|
|
150
|
-
!isBundleSlug(bundle) ||
|
|
151
|
-
bundleRefToString(parsed) !== ref) {
|
|
152
|
-
throw new TypeError(`${path}.ref must round-trip as the same fully-qualified bundle ref without a fragment`);
|
|
153
|
-
}
|
|
154
|
-
if (!EXECUTION_ADAPTER_ID_PATTERN.test(adapter)) {
|
|
155
|
-
throw new TypeError(`${path}.adapter must use the current lowercase kebab-case adapter identifier grammar`);
|
|
156
|
-
}
|
|
157
|
-
const segments = file.split("/");
|
|
158
|
-
if (file.startsWith("/") ||
|
|
159
|
-
WINDOWS_DRIVE_PREFIX_PATTERN.test(file) ||
|
|
160
|
-
file.includes("\\") ||
|
|
161
|
-
segments.some((segment) => !segment || segment === "." || segment === "..")) {
|
|
162
|
-
throw new TypeError(`${path}.file must be a normalized relative POSIX path`);
|
|
43
|
+
const IDENTITY_KEYS = ["ref", "bundle", "adapter", "file", "hash"];
|
|
44
|
+
function identityFrom(value, path) {
|
|
45
|
+
const record = requireRecord(value, path);
|
|
46
|
+
const out = {};
|
|
47
|
+
for (const key of IDENTITY_KEYS) {
|
|
48
|
+
const field = record[key];
|
|
49
|
+
if (typeof field !== "string" || field.length === 0)
|
|
50
|
+
throw new TypeError(`${path}.${key} must be a non-empty string`);
|
|
51
|
+
out[key] = field;
|
|
163
52
|
}
|
|
164
|
-
|
|
165
|
-
throw new TypeError(`${path}.hash must be a SHA-256 hex digest`);
|
|
166
|
-
return { ref, bundle, adapter, file, hash };
|
|
53
|
+
return Object.freeze(out);
|
|
167
54
|
}
|
|
168
|
-
/**
|
|
55
|
+
/** Copy and freeze an identity produced by an adapter renderer. */
|
|
169
56
|
export function createExecutionSourceIdentity(input) {
|
|
170
|
-
return
|
|
57
|
+
return identityFrom(input, "execution source identity");
|
|
171
58
|
}
|
|
172
|
-
/**
|
|
59
|
+
/** Read a source identity back from a frozen request. */
|
|
173
60
|
export function decodeExecutionSourceIdentity(value, path = "execution source identity") {
|
|
174
|
-
return
|
|
61
|
+
return identityFrom(value, path);
|
|
175
62
|
}
|
|
63
|
+
/** Type-check the common frontmatter defaults; unknown keys are ignored. */
|
|
176
64
|
export function cloneUnresolvedExecutionDefaults(input, path = "execution source defaults") {
|
|
177
65
|
const record = requireRecord(input, path);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
"engine",
|
|
181
|
-
"model",
|
|
182
|
-
"inference",
|
|
183
|
-
"outputSchema",
|
|
184
|
-
"tools",
|
|
185
|
-
"timeout",
|
|
186
|
-
"workspace",
|
|
187
|
-
"environment",
|
|
188
|
-
"runtime",
|
|
189
|
-
], path);
|
|
190
|
-
const json = { ...cloneExecutionJsonObject(record, path) };
|
|
66
|
+
const present = (key) => Object.hasOwn(record, key) && record[key] !== undefined;
|
|
67
|
+
const out = {};
|
|
191
68
|
for (const key of ["agent", "engine", "model", "workspace"]) {
|
|
192
|
-
|
|
193
|
-
|
|
69
|
+
if (!present(key))
|
|
70
|
+
continue;
|
|
71
|
+
const value = record[key];
|
|
72
|
+
if (value !== null && typeof value !== "string")
|
|
194
73
|
throw new TypeError(`${path}.${key} must be a string or null`);
|
|
195
|
-
|
|
74
|
+
out[key] = value;
|
|
196
75
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
76
|
+
if (present("timeout")) {
|
|
77
|
+
const timeout = record.timeout;
|
|
78
|
+
if (timeout !== null && typeof timeout !== "string" && typeof timeout !== "number") {
|
|
79
|
+
throw new TypeError(`${path}.timeout must be a string, number, or null`);
|
|
80
|
+
}
|
|
81
|
+
out.timeout = timeout;
|
|
200
82
|
}
|
|
201
83
|
for (const key of ["inference", "outputSchema", "runtime"]) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
84
|
+
if (!present(key))
|
|
85
|
+
continue;
|
|
86
|
+
out[key] = record[key] === null ? null : cloneExecutionJsonObject(record[key], `${path}.${key}`);
|
|
206
87
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
throw new TypeError(`${path}.environment must be an object or null`);
|
|
211
|
-
}
|
|
212
|
-
if (Object.values(environment).some((value) => typeof value !== "string")) {
|
|
88
|
+
if (present("environment")) {
|
|
89
|
+
const environment = record.environment === null ? null : cloneExecutionJsonObject(record.environment, `${path}.environment`);
|
|
90
|
+
if (environment !== null && Object.values(environment).some((value) => typeof value !== "string")) {
|
|
213
91
|
throw new TypeError(`${path}.environment values must be strings`);
|
|
214
92
|
}
|
|
93
|
+
out.environment = environment;
|
|
215
94
|
}
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
return Object.freeze(
|
|
95
|
+
if (present("tools"))
|
|
96
|
+
out.tools = cloneToolSelection(record.tools, `${path}.tools`);
|
|
97
|
+
return Object.freeze(out);
|
|
219
98
|
}
|
|
220
99
|
export function cloneToolSelection(value, path = "tools") {
|
|
221
100
|
if (value === null || typeof value === "string")
|
|
@@ -229,92 +108,19 @@ export function cloneToolSelection(value, path = "tools") {
|
|
|
229
108
|
return cloneExecutionJsonObject(cloned, path);
|
|
230
109
|
}
|
|
231
110
|
export function createAdapterRenderedExecutionSource(input) {
|
|
232
|
-
const
|
|
233
|
-
assertOnlyKeys(record, ["kind", "content", "identity", "defaults", "extensions"], "adapter-rendered execution source");
|
|
234
|
-
const kind = record.kind;
|
|
235
|
-
const content = record.content;
|
|
111
|
+
const { kind, content } = input;
|
|
236
112
|
if (kind !== "command" && kind !== "persona")
|
|
237
113
|
throw new TypeError("execution source kind is invalid");
|
|
238
114
|
if (typeof content !== "string")
|
|
239
115
|
throw new TypeError("execution source content must be a string");
|
|
240
|
-
|
|
241
|
-
if (defaults === undefined) {
|
|
242
|
-
throw new TypeError("execution source defaults must be omitted or be an object");
|
|
243
|
-
}
|
|
244
|
-
const source = {
|
|
116
|
+
return Object.freeze({
|
|
245
117
|
schemaVersion: EXECUTION_SOURCE_SCHEMA_VERSION,
|
|
246
118
|
kind,
|
|
247
119
|
content,
|
|
248
|
-
defaults: cloneUnresolvedExecutionDefaults(defaults),
|
|
249
|
-
identity: createExecutionSourceIdentity(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
Object.defineProperty(source, renderedSourceBrand, { value: true, enumerable: false });
|
|
255
|
-
const frozen = Object.freeze(source);
|
|
256
|
-
renderedSourceInstances.add(frozen);
|
|
257
|
-
return frozen;
|
|
258
|
-
}
|
|
259
|
-
function isAdapterRenderedSource(value, kind) {
|
|
260
|
-
if (typeof value !== "object" || value === null)
|
|
261
|
-
return false;
|
|
262
|
-
if (!renderedSourceInstances.has(value))
|
|
263
|
-
return false;
|
|
264
|
-
if (Object.getPrototypeOf(value) !== Object.prototype || !Object.isFrozen(value))
|
|
265
|
-
return false;
|
|
266
|
-
if (!Object.hasOwn(value, renderedSourceBrand))
|
|
267
|
-
return false;
|
|
268
|
-
const required = new Set(["schemaVersion", "kind", "content", "defaults", "identity"]);
|
|
269
|
-
const optional = new Set(["extensions"]);
|
|
270
|
-
for (const key of Reflect.ownKeys(value)) {
|
|
271
|
-
if (key === renderedSourceBrand)
|
|
272
|
-
continue;
|
|
273
|
-
if (typeof key !== "string" || (!required.delete(key) && !optional.has(key)))
|
|
274
|
-
return false;
|
|
275
|
-
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
276
|
-
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable)
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
if (required.size > 0)
|
|
280
|
-
return false;
|
|
281
|
-
const brand = Object.getOwnPropertyDescriptor(value, renderedSourceBrand);
|
|
282
|
-
const source = value;
|
|
283
|
-
if (brand?.value !== true ||
|
|
284
|
-
brand.enumerable !== false ||
|
|
285
|
-
brand.configurable !== false ||
|
|
286
|
-
brand.writable !== false ||
|
|
287
|
-
source.schemaVersion !== EXECUTION_SOURCE_SCHEMA_VERSION ||
|
|
288
|
-
source.kind !== kind ||
|
|
289
|
-
typeof source.content !== "string" ||
|
|
290
|
-
typeof source.defaults !== "object" ||
|
|
291
|
-
source.defaults === null ||
|
|
292
|
-
!Object.isFrozen(source.defaults) ||
|
|
293
|
-
typeof source.identity !== "object" ||
|
|
294
|
-
source.identity === null ||
|
|
295
|
-
Object.getPrototypeOf(source.identity) !== Object.prototype ||
|
|
296
|
-
!Object.isFrozen(source.identity)) {
|
|
297
|
-
return false;
|
|
298
|
-
}
|
|
299
|
-
if (Object.hasOwn(source, "extensions")) {
|
|
300
|
-
if (typeof source.extensions !== "object" ||
|
|
301
|
-
source.extensions === null ||
|
|
302
|
-
Object.getPrototypeOf(source.extensions) !== null ||
|
|
303
|
-
!Object.isFrozen(source.extensions)) {
|
|
304
|
-
return false;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
try {
|
|
308
|
-
decodeExecutionSourceIdentity(source.identity);
|
|
309
|
-
}
|
|
310
|
-
catch {
|
|
311
|
-
return false;
|
|
312
|
-
}
|
|
313
|
-
return true;
|
|
314
|
-
}
|
|
315
|
-
export function isAdapterRenderedCommandSource(value) {
|
|
316
|
-
return isAdapterRenderedSource(value, "command");
|
|
317
|
-
}
|
|
318
|
-
export function isAdapterRenderedPersonaSource(value) {
|
|
319
|
-
return isAdapterRenderedSource(value, "persona");
|
|
120
|
+
defaults: cloneUnresolvedExecutionDefaults(input.defaults ?? {}),
|
|
121
|
+
identity: createExecutionSourceIdentity(input.identity),
|
|
122
|
+
...(input.extensions
|
|
123
|
+
? { extensions: cloneAdapterExtensions(input.extensions, "execution source extensions") }
|
|
124
|
+
: {}),
|
|
125
|
+
});
|
|
320
126
|
}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* Explicit non-goals (binding, spec §4.1): no GitHub locator grammar, no
|
|
24
24
|
* `akm/command` builtin special case, no resolution, no filesystem access, no
|
|
25
25
|
* guessing. Callers layer builtin detection on top (see
|
|
26
|
-
* `
|
|
26
|
+
* `classifyWorkflowStepUses` in src/workflows/source-semantics.ts).
|
|
27
27
|
*/
|
|
28
28
|
import { bundleRefToString, parseBundleRef } from "../core/asset/asset-ref.js";
|
|
29
29
|
import { UsageError } from "../core/errors.js";
|
|
@@ -34,7 +34,7 @@ import { rethrowIfTestIsolationError } from "../core/errors.js";
|
|
|
34
34
|
import { getDbPath } from "../core/paths.js";
|
|
35
35
|
import { warn } from "../core/warn.js";
|
|
36
36
|
import { closeDatabase, openReadonlyExistingDatabase } from "../storage/repositories/index-connection.js";
|
|
37
|
-
import {
|
|
37
|
+
import { hasCurrentEntriesTable } from "../storage/repositories/index-entry-schema.js";
|
|
38
38
|
let guardSettled = false;
|
|
39
39
|
/** TEST-ONLY: re-arm the once-per-process guard between cases. */
|
|
40
40
|
export function resetBundleIdentityGuardForTests() {
|
|
@@ -49,7 +49,7 @@ function indexedBundlePaths(dbPath) {
|
|
|
49
49
|
db = openReadonlyExistingDatabase(dbPath);
|
|
50
50
|
if (!db)
|
|
51
51
|
return undefined;
|
|
52
|
-
if (!
|
|
52
|
+
if (!hasCurrentEntriesTable(db))
|
|
53
53
|
return undefined;
|
|
54
54
|
return db
|
|
55
55
|
.prepare("SELECT DISTINCT bundle_id AS bundleId, file_path AS filePath FROM entries " +
|
|
@@ -29,16 +29,93 @@ function uniqueSorted(values) {
|
|
|
29
29
|
function normalizeEntity(value) {
|
|
30
30
|
return value.trim().toLowerCase();
|
|
31
31
|
}
|
|
32
|
+
/** Child rows joined to their file row, the same join the loaders read through. */
|
|
33
|
+
const STORED_ENTITIES = `graph_file_entities e
|
|
34
|
+
JOIN graph_files gf ON gf.stash_root = e.stash_root AND gf.file_path = e.file_path AND gf.body_hash = e.body_hash
|
|
35
|
+
WHERE gf.stash_root = ?`;
|
|
36
|
+
const STORED_RELATIONS = `graph_file_relations r
|
|
37
|
+
JOIN graph_files gf ON gf.stash_root = r.stash_root AND gf.file_path = r.file_path AND gf.body_hash = r.body_hash
|
|
38
|
+
WHERE gf.stash_root = ?`;
|
|
39
|
+
/** One comparable key for a file's extraction: its entities and relations, in order. */
|
|
40
|
+
function extractionKey(entities, relations) {
|
|
41
|
+
return JSON.stringify([entities, relations.map((r) => [r.from, r.to, r.type ?? null, r.confidence ?? null])]);
|
|
42
|
+
}
|
|
43
|
+
const EMPTY_EXTRACTION_KEY = extractionKey([], []);
|
|
44
|
+
/** The extraction key of every stored file under a root that has child rows. */
|
|
45
|
+
function readStoredExtractionKeys(db, stashRoot) {
|
|
46
|
+
const entityRows = db
|
|
47
|
+
.prepare(`SELECT e.file_path AS file_path, e.entity AS entity FROM ${STORED_ENTITIES} ORDER BY e.file_path, e.entity_order`)
|
|
48
|
+
.all(stashRoot);
|
|
49
|
+
const relationRows = db
|
|
50
|
+
.prepare(`SELECT r.file_path AS file_path, r.from_entity AS from_entity, r.to_entity AS to_entity,
|
|
51
|
+
r.relation_type AS relation_type, r.confidence AS confidence
|
|
52
|
+
FROM ${STORED_RELATIONS} ORDER BY r.file_path, r.relation_order`)
|
|
53
|
+
.all(stashRoot);
|
|
54
|
+
const byPath = new Map();
|
|
55
|
+
const bucket = (filePath) => {
|
|
56
|
+
let entry = byPath.get(filePath);
|
|
57
|
+
if (!entry) {
|
|
58
|
+
entry = { entities: [], relations: [] };
|
|
59
|
+
byPath.set(filePath, entry);
|
|
60
|
+
}
|
|
61
|
+
return entry;
|
|
62
|
+
};
|
|
63
|
+
for (const row of entityRows)
|
|
64
|
+
bucket(row.file_path).entities.push(row.entity);
|
|
65
|
+
for (const row of relationRows) {
|
|
66
|
+
bucket(row.file_path).relations.push({
|
|
67
|
+
from: row.from_entity,
|
|
68
|
+
to: row.to_entity,
|
|
69
|
+
...(row.relation_type !== null ? { type: row.relation_type } : {}),
|
|
70
|
+
...(row.confidence !== null ? { confidence: row.confidence } : {}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return new Map([...byPath].map(([filePath, stored]) => [filePath, extractionKey(stored.entities, stored.relations)]));
|
|
74
|
+
}
|
|
75
|
+
function roundMetric(value) {
|
|
76
|
+
return Number(value.toFixed(4));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The graph_meta counts, derived from the stored rows of one root. Each field
|
|
80
|
+
* has one meaning (see {@link GraphQualityTelemetry}): files are graph_files
|
|
81
|
+
* rows, entities are distinct case-folded names, relations are distinct
|
|
82
|
+
* case-folded (from, to, type) triples.
|
|
83
|
+
*/
|
|
84
|
+
function readStoredGraphQuality(db, stashRoot) {
|
|
85
|
+
const count = (sql) => db.prepare(sql).get(stashRoot).n;
|
|
86
|
+
const storedFiles = count("SELECT COUNT(*) AS n FROM graph_files WHERE stash_root = ?");
|
|
87
|
+
const filesWithEntities = count(`SELECT COUNT(DISTINCT gf.file_path) AS n FROM ${STORED_ENTITIES}`);
|
|
88
|
+
const entityCount = count(`SELECT COUNT(DISTINCT e.entity_norm) AS n FROM ${STORED_ENTITIES}`);
|
|
89
|
+
const relationCount = count(`SELECT COUNT(*) AS n FROM (
|
|
90
|
+
SELECT DISTINCT r.from_entity_norm, r.to_entity_norm, lower(coalesce(r.relation_type, '')) FROM ${STORED_RELATIONS}
|
|
91
|
+
)`);
|
|
92
|
+
const maxEdges = entityCount > 1 ? (entityCount * (entityCount - 1)) / 2 : 0;
|
|
93
|
+
return {
|
|
94
|
+
consideredFiles: storedFiles,
|
|
95
|
+
extractedFiles: filesWithEntities,
|
|
96
|
+
entityCount,
|
|
97
|
+
relationCount,
|
|
98
|
+
extractionCoverage: storedFiles > 0 ? roundMetric(filesWithEntities / storedFiles) : 0,
|
|
99
|
+
density: maxEdges > 0 ? roundMetric(relationCount / maxEdges) : 0,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
32
102
|
/**
|
|
33
103
|
* Persist (or update) a graph snapshot for a stash root.
|
|
34
104
|
*
|
|
35
105
|
* #624-P1: keyed on (stash_root, file_path, body_hash) — NOT entries.id. Graph
|
|
36
106
|
* rows are self-keyed by path, so they survive an entries delete + reinsert
|
|
37
|
-
* (a reindex) when body_hash is unchanged.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
107
|
+
* (a reindex) when body_hash is unchanged. A file whose body_hash is unchanged
|
|
108
|
+
* keeps its row; its entity and relation rows are rewritten only when they
|
|
109
|
+
* differ from the snapshot's (a re-extraction of the same body, e.g. after a
|
|
110
|
+
* model or prompt change or a failed first attempt, must land). Files whose
|
|
111
|
+
* body_hash changed have their old row + child rows deleted and the new content
|
|
112
|
+
* inserted; files in DB but absent from the new snapshot are deleted. There is
|
|
113
|
+
* no entry_id resolution and no orphan-skip — a graph file no longer needs a
|
|
114
|
+
* matching entries row.
|
|
115
|
+
*
|
|
116
|
+
* graph_meta records the snapshot's schema version, time and run telemetry;
|
|
117
|
+
* its counts are derived from the rows as stored after the write, never from
|
|
118
|
+
* the caller's in-memory graph.
|
|
42
119
|
*/
|
|
43
120
|
export function replaceStoredGraph(db, graph) {
|
|
44
121
|
const upsertMeta = db.prepare(`INSERT INTO graph_meta (
|
|
@@ -94,16 +171,15 @@ export function replaceStoredGraph(db, graph) {
|
|
|
94
171
|
const insertRelation = db.prepare(`INSERT INTO graph_file_relations (
|
|
95
172
|
stash_root, file_path, body_hash, relation_order, from_entity_norm, from_entity, to_entity_norm, to_entity, relation_type, confidence
|
|
96
173
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
97
|
-
const quality = graph.quality;
|
|
98
174
|
const telemetry = graph.telemetry;
|
|
99
175
|
db.transaction(() => {
|
|
100
|
-
upsertMeta.run(graph.stashRoot, graph.schemaVersion, graph.generatedAt, quality?.consideredFiles ?? graph.files.length, quality?.extractedFiles ?? graph.files.length, quality?.entityCount ?? graph.entities?.length ?? 0, quality?.relationCount ?? graph.relations?.length ?? 0, quality?.extractionCoverage ?? 0, quality?.density ?? 0, telemetry?.extractorId ?? null, telemetry?.extractionRunId ?? null, telemetry?.model ?? null, telemetry?.promptVersion ?? null, telemetry?.batchSize ?? null, telemetry?.cacheHits ?? 0, telemetry?.cacheMisses ?? 0, telemetry?.truncationCount ?? 0, telemetry?.failureCount ?? 0);
|
|
101
176
|
// Build a snapshot of existing rows for incremental compare. The unique
|
|
102
177
|
// index idx_graph_files_path guarantees at most one row per file_path.
|
|
103
178
|
const existingRows = selectExisting.all(graph.stashRoot);
|
|
104
179
|
const existingByPath = new Map();
|
|
105
180
|
for (const row of existingRows)
|
|
106
181
|
existingByPath.set(row.file_path, row);
|
|
182
|
+
const storedKeys = readStoredExtractionKeys(db, graph.stashRoot);
|
|
107
183
|
const presentPaths = new Set();
|
|
108
184
|
for (const [fileOrder, node] of graph.files.entries()) {
|
|
109
185
|
// body_hash is part of the PK; default to a sentinel for inputs (test
|
|
@@ -112,22 +188,32 @@ export function replaceStoredGraph(db, graph) {
|
|
|
112
188
|
// correct behaviour for "unknown" bodies. Distinct files in one stash
|
|
113
189
|
// are still keyed apart by file_path, so the empty sentinel is safe.
|
|
114
190
|
const bodyHash = node.bodyHash && node.bodyHash.length > 0 ? node.bodyHash : "";
|
|
191
|
+
const status = node.status ?? (node.entities.length > 0 ? "extracted" : "empty");
|
|
192
|
+
const reason = node.reason ?? (node.entities.length > 0 ? "none" : "no_graph_content");
|
|
193
|
+
const runId = node.extractionRunId ?? telemetry?.extractionRunId ?? null;
|
|
115
194
|
presentPaths.add(node.path);
|
|
116
195
|
const existing = existingByPath.get(node.path);
|
|
117
196
|
if (existing && existing.body_hash === bodyHash) {
|
|
118
|
-
// Body unchanged —
|
|
119
|
-
|
|
120
|
-
|
|
197
|
+
// Body unchanged — refresh the file meta, and rewrite the child rows
|
|
198
|
+
// only when this snapshot's extraction differs from the stored one.
|
|
199
|
+
updateFileMeta.run(fileOrder, node.type, node.confidence ?? null, status, reason, runId, graph.stashRoot, node.path, bodyHash);
|
|
200
|
+
const storedKey = storedKeys.get(node.path) ?? EMPTY_EXTRACTION_KEY;
|
|
201
|
+
if (storedKey === extractionKey(node.entities, node.relations))
|
|
202
|
+
continue;
|
|
203
|
+
deleteEntities.run(graph.stashRoot, node.path, bodyHash);
|
|
204
|
+
deleteRelations.run(graph.stashRoot, node.path, bodyHash);
|
|
121
205
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
206
|
+
else {
|
|
207
|
+
if (existing) {
|
|
208
|
+
// Stale row (different body_hash for this path). Delete the old row by
|
|
209
|
+
// its OLD body_hash; child rows cascade, but explicit DELETE keeps the
|
|
210
|
+
// order deterministic and is safe regardless of the FK pragma.
|
|
211
|
+
deleteEntities.run(graph.stashRoot, existing.file_path, existing.body_hash);
|
|
212
|
+
deleteRelations.run(graph.stashRoot, existing.file_path, existing.body_hash);
|
|
213
|
+
deleteFile.run(graph.stashRoot, existing.file_path, existing.body_hash);
|
|
214
|
+
}
|
|
215
|
+
insertFile.run(graph.stashRoot, node.path, fileOrder, node.type, bodyHash, node.confidence ?? null, status, reason, runId);
|
|
129
216
|
}
|
|
130
|
-
insertFile.run(graph.stashRoot, node.path, fileOrder, node.type, bodyHash, node.confidence ?? null, node.status ?? (node.entities.length > 0 ? "extracted" : "empty"), node.reason ?? (node.entities.length > 0 ? "none" : "no_graph_content"), node.extractionRunId ?? telemetry?.extractionRunId ?? null);
|
|
131
217
|
for (const [entityOrder, entity] of node.entities.entries()) {
|
|
132
218
|
insertEntity.run(graph.stashRoot, node.path, bodyHash, entityOrder, normalizeEntity(entity), entity);
|
|
133
219
|
}
|
|
@@ -144,6 +230,8 @@ export function replaceStoredGraph(db, graph) {
|
|
|
144
230
|
deleteFile.run(graph.stashRoot, row.file_path, row.body_hash);
|
|
145
231
|
}
|
|
146
232
|
}
|
|
233
|
+
const quality = readStoredGraphQuality(db, graph.stashRoot);
|
|
234
|
+
upsertMeta.run(graph.stashRoot, graph.schemaVersion, graph.generatedAt, quality.consideredFiles, quality.extractedFiles, quality.entityCount, quality.relationCount, quality.extractionCoverage, quality.density, telemetry?.extractorId ?? null, telemetry?.extractionRunId ?? null, telemetry?.model ?? null, telemetry?.promptVersion ?? null, telemetry?.batchSize ?? null, telemetry?.cacheHits ?? 0, telemetry?.cacheMisses ?? 0, telemetry?.truncationCount ?? 0, telemetry?.failureCount ?? 0);
|
|
147
235
|
})();
|
|
148
236
|
}
|
|
149
237
|
export function deleteStoredGraph(db, stashPath) {
|
|
@@ -191,34 +279,6 @@ export function enqueueGraphExtraction(db, stashRoot, filePath, bodyHash, priori
|
|
|
191
279
|
rethrowIfTestIsolationError(err);
|
|
192
280
|
}
|
|
193
281
|
}
|
|
194
|
-
/**
|
|
195
|
-
* #624-P3 — drain up to `limit` queued files for a stash, highest-priority
|
|
196
|
-
* first (then oldest queued_at). The returned rows are DELETED from the queue
|
|
197
|
-
* in the SAME transaction (SELECT-then-DELETE-by-PK), so a drain is exactly
|
|
198
|
-
* once. Tolerant of a missing table / db error (returns []), but never masks
|
|
199
|
-
* the bun-test isolation guard.
|
|
200
|
-
*/
|
|
201
|
-
export function drainExtractionQueue(db, stashRoot, limit) {
|
|
202
|
-
try {
|
|
203
|
-
return db.transaction(() => {
|
|
204
|
-
const rows = db
|
|
205
|
-
.prepare(`SELECT file_path, body_hash, priority
|
|
206
|
-
FROM graph_extraction_queue
|
|
207
|
-
WHERE stash_root = ?
|
|
208
|
-
ORDER BY priority DESC, queued_at ASC
|
|
209
|
-
LIMIT ?`)
|
|
210
|
-
.all(stashRoot, limit);
|
|
211
|
-
const del = db.prepare("DELETE FROM graph_extraction_queue WHERE stash_root = ? AND file_path = ?");
|
|
212
|
-
for (const row of rows)
|
|
213
|
-
del.run(stashRoot, row.file_path);
|
|
214
|
-
return rows.map((row) => ({ filePath: row.file_path, bodyHash: row.body_hash, priority: row.priority }));
|
|
215
|
-
})();
|
|
216
|
-
}
|
|
217
|
-
catch (err) {
|
|
218
|
-
rethrowIfTestIsolationError(err);
|
|
219
|
-
return [];
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
282
|
/** Read queued graph work without claiming or deleting it. */
|
|
223
283
|
export function peekExtractionQueue(db, stashRoot, limit) {
|
|
224
284
|
try {
|