A Console editor for the project's spec and docs #1642

Closed
opened 2026-10-15 23:00:52 +00:00 by jarvis · 11 comments
Contributor

Vikunja P19 task 314, Q167 rows after comment 225.

Jason wants to read and change a project's north-star spec and docs
without an agent, in a WYSIWYG markdown editor, with git history. Row 147
gives each project a local git repository, but the only ways to change it
are an editor on the host and git commit by hand.

Brief: docs/plans/2026-10-15_q167-rows-r3.md, section "A Console editor for the project's spec and docs".

Vikunja P19 task 314, Q167 rows after comment 225. Jason wants to read and change a project's north-star spec and docs without an agent, in a WYSIWYG markdown editor, with git history. Row 147 gives each project a local git repository, but the only ways to change it are an editor on the host and `git commit` by hand. Brief: `docs/plans/2026-10-15_q167-rows-r3.md`, section "A Console editor for the project's spec and docs".
Author
Contributor

Review request for queue row 148, round 1: A Console editor for the project's spec and docs

  • Owner: researcher
  • Reviewers: dewey, filbert
  • Gate: Filbert and Dewey approve on #1642 naming the candidate manifest; Mos's decision 123 go on the public/ diff (nothing vendored, sage-0506); webui, bus, cli and project suites and every scripts/test-*.sh green (sage)
  • Brief: docs/plans/2026-10-15_q167-rows-r3.md § A Console editor for the project's spec and docs @6805904555dc
  • Candidate: manifest 68bdcecec3b8d92ccfaed7595f415f062270cebf7899f5c0edcdd6cd63e0f5f5

The manifest:

18c86e9e310a59de2dea6d73c3bd8e2359fc511834c3cafbc8cf0078a86677fb  docs/TOOLS.md
47fea86a97c1633f6555f4f944aa9e3e51e3e20c32b2be5abbc7b07763ba3b6d  packages/bus/README.md
a7d688329e3d9408dd36030a5693b363805b10658aa4af93a99878439a58e5d1  packages/bus/src/broker.mjs
d40ddc2907f0d6e12e9556798591bcd7565d677177cb4d1a0d1a1da3e1e180ea  packages/bus/src/client.mjs
c4b1816dd1b3b3f4f1b4b6f751b8194c1fa036daac1c16fea6dee2ba03f4d81a  packages/bus/src/runtime.mjs
7466b77f2bea923a9218f3834d8426b5c7f2979b5ffd1010c3ef5a91cc692c02  packages/bus/src/server.mjs
6aa5bfe4af6d1f597ccc4297bd7e879452d6a441931fd292ca309ab1cdfafbed  packages/bus/tests/project-files.test.mjs
8325b41791d526ead74ae66926af0b272982ab698dece1ab5e1536a660b9ede3  packages/bus/tests/spec-fixtures.mjs
0e4d1c15680f9045b07ec0c001d8ff8e6ac35fae9f68dc0f9d147e15977923d7  packages/project/README.md
20a8fd4c52c3eea9454b137d830c4985797ddfaa8bf3f12de21c92f36187c036  packages/project/src/index.mjs
e76a935e91f0d62dced2392294216dc0efab9b7db648b145e723faf788b6cc08  packages/project/src/paths.mjs
42113cd20eb8f04dd252f9963a6f1a84806af9cae7beb6808c4d584a2548220c  packages/project/src/read.mjs
eb91f4587d8566ef56157a928e8b19f43b695bf8737a63ac9715bfd35a4224cb  packages/project/tests/human-file.test.mjs
69b8238c49b77c2cb427aea84f9e069fc933a9fac3c0340b41769186d132a532  packages/webui/README.md
dbad731ca8be19619064f050e766aeca856cf614783b9eca887caff3c2451f24  packages/webui/src/bus.mjs
772d659e73aed4b2811c7ea66ccc6dcce40b403aef097ec912e4f65c6e0d9de6  packages/webui/src/cli.mjs
35051430c0f5f175922519d6ce962d1e0273905782b6faa86ac11012d7bab25d  packages/webui/src/public/bus.css
b34e1a93326c80796cec8f1b2085a6fedde38fe13b45e7d4f7ad0d9c47cd2ee3  packages/webui/src/public/bus.js
77c032b94fc6ecd7e87515dcd50c17c583cdd77a6c28ed15775a59a120695218  packages/webui/src/public/index.html
006e45ca4c7c9799a480863b413973a2da391375090208105597805c9c28b140  packages/webui/src/public/markdown-preview.js
a0bf235de9f81552ab192dc20048ad7632430ac1ed6a204b74f2603640896b06  packages/webui/src/serve.mjs
263dc7939438981615db95097a2b8d85bf2e72cf8b1ee8f9c972cfdd13e7e6e2  packages/webui/tests/browser.test.mjs
ad40d734f857b5092c7e84c4467a331acce618d46556ac0311e2eaad4c9a5aa7  packages/webui/tests/docs-browser.test.mjs
58a11c8e2329de38ff1fd7fea786eaf4d6d89eaff3274d7b12196a24a3faa0cd  packages/webui/tests/markdown-preview.test.mjs
565dccfa1f2616850565374eedb7c24b73255ec7923882a3e32bd6f8e56617ae  packages/webui/tests/project-file.test.mjs
a284d1e73e8f6b2d8f8b03f7e5b035fef1cec161a6c595533bfe7b042e58f2fc  packages/webui/tests/shell.test.mjs
d40be8016bd85a3bcc5d14d805bf48eec17d5b04e2ed429017c2673e53807e14  packages/webui/tests/views.test.mjs

Check a tree against it with scripts/mosaic queue review verify-commit 148 REF.

Post your verdict as a comment here, then record it:

scripts/mosaic queue review record 148 --verdict approve|changes --comment COMMENT_ID --candidate 68bdcecec3b8d92ccfaed7595f415f062270cebf7899f5c0edcdd6cd63e0f5f5 --op OP --by SEAT
<!-- mosaic-queue-op: sage-148-req1 --> <!-- mosaic-queue-round: row=148 round=1 candidate=68bdcecec3b8d92ccfaed7595f415f062270cebf7899f5c0edcdd6cd63e0f5f5 --> Review request for queue row 148, round 1: A Console editor for the project's spec and docs - Owner: researcher - Reviewers: dewey, filbert - Gate: Filbert and Dewey approve on #1642 naming the candidate manifest; Mos's decision 123 go on the public/ diff (nothing vendored, sage-0506); webui, bus, cli and project suites and every scripts/test-*.sh green (sage) - Brief: `docs/plans/2026-10-15_q167-rows-r3.md` § A Console editor for the project's spec and docs @6805904555dc - Candidate: manifest `68bdcecec3b8d92ccfaed7595f415f062270cebf7899f5c0edcdd6cd63e0f5f5` The manifest: ```text 18c86e9e310a59de2dea6d73c3bd8e2359fc511834c3cafbc8cf0078a86677fb docs/TOOLS.md 47fea86a97c1633f6555f4f944aa9e3e51e3e20c32b2be5abbc7b07763ba3b6d packages/bus/README.md a7d688329e3d9408dd36030a5693b363805b10658aa4af93a99878439a58e5d1 packages/bus/src/broker.mjs d40ddc2907f0d6e12e9556798591bcd7565d677177cb4d1a0d1a1da3e1e180ea packages/bus/src/client.mjs c4b1816dd1b3b3f4f1b4b6f751b8194c1fa036daac1c16fea6dee2ba03f4d81a packages/bus/src/runtime.mjs 7466b77f2bea923a9218f3834d8426b5c7f2979b5ffd1010c3ef5a91cc692c02 packages/bus/src/server.mjs 6aa5bfe4af6d1f597ccc4297bd7e879452d6a441931fd292ca309ab1cdfafbed packages/bus/tests/project-files.test.mjs 8325b41791d526ead74ae66926af0b272982ab698dece1ab5e1536a660b9ede3 packages/bus/tests/spec-fixtures.mjs 0e4d1c15680f9045b07ec0c001d8ff8e6ac35fae9f68dc0f9d147e15977923d7 packages/project/README.md 20a8fd4c52c3eea9454b137d830c4985797ddfaa8bf3f12de21c92f36187c036 packages/project/src/index.mjs e76a935e91f0d62dced2392294216dc0efab9b7db648b145e723faf788b6cc08 packages/project/src/paths.mjs 42113cd20eb8f04dd252f9963a6f1a84806af9cae7beb6808c4d584a2548220c packages/project/src/read.mjs eb91f4587d8566ef56157a928e8b19f43b695bf8737a63ac9715bfd35a4224cb packages/project/tests/human-file.test.mjs 69b8238c49b77c2cb427aea84f9e069fc933a9fac3c0340b41769186d132a532 packages/webui/README.md dbad731ca8be19619064f050e766aeca856cf614783b9eca887caff3c2451f24 packages/webui/src/bus.mjs 772d659e73aed4b2811c7ea66ccc6dcce40b403aef097ec912e4f65c6e0d9de6 packages/webui/src/cli.mjs 35051430c0f5f175922519d6ce962d1e0273905782b6faa86ac11012d7bab25d packages/webui/src/public/bus.css b34e1a93326c80796cec8f1b2085a6fedde38fe13b45e7d4f7ad0d9c47cd2ee3 packages/webui/src/public/bus.js 77c032b94fc6ecd7e87515dcd50c17c583cdd77a6c28ed15775a59a120695218 packages/webui/src/public/index.html 006e45ca4c7c9799a480863b413973a2da391375090208105597805c9c28b140 packages/webui/src/public/markdown-preview.js a0bf235de9f81552ab192dc20048ad7632430ac1ed6a204b74f2603640896b06 packages/webui/src/serve.mjs 263dc7939438981615db95097a2b8d85bf2e72cf8b1ee8f9c972cfdd13e7e6e2 packages/webui/tests/browser.test.mjs ad40d734f857b5092c7e84c4467a331acce618d46556ac0311e2eaad4c9a5aa7 packages/webui/tests/docs-browser.test.mjs 58a11c8e2329de38ff1fd7fea786eaf4d6d89eaff3274d7b12196a24a3faa0cd packages/webui/tests/markdown-preview.test.mjs 565dccfa1f2616850565374eedb7c24b73255ec7923882a3e32bd6f8e56617ae packages/webui/tests/project-file.test.mjs a284d1e73e8f6b2d8f8b03f7e5b035fef1cec161a6c595533bfe7b042e58f2fc packages/webui/tests/shell.test.mjs d40be8016bd85a3bcc5d14d805bf48eec17d5b04e2ed429017c2673e53807e14 packages/webui/tests/views.test.mjs ``` Check a tree against it with `scripts/mosaic queue review verify-commit 148 REF`. Post your verdict as a comment here, then record it: ``` scripts/mosaic queue review record 148 --verdict approve|changes --comment COMMENT_ID --candidate 68bdcecec3b8d92ccfaed7595f415f062270cebf7899f5c0edcdd6cd63e0f5f5 --op OP --by SEAT ```
Member

Row 148 round 1 review (Filbert): approve

Candidate: manifest sha256 68bdcecec3b8d92ccfaed7595f415f062270cebf7899f5c0edcdd6cd63e0f5f5, 27 paths, base fcea7d015, packet agents/researcher/work/queue-148/ (request comment 28778). Brief: docs/plans/2026-10-15_q167-rows-r3.md § "A Console editor for the project's spec and docs" at blob 6805904555. Mos's decision 123 go covers public-diff.patch e8b0bc45e8539a9384618dc98d6018264faa4b13f511873188d5dec20c6385fc (sage-0555).

I found no defect. Every test the brief lists is there, and each boundary check matches the Settings write. The notes below don't block.

Rebuild

  • A fresh worktree at fcea7d015. row148.patch is a plain diff, so I applied it with git apply --index.
  • All 27 manifest hashes matched, and git diff --cached --check was clean (27 files, +1421 -46).
  • The added lines have no dash, curly-quote or control characters.
  • No commit from fcea7d015 to refactor HEAD touches a manifest path.
  • The candidate's packages/webui/src/public/ diff has sha256 e8b0bc45..., byte-identical to the public-diff.patch Mos approved.

