@rightkit/release 0.6.10 → 0.6.11

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 +762 -345
  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 +62 -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.11",
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,8 @@
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"
380
382
  ],
381
383
  "@rightkit/settings-ui": [
382
384
  "0.1.0",
@@ -457,56 +459,57 @@
457
459
  "rightkit-asr": "0.3.1",
458
460
  "rightkit-audio": "0.3.1",
459
461
  "rightkit-automation": "0.6.1",
460
- "rightkit-browser": "0.3.6",
462
+ "rightkit-browser": "0.3.7",
461
463
  "rightkit-cache": "0.2.3",
462
- "rightkit-channel-memory": "0.2.1",
464
+ "rightkit-channel-memory": "0.2.2",
463
465
  "rightkit-child-process": "0.5.7",
464
466
  "rightkit-compose": "0.2.0",
465
- "rightkit-contracts": "0.1.7",
467
+ "rightkit-contracts": "0.1.8",
466
468
  "rightkit-crash": "0.3.6",
467
469
  "rightkit-credentials": "0.4.2",
468
470
  "rightkit-crypto": "0.1.0",
469
471
  "rightkit-daemon": "0.4.0",
470
- "rightkit-db": "0.5.2",
472
+ "rightkit-db": "0.5.3",
471
473
  "rightkit-device": "0.4.0",
472
- "rightkit-docs": "0.1.2",
473
- "rightkit-embed": "0.6.1",
474
+ "rightkit-docs": "0.1.3",
475
+ "rightkit-embed": "0.6.2",
474
476
  "rightkit-ffmpeg": "0.2.11",
475
- "rightkit-file-watch": "0.4.1",
477
+ "rightkit-file-watch": "0.4.2",
476
478
  "rightkit-fs": "0.4.7",
477
- "rightkit-fsindex": "0.5.0",
479
+ "rightkit-fsindex": "0.6.0",
478
480
  "rightkit-google-identity": "0.2.0",
479
481
  "rightkit-hooks": "0.2.0",
480
482
  "rightkit-hotkeys": "0.1.2",
481
- "rightkit-http": "0.3.8",
483
+ "rightkit-http": "0.3.9",
482
484
  "rightkit-identity": "0.1.0",
483
485
  "rightkit-job-queue": "0.5.1",
484
486
  "rightkit-license": "0.2.5",
485
487
  "rightkit-llm": "0.3.1",
486
- "rightkit-logs": "0.3.5",
487
- "rightkit-mcp": "0.7.0",
488
- "rightkit-models": "0.5.1",
488
+ "rightkit-logs": "0.3.6",
489
+ "rightkit-mcp": "0.7.1",
490
+ "rightkit-models": "0.5.2",
489
491
  "rightkit-motion": "0.2.0",
490
492
  "rightkit-oauth": "0.10.0",
491
493
  "rightkit-ocr": "0.2.3",
492
494
  "rightkit-onnx": "0.2.8",
493
495
  "rightkit-overlay": "0.2.0",
494
496
  "rightkit-overlay-win": "0.1.0",
495
- "rightkit-ownership": "0.4.2",
496
- "rightkit-pdf": "0.3.3",
497
+ "rightkit-ownership": "0.4.3",
498
+ "rightkit-pdf": "0.3.4",
497
499
  "rightkit-platform-rules": "0.1.0",
498
500
  "rightkit-playback": "0.3.0",
501
+ "rightkit-qa": "0.9.2",
499
502
  "rightkit-qa-driver": "0.7.0",
500
503
  "rightkit-record": "0.3.3",
501
504
  "rightkit-review": "0.4.0",
502
505
  "rightkit-sandbox": "0.6.1",
503
- "rightkit-search": "0.4.0",
504
- "rightkit-send": "0.2.0",
506
+ "rightkit-search": "0.4.1",
507
+ "rightkit-send": "0.3.0",
505
508
  "rightkit-sidecar": "0.3.0",
506
509
  "rightkit-speech-client": "0.4.0",
507
510
  "rightkit-spend": "0.2.0",
508
511
  "rightkit-sync": "0.2.2",
509
- "rightkit-tauri-shell": "0.6.0",
512
+ "rightkit-tauri-shell": "0.7.0",
510
513
  "rightkit-time": "0.1.0",
511
514
  "rightkit-transcript": "0.4.0",
512
515
  "rightkit-tts": "0.2.3",
@@ -542,7 +545,7 @@
542
545
  "rightkit-browser": "0.3.6",
543
546
  "rightkit-cache": "0.2.3",
544
547
  "rightkit-channel-memory": "0.2.1",
545
- "rightkit-child-process": "0.5.6",
548
+ "rightkit-child-process": "0.5.7",
546
549
  "rightkit-compose": "0.2.0",
547
550
  "rightkit-contracts": "0.1.7",
548
551
  "rightkit-control": "0.3.0",
@@ -631,13 +634,13 @@
631
634
  "@rightkit/logs": "0.1.6",
632
635
  "@rightkit/media-ui": "0.3.4",
633
636
  "@rightkit/onboarding": "0.6.3",
634
- "@rightkit/outdated": "0.2.0",
637
+ "@rightkit/outdated": "0.3.0",
635
638
  "@rightkit/pdf-ui": "0.2.0",
636
639
  "@rightkit/platform-ui": "0.4.0",
637
640
  "@rightkit/qa": "0.6.1",
638
- "@rightkit/qa-chrome": "0.1.11",
641
+ "@rightkit/qa-chrome": "0.1.12",
639
642
  "@rightkit/react-shell": "0.7.2",
640
- "@rightkit/release": "0.6.6",
643
+ "@rightkit/release": "0.6.10",
641
644
  "@rightkit/settings-ui": "0.7.0",
642
645
  "@rightkit/shell": "0.1.1",
643
646
  "@rightkit/tauri": "0.3.0",
@@ -652,7 +655,8 @@
652
655
  "0.2.1",
653
656
  "0.3.1",
654
657
  "0.4.0",
655
- "0.4.1"
658
+ "0.4.1",
659
+ "0.4.2"
656
660
  ],
