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.
Files changed (119) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/CHANGELOG.zh-CN.md +22 -0
  3. package/README.md +12 -10
  4. package/README.zh-CN.md +12 -10
  5. package/package.json +3 -3
  6. package/release-notes/0.6.0.yaml +25 -0
  7. package/src/adopt-plan.mjs +81 -159
  8. package/src/check.mjs +443 -19
  9. package/src/cli.mjs +80 -1
  10. package/src/core-check.mjs +8 -4
  11. package/src/entry-check.mjs +384 -0
  12. package/src/errors.mjs +21 -0
  13. package/src/index.mjs +35 -6
  14. package/src/projection.mjs +221 -12
  15. package/src/relock.mjs +397 -0
  16. package/src/skeleton.mjs +162 -4
  17. package/CODE_OF_CONDUCT.md +0 -131
  18. package/CONTRIBUTING.md +0 -25
  19. package/SECURITY.md +0 -33
  20. package/docs/.nojekyll +0 -0
  21. package/docs/404.html +0 -1666
  22. package/docs/agents/architecture-routing/index.html +0 -1944
  23. package/docs/agents/capability-catalog.en.json +0 -1821
  24. package/docs/agents/capability-catalog.json +0 -1073
  25. package/docs/agents/capability-catalog.schema.json +0 -179
  26. package/docs/agents/capability-catalog.zh-CN.json +0 -1821
  27. package/docs/agents/index.html +0 -1914
  28. package/docs/architecture/index.html +0 -2194
  29. package/docs/assets/images/favicon.png +0 -0
  30. package/docs/assets/javascripts/bundle.d7400e89.min.js +0 -16
  31. package/docs/assets/javascripts/lunr/min/lunr.ar.min.js +0 -1
  32. package/docs/assets/javascripts/lunr/min/lunr.da.min.js +0 -18
  33. package/docs/assets/javascripts/lunr/min/lunr.de.min.js +0 -18
  34. package/docs/assets/javascripts/lunr/min/lunr.du.min.js +0 -18
  35. package/docs/assets/javascripts/lunr/min/lunr.el.min.js +0 -1
  36. package/docs/assets/javascripts/lunr/min/lunr.es.min.js +0 -18
  37. package/docs/assets/javascripts/lunr/min/lunr.fi.min.js +0 -18
  38. package/docs/assets/javascripts/lunr/min/lunr.fr.min.js +0 -18
  39. package/docs/assets/javascripts/lunr/min/lunr.he.min.js +0 -1
  40. package/docs/assets/javascripts/lunr/min/lunr.hi.min.js +0 -1
  41. package/docs/assets/javascripts/lunr/min/lunr.hu.min.js +0 -18
  42. package/docs/assets/javascripts/lunr/min/lunr.hy.min.js +0 -1
  43. package/docs/assets/javascripts/lunr/min/lunr.it.min.js +0 -18
  44. package/docs/assets/javascripts/lunr/min/lunr.ja.min.js +0 -1
  45. package/docs/assets/javascripts/lunr/min/lunr.jp.min.js +0 -1
  46. package/docs/assets/javascripts/lunr/min/lunr.kn.min.js +0 -1
  47. package/docs/assets/javascripts/lunr/min/lunr.ko.min.js +0 -1
  48. package/docs/assets/javascripts/lunr/min/lunr.multi.min.js +0 -1
  49. package/docs/assets/javascripts/lunr/min/lunr.nl.min.js +0 -18
  50. package/docs/assets/javascripts/lunr/min/lunr.no.min.js +0 -18
  51. package/docs/assets/javascripts/lunr/min/lunr.pt.min.js +0 -18
  52. package/docs/assets/javascripts/lunr/min/lunr.ro.min.js +0 -18
  53. package/docs/assets/javascripts/lunr/min/lunr.ru.min.js +0 -18
  54. package/docs/assets/javascripts/lunr/min/lunr.sa.min.js +0 -1
  55. package/docs/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -1
  56. package/docs/assets/javascripts/lunr/min/lunr.sv.min.js +0 -18
  57. package/docs/assets/javascripts/lunr/min/lunr.ta.min.js +0 -1
  58. package/docs/assets/javascripts/lunr/min/lunr.te.min.js +0 -1
  59. package/docs/assets/javascripts/lunr/min/lunr.th.min.js +0 -1
  60. package/docs/assets/javascripts/lunr/min/lunr.tr.min.js +0 -18
  61. package/docs/assets/javascripts/lunr/min/lunr.vi.min.js +0 -1
  62. package/docs/assets/javascripts/lunr/min/lunr.zh.min.js +0 -1
  63. package/docs/assets/javascripts/lunr/tinyseg.js +0 -206
  64. package/docs/assets/javascripts/lunr/wordcut.js +0 -6708
  65. package/docs/assets/javascripts/workers/search.2c215733.min.js +0 -42
  66. package/docs/assets/stylesheets/main.ec1eaa64.min.css +0 -1
  67. package/docs/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
  68. package/docs/en/agents/architecture-routing/index.html +0 -1944
  69. package/docs/en/agents/index.html +0 -1925
  70. package/docs/en/architecture/index.html +0 -2196
  71. package/docs/en/examples-and-fixtures/index.html +0 -1888
  72. package/docs/en/help/index.html +0 -2035
  73. package/docs/en/index.html +0 -1895
  74. package/docs/en/licensing/index.html +0 -1917
  75. package/docs/en/migration/index.html +0 -2335
  76. package/docs/en/quickstart/index.html +0 -1919
  77. package/docs/en/recipes/adapter-text-closure/index.html +0 -2000
  78. package/docs/en/recipes/adopt-existing-repository/index.html +0 -1987
  79. package/docs/en/recipes/deterministic-human-report/index.html +0 -1998
  80. package/docs/en/recipes/domain-schema-validation/index.html +0 -2009
  81. package/docs/en/recipes/durable-local-state/index.html +0 -2009
  82. package/docs/en/recipes/host-profile-integration/index.html +0 -2001
  83. package/docs/en/recipes/index.html +0 -1877
  84. package/docs/en/recipes/safe-filesystem-and-atomic-write/index.html +0 -1994
  85. package/docs/en/reference/api/index.html +0 -1941
  86. package/docs/en/reference/compatibility/index.html +0 -2036
  87. package/docs/en/reference/failure-and-side-effect-matrix/index.html +0 -2216
  88. package/docs/examples-and-fixtures/index.html +0 -1871
  89. package/docs/git-lifecycle/index.html +0 -2029
  90. package/docs/help/index.html +0 -2035
  91. package/docs/index.html +0 -1893
  92. package/docs/integration/audit/baseline/audit-codes.json +0 -62
  93. package/docs/integration/audit/failure-evidence/index.html +0 -1925
  94. package/docs/integration/audit/independence/index.html +0 -1894
  95. package/docs/integration/audit/index.html +0 -1880
  96. package/docs/integration/audit/mutation-taxonomy/index.html +0 -2075
  97. package/docs/integration/audit/schemas/audit-evidence.schema.json +0 -182
  98. package/docs/integration/audit/version-compatibility/index.html +0 -1902
  99. package/docs/licensing/index.html +0 -1917
  100. package/docs/migration/index.html +0 -2335
  101. package/docs/public/status/index.html +0 -1884
  102. package/docs/quickstart/index.html +0 -1919
  103. package/docs/recipes/adapter-text-closure/index.html +0 -2000
  104. package/docs/recipes/adopt-existing-repository/index.html +0 -1987
  105. package/docs/recipes/deterministic-human-report/index.html +0 -1998
  106. package/docs/recipes/domain-schema-validation/index.html +0 -2009
  107. package/docs/recipes/durable-local-state/index.html +0 -2009
  108. package/docs/recipes/host-profile-integration/index.html +0 -2001
  109. package/docs/recipes/index.html +0 -1877
  110. package/docs/recipes/safe-filesystem-and-atomic-write/index.html +0 -1994
  111. package/docs/reference/api/contracts/index.html +0 -2558
  112. package/docs/reference/api/engineering-kit/index.html +0 -2577
  113. package/docs/reference/api/harness/index.html +0 -3029
  114. package/docs/reference/api/index.html +0 -1881
  115. package/docs/reference/compatibility/index.html +0 -2036
  116. package/docs/reference/failure-and-side-effect-matrix/index.html +0 -2216
  117. package/docs/search/search_index.json +0 -1
  118. package/docs/setup/index.html +0 -1995
  119. package/docs/sitemap.xml +0 -207
@@ -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 resolveContained(rootAbs, rel);
684
+ receipt = await readFileStrict(authorityReal, rel);
487
685
  } catch (cause) {
488
- throw invalidManifest("preparedProjection authority source escapes the target root", { path: rel, cause: cause?.message ?? "unknown" });
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
- const state = await readRegularState(rootAbs, rel);
491
- if (state.state !== source.type || state.sha256 !== source.sha256 || state.mode !== source.mode) {
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
- actual: publicState(state),
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
- await verifyPreparedAuthority(rootAbs, canonical);
560
- await verifyPreparedPreviousClosure(rootAbs, canonical);
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 });