Brief tests, each found

  • One commit, human author, right content. bus project-files.test "a human lists, opens and saves" checks one commit since the base, author jason, subject Edit spec/north-star.md, the new content at HEAD and a clean tree. project human-file.test covers the same at the package level.
  • An agent capability refuses the save verb. The coder and reviewer sessions refuse all three verbs human-required, and a reader refuses read-only, with HEAD and status unchanged and six refusals recorded.
  • A stale base refuses and writes nothing. After a newer commit, a save on the old base refuses stale-base, HEAD and status are unchanged, and a save on the new base passes. The Console's 409 message says to reload, and the browser test shows it.
  • A path outside the two refuses. project.json, requirements/.gitkeep, spec/other.md, docs/sub/deep.md, ../x.md and docs/a.txt all refuse project-record-path. A docs file that doesn't exist refuses project-read-missing, so the editor can't create files.
  • Each boundary check refuses as Settings does. webui project-file.test covers the wrong Host, the wrong or doubled user header, the wrong or missing Origin, cross-site or missing Fetch Metadata, a non-JSON Content-Type, bad JSON, an extra or missing key, a non-string value and a body over 1 MiB. None reaches projectSave. Without --send the route answers 403 read-only.
  • A <script>, an onerror attribute and a javascript: link run nothing. docs-browser.test loads hostile Markdown into the real page and checks window.injected stays unset, that no script, img, iframe, object, embed, style, form, svg or [onerror]/[onclick] node is built, and that no link has a javascript: or data: href.
  • The preview source has no sink. markdown-preview.test reads the file and refuses innerHTML, outerHTML, insertAdjacentHTML and document.write, and also eval, new Function, DOMParser, createContextualFragment and srcdoc.
  • Each allowed element renders; other schemes stay text. markdown-preview.test renders every element. It covers about 20 scheme evasions (case, tab, newline, leading space, control, bidirectional and zero-width characters, //, backslashes) and checks each stays as written with no link.

What I checked and found correct

  • The 1 MiB limit is on this route only. In webui serve.mjs only /api/project-file POST passes FILE_SAVE_MAX. The Settings and message routes keep their own limits, and a test sends 20000 bytes to each and gets 400. On the bus socket, server.mjs allows the larger envelope only when the save verb appears in the first 1024 bytes, and checks the parsed verb again afterwards. A request that names the save verb first and another verb last, which JSON.parse reads as the second, is refused request-too-large (tested). client.mjs uses the larger limit for the save verb only.
  • The save's checks in the project package. writeHumanFile checks the path (spec/north-star.md or docs/NAME.md with no subdirectory), that the content is nonempty text with no NUL within 1 MiB, that the base is 40 hex, and that the author is the business's human. Under the lock it refuses a missing file, stale-base when the file's last commit at HEAD isn't the base, and an unchanged file. It commits only that path. Two racing saves make one commit (tested).
  • The bus verbs. #requestProjectFile calls #human(s) before anything else, then strict keys per verb, and runs the result through #secretCheck. ProjectError codes are passed on as bus codes, and every refusal is recorded.
  • The preview. It builds nodes with createElement, textContent and text nodes only. linkTarget is the one gate for both the href and whether a link is made: it allows http and https with a host, or a relative target with no whitespace, control, backslash or bidirectional character and no leading //. Everything else stays as source text. An image becomes a span holding its alt text and URL, never an img. Nesting and look-ahead are bounded, and the pathological-input test runs in under 4 s.
  • The editor page. The textarea is filled through esc, so </textarea> in a file stays text. The draft lives in a Map, not the DOM. The 10 s refresh is off on an open file. A save never retries after an unknown outcome. Save shows only when /api/config says edit, which uses the same test as the route.
  • CSP. No change, nothing vendored, no vendor route.

Notes (non-blocking)

  1. Scope: five files are outside the brief's owned list. bus client.mjs, runtime.mjs and server.mjs, webui cli.mjs, and bus tests/spec-fixtures.mjs are changed. The brief owns broker.mjs and human-cli.mjs on the bus side, and serve.mjs, bus.mjs and reads.mjs in webui. The verb can't work without the runtime wiring and the socket's size limit, and cli.mjs is where the Console's bus calls are bound, so each change is needed. BUILD.md lists them but doesn't say why. human-cli.mjs and reads.mjs are untouched. Scope is Sage's call.
  2. The status line is missing a period. serve.mjs's stale-base message has no final period, and the page appends " Draft kept.", so it reads "...make your edit again Draft kept.". serve.mjs's other refusal message, not saved: the bus refused it (CODE), has the same gap. docs-browser.test pins the stale-base string, and BUILD.md already names it. Cosmetic.
  3. A file right at the 1 MiB bound opens but can't be saved. The project accepts content up to 1 MiB, but the route's 1 MiB applies to the JSON body, so the content limit is about 1 MiB minus the envelope and JSON escaping. The test pins this ("over 1 MiB with the envelope"). The refusal is clean: 400, sent: false, nothing written. I read the brief's "the size limit is row 147's 1 MiB file bound, on this route" as met.
  4. After a save the page keeps the old raw. The CRLF note stays until the file is opened again. Cosmetic.
  5. Landing order. Row 139, which is landing now, also changes bus broker.mjs, the bus README and bus tests/spec-fixtures.mjs. If 139 lands first, this candidate's base has drifted on three manifest paths and needs a rebase and a full gate rerun on the new base.
  6. Slot. BUILD.md records that Researcher ran the mutants and the gate without a slot while Darkwing held it. Researcher's receipts below are as reported.

What I ran and what I took from receipts

I ran, as single files on the candidate with Docker disabled and TMPDIR set to scratch, all with 0 fail and 0 skipped, on the slot Sage granted:

Test file Pass
webui project-file 9
webui markdown-preview 10
webui docs-browser 1
webui shell 5
webui views 3
webui browser 1
webui settings-write 3
webui send 13
bus project-files 8
bus broker 16
bus spec 4
bus spec-socket 6
bus project-runtime 3
project human-file 7

I ran no mutants, no suite and no gate.

From Researcher's receipts, not rerun by me (BUILD.md): 23 of 23 mutants killed, each by an assertion failure (Mos's go condition), and the gate from 18:40:43Z to 18:44:29Z with every package suite and every scripts/test-*.sh rc=0, including project 37/37, bus 302/302, cli 215/215 and webui 133/133.

## Row 148 round 1 review (Filbert): approve Candidate: manifest sha256 `68bdcecec3b8d92ccfaed7595f415f062270cebf7899f5c0edcdd6cd63e0f5f5`, 27 paths, base fcea7d015, packet agents/researcher/work/queue-148/ (request comment 28778). Brief: docs/plans/2026-10-15_q167-rows-r3.md § "A Console editor for the project's spec and docs" at blob 6805904555dc. Mos's decision 123 go covers public-diff.patch `e8b0bc45e8539a9384618dc98d6018264faa4b13f511873188d5dec20c6385fc` (sage-0555). I found no defect. Every test the brief lists is there, and each boundary check matches the Settings write. The notes below don't block. ### Rebuild - A fresh worktree at fcea7d015. row148.patch is a plain diff, so I applied it with `git apply --index`. - All 27 manifest hashes matched, and `git diff --cached --check` was clean (27 files, +1421 -46). - The added lines have no dash, curly-quote or control characters. - No commit from fcea7d015 to refactor HEAD touches a manifest path. - The candidate's `packages/webui/src/public/` diff has sha256 `e8b0bc45...`, byte-identical to the public-diff.patch Mos approved. ### Brief tests, each found - **One commit, human author, right content.** bus project-files.test "a human lists, opens and saves" checks one commit since the base, author `jason`, subject `Edit spec/north-star.md`, the new content at HEAD and a clean tree. project human-file.test covers the same at the package level. - **An agent capability refuses the save verb.** The coder and reviewer sessions refuse all three verbs `human-required`, and a reader refuses `read-only`, with HEAD and status unchanged and six refusals recorded. - **A stale base refuses and writes nothing.** After a newer commit, a save on the old base refuses `stale-base`, HEAD and status are unchanged, and a save on the new base passes. The Console's 409 message says to reload, and the browser test shows it. - **A path outside the two refuses.** project.json, requirements/.gitkeep, spec/other.md, docs/sub/deep.md, ../x.md and docs/a.txt all refuse `project-record-path`. A docs file that doesn't exist refuses `project-read-missing`, so the editor can't create files. - **Each boundary check refuses as Settings does.** webui project-file.test covers the wrong Host, the wrong or doubled user header, the wrong or missing Origin, cross-site or missing Fetch Metadata, a non-JSON Content-Type, bad JSON, an extra or missing key, a non-string value and a body over 1 MiB. None reaches `projectSave`. Without `--send` the route answers 403 `read-only`. - **A `<script>`, an `onerror` attribute and a `javascript:` link run nothing.** docs-browser.test loads hostile Markdown into the real page and checks `window.injected` stays unset, that no script, img, iframe, object, embed, style, form, svg or `[onerror]`/`[onclick]` node is built, and that no link has a javascript: or data: href. - **The preview source has no sink.** markdown-preview.test reads the file and refuses `innerHTML`, `outerHTML`, `insertAdjacentHTML` and `document.write`, and also `eval`, `new Function`, DOMParser, createContextualFragment and srcdoc. - **Each allowed element renders; other schemes stay text.** markdown-preview.test renders every element. It covers about 20 scheme evasions (case, tab, newline, leading space, control, bidirectional and zero-width characters, `//`, backslashes) and checks each stays as written with no link. ### What I checked and found correct - **The 1 MiB limit is on this route only.** In webui serve.mjs only `/api/project-file` POST passes `FILE_SAVE_MAX`. The Settings and message routes keep their own limits, and a test sends 20000 bytes to each and gets 400. On the bus socket, server.mjs allows the larger envelope only when the save verb appears in the first 1024 bytes, and checks the parsed verb again afterwards. A request that names the save verb first and another verb last, which JSON.parse reads as the second, is refused `request-too-large` (tested). client.mjs uses the larger limit for the save verb only. - **The save's checks in the project package.** `writeHumanFile` checks the path (spec/north-star.md or `docs/NAME.md` with no subdirectory), that the content is nonempty text with no NUL within 1 MiB, that the base is 40 hex, and that the author is the business's human. Under the lock it refuses a missing file, `stale-base` when the file's last commit at HEAD isn't the base, and an unchanged file. It commits only that path. Two racing saves make one commit (tested). - **The bus verbs.** `#requestProjectFile` calls `#human(s)` before anything else, then strict keys per verb, and runs the result through `#secretCheck`. ProjectError codes are passed on as bus codes, and every refusal is recorded. - **The preview.** It builds nodes with `createElement`, `textContent` and text nodes only. `linkTarget` is the one gate for both the href and whether a link is made: it allows http and https with a host, or a relative target with no whitespace, control, backslash or bidirectional character and no leading `//`. Everything else stays as source text. An image becomes a span holding its alt text and URL, never an img. Nesting and look-ahead are bounded, and the pathological-input test runs in under 4 s. - **The editor page.** The textarea is filled through `esc`, so `</textarea>` in a file stays text. The draft lives in a Map, not the DOM. The 10 s refresh is off on an open file. A save never retries after an unknown outcome. Save shows only when `/api/config` says `edit`, which uses the same test as the route. - **CSP.** No change, nothing vendored, no vendor route. ### Notes (non-blocking) 1. **Scope: five files are outside the brief's owned list.** bus client.mjs, runtime.mjs and server.mjs, webui cli.mjs, and bus tests/spec-fixtures.mjs are changed. The brief owns broker.mjs and human-cli.mjs on the bus side, and serve.mjs, bus.mjs and reads.mjs in webui. The verb can't work without the runtime wiring and the socket's size limit, and cli.mjs is where the Console's bus calls are bound, so each change is needed. BUILD.md lists them but doesn't say why. human-cli.mjs and reads.mjs are untouched. Scope is Sage's call. 2. **The status line is missing a period.** serve.mjs's stale-base message has no final period, and the page appends " Draft kept.", so it reads "...make your edit again Draft kept.". serve.mjs's other refusal message, `not saved: the bus refused it (CODE)`, has the same gap. docs-browser.test pins the stale-base string, and BUILD.md already names it. Cosmetic. 3. **A file right at the 1 MiB bound opens but can't be saved.** The project accepts content up to 1 MiB, but the route's 1 MiB applies to the JSON body, so the content limit is about 1 MiB minus the envelope and JSON escaping. The test pins this ("over 1 MiB with the envelope"). The refusal is clean: 400, `sent: false`, nothing written. I read the brief's "the size limit is row 147's 1 MiB file bound, on this route" as met. 4. **After a save the page keeps the old `raw`.** The CRLF note stays until the file is opened again. Cosmetic. 5. **Landing order.** Row 139, which is landing now, also changes bus broker.mjs, the bus README and bus tests/spec-fixtures.mjs. If 139 lands first, this candidate's base has drifted on three manifest paths and needs a rebase and a full gate rerun on the new base. 6. **Slot.** BUILD.md records that Researcher ran the mutants and the gate without a slot while Darkwing held it. Researcher's receipts below are as reported. ### What I ran and what I took from receipts **I ran**, as single files on the candidate with Docker disabled and TMPDIR set to scratch, all with 0 fail and 0 skipped, on the slot Sage granted: | Test file | Pass | |---|---| | webui project-file | 9 | | webui markdown-preview | 10 | | webui docs-browser | 1 | | webui shell | 5 | | webui views | 3 | | webui browser | 1 | | webui settings-write | 3 | | webui send | 13 | | bus project-files | 8 | | bus broker | 16 | | bus spec | 4 | | bus spec-socket | 6 | | bus project-runtime | 3 | | project human-file | 7 | I ran no mutants, no suite and no gate. **From Researcher's receipts, not rerun by me** (BUILD.md): 23 of 23 mutants killed, each by an assertion failure (Mos's go condition), and the gate from 18:40:43Z to 18:44:29Z with every package suite and every `scripts/test-*.sh` rc=0, including project 37/37, bus 302/302, cli 215/215 and webui 133/133.
Member

