@codyswann/lisa 4.71.6 → 4.71.8

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 (100) hide show
  1. package/all/copy-overwrite/scripts/check-threshold-ratchet.mjs +571 -0
  2. package/all/copy-overwrite/scripts/check-workflow-load-failures.mjs +31 -9
  3. package/all/copy-overwrite/scripts/lib/automation-provenance-contract.mjs +19 -0
  4. package/all/copy-overwrite/scripts/lib/automation-provenance-files.mjs +49 -0
  5. package/all/copy-overwrite/scripts/lib/automation-provenance-local.mjs +54 -38
  6. package/all/copy-overwrite/scripts/lib/npm-update-allocate.mjs +8 -2
  7. package/all/copy-overwrite/scripts/lib/npm-update-bun.mjs +189 -0
  8. package/all/copy-overwrite/scripts/lib/npm-update-cancel-origin.mjs +7 -3
  9. package/all/copy-overwrite/scripts/lib/npm-update-cancellation-proof.mjs +6 -1
  10. package/all/copy-overwrite/scripts/lib/npm-update-contract.mjs +26 -5
  11. package/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +8 -5
  12. package/all/copy-overwrite/scripts/lib/npm-update-github.mjs +2 -2
  13. package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +3 -4
  14. package/all/copy-overwrite/scripts/lib/npm-update-hook-installation.mjs +68 -1
  15. package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +13 -80
  16. package/all/copy-overwrite/scripts/lib/npm-update-leaf-contract.mjs +6 -5
  17. package/all/copy-overwrite/scripts/lib/npm-update-prepare.mjs +35 -7
  18. package/all/copy-overwrite/scripts/lib/npm-update-publish.mjs +1 -1
  19. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +43 -16
  20. package/all/copy-overwrite/scripts/threshold-ratchet-compare.mjs +521 -0
  21. package/all/copy-overwrite/scripts/threshold-ratchet-families.mjs +546 -0
  22. package/dist/configs/repo-scan.js +1 -1
  23. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  24. package/dist/core/lisa-owned-hash-ledger.js +62 -0
  25. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  26. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  27. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  28. package/dist/core/upstream-evidence-manifest.js +46 -32
  29. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  30. package/package.json +4 -4
  31. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa/hooks/threshold-ratchet-compare.mjs +3 -3
  34. package/plugins/lisa/hooks/threshold-ratchet.mjs +1 -1
  35. package/plugins/lisa-agy/plugin.json +1 -1
  36. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  39. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-copilot/hooks/threshold-ratchet-compare.mjs +3 -3
  43. package/plugins/lisa-copilot/hooks/threshold-ratchet.mjs +1 -1
  44. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-cursor/hooks/threshold-ratchet-compare.mjs +3 -3
  46. package/plugins/lisa-cursor/hooks/threshold-ratchet.mjs +1 -1
  47. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-expo-agy/plugin.json +1 -1
  50. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  55. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  60. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  64. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  65. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  69. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  70. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  74. package/plugins/lisa-rails-agy/plugin.json +1 -1
  75. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  79. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  80. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  84. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  85. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  87. package/plugins/materialized-artifacts.json +3 -0
  88. package/plugins/src/base/hooks/threshold-ratchet-compare.mjs +3 -3
  89. package/plugins/src/base/hooks/threshold-ratchet.mjs +1 -1
  90. package/rails/copy-overwrite/scripts/check-threshold-ratchet.mjs +1 -1
  91. package/rails/copy-overwrite/scripts/threshold-ratchet-compare.mjs +3 -3
  92. package/scripts/build-plugins.sh +2 -1
  93. package/scripts/generate-scratch-supervisor-profile.mjs +1 -1
  94. package/scripts/two-channel-couplings.json +9 -7
  95. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +22 -3
  96. package/typescript/copy-overwrite/scripts/check-threshold-ratchet.mjs +1 -1
  97. package/typescript/copy-overwrite/scripts/threshold-ratchet-compare.mjs +3 -3
  98. /package/{rails → all}/copy-contents/scripts/lisa-mutation.sh +0 -0
  99. /package/{rails → all}/copy-overwrite/scripts/lisa-clean-git-env.sh +0 -0
  100. /package/{rails → all}/copy-overwrite/scripts/lisa-scratch-run.sh +0 -0
