@rightkit/release 0.6.10 → 0.6.12

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 (69) hide show
  1. package/README.md +7 -5
  2. package/admission-files.mjs +3 -0
  3. package/admission.mjs +2 -2
  4. package/advisory-exceptions.mjs +41 -5
  5. package/android-play-upload.mjs +16 -15
  6. package/app-check.mjs +13 -9
  7. package/bin/rightkit-gate.mjs +772 -355
  8. package/broker-pins.mjs +152 -0
  9. package/build-release.mjs +80 -27
  10. package/cargo-ledger.mjs +62 -0
  11. package/cargo-target-shim.mjs +98 -26
  12. package/cli/right-release.mjs +28 -4
  13. package/clippy-equivalence.mjs +202 -0
  14. package/clippy-route.mjs +543 -0
  15. package/committed-source.mjs +39 -5
  16. package/crate-workspaces.mjs +87 -0
  17. package/create-mac-updater.mjs +14 -2
  18. package/deterministic-scans.mjs +196 -21
  19. package/development.mjs +13 -4
  20. package/direct-receipt.mjs +135 -0
  21. package/door-policy.mjs +10 -0
  22. package/door.mjs +6 -6
  23. package/freshness-census.mjs +257 -0
  24. package/gate-stages.mjs +76 -10
  25. package/hook-retirement.mjs +65 -0
  26. package/init-command.mjs +1 -1
  27. package/inventory-check.mjs +11 -8
  28. package/ios-upload.mjs +20 -15
  29. package/local-publish.mjs +77 -41
  30. package/local-qualify.mjs +5 -3
  31. package/local-runtime.mjs +5 -4
  32. package/mac-package-recipe.mjs +4 -1
  33. package/mirror-root-artifact.mjs +18 -5
  34. package/ownership-class-rules.mjs +1 -1
  35. package/ownership-evidence.mjs +37 -13
  36. package/ownership-hooks.mjs +20 -59
  37. package/ownership-lockfiles.mjs +14 -1
  38. package/ownership-policy-bundle.mjs +3 -2
  39. package/ownership-staged.mjs +2 -1
  40. package/ownership.mjs +24 -0
  41. package/package-recipe-targets.mjs +35 -6
  42. package/package.json +1 -1
  43. package/pipeline-admission.mjs +4 -4
  44. package/preflight.mjs +27 -4
  45. package/publish-cargo.mjs +4 -3
  46. package/recipe-build.mjs +38 -14
  47. package/release-artifact-path.mjs +341 -0
  48. package/release-invocation.mjs +8 -1
  49. package/release-qa-command.mjs +25 -9
  50. package/release-source-brand.mjs +17 -0
  51. package/release-source-identity.mjs +171 -0
  52. package/release.mjs +44 -17
  53. package/rightkit-versions.json +63 -42
  54. package/runtime-budgets.mjs +6 -2
  55. package/scan-digest.mjs +71 -5
  56. package/sign-updater.mjs +6 -3
  57. package/snapshot-broker.mjs +172 -0
  58. package/snapshot-check.mjs +12 -6
  59. package/snapshot-request.mjs +207 -5
  60. package/snapshot.mjs +123 -10
  61. package/source-split.mjs +73 -0
  62. package/stage-commands.mjs +2 -2
  63. package/stage-ledger.mjs +204 -0
  64. package/stage-sidecar-command.mjs +5 -1
  65. package/stage-sidecar.mjs +100 -20
  66. package/swap-helper-bundle.mjs +5 -2
  67. package/unit-gate.mjs +45 -3
  68. package/web-build.mjs +6 -4
  69. package/win-package-recipe.mjs +20 -7
package/release.mjs CHANGED
@@ -14,6 +14,8 @@ import { assertQaAdoption, assertQaBackdoorContract } from "./qa-contract.mjs";
14
14
  import { assertLegalReleaseContract } from "./legal-contract.mjs";
15
15
  import { DECLARED_UNRELATED_POLICY, declaredUnrelatedRefusal, releaseArtifactPaths } from "./hosted-workflow-policy.mjs";
