@kungfu-tech/buildchain 2.10.11 → 2.11.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.
@@ -27,6 +27,7 @@ name = "paper-observer-declared-timelines"
27
27
  [publication]
28
28
  kind = "paper"
29
29
  title = "Observer-Declared Timelines for Real-World Agent Work"
30
+ version = "0.1.0"
30
31
  primary_artifact = "_build/main.pdf"
31
32
  artifact_paths = ["_build/main.pdf"]
32
33
  metadata_paths = ["README.md", "docs/MAP.md"]
@@ -35,6 +36,14 @@ site_consumers = ["papers.libkungfu.dev"]
35
36
  manifest_path = ".buildchain/publication/publication-artifact.json"
36
37
  source_bundle_path = ".buildchain/publication/source.tar.gz"
37
38
 
39
+ [publication.archive]
40
+ id = "observer-declared-timelines"
41
+ canonical_url = "https://papers.libkungfu.dev/observer-declared-timelines/"
42
+ latest_url = "https://papers.libkungfu.dev/observer-declared-timelines/latest/"
43
+ latest_evidence_url = "https://papers.libkungfu.dev/observer-declared-timelines/latest/buildchain.release.json"
44
+ immutable_base_url = "https://papers.libkungfu.dev/archive"
45
+ registry_path = ".buildchain/publication/publication-registry.json"
46
+
38
47
  [publication.toolchain]
39
48
  type = "latex-docker"
40
49
  image = "ghcr.io/kungfu-systems/latex"
@@ -52,6 +61,26 @@ command = "make check"
52
61
  `source_paths` are archived into a source bundle. `metadata_paths` are hashed
53
62
  and recorded so a site can consume the paper facts without scraping prose.
54
63
 
64
+ `publication.archive` turns the publication into an append-only public archive
65
+ contract:
66
+
67
+ - `canonical_url` is the stable human reader page.
68
+ - `latest_url` and `latest_evidence_url` are movable aliases for the latest
69
+ reader page and latest evidence.
70
+ - `immutable_base_url` plus `id` and `publication.version` produce a versioned
71
+ prefix such as
72
+ `https://papers.libkungfu.dev/archive/observer-declared-timelines/v0.1.0/`.
73
+ - `immutable_url_prefix` can be used instead when the repository already owns
74
+ the full version prefix.
75
+ - `registry_path` records every published version and its manifest, passport,
76
+ source bundle, primary artifact, URLs, and SHA-256 digests.
77
+
78
+ Immutable archive prefixes are append-only. Do not run site deployment commands
79
+ with `sync --delete` or equivalent deletion semantics over those prefixes. A
80
+ same-version republish is allowed only when the immutable digest is unchanged;
81
+ if PDF, source bundle, route, metadata, or toolchain evidence changes for an
82
+ existing version, Buildchain fails before the registry is rewritten.
83
+
55
84
  `publication.toolchain` makes the source-to-PDF transformation part of the
56
85
  machine-readable contract. `latex-docker` is the preferred LaTeX profile: the
57
86
  workflow pulls the declared image by digest and runs the declared command in
@@ -88,8 +117,11 @@ The workflow:
88
117
  - creates a source bundle from `publication.source_paths`;
89
118
  - writes `.buildchain/publication/publication-artifact.json`;
90
119
  - writes `.buildchain/publication/publication-artifact-passport.json`;
91
- - uploads one GitHub artifact containing the PDF, manifest, passport, and
92
- source bundle.
120
+ - when `[publication.archive]` is configured, writes
121
+ `.buildchain/publication/publication-registry.json` and verifies same-version
122
+ immutability;
123
+ - uploads one GitHub artifact containing the PDF, manifest, passport, optional
124
+ registry, and source bundle.
93
125
 
94
126
  The wrapper is build-only. It does not publish npm packages, deploy web pages,
95
127
  or create GitHub Releases. Release publication can be layered later by a
