@magic-spells/constellation 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +147 -0
  3. package/dist/cli/index.d.ts +2 -0
  4. package/dist/cli/index.js +127 -0
  5. package/dist/cli/index.js.map +1 -0
  6. package/dist/core/extract.d.ts +9 -0
  7. package/dist/core/extract.js +56 -0
  8. package/dist/core/extract.js.map +1 -0
  9. package/dist/core/handles.d.ts +13 -0
  10. package/dist/core/handles.js +45 -0
  11. package/dist/core/handles.js.map +1 -0
  12. package/dist/core/index.d.ts +8 -0
  13. package/dist/core/index.js +9 -0
  14. package/dist/core/index.js.map +1 -0
  15. package/dist/core/indexer.d.ts +3 -0
  16. package/dist/core/indexer.js +209 -0
  17. package/dist/core/indexer.js.map +1 -0
  18. package/dist/core/lint.d.ts +9 -0
  19. package/dist/core/lint.js +19 -0
  20. package/dist/core/lint.js.map +1 -0
  21. package/dist/core/parse.d.ts +7 -0
  22. package/dist/core/parse.js +19 -0
  23. package/dist/core/parse.js.map +1 -0
  24. package/dist/core/resolve.d.ts +12 -0
  25. package/dist/core/resolve.js +57 -0
  26. package/dist/core/resolve.js.map +1 -0
  27. package/dist/core/scaffold.d.ts +3 -0
  28. package/dist/core/scaffold.js +36 -0
  29. package/dist/core/scaffold.js.map +1 -0
  30. package/dist/core/types.d.ts +50 -0
  31. package/dist/core/types.js +5 -0
  32. package/dist/core/types.js.map +1 -0
  33. package/dist/core/validate.d.ts +6 -0
  34. package/dist/core/validate.js +64 -0
  35. package/dist/core/validate.js.map +1 -0
  36. package/dist/core/writer.d.ts +34 -0
  37. package/dist/core/writer.js +170 -0
  38. package/dist/core/writer.js.map +1 -0
  39. package/dist/mcp/git.d.ts +34 -0
  40. package/dist/mcp/git.js +172 -0
  41. package/dist/mcp/git.js.map +1 -0
  42. package/dist/mcp/search.d.ts +11 -0
  43. package/dist/mcp/search.js +52 -0
  44. package/dist/mcp/search.js.map +1 -0
  45. package/dist/mcp/server.d.ts +7 -0
  46. package/dist/mcp/server.js +684 -0
  47. package/dist/mcp/server.js.map +1 -0
  48. package/dist/serve/server.d.ts +12 -0
  49. package/dist/serve/server.js +281 -0
  50. package/dist/serve/server.js.map +1 -0
  51. package/docs/001-file-format.md +222 -0
  52. package/docs/002-mcp.md +109 -0
  53. package/examples/constellation/agent/AGENT-CODE-STYLE.md +14 -0
  54. package/examples/constellation/api/API-TICKETS.md +24 -0
  55. package/examples/constellation/component/COMPONENT-TICKET-CARD.md +16 -0
  56. package/examples/constellation/datatype/DATATYPE-CREATE-TICKET-INPUT.md +15 -0
  57. package/examples/constellation/datatype/DATATYPE-TICKET.md +20 -0
  58. package/examples/constellation/db/DB-TICKETS.md +21 -0
  59. package/examples/constellation/diagram/DIAGRAM-SYSTEM-OVERVIEW.md +19 -0
  60. package/examples/constellation/doc/DOC-TICKET-LIFECYCLE.md +22 -0
  61. package/examples/constellation/event/EVENT-TICKET-CREATED.md +13 -0
  62. package/examples/constellation/external/EXTERNAL-EMAIL-PROVIDER.md +13 -0
  63. package/examples/constellation/file/FILE-TICKETS-ROUTE.md +12 -0
  64. package/examples/constellation/flow/FLOW-CREATE-TICKET.md +16 -0
  65. package/examples/constellation/job/JOB-AUTO-ASSIGN.md +15 -0
  66. package/examples/constellation/page/PAGE-INBOX.md +19 -0
  67. package/examples/constellation/plan.md +23 -0
  68. package/examples/constellation/role/ROLE-SUPPORT-AGENT.md +12 -0
  69. package/examples/constellation/state/STATE-TICKET.md +31 -0
  70. package/examples/constellation/test/TEST-CREATE-TICKET.md +17 -0
  71. package/package.json +80 -0
  72. package/schemas/agent.json +21 -0
  73. package/schemas/api.json +47 -0
  74. package/schemas/card.json +37 -0
  75. package/schemas/component.json +40 -0
  76. package/schemas/datatype.json +8 -0
  77. package/schemas/db.json +51 -0
  78. package/schemas/diagram.json +95 -0
  79. package/schemas/doc.json +8 -0
  80. package/schemas/event.json +28 -0
  81. package/schemas/external.json +24 -0
  82. package/schemas/file.json +22 -0
  83. package/schemas/flow.json +20 -0
  84. package/schemas/job.json +25 -0
  85. package/schemas/page.json +36 -0
  86. package/schemas/plan.json +13 -0
  87. package/schemas/role.json +18 -0
  88. package/schemas/state.json +35 -0
  89. package/schemas/test.json +13 -0
  90. package/skill/SKILL.md +105 -0
  91. package/skill/types/agent.md +26 -0
  92. package/skill/types/api.md +38 -0
  93. package/skill/types/component.md +30 -0
  94. package/skill/types/datatype.md +26 -0
  95. package/skill/types/db.md +33 -0
  96. package/skill/types/diagram.md +32 -0
  97. package/skill/types/doc.md +23 -0
  98. package/skill/types/event.md +31 -0
  99. package/skill/types/external.md +30 -0
  100. package/skill/types/file.md +28 -0
  101. package/skill/types/flow.md +29 -0
  102. package/skill/types/job.md +27 -0
  103. package/skill/types/page.md +28 -0
  104. package/skill/types/plan.md +37 -0
  105. package/skill/types/role.md +25 -0
  106. package/skill/types/state.md +38 -0
  107. package/skill/types/test.md +28 -0
  108. package/viewer/dist/assets/arc-Kj6pF3JI.js +1 -0
  109. package/viewer/dist/assets/architecture-7EHR7CIX-CGfWeim3.js +1 -0
  110. package/viewer/dist/assets/architectureDiagram-3BPJPVTR-C5bZdErB.js +36 -0
  111. package/viewer/dist/assets/array-BifhSqXX.js +1 -0
  112. package/viewer/dist/assets/blockDiagram-GPEHLZMM-C1Q6l6fE.js +132 -0
  113. package/viewer/dist/assets/c4Diagram-AAUBKEIU-BmM6Tmtq.js +10 -0
  114. package/viewer/dist/assets/channel-19IdUS_c.js +1 -0
  115. package/viewer/dist/assets/chunk-2J33WTMH-z09tLTpZ.js +1 -0
  116. package/viewer/dist/assets/chunk-3OPIFGDE-BynpXh1r.js +62 -0
  117. package/viewer/dist/assets/chunk-4BX2VUAB-CDOVuPyG.js +1 -0
  118. package/viewer/dist/assets/chunk-55IACEB6-nBwigOgn.js +1 -0
  119. package/viewer/dist/assets/chunk-5ZQYHXKU-Bxe5xIy_.js +2 -0
  120. package/viewer/dist/assets/chunk-727SXJPM-DZmTgL68.js +206 -0
  121. package/viewer/dist/assets/chunk-AQP2D5EJ-B7wr_Owx.js +231 -0
  122. package/viewer/dist/assets/chunk-BSJP7CBP-DbAKfVCK.js +1 -0
  123. package/viewer/dist/assets/chunk-CSCIHK7Q-C0rsBwqP.js +124 -0
  124. package/viewer/dist/assets/chunk-FMBD7UC4-BAtzt0wv.js +15 -0
  125. package/viewer/dist/assets/chunk-KSCS5N6A-CXXwf52I.js +10 -0
  126. package/viewer/dist/assets/chunk-L5ZTLDWV-DS4vRI1U.js +1 -0
  127. package/viewer/dist/assets/chunk-LZXEDZCA-CclT9MXr.js +2 -0
  128. package/viewer/dist/assets/chunk-ND2GUHAM-CUSnPl8t.js +1 -0
  129. package/viewer/dist/assets/chunk-NNHCCRGN-DlpIbxXb.js +159 -0
  130. package/viewer/dist/assets/chunk-NZK2D7GU-Dh986nJk.js +1 -0
  131. package/viewer/dist/assets/chunk-O5CBEL6O-JAEZ_pS6.js +70 -0
  132. package/viewer/dist/assets/chunk-QZHKN3VN-BKc_Kg2Z.js +1 -0
  133. package/viewer/dist/assets/chunk-WU5MYG2G-9ssTSMzt.js +1 -0
  134. package/viewer/dist/assets/chunk-XPW4576I-BwMZI0gv.js +32 -0
  135. package/viewer/dist/assets/classDiagram-4FO5ZUOK-DXv85WFd.js +1 -0
  136. package/viewer/dist/assets/classDiagram-v2-Q7XG4LA2-DXv85WFd.js +1 -0
  137. package/viewer/dist/assets/cose-bilkent-S5V4N54A-NGC7gYHM.js +1 -0
  138. package/viewer/dist/assets/cytoscape.esm-h6BdjjI9.js +321 -0
  139. package/viewer/dist/assets/dagre-BM42HDAG-RD63uyvd.js +4 -0
  140. package/viewer/dist/assets/dagre-Bx709z4p.js +1 -0
  141. package/viewer/dist/assets/defaultLocale-C8Fc0cco.js +1 -0
  142. package/viewer/dist/assets/diagram-2AECGRRQ-hwnqqCcb.js +43 -0
  143. package/viewer/dist/assets/diagram-5GNKFQAL-q8EaoZSG.js +10 -0
  144. package/viewer/dist/assets/diagram-KO2AKTUF-D4_5Qf-l.js +3 -0
  145. package/viewer/dist/assets/diagram-LMA3HP47-D8pwekFs.js +24 -0
  146. package/viewer/dist/assets/diagram-OG6HWLK6-D9KinIWZ.js +24 -0
  147. package/viewer/dist/assets/dist-CFOOgrqc.js +1 -0
  148. package/viewer/dist/assets/erDiagram-TEJ5UH35-D0Wfq250.js +85 -0
  149. package/viewer/dist/assets/eventmodeling-FCH6USID-D3KRSuC1.js +1 -0
  150. package/viewer/dist/assets/flowDiagram-I6XJVG4X-Y2DY-Ze2.js +162 -0
  151. package/viewer/dist/assets/ganttDiagram-6RSMTGT7-BnqkeLVw.js +292 -0
  152. package/viewer/dist/assets/gitGraph-WXDBUCRP-Cft7usRT.js +1 -0
  153. package/viewer/dist/assets/gitGraphDiagram-PVQCEYII-D-cYtraK.js +106 -0
  154. package/viewer/dist/assets/graphlib-B8gBHxth.js +1 -0
  155. package/viewer/dist/assets/index-CDR-riG2.css +2 -0
  156. package/viewer/dist/assets/index-DRPsTWe2.js +98 -0
  157. package/viewer/dist/assets/info-J43DQDTF-Djc8Bx3F.js +1 -0
  158. package/viewer/dist/assets/infoDiagram-5YYISTIA-D-ehtyyJ.js +2 -0
  159. package/viewer/dist/assets/init-D6jRqBbL.js +1 -0
  160. package/viewer/dist/assets/ishikawaDiagram-YF4QCWOH-Ct3f6bH-.js +70 -0
  161. package/viewer/dist/assets/journeyDiagram-JHISSGLW-DXlULEmi.js +139 -0
  162. package/viewer/dist/assets/kanban-definition-UN3LZRKU-3vE9h-R7.js +89 -0
  163. package/viewer/dist/assets/katex-Vhh-h91d.js +257 -0
  164. package/viewer/dist/assets/line-B8MygbLB.js +1 -0
  165. package/viewer/dist/assets/linear-CfMuM0B3.js +1 -0
  166. package/viewer/dist/assets/mermaid-parser.core-DzlZTbbh.js +4 -0
  167. package/viewer/dist/assets/mermaid.core-IM-sPiyq.js +9 -0
  168. package/viewer/dist/assets/mindmap-definition-RKZ34NQL-CMnpAq1T.js +96 -0
  169. package/viewer/dist/assets/ordinal-hYBb2elL.js +1 -0
  170. package/viewer/dist/assets/packet-YPE3B663-D44AzgHh.js +1 -0
  171. package/viewer/dist/assets/path-BWPyau1x.js +1 -0
  172. package/viewer/dist/assets/pie-LRSECV5Y-DL8AVJH_.js +1 -0
  173. package/viewer/dist/assets/pieDiagram-4H26LBE5-FvKK5jd7.js +30 -0
  174. package/viewer/dist/assets/quadrantDiagram-W4KKPZXB-CmjSkU8c.js +7 -0
  175. package/viewer/dist/assets/radar-GUYGQ44K-BvfZTVyH.js +1 -0
  176. package/viewer/dist/assets/requirementDiagram-4Y6WPE33-BOjca3VH.js +84 -0
  177. package/viewer/dist/assets/rough.esm-CSKSodPl.js +1 -0
  178. package/viewer/dist/assets/sankeyDiagram-5OEKKPKP-ANcjfNix.js +40 -0
  179. package/viewer/dist/assets/sequenceDiagram-3UESZ5HK-BLQ9AL7I.js +162 -0
  180. package/viewer/dist/assets/src-CAMdANUp.js +1 -0
  181. package/viewer/dist/assets/stateDiagram-AJRCARHV-D6CriBS6.js +1 -0
  182. package/viewer/dist/assets/stateDiagram-v2-BHNVJYJU-DcTp66RQ.js +1 -0
  183. package/viewer/dist/assets/timeline-definition-PNZ67QCA-BNhWZ_DL.js +120 -0
  184. package/viewer/dist/assets/treeView-BLDUP644-CY6Ph5Pu.js +1 -0
  185. package/viewer/dist/assets/treemap-LRROVOQU-DChSA_Qx.js +1 -0
  186. package/viewer/dist/assets/vennDiagram-CIIHVFJN-C01WznAC.js +34 -0
  187. package/viewer/dist/assets/wardley-L42UT6IY-BJ8uNoJu.js +1 -0
  188. package/viewer/dist/assets/wardleyDiagram-YWT4CUSO-DwDEzlVm.js +78 -0
  189. package/viewer/dist/assets/xychartDiagram-2RQKCTM6-BCvIDwU0.js +7 -0
  190. package/viewer/dist/index.html +20 -0
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # Constellation
2
+
3
+ Your project's architecture plan as markdown files in the repo: typed cards,
4
+ connected into a graph, validated by lint, diffed by git, readable by humans on
5
+ GitHub and by AI agents with nothing more than file access.
6
+
7
+ ```
8
+ constellation/
9
+ plan.md ← the living project plan (PLAN-PROJECT)
10
+ api/API-TICKETS.md ← one file per card; the filename IS the handle
11
+ datatype/DATATYPE-TICKET.md
12
+ db/DB-TICKETS.md
13
+ flow/FLOW-CREATE-TICKET.md
14
+ ...
15
+ ```
16
+
17
+ Each card is frontmatter (structure) + markdown (narrative). Connections are a
18
+ plain list of handles — plus `[[HANDLE]]` links in prose, handle-shaped values in
19
+ frontmatter fields, and handles used as Mermaid node IDs. The indexer derives the
20
+ graph; nothing derived is ever stored.
21
+
22
+ **Why files?** Plans drift from code when they live somewhere else. Here a plan
23
+ change is a commit: it rides the same branch and PR as the code it describes,
24
+ merges per-card, and "what changed in the plan" is `git diff -- constellation/`.
25
+
26
+ ## Install
27
+
28
+ ```sh
29
+ npm install -g @magic-spells/constellation # the `constellation` binary
30
+ # or run without installing:
31
+ npx @magic-spells/constellation lint
32
+ ```
33
+
34
+ Requires Node ≥ 22.
35
+
36
+ ## Usage
37
+
38
+ ```sh
39
+ constellation init # scaffold constellation/ with a starter plan.md
40
+ constellation lint # validate handles, references, folders, schemas
41
+ constellation mcp # run the MCP server (stdio) for AI agents
42
+ constellation serve # open the local viewer (editable; --readonly to disable)
43
+ ```
44
+
45
+ Lint errors (broken graph: bad handles, dangling structured references,
46
+ duplicates) exit non-zero for CI; warnings (wrong folder, schema violations,
47
+ unknown fields, dangling prose links) don't block.
48
+
49
+ ## Repo layout
50
+
51
+ | Path | What |
52
+ |---|---|
53
+ | `docs/001-file-format.md` | The normative format spec |
54
+ | `docs/002-mcp.md` | MCP design: tool surface, hydration, write semantics, git layer |
55
+ | `schemas/` | JSON Schemas: `card.json` (reserved keys) + one per type |
56
+ | `skill/` | AI authoring skill: `SKILL.md` + per-type references with golden examples |
57
+ | `src/core/` | Parser, reference extraction, indexer, schema validation, lint |
58
+ | `src/cli/` | The `constellation` binary (`lint`, `init`, `mcp`) |
59
+ | `src/mcp/` | MCP server: hydrated retrieval, validated writes, git tools |
60
+ | `examples/constellation/` | Golden sample plan — one card of every type, lints clean, doubles as the test fixture |
61
+
62
+ ## Development
63
+
64
+ ```sh
65
+ npm install
66
+ npm test # vitest
67
+ npm run lint:examples # lint the golden plan
68
+ npm run build # tsc → dist/
69
+ ```
70
+
71
+ ## MCP
72
+
73
+ `constellation mcp` exposes the plan to AI agents over stdio:
74
+
75
+ - **Hydrated retrieval**: `get_card`, `search`, and `traverse` can return
76
+ connected cards with their complete frontmatter and body in one call.
77
+ - **Validated writes**: `create_card`, `update_card`, `delete_card`,
78
+ `add_connection`, `remove_connection` — every write lints and returns issues.
79
+ Body-only updates never reformat frontmatter.
80
+ - **Git-powered change tracking**: `diff_plan` (per-card changes since the sync
81
+ marker), `plan_log`, `set_sync_point`, `check_integrity`.
82
+ - **Visual viewer**: `start_viewer` / `stop_viewer` open and close the local web
83
+ viewer from inside an agent session, returning a clickable URL.
84
+
85
+ ### Add to Claude Code
86
+
87
+ Run from your repo root, so the server starts there and finds the plan:
88
+
89
+ ```sh
90
+ # Run straight from npm — no install needed:
91
+ claude mcp add constellation -- npx -y @magic-spells/constellation mcp
92
+
93
+ # Or, if installed globally (npm i -g @magic-spells/constellation):
94
+ claude mcp add constellation -- constellation mcp
95
+
96
+ # Share with everyone who clones the repo (writes .mcp.json):
97
+ claude mcp add --scope project constellation -- npx -y @magic-spells/constellation mcp
98
+ ```
99
+
100
+ Manage it with `claude mcp list`, `claude mcp get constellation`, and
101
+ `claude mcp remove constellation`.
102
+
103
+ ### Other MCP clients
104
+
105
+ Hand-edit the client's config (Claude Desktop, a project `.mcp.json`, etc.):
106
+
107
+ ```json
108
+ {
109
+ "mcpServers": {
110
+ "constellation": {
111
+ "command": "constellation",
112
+ "args": ["mcp"],
113
+ "cwd": "/path/to/your/repo"
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ Set `cwd` to your repo root (or any folder inside it). The server finds the plan by
120
+ walking up from its working directory; without `cwd` it inherits the client's, which
121
+ may not be your project — in which case tools return `NO_PLAN_FOUND`.
122
+
123
+ ## Viewer
124
+
125
+ `constellation serve` renders the plan as a local website, **editable in place**
126
+ (pass `--readonly` to disable writes). Five themes toggle in the header:
127
+ **observatory** (dark, star-field), **claw** (cream paper, serif, coral accents),
128
+ **black**, **synthwave**, and **sumi**. Card pages show structured fields, the
129
+ markdown body, connection chips in both directions, and a small constellation
130
+ diagram of the card's neighborhood — its nodes tinted by card type. Mermaid blocks
131
+ render in-browser, `[[HANDLE]]` links navigate, and the page live-reloads when plan
132
+ files change on disk.
133
+
134
+ ```sh
135
+ constellation serve # http://localhost:4747 (assets ship prebuilt with the package)
136
+ npm run build:viewer # only when developing from source
137
+ ```
138
+
139
+ In an agent session you don't need the CLI — ask Claude to open the viewer and it
140
+ calls the `start_viewer` MCP tool, which returns the URL (`stop_viewer` closes it).
141
+
142
+ ---
143
+
144
+ <p align="center">
145
+ Made by <a href="https://github.com/coryschulz">Cory Schulz</a>
146
+ </p>
147
+
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+ import { Command } from 'commander';
5
+ import pc from 'picocolors';
6
+ import { lintPlan } from '../core/lint.js';
7
+ import { resolvePlanDir } from '../core/resolve.js';
8
+ const program = new Command();
9
+ program
10
+ .name('constellation')
11
+ .description('Files-first architecture planning for AI-assisted development');
12
+ program
13
+ .command('lint')
14
+ .argument('[path]', 'plan folder, or a directory containing constellation/ (default: walk up from cwd)')
15
+ .description('Validate the plan: handles, references, folders, schemas')
16
+ .action(async (target) => {
17
+ const root = await resolvePlanDir(target ?? undefined);
18
+ if (!root) {
19
+ console.error(pc.red('No constellation/ folder found.') +
20
+ ' Run `constellation init` to create one.');
21
+ process.exit(2);
22
+ }
23
+ const result = await lintPlan(root);
24
+ const byFile = new Map();
25
+ for (const issue of result.issues) {
26
+ if (!byFile.has(issue.file))
27
+ byFile.set(issue.file, []);
28
+ byFile.get(issue.file).push(issue);
29
+ }
30
+ for (const [file, issues] of byFile) {
31
+ console.log(pc.underline(file));
32
+ for (const issue of issues) {
33
+ const tag = issue.severity === 'error'
34
+ ? pc.red(`error ${issue.code}`)
35
+ : pc.yellow(`warn ${issue.code}`);
36
+ console.log(` ${tag} ${issue.message}`);
37
+ }
38
+ }
39
+ if (byFile.size > 0)
40
+ console.log();
41
+ const summary = [
42
+ `${result.index.cards.size} cards`,
43
+ `${result.index.connections.length} connections`,
44
+ result.errors.length > 0
45
+ ? pc.red(`${result.errors.length} errors`)
46
+ : pc.green('0 errors'),
47
+ result.warnings.length > 0
48
+ ? pc.yellow(`${result.warnings.length} warnings`)
49
+ : '0 warnings',
50
+ ].join(', ');
51
+ console.log(`${result.errors.length > 0 ? pc.red('✗') : pc.green('✓')} ${summary}`);
52
+ process.exit(result.errors.length > 0 ? 1 : 0);
53
+ });
54
+ program
55
+ .command('init')
56
+ .argument('[path]', 'directory to create the plan in (default: cwd)', '.')
57
+ .description('Scaffold a constellation/ folder with a starter plan.md')
58
+ .action(async (target) => {
59
+ const { initPlan } = await import('../core/scaffold.js');
60
+ try {
61
+ const root = await initPlan(target);
62
+ console.log(pc.green('✓') + ` Created ${path.relative(process.cwd(), root)}/plan.md`);
63
+ console.log('\nAdd cards as <type>/<HANDLE>.md (e.g. api/API-LIST-USERS.md),\nthen run `constellation lint` to validate.');
64
+ }
65
+ catch (err) {
66
+ console.error(pc.red(err instanceof Error ? err.message : String(err)));
67
+ process.exit(2);
68
+ }
69
+ });
70
+ program
71
+ .command('mcp')
72
+ .description('Run the Constellation MCP server over stdio')
73
+ .action(async () => {
74
+ const { startMcpServer } = await import('../mcp/server.js');
75
+ await startMcpServer();
76
+ });
77
+ program
78
+ .command('serve')
79
+ .argument('[path]', 'plan folder or a directory containing constellation/')
80
+ .option('-p, --port <port>', 'port to listen on', '4747')
81
+ .option('--no-open', 'do not open the browser')
82
+ .option('--readonly', 'disable editing from the browser')
83
+ .description('Serve a website rendering the plan, editable in place')
84
+ .action(async (target, opts) => {
85
+ const root = await resolvePlanDir(target ?? undefined);
86
+ if (!root) {
87
+ console.error(pc.red('No constellation/ folder found.'));
88
+ process.exit(2);
89
+ }
90
+ const { startServer } = await import('../serve/server.js');
91
+ let running;
92
+ try {
93
+ running = await startServer({
94
+ planRoot: root,
95
+ port: Number(opts.port),
96
+ readonly: opts.readonly ?? false,
97
+ });
98
+ }
99
+ catch (err) {
100
+ const code = err?.code;
101
+ if (code === 'EADDRINUSE') {
102
+ console.error(pc.red(`Port ${opts.port} is already in use.`) +
103
+ ` Pick another with: constellation serve -p <port>`);
104
+ }
105
+ else {
106
+ console.error(pc.red(err instanceof Error ? err.message : String(err)));
107
+ }
108
+ process.exit(2);
109
+ }
110
+ const url = `http://localhost:${running.port}`;
111
+ console.log(`${pc.green('✓')} Constellation viewer at ${pc.underline(url)}`);
112
+ console.log(pc.dim(` plan: ${root}`));
113
+ if (opts.open) {
114
+ const { spawn } = await import('node:child_process');
115
+ const cmd = process.platform === 'darwin'
116
+ ? 'open'
117
+ : process.platform === 'win32'
118
+ ? 'start'
119
+ : 'xdg-open';
120
+ spawn(cmd, [url], { stdio: 'ignore', detached: true }).unref();
121
+ }
122
+ });
123
+ program.parseAsync(process.argv).catch((err) => {
124
+ console.error(pc.red(err instanceof Error ? err.message : String(err)));
125
+ process.exit(1);
126
+ });
127
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /** [[HANDLE]] wiki-links. Non-handle-shaped link targets are ignored. */
2
+ export declare function extractWikiLinks(body: string): string[];
3
+ /** Handle-shaped identifiers inside ```mermaid fences. */
4
+ export declare function extractMermaidRefs(body: string): string[];
5
+ /**
6
+ * Handle-shaped string values anywhere in frontmatter, excluding the
7
+ * `connections` key (collected separately) and the card's own handle.
8
+ */
9
+ export declare function extractFrontmatterRefs(frontmatter: Record<string, unknown>, ownHandle: string): string[];
@@ -0,0 +1,56 @@
1
+ import { isKnownHandle } from './handles.js';
2
+ const WIKI_LINK = /\[\[([^\[\]]+)\]\]/g;
3
+ const MERMAID_BLOCK = /```mermaid[^\n]*\n([\s\S]*?)```/g;
4
+ const HANDLE_TOKEN = /(?<![A-Za-z0-9-])[A-Z][A-Z0-9]*-[A-Z0-9][A-Z0-9-]*(?![A-Za-z0-9-])/g;
5
+ /** [[HANDLE]] wiki-links. Non-handle-shaped link targets are ignored. */
6
+ export function extractWikiLinks(body) {
7
+ const out = [];
8
+ for (const match of body.matchAll(WIKI_LINK)) {
9
+ const target = match[1].trim();
10
+ if (isKnownHandle(target))
11
+ out.push(target);
12
+ }
13
+ return dedupe(out);
14
+ }
15
+ /** Handle-shaped identifiers inside ```mermaid fences. */
16
+ export function extractMermaidRefs(body) {
17
+ const out = [];
18
+ for (const block of body.matchAll(MERMAID_BLOCK)) {
19
+ for (const token of block[1].matchAll(HANDLE_TOKEN)) {
20
+ if (isKnownHandle(token[0]))
21
+ out.push(token[0]);
22
+ }
23
+ }
24
+ return dedupe(out);
25
+ }
26
+ /**
27
+ * Handle-shaped string values anywhere in frontmatter, excluding the
28
+ * `connections` key (collected separately) and the card's own handle.
29
+ */
30
+ export function extractFrontmatterRefs(frontmatter, ownHandle) {
31
+ const out = [];
32
+ for (const [key, value] of Object.entries(frontmatter)) {
33
+ if (key === 'connections')
34
+ continue;
35
+ walk(value, out);
36
+ }
37
+ return dedupe(out.filter((h) => h !== ownHandle));
38
+ }
39
+ function walk(value, out) {
40
+ if (typeof value === 'string') {
41
+ if (isKnownHandle(value))
42
+ out.push(value);
43
+ }
44
+ else if (Array.isArray(value)) {
45
+ for (const item of value)
46
+ walk(item, out);
47
+ }
48
+ else if (value && typeof value === 'object') {
49
+ for (const item of Object.values(value))
50
+ walk(item, out);
51
+ }
52
+ }
53
+ function dedupe(items) {
54
+ return [...new Set(items)];
55
+ }
56
+ //# sourceMappingURL=extract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.js","sourceRoot":"","sources":["../../src/core/extract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,MAAM,SAAS,GAAG,qBAAqB,CAAC;AACxC,MAAM,aAAa,GAAG,kCAAkC,CAAC;AACzD,MAAM,YAAY,GAAG,qEAAqE,CAAC;AAE3F,yEAAyE;AACzE,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/B,IAAI,aAAa,CAAC,MAAM,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;AACrB,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;QACjD,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;YACpD,IAAI,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAClD,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;AACrB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CACpC,WAAoC,EACpC,SAAiB;IAEjB,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QACvD,IAAI,GAAG,KAAK,aAAa;YAAE,SAAS;QACpC,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACnB,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC;AACpD,CAAC;AAED,SAAS,IAAI,CAAC,KAAc,EAAE,GAAa;IACzC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,aAAa,CAAC,KAAK,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC5C,CAAC;SAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAChC,KAAK,MAAM,IAAI,IAAI,KAAK;YAAE,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC5C,CAAC;SAAM,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9C,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC;YAAE,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC;AAED,SAAS,MAAM,CAAC,KAAe;IAC7B,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;AAC7B,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { type TypeName } from './types.js';
2
+ /** Handle grammar: PREFIX-NAME, uppercase letters/digits/dashes. */
3
+ export declare const HANDLE_PATTERN: RegExp;
4
+ export declare const HANDLE_MIN_LENGTH = 3;
5
+ export declare const HANDLE_MAX_LENGTH = 135;
6
+ /** Folder each type's cards live in, relative to the plan root. */
7
+ export declare const TYPE_FOLDERS: Record<TypeName, string>;
8
+ /** True when the string matches the handle grammar (prefix may be unknown). */
9
+ export declare function isHandleShaped(value: string): boolean;
10
+ /** Type for a handle's prefix, or null when the prefix is not one of the 17. */
11
+ export declare function typeForHandle(handle: string): TypeName | null;
12
+ /** Shaped AND carries a known prefix — the test used for reference extraction. */
13
+ export declare function isKnownHandle(value: string): boolean;
@@ -0,0 +1,45 @@
1
+ import { TYPE_NAMES } from './types.js';
2
+ /** Handle grammar: PREFIX-NAME, uppercase letters/digits/dashes. */
3
+ export const HANDLE_PATTERN = /^[A-Z][A-Z0-9]*-[A-Z0-9][A-Z0-9-]*$/;
4
+ export const HANDLE_MIN_LENGTH = 3;
5
+ export const HANDLE_MAX_LENGTH = 135;
6
+ /** Folder each type's cards live in, relative to the plan root. */
7
+ export const TYPE_FOLDERS = {
8
+ API: 'api',
9
+ DB: 'db',
10
+ DATATYPE: 'datatype',
11
+ ROLE: 'role',
12
+ DOC: 'doc',
13
+ FILE: 'file',
14
+ TEST: 'test',
15
+ EXTERNAL: 'external',
16
+ EVENT: 'event',
17
+ COMPONENT: 'component',
18
+ PAGE: 'page',
19
+ JOB: 'job',
20
+ FLOW: 'flow',
21
+ STATE: 'state',
22
+ DIAGRAM: 'diagram',
23
+ AGENT: 'agent',
24
+ PLAN: 'plan',
25
+ };
26
+ const TYPE_SET = new Set(TYPE_NAMES);
27
+ /** True when the string matches the handle grammar (prefix may be unknown). */
28
+ export function isHandleShaped(value) {
29
+ return (value.length >= HANDLE_MIN_LENGTH &&
30
+ value.length <= HANDLE_MAX_LENGTH &&
31
+ HANDLE_PATTERN.test(value));
32
+ }
33
+ /** Type for a handle's prefix, or null when the prefix is not one of the 17. */
34
+ export function typeForHandle(handle) {
35
+ const dash = handle.indexOf('-');
36
+ if (dash <= 0)
37
+ return null;
38
+ const prefix = handle.slice(0, dash);
39
+ return TYPE_SET.has(prefix) ? prefix : null;
40
+ }
41
+ /** Shaped AND carries a known prefix — the test used for reference extraction. */
42
+ export function isKnownHandle(value) {
43
+ return isHandleShaped(value) && typeForHandle(value) !== null;
44
+ }
45
+ //# sourceMappingURL=handles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handles.js","sourceRoot":"","sources":["../../src/core/handles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAiB,MAAM,YAAY,CAAC;AAEvD,oEAAoE;AACpE,MAAM,CAAC,MAAM,cAAc,GAAG,qCAAqC,CAAC;AAEpE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AACnC,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAErC,mEAAmE;AACnE,MAAM,CAAC,MAAM,YAAY,GAA6B;IACpD,GAAG,EAAE,KAAK;IACV,EAAE,EAAE,IAAI;IACR,QAAQ,EAAE,UAAU;IACpB,IAAI,EAAE,MAAM;IACZ,GAAG,EAAE,KAAK;IACV,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,MAAM;IACZ,QAAQ,EAAE,UAAU;IACpB,KAAK,EAAE,OAAO;IACd,SAAS,EAAE,WAAW;IACtB,IAAI,EAAE,MAAM;IACZ,GAAG,EAAE,KAAK;IACV,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,SAAS;IAClB,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,MAAM;CACb,CAAC;AAEF,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAS,UAAU,CAAC,CAAC;AAE7C,+EAA+E;AAC/E,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,CACL,KAAK,CAAC,MAAM,IAAI,iBAAiB;QACjC,KAAK,CAAC,MAAM,IAAI,iBAAiB;QACjC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAC3B,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,IAAI,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3B,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IACrC,OAAO,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAE,MAAmB,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5D,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,OAAO,cAAc,CAAC,KAAK,CAAC,IAAI,aAAa,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC;AAChE,CAAC"}
@@ -0,0 +1,8 @@
1
+ export * from './types.js';
2
+ export * from './handles.js';
3
+ export * from './parse.js';
4
+ export * from './extract.js';
5
+ export * from './indexer.js';
6
+ export * from './validate.js';
7
+ export * from './lint.js';
8
+ export * from './writer.js';
@@ -0,0 +1,9 @@
1
+ export * from './types.js';
2
+ export * from './handles.js';
3
+ export * from './parse.js';
4
+ export * from './extract.js';
5
+ export * from './indexer.js';
6
+ export * from './validate.js';
7
+ export * from './lint.js';
8
+ export * from './writer.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { PlanIndex } from './types.js';
2
+ /** Load a plan folder into an index: cards, edges, and structural issues. */
3
+ export declare function loadPlan(root: string): Promise<PlanIndex>;
@@ -0,0 +1,209 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { isHandleShaped, isKnownHandle, typeForHandle, TYPE_FOLDERS, } from './handles.js';
4
+ import { parseFile } from './parse.js';
5
+ import { extractFrontmatterRefs, extractMermaidRefs, extractWikiLinks, } from './extract.js';
6
+ /** Load a plan folder into an index: cards, edges, and structural issues. */
7
+ export async function loadPlan(root) {
8
+ const absRoot = path.resolve(root);
9
+ const issues = [];
10
+ const cards = new Map();
11
+ for (const relPath of await listMarkdownFiles(absRoot)) {
12
+ const filePath = path.join(absRoot, relPath);
13
+ const raw = await readFile(filePath, 'utf8');
14
+ const card = readCard(relPath, filePath, raw, issues);
15
+ if (!card)
16
+ continue;
17
+ const existing = cards.get(card.handle);
18
+ if (existing) {
19
+ issues.push({
20
+ severity: 'error',
21
+ code: 'E003',
22
+ message: `Duplicate handle ${card.handle} (also defined in ${existing.relPath})`,
23
+ file: relPath,
24
+ });
25
+ continue;
26
+ }
27
+ cards.set(card.handle, card);
28
+ }
29
+ resolveRefs(cards, issues);
30
+ const connections = buildConnections(cards);
31
+ const connectedHandles = new Map();
32
+ for (const conn of connections) {
33
+ if (!connectedHandles.has(conn.a))
34
+ connectedHandles.set(conn.a, new Set());
35
+ if (!connectedHandles.has(conn.b))
36
+ connectedHandles.set(conn.b, new Set());
37
+ connectedHandles.get(conn.a).add(conn.b);
38
+ connectedHandles.get(conn.b).add(conn.a);
39
+ }
40
+ return { root: absRoot, cards, connections, connectedHandles, issues };
41
+ }
42
+ function readCard(relPath, filePath, raw, issues) {
43
+ // The one special file: plan.md at the plan root is PLAN-PROJECT.
44
+ const isRootPlan = relPath === 'plan.md';
45
+ const handle = isRootPlan ? 'PLAN-PROJECT' : path.basename(relPath, '.md');
46
+ if (!isHandleShaped(handle)) {
47
+ issues.push({
48
+ severity: 'error',
49
+ code: 'E001',
50
+ message: `Filename is not a valid handle: ${handle}`,
51
+ file: relPath,
52
+ });
53
+ return null;
54
+ }
55
+ const type = typeForHandle(handle);
56
+ if (!type) {
57
+ issues.push({
58
+ severity: 'error',
59
+ code: 'E002',
60
+ message: `Unknown handle prefix: ${handle.split('-')[0]}- (expected one of the 17 canonical prefixes)`,
61
+ file: relPath,
62
+ });
63
+ return null;
64
+ }
65
+ const parsed = parseFile(raw);
66
+ if (parsed.yamlError) {
67
+ issues.push({
68
+ severity: 'error',
69
+ code: 'E006',
70
+ message: `Invalid YAML frontmatter: ${parsed.yamlError}`,
71
+ file: relPath,
72
+ });
73
+ }
74
+ if (!isRootPlan) {
75
+ const folder = relPath.includes(path.sep) ? relPath.split(path.sep)[0] : '';
76
+ const expected = TYPE_FOLDERS[type];
77
+ if (folder !== expected) {
78
+ issues.push({
79
+ severity: 'warning',
80
+ code: 'W001',
81
+ message: `${handle} is a ${type} card and belongs in ${expected}/`,
82
+ file: relPath,
83
+ });
84
+ }
85
+ }
86
+ const fm = parsed.frontmatter;
87
+ const connections = [];
88
+ if (fm.connections !== undefined) {
89
+ const list = Array.isArray(fm.connections) ? fm.connections : null;
90
+ if (list === null) {
91
+ issues.push({
92
+ severity: 'error',
93
+ code: 'E004',
94
+ message: `connections must be a list of handles`,
95
+ file: relPath,
96
+ });
97
+ }
98
+ else {
99
+ for (const entry of list) {
100
+ if (typeof entry === 'string' && isKnownHandle(entry)) {
101
+ if (entry !== handle)
102
+ connections.push(entry);
103
+ }
104
+ else {
105
+ issues.push({
106
+ severity: 'error',
107
+ code: 'E004',
108
+ message: `connections entry is not a valid handle: ${JSON.stringify(entry)}`,
109
+ file: relPath,
110
+ });
111
+ }
112
+ }
113
+ }
114
+ }
115
+ return {
116
+ handle,
117
+ type,
118
+ relPath,
119
+ filePath,
120
+ frontmatter: fm,
121
+ body: parsed.body,
122
+ name: typeof fm.name === 'string' ? fm.name : undefined,
123
+ kind: typeof fm.kind === 'string' ? fm.kind : undefined,
124
+ status: typeof fm.status === 'string' ? fm.status : undefined,
125
+ refs: {
126
+ connections: [...new Set(connections)],
127
+ frontmatter: extractFrontmatterRefs(fm, handle),
128
+ body: extractWikiLinks(parsed.body),
129
+ mermaid: extractMermaidRefs(parsed.body),
130
+ },
131
+ };
132
+ }
133
+ function resolveRefs(cards, issues) {
134
+ for (const card of cards.values()) {
135
+ // Structured references are contracts: missing targets are errors.
136
+ for (const target of [...card.refs.connections, ...card.refs.frontmatter]) {
137
+ if (!cards.has(target)) {
138
+ issues.push({
139
+ severity: 'error',
140
+ code: 'E005',
141
+ message: `Reference to ${target} does not resolve to a card`,
142
+ file: card.relPath,
143
+ });
144
+ }
145
+ }
146
+ // Prose references may point at cards not yet written: warnings.
147
+ for (const target of [...card.refs.body, ...card.refs.mermaid]) {
148
+ if (target !== card.handle && !cards.has(target)) {
149
+ issues.push({
150
+ severity: 'warning',
151
+ code: 'W004',
152
+ message: `Body reference [[${target}]] does not resolve to a card`,
153
+ file: card.relPath,
154
+ });
155
+ }
156
+ }
157
+ }
158
+ }
159
+ function buildConnections(cards) {
160
+ const seen = new Set();
161
+ const connections = [];
162
+ for (const card of cards.values()) {
163
+ const targets = [
164
+ ...card.refs.connections,
165
+ ...card.refs.frontmatter,
166
+ ...card.refs.body,
167
+ ...card.refs.mermaid,
168
+ ];
169
+ for (const target of targets) {
170
+ if (target === card.handle || !cards.has(target))
171
+ continue;
172
+ const [a, b] = card.handle < target ? [card.handle, target] : [target, card.handle];
173
+ const key = `${a}\u0000${b}`;
174
+ if (seen.has(key))
175
+ continue;
176
+ seen.add(key);
177
+ connections.push({ a, b });
178
+ }
179
+ }
180
+ return connections;
181
+ }
182
+ async function listMarkdownFiles(root) {
183
+ const out = [];
184
+ await walk(root, '');
185
+ return out.sort();
186
+ async function walk(dir, rel) {
187
+ let entries;
188
+ try {
189
+ entries = await readdir(dir, { withFileTypes: true });
190
+ }
191
+ catch (err) {
192
+ if (rel === '')
193
+ throw err;
194
+ return;
195
+ }
196
+ for (const entry of entries) {
197
+ if (entry.name.startsWith('.'))
198
+ continue;
199
+ const relPath = rel ? path.join(rel, entry.name) : entry.name;
200
+ if (entry.isDirectory()) {
201
+ await walk(path.join(dir, entry.name), relPath);
202
+ }
203
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
204
+ out.push(relPath);
205
+ }
206
+ }
207
+ }
208
+ }
209
+ //# sourceMappingURL=indexer.js.map