akm-cli 0.9.17-alpha.3 → 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 +731 -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 +361 -751
- 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 -441
- 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,455 +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
|
-
|
|
305
|
-
|
|
306
|
-
if (input.enabledActivations && !input.enabledActivations.has(schedulerActivationKey("task", qualifiedRef))) {
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
failures.push(taskFailure(symlink.sourcePath, qualifiedRef, symbolicSourceError(symlink.sourcePath)));
|
|
310
|
-
}
|
|
311
|
-
const physicalOwners = new Map();
|
|
312
|
-
for (const guarded of collector.authoredTaskSources(input.adapterId)) {
|
|
313
|
-
const sourcePath = guarded.sourcePath;
|
|
314
|
-
const relative = guarded.relativePath;
|
|
315
|
-
const conceptId = relative.slice(0, -4);
|
|
316
|
-
const id = input.adapterId === "akm-task" ? conceptId : path.basename(sourcePath, ".yml");
|
|
317
|
-
const qualifiedRefForFailure = makeBundleRef(input.bundleName, conceptId);
|
|
318
|
-
if (input.enabledActivations &&
|
|
319
|
-
!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)))
|
|
320
53
|
continue;
|
|
321
|
-
}
|
|
322
54
|
try {
|
|
323
|
-
const physicalIdentity = guarded.physicalIdentity;
|
|
324
|
-
const priorOwner = physicalOwners.get(physicalIdentity);
|
|
325
|
-
if (priorOwner !== undefined && priorOwner !== sourcePath) {
|
|
326
|
-
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");
|
|
327
|
-
}
|
|
328
|
-
physicalOwners.set(physicalIdentity, sourcePath);
|
|
329
|
-
// Project BEFORE prepareTaskV3Execution so projectability is checked —
|
|
330
|
-
// but build the scheduler bindings from the ORIGINAL task source v4
|
|
331
|
-
// document, not the projection, which deliberately drops
|
|
332
|
-
// `schedule[i].inputs` (project-v4.ts) —
|
|
333
|
-
// schedule-supplied inputs are delivered through the scheduler
|
|
334
|
-
// binding's own compiled invocation tail (P2b Lane B, spec §4.4,
|
|
335
|
-
// B-N3), not through the prepare-seam projection. A task source v4
|
|
336
55
|
const parsed = parseTaskSource({
|
|
337
|
-
yaml:
|
|
338
|
-
filePath:
|
|
56
|
+
yaml: fs.readFileSync(file, "utf8"),
|
|
57
|
+
filePath: file,
|
|
339
58
|
workspaceRoot: input.sourceRoot,
|
|
340
59
|
});
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
assertTaskScheduleInputsSatisfyContract(parsed.v4,
|
|
346
|
-
|
|
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), {
|
|
347
71
|
taskId: id,
|
|
348
|
-
taskRef:
|
|
72
|
+
taskRef: ref,
|
|
349
73
|
bundleName: input.bundleName,
|
|
350
74
|
bundleRoot: input.sourceRoot,
|
|
351
75
|
config: input.config ?? SCHEDULER_PROJECTION_CONFIG,
|
|
352
76
|
...(input.resolveAsset ? { resolveAsset: input.resolveAsset } : {}),
|
|
353
|
-
readFile: (file, bundleRoot) => collector.readBytes(file, bundleRoot ?? input.sourceRoot),
|
|
354
|
-
commandSourceLoader: (ref, kind, options) => {
|
|
355
|
-
const guardedOptions = {
|
|
356
|
-
...options,
|
|
357
|
-
fileContext: (root, file) => collector.fileContext(root, file),
|
|
358
|
-
};
|
|
359
|
-
return kind === "command"
|
|
360
|
-
? loadAdapterExecutionSource(ref, "command", guardedOptions)
|
|
361
|
-
: loadAdapterExecutionSource(ref, "persona", guardedOptions);
|
|
362
|
-
},
|
|
363
77
|
});
|
|
364
|
-
|
|
365
|
-
const sourceBindings = compileTaskSchedulerBindings({
|
|
78
|
+
desired.push(...compileTaskSchedulerBindings({
|
|
366
79
|
id,
|
|
367
|
-
qualifiedRef,
|
|
368
|
-
...(input.bundleTarget ? { bundleTarget: input.bundleTarget } : {}),
|
|
80
|
+
qualifiedRef: ref,
|
|
369
81
|
schedules: parsed.v4.schedule.map((schedule) => ({
|
|
370
82
|
cron: schedule.cron,
|
|
371
83
|
ordinal: schedule.ordinal,
|
|
372
|
-
source: `${
|
|
373
|
-
// P2b Lane B (spec §4.4, B-N3): delivered through the compiled
|
|
374
|
-
// binding's own invocation tail below — the F-B2 flip that closes
|
|
375
|
-
// the P2a B-38 "validated but not yet delivered" gap this comment
|
|
376
|
-
// used to describe.
|
|
84
|
+
source: `${relative}:${schedule.source}`,
|
|
377
85
|
inputs: schedule.inputs,
|
|
378
86
|
})),
|
|
379
|
-
});
|
|
380
|
-
assertTaskScheduleCronValid(parsed.v4, input.backend);
|
|
381
|
-
out.push(...sourceBindings);
|
|
87
|
+
}));
|
|
382
88
|
}
|
|
383
89
|
catch (cause) {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
/**
|
|
389
|
-
* P2b Lane B (spec §4.4, rows B-50/F-B2): validate every v4 `schedule:`
|
|
390
|
-
* entry's `inputs` against the task's OWN declared contract WITH DEFAULTS
|
|
391
|
-
* APPLIED — the same `applyInputDefaults` + `validateInputs` pair
|
|
392
|
-
* `akm task run` uses (`src/tasks/run/load-task.ts`). `parseTaskSource`'s
|
|
393
|
-
* own parse-time check (`task-source-v4.ts`'s `parseScheduleEntry`) already
|
|
394
|
-
* rejects an unknown/malformed entry against the RAW supplied values; this
|
|
395
|
-
* is a deliberate second, independent gate over the DEFAULTED view — the
|
|
396
|
-
* exact set of values a compiled invocation actually delivers — so a
|
|
397
|
-
* violation fails HERE, recorded as a task failure at sync, rather than
|
|
398
|
-
* surfacing for the first time when the scheduler fires the invocation.
|
|
399
|
-
*
|
|
400
|
-
* Extracted so `akm task validate` can run the IDENTICAL gate
|
|
401
|
-
* over a bare file path without a second, potentially-diverging copy of
|
|
402
|
-
* this check. `refLabel` is only the text embedded in the thrown message —
|
|
403
|
-
* `compileTaskSources` passes its bundle-qualified ref; `akm task validate`,
|
|
404
|
-
* which never resolves a bundle for a bare file, passes the file path.
|
|
405
|
-
*/
|
|
406
|
-
export function assertTaskScheduleInputsSatisfyContract(v4, refLabel) {
|
|
407
|
-
const contract = v4.inputs ?? {};
|
|
408
|
-
for (const scheduleEntry of v4.schedule) {
|
|
409
|
-
const defaultedInputs = applyInputDefaults(contract, { ...scheduleEntry.inputs });
|
|
410
|
-
const errors = validateInputs(contract, defaultedInputs);
|
|
411
|
-
if (errors.length > 0) {
|
|
412
|
-
throw new UsageError(`Task ${JSON.stringify(refLabel)} schedule[${scheduleEntry.ordinal}].inputs does not satisfy ` +
|
|
413
|
-
`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 });
|
|
414
92
|
}
|
|
415
93
|
}
|
|
416
94
|
}
|
|
417
|
-
/**
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
* that could silently drift from what `akm task sync` actually enforces.
|
|
428
|
-
*/
|
|
429
|
-
export function assertTaskScheduleCronValid(v4, backend) {
|
|
430
|
-
for (const scheduleEntry of v4.schedule) {
|
|
431
|
-
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);
|
|
432
105
|
}
|
|
106
|
+
if (adapterId === "akm-task")
|
|
107
|
+
return walkFiles(sourceRoot).filter((file) => file.endsWith(".yml"));
|
|
108
|
+
return [];
|
|
433
109
|
}
|
|
434
|
-
|
|
110
|
+
function compileWorkflowSources(input, desired, failures) {
|
|
435
111
|
if (input.adapterId !== "akm" && input.adapterId !== "akm-workflow")
|
|
436
112
|
return;
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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 });
|
|
442
127
|
continue;
|
|
443
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];
|
|
444
138
|
try {
|
|
445
139
|
if (sources.length > 1) {
|
|
446
|
-
throw new WorkflowSourceCollisionError(
|
|
140
|
+
throw new WorkflowSourceCollisionError(conceptId, sources.map((source) => toPosix(path.relative(input.sourceRoot, source))));
|
|
447
141
|
}
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const compiled = compileWorkflowSource(guarded.content, {
|
|
452
|
-
path: guarded.relativePath,
|
|
142
|
+
const relative = toPosix(path.relative(input.sourceRoot, file));
|
|
143
|
+
const compiled = compileWorkflowSource(fs.readFileSync(file, "utf8"), {
|
|
144
|
+
path: relative,
|
|
453
145
|
workspaceRoot: input.sourceRoot,
|
|
454
146
|
});
|
|
455
147
|
if (!compiled.ok) {
|
|
@@ -457,401 +149,319 @@ async function compileWorkflowSources(input, collector, out, evidence, failures)
|
|
|
457
149
|
.map((error) => `${error.path}:${error.line ?? 1} [${error.code}] ${error.message}`)
|
|
458
150
|
.join("; "), "WORKFLOW_SOURCE_INVALID");
|
|
459
151
|
}
|
|
460
|
-
const planDraft =
|
|
152
|
+
const planDraft = checkWorkflowPlan(compiled.plan);
|
|
461
153
|
if (!planDraft.ok) {
|
|
462
|
-
throw new UsageError(planDraft.errors.map((error) => `${
|
|
154
|
+
throw new UsageError(planDraft.errors.map((error) => `${relative}:${error.line} ${error.message}`).join("; "), "WORKFLOW_SOURCE_INVALID");
|
|
463
155
|
}
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
path: guarded.sourcePath,
|
|
469
|
-
sourcePath: input.sourceRoot,
|
|
470
|
-
adapterId: input.adapterId,
|
|
471
|
-
title: canonicalName,
|
|
472
|
-
steps: [],
|
|
473
|
-
sourceIr: compiled.ir,
|
|
474
|
-
};
|
|
475
|
-
const frozen = await compileResolveFreezeWorkflowV4(asset, input.config ?? schedulerProjectionConfig(input), {
|
|
476
|
-
sourceCollector: collector.executionCollector(),
|
|
477
|
-
});
|
|
478
|
-
const planHash = computePlanHash(frozen.plan);
|
|
479
|
-
const executionEvidenceDigest = computeSchedulerExecutionEvidenceDigest(planHash, frozen.plan.sourceReadSet);
|
|
480
|
-
evidence.push(Object.freeze({
|
|
481
|
-
ref: qualifiedRef,
|
|
482
|
-
irVersion: 5,
|
|
483
|
-
planHash,
|
|
484
|
-
sourceReadSet: frozen.plan.sourceReadSet,
|
|
485
|
-
executionEvidenceDigest,
|
|
156
|
+
const schedules = (compiled.plan.schedules ?? []).map((schedule) => ({
|
|
157
|
+
cron: schedule.cron,
|
|
158
|
+
source: `${relative}:${schedule.line}`,
|
|
159
|
+
ordinal: schedule.ordinal,
|
|
486
160
|
}));
|
|
487
|
-
const
|
|
488
|
-
? [
|
|
489
|
-
{
|
|
490
|
-
cron: trigger.cron,
|
|
491
|
-
source: `${trigger.source.path}:${trigger.source.start}`,
|
|
492
|
-
ordinal: trigger.ordinal,
|
|
493
|
-
},
|
|
494
|
-
]
|
|
495
|
-
: []);
|
|
496
|
-
for (const binding of compileWorkflowSchedulerBindings({
|
|
497
|
-
qualifiedRef,
|
|
498
|
-
schedules,
|
|
499
|
-
executionEvidenceDigest,
|
|
500
|
-
})) {
|
|
161
|
+
for (const binding of compileWorkflowSchedulerBindings({ qualifiedRef: ref, schedules })) {
|
|
501
162
|
parseSchedule(binding.cron, input.backend);
|
|
502
|
-
|
|
163
|
+
desired.push(binding);
|
|
503
164
|
}
|
|
504
165
|
}
|
|
505
166
|
catch (cause) {
|
|
506
|
-
failures.push(
|
|
167
|
+
failures.push({ path: file, ref, reason: errorMessage(cause) });
|
|
507
168
|
}
|
|
508
169
|
}
|
|
509
170
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
bundles: {
|
|
515
|
-
[input.bundleName]: {
|
|
516
|
-
path: input.sourceRoot,
|
|
517
|
-
components: { main: { root: ".", adapter: input.adapterId } },
|
|
518
|
-
},
|
|
519
|
-
},
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
function enumerateWorkflowLookups(input, collector, failures) {
|
|
523
|
-
const lookups = new Map();
|
|
524
|
-
for (const guarded of collector.authoredWorkflowSources(input.adapterId)) {
|
|
525
|
-
const sourcePath = guarded.sourcePath;
|
|
526
|
-
if (path.basename(sourcePath).toLowerCase() === "readme.md")
|
|
527
|
-
continue;
|
|
528
|
-
const authoredName = workflowNameForSourcePath(input.sourceRoot, input.adapterId, sourcePath);
|
|
529
|
-
if (authoredName === undefined)
|
|
530
|
-
continue;
|
|
531
|
-
const extension = path.posix.extname(authoredName).toLowerCase();
|
|
532
|
-
const stem = authoredName.slice(0, -extension.length).toLowerCase();
|
|
533
|
-
const nestedSuffix = WORKFLOW_EXTENSIONS.find((suffix) => stem.endsWith(suffix));
|
|
534
|
-
if (nestedSuffix) {
|
|
535
|
-
failures.push(workflowFailure(sourcePath, undefined, new WorkflowSourceNameError(guarded.relativePath, nestedSuffix)));
|
|
536
|
-
continue;
|
|
537
|
-
}
|
|
538
|
-
const canonicalName = canonicalizeWorkflowName(authoredName);
|
|
539
|
-
const owners = lookups.get(canonicalName) ?? [];
|
|
540
|
-
owners.push(guarded);
|
|
541
|
-
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);
|
|
542
175
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
continue;
|
|
546
|
-
if (path.basename(symlink.sourcePath).toLowerCase() === "readme.md")
|
|
547
|
-
continue;
|
|
548
|
-
const authoredName = workflowNameForSourcePath(input.sourceRoot, input.adapterId, symlink.sourcePath);
|
|
549
|
-
if (authoredName === undefined)
|
|
550
|
-
continue;
|
|
551
|
-
const canonicalName = canonicalizeWorkflowName(authoredName);
|
|
552
|
-
// A real sibling sharing this name must not compile either: runtime
|
|
553
|
-
// resolution follows the symlink and may pick it over the file the
|
|
554
|
-
// binding was compiled from. The symbolic failure below is the ref's
|
|
555
|
-
// only report.
|
|
556
|
-
lookups.delete(canonicalName);
|
|
557
|
-
const failureRef = makeBundleRef(input.bundleName, input.adapterId === "akm" ? `workflows/${canonicalName}` : canonicalName);
|
|
558
|
-
if (input.enabledActivations && !input.enabledActivations.has(schedulerActivationKey("workflow", failureRef))) {
|
|
559
|
-
continue;
|
|
560
|
-
}
|
|
561
|
-
failures.push(workflowFailure(symlink.sourcePath, failureRef, symbolicSourceError(symlink.sourcePath)));
|
|
176
|
+
catch {
|
|
177
|
+
return undefined;
|
|
562
178
|
}
|
|
563
|
-
return new Map([...lookups]
|
|
564
|
-
.sort(([left], [right]) => compareCodePoints(left, right))
|
|
565
|
-
.map(([name, sources]) => [name, Object.freeze(sources.sort(compareGuardedSources))]));
|
|
566
179
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
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);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
visit(dir);
|
|
205
|
+
return files.sort(compareCodePoints);
|
|
576
206
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
// basename is not an identity — two unrelated bundles can legitimately
|
|
582
|
-
// share one. Require the entry's own scheduler-context descriptor to
|
|
583
|
-
// additionally confirm the resolved path. An entry whose owning path
|
|
584
|
-
// cannot be established is never assumed to be ours — that silent
|
|
585
|
-
// assumption is exactly what let an isolated/foreign bundle's sync
|
|
586
|
-
// reach for another bundle's real scheduler entries. (`bundlePath` is
|
|
587
|
-
// only set for a primary sync — a `--bundle <target>` entry's
|
|
588
|
-
// descriptor reflects the invoking process's OWN primary directory,
|
|
589
|
-
// not the targeted bundle's, so it is not a meaningful signal there;
|
|
590
|
-
// that case keeps relying on config-name uniqueness below.)
|
|
591
|
-
return entry.ownerBundlePath !== undefined && entry.ownerBundlePath === input.bundlePath;
|
|
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);
|
|
592
211
|
}
|
|
593
|
-
|
|
594
|
-
return
|
|
595
|
-
return false;
|
|
596
|
-
}
|
|
597
|
-
function assertNoForeignIds(desired, input) {
|
|
598
|
-
const wanted = new Set(desired.map(({ id }) => id));
|
|
599
|
-
const foreign = input.installed.find((entry) => wanted.has(entry.id) && !belongsToBundle(entry, input));
|
|
600
|
-
if (!foreign)
|
|
601
|
-
return;
|
|
602
|
-
const where = foreign.ownerBundlePath
|
|
603
|
-
? `the bundle at ${JSON.stringify(foreign.ownerBundlePath)}`
|
|
604
|
-
: foreign.target
|
|
605
|
-
? `bundle ${JSON.stringify(foreign.target)}`
|
|
606
|
-
: "the default bundle";
|
|
607
|
-
const mine = input.bundlePath ? ` (this sync is scoped to ${JSON.stringify(input.bundlePath)})` : "";
|
|
608
|
-
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");
|
|
609
|
-
}
|
|
610
|
-
function assertUniqueDesiredIds(desired) {
|
|
611
|
-
const seen = new Set();
|
|
612
|
-
for (const binding of desired) {
|
|
613
|
-
if (seen.has(binding.id)) {
|
|
614
|
-
throw new UsageError(`Desired scheduler id collision for ${JSON.stringify(binding.id)}; no native definitions were changed.`, "RESOURCE_ALREADY_EXISTS");
|
|
615
|
-
}
|
|
616
|
-
seen.add(binding.id);
|
|
212
|
+
catch {
|
|
213
|
+
return undefined;
|
|
617
214
|
}
|
|
618
215
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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");
|
|
624
230
|
}
|
|
625
|
-
seen.add(binding.id);
|
|
626
231
|
}
|
|
627
232
|
}
|
|
628
233
|
/**
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
*
|
|
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`.
|
|
632
238
|
*/
|
|
633
|
-
function
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
function taskFailure(file, ref, cause) {
|
|
637
|
-
const detail = taskSourceErrorDetail(cause);
|
|
638
|
-
const reason = detail === errorMessage(cause) ? `${file}: ${detail}` : detail;
|
|
639
|
-
return Object.freeze({ path: file, ref, reason });
|
|
640
|
-
}
|
|
641
|
-
function workflowFailure(file, ref, cause) {
|
|
642
|
-
return Object.freeze({ path: file, ...(ref ? { ref } : {}), reason: errorMessage(cause) });
|
|
643
|
-
}
|
|
644
|
-
function errorMessage(cause) {
|
|
645
|
-
return cause instanceof Error ? cause.message : String(cause);
|
|
239
|
+
export function assertTaskScheduleCronValid(v4, backend) {
|
|
240
|
+
for (const scheduleEntry of v4.schedule)
|
|
241
|
+
parseSchedule(scheduleEntry.cron, backend);
|
|
646
242
|
}
|
|
647
|
-
export function
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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;
|
|
657
256
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if (JSON.stringify(current) !== JSON.stringify(manifest)) {
|
|
661
|
-
throw new Error(`directory manifest changed: ${manifest.directoryPath}`);
|
|
662
|
-
}
|
|
257
|
+
catch {
|
|
258
|
+
return undefined;
|
|
663
259
|
}
|
|
664
260
|
}
|
|
665
|
-
|
|
666
|
-
throw new UsageError(`Scheduler desired source read set changed after projection; refusing native mutation: ${errorMessage(cause)}`, "RESOURCE_ALREADY_EXISTS");
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
/**
|
|
670
|
-
* Shared with the symlink classification in {@link compileTaskSources}: a
|
|
671
|
-
* `.yml` under `tasks/` (or, for `akm-task`, anywhere) is a task candidate —
|
|
672
|
-
* one classifier for both a captured file and an uncaptured symlink entry.
|
|
673
|
-
*/
|
|
674
|
-
function isAuthoredTaskRelativePath(adapterId, relativePath) {
|
|
675
|
-
if (!relativePath.endsWith(".yml"))
|
|
676
|
-
return false;
|
|
677
|
-
if (adapterId === "akm-task")
|
|
678
|
-
return true;
|
|
679
|
-
return path.posix.dirname(relativePath) === "tasks";
|
|
261
|
+
return undefined;
|
|
680
262
|
}
|
|
681
263
|
/**
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
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.
|
|
686
269
|
*/
|
|
687
|
-
function
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
#sourceRoot;
|
|
695
|
-
#collector = new GuardedExecutionSourceCollector();
|
|
696
|
-
constructor(input) {
|
|
697
|
-
this.#adapterId = input.adapterId;
|
|
698
|
-
this.#sourceRoot = path.resolve(input.sourceRoot);
|
|
699
|
-
const root = this.#collector.trackDirectory(this.#sourceRoot, this.#sourceRoot);
|
|
700
|
-
if (input.adapterId === "akm") {
|
|
701
|
-
for (const scheduledName of ["tasks", "workflows"]) {
|
|
702
|
-
const entry = root.entries.find((candidate) => candidate.name === scheduledName);
|
|
703
|
-
if (entry?.kind === "symlink") {
|
|
704
|
-
throw new UsageError(`${path.join(this.#sourceRoot, scheduledName)} is a symbolic source with a physical source identity collision; guarded reads require one no-follow owner.`, "RESOURCE_ALREADY_EXISTS");
|
|
705
|
-
}
|
|
706
|
-
}
|
|
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;
|
|
707
277
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
const taskRoot = path.join(this.#sourceRoot, "tasks");
|
|
713
|
-
const taskManifest = this.#collector.trackDirectory(taskRoot, this.#sourceRoot);
|
|
714
|
-
for (const entry of taskManifest.entries) {
|
|
715
|
-
if (entry.kind === "file" && entry.name.endsWith(".yml")) {
|
|
716
|
-
candidates.push(path.join(taskRoot, entry.name));
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
if (rootDirectories.has("workflows")) {
|
|
721
|
-
candidates.push(...this.#collector.enumerateTree(path.join(this.#sourceRoot, "workflows"), this.#sourceRoot));
|
|
722
|
-
}
|
|
278
|
+
if (bundle === undefined) {
|
|
279
|
+
if (row.ownerBundlePath === undefined || row.ownerBundlePath === scope.bundlePath)
|
|
280
|
+
return scope;
|
|
281
|
+
continue;
|
|
723
282
|
}
|
|
724
|
-
|
|
725
|
-
|
|
283
|
+
if (bundle === scope.bundleName && row.ownerBundlePath === scope.bundlePath)
|
|
284
|
+
return scope;
|
|
285
|
+
}
|
|
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]);
|
|
300
|
+
}
|
|
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
|
+
});
|
|
726
314
|
}
|
|
727
|
-
|
|
728
|
-
|
|
315
|
+
}
|
|
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;
|
|
729
334
|
}
|
|
730
|
-
|
|
731
|
-
|
|
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;
|
|
732
346
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
737
|
-
authoredTaskSources(adapterId) {
|
|
738
|
-
return this.#collector
|
|
739
|
-
.snapshot()
|
|
740
|
-
.sources.filter((file) => file.authored && isAuthoredTaskRelativePath(adapterId, file.relativePath))
|
|
741
|
-
.sort(compareGuardedSources);
|
|
742
|
-
}
|
|
743
|
-
authoredWorkflowSources(adapterId) {
|
|
744
|
-
return this.#collector
|
|
745
|
-
.snapshot()
|
|
746
|
-
.sources.filter((file) => file.authored && isAuthoredWorkflowRelativePath(adapterId, file.relativePath))
|
|
747
|
-
.sort(compareGuardedSources);
|
|
748
|
-
}
|
|
749
|
-
/**
|
|
750
|
-
* Every `kind: "symlink"` entry across the directory manifests captured so
|
|
751
|
-
* far (never read, never followed — see `captureGuardedDirectoryManifest`),
|
|
752
|
-
* with its path relative to the source root so callers can classify it
|
|
753
|
-
* with the same predicates as a real, captured file.
|
|
754
|
-
*/
|
|
755
|
-
symlinkSources() {
|
|
756
|
-
const entries = [];
|
|
757
|
-
for (const manifest of this.#collector.snapshot().directoryManifests) {
|
|
758
|
-
for (const entry of manifest.entries) {
|
|
759
|
-
if (entry.kind !== "symlink")
|
|
760
|
-
continue;
|
|
761
|
-
const sourcePath = path.join(manifest.directoryPath, entry.name);
|
|
762
|
-
entries.push({ sourcePath, relativePath: toPosix(path.relative(this.#sourceRoot, sourcePath)) });
|
|
763
|
-
}
|
|
347
|
+
if (expected === undefined || current.signature === expected) {
|
|
348
|
+
unchanged.push(binding.id);
|
|
349
|
+
continue;
|
|
764
350
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
return this.#collector.fileContext(root, file);
|
|
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
|
+
}));
|
|
774
359
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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);
|
|
372
|
+
}
|
|
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);
|
|
377
|
+
}
|
|
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) {
|
|
780
399
|
return Object.freeze({
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
sourceRealPath: root.realPath,
|
|
784
|
-
sourcePhysicalIdentity: root.physicalIdentity,
|
|
785
|
-
sourceDirectoryVersion: root.version,
|
|
786
|
-
files: Object.freeze([...guarded.sources].sort(compareGuardedSources)),
|
|
787
|
-
directoryManifests: guarded.directoryManifests,
|
|
400
|
+
binding: Object.freeze([...current.binding]),
|
|
401
|
+
contextPath: input.installOptions?.contextPath ?? current.contextPath,
|
|
788
402
|
});
|
|
789
403
|
}
|
|
790
|
-
|
|
791
|
-
const root = path.resolve(containmentRoot);
|
|
792
|
-
const parent = path.dirname(path.resolve(file));
|
|
793
|
-
const relative = path.relative(root, parent);
|
|
794
|
-
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
795
|
-
return;
|
|
796
|
-
this.#collector.trackDirectory(root, root);
|
|
797
|
-
if (relative === "")
|
|
798
|
-
return;
|
|
799
|
-
let current = root;
|
|
800
|
-
for (const segment of relative.split(path.sep)) {
|
|
801
|
-
current = path.join(current, segment);
|
|
802
|
-
this.#collector.trackDirectory(current, root);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
function compareGuardedSources(left, right) {
|
|
807
|
-
return compareCodePoints(left.sourcePath, right.sourcePath);
|
|
404
|
+
return input.installOptions ? Object.freeze({ ...input.installOptions }) : undefined;
|
|
808
405
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
+
});
|
|
818
452
|
}
|
|
819
|
-
const conceptId = input.adapterId === "akm" ? `tasks/${invocation[2]}` : invocation[2];
|
|
820
|
-
return Object.freeze({ kind: "task", ref: makeBundleRef(bundle, conceptId) });
|
|
821
453
|
}
|
|
822
|
-
throw new UsageError("Installed scheduler invocation has no exact canonical source owner.", "RESOURCE_ALREADY_EXISTS");
|
|
823
|
-
}
|
|
824
|
-
function freezeRemovalExpectation(expectation) {
|
|
825
|
-
return Object.freeze({
|
|
826
|
-
...expectation,
|
|
827
|
-
logicalSource: Object.freeze({ ...expectation.logicalSource }),
|
|
828
|
-
invocation: Object.freeze([...expectation.invocation]),
|
|
829
|
-
});
|
|
830
|
-
}
|
|
831
|
-
function expectationForBinding(binding, state, fingerprint) {
|
|
832
454
|
return {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
455
|
+
backend,
|
|
456
|
+
dryRun: true,
|
|
457
|
+
adds,
|
|
458
|
+
updates,
|
|
459
|
+
removes,
|
|
460
|
+
unchanged: [...unchanged],
|
|
461
|
+
hasRemovals: removes.length > 0,
|
|
462
|
+
failures: [...failures],
|
|
840
463
|
};
|
|
841
464
|
}
|
|
842
|
-
function
|
|
843
|
-
return
|
|
844
|
-
...expectation,
|
|
845
|
-
logicalSource: Object.freeze({ ...expectation.logicalSource }),
|
|
846
|
-
invocation: Object.freeze([...expectation.invocation]),
|
|
847
|
-
});
|
|
848
|
-
}
|
|
849
|
-
function freezeOperation(operation) {
|
|
850
|
-
return Object.freeze(operation);
|
|
851
|
-
}
|
|
852
|
-
function sameInvocation(left, right) {
|
|
853
|
-
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);
|
|
854
467
|
}
|
|
855
|
-
// Retain the concrete error in this module's public dependency graph so callers
|
|
856
|
-
// can continue to identify ownership failures without importing an adapter.
|
|
857
|
-
export { WorkflowSourceRejectionError };
|