dreamcontext 0.10.2 → 0.10.6
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.
- package/README.md +39 -5
- package/agents/curator-auditor.md +5 -0
- package/agents/dreamcontext-explore.md +5 -1
- package/agents/initializer-ingestor.md +1 -0
- package/agents/initializer-scout.md +5 -0
- package/agents/sleep-product.md +2 -0
- package/agents/sleep-tasks.md +7 -2
- package/dist/agents/curator-auditor.md +5 -0
- package/dist/agents/dreamcontext-explore.md +5 -1
- package/dist/agents/initializer-ingestor.md +1 -0
- package/dist/agents/initializer-scout.md +5 -0
- package/dist/agents/sleep-product.md +2 -0
- package/dist/agents/sleep-tasks.md +7 -2
- package/dist/dashboard/assets/{BrainCanvas3D-BuhjqMJE.js → BrainCanvas3D-8hG96aAi.js} +1 -1
- package/dist/dashboard/assets/{_baseUniq-BFywhIrF.js → _baseUniq-b4AibH2-.js} +1 -1
- package/dist/dashboard/assets/{ar-SA-G6X2FPQ2-0wtAtjf3.js → ar-SA-G6X2FPQ2-DpGPIe5Z.js} +1 -1
- package/dist/dashboard/assets/{arc-CbIZg0XW.js → arc-BHNPUsz3.js} +1 -1
- package/dist/dashboard/assets/{architectureDiagram-Q4EWVU46-DcA2m4dL.js → architectureDiagram-Q4EWVU46-IzEd_B2U.js} +1 -1
- package/dist/dashboard/assets/{az-AZ-76LH7QW2-DypXzn_9.js → az-AZ-76LH7QW2-Ccf6e1ON.js} +1 -1
- package/dist/dashboard/assets/{bg-BG-XCXSNQG7-DqjNOvWi.js → bg-BG-XCXSNQG7-XnDUjubv.js} +1 -1
- package/dist/dashboard/assets/{blockDiagram-DXYQGD6D-DGUYSkmu.js → blockDiagram-DXYQGD6D-CEINLAFi.js} +1 -1
- package/dist/dashboard/assets/{bn-BD-2XOGV67Q-DMT15vLO.js → bn-BD-2XOGV67Q-E9FzPBnu.js} +1 -1
- package/dist/dashboard/assets/{c4Diagram-AHTNJAMY-D0ureHL0.js → c4Diagram-AHTNJAMY-VrHpmy8U.js} +1 -1
- package/dist/dashboard/assets/{ca-ES-6MX7JW3Y-BIf8pakY.js → ca-ES-6MX7JW3Y-Cnp7YBQb.js} +1 -1
- package/dist/dashboard/assets/channel-DwaG7WPN.js +1 -0
- package/dist/dashboard/assets/{chunk-4BX2VUAB-BpMo9p3D.js → chunk-4BX2VUAB-CuueCQHq.js} +1 -1
- package/dist/dashboard/assets/{chunk-4TB4RGXK-BjWqEumI.js → chunk-4TB4RGXK-BhlZWQoi.js} +1 -1
- package/dist/dashboard/assets/{chunk-55IACEB6-CoREiws2.js → chunk-55IACEB6-ClixcVTq.js} +1 -1
- package/dist/dashboard/assets/{chunk-EDXVE4YY-DWGx8NFS.js → chunk-EDXVE4YY-DUj1uWsJ.js} +1 -1
- package/dist/dashboard/assets/{chunk-FMBD7UC4-BPHXOHEi.js → chunk-FMBD7UC4-BZClSbG6.js} +1 -1
- package/dist/dashboard/assets/{chunk-OYMX7WX6-Cg-AvRQR.js → chunk-OYMX7WX6-CMMXG2K9.js} +1 -1
- package/dist/dashboard/assets/{chunk-QZHKN3VN-oNBf0ZAT.js → chunk-QZHKN3VN-CI5x9wkp.js} +1 -1
- package/dist/dashboard/assets/{chunk-YZCP3GAM-BWTEULwI.js → chunk-YZCP3GAM-DnpOMFO-.js} +1 -1
- package/dist/dashboard/assets/classDiagram-6PBFFD2Q-COGCH4LQ.js +1 -0
- package/dist/dashboard/assets/classDiagram-v2-HSJHXN6E-COGCH4LQ.js +1 -0
- package/dist/dashboard/assets/clone-DwBiydth.js +1 -0
- package/dist/dashboard/assets/{cose-bilkent-S5V4N54A-CQAtltI5.js → cose-bilkent-S5V4N54A-C9xnvFgj.js} +1 -1
- package/dist/dashboard/assets/{cs-CZ-2BRQDIVT-sgLhn2uw.js → cs-CZ-2BRQDIVT-BHebgNbB.js} +1 -1
- package/dist/dashboard/assets/{da-DK-5WZEPLOC-56JnY0Ou.js → da-DK-5WZEPLOC-DYuNSWM8.js} +1 -1
- package/dist/dashboard/assets/{dagre-KV5264BT-Czzd8AFD.js → dagre-KV5264BT-Ck8mmM53.js} +1 -1
- package/dist/dashboard/assets/{de-DE-XR44H4JA-CLiIEbrn.js → de-DE-XR44H4JA-CMw5lMuT.js} +1 -1
- package/dist/dashboard/assets/{diagram-5BDNPKRD-BYQz4Ej0.js → diagram-5BDNPKRD-L4erno22.js} +1 -1
- package/dist/dashboard/assets/{diagram-G4DWMVQ6-Bi69CX9y.js → diagram-G4DWMVQ6-Dkmwa2-B.js} +1 -1
- package/dist/dashboard/assets/{diagram-MMDJMWI5-C9rH4USL.js → diagram-MMDJMWI5-v2PnfbZO.js} +1 -1
- package/dist/dashboard/assets/{diagram-TYMM5635-CtGy_47Q.js → diagram-TYMM5635-1ZO4tNjc.js} +1 -1
- package/dist/dashboard/assets/{el-GR-BZB4AONW-DLKTMVgu.js → el-GR-BZB4AONW-PXD8ArOs.js} +1 -1
- package/dist/dashboard/assets/{erDiagram-SMLLAGMA-DYwZiwiF.js → erDiagram-SMLLAGMA-B8gV0kGN.js} +1 -1
- package/dist/dashboard/assets/{es-ES-U4NZUMDT-B8vUZNzj.js → es-ES-U4NZUMDT-CbfskKe2.js} +1 -1
- package/dist/dashboard/assets/{eu-ES-A7QVB2H4-DOBpB_xM.js → eu-ES-A7QVB2H4-iqrfeyOI.js} +1 -1
- package/dist/dashboard/assets/{fa-IR-HGAKTJCU-BSo8AkJw.js → fa-IR-HGAKTJCU-PzU2OiC1.js} +1 -1
- package/dist/dashboard/assets/{fi-FI-Z5N7JZ37-IXouAsQf.js → fi-FI-Z5N7JZ37-CbFLdQ9f.js} +1 -1
- package/dist/dashboard/assets/{flowDiagram-DWJPFMVM-BfsdXM27.js → flowDiagram-DWJPFMVM-CKx3j3bI.js} +1 -1
- package/dist/dashboard/assets/{fr-FR-RHASNOE6-Bq41-iWY.js → fr-FR-RHASNOE6-BuF2MviY.js} +1 -1
- package/dist/dashboard/assets/{ganttDiagram-T4ZO3ILL-YFxSZdBo.js → ganttDiagram-T4ZO3ILL-CAbTd_KO.js} +1 -1
- package/dist/dashboard/assets/{gitGraphDiagram-UUTBAWPF-CzLILfBF.js → gitGraphDiagram-UUTBAWPF-Di7htCsA.js} +1 -1
- package/dist/dashboard/assets/{gl-ES-HMX3MZ6V-kuzaTFsC.js → gl-ES-HMX3MZ6V-BqePaODm.js} +1 -1
- package/dist/dashboard/assets/{graph-BOf2JBnY.js → graph-CC7SYwBm.js} +1 -1
- package/dist/dashboard/assets/{he-IL-6SHJWFNN-RQ2NxAvm.js → he-IL-6SHJWFNN-w9d4AvyL.js} +1 -1
- package/dist/dashboard/assets/{hi-IN-IWLTKZ5I-D9xTaaPK.js → hi-IN-IWLTKZ5I-BwHo17vR.js} +1 -1
- package/dist/dashboard/assets/{hu-HU-A5ZG7DT2-L1HNbSdl.js → hu-HU-A5ZG7DT2-Dm80xRge.js} +1 -1
- package/dist/dashboard/assets/{id-ID-SAP4L64H-B33X72Ml.js → id-ID-SAP4L64H-RpPM18i0.js} +1 -1
- package/dist/dashboard/assets/index-CkxvR2LH.css +32 -0
- package/dist/dashboard/assets/index-DalMBSek.js +511 -0
- package/dist/dashboard/assets/{index-B-6R-7Ot.js → index-sA_NxF_Y.js} +1 -1
- package/dist/dashboard/assets/{infoDiagram-42DDH7IO-DTWl9Wxm.js → infoDiagram-42DDH7IO-z-1I4Qyj.js} +1 -1
- package/dist/dashboard/assets/{ishikawaDiagram-UXIWVN3A-C0CAoNgB.js → ishikawaDiagram-UXIWVN3A-DWGoVsQO.js} +1 -1
- package/dist/dashboard/assets/{it-IT-JPQ66NNP-BK_HSf6M.js → it-IT-JPQ66NNP-DhOvhKWm.js} +1 -1
- package/dist/dashboard/assets/{ja-JP-DBVTYXUO-BRVQwLJ-.js → ja-JP-DBVTYXUO-CEZ1K0xn.js} +1 -1
- package/dist/dashboard/assets/{journeyDiagram-VCZTEJTY-CADHnY3u.js → journeyDiagram-VCZTEJTY-Dqh8uQwc.js} +1 -1
- package/dist/dashboard/assets/{kaa-6HZHGXH3-CYGgpUSe.js → kaa-6HZHGXH3-DuFoQ8nX.js} +1 -1
- package/dist/dashboard/assets/{kab-KAB-ZGHBKWFO-LMEkvoCj.js → kab-KAB-ZGHBKWFO-BkQCP_3o.js} +1 -1
- package/dist/dashboard/assets/{kanban-definition-6JOO6SKY-D1ijOIDj.js → kanban-definition-6JOO6SKY-DCs65HUc.js} +1 -1
- package/dist/dashboard/assets/{kk-KZ-P5N5QNE5-JIbh9_iv.js → kk-KZ-P5N5QNE5-Dh9BXA8_.js} +1 -1
- package/dist/dashboard/assets/{km-KH-HSX4SM5Z-CrWx5kgt.js → km-KH-HSX4SM5Z-D1S2EcQ_.js} +1 -1
- package/dist/dashboard/assets/{ko-KR-MTYHY66A-BQFjXlBC.js → ko-KR-MTYHY66A-DsI3vrEx.js} +1 -1
- package/dist/dashboard/assets/{ku-TR-6OUDTVRD-B6u43c-f.js → ku-TR-6OUDTVRD-CyjK_3XH.js} +1 -1
- package/dist/dashboard/assets/{layout-CVIUlzFz.js → layout-B_y9BOh6.js} +1 -1
- package/dist/dashboard/assets/{linear-TmytBUBH.js → linear-BdGSdQOc.js} +1 -1
- package/dist/dashboard/assets/{lt-LT-XHIRWOB4-b8AKF48V.js → lt-LT-XHIRWOB4-C6tJka1D.js} +1 -1
- package/dist/dashboard/assets/{lv-LV-5QDEKY6T-CRt75m7a.js → lv-LV-5QDEKY6T-C_xyL_ee.js} +1 -1
- package/dist/dashboard/assets/{min-CMOJsVTV.js → min-U_KGhNEt.js} +1 -1
- package/dist/dashboard/assets/{mindmap-definition-QFDTVHPH-DkzlAkBW.js → mindmap-definition-QFDTVHPH-sXmpEpKw.js} +1 -1
- package/dist/dashboard/assets/{mr-IN-CRQNXWMA-Bw3MaxMe.js → mr-IN-CRQNXWMA-C0TwGtAU.js} +1 -1
- package/dist/dashboard/assets/{my-MM-5M5IBNSE-Dw_tWZWl.js → my-MM-5M5IBNSE-RlAQCiXW.js} +1 -1
- package/dist/dashboard/assets/{nb-NO-T6EIAALU-Ci_2058Y.js → nb-NO-T6EIAALU-C1JHqr1t.js} +1 -1
- package/dist/dashboard/assets/{nl-NL-IS3SIHDZ-DYyqePg6.js → nl-NL-IS3SIHDZ-B0BVD_2p.js} +1 -1
- package/dist/dashboard/assets/{nn-NO-6E72VCQL-NdjykAxN.js → nn-NO-6E72VCQL-zQTko94P.js} +1 -1
- package/dist/dashboard/assets/{oc-FR-POXYY2M6-DPEyc6hx.js → oc-FR-POXYY2M6-CBZRg6ku.js} +1 -1
- package/dist/dashboard/assets/{pa-IN-N4M65BXN-D_4V8i72.js → pa-IN-N4M65BXN-7s6qpn6h.js} +1 -1
- package/dist/dashboard/assets/{percentages-BXMCSKIN-4Hgy5SgQ.js → percentages-BXMCSKIN-DckRvLSG.js} +7 -7
- package/dist/dashboard/assets/{pica-D6rAZEqu.js → pica-ByOjSUxE.js} +1 -1
- package/dist/dashboard/assets/{pieDiagram-DEJITSTG-BqsalDEv.js → pieDiagram-DEJITSTG-BJJsFOH9.js} +1 -1
- package/dist/dashboard/assets/{pl-PL-T2D74RX3-CArl0OSe.js → pl-PL-T2D74RX3-QTzNyi10.js} +1 -1
- package/dist/dashboard/assets/{pt-BR-5N22H2LF-BlA9u7zY.js → pt-BR-5N22H2LF-tntUlote.js} +1 -1
- package/dist/dashboard/assets/{pt-PT-UZXXM6DQ-ByCcYKU2.js → pt-PT-UZXXM6DQ-B9M5aHl1.js} +1 -1
- package/dist/dashboard/assets/{quadrantDiagram-34T5L4WZ-BtnExwNB.js → quadrantDiagram-34T5L4WZ-BearsU1w.js} +1 -1
- package/dist/dashboard/assets/{requirementDiagram-MS252O5E-jDWYOtpC.js → requirementDiagram-MS252O5E-BixosHER.js} +1 -1
- package/dist/dashboard/assets/{ro-RO-JPDTUUEW-CafHxA38.js → ro-RO-JPDTUUEW-qbgXrb57.js} +1 -1
- package/dist/dashboard/assets/{ru-RU-B4JR7IUQ-BbJGwp-Z.js → ru-RU-B4JR7IUQ-CgX4A1Ib.js} +1 -1
- package/dist/dashboard/assets/{sankeyDiagram-XADWPNL6-BSU4w3_8.js → sankeyDiagram-XADWPNL6-DMSd55sU.js} +1 -1
- package/dist/dashboard/assets/{sequenceDiagram-FGHM5R23-DBHLScxa.js → sequenceDiagram-FGHM5R23-D2-Uwrtb.js} +1 -1
- package/dist/dashboard/assets/{si-LK-N5RQ5JYF-eDB0s9B0.js → si-LK-N5RQ5JYF-CWEkBvJB.js} +1 -1
- package/dist/dashboard/assets/{sk-SK-C5VTKIMK-qaYN0zkE.js → sk-SK-C5VTKIMK-CG8moyh9.js} +1 -1
- package/dist/dashboard/assets/{sl-SI-NN7IZMDC-BQ4pcyGF.js → sl-SI-NN7IZMDC-CsBFEfKt.js} +1 -1
- package/dist/dashboard/assets/{stateDiagram-FHFEXIEX-DqLkK6dC.js → stateDiagram-FHFEXIEX-vG3jGBpo.js} +1 -1
- package/dist/dashboard/assets/stateDiagram-v2-QKLJ7IA2-V8xCsZlI.js +1 -0
- package/dist/dashboard/assets/{subset-shared.chunk-Hm_CkSQB.js → subset-shared.chunk-Bin8VoC6.js} +1 -1
- package/dist/dashboard/assets/{subset-worker.chunk-YczznLkp.js → subset-worker.chunk-mRsEjVpS.js} +1 -1
- package/dist/dashboard/assets/{sv-SE-XGPEYMSR-DD7_8pqE.js → sv-SE-XGPEYMSR-BMD7GbQV.js} +1 -1
- package/dist/dashboard/assets/{ta-IN-2NMHFXQM-Dd-RFvWz.js → ta-IN-2NMHFXQM-CjZ7GPuA.js} +1 -1
- package/dist/dashboard/assets/{th-TH-HPSO5L25-C7x2tZrZ.js → th-TH-HPSO5L25-BTE57yWh.js} +1 -1
- package/dist/dashboard/assets/{timeline-definition-GMOUNBTQ-DILCBtIX.js → timeline-definition-GMOUNBTQ-Bz7MuXCz.js} +1 -1
- package/dist/dashboard/assets/{tr-TR-DEFEU3FU-BXDs1FJQ.js → tr-TR-DEFEU3FU-CKY-uY5_.js} +1 -1
- package/dist/dashboard/assets/{uk-UA-QMV73CPH-Bp-yEHOn.js → uk-UA-QMV73CPH-BuopbANh.js} +1 -1
- package/dist/dashboard/assets/{vennDiagram-DHZGUBPP-CnQH3XJu.js → vennDiagram-DHZGUBPP-TsesBmH7.js} +1 -1
- package/dist/dashboard/assets/{vi-VN-M7AON7JQ-DQOsitU7.js → vi-VN-M7AON7JQ-CLrvMDM6.js} +1 -1
- package/dist/dashboard/assets/{wardley-RL74JXVD-FdX4RiE_.js → wardley-RL74JXVD-DlttwW9K.js} +1 -1
- package/dist/dashboard/assets/{wardleyDiagram-NUSXRM2D-CK4rbC5W.js → wardleyDiagram-NUSXRM2D-BfZXChhy.js} +1 -1
- package/dist/dashboard/assets/{xychartDiagram-5P7HB3ND-B6UGr9Rl.js → xychartDiagram-5P7HB3ND-jjeDn8EJ.js} +1 -1
- package/dist/dashboard/assets/{zh-CN-LNUGB5OW-BMYbdc2b.js → zh-CN-LNUGB5OW-C1MYPzyx.js} +1 -1
- package/dist/dashboard/assets/{zh-HK-E62DVLB3-CCcVgqN7.js → zh-HK-E62DVLB3-BriaHjNV.js} +1 -1
- package/dist/dashboard/assets/{zh-TW-RAJ6MFWO-C52SJlwU.js → zh-TW-RAJ6MFWO-BUDimtjA.js} +1 -1
- package/dist/dashboard/index.html +3 -3
- package/dist/index.js +2586 -1065
- package/dist/skill-packs/excalidraw/SKILL.md +19 -1
- package/dist/skill-packs/excalidraw/scripts/build_excalidraw.js +47 -8
- package/dist/skill-packs/excalidraw/scripts/lib/style.js +111 -21
- package/dist/skill-packs/goal-skill/SKILL.md +6 -0
- package/package.json +1 -1
- package/skill/SKILL.md +8 -3
- package/skill/references/cli-reference.md +20 -2
- package/skill/references/knowledge-and-recall.md +1 -0
- package/skill/references/sleep.md +6 -5
- package/skill/references/tasks-and-features.md +37 -0
- package/skill-packs/excalidraw/SKILL.md +19 -1
- package/skill-packs/excalidraw/scripts/build_excalidraw.js +47 -8
- package/skill-packs/excalidraw/scripts/lib/style.js +111 -21
- package/skill-packs/goal-skill/SKILL.md +6 -0
- package/dist/dashboard/assets/channel-C5uBjxRg.js +0 -1
- package/dist/dashboard/assets/classDiagram-6PBFFD2Q-MwUwKREr.js +0 -1
- package/dist/dashboard/assets/classDiagram-v2-HSJHXN6E-MwUwKREr.js +0 -1
- package/dist/dashboard/assets/clone-CcQlDA5C.js +0 -1
- package/dist/dashboard/assets/index--chDvBkZ.css +0 -32
- package/dist/dashboard/assets/index-CwZ3YweD.js +0 -582
- package/dist/dashboard/assets/stateDiagram-v2-QKLJ7IA2-TpuacwBg.js +0 -1
|
@@ -161,9 +161,17 @@ Match the team's look by default. The builder now defaults text to **Excalifont
|
|
|
161
161
|
Each is `{ fill, stroke }`. `INK` = `#1e1e1e` (the default 2px card outline). Fills are **solid**,
|
|
162
162
|
rects are **always rounded**.
|
|
163
163
|
- **`card({x,y,w,h,text,color,fontSize})`** → rounded filled rect + centered label, grouped (move as one).
|
|
164
|
+
The label **cannot overflow**: it is hard-wrapped to the card width and the font auto-shrinks (down to
|
|
165
|
+
`minFont`, default 9px) until it also fits the height. Optional `{ minFont, padX, padY }`. If even the
|
|
166
|
+
floor font won't fit, it still draws but logs a `[excalidraw]` warning — that's your cue to enlarge the
|
|
167
|
+
card or shorten the text.
|
|
164
168
|
- **`node(...)`** → smaller card for flow steps.
|
|
165
169
|
- **`sectionTitle({x,y,text,fontSize})`** → big plain header (no box), like "Configuration" / "Flow".
|
|
166
|
-
- **`connector({from,to,label,double,dashed})`** → labeled arrow between two points.
|
|
170
|
+
- **`connector({from,to,label,double,dashed,via,elbow})`** → labeled arrow between two points. Route it
|
|
171
|
+
AROUND intervening boxes instead of through them: `elbow:'hv'` (horizontal then vertical) / `elbow:'vh'`
|
|
172
|
+
(vertical then horizontal) bends it once at a right angle, or `via:[[x,y],…]` threads explicit waypoints.
|
|
173
|
+
- **`fitText({text,w,h,fontSize,minFont})`** → the wrap+shrink primitive `card()` uses; call it directly to
|
|
174
|
+
pre-size a box to its label (returns `{ text, fontSize, lineCount, fits }`).
|
|
167
175
|
- **`annotate({from,to,text})`** → short arrow to a side-spec note (the "90sec / deepseek…" pattern).
|
|
168
176
|
- **`column({x,y,items,color})`** → vertical stack of cards (benefit/risk lists). `items` may be
|
|
169
177
|
strings or `{text,color}`.
|
|
@@ -175,6 +183,16 @@ Compose these into a `buildExcalidraw({ out, elements })` call. Runnable example
|
|
|
175
183
|
Conventions: color nodes by ROLE, label every arrow, put specs in side-annotations, use big plain
|
|
176
184
|
section titles to break the board into "slides", keep one idea per card.
|
|
177
185
|
|
|
186
|
+
### Layout rules that keep boards clean
|
|
187
|
+
- **Size boxes for their text.** A card guarantees the label stays *inside*, but it does so by shrinking
|
|
188
|
+
the font — a long label in a small box ends up tiny. Give wide/long labels a wider `w` (or more `h`),
|
|
189
|
+
or split the idea across two cards. Watch for `[excalidraw]` fit warnings on the console.
|
|
190
|
+
- **Fan out through a margin bus, never as a diagonal star.** A hub wired straight to a vertical stack of
|
|
191
|
+
boxes draws diagonals that cut across the boxes in between. Instead, route the trunk into an empty
|
|
192
|
+
margin beside the stack, run one vertical "bus" line down that margin, then a short horizontal stub from
|
|
193
|
+
the bus into each box. With `connector()`: send the trunk `elbow:'hv'` into the bus x, then one
|
|
194
|
+
`elbow:'hv'` stub per branch (or `via:[[busX, branchY]]`). No arrow crosses a box it isn't pointing at.
|
|
195
|
+
|
|
178
196
|
## Rules & gotchas
|
|
179
197
|
- **Unique image filenames.** Obsidian resolves `[[name.png]]` by basename; if two different files
|
|
180
198
|
share a name anywhere in the vault the link is ambiguous. Name screenshots uniquely
|
|
@@ -55,30 +55,44 @@ function findVaultRoot(startDir) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
// ---------- text sizing ----------
|
|
58
|
+
// Excalifont glyph advance (multiples of fontSize): latin ~0.58, emoji/arrows/CJK/combining ~1.05.
|
|
59
|
+
// A flat factor under-counted wide glyphs, so emoji labels measured narrow and overflowed their box.
|
|
60
|
+
// This mirrors glyphW() in scripts/lib/style.js so card() pre-wrapping and on-board height agree.
|
|
61
|
+
function isWideGlyph(code) {
|
|
62
|
+
return (
|
|
63
|
+
(code >= 0x1100 && code <= 0x115f) || (code >= 0x2190 && code <= 0x21ff) ||
|
|
64
|
+
(code >= 0x2300 && code <= 0x23ff) || (code >= 0x25a0 && code <= 0x27bf) ||
|
|
65
|
+
(code >= 0x2b00 && code <= 0x2bff) || (code >= 0x2e80 && code <= 0x9fff) ||
|
|
66
|
+
(code >= 0xac00 && code <= 0xd7a3) || (code >= 0xf900 && code <= 0xfaff) ||
|
|
67
|
+
(code >= 0xfe30 && code <= 0xfe4f) || (code >= 0xff00 && code <= 0xff60) ||
|
|
68
|
+
(code >= 0x1f000 && code <= 0x1faff) || (code >= 0x1f1e6 && code <= 0x1f1ff)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
function glyphW(ch, fontSize) { return fontSize * (isWideGlyph(ch.codePointAt(0)) ? 1.05 : 0.58); }
|
|
72
|
+
function measureText(s, fontSize) { let w = 0; for (const ch of String(s)) w += glyphW(ch, fontSize); return w; }
|
|
73
|
+
|
|
58
74
|
// When a fixedWidth is given the text element is treated as WRAPPING (autoResize off downstream),
|
|
59
75
|
// so height must reflect word-wrapped line count — otherwise long captions render as one wide line
|
|
60
76
|
// and overlap their neighbours. Honors explicit \n and greedily wraps each logical line to width.
|
|
61
77
|
function sizeText(text, fontSize, fixedWidth) {
|
|
62
78
|
const rawLines = String(text).split('\n');
|
|
63
79
|
if (fixedWidth != null) {
|
|
64
|
-
const charW = fontSize * 0.52; // approx Excalifont glyph advance
|
|
65
|
-
const perLine = Math.max(1, Math.floor(fixedWidth / charW));
|
|
66
80
|
let totalLines = 0;
|
|
67
81
|
for (const ln of rawLines) {
|
|
68
82
|
const words = ln.split(/\s+/).filter(Boolean);
|
|
69
83
|
if (!words.length) { totalLines += 1; continue; }
|
|
70
|
-
let cur =
|
|
84
|
+
let cur = '', lc = 1;
|
|
71
85
|
for (const w of words) {
|
|
72
|
-
|
|
73
|
-
if (cur +
|
|
74
|
-
else cur +=
|
|
86
|
+
if (!cur) { cur = w; continue; }
|
|
87
|
+
if (measureText(cur + ' ' + w, fontSize) > fixedWidth) { lc++; cur = w; }
|
|
88
|
+
else cur += ' ' + w;
|
|
75
89
|
}
|
|
76
90
|
totalLines += lc;
|
|
77
91
|
}
|
|
78
92
|
return { width: fixedWidth, height: Math.max(Math.round(fontSize * 1.25), Math.round(totalLines * fontSize * 1.25)) };
|
|
79
93
|
}
|
|
80
|
-
const
|
|
81
|
-
return { width: Math.max(10, Math.round(
|
|
94
|
+
const maxW = rawLines.reduce((m, l) => Math.max(m, measureText(l, fontSize)), 0);
|
|
95
|
+
return { width: Math.max(10, Math.round(maxW)), height: Math.round(rawLines.length * fontSize * 1.25) };
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
// ---------- element factory ----------
|
|
@@ -119,6 +133,31 @@ function buildExcalidraw(spec) {
|
|
|
119
133
|
const wikilinkMode = spec.wikilinkMode || 'basename';
|
|
120
134
|
const background = spec.background || '#ffffff';
|
|
121
135
|
|
|
136
|
+
// Pre-flight: every referenced image MUST exist on disk before we write the
|
|
137
|
+
// board. A missing asset would otherwise be emitted as a dangling `## Embedded
|
|
138
|
+
// Files` wikilink (`<sha1>: [[missing.png]]`) that can never resolve in any
|
|
139
|
+
// renderer — the board ships with blank images. Collect ALL missing paths and
|
|
140
|
+
// fail once with the full list (and the board path), so the author fixes every
|
|
141
|
+
// gap in a single pass instead of discovering them one throw at a time.
|
|
142
|
+
const missingImages = [];
|
|
143
|
+
for (const e of (spec.elements || [])) {
|
|
144
|
+
if (e && e.type === 'image' && e.path) {
|
|
145
|
+
// A missing file OR a non-file (directory / broken symlink) is unusable —
|
|
146
|
+
// statSync throws for a broken link / absent path, so default to missing.
|
|
147
|
+
let isFile = false;
|
|
148
|
+
try { isFile = fs.statSync(path.resolve(e.path)).isFile(); } catch { isFile = false; }
|
|
149
|
+
if (!isFile) missingImages.push(e.path);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (missingImages.length) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`build_excalidraw: ${missingImages.length} referenced image(s) missing or not a file — ` +
|
|
155
|
+
`refusing to write a board with dangling embeds:\n ` +
|
|
156
|
+
missingImages.join('\n ') +
|
|
157
|
+
`\n(board: ${outPath})`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
122
161
|
const rng = mulberry32(strHash(path.resolve(outPath)));
|
|
123
162
|
const elements = [];
|
|
124
163
|
const textEntries = []; // {text, id}
|
|
@@ -23,30 +23,112 @@ function pal(c) { return PALETTE[c] || { fill: c, stroke: INK }; }
|
|
|
23
23
|
let _gc = 0;
|
|
24
24
|
const newGroup = (hint) => `g${++_gc}-${hint || ''}`;
|
|
25
25
|
const lines = (t) => String(t).split('\n').length;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
|
|
27
|
+
// --- text fitting --------------------------------------------------------
|
|
28
|
+
// Excalifont glyph advance, in multiples of fontSize. Latin is ~0.58; emoji, arrows, CJK and
|
|
29
|
+
// combining marks render roughly square (~1.05) — the old flat 0.52 under-counted them, so wide
|
|
30
|
+
// labels measured narrow and spilled out of their card. Mirrored in build_excalidraw.js sizeText().
|
|
31
|
+
function isWideGlyph(code) {
|
|
32
|
+
return (
|
|
33
|
+
(code >= 0x1100 && code <= 0x115f) || // Hangul Jamo
|
|
34
|
+
(code >= 0x2190 && code <= 0x21ff) || // arrows
|
|
35
|
+
(code >= 0x2300 && code <= 0x23ff) || // misc technical (⏰ ⏳ …)
|
|
36
|
+
(code >= 0x25a0 && code <= 0x27bf) || // geometric shapes, misc symbols, dingbats (✅ ★ …)
|
|
37
|
+
(code >= 0x2b00 && code <= 0x2bff) || // extra arrows / symbols
|
|
38
|
+
(code >= 0x2e80 && code <= 0x9fff) || // CJK radicals … unified ideographs
|
|
39
|
+
(code >= 0xac00 && code <= 0xd7a3) || // Hangul syllables
|
|
40
|
+
(code >= 0xf900 && code <= 0xfaff) || // CJK compatibility ideographs
|
|
41
|
+
(code >= 0xfe30 && code <= 0xfe4f) || // CJK compatibility forms
|
|
42
|
+
(code >= 0xff00 && code <= 0xff60) || // fullwidth forms
|
|
43
|
+
(code >= 0x1f000 && code <= 0x1faff) || // emoji & pictographs
|
|
44
|
+
(code >= 0x1f1e6 && code <= 0x1f1ff) // regional indicators (flags)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
function glyphW(ch, fontSize) { return fontSize * (isWideGlyph(ch.codePointAt(0)) ? 1.05 : 0.58); }
|
|
48
|
+
function measureText(s, fontSize) { let w = 0; for (const ch of String(s)) w += glyphW(ch, fontSize); return w; }
|
|
49
|
+
|
|
50
|
+
// Greedy word-wrap to a pixel width, honoring existing \n and HARD-breaking any single word that is
|
|
51
|
+
// itself wider than the box (e.g. a long URL). Returns the text WITH the newlines baked in — the
|
|
52
|
+
// Obsidian plugin keeps them instead of re-flowing the label into one over-wide centered line.
|
|
53
|
+
function wrapToWidth(text, fontSize, width) {
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const logical of String(text).split('\n')) {
|
|
56
|
+
const words = logical.split(/\s+/).filter(Boolean);
|
|
57
|
+
if (!words.length) { out.push(''); continue; }
|
|
58
|
+
let cur = '';
|
|
59
|
+
for (let word of words) {
|
|
60
|
+
while (measureText(word, fontSize) > width) {
|
|
61
|
+
let acc = '';
|
|
62
|
+
for (const ch of word) {
|
|
63
|
+
if (acc && measureText(acc + ch, fontSize) > width) break;
|
|
64
|
+
acc += ch;
|
|
65
|
+
}
|
|
66
|
+
if (cur) { out.push(cur); cur = ''; }
|
|
67
|
+
out.push(acc);
|
|
68
|
+
word = word.slice(acc.length);
|
|
69
|
+
if (!word) break;
|
|
70
|
+
}
|
|
71
|
+
if (!word) continue;
|
|
72
|
+
const candidate = cur ? cur + ' ' + word : word;
|
|
73
|
+
if (cur && measureText(candidate, fontSize) > width) { out.push(cur); cur = word; }
|
|
74
|
+
else cur = candidate;
|
|
75
|
+
}
|
|
76
|
+
if (cur) out.push(cur);
|
|
77
|
+
}
|
|
78
|
+
return out.join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const _warned = new Set();
|
|
82
|
+
function warnFit(msg) { if (!_warned.has(msg)) { _warned.add(msg); try { console.warn('[excalidraw] ' + msg); } catch (e) {} } }
|
|
83
|
+
|
|
84
|
+
// Wrap `text` to the card's inner width, then shrink the font (down to minFont) until the wrapped
|
|
85
|
+
// label also fits the card height. Returns { text (with baked-in newlines), fontSize, lineCount, fits }.
|
|
86
|
+
function fitText(o) {
|
|
87
|
+
const { text = '', w = 260, h = 92, fontSize = 20, minFont = 9, lineHeight = 1.25, padX = 10, padY = 8 } = o;
|
|
88
|
+
const innerW = Math.max(1, w - 2 * padX);
|
|
89
|
+
const innerH = Math.max(1, h - 2 * padY);
|
|
90
|
+
let fs = Math.max(minFont, Math.round(fontSize));
|
|
91
|
+
let wrapped = wrapToWidth(text, fs, innerW);
|
|
92
|
+
let lineCount = wrapped.split('\n').length;
|
|
93
|
+
while (fs > minFont && lineCount * fs * lineHeight > innerH) {
|
|
94
|
+
fs -= 1;
|
|
95
|
+
wrapped = wrapToWidth(text, fs, innerW);
|
|
96
|
+
lineCount = wrapped.split('\n').length;
|
|
97
|
+
}
|
|
98
|
+
return { text: wrapped, fontSize: fs, lineCount, fits: lineCount * fs * lineHeight <= innerH };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// arc-length midpoint of a polyline — keeps a connector label on the line even when it elbows
|
|
102
|
+
function polyMid(pts) {
|
|
103
|
+
if (!pts || pts.length < 2) return (pts && pts[0]) || [0, 0];
|
|
104
|
+
const seg = [];
|
|
29
105
|
let total = 0;
|
|
30
|
-
for (const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
106
|
+
for (let i = 1; i < pts.length; i++) { const l = Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]); seg.push(l); total += l; }
|
|
107
|
+
let half = total / 2;
|
|
108
|
+
for (let i = 1; i < pts.length; i++) {
|
|
109
|
+
if (half <= seg[i - 1] || i === pts.length - 1) {
|
|
110
|
+
const t = seg[i - 1] ? half / seg[i - 1] : 0;
|
|
111
|
+
return [pts[i - 1][0] + (pts[i][0] - pts[i - 1][0]) * t, pts[i - 1][1] + (pts[i][1] - pts[i - 1][1]) * t];
|
|
112
|
+
}
|
|
113
|
+
half -= seg[i - 1];
|
|
36
114
|
}
|
|
37
|
-
return
|
|
115
|
+
return pts[0];
|
|
38
116
|
}
|
|
39
117
|
|
|
40
|
-
// rounded card: filled rect + centered label, grouped so they move together
|
|
118
|
+
// rounded card: filled rect + centered label, grouped so they move together. The label is
|
|
119
|
+
// hard-wrapped to the card width and the font auto-shrinks (to `minFont`) until it also fits the
|
|
120
|
+
// height, so text can never spill outside the box. Warns if even the floor font won't fit.
|
|
41
121
|
function card(o) {
|
|
42
|
-
const { x, y, w = 260, h = 92, text = '', color = 'blue', fontSize = 20, stroke = INK, strokeWidth = 2, textColor = INK, roundness = true } = o;
|
|
122
|
+
const { x, y, w = 260, h = 92, text = '', color = 'blue', fontSize = 20, stroke = INK, strokeWidth = 2, textColor = INK, roundness = true, minFont = 9, padX = 10, padY = 8 } = o;
|
|
43
123
|
const group = o.group || newGroup('card');
|
|
44
124
|
const p = pal(color);
|
|
45
|
-
const
|
|
46
|
-
|
|
125
|
+
const fit = fitText({ text, w, h, fontSize, minFont, padX, padY });
|
|
126
|
+
if (!fit.fits) warnFit(`label ${JSON.stringify(String(text).slice(0, 32))} won't fit a ${w}×${h} card even at ${minFont}px — enlarge the card or shorten the text.`);
|
|
127
|
+
const textH = fit.lineCount * fit.fontSize * 1.25;
|
|
128
|
+
const ty = y + Math.max(padY, (h - textH) / 2);
|
|
47
129
|
return [
|
|
48
130
|
{ type: 'rectangle', x, y, width: w, height: h, strokeColor: stroke, backgroundColor: p.fill, fillStyle: 'solid', strokeWidth, roundness, group },
|
|
49
|
-
{ type: 'text', x, y: ty, text, fontSize, color: textColor, width: w, align: 'center', fontFamily: 5, group },
|
|
131
|
+
{ type: 'text', x, y: ty, text: fit.text, fontSize: fit.fontSize, color: textColor, width: w, align: 'center', fontFamily: 5, group },
|
|
50
132
|
];
|
|
51
133
|
}
|
|
52
134
|
// small node (flow step) — card with tighter defaults
|
|
@@ -58,13 +140,21 @@ function sectionTitle(o) {
|
|
|
58
140
|
return [{ type: 'text', x, y, text, fontSize, color, fontFamily: 5, width: Math.max(200, String(text).length * fontSize * 0.6) }];
|
|
59
141
|
}
|
|
60
142
|
|
|
61
|
-
// labeled connector arrow between two points (centers of cards, etc.)
|
|
143
|
+
// labeled connector arrow between two points (centers of cards, etc.).
|
|
144
|
+
// Routing: pass `via:[[x,y],…]` for explicit waypoints, or `elbow:'hv'|'vh'` for an auto right-angle
|
|
145
|
+
// bend ('hv' = horizontal then vertical, 'vh' = vertical then horizontal). Use these to route a link
|
|
146
|
+
// AROUND intervening boxes instead of cutting a diagonal straight through them.
|
|
62
147
|
function connector(o) {
|
|
63
|
-
const { from, to, label, double = false, strokeColor = INK, strokeWidth = 2, dashed = false, labelColor = '#495057', fontSize = 16 } = o;
|
|
64
|
-
|
|
148
|
+
const { from, to, label, double = false, strokeColor = INK, strokeWidth = 2, dashed = false, labelColor = '#495057', fontSize = 16, via = null, elbow = null } = o;
|
|
149
|
+
let pts;
|
|
150
|
+
if (via && via.length) pts = [from, ...via, to];
|
|
151
|
+
else if (elbow === 'hv') pts = [from, [to[0], from[1]], to];
|
|
152
|
+
else if (elbow === 'vh') pts = [from, [from[0], to[1]], to];
|
|
153
|
+
else pts = [from, to];
|
|
154
|
+
const out = [{ type: 'arrow', points: pts, strokeColor, strokeWidth, strokeStyle: dashed ? 'dashed' : 'solid', endArrow: 'arrow', startArrow: double ? 'arrow' : null }];
|
|
65
155
|
if (label) {
|
|
66
|
-
const mx
|
|
67
|
-
const w = Math.max(40, String(label)
|
|
156
|
+
const [mx, my] = polyMid(pts);
|
|
157
|
+
const w = Math.max(40, measureText(String(label), fontSize) + fontSize);
|
|
68
158
|
out.push({ type: 'text', x: mx - w / 2, y: my - fontSize - 4, text: label, fontSize, color: labelColor, width: w, align: 'center', fontFamily: 5 });
|
|
69
159
|
}
|
|
70
160
|
return out;
|
|
@@ -127,4 +217,4 @@ const leftOf = (x, y, w, h) => [x, y + h / 2];
|
|
|
127
217
|
const bottomOf = (x, y, w, h) => [x + w / 2, y + h];
|
|
128
218
|
const topOf = (x, y, w, h) => [x + w / 2, y];
|
|
129
219
|
|
|
130
|
-
module.exports = { INK, PALETTE, pal, card, node, sectionTitle, connector, annotate, column, hub, bullets, center, rightOf, leftOf, bottomOf, topOf };
|
|
220
|
+
module.exports = { INK, PALETTE, pal, card, node, sectionTitle, connector, annotate, column, hub, bullets, fitText, measureText, center, rightOf, leftOf, bottomOf, topOf };
|
|
@@ -122,6 +122,12 @@ dreamcontext tasks status <slug> in_progress "plan validated; implementing"
|
|
|
122
122
|
|
|
123
123
|
If `<slug>` already exists, de-collide (append a short suffix) rather than clobbering.
|
|
124
124
|
|
|
125
|
+
If the project has **roadmap objectives** (`_dream_context/core/objectives/` non-empty — they appear
|
|
126
|
+
in your snapshot), link the task to the objective(s) this goal serves: `tasks create --objectives a,b`
|
|
127
|
+
(or `dreamcontext tasks objectives <slug> a,b` after). One goal often serves several objectives —
|
|
128
|
+
that's expected. If unsure which, ask the user in Phase 0; never leave an obviously-serving task
|
|
129
|
+
unlinked, and never overwrite an existing non-empty `objectives:` list.
|
|
130
|
+
|
|
125
131
|
If the project declares **custom task fields** (`_dream_context/overrides/task.md`), `tasks create`
|
|
126
132
|
hard-fails (exit 1) on an unset `required` field — set each with `--field key=value` on create. For
|
|
127
133
|
any field marked `ask: true`, ask the user for the value back in Phase 0 (it's a human judgment) rather
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -76,6 +76,7 @@ dreamcontext is **more than memory files**. Every capability below is real and s
|
|
|
76
76
|
|---|---|---|
|
|
77
77
|
| **Structured memory** | soul/user/memory + knowledge + tasks, auto-loaded each session | this file |
|
|
78
78
|
| **Tasks** | Working documents with changelog, RICE, status lifecycle, start/due date ranges, resolved assignees, and project-declared custom fields (`overrides/task.md`) | [tasks-and-features.md](references/tasks-and-features.md) |
|
|
79
|
+
| **Roadmap / Objectives** | PO-authored OKR board: objectives in `core/objectives/`, many-to-many task links (`objectives:` frontmatter), dependency DAG with full forecast cascade, target vs forecast slip detection, `dreamcontext roadmap` (+ `--json`) | [tasks-and-features.md](references/tasks-and-features.md) |
|
|
79
80
|
| **Features (PRDs)** | Retrospective product docs, updated only during sleep | [tasks-and-features.md](references/tasks-and-features.md) |
|
|
80
81
|
| **Knowledge** | Tagged deep docs, pinning, staleness, Excalidraw diagrams | [knowledge-and-recall.md](references/knowledge-and-recall.md) |
|
|
81
82
|
| **Memory recall** | Haiku/BM25 search over the whole corpus; auto-injected on prompts | [knowledge-and-recall.md](references/knowledge-and-recall.md) |
|
|
@@ -105,7 +106,8 @@ The SessionStart hook injects this automatically every session — answer from i
|
|
|
105
106
|
|
|
106
107
|
- **Soul, User, Memory** — full content (`core/0.soul.md`, `1.user.md`, `2.memory.md`)
|
|
107
108
|
- **Extended core files index** — names/types of style guide, tech stack, system flow
|
|
108
|
-
- **Active tasks** — status, priority, last updated (answer "which tasks are active?" from this)
|
|
109
|
+
- **Active tasks** — status, priority, last updated, and the objectives each serves (answer "which tasks are active?" from this)
|
|
110
|
+
- **Objectives (roadmap)** — active + recently-finished objectives with progress %, target vs forecast, and slip flags. **Weigh decisions against these outcomes** — they are WHAT the project is driving toward
|
|
109
111
|
- **Bookmarks** — tagged important moments from prior sessions, by salience
|
|
110
112
|
- **Contextual reminders** — triggers matching active tasks (prospective memory)
|
|
111
113
|
- **Sleep state** — current debt level, sessions since last sleep, history
|
|
@@ -256,8 +258,9 @@ A ★★★ bookmark or 5+ sessions since last sleep also triggers an advisory.
|
|
|
256
258
|
3. Build a brief inline (cheap CLI): read `state/.sleep.json`, `git status --short`, `git log` since last sleep, `dreamcontext core releases active`.
|
|
257
259
|
4. Dispatch specialists **in parallel** (one message, multiple Agent calls): always `sleep-tasks` + `sleep-state`; fire `sleep-product` when knowledge/features/research signals warrant (over-fire — it no-ops cheaply); fire `sleep-migration` only if `dreamcontext migrations pending` has output.
|
|
258
260
|
5. Wait for reports, then `dreamcontext reflect` (promote only genuinely load-bearing terms).
|
|
259
|
-
6. `
|
|
260
|
-
7.
|
|
261
|
+
6. If `_dream_context/core/objectives/` is non-empty, run `dreamcontext roadmap` — a cheap deterministic call that refreshes the auto-generated board (`knowledge/roadmap/board.md`) from the reconciled tasks. Surface any 🔴 SLIPPING objectives in your summary.
|
|
262
|
+
7. `dreamcontext sleep done "<one-paragraph summary>"` — clears pre-epoch state, resets debt.
|
|
263
|
+
8. Report the consolidated summary to the user.
|
|
261
264
|
|
|
262
265
|
For non-file-change work (decisions, architecture talk): `dreamcontext sleep add <score> "<reason>"`.
|
|
263
266
|
|
|
@@ -282,6 +285,8 @@ Status: `todo → in_progress → in_review → completed`. Sections: `why`, `us
|
|
|
282
285
|
|
|
283
286
|
**Custom fields (if this project declares them).** When `_dream_context/overrides/task.md` exists, every task carries project-defined custom fields. Their **values are surfaced to you inline** — in the snapshot's Active Tasks block and in `dreamcontext tasks list --long` — so you can see them without opening the file; unset **required** fields show as `⚠ UNSET (required)`. When you create or reconcile a task, **set every declared field** (`dreamcontext tasks field <slug> <key> <value>` or `tasks create --field key=value`). **REQUIRED fields are mandatory — never create or complete a task with a required field left empty.** Fields marked **[ASK THE USER]** (`ask: true`) capture a human judgment (e.g. a time estimate) — **ask the user for the value when creating the task instead of guessing it.** The full schema + sync behavior → [tasks-and-features.md](references/tasks-and-features.md).
|
|
284
287
|
|
|
288
|
+
**Objectives (roadmap links).** When the project has objectives (`core/objectives/` non-empty — they're in your snapshot), every task should declare which it serves: `objectives: [slug-a, slug-b]` in frontmatter (many-to-many — one task often lifts several outcomes). Set at creation (`tasks create --objectives a,b`) or later (`dreamcontext tasks objectives <task> a,b`). **Propose objective links for tasks you create; never overwrite a non-empty `objectives:` list — an existing value is a PO decision that sticks.** The field is local-only (never synced to a cloud backend). Rollups, forecasts, and slip detection are computed — `dreamcontext roadmap` / `--json`. Full model → [tasks-and-features.md](references/tasks-and-features.md).
|
|
289
|
+
|
|
285
290
|
**RICE, due dates, tags/people, the Workflow flowchart, versioning, and multi-product** → [tasks-and-features.md](references/tasks-and-features.md).
|
|
286
291
|
**Syncing tasks to a cloud backend (ClickUp _or_ GitHub — one at a time)** → [integrations.md](references/integrations.md).
|
|
287
292
|
|
|
@@ -34,12 +34,13 @@ Every command and flag, grouped. All commands are prefixed with `dreamcontext`.
|
|
|
34
34
|
|
|
35
35
|
| Command | Description |
|
|
36
36
|
|---|---|
|
|
37
|
-
| `tasks list` | List/filter/group tasks (excludes completed by default). Flags: `-s/--status`, `-a/--all`, `--tag <t>` (repeatable, AND), `--any-tag <t>` (repeatable, OR), `--version <id>`, `--priority <level>`, `--feature <slug>`, `-g/--group-by tag\|version\|priority\|status`, `--long`, `--tags`, `--json`. Filters compose (AND), case-insensitive. |
|
|
37
|
+
| `tasks list` | List/filter/group tasks (excludes completed by default). Flags: `-s/--status`, `-a/--all`, `--tag <t>` (repeatable, AND), `--any-tag <t>` (repeatable, OR), `--version <id>`, `--priority <level>`, `--feature <slug>`, `--objective <slug>`, `-g/--group-by tag\|version\|priority\|status`, `--long`, `--tags`, `--json`. Filters compose (AND), case-insensitive. |
|
|
38
38
|
| `tasks tags` | Distinct task tags with counts. `-a/--all`, `--json`. |
|
|
39
|
-
| `tasks create <name>` | Create a task. Flags: `-d/--description`, `-p/--priority critical\|high\|medium\|low`, `-u/--urgency …`, `-s/--status`, `-t/--tags <csv>`, `-w/--why`, `-v/--version`, `--person <name>`, `--reach <1-10>`, `--impact <1-5>`, `--confidence 25\|50\|75\|100`, `--effort <weeks>`, `--start YYYY-MM-DD`, `--due YYYY-MM-DD`, `--field <key=value>` (repeatable; sets declared custom fields), `--allow-missing-required` (create a draft even when a required custom field is unset). **Fails** if a required custom field is unset and `--allow-missing-required` is not given. |
|
|
39
|
+
| `tasks create <name>` | Create a task. Flags: `-d/--description`, `-p/--priority critical\|high\|medium\|low`, `-u/--urgency …`, `-s/--status`, `-t/--tags <csv>`, `-w/--why`, `-v/--version`, `--person <name>`, `--reach <1-10>`, `--impact <1-5>`, `--confidence 25\|50\|75\|100`, `--effort <weeks>`, `--start YYYY-MM-DD`, `--due YYYY-MM-DD`, `--objectives <csv>` (roadmap objective slugs this task serves; slugs must exist), `--field <key=value>` (repeatable; sets declared custom fields), `--allow-missing-required` (create a draft even when a required custom field is unset). **Fails** if a required custom field is unset and `--allow-missing-required` is not given. |
|
|
40
40
|
| `tasks rice <name>` | Print or update RICE values. `--reach`/`--impact`/`--confidence`/`--effort`, `--clear`. |
|
|
41
41
|
| `tasks start <name> <YYYY-MM-DD\|clear>` | Set or clear a planned start date (range start). Must be ≤ the due date; setting it removes the `backlog` tag. |
|
|
42
42
|
| `tasks due <name> <YYYY-MM-DD\|clear>` | Set or clear a due/end date (range end). |
|
|
43
|
+
| `tasks objectives <name> [slugs\|clear]` | Print, set (comma-separated, validated against `core/objectives/`), or clear the roadmap objectives a task serves. LOCAL-ONLY — never synced to a cloud backend. |
|
|
43
44
|
| `tasks tag <name> <tags...>` | Add (or `--remove`) tags. `person:<slug>` assigns a person. |
|
|
44
45
|
| `tasks field <name> <key> [value\|clear]` | Set or clear a user-defined custom field declared in `overrides/task.md` (synced to ClickUp/GitHub). Validates select options + number types. |
|
|
45
46
|
| `tasks insert <name> <section> <content...>` | Insert into a section: `why`, `user_stories`, `acceptance_criteria`, `constraints`, `technical_details`, `notes`, `changelog`. |
|
|
@@ -58,6 +59,23 @@ Sections for `tasks insert`: `why`, `user_stories`, `acceptance_criteria`, `cons
|
|
|
58
59
|
|
|
59
60
|
---
|
|
60
61
|
|
|
62
|
+
## Roadmap (objectives — the OKR board)
|
|
63
|
+
|
|
64
|
+
| Command | Description |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `roadmap` | Render the objective board (rollups, target vs forecast, slip flags) and regenerate `knowledge/roadmap/board.md`. `--json` emits the typed RoadmapModel instead (no writes). |
|
|
67
|
+
| `roadmap objective create <slug>` | Create `core/objectives/<slug>.md`. `--title <str>` (required), `--target YYYY-MM-DD`, `--depends-on <csv>`, `--feature <prd-slug>`, `--why <text>`. |
|
|
68
|
+
| `roadmap objective list` | All objectives with progress %, status, forecast. `--json`. |
|
|
69
|
+
| `roadmap objective show <slug>` | One objective: member tasks, direct dependents, and the transitive "if this slips, so do" set. `--json`. |
|
|
70
|
+
| `roadmap objective edit <slug>` | `--title`, `--target <date\|clear>`, `--feature <slug\|clear>`, `--status not_started\|active\|review\|done\|clear` (manual PO override; `clear` returns to computed). |
|
|
71
|
+
| `roadmap objective delete <slug>` | Delete; other objectives' `depends_on` are healed automatically. `--yes`. |
|
|
72
|
+
| `roadmap objective depend <A> <B>` | A depends on B — **rejected at write time** if it would create a circular dependency. |
|
|
73
|
+
| `roadmap objective undepend <A> <B>` | Remove the dependency edge. |
|
|
74
|
+
|
|
75
|
+
Task-side linkage: `tasks create --objectives a,b` · `tasks objectives <task> a,b|clear` · `tasks list --objective <slug>`. Objectives are recallable: `memory recall "<query>" --types objective`.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
61
79
|
## Features
|
|
62
80
|
|
|
63
81
|
| Command | Description |
|
|
@@ -67,6 +67,7 @@ dreamcontext memory recall "how did we decide on the sleep fan-out"
|
|
|
67
67
|
dreamcontext memory recall "auth flow" --types knowledge,feature
|
|
68
68
|
dreamcontext memory recall "deprecated" --types changelog # ship history
|
|
69
69
|
dreamcontext memory recall "rate limit" --types task # in-flight work
|
|
70
|
+
dreamcontext memory recall "retention goal" --types objective # roadmap objectives (OKR outcomes)
|
|
70
71
|
|
|
71
72
|
# Tune result count / machine-readable output
|
|
72
73
|
dreamcontext memory recall "rice prioritization" --top 3 --json
|
|
@@ -52,10 +52,11 @@ For non-file-change work (architecture discussion, a decision with no edits): `d
|
|
|
52
52
|
- Pass each specialist a small text brief: epoch, session IDs, active task slugs, planning version, the signals relevant to it, optional user hint. Do **not** paste transcript content — specialists call `dreamcontext transcript distill <id>` themselves.
|
|
53
53
|
5. **Wait for all reports** (each returns a short structured report).
|
|
54
54
|
6. **`dreamcontext reflect`** — each candidate is a term seen across multiple sessions not yet in soul/user/memory/knowledge. Promote into `2.memory.md` or a knowledge file ONLY if genuinely load-bearing; most are noise — discard. Never auto-promote.
|
|
55
|
-
7. **
|
|
56
|
-
8. **
|
|
57
|
-
9.
|
|
58
|
-
10.
|
|
55
|
+
7. **Roadmap refresh** if `_dream_context/core/objectives/` is non-empty: run `dreamcontext roadmap` — a cheap deterministic call that recomputes rollups/forecasts from the reconciled tasks and rewrites the auto-generated `knowledge/roadmap/board.md`. Surface any 🔴 SLIPPING objectives (and objectives whose member tasks are now all complete — suggest the PO confirm them done) in your summary. Sleep never edits objective files themselves — they are PO-authored.
|
|
56
|
+
8. **Marketing pass** if `_dream_context/marketing/` exists: `dreamcontext mk rem-sleep`.
|
|
57
|
+
9. **Council promote check:** `dreamcontext council list --unpromoted` — promote if the user engaged positively.
|
|
58
|
+
10. **`dreamcontext sleep done "<one-paragraph summary stitched from specialist reports>"`** — clears pre-epoch state, resets debt, writes a history entry. (If a remote backend — ClickUp or GitHub — is active and any task pushes failed, this auto-retries once, then errors loudly with the failed slugs.)
|
|
59
|
+
11. **Report** the consolidated summary to the user.
|
|
59
60
|
|
|
60
61
|
---
|
|
61
62
|
|
|
@@ -63,7 +64,7 @@ For non-file-change work (architecture discussion, a decision with no edits): `d
|
|
|
63
64
|
|
|
64
65
|
| Specialist | Owns | Notes |
|
|
65
66
|
|---|---|---|
|
|
66
|
-
| `sleep-tasks` | Task files (`state/*.md`) | Reconciles task bodies to truth, bumps statuses, creates tasks for untracked work, attaches to the planning version, sets the start/due range, and keeps declared custom fields current (never fabricating `ask` fields in the no-user sleep context). |
|
|
67
|
+
| `sleep-tasks` | Task files (`state/*.md`) | Reconciles task bodies to truth, bumps statuses, creates tasks for untracked work, attaches to the planning version, sets the start/due range, and keeps declared custom fields current (never fabricating `ask` fields in the no-user sleep context). When objectives exist, proposes `objectives:` links for tasks with an EMPTY/absent list (multiple slugs when a task serves several outcomes) — **never overwrites a non-empty list** (that's a PO decision). |
|
|
67
68
|
| `sleep-state` | Core identity (soul, user, memory, core 3–6), CHANGELOG, RELEASES | Records patterns/decisions/preferences, writes a changelog entry per meaningful change since the epoch, surfaces release readiness, enforces anti-bloat ceilings, flags stale knowledge for `sleep-product`. |
|
|
68
69
|
| `sleep-product` | Knowledge files + feature PRDs | Creates/reconciles `knowledge/*.md` and `core/features/*.md`, processes staleness flags, maintains the knowledge index + taxonomy. |
|
|
69
70
|
| `sleep-migration` | Structure only | Moves/renames folders, normalizes frontmatter, wraps fences. Never alters body prose. |
|
|
@@ -100,6 +100,42 @@ dreamcontext tasks tag <name> person:ada --remove # unassign
|
|
|
100
100
|
|
|
101
101
|
---
|
|
102
102
|
|
|
103
|
+
## Objectives — the OKR roadmap (task ↔ objective, many-to-many)
|
|
104
|
+
|
|
105
|
+
Objectives are **PO-authored outcomes** ("increase retention 20%", "ship v0.2.3", "launch mobile") stored one file each in `_dream_context/core/objectives/<slug>.md` — first-class, durable, recallable (`memory recall --types objective`), independent of any task. The roadmap is NOT a derived shadow of tasks and NOT a list of releases: the PO owns the structure; computation is the assist layer (rollups, forecast, slip detection).
|
|
106
|
+
|
|
107
|
+
**The load-bearing relationship:** a task declares the objectives it serves via `objectives: [a, b]` frontmatter — **many-to-many** (one shipped task often lifts revenue AND retention). The reverse direction (objective → member tasks, objective → dependents) is always **computed**, never stored — so the two sides cannot drift.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
dreamcontext roadmap # text board + regenerate knowledge/roadmap/board.md
|
|
111
|
+
dreamcontext roadmap --json # the typed RoadmapModel (query surface; no writes)
|
|
112
|
+
dreamcontext roadmap objective create <slug> --title "..." [--target YYYY-MM-DD] [--depends-on a,b] [--feature <prd-slug>] [--why "..."]
|
|
113
|
+
dreamcontext roadmap objective list|show <slug> # show = members + dependents + "if this slips, so do: …"
|
|
114
|
+
dreamcontext roadmap objective edit <slug> [--title] [--target <date>|clear] [--status not_started|active|review|done|clear] [--feature <slug>|clear]
|
|
115
|
+
dreamcontext roadmap objective depend <A> <B> # A depends on B — REJECTED at write time if it would create a cycle
|
|
116
|
+
dreamcontext roadmap objective undepend <A> <B>
|
|
117
|
+
dreamcontext roadmap objective delete <slug> --yes # also heals other objectives' depends_on
|
|
118
|
+
dreamcontext tasks create <name> --objectives a,b # link at creation (slugs must exist)
|
|
119
|
+
dreamcontext tasks objectives <task> a,b|clear # set/clear on an existing task
|
|
120
|
+
dreamcontext tasks list --objective <slug> # all tasks serving an objective
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**The computed model** (per objective, from `roadmap --json`):
|
|
124
|
+
- **Progress** = completed ÷ total member tasks (each objective counts over its OWN member set — a shared task contributes to each independently).
|
|
125
|
+
- **Rollup status** (real enum): all `completed`→`done` 🟢 · any `in_progress`→`active` 🔵 · any `in_review`→`review` 🟡 · else `not_started` ⚪. A manual `--status` override wins (`status_source: override`).
|
|
126
|
+
- **Forecast cascade — full transitive DAG:** `forecast_start = max(earliest member start, max(forecast_end of dependencies))`; `forecast_end = max(latest member due, forecast_start)`. A slip anywhere propagates to ALL transitive dependents (diamond shapes included).
|
|
127
|
+
- **Null rule:** no dated member tasks → `forecast = null` ("unforecastable") and it imposes NO constraint on dependents.
|
|
128
|
+
- **Slipping** 🔴 = `forecast_end > target_date` (the PO's committed date). This is the live on-track signal — surfaced in the snapshot and the board.
|
|
129
|
+
|
|
130
|
+
**Rules for agents:**
|
|
131
|
+
1. **Propose, never overwrite.** Suggest `objectives:` for tasks you create or find unlabeled; an existing non-empty list is a PO decision — never change it unless the user asks.
|
|
132
|
+
2. **Local-only field.** `objectives` is never pushed/pulled by cloud sync backends. Do not try to map it to remote labels.
|
|
133
|
+
3. **Objectives are orthogonal to versions/cycles.** `version` = WHEN (the time-box); `objectives` = WHAT outcomes it serves. Both live on the task independently.
|
|
134
|
+
4. **`knowledge/roadmap/board.md` is auto-generated** — regenerate with `dreamcontext roadmap`, never hand-edit it. Objective files themselves are PO-authored prose — edit `## Why`/`## Notes` freely, but rollups/members are computed and don't belong in them.
|
|
135
|
+
5. A feature PRD *may* back an objective via the objective's `feature:` field — a convenience link, not a requirement.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
103
139
|
## The Workflow flowchart (keep it in sync)
|
|
104
140
|
|
|
105
141
|
Every task file has a `## Workflow` mermaid block near the top: one node per acceptance criterion, grouped under milestone subgraphs, with status classes `done` / `active` / `todo` / `blocked`. It is the load-bearing summary of the task — drift makes future sessions misread progress.
|
|
@@ -130,6 +166,7 @@ related_feature: null # feature slug for cross-link
|
|
|
130
166
|
product: null # multi-product scoping (optional)
|
|
131
167
|
start_date: null # YYYY-MM-DD or null — planned start (range start)
|
|
132
168
|
due_date: null # YYYY-MM-DD or null — due / planned end (range end)
|
|
169
|
+
objectives: [] # roadmap objective slugs this task serves (many-to-many, LOCAL-ONLY — never synced)
|
|
133
170
|
rice: { reach: 5, impact: 3, confidence: 75, effort: 2, score: 5.625 }
|
|
134
171
|
custom_fields: {} # project-declared fields (only when overrides/task.md exists)
|
|
135
172
|
---
|
|
@@ -161,9 +161,17 @@ Match the team's look by default. The builder now defaults text to **Excalifont
|
|
|
161
161
|
Each is `{ fill, stroke }`. `INK` = `#1e1e1e` (the default 2px card outline). Fills are **solid**,
|
|
162
162
|
rects are **always rounded**.
|
|
163
163
|
- **`card({x,y,w,h,text,color,fontSize})`** → rounded filled rect + centered label, grouped (move as one).
|
|
164
|
+
The label **cannot overflow**: it is hard-wrapped to the card width and the font auto-shrinks (down to
|
|
165
|
+
`minFont`, default 9px) until it also fits the height. Optional `{ minFont, padX, padY }`. If even the
|
|
166
|
+
floor font won't fit, it still draws but logs a `[excalidraw]` warning — that's your cue to enlarge the
|
|
167
|
+
card or shorten the text.
|
|
164
168
|
- **`node(...)`** → smaller card for flow steps.
|
|
165
169
|
- **`sectionTitle({x,y,text,fontSize})`** → big plain header (no box), like "Configuration" / "Flow".
|
|
166
|
-
- **`connector({from,to,label,double,dashed})`** → labeled arrow between two points.
|
|
170
|
+
- **`connector({from,to,label,double,dashed,via,elbow})`** → labeled arrow between two points. Route it
|
|
171
|
+
AROUND intervening boxes instead of through them: `elbow:'hv'` (horizontal then vertical) / `elbow:'vh'`
|
|
172
|
+
(vertical then horizontal) bends it once at a right angle, or `via:[[x,y],…]` threads explicit waypoints.
|
|
173
|
+
- **`fitText({text,w,h,fontSize,minFont})`** → the wrap+shrink primitive `card()` uses; call it directly to
|
|
174
|
+
pre-size a box to its label (returns `{ text, fontSize, lineCount, fits }`).
|
|
167
175
|
- **`annotate({from,to,text})`** → short arrow to a side-spec note (the "90sec / deepseek…" pattern).
|
|
168
176
|
- **`column({x,y,items,color})`** → vertical stack of cards (benefit/risk lists). `items` may be
|
|
169
177
|
strings or `{text,color}`.
|
|
@@ -175,6 +183,16 @@ Compose these into a `buildExcalidraw({ out, elements })` call. Runnable example
|
|
|
175
183
|
Conventions: color nodes by ROLE, label every arrow, put specs in side-annotations, use big plain
|
|
176
184
|
section titles to break the board into "slides", keep one idea per card.
|
|
177
185
|
|
|
186
|
+
### Layout rules that keep boards clean
|
|
187
|
+
- **Size boxes for their text.** A card guarantees the label stays *inside*, but it does so by shrinking
|
|
188
|
+
the font — a long label in a small box ends up tiny. Give wide/long labels a wider `w` (or more `h`),
|
|
189
|
+
or split the idea across two cards. Watch for `[excalidraw]` fit warnings on the console.
|
|
190
|
+
- **Fan out through a margin bus, never as a diagonal star.** A hub wired straight to a vertical stack of
|
|
191
|
+
boxes draws diagonals that cut across the boxes in between. Instead, route the trunk into an empty
|
|
192
|
+
margin beside the stack, run one vertical "bus" line down that margin, then a short horizontal stub from
|
|
193
|
+
the bus into each box. With `connector()`: send the trunk `elbow:'hv'` into the bus x, then one
|
|
194
|
+
`elbow:'hv'` stub per branch (or `via:[[busX, branchY]]`). No arrow crosses a box it isn't pointing at.
|
|
195
|
+
|
|
178
196
|
## Rules & gotchas
|
|
179
197
|
- **Unique image filenames.** Obsidian resolves `[[name.png]]` by basename; if two different files
|
|
180
198
|
share a name anywhere in the vault the link is ambiguous. Name screenshots uniquely
|
|
@@ -55,30 +55,44 @@ function findVaultRoot(startDir) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
// ---------- text sizing ----------
|
|
58
|
+
// Excalifont glyph advance (multiples of fontSize): latin ~0.58, emoji/arrows/CJK/combining ~1.05.
|
|
59
|
+
// A flat factor under-counted wide glyphs, so emoji labels measured narrow and overflowed their box.
|
|
60
|
+
// This mirrors glyphW() in scripts/lib/style.js so card() pre-wrapping and on-board height agree.
|
|
61
|
+
function isWideGlyph(code) {
|
|
62
|
+
return (
|
|
63
|
+
(code >= 0x1100 && code <= 0x115f) || (code >= 0x2190 && code <= 0x21ff) ||
|
|
64
|
+
(code >= 0x2300 && code <= 0x23ff) || (code >= 0x25a0 && code <= 0x27bf) ||
|
|
65
|
+
(code >= 0x2b00 && code <= 0x2bff) || (code >= 0x2e80 && code <= 0x9fff) ||
|
|
66
|
+
(code >= 0xac00 && code <= 0xd7a3) || (code >= 0xf900 && code <= 0xfaff) ||
|
|
67
|
+
(code >= 0xfe30 && code <= 0xfe4f) || (code >= 0xff00 && code <= 0xff60) ||
|
|
68
|
+
(code >= 0x1f000 && code <= 0x1faff) || (code >= 0x1f1e6 && code <= 0x1f1ff)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
function glyphW(ch, fontSize) { return fontSize * (isWideGlyph(ch.codePointAt(0)) ? 1.05 : 0.58); }
|
|
72
|
+
function measureText(s, fontSize) { let w = 0; for (const ch of String(s)) w += glyphW(ch, fontSize); return w; }
|
|
73
|
+
|
|
58
74
|
// When a fixedWidth is given the text element is treated as WRAPPING (autoResize off downstream),
|
|
59
75
|
// so height must reflect word-wrapped line count — otherwise long captions render as one wide line
|
|
60
76
|
// and overlap their neighbours. Honors explicit \n and greedily wraps each logical line to width.
|
|
61
77
|
function sizeText(text, fontSize, fixedWidth) {
|
|
62
78
|
const rawLines = String(text).split('\n');
|
|
63
79
|
if (fixedWidth != null) {
|
|
64
|
-
const charW = fontSize * 0.52; // approx Excalifont glyph advance
|
|
65
|
-
const perLine = Math.max(1, Math.floor(fixedWidth / charW));
|
|
66
80
|
let totalLines = 0;
|
|
67
81
|
for (const ln of rawLines) {
|
|
68
82
|
const words = ln.split(/\s+/).filter(Boolean);
|
|
69
83
|
if (!words.length) { totalLines += 1; continue; }
|
|
70
|
-
let cur =
|
|
84
|
+
let cur = '', lc = 1;
|
|
71
85
|
for (const w of words) {
|
|
72
|
-
|
|
73
|
-
if (cur +
|
|
74
|
-
else cur +=
|
|
86
|
+
if (!cur) { cur = w; continue; }
|
|
87
|
+
if (measureText(cur + ' ' + w, fontSize) > fixedWidth) { lc++; cur = w; }
|
|
88
|
+
else cur += ' ' + w;
|
|
75
89
|
}
|
|
76
90
|
totalLines += lc;
|
|
77
91
|
}
|
|
78
92
|
return { width: fixedWidth, height: Math.max(Math.round(fontSize * 1.25), Math.round(totalLines * fontSize * 1.25)) };
|
|
79
93
|
}
|
|
80
|
-
const
|
|
81
|
-
return { width: Math.max(10, Math.round(
|
|
94
|
+
const maxW = rawLines.reduce((m, l) => Math.max(m, measureText(l, fontSize)), 0);
|
|
95
|
+
return { width: Math.max(10, Math.round(maxW)), height: Math.round(rawLines.length * fontSize * 1.25) };
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
// ---------- element factory ----------
|
|
@@ -119,6 +133,31 @@ function buildExcalidraw(spec) {
|
|
|
119
133
|
const wikilinkMode = spec.wikilinkMode || 'basename';
|
|
120
134
|
const background = spec.background || '#ffffff';
|
|
121
135
|
|
|
136
|
+
// Pre-flight: every referenced image MUST exist on disk before we write the
|
|
137
|
+
// board. A missing asset would otherwise be emitted as a dangling `## Embedded
|
|
138
|
+
// Files` wikilink (`<sha1>: [[missing.png]]`) that can never resolve in any
|
|
139
|
+
// renderer — the board ships with blank images. Collect ALL missing paths and
|
|
140
|
+
// fail once with the full list (and the board path), so the author fixes every
|
|
141
|
+
// gap in a single pass instead of discovering them one throw at a time.
|
|
142
|
+
const missingImages = [];
|
|
143
|
+
for (const e of (spec.elements || [])) {
|
|
144
|
+
if (e && e.type === 'image' && e.path) {
|
|
145
|
+
// A missing file OR a non-file (directory / broken symlink) is unusable —
|
|
146
|
+
// statSync throws for a broken link / absent path, so default to missing.
|
|
147
|
+
let isFile = false;
|
|
148
|
+
try { isFile = fs.statSync(path.resolve(e.path)).isFile(); } catch { isFile = false; }
|
|
149
|
+
if (!isFile) missingImages.push(e.path);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (missingImages.length) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`build_excalidraw: ${missingImages.length} referenced image(s) missing or not a file — ` +
|
|
155
|
+
`refusing to write a board with dangling embeds:\n ` +
|
|
156
|
+
missingImages.join('\n ') +
|
|
157
|
+
`\n(board: ${outPath})`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
122
161
|
const rng = mulberry32(strHash(path.resolve(outPath)));
|
|
123
162
|
const elements = [];
|
|
124
163
|
const textEntries = []; // {text, id}
|