Skip to content

Tracking fws U6: apply_patch publishes through the guard #2021

Open

Description

@easonLiangWorldedtech

What this unit delivers: the apply_patch tool publishes through the guard; a move carries the source's completeness to its destination instead of claiming completeness for lines the model never read; a partial-source move onto an already-observed destination is rejected before any state changes. It also rolls back the parent directories saveDirectly created when the guard refuses the publish.

Why it matters: patch hunks are the easiest way to "write" a file the model never fully read; routing them through the guard closes that hole.

Implemented by: PR #1915 (branch fws/u6-apply-patch-wiring).

Acceptance:

  • apply_patch publishes via guardedWrite; an unobserved overwrite and a stale version token are refused with the standard remediation.
  • A move inherits the source's complete flag; a partial-source move onto an observed destination is refused pre-state-change.
  • saveDirectly removes the directories it created (innermost-first, rmdir refuses a populated directory) and rethrows the original error.

Part of the file-write-safety epic #1375, landed as a nine-PR chain tracked on #1991. Merge order: U1 -> U2 -> U3 -> U4 -> U5 -> U8 -> U6 -> U7 -> U9.

Activity

  1. easonLiangWorldedtech commented on Nov 9, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Queue gate line for this tracker entry (#1915, a71761a).

    What the gate was: this entry waited on the same two answers - #1912 and #1914 - for the same reason: #1915's asking rows are dispositions by deferral, and the unit they defer to had to be triaged before the deferral meant anything.

    The gate is cleared on my side, by the same reads as the #2023 line: #1912 answered at head 9d420ad in comment 6744486462 (2026-11-09T15:52:00Z, GitHub), #1914 answered by this account with its latest pre-existing review-comment entry id 4512376064 created 2026-11-09T11:48:16Z. #1915's four rows were answered today in comment 6744493793 (4,680 B, 15:52:18Z), with a correction to one sentence of mine in comment 6744527446 (623 B, 15:53:46Z).

    What still gates this queue position: three of the four rows (guardedWrite.spec.ts:696, :801, :965) want the same FIFO-serialisation and pending-reset scenarios that #1916's two rows want, in the same three-copy file named in the #2023 line, and they are one change owed once in #1914's copy 32ab04c7c054 at 7209a5f, not three changes in three copies; the fourth row (ApplyDiffTool.ts:256) is routed to #1916's copy 0de4b3a30290 at da2b913 because routing the diff-view save through the guard is #1916's subject, and a version binding written into #1915's copy 6e59d8c7ee2e would give one guard two designs. Counted lines for #1915 are CI's printed 1,821 against the 500-line cap - CI's number, not a measurement of mine - and the standing stack rule still applies to whatever comes next: no second main-side pull request against the same four CODEOWNERS named in .github/CODEOWNERS while the first is open. Nothing was pushed and nothing is being asked of anyone by this comment.

  2. easonLiangWorldedtech commented on Nov 11, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Split plan draft for the 1915 / 1916 / 1917 / 1918 monoliths (carrier 2021 for 1915, carrier 2024 for 1917)

    Status: draft for review. Nothing posted, nothing opened, no pull touched, no lease pinned.

    0. What the four monoliths ask a reviewer to read, on the axis CI prices

    Construction, identical for all four: the branch head tree, re-parented as the tree of a new sole-parent commit whose only parent is the fork main tip 53cb5b5e7c8014d3226e5636bbca8999ed3b8526, then the repo's own mutation-selection instrument run ci --base <tip> --head <that commit> inside that branch's own detached worktree at its own head, so the tree it selects from is the tree on disk. Each run names its merge base as the tip.

    pull branch head head tree sole-parent commit printed selection line
    1915 a71761a (merged, 44e9009199d9cf59f91851e5b7e2c494b9e65577) b8a79630ef9453b6e1c1f8a238d4e94234a8416f b45ac93755d9e2f6e381ef96b9dc4965c43f2a67 from merge base 53cb5b5: extension (1808 lines)
    1916 da2b913 b2ebc6570682f1e5c788d8278e6919716746a069 af5745687086f3b9f66860edeb4386bd8d1fcaeb from merge base 53cb5b5: extension (1815 lines), webview (12 lines)
    1917 d82d63e 60510eb0c628f8492ac8616f2b0e8511f19cfb53 f257291dcad483bc83e73f8fd2dd17929b68c5e3 from merge base 53cb5b5: extension (1781 lines), webview (12 lines)
    1918 1d993fb 1a1ea02141a0bac5eec59f3d4333196000615949 454d03bfb3406762191966b04b3ef78cea904917 from merge base 53cb5b5: extension (1871 lines), webview (12 lines)

    All four sit three to nearly four times over the 500 changed-executable-line cap. The instrument prints no merge base when the count exceeds its cap, because it checks the per-package cap while building the selection manifest and emits the merge-base line only afterwards, so each run used a copy of the same instrument blob carrying exactly two edits, the cap constant raised from 500 to 2000000 so the count prints rather than throws and the mutation stage short-circuited behind an environment switch so nothing is mutated, with the counting logic untouched, and the copy deleted from each worktree afterwards. On the pre-merge shape the same branch reads far smaller: 1915 against the stale base a0d03a0 prints from merge base 42a1bbb with extension (403 lines), which is the figure its existing review threads were written against and it is not the axis CI prices.

    1. Residue the 1921 split units do not carry

    Measured path by path against the 1921 unit pulls, as same-path-different-blob rows:

    monolith paths the units do not carry same-path-different-blob rows
    1915 9 89
    1916 10 93
    1917 11 103
    1918 13 103

    1918's residue names, among the thirteen: src/core/tools/guardedWrite.ts with its guardedWrite and applyDiffTool.guardedWrite specs, src/services/file-safety/safeWriteText.ts with its two specs, src/integrations/editor/committedWriteBufferConflictError.ts, src/utils/__tests__/fileLockChain.test.ts, and the observationRegistry pair, source file and spec. The four residues overlap heavily, all four name the observationRegistry pair, so the shared residue belongs in one unit that the other units sit on top of rather than being duplicated four times.

    2. Unit boundaries proposed, with the claim each title makes and the specs that travel

    Cut rule applied: one behavioural claim per unit, the specs that assert that claim travel with it, and no unit carries a file whose only role in that unit is to make an import resolve.

    Unit R - observation record and per-path release (queue head). Claim: a task records which version of a path it observed, and a caller that must revoke an authorization it did not earn can release one path without discarding the observations other reads of the same task still rely on. Files: src/core/task/observationRegistry.ts. Specs that travel: src/core/task/__tests__/observationRegistry.spec.ts, including the three rows about per-path release. This unit also carries the merge ruling already proved on 1915: trunk accessor stands as written, the per-path release survives on top of it, and the untested semantic is named as debt, release after dispose is untested rather than safe.

    Unit S - save path re-observes only what it recorded. Claim: saving a diff view re-records the path for the saving task when a complete pre-open observation exists and releases the recorded path when it does not, so a later human write is not treated as agent observed. Files: the save branch of src/integrations/editor/DiffViewProvider.ts. Specs that travel: src/core/tools/__tests__/applyDiffTool.outsideWorkspace.spec.ts and the save-path rows of the DiffViewProvider specs.

    Unit P - preview abandonment releases what the model never read. Claim: closing a preview that observed a version no read claimed releases exactly that path, and only while the preview still owns the entry, proven by deref plus ownership by object identity. Files: the preview-close branch of src/integrations/editor/DiffViewProvider.ts. Specs that travel: the preview-lifecycle rows of the DiffViewProvider specs.

    Unit W - guarded write and its conflict surface (1918 residue, shared with 1916). Claim: a write that races a committed buffer fails with a named conflict rather than overwriting, through the guarded write helper and the safe text writer. Files: src/core/tools/guardedWrite.ts, src/services/file-safety/safeWriteText.ts, src/integrations/editor/committedWriteBufferConflictError.ts. Specs that travel: the guardedWrite and applyDiffTool.guardedWrite specs, both safeWriteText specs, and src/utils/__tests__/fileLockChain.test.ts.

    Boundaries for 1916 and 1917 that this draft does not yet claim: I have not read those trees file by file this round, so their non-registry boundaries are marked by residue counts and named paths rather than proposed as cuts, and the plan should record that the remaining 1917 units come out of its eleven residue paths once the per-file table is attached to the carrier.

    3. Chain order and queue head

    Queue head: Unit R, because both S and P call the accessor and the per-path release that R defines, and W's guarded write is consumed by the save path. Declared merge order: R, then W, then S, then P, then the 1917 history-delete units on top. Non-cumulative branches throughout, each unit its own branch off the tip with the declared merge order recorded on the carrier, and no unit's branch containing a later unit's content.

    4. What each unit measures on the tip axis

    The only per-unit figure measured so far is Unit R's, recorded in section 11 at 2 lines against the 500 cap. The method for the rest is stated without a claim about its outcome: the same construction, a sole-parent commit-tree under 53cb5b5e7c8014d3226e5636bbca8999ed3b8526, in the unit's own worktree so the tree selected from is the tree on disk, with the printed merge base required to name the tip. Nothing in this document states what an unmeasured unit will measure.

    5. The owner-side question this lane carries rather than answers

    These monoliths cannot merge as they stand on the axis CI prices. Someone has to decide what happens to the open pulls 1915, 1916, 1917 and 1918 once split units exist: close them, or leave them open as records that point at the units. Closing an upstream pull is a remote write this campaign has not tested against the token ceiling, so it may not be ours to do at all.


    Additions required before posting (round 73), measured this round, all local at 0 API except the one carrier search named below

    6. Content source of record, pinned per unit

    Every figure in this plan is measured against the fork main tip 53cb5b5e7c8014d3226e5636bbca8999ed3b8526. Content sources are commit shas of existing content, never a stacked per-PR head, because stacked heads each contain their ancestors and are internally inconsistent as a source.

    unit content source of record why that sha
    R, per-path release 44e9009199d9cf59f91851e5b7e2c494b9e65577 the merged-and-adapted 1915 head, whose accessor resolution is already proved: trunk accessor plus the per-path release on the same blob, type-check 0 error lines and 25 passed of 25 including the three per-path release rows
    S, save re-observe 44e9009199d9cf59f91851e5b7e2c494b9e65577 same reason: the save branch and its release only exist in adapted form on the merged head, the pre-merge head a71761afc63cb6cf699fa4bb74faa46d938ffef1 still calls the accessor shape trunk replaced
    P, preview release 44e9009199d9cf59f91851e5b7e2c494b9e65577 same as S, the ownership-by-identity release sits against trunk semantics on the merged head
    W, guarded write 1d993fb42d4b1ae7657336371b40cac9aad72396 1918 head at the time this plan was drafted; it has no named descendant, so the head itself is the source and any later descendant must be named by sha before a port begins

    7. Fidelity anchor for rework units

    These are rework units, content re-cut from an existing pull rather than new design, so the fidelity anchor is the plan document and not the monolith diff. Each unit above cites the carrier issue as its plan of record, every split pull created from these units cites that issue in its body, and any change to a unit's declared claim is a change to this plan posted on the carrier before the pull moves. That is the difference between a planned split and a silent plan change, which the repo's ruleset reads as a merge blocker.

    8. Branch names in the convention the fork already carries, and who owns each shared symbol

    The fork carries 364 refs and 16 of them are unit refs named fws/u<N>-<claim-slug>, among them fws/u1-safewrite-core, fws/u2-lock-key, fws/u3-observation-registry, fws/u3-observation-completeness, fws/u4-read-scope-recording, fws/u5-guard-core, fws/u6-apply-patch-wiring, fws/u7-tool-wiring, fws/u8-diffview-guarded-save, fws/u9-task-history-delete, fws/u9-1914-cut1-safe-write-file-lock and fws/u9-1914-cut2-observation-registry-task. The monolith refs are the last four of those; the new units below therefore continue the numbering at u10 rather than reusing a name, so a reader can tell a unit ref from a monolith ref by eye:

    • fws/u10-per-path-release - unit R
    • fws/u11-save-reobserve - unit S
    • fws/u12-preview-release - unit P
    • fws/u13-guarded-write-conflict - unit W

    Explicit ownership of the cross-unit symbols, stated as who owns what rather than as an implied ordering: R owns src/core/task/observationRegistry.ts including the accessor it inherits from trunk and the per-path release method on top of it, and no other unit may edit that file. W owns src/core/tools/guardedWrite.ts, src/services/file-safety/safeWriteText.ts and src/integrations/editor/committedWriteBufferConflictError.ts. S and P own only their own branches inside src/integrations/editor/DiffViewProvider.ts and the specs named with them, and both consume R accessor through R, so S and P sit after R in the chain because of that ownership and not because of a letter order.

    9. The twelve webview lines are outside the four cuts

    Each of 1916, 1917 and 1918 prints webview (12 lines) in its selection line. Read from the tip-to-head deltas, each of those three carries 45 files under webview-ui/ - for example webview-ui/src/components/chat/ModelSelector.tsx, webview-ui/src/components/chat/__tests__/ModelSelector.spec.tsx, webview-ui/src/components/settings/ApiOptions.tsx, webview-ui/playwright/gallery/stories.tsx and webview-ui/playwright/themes.ts - while the instrument charges only 12 executable lines to that package, which means most of those files are not executable lines under the instrument own counting rule. This draft does not claim to know which of the 45 files produce those 12 lines, because file-level attribution has not been measured, and it therefore states plainly that no unit among R, S, P and W carries webview content: the twelve lines are outside these four cuts and will appear in the later 1916 and 1917 units whose boundaries this draft does not claim.

    10. Which carrier covers 1916 and 1918

    One search, on api.github.com for issues in easonLiangWorldedtech/Zoo-Code whose text names both 1916 and 1918, http 200, total_count 1, and the single hit is issue 41, a VPS2 per-view-state series issue that is not a carrier for these monoliths. So within the fork there is no issue that names 1916 or 1918 as a carrier, and this plan posts on carrier 2021 for 1915 with carrier 2024 named inside it for 1917, states that 1916 and 1918 have no carrier found by that search, and asks for carriers for those two to be named or created before any unit sourced from them is opened. The caveat is that issue search matches the numbers as text in titles and bodies, so an empty result here is evidence about the text and not proof that no carrier exists.

    11. Unit R measured, a budget on the plan rather than a target

    Measured by the same construction as section 0, in its own detached worktree so the tree it selects from is the tree on disk, head tree equal to written tree, porcelain 0, measurement copy deleted afterwards. Unit R tree d822becd92b72ac92508a68da859a03a6ddfaaf4, sole-parent commit 7754ffc7769ec5cabdda0c9c0a5caac72f03ee48 with the tip as its only parent, and the instrument printed Mutation-testing 1 package(s) from merge base 53cb5b5e7c80: extension (2 lines). R is two executable lines against a 500 cap, which is what a queue head that opens a chain should look like, and the arithmetic is recorded beside the figure because it is a measurement rather than an inference. Checked object by object at 0 API in the shared repo: the registry blob at the merged head 44e9009199d9cf59f91851e5b7e2c494b9e65577 is ae9baeef21989044ff9d26b82d4671ac823573c2 against the tip's b3b7a719871d45ac9bb7d08159e017a5b58a3f76; git diff --numstat from the tip to the sole-parent commit 7754ffc7769ec5cabdda0c9c0a5caac72f03ee48, whose only parent is 53cb5b5e7c8014d3226e5636bbca8999ed3b8526, reads 9 added and 0 removed for src/core/task/observationRegistry.ts and 26 added and 11 removed for src/core/task/__tests__/observationRegistry.spec.ts; and the same registry blob lives inside the unit tree d822becd92b72ac92508a68da859a03a6ddfaaf4. So the 2 the instrument printed is what nine added lines look like once the doc comment, the blank line and the closing brace are excluded and only the declaration and the return statement count as executable, and the spec contributing nothing is the same exclusion this campaign measures on both instruments.

    13. Open-pull collision check on the four owned files, taken before any unit branch exists

    File touch was computed locally, merge-base to head, after fetching the pull heads by sha, so the diffing itself cost no metered calls; three listing calls and one call for pull 43 paid for the pull inventory.

    • 42 pulls are open on the fork and exactly one of them touches a file this plan assigns to a unit: pull 43, open, head ref feat/fws-v2-1-atomic-publish-core, base main, 1 commit and 2 changed files, titled "feat(file-safety): atomic text publish primitive (S3 v2-1, epic [EPIC] File Write Safety Prevent Concurrent Write Races Data Corruption #1375)", and it touches src/services/file-safety/safeWriteText.ts, which this plan assigns to W at fws/u13-guarded-write-conflict. That is a real collision between an already-open pull and a unit this plan proposes, and it must be settled before W is opened.
    • No open pull other than the four monoliths touches observationRegistry.ts, guardedWrite.ts or committedWriteBufferConflictError.ts. The four monoliths do touch them, by 3, 3, 3 and 4 of the four files respectively, which is inheritance rather than a contract this plan creates, because those four pulls are the content sources named in section 6 and the plan's answer to them is the split.
    • fws/u3-observation-registry and fws/u3-observation-completeness are not the head refs of the fork-internal pulls 72, 73 and 78. Those three pull 72 = vps2/2226-u3b-preview, pull 73 = vps2/1921-u2-profile-view-refresh and pull 78 = vps2/1914‑u3-profile-deletion-writegroup wait: pull 78 = vps2/1921-u3-profile-deletion-writegroup, all three in the vps2/ naming and all three open. Across the pulls returned by the listing, which covers the 200 earliest-created pulls open or closed, no pull has either u3 observation branch as its head ref, so those two refs are fork-internal refs carrying no pull, and the caveat is that the listing covers the earliest 200 by creation rather than every pull in the repository.
    • A reading fault of mine is recorded here: my first read of pull 43 printed "undefined" for the head and base repository because I wrote String(x.repo).full_name instead of String(x.repo.full_name), and the repository identity above therefore rests on the listing endpoint being the fork's own pulls endpoint rather than on that malformed field.

    12. Faults from this round, recorded as faults

    My earlier statement that the fork carries no fws refs was an artefact of my own pattern: git ls-remote matches a pattern against the whole ref name, so refs/heads/fws/ with no wildcard matches nothing and printed zero while the fork actually carries sixteen such refs, and I retracted it by listing all refs and filtering locally. The junction defect and the three aborted runs that measured nothing because the instrument required full 40-character shas stand as recorded above.

    2385 re-split against 1395 (added 2026-11-11 by the size and mutation evidence lane)

    Fifth state, 2026-11-11: it supersedes the third and fourth states previously published in this section, which are no longer in the body; the numbers quoted in them were correct for the trees they named. The owning lane answered the dispute over the figures and the answer moved three of them, so this section now carries the chain as it ends. An addition to this split-plan comment on carrier 2021, which is the carrier 2385's decision sits on; the sections above are the 1915 / 1916 / 1917 / 1918 monolith plan and are untouched. With 1395 landing first and untouched the 2385 content re-splits against 1395's head into three units, all three under the 500-line cap.

    Fifth state: the chain as it now ends, with the formatting commit where it belongs

    99664d0a5fd7af4a6bd521dcff81e3448a19c830 (1395 head) → d7c20e317ad4880c39bb14651fcada2a2375f26f at tree d2b1e74530d7e87da4f4ae6a742c417a414e7edd, the formatting commit, which carries the formatted module blob 17dea7394f8d8b7a2ab04be5821dd20b649a1f71 and the formatted copy of the incoming spec → 35aa6f7eefd68026c605d3542294b0fc46ce09ab at tree 93dffc920caf6c3f433b1fd6d20a3732918c3226 → f9861f2446e9db117189fcceb75b094c2d46a33a at tree 4abd8f2fc6d8cf2d0541612a4f4934fc89b7ea84. The unit 2 and unit 3 trees are unchanged from the shape published earlier in this section; only unit 1's tree moved, from 841dca5b98a5eb793433b753539c535cc849a5a5 to d2b1e74530d7….

    Unit Head Tree Printed merge base Charged executable lines
    1 d7c20e317ad4… d2b1e74530d7… Mutation-testing 1 package(s) from merge base 99664d0a5fd7… : extension 242 242
    2 35aa6f7eefd6… 93dffc920caf… Mutation-testing 1 package(s) from merge base d7c20e317ad4… : extension 287 287
    3 f9861f2446e9… 4abd8f2fc6d8… Mutation-testing 1 package(s) from merge base 35aa6f7eefd6… : extension 131 131
    the single un-split 2385 tree f0460e4ec35f6a90314ef53e9f8a426aecd1a8af, for comparison priced against main tip 53cb5b5e7c8014d3226e5636bbca8999ed3b8526 526, over by 26

    Each manifest names 1 package, src (the extension), and all three units sit under the 500-line cap; for comparison the single un-split tree prices 526, over by 26, with src/services/file-safety/safeWriteText.ts at 505 and src/utils/fileLock.ts at 19.

    239 remains the honest figure for the tree 841dca5b98a5…, and both figures are quoted here beside their own parents rather than reconciled: 239 for tree 841dca5b98a5… from merge base 99664d0a5fd7…, 242 for tree d2b1e74530d7… from the same merge base. The formatter leg decided the dispute: at tree 841dca5b98a5… the declared formatter produces a non-empty diff of 6 added and 3 removed on src/services/file-safety/safeWriteText.ts and prettier --check on that directory exits 1, so that commit held the unformatted module, and 242 had belonged to a formatted file no commit contained at the moment it was quoted. The owning lane retracts its prettier pass at that head as a directory reading. Unit 2's figure moved from 291 to 287 for the same reason: its parent now carries the formatting it was previously credited with, so those lines are no longer part of unit 2's own delta.

    This lane re-read all four figures with its own instrument at the named trees, each from a fresh checkout with porcelain empty before the run: 242 at tree d2b1e74530d7…, 287 at tree 93dffc920caf…, 131 at tree 4abd8f2fc6d8… (unit 3 charging one file, src/utils/safeWriteJson.ts) and 239 at tree 841dca5b98a5… as measured earlier; git merge-base returns 99664d0a5fd7…, d7c20e317ad4… and 35aa6f7eefd6… as printed, and the formatter diff at the old unit 1 head reproduced at 6 added and 3 removed with prettier --check exiting 1 on that directory.

    The advisory line: its text replaces its count

    The count of one advisory per unit is retired in favour of what the line actually says. At all three heads it is a single stderr warning line reading extension related-test discovery could not start from a spawnSync of the worktree's vitest binary at ENOENT, identical in kind at all three heads. That is a disclosed tooling gap in the measurement environment — the worktree used for pricing carries no vitest binary on disk, in fact no node_modules at all — and it is not a Stryker-disable finding. The count of lines naming a disable directive is 0 in the unit 1 implementation blob d75f1d04fb73f02d2dce7e93997c537041a42520, in the unit 2 implementation blob d1e7cb8257b361986a1289b9b6cac04b55402503, in unit 3's src/utils/safeWriteJson.ts blob 4b90660c49e288e7b252d5bd59bfeb86c105e531 and in 1395's own 571d345098c3444a95b20b805a0712af20b31ac6, counted by this lane as 0 rows in each.

    The mechanical consequence, stated so nobody has to guess: related-test discovery selects tests, not lines, so the discovery failure does not move the priced line counts and no part of any price depends on it. This lane's own instrument prices from the committed diff alone and never executes a test, which is why it produced the same 242, 287 and 131 in a worktree that also has no vitest on disk.

    Rename attribution: where the false names entered and where they left

    The false provenance names entered at unit 1: git ls-tree -r on src/services/file-safety at unit 1's head lists 6 rows, three of them naming u2357.safeWriteText.spec.ts and its two satellites beside the two durability-named specs and the module — 5 rows inside __tests__ alone. The rename happens in unit 2: rename detection prints R100 for the integration copy and R100 for the stagingIdentity copy, with git diff --numstat reading 0 added and 0 removed across each of those two paths, so their blobs are identical before and after (388f862b53ad082f381a595388056e4b621424f7 and 6e23fcea9e25cf8cc97e7ce4b304f81b1849bc16), and R099 for the main spec at 1 added and 1 removed in the rename-only step from unit 1's formatted copy c5079581ae9aa82a861105512ab4f5bd0fbab15e to 007a8fa971338a5acc59cd777136500c0a00f18e.

    What a maintainer actually sees depends on which unit 1 they diff from. Diffing the previously published unit 1 head f5cc89852277… into unit 2 35aa6f7eefd6… prints R100 on both satellites and R099 on the main spec at 25 added against 23 removed: one import identifier following the new file name — - safeWriteText, becoming + safeWriteTextAtomic, — plus repeated rewrapping of await expect calls, 8 on each side of the printed hunk, where a single-line await expect(...).rejects.toThrow( with the message on its own line becomes the call on its own line inside await expect(. Diffing unit 1 as it now ends into unit 2 leaves the satellites byte-identical and the main spec at 1 added and 1 removed.

    Verification, attributed to trees and read from commits

    • Gate at tree 4abd8f2fc6d8… (head f9861f2446e9…, fresh checkout, porcelain empty before the run): eleven test files, 274 passing and 1 skipped, 275 counted in total, 0 failed, 0 unevaluated suites, fileLock.spec.ts 4 of 4, and the skip named safeWriteText.spec.ts > safeWriteText > review fixes > refuses to stage in a .file-safety-staging directory. At tree 93dffc920caf… the nine tracked test files read 269 in total with 0 failed and the same single skip.
    • Type-check: 0 rows at tree 4abd8f2fc6d8… for every file this chain touches. Recorded rather than smoothed: the same run reports 2 rows in src/integrations/misc/export-markdown.ts, a file the chain never touches — TS2694 for BrowserStateBlockParam against the installed @anthropic-ai/sdk types and the dependent TS7006 — so "0 rows" is true of the chain's files and not of the whole package.
    • Prettier, read from a commit at each head rather than from a directory, with porcelain empty at each: prettier --check on src/services/file-safety reports All matched files use Prettier code style at d7c20e317ad4…, at 35aa6f7eefd6… and at f9861f2446e9…, and exits 1 at the superseded f5cc89852277…. This is the leg whose directory reading caused the retraction above.
    • src/eslint-suppressions.json shows 0 changed rows across the whole chain, and src/utils/fileLock.ts is byte-identical to the 1395 head blob d8bdbf910b30a5320c7d178c76c2dc5763a7d174 at both ends, so the durability lock-path contract is untouched. knip is carried from the owning lane's report and was not executed by this lane.
    • Locally verified, not CI-verified: the fork runs no check runs at any head, and no remote head carries d7c20e317ad4…, 35aa6f7eefd6… or f9861f2446e9…, so nothing is pushed and no pull is opened.

    Consequence for the relief ask is unchanged: once 1395 merges, the relief ask on 2385 becomes unnecessary and should be withdrawn rather than approved, because the re-split brings the content under the cap on its own; until 1395 merges it stays open, because a re-split that has not landed is not under the cap.

  3. easonLiangWorldedtech commented on Nov 11, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Plan change: Unit R withdrawn, substantive supersession recorded by blob evidence

    Applies to the split plan posted on this issue. No pull moved by this comment and no branch was created.

    Evidence

    Blob ids read out of trees fetched by sha, differences computed locally:

    open pull head src/core/task/observationRegistry.ts blob reading
    2396 922549655a93218ad1ad782c2972d3cdff287641 b3b7a719871d45ac9bb7d08159e017a5b58a3f76 trunk blob carried unchanged, the file is not modified despite a title naming the registry
    1912 90f56d2cf08bee5e35ce8b54805f436ef664dbdf b3b7a719871d45ac9bb7d08159e017a5b58a3f76 trunk blob carried unchanged, the file is not modified despite a title naming the registry
    1394 12e591f6d5a0780a0271e629520e29cfa8608da2 3d06c826ff5901cf0c35a8f0feac21c659af836d carries the per-path release: declares forget(absolutePath: string): boolean with the identical executable body

    Against the blob ae9baeef21989044ff9d26b82d4671ac823573c2 that Unit R would have carried, 1394's blob differs by seven removed lines and seven added lines, and the difference is member order plus the rationale comment. Both blobs declare the same member surface, observe, get, has, clear, forget, close, get closed, get size, with the same retired check, and the forget body itself does not appear in the diff at all.

    Consequence for the plan

    • Unit R, fws/u10-per-path-release, is withdrawn. A unit whose whole contribution is a declaration order and a doc comment is not a unit, and opening it would put two open upstream pulls editing src/core/task/observationRegistry.ts into one contract.
    • The accessor and the per-path release arrive upstream through 1394.
    • Units S and P are to be based on that contract instead of on a new unit.

    Method consequence

    The title test would have produced the wrong answer in both directions: two of the three pulls whose titles name the registry do not touch the file at all, and one of them carries the whole claim. The plan therefore requires blob identity checked before a unit is proposed, not a title or a branch name.

    Recorded by lane f0-1528, 2026-11-11. Analysis cost 0 metered calls; this comment costs 2, the post and its read-back.

  4. easonLiangWorldedtech commented on Nov 11, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Dependency note, successor to the supersession note: what the guarded-write family sits on

    Nothing in this note creates a branch or moves a pull. The blob and diff figures below were read off git objects at 0 metered calls; the landing-order figures are measured in another lane and are carried here as carried numbers, named as such.

    1. Topology, as read rather than inferred

    Five org-side pulls were read: 2235, 2236 and 2237 at base sha 73931fe8be253b93ec11103a0672c615e849f895, and 2416 and 2385 at base sha 53cb5b5e7c8014d3226e5636bbca8999ed3b8526. All five have base repo Zoo-Code-Org/Zoo-Code and base ref main: no org-side unit in this campaign sits on a predecessor branch ref. What separates a later unit from an earlier one is which main commit it is based on, plus the declared merge order.

    Two pricing axes follow and are kept apart throughout:

    • the unit-only figure is a construct, a sole-parent commit-tree of a unit tree against a predecessor head;
    • the pull axis bases on main, so a later unit shows its predecessor content in its own pull diff until that predecessor merges.

    A number on the first axis is not a pull state and is not quoted as one.

    2. The dependency chain, layer by layer

    layer content state, by blob
    L0 registry accessor plus the per-path release forget inside open pull 1394 at 12e591f6d5a0780a0271e629520e29cfa8608da2, blob 3d06c826ff5901cf0c35a8f0feac21c659af836d. Unit R is withdrawn as carried.
    L1 src/services/file-safety/safeWriteText.ts six distinct blobs live at once and the file is absent from main: 571d345098c3 at 1395 head daee64d6fc88, 495595ebdcd2 at 2357 head d6fa7117e07b, f5acc5ea4018 at 2359 head ebd6d9e17df3, b75391b51ab5 at 2385 head 74fe4eab39eb, 29025b3a2814 at fork pull 43 head 29110ed66e48, 9115854370d2 at the 1918 head 1d993fb42d4b.
    L2 src/core/tools/guardedWrite.ts two live implementations, not none: 32ffe6cebadb (864 lines) at the 2359 head and cdbb704359b3 (715 lines) at the 1918 head. Absent from 2357, 2385, fork 43, 1394 and main.
    L2 src/integrations/editor/committedWriteBufferConflictError.ts uncontested exactly once: 61ac8a6da7fc at the 1918 head, absent from every other head read.
    L3 the two DiffViewProvider.ts lifecycle points blob 85c89ae166cc0ec45561c6bd321f0c66516e6d7b: line 901, save path, saveTask.observationRegistry.forget(absolutePath); lines 2278 and 2279, preview abandonment, the identity test comparing previewTask.observationRegistry.get(previewRecorded.path) against previewRecorded followed by previewTask.observationRegistry.forget(previewRecorded.path). In 1394 own blob 3c1d33ae283bed12d0c756503985eb0f9d5180ec there are 0 mentions of observationRegistry, guardedWrite, preOpenObservation or FileObservation, 0 call sites of .forget( and no machinery import. The gap between the two blobs is 1352 added lines and 81 removed lines across 21 hunks.

    3. Convergence has to be authored, not merged

    Across all 20 ordered pairs of the five implementations the other lane names (1395, 2357, 2359, 2385, fork 43), not one diff has 0 deleted lines: no implementation is a strict content superset of another, so no merge can pick a winner without dropping content someone wrote. That claim was recomputed here from the blobs, 0 of 20 pairs had 0 deletions, and the two nearest shapes reproduce exactly: 2357 to 2385 at plus 210, minus 15 and 1395 to 2357 at plus 855, minus 581; the third nearest is 2357 to 2359 at plus 258, minus 256. The set named there differs from the set read earlier in this note by one head, that lane counts 1395 where this note first counted 1918, and the two safeWriteText blobs differ (571d345098c3 against 9115854370d2), which is why the count of live implementations is six rather than five.

    This campaign resolves a merge conflict to preserve behaviour and prices whatever that produces; it does not resolve a conflict to satisfy the mutation gate. That sentence has to be read before any number below is read as a recommendation.

    4. Landing order and what it prices

    Measured in another lane and carried here:

    • 1395: 359 against main, and 155 to 222 and 162 to 212 in the other positions, so it stays under the 500-line cap wherever it sits.
    • 2357: 756 as a first unit, and 551 or 441 as a second unit depending only on which side wins safeWriteText.ts in the merge.
    • 2359: 577, and 603 to 615, and 641 to 643 in every order measured, because guardedWrite.ts contributes 508 counted lines on its own against a 500 limit, which no ordering rescues.

    5. Consequence for the unit count

    The plan section that drew S and P as two small units resting on 1394 was wrong in a way only a content test could find: both lifecycle points sit inside machinery 1394 does not have, and DiffViewProvider in the merged content imports resolveLockKey from services/file-safety/safeWriteText, the file that carries six live blobs across six heads. S and P inherit the safeWriteText contest rather than escaping it, so they are not two units buildable today: they are content that becomes buildable once the layer under them converges. The build stays held until the reconciliation experiment says which safeWriteText.ts implementation is canonical.

    Two shapes remain open and both are named. Shape A, fold the layer in: S stands on 1394 plus the file-safety and guarded-write layer, so the layer is part of S own base, the unit count drops and S own price carries the layer. Shape B, let the layer land first: whichever of the pulls that already carry a safeWriteText.ts converges upstream, and only then are S and P priced as small units against a base that already contains the layer. The choice turns on the landing order of 1395, 2357, 2359, 2385 and fork 43, and on which implementation the reconciliation experiment declares canonical.

    6. Method and caveats

    Blob and diff figures were resolved from refs/pull/*/head and tree objects by git, at 0 metered calls for the analysis. Pull state was not re-read for 2357, 2359, 2385 or fork 43 in this note: what was measured is blob identity between trees, not pull state. One listing call would say which fork branch refs back open pulls, and that call has not been spent. The statement that nothing competes for guardedWrite.ts was earlier written in this campaign and is corrected here: it has two live implementations, and that correction is a lead-side error recorded as the lead's, not dropped. Recorded by lane f0-1528, 2026-11-11.

  5. easonLiangWorldedtech commented on Nov 11, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Convergence decision for src/services/file-safety/safeWriteText.ts: a sequence with its prices

    This replaces the decision memo posted earlier on this issue. It is a campaign-technical call and the owner question it had asked went unanswered inside the window given. No branch was created and nothing moves on any pull by this comment. Blob, diff, export and marker figures were read off git objects and the six file bodies at 0 metered calls; case counts, failing-test-name counts, harness shares and priced figures are carried from the campaign lanes that measured them and are named as carried.

    Decision

    **2385 is the canonical content of src/services/file-safety/safeWriteText.ts**, and **1395 is the first unit onto trunk.** The reason is sequencing direction, not blob size. Landing the larger body first would force 1395 to delete trunk content it does not have: 1395to1918deletes 631 lines and1395to2357deletes 581, so a unit landing after a richer trunk would spend its diff removing lines rather than adding behaviour, which is not a unit. The larger feature set still wins on content because 2385 is the nearest thing to a superset the thirty ordered pairs contain:2357to2385` reads plus 210, minus 15, and 2385 carries the backup, staging and orphan-recovery families that 2357 and 2359 also carry.

    The sequence and what each step prices

    1. 1395 onto trunk, first. 359 counted lines against main, and 155 to 222 and 162 to 212 in the other positions measured, so it is under the 500-line cap wherever it sits. It is the one pull in this family whose price does not depend on the outcome.
    2. A reconciliation unit authored on trunk on top of 1395, carrying the 2385 feature set. Against 1395 own body the 2385 side is plus 1040, minus 571 as measured here from the two bodies. It adjudicates the 16 test names that fail in both directions, makes the 27 cases in 10 groups that 1395 asserts into trunk behaviour, and brings in the 24 cases in 8 groups that 2357 asserts from the 2385 side.
    3. Only then 2357 and 2359 as delta units behind it. 2359 still needs guardedWrite.ts carved at a seam the code actually has: that file alone is 508 counted lines against a 500 limit in every order measured, and no ordering rescues it.

    What the decision costs, stated plainly

    • 1395 carries a version of safeWriteText.ts that the reconciliation will change under it, so its review thread set may reopen on the new content.
    • 2385 stays open, held and CI-red until its turn comes, which is the same answer the board has been given about it twice.
    • S and P, the two DiffViewProvider lifecycle units, stay unbuildable until a base exists that already contains the converged layer.

    Contract facts, verified rather than copied

    Export sets differ in both directions. Five names exist only on the 1395 side: DaclCaptureError, PublishNotDurableError, DaclRestoreError, isPublishCommittedError, assertPublishWithinAuthorizedRoot. Thirteen exist only on the 2357 and 2385 side, including resolveLockKey, which 1395 does not export, and 2385 adds PublishedEntryMismatchError on top of the 2357 set.

    resolveFileLockPath is not a name in this file on either side. Inside the 1395 head tree it appears in two files, src/utils/fileLock.ts and src/utils/__tests__/fileLock.spec.ts; it appears in 0 files inside the 2357 head tree and 0 files inside the 2385 head tree; and it is absent from safeWriteText.ts on every head read. An earlier sentence in this campaign attributed it to the 1395 side of this file, which was an attribution to the wrong file rather than a phantom name.

    confineTo file-level facts. It appears in 4 files inside the 2357 head tree, src/services/mcp/McpHub.ts and its spec and src/utils/safeWriteJson.ts and its test, and in 0 files inside the 1395 and 2385 head trees. It appears in none of the six safeWriteText.ts bodies.

    The feature markers are asymmetric, and the counts are the asymmetry. backup: 77 at 2385, 69 at 2357, 67 at 2359, 44 at the 1918 head, 23 at fork pull 43, 3 at 1395. authorizedRoot: 7 at 1395 and 0 in every other body. no-replace: 8 at 2385, 5 at 2357, 5 at 2359, 0 at 1395, 0 at fork 43, 0 at 1918. ELOOP: 3 at 1395, 2 at 1918, 1 at each of 2357, 2359 and 2385, 0 at fork 43. Durability, authorized-root and DACL vocabulary lives at 1395; backup, staging and orphan-recovery vocabulary lives at 2385, 2357 and 2359. Neither side is a subset of the other in features.

    Harness caveat, not optional reading

    In each cross leg the harness classes outweigh behaviour, at 50 of 98 and 56 of 98. Ninety-eight failures are not ninety-eight behavioural regressions. The cross legs prove that two contracts and two feature sets differ; they do not rank either implementation as better behaved, and nothing in this decision uses those counts as a quality judgement.

    Method sentence

    A diff taken between two names when one of them does not exist is an empty diff, and an empty diff parses as zero added and zero deleted. That is how a first containment run in this campaign reported nothing at all, ten of ten pairs reading plus 0 minus 0, and it is why the thirty ordered pairs that this decision rests on were produced by the second run: six bodies written under one naming scheme, their byte sizes printed so an empty body cannot hide, and a well-formed three-field numstat line required before any count is taken. Across those thirty ordered pairs no diff has 0 deleted lines, which is the fact that makes the reconciliation above authored rather than a merge that picks a winner. Recorded by lane f0-1528, 2026-11-11.

  6. easonLiangWorldedtech commented on Nov 11, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    mutation-diff on 2385: a size-cap short circuit, named out of the job log

    Read from the Actions logs of run 43024669279 (job 130536222307, mutation-diff), the check-run
    inventory of the superseded head, and the annotations for that run. Facts first, then the one
    choice the instrument printed for itself.

    The step that failed, verbatim from the job log:

    BASE_SHA="$(git rev-parse "$HEAD_SHA^1")"
    node scripts/stryker-diff.mjs ci --base "$BASE_SHA" --head "$HEAD_SHA"
    
    HEAD_SHA: e6ce74173efcc89aa9ff51f2490f29ca503618ab
    PR_HEAD_SHA: e29615e60030ffec8eee5e858f264cb2040f7fa2
    

    Its output, verbatim:

    Mutation gate failed: extension has 548 changed executable lines (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion.
    ##[error]Process completed with exit code 1.
    

    So this is a scope-cap short circuit and not a crash: the line-count check refuses 548 against 500
    before Stryker starts, which is the whole 35 to 54 seconds of a job that never tested a mutant. The
    same conclusion sits under both head identities of this branch — 54 seconds at
    e29615e60030ffec8eee5e858f264cb2040f7fa2 (12:56:28Z -> 12:57:22Z) and 35 seconds at
    66bf48e033ad787cb0e9aa4650364fc0f6197174 — same tree, same parent, same conclusion — so the
    rewrite reproduced the failure rather than causing it.

    HEAD_SHA above is the pull-request merge result, not the branch head: the job is defined at
    .github/workflows/mutation-testing.yml@refs/pull/2385/merge and the checkout step landed on
    Merge e29615e6… into 53cb5b5e7c80…. Whether main's drift is inside the 548 was tested locally at
    zero API cost against refs/pull/2385/merge: git merge-base of
    e29615e60030ffec8eee5e858f264cb2040f7fa2 against that merge commit's first parent returns
    53cb5b5e7c8014d3226e5636bbca8999ed3b8526 — the first parent itself — so the branch was not stale
    under that merge and drift is not in the number. git diff --numstat --find-renames across that
    range puts 4,261 changed executable lines in src; the production files in it are

    • src/services/file-safety/safeWriteText.ts +1,313
    • src/utils/fileLock.ts +53
    • src/utils/safeWriteJson.ts +11 -2

    with the __tests__ files carrying the rest. The 548 is the gate's own executable-lines measure over
    those files.

    The decision the instrument prints for itself, quoted here rather than chosen here: split the PR
    or obtain a maintainer-reviewed narrow exclusion.

    Two infrastructure facts from the same log, recorded as observations and not as causes of the red:

    • Could not write the job summary: EISDIR: illegal operation on a directory, open '/tmp/stryker-summary-vkKojt' — this is why the check-run reads output.title and
      output.summary as null through the API, leaving only the wrapper annotation
      "Process completed with exit code 1." at .github line 12.
    • core Stryker preflight could not start: spawnSync /tmp/stryker-launch-T2rggu/node_modules/.bin/stryker ENOENT, and one surviving mutant reported as
      advisory: packages/core/src/value.ts:1: Survived BooleanLiteral mutant (replacement: false).

    Also measured, so the red is read at its right weight: the gate's own logic tests passed in the same
    job (Test mutation gate logic — 106 tests, 21 suites, 0 failures), and mutation-diff is not one
    of the six contexts the live branch ruleset lists as required — all six read completed and
    successful at both heads — so this leg blocks the campaign's own gate, not the ruleset.

  7. easonLiangWorldedtech commented on Nov 11, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Mutation-gate remedy decision for the safeWriteText unit: numbers, with sources labelled

    Two sources are kept apart throughout. CI-side figures come from the mutation-diff log as read by the campaign Lead and by u9. Local figures come from runs I made in a fresh worktree at the pull head using the repository own instrument, scripts/stryker-diff.mjs. Nothing here is offered as CI verified and no unit is described as green.

    CI-side, quoted rather than measured here. The leg reads completed failure at both heads of this pull, id 130536222307 at 12:56:28Z to 12:57:22Z on the superseded e29615e, reproduced in 35 seconds at 66bf48e, same tree 90819ea8087c and same parent 74f47a7, and the step printed "Mutation gate failed: extension has 548 changed executable lines (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion." The mechanism is in the script, it throws inside the manifest build at lines 264 to 271, before runMutation is called at line 752 or 782, which is why both runs lasted 35 and 54 seconds and why no mutant was ever tested. The same log carries two runner-environment failures that are not ours to fix, the job summary write failing with EISDIR on /tmp/stryker-summary-vkKojt, which is the named reason the API showed null output title and summary, and the core Stryker preflight failing to start with spawnSync stryker ENOENT.

    Measured locally. ci --base 53cb5b5 --head 66bf48e prices extension at 548 changed executable lines and prints the per-file split, src/services/file-safety/safeWriteText.ts 527, src/utils/fileLock.ts 19, src/utils/safeWriteJson.ts 2, and 527 plus 19 plus 2 is 548, so the local split reproduces the CI total. The parent 74f47a7 prices identically at 548 with the same per-file split, because the test-only row and its identity rewrite live in files under tests, a path segment the instrument excludes, so the rewrite neither caused nor contributed to the failure. Two quantities that are easy to confuse are different here, the pull diff shape of plus 1,313 lines on safeWriteText.ts, plus 53 on fileLock.ts and plus 11 minus 2 on safeWriteJson.ts, against the priced quantity, which for safeWriteText.ts is 527 of its 1,313 added lines, the file being 1,314 lines.

    Whether a split is constructible, and the arithmetic that decides it. For a file whose diff status is added the instrument charges every executable line of that file to the unit that creates it, script lines 232 to 235 enumerate the whole file and line 242 filters it to executable lines, while for a modified file it charges only that unit own changed lines. safeWriteText.ts alone prices at 527, above the 500 limit, so no partition of this pull clears the cap while keeping the file whole, and a partition under the cap exists only by cutting inside the module. The one seam in the file that is also a semantic boundary is line 641, between the supporting declarations and the single exported operation. Measured with the same instrument, unit A, the module truncated at line 640 together with the two utility changes, prices at 267 lines, 246 plus 19 plus 2, and unit B, the operation added on top of it, prices at 281 lines, all in safeWriteText.ts, both under 500. Those two candidates are probe objects rather than branches, fe4571896bae95b6cc7cbdb442200b5f7a94bcdb and d3f9ea55c7b14dc4e7d786c233bd9d9e4b3f7b71.

    What that seam costs, stated plainly. It invents a unit whose payload is a module with no operation in it, and it moves the tests rather than only the code. Across the three file-safety spec files there are 102 it-cases, of which 97 call the operation and 5 do not, on 2,470, 151 and 141 lines, so the creating unit keeps about 5 cases and the completing unit carries about 97. A compile at the seam was not run here, it needs a checkout with its own install, and the self-containment argument I have is a name-dependency one, the 640-line prefix names the operation only in comment prose and inside error-message string literals.

    What the alternative exclusion would actually cover. Running the same pricing with safeWriteText.ts excluded leaves extension at 21 priced lines across 2 files, fileLock.ts 19 and safeWriteJson.ts 2. The narrowest exclusion that clears the failure therefore removes 527 of 548 priced lines, 96.2 per cent of the mutation coverage the gate would otherwise exercise on the very file this pull delivers, and the span it covers is src/services/file-safety/safeWriteText.ts lines 1 to 1,314. The two utility changes need no exclusion, at 21 priced lines together. The gate has no line-level lever, its levers are package scoping in PACKAGE_CONFIGS and the excluded path segments, which already cover mocks, tests, fixtures and test-utils and are why 2,762 lines of file-safety specs contribute nothing to the price. A Stryker disable directive gates mutants on the lines it sits on and does not reduce a changed-line count at all, so a directive cannot clear a 548 against 500.

    A second, independent ground, measured locally. A dry-run of the same instrument over the whole 548-line delta reported "Instrumented 3 source file(s) with 547 mutant(s)", above the 400-mutant limit, so this pull would also fail the mutant cap if the line cap ever let it get that far. The dry run then failed on this box with an initial test run timeout, which is a runner fact and not a mutant verdict, the mutant count is printed before the dry run executes. Note the asymmetry, the CI-side 400-mutant cap was never reached on this pull because the line cap throws first.

    The fork fact. GET commits/34be795ec15cbf0dc20214468f0106f8e327c544/check-runs with per_page 100 and filter all returned status 200, total_count 0, 0 rows in the body and no Link pagination header, so the fork records no check runs at its own unit heads. That is stated as neither a failure nor evidence that anything passed, and it means the fork-internal unit pulls in this stack carry no CI rows at all.

    Whose decision this is. The choice between a split and a narrow exclusion is a maintainer act on the carrier ruling recorded as issue 2021 comment 6790813203, which makes this pull the owner of safeWriteText.ts with 1395 landing first. A split amends which branch owns the introduction of that file and which completes it, and either ordering keeps 1395 landing before whichever unit carries the durability text. No push of ours clears the standing changes-requested verdict on this pull, and another push at the current head restarts the advisory lanes without changing the line count. This comment posts no verdict and moves no branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions