@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.149
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 +130 -0
- package/README.md +13 -8
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +13 -3
- package/dist/cli/command-metadata.js +51 -16
- package/dist/cli/diagnostic-invocation.js +46 -0
- package/dist/cli/diagnostic.js +120 -0
- package/dist/cli/help.js +67 -1
- package/dist/cli/hook-ingress.js +27 -3
- package/dist/cli/memory-flow-admission.js +9 -3
- package/dist/cli/memory-init-invocation.js +10 -3
- package/dist/cli/memory-init.js +77 -68
- package/dist/cli/memory-storage-admission.js +3 -2
- package/dist/cli/memory-storage-qualification.js +105 -0
- package/dist/cli/memory-storage-transition.js +123 -0
- package/dist/cli/memory-upgrade.js +189 -0
- package/dist/cli/native-hook-ingress.js +14 -4
- package/dist/cli/native-hook-preparation.js +55 -0
- package/dist/cli/response-file.js +2 -1
- package/dist/cli/run-cli.js +79 -40
- package/dist/cli/setup.js +2 -1
- package/dist/cli.js +18 -0
- package/dist/domain/diagnostic-aspects.js +103 -0
- package/dist/domain/diagnostic-flows.js +8 -0
- package/dist/domain/stage-handler-identities.js +8 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
- package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
- package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +366 -52
- package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
- package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +71 -29
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +73 -16
- package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
- package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
- package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
- package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
- package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
- package/dist/harness-runtime/lib/entity-references.mjs +9 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +24 -9
- package/dist/harness-runtime/lib/native-children.d.mts +8 -1
- package/dist/harness-runtime/lib/native-children.mjs +114 -8
- package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
- package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
- package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
- package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
- package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
- package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +5 -3
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +20 -7
- package/dist/harness-runtime/lib/runner-events.mjs +3 -1
- package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
- package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
- package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
- package/dist/schemas/diagnostic-work-result.schema.json +296 -0
- package/dist/schemas/execution-bundle.schema.json +7 -1
- package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
- package/dist/schemas/memory-repair-work-result.schema.json +9 -2
- package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
- package/dist/schemas/release-impact.schema.json +3 -3
- package/dist/schemas/stage-start-response.schema.json +4 -0
- package/dist/services/accepted-intent.js +13 -1
- package/dist/services/canon.js +43 -7
- package/dist/services/check-environment.js +21 -0
- package/dist/services/check-runner-evidence.js +267 -15
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/cli-upgrade-allowlist.js +1 -1
- package/dist/services/code-checks.js +34 -19
- package/dist/services/compatibility-preflight.js +5 -2
- package/dist/services/controller-execution.js +10 -4
- package/dist/services/controller-fanout.js +34 -10
- package/dist/services/controller-memory-task.js +45 -0
- package/dist/services/controller-memory-worker.js +49 -0
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-settlement.js +5 -3
- package/dist/services/descriptor-run-start.js +46 -20
- package/dist/services/descriptor-stage-coordinator.js +66 -13
- package/dist/services/descriptor-stage-settlement.js +2 -1
- package/dist/services/descriptor-stage-start.js +40 -4
- package/dist/services/diagnostic-evidence.js +73 -0
- package/dist/services/diagnostic-report-publication.js +72 -0
- package/dist/services/diagnostic-report.js +67 -0
- package/dist/services/diagnostic-run-bundle.js +74 -0
- package/dist/services/diagnostic-snapshot.js +159 -0
- package/dist/services/diagnostic-stage-handlers.js +108 -0
- package/dist/services/diagnostic-stage-results.js +132 -0
- package/dist/services/diagnostic-start.js +84 -0
- package/dist/services/diagnostic-work-batch.js +34 -0
- package/dist/services/diagnostic-work-collection.js +97 -0
- package/dist/services/diagnostic-work-registration.js +101 -0
- package/dist/services/diagnostic-work-results.js +93 -0
- package/dist/services/engines.js +64 -14
- package/dist/services/eval-snapshots.js +186 -119
- package/dist/services/execution-bundles.js +19 -7
- package/dist/services/external-work-launch.js +5 -3
- package/dist/services/flow-catalog.js +13 -4
- package/dist/services/frozen-engine.js +148 -11
- package/dist/services/hook-memory-routing.js +188 -0
- package/dist/services/hook-recovery-guard.js +14 -0
- package/dist/services/hook-session-observation.js +24 -0
- package/dist/services/hooks.js +27 -8
- package/dist/services/hybrid-contracts.js +4 -13
- package/dist/services/hybrid-digest.js +14 -0
- package/dist/services/installed-flow-source.js +47 -4
- package/dist/services/lifecycle-invocations.js +179 -38
- package/dist/services/memory-accepted-composition.js +5 -4
- package/dist/services/memory-completed-correction-wave.js +23 -15
- package/dist/services/memory-compose-handler.js +163 -21
- package/dist/services/memory-composition-acceptance.js +186 -10
- package/dist/services/memory-composition-graph.js +42 -11
- package/dist/services/memory-composition-registration.js +23 -8
- package/dist/services/memory-composition-work-custody.js +14 -9
- package/dist/services/memory-composition-work-results.js +15 -3
- package/dist/services/memory-corrected-review-batch.js +10 -3
- package/dist/services/memory-corrected-review-findings.js +4 -2
- package/dist/services/memory-corrected-review-inputs.js +10 -5
- package/dist/services/memory-corrected-review-receipt.js +17 -9
- package/dist/services/memory-corrected-review-registration.js +3 -2
- package/dist/services/memory-correction-check-receipt.js +20 -4
- package/dist/services/memory-custody-snapshot.js +36 -0
- package/dist/services/memory-finish-evidence.js +44 -22
- package/dist/services/memory-finish-handler.js +37 -23
- package/dist/services/memory-finish-integration.js +218 -48
- package/dist/services/memory-finish-readiness.js +66 -19
- package/dist/services/memory-git-integration.js +87 -18
- package/dist/services/memory-historical-corrected-review.js +21 -15
- package/dist/services/memory-historical-original-review.js +22 -9
- package/dist/services/memory-historical-repair-registration.js +14 -8
- package/dist/services/memory-historical-second-review.js +30 -11
- package/dist/services/memory-init-contexts.js +44 -2
- package/dist/services/memory-init-controller-start.js +27 -8
- package/dist/services/memory-init-git-policy.js +50 -21
- package/dist/services/memory-init-lint-capability.js +24 -6
- package/dist/services/memory-init-native-capacity.js +17 -4
- package/dist/services/memory-init-preflight.js +21 -17
- package/dist/services/memory-init-request-candidate.js +73 -17
- package/dist/services/memory-init-request-input.js +23 -7
- package/dist/services/memory-init-run-bundle.js +49 -12
- package/dist/services/memory-init-storage.js +17 -7
- package/dist/services/memory-init-worktrunk-capability.js +3 -7
- package/dist/services/memory-layer.js +71 -6
- package/dist/services/memory-lint-config.js +111 -2
- package/dist/services/memory-native-review-copy.js +5 -67
- package/dist/services/memory-normal-flow-entry.js +12 -0
- package/dist/services/memory-ordinary-defaults.js +22 -5
- package/dist/services/memory-preflight-handler.js +15 -0
- package/dist/services/memory-repair-current-view.js +1 -1
- package/dist/services/memory-repair-document-delta.js +70 -33
- package/dist/services/memory-repair-history.js +12 -7
- package/dist/services/memory-repair-physical-binding.js +1 -119
- package/dist/services/memory-repair-publication-plan.js +5 -3
- package/dist/services/memory-repair-publication-source.js +1 -1
- package/dist/services/memory-repair-publication.js +3 -2
- package/dist/services/memory-repair-registration.js +2 -1
- package/dist/services/memory-repair-work-baseline.js +4 -2
- package/dist/services/memory-repair-work-completion.js +32 -12
- package/dist/services/memory-repair-work-results.js +21 -2
- package/dist/services/memory-repair-workspace.js +11 -2
- package/dist/services/memory-repair-writer-copy.js +2 -1
- package/dist/services/memory-result-binding.js +78 -12
- package/dist/services/memory-retained-review-authority.js +17 -8
- package/dist/services/memory-retained-second-baseline.js +1 -1
- package/dist/services/memory-retained-second-repairs.js +12 -6
- package/dist/services/memory-review-correction-plan.js +47 -12
- package/dist/services/memory-review-handler.js +17 -17
- package/dist/services/memory-review-inputs.js +55 -9
- package/dist/services/memory-review-limits.js +10 -0
- package/dist/services/memory-review-receipt.js +2 -2
- package/dist/services/memory-review-registration.js +13 -4
- package/dist/services/memory-review-work-custody.js +3 -1
- package/dist/services/memory-review-work-input.js +26 -7
- package/dist/services/memory-review-work-results.js +26 -4
- package/dist/services/memory-second-corrected-review-inputs.js +5 -3
- package/dist/services/memory-second-correction-plan.js +2 -1
- package/dist/services/memory-second-repair-launch.js +4 -4
- package/dist/services/memory-second-repair-source.js +1 -1
- package/dist/services/memory-stage-dispatch.js +38 -1
- package/dist/services/memory-stage-results.js +113 -22
- package/dist/services/memory-tool-probe.js +25 -0
- package/dist/services/memory-upgrade-analysis-handler.js +243 -0
- package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
- package/dist/services/memory-upgrade-evidence.js +100 -0
- package/dist/services/memory-upgrade-inventory.js +442 -0
- package/dist/services/memory-upgrade-publication-custody.js +73 -0
- package/dist/services/memory-upgrade-publication.js +413 -0
- package/dist/services/memory-upgrade-selection.js +430 -0
- package/dist/services/memory-upgrade-start.js +60 -0
- package/dist/services/memory-upgrade-storage-readiness.js +340 -0
- package/dist/services/memory-verification-acceptance.js +38 -17
- package/dist/services/memory-verification-checks.js +92 -27
- package/dist/services/memory-verification-receipt.js +6 -5
- package/dist/services/memory-verify-handler.js +15 -13
- package/dist/services/memory-work-result-json.js +12 -0
- package/dist/services/memory-work-result-preparation.js +5 -4
- package/dist/services/merge-server.js +4 -2
- package/dist/services/migrations.js +127 -98
- package/dist/services/native-daemon-history.js +2 -1
- package/dist/services/physical-startup.js +45 -0
- package/dist/services/project-registry.js +97 -0
- package/dist/services/project-summary.js +1 -1
- package/dist/services/projects.js +3 -93
- package/dist/services/prompt-delivery.js +71 -0
- package/dist/services/provider-capacity.js +112 -46
- package/dist/services/recovery-snapshot-database.js +32 -4
- package/dist/services/release-cache.js +8 -0
- package/dist/services/repair-intents.js +1 -12
- package/dist/services/repair-receipt.js +13 -0
- package/dist/services/run-control-worker.js +35 -18
- package/dist/services/run-control.js +2 -1
- package/dist/services/run-controller-adapter.js +2 -1
- package/dist/services/run-controller-process.js +4 -7
- package/dist/services/run-controller-recovery.js +8 -2
- package/dist/services/run-controller.js +215 -94
- package/dist/services/run-fork.js +1 -1
- package/dist/services/run-recovery-guard.js +30 -0
- package/dist/services/run-recovery.js +11 -44
- package/dist/services/run-start-custody.js +17 -5
- package/dist/services/runs.js +6 -1
- package/dist/services/runtime-scope-resume.js +12 -6
- package/dist/services/runtime-scope-stop.js +3 -2
- package/dist/services/runtime-scope-worker.js +46 -19
- package/dist/services/runtime-service.js +2 -1
- package/dist/services/sessions.js +3 -24
- package/dist/services/setup.js +63 -13
- package/dist/services/stage-handlers.js +22 -7
- package/dist/services/stage-pause.js +111 -44
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code.js +2 -2
- package/dist/services/vnext-fanout.js +51 -9
- package/dist/services/vnext-plan-review.js +3 -5
- package/dist/services/work-registry.js +117 -39
- package/dist/services/workspace-bootstrap.js +2 -1
- package/dist/services/workspace-files.js +1 -1
- package/dist/shared/entity-references.js +1 -8
- package/dist/storage/data-preservation.js +65 -0
- package/dist/storage/database.js +53 -19
- package/dist/storage/import-guard.js +84 -27
- package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
- package/dist/storage/paired-writer-migration.js +424 -0
- package/dist/storage/writer-contract.js +11 -4
- package/dist/storage/writer-migration.js +313 -52
- package/dist/storage/writer-restore.js +217 -0
- package/package.json +13 -6
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"work_id": { "type": "string", "minLength": 1, "maxLength": 256 },
|
|
10
10
|
"packet_sha256": { "$ref": "#/$defs/hash" },
|
|
11
11
|
"wave": { "type": "integer", "minimum": 1, "maximum": 2 },
|
|
12
|
-
"outcome": { "enum": ["accepted", "blocked"] },
|
|
12
|
+
"outcome": { "enum": ["accepted", "reconciled", "blocked"] },
|
|
13
13
|
"summary": { "type": "string", "minLength": 1, "maxLength": 16384 },
|
|
14
14
|
"changes": {
|
|
15
15
|
"type": "array", "maxItems": 512,
|
|
@@ -29,12 +29,19 @@
|
|
|
29
29
|
"properties": {
|
|
30
30
|
"finding_id": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
31
31
|
"summary": { "type": "string", "minLength": 1, "maxLength": 16384 },
|
|
32
|
-
"delta_paths": { "type": "array", "
|
|
32
|
+
"delta_paths": { "type": "array", "maxItems": 512, "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 4096 } },
|
|
33
33
|
"evidence": { "type": "array", "minItems": 1, "maxItems": 256, "items": { "$ref": "#/$defs/reference" } }
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
|
+
"allOf": [
|
|
39
|
+
{ "if": { "properties": { "outcome": { "const": "accepted" } } },
|
|
40
|
+
"then": { "properties": { "changes": { "type": "array", "minItems": 1 }, "resolutions": { "type": "array", "minItems": 1, "items": { "type": "object", "properties": { "delta_paths": { "type": "array", "minItems": 1 } } } } } } },
|
|
41
|
+
{ "if": { "properties": { "outcome": { "const": "reconciled" } } },
|
|
42
|
+
"then": { "properties": { "changes": { "type": "array", "maxItems": 0 }, "resolutions": { "type": "array", "minItems": 1,
|
|
43
|
+
"items": { "type": "object", "properties": { "delta_paths": { "type": "array", "maxItems": 0 } } } } } } }
|
|
44
|
+
],
|
|
38
45
|
"$defs": {
|
|
39
46
|
"hash": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
40
47
|
"reference": {
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
"required": ["schema_id", "from_version", "to_version", "minimum_upgrade_mode", "domains", "path_migration", "runtime_migration", "required_checks"],
|
|
8
8
|
"properties": {
|
|
9
9
|
"schema_id": {"const": "dd-flow/release-impact@1"},
|
|
10
|
-
"from_version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]
|
|
11
|
-
"to_version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]
|
|
10
|
+
"from_version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[A-Za-z0-9.-]+)?$"},
|
|
11
|
+
"to_version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[A-Za-z0-9.-]+)?$"},
|
|
12
12
|
"minimum_upgrade_mode": {"enum": ["patch", "targeted_minor", "full_migration"]},
|
|
13
|
-
"domains": {"type": "array", "items": {"enum": ["canonical_layer", "root_structure", "product", "system", "engineering", "operations", "adr", "plans", "scenarios", "ui", "guides", "protocol_evidence", "skills", "archive", "code_traceability"]}, "uniqueItems": true},
|
|
13
|
+
"domains": {"type": "array", "items": {"enum": ["canonical_layer", "flow_contracts", "root_structure", "product", "system", "engineering", "operations", "adr", "plans", "scenarios", "ui", "guides", "protocol_evidence", "skills", "archive", "code_traceability"]}, "uniqueItems": true},
|
|
14
14
|
"path_migration": {"type": "boolean"},
|
|
15
15
|
"runtime_migration": {"type": "boolean"},
|
|
16
16
|
"required_checks": {"type": "array", "items": {"type": "string"}, "minItems": 1},
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
"stage_root": {"type": "string", "minLength": 1},
|
|
20
20
|
"archive_path": {"type": ["string", "null"]},
|
|
21
21
|
"prompt_path": {"type": "string", "minLength": 1},
|
|
22
|
+
"prompt_delivery": {
|
|
23
|
+
"type": "object", "additionalProperties": false, "required": ["path", "sha256"],
|
|
24
|
+
"properties": {"path": {"type": "string", "minLength": 1}, "sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"}}
|
|
25
|
+
},
|
|
22
26
|
"aliases": {"type": "object", "additionalProperties": {"type": "string"}},
|
|
23
27
|
"resolved_context": {"type": "object", "additionalProperties": true},
|
|
24
28
|
"plan_ref": {"type": "string", "pattern": "^\\.memory-bank/protocol/[^/]+/plan\\.json$"},
|
|
@@ -15,12 +15,24 @@ export function acceptedIntentReviewFingerprint(sources) {
|
|
|
15
15
|
* clarification may use the existing coordinator correction, explicitly named
|
|
16
16
|
* as such; it never retroactively expands the independent review's coverage. */
|
|
17
17
|
export function validateAcceptedIntentReview(context, scope, reviewedSources, contextFingerprint, acknowledgement) {
|
|
18
|
+
// Keep file validation outside callers' SQL writer; the prepared validator
|
|
19
|
+
// below consumes only accepted bytes/ordered SQL events.
|
|
18
20
|
const reviewed = acceptedIntentReviewFingerprint(reviewedSources);
|
|
19
21
|
if (!reviewed)
|
|
20
|
-
return null;
|
|
22
|
+
return null;
|
|
21
23
|
if (contextFingerprint !== reviewed)
|
|
22
24
|
throw new AppError("accepted_intent_review_stale", "Review context does not bind the retained accepted inputs", 1, { stage: scope.stage });
|
|
23
25
|
const currentSources = prepareAcceptedIntentSources(context, { ...scope, stageDirectory: "intake", marked: true }).sources;
|
|
26
|
+
return validatePreparedAcceptedIntentReview(context, scope, reviewedSources, contextFingerprint, currentSources, acknowledgement);
|
|
27
|
+
}
|
|
28
|
+
export function validatePreparedAcceptedIntentReview(context, scope, reviewedSources, contextFingerprint, currentSources, acknowledgement) {
|
|
29
|
+
const reviewed = acceptedIntentReviewFingerprint(reviewedSources);
|
|
30
|
+
if (!reviewed)
|
|
31
|
+
return null; // Historical source contract has no new authority.
|
|
32
|
+
if (contextFingerprint !== reviewed)
|
|
33
|
+
throw new AppError("accepted_intent_review_stale", "Review context does not bind the retained accepted inputs", 1, { stage: scope.stage });
|
|
34
|
+
if (currentSources.length > 256)
|
|
35
|
+
throw new AppError("accepted_intent_source_invalid", "Accepted intent source count exceeds its bound", 1);
|
|
24
36
|
const current = acceptedIntentReviewFingerprint(currentSources);
|
|
25
37
|
const reviewedEvent = Math.max(0, ...(reviewedSources ?? []).filter(source => source.role.startsWith("accepted_hitl:")).map(source => Number(source.role.split(":")[1])));
|
|
26
38
|
const rows = context.db.all("SELECT id, payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_resumed' AND json_extract(payload_json, '$.run_id') = ? AND id > ? ORDER BY id LIMIT 256", [scope.projectId, scope.runId, reviewedEvent]);
|
package/dist/services/canon.js
CHANGED
|
@@ -3,6 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
5
|
import { prepareError } from "../cli/input-preparation.js";
|
|
6
|
+
import { resolveDdFlowHome } from "../storage/paths.js";
|
|
7
|
+
import { readSetupStatus } from "./setup.js";
|
|
6
8
|
const canonRootKey = "canon.root";
|
|
7
9
|
export function prepareCanonRegistration(context, input) {
|
|
8
10
|
if (typeof input.root !== "string" || !input.root.trim())
|
|
@@ -41,16 +43,21 @@ export function getCanonStatus(context, input = {}) {
|
|
|
41
43
|
export function resolveCanonRoot(context, input = {}) {
|
|
42
44
|
return resolveCanonCandidates(canonCandidates(context, input));
|
|
43
45
|
}
|
|
44
|
-
/** Read only: resolve
|
|
46
|
+
/** Read only: resolve canonical data without opening SQLite or running migrations. */
|
|
45
47
|
export function resolveCanonRootBeforeContext(env, input = {}) {
|
|
46
48
|
const candidates = [];
|
|
47
49
|
if (input.explicitRoot)
|
|
48
50
|
candidates.push(validateCanonCandidate("explicit", input.explicitRoot));
|
|
49
51
|
else if (env.DD_MEMORYBANK)
|
|
50
52
|
candidates.push(validateCanonCandidate("env", env.DD_MEMORYBANK));
|
|
51
|
-
else
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
else {
|
|
54
|
+
const installed = installedCanonCandidate(resolveDdFlowHome(env));
|
|
55
|
+
if (installed)
|
|
56
|
+
candidates.push(installed);
|
|
57
|
+
else
|
|
58
|
+
for (const known of knownCanonRoots())
|
|
59
|
+
candidates.push(validateCanonCandidate("known", known));
|
|
60
|
+
}
|
|
54
61
|
return resolveCanonCandidates(uniqueBy(candidates, (candidate) => `${candidate.source}:${candidate.root}`));
|
|
55
62
|
}
|
|
56
63
|
function resolveCanonCandidates(candidates) {
|
|
@@ -67,7 +74,7 @@ function resolveCanonCandidates(candidates) {
|
|
|
67
74
|
const memorybankRoot = candidate.memorybank_root ?? root;
|
|
68
75
|
const mbbRoot = candidate.mbb_root ?? path.join(memorybankRoot, "mbb");
|
|
69
76
|
const layout = candidate.layout ?? "legacy_root";
|
|
70
|
-
const metadata = readCanonMetadata(root, flowRoot);
|
|
77
|
+
const metadata = candidate.metadata ?? readCanonMetadata(root, flowRoot);
|
|
71
78
|
return {
|
|
72
79
|
ok: true,
|
|
73
80
|
canon: {
|
|
@@ -114,6 +121,30 @@ function resolveCanonCandidates(candidates) {
|
|
|
114
121
|
]
|
|
115
122
|
};
|
|
116
123
|
}
|
|
124
|
+
/** Archive provenance belongs to the selected release, not an ancestor Git
|
|
125
|
+
* checkout. A damaged installation is not permission to select nearby code. */
|
|
126
|
+
function installedCanonCandidate(home) {
|
|
127
|
+
if (!fs.existsSync(path.join(home, "installation.json")))
|
|
128
|
+
return null;
|
|
129
|
+
const status = readSetupStatus(home), installation = status.installation;
|
|
130
|
+
const invalid = (root, reason) => ({
|
|
131
|
+
source: "installed", root, realpath: null, valid: false, blockers: [reason]
|
|
132
|
+
});
|
|
133
|
+
if (!installation || status.installation_readiness !== "installed")
|
|
134
|
+
return invalid(home, "Installed canonical release is degraded; reconcile setup before selecting its canon.");
|
|
135
|
+
const roots = fs.readdirSync(installation.canon_root);
|
|
136
|
+
if (roots.length !== 1)
|
|
137
|
+
return invalid(installation.canon_root, "Installed canonical archive has no unique root.");
|
|
138
|
+
const candidate = validateCanonCandidate("installed", path.join(installation.canon_root, roots[0]));
|
|
139
|
+
if (!candidate.valid)
|
|
140
|
+
return candidate;
|
|
141
|
+
const version = readTrimmed(path.join(candidate.root, "VERSION"));
|
|
142
|
+
const flowContract = readFlowContractId(candidate.flow_root);
|
|
143
|
+
if (version !== installation.manifest.canon.version || !flowContract)
|
|
144
|
+
return invalid(candidate.root, "Installed canonical metadata differs from its release selection.");
|
|
145
|
+
return { ...candidate, metadata: { version, commit: installation.manifest.canon.commit,
|
|
146
|
+
flow_contract: flowContract, status: "present", diagnostics: [] } };
|
|
147
|
+
}
|
|
117
148
|
function canonCandidates(context, input) {
|
|
118
149
|
const candidates = [];
|
|
119
150
|
if (input.explicitRoot) {
|
|
@@ -129,8 +160,13 @@ function canonCandidates(context, input) {
|
|
|
129
160
|
candidates.push(validateCanonCandidate("registered", registered.root));
|
|
130
161
|
}
|
|
131
162
|
}
|
|
132
|
-
|
|
133
|
-
|
|
163
|
+
if (!candidates.length) {
|
|
164
|
+
const installed = installedCanonCandidate(context.ddFlowHome);
|
|
165
|
+
if (installed)
|
|
166
|
+
return [installed];
|
|
167
|
+
for (const known of knownCanonRoots()) {
|
|
168
|
+
candidates.push(validateCanonCandidate("known", known));
|
|
169
|
+
}
|
|
134
170
|
}
|
|
135
171
|
return uniqueBy(candidates, (candidate) => `${candidate.source}:${candidate.root}`);
|
|
136
172
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
export function validateCheckEnvironment(value) {
|
|
4
|
+
if (value === undefined)
|
|
5
|
+
return undefined;
|
|
6
|
+
const entries = value && typeof value === "object" && !Array.isArray(value) ? Object.entries(value) : [];
|
|
7
|
+
if (!entries.length || entries.some(([key, selected]) => !["CODEX_HOME", "PLAYWRIGHT_BROWSERS_PATH"].includes(key)
|
|
8
|
+
|| typeof selected !== "string" || !selected || selected.length > 4096 || selected.includes("\0") || !path.isAbsolute(selected)))
|
|
9
|
+
throw new AppError("invalid_check_environment", "Owned checks require absolute known resource paths", 2, { effect: "no_effect" });
|
|
10
|
+
return { ...value };
|
|
11
|
+
}
|
|
12
|
+
export function retainedBrowserEnvironment(environment) {
|
|
13
|
+
return environment.PLAYWRIGHT_BROWSERS_PATH === undefined ? undefined
|
|
14
|
+
: validateCheckEnvironment({ PLAYWRIGHT_BROWSERS_PATH: environment.PLAYWRIGHT_BROWSERS_PATH });
|
|
15
|
+
}
|
|
16
|
+
export function ownedCheckEnvironment(environment, requested) {
|
|
17
|
+
const selected = validateCheckEnvironment(requested), retained = retainedBrowserEnvironment(environment);
|
|
18
|
+
if (retained?.PLAYWRIGHT_BROWSERS_PATH && selected?.PLAYWRIGHT_BROWSERS_PATH && retained.PLAYWRIGHT_BROWSERS_PATH !== selected.PLAYWRIGHT_BROWSERS_PATH)
|
|
19
|
+
throw new AppError("invalid_check_environment", "Owned browser resource cannot be rebound", 2, { effect: "no_effect" });
|
|
20
|
+
return retained || selected ? { ...retained, ...selected } : undefined;
|
|
21
|
+
}
|
|
@@ -5,14 +5,24 @@ import path from "node:path";
|
|
|
5
5
|
import { parse } from "shell-quote";
|
|
6
6
|
import { AppError } from "../shared/errors.js";
|
|
7
7
|
import { validateObservationDuration } from "../harness-runtime/lib/observation-clock.mjs";
|
|
8
|
+
import { isDeepStrictEqual } from "node:util";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
8
10
|
function object(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); }
|
|
9
11
|
function boundedString(value, bytes) { return typeof value === "string" && Buffer.byteLength(value) <= bytes; }
|
|
10
12
|
const validatedBrowserProofs = new WeakSet();
|
|
13
|
+
const unsupportedObservationContracts = new WeakSet();
|
|
14
|
+
const validatedNativeProofs = new WeakSet();
|
|
15
|
+
const validatedChildren = new WeakMap();
|
|
16
|
+
const validatedReports = new WeakMap();
|
|
17
|
+
const hash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
18
|
+
const absoluteFile = (value) => typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.includes("\0") && path.isAbsolute(value);
|
|
19
|
+
export function candidateAbsencesValid(value) { return Array.isArray(value) && value.length <= 64 && value.every(item => object(item) && absoluteFile(item.file) && item.path_state === "missing" && object(item.declaration) && absoluteFile(item.declaration.file) && hash(item.declaration.sha256)); }
|
|
11
20
|
function relativeProofPath(value) { return typeof value === "string" && value.length > 0 && Buffer.byteLength(value) <= 4096 && !path.isAbsolute(value) && !value.includes("\\") && value.split("/").every(part => part !== "" && part !== "." && part !== ".."); }
|
|
12
21
|
function proofArtifact(value) { return object(value) && relativeProofPath(value.relative_path) && typeof value.sha256 === "string" && /^[a-f0-9]{64}$/.test(value.sha256); }
|
|
13
22
|
function browserProofShape(proof) {
|
|
14
23
|
const attempt = proof.attempt, step = proof.step;
|
|
15
|
-
return proof.
|
|
24
|
+
return (proof.observation_contract === undefined || proof.observation_contract === "dd-tasks/browser-observation@2" && object(proof.target) && [proof.target.page_id, proof.target.context_id].every(value => boundedString(value, 256) && Boolean(value)) && Number.isSafeInteger(proof.window_epoch) && Number(proof.window_epoch) > 0)
|
|
25
|
+
&& proof.native_code === "PLAYWRIGHT_ACTION_INACTIVITY" && object(attempt) && boundedString(attempt.invocation_id, 4096) && boundedString(attempt.test_id, 4096) && Boolean(attempt.test_id)
|
|
16
26
|
&& [attempt.retry, attempt.repeat_each_index, attempt.worker_index].every(value => Number.isSafeInteger(value) && value >= 0)
|
|
17
27
|
&& object(step) && boundedString(step.id, 4096) && Boolean(step.id) && ["pw:api", "expect"].includes(String(step.category)) && step.file === proof.file && Number.isSafeInteger(step.line) && step.line > 0
|
|
18
28
|
&& proofArtifact(proof.worker_snapshot) && proofArtifact(proof.native_result);
|
|
@@ -29,6 +39,21 @@ export function runnerFailureValid(value) {
|
|
|
29
39
|
return false;
|
|
30
40
|
if (value.repair_evidence !== undefined) {
|
|
31
41
|
const proof = value.repair_evidence;
|
|
42
|
+
if (object(proof) && proof.rule === "candidate_test_runtime_error")
|
|
43
|
+
return ["TypeError", "ReferenceError"].includes(String(proof.exception_type))
|
|
44
|
+
&& absoluteFile(proof.file) && hash(proof.file_sha256) && ["test", "setup", "collection"].includes(String(proof.phase))
|
|
45
|
+
&& [proof.line, proof.column].every(value => Number.isSafeInteger(value) && Number(value) > 0) && proofArtifact(proof.native_artifact);
|
|
46
|
+
if (object(proof) && proof.rule === "candidate_migration_sql")
|
|
47
|
+
return proof.native_code === "42601" && absoluteFile(proof.file) && hash(proof.file_sha256)
|
|
48
|
+
&& boundedString(proof.operation_id, 4096) && Boolean(proof.operation_id) && proofArtifact(proof.native_artifact) && object(proof.world)
|
|
49
|
+
&& boundedString(proof.world.invocation_id, 4096) && typeof proof.world.world_id === "string" && /^[a-f0-9]{32}$/.test(proof.world.world_id)
|
|
50
|
+
&& boundedString(proof.world.role, 128) && Boolean(proof.world.role) && hash(proof.world.migrations_sha256) && proof.world.phase === "migrating" && proof.world.setup_completed === false;
|
|
51
|
+
if (object(proof) && proof.rule === "candidate_tool_diagnostic")
|
|
52
|
+
return absoluteFile(proof.file) && proofArtifact(proof.native_artifact)
|
|
53
|
+
&& object(proof.tool) && { biome: "2.5.6", typescript: "5.9.3", docs: "1", "value-absence": "1" }[String(proof.tool.name)] === proof.tool.version
|
|
54
|
+
&& object(proof.step) && boundedString(proof.step.id, 256) && Boolean(proof.step.id) && Number.isSafeInteger(proof.step.ordinal) && proof.step.ordinal >= 0 && hash(proof.step.command_sha256)
|
|
55
|
+
&& boundedString(proof.diagnostic_code, 256) && (proof.tool.name === "biome" ? ["format", "assist/source/organizeImports"].includes(String(proof.diagnostic_code)) || /^lint\/[\w/-]+$/.test(String(proof.diagnostic_code)) : proof.tool.name === "typescript" ? /^TS\d+$/.test(String(proof.diagnostic_code)) : proof.tool.name === "docs" ? ["required_path_missing", "docs_contract"].includes(String(proof.diagnostic_code)) : proof.diagnostic_code === "value_absence")
|
|
56
|
+
&& (proof.path_state === "present" && hash(proof.file_sha256) || proof.path_state === "missing" && proof.tool.name === "docs" && proof.diagnostic_code === "required_path_missing" && proof.file_sha256 === null && object(proof.declaration) && absoluteFile(proof.declaration.file) && hash(proof.declaration.sha256));
|
|
32
57
|
if (!object(proof) || !boundedString(proof.rule, 128) || !boundedString(proof.native_code, 128) || !boundedString(proof.file, 4096) || !object(proof.world))
|
|
33
58
|
return false;
|
|
34
59
|
const world = proof.world;
|
|
@@ -45,23 +70,132 @@ function candidateFile(root, file) {
|
|
|
45
70
|
if (!root || !path.isAbsolute(file))
|
|
46
71
|
return false;
|
|
47
72
|
try {
|
|
48
|
-
const
|
|
49
|
-
|
|
73
|
+
const realRoot = fs.realpathSync(root), relative = path.relative(realRoot, fs.realpathSync(file));
|
|
74
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
75
|
+
return false;
|
|
76
|
+
let current = file;
|
|
77
|
+
while (fs.realpathSync(current) !== realRoot) {
|
|
78
|
+
if (fs.lstatSync(current).isSymbolicLink())
|
|
79
|
+
return false;
|
|
80
|
+
const parent = path.dirname(current);
|
|
81
|
+
if (parent === current)
|
|
82
|
+
return false;
|
|
83
|
+
current = parent;
|
|
84
|
+
}
|
|
85
|
+
return fs.statSync(file).isFile();
|
|
50
86
|
}
|
|
51
87
|
catch {
|
|
52
88
|
return false;
|
|
53
89
|
}
|
|
54
90
|
}
|
|
55
91
|
export function candidateFailureFiles(evidence, root) {
|
|
56
|
-
return [...new Set((evidence?.failures ?? []).flatMap(failure =>
|
|
92
|
+
return [...new Set((evidence?.failures ?? []).flatMap(failure => {
|
|
93
|
+
const child = validatedChildren.get(failure);
|
|
94
|
+
if (child)
|
|
95
|
+
return candidateFailureFiles({ ...child.evidence, failures: [child.failure] }, root);
|
|
96
|
+
const proof = failure.repair_evidence;
|
|
97
|
+
const checksum = proof && "file_sha256" in proof ? proof.file_sha256 : undefined;
|
|
98
|
+
try {
|
|
99
|
+
return failure.file && candidateFile(root, failure.file) && (checksum === undefined || checksum === crypto.createHash("sha256").update(fs.readFileSync(failure.file)).digest("hex")) ? [failure.file] : [];
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}))];
|
|
105
|
+
}
|
|
106
|
+
function absentCandidate(root, file) {
|
|
107
|
+
if (!root || !absoluteFile(file))
|
|
108
|
+
return false;
|
|
109
|
+
try {
|
|
110
|
+
let current = path.resolve(file);
|
|
111
|
+
try {
|
|
112
|
+
fs.lstatSync(current);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error.code !== "ENOENT")
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
for (;;) {
|
|
120
|
+
try {
|
|
121
|
+
if (fs.lstatSync(current).isSymbolicLink())
|
|
122
|
+
return false;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error.code !== "ENOENT")
|
|
127
|
+
return false;
|
|
128
|
+
const parent = path.dirname(current);
|
|
129
|
+
if (parent === current)
|
|
130
|
+
return false;
|
|
131
|
+
current = parent;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const realRoot = fs.realpathSync(root), relative = path.relative(realRoot, fs.realpathSync(current));
|
|
135
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
136
|
+
return false;
|
|
137
|
+
while (fs.realpathSync(current) !== realRoot) {
|
|
138
|
+
if (fs.lstatSync(current).isSymbolicLink())
|
|
139
|
+
return false;
|
|
140
|
+
current = path.dirname(current);
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
57
147
|
}
|
|
58
|
-
function
|
|
59
|
-
|
|
148
|
+
export function candidateFailureAbsences(evidence, root) {
|
|
149
|
+
return (evidence?.failures ?? []).flatMap(failure => {
|
|
150
|
+
const child = validatedChildren.get(failure);
|
|
151
|
+
if (child)
|
|
152
|
+
return candidateFailureAbsences({ ...child.evidence, failures: [child.failure] }, root);
|
|
153
|
+
try {
|
|
154
|
+
const proof = failure.repair_evidence;
|
|
155
|
+
if (proof?.rule !== "candidate_tool_diagnostic" || proof.path_state !== "missing" || !proof.declaration || !validatedNativeProofs.has(proof)
|
|
156
|
+
|| !absentCandidate(root, proof.file) || !candidateFile(root, proof.declaration.file) || crypto.createHash("sha256").update(fs.readFileSync(proof.declaration.file)).digest("hex") !== proof.declaration.sha256)
|
|
157
|
+
return [];
|
|
158
|
+
return [{ file: proof.file, declaration: proof.declaration, path_state: "missing" }];
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function candidateFailure(failure, evidence, root, capturedFiles, capturedAbsences) {
|
|
166
|
+
const child = validatedChildren.get(failure);
|
|
167
|
+
if (child)
|
|
168
|
+
return candidateFailure(child.failure, child.evidence, root, capturedFiles, capturedAbsences);
|
|
169
|
+
const owned = (file) => {
|
|
170
|
+
if (capturedFiles !== undefined)
|
|
171
|
+
return capturedFiles.includes(file);
|
|
172
|
+
if (!candidateFile(root, file))
|
|
173
|
+
return false;
|
|
174
|
+
const proof = failure.repair_evidence;
|
|
175
|
+
try {
|
|
176
|
+
return !proof || !("file_sha256" in proof) || proof.file_sha256 === crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
60
182
|
if (failure.origin === "tooling" || failure.origin === "unknown")
|
|
61
183
|
return false;
|
|
62
184
|
if (failure.category === "assertion")
|
|
63
185
|
return failure.origin === undefined || failure.origin === "candidate" && failure.file !== undefined && owned(failure.file);
|
|
64
186
|
const proof = failure.repair_evidence;
|
|
187
|
+
if (proof?.rule === "candidate_test_runtime_error")
|
|
188
|
+
return validatedNativeProofs.has(proof) && failure.category === "unclassified" && failure.origin === "candidate"
|
|
189
|
+
&& failure.phase === proof.phase && failure.native_code === proof.exception_type && failure.file === proof.file && owned(proof.file);
|
|
190
|
+
if (proof?.rule === "candidate_migration_sql" || proof?.rule === "candidate_tool_diagnostic") {
|
|
191
|
+
if (!validatedNativeProofs.has(proof) || failure.category !== "unclassified" || failure.origin !== "candidate" || failure.file !== proof.file || !["setup", "collection", "test"].includes(failure.phase ?? ""))
|
|
192
|
+
return false;
|
|
193
|
+
if (proof.rule === "candidate_migration_sql")
|
|
194
|
+
return failure.phase === "setup" && failure.native_code === "42601" && proof.world.invocation_id === evidence.binding.invocation_id && owned(proof.file);
|
|
195
|
+
if (failure.phase !== "collection" || failure.native_code !== proof.diagnostic_code)
|
|
196
|
+
return false;
|
|
197
|
+
return proof.path_state === "missing" ? Boolean(proof.declaration && (capturedAbsences ?? (root ? candidateFailureAbsences(evidence, root) : [])).some(item => item.file === proof.file && isDeepStrictEqual(item.declaration, proof.declaration))) : owned(proof.file);
|
|
198
|
+
}
|
|
65
199
|
if (proof?.rule === "candidate_browser_action_inactivity")
|
|
66
200
|
return validatedBrowserProofs.has(proof) && failure.category === "unclassified" && failure.phase === "test" && failure.origin === "candidate" && failure.native_code === proof.native_code && failure.file === proof.file && failure.test_id === proof.attempt.test_id && proof.attempt.invocation_id === evidence.binding.invocation_id && owned(proof.file);
|
|
67
201
|
return failure.category === "unclassified" && failure.origin === "candidate" && ["test", "setup"].includes(failure.phase ?? "") && proof?.rule === "candidate_sql_integrity" && ["23502", "23503", "23505", "23514"].includes(proof.native_code) && failure.native_code === proof.native_code && failure.file === proof.file && proof.world.invocation_id === evidence.binding.invocation_id && owned(proof.file);
|
|
@@ -107,7 +241,7 @@ export function readRunnerProgress(file, invocationId, lastSequence = 0, now = D
|
|
|
107
241
|
if (!value || typeof value !== "object" || Array.isArray(value) || value.schema_id !== "dd-flow/check-runner-progress@1" || value.invocation_id !== invocationId
|
|
108
242
|
|| !Number.isSafeInteger(value.sequence) || value.sequence <= 0 || typeof value.at !== "string" || !Number.isFinite(Date.parse(value.at))
|
|
109
243
|
|| !["startup", "execution"].includes(value.phase)
|
|
110
|
-
|| !["setup_step_completed", "run_started", "module_collected", "test_started", "test_completed", "step_completed", "run_completed"].includes(value.event))
|
|
244
|
+
|| !["setup_step_completed", "run_started", "module_collected", "test_started", "test_completed", "step_started", "step_completed", "run_completed"].includes(value.event))
|
|
111
245
|
throw new Error("Malformed official progress record");
|
|
112
246
|
if (value.sequence <= lastSequence || Date.parse(value.at) > now)
|
|
113
247
|
continue;
|
|
@@ -164,6 +298,9 @@ function readProofArtifact(root, ref, limit = 256 * 1024) {
|
|
|
164
298
|
function matches(actual, expected) { return object(actual) && Object.entries(expected).every(([key, value]) => actual[key] === value); }
|
|
165
299
|
function validateBrowserArtifacts(root, proof, binding) {
|
|
166
300
|
const snapshot = readProofArtifact(root, proof.worker_snapshot), native = readProofArtifact(root, proof.native_result);
|
|
301
|
+
if (proof.observation_contract !== undefined && (proof.observation_contract !== "dd-tasks/browser-observation@2" || snapshot.observation_contract !== proof.observation_contract || native.observation_contract !== proof.observation_contract
|
|
302
|
+
|| !isDeepStrictEqual(snapshot.target, proof.target) || !isDeepStrictEqual(native.target, proof.target) || snapshot.window_epoch !== proof.window_epoch || native.window_epoch !== proof.window_epoch))
|
|
303
|
+
return false;
|
|
167
304
|
if (!Array.isArray(native.errors) || native.errors.length < 1 || native.errors.length > 64 || native.errors.some(error => !boundedString(error, 4096) || !/^(?:Error: )?(?:[A-Za-z][\w.]*: )?Test timeout of [1-9]\d*ms exceeded\.(?:\nCall log:[\s\S]*)?$/.test(stripVTControlCharacters(String(error)))))
|
|
168
305
|
return false;
|
|
169
306
|
if (!matches(snapshot.binding, binding) || !matches(native.binding, binding) || !matches(snapshot.attempt, proof.attempt) || !matches(native.attempt, proof.attempt)
|
|
@@ -175,7 +312,13 @@ function validateBrowserArtifacts(root, proof, binding) {
|
|
|
175
312
|
return Number.isSafeInteger(quiet) && quiet > 0 && [last, deadline, observed, health, ended].every(Number.isFinite) && Math.abs(deadline - last - quiet) <= 2 && observed >= last && observed <= deadline && health <= observed && health >= last && deadline - health <= Math.min(1000, quiet / 4) && ended >= deadline && ended <= Date.now() && ended - deadline <= 5000;
|
|
176
313
|
}
|
|
177
314
|
export function runnerProofArtifacts(evidence) {
|
|
178
|
-
return [...new Set((evidence?.failures ?? []).flatMap(failure => {
|
|
315
|
+
return [...new Set([...(evidence?.failures ?? []).flatMap(failure => {
|
|
316
|
+
const child = validatedChildren.get(failure);
|
|
317
|
+
if (child && failure.child_evidence)
|
|
318
|
+
return [failure.child_evidence.report.relative_path, ...runnerProofArtifacts(child.evidence).map(file => path.posix.join(path.posix.dirname(failure.child_evidence.report.relative_path), file))];
|
|
319
|
+
const proof = failure.repair_evidence;
|
|
320
|
+
return [...(failure.diagnostic_artifact ? [failure.diagnostic_artifact.relative_path] : []), ...(proof?.rule === "candidate_browser_action_inactivity" ? [proof.worker_snapshot.relative_path, proof.native_result.relative_path] : proof && "native_artifact" in proof ? [proof.native_artifact.relative_path] : [])];
|
|
321
|
+
}), ...(evidence ? validatedReports.get(evidence) ?? [] : []).flatMap(child => [child.report.relative_path, ...runnerProofArtifacts(child.evidence).map(file => path.posix.join(path.posix.dirname(child.report.relative_path), file))])])];
|
|
179
322
|
}
|
|
180
323
|
export function runnerContractsValid(value) {
|
|
181
324
|
if (value === undefined)
|
|
@@ -186,13 +329,15 @@ export function runnerContractsValid(value) {
|
|
|
186
329
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
187
330
|
return false;
|
|
188
331
|
const contract = raw;
|
|
189
|
-
return alias.startsWith("@check/") &&
|
|
332
|
+
return alias.startsWith("@check/") && ["dd-flow/check-runner-evidence@1", "dd-flow/check-runner-evidence@2"].includes(String(contract.schema_id)) && typeof contract.path === "string" && contract.path.length > 0 && contract.path.split("/")[0] !== "engine-outcome.json" && !contract.path.includes("\\") && !path.isAbsolute(contract.path) && !contract.path.split("/").some(part => part === ".." || part === "" || part === ".");
|
|
190
333
|
});
|
|
191
334
|
}
|
|
192
|
-
export function readRunnerEvidence(root, contract, binding) {
|
|
335
|
+
export function readRunnerEvidence(root, contract, binding, depth = 0, budget = { reports: 0 }) {
|
|
193
336
|
try {
|
|
337
|
+
if (depth > 4 || ++budget.reports > 64)
|
|
338
|
+
return null;
|
|
194
339
|
let current = root;
|
|
195
|
-
if (!fs.lstatSync(current).isDirectory())
|
|
340
|
+
if (!fs.lstatSync(current).isDirectory() || fs.lstatSync(current).isSymbolicLink())
|
|
196
341
|
return null;
|
|
197
342
|
for (const part of contract.path.split(/[\\/]/)) {
|
|
198
343
|
current = path.join(current, part);
|
|
@@ -200,34 +345,141 @@ export function readRunnerEvidence(root, contract, binding) {
|
|
|
200
345
|
return null;
|
|
201
346
|
}
|
|
202
347
|
const evidence = JSON.parse(readBoundedProofFile(current, 1024 * 1024).toString("utf8"));
|
|
348
|
+
if (!["dd-flow/check-runner-evidence@1", "dd-flow/check-runner-evidence@2"].includes(contract.schema_id))
|
|
349
|
+
return null;
|
|
350
|
+
const partial = evidence.failure_projection;
|
|
351
|
+
if (partial !== undefined && (evidence.schema_id !== "dd-flow/check-runner-evidence@2" || !object(partial) || partial.state !== "partial" || !["count_bound", "byte_bound", "interrupt"].includes(partial.reason)
|
|
352
|
+
|| ![partial.represented, partial.omitted].every(value => Number.isSafeInteger(value) && value >= 0)))
|
|
353
|
+
return null;
|
|
203
354
|
if (evidence.schema_id !== contract.schema_id || !evidence.binding || Object.entries(binding).some(([key, value]) => evidence.binding[key] !== value)
|
|
204
355
|
|| typeof evidence.runner?.name !== "string" || !evidence.runner.name || typeof evidence.runner.version !== "string" || !evidence.runner.version || typeof evidence.completed !== "boolean" || !evidence.counts
|
|
205
356
|
|| ![evidence.counts.passed, evidence.counts.failed, evidence.counts.skipped].every(n => Number.isSafeInteger(n) && n >= 0)
|
|
206
357
|
|| !Array.isArray(evidence.failures) || evidence.failures.length > 64 || evidence.failures.some(f => !runnerFailureValid(f))
|
|
207
|
-
|| evidence.counts.failed > evidence.failures.length || evidence.observation_unknown !== undefined && typeof evidence.observation_unknown !== "boolean")
|
|
358
|
+
|| !partial && evidence.counts.failed > evidence.failures.length || evidence.observation_unknown !== undefined && typeof evidence.observation_unknown !== "boolean")
|
|
208
359
|
return null;
|
|
360
|
+
const children = new Map();
|
|
361
|
+
const consumed = new Map();
|
|
362
|
+
if (evidence.steps !== undefined) {
|
|
363
|
+
if (!Array.isArray(evidence.steps) || evidence.steps.length > 64)
|
|
364
|
+
return null;
|
|
365
|
+
const ids = new Set(), invocations = new Set();
|
|
366
|
+
let stopped = false;
|
|
367
|
+
for (const [ordinal, step] of evidence.steps.entries()) {
|
|
368
|
+
if (!step || !boundedString(step.id, 256) || !step.id || ids.has(step.id) || step.ordinal !== ordinal || !hash(step.command_sha256) || !["passed", "failed", "unreached"].includes(step.state))
|
|
369
|
+
return null;
|
|
370
|
+
ids.add(step.id);
|
|
371
|
+
if (step.state === "unreached") {
|
|
372
|
+
if (!stopped || step.binding !== undefined || step.report !== undefined || step.exit_code !== undefined)
|
|
373
|
+
return null;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (stopped)
|
|
377
|
+
return null;
|
|
378
|
+
if (step.state === "failed")
|
|
379
|
+
stopped = true;
|
|
380
|
+
if (!step.binding || !proofArtifact(step.report) || !Number.isSafeInteger(step.exit_code) || step.state === "passed" && step.exit_code !== 0 || step.state === "failed" && step.exit_code === 0 || invocations.has(step.binding.invocation_id) || step.binding.invocation_id === binding.invocation_id)
|
|
381
|
+
return null;
|
|
382
|
+
if (step.binding.invocation_id !== `${binding.invocation_id}:step:${step.id}` || step.binding.check_id !== `${binding.check_id}/${step.id}` || step.binding.verification_epoch !== binding.verification_epoch || step.binding.input_hash !== crypto.createHash("sha256").update(JSON.stringify([binding.input_hash, step.command_sha256])).digest("hex"))
|
|
383
|
+
return null;
|
|
384
|
+
if (crypto.createHash("sha256").update(step.binding.command).digest("hex") !== step.command_sha256)
|
|
385
|
+
return null;
|
|
386
|
+
invocations.add(step.binding.invocation_id);
|
|
387
|
+
readProofArtifact(root, step.report, 1024 * 1024);
|
|
388
|
+
const child = readRunnerEvidence(path.join(root, path.dirname(step.report.relative_path)), { schema_id: contract.schema_id, path: path.basename(step.report.relative_path) }, step.binding, depth + 1, budget);
|
|
389
|
+
if (!child || step.state === "passed" && (child.counts.failed !== 0 || child.failures.length !== 0) || step.state === "failed" && !child.failures.length && child.completed && !child.observation_unknown)
|
|
390
|
+
return null;
|
|
391
|
+
if (!child.completed || child.observation_unknown)
|
|
392
|
+
stopped = true;
|
|
393
|
+
children.set(step.id, child);
|
|
394
|
+
}
|
|
395
|
+
for (const key of ["passed", "failed", "skipped"])
|
|
396
|
+
if (evidence.counts[key] !== [...children.values()].reduce((total, child) => total + child.counts[key], 0))
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
209
399
|
for (const failure of evidence.failures) {
|
|
400
|
+
if (failure.child_evidence) {
|
|
401
|
+
const ref = failure.child_evidence, step = evidence.steps?.find(item => item.id === ref.step_id), child = children.get(ref.step_id);
|
|
402
|
+
if (!step || !child || !isDeepStrictEqual(ref.binding, step.binding) || !isDeepStrictEqual(ref.report, step.report))
|
|
403
|
+
return null;
|
|
404
|
+
const original = { ...failure };
|
|
405
|
+
delete original.child_evidence;
|
|
406
|
+
const used = consumed.get(child) ?? new Set();
|
|
407
|
+
consumed.set(child, used);
|
|
408
|
+
const index = child.failures.findIndex((item, index) => { const native = { ...item }; delete native.child_evidence; return !used.has(index) && isDeepStrictEqual(native, original); });
|
|
409
|
+
if (index < 0)
|
|
410
|
+
return null;
|
|
411
|
+
used.add(index);
|
|
412
|
+
const matched = child.failures[index];
|
|
413
|
+
if (!matched.child_evidence && matched.repair_evidence?.rule === "candidate_tool_diagnostic" && !isDeepStrictEqual(matched.repair_evidence.step, { id: step.id, ordinal: step.ordinal, command_sha256: step.command_sha256 }))
|
|
414
|
+
return null;
|
|
415
|
+
validatedChildren.set(failure, { failure: matched, evidence: child });
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
210
418
|
if (failure.diagnostic_artifact && !matches(readProofArtifact(root, failure.diagnostic_artifact, 2 * 1024 * 1024).binding, binding))
|
|
211
419
|
return null;
|
|
212
420
|
const proof = failure.repair_evidence;
|
|
421
|
+
if (proof?.rule === "candidate_migration_sql" || proof?.rule === "candidate_tool_diagnostic" || proof?.rule === "candidate_test_runtime_error") {
|
|
422
|
+
const native = readProofArtifact(root, proof.native_artifact, 2 * 1024 * 1024), facts = Object.fromEntries(Object.entries(proof).filter(([key]) => key !== "native_artifact"));
|
|
423
|
+
if (!isDeepStrictEqual(native.binding, binding) || Object.entries(facts).some(([key, value]) => !isDeepStrictEqual(native[key], value)))
|
|
424
|
+
return null;
|
|
425
|
+
if (proof.rule === "candidate_test_runtime_error") {
|
|
426
|
+
if (evidence.schema_id !== "dd-flow/check-runner-evidence@2" || native.executing_file !== proof.file || native.exception_name !== proof.exception_type
|
|
427
|
+
|| !isDeepStrictEqual(native.top_frame, { file: proof.file, line: proof.line, column: proof.column }) || !boundedString(native.stack, 16384) || !String(native.stack).startsWith(`${proof.exception_type}:`))
|
|
428
|
+
return null;
|
|
429
|
+
const firstFrame = String(native.stack).split("\n").find(line => /^\s*at\s/.test(line));
|
|
430
|
+
const frame = firstFrame?.match(/^\s*at\s+(?:.*?\()?((?:file:\/\/)?\/[^\n]+):(\d+):(\d+)\)?$/);
|
|
431
|
+
if (!frame || (frame[1].startsWith("file://") ? fileURLToPath(frame[1]) : frame[1]) !== proof.file || Number(frame[2]) !== proof.line || Number(frame[3]) !== proof.column)
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
validatedNativeProofs.add(proof);
|
|
435
|
+
}
|
|
213
436
|
if (proof?.rule === "candidate_browser_action_inactivity") {
|
|
214
|
-
if (
|
|
437
|
+
if (proof.observation_contract === undefined && evidence.schema_id === "dd-flow/check-runner-evidence@1") {
|
|
438
|
+
// Legacy diagnostics remain readable; they do not prove the new window/target guarantee.
|
|
439
|
+
if (!validateBrowserArtifacts(root, proof, binding))
|
|
440
|
+
return null;
|
|
441
|
+
evidence.completed = false;
|
|
442
|
+
unsupportedObservationContracts.add(evidence);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (proof.observation_contract !== "dd-tasks/browser-observation@2" || !validateBrowserArtifacts(root, proof, binding))
|
|
215
446
|
return null;
|
|
216
447
|
validatedBrowserProofs.add(proof);
|
|
217
448
|
}
|
|
218
449
|
}
|
|
450
|
+
const represented = [...consumed.values()].reduce((total, used) => total + used.size, 0), childFailures = [...children.values()].reduce((total, child) => total + child.failures.length, 0);
|
|
451
|
+
if (partial) {
|
|
452
|
+
if (children.size ? partial.represented !== represented || partial.omitted !== childFailures - represented : partial.represented !== evidence.failures.filter(failure => failure.code !== "report_truncated").length)
|
|
453
|
+
return null;
|
|
454
|
+
evidence.completed = false;
|
|
455
|
+
}
|
|
456
|
+
else if (represented !== childFailures)
|
|
457
|
+
return null;
|
|
458
|
+
// Valid but incomplete native observation is diagnostic evidence, not a
|
|
459
|
+
// malformed schema. Preserve the primary failure and references through
|
|
460
|
+
// every composite, including a parent that incorrectly claims completion.
|
|
461
|
+
if ([...children.values()].some(child => !child.completed || child.observation_unknown)) {
|
|
462
|
+
evidence.completed = false;
|
|
463
|
+
if ([...children.values()].some(child => unsupportedObservationContracts.has(child)))
|
|
464
|
+
unsupportedObservationContracts.add(evidence);
|
|
465
|
+
if (evidence.schema_id === "dd-flow/check-runner-evidence@1" && [...children.values()].some(child => !child.completed && !unsupportedObservationContracts.has(child)) || [...children.values()].some(child => child.observation_unknown))
|
|
466
|
+
evidence.observation_unknown = true;
|
|
467
|
+
}
|
|
468
|
+
validatedReports.set(evidence, (evidence.steps ?? []).flatMap(step => step.report && children.has(step.id) ? [{ report: step.report, evidence: children.get(step.id) }] : []));
|
|
219
469
|
return evidence;
|
|
220
470
|
}
|
|
221
471
|
catch {
|
|
222
472
|
return null;
|
|
223
473
|
}
|
|
224
474
|
}
|
|
225
|
-
export function runnerEvidencePass(evidence) { return Boolean(evidence?.completed && !evidence.observation_unknown && evidence.counts.failed === 0 && evidence.failures.length === 0); }
|
|
475
|
+
export function runnerEvidencePass(evidence) { return Boolean(evidence?.completed && !evidence.failure_projection && !evidence.observation_unknown && evidence.counts.failed === 0 && evidence.failures.length === 0); }
|
|
226
476
|
export function checkRepairDisposition(receipt) {
|
|
227
477
|
if (receipt.fingerprint_scope != null && receipt.fingerprint_scope !== "check_inputs")
|
|
228
478
|
return "tooling_blocker";
|
|
229
479
|
if (receipt.status === "running" || receipt.status === "aborted" || receipt.runner_evidence?.observation_unknown)
|
|
230
480
|
return "settlement_required";
|
|
481
|
+
if (receipt.runner_evidence && unsupportedObservationContracts.has(receipt.runner_evidence) || receipt.runner_evidence?.failure_projection || receipt.runner_evidence?.schema_id === "dd-flow/check-runner-evidence@2" && !receipt.runner_evidence.completed)
|
|
482
|
+
return "tooling_blocker";
|
|
231
483
|
if (receipt.status === "passed")
|
|
232
484
|
return "passed";
|
|
233
485
|
if (receipt.execution_failures?.length || receipt.mutation_paths?.length || receipt.before_fingerprint !== receipt.after_fingerprint || receipt.diagnostic_evidence_valid === false)
|
|
@@ -241,7 +493,7 @@ export function checkRepairDisposition(receipt) {
|
|
|
241
493
|
return "tooling_blocker";
|
|
242
494
|
if (!evidence.completed)
|
|
243
495
|
return "unclassified";
|
|
244
|
-
if (evidence.failures.some(f => !runnerFailureValid(f) || !candidateFailure(f, evidence, receipt.candidate_root, receipt.candidate_files)))
|
|
496
|
+
if (evidence.failures.some(f => !runnerFailureValid(f) || !candidateFailure(f, evidence, receipt.candidate_root, receipt.candidate_files, receipt.candidate_absences)))
|
|
245
497
|
return "unclassified";
|
|
246
498
|
return "source_repair";
|
|
247
499
|
}
|
|
@@ -32,7 +32,7 @@ export function isMbUpgradeCliOperation(args, env = process.env) {
|
|
|
32
32
|
export function requiresExplicitUpgradeAuthorization(args) {
|
|
33
33
|
const positional = positionalArgs(args);
|
|
34
34
|
const [family, command] = positional;
|
|
35
|
-
if (family === "migration" && command
|
|
35
|
+
if (family === "migration" && ["apply", "readiness"].includes(command ?? ""))
|
|
36
36
|
return true;
|
|
37
37
|
if (family === "run" && command === "start" && optionValue(args, "flow-kind") === "mb-upgrade")
|
|
38
38
|
return true;
|