diff --git a/CHANGELOG.md b/CHANGELOG.md index d581422d..739a8c65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,33 +6,14 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim ## [Unreleased] -### Added -- Named the repeating editor chrome as a host-facing theme-token catalog and Storybook inventory so hosts can override `--cwl-*` custom properties on `.cwl-editor` after checking WCAG 2.2 contrast, without editing Inkspan internals. Color catalog values now distinguish light, dark, and `@media print` remaps; forced-colors mode is not treated as a token assignment. Hosts can call `getEditorThemeTokenContrast()` to compare inventoried pairs, including `--cwl-accent` on `--cwl-accent-soft`, against the 4.5:1 text threshold via `meetsTextContrast`. - -### Accessibility -- Prepared the active-PR dark active-toolbar accent change from protected-main `#4493f8` to `#58a6ff`, increasing `--cwl-accent` text on `--cwl-accent-soft: #163356` from about 4.13:1 to about 5.06:1 so the candidate default 13px active-button text meets the WCAG 2.2 4.5:1 normal-text threshold; this remains active-PR evidence until protected integration. `getEditorThemeTokenContrast()` checks catalog values, and host overrides must be re-checked with `contrastRatioFromHex(actualForegroundHex, actualBackgroundHex)` using the actual resolved colors. -- Restored a visible `:focus-visible` indicator on the editable textbox, mapped it to `CanvasText` in forced-colors mode, and suppresses that interactive focus chrome under `@media print`; dependency-locked Chromium, Firefox, and WebKit acceptance exercises the packed stylesheet on the real `role="textbox"` surface. - -### Security -- Raised workspace-wide transitive development-tool overrides for `fast-uri`, `nanoid`, and `postcss` to patched minimums, keeping the lockfile audit clean without changing runtime package authority. -- Normalized isolated package-verifier temporary roots before containment checks on macOS. - -## [0.6.0] — 2026-08-10 +## [0.6.0] — 2026-08-25 ### Release - Unified the npm editor and `inkspan-office` package manifests at **0.6.0** for the accepted stable OIDC release train; source preparation does not claim registry publication, and public npm/PyPI digest verification remains separate operational acceptance. -### Fixed -- Added the selected standalone Markdown or HTML value to an explicitly configured SSR native form field, preserving controlled-value precedence, external form association, React attribute escaping, and the synchronous post-hydration TipTap transaction mirror - -### Security -- Added a fail-closed draft release asset inventory gate that requires exactly one npm tarball, one Office wheel, `inkspan.spdx.json`, and `SHA256SUMS`, rejects stale or unexpected draft assets before immutable publication, and verifies every GitHub-reported `sha256:` asset digest against the transferred local file -- Kept SSR document disclosure opt-in through `formFieldName`; hidden-field values remain client-controlled submission data and do not replace host authentication, authorization, tenant isolation, CSRF defenses, server validation, durable concurrency, or persistence controls -- Kept collaborative Yjs document content out of server markup until the host-owned client collaboration lifecycle is bound -- Added packed headless Markdown authority verification that rejects external runtime imports, dynamic module loaders, ambient network/environment credential access, React/TipTap/Yjs runtime coupling, CWL host coupling, and model credential references from the dedicated conversion artifact -- Added OIDC Trusted Publishing for the exact validated npm tarball and Office wheel with registry-side credentials kept out of source, build jobs, and long-lived repository secrets; registry publication is verified against the exact artifact digests after publication. - ### Added +- Named the repeating editor chrome as a host-facing theme-token catalog and Storybook inventory so hosts can override `--cwl-*` custom properties on `.cwl-editor` after checking WCAG 2.2 contrast, without editing Inkspan internals. Color catalog values now distinguish light, dark, and `@media print` remaps; forced-colors mode is not treated as a token assignment. Hosts can call `getEditorThemeTokenContrast()` to compare inventoried pairs, including `--cwl-accent` on `--cwl-accent-soft`, against the 4.5:1 text threshold via `meetsTextContrast`. +- Added `docs/product-technical-gap-baseline.md` as the bounded product/technical gap baseline with a documented refresh procedure for the live open-PR/Issue queue, release/registry/alert state, and governance rules. - Added `@contextualwisdomlab/cwl-editor/markdown` as a headless ESM/CommonJS/TypeScript conversion subpath exposing the existing deterministic Markdown/HTML/email/plain-text serializers while sharing framework-neutral safe-link and strict inline-raster policy with the editor instead of importing the React/TipTap extension graph - Added bounded `inspectDocumentEnvelopeIdentity()` and `inspectDocumentEnvelopeIdentityBytes()` routing metadata plus the framework-independent `envelope-identity` package subpath so hosts can select explicit schema migrations without exposing document bodies, weakening the strict current-schema parser, or moving migration/persistence authority into Inkspan - Added one optional construction-time `onSnapshotChange` callback to the framework-free autosave queue and durable autosave session so hosts can observe saving, pending, blocked, recovery, idle, and shutdown state without polling or introducing a subscriber collection @@ -43,11 +24,25 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim - Expanded deterministic DOCX fidelity with bounded informative inline PNG figures and alternative text, rich-text paragraph runs, and bounded paragraph and heading alignment while preserving fail-closed schema/runtime limits and Python 3.11–3.14 verification. - Added bounded relationship-backed external HTTP(S) hyperlinks to DOCX rich-text runs while preserving deterministic network-free rendering, existing run emphasis, fail-closed URI validation, redacted errors, and Python 3.11–3.14 verification. +### Fixed +- Added the selected standalone Markdown or HTML value to an explicitly configured SSR native form field, preserving controlled-value precedence, external form association, React attribute escaping, and the synchronous post-hydration TipTap transaction mirror + +### Security +- Raised workspace-wide transitive development-tool overrides for `fast-uri`, `nanoid`, and `postcss` to patched minimums (fast-uri 3.1.5, nanoid 3.3.18, postcss 8.5.23), keeping the lockfile audit clean without changing runtime package authority. +- Normalized isolated package-verifier temporary roots before containment checks on macOS. +- Added a fail-closed draft release asset inventory gate that requires exactly one npm tarball, one Office wheel, `inkspan.spdx.json`, and `SHA256SUMS`, rejects stale or unexpected draft assets before immutable publication, and verifies every GitHub-reported `sha256:` asset digest against the transferred local file +- Kept SSR document disclosure opt-in through `formFieldName`; hidden-field values remain client-controlled submission data and do not replace host authentication, authorization, tenant isolation, CSRF defenses, server validation, durable concurrency, or persistence controls +- Kept collaborative Yjs document content out of server markup until the host-owned client collaboration lifecycle is bound +- Added packed headless Markdown authority verification that rejects external runtime imports, dynamic module loaders, ambient network/environment credential access, React/TipTap/Yjs runtime coupling, CWL host coupling, and model credential references from the dedicated conversion artifact +- Added OIDC Trusted Publishing for the exact validated npm tarball and Office wheel with registry-side credentials kept out of source, build jobs, and long-lived repository secrets; registry publication is verified against the exact artifact digests after publication. + ### Reliability - Lifecycle observers receive only distinct frozen document-free snapshots; observer exceptions cannot alter save ordering, conflict/failure recovery, queue outcomes, or durable-validator handoff - Durable-session notifications expose a newly committed server validator only after it is coherent with the emitted lifecycle state, preserving host-owned atomic RFC 9110 `If-Match` semantics ### Accessibility +- Changed the dark active-toolbar accent from protected-main `#4493f8` to `#58a6ff`, increasing `--cwl-accent` text on `--cwl-accent-soft: #163356` from about 4.13:1 to about 5.06:1 so the default 13px active-button text meets the WCAG 2.2 4.5:1 normal-text threshold. `getEditorThemeTokenContrast()` checks catalog values, and host overrides must be re-checked with `contrastRatioFromHex(actualForegroundHex, actualBackgroundHex)` using the actual resolved colors. +- Restored a visible `:focus-visible` indicator on the editable textbox, mapped it to `CanvasText` in forced-colors mode, and suppresses that interactive focus chrome under `@media print`; dependency-locked Chromium, Firefox, and WebKit acceptance exercises the packed stylesheet on the real `role="textbox"` surface. - Require explicit author alternative-text intent before inline image insertion: non-empty text is stored as `alt`, an explicitly submitted empty response marks the image decorative with `alt=""`, cancellation inserts nothing, and conversion failures still abort before prompting. - Added programmatic toolbar shortcut discoverability with WAI-ARIA `aria-keyshortcuts` for the implemented bold, italic, link, undo, and redo commands, preserving the same native-button behavior, visible labels, roving focus model, and host-owned shortcut-conflict policy - Completed redo shortcut metadata with `Control+Shift+Z Meta+Shift+Z Control+Y Meta+Y`, matching the configured Tiptap history and collaboration behavior and exposing both `Ctrl/Cmd+Shift+Z` and `Ctrl/Cmd+Y` alternatives without adding new key handling diff --git a/docs/DOCUMENTATION_FITNESS.md b/docs/DOCUMENTATION_FITNESS.md index 1fc994f9..668d85a9 100644 --- a/docs/DOCUMENTATION_FITNESS.md +++ b/docs/DOCUMENTATION_FITNESS.md @@ -56,7 +56,7 @@ Document fitness and implementation maturity are independent. A `present_current | Unified stable registry release train | ADR 0019, protected release workflow and release doctoring | `present_current` | `implemented_on_protected_main` | Stable npm/Office version equality, OIDC Trusted Publishing, exact-artifact publication and post-publication digest verification are source-integrated; live registry publication remains separate operational evidence. | | Framework-neutral Markdown package boundary | ADR 0020, protected `@contextualwisdomlab/cwl-editor/markdown` package subpath and shared policy modules from #114 | `present_current` | `implemented_on_protected_main` | Server/worker consumers can reuse deterministic Markdown/HTML/email/plain-text conversion without evaluating the React/TipTap editor graph. | | CSS paged-media print boundary | ADR 0021, protected `src/styles.css`, packaged stylesheet evidence and real-browser print tests from #116/#127 | `present_current` | `implemented_on_protected_main` | Declarative print output removes screen-only clipping/chrome while preserving authored content without creating a durable PDF service. | -| Editor chrome design tokens / Storybook inventory | ADR 0031, `docs/design-tokens.md`, doctoring, token catalog, and Storybook stories | `present_current` | `implemented_on_active_pr` | Hosts can name, override, and preview repeating toolbar/editor tokens without treating the interchange snapshot as shipped protected-main authority. | +| Editor chrome design tokens / Storybook inventory | ADR 0031, `docs/design-tokens.md`, doctoring, token catalog, and Storybook stories | `present_current` | `implemented_on_protected_main` | Hosts can name, override, and preview the shipped repeating toolbar/editor token catalog; CSS remains presentation authority and host overrides retain host-owned accessibility verification. | | Informative DOCX PNG figures | ADR 0022, Office schema/renderer/tests and guidance | `present_current` | `implemented_on_protected_main` | Strict bounded inline PNG figures preserve informative alternative text without remote-resource or arbitrary OOXML authority. | | DOCX bounded rich-text runs | ADR 0023, Office schema/renderer/tests and doctoring | `present_current` | `implemented_on_protected_main` | Ordered bold/italic/underline runs preserve common inline fidelity under one bounded deterministic contract. | | DOCX bounded paragraph alignment | ADR 0024, Office schema/renderer/tests, Office guidance and doctoring | `present_current` | `implemented_on_protected_main` | `paragraph` and `rich_paragraph` preserve explicit left/center/right/justify alignment while omission retains inherited/default behavior. | @@ -84,7 +84,6 @@ The documentation pack is substantially complete for acquisition review, but rep 1. The protected manifests now agree at `0.6.0`, while registry operational acceptance remains open under issue #118 because the exact protected release still needs its tag/GitHub Release, live npm/PyPI Trusted Publisher execution, and public artifact digest verification. 2. Future protected-source changes must continue to reconcile PRD/TRD/Architecture/ADR/UML/DATA_MODEL/security/test/operability/traceability semantics rather than treating this baseline as permanently complete. 3. Documentation becoming mergeable, green, or protected-merged is never a reason for the commercial loop to stop; the next safe product, release, security, accessibility, package, Office-fidelity, or interoperability lane continues. -4. Hosts still need a named, Storybook-previewable chrome-token catalog so brand theming does not require editing Inkspan internals; that lane is Active PR / Proposed and must not be described as shipped until protected integration. ## Sufficiency decision diff --git a/docs/UML.md b/docs/UML.md index de4e20d3..ca2b6ac9 100644 --- a/docs/UML.md +++ b/docs/UML.md @@ -38,7 +38,7 @@ flowchart LR The host owns transport, authentication, authorization, tenant isolation, persistence, credentials, provider lifecycle, retention, deployment, durable audit, and model-use policy. Inkspan owns deterministic local editor/conversion/evidence behavior only. -A named editor-chrome theme-token catalog and Storybook inventory are Active PR / Proposed. Hosts override `--cwl-*` on `.cwl-editor` after checking WCAG 2.2 contrast for body text and active toolbar text (`--cwl-accent` on `--cwl-accent-soft`). CSS remains runtime presentation authority. Storybook previews a class-level chrome sample (including `:focus-visible`) and a live shipped Toolbar. It does not mount `CwlEditor`. Print-media remaps live in `@media print`; forced-colors mode does not assign those custom properties. Figma Variables, brand certification, and design-tool sync remain host-owned. +A named editor-chrome theme-token catalog and Storybook inventory are shipped editor chrome capabilities. Hosts override `--cwl-*` on `.cwl-editor` after checking WCAG 2.2 contrast for body text and active toolbar text (`--cwl-accent` on `--cwl-accent-soft`). CSS remains runtime presentation authority. Storybook previews a class-level chrome sample (including `:focus-visible`) and a live shipped Toolbar. It does not mount `CwlEditor`. Print-media remaps live in `@media print`; forced-colors mode does not assign those custom properties. Figma Variables, brand certification, and design-tool sync remain host-owned. ## Rich paste sequence diff --git a/docs/adr/0031-editor-design-tokens-storybook.md b/docs/adr/0031-editor-design-tokens-storybook.md index 2e3cfdb0..60083392 100644 --- a/docs/adr/0031-editor-design-tokens-storybook.md +++ b/docs/adr/0031-editor-design-tokens-storybook.md @@ -1,12 +1,12 @@ # ADR 0031: Editor chrome design tokens and Storybook inventory -Status: Proposed +Status: Accepted (2026-08-25; integrated on protected `main` via #362) ## Context -Inkspan ships repeating toolbar buttons, groups, and editor chrome styled through `--cwl-*` custom properties. Hosts already re-theme by overriding those properties, but the names, light/dark/print values, and buyer next action live only inside protected `src/styles.css`. There is no protected-main typed catalog, Design Tokens Format Module interchange snapshot, or Storybook inventory of the repeating objects; those capabilities are proposed on this active PR. +Inkspan ships repeating toolbar buttons, groups, and editor chrome styled through `--cwl-*` custom properties. Hosts already re-theme by overriding those properties, but before this decision the names, light/dark/print values, and host next action lived only inside protected `src/styles.css`, with no typed catalog, Design Tokens Format Module interchange snapshot, or Storybook inventory of the repeating objects. -Inventorying the actual active-toolbar foreground/background pair also exposed a product-owned accessibility defect: protected main's dark `--cwl-accent: #4493f8` on `--cwl-accent-soft: #163356` produces about 4.13:1 for 13px active-button text, below the WCAG 2.2 4.5:1 normal-text threshold. Treating that shipped-default failure as a host override responsibility would contradict Inkspan's ownership of its default presentation. +Inventorying the actual active-toolbar foreground/background pair also exposed a product-owned accessibility defect: the former dark `--cwl-accent: #4493f8` on `--cwl-accent-soft: #163356` produced about 4.13:1 for 13px active-button text, below the WCAG 2.2 4.5:1 normal-text threshold. Treating that shipped-default failure as a host override responsibility would contradict Inkspan's ownership of its default presentation. ## Alternatives considered @@ -18,11 +18,11 @@ Inventorying the actual active-toolbar foreground/background pair also exposed a ## Decision -If integrated, Inkspan will publish a host-facing theme-token catalog for nine inventoried chrome tokens, a Design Tokens Format Module 2025.10 interchange snapshot, and a Storybook inventory of repeating toolbar/editor objects. Inkspan's inventoried normal-text pairs will be required to meet the WCAG 2.2 4.5:1 threshold in the resulting protected light/dark/print defaults; the active-PR candidate therefore uses `#58a6ff` on `#163356` for dark active-toolbar text, about 5.06:1. `getEditorThemeTokenContrast()` evaluates only the catalog values for a named scheme. Hosts overriding `--cwl-*` on `.cwl-editor` must pass their actual resolved foreground/background hex values to `contrastRatioFromHex()` and re-check their resulting body and active-toolbar pairs. Unknown token names fail closed. No Figma, network, persistence, credential, or model authority is added. +Inkspan publishes a host-facing theme-token catalog for nine inventoried chrome tokens, a Design Tokens Format Module 2025.10 interchange snapshot, and a Storybook inventory of repeating toolbar/editor objects. Inkspan's inventoried normal-text pairs are required to meet the WCAG 2.2 4.5:1 threshold in the protected light/dark/print defaults; the integrated dark pair uses `#58a6ff` on `#163356` for dark active-toolbar text, about 5.06:1. `getEditorThemeTokenContrast()` evaluates only the catalog values for a named scheme. Hosts overriding `--cwl-*` on `.cwl-editor` must pass their actual resolved foreground/background hex values to `contrastRatioFromHex()` and re-check their resulting body and active-toolbar pairs. Unknown token names fail closed. No Figma, network, persistence, credential, or model authority is added. ## Consequences and ownership trade-offs -After integration, hosts gain a copyable token list and a Storybook preview. Inkspan keeps CSS as the runtime source and owns accessibility defects in its default token combinations. naruon and other CWL hosts can apply their own overrides through host-owned brand CSS, use `getEditorThemeTokenContrast()` to inspect the Inkspan catalog baseline, and use `contrastRatioFromHex()` to validate their actual resolved custom values. Complete DTCG conformance, Figma Variables, host-theme WCAG certification, and automated remediation of arbitrary host palettes remain out of scope. +Hosts gain a copyable token list and a Storybook preview. Inkspan keeps CSS as the runtime source and owns accessibility defects in its default token combinations. naruon and other CWL hosts can apply their own overrides through host-owned brand CSS, use `getEditorThemeTokenContrast()` to inspect the Inkspan catalog baseline, and use `contrastRatioFromHex()` to validate their actual resolved custom values. Complete DTCG conformance, Figma Variables, host-theme WCAG certification, and automated remediation of arbitrary host palettes remain out of scope. ## Failure and recovery @@ -30,16 +30,16 @@ An unknown token name throws `EditorThemeTokenError` without reflecting caller i ## Security and privacy impact -The proposed catalog contains only public presentation values. It does not carry document bodies, tenant identifiers, credentials, or diagnostics. Storybook is a local development preview and is not a production transport. +The catalog contains only public presentation values. It does not carry document bodies, tenant identifiers, credentials, or diagnostics. Storybook is a local development preview and is not a production transport. ## Compatibility and migration -The catalog is additive if integrated. Existing CSS overrides on `.cwl-editor` continue to work. The active-PR candidate changes the dark default accent from `#4493f8` to `#58a6ff`; hosts that already override `--cwl-accent` are unaffected by that candidate default-value change but remain responsible for validating their actual custom pair. A later CSS token addition or default-value change must update the catalog, directly affected documentation/tests, and this ADR together. +The catalog is additive. Existing CSS overrides on `.cwl-editor` continue to work. The integrated dark default accent is `#58a6ff` rather than the former `#4493f8`; hosts that already override `--cwl-accent` are unaffected by the shipped default-value change but remain responsible for validating their actual custom pair. A later CSS token addition or default-value change must update the catalog, directly affected documentation/tests, and this ADR together. ## Verification and acceptance evidence -Required evidence includes token-catalog tests against `src/styles.css`, deterministic contrast assertions for inventoried normal-text pairs, resolved-hex override guidance tests, documentation-contract tests, Storybook inventory stories for toolbar button states and token swatches, and exact-head CI/coverage/package/security gates on the unchanged head. The accessibility regression must fail against protected main's `#4493f8`/`#163356` dark active pair and pass against the active-PR `#58a6ff`/`#163356` pair. This ADR stays Proposed until protected integration. +Required evidence includes token-catalog tests against `src/styles.css`, deterministic contrast assertions for inventoried normal-text pairs, resolved-hex override guidance tests, documentation-contract tests, Storybook inventory stories for toolbar button states and token swatches, and exact-head CI/coverage/package/security gates on the unchanged head. The accessibility regression records the former `#4493f8`/`#163356` dark active pair as failing and requires the integrated `#58a6ff`/`#163356` pair to pass. ADR 0031 is Accepted because #362 integrated this contract on protected `main`. ## Rollback or supersession -Before integration, rollback removes the catalog export, Storybook inventory/config, operator/doctoring records, and this ADR together. After integration, reverting only the compliant dark accent while retaining the active-text contrast requirement is not a valid partial rollback. Supersession requires a new ADR if CSS ceases to be runtime presentation authority or if a design-tool sync contract is accepted. +A protected rollback must revert the catalog export, Storybook inventory/config, operator/doctoring records, and this ADR together. Reverting only the compliant dark accent while retaining the active-text contrast requirement is not a valid partial rollback. Supersession requires a new ADR if CSS ceases to be runtime presentation authority or if a design-tool sync contract is accepted. diff --git a/docs/adr/README.md b/docs/adr/README.md index b797812e..0140dd02 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -32,7 +32,7 @@ This index records durable architectural decisions. Protected-main implementatio | [0024](0024-bounded-docx-paragraph-alignment.md) | Accepted | Bounded paragraph alignment in deterministic DOCX output | | [0025](0025-bounded-docx-heading-alignment.md) | Accepted | Bounded heading alignment in deterministic DOCX output | | [0026](0026-bounded-docx-external-hyperlinks.md) | Accepted | Bounded external hyperlinks in deterministic DOCX rich text | -| [0031](0031-editor-design-tokens-storybook.md) | Proposed | Editor chrome design tokens and Storybook inventory | +| [0031](0031-editor-design-tokens-storybook.md) | Accepted | Editor chrome design tokens and Storybook inventory | ## Decision discipline diff --git a/docs/design-tokens.md b/docs/design-tokens.md index b1056994..af4c5182 100644 --- a/docs/design-tokens.md +++ b/docs/design-tokens.md @@ -1,8 +1,8 @@ # Editor chrome design tokens -Status: Active PR / Proposed +Status: Shipped protected-main truth (2026-08-25) -Use this catalog when you need to re-theme Inkspan's repeating toolbar and editor chrome. Protected-main CSS defaults remain the shipped presentation baseline; the Active-PR repaired dark active-toolbar pair uses `--cwl-accent: #58a6ff` on `--cwl-accent-soft: #163356` and measures about 5.06:1. When a host overrides any color token, re-check WCAG 2.2 contrast for both body text (`--cwl-fg` on `--cwl-bg`) and active toolbar text (`--cwl-accent` on `--cwl-accent-soft`). Do not edit Inkspan internals. +Use this catalog when you need to re-theme Inkspan's repeating toolbar and editor chrome. Protected-main CSS defaults are the shipped presentation baseline; the shipped dark active-toolbar pair uses `--cwl-accent: #58a6ff` on `--cwl-accent-soft: #163356` and measures about 5.06:1. When a host overrides any color token, re-check WCAG 2.2 contrast for both body text (`--cwl-fg` on `--cwl-bg`) and active toolbar text (`--cwl-accent` on `--cwl-accent-soft`). Do not edit Inkspan internals. `getEditorThemeTokenContrast()` checks only Inkspan catalog values for the requested light/dark/print scheme; it does not read resolved host CSS. After an override, obtain the actual resolved hex colors from the host theme and call `contrastRatioFromHex(actualForegroundHex, actualBackgroundHex)` before shipping that theme. @@ -35,7 +35,7 @@ void tokens; void dtcgGroup; ``` -The default-theme checks above are active-PR product evidence, not protected-main shipped evidence or a host WCAG certification. The stylesheet remains runtime presentation authority. `toDesignTokenFormatGroup()` is an interchange snapshot aligned to Design Tokens Format Module 2025.10; it is not complete DTCG conformance or Figma Variables sync. +The default-theme checks above are protected-main shipped evidence, not a host WCAG certification. The stylesheet remains runtime presentation authority. `toDesignTokenFormatGroup()` is an interchange snapshot aligned to Design Tokens Format Module 2025.10; it is not complete DTCG conformance or Figma Variables sync. Preview the repeating objects in Storybook (`pnpm storybook`) using the inventory in [`storybook-inventory.md`](storybook-inventory.md). diff --git a/docs/doctoring/editor-design-tokens.md b/docs/doctoring/editor-design-tokens.md index 17846810..d64b6c64 100644 --- a/docs/doctoring/editor-design-tokens.md +++ b/docs/doctoring/editor-design-tokens.md @@ -1,24 +1,22 @@ # Doctoring record: editor chrome design tokens **Date:** 2026-08-16 -**Status:** Active PR / Proposed +**Status:** Shipped protected-main truth (2026-08-25) **Decision owner:** ContextualWisdomLab **Scope:** Named `--cwl-*` theme tokens, DTCG 2025.10 interchange snapshot, and Storybook inventory for repeating toolbar/editor objects. ## Buyer-visible gap -Hosts embed Inkspan and need to match brand color, radius, and font without forking `src/styles.css`. The protected stylesheet already uses `--cwl-*` custom properties, but buyers have no protected-main typed catalog, interchange snapshot, or Storybook inventory of the repeating toolbar button and editor chrome. Theme work therefore still requires reading CSS internals until this active PR integrates. +Hosts embed Inkspan and need to match brand color, radius, and font without forking `src/styles.css`. The protected stylesheet uses `--cwl-*` custom properties, and Inkspan now ships a protected-main typed catalog, interchange snapshot, and Storybook inventory of the repeating toolbar button and editor chrome. Hosts can therefore discover and override the inventoried chrome through the supported catalog instead of reading CSS internals, while remaining responsible for validating the resulting host palette. -The same inventory exposed an Inkspan-owned protected-main default-theme defect rather than a host-only customization problem: dark `.cwl-tb-btn.is-active` renders `--cwl-accent` text on `--cwl-accent-soft` at about 4.13:1, below the WCAG 2.2 4.5:1 threshold for normal text. This Active PR now carries dark `--cwl-accent: #58a6ff` against unchanged `--cwl-accent-soft: #163356`, producing about 5.06:1 for the active toolbar pair. That repaired value is active-PR evidence, not shipped protected-main truth, until integration. Host overrides must still re-check their own resulting pairs. +The same inventory exposed a formerly shipped Inkspan default-theme defect rather than a host-only customization problem: dark `.cwl-tb-btn.is-active` rendered `--cwl-accent: #4493f8` text on `--cwl-accent-soft: #163356` at about 4.13:1, below the WCAG 2.2 4.5:1 threshold for normal text. Inkspan now ships dark `--cwl-accent: #58a6ff` against unchanged `--cwl-accent-soft: #163356`, producing about 5.06:1 for the active toolbar pair. That repaired value is shipped protected-main truth. Host overrides must still re-check their own resulting pairs. If contrast fails after a re-theme, override only the named tokens on `.cwl-editor` and re-check WCAG 2.2 text contrast for `--cwl-fg` on `--cwl-bg` and `--cwl-accent` on `--cwl-accent-soft`. `getEditorThemeTokenContrast()` reports Inkspan's catalog baseline only; after a host override, pass the actual resolved pair to `contrastRatioFromHex(actualForegroundHex, actualBackgroundHex)` before shipping. Do not disable forced-colors overrides. ## Decision -If integrated: - 1. Keep `src/styles.css` as runtime presentation authority. -2. Publish `listEditorThemeTokens()` / `getEditorThemeToken()` / `getEditorThemeTokenContrast()` / `contrastRatioFromHex()` / `toDesignTokenFormatGroup()` as a host-facing catalog of the nine inventoried chrome tokens and theme evidence: the name-based contrast helper evaluates the catalog baseline, while the hex helper evaluates actual resolved host colors. +2. Publish `listEditorThemeTokens()` / `getEditorThemeToken()` / `getEditorThemeTokenContrast()` / `contrastRatioFromHex()` / `toDesignTokenFormatGroup()` as a host-facing catalog of the nine shipped chrome tokens and theme evidence: the name-based contrast helper evaluates the catalog baseline, while the hex helper evaluates actual resolved host colors. 3. Keep the catalog light/dark/print values synchronized with the stylesheet and require the inventoried active-toolbar text pair to meet the WCAG 2.2 4.5:1 threshold in Inkspan's own default themes. 4. Reject unknown token names with a stable payload-redacted `EditorThemeTokenError`. 5. Preview repeating `.cwl-tb-btn` states, the shipped Toolbar component, and token swatches in Storybook. @@ -26,26 +24,26 @@ If integrated: ## Standards rationale -The Design Tokens Format Module 2025.10 defines a vendor-neutral JSON interchange for token groups, `$type`, and `$value` (Design Tokens Community Group, 2025). This active PR emits a snapshot of Inkspan CSS custom properties in that shape. The report is a W3C Community Final Specification, not a W3C Standard, so this record does not claim W3C standardization or complete DTCG conformance. +The Design Tokens Format Module 2025.10 defines a vendor-neutral JSON interchange for token groups, `$type`, and `$value` (Design Tokens Community Group, 2025). The protected-main implementation emits a snapshot of Inkspan CSS custom properties in that shape. The report is a W3C Community Final Specification, not a W3C Standard, so this record does not claim W3C standardization or complete DTCG conformance. -WCAG 2.2 requires at least 4.5:1 contrast for normal text under Success Criterion 1.4.3 and at least 3:1 for meaningful user-interface component boundaries/states under Success Criterion 1.4.11 (World Wide Web Consortium, 2024). The protected-main failing default is therefore an Inkspan-owned defect; this active PR repairs it at Inkspan's presentation boundary while host overrides remain the host's contrast responsibility. Storybook's React/Vite preview is the proposed inventory surface for repeating chrome (Storybook, n.d.). +WCAG 2.2 requires at least 4.5:1 contrast for normal text under Success Criterion 1.4.3 and at least 3:1 for meaningful user-interface component boundaries/states under Success Criterion 1.4.11 (World Wide Web Consortium, 2024). The former protected default therefore represented an Inkspan-owned defect; the integrated repair fixes it at Inkspan's presentation boundary while host overrides remain the host's contrast responsibility. Storybook's React/Vite preview is the shipped inventory surface for repeating chrome (Storybook, n.d.). ## Test-first evidence - Original RED: `src/designTokens.test.ts` failed because `./designTokens.js` did not exist. -- Initial GREEN: the active-PR catalog lists nine inventoried tokens, aligns light/dark/print color values with the matching `src/styles.css` media blocks, reports WCAG 2.2 contrast for inventoried color pairs, rejects unknown names without reflecting caller input, and emits a DTCG 2025.10 group. -- Accessibility RED: exact test-only head `a831359d1509811ab8777e7356f6ebd5f251b5cf` changed the active-chrome contract to require the dark `--cwl-accent` / `--cwl-accent-soft` pair to meet 4.5:1. The protected-main values remained `#4493f8` on `#163356` (about 4.13:1), so the new expectation could not pass without a real default-theme change. -- Accessibility GREEN: the active-PR dark `--cwl-accent` is `#58a6ff` in both the typed catalog and runtime stylesheet; against unchanged `#163356` it measures about 5.06:1 and `meetsTextContrast` is true. +- Initial GREEN: the catalog lists nine inventoried tokens, aligns light/dark/print color values with the matching `src/styles.css` media blocks, reports WCAG 2.2 contrast for inventoried color pairs, rejects unknown names without reflecting caller input, and emits a DTCG 2025.10 group. +- Accessibility RED: exact test-only head `a831359d1509811ab8777e7356f6ebd5f251b5cf` changed the active-chrome contract to require the dark `--cwl-accent` / `--cwl-accent-soft` pair to meet 4.5:1. The then-protected values remained `#4493f8` on `#163356` (about 4.13:1), so the new expectation could not pass without a real default-theme change. +- Accessibility GREEN: the integrated dark `--cwl-accent` is `#58a6ff` in both the typed catalog and runtime stylesheet; against unchanged `#163356` it measures about 5.06:1 and `meetsTextContrast` is true. - Override-truth RED: exact-head CI `32149806678` / build-test `95752733876` proved the documentation and API action text still conflated catalog-token contrast with resolved host override contrast. - Override-truth repair: `getEditorThemeTokenContrast()` is explicitly catalog-only and custom host themes are checked with `contrastRatioFromHex()` using the actual resolved colors. ## Residual risk -Print media still remaps the color tokens after a host override. Forced-colors mode only restyles the toolbar focus outline to `CanvasText`; it does not assign `--cwl-*` values. The active-PR repaired dark `.cwl-tb-btn.is-active` pair (`--cwl-accent` on `--cwl-accent-soft`) meets the WCAG 2.2 4.5:1 normal-text threshold and also exceeds the 3:1 non-text threshold; protected main remains at the failing pre-repair pair until integration. `getEditorThemeTokenContrast()` reports only that catalog evidence; after overriding either token, hosts must call `contrastRatioFromHex(actualForegroundHex, actualBackgroundHex)` with the actual resolved values because custom values can reintroduce a contrast failure. The font token snapshot splits a CSS font-family list and does not execute CSS. Storybook success is not Chromium/Firefox/WebKit release evidence. +Print media still remaps the color tokens after a host override. Forced-colors mode only restyles the toolbar focus outline to `CanvasText`; it does not assign `--cwl-*` values. The shipped repaired dark `.cwl-tb-btn.is-active` pair (`--cwl-accent` on `--cwl-accent-soft`) meets the WCAG 2.2 4.5:1 normal-text threshold and also exceeds the 3:1 non-text threshold. `getEditorThemeTokenContrast()` reports only that catalog evidence; after overriding either token, hosts must call `contrastRatioFromHex(actualForegroundHex, actualBackgroundHex)` with the actual resolved values because custom values can reintroduce a contrast failure. The font token snapshot splits a CSS font-family list and does not execute CSS. Storybook success is not Chromium/Firefox/WebKit release evidence. ## Rollback -Rollback before integration removes the catalog export, this record, the operator guide, the Storybook inventory/config/stories, ADR 0031, the changelog entry, and the documentation-index rows together. After integration, reverting the compliant dark accent without also reverting the active-pair contrast contract would deliberately recreate a known accessibility defect and is not a valid partial rollback. +Rollback of the integrated capability must revert the catalog export, this record, the operator guide, the Storybook inventory/config/stories, ADR 0031, the changelog entry, and the documentation-index rows together. Reverting the compliant dark accent without also reverting the active-pair contrast contract would deliberately recreate a known accessibility defect and is not a valid partial rollback. ## References (APA 7th edition) diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index 239e40f6..bca53676 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -1,8 +1,8 @@ # Storybook inventory -Status: Active PR / Proposed +Status: Shipped protected-main truth (2026-08-25) -Run `pnpm storybook` to preview repeating Inkspan chrome before you override tokens in a host. **Active-PR repaired defaults require no host override for the inventoried active-toolbar contrast pair; protected main remains the pre-repair shipped baseline until integration.** Override tokens only when re-theming, and re-check the resulting host palette. The button-state story is a class-level chrome sample. **Editor Chrome / Live Toolbar** mounts the shipped Toolbar component so a token change is visible on the same object buyers already use; the Storybook story and repaired accent remain Active PR / Proposed. These stories do not mount `CwlEditor`. +Run `pnpm storybook` to preview repeating Inkspan chrome before you override tokens in a host. **Shipped defaults require no host override for the inventoried active-toolbar contrast pair.** Override tokens only when re-theming, and re-check the resulting host palette. The button-state story is a class-level chrome sample. **Editor Chrome / Live Toolbar** mounts the shipped Toolbar component so a token change is visible on the same object buyers already use; the Storybook story and repaired accent are shipped protected-main truth. These stories do not mount `CwlEditor`. | Story | Repeating object | Host next action | | --- | --- | --- | diff --git a/scripts/release-metadata.test.mjs b/scripts/release-metadata.test.mjs index 768e4ec4..bb61bc85 100644 --- a/scripts/release-metadata.test.mjs +++ b/scripts/release-metadata.test.mjs @@ -9,6 +9,60 @@ function readRepositoryText(path) { return readFileSync(new URL(`../${path}`, import.meta.url), 'utf8'); } +/** Find one real Markdown heading while ignoring fenced-code lookalikes. */ +function findMarkdownHeadingIndex(markdown, heading) { + let offset = 0; + let openFence = null; + + for (const sourceLine of markdown.split('\n')) { + const line = sourceLine.endsWith('\r') ? sourceLine.slice(0, -1) : sourceLine; + const fenceStart = line.match(/^[ \t]{0,3}(`{3,}|~{3,})/u); + + if (openFence === null && fenceStart !== null) { + openFence = fenceStart[1]; + } else if (openFence !== null) { + const fenceEnd = line.match(/^[ \t]{0,3}(`{3,}|~{3,})[ \t]*$/u); + if ( + fenceEnd !== null && + fenceEnd[1][0] === openFence[0] && + fenceEnd[1].length >= openFence.length + ) { + openFence = null; + } + } else if (line.trimEnd() === heading) { + return offset; + } + + offset += sourceLine.length + 1; + } + + return -1; +} + +test('recognizes only a real Unreleased heading outside fenced code', () => { + assert.equal( + findMarkdownHeadingIndex( + 'ordinary text mentioning ## [Unreleased]\n## [0.6.0] — 2026-08-25\n', + '## [Unreleased]', + ), + -1, + ); + assert.equal( + findMarkdownHeadingIndex( + '```md\n## [Unreleased]\n```\n## [0.6.0] — 2026-08-25\n', + '## [Unreleased]', + ), + -1, + ); + assert.equal( + findMarkdownHeadingIndex( + 'Preface\n## [Unreleased] \n## [0.6.0] — 2026-08-25\n', + '## [Unreleased]', + ), + 'Preface\n'.length, + ); +}); + test('binds package metadata to the current dated changelog release candidate', () => { const packageManifest = JSON.parse(readRepositoryText('package.json')); const changelog = readRepositoryText('CHANGELOG.md'); @@ -18,12 +72,11 @@ test('binds package metadata to the current dated changelog release candidate', 'm', ); const headingMatch = changelog.match(releaseHeading); + const unreleasedIndex = findMarkdownHeadingIndex(changelog, '## [Unreleased]'); assert.notEqual(headingMatch, null); - assert.equal( - changelog.indexOf('## [Unreleased]') < (headingMatch?.index ?? -1), - true, - ); + assert.notEqual(unreleasedIndex, -1); + assert.equal(unreleasedIndex < (headingMatch?.index ?? -1), true); assert.equal( changelog.includes( `Unified the npm editor and \`inkspan-office\` package manifests at **${packageManifest.version}**`, diff --git a/src/designTokenDocumentation.test.ts b/src/designTokenDocumentation.test.ts index bfd752e2..024c77f3 100644 --- a/src/designTokenDocumentation.test.ts +++ b/src/designTokenDocumentation.test.ts @@ -17,13 +17,14 @@ describe('editor design-token documentation contract', () => { const inventory = normalize(repositoryFile('docs/storybook-inventory.md')); const doctoring = repositoryFile('docs/doctoring/editor-design-tokens.md'); const changelog = normalize(repositoryFile('CHANGELOG.md')); + const documentationFitness = repositoryFile('docs/DOCUMENTATION_FITNESS.md'); const index = repositoryFile('docs/README.md'); const adrIndex = repositoryFile('docs/adr/README.md'); const adr = repositoryFile('docs/adr/0031-editor-design-tokens-storybook.md'); expect(index).toContain('[`design-tokens.md`](design-tokens.md)'); - expect(adrIndex).toContain('[0031](0031-editor-design-tokens-storybook.md) | Proposed'); - expect(repositoryFile('docs/UML.md')).toContain('Active PR / Proposed'); + expect(adrIndex).toContain('[0031](0031-editor-design-tokens-storybook.md) | Accepted'); + expect(repositoryFile('docs/UML.md')).toContain('shipped editor chrome'); expect(repositoryFile('docs/UML.md')).toContain('--cwl-*'); expect(existsSync(repositoryPath('docs/adr/0027-editor-design-tokens-storybook.md'))).toBe( false, @@ -32,9 +33,10 @@ describe('editor design-token documentation contract', () => { expect(repositoryFile('docs/TRACEABILITY.md')).not.toContain('ADR 0027'); expect(doctoring).toContain('ADR 0031'); expect(doctoring).not.toContain('ADR 0027'); + expect(operatorGuide).toContain('Status: Shipped protected-main truth (2026-08-25)'); + expect(operatorGuide).not.toContain('Status: Active PR / Proposed'); expect(operatorGuide).toContain('When a host overrides any color token'); - expect(operatorGuide).toContain('Active-PR repaired dark active-toolbar pair uses'); - expect(operatorGuide).not.toContain('shipped dark active-toolbar pair now uses'); + expect(operatorGuide).toContain('shipped dark active-toolbar pair uses'); expect(operatorGuide).toContain('getEditorThemeTokenContrast'); expect(operatorGuide).toContain("getEditorThemeTokenContrast('cwl-accent', 'cwl-accent-soft', 'dark')"); expect(operatorGuide).toContain('`getEditorThemeTokenContrast()` checks only Inkspan catalog values'); @@ -44,21 +46,25 @@ describe('editor design-token documentation contract', () => { expect(operatorGuide).toContain('meetsTextContrast'); expect(operatorGuide).toContain('contrastRatioFromHex'); expect(operatorGuide).toContain('Do not edit Inkspan internals'); + expect(operatorGuide).toContain('protected-main shipped evidence, not a host WCAG certification'); + expect(operatorGuide).not.toContain('active-PR product evidence'); expect(readme).toContain('catalog baseline'); expect(readme).toContain('contrastRatioFromHex'); expect(inventory).toContain('Editor Chrome / Toolbar Button States'); expect(inventory).toContain('Editor Chrome / Live Toolbar'); expect(inventory).toContain('shipped Toolbar'); expect(inventory).toContain(':focus-visible'); - expect(inventory).toContain('Active-PR repaired defaults require no host override'); - expect(inventory).not.toContain('Shipped defaults require no host override'); + expect(inventory).toContain('Shipped defaults require no host override'); expect(inventory).toContain('If re-theming, override `--cwl-accent`'); + expect(doctoring).toContain('**Status:** Shipped protected-main truth (2026-08-25)'); + expect(doctoring).not.toMatch(/\bactive(?:-| )PR\b|If integrated:|until integration|before integration/iu); + expect(doctoring).not.toContain('proposed inventory surface'); + expect(doctoring).not.toContain('protected-main failing default'); + expect(doctoring).not.toContain('protected main remains at the failing pre-repair pair'); expect(doctoring).toContain('cwl-accent-soft'); expect(doctoring).toContain('meets the WCAG 2.2 4.5:1'); - expect(doctoring).toContain('This Active PR now carries dark `--cwl-accent: #58a6ff`'); - expect(doctoring).not.toContain('Inkspan now ships dark `--cwl-accent: #58a6ff`'); - expect(doctoring).toContain('host-facing catalog of the nine inventoried chrome tokens'); - expect(doctoring).not.toContain('host-facing catalog of the nine shipped chrome tokens'); + expect(doctoring).toContain('Inkspan now ships dark `--cwl-accent: #58a6ff`'); + expect(doctoring).toContain('host-facing catalog of the nine shipped chrome tokens'); expect(doctoring).toContain('producing about 5.06:1'); expect(doctoring).toContain('Host overrides must still re-check their own resulting pairs'); expect(doctoring).toContain('contrastRatioFromHex'); @@ -71,11 +77,23 @@ describe('editor design-token documentation contract', () => { ); expect(doctoring).toContain('https://www.w3.org/TR/WCAG22/'); expect(doctoring).toContain('https://storybook.js.org/docs/get-started/frameworks/react-vite'); - expect(adr).toContain('Status: Proposed'); - expect(adr).toContain('If integrated, Inkspan will publish a host-facing theme-token catalog'); - expect(adr).not.toContain('Inkspan publishes a host-facing theme-token catalog'); + expect(adr).toContain('Status: Accepted'); + expect(adr).toContain('Inkspan publishes a host-facing theme-token catalog'); + expect(adr).not.toContain('If integrated, Inkspan will publish a host-facing theme-token catalog'); + expect(adr).not.toMatch( + /\bactive-PR candidate\b|The catalog is additive if integrated\.|The proposed catalog|This ADR stays Proposed until protected integration\.|Before integration, rollback removes/iu, + ); + expect(documentationFitness).toContain( + '| Editor chrome design tokens / Storybook inventory | ADR 0031, `docs/design-tokens.md`, doctoring, token catalog, and Storybook stories | `present_current` | `implemented_on_protected_main` |', + ); + expect(documentationFitness).not.toContain( + 'that lane is Active PR / Proposed and must not be described as shipped until protected integration', + ); expect(changelog).toContain('Named the repeating editor chrome as a host-facing theme-token catalog'); - expect(changelog).toContain('Prepared the active-PR dark active-toolbar accent'); + // The accent change is protected-integrated truth; the active-PR phrasing + // must be gone once the release section ships it. + expect(changelog).toContain('Changed the dark active-toolbar accent'); + expect(changelog).not.toContain('Prepared the active-PR dark active-toolbar accent'); expect(changelog).not.toContain('Raised the shipped dark active-toolbar accent'); expect(repositoryFile('.storybook/main.ts')).toContain("@storybook/react-vite"); expect(repositoryFile('stories/EditorChrome.stories.tsx')).toContain('cwl-tb-btn'); diff --git a/src/unifiedReleaseVersion.test.ts b/src/unifiedReleaseVersion.test.ts index 2de0d71e..4a5d2508 100644 --- a/src/unifiedReleaseVersion.test.ts +++ b/src/unifiedReleaseVersion.test.ts @@ -14,6 +14,57 @@ const changelog = repositoryFile('CHANGELOG.md'); const officeVersion = officeManifest.match(/^version = "([^"]+)"$/mu)?.[1]; +const findMarkdownHeadingIndex = (markdown: string, heading: string): number => { + let offset = 0; + let openFence: string | null = null; + + for (const sourceLine of markdown.split('\n')) { + const line = sourceLine.endsWith('\r') ? sourceLine.slice(0, -1) : sourceLine; + const fenceStart = line.match(/^[ \t]{0,3}(`{3,}|~{3,})/u); + + if (openFence === null && fenceStart !== null) { + openFence = fenceStart[1]; + } else if (openFence !== null) { + const fenceEnd = line.match(/^[ \t]{0,3}(`{3,}|~{3,})[ \t]*$/u); + if ( + fenceEnd !== null && + fenceEnd[1][0] === openFence[0] && + fenceEnd[1].length >= openFence.length + ) { + openFence = null; + } + } else if (line.trimEnd() === heading) { + return offset; + } + + offset += sourceLine.length + 1; + } + + return -1; +}; + +const releaseSection = (headingPattern: RegExp): string => { + const headingMatch = headingPattern.exec(changelog); + expect(headingMatch).not.toBeNull(); + const headingIndex = headingMatch?.index ?? -1; + const afterHeadingIndex = headingIndex + (headingMatch?.[0].length ?? 0); + const nextReleaseMatch = /^## \[/mu.exec(changelog.slice(afterHeadingIndex)); + const endIndex = + nextReleaseMatch?.index === undefined + ? changelog.length + : afterHeadingIndex + nextReleaseMatch.index; + return changelog.slice(headingIndex, endIndex); +}; + +const subsection = (release: string, heading: string): string => { + const marker = `### ${heading}`; + const start = release.indexOf(marker); + expect(start).toBeGreaterThanOrEqual(0); + const tail = release.slice(start + marker.length); + const nextHeading = tail.search(/^### /mu); + return nextHeading < 0 ? tail : tail.slice(0, nextHeading); +}; + describe('unified stable Inkspan release version', () => { it('keeps npm and Office artifacts on one product version', () => { expect(officeVersion).toBeDefined(); @@ -21,15 +72,51 @@ describe('unified stable Inkspan release version', () => { expect(officeVersion).toBe(rootManifest.version); }); + it('recognizes only a real Unreleased heading outside fenced code', () => { + expect( + findMarkdownHeadingIndex( + 'ordinary text mentioning ## [Unreleased]\n## [0.6.0] — 2026-08-25\n', + '## [Unreleased]', + ), + ).toBe(-1); + expect( + findMarkdownHeadingIndex( + '```md\n## [Unreleased]\n```\n## [0.6.0] — 2026-08-25\n', + '## [Unreleased]', + ), + ).toBe(-1); + expect( + findMarkdownHeadingIndex( + 'Preface\n## [Unreleased] \n## [0.6.0] — 2026-08-25\n', + '## [Unreleased]', + ), + ).toBe('Preface\n'.length); + }); + it('binds the current product version to a dated changelog release', () => { const escapedVersion = rootManifest.version.split('.').join('\\.'); const releaseHeading = new RegExp( `^## \\[${escapedVersion}\\] — 20[0-9]{2}-[0-9]{2}-[0-9]{2}$`, 'mu', ); - expect(changelog).toMatch(releaseHeading); - expect(changelog.indexOf('## [Unreleased]')).toBeLessThan( - changelog.search(releaseHeading), + const unreleasedIndex = findMarkdownHeadingIndex(changelog, '## [Unreleased]'); + const currentReleaseIndex = changelog.search(releaseHeading); + expect(unreleasedIndex).toBeGreaterThanOrEqual(0); + expect(currentReleaseIndex).toBeGreaterThanOrEqual(0); + expect(unreleasedIndex).toBeLessThan(currentReleaseIndex); + }); + + it('preserves the current release category boundary for the SSR native form-field repair', () => { + const escapedVersion = rootManifest.version.split('.').join('\\.'); + const releaseHeading = new RegExp( + `^## \\[${escapedVersion}\\] — 20[0-9]{2}-[0-9]{2}-[0-9]{2}$`, + 'mu', ); + const currentRelease = releaseSection(releaseHeading); + const ssrFormFieldEntry = + 'Added the selected standalone Markdown or HTML value to an explicitly configured SSR native form field'; + + expect(subsection(currentRelease, 'Fixed')).toContain(ssrFormFieldEntry); + expect(subsection(currentRelease, 'Security')).not.toContain(ssrFormFieldEntry); }); });