@kungfu-tech/buildchain 3.0.6 → 3.0.7-alpha.1

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 (103) hide show
  1. package/README.md +4 -2
  2. package/actions/promote-buildchain-ref/README.md +10 -0
  3. package/actions/release-tail/README.md +17 -0
  4. package/bin/buildchain.mjs +11 -0
  5. package/bin/internal/command-registry.mjs +2 -0
  6. package/contracts/auditable-demo-scenario-v1.schema.json +1 -1
  7. package/contracts/engineering-housekeeper-v1.schema.json +143 -0
  8. package/contracts/fixtures/engineering-housekeeper-v1/cases.json +68 -0
  9. package/contracts/fixtures/release-tail-capabilities-v1/kungfu-alpha.json +273 -0
  10. package/contracts/publication-rehearsal-capsule-v1.schema.json +173 -0
  11. package/contracts/release-tail-capabilities-v1.schema.json +199 -0
  12. package/contracts/release-tail-provider-bindings-v1.schema.json +56 -0
  13. package/dist/site/agent-index.json +3 -0
  14. package/dist/site/artifact-schemas.json +6 -0
  15. package/dist/site/buildchain-contract.json +42 -27
  16. package/dist/site/buildchain-site.json +384 -33
  17. package/dist/site/capability-registry.json +15 -12
  18. package/dist/site/cli-registry.json +102 -0
  19. package/dist/site/controller-registry.json +19 -3
  20. package/dist/site/kfd-claims.json +359 -17
  21. package/dist/site/kfd-upstream-aggregate.json +1 -1
  22. package/dist/site/manual-registry.json +53 -7
  23. package/dist/site/node-api-registry.json +5882 -3605
  24. package/dist/site/page-registry.json +348 -21
  25. package/dist/site/public-surface-audit.json +396 -16
  26. package/dist/site/publication-authority-registry.json +106 -1
  27. package/dist/site/publication-registry.json +4 -4
  28. package/dist/site/release-provenance.json +7 -0
  29. package/dist/site/schemas/publication-rehearsal-capsule-v1.schema.json +269 -0
  30. package/dist/site/schemas/release-tail-capabilities-v1.schema.json +353 -0
  31. package/dist/site/schemas/release-tail-provider-bindings-v1.schema.json +94 -0
  32. package/dist/site/site-manifest.json +35 -11
  33. package/dist/site/workflow-registry.json +204 -10
  34. package/docs/MAP.md +6 -2
  35. package/docs/auditable-demo.md +2 -2
  36. package/docs/cli-reference.md +136 -0
  37. package/docs/dev-delivery-warrant.md +49 -4
  38. package/docs/engineering-housekeeper.md +138 -0
  39. package/docs/lifecycle-protocol.md +4 -2
  40. package/docs/node-api-reference.md +495 -281
  41. package/docs/publication-rehearsal.md +94 -0
  42. package/docs/release-governance.md +17 -2
  43. package/docs/release-tail-contract.md +160 -0
  44. package/docs/release-tail-provider-plane.md +120 -0
  45. package/docs/reusable-build-surface.md +11 -0
  46. package/package.json +11 -3
  47. package/packages/core/artifact-signing.js +61 -0
  48. package/packages/core/buildchain-agent-manuals.js +3 -0
  49. package/packages/core/buildchain-config.js +66 -6
  50. package/packages/core/buildchain-kfd-claims.js +1 -1
  51. package/packages/core/buildchain-publication-authority.js +5 -0
  52. package/packages/core/controller-evidence.js +2 -1
  53. package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
  54. package/packages/core/dev-delivery-warrant-shadow.js +502 -0
  55. package/packages/core/dev-delivery-warrant.js +15 -6
  56. package/packages/core/diagnostics.js +8 -3
  57. package/packages/core/engineering-housekeeper-github-client.js +222 -0
  58. package/packages/core/engineering-housekeeper-github.js +501 -0
  59. package/packages/core/engineering-housekeeper.js +259 -0
  60. package/packages/core/index.js +42 -0
  61. package/packages/core/kfd-gate.js +45 -15
  62. package/packages/core/paper-agent-entry.js +11 -5
  63. package/packages/core/paper-repository.js +1 -0
  64. package/packages/core/paper-scaffold-content.js +21 -0
  65. package/packages/core/paper.js +28 -2
  66. package/packages/core/publication-rehearsal-projection.js +173 -0
  67. package/packages/core/publication-rehearsal-runtime.js +921 -0
  68. package/packages/core/release-passport.js +130 -20
  69. package/packages/core/release-tail-compatibility.js +60 -0
  70. package/packages/core/release-tail-provider-adapters.js +461 -0
  71. package/packages/core/release-tail-provider-plane.js +1228 -0
  72. package/scripts/assemble-publication-artifact-admission.mjs +1 -1
  73. package/scripts/assemble-self-publication-admission.mjs +2 -2
  74. package/scripts/audit-publication-control-plane.mjs +1 -1
  75. package/scripts/auditable-demo-bundle-verification.mjs +2 -3
  76. package/scripts/auditable-demo-platform.mjs +2 -2
  77. package/scripts/auditable-demo-renditions.mjs +1 -1
  78. package/scripts/auditable-demo.mjs +2 -2
  79. package/scripts/build-contract-core.mjs +8 -3
  80. package/scripts/build-standalone-binary.mjs +14 -3
  81. package/scripts/buildchain-cli-help.mjs +13 -0
  82. package/scripts/check-core-mechanism-inventory.mjs +347 -0
  83. package/scripts/check-inventory.mjs +10 -8
  84. package/scripts/check-maintainability.mjs +9 -2
  85. package/scripts/check-release-tail-contract.mjs +435 -0
  86. package/scripts/dev-delivery-warrant.mjs +31 -4
  87. package/scripts/dev-pr-auto-merge.mjs +30 -4
  88. package/scripts/dev-pr-delivery-warrant.mjs +50 -0
  89. package/scripts/engineering-housekeeper-workflow.mjs +394 -0
  90. package/scripts/generate-channel-promotion-workflow.mjs +10 -8
  91. package/scripts/generate-site-bundle.mjs +47 -4
  92. package/scripts/init-repo.mjs +26 -2
  93. package/scripts/inspect-artifact-signing-requests.mjs +6 -0
  94. package/scripts/materialize-self-release-candidate-version.mjs +137 -0
  95. package/scripts/publication-commit-evidence.mjs +69 -23
  96. package/scripts/release-candidate-resolver.mjs +16 -10
  97. package/scripts/release-tail.mjs +159 -0
  98. package/scripts/resume-from-candidate-run.mjs +123 -9
  99. package/scripts/seal-artifact-signing-requests.mjs +6 -0
  100. package/scripts/site-capability-metadata.mjs +13 -0
  101. package/scripts/v4-architecture.mjs +600 -0
  102. package/scripts/web-surface-core.mjs +8 -2
  103. package/scripts/workflow-call-contract.mjs +184 -5
