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 @@

Hyperide Reports

  • -
    HyperIDE: October 2026 Result Report
    +
    HyperIDE product report: 6 August to 6 November 2026
    - October 2026 - monthly + 6 August to 6 November 2026 + product Ultra / Mextner
    diff --git a/reports/2026-10-own-results-evidence.json b/reports/2026-10-own-results-evidence.json index b850bd6..a3093d7 100644 --- a/reports/2026-10-own-results-evidence.json +++ b/reports/2026-10-own-results-evidence.json @@ -10,59 +10,6 @@ } ], "reports": { - "hyper-2026-10": [ - { - "repo": "hyperide/hyper-saas", - "pr": 810, - "author": "alex-mextner", - "commitAuthors": { - "alex-mextner": 7 - }, - "attribution": "pr-author", - "state": "MERGED", - "mergedAt": "2026-10-14T18:39:58Z", - "mergeSha": "da611247987cc6b2fbb955857e4cc6f9d1410f8e", - "url": "https://github.com/hyperide/hyper-saas/pull/810", - "changedFiles": 68, - "additions": 16876, - "deletions": 215, - "group": "git-push" - }, - { - "repo": "hyperide/hyper-saas", - "pr": 825, - "author": "alex-mextner", - "commitAuthors": { - "alex-mextner": 1 - }, - "attribution": "pr-author", - "state": "MERGED", - "mergedAt": "2026-10-16T01:22:33Z", - "mergeSha": "b0c3a5123add28644d0da367c0e808c7b7e6dd3d", - "url": "https://github.com/hyperide/hyper-saas/pull/825", - "changedFiles": 3, - "additions": 27, - "deletions": 3, - "group": "extension-release" - }, - { - "repo": "hyperide/hyper-saas", - "pr": 837, - "author": "alex-mextner", - "commitAuthors": { - "alex-mextner": 5 - }, - "attribution": "pr-author", - "state": "MERGED", - "mergedAt": "2026-10-30T13:15:07Z", - "mergeSha": "ecd0b8f1b0bf8921f740e76b20337d1a0bc9faac", - "url": "https://github.com/hyperide/hyper-saas/pull/837", - "changedFiles": 10, - "additions": 252, - "deletions": 126, - "group": "dependencies" - } - ], "state-lib-2026-10": [ { "repo": "hyperide/hyper-saas", @@ -80,7 +27,16 @@ "additions": 21928, "deletions": 0, "group": "compiled-entries", - "mergedBy": "alex-mextner" + "mergedBy": "alex-mextner", + "commitProof": { + "method": "pull request commits API, complete pagination", + "firstCommit": "77066361fb2f98f1913a4ac181a20478b3e492d2", + "firstCommitLogin": "alex-mextner", + "allCommitLogins": [ + "alex-mextner" + ], + "commits": 15 + } }, { "repo": "hyperide/hyper-saas", @@ -132,7 +88,16 @@ "additions": 9124, "deletions": 255, "group": "live-docs", - "mergedBy": "alex-mextner" + "mergedBy": "alex-mextner", + "commitProof": { + "method": "pull request commits API, complete pagination", + "firstCommit": "62bfd3c0a9d016d3bc9e2b68a2716beab017134c", + "firstCommitLogin": "alex-mextner", + "allCommitLogins": [ + "alex-mextner" + ], + "commits": 18 + } }, { "repo": "hyperide/hyper-saas", @@ -201,7 +166,16 @@ "additions": 272, "deletions": 1, "group": "live-docs", - "mergedBy": "alex-mextner" + "mergedBy": "alex-mextner", + "commitProof": { + "method": "pull request commits API, complete pagination", + "firstCommit": "cfb0830bf64a4824fb983e82b069d64cf135433b", + "firstCommitLogin": "alex-mextner", + "allCommitLogins": [ + "alex-mextner" + ], + "commits": 1 + } }, { "repo": "hyperide/hyper-saas", @@ -2050,34 +2024,6 @@ "repo": "conloca/private", "run": 41626469902 }, - { - "name": "CI", - "head_sha": "ff519a9864093f1c7b130425077fb99d4bb7951b", - "status": "completed", - "conclusion": "success", - "updated_at": "2026-10-15T13:10:44Z", - "repo": "hyperide/hyper-saas", - "run": 38738722190, - "url": "https://github.com/hyperide/hyper-saas/actions/runs/38738722190", - "event": "pull_request", - "pr": 825, - "prHead": "ff519a9864093f1c7b130425077fb99d4bb7951b", - "mergeSha": "b0c3a5123add28644d0da367c0e808c7b7e6dd3d" - }, - { - "name": "Publish VS Code Extension", - "head_sha": "b0c3a5123add28644d0da367c0e808c7b7e6dd3d", - "status": "completed", - "conclusion": "success", - "updated_at": "2026-10-16T01:27:05Z", - "repo": "hyperide/hyper-saas", - "run": 38834833575, - "url": "https://github.com/hyperide/hyper-saas/actions/runs/38834833575", - "event": "push", - "pr": 825, - "prHead": "ff519a9864093f1c7b130425077fb99d4bb7951b", - "mergeSha": "b0c3a5123add28644d0da367c0e808c7b7e6dd3d" - }, { "name": "CI", "head_sha": "49f981635b1b59af680fe9f81e1c9c0d6ae4e986", @@ -2323,143 +2269,26 @@ "conloca/private": 183 }, "credited": { - "hyperide/hyper-saas": 11, + "hyperide/hyper-saas": 9, "conloca/private": 92 }, "excludedSummary": { - "total": 91, + "total": 93, "byRepository": { - "hyperide/hyper-saas": 0, + "hyperide/hyper-saas": 2, "conloca/private": 91 }, "byReason": { "all commits by other contributors": 79, "automated mirror-sync pull request": 11, - "mixed authorship, own contribution not counted": 1 + "mixed authorship, own contribution not counted": 1, + "maintenance outside the product report scope": 2 }, "localAuditSha256": "fa336d9ba3fa7cefe55a92529fc8fc3d5d0dc19f2e4b0b2f2f68afc59e5e8f73", "note": "Pull requests that are not ours are counted only in aggregate. The per-pull-request audit is kept locally and is not published." } }, "claims": [ - { - "id": "hyper-extension-release", - "group": "extension-release", - "report": "hyper-2026-10", - "prs": [ - 825 - ], - "coveredPrs": [ - 825 - ], - "evidence": [ - { - "type": "ci-run", - "run": 38834833575, - "url": "https://github.com/hyperide/hyper-saas/actions/runs/38834833575" - }, - { - "type": "ci-run", - "run": 38738722190, - "url": "https://github.com/hyperide/hyper-saas/actions/runs/38738722190" - }, - { - "type": "diff", - "path": "CHANGELOG.md", - "pr": 825, - "url": "https://github.com/hyperide/hyper-saas/pull/825/files" - } - ] - }, - { - "id": "hyper-git-push", - "group": "git-push", - "report": "hyper-2026-10", - "prs": [ - 810 - ], - "coveredPrs": [ - 810 - ], - "evidence": [ - { - "type": "pr-body", - "pr": 810, - "url": "https://github.com/hyperide/hyper-saas/pull/810" - }, - { - "type": "tests", - "path": "server/services/__tests__/git-push-tls.test.ts", - "pr": 810, - "url": "https://github.com/hyperide/hyper-saas/pull/810/files" - }, - { - "type": "ci-run", - "run": 38547360197, - "url": "https://github.com/hyperide/hyper-saas/actions/runs/38547360197" - }, - { - "type": "ci-run", - "run": 38547359971, - "url": "https://github.com/hyperide/hyper-saas/actions/runs/38547359971" - } - ] - }, - { - "id": "hyper-dependencies", - "group": "dependencies", - "report": "hyper-2026-10", - "prs": [ - 837, - 810 - ], - "coveredPrs": [ - 837, - 810 - ], - "evidence": [ - { - "type": "pr-body", - "pr": 837, - "url": "https://github.com/hyperide/hyper-saas/pull/837" - }, - { - "type": "tests", - "path": "vscode-extension/hypercanvas-preview/src/__tests__/HyperMcpServer.test.ts", - "pr": 837, - "url": "https://github.com/hyperide/hyper-saas/pull/837/files" - }, - { - "type": "ci-run", - "run": 38547359971, - "url": "https://github.com/hyperide/hyper-saas/actions/runs/38547359971" - } - ] - }, - { - "id": "hyper-local-checks", - "group": "local-checks", - "report": "hyper-2026-10", - "prs": [ - 810 - ], - "coveredPrs": [ - 810 - ], - "evidence": [ - { - "type": "pr-body", - "pr": 810, - "url": "https://github.com/hyperide/hyper-saas/pull/810" - }, - { - "type": "tests", - "path": "scripts/__tests__/conflict-markers.test.ts", - "pr": 810, - "url": "https://github.com/hyperide/hyper-saas/pull/810/files" - } - ] - }, { "id": "state-async-toolkit", "group": "async-toolkit", @@ -2939,13 +2768,7 @@ "group": "gates", "report": "conloca-2026-10", "prs": [ - 251, - 166, - 263, - 303, - 311, - 255, - 315 + 251 ], "coveredPrs": [ 251, @@ -3007,42 +2830,6 @@ } ], "figures": [ - { - "report": "hyper-2026-10", - "measure": "Extension high-severity npm audit, exit status (not an issue count)", - "before": "7, gate failing", - "after": "0", - "pr": 837, - "source": "#837 body", - "repo": "hyperide/hyper-saas" - }, - { - "report": "hyper-2026-10", - "measure": "Root bun audit at high severity", - "before": "Six advisories: MCP SDK, dompurify, happy-dom, undici, zod, body-parser", - "after": "No vulnerabilities reported", - "pr": 837, - "source": "#837 body", - "repo": "hyperide/hyper-saas" - }, - { - "report": "hyper-2026-10", - "measure": "MCP request body limit", - "before": "SDK limits did not apply to bodies the extension parsed itself", - "after": "4 MiB cap, 413 before any server is built", - "pr": 837, - "source": "#837 changelog entry and tests", - "repo": "hyperide/hyper-saas" - }, - { - "report": "hyper-2026-10", - "measure": "Extension release", - "before": "0.1.74", - "after": "0.1.75, tag pushed 16 Oct 2026 01:23 UTC", - "pr": 825, - "source": "Publish run 38834833575", - "repo": "hyperide/hyper-saas" - }, { "report": "state-lib-2026-10", "measure": "Point edit, 10,000-item benchmark fixture, Node median", @@ -3151,15 +2938,6 @@ "source": "#259 body", "repo": "conloca/private" }, - { - "report": "conloca-2026-10", - "measure": "Anti-slop findings on one rebased pull request", - "before": "141, of which 116 on untouched lines", - "after": "Only changed lines counted", - "pr": 263, - "source": "#263 body", - "repo": "conloca/private" - }, { "report": "conloca-2026-10", "measure": "Resize events during a story play", @@ -3168,15 +2946,6 @@ "pr": 257, "source": "#257 body", "repo": "conloca/private" - }, - { - "report": "conloca-2026-10", - "measure": "Summed test time over 141 tasks", - "before": "1,164 s", - "after": "1,079 s and 1,097 s", - "pr": 194, - "source": "#194 body", - "repo": "conloca/private" } ] } diff --git a/reports/conloca-2026-10/index.html b/reports/conloca-2026-10/index.html index 4e4c984..1e9de7a 100644 --- a/reports/conloca-2026-10/index.html +++ b/reports/conloca-2026-10/index.html @@ -10,7 +10,7 @@
    -

    Conloca: October 2026

    Editor fixes, workspace safety, content integrity, the production How it works page

    Overview

    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).

    Key pull requests: #299 · #306 · #302 · #296 · #301 · #297 · 15 pull requests in this group, all in the source record · CI run for #299 · CI run for #302 · CI run for #296

    Invites, tabs and sign-in stay in the right place

    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).

    Key pull requests: #288 · #289 · #192 · #224 · #240 · #225 · 14 pull requests in this group, all in the source record · CI run for #288 · CI run for #289 · CI run for #192

    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).

    Key pull requests: #197 · #243 · #235 · #247 · #259 · #177 · 9 pull requests in this group, all in the source record · CI run for #197 · CI run for #247 · CI run for #259

    The finished How it works page is on conloca.com

    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).

    Key pull requests: #342 · #231 · CI run for #342 · conloca.com/how-it-works

    Browser and story coverage for the editor

    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.

    Key pull requests: #217 · #244 · #304 · #229 · #300 · #320 · 17 pull requests in this group, all in the source record · CI run for #229 · CI run for #300

    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).

    Key pull requests: #200 · #241 · #169 · #155 · 7 pull requests in this group, all in the source record

    Typed contracts and delivery gates

    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).

    Key pull requests: #251 · #166 · #263 · #303 · #311 · #255 · #315 · 28 pull requests in this group, 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.

    MeasureBeforeAfterSource
    Image movement on How it works during delayed hydration (browser test)397.587 px0 px across 53 frames#342 body
    PR-stage Durable Object namespaces after the first eviction runAbout 170 leaked; Deploy Stage failing with error 10067386 of 500#200 body
    Durable Object namespaces per stage109#241 body
    CMS site snapshot import33.6 MB in one call, over the 32 MiB limitParts of at most 16 MiB#259 body
    Anti-slop findings on one rebased pull request141, of which 116 on untouched linesOnly changed lines counted#263 body
    Resize events during a story play30 and 110 and 0#257 body
    Summed test time over 141 tasks1,164 s1,079 s and 1,097 s#194 body

    Awaiting team review: theme picker

    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.

    #489 · Preview · Deployed E2E

    Before and after

    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.

    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.

    October 2026 · Ultra / Mextner · Updated 6 November 2026 · Source record
    +

    Conloca: October 2026

    Editor fixes, workspace safety, content integrity, the production How it works page

    Overview

    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).

    Key pull requests: #299 · #306 · #302 · #296 · #301 · #297 · 15 pull requests in this group, all in the source record · CI run for #299 · CI run for #302 · CI run for #296

    Invites, tabs and sign-in stay in the right place

    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).

    Key pull requests: #288 · #289 · #192 · #224 · #240 · #225 · 14 pull requests in this group, all in the source record · CI run for #288 · CI run for #289 · CI run for #192

    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).

    Key pull requests: #197 · #243 · #235 · #247 · #259 · #177 · 9 pull requests in this group, all in the source record · CI run for #197 · CI run for #247 · CI run for #259

    The finished How it works page is on conloca.com

    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).

    Key pull requests: #342 · #231 · CI run for #342 · conloca.com/how-it-works

    Browser and story coverage for the editor

    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.

    Key pull requests: #217 · #244 · #304 · #229 · #300 · #320 · 17 pull requests in this group, all in the source record · CI run for #229 · CI run for #300

    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).

    Key pull requests: #200 · #241 · #169 · #155 · 7 pull requests in this group, all in the source record

    Typed project validation

    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.

    MeasureBeforeAfterSource
    Image movement on How it works during delayed hydration (browser test)397.587 px0 px across 53 frames#342 body
    PR-stage Durable Object namespaces after the first eviction runAbout 170 leaked; Deploy Stage failing with error 10067386 of 500#200 body
    Durable Object namespaces per stage109#241 body
    CMS site snapshot import33.6 MB in one call, over the 32 MiB limitParts of at most 16 MiB#259 body
    Resize events during a story play30 and 110 and 0#257 body

    Awaiting team review: theme picker

    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.

    #489 · Preview · Deployed E2E

    Before and after

    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.

    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.

    October 2026 · Ultra / Mextner · Updated 6 November 2026 · Source record
    diff --git a/reports/shared/product-report-20261106.css b/reports/shared/product-report-20261106.css new file mode 100644 index 0000000..96e6b46 --- /dev/null +++ b/reports/shared/product-report-20261106.css @@ -0,0 +1,266 @@ +/* Product report layout, loaded after october-own-20261106.css. The earlier files stay unchanged. */ +.outcomes-index .family-nav { + list-style: none; + margin: 12px 0 0; + padding: 0; + display: flex; + flex-wrap: wrap; + gap: 8px 18px; +} +.outcomes-index .family-nav li { + margin: 0; +} +.report-section > .outcomes, +.report-section > .figure-pair { + margin-top: 18px; +} +.report-section h3 { + margin-top: 22px; +} +.outcome { + border-left: 4px solid var(--border); +} +.outcome h3 { + margin-top: 4px; +} +.kind-merged { + border-left-color: var(--green); +} +.kind-merged-library { + border-left-color: rgba(63, 185, 80, 0.55); +} +.kind-implemented-branch { + border-left-color: var(--yellow); +} +.kind-pilot { + border-left-color: var(--orange); +} +.kind-open-gap { + border-left-color: var(--red); +} +.kind-task-only { + border-left-color: var(--accent2); +} +.kind-info { + border-left-color: var(--text-dim); +} +.outcome .what { + margin: 8px 0; +} +.outcome .what strong { + color: var(--text); +} +figure.requirement { + margin: 14px 0; + padding: 12px 16px; + background: rgba(88, 166, 255, 0.05); + border: 1px solid rgba(88, 166, 255, 0.18); + border-radius: 8px; +} +figure.requirement blockquote { + margin: 0; + padding-left: 12px; + border-left: 3px solid rgba(88, 166, 255, 0.5); + color: var(--text); + font-size: 0.98rem; + line-height: 1.55; + overflow-wrap: anywhere; +} +figure.requirement .translation { + margin: 8px 0 0; + max-width: none; + font-size: 0.95rem; + color: var(--text); + overflow-wrap: anywhere; +} +figure.requirement .tag { + display: inline-block; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; + color: var(--accent2); + border: 1px solid rgba(188, 140, 255, 0.4); + border-radius: 4px; + padding: 1px 6px; + margin-right: 6px; +} +figure.requirement figcaption { + margin-top: 8px; + font-size: 0.82rem; + color: var(--text-dim); + line-height: 1.5; + overflow-wrap: anywhere; +} +figure.requirement .role { + color: var(--text); + font-weight: 600; +} +figure.requirement .note, +.outcome .note { + color: var(--text-dim); + font-size: 0.82rem; + margin: 6px 0 0; +} +.outcome li .note { + margin: 0; +} +.outcome .refs { + margin-top: 10px; +} +.outcome .boundary { + margin: 8px 0 0; + font-size: 0.85rem; + color: var(--text-dim); +} +.outcome .boundary strong { + color: var(--text-dim); + font-weight: 600; +} +details.appendix { + margin-top: 8px; + border: 1px solid var(--border); + border-radius: 10px; + padding: 12px 18px; + background: var(--card); +} +details.appendix > summary { + cursor: pointer; + color: var(--text); + font-weight: 600; +} +details.appendix > p { + margin-top: 12px; +} +.appendix-group { + margin-top: 22px; +} +.appendix-group h3 { + margin-top: 0; +} +.outcome details.evidence { + margin-top: 14px; +} +.outcome details.evidence h4 { + margin: 16px 0 6px; + font-size: 0.82rem; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--text-dim); +} +.outcome details.evidence ul { + margin: 6px 0 0 20px; +} +.outcome .scope { + margin-top: 14px; +} +.outcome .state { + display: inline-block; + font-size: 0.72rem; + font-weight: 600; + border: 1px solid var(--border); + border-radius: 10px; + padding: 0 8px; + color: var(--text-dim); +} +.outcome .state-done { + color: var(--green); + border-color: rgba(63, 185, 80, 0.5); +} +.outcome .state-review { + color: var(--accent1); + border-color: rgba(88, 166, 255, 0.5); +} +.outcome .state-progress { + color: var(--yellow); + border-color: rgba(210, 153, 34, 0.5); +} +.outcome .state-todo { + color: var(--text-dim); +} +ul.plain { + margin: 8px 0 0 20px; +} +ul.plain li { + margin: 8px 0; + line-height: 1.6; + color: var(--text-dim); +} +.figures blockquote { + margin: 0 0 4px; + font-size: 0.85rem; + color: var(--text); + overflow-wrap: anywhere; +} +.report-table td:first-child { + min-width: 9em; +} +ul.plain blockquote { + margin: 6px 0 4px; + padding-left: 12px; + border-left: 3px solid var(--border); + color: var(--text); + overflow-wrap: anywhere; +} +.figure-pair { + background: var(--card); + border: 1px solid var(--border); + border-left: 4px solid var(--border); + border-radius: 10px; + padding: 20px 24px; + margin: 18px 0; +} +.figure-pair.kind-merged { + border-left-color: var(--green); +} +.figure-pair.kind-pilot { + border-left-color: var(--orange); +} +.figure-pair.kind-implemented-branch { + border-left-color: var(--yellow); +} +.figure-pair .refs { + margin-top: 12px; +} +@media (max-width: 650px) { + .figure-pair { + padding: 16px 14px; + } + figure.requirement { + padding: 10px 12px; + } + /* Phone crops magnify one region of a real capture. Each offset is tuned to its PNG: + crop-explorer shows the Explorer panel of the 2400x1536 capture (top-left 30% x 26%); + crop-inspector shows the inspector width field of the 1440x835 capture (right 28%). + Re-tune both if an image is replaced. */ + .crop.crop-explorer img { + left: 0; + top: -11.4%; + width: 333%; + } + .crop.crop-inspector { + aspect-ratio: 5 / 4; + } + .crop.crop-inspector img { + left: -257.1%; + top: -72.5%; + width: 357.1%; + } + .outcome details.evidence ul { + margin-left: 16px; + } + .report-table caption { + display: block; + margin-bottom: 8px; + } + .report-table td:first-child { + min-width: 0; + } + .report-table td::before { + display: block; + font-weight: 600; + } + .report-table td:first-child::before { + display: none; + } +} diff --git a/reports/state-lib-2026-10/index.html b/reports/state-lib-2026-10/index.html index 70080a6..ce5c24d 100644 --- a/reports/state-lib-2026-10/index.html +++ b/reports/state-lib-2026-10/index.html @@ -10,7 +10,7 @@
    -

    StateLib: October 2026

    Async state, immutable records, operation ownership, compiled entries

    Overview

    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.

    Key pull requests: #810

    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.

    Key pull requests: #803

    Stale async work cannot publish

    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.

    Key pull requests: #826

    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.

    Key pull requests: #810

    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.

    Key pull requests: #802 · #832

    Cold import skips a full-document copy

    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.

    Key pull requests: #834

    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).

    Key pull requests: #809 · #833 · #807

    Before and after in numbers

    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.

    MeasureBeforeAfterSource
    Point edit, 10,000-item benchmark fixture, Node median4.148 ms0.006 ms#802 body
    Move, 10,000-item benchmark fixture, Node median4.175 ms0.008 ms#802 body
    Entry size, 512-command fixture: raw / gzip / Brotli71,069 / 9,525 / 6,596 bytes42,284 / 8,619 / 5,779 bytes#802 body
    Task-completion handler, 1,200 visible rows, median63.2 ms Chromium, 93 ms WebKit1.0 ms Chromium, 1 ms WebKit#832 body
    Next animation frame, 1,200 visible rows311 ms Chromium, 546 ms WebKit13.8 ms Chromium, 7 ms WebKit#832 body
    Chromium retained heap, 10,000 rows, 1,200 visible15.58 MB4.73 MB#832 body
    Whole-document copies during cold load32#834 body
    Diagram caption in the docs siteQuoted attribute text shown before the labelOnly the intended labelScreenshots below

    Screenshots

    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.

    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.

    October 2026 · Ultra / Mextner · Updated 6 November 2026 · Source record
    +

    StateLib: October 2026

    Async state, immutable records, operation ownership, compiled entries

    Overview

    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.

    Key pull requests: #810

    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.

    Key pull requests: #803

    Stale async work cannot publish

    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.

    Key pull requests: #826

    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.

    Key pull requests: #810

    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.

    Key pull requests: #802 · #832

    Cold import skips a full-document copy

    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.

    Key pull requests: #834

    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).

    Key pull requests: #809 · #833 · #807

    Before and after in numbers

    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.

    MeasureBeforeAfterSource
    Point edit, 10,000-item benchmark fixture, Node median4.148 ms0.006 ms#802 body
    Move, 10,000-item benchmark fixture, Node median4.175 ms0.008 ms#802 body
    Entry size, 512-command fixture: raw / gzip / Brotli71,069 / 9,525 / 6,596 bytes42,284 / 8,619 / 5,779 bytes#802 body
    Task-completion handler, 1,200 visible rows, median63.2 ms Chromium, 93 ms WebKit1.0 ms Chromium, 1 ms WebKit#832 body
    Next animation frame, 1,200 visible rows311 ms Chromium, 546 ms WebKit13.8 ms Chromium, 7 ms WebKit#832 body
    Chromium retained heap, 10,000 rows, 1,200 visible15.58 MB4.73 MB#832 body
    Whole-document copies during cold load32#834 body
    Diagram caption in the docs siteQuoted attribute text shown before the labelOnly the intended labelScreenshots below

    Screenshots

    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.

    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.

    October 2026 · Ultra / Mextner · Updated 6 November 2026 · Source record
    ", "", cls.page, flags=re.S) + cls.text = VisibleText(cls.page).raw + cls.articles = { + m.group(1): m.group(0) + for m in re.finditer(r'
    ', cls.no_script, re.S) + } + + # ------------------------------------------------------------ period, header, first screen + def test_url_preserved_and_period_is_explicit_in_title_header_date(self): + self.assertTrue(PAGE.is_file()) + self.assertIn(f"HyperIDE product report: {PERIOD}", self.page) + self.assertEqual(re.search(r"

    (.+?)

    ", self.no_script).group(1), f"HyperIDE product report: {PERIOD}") + self.assertIn(f"{PERIOD}", self.no_script) + self.assertIn(f'date: "{PERIOD}"', self.page) + self.assertIn(f'', self.no_script) + self.assertEqual(self.data["period"], {"from": "2026-08-06", "to": "2026-11-06"}) + self.assertNotIn("October", re.search(r"(.*?)", self.page).group(1)) + self.assertNotIn("October", re.search(r"

    (.*?)

    ", self.no_script).group(1)) + + def test_static_header_matches_the_report_init_payload(self): + static = self.page.split('data-section="header"', 1)[1].split("", 1)[0] + payload = self.page.split("Report.init({", 1)[1] + title = re.search(r'title: "(.*?)",', payload).group(1) + subtitle = re.search(r'subtitle: "(.*?)",', payload).group(1) + date = re.search(r'date: "(.*?)",', payload).group(1) + self.assertIn(f"

    {html.escape(title, quote=True)}

    ", static) + self.assertIn(f'

    {html.escape(subtitle, quote=True)}

    ', static) + self.assertIn(f"{date}", static) + links = re.findall(r'\{ label: "(.*?)", href: "(.*?)" \}', payload) + self.assertTrue(links) + for label, href in links: + self.assertIn(f'{html.escape(label, quote=True)}', static) + self.assertEqual(static.count("", 1)[0] + self.assertGreaterEqual(overview.count('class="summary-card"'), 3) + self.assertGreaterEqual(len(re.findall(r"

    ", overview)), 4) + self.assertGreater(len(strip(overview)), 1200) + # The first screen names the corrected product behavior, not the report's own metadata. + text = strip(overview) + for needle in ("refused and rolled back with a warning", "lands as an inline style", "A second Fill-color pick replaces the color", + "real pixel size", "real source file", "Open Folder", "Implemented and checked on their own branches, not merged", + "not declared complete"): + self.assertIn(needle, text) + marks = ['class="report-nav"', "

    ", 'class="byline"', 'id="overview"', 'id="architecture"', 'id="outcomes"', + 'id="style-editor"', 'id="task-only"', 'id="canvas-contract"', 'id="state"', 'id="figures"', 'id="coverage"', + 'id="other-specs"', 'id="proof"'] + positions = [self.no_script.index(m) for m in marks] + self.assertEqual(positions, sorted(positions)) + + def test_nothing_is_hidden_and_details_start_closed(self): + self.assertNotIn("fade-in", self.page) + self.assertNotIn("display:none", self.page.replace(" ", "")) + self.assertNotIn("
    Evidence: ", oid) + + # ------------------------------------------------------------ outcomes agree with the manifest + def test_every_manifest_outcome_has_one_article_with_its_text(self): + ids = [o["id"] for o in self.data["outcomes"]] + self.assertEqual(sorted(ids), sorted(self.articles)) + self.assertEqual(len(ids), len(set(ids))) + for o in self.data["outcomes"]: + art = self.articles[o["id"]] + self.assertIn(f'data-status="{o["status"]}"', art) + self.assertEqual(strip(re.search(r'(.*?)', art).group(1)), o["statusLabel"]) + self.assertEqual(strip(re.search(r"

    (.*?)

    ", art).group(1)), o["title"]) + paragraphs = re.findall(r'

    (Before|Now)\. (.*?)

    ', art, re.S) + self.assertEqual([p[0] for p in paragraphs], ["Before", "Now"], o["id"]) + self.assertEqual(strip(paragraphs[0][1]), " ".join(o["before"].split()), o["id"]) + self.assertEqual(strip(paragraphs[1][1]), " ".join(o["now"].split()), o["id"]) + self.assertGreaterEqual(len(re.findall(r"\.(?:\s|$)", o["before"])), 2, o["id"]) + self.assertGreaterEqual(len(re.findall(r"\.(?:\s|$)", o["now"])), 2, o["id"]) + self.assertIn(o["scope"].replace("`", ""), strip(art), o["id"]) + + def test_each_outcome_shows_its_primary_requirement_outside_the_collapsed_block(self): + for o in self.data["outcomes"]: + art = self.articles[o["id"]] + visible = art.split('
    ', 1)[0] + if o.get("requirementTask"): + self.assertIn('class="requirement task-requirement"', visible, o["id"]) + self.assertIn(f">{o['requirementTask']}", visible, o["id"]) + self.assertIn("No spec paragraph is claimed", visible, o["id"]) + self.assertEqual(o["quotes"], [], o["id"]) + else: + primary = [q for q in o["quotes"] if q["primary"]] + self.assertEqual(len(primary), 1, o["id"]) + self.assertEqual(len(re.findall(r"Verification

    ", art) + self.assertIn("

    Caveats

    ", art) + for p in o["pullRequests"]: + self.assertIn(f"{REPO}/pull/{p['number']}", art, o["id"]) + if p["state"] == "merged": + self.assertIn(f"as {p['mergeSha'][:8]}", art) + else: + self.assertIn("open", strip(art), o["id"]) + self.assertIn("not merged", strip(art), o["id"]) + + # ------------------------------------------------------------ quotes: exact revision, lines, language + def _all_quotes(self): + for o in self.data["outcomes"]: + for q in o["quotes"]: + yield o, q + + def test_quotes_carry_spec_file_revision_section_lines_and_exact_links(self): + n = 0 + for o, q in self._all_quotes(): + n += 1 + self.assertRegex(q["revision"], r"^[0-9a-f]{40}$") + a, b = q["lines"] + self.assertLessEqual(a, b) + frag = f"#L{a}" if a == b else f"#L{a}-L{b}" + self.assertEqual(q["link"], f"{REPO}/blob/{q['revision']}/{q['file']}{frag}") + self.assertTrue(q["section"] and q["text"] and q["role"]) + self.assertTrue(q["file"].startswith(("docs/", "packages/state-lib/docs/")), q["file"]) + self.assertTrue(q["file"].endswith(".md")) + art = self.articles[o["id"]] + self.assertIn(f'href="{q["link"]}">{q["revision"][:8]}, ', art, (o["id"], q["link"])) + self.assertIn(f"{q['file']}", art) + self.assertIn(html.escape(q["section"], quote=True), art, (o["id"], q["section"])) + shown = f"line {a}" if a == b else f"lines {a}-{b}" + self.assertIn(f">{q['revision'][:8]}, {shown}", art, o["id"]) + self.assertIn(f'lang="{q["language"]}">', art) + self.assertIn(plain_quote(q["text"]), strip(art), (o["id"], q["text"][:40])) + self.assertEqual(n, self.data["verification"]["quotes"]["count"]) + + def test_quote_verification_is_recorded_for_every_quote(self): + v = self.data["verification"] + total = sum(len(o["quotes"]) for o in self.data["outcomes"]) + self.assertTrue(v["quotes"]["allFound"]) + self.assertEqual(v["quotes"]["count"], total) + self.assertEqual(v["quotes"]["checkedAt"], "2026-11-06") + self.assertFalse(v["testsRerun"]) + self.assertEqual(self.data["mainRevision"], "5c26b9c68060e520e555774d37141b11d2aa12aa") + self.assertEqual(self.data["masterSpecOnMain"], "Rev 0.3.8") + + def test_russian_quotes_keep_the_original_and_a_labelled_translation(self): + ru = [(o, q) for o, q in self._all_quotes() if q["language"] == "ru"] + self.assertGreaterEqual(len(ru), 6) + for o, q in ru: + self.assertRegex(q["text"], CYRILLIC) + self.assertTrue(q.get("translation")) + self.assertNotRegex(q["translation"], CYRILLIC) + art = self.articles[o["id"]] + block = re.search( + r'
    ' + re.escape(html.escape(q["text"], quote=True)).replace("'", "(?:'|')") + r"
    " + r'

    Translation, not source text (.*?)

    ', 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'(.*?)', table) + for row in re.findall(r"(.*?)", table.split("", 1)[1], re.S): + labels = re.findall(r'', row) + self.assertEqual([html.unescape(h) for h in heads], [html.unescape(l) for l in labels], cls) + + # ------------------------------------------------------------ images + def test_images_are_real_decodable_distinct_pairs_in_the_manifest(self): + recorded = {r["path"]: r for r in self.data["images"]} + tags = re.findall(r"]*>", self.no_script) + self.assertEqual(len(tags), len(recorded)) + self.assertGreaterEqual(len(tags), 4) + for tag in tags: + src = re.search(r'src="([^"]+)"', tag).group(1) + r = recorded[f"reports/{SLUG}/{src}"] + data = (PAGE.parent / src).read_bytes() + self.assertEqual(data[:8], b"\x89PNG\r\n\x1a\n") + self.assertEqual(hashlib.sha256(data).hexdigest(), r["sha256"]) + self.assertEqual(struct.unpack(">II", data[16:24]), (r["width"], r["height"])) + self.assertEqual(len(data), r["bytes"]) + self.assertGreater(len(data), 50000) + self.assertRegex(tag, r'alt="[^"]{40,}"') + self.assertIn(f'width="{r["width"]}"', tag) + self.assertIn(f'height="{r["height"]}"', tag) + self.assertRegex(r["sourceRevision"], r"^[0-9a-f]{40}$") + self.assertEqual(r["kind"], "real-editor-screenshot") + for fixture in ("scripts/style-state-pilot", "state-bench", "field-subscriptions", "feedback-capability", "/evidence/fields", "parent.png"): + self.assertNotIn(fixture, r["sourcePath"]) + self.assertNotIn("hyperide.github.io", r["sourcePath"]) + crops = re.findall(r'\s*.*?(.*?)', figs, re.S) + self.assertEqual([(p[0], p[1], p[2]) for p in pairs], [("merged", "fig-explorer-empty-state", "721"), ("implemented-branch", "fig-inspector-width-write", "827")]) + self.assertIn("task-only", pairs[0][3]) + self.assertIn("not merged", pairs[1][3]) + self.assertIn("Implemented and checked on the cited branch", pairs[1][3]) + text = strip(figs) + self.assertIn("It is not a speed result and does not test stale responses or rollback", text) + self.assertIn("Benchmark fixture captures are not shown", text) + self.assertIn("Only real before and after pairs", text) + + # ------------------------------------------------------------ styles and mobile flow + def test_versioned_styles_leave_the_earlier_files_unchanged(self): + for rel, digest in OLD_CSS_SHA256.items(): + self.assertEqual(hashlib.sha256((ROOT / rel).read_bytes()).hexdigest(), digest, rel) + head = self.page.split("", 1)[0] + order = [head.index(f'href="../shared/{n}"') for n in ("styles.css", "october-own-20261106.css", "product-report-20261106.css")] + self.assertEqual(order, sorted(order)) + self.assertNotIn("