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
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
-
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
-
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
|
-
import fs from "node:fs";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { isRecord } from "../core/common.js";
|
|
8
|
-
import { UsageError } from "../core/errors.js";
|
|
9
|
-
export function freezeExecutableIdentity(requested, options = {}) {
|
|
10
|
-
const absolutePath = resolveExecutable(requested, options.cwd ?? process.cwd(), options.path ?? process.env.PATH ?? "");
|
|
11
|
-
let realPath;
|
|
12
|
-
let stat;
|
|
13
|
-
let bytes;
|
|
14
|
-
try {
|
|
15
|
-
fs.accessSync(absolutePath, fs.constants.X_OK);
|
|
16
|
-
realPath = fs.realpathSync(absolutePath);
|
|
17
|
-
stat = fs.lstatSync(realPath, { bigint: true });
|
|
18
|
-
if (!stat.isFile() || stat.isSymbolicLink())
|
|
19
|
-
throw new Error("not a regular executable file");
|
|
20
|
-
bytes = fs.readFileSync(realPath);
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
throw new UsageError(`Executable ${JSON.stringify(requested)} could not be frozen.`, "INVALID_FLAG_VALUE");
|
|
24
|
-
}
|
|
25
|
-
return Object.freeze({
|
|
26
|
-
requested,
|
|
27
|
-
absolutePath,
|
|
28
|
-
realPath,
|
|
29
|
-
device: stat.dev.toString(),
|
|
30
|
-
inode: stat.ino.toString(),
|
|
31
|
-
size: bytes.byteLength,
|
|
32
|
-
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
export function decodeFrozenExecutableIdentity(value, label = "executable") {
|
|
36
|
-
if (!isRecord(value))
|
|
37
|
-
throw invalid(label);
|
|
38
|
-
const allowed = new Set(["requested", "absolutePath", "realPath", "device", "inode", "size", "sha256"]);
|
|
39
|
-
for (const key of Object.keys(value))
|
|
40
|
-
if (!allowed.has(key))
|
|
41
|
-
throw invalid(label);
|
|
42
|
-
if (typeof value.requested !== "string" ||
|
|
43
|
-
typeof value.absolutePath !== "string" ||
|
|
44
|
-
!path.isAbsolute(value.absolutePath) ||
|
|
45
|
-
typeof value.realPath !== "string" ||
|
|
46
|
-
!path.isAbsolute(value.realPath) ||
|
|
47
|
-
typeof value.device !== "string" ||
|
|
48
|
-
typeof value.inode !== "string" ||
|
|
49
|
-
!Number.isSafeInteger(value.size) ||
|
|
50
|
-
value.size < 0 ||
|
|
51
|
-
typeof value.sha256 !== "string" ||
|
|
52
|
-
!/^[0-9a-f]{64}$/.test(value.sha256)) {
|
|
53
|
-
throw invalid(label);
|
|
54
|
-
}
|
|
55
|
-
const decoded = Object.freeze({
|
|
56
|
-
requested: value.requested,
|
|
57
|
-
absolutePath: value.absolutePath,
|
|
58
|
-
realPath: value.realPath,
|
|
59
|
-
device: value.device,
|
|
60
|
-
inode: value.inode,
|
|
61
|
-
size: value.size,
|
|
62
|
-
sha256: value.sha256,
|
|
63
|
-
});
|
|
64
|
-
assertFrozenExecutableIdentity(decoded, label);
|
|
65
|
-
return decoded;
|
|
66
|
-
}
|
|
67
|
-
export function assertFrozenExecutableIdentity(identity, label = "executable") {
|
|
68
|
-
let actual;
|
|
69
|
-
try {
|
|
70
|
-
actual = freezeExecutableIdentity(identity.absolutePath, { cwd: path.dirname(identity.absolutePath) });
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
throw changed(label);
|
|
74
|
-
}
|
|
75
|
-
for (const key of ["absolutePath", "realPath", "device", "inode", "size", "sha256"]) {
|
|
76
|
-
if (actual[key] !== identity[key])
|
|
77
|
-
throw changed(label);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
function resolveExecutable(requested, cwd, pathValue) {
|
|
81
|
-
if (requested.includes("/") || requested.includes("\\"))
|
|
82
|
-
return path.resolve(cwd, requested);
|
|
83
|
-
const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";") : [""];
|
|
84
|
-
for (const directory of pathValue.split(path.delimiter)) {
|
|
85
|
-
if (!directory)
|
|
86
|
-
continue;
|
|
87
|
-
for (const extension of extensions) {
|
|
88
|
-
const candidate = path.resolve(directory, `${requested}${extension}`);
|
|
89
|
-
try {
|
|
90
|
-
fs.accessSync(candidate, fs.constants.X_OK);
|
|
91
|
-
return candidate;
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
// Continue through the frozen search path.
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
throw new UsageError(`Executable ${JSON.stringify(requested)} was not found.`, "INVALID_FLAG_VALUE");
|
|
99
|
-
}
|
|
100
|
-
function invalid(label) {
|
|
101
|
-
return new UsageError(`Frozen ${label} identity is invalid.`, "INVALID_FLAG_VALUE");
|
|
102
|
-
}
|
|
103
|
-
function changed(label) {
|
|
104
|
-
return new UsageError(`Frozen ${label} changed after publication.`, "RESOURCE_ALREADY_EXISTS");
|
|
105
|
-
}
|
|
@@ -1,427 +0,0 @@
|
|
|
1
|
-
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
-
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
-
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
|
-
import fs from "node:fs";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { parseFrontmatter } from "../core/asset/frontmatter.js";
|
|
8
|
-
import { compareCodePoints, toPosix } from "../core/common.js";
|
|
9
|
-
import { UsageError } from "../core/errors.js";
|
|
10
|
-
import { createExecutionSourceIdentity } from "./source.js";
|
|
11
|
-
function errorMessage(cause) {
|
|
12
|
-
return cause instanceof Error ? cause.message : String(cause);
|
|
13
|
-
}
|
|
14
|
-
function physicalIdentity(realPath, stat) {
|
|
15
|
-
return stat.ino === 0n ? `path:${realPath}` : `inode:${stat.dev}:${stat.ino}`;
|
|
16
|
-
}
|
|
17
|
-
function statVersion(stat) {
|
|
18
|
-
return `${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
19
|
-
}
|
|
20
|
-
function sameBigIntStat(left, right) {
|
|
21
|
-
return (left.dev === right.dev &&
|
|
22
|
-
left.ino === right.ino &&
|
|
23
|
-
left.mode === right.mode &&
|
|
24
|
-
left.size === right.size &&
|
|
25
|
-
left.mtimeNs === right.mtimeNs &&
|
|
26
|
-
left.ctimeNs === right.ctimeNs);
|
|
27
|
-
}
|
|
28
|
-
function containedRelative(root, candidate, allowRoot) {
|
|
29
|
-
const relative = path.relative(root, candidate);
|
|
30
|
-
if ((!allowRoot && relative === "") || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
31
|
-
throw new UsageError(`${candidate} resolves outside its guarded containment root.`, "PATH_ESCAPE_VIOLATION");
|
|
32
|
-
}
|
|
33
|
-
return relative;
|
|
34
|
-
}
|
|
35
|
-
function requireContainmentRoot(containmentRootInput) {
|
|
36
|
-
const containmentRoot = path.resolve(containmentRootInput);
|
|
37
|
-
const containmentRealPath = fs.realpathSync(containmentRoot);
|
|
38
|
-
const containmentStat = fs.statSync(containmentRealPath, { bigint: true });
|
|
39
|
-
if (!containmentStat.isDirectory()) {
|
|
40
|
-
throw new UsageError(`${containmentRootInput} is not a guarded source directory.`, "INVALID_FLAG_VALUE");
|
|
41
|
-
}
|
|
42
|
-
return { containmentRoot, containmentRealPath, containmentStat };
|
|
43
|
-
}
|
|
44
|
-
function numberStat(stat) {
|
|
45
|
-
return new Proxy(stat, {
|
|
46
|
-
get(target, property, receiver) {
|
|
47
|
-
const value = Reflect.get(target, property, receiver);
|
|
48
|
-
return typeof value === "bigint" ? Number(value) : value;
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
function decodeUtf8(bytes, sourcePath) {
|
|
53
|
-
try {
|
|
54
|
-
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
throw new UsageError(`${sourcePath} contains invalid UTF-8 bytes.`, "INVALID_FLAG_VALUE");
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function freezeSource(source) {
|
|
61
|
-
const identity = source.identity ? createExecutionSourceIdentity(source.identity) : undefined;
|
|
62
|
-
return Object.freeze({ ...source, ...(identity ? { identity } : {}) });
|
|
63
|
-
}
|
|
64
|
-
function captureRecord(sourcePathInput, containmentRootInput, options = {}) {
|
|
65
|
-
const sourcePath = path.resolve(sourcePathInput);
|
|
66
|
-
const { containmentRoot, containmentRealPath, containmentStat } = requireContainmentRoot(containmentRootInput);
|
|
67
|
-
const lexicalRelative = containedRelative(containmentRoot, sourcePath, false);
|
|
68
|
-
const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
69
|
-
let descriptor;
|
|
70
|
-
try {
|
|
71
|
-
descriptor = fs.openSync(sourcePath, fs.constants.O_RDONLY | noFollow);
|
|
72
|
-
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
73
|
-
if (!before.isFile()) {
|
|
74
|
-
throw new UsageError(`${sourcePath} is not a regular guarded source file.`, "INVALID_FLAG_VALUE");
|
|
75
|
-
}
|
|
76
|
-
const bytes = fs.readFileSync(descriptor);
|
|
77
|
-
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
78
|
-
if (!sameBigIntStat(before, after) || BigInt(bytes.byteLength) !== before.size) {
|
|
79
|
-
throw new UsageError(`${sourcePath} changed while its guarded bytes were read.`, "RESOURCE_ALREADY_EXISTS");
|
|
80
|
-
}
|
|
81
|
-
const pathnameStat = fs.lstatSync(sourcePath, { bigint: true });
|
|
82
|
-
if (pathnameStat.isSymbolicLink()) {
|
|
83
|
-
throw new UsageError(`${sourcePath} is a symbolic source; guarded reads require a regular no-follow owner.`, "RESOURCE_ALREADY_EXISTS");
|
|
84
|
-
}
|
|
85
|
-
if (!pathnameStat.isFile() || pathnameStat.dev !== before.dev || pathnameStat.ino !== before.ino) {
|
|
86
|
-
throw new UsageError(`${sourcePath} changed physical identity during its guarded read.`, "RESOURCE_ALREADY_EXISTS");
|
|
87
|
-
}
|
|
88
|
-
const realPath = fs.realpathSync(sourcePath);
|
|
89
|
-
containedRelative(containmentRealPath, realPath, false);
|
|
90
|
-
const content = decodeUtf8(bytes, sourcePath);
|
|
91
|
-
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
92
|
-
if (options.identity && options.identity.hash !== sha256) {
|
|
93
|
-
throw new UsageError(`${sourcePath} adapter identity hash does not match its guarded bytes.`, "RESOURCE_ALREADY_EXISTS");
|
|
94
|
-
}
|
|
95
|
-
if (options.identity && options.identity.file !== toPosix(lexicalRelative)) {
|
|
96
|
-
throw new UsageError(`${sourcePath} adapter identity file does not match its guarded relative path.`, "RESOURCE_ALREADY_EXISTS");
|
|
97
|
-
}
|
|
98
|
-
return {
|
|
99
|
-
source: freezeSource({
|
|
100
|
-
sourcePath,
|
|
101
|
-
relativePath: toPosix(lexicalRelative),
|
|
102
|
-
realPath,
|
|
103
|
-
containmentRoot,
|
|
104
|
-
containmentRealPath,
|
|
105
|
-
containmentPhysicalIdentity: physicalIdentity(containmentRealPath, containmentStat),
|
|
106
|
-
physicalIdentity: physicalIdentity(realPath, before),
|
|
107
|
-
size: bytes.byteLength,
|
|
108
|
-
mtimeNs: String(before.mtimeNs),
|
|
109
|
-
ctimeNs: String(before.ctimeNs),
|
|
110
|
-
sha256,
|
|
111
|
-
bytesBase64: bytes.toString("base64"),
|
|
112
|
-
content,
|
|
113
|
-
authored: options.authored ?? false,
|
|
114
|
-
...(options.identity ? { identity: options.identity } : {}),
|
|
115
|
-
}),
|
|
116
|
-
stat: numberStat(before),
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
catch (cause) {
|
|
120
|
-
if (cause instanceof UsageError)
|
|
121
|
-
throw cause;
|
|
122
|
-
const code = cause.code;
|
|
123
|
-
if (code === "ELOOP") {
|
|
124
|
-
let linked;
|
|
125
|
-
try {
|
|
126
|
-
linked = fs.realpathSync(sourcePath);
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
// The no-follow failure itself is sufficient when the link is broken.
|
|
130
|
-
}
|
|
131
|
-
if (linked) {
|
|
132
|
-
const relative = path.relative(containmentRealPath, linked);
|
|
133
|
-
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
134
|
-
throw new UsageError(`${sourcePath} resolves outside its containment root through a symbolic source.`, "PATH_ESCAPE_VIOLATION");
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
throw new UsageError(`${sourcePath} is a symbolic source; guarded reads require a regular no-follow owner.`, "RESOURCE_ALREADY_EXISTS");
|
|
138
|
-
}
|
|
139
|
-
throw new UsageError(`${sourcePath} could not be guarded as a contained regular source: ${errorMessage(cause)}`, code === "ENOENT" ? "RESOURCE_ALREADY_EXISTS" : "PATH_ESCAPE_VIOLATION");
|
|
140
|
-
}
|
|
141
|
-
finally {
|
|
142
|
-
if (descriptor !== undefined)
|
|
143
|
-
fs.closeSync(descriptor);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
export function captureGuardedExecutionSource(sourcePath, containmentRoot, options = {}) {
|
|
147
|
-
return captureRecord(sourcePath, containmentRoot, options).source;
|
|
148
|
-
}
|
|
149
|
-
export function captureGuardedDirectoryManifest(directoryPathInput, containmentRootInput) {
|
|
150
|
-
const directoryPath = path.resolve(directoryPathInput);
|
|
151
|
-
const { containmentRoot, containmentRealPath, containmentStat } = requireContainmentRoot(containmentRootInput);
|
|
152
|
-
const lexicalRelative = containedRelative(containmentRoot, directoryPath, true);
|
|
153
|
-
try {
|
|
154
|
-
const before = fs.lstatSync(directoryPath, { bigint: true });
|
|
155
|
-
if (before.isSymbolicLink()) {
|
|
156
|
-
throw new UsageError(`${directoryPath} is a symbolic directory with ambiguous guarded identity.`, "RESOURCE_ALREADY_EXISTS");
|
|
157
|
-
}
|
|
158
|
-
if (!before.isDirectory()) {
|
|
159
|
-
throw new UsageError(`${directoryPath} is not a guarded directory.`, "INVALID_FLAG_VALUE");
|
|
160
|
-
}
|
|
161
|
-
const realPath = fs.realpathSync(directoryPath);
|
|
162
|
-
containedRelative(containmentRealPath, realPath, true);
|
|
163
|
-
const entries = fs
|
|
164
|
-
.readdirSync(directoryPath, { withFileTypes: true })
|
|
165
|
-
.sort((left, right) => compareCodePoints(left.name, right.name))
|
|
166
|
-
.map((entry) => {
|
|
167
|
-
const entryPath = path.join(directoryPath, entry.name);
|
|
168
|
-
const entryStat = fs.lstatSync(entryPath, { bigint: true });
|
|
169
|
-
if (entry.isSymbolicLink() || entryStat.isSymbolicLink()) {
|
|
170
|
-
let linked;
|
|
171
|
-
try {
|
|
172
|
-
linked = fs.realpathSync(entryPath);
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
175
|
-
// Broken symbolic entries still have ambiguous ownership.
|
|
176
|
-
}
|
|
177
|
-
if (linked) {
|
|
178
|
-
const relative = path.relative(containmentRealPath, linked);
|
|
179
|
-
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
180
|
-
throw new UsageError(`${entryPath} resolves outside the bundle root through a symbolic source.`, "PATH_ESCAPE_VIOLATION");
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
throw new UsageError(`${entryPath} is a symbolic source with a physical source identity collision; guarded reads require one no-follow owner.`, "RESOURCE_ALREADY_EXISTS");
|
|
184
|
-
}
|
|
185
|
-
const kind = entry.isDirectory() && entryStat.isDirectory()
|
|
186
|
-
? "directory"
|
|
187
|
-
: entry.isFile() && entryStat.isFile()
|
|
188
|
-
? "file"
|
|
189
|
-
: undefined;
|
|
190
|
-
if (!kind) {
|
|
191
|
-
throw new UsageError(`${entryPath} is neither a regular file nor directory.`, "INVALID_FLAG_VALUE");
|
|
192
|
-
}
|
|
193
|
-
const entryRealPath = fs.realpathSync(entryPath);
|
|
194
|
-
containedRelative(containmentRealPath, entryRealPath, false);
|
|
195
|
-
return Object.freeze({
|
|
196
|
-
name: entry.name,
|
|
197
|
-
kind,
|
|
198
|
-
physicalIdentity: physicalIdentity(entryRealPath, entryStat),
|
|
199
|
-
version: statVersion(entryStat),
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
const after = fs.lstatSync(directoryPath, { bigint: true });
|
|
203
|
-
if (!sameBigIntStat(before, after)) {
|
|
204
|
-
throw new UsageError(`${directoryPath} changed while its guarded directory manifest was read.`, "RESOURCE_ALREADY_EXISTS");
|
|
205
|
-
}
|
|
206
|
-
return Object.freeze({
|
|
207
|
-
directoryPath,
|
|
208
|
-
relativePath: lexicalRelative === "" ? "." : toPosix(lexicalRelative),
|
|
209
|
-
realPath,
|
|
210
|
-
containmentRoot,
|
|
211
|
-
containmentRealPath,
|
|
212
|
-
containmentPhysicalIdentity: physicalIdentity(containmentRealPath, containmentStat),
|
|
213
|
-
physicalIdentity: physicalIdentity(realPath, before),
|
|
214
|
-
version: statVersion(before),
|
|
215
|
-
entries: Object.freeze(entries),
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
catch (cause) {
|
|
219
|
-
if (cause instanceof UsageError)
|
|
220
|
-
throw cause;
|
|
221
|
-
throw new UsageError(`${directoryPath} could not be captured as a guarded directory manifest: ${errorMessage(cause)}`, cause.code === "ENOENT" ? "RESOURCE_ALREADY_EXISTS" : "PATH_ESCAPE_VIOLATION");
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
function sameSnapshotValue(left, right) {
|
|
225
|
-
return JSON.stringify(left) === JSON.stringify(right);
|
|
226
|
-
}
|
|
227
|
-
function sourceSortKey(source) {
|
|
228
|
-
const identity = source.identity;
|
|
229
|
-
return identity
|
|
230
|
-
? `${identity.ref}\0${identity.adapter}\0${identity.file}`
|
|
231
|
-
: `${source.containmentRealPath}\0${source.relativePath}`;
|
|
232
|
-
}
|
|
233
|
-
function directorySortKey(manifest) {
|
|
234
|
-
return `${manifest.containmentRealPath}\0${manifest.relativePath}`;
|
|
235
|
-
}
|
|
236
|
-
export class GuardedExecutionSourceCollector {
|
|
237
|
-
#sources = new Map();
|
|
238
|
-
#directories = new Map();
|
|
239
|
-
capture(sourcePath, containmentRoot, options = {}) {
|
|
240
|
-
const key = path.resolve(sourcePath);
|
|
241
|
-
const existing = this.#sources.get(key);
|
|
242
|
-
if (existing) {
|
|
243
|
-
if (options.identity)
|
|
244
|
-
return this.bindIdentity(sourcePath, containmentRoot, options.identity);
|
|
245
|
-
return existing.source;
|
|
246
|
-
}
|
|
247
|
-
const record = captureRecord(sourcePath, containmentRoot, options);
|
|
248
|
-
this.#assertNoPhysicalOwnerAlias(record.source);
|
|
249
|
-
this.#sources.set(key, record);
|
|
250
|
-
return record.source;
|
|
251
|
-
}
|
|
252
|
-
readBytes(sourcePath, containmentRoot, options = {}) {
|
|
253
|
-
return Buffer.from(this.capture(sourcePath, containmentRoot, options).bytesBase64, "base64");
|
|
254
|
-
}
|
|
255
|
-
fileContext(root, file, options = {}) {
|
|
256
|
-
const source = this.capture(file, root, options);
|
|
257
|
-
const record = this.#sources.get(path.resolve(file));
|
|
258
|
-
if (!record)
|
|
259
|
-
throw new Error("guarded source collector lost a captured file");
|
|
260
|
-
const relPath = source.relativePath;
|
|
261
|
-
const ext = path.extname(file).toLowerCase();
|
|
262
|
-
const fileName = path.basename(file);
|
|
263
|
-
const parentDirAbs = path.dirname(path.resolve(file));
|
|
264
|
-
const relDir = path.posix.dirname(relPath);
|
|
265
|
-
const ancestorDirs = relDir === "." ? [] : relDir.split("/").filter(Boolean);
|
|
266
|
-
let frontmatter;
|
|
267
|
-
let parsed = false;
|
|
268
|
-
return {
|
|
269
|
-
absPath: path.resolve(file),
|
|
270
|
-
relPath,
|
|
271
|
-
ext,
|
|
272
|
-
fileName,
|
|
273
|
-
parentDir: path.basename(parentDirAbs),
|
|
274
|
-
parentDirAbs,
|
|
275
|
-
ancestorDirs,
|
|
276
|
-
stashRoot: path.resolve(root),
|
|
277
|
-
content: () => source.content,
|
|
278
|
-
frontmatter: () => {
|
|
279
|
-
if (!parsed) {
|
|
280
|
-
const result = parseFrontmatter(source.content);
|
|
281
|
-
frontmatter = Object.keys(result.data).length > 0 ? result.data : null;
|
|
282
|
-
parsed = true;
|
|
283
|
-
}
|
|
284
|
-
return frontmatter ?? null;
|
|
285
|
-
},
|
|
286
|
-
stat: () => record.stat,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
bindIdentity(sourcePath, containmentRoot, identityInput) {
|
|
290
|
-
const key = path.resolve(sourcePath);
|
|
291
|
-
const existing = this.#sources.get(key) ?? captureRecord(sourcePath, containmentRoot);
|
|
292
|
-
const identity = createExecutionSourceIdentity(identityInput);
|
|
293
|
-
if (identity.file !== existing.source.relativePath || identity.hash !== existing.source.sha256) {
|
|
294
|
-
throw new UsageError(`${sourcePath} adapter identity does not match its guarded file owner or bytes.`, "RESOURCE_ALREADY_EXISTS");
|
|
295
|
-
}
|
|
296
|
-
if (existing.source.identity) {
|
|
297
|
-
if (!sameSnapshotValue(existing.source.identity, identity)) {
|
|
298
|
-
throw new UsageError(`${sourcePath} already has a different guarded logical owner identity.`, "RESOURCE_ALREADY_EXISTS");
|
|
299
|
-
}
|
|
300
|
-
return existing.source;
|
|
301
|
-
}
|
|
302
|
-
const source = freezeSource({ ...existing.source, identity });
|
|
303
|
-
this.#assertNoPhysicalOwnerAlias(source, key);
|
|
304
|
-
this.#sources.set(key, { ...existing, source });
|
|
305
|
-
return source;
|
|
306
|
-
}
|
|
307
|
-
trackDirectory(directoryPath, containmentRoot) {
|
|
308
|
-
const manifest = captureGuardedDirectoryManifest(directoryPath, containmentRoot);
|
|
309
|
-
const key = path.resolve(directoryPath);
|
|
310
|
-
const existing = this.#directories.get(key);
|
|
311
|
-
if (existing && !sameSnapshotValue(existing, manifest)) {
|
|
312
|
-
throw new UsageError(`${directoryPath} changed between guarded directory reads.`, "RESOURCE_ALREADY_EXISTS");
|
|
313
|
-
}
|
|
314
|
-
if (!existing)
|
|
315
|
-
this.#directories.set(key, manifest);
|
|
316
|
-
return existing ?? manifest;
|
|
317
|
-
}
|
|
318
|
-
enumerateTree(directoryPath, containmentRoot) {
|
|
319
|
-
const files = [];
|
|
320
|
-
const visit = (directory) => {
|
|
321
|
-
const manifest = this.trackDirectory(directory, containmentRoot);
|
|
322
|
-
for (const entry of manifest.entries) {
|
|
323
|
-
const candidate = path.join(directory, entry.name);
|
|
324
|
-
if (entry.kind === "directory")
|
|
325
|
-
visit(candidate);
|
|
326
|
-
else
|
|
327
|
-
files.push(candidate);
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
visit(path.resolve(directoryPath));
|
|
331
|
-
return Object.freeze(files.sort(compareCodePoints));
|
|
332
|
-
}
|
|
333
|
-
snapshot() {
|
|
334
|
-
return Object.freeze({
|
|
335
|
-
sources: Object.freeze([...this.#sources.values()]
|
|
336
|
-
.map((record) => record.source)
|
|
337
|
-
.sort((left, right) => compareCodePoints(sourceSortKey(left), sourceSortKey(right)))),
|
|
338
|
-
directoryManifests: Object.freeze([...this.#directories.values()].sort((left, right) => compareCodePoints(directorySortKey(left), directorySortKey(right)))),
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Merge another collector's captured sources and directory manifests into
|
|
343
|
-
* this one (A-N7, spec docs/plans/specs/p3a-plan-v5-child-freeze.md §4.2
|
|
344
|
-
* step 6). The recursive child-workflow freeze gives each child its OWN
|
|
345
|
-
* fresh collector, so a child's plan (and therefore its `planHash`) is a
|
|
346
|
-
* pure function of its own source, independent of its position in the
|
|
347
|
-
* parent (A-N7's rejected alternative: sharing the parent's collector,
|
|
348
|
-
* which would make an identical child hash differently depending on what
|
|
349
|
-
* the parent had already captured). The parent then absorbs the child's
|
|
350
|
-
* records so its own final pre-publication CAS ({@link revalidate}) covers
|
|
351
|
-
* every child file too.
|
|
352
|
-
*
|
|
353
|
-
* Re-runs `#assertNoPhysicalOwnerAlias` for each newly-absorbed source
|
|
354
|
-
* record, so the shared-physical-owner authority holds ACROSS the
|
|
355
|
-
* composition, not just within one workflow's own freeze. A record whose
|
|
356
|
-
* key (resolved source path) is already present is required to be
|
|
357
|
-
* byte-identical to the one already held — two DISJOINT branches
|
|
358
|
-
* composing the SAME child (a diamond, not a cycle) absorb the same file
|
|
359
|
-
* twice with identical content and pass silently; two branches that
|
|
360
|
-
* somehow captured the same path with different content fail closed.
|
|
361
|
-
*/
|
|
362
|
-
absorb(other) {
|
|
363
|
-
for (const [key, record] of other.#sources) {
|
|
364
|
-
const existing = this.#sources.get(key);
|
|
365
|
-
if (existing) {
|
|
366
|
-
if (!sameSnapshotValue(existing.source, record.source)) {
|
|
367
|
-
throw new UsageError(`${record.source.sourcePath} was captured with conflicting content across a composed workflow freeze.`, "RESOURCE_ALREADY_EXISTS");
|
|
368
|
-
}
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
this.#assertNoPhysicalOwnerAlias(record.source);
|
|
372
|
-
this.#sources.set(key, record);
|
|
373
|
-
}
|
|
374
|
-
for (const [key, manifest] of other.#directories) {
|
|
375
|
-
const existing = this.#directories.get(key);
|
|
376
|
-
if (existing) {
|
|
377
|
-
if (!sameSnapshotValue(existing, manifest)) {
|
|
378
|
-
throw new UsageError(`${manifest.directoryPath} changed between guarded directory reads across a composed workflow freeze.`, "RESOURCE_ALREADY_EXISTS");
|
|
379
|
-
}
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
this.#directories.set(key, manifest);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
revalidate() {
|
|
386
|
-
for (const record of this.#sources.values()) {
|
|
387
|
-
let current;
|
|
388
|
-
try {
|
|
389
|
-
current = captureRecord(record.source.sourcePath, record.source.containmentRoot, {
|
|
390
|
-
authored: record.source.authored,
|
|
391
|
-
...(record.source.identity ? { identity: record.source.identity } : {}),
|
|
392
|
-
}).source;
|
|
393
|
-
}
|
|
394
|
-
catch (cause) {
|
|
395
|
-
throw new UsageError(`Guarded execution source read set changed before publication: ${errorMessage(cause)}`, "RESOURCE_ALREADY_EXISTS");
|
|
396
|
-
}
|
|
397
|
-
if (!sameSnapshotValue(record.source, current)) {
|
|
398
|
-
throw new UsageError("Guarded execution source read set changed before publication.", "RESOURCE_ALREADY_EXISTS");
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
for (const manifest of this.#directories.values()) {
|
|
402
|
-
let current;
|
|
403
|
-
try {
|
|
404
|
-
current = captureGuardedDirectoryManifest(manifest.directoryPath, manifest.containmentRoot);
|
|
405
|
-
}
|
|
406
|
-
catch (cause) {
|
|
407
|
-
throw new UsageError(`Guarded execution source directory manifest changed before publication: ${errorMessage(cause)}`, "RESOURCE_ALREADY_EXISTS");
|
|
408
|
-
}
|
|
409
|
-
if (!sameSnapshotValue(manifest, current)) {
|
|
410
|
-
throw new UsageError("Guarded execution source directory manifest changed before publication.", "RESOURCE_ALREADY_EXISTS");
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
#assertNoPhysicalOwnerAlias(candidate, selfKey) {
|
|
415
|
-
if (!candidate.identity)
|
|
416
|
-
return;
|
|
417
|
-
for (const [key, record] of this.#sources) {
|
|
418
|
-
if (key === selfKey || !record.source.identity)
|
|
419
|
-
continue;
|
|
420
|
-
if (record.source.containmentPhysicalIdentity === candidate.containmentPhysicalIdentity &&
|
|
421
|
-
record.source.physicalIdentity === candidate.physicalIdentity &&
|
|
422
|
-
!sameSnapshotValue(record.source.identity, candidate.identity)) {
|
|
423
|
-
throw new UsageError(`${candidate.sourcePath} aliases the same physical source and root under a different logical owner.`, "RESOURCE_ALREADY_EXISTS");
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|