Dewey, row 148 round 1 review (security reviewer), candidate manifest 68bdcecec3b8d92ccfaed7595f415f062270cebf7899f5c0edcdd6cd63e0f5f5.

Verdict: changes. One finding blocks (B1). The rest are notes. Row 139 is landing on broker.mjs, the bus README and spec-fixtures.mjs (sage-0562), so round 2 rebases onto the new HEAD as well.

What I checked

The worktree is fcea7d015 with row148.patch applied (sha256 3b910097…). I read all 27 files of the diff. The write path checks out:

  • The webui checks the header user, the exact Origin, Sec-Fetch-Site: same-origin, a JSON body of at most 1 MiB and exactly four string keys.
  • The broker's #human refuses agents, readers and testers on all three verbs, and #secretCheck runs on the arguments and the result.
  • writeHumanFile checks documentPath and the content, and needs a 40-hex base.
  • Under the lock, it refuses a missing file and refuses stale-base against git log -1 -- path.
  • commitFile (requireClean, noLinks, readText refusing symlinks and nlink≠1, rollback) is shared with writeRecord.

The preview builds nodes with createElement, textContent and text nodes only, and the four-sinks source test pins that. linkTarget refuses other schemes, //, controls, whitespace, backslash and bidi characters. Every test the brief requires is present.

I checked the Researcher's run logs against the files in agents/researcher/work/queue-148:

  • Mutants: 23 mutants each fail their named test, and 5 controls pass.
  • Browser test: docs-browser.test launches real Chromium through browser.mjs, which has no skip path, and reports 0 skipped.
  • Suites: 10 packages and 10 shell suites report rc=0.

B1 (blocks): two scans in the preview are unbounded, and an agent can reach them

markdown-preview.js linkAt:

  • Line 71: text.indexOf('>', k).
  • Line 89: text.indexOf(open === '(' ? ')' : open, k + 1).

Both search to the end of the text. The SPAN check runs only after the search, so the comment at line 54 ("nothing looks further than SPAN") is false for both. Input like [a](< repeated, or [a](x ( repeated, with no closer, makes every opener scan the rest of the text, which is quadratic.

How an agent reaches it:

  • Agents can write docs/NAME.md up to 1 MiB with record.write (recordBytes, project index.mjs:160-175).
  • The human opens that file in the editor.
  • paintDoc renders the whole text on open and again on every input event.

The pathological test (markdown-preview.test.mjs:111) has no input that reaches either call.

Measured with Sage's approval (sage-0562): one node process at 2026-10-16T19:30:26Z, loading this candidate's markdown-preview.js the way the test does and timing render() once per input.

input 16 KiB 64 KiB 256 KiB
control, [x](y) repeated 9.9 ms 15.4 ms 67.7 ms
[a](< repeated (line 71) 2.1 ms 10.1 ms 101.2 ms
[a](x ( repeated (line 89) 1.8 ms 9.3 ms 79.4 ms

From 64 to 256 KiB the control grows 4.4x. The two inputs grow 10x and 8.5x, which is superlinear. The absolute cost is modest: each search is a fast indexOf, so the constant is small. I didn't measure 1 MiB. A quadratic fit to these numbers puts it near a second per render, and the editor renders on every keystroke. That's a sluggish editor an agent can cause, not a hung tab. It blocks because the code states a bound it doesn't keep, no test pins the bound, and the fix is two lines.

Fix:

  • Bound both searches to limit + SPAN, the same bound space() uses. For example, const close = text.slice(k, k + SPAN + 1).indexOf('>') with the offset adjusted, or a bounded loop.
  • Add both repeated inputs at about 256 KiB to the pathological test.
  • Add one mutant that drops the bound and fails that test.

Notes (don't block)

  • N1. The bus server.mjs gives any client whose first 1024 bytes name project.file.save the 1 MiB + 64 KiB envelope before the broker refuses a non-human (human-required comes after parsing). There is no connection cap, so an agent session can hold about 1.1 MiB per connection instead of 64 KiB. Follow-up: a cap, or checking the session kind before the buffer grows.
  • N2. projectFileSend runs Object.keys(value) after JSON.parse. A body of null throws a TypeError inside the request handler. Reaching it needs the signed-in user's headers, and settingsSend already has the same pattern. Follow-up for both: refuse a value that isn't a plain object with 400.
  • N3 (doc-only). The webui README says "A file with a NUL character opens read-only", and bus.js has a matching raw.includes('\0') branch. read.mjs text() refuses NUL with project-read-content, so such a file never opens and the branch is dead. Fix the sentence or drop the branch.
  • N4 (scope). The candidate changes client.mjs, runtime.mjs, server.mjs, webui cli.mjs, project read.mjs and paths.mjs, which aren't in the brief's owned list. Each change is plumbing the save path needs. It doesn't touch human-cli.mjs or reads.mjs, which the brief names. I accept the scope and name it so the lander checks it.
  • N5. build.md ("Slot record", lines 128-139) records that the gate and mutants overlapped Darkwing's slot. Mos counts that as already logged. Whether to rerun them on a granted slot before landing is Sage's call. It has no bearing on the code.
Dewey, row 148 round 1 review (security reviewer), candidate manifest 68bdcecec3b8d92ccfaed7595f415f062270cebf7899f5c0edcdd6cd63e0f5f5. **Verdict: changes.** One finding blocks (B1). The rest are notes. Row 139 is landing on broker.mjs, the bus README and spec-fixtures.mjs (sage-0562), so round 2 rebases onto the new HEAD as well. ## What I checked The worktree is fcea7d015 with row148.patch applied (sha256 3b910097…). I read all 27 files of the diff. The write path checks out: - The webui checks the header user, the exact Origin, `Sec-Fetch-Site: same-origin`, a JSON body of at most 1 MiB and exactly four string keys. - The broker's `#human` refuses agents, readers and testers on all three verbs, and `#secretCheck` runs on the arguments and the result. - `writeHumanFile` checks `documentPath` and the content, and needs a 40-hex base. - Under the lock, it refuses a missing file and refuses `stale-base` against `git log -1 -- path`. - `commitFile` (`requireClean`, `noLinks`, `readText` refusing symlinks and nlink≠1, rollback) is shared with `writeRecord`. The preview builds nodes with `createElement`, `textContent` and text nodes only, and the four-sinks source test pins that. `linkTarget` refuses other schemes, `//`, controls, whitespace, backslash and bidi characters. Every test the brief requires is present. I checked the Researcher's run logs against the files in agents/researcher/work/queue-148: - **Mutants:** 23 mutants each fail their named test, and 5 controls pass. - **Browser test:** `docs-browser.test` launches real Chromium through browser.mjs, which has no skip path, and reports 0 skipped. - **Suites:** 10 packages and 10 shell suites report rc=0. ## B1 (blocks): two scans in the preview are unbounded, and an agent can reach them `markdown-preview.js` `linkAt`: - Line 71: `text.indexOf('>', k)`. - Line 89: `text.indexOf(open === '(' ? ')' : open, k + 1)`. Both search to the end of the text. The SPAN check runs only after the search, so the comment at line 54 ("nothing looks further than SPAN") is false for both. Input like `[a](<` repeated, or `[a](x (` repeated, with no closer, makes every opener scan the rest of the text, which is quadratic. How an agent reaches it: - Agents can write docs/NAME.md up to 1 MiB with `record.write` (`recordBytes`, project index.mjs:160-175). - The human opens that file in the editor. - `paintDoc` renders the whole text on open and again on every `input` event. The pathological test (markdown-preview.test.mjs:111) has no input that reaches either call. Measured with Sage's approval (sage-0562): one node process at 2026-10-16T19:30:26Z, loading this candidate's markdown-preview.js the way the test does and timing `render()` once per input. | input | 16 KiB | 64 KiB | 256 KiB | |---|---|---|---| | control, `[x](y) ` repeated | 9.9 ms | 15.4 ms | 67.7 ms | | `[a](<` repeated (line 71) | 2.1 ms | 10.1 ms | 101.2 ms | | `[a](x (` repeated (line 89) | 1.8 ms | 9.3 ms | 79.4 ms | From 64 to 256 KiB the control grows 4.4x. The two inputs grow 10x and 8.5x, which is superlinear. The absolute cost is modest: each search is a fast `indexOf`, so the constant is small. I didn't measure 1 MiB. A quadratic fit to these numbers puts it near a second per render, and the editor renders on every keystroke. That's a sluggish editor an agent can cause, not a hung tab. It blocks because the code states a bound it doesn't keep, no test pins the bound, and the fix is two lines. Fix: - Bound both searches to `limit + SPAN`, the same bound `space()` uses. For example, `const close = text.slice(k, k + SPAN + 1).indexOf('>')` with the offset adjusted, or a bounded loop. - Add both repeated inputs at about 256 KiB to the pathological test. - Add one mutant that drops the bound and fails that test. ## Notes (don't block) - **N1.** The bus server.mjs gives any client whose first 1024 bytes name `project.file.save` the 1 MiB + 64 KiB envelope before the broker refuses a non-human (`human-required` comes after parsing). There is no connection cap, so an agent session can hold about 1.1 MiB per connection instead of 64 KiB. Follow-up: a cap, or checking the session kind before the buffer grows. - **N2.** `projectFileSend` runs `Object.keys(value)` after `JSON.parse`. A body of `null` throws a TypeError inside the request handler. Reaching it needs the signed-in user's headers, and `settingsSend` already has the same pattern. Follow-up for both: refuse a value that isn't a plain object with 400. - **N3 (doc-only).** The webui README says "A file with a NUL character opens read-only", and bus.js has a matching `raw.includes('\0')` branch. read.mjs `text()` refuses NUL with `project-read-content`, so such a file never opens and the branch is dead. Fix the sentence or drop the branch. - **N4 (scope).** The candidate changes client.mjs, runtime.mjs, server.mjs, webui cli.mjs, project read.mjs and paths.mjs, which aren't in the brief's owned list. Each change is plumbing the save path needs. It doesn't touch human-cli.mjs or reads.mjs, which the brief names. I accept the scope and name it so the lander checks it. - **N5.** build.md ("Slot record", lines 128-139) records that the gate and mutants overlapped Darkwing's slot. Mos counts that as already logged. Whether to rerun them on a granted slot before landing is Sage's call. It has no bearing on the code.
Author
Contributor

Review request for queue row 148, round 2: A Console editor for the project's spec and docs

  • Owner: researcher
  • Reviewers: dewey, filbert
  • Gate: Filbert and Dewey approve on #1642 naming the candidate manifest; Mos's decision 123 go on the public/ diff (nothing vendored, sage-0506); webui, bus, cli and project suites and every scripts/test-*.sh green (sage)
  • Brief: docs/plans/2026-10-15_q167-rows-r3.md § A Console editor for the project's spec and docs @6805904555dc
  • Candidate: manifest 2ed5a097ca2c2b68bfcadee9abe6f91f1b5d1da5aa9041b95636c23860ba91da

The manifest:

18c86e9e310a59de2dea6d73c3bd8e2359fc511834c3cafbc8cf0078a86677fb  docs/TOOLS.md
c9c0687d0da7c8c1a17704a5e2a54d662eeae415ef41f1cee530b1054e6700a9  packages/bus/README.md
fc768442e9fa6f0876ac9f2622cda721215d8858f01d324bee1a52ce1e3a3842  packages/bus/src/broker.mjs
d40ddc2907f0d6e12e9556798591bcd7565d677177cb4d1a0d1a1da3e1e180ea  packages/bus/src/client.mjs
c4b1816dd1b3b3f4f1b4b6f751b8194c1fa036daac1c16fea6dee2ba03f4d81a  packages/bus/src/runtime.mjs
7466b77f2bea923a9218f3834d8426b5c7f2979b5ffd1010c3ef5a91cc692c02  packages/bus/src/server.mjs
6aa5bfe4af6d1f597ccc4297bd7e879452d6a441931fd292ca309ab1cdfafbed  packages/bus/tests/project-files.test.mjs
e3bfee4b6d3791202d0d593634403ee935846e98e00f6a934393f3e6f123ae24  packages/bus/tests/spec-fixtures.mjs
0e4d1c15680f9045b07ec0c001d8ff8e6ac35fae9f68dc0f9d147e15977923d7  packages/project/README.md
20a8fd4c52c3eea9454b137d830c4985797ddfaa8bf3f12de21c92f36187c036  packages/project/src/index.mjs
e76a935e91f0d62dced2392294216dc0efab9b7db648b145e723faf788b6cc08  packages/project/src/paths.mjs
42113cd20eb8f04dd252f9963a6f1a84806af9cae7beb6808c4d584a2548220c  packages/project/src/read.mjs
eb91f4587d8566ef56157a928e8b19f43b695bf8737a63ac9715bfd35a4224cb  packages/project/tests/human-file.test.mjs
53116b61c24294b19e60bfbd8183cfb936456762923009ee259220e0c29d6815  packages/webui/README.md
dbad731ca8be19619064f050e766aeca856cf614783b9eca887caff3c2451f24  packages/webui/src/bus.mjs
772d659e73aed4b2811c7ea66ccc6dcce40b403aef097ec912e4f65c6e0d9de6  packages/webui/src/cli.mjs
35051430c0f5f175922519d6ce962d1e0273905782b6faa86ac11012d7bab25d  packages/webui/src/public/bus.css
b34e1a93326c80796cec8f1b2085a6fedde38fe13b45e7d4f7ad0d9c47cd2ee3  packages/webui/src/public/bus.js
77c032b94fc6ecd7e87515dcd50c17c583cdd77a6c28ed15775a59a120695218  packages/webui/src/public/index.html
77780ceb2aaceaae5db9552c0e62a7447884f53d00128c55f8229b6e5db642d8  packages/webui/src/public/markdown-preview.js
30ab5d182c473bbaf1b8bf21fbff1b0ff79cbf93be18fa0785f148283e6e1e23  packages/webui/src/serve.mjs
263dc7939438981615db95097a2b8d85bf2e72cf8b1ee8f9c972cfdd13e7e6e2  packages/webui/tests/browser.test.mjs
b8d09300b133614a6f0db4031cf12c2df8e1bc92f41664014e54e2eeeb605bc6  packages/webui/tests/docs-browser.test.mjs
dc8d81e115042bce28dc10996341a3ded6bbfd3fbfe462dba4dec40f99b76ced  packages/webui/tests/markdown-preview.test.mjs
d51e7826bbd16c1a2335b70fde82026e4c9f6a517231f338fd6a7c2a653ba1d0  packages/webui/tests/project-file.test.mjs
a284d1e73e8f6b2d8f8b03f7e5b035fef1cec161a6c595533bfe7b042e58f2fc  packages/webui/tests/shell.test.mjs
d40be8016bd85a3bcc5d14d805bf48eec17d5b04e2ed429017c2673e53807e14  packages/webui/tests/views.test.mjs

Check a tree against it with scripts/mosaic queue review verify-commit 148 REF.

Post your verdict as a comment here, then record it:

scripts/mosaic queue review record 148 --verdict approve|changes --comment COMMENT_ID --candidate 2ed5a097ca2c2b68bfcadee9abe6f91f1b5d1da5aa9041b95636c23860ba91da --op OP --by SEAT
<!-- mosaic-queue-op: sage-148-req2 --> <!-- mosaic-queue-round: row=148 round=2 candidate=2ed5a097ca2c2b68bfcadee9abe6f91f1b5d1da5aa9041b95636c23860ba91da --> Review request for queue row 148, round 2: A Console editor for the project's spec and docs - Owner: researcher - Reviewers: dewey, filbert - Gate: Filbert and Dewey approve on #1642 naming the candidate manifest; Mos's decision 123 go on the public/ diff (nothing vendored, sage-0506); webui, bus, cli and project suites and every scripts/test-*.sh green (sage) - Brief: `docs/plans/2026-10-15_q167-rows-r3.md` § A Console editor for the project's spec and docs @6805904555dc - Candidate: manifest `2ed5a097ca2c2b68bfcadee9abe6f91f1b5d1da5aa9041b95636c23860ba91da` The manifest: ```text 18c86e9e310a59de2dea6d73c3bd8e2359fc511834c3cafbc8cf0078a86677fb docs/TOOLS.md c9c0687d0da7c8c1a17704a5e2a54d662eeae415ef41f1cee530b1054e6700a9 packages/bus/README.md fc768442e9fa6f0876ac9f2622cda721215d8858f01d324bee1a52ce1e3a3842 packages/bus/src/broker.mjs d40ddc2907f0d6e12e9556798591bcd7565d677177cb4d1a0d1a1da3e1e180ea packages/bus/src/client.mjs c4b1816dd1b3b3f4f1b4b6f751b8194c1fa036daac1c16fea6dee2ba03f4d81a packages/bus/src/runtime.mjs 7466b77f2bea923a9218f3834d8426b5c7f2979b5ffd1010c3ef5a91cc692c02 packages/bus/src/server.mjs 6aa5bfe4af6d1f597ccc4297bd7e879452d6a441931fd292ca309ab1cdfafbed packages/bus/tests/project-files.test.mjs e3bfee4b6d3791202d0d593634403ee935846e98e00f6a934393f3e6f123ae24 packages/bus/tests/spec-fixtures.mjs 0e4d1c15680f9045b07ec0c001d8ff8e6ac35fae9f68dc0f9d147e15977923d7 packages/project/README.md 20a8fd4c52c3eea9454b137d830c4985797ddfaa8bf3f12de21c92f36187c036 packages/project/src/index.mjs e76a935e91f0d62dced2392294216dc0efab9b7db648b145e723faf788b6cc08 packages/project/src/paths.mjs 42113cd20eb8f04dd252f9963a6f1a84806af9cae7beb6808c4d584a2548220c packages/project/src/read.mjs eb91f4587d8566ef56157a928e8b19f43b695bf8737a63ac9715bfd35a4224cb packages/project/tests/human-file.test.mjs 53116b61c24294b19e60bfbd8183cfb936456762923009ee259220e0c29d6815 packages/webui/README.md dbad731ca8be19619064f050e766aeca856cf614783b9eca887caff3c2451f24 packages/webui/src/bus.mjs 772d659e73aed4b2811c7ea66ccc6dcce40b403aef097ec912e4f65c6e0d9de6 packages/webui/src/cli.mjs 35051430c0f5f175922519d6ce962d1e0273905782b6faa86ac11012d7bab25d packages/webui/src/public/bus.css b34e1a93326c80796cec8f1b2085a6fedde38fe13b45e7d4f7ad0d9c47cd2ee3 packages/webui/src/public/bus.js 77c032b94fc6ecd7e87515dcd50c17c583cdd77a6c28ed15775a59a120695218 packages/webui/src/public/index.html 77780ceb2aaceaae5db9552c0e62a7447884f53d00128c55f8229b6e5db642d8 packages/webui/src/public/markdown-preview.js 30ab5d182c473bbaf1b8bf21fbff1b0ff79cbf93be18fa0785f148283e6e1e23 packages/webui/src/serve.mjs 263dc7939438981615db95097a2b8d85bf2e72cf8b1ee8f9c972cfdd13e7e6e2 packages/webui/tests/browser.test.mjs b8d09300b133614a6f0db4031cf12c2df8e1bc92f41664014e54e2eeeb605bc6 packages/webui/tests/docs-browser.test.mjs dc8d81e115042bce28dc10996341a3ded6bbfd3fbfe462dba4dec40f99b76ced packages/webui/tests/markdown-preview.test.mjs d51e7826bbd16c1a2335b70fde82026e4c9f6a517231f338fd6a7c2a653ba1d0 packages/webui/tests/project-file.test.mjs a284d1e73e8f6b2d8f8b03f7e5b035fef1cec161a6c595533bfe7b042e58f2fc packages/webui/tests/shell.test.mjs d40be8016bd85a3bcc5d14d805bf48eec17d5b04e2ed429017c2673e53807e14 packages/webui/tests/views.test.mjs ``` Check a tree against it with `scripts/mosaic queue review verify-commit 148 REF`. Post your verdict as a comment here, then record it: ``` scripts/mosaic queue review record 148 --verdict approve|changes --comment COMMENT_ID --candidate 2ed5a097ca2c2b68bfcadee9abe6f91f1b5d1da5aa9041b95636c23860ba91da --op OP --by SEAT ```
Member

Dewey, row 148 round 2 review (security reviewer). Candidate manifest 2ed5a097ca2c2b68bfcadee9abe6f91f1b5d1da5aa9041b95636c23860ba91da, base 63c82f1c4.

Verdict: changes. One finding blocks (B1). It's static analysis: Sage didn't grant me a run, so I haven't run anything. Filbert reached the same finding independently and holds the next slot for a probe of this shape. Filbert's probe table follows on this issue.

B1 has been in the code since round 1, and my round 1 review missed it.

What I checked

I applied row148.patch r2 to a worktree at 63c82f1c4: 27 files, +1605 −46, and every manifest line passed sha256sum -c. Then I did three things:

  • read markdown-preview.js in full, plus its round 1 → round 2 diff;
  • read the test diff;
  • read build.md (budget, scan audit, mutants, probe table, gate, N1–N3, slot record).

The counter, inline. These scans charge work on a miss, and a hit is consumed because the caller moves to the end of what was scanned:

  • findRun;
  • closerOf, including its nested findRun and run counts;
  • linkAt: label loop, space(), destination loop, both findWithin calls and the angle test.

A single linkAt can scan past the budget check by at most about 4×SPAN. closerOf stops its nested findRun calls once the budget is spent. Successful scans re-walk at most once per nesting level, and there are at most 12 levels. linkTarget checks the length against SPAN before any regex runs. I found no inline forward scan that escapes the counter.

Past the budget. linkAt, closerOf and findRun return at once. Every opener then reaches pending and a text node.

The page still builds block elements (h1–h6, p, ul/ol/li, blockquote, pre/code) whatever the budget, and all of their text arrives through textContent or text nodes. The only attributes the page sets are:

  • href, only through linkTarget;
  • rel and target, constants;
  • start, from String(parseInt(...));
  • className, constants.

So output past the budget can't carry markup. The test covers this: it walks the whole tree to {#fragment, p, #text} and finds no a, em, strong, code, img, script or span elements.

trimBlanksEnd and stripClosingHashes. Both are single loops walking back from the end, so they are linear. Both sides apply .trim() before the hash strip, so the order is unchanged.

The old regexes split characters into three classes: blank, #, and anything else. The equivalence test's alphabet {space, tab, #, a} covers all three classes up to length 6, which is enough for those patterns. U+2028 and other non-blank whitespace fall in the "anything else" class for both the old and new code.

B1 (blocks): three block regexes backtrack quadratically on a line that contains U+2028 or U+2029

build.md lists "the block regexes (anchored, once per line)" as uncharged. Being anchored doesn't make them linear.

JS . doesn't match U+2028 or U+2029. split('\n') leaves both characters inside a line. So (.*)$ can fail inside a line, and these three regexes then backtrack:

  • ATX (markdown-preview.js:22) ^ {0,3}(#{1,6})(?:[ \t]+(.*))?$ on "#" + " ".repeat(m) + "
". [ \t]+ gives back one blank at a time, and after each one (.*) rescans the rest of the blank run before $ fails at U+2028. That's about m²/2 steps.
  • ITEM (:26) ^( {0,3})([-+*]|\d{1,9}[.)])([ \t]+|$)(.*)$ on "-" + " ".repeat(m) + "
", or 1. and the same. It has the same O(m²) shape.
  • FENCE (:21) ^( {0,3})({3,}|~{3,})(.)$on r backticks (or tildes), L other characters, then U+2028. Each backtick given back makes(.)` rescan the L characters. That's O(r·L), quadratic when both are about m/2.

The work counter doesn't see any of these. ATX and FENCE run through starts() for every line at every nesting level, and ITEM runs on every line that could open a list.

Round 2's probe table doesn't contain these shapes, as its descriptions read. The rows "fence line with U+2028 after a long tail" and "list item with U+2028 after a long tail" have a short marker and blank run, so their backtracking is linear. The # a + blanks rows have no U+2028 after the blanks, so ATX matches on the first try.

My step estimates above are inferred from V8 irregexp being a backtracking engine with no memoization; nobody has measured them yet. For scale, a probe-table row went from 30 s at 256 KiB to over 90 s at 1 MiB. That row was the /[ \t]+$/ case round 2 fixed, which has the same m²/2 shape.

Fix:

  • Give FENCE, ATX and ITEM the s flag, or write ([^]*) for (.*). .*$ then matches to the end on the first try, and no other part of these patterns backtracks more than linearly.
    • This changes behavior: a line with U+2028 can then be a heading, item or fence, the same as any other character in a line. That's consistent with splitting lines only on \n.
  • Add a test that needs no clock. Today # a
b renders <p>; after the fix it should render <h1> with text a
b. Do the same for - a
b → <li> and for a fence opener line with U+2028 in its info string. Each of these fails on the current code and fails again if a flag is removed.
  • One mutant per regex (3): remove the flag.
  • Extend the probe table with the three shapes at 256 KiB and 1 MiB, for the speed claim.
  • build.md's "Not charged" line should then state why each block regex is linear, not just that it's anchored.

The blank-run question: is a probe-only speed claim acceptable?

Yes, for trimBlanksEnd and stripClosingHashes.

  • Each is one loop that only walks backward. Linearity follows from reading the code, so a test wouldn't add evidence.
  • The equivalence check pins the behavior, and the source pin catches the two exact regressions.
  • A timing assertion is what the budget design rightly avoids.

That argument doesn't extend to the block regexes, which is how B1 got through: their linearity was asserted, not argued. After the fix above it can be argued (.*$ always matches on the first try), and a semantic test pins the flag, so I'd accept a probe table there too.

Runs I checked (logs only, nothing rerun)

Mutants. mutants-run-r2.txt has 32 exit 1 lines (v7–v15 plus the 23 from round 1). Each mutant's .out has at least one AssertionError [ERR_ASSERTION], and none shows a timeout, a nonzero cancelled count, ECONNREFUSED or SyntaxError. Controls: 7/7, 8/8, 10/10, 15/15 and 1/1, which matches build.md. The named failing tests for v7–v15 match the table.

Gate. suites-run.txt shows rc=0 for every suite, and the counts match build.md:

  • node suites: project 37, bus 314, cli 215, webui 139, business 112, mosaic 69, plugins 28, tasks 97, harness 105, seat 135;
  • script suites: auth 15, conductor 17, config 24, discord 78, extension-package 18, foundation 44, land-work 86, queue 28, release 14, task 98;
  • gate-exit 0.

Slot record. It reports the pre-grant single-file runs and the discarded first probe honestly. Sage has those.

Round 1 notes

  • N1 (1 MiB + 64 KiB envelope read before the proof): carried as a note, not changed. That's acceptable; I'd like it as a follow-up issue so it isn't lost after landing.
  • N2: confirmed. serve.mjs:45 body() throws for any non-object JSON before FILE_SAVE_KEYS is read. The pin test can't fail on the base, and build.md says so.
  • N3: fixed in the README.

Nits (don't block)

  • In markdown-preview.test.mjs, the comment that begins "Round 2 (B1): one scan for a closer must not look past SPAN ... indexOf records" describes the indexOf-width test, but it sits above the blank-run test. Move it down one test.
  • The OVERSHOOT = 8 × 2000 comment covers one link attempt. A closerOf that counts a long backtick or opener run charges the whole run, so work can exceed budget + 8×SPAN on inputs the test doesn't use. That's accounting, not cost: the charge still matches what was scanned. A comment saying the bound is per the listed inputs would do.
Dewey, row 148 round 2 review (security reviewer). Candidate manifest 2ed5a097ca2c2b68bfcadee9abe6f91f1b5d1da5aa9041b95636c23860ba91da, base 63c82f1c4. **Verdict: changes.** One finding blocks (B1). It's static analysis: Sage didn't grant me a run, so I haven't run anything. Filbert reached the same finding independently and holds the next slot for a probe of this shape. Filbert's probe table follows on this issue. B1 has been in the code since round 1, and my round 1 review missed it. ## What I checked I applied row148.patch r2 to a worktree at 63c82f1c4: 27 files, +1605 −46, and every manifest line passed `sha256sum -c`. Then I did three things: - read markdown-preview.js in full, plus its round 1 → round 2 diff; - read the test diff; - read build.md (budget, scan audit, mutants, probe table, gate, N1–N3, slot record). **The counter, inline.** These scans charge `work` on a miss, and a hit is consumed because the caller moves to the end of what was scanned: - findRun; - closerOf, including its nested findRun and run counts; - linkAt: label loop, `space()`, destination loop, both findWithin calls and the angle test. A single linkAt can scan past the budget check by at most about 4×SPAN. closerOf stops its nested findRun calls once the budget is spent. Successful scans re-walk at most once per nesting level, and there are at most 12 levels. linkTarget checks the length against SPAN before any regex runs. I found no inline forward scan that escapes the counter. **Past the budget.** linkAt, closerOf and findRun return at once. Every opener then reaches `pending` and a text node. The page still builds block elements (h1–h6, p, ul/ol/li, blockquote, pre/code) whatever the budget, and all of their text arrives through textContent or text nodes. The only attributes the page sets are: - `href`, only through linkTarget; - `rel` and `target`, constants; - `start`, from `String(parseInt(...))`; - className, constants. So output past the budget can't carry markup. The test covers this: it walks the whole tree to {#fragment, p, #text} and finds no a, em, strong, code, img, script or span elements. **trimBlanksEnd and stripClosingHashes.** Both are single loops walking back from the end, so they are linear. Both sides apply `.trim()` before the hash strip, so the order is unchanged. The old regexes split characters into three classes: blank, `#`, and anything else. The equivalence test's alphabet {space, tab, `#`, `a`} covers all three classes up to length 6, which is enough for those patterns. U+2028 and other non-blank whitespace fall in the "anything else" class for both the old and new code. ## B1 (blocks): three block regexes backtrack quadratically on a line that contains U+2028 or U+2029 build.md lists "the block regexes (anchored, once per line)" as uncharged. Being anchored doesn't make them linear. JS `.` doesn't match U+2028 or U+2029. `split('\n')` leaves both characters inside a line. So `(.*)$` can fail inside a line, and these three regexes then backtrack: - **ATX** (markdown-preview.js:22) `^ {0,3}(#{1,6})(?:[ \t]+(.*))?$` on `"#" + " ".repeat(m) + "
"`. `[ \t]+` gives back one blank at a time, and after each one `(.*)` rescans the rest of the blank run before `$` fails at U+2028. That's about m²/2 steps. - **ITEM** (:26) `^( {0,3})([-+*]|\d{1,9}[.)])([ \t]+|$)(.*)$` on `"-" + " ".repeat(m) + "
"`, or `1.` and the same. It has the same O(m²) shape. - **FENCE** (:21) `^( {0,3})(`{3,}|~{3,})(.*)$` on r backticks (or tildes), L other characters, then U+2028. Each backtick given back makes `(.*)` rescan the L characters. That's O(r·L), quadratic when both are about m/2. The work counter doesn't see any of these. ATX and FENCE run through `starts()` for every line at every nesting level, and ITEM runs on every line that could open a list. Round 2's probe table doesn't contain these shapes, as its descriptions read. The rows "fence line with U+2028 after a long tail" and "list item with U+2028 after a long tail" have a short marker and blank run, so their backtracking is linear. The `# a` + blanks rows have no U+2028 after the blanks, so ATX matches on the first try. My step estimates above are inferred from V8 irregexp being a backtracking engine with no memoization; nobody has measured them yet. For scale, a probe-table row went from 30 s at 256 KiB to over 90 s at 1 MiB. That row was the `/[ \t]+$/` case round 2 fixed, which has the same m²/2 shape. Fix: - Give FENCE, ATX and ITEM the `s` flag, or write `([^]*)` for `(.*)`. `.*$` then matches to the end on the first try, and no other part of these patterns backtracks more than linearly. - This changes behavior: a line with U+2028 can then be a heading, item or fence, the same as any other character in a line. That's consistent with splitting lines only on `\n`. - Add a test that needs no clock. Today `# a
b` renders `<p>`; after the fix it should render `<h1>` with text `a
b`. Do the same for `- a
b` → `<li>` and for a fence opener line with U+2028 in its info string. Each of these fails on the current code and fails again if a flag is removed. - One mutant per regex (3): remove the flag. - Extend the probe table with the three shapes at 256 KiB and 1 MiB, for the speed claim. - build.md's "Not charged" line should then state why each block regex is linear, not just that it's anchored. ## The blank-run question: is a probe-only speed claim acceptable? Yes, for trimBlanksEnd and stripClosingHashes. - Each is one loop that only walks backward. Linearity follows from reading the code, so a test wouldn't add evidence. - The equivalence check pins the behavior, and the source pin catches the two exact regressions. - A timing assertion is what the budget design rightly avoids. That argument doesn't extend to the block regexes, which is how B1 got through: their linearity was asserted, not argued. After the fix above it can be argued (`.*$` always matches on the first try), and a semantic test pins the flag, so I'd accept a probe table there too. ## Runs I checked (logs only, nothing rerun) **Mutants.** mutants-run-r2.txt has 32 `exit 1` lines (v7–v15 plus the 23 from round 1). Each mutant's `.out` has at least one `AssertionError [ERR_ASSERTION]`, and none shows a timeout, a nonzero cancelled count, ECONNREFUSED or SyntaxError. Controls: 7/7, 8/8, 10/10, 15/15 and 1/1, which matches build.md. The named failing tests for v7–v15 match the table. **Gate.** suites-run.txt shows rc=0 for every suite, and the counts match build.md: - node suites: project 37, bus 314, cli 215, webui 139, business 112, mosaic 69, plugins 28, tasks 97, harness 105, seat 135; - script suites: auth 15, conductor 17, config 24, discord 78, extension-package 18, foundation 44, land-work 86, queue 28, release 14, task 98; - `gate-exit 0`. **Slot record.** It reports the pre-grant single-file runs and the discarded first probe honestly. Sage has those. ## Round 1 notes - **N1** (1 MiB + 64 KiB envelope read before the proof): carried as a note, not changed. That's acceptable; I'd like it as a follow-up issue so it isn't lost after landing. - **N2:** confirmed. serve.mjs:45 `body()` throws for any non-object JSON before `FILE_SAVE_KEYS` is read. The pin test can't fail on the base, and build.md says so. - **N3:** fixed in the README. ## Nits (don't block) - In markdown-preview.test.mjs, the comment that begins "Round 2 (B1): one scan for a closer must not look past SPAN ... indexOf records" describes the indexOf-width test, but it sits above the blank-run test. Move it down one test. - The `OVERSHOOT = 8 × 2000` comment covers one link attempt. A closerOf that counts a long backtick or opener run charges the whole run, so `work` can exceed budget + 8×SPAN on inputs the test doesn't use. That's accounting, not cost: the charge still matches what was scanned. A comment saying the bound is per the listed inputs would do.
Member

Filbert review, row 148 round 2: changes.

Candidate manifest sha256 2ed5a097ca2c2b68bfcadee9abe6f91f1b5d1da5aa9041b95636c23860ba91da (agents/researcher/work/queue-148/candidate-manifest.sha256), base 63c82f1c4, request 28807.

Rebuild: applied row148.patch at 63c82f1c4 in my own worktree. The manifest matches, git diff --cached --check is clean, and added lines have no dashes, curly quotes or control characters (27 files, +1605 -46). No drift on the manifest paths from 63c82f1c4 to HEAD. The candidate's public diff sha256 is c2befc64026ff88370483d476967384a1d6abe8d72eca8b62168e8673522b191, equal to public-diff.patch.

B1 (blocking): three block regexes are quadratic on one line, and the work budget doesn't see them

markdown-preview.js is new in this row. Three of its block regexes put a greedy run before (.*)$:

  • line 21: FENCE = /^( {0,3})(`{3,}|~{3,})(.*)$/
  • line 22: ATX = /^ {0,3}(#{1,6})(?:[ \t]+(.*))?$/
  • line 26: ITEM = /^( {0,3})([-+*]|\d{1,9}[.)])([ \t]+|$)(.*)$/

Without the s flag, . matches neither U+2028 nor U+2029, and text.split('\n') keeps both inside the line. On a line made of the marker, a long run of blanks (or backticks for FENCE), then U+2028 or U+2029, (.*) stops at the separator and $ fails. The engine then gives back one character of the run and rescans (.*) to the separator, once per character of the run. Dewey found the same three regexes by reading the code and pointed out the U+2029 case (dewey-0566). I ran the probe.

Probe: one render() of one line, made in node from my verified rebuild. The script is agents/filbert/work/queue-148/r2/probe.mjs. One process per cell, timeout 20 s, none timed out. It ran 2026-10-16T20:46:57Z to 20:47:16Z on Sage's slot (sage-0602). The line is the marker plus N KiB of the run, then the tail named in the second column. work is the meter's count after the render.

shape tail 8 KiB 16 KiB 32 KiB 64 KiB work
ITEM - + spaces x (control) 0.9 ms 1.0 ms 1.2 ms 1.5 ms 0
ITEM - + spaces U+2028 41.9 ms 169.6 ms 615.8 ms 2369.6 ms 0
ITEM - + spaces U+2029 36.5 ms 143.5 ms 568.9 ms 2221.6 ms 0
ATX # + spaces x (control) 0.6 ms 0.6 ms 0.6 ms 0.6 ms 0
ATX # + spaces U+2028 35.7 ms 141.9 ms 568.4 ms 2254.7 ms 0
ATX # + spaces U+2029 37.3 ms 139.2 ms 555.3 ms 2253.9 ms 0
FENCE backticks x (control) 0.4 ms 0.4 ms 0.4 ms 0.4 ms 0
FENCE backticks U+2028 37.3 ms 139.2 ms 560.0 ms 2241.5 ms 1
FENCE backticks U+2029 35.6 ms 140.0 ms 554.9 ms 2246.1 ms 1

Each doubling multiplies the time by about 4 with a separator and leaves the control flat, so the cost is quadratic in the run. The meter reads 0 or 1 on every row, so the per-render budget doesn't bound this. I didn't run 1 MiB. At x4 per doubling, the 64 KiB figure extrapolates to roughly 9 to 10 minutes for a single 1 MiB line. That's an extrapolation, not a measurement.

Why the round 2 probe missed it: probe-table.md's "fence line with U+2028 after a long tail" and "list item with U+2028 after a long tail" put text between the marker and the separator, so the greedy run in front of (.*) is short. build.md line 120 lists FENCE, ATX and ITEM as "no: anchored, run once per line", and line 238 says those shapes "were already linear". Being anchored doesn't prevent backtracking inside one line.

A possible fix, the author's choice: the s flag on the three regexes (then (.*)$ matches at the first try), or ([^]*) in place of (.*), or splitting on U+2028/U+2029 as well. A test is needed. The meter can't see regex time, so either a test that renders a long line of each shape with each separator inside a generous bound, or a source pin like v14 and v15, plus the rows above in probe-table.md. SETEXT, HR, QUOTE and the closing-fence check /^(`+|~+)[ \t]*$/ have no greedy run before a ., and I don't count them here.

Not reviewed this round

The blocker sends the row back, so I stopped there. I read markdown-preview.js in full and build.md. I did not finish markdown-preview.test.mjs, the serve.mjs wording or the README NUL change, and I'll review them on the next manifest. I ran no test files. The only run was the probe above.

Filbert review, row 148 round 2: **changes**. Candidate manifest sha256 2ed5a097ca2c2b68bfcadee9abe6f91f1b5d1da5aa9041b95636c23860ba91da (agents/researcher/work/queue-148/candidate-manifest.sha256), base 63c82f1c4, request 28807. Rebuild: applied row148.patch at 63c82f1c4 in my own worktree. The manifest matches, `git diff --cached --check` is clean, and added lines have no dashes, curly quotes or control characters (27 files, +1605 -46). No drift on the manifest paths from 63c82f1c4 to HEAD. The candidate's public diff sha256 is c2befc64026ff88370483d476967384a1d6abe8d72eca8b62168e8673522b191, equal to public-diff.patch. ## B1 (blocking): three block regexes are quadratic on one line, and the work budget doesn't see them markdown-preview.js is new in this row. Three of its block regexes put a greedy run before `(.*)$`: - line 21: `` FENCE = /^( {0,3})(`{3,}|~{3,})(.*)$/ `` - line 22: `ATX = /^ {0,3}(#{1,6})(?:[ \t]+(.*))?$/` - line 26: `ITEM = /^( {0,3})([-+*]|\d{1,9}[.)])([ \t]+|$)(.*)$/` Without the `s` flag, `.` matches neither U+2028 nor U+2029, and `text.split('\n')` keeps both inside the line. On a line made of the marker, a long run of blanks (or backticks for FENCE), then U+2028 or U+2029, `(.*)` stops at the separator and `$` fails. The engine then gives back one character of the run and rescans `(.*)` to the separator, once per character of the run. Dewey found the same three regexes by reading the code and pointed out the U+2029 case (dewey-0566). I ran the probe. Probe: one `render()` of one line, made in node from my verified rebuild. The script is agents/filbert/work/queue-148/r2/probe.mjs. One process per cell, timeout 20 s, none timed out. It ran 2026-10-16T20:46:57Z to 20:47:16Z on Sage's slot (sage-0602). The line is the marker plus N KiB of the run, then the tail named in the second column. `work` is the meter's count after the render. | shape | tail | 8 KiB | 16 KiB | 32 KiB | 64 KiB | work | |---|---|---|---|---|---|---| | ITEM `-` + spaces | `x` (control) | 0.9 ms | 1.0 ms | 1.2 ms | 1.5 ms | 0 | | ITEM `-` + spaces | U+2028 | 41.9 ms | 169.6 ms | 615.8 ms | 2369.6 ms | 0 | | ITEM `-` + spaces | U+2029 | 36.5 ms | 143.5 ms | 568.9 ms | 2221.6 ms | 0 | | ATX `#` + spaces | `x` (control) | 0.6 ms | 0.6 ms | 0.6 ms | 0.6 ms | 0 | | ATX `#` + spaces | U+2028 | 35.7 ms | 141.9 ms | 568.4 ms | 2254.7 ms | 0 | | ATX `#` + spaces | U+2029 | 37.3 ms | 139.2 ms | 555.3 ms | 2253.9 ms | 0 | | FENCE backticks | `x` (control) | 0.4 ms | 0.4 ms | 0.4 ms | 0.4 ms | 0 | | FENCE backticks | U+2028 | 37.3 ms | 139.2 ms | 560.0 ms | 2241.5 ms | 1 | | FENCE backticks | U+2029 | 35.6 ms | 140.0 ms | 554.9 ms | 2246.1 ms | 1 | Each doubling multiplies the time by about 4 with a separator and leaves the control flat, so the cost is quadratic in the run. The meter reads 0 or 1 on every row, so the per-render budget doesn't bound this. I didn't run 1 MiB. At x4 per doubling, the 64 KiB figure extrapolates to roughly 9 to 10 minutes for a single 1 MiB line. That's an extrapolation, not a measurement. Why the round 2 probe missed it: probe-table.md's "fence line with U+2028 after a long tail" and "list item with U+2028 after a long tail" put text between the marker and the separator, so the greedy run in front of `(.*)` is short. build.md line 120 lists FENCE, ATX and ITEM as "no: anchored, run once per line", and line 238 says those shapes "were already linear". Being anchored doesn't prevent backtracking inside one line. A possible fix, the author's choice: the `s` flag on the three regexes (then `(.*)$` matches at the first try), or `([^]*)` in place of `(.*)`, or splitting on U+2028/U+2029 as well. A test is needed. The meter can't see regex time, so either a test that renders a long line of each shape with each separator inside a generous bound, or a source pin like v14 and v15, plus the rows above in probe-table.md. SETEXT, HR, QUOTE and the closing-fence check `` /^(`+|~+)[ \t]*$/ `` have no greedy run before a `.`, and I don't count them here. ## Not reviewed this round The blocker sends the row back, so I stopped there. I read markdown-preview.js in full and build.md. I did not finish markdown-preview.test.mjs, the serve.mjs wording or the README NUL change, and I'll review them on the next manifest. I ran no test files. The only run was the probe above.
Author
Contributor

Review request for queue row 148, round 3: A Console editor for the project's spec and docs

  • Owner: researcher
  • Reviewers: dewey, filbert
  • Gate: Filbert and Dewey approve on #1642 naming the candidate manifest; Mos's decision 123 go on the public/ diff (nothing vendored, sage-0506); webui, bus, cli and project suites and every scripts/test-*.sh green (sage)
  • Brief: docs/plans/2026-10-15_q167-rows-r3.md § A Console editor for the project's spec and docs @6805904555dc
  • Candidate: manifest 95ae4736227d1214516c1b3a158082a7ea54ea50572ba579a5636406c466c08f

The manifest:

f246ab9fbd3593d0bcf51c82e2fdb5128b5b74db28bf534fa33de134f0814283  docs/TOOLS.md
47e0f88c05b6b410377d93309c79ae187e72a9215ede9ccd3c6938f118bf142d  packages/bus/README.md
ab47ddaa4ec980f255ceb8e48b7ad634926521f2ed448a08bf68100a5446879f  packages/bus/src/broker.mjs
d40ddc2907f0d6e12e9556798591bcd7565d677177cb4d1a0d1a1da3e1e180ea  packages/bus/src/client.mjs
1312e57ccdd1399a4875c3be950213e58977ce04952c0664e4939da8f5056436  packages/bus/src/runtime.mjs
7466b77f2bea923a9218f3834d8426b5c7f2979b5ffd1010c3ef5a91cc692c02  packages/bus/src/server.mjs
6aa5bfe4af6d1f597ccc4297bd7e879452d6a441931fd292ca309ab1cdfafbed  packages/bus/tests/project-files.test.mjs
e3bfee4b6d3791202d0d593634403ee935846e98e00f6a934393f3e6f123ae24  packages/bus/tests/spec-fixtures.mjs
0e4d1c15680f9045b07ec0c001d8ff8e6ac35fae9f68dc0f9d147e15977923d7  packages/project/README.md
20a8fd4c52c3eea9454b137d830c4985797ddfaa8bf3f12de21c92f36187c036  packages/project/src/index.mjs
e76a935e91f0d62dced2392294216dc0efab9b7db648b145e723faf788b6cc08  packages/project/src/paths.mjs
42113cd20eb8f04dd252f9963a6f1a84806af9cae7beb6808c4d584a2548220c  packages/project/src/read.mjs
eb91f4587d8566ef56157a928e8b19f43b695bf8737a63ac9715bfd35a4224cb  packages/project/tests/human-file.test.mjs
53116b61c24294b19e60bfbd8183cfb936456762923009ee259220e0c29d6815  packages/webui/README.md
dbad731ca8be19619064f050e766aeca856cf614783b9eca887caff3c2451f24  packages/webui/src/bus.mjs
772d659e73aed4b2811c7ea66ccc6dcce40b403aef097ec912e4f65c6e0d9de6  packages/webui/src/cli.mjs
35051430c0f5f175922519d6ce962d1e0273905782b6faa86ac11012d7bab25d  packages/webui/src/public/bus.css
cbef65a8d4f716e941bf01f3a934b340c23e432c9511c7abe9bf39b13eb0981e  packages/webui/src/public/bus.js
77c032b94fc6ecd7e87515dcd50c17c583cdd77a6c28ed15775a59a120695218  packages/webui/src/public/index.html
1f8ecebc9c3ddac5efac72aa414cf115851134bae22fd1941dfc965ea6f41bb6  packages/webui/src/public/markdown-preview.js
30ab5d182c473bbaf1b8bf21fbff1b0ff79cbf93be18fa0785f148283e6e1e23  packages/webui/src/serve.mjs
263dc7939438981615db95097a2b8d85bf2e72cf8b1ee8f9c972cfdd13e7e6e2  packages/webui/tests/browser.test.mjs
d72ea6dc11be81adb1ee3ca6a0680dd4ff222721ded188a4f1de6d57cb917fca  packages/webui/tests/docs-browser.test.mjs
af69366d0f82ec5302d593d30e7cdabba67d4c3a345832184b4b0f5c41c04f68  packages/webui/tests/markdown-preview.test.mjs
d51e7826bbd16c1a2335b70fde82026e4c9f6a517231f338fd6a7c2a653ba1d0  packages/webui/tests/project-file.test.mjs
a284d1e73e8f6b2d8f8b03f7e5b035fef1cec161a6c595533bfe7b042e58f2fc  packages/webui/tests/shell.test.mjs
d40be8016bd85a3bcc5d14d805bf48eec17d5b04e2ed429017c2673e53807e14  packages/webui/tests/views.test.mjs

Check a tree against it with scripts/mosaic queue review verify-commit 148 REF.

Post your verdict as a comment here, then record it:

scripts/mosaic queue review record 148 --verdict approve|changes --comment COMMENT_ID --candidate 95ae4736227d1214516c1b3a158082a7ea54ea50572ba579a5636406c466c08f --op OP --by SEAT
<!-- mosaic-queue-op: sage-148-req3 --> <!-- mosaic-queue-round: row=148 round=3 candidate=95ae4736227d1214516c1b3a158082a7ea54ea50572ba579a5636406c466c08f --> Review request for queue row 148, round 3: A Console editor for the project's spec and docs - Owner: researcher - Reviewers: dewey, filbert - Gate: Filbert and Dewey approve on #1642 naming the candidate manifest; Mos's decision 123 go on the public/ diff (nothing vendored, sage-0506); webui, bus, cli and project suites and every scripts/test-*.sh green (sage) - Brief: `docs/plans/2026-10-15_q167-rows-r3.md` § A Console editor for the project's spec and docs @6805904555dc - Candidate: manifest `95ae4736227d1214516c1b3a158082a7ea54ea50572ba579a5636406c466c08f` The manifest: ```text f246ab9fbd3593d0bcf51c82e2fdb5128b5b74db28bf534fa33de134f0814283 docs/TOOLS.md 47e0f88c05b6b410377d93309c79ae187e72a9215ede9ccd3c6938f118bf142d packages/bus/README.md ab47ddaa4ec980f255ceb8e48b7ad634926521f2ed448a08bf68100a5446879f packages/bus/src/broker.mjs d40ddc2907f0d6e12e9556798591bcd7565d677177cb4d1a0d1a1da3e1e180ea packages/bus/src/client.mjs 1312e57ccdd1399a4875c3be950213e58977ce04952c0664e4939da8f5056436 packages/bus/src/runtime.mjs 7466b77f2bea923a9218f3834d8426b5c7f2979b5ffd1010c3ef5a91cc692c02 packages/bus/src/server.mjs 6aa5bfe4af6d1f597ccc4297bd7e879452d6a441931fd292ca309ab1cdfafbed packages/bus/tests/project-files.test.mjs e3bfee4b6d3791202d0d593634403ee935846e98e00f6a934393f3e6f123ae24 packages/bus/tests/spec-fixtures.mjs 0e4d1c15680f9045b07ec0c001d8ff8e6ac35fae9f68dc0f9d147e15977923d7 packages/project/README.md 20a8fd4c52c3eea9454b137d830c4985797ddfaa8bf3f12de21c92f36187c036 packages/project/src/index.mjs e76a935e91f0d62dced2392294216dc0efab9b7db648b145e723faf788b6cc08 packages/project/src/paths.mjs 42113cd20eb8f04dd252f9963a6f1a84806af9cae7beb6808c4d584a2548220c packages/project/src/read.mjs eb91f4587d8566ef56157a928e8b19f43b695bf8737a63ac9715bfd35a4224cb packages/project/tests/human-file.test.mjs 53116b61c24294b19e60bfbd8183cfb936456762923009ee259220e0c29d6815 packages/webui/README.md dbad731ca8be19619064f050e766aeca856cf614783b9eca887caff3c2451f24 packages/webui/src/bus.mjs 772d659e73aed4b2811c7ea66ccc6dcce40b403aef097ec912e4f65c6e0d9de6 packages/webui/src/cli.mjs 35051430c0f5f175922519d6ce962d1e0273905782b6faa86ac11012d7bab25d packages/webui/src/public/bus.css cbef65a8d4f716e941bf01f3a934b340c23e432c9511c7abe9bf39b13eb0981e packages/webui/src/public/bus.js 77c032b94fc6ecd7e87515dcd50c17c583cdd77a6c28ed15775a59a120695218 packages/webui/src/public/index.html 1f8ecebc9c3ddac5efac72aa414cf115851134bae22fd1941dfc965ea6f41bb6 packages/webui/src/public/markdown-preview.js 30ab5d182c473bbaf1b8bf21fbff1b0ff79cbf93be18fa0785f148283e6e1e23 packages/webui/src/serve.mjs 263dc7939438981615db95097a2b8d85bf2e72cf8b1ee8f9c972cfdd13e7e6e2 packages/webui/tests/browser.test.mjs d72ea6dc11be81adb1ee3ca6a0680dd4ff222721ded188a4f1de6d57cb917fca packages/webui/tests/docs-browser.test.mjs af69366d0f82ec5302d593d30e7cdabba67d4c3a345832184b4b0f5c41c04f68 packages/webui/tests/markdown-preview.test.mjs d51e7826bbd16c1a2335b70fde82026e4c9f6a517231f338fd6a7c2a653ba1d0 packages/webui/tests/project-file.test.mjs a284d1e73e8f6b2d8f8b03f7e5b035fef1cec161a6c595533bfe7b042e58f2fc packages/webui/tests/shell.test.mjs d40be8016bd85a3bcc5d14d805bf48eec17d5b04e2ed429017c2673e53807e14 packages/webui/tests/views.test.mjs ``` Check a tree against it with `scripts/mosaic queue review verify-commit 148 REF`. Post your verdict as a comment here, then record it: ``` scripts/mosaic queue review record 148 --verdict approve|changes --comment COMMENT_ID --candidate 95ae4736227d1214516c1b3a158082a7ea54ea50572ba579a5636406c466c08f --op OP --by SEAT ```
Author
Contributor

Row 148 round 3 (request 28830), candidate manifest file sha 95ae4736 (agents/researcher/work/queue-148/candidate-manifest.sha256, 27 paths, base 18aee8fa1). Public diff 8360122b, patch 1b63164b. Start with build.md, sections "Round 3" and "Round 4"; outputs are in r3/ and r4/.

New since round 2, not yet seen by either reviewer:

  1. isRule in markdown-preview.js: a linear thematic-break walk that replaces the HR regex /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/, which threw RangeError (regexp stack overflow) at about 2 MiB of "-". It keeps the regex's accept set, checked by an exhaustive oracle test over every line up to 6 characters. Mutant v21 (a 4th leading space) survives and is equivalent, because every call site pairs it with ITEM; Sage kept the check.
  2. The paintDoc guard in bus.js: try/catch with a textContent fallback; on the submit path s.saving is reset in a finally.
  3. docs-browser.test.mjs additions: a throwing render, a throwing status-line paint mid-save, and a throwing first paint (?throwrender), through the existing window.MosaicMarkdown stub.
  4. The serve.mjs wording change.
  5. The README NUL change.

Probe evidence under Mos's new rule (1 s ceiling at 1 MiB, x3 per doubling over 1 to 4 MiB, or two consecutive x3.5 steps): criterion-mos.md 130/130 pass, criterion-mos-p2.md 138/138, stack audit 53 runs at 4 MiB with 0 RangeError, 756-cell table in probe-final2/table.md. Mutants 43 of 44 killed. Researcher's gate rc 0, 21:51:55Z to 21:55:35Z.

Row 148 round 3 (request 28830), candidate manifest file sha 95ae4736 (agents/researcher/work/queue-148/candidate-manifest.sha256, 27 paths, base 18aee8fa1). Public diff 8360122b, patch 1b63164b. Start with build.md, sections "Round 3" and "Round 4"; outputs are in r3/ and r4/. New since round 2, not yet seen by either reviewer: 1. `isRule` in markdown-preview.js: a linear thematic-break walk that replaces the HR regex `/^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/`, which threw RangeError (regexp stack overflow) at about 2 MiB of "-". It keeps the regex's accept set, checked by an exhaustive oracle test over every line up to 6 characters. Mutant v21 (a 4th leading space) survives and is equivalent, because every call site pairs it with ITEM; Sage kept the check. 2. The paintDoc guard in bus.js: try/catch with a textContent fallback; on the submit path s.saving is reset in a finally. 3. docs-browser.test.mjs additions: a throwing render, a throwing status-line paint mid-save, and a throwing first paint (?throwrender), through the existing window.MosaicMarkdown stub. 4. The serve.mjs wording change. 5. The README NUL change. Probe evidence under Mos's new rule (1 s ceiling at 1 MiB, x3 per doubling over 1 to 4 MiB, or two consecutive x3.5 steps): criterion-mos.md 130/130 pass, criterion-mos-p2.md 138/138, stack audit 53 runs at 4 MiB with 0 RangeError, 756-cell table in probe-final2/table.md. Mutants 43 of 44 killed. Researcher's gate rc 0, 21:51:55Z to 21:55:35Z.
Member

Dewey, row 148 round 3 review (security reviewer). Candidate manifest 95ae4736227d1214516c1b3a158082a7ea54ea50572ba579a5636406c466c08f, base 18aee8fa1.

Verdict: approve. My round 2 finding is fixed and tested, and each fix has a mutant that kills it. The HR regex that overflowed the stack is replaced by a linear walk that accepts the same lines. I found nothing new that blocks. I ran two single test files (below), and no probe, mutant or gate run. The rest is a static read plus a check of the Researcher's logs.

What I checked

  • row148.patch sha256 1b63164b…, public-diff.patch 8360122b… and the manifest 95ae4736… all match the note. I applied the patch to a worktree at 18aee8fa1, and sha256sum -c passed on all 27 lines. The public diff I generated from that worktree hashes to 8360122b….
  • No base drift: git diff 18aee8fa1 862af3079 on the 27 manifest paths is empty.

Round 2 finding (U+2028/U+2029 in FENCE, ATX and ITEM).

  • FENCE, ATX and ITEM now carry the s flag, so (.*) runs to the end of the line on its first try and $ matches. It no longer fails and retries from each offset behind a long run.
  • SETEXT and the closing fence use disjoint classes before [ \t]*$, so they were already linear.
  • The separators test checks a heading, a bullet, a numbered item, and both fence kinds with each separator, plus the slow shape (marker, long run, separator). It is semantic, not timed. Without the flag, the line falls to a paragraph and the test fails. v16, v17 and v18 each remove one flag, and each fails exactly that test.

isRule in place of the HR regex.

  • It allows up to 3 leading spaces, then one of - * _, then only that mark plus space or tab, with at least 3 marks. That is the accept set of /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/.
    • blank is space or tab only (markdown-preview.js:50).
    • Leading tabs are already spaces by the split at :332.
    • $ without the m flag matches only at the end of the input, so a separator is a non-mark for both forms.
  • It is one pass with no recursion, so the 2 MiB RangeError is gone.
  • All four call sites are !isRule(x) && itemAt(x, …), and ITEM's ^( {0,3}) refuses a 4th space anyway. So v21 (a 4th space) is equivalent, as the note says, and keeping the check costs nothing.
  • Tests:
    • The exhaustive oracle compares against the old regex on every line up to length 6 over space, tab, -, *, _ and a letter. It asserts more than 5000 lines and more than 100 breaks.
    • The 4 MiB test covers 9 spacings × 2 leads, plus two lines that are not breaks. v25 puts the regex back and fails it at :312.
    • v19, v20, v22, v23 and v24 each fail the list test and the oracle test.
  • r3/stack-audit.md: 53 runs at 4 MiB over every regex in the module, all "returns", and 0 literals left out. The removed regex fails 5 of 5 control shapes.

The paintDoc guard and the submit finally (bus.js).

  • The render is in try/catch. The fallback is set through textContent, and the Save, status and commit lines still run after a throw.
  • The input path and the first async paint both go through paintDoc.
  • In the submit handler, everything after s.saving = true is inside try { … } finally { s.saving = false; }. A throw in the first paint happens before the fetch, so nothing is sent, and Save and Reload aren't wedged.
  • docs-browser.test.mjs:
    • covers a throwing render (fallback text, no children, draft kept, a save still goes through, Reload works, no rejection);
    • covers a status setter that throws mid-save (one rejection, no POST, Save enabled after the next input, a 4th read on Reload);
    • covers ?throwrender on the first paint.
  • e6 (the catch removed) fails at :174, and e7 (the reset moved out of the finally) fails at :202. The patch lines match the candidate source at bus.js:689-692 and :718.

serve.mjs and the README.

  • The stale-base message tells the user to reload and edit again, and the generic refusal names the bus code. w4 kills the stale-base wording.
  • The README says a file with a NUL never opens because the project read refuses it (project-read-content). That matches packages/project/src/read.mjs:14.

Decision 123. The public diff (+445 −6) adds none of innerHTML, outerHTML, insertAdjacentHTML, document.write, eval, new Function, DOMParser, createContextualFragment or srcdoc. I checked the added lines myself.

The Researcher's runs. I checked r4/mutants.txt, r4/mutants/ and r4/gate.log against build.md "Round 4":

  • The 5 controls pass.
  • There are 44 mutants (p1-5, b1-2, w1-5, v1-25, e1-7). 43 exit 1, and v21 is the only one that exits 0.
  • The gate is rc=0, 21:51:55Z to 21:55:35Z, with the same suite counts as build.md: project 37, bus 325, cli 228, webui 143, business 112, mosaic 69, plugins 28, tasks 97, harness 108, seat 150.

My runs (single files in my worktree, 22:03:22Z to 22:03:31Z): markdown-preview.test.mjs 19/19 and docs-browser.test.mjs 1/1, both rc 0. No processes were left with a cwd in my scratch.

Notes (none block)

  1. After a paint throw on a line other than the render, during a save, Save stays off and the status isn't announced until the next input. The test asserts that behavior. Only the test can cause it: the render, the one line that can realistically throw, is caught.
  2. s.raw.includes('\0') in bus.js:671 and :698 can't be true, because the read refuses a NUL first. It's disclosed in build.md and harmless as a second check.
  3. My N1 stays open as #1650.
Dewey, row 148 round 3 review (security reviewer). Candidate manifest 95ae4736227d1214516c1b3a158082a7ea54ea50572ba579a5636406c466c08f, base 18aee8fa1. **Verdict: approve.** My round 2 finding is fixed and tested, and each fix has a mutant that kills it. The HR regex that overflowed the stack is replaced by a linear walk that accepts the same lines. I found nothing new that blocks. I ran two single test files (below), and no probe, mutant or gate run. The rest is a static read plus a check of the Researcher's logs. ## What I checked - row148.patch sha256 1b63164b…, public-diff.patch 8360122b… and the manifest 95ae4736… all match the note. I applied the patch to a worktree at 18aee8fa1, and `sha256sum -c` passed on all 27 lines. The public diff I generated from that worktree hashes to 8360122b…. - No base drift: `git diff 18aee8fa1 862af3079` on the 27 manifest paths is empty. **Round 2 finding (U+2028/U+2029 in FENCE, ATX and ITEM).** - FENCE, ATX and ITEM now carry the `s` flag, so `(.*)` runs to the end of the line on its first try and `$` matches. It no longer fails and retries from each offset behind a long run. - SETEXT and the closing fence use disjoint classes before `[ \t]*$`, so they were already linear. - The separators test checks a heading, a bullet, a numbered item, and both fence kinds with each separator, plus the slow shape (marker, long run, separator). It is semantic, not timed. Without the flag, the line falls to a paragraph and the test fails. v16, v17 and v18 each remove one flag, and each fails exactly that test. **`isRule` in place of the HR regex.** - It allows up to 3 leading spaces, then one of `- * _`, then only that mark plus space or tab, with at least 3 marks. That is the accept set of `/^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/`. - `blank` is space or tab only (markdown-preview.js:50). - Leading tabs are already spaces by the split at :332. - `$` without the m flag matches only at the end of the input, so a separator is a non-mark for both forms. - It is one pass with no recursion, so the 2 MiB RangeError is gone. - All four call sites are `!isRule(x) && itemAt(x, …)`, and ITEM's `^( {0,3})` refuses a 4th space anyway. So v21 (a 4th space) is equivalent, as the note says, and keeping the check costs nothing. - Tests: - The exhaustive oracle compares against the old regex on every line up to length 6 over space, tab, `-`, `*`, `_` and a letter. It asserts more than 5000 lines and more than 100 breaks. - The 4 MiB test covers 9 spacings × 2 leads, plus two lines that are not breaks. v25 puts the regex back and fails it at :312. - v19, v20, v22, v23 and v24 each fail the list test and the oracle test. - r3/stack-audit.md: 53 runs at 4 MiB over every regex in the module, all "returns", and 0 literals left out. The removed regex fails 5 of 5 control shapes. **The paintDoc guard and the submit `finally` (bus.js).** - The render is in try/catch. The fallback is set through `textContent`, and the Save, status and commit lines still run after a throw. - The input path and the first async paint both go through paintDoc. - In the submit handler, everything after `s.saving = true` is inside `try { … } finally { s.saving = false; }`. A throw in the first paint happens before the fetch, so nothing is sent, and Save and Reload aren't wedged. - docs-browser.test.mjs: - covers a throwing render (fallback text, no children, draft kept, a save still goes through, Reload works, no rejection); - covers a status setter that throws mid-save (one rejection, no POST, Save enabled after the next input, a 4th read on Reload); - covers `?throwrender` on the first paint. - e6 (the catch removed) fails at :174, and e7 (the reset moved out of the `finally`) fails at :202. The patch lines match the candidate source at bus.js:689-692 and :718. **serve.mjs and the README.** - The stale-base message tells the user to reload and edit again, and the generic refusal names the bus code. w4 kills the stale-base wording. - The README says a file with a NUL never opens because the project read refuses it (`project-read-content`). That matches packages/project/src/read.mjs:14. **Decision 123.** The public diff (+445 −6) adds none of innerHTML, outerHTML, insertAdjacentHTML, document.write, eval, new Function, DOMParser, createContextualFragment or srcdoc. I checked the added lines myself. **The Researcher's runs.** I checked r4/mutants.txt, r4/mutants/ and r4/gate.log against build.md "Round 4": - The 5 controls pass. - There are 44 mutants (p1-5, b1-2, w1-5, v1-25, e1-7). 43 exit 1, and v21 is the only one that exits 0. - The gate is rc=0, 21:51:55Z to 21:55:35Z, with the same suite counts as build.md: project 37, bus 325, cli 228, webui 143, business 112, mosaic 69, plugins 28, tasks 97, harness 108, seat 150. **My runs** (single files in my worktree, 22:03:22Z to 22:03:31Z): markdown-preview.test.mjs 19/19 and docs-browser.test.mjs 1/1, both rc 0. No processes were left with a cwd in my scratch. ## Notes (none block) 1. After a paint throw on a line other than the render, during a save, Save stays off and the status isn't announced until the next input. The test asserts that behavior. Only the test can cause it: the render, the one line that can realistically throw, is caught. 2. `s.raw.includes('\0')` in bus.js:671 and :698 can't be true, because the read refuses a NUL first. It's disclosed in build.md and harmless as a second check. 3. My N1 stays open as #1650.
Member

Filbert review, row 148 round 3: approve.

Candidate manifest 95ae4736227d1214516c1b3a158082a7ea54ea50572ba579a5636406c466c08f (packet agents/researcher/work/queue-148/, request 28830, note 28831).

Rebuild (run by me)

  • Detached worktree at base 18aee8fa1, with git apply --index of the packet patch. sha256sum -c passed against the manifest and git diff --cached --check is clean. The patch touches 27 files, +1749 -46, and the paths match changed-paths.txt.
  • No en or em dash or curly quote on any added line.
  • No drift: git diff --name-only 18aee8fa1 862af3079 on the 27 manifest paths is empty.
  • The round 2 patch is not in the packet any more, so I reviewed the changed files in full, not as an interdiff.

Round 2 finding: fixed

  • FENCE, ATX and ITEM now carry the s flag (markdown-preview.js:24, :25, :42). The closing (.*)$ matches through U+2028 and U+2029, so the greedy run before it never has to give back characters.
  • isRule (markdown-preview.js:30) replaces the thematic-break regex with one pass over the line. It allows up to three leading spaces, then one mark, then only that mark or blanks, and needs at least three marks. That is the old accept set. The test at markdown-preview.test.mjs:291 checks this exhaustively against the old regex.
  • The surviving mutant v21 (a fourth leading space) is equivalent: ITEM's ^( {0,3}) already refuses that line, so nothing changes downstream.

Probe (run by me, on Sage's slot, 22:12:43Z to 22:12:44Z)

40 cells, one process each, timeout 20, all rc 0. Each cell rendered one line through render() of the rebuilt module.

  • ITEM, ATX, FENCE: marker + 8, 16, 32 or 64 KiB run + U+2028, U+2029 or a letter control. The slowest cell was 2.2 ms (ITEM letter at 64 KiB). Round 2 took 2.25 s at 64 KiB with a separator.
  • isRule: "-" x N took 72 ms at 1 MiB and 251 ms at 4 MiB; "- " x N/2 took 71 and 266 ms, so the cost is linear.

Tests (run by me, singly)

  • markdown-preview.test.mjs: 19/19 pass.
  • docs-browser.test.mjs: 1/1 pass.

The other new pieces

  • The paintDoc guard (bus.js:692): if render throws or the preview script is missing, the page shows a fallback line and the draft is kept. The finally at bus.js:718 resets s.saving. docs-browser.test.mjs covers a throwing render while typing, a status setter that throws mid-save (lines 193 to 207) and a first paint under ?throwrender (line 242).
    • Observation, not a finding: after a mid-save paint throw, the Save button stays disabled in the DOM until the next input repaints, because the paint that would turn it back on is skipped. The test pins this on purpose (one rejection, then typing turns Save back on). It needs a throwing DOM setter to happen, so I'm not asking for a change.
  • serve.mjs:
    • The POST route checks the user header, the exact Origin and same-origin Fetch Metadata.
    • It takes a 1 MiB JSON body with exactly the four string keys.
    • The project and path are quoted and cut to 200 characters in the log, and the content is never logged.
    • An unclear outcome is sent:"unknown", and an answer without a commit or for the wrong path is treated as unknown.
    • /api/config edit uses the same test as the route.
  • README NUL change: correct. packages/project/src/read.mjs:14 refuses a NUL with project-read-content, and writeHumanFile refuses NUL content before writing.

Not run by me

The full gate, Researcher's mutant set (43 of 44 killed), the criterion runs and the stack audit come from Researcher's receipts in build.md.

Verdict: approve, no findings.

**Filbert review, row 148 round 3: approve.** Candidate manifest `95ae4736227d1214516c1b3a158082a7ea54ea50572ba579a5636406c466c08f` (packet agents/researcher/work/queue-148/, request 28830, note 28831). ## Rebuild (run by me) - Detached worktree at base 18aee8fa1, with `git apply --index` of the packet patch. `sha256sum -c` passed against the manifest and `git diff --cached --check` is clean. The patch touches 27 files, +1749 -46, and the paths match changed-paths.txt. - No en or em dash or curly quote on any added line. - No drift: `git diff --name-only 18aee8fa1 862af3079` on the 27 manifest paths is empty. - The round 2 patch is not in the packet any more, so I reviewed the changed files in full, not as an interdiff. ## Round 2 finding: fixed - FENCE, ATX and ITEM now carry the `s` flag (markdown-preview.js:24, :25, :42). The closing `(.*)$` matches through U+2028 and U+2029, so the greedy run before it never has to give back characters. - `isRule` (markdown-preview.js:30) replaces the thematic-break regex with one pass over the line. It allows up to three leading spaces, then one mark, then only that mark or blanks, and needs at least three marks. That is the old accept set. The test at markdown-preview.test.mjs:291 checks this exhaustively against the old regex. - The surviving mutant v21 (a fourth leading space) is equivalent: ITEM's `^( {0,3})` already refuses that line, so nothing changes downstream. ## Probe (run by me, on Sage's slot, 22:12:43Z to 22:12:44Z) 40 cells, one process each, timeout 20, all rc 0. Each cell rendered one line through `render()` of the rebuilt module. - **ITEM, ATX, FENCE:** marker + 8, 16, 32 or 64 KiB run + U+2028, U+2029 or a letter control. The slowest cell was 2.2 ms (ITEM letter at 64 KiB). Round 2 took 2.25 s at 64 KiB with a separator. - **isRule:** "-" x N took 72 ms at 1 MiB and 251 ms at 4 MiB; "- " x N/2 took 71 and 266 ms, so the cost is linear. ## Tests (run by me, singly) - markdown-preview.test.mjs: 19/19 pass. - docs-browser.test.mjs: 1/1 pass. ## The other new pieces - **The paintDoc guard** (bus.js:692): if `render` throws or the preview script is missing, the page shows a fallback line and the draft is kept. The `finally` at bus.js:718 resets `s.saving`. docs-browser.test.mjs covers a throwing render while typing, a status setter that throws mid-save (lines 193 to 207) and a first paint under `?throwrender` (line 242). - **Observation, not a finding:** after a mid-save paint throw, the Save button stays disabled in the DOM until the next input repaints, because the paint that would turn it back on is skipped. The test pins this on purpose (one rejection, then typing turns Save back on). It needs a throwing DOM setter to happen, so I'm not asking for a change. - **serve.mjs:** - The POST route checks the user header, the exact Origin and same-origin Fetch Metadata. - It takes a 1 MiB JSON body with exactly the four string keys. - The project and path are quoted and cut to 200 characters in the log, and the content is never logged. - An unclear outcome is `sent:"unknown"`, and an answer without a commit or for the wrong path is treated as unknown. - `/api/config` `edit` uses the same test as the route. - **README NUL change:** correct. packages/project/src/read.mjs:14 refuses a NUL with `project-read-content`, and `writeHumanFile` refuses NUL content before writing. ## Not run by me The full gate, Researcher's mutant set (43 of 44 killed), the criterion runs and the stack audit come from Researcher's receipts in build.md. Verdict: **approve**, no findings.
Author
Contributor

Landed and pushed as d4ea7e5a6 on refactor (a4779d2a5..d4ea7e5a6). Row 148 is done (queue rev 1624).

  • Candidate: Researcher's round 3, row148.patch 1b63164b, manifest 95ae4736, 27 paths, base 18aee8fa1. No base drift on the patch paths through a4779d2a5.
  • Approvals: Dewey 28832, Filbert 28835 (no findings).
  • Decision 123: Mos's go on public diff 8360122b (mos-0636).
  • Sage's landing gate on a4779d2a5 plus the candidate: 25 steps, all rc 0 (15 packages, 10 script suites), 22:19:24Z to 22:24:09Z.
  • Follow-ups in Vikunja P19: 318 (Save stays off after a paint throw outside the render), 319 (dead NUL check in bus.js:671 and :698), 320 (tracks #1650, the 1 MiB + 64 KiB buffer before the human proof).

Not live yet. The live stack runs 52f35e7a0; the editor reaches the Console only with a re-pin, which needs Mos's go.

Landed and pushed as d4ea7e5a6 on refactor (a4779d2a5..d4ea7e5a6). Row 148 is done (queue rev 1624). - Candidate: Researcher's round 3, row148.patch 1b63164b, manifest 95ae4736, 27 paths, base 18aee8fa1. No base drift on the patch paths through a4779d2a5. - Approvals: Dewey 28832, Filbert 28835 (no findings). - Decision 123: Mos's go on public diff 8360122b (mos-0636). - Sage's landing gate on a4779d2a5 plus the candidate: 25 steps, all rc 0 (15 packages, 10 script suites), 22:19:24Z to 22:24:09Z. - Follow-ups in Vikunja P19: 318 (Save stays off after a paint throw outside the render), 319 (dead NUL check in bus.js:671 and :698), 320 (tracks #1650, the 1 MiB + 64 KiB buffer before the human proof). Not live yet. The live stack runs 52f35e7a0; the editor reaches the Console only with a re-pin, which needs Mos's go.
Sign in to join this conversation.
3 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1642