@kungfu-tech/buildchain 2.8.8-alpha.5 → 2.8.8-alpha.7

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.
@@ -19,6 +19,7 @@
19
19
  "./release-candidate": "./packages/core/release-candidate.js",
20
20
  "./release-passport": "./packages/core/release-passport.js",
21
21
  "./release-propagation": "./packages/core/release-propagation.js",
22
+ "./surface-manifest": "./packages/core/surface-manifest.js",
22
23
  "./buildchain-kfd-claims": "./packages/core/buildchain-kfd-claims.js",
23
24
  "./site/buildchain-site.json": "./dist/site/buildchain-site.json",
24
25
  "./site/site-manifest.json": "./dist/site/site-manifest.json",
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-site-manifest",
4
+ "generatedAt": "2026-07-07T05:58:59.518Z",
5
+ "publishedAt": "2026-07-07T05:58:59.518Z",
6
+ "reproducible": true,
7
+ "timestampPolicy": "ci-injected",
8
+ "deterministicInputs": [
9
+ "README.md",
10
+ "docs/*.md",
11
+ "actions/*/README.md",
12
+ "fixtures/*/README.md",
13
+ "packages/core/README.md",
14
+ "package.json#exports",
15
+ "tests/buildchain-inventory.json",
16
+ "sourceRevision",
17
+ "sourceDateEpoch",
18
+ "package content",
19
+ "declared Buildchain surface manifest contract"
20
+ ],
21
+ "sourceDateEpoch": "0",
22
+ "sourceRevision": "5fdcc9bfde15861e9fd12a29f880bcd3ffc8d719",
23
+ "timestampPolicyDetails": {
24
+ "contract": "kungfu-buildchain-surface-timestamp-policy",
25
+ "timestampFields": [
26
+ "generatedAt",
27
+ "publishedAt"
28
+ ],
29
+ "timestampFieldsParticipateInArtifactDigest": true,
30
+ "artifactDigestScope": "npm package dist/site JSON files",
31
+ "note": "Human-readable timestamps are separate from reproducibility inputs; do not infer reproducibility from epoch timestamps."
32
+ },
4
33
  "product": {
5
34
  "name": "Buildchain",
6
35
  "formalName": "Buildchain by Kungfu",
@@ -8,6 +37,7 @@
8
37
  },
9
38
  "package": {
10
39
  "name": "@kungfu-tech/buildchain",
40
+ "version": "2.8.8-alpha.7",
11
41
  "versionSource": "package.json#version"
12
42
  },
13
43
  "entrypoint": "buildchain-site.json",
@@ -39,7 +69,7 @@
39
69
  "path": "docs/release-passport.md",
40
70
  "plane": "verify",
41
71
  "exists": true,
42
- "digest": "sha256:4d69798646adb84c6d193b1f32e7dc50e389db8932e63e9cb71489b6015e0333"
72
+ "digest": "sha256:56c9000305e9dc23993e6c401568c4063dd496c3586382a44cd44db108a95094"
43
73
  },
44
74
  {
45
75
  "id": "release-propagation",
@@ -87,7 +117,7 @@
87
117
  "path": "docs/site-bundle-contract.md",
88
118
  "plane": "use",
89
119
  "exists": true,
90
- "digest": "sha256:1977091fc18b816208e7af50a177fd15e114498bfa49041573812405b46585aa"
120
+ "digest": "sha256:466318ea255da5a3c70a260773384341afb91b929135f2253ecd8f4e6dae7f7b"
91
121
  },
92
122
  {
93
123
  "id": "migration-inventory",
@@ -127,7 +157,7 @@
127
157
  "path": "docs/lifecycle-protocol.md",
128
158
  "plane": "use",
129
159
  "exists": true,
130
- "digest": "sha256:ad88ce9f86ccbb7db0225e1baf902f34c7b734b28361d9e616ec3bafb37d906d"
160
+ "digest": "sha256:43dcaaa53efa8b7e6cdcdaa92a0d39d3ee263b840ed2bfea0a47fe4af922cd59"
131
161
  },
132
162
  {
133
163
  "id": "reusable-build-surface",
@@ -135,7 +165,7 @@
135
165
  "path": "docs/reusable-build-surface.md",
136
166
  "plane": "use",
137
167
  "exists": true,
138
- "digest": "sha256:aae753c7fcf4bcb13c08af7b31cc83d898d36e41248b5f710ecee61035de3e8e"
168
+ "digest": "sha256:165011e2a932461805925c714c2c1c07f1ee60c4b3a614b6dcdda4b81ec22d68"
139
169
  },
