@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.145
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 +59 -0
- package/README.md +13 -8
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +9 -3
- package/dist/cli/command-metadata.js +33 -14
- package/dist/cli/help.js +32 -1
- package/dist/cli/hook-ingress.js +27 -3
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init-invocation.js +10 -3
- package/dist/cli/memory-init.js +74 -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 +70 -32
- package/dist/cli/setup.js +2 -1
- package/dist/cli.js +12 -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/dd-agy-daemon.mjs +62 -20
- package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +70 -13
- package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
- 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/entity-references.d.mts +2 -0
- package/dist/harness-runtime/lib/entity-references.mjs +9 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +13 -6
- package/dist/harness-runtime/lib/native-hook-command.mjs +4 -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 +4 -2
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +19 -6
- package/dist/harness-runtime/lib/runner-events.mjs +3 -1
- package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
- 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/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/cli-operation-classifier.js +1 -1
- package/dist/services/cli-upgrade-allowlist.js +1 -1
- package/dist/services/code-checks.js +11 -4
- package/dist/services/compatibility-preflight.js +5 -2
- package/dist/services/controller-fanout.js +21 -4
- package/dist/services/controller-memory-task.js +45 -0
- package/dist/services/controller-memory-worker.js +48 -0
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-start.js +26 -19
- package/dist/services/descriptor-stage-coordinator.js +30 -10
- package/dist/services/descriptor-stage-start.js +40 -4
- package/dist/services/engines.js +64 -14
- package/dist/services/eval-snapshots.js +104 -93
- package/dist/services/external-work-launch.js +5 -3
- package/dist/services/flow-catalog.js +8 -4
- package/dist/services/frozen-engine.js +113 -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 +2 -13
- package/dist/services/hybrid-digest.js +14 -0
- package/dist/services/installed-flow-source.js +25 -4
- package/dist/services/lifecycle-invocations.js +33 -5
- 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 +28 -2
- package/dist/services/memory-init-controller-start.js +19 -8
- package/dist/services/memory-init-git-policy.js +50 -21
- package/dist/services/memory-init-lint-capability.js +21 -3
- package/dist/services/memory-init-native-capacity.js +13 -0
- package/dist/services/memory-init-preflight.js +11 -1
- 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-layer.js +68 -4
- 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-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 +77 -23
- 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/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 +69 -8
- package/dist/services/recovery-snapshot-database.js +32 -4
- 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-controller-process.js +2 -6
- package/dist/services/run-controller-recovery.js +6 -1
- package/dist/services/run-controller.js +131 -80
- 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/runtime-scope-worker.js +46 -19
- package/dist/services/sessions.js +3 -24
- package/dist/services/setup.js +63 -13
- package/dist/services/stage-handlers.js +9 -7
- package/dist/services/stage-pause.js +108 -44
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-fanout.js +21 -9
- package/dist/services/vnext-plan-review.js +3 -5
- package/dist/services/work-registry.js +80 -33
- 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 +8 -4
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { PreparedMemoryLayer, isCanonicalOnlyMemoryPath } from "./memory-layer.js";
|
|
6
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
7
|
+
import { readBoundedReleaseFile } from "./release-source.js";
|
|
8
|
+
import { prepareJsonFile } from "../cli/input-preparation.js";
|
|
9
|
+
import { validateSchema } from "./schema-validation.js";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { localEnvironmentAllowed } from "./workspace-files.js";
|
|
13
|
+
const hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
14
|
+
function projectPaths(workspace, gitExecutable, env, trackedOnly = false) {
|
|
15
|
+
const gitEnv = Object.fromEntries(Object.entries({ ...process.env, ...env }).filter(([key]) => !key.startsWith("GIT_")));
|
|
16
|
+
const result = spawnSync(gitExecutable, ["-c", "core.fsmonitor=false", "-c", "core.untrackedCache=false",
|
|
17
|
+
"-C", workspace, "ls-files", "-z", "--cached", ...(trackedOnly ? [] : ["--others", "--exclude-standard"])], { env: { ...gitEnv, GIT_OPTIONAL_LOCKS: "0" }, encoding: "utf8", timeout: 5000, maxBuffer: 1024 * 1024 });
|
|
18
|
+
if (result.error || result.signal || result.status !== 0)
|
|
19
|
+
refuse("git_observation_failed");
|
|
20
|
+
const paths = [...new Set(result.stdout.split("\0").filter(Boolean))].sort();
|
|
21
|
+
if (paths.length > 20_000)
|
|
22
|
+
refuse("source_inventory_exceeds_bound");
|
|
23
|
+
return paths;
|
|
24
|
+
}
|
|
25
|
+
function projectEvidencePath(logical) {
|
|
26
|
+
return typeof logical === "string" && logical.length <= 4096 && !path.posix.isAbsolute(logical) && !path.win32.isAbsolute(logical)
|
|
27
|
+
&& !logical.includes("\\") && !logical.includes("\0") && logical.split("/").every(part => part && part !== "." && part !== ".."
|
|
28
|
+
&& ![".git", "node_modules", ".dd-flow", ".memory-bank", ".tasks", ".dd-eval", ".codex", ".claude", ".zcode"].includes(part.toLowerCase()) && localEnvironmentAllowed(part));
|
|
29
|
+
}
|
|
30
|
+
/** Old public project inputs for preservation claims, never untracked scratch,
|
|
31
|
+
* provider state or link referents. The existing selection size bound applies. */
|
|
32
|
+
export function captureUpgradeProjectFiles(workspace, gitExecutable, env) {
|
|
33
|
+
const files = [];
|
|
34
|
+
let total = 0;
|
|
35
|
+
for (const logical of projectPaths(workspace, gitExecutable, env, true)) {
|
|
36
|
+
if (!projectEvidencePath(logical))
|
|
37
|
+
continue;
|
|
38
|
+
let parent = workspace, safe = true;
|
|
39
|
+
for (const part of logical.split("/").slice(0, -1)) {
|
|
40
|
+
parent = path.join(parent, part);
|
|
41
|
+
if (!fs.lstatSync(parent, { throwIfNoEntry: false })?.isDirectory()) {
|
|
42
|
+
safe = false;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!safe)
|
|
47
|
+
continue;
|
|
48
|
+
const file = path.join(workspace, logical), stat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
49
|
+
if (!stat?.isFile())
|
|
50
|
+
continue;
|
|
51
|
+
if (stat.size > 16 * 1024 * 1024 || (total += stat.size) > 64 * 1024 * 1024)
|
|
52
|
+
refuse("source_inventory_exceeds_bound");
|
|
53
|
+
const observed = observeContent(file);
|
|
54
|
+
if (observed.reference.kind !== "file")
|
|
55
|
+
refuse("source_changed_during_inventory", file);
|
|
56
|
+
files.push({ path: logical, base64: observed.content.toString("base64"), sha256: observed.reference.sha256, mode: observed.reference.mode });
|
|
57
|
+
}
|
|
58
|
+
return files;
|
|
59
|
+
}
|
|
60
|
+
/** Decode frozen bytes only; today's files cannot fill missing old evidence. */
|
|
61
|
+
export function readUpgradeProjectFiles(value) {
|
|
62
|
+
if (!Array.isArray(value) || value.length > 20_000)
|
|
63
|
+
refuse("invalid_project_evidence");
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
let total = 0;
|
|
66
|
+
for (const file of value) {
|
|
67
|
+
if (!file || Object.keys(file).sort().join() !== "base64,mode,path,sha256" || !projectEvidencePath(file.path)
|
|
68
|
+
|| seen.has(file.path) || typeof file.base64 !== "string" || file.base64.length > 24 * 1024 * 1024
|
|
69
|
+
|| !Number.isInteger(file.mode) || file.mode < 0 || file.mode > 0o777)
|
|
70
|
+
refuse("invalid_project_evidence");
|
|
71
|
+
const bytes = Buffer.from(file.base64, "base64");
|
|
72
|
+
if (bytes.toString("base64") !== file.base64 || bytes.length > 16 * 1024 * 1024 || (total += bytes.length) > 64 * 1024 * 1024
|
|
73
|
+
|| hash(bytes) !== file.sha256)
|
|
74
|
+
refuse("invalid_project_evidence");
|
|
75
|
+
seen.add(file.path);
|
|
76
|
+
}
|
|
77
|
+
return structuredClone(value);
|
|
78
|
+
}
|
|
79
|
+
/** Literal link identity only: neither outside nor inside referents are read. */
|
|
80
|
+
export function captureUpgradeProjectLinks(workspace, gitExecutable, env) {
|
|
81
|
+
const paths = projectPaths(workspace, gitExecutable, env), links = [];
|
|
82
|
+
let total = 0;
|
|
83
|
+
for (const logical of paths) {
|
|
84
|
+
const parts = logical.split("/");
|
|
85
|
+
if (path.isAbsolute(logical) || parts.some(part => !part || part === "." || part === ".."))
|
|
86
|
+
refuse("unsafe_source_path");
|
|
87
|
+
if (parts.some(part => [".git", "node_modules", ".dd-flow"].includes(part) || !localEnvironmentAllowed(part)))
|
|
88
|
+
continue;
|
|
89
|
+
let directory = workspace, safe = true;
|
|
90
|
+
for (const part of parts.slice(0, -1)) {
|
|
91
|
+
directory = path.join(directory, part);
|
|
92
|
+
if (!fs.lstatSync(directory, { throwIfNoEntry: false })?.isDirectory()) {
|
|
93
|
+
safe = false;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!safe)
|
|
98
|
+
continue;
|
|
99
|
+
const file = path.join(workspace, logical), stat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
100
|
+
if (!stat?.isSymbolicLink())
|
|
101
|
+
continue;
|
|
102
|
+
const observed = observeContent(file);
|
|
103
|
+
if ((total += observed.content.length) > 1024 * 1024)
|
|
104
|
+
refuse("link_metadata_exceeds_bound");
|
|
105
|
+
links.push({ path: logical, target_base64: observed.content.toString("base64"), sha256: observed.reference.sha256, mode: observed.reference.mode });
|
|
106
|
+
}
|
|
107
|
+
return links;
|
|
108
|
+
}
|
|
109
|
+
function refuse(reason, file) {
|
|
110
|
+
throw new AppError("upgrade_inventory_invalid", "Upgrade inventory cannot safely authorize content changes", 1, { reason, ...(file ? { file } : {}), effect: "no_effect" });
|
|
111
|
+
}
|
|
112
|
+
function observe(file) {
|
|
113
|
+
return observeContent(file).reference;
|
|
114
|
+
}
|
|
115
|
+
function observeContent(file) {
|
|
116
|
+
const before = fs.lstatSync(file);
|
|
117
|
+
if (!before.isFile() && !before.isSymbolicLink())
|
|
118
|
+
refuse("unsupported_source_kind", file);
|
|
119
|
+
const content = before.isSymbolicLink() ? fs.readlinkSync(file, { encoding: "buffer" })
|
|
120
|
+
: before.size === 0 ? Buffer.alloc(0) : readBoundedReleaseFile(file, 64 * 1024 * 1024);
|
|
121
|
+
const after = fs.lstatSync(file);
|
|
122
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.mtimeMs !== after.mtimeMs
|
|
123
|
+
|| before.size !== after.size || before.mode !== after.mode)
|
|
124
|
+
refuse("source_changed_during_inventory", file);
|
|
125
|
+
const reference = { path: file, sha256: hash(content), kind: before.isSymbolicLink() ? "link" : "file", mode: before.mode & 0o777 };
|
|
126
|
+
return { reference, content };
|
|
127
|
+
}
|
|
128
|
+
/** Old @2 producers repeated canon_root under canon_repo_root. Observe that
|
|
129
|
+
* exact redundant hint only; it never selects a live canonical checkout. */
|
|
130
|
+
function validateUpgradeManifest(manifest, file) {
|
|
131
|
+
const data = { ...manifest };
|
|
132
|
+
if (Object.hasOwn(data, "canon_repo_root")) {
|
|
133
|
+
if (data.schema_id !== "dd-flow/project-flow-pack-manifest@2" || typeof data.canon_repo_root !== "string"
|
|
134
|
+
|| data.canon_repo_root !== data.canon_root)
|
|
135
|
+
refuse("legacy_manifest_root_conflict", file);
|
|
136
|
+
delete data.canon_repo_root;
|
|
137
|
+
}
|
|
138
|
+
validateSchema({ schemaName: "project-flow-pack-manifest", file, data, schemaDir: fileURLToPath(new URL("../schemas/", import.meta.url)) });
|
|
139
|
+
}
|
|
140
|
+
/** Read-only ownership/delta map. Version mismatch alone grants no overwrite.
|
|
141
|
+
* Project manifest hashes are hints, not independent canonical authority.
|
|
142
|
+
* Without a prepared old release, canonical-looking files need semantic Work.
|
|
143
|
+
*/
|
|
144
|
+
export function prepareMemoryUpgradeInventory(workspaceRoot, layer, baselineLayer) {
|
|
145
|
+
if (!(layer instanceof PreparedMemoryLayer))
|
|
146
|
+
refuse("unprepared_target_layer");
|
|
147
|
+
if (baselineLayer !== undefined && !(baselineLayer instanceof PreparedMemoryLayer))
|
|
148
|
+
refuse("unprepared_baseline_layer");
|
|
149
|
+
if (!path.isAbsolute(workspaceRoot) || fs.realpathSync(workspaceRoot) !== workspaceRoot)
|
|
150
|
+
refuse("noncanonical_workspace");
|
|
151
|
+
const root = path.join(workspaceRoot, ".memory-bank");
|
|
152
|
+
if (!fs.lstatSync(root).isDirectory())
|
|
153
|
+
refuse("linked_or_missing_memory_bank");
|
|
154
|
+
if (!fs.lstatSync(path.join(root, "dd-flow")).isDirectory())
|
|
155
|
+
refuse("linked_or_missing_project_pack");
|
|
156
|
+
if (!fs.lstatSync(path.join(root, "dd-flow/manifest.json")).isFile())
|
|
157
|
+
refuse("linked_or_missing_manifest");
|
|
158
|
+
const prepared = prepareJsonFile(path.join(root, "dd-flow/manifest.json"), "Current project flow-pack manifest");
|
|
159
|
+
validateUpgradeManifest(prepared.value, prepared.file);
|
|
160
|
+
const manifest = prepared.value, current = new Map(), directories = new Set();
|
|
161
|
+
const baselineFiles = baselineLayer?.files();
|
|
162
|
+
if (baselineFiles) {
|
|
163
|
+
const content = baselineFiles.get("dd-flow/manifest.json");
|
|
164
|
+
if (!content)
|
|
165
|
+
refuse("baseline_manifest_missing");
|
|
166
|
+
let old;
|
|
167
|
+
try {
|
|
168
|
+
old = JSON.parse(content.toString("utf8"));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return refuse("baseline_manifest_invalid");
|
|
172
|
+
}
|
|
173
|
+
validateUpgradeManifest(old, "retained-baseline/dd-flow/manifest.json");
|
|
174
|
+
if (old.source_commit !== manifest.source_commit || old.canon_version_at_source_commit !== manifest.canon_version_at_source_commit)
|
|
175
|
+
refuse("baseline_provenance_mismatch");
|
|
176
|
+
}
|
|
177
|
+
let bytes = 0;
|
|
178
|
+
const collect = (directory, prefix = "") => {
|
|
179
|
+
for (const name of fs.readdirSync(directory).sort()) {
|
|
180
|
+
const relative = prefix + name, file = path.join(directory, name), stat = fs.lstatSync(file);
|
|
181
|
+
if (stat.isDirectory()) {
|
|
182
|
+
if (relative.split("/").length > 64)
|
|
183
|
+
refuse("unbounded_directory_depth");
|
|
184
|
+
directories.add(relative);
|
|
185
|
+
collect(file, `${relative}/`);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
if (current.size >= 20_000 || (bytes += stat.size) > 64 * 1024 * 1024)
|
|
189
|
+
refuse("unbounded_current_inventory");
|
|
190
|
+
current.set(relative, observe(file));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
collect(root);
|
|
195
|
+
const target = layer.files(), installed = new Set(manifest.included_files.map(file => `dd-flow/${file}`));
|
|
196
|
+
const paths = [...new Set([...current.keys(), ...target.keys(), ...installed])].sort();
|
|
197
|
+
const entries = paths.map(logical => {
|
|
198
|
+
const source = current.get(logical) ?? null, content = target.get(logical);
|
|
199
|
+
const expected = logical.startsWith("dd-flow/") ? manifest.file_hashes?.[logical.slice(8)] : undefined;
|
|
200
|
+
const baselineBytes = baselineFiles?.get(logical);
|
|
201
|
+
const baseline = baselineBytes && (logical.startsWith("mbb/") || installed.has(logical) || isCanonicalOnlyMemoryPath(logical))
|
|
202
|
+
&& (expected === undefined || expected === hash(baselineBytes)) ? hash(baselineBytes) : null;
|
|
203
|
+
const destination = content ? { path: logical, sha256: hash(content) } : null;
|
|
204
|
+
const canonical = source?.kind === "file" && baseline !== null && source.sha256 === baseline;
|
|
205
|
+
const generated = logical === "dd-flow/manifest.json" || logical.startsWith("dd-flow/.generated/")
|
|
206
|
+
&& manifest.generated_allow_patterns?.includes(".generated/**");
|
|
207
|
+
let ownership = "project", action = "retain";
|
|
208
|
+
if (generated) {
|
|
209
|
+
ownership = "generated";
|
|
210
|
+
if (source && destination && source.sha256 !== destination.sha256)
|
|
211
|
+
action = "update";
|
|
212
|
+
}
|
|
213
|
+
else if (canonical) {
|
|
214
|
+
ownership = "canonical";
|
|
215
|
+
action = !destination ? "archive" : destination.sha256 === source.sha256 ? "retain" : "update";
|
|
216
|
+
}
|
|
217
|
+
else if (installed.has(logical) || logical.startsWith("dd-flow/") || logical.startsWith("mbb/")) {
|
|
218
|
+
ownership = "local-customized";
|
|
219
|
+
if (destination && (!source || source.kind !== "file" || destination.sha256 !== source.sha256) || installed.has(logical) && !source)
|
|
220
|
+
action = "semantic-work";
|
|
221
|
+
}
|
|
222
|
+
if (!source && destination && !installed.has(logical)) {
|
|
223
|
+
ownership = "canonical";
|
|
224
|
+
action = "add";
|
|
225
|
+
}
|
|
226
|
+
const ancestors = logical.split("/").slice(0, -1).map((_part, index, parts) => parts.slice(0, index + 1).join("/"));
|
|
227
|
+
if (destination && (directories.has(logical) || ancestors.some(parent => current.has(parent)))) {
|
|
228
|
+
ownership = "local-customized";
|
|
229
|
+
action = "semantic-work";
|
|
230
|
+
}
|
|
231
|
+
return { path: logical, ownership, action, current: source, target: destination, baseline_sha256: baseline };
|
|
232
|
+
});
|
|
233
|
+
const inventory = { schema_id: "dd-flow/mb-upgrade-inventory@1", workspace_root: workspaceRoot,
|
|
234
|
+
current_manifest: { path: prepared.file, sha256: hash(prepared.text) }, target_layer_sha256: layer.sha256,
|
|
235
|
+
baseline_layer_sha256: baselineLayer?.sha256 ?? null,
|
|
236
|
+
entries, local_divergences: entries.filter(entry => entry.ownership === "local-customized").map(entry => entry.path) };
|
|
237
|
+
return { inventory, sha256: hybridDigest(inventory) };
|
|
238
|
+
}
|
|
239
|
+
/** Older selections called the CLI-owned manifest "retain" even when its
|
|
240
|
+
* target bytes differed. Admit that exact historical representation only;
|
|
241
|
+
* all source/target hashes, modes, ownership and other actions still match.
|
|
242
|
+
* Never rewrite the retained inventory or its issued evidence projection. */
|
|
243
|
+
export function matchesRetainedMemoryUpgradeInventory(current, retained) {
|
|
244
|
+
if (current.sha256 === retained.sha256)
|
|
245
|
+
return true;
|
|
246
|
+
const legacy = retained.inventory.entries.find(entry => entry.path === "dd-flow/manifest.json"
|
|
247
|
+
&& entry.ownership === "generated" && entry.action === "retain");
|
|
248
|
+
if (!legacy)
|
|
249
|
+
return false;
|
|
250
|
+
const inventory = { ...current.inventory, entries: current.inventory.entries.map(entry => entry.path === legacy.path && entry.ownership === "generated" && entry.action === "update"
|
|
251
|
+
? { ...entry, action: "retain" } : entry) };
|
|
252
|
+
return hybridDigest(inventory) === retained.sha256;
|
|
253
|
+
}
|
|
254
|
+
/** Full accepted upgrade plan, unlike the reusable partial permission helper.
|
|
255
|
+
* Refuse omissions while diff-analysis is still correctable, before its plan
|
|
256
|
+
* becomes immutable or Work/publication effects begin.
|
|
257
|
+
*/
|
|
258
|
+
export function validateUpgradePathMap(inventory, changes) {
|
|
259
|
+
const paths = upgradeAuthoringPaths(inventory, changes);
|
|
260
|
+
for (const entry of inventory.entries) {
|
|
261
|
+
const required = entry.ownership === "canonical" && entry.action !== "retain"
|
|
262
|
+
|| entry.ownership === "generated" && entry.target && entry.current?.sha256 !== entry.target.sha256
|
|
263
|
+
|| entry.ownership === "local-customized" && entry.action === "semantic-work";
|
|
264
|
+
if (!required)
|
|
265
|
+
continue;
|
|
266
|
+
const logical = `.memory-bank/${entry.path}`;
|
|
267
|
+
const covered = changes.some(change => {
|
|
268
|
+
if (change.ownership !== entry.ownership)
|
|
269
|
+
return false;
|
|
270
|
+
if (entry.ownership === "generated")
|
|
271
|
+
return change.action === "update" && change.from === logical && change.to === logical;
|
|
272
|
+
if (entry.ownership === "local-customized")
|
|
273
|
+
return change.from === logical || !entry.current && change.to === logical;
|
|
274
|
+
if (entry.action === "archive")
|
|
275
|
+
return change.from === logical && ["archive", "move"].includes(change.action);
|
|
276
|
+
if (entry.action === "add")
|
|
277
|
+
return change.to === logical && ["add", "move"].includes(change.action);
|
|
278
|
+
return change.from === logical && change.to === logical && ["update", "semantic-work"].includes(change.action);
|
|
279
|
+
});
|
|
280
|
+
if (!covered)
|
|
281
|
+
refuse("incomplete_upgrade_path_map", entry.path);
|
|
282
|
+
}
|
|
283
|
+
return paths;
|
|
284
|
+
}
|
|
285
|
+
/** Finite authoring paths from an independently accepted diff. Retention and
|
|
286
|
+
* archival are not semantic write permissions; removal/publication has its own
|
|
287
|
+
* mechanical owner. A project snapshot cannot grant canonical ownership.
|
|
288
|
+
*/
|
|
289
|
+
export function upgradeAuthoringPaths(inventory, changes) {
|
|
290
|
+
const entries = new Map(inventory.entries.map(entry => [entry.path, entry]));
|
|
291
|
+
const result = new Map();
|
|
292
|
+
const aliases = new Map();
|
|
293
|
+
const physical = (value) => value.normalize("NFC").toLowerCase();
|
|
294
|
+
const currentPhysicalPaths = new Map();
|
|
295
|
+
for (const entry of inventory.entries)
|
|
296
|
+
if (entry.current) {
|
|
297
|
+
const key = physical(entry.path), paths = currentPhysicalPaths.get(key) ?? [];
|
|
298
|
+
paths.push(entry.path);
|
|
299
|
+
currentPhysicalPaths.set(key, paths);
|
|
300
|
+
}
|
|
301
|
+
// A diff describes one transition from the retained tree, not sequential
|
|
302
|
+
// edits. Mechanical publication and semantic Work cannot share a path.
|
|
303
|
+
const claim = (paths) => {
|
|
304
|
+
for (const value of new Set(paths)) {
|
|
305
|
+
const key = physical(value);
|
|
306
|
+
for (const [prior, logical] of aliases) {
|
|
307
|
+
if (prior === key && logical !== value)
|
|
308
|
+
refuse("aliased_upgrade_destinations");
|
|
309
|
+
if (prior === key || prior.startsWith(`${key}/`) || key.startsWith(`${prior}/`))
|
|
310
|
+
refuse("conflicting_upgrade_effects");
|
|
311
|
+
}
|
|
312
|
+
aliases.set(key, value);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const logical = (value) => {
|
|
316
|
+
if (!value.startsWith(".memory-bank/") || value.includes("\\") || value.includes("\0")
|
|
317
|
+
|| value.split("/").some(part => !part || part === "." || part === ".."))
|
|
318
|
+
refuse("invalid_upgrade_authoring_path");
|
|
319
|
+
return value.slice(".memory-bank/".length);
|
|
320
|
+
};
|
|
321
|
+
for (const change of changes) {
|
|
322
|
+
const from = change.from === null ? null : logical(change.from), to = change.to === null ? null : logical(change.to);
|
|
323
|
+
const source = from === null ? undefined : entries.get(from), destination = to === null ? undefined : entries.get(to);
|
|
324
|
+
if (from !== null && !source?.current)
|
|
325
|
+
refuse("missing_upgrade_source");
|
|
326
|
+
const owner = source ?? destination;
|
|
327
|
+
if (owner ? owner.ownership !== change.ownership : change.ownership !== "project")
|
|
328
|
+
refuse("changed_upgrade_ownership");
|
|
329
|
+
if (change.action === "retain" && from !== null && to !== null && from !== to || change.action === "add" && from !== null
|
|
330
|
+
|| change.action === "update" && (from === null || from !== to) || change.action === "move" && (from === null || to === null || from === to))
|
|
331
|
+
refuse("invalid_upgrade_path_action");
|
|
332
|
+
if (change.action === "retain") {
|
|
333
|
+
if (from === null && (to === null || !destination || destination.current !== null || destination.ownership !== "local-customized"))
|
|
334
|
+
refuse("invalid_upgrade_path_action");
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (change.action === "archive") {
|
|
338
|
+
if (source?.ownership !== "canonical" || !source.baseline_sha256)
|
|
339
|
+
refuse("unverified_canonical_archive");
|
|
340
|
+
if (to !== null)
|
|
341
|
+
refuse("invalid_upgrade_path_action");
|
|
342
|
+
claim([from]);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (owner?.ownership === "generated" && change.action !== "update")
|
|
346
|
+
refuse("invalid_generated_upgrade_action");
|
|
347
|
+
if (to === null || change.action === "add" && destination?.current || from !== to && destination?.current)
|
|
348
|
+
refuse("conflicting_upgrade_destination");
|
|
349
|
+
if (owner?.ownership === "local-customized" && change.action !== "semantic-work" && change.action !== "move")
|
|
350
|
+
refuse("customized_upgrade_requires_semantic_work");
|
|
351
|
+
if (owner?.ownership === "canonical" && change.action === "move"
|
|
352
|
+
&& (source?.action !== "archive" || destination?.ownership !== "canonical" || destination.action !== "add"))
|
|
353
|
+
refuse("invalid_canonical_move");
|
|
354
|
+
if (destination?.current?.kind === "link" || source?.current?.kind === "link"
|
|
355
|
+
|| currentPhysicalPaths.get(physical(to))?.some(value => value !== to)
|
|
356
|
+
|| inventory.entries.some(entry => entry.current && (entry.path.startsWith(`${to}/`) || to.startsWith(`${entry.path}/`))))
|
|
357
|
+
refuse("unsafe_upgrade_destination_kind");
|
|
358
|
+
claim(from === null ? [to] : [from, to]);
|
|
359
|
+
if (owner?.ownership === "generated" || owner?.ownership === "canonical" && change.action !== "semantic-work")
|
|
360
|
+
continue;
|
|
361
|
+
const paths = result.get(change.unit_id) ?? [];
|
|
362
|
+
if (!paths.includes(change.to))
|
|
363
|
+
paths.push(change.to);
|
|
364
|
+
result.set(change.unit_id, paths);
|
|
365
|
+
}
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
function preservationFile(inventory, logical) {
|
|
369
|
+
const parts = logical.split("/");
|
|
370
|
+
if (parts.some(part => !part || part === "." || part === "..") || path.isAbsolute(logical) || logical.includes("\\") || logical.includes("\0"))
|
|
371
|
+
refuse("invalid_preservation_path");
|
|
372
|
+
const root = path.join(inventory.workspace_root, ".memory-bank");
|
|
373
|
+
if (fs.realpathSync(root) !== root)
|
|
374
|
+
refuse("linked_preservation_root");
|
|
375
|
+
for (let index = 1; index < parts.length; index++)
|
|
376
|
+
if (!fs.lstatSync(path.join(root, ...parts.slice(0, index))).isDirectory())
|
|
377
|
+
refuse("linked_preservation_parent");
|
|
378
|
+
return path.join(root, logical);
|
|
379
|
+
}
|
|
380
|
+
/** Capture before any content effect. Local facts and customized canonical
|
|
381
|
+
* files are retained alike; a symlink contributes its link bytes, not its target.
|
|
382
|
+
* The enclosing immutable selection/SQL receipt owns custody, not these hashes.
|
|
383
|
+
*/
|
|
384
|
+
export function captureUpgradePreservationBaseline(selected) {
|
|
385
|
+
if (hybridDigest(selected.inventory) !== selected.sha256)
|
|
386
|
+
refuse("changed_baseline_inventory");
|
|
387
|
+
const files = selected.inventory.entries.filter(entry => entry.current).map(entry => {
|
|
388
|
+
const file = preservationFile(selected.inventory, entry.path), observed = observeContent(file);
|
|
389
|
+
if (hybridDigest(observed.reference) !== hybridDigest(entry.current))
|
|
390
|
+
refuse("baseline_source_changed", file);
|
|
391
|
+
return { path: entry.path, base64: observed.content.toString("base64") };
|
|
392
|
+
});
|
|
393
|
+
const baseline = { schema_id: "dd-flow/mb-upgrade-preservation-baseline@1", inventory_sha256: selected.sha256, files };
|
|
394
|
+
readUpgradePreservationBaseline(baseline, selected);
|
|
395
|
+
return baseline;
|
|
396
|
+
}
|
|
397
|
+
/** Pure historical decoding, including under an SQL writer/read snapshot.
|
|
398
|
+
* No filesystem access, restoration, overwrite permission or semantic PASS.
|
|
399
|
+
*/
|
|
400
|
+
export function readUpgradePreservationBaseline(baseline, selected) {
|
|
401
|
+
if (!baseline || Object.keys(baseline).sort().join() !== "files,inventory_sha256,schema_id"
|
|
402
|
+
|| baseline.schema_id !== "dd-flow/mb-upgrade-preservation-baseline@1" || baseline.inventory_sha256 !== selected.sha256
|
|
403
|
+
|| hybridDigest(selected.inventory) !== selected.sha256 || !Array.isArray(baseline.files) || baseline.files.length > 20_000)
|
|
404
|
+
refuse("invalid_preservation_baseline");
|
|
405
|
+
const expected = selected.inventory.entries.filter(entry => entry.current);
|
|
406
|
+
if (baseline.files.length !== expected.length)
|
|
407
|
+
refuse("incomplete_preservation_baseline");
|
|
408
|
+
const files = new Map();
|
|
409
|
+
let size = 0;
|
|
410
|
+
for (const [index, entry] of baseline.files.entries()) {
|
|
411
|
+
const original = expected[index], reference = original.current;
|
|
412
|
+
if (!entry || Object.keys(entry).sort().join() !== "base64,path" || entry.path !== original.path || files.has(entry.path)
|
|
413
|
+
|| typeof entry.base64 !== "string" || entry.base64.length > 90 * 1024 * 1024)
|
|
414
|
+
refuse("invalid_preservation_baseline_file");
|
|
415
|
+
const content = Buffer.from(entry.base64, "base64");
|
|
416
|
+
if ((size += content.length) > 64 * 1024 * 1024 || content.toString("base64") !== entry.base64 || hash(content) !== reference.sha256
|
|
417
|
+
|| !["file", "link"].includes(reference.kind) || !Number.isInteger(reference.mode) || reference.mode < 0 || reference.mode > 0o777)
|
|
418
|
+
refuse("preservation_baseline_bytes_changed");
|
|
419
|
+
files.set(entry.path, { bytes: content, kind: reference.kind, mode: reference.mode, sha256: reference.sha256 });
|
|
420
|
+
}
|
|
421
|
+
return files;
|
|
422
|
+
}
|
|
423
|
+
/** Exact-byte and mode preservation for files outside canonical replacement.
|
|
424
|
+
* Semantic edits are intentionally not excused here: their acceptance needs
|
|
425
|
+
* separately named facts/invariants and independent review, not a path bypass.
|
|
426
|
+
*/
|
|
427
|
+
export function verifyUpgradePreservation(inventory) {
|
|
428
|
+
const violations = [];
|
|
429
|
+
for (const entry of inventory.entries) {
|
|
430
|
+
if (!entry.current || !["project", "local-customized"].includes(entry.ownership))
|
|
431
|
+
continue;
|
|
432
|
+
try {
|
|
433
|
+
const actual = observe(preservationFile(inventory, entry.path));
|
|
434
|
+
if (actual.sha256 !== entry.current.sha256 || actual.kind !== entry.current.kind || actual.mode !== entry.current.mode)
|
|
435
|
+
violations.push({ path: entry.path, reason: "preserved_source_changed" });
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
violations.push({ path: entry.path, reason: "preserved_source_unavailable" });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return { ok: violations.length === 0, violations };
|
|
442
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
3
|
+
import { PreparedMemoryUpgradePublication } from "./memory-upgrade-publication.js";
|
|
4
|
+
export const upgradePublicationIntentEvent = "memory.upgrade_publication_intent";
|
|
5
|
+
export const upgradePublicationCompleteEvent = "memory.upgrade_publication_complete";
|
|
6
|
+
export const upgradePublicationKey = (binding) => `memory_upgrade_publication:${hybridDigest(binding.scope)}`;
|
|
7
|
+
export function upgradePublicationReceipt(binding, plan, complete = false) {
|
|
8
|
+
return { schema_id: complete ? "dd-flow/memory-upgrade-publication-complete@1" : "dd-flow/memory-upgrade-publication-intent@1",
|
|
9
|
+
run_id: binding.scope.run_id, scope_sha256: hybridDigest(binding.scope), binding_sha256: hybridDigest(binding), plan_sha256: plan.sha256,
|
|
10
|
+
...(complete ? { outputs: plan.outputs() } : {}) };
|
|
11
|
+
}
|
|
12
|
+
function refuse(reason) {
|
|
13
|
+
throw new AppError("memory_upgrade_publication_custody_invalid", "Publication lacks its exact SQL intent or completion", 1, { reason, effect: "no_effect" });
|
|
14
|
+
}
|
|
15
|
+
/** SQL-only read. The caller selects binding from RUN/bootstrap/registration
|
|
16
|
+
* and settled Work, not agent JSON. Historical readability grants no effects
|
|
17
|
+
* or current filesystem proof; the physical publisher and Stage check those.
|
|
18
|
+
*/
|
|
19
|
+
export function readMemoryUpgradePublication(context, binding) {
|
|
20
|
+
return context.db.readSnapshot(() => {
|
|
21
|
+
const row = context.db.get("SELECT value_json FROM runtime_config WHERE key=?", [upgradePublicationKey(binding)]);
|
|
22
|
+
const events = (event) => context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.scope_sha256')=? LIMIT 2", [binding.scope.project_id, event, hybridDigest(binding.scope)]);
|
|
23
|
+
const intents = events(upgradePublicationIntentEvent), completions = events(upgradePublicationCompleteEvent);
|
|
24
|
+
if (!row && !intents.length && !completions.length)
|
|
25
|
+
return null;
|
|
26
|
+
if (!row || intents.length !== 1 || completions.length > 1 || row.value_json.length > 180 * 1024 * 1024
|
|
27
|
+
|| intents[0].payload_json.length > 32768 || completions.some(row => row.payload_json.length > 16 * 1024 * 1024))
|
|
28
|
+
refuse("intent_custody_missing_or_unbounded");
|
|
29
|
+
try {
|
|
30
|
+
const value = JSON.parse(row.value_json);
|
|
31
|
+
if (!value || Object.keys(value).sort().join() !== "binding,plan_json,plan_sha256" || hybridDigest(value.binding) !== hybridDigest(binding)
|
|
32
|
+
|| typeof value.plan_json !== "string")
|
|
33
|
+
refuse("publication_binding_changed");
|
|
34
|
+
const plan = PreparedMemoryUpgradePublication.restore(Buffer.from(value.plan_json), value.plan_sha256);
|
|
35
|
+
if (hybridDigest(JSON.parse(intents[0].payload_json)) !== hybridDigest(upgradePublicationReceipt(binding, plan)))
|
|
36
|
+
refuse("intent_changed");
|
|
37
|
+
if (completions.length && hybridDigest(JSON.parse(completions[0].payload_json)) !== hybridDigest(upgradePublicationReceipt(binding, plan, true)))
|
|
38
|
+
refuse("completion_changed");
|
|
39
|
+
return { plan, completed: completions.length === 1 };
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error instanceof AppError)
|
|
43
|
+
throw error;
|
|
44
|
+
return refuse("invalid_publication_record");
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Read a completed publication after Stage acceptance. The original Work
|
|
49
|
+
* digest stays in its independently audited publication intent; later FINISH
|
|
50
|
+
* must not reconstruct original Work from repaired current documents. All
|
|
51
|
+
* remaining identity fields are selected again by the RUN/bootstrap owner.
|
|
52
|
+
* This read grants no publication effects or current-content acceptance. */
|
|
53
|
+
export function readCompletedMemoryUpgradePublication(context, expected) {
|
|
54
|
+
return context.db.readSnapshot(() => {
|
|
55
|
+
const row = context.db.get("SELECT value_json FROM runtime_config WHERE key=?", [upgradePublicationKey(expected)]);
|
|
56
|
+
if (!row || Buffer.byteLength(row.value_json) > 180 * 1024 * 1024)
|
|
57
|
+
refuse("completed_publication_required");
|
|
58
|
+
let binding;
|
|
59
|
+
try {
|
|
60
|
+
binding = JSON.parse(row.value_json).binding;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return refuse("invalid_publication_record");
|
|
64
|
+
}
|
|
65
|
+
if (!binding || typeof binding.work_custody_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(binding.work_custody_sha256)
|
|
66
|
+
|| hybridDigest(binding) !== hybridDigest({ ...expected, work_custody_sha256: binding.work_custody_sha256 }))
|
|
67
|
+
refuse("publication_binding_changed");
|
|
68
|
+
const retained = readMemoryUpgradePublication(context, binding);
|
|
69
|
+
if (!retained?.completed)
|
|
70
|
+
refuse("completed_publication_required");
|
|
71
|
+
return { binding: structuredClone(binding), plan: retained.plan };
|
|
72
|
+
});
|
|
73
|
+
}
|