@@ -123,12 +155,19 @@ the reusable workflow. The generated manifest records:
123
155
  - publication toolchain type, image, digest, command, invocation mode, and trust
124
156
  classification;
125
157
  - timestamp and reproducibility policy;
126
- - downstream site-consumption hints.
158
+ - downstream site-consumption hints;
159
+ - optional archive routes for canonical, latest, latest evidence, immutable
160
+ version prefix, and public artifact URLs.
127
161
 
128
162
  The companion publication artifact passport records the same source and
129
163
  artifact evidence plus an explicit responsibility split. Buildchain proves
130
164
  declared files and hashes; it does not peer-review paper claims.
131
165
 
166
+ When archive config is present, the registry uses the
167
+ `kungfu-buildchain-publication-artifact-registry` contract. A site repository
168
+ can render latest pages and historical version indexes from that registry
169
+ without rebuilding old PDFs from the latest npm package or paper source.
170
+
132
171
  ## Site Consumption
133
172
 
134
173
  A downstream papers site should treat the publication manifest as the single
@@ -139,7 +178,9 @@ For `paper-observer-declared-timelines`, the expected adoption path is:
139
178
 
140
179
  ```text
141
180
  paper repo builds PDF + manifest + source bundle
181
+ paper repo updates publication-registry.json
142
182
  papers site consumes publication-artifact.json
183
+ papers site consumes publication-registry.json for history
143
184
  site renders paper page and links the PDF/source bundle
144
185
  ```
145
186
 
@@ -85,6 +85,53 @@ The upstream release envelope is the post-finalization fact set:
85
85
  The package version and integrity must be exact. Downstream build logic should
86
86
  install that version directly, not resolve `alpha` or `latest` again.
87
87
 
88
+ Publication repositories can propagate immutable publication archive evidence
89
+ without npm package facts. The upstream envelope then includes
90
+ `publicationArtifact`:
91
+
92
+ ```json
93
+ {
94
+ "repository": "kungfu-systems/paper-observer-declared-timelines",
95
+ "channel": "alpha",
96
+ "tag": "v0.1.0-alpha.1",
97
+ "sourceSha": "4444444444444444444444444444444444444444",
98
+ "releasePassport": {
99
+ "url": "https://github.com/kungfu-systems/paper-observer-declared-timelines/releases/download/v0.1.0-alpha.1/buildchain.release.json",
100
+ "sha256": "5555555555555555555555555555555555555555555555555555555555555555"
101
+ },
102
+ "publicationArtifact": {
103
+ "id": "observer-declared-timelines",
104
+ "kind": "paper",
105
+ "version": "0.1.0-alpha.1",
106
+ "canonicalUrl": "https://papers.libkungfu.dev/observer-declared-timelines/",
107
+ "latestUrl": "https://papers.libkungfu.dev/observer-declared-timelines/latest/",
108
+ "latestEvidenceUrl": "https://papers.libkungfu.dev/observer-declared-timelines/latest/buildchain.release.json",
109
+ "immutableVersionUrl": "https://papers.libkungfu.dev/archive/observer-declared-timelines/v0.1.0-alpha.1/",
110
+ "registry": {
111
+ "url": "https://github.com/kungfu-systems/paper-observer-declared-timelines/releases/download/v0.1.0-alpha.1/publication-registry.json",
112
+ "sha256": "6666666666666666666666666666666666666666666666666666666666666666"
113
+ },
114
+ "manifest": {
115
+ "url": "https://github.com/kungfu-systems/paper-observer-declared-timelines/releases/download/v0.1.0-alpha.1/publication-artifact.json",
116
+ "sha256": "7777777777777777777777777777777777777777777777777777777777777777"
117
+ },
118
+ "passport": {
119
+ "url": "https://github.com/kungfu-systems/paper-observer-declared-timelines/releases/download/v0.1.0-alpha.1/publication-artifact-passport.json",
120
+ "sha256": "8888888888888888888888888888888888888888888888888888888888888888"
121
+ },
122
+ "primaryArtifact": {
123
+ "path": "_build/main.pdf",
124
+ "url": "https://papers.libkungfu.dev/archive/observer-declared-timelines/v0.1.0-alpha.1/main.pdf",
125
+ "sha256": "9999999999999999999999999999999999999999999999999999999999999999"
126
+ }
127
+ }
128
+ }
129
+ ```
130
+
131
+ This lets a site repository render the latest reader page and historical
132
+ version index from release facts while keeping old PDFs, source bundles,
133
+ manifests, and passports immutable.
134
+
88
135
  ## CLI
