@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.
Files changed (64) hide show
  1. package/README.md +4 -2
  2. package/actions/release-tail/README.md +17 -0
  3. package/bin/buildchain.mjs +11 -0
  4. package/bin/internal/command-registry.mjs +2 -0
  5. package/contracts/fixtures/release-tail-capabilities-v1/kungfu-alpha.json +273 -0
  6. package/contracts/publication-rehearsal-capsule-v1.schema.json +173 -0
  7. package/contracts/release-tail-capabilities-v1.schema.json +199 -0
  8. package/contracts/release-tail-provider-bindings-v1.schema.json +56 -0
  9. package/dist/site/agent-index.json +3 -0
  10. package/dist/site/artifact-schemas.json +6 -0
  11. package/dist/site/buildchain-contract.json +35 -20
  12. package/dist/site/buildchain-site.json +307 -17
  13. package/dist/site/capability-registry.json +12 -9
  14. package/dist/site/cli-registry.json +102 -0
  15. package/dist/site/controller-registry.json +14 -2
  16. package/dist/site/kfd-claims.json +247 -16
  17. package/dist/site/kfd-upstream-aggregate.json +1 -1
  18. package/dist/site/manual-registry.json +48 -3
  19. package/dist/site/node-api-registry.json +3108 -1409
  20. package/dist/site/page-registry.json +275 -9
  21. package/dist/site/public-surface-audit.json +304 -15
  22. package/dist/site/publication-authority-registry.json +26 -1
  23. package/dist/site/publication-registry.json +4 -4
  24. package/dist/site/release-provenance.json +5 -0
  25. package/dist/site/schemas/publication-rehearsal-capsule-v1.schema.json +269 -0
  26. package/dist/site/schemas/release-tail-capabilities-v1.schema.json +353 -0
  27. package/dist/site/schemas/release-tail-provider-bindings-v1.schema.json +94 -0
  28. package/dist/site/site-manifest.json +31 -7
  29. package/dist/site/workflow-registry.json +86 -9
  30. package/docs/MAP.md +5 -2
  31. package/docs/cli-reference.md +136 -0
  32. package/docs/node-api-reference.md +233 -84
  33. package/docs/publication-rehearsal.md +94 -0
  34. package/docs/release-tail-contract.md +160 -0
  35. package/docs/release-tail-provider-plane.md +120 -0
  36. package/package.json +8 -3
  37. package/packages/core/buildchain-agent-manuals.js +3 -0
  38. package/packages/core/buildchain-kfd-claims.js +1 -1
  39. package/packages/core/buildchain-publication-authority.js +1 -0
  40. package/packages/core/index.js +39 -0
  41. package/packages/core/paper-agent-entry.js +11 -5
  42. package/packages/core/paper-repository.js +1 -0
  43. package/packages/core/paper-scaffold-content.js +21 -0
  44. package/packages/core/paper.js +28 -2
  45. package/packages/core/publication-rehearsal-projection.js +173 -0
  46. package/packages/core/publication-rehearsal-runtime.js +909 -0
  47. package/packages/core/release-tail-compatibility.js +60 -0
  48. package/packages/core/release-tail-provider-adapters.js +461 -0
  49. package/packages/core/release-tail-provider-plane.js +1228 -0
  50. package/scripts/assemble-publication-artifact-admission.mjs +1 -1
  51. package/scripts/assemble-self-publication-admission.mjs +1 -1
  52. package/scripts/buildchain-cli-help.mjs +13 -0
  53. package/scripts/check-core-mechanism-inventory.mjs +347 -0
  54. package/scripts/check-inventory.mjs +8 -8
  55. package/scripts/check-maintainability.mjs +9 -2
  56. package/scripts/check-release-tail-contract.mjs +435 -0
  57. package/scripts/generate-channel-promotion-workflow.mjs +10 -8
  58. package/scripts/generate-site-bundle.mjs +24 -0
  59. package/scripts/init-repo.mjs +26 -2
  60. package/scripts/materialize-self-release-candidate-version.mjs +131 -0
  61. package/scripts/release-tail.mjs +159 -0
  62. package/scripts/site-capability-metadata.mjs +11 -0
  63. package/scripts/v4-architecture.mjs +600 -0
  64. package/scripts/workflow-call-contract.mjs +184 -5