16
16
  import { assertNativeIosPrepareHasManifest, assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
17
+ import { resolveReleaseSource, sourceRootOf } from "./release-source-identity.mjs";
18
+ import { ArtifactPathError, assertSplitCommandPaths, releaseCommandSteps, resolveArtifactPath, splitArtifactContext } from "./release-artifact-path.mjs";
17
19
  import { declaredCargoManifest, hasDefaultTauriManifest, resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
18
20
  import {
19
21
  canonicalPackageIdentity,
@@ -275,10 +277,17 @@ const nativeFinalizer = resolveNativeFinalizerConfig(nativeAssembly);
275
277
  // state root and admission record; a snapshot from anywhere else is refused. The standalone doctor resolves live.
276
278
  const rightKitSnapshotFile = process.env[SNAPSHOT_ENV] || null;
277
279
  let rightKitResolution = null; // null when the registry resolution failed (recorded as the `rightkit-registry` finding)
280
+ // The worker of a door split (row 7.04): its cwd is the snapshot, which has no git directory. Its commit and source root are the selected
281
+ // commit's and the snapshot's (release-source-identity.mjs); the checkout route reads them from git as before.
282
+ let workerSource = null;
283
+ let artifactPathContext = null; // the declared-artifact resolver's context (artifactContext, row 7.04c)
284
+ if (!opts.doctor && process.env.RK_GATE_CHECKOUT) {
285
+ try { workerSource = resolveReleaseSource({ cwd: process.cwd() }); } catch (error) { fail(`${error.code ?? "RK_SOURCE_IDENTITY"}: ${error.message}`); }
286
+ }
278
287
  try {
279
288
  const lockInventory = () => {
280
289
  try {
281
- const repoRoot = git(root, ["rev-parse", "--show-toplevel"]);
290
+ const repoRoot = workerSource ? sourceRootOf(workerSource) : git(root, ["rev-parse", "--show-toplevel"]);
282
291
  let canonicalTarget = config.targets?.[opts.platform];
283
292
  try { canonicalTarget = expandPackageRecipeTarget(canonicalTarget, { platform: opts.platform, appRoot: root, config }); } catch { /* reported later */ }
284
293
  const inputs = resolveReleaseBuildInputs({ repoRoot, appRoot: root, configPath, config, target: canonicalTarget });
@@ -292,7 +301,7 @@ try {
292
301
  if (!rightKitSnapshotFile) throw new RightKitRegistryError("no-admission", "no admitted RightKit registry snapshot was handed to the release worker; run it through `right-release build`, which admits the release");
293
302
  const stateRoot = process.env.RIGHT_RELEASE_STATE_ROOT;
294
303
  if (!stateRoot) throw new RightKitRegistryError("no-admission", "RIGHT_RELEASE_STATE_ROOT is not set; the admitted RightKit registry snapshot cannot be located");
295
- const commit = git(root, ["rev-parse", "HEAD"]);
304
+ const commit = workerSource ? workerSource.commit : git(root, ["rev-parse", "HEAD"]);
296
305
  boundSnapshot = loadAdmittedSnapshot(stateRoot, { releaseId: `${config.app}-${config.version}-${commit.slice(0, 8)}`, commit, platform: opts.platform, snapshotFile: rightKitSnapshotFile });
297
306
  }
298
307
  rightKitResolution = await prepareRightKitResolution({
@@ -348,6 +357,12 @@ await check("target", [], () => {
348
357
  if (!expanded) throw new Error(`${config.app ?? "app"} has no ${opts.platform} release target`);
349
358
  target = expanded;
350
359
  });
360
+ // Row 7.04c (R-T1, R-T4): in a door split each Cargo manifest's target is bound now, once, before any step runs, and no step the app owns may name
361
+ // a target-relative path (the snapshot has no target directory). Checkout mode checks nothing here.
362
+ await check("split-artifact-paths", ["target"], () => {
363
+ if (!workerSource) return;
364
+ assertSplitCommandPaths(releaseCommandSteps({ target, config, root }), artifactContext());
365
+ });
351
366
  const legalCheck = await check("legal-contract", [], () => (config.legal
352
367
  ? assertLegalReleaseContract(root, config.legal, config.app ?? path.basename(root), opts.platform)
353
368
  : null));
@@ -636,7 +651,7 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
636
651
  // writes the App Installer route and the seal record. Never a second signer.
637
652
  await runCommand(target.prePackage, root);
638
653
  // Raw payload scan BEFORE the package is packed (a packed MSIX is only readable through its ZIP directory).
639
- assertNoQaControlBinaries("msix staged payload", [path.resolve(root, target.msix.stageDir)], { requireBinary: true, prePack: true });
654
+ assertNoQaControlBinaries("msix staged payload", [resolveDeclaredPath(root, target.msix.stageDir)], { requireBinary: true, prePack: true });
640
655
  const options = msixBuildOptions({ root, target, app: config.app, dryRun: opts.dryRun, receipts: {
641
656
  pe: windowsReceiptPath(root, "raw-exe"), msix: windowsReceiptPath(root, "installer"),
642
657
  dir: path.dirname(windowsReceiptPath(root, "raw-exe")) } });
@@ -645,7 +660,7 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
645
660
  // Second layer: read the finished package through its ZIP central directory (stored and deflate entries).
646
661
  if (!built.dryRun) assertNoQaControlBinaries("msix package", [options.out], { requireBinary: true });
647
662
  } else if (opts.platform === "win") {
648
- const rawFiles = target.sign.prePackageFiles.map((p) => path.resolve(root, p));
663
+ const rawFiles = target.sign.prePackageFiles.map((p) => resolveDeclaredPath(root, p));
649
664
  const sidecarPlan = await prepareMediaSidecar(root);
650
665
  const externalPlan = await prepareExternalBin(root);
651
666
  await runCommand(target.prePackage, root);
@@ -894,15 +909,25 @@ function assertNoWebBundleMocksInFrontend(phase, { required }) {
894
909
  } catch (error) { fail(error.message); }
895
910
  }
896
911
 
912
+ // Row 7.04c (R-T2): one resolver for every declared artifact path. Checkout mode is the historical rule, byte for byte (the app root, then
913
+ // RIGHT_RELEASE_ARTIFACT_DIR); a door split maps a target-prefixed path to its bound Cargo target and never reads the checkout.
914
+ function artifactContext() {
915
+ artifactPathContext ??= splitArtifactContext({
916
+ cwd: root, env: process.env, nativeLayout, config, target: config.targets?.[opts.platform],
917
+ ...(workerSource?.mode === "snapshot" ? { snapshotRoot: workerSource.snapshot } : {}),
918
+ });
919
+ return artifactPathContext;
920
+ }
921
+
897
922
  function resolveReleaseArtifactPath(root, declared) {
898
- const local = path.resolve(root, declared);
899
- if (existsSync(local)) return local;
900
- const managed = process.env.RIGHT_RELEASE_ARTIFACT_DIR;
901
- if (managed) {
902
- const candidate = path.join(path.resolve(managed), path.basename(declared));
903
- if (existsSync(candidate)) return candidate;
904
- }
905
- return local;
923
+ try { return resolveArtifactPath(declared, artifactContext()); }
924
+ catch (error) { if (error instanceof ArtifactPathError) fail(error.message); throw error; }
925
+ }
926
+
927
+ /** A declared path that never fell back to the managed artifact directory (a staged payload, a raw EXE): the app root, or in a split the same resolver. */
928
+ function resolveDeclaredPath(root, declared) {
929
+ const context = artifactContext();
930
+ return context.mode === "split" ? resolveReleaseArtifactPath(root, declared) : path.resolve(root, declared);
906
931
  }
907
932
 
908
933
  function fail(message) {
@@ -1042,11 +1067,13 @@ function nativeSignedArtifacts(files, root) {
1042
1067
  return values.map((value) => {
1043
1068
  const declared = typeof value === "string" ? value : value?.path ?? value?.file;
1044
1069
  if (!declared) fail("native finalizer signed artifact path is missing");
1045
- const absolute = path.resolve(root, declared);
1046
- if (opts.dryRun) return { path: path.relative(root, absolute).replaceAll("\\", "/") };
1070
+ const absolute = resolveDeclaredPath(root, declared);
1071
+ // A split's bound target lies outside the app: its absolute path is the evidence there.
1072
+ const evidencePath = () => { const relative = path.relative(root, absolute).replaceAll("\\", "/"); return relative.startsWith("..") ? absolute.replaceAll("\\", "/") : relative; };
1073
+ if (opts.dryRun) return { path: evidencePath() };
1047
1074
  if (!existsSync(absolute)) fail(`native finalizer signed artifact missing: ${absolute}`);
1048
1075
  return {
1049
- path: path.relative(root, absolute).replaceAll("\\", "/"),
1076
+ path: evidencePath(),
1050
1077
  sha256: createHash("sha256").update(readFileSync(absolute)).digest("hex"),
1051
1078
  sizeBytes: readFileSync(absolute).length,
1052
1079
  };
@@ -1080,7 +1107,7 @@ function nativeArchiveCandidates({ root, target, nativeAssembly }) {
1080
1107
  ...(target.installer?.artifacts ?? []).map((artifact) => artifact.file),
1081
1108
  ...(target.updater?.artifacts ?? []).map((artifact) => artifact.file),
1082
1109
  ].filter((value) => typeof value === "string" && value.trim());
1083
- return [...new Set(declared.map((value) => path.resolve(root, value)))];
1110
+ return [...new Set(declared.map((value) => resolveDeclaredPath(root, value)))];
1084
1111
  }
1085
1112
 
1086
1113
  function nativeFinalizerOutputPath({ finalizer, target, root }) {
@@ -1906,7 +1933,7 @@ async function verifyWindowsEmbedding({ root, target, files, markers }) {
1906
1933
  console.log("dry-run: verify installer embeds the signed raw EXE");
1907
1934
  return;
1908
1935
  }
1909
- const rawFile = path.resolve(root, target.sign.prePackageFiles[0]);
1936
+ const rawFile = resolveDeclaredPath(root, target.sign.prePackageFiles[0]);
1910
1937
  const rawReceiptPath = windowsReceiptPath(root, "raw-exe");
1911
1938
  let rawReceipt;
1912
1939
  try { rawReceipt = JSON.parse(readFileSync(rawReceiptPath, "utf8")); } catch { fail(`missing raw EXE signing receipt: ${rawReceiptPath}`); }
@@ -29,10 +29,10 @@
29
29
  "@rightkit/outdated": "0.3.0",
30
30
  "@rightkit/pdf-ui": "0.2.0",
31
31
  "@rightkit/platform-ui": "0.4.0",
32
- "@rightkit/qa": "0.6.1",
32
+ "@rightkit/qa": "0.6.2",
33
33
  "@rightkit/qa-chrome": "0.1.12",
34
34
  "@rightkit/react-shell": "0.7.2",
35
- "@rightkit/release": "0.6.10",
35
+ "@rightkit/release": "0.6.12",
36
36
  "@rightkit/settings-ui": "0.7.0",
37
37
  "@rightkit/tauri": "0.3.0",
38
38
  "@rightkit/tauri-shell": "0.1.3",
@@ -236,7 +236,8 @@
236
236
  "0.4.9",
237
237
  "0.5.0",
238
238
  "0.5.1",
239
- "0.5.2"
239
+ "0.5.2",
240
+ "0.6.1"
240
241
  ],
241
242
  "@rightkit/qa-chrome": [
242
243
  "0.1.0",
@@ -376,7 +377,9 @@
376
377
  "0.6.5",
377
378
  "0.6.6",
378
379
  "0.6.7",
379
- "0.6.8"
380
+ "0.6.8",
381
+ "0.6.10",
382
+ "0.6.11"
380
383
  ],
381
384
  "@rightkit/settings-ui": [
382
385
  "0.1.0",
@@ -457,56 +460,57 @@
457
460
  "rightkit-asr": "0.3.1",
458
461
  "rightkit-audio": "0.3.1",
459
462
  "rightkit-automation": "0.6.1",
460
- "rightkit-browser": "0.3.6",
463
+ "rightkit-browser": "0.3.7",
461
464
  "rightkit-cache": "0.2.3",
462
- "rightkit-channel-memory": "0.2.1",
465
+ "rightkit-channel-memory": "0.2.2",
463
466
  "rightkit-child-process": "0.5.7",
464
467
  "rightkit-compose": "0.2.0",
465
- "rightkit-contracts": "0.1.7",
468
+ "rightkit-contracts": "0.1.8",
466
469
  "rightkit-crash": "0.3.6",
467
470
  "rightkit-credentials": "0.4.2",
468
471
  "rightkit-crypto": "0.1.0",
469
472
  "rightkit-daemon": "0.4.0",
470
- "rightkit-db": "0.5.2",
473
+ "rightkit-db": "0.5.3",
471
474
  "rightkit-device": "0.4.0",
472
- "rightkit-docs": "0.1.2",
473
- "rightkit-embed": "0.6.1",
475
+ "rightkit-docs": "0.1.3",
476
+ "rightkit-embed": "0.6.2",
474
477
  "rightkit-ffmpeg": "0.2.11",
475
- "rightkit-file-watch": "0.4.1",
478
+ "rightkit-file-watch": "0.4.2",
476
479
  "rightkit-fs": "0.4.7",
477
- "rightkit-fsindex": "0.5.0",
480
+ "rightkit-fsindex": "0.6.0",
478
481
  "rightkit-google-identity": "0.2.0",
479
482
  "rightkit-hooks": "0.2.0",
480
483
  "rightkit-hotkeys": "0.1.2",
481
- "rightkit-http": "0.3.8",
484
+ "rightkit-http": "0.3.9",
482
485
  "rightkit-identity": "0.1.0",
483
486
  "rightkit-job-queue": "0.5.1",
484
487
  "rightkit-license": "0.2.5",
485
488
  "rightkit-llm": "0.3.1",
486
- "rightkit-logs": "0.3.5",
487
- "rightkit-mcp": "0.7.0",
488
- "rightkit-models": "0.5.1",
489
+ "rightkit-logs": "0.3.6",
490
+ "rightkit-mcp": "0.7.1",
491
+ "rightkit-models": "0.5.2",
489
492
  "rightkit-motion": "0.2.0",
490
493
  "rightkit-oauth": "0.10.0",
491
494
  "rightkit-ocr": "0.2.3",
492
495
  "rightkit-onnx": "0.2.8",
493
496
  "rightkit-overlay": "0.2.0",
494
497
  "rightkit-overlay-win": "0.1.0",
495
- "rightkit-ownership": "0.4.2",
496
- "rightkit-pdf": "0.3.3",
498
+ "rightkit-ownership": "0.4.3",
499
+ "rightkit-pdf": "0.3.4",
497
500
  "rightkit-platform-rules": "0.1.0",
498
501
  "rightkit-playback": "0.3.0",
502
+ "rightkit-qa": "0.9.2",
499
503
  "rightkit-qa-driver": "0.7.0",
500
504
  "rightkit-record": "0.3.3",
501
505
  "rightkit-review": "0.4.0",
502
506
  "rightkit-sandbox": "0.6.1",
503
- "rightkit-search": "0.4.0",
504
- "rightkit-send": "0.2.0",
507
+ "rightkit-search": "0.4.1",
508
+ "rightkit-send": "0.3.0",
505
509
  "rightkit-sidecar": "0.3.0",
506
510
  "rightkit-speech-client": "0.4.0",
507
511
  "rightkit-spend": "0.2.0",
508
512
  "rightkit-sync": "0.2.2",
509
- "rightkit-tauri-shell": "0.6.0",
513
+ "rightkit-tauri-shell": "0.7.0",
510
514
  "rightkit-time": "0.1.0",
511
515
  "rightkit-transcript": "0.4.0",
512
516
  "rightkit-tts": "0.2.3",
@@ -542,7 +546,7 @@
542
546
  "rightkit-browser": "0.3.6",
543
547
  "rightkit-cache": "0.2.3",
544
548
  "rightkit-channel-memory": "0.2.1",
545
- "rightkit-child-process": "0.5.6",
549
+ "rightkit-child-process": "0.5.7",
546
550
  "rightkit-compose": "0.2.0",
547
551
  "rightkit-contracts": "0.1.7",
548
552
  "rightkit-control": "0.3.0",
@@ -631,13 +635,13 @@
631
635
  "@rightkit/logs": "0.1.6",
632
636
  "@rightkit/media-ui": "0.3.4",
633
637
  "@rightkit/onboarding": "0.6.3",
634
- "@rightkit/outdated": "0.2.0",
638
+ "@rightkit/outdated": "0.3.0",
635
639
  "@rightkit/pdf-ui": "0.2.0",
636
640
  "@rightkit/platform-ui": "0.4.0",
637
641
  "@rightkit/qa": "0.6.1",
638
- "@rightkit/qa-chrome": "0.1.11",
642
+ "@rightkit/qa-chrome": "0.1.12",
639
643
  "@rightkit/react-shell": "0.7.2",
640
- "@rightkit/release": "0.6.6",
644
+ "@rightkit/release": "0.6.11",
641
645
  "@rightkit/settings-ui": "0.7.0",
642
646
  "@rightkit/shell": "0.1.1",
643
647
  "@rightkit/tauri": "0.3.0",
@@ -652,7 +656,8 @@
652
656
  "0.2.1",
653
657
  "0.3.1",
654
658
  "0.4.0",
655
- "0.4.1"
659
+ "0.4.1",
660
+ "0.4.2"
656
661
  ],
657
662
  "rightkit-browser": [
658
663
  "0.2.0",
@@ -661,7 +666,8 @@
661
666
  "0.3.0",
662
667
  "0.3.1",
663
668
  "0.3.4",
664
- "0.3.5"
669
+ "0.3.5",
670
+ "0.3.6"
665
671
  ],
666
672
  "rightkit-models": [
667
673
  "0.2.1",
@@ -674,7 +680,8 @@
674
680
  "0.3.0",
675
681
  "0.3.1",
676
682
  "0.3.2",
677
- "0.4.4"
683
+ "0.4.4",
684
+ "0.5.1"
678
685
  ],
679
686
  "rightkit-audio": [
680
687
  "0.1.0",
@@ -690,7 +697,8 @@
690
697
  "0.3.1",
691
698
  "0.4.0",
692
699
  "0.4.3",
693
- "0.5.1"
700
+ "0.5.1",
701
+ "0.5.2"
694
702
  ],
695
703
  "rightkit-embed": [
696
704
  "0.2.1",
@@ -700,7 +708,8 @@
700
708
  "0.3.2",
701
709
  "0.4.0",
702
710
  "0.5.0",
703
- "0.6.0"
711
+ "0.6.0",
712
+ "0.6.1"
704
713
  ],
705
714
  "rightkit-jobs": [
706
715
  "0.2.1",
@@ -720,7 +729,8 @@
720
729
  "0.5.1",
721
730
  "0.5.2",
722
731
  "0.5.3",
723
- "0.6.0"
732
+ "0.6.0",
733
+ "0.7.0"
724
734
  ],
725
735
  "rightkit-media": [
726
736
  "0.2.2",
@@ -772,7 +782,8 @@
772
782
  "0.7.0",
773
783
  "0.7.1",
774
784
  "0.8.0",
775
- "0.9.0"
785
+ "0.9.0",
786
+ "0.9.1"
776
787
  ],
777
788
  "rightkit-sandbox": [
778
789
  "0.1.0",
@@ -833,7 +844,8 @@
833
844
  "0.3.0",
834
845
  "0.3.5",
835
846
  "0.3.6",
836
- "0.3.7"
847
+ "0.3.7",
848
+ "0.3.8"
837
849
  ],
838
850
  "rightkit-fs": [
839
851
  "0.1.0",
@@ -869,7 +881,8 @@
869
881
  "rightkit-pdf": [
870
882
  "0.1.0",
871
883
  "0.3.0",
872
- "0.3.2"
884
+ "0.3.2",
885
+ "0.3.3"
873
886
  ],
874
887
  "rightkit-speech-client": [
875
888
  "0.3.0",
@@ -898,7 +911,8 @@
898
911
  "0.2.0",
899
912
  "0.2.1",
900
913
  "0.3.0",
901
- "0.4.1"
914
+ "0.4.1",
915
+ "0.5.0"
902
916
  ],
903
917
  "rightkit-asr": [
904
918
  "0.0.2",
@@ -912,7 +926,8 @@
912
926
  "0.2.2",
913
927
  "0.3.0",
914
928
  "0.3.2",
915
- "0.3.3"
929
+ "0.3.3",
930
+ "0.3.5"
916
931
  ],
917
932
  "rightkit-ocr": [
918
933
  "0.1.2",
@@ -998,13 +1013,15 @@
998
1013
  "0.1.1"
999
1014
  ],
1000
1015
  "rightkit-channel-memory": [
1001
- "0.1.0"
1016
+ "0.1.0",
1017
+ "0.2.1"
1002
1018
  ],
1003
1019
  "rightkit-compose": [
1004
1020
  "0.1.0"
1005
1021
  ],
1006
1022
  "rightkit-contracts": [
1007
- "0.1.5"
1023
+ "0.1.5",
1024
+ "0.1.7"
1008
1025
  ],
1009
1026
  "rightkit-hooks": [
1010
1027
  "0.1.0",
@@ -1068,7 +1085,8 @@
1068
1085
  ],
1069
1086
  "rightkit-docs": [
1070
1087
  "0.1.0",
1071
- "0.1.1"
1088
+ "0.1.1",
1089
+ "0.1.2"
1072
1090
  ],
1073
1091
  "rightkit-tts-qwen": [
1074
1092
  "0.2.6"
@@ -1078,10 +1096,12 @@
1078
1096
  "0.2.2"
1079
1097
  ],
1080
1098
  "rightkit-send": [
1081
- "0.1.0"
1099
+ "0.1.0",
1100
+ "0.2.0"
1082
1101
  ],
1083
1102
  "rightkit-file-watch": [
1084
- "0.4.0"
1103
+ "0.4.0",
1104
+ "0.4.1"
1085
1105
  ],
1086
1106
  "rightkit-sync": [
1087
1107
  "0.1.1"
@@ -1092,7 +1112,8 @@
1092
1112
  "0.3.3",
1093
1113
  "0.4.0",
1094
1114
  "0.5.0",
1095
- "0.5.1"
1115
+ "0.5.1",
1116
+ "0.6.0"
1096
1117
  ],
1097
1118
  "rightkit-android": [
1098
1119
  "0.2.0"
@@ -251,10 +251,13 @@ const posix = (p) => p.split(path.sep).join("/");
251
251
  * The git pathspecs (relative to the workspace root) whose change makes a measurement stale: the
252
252
  * crate's src, benches, Cargo.toml (features) and build.rs, plus src, Cargo.toml and build.rs of every
253
253
  * workspace crate it reaches through `path` dependencies (transitively). Returns { root, specs, problems }.
254
+ * Every crate is its own Cargo workspace (rightkit#1285), so `root` is the crate directory (its own Cargo.toml and Cargo.lock)
255
+ * and a sibling crate's paths are `../<x>/...`. `deps.base` names the directory the specs are relative to (default: the
256
+ * workspace root); a caller that runs git at the repository root passes it so the specs read crates/<x>/....
254
257
  * Callers must refuse incomplete metadata rather than using a partial dependency scope.
255
258
  */
256
259
  export function measuredPathspecs(crateDir, deps = {}) {
257
- const { exists = existsSync, readFile = readFileSync } = deps;
260
+ const { exists = existsSync, readFile = readFileSync, base } = deps;
258
261
  const problems = [];
259
262
  // Workspace discovery deliberately tolerates bad ancestor manifests elsewhere. Freshness cannot:
260
263
  // a swallowed read/parse failure could hide inherited dependencies and shrink the measured graph.
@@ -271,7 +274,7 @@ export function measuredPathspecs(crateDir, deps = {}) {
271
274
  const checked = { exists, readFile: checkedRead };
272
275
  let root = path.resolve(crateDir);
273
276
  const specs = [];
274
- const add = (dir, names) => { for (const name of names) specs.push(posix(path.relative(root, path.join(dir, name)))); };
277
+ const add = (dir, names) => { for (const name of names) specs.push(posix(path.relative(base ? path.resolve(base) : root, path.join(dir, name)))); };
275
278
  try {
276
279
  root = findWorkspaceRoot(crateDir, checked) ?? root;
277
280
  add(path.resolve(crateDir), CRATE_PATHS);
@@ -365,6 +368,7 @@ export function measurementStaleness(commit, crateDir, git = defaultGit, deps =
365
368
  }
366
369
  } catch (error) { return `cannot resolve Cargo.lock package identity: ${error.message}`; }
367
370
  if (name) {
371
+ // The crate's own workspace lock: root is the crate directory when the crate is its own workspace (#1285).
368
372
  const lockPath = path.join(root, "Cargo.lock");
369
373
  let now;
370
374
  try { now = exists(lockPath) ? readFile(lockPath, "utf8") : null; }
package/scan-digest.mjs CHANGED
@@ -54,6 +54,60 @@ export function rustCratePlan(own = {}) {
54
54
  return { lintSets, defaultCheck, checkSets };
55
55
  }
56
56
 
57
+ // ---- Clippy batching (GATE-CONTRACT §1 "Clippy route & batching"; item 8 rows 8.6, 8.8, 8.15) -----------------------------------------
58
+ // Lives here, beside the scan ids, so the credentialed verifiers (requiredScanIds) and the planner (planScans) spell a batch's id from
59
+ // one function. Pure: no file reads.
60
+
61
+ /** The feature flags that mean the same thing on every package they are applied to: any other set names features of one crate. */
62
+ export const CLIPPY_UNIVERSAL_FEATURE_FLAGS = Object.freeze(["--all-features", "--no-default-features"]);
63
+ const optionValue = (set, names) => {
64
+ for (let i = 0; i < set.length; i += 1) for (const n of names) { if (set[i] === n) return set[i + 1] ?? null; if (set[i].startsWith(`${n}=`)) return set[i].slice(n.length + 1); }
65
+ return null;
66
+ };
67
+ /** The target and profile a feature-set argv selects (null: the toolchain's host target and the dev profile). */
68
+ export const clippySelectionOf = (set) => ({ target: optionValue(set, ["--target"]), profile: set.includes("--release") || set.includes("-r") ? "release" : optionValue(set, ["--profile"]) });
69
+
70
+ /**
71
+ * Groups the Clippy configurations of a plan into invocations. `entries`: `[{ name, crate, ws, set, count, env }]`, one per crate
72
+ * and lint feature set (`count` is how many sets the crate declares). Entries are batched when workspace, feature set, target,
73
+ * profile, lint policy, toolchain and environment are all equal (they share one `-p` list); an entry is its own invocation, with
74
+ * the reason recorded, when:
75
+ * - its crate is in `solo` (ci/shards.json `solo`: a crate that always gets a job of its own);
76
+ * - it is in `separate` (a name to a reason): batching changed what compiles for it (feature unification, item 8 row 8.7);
77
+ * - its feature set names crate-specific features (anything but --all-features / --no-default-features): such a set is
78
+ * per-configuration by construction, for example rightkit-db's SQLCipher variants or rightkit-qa-driver's wv2 generations
79
+ * (ci/gate.config.json featureSets);
80
+ * - its crate is sidecar-bound: it is scheduled after every independent batch and never joins one.
81
+ * A batch never widens a package set: it names exactly its members with `-p`. A batch's id is `clippy:batch:[<ws>:]<8 hex>` over its
82
+ * workspace, feature set, target, profile, lint policy and member list (never the toolchain or the output directory, which a
83
+ * verifier does not know), so the planner and requiredScanIds agree and the id names exactly the crates it covers. Returns
84
+ * `{ batches, notes }`; batches are ordered independent first, then sidecar-bound, each group by workspace then key.
85
+ */
86
+ export function clippyBatchPlan(entries, { solo = [], separate = new Map(), sidecarBound = new Set(), toolchain = null, lint = ["-D", "warnings"] } = {}) {
87
+ const soloSet = new Set(solo);
88
+ const groups = new Map();
89
+ for (const e of entries) {
90
+ const set = [...e.set];
91
+ const crate = e.crate ?? e.name;
92
+ const sel = clippySelectionOf(set);
93
+ let reason = null;
94
+ if (soloSet.has(e.name) || soloSet.has(crate)) reason = "ci/shards.json solo: the crate always gets an invocation of its own";
95
+ else if (separate instanceof Map && separate.has(e.name)) reason = separate.get(e.name);
96
+ else if (set.some((w) => !CLIPPY_UNIVERSAL_FEATURE_FLAGS.includes(w))) reason = `ci/gate.config.json featureSets names crate-specific features (${set.join(" ")})${e.count > 1 ? `; ${crate} declares ${e.count} feature configurations that are separate invocations by construction` : ""}`;
97
+ const bound = sidecarBound.has(e.name) || sidecarBound.has(crate);
98
+ const identity = { ws: e.ws, set, target: sel.target, profile: sel.profile, lint, ...(reason || bound ? { only: e.name, sidecar: bound } : {}) };
99
+ const key = canonicalJson({ ...identity, toolchain, env: e.env ?? null });
100
+ if (!groups.has(key)) groups.set(key, { key, identity: canonicalJson(identity), ws: e.ws, set, target: sel.target, profile: sel.profile, units: [], crates: [], reason: reason ?? (bound ? "sidecar-bound: scheduled after its sidecar prerequisite, never batched with independent crates" : null), order: bound ? "sidecar" : "independent" });
101
+ const g = groups.get(key);
102
+ g.units.push(e.name); if (!g.crates.includes(crate)) g.crates.push(crate);
103
+ }
104
+ const batches = [...groups.values()].map((g) => ({ ...g, units: g.units.sort(), crates: g.crates.sort() })).map((g) => ({ ...g, id: `clippy:batch:${g.ws === "." ? "" : `${g.ws}:`}${sha256(`${g.identity}\0${g.units.join("\0")}`).slice(0, 8)}` }));
105
+ batches.sort((a, b) => (a.order === b.order ? (a.ws === b.ws ? (a.key < b.key ? -1 : 1) : a.ws < b.ws ? -1 : 1) : a.order === "independent" ? -1 : 1));
106
+ const notes = batches.filter((b) => b.reason).map((b) => `clippy batching: ${b.units.join(", ")} (${b.set.join(" ") || "default features"}) stays its own invocation: ${b.reason}`);
107
+ notes.push(`clippy batching: ${entries.length} crate configuration(s) in ${batches.length} invocation(s), each \`-p\` list exactly its members, \`--locked\` and \`--no-deps\` on every one; no batch is \`--workspace\``);
108
+ return { batches, notes };
109
+ }
110
+
57
111
  /** Scan ids every scan of the whole repository always has when it holds Rust units (cargo.toml, workflows, YAML, JSON and licences). */
58
112
  export const WORKSPACE_SCAN_IDS = Object.freeze(["cargo-metadata", "cargo-deny", "cargo-audit", "actionlint", "yaml-parse", "toml-parse", "json-parse", "license", "secret-scan"]);
59
113
  /** Prefixes at least one scan id must carry (rustfmt groups by edition, so its exact ids depend on the files). */
@@ -62,25 +116,35 @@ export const REQUIRED_SCAN_PREFIXES = Object.freeze(["rustfmt:"]);
62
116
  /**
63
117
  * The scan ids a LOCAL scan record must contain, each with status pass, for these publishable Rust `units` ([names], crates/<name>).
64
118
  * `exists(repoPath)` answers from the AUTHENTICATED source (never from the record): src/lib.rs decides `doc`, CHANGELOG.md decides
65
- * `changelog-version`, and the repository's own ci/advisory-exceptions.json, ownership catalog and docs generator decide theirs.
119
+ * `changelog-version`, and the repository's own ci/advisory-exceptions.json (or, per crate, crates/<name>/.rightkit/advisory-exceptions.json), ownership catalog and docs generator decide theirs.
66
120
  * `crateFeatures` is ci/gate.config.json `rust`. `includeTests` adds the per-feature-set test scans. Extra scans in a record are
67
121
  * allowed; a missing one is not. planScans builds its ids from the same helpers, and a test holds the two in agreement.
68
122
  */
69
- export function requiredScanIds({ units, crateFeatures = {}, exists, includeTests = false }) {
123
+ export function requiredScanIds({ units, crateFeatures = {}, exists, includeTests = false, perCrateWorkspaces = undefined, clippyBatching = false, shardSolo = [], clippySeparate = new Map(), sidecarBound = new Set() }) {
70
124
  if (!Array.isArray(units) || units.length === 0 || !units.every((n) => typeof n === "string" && n)) throw new TypeError("requiredScanIds: units must be a non-empty array of names");
71
125
  if (typeof exists !== "function") throw new TypeError("requiredScanIds: exists(repoPath) is required");
72
- const ids = new Set(WORKSPACE_SCAN_IDS);
73
- if (exists("ci/advisory-exceptions.json")) ids.add("advisory-exceptions");
126
+ // One Cargo workspace and lock per crate (rightkit#1285): every unit has its own crates/<name>/Cargo.lock, so the per-workspace scans
127
+ // (metadata, deny, audit and the advisory ledger where the crate has one) are required per unit and carry the crate name; there is no
128
+ // repository-level ledger. A repository with one root workspace has no per-crate lock and keeps the unsuffixed ids.
129
+ const perCrate = perCrateWorkspaces ?? units.every((n) => exists(`crates/${n}/Cargo.lock`));
130
+ const ids = new Set(WORKSPACE_SCAN_IDS.filter((id) => !(perCrate && /^cargo-(metadata|deny|audit)$/.test(id))));
131
+ if (!perCrate && exists("ci/advisory-exceptions.json")) ids.add("advisory-exceptions");
74
132
  if (exists("packages/release/rightkit-ownership.json") && exists("crates/rightkit-ownership/catalog/ownership-catalog.json")) ids.add("ownership-catalog");
75
133
  if (exists("scripts/generate-index.mjs")) ids.add("generated-docs");
134
+ const clippyEntries = [];
76
135
  for (const name of units) {
77
136
  const own = crateFeatures?.[name] ?? {};
78
137
  const { lintSets, defaultCheck, checkSets } = rustCratePlan(own);
138
+ if (perCrate) {
139
+ for (const id of ["cargo-metadata", "cargo-deny", "cargo-audit"]) ids.add(`${id}:${name}`);
140
+ if (exists(`crates/${name}/.rightkit/advisory-exceptions.json`)) ids.add(`advisory-exceptions:${name}`);
141
+ }
79
142
  ids.add(rustScanId.packageList(name));
80
143
  if (exists(`crates/${name}/CHANGELOG.md`)) ids.add(rustScanId.changelog(name));
81
144
  if (defaultCheck) ids.add(rustScanId.check(name));
82
145
  checkSets.forEach((_set, i) => ids.add(rustScanId.checkFeatures(name, i)));
83
- lintSets.forEach((_set, i) => ids.add(rustScanId.clippy(name, lintSets.length, i)));
146
+ if (clippyBatching) lintSets.forEach((set) => clippyEntries.push({ name, crate: name, ws: ".", set, count: lintSets.length, env: null }));
147
+ else lintSets.forEach((_set, i) => ids.add(rustScanId.clippy(name, lintSets.length, i)));
84
148
  if (exists(`crates/${name}/src/lib.rs`)) ids.add(rustScanId.doc(name));
85
149
  if (includeTests) {
86
150
  lintSets.forEach((_set, i) => {
@@ -89,6 +153,8 @@ export function requiredScanIds({ units, crateFeatures = {}, exists, includeTest
89
153
  });
90
154
  }
91
155
  }
156
+ // With batching on, the Clippy ids are the batches' (the same function the planner calls), never the per-crate ones.
157
+ if (clippyBatching) for (const b of clippyBatchPlan(clippyEntries, { solo: shardSolo, separate: clippySeparate, sidecarBound }).batches) ids.add(b.id);
92
158
  return [...ids].sort();
93
159
  }
94
160
 
package/sign-updater.mjs CHANGED
@@ -7,6 +7,7 @@ import { refuseManagedEnvSecrets } from "./managed-host-secrets.mjs";
7
7
  import { execWithUpdaterCredential } from "./protected-credentials.mjs";
8
8
  import { resolvePnpmCli } from "./development.mjs";
9
9
  import { UPDATER_KEY_ID_RE, updaterSignatureKeyId } from "./github-updater-release.mjs";
10
+ import { ArtifactPathError, loadSplitArtifactContext, resolveArtifactPath } from "./release-artifact-path.mjs";
10
11
 
11
12
  export const UPDATER_SIGNING_SECRET = "updater-signing-key";
12
13
 
@@ -14,6 +15,8 @@ export function runSignUpdater(args, {
14
15
  env = process.env, platform = process.platform, cwd = process.cwd(),
15
16
  execCredential = execWithUpdaterCredential, resolvePnpm = resolvePnpmCli,
16
17
  signMac = signMacUpdaterFiles, exists = existsSync, remove = rmSync, read = (file) => readFileSync(file, "utf8"), log = console.log,
18
+ // The artifact-path context of a door split (release-artifact-path.mjs, row 7.04c): `--file` is then the bound Cargo target's, never the checkout's.
19
+ artifactPaths = null,
17
20
  } = {}) {
18
21
  const files = [];
19
22
  let dryRun = false;
@@ -21,7 +24,7 @@ export function runSignUpdater(args, {
21
24
  for (let i = 0; i < args.length; i++) {
22
25
  if (args[i] === "--file") {
23
26
  if (!args[i + 1] || args[i + 1].startsWith("--")) throw new Error("--file requires an artifact path");
24
- files.push(path.resolve(cwd, args[++i]));
27
+ files.push(artifactPaths?.mode === "split" ? resolveArtifactPath(args[++i], artifactPaths) : path.resolve(cwd, args[++i]));
25
28
  } else if (args[i] === "--key-id") {
26
29
  if (keyId !== undefined) throw new Error("--key-id may be given only once");
27
30
  if (!UPDATER_KEY_ID_RE.test(args[i + 1] ?? "")) throw new Error("--key-id requires the 16 upper-case hex digits of the updater key id");
@@ -77,9 +80,9 @@ export function runSignUpdater(args, {
77
80
  }
78
81
 
79
82
  if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
80
- try { runSignUpdater(process.argv.slice(2)); }
83
+ try { runSignUpdater(process.argv.slice(2), { artifactPaths: await loadSplitArtifactContext({ cwd: process.cwd() }) }); }
81
84
  catch (error) {
82
- console.error(`sign-updater: ${error.code ? `[${error.code}] ` : ""}${error.message}`);
85
+ console.error(`sign-updater: ${error instanceof ArtifactPathError ? "" : error.code ? `[${error.code}] ` : ""}${error.message}`);
83
86
  process.exitCode = error.exitCode ?? 1;
84
87
  }
85
88
  }