89
136
 
90
137
  Generate a propagation plan:
@@ -111,7 +158,9 @@ The written lock has contract
111
158
  `kungfu-buildchain-release-propagation-lock` and records:
112
159
 
113
160
  - upstream repository, channel, exact tag, source SHA;
114
- - npm package name, exact version, and sha512 integrity;
161
+ - optional npm package name, exact version, and sha512 integrity;
162
+ - optional publication artifact canonical/latest/immutable URLs, registry,
163
+ manifest, passport, source bundle, and primary artifact digests;
115
164
  - release passport URL and SHA-256;
116
165
  - optional site bundle manifest SHA-256;
117
166
  - downstream repository, channel, base ref, lock path;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.10.11",
3
+ "version": "2.11.0",
4
4
  "private": false,
5
5
  "description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
6
6
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -67,7 +67,9 @@ writeBuildFacts({ fact, output: ".buildchain/facts/native-core.json" });
67
67
  ```
68
68
 
69
69
  Publication repositories can produce site-consumable paper/report facts without
70
- becoming web-surface repositories:
70
+ becoming web-surface repositories. When `[publication.archive]` is configured,
71
+ the same API also maintains the append-only publication registry used by site
72
+ repositories for latest and historical version pages:
71
73
 
72
74
  ```js
73
75
  import { writePublicationArtifact } from "@kungfu-tech/buildchain/publication-artifact";
@@ -397,6 +397,41 @@ function normalizePublicationSection(publication) {
397
397
  sourceBundlePath: publication.source_bundle_path === undefined
398
398
  ? ".buildchain/publication/source.tar.gz"
399
399
  : posixPath(assertString(publication.source_bundle_path, "publication.source_bundle_path")),
400
+ archive: normalizePublicationArchive(publication.archive),
401
+ };
402
+ }
403
+
404
+ function normalizePublicationArchive(archive = undefined) {
405
+ if (archive === undefined) {
406
+ return undefined;
407
+ }
408
+ assertPlainObject(archive, "publication.archive");
409
+ const immutableUrlPrefix = archive.immutable_url_prefix === undefined
410
+ ? ""
411
+ : assertString(archive.immutable_url_prefix, "publication.archive.immutable_url_prefix");
412
+ const immutableBaseUrl = archive.immutable_base_url === undefined
413
+ ? ""
414
+ : assertString(archive.immutable_base_url, "publication.archive.immutable_base_url");
415
+ if (!immutableUrlPrefix && !immutableBaseUrl) {
416
+ throw new Error("publication.archive requires immutable_url_prefix or immutable_base_url");
417
+ }
418
+ return {
419
+ id: archive.id === undefined ? "" : assertString(archive.id, "publication.archive.id"),
420
+ canonicalUrl: assertString(archive.canonical_url, "publication.archive.canonical_url"),
421
+ latestUrl: archive.latest_url === undefined
422
+ ? ""
423
+ : assertString(archive.latest_url, "publication.archive.latest_url"),
424
+ latestEvidenceUrl: archive.latest_evidence_url === undefined
425
+ ? ""
426
+ : assertString(archive.latest_evidence_url, "publication.archive.latest_evidence_url"),
427
+ immutableBaseUrl,
428
+ immutableUrlPrefix,
429
+ artifactUrlPrefix: archive.artifact_url_prefix === undefined
430
+ ? ""
431
+ : assertString(archive.artifact_url_prefix, "publication.archive.artifact_url_prefix"),
432
+ registryPath: archive.registry_path === undefined
433
+ ? ".buildchain/publication/publication-registry.json"
434
+ : posixPath(assertString(archive.registry_path, "publication.archive.registry_path")),
400
435
  };
401
436
  }
