frontend-project-context 1.3.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -2
- package/README.md +94 -16
- package/UPGRADING.md +47 -2
- package/docs/04-PROGRAM-DESIGN.md +40 -4
- package/docs/05-ACCEPTANCE-CONTRACT.md +33 -3
- package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +36 -6
- package/docs/14-FORMAL-RELEASE-READINESS.md +46 -0
- package/docs/18-BRANCH-AWARE-STAGED-CONTEXT-DESIGN.md +62 -2
- package/docs/19-POST-1.3.1-AI-TAKEOVER-EVIDENCE-AND-UPGRADE-PLAN.md +579 -0
- package/docs/20-PHASE-A-AI-TAKEOVER-AND-HEALTH-CLOSURE-DESIGN.md +535 -0
- package/docs/21-PHASE-B-EVIDENCE-FEEDBACK-PROTOCOL-DESIGN.md +347 -0
- package/docs/22-PHASE-C-TARGET-UPGRADE-PROTOCOL-DESIGN.md +398 -0
- package/docs/README.md +21 -5
- package/docs/USER-AND-AI-OPERATION-MANUAL.md +797 -0
- package/examples/README.md +38 -0
- package/examples/package.json +6 -2
- package/migration-manifest.json +88 -0
- package/package.json +3 -2
- package/schemas/action-plan.schema.json +31 -3
- package/schemas/capabilities.schema.json +50 -18
- package/schemas/evidence-bundle.schema.json +64 -0
- package/schemas/evidence-input.schema.json +82 -0
- package/schemas/migration-manifest.schema.json +29 -0
- package/schemas/migration-plan.schema.json +32 -0
- package/schemas/project-status.schema.json +75 -0
- package/schemas/projection-lock.schema.json +48 -0
- package/schemas/review-bundle.schema.json +3 -3
- package/schemas/upgrade-assessment.schema.json +48 -0
- package/schemas/upgrade-result-bundle.schema.json +35 -0
- package/src/project-context/ai-entry.mjs +320 -0
- package/src/project-context/capabilities.mjs +44 -17
- package/src/project-context/checker.mjs +20 -3
- package/src/project-context/cli.mjs +84 -7
- package/src/project-context/contract-schema.mjs +30 -16
- package/src/project-context/dashboard-model.mjs +4 -4
- package/src/project-context/dashboard-renderer.mjs +3 -3
- package/src/project-context/discovery.mjs +6 -1
- package/src/project-context/evidence-schema.mjs +209 -0
- package/src/project-context/evidence.mjs +99 -0
- package/src/project-context/exchange-schema.mjs +21 -11
- package/src/project-context/exchange.mjs +26 -4
- package/src/project-context/maintenance.mjs +2 -2
- package/src/project-context/migration-manifest.mjs +166 -0
- package/src/project-context/project-status.mjs +157 -0
- package/src/project-context/projection-store.mjs +8 -1
- package/src/project-context/task-context-schema.mjs +237 -1
- package/src/project-context/task-context.mjs +154 -13
- package/src/project-context/upgrade-schema.mjs +215 -0
- package/src/project-context/upgrade.mjs +494 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { canonicalJson, canonicalValue, digestJson, validateJsonValue } from "./canonical-json.mjs";
|
|
2
|
+
import { fail } from "./errors.mjs";
|
|
3
|
+
|
|
4
|
+
export const EVIDENCE_INPUT_SCHEMA_VERSION = 1;
|
|
5
|
+
export const EVIDENCE_BUNDLE_SCHEMA_VERSION = 1;
|
|
6
|
+
export const EVIDENCE_INPUT_MAX_UTF8_BYTES = 32 * 1024;
|
|
7
|
+
export const EVIDENCE_OUTPUT_MAX_UTF8_BYTES = 48 * 1024;
|
|
8
|
+
|
|
9
|
+
const ID = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/u;
|
|
10
|
+
const SHA256 = /^sha256:[a-f0-9]{64}$/u;
|
|
11
|
+
const FIELD_PATH = /^[a-z][A-Za-z0-9]*(?:\.[a-z][A-Za-z0-9]*)*$/u;
|
|
12
|
+
const CAPABILITIES = new Set(["setup", "takeover", "maintenance", "staged-context", "upgrade", "other-protocol"]);
|
|
13
|
+
const RESULTS = new Set(["passed", "degraded", "blocked", "failed"]);
|
|
14
|
+
const RUNTIMES = new Set(["node", "other", "redacted"]);
|
|
15
|
+
const PACKAGE_MANAGERS = new Set(["npm", "pnpm", "yarn", "bun", "other", "redacted"]);
|
|
16
|
+
const PROJECT_SHAPES = new Set(["single", "monorepo", "multi-context", "redacted"]);
|
|
17
|
+
const OPERATIONS = new Set([
|
|
18
|
+
"capabilities", "status", "setup-preview", "sync", "check", "context", "preflight", "stage-context",
|
|
19
|
+
"integration-review", "publish-entry-preview", "remove-entry-preview", "upgrade-baseline", "other-protocol",
|
|
20
|
+
]);
|
|
21
|
+
const REDACTION_METHODS = new Set(["removed", "generalized", "hashed", "redacted", "not-collected"]);
|
|
22
|
+
const STRUCTURALLY_FORBIDDEN_FIELDS = new Set([
|
|
23
|
+
"approval", "approve", "authorization", "base64", "body", "branch", "branchName", "businessCode", "chat", "codeBody",
|
|
24
|
+
"cookie", "credential", "credentials", "cwd", "decision", "destination", "diff", "environmentVariables", "externalId", "fileName", "filename",
|
|
25
|
+
"generatedAt", "hostName", "hostname", "itemIds", "items", "locator", "log", "logs", "name", "output", "path",
|
|
26
|
+
"paths", "priority", "projectId", "projectName", "release", "repository", "repositoryUrl", "reviewer", "sourceBody",
|
|
27
|
+
"password", "secret", "sourceIds", "sources", "targetVersion", "timestamp", "token", "upload", "url", "userName", "username", "value",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function invalid(message, details) {
|
|
31
|
+
fail("evidence-input-invalid", message, { details });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function object(value, label) {
|
|
35
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) invalid(`${label} must be an object`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function exactKeys(value, allowed, label) {
|
|
39
|
+
for (const key of Object.keys(value)) {
|
|
40
|
+
if (STRUCTURALLY_FORBIDDEN_FIELDS.has(key)) {
|
|
41
|
+
fail("evidence-redaction-blocked", `${label} contains a structurally forbidden field: ${key}`, { details: { field: key } });
|
|
42
|
+
}
|
|
43
|
+
if (!allowed.has(key)) invalid(`${label} contains unknown field: ${key}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function requiredKeys(value, required, label) {
|
|
48
|
+
for (const key of required) if (!Object.hasOwn(value, key)) invalid(`${label}.${key} is required`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function string(value, label) {
|
|
52
|
+
if (typeof value !== "string" || value.length === 0) invalid(`${label} must be a non-empty string`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function stableId(value, label) {
|
|
56
|
+
string(value, label);
|
|
57
|
+
if (!ID.test(value)) invalid(`${label} must use stable lowercase dot/kebab naming`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function summary(value, label) {
|
|
61
|
+
string(value, label);
|
|
62
|
+
if (/\r|\n|\u2028|\u2029/u.test(value)) invalid(`${label} must be a single line`);
|
|
63
|
+
if (Buffer.byteLength(value, "utf8") > 500) invalid(`${label} exceeds 500 UTF-8 bytes`);
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function enumValue(value, allowed, label) {
|
|
68
|
+
if (!allowed.has(value)) invalid(`${label} is invalid`);
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function array(value, label, max) {
|
|
73
|
+
if (!Array.isArray(value)) invalid(`${label} must be an array`);
|
|
74
|
+
if (value.length > max) invalid(`${label} exceeds ${max} entries`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function uniqueSorted(values, identity = (value) => canonicalJson(value)) {
|
|
78
|
+
return [...new Map(values.map((value) => [identity(value), value])).values()]
|
|
79
|
+
.sort((left, right) => identity(left).localeCompare(identity(right)));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeCodes(value, label) {
|
|
83
|
+
array(value, label, 32);
|
|
84
|
+
const normalized = value.map((entry, index) => {
|
|
85
|
+
stableId(entry, `${label}[${index}]`);
|
|
86
|
+
return entry;
|
|
87
|
+
});
|
|
88
|
+
return uniqueSorted(normalized, (entry) => entry);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeEnvironment(value) {
|
|
92
|
+
object(value, "environment");
|
|
93
|
+
const keys = new Set(["runtime", "runtimeMajor", "packageManager", "projectShape"]);
|
|
94
|
+
exactKeys(value, keys, "environment");
|
|
95
|
+
requiredKeys(value, keys, "environment");
|
|
96
|
+
if (!Number.isInteger(value.runtimeMajor) || value.runtimeMajor < 0 || value.runtimeMajor > 999) {
|
|
97
|
+
invalid("environment.runtimeMajor must be an integer from 0 through 999");
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
runtime: enumValue(value.runtime, RUNTIMES, "environment.runtime"),
|
|
101
|
+
runtimeMajor: value.runtimeMajor,
|
|
102
|
+
packageManager: enumValue(value.packageManager, PACKAGE_MANAGERS, "environment.packageManager"),
|
|
103
|
+
projectShape: enumValue(value.projectShape, PROJECT_SHAPES, "environment.projectShape"),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeObservation(value, label) {
|
|
108
|
+
object(value, label);
|
|
109
|
+
const keys = new Set(["code", "summary"]);
|
|
110
|
+
exactKeys(value, keys, label);
|
|
111
|
+
requiredKeys(value, keys, label);
|
|
112
|
+
stableId(value.code, `${label}.code`);
|
|
113
|
+
return { code: value.code, summary: summary(value.summary, `${label}.summary`) };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeReproduction(value) {
|
|
117
|
+
array(value, "reproduction", 20);
|
|
118
|
+
return value.map((entry, index) => {
|
|
119
|
+
const label = `reproduction[${index}]`;
|
|
120
|
+
object(entry, label);
|
|
121
|
+
const keys = new Set(["operation", "outcome", "findingCodes"]);
|
|
122
|
+
exactKeys(entry, keys, label);
|
|
123
|
+
requiredKeys(entry, keys, label);
|
|
124
|
+
return {
|
|
125
|
+
operation: enumValue(entry.operation, OPERATIONS, `${label}.operation`),
|
|
126
|
+
outcome: enumValue(entry.outcome, RESULTS, `${label}.outcome`),
|
|
127
|
+
findingCodes: normalizeCodes(entry.findingCodes, `${label}.findingCodes`),
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeArtifacts(value) {
|
|
133
|
+
array(value, "artifacts", 32);
|
|
134
|
+
const normalized = value.map((entry, index) => {
|
|
135
|
+
const label = `artifacts[${index}]`;
|
|
136
|
+
object(entry, label);
|
|
137
|
+
const keys = new Set(["kind", "digest"]);
|
|
138
|
+
exactKeys(entry, keys, label);
|
|
139
|
+
requiredKeys(entry, keys, label);
|
|
140
|
+
stableId(entry.kind, `${label}.kind`);
|
|
141
|
+
if (typeof entry.digest !== "string" || !SHA256.test(entry.digest)) invalid(`${label}.digest must be sha256`);
|
|
142
|
+
return { kind: entry.kind, digest: entry.digest };
|
|
143
|
+
});
|
|
144
|
+
return uniqueSorted(normalized);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeRedactions(value) {
|
|
148
|
+
array(value, "redactions", 32);
|
|
149
|
+
const normalized = value.map((entry, index) => {
|
|
150
|
+
const label = `redactions[${index}]`;
|
|
151
|
+
object(entry, label);
|
|
152
|
+
const keys = new Set(["field", "method"]);
|
|
153
|
+
exactKeys(entry, keys, label);
|
|
154
|
+
requiredKeys(entry, keys, label);
|
|
155
|
+
string(entry.field, `${label}.field`);
|
|
156
|
+
if (!FIELD_PATH.test(entry.field)) invalid(`${label}.field must be a dotted input field path`);
|
|
157
|
+
return { field: entry.field, method: enumValue(entry.method, REDACTION_METHODS, `${label}.method`) };
|
|
158
|
+
});
|
|
159
|
+
return uniqueSorted(normalized);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function normalizeEvidenceInput(input) {
|
|
163
|
+
try {
|
|
164
|
+
validateJsonValue(input);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
invalid("evidence input must be JSON-compatible", { reason: error.message });
|
|
167
|
+
}
|
|
168
|
+
object(input, "evidence input");
|
|
169
|
+
const keys = new Set([
|
|
170
|
+
"schemaVersion", "capability", "result", "environment", "expected", "observed", "errorCodes",
|
|
171
|
+
"reproduction", "artifacts", "redactions",
|
|
172
|
+
]);
|
|
173
|
+
exactKeys(input, keys, "evidence input");
|
|
174
|
+
requiredKeys(input, keys, "evidence input");
|
|
175
|
+
if (input.schemaVersion !== EVIDENCE_INPUT_SCHEMA_VERSION) invalid("evidence input schemaVersion must be 1");
|
|
176
|
+
const normalized = {
|
|
177
|
+
schemaVersion: EVIDENCE_INPUT_SCHEMA_VERSION,
|
|
178
|
+
capability: enumValue(input.capability, CAPABILITIES, "capability"),
|
|
179
|
+
result: enumValue(input.result, RESULTS, "result"),
|
|
180
|
+
environment: normalizeEnvironment(input.environment),
|
|
181
|
+
expected: normalizeObservation(input.expected, "expected"),
|
|
182
|
+
observed: normalizeObservation(input.observed, "observed"),
|
|
183
|
+
errorCodes: normalizeCodes(input.errorCodes, "errorCodes"),
|
|
184
|
+
reproduction: normalizeReproduction(input.reproduction),
|
|
185
|
+
artifacts: normalizeArtifacts(input.artifacts),
|
|
186
|
+
redactions: normalizeRedactions(input.redactions),
|
|
187
|
+
};
|
|
188
|
+
if (Buffer.byteLength(canonicalJson(normalized), "utf8") > EVIDENCE_INPUT_MAX_UTF8_BYTES) {
|
|
189
|
+
fail("evidence-input-budget-exceeded", `canonical evidence input exceeds ${EVIDENCE_INPUT_MAX_UTF8_BYTES} UTF-8 bytes`);
|
|
190
|
+
}
|
|
191
|
+
return canonicalValue(normalized);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function finalizeEvidenceBundle(bundleWithoutDigest) {
|
|
195
|
+
const canonical = canonicalValue(bundleWithoutDigest);
|
|
196
|
+
const bundle = canonicalValue({ ...canonical, bundleDigest: digestJson(canonical) });
|
|
197
|
+
if (Buffer.byteLength(canonicalJson(bundle), "utf8") > EVIDENCE_OUTPUT_MAX_UTF8_BYTES) {
|
|
198
|
+
fail("evidence-output-budget-exceeded", `canonical evidence output exceeds ${EVIDENCE_OUTPUT_MAX_UTF8_BYTES} UTF-8 bytes`);
|
|
199
|
+
}
|
|
200
|
+
return bundle;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function verifyEvidenceBundleDigest(bundle) {
|
|
204
|
+
object(bundle, "evidence bundle");
|
|
205
|
+
const copy = structuredClone(bundle);
|
|
206
|
+
const actual = copy.bundleDigest;
|
|
207
|
+
delete copy.bundleDigest;
|
|
208
|
+
return typeof actual === "string" && actual === digestJson(canonicalValue(copy));
|
|
209
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { PERMANENT_BOUNDARIES } from "./capabilities.mjs";
|
|
4
|
+
import { fail, ProjectContextError } from "./errors.mjs";
|
|
5
|
+
import { EVIDENCE_BUNDLE_SCHEMA_VERSION, finalizeEvidenceBundle, normalizeEvidenceInput } from "./evidence-schema.mjs";
|
|
6
|
+
import { EXCHANGE_PROTOCOL_VERSION, PACKAGE_VERSION } from "./exchange-schema.mjs";
|
|
7
|
+
import { normalizeRelativePath, resolveExistingInside } from "./path-policy.mjs";
|
|
8
|
+
import { buildProjectStatus } from "./project-status.mjs";
|
|
9
|
+
|
|
10
|
+
async function readEvidenceInput(root, inputPath) {
|
|
11
|
+
let normalized;
|
|
12
|
+
try {
|
|
13
|
+
normalized = normalizeRelativePath(inputPath, { label: "evidence input" });
|
|
14
|
+
} catch (error) {
|
|
15
|
+
fail("evidence-input-outside-project", "evidence input must stay inside the project", { cause: error, details: { path: inputPath } });
|
|
16
|
+
}
|
|
17
|
+
const candidate = path.join(root, normalized);
|
|
18
|
+
let info;
|
|
19
|
+
try {
|
|
20
|
+
info = await lstat(candidate);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error?.code === "ENOENT") fail("evidence-input-missing", `evidence input does not exist: ${normalized}`, { cause: error });
|
|
23
|
+
fail("evidence-input-invalid", `cannot inspect evidence input: ${normalized}`, { cause: error });
|
|
24
|
+
}
|
|
25
|
+
if (info.isSymbolicLink()) {
|
|
26
|
+
fail("evidence-input-outside-project", "evidence input cannot be a symlink", { details: { path: normalized } });
|
|
27
|
+
}
|
|
28
|
+
if (!info.isFile()) fail("evidence-input-invalid", "evidence input must be an ordinary file", { details: { path: normalized } });
|
|
29
|
+
let resolved;
|
|
30
|
+
try {
|
|
31
|
+
resolved = await resolveExistingInside(root, normalized);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error?.code === "path-outside-project") {
|
|
34
|
+
fail("evidence-input-outside-project", "evidence input must stay inside the project", { cause: error, details: { path: normalized } });
|
|
35
|
+
}
|
|
36
|
+
if (error?.code === "source-missing") fail("evidence-input-missing", `evidence input does not exist: ${normalized}`, { cause: error });
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
let source;
|
|
40
|
+
try {
|
|
41
|
+
source = await readFile(resolved.absolute, "utf8");
|
|
42
|
+
} catch (error) {
|
|
43
|
+
fail("evidence-input-invalid", `cannot read evidence input: ${normalized}`, { cause: error });
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(source);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
fail("evidence-input-invalid", "evidence input contains invalid JSON", { cause: error });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function minimalProjectContext(status) {
|
|
53
|
+
return {
|
|
54
|
+
initialization: status.initialization.state,
|
|
55
|
+
health: status.health,
|
|
56
|
+
entryState: status.entry.state,
|
|
57
|
+
findingCodes: [...new Set(status.findingCodes)].sort((left, right) => left.localeCompare(right)),
|
|
58
|
+
schemas: {
|
|
59
|
+
contract: 2,
|
|
60
|
+
sourcesLock: 1,
|
|
61
|
+
projectionsLock: 2,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function buildEvidenceBundle(root, input) {
|
|
67
|
+
const normalized = normalizeEvidenceInput(input);
|
|
68
|
+
let statusResult;
|
|
69
|
+
try {
|
|
70
|
+
statusResult = await buildProjectStatus(root, PERMANENT_BOUNDARIES);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
fail("evidence-project-state-unreadable", "cannot form minimal Project Context status", {
|
|
73
|
+
cause: error,
|
|
74
|
+
details: { reason: error instanceof ProjectContextError ? error.code : "internal-state-error" },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const { schemaVersion: _inputSchemaVersion, ...observation } = normalized;
|
|
78
|
+
return finalizeEvidenceBundle({
|
|
79
|
+
schemaVersion: EVIDENCE_BUNDLE_SCHEMA_VERSION,
|
|
80
|
+
kind: "target-project-evidence",
|
|
81
|
+
product: {
|
|
82
|
+
name: "frontend-project-context",
|
|
83
|
+
version: PACKAGE_VERSION,
|
|
84
|
+
exchangeProtocolVersion: EXCHANGE_PROTOCOL_VERSION,
|
|
85
|
+
},
|
|
86
|
+
...observation,
|
|
87
|
+
projectContext: minimalProjectContext(statusResult.status),
|
|
88
|
+
transfer: {
|
|
89
|
+
state: "human-review-required",
|
|
90
|
+
automaticUpload: false,
|
|
91
|
+
destination: null,
|
|
92
|
+
},
|
|
93
|
+
boundaries: { ...PERMANENT_BOUNDARIES },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function buildEvidenceBundleFile(root, inputPath) {
|
|
98
|
+
return buildEvidenceBundle(root, await readEvidenceInput(root, inputPath));
|
|
99
|
+
}
|
|
@@ -2,27 +2,30 @@ import { canonicalValue, digestJson, validateJsonValue } from "./canonical-json.
|
|
|
2
2
|
import { fail } from "./errors.mjs";
|
|
3
3
|
import { normalizeRelativePath } from "./path-policy.mjs";
|
|
4
4
|
|
|
5
|
-
export const PACKAGE_VERSION = "1.
|
|
6
|
-
export const EXCHANGE_PROTOCOL_VERSION =
|
|
7
|
-
export const ACTION_PLAN_SCHEMA_VERSION =
|
|
8
|
-
export const REVIEW_BUNDLE_SCHEMA_VERSION =
|
|
9
|
-
export const CAPABILITIES_SCHEMA_VERSION =
|
|
5
|
+
export const PACKAGE_VERSION = "1.6.0";
|
|
6
|
+
export const EXCHANGE_PROTOCOL_VERSION = 4;
|
|
7
|
+
export const ACTION_PLAN_SCHEMA_VERSION = 2;
|
|
8
|
+
export const REVIEW_BUNDLE_SCHEMA_VERSION = 2;
|
|
9
|
+
export const CAPABILITIES_SCHEMA_VERSION = 4;
|
|
10
10
|
|
|
11
11
|
export const ACTION_KINDS = Object.freeze([
|
|
12
12
|
"accept-source-change",
|
|
13
13
|
"deprecate-item",
|
|
14
14
|
"deprecate-source",
|
|
15
15
|
"propose-item",
|
|
16
|
+
"publish-ai-entry",
|
|
16
17
|
"publish-projection",
|
|
17
18
|
"register-source",
|
|
19
|
+
"remove-ai-entry",
|
|
18
20
|
"request-item-approval",
|
|
19
21
|
"revise-item",
|
|
20
22
|
]);
|
|
21
23
|
|
|
22
24
|
export const COMMANDS = Object.freeze([
|
|
23
25
|
"accept-source-change", "approve", "capabilities", "check", "context", "dashboard", "deprecate",
|
|
24
|
-
"deprecate-source", "discover", "init", "integration-review", "preflight", "propose", "publish", "register",
|
|
25
|
-
"review-source", "revise", "setup", "stage-context", "sync",
|
|
26
|
+
"deprecate-source", "discover", "evidence", "init", "integration-review", "preflight", "propose", "publish", "register",
|
|
27
|
+
"publish-entry", "remove-entry", "review-source", "revise", "setup", "stage-context", "status", "sync",
|
|
28
|
+
"upgrade-apply", "upgrade-check", "upgrade-plan",
|
|
26
29
|
]);
|
|
27
30
|
|
|
28
31
|
const ACTION_KIND_SET = new Set(ACTION_KINDS);
|
|
@@ -253,13 +256,20 @@ function normalizeActionInput(kind, input, label) {
|
|
|
253
256
|
digest(input.expectedContentDigest, `${label}.expectedContentDigest`, { nullable: true });
|
|
254
257
|
return { target: input.target, output, paths: [...paths].sort(), expectedContentDigest: input.expectedContentDigest };
|
|
255
258
|
}
|
|
259
|
+
if (kind === "publish-ai-entry" || kind === "remove-ai-entry") {
|
|
260
|
+
object(input, label);
|
|
261
|
+
exactKeys(input, new Set(["output"]), label);
|
|
262
|
+
const output = projectPath(input.output, `${label}.output`);
|
|
263
|
+
if (output.split("/").at(-1) !== "AGENTS.md") invalid(`${label}.output must name AGENTS.md`);
|
|
264
|
+
return { output };
|
|
265
|
+
}
|
|
256
266
|
invalid(`${label} kind is unsupported: ${kind}`);
|
|
257
267
|
}
|
|
258
268
|
|
|
259
269
|
export function validateActionPlan(input) {
|
|
260
270
|
object(input, "action plan");
|
|
261
271
|
exactKeys(input, new Set(["schemaVersion", "projectId", "baselines", "actions"]), "action plan");
|
|
262
|
-
if (input.schemaVersion
|
|
272
|
+
if (![1, ACTION_PLAN_SCHEMA_VERSION].includes(input.schemaVersion)) invalid(`action plan schemaVersion must be 1 or ${ACTION_PLAN_SCHEMA_VERSION}`);
|
|
263
273
|
stableId(input.projectId, "action plan.projectId");
|
|
264
274
|
object(input.baselines, "action plan.baselines");
|
|
265
275
|
exactKeys(input.baselines, new Set(["contract", "sourcesLock", "projectionsLock"]), "action plan.baselines");
|
|
@@ -273,10 +283,10 @@ export function validateActionPlan(input) {
|
|
|
273
283
|
stableId(action.id, `${label}.id`);
|
|
274
284
|
if (ids.has(action.id)) invalid(`action plan contains duplicate action id: ${action.id}`);
|
|
275
285
|
ids.add(action.id);
|
|
276
|
-
if (!ACTION_KIND_SET.has(action.kind)) invalid(`${label}.kind is invalid`);
|
|
286
|
+
if (!ACTION_KIND_SET.has(action.kind) || (input.schemaVersion === 1 && ["publish-ai-entry", "remove-ai-entry"].includes(action.kind))) invalid(`${label}.kind is invalid`);
|
|
277
287
|
return { id: action.id, kind: action.kind, input: normalizeActionInput(action.kind, action.input, `${label}.input`) };
|
|
278
288
|
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
279
|
-
return canonicalValue({ schemaVersion:
|
|
289
|
+
return canonicalValue({ schemaVersion: input.schemaVersion, projectId: input.projectId, baselines: canonicalValue(input.baselines), actions });
|
|
280
290
|
}
|
|
281
291
|
|
|
282
292
|
export function actionPlanDigest(plan) {
|
|
@@ -496,7 +506,7 @@ export function assertActionPlanConflictFree(plan) {
|
|
|
496
506
|
if (["register-source", "accept-source-change", "deprecate-source"].includes(action.kind)) targets.push(`source:${action.input.id}`);
|
|
497
507
|
if (["propose-item", "revise-item", "deprecate-item"].includes(action.kind)) targets.push(`item:${action.input.id}`);
|
|
498
508
|
if (action.kind === "request-item-approval") targets.push(...action.input.ids.map((id) => `item:${id}`));
|
|
499
|
-
if (
|
|
509
|
+
if (["publish-projection", "publish-ai-entry", "remove-ai-entry"].includes(action.kind)) targets.push(`projection:${action.input.output}`);
|
|
500
510
|
for (const target of targets) {
|
|
501
511
|
const existing = owners.get(target);
|
|
502
512
|
if (existing) conflicts.push({ target, actions: [existing, action.id].sort() });
|
|
@@ -16,6 +16,7 @@ import { acceptSourceChange, deprecateItem, deprecateSource, reviewSource, revis
|
|
|
16
16
|
import { resolveExistingInside, resolveWritableInside } from "./path-policy.mjs";
|
|
17
17
|
import { loadProject } from "./project-store.mjs";
|
|
18
18
|
import { publishProjection } from "./projection-store.mjs";
|
|
19
|
+
import { publishAiEntry, removeAiEntry } from "./ai-entry.mjs";
|
|
19
20
|
export { buildCapabilities } from "./capabilities.mjs";
|
|
20
21
|
|
|
21
22
|
const INTERNAL_AUTHORITY_SENTINEL = "exchange-preflight-not-authority";
|
|
@@ -29,6 +30,8 @@ const GROUP_BY_ACTION = new Map([
|
|
|
29
30
|
["deprecate-source", "deprecation"],
|
|
30
31
|
["request-item-approval", "approval-request"],
|
|
31
32
|
["publish-projection", "projection"],
|
|
33
|
+
["publish-ai-entry", "projection"],
|
|
34
|
+
["remove-ai-entry", "projection"],
|
|
32
35
|
]);
|
|
33
36
|
|
|
34
37
|
function uniqueSorted(values) {
|
|
@@ -37,13 +40,13 @@ function uniqueSorted(values) {
|
|
|
37
40
|
|
|
38
41
|
|
|
39
42
|
function allProjectionPaths(project) {
|
|
40
|
-
return project.projectionsLock.projections.map((entry) => entry.path).sort();
|
|
43
|
+
return project.projectionsLock.projections.filter((entry) => entry.ownership !== "region").map((entry) => entry.path).sort();
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
function directProjectionPaths(project, itemIds) {
|
|
44
47
|
const ids = new Set(itemIds);
|
|
45
48
|
return project.projectionsLock.projections
|
|
46
|
-
.filter((entry) => entry.itemIds.some((id) => ids.has(id)))
|
|
49
|
+
.filter((entry) => entry.ownership !== "region" && entry.itemIds.some((id) => ids.has(id)))
|
|
47
50
|
.map((entry) => entry.path)
|
|
48
51
|
.sort();
|
|
49
52
|
}
|
|
@@ -414,6 +417,24 @@ async function previewPublish(root, project, action) {
|
|
|
414
417
|
};
|
|
415
418
|
}
|
|
416
419
|
|
|
420
|
+
async function previewEntry(root, project, action) {
|
|
421
|
+
const result = action.kind === "publish-ai-entry"
|
|
422
|
+
? await publishAiEntry(root, project, { output: action.input.output, write: false })
|
|
423
|
+
: await removeAiEntry(root, project, { output: action.input.output, write: false });
|
|
424
|
+
return {
|
|
425
|
+
current: result.current,
|
|
426
|
+
proposed: { ...result.proposed, action: result.action, migration: result.migration, afterSourceDigest: result.impact.afterSourceDigest },
|
|
427
|
+
baselines: actionBaseline(project, { currentContentDigest: result.baselines.content }),
|
|
428
|
+
impact: {
|
|
429
|
+
itemIds: result.impact.itemIds,
|
|
430
|
+
sourceIds: result.impact.sourceIds,
|
|
431
|
+
paths: result.impact.paths,
|
|
432
|
+
projectionPaths: result.impact.projectionPaths,
|
|
433
|
+
},
|
|
434
|
+
invocation: invocation(root, action.kind === "publish-ai-entry" ? "publish-entry" : "remove-entry", ["--output", action.input.output]),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
417
438
|
async function previewAction(root, project, action) {
|
|
418
439
|
if (action.kind === "register-source") return previewRegister(root, project, action);
|
|
419
440
|
if (action.kind === "propose-item") return previewPropose(root, project, action);
|
|
@@ -423,6 +444,7 @@ async function previewAction(root, project, action) {
|
|
|
423
444
|
if (action.kind === "deprecate-source") return previewDeprecateSource(root, project, action);
|
|
424
445
|
if (action.kind === "request-item-approval") return previewApproval(root, project, action);
|
|
425
446
|
if (action.kind === "publish-projection") return previewPublish(root, project, action);
|
|
447
|
+
if (action.kind === "publish-ai-entry" || action.kind === "remove-ai-entry") return previewEntry(root, project, action);
|
|
426
448
|
fail("action-plan-schema-invalid", `unsupported action kind: ${action.kind}`);
|
|
427
449
|
}
|
|
428
450
|
|
|
@@ -452,9 +474,9 @@ function blockedContext(project, action, error) {
|
|
|
452
474
|
current = action.input.ids.map((id) => structuredClone(project.contract.items.find((item) => item.id === id) ?? null));
|
|
453
475
|
impact.itemIds = [...action.input.ids];
|
|
454
476
|
}
|
|
455
|
-
if (
|
|
477
|
+
if (["publish-projection", "publish-ai-entry", "remove-ai-entry"].includes(action.kind)) {
|
|
456
478
|
current = structuredClone(project.projectionsLock.projections.find((entry) => entry.path === action.input.output) ?? null);
|
|
457
|
-
impact.paths = uniqueSorted([...action.input.paths, action.input.output]);
|
|
479
|
+
impact.paths = uniqueSorted([...(action.input.paths ?? []), action.input.output]);
|
|
458
480
|
impact.projectionPaths = [action.input.output];
|
|
459
481
|
}
|
|
460
482
|
return {
|
|
@@ -72,10 +72,10 @@ export function sourceImpact(project, sourceId) {
|
|
|
72
72
|
const relations = relationDetails(project.contract, sourceId);
|
|
73
73
|
const affectedIds = new Set(relations.items.map((item) => item.id));
|
|
74
74
|
const directProjectionPaths = project.projectionsLock.projections
|
|
75
|
-
.filter((entry) => entry.itemIds.some((id) => affectedIds.has(id)))
|
|
75
|
+
.filter((entry) => entry.ownership !== "region" && entry.itemIds.some((id) => affectedIds.has(id)))
|
|
76
76
|
.map((entry) => entry.path)
|
|
77
77
|
.sort();
|
|
78
|
-
const staleProjectionPaths = project.projectionsLock.projections.map((entry) => entry.path).sort();
|
|
78
|
+
const staleProjectionPaths = project.projectionsLock.projections.filter((entry) => entry.ownership !== "region").map((entry) => entry.path).sort();
|
|
79
79
|
return { ...relations, directProjectionPaths, staleProjectionPaths };
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { digestJson } from "./canonical-json.mjs";
|
|
4
|
+
import { fail } from "./errors.mjs";
|
|
5
|
+
|
|
6
|
+
export const MIGRATION_MANIFEST_SCHEMA_VERSION = 2;
|
|
7
|
+
|
|
8
|
+
export const BUILT_IN_MIGRATIONS = Object.freeze({
|
|
9
|
+
"upgrade.republish-ai-entry.v1": "republish-ai-entry",
|
|
10
|
+
"upgrade.republish-projection.v1": "republish-projection",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const MANIFEST_FILE = fileURLToPath(new URL("../../migration-manifest.json", import.meta.url));
|
|
14
|
+
const SHA256 = /^sha256:[a-f0-9]{64}$/u;
|
|
15
|
+
const VERSION = /^\d+\.\d+\.\d+$/u;
|
|
16
|
+
const MIGRATION_KINDS = new Set([
|
|
17
|
+
"package-only",
|
|
18
|
+
"republish-ai-entry",
|
|
19
|
+
"republish-projection",
|
|
20
|
+
"built-in-store-migration",
|
|
21
|
+
"invalidate-ephemeral-protocol",
|
|
22
|
+
]);
|
|
23
|
+
const ROLLBACK_CLASSES = new Set(["package-only", "reversible-data", "forward-only"]);
|
|
24
|
+
const CONSUMER_CHANGE_KEYS = new Set([
|
|
25
|
+
"actionPlan", "capabilities", "evidenceBundle", "evidenceInput", "exchange",
|
|
26
|
+
"integrationReviewBundle", "reviewBundle", "stageContextBundle", "stageReceipt", "taskContextPlan",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
function invalid(message, details) {
|
|
30
|
+
fail("upgrade-manifest-invalid", message, { details });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function object(value, label) {
|
|
34
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) invalid(`${label} must be an object`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function exactKeys(value, allowed, label) {
|
|
38
|
+
object(value, label);
|
|
39
|
+
const keys = Object.keys(value).sort();
|
|
40
|
+
const expected = [...allowed].sort();
|
|
41
|
+
if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) {
|
|
42
|
+
invalid(`${label} must contain exactly: ${expected.join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function string(value, label) {
|
|
47
|
+
if (typeof value !== "string" || value.length === 0) invalid(`${label} must be a non-empty string`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function boolean(value, label) {
|
|
51
|
+
if (typeof value !== "boolean") invalid(`${label} must be a boolean`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function integer(value, label) {
|
|
55
|
+
if (!Number.isInteger(value) || value < 1) invalid(`${label} must be a positive integer`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sortedUnique(values, label, validate = string) {
|
|
59
|
+
if (!Array.isArray(values)) invalid(`${label} must be an array`);
|
|
60
|
+
values.forEach((value) => validate(value, `${label} entry`));
|
|
61
|
+
const sorted = [...values].sort((left, right) => String(left).localeCompare(String(right)));
|
|
62
|
+
if (new Set(values).size !== values.length || values.some((value, index) => value !== sorted[index])) {
|
|
63
|
+
invalid(`${label} must be unique and sorted`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function versions(values, label) {
|
|
68
|
+
sortedUnique(values, label, (value, entryLabel) => {
|
|
69
|
+
string(value, entryLabel);
|
|
70
|
+
if (!VERSION.test(value)) invalid(`${entryLabel} must be an exact semantic version`);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function versionMatrix(value, label) {
|
|
75
|
+
exactKeys(value, new Set(["readable", "written"]), label);
|
|
76
|
+
sortedUnique(value.readable, `${label}.readable`, integer);
|
|
77
|
+
if (Array.isArray(value.written)) sortedUnique(value.written, `${label}.written`, integer);
|
|
78
|
+
else integer(value.written, `${label}.written`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function validateMigration(entry, index) {
|
|
82
|
+
const label = `builtInMigrations[${index}]`;
|
|
83
|
+
exactKeys(entry, new Set(["id", "kind", "input", "targetType", "writes", "rollbackClass", "reverseMigrationId"]), label);
|
|
84
|
+
string(entry.id, `${label}.id`);
|
|
85
|
+
if (!MIGRATION_KINDS.has(entry.kind)) invalid(`${label}.kind is unsupported`);
|
|
86
|
+
exactKeys(entry.input, new Set(["schemaVersions", "beforeDigestRequired"]), `${label}.input`);
|
|
87
|
+
sortedUnique(entry.input.schemaVersions, `${label}.input.schemaVersions`, integer);
|
|
88
|
+
boolean(entry.input.beforeDigestRequired, `${label}.input.beforeDigestRequired`);
|
|
89
|
+
if (!["none", "ai-entry", "projection", "store", "ephemeral-protocol"].includes(entry.targetType)) invalid(`${label}.targetType is invalid`);
|
|
90
|
+
boolean(entry.writes, `${label}.writes`);
|
|
91
|
+
if (!ROLLBACK_CLASSES.has(entry.rollbackClass)) invalid(`${label}.rollbackClass is invalid`);
|
|
92
|
+
if (entry.reverseMigrationId !== null) string(entry.reverseMigrationId, `${label}.reverseMigrationId`);
|
|
93
|
+
if (entry.rollbackClass === "reversible-data" && entry.reverseMigrationId === null) invalid(`${label} requires a reverse migration`);
|
|
94
|
+
if (BUILT_IN_MIGRATIONS[entry.id] !== entry.kind) invalid(`${label} does not match the runtime migration registry`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function validateMigrationManifest(input) {
|
|
98
|
+
const manifest = structuredClone(input);
|
|
99
|
+
exactKeys(manifest, new Set([
|
|
100
|
+
"schemaVersion", "package", "upgradeFrom", "stores", "renderers", "protocols", "paths",
|
|
101
|
+
"builtInMigrations", "consumerChanges", "acceptanceCommands", "rollback", "externalEffects", "manifestDigest",
|
|
102
|
+
]), "migration manifest");
|
|
103
|
+
if (manifest.schemaVersion !== MIGRATION_MANIFEST_SCHEMA_VERSION) invalid("migration manifest schemaVersion must be 2");
|
|
104
|
+
exactKeys(manifest.package, new Set(["name", "version"]), "migration manifest package");
|
|
105
|
+
if (manifest.package.name !== "frontend-project-context" || manifest.package.version !== "1.6.0") invalid("migration manifest package does not match this runtime");
|
|
106
|
+
versions(manifest.upgradeFrom, "upgradeFrom");
|
|
107
|
+
if (manifest.upgradeFrom.length === 0) invalid("upgradeFrom must not be empty");
|
|
108
|
+
exactKeys(manifest.stores, new Set(["contract", "projectionLock", "proposal", "sourceLock"]), "stores");
|
|
109
|
+
for (const name of Object.keys(manifest.stores)) versionMatrix(manifest.stores[name], `stores.${name}`);
|
|
110
|
+
exactKeys(manifest.renderers, new Set(["aiEntry", "projection"]), "renderers");
|
|
111
|
+
for (const name of Object.keys(manifest.renderers)) versionMatrix(manifest.renderers[name], `renderers.${name}`);
|
|
112
|
+
exactKeys(manifest.protocols, new Set([
|
|
113
|
+
"exchange", "actionPlan", "reviewBundle", "evidenceInput", "evidenceBundle", "taskContextPlan",
|
|
114
|
+
"stageReceipt", "stageContextBundle", "integrationReviewBundle",
|
|
115
|
+
]), "protocols");
|
|
116
|
+
for (const name of Object.keys(manifest.protocols)) versionMatrix(manifest.protocols[name], `protocols.${name}`);
|
|
117
|
+
if (!Array.isArray(manifest.builtInMigrations)) invalid("builtInMigrations must be an array");
|
|
118
|
+
manifest.builtInMigrations.forEach(validateMigration);
|
|
119
|
+
const migrationIds = manifest.builtInMigrations.map((entry) => entry.id);
|
|
120
|
+
sortedUnique(migrationIds, "builtInMigrations ids");
|
|
121
|
+
const runtimeMigrationIds = Object.keys(BUILT_IN_MIGRATIONS).sort((left, right) => left.localeCompare(right));
|
|
122
|
+
if (migrationIds.length !== runtimeMigrationIds.length || migrationIds.some((id, index) => id !== runtimeMigrationIds[index])) {
|
|
123
|
+
invalid("builtInMigrations must exactly match the runtime migration registry");
|
|
124
|
+
}
|
|
125
|
+
if (!Array.isArray(manifest.paths) || manifest.paths.length !== manifest.upgradeFrom.length) invalid("paths must contain one path for every upgradeFrom version");
|
|
126
|
+
manifest.paths.forEach((entry, index) => {
|
|
127
|
+
const label = `paths[${index}]`;
|
|
128
|
+
exactKeys(entry, new Set(["fromVersion", "classification", "migrationIds", "requiresHumanReview", "rollbackClass", "acceptance"]), label);
|
|
129
|
+
if (entry.fromVersion !== manifest.upgradeFrom[index]) invalid("paths must be ordered exactly like upgradeFrom");
|
|
130
|
+
if (!VERSION.test(entry.fromVersion)) invalid(`${label}.fromVersion must be exact`);
|
|
131
|
+
if (!MIGRATION_KINDS.has(entry.classification)) invalid(`${label}.classification is invalid`);
|
|
132
|
+
sortedUnique(entry.migrationIds, `${label}.migrationIds`);
|
|
133
|
+
for (const id of entry.migrationIds) if (!migrationIds.includes(id)) invalid(`${label} references unknown migration: ${id}`);
|
|
134
|
+
boolean(entry.requiresHumanReview, `${label}.requiresHumanReview`);
|
|
135
|
+
if (!ROLLBACK_CLASSES.has(entry.rollbackClass)) invalid(`${label}.rollbackClass is invalid`);
|
|
136
|
+
sortedUnique(entry.acceptance, `${label}.acceptance`);
|
|
137
|
+
});
|
|
138
|
+
exactKeys(manifest.consumerChanges, CONSUMER_CHANGE_KEYS, "consumerChanges");
|
|
139
|
+
for (const [name, change] of Object.entries(manifest.consumerChanges)) {
|
|
140
|
+
exactKeys(change, new Set(["state", "reason"]), `consumerChanges.${name}`);
|
|
141
|
+
if (!["preserve", "regenerate", "invalidate"].includes(change.state)) invalid(`consumerChanges.${name}.state is invalid`);
|
|
142
|
+
string(change.reason, `consumerChanges.${name}.reason`);
|
|
143
|
+
}
|
|
144
|
+
sortedUnique(manifest.acceptanceCommands, "acceptanceCommands");
|
|
145
|
+
exactKeys(manifest.rollback, new Set(["defaultClass", "externalRestoreRequired", "automatic"]), "rollback");
|
|
146
|
+
if (!ROLLBACK_CLASSES.has(manifest.rollback.defaultClass)) invalid("rollback.defaultClass is invalid");
|
|
147
|
+
boolean(manifest.rollback.externalRestoreRequired, "rollback.externalRestoreRequired");
|
|
148
|
+
if (manifest.rollback.automatic !== false) invalid("rollback.automatic must be false");
|
|
149
|
+
exactKeys(manifest.externalEffects, new Set(["packageManager", "network", "git", "projectTests", "businessCode", "automaticUpgrade"]), "externalEffects");
|
|
150
|
+
for (const [name, value] of Object.entries(manifest.externalEffects)) if (value !== false) invalid(`externalEffects.${name} must be false`);
|
|
151
|
+
if (!SHA256.test(manifest.manifestDigest)) invalid("manifestDigest must be sha256");
|
|
152
|
+
const unsigned = Object.fromEntries(Object.entries(manifest).filter(([key]) => key !== "manifestDigest"));
|
|
153
|
+
if (digestJson(unsigned) !== manifest.manifestDigest) invalid("manifestDigest does not match manifest bytes");
|
|
154
|
+
return manifest;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function loadMigrationManifest(dependencies = {}) {
|
|
158
|
+
const read = dependencies.readFile ?? readFile;
|
|
159
|
+
let parsed;
|
|
160
|
+
try {
|
|
161
|
+
parsed = JSON.parse(await read(MANIFEST_FILE, "utf8"));
|
|
162
|
+
} catch (error) {
|
|
163
|
+
invalid("cannot read the package migration manifest", { reason: error.message });
|
|
164
|
+
}
|
|
165
|
+
return validateMigrationManifest(parsed);
|
|
166
|
+
}
|