@deksden-com/dd-flow-cli 0.5.0 → 0.7.0
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 +12 -0
- package/README.md +10 -3
- package/dist/build-info.json +5 -5
- package/dist/cli/help.js +24 -11
- package/dist/cli/run-cli.js +117 -28
- package/dist/domain/flow-contract.js +15 -4
- package/dist/domain/session-coverage.js +88 -0
- package/dist/runtime/context.js +8 -2
- package/dist/schemas/code-stage-report.schema.json +7 -2
- package/dist/schemas/engine-manifest.schema.json +22 -0
- package/dist/schemas/flow-contract.schema.json +15 -13
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/schemas/mb-upgrade-migration-report.schema.json +3 -1
- package/dist/schemas/merge-stage-report-legacy-0.4.2.schema.json +24 -0
- package/dist/schemas/run-engine-binding.schema.json +37 -0
- package/dist/schemas/stage-prompt.schema.json +9 -5
- package/dist/schemas/stage-start-response.schema.json +6 -5
- package/dist/services/canon.js +15 -1
- package/dist/services/cleanup.js +77 -0
- package/dist/services/cli-operation-classifier.js +52 -8
- package/dist/services/compatibility-preflight.js +1 -1
- package/dist/services/dashboard.js +2 -2
- package/dist/services/engines.js +408 -30
- package/dist/services/hooks.js +28 -15
- package/dist/services/lanes.js +0 -4
- package/dist/services/merge-queue.js +48 -0
- package/dist/services/merge-worker.js +3 -4
- package/dist/services/migrations.js +307 -44
- package/dist/services/plan-runtime.js +4 -4
- package/dist/services/plans.js +5 -3
- package/dist/services/protocols.js +23 -2
- package/dist/services/run-engine-bindings.js +157 -0
- package/dist/services/run-projection.js +18 -4
- package/dist/services/runs.js +54 -7
- package/dist/services/schema-validation.js +96 -0
- package/dist/services/sessions.js +33 -73
- package/dist/services/stage-lifecycle.js +298 -45
- package/dist/services/status.js +8 -3
- package/dist/storage/database.js +32 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add target-driven engine routing, explicit storage migration safety, immutable RUN engine bindings, engine schema registry validation, and self-contained plan execution guidance.
|
|
8
|
+
|
|
9
|
+
## 0.6.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 47cd270: Add SPC-006 bootstrap stage start/context packets and sealed stage finish, while repairing Memory Bank upgrade stage routing, Codex hook event identity, PATH-stable per-home hook installation, canonical Codex session registration, session coverage projections, terminal orchestrator closure, and safe reconciliation of runs whose legacy authority and workspace are gone.
|
|
14
|
+
|
|
3
15
|
## 0.5.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -75,10 +75,10 @@ dd-flow protocol register HANDSHAKE-001 --project-root "$PWD" --json
|
|
|
75
75
|
dd-flow protocol status PRT-HANDSHAKE-001 --project-root "$PWD" --json
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
Inspect the canonical plan and update its SQLite progress projection:
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
dd-flow plan
|
|
81
|
+
dd-flow plan status PRT-HANDSHAKE-001 --project-root "$PWD" --json
|
|
82
82
|
dd-flow plan item start PRT-HANDSHAKE-001 P1 --project-root "$PWD" --json
|
|
83
83
|
dd-flow plan item done PRT-HANDSHAKE-001 P1 --project-root "$PWD" --summary "Implemented" --evidence "pnpm test" --json
|
|
84
84
|
```
|
|
@@ -355,9 +355,16 @@ Codex integration is explicit and auditable. Managed Codex homes and hooks are l
|
|
|
355
355
|
```bash
|
|
356
356
|
dd-flow codex home plan --project-root "$PROJECT_ROOT" --json
|
|
357
357
|
dd-flow codex hooks status --project-root "$PROJECT_ROOT" --target isolated --json
|
|
358
|
-
dd-flow codex hook handle --event Stop --
|
|
358
|
+
dd-flow codex hook handle --event Stop --json
|
|
359
359
|
```
|
|
360
360
|
|
|
361
|
+
Installed Codex hooks intentionally omit `--project-root`: Codex supplies the
|
|
362
|
+
session `cwd` in the hook JSON on stdin, and the handler resolves the registered
|
|
363
|
+
project from that path. The option remains available as a manual override. If
|
|
364
|
+
the CLI is upgraded, reinstall the hook in each active `CODEX_HOME`, verify it
|
|
365
|
+
with `dd-flow codex hooks status`, and start a new Codex session;
|
|
366
|
+
`--project-root` belongs to installer/status commands, not the stored handler.
|
|
367
|
+
|
|
361
368
|
## Verification
|
|
362
369
|
|
|
363
370
|
Local verification for this repository:
|
package/dist/build-info.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-08-
|
|
3
|
+
"cli_version": "0.7.0",
|
|
4
|
+
"cli_commit": "a09f6b663231d3179bea64b2fd9bb4ffa30096c2",
|
|
5
|
+
"built_at": "2026-08-13T04:31:51.147Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
|
-
"version": "3.
|
|
8
|
-
"commit": "
|
|
7
|
+
"version": "3.2.0",
|
|
8
|
+
"commit": "2a1aaec84ee7d62b9f5a6549de5d1f0cb88082c0",
|
|
9
9
|
"flow_contract": "dd-flow-canonical-2026-08",
|
|
10
10
|
"repo_root": "/Users/deksden/Documents/_Projects/dd-memorybank",
|
|
11
11
|
"memorybank_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank",
|
package/dist/cli/help.js
CHANGED
|
@@ -12,13 +12,13 @@ Core commands:
|
|
|
12
12
|
version Print the installed dd-flow CLI package version.
|
|
13
13
|
status Inspect dd-flow runtime, project and canon roots.
|
|
14
14
|
engine install/list/info Manage locally installed dd-flow engine snapshots.
|
|
15
|
-
engine resolve/doctor
|
|
15
|
+
engine resolve/doctor/bind-run Inspect router selection, health, and recover a legacy RUN binding.
|
|
16
16
|
canon register/status/resolve Register and resolve the canonical Memory Bank.
|
|
17
17
|
project register/status/summary Register a project, inspect state, and publish project summaries.
|
|
18
18
|
project resolve/archive Resolve typed ids and archive stale project roots.
|
|
19
19
|
project migrate-ids Migrate old project ids.
|
|
20
20
|
protocol register/status/transition Register, inspect, transition and repair protocol state.
|
|
21
|
-
migration impact/plan/report/verify Assess release impact and
|
|
21
|
+
migration impact/plan/report/verify/apply Assess release impact and explicitly apply mb-upgrade migration evidence.
|
|
22
22
|
cleanup scan/apply Detect and repair stale local runtime state.
|
|
23
23
|
run start/status/list/timeline/usage/flags Track concrete flow executions, timing, source-aware usage and RUN-local flow flags.
|
|
24
24
|
stage start/finish Run the canonical two-command stage lifecycle and generate evidence.
|
|
@@ -28,7 +28,7 @@ Core commands:
|
|
|
28
28
|
lane workspace/lock/status Manage shared workspace lanes and leases.
|
|
29
29
|
merge status/one-shot/bundle Inspect, claim, or complete merge work from the current session.
|
|
30
30
|
merge-worker status/start/stop Manage long-lived project merge workers.
|
|
31
|
-
merge-queue status/next
|
|
31
|
+
merge-queue status/claim/next Claim, wait for, complete, or fail merge jobs.
|
|
32
32
|
session register/status/stop Register flow sessions and stop workers.
|
|
33
33
|
dashboard data/render/open/refresh Render, refresh, open, and inspect project/global dashboards.
|
|
34
34
|
schema validate Validate canonical dd-flow JSON data contracts.
|
|
@@ -86,6 +86,7 @@ Usage:
|
|
|
86
86
|
dd-flow engine info [--package <name>] [--version <version>] --json
|
|
87
87
|
dd-flow engine resolve [--project-root <root>] --json
|
|
88
88
|
dd-flow engine doctor [--project-root <root>] --json
|
|
89
|
+
dd-flow engine bind-run <RUN-ID> --project-root <root> --engine-version <version> --reason <text> [--integrity-checksum <sha256>] --json
|
|
89
90
|
|
|
90
91
|
The globally invoked dd-flow command is the stable router surface. Engine snapshots live under DD_FLOW_HOME/engines and are selected by project Memory Bank compatibility data. Running an old package through npx can install that package's own engine without replacing the global router:
|
|
91
92
|
|
|
@@ -93,11 +94,14 @@ The globally invoked dd-flow command is the stable router surface. Engine snapsh
|
|
|
93
94
|
|
|
94
95
|
Router-native engine commands never mutate project runtime data. Normal project commands are routed to a compatible engine or fail closed with a missing-engine diagnostic.
|
|
95
96
|
|
|
97
|
+
engine bind-run is the one recovery command for a legacy RUN without an engine-binding sidecar. It probes the requested installed snapshot with run status before atomically writing an immutable binding; later RUN commands route through that snapshot.
|
|
98
|
+
|
|
96
99
|
Examples:
|
|
97
100
|
dd-flow engine install --json
|
|
98
101
|
dd-flow engine list --json
|
|
99
102
|
dd-flow engine resolve --project-root "$PWD" --json
|
|
100
|
-
dd-flow engine doctor --project-root "$PWD" --json
|
|
103
|
+
dd-flow engine doctor --project-root "$PWD" --json
|
|
104
|
+
dd-flow engine bind-run RUN-193 --project-root "$PWD" --engine-version 0.4.2 --reason "legacy flow-run@1" --json`
|
|
101
105
|
],
|
|
102
106
|
[
|
|
103
107
|
"status",
|
|
@@ -143,7 +147,7 @@ Usage:
|
|
|
143
147
|
dd-flow id next --type protocol|run --project-root <root> --slug <slug> --json
|
|
144
148
|
dd-flow run status <RUN-ID|RUN-short-id> --project-root <root> --json
|
|
145
149
|
dd-flow run list --project-root <root> --json
|
|
146
|
-
dd-flow run timeline <RUN-ID|RUN-short-id> --project-root <root>
|
|
150
|
+
dd-flow run timeline <RUN-ID|RUN-short-id> --project-root <root> --json
|
|
147
151
|
dd-flow run usage <RUN-ID|RUN-short-id> --project-root <root> [--group-by session|role|stage|aspect|plan-item] --json
|
|
148
152
|
dd-flow run flags status <RUN-ID|RUN-short-id> --project-root <root> --json
|
|
149
153
|
dd-flow run flags revise <RUN-ID|RUN-short-id> --project-root <root> --expected-revision <n> --idempotency-key <key> [--preset <name>] [--flag key=value] [--flag key=value] [--allow-downgrade --reason <text>] --json
|
|
@@ -165,9 +169,10 @@ Examples:
|
|
|
165
169
|
|
|
166
170
|
Usage:
|
|
167
171
|
dd-flow stage start <RUN-ID|RUN-short-id> --project-root <root> --stage <name> [--dir <NN-stage-slug>] --json
|
|
168
|
-
dd-flow stage
|
|
172
|
+
dd-flow stage start --bootstrap --project-root <root> --stage specify --subject <safe-subject> [--intake-file <path>] --json
|
|
173
|
+
dd-flow stage finish <RUN-ID|RUN-short-id> --project-root <root> --stage <name> [--outcome done|blocked|failed] [--semantic-file <RUN-local-json>] --json
|
|
169
174
|
|
|
170
|
-
stage start
|
|
175
|
+
stage start is the worker's first practical flow command. It performs deterministic setup and returns a ready-to-use, stage-specific work packet with trusted runtime facts, compatibility and permission preflight, aliases, write boundary and exact finish command. The same packet is written to RUN-local stage-prompt.md and stage-prompt.json. --bootstrap creates the initial protocol/RUN envelope and copies the supplied intake into the RUN home before starting specify. stage finish defaults to @stage/stage-input.json, accepts semantic --outcome and always generates validated JSON, Markdown, template-backed HTML and a durable protocol-summary projection. Raw intake and telemetry remain RUN-local.`
|
|
171
176
|
],
|
|
172
177
|
[
|
|
173
178
|
"id",
|
|
@@ -233,11 +238,12 @@ New registrations use full typed ids such as PRJ-001-dd-flow-playground and shor
|
|
|
233
238
|
`dd-flow migration - plan and verify mb-upgrade runtime/home data migrations
|
|
234
239
|
|
|
235
240
|
Usage:
|
|
236
|
-
dd-flow migration plan --project-root <root> [--source-version <version>] [--target-version <version>] [--run <RUN-ID>] [--backup-path <path>] [--backup-created-at <iso>]
|
|
237
|
-
dd-flow migration report --project-root <root> [--source-version <version>] [--target-version <version>] [--run <RUN-ID>] [--backup-path <path>] [--backup-created-at <iso>]
|
|
241
|
+
dd-flow migration plan --project-root <root> [--source-version <version>] [--target-version <version>] [--run <RUN-ID>] [--backup-path <path>] [--backup-created-at <iso>] --json
|
|
242
|
+
dd-flow migration report --project-root <root> [--source-version <version>] [--target-version <version>] [--run <RUN-ID>] [--backup-path <path>] [--backup-created-at <iso>] --json
|
|
238
243
|
dd-flow migration verify --file <mb-upgrade-migration-report.json> --json
|
|
244
|
+
dd-flow migration apply --project-root <root> --file <mb-upgrade-migration-report.json> --json
|
|
239
245
|
|
|
240
|
-
Migration commands are evidence and guardrail helpers for canonical mb-upgrade. They do not silently migrate runtime/home data during ordinary CLI use. Applying migrations
|
|
246
|
+
Migration commands are evidence and guardrail helpers for canonical mb-upgrade. They do not silently migrate runtime/home data during ordinary CLI use. Applying migrations is an explicit contour requiring a real verified backup, adjacent migration chain, quiescent shared home, recovery evidence and idempotent rerun.
|
|
241
247
|
|
|
242
248
|
Related commands:
|
|
243
249
|
dd-flow status --project-root <root> --json
|
|
@@ -353,13 +359,14 @@ start is idempotent: if a worker, claimed job, or active merge lock already exis
|
|
|
353
359
|
Usage:
|
|
354
360
|
dd-flow merge-queue status --project-root <root> --json
|
|
355
361
|
dd-flow merge-queue next --project-root <root> --worker-id <id> [--path <workspace>] --json
|
|
362
|
+
dd-flow merge-queue claim <protocol-id> [--project-root <root>] --worker-id <id> [--path <workspace>] --json
|
|
356
363
|
dd-flow merge-queue wait-next --project-root <root> --worker-id <id> [--path <workspace>] --timeout <seconds> --poll-interval <seconds> [--acquire-lock true] --json
|
|
357
364
|
dd-flow merge-queue complete <protocol-id> [--project-root <root>] --worker-id <id> [--path <workspace>] --summary <text> --json
|
|
358
365
|
dd-flow merge-queue note <protocol-id> [--project-root <root>] --worker-id <id> --summary <text> --json
|
|
359
366
|
dd-flow merge-queue fail <protocol-id> [--project-root <root>] --worker-id <id> [--path <workspace>] --reason <text> --requeue true|false --json
|
|
360
367
|
dd-flow merge-queue cancel <protocol-id> [--project-root <root>] --reason <text> [--worker-id <id>] [--path <workspace>] [--force] --json
|
|
361
368
|
|
|
362
|
-
The worker must own the merge lane from the registered merge workspace before claiming, completing, or failing queued protocols. Outputs use queue_item/protocol/claim as the primary contract and retain job as a compatibility alias. note updates the final completion summary after post-complete cleanup and does not require a lane lock. --path defaults to the current working directory. Use --worker-id as the owner identity.
|
|
369
|
+
The worker must own the merge lane from the registered merge workspace before claiming, completing, or failing queued protocols. next remains FIFO; claim <protocol-id> atomically claims only that ready/requeued job. Outputs use queue_item/protocol/claim as the primary contract and retain job as a compatibility alias. note updates the final completion summary after post-complete cleanup and does not require a lane lock. --path defaults to the current working directory. Use --worker-id as the owner identity.
|
|
363
370
|
|
|
364
371
|
For branch-level integration prefer:
|
|
365
372
|
dd-flow merge bundle status --project-root "$PWD" --path "$PWD" --json
|
|
@@ -526,6 +533,12 @@ Usage:
|
|
|
526
533
|
dd-flow codex home print-env --project-root <root> [--profile <name>] --json
|
|
527
534
|
dd-flow codex home remove --project-root <root> [--profile <name>] --mode keep-shared|remove-owned --json
|
|
528
535
|
|
|
536
|
+
dd-flow codex hook handle --event <HookEventName> [--project-root <root>] --json
|
|
537
|
+
|
|
538
|
+
Managed hooks omit --project-root. Codex supplies cwd in the stdin payload; the
|
|
539
|
+
handler resolves the registered project from cwd. The option is only a manual
|
|
540
|
+
override.
|
|
541
|
+
|
|
529
542
|
Managed homes isolate dd-flow hooks while sharing selected Codex state from the source home.`
|
|
530
543
|
],
|
|
531
544
|
[
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import { createContext } from "../runtime/context.js";
|
|
2
|
+
import { createContext, createRouterContext } from "../runtime/context.js";
|
|
3
3
|
import { helpForArgs } from "./help.js";
|
|
4
4
|
import { AppError, isAppError } from "../shared/errors.js";
|
|
5
5
|
import { writeJson } from "../shared/json.js";
|
|
6
6
|
import { archiveProject, getProjectStatus, migrateProjectIds, registerProject, resolveProject } from "../services/projects.js";
|
|
7
7
|
import { cancelProtocol, getProtocolBlockers, getProtocolStatus, getReadyProtocols, implementProtocol, readyForMerge, registerProtocol, requireProtocol, syncProtocolFromRun, transitionProtocol } from "../services/protocols.js";
|
|
8
8
|
import { blockPlanItem, completePlanItem, getPlanStatus, skipPlanItem, startPlanItem } from "../services/plans.js";
|
|
9
|
-
import { cancelMergeQueueJob, claimNextMergeJob, claimMergeBundle, completeMergeBundle, completeMergeJob, failMergeBundle, failMergeJob, getMergeBundleStatus, getMergeQueueStatus, noteMergeJob, waitNextMergeJob } from "../services/merge-queue.js";
|
|
9
|
+
import { cancelMergeQueueJob, claimNextMergeJob, claimMergeJob, claimMergeBundle, completeMergeBundle, completeMergeJob, failMergeBundle, failMergeJob, getMergeBundleStatus, getMergeQueueStatus, noteMergeJob, waitNextMergeJob } from "../services/merge-queue.js";
|
|
10
10
|
import { getMergeWorkerStatus, oneShotMergeClaim, startMergeWorker, stopProjectMergeWorker } from "../services/merge-worker.js";
|
|
11
11
|
import { cleanupApply, cleanupScan } from "../services/cleanup.js";
|
|
12
12
|
import { getCodexHomeStatus, getCodexHooksStatus, handleCodexHook, initCodexHome, installCodexHooks, planCodexHome, printCodexHomeEnv, printCodexHooks, removeCodexHome, removeCodexHooks } from "../services/hooks.js";
|
|
@@ -24,12 +24,13 @@ import { attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunTi
|
|
|
24
24
|
import { previewNextEntityId } from "../services/ids.js";
|
|
25
25
|
import { validateSchema } from "../services/schema-validation.js";
|
|
26
26
|
import { preflightMemoryPermissions } from "../services/memory-permissions.js";
|
|
27
|
-
import { assessMigrationImpact, planMigration, verifyMigrationReport } from "../services/migrations.js";
|
|
27
|
+
import { applyMigration, assessMigrationImpact, assertUpgradeRunStorageReady, planMigration, verifyMigrationReport } from "../services/migrations.js";
|
|
28
28
|
import { publishProjectSummary } from "../services/project-summary.js";
|
|
29
29
|
import { requireProjectByRoot } from "../services/projects.js";
|
|
30
30
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
31
|
-
import { doctorEngines, engineInfo, installCurrentEngine, listEngines, resolveEngine, resolveOperationProjectRoot, routeArgsThroughEngine } from "../services/engines.js";
|
|
31
|
+
import { doctorEngines, bindRunEngine, engineInfo, installCurrentEngine, listEngines, resolveEngine, resolveExplicitProjectRoot, resolveOperationProjectRoot, routeArgsThroughEngine } from "../services/engines.js";
|
|
32
32
|
import { preflightCliCompatibility } from "../services/compatibility-preflight.js";
|
|
33
|
+
import { classifyCliOperation, requiresExplicitUpgradeAuthorization } from "../services/cli-operation-classifier.js";
|
|
33
34
|
import { renderWorkerPrompt } from "../services/prompts.js";
|
|
34
35
|
import { finishStage, startStage } from "../services/stage-lifecycle.js";
|
|
35
36
|
const defaultIo = {
|
|
@@ -58,20 +59,27 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
58
59
|
io.stdout.write(`${help}\n`);
|
|
59
60
|
return 0;
|
|
60
61
|
}
|
|
61
|
-
const
|
|
62
|
+
const classification = classifyCliOperation(output.args, env);
|
|
63
|
+
if (requiresExplicitUpgradeAuthorization(output.args) && classification.mode !== "mb_upgrade") {
|
|
64
|
+
throw new AppError("upgrade_authorization_required", "This mb-upgrade command requires the explicit mb-upgrade authorization marker and allowlisted target context", 1, {
|
|
65
|
+
operation: classification.operation,
|
|
66
|
+
required_marker: "DD_FLOW_COMPATIBILITY_MODE=mb-upgrade"
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const routerContext = createRouterContext(env);
|
|
62
70
|
emitHumanProgress(io, output, progress, "start");
|
|
63
|
-
const scopeProjectRoot =
|
|
64
|
-
const routed = await routeArgsThroughEngine(
|
|
71
|
+
const scopeProjectRoot = resolveProjectRootBeforeRouting(output.args, env);
|
|
72
|
+
const routed = await routeArgsThroughEngine(routerContext, output.json ? [...output.args, "--json"] : output.args, io, io.stdin, env, scopeProjectRoot);
|
|
65
73
|
if (routed?.routed && !routed.inProcess) {
|
|
66
74
|
emitHumanProgress(io, output, progress, "done");
|
|
67
75
|
return routed.exitCode;
|
|
68
76
|
}
|
|
69
77
|
const dispatchContext = routed?.inProcess
|
|
70
|
-
? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version })
|
|
71
|
-
:
|
|
78
|
+
? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version }, databaseModeForCommand(output.args, classification))
|
|
79
|
+
: createContext(env, databaseModeForCommand(output.args, classification));
|
|
72
80
|
preflightCliCompatibility(dispatchContext, output.args, env, scopeProjectRoot);
|
|
73
|
-
const result = await dispatch(output.args, dispatchContext, io, scopeProjectRoot);
|
|
74
|
-
const refreshWarnings = refreshDashboardsAfterMutation(
|
|
81
|
+
const result = await dispatch(output.args, dispatchContext, io, scopeProjectRoot, classification);
|
|
82
|
+
const refreshWarnings = refreshDashboardsAfterMutation(dispatchContext, output.args, result, scopeProjectRoot);
|
|
75
83
|
const resultWithWarnings = addWarnings(result, refreshWarnings);
|
|
76
84
|
emitHumanProgress(io, output, progress, "done");
|
|
77
85
|
const payload = attachProgress(resultWithWarnings, progress, output);
|
|
@@ -94,6 +102,34 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
94
102
|
return isAppError(error) ? error.exitCode : 1;
|
|
95
103
|
}
|
|
96
104
|
}
|
|
105
|
+
function resolveProjectRootBeforeRouting(args, env) {
|
|
106
|
+
const explicit = resolveExplicitProjectRoot(args);
|
|
107
|
+
if (explicit)
|
|
108
|
+
return explicit;
|
|
109
|
+
try {
|
|
110
|
+
// Router identity lookup is read-only; it must not initialize or migrate runtime storage.
|
|
111
|
+
return resolveOperationProjectRoot(createContext(env, "read_existing"), args);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function databaseModeForCommand(args, classification) {
|
|
118
|
+
const [family, command] = args;
|
|
119
|
+
if (family === "engine" ||
|
|
120
|
+
family === "version" ||
|
|
121
|
+
family === "schema" ||
|
|
122
|
+
family === "canon" && command === "resolve" ||
|
|
123
|
+
classification.mode === "read_only_diagnostics" ||
|
|
124
|
+
classification.mode === "mb_upgrade" && (family === "status" ||
|
|
125
|
+
family === "engine" ||
|
|
126
|
+
family === "migration" && ["impact", "plan", "report", "verify", "apply"].includes(command ?? "") ||
|
|
127
|
+
family === "memory" && command === "permissions" ||
|
|
128
|
+
family === "dashboard" && ["data", "open"].includes(command ?? "") ||
|
|
129
|
+
family === "session" && command === "status"))
|
|
130
|
+
return "read_existing";
|
|
131
|
+
return "initialize";
|
|
132
|
+
}
|
|
97
133
|
function renderHumanError(payload) {
|
|
98
134
|
const lines = [`dd-flow: ${payload.error.message}`];
|
|
99
135
|
const details = recordObject(payload.error.details);
|
|
@@ -380,11 +416,11 @@ function stringValue(value) {
|
|
|
380
416
|
function shortSha(value) {
|
|
381
417
|
return value ? value.slice(0, 12) : "unknown";
|
|
382
418
|
}
|
|
383
|
-
async function dispatch(args, context, io, scopeProjectRoot = null) {
|
|
419
|
+
async function dispatch(args, context, io, scopeProjectRoot = null, classification = classifyCliOperation(args, context.env)) {
|
|
384
420
|
const [family, command, ...rest] = args;
|
|
385
421
|
const parsed = parseArgs(rest);
|
|
386
422
|
if (family === "engine") {
|
|
387
|
-
return dispatchEngine(context, command, parsed);
|
|
423
|
+
return dispatchEngine(context, command, parsed, classification);
|
|
388
424
|
}
|
|
389
425
|
if (family === "status") {
|
|
390
426
|
const rootParsed = parseArgs([command ?? "", ...rest]);
|
|
@@ -541,23 +577,28 @@ async function dispatch(args, context, io, scopeProjectRoot = null) {
|
|
|
541
577
|
}
|
|
542
578
|
if (family === "stage" && command === "start") {
|
|
543
579
|
const dir = optionalOption(parsed, "dir");
|
|
580
|
+
const bootstrap = hasOption(parsed, "bootstrap");
|
|
581
|
+
const subject = optionalOption(parsed, "subject");
|
|
582
|
+
const intakeFile = optionalOption(parsed, "intake-file");
|
|
583
|
+
const sessionId = optionalOption(parsed, "session-id");
|
|
544
584
|
return startStage(context, {
|
|
545
585
|
projectRoot: requiredOption(parsed, "project-root"),
|
|
546
|
-
runId: requiredPosition(parsed, 0, "run-id"),
|
|
586
|
+
...(bootstrap ? { bootstrap: { subject: subject ?? requiredOption(parsed, "subject"), ...(intakeFile ? { intakeFile } : {}) } } : { runId: requiredPosition(parsed, 0, "run-id") }),
|
|
547
587
|
stage: requiredOption(parsed, "stage"),
|
|
588
|
+
...(sessionId ? { sessionId } : {}),
|
|
548
589
|
...(dir ? { dir } : {})
|
|
549
590
|
});
|
|
550
591
|
}
|
|
551
592
|
if (family === "stage" && command === "finish") {
|
|
552
593
|
const dir = optionalOption(parsed, "dir");
|
|
553
|
-
const
|
|
594
|
+
const outcome = optionalOption(parsed, "outcome") ?? optionalOption(parsed, "status");
|
|
554
595
|
const semanticFile = optionalOption(parsed, "semantic-file") ?? optionalOption(parsed, "data");
|
|
555
596
|
return finishStage(context, {
|
|
556
597
|
projectRoot: requiredOption(parsed, "project-root"),
|
|
557
598
|
runId: requiredPosition(parsed, 0, "run-id"),
|
|
558
599
|
stage: requiredOption(parsed, "stage"),
|
|
559
600
|
...(dir ? { dir } : {}),
|
|
560
|
-
...(
|
|
601
|
+
...(outcome ? { outcome } : {}),
|
|
561
602
|
...(semanticFile ? { semanticFile } : {})
|
|
562
603
|
});
|
|
563
604
|
}
|
|
@@ -592,13 +633,13 @@ async function dispatch(args, context, io, scopeProjectRoot = null) {
|
|
|
592
633
|
return dispatchDashboard(context, command, parsed);
|
|
593
634
|
}
|
|
594
635
|
if (family === "schema") {
|
|
595
|
-
return dispatchSchema(command, parsed);
|
|
636
|
+
return dispatchSchema(context, command, parsed);
|
|
596
637
|
}
|
|
597
638
|
if (family === "memory") {
|
|
598
639
|
return dispatchMemory(command, parsed);
|
|
599
640
|
}
|
|
600
641
|
if (family === "migration") {
|
|
601
|
-
return dispatchMigration(context, command, parsed);
|
|
642
|
+
return dispatchMigration(context, command, parsed, classification);
|
|
602
643
|
}
|
|
603
644
|
if (family === "codex" && command === "hooks") {
|
|
604
645
|
return dispatchCodexHooks(context, parsed);
|
|
@@ -614,7 +655,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null) {
|
|
|
614
655
|
}
|
|
615
656
|
throw new AppError("usage", `Unknown command: ${args.join(" ") || "<empty>"}`, 2);
|
|
616
657
|
}
|
|
617
|
-
function dispatchEngine(context, command, parsed) {
|
|
658
|
+
function dispatchEngine(context, command, parsed, classification) {
|
|
618
659
|
if (command === "install") {
|
|
619
660
|
return installCurrentEngine(context, { force: hasOption(parsed, "force") });
|
|
620
661
|
}
|
|
@@ -625,14 +666,32 @@ function dispatchEngine(context, command, parsed) {
|
|
|
625
666
|
return engineInfo(context, { packageName: optionalOption(parsed, "package"), version: optionalOption(parsed, "version") });
|
|
626
667
|
}
|
|
627
668
|
if (command === "resolve") {
|
|
628
|
-
return resolveEngine(context, {
|
|
669
|
+
return resolveEngine(context, {
|
|
670
|
+
projectRoot: optionalOption(parsed, "project-root") ?? optionalOption(parsed, "root"),
|
|
671
|
+
env: context.env,
|
|
672
|
+
classification
|
|
673
|
+
});
|
|
629
674
|
}
|
|
630
675
|
if (command === "doctor") {
|
|
631
|
-
return doctorEngines(context, {
|
|
676
|
+
return doctorEngines(context, {
|
|
677
|
+
projectRoot: optionalOption(parsed, "project-root") ?? optionalOption(parsed, "root"),
|
|
678
|
+
env: context.env,
|
|
679
|
+
classification
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
if (command === "bind-run") {
|
|
683
|
+
return bindRunEngine(context, {
|
|
684
|
+
projectRoot: requiredOption(parsed, "project-root"),
|
|
685
|
+
runId: requiredPosition(parsed, 0, "run-id"),
|
|
686
|
+
packageName: optionalOption(parsed, "package"),
|
|
687
|
+
engineVersion: requiredOption(parsed, "engine-version"),
|
|
688
|
+
integrityChecksum: optionalOption(parsed, "integrity-checksum"),
|
|
689
|
+
reason: requiredOption(parsed, "reason")
|
|
690
|
+
});
|
|
632
691
|
}
|
|
633
692
|
throw new AppError("usage", `Unknown engine command: ${command ?? "<empty>"}`, 2);
|
|
634
693
|
}
|
|
635
|
-
function dispatchMigration(context, command, parsed) {
|
|
694
|
+
function dispatchMigration(context, command, parsed, classification) {
|
|
636
695
|
if (command === "impact")
|
|
637
696
|
return assessMigrationImpact(context, { projectRoot: requiredOption(parsed, "project-root"), ...(optionalOption(parsed, "target-version") ? { targetVersion: optionalOption(parsed, "target-version") } : {}), ...(optionalOption(parsed, "mode") ? { mode: optionalOption(parsed, "mode") } : {}) });
|
|
638
697
|
if (command === "plan" || command === "report") {
|
|
@@ -643,12 +702,26 @@ function dispatchMigration(context, command, parsed) {
|
|
|
643
702
|
runId: optionalOption(parsed, "run"),
|
|
644
703
|
backupPath: optionalOption(parsed, "backup-path"),
|
|
645
704
|
backupCreatedAt: optionalOption(parsed, "backup-created-at"),
|
|
646
|
-
allowActive:
|
|
705
|
+
allowActive: false,
|
|
706
|
+
classification
|
|
647
707
|
});
|
|
648
708
|
}
|
|
649
709
|
if (command === "verify") {
|
|
650
710
|
return verifyMigrationReport(context, { file: requiredOption(parsed, "file") });
|
|
651
711
|
}
|
|
712
|
+
if (command === "apply") {
|
|
713
|
+
return applyMigration(context, {
|
|
714
|
+
projectRoot: requiredOption(parsed, "project-root"),
|
|
715
|
+
sourceVersion: optionalOption(parsed, "source-version"),
|
|
716
|
+
targetVersion: optionalOption(parsed, "target-version"),
|
|
717
|
+
runId: optionalOption(parsed, "run"),
|
|
718
|
+
backupPath: optionalOption(parsed, "backup-path"),
|
|
719
|
+
backupCreatedAt: optionalOption(parsed, "backup-created-at"),
|
|
720
|
+
reportFile: optionalOption(parsed, "file"),
|
|
721
|
+
allowActive: false,
|
|
722
|
+
classification
|
|
723
|
+
});
|
|
724
|
+
}
|
|
652
725
|
throw new AppError("usage", `Unknown migration command: ${command ?? "<empty>"}`, 2);
|
|
653
726
|
}
|
|
654
727
|
function dispatchCanon(context, command, parsed) {
|
|
@@ -677,6 +750,14 @@ function dispatchMergeQueue(context, command, parsed, scopeProjectRoot) {
|
|
|
677
750
|
workspacePath: workspacePathOption(parsed)
|
|
678
751
|
});
|
|
679
752
|
}
|
|
753
|
+
if (command === "claim") {
|
|
754
|
+
return claimMergeJob(context, {
|
|
755
|
+
protocolId: requiredPosition(parsed, 0, "protocol-id"),
|
|
756
|
+
projectRoot: requiredOptionOrScope(parsed, scopeProjectRoot, "project-root"),
|
|
757
|
+
workerId: requiredWorkerId(parsed),
|
|
758
|
+
workspacePath: workspacePathOption(parsed)
|
|
759
|
+
});
|
|
760
|
+
}
|
|
680
761
|
if (command === "wait-next") {
|
|
681
762
|
return waitNextMergeJob(context, {
|
|
682
763
|
projectRoot: requiredOptionOrScope(parsed, scopeProjectRoot, "project-root"),
|
|
@@ -882,6 +963,9 @@ function dispatchSession(context, command, parsed) {
|
|
|
882
963
|
}
|
|
883
964
|
function dispatchRun(context, command, parsed) {
|
|
884
965
|
if (command === "start") {
|
|
966
|
+
if (optionalOption(parsed, "flow-kind") === "mb-upgrade") {
|
|
967
|
+
assertUpgradeRunStorageReady(context, requiredOption(parsed, "project-root"));
|
|
968
|
+
}
|
|
885
969
|
const protocolOverrides = readOptionalJsonFile(optionalOption(parsed, "protocol-override-file"));
|
|
886
970
|
return startFlowRun(context, {
|
|
887
971
|
projectRoot: requiredOption(parsed, "project-root"),
|
|
@@ -1139,7 +1223,7 @@ function withDashboardGuidance(action, target, result, options = {}) {
|
|
|
1139
1223
|
related_commands: relatedDashboardCommands(action, target.kind)
|
|
1140
1224
|
};
|
|
1141
1225
|
}
|
|
1142
|
-
function dispatchSchema(command, parsed) {
|
|
1226
|
+
function dispatchSchema(context, command, parsed) {
|
|
1143
1227
|
if (command === "validate") {
|
|
1144
1228
|
const schemaDir = optionalOption(parsed, "schema-dir");
|
|
1145
1229
|
const projectRoot = optionalOption(parsed, "project-root");
|
|
@@ -1147,7 +1231,8 @@ function dispatchSchema(command, parsed) {
|
|
|
1147
1231
|
schemaName: requiredOption(parsed, "schema"),
|
|
1148
1232
|
file: requiredOption(parsed, "file"),
|
|
1149
1233
|
...(schemaDir ? { schemaDir } : {}),
|
|
1150
|
-
...(projectRoot ? { projectRoot } : {})
|
|
1234
|
+
...(projectRoot ? { projectRoot } : {}),
|
|
1235
|
+
ddFlowHome: context.ddFlowHome
|
|
1151
1236
|
});
|
|
1152
1237
|
}
|
|
1153
1238
|
throw new AppError("usage", `Unknown schema command: ${command ?? "<empty>"}`, 2);
|
|
@@ -1327,11 +1412,15 @@ function dispatchCodexHome(context, parsed) {
|
|
|
1327
1412
|
function dispatchCodexHook(context, parsed, stdin) {
|
|
1328
1413
|
const action = requiredPosition(parsed, 0, "codex hook action");
|
|
1329
1414
|
if (action === "handle") {
|
|
1330
|
-
|
|
1415
|
+
const result = handleCodexHook(context, {
|
|
1331
1416
|
event: requiredOption(parsed, "event"),
|
|
1332
1417
|
projectRoot: optionalOption(parsed, "project-root"),
|
|
1333
1418
|
stdin
|
|
1334
1419
|
});
|
|
1420
|
+
if (result && typeof result === "object" && !Array.isArray(result) && "hookSpecificOutput" in result) {
|
|
1421
|
+
return { hookSpecificOutput: result.hookSpecificOutput };
|
|
1422
|
+
}
|
|
1423
|
+
return {};
|
|
1335
1424
|
}
|
|
1336
1425
|
throw new AppError("usage", `Unknown codex hook action: ${action}`, 2);
|
|
1337
1426
|
}
|
|
@@ -1670,7 +1759,7 @@ function projectRootForLaneMutation(command, parsed) {
|
|
|
1670
1759
|
return ["acquire", "heartbeat", "release", "wait-acquire"].includes(action) ? requiredOption(parsed, "project-root") : undefined;
|
|
1671
1760
|
}
|
|
1672
1761
|
function projectRootForMergeQueueMutation(context, command, parsed) {
|
|
1673
|
-
if (["next", "wait-next"].includes(command ?? "")) {
|
|
1762
|
+
if (["next", "wait-next", "claim"].includes(command ?? "")) {
|
|
1674
1763
|
return requiredOption(parsed, "project-root");
|
|
1675
1764
|
}
|
|
1676
1765
|
if (["complete", "fail", "cancel"].includes(command ?? "")) {
|
|
@@ -1691,7 +1780,7 @@ function isScopedProtocolMutation(family, command) {
|
|
|
1691
1780
|
if (family === "worktree")
|
|
1692
1781
|
return ["create", "bootstrap", "close"].includes(command ?? "");
|
|
1693
1782
|
if (family === "merge-queue")
|
|
1694
|
-
return ["complete", "note", "fail", "cancel"].includes(command ?? "");
|
|
1783
|
+
return ["claim", "complete", "note", "fail", "cancel"].includes(command ?? "");
|
|
1695
1784
|
return false;
|
|
1696
1785
|
}
|
|
1697
1786
|
function projectRootFromResult(result) {
|
|
@@ -64,6 +64,17 @@ export function loadProjectFlowContract(projectRoot) {
|
|
|
64
64
|
if (!contractPath) {
|
|
65
65
|
return defaultFlowContract;
|
|
66
66
|
}
|
|
67
|
+
return loadFlowContractFile(contractPath);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Canonical-only flows execute against the pinned canonical contract before
|
|
71
|
+
* the target project flow pack is synchronized. Keep this path explicit so a
|
|
72
|
+
* stale project-local contract cannot become the upgrade RUN authority.
|
|
73
|
+
*/
|
|
74
|
+
export function loadCanonicalFlowContract(flowRoot) {
|
|
75
|
+
return loadFlowContractFile(path.join(flowRoot, "flow-contract.json"));
|
|
76
|
+
}
|
|
77
|
+
function loadFlowContractFile(contractPath) {
|
|
67
78
|
try {
|
|
68
79
|
const value = readFlowContractObject(contractPath);
|
|
69
80
|
validateFlowContractStructure(value);
|
|
@@ -332,7 +343,6 @@ function flagStrength(key, value, definition) {
|
|
|
332
343
|
"subagents.route": ["self_check", "grouped_subagent", "focused_subagent", "mixed"],
|
|
333
344
|
"subagents.grouping": ["off", "allowlisted"],
|
|
334
345
|
"subagents.pool_fallback": ["one", "runtime"],
|
|
335
|
-
"observability.detail": ["compact", "normal", "full"],
|
|
336
346
|
"knowledge.extract": ["skip", "conditional", "required"],
|
|
337
347
|
"knowledge.promote": ["skip", "conditional", "required"],
|
|
338
348
|
"workspace.bootstrap.mode": ["not_required", "revalidate", "required"],
|
|
@@ -362,10 +372,10 @@ function numberValue(value) {
|
|
|
362
372
|
export function normalizeFlowContract(value) {
|
|
363
373
|
const version = requireNumber(value.version, "/version");
|
|
364
374
|
const id = requireString(value.id, "/id");
|
|
365
|
-
if (version === 4 || version === 5) {
|
|
375
|
+
if (version === 4 || version === 5 || version === 6) {
|
|
366
376
|
return normalizeCanonicalRuntimeContract(value, id, version);
|
|
367
377
|
}
|
|
368
|
-
if (version >
|
|
378
|
+
if (version > 6)
|
|
369
379
|
fail("/version", `unsupported flow-contract version: ${version}`);
|
|
370
380
|
if (version === 3 && !/^dd-flow-canonical-[0-9]{4}-[0-9]{2}$/.test(id)) {
|
|
371
381
|
fail("/id", "must identify a canonical version 3 flow contract");
|
|
@@ -462,7 +472,8 @@ function normalizeCanonicalRuntimeContract(value, id, version) {
|
|
|
462
472
|
"flow_run_snapshot.v1",
|
|
463
473
|
"session_coverage_units.v1",
|
|
464
474
|
"privacy_safe_observability.v1",
|
|
465
|
-
...(version
|
|
475
|
+
...(version >= 5 ? ["canonical_protocol_plan.v1"] : []),
|
|
476
|
+
...(version >= 6 ? ["stage_bootstrap_context_packet.v1"] : [])
|
|
466
477
|
],
|
|
467
478
|
stages,
|
|
468
479
|
transitions,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export function reconcileSessionCoverageRows(sessions) {
|
|
2
|
+
const sessionCounts = countValues(sessions.map((session) => session.session_id));
|
|
3
|
+
const duplicateSessionIds = duplicates(sessionCounts);
|
|
4
|
+
const invalidSessionIds = [];
|
|
5
|
+
const parsed = sessions.map((session) => {
|
|
6
|
+
try {
|
|
7
|
+
return { session, units: normalizeCoverageUnits(JSON.parse(session.coverage_units_json || "[]")) };
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
invalidSessionIds.push(session.session_id);
|
|
11
|
+
return { session, units: [] };
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
const orchestrators = parsed.filter(({ session }) => session.session_kind === "orchestrator");
|
|
15
|
+
const workers = parsed.filter(({ session }) => session.session_kind !== "orchestrator");
|
|
16
|
+
const expectedCounts = countValues(orchestrators.flatMap(({ units }) => units.map((unit) => unit.unit_id)));
|
|
17
|
+
const expectedUnitIds = [...expectedCounts.keys()].sort();
|
|
18
|
+
const duplicateExpectedUnitIds = duplicates(expectedCounts);
|
|
19
|
+
const observedOwners = new Map();
|
|
20
|
+
for (const { session, units } of workers) {
|
|
21
|
+
for (const unit of units) {
|
|
22
|
+
const owners = observedOwners.get(unit.unit_id) ?? [];
|
|
23
|
+
owners.push(session.session_id);
|
|
24
|
+
observedOwners.set(unit.unit_id, owners);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const observedUnitIds = [...observedOwners.keys()].sort();
|
|
28
|
+
const missingUnitIds = expectedUnitIds.filter((unitId) => !observedOwners.has(unitId));
|
|
29
|
+
const duplicateUnitIds = [...observedOwners.entries()]
|
|
30
|
+
.filter(([, owners]) => new Set(owners).size > 1)
|
|
31
|
+
.map(([id]) => id)
|
|
32
|
+
.sort();
|
|
33
|
+
const diagnostics = [];
|
|
34
|
+
if (orchestrators.length === 0)
|
|
35
|
+
diagnostics.push("orchestrator_session_missing");
|
|
36
|
+
if (orchestrators.length > 1)
|
|
37
|
+
diagnostics.push("multiple_orchestrator_sessions");
|
|
38
|
+
if (workers.length > 0 && expectedUnitIds.length === 0)
|
|
39
|
+
diagnostics.push("expected_worker_units_missing");
|
|
40
|
+
if (missingUnitIds.length > 0)
|
|
41
|
+
diagnostics.push("expected_worker_units_unobserved");
|
|
42
|
+
if (duplicateUnitIds.length > 0 || duplicateExpectedUnitIds.length > 0)
|
|
43
|
+
diagnostics.push("duplicate_coverage_binding");
|
|
44
|
+
if (duplicateSessionIds.length > 0)
|
|
45
|
+
diagnostics.push("duplicate_session_binding");
|
|
46
|
+
if (invalidSessionIds.length > 0)
|
|
47
|
+
diagnostics.push("invalid_coverage_units");
|
|
48
|
+
const unavailable = orchestrators.length === 0 || invalidSessionIds.length > 0;
|
|
49
|
+
const partial = orchestrators.length !== 1
|
|
50
|
+
|| (workers.length > 0 && expectedUnitIds.length === 0)
|
|
51
|
+
|| missingUnitIds.length > 0
|
|
52
|
+
|| duplicateUnitIds.length > 0
|
|
53
|
+
|| duplicateExpectedUnitIds.length > 0
|
|
54
|
+
|| duplicateSessionIds.length > 0;
|
|
55
|
+
return {
|
|
56
|
+
status: unavailable ? "unavailable" : partial ? "partial" : "complete",
|
|
57
|
+
expected_unit_ids: expectedUnitIds,
|
|
58
|
+
observed_unit_ids: observedUnitIds,
|
|
59
|
+
missing_unit_ids: missingUnitIds,
|
|
60
|
+
duplicate_unit_ids: duplicateUnitIds,
|
|
61
|
+
duplicate_expected_unit_ids: duplicateExpectedUnitIds,
|
|
62
|
+
duplicate_session_ids: duplicateSessionIds,
|
|
63
|
+
orchestrator_session_ids: orchestrators.map(({ session }) => session.session_id).sort(),
|
|
64
|
+
invalid_session_ids: [...new Set(invalidSessionIds)].sort(),
|
|
65
|
+
diagnostics
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function normalizeCoverageUnits(value) {
|
|
69
|
+
if (!Array.isArray(value))
|
|
70
|
+
throw new Error("coverage_units must be an array");
|
|
71
|
+
return value.map((item) => {
|
|
72
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
73
|
+
throw new Error("coverage unit must be an object");
|
|
74
|
+
const unitId = item.unit_id;
|
|
75
|
+
if (typeof unitId !== "string" || unitId.length === 0)
|
|
76
|
+
throw new Error("coverage unit requires unit_id");
|
|
77
|
+
return { unit_id: unitId };
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function countValues(values) {
|
|
81
|
+
const counts = new Map();
|
|
82
|
+
for (const value of values)
|
|
83
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
84
|
+
return counts;
|
|
85
|
+
}
|
|
86
|
+
function duplicates(counts) {
|
|
87
|
+
return [...counts.entries()].filter(([, count]) => count > 1).map(([id]) => id).sort();
|
|
88
|
+
}
|