@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.
- package/LICENSE +674 -0
- package/README.md +147 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +127 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/extract.d.ts +9 -0
- package/dist/core/extract.js +56 -0
- package/dist/core/extract.js.map +1 -0
- package/dist/core/handles.d.ts +13 -0
- package/dist/core/handles.js +45 -0
- package/dist/core/handles.js.map +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.js +9 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/indexer.d.ts +3 -0
- package/dist/core/indexer.js +209 -0
- package/dist/core/indexer.js.map +1 -0
- package/dist/core/lint.d.ts +9 -0
- package/dist/core/lint.js +19 -0
- package/dist/core/lint.js.map +1 -0
- package/dist/core/parse.d.ts +7 -0
- package/dist/core/parse.js +19 -0
- package/dist/core/parse.js.map +1 -0
- package/dist/core/resolve.d.ts +12 -0
- package/dist/core/resolve.js +57 -0
- package/dist/core/resolve.js.map +1 -0
- package/dist/core/scaffold.d.ts +3 -0
- package/dist/core/scaffold.js +36 -0
- package/dist/core/scaffold.js.map +1 -0
- package/dist/core/types.d.ts +50 -0
- package/dist/core/types.js +5 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/validate.d.ts +6 -0
- package/dist/core/validate.js +64 -0
- package/dist/core/validate.js.map +1 -0
- package/dist/core/writer.d.ts +34 -0
- package/dist/core/writer.js +170 -0
- package/dist/core/writer.js.map +1 -0
- package/dist/mcp/git.d.ts +34 -0
- package/dist/mcp/git.js +172 -0
- package/dist/mcp/git.js.map +1 -0
- package/dist/mcp/search.d.ts +11 -0
- package/dist/mcp/search.js +52 -0
- package/dist/mcp/search.js.map +1 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +684 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/serve/server.d.ts +12 -0
- package/dist/serve/server.js +281 -0
- package/dist/serve/server.js.map +1 -0
- package/docs/001-file-format.md +222 -0
- package/docs/002-mcp.md +109 -0
- package/examples/constellation/agent/AGENT-CODE-STYLE.md +14 -0
- package/examples/constellation/api/API-TICKETS.md +24 -0
- package/examples/constellation/component/COMPONENT-TICKET-CARD.md +16 -0
- package/examples/constellation/datatype/DATATYPE-CREATE-TICKET-INPUT.md +15 -0
- package/examples/constellation/datatype/DATATYPE-TICKET.md +20 -0
- package/examples/constellation/db/DB-TICKETS.md +21 -0
- package/examples/constellation/diagram/DIAGRAM-SYSTEM-OVERVIEW.md +19 -0
- package/examples/constellation/doc/DOC-TICKET-LIFECYCLE.md +22 -0
- package/examples/constellation/event/EVENT-TICKET-CREATED.md +13 -0
- package/examples/constellation/external/EXTERNAL-EMAIL-PROVIDER.md +13 -0
- package/examples/constellation/file/FILE-TICKETS-ROUTE.md +12 -0
- package/examples/constellation/flow/FLOW-CREATE-TICKET.md +16 -0
- package/examples/constellation/job/JOB-AUTO-ASSIGN.md +15 -0
- package/examples/constellation/page/PAGE-INBOX.md +19 -0
- package/examples/constellation/plan.md +23 -0
- package/examples/constellation/role/ROLE-SUPPORT-AGENT.md +12 -0
- package/examples/constellation/state/STATE-TICKET.md +31 -0
- package/examples/constellation/test/TEST-CREATE-TICKET.md +17 -0
- package/package.json +80 -0
- package/schemas/agent.json +21 -0
- package/schemas/api.json +47 -0
- package/schemas/card.json +37 -0
- package/schemas/component.json +40 -0
- package/schemas/datatype.json +8 -0
- package/schemas/db.json +51 -0
- package/schemas/diagram.json +95 -0
- package/schemas/doc.json +8 -0
- package/schemas/event.json +28 -0
- package/schemas/external.json +24 -0
- package/schemas/file.json +22 -0
- package/schemas/flow.json +20 -0
- package/schemas/job.json +25 -0
- package/schemas/page.json +36 -0
- package/schemas/plan.json +13 -0
- package/schemas/role.json +18 -0
- package/schemas/state.json +35 -0
- package/schemas/test.json +13 -0
- package/skill/SKILL.md +105 -0
- package/skill/types/agent.md +26 -0
- package/skill/types/api.md +38 -0
- package/skill/types/component.md +30 -0
- package/skill/types/datatype.md +26 -0
- package/skill/types/db.md +33 -0
- package/skill/types/diagram.md +32 -0
- package/skill/types/doc.md +23 -0
- package/skill/types/event.md +31 -0
- package/skill/types/external.md +30 -0
- package/skill/types/file.md +28 -0
- package/skill/types/flow.md +29 -0
- package/skill/types/job.md +27 -0
- package/skill/types/page.md +28 -0
- package/skill/types/plan.md +37 -0
- package/skill/types/role.md +25 -0
- package/skill/types/state.md +38 -0
- package/skill/types/test.md +28 -0
- package/viewer/dist/assets/arc-Kj6pF3JI.js +1 -0
- package/viewer/dist/assets/architecture-7EHR7CIX-CGfWeim3.js +1 -0
- package/viewer/dist/assets/architectureDiagram-3BPJPVTR-C5bZdErB.js +36 -0
- package/viewer/dist/assets/array-BifhSqXX.js +1 -0
- package/viewer/dist/assets/blockDiagram-GPEHLZMM-C1Q6l6fE.js +132 -0
- package/viewer/dist/assets/c4Diagram-AAUBKEIU-BmM6Tmtq.js +10 -0
- package/viewer/dist/assets/channel-19IdUS_c.js +1 -0
- package/viewer/dist/assets/chunk-2J33WTMH-z09tLTpZ.js +1 -0
- package/viewer/dist/assets/chunk-3OPIFGDE-BynpXh1r.js +62 -0
- package/viewer/dist/assets/chunk-4BX2VUAB-CDOVuPyG.js +1 -0
- package/viewer/dist/assets/chunk-55IACEB6-nBwigOgn.js +1 -0
- package/viewer/dist/assets/chunk-5ZQYHXKU-Bxe5xIy_.js +2 -0
- package/viewer/dist/assets/chunk-727SXJPM-DZmTgL68.js +206 -0
- package/viewer/dist/assets/chunk-AQP2D5EJ-B7wr_Owx.js +231 -0
- package/viewer/dist/assets/chunk-BSJP7CBP-DbAKfVCK.js +1 -0
- package/viewer/dist/assets/chunk-CSCIHK7Q-C0rsBwqP.js +124 -0
- package/viewer/dist/assets/chunk-FMBD7UC4-BAtzt0wv.js +15 -0
- package/viewer/dist/assets/chunk-KSCS5N6A-CXXwf52I.js +10 -0
- package/viewer/dist/assets/chunk-L5ZTLDWV-DS4vRI1U.js +1 -0
- package/viewer/dist/assets/chunk-LZXEDZCA-CclT9MXr.js +2 -0
- package/viewer/dist/assets/chunk-ND2GUHAM-CUSnPl8t.js +1 -0
- package/viewer/dist/assets/chunk-NNHCCRGN-DlpIbxXb.js +159 -0
- package/viewer/dist/assets/chunk-NZK2D7GU-Dh986nJk.js +1 -0
- package/viewer/dist/assets/chunk-O5CBEL6O-JAEZ_pS6.js +70 -0
- package/viewer/dist/assets/chunk-QZHKN3VN-BKc_Kg2Z.js +1 -0
- package/viewer/dist/assets/chunk-WU5MYG2G-9ssTSMzt.js +1 -0
- package/viewer/dist/assets/chunk-XPW4576I-BwMZI0gv.js +32 -0
- package/viewer/dist/assets/classDiagram-4FO5ZUOK-DXv85WFd.js +1 -0
- package/viewer/dist/assets/classDiagram-v2-Q7XG4LA2-DXv85WFd.js +1 -0
- package/viewer/dist/assets/cose-bilkent-S5V4N54A-NGC7gYHM.js +1 -0
- package/viewer/dist/assets/cytoscape.esm-h6BdjjI9.js +321 -0
- package/viewer/dist/assets/dagre-BM42HDAG-RD63uyvd.js +4 -0
- package/viewer/dist/assets/dagre-Bx709z4p.js +1 -0
- package/viewer/dist/assets/defaultLocale-C8Fc0cco.js +1 -0
- package/viewer/dist/assets/diagram-2AECGRRQ-hwnqqCcb.js +43 -0
- package/viewer/dist/assets/diagram-5GNKFQAL-q8EaoZSG.js +10 -0
- package/viewer/dist/assets/diagram-KO2AKTUF-D4_5Qf-l.js +3 -0
- package/viewer/dist/assets/diagram-LMA3HP47-D8pwekFs.js +24 -0
- package/viewer/dist/assets/diagram-OG6HWLK6-D9KinIWZ.js +24 -0
- package/viewer/dist/assets/dist-CFOOgrqc.js +1 -0
- package/viewer/dist/assets/erDiagram-TEJ5UH35-D0Wfq250.js +85 -0
- package/viewer/dist/assets/eventmodeling-FCH6USID-D3KRSuC1.js +1 -0
- package/viewer/dist/assets/flowDiagram-I6XJVG4X-Y2DY-Ze2.js +162 -0
- package/viewer/dist/assets/ganttDiagram-6RSMTGT7-BnqkeLVw.js +292 -0
- package/viewer/dist/assets/gitGraph-WXDBUCRP-Cft7usRT.js +1 -0
- package/viewer/dist/assets/gitGraphDiagram-PVQCEYII-D-cYtraK.js +106 -0
- package/viewer/dist/assets/graphlib-B8gBHxth.js +1 -0
- package/viewer/dist/assets/index-CDR-riG2.css +2 -0
- package/viewer/dist/assets/index-DRPsTWe2.js +98 -0
- package/viewer/dist/assets/info-J43DQDTF-Djc8Bx3F.js +1 -0
- package/viewer/dist/assets/infoDiagram-5YYISTIA-D-ehtyyJ.js +2 -0
- package/viewer/dist/assets/init-D6jRqBbL.js +1 -0
- package/viewer/dist/assets/ishikawaDiagram-YF4QCWOH-Ct3f6bH-.js +70 -0
- package/viewer/dist/assets/journeyDiagram-JHISSGLW-DXlULEmi.js +139 -0
- package/viewer/dist/assets/kanban-definition-UN3LZRKU-3vE9h-R7.js +89 -0
- package/viewer/dist/assets/katex-Vhh-h91d.js +257 -0
- package/viewer/dist/assets/line-B8MygbLB.js +1 -0
- package/viewer/dist/assets/linear-CfMuM0B3.js +1 -0
- package/viewer/dist/assets/mermaid-parser.core-DzlZTbbh.js +4 -0
- package/viewer/dist/assets/mermaid.core-IM-sPiyq.js +9 -0
- package/viewer/dist/assets/mindmap-definition-RKZ34NQL-CMnpAq1T.js +96 -0
- package/viewer/dist/assets/ordinal-hYBb2elL.js +1 -0
- package/viewer/dist/assets/packet-YPE3B663-D44AzgHh.js +1 -0
- package/viewer/dist/assets/path-BWPyau1x.js +1 -0
- package/viewer/dist/assets/pie-LRSECV5Y-DL8AVJH_.js +1 -0
- package/viewer/dist/assets/pieDiagram-4H26LBE5-FvKK5jd7.js +30 -0
- package/viewer/dist/assets/quadrantDiagram-W4KKPZXB-CmjSkU8c.js +7 -0
- package/viewer/dist/assets/radar-GUYGQ44K-BvfZTVyH.js +1 -0
- package/viewer/dist/assets/requirementDiagram-4Y6WPE33-BOjca3VH.js +84 -0
- package/viewer/dist/assets/rough.esm-CSKSodPl.js +1 -0
- package/viewer/dist/assets/sankeyDiagram-5OEKKPKP-ANcjfNix.js +40 -0
- package/viewer/dist/assets/sequenceDiagram-3UESZ5HK-BLQ9AL7I.js +162 -0
- package/viewer/dist/assets/src-CAMdANUp.js +1 -0
- package/viewer/dist/assets/stateDiagram-AJRCARHV-D6CriBS6.js +1 -0
- package/viewer/dist/assets/stateDiagram-v2-BHNVJYJU-DcTp66RQ.js +1 -0
- package/viewer/dist/assets/timeline-definition-PNZ67QCA-BNhWZ_DL.js +120 -0
- package/viewer/dist/assets/treeView-BLDUP644-CY6Ph5Pu.js +1 -0
- package/viewer/dist/assets/treemap-LRROVOQU-DChSA_Qx.js +1 -0
- package/viewer/dist/assets/vennDiagram-CIIHVFJN-C01WznAC.js +34 -0
- package/viewer/dist/assets/wardley-L42UT6IY-BJ8uNoJu.js +1 -0
- package/viewer/dist/assets/wardleyDiagram-YWT4CUSO-DwDEzlVm.js +78 -0
- package/viewer/dist/assets/xychartDiagram-2RQKCTM6-BCvIDwU0.js +7 -0
- 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,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,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,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
|