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,446 +1,147 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Scheduler reconciliation: compile the desired bindings from a bundle's
|
|
6
|
+
* task and workflow sources, then diff them against the rows the native
|
|
7
|
+
* scheduler holds.
|
|
8
|
+
*
|
|
9
|
+
* `compileSchedulerSources` reads each source once. One source that fails
|
|
10
|
+
* to parse or prepare is reported and skipped; every other source still
|
|
11
|
+
* compiles (#867). `planSchedulerSync` is pure: given the desired bindings,
|
|
12
|
+
* the installed rows, and the bundles this sync owns, it decides which rows
|
|
13
|
+
* to install, update, remove, or leave alone. Rows it cannot attribute to
|
|
14
|
+
* one of those bundles are never touched.
|
|
15
|
+
*/
|
|
16
|
+
import fs from "node:fs";
|
|
6
17
|
import path from "node:path";
|
|
7
|
-
import {
|
|
8
|
-
import { makeBundleRef } from "../core/asset/asset-ref.js";
|
|
18
|
+
import { makeBundleRef, parseBundleRef } from "../core/asset/asset-ref.js";
|
|
9
19
|
import { compareCodePoints, toPosix } from "../core/common.js";
|
|
10
20
|
import { UsageError } from "../core/errors.js";
|
|
11
21
|
import { canonicalizeWorkflowName, WORKFLOW_EXTENSIONS } from "../core/recognition-util.js";
|
|
12
|
-
import { captureGuardedDirectoryManifest, captureGuardedExecutionSource, GuardedExecutionSourceCollector, } from "../execution/guarded-source.js";
|
|
13
22
|
import { applyInputDefaults, validateInputs } from "../execution/input-contract.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { canonicalJson, computePlanHash } from "../workflows/ir/plan-hash.js";
|
|
17
|
-
import { WorkflowSourceCollisionError, WorkflowSourceNameError, WorkflowSourceRejectionError, workflowNameForSourcePath, } from "../workflows/source-files.js";
|
|
18
|
-
import { compileWorkflowSource } from "../workflows/source-ir/compile.js";
|
|
23
|
+
import { checkWorkflowPlan, compileWorkflowSource } from "../workflows/compile.js";
|
|
24
|
+
import { WorkflowSourceCollisionError, WorkflowSourceNameError, workflowNameForSourcePath, } from "../workflows/source-files.js";
|
|
19
25
|
import { prepareTaskV3Execution } from "./prepare/prepare.js";
|
|
20
26
|
import { parseSchedule } from "./schedule.js";
|
|
21
|
-
import {
|
|
27
|
+
import { compileTaskSchedulerBindings, compileWorkflowSchedulerBindings, schedulerBindingNativeId, schedulerNativeBindingId, } from "./scheduler-binding.js";
|
|
22
28
|
import { parseTaskSource } from "./source/parse-task-source.js";
|
|
23
29
|
import { projectTaskSourceV4 } from "./source/project-v4.js";
|
|
24
30
|
import { taskSourceErrorDetail } from "./source-v3.js";
|
|
25
|
-
export function computeSchedulerExecutionEvidenceDigest(planHash, sourceReadSet) {
|
|
26
|
-
const envelope = canonicalJson({
|
|
27
|
-
version: 1,
|
|
28
|
-
planHash,
|
|
29
|
-
sourceReadSet: sourceReadSet.map((snapshot) => ({
|
|
30
|
-
identity: snapshot.identity,
|
|
31
|
-
containmentPhysicalIdentity: snapshot.containmentPhysicalIdentity,
|
|
32
|
-
physicalIdentity: snapshot.physicalIdentity,
|
|
33
|
-
size: snapshot.size,
|
|
34
|
-
})),
|
|
35
|
-
});
|
|
36
|
-
return createHash("sha256").update("akm.scheduler.workflow-evidence\0v1\0").update(envelope).digest("hex");
|
|
37
|
-
}
|
|
38
31
|
const SCHEDULER_PROJECTION_CONFIG = Object.freeze({
|
|
39
32
|
configVersion: "0.9.0",
|
|
40
33
|
semanticSearchMode: "off",
|
|
41
34
|
});
|
|
42
|
-
export async function
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const sourceSnapshot = collector.snapshot();
|
|
46
|
-
assertSchedulerSourceSnapshot(sourceSnapshot);
|
|
47
|
-
return Object.freeze({
|
|
48
|
-
desired: compiled.desired,
|
|
49
|
-
sourceSnapshot,
|
|
50
|
-
executableWorkflows: compiled.executableWorkflows,
|
|
51
|
-
failures: compiled.failures,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
function schedulerActivationKey(kind, ref) {
|
|
55
|
-
return `${kind}\0${ref}`;
|
|
56
|
-
}
|
|
57
|
-
export function finalizeSchedulerSyncPlan(input, prepared) {
|
|
58
|
-
const inspection = inspectionForPlan(input);
|
|
59
|
-
const coherentInput = {
|
|
60
|
-
...input,
|
|
61
|
-
installed: inspection.installed,
|
|
62
|
-
nativeArtifacts: inspection.artifacts,
|
|
63
|
-
};
|
|
64
|
-
const desired = prepared.desired;
|
|
65
|
-
assertUniqueDesiredIds(desired);
|
|
66
|
-
assertSchedulerBackendInspection(inspection, desired, input.inspection !== undefined);
|
|
67
|
-
assertNoForeignIds(desired, coherentInput);
|
|
68
|
-
const scopedInstalled = coherentInput.installed.filter((entry) => belongsToBundle(entry, coherentInput));
|
|
69
|
-
const present = new Map(scopedInstalled.map((entry) => [entry.id, entry]));
|
|
70
|
-
const installed = [];
|
|
71
|
-
const updated = [];
|
|
72
|
-
const unchanged = [];
|
|
73
|
-
const operations = [];
|
|
74
|
-
for (const binding of desired) {
|
|
75
|
-
const current = present.get(binding.id);
|
|
76
|
-
const options = installOptionsFor(coherentInput, current);
|
|
77
|
-
const resultFingerprint = coherentInput.expectedSignature?.(binding, options);
|
|
78
|
-
if (!current) {
|
|
79
|
-
installed.push(binding.id);
|
|
80
|
-
operations.push(freezeOperation({
|
|
81
|
-
kind: "install",
|
|
82
|
-
binding,
|
|
83
|
-
expected: freezeMutationExpectation(expectationForBinding(binding, "absent")),
|
|
84
|
-
...(resultFingerprint !== undefined ? { resultFingerprint } : {}),
|
|
85
|
-
...(options ? { options } : {}),
|
|
86
|
-
}));
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
if (current.signature !== undefined && resultFingerprint !== undefined && current.signature === resultFingerprint) {
|
|
90
|
-
unchanged.push(binding.id);
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
const artifact = exactInstalledArtifact(binding.id, current, inspection.artifacts);
|
|
94
|
-
const priorFingerprint = artifact?.fingerprint ?? current.signature;
|
|
95
|
-
if (artifact === undefined || priorFingerprint === undefined) {
|
|
96
|
-
throw new UsageError(`Installed scheduler binding ${JSON.stringify(binding.id)} has no exact native fingerprint; refusing update.`, "RESOURCE_ALREADY_EXISTS");
|
|
97
|
-
}
|
|
98
|
-
updated.push(binding.id);
|
|
99
|
-
operations.push(freezeOperation({
|
|
100
|
-
kind: "update",
|
|
101
|
-
binding,
|
|
102
|
-
expected: freezeMutationExpectation(expectationForBinding(binding, "present", priorFingerprint)),
|
|
103
|
-
...(resultFingerprint !== undefined ? { resultFingerprint } : {}),
|
|
104
|
-
...(options ? { options } : {}),
|
|
105
|
-
}));
|
|
106
|
-
}
|
|
107
|
-
const desiredIds = new Set(desired.map(({ id }) => id));
|
|
108
|
-
const removed = scopedInstalled
|
|
109
|
-
.map(({ id }) => id)
|
|
110
|
-
.filter((id) => !desiredIds.has(id))
|
|
111
|
-
.sort(compareCodePoints);
|
|
112
|
-
for (const id of removed) {
|
|
113
|
-
const current = present.get(id);
|
|
114
|
-
operations.push(buildSchedulerRemoveOperation(id, current, inspection.artifacts, coherentInput));
|
|
115
|
-
}
|
|
116
|
-
return Object.freeze({
|
|
117
|
-
desired,
|
|
118
|
-
installed: Object.freeze(installed),
|
|
119
|
-
updated: Object.freeze(updated),
|
|
120
|
-
removed: Object.freeze(removed),
|
|
121
|
-
unchanged: Object.freeze(unchanged),
|
|
122
|
-
operations: Object.freeze(operations),
|
|
123
|
-
sourceSnapshot: prepared.sourceSnapshot,
|
|
124
|
-
failures: prepared.failures,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
/** Validate one coherent whole-backend read before deriving any mutation plan. */
|
|
128
|
-
export function assertSchedulerBackendInspection(inspection, desired = [], requireCompleteFingerprint = true) {
|
|
129
|
-
assertCoherentInspection(inspection, requireCompleteFingerprint);
|
|
130
|
-
assertUniqueInstalledIds(inspection.installed);
|
|
131
|
-
assertSchedulerNativeArtifactOwnership(desired, inspection.artifacts);
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Build the exact removal operation for one installed binding: same
|
|
135
|
-
* exact-native-fingerprint / ordinal-attribution safety checks
|
|
136
|
-
* `finalizeSchedulerSyncPlan`'s remove loop always applied, factored out so
|
|
137
|
-
* `akm task prune` (#851) can build removal operations for entries
|
|
138
|
-
* `belongsToBundle` structurally can't see (unresolvable ownership) without
|
|
139
|
-
* re-deriving — or weakening — this logic. Throws the same `UsageError`s a
|
|
140
|
-
* sync removal would on an inexact match; callers computing prune candidates
|
|
141
|
-
* should only pass entries they've already independently confirmed are safe
|
|
142
|
-
* to remove.
|
|
143
|
-
*/
|
|
144
|
-
export function buildSchedulerRemoveOperation(id, current, artifacts, input) {
|
|
145
|
-
if (!current?.invocation) {
|
|
146
|
-
throw nativeArtifactCollision({ nativeId: current?.nativeId ?? schedulerNativeBindingId(id), bindingId: id }, { nativeId: current?.nativeId ?? schedulerNativeBindingId(id) });
|
|
147
|
-
}
|
|
148
|
-
const nativeId = exactInstalledNativeId(id, current, artifacts);
|
|
149
|
-
const artifact = artifacts.find((candidate) => candidate.nativeId === nativeId && candidate.bindingId === id);
|
|
150
|
-
const priorFingerprint = current.signature ?? artifact?.fingerprint;
|
|
151
|
-
if (!artifact || priorFingerprint === undefined) {
|
|
152
|
-
throw new UsageError(`Installed scheduler binding ${JSON.stringify(id)} has no exact native fingerprint; refusing removal.`, "RESOURCE_ALREADY_EXISTS");
|
|
153
|
-
}
|
|
154
|
-
const logicalSource = installedLogicalSource(current.invocation, input);
|
|
155
|
-
const ordinal = schedulerBindingOrdinal(id, logicalSource, current.invocation);
|
|
156
|
-
if (ordinal === undefined) {
|
|
157
|
-
throw new UsageError(`Installed scheduler binding ${JSON.stringify(id)} cannot be attributed to an exact schedule ordinal; refusing removal.`, "RESOURCE_ALREADY_EXISTS");
|
|
158
|
-
}
|
|
159
|
-
return Object.freeze({
|
|
160
|
-
kind: "remove",
|
|
161
|
-
id,
|
|
162
|
-
nativeId,
|
|
163
|
-
expected: freezeRemovalExpectation({
|
|
164
|
-
state: "present",
|
|
165
|
-
bindingId: id,
|
|
166
|
-
nativeId,
|
|
167
|
-
logicalSource,
|
|
168
|
-
ordinal,
|
|
169
|
-
invocation: current.invocation,
|
|
170
|
-
fingerprint: priorFingerprint,
|
|
171
|
-
}),
|
|
172
|
-
...(current.ownerBundlePath !== undefined ? { ownerBundlePath: current.ownerBundlePath } : {}),
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
function exactInstalledNativeId(logicalId, current, artifacts) {
|
|
176
|
-
const exact = artifacts.find((artifact) => artifact.bindingId === logicalId);
|
|
177
|
-
return exact?.nativeId ?? current?.nativeId ?? schedulerNativeBindingId(logicalId);
|
|
178
|
-
}
|
|
179
|
-
function exactInstalledArtifact(bindingId, current, artifacts) {
|
|
180
|
-
const nativeId = current.nativeId ?? schedulerNativeBindingId(bindingId);
|
|
181
|
-
return artifacts.find((artifact) => artifact.nativeId === nativeId && (artifact.bindingId === bindingId || artifact.bindingId === undefined));
|
|
182
|
-
}
|
|
183
|
-
function nativeArtifactsForPlan(input) {
|
|
184
|
-
return (input.nativeArtifacts ??
|
|
185
|
-
input.installed.map((entry) => ({
|
|
186
|
-
nativeId: entry.nativeId ?? schedulerNativeBindingId(entry.id),
|
|
187
|
-
bindingId: entry.id,
|
|
188
|
-
...(entry.invocation ? { invocation: Object.freeze([...entry.invocation]) } : {}),
|
|
189
|
-
...(entry.signature !== undefined ? { fingerprint: entry.signature } : {}),
|
|
190
|
-
})));
|
|
191
|
-
}
|
|
192
|
-
function inspectionForPlan(input) {
|
|
193
|
-
return input.inspection ?? Object.freeze({ installed: input.installed, artifacts: nativeArtifactsForPlan(input) });
|
|
194
|
-
}
|
|
195
|
-
export function assertSchedulerNativeArtifactOwnership(desired, installed) {
|
|
196
|
-
const desiredByKey = new Map();
|
|
197
|
-
for (const binding of desired) {
|
|
198
|
-
const nativeId = schedulerBindingNativeId(binding);
|
|
199
|
-
const key = schedulerNativeArtifactKey(nativeId);
|
|
200
|
-
const prior = desiredByKey.get(key);
|
|
201
|
-
if (prior && !sameDesiredArtifact(prior, binding)) {
|
|
202
|
-
throw nativeArtifactCollision(desiredArtifact(prior), desiredArtifact(binding));
|
|
203
|
-
}
|
|
204
|
-
desiredByKey.set(key, binding);
|
|
205
|
-
}
|
|
206
|
-
const installedByKey = new Map();
|
|
207
|
-
for (const artifact of installed) {
|
|
208
|
-
const key = schedulerNativeArtifactKey(artifact.nativeId);
|
|
209
|
-
const prior = installedByKey.get(key);
|
|
210
|
-
if (prior) {
|
|
211
|
-
throw nativeArtifactCollision(prior, artifact);
|
|
212
|
-
}
|
|
213
|
-
installedByKey.set(key, artifact);
|
|
214
|
-
const wanted = desiredByKey.get(key);
|
|
215
|
-
if (!wanted)
|
|
216
|
-
continue;
|
|
217
|
-
// Re-derive ownership from the artifact's own invocation content (not the
|
|
218
|
-
// caller-supplied `bindingId` label) so a proven owner whose invocation
|
|
219
|
-
// no longer matches the desired shape is an UPDATE, not a refusal — that
|
|
220
|
-
// reconciliation happens below in finalizeSchedulerSyncPlan. An artifact
|
|
221
|
-
// whose invocation content does not actually prove it belongs to
|
|
222
|
-
// `wanted` (unproven, malformed, or a different logical owner) is still
|
|
223
|
-
// a genuine collision.
|
|
224
|
-
const provenBindingId = artifact.invocation !== undefined
|
|
225
|
-
? schedulerNativeArtifactOwner(artifact.nativeId, artifact.invocation)?.logicalId
|
|
226
|
-
: undefined;
|
|
227
|
-
if (artifact.nativeId !== schedulerBindingNativeId(wanted) || provenBindingId !== wanted.id) {
|
|
228
|
-
throw nativeArtifactCollision(desiredArtifact(wanted), artifact);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
function assertCoherentInspection(inspection, requireCompleteFingerprint = false) {
|
|
233
|
-
const seenNativeKeys = new Set();
|
|
234
|
-
for (const artifact of inspection.artifacts) {
|
|
235
|
-
const key = schedulerNativeArtifactKey(artifact.nativeId);
|
|
236
|
-
if (seenNativeKeys.has(key)) {
|
|
237
|
-
throw new UsageError(`Scheduler inspection has duplicate normalized native artifact ${JSON.stringify(artifact.nativeId)}; expected cardinality one.`, "RESOURCE_ALREADY_EXISTS");
|
|
238
|
-
}
|
|
239
|
-
seenNativeKeys.add(key);
|
|
240
|
-
}
|
|
241
|
-
for (const installed of inspection.installed) {
|
|
242
|
-
const nativeId = installed.nativeId ?? schedulerNativeBindingId(installed.id);
|
|
243
|
-
const artifact = assertSchedulerNativeArtifactCardinality(inspection.artifacts, nativeId, 1);
|
|
244
|
-
if (!artifact ||
|
|
245
|
-
(installed.signature !== undefined &&
|
|
246
|
-
(artifact.fingerprint !== undefined
|
|
247
|
-
? installed.signature !== artifact.fingerprint
|
|
248
|
-
: requireCompleteFingerprint)) ||
|
|
249
|
-
(artifact.bindingId !== undefined && artifact.bindingId !== installed.id)) {
|
|
250
|
-
throw new UsageError(`Scheduler inspection is not coherent for ${JSON.stringify(nativeId)}: installed and native fingerprints differ.`, "RESOURCE_ALREADY_EXISTS");
|
|
251
|
-
}
|
|
252
|
-
if (installed.invocation !== undefined &&
|
|
253
|
-
(artifact.invocation === undefined || !sameInvocation(installed.invocation, artifact.invocation))) {
|
|
254
|
-
throw new UsageError(`Scheduler inspection is not coherent for ${JSON.stringify(nativeId)}: installed and native owners differ.`, "RESOURCE_ALREADY_EXISTS");
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
function nativeArtifactCollision(left, right) {
|
|
259
|
-
const owner = (value) => value.bindingId === undefined || value.invocation === undefined
|
|
260
|
-
? `${JSON.stringify(value.nativeId)} (unproven owner)`
|
|
261
|
-
: `binding ${JSON.stringify(value.bindingId)} invoking ${JSON.stringify(value.invocation)}`;
|
|
262
|
-
return new UsageError(`Native scheduler artifact collision between ${owner(left)} and ${owner(right)}; refusing to overwrite an existing or ambiguous native owner.`, "RESOURCE_ALREADY_EXISTS");
|
|
263
|
-
}
|
|
264
|
-
function desiredArtifact(binding) {
|
|
265
|
-
return {
|
|
266
|
-
nativeId: schedulerBindingNativeId(binding),
|
|
267
|
-
bindingId: binding.id,
|
|
268
|
-
invocation: binding.invocation,
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
function sameDesiredArtifact(left, right) {
|
|
272
|
-
return (schedulerBindingNativeId(left) === schedulerBindingNativeId(right) &&
|
|
273
|
-
left.id === right.id &&
|
|
274
|
-
left.logicalSource.kind === right.logicalSource.kind &&
|
|
275
|
-
left.logicalSource.ref === right.logicalSource.ref &&
|
|
276
|
-
left.ordinal === right.ordinal &&
|
|
277
|
-
left.executionEvidenceDigest === right.executionEvidenceDigest &&
|
|
278
|
-
sameInvocation(left.invocation, right.invocation));
|
|
279
|
-
}
|
|
280
|
-
async function compileDesiredSourceSet(input, collector) {
|
|
281
|
-
const bindings = [];
|
|
282
|
-
const executableWorkflows = [];
|
|
35
|
+
export async function compileSchedulerSources(input) {
|
|
36
|
+
const sourceRoot = path.resolve(input.sourceRoot);
|
|
37
|
+
const desired = [];
|
|
283
38
|
const failures = [];
|
|
284
|
-
await compileTaskSources(input,
|
|
285
|
-
|
|
286
|
-
// Degrade, don't reject (#867): one source that fails to parse/prepare no
|
|
287
|
-
// longer poisons the whole desired set — it is dropped from `desired` and
|
|
288
|
-
// reported here instead, so every OTHER task/workflow still reconciles.
|
|
289
|
-
// Genuinely cross-cutting integrity violations (duplicate ids, native
|
|
290
|
-
// artifact ownership conflicts, an incoherent backend inspection) are
|
|
291
|
-
// asserted separately in `finalizeSchedulerSyncPlan` and still hard-fail
|
|
292
|
-
// the whole sync — this only relaxes the per-source parse/prepare gate.
|
|
39
|
+
await compileTaskSources({ ...input, sourceRoot }, desired, failures);
|
|
40
|
+
compileWorkflowSources({ ...input, sourceRoot }, desired, failures);
|
|
293
41
|
return Object.freeze({
|
|
294
|
-
desired: Object.freeze(
|
|
295
|
-
executableWorkflows: Object.freeze(executableWorkflows.sort((left, right) => compareCodePoints(left.ref, right.ref))),
|
|
42
|
+
desired: Object.freeze(desired),
|
|
296
43
|
failures: Object.freeze(failures.sort((left, right) => compareCodePoints(left.path, right.path))),
|
|
297
44
|
});
|
|
298
45
|
}
|
|
299
|
-
async function compileTaskSources(input,
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
const conceptId = relative.slice(0, -4);
|
|
307
|
-
const id = input.adapterId === "akm-task" ? conceptId : path.basename(sourcePath, ".yml");
|
|
308
|
-
const qualifiedRefForFailure = makeBundleRef(input.bundleName, conceptId);
|
|
309
|
-
if (input.enabledActivations &&
|
|
310
|
-
!input.enabledActivations.has(schedulerActivationKey("task", qualifiedRefForFailure))) {
|
|
46
|
+
async function compileTaskSources(input, desired, failures) {
|
|
47
|
+
for (const file of taskSourceFiles(input.sourceRoot, input.adapterId)) {
|
|
48
|
+
const relative = toPosix(path.relative(input.sourceRoot, file));
|
|
49
|
+
const conceptId = relative.slice(0, -".yml".length);
|
|
50
|
+
const id = input.adapterId === "akm-task" ? conceptId : path.basename(file, ".yml");
|
|
51
|
+
const ref = sourceRef(input.bundleName, conceptId);
|
|
52
|
+
if (ref === undefined || (input.enabledRefs && !input.enabledRefs.has(ref)))
|
|
311
53
|
continue;
|
|
312
|
-
}
|
|
313
54
|
try {
|
|
314
|
-
const physicalIdentity = guarded.physicalIdentity;
|
|
315
|
-
const priorOwner = physicalOwners.get(physicalIdentity);
|
|
316
|
-
if (priorOwner !== undefined && priorOwner !== sourcePath) {
|
|
317
|
-
throw new UsageError(`Task sources ${JSON.stringify(priorOwner)} and ${JSON.stringify(sourcePath)} resolve to the same physical source identity; refusing canonical task identity collision.`, "RESOURCE_ALREADY_EXISTS");
|
|
318
|
-
}
|
|
319
|
-
physicalOwners.set(physicalIdentity, sourcePath);
|
|
320
|
-
// Project BEFORE prepareTaskV3Execution so projectability is checked —
|
|
321
|
-
// but build the scheduler bindings from the ORIGINAL task source v4
|
|
322
|
-
// document, not the projection, which deliberately drops
|
|
323
|
-
// `schedule[i].inputs` (project-v4.ts) —
|
|
324
|
-
// schedule-supplied inputs are delivered through the scheduler
|
|
325
|
-
// binding's own compiled invocation tail (P2b Lane B, spec §4.4,
|
|
326
|
-
// B-N3), not through the prepare-seam projection. A task source v4
|
|
327
55
|
const parsed = parseTaskSource({
|
|
328
|
-
yaml:
|
|
329
|
-
filePath:
|
|
56
|
+
yaml: fs.readFileSync(file, "utf8"),
|
|
57
|
+
filePath: file,
|
|
330
58
|
workspaceRoot: input.sourceRoot,
|
|
331
59
|
});
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
assertTaskScheduleInputsSatisfyContract(parsed.v4,
|
|
337
|
-
|
|
60
|
+
// A task with no `schedule:` is run by hand or by a workflow step; it
|
|
61
|
+
// contributes no binding and is not sync's to validate.
|
|
62
|
+
if (parsed.v4.schedule.length === 0)
|
|
63
|
+
continue;
|
|
64
|
+
assertTaskScheduleInputsSatisfyContract(parsed.v4, ref);
|
|
65
|
+
assertTaskScheduleCronValid(parsed.v4, input.backend);
|
|
66
|
+
// Project so a target that cannot resolve (a missing workflow, an
|
|
67
|
+
// unconfigured engine) is reported here rather than at fire time.
|
|
68
|
+
// Bindings compile from the original document: the projection drops
|
|
69
|
+
// `schedule[i].inputs`, which are delivered through the invocation tail.
|
|
70
|
+
await prepareTaskV3Execution(projectTaskSourceV4(parsed.v4), {
|
|
338
71
|
taskId: id,
|
|
339
|
-
taskRef:
|
|
72
|
+
taskRef: ref,
|
|
340
73
|
bundleName: input.bundleName,
|
|
341
74
|
bundleRoot: input.sourceRoot,
|
|
342
75
|
config: input.config ?? SCHEDULER_PROJECTION_CONFIG,
|
|
343
76
|
...(input.resolveAsset ? { resolveAsset: input.resolveAsset } : {}),
|
|
344
|
-
readFile: (file, bundleRoot) => collector.readBytes(file, bundleRoot ?? input.sourceRoot),
|
|
345
|
-
commandSourceLoader: (ref, kind, options) => {
|
|
346
|
-
const guardedOptions = {
|
|
347
|
-
...options,
|
|
348
|
-
fileContext: (root, file) => collector.fileContext(root, file),
|
|
349
|
-
};
|
|
350
|
-
return kind === "command"
|
|
351
|
-
? loadAdapterExecutionSource(ref, "command", guardedOptions)
|
|
352
|
-
: loadAdapterExecutionSource(ref, "persona", guardedOptions);
|
|
353
|
-
},
|
|
354
77
|
});
|
|
355
|
-
|
|
356
|
-
const sourceBindings = compileTaskSchedulerBindings({
|
|
78
|
+
desired.push(...compileTaskSchedulerBindings({
|
|
357
79
|
id,
|
|
358
|
-
qualifiedRef,
|
|
359
|
-
...(input.bundleTarget ? { bundleTarget: input.bundleTarget } : {}),
|
|
80
|
+
qualifiedRef: ref,
|
|
360
81
|
schedules: parsed.v4.schedule.map((schedule) => ({
|
|
361
82
|
cron: schedule.cron,
|
|
362
83
|
ordinal: schedule.ordinal,
|
|
363
|
-
source: `${
|
|
364
|
-
// P2b Lane B (spec §4.4, B-N3): delivered through the compiled
|
|
365
|
-
// binding's own invocation tail below — the F-B2 flip that closes
|
|
366
|
-
// the P2a B-38 "validated but not yet delivered" gap this comment
|
|
367
|
-
// used to describe.
|
|
84
|
+
source: `${relative}:${schedule.source}`,
|
|
368
85
|
inputs: schedule.inputs,
|
|
369
86
|
})),
|
|
370
|
-
});
|
|
371
|
-
assertTaskScheduleCronValid(parsed.v4, input.backend);
|
|
372
|
-
out.push(...sourceBindings);
|
|
87
|
+
}));
|
|
373
88
|
}
|
|
374
89
|
catch (cause) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* P2b Lane B (spec §4.4, rows B-50/F-B2): validate every v4 `schedule:`
|
|
381
|
-
* entry's `inputs` against the task's OWN declared contract WITH DEFAULTS
|
|
382
|
-
* APPLIED — the same `applyInputDefaults` + `validateInputs` pair
|
|
383
|
-
* `akm task run` uses (`src/tasks/run/load-task.ts`). `parseTaskSource`'s
|
|
384
|
-
* own parse-time check (`task-source-v4.ts`'s `parseScheduleEntry`) already
|
|
385
|
-
* rejects an unknown/malformed entry against the RAW supplied values; this
|
|
386
|
-
* is a deliberate second, independent gate over the DEFAULTED view — the
|
|
387
|
-
* exact set of values a compiled invocation actually delivers — so a
|
|
388
|
-
* violation fails HERE, recorded as a task failure at sync, rather than
|
|
389
|
-
* surfacing for the first time when the scheduler fires the invocation.
|
|
390
|
-
*
|
|
391
|
-
* Extracted so `akm task validate` can run the IDENTICAL gate
|
|
392
|
-
* over a bare file path without a second, potentially-diverging copy of
|
|
393
|
-
* this check. `refLabel` is only the text embedded in the thrown message —
|
|
394
|
-
* `compileTaskSources` passes its bundle-qualified ref; `akm task validate`,
|
|
395
|
-
* which never resolves a bundle for a bare file, passes the file path.
|
|
396
|
-
*/
|
|
397
|
-
export function assertTaskScheduleInputsSatisfyContract(v4, refLabel) {
|
|
398
|
-
const contract = v4.inputs ?? {};
|
|
399
|
-
for (const scheduleEntry of v4.schedule) {
|
|
400
|
-
const defaultedInputs = applyInputDefaults(contract, { ...scheduleEntry.inputs });
|
|
401
|
-
const errors = validateInputs(contract, defaultedInputs);
|
|
402
|
-
if (errors.length > 0) {
|
|
403
|
-
throw new UsageError(`Task ${JSON.stringify(refLabel)} schedule[${scheduleEntry.ordinal}].inputs does not satisfy ` +
|
|
404
|
-
`its declared inputs once defaults are applied: ${errors.join("; ")}`, "TASK_SOURCE_INVALID");
|
|
90
|
+
const detail = taskSourceErrorDetail(cause);
|
|
91
|
+
failures.push({ path: file, ref, reason: detail === errorMessage(cause) ? `${file}: ${detail}` : detail });
|
|
405
92
|
}
|
|
406
93
|
}
|
|
407
94
|
}
|
|
408
|
-
/**
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
* that could silently drift from what `akm task sync` actually enforces.
|
|
419
|
-
*/
|
|
420
|
-
export function assertTaskScheduleCronValid(v4, backend) {
|
|
421
|
-
for (const scheduleEntry of v4.schedule) {
|
|
422
|
-
parseSchedule(scheduleEntry.cron, backend);
|
|
95
|
+
/** Task candidates: `tasks/*.yml` for the akm adapter, every `.yml` in the tree for `akm-task`. */
|
|
96
|
+
function taskSourceFiles(sourceRoot, adapterId) {
|
|
97
|
+
if (adapterId === "akm") {
|
|
98
|
+
const dir = path.join(sourceRoot, "tasks");
|
|
99
|
+
const names = listDirectory(dir);
|
|
100
|
+
return names
|
|
101
|
+
.filter((name) => name.endsWith(".yml"))
|
|
102
|
+
.map((name) => path.join(dir, name))
|
|
103
|
+
.filter((file) => statOrUndefined(file)?.isFile() === true)
|
|
104
|
+
.sort(compareCodePoints);
|
|
423
105
|
}
|
|
106
|
+
if (adapterId === "akm-task")
|
|
107
|
+
return walkFiles(sourceRoot).filter((file) => file.endsWith(".yml"));
|
|
108
|
+
return [];
|
|
424
109
|
}
|
|
425
|
-
|
|
110
|
+
function compileWorkflowSources(input, desired, failures) {
|
|
426
111
|
if (input.adapterId !== "akm" && input.adapterId !== "akm-workflow")
|
|
427
112
|
return;
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
113
|
+
const root = input.adapterId === "akm" ? path.join(input.sourceRoot, "workflows") : input.sourceRoot;
|
|
114
|
+
const byName = new Map();
|
|
115
|
+
for (const file of walkFiles(root)) {
|
|
116
|
+
if (path.basename(file).toLowerCase() === "readme.md")
|
|
117
|
+
continue;
|
|
118
|
+
const authoredName = workflowNameForSourcePath(input.sourceRoot, input.adapterId, file);
|
|
119
|
+
if (authoredName === undefined)
|
|
120
|
+
continue;
|
|
121
|
+
const extension = path.posix.extname(authoredName).toLowerCase();
|
|
122
|
+
const stem = authoredName.slice(0, -extension.length).toLowerCase();
|
|
123
|
+
const nestedSuffix = WORKFLOW_EXTENSIONS.find((suffix) => stem.endsWith(suffix));
|
|
124
|
+
if (nestedSuffix) {
|
|
125
|
+
const relative = toPosix(path.relative(input.sourceRoot, file));
|
|
126
|
+
failures.push({ path: file, reason: new WorkflowSourceNameError(relative, nestedSuffix).message });
|
|
433
127
|
continue;
|
|
434
128
|
}
|
|
129
|
+
const canonicalName = canonicalizeWorkflowName(authoredName);
|
|
130
|
+
byName.set(canonicalName, [...(byName.get(canonicalName) ?? []), file]);
|
|
131
|
+
}
|
|
132
|
+
for (const [canonicalName, sources] of [...byName].sort(([left], [right]) => compareCodePoints(left, right))) {
|
|
133
|
+
const conceptId = input.adapterId === "akm" ? `workflows/${canonicalName}` : canonicalName;
|
|
134
|
+
const ref = sourceRef(input.bundleName, conceptId);
|
|
135
|
+
if (ref === undefined || (input.enabledRefs && !input.enabledRefs.has(ref)))
|
|
136
|
+
continue;
|
|
137
|
+
const file = sources[0];
|
|
435
138
|
try {
|
|
436
139
|
if (sources.length > 1) {
|
|
437
|
-
throw new WorkflowSourceCollisionError(
|
|
140
|
+
throw new WorkflowSourceCollisionError(conceptId, sources.map((source) => toPosix(path.relative(input.sourceRoot, source))));
|
|
438
141
|
}
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const compiled = compileWorkflowSource(guarded.content, {
|
|
443
|
-
path: guarded.relativePath,
|
|
142
|
+
const relative = toPosix(path.relative(input.sourceRoot, file));
|
|
143
|
+
const compiled = compileWorkflowSource(fs.readFileSync(file, "utf8"), {
|
|
144
|
+
path: relative,
|
|
444
145
|
workspaceRoot: input.sourceRoot,
|
|
445
146
|
});
|
|
446
147
|
if (!compiled.ok) {
|
|
@@ -448,336 +149,319 @@ async function compileWorkflowSources(input, collector, out, evidence, failures)
|
|
|
448
149
|
.map((error) => `${error.path}:${error.line ?? 1} [${error.code}] ${error.message}`)
|
|
449
150
|
.join("; "), "WORKFLOW_SOURCE_INVALID");
|
|
450
151
|
}
|
|
451
|
-
const planDraft =
|
|
152
|
+
const planDraft = checkWorkflowPlan(compiled.plan);
|
|
452
153
|
if (!planDraft.ok) {
|
|
453
|
-
throw new UsageError(planDraft.errors.map((error) => `${
|
|
154
|
+
throw new UsageError(planDraft.errors.map((error) => `${relative}:${error.line} ${error.message}`).join("; "), "WORKFLOW_SOURCE_INVALID");
|
|
454
155
|
}
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
path: guarded.sourcePath,
|
|
460
|
-
sourcePath: input.sourceRoot,
|
|
461
|
-
adapterId: input.adapterId,
|
|
462
|
-
title: canonicalName,
|
|
463
|
-
steps: [],
|
|
464
|
-
sourceIr: compiled.ir,
|
|
465
|
-
};
|
|
466
|
-
const frozen = await compileResolveFreezeWorkflowV4(asset, input.config ?? schedulerProjectionConfig(input), {
|
|
467
|
-
sourceCollector: collector.executionCollector(),
|
|
468
|
-
});
|
|
469
|
-
const planHash = computePlanHash(frozen.plan);
|
|
470
|
-
const executionEvidenceDigest = computeSchedulerExecutionEvidenceDigest(planHash, frozen.plan.sourceReadSet);
|
|
471
|
-
evidence.push(Object.freeze({
|
|
472
|
-
ref: qualifiedRef,
|
|
473
|
-
irVersion: 5,
|
|
474
|
-
planHash,
|
|
475
|
-
sourceReadSet: frozen.plan.sourceReadSet,
|
|
476
|
-
executionEvidenceDigest,
|
|
156
|
+
const schedules = (compiled.plan.schedules ?? []).map((schedule) => ({
|
|
157
|
+
cron: schedule.cron,
|
|
158
|
+
source: `${relative}:${schedule.line}`,
|
|
159
|
+
ordinal: schedule.ordinal,
|
|
477
160
|
}));
|
|
478
|
-
const
|
|
479
|
-
? [
|
|
480
|
-
{
|
|
481
|
-
cron: trigger.cron,
|
|
482
|
-
source: `${trigger.source.path}:${trigger.source.start}`,
|
|
483
|
-
ordinal: trigger.ordinal,
|
|
484
|
-
},
|
|
485
|
-
]
|
|
486
|
-
: []);
|
|
487
|
-
for (const binding of compileWorkflowSchedulerBindings({
|
|
488
|
-
qualifiedRef,
|
|
489
|
-
schedules,
|
|
490
|
-
executionEvidenceDigest,
|
|
491
|
-
})) {
|
|
161
|
+
for (const binding of compileWorkflowSchedulerBindings({ qualifiedRef: ref, schedules })) {
|
|
492
162
|
parseSchedule(binding.cron, input.backend);
|
|
493
|
-
|
|
163
|
+
desired.push(binding);
|
|
494
164
|
}
|
|
495
165
|
}
|
|
496
166
|
catch (cause) {
|
|
497
|
-
failures.push(
|
|
167
|
+
failures.push({ path: file, ref, reason: errorMessage(cause) });
|
|
498
168
|
}
|
|
499
169
|
}
|
|
500
170
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
bundles: {
|
|
506
|
-
[input.bundleName]: {
|
|
507
|
-
path: input.sourceRoot,
|
|
508
|
-
components: { main: { root: ".", adapter: input.adapterId } },
|
|
509
|
-
},
|
|
510
|
-
},
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
function enumerateWorkflowLookups(input, collector, failures) {
|
|
514
|
-
const lookups = new Map();
|
|
515
|
-
for (const guarded of collector.authoredWorkflowSources(input.adapterId)) {
|
|
516
|
-
const sourcePath = guarded.sourcePath;
|
|
517
|
-
if (path.basename(sourcePath).toLowerCase() === "readme.md")
|
|
518
|
-
continue;
|
|
519
|
-
const authoredName = workflowNameForSourcePath(input.sourceRoot, input.adapterId, sourcePath);
|
|
520
|
-
if (authoredName === undefined)
|
|
521
|
-
continue;
|
|
522
|
-
const extension = path.posix.extname(authoredName).toLowerCase();
|
|
523
|
-
const stem = authoredName.slice(0, -extension.length).toLowerCase();
|
|
524
|
-
const nestedSuffix = WORKFLOW_EXTENSIONS.find((suffix) => stem.endsWith(suffix));
|
|
525
|
-
if (nestedSuffix) {
|
|
526
|
-
failures.push(workflowFailure(sourcePath, undefined, new WorkflowSourceNameError(guarded.relativePath, nestedSuffix)));
|
|
527
|
-
continue;
|
|
528
|
-
}
|
|
529
|
-
const canonicalName = canonicalizeWorkflowName(authoredName);
|
|
530
|
-
const owners = lookups.get(canonicalName) ?? [];
|
|
531
|
-
owners.push(guarded);
|
|
532
|
-
lookups.set(canonicalName, owners);
|
|
171
|
+
/** A file whose name cannot form a ref (a `#` in it) cannot be enabled, so it is not this sync's. */
|
|
172
|
+
function sourceRef(bundleName, conceptId) {
|
|
173
|
+
try {
|
|
174
|
+
return makeBundleRef(bundleName, conceptId);
|
|
533
175
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
.map(([name, sources]) => [name, Object.freeze(sources.sort(compareGuardedSources))]));
|
|
537
|
-
}
|
|
538
|
-
function installOptionsFor(input, current) {
|
|
539
|
-
if (current && !input.rebind) {
|
|
540
|
-
return Object.freeze({
|
|
541
|
-
...(input.bundleTarget ? { target: input.bundleTarget } : {}),
|
|
542
|
-
binding: Object.freeze([...current.binding]),
|
|
543
|
-
contextPath: current.contextPath,
|
|
544
|
-
});
|
|
176
|
+
catch {
|
|
177
|
+
return undefined;
|
|
545
178
|
}
|
|
546
|
-
return input.installOptions ? Object.freeze({ ...input.installOptions }) : undefined;
|
|
547
179
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
// reach for another bundle's real scheduler entries. (`bundlePath` is
|
|
558
|
-
// only set for a primary sync — a `--bundle <target>` entry's
|
|
559
|
-
// descriptor reflects the invoking process's OWN primary directory,
|
|
560
|
-
// not the targeted bundle's, so it is not a meaningful signal there;
|
|
561
|
-
// that case keeps relying on config-name uniqueness below.)
|
|
562
|
-
return entry.ownerBundlePath !== undefined && entry.ownerBundlePath === input.bundlePath;
|
|
180
|
+
/** Directory entries, or none when the directory does not exist. Any other read error is the bundle's own failure. */
|
|
181
|
+
function listDirectory(dir) {
|
|
182
|
+
try {
|
|
183
|
+
return fs.readdirSync(dir);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
if (error.code === "ENOENT")
|
|
187
|
+
return [];
|
|
188
|
+
throw error;
|
|
563
189
|
}
|
|
564
|
-
if (entry.target === input.bundleName || entry.target === input.bundleTarget)
|
|
565
|
-
return true;
|
|
566
|
-
return false;
|
|
567
|
-
}
|
|
568
|
-
function assertNoForeignIds(desired, input) {
|
|
569
|
-
const wanted = new Set(desired.map(({ id }) => id));
|
|
570
|
-
const foreign = input.installed.find((entry) => wanted.has(entry.id) && !belongsToBundle(entry, input));
|
|
571
|
-
if (!foreign)
|
|
572
|
-
return;
|
|
573
|
-
const where = foreign.ownerBundlePath
|
|
574
|
-
? `the bundle at ${JSON.stringify(foreign.ownerBundlePath)}`
|
|
575
|
-
: foreign.target
|
|
576
|
-
? `bundle ${JSON.stringify(foreign.target)}`
|
|
577
|
-
: "the default bundle";
|
|
578
|
-
const mine = input.bundlePath ? ` (this sync is scoped to ${JSON.stringify(input.bundlePath)})` : "";
|
|
579
|
-
throw new UsageError(`Scheduler id ${JSON.stringify(foreign.id)} is already scheduled from ${where}${mine}; desired source ids must not collide across bundles.`, "RESOURCE_ALREADY_EXISTS");
|
|
580
190
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
191
|
+
/** Every regular file under `dir` (symlinks followed), sorted. */
|
|
192
|
+
function walkFiles(dir) {
|
|
193
|
+
const files = [];
|
|
194
|
+
const visit = (directory) => {
|
|
195
|
+
for (const name of listDirectory(directory)) {
|
|
196
|
+
const candidate = path.join(directory, name);
|
|
197
|
+
const stat = statOrUndefined(candidate);
|
|
198
|
+
if (stat?.isDirectory())
|
|
199
|
+
visit(candidate);
|
|
200
|
+
else if (stat?.isFile())
|
|
201
|
+
files.push(candidate);
|
|
586
202
|
}
|
|
587
|
-
|
|
203
|
+
};
|
|
204
|
+
visit(dir);
|
|
205
|
+
return files.sort(compareCodePoints);
|
|
206
|
+
}
|
|
207
|
+
/** A dangling or looping symlink is not a source file; it is skipped, never fatal. */
|
|
208
|
+
function statOrUndefined(file) {
|
|
209
|
+
try {
|
|
210
|
+
return fs.statSync(file);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return undefined;
|
|
588
214
|
}
|
|
589
215
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
216
|
+
/**
|
|
217
|
+
* Validate every v4 `schedule:` entry's `inputs` against the task's own
|
|
218
|
+
* declared contract with defaults applied — the exact set of values a
|
|
219
|
+
* compiled invocation delivers — so a violation is reported at sync rather
|
|
220
|
+
* than when the scheduler fires. Shared with `akm task validate`.
|
|
221
|
+
*/
|
|
222
|
+
export function assertTaskScheduleInputsSatisfyContract(v4, refLabel) {
|
|
223
|
+
const contract = v4.inputs ?? {};
|
|
224
|
+
for (const scheduleEntry of v4.schedule) {
|
|
225
|
+
const defaultedInputs = applyInputDefaults(contract, { ...scheduleEntry.inputs });
|
|
226
|
+
const errors = validateInputs(contract, defaultedInputs);
|
|
227
|
+
if (errors.length > 0) {
|
|
228
|
+
throw new UsageError(`Task ${JSON.stringify(refLabel)} schedule[${scheduleEntry.ordinal}].inputs does not satisfy ` +
|
|
229
|
+
`its declared inputs once defaults are applied: ${errors.join("; ")}`, "TASK_SOURCE_INVALID");
|
|
595
230
|
}
|
|
596
|
-
seen.add(binding.id);
|
|
597
231
|
}
|
|
598
232
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
return cause instanceof Error ? cause.message : String(cause);
|
|
233
|
+
/**
|
|
234
|
+
* Validate every v4 `schedule:` entry's `cron` against the active backend's
|
|
235
|
+
* dialect: cron is the most permissive of the three, so a task authored on
|
|
236
|
+
* Linux can carry an expression launchd/schtasks cannot translate. Shared
|
|
237
|
+
* with `akm task validate`.
|
|
238
|
+
*/
|
|
239
|
+
export function assertTaskScheduleCronValid(v4, backend) {
|
|
240
|
+
for (const scheduleEntry of v4.schedule)
|
|
241
|
+
parseSchedule(scheduleEntry.cron, backend);
|
|
609
242
|
}
|
|
610
|
-
export function
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
243
|
+
export function installedRowNativeId(row) {
|
|
244
|
+
return row.nativeId ?? schedulerNativeBindingId(row.id);
|
|
245
|
+
}
|
|
246
|
+
/** The bundle an installed row's own invocation names: `--bundle <x>`, or a workflow ref's bundle. */
|
|
247
|
+
export function scheduledInvocationBundle(invocation) {
|
|
248
|
+
if (!invocation)
|
|
249
|
+
return undefined;
|
|
250
|
+
const bundleIndex = invocation.indexOf("--bundle");
|
|
251
|
+
if (bundleIndex >= 0)
|
|
252
|
+
return invocation[bundleIndex + 1];
|
|
253
|
+
if (invocation[0] === "workflow" && invocation[1] === "run" && invocation[2]) {
|
|
254
|
+
try {
|
|
255
|
+
return parseBundleRef(invocation[2]).bundle;
|
|
620
256
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
if (JSON.stringify(current) !== JSON.stringify(manifest)) {
|
|
624
|
-
throw new Error(`directory manifest changed: ${manifest.directoryPath}`);
|
|
625
|
-
}
|
|
257
|
+
catch {
|
|
258
|
+
return undefined;
|
|
626
259
|
}
|
|
627
260
|
}
|
|
628
|
-
|
|
629
|
-
throw new UsageError(`Scheduler desired source read set changed after projection; refusing native mutation: ${errorMessage(cause)}`, "RESOURCE_ALREADY_EXISTS");
|
|
630
|
-
}
|
|
261
|
+
return undefined;
|
|
631
262
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
if (
|
|
643
|
-
if (
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
for (const entry of taskManifest.entries) {
|
|
647
|
-
if (entry.kind === "file" && entry.name.endsWith(".yml")) {
|
|
648
|
-
candidates.push(path.join(taskRoot, entry.name));
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
if (rootDirectories.has("workflows")) {
|
|
653
|
-
candidates.push(...this.#collector.enumerateTree(path.join(this.#sourceRoot, "workflows"), this.#sourceRoot));
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
else if (input.adapterId === "akm-task") {
|
|
657
|
-
candidates.push(...this.#collector.enumerateTree(this.#sourceRoot, this.#sourceRoot).filter((file) => file.endsWith(".yml")));
|
|
658
|
-
}
|
|
659
|
-
else if (input.adapterId === "akm-workflow") {
|
|
660
|
-
candidates.push(...this.#collector.enumerateTree(this.#sourceRoot, this.#sourceRoot));
|
|
263
|
+
/**
|
|
264
|
+
* The scope that owns an installed row, or none. A row written before
|
|
265
|
+
* `--bundle` existed names no bundle and belongs to the primary scope
|
|
266
|
+
* unless its descriptor says otherwise; a row that names a bundle belongs
|
|
267
|
+
* to the scope of that name — for the primary scope only when its
|
|
268
|
+
* descriptor also proves the path.
|
|
269
|
+
*/
|
|
270
|
+
export function installedRowScope(row, scopes) {
|
|
271
|
+
const bundle = row.target ?? scheduledInvocationBundle(row.invocation);
|
|
272
|
+
for (const scope of scopes) {
|
|
273
|
+
if (scope.bundlePath === undefined) {
|
|
274
|
+
if (bundle === scope.bundleName)
|
|
275
|
+
return scope;
|
|
276
|
+
continue;
|
|
661
277
|
}
|
|
662
|
-
|
|
663
|
-
|
|
278
|
+
if (bundle === undefined) {
|
|
279
|
+
if (row.ownerBundlePath === undefined || row.ownerBundlePath === scope.bundlePath)
|
|
280
|
+
return scope;
|
|
281
|
+
continue;
|
|
664
282
|
}
|
|
283
|
+
if (bundle === scope.bundleName && row.ownerBundlePath === scope.bundlePath)
|
|
284
|
+
return scope;
|
|
665
285
|
}
|
|
666
|
-
|
|
667
|
-
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
export function planSchedulerSync(input) {
|
|
289
|
+
const failures = [];
|
|
290
|
+
const installedByNative = new Map();
|
|
291
|
+
for (const row of input.installed)
|
|
292
|
+
installedByNative.set(installedRowNativeId(row), row);
|
|
293
|
+
// Two sources claiming one native row — compared case-insensitively, as
|
|
294
|
+
// Task Scheduler and a default macOS volume compare them: neither is
|
|
295
|
+
// installed and the row, if any, is left exactly as it is.
|
|
296
|
+
const desiredByNative = new Map();
|
|
297
|
+
for (const binding of input.desired) {
|
|
298
|
+
const key = schedulerBindingNativeId(binding).toLowerCase();
|
|
299
|
+
desiredByNative.set(key, [...(desiredByNative.get(key) ?? []), binding]);
|
|
668
300
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
301
|
+
const skipped = new Set();
|
|
302
|
+
for (const group of desiredByNative.values()) {
|
|
303
|
+
if (group.length < 2)
|
|
304
|
+
continue;
|
|
305
|
+
const refs = group.map((binding) => JSON.stringify(binding.logicalSource.ref)).join(" and ");
|
|
306
|
+
for (const binding of group) {
|
|
307
|
+
const nativeId = schedulerBindingNativeId(binding);
|
|
308
|
+
skipped.add(nativeId);
|
|
309
|
+
failures.push({
|
|
310
|
+
path: binding.source,
|
|
311
|
+
ref: binding.logicalSource.ref,
|
|
312
|
+
reason: `Scheduler id ${JSON.stringify(nativeId)} is claimed by ${refs}. Rename one task before enabling both.`,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
680
315
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
.
|
|
316
|
+
const installed = [];
|
|
317
|
+
const updated = [];
|
|
318
|
+
const unchanged = [];
|
|
319
|
+
const writes = [];
|
|
320
|
+
for (const binding of input.desired) {
|
|
321
|
+
const nativeId = schedulerBindingNativeId(binding);
|
|
322
|
+
if (skipped.has(nativeId))
|
|
323
|
+
continue;
|
|
324
|
+
const current = installedByNative.get(nativeId);
|
|
325
|
+
if (current &&
|
|
326
|
+
installedRowScope(current, input.scopes)?.bundleName !== parseBundleRef(binding.logicalSource.ref).bundle) {
|
|
327
|
+
skipped.add(nativeId);
|
|
328
|
+
failures.push({
|
|
329
|
+
path: binding.source,
|
|
330
|
+
ref: binding.logicalSource.ref,
|
|
331
|
+
reason: foreignRowMessage(nativeId, current),
|
|
332
|
+
});
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const options = installOptionsFor(input, current);
|
|
336
|
+
const expected = input.expectedSignature?.(binding, options);
|
|
337
|
+
if (!current) {
|
|
338
|
+
installed.push(binding.id);
|
|
339
|
+
writes.push(Object.freeze({
|
|
340
|
+
kind: "install",
|
|
341
|
+
binding,
|
|
342
|
+
...(options ? { options } : {}),
|
|
343
|
+
...(expected !== undefined ? { expectedSignature: expected } : {}),
|
|
344
|
+
}));
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (expected === undefined || current.signature === expected) {
|
|
348
|
+
unchanged.push(binding.id);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
updated.push(binding.id);
|
|
352
|
+
writes.push(Object.freeze({
|
|
353
|
+
kind: "update",
|
|
354
|
+
binding,
|
|
355
|
+
...(options ? { options } : {}),
|
|
356
|
+
...(current.signature !== undefined ? { installedSignature: current.signature } : {}),
|
|
357
|
+
expectedSignature: expected,
|
|
358
|
+
}));
|
|
692
359
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
360
|
+
const desiredNative = new Set(input.desired.map(schedulerBindingNativeId));
|
|
361
|
+
const removals = new Map();
|
|
362
|
+
for (const row of input.installed) {
|
|
363
|
+
const nativeId = installedRowNativeId(row);
|
|
364
|
+
if (desiredNative.has(nativeId) || skipped.has(nativeId))
|
|
365
|
+
continue;
|
|
366
|
+
const scope = installedRowScope(row, input.scopes);
|
|
367
|
+
if (!scope)
|
|
368
|
+
continue;
|
|
369
|
+
const ref = installedRowRef(row, scope);
|
|
370
|
+
if (ref === undefined || !input.keepRefs?.has(ref))
|
|
371
|
+
removals.set(nativeId, row);
|
|
696
372
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
373
|
+
for (const row of input.extraRemovals ?? []) {
|
|
374
|
+
const nativeId = installedRowNativeId(row);
|
|
375
|
+
if (!desiredNative.has(nativeId) && !removals.has(nativeId))
|
|
376
|
+
removals.set(nativeId, row);
|
|
700
377
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
378
|
+
const removeOperations = [...removals.values()]
|
|
379
|
+
.sort((left, right) => compareCodePoints(left.id, right.id))
|
|
380
|
+
.map((row) => Object.freeze({
|
|
381
|
+
kind: "remove",
|
|
382
|
+
id: row.id,
|
|
383
|
+
nativeId: installedRowNativeId(row),
|
|
384
|
+
...(row.ownerBundlePath !== undefined ? { ownerBundlePath: row.ownerBundlePath } : {}),
|
|
385
|
+
}));
|
|
386
|
+
return Object.freeze({
|
|
387
|
+
desired: input.desired,
|
|
388
|
+
installed: Object.freeze(installed),
|
|
389
|
+
updated: Object.freeze(updated),
|
|
390
|
+
removed: Object.freeze(removeOperations.map((operation) => operation.id)),
|
|
391
|
+
unchanged: Object.freeze(unchanged),
|
|
392
|
+
operations: Object.freeze([...removeOperations, ...writes]),
|
|
393
|
+
failures: Object.freeze(failures),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
/** An installed row keeps its own launcher unless `rebind`; the descriptor always follows the current policy. */
|
|
397
|
+
function installOptionsFor(input, current) {
|
|
398
|
+
if (current && !input.rebind) {
|
|
706
399
|
return Object.freeze({
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
sourceRealPath: root.realPath,
|
|
710
|
-
sourcePhysicalIdentity: root.physicalIdentity,
|
|
711
|
-
sourceDirectoryVersion: root.version,
|
|
712
|
-
files: Object.freeze([...guarded.sources].sort(compareGuardedSources)),
|
|
713
|
-
directoryManifests: guarded.directoryManifests,
|
|
400
|
+
binding: Object.freeze([...current.binding]),
|
|
401
|
+
contextPath: input.installOptions?.contextPath ?? current.contextPath,
|
|
714
402
|
});
|
|
715
403
|
}
|
|
716
|
-
|
|
717
|
-
const root = path.resolve(containmentRoot);
|
|
718
|
-
const parent = path.dirname(path.resolve(file));
|
|
719
|
-
const relative = path.relative(root, parent);
|
|
720
|
-
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
721
|
-
return;
|
|
722
|
-
this.#collector.trackDirectory(root, root);
|
|
723
|
-
if (relative === "")
|
|
724
|
-
return;
|
|
725
|
-
let current = root;
|
|
726
|
-
for (const segment of relative.split(path.sep)) {
|
|
727
|
-
current = path.join(current, segment);
|
|
728
|
-
this.#collector.trackDirectory(current, root);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
function compareGuardedSources(left, right) {
|
|
733
|
-
return compareCodePoints(left.sourcePath, right.sourcePath);
|
|
404
|
+
return input.installOptions ? Object.freeze({ ...input.installOptions }) : undefined;
|
|
734
405
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
406
|
+
/** The source ref an installed row runs, read from its own invocation. */
|
|
407
|
+
function installedRowRef(row, scope) {
|
|
408
|
+
const [verb, action, subject] = row.invocation ?? [];
|
|
409
|
+
if (action !== "run" || !subject)
|
|
410
|
+
return undefined;
|
|
411
|
+
if (verb === "workflow")
|
|
412
|
+
return subject;
|
|
413
|
+
if (verb !== "task")
|
|
414
|
+
return undefined;
|
|
415
|
+
return `${scope.bundleName}//${scope.adapterId === "akm-task" ? subject : `tasks/${subject}`}`;
|
|
416
|
+
}
|
|
417
|
+
/** Where an installed row was scheduled from, for messages. */
|
|
418
|
+
export function installedRowOwner(row) {
|
|
419
|
+
const bundle = row.target ?? scheduledInvocationBundle(row.invocation);
|
|
420
|
+
if (row.ownerBundlePath)
|
|
421
|
+
return `the bundle at ${JSON.stringify(row.ownerBundlePath)}`;
|
|
422
|
+
return bundle ? `bundle ${JSON.stringify(bundle)}` : "an installation this sync cannot attribute";
|
|
423
|
+
}
|
|
424
|
+
function foreignRowMessage(nativeId, row) {
|
|
425
|
+
return `Scheduler id ${JSON.stringify(nativeId)} is already scheduled from ${installedRowOwner(row)}; desired source ids must not collide across bundles. Leaving it out of this sync.`;
|
|
426
|
+
}
|
|
427
|
+
/** Project a set of operations into a preview report. Pure: no I/O, no backend calls. */
|
|
428
|
+
export function renderSchedulerPlanPreview(backend, operations, unchanged = [], failures = []) {
|
|
429
|
+
const adds = [];
|
|
430
|
+
const updates = [];
|
|
431
|
+
const removes = [];
|
|
432
|
+
for (const operation of operations) {
|
|
433
|
+
if (operation.kind === "remove") {
|
|
434
|
+
removes.push({
|
|
435
|
+
id: operation.id,
|
|
436
|
+
kind: "remove",
|
|
437
|
+
nativeId: operation.nativeId,
|
|
438
|
+
...(operation.ownerBundlePath !== undefined ? { ownerBundlePath: operation.ownerBundlePath } : {}),
|
|
439
|
+
...(operation.reason !== undefined ? { reason: operation.reason } : {}),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
else if (operation.kind === "install") {
|
|
443
|
+
adds.push({ id: operation.binding.id, kind: "install" });
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
updates.push({
|
|
447
|
+
id: operation.binding.id,
|
|
448
|
+
kind: "update",
|
|
449
|
+
...(operation.installedSignature !== undefined ? { installedFingerprint: operation.installedSignature } : {}),
|
|
450
|
+
...(operation.expectedSignature !== undefined ? { expectedFingerprint: operation.expectedSignature } : {}),
|
|
451
|
+
});
|
|
744
452
|
}
|
|
745
|
-
const conceptId = input.adapterId === "akm" ? `tasks/${invocation[2]}` : invocation[2];
|
|
746
|
-
return Object.freeze({ kind: "task", ref: makeBundleRef(bundle, conceptId) });
|
|
747
453
|
}
|
|
748
|
-
throw new UsageError("Installed scheduler invocation has no exact canonical source owner.", "RESOURCE_ALREADY_EXISTS");
|
|
749
|
-
}
|
|
750
|
-
function freezeRemovalExpectation(expectation) {
|
|
751
|
-
return Object.freeze({
|
|
752
|
-
...expectation,
|
|
753
|
-
logicalSource: Object.freeze({ ...expectation.logicalSource }),
|
|
754
|
-
invocation: Object.freeze([...expectation.invocation]),
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
function expectationForBinding(binding, state, fingerprint) {
|
|
758
454
|
return {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
455
|
+
backend,
|
|
456
|
+
dryRun: true,
|
|
457
|
+
adds,
|
|
458
|
+
updates,
|
|
459
|
+
removes,
|
|
460
|
+
unchanged: [...unchanged],
|
|
461
|
+
hasRemovals: removes.length > 0,
|
|
462
|
+
failures: [...failures],
|
|
766
463
|
};
|
|
767
464
|
}
|
|
768
|
-
function
|
|
769
|
-
return
|
|
770
|
-
...expectation,
|
|
771
|
-
logicalSource: Object.freeze({ ...expectation.logicalSource }),
|
|
772
|
-
invocation: Object.freeze([...expectation.invocation]),
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
|
-
function freezeOperation(operation) {
|
|
776
|
-
return Object.freeze(operation);
|
|
777
|
-
}
|
|
778
|
-
function sameInvocation(left, right) {
|
|
779
|
-
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
465
|
+
function errorMessage(cause) {
|
|
466
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
780
467
|
}
|
|
781
|
-
// Retain the concrete error in this module's public dependency graph so callers
|
|
782
|
-
// can continue to identify ownership failures without importing an adapter.
|
|
783
|
-
export { WorkflowSourceRejectionError };
|