402
437
 
@@ -186,8 +186,10 @@ export {
186
186
  } from "./public-surface-audit.js";
187
187
 
188
188
  export {
189
+ PUBLICATION_ARTIFACT_ARCHIVE_CONTRACT,
189
190
  PUBLICATION_ARTIFACT_MANIFEST_CONTRACT,
190
191
  PUBLICATION_ARTIFACT_PASSPORT_CONTRACT,
192
+ PUBLICATION_ARTIFACT_REGISTRY_CONTRACT,
191
193
  collectPublicationArtifact,
192
194
  createPublicationSourceBundle,
193
195
  writePublicationArtifact,
@@ -6,6 +6,8 @@ import { loadBuildchainConfig } from "./buildchain-config.js";
6
6
 
7
7
  export const PUBLICATION_ARTIFACT_MANIFEST_CONTRACT = "kungfu-buildchain-publication-artifact-manifest";
8
8
  export const PUBLICATION_ARTIFACT_PASSPORT_CONTRACT = "kungfu-buildchain-publication-artifact-passport";
9
+ export const PUBLICATION_ARTIFACT_ARCHIVE_CONTRACT = "kungfu-buildchain-publication-artifact-archive";
10
+ export const PUBLICATION_ARTIFACT_REGISTRY_CONTRACT = "kungfu-buildchain-publication-artifact-registry";
9
11
  const KFD2_RESIDUAL_RISK_SCHEMA = "https://kfd.libkungfu.dev/schemas/kfd-2/trust-taxonomy.schema.json#/$defs/residualRisk";
10
12
 
11
13
  function toPosix(value) {
@@ -20,6 +22,28 @@ function sha256FilePath(filePath) {
20
22
  return sha256Buffer(fs.readFileSync(filePath));
21
23
  }
22
24
 
25
+ function sortJson(value) {
26
+ if (Array.isArray(value)) {
27
+ return value.map(sortJson);
28
+ }
29
+ if (!value || typeof value !== "object") {
30
+ return value;
31
+ }
32
+ return Object.fromEntries(
33
+ Object.entries(value)
34
+ .sort(([left], [right]) => left.localeCompare(right))
35
+ .map(([key, entry]) => [key, sortJson(entry)]),
36
+ );
37
+ }
38
+
39
+ function stableJson(value) {
40
+ return `${JSON.stringify(sortJson(value), null, 2)}\n`;
41
+ }
42
+
43
+ function sha256Json(value) {
44
+ return sha256Buffer(Buffer.from(stableJson(value)));
45
+ }
46
+
23
47
  function repoRelative(cwd, filePath) {
24
48
  const relative = path.relative(cwd, filePath);
25
49
  return relative && !relative.startsWith("..") && !path.isAbsolute(relative)
@@ -75,6 +99,29 @@ function createdAt(now = new Date()) {
75
99
  return now.toISOString();
76
100
  }
77
101
 
102
+ function ensureTrailingSlash(value) {
103
+ const text = String(value || "").trim();
104
+ return text.endsWith("/") ? text : `${text}/`;
105
+ }
106
+
107
+ function joinUrl(prefix, ...segments) {
108
+ const base = ensureTrailingSlash(prefix);
109
+ const suffix = segments
110
+ .filter(Boolean)
111
+ .map((segment) => String(segment).replace(/^\/+/, "").replace(/\\/g, "/"))
112
+ .join("/");
113
+ return suffix ? `${base}${suffix}` : base;
114
+ }
115
+
116
+ function versionLabel(version) {
117
+ const text = String(version || "").trim();
118
+ return text.startsWith("v") ? text : `v${text}`;
119
+ }
120
+
121
+ function archiveFileUrl(prefix, relPath) {
122
+ return joinUrl(prefix, path.posix.basename(toPosix(relPath)));
123
+ }
124
+
78
125
  function publicationToolchainFacts(publication) {
79
126
  const envType = String(process.env.BUILDCHAIN_PUBLICATION_TOOLCHAIN_TYPE || "").trim();
80
127
  const envToolchain = envType
@@ -138,6 +185,67 @@ function publicationResidualRisk(toolchainFacts) {
138
185
  return risks;
139
186
  }
140
187
 
188
+ function publicationArchiveFacts({ loaded, publication, manifestPath, passportPath, artifacts, bundle }) {
189
+ if (!publication.archive) {
190
+ return undefined;
191
+ }
192
+ const version = String(publication.version || "").trim();
193
+ if (!version) {
194
+ throw new Error("publication.archive requires publication.version");
195
+ }
196
+ const archive = publication.archive;
197
+ const id = archive.id || loaded.config.project?.name || "publication";
198
+ const immutableVersionPrefix = archive.immutableUrlPrefix
199
+ ? ensureTrailingSlash(archive.immutableUrlPrefix)
200
+ : ensureTrailingSlash(joinUrl(archive.immutableBaseUrl, id, versionLabel(version)));
201
+ const artifactUrlPrefix = archive.artifactUrlPrefix
202
+ ? ensureTrailingSlash(archive.artifactUrlPrefix)
203
+ : immutableVersionPrefix;
204
+ return {
205
+ contract: PUBLICATION_ARTIFACT_ARCHIVE_CONTRACT,
206
+ id,
207
+ version,
208
+ appendOnly: true,
209
+ sameVersionRepublish: "fail-on-digest-change",
210
+ immutablePathPolicy: "do-not-sync-delete-immutable-prefixes",
211
+ routes: {
212
+ canonicalUrl: archive.canonicalUrl,
213
+ latestUrl: archive.latestUrl || archive.canonicalUrl,
214
+ latestEvidenceUrl: archive.latestEvidenceUrl,
215
+ immutableVersionPrefix,
216
+ immutableVersionUrl: immutableVersionPrefix,
217
+ artifactUrlPrefix,
218
+ },
219
+ registry: {
220
+ contract: PUBLICATION_ARTIFACT_REGISTRY_CONTRACT,
221
+ path: archive.registryPath,
222
+ },
223
+ publicArtifacts: {
224
+ manifest: {
225
+ path: toPosix(manifestPath),
226
+ url: archiveFileUrl(artifactUrlPrefix, manifestPath),
227
+ },
228
+ passport: {
229
+ path: toPosix(passportPath),
230
+ url: archiveFileUrl(artifactUrlPrefix, passportPath),
231
+ },
232
+ sourceBundle: bundle
233
+ ? {
234
+ path: bundle.path,
235
+ url: archiveFileUrl(artifactUrlPrefix, bundle.path),
236
+ sha256: bundle.sha256,
237
+ bytes: bundle.bytes,
238
+ }
239
+ : undefined,
240
+ artifacts: artifacts.map((artifact) => ({
241
+ ...artifact,
242
+ role: artifact.path === publication.primaryArtifact ? "primary" : "attachment",
243
+ url: archiveFileUrl(artifactUrlPrefix, artifact.path),
244
+ })),
245
+ },
246
+ };
247
+ }
248
+
141
249
  export function createPublicationSourceBundle({
142
250
  cwd = process.cwd(),
143
251
  sourcePaths = [],
@@ -166,6 +274,8 @@ export function collectPublicationArtifact({
166
274
  sourceBundle = true,
167
275
  sourceBundlePath = "",
168
276
  generatedAt = "",
277
+ manifestPath = "",
278
+ passportPath = "",
169
279
  } = {}) {
170
280
  const loaded = loadBuildchainConfig(cwd);
171
281
  if (!loaded?.config?.publication) {
@@ -195,6 +305,16 @@ export function collectPublicationArtifact({
195
305
  const sourceTreeSha = gitValue(cwd, ["rev-parse", `${resolvedSourceSha}^{tree}`]);
196
306
  const timestamp = generatedAt || createdAt();
197
307
  const toolchain = publicationToolchainFacts(publication);
308
+ const resolvedManifestPath = manifestPath || publication.manifestPath;
309
+ const resolvedPassportPath = passportPath || ".buildchain/publication/publication-artifact-passport.json";
310
+ const archive = publicationArchiveFacts({
311
+ loaded,
312
+ publication,
313
+ manifestPath: resolvedManifestPath,
314
+ passportPath: resolvedPassportPath,
315
+ artifacts,
316
+ bundle,
317
+ });
198
318
  const manifest = {
199
319
  schemaVersion: 1,
200
320
  contract: PUBLICATION_ARTIFACT_MANIFEST_CONTRACT,
@@ -210,6 +330,7 @@ export function collectPublicationArtifact({
210
330
  authors: publication.authors,
211
331
  primaryArtifact: publication.primaryArtifact,
212
332
  siteConsumers: publication.siteConsumers,
333
+ archive,
213
334
  },
214
335
  toolchain,
215
336
  source: {
@@ -241,16 +362,38 @@ export function collectPublicationArtifact({
241
362
  artifactDigestScope: "publication artifact digests cover files and source bundle, not manifest timestamps",
242
363
  },
243
364
  };
365
+ const manifestDigest = `sha256:${sha256Buffer(Buffer.from(JSON.stringify(manifest, null, 2)))}`;
244
366
  return {
245
367
  manifest,
246
368
  passport: {
247
369
  schemaVersion: 1,
248
370
  contract: PUBLICATION_ARTIFACT_PASSPORT_CONTRACT,
249
371
  status: "passed",
250
- manifestDigest: `sha256:${sha256Buffer(Buffer.from(JSON.stringify(manifest, null, 2)))}`,
372
+ manifestDigest,
251
373
  source: manifest.source,
252
374
  toolchain,
253
375
  artifacts: manifest.artifacts,
376
+ publicationArchive: archive
377
+ ? {
378
+ contract: archive.contract,
379
+ id: archive.id,
380
+ version: archive.version,
381
+ routes: archive.routes,
382
+ registry: archive.registry,
383
+ publicArtifacts: {
384
+ manifest: {
385
+ ...archive.publicArtifacts.manifest,
386
+ sha256: manifestDigest.replace(/^sha256:/, ""),
387
+ },
388
+ passport: archive.publicArtifacts.passport,
389
+ sourceBundle: archive.publicArtifacts.sourceBundle,
390
+ artifacts: archive.publicArtifacts.artifacts,
391
+ },
392
+ appendOnly: archive.appendOnly,
393
+ sameVersionRepublish: archive.sameVersionRepublish,
394
+ immutablePathPolicy: archive.immutablePathPolicy,
395
+ }
396
+ : undefined,
254
397
  responsibility: {
255
398
  producer: "publication repository",
256
399
  renderer: "site repository",
@@ -277,31 +420,179 @@ export function collectPublicationArtifact({
277
420
  };
278
421
  }
279
422
 
423
+ function publicationArchiveRecord({ collected, manifestPath, passportPath, registryPath, generatedAt }) {
424
+ const archive = collected.manifest.publication.archive;
425
+ if (!archive) {
426
+ return undefined;
427
+ }
428
+ const manifestDigest = sha256FilePath(manifestPath);
429
+ const passportDigest = sha256FilePath(passportPath);
430
+ const immutableEvidence = {
431
+ version: archive.version,
432
+ source: {
433
+ sha: collected.manifest.source.sha,
434
+ treeSha: collected.manifest.source.treeSha,
435
+ sourceBundle: archive.publicArtifacts.sourceBundle,
436
+ },
437
+ routes: archive.routes,
438
+ artifacts: archive.publicArtifacts.artifacts.map((artifact) => ({
439
+ path: artifact.path,
440
+ role: artifact.role,
441
+ url: artifact.url,
442
+ bytes: artifact.bytes,
443
+ sha256: artifact.sha256,
444
+ })),
445
+ metadata: collected.manifest.metadata,
446
+ toolchain: collected.manifest.toolchain,
447
+ };
448
+ const immutableDigest = sha256Json(immutableEvidence);
449
+ const record = {
450
+ version: archive.version,
451
+ status: "published",
452
+ publishedAt: generatedAt || collected.manifest.publishedAt,
453
+ immutableDigest: `sha256:${immutableDigest}`,
454
+ source: {
455
+ repository: collected.manifest.source.repository,
456
+ sha: collected.manifest.source.sha,
457
+ treeSha: collected.manifest.source.treeSha,
458
+ sourceBundle: archive.publicArtifacts.sourceBundle,
459
+ },
460
+ routes: archive.routes,
461
+ artifacts: archive.publicArtifacts.artifacts,
462
+ metadata: collected.manifest.metadata,
463
+ manifest: {
464
+ ...archive.publicArtifacts.manifest,
465
+ sha256: manifestDigest,
466
+ },
467
+ passport: {
468
+ ...archive.publicArtifacts.passport,
469
+ sha256: passportDigest,
470
+ },
471
+ immutableEvidenceSha256: immutableDigest,
472
+ };
473
+ record.recordSha256 = sha256Json({ ...record, recordSha256: undefined });
474
+ return {
475
+ registryPath: archive.registry.path || registryPath,
476
+ record,
477
+ };
478
+ }
479
+
480
+ function updatePublicationArchiveRegistry({ cwd, collected, manifestPath, passportPath, registryOutput, generatedAt }) {
481
+ const archive = collected.manifest.publication.archive;
482
+ if (!archive) {
483
+ return undefined;
484
+ }
485
+ const registryPath = path.resolve(cwd, registryOutput || archive.registry.path);
486
+ const { record } = publicationArchiveRecord({
487
+ collected,
488
+ manifestPath,
489
+ passportPath,
490
+ registryPath,
491
+ generatedAt,
492
+ });
493
+ let registry = {
494
+ schemaVersion: 1,
495
+ contract: PUBLICATION_ARTIFACT_REGISTRY_CONTRACT,
496
+ publication: {
497
+ id: archive.id,
498
+ kind: collected.manifest.publication.kind,
499
+ title: collected.manifest.publication.title,
500
+ canonicalUrl: archive.routes.canonicalUrl,
501
+ latestUrl: archive.routes.latestUrl,
502
+ latestEvidenceUrl: archive.routes.latestEvidenceUrl,
503
+ siteConsumers: collected.manifest.publication.siteConsumers,
504
+ },
505
+ appendOnly: true,
506
+ sameVersionRepublish: "fail-on-digest-change",
507
+ immutablePathPolicy: archive.immutablePathPolicy,
508
+ versions: [],
509
+ };
510
+ if (fs.existsSync(registryPath)) {
511
+ registry = JSON.parse(fs.readFileSync(registryPath, "utf8"));
512
+ if (registry.contract !== PUBLICATION_ARTIFACT_REGISTRY_CONTRACT) {
513
+ throw new Error(`publication archive registry has unsupported contract: ${registry.contract || "(missing)"}`);
514
+ }
515
+ }
516
+ const versions = Array.isArray(registry.versions) ? registry.versions : [];
517
+ const existingIndex = versions.findIndex((entry) => entry.version === record.version);
518
+ if (existingIndex !== -1) {
519
+ const existing = versions[existingIndex];
520
+ if (existing.immutableDigest !== record.immutableDigest) {
521
+ throw new Error(
522
+ `publication archive version ${record.version} is immutable: existing digest ${existing.immutableDigest}, new digest ${record.immutableDigest}`,
523
+ );
524
+ }
525
+ versions[existingIndex] = {
526
+ ...existing,
527
+ latestObservedAt: generatedAt || collected.manifest.publishedAt,
528
+ };
529
+ } else {
530
+ versions.push(record);
531
+ }
532
+ registry = {
533
+ ...registry,
534
+ publication: {
535
+ ...registry.publication,
536
+ id: archive.id,
537
+ kind: collected.manifest.publication.kind,
538
+ title: collected.manifest.publication.title,
539
+ canonicalUrl: archive.routes.canonicalUrl,
540
+ latestUrl: archive.routes.latestUrl,
541
+ latestEvidenceUrl: archive.routes.latestEvidenceUrl,
542
+ siteConsumers: collected.manifest.publication.siteConsumers,
543
+ },
544
+ versions: versions.sort((left, right) => left.version.localeCompare(right.version)),
545
+ };
546
+ registry.registrySha256 = sha256Json({ ...registry, registrySha256: undefined });
547
+ fs.mkdirSync(path.dirname(registryPath), { recursive: true });
548
+ fs.writeFileSync(registryPath, stableJson(registry));
549
+ return {
550
+ path: toPosix(path.relative(cwd, registryPath)),
551
+ sha256: sha256FilePath(registryPath),
552
+ record,
553
+ registry,
554
+ };
555
+ }
556
+
280
557
  export function writePublicationArtifact({
281
558
  cwd = process.cwd(),
282
559
  output = "",
283
560
  passportOutput = "",
561
+ registryOutput = "",
284
562
  sourceSha = "",
285
563
  sourceBundle = true,
286
564
  sourceBundlePath = "",
287
565
  generatedAt = "",
288
566
  } = {}) {
567
+ const loaded = loadBuildchainConfig(cwd);
568
+ const manifestOutput = output || loaded.config.publication.manifestPath;
569
+ const passportPath = passportOutput || ".buildchain/publication/publication-artifact-passport.json";
289
570
  const collected = collectPublicationArtifact({
290
571
  cwd,
291
572
  sourceSha,
292
573
  sourceBundle,
293
574
  sourceBundlePath,
294
575
  generatedAt,
576
+ manifestPath: manifestOutput,
577
+ passportPath,
295
578
  });
296
- const manifestOutput = output || loadBuildchainConfig(cwd).config.publication.manifestPath;
297
- const passportPath = passportOutput || ".buildchain/publication/publication-artifact-passport.json";
298
579
  fs.mkdirSync(path.dirname(path.resolve(cwd, manifestOutput)), { recursive: true });
299
580
  fs.mkdirSync(path.dirname(path.resolve(cwd, passportPath)), { recursive: true });
300
581
  fs.writeFileSync(path.resolve(cwd, manifestOutput), `${JSON.stringify(collected.manifest, null, 2)}\n`);
301
582
  fs.writeFileSync(path.resolve(cwd, passportPath), `${JSON.stringify(collected.passport, null, 2)}\n`);
583
+ const registry = updatePublicationArchiveRegistry({
584
+ cwd,
585
+ collected,
586
+ manifestPath: path.resolve(cwd, manifestOutput),
587
+ passportPath: path.resolve(cwd, passportPath),
588
+ registryOutput,
589
+ generatedAt,
590
+ });
302
591
  return {
303
592
  ...collected,
304
593
  manifestPath: toPosix(manifestOutput),
305
594
  passportPath: toPosix(passportPath),
595
+ registryPath: registry?.path,
596
+ registry,
306
597
  };
307
598
  }