From a82bc2f3a1d75ddbc0b7fa8c13dd83e423f71f3d Mon Sep 17 00:00:00 2001 From: Andrew Peris Date: Wed, 7 Oct 2026 15:50:47 -0400 Subject: [PATCH 1/8] Prepare Comic Manifest v1 contract release assets --- releases/README.md | 6 ++ releases/comic-manifest-v1.json | 8 +++ releases/comic-manifest-v1.md | 107 +++++++++++++++++++++++++++++ scripts/build-contract-release.mjs | 2 +- tests/contract-release.test.mjs | 38 ++++++++++ 5 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 releases/comic-manifest-v1.json create mode 100644 releases/comic-manifest-v1.md diff --git a/releases/README.md b/releases/README.md index 276d62f..9d2461a 100644 --- a/releases/README.md +++ b/releases/README.md @@ -4,6 +4,12 @@ For the additive Context Builder family use the independent [#86 checklist](context-builder-v1.md) and explicit `context-builder-v1` catalog. The historical five-contract catalog below must not be republished at its existing tags. +For the additive Comic Manifest family use the independent +[#99 checklist](comic-manifest-v1.md) and explicit `comic-manifest-v1` catalog. +It contains only `comic-manifest` v1.0.0 at +`contract/comic-manifest/v1.0.0`; neither the Prompt SDK nor Context Builder +catalog or immutable bytes are republished by this catalog. + Owner: @andrewperis. This is issue #72 release preparation, not evidence that the contracts are already published. Existing provisional notices remain true until the steps below complete. No release or tag is created by the builder. diff --git a/releases/comic-manifest-v1.json b/releases/comic-manifest-v1.json new file mode 100644 index 0000000..f4e0ffe --- /dev/null +++ b/releases/comic-manifest-v1.json @@ -0,0 +1,8 @@ +{ + "version": "1.0.0", + "repository": "DefinitelySecureStudio/codex", + "constitution_commit": "a9cc8a503aa30e17820edc62ac95f7cbe10e0564", + "contracts": [ + { "name": "comic-manifest", "schema": "schemas/json/comic-manifest/v1/comic-manifest.schema.json" } + ] +} diff --git a/releases/comic-manifest-v1.md b/releases/comic-manifest-v1.md new file mode 100644 index 0000000..7bcbf63 --- /dev/null +++ b/releases/comic-manifest-v1.md @@ -0,0 +1,107 @@ +# Comic Manifest v1 publication checklist — Studio #99 + +Owner: @andrewperis. This is release preparation only. It creates no tag, +release, asset, npm publication, visibility change, security-policy change, or +production permission. The candidate catalog contains only the additive +`comic-manifest` contract; the Prompt SDK five-contract and Context Builder +single-contract catalogs remain separate and their historical releases stay +unchanged. + +## Candidate and review boundary + +- Contract: `comic-manifest` 1.0.0, exact tag + `contract/comic-manifest/v1.0.0`, schema ID + `urn:definitely-secure:contract:comic-manifest:1.0.0:comic-manifest`. +- The contract combines production manifest, build-result, public-release, and + detached approval definitions in the one reviewed closed schema. It does not + authorize execution, publication, access, or Canon promotion. +- Normative sources are RFC 0007, RFC 0008, the Comic Manifest v1 specification, + versioning policy, schema README, and synthetic fixtures. The owner-approved + golden/catalog/oracle packet recorded by Platform #41 is evidence only for + those exact proposed files at source head + `97dcd990e87bd0368ddcbecf66d1a6718bc7629e` (chat acceptance + `2026-10-07T19:15:38Z`). It was not a GitHub review and is not release approval. + No approved golden, oracle, source, or scenario bytes are modified here. +- Constitution v1.0.0 is pinned to `constitution/v1.0.0`, Studio commit + `a9cc8a503aa30e17820edc62ac95f7cbe10e0564`. The public exception register + lists no active Article 12 exception; this candidate does not rely on one. + AI-assisted review and automation are advisory or establish only recorded, + bounded predicates. They do not satisfy Article 9.4 reserved human judgments + or final A4 release decisions. No human review of these new release artifacts + is claimed by this preparation record. +- Existing dependency versions and lock bytes are unchanged. Review the exact + lockfile audit report and the Apache-2.0 LICENSE, NOTICE, and any + THIRD_PARTY_NOTICES included in the bundle before approving release. + +## Build and verify locally + +1. Merge the reviewed Codex preparation change only after the contract owner + approves its exact head. Then use the clean merged Codex commit as the + release source; a branch, PR head, or local preparation build is not the + final release identity. +2. Run the full Codex test suite and the fresh locked-dependency and + license/NOTICE review. Resolve Blocker/Major findings before release. +3. On a clean checkout, build twice into two new external directories: + + ```sh + node scripts/build-contract-release.mjs /absolute/new/codex-comic-1 comic-manifest-v1 + node scripts/build-contract-release.mjs /absolute/new/codex-comic-2 comic-manifest-v1 + ``` + +4. Compare every filename and byte across both directories. Each directory has + exactly these three expected outputs: + `comic-manifest-v1.0.0.schema.json`, + `comic-manifest-v1.0.0.bundle.json`, and + `comic-manifest-v1.0.0.manifest.json`. The manifest binds the exact source + commit, schema ID, tag, schema/bundle URIs, declared media types, sizes and + SHA-256 values. Its schema and bundle `assets` entries are the two payload + assets; the manifest file is attached as the third release asset and has its + own independently recorded digest in the approval packet. This avoids a + self-referential manifest digest. +5. Inspect the schema and bundle bytes, check every decoded bundle member's + relative path, byte count and digest, and confirm LICENSE and NOTICE are + included. The bundle carries the reviewed repository source for provenance; + only the single Comic Manifest contract is an asset/tag in this catalog. +6. Confirm the exact release/tag target, asset names and immutable-release + setting. `application/schema+json` and `application/json` describe the + expected downloaded file contents. GitHub upload/download response MIME + headers are transport metadata and may be `application/octet-stream`; record + both values separately and never rewrite the declared contract media type to + match the transport header. + +## Owner-approved publication sequence + +This checklist is not itself permission to publish. After review/merge, request +an explicit owner decision naming the exact merged Codex commit and all three +asset sizes/digests. Only then may the owner create a draft release at +`contract/comic-manifest/v1.0.0`, attach the schema, bundle, and manifest, and +review the notes. Download all three draft assets independently and compare +every byte with both clean builds. Publish once only after this review and +download check. Then independently verify that the release is published and +immutable, the exact tag resolves to the approved commit, and fresh downloads +still match all identities. Never move the tag or replace an asset; a correction +requires a new reviewed version. + +## Consumer handoff and compatibility + +After Codex publication, Platform separately downloads the exact assets, checks +the tag/commit, sizes and digests, regenerates its validator from the downloaded +schema, reviews the consumer lock, and passes its supported CI. Platform's +component tag proposal is `comic-manifest/v1.0.0`; its additive package release +candidate is 1.2.0. Neither candidate is final until that adoption and its own +owner release decision are complete. Prompt SDK v1, Context Package v1, and +Context Builder v1 immutable artifacts are not republished. + +Compatibility remains exact kind/version negotiation. A compatible correction +requires a reviewed patch release; additive contract capabilities require an +explicitly supported version; breaking meaning requires an accepted RFC, new +major, affected-consumer inventory, side-by-side fixtures, migration and +rollback plan, deprecation release, and dated support window. No retirement or +support window starts in this initial release preparation. + +Issue #101 / Build Orchestrator receives verified Codex and Platform artifact +tuples, conformance and verification evidence, API/CLI and adapter boundaries, +limits, known limitations, and unresolved production-trust/owner gates. This +handoff does not add scheduling, retries, providers, rendering, production +trust, access renewal, canon, or publishing automation. Close #99 and Epic #6 +only after the actual approved publications and fresh-download verifications. diff --git a/scripts/build-contract-release.mjs b/scripts/build-contract-release.mjs index f505c25..bf073aa 100644 --- a/scripts/build-contract-release.mjs +++ b/scripts/build-contract-release.mjs @@ -7,7 +7,7 @@ const root = fileURLToPath(new URL('../', import.meta.url)); const git = (...args) => execFileSync('git', args, { cwd: root, maxBuffer: 16 * 1024 * 1024 }); const sha = bytes => 'sha256:' + createHash('sha256').update(bytes).digest('hex'); export async function buildContractRelease(destination, catalogName = 'prompt-sdk-v1') { - if (!['prompt-sdk-v1', 'context-builder-v1'].includes(catalogName)) throw new Error('Unknown release catalog.'); + if (!['prompt-sdk-v1', 'context-builder-v1', 'comic-manifest-v1'].includes(catalogName)) throw new Error('Unknown release catalog.'); if (!destination) throw new Error('Provide a new output directory outside the checkout.'); const output = resolve(destination), rel = relative(root, output); if (!rel || (!rel.startsWith('..' + '/') && !isAbsolute(rel))) throw new Error('Output must be outside the checkout.'); diff --git a/tests/contract-release.test.mjs b/tests/contract-release.test.mjs index f0f9a06..1c8215e 100644 --- a/tests/contract-release.test.mjs +++ b/tests/contract-release.test.mjs @@ -63,3 +63,41 @@ test('Builder catalog publishes only the additive family with unchanged schema b assert.ok(bundle.files.some(file => file.path === path)); } }); + +test('Comic Manifest has a separate deterministic one-contract catalog', async t => { + const root = await mkdtemp(join(tmpdir(), 'comic-manifest-release-test-')); + t.after(() => rm(root, { recursive: true, force: true })); + const first = join(root, 'first'), second = join(root, 'second'); + const [manifest] = await buildContractRelease(first, 'comic-manifest-v1'); + await buildContractRelease(second, 'comic-manifest-v1'); + + assert.equal(manifest.contract, 'comic-manifest'); + assert.equal(manifest.version, '1.0.0'); + assert.equal(manifest.tag, 'contract/comic-manifest/v1.0.0'); + assert.equal(manifest.schema_id, 'urn:definitely-secure:contract:comic-manifest:1.0.0:comic-manifest'); + assert.equal(manifest.constitution_commit, 'a9cc8a503aa30e17820edc62ac95f7cbe10e0564'); + assert.equal(manifest.assets.length, 2); + assert.deepEqual(manifest.assets.map(asset => asset.filename), [ + 'comic-manifest-v1.0.0.schema.json', 'comic-manifest-v1.0.0.bundle.json' + ]); + assert.equal((await readdir(first)).length, 3); + for (const file of await readdir(first)) assert.deepEqual(await readFile(join(first, file)), await readFile(join(second, file))); + + const schema = JSON.parse(await readFile(join(first, 'comic-manifest-v1.0.0.schema.json'))); + assert.equal(schema.$id, manifest.schema_id); + for (const asset of manifest.assets) { + const bytes = await readFile(join(first, asset.filename)); + assert.equal(bytes.length, asset.byte_size); + assert.equal(hash(bytes), asset.sha256); + assert.equal(asset.artifact_uri, `https://github.com/DefinitelySecureStudio/codex/releases/download/${encodeURIComponent(manifest.tag)}/${asset.filename}`); + } + const bundle = JSON.parse(await readFile(join(first, 'comic-manifest-v1.0.0.bundle.json'))); + assert.equal(bundle.contract, 'comic-manifest'); + assert.equal(bundle.version, '1.0.0'); + assert.ok(bundle.files.some(file => file.path === 'LICENSE')); + assert.ok(bundle.files.some(file => file.path === 'NOTICE')); + assert.ok(bundle.files.some(file => file.path === 'releases/comic-manifest-v1.json')); + assert.ok(bundle.files.some(file => file.path === 'releases/comic-manifest-v1.md')); + assert.ok(!manifest.tag.includes('prompt-definition')); + assert.ok(!manifest.tag.includes('context-builder')); +}); From 7a13c5ded2c902da77355fdd606ec8a5337279fc Mon Sep 17 00:00:00 2001 From: Andrew Peris Date: Wed, 7 Oct 2026 15:54:03 -0400 Subject: [PATCH 2/8] Bind release artifacts to exact source tree and record Constitution assessment --- CONSTITUTION_CONFORMANCE.md | 31 ++++++++++++++++++++++++++++++ releases/comic-manifest-v1.md | 8 ++++++++ scripts/build-contract-release.mjs | 28 ++++++++++++++++++++------- tests/contract-release.test.mjs | 11 +++++++++++ 4 files changed, 71 insertions(+), 7 deletions(-) diff --git a/CONSTITUTION_CONFORMANCE.md b/CONSTITUTION_CONFORMANCE.md index 236d1e8..b77d91f 100644 --- a/CONSTITUTION_CONFORMANCE.md +++ b/CONSTITUTION_CONFORMANCE.md @@ -178,3 +178,34 @@ The owner approves this exact assessment by reviewing and merging the contract pull request. No provider response, schema publication, restricted raw store, consumer implementation, downstream use, deployment, publication, or release inherits approval. + +## Studio #99 Comic Manifest v1 release-preparation candidate — 2026-10-07 + +Accountable owner: @andrewperis. Assessed source: Codex +`8044643bf888067f5a6e0d212f843d72e8787f2b` plus the exact release-preparation +candidate diff. Constitution v1.0.0, tag `constitution/v1.0.0`, Studio commit +`a9cc8a503aa30e17820edc62ac95f7cbe10e0564`. Profiles: universal; +repository/production-system; stable-contract release. Scope is the separate +Comic Manifest 1.0.0 catalog, its builder path and focused tests, and the +publication checklist. The Comic Manifest schema/specification/fixture bytes, +five-contract Prompt SDK catalog, Context Builder catalog, locked dependencies, +and their historical tags/assets are unchanged. Status: **Transition required**; +this is not a release approval or publication record. + +The Issue 98 owner golden acceptance cited by Platform PR #41 is bounded to the +proposed catalog/golden/oracle bytes at Platform source head +`97dcd990e87bd0368ddcbecf66d1a6718bc7629e`, recorded in chat at +`2026-10-07T19:15:38Z`. PR #41 states that it was not a submitted GitHub review. +It does not approve this Codex release-preparation diff, the Codex release +assets, a consumer adoption, or final publication. No human review of those +release artifacts is claimed here. + +The current public Studio exception register says there are no active Article +12 exceptions. This assessment therefore identifies no AI-review exception +and relies on none. AI-assisted review and automated checks are advisory or +bounded predicate evidence; Constitution Section 9.4 reserved judgments and +final A4 decisions still require the authorized qualified human. The release +gate remains open for exact-head owner/CODEOWNER review, a fresh locked-dependency +audit, final license/NOTICE review, post-merge reproducible builds, and explicit +owner publication approval. No tag, release, asset, npm publication, visibility +change, production trust, or Epic #6 closeout is authorized or represented. diff --git a/releases/comic-manifest-v1.md b/releases/comic-manifest-v1.md index 7bcbf63..e8724cc 100644 --- a/releases/comic-manifest-v1.md +++ b/releases/comic-manifest-v1.md @@ -48,6 +48,14 @@ unchanged. node scripts/build-contract-release.mjs /absolute/new/codex-comic-2 comic-manifest-v1 ``` + For a local repository snapshot whose verified remote source commit has a + different commit object but the exact same tree, both commands may append + `--source-commit VERIFIED_COMMIT --source-tree VERIFIED_TREE`. The builder + requires the supplied tree to equal the clean local `HEAD` tree and reads + every file from that immutable local Git tree. Obtain both values from the + repository's Git commit API; do not infer them from a branch name. A normal + merged checkout uses `HEAD` and needs no override. + 4. Compare every filename and byte across both directories. Each directory has exactly these three expected outputs: `comic-manifest-v1.0.0.schema.json`, diff --git a/scripts/build-contract-release.mjs b/scripts/build-contract-release.mjs index bf073aa..ae38ea7 100644 --- a/scripts/build-contract-release.mjs +++ b/scripts/build-contract-release.mjs @@ -6,17 +6,24 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; const root = fileURLToPath(new URL('../', import.meta.url)); const git = (...args) => execFileSync('git', args, { cwd: root, maxBuffer: 16 * 1024 * 1024 }); const sha = bytes => 'sha256:' + createHash('sha256').update(bytes).digest('hex'); -export async function buildContractRelease(destination, catalogName = 'prompt-sdk-v1') { +export async function buildContractRelease(destination, catalogName = 'prompt-sdk-v1', sourceIdentity = {}) { if (!['prompt-sdk-v1', 'context-builder-v1', 'comic-manifest-v1'].includes(catalogName)) throw new Error('Unknown release catalog.'); if (!destination) throw new Error('Provide a new output directory outside the checkout.'); const output = resolve(destination), rel = relative(root, output); if (!rel || (!rel.startsWith('..' + '/') && !isAbsolute(rel))) throw new Error('Output must be outside the checkout.'); if (git('status', '--porcelain').length) throw new Error('Commit or remove working-tree changes before building.'); - const commit = git('rev-parse', 'HEAD').toString().trim(); - const catalog = JSON.parse(git('show', commit + ':releases/' + catalogName + '.json')); - const files = git('ls-tree', '-r', '--name-only', '-z', commit).toString().split('\0').filter(Boolean).sort(); + const localCommit = git('rev-parse', 'HEAD').toString().trim(); + const localTree = git('rev-parse', localCommit + '^{tree}').toString().trim(); + const hasRemoteIdentity = sourceIdentity?.commit !== undefined || sourceIdentity?.tree !== undefined; + if (hasRemoteIdentity && (!/^[a-f0-9]{40}$/.test(sourceIdentity.commit ?? '') || + !/^[a-f0-9]{40}$/.test(sourceIdentity.tree ?? '') || sourceIdentity.tree !== localTree)) { + throw new Error('Supplied source commit must be paired with its exact matching Git tree.'); + } + const commit = hasRemoteIdentity ? sourceIdentity.commit : localCommit; + const catalog = JSON.parse(git('show', localCommit + ':releases/' + catalogName + '.json')); + const files = git('ls-tree', '-r', '--name-only', '-z', localCommit).toString().split('\0').filter(Boolean).sort(); const entries = files.map(path => { - const bytes = git('show', commit + ':' + path); + const bytes = git('show', localCommit + ':' + path); return { path, byte_size: bytes.length, sha256: sha(bytes), content_base64: bytes.toString('base64') }; }); // Includes all reviewed tracked sources so relative documentation links, @@ -26,7 +33,7 @@ export async function buildContractRelease(destination, catalogName = 'prompt-sd for (const contract of catalog.contracts) { const tag = 'contract/' + contract.name + '/v' + catalog.version; const prefix = contract.name + '-v' + catalog.version; - const schema = git('show', commit + ':' + contract.schema); + const schema = git('show', localCommit + ':' + contract.schema); const schemaId = JSON.parse(schema).$id; if (!schemaId.startsWith('urn:definitely-secure:contract:' + contract.name + ':' + catalog.version + ':')) throw new Error('Schema identifier does not match catalog.'); const bundle = Buffer.from(JSON.stringify({ format: 'studio-contract-source-bundle-v1', repository: catalog.repository, contract: contract.name, version: catalog.version, commit, files: entries }, null, 2) + '\n'); @@ -42,5 +49,12 @@ export async function buildContractRelease(destination, catalogName = 'prompt-sd return releases; } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { - console.log(JSON.stringify(await buildContractRelease(process.argv[2], process.argv[3]), null, 2)); + const options = process.argv.slice(4); + const identity = {}; + for (let index = 0; index < options.length; index += 2) { + const key = options[index], value = options[index + 1]; + if (!['--source-commit', '--source-tree'].includes(key) || !value || value.startsWith('--')) throw new Error('Unknown or incomplete argument.'); + identity[key === '--source-commit' ? 'commit' : 'tree'] = value; + } + console.log(JSON.stringify(await buildContractRelease(process.argv[2], process.argv[3], identity), null, 2)); } diff --git a/tests/contract-release.test.mjs b/tests/contract-release.test.mjs index 1c8215e..cde3009 100644 --- a/tests/contract-release.test.mjs +++ b/tests/contract-release.test.mjs @@ -4,6 +4,7 @@ import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createHash } from 'node:crypto'; +import { execFileSync } from 'node:child_process'; import { buildContractRelease } from '../scripts/build-contract-release.mjs'; const hash = bytes => 'sha256:' + createHash('sha256').update(bytes).digest('hex'); test('contract release bundles are reproducible and every file identity verifies', async t => { @@ -101,3 +102,13 @@ test('Comic Manifest has a separate deterministic one-contract catalog', async t assert.ok(!manifest.tag.includes('prompt-definition')); assert.ok(!manifest.tag.includes('context-builder')); }); + +test('release builder accepts an explicit source commit only for an exact matching tree', async t => { + const root = await mkdtemp(join(tmpdir(), 'codex-release-source-identity-')); + t.after(() => rm(root, { recursive: true, force: true })); + const tree = execFileSync('git', ['rev-parse', 'HEAD^{tree}'], { cwd: new URL('../', import.meta.url) }).toString().trim(); + const remoteCommit = 'a'.repeat(40); + const [manifest] = await buildContractRelease(join(root, 'matching'), 'comic-manifest-v1', { commit: remoteCommit, tree }); + assert.equal(manifest.commit, remoteCommit); + await assert.rejects(buildContractRelease(join(root, 'mismatch'), 'comic-manifest-v1', { commit: remoteCommit, tree: 'b'.repeat(40) }), /exact matching Git tree/); +}); From edef7684d07f7b1f53ca7b077b521077a0a164d4 Mon Sep 17 00:00:00 2001 From: Andrew Peris Date: Wed, 7 Oct 2026 16:17:31 -0400 Subject: [PATCH 3/8] Clarify A3 code-merge exception and release boundary --- CONSTITUTION_CONFORMANCE.md | 19 ++++++++++--------- releases/comic-manifest-v1.md | 15 +++++++++------ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/CONSTITUTION_CONFORMANCE.md b/CONSTITUTION_CONFORMANCE.md index b77d91f..92cf7c7 100644 --- a/CONSTITUTION_CONFORMANCE.md +++ b/CONSTITUTION_CONFORMANCE.md @@ -200,12 +200,13 @@ It does not approve this Codex release-preparation diff, the Codex release assets, a consumer adoption, or final publication. No human review of those release artifacts is claimed here. -The current public Studio exception register says there are no active Article -12 exceptions. This assessment therefore identifies no AI-review exception -and relies on none. AI-assisted review and automated checks are advisory or -bounded predicate evidence; Constitution Section 9.4 reserved judgments and -final A4 decisions still require the authorized qualified human. The release -gate remains open for exact-head owner/CODEOWNER review, a fresh locked-dependency -audit, final license/NOTICE review, post-merge reproducible builds, and explicit -owner publication approval. No tag, release, asset, npm publication, visibility -change, production trust, or Epic #6 closeout is authorized or represented. +The owner-provided Epic #6 A3 AI-review exception applies to code-merge review +only. It authorizes AI review within that merge scope; it does not authorize an +A4 release decision, tag, release asset, or publication. No release/publication +exception is provided. No human review of the final release artifacts is +claimed here, and a separate explicit owner release approval remains required. +The release gate remains open for exact-head review/merge, a fresh +locked-dependency audit, final license/NOTICE review, post-merge reproducible +builds, and owner approval of exact artifact tuples. No tag, release, asset, +npm publication, visibility change, production trust, or Epic #6 closeout is +authorized or represented. diff --git a/releases/comic-manifest-v1.md b/releases/comic-manifest-v1.md index e8724cc..3954666 100644 --- a/releases/comic-manifest-v1.md +++ b/releases/comic-manifest-v1.md @@ -23,15 +23,18 @@ unchanged. `2026-10-07T19:15:38Z`). It was not a GitHub review and is not release approval. No approved golden, oracle, source, or scenario bytes are modified here. - Constitution v1.0.0 is pinned to `constitution/v1.0.0`, Studio commit - `a9cc8a503aa30e17820edc62ac95f7cbe10e0564`. The public exception register - lists no active Article 12 exception; this candidate does not rely on one. - AI-assisted review and automation are advisory or establish only recorded, - bounded predicates. They do not satisfy Article 9.4 reserved human judgments - or final A4 release decisions. No human review of these new release artifacts - is claimed by this preparation record. + `a9cc8a503aa30e17820edc62ac95f7cbe10e0564`. The owner's task-specific Epic + #6 A3 AI-review exception applies to code-merge review only. It does not + authorize an A4 release decision, tag, release asset, or publication; no + release/publication exception is provided. No human review of final release + artifacts is claimed here, and separate explicit owner release approval + remains required. - Existing dependency versions and lock bytes are unchanged. Review the exact lockfile audit report and the Apache-2.0 LICENSE, NOTICE, and any THIRD_PARTY_NOTICES included in the bundle before approving release. +- A fresh `npm audit --json` against this Codex preparation branch's lockfile + reports zero advisories; rerun it against the exact merged head before + publication. ## Build and verify locally From eaef2da8878f870e2bad1dddd3db06c7844846b2 Mon Sep 17 00:00:00 2001 From: Andrew Peris Date: Wed, 7 Oct 2026 16:42:59 -0400 Subject: [PATCH 4/8] Verify contract release source provenance --- releases/comic-manifest-v1.md | 13 +++++--- scripts/build-contract-release.mjs | 51 +++++++++++++++++++++++++----- tests/contract-release.test.mjs | 26 +++++++++++---- 3 files changed, 70 insertions(+), 20 deletions(-) diff --git a/releases/comic-manifest-v1.md b/releases/comic-manifest-v1.md index 3954666..ab2527a 100644 --- a/releases/comic-manifest-v1.md +++ b/releases/comic-manifest-v1.md @@ -53,11 +53,14 @@ unchanged. For a local repository snapshot whose verified remote source commit has a different commit object but the exact same tree, both commands may append - `--source-commit VERIFIED_COMMIT --source-tree VERIFIED_TREE`. The builder - requires the supplied tree to equal the clean local `HEAD` tree and reads - every file from that immutable local Git tree. Obtain both values from the - repository's Git commit API; do not infer them from a branch name. A normal - merged checkout uses `HEAD` and needs no override. + `--source-commit VERIFIED_COMMIT --source-tree VERIFIED_TREE`. First make + the source commit object available locally (for example, fetch that exact + public commit without tags) and confirm the GitHub commit API's tree. The + builder independently resolves the supplied commit object's tree, requires + it to match both the supplied tree and the clean local `HEAD` tree, then + reads all bundled files from that local tree. A commit-shaped caller string + without a verified local commit object fails closed. A normal merged + checkout uses `HEAD` and needs no override. 4. Compare every filename and byte across both directories. Each directory has exactly these three expected outputs: diff --git a/scripts/build-contract-release.mjs b/scripts/build-contract-release.mjs index ae38ea7..85bf200 100644 --- a/scripts/build-contract-release.mjs +++ b/scripts/build-contract-release.mjs @@ -1,23 +1,58 @@ import { execFileSync } from 'node:child_process'; -import { mkdir, writeFile } from 'node:fs/promises'; +import { lstat, mkdir, realpath, stat, writeFile } from 'node:fs/promises'; import { createHash } from 'node:crypto'; -import { resolve, relative, isAbsolute } from 'node:path'; +import { resolve, relative, isAbsolute, sep, dirname, basename } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; const root = fileURLToPath(new URL('../', import.meta.url)); const git = (...args) => execFileSync('git', args, { cwd: root, maxBuffer: 16 * 1024 * 1024 }); const sha = bytes => 'sha256:' + createHash('sha256').update(bytes).digest('hex'); + +export async function resolveExternalOutput(destination, checkoutRoot = root) { + if (!destination) throw new Error('Provide a new output directory outside the checkout.'); + const requested = resolve(destination), missing = []; + let ancestor = requested; + while (true) { + try { await lstat(ancestor); break; } + catch (error) { + if (error.code !== 'ENOENT') throw error; + const parent = dirname(ancestor); + if (parent === ancestor) throw error; + missing.unshift(basename(ancestor)); + ancestor = parent; + } + } + if (!missing.length) throw new Error('Output directory must be new.'); + const physicalAncestor = await realpath(ancestor); + if (!(await stat(physicalAncestor)).isDirectory()) throw new Error('Output path must descend from an existing directory.'); + const output = resolve(physicalAncestor, ...missing); + const checkout = await realpath(checkoutRoot); + const rel = relative(checkout, output); + if (!rel || (!rel.startsWith('..' + sep) && rel !== '..' && !isAbsolute(rel))) { + throw new Error('Output must be outside the checkout.'); + } + return output; +} + export async function buildContractRelease(destination, catalogName = 'prompt-sdk-v1', sourceIdentity = {}) { if (!['prompt-sdk-v1', 'context-builder-v1', 'comic-manifest-v1'].includes(catalogName)) throw new Error('Unknown release catalog.'); - if (!destination) throw new Error('Provide a new output directory outside the checkout.'); - const output = resolve(destination), rel = relative(root, output); - if (!rel || (!rel.startsWith('..' + '/') && !isAbsolute(rel))) throw new Error('Output must be outside the checkout.'); + const output = await resolveExternalOutput(destination); if (git('status', '--porcelain').length) throw new Error('Commit or remove working-tree changes before building.'); const localCommit = git('rev-parse', 'HEAD').toString().trim(); const localTree = git('rev-parse', localCommit + '^{tree}').toString().trim(); const hasRemoteIdentity = sourceIdentity?.commit !== undefined || sourceIdentity?.tree !== undefined; - if (hasRemoteIdentity && (!/^[a-f0-9]{40}$/.test(sourceIdentity.commit ?? '') || - !/^[a-f0-9]{40}$/.test(sourceIdentity.tree ?? '') || sourceIdentity.tree !== localTree)) { - throw new Error('Supplied source commit must be paired with its exact matching Git tree.'); + if (hasRemoteIdentity) { + if (!/^[a-f0-9]{40}$/.test(sourceIdentity.commit ?? '') || !/^[a-f0-9]{40}$/.test(sourceIdentity.tree ?? '')) { + throw new Error('Supplied source identity must use full Git commit and tree IDs.'); + } + let verifiedTree; + try { + if (git('cat-file', '-t', sourceIdentity.commit).toString().trim() !== 'commit') throw new Error('not a commit object'); + verifiedTree = git('rev-parse', sourceIdentity.commit + '^{tree}').toString().trim(); + } + catch { throw new Error('Supplied source commit is not present as a verified local Git commit object.'); } + if (verifiedTree !== sourceIdentity.tree || verifiedTree !== localTree) { + throw new Error('Supplied source commit must resolve to the exact matching local Git tree.'); + } } const commit = hasRemoteIdentity ? sourceIdentity.commit : localCommit; const catalog = JSON.parse(git('show', localCommit + ':releases/' + catalogName + '.json')); diff --git a/tests/contract-release.test.mjs b/tests/contract-release.test.mjs index cde3009..58732cb 100644 --- a/tests/contract-release.test.mjs +++ b/tests/contract-release.test.mjs @@ -1,11 +1,11 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises'; +import { mkdtemp, readdir, readFile, rm, symlink } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createHash } from 'node:crypto'; import { execFileSync } from 'node:child_process'; -import { buildContractRelease } from '../scripts/build-contract-release.mjs'; +import { buildContractRelease, resolveExternalOutput } from '../scripts/build-contract-release.mjs'; const hash = bytes => 'sha256:' + createHash('sha256').update(bytes).digest('hex'); test('contract release bundles are reproducible and every file identity verifies', async t => { const root = await mkdtemp(join(tmpdir(), 'codex-release-test-')); @@ -40,7 +40,7 @@ test('contract release bundles are reproducible and every file identity verifies }); test('release builder requires an explicit external destination', async () => { await assert.rejects(buildContractRelease(), /Provide a new output/); - await assert.rejects(buildContractRelease(new URL('../', import.meta.url).pathname), /outside/); + await assert.rejects(buildContractRelease(new URL('../', import.meta.url).pathname), /new|outside/); await assert.rejects(buildContractRelease('/tmp/unused', '../untrusted'), /Unknown release catalog/); }); @@ -107,8 +107,20 @@ test('release builder accepts an explicit source commit only for an exact matchi const root = await mkdtemp(join(tmpdir(), 'codex-release-source-identity-')); t.after(() => rm(root, { recursive: true, force: true })); const tree = execFileSync('git', ['rev-parse', 'HEAD^{tree}'], { cwd: new URL('../', import.meta.url) }).toString().trim(); - const remoteCommit = 'a'.repeat(40); - const [manifest] = await buildContractRelease(join(root, 'matching'), 'comic-manifest-v1', { commit: remoteCommit, tree }); - assert.equal(manifest.commit, remoteCommit); - await assert.rejects(buildContractRelease(join(root, 'mismatch'), 'comic-manifest-v1', { commit: remoteCommit, tree: 'b'.repeat(40) }), /exact matching Git tree/); + const verifiedCommit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: new URL('../', import.meta.url) }).toString().trim(); + const wrongCommit = execFileSync('git', ['rev-parse', 'HEAD^'], { cwd: new URL('../', import.meta.url) }).toString().trim(); + const [manifest] = await buildContractRelease(join(root, 'matching'), 'comic-manifest-v1', { commit: verifiedCommit, tree }); + assert.equal(manifest.commit, verifiedCommit); + await assert.rejects(buildContractRelease(join(root, 'wrong-tree'), 'comic-manifest-v1', { commit: verifiedCommit, tree: 'b'.repeat(40) }), /exact matching local Git tree/); + await assert.rejects(buildContractRelease(join(root, 'missing-commit'), 'comic-manifest-v1', { commit: 'a'.repeat(40), tree }), /not present as a verified local Git commit object/); + await assert.rejects(buildContractRelease(join(root, 'wrong-commit'), 'comic-manifest-v1', { commit: wrongCommit, tree }), /exact matching local Git tree/); +}); + +test('release output resolution rejects symlinked parents into the checkout', async t => { + const temp = await mkdtemp(join(tmpdir(), 'codex-release-output-')); + t.after(() => rm(temp, { recursive: true, force: true })); + const checkout = new URL('../', import.meta.url).pathname; + const alias = join(temp, 'checkout-alias'); + await symlink(checkout, alias, 'dir'); + await assert.rejects(resolveExternalOutput(join(alias, 'artifacts')), /outside the checkout/); }); From 578585d6a867bfbe71d7e2d2436cc8ed466d7946 Mon Sep 17 00:00:00 2001 From: Andrew Peris Date: Wed, 7 Oct 2026 16:44:23 -0400 Subject: [PATCH 5/8] Update release builder output regression expectation --- tests/contract-release.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/contract-release.test.mjs b/tests/contract-release.test.mjs index 58732cb..b619fa0 100644 --- a/tests/contract-release.test.mjs +++ b/tests/contract-release.test.mjs @@ -36,7 +36,7 @@ test('contract release bundles are reproducible and every file identity verifies } } } - await assert.rejects(buildContractRelease(first), /EEXIST/); + await assert.rejects(buildContractRelease(first), /new/); }); test('release builder requires an explicit external destination', async () => { await assert.rejects(buildContractRelease(), /Provide a new output/); From 36fd0a90230e4e8166f67248ba97b8fb6440d024 Mon Sep 17 00:00:00 2001 From: Andrew Peris Date: Wed, 7 Oct 2026 16:47:21 -0400 Subject: [PATCH 6/8] Cover unverified release commit identities --- tests/contract-release.test.mjs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/contract-release.test.mjs b/tests/contract-release.test.mjs index b619fa0..8e5dc01 100644 --- a/tests/contract-release.test.mjs +++ b/tests/contract-release.test.mjs @@ -106,13 +106,20 @@ test('Comic Manifest has a separate deterministic one-contract catalog', async t test('release builder accepts an explicit source commit only for an exact matching tree', async t => { const root = await mkdtemp(join(tmpdir(), 'codex-release-source-identity-')); t.after(() => rm(root, { recursive: true, force: true })); - const tree = execFileSync('git', ['rev-parse', 'HEAD^{tree}'], { cwd: new URL('../', import.meta.url) }).toString().trim(); - const verifiedCommit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: new URL('../', import.meta.url) }).toString().trim(); - const wrongCommit = execFileSync('git', ['rev-parse', 'HEAD^'], { cwd: new URL('../', import.meta.url) }).toString().trim(); + const checkout = new URL('../', import.meta.url); + const tree = execFileSync('git', ['rev-parse', 'HEAD^{tree}'], { cwd: checkout }).toString().trim(); + const verifiedCommit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: checkout }).toString().trim(); + const wrongTree = execFileSync('git', ['mktree'], { cwd: checkout, input: '' }).toString().trim(); + const wrongCommit = execFileSync('git', ['commit-tree', wrongTree, '-m', 'release identity regression fixture'], { + cwd: checkout, encoding: 'utf8', + env: { ...process.env, GIT_AUTHOR_NAME: 'Release Fixture', GIT_AUTHOR_EMAIL: 'fixture@example.invalid', + GIT_COMMITTER_NAME: 'Release Fixture', GIT_COMMITTER_EMAIL: 'fixture@example.invalid' } + }).trim(); const [manifest] = await buildContractRelease(join(root, 'matching'), 'comic-manifest-v1', { commit: verifiedCommit, tree }); assert.equal(manifest.commit, verifiedCommit); await assert.rejects(buildContractRelease(join(root, 'wrong-tree'), 'comic-manifest-v1', { commit: verifiedCommit, tree: 'b'.repeat(40) }), /exact matching local Git tree/); - await assert.rejects(buildContractRelease(join(root, 'missing-commit'), 'comic-manifest-v1', { commit: 'a'.repeat(40), tree }), /not present as a verified local Git commit object/); + await assert.rejects(buildContractRelease(join(root, 'non-commit-object'), 'comic-manifest-v1', { commit: tree, tree }), /not present as a verified local Git commit object/); + await assert.rejects(buildContractRelease(join(root, 'missing-commit'), 'comic-manifest-v1', { commit: '0'.repeat(40), tree }), /not present as a verified local Git commit object/); await assert.rejects(buildContractRelease(join(root, 'wrong-commit'), 'comic-manifest-v1', { commit: wrongCommit, tree }), /exact matching local Git tree/); }); From 1bee0b483ec50c5c79e23272ab5932ac5e8f45bf Mon Sep 17 00:00:00 2001 From: Andrew Peris Date: Wed, 7 Oct 2026 17:14:14 -0400 Subject: [PATCH 7/8] Ignore Git replacement refs in contract release provenance --- releases/comic-manifest-v1.md | 10 +++++--- scripts/build-contract-release.mjs | 41 ++++++++++++++++++------------ tests/contract-release.test.mjs | 26 ++++++++++++++++++- 3 files changed, 57 insertions(+), 20 deletions(-) diff --git a/releases/comic-manifest-v1.md b/releases/comic-manifest-v1.md index ab2527a..5544d3e 100644 --- a/releases/comic-manifest-v1.md +++ b/releases/comic-manifest-v1.md @@ -58,9 +58,13 @@ unchanged. public commit without tags) and confirm the GitHub commit API's tree. The builder independently resolves the supplied commit object's tree, requires it to match both the supplied tree and the clean local `HEAD` tree, then - reads all bundled files from that local tree. A commit-shaped caller string - without a verified local commit object fails closed. A normal merged - checkout uses `HEAD` and needs no override. + reads all bundled files from that local tree. Every builder Git invocation + sets `GIT_NO_REPLACE_OBJECTS=1` for object checks and source reads, without + changing global Git configuration. Regression tests create isolated replace + refs and prove they cannot change either accepted or rejected commit/tree + relationships. A commit-shaped caller string without a verified local + commit object fails closed. A normal merged checkout uses `HEAD` and needs + no override. 4. Compare every filename and byte across both directories. Each directory has exactly these three expected outputs: diff --git a/scripts/build-contract-release.mjs b/scripts/build-contract-release.mjs index 85bf200..f70e62b 100644 --- a/scripts/build-contract-release.mjs +++ b/scripts/build-contract-release.mjs @@ -4,9 +4,32 @@ import { createHash } from 'node:crypto'; import { resolve, relative, isAbsolute, sep, dirname, basename } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; const root = fileURLToPath(new URL('../', import.meta.url)); -const git = (...args) => execFileSync('git', args, { cwd: root, maxBuffer: 16 * 1024 * 1024 }); +const gitAt = cwd => (...args) => execFileSync('git', args, { + cwd, env: { ...process.env, GIT_NO_REPLACE_OBJECTS: '1' }, maxBuffer: 16 * 1024 * 1024 +}); +const git = gitAt(root); const sha = bytes => 'sha256:' + createHash('sha256').update(bytes).digest('hex'); +export function verifySourceIdentity(sourceIdentity, localTree, checkoutRoot = root) { + const hasRemoteIdentity = sourceIdentity?.commit !== undefined || sourceIdentity?.tree !== undefined; + if (!hasRemoteIdentity) return false; + if (!/^[a-f0-9]{40}$/.test(sourceIdentity.commit ?? '') || !/^[a-f0-9]{40}$/.test(sourceIdentity.tree ?? '')) { + throw new Error('Supplied source identity must use full Git commit and tree IDs.'); + } + const checkoutGit = gitAt(checkoutRoot); + let verifiedTree; + try { + if (checkoutGit('cat-file', '-t', sourceIdentity.commit).toString().trim() !== 'commit') throw new Error('not a commit object'); + verifiedTree = checkoutGit('rev-parse', sourceIdentity.commit + '^{tree}').toString().trim(); + } catch { + throw new Error('Supplied source commit is not present as a verified local Git commit object.'); + } + if (verifiedTree !== sourceIdentity.tree || verifiedTree !== localTree) { + throw new Error('Supplied source commit must resolve to the exact matching local Git tree.'); + } + return true; +} + export async function resolveExternalOutput(destination, checkoutRoot = root) { if (!destination) throw new Error('Provide a new output directory outside the checkout.'); const requested = resolve(destination), missing = []; @@ -39,21 +62,7 @@ export async function buildContractRelease(destination, catalogName = 'prompt-sd if (git('status', '--porcelain').length) throw new Error('Commit or remove working-tree changes before building.'); const localCommit = git('rev-parse', 'HEAD').toString().trim(); const localTree = git('rev-parse', localCommit + '^{tree}').toString().trim(); - const hasRemoteIdentity = sourceIdentity?.commit !== undefined || sourceIdentity?.tree !== undefined; - if (hasRemoteIdentity) { - if (!/^[a-f0-9]{40}$/.test(sourceIdentity.commit ?? '') || !/^[a-f0-9]{40}$/.test(sourceIdentity.tree ?? '')) { - throw new Error('Supplied source identity must use full Git commit and tree IDs.'); - } - let verifiedTree; - try { - if (git('cat-file', '-t', sourceIdentity.commit).toString().trim() !== 'commit') throw new Error('not a commit object'); - verifiedTree = git('rev-parse', sourceIdentity.commit + '^{tree}').toString().trim(); - } - catch { throw new Error('Supplied source commit is not present as a verified local Git commit object.'); } - if (verifiedTree !== sourceIdentity.tree || verifiedTree !== localTree) { - throw new Error('Supplied source commit must resolve to the exact matching local Git tree.'); - } - } + const hasRemoteIdentity = verifySourceIdentity(sourceIdentity, localTree); const commit = hasRemoteIdentity ? sourceIdentity.commit : localCommit; const catalog = JSON.parse(git('show', localCommit + ':releases/' + catalogName + '.json')); const files = git('ls-tree', '-r', '--name-only', '-z', localCommit).toString().split('\0').filter(Boolean).sort(); diff --git a/tests/contract-release.test.mjs b/tests/contract-release.test.mjs index 8e5dc01..2830bf4 100644 --- a/tests/contract-release.test.mjs +++ b/tests/contract-release.test.mjs @@ -5,7 +5,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createHash } from 'node:crypto'; import { execFileSync } from 'node:child_process'; -import { buildContractRelease, resolveExternalOutput } from '../scripts/build-contract-release.mjs'; +import { buildContractRelease, resolveExternalOutput, verifySourceIdentity } from '../scripts/build-contract-release.mjs'; const hash = bytes => 'sha256:' + createHash('sha256').update(bytes).digest('hex'); test('contract release bundles are reproducible and every file identity verifies', async t => { const root = await mkdtemp(join(tmpdir(), 'codex-release-test-')); @@ -123,6 +123,30 @@ test('release builder accepts an explicit source commit only for an exact matchi await assert.rejects(buildContractRelease(join(root, 'wrong-commit'), 'comic-manifest-v1', { commit: wrongCommit, tree }), /exact matching local Git tree/); }); +test('release source provenance ignores Git replacement refs in an isolated repository', async t => { + const temp = await mkdtemp(join(tmpdir(), 'codex-release-replace-ref-')); + t.after(() => rm(temp, { recursive: true, force: true })); + const isolated = join(temp, 'repo'); + const checkout = new URL('../', import.meta.url).pathname; + execFileSync('git', ['clone', '--local', '--no-hardlinks', checkout, isolated], { stdio: 'ignore' }); + const git = (...args) => execFileSync('git', args, { cwd: isolated, encoding: 'utf8' }).trim(); + const sourceCommit = git('rev-parse', 'HEAD'); + const tree = git('rev-parse', 'HEAD^{tree}'); + const emptyTree = execFileSync('git', ['mktree'], { cwd: isolated, encoding: 'utf8', input: '' }).trim(); + const replacementCommit = execFileSync('git', [ + '-c', 'user.name=Release Fixture', '-c', 'user.email=fixture@example.invalid', + 'commit-tree', emptyTree, '-p', sourceCommit, '-m', 'replacement-ref provenance fixture' + ], { cwd: isolated, encoding: 'utf8' }).trim(); + + git('replace', sourceCommit, replacementCommit); + assert.equal(verifySourceIdentity({ commit: sourceCommit, tree }, tree, isolated), true, + 'a replacement ref cannot change the verified tree of a valid source commit'); + git('replace', '-d', sourceCommit); + git('replace', replacementCommit, sourceCommit); + assert.throws(() => verifySourceIdentity({ commit: replacementCommit, tree }, tree, isolated), /exact matching local Git tree/, + 'a replacement ref cannot make a wrong commit appear to contain the checkout tree'); +}); + test('release output resolution rejects symlinked parents into the checkout', async t => { const temp = await mkdtemp(join(tmpdir(), 'codex-release-output-')); t.after(() => rm(temp, { recursive: true, force: true })); From 028d5638e20d9283a2955aacbd38e1bfc6dca259 Mon Sep 17 00:00:00 2001 From: Andrew Peris Date: Wed, 7 Oct 2026 17:17:53 -0400 Subject: [PATCH 8/8] Read contract release assets from verified source commit --- releases/comic-manifest-v1.md | 2 +- scripts/build-contract-release.mjs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/releases/comic-manifest-v1.md b/releases/comic-manifest-v1.md index 5544d3e..2dd697e 100644 --- a/releases/comic-manifest-v1.md +++ b/releases/comic-manifest-v1.md @@ -58,7 +58,7 @@ unchanged. public commit without tags) and confirm the GitHub commit API's tree. The builder independently resolves the supplied commit object's tree, requires it to match both the supplied tree and the clean local `HEAD` tree, then - reads all bundled files from that local tree. Every builder Git invocation + reads all bundled files from the verified source commit. Every builder Git invocation sets `GIT_NO_REPLACE_OBJECTS=1` for object checks and source reads, without changing global Git configuration. Regression tests create isolated replace refs and prove they cannot change either accepted or rejected commit/tree diff --git a/scripts/build-contract-release.mjs b/scripts/build-contract-release.mjs index f70e62b..818f148 100644 --- a/scripts/build-contract-release.mjs +++ b/scripts/build-contract-release.mjs @@ -64,10 +64,10 @@ export async function buildContractRelease(destination, catalogName = 'prompt-sd const localTree = git('rev-parse', localCommit + '^{tree}').toString().trim(); const hasRemoteIdentity = verifySourceIdentity(sourceIdentity, localTree); const commit = hasRemoteIdentity ? sourceIdentity.commit : localCommit; - const catalog = JSON.parse(git('show', localCommit + ':releases/' + catalogName + '.json')); - const files = git('ls-tree', '-r', '--name-only', '-z', localCommit).toString().split('\0').filter(Boolean).sort(); + const catalog = JSON.parse(git('show', commit + ':releases/' + catalogName + '.json')); + const files = git('ls-tree', '-r', '--name-only', '-z', commit).toString().split('\0').filter(Boolean).sort(); const entries = files.map(path => { - const bytes = git('show', localCommit + ':' + path); + const bytes = git('show', commit + ':' + path); return { path, byte_size: bytes.length, sha256: sha(bytes), content_base64: bytes.toString('base64') }; }); // Includes all reviewed tracked sources so relative documentation links, @@ -77,7 +77,7 @@ export async function buildContractRelease(destination, catalogName = 'prompt-sd for (const contract of catalog.contracts) { const tag = 'contract/' + contract.name + '/v' + catalog.version; const prefix = contract.name + '-v' + catalog.version; - const schema = git('show', localCommit + ':' + contract.schema); + const schema = git('show', commit + ':' + contract.schema); const schemaId = JSON.parse(schema).$id; if (!schemaId.startsWith('urn:definitely-secure:contract:' + contract.name + ':' + catalog.version + ':')) throw new Error('Schema identifier does not match catalog.'); const bundle = Buffer.from(JSON.stringify({ format: 'studio-contract-source-bundle-v1', repository: catalog.repository, contract: contract.name, version: catalog.version, commit, files: entries }, null, 2) + '\n');