docs(wui): Dewey's design work: brand board, five mockups, slice 1 S5 views and checks
Brand board revisions, the five dashboard mockups with their verify
script and evidence, the slice 1 S5 design note (SLICE1-VIEWS.md, rev 3
plus the S2c section 10 update), the Console mockup of inbox, tasks,
agents and trail (mockups/slice1/) and checks/slice1-verify.mjs (16
checks; check 16 pinned to bus at d27042fa). Also the CHAT-03 progress
log and the 2026-09-26 dirty-tree manifest. Design and evidence only;
no package code. Row 40 (#1522), committed on its own at Sage's
direction, 2026-10-09.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
@@ -0,0 +1,18 @@
|
||||
# CHAT-03 I1 progress (Dewey, row 5)
|
||||
|
||||
Append-only. One line per working stop.
|
||||
|
||||
- 2026-10-04: resumed after Sage's REQUEST. Done and untracked: claim, cohort, shim, engine, events, framing, guard, pi-pin, records, text-policy, turns. Next: src/controller.mjs, then client, terminal, tests/fake-pi.mjs and the fixtures.
|
||||
- 2026-10-04: src/controller.mjs written and import-checked (Interrupt dedup added). tests/fake-pi.mjs written (in-process FakePi + FakeLauncher, process form with control socket). Next: first end-to-end fixture, then client, terminal and the full fixture set.
|
||||
- 2026-10-04: src/client.mjs, tests/harness.mjs, tests/ctrl-child.mjs and tests/claim.test.mjs (W1–W20, G1–G3, 21 tests) green. Controller fixes: written marked on the prompt response before the ack; launcher forceStop hook; kill handle on close; socket dir prepared only after classification (a refused contender no longer unlinks a live socket). Next: N1–N25, then H, K, E/S/P3, terminal, README, suites.
|
||||
- 2026-10-04: tests/turns.test.mjs (N1–N25, 26 tests) green three runs in a row. fake-pi gained extension step scripts, triggerTurn queueing while streaming, an after-start point and a held get_state. No controller change was needed for N. Next: H, K, E/S/P3, terminal, README, suites.
|
||||
- 2026-10-04: tests/races.test.mjs (H1–H4, H9–H23, 21 tests) green. Whole package 97/97. Three controller defects found and fixed in my files, to report to Sage: (1) `#evaluate` ran `res.outcome` on recover's Promise, so recover over the wire threw internal (W8 missed it because it resumes through a restart); (2) `#admission` skipped the binding push when only the state changed, so clients kept showing "stopping" after a failed force stop (also in `#uncertain`); (3) the client kept non-final receipts live across a new incarnation and now marks them outcome unknown. Fixture fix: fake-pi removes a stale control socket left behind by a SIGKILLed predecessor. Next: K1–K18, E/S/P3, terminal, README, suites.
|
||||
- 2026-10-04: tests/cohort.test.mjs (K1–K18, 18 tests) green twice, no leaked scopes. Fixture changes: fake-pi tool children can ignore TERM and fork `sleep` in a loop; ctrl-child answers `proof`. K13 escapes are refused on this host (nsdelegate). K14 stops the scope with SIGKILL because the shim ignores TERM. Next: terminal, S1–S7, E1–E7, P3, README, suites.
|
||||
- 2026-10-04: src/transcript.mjs and src/terminal.mjs written; tests/flows.test.mjs (S1–S7, P3, E1–E7, two terminal checks, schema check) green twice; package 134/134. Fixed in my source: the `unknown` push's `type` field overwrote the envelope's `type: "push"`, so clients dropped it (field renamed `nativeType`; `#push` now sets `type` last); a Transcript attached to a connected client now reads the page at once. Finding for Sage: pinned Pi appends `thinking_level_change` at startup when the branch lacks one, so K8 fails closed on such sessions. Next: smoke test, README, package.json, suites, mutants, packet.
|
||||
- 2026-10-04: tests/smoke.test.mjs green (pinned Pi 0.85.1 sealed, no credentials, no prompt; also pins the bundled inline `/llama` command and the startup append). README and package.json done. All nine suites green; package tests conversation 137/137, webui 14/14, seat 19/19; chat-00, chat-01 and chat-01c checks pass. control-board failed 1 test: its refusal-status test scans every `new Refusal("…")` in conversation/src and found the controller's `configuration`, `channel` and `target`. Fixed in my files: control-layer refusals now throw `ControlRefusal` (subclass, safe-fs.mjs); README says why; control-board 124/124. Mutation pass running in /tmp scratch copies. Next: mutant results, manifest, packet, queue move.
|
||||
2026-10-04 (later): mutation pass running; kill tests added for mutants 1 (A-B-A generation, H3), 3 (publish race, W1), 8 (single-use recover confirmation, K6), 12 (no revision name before sync, W1). Mutant 14 as first written was equivalent (unreachable line); guard.check restructured so the real-path check sits in one place, mutant retargeted. Next: re-run changed mutants in scratch, remaining survivors, manifest, packet, queue move.
|
||||
- 2026-10-04: candidate frozen and in review. Manifest I1-manifest.sha256 (1404341e, 27 files), packet BUILD-I1.md. Suites green (conversation 141/141). All 41 mutants killed; kill tests added for 16 (link-level poison, H19) and 19 (clear_queue error response, N7; fake-pi failResponse). Row 5 in-review round 1, comment 26671 on #1507. Next: answer reviewer findings (two rounds at most).
|
||||
- 2026-10-04 round 1 rework: Darkwing B1 (seal now an allow-list over extraArgs; non-list engine args refuse; command/preArgs documented as a test hook) and B2 (session key = Pi header ID; header change refuses target) fixed in the tree with N24 and three controller-level W4 tests; n1 and n2 in the README. Round-2 mutants r2-B1..B2b running in scratch. Waiting on Filbert's round-1 findings before freezing the round-2 manifest.
|
||||
- 2026-10-04 round 2 requested: Darkwing B1-B2 and Filbert B1-B6 folded in; conversation 152/152, all other suites green; 60 of 61 mutants killed, r2-B5b (freeze not waited on) survives and is explained in BUILD-I1-r2.md; manifest I1-r2-manifest.sha256 (2b48e333); row 5 in-review round 2, comment 26689 on #1507 (revs 56-58). Next: round-2 verdicts. Sage commits after approval.
|
||||
- 2026-10-04 round 2: Filbert approves (comment 26694 on #1507, rev 60). Follow-ups, none blocking: F1, correction to BUILD-I1-r2.md: only K12's cgroup.freeze read is timing-independent; the post-TERM pid-log check catches a missing freeze only when the fork loop forks between members and kill (Filbert's C4 and r2-B5b survive). A test-only hold after members would make C4 deterministic; r2-B5b still needs a FUSE or privileged fixture. F2: Ctrl-T, text and Enter in one chunk leaves the text in the composer (Enter checked before the queued takeover); fails safe. F3: default engine.command with caller preArgs reaches real Pi unsealed; documented test hook, no I1 production caller. Waiting on Darkwing's round-2 verdict.
|
||||
- 2026-10-04: I1 committed by Sage as 243e153c and pushed after both reviewers approved round 2. Row 5 waiting-on-jason for Gate E (revs 61-62, 1595553a; summary comment 26697 on #1507). Reviewer follow-ups are in DEFERRED for the I3 brief. Nothing further on row 5 until Gate E.
|
||||
@@ -0,0 +1,199 @@
|
||||
# Dewey dirty-tree manifest, 2026-09-26
|
||||
|
||||
Requested by Sage (lead) over T3 on 2026-09-26. Nothing here is committed and
|
||||
nothing was edited to produce it. Baseline: HEAD `21e3e908` on `refactor`;
|
||||
origin/refactor `43d7574d`.
|
||||
|
||||
Sources: Dewey's Pi session
|
||||
`.pi/state/dewey/sessions/2026-09-13T00-10-59-217Z_01a0981a-*.jsonl` (every
|
||||
write, edit and git call it made), `git diff`/`git status`, and Darkwing's
|
||||
`agents/darkwing/work/tree-manifest-2026-09-26.md`. That file already covers
|
||||
the package code, and I agree with its grouping.
|
||||
|
||||
## 1. Paths Dewey authored
|
||||
|
||||
### 1a. WUI design workspace, `agents/dewey/work/wui/`
|
||||
|
||||
Phase B (brand board, 09-08) was published as 53 pinned files in `29c1defe`
|
||||
(#1497). Everything below came later or was left out on purpose.
|
||||
|
||||
| Path | Work | Approval receipt | Ready |
|
||||
|---|---|---|---|
|
||||
| README.md, TASKS.md, DECISIONS.md, HISTORY.md, VERIFY.md (tracked, modified) | D01–D05 records, 09-12 | None. These are author records. | Yes, as author records, once the subset in section 6 is accepted. D-002 was recorded later on 2026-09-26 (section 6). |
|
||||
| DESIGN.md, FEATURES.md (new) | D01, D02 | None | Yes, in the section 6 subset. DESIGN.md's open questions now point to the answers. |
|
||||
| checks/mockups-verify.mjs (new) | D04 | None | No. Needs review. |
|
||||
| mockups/** (15 new files: index, five designs, shared app/data/views/css) | D03 | Partial: the Console subset was copied into `packages/webui` and Filbert verified the copies byte-identical in the ea00ec66 review. The other four designs have no review. | No. They need an independent review, and Jason has to say whether unchosen designs get published at all. |
|
||||
| evidence/mockups/** (66 files, 11 MB, screenshots plus report.json) | D04 evidence | None | No. I recommend committing report.json and a few screenshots, not all 11 MB. |
|
||||
| evidence/t3-*.png, evidence/buzz-*.png, evidence/references.json | B02 third-party captures | Deliberately excluded by the #1497 boundary (`reviews/2026-09-08_publication-trial-dewey-boundary.md`, lines 33–39) | Never. They stay local. |
|
||||
| evidence/candidate-sha256.txt, evidence/first-feedback-notes-sha256.txt | Historical receipts | Excluded by the same boundary | Never. They stay local. |
|
||||
| evidence/downloads/ (2 files) | Test exports, not Jason's review | Excluded by the same boundary | Never. They stay local. |
|
||||
| evidence/board-*.png (6), focus-short-desktop.png, section-*.png (4), detail-check-run.txt, verify-run.txt | B07 captures and logs that were not on the allowlist | Omitted by the boundary to keep the package bounded, not for privacy | Stay local unless someone asks for them. |
|
||||
|
||||
I scanned the D-phase files for credential shapes (GitHub, GitLab, Slack,
|
||||
OpenAI-style, bearer, private key, Discord token) and found nothing.
|
||||
`mockups/shared/data.js` is hand-written sample data. It includes Jason's
|
||||
first name and seat names, but no paths under `/home` and no email addresses.
|
||||
|
||||
`agents/dewey/scratch/` is empty. `.pi/state/dewey/` is ignored by `.pi/.gitignore`.
|
||||
|
||||
### 1b. Lines Dewey wrote in shared records
|
||||
|
||||
| Path | Dewey's part | Approval receipt | Ready |
|
||||
|---|---|---|---|
|
||||
| BUILD-LOG.md | `## 2026-09-13 — Session-chat refinement and owner confirmation (#1507)`. The CHAT-00, CHAT-01 and CHAT-01C entries next to it were not written from Dewey's session. They read as Darkwing's. | Owner confirmation: #1507 comment 26093 | Yes, but only after the working file is rebuilt as HEAD plus the uncommitted entries. The working copy is missing the committed row 23–25 entries, as Darkwing's section 5 says. Staging it as it stands would delete append-only history. |
|
||||
| docs/SESSIONS.md | 14 `— dewey —` lines, 2026-09-13 to 09-15 | Append-only log | Yes, with the rest of the file |
|
||||
| docs/plans/QUEUE.md | Row 5 State (Darkwing last rewrote it on 09-13; it is stale, see below). Log line `2026-09-13 — dewey: Jason feedback blocks WebUI Gate E…`. Parked row `Console features outside the refined session-chat brief…`, which replaced `Console screens beyond the first`. | #1507 comment 26082 (feedback); WEBUI Q1 (deferral) | Row 5 State needs correcting before commit. The log line and parked row are ready. |
|
||||
| docs/plans/CURRENT.md | Row 5 narrative (the piece-4 paragraph, from "Jason's 2026-09-13 feedback" to "No code or live effects started"). Dewey and Darkwing both wrote it. | 26082, 26093, 26094, 26100, 26103, 26131, 26152 | Needs the same correction as row 5 |
|
||||
| docs/plans/reviews/2026-09-08_publication-trial-dewey-boundary.md (new) | Whole file, Dewey's #1497 author declaration. Darkwing's manifest counts it among his 21 publication-trial files. | Part of the #1497 trial; Darkwing's receipt is in `…-wui-coordination.md` | Yes, as a record, in Darkwing's batch |
|
||||
|
||||
**Row 5 correction.** `b023841c` (CHAT-01C, author and committer Darkwing) is on
|
||||
origin/refactor: `git branch -r --contains b023841c` returns `origin/refactor`.
|
||||
"Publication remains held" and the HTTP 403 text are out of date. Suggested
|
||||
State: `CHAT-00/01/01C contracts published (370823b3, 28d4e98a, b023841c);
|
||||
CHAT-02..08 not chartered; Gate E blocked, see agents/dewey/work/tree-manifest-2026-09-26.md`.
|
||||
I have not edited QUEUE.md, because Sage is editing it today.
|
||||
|
||||
## 2. `packages/webui`: Dewey is the path owner, not the author
|
||||
|
||||
Dewey's own source is all in `ea00ec66` (published 09-13, Filbert R1 approved).
|
||||
None of the uncommitted edits below are Dewey's. Dewey reviewed the UX for
|
||||
three of the four rows.
|
||||
|
||||
| Path | Author, row | Dewey's review |
|
||||
|---|---|---|
|
||||
| src/public/app.js | Filbert (row 18 connector status, #1512 relaunch notice); Rocko (#1511 set-by) | Row 18 R1–R3 approved (26255–26257); #1511 R2 approved (`agents/darkwing/work/task-attribution/r2-review.md`); #1512 R1 approved (`…/relaunch-activity/r1-review.md`) |
|
||||
| tests/fixture.mjs | Darkwing (row 22 `Input needed:` prefix); Rocko (#1511 setter) | Row 22 change acknowledged, no review asked; #1511 R2 |
|
||||
| tests/browser.test.mjs, tests/browser-edge.test.mjs | Rocko (#1511) | #1511 R2 |
|
||||
| tests/discord.test.mjs (new) | Filbert (row 18) | Row 18 R1–R3 |
|
||||
| tests/relaunch.test.mjs (new) | Filbert (#1512) | #1512 R1 |
|
||||
|
||||
`app.js` on disk has SHA-256 `ce7d79a4…`, the same value as the #1512 R1 rerun
|
||||
manifest (`/tmp/relaunch-activity-r1-rerun-oupRiqIC/CANDIDATE.sha256`). HEAD's
|
||||
copy is `7f30847b…`. I agree with Darkwing: one cumulative commit for rows 18,
|
||||
22, #1511 and #1512, citing each receipt. WebUI test run today on the working
|
||||
tree: see section 3.
|
||||
|
||||
## 3. Test evidence, today
|
||||
|
||||
`node --test --test-concurrency=1 packages/webui/tests/` on the working tree.
|
||||
The log is at `/tmp/dewey-webui-tests-2026-09-26.txt`. Result: 8 tests, 8
|
||||
pass, 0 fail, about 5.0 s. The run covers the Console browser, the Discord row,
|
||||
the relaunch notice, loopback and origin fencing, board passthrough, proxy
|
||||
byte preservation and CLI rejection.
|
||||
|
||||
The `app.js` in #1511 R2's manifest (`954d98fa…`) predates the #1512 layer.
|
||||
The current file is the one #1512 R1 reviewed, and that review built on #1511.
|
||||
|
||||
## 4. Row 5 (#1507): what is left before Gate E
|
||||
|
||||
Published contracts: CHAT-00 `370823b3`, CHAT-01 `28d4e98a`, CHAT-01C
|
||||
`b023841c`. The refinement plan `2026-09-13_webui-session-chat.md` is
|
||||
committed at `4c436f4c` and has no local edits. No runtime code for session
|
||||
chat exists.
|
||||
|
||||
Gate E, from plan section 6: every in-scope seat reaches Dewey's Console
|
||||
through a verified live adapter, Jason uses it for a workday, and then rules.
|
||||
Tasks still open:
|
||||
|
||||
| Task | What | Blocking gates |
|
||||
|---|---|---|
|
||||
| CHAT-02 | Read-only histories and relative Age; carries Rocko R3-1 | Charter and backend author |
|
||||
| CHAT-03 | Pi and Claude live adapters, mediated terminal | B1/B2 protocol, trust and tool-isolation proof |
|
||||
| CHAT-03I / CHAT-03D | Ingress and handoff contract; native dialog contract | 03I changes fleet comms, so Jason rules first |
|
||||
| CHAT-04 | Durable queues, attachments | B3/B4 every-ingress fencing, crash recovery, supervisor evidence |
|
||||
| CHAT-04R | Private remote control | Q22 prerequisites |
|
||||
| CHAT-05 | Console session-chat UI (Dewey writes, Filbert reviews) | Stable CHAT-02..04 surfaces |
|
||||
| CHAT-06 / 06P | Combined candidate, publication | Reviews, then Jason's push word |
|
||||
| CHAT-07 | Live cutover, one seat at a time | B6 envelope for each target, Jason's approval for each seat |
|
||||
| CHAT-08 | All-seat demo, which is Gate E | Everything above, then Jason's workday ruling |
|
||||
|
||||
Rocko R3-2 belongs in CHAT-01C, which is already published. I have not
|
||||
checked whether it landed there. R3-3 (observer revocation) waits for a later
|
||||
fixture revision.
|
||||
|
||||
### Dewey can do now, without Jason
|
||||
|
||||
1. Correct the stale row 5 and CURRENT text (section 1b). I'll send Sage the
|
||||
wording, since QUEUE is shared.
|
||||
2. Append the D-002 disposition to TASKS.md and HISTORY.md in `wui/`.
|
||||
3. Write the regression the plan requires for the reported return-flow
|
||||
failure, and restore relative Age from the board's existing `ageSeconds`.
|
||||
These are small Console changes and need a Filbert review.
|
||||
4. Design and prototype CHAT-05 in `agents/dewey/work/` against the CHAT-01
|
||||
and CHAT-01C schemas, with no live wiring.
|
||||
5. Review the UI semantics of CHAT-03D and CHAT-03I, plus the UX of any
|
||||
backend candidate.
|
||||
|
||||
### Needs Jason
|
||||
|
||||
1. **What "all seats" means now.** Q3/Q19 were decided when the `~/.mosaic`
|
||||
fleet was the seat set. That fleet is being retired, development seats run
|
||||
in T3, and row 8 is parked. Gate E's scope, CHAT-04R and CHAT-07 all
|
||||
depend on this.
|
||||
2. **Priority.** His named priority is row 6, with #1508 rows 9–13 after it.
|
||||
Q22 orders row 5 behind that, so CHAT-02..08 cannot start without his word.
|
||||
3. **Charters and backend authors for CHAT-02..04.** Each earlier CHAT phase
|
||||
had his approval. The plan names Darkwing as the backend's accountable
|
||||
owner, and that now goes through Sage as lead.
|
||||
4. The CHAT-03I ruling on fleet comms.
|
||||
5. Sacrificial force-stop runs that need his approval under B3/B4.
|
||||
6. His push word for each publication.
|
||||
7. His approval for each seat's cutover.
|
||||
8. The workday itself, and his ruling after it.
|
||||
|
||||
## 5. The WebUI process running now, PID 1266267
|
||||
|
||||
It runs current source. Started 2026-09-13 16:19 CDT as
|
||||
`node packages/webui/src/cli.mjs serve`.
|
||||
|
||||
- Server code: `cli.mjs` and `serve.mjs` have not changed since `ea00ec66`,
|
||||
and they match HEAD and the working tree. The process loaded the same code
|
||||
that is on disk.
|
||||
- Static assets: `serve.mjs` reads each file from disk for every request and
|
||||
sends `cache-control: no-store`. The page therefore serves the working
|
||||
tree, including the uncommitted #1512 `app.js` (`ce7d79a4…`), not HEAD's
|
||||
(`7f30847b…`). Hashes of the live `/`, `/app.js` and `/live.css` responses
|
||||
match the working-tree files. `/healthz` returns ok. `/api/config` points
|
||||
to the board at `http://127.0.0.1:7331`.
|
||||
- Caveat: the board backend it proxies (PID 3414098, started 2026-09-14
|
||||
19:10) predates #1512. `/api/board` returns 44 rows, with `taskSetBy` and
|
||||
`connector` fields but no `relaunchedAt`, so the relaunch notice cannot
|
||||
show up in live use until the board is restarted. That board belongs to
|
||||
Darkwing and Filbert. I have not touched it.
|
||||
|
||||
## 6. Update later on 2026-09-26: D-002 recorded, and a subset proposal
|
||||
|
||||
Sage approved both. Both stay uncommitted.
|
||||
|
||||
**D-002 recorded.** TASKS.md now marks D05 and I01 done and I02 and I03
|
||||
superseded, and the Next action is none. README.md's status line, DESIGN.md's
|
||||
open questions, a new DECISIONS.md section and a new HISTORY.md entry all
|
||||
point to D-002, #1507 comment 26082 and the session-chat plan. The logo and
|
||||
the default palette are still Jason's to decide.
|
||||
|
||||
**Proposed commit subset for `agents/dewey/work/wui/`, about 1.5 MB:**
|
||||
|
||||
| Include | Size | Why |
|
||||
|---|---|---|
|
||||
| README.md, TASKS.md, DECISIONS.md, HISTORY.md, VERIFY.md (tracked edits) | text | The D-phase record and the D-002 reconciliation |
|
||||
| DESIGN.md, FEATURES.md | text | D01/D02, cited by the records |
|
||||
| checks/mockups-verify.mjs | 68 KB dir | Reproduces the evidence; VERIFY.md cites its 31-check run |
|
||||
| mockups/** (all 15) | 192 KB | Console depends on the shared app, views, data and css, so it can't be split out without breaking the index. The other four stay as reference. |
|
||||
| evidence/mockups/report.json | 7 KB | The only evidence file README.md and VERIFY.md cite |
|
||||
| evidence/mockups/index-390.png, index-1440.png, d3-console-home-{390,1440,3440}.png, d3-console-inspector-1440.png, d3-console-text-200-320.png | about 1.2 MB with report.json | The comparison page plus the chosen design at phone, desktop, ultrawide and 200% text sizes |
|
||||
|
||||
**Stays local:** the other 58 screenshots in `evidence/mockups/` (about
|
||||
9.8 MB), which are regenerated by `checks/mockups-verify.mjs`, and all the
|
||||
excluded B-phase items in section 1a.
|
||||
|
||||
Open points for the commit plan:
|
||||
|
||||
1. Nobody has independently reviewed these files. Filbert only
|
||||
byte-compared the Console copies. A light review would cover the records'
|
||||
accuracy and the sample data in `mockups/shared/data.js`: no credentials,
|
||||
no paths under `/home`, no email addresses, only Jason's first name and seat
|
||||
names.
|
||||
2. The four designs Jason didn't choose are in `mockups/`, so they go public
|
||||
when this pushes. Jason's push word should cover this list, or the
|
||||
commit should leave out `d1`, `d2`, `d4` and `d5` HTML/CSS. If they are
|
||||
left out, the index links break.
|
||||
@@ -80,3 +80,20 @@ Jason clarified that unfinished iconography should not prevent design progress.
|
||||
- Final logo and iconography remain unresolved, but are not blockers. Use a Mosaic Stack text wordmark and provisional, labeled UI icons in the meantime.
|
||||
- Prioritize the source-linked feature inventory, then five distinct navigable dashboard mockups and a central comparison page. Treat implemented, planned, and newly proposed backend capabilities separately.
|
||||
- This does not authorize backend coding, integration, deployment, commits, or pushes. No new goal was activated during this clarification.
|
||||
|
||||
## 2026-09-12: dashboard mockup decisions (Dewey, pending Jason's selection)
|
||||
|
||||
- Landing page: the control board from MOSAIC-STACK-D-001 is the home route in all five designs. Tradeoff: sessions and projects become one click deeper than a conversation-first home. Marked "proposed" in the interface because the foundation has a scanner but no page yet.
|
||||
- Five styles: Rail (left rail), Deck (top tabs and cards), Console (command bar, tree, dense table, inspector), Board (kanban by session state), Stream (pinned inbox and timeline with bottom tabs). Chosen to differ in navigation model rather than decoration, so the comparison is about how Jason works.
|
||||
- Defaults: Harbor palette; appearance follows the system preference until chosen; Dim is selectable. Provisional, carried over from the brand board.
|
||||
- Simulated actions: every control shows a notice that begins with "Simulated" and says nothing changed. Refusals (retired workspace, duplicate launch, empty message) are shown as refusals with a reason, never as success. Prototype notes appear in every shell.
|
||||
- Status colours: counters on warning, success, and danger backgrounds use canvas-coloured text rather than white so Dim and Dark keep 4.5:1. Badge tints are 8 percent and the current-nav tint 9 percent for the same reason.
|
||||
- Contrast harness: fixed the sampler to read Chromium's `color(srgb ...)` output instead of relaxing the threshold.
|
||||
- Board header: below 1600 px the pill navigation wraps under the brand row on purpose, matching the phone layout, instead of pushing the controls to a second row.
|
||||
- Not decided: the design to iterate, added or dropped features, and the logo. These are Jason's.
|
||||
|
||||
## 2026-09-26: D05 disposition recorded (Dewey)
|
||||
|
||||
- Jason selected d3 Console on 2026-09-12, recorded as MOSAIC-STACK-D-002 in `docs/plans/2026-09-12_control-board-mvp.md`, Piece 4. The section above said "pending Jason's selection" until today, because nobody had copied the ruling into this workspace.
|
||||
- The other open items above are settled as follows. Design: Console. Feature scope: #1507 comment 26082 and the session-chat plan (Q1-Q22). Logo and default palette: still Jason's, and still undecided.
|
||||
- I02 and I03 are superseded because Console went straight into `packages/webui` (ea00ec66). The mockups are kept for reference. Whether the four designs Jason didn't choose get published is his call.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Five dashboard mockups: shared design spec (D02)
|
||||
|
||||
Author: Dewey. Date: 2026-09-12. Inputs: [FEATURES.md](FEATURES.md), [BRIEF.md](BRIEF.md), [DECISIONS.md](DECISIONS.md), brand.js tokens.
|
||||
Prototype only. Local HTML, no build, no network, no backend. Every action is simulated in the browser and says so.
|
||||
|
||||
## Shared rules across all five
|
||||
|
||||
- Typography: Manrope from assets/fonts. Text wordmark "Mosaic Stack". Provisional labeled outline icons from brand.js; every icon has a visible or accessible text label.
|
||||
- Color: the ten brand palettes and Light, Dim, Dark modes from brand.js tokens. Semantic status colors (success, warning, danger) stay stable across palettes.
|
||||
- Landing route is the control board (MOSAIC-STACK-D-001). "Waiting on you" is above the fold in every design.
|
||||
- Every session and work row shows agent, project, workspace.
|
||||
- Feature legend in each mockup: implemented, planned, proposed, mockup, matching FEATURES.md.
|
||||
- Widths: usable from 320 px to 3440 px. Layouts use available width; prose is bounded; ultrawide adds columns rather than stretching text.
|
||||
- Keyboard: skip link, visible focus, all controls reachable in order, Escape closes overlays, hash routes work with the back button.
|
||||
- States shown in every design: loading, empty, error with recovery, success, disabled, long content, and a refusal (fail-closed) notice.
|
||||
- No fake completion: the demo "state" switch changes what is rendered; nothing is saved or sent.
|
||||
|
||||
## Routes (hash router, shared)
|
||||
|
||||
| Route | Name | Primary content |
|
||||
| --- | --- | --- |
|
||||
| #/ | Control board | Waiting on you; running sessions across projects; recent settled; quick launch |
|
||||
| #/sessions | Sessions | All sessions with filters, controller, generation, config check |
|
||||
| #/sessions/:id | Session detail | Transcript excerpt, goal footer, steering queue, attach as observe or control, config check, evidence |
|
||||
| #/projects | Projects | Projects with their workspaces, registered agents, missions |
|
||||
| #/projects/:id | Project detail | Workspaces, registrations, missions, decisions |
|
||||
| #/agents | Agents | Agent definitions, harness, SOUL revision, where registered |
|
||||
| #/work | Work | Missions, tasks, assignments, kanban columns |
|
||||
| #/runs | Runs and evidence | Run records, result.json summary, prune receipts, releases |
|
||||
| #/messages | Messages | Workspace-addressed threads; comms list |
|
||||
| #/library | Library | Skills, extensions, packages lifecycle |
|
||||
| #/settings | Settings | Appearance (palette, mode), accounts by metadata, roles, onboarding |
|
||||
| #/launch | Launch | Agent, project, workspace, Resume or Fresh, work decision |
|
||||
| any other | Not found | Recovery links |
|
||||
|
||||
## Primary flows to exercise
|
||||
|
||||
1. See what needs me: open home, read "waiting on you", open the item, act (approve, reply, connect) in simulation.
|
||||
2. Check a running session: home row to session detail, request observe, note controller and generation.
|
||||
3. Launch: pick agent, project, workspace; choose Resume; get an already-running conflict; choose connect.
|
||||
4. Find work: Work route, filter by project, open a task, see assignment and dependencies.
|
||||
5. Change appearance: Settings, choose palette and mode; persists locally.
|
||||
6. Hit a refusal: Launch with missing workspace policy; fail-closed notice with next step.
|
||||
|
||||
## Sample data (shared/data.js)
|
||||
|
||||
- Projects: mosaic-stack (workspaces sessions, skills, wui), personal (journal, health), acme-ops (ops, social).
|
||||
- Agents: darkwing (coordinator), dewey (design), rocko (coder), filbert (reviewer), sage (advisor), velma (research), topher (writer).
|
||||
- Sessions: ten across projects with mixed states: running, waiting on owner, quiet wait, blocked, stalled, settled, stopped. One conflict candidate.
|
||||
- Waiting on you: five items: acceptance gate, blocked goal report, quiet wait check-in, decision needed, config mismatch.
|
||||
- Missions, tasks, assignments, decisions, runs, releases, accounts (metadata only, fake markers), skills, messages.
|
||||
|
||||
## The five styles
|
||||
|
||||
| ID | Name | Navigation | Home composition | Who it suits | Risk |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| d1 | Rail | Left icon rail with labels, collapsible; page header with breadcrumbs | Two-column: waiting list left, running sessions table right; ultrawide adds evidence column | Professionals used to Slack, Linear, T3 | Familiar; may feel generic |
|
||||
| d2 | Deck | Top bar with tabs; content is cards on a fluid grid | Card deck: one card per session; "waiting on you" as a highlighted row of cards | Homemakers, students, mixed audiences | Cards can hide density |
|
||||
| d3 | Console | Command bar first (Ctrl+K), left project tree, right inspector drawer | Dense table with keyboard row selection and an inspector pane | Developers, operators | Steeper learning curve |
|
||||
| d4 | Board | Columns by state (kanban), project filter chips across the top | Columns: Waiting on you, Running, Quiet wait, Blocked, Settled; sessions as cards | Project managers, kanban users | Wide layouts; narrow screens scroll horizontally |
|
||||
| d5 | Stream | Bottom tab bar on mobile, side tabs on desktop; conversation-first | Chronological stream of session events with a pinned "waiting on you" inbox | Personal assistant use, phones first | Weaker cross-project overview |
|
||||
|
||||
## Open questions recorded for the owner
|
||||
|
||||
- Which of the five to iterate (D05 gate). Answered 2026-09-12: d3 Console, MOSAIC-STACK-D-002.
|
||||
- Whether the web client should offer control or only observe at first. Answered in the session-chat plan (Q1-Q22): full two-way chat, tools, attachments and native approvals, with enforced controller transfer.
|
||||
- Default palette and mode remain unselected. Mockups default to Harbor and system preference.
|
||||
@@ -0,0 +1,156 @@
|
||||
# WUI feature inventory (D01)
|
||||
|
||||
Author: Dewey. Date: 2026-09-12. Source baseline: working tree on `refactor` at 1993039c plus uncommitted docs.
|
||||
Purpose: the source-linked list of capabilities the five dashboard mockups must show, with each item labelled by how real it is today. This inventory drives D02 routes and sample data. It is not a product commitment, a backend contract, or a claim that any item is user-tested.
|
||||
|
||||
## Status labels
|
||||
|
||||
| Label | Meaning | Mockup treatment |
|
||||
| --- | --- | --- |
|
||||
| implemented | Exists in the rebuild today as a script, extension, or suite-covered behavior. | Show as a working surface with realistic sample records. |
|
||||
| planned | Owner-accepted plan, charter, or ruling with defined behavior, not yet built. | Show fully; mark "planned" in the mockup's feature legend. |
|
||||
| proposed | Owner direction or concept note without an accepted plan. | Show where it shapes navigation; label "proposed" and keep it shallow. |
|
||||
| mockup | Interface behavior invented for the prototype to make the flow complete. | Label "mockup" in the legend and in DESIGN.md open questions. |
|
||||
|
||||
No v1 UI was inspected. Nothing here was verified by running the runtime; labels come from reading the cited files.
|
||||
|
||||
## 1. Landing: control board (owner MVP decision)
|
||||
|
||||
| Capability | Status | Source |
|
||||
| --- | --- | --- |
|
||||
| One page listing running agent sessions across projects with status. Deliberately smaller than the current registry line. | proposed, owner decision MOSAIC-STACK-D-001 (2026-09-12) | docs/plans/CURRENT.md lines 17-24 |
|
||||
| A "waiting on you" section: items where the owner is the blocker (goal_report blocked, owner checkpoints, acceptance gates, quiet waits). | proposed, same decision | docs/plans/CURRENT.md lines 17-24; extensions/goal/README.md (blocked and quiet-wait states) |
|
||||
| Per-session status vocabulary: running, waiting on owner, quiet wait, blocked, settled, stalled, stopped. | planned (goal states) and proposed (stall detection) | extensions/goal/README.md; docs/plans/2026-09-06_foundation-mechanical-workflow-topics.md |
|
||||
| Cross-project view means agent, project, workspace shown together on each row. | planned (R5, R11) | docs/plans/2026-09-06_agent-project-workspace-foundation.md R5, R11 |
|
||||
|
||||
Design consequence: every mockup's home route is the control board. Everything else is one step away from it.
|
||||
|
||||
## 2. Agents, projects, workspaces
|
||||
|
||||
| Capability | Status | Source |
|
||||
| --- | --- | --- |
|
||||
| Reusable agent definition: identity, type, harness, SOUL, configuration, permission ceiling. | planned (R1, record catalog `agent-definition`) | foundation plan R1; phase2 contract section 3 |
|
||||
| Project registers agents with RBAC; contains N workspaces; workspace has exactly one parent project. | planned (R2, R3) | foundation plan R2, R3 |
|
||||
| Same agent in many projects and workspaces with separate sessions. | planned (R4) | foundation plan R4 |
|
||||
| Launch names agent, project, workspace. Resume is default; Fresh is distinct; first use auto-creates and announces. | planned (R5, R7) and implemented today only as `scripts/agent.sh <name>` interactive TUI launch | foundation plan R5, R7; docs/TOOLS.md agent.sh |
|
||||
| Fresh work decision: Continue, Abandon, Select, None. | planned (R8; command flags `--work`) | phase2 contract section 5 |
|
||||
| Registration records with role, narrowing restrictions, active or revoked. | planned (R32; `registration` kind) | phase2 contract sections 3, 4 |
|
||||
| Workspace retire and reopen. | planned (R29) | phase2 contract section 5 |
|
||||
| Legacy sessions adopted only by explicit reviewed assignment. | planned (R30) | foundation plan R30 |
|
||||
| Managed worktrees: checkout ownership, protected work, recovery. | proposed concept | docs/concepts/managed-worktrees.md |
|
||||
| Agent seats today: agents/<name>/ with SOUL.md, CONTEXT.md, work folder. | implemented (repository convention) | agents/README.md; agents/dewey/* |
|
||||
| Standard roles catalog: Reader, Contributor, Reviewer, Coordinator, plus separate explicit grants. | planned (phase2 section 4) | phase2 contract section 4; roles/*.json (implemented role files today) |
|
||||
|
||||
## 3. Sessions, attachment, control
|
||||
|
||||
| Capability | Status | Source |
|
||||
| --- | --- | --- |
|
||||
| Session directory per named session under `<dataRoot>/sessions/`; persistent sessions and forks for headless runs. | implemented | AGENTS.md data map; docs/TOOLS.md run-task.sh |
|
||||
| One controlling connection per running session; others observe; explicit transfer with generation. | planned (R24; `execution connect`, `execution transfer`) | foundation plan R24; phase2 contract section 5 |
|
||||
| Resume against an already-running session reports the conflict and offers connection, never auto-attaches. | planned (R21) | foundation plan R21 |
|
||||
| Session attachment across TUI, desktop, and web with the same authority. | proposed concept | docs/concepts/session-attachment.md |
|
||||
| Config-check: launch fingerprint versus current base configuration, non-blocking mismatch notice recommending Fresh. | planned (R16, R17; `execution config-check`) | foundation plan R16, R17; phase2 contract section 8 |
|
||||
| Steering and cancellation: queued versus started work, honest interruption semantics. | proposed concept | docs/concepts/queue-steering.md |
|
||||
| Collaborative state awareness: changed decisions, reconciliation, notification boundaries. | proposed concept | docs/concepts/session-state.md |
|
||||
| Managed terminal: Mosaic-controlled, Pi remains the engine, no native screen parity required. | planned (R34, owner Q28 A) | foundation plan R34 |
|
||||
| tmux agent-send and agent-watch as the current transport. | implemented (scripts) | docs/TOOLS.md agent-send, agent-watch |
|
||||
| Fresh during active work requests controlled replacement. | planned (R25) | foundation plan R25 |
|
||||
|
||||
## 4. Goals, missions, tasks, work coordination
|
||||
|
||||
| Capability | Status | Source |
|
||||
| --- | --- | --- |
|
||||
| Goal extension: /goal set, stop, resume, clear; goal_report satisfied, blocked, in_progress; quiet waits; deadline wake; NG footer and Alt+G recall. | implemented (extension with test suite) | extensions/goal/README.md; docs/plans/2026-09-06_goal-quiet-waits.md; 2026-09-06_ng-goal-footer-dev.md |
|
||||
| Missions and tasks as declarative JSON with strict schemas; missions govern tasks by least-privilege intersection. | implemented | AGENTS.md invariant 4, 7; missions/hello.json; tasks/workspace-demo.json |
|
||||
| Missions at project and workspace level, single parent, one owner. | planned (R18) | foundation plan R18 |
|
||||
| Assignments bind task to agent; coordinator decisions; delegated within-plan decomposition. | planned (R19; `assignment`, `decision` kinds) | phase2 contract section 3; foundation plan R19 |
|
||||
| Approved plan change pauses affected work for reconciliation. | planned (R31) | foundation plan R31 |
|
||||
| Kanban triggering, stall detection, recovery ladder, minimal owner remediation. | proposed | docs/plans/2026-09-06_foundation-mechanical-workflow-topics.md |
|
||||
| Standing intents: events, schedules, aspirations, real wake ownership. | proposed concept | docs/concepts/standing-intents.md |
|
||||
| Conductor role: decompose, dispatch, review, verify, integrate; conductor-apply commits locally. | implemented (protocol and script) | docs/plans/CONDUCTOR.md; scripts/conductor-apply.sh |
|
||||
| CURRENT.md single next action; SESSIONS.md registry; BUILD-LOG phases. | implemented (file conventions) | AGENTS.md session protocol |
|
||||
|
||||
## 5. Runs, evidence, releases
|
||||
|
||||
| Capability | Status | Source |
|
||||
| --- | --- | --- |
|
||||
| Headless task run: `run-task.sh run <task.json>`; write-once run record with result.json, snapshots, stderr.txt. | implemented | docs/TOOLS.md; AGENTS.md invariant 5 |
|
||||
| `mosaic-task.mjs validate, run, show, list, retry, prune, resolve-role`; prune leaves a receipt. | implemented | docs/TOOLS.md |
|
||||
| Invocation-level command evidence: actor, scope, assignment, command, limits, start, end, outcome, evidence refs. | planned (R33, owner Q27 A) | foundation plan R33 |
|
||||
| Audit recording failure blocks affected executions. | planned (R27) | foundation plan R27 |
|
||||
| Release: package, activate, rollback, status; activation log append-only. | implemented | docs/TOOLS.md release.sh; AGENTS.md data map |
|
||||
| Release ensure and unified `mosaic` CLI. | planned (ROADMAP M16, M20) | docs/plans/ROADMAP.md |
|
||||
| Execution context inspector: selected agent, project, workspace, harness, model, each input's source, revision, hash, inclusion decision, size, token estimate. | planned (ACT-1) with partial prompt snapshot today | docs/concepts/context.md; docs/plans/2026-09-07_agent-context-templates-and-migration.md |
|
||||
| Foundation synthetic inspector CLI: permission preview, always labelled SYNTHETIC PREVIEW. | planned charter, partly implemented as fixture-only increment (#1500) | docs/plans/2026-09-06_foundation-inspector-charter.md; docs/plans/CURRENT.md |
|
||||
|
||||
## 6. Auth, providers, harnesses, models
|
||||
|
||||
| Capability | Status | Source |
|
||||
| --- | --- | --- |
|
||||
| `auth.sh status, accounts`; runtime-only secrets, never in repo or image. | implemented | docs/TOOLS.md; AGENTS.md invariant 3 |
|
||||
| Registry of providers, accounts (metadata split from credential file), settings profiles, seat selections, harness manifests; `registry validate, list`. | planned charter (#1499 increment 1), fixture-only pieces done (#1500) | docs/plans/2026-09-10_m20-increment1-charter.md; 2026-09-10_m20-increment2-charter.md |
|
||||
| Multi-account auth, per-seat default accounts, OAuth refresh (gate 7 resolved). | planned (ROADMAP M19; auth plan) | docs/plans/ROADMAP.md; docs/plans/2026-09-03_auth-provider-harness-registry.md |
|
||||
| Agent runtimes: provider, model, harness distinctions; adapter evidence. | proposed concept | docs/concepts/agent-runtimes.md |
|
||||
| Pi pinned exactly; release identity from RELEASE file. | implemented | AGENTS.md version pin |
|
||||
|
||||
## 7. Skills, extensions, packages
|
||||
|
||||
| Capability | Status | Source |
|
||||
| --- | --- | --- |
|
||||
| Skills lifecycle: install, activate, deactivate, uninstall, role ceilings on skills. | planned (ROADMAP M17, M18) | docs/plans/ROADMAP.md |
|
||||
| Extensions directory as canonical source; packages/* monorepo succession. | planned (M20, monorepo layout) | docs/plans/2026-09-06_monorepo-source-layout.md; technical map |
|
||||
| unslop-check as a repository prose gate. | implemented | docs/TOOLS.md unslop-check |
|
||||
| Skill launcher mismatch recovery. | implemented fix record | docs/plans/2026-09-08_skill-launcher-mismatch.md |
|
||||
|
||||
## 8. Messaging, people, federation
|
||||
|
||||
| Capability | Status | Source |
|
||||
| --- | --- | --- |
|
||||
| Messages address a workspace; identity reuse must not mix conversations. | planned (R12) | foundation plan R12 |
|
||||
| `mosaic comms` boundary with site, instance, project, workspace, source, target; list and message. | proposed (owner CLI example) | docs/plans/2026-09-06_foundation-federation-comms-topics.md |
|
||||
| Master registry: site, instance, project, workspace, agent; federation across sites. | proposed | same file |
|
||||
| Multi-user authority: attribution, observation, control, scoped permission. | proposed concept; owner also specified multi-user by default | docs/concepts/multi-user.md; install-onboarding topics |
|
||||
| Transports behind Mosaic: tmux internal, Git durable comms, Matrix, Discord, Slack. | proposed, no backend selected | federation-comms topics |
|
||||
|
||||
## 9. Memory and context
|
||||
|
||||
| Capability | Status | Source |
|
||||
| --- | --- | --- |
|
||||
| Memory architecture: knowledge categories, admission, scope, retrieval. | proposed concept | docs/concepts/memory-architecture.md |
|
||||
| Memory provenance: lineage, correction, deletion coverage. | proposed concept | docs/concepts/memory-provenance.md |
|
||||
| Only designated general preferences shared by default; personal and project context supplied where authorized. | planned (R28) | foundation plan R28 |
|
||||
| One canonical SOUL per agent; template bootstrap; revision resolved at launch. | planned (R16; ACT-1) | docs/concepts/soul.md |
|
||||
| Context sources with permitted scopes and sharing designation. | planned (`context-source` kind) | phase2 contract section 3 |
|
||||
|
||||
## 10. Onboarding, installation, settings
|
||||
|
||||
| Capability | Status | Source |
|
||||
| --- | --- | --- |
|
||||
| Install wizard: deployment type, system auth (SSO, OIDC, LDAP, internal), break-glass admin, initial user, site, instance, initial agent (name, gender, personality, communication style), default harness, harness account, first project and workspace, routing. Required versus optional steps with Skip. | proposed (owner requirements) | docs/plans/2026-09-06_foundation-install-onboarding-topics.md |
|
||||
| Use-case presets: software factory, personal assistant, executive assistant, journal, health tracker, writing, social media, business operations, job applications. | proposed | same file |
|
||||
| Advanced user profile and account linking, opt-in and access-controlled. | proposed | same file |
|
||||
| Reconfiguration mode at any later time, resumable and validated. | proposed | same file |
|
||||
| Ten palettes, Light, Dim, Dark, user-selectable, shared across site and apps. | mockup (brand board), owner-liked | DECISIONS.md; brand.js |
|
||||
| System config is a single fail-closed file created by bootstrap only. | implemented | AGENTS.md invariant 2 |
|
||||
|
||||
## 11. Cross-cutting interface rules taken from the sources
|
||||
|
||||
- Show agent, project, workspace on every session and work surface (R5, R11). Never let the interface keep a competing task list.
|
||||
- Distinguish accepted, planned, proposed, and unverified. Proposals are labelled as proposals (owner Q6). The interface must never present a mockup action as a live effect.
|
||||
- Refusals are first-class: a fail-closed refusal is shown with its reason and what to do, never routed around (AGENTS.md invariant 6).
|
||||
- Waiting on the owner is never expiry into approval (phase2 section 11). The control board shows what is waiting and for how long, with the next step.
|
||||
- Observers cannot control. The interface shows the current controller, generation, and how to request transfer (R24).
|
||||
- Configuration mismatch is a non-blocking notice with an on-demand check (R17).
|
||||
- Secrets never appear. Accounts are shown by metadata only (AGENTS.md invariant 3; M20 charter).
|
||||
- Append-only records are shown as history, never edited in place (AGENTS.md invariant 9).
|
||||
|
||||
## 12. Gaps the mockups will surface for the backend
|
||||
|
||||
These are not features. They are questions the UI raises that the docs do not yet answer.
|
||||
|
||||
1. Where the "waiting on you" list comes from: goal reports, decision records, quiet waits, and acceptance gates are separate sources today.
|
||||
2. Stall detection thresholds and who may act on a stalled session from the web.
|
||||
3. Whether the web client can be a controller (R24) or only an observer at first.
|
||||
4. How notifications reach a user outside the app (no transport chosen).
|
||||
5. Whether project and workspace creation is a web action or a reviewed commit.
|
||||
6. How the control board scopes across sites and instances once federation exists.
|
||||
@@ -26,3 +26,56 @@ Append-only author notes. Goal lifecycle belongs to the operator's extension; TA
|
||||
- First owner review: icon has a robot-crotch association; themes and Manrope received positive feedback. Recorded interpretation and next revision in DECISIONS.md and B09. Prototype files unchanged. First goal remains closed; no dashboard work started.
|
||||
|
||||
- Owner removed final logo/iconography as a dashboard prerequisite. Updated BRIEF.md and TASKS.md: D01 is ready, with Manrope/current themes, text wordmark and provisional labeled icons. This clarification changes task dependencies, not the completed first goal or backend authority.
|
||||
|
||||
## 2026-09-12: five dashboard mockups and comparison (D01 to D05)
|
||||
|
||||
- Took over the Dewey seat from the pi session 01a08111 at Jason's direction. Registered in docs/SESSIONS.md. Folded in owner decision MOSAIC-STACK-D-001: the control board is the landing page of every design.
|
||||
- D01: FEATURES.md inventories capabilities from README, TOOLS, plans, CURRENT.md, and the control board plan, marked implemented, planned, or proposed. No v1 UI was opened.
|
||||
- D02: DESIGN.md fixes 14 routes, primary flows, demo states, and the five styles. mockups/shared/data.js holds a hand-written sample mirroring current sessions and records; credentials appear only as "runtime mount, not shown".
|
||||
- D03: built mockups/index.html and five designs (Rail, Deck, Console, Board, Stream) over one shared app, views, data, and stylesheet. Hash router, palette/mode/state URL overrides, local-storage persistence, and a global rule that every action produces a labelled simulated notice.
|
||||
- D04: wrote checks/mockups-verify.mjs. First run found 11 failures; most were real layout defects at 320 px, two were harness defects (colour parser, static check reading over file URL). Fixed absolutely positioned hidden labels escaping scroll boxes, nav rows growing past headers, breadcrumb and kanban wrapping, bottom-tab sizing, tinted counters failing contrast, and badge tints. Final run: 31 checks, 0 failed, 2 not verified. Author inspected phone, desktop, and 3440 px captures for every design and fixed a stretched avatar and an accidental header wrap in Board.
|
||||
- D05: README.md now opens with the mockup entry path, a comparison table, and an eight-step review script. Stopped at the selection gate. No design is approved; no commit, push, backend, or fleet change occurred. Test Chromium processes and profiles were closed by the script.
|
||||
|
||||
## 2026-09-26: tracking reconciled to D-002
|
||||
|
||||
- Took over the Dewey seat from pi session 01a0981a in T3, under Sage as lead. Found TASKS, README, DESIGN and DECISIONS still saying D05 was waiting on Jason, two weeks after he picked Console (D-002). Recorded the selection, marked D05 and I01 done, and marked I02 and I03 superseded. The Console implementation (ea00ec66) and the session-chat plan replaced them.
|
||||
- Sage asked for this on 2026-09-26. Nothing else in this workspace changed. Nothing was committed.
|
||||
|
||||
## 2026-10-04: slice 1 S5 mockup (row 40, #1522)
|
||||
|
||||
- Built `mockups/slice1/`: inbox, tasks, agents and trail views in the live Console shell, from fixture rows. `reads(S)` in `app.js` stands in for the `packages/bus` read module that Rocko owns (lead decision 56, Q1). The renderers see only its output.
|
||||
- Moved the fixtures from schema v3 to v3a (d55e41fd) once S2 switched. `task.created` now cites its `human.input` in `request`, and the task trail joins through that id. Section 5 of SLICE1-VIEWS.md corrects my Q3 wording, which had put the task ref on the `human.input`.
|
||||
- `checks/slice1-verify.mjs`: 15 checks, 0 failed, 3 not verified. One check inserts every fixture row into schema v3a. On its first run it found a malformed digest, and message and delivery seq numbers that ran against time. All three are fixed.
|
||||
- Mutation round 1 (12 mutants): 11 killed. The skip-link mutant survived because the hashchange guard alone kept the router away. The keyboard check now also requires the route to stay in the URL.
|
||||
- Mutation round 2 (15 mutants, run in a scratch copy that mirrors the repository paths, after a clean baseline): 15 killed. The three new mutants cover the request join, a `task.created` citing an unknown request, and message seq out of time order.
|
||||
- Reported a finding for Rocko and Darkwing (SLICE1-VIEWS.md section 8): a stale poll read becomes the latest snapshot, so `tasks_open` shows the old bucket until the next tick.
|
||||
- Nothing committed.
|
||||
|
||||
## 2026-10-04: S5 mockup reshaped to the Q1 module and schema v3b
|
||||
|
||||
- Lead decision 60: check 15 now loads schema v3b (179ffe35) and reads current state from `task_current` and `tasks_open`. It agrees with the page on every task, #42 included. The "stale read" marks must be exactly the snapshot rows `task_current` skips.
|
||||
- Reshaped to Sage's Q1 shapes. `data.js` holds raw v3b rows, written the way the candidate broker writes them, with millisecond timestamps (the candidate Store refuses the 20-character form; the schema doesn't check it). New `q1.js` ports the four reader verbs. `reads(S)` builds views only from them, and labels anything else "not in the Q1 module". Inbox and decision pages show the `authorization` context and badge the approving option.
|
||||
- New check 16 runs Rocko's uncommitted Store and Broker on a temporary data root with the same rows and compares `inbox`, `tasks`, `agents` and 21 trails with `q1.js`: 203 rows, equal (broker.mjs ead6e668, store.mjs e69cb395). It only imports from `packages/bus`.
|
||||
- Verify: 15 checks, 0 failed, 2 not verified (forced colors, screen reader). The "Q1 module shape" not-verified entry is gone.
|
||||
- Mutation round 3 (21 mutants, scratch copy): 20 killed on the first pass. Reversing the decision trail survived, because check 8 only checked the task trail's order. Check 8 now checks both, and the rerun killed it. M5 was caught by check 15 rather than the check I had named.
|
||||
- SLICE1-VIEWS.md revision 3; section 9 lists what the views need that no Q1 verb returns, for Rocko.
|
||||
- Nothing committed.
|
||||
|
||||
## 2026-10-05: lead decision 63
|
||||
|
||||
- Sage sent the section 9 gap list to Rocko for S4; session events go to S6, credential events to S3. Unfilled gaps keep their label in S5. Check 16 will pin shas from the S2 round 2 commit (manifest 61519059) once Sage names it. Recorded in SLICE1-VIEWS.md and TASKS.md. Nothing committed.
|
||||
- Checked ahead of the commit: the working tree's packages/bus now matches the round 2 manifest (broker.mjs aa5efbbf, store.mjs e69cb395, views.mjs 446b6da4, schema.sql 179ffe35). Verify against it: 15 checks, 0 failed, 2 not verified; check 16 compared 203 rows, all equal (`~/dewey-scratch/s1-verify-r2.txt`).
|
||||
- S2 landed at 38828a2c (Sage; the round 2 manifest 61519059). Check 16 now extracts packages/bus from that commit with `git archive` into its temporary directory and imports from there, so working-tree edits (Rocko's uncommitted README change for row 43, or anything later) don't count. It prints the shas from the commit: broker.mjs aa5efbbf, store.mjs e69cb395, schema.sql 179ffe35. Run from a copy outside the repository it needs `DEWEY_S1_REPO`, or it records "not verified"; tried both ways in the scratch copy. Verify: 15 checks, 0 failed, 2 not verified; 203 rows equal. The six mutants that reach the stand-in (M4, M13, M14, M15, M19, M20) are still killed. Writer shapes in the committed broker match data.js. Headers and SLICE1-VIEWS.md no longer call packages/bus a candidate. Nothing committed.
|
||||
|
||||
## 2026-10-05: S2b (4afab552)
|
||||
|
||||
- Repinned check 16 to 4afab552: broker.mjs 44c161c2, store.mjs e69cb395, schema.sql 179ffe35. Verify: 15 checks, 0 failed, 2 not verified; 203 rows equal. Check 15 doesn't use packages/bus and is unchanged.
|
||||
- Finding (SLICE1-VIEWS.md section 10): S2b sends every agent `message.send` through the single-use authority helper, so a supplied `decision` is checked as an authorization even for a within-role send. A PM `DECISION` message citing an open `role.launch` decision is refused with `decision-mismatch`; at 38828a2c it was written. Reproduced on both commits extracted with git archive (`~/dewey-scratch/s2b-probe.w9pJ`). The fixture's decision DMs (messages 101, 102) are such messages. Check 16 can't see this: it loads rows by SQL and tests only the reader. Recommended the connector send the DM from the decision itself. Fixture unchanged pending a ruling. Nothing committed.
|
||||
- Lead decision 65 (dd35a0ab): a within-role `message.send` treats `decision` as a citation (exists check, stored, not class-matched, not consumed, not in `action.allowed`); otherwise it stays the authority. Messages 101 and 102 stay in the fixture. S2c (row 44, #1526) implements it. Recorded in SLICE1-VIEWS.md section 10 and TASKS.md. Nothing committed.
|
||||
|
||||
## 2026-10-05: S2c (d27042fa)
|
||||
|
||||
- Repinned check 16 to d27042fa (S2c, row 44): broker.mjs 51e03b33, store.mjs e69cb395, schema.sql 179ffe35. Verify: 15 checks, 0 failed, 2 not verified; 203 rows equal; check 15 loads 106 rows. Output: `~/dewey-scratch/s1-verify-d27042fa.txt`.
|
||||
- Probe rerun on 38828a2c, 4afab552 and d27042fa. At d27042fa a within-role citing send is written, the decision is stored and not consumed, an unknown decision refuses with `decision-not-found`, and the send's `action.allowed` carries no decision. That is lead decision 65 as written.
|
||||
- Fixture: added the `action.allowed` {action `message.send`, class `within-role`, target `human`} event 1 ms before messages 101 and 102, as the broker writes it since S2b. Headers in `data.js` and `q1.js` name d27042fa.
|
||||
- SLICE1-VIEWS.md: section 10 says S2c restores 101 and 102, with a d27042fa column in the probe table; sources, sections 1, 7 and 9 cite d27042fa. A broker-written fixture with a citing `DECISION` message is still the regression test check 16 can't give.
|
||||
|
||||
@@ -1,6 +1,41 @@
|
||||
# Mosaic Stack brand board
|
||||
# Mosaic Stack WUI design work
|
||||
|
||||
Status: ready for Jason's branding review. No brand choice is approved and no dashboard mockups have been built.
|
||||
Status as of 2026-09-26: Jason selected d3 Console on 2026-09-12 (MOSAIC-STACK-D-002). It is implemented in `packages/webui` (ea00ec66, #1507) and continues under the session-chat plan, `docs/plans/2026-09-13_webui-session-chat.md`. The five mockups and the brand board below are kept for reference. No brand choice is approved.
|
||||
|
||||
## Dashboard mockups: open the comparison
|
||||
|
||||
Entry page:
|
||||
|
||||
`/mnt/storage/src/mosaic-stack/agents/dewey/work/wui/mockups/index.html`
|
||||
|
||||
Open that file in a browser on this host. It needs the `mockups/shared` folder, `brand.js`, and `assets/fonts` beside it. No server, package, account, or network is required. Everything is a local design prototype: every button produces a "Simulated" notice and nothing touches the fleet, the data root, or credentials.
|
||||
|
||||
The five designs share one data set, one route list, and one settings page. They differ in navigation and in how the control board lays out.
|
||||
|
||||
| Design | Shape | Choose it when |
|
||||
| --- | --- | --- |
|
||||
| Rail | Left rail, board with waiting list on top, running table, side panels; fourth column on ultrawide | You live in it all day and want stable navigation at any width |
|
||||
| Deck | Top tabs, waiting cards in a row, session cards | You glance a few times a day and want the waiting row to shout |
|
||||
| Console | Command bar with Ctrl+K, project tree, dense table, inspector drawer | You run many sessions and prefer keyboard and density |
|
||||
| Board | Kanban by session state with project chips | You think in states and want to see load per column |
|
||||
| Stream | Pinned inbox, time-ordered stream, bottom tabs on phones | You check from a phone often |
|
||||
|
||||
Suggested review, about fifteen minutes:
|
||||
|
||||
1. Pick a palette and appearance on the comparison page. The buttons under each design open it with that choice.
|
||||
2. Open each design's control board. Ask whether the waiting-on-you section would make you act within a few seconds.
|
||||
3. Narrow the window to a phone width, then widen it to your full monitor. The shell should stay usable at both.
|
||||
4. Open the Darkwing session from the Session view button. Try Observe, Request control, Stop, and Check now on the config notice.
|
||||
5. Open Launch. Try the retired workspace archive-2025 and an agent that is already running. Both refuse with a reason.
|
||||
6. On Settings, change the demo state to loading, empty, and error, then back to normal.
|
||||
7. Keyboard only: Tab from the top, use Escape to close anything open, and in Console press Ctrl+K.
|
||||
8. Write notes on the comparison page and download them, or reply here with the design to iterate and what to change.
|
||||
|
||||
What to decide: the design to iterate in HTML, features to add or drop, and whether the control board should stay the landing page. Selection is a human decision; nothing in the pages records approval.
|
||||
|
||||
Records: [feature inventory](FEATURES.md), [design spec](DESIGN.md), [verification](VERIFY.md) with `evidence/mockups/report.json` and screenshots, [tasks](TASKS.md), [history](HISTORY.md), [decisions](DECISIONS.md).
|
||||
|
||||
## Brand board (earlier phase)
|
||||
|
||||
## Open the board
|
||||
|
||||
@@ -32,7 +67,7 @@ You can also download the selected palette's tokens for all three modes. This is
|
||||
- Default palette and appearance behavior.
|
||||
- Palettes to change or remove before dashboard exploration.
|
||||
|
||||
Branding approval is a separate human decision. Choosing controls or downloading notes does not approve anything automatically. Dashboard work stays stopped until Jason supplies the branding decision and direction to proceed.
|
||||
Branding approval is a separate human decision. Choosing controls or downloading notes does not approve anything automatically. Jason later allowed dashboard work to proceed with Manrope, the ten palettes, and a text wordmark while the logo stays unresolved; see DECISIONS.md.
|
||||
|
||||
## Evidence and project records
|
||||
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
# Slice 1 S5: WebUI views for inbox, tasks, agents and trails (design note)
|
||||
|
||||
Dewey, 2026-10-04. Row 40, #1522. Design only: no code, no commit. S5
|
||||
follows S4, so nothing here starts the build.
|
||||
|
||||
Revision 2, same day: lead decision 56 (c9c1699a) answered Q1 to Q5 and
|
||||
accepted all five recommendations. Sections 1, 3.2, 3.5, 5 and 7 now say
|
||||
what was decided. Section 8 is a new finding for the Q1 module. The
|
||||
mockup is in `mockups/slice1/`, checked by `checks/slice1-verify.mjs`.
|
||||
Its fixtures follow schema v3a, and the verify script loads every row
|
||||
into that schema.
|
||||
|
||||
Revision 3, same day. Three things changed:
|
||||
- Schema v3b (179ffe35, lead decision 60) landed. It adds the
|
||||
`task_current` view and builds `tasks_open` on it (lead decision 59).
|
||||
The fixture check now loads v3b and reads current state from those two
|
||||
views. It agrees with the page on every task, #42 included. Section 8
|
||||
records the ruling.
|
||||
- The mockup is reshaped to the Q1 module (Sage's Q1 shapes). `data.js`
|
||||
holds raw v3b rows written the way the candidate broker writes them.
|
||||
`mockups/slice1/q1.js` ports the four reader verbs, and `reads(S)` in
|
||||
`app.js` builds the views only from what those verbs return. A new
|
||||
check runs Rocko's own candidate reader over the same rows and
|
||||
compares. Section 1 and section 7 describe this.
|
||||
- Inbox rows now carry `authorization {action, target, approvalChoice}`
|
||||
from the `decision.raise` event, and the views show it (section 3.1).
|
||||
What the page needs and no Q1 verb returns is labelled on the page and
|
||||
listed for Rocko in section 9.
|
||||
|
||||
Sources, pinned by sha256 prefix:
|
||||
- brief `docs/plans/2026-10-04_slice-1.md` (655e1290, commit 43c48d7a), S5
|
||||
section and the shared rules;
|
||||
- data model note (948b94ce), addendum A (0b36acb0), addendum B (14e747f5).
|
||||
B wins over A, and A wins over the note;
|
||||
- schema v3 `agents/darkwing/work/slice1-proto/schema-v3.sql` (084d0a07,
|
||||
commit 7ed83178, lead decision 55), replaced by schema v3a
|
||||
`schema-v3a.sql` (d55e41fd), which S2 builds on, then by schema v3b
|
||||
`schema-v3b.sql` (179ffe35, lead decision 60). The mockup follows v3b;
|
||||
- `packages/bus` as committed in d27042fa (S2c, row 44; S2 was
|
||||
38828a2c, S2b 4afab552): broker.mjs 51e03b33, store.mjs e69cb395,
|
||||
views.mjs 446b6da4;
|
||||
- PRD draft 0.4 `docs/prd/mosaic-stack.md` (d907705a): REQ-WEB-1,
|
||||
REQ-DEC-1 to 4, REQ-CLI-1, REQ-EVT-1; round 3, 7B;
|
||||
- DEFERRED.md, "CHAT-03 I1 round 2 follow-ups, for the I3 brief".
|
||||
|
||||
Not read on purpose: today's queue. The views follow the data model.
|
||||
|
||||
## 1. What the WebUI reads, and through what
|
||||
|
||||
REQ-WEB-1 says the WebUI shows the same data as the CLI. The S4 brief says
|
||||
every CLI command reads through the broker and never opens `bus.sqlite`.
|
||||
The WebUI follows the same rule:
|
||||
|
||||
- The browser never talks to the broker. The WebUI server (`packages/webui`)
|
||||
calls the broker's read verbs over the local socket and serves JSON to
|
||||
the page, the way it proxies the board today.
|
||||
- The WebUI never opens `bus.sqlite`, the business file or a token file.
|
||||
Bucket titles, bot names and launch limits come from the broker, which
|
||||
already loaded them.
|
||||
- The WebUI adds no write route for slice 1 data (section 6).
|
||||
|
||||
One set of read functions serves both renderers, so the two can't drift.
|
||||
Lead decision 56, Q1: the module lives in `packages/bus`, Rocko owns it,
|
||||
and both the CLI and the WebUI import it. The CLI formats text and the
|
||||
WebUI server serves JSON. The module, `packages/bus/src/views.mjs`, is a
|
||||
thin client: `inbox`, `tasks`, `agents` and `trail(subject)`, each
|
||||
one broker call. The mockup's `mockups/slice1/q1.js` stands in for those
|
||||
four calls over fixture rows, and the verify script checks it returns
|
||||
what the broker at commit d27042fa returns. `reads(S)` in `mockups/slice1/app.js`
|
||||
builds the view objects from the stand-in alone. A second object,
|
||||
`missing(S)`, holds what no verb returns. Each entry names its source,
|
||||
and every renderer that shows one labels it "not in the Q1 module".
|
||||
When Rocko pins an example fixture, it replaces `data.js`.
|
||||
|
||||
## 2. Routes
|
||||
|
||||
Console keeps its shell, its palettes and its keyboard rules. The left
|
||||
column gains a section list above the project tree. Hash routes work with
|
||||
the back button, as the mockups did.
|
||||
|
||||
| Route | View | Main source |
|
||||
|---|---|---|
|
||||
| `#/` | Control board (unchanged) | the board scan |
|
||||
| `#/inbox` | Decisions routed to the human, open first | `decisions`, `decision_events` |
|
||||
| `#/inbox/<id>` | One decision, its options and its history | same, plus `supersedes` chain |
|
||||
| `#/tasks` | Tasks by state | latest `task_snapshots` per task, `task.*` events |
|
||||
| `#/tasks/<ref>` | One task and its trail | same, plus the trail (section 3.4) |
|
||||
| `#/agents` | Role instances, holders and live sessions | business file via broker, `role_claims`, `session.*` events |
|
||||
| `#/trail/task/<ref>`, `#/trail/decision/<id>` | The ordered trail | events, decisions, messages, deliveries |
|
||||
| `#/conversation/...` | CHAT-02 history and CHAT-03 drive (existing) | the board and `packages/conversation` |
|
||||
|
||||
A task ref in a URL is `vikunja:<project>/<task>`, encoded. The `#12` a
|
||||
person sees in Vikunja is display only (addendum A section 1).
|
||||
|
||||
## 3. The views
|
||||
|
||||
Every view keeps the Console states: loading, empty, error with the last
|
||||
good data kept and a warning, and refusal with the reason. All
|
||||
agent-authored text (questions, option text, recommendations, message
|
||||
bodies, task titles and descriptions) is shown as text under the CHAT-02
|
||||
rules: Markdown stays as source, and controls and bidi marks show as
|
||||
visible symbols.
|
||||
|
||||
### 3.1 Inbox
|
||||
|
||||
- Rows: decisions with `route_to = 'human'` and no closing row (`resolved`,
|
||||
`withdrawn`, `expired`). Blocking first (the `urgent_inbox` view), then
|
||||
oldest first. The note's rule is "gated first, then oldest", and every
|
||||
human-routed decision is gated, so `blocking` is the useful split.
|
||||
- Each row: question, class and action, raised by role and run, age,
|
||||
`task_ref` and `requirement_ref` as links, a "blocking" badge, and
|
||||
whether a DM went out (a `deliveries` row with transport `discord-dm`).
|
||||
- Detail: the options as a numbered list of key and text, the
|
||||
recommendation marked, the `seen` history, and earlier versions through
|
||||
`supersedes`, each shown `withdrawn`.
|
||||
- Approval context, from the row's `authorization`: the card and the
|
||||
detail say "Choosing “<key>” approves <action> on <target>.", and the
|
||||
approving option carries an "approves <action>" badge. The approving
|
||||
choice need not be the recommendation: in the fixture, the conflict on
|
||||
#48 recommends `queue`, and `vikunja` approves `task.close`. A decision
|
||||
without a recorded raise says no option is marked as approving.
|
||||
- Resolution stays in the CLI (REQ-DEC-3). The detail shows the exact
|
||||
command, `mosaic decide <id> <key>`, one per option, with a Copy button.
|
||||
Copy puts text on the clipboard and does nothing else. There is no
|
||||
Resolve button, disabled or otherwise.
|
||||
- A second tab, "Routed to roles", lists open cross-role decisions by
|
||||
arbiter (`pm`, `cto`). It's read-only too. Closed decisions get a
|
||||
"Recent" list: choice, by, via and time open.
|
||||
|
||||
### 3.2 Tasks
|
||||
|
||||
- Rows: the latest snapshot of each task in the business, grouped by the
|
||||
five bucket titles (`todo`, `in-progress`, `in-review`, `blocked`,
|
||||
`done`). `done` is collapsed and shows the last 20.
|
||||
- Columns: `#index` and title, assignee (bot name), requirement id (from
|
||||
`task.created`, not the description line), due date, priority, percent
|
||||
done, queue row if the task has one, and the time of the last change.
|
||||
- Markers, each linked to its event:
|
||||
- "changed in Vikunja" for a task in `task_external_changes`. A task the
|
||||
broker never wrote is always in that view (addendum B section 5), so
|
||||
the marker reads "not created by the stack" when no `self` snapshot
|
||||
exists;
|
||||
- "conflict" for an unresolved `task.conflict`, linked to the decision
|
||||
raised to the PM;
|
||||
- "missing" with the reason (`moved`, `not-found`, `no-access`) for a
|
||||
tombstone, and the new project id for `moved`.
|
||||
- Freshness: a line under the heading, "Vikunja last read <time>", from
|
||||
the broker status read S3 adds (Q2).
|
||||
- Which snapshot to show: not always the latest one. Section 8.
|
||||
- "Open in Vikunja" links to the task on `tracker.baseUrl`. It leaves
|
||||
Console and changes nothing.
|
||||
|
||||
### 3.3 Agents
|
||||
|
||||
- One row per role instance in the business file (`pm`, `cto`, `coder`,
|
||||
`reviewer` in slice 1): definition, harness, model.
|
||||
- Holder: the latest `role_claims` row for the instance. A `claim` shows
|
||||
the holder run, harness, address and time held. `release` and `revoke`
|
||||
show "not held" and who released or revoked it; a revoke links its
|
||||
decision.
|
||||
- A claim from a T3 seat carries "advisory" (decision 46, note 6.3).
|
||||
- Live sessions: `session.launched` rows with no matching `session.ended`,
|
||||
with model family and `launched_by`. Above the table, live counts against
|
||||
the business file's limits, for example "Opus 2 of 4, Sonnet 1 of 4", and
|
||||
the launch state from the `launch_state` view ("launching allowed" or
|
||||
"launching revoked at <time>").
|
||||
- Credentials: per instance and service, the latest of
|
||||
`credential.expiring`, `credential.expired` and `credential.changed`.
|
||||
Service names and dates only. There is never a value or a path.
|
||||
- Today's board seats stay on `#/`. The Agents view is about roles. A seat
|
||||
that holds a role links to its board row.
|
||||
|
||||
### 3.4 Trail
|
||||
|
||||
The S4 brief lists the order: request, decisions, launches, task changes,
|
||||
review, close. The trail view is a single time-ordered list with one row
|
||||
per event and a filter by kind. Each row says what happened, which role
|
||||
and run did it, and links to the object.
|
||||
|
||||
For a task, the rows I can join from schema v3:
|
||||
- events with `subject` = the task ref (`task.created`, `.assigned`,
|
||||
`.state`, `.closed`, `.changed.external`, `.conflict`, `.missing`);
|
||||
- decisions with `task_ref` = the task ref, and their `decision_events`;
|
||||
- messages that cite one of those decisions, with their deliveries;
|
||||
- `review.requested` and `review.verdict`, if their subject is the task ref
|
||||
or its queue row.
|
||||
|
||||
What I couldn't join from the schema alone: which launch worked on the
|
||||
task, and which `human.input` was the original request. Q3 was decided:
|
||||
`task.created` names the request in `body.request`, and the broker's
|
||||
trail follows it to the `human.input` and its message. Launches come in
|
||||
through the runs seen on the trail's rows. For a decision, the trail is the decision, its events, the
|
||||
messages that cite it and the events whose body cites it.
|
||||
|
||||
### 3.5 Conversations and CHAT-03
|
||||
|
||||
The brief puts two I3 follow-ups into S5, because the live PM session
|
||||
needs them:
|
||||
- the seal covers the engine command: the I3 entry point takes no engine
|
||||
command or `preArgs` from config, so only the pinned Pi starts;
|
||||
- an explicit engine environment: the engine starts with an allow-listed
|
||||
environment, not `process.env`. That matches addendum A section 4, which
|
||||
starts every agent with an allow-listed environment.
|
||||
|
||||
Lead decision 56, Q5, adds two more of the DEFERRED items to S5: clear
|
||||
`escalating` after a throw, and handle takeover plus Enter arriving in one
|
||||
chunk. Both sit in files S5 already changes. The test that members are
|
||||
listed only under freeze stays deferred.
|
||||
|
||||
Which session Console drives depends on S6. The PM moves off T3 there, and
|
||||
the stack owns its session. Console shows the PM's conversation from
|
||||
whatever S6 records, through `packages/conversation`.
|
||||
|
||||
## 4. Gate E
|
||||
|
||||
Round 3, 7B: Gate E is shown during this step, not separately. The brief:
|
||||
an interactive demonstration with all seats, then Jason's workday ruling.
|
||||
Live cutover still needs its own approval.
|
||||
|
||||
A draft script, to be fixed once S4 and S6 exist:
|
||||
1. Jason opens Console. The board shows the seats, and Agents shows the
|
||||
four role instances with their holders.
|
||||
2. He sends the PM a one-sentence request from the CLI, then watches the
|
||||
task appear in Tasks with its requirement id.
|
||||
3. A gated decision reaches the inbox and his DM. He reads it in Console,
|
||||
copies the `mosaic decide` command and runs it in a terminal. The
|
||||
inbox row closes on the next refresh.
|
||||
4. He follows the task's trail to review and close.
|
||||
5. He drives a conversation through CHAT-03 and takes control, then hands
|
||||
it back.
|
||||
6. He uses Console for the rest of his workday and rules on it.
|
||||
|
||||
Evidence: browser runs against a fixture broker with recorded data, and
|
||||
the live run's screen captures. Model calls during the demonstration are
|
||||
Jason's to start. I don't run them for discovery.
|
||||
|
||||
## 5. Questions, now decided
|
||||
|
||||
Lead decision 56 (c9c1699a) accepted all five recommendations below. Each
|
||||
answer is quoted after the question; the original text stays for the
|
||||
record.
|
||||
|
||||
Q1. **One read layer for CLI and WebUI.** Recommendation: S4 exposes the
|
||||
inbox, tasks, agents and trail reads as functions over the broker client,
|
||||
in a module `packages/webui` can import. The CLI formats text and the
|
||||
WebUI serves JSON. For Rocko, through Sage.
|
||||
*Decided:* one read module in `packages/bus`, owned by Rocko, imported by
|
||||
the CLI and the WebUI.
|
||||
|
||||
Q2. **Freshness of task data.** A quiet poll writes no snapshot (addendum
|
||||
B section 5), so the database can't say when Vikunja was last read, or
|
||||
whether the poller is failing. Recommendation: the broker answers a status
|
||||
read with the last successful board read and cursor read per project, and
|
||||
the last poll error. S3 owns the poller and S2 the read verb. For Darkwing
|
||||
and Rocko.
|
||||
*Decided:* the broker status read goes in S3, Darkwing's.
|
||||
|
||||
Q3. **Trail join keys.** `session.launched` and `human.input` carry no
|
||||
task ref in the schema. Recommendation: put the task ref in `subject` for
|
||||
any event about one task, including `session.launched` when the launch is
|
||||
for a task, and `human.input` when it creates one. That is a body and
|
||||
subject rule, not a new kind, so the closed kind list holds. For Rocko and
|
||||
Darkwing.
|
||||
*Decided:* the task ref goes in `subject` for any event about one task.
|
||||
`task.create` names the `human.input` that asked for it. Darkwing picks
|
||||
whether a trigger or the broker enforces it. Schema v3a does it with
|
||||
triggers: `events_task_subject` for every `task.*` event, and
|
||||
`events_task_created_body`, which requires `request` (the `human.input`
|
||||
id) and `requirement` in the `task.created` body. For `session.launched`
|
||||
and the other kinds, the broker sets `subject`.
|
||||
|
||||
My original Q3 text was wrong on one point. A `human.input` can't carry
|
||||
the task ref, because the task doesn't exist when the request is
|
||||
recorded. The join runs the other way: the task trail takes the
|
||||
`human.input` named in `task.created.request`. The mockup does that, and
|
||||
a check confirms that #41's trail shows its own request and not another
|
||||
task's.
|
||||
|
||||
Q4. **`seen` from the browser.** `decision_events` allows `op = 'seen'`,
|
||||
and the note lists `webui` under `via`. Recommendation: the WebUI writes
|
||||
nothing in slice 1, not even `seen`. Every broker write is stamped from a
|
||||
launch record or comes from the CLI outside a run, and an unauthenticated
|
||||
loopback page is neither. `seen` from the WebUI comes with Pocket ID. For
|
||||
Sage.
|
||||
*Decided:* the WebUI writes nothing in slice 1. It shows the `mosaic
|
||||
decide` command to copy.
|
||||
|
||||
Q5. **I3 follow-ups beyond the brief's two.** Recommendation: fix
|
||||
`escalating` and the takeover-plus-Enter case in S5, since both sit in
|
||||
files S5 already owns. Keep the freeze test as a follow-up. For Sage.
|
||||
*Decided:* as recommended (section 3.5).
|
||||
|
||||
## 6. Boundaries
|
||||
|
||||
- No write route for decisions, tasks, claims, messages or events. Copy is
|
||||
clipboard only.
|
||||
- The page and server stay loopback-only with the existing checks (Host,
|
||||
Origin, body limits, no CORS). The new routes are GET only.
|
||||
- No token value, token path or business-file path reaches the page.
|
||||
Credential rows show the service and dates.
|
||||
- Agent-authored text is inert.
|
||||
- No model calls. Tests use a fixture broker.
|
||||
|
||||
## 7. Before S4 lands
|
||||
|
||||
- Done: a Console mockup of the four views, `mockups/slice1/`.
|
||||
`checks/slice1-verify.mjs` checks it at eight widths, ten palettes in
|
||||
three modes, and covers inert text, read-only behaviour, the inbox,
|
||||
tasks, agents and trail rules, the loading/empty/failed/refused states,
|
||||
the keyboard and long text.
|
||||
- Done: fixtures as raw schema v3b rows (`data.js`), written in the
|
||||
broker's order and with its millisecond timestamps. The Store
|
||||
refuses any `at` or `read_at` that isn't the 24-character
|
||||
`toISOString()` form; the schema doesn't check that, so the fixture had
|
||||
to change.
|
||||
- Check 15 inserts every fixture row (104) into schema v3b (179ffe35) in
|
||||
time order, so its CHECKs and triggers judge the fixture. It compares
|
||||
`urgent_inbox`, `task_external_changes`, `task_current` and
|
||||
`tasks_open` with the page, and requires the "stale read" marks to be
|
||||
exactly the snapshot rows `task_current` skips (#42 only).
|
||||
- Check 16 extracts `packages/bus` from commit d27042fa (S2c, row 44;
|
||||
S2 was 38828a2c, S2b 4afab552) with `git archive`, so working-tree
|
||||
edits don't count. It opens that Store and Broker on a temporary data
|
||||
root, inserts the same rows, binds a reader and requests `inbox`,
|
||||
`tasks`, `agents` and 21 trails (every task, decision and message). The
|
||||
results must equal `q1.js` on the same rows: 203 rows, equal, with
|
||||
broker.mjs 51e03b33, store.mjs e69cb395 and schema.sql 179ffe35. It
|
||||
checks the reader only: rows go in by SQL, not through the broker's
|
||||
writers, so it can't tell whether the broker would write them
|
||||
(section 10). It
|
||||
writes nothing outside its temporary directory, which it removes. Run
|
||||
from a copy outside the repository, it needs `DEWEY_S1_REPO`, or it
|
||||
records "not verified".
|
||||
- A mutation run in a scratch copy breaks one rule at a time, and each
|
||||
check has to fail on the mutant it guards against. Round 3, 21
|
||||
mutants, 20 killed on the first pass. The survivor reversed the
|
||||
decision trail's order; check 8 only checked the task trail's order,
|
||||
and now checks both. M5 (stale mark on the current row) failed check
|
||||
15, not check 6 as I had expected; check 15 now owns that rule. After
|
||||
the fix, 21 of 21 (`~/dewey-scratch/s1mut2/mutants.txt`, scratch only).
|
||||
Report: `evidence/slice1/report.json`.
|
||||
- Next: switch `data.js` to Rocko's pinned example fixture when it
|
||||
arrives. Section 9's gaps decide what S5-B1 needs beyond the four verbs.
|
||||
|
||||
## 8. Finding: a stale poll read becomes the latest snapshot
|
||||
|
||||
*Ruled:* lead decision 59 (e153c3a3) takes the read-side fix and puts it
|
||||
in the schema. The poller keeps writing the stale row as evidence. A v3b
|
||||
amendment (Darkwing) adds a view of each task's current snapshot that
|
||||
skips the stale `poll` row, and rebuilds `tasks_open` on that view. The
|
||||
Q1 module, the CLI and the WebUI read that view and don't reimplement the
|
||||
rule. When v3b lands, the fixture check moves to it and expects
|
||||
`tasks_open` to match the page on every row, #42 included. The Tasks view
|
||||
keeps the "stale read, not shown" marker in the snapshot list.
|
||||
|
||||
*Landed:* v3b (179ffe35, lead decision 60). Check 15 loads it and agrees
|
||||
with the page on every task, #42 included. The page reads the current row
|
||||
from the stand-in `tasks` verb, which reads `task_current`, and marks as
|
||||
stale the snapshot rows after it. The original finding follows.
|
||||
|
||||
For Rocko (the Q1 module) and Darkwing (S3 poller), through Sage.
|
||||
|
||||
Addendum B section 5 writes a poll snapshot whenever its digest differs
|
||||
from the task's latest snapshot, and ignores a stale read when it detects
|
||||
external changes: `task_external_changes` requires
|
||||
`p.read_at > ls.at`, so a board read that started before the broker's
|
||||
own write doesn't count as a change in Vikunja. The next tick settles it.
|
||||
|
||||
The stale read is still stored, though, and it is now the latest
|
||||
snapshot. Anything that shows "the latest snapshot per task", including
|
||||
`tasks_open` and the Tasks view, shows the old bucket until the next tick.
|
||||
|
||||
Example in the fixtures, task #42:
|
||||
- 14:59:40, `self` snapshot: the broker moved #42 to `in-progress`.
|
||||
`updated` stays 09:50, because a move between open columns doesn't change
|
||||
it (probe P3).
|
||||
- 14:59:41, `poll` snapshot, `read_at` 14:59:39: the board read started
|
||||
before the move and still sees `todo`. Its digest differs, so it is
|
||||
written.
|
||||
- Latest snapshot: `todo`. Not external, correctly. Shown as `todo`,
|
||||
wrongly.
|
||||
|
||||
The verify script confirms this against the real schema. After the
|
||||
fixture is loaded into schema v3a, `tasks_open` gives `todo` for #42,
|
||||
while the page shows `in-progress`. That is the only row where they
|
||||
differ.
|
||||
|
||||
What the mockup does: when choosing the row to display, skip a poll
|
||||
snapshot whose `read_at` is at or before the latest `self` snapshot's
|
||||
`at`. The task's snapshot list still shows that row, marked "stale read,
|
||||
not shown". The verify script checks #42 sits under `in-progress` and the
|
||||
marker.
|
||||
|
||||
Recommendation: the Q1 module applies the same rule, and `tasks_open` gets
|
||||
it too, so the CLI and the WebUI agree. The other fix is in the poller:
|
||||
don't write a poll snapshot whose `read_at` is at or before the latest
|
||||
`self` snapshot. That loses no information the external-change rule uses,
|
||||
but it drops a row from the evidence, so I'd leave the choice to Darkwing.
|
||||
|
||||
## 9. What the views need that no Q1 verb returns
|
||||
|
||||
*Ruled:* lead decision 63 (Sage, 2026-10-05) sends this list to Rocko
|
||||
for S4. Session events go to S6 and credential events to S3. Whatever S4
|
||||
doesn't add keeps its "not in the Q1 module" label in S5.
|
||||
|
||||
For Rocko, through Sage. The reader (d27042fa) has four verbs: `inbox`,
|
||||
`tasks`, `agents` and `trail(subject)`. The mockup builds everything it
|
||||
can from those. The rest comes from `missing(S)` in `q1.js`, and the page
|
||||
labels each one "not in the Q1 module". Suggestions are the smallest
|
||||
change I can see; the choice is Rocko's.
|
||||
|
||||
Inbox and decisions:
|
||||
- Decisions routed to roles, and closed decisions (the "Routed to roles"
|
||||
tab and "Recent"). `inbox` returns only open, human-routed decisions.
|
||||
Suggestion: an argument to `inbox`, or a `decisions` verb.
|
||||
- Seen and DM state per inbox row. The page gets them with one trail read
|
||||
per decision. Suggestion: `seen_at` and `dm_at` on the inbox row.
|
||||
- The forward link from a withdrawn decision to the one that replaced it.
|
||||
The trail's decision query matches `id` and `task_ref` only.
|
||||
Suggestion: add `OR supersedes = ?`.
|
||||
- Approval context for a decision that isn't in the inbox. The page reads
|
||||
it from the `decision.raise` event in the trail, which works, but it is
|
||||
the same derivation `#decisionView` already does.
|
||||
|
||||
Tasks:
|
||||
- Requirement, conflict, external-change fields, review and the missing
|
||||
reason all need one trail read per task. Suggestion: an `external` flag
|
||||
from `task_external_changes` and the requirement on each `tasks` row.
|
||||
- Bucket titles, bot names and the tracker `baseUrl`. They are in the
|
||||
business file and the kanban view, which the broker has loaded.
|
||||
- Freshness ("Vikunja last read"): the S3 status read, not built yet.
|
||||
|
||||
Agents:
|
||||
- Role instances, limits and model families, from the business file.
|
||||
- Launch state: the `launch_state` view exists in the schema; no verb
|
||||
returns it.
|
||||
- Live and ended sessions. Nothing writes `session.ended` yet, and
|
||||
`session.launched` carries no model and no `launched_by`.
|
||||
- Credential notices. Nothing writes credential events yet.
|
||||
- `agents` returns only roles whose latest row is a claim, so the view
|
||||
can't say who released or revoked a role. Suggestion: return the latest
|
||||
row per role and let the reader filter.
|
||||
|
||||
## 10. Finding: S2b stopped an agent message citing its decision; S2c restores it
|
||||
|
||||
*Ruled:* lead decision 65 (dd35a0ab) took none of the options below as
|
||||
written. For a within-role `message.send`, `decision` is a citation: the
|
||||
broker checks the decision exists and stores it in `messages.decision`,
|
||||
but doesn't class-match it, doesn't consume it, and leaves it out of the
|
||||
`action.allowed` event. For a send that isn't within-role, it stays the
|
||||
authority. `messages.decision` keeps its "about this decision" meaning,
|
||||
and messages 101 and 102 stay valid. S4 still sends and records the
|
||||
Discord DM. The fix is row 44, S2c (#1526, Rocko, Darkwing reviewing).
|
||||
|
||||
*Landed:* S2c is commit d27042fa (row 44 done at queue rev 157), and it
|
||||
restores messages 101 and 102: the broker at d27042fa writes both. The
|
||||
probe below, rerun on d27042fa, shows a within-role citing send is
|
||||
written, the decision is stored and not consumed (a second citing
|
||||
message is written too), an unknown decision refuses with
|
||||
`decision-not-found`, and the send's `action.allowed` event is {action
|
||||
`message.send`, class `within-role`, target `human`} with no decision.
|
||||
No `action.allowed` row outside `decision.raise` names the decision. The
|
||||
fixture now carries that event 1 ms before each of 101 and 102, and
|
||||
check 16 is pinned to d27042fa. Check 16 still inserts by SQL, so a
|
||||
broker-written fixture with a citing `DECISION` message remains the
|
||||
regression test it can't give. The original finding follows.
|
||||
|
||||
For Rocko and Sage. Found 2026-10-05 while repinning check 16 to S2b.
|
||||
|
||||
`message.send` takes `decision?`, and the broker stores it in
|
||||
`messages.decision`. Up to S2 (38828a2c), a role with `message.send` in
|
||||
its within-role authority returned before the decision was looked at, so
|
||||
`decision` worked as a citation: the PM could send the human a `DECISION`
|
||||
message about an open gated decision. S2b (4afab552) passes every agent
|
||||
send through `#consumeAuthority`, which now checks a supplied decision
|
||||
even for a within-role action. The decision's class and action must
|
||||
match `message.send`, so a citation of a `role.launch` decision refuses
|
||||
with `decision-mismatch`.
|
||||
|
||||
Probe (`~/dewey-scratch/s2b-probe.*/probe.mjs`, both commits extracted
|
||||
with `git archive`, PM with `message.send` within role, an open blocking
|
||||
`role.launch` decision routed to the human):
|
||||
|
||||
| | 38828a2c | 4afab552 | d27042fa (S2c) |
|
||||
| --- | --- | --- | --- |
|
||||
| PM `DECISION` message with `decision: <id>` | written | refused, `decision-mismatch` | written; decision stored, not consumed |
|
||||
| the same message without `decision` | written | written, plus `action.allowed` {action `message.send`, class `within-role`, target `human`} with subject `human` | written, plus the same `action.allowed` |
|
||||
| citing an unknown decision | refused, `decision-not-found` | refused, `decision-not-found` | refused, `decision-not-found` |
|
||||
|
||||
What depends on the citation:
|
||||
- the trail's decision query collects `messages WHERE decision = ?`, and
|
||||
deliveries through those messages;
|
||||
- the inbox's "DM sent" state, which comes from a `discord-dm` delivery
|
||||
of a message citing the decision (section 3.1). The fixture's messages
|
||||
101 and 102 are those DMs, and the broker would now refuse both.
|
||||
|
||||
After S2b, a non-null `messages.decision` can only name a decision that
|
||||
approved `message.send` itself, and that decision is used up by the
|
||||
send. The column now means "the decision that authorized this message".
|
||||
The data model and the views read it as "the decision this message is
|
||||
about".
|
||||
|
||||
Options, for Rocko and Sage:
|
||||
1. A separate citation argument (for example `about`), stored apart from
|
||||
the authorizing decision. That needs a schema column or an event.
|
||||
2. The Discord connector sends the DM from the decision itself, not from
|
||||
an agent message, and records its own delivery. The views then read
|
||||
"DM sent" from that record.
|
||||
3. Keep S2b's rule, and drop "DM sent" and decision messages from the
|
||||
views.
|
||||
|
||||
My recommendation is 2. REQ-DEC-4 and the slice 1 brief's S4 section
|
||||
already give the outbound DM for a blocking gated decision to S4,
|
||||
"through the existing connector (#1509)", so the PM doesn't need to send
|
||||
one. It also leaves `message.send`'s `decision` meaning one thing. S4
|
||||
then has to record the DM somewhere the views can read it; Darkwing's
|
||||
prototype has a `digest.sent` event with a `transport` for the digest
|
||||
side. Until a ruling, the fixture keeps messages
|
||||
101 and 102 as they are, and this section marks them as rows the broker
|
||||
at 4afab552 would refuse. (Ruled above: they stay, and S2c restores
|
||||
them.)
|
||||
|
||||
@@ -25,19 +25,19 @@ Jason removed logo/iconography completion as a prerequisite for dashboard design
|
||||
|
||||
| ID | Task and acceptance evidence | Dependencies | Status | Next action |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| D01 | Research current docs/plans and save a source-linked feature inventory. Distinguish implemented, planned, and owner-proposed capabilities. | B08 and owner gate revision | ready | Read current foundation feature sources, not the v1 UI. Final logo/iconography is not required. |
|
||||
| D02 | Define shared routes, primary flows, states, sample data, and five genuinely different dashboard/navigation styles. | D01 | waiting | Establish comparable coverage across designs. |
|
||||
| D03 | Build a central HTML index linking all five navigable, feature-rich mockups. Apply Manrope, ten palettes, and three modes consistently; use a text wordmark and provisional labeled icons until identity work is finalized. | D02 | waiting | Build shared prototype resources and distinct layouts. |
|
||||
| D04 | Verify mobile/full-width/ultrawide layout, navigation, primary flows, keyboard access, and loading/empty/error/success/disabled/long-content states. Simulate unsupported features honestly. | D03 | waiting | Exercise each design in a browser and record evidence. |
|
||||
| D05 | Deliver the comparison and obtain Jason's design selection. | D04 | waiting | Provide a short review script and wait for selection. |
|
||||
| D01 | Research current docs/plans and save a source-linked feature inventory. Distinguish implemented, planned, and owner-proposed capabilities. | B08 and owner gate revision | done | FEATURES.md, sourced from README, TOOLS, plans, CURRENT.md and D-001. No v1 UI was inspected. |
|
||||
| D02 | Define shared routes, primary flows, states, sample data, and five genuinely different dashboard/navigation styles. | D01 | done | DESIGN.md and mockups/shared/data.js. Control board is the landing route in every design. |
|
||||
| D03 | Build a central HTML index linking all five navigable, feature-rich mockups. Apply Manrope, ten palettes, and three modes consistently; use a text wordmark and provisional labeled icons until identity work is finalized. | D02 | done | mockups/index.html plus Rail, Deck, Console, Board, Stream; shared app, views, data and styles. |
|
||||
| D04 | Verify mobile/full-width/ultrawide layout, navigation, primary flows, keyboard access, and loading/empty/error/success/disabled/long-content states. Simulate unsupported features honestly. | D03 | done | checks/mockups-verify.mjs: 31 checks, 0 failed, 2 not verified (forced colours, screen reader). VERIFY.md and evidence/mockups. |
|
||||
| D05 | Deliver the comparison and obtain Jason's design selection. | D04 | done | Delivered 2026-09-12: README.md review script and mockups/index.html. Jason selected d3 Console the same day, recorded as MOSAIC-STACK-D-002 (`docs/plans/2026-09-12_control-board-mvp.md`, Piece 4). This row was not updated until 2026-09-26. |
|
||||
|
||||
## Phase 3: selected HTML design
|
||||
|
||||
| ID | Task and acceptance evidence | Dependencies | Status | Next action |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| I01 | Record Jason's selected design, change requests, and newly requested features. | D05 | waiting | Turn feedback into bounded prototype tasks. |
|
||||
| I02 | Iterate and reverify the chosen HTML mockup through explicit owner acceptance. Record unsupported backend requirements separately. | I01 | waiting | Implement approved prototype revisions only. |
|
||||
| I03 | Prepare design assets, tokens, interaction specification, and backend dependency notes for a separately authorized coding/integration phase. | I02 | waiting | Deliver handoff without starting integration. |
|
||||
| I01 | Record Jason's selected design, change requests, and newly requested features. | D05 | done | Selection: D-002. Change requests and new features: #1507 comment 26082, refined into `docs/plans/2026-09-13_webui-session-chat.md` (Q1-Q22, CHAT-00..08). |
|
||||
| I02 | Iterate and reverify the chosen HTML mockup through explicit owner acceptance. Record unsupported backend requirements separately. | I01 | superseded | D-002 took Console straight into `packages/webui` (ea00ec66, #1507). Further iteration happens there under the session-chat plan, not in these mockups. |
|
||||
| I03 | Prepare design assets, tokens, interaction specification, and backend dependency notes for a separately authorized coding/integration phase. | I02 | superseded | Dewey implemented the Console directly in ea00ec66, so no separate handoff was needed. Backend dependencies are in the session-chat plan. |
|
||||
|
||||
## Boundaries and recovery
|
||||
|
||||
@@ -45,5 +45,15 @@ Jason removed logo/iconography completion as a prerequisite for dashboard design
|
||||
- No v1 UI inspection, backend integration, credentials, live fleet changes, publication, commit, or push.
|
||||
- Do not claim unperformed browser checks, approved branding, or backend support.
|
||||
- No numeric budget supplied. No external requests, watches, or uncertain operations outstanding.
|
||||
- Next action: D01 feature inventory, then D02-D05 five dashboard alternatives. Logo/iconography refinement is non-blocking under Jason's revised direction. The completed first goal has not been restarted.
|
||||
- Next action (2026-09-26): none in this workspace. WebUI work continues under QUEUE row 5 and the session-chat plan. Logo/iconography refinement is non-blocking under Jason's revised direction. The completed first goal has not been restarted.
|
||||
- The operator goal ends at B08, a review-ready brand board with documented limits. The extension owns its completion status. B09 is not automatically accepted. Later authorized work covers brand iteration or subsequent phases.
|
||||
|
||||
## Phase 4: slice 1 S5 (row 40, #1522)
|
||||
|
||||
Brief: `docs/plans/2026-10-04_slice-1.md`, S5. S5 follows S4. Until then only design work happens here, read from the slice 1 data model, not the queue (Sage, 2026-10-04).
|
||||
|
||||
| ID | Task and acceptance evidence | Dependencies | Status | Next action |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| S5-D1 | Design note: routes, the four views, data sources, Gate E draft, open questions. Evidence: SLICE1-VIEWS.md. | Schema v3 (7ed83178) | done | Revision 2 records lead decision 56 (Q1 to Q5) and the stale-read finding (section 8). Revision 3 records v3b (lead decision 60), the Q1 reshape and the gap list (section 9). |
|
||||
| S5-D2 | Console mockup of inbox, tasks, agents and trail from fixture rows in the Q1 module's shapes; checked at the usual widths, palettes and modes. | S5-D1 | in review | `mockups/slice1/`; `checks/slice1-verify.mjs`: 15 checks, 0 failed, 2 not verified (forced colors, screen reader). Fixtures are raw schema v3b (179ffe35) rows; check 15 agrees with `task_current` and `tasks_open` on every task (lead decision 60). `q1.js` stands in for the four reader verbs, and check 16 matches it against the reader extracted from the S2c commit d27042fa (203 rows). S2b refused an agent message citing its decision; S2c (row 44, #1526, lead decision 65) restores it, and the probe confirms it on d27042fa (SLICE1-VIEWS.md section 10). The fixture carries the `message.send` `action.allowed` event for messages 101 and 102. Gaps for Rocko: SLICE1-VIEWS.md section 9. Next: switch to Rocko's pinned fixture when it arrives. Gaps went to S4, S6 and S3 (lead decision 63). |
|
||||
| S5-B1 | Build in `packages/webui`, `packages/conversation`, `packages/control-board`; I3 follow-ups; Gate E. | S4 done, S6 for the PM session | blocked | Wait for S4. |
|
||||
|
||||
@@ -59,3 +59,61 @@ Environment used: Node 26.8.1, Chromium 152.0.7977.75 on Arch Linux. The CDP hel
|
||||
- Logos are original author studies, not independently reviewed, user-tested, trademark-cleared, or final production assets. Small-size optical refinements await selection.
|
||||
- No independent technical review, issue lifecycle, repository delivery suites, integration, deployment, commits, or pushes were performed for this standalone design package. Those delivery gates remain separate if the prototype is promoted into product work.
|
||||
- Jason has not approved branding. B09 and all dashboard tasks remain behind that human gate.
|
||||
|
||||
|
||||
# Dashboard mockup verification
|
||||
|
||||
Author: Dewey. Date: 2026-09-12. Scope: five navigable HTML dashboard mockups and a comparison page under `mockups/`. These are design prototypes for Jason's selection, not product code. Every action is simulated and labelled as such in the interface.
|
||||
|
||||
## Reproduce
|
||||
|
||||
From `/mnt/storage/src/mosaic-stack/agents/dewey/work/wui`:
|
||||
|
||||
```sh
|
||||
node --check mockups/shared/app.js
|
||||
node --check mockups/shared/views.js
|
||||
node --check mockups/shared/data.js
|
||||
node checks/mockups-verify.mjs
|
||||
```
|
||||
|
||||
Environment used: Node 26.8.1, Chromium at `/usr/bin/chromium` on Linux, headless, fresh temporary profile, file URLs only. No server, package, network, Mosaic backend, or credential is used. The script writes `evidence/mockups/report.json` and the screenshots listed below, and exits non-zero on any failed check.
|
||||
|
||||
Final run: 31 checks, 0 failed, 2 not verified. Report generated 2026-09-12T12:40Z.
|
||||
|
||||
## Results
|
||||
|
||||
| Requirement | Result | Evidence |
|
||||
| --- | --- | --- |
|
||||
| Five distinct navigable designs, one comparison page | pass | `mockups/index.html` links Rail, Deck, Console, Board, Stream. Each design renders 14 routes (control board, sessions, session detail x2, projects, project detail, agents, work, runs, messages, library, settings, launch, unknown route) from one shared data set. |
|
||||
| Control board as landing view (MOSAIC-STACK-D-001) | pass as proposed design | Every design's `#/` route lists running sessions across projects with status, controller, since, config state, and a waiting-on-you section. Labelled "proposed" in the interface because the page does not exist in the foundation yet. |
|
||||
| Responsive, full width, ultrawide | pass for measured layouts | 14 routes x 8 widths (320, 390, 768, 960, 1440, 1920, 2560, 3440) per design: no script errors, no horizontal document overflow, main content present, unknown route shows the not-found page. Author inspected `*-home-390.png`, `*-home-1440.png`, `*-home-3440.png`, `d4-board-home-1200.png`, `d1-rail-session-390.png`, `d3-console-work-390.png`. |
|
||||
| Manrope, ten palettes, three appearances | pass | Manrope WOFF2 loads from `assets/fonts`. 10 palettes x 3 modes apply through Brand tokens via `?palette=&mode=` and the settings page. Sampled captures: `palette-harbor-{light,dim,dark}.png`, `palette-graphite-dim.png`, `palette-grove-dark.png`. |
|
||||
| Text and control contrast, A11Y-01/02 | pass for sampled pairs | 600 computed text/background pairs (20 selectors x 30 palette/mode sets) at or above 4.5:1 for normal text and 3:1 for large text. Lowest observed 4.50:1 (Harbor Dim muted badge and count). Badge tints were reduced from 12 to 14 percent down to 8 percent and the current-nav tint to 9 percent during this pass to reach the threshold. Not an image or gradient check and not a full audit. |
|
||||
| Loading, empty, error, success, disabled states | pass | Per design, `?state=loading|empty|error` render spinner plus skeleton, "No X yet" with a next step, and an error notice that says the view is unchanged and offers Retry and a settings link. Retry recovers to normal. Success and refusal outcomes appear as labelled toasts. Controller-dependent buttons are disabled with a reason: Observe is disabled when you already observe, Request control is disabled when you hold control. Captures: `d1-rail-state-*.png`, `d4-board-state-*.png`. |
|
||||
| Primary flows | pass as simulations | Per design: launch into a retired workspace is refused and nothing launches; launch of an already running agent/workspace reports the conflict and offers observe or request control; a valid launch shows a simulated launch notice; composer with empty text is blocked with "Nothing was sent"; a session whose base config changed shows a non-blocking notice with Check now and Plan Fresh; settings choice Grove/Dim persists through reload. Capture: `d2-deck-launch-refusal-1440.png`. |
|
||||
| Keyboard operation and focus, A11Y-05/06 | pass for tested paths | Skip link is the first Tab stop and Enter moves focus to main; focus rings are visible on links and buttons; Escape closes open menus, dialogs, and toasts. Design specific: Console Ctrl+K opens the command palette, arrow keys move through commands and through the session table, Enter opens the inspector, Escape closes it (`d3-console-inspector-1440.png`); Rail phone drawer opens from the Menu button and closes with Escape, rail collapses to icons with labels kept for assistive technology (`d1-rail-drawer-390.png`, `d1-rail-collapsed-1440.png`); Deck menu closes with Escape; Stream bottom bar exposes four tabs plus a More menu with the remaining pages (`d5-stream-more-390.png`). Not a screen-reader test. |
|
||||
| Text enlargement and long content, A11Y-07/08 | pass | At 320 px with a 200 percent root font size and with long unbroken identifiers inserted, no route overflows the viewport. Captures: `*-text-200-320.png`. Native browser zoom was not exercised. |
|
||||
| Secrets never rendered | pass | Account credentials appear only as "runtime mount, not shown"; no token-shaped strings exist in the sample data or in the rendered DOM across routes. |
|
||||
| Simulated behaviour labelled | pass | All 70 `data-action` buttons across all routes and designs produce a toast that begins with "Simulated" and states that nothing changed. Each shell carries a prototype note. |
|
||||
| Reduced motion | pass for declared rules | Shared stylesheet disables transitions and the spinner under `prefers-reduced-motion`. Behaviour does not rely on animation. |
|
||||
| Forced colours | not verified | Rules exist in `shared/app.css` for `forced-colors: active`, but headless Chromium did not emulate Windows high contrast in this harness. |
|
||||
| Screen reader announcements | not verified | Route changes and toasts update `aria-live` regions and `aria-current`; no screen reader was run. |
|
||||
| Public site completeness | not applicable | Local prototype behind file URLs. No About, Contact, privacy, consent, or HTTP 404 behaviour is in scope. |
|
||||
| Backend support | not applicable, recorded separately | FEATURES.md separates implemented, planned, and proposed capabilities. The mockups show proposed items (control board, steering queue, config drift notice, kanban work view) with a "proposed" mark; none of them assert backend support. |
|
||||
|
||||
## Fixes made during verification
|
||||
|
||||
- Sticky navigation lists in Console and Board scrolled correctly, but visually hidden labels positioned absolutely inside them escaped the scroll box and widened the page at 320 px. Made the scroll containers positioned so hidden labels stay contained.
|
||||
- Nav rows in Console and Board grew to their content width when the header wrapped at 960 px. Constrained them to the header width; Board now wraps its nav under the brand row deliberately below 1600 px.
|
||||
- Breadcrumb separators had no break opportunity, so a long crumb pushed past 320 px at 200 percent text. Breadcrumbs now wrap.
|
||||
- Kanban column headings and task cards used flex rows that could not shrink at 200 percent text. Columns now constrain children and task summaries wrap.
|
||||
- Stream bottom tabs used equal columns that could not shrink; the More menu list could open past the viewport edge. Both constrained.
|
||||
- Warning, success, and danger counters used white text on tinted backgrounds and failed contrast in Dim and Dark. They now use canvas-coloured text.
|
||||
- The contrast sampler misread `color(srgb ...)` values that Chromium reports for colour-mix backgrounds; the parser was fixed rather than the threshold.
|
||||
- Message thread avatars stretched after a min-width fix; excluded from the flex rule.
|
||||
|
||||
## Limits
|
||||
|
||||
- One desktop Chromium engine, headless. Safari, Firefox, native mobile, touch, assistive technology, forced colours, and native zoom are not verified.
|
||||
- Data is a hand-written sample (`shared/data.js`) that mirrors the foundation's records and current sessions; it is not read from `<dataRoot>`.
|
||||
- Nothing here is integrated, committed, pushed, or reviewed independently. Selection of a design is Jason's decision and no design is approved.
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
// Verification for the five WUI mockups. Run: node checks/mockups-verify.mjs
|
||||
// Writes screenshots and a JSON report under evidence/mockups/. Uses the installed Chromium over CDP; no npm.
|
||||
import { browser } from './browser.mjs';
|
||||
import { mkdir, writeFile, readFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const base = 'file://' + join(root, 'mockups') + '/';
|
||||
const out = join(root, 'evidence', 'mockups');
|
||||
await mkdir(out, { recursive: true });
|
||||
|
||||
const designs = ['d1-rail', 'd2-deck', 'd3-console', 'd4-board', 'd5-stream'];
|
||||
const routes = ['#/', '#/sessions', '#/sessions/s-01a07cfa', '#/sessions/s-01a08111', '#/projects', '#/projects/mosaic-stack', '#/agents', '#/work', '#/runs', '#/messages', '#/library', '#/settings', '#/launch', '#/does-not-exist'];
|
||||
const widths = [320, 390, 768, 960, 1440, 1920, 2560, 3440];
|
||||
const modes = ['light', 'dim', 'dark'];
|
||||
const report = { generated: new Date().toISOString(), checks: [] };
|
||||
let failures = 0;
|
||||
function record(name, status, detail = '') { report.checks.push({ name, status, detail }); if (status === 'fail') failures++; const mark = { pass: 'PASS', fail: 'FAIL', 'not verified': 'N/V ' }[status]; console.log(`${mark} ${name}${detail ? ' : ' + detail : ''}`); }
|
||||
|
||||
const b = await browser();
|
||||
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: 'window.__errs=[];window.addEventListener("error",e=>window.__errs.push(e.message+" @"+(e.filename||"").split("/").pop()+":"+e.lineno));window.addEventListener("unhandledrejection",e=>window.__errs.push("rejection: "+e.reason));' });
|
||||
const settle = ms => b.evaluate(`new Promise(r=>setTimeout(r,${ms}))`);
|
||||
const go = async hash => { await b.evaluate(`location.hash=${JSON.stringify(hash)}`); await settle(60); };
|
||||
const page = async (d, query = '') => { await b.navigate(`${base}${d}.html${query}`); await settle(60); };
|
||||
const probe = () => b.evaluate(`JSON.stringify({errs:window.__errs, sw:document.documentElement.scrollWidth, cw:document.documentElement.clientWidth, main:(document.getElementById('main')||{}).innerHTML?.length||0, title:document.title, h1:document.querySelector('#main h1')?.textContent||''})`).then(JSON.parse);
|
||||
|
||||
// 1. Every route in every design at every width: renders, no script errors, no horizontal overflow.
|
||||
for (const d of designs) {
|
||||
const problems = [];
|
||||
for (const w of widths) {
|
||||
await b.viewport(w, w < 700 ? 844 : 1000);
|
||||
await page(d);
|
||||
for (const r of routes) {
|
||||
await go(r);
|
||||
const p = await probe();
|
||||
if (p.errs.length) problems.push(`${w} ${r} errors: ${p.errs.join('; ')}`);
|
||||
if (p.sw > p.cw) problems.push(`${w} ${r} overflow ${p.sw}>${p.cw}`);
|
||||
if (p.main < 200) problems.push(`${w} ${r} empty main`);
|
||||
if (r === '#/does-not-exist' && !/not found/i.test(p.h1)) problems.push(`${w} not-found page missing`);
|
||||
if (r === '#/' && (w === 390 || w === 1440 || w === 3440)) await b.screenshot(join(out, `${d}-home-${w}.png`));
|
||||
if (w === 1440 && ['#/sessions/s-01a07cfa', '#/work', '#/launch', '#/settings'].includes(r)) await b.screenshot(join(out, `${d}-${r.slice(2).replace(/[^a-z0-9]+/gi, '-')}-1440.png`));
|
||||
if (w === 390 && r === '#/sessions/s-01a07cfa') await b.screenshot(join(out, `${d}-session-390.png`));
|
||||
}
|
||||
}
|
||||
record(`${d}: ${routes.length} routes × ${widths.length} widths render without script errors or horizontal overflow`, problems.length ? 'fail' : 'pass', problems.slice(0, 6).join(' | '));
|
||||
}
|
||||
// Index page
|
||||
{
|
||||
const problems = [];
|
||||
for (const w of widths) { await b.viewport(w, 1000); await page('index'); const p = await probe(); if (p.errs.length || p.sw > p.cw) problems.push(`${w}: ${p.errs.join(';')} ${p.sw > p.cw ? 'overflow' : ''}`); }
|
||||
await b.viewport(1440, 1000); await page('index'); await b.screenshot(join(out, 'index-1440.png'));
|
||||
await b.viewport(390, 844); await b.screenshot(join(out, 'index-390.png'));
|
||||
const links = await b.evaluate(`[...document.querySelectorAll('#designs a.primary')].map(a=>a.getAttribute('href').split('?')[0])`);
|
||||
record('index: renders at all widths and links all five designs', problems.length || designs.some(d => !links.includes(d + '.html')) ? 'fail' : 'pass', problems.join(' | ') || links.join(', '));
|
||||
}
|
||||
|
||||
// 2. Palettes and modes: every palette in every mode applies tokens, and measured text contrast on the rendered board meets 4.5:1 (3:1 for large text and UI parts).
|
||||
await b.viewport(1440, 1000);
|
||||
{
|
||||
const palettes = await (async () => { await page('d1-rail'); return b.evaluate('window.Brand.palettes.map(p=>p.id)'); })();
|
||||
const fails = [];
|
||||
const contrastScript = `(() => {
|
||||
const parse = c => { if (c.startsWith('color(')) { const m = c.match(/[\\d.]+/g).map(Number); return [m[0] * 255, m[1] * 255, m[2] * 255, m.length > 3 ? m[3] : 1]; } const m = c.match(/[\\d.]+/g).map(Number); return [m[0], m[1], m[2], m.length > 3 ? m[3] : 1]; };
|
||||
const lum = c => { const m = parse(c); const f = v => { v /= 255; return v <= .03928 ? v / 12.92 : ((v + .055) / 1.055) ** 2.4; }; return .2126 * f(m[0]) + .7152 * f(m[1]) + .0722 * f(m[2]); };
|
||||
const ratio = (a, b) => { const x = lum(a), y = lum(b); return (Math.max(x, y) + .05) / (Math.min(x, y) + .05); };
|
||||
const bgOf = el => { let e = el; while (e) { const bg = getComputedStyle(e).backgroundColor; if (bg && !bg.startsWith('rgba(0, 0, 0, 0)') && bg !== 'transparent') return bg; e = e.parentElement; } return getComputedStyle(document.documentElement).backgroundColor; };
|
||||
const blend = (fg, bg) => { const f = parse(fg), g = parse(bg); const a = f[3]; if (a >= 1) return fg; return 'rgb(' + [0,1,2].map(i => Math.round(f[i] * a + g[i] * (1 - a))).join(', ') + ')'; };
|
||||
const bgFlat = el => { let e = el, acc = null; const layers = []; while (e) { const bg = getComputedStyle(e).backgroundColor; if (bg && bg !== 'transparent' && !bg.startsWith('rgba(0, 0, 0, 0)')) layers.push(bg); e = e.parentElement; } layers.push(getComputedStyle(document.documentElement).backgroundColor); let result = layers.pop(); while (layers.length) result = blend(layers.pop(), result); return result; };
|
||||
const sel = ['#main h1', '.wait-detail', '.wait-since', '.goal-report', '.muted', '.badge-ok', '.badge-attn', '.badge-danger', '.badge-muted', '.btn.primary', '.btn:not(.primary)', '.nav-link[aria-current=page]', '.feat-implemented', '.feat-planned', '.feat-proposed', '.feat-mockup', '.cfg-changed', '.count', 'th', '.tab[aria-current=page]', '.bar-link[aria-current=page]', '.cmd-link[aria-current=page]', 'a.rail-count', '.rail-count', '.tab-dot', '.chip[aria-current=true]', '.gen', '.event time', 'code', '.stream a', '.crumbs a', '.legend-link'];
|
||||
const out = [];
|
||||
for (const s of sel) { const el = document.querySelector(s); if (!el) continue; const cs = getComputedStyle(el); const fg = cs.color; const bg = bgFlat(el); const size = parseFloat(cs.fontSize); const large = size >= 24 || (size >= 18.66 && parseInt(cs.fontWeight) >= 700); const r = ratio(fg, bg); out.push({ s, r: +r.toFixed(2), need: large ? 3 : 4.5, ok: r >= (large ? 3 : 4.5) }); }
|
||||
return out;
|
||||
})()`;
|
||||
const worst = [];
|
||||
for (const p of palettes) for (const m of modes) {
|
||||
await page('d1-rail', `?palette=${p}&mode=${m}`);
|
||||
const applied = await b.evaluate(`document.documentElement.dataset.palette+'/'+document.documentElement.dataset.mode`);
|
||||
if (applied !== `${p}/${m}`) fails.push(`${p}/${m} not applied (${applied})`);
|
||||
const rows = await b.evaluate(contrastScript);
|
||||
for (const r of rows) if (!r.ok) fails.push(`${p}/${m} ${r.s} ${r.r}:1 < ${r.need}`);
|
||||
worst.push(...rows.map(r => ({ p, m, ...r })));
|
||||
if (p === 'harbor' || (p === 'graphite' && m === 'dim') || (p === 'grove' && m === 'dark')) await b.screenshot(join(out, `palette-${p}-${m}.png`));
|
||||
}
|
||||
worst.sort((a, b) => a.r - b.r);
|
||||
report.contrastLowest = worst.slice(0, 12);
|
||||
record(`palettes × modes: ${palettes.length} palettes × 3 modes apply, and ${worst.length} sampled text/UI pairs meet WCAG contrast`, fails.length ? 'fail' : 'pass', fails.slice(0, 8).join(' | ') || `lowest ${worst[0].r}:1 (${worst[0].p}/${worst[0].m} ${worst[0].s})`);
|
||||
}
|
||||
|
||||
// 3. Demo states on every design's board: loading, empty, error render with recovery, and the error state offers retry.
|
||||
for (const d of designs) {
|
||||
const problems = [];
|
||||
for (const s of ['loading', 'empty', 'error']) {
|
||||
await page(d, `?state=${s}`);
|
||||
const t = await b.evaluate(`document.getElementById('main').innerText`);
|
||||
if (s === 'loading' && !/Loading/.test(t)) problems.push('loading text missing');
|
||||
if (s === 'empty' && !/No .* yet/.test(t)) problems.push('empty text missing');
|
||||
if (s === 'error' && !(/Could not load/.test(t) && /unchanged/.test(t) && await b.evaluate(`!!document.querySelector('[data-action=retry]')`))) problems.push('error state lacks explanation or retry');
|
||||
if (d === 'd1-rail' || d === 'd4-board') await b.screenshot(join(out, `${d}-state-${s}-1440.png`));
|
||||
}
|
||||
// Retry recovers
|
||||
await b.evaluate(`document.querySelector('[data-action=retry]').click()`); await settle(60);
|
||||
if (await b.evaluate(`window.MosaicApp.ctx.demoState`) !== 'normal') problems.push('retry did not recover');
|
||||
record(`${d}: loading, empty, and error states render and error recovers via retry`, problems.length ? 'fail' : 'pass', problems.join(' | '));
|
||||
}
|
||||
|
||||
// 4. Primary flows (simulated): launch conflict, retired-workspace refusal, successful simulated launch, empty-message guard, disabled controls on a session you already control, config-changed notice non-blocking.
|
||||
for (const d of designs) {
|
||||
const problems = [];
|
||||
await page(d); await go('#/launch');
|
||||
const submit = async (set) => b.evaluate(`(() => { const f = document.querySelector('form[data-form=launch]'); ${Object.entries(set).map(([k, v]) => `f.elements['${k}'].value = ${JSON.stringify(v)}; f.elements['${k}'].dispatchEvent(new Event('change', {bubbles:true}));`).join('')} f.requestSubmit(); return f.querySelector('.form-result').innerText; })()`);
|
||||
const conflict = await submit({ agent: 'dewey', project: 'mosaic-stack', workspace: 'wui' });
|
||||
if (!/Already running/.test(conflict) || !/Nothing was launched twice/.test(conflict)) problems.push('conflict path: ' + conflict.slice(0, 80));
|
||||
await b.evaluate(`(() => { const f = document.querySelector('form[data-form=launch]'); f.elements.project.value = 'mosaic-stack'; f.elements.project.dispatchEvent(new Event('change', {bubbles:true})); })()`);
|
||||
const refusal = await submit({ agent: 'rocko', workspace: 'archive-2025' });
|
||||
if (!/Refused/.test(refusal) || !/Nothing was launched/.test(refusal)) problems.push('refusal path: ' + refusal.slice(0, 80));
|
||||
const ok = await submit({ agent: 'sage', project: 'mosaic-stack', workspace: 'skills' });
|
||||
if (!/Simulated launch/.test(ok)) problems.push('success path: ' + ok.slice(0, 80));
|
||||
if (d === 'd2-deck') await b.screenshot(join(out, 'd2-deck-launch-refusal-1440.png'));
|
||||
await go('#/sessions/s-01a08111');
|
||||
const guard = await b.evaluate(`(() => { const f = document.querySelector('form[data-form=composer]'); f.requestSubmit(); return f.querySelector('.form-result').innerText; })()`);
|
||||
if (!/Write a message first/.test(guard)) problems.push('empty message guard: ' + guard.slice(0, 60));
|
||||
const disabled = await b.evaluate(`[...document.querySelectorAll('.head-actions button')].map(b => b.textContent.trim() + ':' + b.disabled).join(',')`);
|
||||
if (!/Observe:true/.test(disabled) || !/You control:true/.test(disabled)) problems.push('controller buttons: ' + disabled);
|
||||
await go('#/sessions/s-01a07cfa');
|
||||
const cfg = await b.evaluate(`(() => { const p = document.querySelector('.cfg-panel'); return (p.classList.contains('cfg-changed') ? 'changed ' : '') + (document.querySelector('form[data-form=composer] textarea').disabled ? 'composer-disabled' : 'composer-enabled'); })()`);
|
||||
if (cfg !== 'changed composer-enabled') problems.push('config notice: ' + cfg);
|
||||
const toast = await b.evaluate(`(() => { document.querySelector('[data-action^="config-check"]').click(); return document.querySelector('#toasts .toast')?.innerText || ''; })()`);
|
||||
if (!/Simulated/.test(toast) || !/No live effect/.test(toast)) problems.push('simulated action not labelled: ' + toast.slice(0, 60));
|
||||
// Settings: palette + mode persist through reload
|
||||
await go('#/settings');
|
||||
await b.evaluate(`(() => { const s = document.getElementById('palette-select'); s.value = 'grove'; s.dispatchEvent(new Event('change', {bubbles:true})); const r = document.querySelector('[data-theme=mode][value=dim]'); r.checked = true; r.dispatchEvent(new Event('change', {bubbles:true})); })()`);
|
||||
await page(d);
|
||||
const persisted = await b.evaluate(`document.documentElement.dataset.palette + '/' + document.documentElement.dataset.mode`);
|
||||
if (persisted !== 'grove/dim') problems.push('persistence: ' + persisted);
|
||||
await b.evaluate(`localStorage.removeItem('mosaic-wui-mockup-v1')`);
|
||||
record(`${d}: launch conflict, retired refusal, simulated launch, empty-message guard, controller-disabled buttons, non-blocking config notice, labelled simulation, appearance persistence`, problems.length ? 'fail' : 'pass', problems.join(' | '));
|
||||
}
|
||||
|
||||
// 5. Keyboard path: skip link first, Tab reaches navigation and the first waiting action, Escape closes overlays, design-specific keys.
|
||||
for (const d of designs) {
|
||||
const problems = [];
|
||||
await b.viewport(1440, 1000); await page(d);
|
||||
await b.key('Tab');
|
||||
const first = await b.evaluate(`document.activeElement.className + '|' + document.activeElement.textContent.trim()`);
|
||||
if (!/skip/.test(first)) problems.push('first tab is not the skip link: ' + first);
|
||||
await b.key('Enter'); await settle(30);
|
||||
const afterSkip = await b.evaluate(`document.activeElement.id || document.activeElement.tagName`);
|
||||
if (afterSkip !== 'main') problems.push('skip link did not focus main: ' + afterSkip);
|
||||
await b.key('Tab');
|
||||
const inMain = await b.evaluate(`document.getElementById('main').contains(document.activeElement) && (document.activeElement.tagName + ' ' + document.activeElement.textContent.trim().slice(0, 30))`);
|
||||
if (!inMain) problems.push('tab after skip did not enter main content');
|
||||
// Every interactive element in the board has a visible focus style (outline) when focused
|
||||
const focusOk = await b.evaluate(`(() => { const els = [...document.querySelectorAll('#main a, #main button, #main select, #main textarea')].slice(0, 40); let bad = 0; for (const el of els) { el.focus(); const cs = getComputedStyle(el); if (!document.activeElement.isSameNode(el)) continue; if (!cs.outlineStyle || cs.outlineStyle === 'none' || parseFloat(cs.outlineWidth) < 2) { if (!el.closest('.segmented')) bad++; } } return bad; })()`);
|
||||
// Chromium computed outline on :focus-visible needs real keyboard focus; if programmatic focus reports none, verify via matches
|
||||
if (focusOk > 0) { const visible = await b.evaluate(`(() => { const el = document.querySelector('#main a'); el.focus(); return el.matches(':focus-visible') ? getComputedStyle(el).outlineWidth : 'no-focus-visible'; })()`); if (visible === 'no-focus-visible') problems.push(`focus ring: ${focusOk} elements without outline (programmatic focus)`); }
|
||||
if (d === 'd3-console') {
|
||||
await b.key('k', 'KeyK', 2); await settle(50);
|
||||
const open = await b.evaluate(`document.getElementById('palette').open && document.activeElement.id`);
|
||||
if (open !== 'cmd-input') problems.push('Ctrl+K did not open the command palette');
|
||||
await b.evaluate(`(() => { const i = document.getElementById('cmd-input'); i.value = 'sess'; i.dispatchEvent(new Event('input', {bubbles:true})); })()`);
|
||||
await b.key('ArrowDown'); await b.key('Enter'); await settle(80);
|
||||
const dest = await b.evaluate(`location.hash`);
|
||||
if (!/sessions/.test(dest)) problems.push('palette Enter did not navigate: ' + dest);
|
||||
await go('#/');
|
||||
await b.evaluate(`document.querySelector('table.sessions tbody tr').focus()`);
|
||||
await b.key('ArrowDown'); await b.key('Enter'); await settle(50);
|
||||
const insp = await b.evaluate(`!document.getElementById('inspector').hidden && document.querySelector('table.sessions tbody tr[aria-selected=true]')?.dataset.session`);
|
||||
if (insp !== 's-01a07cfa') problems.push('arrow + enter did not inspect second row: ' + insp);
|
||||
await b.key('Escape'); await settle(30);
|
||||
await b.screenshot(join(out, 'd3-console-inspector-1440.png'));
|
||||
}
|
||||
if (d === 'd1-rail') {
|
||||
await b.viewport(390, 844); await page(d);
|
||||
await b.evaluate(`document.getElementById('menu-btn').click()`); await settle(250);
|
||||
const opened = await b.evaluate(`document.body.classList.contains('rail-open') && document.getElementById('rail').contains(document.activeElement)`);
|
||||
if (!opened) problems.push('phone menu did not open or focus the rail');
|
||||
await b.screenshot(join(out, 'd1-rail-drawer-390.png'));
|
||||
await b.key('Escape'); await settle(250);
|
||||
if (await b.evaluate(`document.body.classList.contains('rail-open')`)) problems.push('Escape did not close the drawer');
|
||||
await b.viewport(1440, 1000); await page(d);
|
||||
await b.evaluate(`document.getElementById('rail-toggle').click()`); await settle(30);
|
||||
const collapsed = await b.evaluate(`document.body.classList.contains('rail-collapsed') && document.querySelector('.rail .nav-link').textContent.trim()`);
|
||||
if (!collapsed) problems.push('rail collapse lost accessible names');
|
||||
await b.screenshot(join(out, 'd1-rail-collapsed-1440.png'));
|
||||
await b.evaluate(`document.getElementById('rail-toggle').click(); localStorage.removeItem('mosaic-wui-mockup-v1')`);
|
||||
}
|
||||
if (d === 'd2-deck') {
|
||||
await b.evaluate(`document.querySelector('details.menu').open = true`); await settle(30);
|
||||
await b.key('Escape'); await settle(30);
|
||||
if (await b.evaluate(`document.querySelector('details.menu').open`)) problems.push('Escape did not close the appearance menu');
|
||||
}
|
||||
if (d === 'd5-stream') {
|
||||
await b.viewport(390, 844); await page(d);
|
||||
const tabs = await b.evaluate(`[...document.querySelectorAll('.tabbar > ul > li > a')].length + '/' + getComputedStyle(document.querySelector('.tabbar')).display`);
|
||||
if (!/^4\/block/.test(tabs)) problems.push('phone tab bar: ' + tabs);
|
||||
await b.evaluate(`document.querySelector('details.more').open = true`); await settle(30);
|
||||
const moreLinks = await b.evaluate(`[...document.querySelectorAll('.more-list a')].map(a => a.getAttribute('href')).join(',')`);
|
||||
for (const need of ['#/projects', '#/agents', '#/runs', '#/library', '#/settings', '#/launch']) if (!moreLinks.includes(need)) problems.push('More menu missing ' + need);
|
||||
await b.screenshot(join(out, 'd5-stream-more-390.png'));
|
||||
await b.key('Escape'); await settle(30);
|
||||
await b.viewport(1440, 1000);
|
||||
}
|
||||
record(`${d}: keyboard path (skip link, main focus, focus rings, Escape, design-specific keys)`, problems.length ? 'fail' : 'pass', problems.join(' | '));
|
||||
}
|
||||
|
||||
// 6. Long content and text scaling: a 400-character goal and 200% zoom at 320 px do not overflow.
|
||||
for (const d of designs) {
|
||||
const problems = [];
|
||||
await b.viewport(320, 700); await page(d);
|
||||
await b.evaluate(`(() => { const D = window.MosaicData; D.sessions[1].goal.text = 'Reconcile the queue steering plan with MOSAIC-STACK-D-001 and produce a written comparison of every affected route, including the sessions list, the session view, and the launch form, then summarise what changes for observers versus controllers'.repeat(2); D.sessions[1].last = 'https://example.invalid/a/very/long/unbroken/path/segment/that/should/wrap/rather/than/overflow/the/column/0123456789'; window.MosaicApp.render(); })()`);
|
||||
for (const r of ['#/', '#/sessions', '#/sessions/s-01a07cfa', '#/work']) { await go(r); const p = await probe(); if (p.sw > p.cw) problems.push(`${r} overflow at 320 with long text`); }
|
||||
await b.evaluate(`document.documentElement.style.fontSize = '32px'`); await settle(30);
|
||||
for (const r of ['#/', '#/sessions/s-01a07cfa', '#/settings']) { await go(r); const p = await probe(); if (p.sw > p.cw) problems.push(`${r} overflow at 320 with 200% text`); }
|
||||
await go('#/'); await b.screenshot(join(out, `${d}-text-200-320.png`));
|
||||
record(`${d}: long unbroken content and 200% text at 320 px stay within the viewport`, problems.length ? 'fail' : 'pass', problems.join(' | '));
|
||||
}
|
||||
|
||||
// 7. Secrets and simulation labelling: no credential-shaped values anywhere in the rendered accounts table; every data-action button produces a labelled simulated toast.
|
||||
{
|
||||
const problems = [];
|
||||
await b.viewport(1440, 1000); await page('d1-rail'); await go('#/settings');
|
||||
const text = await b.evaluate(`document.getElementById('main').innerText`);
|
||||
if (/sk-[a-z0-9]{8,}|Bearer |-----BEGIN/i.test(text)) problems.push('credential-shaped text on settings page');
|
||||
if (!/not shown/.test(text)) problems.push('accounts table does not state credentials are not shown');
|
||||
const count = await b.evaluate(`(() => { let n = 0, bad = []; for (const r of ['#/', '#/sessions/s-01a07cfa', '#/projects/mosaic-stack', '#/agents', '#/work', '#/runs', '#/library', '#/settings']) { location.hash = r; window.MosaicApp.render(); for (const btn of document.querySelectorAll('#main [data-action]:not(:disabled)')) { if (btn.dataset.action.startsWith('observe') || btn.dataset.action.startsWith('cancel') || btn.dataset.action === 'retry') continue; document.querySelector('#toasts').replaceChildren(); btn.click(); const t = document.querySelector('#toasts .toast')?.innerText || ''; n++; if (!/Simulated/.test(t)) bad.push(r + ' ' + btn.dataset.action); } } return JSON.stringify({ n, bad }); })()`).then(JSON.parse);
|
||||
if (count.bad.length) problems.push('unlabelled actions: ' + count.bad.slice(0, 5).join(', '));
|
||||
record(`secrets never rendered; ${count.n} action buttons across routes produce a labelled simulated notice`, problems.length ? 'fail' : 'pass', problems.join(' | '));
|
||||
}
|
||||
|
||||
// 8. Reduced motion and forced colors media queries exist in the shared stylesheet (static check; visual verification of forced colors not available headless).
|
||||
{
|
||||
const css = await readFile(join(root, 'mockups', 'shared', 'app.css'), 'utf8');
|
||||
const hasRM = /prefers-reduced-motion/.test(css), hasFC = /forced-colors/.test(css);
|
||||
record('shared stylesheet declares reduced-motion and forced-colors rules', hasRM && hasFC ? 'pass' : 'fail', `reduced-motion ${hasRM}, forced-colors ${hasFC}`);
|
||||
record('forced-colors rendering', 'not verified', 'headless Chromium does not emulate Windows high contrast here; rules exist but were not visually checked');
|
||||
record('screen reader announcement behaviour', 'not verified', 'aria-live regions are present and updated on route change; no screen reader was run');
|
||||
}
|
||||
|
||||
await b.close();
|
||||
report.summary = { total: report.checks.length, failed: failures, notVerified: report.checks.filter(c => c.status === 'not verified').length };
|
||||
await writeFile(join(out, 'report.json'), JSON.stringify(report, null, 2));
|
||||
console.log(`\n${report.summary.total} checks, ${failures} failed, ${report.summary.notVerified} not verified. Report: evidence/mockups/report.json`);
|
||||
process.exit(failures ? 1 : 0);
|
||||
@@ -0,0 +1,430 @@
|
||||
// Verification for the slice 1 S5 mockup (mockups/slice1/). Run: node checks/slice1-verify.mjs
|
||||
// Writes screenshots and a JSON report under evidence/slice1/. Uses the installed Chromium over CDP; no npm.
|
||||
import { browser } from './browser.mjs';
|
||||
import { mkdir, writeFile, readFile, mkdtemp, rm } from 'node:fs/promises';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
|
||||
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const page = 'file://' + join(root, 'mockups', 'slice1', 'index.html');
|
||||
const out = join(root, 'evidence', 'slice1');
|
||||
await mkdir(out, { recursive: true });
|
||||
|
||||
const id = n => `0d1a${String(n).padStart(4, '0')}-5e7c-4b1a-9c3e-1f2a3b4c5d6e`;
|
||||
const D = { launch: id(1), prd: id(2), prdOld: id(3), conflict: id(4), scope: id(5), merged: id(6) }; // as in mockups/slice1/data.js
|
||||
const ref = n => `vikunja%3A3%2F${n}`;
|
||||
const routes = ['#/', '#/inbox', '#/inbox?view=roles', '#/inbox?view=recent', `#/inbox/${D.launch}`, `#/inbox/${D.prd}`, `#/inbox/${D.merged}`, '#/tasks', `#/tasks/${ref(41)}`, `#/tasks/${ref(47)}`, `#/tasks/${ref(48)}`, '#/agents', `#/trail/task/${ref(41)}`, `#/trail/task/${ref(44)}?kind=decision`, `#/trail/decision/${D.prd}`, '#/does-not-exist', `#/inbox/${id(99)}`];
|
||||
const widths = [320, 390, 768, 960, 1440, 1920, 2560, 3440];
|
||||
const modes = ['light', 'dim', 'dark'];
|
||||
const report = { generated: new Date().toISOString(), checks: [] };
|
||||
let failures = 0;
|
||||
function record(name, status, detail = '') { report.checks.push({ name, status, detail }); if (status === 'fail') failures++; const mark = { pass: 'PASS', fail: 'FAIL', 'not verified': 'N/V ' }[status]; console.log(`${mark} ${name}${detail ? ' : ' + detail : ''}`); }
|
||||
const check = (name, problems, ok = '') => record(name, problems.length ? 'fail' : 'pass', problems.slice(0, 8).join(' | ') || ok);
|
||||
|
||||
const b = await browser();
|
||||
// Count every way a page could send something. The mockup must never call any of them.
|
||||
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: `window.__errs=[];window.__net=[];
|
||||
window.addEventListener("error",e=>window.__errs.push(e.message+" @"+(e.filename||"").split("/").pop()+":"+e.lineno));
|
||||
window.addEventListener("unhandledrejection",e=>window.__errs.push("rejection: "+e.reason));
|
||||
const f=window.fetch;window.fetch=(...a)=>{window.__net.push("fetch "+a[0]);return f(...a)};
|
||||
const o=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(...a){window.__net.push("xhr "+a[1]);return o.apply(this,a)};
|
||||
navigator.sendBeacon=(...a)=>{window.__net.push("beacon "+a[0]);return false};
|
||||
const W=window.WebSocket;window.WebSocket=function(u){window.__net.push("ws "+u);return new W(u)};
|
||||
window.__clip=[];` });
|
||||
const settle = ms => b.evaluate(`new Promise(r=>setTimeout(r,${ms}))`);
|
||||
const go = async hash => { await b.evaluate(`location.hash=${JSON.stringify(hash)}`); await settle(50); };
|
||||
const load = async (query = '') => { await b.navigate(page + query); await settle(60); };
|
||||
const probe = () => b.evaluate(`JSON.stringify({errs:window.__errs, net:window.__net, sw:document.documentElement.scrollWidth, cw:document.documentElement.clientWidth, main:document.getElementById('main').innerText.length, h1:document.querySelector('#main h1')?.textContent||''})`).then(JSON.parse);
|
||||
const text = () => b.evaluate(`document.getElementById('main').innerText`);
|
||||
const shot = name => b.screenshot(join(out, name + '.png'));
|
||||
|
||||
// 1. Every route at every width: renders, no script errors, no horizontal overflow, nothing sent.
|
||||
{
|
||||
const problems = [];
|
||||
for (const w of widths) {
|
||||
await b.viewport(w, w < 700 ? 844 : 1000); await load();
|
||||
for (const r of routes) {
|
||||
await go(r); const p = await probe();
|
||||
if (p.errs.length) problems.push(`${w} ${r} errors: ${p.errs.join('; ')}`);
|
||||
if (p.net.length) problems.push(`${w} ${r} sent: ${p.net.join('; ')}`);
|
||||
if (p.sw > p.cw) problems.push(`${w} ${r} overflow ${p.sw}>${p.cw}`);
|
||||
if (p.main < 40) problems.push(`${w} ${r} empty main`);
|
||||
if ((r === '#/does-not-exist' || r.endsWith(id(99))) && !/not found/i.test(p.h1)) problems.push(`${w} ${r} not-found page missing`);
|
||||
const name = r.slice(2).replace(/[^a-z0-9]+/gi, '-').replace(/-+$/, '') || 'board';
|
||||
if (w === 1440 && !r.includes('does-not') && !r.endsWith(id(99))) await shot(`${name.slice(0, 60)}-1440`);
|
||||
if (w === 390 && ['#/inbox', `#/inbox/${D.launch}`, '#/tasks', '#/agents', `#/trail/task/${ref(41)}`].includes(r)) await shot(`${name.slice(0, 60)}-390`);
|
||||
}
|
||||
}
|
||||
check(`${routes.length} routes × ${widths.length} widths render without script errors, network calls or horizontal overflow`, problems);
|
||||
}
|
||||
|
||||
// 2. Palettes × modes: tokens apply and sampled text/UI pairs meet WCAG contrast on each view.
|
||||
await b.viewport(1440, 1000);
|
||||
{
|
||||
await load();
|
||||
const palettes = await b.evaluate('window.Brand.palettes.map(p=>p.id)');
|
||||
const contrast = `(() => {
|
||||
const parse = c => { const m = c.match(/[\\d.]+/g).map(Number); return c.startsWith('color(') ? [m[0] * 255, m[1] * 255, m[2] * 255, m.length > 3 ? m[3] : 1] : [m[0], m[1], m[2], m.length > 3 ? m[3] : 1]; };
|
||||
const lum = c => { const m = parse(c); const f = v => { v /= 255; return v <= .03928 ? v / 12.92 : ((v + .055) / 1.055) ** 2.4; }; return .2126 * f(m[0]) + .7152 * f(m[1]) + .0722 * f(m[2]); };
|
||||
const ratio = (a, b) => { const x = lum(a), y = lum(b); return (Math.max(x, y) + .05) / (Math.min(x, y) + .05); };
|
||||
const blend = (fg, bg) => { const f = parse(fg), g = parse(bg); const a = f[3]; if (a >= 1) return fg; return 'rgb(' + [0,1,2].map(i => Math.round(f[i] * a + g[i] * (1 - a))).join(', ') + ')'; };
|
||||
const bgFlat = el => { let e = el; const layers = []; while (e) { const bg = getComputedStyle(e).backgroundColor; if (bg && bg !== 'transparent' && !bg.startsWith('rgba(0, 0, 0, 0)')) layers.push(bg); e = e.parentElement; } layers.push(getComputedStyle(document.documentElement).backgroundColor); let r = layers.pop(); while (layers.length) r = blend(layers.pop(), r); return r; };
|
||||
const sel = ['#main h1', '#status', '.muted', '.source', '.badge-ok', '.badge-attn', '.badge-danger', '.badge-muted', '.btn', '.chip[aria-current=true]', '.chip:not([aria-current])', '.count', 'th', 'code', '.s1-dec-open', '.s1-section[aria-current=page]', '.s1-section:not([aria-current])', '.s1-tkind', '.s1-twhen', '.feat', '#main a', '.s1-key', '.tree-title'];
|
||||
const out = [];
|
||||
for (const s of sel) { const el = document.querySelector(s); if (!el) continue; const cs = getComputedStyle(el); const size = parseFloat(cs.fontSize); const large = size >= 24 || (size >= 18.66 && parseInt(cs.fontWeight) >= 700); const r = ratio(cs.color, bgFlat(el)); out.push({ s, r: +r.toFixed(2), need: large ? 3 : 4.5, ok: r >= (large ? 3 : 4.5) }); }
|
||||
return out;
|
||||
})()`;
|
||||
const views = ['#/inbox', `#/inbox/${D.prd}`, '#/tasks', '#/agents', `#/trail/task/${ref(41)}`];
|
||||
const fails = [], all = [];
|
||||
for (const p of palettes) for (const m of modes) {
|
||||
await load(`?palette=${p}&mode=${m}`);
|
||||
const applied = await b.evaluate(`document.documentElement.dataset.palette+'/'+document.documentElement.dataset.mode`);
|
||||
if (applied !== `${p}/${m}`) fails.push(`${p}/${m} not applied (${applied})`);
|
||||
for (const v of views) {
|
||||
await go(v);
|
||||
for (const r of await b.evaluate(contrast)) { all.push({ p, m, v, ...r }); if (!r.ok) fails.push(`${p}/${m} ${v} ${r.s} ${r.r}:1 < ${r.need}`); }
|
||||
}
|
||||
if ((p === 'harbor' && m !== 'light') || (p === 'grove' && m === 'dark')) { await go('#/inbox'); await shot(`palette-${p}-${m}-inbox`); }
|
||||
}
|
||||
all.sort((a, b) => a.r - b.r); report.contrastLowest = all.slice(0, 12);
|
||||
check(`${palettes.length} palettes × 3 modes apply; ${all.length} sampled text/UI pairs across 5 views meet WCAG contrast`, fails, `lowest ${all[0].r}:1 (${all[0].p}/${all[0].m} ${all[0].v} ${all[0].s})`);
|
||||
}
|
||||
|
||||
// 3. Agent-authored text is inert: no element or handler from it, controls and bidi marks visible, Markdown as source.
|
||||
{
|
||||
const problems = [];
|
||||
await load();
|
||||
for (const r of ['#/inbox', `#/inbox/${D.prd}`, `#/trail/decision/${D.prd}`]) {
|
||||
await go(r);
|
||||
const st = await b.evaluate(`JSON.stringify({ img: document.querySelectorAll('#main img').length, script: document.querySelectorAll('#main script').length, pwned: window.__pwned || 0, onerr: !!document.querySelector('#main [onerror]') })`).then(JSON.parse);
|
||||
if (st.img || st.script || st.pwned || st.onerr) problems.push(`${r} created markup from text: ${JSON.stringify(st)}`);
|
||||
}
|
||||
await go(`#/inbox/${D.prd}`);
|
||||
const t = await text();
|
||||
for (const need of ['<img src=x onerror="window.__pwned=1">', '<script>window.__pwned=2</script>', '[U+202E]exe.txt[U+202C]', '␇', '**"every 30 seconds"**']) if (!t.includes(need)) problems.push(`decision text lacks ${JSON.stringify(need)}`);
|
||||
if (/|\u0007/.test(t)) problems.push('raw bidi or bell character reached the page');
|
||||
await go('#/tasks');
|
||||
if (!(await text()).includes('#43 `mosaic inbox` and `mosaic decide`')) problems.push('task title Markdown not shown as source');
|
||||
check('agent- and tracker-authored text renders inert (no markup, visible controls and bidi marks, Markdown as source)', problems);
|
||||
}
|
||||
|
||||
// 4. Read-only: no form or text field, the only buttons are Copy and Read again, Copy writes only the
|
||||
// clipboard with the exact command, and nothing is sent anywhere.
|
||||
{
|
||||
const problems = [];
|
||||
await load();
|
||||
await b.evaluate(`navigator.clipboard.writeText = t => { window.__clip.push(t); return Promise.resolve(); }`);
|
||||
let copies = 0;
|
||||
for (const r of routes) {
|
||||
await go(r);
|
||||
const st = await b.evaluate(`JSON.stringify({ forms: document.querySelectorAll('form').length, fields: document.querySelectorAll('#main input, #main textarea, #main select').length, buttons: [...document.querySelectorAll('#main button')].filter(x => !x.hasAttribute('data-copy') && !x.hasAttribute('data-retry')).map(x => x.textContent), post: document.documentElement.outerHTML.match(/method=.?post/i) ? 1 : 0 })`).then(JSON.parse);
|
||||
if (st.forms || st.fields || st.buttons.length || st.post) problems.push(`${r}: ${JSON.stringify(st)}`);
|
||||
}
|
||||
for (const d of [D.launch, D.prd]) {
|
||||
await go(`#/inbox/${d}`);
|
||||
const cmds = await b.evaluate(`[...document.querySelectorAll('[data-copy]')].map(x => x.dataset.copy)`);
|
||||
const keys = await b.evaluate(`window.S1App.reads().decision(${JSON.stringify(d)}).options.map(o => o.key)`);
|
||||
if (JSON.stringify(cmds) !== JSON.stringify(keys.map(k => `mosaic decide ${d} ${k}`))) problems.push(`${d} commands ${JSON.stringify(cmds)}`);
|
||||
await b.evaluate(`window.__clip = []; [...document.querySelectorAll('[data-copy]')].forEach(x => x.click())`); await settle(30);
|
||||
const clip = await b.evaluate('window.__clip');
|
||||
if (JSON.stringify(clip) !== JSON.stringify(cmds)) problems.push(`${d} clipboard got ${JSON.stringify(clip)}`);
|
||||
copies += clip.length;
|
||||
if (!/Copied: mosaic decide/.test(await b.evaluate(`document.getElementById('copy-status').textContent`))) problems.push('copy status not shown');
|
||||
}
|
||||
// Clipboard refused: the command is selected instead, and the page says so.
|
||||
await b.evaluate(`navigator.clipboard.writeText = () => Promise.reject(new Error('denied'))`);
|
||||
await b.evaluate(`document.querySelector('[data-copy]').click()`); await settle(30);
|
||||
const fb = await b.evaluate(`JSON.stringify({ sel: String(getSelection()), msg: document.getElementById('copy-status').textContent })`).then(JSON.parse);
|
||||
if (!fb.sel.startsWith('mosaic decide ') || !/press Ctrl\+C/.test(fb.msg)) problems.push('clipboard fallback: ' + JSON.stringify(fb));
|
||||
// Closed decisions show the outcome, no commands.
|
||||
for (const d of [D.merged, D.prdOld]) { await go(`#/inbox/${d}`); if (await b.evaluate(`document.querySelectorAll('[data-copy]').length`)) problems.push(`closed decision ${d} still offers commands`); }
|
||||
const net = await b.evaluate('window.__net');
|
||||
if (net.length) problems.push('sent: ' + net.join('; '));
|
||||
const src = (await readFile(join(root, 'mockups', 'slice1', 'app.js'), 'utf8')) + (await readFile(join(root, 'mockups', 'slice1', 'index.html'), 'utf8'));
|
||||
for (const bad of ['fetch(', 'XMLHttpRequest', 'sendBeacon', 'WebSocket', '<form', 'localStorage.setItem']) if (src.includes(bad)) problems.push(`source contains ${bad}`);
|
||||
check(`read-only: no forms, fields or write buttons on ${routes.length} routes; ${copies} Copy clicks wrote only the exact command to the clipboard; fallback selects it; closed decisions offer none; nothing sent`, problems);
|
||||
}
|
||||
|
||||
// 5. Inbox rules from the data model: open human-routed first by blocking then oldest; closed ones only in Recent;
|
||||
// cross-role ones grouped by arbiter; the supersedes chain and DM state show; the approval context the broker
|
||||
// recorded at decision.raise shows on cards and options, never inferred from the recommendation.
|
||||
{
|
||||
const problems = [];
|
||||
await load(); await go('#/inbox');
|
||||
const order = await b.evaluate(`[...document.querySelectorAll('.s1-dec-open')].map(a => decodeURIComponent(a.getAttribute('href').split('/').pop()))`);
|
||||
if (JSON.stringify(order) !== JSON.stringify([D.launch, D.prd])) problems.push('For you order ' + JSON.stringify(order));
|
||||
if (await b.evaluate(`!document.querySelector('.s1-dec.is-attn .badge-attn') || !/DM sent/.test(document.querySelector('.s1-dec.is-attn').innerText)`)) problems.push('blocking card lacks badge or DM state');
|
||||
const nav = await b.evaluate(`document.querySelector('.s1-section[aria-current=page]').innerText`);
|
||||
if (!/Inbox\s*2/.test(nav) || !/1 blocking/.test(nav)) problems.push('section count: ' + nav);
|
||||
await go('#/inbox?view=roles');
|
||||
const roles = await b.evaluate(`[...document.querySelectorAll('#main h2')].map(h => h.textContent.replace(/\\s+/g, ' ').trim())`);
|
||||
if (JSON.stringify(roles) !== JSON.stringify(['For pm 1', 'For cto 1'])) problems.push('roles groups ' + JSON.stringify(roles));
|
||||
await go('#/inbox?view=recent');
|
||||
const recent = await text();
|
||||
if (!/chose “yes”/.test(recent) || !/withdrawn/.test(recent)) problems.push('Recent lacks the resolved or the withdrawn decision');
|
||||
await go(`#/inbox/${D.prd}`);
|
||||
const det = await text();
|
||||
if (!/Earlier versions/.test(det) || !det.includes(D.prdOld.slice(0, 8)) || !/withdrawn/.test(det)) problems.push('supersedes chain missing');
|
||||
if (!/not needed \(not blocking\)/.test(det)) problems.push('DM line for non-blocking decision');
|
||||
await go('#/inbox');
|
||||
const auth = await b.evaluate(`[...document.querySelectorAll('.s1-dec .s1-auth')].map(p => p.textContent)`);
|
||||
if (JSON.stringify(auth) !== JSON.stringify(['Choosing “yes” approves role.launch on coder.', 'Choosing “a” approves prd.approve on REQ-TASK-2.'])) problems.push('card approval context ' + JSON.stringify(auth));
|
||||
// #48's conflict: the recommendation is "queue", the recorded approvalChoice is "vikunja".
|
||||
await go(`#/inbox/${D.conflict}`);
|
||||
const opts = await b.evaluate(`JSON.stringify([...document.querySelectorAll('.s1-opt')].map(o => [o.querySelector('.s1-key').textContent, [...o.querySelectorAll('.badge')].map(x => x.textContent)]))`).then(JSON.parse);
|
||||
if (JSON.stringify(opts) !== JSON.stringify([['queue', ['recommended']], ['vikunja', ['approves task.close']]])) problems.push('approval badge does not follow the recorded approvalChoice: ' + JSON.stringify(opts));
|
||||
if (!/Choosing “vikunja” approves task\.close on vikunja:3\/48\./.test(await text())) problems.push('decision page lacks the approval line');
|
||||
for (const v of ['roles', 'recent']) { await go(`#/inbox?view=${v}`); if (!/not in the Q1 module/.test(await text())) problems.push(`${v} view does not say its source is outside the Q1 module`); }
|
||||
await go(`#/inbox/${D.launch}`);
|
||||
if (!/Seen by jason via cli/.test(await text())) problems.push('seen history missing');
|
||||
check('inbox: blocking then oldest, closed only in Recent, roles grouped by arbiter, DM state, seen history, supersedes chain, recorded approval context on cards and options, non-Q1 views labelled', problems);
|
||||
}
|
||||
|
||||
// 6. Tasks: bucket groups from the latest snapshot; markers for external change, never-self, conflict and missing; freshness line.
|
||||
{
|
||||
const problems = [];
|
||||
await load(); await go('#/tasks');
|
||||
const groups = await b.evaluate(`JSON.stringify(window.S1App.reads().tasks().groups.map(g => [g.bucket, g.rows.map(r => r.index)]))`).then(JSON.parse);
|
||||
const want = [['todo', [46]], ['in-progress', [42, 43]], ['in-review', [41]], ['blocked', [44]], ['done', [48, 45]]];
|
||||
if (JSON.stringify(groups) !== JSON.stringify(want)) problems.push('groups ' + JSON.stringify(groups));
|
||||
const t = await text();
|
||||
for (const need of ['not created by the stack', 'changed in Vikunja: bucket', 'missing: not-found', 'not built yet']) if (!t.includes(need)) problems.push('missing marker text: ' + need);
|
||||
if (await b.evaluate(`document.querySelector('details.s1-done').open`)) problems.push('done group open by default');
|
||||
await b.evaluate(`document.querySelector('details.s1-done').open = true`);
|
||||
const conflict = await b.evaluate(`document.querySelector('a.s1-marker')?.getAttribute('href')`);
|
||||
if (conflict !== `#/inbox/${D.conflict}`) problems.push('conflict marker link ' + conflict);
|
||||
// #44 has only self snapshots: no marker. #42's latest row is a stale board read: not external (the v3
|
||||
// read_at rule), and not shown either, so #42 stays in in-progress (checked by the groups above).
|
||||
for (const n of [44, 42]) { const x = await b.evaluate(`JSON.stringify(window.S1App.reads().task('vikunja:3/${n}').external)`); if (x !== 'null') problems.push(`#${n} marked external: ${x}`); }
|
||||
await go(`#/tasks/${ref(42)}`);
|
||||
if (!/stale read, not shown/.test(await b.evaluate(`document.querySelector('.s1-snaps').textContent`))) problems.push('#42 snapshot list does not label the stale read');
|
||||
await go('#/tasks');
|
||||
await go(`#/tasks/${ref(48)}`);
|
||||
const t48 = await text();
|
||||
if (!/queue says in-review, Vikunja says done/.test(t48)) problems.push('#48 detail lacks conflict');
|
||||
if (await b.evaluate(`document.querySelector('a[target=_blank]')?.getAttribute('href')`) !== 'http://127.0.0.1:3456/tasks/48') problems.push('Open in Vikunja link');
|
||||
check('tasks: groups by bucket from the latest snapshot, markers (changed in Vikunja, not created by the stack, conflict, missing), done collapsed, freshness line, Vikunja link', problems);
|
||||
}
|
||||
|
||||
// 7. Agents: limits, launch state, holders, advisory T3 claims, released role, credential notices without values.
|
||||
{
|
||||
const problems = [];
|
||||
await load(); await go('#/agents');
|
||||
const t = await text();
|
||||
for (const need of ['Opus 2 of 4, Sonnet 1 of 4', 'launching allowed', 'advisory', 'expires 2026-10-13', 'pm may launch coder and reviewer', 'not in the Q1 module']) if (!t.includes(need)) problems.push('missing: ' + need);
|
||||
// The module returns current claims only, so the released reviewer shows as not held.
|
||||
const rev = await b.evaluate(`[...document.querySelectorAll('.s1-agents tbody tr')].find(r => r.querySelector('th').firstChild.textContent === 'reviewer')?.innerText || ''`);
|
||||
if (!/not held/.test(rev)) problems.push('released reviewer: ' + rev);
|
||||
const advisory = await b.evaluate(`[...document.querySelectorAll('.s1-agents tbody tr')].filter(r => r.querySelector('.badge-muted')?.textContent === 'advisory').map(r => r.querySelector('th').firstChild.textContent)`);
|
||||
if (JSON.stringify(advisory) !== '["cto"]') problems.push('advisory on ' + JSON.stringify(advisory));
|
||||
if (/\.token|secrets\/|Bearer |-----BEGIN|[A-Za-z0-9]{32,}/.test(t)) problems.push('credential-shaped text on the Agents page');
|
||||
check('agents: live counts against limits, launch state, holders, advisory only for the T3 claim, released role not held, credential dates without values or paths, non-Q1 sources labelled', problems);
|
||||
}
|
||||
|
||||
// 8. Trail: oldest first, filters partition the rows, the decision trail includes earlier versions, the Q3 request join labelled.
|
||||
{
|
||||
const problems = [];
|
||||
await load(); await go(`#/trail/task/${ref(41)}`);
|
||||
const times = await b.evaluate(`[...document.querySelectorAll('.s1-trail time')].map(t => t.getAttribute('datetime'))`);
|
||||
if (times.join() !== [...times].sort().join()) problems.push('not oldest first');
|
||||
const counts = await b.evaluate(`[...document.querySelectorAll('#main .chips .chip')].map(c => +c.querySelector('.count').textContent)`);
|
||||
if (counts[0] !== counts.slice(1).reduce((a, c) => a + c, 0) + await b.evaluate(`window.S1App.reads().trail('task','vikunja:3/41').filter(r => r.group === 'other').length`)) problems.push('filter counts do not add up: ' + counts);
|
||||
const kinds = await b.evaluate(`[...document.querySelectorAll('.s1-tkind')].map(k => k.textContent)`);
|
||||
for (const k of ['human.input', 'task.created', 'decision.raised', 'decision.resolved', 'session.launched', 'review.verdict']) if (!kinds.includes(k)) problems.push('task trail lacks ' + k);
|
||||
if (!/the request task\.created names/.test(await text())) problems.push('Q3 request join not labelled');
|
||||
// The request has no task subject; task.created names it (schema v3a onward). Only that request joins.
|
||||
const t41 = await text();
|
||||
if (!t41.includes('Build the bus store next.')) problems.push('#41 trail lacks the request its task.created names');
|
||||
if (t41.includes('Write the identity runbook first.')) problems.push('#41 trail shows another task\'s request');
|
||||
await go(`#/trail/task/${ref(43)}`);
|
||||
if (!(await text()).includes('Then the read verbs')) problems.push('#43 trail lacks its shared request');
|
||||
await go(`#/trail/task/${ref(41)}?kind=launch`);
|
||||
const launchKinds = await b.evaluate(`[...new Set([...document.querySelectorAll('.s1-tkind')].map(k => k.textContent))]`);
|
||||
if (launchKinds.some(k => !k.startsWith('session.') && !k.startsWith('launch.') && !k.startsWith('claim.'))) problems.push('launch filter shows ' + launchKinds);
|
||||
await go(`#/trail/decision/${D.prd}`);
|
||||
if (!/decision\.withdrawn/.test(await text())) problems.push('decision trail lacks the withdrawn earlier version');
|
||||
// The decision trail merges one read per version, so it sorts again; check that order too.
|
||||
const dTimes = await b.evaluate(`[...document.querySelectorAll('.s1-trail time')].map(t => t.getAttribute('datetime'))`);
|
||||
if (dTimes.length < 2 || dTimes.join() !== [...dTimes].sort().join()) problems.push('decision trail not oldest first');
|
||||
check('trail: oldest first, filters partition rows, task trail spans request to verdict, decision trail includes earlier versions, Q3 request join labelled', problems);
|
||||
}
|
||||
|
||||
// 9. States: loading, empty, failed read (keeps data, warns, recovers), broker refusal (shows no data).
|
||||
{
|
||||
const problems = [];
|
||||
await load('?state=loading'); if (!/Reading from the broker/.test(await text())) problems.push('loading');
|
||||
await load('?state=empty');
|
||||
const empties = { '#/inbox': /Nothing is waiting on you/, '#/inbox?view=roles': /No open decisions between roles/, '#/inbox?view=recent': /No decisions closed yet/, '#/tasks': /No tasks in this business yet/, '#/agents': /not held/ };
|
||||
for (const [r, re] of Object.entries(empties)) { await go(r); if (!re.test(await text())) problems.push('empty ' + r); }
|
||||
await load('?state=error'); await go('#/tasks');
|
||||
const err = await text();
|
||||
if (!/last read failed/.test(err) || !/Nothing was changed/.test(err) || !/#41 Bus store/.test(err)) problems.push('failed read does not keep data with a warning');
|
||||
await shot('state-error-tasks-1440');
|
||||
await b.evaluate(`document.querySelector('[data-retry]').click()`); await settle(40);
|
||||
if (/last read failed/.test(await text())) problems.push('Read again did not recover');
|
||||
await load('?state=refused'); await go('#/inbox');
|
||||
const ref_ = await text();
|
||||
if (!/refused the read/.test(ref_) || /Launch a second coder/.test(ref_) || await b.evaluate(`document.querySelectorAll('#main table, #main .s1-dec').length`)) problems.push('refusal shows data or lacks reason');
|
||||
await shot('state-refused-1440');
|
||||
check('states: loading, empty for every view, failed read keeps data and recovers, broker refusal shows no data', problems);
|
||||
}
|
||||
|
||||
// 10. Keyboard: skip link first, Enter focuses main, navigation moves focus to the new h1, focus rings on controls.
|
||||
{
|
||||
const problems = [];
|
||||
await load('#/inbox');
|
||||
await b.key('Tab');
|
||||
if (!/skip/.test(await b.evaluate(`document.activeElement.className`))) problems.push('first Tab is not the skip link');
|
||||
await b.key('Enter'); await settle(30);
|
||||
if (await b.evaluate(`document.activeElement.id`) !== 'main') problems.push('skip link did not focus main');
|
||||
// The route must stay in the URL, so a reload or bookmark after skipping still opens this view.
|
||||
if (await b.evaluate(`location.hash`) !== '#/inbox') problems.push('skip link replaced the route in the URL: ' + await b.evaluate(`location.hash`));
|
||||
await b.key('Tab');
|
||||
if (!await b.evaluate(`document.getElementById('main').contains(document.activeElement)`)) problems.push('Tab after skip did not enter main');
|
||||
await b.evaluate(`document.querySelector('.s1-dec-open').focus()`); await b.key('Enter'); await settle(60);
|
||||
const after = await b.evaluate(`document.activeElement.tagName + ' ' + location.hash`);
|
||||
if (!after.startsWith('H1 #/inbox/')) problems.push('Enter on a decision did not move focus to its h1: ' + after);
|
||||
await b.key('Tab'); await b.key('Tab');
|
||||
const ring = await b.evaluate(`(() => { const el = document.activeElement; const cs = getComputedStyle(el); return el.matches(':focus-visible') ? cs.outlineStyle + ' ' + cs.outlineWidth : 'no focus-visible on ' + el.tagName; })()`);
|
||||
if (!/solid|auto/.test(ring) || parseFloat(ring.split(' ')[1]) < 2) problems.push('focus ring: ' + ring);
|
||||
check('keyboard: skip link keeps the route, main focus, focus moves to the new page heading, visible focus rings', problems);
|
||||
}
|
||||
|
||||
// 15. The fixture is valid schema v3b data, and the schema's own views agree with what the page shows.
|
||||
// Every row the page reads goes into the real schema, in time order, so its CHECKs and triggers judge
|
||||
// the fixture. Then urgent_inbox, task_external_changes, task_current and tasks_open are compared with
|
||||
// reads(). Current task state comes from task_current (lead decisions 59 and 60), never the raw latest
|
||||
// row; the page must mark as stale exactly the rows task_current skipped.
|
||||
{
|
||||
const problems = [];
|
||||
const schemaPath = join(root, '..', '..', '..', 'darkwing', 'work', 'slice1-proto', 'schema-v3b.sql');
|
||||
const schema = await readFile(schemaPath, 'utf8');
|
||||
const sha = createHash('sha256').update(schema).digest('hex');
|
||||
if (!sha.startsWith('179ffe35')) problems.push('schema-v3b.sql changed: ' + sha.slice(0, 8));
|
||||
await load();
|
||||
const S = JSON.parse(await b.evaluate(`JSON.stringify(window.S1)`));
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec(schema);
|
||||
const order = ['decisions', 'messages', 'deliveries', 'decision_events', 'role_claims', 'events', 'task_snapshots'];
|
||||
const cols = Object.fromEntries(order.map(t => [t, db.prepare(`PRAGMA table_info(${t})`).all().map(c => c.name).filter(c => c !== 'seq')]));
|
||||
const rows = order.flatMap((t, ti) => S[t].map(r => ({ t, ti, r }))).sort((a, b) => a.r.at.localeCompare(b.r.at) || a.ti - b.ti);
|
||||
for (const { t, r } of rows) {
|
||||
const extra = Object.keys(r).filter(k => k !== 'seq' && k !== 'proposedJoin' && !cols[t].includes(k));
|
||||
if (extra.length) problems.push(`${t} row has columns the schema lacks: ${extra}`);
|
||||
try { db.prepare(`INSERT INTO ${t} (${cols[t].join(',')}) VALUES (${cols[t].map(() => '?').join(',')})`).run(...cols[t].map(c => r[c] ?? null)); }
|
||||
catch (e) { problems.push(`${t} ${r.id || r.task_ref || r.role || r.decision || ''} at ${r.at}: ${e.message}`); }
|
||||
}
|
||||
// Inserting in time order assigns seq; the fixture's own seq must agree with it.
|
||||
for (const t of order) {
|
||||
const key = { decisions: 'id', messages: 'id', events: 'id' }[t];
|
||||
const db_ = db.prepare(`SELECT ${key || 'at'} AS k FROM ${t} ORDER BY seq`).all().map(x => x.k);
|
||||
const fx = [...S[t]].sort((a, b) => a.seq - b.seq).map(x => key ? x[key] : x.at);
|
||||
if (db_.join() !== fx.join()) problems.push(`${t}: fixture seq order differs from time order`);
|
||||
}
|
||||
const R = JSON.parse(await b.evaluate(`JSON.stringify((() => { const r = window.S1App.reads(); const ts = r.tasks().groups.flatMap(g => g.rows); return {
|
||||
blocking: r.inbox().human.filter(d => d.blocking && !d.closed).map(d => d.id).sort(),
|
||||
external: ts.filter(t => t.external).map(t => t.ref).sort(),
|
||||
missing: r.tasks().missing.map(t => t.ref),
|
||||
open: ts.filter(t => !t.done).map(t => t.ref + ' ' + t.bucket).sort(),
|
||||
current: [...ts, ...r.tasks().missing].map(t => { const d = r.task(t.ref); return { ref: t.ref, bucket: d.missing ? 'gone' : d.bucket, stale: d.snapshots.filter(x => x.stale).map(x => x.at) }; }) }; })())`));
|
||||
const urgent = db.prepare('SELECT id FROM urgent_inbox ORDER BY id').all().map(x => x.id);
|
||||
if (urgent.join() !== R.blocking.join()) problems.push(`urgent_inbox ${urgent} vs page ${R.blocking}`);
|
||||
// The view also lists a tombstone (its latest row is a poll); the page shows that task as missing instead.
|
||||
const ext = db.prepare('SELECT task_ref FROM task_external_changes ORDER BY task_ref').all().map(x => x.task_ref).filter(r => !R.missing.includes(r));
|
||||
if (ext.join() !== R.external.join()) problems.push(`task_external_changes ${ext} vs page ${R.external}`);
|
||||
const open = db.prepare('SELECT task_ref, bucket FROM tasks_open').all().map(x => `${x.task_ref} ${S.lookups.buckets[x.bucket]}`).sort();
|
||||
if (open.join() !== R.open.join()) problems.push(`tasks_open ${JSON.stringify(open)} vs page ${JSON.stringify(R.open)}`);
|
||||
const page = Object.fromEntries(R.current.map(t => [t.ref, t]));
|
||||
const cur = db.prepare('SELECT task_ref, seq, fields FROM task_current ORDER BY task_ref').all();
|
||||
if (cur.map(c => c.task_ref).join() !== Object.keys(page).sort().join()) problems.push(`task_current refs ${cur.map(c => c.task_ref)} vs page ${Object.keys(page).sort()}`);
|
||||
for (const c of cur) {
|
||||
const f = JSON.parse(c.fields), p = page[c.task_ref];
|
||||
if (!p) continue;
|
||||
const bucket = f.gone ? 'gone' : S.lookups.buckets[f.bucket];
|
||||
if (bucket !== p.bucket) problems.push(`${c.task_ref}: task_current says ${bucket}, page says ${p.bucket}`);
|
||||
const skipped = db.prepare('SELECT at FROM task_snapshots WHERE task_ref = ? AND seq > ? ORDER BY seq').all(c.task_ref, c.seq).map(x => x.at);
|
||||
if (skipped.join() !== p.stale.join()) problems.push(`${c.task_ref}: task_current skipped ${skipped}, page marks stale ${p.stale}`);
|
||||
}
|
||||
const marked = R.current.filter(t => t.stale.length).map(t => t.ref);
|
||||
if (marked.join() !== 'vikunja:3/42') problems.push(`stale reads marked on ${marked}, expected only vikunja:3/42`);
|
||||
db.close();
|
||||
check('fixture: every row inserts into schema v3b (179ffe35) in time order; urgent_inbox, task_external_changes, task_current and tasks_open match the page; stale marks are exactly the rows task_current skips (#42 only)', problems, `${rows.length} rows`);
|
||||
}
|
||||
|
||||
// 16. The Q1 stand-in returns what the real broker returns. q1.js ports the four reader verbs; this
|
||||
// runs the reader from packages/bus as committed in BUS_COMMIT (S2c, row 44) over the same rows in a
|
||||
// throwaway data root, and compares inbox, tasks, agents and the trail of every task, decision and
|
||||
// message. The package is extracted from the commit with git archive, so working-tree edits don't count.
|
||||
// Interim: Rocko's pinned example fixture replaces it (lead decision 56, Q1). The repository is only read.
|
||||
// DEWEY_S1_REPO points at the repository when the check runs from a copy outside it (mutation scratch).
|
||||
const BUS_COMMIT = 'd27042fa';
|
||||
{
|
||||
const repo = process.env.DEWEY_S1_REPO || join(root, '..', '..', '..', '..');
|
||||
let tree = null;
|
||||
try { tree = execFileSync('git', ['-C', repo, 'archive', '--format=tar', BUS_COMMIT, 'packages/bus/package.json', 'packages/bus/schema.sql', 'packages/bus/src'], { maxBuffer: 1 << 26 }); } catch { /* recorded below */ }
|
||||
if (!tree) record('the Q1 reader at the S2 commit', 'not verified', `git archive ${BUS_COMMIT} failed in ${repo}`);
|
||||
else {
|
||||
const problems = [];
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dewey-s1-bus-')), code = join(dir, 'code'), data = join(dir, 'data');
|
||||
let store, count = 0, pins = '', subjects = [];
|
||||
try {
|
||||
await mkdir(code, { mode: 0o700 }); await mkdir(data, { mode: 0o700 });
|
||||
execFileSync('tar', ['-x', '-C', code], { input: tree });
|
||||
const bus = join(code, 'packages', 'bus');
|
||||
const pin = async f => createHash('sha256').update(await readFile(join(bus, f))).digest('hex').slice(0, 8);
|
||||
pins = `commit ${BUS_COMMIT}: broker.mjs ${await pin('src/broker.mjs')}, store.mjs ${await pin('src/store.mjs')}, schema.sql ${await pin('schema.sql')}`;
|
||||
await load();
|
||||
const S = JSON.parse(await b.evaluate(`JSON.stringify(window.S1)`));
|
||||
subjects = [...new Set([...S.task_snapshots.map(s => s.task_ref), ...S.decisions.map(d => d.id), ...S.messages.map(m => m.id)])];
|
||||
const { Store } = await import(pathToFileURL(join(bus, 'src', 'store.mjs')));
|
||||
const { Broker } = await import(pathToFileURL(join(bus, 'src', 'broker.mjs')));
|
||||
store = new Store(data);
|
||||
const order = ['decisions', 'messages', 'deliveries', 'decision_events', 'role_claims', 'events', 'task_snapshots'];
|
||||
const cols = Object.fromEntries(order.map(t => [t, store.all(`PRAGMA table_info(${t})`).map(c => c.name).filter(c => c !== 'seq')]));
|
||||
const rows = order.flatMap((t, ti) => S[t].map(r => ({ t, ti, r }))).sort((a, b) => a.r.at.localeCompare(b.r.at) || a.ti - b.ti);
|
||||
store.transaction(() => { for (const { t, r } of rows) store.run(`INSERT INTO ${t} (${cols[t].join(',')}) VALUES (${cols[t].map(() => '?').join(',')})`, ...cols[t].map(c => r[c] ?? null)); });
|
||||
const role = { authority: { withinRole: [], crossRole: [] } }, B = S.lookups.business;
|
||||
const broker = new Broker({ store, businesses: { [B]: { id: B, human: S.lookups.human, roles: { pm: role, cto: role, coder: role, reviewer: role }, arbiters: { technical: 'cto', delivery: 'pm' } } } });
|
||||
const cap = broker.bindReader({ business: B });
|
||||
const want = { inbox: broker.request(cap, { verb: 'inbox' }), tasks: broker.request(cap, { verb: 'tasks' }), agents: broker.request(cap, { verb: 'agents' }) };
|
||||
for (const x of subjects) want[`trail ${x}`] = broker.request(cap, { verb: 'trail', args: { subject: x } });
|
||||
const got = JSON.parse(await b.evaluate(`JSON.stringify((() => { const q = S1Q1.views(window.S1), out = { inbox: q.inbox(), tasks: q.tasks(), agents: q.agents() }; for (const x of ${JSON.stringify(subjects)}) out['trail ' + x] = q.trail(x); return out; })())`));
|
||||
// Key order and absent-versus-null columns don't count; values and row order do.
|
||||
const canon = v => JSON.stringify(v, (k, x) => x && typeof x === 'object' && !Array.isArray(x) ? Object.fromEntries(Object.keys(x).sort().filter(n => x[n] !== null && x[n] !== undefined).map(n => [n, x[n]])) : x);
|
||||
for (const [name, a] of Object.entries(want)) {
|
||||
const g = got[name] || []; count += a.length;
|
||||
if (canon(a) === canon(g)) continue;
|
||||
const i = Math.max(0, a.findIndex((x, j) => canon(x) !== canon(g[j])));
|
||||
problems.push(`${name}: candidate ${a.length} rows, stand-in ${g.length}; first difference at ${i}: ${canon(a[i]).slice(0, 200)} vs ${canon(g[i]).slice(0, 200)}`);
|
||||
}
|
||||
if (!want.inbox.length || !want.tasks.length || !want.agents.length) problems.push('a candidate read came back empty');
|
||||
} catch (e) { problems.push(`candidate reader failed: ${e.code || ''} ${e.message}`); }
|
||||
finally { store?.close(); await rm(dir, { recursive: true, force: true }); }
|
||||
check(`Q1 stand-in: q1.js returns what the packages/bus reader returns (${pins}) for inbox, tasks, agents and ${subjects.length} trails`, problems, `${count} rows compared`);
|
||||
}
|
||||
}
|
||||
|
||||
// 11. Long content and 200% text at 320 px stay within the viewport.
|
||||
{
|
||||
const problems = [];
|
||||
await b.viewport(320, 700); await load();
|
||||
await b.evaluate(`(() => { const d = window.S1.decisions.find(x => x.id === ${JSON.stringify(D.launch)}); d.question = 'Launch a second coder instance so the poller can move '.repeat(6) + 'https://example.invalid/a/very/long/unbroken/path/segment/that/must/wrap/0123456789abcdef0123456789abcdef'; window.S1App.render(); })()`);
|
||||
for (const r of ['#/inbox', `#/inbox/${D.launch}`, `#/trail/task/${ref(44)}`]) { await go(r); const p = await probe(); if (p.sw > p.cw) problems.push(`${r} overflow with long text`); }
|
||||
await b.evaluate(`document.documentElement.style.fontSize = '30px'`); await settle(30);
|
||||
for (const r of ['#/inbox', `#/inbox/${D.launch}`, '#/tasks', '#/agents', `#/trail/task/${ref(41)}`]) { await go(r); const p = await probe(); if (p.sw > p.cw) problems.push(`${r} overflow at 200% text`); }
|
||||
await go(`#/inbox/${D.launch}`); await shot('text-200-decision-320');
|
||||
check('long unbroken content and 200% text at 320 px stay within the viewport (tables scroll inside their region)', problems);
|
||||
}
|
||||
|
||||
record('forced-colors rendering', 'not verified', 'slice1.css adds forced-colors rules; headless Chromium here does not emulate them');
|
||||
record('screen reader behaviour', 'not verified', 'focus moves to the page heading and #announce is updated; no screen reader was run');
|
||||
|
||||
await b.close();
|
||||
report.summary = { total: report.checks.length, failed: failures, notVerified: report.checks.filter(c => c.status === 'not verified').length };
|
||||
await writeFile(join(out, 'report.json'), JSON.stringify(report, null, 2));
|
||||
console.log(`\n${report.summary.total} checks, ${failures} failed, ${report.summary.notVerified} not verified. Report: evidence/slice1/report.json`);
|
||||
process.exit(failures ? 1 : 0);
|
||||
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 121 KiB |
@@ -0,0 +1,75 @@
|
||||
f2f8594b6b6c9655b326012f4a2d4e5528527d03b14b55ea389d334c56ccf81c agents/dewey/work/wui/BRIEF.md
|
||||
74572ec807bc262f3a6e2c40d59389ed0de5ce612d19d76b6b9d6fd0122c39a9 agents/dewey/work/wui/DECISIONS.md
|
||||
75728670f0b11c96c18e2b541f1c54e74cad1c444143b3b57770279666bd9d07 agents/dewey/work/wui/HISTORY.md
|
||||
e9cac40fe832d36429f02c4b0fa3e50cf63ccf3457b08e0326b8fc1b6c227b49 agents/dewey/work/wui/README.md
|
||||
fcc6d1ec1a35713f1bf765cbe83c9e526a4907d0cd2ce8889a2ef9ed6c54b25d agents/dewey/work/wui/RESEARCH.md
|
||||
77f6a53c6bf868a464debc8e4fc7ddd7b8d31db229b1f48c12c1b7be0fc62267 agents/dewey/work/wui/TASKS.md
|
||||
63e96d72b6afd4eb8058c8b5b6846dbaa1c808dec1054ab23bfb3f5871f78801 agents/dewey/work/wui/VERIFY.md
|
||||
9fea608a947e67020c33cad9a6fe3d60c54119dfb8cff87768a8117a15ed7543 agents/dewey/work/wui/assets/fonts/dm-sans-400.woff2
|
||||
9fea608a947e67020c33cad9a6fe3d60c54119dfb8cff87768a8117a15ed7543 agents/dewey/work/wui/assets/fonts/dm-sans-500.woff2
|
||||
9fea608a947e67020c33cad9a6fe3d60c54119dfb8cff87768a8117a15ed7543 agents/dewey/work/wui/assets/fonts/dm-sans-600.woff2
|
||||
9fea608a947e67020c33cad9a6fe3d60c54119dfb8cff87768a8117a15ed7543 agents/dewey/work/wui/assets/fonts/dm-sans-700.woff2
|
||||
9af36190332437f5ecd09974de43c1f7c77a310a996cdd8ceb25628b458840e1 agents/dewey/work/wui/assets/fonts/dm-sans-OFL.txt
|
||||
89947da00bec2cbcacab815b7e36bf07a635dec41e3ca87a6eb2484a605a4e71 agents/dewey/work/wui/assets/fonts/dm-sans-sources.txt
|
||||
e2291e842cf5af167122a22881a740c7f2dda7716f1e8cd76680264f4a859470 agents/dewey/work/wui/assets/fonts/ibm-plex-sans-400.woff2
|
||||
e2291e842cf5af167122a22881a740c7f2dda7716f1e8cd76680264f4a859470 agents/dewey/work/wui/assets/fonts/ibm-plex-sans-500.woff2
|
||||
e2291e842cf5af167122a22881a740c7f2dda7716f1e8cd76680264f4a859470 agents/dewey/work/wui/assets/fonts/ibm-plex-sans-600.woff2
|
||||
e2291e842cf5af167122a22881a740c7f2dda7716f1e8cd76680264f4a859470 agents/dewey/work/wui/assets/fonts/ibm-plex-sans-700.woff2
|
||||
7e6b2818edbd8f6a01ae80641cc8f16a51080d08fb4e532be3a0b6f74adb07da agents/dewey/work/wui/assets/fonts/ibm-plex-sans-OFL.txt
|
||||
a762adc472b0911c4f5d0e3f0dd672c557c4b3debbc9b617488b46c36e10b183 agents/dewey/work/wui/assets/fonts/ibm-plex-sans-sources.txt
|
||||
a30ddcd349703aff7464c34bef3fffdff405ee50c113440d7c8693c02d210972 agents/dewey/work/wui/assets/fonts/manrope-400.woff2
|
||||
a30ddcd349703aff7464c34bef3fffdff405ee50c113440d7c8693c02d210972 agents/dewey/work/wui/assets/fonts/manrope-500.woff2
|
||||
a30ddcd349703aff7464c34bef3fffdff405ee50c113440d7c8693c02d210972 agents/dewey/work/wui/assets/fonts/manrope-600.woff2
|
||||
a30ddcd349703aff7464c34bef3fffdff405ee50c113440d7c8693c02d210972 agents/dewey/work/wui/assets/fonts/manrope-700.woff2
|
||||
e01b637272e0cbdfb240184dd98ea5cc671556d9894dae2668d92ab2c906787c agents/dewey/work/wui/assets/fonts/manrope-OFL.txt
|
||||
0d9aaaaa963b675d94d8fbee0cf47a8ecf03ea412f8b14aba96416375e5a2bb5 agents/dewey/work/wui/assets/fonts/manrope-sources.txt
|
||||
a1a047eb0b7f9c8f3e656d97d2292545371f8f0a92b4eb273cfd2fc371bd50ad agents/dewey/work/wui/assets/logos/aperture.svg
|
||||
d3d0eb03872b7336b00186beb95567840e5caf2bef29c2a9599480b29eb2f8da agents/dewey/work/wui/assets/logos/assembly.svg
|
||||
446cb7c0b75bb5cc7e3e08d9feb979c000531adfba0523aec81fb9af77f1ae55 agents/dewey/work/wui/assets/logos/relay.svg
|
||||
9efa30b490fa2cde89dcd3c9eafd37ae83a77a84a2a1965c78828e410be2b568 agents/dewey/work/wui/assets/palettes.json
|
||||
31222b6c40af7f253cceccc3d3caef5c2b0f9c0e096ea2eb5828b2224c3c47fe agents/dewey/work/wui/board.js
|
||||
d568df6dca14ae336db3b627639c8181c088649a21ffe992f9bfacc20b5b6238 agents/dewey/work/wui/brand.js
|
||||
e884333a3191921050c6ab45c7d87d40c6bc3092ba9d55c3c06bc5f0d01d5da3 agents/dewey/work/wui/checks/assets.mjs
|
||||
3ff14de07e87cc624433db6425b9d05e40d98f61e40f2bbf1fbee7431f9d73c1 agents/dewey/work/wui/checks/browser.mjs
|
||||
16b3cf3f7c8897dddef3b4caf1e9200272078cea8226a6df5e9ea1e7d25fe598 agents/dewey/work/wui/checks/detail-checks.mjs
|
||||
af924e53a19e3a639e5bc4c1e6a27253d03adcd935a8e1a847806761b797336c agents/dewey/work/wui/checks/fetch-fonts.py
|
||||
fd7d5baad1eb01c41ae98e1341aa5b16bb22e02f5296951381f48705b42d6855 agents/dewey/work/wui/checks/final-checks.mjs
|
||||
df79defbecf1500b6b5abdf7f0568c22ae88b437e841f0c95a7fa16d5d5fd3d2 agents/dewey/work/wui/checks/references.mjs
|
||||
bd6cc0c1021401207519e3489105284d120da2d8b85710e8fd51c1939f10e411 agents/dewey/work/wui/checks/verify.mjs
|
||||
fd4fece4624de03d151434d6fdbaa2afd89637a9ff6beb3deec02fb98eba605a agents/dewey/work/wui/evidence/board-all-text-200-320.png
|
||||
41d4aababef4fdd5beff4f0b75fcdcd59e9a2fdf408a6a7aaf9f7afd86d1ac02 agents/dewey/work/wui/evidence/board-dark-1440.png
|
||||
9599769f4ed18363c3b7c0061823e0fd772d9cd516d3f72ab02e7b74a01de93c agents/dewey/work/wui/evidence/board-dim-1440.png
|
||||
efc8536d18abcc6eaf389be38611667e6e15ac3861ab84352e4ea1460236ef95 agents/dewey/work/wui/evidence/board-font-storage-fallback-320.png
|
||||
d8097a2896c3c4348f709c1b25609207dad754d051994bc6f4835ed38d2cf290 agents/dewey/work/wui/evidence/board-light-1440.png
|
||||
b6c75f3754c9b68cbb6041aabcb1400f4c1c2a6a9bb532fb5714f0e3b0fcc0dc agents/dewey/work/wui/evidence/board-light-320.png
|
||||
8f5d62c30b8e6694e53608ba69c841024926c47d3934a76101a722856fa1e890 agents/dewey/work/wui/evidence/board-light-3440.png
|
||||
9e834a3f97bb446155d07f053adceaaeb1907d0397a44f5b31f8c7553d79931c agents/dewey/work/wui/evidence/board-light-390.png
|
||||
59974cf0948708d64792c2e0f8a3d8dfafba7ae95f1f359c5dc9694abb3abfc6 agents/dewey/work/wui/evidence/board-light-768.png
|
||||
c337277efa1a90f82c8d70874a09a833785f29943f413e676f7671a0944fcc70 agents/dewey/work/wui/evidence/board-text-200-320.png
|
||||
7eb8d483950957eb75ca8e199fe78e9ad64c3498d35a42c945bd3cf590eecd6c agents/dewey/work/wui/evidence/board-text-spacing-320.png
|
||||
54b5e3c2f057aff0ef8d0dd21875d08b00129526e296dc54026cb7c04d11831e agents/dewey/work/wui/evidence/buzz-desktop.png
|
||||
09fed8a1e59ef1f74ea47345d6679c44cfebd408779ac1db2408a853372b7246 agents/dewey/work/wui/evidence/buzz-details.png
|
||||
28fbb20e0612c2c05cc2d5cc2c1d06f5a5f1e31fc0e01a704c02ac83358b5c06 agents/dewey/work/wui/evidence/buzz-mobile.png
|
||||
093e79080560bed7fdb0577298726613049830d1294f3515bc394dce7e62bcd2 agents/dewey/work/wui/evidence/detail-check-run.txt
|
||||
093e79080560bed7fdb0577298726613049830d1294f3515bc394dce7e62bcd2 agents/dewey/work/wui/evidence/detail-verification.json
|
||||
5bbbaf13277d24e11addf2a1a7a47a841519ddb3bfb48147ba1512bec8cfa045 agents/dewey/work/wui/evidence/downloads/mosaic-brand-review.txt
|
||||
ae27b3763f7b856c8f45959c6f4680eefd176fd9f663d48767152c920d1cd3c8 agents/dewey/work/wui/evidence/downloads/mosaic-harbor-tokens.json
|
||||
769e5e695ddec233323b59d226941edc6d98590b8e896588a92bf3366a2cee65 agents/dewey/work/wui/evidence/final-verification.json
|
||||
8e76ad9e0d20ad83b41da4c4b5c7dcfc9062147c68486334163447198060ed33 agents/dewey/work/wui/evidence/focus-short-desktop.png
|
||||
b3e12958c0596b9881016d0417111c15c73fed7ba68f0e8ee73474de61abc562 agents/dewey/work/wui/evidence/references.json
|
||||
ea723cf8f51c2384389619688fa65cd3bb944ec6338c7ee6bd458a25ca2e1f03 agents/dewey/work/wui/evidence/section-color-dark.png
|
||||
f6bc389adfc7a1e47f03366f192971051ba47f5bbdf4bfbc6300c3230c578d35 agents/dewey/work/wui/evidence/section-color-light.png
|
||||
4b213ba93554d1651679d3d3189b6c25f799d2d6e88106ff09e109c0bf44685c agents/dewey/work/wui/evidence/section-color-ultrawide.png
|
||||
7dbe97db03c868904404d5302781761dd06064184a27bd8963b005b0a3d7975d agents/dewey/work/wui/evidence/section-components-light.png
|
||||
093f94495b5f4363a89ebb16228997e5c495f877f2ad4f621de2ec5e19acdaac agents/dewey/work/wui/evidence/section-components-mobile.png
|
||||
693de8c423cea8775810be8755ffe8a803215725bc8e9e1c9cb367bd905a1161 agents/dewey/work/wui/evidence/section-identity-light.png
|
||||
1e7657e831e1a870373cd418d386d90eda7cde788292454a3d9c0d5458adcfb3 agents/dewey/work/wui/evidence/section-identity-mobile.png
|
||||
ffecc26d0f2e4f4a9c1d907160ca5a5ef9d85c4802c1aa5fd06304a02c7ea60e agents/dewey/work/wui/evidence/section-review-light.png
|
||||
379a5fea56a84bce71cd88995bae3011239f1990d51c5f5a270d7d47238b7ce3 agents/dewey/work/wui/evidence/section-typography-light.png
|
||||
d61035c64fce422504acedc148e752f135a7afdef955ee09cae5de7a8b3281ec agents/dewey/work/wui/evidence/t3-desktop.png
|
||||
a507d4f6c161ed983fce6d325bd9940dd35c734f16b4d159437b2814f857e512 agents/dewey/work/wui/evidence/t3-details.png
|
||||
6c6a22be11b7205caded38fdb2fbf1ec525c3f038ff1ef50914c25d2eb8eb35c agents/dewey/work/wui/evidence/t3-mobile.png
|
||||
73cd0e1795f2e81d1a5e6ae5068c5dea484dd37410ab233f6e30173a21c93cf7 agents/dewey/work/wui/evidence/verification.json
|
||||
9479f68d96863bf7bdae536ecb0df835b48350207b2ee5035bedbe0f086b7a80 agents/dewey/work/wui/evidence/verify-run.txt
|
||||
ff0b6a8fd4e8cf5a0e8d3361243b730d83a86a8563562ffb62338b6635965235 agents/dewey/work/wui/index.html
|
||||
4f1309f4b4aec0d595300da060f49f7feb2b3042a8d73468894965ae83b9a04e agents/dewey/work/wui/styles.css
|
||||
@@ -0,0 +1,257 @@
|
||||
{
|
||||
"observedAt": "2026-09-08T13:49:13.216Z",
|
||||
"checks": [
|
||||
{
|
||||
"name": "Computed rendered text contrast across 30 combinations",
|
||||
"result": "pass"
|
||||
},
|
||||
{
|
||||
"name": "Sidebar keyboard focus visible at 768 px viewport height",
|
||||
"result": "pass",
|
||||
"focus": {
|
||||
"text": "05 Your review",
|
||||
"top": 630.109375,
|
||||
"bottom": 674.109375,
|
||||
"viewport": 768,
|
||||
"style": "solid"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Chromium accessibility tree gives interactive elements accessible names",
|
||||
"result": "pass",
|
||||
"namedControls": 60
|
||||
},
|
||||
{
|
||||
"name": "Review notes and all-three-mode palette JSON downloaded and content-verified",
|
||||
"result": "pass"
|
||||
},
|
||||
{
|
||||
"name": "Every computed text size doubled at 320 px",
|
||||
"result": "pass",
|
||||
"measurement": {
|
||||
"width": 320,
|
||||
"scroll": 305
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Blocked fonts and denied storage: controls still work and fallback reflows",
|
||||
"result": "pass",
|
||||
"measurement": {
|
||||
"width": 320,
|
||||
"scroll": 305
|
||||
}
|
||||
}
|
||||
],
|
||||
"renderedContrast": [
|
||||
{
|
||||
"palette": "harbor",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.612829123046221,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "harbor",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.504658476260286,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "harbor",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 5.167615908293434,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "carmine",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.662315521062682,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "carmine",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.587463027691096,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "carmine",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 4.695553088417374,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "atlantic",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.515881820137726,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "atlantic",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.516521277596746,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "atlantic",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 5.597708271207925,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "terracotta",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.518475634580629,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "terracotta",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.538542941476706,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "terracotta",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 5.525894813496129,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "aubergine",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.664757683683436,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "aubergine",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.560324571031366,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "aubergine",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 4.999968974923534,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "mineral",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.549690583339469,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "mineral",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.575057699689022,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "mineral",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 5.6577550779736105,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "cobalt",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.706276899711153,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "cobalt",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.5100156460147796,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "cobalt",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 4.534974293870712,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "rosewood",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.636598241711077,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "rosewood",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.584411037938534,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "rosewood",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 4.964802403187582,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "graphite",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.613910901400341,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "graphite",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.565616755282305,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "graphite",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 5.478526170936041,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "grove",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.52023260851089,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "grove",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.564633732635816,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "grove",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 5.707548102674204,
|
||||
"failures": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
Mosaic Stack branding review
|
||||
Current preview: Assembly · DM Sans · Harbor · Light.
|
||||
|
||||
Prefer Relay. Please refine the small-size gaps.
|
||||
|
||||
These selections are review preferences, not an automatic approval.
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"prototype": true,
|
||||
"palette": "Harbor",
|
||||
"theory": "Complementary",
|
||||
"rationale": "Clear blue with a restrained copper counterpoint. The warm accent balances cool navigation and gives selected details a second voice.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f3f5f7",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#e6eaef",
|
||||
"text": "#434d5b",
|
||||
"muted": "#5e6978",
|
||||
"line": "#ced5df",
|
||||
"border": "#636e7e",
|
||||
"action": "#2266bf",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#8d5e20",
|
||||
"focus": "#2266bf",
|
||||
"success": "#287658",
|
||||
"warning": "#8f5c19",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#242d38",
|
||||
"surface": "#2e3947",
|
||||
"raised": "#384556",
|
||||
"text": "#d6dbe1",
|
||||
"muted": "#a9b1bc",
|
||||
"line": "#47576c",
|
||||
"border": "#a4acb7",
|
||||
"action": "#8ab4ea",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#e1b47a",
|
||||
"focus": "#8ab4ea",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#ea9d99"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#12161c",
|
||||
"surface": "#1c232b",
|
||||
"raised": "#28323e",
|
||||
"text": "#b9c1cb",
|
||||
"muted": "#a4acb7",
|
||||
"line": "#384556",
|
||||
"border": "#a4acb7",
|
||||
"action": "#74a6e7",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#e1b47a",
|
||||
"focus": "#74a6e7",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e37d78"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
f0b9a1dc8ccc3f33b887797ee83ccf296b93e17905d3fb274950347688096b7e agents/dewey/work/wui/DECISIONS.md
|
||||
6b8ce92f7193d61cdd48bbf079012d24ca09de713ac26cfa2f088440daa38d83 agents/dewey/work/wui/TASKS.md
|
||||
bb98386d4a2f7ef645f3d488e2a8b17e0197e717203892aa6bc8e2a48799a995 agents/dewey/work/wui/HISTORY.md
|
||||
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 269 KiB |
|
After Width: | Height: | Size: 436 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 336 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 177 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 322 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 273 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 271 KiB |
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 269 KiB |
|
After Width: | Height: | Size: 269 KiB |
|
After Width: | Height: | Size: 269 KiB |
|
After Width: | Height: | Size: 271 KiB |
|
After Width: | Height: | Size: 266 KiB |
@@ -0,0 +1,263 @@
|
||||
{
|
||||
"generated": "2026-09-12T12:40:00.463Z",
|
||||
"checks": [
|
||||
{
|
||||
"name": "d1-rail: 14 routes × 8 widths render without script errors or horizontal overflow",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d2-deck: 14 routes × 8 widths render without script errors or horizontal overflow",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d3-console: 14 routes × 8 widths render without script errors or horizontal overflow",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d4-board: 14 routes × 8 widths render without script errors or horizontal overflow",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d5-stream: 14 routes × 8 widths render without script errors or horizontal overflow",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "index: renders at all widths and links all five designs",
|
||||
"status": "pass",
|
||||
"detail": "d1-rail.html, d2-deck.html, d3-console.html, d4-board.html, d5-stream.html"
|
||||
},
|
||||
{
|
||||
"name": "palettes × modes: 10 palettes × 3 modes apply, and 600 sampled text/UI pairs meet WCAG contrast",
|
||||
"status": "pass",
|
||||
"detail": "lowest 4.5:1 (harbor/dim .badge-muted)"
|
||||
},
|
||||
{
|
||||
"name": "d1-rail: loading, empty, and error states render and error recovers via retry",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d2-deck: loading, empty, and error states render and error recovers via retry",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d3-console: loading, empty, and error states render and error recovers via retry",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d4-board: loading, empty, and error states render and error recovers via retry",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d5-stream: loading, empty, and error states render and error recovers via retry",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d1-rail: launch conflict, retired refusal, simulated launch, empty-message guard, controller-disabled buttons, non-blocking config notice, labelled simulation, appearance persistence",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d2-deck: launch conflict, retired refusal, simulated launch, empty-message guard, controller-disabled buttons, non-blocking config notice, labelled simulation, appearance persistence",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d3-console: launch conflict, retired refusal, simulated launch, empty-message guard, controller-disabled buttons, non-blocking config notice, labelled simulation, appearance persistence",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d4-board: launch conflict, retired refusal, simulated launch, empty-message guard, controller-disabled buttons, non-blocking config notice, labelled simulation, appearance persistence",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d5-stream: launch conflict, retired refusal, simulated launch, empty-message guard, controller-disabled buttons, non-blocking config notice, labelled simulation, appearance persistence",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d1-rail: keyboard path (skip link, main focus, focus rings, Escape, design-specific keys)",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d2-deck: keyboard path (skip link, main focus, focus rings, Escape, design-specific keys)",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d3-console: keyboard path (skip link, main focus, focus rings, Escape, design-specific keys)",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d4-board: keyboard path (skip link, main focus, focus rings, Escape, design-specific keys)",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d5-stream: keyboard path (skip link, main focus, focus rings, Escape, design-specific keys)",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d1-rail: long unbroken content and 200% text at 320 px stay within the viewport",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d2-deck: long unbroken content and 200% text at 320 px stay within the viewport",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d3-console: long unbroken content and 200% text at 320 px stay within the viewport",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d4-board: long unbroken content and 200% text at 320 px stay within the viewport",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "d5-stream: long unbroken content and 200% text at 320 px stay within the viewport",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "secrets never rendered; 70 action buttons across routes produce a labelled simulated notice",
|
||||
"status": "pass",
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "shared stylesheet declares reduced-motion and forced-colors rules",
|
||||
"status": "pass",
|
||||
"detail": "reduced-motion true, forced-colors true"
|
||||
},
|
||||
{
|
||||
"name": "forced-colors rendering",
|
||||
"status": "not verified",
|
||||
"detail": "headless Chromium does not emulate Windows high contrast here; rules exist but were not visually checked"
|
||||
},
|
||||
{
|
||||
"name": "screen reader announcement behaviour",
|
||||
"status": "not verified",
|
||||
"detail": "aria-live regions are present and updated on route change; no screen reader was run"
|
||||
}
|
||||
],
|
||||
"contrastLowest": [
|
||||
{
|
||||
"p": "harbor",
|
||||
"m": "dim",
|
||||
"s": ".badge-muted",
|
||||
"r": 4.5,
|
||||
"need": 4.5,
|
||||
"ok": true
|
||||
},
|
||||
{
|
||||
"p": "harbor",
|
||||
"m": "dim",
|
||||
"s": ".count",
|
||||
"r": 4.5,
|
||||
"need": 4.5,
|
||||
"ok": true
|
||||
},
|
||||
{
|
||||
"p": "atlantic",
|
||||
"m": "light",
|
||||
"s": ".badge-muted",
|
||||
"r": 4.52,
|
||||
"need": 4.5,
|
||||
"ok": true
|
||||
},
|
||||
{
|
||||
"p": "atlantic",
|
||||
"m": "light",
|
||||
"s": ".count",
|
||||
"r": 4.52,
|
||||
"need": 4.5,
|
||||
"ok": true
|
||||
},
|
||||
{
|
||||
"p": "atlantic",
|
||||
"m": "dim",
|
||||
"s": ".badge-muted",
|
||||
"r": 4.52,
|
||||
"need": 4.5,
|
||||
"ok": true
|
||||
},
|
||||
{
|
||||
"p": "atlantic",
|
||||
"m": "dim",
|
||||
"s": ".count",
|
||||
"r": 4.52,
|
||||
"need": 4.5,
|
||||
"ok": true
|
||||
},
|
||||
{
|
||||
"p": "terracotta",
|
||||
"m": "light",
|
||||
"s": ".badge-muted",
|
||||
"r": 4.52,
|
||||
"need": 4.5,
|
||||
"ok": true
|
||||
},
|
||||
{
|
||||
"p": "terracotta",
|
||||
"m": "light",
|
||||
"s": ".count",
|
||||
"r": 4.52,
|
||||
"need": 4.5,
|
||||
"ok": true
|
||||
},
|
||||
{
|
||||
"p": "mineral",
|
||||
"m": "light",
|
||||
"s": ".badge-muted",
|
||||
"r": 4.55,
|
||||
"need": 4.5,
|
||||
"ok": true
|
||||
},
|
||||
{
|
||||
"p": "mineral",
|
||||
"m": "light",
|
||||
"s": ".count",
|
||||
"r": 4.55,
|
||||
"need": 4.5,
|
||||
"ok": true
|
||||
},
|
||||
{
|
||||
"p": "cobalt",
|
||||
"m": "dim",
|
||||
"s": ".nav-link[aria-current=page]",
|
||||
"r": 4.55,
|
||||
"need": 4.5,
|
||||
"ok": true
|
||||
},
|
||||
{
|
||||
"p": "aubergine",
|
||||
"m": "dim",
|
||||
"s": ".badge-muted",
|
||||
"r": 4.56,
|
||||
"need": 4.5,
|
||||
"ok": true
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 31,
|
||||
"failed": 0,
|
||||
"notVerified": 2
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 76 KiB |