@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.156
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 +31 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +33 -3
- package/dist/cli/command-metadata.js +73 -2
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +23 -7
- package/dist/cli/diagnostic.js +14 -3
- package/dist/cli/help.js +117 -1
- package/dist/cli/hook-ingress.js +36 -49
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init.js +5 -4
- package/dist/cli/native-hook-ingress.js +5 -1
- package/dist/cli/native-hook-preparation.js +31 -4
- package/dist/cli/run-cli.js +274 -19
- package/dist/cli.js +17 -3
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
- package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
- package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
- package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +115 -17
- package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
- package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +15 -5
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
- package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
- package/dist/harness-runtime/lib/native-streams.mjs +3 -10
- package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
- package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
- package/dist/harness-runtime/lib/process-json.mjs +4 -5
- package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +6 -2
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +1 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/check-runner-evidence.js +45 -8
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +46 -29
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-child-repair.js +95 -0
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +19 -9
- package/dist/services/controller-memory-worker.js +6 -4
- package/dist/services/controller-result-authority.js +122 -0
- package/dist/services/dashboard.js +4 -2
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +3 -1
- package/dist/services/descriptor-run-start.js +24 -4
- package/dist/services/descriptor-stage-coordinator.js +78 -10
- package/dist/services/descriptor-stage-settlement.js +59 -14
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report.js +30 -0
- package/dist/services/diagnostic-run-bundle.js +48 -9
- package/dist/services/diagnostic-snapshot.js +2 -2
- package/dist/services/diagnostic-stage-handlers.js +3 -2
- package/dist/services/diagnostic-stage-results.js +26 -0
- package/dist/services/diagnostic-start.js +56 -6
- package/dist/services/diagnostic-work-collection.js +3 -3
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/eval-snapshots.js +17 -7
- package/dist/services/external-work-launch.js +8 -2
- package/dist/services/flow-catalog.js +24 -0
- package/dist/services/frozen-engine.js +19 -2
- package/dist/services/generic-execution.js +4 -3
- package/dist/services/harness-adapter.js +2 -14
- package/dist/services/hook-memory-routing.js +1 -1
- package/dist/services/hooks.js +31 -6
- package/dist/services/hybrid-contracts.js +6 -1
- package/dist/services/installed-flow-source.js +37 -2
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lifecycle-invocations.js +250 -33
- package/dist/services/managed-daemon-binding.js +52 -1
- package/dist/services/memory-compose-handler.js +2 -22
- package/dist/services/memory-finish-integration.js +58 -17
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-init-contexts.js +55 -5
- package/dist/services/memory-init-controller-start.js +20 -4
- package/dist/services/memory-layer.js +7 -3
- package/dist/services/memory-native-review-copy.js +27 -3
- package/dist/services/memory-repair-document-delta.js +1 -0
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/repair-intents.js +2 -7
- package/dist/services/retained-native-hook.js +274 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-worker.js +42 -1
- package/dist/services/run-control.js +40 -5
- package/dist/services/run-controller-adapter.js +37 -3
- package/dist/services/run-controller-process.js +25 -4
- package/dist/services/run-controller-recovery.js +46 -6
- package/dist/services/run-controller-state.js +3 -2
- package/dist/services/run-controller.js +257 -44
- package/dist/services/run-recovery.js +31 -2
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +18 -31
- package/dist/services/sessions.js +2 -1
- package/dist/services/stage-handlers.js +55 -3
- package/dist/services/stage-lifecycle.js +4 -3
- package/dist/services/stage-pause.js +26 -9
- package/dist/services/stage-report-renderer.js +2 -6
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/verification-matrix.js +1 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +74 -5
- package/dist/services/vnext-code.js +8 -32
- package/dist/services/vnext-fanout.js +53 -8
- package/dist/services/vnext-merge.js +10 -5
- package/dist/services/vnext-plan-review.js +1 -1
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/dist/services/vnext-specify.js +10 -5
- package/dist/services/work-registry.js +255 -69
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +3 -2
- package/tools/verification-jobs.mjs +35 -10
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
6
|
+
import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
7
|
+
import { readInteractiveStageScope } from "./interactive-stage-entry.js";
|
|
8
|
+
import { readAcceptedInteractiveStage } from "./interactive-stage-handlers.js";
|
|
9
|
+
import { readCompletedInteractiveWork } from "./interactive-work-completion.js";
|
|
10
|
+
import { PreparedInteractiveWorkspaceChanges, assertInteractiveUserBasis } from "./interactive-mutation-scope.js";
|
|
11
|
+
import { readInteractiveWorkGit } from "./interactive-work-start.js";
|
|
12
|
+
import { assertReviewedProjectGitInventory } from "./project-git-integration.js";
|
|
13
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
14
|
+
function refuse(reason) {
|
|
15
|
+
throw new AppError("interactive_finish_integration_invalid", "Integration requires the accepted interactive task, review and exact original Git inputs", 2, { reason, effect: "no_effect" });
|
|
16
|
+
}
|
|
17
|
+
/** Frozen SQL/bundle policy, never ambient branch inference or Git authority. */
|
|
18
|
+
export function readInteractiveGitPolicy(context, scope, bundle) {
|
|
19
|
+
if (bundle.manifest.flow_id !== "interactive")
|
|
20
|
+
refuse("interactive_bundle_required");
|
|
21
|
+
const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle), logical = "runtime/git-policy.json";
|
|
22
|
+
const references = bundle.manifest.inventory.filter(ref => ref.path === logical), bytes = bundle.blob(logical);
|
|
23
|
+
if (references.length !== 1 || !bytes.length || bytes.length > 32768 || references[0].sha256 !== hash(bytes))
|
|
24
|
+
refuse("frozen_git_policy_required");
|
|
25
|
+
let value;
|
|
26
|
+
try {
|
|
27
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return refuse("invalid_git_policy");
|
|
31
|
+
}
|
|
32
|
+
const frozen = bundle.execution.snapshot().scope;
|
|
33
|
+
if (value?.schema_id !== "dd-flow/interactive-git-policy@1" || Object.keys(value).sort().join() !== "git_executable,git_sha256,policy,schema_id,scope"
|
|
34
|
+
|| Object.keys(value.scope ?? {}).sort().join() !== "projectRoot,requestId,workspaceRoot"
|
|
35
|
+
|| value.scope?.projectRoot !== run.project_root || value.scope?.workspaceRoot !== run.workspace_root
|
|
36
|
+
|| value.scope.projectRoot !== frozen.projectRoot || value.scope.workspaceRoot !== frozen.workspace || value.scope.requestId !== frozen.requestId
|
|
37
|
+
|| typeof value.git_executable !== "string" || !path.isAbsolute(value.git_executable) || !/^[a-f0-9]{64}$/.test(value.git_sha256)
|
|
38
|
+
|| value.policy?.source !== "explicit_request" || value.policy.push_authorized !== false
|
|
39
|
+
|| !["feature_worktree", "integration_branch_direct"].includes(value.policy.route)
|
|
40
|
+
|| typeof value.policy.repository !== "string" || !path.isAbsolute(value.policy.repository)
|
|
41
|
+
|| !value.policy.workspace_branch || !value.policy.integration_branch
|
|
42
|
+
|| value.policy.initial_head !== null && !/^[a-f0-9]{40,64}$/.test(value.policy.initial_head))
|
|
43
|
+
refuse("git_policy_scope_changed");
|
|
44
|
+
return structuredClone(value);
|
|
45
|
+
}
|
|
46
|
+
/** Read-only early inspection; no accepted review, Git write or inclusion
|
|
47
|
+
* approval is required. Resolve any user decision before freezing review. */
|
|
48
|
+
export function observeInteractiveOriginalGitInputs(context, input) {
|
|
49
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
50
|
+
refuse("physical_observation_outside_writer_required");
|
|
51
|
+
const selected = readInteractiveStageScope(context, input), policy = readInteractiveGitPolicy(context, selected.scope, selected.bundle);
|
|
52
|
+
const git = readInteractiveWorkGit(selected.bundle);
|
|
53
|
+
if (git.git_executable !== policy.git_executable || git.git_sha256 !== policy.git_sha256)
|
|
54
|
+
refuse("selected_git_changed");
|
|
55
|
+
const baseline = selected.selected.baseline.snapshot();
|
|
56
|
+
if (policy.policy.repository !== baseline.repository || policy.policy.workspace_branch !== baseline.branch
|
|
57
|
+
|| policy.policy.initial_head !== baseline.head)
|
|
58
|
+
refuse("reviewed_git_baseline_changed");
|
|
59
|
+
const changes = PreparedInteractiveWorkspaceChanges.prepare(context, selected.scope, selected.bundle, {
|
|
60
|
+
projectRoot: policy.scope.projectRoot, workspace: policy.scope.workspaceRoot, gitExecutable: git.git_executable, env: context.env
|
|
61
|
+
});
|
|
62
|
+
const originalInputs = readOriginalGitInputs(context, policy, selected.selected.active_paths, selected.selected.before_versions);
|
|
63
|
+
changes.revalidate();
|
|
64
|
+
selected.assertCustody(context);
|
|
65
|
+
if (hybridDigest(readInteractiveGitPolicy(context, selected.scope, selected.bundle)) !== hybridDigest(policy))
|
|
66
|
+
refuse("git_policy_scope_changed");
|
|
67
|
+
return { baseline_sha256: selected.selected.baseline.sha256, user_inputs_sha256: selected.selected.inputs.sha256,
|
|
68
|
+
scope_revision: selected.selected.revision, original_inputs: originalInputs };
|
|
69
|
+
}
|
|
70
|
+
function readOriginalGitInputs(context, policy, paths, beforeVersions) {
|
|
71
|
+
if (paths.length > 4096)
|
|
72
|
+
refuse("reviewed_inventory_exceeds_bound");
|
|
73
|
+
const before = new Map(beforeVersions.map(item => [item.path, item]));
|
|
74
|
+
const env = { ...Object.fromEntries(Object.entries(context.env).filter(([key]) => !key.startsWith("GIT_"))), GIT_OPTIONAL_LOCKS: "0", GIT_LITERAL_PATHSPECS: "1" };
|
|
75
|
+
const git = (args) => {
|
|
76
|
+
const result = spawnSync(policy.git_executable, ["-c", "core.fsmonitor=false", "-C", policy.scope.workspaceRoot, ...args], { env, timeout: 10000, maxBuffer: 16 * 1024 * 1024, shell: false, windowsHide: true });
|
|
77
|
+
if (result.error || result.signal || result.status !== 0)
|
|
78
|
+
refuse("original_git_inputs_unavailable");
|
|
79
|
+
return result.stdout;
|
|
80
|
+
};
|
|
81
|
+
let total = 0;
|
|
82
|
+
const version = (header, retain) => {
|
|
83
|
+
const match = /^([0-7]{6}) (?:blob )?([a-f0-9]{40,64})(?: 0)?$/.exec(header);
|
|
84
|
+
if (!match || !["100644", "100755", "120000"].includes(match[1]))
|
|
85
|
+
refuse("regular_original_git_input_required");
|
|
86
|
+
const bytes = git(["cat-file", "blob", match[2]]);
|
|
87
|
+
total += bytes.length;
|
|
88
|
+
if (total > 64 * 1024 * 1024)
|
|
89
|
+
refuse("original_git_inputs_exceed_bound");
|
|
90
|
+
return { sha256: hash(bytes), git_mode: match[1], ...(retain ? { base64: bytes.toString("base64") } : {}) };
|
|
91
|
+
};
|
|
92
|
+
const entries = (bytes, retain) => {
|
|
93
|
+
let text;
|
|
94
|
+
try {
|
|
95
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return refuse("original_git_paths_invalid");
|
|
99
|
+
}
|
|
100
|
+
const values = new Map();
|
|
101
|
+
for (const entry of text.split("\0").filter(Boolean)) {
|
|
102
|
+
const tab = entry.indexOf("\t"), logical = entry.slice(tab + 1);
|
|
103
|
+
if (tab < 0 || !paths.includes(logical) || values.has(logical))
|
|
104
|
+
refuse("original_git_paths_invalid");
|
|
105
|
+
values.set(logical, version(entry.slice(0, tab), retain));
|
|
106
|
+
}
|
|
107
|
+
return values;
|
|
108
|
+
};
|
|
109
|
+
const heads = entries(paths.length && policy.policy.initial_head ? git(["ls-tree", "-z", policy.policy.initial_head, "--", ...paths]) : Buffer.alloc(0), false);
|
|
110
|
+
const indexes = entries(paths.length ? git(["ls-files", "--stage", "-z", "--", ...paths]) : Buffer.alloc(0), true);
|
|
111
|
+
const same = (left, right) => left?.sha256 === right?.sha256 && left?.git_mode === right?.git_mode;
|
|
112
|
+
return paths.map(logical => {
|
|
113
|
+
const workspace = before.get(logical);
|
|
114
|
+
if (!workspace)
|
|
115
|
+
refuse("working_change_before_version_missing");
|
|
116
|
+
const head = heads.get(logical) ?? null, index = indexes.get(logical) ?? null;
|
|
117
|
+
const source = workspace.kind === "file" ? { sha256: workspace.sha256, git_mode: workspace.mode & 0o111 ? "100755" : "100644" } : null;
|
|
118
|
+
return { path: logical, workspace: structuredClone(workspace), head, index,
|
|
119
|
+
preexisting_changes: !same(source, head) || !same(index, head) };
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/** Select the exact accepted review and its entry-relative file delta from
|
|
123
|
+
* retained SQL. Works after Git effects: no fresh workspace/HEAD is used to
|
|
124
|
+
* manufacture a replacement reviewed inventory. Dispatch still needs original
|
|
125
|
+
* dirty-input admission, a retained operation intent and physical revalidation. */
|
|
126
|
+
export function prepareInteractiveReviewedGitInputs(context, input) {
|
|
127
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
128
|
+
refuse("preparation_outside_writer_required");
|
|
129
|
+
const selected = readInteractiveStageScope(context, { ...input, stage: "integration" }), { scope, bundle } = selected;
|
|
130
|
+
const review = readAcceptedInteractiveStage(context, { ...input, stage: "review" });
|
|
131
|
+
const read = (writer) => {
|
|
132
|
+
const policy = readInteractiveGitPolicy(writer, scope, bundle), state = selected.selected;
|
|
133
|
+
const result = review.result, collection = review.proof.collection;
|
|
134
|
+
if (!collection || result.details.review?.verdict !== "accept" || state.needs_scope_refresh
|
|
135
|
+
|| result.baseline_sha256 !== state.baseline.sha256 || result.scope_revision !== state.revision
|
|
136
|
+
|| result.user_inputs_sha256 !== state.inputs.sha256 || !state.inputs.finish_requested)
|
|
137
|
+
refuse("current_accepted_review_required");
|
|
138
|
+
const work = readCompletedInteractiveWork(writer, { ...scope, workId: collection.work_id }, bundle), packet = work.start.assignment.packet;
|
|
139
|
+
if (packet.stage !== "review" || packet.owner_stage !== "review" || work.result.verdict !== "ready" || work.receipt.changes.length
|
|
140
|
+
|| packet.input_sha256 !== result.input_sha256 || packet.baseline_sha256 !== result.baseline_sha256
|
|
141
|
+
|| packet.scope_revision !== result.scope_revision || packet.user_inputs_sha256 !== result.user_inputs_sha256
|
|
142
|
+
|| packet.file_changes_sha256 !== result.file_changes_sha256)
|
|
143
|
+
refuse("accepted_readonly_critic_required");
|
|
144
|
+
const baseline = state.baseline.snapshot(), delta = state.baseline.changes(work.start.baseline);
|
|
145
|
+
if (delta.head_changed || delta.index_changed || hybridDigest(delta.changes) !== result.file_changes_sha256
|
|
146
|
+
|| work.receipt.observed_snapshot_sha256 !== work.start.baseline.sha256
|
|
147
|
+
|| policy.policy.repository !== baseline.repository || policy.policy.workspace_branch !== baseline.branch
|
|
148
|
+
|| policy.policy.initial_head !== baseline.head)
|
|
149
|
+
refuse("reviewed_git_baseline_changed");
|
|
150
|
+
if (delta.changes.length > 4096)
|
|
151
|
+
refuse("reviewed_inventory_exceeds_bound");
|
|
152
|
+
const originals = new Map(state.before_versions.map(item => [item.path, item]));
|
|
153
|
+
const allowed = new Set(state.active_paths);
|
|
154
|
+
const references = [], removals = [];
|
|
155
|
+
for (const change of delta.changes) {
|
|
156
|
+
const before = originals.get(change.path);
|
|
157
|
+
if (!allowed.has(change.path) || !before
|
|
158
|
+
|| change.before && (change.before.kind !== "file" || before.sha256 !== change.before.sha256 || before.mode !== change.before.mode)
|
|
159
|
+
|| !change.before && before.kind !== "missing" || change.after && change.after.kind !== "file")
|
|
160
|
+
refuse("reviewed_file_scope_changed");
|
|
161
|
+
const entry = change.after ?? change.before;
|
|
162
|
+
if (!entry?.sha256 || entry.mode === null)
|
|
163
|
+
refuse("reviewed_regular_file_required");
|
|
164
|
+
(change.after ? references : removals).push({ root: "workspace", path: change.path, sha256: entry.sha256,
|
|
165
|
+
git_mode: entry.mode & 0o111 ? "100755" : "100644" });
|
|
166
|
+
}
|
|
167
|
+
// An honest empty patch is a terminal no-op, not permission to merge a
|
|
168
|
+
// pre-existing feature branch or commit unrelated staged/dirty files.
|
|
169
|
+
if (delta.changes.length)
|
|
170
|
+
assertReviewedProjectGitInventory(references, removals);
|
|
171
|
+
return { policy, review_sha256: review.proof.result_sha256, review_result: review.reference, review_work_id: collection.work_id,
|
|
172
|
+
reviewed_snapshot_sha256: work.receipt.observed_snapshot_sha256, reviewed_sources_sha256: hybridDigest(work.start.baseline.snapshot().entries), baseline_sha256: state.baseline.sha256,
|
|
173
|
+
user_inputs_sha256: state.inputs.sha256, scope_revision: state.revision, file_changes_sha256: result.file_changes_sha256,
|
|
174
|
+
changes: delta.changes, inventory: { references, removals }, empty: delta.changes.length === 0 };
|
|
175
|
+
};
|
|
176
|
+
const original = context.db.readSnapshot(() => read(context));
|
|
177
|
+
const assertCustody = (writer) => {
|
|
178
|
+
selected.assertCustody(writer);
|
|
179
|
+
review.assertCustody(writer);
|
|
180
|
+
if (hybridDigest(read(writer)) !== hybridDigest(original))
|
|
181
|
+
refuse("accepted_integration_custody_changed");
|
|
182
|
+
};
|
|
183
|
+
const observeBeforeDispatch = () => {
|
|
184
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
185
|
+
refuse("physical_observation_outside_writer_required");
|
|
186
|
+
const git = readInteractiveWorkGit(bundle);
|
|
187
|
+
if (git.git_executable !== original.policy.git_executable || git.git_sha256 !== original.policy.git_sha256)
|
|
188
|
+
refuse("selected_git_changed");
|
|
189
|
+
const changes = PreparedInteractiveWorkspaceChanges.prepare(context, scope, bundle, { projectRoot: original.policy.scope.projectRoot,
|
|
190
|
+
workspace: original.policy.scope.workspaceRoot, gitExecutable: git.git_executable, env: context.env });
|
|
191
|
+
if (hash(Buffer.from(JSON.stringify(changes.currentSnapshot()))) !== original.reviewed_snapshot_sha256)
|
|
192
|
+
refuse("reviewed_sources_changed");
|
|
193
|
+
assertCustody(context);
|
|
194
|
+
return changes;
|
|
195
|
+
};
|
|
196
|
+
const assertReviewedSources = () => {
|
|
197
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
198
|
+
refuse("physical_observation_outside_writer_required");
|
|
199
|
+
// Admit the owned HEAD/index effects, not a different working source tree.
|
|
200
|
+
// An unselected dependency changing after checks cannot inherit readiness.
|
|
201
|
+
const observed = selected.selected.baseline.observeChanges({ projectRoot: original.policy.scope.projectRoot,
|
|
202
|
+
workspace: original.policy.scope.workspaceRoot, gitExecutable: original.policy.git_executable, env: context.env }, selected.selected.active_paths);
|
|
203
|
+
if (hybridDigest(observed.current.snapshot().entries) !== original.reviewed_sources_sha256)
|
|
204
|
+
refuse("reviewed_source_footprint_changed");
|
|
205
|
+
assertCustody(context);
|
|
206
|
+
};
|
|
207
|
+
const observeOriginalInputs = () => {
|
|
208
|
+
const changes = observeBeforeDispatch(), policy = original.policy;
|
|
209
|
+
const inputs = readOriginalGitInputs(context, policy, original.changes.map(change => change.path), selected.selected.before_versions);
|
|
210
|
+
changes.revalidate();
|
|
211
|
+
assertCustody(context);
|
|
212
|
+
return inputs;
|
|
213
|
+
};
|
|
214
|
+
/** Semantic interpretation stays with the agent. The CLI verifies that a
|
|
215
|
+
* declared inclusion decision cites genuine delivered user input and covers
|
|
216
|
+
* exactly the pre-existing selected changes. Finish alone is not approval.
|
|
217
|
+
* This value must be retained with the future Git operation's intent before
|
|
218
|
+
* effects; calling this helper itself performs no acceptance or Git writes. */
|
|
219
|
+
const prepareOriginalInputsAgreement = (agreement) => {
|
|
220
|
+
const originals = observeOriginalInputs(), paths = originals.filter(item => item.preexisting_changes).map(item => item.path).sort();
|
|
221
|
+
if (!paths.length) {
|
|
222
|
+
if (agreement)
|
|
223
|
+
throw new AppError("interactive_finish_integration_invalid", "Selected files have no pre-RUN changes; retry integration without --result-file", 2, { reason: "unexpected_existing_input_agreement", phase: "prepare", effect: "no_effect", recoverable: true });
|
|
224
|
+
return { original_inputs: originals, agreement: null };
|
|
225
|
+
}
|
|
226
|
+
if (!agreement)
|
|
227
|
+
throw new AppError("interactive_git_agreement_required", "Selected files contain pre-RUN changes; preserve them or explicitly agree their inclusion before commit", 2, { reason: "preexisting_selected_changes", effect: "no_effect", recoverable: true, paths, continuation: { kind: "existing_input_decision_required" } });
|
|
228
|
+
if (typeof agreement.reason !== "string" || !agreement.reason.trim() || agreement.reason.length > 64 * 1024
|
|
229
|
+
|| hybridDigest(agreement.include_existing_paths) !== hybridDigest(paths))
|
|
230
|
+
refuse("exact_existing_input_agreement_required");
|
|
231
|
+
assertInteractiveUserBasis(agreement.basis, selected.selected.inputs);
|
|
232
|
+
assertCustody(context);
|
|
233
|
+
return { original_inputs: originals, agreement: { ...structuredClone(agreement), user_inputs_sha256: original.user_inputs_sha256 } };
|
|
234
|
+
};
|
|
235
|
+
return { original: structuredClone(original), assertCustody, observeBeforeDispatch, assertReviewedSources, observeOriginalInputs, prepareOriginalInputsAgreement };
|
|
236
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { AppError } from "../shared/errors.js";
|
|
6
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
7
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
8
|
+
function refuse(reason) {
|
|
9
|
+
throw new AppError("interactive_git_baseline_invalid", "Interactive needs its actual project/workspace Git baseline", 2, { reason, effect: "no_effect" });
|
|
10
|
+
}
|
|
11
|
+
function logical(value) {
|
|
12
|
+
return typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.includes("\\") && !value.includes("\0")
|
|
13
|
+
&& !path.isAbsolute(value) && value.split("/").every(part => part && part !== "." && part !== ".." && part !== ".git");
|
|
14
|
+
}
|
|
15
|
+
/** Hash regular files incrementally: large assets do not require a second full
|
|
16
|
+
* in-memory copy. Do not follow symlinks, read FIFOs or invoke Git clean filters. */
|
|
17
|
+
function entry(root, name) {
|
|
18
|
+
const file = path.join(root, name);
|
|
19
|
+
let parent = root;
|
|
20
|
+
for (const part of name.split("/").slice(0, -1)) {
|
|
21
|
+
parent = path.join(parent, part);
|
|
22
|
+
try {
|
|
23
|
+
if (!fs.lstatSync(parent).isDirectory())
|
|
24
|
+
return { path: name, kind: "missing", mode: null, sha256: null };
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.code === "ENOENT")
|
|
28
|
+
return { path: name, kind: "missing", mode: null, sha256: null };
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
let stat;
|
|
33
|
+
try {
|
|
34
|
+
stat = fs.lstatSync(file);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.code === "ENOENT")
|
|
38
|
+
return { path: name, kind: "missing", mode: null, sha256: null };
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
const mode = stat.mode & 0o7777;
|
|
42
|
+
if (stat.isSymbolicLink())
|
|
43
|
+
return { path: name, kind: "link", mode, sha256: hash(fs.readlinkSync(file, { encoding: "buffer" })) };
|
|
44
|
+
// Gitlinks/submodules are opaque. Their nested working tree is not included
|
|
45
|
+
// in this parent repository's git-visible scope or a writable-file delta.
|
|
46
|
+
if (stat.isDirectory())
|
|
47
|
+
return { path: name, kind: "directory", mode, sha256: null };
|
|
48
|
+
if (!stat.isFile())
|
|
49
|
+
refuse("special_file");
|
|
50
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
51
|
+
try {
|
|
52
|
+
const before = fs.fstatSync(fd, { bigint: true }), digest = createHash("sha256"), buffer = Buffer.allocUnsafe(256 * 1024);
|
|
53
|
+
if (before.dev !== BigInt(stat.dev) || before.ino !== BigInt(stat.ino) || !before.isFile())
|
|
54
|
+
refuse("file_changed_during_capture");
|
|
55
|
+
let length = 0n;
|
|
56
|
+
for (;;) {
|
|
57
|
+
const count = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
58
|
+
if (!count)
|
|
59
|
+
break;
|
|
60
|
+
digest.update(buffer.subarray(0, count));
|
|
61
|
+
length += BigInt(count);
|
|
62
|
+
}
|
|
63
|
+
const after = fs.fstatSync(fd, { bigint: true });
|
|
64
|
+
if (length !== before.size || after.size !== before.size || after.mtimeNs !== before.mtimeNs || after.ctimeNs !== before.ctimeNs)
|
|
65
|
+
refuse("file_changed_during_capture");
|
|
66
|
+
return { path: name, kind: "file", mode, sha256: digest.digest("hex") };
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
fs.closeSync(fd);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function stamp(root, name) {
|
|
73
|
+
try {
|
|
74
|
+
let parent = root;
|
|
75
|
+
for (const part of name.split("/").slice(0, -1)) {
|
|
76
|
+
parent = path.join(parent, part);
|
|
77
|
+
if (!fs.lstatSync(parent).isDirectory())
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const value = fs.lstatSync(path.join(root, name), { bigint: true });
|
|
81
|
+
return [value.dev, value.ino, value.mode, value.size, value.mtimeNs, value.ctimeNs].map(String);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? ""))
|
|
85
|
+
return null;
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function observe(input, retainedPaths = []) {
|
|
90
|
+
if (!path.isAbsolute(input.gitExecutable))
|
|
91
|
+
refuse("absolute_git_executable_required");
|
|
92
|
+
for (const root of [input.projectRoot, input.workspace]) {
|
|
93
|
+
if (!path.isAbsolute(root) || fs.realpathSync(root) !== root || !fs.lstatSync(root).isDirectory())
|
|
94
|
+
refuse("canonical_workspace_required");
|
|
95
|
+
}
|
|
96
|
+
const env = { ...Object.fromEntries(Object.entries(input.env).filter(([key]) => !key.startsWith("GIT_"))), GIT_OPTIONAL_LOCKS: "0" };
|
|
97
|
+
const git = (root, args, absent = false) => {
|
|
98
|
+
const result = spawnSync(input.gitExecutable, ["-c", "core.fsmonitor=false", "-C", root, ...args], { env, timeout: 15000, maxBuffer: 32 * 1024 * 1024 });
|
|
99
|
+
if (result.error || result.signal || result.status !== 0 && !(absent && [1, 128].includes(result.status ?? -1)))
|
|
100
|
+
refuse("git_observation_failed");
|
|
101
|
+
return result.status === 0 ? result.stdout : null;
|
|
102
|
+
};
|
|
103
|
+
const text = (root, args, absent = false) => git(root, args, absent)?.toString("utf8").trim() ?? null;
|
|
104
|
+
const repository = fs.realpathSync(text(input.projectRoot, ["rev-parse", "--path-format=absolute", "--git-common-dir"]));
|
|
105
|
+
if (fs.realpathSync(text(input.workspace, ["rev-parse", "--path-format=absolute", "--git-common-dir"])) !== repository
|
|
106
|
+
|| text(input.projectRoot, ["rev-parse", "--show-toplevel"]) !== input.projectRoot
|
|
107
|
+
|| text(input.workspace, ["rev-parse", "--show-toplevel"]) !== input.workspace)
|
|
108
|
+
refuse("foreign_workspace_repository");
|
|
109
|
+
if (git(input.workspace, ["ls-files", "--unmerged", "-z"]).length)
|
|
110
|
+
refuse("unmerged_index");
|
|
111
|
+
const identity = fs.statSync(input.workspace);
|
|
112
|
+
const meta = () => {
|
|
113
|
+
const index = text(input.workspace, ["rev-parse", "--path-format=absolute", "--git-path", "index"]);
|
|
114
|
+
let indexHash = null;
|
|
115
|
+
try {
|
|
116
|
+
indexHash = hash(fs.readFileSync(index));
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error.code !== "ENOENT")
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
return { head: text(input.workspace, ["rev-parse", "--verify", "HEAD"], true), branch: text(input.workspace, ["symbolic-ref", "--quiet", "--short", "HEAD"], true),
|
|
123
|
+
index_sha256: indexHash, status_base64: git(input.workspace, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignore-submodules=all"]).toString("base64") };
|
|
124
|
+
};
|
|
125
|
+
const names = () => {
|
|
126
|
+
const bytes = git(input.workspace, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]);
|
|
127
|
+
let value;
|
|
128
|
+
try {
|
|
129
|
+
value = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return refuse("non_utf8_file_name");
|
|
133
|
+
}
|
|
134
|
+
const items = [...new Set(value.split("\0").filter(Boolean))].sort();
|
|
135
|
+
if (items.length > 200_000 || items.some(name => !logical(name)))
|
|
136
|
+
refuse("unsafe_or_unbounded_inventory");
|
|
137
|
+
return items;
|
|
138
|
+
};
|
|
139
|
+
const before = meta(), visible = names(), paths = [...new Set([...visible, ...retainedPaths])].sort();
|
|
140
|
+
if (paths.length > 200_000 || paths.some(name => !logical(name)))
|
|
141
|
+
refuse("unsafe_or_unbounded_inventory");
|
|
142
|
+
const stamps = paths.map(name => stamp(input.workspace, name)), entries = paths.map(name => entry(input.workspace, name));
|
|
143
|
+
const currentRoot = fs.statSync(input.workspace);
|
|
144
|
+
if (identity.dev !== currentRoot.dev || identity.ino !== currentRoot.ino || hybridDigest(stamps) !== hybridDigest(paths.map(name => stamp(input.workspace, name)))
|
|
145
|
+
|| hybridDigest(before) !== hybridDigest(meta()) || hybridDigest(visible) !== hybridDigest(names()))
|
|
146
|
+
refuse("workspace_changed_during_capture");
|
|
147
|
+
return { schema_id: "dd-flow/interactive-git-baseline@1", project_root: input.projectRoot, workspace: input.workspace,
|
|
148
|
+
repository, workspace_identity: { dev: identity.dev, ino: identity.ino }, ...before, visibility: "git-visible-and-retained@1", entries };
|
|
149
|
+
}
|
|
150
|
+
/** Read-only physical baseline, not permission to edit/commit existing dirty
|
|
151
|
+
* files. The RUN owner must retain this in its admitted bundle before work.
|
|
152
|
+
* Ignored files and opaque submodule contents are explicitly outside visibility. */
|
|
153
|
+
export class PreparedInteractiveGitBaseline {
|
|
154
|
+
sha256;
|
|
155
|
+
#value;
|
|
156
|
+
constructor(value) {
|
|
157
|
+
if (Buffer.byteLength(JSON.stringify(value)) > 32 * 1024 * 1024)
|
|
158
|
+
refuse("unbounded_baseline");
|
|
159
|
+
this.#value = structuredClone(value);
|
|
160
|
+
this.sha256 = hash(Buffer.from(JSON.stringify(value)));
|
|
161
|
+
Object.freeze(this);
|
|
162
|
+
}
|
|
163
|
+
static capture(input) { return new PreparedInteractiveGitBaseline(observe(input)); }
|
|
164
|
+
/** Parsing only. Expected bytes/hash must come from the RUN's admitted bundle,
|
|
165
|
+
* not a caller's self-hashed replacement or a current project observation. */
|
|
166
|
+
static restore(bytes, expectedSha256) {
|
|
167
|
+
if (!bytes.length || bytes.length > 32 * 1024 * 1024 || !/^[a-f0-9]{64}$/.test(expectedSha256) || hash(bytes) !== expectedSha256)
|
|
168
|
+
refuse("retained_baseline_changed");
|
|
169
|
+
let value;
|
|
170
|
+
try {
|
|
171
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return refuse("invalid_retained_baseline");
|
|
175
|
+
}
|
|
176
|
+
const checksum = (item) => typeof item === "string" && /^[a-f0-9]{64}$/.test(item);
|
|
177
|
+
if (!value || Object.keys(value).sort().join() !== "branch,entries,head,index_sha256,project_root,repository,schema_id,status_base64,visibility,workspace,workspace_identity"
|
|
178
|
+
|| value.schema_id !== "dd-flow/interactive-git-baseline@1" || value.visibility !== "git-visible-and-retained@1"
|
|
179
|
+
|| [value.project_root, value.workspace, value.repository].some(item => typeof item !== "string" || !path.isAbsolute(item))
|
|
180
|
+
|| !value.workspace_identity || Object.keys(value.workspace_identity).sort().join() !== "dev,ino"
|
|
181
|
+
|| Object.values(value.workspace_identity).some(item => !Number.isSafeInteger(item) || item < 0)
|
|
182
|
+
|| value.branch !== null && (typeof value.branch !== "string" || !value.branch)
|
|
183
|
+
|| value.head !== null && (typeof value.head !== "string" || !/^[a-f0-9]{40,64}$/.test(value.head))
|
|
184
|
+
|| value.index_sha256 !== null && !checksum(value.index_sha256)
|
|
185
|
+
|| typeof value.status_base64 !== "string" || Buffer.from(value.status_base64, "base64").toString("base64") !== value.status_base64
|
|
186
|
+
|| !Array.isArray(value.entries) || value.entries.length > 200_000)
|
|
187
|
+
refuse("invalid_retained_baseline");
|
|
188
|
+
let previous = "";
|
|
189
|
+
for (const item of value.entries) {
|
|
190
|
+
if (!item || Object.keys(item).sort().join() !== "kind,mode,path,sha256" || !logical(item.path) || item.path <= previous
|
|
191
|
+
|| !["file", "link", "directory", "missing"].includes(item.kind)
|
|
192
|
+
|| (item.kind === "missing" ? item.mode !== null : !Number.isInteger(item.mode) || item.mode < 0 || item.mode > 0o7777)
|
|
193
|
+
|| (["file", "link"].includes(item.kind) ? !checksum(item.sha256) : item.sha256 !== null))
|
|
194
|
+
refuse("invalid_retained_inventory");
|
|
195
|
+
previous = item.path;
|
|
196
|
+
}
|
|
197
|
+
const restored = new PreparedInteractiveGitBaseline(value);
|
|
198
|
+
if (restored.sha256 !== expectedSha256)
|
|
199
|
+
refuse("retained_baseline_changed");
|
|
200
|
+
return restored;
|
|
201
|
+
}
|
|
202
|
+
snapshot() { return structuredClone(this.#value); }
|
|
203
|
+
bytes() { return Buffer.from(JSON.stringify(this.#value)); }
|
|
204
|
+
revalidate(input) {
|
|
205
|
+
if (PreparedInteractiveGitBaseline.capture(input).sha256 !== this.sha256)
|
|
206
|
+
refuse("baseline_changed_before_retention");
|
|
207
|
+
}
|
|
208
|
+
observeChanges(input, retainedPaths = []) {
|
|
209
|
+
const current = new PreparedInteractiveGitBaseline(observe(input, [...this.#value.entries.map(item => item.path), ...retainedPaths]));
|
|
210
|
+
return { current, delta: this.changes(current) };
|
|
211
|
+
}
|
|
212
|
+
/** Exact working-file delta relative to entry, not HEAD. Pre-existing dirty
|
|
213
|
+
* content is not automatically attributed to this interactive RUN. Git/index
|
|
214
|
+
* movement is reported separately, never silently approved as integration. */
|
|
215
|
+
changes(current) {
|
|
216
|
+
const before = this.#value, after = current.#value;
|
|
217
|
+
if (before.project_root !== after.project_root || before.workspace !== after.workspace || before.repository !== after.repository
|
|
218
|
+
|| hybridDigest(before.workspace_identity) !== hybridDigest(after.workspace_identity) || before.branch !== after.branch)
|
|
219
|
+
refuse("workspace_identity_changed");
|
|
220
|
+
const left = new Map(before.entries.map(item => [item.path, item])), right = new Map(after.entries.map(item => [item.path, item]));
|
|
221
|
+
if ([...left.keys()].some(name => !right.has(name)))
|
|
222
|
+
refuse("baseline_paths_not_observed");
|
|
223
|
+
const changes = [...new Set([...left.keys(), ...right.keys()])].sort().flatMap(name => {
|
|
224
|
+
const old = left.get(name)?.kind === "missing" ? null : left.get(name) ?? null, next = right.get(name)?.kind === "missing" ? null : right.get(name) ?? null;
|
|
225
|
+
return hybridDigest(old) === hybridDigest(next) ? [] : [{ path: name, before: old, after: next }];
|
|
226
|
+
});
|
|
227
|
+
return { baseline_sha256: this.sha256, current_sha256: current.sha256, changes,
|
|
228
|
+
head_changed: before.head !== after.head, index_changed: before.index_sha256 !== after.index_sha256 };
|
|
229
|
+
}
|
|
230
|
+
}
|