mulmoterminal 7.2.0 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +13 -10
  2. package/blueprints/compare/checks/articles.d.mts +8 -0
  3. package/blueprints/compare/checks/articles.mjs +109 -0
  4. package/blueprints/compare/checks/base.mjs +12 -0
  5. package/blueprints/compare/checks/comparison.mjs +22 -0
  6. package/blueprints/compare/checks/comparisonView.d.mts +4 -0
  7. package/blueprints/compare/checks/comparisonView.mjs +14 -0
  8. package/blueprints/compare/checks/report.mjs +47 -0
  9. package/blueprints/compare/checks/versions.mjs +33 -0
  10. package/blueprints/compare/hearing.json +27 -0
  11. package/blueprints/compare/manifest.json +9 -0
  12. package/blueprints/compare/presets/itaku-kaitei/kyu.txt +31 -0
  13. package/blueprints/compare/presets/itaku-kaitei/shin.txt +31 -0
  14. package/blueprints/compare/presets.json +11 -0
  15. package/blueprints/compare/skills/pair/SKILL.md +51 -0
  16. package/blueprints/compare/skills/report/SKILL.md +33 -0
  17. package/blueprints/compare/steps.json +20 -0
  18. package/blueprints/docs/checks/markdown.d.mts +9 -0
  19. package/blueprints/docs/checks/markdown.mjs +16 -0
  20. package/blueprints/from-collection/skills/acceptance/SKILL.md +3 -0
  21. package/blueprints/from-collection/skills/acceptance-cloudflare/SKILL.md +3 -0
  22. package/blueprints/from-collection/skills/acceptance-firebase/SKILL.md +3 -0
  23. package/blueprints/from-collection/skills/acceptance-supabase/SKILL.md +3 -0
  24. package/blueprints/from-collection/skills/actions/SKILL.md +2 -1
  25. package/blueprints/from-collection/skills/actions-cloudflare/SKILL.md +2 -1
  26. package/blueprints/from-collection/skills/actions-firebase/SKILL.md +2 -1
  27. package/blueprints/from-collection/skills/actions-supabase/SKILL.md +2 -1
  28. package/blueprints/from-collection/skills/import-supabase/SKILL.md +7 -3
  29. package/blueprints/from-collection/spec/conversion.md +2 -2
  30. package/blueprints/from-collection/spec/requirements.md +2 -2
  31. package/blueprints/summarize/checks/base.mjs +12 -0
  32. package/blueprints/summarize/checks/parts.d.mts +14 -0
  33. package/blueprints/summarize/checks/parts.mjs +115 -0
  34. package/blueprints/summarize/checks/report.mjs +51 -0
  35. package/blueprints/summarize/checks/summary.mjs +48 -0
  36. package/blueprints/summarize/hearing.json +34 -0
  37. package/blueprints/summarize/manifest.json +9 -0
  38. package/blueprints/summarize/presets/keihi/keihi.md +19 -0
  39. package/blueprints/summarize/presets.json +11 -0
  40. package/blueprints/summarize/skills/report/SKILL.md +26 -0
  41. package/blueprints/summarize/skills/summarize/SKILL.md +50 -0
  42. package/blueprints/summarize/steps.json +20 -0
  43. package/blueprints/supabase/checks/advisors.mjs +26 -3
  44. package/blueprints/supabase/checks/declared-open.d.mts +3 -0
  45. package/blueprints/supabase/checks/declared-open.mjs +48 -0
  46. package/blueprints/supabase/checks/local-stack.sh +25 -1
  47. package/blueprints/supabase/checks/public-tables.mjs +20 -0
  48. package/blueprints/supabase/checks/security-probe.mjs +2 -18
  49. package/blueprints/supabase/skills/auth/SKILL.md +2 -1
  50. package/common/blueprint/collectionSource.ts +17 -1
  51. package/common/blueprint/personalData.ts +80 -0
  52. package/common/blueprint/refusal.ts +16 -0
  53. package/common/blueprint/sourceStatus.ts +14 -0
  54. package/common/headerButtonEntries.ts +115 -14
  55. package/common/headerButtonFolders.ts +149 -0
  56. package/common/hiddenCharacters.ts +34 -1
  57. package/common/keymapPresets.ts +39 -5
  58. package/common/viewTargets.ts +5 -0
  59. package/dist/assets/{abnfDiagram-O67JEVCF-DfYKie1f.js → abnfDiagram-O67JEVCF-BnQ6PasO.js} +1 -1
  60. package/dist/assets/architecture-7GRP2DOG-WG-E0vWg.js +1 -0
  61. package/dist/assets/{architectureDiagram-NJMV4G6O-vsLzjzQx.js → architectureDiagram-NJMV4G6O-BKtC0YZv.js} +1 -1
  62. package/dist/assets/{blockDiagram-BEXU5L5S-DssDT1TO.js → blockDiagram-BEXU5L5S-az-TvPjp.js} +1 -1
  63. package/dist/assets/{c4Diagram-YGBWAQC7-P0CrW8QT.js → c4Diagram-YGBWAQC7-xDds9K_k.js} +1 -1
  64. package/dist/assets/channel-Dyz5xJeN.js +1 -0
  65. package/dist/assets/{chunk-3FUC2YCW-CvTm_ho-.js → chunk-3FUC2YCW-DLr3D6jO.js} +1 -1
  66. package/dist/assets/{chunk-5DYCD2WN-BVnziItI.js → chunk-5DYCD2WN-5H5gAWR8.js} +1 -1
  67. package/dist/assets/{chunk-742MDFTN-CccjktcU.js → chunk-742MDFTN-CcavS_nS.js} +1 -1
  68. package/dist/assets/{chunk-7INBJB4K-Bf0Shv2-.js → chunk-7INBJB4K-DplkzUdE.js} +1 -1
  69. package/dist/assets/{chunk-7M6MHVWA-BtwnZqTt.js → chunk-7M6MHVWA-D4sIxByI.js} +1 -1
  70. package/dist/assets/{chunk-7PRAP22T-59aFrPRm.js → chunk-7PRAP22T-sPfItgkT.js} +1 -1
  71. package/dist/assets/{chunk-GTNCS2PH-CT1kVgRW.js → chunk-GTNCS2PH-o55BwD7R.js} +1 -1
  72. package/dist/assets/{chunk-GWA4HPMP-C94KrS1K.js → chunk-GWA4HPMP-DsJRcpHG.js} +1 -1
  73. package/dist/assets/{chunk-MBY4JIJT-D9Nn3Npg.js → chunk-MBY4JIJT-BCFvhNqi.js} +1 -1
  74. package/dist/assets/{chunk-NETBCI7D-C-jbM8IP.js → chunk-NETBCI7D-GQGQ0f8K.js} +1 -1
  75. package/dist/assets/{chunk-O7XYJQB3-BQYzKLhh.js → chunk-O7XYJQB3-CNBOCJb8.js} +1 -1
  76. package/dist/assets/{chunk-UA2S7LBM-D7ovzGAr.js → chunk-UA2S7LBM-DVKHgPob.js} +1 -1
  77. package/dist/assets/{chunk-WEXAMYUT-BmlxOjG3.js → chunk-WEXAMYUT-CuAljQbF.js} +1 -1
  78. package/dist/assets/{chunk-Z7XXMR3K-BSVBhxk7.js → chunk-Z7XXMR3K-ChACA7mW.js} +1 -1
  79. package/dist/assets/{chunk-ZIGJFQKS-x3IHLAdW.js → chunk-ZIGJFQKS-BWf0ZX3U.js} +1 -1
  80. package/dist/assets/{classDiagram-v2-NBCMYWYE-C6eb81l4.js → classDiagram-v2-NBCMYWYE-2EURLE58.js} +1 -1
  81. package/dist/assets/{cynefin-OW5HDTMX-CM1PFDYP.js → cynefin-OW5HDTMX-C5kb1EwK.js} +1 -1
  82. package/dist/assets/{cynefinDiagram-VND7K2PF-CxhcOZTy.js → cynefinDiagram-VND7K2PF-DMytcOCT.js} +1 -1
  83. package/dist/assets/{dagre-6A5THRUB-D5O9prrg.js → dagre-6A5THRUB-CUUE8Q1U.js} +1 -1
  84. package/dist/assets/{diagram-22UHCM2B-h7R_GutV.js → diagram-22UHCM2B-CC1W7G0a.js} +1 -1
  85. package/dist/assets/{diagram-3UASUU5V-DPmc5Ai4.js → diagram-3UASUU5V-DMERag7H.js} +1 -1
  86. package/dist/assets/{diagram-ATOU4E4O-COI1PRUd.js → diagram-ATOU4E4O-DAMncBlv.js} +1 -1
  87. package/dist/assets/{diagram-CDSNMT55-ehWBAAIO.js → diagram-CDSNMT55-C69-LfJU.js} +1 -1
  88. package/dist/assets/{diagram-MLGK6HIB-CXxA6YAe.js → diagram-MLGK6HIB-Qc95cLSN.js} +1 -1
  89. package/dist/assets/{diagram-MPIPVDR6-B6ANSQ5G.js → diagram-MPIPVDR6-DrmRWZxC.js} +1 -1
  90. package/dist/assets/{dist-Bx7SWRAa.js → dist-B84r2Zu4.js} +1 -1
  91. package/dist/assets/{dist-BnY3MUXM.js → dist-BFtxixYt.js} +1 -1
  92. package/dist/assets/{dist-DHjjPgmc.js → dist-C9QzqYij.js} +1 -1
  93. package/dist/assets/{dist-DeIGO4lT.js → dist-CZ9-VOqZ.js} +1 -1
  94. package/dist/assets/{dist-Dn4ta1sK.js → dist-DF-Vjhnj.js} +1 -1
  95. package/dist/assets/{ebnfDiagram-ZINNZB2B-ByHtvU2q.js → ebnfDiagram-ZINNZB2B-DbzDNXYn.js} +1 -1
  96. package/dist/assets/{elk-276RUBZZ-BF00ltRi.js → elk-276RUBZZ-CLrfQluT.js} +1 -1
  97. package/dist/assets/{erDiagram-OPXOYQCR-nW_7R9Lb.js → erDiagram-OPXOYQCR-B1BCJu2j.js} +1 -1
  98. package/dist/assets/eventmodeling-NTZA5JFV-DO2IaAYE.js +1 -0
  99. package/dist/assets/flowDiagram-KWPJA3E3-Os30OifC.js +1 -0
  100. package/dist/assets/{ganttDiagram-FUAMR5RP-DaWxnbRm.js → ganttDiagram-FUAMR5RP-D0VKOYxQ.js} +1 -1
  101. package/dist/assets/{gitGraph-4MIJSDKK-F7iDxaID.js → gitGraph-4MIJSDKK-CzOJUjdw.js} +1 -1
  102. package/dist/assets/{gitGraphDiagram-X574FWY7-D9fTMDaK.js → gitGraphDiagram-X574FWY7-vDZYUYtS.js} +1 -1
  103. package/dist/assets/index-DyqFaGZi.css +1 -0
  104. package/dist/assets/{index-ffQ8fSEK.js → index-K_24iRwq.js} +355 -355
  105. package/dist/assets/{info-A6RAGUB7-BIvkp14y.js → info-A6RAGUB7-B9K2IdSh.js} +1 -1
  106. package/dist/assets/{infoDiagram-VRGFBTTK-CF3pSadX.js → infoDiagram-VRGFBTTK-CXpbV_l5.js} +1 -1
  107. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CnStwZlQ.js → ishikawaDiagram-OU5B5YK6-CerjLKSu.js} +1 -1
  108. package/dist/assets/{journeyDiagram-ZHPQQLJL-X6FpP2mU.js → journeyDiagram-ZHPQQLJL-CYE7e5SQ.js} +1 -1
  109. package/dist/assets/{kanban-definition-PNTS6WVX-DGW7MkGx.js → kanban-definition-PNTS6WVX-hTQifntC.js} +1 -1
  110. package/dist/assets/{line-G8J9PhiP.js → line-DHCgFjyl.js} +1 -1
  111. package/dist/assets/{mermaid-parser.core-DlsoX-YC.js → mermaid-parser.core-Ci8-dBWZ.js} +3 -3
  112. package/dist/assets/{mermaid.core-DPC-93k6.js → mermaid.core-D1g_MhHe.js} +4 -4
  113. package/dist/assets/{mindmap-definition-NLK3R4M7-D-K5bSnd.js → mindmap-definition-NLK3R4M7-3-mAUeFh.js} +1 -1
  114. package/dist/assets/{packet-AYTQ26CC-CmDsNYF8.js → packet-AYTQ26CC-Bi4OEG7f.js} +1 -1
  115. package/dist/assets/{pegDiagram-GJSIUBJH-3rBm1VWr.js → pegDiagram-GJSIUBJH-BvlMXL59.js} +1 -1
  116. package/dist/assets/{pie-WAS4IAKB-DwWpCstp.js → pie-WAS4IAKB-D0Bzfos1.js} +1 -1
  117. package/dist/assets/{pieDiagram-5QR66LMP-IolT-z46.js → pieDiagram-5QR66LMP-ByI7bRXL.js} +1 -1
  118. package/dist/assets/{quadrantDiagram-O4NWA36T-Biq9fhdq.js → quadrantDiagram-O4NWA36T-DHt1QBiU.js} +1 -1
  119. package/dist/assets/{radar-RG4KPBEZ-DH0gPMqI.js → radar-RG4KPBEZ-DNfkRPMH.js} +1 -1
  120. package/dist/assets/{railroad-74A4TZTK-SDx9VkwS.js → railroad-74A4TZTK-1yUJUd_D.js} +1 -1
  121. package/dist/assets/railroad-abnf-HS5TGJTU-D3EYjt_F.js +1 -0
  122. package/dist/assets/railroad-ebnf-LZEXJU2U-8aE6RTc7.js +1 -0
  123. package/dist/assets/railroad-peg-WCYAUIDC-CFCfxkTd.js +1 -0
  124. package/dist/assets/{railroadDiagram-XR7U4H2S-kQzwizD6.js → railroadDiagram-XR7U4H2S-y8FT2bjZ.js} +1 -1
  125. package/dist/assets/{requirementDiagram-PLB6GJNP-B3spDS6j.js → requirementDiagram-PLB6GJNP-DCE3eASQ.js} +1 -1
  126. package/dist/assets/{sankeyDiagram-IPEJSGJF-DkXWUHlz.js → sankeyDiagram-IPEJSGJF-DkfyNgDg.js} +1 -1
  127. package/dist/assets/{sequenceDiagram-PO4LG4MO-Dt80gEIt.js → sequenceDiagram-PO4LG4MO-DFeaLdZP.js} +1 -1
  128. package/dist/assets/{stateDiagram-v2-GCMORJYK-CG5xmXxG.js → stateDiagram-v2-GCMORJYK-DXKYQ2gY.js} +1 -1
  129. package/dist/assets/{swimlanes-2SLR337P-B3S4pJ9E.js → swimlanes-2SLR337P-BpbeyLoP.js} +1 -1
  130. package/dist/assets/swimlanesDiagram-TC7HE7FX-BL6ovFkZ.js +8 -0
  131. package/dist/assets/{timeline-definition-EJHVYXUP-DuKW3pM-.js → timeline-definition-EJHVYXUP-CHZxYKup.js} +1 -1
  132. package/dist/assets/{treeView-Q6P3EWNA-CeeuwKmy.js → treeView-Q6P3EWNA-DnkPtA3-.js} +1 -1
  133. package/dist/assets/{treemap-WGGIJYW6-CFK6V3lz.js → treemap-WGGIJYW6-DLBjcQqV.js} +1 -1
  134. package/dist/assets/{usecaseDiagram-POWQR4AR-CYbKz4kt.js → usecaseDiagram-POWQR4AR-ChaNA2Rg.js} +1 -1
  135. package/dist/assets/{vennDiagram-UO4OBE2U-De8JmnbH.js → vennDiagram-UO4OBE2U-pLa46WbE.js} +1 -1
  136. package/dist/assets/{wardley-WFR3VGLG-BUvlimn0.js → wardley-WFR3VGLG-RCPsNr08.js} +1 -1
  137. package/dist/assets/{wardleyDiagram-VNRHLVJA-e3jrcLF3.js → wardleyDiagram-VNRHLVJA-BIvz-wXl.js} +1 -1
  138. package/dist/assets/{xychartDiagram-PMCCYNJV-C1Q2Y2pt.js → xychartDiagram-PMCCYNJV-GkaA0v7z.js} +1 -1
  139. package/dist/index.html +2 -2
  140. package/docs/guide/en/v7.3.0.md +92 -0
  141. package/docs/guide/ja/v7.3.0.md +89 -0
  142. package/package.json +1 -1
  143. package/server/backends/rawServingPlan.ts +9 -8
  144. package/server/blueprint/collectionSnapshot.ts +37 -8
  145. package/server/blueprint/packs.ts +4 -1
  146. package/server/blueprint/routes.ts +49 -4
  147. package/server/blueprint/sourceFingerprint.ts +56 -0
  148. package/server/blueprint/sourceStatus.ts +38 -0
  149. package/server/config/agent-entry-routes.ts +3 -2
  150. package/server/config/config-routes.ts +25 -9
  151. package/server/config/config-schema.ts +1 -1
  152. package/server/config/header-button-routes.ts +55 -7
  153. package/server/config/header-config.ts +1 -1
  154. package/server/files/mdPreviewReporter.ts +10 -2
  155. package/server/files/pathContainment.ts +12 -3
  156. package/server/files/tree-ops.ts +14 -3
  157. package/server/skills/mulmoterminal-config/SKILL.md +3 -3
  158. package/server/skills/mulmoterminal-header/SKILL.md +2 -2
  159. package/server/skills/mulmoterminal-keys/SKILL.md +1 -1
  160. package/dist/assets/architecture-7GRP2DOG-BuiSlwXB.js +0 -1
  161. package/dist/assets/channel-BrsbZpfz.js +0 -1
  162. package/dist/assets/eventmodeling-NTZA5JFV-C74VV9Z5.js +0 -1
  163. package/dist/assets/flowDiagram-KWPJA3E3-dKJG6rhg.js +0 -1
  164. package/dist/assets/index-xE6PHMoe.css +0 -1
  165. package/dist/assets/railroad-abnf-HS5TGJTU-CCSPvBtI.js +0 -1
  166. package/dist/assets/railroad-ebnf-LZEXJU2U-DTWbjX18.js +0 -1
  167. package/dist/assets/railroad-peg-WCYAUIDC--dNY7xHy.js +0 -1
  168. package/dist/assets/swimlanesDiagram-TC7HE7FX-5BPq1RA-.js +0 -8
