@kungfu-tech/buildchain 3.0.9-alpha.2 → 3.0.9-alpha.4

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 (50) hide show
  1. package/contracts/fixtures/next-development-transition-v1/anchored-manual-waiting.json +47 -0
  2. package/contracts/fixtures/next-development-transition-v1/semver-auto-planned.json +47 -0
  3. package/contracts/fixtures/next-development-transition-v1/version-model-cases.json +40 -0
  4. package/contracts/next-development-request-v1.schema.json +66 -0
  5. package/contracts/next-development-transition-v1.schema.json +292 -0
  6. package/dist/site/buildchain-contract.json +4 -4
  7. package/dist/site/buildchain-site.json +89 -18
  8. package/dist/site/capability-registry.json +4 -3
  9. package/dist/site/kfd-claims.json +65 -5
  10. package/dist/site/kfd-upstream-aggregate.json +1 -1
  11. package/dist/site/manual-registry.json +19 -3
  12. package/dist/site/node-api-registry.json +1017 -37
  13. package/dist/site/page-registry.json +74 -11
  14. package/dist/site/public-surface-audit.json +7 -3
  15. package/dist/site/publication-registry.json +4 -4
  16. package/dist/site/release-provenance.json +3 -0
  17. package/dist/site/site-manifest.json +15 -7
  18. package/dist/site/workflow-registry.json +4 -4
  19. package/docs/MAP.md +2 -1
  20. package/docs/aws-us-elastic-runner-burst-plane.md +25 -3
  21. package/docs/dev-alpha-candidate-patrol.md +8 -0
  22. package/docs/next-development-transition.md +119 -0
  23. package/docs/node-api-reference.md +126 -73
  24. package/docs/versioning.md +1 -1
  25. package/package.json +8 -3
  26. package/packages/core/buildchain-agent-manuals.js +1 -0
  27. package/packages/core/channel-candidate.js +8 -0
  28. package/packages/core/channel-promotion-baseline.js +52 -0
  29. package/packages/core/dev-alpha-candidate-selection.js +10 -2
  30. package/packages/core/next-development-candidate-reservation.js +186 -0
  31. package/packages/core/next-development-controller.js +726 -0
  32. package/packages/core/next-development-projection.js +288 -0
  33. package/packages/core/next-development-transition.js +738 -0
  34. package/packages/core/paper-agent-entry.js +7 -4
  35. package/packages/core/paper.js +14 -7
  36. package/packages/core/release-passport-contract.js +1 -1
  37. package/scripts/aws-macos-jit-controller-core.mjs +109 -4
  38. package/scripts/aws-macos-jit-controller.mjs +71 -2
  39. package/scripts/aws-macos-jit-instance-rehydrate.mjs +260 -0
  40. package/scripts/aws-macos-jit-source-rebind.mjs +71 -14
  41. package/scripts/check-inventory.mjs +11 -0
  42. package/scripts/dev-alpha-candidate-patrol.mjs +22 -1
  43. package/scripts/generate-next-development-guidance.mjs +49 -0
  44. package/scripts/generate-site-bundle.mjs +2 -0
  45. package/scripts/init-repo.mjs +119 -62
  46. package/scripts/next-development-self-dogfood-harness.mjs +409 -0
  47. package/scripts/next-development-self-dogfood.mjs +532 -0
  48. package/scripts/next-development-transition.mjs +47 -0
  49. package/scripts/site-capability-metadata.mjs +3 -0
  50. package/scripts/stable-candidate-qualification.mjs +7 -7
