diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml
index c5647ad..e501776 100644
--- a/.github/workflows/npm-publish.yml
+++ b/.github/workflows/npm-publish.yml
@@ -6,7 +6,7 @@ on:
workflow_dispatch:
inputs:
release_tag:
- description: "Release tag to publish, for example v3.2.1"
+ description: "Release tag to publish, for example v3.2.3"
required: true
type: string
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 21f1444..0e9aad9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,13 @@
All notable changes to `layout-style-css` are documented here. This project follows semantic versioning.
+## [3.2.3] - 2026-09-26
+
+### Fixed
+
+- Recovered the release train with a fresh immutable version after the unpublished `v3.2.1` and `v3.2.2` tags were found to reference mismatched package metadata.
+- Preserved the reviewed 3.2.1 CSS, exports, and compatibility behavior without moving either pre-existing tag.
+
## [3.2.1] - 2026-09-24
### Security
diff --git a/README.md b/README.md
index affa8dd..0eb2caf 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Zero-configuration, dependency-free structural CSS that responds to the width and height a layout actually receives.
-`layout-style-css@3.2.1` is the current v3 release candidate. It provides intrinsic wrappers, Mosaic and Action Bar compositions, seven semantic recipes, explicit local density controls, and twenty spatial personalities. Layout owns structure; UI Style Kit owns paint; Interactive Surface owns interaction styling.
+`layout-style-css@3.2.3` is the current v3 release candidate. It provides intrinsic wrappers, Mosaic and Action Bar compositions, seven semantic recipes, explicit local density controls, and twenty spatial personalities. Layout owns structure; UI Style Kit owns paint; Interactive Surface owns interaction styling.
## Requirements
@@ -13,7 +13,7 @@ Zero-configuration, dependency-free structural CSS that responds to the width an
## Install
```bash
-npm install layout-style-css@3.2.1
+npm install layout-style-css@3.2.3
```
Most applications need one import:
@@ -200,8 +200,8 @@ This order lets UI Style Kit establish paint and theme roles, Interactive Surfac
## CDN
```html
-
-
+
+
```
## Clean-Break Migration
diff --git a/demo/index.html b/demo/index.html
index 7c64571..e5b1a5f 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -6,7 +6,7 @@
-
+
@@ -66,15 +66,15 @@
Layout Style CSS v3.2 — Intrinsic Responsive Demo
-
-
+
+
-
-
+
+
Skip to the layout lab
diff --git a/demo/sitemap.xml b/demo/sitemap.xml
index 896fec4..1f6df80 100644
--- a/demo/sitemap.xml
+++ b/demo/sitemap.xml
@@ -2,7 +2,7 @@
https://foscat.github.io/Layout-Style-CSS/
- 2026-09-24
+ 2026-09-26weekly0.8
diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md
index a1e53a9..eb84cd1 100644
--- a/docs/wiki/Getting-Started.md
+++ b/docs/wiki/Getting-Started.md
@@ -1,9 +1,9 @@
# Getting Started
-Install `layout-style-css@3.2.1` and import the root bundle:
+Install `layout-style-css@3.2.3` and import the root bundle:
```bash
-npm install layout-style-css@3.2.1
+npm install layout-style-css@3.2.3
```
```js
diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md
index df70575..4955103 100644
--- a/docs/wiki/Home.md
+++ b/docs/wiki/Home.md
@@ -1,6 +1,6 @@
# Layout Style CSS v3
-`layout-style-css@3.2.1` is the current release candidate. It remains a dependency-free, CSS-only structural system for layouts that adapt to available width and height.
+`layout-style-css@3.2.3` is the current release candidate. It remains a dependency-free, CSS-only structural system for layouts that adapt to available width and height.
## Contract
diff --git a/docs/wiki/Installation-And-CDN.md b/docs/wiki/Installation-And-CDN.md
index f7125af..4cad065 100644
--- a/docs/wiki/Installation-And-CDN.md
+++ b/docs/wiki/Installation-And-CDN.md
@@ -9,7 +9,7 @@
## Package
```bash
-npm install layout-style-css@3.2.1
+npm install layout-style-css@3.2.3
```
```js
@@ -36,11 +36,11 @@ Package and bundler defaults (`main`, `style`, and the root export) use the read
## CDN
```html
-
+
```
```html
-
+
```
The CDN paths include `/dist/` because CDN clients address files in the published tarball, while package import maps use the public exports.
diff --git a/docs/wiki/Release-And-Publishing.md b/docs/wiki/Release-And-Publishing.md
index 9510dd8..f9f29d7 100644
--- a/docs/wiki/Release-And-Publishing.md
+++ b/docs/wiki/Release-And-Publishing.md
@@ -1,6 +1,6 @@
# Release And Publishing
-This guide describes the release gate for `layout-style-css@3.2.1`. Running verification does not publish, tag, push, create a GitHub Release, or change the npm registry.
+This guide describes the release gate for `layout-style-css@3.2.3`. Running verification does not publish, tag, push, create a GitHub Release, or change the npm registry.
## Local Candidate Gate
@@ -35,8 +35,8 @@ The UI fixture SHA is deliberately stable: companion workflows use it to load th
Confirm:
-- package version is `3.2.1`
-- intended tag would be `v3.2.1`
+- package version is `3.2.3`
+- intended tag would be `v3.2.3`
- runtime and peer dependencies are empty
- exact exports and tarball files match the v3 contract
- generated CSS matches authored sources
@@ -50,7 +50,7 @@ Only after explicit approval:
1. Commit the verified candidate.
2. Push the intended branch.
3. Merge through the repository's normal review policy.
-4. Create the exact `v3.2.1` tag from the protected release commit.
+4. Create the exact `v3.2.3` tag from the protected release commit.
5. Publish the GitHub Release.
6. Let the protected npm workflow verify the tag and publish with provenance.
7. Confirm the GitHub Release, tag, and npm registry state independently.
diff --git a/manifest.json b/manifest.json
index cebd318..bdda43b 100644
--- a/manifest.json
+++ b/manifest.json
@@ -5,7 +5,7 @@
"breakingChange": "increment-schemaVersion-before-removing-or-renaming-fields"
},
"name": "layout-style-css",
- "version": "3.2.1",
+ "version": "3.2.3",
"entrypoints": {
"default": "./dist/layout-style-css.css",
"minified": "./dist/layout-style-css.min.css",
diff --git a/package-lock.json b/package-lock.json
index 5146629..c549619 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "layout-style-css",
- "version": "3.2.1",
+ "version": "3.2.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "layout-style-css",
- "version": "3.2.1",
+ "version": "3.2.3",
"license": "MIT",
"devDependencies": {
"@playwright/test": "1.61.1",
diff --git a/package.json b/package.json
index 72af19e..4c0beb9 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "layout-style-css",
"author": "Kyle Foster",
- "version": "3.2.1",
+ "version": "3.2.3",
"description": "Dependency-free structural CSS for responsive wrappers, composition primitives, recipes, utilities, and layout personalities.",
"type": "module",
"license": "MIT",
diff --git a/scripts/release-fixture-contract.mjs b/scripts/release-fixture-contract.mjs
index 87d4b50..c99d942 100644
--- a/scripts/release-fixture-contract.mjs
+++ b/scripts/release-fixture-contract.mjs
@@ -220,7 +220,54 @@ export function resolveFixturePackageSpec(fixtureRoot, packageName) {
}
/**
- * Forces the staged UI checker to consume the published UI package for current checks.
+ * Resolves an exact package spec from the immutable compatibility contract at a
+ * reviewed Git revision instead of reading potentially newer worktree content.
+ *
+ * @param {string} fixtureRoot UI fixture repository path.
+ * @param {string} revision Reviewed 40-character commit SHA.
+ * @param {string} packageName Ecosystem package name.
+ * @returns {string} Exact npm package specifier recorded by the reviewed commit.
+ */
+export function resolveFixturePackageSpecAtRevision(
+ fixtureRoot,
+ revision,
+ packageName,
+) {
+ assert.match(
+ revision,
+ /^[0-9a-f]{40}$/,
+ "Fixture revision must be an immutable 40-character commit SHA.",
+ );
+ assert.ok(packageName, "Package name is required.");
+
+ const result = spawnSync(
+ "git",
+ [
+ "-C",
+ path.resolve(fixtureRoot),
+ "show",
+ `${revision}:ecosystem-compatibility.json`,
+ ],
+ { encoding: "utf8" },
+ );
+ assert.equal(
+ result.status,
+ 0,
+ `Unable to read the reviewed compatibility contract at ${revision}: ${result.stderr}`,
+ );
+
+ const contract = JSON.parse(result.stdout);
+ const version = contract.supportedCombinations?.current?.[packageName];
+ assert.ok(
+ version,
+ `Reviewed compatibility contract must define a current ${packageName} version.`,
+ );
+ return `${packageName}@${version}`;
+}
+
+/**
+ * Forces the staged UI checker and compatibility matrix to consume the same
+ * published UI package for current checks.
*
* This avoids running the UI package's own release tests against the temporary
* Layout candidate contract while leaving the minimum matrix under the fixture's
@@ -228,15 +275,29 @@ export function resolveFixturePackageSpec(fixtureRoot, packageName) {
*
* @param {string} fixtureRoot Staged UI fixture root.
* @param {string} uiPackageSpec Exact UI package specifier.
+ * @param {Record} [reviewedCompanionVersions] Published companion versions from the reviewed contract.
* @returns {void}
*/
-export function applyCurrentMatrixUiSpec(fixtureRoot, uiPackageSpec) {
+export function applyCurrentMatrixUiSpec(
+ fixtureRoot,
+ uiPackageSpec,
+ reviewedCompanionVersions = {},
+) {
assert.match(
uiPackageSpec,
/^@?[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)?@[0-9A-Za-z_.-]+$/,
"UI package spec must be an exact npm specifier.",
);
+ const versionSeparator = uiPackageSpec.lastIndexOf("@");
+ const packageName = uiPackageSpec.slice(0, versionSeparator);
+ const packageVersion = uiPackageSpec.slice(versionSeparator + 1);
+ assert.equal(
+ packageName,
+ "ui-style-kit-css",
+ "Current matrix override must target ui-style-kit-css.",
+ );
+
const preflightPath = path.join(
path.resolve(fixtureRoot),
"scripts",
@@ -256,6 +317,41 @@ export function applyCurrentMatrixUiSpec(fixtureRoot, uiPackageSpec) {
`const currentArgs = [checkerPath, '--matrix', 'current', '--ui-spec', '${uiPackageSpec}', \`--\${candidateKey}-spec\`, tarball];`,
),
);
+
+ const compatibilityPath = path.join(
+ path.resolve(fixtureRoot),
+ "ecosystem-compatibility.json",
+ );
+ const compatibility = JSON.parse(
+ fs.readFileSync(compatibilityPath, "utf8"),
+ );
+ assert.ok(
+ compatibility.supportedCombinations?.current,
+ "Staged compatibility contract must define a current matrix.",
+ );
+ compatibility.supportedCombinations.current[packageName] = packageVersion;
+ for (const [companionName, companionVersion] of Object.entries(
+ reviewedCompanionVersions,
+ )) {
+ assert.ok(
+ Object.hasOwn(
+ compatibility.supportedCombinations.current,
+ companionName,
+ ),
+ `Staged compatibility contract must define ${companionName}.`,
+ );
+ assert.match(
+ companionVersion,
+ /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/,
+ `${companionName} must use an exact published semantic version.`,
+ );
+ compatibility.supportedCombinations.current[companionName] =
+ companionVersion;
+ }
+ fs.writeFileSync(
+ compatibilityPath,
+ `${JSON.stringify(compatibility, null, 2)}\n`,
+ );
}
/**
@@ -276,6 +372,19 @@ export function resolveInteractiveRoot(
);
}
+/**
+ * Detects whether a reviewed UI preflight can register unpublished companion
+ * candidates without passing an option that older immutable fixtures reject.
+ *
+ * @param {string} preflightSource UI release-preflight module source.
+ * @returns {boolean} Whether the companion candidate option is implemented.
+ */
+export function supportsCompanionCandidateRoot(preflightSource) {
+ return /arg\s*===\s*['"]--companion-candidate-root['"]/.test(
+ preflightSource,
+ );
+}
+
async function runCli(args) {
const descriptor = readFixtureDescriptor(rootDir);
if (args.includes("--write-github-outputs")) {
@@ -308,23 +417,53 @@ async function runCli(args) {
candidatePackage: packageName,
candidateVersion: packageManifest.version,
});
+ const siblingInteractive = resolveInteractiveRoot(rootDir);
+ const stagedPreflightModule = path.join(
+ stagedFixture.fixtureRoot,
+ "scripts",
+ "release-preflight.mjs",
+ );
+ assert.ok(
+ fs.existsSync(stagedPreflightModule),
+ `Staged UI release fixture is missing ${stagedPreflightModule}.`,
+ );
+ const hasExplicitInteractiveRepo = forwardedArgs.includes(
+ "--interactive-repo",
+ );
+ const useLocalInteractiveCandidate =
+ !hasExplicitInteractiveRepo &&
+ supportsCompanionCandidateRoot(
+ fs.readFileSync(stagedPreflightModule, "utf8"),
+ );
if (stagedFixture.fixtureRoot !== resolvedFixtureRoot) {
+ const reviewedInteractiveSpec = resolveFixturePackageSpecAtRevision(
+ resolvedFixtureRoot,
+ descriptor.revision,
+ "interactive-surface-css",
+ );
+ const interactiveVersion = useLocalInteractiveCandidate
+ ? JSON.parse(
+ fs.readFileSync(
+ path.join(siblingInteractive, "package.json"),
+ "utf8",
+ ),
+ ).version
+ : reviewedInteractiveSpec.slice(
+ reviewedInteractiveSpec.lastIndexOf("@") + 1,
+ );
applyCurrentMatrixUiSpec(
stagedFixture.fixtureRoot,
- resolveFixturePackageSpec(stagedFixture.fixtureRoot, "ui-style-kit-css"),
+ resolveFixturePackageSpecAtRevision(
+ resolvedFixtureRoot,
+ descriptor.revision,
+ "ui-style-kit-css",
+ ),
+ {
+ "interactive-surface-css": interactiveVersion,
+ },
);
}
- const siblingInteractive = resolveInteractiveRoot(rootDir);
try {
- const stagedPreflightModule = path.join(
- stagedFixture.fixtureRoot,
- "scripts",
- "release-preflight.mjs",
- );
- assert.ok(
- fs.existsSync(stagedPreflightModule),
- `Staged UI release fixture is missing ${stagedPreflightModule}.`,
- );
const commandArgs = [
stagedPreflightModule,
"--fixture-root",
@@ -333,12 +472,14 @@ async function runCli(args) {
rootDir,
"--candidate-package",
packageName,
- "--companion-candidate-root",
- stagedFixture.fixtureRoot,
- "--companion-candidate-root",
- siblingInteractive,
- "--interactive-repo",
- siblingInteractive,
+ ...(useLocalInteractiveCandidate
+ ? [
+ "--companion-candidate-root",
+ siblingInteractive,
+ "--interactive-repo",
+ siblingInteractive,
+ ]
+ : []),
"--interactive-docs-repo",
siblingInteractive,
"--layout-docs-repo",
diff --git a/test/demo-smoke.test.mjs b/test/demo-smoke.test.mjs
index c1aa1b3..de94620 100644
--- a/test/demo-smoke.test.mjs
+++ b/test/demo-smoke.test.mjs
@@ -125,7 +125,7 @@ const topologyEdges = [
const assertStaticDemoContract = () => {
assert.match(demoHtml, /Layout Style CSS v3\.2/);
- assert.match(demoHtml, /content="3\.2\.1"/);
+ assert.match(demoHtml, /content="3\.2\.3"/);
for (const companionPackage of ["ui-style-kit-css", "interactive-surface-css"]) {
assert.doesNotMatch(
`${demoHtml}\n${demoJs}`,
@@ -146,17 +146,17 @@ const assertStaticDemoContract = () => {
assert.match(demoHtml, /id="topologyReadout"/);
assert.match(
demoHtml,
- /href="\.\.\/dist\/layout-style-css\.css\?v=3\.2\.1"/,
+ /href="\.\.\/dist\/layout-style-css\.css\?v=3\.2\.3"/,
"The demo should cache-bust its v3 layout bundle."
);
assert.match(
demoHtml,
- /href="\.\/demo\.css\?v=3\.2\.1"/,
+ /href="\.\/demo\.css\?v=3\.2\.3"/,
"The demo should cache-bust its v3 presentation styles."
);
assert.match(
demoHtml,
- /src="\.\/demo\.js\?v=3\.2\.1"/,
+ /src="\.\/demo\.js\?v=3\.2\.3"/,
"The demo should cache-bust its v3 controller."
);
assert.doesNotMatch(demoHtml, /integrations\/ui-style-kit\.css/);
@@ -989,7 +989,7 @@ const verifyCompositionFixtures = async (page, baseUrl) => {
};
const verifyPersonalityOptionsUsePairingMetadata = async (page, baseUrl) => {
- const metadataUrl = new URL("../personalities.json?v=3.2.1", baseUrl).toString();
+ const metadataUrl = new URL("../personalities.json?v=3.2.3", baseUrl).toString();
const pairingFixture = {
schemaVersion: 1,
personalities: [
@@ -1052,7 +1052,7 @@ const verifyPersonalityOptionsUsePairingMetadata = async (page, baseUrl) => {
* @returns {Promise}
*/
const verifyPersonalityMetadataFailureRecovery = async (page, baseUrl) => {
- const metadataUrl = new URL("../personalities.json?v=3.2.1", baseUrl).toString();
+ const metadataUrl = new URL("../personalities.json?v=3.2.3", baseUrl).toString();
const recoveryContext = await page.context().browser().newContext();
try {
const recoveryPage = await recoveryContext.newPage();
diff --git a/test/layout-css-contract.test.mjs b/test/layout-css-contract.test.mjs
index 6401de0..ae95ca6 100644
--- a/test/layout-css-contract.test.mjs
+++ b/test/layout-css-contract.test.mjs
@@ -126,7 +126,7 @@ function customPropertyValue(css, property) {
return value.replace(/\s+/g, " ").trim();
}
-assert.equal(packageJson.version, "3.2.1", "The patch branch must expose version 3.2.1");
+assert.equal(packageJson.version, "3.2.3", "The patch branch must expose version 3.2.3");
assert.equal(packageJson.engines?.node, ">=20", "Development must retain the Node 20 floor");
assert.deepEqual(packageJson.exports, expectedExports, "Package exports must match the clean v3 API");
assert.deepEqual(
diff --git a/test/manifest-contract.test.mjs b/test/manifest-contract.test.mjs
index 97b2858..615ea8d 100644
--- a/test/manifest-contract.test.mjs
+++ b/test/manifest-contract.test.mjs
@@ -171,7 +171,7 @@ function runtimeConsumedCustomProperties(css) {
test("ecosystem manifest publishes the structural API and package export", () => {
assert.equal(manifest.schemaVersion, 1);
assert.equal(manifest.name, packageJson.name);
- assert.equal(manifest.version, "3.2.1");
+ assert.equal(manifest.version, "3.2.3");
assert.equal(manifest.version, packageJson.version);
assert.equal(manifest.schemaPolicy.compatibility, "additive-within-major");
assert.equal(
diff --git a/test/package-contract.test.mjs b/test/package-contract.test.mjs
index 8dbf643..5af700d 100644
--- a/test/package-contract.test.mjs
+++ b/test/package-contract.test.mjs
@@ -131,11 +131,11 @@ test("package defaults and homepage expose the intended distribution contract",
}
});
-test("release metadata stays synchronized for the 3.2.1 patch", () => {
- assert.equal(manifest.version, "3.2.1");
- assert.equal(packageLock.version, "3.2.1");
- assert.equal(packageLock.packages[""].version, "3.2.1");
- assert.equal(ecosystemManifest.version, "3.2.1");
+test("release metadata stays synchronized for the 3.2.3 patch", () => {
+ assert.equal(manifest.version, "3.2.3");
+ assert.equal(packageLock.version, "3.2.3");
+ assert.equal(packageLock.packages[""].version, "3.2.3");
+ assert.equal(ecosystemManifest.version, "3.2.3");
assert.equal(manifest.devDependencies["ui-style-kit-css"], "2.4.0");
assert.equal(packageLock.packages[""].devDependencies["ui-style-kit-css"], "2.4.0");
assert.equal(packageLock.packages["node_modules/ui-style-kit-css"].version, "2.4.0");
diff --git a/test/pages-artifact.test.mjs b/test/pages-artifact.test.mjs
index 65a3b0f..b176c06 100644
--- a/test/pages-artifact.test.mjs
+++ b/test/pages-artifact.test.mjs
@@ -81,7 +81,7 @@ for (const { attribute, path } of [
const contents = readFileSync(join(outputDir, ...path.split("/")));
const fingerprint = createHash("sha256").update(contents).digest("hex").slice(0, 12);
assert(
- index.includes(`${attribute}="./${path}?v=3.2.1-${fingerprint}"`),
+ index.includes(`${attribute}="./${path}?v=3.2.3-${fingerprint}"`),
`Pages should fingerprint ${path} from its deployed contents.`
);
}
@@ -101,7 +101,7 @@ assert(
"Pages demo should preserve the source demo canonical URL"
);
assert(
- index.includes('"version": "3.2.1"') && index.includes("Layout Style CSS v3"),
+ index.includes('"version": "3.2.3"') && index.includes("Layout Style CSS v3"),
"Pages metadata should identify the v3 intrinsic responsive lab"
);
assert(
@@ -124,7 +124,7 @@ assert(
sitemap.includes(`${expectedPagesBaseUrl}`),
"The sitemap must use the case-sensitive Pages canonical URL."
);
-assert(sitemap.includes("2026-09-24"), "Pages sitemap should carry 3.2.1 metadata");
+assert(sitemap.includes("2026-09-26"), "Pages sitemap should carry 3.2.3 metadata");
const pagesWorkflow = readFileSync(pagesWorkflowPath, "utf8");
const pagesPreflightStep = pagesWorkflow.indexOf("- name: Verify Pages configuration");
diff --git a/test/release-docs-contract.test.mjs b/test/release-docs-contract.test.mjs
index 2da4c36..a3bfb18 100644
--- a/test/release-docs-contract.test.mjs
+++ b/test/release-docs-contract.test.mjs
@@ -100,7 +100,7 @@ assert(
);
for (const requiredText of [
- "3.2.1",
+ "3.2.3",
"Node.js 20",
"dependency-free",
"zero-configuration",
@@ -329,9 +329,11 @@ const nextMinorReleaseHeadings = changelog.match(/^## \[3\.2\.0\] - 2026-09-08$/
assert.equal(nextMinorReleaseHeadings.length, 1, "Changelog needs one dated 3.2.0 section.");
const responsivePatchHeadings = changelog.match(/^## \[3\.2\.1\] - 2026-09-24$/gm) ?? [];
assert.equal(responsivePatchHeadings.length, 1, "Changelog needs one dated 3.2.1 section.");
+const releaseRecoveryHeadings = changelog.match(/^## \[3\.2\.3\] - 2026-09-26$/gm) ?? [];
+assert.equal(releaseRecoveryHeadings.length, 1, "Changelog needs one dated 3.2.3 section.");
assert(
- release.includes("layout-style-css@3.2.1") &&
- release.includes("v3.2.1") &&
+ release.includes("layout-style-css@3.2.3") &&
+ release.includes("v3.2.3") &&
release.includes("does not publish"),
"Release documentation must distinguish verification from publication."
);
@@ -398,7 +400,7 @@ for (const browser of ["chromium", "firefox", "webkit"]) {
const publishWorkflow = read(".github", "workflows", "npm-publish.yml");
const demoSmoke = read("test", "demo-smoke.test.mjs");
-assert(publishWorkflow.includes("for example v3.2.1"));
+assert(publishWorkflow.includes("for example v3.2.3"));
assert(publishWorkflow.includes("playwright install --with-deps chromium firefox webkit"));
assert(
!/^\s*run:\s+npm\s+run\s+release:verify\s*$/m.test(publishWorkflow) &&
diff --git a/test/release-preflight.test.mjs b/test/release-preflight.test.mjs
index 138ac90..94d1c4f 100644
--- a/test/release-preflight.test.mjs
+++ b/test/release-preflight.test.mjs
@@ -46,6 +46,47 @@ test("pins an immutable reviewed UI release fixture and writes exact checkout ou
}
});
+test("resolves the published UI package from the reviewed revision instead of descendant worktree metadata", () => {
+ const tempRoot = fs.mkdtempSync(
+ path.join(os.tmpdir(), "layout-release-fixture-revision-"),
+ );
+ try {
+ assert.equal(spawnSync("git", ["init", tempRoot]).status, 0);
+ assert.equal(
+ spawnSync("git", ["-C", tempRoot, "config", "user.email", "fixture@example.test"]).status,
+ 0,
+ );
+ assert.equal(
+ spawnSync("git", ["-C", tempRoot, "config", "user.name", "Fixture Test"]).status,
+ 0,
+ );
+ fs.writeFileSync(
+ path.join(tempRoot, "ecosystem-compatibility.json"),
+ `${JSON.stringify({ supportedCombinations: { current: { "ui-style-kit-css": "2.4.0" } } }, null, 2)}\n`,
+ );
+ assert.equal(spawnSync("git", ["-C", tempRoot, "add", "ecosystem-compatibility.json"]).status, 0);
+ assert.equal(spawnSync("git", ["-C", tempRoot, "commit", "-m", "fixture baseline"]).status, 0);
+ const revision = spawnSync("git", ["-C", tempRoot, "rev-parse", "HEAD"], {
+ encoding: "utf8",
+ }).stdout.trim();
+ fs.writeFileSync(
+ path.join(tempRoot, "ecosystem-compatibility.json"),
+ `${JSON.stringify({ supportedCombinations: { current: { "ui-style-kit-css": "2.5.0" } } }, null, 2)}\n`,
+ );
+
+ assert.equal(
+ releaseContract.resolveFixturePackageSpecAtRevision(
+ tempRoot,
+ revision,
+ "ui-style-kit-css",
+ ),
+ "ui-style-kit-css@2.4.0",
+ );
+ } finally {
+ fs.rmSync(tempRoot, { recursive: true, force: true });
+ }
+});
+
test("local release verification accepts an explicit reviewed Interactive fixture", () => {
assert.equal(
releaseContract.resolveInteractiveRoot("C:/workspace/Layout-Style-CSS", {
@@ -59,6 +100,21 @@ test("local release verification accepts an explicit reviewed Interactive fixtur
);
});
+test("detects whether the reviewed UI preflight accepts coordinated companion candidates", () => {
+ assert.equal(
+ releaseContract.supportsCompanionCandidateRoot(
+ "if (arg === '--companion-candidate-root') parsed.push(value);",
+ ),
+ true,
+ );
+ assert.equal(
+ releaseContract.supportsCompanionCandidateRoot(
+ "throw new Error(`Unknown release-preflight option: ${arg}`);",
+ ),
+ false,
+ );
+});
+
test("stages active candidate version without mutating the reviewed UI fixture", () => {
assert.ok(
releaseContract,
@@ -96,7 +152,7 @@ test("stages active candidate version without mutating the reviewed UI fixture",
const staged = releaseContract.stageFixtureForCandidate(sourceRoot, {
candidatePackage: "layout-style-css",
- candidateVersion: "3.2.1",
+ candidateVersion: "3.2.3",
});
try {
assert.notEqual(staged.fixtureRoot, sourceRoot);
@@ -111,7 +167,7 @@ test("stages active candidate version without mutating the reviewed UI fixture",
"utf8",
),
).supportedCombinations.current["layout-style-css"],
- "3.2.1",
+ "3.2.3",
);
assert.equal(
JSON.parse(
@@ -153,11 +209,28 @@ test("staged preflight uses published UI only for the current matrix", () => {
"const currentArgs = [checkerPath, '--matrix', 'current', `--${candidateKey}-spec`, tarball];\n" +
"console.log(run(process.execPath, [checkerPath, '--matrix', 'minimum', '--skip-docs'], { cwd: fixtureRoot }));\n",
);
+ fs.writeFileSync(
+ path.join(tempRoot, "ecosystem-compatibility.json"),
+ `${JSON.stringify(
+ {
+ supportedCombinations: {
+ current: {
+ "ui-style-kit-css": "2.5.0",
+ "interactive-surface-css": "1.7.1",
+ "layout-style-css": "3.2.3",
+ },
+ },
+ },
+ null,
+ 2,
+ )}\n`,
+ );
try {
releaseContract.applyCurrentMatrixUiSpec(
tempRoot,
"ui-style-kit-css@2.4.0",
+ { "interactive-surface-css": "1.7.0" },
);
const patchedScript = fs.readFileSync(scriptPath, "utf8");
assert.match(
@@ -169,6 +242,26 @@ test("staged preflight uses published UI only for the current matrix", () => {
/\[checkerPath, '--matrix', 'minimum', '--skip-docs'\]/,
"The minimum matrix must keep using the fixture's published minimum packages.",
);
+ assert.equal(
+ JSON.parse(
+ fs.readFileSync(
+ path.join(tempRoot, "ecosystem-compatibility.json"),
+ "utf8",
+ ),
+ ).supportedCombinations.current["ui-style-kit-css"],
+ "2.4.0",
+ "The current matrix metadata must describe the reviewed published UI artifact.",
+ );
+ assert.equal(
+ JSON.parse(
+ fs.readFileSync(
+ path.join(tempRoot, "ecosystem-compatibility.json"),
+ "utf8",
+ ),
+ ).supportedCombinations.current["interactive-surface-css"],
+ "1.7.0",
+ "The current matrix metadata must retain the reviewed published Interactive artifact.",
+ );
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}