specifian 0.1.0-beta.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 +21 -0
- package/README.md +314 -0
- package/dist/chunk-774NFJLC.js +267 -0
- package/dist/chunk-JY5QLNZ4.js +127 -0
- package/dist/cli/index.js +1757 -0
- package/dist/client/assets/Assistant-Bold-gm-uSS1B.woff2 +0 -0
- package/dist/client/assets/Assistant-Medium-DrcxCXg3.woff2 +0 -0
- package/dist/client/assets/Assistant-Regular-DVxZuzxb.woff2 +0 -0
- package/dist/client/assets/Assistant-SemiBold-SCI4bEL9.woff2 +0 -0
- package/dist/client/assets/ar-SA-G6X2FPQ2-Dd0X68bc.js +10 -0
- package/dist/client/assets/arc-OeTCl9Oy.js +1 -0
- package/dist/client/assets/architecture-7EHR7CIX-CaxcM-P5.js +1 -0
- package/dist/client/assets/architectureDiagram-3BPJPVTR-BIjzr_cP.js +36 -0
- package/dist/client/assets/array-BifhSqXX.js +1 -0
- package/dist/client/assets/az-AZ-76LH7QW2-CikYmJ_f.js +1 -0
- package/dist/client/assets/bg-BG-XCXSNQG7-3zBzA4I3.js +5 -0
- package/dist/client/assets/blockDiagram-GPEHLZMM-K767VD5J.js +132 -0
- package/dist/client/assets/bn-BD-2XOGV67Q-F99bvhop.js +5 -0
- package/dist/client/assets/c4Diagram-AAUBKEIU-Su5ecvGw.js +10 -0
- package/dist/client/assets/ca-ES-6MX7JW3Y-tT1B4cci.js +8 -0
- package/dist/client/assets/channel-B5WJ2uRW.js +1 -0
- package/dist/client/assets/chunk-2J33WTMH-CRsWeOs1.js +1 -0
- package/dist/client/assets/chunk-3OPIFGDE-Ba3bqwD7.js +62 -0
- package/dist/client/assets/chunk-4BX2VUAB-G5rryZd4.js +1 -0
- package/dist/client/assets/chunk-55IACEB6-pY-b07PU.js +1 -0
- package/dist/client/assets/chunk-5ZQYHXKU-txrrIoic.js +2 -0
- package/dist/client/assets/chunk-6U3AYISY-C6-Mtja1.js +12 -0
- package/dist/client/assets/chunk-727SXJPM-B4WReBxI.js +206 -0
- package/dist/client/assets/chunk-AQP2D5EJ-C8Hs2uxD.js +231 -0
- package/dist/client/assets/chunk-BSJP7CBP-Dc0utuAA.js +1 -0
- package/dist/client/assets/chunk-CSCIHK7Q-D5y67sbU.js +124 -0
- package/dist/client/assets/chunk-EIO257PC-Bx5t8kU4.js +22 -0
- package/dist/client/assets/chunk-FMBD7UC4-RPQDsV4e.js +15 -0
- package/dist/client/assets/chunk-K2UTITRG-3wps4yb2.js +35 -0
- package/dist/client/assets/chunk-KSCS5N6A-CUZckNfe.js +10 -0
- package/dist/client/assets/chunk-L5ZTLDWV-CKWh4nJc.js +1 -0
- package/dist/client/assets/chunk-ND2GUHAM-CRxqa1cH.js +1 -0
- package/dist/client/assets/chunk-NNHCCRGN-DlpIbxXb.js +159 -0
- package/dist/client/assets/chunk-NZK2D7GU-DzyoezBz.js +1 -0
- package/dist/client/assets/chunk-O5CBEL6O-DW5LwnUN.js +70 -0
- package/dist/client/assets/chunk-QZHKN3VN-D8ZpkZ2T.js +1 -0
- package/dist/client/assets/chunk-SRAX5OIU-CiRgnkYo.js +1 -0
- package/dist/client/assets/chunk-Z3N5DIM6-D9TMVU7m.js +1 -0
- package/dist/client/assets/chunk-ZUYEQ4TG-BzFOpW--.js +7 -0
- package/dist/client/assets/chunk-aKtaBQYM.js +1 -0
- package/dist/client/assets/classDiagram-4FO5ZUOK-Bu-eJOPP.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Bu-eJOPP.js +1 -0
- package/dist/client/assets/cose-bilkent-S5V4N54A-CebQ3rSv.js +1 -0
- package/dist/client/assets/cs-CZ-2BRQDIVT-CfiY7B0I.js +11 -0
- package/dist/client/assets/cytoscape.esm-h6BdjjI9.js +321 -0
- package/dist/client/assets/da-DK-5WZEPLOC-_se7cnqh.js +5 -0
- package/dist/client/assets/dagre-BM42HDAG-Dh4U_FZ3.js +4 -0
- package/dist/client/assets/dagre-Bx709z4p.js +1 -0
- package/dist/client/assets/de-DE-XR44H4JA-8OXIdaPv.js +8 -0
- package/dist/client/assets/defaultLocale-C8Fc0cco.js +1 -0
- package/dist/client/assets/diagram-2AECGRRQ-x13SPAsu.js +43 -0
- package/dist/client/assets/diagram-5GNKFQAL-Dd2ojJwV.js +10 -0
- package/dist/client/assets/diagram-KO2AKTUF-B8fPg745.js +3 -0
- package/dist/client/assets/diagram-LMA3HP47-BCOXIOSa.js +24 -0
- package/dist/client/assets/diagram-OG6HWLK6-koOG8baV.js +24 -0
- package/dist/client/assets/directory-open-01563666-D4xXyWb_.js +1 -0
- package/dist/client/assets/directory-open-4ed118d0-1i309Asm.js +1 -0
- package/dist/client/assets/dist-CkHWSq-9.js +19 -0
- package/dist/client/assets/dist-DbpddOpB.js +1 -0
- package/dist/client/assets/el-GR-BZB4AONW-DKyQ9Zbj.js +10 -0
- package/dist/client/assets/en-B4ZKOASM-C0nvDD-u.js +1 -0
- package/dist/client/assets/erDiagram-TEJ5UH35-BEkaZffl.js +85 -0
- package/dist/client/assets/es-ES-U4NZUMDT-BP9WJyyE.js +9 -0
- package/dist/client/assets/esm-BBZsBR2Q.js +135 -0
- package/dist/client/assets/eu-ES-A7QVB2H4-FF8Yo9ZO.js +11 -0
- package/dist/client/assets/eventmodeling-FCH6USID-BMNHUh4b.js +1 -0
- package/dist/client/assets/fa-IR-HGAKTJCU-9qIByi7w.js +8 -0
- package/dist/client/assets/fi-FI-Z5N7JZ37-PJ5NZtDN.js +6 -0
- package/dist/client/assets/file-open-002ab408-BHUWm0Sh.js +1 -0
- package/dist/client/assets/file-open-7c801643-DZtJT5zp.js +1 -0
- package/dist/client/assets/file-save-3189631c-CO9S4HFW.js +1 -0
- package/dist/client/assets/file-save-745eba88-Bwdfz6OZ.js +1 -0
- package/dist/client/assets/flowDiagram-I6XJVG4X-DyWXWaQw.js +162 -0
- package/dist/client/assets/fr-FR-RHASNOE6-jpsFHqOx.js +9 -0
- package/dist/client/assets/ganttDiagram-6RSMTGT7-ZyvknnRG.js +292 -0
- package/dist/client/assets/gitGraph-WXDBUCRP-Q6znYb8F.js +1 -0
- package/dist/client/assets/gitGraphDiagram-PVQCEYII-BduTvbdg.js +106 -0
- package/dist/client/assets/gl-ES-HMX3MZ6V-BJfquIk1.js +10 -0
- package/dist/client/assets/graphlib-B8gBHxth.js +1 -0
- package/dist/client/assets/he-IL-6SHJWFNN-BcKH2buH.js +10 -0
- package/dist/client/assets/hi-IN-IWLTKZ5I-CZ6xC-p-.js +4 -0
- package/dist/client/assets/hu-HU-A5ZG7DT2-Qdmeb1R8.js +7 -0
- package/dist/client/assets/id-ID-SAP4L64H-BF7hTexH.js +10 -0
- package/dist/client/assets/image-GAAHSSAO-614nODhe.js +1 -0
- package/dist/client/assets/image-blob-reduce.esm-NzmDxm1v.js +2 -0
- package/dist/client/assets/index-BM1wVbBu.css +1 -0
- package/dist/client/assets/index-CJS5C-21.js +236 -0
- package/dist/client/assets/info-J43DQDTF-B0NWKxg9.js +1 -0
- package/dist/client/assets/infoDiagram-5YYISTIA-S14LSWq2.js +2 -0
- package/dist/client/assets/init-D6jRqBbL.js +1 -0
- package/dist/client/assets/ishikawaDiagram-YF4QCWOH-mBgTlJYa.js +70 -0
- package/dist/client/assets/it-IT-JPQ66NNP-Cpwr4nh4.js +11 -0
- package/dist/client/assets/ja-JP-DBVTYXUO-aRcxqHgI.js +8 -0
- package/dist/client/assets/journeyDiagram-JHISSGLW-IqSYFtBk.js +139 -0
- package/dist/client/assets/kaa-6HZHGXH3-C3lGQNoX.js +1 -0
- package/dist/client/assets/kab-KAB-ZGHBKWFO-BTzCFBfa.js +8 -0
- package/dist/client/assets/kanban-definition-UN3LZRKU-56oDzy8w.js +89 -0
- package/dist/client/assets/katex-Vhh-h91d.js +257 -0
- package/dist/client/assets/kk-KZ-P5N5QNE5-BN2QE2qN.js +1 -0
- package/dist/client/assets/km-KH-HSX4SM5Z-ntycs-3y.js +11 -0
- package/dist/client/assets/ko-KR-MTYHY66A-z81zH0yV.js +9 -0
- package/dist/client/assets/ku-TR-6OUDTVRD-B3Hewbuw.js +9 -0
- package/dist/client/assets/line-Dw_ht09V.js +1 -0
- package/dist/client/assets/linear-CyOYhOcs.js +1 -0
- package/dist/client/assets/lt-LT-XHIRWOB4-A6GRBBpG.js +3 -0
- package/dist/client/assets/lv-LV-5QDEKY6T-JE4Th9NB.js +7 -0
- package/dist/client/assets/mermaid-parser.core-BLpZ1Xi3.js +4 -0
- package/dist/client/assets/mermaid.core-De7CyoQD.js +40 -0
- package/dist/client/assets/mindmap-definition-RKZ34NQL-uGJycOYz.js +96 -0
- package/dist/client/assets/mr-IN-CRQNXWMA-B9eQuk1Z.js +13 -0
- package/dist/client/assets/my-MM-5M5IBNSE-CHlvzYOu.js +1 -0
- package/dist/client/assets/nb-NO-T6EIAALU-FdyMoR8J.js +10 -0
- package/dist/client/assets/nl-NL-IS3SIHDZ-CcM_lR89.js +8 -0
- package/dist/client/assets/nn-NO-6E72VCQL-NsOr1ixK.js +8 -0
- package/dist/client/assets/oc-FR-POXYY2M6-ClC5X0pl.js +8 -0
- package/dist/client/assets/ordinal-hYBb2elL.js +1 -0
- package/dist/client/assets/pa-IN-N4M65BXN-LWsiX5S8.js +4 -0
- package/dist/client/assets/packet-YPE3B663-CJHLPqbQ.js +1 -0
- package/dist/client/assets/path-BWPyau1x.js +1 -0
- package/dist/client/assets/percentages-BXMCSKIN-V9nyH_gb.js +1 -0
- package/dist/client/assets/pica-DjQMjoX_.js +2 -0
- package/dist/client/assets/pie-LRSECV5Y-BUHMb4Eu.js +1 -0
- package/dist/client/assets/pieDiagram-4H26LBE5-GAPDiBah.js +30 -0
- package/dist/client/assets/pl-PL-T2D74RX3-Da6GvME-.js +9 -0
- package/dist/client/assets/prod-B3A6UDL8.js +149 -0
- package/dist/client/assets/prod-DtRVLazd.css +1 -0
- package/dist/client/assets/pt-BR-5N22H2LF-DfjURw9w.js +9 -0
- package/dist/client/assets/pt-PT-UZXXM6DQ-DwBqhsuh.js +9 -0
- package/dist/client/assets/quadrantDiagram-W4KKPZXB-BRcZqGCz.js +7 -0
- package/dist/client/assets/radar-GUYGQ44K-D2D556tt.js +1 -0
- package/dist/client/assets/requirementDiagram-4Y6WPE33-CX1Co4la.js +84 -0
- package/dist/client/assets/ro-RO-JPDTUUEW-CG0iQOM3.js +11 -0
- package/dist/client/assets/rough.esm-CSKSodPl.js +1 -0
- package/dist/client/assets/roundRect-D01gJrlt.js +1 -0
- package/dist/client/assets/ru-RU-B4JR7IUQ-Dg3BF3G-.js +9 -0
- package/dist/client/assets/sankeyDiagram-5OEKKPKP-D9ysMket.js +40 -0
- package/dist/client/assets/sequenceDiagram-3UESZ5HK-0g_Spb0h.js +162 -0
- package/dist/client/assets/si-LK-N5RQ5JYF-CpzQXp0R.js +1 -0
- package/dist/client/assets/sk-SK-C5VTKIMK-C6iDRiUH.js +6 -0
- package/dist/client/assets/sl-SI-NN7IZMDC-CpsmjUH1.js +6 -0
- package/dist/client/assets/src-BViOx7y3.js +1 -0
- package/dist/client/assets/stateDiagram-AJRCARHV-r4AmMQTe.js +1 -0
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Ck2vn4J9.js +1 -0
- package/dist/client/assets/subset-shared.chunk-CI4ywhrU.js +1 -0
- package/dist/client/assets/subset-worker.chunk-7sWn7Ze3.js +1 -0
- package/dist/client/assets/sv-SE-XGPEYMSR-B5E6Hc1s.js +10 -0
- package/dist/client/assets/ta-IN-2NMHFXQM-DNdbg2NX.js +9 -0
- package/dist/client/assets/th-TH-HPSO5L25-CwCFFmIs.js +2 -0
- package/dist/client/assets/timeline-definition-PNZ67QCA-BJtGyu1Y.js +120 -0
- package/dist/client/assets/tr-TR-DEFEU3FU-D-USnzJt.js +7 -0
- package/dist/client/assets/treeView-BLDUP644-CpwJjSVv.js +1 -0
- package/dist/client/assets/treemap-LRROVOQU-DKQuF8GY.js +1 -0
- package/dist/client/assets/uk-UA-QMV73CPH-BGFyYrZm.js +6 -0
- package/dist/client/assets/vennDiagram-CIIHVFJN-BJ7Omwmb.js +34 -0
- package/dist/client/assets/vi-VN-M7AON7JQ-BEVYsUmB.js +5 -0
- package/dist/client/assets/wardley-L42UT6IY-B6p__8sD.js +1 -0
- package/dist/client/assets/wardleyDiagram-YWT4CUSO-BZNv-WBl.js +78 -0
- package/dist/client/assets/xychartDiagram-2RQKCTM6-DL9nFgTt.js +7 -0
- package/dist/client/assets/zh-CN-LNUGB5OW-Ya4bFRUj.js +10 -0
- package/dist/client/assets/zh-HK-E62DVLB3-B_9mh-qR.js +1 -0
- package/dist/client/assets/zh-TW-RAJ6MFWO-EeeAfw19.js +9 -0
- package/dist/client/index.html +14 -0
- package/dist/lintCore-E4ZG4DFJ.js +8 -0
- package/dist/store-WLSSJHFW.js +13 -0
- package/examples/specs/_.mdx +51 -0
- package/examples/specs/_components/ColumnCount.tsx +78 -0
- package/examples/specs/_components/StatusBadge.tsx +42 -0
- package/examples/specs/_generators/typescript-interface.md +16 -0
- package/examples/specs/api/_.mdx +10 -0
- package/examples/specs/api/_schema.json +77 -0
- package/examples/specs/api/_template.mdx +39 -0
- package/examples/specs/api/users-api.mdx +76 -0
- package/examples/specs/architecture/_.mdx +10 -0
- package/examples/specs/architecture/overview.mdx +45 -0
- package/examples/specs/screens/_.mdx +10 -0
- package/examples/specs/screens/_template.mdx +29 -0
- package/examples/specs/screens/login.excalidraw +282 -0
- package/examples/specs/screens/login.mdx +37 -0
- package/examples/specs/tables/_.mdx +10 -0
- package/examples/specs/tables/_schema.json +82 -0
- package/examples/specs/tables/_template.mdx +19 -0
- package/examples/specs/tables/posts.mdx +60 -0
- package/examples/specs/tables/users.mdx +34 -0
- package/package.json +88 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kengo Asamizu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# specifian
|
|
2
|
+
|
|
3
|
+
Storybook のように、ローカルの `specs/` 配下の `.mdx` ファイルを Markdown で記述し、Web UI で表示・編集できる設計ドキュメント管理ツールです。**front-matter を構造化設計データとして扱う** ため、API でのデータ取得やコード生成テンプレートへの入力として活用できます。
|
|
4
|
+
|
|
5
|
+
## 特徴
|
|
6
|
+
|
|
7
|
+
- **MDX レンダリング・Web 編集** — specs/ の MDX ファイルをリアルタイムで Web UI で表示・編集可能。ファイル監視により外部編集も自動反映
|
|
8
|
+
- **front-matter がデータ API** — 各スペックの YAML front-matter を構造化設計データとして扱い、React コンポーネントや REST API で参照可能
|
|
9
|
+
- **wiki リンク・グラフ可視化** — `[[category:slug]]` の形式で同プロジェクト内のスペック間をリンク。グラフページでリンクネットワークをインタラクティブに表示
|
|
10
|
+
- **テンプレートからのスペック作成** — カテゴリーごとに `_template.mdx` を用意することで、新規スペック作成時に自動的にテンプレートをコピー
|
|
11
|
+
- **scaffdog テンプレートによるコード生成** — `_generators/*.md` に scaffdog 形式のテンプレートを記述し、スペック情報をコード生成に活用
|
|
12
|
+
- **ユーザー定義コンポーネント** — `specs/_components/*.tsx` に React コンポーネントを置くと、import 不要で全 MDX から利用可能。useState などフックを使ったインタラクティブな設計画面も作れる
|
|
13
|
+
- **Mermaid 図** — ` ```mermaid ` フェンスでフローチャートや ER 図を描画
|
|
14
|
+
- **全文検索** — `Ctrl+K` / `Cmd+K` のコマンドパレットで title / description / front-matter / 本文を横断検索
|
|
15
|
+
- **スキーマバリデーション** — カテゴリーに `_schema.json` (JSON Schema) を置くと front-matter を検証。Web UI に警告表示、`specifian validate` で CI 連携も可能
|
|
16
|
+
- **フォーム編集** — `_schema.json` から編集フォームを自動生成。エディターの [フォーム] タブで front-matter を GUI 編集(カラム定義はテーブル UI で行の追加・削除が可能)。スキーマが無い場合も既存データから型推論してフォームを生成
|
|
17
|
+
- **画面設計 (Excalidraw)** — `<Drawing src="screens/login" />` で Excalidraw のワイヤーフレームを MDX に埋め込み。閲覧は静的 SVG、クリックで編集モーダル。図は `specs/**/*.excalidraw` として Git 管理
|
|
18
|
+
- **TypeScript + React** — 最新の Web スタック。クライアント・サーバー双方を TypeScript で実装
|
|
19
|
+
|
|
20
|
+
## インストールと使い方
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g specifian
|
|
24
|
+
# またはプロジェクトに導入
|
|
25
|
+
npm install -D specifian
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### はじめる
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# サンプル入りの specs/ を作成
|
|
32
|
+
specifian init
|
|
33
|
+
|
|
34
|
+
# サーバー起動 (http://localhost:4400)
|
|
35
|
+
specifian serve
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
ブラウザーで `http://localhost:4400` を開き、サンプルスペックが表示されることを確認します。
|
|
39
|
+
`npx specifian serve` のように npx 経由でも実行できます。
|
|
40
|
+
|
|
41
|
+
### 日常の使い方
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# specs ディレクトリーとポートを指定してサーバー起動
|
|
45
|
+
specifian serve --dir ./specs --port 4400
|
|
46
|
+
|
|
47
|
+
# コード生成テンプレートからコード生成
|
|
48
|
+
specifian generate typescript-interface --out ./src/generated
|
|
49
|
+
|
|
50
|
+
# CI などでの front-matter スキーマ検証 (違反があれば exit 1)
|
|
51
|
+
specifian validate --dir ./specs
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### CLI コマンド
|
|
55
|
+
|
|
56
|
+
| コマンド | 説明 |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `specifian serve [--dir ./specs] [--port 4400] [--open]` | 開発サーバー起動。`--open` でブラウザー自動起動 |
|
|
59
|
+
| `specifian init [--dir ./specs]` | テンプレートから specs/ を初期化 |
|
|
60
|
+
| `specifian generate <generator> [--dir ./specs] [--spec <id>] [--out .]` | scaffdog テンプレートからコード生成 |
|
|
61
|
+
| `specifian validate [--dir ./specs]` | `_schema.json` で front-matter を検証。違反があれば exit code 1 |
|
|
62
|
+
| `specifian mcp [--dir ./specs]` | MCP サーバー (stdio) を起動。AI エージェントがスペックを読み書きできる |
|
|
63
|
+
|
|
64
|
+
## specs/ の構成ルール
|
|
65
|
+
|
|
66
|
+
specifian の動作を理解するため、specs/ のファイル構成を把握しておきましょう。
|
|
67
|
+
|
|
68
|
+
- **`specs/<category>/`** — カテゴリー(フォルダー)。ネストも可(例: `specs/api/v1/`)
|
|
69
|
+
- **`specs/<category>/<slug>.mdx`** — スペック。ID は `<category>:<slug>` 形式(例: `tables:users`)
|
|
70
|
+
- **`specs/<category>/_.mdx`** — カテゴリーインデックス。该カテゴリーのスペック一覧などを表示
|
|
71
|
+
- **`specs/<category>/_template.mdx`** — テンプレート。新規スペック作成時に自動的にコピー
|
|
72
|
+
- **`specs/_generators/*.md`** — scaffdog 形式のコード生成テンプレート(スペック扱いしない)
|
|
73
|
+
|
|
74
|
+
### ファイル例
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
specs/
|
|
78
|
+
_.mdx # ホームページ(ルート)
|
|
79
|
+
tables/
|
|
80
|
+
_.mdx # テーブルカテゴリーのインデックス
|
|
81
|
+
_template.mdx # テーブル作成時のテンプレート
|
|
82
|
+
users.mdx # ユーザーテーブル定義
|
|
83
|
+
posts.mdx # 投稿テーブル定義
|
|
84
|
+
api/
|
|
85
|
+
_.mdx # API カテゴリーのインデックス
|
|
86
|
+
_template.mdx # API スペック作成時のテンプレート
|
|
87
|
+
users-api.mdx # ユーザー API 仕様
|
|
88
|
+
screens/
|
|
89
|
+
_.mdx # 画面設計カテゴリーのインデックス
|
|
90
|
+
_template.mdx # 画面スペック作成時のテンプレート
|
|
91
|
+
login.mdx # ログイン画面仕様
|
|
92
|
+
login.excalidraw # ログイン画面のワイヤーフレーム
|
|
93
|
+
_generators/
|
|
94
|
+
typescript.md # TypeScript 型定義生成テンプレート
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## front-matter とコンポーネント
|
|
98
|
+
|
|
99
|
+
### front-matter の役割
|
|
100
|
+
|
|
101
|
+
各 `.mdx` ファイルの YAML front-matter は、その設計ドキュメントのメタデータです。
|
|
102
|
+
|
|
103
|
+
```markdown
|
|
104
|
+
---
|
|
105
|
+
title: users テーブル
|
|
106
|
+
description: ユーザーアカウント情報
|
|
107
|
+
table:
|
|
108
|
+
name: users
|
|
109
|
+
columns:
|
|
110
|
+
- { name: id, type: bigint, primaryKey: true }
|
|
111
|
+
- { name: email, type: varchar(255) }
|
|
112
|
+
---
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
front-matter は `data` 変数として MDX 内から参照できます。また、REST API(`GET /api/data`) でも一括取得可能です。
|
|
116
|
+
|
|
117
|
+
### MDX 内で使える変数
|
|
118
|
+
|
|
119
|
+
MDX 本文では、以下の変数を直接使用できます:
|
|
120
|
+
|
|
121
|
+
- **`data`** — 自身の front-matter オブジェクト
|
|
122
|
+
- **`specs`** — 全スペックの SpecMeta 配列
|
|
123
|
+
- **`category`** — 自身のカテゴリー(文字列)
|
|
124
|
+
- **`slug`** — 自身のスラッグ(文字列)
|
|
125
|
+
|
|
126
|
+
### 組み込みコンポーネント
|
|
127
|
+
|
|
128
|
+
MDX 内で import 不要で使える組み込みコンポーネント:
|
|
129
|
+
|
|
130
|
+
- **`<TableDefinition data={data.table} />`** — DB テーブル定義を描画。`data.table` は `{ name, description?, columns: [...] }` 構造
|
|
131
|
+
- **`<SpecList category="tables" />`** — カテゴリー内のスペック一覧を表示。category 省略時は自カテゴリー
|
|
132
|
+
- **`<DataView data={...} />`** — オブジェクトを折りたたみ可能な JSON ビューとして整形表示
|
|
133
|
+
- **`<SpecLink to="tables:users">...</SpecLink>`** — wiki リンクのコンポーネント版
|
|
134
|
+
- **`<Drawing src="screens/login" />`** — Excalidraw のワイヤーフレームを埋め込み。ホバーで編集ボタン、クリックでフルスクリーン編集モーダル
|
|
135
|
+
|
|
136
|
+
### 使用例
|
|
137
|
+
|
|
138
|
+
```markdown
|
|
139
|
+
---
|
|
140
|
+
title: users テーブル
|
|
141
|
+
table:
|
|
142
|
+
name: users
|
|
143
|
+
columns:
|
|
144
|
+
- { name: id, type: bigint, primaryKey: true }
|
|
145
|
+
- { name: email, type: varchar(255), nullable: false }
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
# {title}
|
|
149
|
+
|
|
150
|
+
ユーザー情報を管理するテーブルです。
|
|
151
|
+
|
|
152
|
+
<TableDefinition data={data.table} />
|
|
153
|
+
|
|
154
|
+
## 関連スペック
|
|
155
|
+
|
|
156
|
+
[[tables:posts]] で投稿テーブルを参照しています。
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### フォーム編集
|
|
160
|
+
|
|
161
|
+
エディターの [テキスト] / [フォーム] タブを切り替えることで、YAML front-matter を GUI フォームで編集できます。
|
|
162
|
+
スキーマの `title` がフォーム項目のラベル、`description` がヘルプテキストとして表示されます。
|
|
163
|
+
`enum` が定義されている項目はドロップダウンになります。
|
|
164
|
+
カテゴリーに `_schema.json` が無い場合でも、既存データから型を自動推論してフォームを生成します。
|
|
165
|
+
ただし、フォーム編集時は YAML のコメントや独自の整形は正規化されることをご注意ください。
|
|
166
|
+
|
|
167
|
+
## wiki リンク
|
|
168
|
+
|
|
169
|
+
スペック間の関連性を表現するため、`[[category:slug]]` 形式の wiki リンクを使用します。
|
|
170
|
+
|
|
171
|
+
### リンク記法
|
|
172
|
+
|
|
173
|
+
```markdown
|
|
174
|
+
[[tables:users]] # specs/tables/users.mdx へのリンク
|
|
175
|
+
[[tables:users|ユーザー]] # 表示テキストを指定
|
|
176
|
+
[[api:v1:users-api]] # ネストされたカテゴリーもサポート
|
|
177
|
+
[[tables:_]] # カテゴリーインデックスへのリンクも可
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### グラフページ
|
|
181
|
+
|
|
182
|
+
すべてのスペックと wiki リンクを自動的に抽出し、グラフページ (`/graph`) でリンクネットワークを可視化します。
|
|
183
|
+
|
|
184
|
+
- **ドラッグ可能** — ノードをドラッグして位置調整
|
|
185
|
+
- **カテゴリー別色分け** — 視覚的にカテゴリーを区別
|
|
186
|
+
- **クリックで遷移** — ノードをクリックするとそのスペックへ移動
|
|
187
|
+
- **欠損ノード** — リンク先が存在しないスペックは破線で表示
|
|
188
|
+
|
|
189
|
+
## REST API
|
|
190
|
+
|
|
191
|
+
specifian サーバーが提供する API。すべてのパスはベース URL(例: `http://localhost:4400`)に相対。
|
|
192
|
+
|
|
193
|
+
| メソッド | パス | 説明 |
|
|
194
|
+
|---|---|---|
|
|
195
|
+
| `GET` | `/api/specs` | 全スペック SpecMeta 一覧(`_template` 除く) |
|
|
196
|
+
| `GET` | `/api/specs/<category>/<slug>` | スペック 1 件取得(メタデータ + 生テキスト) |
|
|
197
|
+
| `PUT` | `/api/specs/<category>/<slug>` | スペック保存(body: `{ content: string }` ) |
|
|
198
|
+
| `POST` | `/api/specs` | スペック新規作成(body: `{ category, slug, title? }` ) |
|
|
199
|
+
| `POST` | `/api/categories` | カテゴリー新規作成(body: `{ path }` ) |
|
|
200
|
+
| `GET` | `/api/data` | 全スペックの front-matter データ(カテゴリー別) |
|
|
201
|
+
| `GET` | `/api/data/<category>` | 特定カテゴリーの front-matter データ |
|
|
202
|
+
| `GET` | `/api/graph` | wiki リンク構築のグラフデータ(ノード・エッジ) |
|
|
203
|
+
| `GET` | `/api/generators` | 利用可能なコード生成テンプレート名一覧 |
|
|
204
|
+
| `POST` | `/api/generate` | コード生成(body: `{ generator, specId?, out? }` ) |
|
|
205
|
+
| `GET` | `/api/drawings` | 全 Excalidraw ファイルの一覧 |
|
|
206
|
+
| `GET` | `/api/drawings/<path>` | Excalidraw シーン JSON 取得(path は拡張子なし) |
|
|
207
|
+
| `PUT` | `/api/drawings/<path>` | Excalidraw シーン JSON 保存 |
|
|
208
|
+
| `WS` | `/ws` | WebSocket。ファイル変更イベントをブロードキャスト |
|
|
209
|
+
|
|
210
|
+
### エラーレスポンス
|
|
211
|
+
|
|
212
|
+
すべてのエラーは `{ error: string }` 形式で、適切な HTTP ステータスコード付き。
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"error": "Spec not found"
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## MCP サーバー
|
|
221
|
+
|
|
222
|
+
specifian は MCP (Model Context Protocol) サーバーを内蔵しており、AI エージェント (Claude Code など) が
|
|
223
|
+
スペックドキュメントを安全に読み書きできます。stdio トランスポートで動作します。
|
|
224
|
+
|
|
225
|
+
### 起動
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
specifian mcp --dir ./specs
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
> stdout は MCP プロトコル (JSON-RPC) が占有するため、ログはすべて stderr に出力されます。
|
|
232
|
+
|
|
233
|
+
### Claude Code への登録例
|
|
234
|
+
|
|
235
|
+
`.mcp.json`(または Claude Code の MCP 設定)に以下を追加します。
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"mcpServers": {
|
|
240
|
+
"specifian": {
|
|
241
|
+
"command": "npx",
|
|
242
|
+
"args": ["specifian", "mcp", "--dir", "./specs"]
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### 提供ツール
|
|
249
|
+
|
|
250
|
+
スペック ID はすべて `"category:slug"` 形式(インデックスは `tables:_`)。`content` は front-matter を含む MDX 全文です。
|
|
251
|
+
|
|
252
|
+
| ツール | 説明 |
|
|
253
|
+
|---|---|
|
|
254
|
+
| `list_specs` | 全スペックのメタ情報 (SpecMeta[]) を返す(`_template` 除く) |
|
|
255
|
+
| `read_spec` | `{ id }` → `{ meta, content }`。存在しなければエラー |
|
|
256
|
+
| `write_spec` | `{ id, content }` で既存スペックを上書き保存し `{ meta, issues }` を返す |
|
|
257
|
+
| `create_spec` | `{ category, slug, title? }` で新規作成。`_template.mdx` があればコピー |
|
|
258
|
+
| `rename_spec` | `{ from, to }` でリネーム+wiki リンク一括書き換え。`{ meta, rewrittenFiles }` |
|
|
259
|
+
| `delete_spec` | `{ id }` で削除。`{ ok, brokenRefs }` で壊れた参照を通知 |
|
|
260
|
+
| `get_refs` | `{ id }` → `{ refs }`。id を参照しているスペック ID 一覧 |
|
|
261
|
+
| `search` | `{ query, limit? }` → SearchResult[]。全文検索 |
|
|
262
|
+
| `get_data` | `{ category? }` → front-matter データ map |
|
|
263
|
+
| `validate` | front-matter を `_schema.json` で検証し ValidationReport を返す |
|
|
264
|
+
| `lint` | `{ content, category?, slug? }` で保存せず検証し `{ issues }` を返す |
|
|
265
|
+
| `list_generators` | 利用可能なコード生成テンプレート名一覧 (string[]) |
|
|
266
|
+
| `generate` | `{ generator, specId?, out? }` でコード生成。`{ files }` を返す |
|
|
267
|
+
|
|
268
|
+
## 開発
|
|
269
|
+
|
|
270
|
+
### 開発モード起動
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
npm run dev
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
- サーバー(port 4399) + Vite 開発サーバー(port 5180)を同時起動
|
|
277
|
+
- クライアント側は Vite で `/api` と `/ws` を localhost:4399 へプロキシ
|
|
278
|
+
- `examples/specs` をサンプルディレクトリーとして使用
|
|
279
|
+
|
|
280
|
+
### ビルド
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
npm run build
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
- `tsup` で CLI・サーバーを `dist/` へビルド
|
|
287
|
+
- `vite build` でクライアントを `dist/client/` へビルド
|
|
288
|
+
|
|
289
|
+
### 起動(ビルド後)
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
npm start
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
例:`examples/specs` で本番ビルドの specifian を起動します。
|
|
296
|
+
|
|
297
|
+
## テクノロジースタック
|
|
298
|
+
|
|
299
|
+
| 領域 | 技術 |
|
|
300
|
+
|---|---|
|
|
301
|
+
| 言語 | TypeScript (strict mode), ESM |
|
|
302
|
+
| CLI | commander |
|
|
303
|
+
| サーバー | Express 4 + WebSocket (ws) + ファイル監視 (chokidar) + YAML 解析 (gray-matter) |
|
|
304
|
+
| クライアント | React 18 + React Router 6 + Vite 8 |
|
|
305
|
+
| MDX | @mdx-js/mdx v3 + remark プラグイン |
|
|
306
|
+
| エディター | @uiw/react-codemirror |
|
|
307
|
+
| グラフ可視化 | d3-force |
|
|
308
|
+
| コード生成 | @scaffdog/engine |
|
|
309
|
+
|
|
310
|
+
Node.js >= 20 が必須。Windows 対応済み。
|
|
311
|
+
|
|
312
|
+
## License
|
|
313
|
+
|
|
314
|
+
MIT
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
WIKILINK_PATTERN,
|
|
4
|
+
loadSpecs
|
|
5
|
+
} from "./chunk-JY5QLNZ4.js";
|
|
6
|
+
|
|
7
|
+
// src/server/lintCore.ts
|
|
8
|
+
import matter from "gray-matter";
|
|
9
|
+
import { compile } from "@mdx-js/mdx";
|
|
10
|
+
import remarkGfm from "remark-gfm";
|
|
11
|
+
import remarkFrontmatter from "remark-frontmatter";
|
|
12
|
+
import Ajv2 from "ajv";
|
|
13
|
+
|
|
14
|
+
// src/server/validate.ts
|
|
15
|
+
import fs from "fs/promises";
|
|
16
|
+
import path from "path";
|
|
17
|
+
import Ajv from "ajv";
|
|
18
|
+
async function loadCategorySchema(specsDir, category) {
|
|
19
|
+
const segments = category === "" ? [] : category.split("/");
|
|
20
|
+
const schemaPath = path.join(specsDir, ...segments, "_schema.json");
|
|
21
|
+
let schemaText;
|
|
22
|
+
try {
|
|
23
|
+
schemaText = await fs.readFile(schemaPath, "utf-8");
|
|
24
|
+
} catch (err) {
|
|
25
|
+
const code = err.code;
|
|
26
|
+
if (code === "ENOENT") {
|
|
27
|
+
return { schema: null };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
schema: null,
|
|
31
|
+
error: `_schema.json \u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093\u3067\u3057\u305F: ${String(err)}`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = JSON.parse(schemaText);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return {
|
|
39
|
+
schema: null,
|
|
40
|
+
error: `_schema.json \u306E JSON \u30D1\u30FC\u30B9\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${String(err)}`
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
44
|
+
return {
|
|
45
|
+
schema: null,
|
|
46
|
+
error: "_schema.json \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059"
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return { schema: parsed };
|
|
50
|
+
}
|
|
51
|
+
async function validateSpecs(specsDir) {
|
|
52
|
+
const issues = [];
|
|
53
|
+
const allSpecs = await loadSpecs(specsDir);
|
|
54
|
+
const categoriesWithSchema = /* @__PURE__ */ new Set();
|
|
55
|
+
for (const spec of allSpecs) {
|
|
56
|
+
if (spec.category !== "") {
|
|
57
|
+
const schemaPath = path.join(specsDir, ...spec.category.split("/"), "_schema.json");
|
|
58
|
+
try {
|
|
59
|
+
await fs.access(schemaPath);
|
|
60
|
+
categoriesWithSchema.add(spec.category);
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
66
|
+
const compiledSchemas = /* @__PURE__ */ new Map();
|
|
67
|
+
for (const category of categoriesWithSchema) {
|
|
68
|
+
const schemaSpecId = `${category}:_schema`;
|
|
69
|
+
const result = await loadCategorySchema(specsDir, category);
|
|
70
|
+
if (result.error !== void 0) {
|
|
71
|
+
issues.push({
|
|
72
|
+
specId: schemaSpecId,
|
|
73
|
+
path: "/",
|
|
74
|
+
message: result.error
|
|
75
|
+
});
|
|
76
|
+
compiledSchemas.set(category, null);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (result.schema === null) {
|
|
80
|
+
compiledSchemas.set(category, null);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const validate = ajv.compile(result.schema);
|
|
85
|
+
compiledSchemas.set(category, validate);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
issues.push({
|
|
88
|
+
specId: schemaSpecId,
|
|
89
|
+
path: "/",
|
|
90
|
+
message: `_schema.json \u306E\u30B9\u30AD\u30FC\u30DE\u30B3\u30F3\u30D1\u30A4\u30EB\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${String(err)}`
|
|
91
|
+
});
|
|
92
|
+
compiledSchemas.set(category, null);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const spec of allSpecs) {
|
|
96
|
+
if (!categoriesWithSchema.has(spec.category)) continue;
|
|
97
|
+
if (spec.slug === "_" || spec.slug === "_template") continue;
|
|
98
|
+
const validate = compiledSchemas.get(spec.category);
|
|
99
|
+
if (!validate) continue;
|
|
100
|
+
const valid = validate(spec.data);
|
|
101
|
+
if (!valid && validate.errors) {
|
|
102
|
+
for (const err of validate.errors) {
|
|
103
|
+
const instancePath = err.instancePath || "/";
|
|
104
|
+
let message = err.message ?? "\u30D0\u30EA\u30C7\u30FC\u30B7\u30E7\u30F3\u30A8\u30E9\u30FC";
|
|
105
|
+
if (err.keyword === "required" && err.params && "missingProperty" in err.params) {
|
|
106
|
+
message = `\u5FC5\u9808\u30D7\u30ED\u30D1\u30C6\u30A3\u304C\u3042\u308A\u307E\u305B\u3093: '${String(err.params["missingProperty"])}'`;
|
|
107
|
+
} else if (err.keyword === "additionalProperties" && err.params && "additionalProperty" in err.params) {
|
|
108
|
+
message = `\u8A31\u53EF\u3055\u308C\u3066\u3044\u306A\u3044\u30D7\u30ED\u30D1\u30C6\u30A3: '${String(err.params["additionalProperty"])}'`;
|
|
109
|
+
} else if (err.keyword === "enum" && err.params && "allowedValues" in err.params) {
|
|
110
|
+
const allowed = err.params["allowedValues"].join(", ");
|
|
111
|
+
message = `${err.message ?? "enum \u30A8\u30E9\u30FC"} (\u8A31\u53EF\u5024: ${allowed})`;
|
|
112
|
+
} else if (err.keyword === "type" && err.params && "type" in err.params) {
|
|
113
|
+
message = `\u578B\u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093: ${String(err.params["type"])} \u304C\u5FC5\u8981\u3067\u3059`;
|
|
114
|
+
}
|
|
115
|
+
issues.push({
|
|
116
|
+
specId: spec.id,
|
|
117
|
+
path: instancePath,
|
|
118
|
+
message
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { issues };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/server/lintCore.ts
|
|
127
|
+
function stripCodeBlocks(src) {
|
|
128
|
+
let result = src.replace(/```[\s\S]*?```/g, "");
|
|
129
|
+
result = result.replace(/`[^`]*`/g, "");
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
function extractWikilinkIds(body) {
|
|
133
|
+
const stripped = stripCodeBlocks(body);
|
|
134
|
+
const seen = /* @__PURE__ */ new Set();
|
|
135
|
+
const ids = [];
|
|
136
|
+
const re = new RegExp(WIKILINK_PATTERN.source, WIKILINK_PATTERN.flags);
|
|
137
|
+
let m;
|
|
138
|
+
while ((m = re.exec(stripped)) !== null) {
|
|
139
|
+
const target = m[1].trim();
|
|
140
|
+
if (!target.includes(":")) continue;
|
|
141
|
+
if (!seen.has(target)) {
|
|
142
|
+
seen.add(target);
|
|
143
|
+
ids.push(target);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return ids;
|
|
147
|
+
}
|
|
148
|
+
function ajvErrorToMessage(err) {
|
|
149
|
+
let message = err.message ?? "\u30D0\u30EA\u30C7\u30FC\u30B7\u30E7\u30F3\u30A8\u30E9\u30FC";
|
|
150
|
+
if (err.keyword === "required" && err.params && "missingProperty" in err.params) {
|
|
151
|
+
message = `\u5FC5\u9808\u30D7\u30ED\u30D1\u30C6\u30A3\u304C\u3042\u308A\u307E\u305B\u3093: '${String(err.params["missingProperty"])}'`;
|
|
152
|
+
} else if (err.keyword === "additionalProperties" && err.params && "additionalProperty" in err.params) {
|
|
153
|
+
message = `\u8A31\u53EF\u3055\u308C\u3066\u3044\u306A\u3044\u30D7\u30ED\u30D1\u30C6\u30A3: '${String(err.params["additionalProperty"])}'`;
|
|
154
|
+
} else if (err.keyword === "enum" && err.params && "allowedValues" in err.params) {
|
|
155
|
+
const allowed = err.params["allowedValues"].join(", ");
|
|
156
|
+
message = `${err.message ?? "enum \u30A8\u30E9\u30FC"} (\u8A31\u53EF\u5024: ${allowed})`;
|
|
157
|
+
} else if (err.keyword === "type" && err.params && "type" in err.params) {
|
|
158
|
+
message = `\u578B\u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093: ${String(err.params["type"])} \u304C\u5FC5\u8981\u3067\u3059`;
|
|
159
|
+
}
|
|
160
|
+
return message;
|
|
161
|
+
}
|
|
162
|
+
async function lintContent(specsDir, req) {
|
|
163
|
+
const issues = [];
|
|
164
|
+
const { content, category, slug } = req;
|
|
165
|
+
let frontmatterData = {};
|
|
166
|
+
let bodyContent = content;
|
|
167
|
+
let yamlFailed = false;
|
|
168
|
+
try {
|
|
169
|
+
const parsed = matter(content);
|
|
170
|
+
frontmatterData = parsed.data;
|
|
171
|
+
bodyContent = parsed.content;
|
|
172
|
+
} catch (err) {
|
|
173
|
+
yamlFailed = true;
|
|
174
|
+
const e = err;
|
|
175
|
+
const rawLine = e.mark?.line;
|
|
176
|
+
const line = typeof rawLine === "number" ? rawLine + 1 : void 0;
|
|
177
|
+
issues.push({
|
|
178
|
+
severity: "error",
|
|
179
|
+
rule: "yaml",
|
|
180
|
+
message: `YAML \u30D1\u30FC\u30B9\u30A8\u30E9\u30FC: ${e.message}`,
|
|
181
|
+
...line !== void 0 ? { line } : {}
|
|
182
|
+
});
|
|
183
|
+
bodyContent = content;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
await compile(content, {
|
|
187
|
+
remarkPlugins: [remarkGfm, remarkFrontmatter]
|
|
188
|
+
// VFile のメッセージ (warnings) は必要ないのでサイレント
|
|
189
|
+
});
|
|
190
|
+
} catch (err) {
|
|
191
|
+
const e = err;
|
|
192
|
+
const message = e.reason ?? e.message ?? "MDX \u30B3\u30F3\u30D1\u30A4\u30EB\u30A8\u30E9\u30FC";
|
|
193
|
+
const issue = {
|
|
194
|
+
severity: "error",
|
|
195
|
+
rule: "mdx",
|
|
196
|
+
message
|
|
197
|
+
};
|
|
198
|
+
if (typeof e.line === "number") issue.line = e.line;
|
|
199
|
+
if (typeof e.column === "number") issue.column = e.column;
|
|
200
|
+
issues.push(issue);
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const allSpecs = await loadSpecs(specsDir);
|
|
204
|
+
const knownIds = new Set(
|
|
205
|
+
allSpecs.filter((s) => s.slug !== "_template").map((s) => s.id)
|
|
206
|
+
);
|
|
207
|
+
const ids = extractWikilinkIds(bodyContent);
|
|
208
|
+
const seenUnresolved = /* @__PURE__ */ new Set();
|
|
209
|
+
for (const id of ids) {
|
|
210
|
+
if (!knownIds.has(id) && !seenUnresolved.has(id)) {
|
|
211
|
+
seenUnresolved.add(id);
|
|
212
|
+
issues.push({
|
|
213
|
+
severity: "warning",
|
|
214
|
+
rule: "wikilink",
|
|
215
|
+
message: `\u672A\u89E3\u6C7A\u306E wiki \u30EA\u30F3\u30AF: [[${id}]]`
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
}
|
|
221
|
+
if (!yamlFailed && category !== void 0) {
|
|
222
|
+
const effectiveSlug = slug ?? "";
|
|
223
|
+
const isNormalSpec = effectiveSlug !== "_" && effectiveSlug !== "_template";
|
|
224
|
+
if (isNormalSpec) {
|
|
225
|
+
try {
|
|
226
|
+
const schemaResult = await loadCategorySchema(specsDir, category);
|
|
227
|
+
if (schemaResult.error !== void 0) {
|
|
228
|
+
} else if (schemaResult.schema !== null) {
|
|
229
|
+
const ajv = new Ajv2({ allErrors: true, strict: false });
|
|
230
|
+
let validate;
|
|
231
|
+
try {
|
|
232
|
+
validate = ajv.compile(schemaResult.schema);
|
|
233
|
+
} catch {
|
|
234
|
+
validate = null;
|
|
235
|
+
}
|
|
236
|
+
if (validate) {
|
|
237
|
+
const valid = validate(frontmatterData);
|
|
238
|
+
if (!valid && validate.errors) {
|
|
239
|
+
for (const err of validate.errors) {
|
|
240
|
+
const instancePath = err.instancePath || "/";
|
|
241
|
+
const message = ajvErrorToMessage({
|
|
242
|
+
instancePath,
|
|
243
|
+
keyword: err.keyword,
|
|
244
|
+
message: err.message,
|
|
245
|
+
params: err.params
|
|
246
|
+
});
|
|
247
|
+
issues.push({
|
|
248
|
+
severity: "error",
|
|
249
|
+
rule: "schema",
|
|
250
|
+
message: `${instancePath}: ${message}`
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return issues;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export {
|
|
264
|
+
loadCategorySchema,
|
|
265
|
+
validateSpecs,
|
|
266
|
+
lintContent
|
|
267
|
+
};
|