@@ -0,0 +1,49 @@
1
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ // Do not edit directly — durable changes belong upstream in Lisa.
3
+
4
+ /** Descriptor-backed readers reject aliases and growth before proof bytes gain authority. */
5
+ import {
6
+ constants,
7
+ closeSync,
8
+ fstatSync,
9
+ lstatSync,
10
+ openSync,
11
+ readSync,
12
+ } from "node:fs";
13
+ import { requireProof } from "./github-attestation-verifier.mjs";
14
+
15
+ /** Only native ENOENT establishes absence; dangling aliases and query errors refuse. */
16
+ export function fileAbsent(file) {
17
+ try {
18
+ lstatSync(file);
19
+ return false;
20
+ } catch (error) {
21
+ if (error.code === "ENOENT") return true;
22
+ throw error;
23
+ }
24
+ }
25
+
26
+ /** Refuse aliases and oversize files; snapshots remain private, regular files. */
27
+ export function privateBytes(file, maximum, privateFile = true) {
28
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
29
+ try {
30
+ const stat = fstatSync(fd);
31
+ requireProof(
32
+ stat.isFile() &&
33
+ stat.size <= maximum &&
34
+ (!privateFile || (stat.mode & 0o077) === 0),
35
+ "invalid regular bounded proof file"
36
+ );
37
+ const bytes = Buffer.alloc(maximum + 1);
38
+ let count = 0;
39
+ while (count < bytes.length) {
40
+ const added = readSync(fd, bytes, count, bytes.length - count, null);
41
+ if (added === 0) break;
42
+ count += added;
43
+ }
44
+ requireProof(count <= maximum, "proof file grew beyond limit");
45
+ return bytes.subarray(0, count);
46
+ } finally {
47
+ closeSync(fd);
48
+ }
49
+ }
@@ -2,14 +2,9 @@
2
2
  // Do not edit directly — durable changes belong upstream in Lisa.
3
3
 
4
4
  /** Local proof snapshots and the genuine canonical resolver preserve exact final-read authority. */
5
- import {
6
- constants,
7
- closeSync,
8
- fstatSync,
9
- lstatSync,
10
- openSync,
11
- readSync,
12
- } from "node:fs";
5
+ import { lstatSync } from "node:fs";
6
+ import { fileAbsent, privateBytes } from "./automation-provenance-files.mjs";
7
+ export { privateBytes } from "./automation-provenance-files.mjs";
13
8
  import { dirname, resolve } from "node:path";
14
9
  import { TextDecoder } from "node:util";
15
10
  import { resolveWorkItemContext, run } from "../lisa-work-item.mjs";
@@ -18,37 +13,13 @@ import { boundedSpawnSync } from "./bounded-spawn.mjs";
18
13
  import {
19
14
  canonicalJson,
20
15
  exactKeys,
21
- FILES,
16
+ proposalFileNames,
17
+ optionalLockFields,
22
18
  DESCRIPTOR_KEYS,
23
19
  HEX,
24
20
  OBJECT_ID,
25
21
  } from "./automation-provenance-contract.mjs";
26
22
 
27
- /** Refuse aliases and oversize files; snapshots remain private, regular files. */
28
- export function privateBytes(file, maximum, privateFile = true) {
29
- const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
30
- try {
31
- const stat = fstatSync(fd);
32
- requireProof(
33
- stat.isFile() &&
34
- stat.size <= maximum &&
35
- (!privateFile || (stat.mode & 0o077) === 0),
36
- "invalid regular bounded proof file"
37
- );
38
- const bytes = Buffer.alloc(maximum + 1);
39
- let count = 0;
40
- while (count < bytes.length) {
41
- const added = readSync(fd, bytes, count, bytes.length - count, null);
42
- if (added === 0) break;
43
- count += added;
44
- }
45
- requireProof(count <= maximum, "proof file grew beyond limit");
46
- return bytes.subarray(0, count);
47
- } finally {
48
- closeSync(fd);
49
- }
50
- }
51
-
52
23
  /** Git reads always use the actual checkout and bounded shared runner. */