657
661
  "rightkit-browser": [
658
662
  "0.2.0",
@@ -661,7 +665,8 @@
661
665
  "0.3.0",
662
666
  "0.3.1",
663
667
  "0.3.4",
664
- "0.3.5"
668
+ "0.3.5",
669
+ "0.3.6"
665
670
  ],
666
671
  "rightkit-models": [
667
672
  "0.2.1",
@@ -674,7 +679,8 @@
674
679
  "0.3.0",
675
680
  "0.3.1",
676
681
  "0.3.2",
677
- "0.4.4"
682
+ "0.4.4",
683
+ "0.5.1"
678
684
  ],
679
685
  "rightkit-audio": [
680
686
  "0.1.0",
@@ -690,7 +696,8 @@
690
696
  "0.3.1",
691
697
  "0.4.0",
692
698
  "0.4.3",
693
- "0.5.1"
699
+ "0.5.1",
700
+ "0.5.2"
694
701
  ],
695
702
  "rightkit-embed": [
696
703
  "0.2.1",
@@ -700,7 +707,8 @@
700
707
  "0.3.2",
701
708
  "0.4.0",
702
709
  "0.5.0",
703
- "0.6.0"
710
+ "0.6.0",
711
+ "0.6.1"
704
712
  ],
705
713
  "rightkit-jobs": [
706
714
  "0.2.1",
@@ -720,7 +728,8 @@
720
728
  "0.5.1",
721
729
  "0.5.2",
722
730
  "0.5.3",
723
- "0.6.0"
731
+ "0.6.0",
732
+ "0.7.0"
724
733
  ],
725
734
  "rightkit-media": [
726
735
  "0.2.2",
@@ -772,7 +781,8 @@
772
781
  "0.7.0",
773
782
  "0.7.1",
774
783
  "0.8.0",
775
- "0.9.0"
784
+ "0.9.0",
785
+ "0.9.1"
776
786
  ],
777
787
  "rightkit-sandbox": [
778
788
  "0.1.0",
@@ -833,7 +843,8 @@
833
843
  "0.3.0",
834
844
  "0.3.5",
835
845
  "0.3.6",
836
- "0.3.7"
846
+ "0.3.7",
847
+ "0.3.8"
837
848
  ],
838
849
  "rightkit-fs": [
839
850
  "0.1.0",
@@ -869,7 +880,8 @@
869
880
  "rightkit-pdf": [
870
881
  "0.1.0",
871
882
  "0.3.0",
872
- "0.3.2"
883
+ "0.3.2",
884
+ "0.3.3"
873
885
  ],
874
886
  "rightkit-speech-client": [
875
887
  "0.3.0",
@@ -898,7 +910,8 @@
898
910
  "0.2.0",
899
911
  "0.2.1",
900
912
  "0.3.0",
901
- "0.4.1"
913
+ "0.4.1",
914
+ "0.5.0"
902
915
  ],
903
916
  "rightkit-asr": [
904
917
  "0.0.2",
@@ -912,7 +925,8 @@
912
925
  "0.2.2",
913
926
  "0.3.0",
914
927
  "0.3.2",
915
- "0.3.3"
928
+ "0.3.3",
929
+ "0.3.5"
916
930
  ],
917
931
  "rightkit-ocr": [
918
932
  "0.1.2",
@@ -998,13 +1012,15 @@
998
1012
  "0.1.1"
999
1013
  ],
1000
1014
  "rightkit-channel-memory": [
1001
- "0.1.0"
1015
+ "0.1.0",
1016
+ "0.2.1"
1002
1017
  ],
1003
1018
  "rightkit-compose": [
1004
1019
  "0.1.0"
1005
1020
  ],
1006
1021
  "rightkit-contracts": [
1007
- "0.1.5"
1022
+ "0.1.5",
1023
+ "0.1.7"
1008
1024
  ],
1009
1025
  "rightkit-hooks": [
1010
1026
  "0.1.0",
@@ -1068,7 +1084,8 @@
1068
1084
  ],
1069
1085
  "rightkit-docs": [
1070
1086
  "0.1.0",
1071
- "0.1.1"
1087
+ "0.1.1",
1088
+ "0.1.2"
1072
1089
  ],
1073
1090
  "rightkit-tts-qwen": [
1074
1091
  "0.2.6"
@@ -1078,10 +1095,12 @@
1078
1095
  "0.2.2"
1079
1096
  ],
1080
1097
  "rightkit-send": [
1081
- "0.1.0"
1098
+ "0.1.0",
1099
+ "0.2.0"
1082
1100
  ],
1083
1101
  "rightkit-file-watch": [
1084
- "0.4.0"
1102
+ "0.4.0",
1103
+ "0.4.1"
1085
1104
  ],
1086
1105
  "rightkit-sync": [
1087
1106
  "0.1.1"
@@ -1092,7 +1111,8 @@
1092
1111
  "0.3.3",
1093
1112
  "0.4.0",
1094
1113
  "0.5.0",
1095
- "0.5.1"
1114
+ "0.5.1",
1115
+ "0.6.0"
1096
1116
  ],
1097
1117
  "rightkit-android": [
1098
1118
  "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
  }