140
170
  {
141
171
  "id": "publish-transaction",
@@ -143,7 +173,7 @@
143
173
  "path": "docs/publish-transaction.md",
144
174
  "plane": "verify",
145
175
  "exists": true,
146
- "digest": "sha256:ba45f87893b15c3582a52b3cbecba418f35bf409bc672a0d89842f048788e7f7"
176
+ "digest": "sha256:94844f7bb38dbd85c02f1bc9d55e34dcebd9fc96585789766e8075ac635bc6c0"
147
177
  },
148
178
  {
149
179
  "id": "release-governance",
@@ -151,7 +181,7 @@
151
181
  "path": "docs/release-governance.md",
152
182
  "plane": "why",
153
183
  "exists": true,
154
- "digest": "sha256:85f25aadbadcd8579b3e29204d5ce70dd8831ac3960dfc3d17a547537eb267bd"
184
+ "digest": "sha256:7fb654a147104125eccbbbcccdeb28c5cad5a6097731e560e72919b9231f985f"
155
185
  },
156
186
  {
157
187
  "id": "release-flow",
@@ -183,7 +213,7 @@
183
213
  "path": "docs/web-surface-deployments.md",
184
214
  "plane": "use",
185
215
  "exists": true,
186
- "digest": "sha256:61891635707e9ed27b8754b83713db61cf9a7a6bdf65beb6a9ac8aa39356e168"
216
+ "digest": "sha256:5b374e65dc2740748205db597a7cd9779d71daa1b30ad46e542f95b093e56c98"
187
217
  }
188
218
  ],