@@ -69,6 +69,7 @@ function normalizeCandidate(input, expected) {
69
69
  closureRoot: exactRoot(input.closureRoot, "closureRoot"),
70
70
  dependencyRoot: exactRoot(input.dependencyRoot, "dependencyRoot"),
71
71
  toolchainRoot: exactRoot(input.toolchainRoot, "toolchainRoot"),
72
+ ...(Object.hasOwn(input, "sourceWorkflowRunId") ? { sourceWorkflowRunId: nonNegativeInteger(input.sourceWorkflowRunId, "candidate sourceWorkflowRunId", 0) } : {}),
72
73
  priority: priority(input.priority),
73
74
  enqueuedAt: timestamp(input.enqueuedAt, "candidate enqueuedAt"),
74
75
  updatedAt: timestamp(input.updatedAt || input.enqueuedAt, "candidate updatedAt"),
@@ -178,6 +179,7 @@ function submissionReceipt({ before, after, candidate, action, now }) {
178
179
  closureRoot: candidate.closureRoot,
179
180
  dependencyRoot: candidate.dependencyRoot,
180
181
  toolchainRoot: candidate.toolchainRoot,
182
+ sourceWorkflowRunId: candidate.sourceWorkflowRunId,
181
183
  deliveryClass: candidate.deliveryClass,
182
184
  priority: candidate.priority,
183
185
  retainedEnqueuedAt: candidate.enqueuedAt,
@@ -223,7 +225,12 @@ export function submitDevDeliveryCandidate(queueInput, input, { now = new Date()
223
225
  selected = existing;
224
226
  } else {
225
227
  if (before.activeWarrant?.candidateId === existing.candidateId) {
226
- throw new Error("selected candidate sourceHead cannot change before terminal Warrant closeout");
228
+ if (existing.sourceHead !== attemptedCandidate.sourceHead) {
229
+ throw new Error("selected candidate sourceHead cannot change before terminal Warrant closeout");
230
+ }
231
+ action = "active-warrant-retained-noop";
232
+ selected = existing;
233
+ return { candidate: selected, action };
227
234
  }
228
235
  const headChanged = existing.sourceHead !== attemptedCandidate.sourceHead;
229
236
  existing.sourceHead = attemptedCandidate.sourceHead;
@@ -309,7 +316,7 @@ export function selectDevDeliveryWarrant(queueInput, { now = new Date().toISOStr
309
316
  candidateId: queue.activeWarrant.candidateId,
310
317
  fencingToken: queue.activeWarrant.fencingToken,
311
318
  leaseGeneration: queue.activeWarrant.generation,
312
- expectedOldStateRoot: queue.stateRoot,
319
+ expectedOldStateRoot: recoveryReceipt?.expectedOldStateRoot || queue.stateRoot,
313
320
  nextStateRoot: queue.stateRoot,
314
321
  nextAction: "Continue the active delivery attempt; later candidates remain visibly queued.",
315
322
  };
@@ -363,6 +370,7 @@ export function selectDevDeliveryWarrant(queueInput, { now = new Date().toISOStr
363
370
  closureRoot: candidate.closureRoot,
364
371
  dependencyRoot: candidate.dependencyRoot,
365
372
  toolchainRoot: candidate.toolchainRoot,
373
+ sourceWorkflowRunId: candidate.sourceWorkflowRunId,
366
374
  deliveryClass: candidate.deliveryClass,
367
375
  generation: next.fencingCounter,
368
376
  expectedOldStateRoot: before.stateRoot,
@@ -397,6 +405,7 @@ export function selectDevDeliveryWarrant(queueInput, { now = new Date().toISOStr
397
405
  closureRoot: transaction.result.candidate.closureRoot,
398
406
  dependencyRoot: transaction.result.candidate.dependencyRoot,
399
407
  toolchainRoot: transaction.result.candidate.toolchainRoot,
408
+ sourceWorkflowRunId: transaction.result.candidate.sourceWorkflowRunId,
400
409
  deliveryClass: transaction.result.candidate.deliveryClass,
401
410
  queueAgeSeconds: selected.priority.ageSeconds,
402
411
  basePriority: selected.priority.basePriority,
@@ -404,7 +413,7 @@ export function selectDevDeliveryWarrant(queueInput, { now = new Date().toISOStr
404
413
  effectivePriority: selected.priority.score,
405
414
  fencingToken: transaction.result.warrant.fencingToken,
406
415
  leaseGeneration: transaction.result.warrant.generation,
407
- expectedOldStateRoot: transaction.expectedOldStateRoot,
416
+ expectedOldStateRoot: recoveryReceipt?.expectedOldStateRoot || transaction.expectedOldStateRoot,
408
417
  nextStateRoot: transaction.after.stateRoot,
409
418
  nextAction: transaction.result.warrant.nextAction,
410
419
  };
@@ -417,12 +426,12 @@ export function selectDevDeliveryWarrant(queueInput, { now = new Date().toISOStr
417
426
  };
418
427
  }
419
428
 
420
- function assertWarrantMutation(queue, warrant, now) {
429
+ function assertWarrantMutation(queue, warrant, now, { allowExpired = false } = {}) {
421
430
  if (!queue.activeWarrant) throw new Error("no active Delivery Warrant");
422
431
  if (text(warrant?.fencingToken) !== queue.activeWarrant.fencingToken) throw new Error("stale fencing token");
423
432
  if (Number(warrant?.generation) !== queue.activeWarrant.generation) throw new Error("stale lease generation");
424
433
  if (text(warrant?.candidateId) !== queue.activeWarrant.candidateId) throw new Error("Warrant candidate mismatch");
425
- if (Date.parse(queue.activeWarrant.expiresAt) <= Date.parse(now)) throw new Error("Delivery Warrant lease expired");
434
+ if (!allowExpired && Date.parse(queue.activeWarrant.expiresAt) <= Date.parse(now)) throw new Error("Delivery Warrant lease expired");
426
435
  }
427
436
 
428
437
  export function heartbeatDevDeliveryWarrant(queueInput, warrant, { now = new Date().toISOString(), leaseSeconds } = {}) {
@@ -517,7 +526,7 @@ export function closeDevDeliveryWarrant(queueInput, warrant, { outcome, evidence
517
526
  const transaction = transition(
518
527
  queueInput,
519
528
  (queue, before) => {
520
- assertWarrantMutation(before, warrant, currentTime);
529
+ assertWarrantMutation(before, warrant, currentTime, { allowExpired: true });
521
530
  const active = clone(queue.activeWarrant);
522
531
  const candidate = queue.candidates.find((entry) => entry.candidateId === active.candidateId);
523
532
  candidate.status = normalizedOutcome;
@@ -84,10 +84,14 @@ function fileStats(cwd, entries = []) {
84
84
  });
85
85
  }
86
86
 
87
- function commandVersion(command, args = ["--version"], cwd = process.cwd()) {
87
+ const TOOL_VERSION_PROBE_CWD = path.parse(process.execPath).root;
88
+
89
+ function commandVersion(command, args = ["--version"]) {
88
90
  try {
89
91
  return execFileSync(command, args, {
90
- cwd,
92
+ // Package managers may walk every parent even for --version. Keep this
93
+ // bounded instead of scanning an arbitrary consumer or shared temp root.
94
+ cwd: TOOL_VERSION_PROBE_CWD,
91
95
  encoding: "utf8",
92
96
  stdio: ["ignore", "pipe", "ignore"],
93
97
  timeout: 5000,
@@ -493,7 +497,8 @@ export function collectRunnerDiagnostics() {
493
497
  }
494
498
 
495
499
  export function collectToolDiagnostics({ cwd = process.cwd(), tools = ["node", "pnpm", "npm", "git", "cmake", "ninja", "ccache", "sccache"] } = {}) {
496
- return Object.fromEntries(tools.map((tool) => [tool, { version: commandVersion(tool, ["--version"], cwd) }]));
500
+ void cwd;
501
+ return Object.fromEntries(tools.map((tool) => [tool, { version: commandVersion(tool) }]));
497
502
  }
498
503
 
499
504
  function parseJsonDiagnostics(value = "") {
@@ -0,0 +1,222 @@
1
+ function requiredString(value, field) {
2
+ const normalized = String(value || "").trim();
3
+ if (!normalized) throw new Error(`${field} is required`);
4
+ return normalized;
5
+ }
6
+
7
+ function normalizeRepository(value) {
8
+ const normalized = requiredString(value?.fullName || value, "repository");
9
+ const match = normalized.match(/^([^/\s]+)\/([^/\s]+)$/);
10
+ if (!match) throw new Error(`repository must be owner/repo, got: ${normalized}`);
11
+ return { owner: match[1], repo: match[2], fullName: normalized };
12
+ }
13
+
14
+ function refPath(name) {
15
+ return String(name)
16
+ .split("/")
17
+ .map((part) => encodeURIComponent(part))
18
+ .join("/");
19
+ }
20
+
21
+ function parseLinkHeader(value) {
22
+ const links = {};
23
+ for (const part of String(value || "").split(",")) {
24
+ const match = part.match(/<([^>]+)>;\s*rel="([^"]+)"/);
25
+ if (match) links[match[2]] = match[1];
26
+ }
27
+ return links;
28
+ }
29
+
30
+ function branchHeadOid(branch) {
31
+ return String(
32
+ branch?.commit?.sha || branch?.object?.sha || branch?.headOid || "",
33
+ ).toLowerCase();
34
+ }
35
+
36
+ export class GitHubHousekeeperProviderError extends Error {
37
+ constructor(message, { operation = "github", status = 0, cause } = {}) {
38
+ super(message, { cause });
39
+ this.name = "GitHubHousekeeperProviderError";
40
+ this.operation = operation;
41
+ this.status = status;
42
+ }
43
+ }
44
+
45
+ function providerError(error, operation) {
46
+ if (error instanceof GitHubHousekeeperProviderError) return error;
47
+ return new GitHubHousekeeperProviderError(
48
+ `${operation} failed: ${error?.message || String(error)}`,
49
+ { operation, cause: error },
50
+ );
51
+ }
52
+
53
+ export class GitHubHousekeeperClient {
54
+ constructor({
55
+ token,
56
+ apiUrl = "https://api.github.com",
57
+ fetchImpl = globalThis.fetch,
58
+ } = {}) {
59
+ if (typeof fetchImpl !== "function") throw new Error("fetch is required");
60
+ this.token = requiredString(token, "GitHub token");
61
+ this.apiUrl = String(apiUrl).replace(/\/+$/, "");
62
+ this.fetch = fetchImpl;
63
+ }
64
+
65
+ async request(method, requestPath, { body } = {}) {
66
+ const url = requestPath.startsWith("http")
67
+ ? requestPath
68
+ : `${this.apiUrl}${requestPath}`;
69
+ if (new URL(url).origin !== new URL(this.apiUrl).origin) {
70
+ throw new GitHubHousekeeperProviderError(
71
+ `GitHub pagination cannot leave API origin: ${new URL(url).origin}`,
72
+ { operation: `${method} ${requestPath}` },
73
+ );
74
+ }
75
+ let response;
76
+ try {
77
+ response = await this.fetch(url, {
78
+ method,
79
+ headers: {
80
+ accept: "application/vnd.github+json",
81
+ authorization: `Bearer ${this.token}`,
82
+ "content-type": "application/json",
83
+ "x-github-api-version": "2022-11-28",
84
+ },
85
+ body: body === undefined ? undefined : JSON.stringify(body),
86
+ });
87
+ } catch (error) {
88
+ throw providerError(error, `${method} ${requestPath}`);
89
+ }
90
+ const text = await response.text();
91
+ let data = null;
92
+ try {
93
+ data = text ? JSON.parse(text) : null;
94
+ } catch (error) {
95
+ throw new GitHubHousekeeperProviderError(
96
+ `${method} ${requestPath} returned invalid JSON`,
97
+ {
98
+ operation: `${method} ${requestPath}`,
99
+ status: response.status,
100
+ cause: error,
101
+ },
102
+ );
103
+ }
104
+ if (!response.ok) {
105
+ throw new GitHubHousekeeperProviderError(
106
+ `${method} ${requestPath} failed with ${response.status}: ${data?.message || text}`,
107
+ { operation: `${method} ${requestPath}`, status: response.status },
108
+ );
109
+ }
110
+ return { data, response };
111
+ }
112
+
113
+ async paginate(requestPath, { maxPages = 1000 } = {}) {
114
+ const items = [];
115
+ const visited = new Set();
116
+ let next = requestPath;
117
+ while (next) {
118
+ if (visited.has(next)) {
119
+ throw new GitHubHousekeeperProviderError(
120
+ "GitHub pagination cycle detected",
121
+ { operation: `GET ${requestPath}` },
122
+ );
123
+ }
124
+ if (visited.size >= maxPages) {
125
+ throw new GitHubHousekeeperProviderError(
126
+ `GitHub pagination exceeded ${maxPages} pages`,
127
+ { operation: `GET ${requestPath}` },
128
+ );
129
+ }
130
+ visited.add(next);
131
+ const { data, response } = await this.request("GET", next);
132
+ if (!Array.isArray(data)) {
133
+ throw new GitHubHousekeeperProviderError(
134
+ "GitHub paginated response must be an array",
135
+ { operation: `GET ${next}` },
136
+ );
137
+ }
138
+ items.push(...data);
139
+ next = parseLinkHeader(response.headers.get("link")).next || "";
140
+ }
141
+ return items;
142
+ }
143
+
144
+ async getRepository(repository) {
145
+ const coordinate = normalizeRepository(repository);
146
+ return (
147
+ await this.request("GET", `/repos/${coordinate.owner}/${coordinate.repo}`)
148
+ ).data;
149
+ }
150
+
151
+ async listBranches(repository) {
152
+ const coordinate = normalizeRepository(repository);
153
+ return this.paginate(
154
+ `/repos/${coordinate.owner}/${coordinate.repo}/branches?per_page=100`,
155
+ );
156
+ }
157
+
158
+ async listOpenPullRequests(repository) {
159
+ const coordinate = normalizeRepository(repository);
160
+ return this.paginate(
161
+ `/repos/${coordinate.owner}/${coordinate.repo}/pulls?state=open&sort=updated&direction=asc&per_page=100`,
162
+ );
163
+ }
164
+
165
+ async getBranch(repository, name) {
166
+ const coordinate = normalizeRepository(repository);
167
+ return (
168
+ await this.request(
169
+ "GET",
170
+ `/repos/${coordinate.owner}/${coordinate.repo}/branches/${refPath(name)}`,
171
+ )
172
+ ).data;
173
+ }
174
+
175
+ async getPullRequest(repository, number) {
176
+ const coordinate = normalizeRepository(repository);
177
+ return (
178
+ await this.request(
179
+ "GET",
180
+ `/repos/${coordinate.owner}/${coordinate.repo}/pulls/${number}`,
181
+ )
182
+ ).data;
183
+ }
184
+
185
+ async compareCommits(repository, baseOid, headOid) {
186
+ const coordinate = normalizeRepository(repository);
187
+ return (
188
+ await this.request(
189
+ "GET",
190
+ `/repos/${coordinate.owner}/${coordinate.repo}/compare/${baseOid}...${headOid}`,
191
+ )
192
+ ).data;
193
+ }
194
+
195
+ async deleteBranch(repository, name, { expectedHeadOid } = {}) {
196
+ const coordinate = normalizeRepository(repository);
197
+ const currentHeadOid = branchHeadOid(
198
+ await this.getBranch(coordinate.fullName, name),
199
+ );
200
+ if (!expectedHeadOid || currentHeadOid !== expectedHeadOid) {
201
+ throw new GitHubHousekeeperProviderError(
202
+ `branch ${name} changed before delete`,
203
+ { operation: `delete-ref ${name}`, status: 409 },
204
+ );
205
+ }
206
+ await this.request(
207
+ "DELETE",
208
+ `/repos/${coordinate.owner}/${coordinate.repo}/git/refs/heads/${refPath(name)}`,
209
+ );
210
+ }
211
+
212
+ async addLabels(repository, number, labels) {
213
+ const coordinate = normalizeRepository(repository);
214
+ return (
215
+ await this.request(
216
+ "POST",
217
+ `/repos/${coordinate.owner}/${coordinate.repo}/issues/${number}/labels`,
218
+ { body: { labels } },
219
+ )
220
+ ).data;
221
+ }
222
+ }