53
24
  export function git(args) {
54
25
  return run("git", args, {
@@ -59,7 +30,7 @@ export function git(args) {
59
30
 
60
31
  /** Alias and exact staged-blob checks run after descriptor metadata validation. */
61
32
  function stagedFiles(descriptor) {
62
- for (const file of FILES) {
33
+ for (const file of proposalFileNames(descriptor)) {
63
34
  const entry = git(["ls-files", "--stage", "--", file]);
64
35
  requireProof(
65
36
  entry.startsWith("100644 ") && entry.split("\n").length === 1,
@@ -76,6 +47,47 @@ function stagedFiles(descriptor) {
76
47
  }
77
48
  }
78
49
 
50
+ /** Bind optional lock presence to HEAD and refuse aliased working-tree counterparts. */
51
+ function originalBun(descriptor) {
52
+ const original = git(["ls-tree", "HEAD", "--", "bun.lock"]);
53
+ if (!Object.hasOwn(descriptor, "bunLockSha256")) {
54
+ requireProof(original === "", "original Bun lock omitted from descriptor");
55
+ requireProof(
56
+ fileAbsent(resolve("bun.lock")),
57
+ "unexpected working Bun lock"
58
+ );
59
+ return;
60
+ }
61
+ requireProof(
62
+ /^100644 blob [a-f0-9]{40}\tbun\.lock$/.test(original),
63
+ "original Bun lock is not regular"
64
+ );
65
+ const bytes = run("git", ["show", "HEAD:bun.lock"], {
66
+ timeout: 30_000,
67
+ maxBuffer: 1_048_576,
68
+ }).stdout;
69
+ requireProof(
70
+ sha256(bytes) === descriptor.bunLockSha256,
71
+ "original Bun lock digest differs"
72
+ );
73
+ const path = resolve("bun.lock");
74
+ const before = lstatSync(path);
75
+ requireProof(
76
+ before.isFile() && before.nlink === 1,
77
+ "Bun lock path is aliased"
78
+ );
79
+ const current = privateBytes(path, 1_048_576, false);
80
+ const after = lstatSync(path);
81
+ requireProof(
82
+ after.isFile() &&
83
+ after.nlink === 1 &&
84
+ before.dev === after.dev &&
85
+ before.ino === after.ino &&
86
+ sha256(current) === descriptor.files["bun.lock"],
87
+ "working Bun lock identity differs"
88
+ );
89
+ }
90
+
79
91
  /** Match complete message and Git metadata before any network/proof acceptance. */
80
92
  export function localDescriptor(descriptor, messageBytes, reference, policy) {
81
93
  // Parsing never supplies the signed digest. Reject malformed UTF8 only on
@@ -84,7 +96,11 @@ export function localDescriptor(descriptor, messageBytes, reference, policy) {
84
96
  fatal: true,
85
97
  ignoreBOM: true,
86
98
  }).decode(messageBytes);
87
- exactKeys(descriptor, DESCRIPTOR_KEYS, "descriptor");
99
+ exactKeys(
100
+ descriptor,
101
+ [...DESCRIPTOR_KEYS, ...optionalLockFields(descriptor)],
102
+ "descriptor"
103
+ );
88
104
  requireProof(
89
105
  descriptor.version === 1 &&
90
106
  descriptor.runId === reference.runId &&
@@ -139,10 +155,10 @@ export function localDescriptor(descriptor, messageBytes, reference, policy) {
139
155
  .split("\n")
140
156
  .sort();
141
157
  requireProof(
142
- changed.join("\n") === FILES.join("\n"),
158
+ changed.join("\n") === proposalFileNames(descriptor).join("\n"),
143
159
  "proposal changes foreign files"
144
160
  );
145
- exactKeys(descriptor.files, FILES, "proposal files");
161
+ originalBun(descriptor);
146
162
  stagedFiles(descriptor);
147
163
  }
148
164
 
@@ -2,7 +2,12 @@
2
2
  // Do not edit directly — durable changes belong upstream in Lisa.
3
3
 
4
4
  /** Fixed-schema writer evidence and idempotent current claim. @module npm-updater */
5
- import { required, CLAIM, validateProposal } from "./npm-update-contract.mjs";
5
+ import {
6
+ required,
7
+ CLAIM,
8
+ validateProposal,
9
+ proposalFileNames,
10
+ } from "./npm-update-contract.mjs";
6
11
  import { baseline } from "./npm-update-prepare.mjs";
7
12
  import { locateCheckpoint } from "./npm-update-recovery.mjs";
8
13
  import { runProcess } from "./npm-update-process.mjs";
@@ -45,6 +50,7 @@ export async function filingEvidence(api, proposal, config, cwd) {
45
50
  "--",
46
51
  "package.json",
47
52
  "package-lock.json",
53
+ ...(Object.hasOwn(proposal, "bunLockSha256") ? ["bun.lock"] : []),
48
54
  ],
49
55
  {
50
56
  cwd,
@@ -63,7 +69,7 @@ export async function filingEvidence(api, proposal, config, cwd) {
63
69
  );
64
70
  return {
65
71
  history: {
66
- command: `git log ${proposal.parent} --max-count=20 -- package.json package-lock.json`,
72
+ command: `git log ${proposal.parent} --max-count=20 -- package.json package-lock.json${proposalFileNames(proposal).includes("bun.lock") ? " bun.lock" : ""}`,
67
73
  result: history.stdout.toString() || "No matching history.",
68
74
  },
69
75
  search: { query, total: search.total_count },
@@ -0,0 +1,189 @@
1
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
2
+ // Do not edit directly — durable changes belong upstream in Lisa.
3
+
4
+ /** Optional Bun coherence uses a pinned native tool and the original committed lock. */
5
+ import { existsSync, lstatSync, realpathSync, readFileSync } from "node:fs";
6
+ import { isAbsolute, join, resolve } from "node:path";
7
+ import { required } from "./npm-update-invariants.mjs";
8
+ import { sha256 } from "./github-attestation-verifier.mjs";
9
+ import { runProcess, readBytes } from "./npm-update-process-core.mjs";
10
+ import { binaryDigest } from "./npm-update-worker-lifecycle.mjs";
11
+ import { canonicalJson } from "./automation-provenance-contract.mjs";
12
+
13
+ /** An untracked, removed, aliased or changed original lock never becomes authority. */
14
+ export async function committedBun(cwd, command, proposal) {
15
+ const entry = (
16
+ await command(["ls-tree", "HEAD", "--", "bun.lock"])
17
+ ).stdout.toString();
18
+ const path = join(cwd, "bun.lock");
19
+ if (!entry) {
20
+ required(!existsSync(path), "unexpected Bun lock path");
21
+ try {
22
+ lstatSync(path);
23
+ required(false, "unexpected Bun lock alias");
24
+ } catch (error) {
25
+ if (error.code !== "ENOENT") throw error;
26
+ }
27
+ required(
28
+ !proposal || !Object.hasOwn(proposal, "bunLockSha256"),
29
+ "original Bun lock was removed"
30
+ );
31
+ return undefined;
32
+ }
33
+ required(
34
+ /^100644 blob [a-f0-9]{40}\tbun\.lock\n$/.test(entry),
35
+ "regular committed Bun lock required"
36
+ );
37
+ const stat = lstatSync(path);
38
+ required(
39
+ stat.isFile() &&
40
+ !stat.isSymbolicLink() &&
41
+ stat.nlink === 1 &&
42
+ realpathSync(path) === path,
43
+ "Bun lock path is aliased"
44
+ );
45
+ const bytes = (await command(["show", "HEAD:bun.lock"])).stdout;
46
+ const digest = sha256(bytes);
47
+ required(
48
+ proposal
49
+ ? proposal.bunLockSha256 === digest
50
+ : readBytes(path, 1_048_576, false).equals(bytes),
51
+ "original Bun lock bytes differ"
52
+ );
53
+ return bytes.toString("utf8");
54
+ }
55
+
56
+ /** Recheck the same regular executable identity and bytes around every native call. */
57
+ function toolIdentity(path) {
58
+ const stat = lstatSync(path);
59
+ required(
60
+ isAbsolute(path) &&
61
+ stat.isFile() &&
62
+ !stat.isSymbolicLink() &&
63
+ stat.nlink === 1 &&
64
+ realpathSync(path) === path,
65
+ "Bun executable is aliased"
66
+ );
67
+ return `${stat.dev}:${stat.ino}:${stat.uid}:${stat.mode}:${binaryDigest(path)}`;
68
+ }
69
+
70
+ /** Only the original trusted PATH may select a supported native Bun installation. */
71
+ export async function qualifiedBun(env, { executable, deadline } = {}) {
72
+ const path =
73
+ executable ??
74
+ (env.PATH ?? "")
75
+ .split(":")
76
+ .filter(isAbsolute)
77
+ .map(directory => join(directory, "bun"))
78
+ .find(existsSync);
79
+ required(typeof path === "string", "native Bun is unavailable");
80
+ const absolute = resolve(path);
81
+ const identity = toolIdentity(absolute);
82
+ const invoke = async (args, options = {}) => {
83
+ required(
84
+ toolIdentity(absolute) === identity,
85
+ "Bun executable identity changed"
86
+ );
87
+ const timeout = Math.min(
88
+ options.timeout ?? 120_000,
89
+ 120_000,
90
+ deadline === undefined ? 120_000 : deadline - Date.now()
91
+ );
92
+ required(
93
+ Number.isSafeInteger(timeout) && timeout > 0,
94
+ "original Bun operation deadline expired"
95
+ );
96
+ const result = await runProcess(absolute, args, {
97
+ ...options,
98
+ env: { ...env, BUN_FEATURE_FLAG_DISABLE_NATIVE_DEPENDENCY_LINKER: "1" },
99
+ timeout,
100
+ });
101
+ required(
102
+ toolIdentity(absolute) === identity,
103
+ "Bun executable changed during execution"
104
+ );
105
+ return result;
106
+ };
107
+ const version = await invoke(["--version"]);
108
+ required(
109
+ version.stdout.toString() === "1.3.8\n" && !version.stderr.length,
110
+ "Bun1.3.8 is required"
111
+ );
112
+ return invoke;
113
+ }
114
+
115
+ /** Native JSONC parsing is data-only; every Bun package must match a public npm lock identity. */
116
+ export async function verifyBunLock(bun, app, options = {}) {
117
+ const text = readBytes(join(app, "bun.lock"), 1_048_576, false);
118
+ const parsed = await bun(
119
+ [
120
+ "--eval",
121
+ "process.stdout.write(JSON.stringify(Bun.JSONC.parse(await Bun.stdin.text())))",
122
+ ],
123
+ { ...options, cwd: app, input: text, maximum: 1_048_576 }
124
+ );
125
+ const value = JSON.parse(parsed.stdout.toString());
126
+ const manifest = JSON.parse(readFileSync(join(app, "package.json"), "utf8"));
127
+ const npm = JSON.parse(readFileSync(join(app, "package-lock.json"), "utf8"));
128
+ required(
129
+ value.lockfileVersion === 1 &&
130
+ Object.keys(value.workspaces ?? {}).join() === "" &&
131
+ value.workspaces?.[""]?.name === manifest.name,
132
+ "unsupported Bun root lock"
133
+ );
134
+ for (const section of [
135
+ "dependencies",
136
+ "devDependencies",
137
+ "optionalDependencies",
138
+ "peerDependencies",
139
+ ])
140
+ required(
141
+ canonicalJson(value.workspaces[""][section] ?? {}) ===
142
+ canonicalJson(manifest[section] ?? {}),
143
+ "Bun root selections differ"
144
+ );
145
+ required(
146
+ value.packages &&
147
+ typeof value.packages === "object" &&
148
+ !Array.isArray(value.packages),
149
+ "Bun packages are absent"
150
+ );
151
+ const nodes = Object.entries(npm.packages).filter(([name]) => name !== "");
152
+ for (const row of Object.values(value.packages)) {
153
+ required(
154
+ Array.isArray(row) && row.length === 4 && typeof row[0] === "string",
155
+ "unsupported Bun package record"
156
+ );
157
+ const matching = nodes.some(
158
+ ([path, node]) =>
159
+ row[0] === `${path.split("node_modules/").at(-1)}@${node.version}` &&
160
+ row[3] === node.integrity &&
161
+ (row[1] === "" || row[1] === node.resolved)
162
+ );
163
+ required(matching, "Bun package identity differs from public npm lock");
164
+ }
165
+ }
166
+
167
+ /** Frozen native installation must preserve every prepared manifest and lock byte. */
168
+ export async function frozenBun(bun, app, files, options = {}) {
169
+ await verifyBunLock(bun, app, options);
170
+ const before = files.map(file =>
171
+ readBytes(join(app, file), 1_048_576, false)
172
+ );
173
+ await bun(
174
+ [
175
+ "install",
176
+ "--frozen-lockfile",
177
+ "--ignore-scripts",
178
+ "--registry",
179
+ "https://registry.npmjs.org/",
180
+ ],
181
+ { ...options, cwd: app }
182
+ );
183
+ required(
184
+ files.every((file, index) =>
185
+ readBytes(join(app, file), 1_048_576, false).equals(before[index])
186
+ ),
187
+ "frozen Bun rewrote manifest or lock"
188
+ );
189
+ }
@@ -28,7 +28,10 @@ import {
28
28
  import { sha256 } from "./github-attestation-verifier.mjs";
29
29
  import { verifyStaleOriginProvider } from "./github-attestation-provider.mjs";
30
30
  import { verifyHistoricalDescriptor } from "./github-attestation-recovery.mjs";
31
- import { DESCRIPTOR_KEYS } from "./automation-provenance-contract.mjs";
31
+ import {
32
+ DESCRIPTOR_KEYS,
33
+ optionalLockFields,
34
+ } from "./automation-provenance-contract.mjs";
32
35
 
33
36
  /** Only complete, digest-checked transport is considered; incomplete chunks grant nothing. */
34
37
  export function completeCheckpoints(comments) {
@@ -168,11 +171,11 @@ async function inspectOrigin(api, issue, payload, oldConfig, currentConfig) {
168
171
  }
169
172
 
170
173
  /** Descriptor fields preserve the complete original immutable proposal and claim. */
171
- function originalDescriptor(checkpoint, old, allocation, authority) {
174
+ export function originalDescriptor(checkpoint, old, allocation, authority) {
172
175
  const preview = checkpoint.payload.preview;
173
176
  keys(preview, ["descriptor", "message", "epoch"]);
174
177
  const d = preview.descriptor;
175
- keys(d, DESCRIPTOR_KEYS);
178
+ keys(d, [...DESCRIPTOR_KEYS, ...optionalLockFields(old)]);
176
179
  required(
177
180
  d.version === 1 &&
178
181
  d.parent === old.parent &&
@@ -183,6 +186,7 @@ function originalDescriptor(checkpoint, old, allocation, authority) {
183
186
  d.proposalKey === old.bindingKey &&
184
187
  d.policySha256 === sha256(canonicalJson(authority)) &&
185
188
  canonicalJson(d.files) === canonicalJson(old.hashes) &&
189
+ d.bunLockSha256 === old.bunLockSha256 &&
186
190
  canonicalJson(d.updates) === canonicalJson(old.updates) &&
187
191
  typeof preview.message === "string" &&
188
192
  sha256(preview.message) === d.messageSha256 &&
@@ -105,7 +105,12 @@ export function assertManualCancellation(
105
105
  /** The closed subject preserves one stale origin and one exact replacement, with no published destination. */
106
106
  export function validateCancellation(value, expected) {
107
107
  keys(value, RECORD_KEYS);
108
- keys(value.proposalHashes, FILES);
108
+ keys(
109
+ value.proposalHashes,
110
+ Object.hasOwn(expected.proposalHashes, "bun.lock")
111
+ ? ["bun.lock", ...FILES]
112
+ : FILES
113
+ );
109
114
  keys(value.destination, ["branch", "expectedBranchHead", "prNumber"]);
110
115
  required(
111
116
  value.version === 1 &&
@@ -10,6 +10,11 @@ import { required, keys } from "./npm-update-invariants.mjs";
10
10
  export { UpdaterError, required, keys } from "./npm-update-invariants.mjs";
11
11
  import { canonicalJson } from "../lisa-automation-provenance.mjs";
12
12
  import { sha256 } from "./github-attestation-verifier.mjs";
13
+ import {
14
+ optionalLockFields,
15
+ proposalFileNames,
16
+ } from "./automation-provenance-contract.mjs";
17
+ export { proposalFileNames } from "./automation-provenance-contract.mjs";
13
18
 
14
19
  export const FILES = ["package-lock.json", "package.json"];
15
20
  export const SECTIONS = [
@@ -207,9 +212,17 @@ export function selectionKey(policy, updates) {
207
212
  }
208
213
 
209
214
  /** Recovery binds policy as well as versions; the hook's signed proposal key is separate. */
210
- export function proposalFrom(policy, parent, before, files, updates) {
215
+ export function proposalFrom(
216
+ policy,
217
+ parent,
218
+ before,
219
+ files,
220
+ updates,
221
+ bunLockSha256
222
+ ) {
211
223
  required(OBJECT.test(parent), "unsupported Git parent or object format");
212
- keys(files, FILES);
224
+ const optional = bunLockSha256 === undefined ? {} : { bunLockSha256 };
225
+ const names = proposalFileNames({ files, ...optional });
213
226
  for (const value of Object.values(files))
214
227
  required(
215
228
  typeof value === "string" && Buffer.byteLength(value) <= 1_048_576,
@@ -228,6 +241,7 @@ export function proposalFrom(policy, parent, before, files, updates) {
228
241
  parent,
229
242
  policySha256: sha256(canonicalJson(policy)),
230
243
  updates: ordered,
244
+ ...optional,
231
245
  };
232
246
  return {
233
247
  version: 1,
@@ -235,11 +249,16 @@ export function proposalFrom(policy, parent, before, files, updates) {
235
249
  selectionKey: selectionKey(policy, ordered),
236
250
  key: sha256(canonicalJson(identity)),
237
251
  bindingKey: sha256(
238
- canonicalJson({ repository: policy.repository, parent, updates: ordered })
252
+ canonicalJson({
253
+ repository: policy.repository,
254
+ parent,
255
+ updates: ordered,
256
+ ...optional,
257
+ })
239
258
  ),
240
259
  before,
241
260
  files,
242
- hashes: Object.fromEntries(FILES.map(file => [file, sha256(files[file])])),
261
+ hashes: Object.fromEntries(names.map(file => [file, sha256(files[file])])),
243
262
  };
244
263
  }
245
264
 
@@ -260,13 +279,15 @@ export function validateProposal(value, policy) {
260
279
  "before",
261
280
  "files",
262
281
  "hashes",
282
+ ...optionalLockFields(value),
263
283
  ]);
264
284
  const expected = proposalFrom(
265
285
  policy,
266
286
  value.parent,
267
287
  value.before,
268
288
  value.files,
269
- value.updates
289
+ value.updates,
290
+ value.bunLockSha256
270
291
  );
271
292
  required(
272
293
  canonicalJson(value) === canonicalJson(expected),
@@ -10,7 +10,7 @@ import { writeFileSync } from "node:fs";
10
10
  import { join } from "node:path";
11
11
  import { canonicalJson } from "../lisa-automation-provenance.mjs";
12
12
  import {
13
- FILES,
13
+ proposalFileNames,
14
14
  OBJECT,
15
15
  required,
16
16
  validateProposal,
@@ -95,7 +95,7 @@ export async function descriptorFor({
95
95
  await baseline(cwd, env, proposal);
96
96
  const staged = { ...env, GIT_INDEX_FILE: join(root, "index") };
97
97
  await git(cwd, staged, ["read-tree", proposal.parent]);
98
- for (const file of FILES) {
98
+ for (const file of proposalFileNames(proposal)) {
99
99
  const blob = (
100
100
  await git(
101
101
  cwd,
@@ -125,6 +125,9 @@ export async function descriptorFor({
125
125
  author: identity,
126
126
  committer: identity,
127
127
  files: proposal.hashes,
128
+ ...(proposal.bunLockSha256 === undefined
129
+ ? {}
130
+ : { bunLockSha256: proposal.bunLockSha256 }),
128
131
  messageSha256: sha256(message),
129
132
  updates: proposal.updates,
130
133
  proposalKey: proposal.bindingKey,
@@ -161,9 +164,9 @@ async function stageProposal(context, env) {
161
164
  await withStage("gate-attach", () =>
162
165
  executeCanonicalHelper(context, "stage-read", "attach-branch")
163
166
  );
164
- for (const file of FILES)
167
+ for (const file of proposalFileNames(context.proposal))
165
168
  writeFileSync(join(cwd, file), proposal.files[file]);
166
- await command(["add", "--", ...FILES]);
169
+ await command(["add", "--", ...proposalFileNames(context.proposal)]);
167
170
  return branch;
168
171
  }
169
172
 
@@ -192,7 +195,7 @@ async function committedProposal(
192
195
  .split("\n")
193
196
  .sort();
194
197
  required(
195
- changed.join("\n") === FILES.join("\n"),
198
+ changed.join("\n") === proposalFileNames(proposal).join("\n"),
196
199
  "committed diff includes foreign paths"
197
200
  );
198
201
  const ranges = (
@@ -7,7 +7,7 @@ import { assertPinnedVerifier } from "./github-attestation-verifier.mjs";
7
7
  import { required } from "./npm-update-contract.mjs";
8
8
  import { runProcess } from "./npm-update-process.mjs";
9
9
  import { gitObjectId } from "./npm-update-object.mjs";
10
- import { FILES } from "./npm-update-contract.mjs";
10
+ import { proposalFileNames } from "./npm-update-contract.mjs";
11
11
  import { executeCanonicalHelper } from "./npm-update-controller-factory.mjs";
12
12
  /** Explicit minimum gh identity and token; no caller/candidate executable override. */
13
13
  const HOST_OPTION = "--hostname";
@@ -179,7 +179,7 @@ export class GitHub {
179
179
  async gitCommit(proposal, descriptor, commit, authorize, cwd) {
180
180
  const path = `repos/${this.policy.repository}/git`;
181
181
  const tree = [];
182
- for (const file of FILES) {
182
+ for (const file of proposalFileNames(proposal)) {
183
183
  await authorize();
184
184
  const bytes = Buffer.from(proposal.files[file]);
185
185
  const blob = await this.request(`${path}/blobs`, "POST", {
@@ -29,9 +29,8 @@ const COMMON = [
29
29
  "lib/github-attestation-provider.mjs",
30
30
  "lib/github-attestation-recovery.mjs",
31
31
  "lib/automation-provenance-contract.mjs",
32
+ "lib/automation-provenance-files.mjs",
32
33
  "lib/automation-provenance-local.mjs",
33
- ];
34
- const RAILS = [
35
34
  "lisa-clean-git-env.sh",
36
35
  "lisa-scratch-run.sh",
37
36
  "check-threshold-ratchet.mjs",
@@ -42,6 +41,7 @@ const PRODUCER = [
42
41
  "allocate",
43
42
  "authorization",
44
43
  "broker-client",
44
+ "bun",
45
45
  "cancel-origin",
46
46
  "cancellation",
47
47
  "cancellation-proof",
@@ -125,7 +125,6 @@ export function managedTemplateMembers() {
125
125
  "npm-updater-gate.Dockerfile",
126
126
  ...PRODUCER.map(name => `lib/npm-update-${name}.mjs`),
127
127
  ].map(member => [member, `all/copy-overwrite/scripts/${member}`]),
128
- ...RAILS.map(member => [member, `rails/copy-overwrite/scripts/${member}`]),
129
- ["lisa-mutation.sh", "rails/copy-contents/scripts/lisa-mutation.sh"],
128
+ ["lisa-mutation.sh", "all/copy-contents/scripts/lisa-mutation.sh"],
130
129
  ]);
131
130
  }