skill-family-engineering-kit 0.5.0 → 0.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 +22 -0
- package/CHANGELOG.zh-CN.md +22 -0
- package/README.md +12 -10
- package/README.zh-CN.md +12 -10
- package/package.json +3 -3
- package/release-notes/0.6.0.yaml +25 -0
- package/src/adopt-plan.mjs +81 -159
- package/src/check.mjs +443 -19
- package/src/cli.mjs +80 -1
- package/src/core-check.mjs +8 -4
- package/src/entry-check.mjs +384 -0
- package/src/errors.mjs +21 -0
- package/src/index.mjs +35 -6
- package/src/projection.mjs +221 -12
- package/src/relock.mjs +397 -0
- package/src/skeleton.mjs +162 -4
- package/CODE_OF_CONDUCT.md +0 -131
- package/CONTRIBUTING.md +0 -25
- package/SECURITY.md +0 -33
- package/docs/.nojekyll +0 -0
- package/docs/404.html +0 -1666
- package/docs/agents/architecture-routing/index.html +0 -1944
- package/docs/agents/capability-catalog.en.json +0 -1821
- package/docs/agents/capability-catalog.json +0 -1073
- package/docs/agents/capability-catalog.schema.json +0 -179
- package/docs/agents/capability-catalog.zh-CN.json +0 -1821
- package/docs/agents/index.html +0 -1914
- package/docs/architecture/index.html +0 -2194
- package/docs/assets/images/favicon.png +0 -0
- package/docs/assets/javascripts/bundle.d7400e89.min.js +0 -16
- package/docs/assets/javascripts/lunr/min/lunr.ar.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.da.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.de.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.du.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.el.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.es.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.fi.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.fr.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.he.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.hi.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.hu.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.hy.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.it.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.ja.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.jp.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.kn.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.ko.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.multi.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.nl.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.no.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.pt.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.ro.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.ru.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.sa.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.sv.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.ta.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.te.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.th.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.tr.min.js +0 -18
- package/docs/assets/javascripts/lunr/min/lunr.vi.min.js +0 -1
- package/docs/assets/javascripts/lunr/min/lunr.zh.min.js +0 -1
- package/docs/assets/javascripts/lunr/tinyseg.js +0 -206
- package/docs/assets/javascripts/lunr/wordcut.js +0 -6708
- package/docs/assets/javascripts/workers/search.2c215733.min.js +0 -42
- package/docs/assets/stylesheets/main.ec1eaa64.min.css +0 -1
- package/docs/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
- package/docs/en/agents/architecture-routing/index.html +0 -1944
- package/docs/en/agents/index.html +0 -1925
- package/docs/en/architecture/index.html +0 -2196
- package/docs/en/examples-and-fixtures/index.html +0 -1888
- package/docs/en/help/index.html +0 -2035
- package/docs/en/index.html +0 -1895
- package/docs/en/licensing/index.html +0 -1917
- package/docs/en/migration/index.html +0 -2335
- package/docs/en/quickstart/index.html +0 -1919
- package/docs/en/recipes/adapter-text-closure/index.html +0 -2000
- package/docs/en/recipes/adopt-existing-repository/index.html +0 -1987
- package/docs/en/recipes/deterministic-human-report/index.html +0 -1998
- package/docs/en/recipes/domain-schema-validation/index.html +0 -2009
- package/docs/en/recipes/durable-local-state/index.html +0 -2009
- package/docs/en/recipes/host-profile-integration/index.html +0 -2001
- package/docs/en/recipes/index.html +0 -1877
- package/docs/en/recipes/safe-filesystem-and-atomic-write/index.html +0 -1994
- package/docs/en/reference/api/index.html +0 -1941
- package/docs/en/reference/compatibility/index.html +0 -2036
- package/docs/en/reference/failure-and-side-effect-matrix/index.html +0 -2216
- package/docs/examples-and-fixtures/index.html +0 -1871
- package/docs/git-lifecycle/index.html +0 -2029
- package/docs/help/index.html +0 -2035
- package/docs/index.html +0 -1893
- package/docs/integration/audit/baseline/audit-codes.json +0 -62
- package/docs/integration/audit/failure-evidence/index.html +0 -1925
- package/docs/integration/audit/independence/index.html +0 -1894
- package/docs/integration/audit/index.html +0 -1880
- package/docs/integration/audit/mutation-taxonomy/index.html +0 -2075
- package/docs/integration/audit/schemas/audit-evidence.schema.json +0 -182
- package/docs/integration/audit/version-compatibility/index.html +0 -1902
- package/docs/licensing/index.html +0 -1917
- package/docs/migration/index.html +0 -2335
- package/docs/public/status/index.html +0 -1884
- package/docs/quickstart/index.html +0 -1919
- package/docs/recipes/adapter-text-closure/index.html +0 -2000
- package/docs/recipes/adopt-existing-repository/index.html +0 -1987
- package/docs/recipes/deterministic-human-report/index.html +0 -1998
- package/docs/recipes/domain-schema-validation/index.html +0 -2009
- package/docs/recipes/durable-local-state/index.html +0 -2009
- package/docs/recipes/host-profile-integration/index.html +0 -2001
- package/docs/recipes/index.html +0 -1877
- package/docs/recipes/safe-filesystem-and-atomic-write/index.html +0 -1994
- package/docs/reference/api/contracts/index.html +0 -2558
- package/docs/reference/api/engineering-kit/index.html +0 -2577
- package/docs/reference/api/harness/index.html +0 -3029
- package/docs/reference/api/index.html +0 -1881
- package/docs/reference/compatibility/index.html +0 -2036
- package/docs/reference/failure-and-side-effect-matrix/index.html +0 -2216
- package/docs/search/search_index.json +0 -1
- package/docs/setup/index.html +0 -1995
- package/docs/sitemap.xml +0 -207
package/src/projection.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
classifyPathInput,
|
|
18
18
|
computeResourceClosure,
|
|
19
19
|
digestBytes,
|
|
20
|
+
readFileStrict,
|
|
20
21
|
resolveContained,
|
|
21
22
|
writeFileAtomic,
|
|
22
23
|
} from "skill-family-harness-node";
|
|
@@ -220,11 +221,145 @@ function normalizeAuthoritySources(rawSources, candidatePaths) {
|
|
|
220
221
|
return { sources, ids, paths, digestAlgorithm: "sha256", digest: canonicalDigest(sources) };
|
|
221
222
|
}
|
|
222
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Authority binding kinds for projections whose authority does not live in
|
|
226
|
+
* the target root (FG-3). "external-root" re-reads each authority source from
|
|
227
|
+
* a separate frozen directory using the strict no-follow reader; "caller-bytes"
|
|
228
|
+
* binds caller-provided base64 authority bytes to the declared sha256 digests
|
|
229
|
+
* with no authority filesystem access at all. Both keep the target root free
|
|
230
|
+
* of forged local authority facts.
|
|
231
|
+
*/
|
|
232
|
+
export const PROJECTION_AUTHORITY_BINDING_KINDS = Object.freeze(["external-root", "caller-bytes"]);
|
|
233
|
+
|
|
234
|
+
const BASE64_PATTERN = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
235
|
+
|
|
236
|
+
function normalizeAuthorityRoot(rawRoot, rootBinding) {
|
|
237
|
+
if (typeof rawRoot !== "string" || !path.isAbsolute(rawRoot) || rawRoot.includes("\0")) {
|
|
238
|
+
projectionPlanInputError("authorityBinding.authorityRoot must be a canonical absolute path");
|
|
239
|
+
}
|
|
240
|
+
const normalized = path.normalize(rawRoot);
|
|
241
|
+
if (normalized !== rawRoot) {
|
|
242
|
+
projectionPlanInputError("authorityBinding.authorityRoot must already be normalized", {
|
|
243
|
+
authorityRoot: rawRoot,
|
|
244
|
+
normalized,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (normalized === rootBinding) {
|
|
248
|
+
projectionPlanInputError("authorityBinding.authorityRoot must be external to the target rootBinding");
|
|
249
|
+
}
|
|
250
|
+
const rootFromAuthority = path.relative(normalized, rootBinding);
|
|
251
|
+
const authorityFromRoot = path.relative(rootBinding, normalized);
|
|
252
|
+
if (
|
|
253
|
+
rootFromAuthority === "" || authorityFromRoot === ""
|
|
254
|
+
|| !rootFromAuthority.startsWith("..") || !authorityFromRoot.startsWith("..")
|
|
255
|
+
) {
|
|
256
|
+
projectionPlanInputError("authorityBinding.authorityRoot must be external to the target rootBinding", {
|
|
257
|
+
authorityRoot: normalized,
|
|
258
|
+
rootBinding,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return normalized;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function normalizeCallerBytes(rawBytes, authority) {
|
|
265
|
+
if (!rawBytes || typeof rawBytes !== "object" || Array.isArray(rawBytes)) {
|
|
266
|
+
projectionPlanInputError("authorityBinding.bytes must map every authority source id to base64 bytes");
|
|
267
|
+
}
|
|
268
|
+
const provided = new Set(Object.keys(rawBytes));
|
|
269
|
+
for (const source of authority.sources) {
|
|
270
|
+
if (!provided.has(source.id)) {
|
|
271
|
+
projectionPlanInputError("authorityBinding.bytes must provide bytes for every authority source", { id: source.id });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (const id of provided) {
|
|
275
|
+
if (!authority.ids.has(id)) {
|
|
276
|
+
projectionPlanInputError("authorityBinding.bytes names an unknown authority source id", { id });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const normalized = {};
|
|
280
|
+
for (const source of authority.sources) {
|
|
281
|
+
const base64 = rawBytes[source.id];
|
|
282
|
+
if (typeof base64 !== "string" || !BASE64_PATTERN.test(base64)) {
|
|
283
|
+
projectionPlanInputError("authorityBinding.bytes entries must be base64 strings", { id: source.id });
|
|
284
|
+
}
|
|
285
|
+
const bytes = Buffer.from(base64, "base64");
|
|
286
|
+
if (bytes.toString("base64") !== base64) {
|
|
287
|
+
projectionPlanInputError("authorityBinding.bytes entries must be canonical base64", { id: source.id });
|
|
288
|
+
}
|
|
289
|
+
if (digestBytes(bytes) !== source.sha256) {
|
|
290
|
+
projectionPlanInputError("authorityBinding.bytes digest differs from the declared authority source digest", {
|
|
291
|
+
id: source.id,
|
|
292
|
+
expected: source.sha256,
|
|
293
|
+
actual: digestBytes(bytes),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
normalized[source.id] = base64;
|
|
297
|
+
}
|
|
298
|
+
return normalized;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Validates and normalizes the optional authority binding (FG-3). Pure: no
|
|
303
|
+
* filesystem access. Returns null when no binding is supplied (legacy
|
|
304
|
+
* target-local authority). The normalized binding is embedded in the plan so
|
|
305
|
+
* the prepared run can re-verify authority outside the target root.
|
|
306
|
+
*/
|
|
307
|
+
function normalizeAuthorityBinding(rawBinding, { rootBinding, authority, previous }) {
|
|
308
|
+
if (rawBinding === undefined) return null;
|
|
309
|
+
if (!rawBinding || typeof rawBinding !== "object" || Array.isArray(rawBinding)) {
|
|
310
|
+
projectionPlanInputError("authorityBinding must be an object");
|
|
311
|
+
}
|
|
312
|
+
const kind = rawBinding.kind;
|
|
313
|
+
if (!PROJECTION_AUTHORITY_BINDING_KINDS.includes(kind)) {
|
|
314
|
+
projectionPlanInputError(`authorityBinding.kind must be one of: ${PROJECTION_AUTHORITY_BINDING_KINDS.join(", ")}`);
|
|
315
|
+
}
|
|
316
|
+
const freshRoot = rawBinding.freshRoot === undefined ? false : rawBinding.freshRoot;
|
|
317
|
+
if (typeof freshRoot !== "boolean") {
|
|
318
|
+
projectionPlanInputError("authorityBinding.freshRoot must be a boolean");
|
|
319
|
+
}
|
|
320
|
+
if (freshRoot && previous.resourceCount !== 0) {
|
|
321
|
+
projectionPlanInputError("authorityBinding.freshRoot requires an empty previousOwnedClosure");
|
|
322
|
+
}
|
|
323
|
+
if (kind === "external-root") {
|
|
324
|
+
const extra = Object.keys(rawBinding).filter((key) => !["kind", "authorityRoot", "freshRoot"].includes(key));
|
|
325
|
+
if (extra.length > 0) projectionPlanInputError("authorityBinding declares unknown fields", { fields: extra.sort() });
|
|
326
|
+
const authorityRoot = normalizeAuthorityRoot(rawBinding.authorityRoot, rootBinding);
|
|
327
|
+
return Object.freeze({ kind, authorityRoot, freshRoot });
|
|
328
|
+
}
|
|
329
|
+
const extra = Object.keys(rawBinding).filter((key) => !["kind", "bytes", "freshRoot"].includes(key));
|
|
330
|
+
if (extra.length > 0) projectionPlanInputError("authorityBinding declares unknown fields", { fields: extra.sort() });
|
|
331
|
+
const bytes = normalizeCallerBytes(rawBinding.bytes, authority);
|
|
332
|
+
return Object.freeze({ kind, bytes, freshRoot });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function assertExternalRoots(firstReal, secondReal, message) {
|
|
336
|
+
const firstFromSecond = path.relative(secondReal, firstReal);
|
|
337
|
+
const secondFromFirst = path.relative(firstReal, secondReal);
|
|
338
|
+
if (
|
|
339
|
+
firstFromSecond === "" || secondFromFirst === ""
|
|
340
|
+
|| !firstFromSecond.startsWith("..") || !secondFromFirst.startsWith("..")
|
|
341
|
+
) {
|
|
342
|
+
throw invalidManifest(message, { first: firstReal, second: secondReal });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
223
346
|
/**
|
|
224
347
|
* Purely compiles frozen consumer authority into target-fact and projection
|
|
225
348
|
* descriptions. It performs no filesystem access. runProjection reloads the
|
|
226
349
|
* target-owned facts by default, or accepts this complete canonical result
|
|
227
350
|
* after independently binding it to the live target and candidate closures.
|
|
351
|
+
*
|
|
352
|
+
* Authority binding (FG-3): by default authority sources must live inside the
|
|
353
|
+
* target root (verified live at run time). The optional `authorityBinding`
|
|
354
|
+
* input declares that authority is frozen OUTSIDE the target root:
|
|
355
|
+
* - { kind: "external-root", authorityRoot, freshRoot? } — authority is
|
|
356
|
+
* re-read from a separate frozen directory (strict, no-follow);
|
|
357
|
+
* - { kind: "caller-bytes", bytes, freshRoot? } — the caller supplies the
|
|
358
|
+
* authority bytes and the plan binds them to the declared digests.
|
|
359
|
+
* freshRoot declares a completely fresh payload root and requires an empty
|
|
360
|
+
* previousOwnedClosure. With either binding the projection never reads or
|
|
361
|
+
* forges target-local authority facts. Legacy inputs without authorityBinding
|
|
362
|
+
* compile to byte-identical plans.
|
|
228
363
|
*/
|
|
229
364
|
export function compileProjectionPlan({
|
|
230
365
|
rootBinding,
|
|
@@ -233,6 +368,7 @@ export function compileProjectionPlan({
|
|
|
233
368
|
handwrittenPolicy,
|
|
234
369
|
previousOwnedClosure,
|
|
235
370
|
externalCandidateClosure,
|
|
371
|
+
authorityBinding,
|
|
236
372
|
} = {}) {
|
|
237
373
|
const canonicalRootBinding = normalizeRootBinding(rootBinding);
|
|
238
374
|
const previous = normalizePlanClosure(previousOwnedClosure, "previousOwnedClosure");
|
|
@@ -240,6 +376,11 @@ export function compileProjectionPlan({
|
|
|
240
376
|
const previousByPath = new Map(previous.resources.map((resource) => [resource.path, resource]));
|
|
241
377
|
const candidateByPath = new Map(candidate.resources.map((resource) => [resource.path, resource]));
|
|
242
378
|
const authority = normalizeAuthoritySources(authoritySources, new Set(candidateByPath.keys()));
|
|
379
|
+
const binding = normalizeAuthorityBinding(authorityBinding, {
|
|
380
|
+
rootBinding: canonicalRootBinding,
|
|
381
|
+
authority,
|
|
382
|
+
previous,
|
|
383
|
+
});
|
|
243
384
|
|
|
244
385
|
if (!handwrittenPolicy || typeof handwrittenPolicy !== "object" || Array.isArray(handwrittenPolicy)) {
|
|
245
386
|
projectionPlanInputError("handwrittenPolicy must be an object");
|
|
@@ -400,6 +541,7 @@ export function compileProjectionPlan({
|
|
|
400
541
|
digestAlgorithm: authority.digestAlgorithm,
|
|
401
542
|
digest: authority.digest,
|
|
402
543
|
handwrittenPolicySource: handwrittenPolicy.authoritySource,
|
|
544
|
+
...(binding === null ? {} : { binding }),
|
|
403
545
|
},
|
|
404
546
|
handwrittenPolicy: {
|
|
405
547
|
authoritySource: handwrittenPolicy.authoritySource,
|
|
@@ -455,6 +597,7 @@ function preparedPlanInput(prepared) {
|
|
|
455
597
|
}),
|
|
456
598
|
previousOwnedClosure: prepared.previousOwnedClosure,
|
|
457
599
|
externalCandidateClosure: prepared.externalCandidateClosure,
|
|
600
|
+
authorityBinding: prepared.authority.binding,
|
|
458
601
|
};
|
|
459
602
|
}
|
|
460
603
|
|
|
@@ -479,22 +622,79 @@ function protectedPreparedPaths(prepared) {
|
|
|
479
622
|
].map(portableKey));
|
|
480
623
|
}
|
|
481
624
|
|
|
482
|
-
async function verifyPreparedAuthority(rootAbs, prepared) {
|
|
625
|
+
async function verifyPreparedAuthority(rootAbs, prepared, candidateAbs = null) {
|
|
626
|
+
const binding = prepared.authority.binding;
|
|
627
|
+
if (binding === undefined) {
|
|
628
|
+
for (const source of prepared.authority.sources) {
|
|
629
|
+
const rel = normalizeRelPath(source.path);
|
|
630
|
+
try {
|
|
631
|
+
await resolveContained(rootAbs, rel);
|
|
632
|
+
} catch (cause) {
|
|
633
|
+
throw invalidManifest("preparedProjection authority source escapes the target root", { path: rel, cause: cause?.message ?? "unknown" });
|
|
634
|
+
}
|
|
635
|
+
const state = await readRegularState(rootAbs, rel);
|
|
636
|
+
if (state.state !== source.type || state.sha256 !== source.sha256 || state.mode !== source.mode) {
|
|
637
|
+
throw invalidManifest("preparedProjection authority source differs from the live target", {
|
|
638
|
+
path: rel,
|
|
639
|
+
expectedSha256: source.sha256,
|
|
640
|
+
expectedType: source.type,
|
|
641
|
+
expectedMode: source.mode,
|
|
642
|
+
actual: publicState(state),
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (binding.kind === "caller-bytes") {
|
|
649
|
+
for (const source of prepared.authority.sources) {
|
|
650
|
+
const bytes = Buffer.from(binding.bytes[source.id], "base64");
|
|
651
|
+
if (digestBytes(bytes) !== source.sha256) {
|
|
652
|
+
throw invalidManifest("preparedProjection caller-provided authority bytes differ from the frozen digest binding", {
|
|
653
|
+
id: source.id,
|
|
654
|
+
expectedSha256: source.sha256,
|
|
655
|
+
actualSha256: digestBytes(bytes),
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
// external-root binding: authority is frozen outside the target root and is
|
|
662
|
+
// re-read strictly (no-follow, regular-file identity, digest) from there.
|
|
663
|
+
let authorityReal;
|
|
664
|
+
try {
|
|
665
|
+
authorityReal = await realpath(binding.authorityRoot);
|
|
666
|
+
} catch {
|
|
667
|
+
throw invalidManifest("preparedProjection authority root does not resolve to an existing path", {
|
|
668
|
+
authorityRoot: binding.authorityRoot,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
const authorityStats = await stat(authorityReal);
|
|
672
|
+
if (!authorityStats.isDirectory()) {
|
|
673
|
+
throw invalidManifest("preparedProjection authority root must be a directory", { authorityRoot: authorityReal });
|
|
674
|
+
}
|
|
675
|
+
const targetReal = await realpath(rootAbs);
|
|
676
|
+
assertExternalRoots(authorityReal, targetReal, "preparedProjection authority root must be external to the target root");
|
|
677
|
+
if (candidateAbs !== null) {
|
|
678
|
+
assertExternalRoots(authorityReal, candidateAbs, "preparedProjection authority root must be external to the candidate root");
|
|
679
|
+
}
|
|
483
680
|
for (const source of prepared.authority.sources) {
|
|
484
681
|
const rel = normalizeRelPath(source.path);
|
|
682
|
+
let receipt;
|
|
485
683
|
try {
|
|
486
|
-
await
|
|
684
|
+
receipt = await readFileStrict(authorityReal, rel);
|
|
487
685
|
} catch (cause) {
|
|
488
|
-
throw invalidManifest("preparedProjection authority source
|
|
686
|
+
throw invalidManifest("preparedProjection authority source differs from the frozen external authority root", {
|
|
687
|
+
path: rel,
|
|
688
|
+
cause: cause?.details?.kind ?? cause?.message ?? "unknown",
|
|
689
|
+
});
|
|
489
690
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
throw invalidManifest("preparedProjection authority source differs from the live target", {
|
|
691
|
+
if (receipt.sha256 !== source.sha256 || receipt.mode !== source.mode) {
|
|
692
|
+
throw invalidManifest("preparedProjection authority source differs from the frozen external authority root", {
|
|
493
693
|
path: rel,
|
|
494
694
|
expectedSha256: source.sha256,
|
|
495
|
-
expectedType: source.type,
|
|
496
695
|
expectedMode: source.mode,
|
|
497
|
-
|
|
696
|
+
actualSha256: receipt.sha256,
|
|
697
|
+
actualMode: receipt.mode,
|
|
498
698
|
});
|
|
499
699
|
}
|
|
500
700
|
}
|
|
@@ -556,9 +756,18 @@ async function validatePreparedProjection({ prepared, rootAbs, loaded, candidate
|
|
|
556
756
|
throw invalidManifest("preparedProjection candidate may not supply an authority or target-facts path", { path: operation.source });
|
|
557
757
|
}
|
|
558
758
|
}
|
|
559
|
-
|
|
560
|
-
|
|
759
|
+
const binding = canonical.authority.binding;
|
|
760
|
+
if (binding && binding.freshRoot) {
|
|
761
|
+
const existing = await readdir(rootAbs);
|
|
762
|
+
if (existing.length > 0) {
|
|
763
|
+
throw invalidManifest("preparedProjection freshRoot requires a completely empty target root", {
|
|
764
|
+
found: existing.sort(),
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
561
768
|
const candidate = await verifyCandidateRoot(candidateRoot, rootAbs, loaded.manifest);
|
|
769
|
+
await verifyPreparedAuthority(rootAbs, canonical, candidate.rootAbs);
|
|
770
|
+
await verifyPreparedPreviousClosure(rootAbs, canonical);
|
|
562
771
|
if (!isDeepStrictEqual(canonicalCandidateClosure(candidate), canonical.externalCandidateClosure)) {
|
|
563
772
|
throw invalidManifest("preparedProjection external candidate closure differs from candidateRoot");
|
|
564
773
|
}
|
|
@@ -1051,9 +1260,9 @@ async function runProjectionV2({ rootAbs, facts, manifestPath, manifest, externa
|
|
|
1051
1260
|
|
|
1052
1261
|
try {
|
|
1053
1262
|
for (const [executionIndex, action] of actions.entries()) {
|
|
1054
|
-
if (preparedProjection) await verifyPreparedAuthority(rootAbs, preparedProjection);
|
|
1263
|
+
if (preparedProjection) await verifyPreparedAuthority(rootAbs, preparedProjection, candidate.rootAbs);
|
|
1055
1264
|
await invokeFault(faultInjector, "beforeMutation", { index: executionIndex, operation: action.publicOperation });
|
|
1056
|
-
if (preparedProjection) await verifyPreparedAuthority(rootAbs, preparedProjection);
|
|
1265
|
+
if (preparedProjection) await verifyPreparedAuthority(rootAbs, preparedProjection, candidate.rootAbs);
|
|
1057
1266
|
const current = await readRegularState(rootAbs, action.rel);
|
|
1058
1267
|
if (!stateEquals(current, action.before)) {
|
|
1059
1268
|
throw kitError(KIT_ERROR_KINDS.CONFLICT_DRIFT, `optimistic concurrency drift before ${action.rel}`, { path: action.rel, executionIndex });
|