From b26a9f5bed340c97976348e55f5a369ccdbfee03 Mon Sep 17 00:00:00 2001
From: alex-mextner <111590326+alex-mextner@users.noreply.github.com>
Date: Fri, 6 Nov 2026 21:04:35 +0100
Subject: [PATCH] fix(reports): rewrite the HyperIDE report as a spec-traced
product report for 6 Aug to 6 Nov 2026 (Refs #6)
Replaces the October release summary at reports/hyper-2026-10/ (URL kept) with a
product and architecture report for 6 August to 6 November 2026.
- The first screen names the corrected behavior: refused and rolled-back style edits,
inline-style fallback for a missing CSS rule, second Fill pick, real inspector size
and source navigation, preview and Explorer fixes.
- Results are completed outcomes only: merged Master Style Editor fixes, task-only
inspector, preview and Explorer fixes, and Canvas contract and Style Editor
stale-write scopes labelled "implemented and checked on the cited branch, not merged"
with an exact boundary next to the proof.
- Each result gives what was broken, what changed, a spec quote with file, exact
revision, section and line range (Russian originals with a labelled English
translation), the linked tasks (key, title, state, UUID), pull requests and owned
commits, and the recorded proof.
- Spec coverage and boundaries (spec revisions, Retro, state master spec, older specs,
open task-only work) sit in one collapsed appendix and are not presented as results.
- Only real before and after pairs are shown (4 hash-recorded PNGs).
- New source record reports/hyper-2026-10/evidence.json and versioned stylesheet
reports/shared/product-report-20261106.css; earlier stylesheets unchanged.
- StateLib and Conloca: product-focused corrections only (#833 stays credited by its
original commit with first-commit proof recorded; count, dependency and CI-gate
content removed from the Conloca lead, tiles and typed-contracts result).
- New contract test scripts/ci/check-product-report.py, shared HTML helpers, updated
own-results test (including an opener/squash-author regression) and rendered
contract, wired into CI.
---
.github/workflows/ci.yml | 3 +
index.html | 6 +-
reports/2026-10-own-results-evidence.json | 303 +-
reports/conloca-2026-10/index.html | 2 +-
reports/hyper-2026-10/evidence.json | 3029 +++++++++++++++++
.../images/explorer-no-folder-after.png | Bin 0 -> 409560 bytes
.../images/explorer-no-folder-before.png | Bin 0 -> 398390 bytes
.../images/style-write-after.png | Bin 0 -> 260507 bytes
.../images/style-write-before.png | Bin 0 -> 259980 bytes
reports/hyper-2026-10/index.html | 17 +-
reports/shared/product-report-20261106.css | 266 ++
reports/state-lib-2026-10/index.html | 2 +-
scripts/ci/_report_html.py | 52 +
scripts/ci/check-october-render.mjs | 19 +-
scripts/ci/check-own-results.py | 96 +-
scripts/ci/check-product-report.py | 679 ++++
16 files changed, 4141 insertions(+), 333 deletions(-)
create mode 100644 reports/hyper-2026-10/evidence.json
create mode 100644 reports/hyper-2026-10/images/explorer-no-folder-after.png
create mode 100644 reports/hyper-2026-10/images/explorer-no-folder-before.png
create mode 100644 reports/hyper-2026-10/images/style-write-after.png
create mode 100644 reports/hyper-2026-10/images/style-write-before.png
create mode 100644 reports/shared/product-report-20261106.css
create mode 100644 scripts/ci/_report_html.py
create mode 100644 scripts/ci/check-product-report.py
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4a964c4..f83d461 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -22,6 +22,9 @@ jobs:
- name: Verify report attribution and evidence
run: python3 scripts/ci/check-own-results.py
+ - name: Verify the HyperIDE product report against its evidence record
+ run: python3 scripts/ci/check-product-report.py
+
- name: Install dependencies
run: npm ci
diff --git a/index.html b/index.html
index 005c0b6..3f0a20c 100644
--- a/index.html
+++ b/index.html
@@ -62,10 +62,10 @@
Conloca is a visual CMS for Astro sites: editors change pages in the browser and the content is committed to Git. In October Ultra / Mextner delivered fixes and checks across the editor, invites and sign-in, content and media handling, the production site and the delivery pipeline, in 92 merged pull requests, one of them credited only for Ultra’s own commits.
The documentation theme picker is implemented and checked but awaits team review, so it is shown separately below.
397.6 → 0 px
How it works image movement during delayed hydration, in the browser test of #342
33.6 MB
snapshot that now imports in bounded parts of at most 16 MiB (#259)
160 files
changed in the move to validators generated from the TypeScript types (#251)
What we delivered
Editor and page actions
Dragging blocks into a page, Undo after adding a block and the Dashboard New Page button now work. A disabled Preview button explains why it is unavailable. Never-published pages read Draft in the Pages list and on the Dashboard. A route or schema error shows a CMS error page or notice instead of the developer screen, and the branch-name field formats as you type. These were everyday actions that did nothing or showed the wrong state.
Technical detail
Puck ignores a press on a real <button>, so the block-list item is a div role="button" again, and an empty page says how to add a block (#299). Undo and Redo remove and restore a block added by drag or double-click (#306).
The Dashboard New Page button opens the create-page dialog in place (#302). One pageStatusOf() and a LocaleVersion.unpublished marker decide page status, and saving a new translation keeps a never-published page a draft (#296, #307).
A route that fails to render shows RouteErrorPage, and a site schema that is not a generated host schema shows a notice instead of crashing the CMS (#301, #293, #295). The disabled Preview button shows its explanation when clicked (#262).
The New branch field lowercases, hyphenates and keeps the caret in place as you type (#297, #316). The editor opens pages with no English variant (#196), and draft previews parse records with each collection’s schema so dates and defaults match the build (#248).
Frontmatter keys such as __proto__.polluted cannot mutate Object.prototype, while own-data keys, including legitimate constructor and prototype names, are kept (#239). The sign-in divider is readable in dark mode; login-theme.spec.ts checks light and dark at 390 and 1280 px (#220).
Accepting an invite lands in the invited workspace, from both the in-app button and the emailed link. Each editor tab sends its repository, branch and site with every write, and the server rejects a mismatch with a 409 instead of writing to the wrong place. Tabs of one browser origin that refresh at the same moment share one token rotation instead of revoking the whole token family.
Technical detail
Mutations carry X-Conloca-Editor-Repo, -Branch and -Site headers. The worker answers 409 workspace-mismatch before any billing, storage or Git work, and the tab re-enters its own branch and retries once (#192, #224).
Accept waits at most 5 seconds for the membership flush and the Org Durable Object’s alarm delivers the rest; an invite screen that would stay on “accepting” is released after 20 seconds (#292, #305). A deployed-stage proof, invite-landing.spec.ts, covers an invitee who already has a workspace (#294).
A second use of a refresh cookie within 10 seconds gets the live token from the rotation chain, and the browser refreshes under a Web Lock so same-origin tabs send one request per rotation (#240). An OAuth failure shows its failure screen instead of looping (#225).
Splitting the sign-in and Members screens into props-only views fixed a double submit that requested two sign-in links and a double click that sent two invitations (#253, #223).
Cloudflare Access configuration fails closed with 401 when only one of its two settings is present (#191). Overlay rows for pages deleted in Git are pruned (#188). Workspace and flag types are narrowed instead of cast (#222).
Scoped content commits, safe asset moves and correct write-back
CMS commits include only content files, so other people’s staged work is left out, and an asset move claims its destination instead of overwriting an existing file. Draft images render in the canvas before Publish, and a docs-root page is written back to the file it was read from. The hosted preview shows pages committed after its build, and a site snapshot over Cloudflare’s 32 MiB call limit imports in bounded parts.
Technical detail
CMS commits build their tree in a private index seeded from HEAD and move HEAD by compare-and-swap with update-ref, so a concurrent commit is not reverted. Asset moves run one at a time and claim the destination with a hard link. Tests use real temporary repositories: foreign staged files, two concurrent instances, a held index.lock and concurrent moves racing for one name (#197).
Draft images are served from /api/v1/assets/draft/<folder>/<file> while stored data keeps the canonical /assets/... reference (#243). A docs-root MDX page used to be written to a shadow file; mdx-pages-root.test.ts covers edit, delete, create-after-delete and a collision (#235).
The hosted preview records its own buildCommit and sends it as ?build=; the worker diffs that commit against the branch head and matches entries by blob id. A deployed-stage test commits a title without redeploying and expects the new heading (#247, spec change #228).
The depth-1 snapshot of one pull request packs to about 33.6 MB, over the 32 MiB RPC limit. It now lands in parts of at most 16 MiB with the tree and commit last, and a live check against github.com imported the full closure of 4,836 objects (#259). A missing base tree is fetched by object id instead of failing the import (#256).
Org import now always retries the tracker reconcile when a ref already matches upstream, so a stale preview session recovers (#177). Only three of Ultra’s 29 commits in #177 are credited: the repair, its at-head regression test and the browser publication proof. The vocabulary sample stays off the Git lookup behind the unpublished marker (#308).
The production How it works page uses the completed design: a four-step flow with MDX, JSON and editor visuals, twelve responsive images and an install banner that survives 200% text. The design is by Saba; our work is the integration into the production site and its checks. In the pull request’s delayed-hydration test, images moved 397.6 px before the fix and 0 px after it.
Technical detail
The twelve PNGs were imported byte for byte, with SHA-256 and byte lengths checked on the preview. Existing routes, app links, docs links and install tabs are kept, and text is darkened where the original failed contrast (#342).
The test measured 397.587 px of movement before and 0 px across 53 frames after. Twenty-one website targets and 50 browser checks were rerun.
The merge to the main branch ran Validate, deployed-stage browser tests and Deploy Production, all successful. Locally, the split-port development stack redirects backend landings to the app and keeps published media on local origins (#231).
The CMS and the app each build a static Storybook catalogue in CI, and state stories cover save, sync and workspace status, the page list and the conflict dialogs. Deployed-stage browser tests walk every CMS route, create a branch through the Branch menu, confirm a new page reads Draft and check that typed text survives reopening the editor in both themes. CI keeps the proof folder of every deployed-stage run.
Technical detail
State stories render the real components with fixtures that stop at HostClient and the content API fetch, so nothing reaches the network. Counts: SaveSlot 6 plus phone, SyncStatusSlot 8 plus phone, WorkspaceStatusSlot 8 plus phone, CmsHostPlaceholder 9 (#244). CI saves a screenshot of every story at 1280 × 800 and 390 × 844. The app Storybook runner now compiles Tailwind, because classes such as max-sm:hidden were missing from all app examples.
A flaky failure in the Several Locales scenario, caused by a viewport resize landing mid-play, is gone: resizes during play went from 30 and 11 to 0 and 0 in the probe runs (#257).
The deployed-stage job keeps its proof folder on every run and the Playwright report on failed runs (#320). A manual staging proof workflow adds two new legs (#261), and the emitted billing deploy bundle runs in CI (#238).
Real-engine and typed tests for Git paths, billing identity mapping, workspace recovery and memory behaviour were added or made deterministic (#198, #234, #221, #236, #201, #254). Branch creation and the Draft check are in #229 and #309, and the editor-reopen check is in #226.
A pull-request stage environment that keeps working
Pull-request stages are cleaned up on a schedule with capacity reporting and stage leases, which brought the Cloudflare namespace count to 386 of 500 after the first eviction run. Each stage uses one Durable Object namespace fewer, and each deployment attempt gets its own preview fixture without resetting drafts. Before this, Deploy Stage was failing account-wide with Cloudflare error 10067 after about 170 namespaces leaked from closed pull requests.
Technical detail
A scheduled workflow runs every six hours, reports capacity and fails above 400 of 500 namespaces. Drafts hold a stage for 24 hours and ready pull requests for 72 hours; keep-stage and evict-stage override that. tooling/pr-stage-gc.test.ts checks that only closed same-repo pull requests are selected (#200).
Deleting the unused EventLogDO takes each stage from 10 namespaces to 9, which the pull request estimates moves live capacity from about 48 to about 53 stages (#241).
Readiness rejects dirty overlays and divergent session heads, and older attempt orgs are retired after 24 hours (#169). A read-only inventory of legacy preview resources deletes nothing (#155). A Deploy Production failure on a missing generated contracts module was fixed (#258), and E2E runs leave branches clean (#193).
Project validation moved from hand-written Zod schemas to validators generated from the TypeScript types with Typia, in a 160-file change; we adapted the existing generated-contract work to the editor, loader, CLI and sites. CI gates now block only the debt a change adds, and dependency review, mirror sync and the devenv install kept running when GitHub-hosted jobs could not start. DOMPurify moved to 3.4.17 for a critical advisory.
Technical detail
Site schemas become HostSchema<T>: a generated validator with JSON Schema metadata. The editor builds its forms from that metadata, and the loader reports field paths. content-api drops zod and conformance rule A76 forbids it in every workspace package (#251).
Before #263 a rebased pull request got 141 findings, 116 of them on lines it never touched; the gate now looks at changed lines. A per-file type-assertion ratchet refuses to count when a library has no dist, because counts differ without built declarations (2,311 versus 2,325) (#263, #303, #322, #319).
GitHub-hosted jobs stopped starting from about 23 October because of a billing block, so dependency review and mirror sync moved to the self-hosted pool. devenv main had moved to a Nix version the cache does not serve, so the install is pinned to v2.4.0. A test now fails on unjustified sudo in workflows (#311, #313, #255, #314).
DOMPurify has a failing-then-passing floor test (#290, #315). Tests use a RAM-backed temp directory and Git without fsync: summed over 141 test tasks, time went from 1,164 s to 1,079 s and 1,097 s, about 6% less (#194). Bun installs stay out of Bun’s global store (#232).
Smaller changes in this group: Rig verification contract and commands (#174, #230), warning-only pre-push anti-slop check (#167), changed-source gates in CI (#166), dependency review green on private (#260), mirror and tooling fixes (#129, #158, #175, #176, #178, #195, #204, #233, #237, #298).
Opened in November and separate from the October merges above. Implemented and checked, not merged.
IMPLEMENTED · NOT MERGED
Documentation theme picker
The documentation site’s native Auto dropdown is replaced by Light, Dark and System controls with a visible selection, keyboard navigation and touch targets. The head of the pull request passed its deployed-stage browser tests, and the preview site is live. The change awaits team review.
The theme control on production and on the PR #489 preview, both captured at 1440 px on 5 November. On a phone the pictures show the header where the control sits; tap one to open the full capture.
Before: native dropdown. Production, 5 November.After: Light, Dark and System choices. PR #489 preview, 5 November.
Proof
Hosted CI runs for the final commit of each pull request, or for the merge commit where noted.
Work by Ultra / Mextner (GitHub alex-mextner, Alex Ultra). Pull requests merged 1 to 31 October 2026 are credited by commit authorship. Status checked 6 November. Source links may require repository access.
Attribution: every commit in these pull requests is by Ultra, except #177. #167, #168 and #169 were opened from the smoothbricker account and merged by alex-mextner. #177 is credited only for three of Ultra’s 29 commits there: the stale preview-session repair, its at-head regression test and the browser publication proof. Pull requests whose commits are by other authors are counted only in aggregate in the source record.
All merged pull requests counted here are listed with their authors and merge commits in the source record.
+
Conloca: October 2026
Editor fixes, workspace safety, content integrity, the production How it works page
Conloca is a visual CMS for Astro sites: editors change pages in the browser and the content is committed to Git. In October Ultra / Mextner delivered fixes across the editor, invites and sign-in, content and media handling, and the production site.
The documentation theme picker is implemented and checked but awaits team review, so it is shown separately below.
397.6 → 0 px
How it works image movement during delayed hydration, in the browser test of #342
33.6 MB
snapshot that now imports in bounded parts of at most 16 MiB (#259)
409
answer to a write from an editor tab whose repository, branch or site differs, given before any billing, storage or Git work (#192, #224)
What we delivered
Editor and page actions
Dragging blocks into a page, Undo after adding a block and the Dashboard New Page button now work. A disabled Preview button explains why it is unavailable. Never-published pages read Draft in the Pages list and on the Dashboard. A route or schema error shows a CMS error page or notice instead of the developer screen, and the branch-name field formats as you type. These were everyday actions that did nothing or showed the wrong state.
Technical detail
Puck ignores a press on a real <button>, so the block-list item is a div role="button" again, and an empty page says how to add a block (#299). Undo and Redo remove and restore a block added by drag or double-click (#306).
The Dashboard New Page button opens the create-page dialog in place (#302). One pageStatusOf() and a LocaleVersion.unpublished marker decide page status, and saving a new translation keeps a never-published page a draft (#296, #307).
A route that fails to render shows RouteErrorPage, and a site schema that is not a generated host schema shows a notice instead of crashing the CMS (#301, #293, #295). The disabled Preview button shows its explanation when clicked (#262).
The New branch field lowercases, hyphenates and keeps the caret in place as you type (#297, #316). The editor opens pages with no English variant (#196), and draft previews parse records with each collection’s schema so dates and defaults match the build (#248).
Frontmatter keys such as __proto__.polluted cannot mutate Object.prototype, while own-data keys, including legitimate constructor and prototype names, are kept (#239). The sign-in divider is readable in dark mode; login-theme.spec.ts checks light and dark at 390 and 1280 px (#220).
Accepting an invite lands in the invited workspace, from both the in-app button and the emailed link. Each editor tab sends its repository, branch and site with every write, and the server rejects a mismatch with a 409 instead of writing to the wrong place. Tabs of one browser origin that refresh at the same moment share one token rotation instead of revoking the whole token family.
Technical detail
Mutations carry X-Conloca-Editor-Repo, -Branch and -Site headers. The worker answers 409 workspace-mismatch before any billing, storage or Git work, and the tab re-enters its own branch and retries once (#192, #224).
Accept waits at most 5 seconds for the membership flush and the Org Durable Object’s alarm delivers the rest; an invite screen that would stay on “accepting” is released after 20 seconds (#292, #305). A deployed-stage proof, invite-landing.spec.ts, covers an invitee who already has a workspace (#294).
A second use of a refresh cookie within 10 seconds gets the live token from the rotation chain, and the browser refreshes under a Web Lock so same-origin tabs send one request per rotation (#240). An OAuth failure shows its failure screen instead of looping (#225).
Splitting the sign-in and Members screens into props-only views fixed a double submit that requested two sign-in links and a double click that sent two invitations (#253, #223).
Cloudflare Access configuration fails closed with 401 when only one of its two settings is present (#191). Overlay rows for pages deleted in Git are pruned (#188). Workspace and flag types are narrowed instead of cast (#222).
Scoped content commits, safe asset moves and correct write-back
CMS commits include only content files, so other people’s staged work is left out, and an asset move claims its destination instead of overwriting an existing file. Draft images render in the canvas before Publish, and a docs-root page is written back to the file it was read from. The hosted preview shows pages committed after its build, and a site snapshot over Cloudflare’s 32 MiB call limit imports in bounded parts.
Technical detail
CMS commits build their tree in a private index seeded from HEAD and move HEAD by compare-and-swap with update-ref, so a concurrent commit is not reverted. Asset moves run one at a time and claim the destination with a hard link. Tests use real temporary repositories: foreign staged files, two concurrent instances, a held index.lock and concurrent moves racing for one name (#197).
Draft images are served from /api/v1/assets/draft/<folder>/<file> while stored data keeps the canonical /assets/... reference (#243). A docs-root MDX page used to be written to a shadow file; mdx-pages-root.test.ts covers edit, delete, create-after-delete and a collision (#235).
The hosted preview records its own buildCommit and sends it as ?build=; the worker diffs that commit against the branch head and matches entries by blob id. A deployed-stage test commits a title without redeploying and expects the new heading (#247, spec change #228).
The depth-1 snapshot of one pull request packs to about 33.6 MB, over the 32 MiB RPC limit. It now lands in parts of at most 16 MiB with the tree and commit last, and a live check against github.com imported the full closure of 4,836 objects (#259). A missing base tree is fetched by object id instead of failing the import (#256).
Org import now always retries the tracker reconcile when a ref already matches upstream, so a stale preview session recovers (#177). Only three of Ultra’s 29 commits in #177 are credited: the repair, its at-head regression test and the browser publication proof. The vocabulary sample stays off the Git lookup behind the unpublished marker (#308).
The production How it works page uses the completed design: a four-step flow with MDX, JSON and editor visuals, twelve responsive images and an install banner that survives 200% text. The design is by Saba; our work is the integration into the production site and its checks. In the pull request’s delayed-hydration test, images moved 397.6 px before the fix and 0 px after it.
Technical detail
The twelve PNGs were imported byte for byte, with SHA-256 and byte lengths checked on the preview. Existing routes, app links, docs links and install tabs are kept, and text is darkened where the original failed contrast (#342).
The test measured 397.587 px of movement before and 0 px across 53 frames after. Twenty-one website targets and 50 browser checks were rerun.
The merge to the main branch ran Validate, deployed-stage browser tests and Deploy Production, all successful. Locally, the split-port development stack redirects backend landings to the app and keeps published media on local origins (#231).
The CMS and the app each build a static Storybook catalogue in CI, and state stories cover save, sync and workspace status, the page list and the conflict dialogs. Deployed-stage browser tests walk every CMS route, create a branch through the Branch menu, confirm a new page reads Draft and check that typed text survives reopening the editor in both themes. CI keeps the proof folder of every deployed-stage run.
Technical detail
State stories render the real components with fixtures that stop at HostClient and the content API fetch, so nothing reaches the network. Counts: SaveSlot 6 plus phone, SyncStatusSlot 8 plus phone, WorkspaceStatusSlot 8 plus phone, CmsHostPlaceholder 9 (#244). CI saves a screenshot of every story at 1280 × 800 and 390 × 844. The app Storybook runner now compiles Tailwind, because classes such as max-sm:hidden were missing from all app examples.
A flaky failure in the Several Locales scenario, caused by a viewport resize landing mid-play, is gone: resizes during play went from 30 and 11 to 0 and 0 in the probe runs (#257).
The deployed-stage job keeps its proof folder on every run and the Playwright report on failed runs (#320). A manual staging proof workflow adds two new legs (#261), and the emitted billing deploy bundle runs in CI (#238).
Real-engine and typed tests for Git paths, billing identity mapping, workspace recovery and memory behaviour were added or made deterministic (#198, #234, #221, #236, #201, #254). Branch creation and the Draft check are in #229 and #309, and the editor-reopen check is in #226.
A pull-request stage environment that keeps working
Pull-request stages are cleaned up on a schedule with capacity reporting and stage leases, which brought the Cloudflare namespace count to 386 of 500 after the first eviction run. Each stage uses one Durable Object namespace fewer, and each deployment attempt gets its own preview fixture without resetting drafts. Before this, Deploy Stage was failing account-wide with Cloudflare error 10067 after about 170 namespaces leaked from closed pull requests.
Technical detail
A scheduled workflow runs every six hours, reports capacity and fails above 400 of 500 namespaces. Drafts hold a stage for 24 hours and ready pull requests for 72 hours; keep-stage and evict-stage override that. tooling/pr-stage-gc.test.ts checks that only closed same-repo pull requests are selected (#200).
Deleting the unused EventLogDO takes each stage from 10 namespaces to 9, which the pull request estimates moves live capacity from about 48 to about 53 stages (#241).
Readiness rejects dirty overlays and divergent session heads, and older attempt orgs are retired after 24 hours (#169). A read-only inventory of legacy preview resources deletes nothing (#155). A Deploy Production failure on a missing generated contracts module was fixed (#258), and E2E runs leave branches clean (#193).
Project validation moved from hand-written Zod schemas to validators generated from the TypeScript types with Typia. The editor builds its forms from the generated schema metadata, and the loader reports field paths. We adapted the existing generated-contract work to the editor, loader, CLI and sites.
Technical detail
Site schemas become HostSchema<T>: a generated validator with JSON Schema metadata. The editor builds its forms from that metadata, and the loader reports field paths. content-api drops zod and conformance rule A76 forbids it in every workspace package (#251).
Key pull request: #251 · 28 pull requests in this group, most of them build, test and CI changes, all in the source record
Before and after in numbers
Each row compares the state before the change with the state after it. Figures are taken from the pull request named in the last column.
Measure
Before
After
Source
Image movement on How it works during delayed hydration (browser test)
Opened in November and separate from the October merges above. Implemented and checked, not merged.
IMPLEMENTED · NOT MERGED
Documentation theme picker
The documentation site’s native Auto dropdown is replaced by Light, Dark and System controls with a visible selection, keyboard navigation and touch targets. The head of the pull request passed its deployed-stage browser tests, and the preview site is live. The change awaits team review.
The theme control on production and on the PR #489 preview, both captured at 1440 px on 5 November. On a phone the pictures show the header where the control sits; tap one to open the full capture.
Before: native dropdown. Production, 5 November.After: Light, Dark and System choices. PR #489 preview, 5 November.
Proof
Hosted CI runs for the final commit of each pull request, or for the merge commit where noted.
Work by Ultra / Mextner (GitHub alex-mextner, Alex Ultra). Pull requests merged 1 to 31 October 2026 are credited by commit authorship, and 92 merged pull requests are counted, one of them for named commits only. Status checked 6 November. Source links may require repository access.
Attribution: every commit in these pull requests is by Ultra, except #177. #167, #168 and #169 were opened from the smoothbricker account and merged by alex-mextner. #177 is credited only for three of Ultra’s 29 commits there: the stale preview-session repair, its at-head regression test and the browser publication proof. Pull requests whose commits are by other authors are counted only in aggregate in the source record.
All merged pull requests counted here are listed with their authors and merge commits in the source record.
StateLib, in packages/state-lib of the HyperIDE repository, is a synchronous state Runtime with a Rust compiler for application entries and a documentation site with live examples. In October Ultra / Mextner added async state and immutable records to the Runtime, stopped stale async work from publishing, compiled whole entries with rollback, removed a document copy from cold import, and added live examples that compile in the browser.
The numbers on this page are historical local measurements on benchmark fixtures, each tied to the pull request that recorded it. They are not claims about the whole editor.
4.15 → 0.006 ms
point edit on a 10,000-item benchmark fixture, Node median (#802)
63 → 1 ms
completion handler, 1,200 visible rows, Chromium median (#832)
15.6 → 4.7 MB
Chromium retained heap, 10,000 rows with 1,200 visible (#832)
What we delivered
One tested async toolkit instead of per-framework glue
A framework-neutral async toolkit now sits on the Runtime, with an async resource, matchAsyncState and a conformance suite. Production and testing entry points are separate, and an import-boundary test checks the split. One implementation means an async-state bug is fixed once instead of once per framework.
Technical detail
Package @ultrabricks/state-toolkit-async@0.0.0-pilot.0 under packages/state-lib/toolkit, with a peer on @ultrabricks/state-lib@0.1.0-pilot.1. The . and ./testing exports are checked by import-boundary.test.ts and package-contract.test.ts.
Tests: async-conformance.test.ts, async-ownership.test.ts, async-resource.test.ts, async-thenable-once.test.ts and match-async-state.test.ts.
Validated immutable records on the existing Runtime
createRecord adds validated, deeply frozen records on the Runtime for small plain objects such as editor workspace preferences. Callers cannot change a committed snapshot, and a nested update that re-enters the same record now throws instead of letting a stale draft overwrite it. It reuses the Runtime’s transactions, so there is no second state engine to maintain.
Technical detail
A regression test written first reproduced the re-entrancy bug. The error is STATE_RECORD_REENTRANT, thrown before the inner mutation, and async recipes are rejected.
record.test.ts runs under a strict tsconfig.record.json; the pull request records 75 tests passing across core, regression, follow-up and adapter cases.
createOperationScope gives each lane of async work a context generation. Once a lane is replaced, its context changes or it is disposed, the old write or feedback cannot publish, and an old finish or cancel cannot release a newer lease. It is library code; the pull request does not turn it on in the inspector.
Technical detail
Independent lanes, synchronous commit and notify gates, callback binding checked at invocation, and bounded metadata-only diagnostics.
Tests: operation-scope.test.ts, operation-scope-async.test.ts and a type fixture. A real-Chromium check runs the emitted package pair through nine scenarios, and with the stale-publication guard removed, five of seven checks fail.
Package export fails closed and records provenance
The package exporter assembles in an owner-only staging directory on the destination filesystem and publishes by rename. It refuses to overwrite existing files, directories or dangling symlinks and refuses an output directory inside the source trees. Each export records source and output hashes and tool versions, so a package can be traced to its source.
Technical detail
export-package.mjs keeps the original cause on errors and reports both failures if export and cleanup fail. export-filesystem.test.ts covers it, and #826 added cases. The pull request states the limits: the publish is not atomic no-replace and not crash-durable.
Generated commands and complete entries with rollback
The compiler generates immutable commands for declared edits such as assign, toggle, row assign and move, so on the benchmark fixture those edits no longer copy the whole document. state build compiles complete fixed application entries and publishes each output with rollback, and size budgets reject an oversized entry and keep the previous output. The figures below carry their fixtures and comparators; they are not a general speed or size claim.
Technical detail
Native Rust compiler: packages/state-lib/compiler/src/command.rs and lib.rs. Median improved in all 42 Node measurements; 26 of 42 also pass the stricter max-versus-min cell test (#802).
One ownership-aware publication and history module runs on a single dedicated Runtime. Entry budgets are maxRawBytes, maxGzipBytes and maxBrotliBytes plus a relative-growth limit (#802).
Fingerprint walks went from three to two and path resolution is cached within a build. A lock-cleanup failure after a successful install is only a warning (#832). A hole that let an alias into authored src/node_modules bypass the browser-root check was closed, with nine regression tests; the pull request says this is not a sandbox for third-party code.
An optional immutable-row adapter updates completion on existing rows in the board view and falls back to the full renderer for unsupported changes (#832). On the small fixtures the same pull request records about 32 to 38 KB more retained heap, and an entry of 42,166 gzip bytes against 37,132 for the equally optimized original.
importJsonField certifies a JSON field while importing it, and replaceImported adopts that exact root. A cold load skips one whole-document structuredClone, while the normal replace() still copies and behaves as before.
Technical detail
replaceImported throws TypeError for anything not produced by importJsonField, so the shortcut cannot be used with an uncertified value. Certification, freezing, history reset and the save payload contract are unchanged.
owned-import.test.ts was written red first and has 11 passing tests. The pull request records whole-call structuredClone uses during load going from 3 to 2 and makes no timing claim.
Live examples, a keyed-row patch boundary and correct docs
The documentation site has two live examples, /repl/board-commands and /repl/issue-triage. Visitors edit multi-file source, compile it with the real Rust compiler in WebAssembly, and see generated commands, readable and minified output, sizes, live state, Undo and Redo, and browser benchmarks. A checked keyed-row patch boundary lets an app update real text edits without rebuilding the list, and the generated API docs keep ordinary member names.
Technical detail
Edited code runs in a disposable worker inside an opaque-origin, network-denied frame. Cancellation, latest-request authority, timeout and keeping the last good build after a failed build are tested (#809).
patchKeyedRows locates every target before the first row write and rejects a null or invalid plan without applying rows; the pull request documents that a throwing callback leaves a view to rebuild (#833). 14 planner and boundary tests pass.
apiSymbolName() used to throw on a real member named like __@iterator@999 and abort API-doc generation. It now accepts such a name only when a declaration’s string literal is exactly that name, and the regenerated docs are unchanged at 119 entries (#807).
Diagram captions no longer show quoted attribute text, and docs-site readiness waits for exactly three lazy editors. SVG comparison in the gallery tolerates only narrowly parsed coordinate roundoff (#810).
Historical local measurements, recorded in the pull requests named in the last column; the first column gives the fixture. #802 improves the median in all 42 Node measurements and passes its stricter cell test in 26 of 42. #832 records about 32 to 38 KB more retained heap on small fixtures.
Measure
Before
After
Source
Point edit, 10,000-item benchmark fixture, Node median
Diagram caption in the StateLib documentation site, captured for #810 on 14 October. The same fixture is shown before and after the fix. Tap an image to open it full size.
Before: b"> leaks into the caption.After: only the intended label is rendered.
Original browser captures from 14 October, retained with #810.
Proof
Hosted CI runs for the final commit of each pull request, or for the merge commit where noted.
Work by Ultra / Mextner (GitHub alex-mextner, Alex Ultra). Pull requests merged 1 to 31 October 2026 are credited by commit authorship. Status checked 6 November. Source links may require repository access.
Attribution: every commit in these pull requests is by Ultra. #802, #809 and #833 were opened from the smoothbricker account and merged by alex-mextner.
All merged pull requests counted here are listed with their authors and merge commits in the source record.
StateLib, in packages/state-lib of the HyperIDE repository, is a synchronous state Runtime with a Rust compiler for application entries and a documentation site with live examples. In October Ultra / Mextner added async state and immutable records to the Runtime, stopped stale async work from publishing, compiled whole entries with rollback, removed a document copy from cold import, and added live examples that compile in the browser.
The numbers on this page are historical local measurements on benchmark fixtures, each tied to the pull request that recorded it. They are not claims about the whole editor, which does not import state-lib on main. The Style Editor pilot is described in the HyperIDE report.
4.15 → 0.006 ms
point edit on a 10,000-item benchmark fixture, Node median (#802)
63 → 1 ms
completion handler, 1,200 visible rows, Chromium median (#832)
15.6 → 4.7 MB
Chromium retained heap, 10,000 rows with 1,200 visible (#832)
What we delivered
One tested async toolkit instead of per-framework glue
A framework-neutral async toolkit now sits on the Runtime, with an async resource, matchAsyncState and a conformance suite. Production and testing entry points are separate, and an import-boundary test checks the split. One implementation means an async-state bug is fixed once instead of once per framework.
Technical detail
Package @ultrabricks/state-toolkit-async@0.0.0-pilot.0 under packages/state-lib/toolkit, with a peer on @ultrabricks/state-lib@0.1.0-pilot.1. The . and ./testing exports are checked by import-boundary.test.ts and package-contract.test.ts.
Tests: async-conformance.test.ts, async-ownership.test.ts, async-resource.test.ts, async-thenable-once.test.ts and match-async-state.test.ts.
Validated immutable records on the existing Runtime
createRecord adds validated, deeply frozen records on the Runtime for small plain objects such as editor workspace preferences. Callers cannot change a committed snapshot, and a nested update that re-enters the same record now throws instead of letting a stale draft overwrite it. It reuses the Runtime’s transactions, so there is no second state engine to maintain.
Technical detail
A regression test written first reproduced the re-entrancy bug. The error is STATE_RECORD_REENTRANT, thrown before the inner mutation, and async recipes are rejected.
record.test.ts runs under a strict tsconfig.record.json; the pull request records 75 tests passing across core, regression, follow-up and adapter cases.
createOperationScope gives each lane of async work a context generation. Once a lane is replaced, its context changes or it is disposed, the old write or feedback cannot publish, and an old finish or cancel cannot release a newer lease. It is library code; the pull request does not turn it on in the inspector.
Technical detail
Independent lanes, synchronous commit and notify gates, callback binding checked at invocation, and bounded metadata-only diagnostics.
Tests: operation-scope.test.ts, operation-scope-async.test.ts and a type fixture. A real-Chromium check runs the emitted package pair through nine scenarios, and with the stale-publication guard removed, five of seven checks fail.
Package export fails closed and records provenance
The package exporter assembles in an owner-only staging directory on the destination filesystem and publishes by rename. It refuses to overwrite existing files, directories or dangling symlinks and refuses an output directory inside the source trees. Each export records source and output hashes and tool versions, so a package can be traced to its source.
Technical detail
export-package.mjs keeps the original cause on errors and reports both failures if export and cleanup fail. export-filesystem.test.ts covers it, and #826 added cases. The pull request states the limits: the publish is not atomic no-replace and not crash-durable.
Generated commands and complete entries with rollback
The compiler generates immutable commands for declared edits such as assign, toggle, row assign and move, so on the benchmark fixture those edits no longer copy the whole document. state build compiles complete fixed application entries and publishes each output with rollback, and size budgets reject an oversized entry and keep the previous output. The figures below carry their fixtures and comparators; they are not a general speed or size claim.
Technical detail
Native Rust compiler: packages/state-lib/compiler/src/command.rs and lib.rs. Median improved in all 42 Node measurements; 26 of 42 also pass the stricter max-versus-min cell test (#802).
One ownership-aware publication and history module runs on a single dedicated Runtime. Entry budgets are maxRawBytes, maxGzipBytes and maxBrotliBytes plus a relative-growth limit (#802).
Fingerprint walks went from three to two and path resolution is cached within a build. A lock-cleanup failure after a successful install is only a warning (#832). A hole that let an alias into authored src/node_modules bypass the browser-root check was closed, with nine regression tests; the pull request says this is not a sandbox for third-party code.
An optional immutable-row adapter updates completion on existing rows in the board view and falls back to the full renderer for unsupported changes (#832). On the small fixtures the same pull request records about 32 to 38 KB more retained heap, and an entry of 42,166 gzip bytes against 37,132 for the equally optimized original.
importJsonField certifies a JSON field while importing it, and replaceImported adopts that exact root. A cold load skips one whole-document structuredClone, while the normal replace() still copies and behaves as before.
Technical detail
replaceImported throws TypeError for anything not produced by importJsonField, so the shortcut cannot be used with an uncertified value. Certification, freezing, history reset and the save payload contract are unchanged.
owned-import.test.ts was written red first and has 11 passing tests. The pull request records whole-call structuredClone uses during load going from 3 to 2 and makes no timing claim.
Live examples, a keyed-row patch boundary and correct docs
The documentation site has two live examples, /repl/board-commands and /repl/issue-triage. Visitors edit multi-file source, compile it with the real Rust compiler in WebAssembly, and see generated commands, readable and minified output, sizes, live state, Undo and Redo, and browser benchmarks. A checked keyed-row patch boundary lets an app update real text edits without rebuilding the list, and the generated API docs keep ordinary member names.
Technical detail
Edited code runs in a disposable worker inside an opaque-origin, network-denied frame. Cancellation, latest-request authority, timeout and keeping the last good build after a failed build are tested (#809).
patchKeyedRows locates every target before the first row write and rejects a null or invalid plan without applying rows; the pull request documents that a throwing callback leaves a view to rebuild (#833). 14 planner and boundary tests pass.
apiSymbolName() used to throw on a real member named like __@iterator@999 and abort API-doc generation. It now accepts such a name only when a declaration’s string literal is exactly that name, and the regenerated docs are unchanged at 119 entries (#807).
Diagram captions no longer show quoted attribute text, and docs-site readiness waits for exactly three lazy editors. SVG comparison in the gallery tolerates only narrowly parsed coordinate roundoff (#810).
Historical local measurements, recorded in the pull requests named in the last column; the first column gives the fixture. #802 improves the median in all 42 Node measurements and passes its stricter cell test in 26 of 42. #832 records about 32 to 38 KB more retained heap on small fixtures.
Measure
Before
After
Source
Point edit, 10,000-item benchmark fixture, Node median
Diagram caption in the StateLib documentation site, captured for #810 on 14 October. The same fixture is shown before and after the fix. Tap an image to open it full size.
Before: b"> leaks into the caption.After: only the intended label is rendered.
Original browser captures from 14 October, retained with #810.
Proof
Hosted CI runs for the final commit of each pull request, or for the merge commit where noted.
Work by Ultra / Mextner (GitHub alex-mextner, Alex Ultra). Pull requests merged 1 to 31 October 2026 are credited by commit authorship. Status checked 6 November. Source links may require repository access.
Attribution: every commit in these pull requests is by Ultra. #802, #809 and #833 were opened from the smoothbricker account and merged by alex-mextner.
All merged pull requests counted here are listed with their authors and merge commits in the source record.
', art, re.S)
+ self.assertIsNotNone(block, (o["id"], q["text"][:30]))
+ self.assertEqual(strip(block.group(1)), plain_quote(q["translation"]))
+ for o, q in self._all_quotes():
+ if q["language"] == "en":
+ self.assertNotRegex(q["text"], CYRILLIC)
+ self.assertNotIn("translation", q)
+ # Every translation paragraph on the page belongs to a Russian quote, and each is labelled.
+ translations = re.findall(r'
(.*?)
', self.no_script, re.S)
+ self.assertTrue(all(t.startswith('Translation, not source text') for t in translations))
+ self.assertEqual(len(re.findall(r'
', self.no_script)), len(translations))
+ # The translation never sits inside the quoted source text.
+ for m in re.finditer(r"
(.*?)
", self.no_script, re.S):
+ self.assertNotIn("Translation", m.group(1))
+
+ def test_russian_task_title_is_marked_and_translated(self):
+ ub = [t for o in self.data["outcomes"] for t in o["tasks"] if t["key"] == "UB-113"]
+ self.assertTrue(ub)
+ for t in ub:
+ self.assertRegex(t["title"], CYRILLIC)
+ self.assertIn('', self.page)
+ self.assertIn("Translation, not source text: “state-lib: shared mechanism for drafts, saving and protection against late responses”", self.text)
+
+ # ------------------------------------------------------------ tasks
+ def test_tasks_have_verified_identity_and_links(self):
+ seen = set()
+ for o in self.data["outcomes"]:
+ art = self.articles[o["id"]]
+ for t in o["tasks"]:
+ self.assertRegex(t["key"], r"^(HYP|UB)-\d+$")
+ self.assertRegex(t["uuid"], r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
+ self.assertEqual(t["url"], f"https://tasks.glide.vc/issues/{t['uuid']}")
+ self.assertIn(t["state"], ("todo", "in-progress", "in-review", "done"))
+ self.assertTrue(t["title"].strip())
+ self.assertIn(f'{t["key"]}: {t['uuid']}", art)
+ self.assertIn(f'{t["state"]}', art)
+ seen.add((t["key"], t["uuid"], t["state"], t["title"]))
+ keys = {}
+ for key, uuid, state, title in seen:
+ keys.setdefault(key, set()).add((uuid, state, title))
+ self.assertTrue(all(len(v) == 1 for v in keys.values()), "one task identity per key")
+ self.assertNotIn("HYP-1507", {k for k in keys})
+
+ def test_open_or_unresolved_tasks_are_labelled_as_such(self):
+ tasks = {t["key"]: t for o in self.data["outcomes"] for t in o["tasks"]}
+ self.assertEqual(tasks["HYP-1588"]["state"], "todo")
+ self.assertEqual(tasks["HYP-1536"]["state"], "in-progress")
+ self.assertEqual(tasks["HYP-1574"]["state"], "todo")
+ for key in ("HYP-1232", "HYP-1309", "HYP-1735"):
+ self.assertEqual(tasks[key]["state"], "todo")
+ self.assertIn("no implementing commit exists", self.text)
+ self.assertIn("HYP-1507 used in the pull request bodies no longer resolves in the tracker", self.text)
+
+ # ------------------------------------------------------------ merged versus awaiting approval
+ def _assert_merged_pull_requests_are_in_the_period(self, o):
+ start, end = self.data["period"]["from"], self.data["period"]["to"]
+ for p in o["pullRequests"]:
+ if p["state"] == "merged":
+ self.assertRegex(p["mergeSha"], r"^[0-9a-f]{40}$")
+ self.assertTrue(start <= p["mergedAt"][:10] <= end, (o["id"], p["number"]))
+
+ def _assert_pull_request_state(self, o, expected):
+ prs = o["pullRequests"]
+ if expected == "none":
+ self.assertEqual(prs, [], o["id"])
+ elif expected == "some merged":
+ self.assertTrue(any(p["state"] == "merged" for p in prs), o["id"])
+ else:
+ self.assertTrue(prs, o["id"])
+ self.assertEqual({p["state"] for p in prs}, {expected}, o["id"])
+
+ def _assert_label_phrases(self, o, must, must_not):
+ label = o["statusLabel"].lower()
+ for phrase in must:
+ self.assertIn(phrase, label, o["id"])
+ for phrase in must_not:
+ self.assertNotIn(phrase, label, o["id"])
+
+ def test_status_kinds_are_separated_by_real_pull_request_state(self):
+ # status -> (state every pull request must have, label phrases that must / must not appear)
+ rules = {
+ "implemented-branch": ("open", ("implemented and checked on the cited branch", "not merged"), ("merged to main", "awaiting")),
+ "pilot": ("open", ("open pull request",), ("merged to main",)),
+ "task-only": ("merged", ("task-only", "no spec claim"), ()),
+ "merged-library": ("merged", ("not used by the shipped editor",), ()),
+ "info": ("none", (), ()),
+ "open-gap": ("none", (), ()),
+ "merged": ("some merged", ("merged",), ()),
+ }
+ for o in self.data["outcomes"]:
+ expected, must, must_not = rules[o["status"]]
+ self._assert_merged_pull_requests_are_in_the_period(o)
+ self._assert_pull_request_state(o, expected)
+ self._assert_label_phrases(o, must, must_not)
+ merged = [o for o in self.data["outcomes"] if o["status"] == "merged"]
+ self.assertTrue(all(o["statusLabel"].lower().startswith("merged") for o in merged))
+
+ def test_status_legend_covers_every_status_used(self):
+ self.assertLessEqual({o["status"] for o in self.data["outcomes"]}, set(self.data["statusKinds"]))
+
+ def test_first_screen_tiles_name_corrected_product_behavior_not_counters(self):
+ tiles = self.data["tiles"]
+ self.assertGreaterEqual(len(tiles), 3)
+ for t in tiles:
+ self.assertIn(f'
{html.escape(t["value"])}
{html.escape(t["label"])}
', self.no_script)
+ self.assertNotRegex(t["value"], r"\d|Rev|speedup|awaiting")
+ self.assertRegex(t["label"], r"\(#\d+")
+ self.assertNotIn("0 speedups", self.text)
+
+ def test_canvas_contract_stack_is_labelled_not_merged_everywhere_it_is_named(self):
+ canvas = [o for o in self.data["outcomes"] if o["family"] == "canvas-contract"]
+ self.assertEqual([o["id"] for o in canvas], self.data["families"]["canvas-contract"])
+ self.assertTrue(canvas)
+ for o in canvas:
+ self.assertEqual(o["status"], "implemented-branch")
+ self.assertTrue(o["boundary"])
+ self.assertIn(f"Boundary.", self.articles[o["id"]])
+ for p in o["pullRequests"]:
+ self.assertEqual(p["state"], "open")
+ intro = self.no_script.split('id="canvas-contract"', 1)[1].split("", 1)[0]
+ for needle in ("is not on main", "None is merged", "commit authorship", "reviewable proposal pending the affected Part owner's re-sign"):
+ self.assertIn(needle, html.unescape(intro).replace("'", "'"))
+ c2 = next(o for o in canvas if o["id"] == "c2-verified-move")
+ self.assertEqual([p["reviewDecision"] for p in c2["pullRequests"]], ["CHANGES_REQUESTED"])
+ self.assertIn("Review changes were requested on an earlier head", strip(self.articles["c2-verified-move"]))
+ self.assertIn("conflicts with its base", strip(self.articles["c2-verified-move"]))
+
+ def test_state_library_claims_stay_honest(self):
+ by_id = {o["id"]: o for o in self.data["outcomes"]}
+ self.assertEqual(by_id["s1-operation-authority-in-style-editor"]["status"], "implemented-branch")
+ self.assertEqual(by_id["s2-measured-pilot-no-speedup"]["status"], "pilot")
+ for oid in ("s1-operation-authority-in-style-editor", "s2-measured-pilot-no-speedup"):
+ self.assertTrue(all(p["state"] == "open" for p in by_id[oid]["pullRequests"]))
+ s1, s2, s3 = (strip(self.articles[k]) for k in ("s1-operation-authority-in-style-editor", "s2-measured-pilot-no-speedup", "s3-state-library-merged-not-adopted"))
+ self.assertIn("no latency, memory, code-size or adoption win is claimed, so there is no shipped speedup", s1)
+ self.assertIn("conflicts with main", s1)
+ self.assertIn("equally localized native version reaches 831 ms", s2)
+ self.assertIn("No performance gain is shipped", s2)
+ self.assertIn("in a controlled fixture", s2)
+ self.assertIn("none is shown in this report as the editor", s2)
+ self.assertIn("imports state-lib", s3)
+ self.assertIn("HYPER_STATE_SPIKE=1", s3)
+ self.assertIn("The operation scope has no user-visible effect until #827 merges", s3)
+ for banned in ("faster typing", "improved performance", "performance win", "is faster"):
+ self.assertNotIn(banned, self.text.casefold())
+ self.assertIn("on main no client, server, shared or extension file imports it", self.text)
+ # Authorship of the state spec is not described through the squash author.
+ self.assertNotIn("first added by a colleague", self.text)
+
+ def test_retro_is_described_as_tooling_and_w2_w3_stay_open(self):
+ by_id = {o["id"]: o for o in self.data["outcomes"]}
+ self.assertEqual(by_id["r1-retro-identity"]["status"], "info")
+ self.assertEqual(by_id["r2-canvas-preview-w2-w3"]["status"], "open-gap")
+ r2 = strip(self.articles["r2-canvas-preview-w2-w3"])
+ for needle in ("Not delivered in this window", "still `pathname", "HYP-1574", "W-2", "W-3"):
+ self.assertIn(needle.replace("`", ""), r2)
+ self.assertIn("agent tooling", strip(self.articles["r1-retro-identity"]))
+ self.assertIn("this report does not count that tooling work as product results", strip(self.articles["r1-retro-identity"]))
+
+ def test_master_spec_is_not_declared_complete(self):
+ proof = strip(self.no_script.split('id="proof"', 1)[1])
+ self.assertIn("The Master Styles spec is not declared complete", proof)
+ self.assertIn("Open pull requests are labelled as not merged", proof)
+ self.assertIn("not declared complete", self.text)
+ self.assertIn("Rev 0.3.8", self.text)
+ self.assertEqual(len(self.data["masterSpecNotMerged"]), 2)
+
+ # ------------------------------------------------------------ results versus coverage appendix
+ def test_main_results_hold_only_completed_outcomes(self):
+ main_html, appendix_html = self.no_script.split('id="coverage"', 1)
+ appendix_html = appendix_html.split('id="proof"', 1)[0]
+ main_ids = set(re.findall(r'', 1)[1].split("", 1)[0]
+ self.assertTrue(appendix.lstrip().startswith("
Specification coverage and boundaries
"))
+ self.assertEqual(appendix.count(''), 1)
+ self.assertIn("Show how each spec relates to the work", appendix)
+ self.assertIn('id="other-specs"', appendix)
+ for group in ("master-spec", "retro", "state"):
+ self.assertIn(f'id="coverage-{group}"', appendix)
+ self.assertIn("Open task-only work", appendix)
+ for o in self.data["outcomes"]:
+ if o["family"] == "coverage":
+ for q in o["quotes"]:
+ self.assertIn(q["link"], appendix)
+
+ def test_incomplete_branches_are_never_called_ready(self):
+ for phrase in ("awaiting team approval", "Ready, awaiting approval", "ready for approval"):
+ self.assertNotIn(phrase, self.page)
+ for o in self.data["outcomes"]:
+ if o["status"] == "implemented-branch":
+ self.assertEqual({p["state"] for p in o["pullRequests"]}, {"open"})
+ self.assertIn("Implemented and checked on the cited branch", o["statusLabel"])
+ # Proof that is only an author report is called recorded proof, not a new run.
+ for o in self.data["outcomes"]:
+ for bullet in o["verification"]:
+ if re.search(r"Author[- ]report", bullet):
+ self.fail((o["id"], bullet))
+
+ # ------------------------------------------------------------ task-only labels
+ def test_task_only_results_are_labelled_and_have_no_spec_claim(self):
+ task_only = [o for o in self.data["outcomes"] if o["family"] == "task-only"]
+ self.assertEqual(len(task_only), 3)
+ for o in task_only:
+ self.assertEqual(o["status"], "task-only")
+ self.assertEqual(o["quotes"], [])
+ self.assertIn(o["requirementTask"], [t["key"] for t in o["tasks"]])
+ self.assertIn("no spec claim", o["statusLabel"].lower())
+ self.assertEqual(len(self.data["taskOnly"]), 10)
+ table = self.no_script.split('class="figures report-table taskonly-table"', 1)[1].split("", 1)[0]
+ self.assertEqual(table.count("
") - 1, len(self.data["taskOnly"]))
+ for row in self.data["taskOnly"]:
+ for p in row["pullRequests"]:
+ self.assertEqual(p["state"], "merged")
+ self.assertIn(f"{REPO}/pull/{p['number']}", table)
+ for t in row["tasks"]:
+ self.assertIn(f'href="{t["url"]}">{t["key"]}', table)
+ for item in self.data["openTaskOnly"]:
+ self.assertEqual(item["pullRequest"]["state"], "open")
+ self.assertIn("Task-only, and not counted as delivered", self.text)
+ self.assertIn("The report claims no spec requirement for them", self.text)
+
+ # ------------------------------------------------------------ authorship and scope
+ def _assert_credit(self, o, p):
+ credit = p["credit"]
+ if credit["kind"] == "scoped":
+ self.assertTrue(credit["commits"])
+ for sha in credit["commits"]:
+ self.assertRegex(sha, r"^[0-9a-f]{40}$")
+ self.assertIn(f"{REPO}/commit/{sha}", self.articles[o["id"]])
+ else:
+ self.assertEqual(credit["kind"], "whole")
+ self.assertIsInstance(credit["commits"], int)
+ self.assertGreaterEqual(credit["commits"], 1)
+ if p["openedFromAnotherAccount"]:
+ self.assertEqual(credit["kind"], "scoped", p["number"])
+
+ def test_every_pull_request_has_a_whole_or_scoped_credit(self):
+ for o in self.data["outcomes"]:
+ for p in o["pullRequests"]:
+ self._assert_credit(o, p)
+
+ def test_other_contributors_work_is_not_claimed_anywhere(self):
+ text = self.page + MANIFEST.read_text(encoding="utf-8")
+ numbers = {p["number"] for o in self.data["outcomes"] for p in o["pullRequests"]}
+ for row in self.data["taskOnly"]:
+ numbers |= {p["number"] for p in row["pullRequests"]}
+ self.assertFalse(numbers & EXCLUDED_PRS)
+ for n in EXCLUDED_PRS:
+ self.assertNotIn(f"/pull/{n}\"", text)
+ self.assertNotIn(f"#{n}", self.text)
+ for name in ("25c37539", "smoothbricker", "smoothbricks"):
+ self.assertNotIn(name, text.casefold())
+
+ def test_authorship_statements_and_narrow_credit_are_visible(self):
+ proof = strip(self.no_script.split('id="proof"', 1)[1])
+ self.assertIn("credited by commit authorship and not by who opened a pull request", proof)
+ self.assertIn("only the listed commits are credited", proof)
+ # Narrow credit on the mixed pull requests, as the canvas results state it.
+ c5 = strip(self.articles["c5-edit-component-drag-undo"])
+ self.assertIn("In #794 only commit fda9e4a9 is claimed", c5)
+ self.assertIn("the Undo-guard idea is not claimed", c5)
+ c4 = next(o for o in self.data["outcomes"] if o["id"] == "c4-move-scope-and-selection")
+ p823 = next(p for p in c4["pullRequests"] if p["number"] == 823)
+ self.assertEqual(len(p823["credit"]["commits"]), 3)
+
+ def test_manifest_has_no_census_of_other_peoples_work(self):
+ raw = MANIFEST.read_text(encoding="utf-8")
+ accounts = set(re.findall(r'"(?:author|login|account|openedBy|commitAuthors)":\s*"([^"]+)"', raw))
+ self.assertLessEqual(accounts, {"alex-mextner"})
+ self.assertNotIn("commitAuthors", raw)
+
+ # ------------------------------------------------------------ rejected topics and links
+ def test_no_rejected_links_topics_or_failure_narrative(self):
+ low = self.text.casefold()
+ self.assertNotIn("pages.hyperide.ai", self.page.casefold())
+ for phrase in REJECTED_TOPICS + REJECTED_NARRATIVE:
+ self.assertNotIn(phrase, low, phrase)
+
+ def test_all_links_stay_on_repository_task_tracker_or_the_site(self):
+ external = r"^https://(github\.com/hyperide/hyper-saas/(pull/\d+|commit/[0-9a-f]{40}|blob/[0-9a-f]{40}/[^#\s]+(#L\d+(-L\d+)?)?)|tasks\.glide\.vc/issues/[0-9a-f-]{36})$"
+ for url in self.urls:
+ if url.startswith("#"):
+ continue
+ if url.startswith("https://"):
+ self.assertRegex(url, external, url)
+ continue
+ # Relative links must resolve to a real file, or to a directory that holds an index.html.
+ target = (PAGE.parent / url.split("#", 1)[0]).resolve()
+ self.assertTrue(target.is_file() or (target / "index.html").is_file(), url)
+
+ def test_pull_request_links_are_all_in_the_manifest(self):
+ known = {p["number"] for o in self.data["outcomes"] for p in o["pullRequests"]}
+ known |= {p["number"] for r in self.data["taskOnly"] for p in r["pullRequests"]}
+ known |= {i["pullRequest"]["number"] for i in self.data["openTaskOnly"]}
+ known |= {608, 609} # spec-only documents listed under other specs
+ for url in self.urls:
+ m = re.search(r"/pull/(\d+)$", url)
+ if m:
+ self.assertIn(int(m.group(1)), known, url)
+
+ def test_merge_dates_on_the_page_match_the_manifest(self):
+ months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
+ merged = [(o["id"], p) for o in self.data["outcomes"] for p in o["pullRequests"] if p["state"] == "merged"]
+ for oid, p in merged:
+ y, mo, d = map(int, p["mergedAt"][:10].split("-"))
+ self.assertIn(f"merged {d} {months[mo - 1]} {y} as", strip(self.articles[oid]), (oid, p["number"]))
+
+ # ------------------------------------------------------------ architecture, other specs, tables
+ def test_architecture_before_after_table_is_complete(self):
+ table = self.no_script.split('id="architecture"', 1)[1].split("", 1)[0]
+ self.assertEqual(len(re.findall(r'
', table)), 4)
+ rows = re.findall(r"
(.*?)
", table.split("", 1)[1], re.S)
+ self.assertEqual(len(rows), 6)
+ for row in rows:
+ cells = re.findall(r'
(.*?)
', row, re.S)
+ self.assertEqual([c[0] for c in cells], ["Layer, spec section", "Before", "On main now", "Candidate, not merged"])
+ self.assertTrue(all(strip(c[1]) for c in cells))
+ text = strip(table)
+ for needle in ("§9.2a", "§8.1", "§5.2", "§3.8", "§14.6a", "Part 16"):
+ self.assertIn(needle, text)
+
+ def test_other_spec_table_covers_every_banner_with_exact_links(self):
+ specs = self.data["otherSpecs"]
+ self.assertEqual(len(specs), 10)
+ table = self.no_script.split('class="figures report-table specs-table"', 1)[1].split("", 1)[0]
+ self.assertEqual(table.count("
") - 1, 10)
+ for s in specs:
+ self.assertEqual(s["link"], f"{REPO}/blob/{s['revision']}/{s['file']}#L1")
+ self.assertIn(f'href="{s["link"]}">{s["revision"][:8]}, line 1', table)
+ self.assertIn(plain_quote(s["banner"]), strip(table))
+ self.assertRegex(s["banner"], r"SUPERSEDED|ARCHIVED|Retained for history|superseded by the master styles spec")
+ self.assertEqual(table.count(">Archived<"), 3)
+ self.assertEqual(table.count(">Superseded<"), 7)
+ self.assertIn("No window commit found, so no delivery is claimed", strip(table))
+
+ def test_tables_stack_on_phones_with_labels(self):
+ for cls in ("arch-table", "specs-table", "taskonly-table"):
+ table = self.no_script.split(f'class="figures report-table {cls}"', 1)[1].split("", 1)[0]
+ heads = re.findall(r'