189
219
  "facts": [
@@ -250,10 +250,12 @@ checks, and admin enforcement for human channel changes, but adds the
250
250
  authenticated promotion token user or app to the managed bypass allowlist for
251
251
  generated release bookkeeping. Buildchain also creates the configured required
252
252
  check on the exact generated version-state commit before patching the protected
253
- ref, so the automated bookkeeping remains compatible with strict required
254
- checks. If the direct generated update is rejected, promotion fails with a
255
- token/protection diagnostic instead of asking humans to review a post-publish
256
- version-state PR.
253
+ ref, then applies the protected ref update with the declared generated ref
254
+ update token. The reusable wrapper uses
255
+ `secrets.BUILDCHAIN_PROMOTION_TOKEN || github.token` for that protected
256
+ bookkeeping update. If the direct generated update is rejected, promotion fails
257
+ with a token/protection diagnostic instead of asking humans to review a
258
+ post-publish version-state PR.
257
259
 
258
260
  For `version.strategy = "anchored"` with `version.next = "manual"`, release
259
261
  promotion does not generate a Buildchain-owned version-state commit. In that
@@ -293,7 +293,10 @@ Buildchain-managed promotions must finish generated version-state bookkeeping
293
293
  with the promotion token directly; they must not require a post-publish human
294
294
  PR. Before patching a protected generated bookkeeping ref, Buildchain emits the
295
295
  configured required check on the exact generated version-state commit so strict
296
- branch protection can validate the automation path without a second build.
296
+ branch protection can validate the automation path without a second build, then
297
+ uses the generated ref update token for the protected ref PATCH. The reusable
298
+ wrapper defaults that token to `secrets.BUILDCHAIN_PROMOTION_TOKEN ||
299
+ github.token`.
297
300
 
298
301
  If finalization fails after an exact Git tag, a channel branch, or dev/alpha
299
302
  sync ref has already moved, the next run reads the durable `finalizing` state
@@ -416,8 +416,11 @@ token's authenticated user or app to the managed bypass allowlist. Before
416
416
  patching a protected generated bookkeeping ref, the action creates the
417
417
  configured required check on the exact generated version-state commit, so strict
418
418
  status checks are satisfied by machine-verifiable Buildchain evidence rather
419
- than a human PR. If direct generated bookkeeping is still rejected, Buildchain
420
- fails with a token/protection diagnostic instead of creating a post-publish PR.
419
+ than a human PR. The protected ref PATCH itself uses the generated ref update
420
+ token; the reusable wrapper defaults it to
421
+ `secrets.BUILDCHAIN_PROMOTION_TOKEN || github.token`. If direct generated
422
+ bookkeeping is still rejected, Buildchain fails with a token/protection
423
+ diagnostic instead of creating a post-publish PR.
421
424
  Buildchain's own promotion workflow reads `BUILDCHAIN_PROMOTION_BYPASS_APPS`,
422
425
  `BUILDCHAIN_PROMOTION_BYPASS_USERS`, and
423
426
  `BUILDCHAIN_PROMOTION_BYPASS_TEAMS` repository variables so the declared bypass
@@ -47,6 +47,10 @@ Additive passport sections:
47
47
  trusted publishing was enabled.
48
48
  - `transaction`: durable Buildchain release transaction id, state, exact tag,
49
49
  release SHA, state ref, and state SHA.
50
+ - `surfaceTimestampPolicy`: the common Buildchain surface manifest timestamp
51
+ policy. It records real CI/release generation and publication times,
52
+ reproducibility inputs, source revision or source-date-epoch, and whether
53
+ timestamp fields participate in the release artifact digest.
50
54
  - `artifacts`: release assets and registry artifacts in one list, each pointing
51
55
  back to the evidence file that proves its digest.
52
56
 
@@ -515,9 +515,13 @@ so the workflow automation can apply generated version-state and channel
515
515
  bookkeeping on protected `dev`/`alpha`/`release` branches after the reviewed
516
516
  channel PR has merged; consumers using a different promotion identity can pass
517
517
  `branch-protection-bypass-users`, `branch-protection-bypass-teams`, or a
518
- different app slug declaratively. It does not call `.build.yml`, does not create
519
- a matrix, and must fail before publish if the RC evidence, payload set, or
520
- source-lock ref is missing or ambiguous.
518
+ different app slug declaratively. The wrapper uses
519
+ `secrets.BUILDCHAIN_PROMOTION_TOKEN || github.token` as the generated ref update
520
+ token for protected bookkeeping PATCH calls, so a bypass-capable promotion token
521
+ can sync dev immediately after alpha/release publish without a post-publish PR.
522
+ It does not call `.build.yml`, does not create a matrix, and must fail before
523
+ publish if the RC evidence, payload set, or source-lock ref is missing or
524
+ ambiguous.
521
525
 
522
526
  ```yaml
523
527
  jobs:
@@ -52,6 +52,34 @@ from `packages/core/buildchain-kfd-claims.js` and enumerates the public release
52
52
  claims plus the KFD-3 collaboration surfaces that Buildchain self-verifies
53
53
  during release promotion.
54
54
 
55
+ ## Timestamp and Reproducibility Policy
56
+
57
+ Every Buildchain-owned surface manifest uses the same timestamp policy fields:
58
+
59
+ - `generatedAt`: when the manifest JSON was generated.
60
+ - `publishedAt`: when the surface was published, when known.
61
+ - `reproducible`: whether the manifest declares its reproducibility inputs.
62
+ - `timestampPolicy`: `ci-injected` for release/workflow-generated public
63
+ artifacts, or `source-date-epoch` for local deterministic source checks.
64
+ - `deterministicInputs`: the source files, revisions, package metadata, and
65
+ declared Buildchain contracts that determine the manifest bytes.
66
+ - `sourceDateEpoch` / `sourceRevision`: the deterministic time input or source
67
+ revision used to reproduce the manifest.
68
+ - `timestampPolicyDetails.timestampFieldsParticipateInArtifactDigest`: whether
69
+ timestamp fields are included in the artifact digest being audited.
70
+
71
+ The policy is defined by `@kungfu-tech/buildchain/surface-manifest` and applies
72
+ to the root site bundle, `site-manifest.json`, and web-surface deployment
73
+ manifests for named surfaces such as KFD, Buildchain, and Core. Site
74
+ repositories should render these fields; they should not invent their own
75
+ manifest time semantics.
76
+
77
+ Source checkouts may use `SOURCE_DATE_EPOCH` for deterministic local checks.
78
+ Published CI/release artifacts should inject real timestamps with
79
+ `BUILDCHAIN_SITE_GENERATED_AT` / `BUILDCHAIN_SITE_PUBLISHED_AT` or the matching
80
+ `BUILDCHAIN_SURFACE_*` variables, so public manifests do not expose epoch time
81
+ as if it were a real metadata time.
82
+
55
83
  ## npm Consumption
56
84
 
57
85
  ```bash
@@ -195,6 +195,25 @@ rollback:
195
195
  "channel": "preview",
196
196
  "alias": "sha-abcdef123456",
197
197
  "url": "https://sha-abcdef123456.preview.libkungfu.dev",
198
+ "generatedAt": "2026-07-01T00:00:00.000Z",
199
+ "publishedAt": "2026-07-01T00:00:00.000Z",
200
+ "reproducible": true,
201
+ "timestampPolicy": "ci-injected",
202
+ "deterministicInputs": [
203
+ "web-surface artifact content",
204
+ "buildchain.toml web-surface channels/deploy/surfaces",
205
+ "sourceSha",
206
+ "artifactHash",
207
+ "deployment channel",
208
+ "deployment alias"
209
+ ],
210
+ "sourceRevision": "...",
211
+ "timestampPolicyDetails": {
212
+ "contract": "kungfu-buildchain-surface-timestamp-policy",
213
+ "timestampFields": ["generatedAt", "publishedAt", "deployedAt"],
214
+ "timestampFieldsParticipateInArtifactDigest": false,
215
+ "artifactDigestScope": "web-surface artifactHash excludes deployment manifest timestamps"
216
+ },
198
217
  "sourceSha": "...",
199
218
  "artifactHash": "...",
200
219
  "deployTarget": "libkungfu-dev-preview",
@@ -231,6 +250,13 @@ Dynamic adapters can also fill `runtimeId`, `configFingerprint`,
231
250
  `rollbackLimitations`. Buildchain records secret reference names only, never
232
251
  secret values.
233
252
 
253
+ The timestamp policy is shared with package site bundles. Public deployment
254
+ manifests should expose real workflow generation/publication times while
255
+ separately declaring why the deployed artifact remains reproducible. For
256
+ web-surface deployment manifests, `artifactHash` is the static site artifact
257
+ digest and does not include deployment timestamp fields; the manifest itself
258
+ still records those fields for human and agent audit.
259
+
234
260
  ## Deploy Plans
235
261
 
236
262
  Deploy planning is the default behavior. It plans the adapter steps and writes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.8.8-alpha.5",
3
+ "version": "2.8.8-alpha.7",
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",
@@ -22,6 +22,7 @@
22
22
  "./release-candidate": "./packages/core/release-candidate.js",
23
23
  "./release-passport": "./packages/core/release-passport.js",
24
24
  "./release-propagation": "./packages/core/release-propagation.js",
25
+ "./surface-manifest": "./packages/core/surface-manifest.js",
25
26
  "./buildchain-kfd-claims": "./packages/core/buildchain-kfd-claims.js",
26
27
  "./site/buildchain-site.json": "./dist/site/buildchain-site.json",
27
28
  "./site/site-manifest.json": "./dist/site/site-manifest.json",
@@ -203,3 +203,9 @@ export {
203
203
  resolvePropagationChannel,
204
204
  writeReleasePropagationLock,
205
205
  } from "./release-propagation.js";
206
+
207
+ export {
208
+ SURFACE_TIMESTAMP_POLICY_CONTRACT,
209
+ applySurfaceTimestampPolicy,
210
+ createSurfaceTimestampPolicy,
211
+ } from "./surface-manifest.js";
@@ -14,6 +14,7 @@ import {
14
14
  validateKfd1ReleaseGateEvidence,
15
15
  validateKfd3CollaborationInterfaceReleaseGateEvidence,
16
16
  } from "./kfd-gate.js";
17
+ import { createSurfaceTimestampPolicy } from "./surface-manifest.js";
17
18
 
18
19
  export const RELEASE_PASSPORT_CONTRACT = "kungfu-buildchain-release-passport";
19
20
  export const ARTIFACT_EVIDENCE_CONTRACT = "kungfu-buildchain-artifact-evidence";
@@ -953,6 +954,7 @@ export function createReleasePassport({
953
954
  ? normalizeEvidenceDocument(distTagPromotionEvidence, "distTagPromotionEvidence")
954
955
  : undefined;
955
956
  const normalizedImpact = normalizeImpactLedger(impact, { tag: normalizedTag, line });
957
+ const generatedAt = nowIso();
956
958
  const kfd1Metadata = resolveKfd1Metadata();
957
959
  const normalizedKfd1 = kfd1?.passportSection ? kfd1 : undefined;
958
960
  const normalizedKfd3 = kfd3?.passportSection ? kfd3 : undefined;
@@ -1001,7 +1003,24 @@ export function createReleasePassport({
1001
1003
  return {
1002
1004
  schemaVersion: 1,
1003
1005
  contract: RELEASE_PASSPORT_CONTRACT,
1004
- generatedAt: nowIso(),
1006
+ generatedAt,
1007
+ surfaceTimestampPolicy: createSurfaceTimestampPolicy({
1008
+ generatedAt,
1009
+ publishedAt: optionalString(release.publishedAt || release.published_at || normalizedPublishEvidence?.publishedAt),
1010
+ sourceRevision: optionalString(sourceSha || releaseSha || normalizedTransaction?.releaseSha),
1011
+ timestampPolicy: "ci-injected",
1012
+ deterministicInputs: [
1013
+ "release.sourceSha",
1014
+ "release.releaseSha",
1015
+ "release.releaseMaterialSha",
1016
+ "artifact-evidence.json",
1017
+ "publish evidence",
1018
+ "release-state transaction",
1019
+ ],
1020
+ timestampFields: ["generatedAt", "publishedAt", "surfaceTimestampPolicy.generatedAt", "surfaceTimestampPolicy.publishedAt"],
1021
+ timestampFieldsParticipateInArtifactDigest: true,
1022
+ artifactDigestScope: "release passport JSON and release evidence bundle",
1023
+ }),
1005
1024
  product: {
1006
1025
  name: optionalString(productName || "Buildchain"),
1007
1026
  repository: optionalString(repository),
@@ -0,0 +1,87 @@
1
+ export const SURFACE_TIMESTAMP_POLICY_CONTRACT = "kungfu-buildchain-surface-timestamp-policy";
2
+
3
+ const DEFAULT_SOURCE_DATE_EPOCH = "0";
4
+
5
+ function nonEmptyString(value, fallback = "") {
6
+ const normalized = String(value ?? "").trim();
7
+ return normalized || fallback;
8
+ }
9
+
10
+ function isoFromEpochSeconds(value) {
11
+ const raw = nonEmptyString(value, DEFAULT_SOURCE_DATE_EPOCH);
12
+ if (!/^-?\d+$/.test(raw)) {
13
+ throw new Error(`sourceDateEpoch must be integer seconds since Unix epoch: ${value}`);
14
+ }
15
+ return new Date(Number(raw) * 1000).toISOString();
16
+ }
17
+
18
+ function normalizeIso(value, label) {
19
+ const normalized = nonEmptyString(value);
20
+ if (!normalized) {
21
+ return "";
22
+ }
23
+ const date = new Date(normalized);
24
+ if (Number.isNaN(date.getTime())) {
25
+ throw new Error(`${label} must be an ISO-8601 timestamp`);
26
+ }
27
+ return date.toISOString();
28
+ }
29
+
30
+ function uniqueStrings(values = []) {
31
+ return [...new Set(values.map((value) => nonEmptyString(value)).filter(Boolean))];
32
+ }
33
+
34
+ export function createSurfaceTimestampPolicy({
35
+ generatedAt = "",
36
+ publishedAt = "",
37
+ sourceDateEpoch = process.env.SOURCE_DATE_EPOCH || DEFAULT_SOURCE_DATE_EPOCH,
38
+ sourceRevision = "",
39
+ deterministicInputs = [],
40
+ timestampPolicy = "",
41
+ timestampFields = ["generatedAt", "publishedAt"],
42
+ timestampFieldsParticipateInArtifactDigest = true,
43
+ artifactDigestScope = "manifest-and-artifact",
44
+ reproducible = true,
45
+ } = {}) {
46
+ const normalizedGeneratedAt = normalizeIso(
47
+ generatedAt || isoFromEpochSeconds(sourceDateEpoch),
48
+ "generatedAt",
49
+ );
50
+ const normalizedPublishedAt = normalizeIso(publishedAt || generatedAt || "", "publishedAt");
51
+ const normalizedPolicy = nonEmptyString(
52
+ timestampPolicy,
53
+ generatedAt || publishedAt ? "ci-injected" : "source-date-epoch",
54
+ );
55
+ return {
56
+ generatedAt: normalizedGeneratedAt,
57
+ ...(normalizedPublishedAt ? { publishedAt: normalizedPublishedAt } : {}),
58
+ reproducible: Boolean(reproducible),
59
+ timestampPolicy: normalizedPolicy,
60
+ deterministicInputs: uniqueStrings([
61
+ ...deterministicInputs,
62
+ sourceRevision ? "sourceRevision" : "",
63
+ sourceDateEpoch ? "sourceDateEpoch" : "",
64
+ "package content",
65
+ "declared Buildchain surface manifest contract",
66
+ ]),
67
+ sourceDateEpoch: nonEmptyString(sourceDateEpoch),
68
+ ...(sourceRevision ? { sourceRevision: nonEmptyString(sourceRevision) } : {}),
69
+ timestampPolicyDetails: {
70
+ contract: SURFACE_TIMESTAMP_POLICY_CONTRACT,
71
+ timestampFields: uniqueStrings(timestampFields),
72
+ timestampFieldsParticipateInArtifactDigest: Boolean(timestampFieldsParticipateInArtifactDigest),
73
+ artifactDigestScope: nonEmptyString(artifactDigestScope, "manifest-and-artifact"),
74
+ note: "Human-readable timestamps are separate from reproducibility inputs; do not infer reproducibility from epoch timestamps.",
75
+ },
76
+ };
77
+ }
78
+
79
+ export function applySurfaceTimestampPolicy(manifest, options = {}) {
80
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
81
+ throw new Error("surface manifest must be a JSON object");
82
+ }
83
+ return {
84
+ ...manifest,
85
+ ...createSurfaceTimestampPolicy(options),
86
+ };
87
+ }
@@ -123,6 +123,9 @@ if (rootPackage.exports?.["./release-passport"] !== "./packages/core/release-pas
123
123
  if (rootPackage.exports?.["./release-propagation"] !== "./packages/core/release-propagation.js") {
124
124
  throw new Error("root package must export @kungfu-tech/buildchain/release-propagation");
125
125
  }
126
+ if (rootPackage.exports?.["./surface-manifest"] !== "./packages/core/surface-manifest.js") {
127
+ throw new Error("root package must export @kungfu-tech/buildchain/surface-manifest");
128
+ }
126
129
  if (rootPackage.exports?.["./buildchain-kfd-claims"] !== "./packages/core/buildchain-kfd-claims.js") {
127
130
  throw new Error("root package must export @kungfu-tech/buildchain/buildchain-kfd-claims");
128
131
  }
@@ -193,9 +196,18 @@ if (!coreIndexSource.includes("KFD2_RELEASE_TRUST_PASSPORT_CONTRACT")) {
193
196
  if (!coreIndexSource.includes("KFD2_TRUST_PROOF_CONTRACT")) {
194
197
  throw new Error("packages/core/index.js must export KFD-2 trust proof contract");
195
198
  }
199
+ if (!coreIndexSource.includes("createSurfaceTimestampPolicy")) {
200
+ throw new Error("packages/core/index.js must export surface manifest timestamp policy APIs");
201
+ }
196
202
  if (siteBundle.contract !== "kungfu-buildchain-site-bundle") {
197
203
  throw new Error("buildchain-site.json must expose the Buildchain site bundle contract");
198
204
  }
205
+ if (siteBundle.package?.version !== rootPackage.version) {
206
+ throw new Error("buildchain-site.json package.version must match package.json version");
207
+ }
208
+ if (siteManifest.package?.version !== rootPackage.version) {
209
+ throw new Error("site-manifest.json package.version must match package.json version");
210
+ }
199
211
  if (siteBundle.source?.homepageTextSource !== "README.md") {
200
212
  throw new Error("buildchain-site.json source.homepageTextSource must be README.md");
201
213
  }
@@ -241,6 +253,20 @@ if (siteBundle.pageRegistry?.path !== "page-registry.json" || siteBundle.pageReg
241
253
  if (!siteManifest.facts?.includes("page-registry.json") || !siteBundle.entrypoints?.includes("page-registry.json")) {
242
254
  throw new Error("site bundle entrypoints must include page-registry.json");
243
255
  }
256
+ for (const [name, manifest] of [["buildchain-site.json", siteBundle], ["site-manifest.json", siteManifest]]) {
257
+ if (!manifest.generatedAt) {
258
+ throw new Error(`${name} must expose generatedAt`);
259
+ }
260
+ if (manifest.timestampPolicy === "ci-injected" && manifest.generatedAt === "1970-01-01T00:00:00.000Z") {
261
+ throw new Error(`${name} must not use epoch generatedAt when timestampPolicy=ci-injected`);
262
+ }
263
+ if (!manifest.timestampPolicy || !manifest.reproducible || !Array.isArray(manifest.deterministicInputs)) {
264
+ throw new Error(`${name} must expose the Buildchain surface timestamp/reproducibility policy`);
265
+ }
266
+ if (manifest.timestampPolicyDetails?.contract !== "kungfu-buildchain-surface-timestamp-policy") {
267
+ throw new Error(`${name} must expose timestampPolicyDetails.contract`);
268
+ }
269
+ }
244
270
  function immediateReadmes(dir) {
245
271
  const absoluteDir = path.join(root, dir);
246
272
  if (!fs.existsSync(absoluteDir)) return [];
@@ -8,6 +8,7 @@ import {
8
8
  BUILDCHAIN_AGENT_MANUALS,
9
9
  createBuildchainKfdClaimRegistry,
10
10
  } from "../packages/core/buildchain-kfd-claims.js";
11
+ import { createSurfaceTimestampPolicy } from "../packages/core/surface-manifest.js";
11
12
 
12
13
  const SITE_BUNDLE_CONTRACT = "kungfu-buildchain-site-bundle";
13
14
  const README_PATH = "README.md";
@@ -31,6 +32,10 @@ function stableJson(value) {
31
32
  return `${JSON.stringify(value, null, 2)}\n`;
32
33
  }
33
34
 
35
+ function env(name) {
36
+ return String(process.env[name] || "").trim();
37
+ }
38
+
34
39
  function sha256File(rel) {
35
40
  const filePath = path.join(root, rel);
36
41
  return fs.existsSync(filePath) && fs.statSync(filePath).isFile()
@@ -215,6 +220,30 @@ function buildSiteBundle() {
215
220
  const readme = parseReadme(readText(README_PATH));
216
221
  const docs = BUILDCHAIN_AGENT_MANUALS.map((manual) => docEntry(manual.id, manual.title, manual.path, manual.plane));
217
222
  const pages = buildSitePages();
223
+ const generatedAtInput = env("BUILDCHAIN_SITE_GENERATED_AT") || env("BUILDCHAIN_SURFACE_GENERATED_AT");
224
+ const publishedAtInput = env("BUILDCHAIN_SITE_PUBLISHED_AT") || env("BUILDCHAIN_SURFACE_PUBLISHED_AT");
225
+ const timestampPolicyInput = env("BUILDCHAIN_SITE_TIMESTAMP_POLICY") || env("BUILDCHAIN_SURFACE_TIMESTAMP_POLICY");
226
+ const sourceRevisionInput =
227
+ env("BUILDCHAIN_SOURCE_SHA") ||
228
+ (generatedAtInput || publishedAtInput || timestampPolicyInput ? env("GITHUB_SHA") : "");
229
+ const timestampPolicy = createSurfaceTimestampPolicy({
230
+ generatedAt: generatedAtInput,
231
+ publishedAt: publishedAtInput,
232
+ sourceDateEpoch: env("SOURCE_DATE_EPOCH") || "0",
233
+ sourceRevision: sourceRevisionInput,
234
+ timestampPolicy: timestampPolicyInput,
235
+ deterministicInputs: [
236
+ "README.md",
237
+ "docs/*.md",
238
+ "actions/*/README.md",
239
+ "fixtures/*/README.md",
240
+ "packages/core/README.md",
241
+ "package.json#exports",
242
+ "tests/buildchain-inventory.json",
243
+ ],
244
+ timestampFieldsParticipateInArtifactDigest: true,
245
+ artifactDigestScope: "npm package dist/site JSON files",
246
+ });
218
247
 
219
248
  const cliRegistry = {
220
249
  schemaVersion: 1,
@@ -438,6 +467,7 @@ function buildSiteBundle() {
438
467
  const siteManifest = {
439
468
  schemaVersion: 1,
440
469
  contract: "kungfu-buildchain-site-manifest",
470
+ ...timestampPolicy,
441
471
  product: {
442
472
  name: "Buildchain",
443
473
  formalName: "Buildchain by Kungfu",
@@ -445,6 +475,7 @@ function buildSiteBundle() {
445
475
  },
446
476
  package: {
447
477
  name: packageJson.name,
478
+ version: packageJson.version,
448
479
  versionSource: "package.json#version",
449
480
  },
450
481
  entrypoint: "buildchain-site.json",
@@ -549,6 +580,7 @@ function buildSiteBundle() {
549
580
  const siteBundle = {
550
581
  schemaVersion: 1,
551
582
  contract: SITE_BUNDLE_CONTRACT,
583
+ ...timestampPolicy,
552
584
  product: siteManifest.product,
553
585
  package: siteManifest.package,
554
586
  source: {
@@ -5,6 +5,7 @@ import fs from "node:fs";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { loadBuildchainConfig, validateBuildchainConfig } from "../packages/core/buildchain-config.js";
8
+ import { createSurfaceTimestampPolicy } from "../packages/core/surface-manifest.js";
8
9
 
9
10
  const DEFAULT_RETENTION = Object.freeze({
10
11
  preview: {
@@ -609,9 +610,27 @@ export function createWebSurfaceDeploymentManifest({
609
610
  deployConfig,
610
611
  });
611
612
  const primaryBinding = surfaceBindings[0];
613
+ const timestampPolicy = createSurfaceTimestampPolicy({
614
+ generatedAt: deployedAt,
615
+ publishedAt: deployedAt,
616
+ sourceRevision: sourceSha,
617
+ timestampPolicy: "ci-injected",
618
+ deterministicInputs: [
619
+ "web-surface artifact content",
620
+ "buildchain.toml web-surface channels/deploy/surfaces",
621
+ "sourceSha",
622
+ "artifactHash",
623
+ "deployment channel",
624
+ "deployment alias",
625
+ ],
626
+ timestampFields: ["generatedAt", "publishedAt", "deployedAt"],
627
+ timestampFieldsParticipateInArtifactDigest: false,
628
+ artifactDigestScope: "web-surface artifactHash excludes deployment manifest timestamps",
629
+ });
612
630
  return {
613
631
  schemaVersion: 1,
614
632
  contract: "kungfu-buildchain-web-surface-deployment",
633
+ ...timestampPolicy,
615
634
  site: config.project.site || config.project.name || "",
616
635
  channel,
617
636
  alias,
@@ -77,6 +77,10 @@ export function webSurfaceCli() {
77
77
  "alias",
78
78
  process.env.BUILDCHAIN_WEB_SURFACE_ALIAS || defaultWebSurfaceAlias({ channel, sourceSha, pullNumber }),
79
79
  );
80
+ const generatedAt = readArg(
81
+ "generated-at",
82
+ process.env.BUILDCHAIN_SITE_GENERATED_AT || process.env.BUILDCHAIN_SURFACE_GENERATED_AT || "",
83
+ );
80
84
  const result = planWebSurfaceDeploy({
81
85
  cwd,
82
86
  channel,
@@ -89,6 +93,7 @@ export function webSurfaceCli() {
89
93
  rollbackPointer: readArg("rollback-pointer", process.env.BUILDCHAIN_ROLLBACK_REF || ""),
90
94
  rollbackLimitations: readArg("rollback-limitations", process.env.BUILDCHAIN_ROLLBACK_LIMITATIONS || ""),
91
95
  dryRun: readBooleanArg("dry-run", true),
96
+ ...(generatedAt ? { deployedAt: generatedAt } : {}),
92
97
  });
93
98
  const outputResult = mode === "manifest" ? result.manifest : result;
94
99
  writeJson(outputResult, output);
@@ -111,6 +116,10 @@ export function webSurfaceCli() {
111
116
  "alias",
112
117
  process.env.BUILDCHAIN_WEB_SURFACE_ALIAS || defaultWebSurfaceAlias({ channel, sourceSha, pullNumber }),
113
118
  );
119
+ const publishedAt = readArg(
120
+ "published-at",
121
+ process.env.BUILDCHAIN_SITE_PUBLISHED_AT || process.env.BUILDCHAIN_SURFACE_PUBLISHED_AT || "",
122
+ );
114
123
  const result = applyWebSurfaceDeploy({
115
124
  cwd,
116
125
  channel,
@@ -122,6 +131,7 @@ export function webSurfaceCli() {
122
131
  dryRun: readBooleanArg("dry-run", true),
123
132
  actor: readArg("actor", process.env.GITHUB_ACTOR || ""),
124
133
  runId: readArg("run-id", process.env.GITHUB_RUN_ID || ""),
134
+ ...(publishedAt ? { appliedAt: publishedAt } : {}),
125
135
  });
126
136
  writeJson(result, output);
127
137
  writeGitHubOutputs({