From ff454ee7b59c4e94f40cfadd43b343f5ccf3145b Mon Sep 17 00:00:00 2001 From: Andrew Peris Date: Tue, 6 Oct 2026 13:33:38 -0400 Subject: [PATCH 1/3] feat(codex): specify comic rendition profiles --- CHANGELOG.md | 7 + fixtures/comic-manifest-v1-cases.json | 202 ++++++++++++++++++ fixtures/comic-manifest-v1.json | 63 +++--- fixtures/comic-manifest-v1.md | 19 +- .../0008-comic-manifest-rendition-profiles.md | 145 +++++++++++++ schemas/json/comic-manifest/v1/README.md | 3 + .../v1/comic-manifest.schema.json | 50 +++++ specs/manifests/COMIC-MANIFEST-VERSIONING.md | 7 + specs/manifests/comic-manifest-v1.md | 67 ++++-- tests/comic-manifest.test.cjs | 96 +++++++++ tests/support/comic-manifest-oracle.cjs | 25 ++- 11 files changed, 640 insertions(+), 44 deletions(-) create mode 100644 rfcs/0008-comic-manifest-rendition-profiles.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 242b628..3939e0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,13 @@ compatible, or editorial. ## Unreleased +- Extended the unreleased Comic Manifest 1.0.0 candidate for Studio #94 with + immutable versioned output profiles, per-output limits, exact requirement/result/ + release metadata matching and explicit rights/accessibility/publication boundaries. + RFC 0008 and synthetic passing/failing fixtures accompany the proposed change. + This supersedes only the prior unreleased candidate source; released SDK, Context + Package and Context Builder bytes remain unchanged. + - Added Comic Manifest 1.0.0 (new additive, unreleased family) for Studio #89: RFC 0007, three closed record schemas, detached approval bindings, normative identity/privacy/compatibility semantics and offline synthetic conformance. diff --git a/fixtures/comic-manifest-v1-cases.json b/fixtures/comic-manifest-v1-cases.json index 9ebd326..1e98269 100644 --- a/fixtures/comic-manifest-v1-cases.json +++ b/fixtures/comic-manifest-v1-cases.json @@ -593,6 +593,208 @@ "code": "OUTPUT_REQUIREMENT", "rebind": true }, + { + "name": "unknown rendition profile id", + "layer": "schema", + "edits": [ + { + "path": [ + "production", + "renditions", + 0, + "profile", + "profile_id" + ], + "value": "comic-page-image-v2" + } + ] + }, + { + "name": "result transcript must match requirement", + "layer": "relational", + "edits": [ + { + "path": [ + "result", + "outputs", + 0, + "transcript" + ], + "value": "A different synthetic transcript." + } + ], + "code": "OUTPUT_REQUIREMENT", + "rebind": true + }, + { + "name": "rendition profile is required", + "layer": "schema", + "edits": [ + { + "path": [ + "production", + "renditions", + 0, + "profile" + ], + "remove": true + } + ] + }, + { + "name": "unknown rendition profile version", + "layer": "schema", + "edits": [ + { + "path": [ + "production", + "renditions", + 0, + "profile", + "profile_version" + ], + "value": "2.0.0" + } + ] + }, + { + "name": "transcript profile rejects another media type", + "layer": "relational", + "edits": [ + { + "path": [ + "production", + "renditions", + 0, + "media_type" + ], + "value": "application/pdf" + } + ], + "code": "RENDITION_MEDIA", + "rebind": true + }, + { + "name": "raster profile rejects dimensions over pixel limit", + "layer": "relational", + "edits": [ + { + "path": [ + "production", + "renditions", + 0, + "profile", + "profile_id" + ], + "value": "comic-page-image" + }, + { + "path": [ + "production", + "renditions", + 0, + "media_type" + ], + "value": "image/png" + }, + { + "path": [ + "production", + "renditions", + 0, + "dimensions" + ], + "value": { + "width": 8192, + "height": 4097 + } + } + ], + "code": "RENDITION_DIMENSIONS", + "rebind": true + }, + { + "name": "transcript profile cap cannot be raised", + "layer": "relational", + "edits": [ + { + "path": [ + "production", + "renditions", + 0, + "max_bytes" + ], + "value": 131073 + } + ], + "code": "RENDITION_LIMIT", + "rebind": true + }, + { + "name": "result cannot raise the declared output cap", + "layer": "relational", + "edits": [ + { + "path": [ + "production", + "renditions", + 0, + "max_bytes" + ], + "value": 35 + }, + { + "path": [ + "result", + "outputs", + 0, + "max_bytes" + ], + "value": 35 + }, + { + "path": [ + "release", + "outputs", + 0, + "max_bytes" + ], + "value": 35 + } + ], + "code": "OUTPUT_LIMIT", + "rebind": true + }, + { + "name": "result accessibility description must match", + "layer": "relational", + "edits": [ + { + "path": [ + "result", + "outputs", + 0, + "alt_text" + ], + "value": "A different synthetic description." + } + ], + "code": "OUTPUT_REQUIREMENT", + "rebind": true + }, + { + "name": "public release requires exact studio credit", + "layer": "schema", + "edits": [ + { + "path": [ + "release", + "production_credit" + ], + "value": "A different production." + } + ] + }, { "name": "package substitution", "layer": "relational", diff --git a/fixtures/comic-manifest-v1.json b/fixtures/comic-manifest-v1.json index 2f03519..717cf73 100644 --- a/fixtures/comic-manifest-v1.json +++ b/fixtures/comic-manifest-v1.json @@ -137,7 +137,12 @@ "dimensions": null, "alt_text": "A blue cube, followed by a red sphere.", "transcript": "Panel 1: A cube.\nPanel 2: A sphere.", - "rights_notice": "Synthetic conformance fixture; Apache-2.0." + "rights_notice": "Synthetic conformance fixture; Apache-2.0.", + "profile": { + "profile_id": "comic-accessible-transcript", + "profile_version": "1.0.0" + }, + "max_bytes": 131072 } ] }, @@ -151,8 +156,8 @@ "revision": 1, "identity": { "canonicalization": "studio-json-v1", - "byte_size": 3359, - "sha256": "sha256:9447b652a4ab121bf5355e5faa21a224b2d6d4259d403c005580b757511d1e19" + "byte_size": 3459, + "sha256": "sha256:9250217ad23125b0356a9e7002f57a55dacf9d982f069caa403b89f8c6c234dc" } }, "classification": "internal", @@ -258,7 +263,12 @@ "dimensions": null, "rights_notice": "Synthetic conformance fixture; Apache-2.0.", "alt_text": "A blue cube, followed by a red sphere.", - "transcript": "Panel 1: A cube.\nPanel 2: A sphere." + "transcript": "Panel 1: A cube.\nPanel 2: A sphere.", + "profile": { + "profile_id": "comic-accessible-transcript", + "profile_version": "1.0.0" + }, + "max_bytes": 131072 } ], "gates": [ @@ -385,7 +395,12 @@ "dimensions": null, "rights_notice": "Synthetic conformance fixture; Apache-2.0.", "alt_text": "A blue cube, followed by a red sphere.", - "transcript": "Panel 1: A cube.\nPanel 2: A sphere." + "transcript": "Panel 1: A cube.\nPanel 2: A sphere.", + "profile": { + "profile_id": "comic-accessible-transcript", + "profile_version": "1.0.0" + }, + "max_bytes": 131072 } ], "gates": [ @@ -513,8 +528,8 @@ "decided_at": "2026-09-15T12:02:00Z", "subject": { "canonicalization": "studio-json-v1", - "byte_size": 3359, - "sha256": "sha256:9447b652a4ab121bf5355e5faa21a224b2d6d4259d403c005580b757511d1e19" + "byte_size": 3459, + "sha256": "sha256:9250217ad23125b0356a9e7002f57a55dacf9d982f069caa403b89f8c6c234dc" }, "artifact_digests": [], "scope": { @@ -535,8 +550,8 @@ "decided_at": "2026-09-15T12:02:00Z", "subject": { "canonicalization": "studio-json-v1", - "byte_size": 5269, - "sha256": "sha256:1d2b5e1bfc49f40ceea1662b04d7380735b82f730f55a61cc71f5a6696b750c2" + "byte_size": 5369, + "sha256": "sha256:fe77969b47aa6b5dde423c9ae13075b082974f375bd9a71502b09f89859bc8bf" }, "artifact_digests": [ "sha256:5aa128a0dd0945b00ba3d71a5f7b5916e06f40c53048c31d74432eb03f4e764d" @@ -559,8 +574,8 @@ "decided_at": "2026-09-15T12:02:00Z", "subject": { "canonicalization": "studio-json-v1", - "byte_size": 5269, - "sha256": "sha256:1d2b5e1bfc49f40ceea1662b04d7380735b82f730f55a61cc71f5a6696b750c2" + "byte_size": 5369, + "sha256": "sha256:fe77969b47aa6b5dde423c9ae13075b082974f375bd9a71502b09f89859bc8bf" }, "artifact_digests": [ "sha256:5aa128a0dd0945b00ba3d71a5f7b5916e06f40c53048c31d74432eb03f4e764d" @@ -583,8 +598,8 @@ "decided_at": "2026-09-15T12:02:00Z", "subject": { "canonicalization": "studio-json-v1", - "byte_size": 5269, - "sha256": "sha256:1d2b5e1bfc49f40ceea1662b04d7380735b82f730f55a61cc71f5a6696b750c2" + "byte_size": 5369, + "sha256": "sha256:fe77969b47aa6b5dde423c9ae13075b082974f375bd9a71502b09f89859bc8bf" }, "artifact_digests": [ "sha256:5aa128a0dd0945b00ba3d71a5f7b5916e06f40c53048c31d74432eb03f4e764d" @@ -605,19 +620,19 @@ "revision": 1, "identity": { "canonicalization": "studio-json-v1", - "byte_size": 3359, - "sha256": "sha256:9447b652a4ab121bf5355e5faa21a224b2d6d4259d403c005580b757511d1e19" + "byte_size": 3459, + "sha256": "sha256:9250217ad23125b0356a9e7002f57a55dacf9d982f069caa403b89f8c6c234dc" } }, "result_identity": { "canonicalization": "studio-json-v1", - "byte_size": 4465, - "sha256": "sha256:871cb595c8d3b44d6245fd9c86daf9aa9185b76081f918b5a44c4568062f1dcf" + "byte_size": 4565, + "sha256": "sha256:5d0026491f8400ac9bbb1c5919065c97d3d0aab33c5458d1e454124ca429157a" }, "release_identity": { "canonicalization": "studio-json-v1", - "byte_size": 5269, - "sha256": "sha256:1d2b5e1bfc49f40ceea1662b04d7380735b82f730f55a61cc71f5a6696b750c2" + "byte_size": 5369, + "sha256": "sha256:fe77969b47aa6b5dde423c9ae13075b082974f375bd9a71502b09f89859bc8bf" }, "attestation_reference": "urn:uuid:10000000-0000-4000-8000-000000000300" }, @@ -638,19 +653,19 @@ "approvals": [ { "decision_id": "urn:uuid:10000000-0000-4000-8000-000000000199", - "sha256": "sha256:21cf0df439e7d871bb1af47e40b9a8b65e87c3d0e0d1b347612cc5a6c3ec09d5" + "sha256": "sha256:01dbf5f12ff893334d732144d5a0e413b9edbe7d30e3c3e5e8736626192bf7f9" }, { "decision_id": "urn:uuid:10000000-0000-4000-8000-000000000200", - "sha256": "sha256:9b08c55b7da322ba912971c7a04c66518b62972acfc9a01e76e52c765f397603" + "sha256": "sha256:7a26286430ffa21e15575dbf410eacd7e3a5c3c506c334d26178274e5e87ca64" }, { "decision_id": "urn:uuid:10000000-0000-4000-8000-000000000201", - "sha256": "sha256:ec2b4192b3c31f50be3ec5628a97d181090530d30baa1fe56c5a4295c67740c8" + "sha256": "sha256:344605674729c54422a344a1d46de39854fcc75ae10f55fb0338b1d053c04f8a" }, { "decision_id": "urn:uuid:10000000-0000-4000-8000-000000000202", - "sha256": "sha256:310efb68d2464edebedc0505456c402e04770c26e3f782c1d89104a7b2fffc94" + "sha256": "sha256:1f598d171b62d79047dbfb7b1ac7813695b3f3a7c2ef732e51b83cf0192228d5" } ], "revoked": [], @@ -664,7 +679,7 @@ "linkage_identity": { "canonicalization": "studio-json-v1", "byte_size": 627, - "sha256": "sha256:0fa52c096dcc5e041979fd5887bf0938abc0a486b4b659c19b6414ae43b43e0b" + "sha256": "sha256:c8bf79ebeb5a52ef933fb779c7dc8ad388341da053ee653a5fc3c54e845a3829" } } } diff --git a/fixtures/comic-manifest-v1.md b/fixtures/comic-manifest-v1.md index d516ff5..d75d5cf 100644 --- a/fixtures/comic-manifest-v1.md +++ b/fixtures/comic-manifest-v1.md @@ -17,11 +17,16 @@ real edits preserve or renew approvals. `rebind: false` retains historical evide to test invalidation. See the [test-only oracle](../tests/support/comic-manifest-oracle.cjs). The suite includes protected-context and authored public-only success, append-only -correction, partial/failed evidence, deterministic identity, unknown versions/fields, -ordered identities, outputs, immutable references, package bindings, stale/revoked/ -forged approvals, public gates and private-context projection. Output text bytes and -public dependency bytes are explicit fixture inputs and independently hashed. -Tests never create an approval from a production runtime or execute a provider. +correction, partial/failed evidence, deterministic identity, exact versioned output +profiles, inclusive pixel/byte boundaries, profile/media incompatibility, missing, +duplicate and undeclared outputs, accessibility/rights metadata mismatch, unknown +versions/fields, ordered identities, immutable references, package bindings, +stale/revoked/forged approvals, public gates and private-context projection. Tests +compose non-rendered image/PDF placeholder bytes to exercise declared profile +relationships; they deliberately do not claim media signatures or rendering. +Output text bytes and public dependency bytes are explicit fixture inputs and +independently hashed. Tests never create an approval from a production runtime or +execute a provider. Run `npm ci --ignore-scripts`, then `npm run test:comic`. Run `npm test` from a clean committed tree to include existing release-bundle reproducibility tests (the release @@ -30,7 +35,9 @@ the lockfile. Neither command needs network after dependency installation. Coverage limits are explicit: the oracle exercises fixture relationships, not a hardened parser, production trust verifier, media decoder, issuer, secure store or -public projection implementation. Current package use is tested by the companion +public projection implementation. A rights notice is metadata, not evidence of +ownership or permission; text presence does not prove accessibility quality or +disclosure safety. Current package use is tested by the companion Platform proof using the released SDK. Unicode/duplicate-key/size/depth raw parsing, real revocation adapters, denied-read guarantees, free-text disclosure and artifact rights need their later runtime and qualified human checks. Passing fixtures alone diff --git a/rfcs/0008-comic-manifest-rendition-profiles.md b/rfcs/0008-comic-manifest-rendition-profiles.md new file mode 100644 index 0000000..1d92378 --- /dev/null +++ b/rfcs/0008-comic-manifest-rendition-profiles.md @@ -0,0 +1,145 @@ +# RFC 0008: Comic Manifest rendition profiles + +- Status: Draft +- Authors: @andrewperis +- Created: 2026-10-06 +- Contract families: Comic Manifest +- Compatibility: Breaking to the unreleased 1.0.0 candidate; no released consumer +- Supersedes: None +- Superseded by: None +- Constitution version: 1.0.0 +- Constitution tag: `constitution/v1.0.0` +- Constitution commit: `a9cc8a503aa30e17820edc62ac95f7cbe10e0564` +- Applicable checklist profile: ADR, RFC, and stable specification +- Conformance evidence: [Studio #94](https://github.com/DefinitelySecureStudio/studio/issues/94) +- Decision owner and required A4 reviewers: @andrewperis; Codex and Platform maintainers + +## Summary + +Complete the rendition requirements for the unreleased Comic Manifest 1.0.0 +candidate. Each declared output will use an exact versioned profile with explicit +media type, dimensions, pixel/byte limits and accessibility/rights metadata. +Build-result and public-release output records will repeat those requirements and +be checked for exact compatibility. This proposal does not alter any released +contract or authorize production use. + +## Motivation + +The #89 candidate already declares output media, dimensions, alternative text, +transcript and rights notice. It does not name an immutable output profile, place +a cap on an individual output, limit pixel count, or require profile metadata to be +preserved in build-result and public-release output records. A consumer therefore +cannot negotiate a known output contract or reject every oversized/incompatible +output from the declared records alone. + +The gap belongs in Codex because output profile semantics must be shared before +Platform implements them. Studio #94 also depends on #91 episode identity and #92 +immutable references; those contracts remain in force. + +## Specification + +The unreleased Comic Manifest 1.0.0 candidate will recognize these immutable +profile ID/version pairs: + +| Profile ID | Version | Permitted media type(s) | Dimensions | Maximum `max_bytes` | +| --- | --- | --- | --- | ---: | +| `comic-page-image` | `1.0.0` | `image/png`, `image/jpeg`, `image/webp` | Required exact width and height; each at most 8,192 and product at most 33,554,432 pixels | 50,000,000 | +| `comic-portable-document` | `1.0.0` | `application/pdf` | `null` | 50,000,000 | +| `comic-accessible-transcript` | `1.0.0` | `text/plain` | `null` | 131,072 | + +Every production rendition MUST explicitly carry `profile: {profile_id, +profile_version}`, one permitted `media_type`, exact target `dimensions` (or +`null`), and a positive `max_bytes` at or below its profile cap. No default, +alias, wildcard, `latest`, downgrade or fallback is permitted. Unknown profile +pairs and cross-profile media/dimension combinations MUST fail. A profile's +meaning and limits are immutable; a changed constraint requires a new profile +version and explicit consumer support. + +Every build-result and public-release output MUST repeat the exact profile pair, +declared `max_bytes`, dimensions and accessibility/rights metadata from its +production requirement. Its media type MUST be permitted by the profile; its +dimensions and metadata MUST match the requirement exactly; and the recorded +artifact byte size MUST NOT exceed `max_bytes`. A complete result MUST include +every required rendition exactly once, MAY omit optional renditions, and MUST NOT +include missing, duplicate or undeclared output IDs. The public release MUST +preserve the selected result's output order and exact public output metadata. + +`alt_text`, `transcript` and `rights_notice` MUST be nonempty. Alternative text is +a concise description of relevant visual information. A transcript preserves +ordered dialogue and captions. A rights notice states the intended rights notice +for the output. The exact standard `production_credit`, final title, Universe- +assigned `DS-NNNN` episode identity, release revision/predecessor, canon scope, +destination, audience, purpose and publication time remain required public +metadata. The opaque production UUID and public episode number are different +identities; the number MUST be assigned through Universe's explicit boundary and +MUST NOT be derived from private identity or content. + +Declarations are not proof. A supported profile, matching media-type string, +dimension value, size/digest, alt text, transcript, credit or rights notice does +not prove that the bytes decode/render correctly, that descriptions are useful, +that the visible work meets creative or accessibility quality, that credit is +present in the output, that anyone owns or has permission for the material, or +that publication is safe. Consumers verify bytes and actual dimensions. Qualified +humans review creative quality, accessibility, rights, disclosure and publication +under the applicable approval policy. No rendering, lettering, distribution, +rights decision or canon promotion is implemented here. + +## Compatibility and migration + +The change is breaking against the earlier unreleased #89 1.0.0 candidate because +the closed rendition/output records gain required fields. No released Comic +Manifest exists and no released contract is changed. The accepted candidate schema +will remain `spec_version: "1.0.0"`; the current source must be owner-accepted +before Platform adopts its exact new schema bytes. Existing #90–#93 development +consumers must update their explicit source/schema/fixture pins and validate exact +profile pairs before accepting the candidate. Unknown profile versions fail until +that exact pair is supported. + +No change or migration applies to Prompt SDK v1, Context Package v1, Context +Builder v1, or their immutable artifacts. Immutable Comic Manifest release and +production adoption remain separately gated by #99 and complete release review. + +## Security and privacy + +Profile checking is local and deterministic. It must not retrieve media, follow a +URI, select an alternate artifact or execute a renderer. Output free text, URLs, +filenames, alternative text and rights notices may disclose protected information; +public projection remains allowlisted and requires separate disclosure review. +Production and build-result identities remain protected when their records are +protected. Public numbering and metadata must not expose private identifiers, +source locations, hashes or content-derived fingerprints. + +## Rights, provenance, accessibility, and portability + +The profile catalog is provider-neutral and uses common media types with explicit +limits. The transcript and alternative-text fields make accessibility inputs +required; reviewer judgment about quality and the output itself remains necessary. +Rights notices communicate declared metadata only. Ownership, licensing, +permission, attribution and third-party rights are A4 human judgments under the +Constitution and are never inferred by schema or fixture success. + +## Alternatives considered + +- **One mutable `latest` profile:** rejected because consumers could silently + reinterpret old records after profile changes. +- **Let each manifest invent its own profile semantics:** rejected because a + record could raise its own limits or declare incompatible media without a + shared consumer contract. +- **Trust only result byte declarations:** rejected because they do not constrain + requested output geometry/size or establish media correctness. +- **Treat a rights notice or accessibility string as approval:** rejected because + data presence cannot replace qualified review or authority. + +## Implementation plan + +1. Owner-review and merge this normative Codex proposal and its closed schema, + specification, compatibility note and synthetic fixtures. +2. After that merge, Platform adopts the exact schema source, implements profile + and cross-record output checks, and adds passing/failing integration fixtures. +3. Keep release/tag publication, immutable artifact verification and production + use under the separate #99 gates. + +## Unresolved questions + +No unresolved schema choice is proposed. The exact limits above remain subject to +the decision owner's review before acceptance. diff --git a/schemas/json/comic-manifest/v1/README.md b/schemas/json/comic-manifest/v1/README.md index d3d69d2..84a2d70 100644 --- a/schemas/json/comic-manifest/v1/README.md +++ b/schemas/json/comic-manifest/v1/README.md @@ -7,5 +7,8 @@ Use its `$defs/approval` entry point for detached approval bindings. [Normative semantics](../../../../specs/manifests/comic-manifest-v1.md), [versioning](../../../../specs/manifests/COMIC-MANIFEST-VERSIONING.md) and [fixture guide](../../../../fixtures/comic-manifest-v1.md) are required companions. +The exact versioned rendition profiles and their media, dimensions and byte/pixel +limits are defined in the normative specification and enforced relationally by +consumer conformance checks. Schema validation is only the structural layer; it proves neither cross-record integrity nor authority. Unreleased candidate; production consumption waits for #99. diff --git a/schemas/json/comic-manifest/v1/comic-manifest.schema.json b/schemas/json/comic-manifest/v1/comic-manifest.schema.json index c340b02..fc36af0 100644 --- a/schemas/json/comic-manifest/v1/comic-manifest.schema.json +++ b/schemas/json/comic-manifest/v1/comic-manifest.schema.json @@ -507,14 +507,36 @@ } ] }, + "renditionProfile": { + "type": "object", + "additionalProperties": false, + "required": [ + "profile_id", + "profile_version" + ], + "properties": { + "profile_id": { + "enum": [ + "comic-page-image", + "comic-portable-document", + "comic-accessible-transcript" + ] + }, + "profile_version": { + "const": "1.0.0" + } + } + }, "rendition": { "type": "object", "additionalProperties": false, "required": [ "rendition_id", "required", + "profile", "media_type", "dimensions", + "max_bytes", "alt_text", "transcript", "rights_notice" @@ -526,6 +548,9 @@ "required": { "type": "boolean" }, + "profile": { + "$ref": "#/$defs/renditionProfile" + }, "media_type": { "enum": [ "image/png", @@ -538,6 +563,11 @@ "dimensions": { "$ref": "#/$defs/dimensions" }, + "max_bytes": { + "type": "integer", + "minimum": 1, + "maximum": 50000000 + }, "alt_text": { "type": "string", "minLength": 1, @@ -634,7 +664,9 @@ "required": [ "rendition_id", "artifact", + "profile", "dimensions", + "max_bytes", "rights_notice", "alt_text", "transcript" @@ -646,9 +678,17 @@ "artifact": { "$ref": "#/$defs/artifact" }, + "profile": { + "$ref": "#/$defs/renditionProfile" + }, "dimensions": { "$ref": "#/$defs/dimensions" }, + "max_bytes": { + "type": "integer", + "minimum": 1, + "maximum": 50000000 + }, "rights_notice": { "type": "string", "minLength": 1, @@ -672,7 +712,9 @@ "required": [ "rendition_id", "artifact", + "profile", "dimensions", + "max_bytes", "rights_notice", "alt_text", "transcript" @@ -714,9 +756,17 @@ } } }, + "profile": { + "$ref": "#/$defs/renditionProfile" + }, "dimensions": { "$ref": "#/$defs/dimensions" }, + "max_bytes": { + "type": "integer", + "minimum": 1, + "maximum": 50000000 + }, "rights_notice": { "type": "string", "minLength": 1, diff --git a/specs/manifests/COMIC-MANIFEST-VERSIONING.md b/specs/manifests/COMIC-MANIFEST-VERSIONING.md index d0c4359..523f25a 100644 --- a/specs/manifests/COMIC-MANIFEST-VERSIONING.md +++ b/specs/manifests/COMIC-MANIFEST-VERSIONING.md @@ -5,6 +5,13 @@ record kinds and detached approval definition travel together in one contract bundle. Their `spec_version` is independent of episode revisions, artifact logical versions and the Context Package/Builder/Prompt SDK contract versions. +RFC 0008 completes the rendition/publication portion of this same pre-release +candidate. It adds required profile and output-cap fields before Comic Manifest +1.0.0 is accepted or published. The earlier #89 candidate source and the #90–#93 +development consumers are not a released 1.0.0 contract; consumers must update to +the Codex revision accepted for the candidate before using the expanded schema. +The new profile references themselves use exact immutable ID/version pairs. + Consumers must advertise exact supported kind/version pairs; a 1.0.0 validator rejects 1.0.1/1.1.0 until explicitly supported. This is the RFC's intentional narrow capability rule, not a promise of implicit minor compatibility. Closed records diff --git a/specs/manifests/comic-manifest-v1.md b/specs/manifests/comic-manifest-v1.md index fe8a90e..538d465 100644 --- a/specs/manifests/comic-manifest-v1.md +++ b/specs/manifests/comic-manifest-v1.md @@ -1,8 +1,10 @@ # Comic Manifest v1 Status: proposed 1.0.0 candidate, **unreleased; not a production dependency**. -Owner: @andrewperis. [RFC 0007](../../rfcs/0007-comic-manifest-v1.md). -[Studio #89](https://github.com/DefinitelySecureStudio/studio/issues/89). +Owner: @andrewperis. [RFC 0007](../../rfcs/0007-comic-manifest-v1.md) and +[RFC 0008](../../rfcs/0008-comic-manifest-rendition-profiles.md). +[Studio #89](https://github.com/DefinitelySecureStudio/studio/issues/89) and +[#94](https://github.com/DefinitelySecureStudio/studio/issues/94). Authority: [Studio ADR 0018](https://github.com/DefinitelySecureStudio/studio/blob/7b5065cef76bea9580609caba356b0d8fe7cc17c/adr/0018-comic-manifest-architecture.md) and [architecture](https://github.com/DefinitelySecureStudio/studio/blob/7b5065cef76bea9580609caba356b0d8fe7cc17c/comic-manifest/ARCHITECTURE.md). Constitution: v1.0.0, `constitution/v1.0.0`, Studio commit @@ -121,19 +123,56 @@ regardless of whether it happens to contain an innocuous string. ## Renditions and build results -A production declares at least one required rendition. IDs, required/optional -status, supported media type, dimensions, alt text, transcript and rights notice -are explicit. Images require dimensions; text/PDF use null dimensions in this v1 -baseline. The contract does not render or inspect media. Consumers must check -actual media signatures/dimensions and humans must assess accessibility/meaning; -a claimed media type or transcript is insufficient proof. Source editing formats, -layout engines and illustration generation are outside v1. +A production declares at least one required rendition. Every requirement names an +immutable profile ID/version, one permitted media type, exact target dimensions or +null, and a positive `max_bytes` no greater than the profile cap. A profile version +has fixed semantics; a changed media set or limit requires a new version. Consumers +MUST support the exact profile pair and MUST reject unknown pairs, aliases, +wildcards, `latest` or silent fallback. + +The v1 profile catalog is: + +| Profile ID | Version | Permitted media type(s) | Dimensions | Maximum declared bytes | +| --- | --- | --- | --- | ---: | +| `comic-page-image` | `1.0.0` | `image/png`, `image/jpeg`, `image/webp` | Both positive integer dimensions required; each at most 8,192 pixels and product at most 33,554,432 pixels | 50,000,000 | +| `comic-portable-document` | `1.0.0` | `application/pdf` | `null` | 50,000,000 | +| `comic-accessible-transcript` | `1.0.0` | `text/plain` | `null` | 131,072 | + +The declared dimensions are the exact expected output dimensions, not a scaling +instruction. Each result/public output repeats the profile pair, declared byte cap, +dimensions, media type and accessibility/rights metadata. Those fields MUST equal +the production requirement. Result and release artifacts MUST not exceed the +requirement's `max_bytes`; a profile-level cap cannot be raised by a manifest. +Complete results MUST contain each required rendition once, MAY omit optional +renditions, and MUST NOT contain undeclared or duplicate rendition IDs. + +`alt_text` MUST be nonempty and MUST describe the relevant visual content concisely. +`transcript` MUST be nonempty and preserve ordered dialogue/captions for the episode. +`rights_notice` MUST be nonempty and state the intended notice for that output. +These declarations are carried unchanged from production into result and release; +they are review inputs, not proof that text is accurate, accessible, legally +sufficient, or cleared. The public release MUST also contain the exact standard +`production_credit` and its final title, Universe-assigned `DS-NNNN` episode ID, +release revision/predecessor, canon scope, destination, audience, purpose and +publication time. The public episode ID is distinct from the opaque production +UUID; assignment consistency is checked explicitly and the number is never +derived from a private identity. + +The contract does not render, decode, letter or inspect media. Matching declared +type, dimensions, byte size, descriptions, credit or rights notice does not prove +that bytes have that media type, render correctly, meet creative/accessibility +quality, include the credit visibly, establish rights ownership/permission or are +safe to publish. Consumers must verify actual artifact bytes and dimensions; +qualified humans review accessibility, creative quality, rights, disclosure and +publication. Source editing formats, layout engines and illustration generation +are outside v1. A result references the exact complete production record and repeats its exact `inputs`. It carries distinct result/attempt IDs; attempts never mutate production. -Outputs MUST have unique declared rendition IDs and match each requirement's media, -dimensions, alt text, transcript and rights. Raw bytes MUST verify against recorded -size/digest. A result may use HTTPS output locations or protected store UUID handles; +Outputs MUST have unique declared rendition IDs and exactly match each requirement's +profile/version, media, dimensions, byte cap, alt text, transcript and rights notice. +Artifact byte size MUST be within the declared cap and raw bytes MUST verify against +recorded size/digest. A result may use HTTPS output locations or protected store UUID handles; these are never automatically copied into public records. `complete` requires every required rendition, at least one output and no diagnostics. Optional absent outputs are allowed; unexpected outputs fail. `partial`/`failed` require a safe diagnostic @@ -187,7 +226,9 @@ protected references are excluded. Public outputs contain the exact selected result's output list in result order, with identical content identities/requirements and approved public HTTPS locations. V1 publishes that result's entire selected output set; a different selection requires a separate result/candidate. Output -identity is independently verifiable without access to private systems. +identity is independently verifiable without access to private systems. Every +published output preserves the exact profile ID/version and byte cap carried by +the production requirement and build result. Public tool, workflow identity, times and reproducibility MUST agree with the selected result. Public transformations preserve ordered step IDs. They retain only input/output diff --git a/tests/comic-manifest.test.cjs b/tests/comic-manifest.test.cjs index 68911e4..6d0358d 100644 --- a/tests/comic-manifest.test.cjs +++ b/tests/comic-manifest.test.cjs @@ -26,6 +26,38 @@ function rebind(s) { s.trust.approvals = s.approvals.map(a=>({ decision_id:a.decision_id, sha256:identity(a).sha256 })); if (s.trust.attestation) s.trust.attestation.linkage_identity = identity(s.linkage); } +function addSyntheticOutput(s, { renditionId, profileId, mediaType, dimensions, placeholder, maxBytes }) { + const raw = placeholder; + const req = { + rendition_id: renditionId, required: true, + profile: { profile_id: profileId, profile_version: '1.0.0' }, + media_type: mediaType, dimensions, max_bytes: maxBytes ?? Buffer.byteLength(raw), + alt_text: 'Synthetic accessibility description.', + transcript: s.production.renditions[0].transcript, + rights_notice: 'Synthetic fixture; no rights assertion.' + }; + s.production.renditions.push(req); + s.output_bytes[renditionId] = raw; + const digest = sha(raw); + const output = { + rendition_id: renditionId, + artifact: { + artifact_uri: 'urn:uuid:10000000-0000-4000-8000-000000000011', + media_type: mediaType, byte_size: Buffer.byteLength(raw), sha256: digest + }, + profile: structuredClone(req.profile), dimensions: structuredClone(dimensions), + max_bytes: req.max_bytes, alt_text: req.alt_text, + transcript: req.transcript, rights_notice: req.rights_notice + }; + s.result.outputs.push(output); + s.release.outputs.push({ ...structuredClone(output), artifact: { + ...output.artifact, artifact_uri: `https://example.invalid/synthetic/${renditionId}` + } }); + s.result.execution.transformations[0].output_digests.push(digest); + s.release.execution.transformations[0].output_digests.push(digest); + rebind(s); + return s; +} test('synthetic three-record slice and detached approvals validate', () => { schemaValid(base); verifyScenario(base, foundation); }); test('every structured object is closed', () => { function walk(x) { @@ -98,6 +130,70 @@ test('missing one required rendition fails even with another valid output', () = const s=structuredClone(base);s.production.renditions.push({...s.production.renditions[0],rendition_id:'second-required'});rebind(s);schemaValid(s); assert.throws(()=>verifyScenario(s,foundation),/REQUIRED_OUTPUT/); }); +test('versioned image/document profiles accept their exact media, pixel and byte boundaries', () => { + // Placeholder output bytes intentionally do not claim to be rendered media. + const image = addSyntheticOutput(structuredClone(base), { + renditionId: 'page', profileId: 'comic-page-image', mediaType: 'image/png', + dimensions: { width: 8192, height: 4096 }, placeholder: 'SYNTHETIC-IMAGE-PLACEHOLDER' + }); + schemaValid(image); verifyScenario(image, foundation); + const document = addSyntheticOutput(structuredClone(base), { + renditionId: 'document', profileId: 'comic-portable-document', mediaType: 'application/pdf', + dimensions: null, placeholder: 'SYNTHETIC-DOCUMENT-PLACEHOLDER', maxBytes: 50_000_000 + }); + schemaValid(document); verifyScenario(document, foundation); +}); +test('raster profiles reject side and pixel limits just above their inclusive boundary', () => { + for (const dimensions of [{ width: 8192, height: 4097 }, { width: 8193, height: 1 }]) { + const s = addSyntheticOutput(structuredClone(base), { + renditionId: 'page', profileId: 'comic-page-image', mediaType: 'image/png', + dimensions, placeholder: 'SYNTHETIC-IMAGE-PLACEHOLDER' + }); + schemaValid(s); assert.throws(() => verifyScenario(s, foundation), /RENDITION_DIMENSIONS/); + } +}); +test('actual artifact bytes cannot exceed the declared rendition cap', () => { + const size = Buffer.byteLength('SYNTHETIC-IMAGE-PLACEHOLDER'); + const exact = addSyntheticOutput(structuredClone(base), { + renditionId: 'page', profileId: 'comic-page-image', mediaType: 'image/webp', + dimensions: { width: 1, height: 1 }, placeholder: 'SYNTHETIC-IMAGE-PLACEHOLDER', maxBytes: size + }); + schemaValid(exact); verifyScenario(exact, foundation); + const over = addSyntheticOutput(structuredClone(base), { + renditionId: 'page', profileId: 'comic-page-image', mediaType: 'image/webp', + dimensions: { width: 1, height: 1 }, placeholder: 'SYNTHETIC-IMAGE-PLACEHOLDER', maxBytes: size - 1 + }); + schemaValid(over); assert.throws(() => verifyScenario(over, foundation), /OUTPUT_LIMIT/); +}); +test('complete result rejects a missing required output and duplicate output IDs', () => { + const missing = addSyntheticOutput(structuredClone(base), { + renditionId: 'page', profileId: 'comic-page-image', mediaType: 'image/png', + dimensions: { width: 1, height: 1 }, placeholder: 'SYNTHETIC-IMAGE-PLACEHOLDER' + }); + missing.result.outputs.pop(); rebind(missing); schemaValid(missing); + assert.throws(() => verifyScenario(missing, foundation), /REQUIRED_OUTPUT/); + const duplicate = addSyntheticOutput(structuredClone(base), { + renditionId: 'page', profileId: 'comic-page-image', mediaType: 'image/png', + dimensions: { width: 1, height: 1 }, placeholder: 'SYNTHETIC-IMAGE-PLACEHOLDER' + }); + duplicate.result.outputs.push(structuredClone(duplicate.result.outputs.at(-1))); + rebind(duplicate); schemaValid(duplicate); + assert.throws(() => verifyScenario(duplicate, foundation), /OUTPUT_ID/); +}); +test('public release preserves exact selected output profile and metadata', () => { + const s = structuredClone(base); + s.release.outputs[0].profile.profile_id = 'comic-page-image'; + rebind(s); schemaValid(s); + assert.throws(() => verifyScenario(s, foundation), /RELEASE_OUTPUTS/); +}); +test('profile and accessibility fields cannot be omitted from any rendition/output record', () => { + for (const [kind, record] of [['production', 'renditions'], ['result', 'outputs'], ['release', 'outputs']]) { + for (const field of ['profile', 'max_bytes', 'alt_text', 'transcript', 'rights_notice']) { + const s = structuredClone(base); delete s[kind][record][0][field]; + assert.equal(validate(s[kind]), false, `${kind}.${record}.${field}`); + } + } +}); test('public asset references resolve and contribute exact public dependencies', () => { const s=structuredClone(base);s.production.inputs.assets=[{asset_id:'synthetic-asset',classification:'public',reference:{kind:'public',dependency:s.production.inputs.canon},rights_notice:'Synthetic fixture.'}]; s.production.panels[0].asset_ids=['synthetic-asset'];s.release.dependencies.push(structuredClone(s.production.inputs.canon)); diff --git a/tests/support/comic-manifest-oracle.cjs b/tests/support/comic-manifest-oracle.cjs index 7dc0d3e..910c459 100644 --- a/tests/support/comic-manifest-oracle.cjs +++ b/tests/support/comic-manifest-oracle.cjs @@ -7,6 +7,23 @@ const check = (v, code) => assert.ok(v, code); const unique = (xs, code) => eq(new Set(xs).size, xs.length, code); const rank = c => ['public', 'internal', 'confidential', 'restricted'].indexOf(c); const gates = ['editorial', 'canon-continuity', 'visual-text', 'integrity', 'provenance', 'security-privacy', 'rights', 'accessibility', 'packaging']; +const renditionProfiles = { + 'comic-page-image@1.0.0': { media: ['image/png', 'image/jpeg', 'image/webp'], maxBytes: 50_000_000, image: true }, + 'comic-portable-document@1.0.0': { media: ['application/pdf'], maxBytes: 50_000_000, image: false }, + 'comic-accessible-transcript@1.0.0': { media: ['text/plain'], maxBytes: 131_072, image: false } +}; +function verifyRenditionProfile(r) { + const profile = renditionProfiles[`${r.profile.profile_id}@${r.profile.profile_version}`]; + check(profile, 'RENDITION_PROFILE'); + const mediaType = r.artifact ? r.artifact.media_type : r.media_type; + check(profile.media.includes(mediaType), 'RENDITION_MEDIA'); + if (profile.image) { + check(r.dimensions !== null && r.dimensions.width <= 8_192 && r.dimensions.height <= 8_192 && + r.dimensions.width * r.dimensions.height <= 33_554_432, 'RENDITION_DIMENSIONS'); + } else eq(r.dimensions, null, 'RENDITION_DIMENSIONS'); + check(r.max_bytes <= profile.maxBytes, 'RENDITION_LIMIT'); + if (r.artifact) check(r.artifact.byte_size <= r.max_bytes, 'OUTPUT_LIMIT'); +} const productionRef = p => ({ production_id: p.production_id, revision: p.revision, identity: identity(p) }); function verifyProduction(p) { eq(p.previous === null, p.revision === 1, 'REVISION'); @@ -30,7 +47,10 @@ function verifyProduction(p) { check(rank(p.classification) >= rank(b.context.classification), 'CLASSIFICATION'); unique(b.context.sections, 'CONTEXT_SECTIONS'); } - for (const r of p.renditions) eq(r.media_type.startsWith('image/'), r.dimensions !== null, 'DIMENSIONS'); + for (const r of p.renditions) { + verifyRenditionProfile(r); + eq(r.media_type.startsWith('image/'), r.dimensions !== null, 'DIMENSIONS'); + } check(p.renditions.some(r => r.required), 'REQUIRED_OUTPUT'); } function verifyResult(p, r, bytes) { @@ -44,6 +64,8 @@ function verifyResult(p, r, bytes) { for(const g of r.execution.generation) unique(g.parameters.map(p=>p.name),'PARAMETER_ID'); for (const out of r.outputs) { const req = p.renditions.find(x => x.rendition_id === out.rendition_id); check(req, 'UNEXPECTED_OUTPUT'); + eq(out.profile, req.profile, 'OUTPUT_REQUIREMENT'); eq(out.max_bytes, req.max_bytes, 'OUTPUT_REQUIREMENT'); + verifyRenditionProfile(out); eq(out.artifact.media_type, req.media_type, 'MEDIA'); eq(out.dimensions, req.dimensions, 'DIMENSIONS'); for (const key of ['alt_text', 'transcript', 'rights_notice']) eq(out[key], req[key], 'OUTPUT_REQUIREMENT'); const raw = bytes[out.rendition_id]; check(typeof raw === 'string', 'MISSING_BYTES'); @@ -102,6 +124,7 @@ function verifyScenario(s, foundation) { eq(rel.outputs.map(x => x.rendition_id), r.outputs.map(x => x.rendition_id), 'RELEASE_OUTPUTS'); for (let i=0; i Date: Tue, 6 Oct 2026 13:49:13 -0400 Subject: [PATCH 2/3] fix(codex): require selected rendition media type --- fixtures/comic-manifest-v1-cases.json | 44 +++++++++++++++++++ .../0008-comic-manifest-rendition-profiles.md | 8 ++-- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/fixtures/comic-manifest-v1-cases.json b/fixtures/comic-manifest-v1-cases.json index 1e98269..423e617 100644 --- a/fixtures/comic-manifest-v1-cases.json +++ b/fixtures/comic-manifest-v1-cases.json @@ -540,6 +540,50 @@ "code": "MEDIA", "rebind": true }, + { + "name": "allowed profile alternative still must equal selected media type", + "layer": "relational", + "edits": [ + { + "path": ["production", "renditions", 0, "profile", "profile_id"], + "value": "comic-page-image" + }, + { + "path": ["production", "renditions", 0, "media_type"], + "value": "image/png" + }, + { + "path": ["production", "renditions", 0, "dimensions"], + "value": {"width": 1, "height": 1} + }, + { + "path": ["result", "outputs", 0, "profile", "profile_id"], + "value": "comic-page-image" + }, + { + "path": ["result", "outputs", 0, "artifact", "media_type"], + "value": "image/webp" + }, + { + "path": ["result", "outputs", 0, "dimensions"], + "value": {"width": 1, "height": 1} + }, + { + "path": ["release", "outputs", 0, "profile", "profile_id"], + "value": "comic-page-image" + }, + { + "path": ["release", "outputs", 0, "artifact", "media_type"], + "value": "image/webp" + }, + { + "path": ["release", "outputs", 0, "dimensions"], + "value": {"width": 1, "height": 1} + } + ], + "code": "MEDIA", + "rebind": true + }, { "name": "wrong output digest", "layer": "relational", diff --git a/rfcs/0008-comic-manifest-rendition-profiles.md b/rfcs/0008-comic-manifest-rendition-profiles.md index 1d92378..7289f87 100644 --- a/rfcs/0008-comic-manifest-rendition-profiles.md +++ b/rfcs/0008-comic-manifest-rendition-profiles.md @@ -57,9 +57,11 @@ version and explicit consumer support. Every build-result and public-release output MUST repeat the exact profile pair, declared `max_bytes`, dimensions and accessibility/rights metadata from its -production requirement. Its media type MUST be permitted by the profile; its -dimensions and metadata MUST match the requirement exactly; and the recorded -artifact byte size MUST NOT exceed `max_bytes`. A complete result MUST include +production requirement. Its media type MUST equal the exact media type selected +by the production requirement, even when the profile permits other types; that +selected type MUST also be permitted by the profile. Its dimensions and metadata +MUST match the requirement exactly, and its recorded artifact byte size MUST NOT +exceed `max_bytes`. A complete result MUST include every required rendition exactly once, MAY omit optional renditions, and MUST NOT include missing, duplicate or undeclared output IDs. The public release MUST preserve the selected result's output order and exact public output metadata. From c9b2d556c788d346c4628a91150a77c803574482 Mon Sep 17 00:00:00 2001 From: Andrew Peris Date: Tue, 6 Oct 2026 13:57:39 -0400 Subject: [PATCH 3/3] test(codex): cover public output media compatibility --- fixtures/comic-manifest-v1-cases.json | 44 +++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/fixtures/comic-manifest-v1-cases.json b/fixtures/comic-manifest-v1-cases.json index 423e617..58dc1b7 100644 --- a/fixtures/comic-manifest-v1-cases.json +++ b/fixtures/comic-manifest-v1-cases.json @@ -584,6 +584,50 @@ "code": "MEDIA", "rebind": true }, + { + "name": "public release preserves selected PNG despite profile allowing WebP", + "layer": "relational", + "edits": [ + { + "path": ["production", "renditions", 0, "profile", "profile_id"], + "value": "comic-page-image" + }, + { + "path": ["production", "renditions", 0, "media_type"], + "value": "image/png" + }, + { + "path": ["production", "renditions", 0, "dimensions"], + "value": {"width": 1, "height": 1} + }, + { + "path": ["result", "outputs", 0, "profile", "profile_id"], + "value": "comic-page-image" + }, + { + "path": ["result", "outputs", 0, "artifact", "media_type"], + "value": "image/png" + }, + { + "path": ["result", "outputs", 0, "dimensions"], + "value": {"width": 1, "height": 1} + }, + { + "path": ["release", "outputs", 0, "profile", "profile_id"], + "value": "comic-page-image" + }, + { + "path": ["release", "outputs", 0, "artifact", "media_type"], + "value": "image/webp" + }, + { + "path": ["release", "outputs", 0, "dimensions"], + "value": {"width": 1, "height": 1} + } + ], + "code": "RELEASE_OUTPUTS", + "rebind": true + }, { "name": "wrong output digest", "layer": "relational",