@@ -97,6 +97,14 @@ export function decideChannelCandidate(input) {
97
97
  const sourceSha = sha(input.sourceSha, "sourceSha");
98
98
  const targetSha = sha(input.targetSha, "targetSha");
99
99
  const selection = normalizeChannelCandidateSelection(input.selection);
100
+ if (
101
+ selection?.versionReservation &&
102
+ selection.versionReservation.candidateSha !== sourceSha
103
+ ) {
104
+ throw new Error(
105
+ "selection version reservation must bind the selected source SHA",
106
+ );
107
+ }
100
108
  const now = required(input.now || new Date().toISOString(), "now");
101
109
  const maxAgeSeconds = Number(input.maxAgeSeconds ?? 7 * 24 * 60 * 60);
102
110
  if (!Number.isSafeInteger(maxAgeSeconds) || maxAgeSeconds <= 0) {
@@ -1,5 +1,10 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
 
3
+ import {
4
+ nextDevelopmentPatrolContext,
5
+ normalizeNextDevelopmentVersionReservation,
6
+ } from "./next-development-candidate-reservation.js";
7
+
3
8
  const SHA = /^[0-9a-f]{40}$/;
4
9
 
5
10
  function text(value = "") {
@@ -59,6 +64,18 @@ export function normalizeChannelCandidateSelection(input) {
59
64
  ),
60
65
  }
61
66
  : {}),
67
+ ...(input.versionReservation
68
+ ? {
69
+ versionReservation: normalizeNextDevelopmentVersionReservation(
70
+ input.versionReservation,
71
+ {
72
+ reservationSha: input.versionReservation.reservationSha,
73
+ candidateSha: input.versionReservation.candidateSha,
74
+ targetVersion: input.versionReservation.targetVersion,
75
+ },
76
+ ),
77
+ }
78
+ : {}),
62
79
  };
63
80
  }
64
81
 
