kennel 0.2.2 → 0.2.5
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 +23 -13
- package/dist/cli/dispatch.js +16 -3
- package/dist/cli/src/cli/dispatch.js +16 -3
- package/dist/cli/src/cli/usage.js +1 -1
- package/dist/cli/src/core/command.js +1 -0
- package/dist/cli/src/core/init.js +5 -5
- package/dist/cli/src/plugins/external.d.ts +29 -3
- package/dist/cli/src/plugins/external.js +78 -24
- package/dist/cli/src/site/server-plugins.d.ts +2 -7
- package/dist/cli/src/site/viewer.js +1 -0
- package/dist/cli/usage.js +1 -1
- package/dist/core/command.js +1 -0
- package/dist/core/init.js +5 -5
- package/dist/core/runtime.js +357 -3207
- package/dist/package/preview.js +1232 -5113
- package/dist/plugins/external.d.ts +29 -3
- package/dist/plugins/external.js +78 -24
- package/dist/site/server-plugins.d.ts +2 -7
- package/dist/site/server-plugins.js +552 -10447
- package/dist/site/viewer.js +1 -0
- package/kernel/apps/web/package.json +1 -5
- package/kernel/apps/web/scripts/dev.mjs +53 -0
- package/kernel/apps/web/src/app.css +0 -3
- package/kernel/apps/web/src/main.tsx +0 -1
- package/kernel/apps/web/src/plugins.ts +0 -30
- package/kernel/apps/web/tsconfig.json +1 -2
- package/kernel/apps/web/vite.config.ts +31 -13
- package/kernel/packages/api/package.json +0 -4
- package/kernel/packages/api/src/server/folder-metadata-plugin.ts +13 -38
- package/kernel/packages/api/src/server/index.ts +0 -1
- package/kernel/packages/api/src/server/plugins.ts +49 -16
- package/kernel/packages/api/test/core.test.ts +13 -25
- package/kernel/packages/markdown/README.md +1 -1
- package/package.json +1 -1
- package/web/assets/{abnfDiagram-VRR7QNED-Bgfb1nEB.js → abnfDiagram-VRR7QNED-xd_g1uK3.js} +1 -1
- package/web/assets/app-CX20nNIa.js +2 -0
- package/web/assets/{app-options-D-RJiQ-X.js → app-options-CkPJE5ep.js} +1 -1
- package/web/assets/{app-view-header-BLvLq02T.js → app-view-header-CNk5KnA-.js} +1 -1
- package/web/assets/{arc-Dk_KI36B.js → arc-B0u5yBJA.js} +1 -1
- package/web/assets/{architectureDiagram-ZJ3FMSHR-NR_KhRCf.js → architectureDiagram-ZJ3FMSHR-DfxOAo8P.js} +1 -1
- package/web/assets/{ask-tray-TzeTcO6-.js → ask-tray-SscJecDY.js} +1 -1
- package/web/assets/{blockDiagram-677ZJIJ3-CkLkwx9C.js → blockDiagram-677ZJIJ3-Ci3Usxh6.js} +1 -1
- package/web/assets/{c4Diagram-LMCZKHZV-CFCUH8e_.js → c4Diagram-LMCZKHZV-BqgwIVgh.js} +1 -1
- package/web/assets/channel-X7WHyxxT.js +1 -0
- package/web/assets/{chunk-2Q5K7J3B-B_BPLWsW.js → chunk-2Q5K7J3B-CfX69l2U.js} +1 -1
- package/web/assets/{chunk-32BRIVSS-B1VB1hOy.js → chunk-32BRIVSS-CuFlzZkl.js} +1 -1
- package/web/assets/{chunk-5VM5RSS4-KFcYr8Co.js → chunk-5VM5RSS4-DidCMlJa.js} +1 -1
- package/web/assets/{chunk-EX3LRPZG-CpBX3kuK.js → chunk-EX3LRPZG-B5CDm1dS.js} +1 -1
- package/web/assets/{chunk-JWPE2WC7-BPSl40wG.js → chunk-JWPE2WC7-D5fGV_sH.js} +1 -1
- package/web/assets/{chunk-MOJQB5TN-D2RcYmoU.js → chunk-MOJQB5TN-ayhH5Q-H.js} +1 -1
- package/web/assets/{chunk-RYQCIY6F-m7Gyn-t3.js → chunk-RYQCIY6F-C4BfOOZn.js} +1 -1
- package/web/assets/{chunk-V7JOEXUC-TDvdopxS.js → chunk-V7JOEXUC-DpUEIM9V.js} +1 -1
- package/web/assets/{chunk-VR4S4FIN-DZTsG51a.js → chunk-VR4S4FIN-DoQuFFRh.js} +1 -1
- package/web/assets/{chunk-XXDRQBXY-Dp0--RE2.js → chunk-XXDRQBXY-CI9eiLwn.js} +1 -1
- package/web/assets/classDiagram-OUVF2IWQ-BjsLQ3dz.js +1 -0
- package/web/assets/classDiagram-v2-EOCWNBFH-BjsLQ3dz.js +1 -0
- package/web/assets/client-7OWw5kW2.js +6 -0
- package/web/assets/client-DYj_td4Q.js +17 -0
- package/web/assets/{client-j2kQmxsO.js → client-Dk-NBvJR.js} +4 -4
- package/web/assets/command-zkzH4hVz.js +38 -0
- package/web/assets/{copy-context-C0Uqsp-3.js → copy-context-D5oiWrYC.js} +1 -1
- package/web/assets/{cose-bilkent-JH36ORCC-CPu3PI8h.js → cose-bilkent-JH36ORCC-BHRk1mO3.js} +1 -1
- package/web/assets/{cynefin-VYW2F7L2-DWxftYAm.js → cynefin-VYW2F7L2-C1B09TRE.js} +1 -1
- package/web/assets/{cynefinDiagram-TSTJHNR4-DsmN2V-p.js → cynefinDiagram-TSTJHNR4-B27qXQbv.js} +1 -1
- package/web/assets/{dagre-VKFMJZFB-COEbApyY.js → dagre-VKFMJZFB-B1qEi32g.js} +1 -1
- package/web/assets/{diagram-FQU43EPY-0O9WGWKB.js → diagram-FQU43EPY-BMkkgADD.js} +1 -1
- package/web/assets/{diagram-G47NLZAW-CutEdMVh.js → diagram-G47NLZAW-Cb8_g1A5.js} +1 -1
- package/web/assets/{diagram-NH7WQ7WH-C9KZc3B2.js → diagram-NH7WQ7WH-B51wuc5m.js} +1 -1
- package/web/assets/{diagram-OA4YK3LP-D8kkPXbY.js → diagram-OA4YK3LP-xnsecbbI.js} +1 -1
- package/web/assets/{diagram-WEI45ONY-_YqiTaGP.js → diagram-WEI45ONY-DSZdyaAV.js} +1 -1
- package/web/assets/{document-Dgt5eZFj.js → document-Dc2dftTW.js} +3 -3
- package/web/assets/{ebnfDiagram-CCIWWBDH-JO-zQh6g.js → ebnfDiagram-CCIWWBDH-aTHmcUGy.js} +1 -1
- package/web/assets/{entities-C3wz7FCx.js → entities-CCUtpskY.js} +1 -1
- package/web/assets/{erDiagram-Q63AITRT-9UjQwJ4P.js → erDiagram-Q63AITRT-CUUPBcvo.js} +1 -1
- package/web/assets/{file-viewer-DZJ8nYnl.js → file-viewer-DJ3rsmef.js} +1 -1
- package/web/assets/{flowDiagram-23GEKE2U-Ca08tnYM.js → flowDiagram-23GEKE2U-aTQXx2Mh.js} +1 -1
- package/web/assets/{folder-image-BfGgxo7i.js → folder-image-Bnv43tzP.js} +1 -1
- package/web/assets/{folder-landing-qUKBCnG-.js → folder-landing-DCnzYXXY.js} +1 -1
- package/web/assets/{folder-list-BfFjv7rR.js → folder-list-DvVkVjiR.js} +1 -1
- package/web/assets/{folder-metadata-DCw8xzHk.js → folder-metadata-C9hMMzIH.js} +1 -1
- package/web/assets/{folder-mobile-bar-Ubz4babf.js → folder-mobile-bar-VoTL1GS0.js} +1 -1
- package/web/assets/folder-plugin-view-Cvd804ZU.js +6 -0
- package/web/assets/{ganttDiagram-NO4QXBWP-KnHzX5W3.js → ganttDiagram-NO4QXBWP-DiwdUp7u.js} +1 -1
- package/web/assets/{gitGraphDiagram-IHSO6WYX-Cm8woNtK.js → gitGraphDiagram-IHSO6WYX-BL2gG_IS.js} +1 -1
- package/web/assets/{index-CNyy98F8.js → index-CUTtMSyJ.js} +2 -2
- package/web/assets/index-CegvVQ1C.js +1 -0
- package/web/assets/index-Dwhf8F_U.css +1 -0
- package/web/assets/{infoDiagram-FWYZ7A6U-yyqmUtIb.js → infoDiagram-FWYZ7A6U-B3n0zZpt.js} +1 -1
- package/web/assets/{ishikawaDiagram-FXEZZL3T-BfvR_Zy4.js → ishikawaDiagram-FXEZZL3T-D7YytbEv.js} +1 -1
- package/web/assets/{journeyDiagram-5HDEW3XC-BKuw06HM.js → journeyDiagram-5HDEW3XC-BYwqEl3C.js} +1 -1
- package/web/assets/{kanban-definition-HUTT4EX6-n0010rmQ.js → kanban-definition-HUTT4EX6-BQHNJgXZ.js} +1 -1
- package/web/assets/{linear-C0d0lT8O.js → linear-BPiQCLNk.js} +1 -1
- package/web/assets/{mermaid.core-BDVbShyO.js → mermaid.core-CD-KvBjV.js} +4 -4
- package/web/assets/{metadata-KV0dpp4G.js → metadata-COJ7fZul.js} +1 -1
- package/web/assets/{mindmap-definition-LN4V7U3C--_0ks3GF.js → mindmap-definition-LN4V7U3C-C1L_gyoj.js} +1 -1
- package/web/assets/{pegDiagram-2B236MQR-CBoNIpKR.js → pegDiagram-2B236MQR-BDAfItqS.js} +1 -1
- package/web/assets/{pieDiagram-ENE6RG2P-MA5tkYh5.js → pieDiagram-ENE6RG2P-CkZYH3Vz.js} +1 -1
- package/web/assets/{popover-DBD7FF-W.js → popover-0oA2lLUV.js} +2 -2
- package/web/assets/{project-search-dialog-DM1IkSZu.js → project-search-dialog-B2oqMT6h.js} +1 -1
- package/web/assets/{quadrantDiagram-ABIIQ3AL-CWT1ryqF.js → quadrantDiagram-ABIIQ3AL-Dy8Al3YZ.js} +1 -1
- package/web/assets/{railroadDiagram-RFXS5EU6-ZiLB5FGi.js → railroadDiagram-RFXS5EU6-Dd9_yo7B.js} +1 -1
- package/web/assets/{requirementDiagram-TGXJPOKE-CQq9uneq.js → requirementDiagram-TGXJPOKE-DPhIUsbs.js} +1 -1
- package/web/assets/{sankeyDiagram-HTMAVEWB-qDG-0JqI.js → sankeyDiagram-HTMAVEWB-B2QkYtub.js} +1 -1
- package/web/assets/{search-DNrQA5fF.js → search-C1J0-xiB.js} +1 -1
- package/web/assets/select-CtXC_PLC.js +30 -0
- package/web/assets/{selection-popup-DmtTeRs2.js → selection-popup-qhlUFzGq.js} +1 -1
- package/web/assets/{sequenceDiagram-DBY2YBRQ-CiApurLO.js → sequenceDiagram-DBY2YBRQ-Bx7fcZDx.js} +1 -1
- package/web/assets/{sizeCapture-X5ZJPWSS-DZSR5QXm.js → sizeCapture-X5ZJPWSS-CIXIhl_A.js} +1 -1
- package/web/assets/{stateDiagram-2N3HPSRC-CjJJR9_z.js → stateDiagram-2N3HPSRC-DCrdAflB.js} +1 -1
- package/web/assets/stateDiagram-v2-6OUMAXLB-0KCFLYeQ.js +1 -0
- package/web/assets/{swimlanes-5IMT3BWC-CKmo1sg6.js → swimlanes-5IMT3BWC-DA2vc7iZ.js} +2 -2
- package/web/assets/swimlanesDiagram-G3AALYLV-CAw3AV84.js +8 -0
- package/web/assets/{timeline-definition-FHXFAJF6-D-NMwS3y.js → timeline-definition-FHXFAJF6-lKfJiV9V.js} +1 -1
- package/web/assets/{vennDiagram-L72KCM5P-CfL6LBWw.js → vennDiagram-L72KCM5P-DO1q2KzG.js} +1 -1
- package/web/assets/{view-context-gyDl_p2V.js → view-context-DXKM3-l1.js} +4 -4
- package/web/assets/{viewer-capabilities-Bpp8Uvcg.js → viewer-capabilities-Cig_xM9u.js} +1 -1
- package/web/assets/{wardleyDiagram-EHGQE667-DgldMxaj.js → wardleyDiagram-EHGQE667-Dal9LkuZ.js} +1 -1
- package/web/assets/{xychartDiagram-FW5EYKEG-CIoAIFX8.js → xychartDiagram-FW5EYKEG-B3dPLPII.js} +1 -1
- package/web/index.html +2 -2
- package/kernel/packages/api/test/partial-loading.test.ts +0 -43
- package/kernel/plugins/calendar/SKILL.md +0 -173
- package/kernel/plugins/calendar/eslint.config.js +0 -10
- package/kernel/plugins/calendar/package.json +0 -73
- package/kernel/plugins/calendar/schema.json +0 -137
- package/kernel/plugins/calendar/src/calendar.tsx +0 -328
- package/kernel/plugins/calendar/src/client.tsx +0 -139
- package/kernel/plugins/calendar/src/context.ts +0 -48
- package/kernel/plugins/calendar/src/copy-button.tsx +0 -66
- package/kernel/plugins/calendar/src/event-dialog.tsx +0 -270
- package/kernel/plugins/calendar/src/index.ts +0 -4
- package/kernel/plugins/calendar/src/manifest.ts +0 -7
- package/kernel/plugins/calendar/src/parse.ts +0 -610
- package/kernel/plugins/calendar/src/server.ts +0 -160
- package/kernel/plugins/calendar/src/utils.ts +0 -80
- package/kernel/plugins/calendar/src/views.tsx +0 -418
- package/kernel/plugins/calendar/test/context.test.mjs +0 -60
- package/kernel/plugins/calendar/test/package.test.mjs +0 -43
- package/kernel/plugins/calendar/test/parse.test.mjs +0 -392
- package/kernel/plugins/calendar/test/server.test.mjs +0 -126
- package/kernel/plugins/calendar/tsconfig.json +0 -17
- package/kernel/plugins/mail/SKILL.md +0 -97
- package/kernel/plugins/mail/eslint.config.js +0 -10
- package/kernel/plugins/mail/package.json +0 -79
- package/kernel/plugins/mail/src/api.ts +0 -359
- package/kernel/plugins/mail/src/bun-ffi.d.ts +0 -14
- package/kernel/plugins/mail/src/client.tsx +0 -14
- package/kernel/plugins/mail/src/flock.ts +0 -137
- package/kernel/plugins/mail/src/folders.ts +0 -37
- package/kernel/plugins/mail/src/history.ts +0 -485
- package/kernel/plugins/mail/src/index.ts +0 -3
- package/kernel/plugins/mail/src/mail-card.tsx +0 -123
- package/kernel/plugins/mail/src/mail-conversation.tsx +0 -153
- package/kernel/plugins/mail/src/mail-detail-page.tsx +0 -467
- package/kernel/plugins/mail/src/mail-hooks.ts +0 -252
- package/kernel/plugins/mail/src/mail-page.tsx +0 -109
- package/kernel/plugins/mail/src/mail-request.ts +0 -78
- package/kernel/plugins/mail/src/mail-send-box.tsx +0 -175
- package/kernel/plugins/mail/src/mailos.ts +0 -723
- package/kernel/plugins/mail/src/manifest.ts +0 -7
- package/kernel/plugins/mail/src/node.ts +0 -171
- package/kernel/plugins/mail/src/parse.ts +0 -139
- package/kernel/plugins/mail/src/server.ts +0 -125
- package/kernel/plugins/mail/src/types.ts +0 -10
- package/kernel/plugins/mail/src/viewer.tsx +0 -147
- package/kernel/plugins/mail/test/flock.test.mjs +0 -156
- package/kernel/plugins/mail/test/folders.test.mjs +0 -138
- package/kernel/plugins/mail/test/history.test.mjs +0 -460
- package/kernel/plugins/mail/test/mailos.test.mjs +0 -752
- package/kernel/plugins/mail/test/node.test.mjs +0 -207
- package/kernel/plugins/mail/test/package.test.mjs +0 -54
- package/kernel/plugins/mail/test/parse.test.mjs +0 -32
- package/kernel/plugins/mail/test/server.test.mjs +0 -116
- package/kernel/plugins/mail/test/viewer.test.mjs +0 -192
- package/kernel/plugins/mail/tsconfig.json +0 -17
- package/kernel/plugins/projects/SKILL.md +0 -73
- package/kernel/plugins/projects/eslint.config.js +0 -10
- package/kernel/plugins/projects/package.json +0 -70
- package/kernel/plugins/projects/src/client.tsx +0 -17
- package/kernel/plugins/projects/src/data/model.ts +0 -129
- package/kernel/plugins/projects/src/index.ts +0 -17
- package/kernel/plugins/projects/src/manifest.ts +0 -7
- package/kernel/plugins/projects/src/project-context.ts +0 -41
- package/kernel/plugins/projects/src/server.ts +0 -276
- package/kernel/plugins/projects/src/ui/project-card.tsx +0 -61
- package/kernel/plugins/projects/src/ui/projects-page.tsx +0 -102
- package/kernel/plugins/projects/test/package.test.mjs +0 -51
- package/kernel/plugins/projects/tsconfig.json +0 -17
- package/kernel/plugins/tasks/SKILL.md +0 -341
- package/kernel/plugins/tasks/eslint.config.js +0 -10
- package/kernel/plugins/tasks/package.json +0 -74
- package/kernel/plugins/tasks/schema.json +0 -939
- package/kernel/plugins/tasks/src/api.ts +0 -212
- package/kernel/plugins/tasks/src/client.tsx +0 -23
- package/kernel/plugins/tasks/src/data/parse.ts +0 -544
- package/kernel/plugins/tasks/src/data/task-id.ts +0 -56
- package/kernel/plugins/tasks/src/data/todo-model.ts +0 -795
- package/kernel/plugins/tasks/src/index.ts +0 -34
- package/kernel/plugins/tasks/src/manifest.ts +0 -7
- package/kernel/plugins/tasks/src/pages/task-detail.tsx +0 -48
- package/kernel/plugins/tasks/src/pages/task-list.tsx +0 -88
- package/kernel/plugins/tasks/src/server.ts +0 -447
- package/kernel/plugins/tasks/src/styles.css +0 -9
- package/kernel/plugins/tasks/src/ui/compact-todo-list.tsx +0 -372
- package/kernel/plugins/tasks/src/ui/error-details.tsx +0 -78
- package/kernel/plugins/tasks/src/ui/task-page.tsx +0 -392
- package/kernel/plugins/tasks/src/ui/todo-agent-filter.tsx +0 -36
- package/kernel/plugins/tasks/src/ui/todo-filter-bar.tsx +0 -66
- package/kernel/plugins/tasks/src/ui/todo-item.tsx +0 -494
- package/kernel/plugins/tasks/src/ui/todo-status-filter.tsx +0 -60
- package/kernel/plugins/tasks/src/ui/todo-tag-filter.tsx +0 -48
- package/kernel/plugins/tasks/src/ui/todo-warnings.tsx +0 -106
- package/kernel/plugins/tasks/test/api.test.mjs +0 -99
- package/kernel/plugins/tasks/test/folder-sources.test.mjs +0 -46
- package/kernel/plugins/tasks/test/package.test.mjs +0 -54
- package/kernel/plugins/tasks/test/parse.test.mjs +0 -225
- package/kernel/plugins/tasks/test/server.test.mjs +0 -386
- package/kernel/plugins/tasks/tsconfig.json +0 -17
- package/web/assets/app-DoGr2Zi7.js +0 -2
- package/web/assets/channel-uMEjV8qc.js +0 -1
- package/web/assets/classDiagram-OUVF2IWQ-fbZocVor.js +0 -1
- package/web/assets/classDiagram-v2-EOCWNBFH-fbZocVor.js +0 -1
- package/web/assets/client-C6Nxdfif.js +0 -2
- package/web/assets/client-CXYfBko5.js +0 -17
- package/web/assets/client-CoKtOq-X.js +0 -7
- package/web/assets/client-DyRQz5RU.js +0 -53
- package/web/assets/client-OY6HUzuD.js +0 -98
- package/web/assets/client-YOYWZIFi.js +0 -34
- package/web/assets/command-BugZBGXE.js +0 -38
- package/web/assets/folder-plugin-view-DsWR1zmQ.js +0 -6
- package/web/assets/index-BaRnbYRw.js +0 -1
- package/web/assets/index-BexmLxIP.css +0 -1
- package/web/assets/index-DDirZMgx.js +0 -1
- package/web/assets/note-card-DBfX6gW1.js +0 -6
- package/web/assets/select-D8cqNgcp.js +0 -30
- package/web/assets/stateDiagram-v2-6OUMAXLB-BpeGSCu9.js +0 -1
- package/web/assets/swimlanesDiagram-G3AALYLV-D9RjLKb9.js +0 -8
- package/web/assets/use-folder-filter-LQyXns5Q.js +0 -1
|
@@ -1,544 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validation functions for the raw `todo.json` source document.
|
|
3
|
-
*
|
|
4
|
-
* This module is the tasks plugin's "validate" seam: it turns a source file
|
|
5
|
-
* into either a typed document or a list of precise, human-readable issues
|
|
6
|
-
* (`path` + `message`). It is deliberately dependency-free and shared by the
|
|
7
|
-
* server entry (detection + data loading), the client error panel, and the
|
|
8
|
-
* plugin's node tests.
|
|
9
|
-
*
|
|
10
|
-
* Parsing never throws: invalid JSON and structural mistakes are reported as
|
|
11
|
-
* issues so the host and viewer can show exactly what is wrong instead of a
|
|
12
|
-
* 404 or a generic "could not parse" message.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { ContentIssue } from "@raggle-ai/folder-plugin/validation"
|
|
16
|
-
import {
|
|
17
|
-
issueSummary,
|
|
18
|
-
parseJsonSource,
|
|
19
|
-
} from "@raggle-ai/folder-plugin/validation"
|
|
20
|
-
|
|
21
|
-
export const TASK_TYPES = [
|
|
22
|
-
"task",
|
|
23
|
-
"bug",
|
|
24
|
-
"feature",
|
|
25
|
-
"chore",
|
|
26
|
-
"spike",
|
|
27
|
-
"epic",
|
|
28
|
-
"milestone",
|
|
29
|
-
] as const
|
|
30
|
-
|
|
31
|
-
export const TASK_STATUSES = [
|
|
32
|
-
"todo",
|
|
33
|
-
"in_progress",
|
|
34
|
-
"blocked",
|
|
35
|
-
"review",
|
|
36
|
-
"done",
|
|
37
|
-
"canceled",
|
|
38
|
-
] as const
|
|
39
|
-
|
|
40
|
-
export const RELATION_TYPES = [
|
|
41
|
-
"blocks",
|
|
42
|
-
"blocked_by",
|
|
43
|
-
"related_to",
|
|
44
|
-
"duplicates",
|
|
45
|
-
"duplicated_by",
|
|
46
|
-
"parent_of",
|
|
47
|
-
"child_of",
|
|
48
|
-
] as const
|
|
49
|
-
|
|
50
|
-
export const LINK_PROVIDERS = ["github", "linear", "jira", "web"] as const
|
|
51
|
-
|
|
52
|
-
export const LINK_KINDS = [
|
|
53
|
-
"issue",
|
|
54
|
-
"pull_request",
|
|
55
|
-
"commit",
|
|
56
|
-
"repository",
|
|
57
|
-
"project",
|
|
58
|
-
"document",
|
|
59
|
-
"design",
|
|
60
|
-
"discussion",
|
|
61
|
-
"external",
|
|
62
|
-
] as const
|
|
63
|
-
|
|
64
|
-
export const LINK_RELATIONS = [
|
|
65
|
-
"references",
|
|
66
|
-
"implemented_by",
|
|
67
|
-
"fixes",
|
|
68
|
-
"reported_by",
|
|
69
|
-
"spec",
|
|
70
|
-
"design",
|
|
71
|
-
"review",
|
|
72
|
-
"deployment",
|
|
73
|
-
"blocked_by",
|
|
74
|
-
] as const
|
|
75
|
-
|
|
76
|
-
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/
|
|
77
|
-
|
|
78
|
-
/** One precise problem found in a task document. `path` is JSON-pointer-ish. */
|
|
79
|
-
export type TodoIssue = ContentIssue
|
|
80
|
-
|
|
81
|
-
/** The source document after validation; tasks keep their raw shapes. */
|
|
82
|
-
export type TodoFile = {
|
|
83
|
-
$schema?: string
|
|
84
|
-
id?: string
|
|
85
|
-
tasks: Array<Record<string, unknown>>
|
|
86
|
-
title?: string
|
|
87
|
-
version?: number
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export type ParsedTodo =
|
|
91
|
-
{ ok: true; document: TodoFile } | { ok: false; issues: TodoIssue[] }
|
|
92
|
-
|
|
93
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
94
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function nonEmptyString(value: unknown): value is string {
|
|
98
|
-
return typeof value === "string" && value.trim().length > 0
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Compact summary for nav badges and error headers. */
|
|
102
|
-
export function todoIssueSummary(issues: TodoIssue[]): string {
|
|
103
|
-
return issueSummary(issues, "todo.json")
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Validate an already-parsed JSON value. Returns every problem found; an
|
|
108
|
-
* empty array means the document is structurally valid. Enums below mirror
|
|
109
|
-
* the published schema (`https://raggle.co/schemas/tasks.json`) and the
|
|
110
|
-
* client decoder in `./ui` (this plugin's vendored todo UI).
|
|
111
|
-
*/
|
|
112
|
-
export function validateTodoFile(value: unknown): TodoIssue[] {
|
|
113
|
-
if (!isRecord(value)) {
|
|
114
|
-
return [{ path: "#", message: "todo.json must contain a JSON object" }]
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const issues: TodoIssue[] = []
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
value.version !== undefined &&
|
|
121
|
-
value.version !== 1 &&
|
|
122
|
-
value.version !== 2
|
|
123
|
-
) {
|
|
124
|
-
issues.push({
|
|
125
|
-
path: "version",
|
|
126
|
-
message: `must be 1 or 2; got ${JSON.stringify(value.version)}`,
|
|
127
|
-
})
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (value.$schema !== undefined && !nonEmptyString(value.$schema)) {
|
|
131
|
-
issues.push({ path: "$schema", message: "must be a non-empty string" })
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (value.id !== undefined && !nonEmptyString(value.id)) {
|
|
135
|
-
issues.push({ path: "id", message: "must be a non-empty string" })
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (value.title !== undefined && !nonEmptyString(value.title)) {
|
|
139
|
-
issues.push({ path: "title", message: "must be a non-empty string" })
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (!Array.isArray(value.tasks)) {
|
|
143
|
-
issues.push({ path: "tasks", message: "must be an array of tasks" })
|
|
144
|
-
return issues
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const ids = new Set<string>()
|
|
148
|
-
const keys = new Set<string>()
|
|
149
|
-
value.tasks.forEach((task, index) => {
|
|
150
|
-
validateTask(task, `tasks[${index}]`, issues, ids, keys)
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
// Cross-task integrity: relation targets must point at a known task id.
|
|
154
|
-
value.tasks.forEach((task, index) => {
|
|
155
|
-
if (!isRecord(task) || !Array.isArray(task.relations)) return
|
|
156
|
-
task.relations.forEach((relation, relIndex) => {
|
|
157
|
-
if (!isRecord(relation)) return
|
|
158
|
-
const target = relation.targetTaskId
|
|
159
|
-
if (typeof target === "string" && !ids.has(target)) {
|
|
160
|
-
issues.push({
|
|
161
|
-
path: `tasks[${index}].relations[${relIndex}].targetTaskId`,
|
|
162
|
-
message: `references unknown task id ${JSON.stringify(target)}`,
|
|
163
|
-
})
|
|
164
|
-
}
|
|
165
|
-
})
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
return issues
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function validateTask(
|
|
172
|
-
task: unknown,
|
|
173
|
-
path: string,
|
|
174
|
-
issues: TodoIssue[],
|
|
175
|
-
ids: Set<string>,
|
|
176
|
-
keys: Set<string>,
|
|
177
|
-
) {
|
|
178
|
-
if (!isRecord(task)) {
|
|
179
|
-
issues.push({ path, message: "must be a task object" })
|
|
180
|
-
return
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (!nonEmptyString(task.name)) {
|
|
184
|
-
issues.push({
|
|
185
|
-
path: `${path}.name`,
|
|
186
|
-
message: "required; a task needs a non-empty name",
|
|
187
|
-
})
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (task.id !== undefined) {
|
|
191
|
-
if (!nonEmptyString(task.id)) {
|
|
192
|
-
issues.push({ path: `${path}.id`, message: "must be a non-empty string" })
|
|
193
|
-
} else if (ids.has(task.id)) {
|
|
194
|
-
issues.push({
|
|
195
|
-
path: `${path}.id`,
|
|
196
|
-
message: `duplicate task id ${JSON.stringify(task.id)}`,
|
|
197
|
-
})
|
|
198
|
-
} else {
|
|
199
|
-
ids.add(task.id)
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (task.key !== undefined) {
|
|
204
|
-
if (!nonEmptyString(task.key)) {
|
|
205
|
-
issues.push({
|
|
206
|
-
path: `${path}.key`,
|
|
207
|
-
message: "must be a non-empty string",
|
|
208
|
-
})
|
|
209
|
-
} else if (keys.has(task.key)) {
|
|
210
|
-
issues.push({
|
|
211
|
-
path: `${path}.key`,
|
|
212
|
-
message: `duplicate task key ${JSON.stringify(task.key)}`,
|
|
213
|
-
})
|
|
214
|
-
} else {
|
|
215
|
-
keys.add(task.key)
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (task.type !== undefined && !inEnum(task.type, TASK_TYPES)) {
|
|
220
|
-
issues.push({
|
|
221
|
-
path: `${path}.type`,
|
|
222
|
-
message: `must be one of ${TASK_TYPES.join(", ")}; got ${JSON.stringify(task.type)}`,
|
|
223
|
-
})
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (task.status !== undefined && !inEnum(task.status, TASK_STATUSES)) {
|
|
227
|
-
issues.push({
|
|
228
|
-
path: `${path}.status`,
|
|
229
|
-
message: `must be one of ${TASK_STATUSES.join(", ")}; got ${JSON.stringify(task.status)}`,
|
|
230
|
-
})
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (task.done !== undefined && typeof task.done !== "boolean") {
|
|
234
|
-
issues.push({ path: `${path}.done`, message: "must be a boolean" })
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (task.priority !== undefined && task.priority !== null) {
|
|
238
|
-
const priority = task.priority
|
|
239
|
-
if (
|
|
240
|
-
typeof priority !== "number" ||
|
|
241
|
-
!Number.isInteger(priority) ||
|
|
242
|
-
priority < 1 ||
|
|
243
|
-
priority > 5
|
|
244
|
-
) {
|
|
245
|
-
issues.push({
|
|
246
|
-
path: `${path}.priority`,
|
|
247
|
-
message: "must be an integer between 1 and 5",
|
|
248
|
-
})
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
task.deadline !== undefined &&
|
|
254
|
-
task.deadline !== null &&
|
|
255
|
-
(typeof task.deadline !== "string" || !DATE_PATTERN.test(task.deadline))
|
|
256
|
-
) {
|
|
257
|
-
issues.push({
|
|
258
|
-
path: `${path}.deadline`,
|
|
259
|
-
message: "must be an ISO date (YYYY-MM-DD)",
|
|
260
|
-
})
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
for (const section of ["acceptanceCriteria", "steps"]) {
|
|
264
|
-
const items = task[section]
|
|
265
|
-
if (items === undefined) continue
|
|
266
|
-
if (!Array.isArray(items)) {
|
|
267
|
-
issues.push({
|
|
268
|
-
path: `${path}.${section}`,
|
|
269
|
-
message: "must be an array of check items",
|
|
270
|
-
})
|
|
271
|
-
continue
|
|
272
|
-
}
|
|
273
|
-
items.forEach((item, index) => {
|
|
274
|
-
validateCheckItem(item, `${path}.${section}[${index}]`, issues)
|
|
275
|
-
})
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (task.links !== undefined) {
|
|
279
|
-
if (!Array.isArray(task.links)) {
|
|
280
|
-
issues.push({
|
|
281
|
-
path: `${path}.links`,
|
|
282
|
-
message: "must be an array of links",
|
|
283
|
-
})
|
|
284
|
-
} else {
|
|
285
|
-
task.links.forEach((link, index) => {
|
|
286
|
-
validateLink(link, `${path}.links[${index}]`, issues)
|
|
287
|
-
})
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (task.notes !== undefined) {
|
|
292
|
-
if (!Array.isArray(task.notes)) {
|
|
293
|
-
issues.push({
|
|
294
|
-
path: `${path}.notes`,
|
|
295
|
-
message: "must be an array of note references",
|
|
296
|
-
})
|
|
297
|
-
} else {
|
|
298
|
-
task.notes.forEach((note, index) => {
|
|
299
|
-
validateNoteRef(note, `${path}.notes[${index}]`, issues)
|
|
300
|
-
})
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const UNSAFE_NOTE_REFERENCE = /^(?:\/|\\\\|[a-z][a-z0-9+.-]*:|~)/i
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* One `notes` entry: an https URL, or a folder-relative note path with an
|
|
309
|
-
* optional `#section` heading (for example "notes/spec.md#Plan").
|
|
310
|
-
*/
|
|
311
|
-
function validateNoteRef(note: unknown, path: string, issues: TodoIssue[]) {
|
|
312
|
-
if (!nonEmptyString(note)) {
|
|
313
|
-
issues.push({ path, message: "must be a non-empty string" })
|
|
314
|
-
return
|
|
315
|
-
}
|
|
316
|
-
if (/^https?:\/\//i.test(note)) {
|
|
317
|
-
let url: URL
|
|
318
|
-
try {
|
|
319
|
-
url = new URL(note)
|
|
320
|
-
} catch {
|
|
321
|
-
issues.push({ path, message: "is not a valid absolute URL" })
|
|
322
|
-
return
|
|
323
|
-
}
|
|
324
|
-
if (url.username || url.password) {
|
|
325
|
-
issues.push({ path, message: "must not contain embedded credentials" })
|
|
326
|
-
return
|
|
327
|
-
}
|
|
328
|
-
const isLocalhost =
|
|
329
|
-
url.hostname === "localhost" ||
|
|
330
|
-
url.hostname === "127.0.0.1" ||
|
|
331
|
-
url.hostname === "::1"
|
|
332
|
-
if (
|
|
333
|
-
url.protocol !== "https:" &&
|
|
334
|
-
!(url.protocol === "http:" && isLocalhost)
|
|
335
|
-
) {
|
|
336
|
-
issues.push({
|
|
337
|
-
path,
|
|
338
|
-
message: "must use https, except localhost development links",
|
|
339
|
-
})
|
|
340
|
-
}
|
|
341
|
-
return
|
|
342
|
-
}
|
|
343
|
-
if (UNSAFE_NOTE_REFERENCE.test(note) || note.split(/[\\/]/).includes("..")) {
|
|
344
|
-
issues.push({
|
|
345
|
-
path,
|
|
346
|
-
message: "must be a folder-relative path (or an https URL)",
|
|
347
|
-
})
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function validateCheckItem(item: unknown, path: string, issues: TodoIssue[]) {
|
|
352
|
-
if (!isRecord(item)) {
|
|
353
|
-
issues.push({
|
|
354
|
-
path,
|
|
355
|
-
message: "must be an object with id, text and done",
|
|
356
|
-
})
|
|
357
|
-
return
|
|
358
|
-
}
|
|
359
|
-
if (!nonEmptyString(item.id)) {
|
|
360
|
-
issues.push({ path: `${path}.id`, message: "required non-empty string" })
|
|
361
|
-
}
|
|
362
|
-
if (!nonEmptyString(item.text)) {
|
|
363
|
-
issues.push({ path: `${path}.text`, message: "required non-empty string" })
|
|
364
|
-
}
|
|
365
|
-
if (typeof item.done !== "boolean") {
|
|
366
|
-
issues.push({ path: `${path}.done`, message: "required boolean" })
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function validateLink(link: unknown, path: string, issues: TodoIssue[]) {
|
|
371
|
-
if (!isRecord(link)) {
|
|
372
|
-
issues.push({ path, message: "must be a link object" })
|
|
373
|
-
return
|
|
374
|
-
}
|
|
375
|
-
for (const field of ["id", "provider", "kind", "relation", "url"]) {
|
|
376
|
-
if (!nonEmptyString(link[field])) {
|
|
377
|
-
issues.push({
|
|
378
|
-
path: `${path}.${field}`,
|
|
379
|
-
message: "required non-empty string",
|
|
380
|
-
})
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
if (link.provider !== undefined && !inEnum(link.provider, LINK_PROVIDERS)) {
|
|
384
|
-
issues.push({
|
|
385
|
-
path: `${path}.provider`,
|
|
386
|
-
message: `must be one of ${LINK_PROVIDERS.join(", ")}; got ${JSON.stringify(link.provider)}`,
|
|
387
|
-
})
|
|
388
|
-
}
|
|
389
|
-
if (link.kind !== undefined && !inEnum(link.kind, LINK_KINDS)) {
|
|
390
|
-
issues.push({
|
|
391
|
-
path: `${path}.kind`,
|
|
392
|
-
message: `must be one of ${LINK_KINDS.join(", ")}; got ${JSON.stringify(link.kind)}`,
|
|
393
|
-
})
|
|
394
|
-
}
|
|
395
|
-
if (link.relation !== undefined && !inEnum(link.relation, LINK_RELATIONS)) {
|
|
396
|
-
issues.push({
|
|
397
|
-
path: `${path}.relation`,
|
|
398
|
-
message: `must be one of ${LINK_RELATIONS.join(", ")}; got ${JSON.stringify(link.relation)}`,
|
|
399
|
-
})
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const urlValue = link.url
|
|
403
|
-
if (typeof urlValue === "string") {
|
|
404
|
-
let url: URL
|
|
405
|
-
try {
|
|
406
|
-
url = new URL(urlValue)
|
|
407
|
-
} catch {
|
|
408
|
-
issues.push({
|
|
409
|
-
path: `${path}.url`,
|
|
410
|
-
message: "is not a valid absolute URL",
|
|
411
|
-
})
|
|
412
|
-
return
|
|
413
|
-
}
|
|
414
|
-
if (url.username || url.password) {
|
|
415
|
-
issues.push({
|
|
416
|
-
path: `${path}.url`,
|
|
417
|
-
message: "must not contain embedded credentials",
|
|
418
|
-
})
|
|
419
|
-
}
|
|
420
|
-
const isLocalhost =
|
|
421
|
-
url.hostname === "localhost" ||
|
|
422
|
-
url.hostname === "127.0.0.1" ||
|
|
423
|
-
url.hostname === "::1"
|
|
424
|
-
if (
|
|
425
|
-
url.protocol !== "https:" &&
|
|
426
|
-
!(url.protocol === "http:" && isLocalhost)
|
|
427
|
-
) {
|
|
428
|
-
issues.push({
|
|
429
|
-
path: `${path}.url`,
|
|
430
|
-
message: "must use https, except localhost development links",
|
|
431
|
-
})
|
|
432
|
-
}
|
|
433
|
-
if (link.provider === "github" && url.hostname !== "github.com") {
|
|
434
|
-
issues.push({
|
|
435
|
-
path: `${path}.url`,
|
|
436
|
-
message: "github links must use github.com",
|
|
437
|
-
})
|
|
438
|
-
}
|
|
439
|
-
if (
|
|
440
|
-
link.provider === "linear" &&
|
|
441
|
-
url.hostname !== "linear.app" &&
|
|
442
|
-
!url.hostname.endsWith(".linear.app")
|
|
443
|
-
) {
|
|
444
|
-
issues.push({
|
|
445
|
-
path: `${path}.url`,
|
|
446
|
-
message: "linear links must use linear.app",
|
|
447
|
-
})
|
|
448
|
-
}
|
|
449
|
-
if (
|
|
450
|
-
link.provider === "jira" &&
|
|
451
|
-
!url.hostname.endsWith(".atlassian.net") &&
|
|
452
|
-
url.hostname !== "jira.atlassian.com"
|
|
453
|
-
) {
|
|
454
|
-
issues.push({
|
|
455
|
-
path: `${path}.url`,
|
|
456
|
-
message: "jira links must use an Atlassian Jira host",
|
|
457
|
-
})
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function inEnum(value: unknown, allowed: readonly string[]) {
|
|
463
|
-
return allowed.includes(String(value))
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* Advisory checks that do not invalidate the file. Today there is one: a
|
|
468
|
-
* task with no `notes` entries warns, because todos are meant to be
|
|
469
|
-
* accompanied by notes — link a note file or an https reference. Paths use
|
|
470
|
-
* the same `tasks[i]` convention as `validateTodoFile` issues, relative to
|
|
471
|
-
* the file the task lives in.
|
|
472
|
-
*/
|
|
473
|
-
export function todoFileWarnings(value: unknown): TodoIssue[] {
|
|
474
|
-
if (!isRecord(value) || !Array.isArray(value.tasks)) return []
|
|
475
|
-
return value.tasks.flatMap((task, index) =>
|
|
476
|
-
isRecord(task) &&
|
|
477
|
-
Array.isArray(task.notes) &&
|
|
478
|
-
task.notes.some((note) => nonEmptyString(note))
|
|
479
|
-
? []
|
|
480
|
-
: [
|
|
481
|
-
{
|
|
482
|
-
message:
|
|
483
|
-
"no notes; link the accompanying note file or https reference",
|
|
484
|
-
path: `tasks[${index}].notes`,
|
|
485
|
-
severity: "warning" as const,
|
|
486
|
-
},
|
|
487
|
-
],
|
|
488
|
-
)
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Parse a `todo.json` source string into a validated document. Never throws:
|
|
493
|
-
* invalid JSON and structural mistakes come back as issues with paths, so the
|
|
494
|
-
* caller can render "what is wrong and where" directly.
|
|
495
|
-
*/
|
|
496
|
-
export function parseTodoSource(source: string): ParsedTodo {
|
|
497
|
-
return parseJsonSource<TodoFile>(source, validateTodoFile)
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Starter `todo.json` for `kennel init`: a valid v2 document with one sample
|
|
502
|
-
* task showing the common fields. The folder name becomes the project title.
|
|
503
|
-
*/
|
|
504
|
-
export function starterTodoSource(folder: string): string {
|
|
505
|
-
const title = folder.trim().split(/[\\/]/).filter(Boolean).at(-1) ?? "Tasks"
|
|
506
|
-
return `${JSON.stringify(
|
|
507
|
-
{
|
|
508
|
-
$schema: "https://raggle.co/schemas/tasks.json",
|
|
509
|
-
version: 2,
|
|
510
|
-
id: "todo",
|
|
511
|
-
title,
|
|
512
|
-
people: [],
|
|
513
|
-
projects: [],
|
|
514
|
-
cycles: [],
|
|
515
|
-
milestones: [],
|
|
516
|
-
components: [],
|
|
517
|
-
areas: [],
|
|
518
|
-
integrations: [],
|
|
519
|
-
customFields: [],
|
|
520
|
-
automations: [],
|
|
521
|
-
tasks: [
|
|
522
|
-
{
|
|
523
|
-
id: "todo:0",
|
|
524
|
-
key: "TODO-1",
|
|
525
|
-
index: 0,
|
|
526
|
-
rank: "0001",
|
|
527
|
-
name: "First task",
|
|
528
|
-
description: "Describe what needs to happen.",
|
|
529
|
-
type: "task",
|
|
530
|
-
status: "todo",
|
|
531
|
-
done: false,
|
|
532
|
-
priority: 3,
|
|
533
|
-
deadline: null,
|
|
534
|
-
assigneeIds: [],
|
|
535
|
-
acceptanceCriteria: [
|
|
536
|
-
{ id: "ac-1", text: "Acceptance check", done: false },
|
|
537
|
-
],
|
|
538
|
-
},
|
|
539
|
-
],
|
|
540
|
-
},
|
|
541
|
-
null,
|
|
542
|
-
2,
|
|
543
|
-
)}\n`
|
|
544
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { folderPathId } from "@raggle-ai/folder-plugin/client"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Tasks from several `todo.json` sources merge into one workspace list, and
|
|
5
|
-
* file-local ids such as `todo:1` repeat across sources. Task addresses
|
|
6
|
-
* therefore qualify the task id with its owning folder:
|
|
7
|
-
*
|
|
8
|
-
* `<folderPathId(source folder)>@<task id|key>`
|
|
9
|
-
*
|
|
10
|
-
* `folderPathId` is the viewer's stable base64url folder id, so the address
|
|
11
|
-
* stays one URL-safe path segment and survives the single-segment
|
|
12
|
-
* `/folder/<id>/views/tasks/<subpath>` route. A bare `<taskId|key>` remains
|
|
13
|
-
* valid as a legacy address and resolves to the first matching source (the
|
|
14
|
-
* previous behavior), but every address the plugin builds is qualified.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const QUALIFIER = "@"
|
|
18
|
-
|
|
19
|
-
/** Base64url output of `folderPathId`; a task id ref must look like one. */
|
|
20
|
-
const REF_PATTERN = /^[A-Za-z0-9_-]+$/
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Qualified task address for a task inside `sourceFolder`, or undefined when
|
|
24
|
-
* the task has neither id nor key (or the source folder is unknown).
|
|
25
|
-
*/
|
|
26
|
-
export function qualifiedTaskId(
|
|
27
|
-
sourceFolder: unknown,
|
|
28
|
-
task: { id?: unknown; key?: unknown },
|
|
29
|
-
): string | undefined {
|
|
30
|
-
if (typeof sourceFolder !== "string" || !sourceFolder) return undefined
|
|
31
|
-
const target =
|
|
32
|
-
typeof task.id === "string" && task.id.trim()
|
|
33
|
-
? task.id.trim()
|
|
34
|
-
: typeof task.key === "string" && task.key.trim()
|
|
35
|
-
? task.key.trim()
|
|
36
|
-
: undefined
|
|
37
|
-
if (!target) return undefined
|
|
38
|
-
return `${folderPathId(sourceFolder)}${QUALIFIER}${target}`
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Splits a qualified address; undefined for a plain task id/key string. */
|
|
42
|
-
export function parseQualifiedTaskId(
|
|
43
|
-
value: string,
|
|
44
|
-
): { id: string; ref: string } | undefined {
|
|
45
|
-
const at = value.indexOf(QUALIFIER)
|
|
46
|
-
if (at < 1) return undefined
|
|
47
|
-
const ref = value.slice(0, at)
|
|
48
|
-
const id = value.slice(at + 1)
|
|
49
|
-
if (!id || !REF_PATTERN.test(ref)) return undefined
|
|
50
|
-
return { id, ref }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** True when `ref` is the view-stable id of `sourceFolder`. */
|
|
54
|
-
export function refMatchesFolder(ref: string, sourceFolder: unknown): boolean {
|
|
55
|
-
return typeof sourceFolder === "string" && folderPathId(sourceFolder) === ref
|
|
56
|
-
}
|