@wscytz/dsl 0.0.0-stage → 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 ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 cy
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md CHANGED
@@ -1,3 +1,82 @@
1
- # Temporary Holding Version
1
+ # @wscytz/dsl
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ cys-dsl — cy's Stift 画布的**双向文字表示(转义)**。整张画布能压成一段文本,文本也能重建/改画布;任何 AI 或外部工具读写一段文本就能操作画布。
4
+
5
+ ## 是什么
6
+
7
+ cys-dsl **v9** 文法:**5 种元素**(`card`/`rect`/`frame`/`text`/`arrow`)、6 色名(legacy 枚举冻结)、**关系式放置**(`right-of`/`below #锚` + `@gap`;v9 起 `@组名` 组级相对锚——锚定组包围盒,支持 `@"含空格名"` 与中文裸名)、箭头签名(label/color/dash/arrowhead/route/wikilink)、**卡片内容 `@title`(短)/`@content`(长 markdown,`\n` 转义多行)**,以及语义 directive:**`@group("名")`**(v7,语义分组)、**`@href(#a;#b)`**(v7,显式引用)、**`@compute("公式")`**(v7,安全公式,仅 text)、**`@type/@tags/@links/@code/@quote`**(v8,卡片结构化字段)。v5 内容;v6 freedraw 出 DSL;v9 组级相对锚(锚位三形态与保留字防线见 `DSL_GRAMMAR_REFERENCE`)。
8
+
9
+ ## public API
10
+
11
+ - **grammar**:`DSL_VERSION`、`DSL_KINDS`、`DSL_COLORS`、`DSL_COLOR_ALIASES`、`DSL_GRAMMAR_REFERENCE`、`DSL_RESERVED_GROUP_ANCHOR_WORDS`(v9 组锚保留字)、`DSL_MAX_TEXT_LEN`、`DSL_MAX_CONTENT_LEN`、`truncateDslText`(代理对安全截断)
12
+ - **serialize**:`serializeCanvas`、`serializeCanvasReadable`、`serializeElement`
13
+ - **parse**:`parseDsl`、`parseDslWithDiagnostics`、`parseDslStrictWithDiagnostics`(Peggy 语法驱动)
14
+ - **sanitize**:`sanitizeDslOps`(opt-in 修正层,防 LLM 产非法值不崩)
15
+ - **solve**:`solveRelational` + `ExistingGeom`/`SolveResult`(关系式 → 绝对坐标;组锚、碰撞避让、断链诊断)
16
+ - **adapter**:`toJsonCanvas`(→ JSON Canvas 1.0;integer 坐标、id 去重、跳过留痕)+ `JsonCanvasFile/Node/Edge/SkippedOp` 类型
17
+ - **svg**:`toSvg`(零依赖预览级 SVG,CLI `render --svg` 的底层)
18
+ - **types**:`CanvasElement` 等边界类型(与 canvas-engine/domain 结构兼容,`types-compat.test.ts` 钉住)
19
+
20
+ ## 用法
21
+
22
+ ```ts
23
+ import { serializeCanvas, parseDsl, sanitizeDslOps, solveRelational, DSL_GRAMMAR_REFERENCE } from '@wscytz/dsl'
24
+
25
+ const text = serializeCanvas(elements) // 画布 → 文本
26
+ const ops = parseDsl(text) // 文本 → ops
27
+ const clean = sanitizeDslOps(ops, ctx) // 修正非法值(永不抛错)
28
+ const solved = solveRelational(clean.ops) // 关系式 → 绝对坐标
29
+ ```
30
+
31
+ ## CLI
32
+
33
+ `pnpm --filter @wscytz/dsl cli` 或 `tsx src/cli.ts <命令> <file.sift>`(npm 发布后为 `npx`;后缀 `.sift` 为约定,任意文本文件可读):
34
+
35
+ - `validate` — 语法/卫生/求解全链检查;诊断走 stderr,警告不阻断(exit 0)/ 语法错 exit 1 / 用法错 exit 2。
36
+ - `render` — 默认输出 JSON Canvas 1.0(Obsidian `.canvas` 直接可开);`--svg` 输出预览级 SVG。SVG 侧:默认路由**自动避障**(正交可见图 + 方向状态 A* 求解器规划,free 模式叠视线拉直;生成路径不穿卡;显式 `@route/@curve/@elbow` 不干预;源/目标被围死退启发式兜底);`@route(elbow)` 未给折点 = 正交自动路由(求解器,严格水平/垂直);折线转角圆角化。求解器参数可调(`RoutingConfig`:安全边距/折代价 λ/量化步长等,见 `src/routing.ts`)。
37
+ - `roundtrip` — DSL→元素→DSL 双往返签名比对(转义保真)。
38
+
39
+ ## 依赖
40
+
41
+ **零运行时依赖**(2026-10-24 剥离定案):src/ 只 import 包内模块与 Node 内建(cli.ts 的 `node:fs`/`node:url`,不进 barrel)。原 `@cys-stift/canvas-engine`/`@cys-stift/domain` 类型依赖已改为包内 `types.ts` 边界副本 + 结构兼容测试(`types-compat.test.ts`);二者仅作为 devDep 供测试比对。框架无关,可独立于本 monorepo 复用。
42
+
43
+ ## 文法治理
44
+
45
+ - 增删指令种类 / 属性 / 颜色枚举 → bump `DSL_VERSION`(`dsl-grammar.ts`)。
46
+ - 改 Peggy 语法后跑 `pnpm --filter @wscytz/dsl gen` 重新生成 parser。
47
+ - round-trip 契约由 `__tests__/` 守护(roundtrip / v4-stability / robustness)。
48
+
49
+ ## 不变量
50
+
51
+ - **round-trip**:活跃 kind 的 serialize↔parse 双向稳定。
52
+ - **sanitize 永不抛错**:坏输入降级,不整块丢。
53
+ - **freedraw 不在 DSL**:freedraw 已出 DSL(程序自管 R2 + 渲染);serialize 按 `DSL_KINDS` 过滤,`[freedraw]` 行 parse 报 unrecognized。点序列/位置都不进 DSL(隐私 + 防 DoS + 存储重/意义低)。
54
+ - **Bauhaus 6 色**:不新增色。
55
+ - **截断代理对安全**:`truncateDslText` 不劈开 emoji/增补平面字符(不产孤立代理位)。
56
+
57
+ ## 内容能力与边界(v7;内容自 v5 引入)
58
+
59
+ v5 的卡片内容(`@title`/`@content`)在格式层、apply 层、`/ask` 落库层均已接通(无遗留适配缝)。A/D/E 三处此前的缺口/局限于 2026-07-22 全部闭合:
60
+
61
+ - **A(`/ask` 落库)**:`applyOpsAndPersist` 建/改卡写回 `@title`/`@content`(create 不再落空标题卡;update 经 post-hoc 回写写回 `Card.title/body`;回滚 + 一次性 undo 覆盖内容);companion live 路径(`makeOnCardCreate`/`makeOnCardUpdate`)同接。锁于 `apps/web` 的 `canvas-host-builder.test.ts`。
62
+ - **D(清空内容)**:`@title("")`/`@content("")` parse 成空串 → apply 真写空串清空(锁于 `dsl-content.test.ts`)。serialize 对空串**不发** token(空=默认态,by-design)。
63
+ - **E(纯内容/属性编辑)**:现有卡的 `@title`/`@content`/`@color`/`@size` 编辑**可省 `@pos`**(`keepExistingPos` 标志,apply 时几何沿用现有卡);`@pos` 仅在**移动**卡或**建卡**时必需。裸行(无任何字段)/ `create` 无 `@pos` 仍报 `missing @pos`。锁于 `dsl-content.test.ts` + `apply-content.test.ts`。
64
+ - DSL 模态编辑器(人读)与 `/ask` agent(AI 写)两条路径对 `@title`/`@content` 行为一致。
65
+
66
+ **边界(by-design,非局限)**:serialize 不发空 token(空是默认态,无需冗余);`keepExistingPos` 是**输入专用**——serialize 始终发绝对 `@pos`,故 round-trip 序列化格式不变(未 bump `DSL_VERSION`)。
67
+
68
+ ## 预期能力测试(qwen-max-7-21)
69
+
70
+ `__tests__/qwen-max-7-21-*.test.ts` 是本包自带的**预期能力测试集**(与具体 LLM 解耦的基线 + 可选实弹):
71
+
72
+ - `qwen-max-7-21-roundtrip`:完整文字化——整张画布(几何 + 内容)无损往返。
73
+ - `qwen-max-7-21-ai-grammar`:AI 可驱动·文法侧——真实风格 LLM 输出样本零诊断可解析 + sanitize 不抛。
74
+ - `qwen-max-7-21-ai-apply`(在 `apps/web`):AI 可驱动·应用侧——AI 重排 DSL 经 `applyLayout` 真结构化画布。
75
+ - `qwen-max-7-21-live-llm`:**live 实测**(env 门控,默认跳过)——真调 LLM(DeepSeek `deepseek-chat` / `deepseek-reasoner`,或 GLM 兜底)只凭语法参考产出合法 DSL。启用:
76
+ `CYS_DSL_LIVE_LLM=1 DEEPSEEK_API_KEY=… pnpm --filter @wscytz/dsl test qwen-max-7-21-live-llm`。
77
+ prompt 仅含合成画布;key 只进 auth header,不入日志/提交。
78
+
79
+ ## 相关
80
+
81
+ - 论证与 v5 内容扩展计划:私有仓 `docs/research/2026-07-21-dsl-v5-content-rationale.md`、`docs/plans/2026-07-21-dsl-execution.md`。
82
+ - 提取先例:`packages/canvas-engine`(同款"解耦 + 重新打包"打法)。
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Canvas DSL — the unified bidirectional text format for the canvas (Phase 0 / T3).
3
+ *
4
+ * `serializeCanvas(elements)` is the **canvas → text** direction (the missing
5
+ * half before Phase 0). The **text → canvas** direction is `parseDsl` (in
6
+ * ./dsl-parser.ts); together they round-trip the active geometric kinds.
7
+ *
8
+ * Grammar (active kinds only; see CanvasElementKind active/legacy split):
9
+ * [card #<id>] @pos(<x>,<y>) @size(<w>,<h>) @color(<c>)
10
+ * [rect #<id>] @pos(<x>,<y>) @size(<w>,<h>) @color(<c>)
11
+ * [text #<id>] @pos(<x>,<y>) @text("<t>") @color(<c>)
12
+ * [arrow #<id>] from #<a> to #<b> @label("<l>") @color(<c>) @dash(solid|dashed|dotted) @arrowhead(arrow|triangle|none)
13
+ *
14
+ * Legacy kinds (ellipse/line/note/image) and freedraw are NOT serialized — they're
15
+ * not in DSL_KINDS. freedraw is program-managed (R2 store + renderer): point sequence
16
+ * is heavy / low-value / privacy-sensitive, so it stays out of the text format entirely.
17
+ * Canonical single source: ./dsl-grammar.ts (DSL_KINDS / DSL_COLORS / DSL_GRAMMAR_REFERENCE).
18
+ *
19
+ * 已知限制(2026-08-29 审计登记):rotation 不序列化 —— DSL 文本路径只往返
20
+ * x/y/w/h。当前渲染器不消费 rotation(self-built-render 零引用)、无任何 UI 手柄
21
+ * 产出非零值,DB 路径(canvas-binding)保留字段;若将来引入旋转手柄,须同步在
22
+ * grammar 加 @rotation 指令(加法,bump DSL_VERSION),否则 DSL Apply 会把旋转归零。
23
+ */
24
+ import type { CanvasElement, CardType, TagRef, LinkPreview, CodeBlock, Quote } from './types.js';
25
+ /**
26
+ * v8:card 行可注入的卡片内容/结构化字段(消费者从 CardService 读,经 resolve 回调传入)。
27
+ * title/content 是 v5;type/tags/links/codeSnippets/quotes 是 v8。media 故意不在(二进制重/隐私,
28
+ * 不进 DSL)。缺省几何-only(不传 resolve → 所有现有调用点零改动,向后兼容)。
29
+ */
30
+ export type CardDslContent = {
31
+ title?: string;
32
+ content?: string;
33
+ type?: CardType;
34
+ tags?: TagRef[];
35
+ links?: LinkPreview[];
36
+ codeSnippets?: CodeBlock[];
37
+ quotes?: Quote[];
38
+ };
39
+ /**
40
+ * Serialize the canvas's active elements to a text block the AI can read.
41
+ * Pure function of the element list — no side-effects, no engine access.
42
+ */
43
+ export declare function serializeCanvas(elements: CanvasElement[],
44
+ /** v5:可选,card id → 卡片内容(v8:含结构化字段,见 {@link CardDslContent})。
45
+ * 不传 → 几何-only(向后兼容,所有现有调用点零改动)。传了 → card 行附相应指令。 */
46
+ resolve?: (id: string) => CardDslContent | undefined): string;
47
+ /**
48
+ * 面向人的可读序列化(DSL 编辑器用)。
49
+ *
50
+ * v5 现状:与 strict {@link serializeCanvas} **逐字节收敛**(card 行带真实 @title/@content
51
+ * token,由 resolve 注入;旧的 ` # title:` 注释行已退役)。故本函数**委托** serializeCanvas,
52
+ * 不另留一份相同实现(消除漂移风险;F)。
53
+ *
54
+ * 保留独立命名的理由:它是编辑器的「人读视图」语义入口。将来若要加人读增强(如 @content
55
+ * 多行展开显示、注释),从**这里**分叉,strict 仍是机器往返形态。当前二者同形态。
56
+ *
57
+ * resolve: 可选,card id → {title, content}(消费者从 CardService 读)。不给 → 几何-only。
58
+ */
59
+ export declare function serializeCanvasReadable(elements: CanvasElement[], resolve?: (id: string) => CardDslContent | undefined): string;
60
+ export declare function serializeElement(e: CanvasElement,
61
+ /** v5/v8:card 的内容与结构化字段(可选,由 serializeCanvas 的 resolve 注入)。非 card 元素忽略。 */
62
+ content?: CardDslContent): string;
@@ -0,0 +1,215 @@
1
+ import { DSL_KINDS, DSL_MAX_TEXT_LEN, DSL_MAX_CONTENT_LEN, DSL_MAX_HREF_TARGETS, DSL_MAX_TAG_COUNT, DSL_MAX_LINK_COUNT, DSL_MAX_CODE_BLOCKS, DSL_MAX_QUOTES } from './dsl-grammar.js';
2
+ /**
3
+ * Serialize the canvas's active elements to a text block the AI can read.
4
+ * Pure function of the element list — no side-effects, no engine access.
5
+ */
6
+ export function serializeCanvas(elements,
7
+ /** v5:可选,card id → 卡片内容(v8:含结构化字段,见 {@link CardDslContent})。
8
+ * 不传 → 几何-only(向后兼容,所有现有调用点零改动)。传了 → card 行附相应指令。 */
9
+ resolve) {
10
+ return elements
11
+ .filter((e) => DSL_KINDS.includes(e.kind))
12
+ .map((e) => serializeElement(e, e.kind === 'card' ? resolve?.(e.id) : undefined))
13
+ .filter(Boolean)
14
+ .join('\n');
15
+ }
16
+ /**
17
+ * 面向人的可读序列化(DSL 编辑器用)。
18
+ *
19
+ * v5 现状:与 strict {@link serializeCanvas} **逐字节收敛**(card 行带真实 @title/@content
20
+ * token,由 resolve 注入;旧的 ` # title:` 注释行已退役)。故本函数**委托** serializeCanvas,
21
+ * 不另留一份相同实现(消除漂移风险;F)。
22
+ *
23
+ * 保留独立命名的理由:它是编辑器的「人读视图」语义入口。将来若要加人读增强(如 @content
24
+ * 多行展开显示、注释),从**这里**分叉,strict 仍是机器往返形态。当前二者同形态。
25
+ *
26
+ * resolve: 可选,card id → {title, content}(消费者从 CardService 读)。不给 → 几何-only。
27
+ */
28
+ export function serializeCanvasReadable(elements, resolve) {
29
+ return serializeCanvas(elements, resolve);
30
+ }
31
+ export function serializeElement(e,
32
+ /** v5/v8:card 的内容与结构化字段(可选,由 serializeCanvas 的 resolve 注入)。非 card 元素忽略。 */
33
+ content) {
34
+ const pos = `@pos(${e.x.toFixed(1)},${e.y.toFixed(1)})`;
35
+ const color = e.color ? ` @color(${e.color})` : '';
36
+ switch (e.kind) {
37
+ case 'card': {
38
+ // v5:可选 @title/@content;v8:可选 @type/@tags/@links/@code/@quote(消费者注入)。
39
+ // 缺省几何-only(round-trip 与 v4 等价)。
40
+ // 超长防护(2026-08-29 P1):parser 对 @title/@content 静默截断(DoS 防护,保留)。
41
+ // serialize 侧若照常 emit 超长值,DSL 编辑器 Apply 会把**截断后的值**写回
42
+ // Card.body → 数据尾部永久丢失。故超长字段此处**不 emit**(parse 后为
43
+ // undefined,所有写回路径的 `!== undefined` 守卫自然跳过)——数据保住,
44
+ // 截断防护不动。text/frame text 同理走 textAttr helper。
45
+ const titleAttr = quotedAttr('title', content?.title, DSL_MAX_TEXT_LEN);
46
+ const contentAttr = quotedAttr('content', content?.content, DSL_MAX_CONTENT_LEN);
47
+ return `[card #${e.id}] ${pos} @size(${e.w.toFixed(1)},${e.h.toFixed(1)})${color}${titleAttr}${contentAttr}${metaGroup(e)}${metaHref(e)}${metaType(content)}${metaTags(content)}${metaLinks(content)}${metaCode(content)}${metaQuote(content)}`;
48
+ }
49
+ case 'rect':
50
+ return `[rect #${e.id}] ${pos} @size(${e.w.toFixed(1)},${e.h.toFixed(1)})${color}${metaGroup(e)}`;
51
+ case 'frame':
52
+ return (`[frame #${e.id}] ${pos} @size(${e.w.toFixed(1)},${e.h.toFixed(1)})` +
53
+ quotedAttr('text', e.text ?? '', DSL_MAX_TEXT_LEN) +
54
+ color +
55
+ metaGroup(e));
56
+ case 'text':
57
+ return (`[text #${e.id}] ${pos}${quotedAttr('text', e.text ?? '', DSL_MAX_TEXT_LEN)}${color}${metaGroup(e)}${metaCompute(e)}`);
58
+ case 'arrow': {
59
+ // Shared relation signature (label/color/dash/arrowhead/route).
60
+ // route 只在非 straight(或显式设过)时输出,保向后兼容(旧直线箭头无 @route)。
61
+ // route + 对应数据(curve / elbow)按 route 输出,三者同源见 arrowRoute。
62
+ const routeAttr = e.route === 'curve' || e.route === 'elbow' || e.route === 'straight'
63
+ ? ` @route(${e.route})`
64
+ : '';
65
+ const elbowAttr = e.elbow && e.elbow.length > 0
66
+ ? ` @elbow(${e.elbow.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(';')})`
67
+ : '';
68
+ const sig = quotedAttr('label', e.text ?? undefined, DSL_MAX_TEXT_LEN) +
69
+ color +
70
+ (e.dash ? ` @dash(${e.dash})` : '') +
71
+ (e.arrowhead ? ` @arrowhead(${e.arrowhead})` : '') +
72
+ (e.curve ? ` @curve(${e.curve.cx.toFixed(1)},${e.curve.cy.toFixed(1)})` : '') +
73
+ routeAttr +
74
+ elbowAttr +
75
+ (e.meta?.wikilink === true ? ' @wikilink' : '');
76
+ if (e.from && e.to) {
77
+ // Relation arrow: endpoint references.
78
+ return `[arrow #${e.id}] from #${e.from} to #${e.to}${sig}`;
79
+ }
80
+ // Free arrow: bbox encodes the segment (w/h may be negative for direction).
81
+ return `[arrow #${e.id}] ${pos} @size(${e.w.toFixed(1)},${e.h.toFixed(1)})${sig}`;
82
+ }
83
+ case 'freedraw':
84
+ // Position only — never the point sequence (R2 + privacy)。注意:freedraw 已出 DSL 契约
85
+ // (serializeCanvas 按 DSL_KINDS 过滤,不 emit)。本 case 仅供**直接调用方**(如 AI snapshot,
86
+ // 它是单向上下文格式、非往返 DSL)用;parseDsl 不接受 `[freedraw]`(→ unrecognized)。
87
+ return `[freedraw #${e.id}] ${pos}`;
88
+ default:
89
+ // ellipse / line / note / image (legacy) — not in the DSL。
90
+ return '';
91
+ }
92
+ }
93
+ /** Escape a string for a quoted DSL value:\\ = backslash, \" = quote, \n = newline(v5,@content 多行),
94
+ * \` = backtick(v8,@code/@quote 内容里的 ``` 不提前闭合 AI prompt 的 markdown 围栏)。
95
+ * 顺序:先 \ (防后续插入的 \ 被二次转义),再 ",再换行,再反引号。是 dsl-parser unescapeQuoted 的逆
96
+ * (unescapeQuoted 的 \X→X 通用,天然对称)。 */
97
+ function escapeQuoted(s) {
98
+ return s
99
+ .replace(/\\/g, '\\\\')
100
+ .replace(/"/g, '\\"')
101
+ .replace(/\n/g, '\\n')
102
+ .replace(/`/g, '\\`');
103
+ }
104
+ /** 带长度上限的引号属性 emit(2026-08-29 P1):值为空/undefined → '';超上限 → ''
105
+ * (**不 emit**,防 parser 静默截断后经 Apply 写回毁数据,见 serializeElement card case 注释);
106
+ * 正常值 → ` @name("escaped")`。 */
107
+ function quotedAttr(name, value, max) {
108
+ if (typeof value !== 'string' || value === '' || value.length > max)
109
+ return '';
110
+ return ` @${name}("${escapeQuoted(value)}")`;
111
+ }
112
+ /** v7:读 element.meta.group(语义分组名)。非字符串 / 空 → 不 emit。
113
+ * 超长(>DSL_MAX_TEXT_LEN)不 emit:parse 侧 truncate 会截断(见 quotedAttr 注释)。 */
114
+ function metaGroup(e) {
115
+ const g = e.meta?.group;
116
+ return quotedAttr('group', typeof g === 'string' ? g : undefined, DSL_MAX_TEXT_LEN);
117
+ }
118
+ /** v7:读 element.meta.href(卡片显式语义引用目标 id 列表)。非数组 / 空 → 不 emit。
119
+ * emit 形如 ` @href(#a;#b)`(每个 id 补 `#`,`;` 分隔),是 parseHref 的逆。
120
+ * 超上限(>DSL_MAX_HREF_TARGETS)截到上限——parse 侧同样截断,**两侧同界**,
121
+ * round-trip 稳定(与文本字段不同:截尾丢的是列表项,parse 后仍可写回稳定子集,
122
+ * 不会造成「serialize 全量 → parse 截断 → 写回」的不对称丢失)。 */
123
+ function metaHref(e) {
124
+ const h = e.meta?.href;
125
+ if (!Array.isArray(h))
126
+ return '';
127
+ const ids = h
128
+ .filter((x) => typeof x === 'string' && x !== '')
129
+ .slice(0, DSL_MAX_HREF_TARGETS);
130
+ if (ids.length === 0)
131
+ return '';
132
+ return ` @href(${ids.map((id) => '#' + id).join(';')})`;
133
+ }
134
+ /** v7:读 element.meta.compute(text 元素安全公式原文)。非字符串 / 空 → 不 emit。
135
+ * 超长不 emit(同 metaGroup 理由)。 */
136
+ function metaCompute(e) {
137
+ const c = e.meta?.compute;
138
+ return quotedAttr('compute', typeof c === 'string' ? c : undefined, DSL_MAX_TEXT_LEN);
139
+ }
140
+ /** v8:读注入的卡片 type(语义类型)。缺省 / 空 → 不 emit。CardType 枚举值(note|image|link|code|quote)
141
+ * 为安全 token,无需转义。image 卡照样 emit type(media 另排除,不进 DSL)。 */
142
+ function metaType(c) {
143
+ return c?.type ? ` @type(${c.type})` : '';
144
+ }
145
+ /** v8 list 值编码:encodeURIComponent 之外再编码 `(` `)`——grammar 用 `[^)]+` 捕获 @tags/@links
146
+ * (dsl.peggy tagsDir/linksDir),裸 `)` 会提前闭合指令并**级联吞掉后续项**(如 Wikipedia
147
+ * `Cat_(disambiguation)` URL 丢尾 `)`→ 404,其后所有项被当残留丢弃)。parse 侧
148
+ * decodeURIComponent(dsl-parser.ts parseTagList/parseLinkList)自动反向,无需改 grammar。 */
149
+ function encodeListValue(v) {
150
+ try {
151
+ return encodeURIComponent(v).replace(/\(/g, '%28').replace(/\)/g, '%29');
152
+ }
153
+ catch {
154
+ // 孤立代理对(非法 Unicode,来自异常切片而非正常输入)会让 encodeURIComponent
155
+ // 抛 URIError,进而**整张画布**导出 / AI 快照崩。降级:替换孤立代理为 U+FFFD
156
+ // 再编码,保导出可用(非法输入本就不要求逐字节往返)。
157
+ return encodeURIComponent(v.replace(/[\ud800-\udfff]/g, '�')).replace(/\(/g, '%28').replace(/\)/g, '%29');
158
+ }
159
+ }
160
+ /** v8:读注入的卡片 tags。value 过滤空 → 各 encodeListValue(防 `;` 分隔符碰撞 + `)` 提前闭合)→ `;` 连接。
161
+ * 无有效 tag → 不 emit。是 parseTagList 的逆。
162
+ * 超数防护(2026-08-29 P1):截到 DSL_MAX_TAG_COUNT —— 与 parse 侧同界,round-trip 稳定。 */
163
+ function metaTags(c) {
164
+ const values = (c?.tags ?? [])
165
+ .map((t) => (typeof t?.value === 'string' ? t.value.trim() : ''))
166
+ .filter((v) => v !== '')
167
+ .slice(0, DSL_MAX_TAG_COUNT);
168
+ return values.length > 0 ? ` @tags(${values.map(encodeListValue).join(';')})` : '';
169
+ }
170
+ /** v8:读注入的卡片 links(仅 URL,见 CardDslContent 说明)。url 过滤空 → 各 encodeListValue
171
+ * (URL 含 `;`/`=`/`&` 会撞分隔符;含 `)` 如 Wikipedia 消歧义页会提前闭合)→ `;` 连接。
172
+ * 无有效 url → 不 emit。是 parseLinkList 的逆。超数防护同 metaTags(DSL_MAX_LINK_COUNT)。 */
173
+ function metaLinks(c) {
174
+ const urls = (c?.links ?? [])
175
+ .map((l) => (typeof l?.url === 'string' ? l.url.trim() : ''))
176
+ .filter((u) => u !== '')
177
+ .slice(0, DSL_MAX_LINK_COUNT);
178
+ return urls.length > 0 ? ` @links(${urls.map(encodeListValue).join(';')})` : '';
179
+ }
180
+ /** v8:读注入的卡片 codeSnippets。每个 emit 一条 ` @code(lang,"code"[,"caption"])`(可重复指令)。
181
+ * lang 收窄到 grammar codeLang 字符集(越界字符丢弃,防破坏指令;空语言合法)。code/caption 走
182
+ * escapeQuoted(多行/引号/反引号)。是 fold code 累积的逆。
183
+ * 超长防护(2026-08-29 P1):code 超 DSL_MAX_CONTENT_LEN / 块数超 DSL_MAX_CODE_BLOCKS
184
+ * 的条目不 emit —— parse 侧会截断/截数,照常 emit 会经 Apply 写回截断值(同 quotedAttr 理由)。 */
185
+ function metaCode(c) {
186
+ return (c?.codeSnippets ?? [])
187
+ .filter((b) => typeof b?.code === 'string' && b.code !== '' && b.code.length <= DSL_MAX_CONTENT_LEN)
188
+ .slice(0, DSL_MAX_CODE_BLOCKS)
189
+ .map((b) => {
190
+ const lang = (typeof b.language === 'string' ? b.language : '').replace(/[^a-zA-Z0-9_+#.-]/g, '');
191
+ const caption = typeof b.caption === 'string' && b.caption !== '' ? `,"${escapeQuoted(b.caption)}"` : '';
192
+ return ` @code(${lang},"${escapeQuoted(b.code)}"${caption})`;
193
+ })
194
+ .join('');
195
+ }
196
+ /** v8:读注入的卡片 quotes。每个 emit 一条 ` @quote("text"[,"attribution"[,"sourceUrl"]])`(可重复)。
197
+ * arity:有 sourceUrl → 三参(attribution 缺省补空串占位);否则有 attribution → 两参;否则一参。
198
+ * 各值走 escapeQuoted。是 fold quote 累积的逆(空串占位 parse 侧归一 undefined)。
199
+ * 超长/超数防护同 metaCode(2026-08-29 P1)。 */
200
+ function metaQuote(c) {
201
+ return (c?.quotes ?? [])
202
+ .filter((q) => typeof q?.text === 'string' && q.text !== '' && q.text.length <= DSL_MAX_CONTENT_LEN)
203
+ .slice(0, DSL_MAX_QUOTES)
204
+ .map((q) => {
205
+ const text = `"${escapeQuoted(q.text)}"`;
206
+ const by = typeof q.attribution === 'string' ? q.attribution : '';
207
+ const url = typeof q.sourceUrl === 'string' ? q.sourceUrl : '';
208
+ if (url !== '')
209
+ return ` @quote(${text},"${escapeQuoted(by)}","${escapeQuoted(url)}")`;
210
+ if (by !== '')
211
+ return ` @quote(${text},"${escapeQuoted(by)}")`;
212
+ return ` @quote(${text})`;
213
+ })
214
+ .join('');
215
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /** CLI 结果:stdout(报告/JSON)与 stderr(警告)分流,code 即进程退出码。 */
3
+ export type CliResult = {
4
+ code: 0 | 1;
5
+ stdout: string;
6
+ stderr: string;
7
+ };
8
+ export declare function validateText(text: string): CliResult;
9
+ export declare function renderText(text: string, opts?: {
10
+ svg?: boolean;
11
+ }): CliResult;
12
+ export declare function roundtripText(text: string): CliResult;
13
+ export declare function main(argv: readonly string[]): number;