@kungfu-tech/buildchain 3.0.6 → 3.0.7-alpha.0
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.
- package/README.md +4 -2
- package/actions/release-tail/README.md +17 -0
- package/bin/buildchain.mjs +11 -0
- package/bin/internal/command-registry.mjs +2 -0
- package/contracts/fixtures/release-tail-capabilities-v1/kungfu-alpha.json +273 -0
- package/contracts/publication-rehearsal-capsule-v1.schema.json +173 -0
- package/contracts/release-tail-capabilities-v1.schema.json +199 -0
- package/contracts/release-tail-provider-bindings-v1.schema.json +56 -0
- package/dist/site/agent-index.json +3 -0
- package/dist/site/artifact-schemas.json +6 -0
- package/dist/site/buildchain-contract.json +35 -20
- package/dist/site/buildchain-site.json +307 -17
- package/dist/site/capability-registry.json +12 -9
- package/dist/site/cli-registry.json +102 -0
- package/dist/site/controller-registry.json +14 -2
- package/dist/site/kfd-claims.json +247 -16
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +48 -3
- package/dist/site/node-api-registry.json +3108 -1409
- package/dist/site/page-registry.json +275 -9
- package/dist/site/public-surface-audit.json +304 -15
- package/dist/site/publication-authority-registry.json +26 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +5 -0
- package/dist/site/schemas/publication-rehearsal-capsule-v1.schema.json +269 -0
- package/dist/site/schemas/release-tail-capabilities-v1.schema.json +353 -0
- package/dist/site/schemas/release-tail-provider-bindings-v1.schema.json +94 -0
- package/dist/site/site-manifest.json +31 -7
- package/dist/site/workflow-registry.json +86 -9
- package/docs/MAP.md +5 -2
- package/docs/cli-reference.md +136 -0
- package/docs/node-api-reference.md +233 -84
- package/docs/publication-rehearsal.md +94 -0
- package/docs/release-tail-contract.md +160 -0
- package/docs/release-tail-provider-plane.md +120 -0
- package/package.json +8 -3
- package/packages/core/buildchain-agent-manuals.js +3 -0
- package/packages/core/buildchain-kfd-claims.js +1 -1
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/index.js +39 -0
- package/packages/core/paper-agent-entry.js +11 -5
- package/packages/core/paper-repository.js +1 -0
- package/packages/core/paper-scaffold-content.js +21 -0
- package/packages/core/paper.js +28 -2
- package/packages/core/publication-rehearsal-projection.js +173 -0
- package/packages/core/publication-rehearsal-runtime.js +909 -0
- package/packages/core/release-tail-compatibility.js +60 -0
- package/packages/core/release-tail-provider-adapters.js +461 -0
- package/packages/core/release-tail-provider-plane.js +1228 -0
- package/scripts/assemble-publication-artifact-admission.mjs +1 -1
- package/scripts/assemble-self-publication-admission.mjs +1 -1
- package/scripts/buildchain-cli-help.mjs +13 -0
- package/scripts/check-core-mechanism-inventory.mjs +347 -0
- package/scripts/check-inventory.mjs +8 -8
- package/scripts/check-maintainability.mjs +9 -2
- package/scripts/check-release-tail-contract.mjs +435 -0
- package/scripts/generate-channel-promotion-workflow.mjs +10 -8
- package/scripts/generate-site-bundle.mjs +24 -0
- package/scripts/init-repo.mjs +26 -2
- package/scripts/materialize-self-release-candidate-version.mjs +131 -0
- package/scripts/release-tail.mjs +159 -0
- package/scripts/site-capability-metadata.mjs +11 -0
- package/scripts/v4-architecture.mjs +600 -0
- package/scripts/workflow-call-contract.mjs +184 -5
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
const INVENTORY_PATH = "architecture/release-tail-contract-inventory.json";
|
|
8
|
+
const ROOT_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
|
9
|
+
const SHA_PATTERN = /^[0-9a-f]{40}$/u;
|
|
10
|
+
|
|
11
|
+
function loadJson(root, file) {
|
|
12
|
+
return JSON.parse(fs.readFileSync(path.join(root, file), "utf8"));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function read(root, file) {
|
|
16
|
+
return fs.readFileSync(path.join(root, file), "utf8");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function commandInputs(text, indent, stopAt = "") {
|
|
20
|
+
const source = stopAt ? text.split(new RegExp(`^${stopAt}`, "mu"))[0] : text;
|
|
21
|
+
const pattern = new RegExp(`^ {${indent}}([a-z0-9-]+-command):\\s*$`, "gmu");
|
|
22
|
+
return [...source.matchAll(pattern)].map((match) => match[1]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function workflowCoordinates(root, inventory) {
|
|
26
|
+
return (inventory.reverseScan.workflowInputs || []).flatMap((file) =>
|
|
27
|
+
commandInputs(read(root, file), 6, "jobs:").map(
|
|
28
|
+
(name) => `workflow:${file}#${name}`,
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function actionCoordinates(root, inventory) {
|
|
34
|
+
return (inventory.reverseScan.actionInputs || []).flatMap((file) =>
|
|
35
|
+
commandInputs(read(root, file).split(/^outputs:/mu)[0], 2).map(
|
|
36
|
+
(name) => `action:${file}#${name}`,
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sortedUnique(values) {
|
|
42
|
+
return [...new Set(values)].sort();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sameSet(left, right) {
|
|
46
|
+
return (
|
|
47
|
+
JSON.stringify(sortedUnique(left)) === JSON.stringify(sortedUnique(right))
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function validateReverseScan(root, inventory, surfaces, issues) {
|
|
52
|
+
const coordinates = [
|
|
53
|
+
...workflowCoordinates(root, inventory),
|
|
54
|
+
...actionCoordinates(root, inventory),
|
|
55
|
+
...(inventory.reverseScan.configAndCliAliases || []),
|
|
56
|
+
];
|
|
57
|
+
const owned = new Map();
|
|
58
|
+
for (const surface of surfaces) {
|
|
59
|
+
for (const coordinate of surface.coordinates || []) {
|
|
60
|
+
if (owned.has(coordinate))
|
|
61
|
+
issues.push(
|
|
62
|
+
`${coordinate}: ambiguous surface ownership (${owned.get(coordinate)}, ${surface.id})`,
|
|
63
|
+
);
|
|
64
|
+
owned.set(coordinate, surface.id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const coordinate of coordinates) {
|
|
68
|
+
if (!owned.has(coordinate))
|
|
69
|
+
issues.push(`unclassified release-tail command surface: ${coordinate}`);
|
|
70
|
+
}
|
|
71
|
+
for (const coordinate of owned.keys()) {
|
|
72
|
+
if (!coordinates.includes(coordinate))
|
|
73
|
+
issues.push(
|
|
74
|
+
`declared release-tail coordinate is not reverse-discovered: ${coordinate}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const workflowNames = coordinates
|
|
79
|
+
.filter((entry) => entry.startsWith("workflow:"))
|
|
80
|
+
.map((entry) => entry.split("#")[1]);
|
|
81
|
+
const actionNames = coordinates
|
|
82
|
+
.filter((entry) => entry.startsWith("action:"))
|
|
83
|
+
.map((entry) => entry.split("#")[1]);
|
|
84
|
+
if (!sameSet(workflowNames, inventory.reverseScan.workflowCommandNames || []))
|
|
85
|
+
issues.push("reverse scan workflow command-name inventory drifted");
|
|
86
|
+
if (!sameSet(actionNames, inventory.reverseScan.actionCommandNames || []))
|
|
87
|
+
issues.push("reverse scan Action command-name inventory drifted");
|
|
88
|
+
|
|
89
|
+
let executionSites = 0;
|
|
90
|
+
for (const surface of surfaces) {
|
|
91
|
+
for (const site of surface.executionSites || []) {
|
|
92
|
+
const file = path.join(root, site.path || "");
|
|
93
|
+
if (!site.path || !site.marker || !fs.existsSync(file)) {
|
|
94
|
+
issues.push(
|
|
95
|
+
`${surface.id}: execution site is incomplete: ${site.path || "<empty>"}`,
|
|
96
|
+
);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!fs.readFileSync(file, "utf8").includes(site.marker))
|
|
100
|
+
issues.push(
|
|
101
|
+
`${surface.id}: execution marker is missing from ${site.path}`,
|
|
102
|
+
);
|
|
103
|
+
executionSites += 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { coordinates: coordinates.length, executionSites };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function validateTransaction(inventory, issues) {
|
|
110
|
+
const transaction = inventory.canonicalTransaction || {};
|
|
111
|
+
const phases = (transaction.phases || []).map((entry) => entry.id);
|
|
112
|
+
if (
|
|
113
|
+
transaction.id !== "buildchain.release-tail/v1" ||
|
|
114
|
+
transaction.singleWriter !== true
|
|
115
|
+
)
|
|
116
|
+
issues.push(
|
|
117
|
+
"canonical release transaction identity or single-writer invariant is invalid",
|
|
118
|
+
);
|
|
119
|
+
if (
|
|
120
|
+
!sameSet(phases, [
|
|
121
|
+
"prepare",
|
|
122
|
+
"publish",
|
|
123
|
+
"commit",
|
|
124
|
+
"activate",
|
|
125
|
+
"readback",
|
|
126
|
+
"settle",
|
|
127
|
+
])
|
|
128
|
+
)
|
|
129
|
+
issues.push("canonical release transaction must own all six frozen phases");
|
|
130
|
+
if (
|
|
131
|
+
transaction.effectSchema !== "kungfu.buildchain.release-tail.effect/v1" ||
|
|
132
|
+
transaction.observationSchema !==
|
|
133
|
+
"kungfu.buildchain.release-tail.observation/v1" ||
|
|
134
|
+
transaction.receiptSchema !== "kungfu.buildchain.release-tail.receipt/v1"
|
|
135
|
+
)
|
|
136
|
+
issues.push("canonical effect, observation, and receipt schemas drifted");
|
|
137
|
+
if ((transaction.retryClasses || []).some((entry) => entry.localAttempts > 3))
|
|
138
|
+
issues.push("canonical transaction permits unbounded local retry");
|
|
139
|
+
for (const terminal of [
|
|
140
|
+
"complete",
|
|
141
|
+
"blocked",
|
|
142
|
+
"repair-required",
|
|
143
|
+
"terminal-failure",
|
|
144
|
+
]) {
|
|
145
|
+
if (!(transaction.terminalClasses || []).includes(terminal))
|
|
146
|
+
issues.push(`canonical transaction omits terminal class ${terminal}`);
|
|
147
|
+
}
|
|
148
|
+
for (const forbidden of [
|
|
149
|
+
"choose transaction transitions",
|
|
150
|
+
"execute repository-supplied shell",
|
|
151
|
+
]) {
|
|
152
|
+
if (!(transaction.adapterBoundary?.mustNot || []).includes(forbidden))
|
|
153
|
+
issues.push(`adapter boundary omits ${forbidden}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function findForbiddenKey(value, forbidden, at = "$") {
|
|
158
|
+
if (Array.isArray(value)) {
|
|
159
|
+
for (const [index, entry] of value.entries()) {
|
|
160
|
+
const found = findForbiddenKey(entry, forbidden, `${at}[${index}]`);
|
|
161
|
+
if (found) return found;
|
|
162
|
+
}
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
if (!value || typeof value !== "object") return "";
|
|
166
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
167
|
+
if (forbidden.has(key.toLowerCase())) return `${at}.${key}`;
|
|
168
|
+
const found = findForbiddenKey(entry, forbidden, `${at}.${key}`);
|
|
169
|
+
if (found) return found;
|
|
170
|
+
}
|
|
171
|
+
return "";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function validateCapabilityMessages(
|
|
175
|
+
inventory,
|
|
176
|
+
capability,
|
|
177
|
+
fixturePath,
|
|
178
|
+
issues,
|
|
179
|
+
) {
|
|
180
|
+
if (
|
|
181
|
+
capability.effect?.schema !==
|
|
182
|
+
inventory.canonicalTransaction?.effectSchema ||
|
|
183
|
+
capability.observation?.schema !==
|
|
184
|
+
inventory.canonicalTransaction?.observationSchema ||
|
|
185
|
+
capability.receipt?.schema !== inventory.canonicalTransaction?.receiptSchema
|
|
186
|
+
)
|
|
187
|
+
issues.push(`${fixturePath}: ${capability.id} message schema drifted`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function validateCapabilityOperation(capability, fixturePath, issues) {
|
|
191
|
+
if (
|
|
192
|
+
!ROOT_PATTERN.test(capability.operationIdentity?.transactionRoot || "") ||
|
|
193
|
+
capability.operationIdentity?.capabilityId !== capability.id ||
|
|
194
|
+
!ROOT_PATTERN.test(capability.operationIdentity?.subjectRoot || "") ||
|
|
195
|
+
!ROOT_PATTERN.test(capability.operationIdentity?.targetRoot || "") ||
|
|
196
|
+
!String(capability.operationIdentity?.attemptKey || "").trim()
|
|
197
|
+
)
|
|
198
|
+
issues.push(
|
|
199
|
+
`${fixturePath}: ${capability.id} operation identity is incomplete or inconsistent`,
|
|
200
|
+
);
|
|
201
|
+
if (
|
|
202
|
+
!Number.isInteger(capability.retry?.localAttempts) ||
|
|
203
|
+
capability.retry.localAttempts < 0 ||
|
|
204
|
+
capability.retry.localAttempts > 3
|
|
205
|
+
)
|
|
206
|
+
issues.push(`${fixturePath}: ${capability.id} local retry is invalid`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function validateCapabilityDimensions(
|
|
210
|
+
inventory,
|
|
211
|
+
capability,
|
|
212
|
+
fixturePath,
|
|
213
|
+
issues,
|
|
214
|
+
) {
|
|
215
|
+
for (const field of inventory.declarativeContract.requiredDimensions || []) {
|
|
216
|
+
if (capability[field] === undefined)
|
|
217
|
+
issues.push(`${fixturePath}: ${capability.id} omits ${field}`);
|
|
218
|
+
}
|
|
219
|
+
if (!(capability.readbackPredicates || []).length)
|
|
220
|
+
issues.push(`${fixturePath}: ${capability.id} has no readback predicate`);
|
|
221
|
+
if (!(capability.evidenceRequirements || []).length)
|
|
222
|
+
issues.push(`${fixturePath}: ${capability.id} has no evidence requirement`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function validateCapabilityFixture(inventory, fixture, fixturePath, issues) {
|
|
226
|
+
const contract = inventory.declarativeContract || {};
|
|
227
|
+
if (
|
|
228
|
+
fixture.contract !== contract.contract ||
|
|
229
|
+
fixture.schemaVersion !== contract.schemaVersion ||
|
|
230
|
+
fixture.transactionPolicy !== inventory.canonicalTransaction?.id
|
|
231
|
+
)
|
|
232
|
+
issues.push(`${fixturePath}: declaration identity is invalid`);
|
|
233
|
+
if (!SHA_PATTERN.test(fixture.subject?.sourceSha || ""))
|
|
234
|
+
issues.push(`${fixturePath}: subject sourceSha is not exact`);
|
|
235
|
+
const forbidden = new Set(contract.forbiddenKeys || []);
|
|
236
|
+
const forbiddenPath = findForbiddenKey(fixture, forbidden);
|
|
237
|
+
if (forbiddenPath)
|
|
238
|
+
issues.push(
|
|
239
|
+
`${fixturePath}: executable key is forbidden at ${forbiddenPath}`,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const capabilities = fixture.capabilities || [];
|
|
243
|
+
const ids = capabilities.map((entry) => entry.id);
|
|
244
|
+
if (!sameSet(ids, contract.requiredCapabilityIds || []))
|
|
245
|
+
issues.push(`${fixturePath}: required capability set drifted`);
|
|
246
|
+
if (ids.length !== new Set(ids).size)
|
|
247
|
+
issues.push(`${fixturePath}: capability ids must be unique`);
|
|
248
|
+
for (const capability of capabilities) {
|
|
249
|
+
validateCapabilityDimensions(inventory, capability, fixturePath, issues);
|
|
250
|
+
validateCapabilityMessages(inventory, capability, fixturePath, issues);
|
|
251
|
+
validateCapabilityOperation(capability, fixturePath, issues);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function validateContract(root, inventory, issues) {
|
|
256
|
+
const contract = inventory.declarativeContract || {};
|
|
257
|
+
if (!fs.existsSync(path.join(root, contract.schemaPath || "")))
|
|
258
|
+
issues.push("declarative release-tail schema path is missing");
|
|
259
|
+
else {
|
|
260
|
+
const schema = loadJson(root, contract.schemaPath);
|
|
261
|
+
if (
|
|
262
|
+
schema.properties?.contract?.const !== contract.contract ||
|
|
263
|
+
schema.properties?.schemaVersion?.const !== contract.schemaVersion
|
|
264
|
+
)
|
|
265
|
+
issues.push("declarative release-tail JSON Schema identity drifted");
|
|
266
|
+
}
|
|
267
|
+
for (const fixturePath of contract.fixturePaths || []) {
|
|
268
|
+
if (!fs.existsSync(path.join(root, fixturePath))) {
|
|
269
|
+
issues.push(`declarative fixture is missing: ${fixturePath}`);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
validateCapabilityFixture(
|
|
273
|
+
inventory,
|
|
274
|
+
loadJson(root, fixturePath),
|
|
275
|
+
fixturePath,
|
|
276
|
+
issues,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function validateSurfaceInventory(inventory, issues) {
|
|
282
|
+
const surfaces = inventory.legacyExecutableSurfaces || [];
|
|
283
|
+
const surfaceIds = new Set();
|
|
284
|
+
for (const surface of surfaces) {
|
|
285
|
+
if (!surface.id || surfaceIds.has(surface.id))
|
|
286
|
+
issues.push(
|
|
287
|
+
`release-tail surface id is missing or duplicated: ${surface.id || "<empty>"}`,
|
|
288
|
+
);
|
|
289
|
+
surfaceIds.add(surface.id);
|
|
290
|
+
for (const field of [
|
|
291
|
+
"owner",
|
|
292
|
+
"classification",
|
|
293
|
+
"default",
|
|
294
|
+
"replacement",
|
|
295
|
+
"disposition",
|
|
296
|
+
]) {
|
|
297
|
+
if (!String(surface[field] || "").trim())
|
|
298
|
+
issues.push(`${surface.id}: ${field} is empty`);
|
|
299
|
+
}
|
|
300
|
+
if (!(surface.publicNames || []).length)
|
|
301
|
+
issues.push(`${surface.id}: publicNames is empty`);
|
|
302
|
+
if (!(surface.coordinates || []).length)
|
|
303
|
+
issues.push(`${surface.id}: coordinates is empty`);
|
|
304
|
+
}
|
|
305
|
+
return { surfaces, surfaceIds };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function validateManagedCallers(inventory, surfaceIds, issues) {
|
|
309
|
+
for (const caller of inventory.managedConsumerCallers || []) {
|
|
310
|
+
if (
|
|
311
|
+
!caller.id ||
|
|
312
|
+
!caller.repository ||
|
|
313
|
+
!caller.workflow ||
|
|
314
|
+
!caller.runtimeRef
|
|
315
|
+
)
|
|
316
|
+
issues.push(
|
|
317
|
+
`managed consumer caller is incomplete: ${caller.id || "<empty>"}`,
|
|
318
|
+
);
|
|
319
|
+
if (
|
|
320
|
+
!SHA_PATTERN.test(caller.sourceCommit || "") ||
|
|
321
|
+
!SHA_PATTERN.test(caller.sourceTree || "")
|
|
322
|
+
)
|
|
323
|
+
issues.push(`${caller.id}: managed consumer cut is not exact`);
|
|
324
|
+
for (const surfaceId of caller.executableSurfaceIds || []) {
|
|
325
|
+
if (!surfaceIds.has(surfaceId))
|
|
326
|
+
issues.push(`${caller.id}: unknown executable surface ${surfaceId}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function validateInventory(inventory, issues) {
|
|
332
|
+
if (
|
|
333
|
+
inventory.schemaVersion !== 1 ||
|
|
334
|
+
inventory.contract !== "kungfu-buildchain-release-tail-contract-inventory"
|
|
335
|
+
)
|
|
336
|
+
issues.push("release-tail inventory identity is invalid");
|
|
337
|
+
if (
|
|
338
|
+
!SHA_PATTERN.test(inventory.baseline?.commit || "") ||
|
|
339
|
+
!SHA_PATTERN.test(inventory.baseline?.tree || "")
|
|
340
|
+
)
|
|
341
|
+
issues.push("release-tail baseline must bind an exact commit and tree");
|
|
342
|
+
const { surfaces, surfaceIds } = validateSurfaceInventory(inventory, issues);
|
|
343
|
+
validateManagedCallers(inventory, surfaceIds, issues);
|
|
344
|
+
return surfaces;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function validateMigration(inventory, issues) {
|
|
348
|
+
const migration = inventory.migration || {};
|
|
349
|
+
const window = migration.compatibilityWindow || {};
|
|
350
|
+
if (
|
|
351
|
+
window.maximumDurationDays !== 90 ||
|
|
352
|
+
window.maximumMinorLines !== 2 ||
|
|
353
|
+
window.permanentEscapeHatch !== false
|
|
354
|
+
)
|
|
355
|
+
issues.push(
|
|
356
|
+
"compatibility window is not bounded by time, minor lines, and no-escape policy",
|
|
357
|
+
);
|
|
358
|
+
if ((migration.publishedReleasePreservation || []).length < 3)
|
|
359
|
+
issues.push("published-release preservation rules are incomplete");
|
|
360
|
+
const codes = new Set(
|
|
361
|
+
(migration.rejectionRules || []).map((entry) => entry.code),
|
|
362
|
+
);
|
|
363
|
+
for (const code of [
|
|
364
|
+
"release-tail-command-forbidden",
|
|
365
|
+
"release-tail-alias-collision",
|
|
366
|
+
"release-tail-operation-id-missing",
|
|
367
|
+
"release-tail-readback-missing",
|
|
368
|
+
"release-tail-retry-unbounded",
|
|
369
|
+
]) {
|
|
370
|
+
if (!codes.has(code)) issues.push(`migration rejection rules omit ${code}`);
|
|
371
|
+
}
|
|
372
|
+
const excepted = new Set(
|
|
373
|
+
(migration.exceptionLedger || []).flatMap(
|
|
374
|
+
(entry) => entry.surfaceIds || [],
|
|
375
|
+
),
|
|
376
|
+
);
|
|
377
|
+
for (const surface of inventory.legacyExecutableSurfaces || []) {
|
|
378
|
+
if (
|
|
379
|
+
surface.classification !== "adjacent-non-tail" &&
|
|
380
|
+
!excepted.has(surface.id)
|
|
381
|
+
)
|
|
382
|
+
issues.push(
|
|
383
|
+
`${surface.id}: compatibility exception has no owner and sunset test`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
for (const exception of migration.exceptionLedger || []) {
|
|
387
|
+
if (!exception.owner || !exception.expires || !exception.removalTest)
|
|
388
|
+
issues.push(`${exception.id}: exception ledger entry is incomplete`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function checkReleaseTailContract({
|
|
393
|
+
root = process.cwd(),
|
|
394
|
+
inventory = loadJson(root, INVENTORY_PATH),
|
|
395
|
+
fixtures,
|
|
396
|
+
} = {}) {
|
|
397
|
+
const issues = [];
|
|
398
|
+
const surfaces = validateInventory(inventory, issues);
|
|
399
|
+
validateTransaction(inventory, issues);
|
|
400
|
+
if (fixtures) {
|
|
401
|
+
for (const [fixturePath, fixture] of Object.entries(fixtures))
|
|
402
|
+
validateCapabilityFixture(inventory, fixture, fixturePath, issues);
|
|
403
|
+
} else {
|
|
404
|
+
validateContract(root, inventory, issues);
|
|
405
|
+
}
|
|
406
|
+
const reverseScan = validateReverseScan(root, inventory, surfaces, issues);
|
|
407
|
+
validateMigration(inventory, issues);
|
|
408
|
+
if (issues.length)
|
|
409
|
+
throw new Error(
|
|
410
|
+
`release-tail contract check failed:\n- ${issues.join("\n- ")}`,
|
|
411
|
+
);
|
|
412
|
+
return {
|
|
413
|
+
surfaces: surfaces.length,
|
|
414
|
+
managedCallers: inventory.managedConsumerCallers.length,
|
|
415
|
+
capabilities: inventory.declarativeContract.requiredCapabilityIds.length,
|
|
416
|
+
...reverseScan,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (
|
|
421
|
+
process.argv[1] &&
|
|
422
|
+
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
|
|
423
|
+
) {
|
|
424
|
+
try {
|
|
425
|
+
const report = checkReleaseTailContract();
|
|
426
|
+
console.log(
|
|
427
|
+
`release-tail contract check passed: ${report.surfaces} classified surfaces, ${report.coordinates} reverse-discovered coordinates, ${report.executionSites} execution sites, ${report.capabilities} declarative capabilities, ${report.managedCallers} managed callers`,
|
|
428
|
+
);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
431
|
+
process.exitCode = 1;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export { checkReleaseTailContract };
|
|
@@ -20,6 +20,7 @@ const internalInputs = new Set([
|
|
|
20
20
|
"promotion-publication-channel",
|
|
21
21
|
"promotion-target-ref",
|
|
22
22
|
"promotion-override-used",
|
|
23
|
+
"publication-authority-workflow-path",
|
|
23
24
|
]);
|
|
24
25
|
|
|
25
26
|
function blockBetween(source, start, end) {
|
|
@@ -116,6 +117,9 @@ function forwardedInputs(inputNames, { includeInternal = true, unsupportedInputs
|
|
|
116
117
|
if (name === "promotion-override-used") {
|
|
117
118
|
return ` ${name}: \${{ needs.resolve-promotion.outputs.override-used == 'true' }}`;
|
|
118
119
|
}
|
|
120
|
+
if (name === "publication-authority-workflow-path") {
|
|
121
|
+
return " publication-authority-workflow-path: .github/workflows/.release-candidate-promote.yml";
|
|
122
|
+
}
|
|
119
123
|
return routed
|
|
120
124
|
? ` ${name}: \${{ needs.resolve-promotion.outputs.${routed} }}`
|
|
121
125
|
: ` ${name}: \${{ inputs.${name} }}`;
|
|
@@ -255,15 +259,13 @@ jobs:
|
|
|
255
259
|
exit 1
|
|
256
260
|
fi
|
|
257
261
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
exit 1
|
|
262
|
+
if [[ "\${ref}" =~ ^[0-9A-Fa-f]{40}$ ]]; then sha="\${ref,,}"; else
|
|
263
|
+
remote_url="https://github.com/\${repository}.git"; refs="$(git ls-remote "\${remote_url}" "refs/heads/\${ref}" "refs/tags/\${ref}" "refs/tags/\${ref}^{}")"
|
|
264
|
+
head_sha="$(printf '%s\\n' "\${refs}" | awk -v name="refs/heads/\${ref}" '$2 == name { print tolower($1) }')"
|
|
265
|
+
tag_sha="$(printf '%s\\n' "\${refs}" | awk -v peeled="refs/tags/\${ref}^{}" -v name="refs/tags/\${ref}" '$2 == peeled { print tolower($1); found=1 } $2 == name && !found { fallback=tolower($1) } END { if (!found && fallback != "") print fallback }')"
|
|
266
|
+
if [[ -n "\${head_sha}" && -n "\${tag_sha}" && "\${head_sha}" != "\${tag_sha}" ]]; then echo "::error::Promotion router ref is ambiguous between branch and tag"; exit 1; fi
|
|
267
|
+
sha="\${head_sha:-\${tag_sha}}"
|
|
265
268
|
fi
|
|
266
|
-
sha="\${head_sha:-\${tag_sha}}"
|
|
267
269
|
if [[ ! "\${sha}" =~ ^[0-9a-f]{40}$ ]]; then
|
|
268
270
|
echo "::error::Promotion router ref did not resolve to one exact commit SHA"
|
|
269
271
|
exit 1
|
|
@@ -360,6 +360,9 @@ const manualMetaById = new Map(Object.entries({
|
|
|
360
360
|
"auditable-demo": { capabilityGroup: "reusable-build", audience: ["consumer", "agent"], maturity: "preview", order: 207 },
|
|
361
361
|
"binary-distribution": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "stable", order: 110 },
|
|
362
362
|
"publish-transaction": { capabilityGroup: "release-passport-trust", audience: ["release-operator"], maturity: "stable", order: 120 },
|
|
363
|
+
"release-tail-contract": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent", "maintainer"], maturity: "draft", order: 122 },
|
|
364
|
+
"release-tail-provider-plane": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent", "maintainer"], maturity: "preview", order: 123 },
|
|
365
|
+
"publication-rehearsal": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent", "maintainer"], maturity: "preview", order: 124 },
|
|
363
366
|
"release-activation-transaction": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "preview", order: 125 },
|
|
364
367
|
"release-candidate": { capabilityGroup: "reusable-build", audience: ["release-operator", "consumer"], maturity: "stable", order: 130 },
|
|
365
368
|
"stable-candidate-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer"], maturity: "preview", order: 135 },
|
|
@@ -514,6 +517,7 @@ function buildCapabilityRegistry({ docs, pages, cliRegistry, manualRegistry, nod
|
|
|
514
517
|
|
|
515
518
|
function workflowCapabilityGroup(entry) {
|
|
516
519
|
if (entry.id === "github-artifact-attestation") return capabilityGroup("release-passport-trust");
|
|
520
|
+
if (entry.id === "release-tail") return capabilityGroup("release-passport-trust");
|
|
517
521
|
if (["web-surface", "release-propagation"].includes(entry.id)) return capabilityGroup("site-and-propagation");
|
|
518
522
|
if (["build", "release-candidate-promote", "publication-artifact", "paper-release"].includes(entry.id)) return capabilityGroup("reusable-build");
|
|
519
523
|
if (["buildchain-ref-promotion", "release-line-bootstrap"].includes(entry.id)) return capabilityGroup("release-passport-trust");
|
|
@@ -524,6 +528,7 @@ function workflowCapabilityGroup(entry) {
|
|
|
524
528
|
|
|
525
529
|
function actionCapabilityGroup(id) {
|
|
526
530
|
if (id === "github-artifact-attestation") return capabilityGroup("release-passport-trust");
|
|
531
|
+
if (id === "release-tail") return capabilityGroup("release-passport-trust");
|
|
527
532
|
if (id === "promote-buildchain-ref") return capabilityGroup("release-passport-trust");
|
|
528
533
|
if (id === "run-lifecycle" || id === "validate-config") return capabilityGroup("reusable-build");
|
|
529
534
|
if (id === "report-buildchain-issue") return capabilityGroup("observability-diagnostics");
|
|
@@ -776,6 +781,7 @@ function buildSiteBundle() {
|
|
|
776
781
|
["release-candidate-promote", "release-governance"],
|
|
777
782
|
["paper-release", "reusable-build"],
|
|
778
783
|
["release-propagation", "release-propagation"],
|
|
784
|
+
["release-tail", "release-tail-provider-plane"],
|
|
779
785
|
["dev-pr-auto-merge", "dev-governance"],
|
|
780
786
|
["buildchain-dev-delivery", "dev-governance"],
|
|
781
787
|
["github-governance-audit", "dev-governance"],
|
|
@@ -901,6 +907,9 @@ function buildSiteBundle() {
|
|
|
901
907
|
"buildchain.release.json",
|
|
902
908
|
"release-passport-check-manifest.json",
|
|
903
909
|
"schemas/release-passport-v1.schema.json",
|
|
910
|
+
"schemas/publication-rehearsal-capsule-v1.schema.json",
|
|
911
|
+
"schemas/release-tail-capabilities-v1.schema.json",
|
|
912
|
+
"schemas/release-tail-provider-bindings-v1.schema.json",
|
|
904
913
|
"schemas/kfd-agent-hub-adoption.schema.json",
|
|
905
914
|
"schemas/kfd-product-gate-input-v1.schema.json",
|
|
906
915
|
"schemas/kfd-support-projection-v1.schema.json",
|
|
@@ -936,6 +945,9 @@ function buildSiteBundle() {
|
|
|
936
945
|
"artifact-schemas.json",
|
|
937
946
|
"release-passport-check-manifest.json",
|
|
938
947
|
"schemas/release-passport-v1.schema.json",
|
|
948
|
+
"schemas/publication-rehearsal-capsule-v1.schema.json",
|
|
949
|
+
"schemas/release-tail-capabilities-v1.schema.json",
|
|
950
|
+
"schemas/release-tail-provider-bindings-v1.schema.json",
|
|
939
951
|
"schemas/kfd-agent-hub-adoption.schema.json",
|
|
940
952
|
"schemas/kfd-product-gate-input-v1.schema.json",
|
|
941
953
|
"schemas/kfd-support-projection-v1.schema.json",
|
|
@@ -1067,6 +1079,9 @@ function buildSiteBundle() {
|
|
|
1067
1079
|
"artifact-schemas.json",
|
|
1068
1080
|
"release-passport-check-manifest.json",
|
|
1069
1081
|
"schemas/release-passport-v1.schema.json",
|
|
1082
|
+
"schemas/publication-rehearsal-capsule-v1.schema.json",
|
|
1083
|
+
"schemas/release-tail-capabilities-v1.schema.json",
|
|
1084
|
+
"schemas/release-tail-provider-bindings-v1.schema.json",
|
|
1070
1085
|
"schemas/kfd-agent-hub-adoption.schema.json",
|
|
1071
1086
|
"schemas/kfd-product-gate-input-v1.schema.json",
|
|
1072
1087
|
"schemas/kfd-support-projection-v1.schema.json",
|
|
@@ -1242,6 +1257,15 @@ function buildSiteBundle() {
|
|
|
1242
1257
|
"artifact-schemas.json": artifactSchemas,
|
|
1243
1258
|
"release-passport-check-manifest.json": createReleasePassportCheckManifest(),
|
|
1244
1259
|
"schemas/release-passport-v1.schema.json": RELEASE_PASSPORT_SCHEMA,
|
|
1260
|
+
"schemas/publication-rehearsal-capsule-v1.schema.json": readJson(
|
|
1261
|
+
"contracts/publication-rehearsal-capsule-v1.schema.json",
|
|
1262
|
+
),
|
|
1263
|
+
"schemas/release-tail-capabilities-v1.schema.json": readJson(
|
|
1264
|
+
"contracts/release-tail-capabilities-v1.schema.json",
|
|
1265
|
+
),
|
|
1266
|
+
"schemas/release-tail-provider-bindings-v1.schema.json": readJson(
|
|
1267
|
+
"contracts/release-tail-provider-bindings-v1.schema.json",
|
|
1268
|
+
),
|
|
1245
1269
|
"schemas/kfd-agent-hub-adoption.schema.json": KFD_AGENT_HUB_ADOPTION_SCHEMA,
|
|
1246
1270
|
"schemas/kfd-product-gate-input-v1.schema.json": KFD_PRODUCT_GATE_INPUT_SCHEMA,
|
|
1247
1271
|
"schemas/kfd-support-projection-v1.schema.json": KFD_SUPPORT_PROJECTION_SCHEMA,
|
package/scripts/init-repo.mjs
CHANGED
|
@@ -4,6 +4,12 @@ import path from "node:path";
|
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { BUILDCHAIN_CONFIG_PATH } from "../packages/core/buildchain-layout.js";
|
|
6
6
|
import { detectPackageManager, assertPackageManager } from "../packages/core/package-manager.js";
|
|
7
|
+
import {
|
|
8
|
+
PUBLICATION_REHEARSAL_WORKFLOW_PATH,
|
|
9
|
+
appendPublicationRehearsalToml,
|
|
10
|
+
mergePublicationRehearsalAgentInstructions,
|
|
11
|
+
publicationRehearsalWorkflow,
|
|
12
|
+
} from "../packages/core/publication-rehearsal-projection.js";
|
|
7
13
|
|
|
8
14
|
const BUILDCHAIN_WORKFLOW_REF = "kungfu-systems/buildchain/.github/workflows/.build.yml@v3";
|
|
9
15
|
const DEFAULT_PUBLICATION_LATEX_IMAGE = "ghcr.io/kungfu-systems/build-images/latex-pdf-builder";
|
|
@@ -401,6 +407,13 @@ function writeIfAllowed(filePath, content, { force }) {
|
|
|
401
407
|
return filePath;
|
|
402
408
|
}
|
|
403
409
|
|
|
410
|
+
function writeManagedAgentEntry(filePath, current) {
|
|
411
|
+
const content = mergePublicationRehearsalAgentInstructions(current);
|
|
412
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
413
|
+
fs.writeFileSync(filePath, content);
|
|
414
|
+
return filePath;
|
|
415
|
+
}
|
|
416
|
+
|
|
404
417
|
export function initBuildchainRepo({
|
|
405
418
|
cwd = process.cwd(),
|
|
406
419
|
type = "package",
|
|
@@ -411,7 +424,7 @@ export function initBuildchainRepo({
|
|
|
411
424
|
} = {}) {
|
|
412
425
|
const resolvedCwd = path.resolve(cwd);
|
|
413
426
|
const manager = detectOrDefaultPackageManager(resolvedCwd, packageManager);
|
|
414
|
-
const toml = (() => {
|
|
427
|
+
const toml = appendPublicationRehearsalToml((() => {
|
|
415
428
|
if (type === "package") {
|
|
416
429
|
return packageToml(resolvedCwd, manager);
|
|
417
430
|
}
|
|
@@ -431,7 +444,12 @@ export function initBuildchainRepo({
|
|
|
431
444
|
return anchoredPackageToml(resolvedCwd, manager);
|
|
432
445
|
}
|
|
433
446
|
throw new Error("init --type must be one of package, native, web-surface, infra-contract, publication-artifact, or anchored-package");
|
|
434
|
-
})();
|
|
447
|
+
})());
|
|
448
|
+
|
|
449
|
+
const agentsPath = path.join(resolvedCwd, "AGENTS.md");
|
|
450
|
+
const currentAgents = fs.existsSync(agentsPath)
|
|
451
|
+
? fs.readFileSync(agentsPath, "utf8")
|
|
452
|
+
: "";
|
|
435
453
|
|
|
436
454
|
const written = [
|
|
437
455
|
writeIfAllowed(path.join(resolvedCwd, BUILDCHAIN_CONFIG_PATH), toml, { force }),
|
|
@@ -446,6 +464,12 @@ export function initBuildchainRepo({
|
|
|
446
464
|
}),
|
|
447
465
|
{ force },
|
|
448
466
|
),
|
|
467
|
+
writeIfAllowed(
|
|
468
|
+
path.join(resolvedCwd, PUBLICATION_REHEARSAL_WORKFLOW_PATH),
|
|
469
|
+
publicationRehearsalWorkflow("v3"),
|
|
470
|
+
{ force },
|
|
471
|
+
),
|
|
472
|
+
writeManagedAgentEntry(agentsPath, currentAgents),
|
|
449
473
|
];
|
|
450
474
|
|
|
451
475
|
if (type === "anchored-package" && !fs.existsSync(path.join(resolvedCwd, "release.json"))) {
|