{"_id":"@clossys/publisher","_rev":"7-5f0ea52d49ab68a8b0ea720b807afdc0","name":"@clossys/publisher","dist-tags":{"latest":"0.7.0"},"versions":{"0.1.10":{"name":"@clossys/publisher","version":"0.1.10","keywords":["publisher","composition","render","web","email","print","image","slides","ledger","append-only","attribution","drift","content-addressed","typescript"],"author":{"name":"Calvin Hung"},"license":"MIT","_id":"@clossys/publisher@0.1.10","maintainers":[{"name":"thecalvinhung","email":"hello@clossys.com"}],"homepage":"https://github.com/clossys/foundry/tree/main/packages/publisher#readme","bugs":{"url":"https://github.com/clossys/foundry/issues"},"bin":{"publisher-media-check":"dist/media/cli.js","publisher-record-check":"dist/record/cli.js"},"dist":{"shasum":"69bd29fe70e6b3c9b735f8cc0b57cd3c3eef3eb7","tarball":"https://registry.npmjs.org/@clossys/publisher/-/publisher-0.1.10.tgz","fileCount":399,"integrity":"sha512-ubkRI+hk1SJgGkFYpU8P47fBKKUurzWAPfFISeEZsocZv9ZvnWB5MR4zglin37d35xpoHsCZhlPV2hTg1S9Fag==","signatures":[{"sig":"MEUCIDglyJ6Y0CTmYfWV5sUftsH0PqhJr9ZSjhV6KAbnlPizAiEApc9vd2rqa1mMzSqbxdWg+OImSF3SsNNkKj/9rSnShVY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":2053269},"type":"module","engines":{"node":">=20"},"exports":{"./web":{"types":"./dist/web/index.d.ts","import":"./dist/web/index.js","react-server":"./dist/web/server.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js"},"./email":{"types":"./dist/email/index.d.ts","import":"./dist/email/index.js"},"./image":{"types":"./dist/image/index.d.ts","import":"./dist/image/index.js"},"./media":{"types":"./dist/media/index.d.ts","import":"./dist/media/index.js"},"./print":{"types":"./dist/print/index.d.ts","import":"./dist/print/index.js"},"./record":{"types":"./dist/record/index.d.ts","import":"./dist/record/index.js"},"./slides":{"types":"./dist/slides/index.d.ts","import":"./dist/slides/index.js"},"./document":{"types":"./dist/document/index.d.ts","import":"./dist/document/index.js"}},"imports":{"#publisher-web-views":{"types":"./dist/web/views/index.d.ts","default":"./dist/web/views/index.js","react-server":"./dist/web/views/server.js"}},"private":false,"scripts":{"test":"vitest run","build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.json --noEmit","prepublishOnly":"node ../../scripts/check-name-collision.mjs . && npm run build"},"_npmUser":{"name":"thecalvinhung","email":"hello@clossys.com"},"repository":{"url":"git+https://github.com/clossys/foundry.git","type":"git","directory":"packages/publisher"},"_npmVersion":"11.17.0","description":"The publisher role: did we put it out to an audience, and can we prove what shipped? Surface composition, media registries, and channel renderers for web, email, print, images, and slides, plus a product-neutral structured-document contract, under `./core","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","dependencies":{"@clossys/writer":"^0.3.0","@clossys/designer":"^0.2.4","@clossys/controller":"~0.8.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^26.1.0","react":"^19.2.8","vitest":"^4.1.9","react-dom":"^19.2.8","typescript":"~6.0.0","@types/node":"^22.10.0","tailwindcss":"^4.3.3","@types/react":"^19.2.0","tailwind-merge":"^3.0.0","@types/react-dom":"^19.2.0","react-aria-components":"^1.20.0","@testing-library/react":"^16.3.2","@internationalized/date":"^3.12.3","@testing-library/jest-dom":"^7.0.1","@testing-library/user-event":"^14.6.3"},"peerDependencies":{"react":">=18","react-dom":">=18","tailwindcss":"^4.0.0","tailwind-merge":"^3.0.0","react-aria-components":"^1.19.0","@internationalized/date":"^3.12.2"},"peerDependenciesMeta":{"react":{"optional":true},"react-dom":{"optional":true},"tailwindcss":{"optional":true},"tailwind-merge":{"optional":true},"react-aria-components":{"optional":true},"@internationalized/date":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/publisher_0.1.10_1788210126432_0.7230806772386182","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@clossys/publisher","version":"0.2.1","keywords":["publisher","composition","render","web","email","print","image","slides","ledger","append-only","attribution","drift","content-addressed","typescript"],"author":{"name":"Calvin Hung"},"license":"MIT","_id":"@clossys/publisher@0.2.1","maintainers":[{"name":"thecalvinhung","email":"hello@clossys.com"}],"homepage":"https://github.com/clossys/foundry/tree/main/packages/publisher#readme","bugs":{"url":"https://github.com/clossys/foundry/issues"},"bin":{"publisher-media-check":"dist/media/cli.js","publisher-record-check":"dist/record/cli.js"},"dist":{"shasum":"6f42f43dcebae94a7cdbc84134e9d3ec502eec0b","tarball":"https://registry.npmjs.org/@clossys/publisher/-/publisher-0.2.1.tgz","fileCount":434,"integrity":"sha512-Kd0SDwRTbY97HrGnkHvxz9Is30QAxED5kDOJEPzR4DWAwPpy3vn4H8LuanoHJsbtSjonkaQnxxlOU9IiWRwgVA==","signatures":[{"sig":"MEUCIQDSZ2MwjH0BUx0hm6kGmq63TyTYwdwjKWdPZuHyv1PV3AIgDw9hsaZve4WjztPa8awk3zJ0fI9xXnqH3Wzj/wmVVjM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@clossys%2fpublisher@0.2.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":2299399},"type":"module","engines":{"node":">=20"},"exports":{"./web":{"types":"./dist/web/index.d.ts","import":"./dist/web/index.js","react-server":"./dist/web/server.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js"},"./email":{"types":"./dist/email/index.d.ts","import":"./dist/email/index.js"},"./image":{"types":"./dist/image/index.d.ts","import":"./dist/image/index.js"},"./media":{"types":"./dist/media/index.d.ts","import":"./dist/media/index.js"},"./print":{"types":"./dist/print/index.d.ts","import":"./dist/print/index.js"},"./record":{"types":"./dist/record/index.d.ts","import":"./dist/record/index.js"},"./slides":{"types":"./dist/slides/index.d.ts","import":"./dist/slides/index.js"},"./document":{"types":"./dist/document/index.d.ts","import":"./dist/document/index.js"}},"imports":{"#publisher-web-views":{"types":"./dist/web/views/index.d.ts","default":"./dist/web/views/index.js","react-server":"./dist/web/views/server.js"}},"private":false,"scripts":{"test":"vitest run","build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.json --noEmit","prepublishOnly":"node ../../scripts/check-name-collision.mjs . && npm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:8c63dd08-c972-407c-9371-363bd92ac813"}},"repository":{"url":"git+https://github.com/clossys/foundry.git","type":"git","directory":"packages/publisher"},"_npmVersion":"11.17.0","description":"The publisher role: did we put it out to an audience, and can we prove what shipped? Surface composition, media registries, and channel renderers for web, email, print, images, and slides, plus a product-neutral structured-document contract, under `./core","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","dependencies":{"@clossys/writer":"^0.3.0","@clossys/designer":"^0.2.7","@clossys/controller":"~0.8.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^26.1.0","react":"^19.2.8","vitest":"^4.1.9","react-dom":"^19.2.8","typescript":"~6.0.0","@types/node":"^22.10.0","tailwindcss":"^4.3.3","@types/react":"^19.2.0","tailwind-merge":"^3.0.0","@types/react-dom":"^19.2.0","react-aria-components":"^1.20.0","@testing-library/react":"^16.3.2","@internationalized/date":"^3.12.3","@testing-library/jest-dom":"^7.0.1","@testing-library/user-event":"^14.6.3"},"peerDependencies":{"react":">=18","react-dom":">=18","tailwindcss":"^4.0.0","tailwind-merge":"^3.0.0","react-aria-components":"^1.19.0","@internationalized/date":"^3.12.2"},"peerDependenciesMeta":{"react":{"optional":true},"react-dom":{"optional":true},"tailwindcss":{"optional":true},"tailwind-merge":{"optional":true},"react-aria-components":{"optional":true},"@internationalized/date":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/publisher_0.2.1_1788264155855_0.5560882869972239","host":"s3://npm-registry-packages-npm-production"}},"0.4.1":{"name":"@clossys/publisher","version":"0.4.1","keywords":["publisher","composition","render","web","email","print","image","slides","ledger","append-only","attribution","drift","content-addressed","typescript"],"author":{"name":"Clossys"},"license":"MIT","_id":"@clossys/publisher@0.4.1","maintainers":[{"name":"thecalvinhung","email":"hello@clossys.com"}],"homepage":"https://github.com/clossys/foundry/tree/main/packages/publisher#readme","bugs":{"url":"https://github.com/clossys/foundry/issues"},"bin":{"publisher-media-check":"dist/media/cli.js","publisher-record-check":"dist/record/cli.js"},"dist":{"shasum":"2cd3ac854179b98eb65417e19a0b605b0dcd1b9c","tarball":"https://registry.npmjs.org/@clossys/publisher/-/publisher-0.4.1.tgz","fileCount":439,"integrity":"sha512-91ue08Rze7x5DlTqUdbePwx5SKHfLKjYRNdkiXQAMQ71iSHIOieIMGGlCeiW9st8Pv9Qm+I3FFDrAYTJXN2Xyw==","signatures":[{"sig":"MEUCIQDrCsitqtojZrIg0jp6yhnC8Pb1o+SLa404/S4t3kUrzgIgE/ZHfZskq1jJz4ZI1LppWtOyWYcPu9h13UJ7tVm+Anc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@clossys%2fpublisher@0.4.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":2336069},"type":"module","engines":{"node":">=20"},"exports":{"./web":{"types":"./dist/web/index.d.ts","import":"./dist/web/index.js","react-server":"./dist/web/server.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js"},"./email":{"types":"./dist/email/index.d.ts","import":"./dist/email/index.js"},"./image":{"types":"./dist/image/index.d.ts","import":"./dist/image/index.js"},"./media":{"types":"./dist/media/index.d.ts","import":"./dist/media/index.js"},"./print":{"types":"./dist/print/index.d.ts","import":"./dist/print/index.js"},"./record":{"types":"./dist/record/index.d.ts","import":"./dist/record/index.js"},"./slides":{"types":"./dist/slides/index.d.ts","import":"./dist/slides/index.js"},"./document":{"types":"./dist/document/index.d.ts","import":"./dist/document/index.js"}},"imports":{"#publisher-web-views":{"types":"./dist/web/views/index.d.ts","default":"./dist/web/views/index.js","react-server":"./dist/web/views/server.js"}},"private":false,"scripts":{"test":"vitest run","build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.json --noEmit","prepublishOnly":"node ../../scripts/check-name-collision.mjs . && npm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:8c63dd08-c972-407c-9371-363bd92ac813"}},"repository":{"url":"git+https://github.com/clossys/foundry.git","type":"git","directory":"packages/publisher"},"_npmVersion":"11.17.0","description":"The publisher role: did we put it out to an audience, and can we prove what shipped? Surface composition, media registries, and channel renderers for web, email, print, images, and slides, plus a product-neutral structured-document contract, under `./core","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","dependencies":{"@clossys/writer":"^0.3.0","@clossys/designer":"^0.4.0","@clossys/controller":"~0.9.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^26.1.0","react":"^19.2.8","vitest":"^4.1.9","react-dom":"^19.2.8","typescript":"~6.0.0","@types/node":"^22.10.0","tailwindcss":"^4.3.3","@types/react":"^19.2.0","tailwind-merge":"^3.0.0","@types/react-dom":"^19.2.5","react-aria-components":"^1.20.0","@testing-library/react":"^16.3.3","@internationalized/date":"^3.12.3","@testing-library/jest-dom":"^7.0.1","@testing-library/user-event":"^14.6.6"},"peerDependencies":{"react":">=18","react-dom":">=18","tailwindcss":"^4.0.0","tailwind-merge":"^3.0.0","react-aria-components":"^1.19.0","@internationalized/date":"^3.12.2"},"peerDependenciesMeta":{"react":{"optional":true},"react-dom":{"optional":true},"tailwindcss":{"optional":true},"tailwind-merge":{"optional":true},"react-aria-components":{"optional":true},"@internationalized/date":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/publisher_0.4.1_1788490942079_0.8015875795592569","host":"s3://npm-registry-packages-npm-production"}},"0.4.3":{"name":"@clossys/publisher","version":"0.4.3","keywords":["publisher","composition","render","web","email","print","image","slides","ledger","append-only","attribution","drift","content-addressed","typescript"],"author":{"name":"Clossys"},"license":"MIT","_id":"@clossys/publisher@0.4.3","maintainers":[{"name":"thecalvinhung","email":"hello@clossys.com"}],"homepage":"https://github.com/clossys/foundry/tree/main/packages/publisher#readme","bugs":{"url":"https://github.com/clossys/foundry/issues"},"bin":{"publisher-media-check":"dist/media/cli.js","publisher-record-check":"dist/record/cli.js"},"dist":{"shasum":"b55ced7ccc3ee880fe7d20ce90d068c9d98b0eff","tarball":"https://registry.npmjs.org/@clossys/publisher/-/publisher-0.4.3.tgz","fileCount":439,"integrity":"sha512-BbQzkLY4HYZ/3vB1SbWhngRPpVTYECBOG4J0iTRPLx31PMPwLUa0mU0P3igKXxrrmhXjh5nIlcPcREuxU1BZEA==","signatures":[{"sig":"MEYCIQCRlktj286e8mn6FCetdDewGB6cDgr7p8fmxIzgr0XEKgIhAIUcE8tRA/1A5l9B5qyNCpYSFaUXplsUYSp1UWRWqDMq","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCIElZEhqZkiTUjlRwcAtR89ygv9mnGaLnxBhJR+7bll8UAiAbs+c15bSOf2uLERwUG3yBwwVixOsF0/QX3wLO4kpYlA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@clossys%2fpublisher@0.4.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":2336736},"type":"module","engines":{"node":">=20"},"exports":{"./web":{"types":"./dist/web/index.d.ts","import":"./dist/web/index.js","react-server":"./dist/web/server.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js"},"./email":{"types":"./dist/email/index.d.ts","import":"./dist/email/index.js"},"./image":{"types":"./dist/image/index.d.ts","import":"./dist/image/index.js"},"./media":{"types":"./dist/media/index.d.ts","import":"./dist/media/index.js"},"./print":{"types":"./dist/print/index.d.ts","import":"./dist/print/index.js"},"./record":{"types":"./dist/record/index.d.ts","import":"./dist/record/index.js"},"./slides":{"types":"./dist/slides/index.d.ts","import":"./dist/slides/index.js"},"./document":{"types":"./dist/document/index.d.ts","import":"./dist/document/index.js"}},"imports":{"#publisher-web-views":{"types":"./dist/web/views/index.d.ts","default":"./dist/web/views/index.js","react-server":"./dist/web/views/server.js"}},"private":false,"scripts":{"test":"vitest run","build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.json --noEmit","prepublishOnly":"node ../../scripts/check-name-collision.mjs . && npm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:8c63dd08-c972-407c-9371-363bd92ac813"}},"repository":{"url":"git+https://github.com/clossys/foundry.git","type":"git","directory":"packages/publisher"},"_npmVersion":"11.17.0","description":"The publisher role: did we put it out to an audience, and can we prove what shipped? Surface composition, media registries, and channel renderers for web, email, print, images, and slides, plus a product-neutral structured-document contract, under `./core","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","dependencies":{"@clossys/writer":"^0.3.0","@clossys/designer":"^0.4.0","@clossys/controller":"~0.9.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^26.1.0","react":"^19.2.8","vitest":"^5.0.0","react-dom":"^19.2.8","typescript":"~6.0.0","@types/node":"^26.4.1","tailwindcss":"^4.3.3","@types/react":"^19.2.0","tailwind-merge":"^3.0.0","@types/react-dom":"^19.2.5","react-aria-components":"^1.21.1","@testing-library/react":"^16.3.3","@internationalized/date":"^3.12.3","@testing-library/jest-dom":"^7.0.1","@testing-library/user-event":"^14.6.6"},"peerDependencies":{"react":">=18","react-dom":">=18","tailwindcss":"^4.0.0","tailwind-merge":"^3.0.0","react-aria-components":"^1.19.0","@internationalized/date":"^3.12.2"},"peerDependenciesMeta":{"react":{"optional":true},"react-dom":{"optional":true},"tailwindcss":{"optional":true},"tailwind-merge":{"optional":true},"react-aria-components":{"optional":true},"@internationalized/date":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/publisher_0.4.3_1789391346838_0.6772457963585965","host":"s3://npm-registry-packages-npm-production"}},"0.4.4":{"name":"@clossys/publisher","version":"0.4.4","keywords":["publisher","composition","render","web","email","print","image","slides","ledger","append-only","attribution","drift","content-addressed","typescript"],"author":{"name":"Clossys"},"license":"MIT","_id":"@clossys/publisher@0.4.4","maintainers":[{"name":"thecalvinhung","email":"hello@clossys.com"}],"homepage":"https://github.com/clossys/foundry/tree/main/packages/publisher#readme","bugs":{"url":"https://github.com/clossys/foundry/issues"},"bin":{"publisher-media-check":"dist/media/cli.js","publisher-record-check":"dist/record/cli.js"},"dist":{"shasum":"a6e67ac8804e17445f7d46829e2b65943c0cdad2","tarball":"https://registry.npmjs.org/@clossys/publisher/-/publisher-0.4.4.tgz","fileCount":439,"integrity":"sha512-Jxp/mxKeuAAYibu5Rp16D2l+52I2f1ZByDkiXJRPGtu89KBsTHoMuYcyeh1zfo1Um1QzSy9g1HJPEsDfVkEVSQ==","signatures":[{"sig":"MEUCIBZl2izvQcjpuqYhJwONcyNrui6a+MQCv9AmYLGtmk9fAiEA7AKEIiKCdym09ZK/PDsYAkDwv9fEi2hUsWdd0cvkCpc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIFm25dgXz+5bXsAkqudwVNLuQW+Zy1ZT7mhWP+9EpylzAiEA042MoQcpcwDKGpWj9yx8Ar9g6xsct5oY5GsjioweyyA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@clossys%2fpublisher@0.4.4","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":2339644},"type":"module","engines":{"node":">=20"},"exports":{"./web":{"types":"./dist/web/index.d.ts","import":"./dist/web/index.js","react-server":"./dist/web/server.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js"},"./email":{"types":"./dist/email/index.d.ts","import":"./dist/email/index.js"},"./image":{"types":"./dist/image/index.d.ts","import":"./dist/image/index.js"},"./media":{"types":"./dist/media/index.d.ts","import":"./dist/media/index.js"},"./print":{"types":"./dist/print/index.d.ts","import":"./dist/print/index.js"},"./record":{"types":"./dist/record/index.d.ts","import":"./dist/record/index.js"},"./slides":{"types":"./dist/slides/index.d.ts","import":"./dist/slides/index.js"},"./document":{"types":"./dist/document/index.d.ts","import":"./dist/document/index.js"}},"imports":{"#publisher-web-views":{"types":"./dist/web/views/index.d.ts","default":"./dist/web/views/index.js","react-server":"./dist/web/views/server.js"}},"private":false,"scripts":{"test":"vitest run","build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.json --noEmit","prepublishOnly":"node ../../scripts/check-name-collision.mjs . && npm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:8c63dd08-c972-407c-9371-363bd92ac813"}},"repository":{"url":"git+https://github.com/clossys/foundry.git","type":"git","directory":"packages/publisher"},"_npmVersion":"11.17.0","description":"The publisher role: did we put it out to an audience, and can we prove what shipped? Surface composition, media registries, and channel renderers for web, email, print, images, and slides, plus a product-neutral structured-document contract, under `./core","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","dependencies":{"@clossys/writer":"^0.3.0","@clossys/designer":"^0.4.0","@clossys/controller":"~0.9.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^26.1.0","react":"^19.3.0","vitest":"^5.0.0","react-dom":"^19.3.0","typescript":"~6.0.0","@types/node":"^26.5.1","tailwindcss":"^4.3.3","@types/react":"^19.3.0","tailwind-merge":"^3.0.0","@types/react-dom":"^19.3.0","react-aria-components":"^1.21.1","@testing-library/react":"^16.3.3","@internationalized/date":"^3.12.3","@testing-library/jest-dom":"^7.0.1","@testing-library/user-event":"^14.6.6"},"peerDependencies":{"react":">=18","react-dom":">=18","tailwindcss":"^4.0.0","tailwind-merge":"^3.0.0","react-aria-components":"^1.19.0","@internationalized/date":"^3.12.2"},"peerDependenciesMeta":{"react":{"optional":true},"react-dom":{"optional":true},"tailwindcss":{"optional":true},"tailwind-merge":{"optional":true},"react-aria-components":{"optional":true},"@internationalized/date":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/publisher_0.4.4_1789536744872_0.9927004886247957","host":"s3://npm-registry-packages-npm-production"}},"0.4.5":{"name":"@clossys/publisher","version":"0.4.5","keywords":["publisher","composition","render","web","email","print","image","slides","ledger","append-only","attribution","drift","content-addressed","typescript"],"author":{"name":"Clossys"},"license":"MIT","_id":"@clossys/publisher@0.4.5","maintainers":[{"name":"thecalvinhung","email":"hello@clossys.com"}],"homepage":"https://github.com/clossys/foundry/tree/main/packages/publisher#readme","bugs":{"url":"https://github.com/clossys/foundry/issues"},"bin":{"publisher-rate-check":"dist/verified-publication-rate-cli.js","publisher-media-check":"dist/media/cli.js","publisher-record-check":"dist/record/cli.js"},"dist":{"shasum":"6a9a947cb759b491de76fd86ab13691d3d76813b","tarball":"https://registry.npmjs.org/@clossys/publisher/-/publisher-0.4.5.tgz","fileCount":449,"integrity":"sha512-5d/zSR4doj3xH7zhgTiADpKb3vpZVPwkCS7RjUI6BbhfdWfiNpoRwKq0LvP/Ll1f2mC50OblVgAP+N2yx6NjWg==","signatures":[{"sig":"MEYCIQDRXQcTfztqI9957/0+/x+oTLEFgyDGofBZei44Yz8rrQIhAPK2NdL+zYmo9sohe3vIBdVI+JY4bHYwnMdeWcVmmNRo","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIGIOlQt82X4VXu3eVcG5KekLeDUb8hN2sdXtvjhtmCMQAiEA2nJHHA6hY+tl1CfXYYafoKJcnLbB8wSzFcOFZbfS7yc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@clossys%2fpublisher@0.4.5","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":2374755},"type":"module","engines":{"node":">=20"},"exports":{"./web":{"types":"./dist/web/index.d.ts","import":"./dist/web/index.js","react-server":"./dist/web/server.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js"},"./email":{"types":"./dist/email/index.d.ts","import":"./dist/email/index.js"},"./image":{"types":"./dist/image/index.d.ts","import":"./dist/image/index.js"},"./media":{"types":"./dist/media/index.d.ts","import":"./dist/media/index.js"},"./print":{"types":"./dist/print/index.d.ts","import":"./dist/print/index.js"},"./record":{"types":"./dist/record/index.d.ts","import":"./dist/record/index.js"},"./slides":{"types":"./dist/slides/index.d.ts","import":"./dist/slides/index.js"},"./document":{"types":"./dist/document/index.d.ts","import":"./dist/document/index.js"},"./assessment":{"types":"./dist/verified-publication-rate.d.ts","import":"./dist/verified-publication-rate.js"}},"foundry":{"assessment":{"bin":"publisher-rate-check","invocation":"single-json-input"}},"imports":{"#publisher-web-views":{"types":"./dist/web/views/index.d.ts","default":"./dist/web/views/index.js","react-server":"./dist/web/views/server.js"}},"private":false,"scripts":{"test":"vitest run","build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.json --noEmit","prepublishOnly":"node ../../scripts/check-name-collision.mjs . && npm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:8c63dd08-c972-407c-9371-363bd92ac813"}},"repository":{"url":"git+https://github.com/clossys/foundry.git","type":"git","directory":"packages/publisher"},"_npmVersion":"11.17.0","description":"The publisher role: did we put it out to an audience, and can we prove what shipped? Surface composition, media registries, and channel renderers for web, email, print, images, and slides, plus a product-neutral structured-document contract, under `./core","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","dependencies":{"@clossys/writer":"^0.3.0","@clossys/designer":"^0.4.0","@clossys/controller":"~0.9.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^26.1.0","react":"^19.3.0","vitest":"^5.0.0","react-dom":"^19.3.0","typescript":"~6.0.0","@types/node":"^26.5.1","tailwindcss":"^4.3.3","@types/react":"^19.3.0","tailwind-merge":"^3.0.0","@types/react-dom":"^19.3.0","react-aria-components":"^1.21.1","@testing-library/react":"^16.3.3","@internationalized/date":"^3.12.3","@testing-library/jest-dom":"^7.0.1","@testing-library/user-event":"^14.6.6"},"peerDependencies":{"react":">=18","react-dom":">=18","tailwindcss":"^4.0.0","tailwind-merge":"^3.0.0","react-aria-components":"^1.19.0","@internationalized/date":"^3.12.2"},"peerDependenciesMeta":{"react":{"optional":true},"react-dom":{"optional":true},"tailwindcss":{"optional":true},"tailwind-merge":{"optional":true},"react-aria-components":{"optional":true},"@internationalized/date":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/publisher_0.4.5_1789797558450_0.48886481880986166","host":"s3://npm-registry-packages-npm-production"}},"0.7.0":{"_id":"@clossys/publisher@0.7.0","bin":{"publisher-preview":"dist/preview/cli.js","publisher-rate-check":"dist/verified-publication-rate-cli.js","publisher-media-check":"dist/media/cli.js","publisher-record-check":"dist/record/cli.js","publisher-web-route-check":"dist/web/checkWebRoutesCli.js"},"bugs":{"url":"https://github.com/clossys/foundry/issues"},"dist":{"shasum":"62ae6907f746d8346666bdca5e4343e3371eac1f","tarball":"https://registry.npmjs.org/@clossys/publisher/-/publisher-0.7.0.tgz","fileCount":676,"integrity":"sha512-UVugXPBE+7MXHjWFFvflMr5hYNwqQGL5yTylOGR4SV/9URblBGqBLjaPeQOtsG/4F2IzCE9PcL9OHCa7LSIHDA==","signatures":[{"sig":"MEYCIQCuN3yL9qP0+8jrEJJC/ZsE3KEMWX5aNSD3ZHgozBNpIgIhAPHpZKy/5HP7vGCEQk8jPNQqQrEfG8ECZ3ZiEvv06Ujh","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD7wSbvZadF7G/TxXHb4MpqhG1UFXTyVfSycD9Q8/22FgIgNgWSZud4/Hv7w/Dzw/JqhZJC8dD4kGXRr8HykwEHTRc="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@clossys%2fpublisher@0.7.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":3019197},"name":"@clossys/publisher","type":"module","author":{"name":"Clossys"},"engines":{"node":">=20"},"exports":{"./web":{"types":"./dist/web/index.d.ts","import":"./dist/web/index.js","react-server":"./dist/web/server.js"},"./core":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js"},"./pack":{"types":"./dist/pack/index.d.ts","import":"./dist/pack/index.js"},"./email":{"types":"./dist/email/index.d.ts","import":"./dist/email/index.js"},"./image":{"types":"./dist/image/index.d.ts","import":"./dist/image/index.js"},"./media":{"types":"./dist/media/index.d.ts","import":"./dist/media/index.js"},"./print":{"types":"./dist/print/index.d.ts","import":"./dist/print/index.js"},"./record":{"types":"./dist/record/index.d.ts","import":"./dist/record/index.js"},"./slides":{"types":"./dist/slides/index.d.ts","import":"./dist/slides/index.js"},"./document":{"types":"./dist/document/index.d.ts","import":"./dist/document/index.js"},"./surfaces":{"types":"./dist/surfaces/index.d.ts","import":"./dist/surfaces/index.js"},"./materials":{"types":"./dist/materials/index.d.ts","import":"./dist/materials/index.js"},"./templates":{"types":"./dist/templates/index.d.ts","import":"./dist/templates/index.js"},"./assessment":{"types":"./dist/verified-publication-rate.d.ts","import":"./dist/verified-publication-rate.js"}},"foundry":{"fit":"fit-signals.json","feeds":[{"path":"clossys/publisher/surfaces/","artifact":"surface-documents"}],"needs":[{"artifact":"strategy-brief","producerRole":"@clossys/strategist"},{"artifact":"token-contract-and-brand-overlay-binding","producerRole":"@clossys/designer"},{"artifact":"copy-registry","producerRole":"@clossys/writer"},{"artifact":"keep-verdict","producerRole":"@clossys/customer"}],"solves":[{"metric":"verified publication rate","problem":"publisher-verified-release","evidence":"designed","proofCase":"rate-violated","statement":"We're not sure what's actually live, or that it matches what we approved.","capability":"sealing-and-the-publication-record"}],"outputs":["clossys/publisher/pack.json","clossys/publisher/surfaces/"],"assessment":{"bin":"publisher-rate-check","invocation":"single-json-input"},"capabilities":[{"id":"v0-launch-pack-planning-and-inventory","v0":true,"inputs":[{"artifact":"strategy-brief","producerRole":"@clossys/strategist"},{"artifact":"brand-kit-assembly","producerRole":"@clossys/designer"},{"artifact":"messaging-kit","producerRole":"@clossys/writer"}],"outputs":["clossys/publisher/pack.json"],"maturity":"planned","proofCase":null,"worldClass":"Every new product gets one declared inventory (clossys/publisher/pack.json, #1204) of what the v0 Launch pack owes it and what state each item is actually in — status, condition, version, source pins — never assembled ad hoc per product. Today: `src/pack/` ships the manifest contract (#1204) — `validatePackManifest` (schema, needs-graph, and lifecycle-vocabulary validation), `computePackReadiness`/`planPackOrder`/`sealableItemIds` (readiness and sealing order derived from the needs graph), and `detectExistingPackItems`/`foundPackItem` (adopt-don't-override detection with sha256 fingerprints) — as library functions over `PackManifest`; no CLI yet assembles or checks a real product's `clossys/publisher/pack.json` from them. No qualification case exercises this capability yet, so it is `planned` with no `proofCase` (#1272).","subQuestion":"What does this product owe its v0 Launch pack, and what already exists versus is still needed?","businessLifecycleStage":"launch"},{"id":"surface-documents","v0":true,"inputs":[{"artifact":"copy-registry","producerRole":"@clossys/writer"},{"artifact":"components-and-blocks","producerRole":"@clossys/designer"}],"outputs":["clossys/publisher/surfaces/"],"maturity":"planned","proofCase":null,"worldClass":"Every published surface is one validated SurfaceDocument resolving to a registered template and a real channel location — never a page composed ad hoc outside the document. Today: `validateSurfaceDocument` and `resolveSurfaceDocument` (`src/core/`) validate and resolve one document as library functions, and the shipped site template (`templates/site/app/*/page.tsx`) reads `clossys/publisher/surfaces/*.json` at build time, but no bin yet validates a consumer's own surface documents. No qualification case exercises this capability yet, so it is `planned` with no `proofCase` (#1272).","subQuestion":"Is every audience-facing surface declared once as a validated document, with its template and locations resolved?","businessLifecycleStage":"launch"},{"id":"channel-rendering","v0":true,"inputs":[{"artifact":"token-contract-and-brand-overlay-binding","producerRole":"@clossys/designer"}],"outputs":["clossys/publisher/.generated/render-report.json"],"maturity":"built","proofCase":"preview-rendered","worldClass":"A surface document renders byte-identically to its golden fixture on every channel it declares, checked, never eyeballed per release.","subQuestion":"Does every declared surface render correctly to its channel — web, email, image, print, document, or slides?","businessLifecycleStage":"launch"},{"id":"templates-and-channel-specs","v0":true,"inputs":[],"outputs":["clossys/publisher/templates.json"],"maturity":"planned","proofCase":null,"worldClass":"A channel template is data — a declared block sequence and spec — never an arbitrary per-page build function, per #1207's full set (overview, deck, email kit, social profile, video-call backgrounds). Today: `src/templates/` ships `overviewTemplate.ts`, `deckTemplate.ts`, `emailSignature.ts` (`buildEmailSignatureHtml`/`buildEmailSignatureText`), and `channelSpecs.ts` (`SOCIAL_CHANNEL_SPECS`, `OG_SHARE_CARD_SPEC`, `VIDEO_CALL_BACKGROUND_SPECS`, `staleChannelSpecEntries`) alongside the earlier web page templates (#1207); nothing yet assembles them into the declared `clossys/publisher/templates.json` output or checks them from a bin. `publisher-preview` renders from these templates and specs using built-in fixtures, but checks nothing about them. No qualification case exercises this capability yet, so it is `planned` with no `proofCase` (#1272).","subQuestion":"Is every channel's template and spec declared once and reused, never improvised per page?","businessLifecycleStage":"build"},{"id":"materials-site","v0":true,"inputs":[{"artifact":"messaging-kit","producerRole":"@clossys/writer"},{"artifact":"brand-kit-assembly","producerRole":"@clossys/designer"}],"outputs":["clossys/publisher/materials/index.json"],"maturity":"partial","proofCase":"preview-rendered","worldClass":"One assembled, current internal index (#1206) carries every version of the company overview (short/medium/long) and pitch deck (HTML slides with audience variants), each with a print stylesheet, opened locally or handed to a client — never a scatter of one-off files assembled by hand. Not a publicly deployed site: that is #1208's separate apps/site template. Today: `src/materials/` ships `renderMaterialsIndexHtml`, `renderPitchDeckHtml`, `selectAudienceVariant`, and `materialsPrintStylesheet` (#1206), but `renderMaterialsIndexHtml` takes its entries as a plain argument — nothing yet reads a product's own `clossys/publisher/pack.json` to derive them, or writes the declared `clossys/publisher/materials/index.json` output from a bin.","subQuestion":"Is there one internal, locally opened materials index a client can browse every version of?","businessLifecycleStage":"launch"},{"id":"channel-kits","v0":true,"inputs":[{"artifact":"messaging-kit","producerRole":"@clossys/writer"},{"artifact":"logo-and-identity-files","producerRole":"@clossys/designer"}],"outputs":["clossys/publisher/channel-kits.json"],"maturity":"planned","proofCase":null,"worldClass":"Every channel (email signatures, social profiles, video-call backgrounds, per #1207) has one governed kit of checked assets and copy slots at the exact sizes and roles that channel requires — never assembled by hand per send. Today: `buildEmailSignatureHtml`/`buildEmailSignatureText` and the channel spec registry (`src/templates/`) exist, and `publisher-preview` renders an email signature and social and video-call cards from built-in fixtures, but nothing assembles or checks a governed per-channel kit or writes the declared `clossys/publisher/channel-kits.json`. No qualification case exercises this capability yet, so it is `planned` with no `proofCase` (#1272).","subQuestion":"Is there one governed kit — assets, copy slots, specs — per channel a client can hand to that channel's own tool?","businessLifecycleStage":"launch"},{"id":"asset-roster-and-coverage","v0":true,"inputs":[],"outputs":["clossys/publisher/asset-roster.json"],"maturity":"built","proofCase":"media-satisfied","worldClass":"Every image/video asset a document binds resolves to a registered roster entry (favicon, OG, Twitter, email-safe, app icons…) at its required pixel size with alt text — checked both directions, never a broken or orphaned reference.","subQuestion":"Does every asset a document references resolve to a registered, checked entry at its required role and size?","businessLifecycleStage":"build"},{"id":"route-and-visibility-governance","v0":true,"inputs":[],"outputs":["clossys/publisher/routes.json"],"maturity":"partial","proofCase":"web-routes-clean","worldClass":"Every route composes only through a registered SurfaceDocument template -- never Designer blocks assembled directly in a route file -- and declares visibility as `internal` or `public` (#1204; supersedes the earlier public/unlisted/access-protected model #1206 retired); a route serving an internal-only item is refused, never silently served. Today: `PACK_VISIBILITIES` (`internal`/`public`) and `checkMaterialsVisibility` declare and check visibility at the materials level (#1206), but `checkWebRoutes`/`publisher-web-route-check` still checks only the route-to-template mapping -- no route-level visibility declaration or internal-route refusal runs yet.","subQuestion":"Does every publishing web route resolve to a registered template, with visibility declared as internal or public rather than inferred, and internal-only routes refused from serving?","businessLifecycleStage":"launch"},{"id":"site-template","v0":true,"inputs":[{"artifact":"route-and-visibility-governance","producerRole":"@clossys/publisher"}],"outputs":["clossys/publisher/site.json"],"maturity":"planned","proofCase":null,"worldClass":"An apps/site template (#1208) renders Publisher's web views and Designer's theme, reading copy, tokens, and assets from clossys/ at build time and never copying them — routes for landing, about, contact, privacy/terms, 404, metadata, sitemap, robots, all included. Today: `templates/site/` ships all of the above (#1208), plus `web-route-manifest.json` for `publisher-web-route-check`, but is template content copied by Launcher (#1215) into a consumer repository, not part of this package's own npm workspace — its own README says plainly it is \"not built, typechecked, or tested\" by this repository's `npm run build`/`npm run typecheck`/`npm test`, and Launcher's own apply step (#1215) is still a follow-up. No qualification case exercises this capability yet, so it is `planned` with no `proofCase` (#1272).","subQuestion":"Does a product repository get a working marketing site wired to its team's own records from the first commit?","businessLifecycleStage":"launch"},{"id":"sealing-and-the-publication-record","v0":true,"inputs":[{"artifact":"keep-verdict","producerRole":"@clossys/customer"}],"outputs":["clossys/publisher/record.json"],"maturity":"built","proofCase":"record-append-only-clean","worldClass":"Every publication event is appended once to an immutable record — never overwritten, never silently lost — and every fact cited in what was sealed is re-checked against the current source value at seal time, with drift reported, never assumed absent. A surface seals only once Customer has recorded a keep.","subQuestion":"Once published, is the exact shipped result sealed into an immutable, append-only record, with every cited fact still current?","businessLifecycleStage":"deliver"},{"id":"live-parity","v0":false,"inputs":[{"artifact":"sealing-and-the-publication-record","producerRole":"@clossys/publisher"}],"outputs":["clossys/publisher/.generated/live-parity-report.json"],"maturity":"planned","proofCase":null,"worldClass":"`verify` (#1209) proves the live site actually serves what was sealed -- head, OG, and metadata for public surfaces -- independently re-reading the live URL, never trusting the deploy provider's own \"success\" status. Scoped to public surfaces only, per #1206 (materials visibility is declared per item; an internal item is never expected to be live-servable at all). Today: the reconciliation-loop contracts (an independent witness compared against a ledger) exist; nothing yet re-reads a live production URL. No qualification case exercises this capability yet, so it is `planned` with no `proofCase` (#1272).","subQuestion":"Does the production URL actually serve what was sealed — head, OG, metadata, and the materials index — independent of the hosting provider's own success signal?","businessLifecycleStage":"operate"}]},"imports":{"#publisher-web-views":{"types":"./dist/web/views/index.d.ts","default":"./dist/web/views/index.js","react-server":"./dist/web/views/server.js"}},"license":"MIT","private":false,"scripts":{"test":"vitest run","build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.json --noEmit","prepublishOnly":"node ../../scripts/check-name-collision.mjs . && npm run build"},"version":"0.7.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:8c63dd08-c972-407c-9371-363bd92ac813"}},"homepage":"https://github.com/clossys/foundry/tree/main/packages/publisher#readme","keywords":["publisher","composition","render","web","email","print","image","slides","ledger","append-only","attribution","drift","content-addressed","typescript"],"repository":{"url":"git+https://github.com/clossys/foundry.git","type":"git","directory":"packages/publisher"},"_npmVersion":"11.17.0","description":"The publisher role: did we put it out to an audience, and can we prove what shipped? Surface composition, media registries, and channel renderers for web, email, print, images, and slides, plus a product-neutral structured-document contract, under `./core","directories":{},"maintainers":[{"name":"thecalvinhung","email":"hello@clossys.com"}],"sideEffects":false,"_nodeVersion":"24.19.0","dependencies":{"@clossys/writer":"^0.4.0","@clossys/designer":"^0.6.0","@clossys/controller":"~0.9.14"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^26.1.0","react":"^19.3.0","vitest":"^5.0.1","react-dom":"^19.3.0","typescript":"~6.0.0","@types/node":"^26.5.1","tailwindcss":"^4.3.3","@types/react":"^19.3.0","tailwind-merge":"^3.7.0","@types/react-dom":"^19.3.0","react-aria-components":"^1.21.1","@testing-library/react":"^16.3.3","@internationalized/date":"^3.12.3","@testing-library/jest-dom":"^7.0.1","@testing-library/user-event":"^14.6.7"},"peerDependencies":{"react":">=18","react-dom":">=18","tailwindcss":"^4.0.0","tailwind-merge":"^3.7.0","react-aria-components":"^1.19.0","@internationalized/date":"^3.12.2"},"peerDependenciesMeta":{"react":{"optional":true},"react-dom":{"optional":true},"tailwindcss":{"optional":true},"tailwind-merge":{"optional":true},"react-aria-components":{"optional":true},"@internationalized/date":{"optional":true}},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/publisher_0.7.0_1790283799907_0.6061144009351411"}}},"time":{"created":"2026-08-31T21:02:06.085Z","modified":"2026-09-24T21:03:20.461Z","0.1.10":"2026-08-31T21:02:06.589Z","0.2.1":"2026-09-01T12:02:36.037Z","0.4.1":"2026-09-04T03:02:22.353Z","0.4.3":"2026-09-14T13:09:06.994Z","0.4.4":"2026-09-16T05:32:25.010Z","0.4.5":"2026-09-19T05:59:18.552Z","0.7.0":"2026-09-24T21:03:20.007Z"},"bugs":{"url":"https://github.com/clossys/foundry/issues"},"author":{"name":"Clossys"},"license":"MIT","homepage":"https://github.com/clossys/foundry/tree/main/packages/publisher#readme","keywords":["publisher","composition","render","web","email","print","image","slides","ledger","append-only","attribution","drift","content-addressed","typescript"],"repository":{"url":"git+https://github.com/clossys/foundry.git","type":"git","directory":"packages/publisher"},"description":"The publisher role: did we put it out to an audience, and can we prove what shipped? Surface composition, media registries, and channel renderers for web, email, print, images, and slides, plus a product-neutral structured-document contract, under `./core","maintainers":[{"name":"thecalvinhung","email":"hello@clossys.com"}],"readme":"# @clossys/publisher\n\n**The publisher role — did we put it out to an audience, and can we prove\nwhat shipped?** This package is named for the job, not the artifact: it is\nrecut from two donor packages, `@example/surface` (the composer half\n— eight subpaths, unchanged) and `@example/ledger` (the record half,\nnow the `./record` subpath), per\n[decision 10](../../docs/DECISIONS.md#10-recutting-the-expression-surface-into-role-shaped-packages).\nThe vocabulary inside each half is unchanged: renaming the role does not\nrename what it composes or what it records.\n\n```bash\nnpm install @clossys/publisher\n```\n\nThis package is published to `https://registry.npmjs.org` with public\naccess; installing it requires no authentication.\n\n## Verified publication rate\n\nIndependent consumer evidence shows the position's owned metric meets its\nsetpoint over the declared review cadence. The owned metric is `verified\npublication rate`, computed by `assessVerifiedPublicationRate()`. An empty\nevaluated set is `indeterminate`, never a perfect rate of 1.\n`publisher-media-check` and `publisher-record-check` remain the gates they\nare; neither is this rate. The record half still records and does not\njudge. This package does not measure consumer evidence and does not close\nthe loop. A green run of this package's tests is not a close.\n\n```ts\nimport { assessVerifiedPublicationRate } from \"@clossys/publisher/assessment\";\n\nconst report = assessVerifiedPublicationRate(input);\n```\n\n```bash\npublisher-rate-check assessment.json\n```\n\nThe command prints JSON and exits `0` for satisfied, `1` for violated, and\n`2` for indeterminate, unreadable, or invalid input.\n\nThis package declares that command as its first-day assessment surface in\nits own manifest:\n\n```json\n\"foundry\": { \"assessment\": { \"bin\": \"publisher-rate-check\", \"invocation\": \"single-json-input\" } }\n```\n\nOnboarding discovers that declaration from the installed manifest and never\ninfers a surface. `publisher-media-check` and `publisher-record-check`\nremain gates and are not the assessment surface. Publisher is not a\nrequired first-day role; Advisor remains the only required first-day\nassessment.\n\n## The job\n\n**Aim** — every audience-facing release is accounted for in an immutable\npublication record, and nothing in the record names something that never\nshipped.\n\nPublisher is a **reconciliation** role, not a gate over a source tree. It\ndoes not scan prose or stylesheets and return a verdict the way `writer-check`\nor `designer-fold-check` do. It compares **two records that can disagree**:\nwhat an independent observer reports was released to an audience, and what\nthe append-only ledger says was published.\n\n## Escape\n\nAn **escape** is either:\n\n1. **Unrecorded release** — something reached an audience with no matching\n   immutable publication record.\n2. **Phantom record** — the ledger names a publication that never shipped, or\n   names bytes or strategy citations that no longer match what actually went\n   out.\n\nA green run of this package's own tests, or a ledger authored from the same\nknowledge that would check it, is **not** evidence of zero escapes — that is\nthe house failure pattern this role exists to prevent.\n\n## The metric\n\nThe charter metric is **`verified publication rate`**: due publication\nintents where independent observers report both audience release and a\nmatching immutable record, divided by all due intents actually evaluated. An\nempty evaluated set is **indeterminate**, never a perfect rate of 1. See\n[Verified publication rate](#verified-publication-rate) above for\n`assessVerifiedPublicationRate()` and `publisher-rate-check`.\n\n`publisher-media-check` reports registry coverage (referenced asset ids vs\nregistered entries). `publisher-record-check` reports fact drift, append-only\nintegrity, and join-key completeness on a ledger a consumer already holds.\nThose counts are **inputs** to reconciliation; none of them is the charter\nrate, and none of them alone proves the escape surface is closed.\n\n## Loop\n\n- **sense** — enumerate due publication intents; collect independent\n  observations of audience release; hold the append-only ledger and the fact\n  snapshot `publisher-record-check` compares against.\n- **judge** — `assessVerifiedPublicationRate()` for the charter metric;\n  `publisher-record-check` for drift, append-only violations, and join-key\n  gaps on declared ledger entries.\n- **act** — append an immutable `PublicationEntry` when something ships;\n  remediate drift findings and missing join keys; retract or supersede\n  entries when a release is withdrawn.\n- **learn** — repeated `indeterminate` or `violated` assessments, or drift\n  findings that keep reopening on the same intent, signal that the publish\n  path is not emitting a record an observer can match — not that the gate\n  needs softer rules.\n\n**Measurer today:** `publisher-rate-check` (charter metric from\nconsumer-supplied observations) and `publisher-record-check` (ledger gates).\n**Blocker:** nothing in this package yet produces a **second**,\npublish-path-emitted view of what actually shipped for an automatic\nreconcile — issue #502 tracks that gap. Until two independent records exist,\nevery rate reads **unmeasured** rather than zero; treat silence as\nindeterminate, not health.\n\n## Close condition\n\nThis loop closes when **`verified publication rate` reads satisfied** from\n**independent consumer evidence** over a non-empty set of due intents — each\nintent independently observed with both `audienceReleased` and\n`matchingImmutableRecord` — and when reconciliation machinery can compare\nthat evidence to a publish-path-emitted ledger without hand-copying the same\nfacts into both sides.\n\nIt does **not** close when: no observer runs the assessment; the evaluated\nset is empty; `publisher` observes itself; or the ledger and the \"what\nshipped\" view share one author. A consumer wiring `publisher-record-check` in\nCI proves ledger **shape** integrity, not that the escape surface is zero.\n\nThat close is **not claimed today** — the reconciler gap in #502 is the long\npole. This README states the contract so the gap stays visible instead of\nreading as a perfect zero escape rate from outside.\n\n## Public entry points\n\nUse explicit subpaths:\n\n- `@clossys/publisher/assessment` — `assessVerifiedPublicationRate`, the charter close metric. Empty evaluated set is indeterminate, never 1. The CLI is `publisher-rate-check`.\n- `@clossys/publisher/core` — canonical `SurfaceDocument` contract, validation, copy/media resolution, and output manifests.\n- `@clossys/publisher/media` — media registry, reader, and coverage check.\n- `@clossys/publisher/web` — web composition, head metadata, and the dedicated\n  resolved-model `SectionedView` renderer. Under React's\n  `react-server` export condition it resolves a server-safe target with the\n  same runtime export names and Designer's server-only component barrels;\n  ordinary imports retain the interactive React Aria FAQ.\n- `@clossys/publisher/document` — the product-neutral structured-document contract (sections, paragraphs, lists, tables, callouts, safe links) and its renderer.\n- `@clossys/publisher/email`, `/print`, `/image`, `/slides` — channel renderers.\n- `@clossys/publisher/record` — the append-only, content-addressed publication ledger and its drift checker. See \"`record` — the append-only publication ledger,\" below.\n- `@clossys/publisher/pack` — the v0 Launch pack manifest contract: types, schema validation, needs-graph readiness, and adopt-don't-override detection. See \"The pack,\" below.\n- `@clossys/publisher/surfaces` — the one-owner-per-file contract for surface documents under `clossys/publisher/surfaces/`. See \"Surface documents move to Publisher,\" below.\n- `@clossys/publisher/materials` — the materials mini-site (overviews, pitch decks, audience variants). See \"Materials site,\" below.\n- `@clossys/publisher/templates` — the pack's default templates and the channel spec registry. See \"Templates and channel specs,\" below.\n\nThe package has no root export. `core` is deliberately framework-agnostic;\nthe web and document subpaths have optional React peers, while `web` also\ndeclares Designer's optional runtime peers directly so a public-registry\nconsumer receives a complete, inspectable peer contract. Non-web renderers do\nnot require them at runtime. `record` is pure and has no peer dependencies of\nits own.\n\nThe web condition changes only the implementation selected for server\nrendering, not the API. `MarketingView` keeps the same props and regional\nlayout; its server target uses Designer's native `details`/`summary` FAQ while\nthe ordinary target keeps Designer's React Aria FAQ. `AuthView`, `ErrorView`,\n`CaptureView`, `CollectionView`, `DocumentView`, the renderer functions,\ntemplate helpers, error class, and all runtime export names are present in\nboth targets.\n\n## Why `publisher` is one package, not two\n\nComposition without a record is unprovable, and every time the publisher\nruns, the record runs — there is no publish that legitimately skips it. That\nargues for one install and one version, which one package with a `./record`\nsubpath delivers.\n\nThe measurement that originally argued for two separate packages is\naccommodated rather than overturned: **the record shares no code with the\ncomposer and does not import it**, so the two import surfaces stay genuinely\nseparate under one version. Fusing the *packaging* was never the same as\nfusing the *dependency graph*, and only the second would have cost anything —\nsee \"`record` — the append-only publication ledger,\" below, for the half\nthat proves it: a publication record is a DOCUMENT the composer never\nimports.\n\n## Scope: this package renders and validates. It does not compose.\n\nStated plainly because evaluating it and discovering this costs real time:\n**there is no compose step here, and there will not be one.** Nothing in this\npackage takes an intent, a brief, or a content plan and *selects* a template\nto put it in. Every export is a `render*`, a `resolve*`, or a `validate*`.\n\nThe boundary is: a caller authors a `SurfaceDocument` — naming its template\nexplicitly — and this package validates it, resolves its copy and assets\nagainst real approved registries, renders it for a channel, and reports what\nit did. Deciding *which* document to build, from *what* intent, is the\nconsumer's job and stays there.\n\nThis is a scope decision, not a gap awaiting a contributor. Template selection\nis where product judgement lives: which page shape serves which audience at\nwhich moment is exactly the reasoning a shared package cannot hold for someone\nelse, and a `compose(intent, target)` that guessed would be wrong in a way\nthat is expensive to discover and impossible to override cleanly. Drawing the\nline at \"you name the template, we guarantee everything after it\" is what lets\nthis package promise something real — validated content, resolved copy\nprovenance, deterministic output — instead of promising judgement it does not\nhave.\n\nWhat that leaves the consumer owning: intent-to-template selection, and any\ncatalog of their own templates worth selecting from. What it leaves this\npackage owning: everything from a named template onward.\n\nMaking the *set* of selectable web templates extensible is a separate and\ngenuinely open question — see the template-registry proposal in this\nrepository's issues. That is about who may add a template, not about who picks\none; extensibility does not imply composition.\n\n## SurfaceDocument and renderer boundary\n\n`SurfaceDocument` is the canonical authored contract. It replaces the\nstring-bearing `ComposeDocument` with `CopyRef` values for audience-facing\nslots and metadata: web titles/descriptions, email subjects/preheaders, image\nalt text, and slide notes. A binding is exactly one of `copy`, `node`, or\n`assetId`; `node` preserves an explicit consumer-provided interactive/rich UI\nnode without pretending it is copy. Use `resolveSurfaceDocument(surface,\ncopyResolver)` at render time: it\nvalidates the canonical document, resolves every required `CopyRef` against a\nreal approved registry, returns renderer-facing data plus the full resolution\nprovenance, and fails closed for invalid, missing, or unsupported node\nbindings. New consumer code authors `SurfaceDocument`; `ComposeDocument` is\nthe renderer-facing shape produced by this package, not a consumer migration\nAPI.\n\nWeb and email templates use `FlowLayoutSpec`, which contains ordered keys and\nrequiredness only. Print, slides, and image surfaces use `CanvasLayoutSpec`\nwith frames and element kinds. This prevents flowed surfaces from carrying\nfictional canvas geometry.\n\n### Repeating-group bindings\n\nA `SurfaceDocument`'s `bindings` array accepts two shapes: a\n`SurfaceSlotBinding` (one slot, exactly one of `copy`/`node`/`assetId`, as\nabove) or a `SurfaceRepeatingSlotBinding` — the same slot, bound to an\n**ordered list** of items instead of a single source. This closes part of\nissue #166: a template can commit a slot to holding N items (a capability\ngrid, a stat band, a testimonial list) where N is a run-time fact the\ntemplate's own layout cannot encode, since `FlowLayoutSpec`/`CanvasLayoutSpec`\nname a slot once, not \"this slot, repeated.\"\n\nA repeating binding still names one explicit slot the consumer already\ndecided exists — it does not select a template or invent a slot, the same\nboundary every other binding in this package holds to (see \"Scope,\" above).\nEach item in `items` independently obeys the identical exactly-one-of\ndiscipline a single binding does. For a one-value item, use `copy`, `node`,\nor `assetId` as before. For ordinary multi-field editorial content, use one\nnamed `fields` map instead; each field is exactly one `copy` or `assetId`\nbinding. A field may never be a `node`, so structured copy cannot bypass the\nregistry, voice checks, locale selection, or output provenance.\n\n```ts\nimport type { SurfaceDocument } from \"@clossys/publisher/core\";\n\nconst acmeCapabilities: SurfaceDocument[\"bindings\"][number] = {\n  slot: \"capabilities\",\n  items: [\n    { copy: { id: \"acme.capability.one\" } },\n    { copy: { id: \"acme.capability.two\" } },\n    { assetId: \"acme.capability.icon.three\" },\n  ],\n};\n```\n\nTemplates opt into structured items by declaring their accepted field names\nand requiredness in `repeatingSlots`. At render time, an unknown field, a\nmissing required field, a malformed field map, or a legacy one-value item\nagainst a structured slot fails closed. A field map against a slot that did\nnot declare fields fails closed too. This keeps the template—not a caller's\nad hoc object—the authority for the repeating item's shape.\n\n`items` may be an empty array. That is a deliberate choice, not an\noversight: this package cannot tell \"the consumer configured zero of these\non purpose\" (a legitimately-empty testimonial list) apart from \"something\nupstream failed to populate this,\" so it validates an explicit `items: []`\nas clean rather than guessing. See `types.ts`'s `SurfaceRepeatingSlotBinding`\ndoc comment for the fuller reasoning.\n\n`resolveSurfaceDocument` resolves a repeating binding's items in order and\nreturns them on `ResolvedSurfaceDocument.groups` — an array of\n`{ slot, items: [{ index, value?, node?, assetId?, fields? }, ...] }` — rather than\nfolding them into the legacy `ComposeDocument.bindings` shape, which has no\nway to carry more than one source per slot. `groups` is omitted entirely\n(not an empty array) on a document with no repeating binding, so an existing\nsingle-binding-only `SurfaceDocument` resolves identically to before this\nexisted. A bad item — an unresolvable `CopyRef`, same as any single\nbinding's — fails the whole `resolveSurfaceDocument` call with a message\nnaming the specific item (`bindings.N.items.M`), the same fail-closed,\nall-or-nothing contract this function already holds for a single binding;\nit does not invent a second, partial-success mode just because the content\nis array-shaped. Per-item copy resolutions flow into the same\n`resolutions`/`collectCopyProvenance` provenance path a single binding's\ndoes, so a repeating-group slot shows up in manifest provenance per item,\nnot just per slot — see `output-manifest.ts`. A structured field resolved\nfrom `assetId` instead carries registry-validated asset evidence into the\ntarget renderer; it has no copy provenance because it is not copy.\nAt the public `RenderWebOptions.groups` boundary, items must retain the\nresolver's contiguous source order: item `index` is exactly its zero-based\narray position. This prevents a direct caller from silently reordering,\nduplicating, or sparsifying the authored group after resolution.\n\n### Structured repeating-item migration and planned semver boundary\n\nPublisher `0.1.10` exposed the FAQ repeat as a legacy one-value/node-shaped\ncontract. That shape cannot represent two separately governed editorial\nfields, and it is not compatible with the structured FAQ contract above.\nMigrate each FAQ item from a caller-authored node to explicit approved-copy\nfields:\n\n```ts\n// Before: legacy node-shaped FAQ item (do not carry this forward).\n{ node: { question: \"...\", answer: \"...\" } }\n\n// After: every audience-facing field is a CopyRef.\n{\n  fields: {\n    question: { copy: ref(\"acme.faq.question\") },\n    answer: { copy: ref(\"acme.faq.answer\") },\n  },\n}\n```\n\nThis is a breaking contract correction, so Publisher source is now the planned\n`0.2.0` successor; a `^0.1.x` range must not satisfy it. The source is neither\nqualified nor published: an exact-head candidate and public dependency\nverification remain required before any release action.\n\n### Choosing a shipped view — closed kinds, `defineWebTemplate` for the rest\n\nName a shipped template when its slots cover the page:\n\n- **`MarketingView`** — pre-auth marketing landing (hero, features, optional\n  FAQ, CTA).\n- **`SectionedView`** — long public pages whose sections are exactly the\n  closed six kinds (`hero`, `feature-grid`, `faq`, `ordered-step-sequence`,\n  `status-list`, `stat-grid`).\n- **`AuthView`** / **`ErrorView`** — authentication and error shells.\n\nIf a required band is not a slot on any shipped template and not one of the\nsix `SectionedView` kinds, **do not flatten** it into a one-item\n`feature-grid` or any other shipped kind — that produces a document that\nvalidates while the page is wrong. Register `defineWebTemplate` in the\nconsumer instead; its `build` function maps resolved slots to Designer\nblocks. That registry path is the public custom-page extension.\n\nComposing Designer blocks directly in an unregistered route file can work as\na one-off, but it is a workaround: it bypasses the template registry,\n`validateSurfaceDocument`/`resolveSurfaceDocument`, and copy provenance for\nthat page shape. Prefer `defineWebTemplate` + `createWebRenderer` so the\npage stays provable.\n\n### Pre-auth marketing pages — `MarketingView` first\n\nFor a pre-auth marketing landing page, use `MarketingView` (header, Hero with\noptional `heroMedia` and `heroActions`, feature grid, optional FAQ, CTA band,\nshell) and fill its slots. In the first viewport, ship exactly one primary CTA\nin `heroActions`; `heroMedia` must be the product surface or original art, not\ndecorative stock imagery. See\n[`MarketingView`](#marketingview--the-flowed-marketing-template) below. When\nthe page needs a Designer block that is not a MarketingView slot, import that\nnamed block in your page module. Do not flatten into `SectionedView` —\nthat document assembler is not the pre-auth path.\n\n### `SectionedViewDocument` — Designer-independent long-page core\n\n`@clossys/publisher/core` now owns the closed, data-only source model for a\nlong public site page: `SectionedViewDocument`. It requires one or more\nordered sections with unique lowercase fragment-safe ids and one of six\nnamed kinds: `hero`, `feature-grid`, `faq`, `ordered-step-sequence`,\n`status-list`, or `stat-grid`. Grounds are the closed `base`/`sunken`/`inverse` vocabulary;\nstatus values are the closed `available`/`partial`/`planned` readiness axis,\nwith a separate `not-offered` disposition for deliberate non-capabilities.\nEvery audience-facing label, heading, description, question, answer, ordinal,\nand status label is a `CopyRef`. There are no React nodes, render callbacks,\nrouter fields, locale overrides, classes, styles, arbitrary colours, or\ncomposition escape hatches.\n\nThis is a closed wire model at runtime as well as in TypeScript: structural\nobjects use only enumerable own data properties (no inherited, symbol, hidden,\nor accessor fields), and every ordered section, item, and status-group array\nmust be dense. A `CopyRef` has only its non-empty `id`, optional non-empty\n`locale`, and an optional plain interpolation-value record whose values are\nstrings, numbers, or booleans. Malformed input is rejected before a custom\nresolver is called.\n\n`validateSectionedViewDocument` reports malformed or unknown structure;\n`resolveSectionedViewDocument` resolves every CopyRef depth-first in authored\norder and returns its ordinary `CopyResolution[]`. Pass that list directly to\n`collectCopyProvenance` or existing output-manifest helpers—there is no second\nprovenance format. A missing or empty resolution fails the entire document and\nnames the exact authored path. An unknown section kind is refused at validate\nand resolve time and names that kind — there is no implicit remap to\n`feature-grid` or any other shipped kind. This core stage intentionally imports neither\nReact nor Designer and does not render a web view. The grounded web renderer\nuses Designer `0.4.0`'s server-safe site-block API, including the separate\n`not-offered` disposition and its caller-localized `labels.dispositions` map,\nthe non-hero `eyebrow` slot, the per-row `detail` slot, and the flat `items`\nalternative to `StatusList`'s `groups`.\n\n`SectionedView` is now the dedicated web renderer after that Designer floor is\navailable. Resolve the CopyRef document first, retain its `resolutions` as the\nonly publication provenance, and pass the resolved model directly:\nHere `resolveCopy` is the consumer's approved `CopyResolver`.\n\n```tsx\nimport { resolveSectionedViewDocument } from \"@clossys/publisher/core\";\nimport { SectionedView } from \"@clossys/publisher/web\";\n\nconst resolved = resolveSectionedViewDocument({\n  id: \"acme-home\",\n  sections: [{\n    id: \"welcome\",\n    kind: \"hero\",\n    ground: \"base\",\n    heading: { id: \"acme.home.heading\" },\n    description: { id: \"acme.home.description\" },\n  }],\n}, resolveCopy);\n\nconst page = <SectionedView document={resolved} />;\n// resolved.resolutions feeds collectCopyProvenance/output-manifest helpers.\n```\n\n#### Optional fields the document may also carry\n\nFour slots are optional and additive: a document written without them\nvalidates and renders exactly as it did before they existed.\n\n- **`eyebrow` on every section kind.** `hero` always had one; `feature-grid`,\n  `faq`, `ordered-step-sequence`, and `status-list` now carry the same\n  optional `CopyRef`, so authored eyebrow copy is no longer dropped at\n  conversion time.\n- **`actions` on the hero section.** An optional, non-empty list of\n  `{ id, label, href }`, where `label` is a `CopyRef` and `href` must be a\n  fragment, a one-origin path, an `http(s)` URL, or a `mailto:` link. It stays\n  data: there is no node, class, or handler slot, and the view renders\n  underlined `<a href>` elements into the Designer `Hero` block's existing\n  `actions` slot (not `Button` atoms — the wire model stays href-shaped).\n- **`media` on the hero section.** An optional `{ assetId, alt }` pair:\n  `alt` is a `CopyRef` (provenance-complete) and `assetId` resolves at render\n  time through `SectionedView`'s `resolveAssetId`, the same seam\n  `renderWebDocument` uses for `MarketingView`'s `heroMedia`. Presence\n  switches `Hero` to the two-column layout (`tablet:grid-cols-2`). Product\n  surface or original art only — not decorative stock.\n- **`stat-grid` sections.** A titled grid of metrics: each item carries\n  `label` and `value` `CopyRef`s plus optional `delta`, closed `trend`\n  (`up` | `down` | `neutral`, only with `delta`), and optional `description`.\n  The view maps each row onto Designer `Stat`; do not flatten metrics into\n  `feature-grid`.\n- **`detail` on a status-list item.** An optional `CopyRef` carrying that\n  row's own explanation, including the reasoning behind a `not-offered`\n  answer. It renders as a second description of the same row, so the\n  definition-list semantics stay intact.\n\n```tsx\nconst resolved = resolveSectionedViewDocument({\n  id: \"acme-trust\",\n  sections: [\n    {\n      id: \"welcome\",\n      kind: \"hero\",\n      ground: \"base\",\n      heading: { id: \"acme.trust.heading\" },\n      actions: [{ id: \"contact\", label: { id: \"acme.trust.contact\" }, href: \"/contact\" }],\n    },\n    {\n      id: \"posture\",\n      kind: \"status-list\",\n      ground: \"sunken\",\n      eyebrow: { id: \"acme.trust.eyebrow\" },\n      heading: { id: \"acme.trust.posture\" },\n      labels: {\n        available: { id: \"acme.status.available\" },\n        partial: { id: \"acme.status.partial\" },\n        planned: { id: \"acme.status.planned\" },\n        dispositions: { \"not-offered\": { id: \"acme.status.not-offered\" } },\n      },\n      groups: [{\n        id: \"core\",\n        heading: { id: \"acme.trust.core\" },\n        items: [{\n          id: \"audit\",\n          label: { id: \"acme.trust.audit.label\" },\n          detail: { id: \"acme.trust.audit.detail\" },\n          disposition: \"not-offered\",\n        }],\n      }],\n    },\n  ],\n}, resolveCopy);\n```\n\n#### At most one hero, and a status-list may go flat\n\nTwo contract rules relaxed additively, both keeping every previously-valid\ndocument valid and unchanged:\n\n- **A hero section is now optional, and may sit anywhere.** The document\n  previously required exactly one `hero` section and required it to be\n  first. It now permits zero or one — never more than one — in any position.\n  A document with a single leading hero, the shape every document authored\n  before this change already has, still validates and renders identically:\n  `SectionedView` already assigned `headingLevel={1}` only to a hero at\n  index 0 and `headingLevel={2}` to any other section, so a non-leading hero\n  renders as an `h2`, the same fixed-outline discipline the view has always\n  held. This unlocks a closing call-to-action band authored as its own\n  section, and a page with no hero section at all.\n- **A `status-list` section's `groups` is now optional, with a flat `items`\n  alternative.** Provide exactly one of `groups` (unchanged: one heading and\n  one definition list per group) or `items` (new: the same row shape with no\n  group at all) — the common shape for a short list with nothing to group.\n  It renders through Designer `0.4.0`'s `StatusList` `items` prop as a\n  single definition list with no group heading.\n\n```tsx\nconst resolved = resolveSectionedViewDocument({\n  id: \"acme-trust\",\n  sections: [\n    {\n      id: \"core\",\n      kind: \"status-list\",\n      ground: \"base\",\n      heading: { id: \"acme.trust.posture\" },\n      labels: {\n        available: { id: \"acme.status.available\" },\n        partial: { id: \"acme.status.partial\" },\n        planned: { id: \"acme.status.planned\" },\n        dispositions: { \"not-offered\": { id: \"acme.status.not-offered\" } },\n      },\n      items: [{ id: \"audit\", label: { id: \"acme.trust.audit.label\" }, disposition: \"not-offered\" }],\n    },\n    // A closing call-to-action band: a second hero, not first, rendered as h2.\n    { id: \"cta\", kind: \"hero\", ground: \"inverse\", heading: { id: \"acme.trust.cta\" } },\n  ],\n}, resolveCopy);\n```\n\n#### Mounting part of a page: the `landmark` prop\n\n`SectionedView` renders its own `main` landmark by default, which is right\nwhen the whole page is the document. A page that can express only some of its\nsections through this contract needs the other option, or its remaining\ncontent ends up outside the page's only `main`:\n\n```tsx\n<main>\n  <SectionedView document={resolved} landmark=\"none\" />\n  <ConsumerOwnedSection />\n</main>\n```\n\n`landmark=\"none\"` renders the same sections in a plain grouping element with\nno landmark role and no accessible name. Default behaviour is unchanged, and\nchoosing it makes the surrounding page responsible for supplying exactly one\n`main` landmark containing this output.\n\n#### Contract holds measured in issue #756\n\nThree 0.2.1 refusals stay deliberate after the additive fixes above; they are\nnot backlog slots waiting for a loosening pass.\n\n- **Required `labels` on every `status-list` section.** The section must\n  carry exactly `available`, `partial`, `planned`, and\n  `dispositions.not-offered` as `CopyRef`s. Designer `StatusList` renders\n  human-readable readiness names from this caller-localized map, so a surface\n  cannot mount a status list until it has merged reader-facing words for all\n  four axes. Omit the section when the page has no readiness posture to\n  report.\n- **Required `ordinal` as a `CopyRef` on each ordered step.** Ordinals\n  participate in copy provenance and locale like every other audience field;\n  a surface that paints step numbers visually still authors copy entries\n  (often `\"1\"`, `\"2\"`) rather than relying on presentation-only counters.\n- **Non-empty repeating arrays.** Every `items` array, status-list `groups`\n  array, group `items` array, and optional hero `actions` array must contain\n  at least one entry when present — an explicit `items: []` is refused\n  (`sectioned-view-items-shape` / `sectioned-view-status-items-shape`).\n  Unlike `MarketingView`'s repeating bindings, this document has no separate\n  \"slot omitted\" vs \"slot empty\" distinction at the section level: a section\n  kind in the wire model is a commitment to render that block with at least\n  one row. Express stated absence by omitting the section or by owning an\n  empty state outside this contract (`StructuredDocument`, host markup).\n\nThe ordinary and `react-server` `@clossys/publisher/web` exports are aligned.\nThe view owns section markup, source order, unique section ids, grounds, and\nthe h1/h2/h3 outline; Designer owns visual tokens and block internals. Per\n#708, Publisher does not own locale selection, routing, or document-level\n`html`, `lang`, or `dir` attributes: the host application supplies those\nboundaries around this renderer.\n\n`section-header` and `article-body` remain intentionally outside the closed\n`SectionedView` kinds: the former's action region and the latter's full\nstructured-document rendering still need a grounded view integration to stay\nfully data-shaped and provenance-complete without node slots. Compose those\nDesigner blocks in the consumer renderer or register `defineWebTemplate` for\nthe page band instead of flattening into `feature-grid` or `stat-grid`.\n\nOn its own, this is a repeating-group *binding* primitive only — it says\nnothing about which template actually consumes it. `web`'s `MarketingView`\ntemplate (below) is that consumer, closing the second and final half of\nissue #166.\n\n### `MarketingView` — the flowed marketing template\n\n`web`'s template registry (`listWebTemplateNames()`) knows three names:\n`AuthView`, `ErrorView`, and `MarketingView` — an ordinary flowed page with\na persistent header/footer, a hero, a feature grid, an optional FAQ list,\nand a closing call-to-action band. Like `AuthView`/`ErrorView`, it is one\nmore explicit, nameable `SurfaceDocument.template` value, not a mechanism\nthat picks one — see \"Scope,\" above: this package still does not compose.\n\n`MarketingView`'s fixed slot set: `brand` and `heroHeading` and\n`ctaHeading` are required flowed text slots; `heroEyebrow`,\n`heroDescription`, `heroActions`, `heroMedia` (an asset slot),\n`featuresHeading`, `featuresDescription`, `faqHeading`, `faqDescription`,\n`ctaDescription`, `ctaAction`, and `footerSecondary` are optional flowed\nslots; `features` (required) and `faq` (optional) are **repeating** slots,\neach bound via a `SurfaceRepeatingSlotBinding` and rendered through\n`@clossys/designer`'s `FeatureGrid`/`Faq` blocks respectively. An empty\nrepeating group (`items: []`) renders that section with zero entries —\nnever an error, the same \"empty is a deliberate, valid choice\" contract\n`SurfaceRepeatingSlotBinding` itself holds to, above; a `faq` binding that\nwas never authored at all omits the whole FAQ section instead, which is a\ndifferent, equally valid outcome (see `MarketingView`'s own `faq` prop doc\ncomment).\n\n`faq` declares two required structured fields: `question` and `answer`.\nEach resolves through the normal `CopyRef` path, so its locale, approved\nvoice, and provenance stay visible alongside the rest of the page. A FAQ\nitem authored as a caller-owned `node`, as a legacy single value, or with an\nunknown/missing field is refused rather than bypassing editorial governance.\n\n```ts\nimport type { SurfaceDocument } from \"@clossys/publisher/core\";\nimport { resolveSurfaceDocument } from \"@clossys/publisher/core\";\nimport { renderWebDocument } from \"@clossys/publisher/web\";\n\nconst ref = (id: string) => ({ id });\n\nconst acmeMarketingHome: SurfaceDocument = {\n  id: \"acme.marketing.home\",\n  channel: \"web\",\n  template: \"MarketingView\",\n  meta: { channel: \"web\", title: ref(\"acme.brand\"), description: ref(\"acme.hero.description\") },\n  bindings: [\n    { slot: \"brand\", copy: ref(\"acme.brand\") },\n    { slot: \"heroHeading\", copy: ref(\"acme.hero.heading\") },\n    { slot: \"heroDescription\", copy: ref(\"acme.hero.description\") },\n    { slot: \"ctaHeading\", copy: ref(\"acme.cta.heading\") },\n    // Required repeating slot; an explicitly empty grid is valid.\n    { slot: \"features\", items: [] },\n    // A repeating slot — one CopyRef per placeholder feature, in order.\n    {\n      slot: \"faq\",\n      items: [\n        {\n          fields: {\n            question: { copy: ref(\"acme.faq.one.question\") },\n            answer: { copy: ref(\"acme.faq.one.answer\") },\n          },\n        },\n      ],\n    },\n  ],\n};\n\nconst resolved = resolveSurfaceDocument(acmeMarketingHome, myCopyResolver);\nconst { element, head } = renderWebDocument(resolved.document, {\n  groups: resolved.groups, // carries the structured FAQ fields\n});\n```\n\n`resolved.groups` is exactly `ResolvedSurfaceDocument.groups` — pass it\nstraight through as `RenderWebOptions.groups`; `renderWebDocument` maps\neach declared repeating slot's resolved items onto `MarketingView`'s\n`features`/`faq` props in authored order. Passing a group for a slot\n`MarketingView` does not declare as repeating, or omitting the required\n`features` group entirely, both fail closed with\n`RenderError(\"resolution-failed\", ...)` — the same error contract a\nmissing/unknown single-slot binding already produces for `AuthView`/\n`ErrorView`.\n\n### `CaptureView`, `DocumentView`, and `CollectionView` — fixed publisher page shells\n\nThese exports are direct, server-safe page shells rather than new\n`SurfaceDocument.template` registrations. They deliberately do not select\ncontent, load a CMS, own a router, or add client state.\n\n`CaptureView` provides the site chrome, one heading, a consumer-owned form\nregion, and a footer. The consumer owns form fields, submission, validation,\nand network effects. On a failed client-side submission, pass both\n`errorSummary` and `errorSummaryId`, focus that id, and keep the summary\nbefore the form; the view makes it a focusable `role=\"alert\"`. On success,\npass `submitted`: it replaces the form in the same position in a polite live\nregion. `CaptureView` intentionally does not choose a form library, add spam\nhandling, or model submission state.\n\n`DocumentView` accepts a `StructuredDocument` and an approved-copy resolver,\nthen calls `renderStructuredDocument` itself. A caller cannot supply a\npre-rendered article node or skip heading and in-document-fragment validation\non this path. The document title becomes the page `h1`; optional summary and\neffective-date labels remain `CopyRef`s. An effective date is\n`{ dateTime, text }`, so its visible approved copy is paired with a semantic\n`time` value; `dateTime` must be a real ISO date or date-time, not arbitrary\ndisplay text. Invalid document structure, unresolved fragments, missing copy,\nand malformed effective-date metadata fail closed with `RenderError`.\n\n`CollectionView` supplies an accessible collection index: each non-empty\nentry has a unique linked title, a semantic `time`, optional summary and tag\nlist, and a navigation landmark for consumer-owned pagination. Its required\n`empty` state prevents an empty index from silently becoming a blank region.\nEntries use a deliberately small closed shape (`id`, `href`, string `title`,\n`date`, optional string `summary`/`tags`); its raw props are consumer-owned\nview data and do not themselves carry CopyRef provenance. Consumers that need\neditorial provenance resolve structured fields before constructing these\nstrings, while Publisher's structured renderer continues to retain the\nunderlying field-level copy/asset evidence. Pagination and empty-state actions\nare likewise `{ href, label }` data, not node escape hatches. Route loading,\ntaxonomy, and paging state remain outside Publisher. If a router replaces collection items in place, it\nmust move focus to `focusTargetId` (the focusable `PageHeader` region that\ncontains the view's `h1`); ordinary links retain normal browser route focus\nhandling. Entry, empty-state, and pagination links accept only a fragment,\na single-root-relative path (never `//`), `http(s)`, or non-empty `mailto:`;\nscript, data, file, and protocol-relative URLs fail closed.\n\nThere is intentionally no `EntryView`. A document-backed entry page uses\n`DocumentView`, with its optional header action linking back to the\ncollection, rather than duplicating the validated document page contract.\nThis is the narrow disposition for entry pages; it does not add CMS, parser,\nor taxonomy behavior. A future Designer-block integration is separately\nstaged and is not part of these publisher shells.\n\n### `defineWebTemplate` / `createWebRenderer` — an extensible, instance-scoped web-template registry\n\n`AuthView`, `ErrorView`, and `MarketingView` are this package's own three\ntemplates. `defineWebTemplate` and `createWebRenderer` let a consumer\nregister their *own* page shapes against the same `web` renderer pipeline —\nthe same validation, resolution, and provenance guarantees, extended to a\ntemplate this package never shipped.\n\n**This is still not composition.** `SurfaceDocument.template` remains a\nplain string the caller names explicitly on every document — extensibility\nhere is about *who may add* a template (now: any caller, not just this\npackage), never about *who picks one* at render time. See \"Scope,\" above:\nthis package still renders and validates; it does not compose.\n\n```ts\nimport { createElement } from \"react\";\nimport { renderToStaticMarkup } from \"react-dom/server\";\nimport type { SurfaceDocument } from \"@clossys/publisher/core\";\nimport { resolveSurfaceDocument } from \"@clossys/publisher/core\";\nimport { createWebRenderer, defineWebTemplate } from \"@clossys/publisher/web\";\nimport { DashboardWidget } from \"./DashboardWidget.js\"; // a consumer's own component\n\nconst DashboardView = defineWebTemplate({\n  name: \"DashboardView\",\n  flow: { slots: [{ key: \"heading\", required: true }, { key: \"widget\", required: true }] },\n  // A slot key absent from slotKinds defaults to [\"copy\", \"asset\"] — the\n  // same two sources AuthView/ErrorView's slots already accept. \"widget\"\n  // opts INTO \"node\" explicitly, per slot — never a renderer-wide switch.\n  slotKinds: { widget: [\"node\"] },\n  build: (content) => createElement(\"main\", null, createElement(\"h1\", null, content.heading), content.widget),\n});\n\n// Every renderer instance is isolated — see \"Instance-scoped, never a\n// global mutable registry\" below. `includeBuiltins: true` additionally\n// registers AuthView/ErrorView/MarketingView on this same instance.\nconst renderer = createWebRenderer({ templates: [DashboardView], includeBuiltins: true });\n\nconst ref = (id: string) => ({ id });\nconst acmeDashboard: SurfaceDocument = {\n  id: \"acme.dashboard.home\",\n  channel: \"web\",\n  template: \"DashboardView\",\n  meta: { channel: \"web\", title: ref(\"acme.dashboard.heading\"), description: ref(\"acme.dashboard.heading\") },\n  bindings: [\n    { slot: \"heading\", copy: ref(\"acme.dashboard.heading\") },\n    // A caller-owned, already-composed React element — never a raw HTML\n    // string, never audience-supplied content. See \"Rich-node slots\" below.\n    { slot: \"widget\", node: createElement(DashboardWidget, { chartId: \"acme.chart.mrr\" }) },\n  ],\n};\n\n// Tell resolveSurfaceDocument which of THIS template's slots accept a\n// node — derived from the template's own declaration, never hardcoded —\n// and which template names this renderer instance knows.\nconst resolved = resolveSurfaceDocument(acmeDashboard, myCopyResolver, {\n  knownTemplates: renderer.listWebTemplateNames(),\n  nodeSlots: Object.entries(DashboardView.slotKinds ?? {})\n    .filter(([, kinds]) => kinds.includes(\"node\"))\n    .map(([slot]) => slot),\n});\n\nconst { element } = renderer.renderWebDocument(resolved.document, {\n  groups: resolved.groups, // repeating slots, if the template declares any\n  nodes: resolved.nodes, // resolved single-binding node slots\n});\nrenderToStaticMarkup(element);\n```\n\n**What a consumer-defined template is still forced through.** A `build`\nfunction only ever receives already-validated, already-resolved `content` —\nit never sees or influences a raw `SurfaceDocument`, so it cannot become a\npath around validation:\n\n- `validateSurfaceDocument`/`validateComposeDocument` run unchanged on\n  every document, built-in template or not — a custom template does not\n  bypass shape validation of `bindings`, `meta`, or `layout`.\n- `resolveDocument`'s `ok`/`missingRequired`/`unknownBindings`/\n  `bindingFindings` contract (issue #43's \"resolved nothing is never\n  `ok: true`\" bar) is reused as-is for a custom template's `flow`.\n- `renderWebDocument`'s \"every required slot must resolve to real content\n  or the render throws\" discipline (`RenderError(\"empty-output\", ...)`)\n  applies identically, whether that content came from a `copy` binding, an\n  `assetId`, or an authorized `node`.\n- `createResolvedOutputManifest`/`collectCopyProvenance` receive the same\n  `CopyResolution[]` for a custom template's `copy`-kind slots as they do\n  for `AuthView`/`ErrorView`'s — a custom template is never a hole through\n  which resolved copy reaches a page without leaving the provenance trail\n  every other slot leaves. A `node`-kind slot's content is the one\n  documented exception: it never goes through `CopyRef` resolution at all\n  (it is not audience-facing copy), so it contributes no `CopyResolution`\n  and appears nowhere in `resolutions`/manifest copy provenance — that\n  absence is the intended behavior, not a gap.\n\n**Rich-node slots are the dangerous surface, so they are the narrow one.**\nA `\"node\"`-kind slot accepts a real `ReactNode` the caller's *own trusted\ncode* already constructed — a composed `AuthView` form, a widget built from\n`@clossys/designer` atoms, a small caller-authored component. It never\naccepts and never interprets a raw HTML string, and there is no\n`dangerouslySetInnerHTML` anywhere on this path — React's own\nchild-rendering already escapes text/attribute values by default, and a\nnode slot's safety rests entirely on staying inside that path. `\"node\"` is\nopt-in *per slot*, declared explicitly in `slotKinds`; a slot left off\n`slotKinds` (or listed without `\"node\"`) never accepts one, and every\nmismatch — a node for an unregistered or non-node-kind slot, a copy/asset\nbinding against a node-only slot, a node colliding with a slot a copy/asset\nbinding already filled — fails closed with\n`RenderError(\"resolution-failed\", ...)`, never silently coerced or dropped.\n`core`'s own `resolveSurfaceDocument` mirrors this at the canonical-document\nlayer: a single binding's `node` still refuses unconditionally\n(`SurfaceResolutionError(\"unsupported-node\", ...)`) unless its `slot` is\nnamed in that call's own `nodeSlots` option — an opt-in allowlist a caller\nbuilds from the target template's own `slotKinds`, never inferred.\n\n**Instance-scoped, never a global mutable registry.** `createWebRenderer()`\nwith no arguments knows *zero* templates — not the three built-ins.\n`AuthView`/`ErrorView`/`MarketingView` remain exported, unchanged; the\nmodule-level `renderWebDocument`/`listWebTemplateNames` functions (this\npackage's only entry point before this feature existed) keep rendering\nthem exactly as before — a zero-line diff for every existing caller. Two\nindependently created `createWebRenderer()` instances never observe each\nother's templates, and this package exports no `registerWebTemplate` or\nother function that could mutate a shared, module-level map — the\nglobal-mutation alternative is not merely discouraged, it is structurally\nabsent from this package's exports. See `defineWebTemplate`'s and\n`createWebRenderer`'s own doc comments for the full argument (order\ndependence on import timing, and cross-consumer/cross-request collision in\na shared process, the same two failure modes a module-level mutable\nregistry has always risked elsewhere).\n\n**Fails closed on a malformed definition or a duplicate name.**\n`defineWebTemplate` validates `flow` (non-empty, unique slot keys — the\nsame discipline `validateComposeDocument` already holds a `LayoutSpec` to,\napplied here to a `FlowLayoutSpec` at definition time instead of first\nrender), rejects a `slotKinds` entry naming a slot `flow.slots` does not\ndeclare, and rejects a `repeatingSlots` key that collides with a flowed\nslot or with itself — every one of these throws\n`RenderError(\"invalid-template-definition\", ...)`.\n`createWebRenderer` throws `RenderError(\"duplicate-template\", ...)` if two\nentries (across `templates`, and the built-ins when `includeBuiltins` is\n`true`) share a `name` — never silently keeps the last one registered.\nBoth reuse this package's existing `RenderError`/`RenderErrorReason`\ncontract (`internal/errors.ts`) rather than introducing a second error\ntype — a caller catching errors from this package never needs a second\n`instanceof` check depending on whether a failure happened at template\ndefinition time or at render time.\n\n**One `SurfaceDocument` is exactly one canvas — pagination is out of scope\nby design, not an oversight.** `LayoutSpec`'s slots are fractional positions\n(`Frame = {x, y, w, h}`, 0..1 of a single fixed canvas); there is no flow,\nno auto-height, and no array of canvases on the contract. This fits a\nsingle-page artifact — an OG/share-card image, one slide, one print page —\ncleanly. A multi-page document (a book, a paginated report) is a\nconsumer-side concern: compose it as an ordered sequence of\n`SurfaceDocument`s, one per page, each resolved and rendered independently,\nand assemble the resulting artifacts (e.g. concatenate PDF pages) outside\nthis package. `surface` has no opinion on pagination, running headers, page\nnumbering, or cross-page layout — those stay with whatever assembles the\nsequence.\n\n`createOutputManifest` is the lower-level hand-off seam to a consumer\npublisher. `createResolvedOutputManifest(surface, resolved, artifacts,\nstrategyProvenance?)` is the normal pipeline entry point: it additionally\nrecords structural copy provenance grouped by registry, revision, locale,\nsource, and resolved entry identifier. It intentionally excludes rendered\ntext and `CopyRef.values`, which can contain audience language or\nrequest-specific data. Both helpers describe artifact paths and media types\nbut never write or upload files. Strategy provenance stays structural, so\nthis package never imports or depends on the Strategist package.\n\nThe package test suite includes a product-neutral reference pipeline fixture:\nan approved, versioned `CopyRegistry` resolves all content; flowed web/email\nslots avoid canvas placeholders; web, email, image, print, and slide outputs\neach receive a manifest with structural strategy provenance. It also asserts\nthat draft or malformed sources fail closed.\n\n## `media` — the asset registry contract, responsive images, and video (v2)\n\n`@clossys/publisher/media` registers a consumer's own image and video\nassets under a stable `assetId`, the identical role `@clossys/writer`\nplays for text: a registry, never a generation engine (it never calls an\nimage/video API, never talks to a model, never transcodes or extracts a\nposter frame — see `src/media/types.ts`'s own top comment).\n\n**v2 (issue #177) added a required `type` discriminator, responsive image\nsources, and video — a breaking change from v1.** `AssetEntry` is now a\ndiscriminated union:\n\n```ts\ntype AssetEntry = ImageAssetEntry | VideoAssetEntry;\n\ninterface ImageAssetEntry {\n  id: string;\n  type: \"image\";\n  src: string;            // primary/fallback source — unchanged from v1\n  width: number;\n  height: number;\n  alt: string;             // required, unchanged from v1\n  mimeType?: string;\n  licence?: string;        // optional — unchanged from v1, see below\n  credit?: string;\n  sources?: { src: string; width: number; format?: string }[]; // NEW — responsive <picture>/srcset\n}\n\ninterface VideoAssetEntry {\n  id: string;\n  type: \"video\";           // NEW — this package's first video support at all\n  sources: { src: string; mimeType: string }[]; // required, at least one\n  width: number;\n  height: number;\n  alt: string;\n  captions?: { src: string; srclang: string; label: string }[];\n  transcript?: string;     // at least one of captions/transcript is REQUIRED\n  poster?: string;\n  reducedMotion: \"pause\" | \"no-autoplay\" | \"static-poster\"; // required\n  autoplay?: boolean;\n  loop?: boolean;\n  muted?: boolean;\n  licence?: string;\n  credit?: string;\n}\n```\n\n**Migrating a v1 registry:** add `type: \"image\"` to every existing entry.\n`validateAssetRecordShape`'s new `\"type-shape\"` rule rejects any entry with\nno `type` at all — there is no silent default, on purpose (a registry that\nguessed \"image\" on a caller's behalf would hide the one piece of\ninformation a reviewer most needs to see stated). Every other v1 field\n(`src`/`width`/`height`/`alt`/`mimeType`/`licence`/`credit`) is unchanged.\n\n**`licence`/`credit` are not new in v2 and stay optional.** Both already\nexisted in v1 with real shape validation (`\"licence-shape\"`/\n`\"credit-shape\"`). v1's actual gap was that a missing licence produced no\nsignal at all — closed not by making `licence` schema-required (which would\ninvalidate every already-registered v1 entry the moment its owner upgrades)\nbut by a new `checkAssetCoverage` finding: `\"asset-missing-licence\"`\n(`severity: \"warning\"`), reported for every registered entry — referenced or\nnot — with no `licence`. `AssetCoverageReport` also gained `registeredByType:\n{ image: number; video: number }`, so a reviewer can tell what is actually\nregistered without re-reading the raw JSON; `checkAssetCoverage`'s own\nid-matching does not branch on `type` — an id either matches a registered\nentry or it doesn't, regardless of kind.\n\n**Video accessibility is enforced at the schema layer, not the renderer.**\nA `VideoAssetEntry` with neither `captions` nor `transcript` fails\n`validateAssetRecordShape` (`\"video-caption-or-transcript-required\"`,\n`severity: \"error\"`) and can never reach a renderer at all — the identical\n\"no later recovery point\" reasoning `alt`'s own required-and-non-whitespace\nrule already holds images to, restated for captions (recovering one after\nthe fact means re-transcribing the video, not re-typing a sentence).\n`reducedMotion` is similarly required\n(`\"video-reduced-motion-required\"`), and `reducedMotion: \"static-poster\"`\nadditionally requires `poster` (`\"video-static-poster-requires-poster\"`).\n`../internal/assets.ts`'s render-time validation (`isRenderVideoAsset`)\nenforces the identical bars independently, since a hand-rolled `AssetLookup`\n(a CMS, a CDN manifest, a test double) can hand a renderer a video-shaped\nvalue that never passed through this package's own schema at all.\n\n### Per-channel behaviour\n\n| Channel | Responsive images (`sources`) | Video |\n| --- | --- | --- |\n| `web` | Real `<picture>`/`<source>`/`srcset`, falling back to the primary `<img>` | Real `<video>` with every `<source>`/`<track kind=\"captions\">`, gated by the reduced-motion contract below |\n| `email`, `print`, `image`, `slides` | Ignored — `sources` is dropped; the primary `src` renders exactly as a v1 image would | No playback capability of any kind. A video entry's `poster` renders as a plain `<img>`/`<image>`, exactly like an image asset. A video with **no `poster`** is an unresolvable asset — the render refuses (`RenderError(\"empty-output\", ...)`), the identical fail-closed bar an unresolved `assetId` already gets. Never silently rendered as nothing. |\n\n**Reduced motion is a rendering-time decision, not a build-time one.**\n`renderWebDocument`/`RenderWebOptions.prefersReducedMotion` (issue #177) is\na caller-supplied boolean — this package has no `window`/DOM access at\nrender time (it may run on a server, in a build step, or in a browser), so\nit cannot itself call `window.matchMedia(\"(prefers-reduced-motion:\nreduce)\")`; a caller derives that value from a `Sec-CH-Prefers-Reduced-Motion`\nclient hint on the server, or a direct `matchMedia` read on the client, and\npasses it through. Applied against a resolved `VideoAssetEntry.reducedMotion`:\n\n- Omitted (or `false`) — every video's own `autoplay` renders exactly as\n  authored. Regression-safe: unchanged from before this option existed.\n- `true` and `reducedMotion` is `\"pause\"` or `\"no-autoplay\"` — the entry's\n  `autoplay` is force-suppressed; every other attribute (loop/muted/\n  poster/sources/captions) renders unchanged, so a viewer can still press\n  play.\n- `true` and `reducedMotion` is `\"static-poster\"` — no `<video>` element is\n  emitted at all; a static `<img>` built from `poster` renders instead.\n\n### Explicit non-goals\n\n- **No responsive-source or video support on `email`/`print`/`image`/\n  `slides`.** `RenderAsset`'s discriminated shape is shared by all five\n  channel renderers by construction, so each non-web channel must at\n  minimum not crash or silently mis-render a video/multi-source entry — but\n  real playback support is `web`-only, and a `<picture>`-equivalent\n  construct does not exist in an SVG canvas or reliably in an email client.\n- **No licence content-validation.** `licence` stays free text — this\n  package has no authority over what licences a consumer's assets actually\n  carry, unchanged from v1.\n- **No automatic captioning/transcription.** A missing captions/transcript\n  pair is a hard validation failure a human must resolve, never something\n  this package infers or generates.\n- **No video generation, transcoding, or poster extraction** — the\n  identical \"registry, not an engine\" boundary this file's own top comment\n  already draws for images.\n\n## `document` — a product-neutral structured-document contract and renderer\n\nA help article, a policy page, a changelog entry, a long-form explainer —\nany page whose body is \"read this document,\" not \"fill in these five named\nregions\" — has no shape in `SurfaceSlotBinding` (a single `CopyRef` or a\ncaller-owned `node`, never an ordered sequence of headings, paragraphs,\nlists, tables, and callouts). `@clossys/publisher/document` is that\nshape: `StructuredDocument`, `validateStructuredDocument`, and\n`renderStructuredDocument`.\n\n```ts\nimport { validateStructuredDocument, renderStructuredDocument } from \"@clossys/publisher/document\";\nimport type { StructuredDocument } from \"@clossys/publisher/document\";\n\nconst ref = (id: string) => ({ id });\n\nconst helpArticle: StructuredDocument = {\n  id: \"acme.help.getting-started\",\n  title: ref(\"acme.doc.title\"),\n  sections: [\n    {\n      kind: \"section\",\n      id: \"overview\", // a literal, author-supplied, locale-stable anchor — never derived from `heading`\n      level: 2, // h1 is reserved for the page's own title, rendered outside this contract\n      heading: ref(\"acme.overview.heading\"),\n      blocks: [\n        {\n          kind: \"paragraph\",\n          content: [\n            { kind: \"text\", text: ref(\"acme.overview.p1\") },\n            { kind: \"link\", text: ref(\"acme.overview.link\"), href: \"#pricing\" }, // an in-document fragment link\n          ],\n        },\n        { kind: \"list\", style: \"ordered\", items: [[{ kind: \"text\", text: ref(\"acme.overview.item1\") }]] },\n      ],\n    },\n    {\n      kind: \"section\",\n      id: \"pricing\",\n      level: 2,\n      heading: ref(\"acme.pricing.heading\"),\n      blocks: [{ kind: \"table\", headers: [ref(\"acme.pricing.plan\"), ref(\"acme.pricing.price\")], rows: [[ref(\"acme.pricing.plan1\"), ref(\"acme.pricing.price1\")]] }],\n    },\n  ],\n};\n\nconst findings = validateStructuredDocument(helpArticle); // [] when clean — see \"What is validated\" below\nconst { element, resolutions } = renderStructuredDocument(helpArticle, { resolveCopyId: myCopyResolver });\n```\n\n**Every leaf of content is a `CopyRef`, never a literal string** — the same\ndiscipline `SurfaceSlotBinding.copy` already holds document content to.\n`DocumentBlock` is a closed, six-member vocabulary (`section`, `paragraph`,\n`list`, `definition-list`, `table`, `callout`); `DocumentInline` (inside a\nparagraph, list item, or callout — never a block on its own) is `text` or\n`link`. A `DocumentSection` (`id`, `level: 2–6`, `heading`, `blocks`) is the\none block kind that nests, and is also what `StructuredDocument.sections`\nis made of at the top level. See `src/document/types.ts` for the full\nshape and every field's own doc comment.\n\n**What is validated (`validateStructuredDocument(value): ComposeFinding[]`)**\n— shape (every block/inline kind checked against its own fields, a\ndistinct `rule` name per failure, the same `{ rule, severity, message,\npath }` shape every other validator in this package uses, attributed to a\nprecise path like `sections.0.blocks.2.rows.1`), plus four checks worth\ncalling out:\n\n- **Heading order.** A top-level `sections` entry must be `level: 2`\n  (`\"section-level-must-be-two-at-top\"`); a nested section's `level` must\n  equal its parent's `+ 1` — never equal, lower, or skipped ahead\n  (`\"section-level-skip\"`, the exact h2→h4 jump this contract exists to\n  catch); a `level: 6` section may not contain a nested section, since\n  there is no `level: 7` (`\"section-level-max-depth\"`).\n- **Links.** A `\"link\"`'s `href` is checked against a closed scheme\n  allowlist — `https:`, `http:`, `mailto:` — the same\n  `protocol !== \"https:\" && protocol !== \"http:\"` shape\n  `packages/auth/src/redirect.ts`'s `parseHttpUrl` already uses elsewhere\n  in this repository, extended with `mailto:`. A rejected scheme\n  (`javascript:`, `data:`, `file:`, or an unparseable value) is\n  `\"link-scheme-not-allowed\"`, **an error finding, never a silent drop**:\n  it is never omitted from the block, never replaced with a placeholder,\n  and never rendered inert — the finding makes the whole document invalid,\n  and `renderStructuredDocument` refuses to render at all.\n\n  Two **schemeless** forms are accepted alongside those three schemes,\n  because a prose document overwhelmingly links inside its own site:\n\n  - A `\"#fragment\"` link skips the scheme check (there is no scheme) but\n    must resolve against a real `DocumentSection.id` present anywhere in\n    the same document, at any nesting depth — a fragment naming no such id\n    is `\"link-fragment-unresolved\"`.\n  - A **root-relative** `\"/pricing\"` is accepted as-is. It is same-origin\n    by construction, so there is no scheme to allowlist, and requiring an\n    absolute URL instead would bake the deployment's hostname into content\n    the copy registry owns.\n\n  Two forms that *look* relative are rejected, and the distinction is the\n  point:\n\n  - `\"//host/path\"` — **protocol-relative** — is\n    `\"link-protocol-relative\"`. It reads as same-site and is not: it\n    inherits only the scheme and resolves to whatever host follows the\n    `//`. Write the absolute `https:` URL if that other origin is\n    genuinely intended.\n  - `\"docs/foo\"`, `\"../sibling\"` — **path-relative** — is\n    `\"link-scheme-not-allowed\"`. It resolves against whichever route the\n    document is mounted at, and this contract exists precisely so one\n    `StructuredDocument` can be rendered in more than one place; a link\n    that means different things per mount point is a defect that would\n    only surface on the second mount.\n- **Tables.** `headers` must be a non-empty `CopyRef[]`\n  (`\"table-headers-required\"`); every row must have exactly\n  `headers.length` cells, never padded or truncated\n  (`\"table-row-length-mismatch\"`, reported per offending row index).\n  `renderStructuredDocument` renders `headers` as `<th scope=\"col\">` inside\n  a `<thead>` and every row as `<td>` inside `<tbody>` — the header-to-cell\n  association an accessible table needs is therefore structural (the fixed\n  cell count matching a real `<th>` per column), not left to visual\n  alignment.\n- **Anchors.** Every `DocumentSection.id`, at every nesting depth, must be\n  unique across the **whole document**, not just among siblings — a\n  duplicate is `\"section-anchor-duplicate\"`, reported for the second (and\n  every subsequent) occurrence, naming the path of the first. Never\n  auto-renamed, suffixed, or dropped to force uniqueness — resolving the\n  collision is the author's job.\n\nAn empty document (`sections: []`), an empty list (`items: []`), and an\nempty table body (`rows: []`, headers still required) are each valid —\n\"empty is a fact to report, not to hide,\" the same discipline\n`SurfaceRepeatingSlotBinding.items` already holds `surface/core` to —\nnever a special-cased finding.\n\n**`renderStructuredDocument(doc, options?)` renders to semantic HTML only**\n— `<section>`, `<h2>`–`<h6>`, `<p>`, `<ul>`/`<ol>`, `<dl>`, `<table>`/\n`<thead>`/`<tbody>`/`<th>`/`<td>`, `<a>`, and `<aside role=\"note\"\ndata-callout-tone=\"…\">` for a callout (there is no single HTML element for\n\"callout\"; `<aside>` is the closest semantic fit, and `data-callout-tone`\nis this subpath's own public, documented attribute naming the closed\n`tone` vocabulary — not an internal-convention leak). Every block and every\ninline node is a typed primitive this renderer walks explicitly and emits\nas a specific element: there is no `\"html\"` block kind, no markdown string\nparsed into markup, and no `dangerouslySetInnerHTML` anywhere on this\npath — the same non-goal issue #175 (the web-template registry) states for\nits own `\"node\"`-kind slots, applied here to document content specifically.\n\nIt **refuses to render an invalid document at all**:\n`validateStructuredDocument` runs first, and any `severity: \"error\"`\nfinding throws `RenderError(\"resolution-failed\", ...)` before a single\nelement is built — reusing this package's existing closed\n`RenderErrorReason` vocabulary (`src/internal/errors.ts`) rather than\ninventing a parallel one, and never partially rendering a document with\nknown-invalid content. The same error, and the same reason, is thrown if a\n`CopyRef` fails to resolve during rendering (no `options.resolveCopyId`\nsupplied, an unresolved id, or empty resolved text) — the identical\nfail-closed shape `resolveSurfaceDocument` already uses for every other\nunresolved/invalid input in this package.\n\n`doc.title` is resolved (and appears in the returned `resolutions`, for\nprovenance) but is **never rendered into the output tree** — the page's\nown `<h1>` stays the caller's job, the same discipline `ErrorView` already\nholds between its own `<h1>` and `EmptyState`'s `<h2>`.\n\n**`resolveCopyId`'s type, and the `{ element, resolutions }` return\nshape.** `RenderStructuredDocumentOptions.resolveCopyId` is\n`@clossys/writer`'s own ref-based `CopyResolver` —\n`(ref: CopyRef) => CopyResolution | undefined`, the same type\n`resolveSurfaceDocument`'s own `resolver` parameter takes — **not**\n`surface/web`'s string-keyed `CopyResolver` (`(copyId: string) => string |\nundefined`). Every `CopyRef` this render resolves (`title`, every\n`heading`, every inline `text`/`link` text, every table header/cell, every\ncallout/definition-list text) is collected into a `CopyResolution[]`,\nreturned as `resolutions` alongside the rendered `element` — feed it to\n`collectCopyProvenance` (`surface/core`) exactly as\n`ResolvedSurfaceDocument.resolutions` already is.\n\n**Plugging a rendered document into a page.** `renderStructuredDocument`'s\n`element` is a plain `ReactNode` a consumer's own `surface/web` template\ncan accept through a `\"node\"`-kind slot (`defineWebTemplate`/\n`createWebRenderer`, see \"`defineWebTemplate` / `createWebRenderer`\"\nabove) — the page shell (header, nav, footer) around the document body is\nexactly the kind of thing a consumer's own template already provides, and\nthis package invents no second, parallel composition seam for document\ncontent specifically:\n\n```ts\nimport { createElement } from \"react\";\nimport { defineWebTemplate, createWebRenderer } from \"@clossys/publisher/web\";\nimport { resolveSurfaceDocument } from \"@clossys/publisher/core\";\nimport type { SurfaceDocument } from \"@clossys/publisher/core\";\n\nconst HelpArticleView = defineWebTemplate({\n  name: \"HelpArticleView\",\n  flow: { slots: [{ key: \"heading\", required: true }, { key: \"body\", required: true }] },\n  slotKinds: { body: [\"node\"] },\n  build: (content) => createElement(\"main\", null, createElement(\"h1\", null, content.heading), content.body),\n});\n\nconst page: SurfaceDocument = {\n  id: \"acme.help.getting-started.page\",\n  channel: \"web\",\n  template: \"HelpArticleView\",\n  meta: { channel: \"web\", title: ref(\"acme.page.title\"), description: ref(\"acme.page.title\") },\n  bindings: [\n    { slot: \"heading\", copy: ref(\"acme.page.title\") },\n    { slot: \"body\", node:","readmeFilename":"README.md"}