package/README.md CHANGED
@@ -183,18 +183,19 @@ than as bytes (files within the session's working directory only):
183
183
  **A line after the path is followed.** `src/a.ts:42`, `src/a.ts:42:7` (gcc, clang, `eslint -f unix`,
184
184
  stack traces) and `src/a.ts(12,5)` (tsc) are one link. In the pane, and in the Files view, the file
185
185
  opens as text with the caret on that line and column — an HTML page, an SVG or a table included, and a
186
- tab reading in Preview switches to Edit. A route that renders the file in a new tab (`.md`, `.json`,
186
+ tab reading in Preview switches to Edit. An image, a PDF, a video or an audio file has no line to go
187
+ to and simply opens as itself. A route that renders the file in a new tab (`.md`, `.json`,
187
188
  `.csv`, `.html` with no cell enlarged) ignores the line. When the pane beside an enlarged cell takes
188
189
  the click, the keyboard stays in the terminal; the full-screen Files view, with no terminal beside it,
189
190
  puts it in the editor. (ESLint's default output puts the path on a line of its own, with no line number to follow.)
190
191
 
191
192
  **While a grid cell is enlarged, the [Files pane](#files-view-browse--edit) takes the click first** — every
192
- row above except PDF, video and audio, since the pane is the same editor with a Preview for
193
- Markdown, HTML and SVG, and shows an image as a picture. The file opens *beside* the terminal
193
+ row above, since the pane is the same editor with a Preview for Markdown, HTML and SVG, shows an
194
+ image as a picture and a PDF in a frame, and plays a video or an audio file with the browser's own
195
+ controls (seeking included, whatever the file's size). The file opens *beside* the terminal
194
196
  that printed it, and the pane opens itself if it was closed. It declines, leaving the routing
195
- above untouched, when nothing is enlarged, when the path is not under that cell's own directory
196
- (the pane cannot walk above its root), or for a PDF, a video or an audio file, which only a
197
- browser tab can play.
197
+ above untouched, when nothing is enlarged, or when the path is not under that cell's own directory
198
+ (the pane cannot walk above its root). A file too large to edit as text still offers **Open in OS**.
198
199
 
199
200
  Highlighting in the Files view covers the JS/TS family, JSON and Markdown (the modes
200
201
  `cmEditor.ts` bundles); other languages open as plain text.
@@ -823,7 +824,7 @@ The Settings modal (the gear button) persists per-user UI choices to `~/.mulmote
823
824
  | `autoDirIcon` | Whether a project that sets no `icon` shows the favicon its repository already ships (`public/favicon.svg`, `apple-touch-icon.png`, a web manifest — first hit wins, ordered by how the image survives at 14px). **On by default**; Settings → **Directory appearance** has the switch. A single project opts out with `"icon": false` in its own `.mulmoterminal.json`, which this does not override. A key that was written and got it wrong shows nothing rather than falling back — a broken setting has to look broken. `true` / `false` only. |
824
825
  | `cockpitLines` | `{ summary, prompt, response }` — how many lines each **cockpit-roster** row shows before it clamps (default `2` / `2` / `3`, each clamped to `1`–`20`). Raising them trades how many sessions fit on screen for reading a long one in place. Three steppers in Settings → **Waiting rows**. |
825
826
  | `showLoadAverage` | Whether the grid header draws this machine's **load average** beside the 5h / 7d usage windows, as a percentage of its cores (`load 334%` = a 20-core machine with 66.8 runnable processes). **On by default**; amber at 100%, red at 200%, hover for the raw 1 / 5 / 15-minute figures. Settings → **Grid header read-outs** has the switch. A host that keeps no load average (Windows) shows nothing rather than `0%`, whatever this says. `true` / `false` only. |
826
- | `remoteServer` | **Experimental.** The server runs on another machine than your browser (an SSH tunnel — [guide](https://receptron.github.io/mulmoterminal/guide/en/remote.html)): the actions that would act on the server's screen (the file dialog, the file manager, the OS app) are hidden or say why, and a dropped file is always uploaded. **Off unless `true`**; set it in the server's config. |
827
+ | `remoteServer` | **Experimental.** The server runs on another machine than your browser (an SSH tunnel — [guide](https://receptron.github.io/mulmoterminal/guide/en/remote.html)): the actions that would act on the server's screen (the file dialog, the file manager, the OS app) are hidden or say why, and a dropped file is always uploaded. **Off unless `true`**; Settings → **Sessions and background tasks** has the box (or set it in the server's config). |
827
828
  | `fontFamily` | The **terminal font** every session renders in — a CSS font-family stack, e.g. `"'Cica', 'MS Gothic', monospace"`. Set it in Settings → **Terminal font**, applied at once; editing the file instead needs a **restart** (this config is read once at startup). Unset uses the built-in stack (JetBrains Mono / Fira Code / Menlo / Consolas, then CJK faces for Japanese, Korean and Chinese). Unlike the per-browser font **size**, this is one value for the whole host — it names fonts, and which fonts exist is a property of the machine. A directory can override it. See the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#font-family). |
828
829
 
829
830
  Every MulmoTerminal on the machine shares this one file, so an older build could save over a key a
@@ -1342,11 +1343,13 @@ or changed (amber), and a notch where lines were removed, as VS Code's gutter sh
1342
1343
  to date as you type. **Changes** in the header also shows the removed lines in place, as a unified
1343
1344
  diff. Nothing is marked for a file outside git or one not yet committed.
1344
1345
 
1345
- **HTML pages and images show in the pane too.** An `.html` file has a **Preview** like a Markdown
1346
+ **HTML pages, images, PDFs, video and audio show in the pane too.** An `.html` file has a **Preview** like a Markdown
1346
1347
  file: the page itself, sandboxed as presentHtml pages are — its scripts run on an opaque origin with
1347
1348
  no fetch/XHR; images and the curated CDN list still load, and so does an image it links beside it,
1348
- while a relative stylesheet or script does not. An `.svg` previews as the picture it draws, and a PNG, JPEG, GIF or WebP opens as the image
1349
- rather than as "not text". A path to any of these clicked in terminal output opens in the pane when
1349
+ while a relative stylesheet or script does not. An `.svg` previews as the picture it draws, a PNG, JPEG, GIF or WebP opens as the image
1350
+ rather than as "not text", a PDF opens in a frame, and a video or audio file plays with the
1351
+ browser's controls, seeking by range rather than fetching the whole file. A text file too large to edit
1352
+ says so and still offers **Open in OS**. A path to any of these clicked in terminal output opens in the pane when
1350
1353
  it is up, so a chart an agent just wrote stays in the grid. They are served only under the workspace
1351
1354
  or a terminal's directory, like every other file the app hands to the browser.
1352
1355
 
@@ -0,0 +1,8 @@
1
+ // Types for articles.mjs, which the pack runs with plain node and the specs import.
2
+ export type Article = { readonly address: string; readonly label: string; readonly body: string };
3
+ export type Row = { readonly old?: string | null; readonly new?: string | null; readonly change: string; readonly what?: string };
4
+
5
+ export declare const CHANGES: readonly string[];
6
+ export declare const articlesIn: (tree: unknown, text: string) => Article[];
7
+ export declare const mentions: (text: string, name: string) => boolean;
8
+ export declare const comparisonProblems: (rows: unknown, olds: readonly Article[], news: readonly Article[]) => string[];
@@ -0,0 +1,109 @@
1
+ // Two versions of a document, article by article: each article's body as chaff's tree spans it, and whether the
2
+ // pairing an agent wrote says the truth about them. What changed is read by the agent; whether anything changed
3
+ // at all is decided here, by comparing text, so "unchanged" cannot be written over a changed article or the
4
+ // other way round.
5
+
6
+ export const CHANGES = ["same", "changed", "added", "removed"];
7
+
8
+ // Spacing aside: a reflowed line is not a change to what an article says.
9
+ const squash = (text) => String(text).replace(/\s+/gu, "");
10
+
11
+ const articleNodes = (node) => [
12
+ ...(node?.kind === "article" && typeof node.address === "string" && node.address !== "" ? [node] : []),
13
+ ...(Array.isArray(node?.children) ? node.children.flatMap(articleNodes) : []),
14
+ ];
15
+
16
+ // The article's own number (第4条, Article 4) is where it sits, not what it says: a renumbered article is the same.
17
+ const withoutLabel = (text, label) => {
18
+ const trimmed = text.trimStart();
19
+ return typeof label === "string" && label !== "" && trimmed.startsWith(label) ? trimmed.slice(label.length) : trimmed;
20
+ };
21
+
22
+ /** Each article of `text` as `tree` (chaff tree --format json) reads it: its address, label and body. */
23
+ export const articlesIn = (tree, text) =>
24
+ articleNodes(tree).map((node) => {
25
+ const whole = String(text).slice(node.span?.start ?? 0, node.span?.end ?? 0);
26
+ return { address: node.address, label: node.attrs?.label ?? node.address, body: squash(withoutLabel(whole, node.attrs?.label)) };
27
+ });
28
+
29
+ const rowShape = (row, index) => {
30
+ if (row === null || typeof row !== "object") return `row ${index + 1} is not an object`;
31
+ if (!CHANGES.includes(row.change)) return `row ${index + 1}: "change" must be one of ${CHANGES.join(", ")}`;
32
+ const has = (side) => typeof row[side] === "string" && row[side] !== "";
33
+ const wanted = { same: [true, true], changed: [true, true], added: [false, true], removed: [true, false] }[row.change];
34
+ if (has("old") !== wanted[0] || has("new") !== wanted[1])
35
+ return `row ${index + 1}: a "${row.change}" row has ${wanted[0] ? "an" : "no"} "old" and ${wanted[1] ? "a" : "no"} "new"`;
36
+ if (row.change === "changed" && !(typeof row.what === "string" && row.what.trim() !== "")) return `row ${index + 1}: say in "what" what changed`;
37
+ return null;
38
+ };
39
+
40
+ // Every article of a version in exactly one row, and no row naming one that is not there.
41
+ const coverage = (rows, articles, side, version) => {
42
+ const named = rows.map((row) => row[side]).filter((address) => typeof address === "string" && address !== "");
43
+ const known = articles.map((article) => article.address);
44
+ return [
45
+ ...known.filter((address) => !named.includes(address)).map((address) => `${version} ${labelOf(articles, address)}: in no row`),
46
+ ...[...new Set(named.filter((address, index) => named.indexOf(address) !== index))].map(
47
+ (address) => `${version} ${labelOf(articles, address)}: in more than one row`,
48
+ ),
49
+ ...named.filter((address) => !known.includes(address)).map((address) => `${version} has no article at ${address}`),
50
+ ];
51
+ };
52
+
53
+ const labelOf = (articles, address) => articles.find((article) => article.address === address)?.label ?? address;
54
+
55
+ // A pair said to be the same must read the same, and one said to have changed must not.
56
+ const truth = (row, olds, news) => {
57
+ if (row.change !== "same" && row.change !== "changed") return null;
58
+ const [was, now] = [olds.find((article) => article.address === row.old), news.find((article) => article.address === row.new)];
59
+ if (!was || !now) return null;
60
+ const pair = `${labelOf(olds, row.old)} → ${labelOf(news, row.new)}`;
61
+ if (row.change === "same" && was.body !== now.body) return `${pair}: written "same", but the text differs`;
62
+ if (row.change === "changed" && was.body === now.body) return `${pair}: written "changed", but the text is the same`;
63
+ return null;
64
+ };
65
+
66
+ // A provision removed from the old version and added to the new one word for word is one that moved: a pair missed.
67
+ const missedPairs = (rows, olds, news) => {
68
+ const bodyOf = (articles, address) => articles.find((article) => article.address === address)?.body;
69
+ const added = rows.filter((row) => row.change === "added").map((row) => row.new);
70
+ return rows
71
+ .filter((row) => row.change === "removed")
72
+ .flatMap((row) => {
73
+ const body = bodyOf(olds, row.old);
74
+ const twin = added.find((address) => body !== undefined && bodyOf(news, address) === body);
75
+ return twin === undefined ? [] : [`${labelOf(olds, row.old)} (removed) and ${labelOf(news, twin)} (added) read the same: pair them as "same"`];
76
+ });
77
+ };
78
+
79
+ /** What is wrong with `rows` (the pairing) for the articles `olds` and `news`. */
80
+ export const comparisonProblems = (rows, olds, news) => {
81
+ if (!Array.isArray(rows)) return ['the pairing needs a "rows" list'];
82
+ const shapes = rows.map(rowShape).filter((problem) => problem !== null);
83
+ if (shapes.length > 0) return shapes;
84
+ return [
85
+ ...coverage(rows, olds, "old", "the old version"),
86
+ ...coverage(rows, news, "new", "the new version"),
87
+ ...rows.map((row) => truth(row, olds, news)).filter((problem) => problem !== null),
88
+ ...missedPairs(rows, olds, news),
89
+ ];
90
+ };
91
+
92
+ // Where a name stops and another article's number would go on. A name ending in a letter or digit ("Article 1")
93
+ // stops at a word boundary, so "Article 12", "Article 1bis" and "Article 1-2" are other words. A name ending in any
94
+ // other character (第1条) goes on only into a branch number: a digit, or の/之/ノ with a numeral (第1条の2,
95
+ // 第二十一条の二) — 「第1条の委託料」 still names 第1条, and so does 「第1条第2項」.
96
+ // Letters of the Latin script (full-width ones too) and decimal digits of any width: kana and kanji end the word.
97
+ const WORD_END = /[\p{Script=Latin}\p{Nd}]$/u;
98
+ // A dash of any kind, a point or a slash between two word characters joins them into one number (1-2, 1–2, 1.2, 1/2).
99
+ const WORD_GOES_ON = /^(?:[\p{Script=Latin}\p{Nd}]|[\p{Pd}..//][\p{Script=Latin}\p{Nd}])/u;
100
+ const BRANCH_GOES_ON = /^(?:[0-90-9]|[の之ノ][0-90-9一二三四五六七八九十百千])/u;
101
+
102
+ /** Whether `text` names the article `name`: a match that goes on into a longer number names another article. */
103
+ export const mentions = (text, name) => {
104
+ const goesOn = WORD_END.test(name) ? WORD_GOES_ON : BRANCH_GOES_ON;
105
+ const source = String(text);
106
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
107
+ const hits = [...source.matchAll(new RegExp(escaped, "gu"))].map((match) => match.index ?? 0);
108
+ return hits.some((index) => !goesOn.test(source.slice(index + name.length)));
109
+ };
@@ -0,0 +1,12 @@
1
+ // The docs base holds the helpers every usecase on it shares (running chaff, reading Markdown sections).
2
+ // A usecase loads them from BLUEPRINT_BASE, which the executor sets for every check.
3
+ import { join } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ if (!process.env.BLUEPRINT_BASE) {
7
+ console.error("BLUEPRINT_BASE is not set: run this check through the blueprint executor");
8
+ process.exit(1);
9
+ }
10
+
11
+ /** The URL of a module in the docs base's checks/, for `await import(...)`. */
12
+ export const fromBase = (file) => pathToFileURL(join(process.env.BLUEPRINT_BASE, "checks", file)).href;
@@ -0,0 +1,22 @@
1
+ // The pair step's check: .blueprint/comparison.json pairs every article of the old version with its article in the
2
+ // new one, and says truly whether each changed. Writes the pairing as a person reads it at the gate, and records
3
+ // what was checked so the report can tell the documents and the pairing were not touched since.
4
+ import { writeFileSync } from "node:fs";
5
+ import { fromBase } from "./base.mjs";
6
+ import { comparisonProblems } from "./articles.mjs";
7
+ import { comparisonText } from "./comparisonView.mjs";
8
+ import { readVersions } from "./versions.mjs";
9
+ const { fail, readJson } = await import(fromBase("chaff.mjs"));
10
+ const { fingerprint } = await import(fromBase("documents.mjs"));
11
+
12
+ const PAIRING = ".blueprint/comparison.json";
13
+ const READABLE = ".blueprint/comparison.txt";
14
+ const CHECKED = ".blueprint/.comparison-checked";
15
+
16
+ const versions = readVersions();
17
+ const rows = readJson(PAIRING, '{ "rows": [{ "old", "new", "change", "what" }] }')?.rows;
18
+ const problems = comparisonProblems(rows, versions.old.articles, versions.new.articles);
19
+ if (problems.length > 0) fail(`${PAIRING}:\n ${problems.join("\n ")}`);
20
+ writeFileSync(READABLE, comparisonText(rows, versions.old.articles, versions.new.articles));
21
+ writeFileSync(CHECKED, JSON.stringify({ old: versions.old.print, new: versions.new.print, pairing: fingerprint(PAIRING) }) + "\n");
22
+ console.log(`${rows.length} pair(s): every article accounted for`);
@@ -0,0 +1,4 @@
1
+ // Types for comparisonView.mjs, which the pack runs with plain node and the specs import.
2
+ import type { Article, Row } from "./articles.mjs";
3
+
4
+ export declare const comparisonText: (rows: readonly Row[], olds: readonly Article[], news: readonly Article[]) => string;
@@ -0,0 +1,14 @@
1
+ // The pairing as plain text a person reads at the gate: one line a pair, the old article first. The marks are the
2
+ // ones a diff uses, so they read the same in any language: "=" unchanged, "~" changed, "+" added, "-" removed.
3
+
4
+ const MARKS = { same: "=", changed: "~", added: "+", removed: "-" };
5
+
6
+ const labelOf = (articles, address) => (typeof address === "string" ? (articles.find((article) => article.address === address)?.label ?? address) : "");
7
+
8
+ const lineOf = (row, olds, news) => {
9
+ const pair = `${labelOf(olds, row.old)} → ${labelOf(news, row.new)}`.trim();
10
+ const what = typeof row.what === "string" && row.what.trim() !== "" ? ` ${row.what.split("\n").join(" ").trim()}` : "";
11
+ return `${MARKS[row.change] ?? "?"} ${pair}${what}`;
12
+ };
13
+
14
+ export const comparisonText = (rows, olds, news) => [...rows.map((row) => lineOf(row, olds, news)), ""].join("\n");
@@ -0,0 +1,47 @@
1
+ // The report is the comparison table the person keeps: every pair that is not unchanged is in it, the documents
2
+ // and the pairing are as the pair step checked them, and it says what was checked.
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { fromBase } from "./base.mjs";
5
+ import { mentions } from "./articles.mjs";
6
+ import { readVersions } from "./versions.mjs";
7
+ const { fail, readJson } = await import(fromBase("chaff.mjs"));
8
+ const { missingSections, sectionText } = await import(fromBase("markdown.mjs"));
9
+ const { fingerprint } = await import(fromBase("documents.mjs"));
10
+
11
+ const PAIRING = ".blueprint/comparison.json";
12
+ const CHECKED = ".blueprint/.comparison-checked";
13
+ const REPORT = ".blueprint/compare-report.md";
14
+ const TABLE = ["新旧対照表", "Comparison table"];
15
+ const SECTIONS = [TABLE, ["確かめたこと", "What was checked"]];
16
+
17
+ const versions = readVersions();
18
+ // What the pair step recorded; anything else (missing, not JSON) means that step has to run again.
19
+ const recorded = () => {
20
+ try {
21
+ return JSON.parse(readFileSync(CHECKED, "utf8"));
22
+ } catch {
23
+ return null;
24
+ }
25
+ };
26
+ const checked = existsSync(CHECKED) ? recorded() : null;
27
+ if (checked === null || typeof checked !== "object") fail("the pair step's record is missing: run that step again");
28
+ if (checked?.old !== versions.old.print || checked?.new !== versions.new.print) fail("a version changed since the pair step checked it: run that step again");
29
+ if (checked.pairing !== fingerprint(PAIRING)) fail(`${PAIRING} changed since the pair step checked it: run that step again`);
30
+ const rows = readJson(PAIRING, "the pairing").rows;
31
+
32
+ if (!existsSync(REPORT)) fail(`${REPORT} is missing`);
33
+ const text = readFileSync(REPORT, "utf8");
34
+ const missing = missingSections(text, SECTIONS);
35
+ if (missing.length > 0) fail(`${REPORT} lacks sections: ${missing.join(", ")}`);
36
+
37
+ const table = sectionText(text, TABLE);
38
+
39
+ const labelOf = (articles, address) => articles.find((article) => article.address === address)?.label ?? address;
40
+ const absent = rows
41
+ .filter((row) => row.change !== "same")
42
+ .flatMap((row) => {
43
+ const names = [...(row.old ? [labelOf(versions.old.articles, row.old)] : []), ...(row.new ? [labelOf(versions.new.articles, row.new)] : [])];
44
+ return names.every((name) => mentions(table, name)) ? [] : [`${row.change} ${names.join(" → ")}`];
45
+ });
46
+ if (absent.length > 0) fail(`${REPORT}: the table does not show: ${absent.join(", ")}`);
47
+ console.log("the report shows every article that changed, was added or was removed");
@@ -0,0 +1,33 @@
1
+ // The two versions the person named, read the same way by every check: one file each, inside this folder, with
2
+ // its articles as chaff's tree gives them.
3
+ import { readFileSync } from "node:fs";
4
+ import { fromBase } from "./base.mjs";
5
+ import { articlesIn } from "./articles.mjs";
6
+ const { fail, readJson } = await import(fromBase("chaff.mjs"));
7
+ const { documentsNamed, fingerprint } = await import(fromBase("documents.mjs"));
8
+ const { treeOf } = await import(fromBase("places.mjs"));
9
+
10
+ const oneFile = (listed, which) => {
11
+ const files = documentsNamed(listed);
12
+ if (files.length !== 1) fail(`the ${which} version must be one file; the answer names ${files.length}`);
13
+ return files[0];
14
+ };
15
+
16
+ const articlesOf = (file, which) => {
17
+ const tree = treeOf(file);
18
+ if (tree === null) fail(`chaff could not read the ${which} version, ${file}`);
19
+ const articles = articlesIn(tree, readFileSync(file, "utf8"));
20
+ if (articles.length === 0) fail(`the ${which} version, ${file}, has no article chaff reads (第1条, Article 1): this compares documents numbered by article`);
21
+ return articles;
22
+ };
23
+
24
+ /** Both versions: their files, articles and fingerprints. */
25
+ export const readVersions = () => {
26
+ const answers = readJson(".blueprint/answers.json", "the interview answers");
27
+ const [oldFile, newFile] = [oneFile(answers.old, "old"), oneFile(answers.new, "new")];
28
+ if (oldFile === newFile) fail("the old and the new version are the same file");
29
+ return {
30
+ old: { file: oldFile, articles: articlesOf(oldFile, "old"), print: fingerprint(oldFile) },
31
+ new: { file: newFile, articles: articlesOf(newFile, "new"), print: fingerprint(newFile) },
32
+ };
33
+ };
@@ -0,0 +1,27 @@
1
+ {
2
+ "questions": [
3
+ {
4
+ "id": "old",
5
+ "label": "旧版の文書(このフォルダの中のファイル 1 つ)",
6
+ "why": "比べる元。条ごとに新版と対応づける",
7
+ "kind": "text",
8
+ "pick": "files",
9
+ "lines": true
10
+ },
11
+ {
12
+ "id": "new",
13
+ "label": "新版の文書(このフォルダの中のファイル 1 つ)",
14
+ "why": "比べる先",
15
+ "kind": "text",
16
+ "pick": "files",
17
+ "lines": true
18
+ },
19
+ {
20
+ "id": "focus",
21
+ "label": "特に見てほしいこと(任意)",
22
+ "why": "その点に関わる変更を先に、詳しく書く",
23
+ "kind": "text",
24
+ "required": false
25
+ }
26
+ ]
27
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "slug": "compare",
3
+ "kind": "usecase",
4
+ "title": "版を比べる(新旧対照表をつくる)",
5
+ "version": "0.1.0",
6
+ "description": "契約書や規程の旧版と新版を条ごとに対応づけ、何が変わり、何が足され、何が消えたかを新旧対照表にする。変わったかどうかは条の本文を機械で比べて決めるので、変わっていない条を「変わった」と書く誤り、その逆は通らない。原本は変えない。",
7
+ "bases": ["docs"],
8
+ "report": ".blueprint/compare-report.md"
9
+ }
@@ -0,0 +1,31 @@
1
+ 業務委託契約書
2
+
3
+ 株式会社みなと(以下「甲」という。)と株式会社そよかぜ(以下「乙」という。)は、次のとおり業務委託契約を結ぶ。
4
+
5
+ 第1条(目的)
6
+ 甲は、第2条に定める業務を乙に委託し、乙はこれを受託する。
7
+
8
+ 第2条(委託業務)
9
+ 乙は、次の業務を行う。
10
+ 一 甲の会員向けウェブサイトの保守
11
+ 二 月に一度の利用状況の報告
12
+ 三 前二号に付随する業務
13
+
14
+ 第3条(定義)
15
+ この契約において「成果物」とは、乙が委託業務として作成し甲に納める報告書をいう。
16
+
17
+ 第4条(委託料と支払)
18
+ 甲は、乙に対し、委託料として月額200,000円を支払う。
19
+ 2 甲は、乙が第12条に定める報告書を納めた月の翌月末日までに、前項の委託料を支払う。
20
+
21
+ 第6条(検収)
22
+ 甲は、成果物を受け取った日から10日以内に検収を行う。甲は、検収に合格した成果物について、検収の日から60日以内に委託料を支払う。
23
+
24
+ 第7条(再委託)
25
+ 乙は、甲の書面による承諾なく、委託業務を第三者に委託してはならない。
26
+
27
+ 第8条(成果物の扱い)
28
+ この契約において「成果物」とは、乙が作成したプログラム、文書その他一切のものをいう。成果物の著作権は、委託料の支払いが済んだときに乙から甲に移る。
29
+
30
+ 第9条(契約期間)
31
+ この契約の期間は、2026年10月1日から2027年9月30日までとする。
@@ -0,0 +1,31 @@
1
+ 業務委託契約書
2
+
3
+ 株式会社みなと(以下「甲」という。)と株式会社そよかぜ(以下「乙」という。)は、次のとおり業務委託契約を結ぶ。
4
+
5
+ 第1条(目的)
6
+ 甲は、第2条に定める業務を乙に委託し、乙はこれを受託する。
7
+
8
+ 第2条(委託業務)
9
+ 乙は、次の業務を行う。
10
+ 一 甲の会員向けウェブサイトの保守
11
+ 二 月に二度の利用状況の報告
12
+ 三 前二号に付随する業務
13
+
14
+ 第3条(定義)
15
+ この契約において「成果物」とは、乙が委託業務として作成し甲に納める報告書をいう。
16
+
17
+ 第4条(委託料と支払)
18
+ 甲は、乙に対し、委託料として月額220,000円を支払う。
19
+ 2 甲は、乙が第12条に定める報告書を納めた月の翌月末日までに、前項の委託料を支払う。
20
+
21
+ 第5条(報告)
22
+ 乙は、毎月5日までに、前月の利用状況をまとめた報告書を甲に納める。
23
+
24
+ 第6条(検収)
25
+ 甲は、成果物を受け取った日から10日以内に検収を行う。甲は、検収に合格した成果物について、検収の日から60日以内に委託料を支払う。
26
+
27
+ 第7条(再委託)
28
+ 乙は、甲の書面による承諾なく、委託業務を第三者に委託してはならない。
29
+
30
+ 第8条(契約期間)
31
+ この契約の期間は、2026年10月1日から2027年9月30日までとする。
@@ -0,0 +1,11 @@
1
+ {
2
+ "presets": [
3
+ {
4
+ "id": "itaku-kaitei",
5
+ "title": "業務委託契約書の改定を比べる",
6
+ "description": "見本の業務委託契約書の旧版(kyu.txt)と改定版(shin.txt)を条ごとに対応づけ、委託料や報告の回数が変わったこと、報告の条が足され、成果物の条が消えて契約期間の条が繰り上がったことを新旧対照表にする。原本は変えない。",
7
+ "base": "docs",
8
+ "answers": { "old": "kyu.txt", "new": "shin.txt", "focus": "" }
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: blueprint-compare-pair
3
+ description: "Pair every article of the old version with its article in the new one, and say whether each changed, was added or was removed — writing .blueprint/comparison.json and changing no document."
4
+ ---
5
+
6
+ # Pair the articles of the two versions
7
+
8
+ `.blueprint/answers.json` names the old version (`old`), the new version (`new`) — one file each — and what the
9
+ person wants looked at first (`focus`, may be empty). Read both in full. The person may not be an engineer: say
10
+ what you do in plain words.
11
+
12
+ Change nothing in the repository: neither version is touched. Write only under `.blueprint/`.
13
+
14
+ ## Find the articles
15
+
16
+ Run `sh <base pack>/checks/chaff.sh tree <file> --format json` on each version. Every node whose `kind` is
17
+ `article` is one article; its `address` (`"4"`) is how you name it below, and `attrs.label` (`第4条`,
18
+ `Article 4`) is how the person reads it. The check uses the same tree, so an article chaff does not read as one
19
+ is not yours to pair.
20
+
21
+ ## Pair them
22
+
23
+ For each article of the old version, find the article of the new version that carries the same provision — by
24
+ what it says, not by its number: a provision that moved or was renumbered (old 第9条 is new 第8条) is one pair.
25
+ Then write `.blueprint/comparison.json`:
26
+
27
+ ```json
28
+ { "rows": [
29
+ { "old": "1", "new": "1", "change": "same" },
30
+ { "old": "4", "new": "4", "change": "changed", "what": "委託料が月額200,000円から220,000円になった" },
31
+ { "old": null, "new": "5", "change": "added", "what": "毎月5日までの報告の条が足された" },
32
+ { "old": "8", "new": null, "change": "removed", "what": "成果物の扱いの条が消えた" },
33
+ { "old": "9", "new": "8", "change": "same" }
34
+ ] }
35
+ ```
36
+
37
+ - `same` / `changed` — both `old` and `new`. `changed` needs `what`: one line, in the person's language, saying
38
+ what is different, with the words or numbers from both versions. A change in a paragraph or an item is a change
39
+ to the article that holds it.
40
+ - `added` — only `new`; `removed` — only `old`. A `what` saying what the article is about helps the person.
41
+ - Every article of each version appears in exactly one row.
42
+
43
+ Whether a pair is `same` or `changed` is not a judgement: the check compares the two articles' text (spacing
44
+ aside, and leaving out the article's own number) and refuses a `same` whose text differs and a `changed` whose
45
+ text does not. When you pair two articles that the check calls different but you think the change is trivial
46
+ (punctuation, a character's width), it is still `changed`: say so in `what`.
47
+
48
+ ## Done when
49
+
50
+ `node <usecase pack>/checks/comparison.mjs` (with `BLUEPRINT_BASE` and `BLUEPRINT_USECASE` set to the pack folders
51
+ from your prompt) passes. It writes `.blueprint/comparison.txt`, the pairing a person reads before the table.
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: blueprint-compare-report
3
+ description: "Write the comparison table of the two versions from the checked pairing, and what was checked."
4
+ ---
5
+
6
+ # The comparison table
7
+
8
+ `.blueprint/comparison.json` is the pairing the person approved. Write `.blueprint/compare-report.md` for them,
9
+ in their language and in plain words:
10
+
11
+ - `## 新旧対照表` / `## Comparison table` — one row per pair that is not `same`, in the order of the new version
12
+ (a removed article where it stood in the old one): the old article, the new article, and what changed. Name
13
+ each article as the document does (`第4条`, `Article 4`), and quote the words or numbers that differ from each
14
+ version. A Markdown table works well:
15
+
16
+ | 旧 | 新 | 変わったこと |
17
+ |---|---|---|
18
+ | 第4条(委託料と支払) | 第4条(委託料と支払) | 月額「200,000円」→「220,000円」 |
19
+ | (なし) | 第5条(報告) | 足された: 毎月5日までに前月の報告書を納める |
20
+
21
+ The check looks for every changed, added and removed article's name in this section.
22
+ - A line after the table on the articles that are the same, including those that only moved or were renumbered
23
+ (「旧第9条は新第8条になった。中身は同じ」).
24
+ - `## 確かめたこと` / `## What was checked` — that every article of both versions is in one pair; that whether each
25
+ pair changed was decided by comparing the text by machine, not by reading; that neither document was changed.
26
+ Say what was not checked: a change's effect on other articles that refer to it (a renumbered article that other
27
+ articles still cite by its old number), and anything outside the articles (the title, the preamble).
28
+
29
+ When `focus` names something, put the pairs that touch it first under the table's heading, and say so.
30
+
31
+ ## Done when
32
+
33
+ `node <usecase pack>/checks/report.mjs` (with `BLUEPRINT_BASE` and `BLUEPRINT_USECASE` set) passes.
@@ -0,0 +1,20 @@
1
+ {
2
+ "steps": [
3
+ {
4
+ "id": "pair",
5
+ "title": "旧版と新版の条を対応づける",
6
+ "description": "旧版のどの条が新版のどの条になったかを一組ずつ書き、変わった・足された・消えたを付ける。変わったかどうかは条の本文を機械で比べて確かめる。",
7
+ "skill": "skills/pair",
8
+ "check": "node \"$BLUEPRINT_USECASE/checks/comparison.mjs\""
9
+ },
10
+ {
11
+ "id": "report",
12
+ "title": "新旧対照表をつくる",
13
+ "description": "対応づけを確かめてから、変わった条・足された条・消えた条を新旧対照表にする。",
14
+ "skill": "skills/report",
15
+ "check": "node \"$BLUEPRINT_USECASE/checks/report.mjs\"",
16
+ "gates": ["review"],
17
+ "reads": [".blueprint/comparison.txt"]
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,9 @@
1
+ // Types for markdown.mjs, which the packs run with plain node and the specs import.
2
+ export type Section = { readonly heading: string; readonly hasBody: boolean };
3
+ export type Skeleton = { readonly headings: string[]; readonly code: string[]; readonly links: string[] };
4
+
5
+ export declare const sectionsOf: (markdown: string) => Section[];
6
+ export declare const missingSections: (markdown: string, wanted: readonly (readonly string[])[]) => string[];
7
+ export declare const skeletonOf: (markdown: string) => Skeleton;
8
+ export declare const skeletonChanges: (before: string, after: string) => string[];
9
+ export declare const sectionText: (markdown: string, names: readonly string[]) => string;
@@ -82,3 +82,19 @@ export const skeletonChanges = (before, after) => {
82
82
  ...(differs(was.links, now.links) ? ["link targets"] : []),
83
83
  ];
84
84
  };
85
+
86
+ const headingLevel = (line) => /^(#{1,6}) /u.exec(line)?.[1].length ?? 0;
87
+
88
+ /**
89
+ * The text under the first `##` or `###` heading starting with one of `names` (the depths a report's sections may
90
+ * have), up to the next heading at the same depth or above. Empty when there is none.
91
+ */
92
+ export const sectionText = (markdown, names) => {
93
+ const lines = String(markdown).split("\n");
94
+ const start = lines.findIndex((line) => [2, 3].includes(headingLevel(line)) && names.some((name) => line.replace(/^#+ /u, "").trim().startsWith(name)));
95
+ if (start < 0) return "";
96
+ const depth = headingLevel(lines[start] ?? "");
97
+ const rest = lines.slice(start + 1);
98
+ const end = rest.findIndex((line) => headingLevel(line) > 0 && headingLevel(line) <= depth);
99
+ return rest.slice(0, end < 0 ? rest.length : end).join("\n");
100
+ };
@@ -10,6 +10,9 @@ The spec numbers the must-haves. For EACH one:
10
10
  1. Write a test in `test/acceptance.test.ts` named after it (`must-have 1: …`) that walks the real flow through the
11
11
  API on a temporary database — create what it needs, do the action, and read back the result the user would see.
12
12
  2. If a must-have does not work yet, make it work — this step is where gaps between the spec and the app close.
13
+ When that means building one of the source's actions and `.blueprint/actions.json` decides it `feature`, build it as
14
+ its entry says; the actions step then finds it done. If the entry decides `manual` or `drop`, the spec contradicts
15
+ itself: ask through the blueprint question tool which one holds, and stop.
13
16
  3. If a must-have cannot be tested through the API (it is purely visual), test the screen instead and say so in a
14
17
  comment on the test.
15
18
 
@@ -12,6 +12,9 @@ The spec numbers the must-haves. For EACH one:
12
12
  would see.
13
13
  2. If the must-have is limited to some people (signed in, an owner), the same action by someone else is refused.
14
14
  3. If a must-have does not work yet, make it work — this step is where gaps between the spec and the app close.
15
+ When that means building one of the source's actions and `.blueprint/actions.json` decides it `feature`, build it as
16
+ its entry says; the actions step then finds it done. If the entry decides `manual` or `drop`, the spec contradicts
17
+ itself: ask through the blueprint question tool which one holds, and stop.
15
18
  4. If a must-have cannot be tested through the API (it is purely visual), test the screen instead and say so in a
16
19
  comment on the test.
17
20
 
@@ -13,6 +13,9 @@ The spec numbers the must-haves. For EACH one, in `test/blueprint/acceptance.spe
13
13
  2. Then the refusal: the same action by someone who must not be able to do it (another user, a signed-out
14
14
  visitor) is refused by the rules.
15
15
  3. If a must-have does not work yet, make it work — this step is where gaps between the spec and the app close.
16
+ When that means building one of the source's actions and `.blueprint/actions.json` decides it `feature`, build it as
17
+ its entry says; the actions step then finds it done. If the entry decides `manual` or `drop`, the spec contradicts
18
+ itself: ask through the blueprint question tool which one holds, and stop.
16
19
 
17
20
  Do not add features that are not must-haves.
18
21
 
@@ -14,6 +14,9 @@ The spec numbers the must-haves. For EACH one, in `test/acceptance.test.ts`, nam
14
14
  2. Then the refusal: the same action by someone who must not be able to do it (another signed-in user, a signed-out
15
15
  visitor) is refused by the database — a `42501`, or a read that comes back empty.
16
16
  3. If a must-have does not work yet, make it work — this step is where gaps between the spec and the app close.
17
+ When that means building one of the source's actions and `.blueprint/actions.json` decides it `feature`, build it as
18
+ its entry says; the actions step then finds it done. If the entry decides `manual` or `drop`, the spec contradicts
19
+ itself: ask through the blueprint question tool which one holds, and stop.
17
20
  4. If a must-have is purely visual, test the screen instead and say so in a comment on the test.
18
21
 
19
22
  Do not add features that are not must-haves.