@@ -0,0 +1,600 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync, spawnSync } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+
8
+ const MANIFEST_PATH = "architecture/v4-capability-state-machine-manifest.json";
9
+ const MANIFEST_SCHEMA_PATH =
10
+ "architecture/v4-capability-state-machine-manifest.schema.json";
11
+ const EXCEPTION_PATH = "architecture/v4-exception-ledger.json";
12
+ const EXCEPTION_SCHEMA_PATH = "architecture/v4-exception-ledger.schema.json";
13
+ const BOOTSTRAP_PATH = "architecture/v4-bootstrap-authority.json";
14
+ const BUDGET_DIMENSIONS = Object.freeze([
15
+ "authority",
16
+ "semanticState",
17
+ "boundary",
18
+ "structural",
19
+ "agentCognitive",
20
+ "recoveryFault",
21
+ ]);
22
+ const MIGRATION_PHASES = new Set([
23
+ "legacy-authoritative",
24
+ "shadow",
25
+ "cutover-ready",
26
+ "rust-authoritative",
27
+ "legacy-retired",
28
+ ]);
29
+
30
+ function readJson(root, relativePath) {
31
+ return JSON.parse(fs.readFileSync(path.join(root, relativePath), "utf8"));
32
+ }
33
+
34
+ function requiredString(value, coordinate, issues) {
35
+ if (typeof value !== "string" || value.trim() === "") {
36
+ issues.push(`${coordinate} must be a non-empty string`);
37
+ }
38
+ }
39
+
40
+ function requiredStringArray(value, coordinate, issues) {
41
+ if (!Array.isArray(value) || value.length === 0) {
42
+ issues.push(`${coordinate} must be a non-empty array`);
43
+ return;
44
+ }
45
+ value.forEach((entry, index) =>
46
+ requiredString(entry, `${coordinate}[${index}]`, issues),
47
+ );
48
+ }
49
+
50
+ function uniqueIds(entries, coordinate, issues) {
51
+ const ids = new Set();
52
+ for (const [index, entry] of (entries || []).entries()) {
53
+ requiredString(entry?.id, `${coordinate}[${index}].id`, issues);
54
+ if (ids.has(entry?.id))
55
+ issues.push(`${coordinate} has duplicate id: ${entry.id}`);
56
+ ids.add(entry?.id);
57
+ }
58
+ return ids;
59
+ }
60
+
61
+ function dependencyCycles(layers) {
62
+ const graph = new Map(
63
+ layers.map((layer) => [layer.id, new Set(layer.mayDependOn || [])]),
64
+ );
65
+ const cycles = [];
66
+ const complete = new Set();
67
+ const active = new Set();
68
+ const stack = [];
69
+ const visit = (id) => {
70
+ if (active.has(id)) {
71
+ cycles.push([...stack.slice(stack.indexOf(id)), id]);
72
+ return;
73
+ }
74
+ if (complete.has(id)) return;
75
+ active.add(id);
76
+ stack.push(id);
77
+ for (const target of graph.get(id) || []) visit(target);
78
+ stack.pop();
79
+ active.delete(id);
80
+ complete.add(id);
81
+ };
82
+ for (const id of graph.keys()) visit(id);
83
+ return cycles;
84
+ }
85
+
86
+ function validateSchemaDocuments(manifestSchema, exceptionSchema, issues) {
87
+ for (const [name, schema] of [
88
+ [MANIFEST_SCHEMA_PATH, manifestSchema],
89
+ [EXCEPTION_SCHEMA_PATH, exceptionSchema],
90
+ ]) {
91
+ if (schema?.$schema !== "https://json-schema.org/draft/2020-12/schema") {
92
+ issues.push(`${name} must use JSON Schema draft 2020-12`);
93
+ }
94
+ requiredString(schema?.$id, `${name}.$id`, issues);
95
+ if (schema?.type !== "object" || schema?.additionalProperties !== false) {
96
+ issues.push(`${name} must be a closed object schema`);
97
+ }
98
+ }
99
+ }
100
+
101
+ function validateExceptionLedger(ledger, issues) {
102
+ if (ledger?.schemaVersion !== 1)
103
+ issues.push("exception ledger schemaVersion must be 1");
104
+ if (
105
+ ledger?.contract !== "kungfu-buildchain-v4-architecture-exception-ledger"
106
+ ) {
107
+ issues.push("exception ledger contract is invalid");
108
+ }
109
+ if (!Array.isArray(ledger?.exceptions)) {
110
+ issues.push("exception ledger exceptions must be an array");
111
+ return;
112
+ }
113
+ const ids = new Set();
114
+ for (const [index, entry] of ledger.exceptions.entries()) {
115
+ const coordinate = `exceptions[${index}]`;
116
+ for (const field of [
117
+ "id",
118
+ "status",
119
+ "owner",
120
+ "budgetDimension",
121
+ "scope",
122
+ "rationale",
123
+ "expiresAt",
124
+ "removalCondition",
125
+ ]) {
126
+ requiredString(entry?.[field], `${coordinate}.${field}`, issues);
127
+ }
128
+ if (ids.has(entry?.id))
129
+ issues.push(`duplicate architecture exception: ${entry.id}`);
130
+ ids.add(entry?.id);
131
+ if (!BUDGET_DIMENSIONS.includes(entry?.budgetDimension)) {
132
+ issues.push(`${coordinate}.budgetDimension is invalid`);
133
+ }
134
+ if (!["active", "closed"].includes(entry?.status)) {
135
+ issues.push(`${coordinate}.status is invalid`);
136
+ }
137
+ if (!Array.isArray(entry?.evidence) || entry.evidence.length === 0) {
138
+ issues.push(`${coordinate}.evidence must be non-empty`);
139
+ }
140
+ if (Number.isNaN(Date.parse(entry?.expiresAt || ""))) {
141
+ issues.push(`${coordinate}.expiresAt must be an ISO-8601 timestamp`);
142
+ }
143
+ }
144
+ }
145
+
146
+ function validateBudgets(budgets, issues) {
147
+ for (const dimension of BUDGET_DIMENSIONS) {
148
+ const ceilings = budgets?.[dimension]?.hardCeilings;
149
+ if (!ceilings || typeof ceilings !== "object" || Array.isArray(ceilings)) {
150
+ issues.push(`budgets.${dimension}.hardCeilings must be an object`);
151
+ continue;
152
+ }
153
+ for (const [name, value] of Object.entries(ceilings)) {
154
+ if (!Number.isInteger(value) || value < 0) {
155
+ issues.push(
156
+ `budgets.${dimension}.${name} must be a non-negative integer`,
157
+ );
158
+ }
159
+ }
160
+ }
161
+ const hardZero = [
162
+ ["authority", "undeclaredAuthorities"],
163
+ ["authority", "permanentDualWrites"],
164
+ ["authority", "crossMachineMutableFiles"],
165
+ ["semanticState", "undeclaredStates"],
166
+ ["semanticState", "undeclaredEvents"],
167
+ ["semanticState", "bypassTransitions"],
168
+ ["boundary", "providerSdkImportsInContracts"],
169
+ ["boundary", "providerSdkImportsInRustDomain"],
170
+ ["boundary", "dependencyCycles"],
171
+ ["structural", "unownedSourceFiles"],
172
+ ["structural", "unmappedCapabilities"],
173
+ ["structural", "manualProjectionCopies"],
174
+ ["agentCognitive", "duplicateArchitectureExplanations"],
175
+ ["agentCognitive", "implicitAuthorityDecisions"],
176
+ ["recoveryFault", "stateMachinesWithoutRecovery"],
177
+ ["recoveryFault", "unboundedRetries"],
178
+ ["recoveryFault", "unknownFailureClasses"],
179
+ ];
180
+ for (const [dimension, name] of hardZero) {
181
+ if (budgets?.[dimension]?.hardCeilings?.[name] !== 0) {
182
+ issues.push(`hard-zero budget was widened: ${dimension}.${name}`);
183
+ }
184
+ }
185
+ if (budgets?.authority?.hardCeilings?.writersPerStateMachine !== 1) {
186
+ issues.push("authority.writersPerStateMachine must remain exactly 1");
187
+ }
188
+ }
189
+
190
+ function validateDependencyLayers(layers, issues) {
191
+ if (!Array.isArray(layers) || layers.length === 0) {
192
+ issues.push("dependencyLayers must be non-empty");
193
+ return;
194
+ }
195
+ const layerIds = uniqueIds(layers, "dependencyLayers", issues);
196
+ for (const [index, layer] of layers.entries()) {
197
+ if (!Array.isArray(layer?.mayDependOn))
198
+ issues.push(`dependencyLayers[${index}].mayDependOn must be an array`);
199
+ for (const target of layer?.mayDependOn || []) {
200
+ if (!layerIds.has(target))
201
+ issues.push(`${layer.id} depends on unknown layer: ${target}`);
202
+ if (target === layer.id)
203
+ issues.push(`${layer.id} cannot depend on itself`);
204
+ }
205
+ if (
206
+ ["contracts", "rust-domain"].includes(layer?.id) &&
207
+ layer?.providerSdkImports !== "forbidden"
208
+ )
209
+ issues.push(`${layer.id} must forbid provider SDK imports`);
210
+ }
211
+ for (const cycle of dependencyCycles(layers))
212
+ issues.push(`dependency layer cycle: ${cycle.join(" -> ")}`);
213
+ }
214
+
215
+ function validateCapabilities(capabilities, inventory, issues) {
216
+ if (!Array.isArray(capabilities) || capabilities.length === 0)
217
+ issues.push("capabilities must be non-empty");
218
+ const capabilityIds = uniqueIds(capabilities || [], "capabilities", issues);
219
+ const inventoryById = new Map(
220
+ (inventory?.mechanisms || []).map((entry) => [entry.id, entry]),
221
+ );
222
+ for (const [index, capability] of (capabilities || []).entries()) {
223
+ requiredString(capability?.owner, `capabilities[${index}].owner`, issues);
224
+ requiredStringArray(
225
+ capability?.surfaces,
226
+ `capabilities[${index}].surfaces`,
227
+ issues,
228
+ );
229
+ const source = inventoryById.get(capability?.sourceInventoryId);
230
+ if (!source)
231
+ issues.push(`${capability?.id} has unknown v3 source inventory id`);
232
+ else {
233
+ if (capability.owner !== source.owner)
234
+ issues.push(`${capability.id} owner drifted from the v3 inventory`);
235
+ for (const surface of capability.surfaces || [])
236
+ if (!source.publicSurfaces.includes(surface))
237
+ issues.push(`${capability.id} has undeclared v3 surface: ${surface}`);
238
+ }
239
+ if (!MIGRATION_PHASES.has(capability?.migrationPhase))
240
+ issues.push(`${capability?.id} has invalid migrationPhase`);
241
+ }
242
+ for (const id of inventoryById.keys()) {
243
+ if (!(capabilities || []).some((entry) => entry.sourceInventoryId === id))
244
+ issues.push(`v3 mechanism is absent from v4 capability manifest: ${id}`);
245
+ }
246
+ return capabilityIds;
247
+ }
248
+
249
+ function validateStateMachines(machines, capabilityIds, issues) {
250
+ if (!Array.isArray(machines) || machines.length === 0)
251
+ issues.push("stateMachines must be non-empty");
252
+ uniqueIds(machines || [], "stateMachines", issues);
253
+ for (const [index, machine] of (machines || []).entries()) {
254
+ const coordinate = `stateMachines[${index}]`;
255
+ if (!capabilityIds.has(machine?.capabilityId))
256
+ issues.push(`${machine?.id} names unknown capabilityId`);
257
+ requiredString(machine?.owner, `${coordinate}.owner`, issues);
258
+ requiredString(machine?.store, `${coordinate}.store`, issues);
259
+ for (const field of [
260
+ "schemas",
261
+ "states",
262
+ "events",
263
+ "invariants",
264
+ "effects",
265
+ "adapters",
266
+ "tests",
267
+ "recovery",
268
+ ])
269
+ requiredStringArray(machine?.[field], `${coordinate}.${field}`, issues);
270
+ if (
271
+ machine?.writer?.authoritative !== true ||
272
+ machine?.writer?.secondWriterBudget !== 0
273
+ )
274
+ issues.push(
275
+ `${machine?.id} must retain one authoritative writer and zero second-writer budget`,
276
+ );
277
+ requiredString(
278
+ machine?.writer?.runtime,
279
+ `${coordinate}.writer.runtime`,
280
+ issues,
281
+ );
282
+ if (!MIGRATION_PHASES.has(machine?.migrationPhase))
283
+ issues.push(`${machine?.id} has invalid migrationPhase`);
284
+ for (const field of [
285
+ "undeclaredStates",
286
+ "undeclaredEvents",
287
+ "unboundedRetries",
288
+ ])
289
+ if (machine?.budgets?.[field] !== 0)
290
+ issues.push(`${machine?.id}.${field} must remain zero`);
291
+ }
292
+ }
293
+
294
+ function validateManifest({ manifest, inventory, bootstrap, ledger }) {
295
+ const issues = [];
296
+ if (manifest?.schemaVersion !== 1)
297
+ issues.push("manifest schemaVersion must be 1");
298
+ if (
299
+ manifest?.contract !==
300
+ "kungfu-buildchain-v4-capability-state-machine-manifest"
301
+ )
302
+ issues.push("manifest contract is invalid");
303
+ if (manifest?.releaseLine?.branch !== "dev/v4/v4.0")
304
+ issues.push("releaseLine.branch must be dev/v4/v4.0");
305
+ if (manifest?.releaseLine?.productionFallback !== "dev/v3/v3.0")
306
+ issues.push("releaseLine.productionFallback must remain dev/v3/v3.0");
307
+ if (
308
+ manifest?.releaseLine?.qualificationMode !==
309
+ "exact-n-minus-one-git-revision"
310
+ )
311
+ issues.push(
312
+ "releaseLine.qualificationMode must be exact-n-minus-one-git-revision",
313
+ );
314
+ if (manifest?.exceptionLedger !== EXCEPTION_PATH)
315
+ issues.push(`manifest exceptionLedger must be ${EXCEPTION_PATH}`);
316
+ if (bootstrap?.contract !== "kungfu-buildchain-v4-bootstrap-authority")
317
+ issues.push("bootstrap authority contract is invalid");
318
+ if (bootstrap?.releaseLine?.candidateBranch !== manifest?.releaseLine?.branch)
319
+ issues.push(
320
+ "bootstrap candidate branch does not match manifest release line",
321
+ );
322
+ validateBudgets(manifest?.budgets, issues);
323
+ validateExceptionLedger(ledger, issues);
324
+ validateDependencyLayers(manifest?.dependencyLayers, issues);
325
+ const capabilityIds = validateCapabilities(
326
+ manifest?.capabilities,
327
+ inventory,
328
+ issues,
329
+ );
330
+ validateStateMachines(manifest?.stateMachines, capabilityIds, issues);
331
+ if (issues.length > 0)
332
+ throw new Error(
333
+ `v4 architecture validation failed:\n- ${issues.join("\n- ")}`,
334
+ );
335
+ return {
336
+ schemaVersion: 1,
337
+ contract: "kungfu-buildchain-v4-architecture-validation",
338
+ ok: true,
339
+ releaseLine: manifest.releaseLine.branch,
340
+ capabilities: manifest.capabilities.length,
341
+ stateMachines: manifest.stateMachines.length,
342
+ dependencyLayers: manifest.dependencyLayers.length,
343
+ dependencyCycles: 0,
344
+ activeExceptions: ledger.exceptions.filter(
345
+ (entry) => entry.status === "active",
346
+ ).length,
347
+ productionWriterMigrations: manifest.stateMachines.filter(
348
+ (entry) => entry.migrationPhase !== "legacy-authoritative",
349
+ ).length,
350
+ };
351
+ }
352
+
353
+ function loadArchitecture(root = process.cwd()) {
354
+ const manifestSchema = readJson(root, MANIFEST_SCHEMA_PATH);
355
+ const exceptionSchema = readJson(root, EXCEPTION_SCHEMA_PATH);
356
+ const manifest = readJson(root, MANIFEST_PATH);
357
+ const ledger = readJson(root, EXCEPTION_PATH);
358
+ const bootstrap = readJson(root, BOOTSTRAP_PATH);
359
+ const inventory = readJson(
360
+ root,
361
+ "architecture/v3-core-mechanism-inventory.json",
362
+ );
363
+ const schemaIssues = [];
364
+ validateSchemaDocuments(manifestSchema, exceptionSchema, schemaIssues);
365
+ if (schemaIssues.length > 0) {
366
+ throw new Error(
367
+ `v4 architecture schema validation failed:\n- ${schemaIssues.join("\n- ")}`,
368
+ );
369
+ }
370
+ const report = validateManifest({ manifest, inventory, bootstrap, ledger });
371
+ return { manifest, ledger, bootstrap, report };
372
+ }
373
+
374
+ function architectureList(root = process.cwd()) {
375
+ const { manifest, report } = loadArchitecture(root);
376
+ return {
377
+ schemaVersion: 1,
378
+ contract: "kungfu-buildchain-v4-architecture-list",
379
+ source: MANIFEST_PATH,
380
+ releaseLine: report.releaseLine,
381
+ capabilities: manifest.capabilities.map((entry) => ({
382
+ id: entry.id,
383
+ owner: entry.owner,
384
+ migrationPhase: entry.migrationPhase,
385
+ stateMachine: manifest.stateMachines.some(
386
+ (machine) => machine.capabilityId === entry.id,
387
+ ),
388
+ })),
389
+ };
390
+ }
391
+
392
+ function architectureShow(id, root = process.cwd()) {
393
+ if (typeof id !== "string" || id.trim() === "") {
394
+ throw new Error("architecture id must be a non-empty string");
395
+ }
396
+ const { manifest } = loadArchitecture(root);
397
+ const capability = manifest.capabilities.find((entry) => entry.id === id);
398
+ if (!capability) throw new Error(`unknown architecture capability: ${id}`);
399
+ return {
400
+ schemaVersion: 1,
401
+ contract: "kungfu-buildchain-v4-architecture-show",
402
+ source: MANIFEST_PATH,
403
+ capability,
404
+ stateMachines: manifest.stateMachines.filter(
405
+ (entry) => entry.capabilityId === id,
406
+ ),
407
+ budgets: manifest.budgets,
408
+ };
409
+ }
410
+
411
+ function gitJson(root, revision, relativePath) {
412
+ return JSON.parse(
413
+ execFileSync("git", ["show", `${revision}:${relativePath}`], {
414
+ cwd: root,
415
+ encoding: "utf8",
416
+ }),
417
+ );
418
+ }
419
+
420
+ function exactRevision(root, revision) {
421
+ return execFileSync(
422
+ "git",
423
+ ["rev-parse", "--verify", `${revision}^{commit}`],
424
+ {
425
+ cwd: root,
426
+ encoding: "utf8",
427
+ },
428
+ ).trim();
429
+ }
430
+
431
+ function compareNMinusOne({
432
+ root = process.cwd(),
433
+ authorityRevision,
434
+ candidateRevision = "HEAD",
435
+ }) {
436
+ if (
437
+ typeof authorityRevision !== "string" ||
438
+ authorityRevision.trim() === ""
439
+ ) {
440
+ throw new Error("authorityRevision must be a non-empty string");
441
+ }
442
+ const authority = exactRevision(root, authorityRevision);
443
+ const candidate = exactRevision(root, candidateRevision);
444
+ if (authority === candidate)
445
+ throw new Error("N-1 qualification rejects candidate self-qualification");
446
+ const ancestry = spawnSync(
447
+ "git",
448
+ ["merge-base", "--is-ancestor", authority, candidate],
449
+ { cwd: root },
450
+ );
451
+ if (ancestry.status !== 0)
452
+ throw new Error(
453
+ "candidate revision must descend from the N-1 authority revision",
454
+ );
455
+ const authorityManifest = gitJson(root, authority, MANIFEST_PATH);
456
+ const candidateManifest = gitJson(root, candidate, MANIFEST_PATH);
457
+ const authorityLedger = gitJson(root, authority, EXCEPTION_PATH);
458
+ const candidateLedger = gitJson(root, candidate, EXCEPTION_PATH);
459
+ for (const field of [
460
+ "releaseLine",
461
+ "dependencyLayers",
462
+ "capabilities",
463
+ "stateMachines",
464
+ ]) {
465
+ if (
466
+ JSON.stringify(candidateManifest[field]) !==
467
+ JSON.stringify(authorityManifest[field])
468
+ )
469
+ throw new Error(
470
+ `candidate drifted from N-1 manifest authority: ${field}`,
471
+ );
472
+ }
473
+ const deltas = [];
474
+ for (const dimension of BUDGET_DIMENSIONS) {
475
+ const prior = authorityManifest.budgets?.[dimension]?.hardCeilings || {};
476
+ const next = candidateManifest.budgets?.[dimension]?.hardCeilings || {};
477
+ for (const [name, candidateValue] of Object.entries(next)) {
478
+ const authorityValue = prior[name];
479
+ if (!Number.isInteger(authorityValue)) {
480
+ throw new Error(
481
+ `candidate introduced an unqualified budget: ${dimension}.${name}`,
482
+ );
483
+ }
484
+ const direction =
485
+ candidateValue === authorityValue
486
+ ? "unchanged"
487
+ : candidateValue < authorityValue
488
+ ? "tightened"
489
+ : "widened";
490
+ deltas.push({
491
+ dimension,
492
+ name,
493
+ authority: authorityValue,
494
+ candidate: candidateValue,
495
+ direction,
496
+ });
497
+ if (candidateValue > authorityValue) {
498
+ throw new Error(
499
+ `candidate widened N-1 ceiling: ${dimension}.${name} (${authorityValue} -> ${candidateValue})`,
500
+ );
501
+ }
502
+ }
503
+ for (const name of Object.keys(prior)) {
504
+ if (!Object.hasOwn(next, name))
505
+ throw new Error(`candidate removed N-1 budget: ${dimension}.${name}`);
506
+ }
507
+ }
508
+ const authorityExceptions = new Map(
509
+ authorityLedger.exceptions.map((entry) => [entry.id, entry]),
510
+ );
511
+ for (const entry of candidateLedger.exceptions.filter(
512
+ (item) => item.status === "active",
513
+ )) {
514
+ const prior = authorityExceptions.get(entry.id);
515
+ if (!prior || JSON.stringify(prior) !== JSON.stringify(entry)) {
516
+ throw new Error(
517
+ `candidate created or broadened an N-1 qualification exception: ${entry.id}`,
518
+ );
519
+ }
520
+ }
521
+ return {
522
+ schemaVersion: 1,
523
+ contract: "kungfu-buildchain-v4-n-minus-one-qualification",
524
+ ok: true,
525
+ authorityRevision: authority,
526
+ candidateRevision: candidate,
527
+ authoritySource: `git show ${authority}:${MANIFEST_PATH}`,
528
+ candidateSelfQualified: false,
529
+ frozenManifestFields: 4,
530
+ budgetDeltas: deltas,
531
+ activeExceptions: candidateLedger.exceptions.filter(
532
+ (entry) => entry.status === "active",
533
+ ).length,
534
+ };
535
+ }
536
+
537
+ function flag(args, name, fallback = "") {
538
+ const index = args.indexOf(`--${name}`);
539
+ return index === -1 ? fallback : args[index + 1] || fallback;
540
+ }
541
+
542
+ function print(value, json = false) {
543
+ process.stdout.write(`${JSON.stringify(value, null, json ? 2 : 0)}\n`);
544
+ }
545
+
546
+ function runArchitectureCli(args = []) {
547
+ const [command = "validate", ...rest] = args;
548
+ const root = path.resolve(flag(rest, "cwd", process.cwd()));
549
+ const json = rest.includes("--json");
550
+ if (command === "validate") return print(loadArchitecture(root).report, json);
551
+ if (command === "list") return print(architectureList(root), json);
552
+ if (command === "show") {
553
+ const id = rest.find(
554
+ (entry) => !entry.startsWith("--") && entry !== flag(rest, "cwd"),
555
+ );
556
+ if (!id)
557
+ throw new Error(
558
+ "usage: buildchain architecture show <capability-id> [--json]",
559
+ );
560
+ return print(architectureShow(id, root), json);
561
+ }
562
+ if (command === "qualify") {
563
+ const authorityRevision = flag(rest, "authority-revision");
564
+ if (!authorityRevision)
565
+ throw new Error(
566
+ "architecture qualify requires --authority-revision <git-revision>",
567
+ );
568
+ return print(
569
+ compareNMinusOne({
570
+ root,
571
+ authorityRevision,
572
+ candidateRevision: flag(rest, "candidate-revision", "HEAD"),
573
+ }),
574
+ json,
575
+ );
576
+ }
577
+ throw new Error(`unsupported architecture command: ${command}`);
578
+ }
579
+
580
+ if (
581
+ process.argv[1] &&
582
+ import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
583
+ ) {
584
+ try {
585
+ runArchitectureCli(process.argv.slice(2));
586
+ } catch (error) {
587
+ console.error(
588
+ `buildchain architecture: ${error instanceof Error ? error.message : String(error)}`,
589
+ );
590
+ process.exitCode = 1;
591
+ }
592
+ }
593
+
594
+ export {
595
+ architectureList,
596
+ architectureShow,
597
+ compareNMinusOne,
598
+ loadArchitecture,
599
+ validateManifest,
600
+ };