@@ -117,6 +134,7 @@ export async function resolvePatrolSourceInputs({
117
134
  let comparison = headComparison;
118
135
  let workflowEvidence = [];
119
136
  let skippedNewerCommitCount = 0;
137
+ let versionReservation;
120
138
  let qualificationError;
121
139
  if (
122
140
  headComparison.status === "ahead" &&
@@ -130,6 +148,8 @@ export async function resolvePatrolSourceInputs({
130
148
  ),
131
149
  ]);
132
150
  try {
151
+ const { preparations, ignoredSourceShas } =
152
+ nextDevelopmentPatrolContext(sourceHistory);
133
153
  const selected = selectQualifiedSource({
134
154
  sourceHistory,
135
155
  workflowRunsByPath: new Map(
@@ -141,8 +161,39 @@ export async function resolvePatrolSourceInputs({
141
161
  requiredWorkflowPaths,
142
162
  now: options.now,
143
163
  maxAgeSeconds: options.maxAgeSeconds,
164
+ ignoredSourceShas,
144
165
  });
145
166
  ({ sourceSha, skippedNewerCommitCount, workflowEvidence } = selected);
167
+ const selectedIndex = sourceHistory.findIndex((entry) =>
168
+ (typeof entry === "string" ? entry : entry?.sha) === sourceSha,
169
+ );
170
+ const reservation = preparations.find(
171
+ ({ index }) => index > selectedIndex,
172
+ )?.preparation;
173
+ if (reservation) {
174
+ if (typeof client.readNextDevelopmentVersionReservation !== "function") {
175
+ throw new Error(
176
+ "candidate follows next-development preparation but reservation readback is unavailable",
177
+ );
178
+ }
179
+ versionReservation = normalizeNextDevelopmentVersionReservation(
180
+ await client.readNextDevelopmentVersionReservation({
181
+ reservationSha: reservation.sha,
182
+ candidateSha: sourceSha,
183
+ targetVersion: reservation.targetVersion,
184
+ }),
185
+ {
186
+ reservationSha: reservation.sha,
187
+ candidateSha: sourceSha,
188
+ targetVersion: reservation.targetVersion,
189
+ },
190
+ );
191
+ if (versionReservation.status !== "current") {
192
+ throw new Error(
193
+ `next-development version reservation is ${versionReservation.status} for candidate ${sourceSha}`,
194
+ );
195
+ }
196
+ }
146
197
  if (sourceSha !== observedSourceHeadSha)
147
198
  comparison = await client.compare(boundarySha, sourceSha);
148
199
  } catch (error) {
@@ -158,6 +209,7 @@ export async function resolvePatrolSourceInputs({
158
209
  comparison,
159
210
  workflowEvidence,
160
211
  skippedNewerCommitCount,
212
+ versionReservation,
161
213
  qualificationError,
162
214
  };
163
215
  }
@@ -82,7 +82,9 @@ export function selectLatestQualifiedSource({
82
82
  requiredWorkflowPaths,
83
83
  now,
84
84
  maxAgeSeconds,
85
+ ignoredSourceShas = [],
85
86
  }) {
87
+ const ignored = new Set(ignoredSourceShas);
86
88
  const latestByPath = new Map(
87
89
  requiredWorkflowPaths.map((workflow) => [
88
90
  workflow,
@@ -90,7 +92,11 @@ export function selectLatestQualifiedSource({
90
92
  ]),
91
93
  );
92
94
  for (let index = 0; index < sourceHistory.length; index += 1) {
93
- const sourceSha = sourceHistory[index];
95
+ const sourceSha =
96
+ typeof sourceHistory[index] === "string"
97
+ ? sourceHistory[index]
98
+ : sourceHistory[index]?.sha;
99
+ if (ignored.has(sourceSha)) continue;
94
100
  const rows = requiredWorkflowPaths.map((workflow) =>
95
101
  latestByPath.get(workflow).get(sourceSha),
96
102
  );
@@ -115,7 +121,9 @@ export function selectLatestQualifiedSource({
115
121
  };
116
122
  }
117
123
  }
118
- const staleSuccessfulPair = sourceHistory.some((sourceSha) => {
124
+ const staleSuccessfulPair = sourceHistory.some((entry) => {
125
+ const sourceSha = typeof entry === "string" ? entry : entry?.sha;
126
+ if (ignored.has(sourceSha)) return false;
119
127
  const rows = requiredWorkflowPaths.map((workflow) =>
120
128
  latestByPath.get(workflow).get(sourceSha),
121
129
  );
@@ -0,0 +1,186 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+
3
+ import crypto from "node:crypto";
4
+
5
+ const SHA = /^[0-9a-f]{40}$/u;
6
+ const ROOT = /^sha256:[0-9a-f]{64}$/u;
7
+
8
+ function text(value = "") {
9
+ return String(value ?? "").trim();
10
+ }
11
+
12
+ function exactSha(value, label) {
13
+ const normalized = text(value).toLowerCase();
14
+ if (!SHA.test(normalized)) {
15
+ throw new Error(`${label} must be an exact 40-character SHA`);
16
+ }
17
+ return normalized;
18
+ }
19
+
20
+ function exactRoot(value, label) {
21
+ const normalized = text(value).toLowerCase();
22
+ if (!ROOT.test(normalized)) {
23
+ throw new Error(`${label} must be an exact sha256 content root`);
24
+ }
25
+ return normalized;
26
+ }
27
+
28
+ function canonical(value) {
29
+ if (Array.isArray(value)) return value.map(canonical);
30
+ if (value && typeof value === "object") {
31
+ return Object.fromEntries(
32
+ Object.entries(value)
33
+ .sort(([left], [right]) => left.localeCompare(right))
34
+ .map(([key, item]) => [key, canonical(item)]),
35
+ );
36
+ }
37
+ return value;
38
+ }
39
+
40
+ function evidenceRoot(value) {
41
+ return `sha256:${crypto
42
+ .createHash("sha256")
43
+ .update(JSON.stringify(canonical(value)))
44
+ .digest("hex")}`;
45
+ }
46
+
47
+ export function nextDevelopmentPreparedVersion(message) {
48
+ const title = text(message).split("\n", 1)[0];
49
+ return (
50
+ title.match(
51
+ /^(?:chore\(release\): prepare|Prepare)\s+v?([0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?)$/u,
52
+ )?.[1] || ""
53
+ );
54
+ }
55
+
56
+ function preparation(entry) {
57
+ if (!entry || typeof entry === "string") return undefined;
58
+ const targetVersion = nextDevelopmentPreparedVersion(entry.message);
59
+ if (!targetVersion) return undefined;
60
+ return {
61
+ sha: exactSha(entry.sha, "sourceHistory.sha"),
62
+ targetVersion,
63
+ };
64
+ }
65
+
66
+ export function nextDevelopmentPatrolContext(sourceHistory) {
67
+ const preparations = sourceHistory
68
+ .map((entry, index) => ({ index, preparation: preparation(entry) }))
69
+ .filter((entry) => entry.preparation);
70
+ const preparationShas = new Set(
71
+ preparations.map(({ preparation: item }) => item.sha),
72
+ );
73
+ const integrationShas = sourceHistory
74
+ .filter(
75
+ (entry) =>
76
+ entry &&
77
+ typeof entry !== "string" &&
78
+ Array.isArray(entry.parents) &&
79
+ entry.parents.length === 2 &&
80
+ entry.parents.some((parent) => preparationShas.has(parent)),
81
+ )
82
+ .map((entry) => exactSha(entry.sha, "sourceHistory.sha"));
83
+ return {
84
+ preparations,
85
+ ignoredSourceShas: [...preparationShas, ...integrationShas],
86
+ };
87
+ }
88
+
89
+ export function normalizeNextDevelopmentVersionReservation(input, expected) {
90
+ const status = text(input?.status);
91
+ if (!["current", "absent", "stale"].includes(status)) {
92
+ throw new Error("next-development version reservation status is invalid");
93
+ }
94
+ const normalized = {
95
+ status,
96
+ reservationSha: exactSha(
97
+ input?.reservationSha,
98
+ "versionReservation.reservationSha",
99
+ ),
100
+ candidateSha: exactSha(
101
+ input?.candidateSha,
102
+ "versionReservation.candidateSha",
103
+ ),
104
+ targetVersion: text(input?.targetVersion),
105
+ paths: [...new Set((input?.paths || []).map(text))].sort(),
106
+ evidenceRoot: exactRoot(
107
+ input?.evidenceRoot,
108
+ "versionReservation.evidenceRoot",
109
+ ),
110
+ };
111
+ if (
112
+ normalized.reservationSha !== expected.reservationSha ||
113
+ normalized.candidateSha !== expected.candidateSha ||
114
+ normalized.targetVersion !== expected.targetVersion
115
+ ) {
116
+ throw new Error("next-development version reservation identity drifted");
117
+ }
118
+ if (normalized.paths.length === 0 || normalized.paths.some((path) => !path)) {
119
+ throw new Error("next-development version reservation paths are required");
120
+ }
121
+ return normalized;
122
+ }
123
+
124
+ function encodeRepositoryPath(value) {
125
+ return value.split("/").map(encodeURIComponent).join("/");
126
+ }
127
+
128
+ export async function readGitHubNextDevelopmentVersionReservation({
129
+ api,
130
+ owner,
131
+ repo,
132
+ reservationSha,
133
+ candidateSha,
134
+ targetVersion,
135
+ }) {
136
+ const commit = await api(
137
+ `/repos/${owner}/${repo}/commits/${encodeURIComponent(reservationSha)}`,
138
+ );
139
+ if (
140
+ text(commit.sha) !== reservationSha ||
141
+ nextDevelopmentPreparedVersion(commit.commit?.message) !== targetVersion
142
+ ) {
143
+ throw new Error(
144
+ "next-development preparation commit identity or version drifted",
145
+ );
146
+ }
147
+ const paths = [
148
+ ...new Set((commit.files || []).map((file) => text(file.filename))),
149
+ ]
150
+ .filter(Boolean)
151
+ .sort();
152
+ if (paths.length === 0) {
153
+ throw new Error(
154
+ "next-development preparation commit has no version-state paths",
155
+ );
156
+ }
157
+ const observations = [];
158
+ let status = "current";
159
+ for (const filePath of paths) {
160
+ const endpoint = `/repos/${owner}/${repo}/contents/${encodeRepositoryPath(filePath)}`;
161
+ const prepared = await api(
162
+ `${endpoint}?ref=${encodeURIComponent(reservationSha)}`,
163
+ { allow404: true },
164
+ );
165
+ const candidate = await api(
166
+ `${endpoint}?ref=${encodeURIComponent(candidateSha)}`,
167
+ { allow404: true },
168
+ );
169
+ const preparedBlob =
170
+ text(prepared?.type) === "file" ? text(prepared?.sha) : "";
171
+ const candidateBlob =
172
+ text(candidate?.type) === "file" ? text(candidate?.sha) : "";
173
+ if (!candidateBlob) status = "absent";
174
+ else if (!preparedBlob || candidateBlob !== preparedBlob) status = "stale";
175
+ observations.push({ path: filePath, preparedBlob, candidateBlob });
176
+ }
177
+ const body = {
178
+ status,
179
+ reservationSha,
180
+ candidateSha,
181
+ targetVersion,
182
+ paths,
183
+ observations,
184
+ };
185
+ return { ...body, evidenceRoot: evidenceRoot(body) };
186
+ }