justskill-design 0.0.1 → 3.2.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/README.en.md +235 -0
- package/README.md +184 -11
- package/SKILL.md +435 -0
- package/canvas/app.mjs +2870 -0
- package/canvas/groups.mjs +204 -0
- package/canvas/index.html +141 -0
- package/canvas/styles.css +1032 -0
- package/canvas/tree.mjs +249 -0
- package/cordis.patch.yml +8 -0
- package/design.mjs +172 -0
- package/docs/dsh-workbench.png +0 -0
- package/dsh/client.js +864 -0
- package/dsh/index.js +354 -0
- package/dsh/scan.js +139 -0
- package/dsh/skill.js +41 -0
- package/lib/base-css.mjs +92 -0
- package/lib/detect.mjs +123 -0
- package/lib/diagnostics.mjs +382 -0
- package/lib/export-html.mjs +309 -0
- package/lib/export-svg.mjs +111 -0
- package/lib/fragment.mjs +702 -0
- package/lib/geometry.mjs +827 -0
- package/lib/icons.mjs +106 -0
- package/lib/levenshtein.mjs +133 -0
- package/lib/node/cli.mjs +387 -0
- package/lib/node/export-svg.mjs +75 -0
- package/lib/node/fs.mjs +309 -0
- package/lib/node/op-json.mjs +75 -0
- package/lib/node/raster-run.mjs +115 -0
- package/lib/node/server.mjs +1264 -0
- package/lib/node/static.mjs +206 -0
- package/lib/node/verbs/add.mjs +220 -0
- package/lib/node/verbs/export.mjs +161 -0
- package/lib/node/verbs/format.mjs +106 -0
- package/lib/node/verbs/import.mjs +126 -0
- package/lib/node/verbs/ls.mjs +98 -0
- package/lib/node/verbs/migrate.mjs +236 -0
- package/lib/node/verbs/move.mjs +76 -0
- package/lib/node/verbs/new.mjs +284 -0
- package/lib/node/verbs/remove.mjs +41 -0
- package/lib/node/verbs/rename.mjs +87 -0
- package/lib/node/verbs/serve.mjs +164 -0
- package/lib/node/verbs/set.mjs +150 -0
- package/lib/node/verbs/show.mjs +122 -0
- package/lib/node/verbs/tokens.mjs +167 -0
- package/lib/node/verbs/validate.mjs +130 -0
- package/lib/node/write-verbs.mjs +215 -0
- package/lib/normalize.mjs +288 -0
- package/lib/ops.mjs +2063 -0
- package/lib/parse.mjs +997 -0
- package/lib/raster.mjs +186 -0
- package/lib/readers/pen.mjs +1684 -0
- package/lib/receipt.mjs +203 -0
- package/lib/render.mjs +503 -0
- package/lib/serialize.mjs +441 -0
- package/lib/spans.mjs +177 -0
- package/lib/svg-path.mjs +483 -0
- package/lib/table.mjs +1047 -0
- package/lib/tokens.mjs +295 -0
- package/lib/validate.mjs +929 -0
- package/package.json +68 -6
- package/references/cookbook.md +156 -0
- package/references/images.md +171 -0
- package/references/product-flow.md +408 -0
- package/references/schema.md +90 -0
- package/references/styling.md +181 -0
- package/scripts/install.mjs +95 -0
package/lib/receipt.mjs
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/receipt.mjs — what the user is told after an edit.
|
|
3
|
+
*
|
|
4
|
+
* PURE ESM. No `node:` imports, no DOM, no dependencies.
|
|
5
|
+
*
|
|
6
|
+
* A receipt is the machine-readable half of a verb's result; `formatReceipt` is the
|
|
7
|
+
* half a person reads. Both come from the same object, so the panel, the JSON output
|
|
8
|
+
* and the terminal can never disagree about what happened.
|
|
9
|
+
*
|
|
10
|
+
* ## The invariant this file exists to keep
|
|
11
|
+
*
|
|
12
|
+
* **Zero silent drops.** Every declaration the model sent is either consumed into the
|
|
13
|
+
* file or listed in `dropped` with a reason. A receipt that says `ok: true` and is
|
|
14
|
+
* quiet about a declaration is a bug, not a formatting choice.
|
|
15
|
+
*
|
|
16
|
+
* ## Assumptions where docs/format-v3.md is silent
|
|
17
|
+
*
|
|
18
|
+
* R1. `ok` is false when the caller refused, or when any diagnostic has severity
|
|
19
|
+
* `error`. Warnings do not make a receipt fail.
|
|
20
|
+
* R2. A failure carries a `refused` block — `{code, message, hint, suggestion}` — so
|
|
21
|
+
* the human text can print the reason and the one-line fix. `buildReceipt` takes
|
|
22
|
+
* it from the caller, or builds it from the first error diagnostic. A successful
|
|
23
|
+
* receipt has exactly the nine documented keys.
|
|
24
|
+
* R3. `changed` defaults to `applied.length > 0`. The caller that knows whether the
|
|
25
|
+
* bytes moved (it compares hashes) should pass it.
|
|
26
|
+
* R4. The human text is English except for the two lines the product fixes:
|
|
27
|
+
* `REFUSED` and `文件未改动。` — the promise that a refusal left the file alone is
|
|
28
|
+
* printed in the language the file's owner reads.
|
|
29
|
+
* R5. Lines appear in this order and only when non-empty: added, normalized, dropped,
|
|
30
|
+
* applied-op count with revision, then diagnostics. `applied` is printed when at
|
|
31
|
+
* least one op ran, or when the caller reports the file changed.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** Labels are padded so every value starts in the same column. */
|
|
35
|
+
const LABEL = 11;
|
|
36
|
+
|
|
37
|
+
const DIM = "\u001b[2m";
|
|
38
|
+
const RESET = "\u001b[0m";
|
|
39
|
+
const RED = "\u001b[31m";
|
|
40
|
+
const YELLOW = "\u001b[33m";
|
|
41
|
+
|
|
42
|
+
function list(value) {
|
|
43
|
+
return Array.isArray(value) ? value : [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build the receipt object.
|
|
48
|
+
*
|
|
49
|
+
* @param {{verb?: string, applied?: object[], added?: string[], normalized?: object[],
|
|
50
|
+
* dropped?: object[], diagnostics?: object[], revision?: string|null,
|
|
51
|
+
* changed?: boolean, refused?: object|null, error?: object|null}} input
|
|
52
|
+
*/
|
|
53
|
+
export function buildReceipt(input = {}) {
|
|
54
|
+
const source = input && typeof input === "object" ? input : {};
|
|
55
|
+
const applied = list(source.applied);
|
|
56
|
+
const diagnostics = list(source.diagnostics);
|
|
57
|
+
const errors = diagnostics.filter((entry) => entry && entry.severity === "error");
|
|
58
|
+
|
|
59
|
+
let refused = source.refused ?? null;
|
|
60
|
+
if (refused === null && source.error) refused = source.error;
|
|
61
|
+
if (refused === null && errors.length > 0) {
|
|
62
|
+
const first = errors[0];
|
|
63
|
+
refused = {
|
|
64
|
+
code: first.code ?? null,
|
|
65
|
+
message: first.message ?? "",
|
|
66
|
+
hint: first.hint ?? null,
|
|
67
|
+
suggestion: first.fix ?? null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const receipt = {
|
|
72
|
+
ok: refused === null && errors.length === 0,
|
|
73
|
+
verb: typeof source.verb === "string" && source.verb !== "" ? source.verb : null,
|
|
74
|
+
applied,
|
|
75
|
+
added: list(source.added),
|
|
76
|
+
normalized: list(source.normalized),
|
|
77
|
+
dropped: list(source.dropped),
|
|
78
|
+
diagnostics,
|
|
79
|
+
revision: typeof source.revision === "string" && source.revision !== "" ? source.revision : null,
|
|
80
|
+
changed: typeof source.changed === "boolean" ? source.changed : applied.length > 0,
|
|
81
|
+
};
|
|
82
|
+
if (refused !== null) {
|
|
83
|
+
receipt.refused = {
|
|
84
|
+
code: refused.code ?? null,
|
|
85
|
+
message: refused.message ?? "",
|
|
86
|
+
hint: refused.hint ?? null,
|
|
87
|
+
suggestion: refused.suggestion ?? null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return receipt;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function paint(text, code, color) {
|
|
94
|
+
return color ? `${code}${text}${RESET}` : text;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function line(label, value) {
|
|
98
|
+
return `${label.padEnd(LABEL)} ${value}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizedText(entry) {
|
|
102
|
+
const target = entry?.target ?? "?";
|
|
103
|
+
const prop = entry?.prop ?? "?";
|
|
104
|
+
const from = entry?.from ?? "";
|
|
105
|
+
const to = entry?.to ?? "";
|
|
106
|
+
const name = entry?.alias ? `${target}.${entry.alias} → ${prop}` : `${target}.${prop}`;
|
|
107
|
+
if (from === to && entry?.alias) return name;
|
|
108
|
+
return `${name} ${from} → ${to}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function droppedText(entry) {
|
|
112
|
+
const target = entry?.target ?? "?";
|
|
113
|
+
const prop = entry?.prop ?? "?";
|
|
114
|
+
const value = entry?.value ?? "";
|
|
115
|
+
const reason = entry?.reason ?? "dropped";
|
|
116
|
+
return `${target}.${prop}${value === "" ? "" : ` ${value}`} (${reason})`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function diagnosticText(entry) {
|
|
120
|
+
const severity = entry?.severity === "error" ? "error" : "warning";
|
|
121
|
+
const code = entry?.code ?? "?";
|
|
122
|
+
const target = entry?.target ? ` ${entry.target}` : "";
|
|
123
|
+
return `${severity} ${code}${target} ${entry?.message ?? ""}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Render a receipt as text.
|
|
128
|
+
*
|
|
129
|
+
* @param {object} receipt from `buildReceipt`
|
|
130
|
+
* @param {{color?: boolean, maxItems?: number}} [options]
|
|
131
|
+
* @returns {string} text, newline-terminated
|
|
132
|
+
*/
|
|
133
|
+
export function formatReceipt(receipt, options = {}) {
|
|
134
|
+
const source = receipt && typeof receipt === "object" ? receipt : {};
|
|
135
|
+
const color = options.color === true;
|
|
136
|
+
const maxItems = Number.isInteger(options.maxItems) && options.maxItems > 0 ? options.maxItems : 20;
|
|
137
|
+
const lines = [];
|
|
138
|
+
|
|
139
|
+
const overflow = (count) => ` … and ${count} more`;
|
|
140
|
+
|
|
141
|
+
if (!source.ok) {
|
|
142
|
+
const reason = source.refused ?? {
|
|
143
|
+
code: null,
|
|
144
|
+
message: (list(source.diagnostics).find((entry) => entry?.severity === "error") ?? {}).message ?? "The change was refused.",
|
|
145
|
+
hint: null,
|
|
146
|
+
suggestion: null,
|
|
147
|
+
};
|
|
148
|
+
lines.push(`${paint("REFUSED", RED, color)} ${reason.message}`);
|
|
149
|
+
if (reason.hint) lines.push(` hint: ${reason.hint}`);
|
|
150
|
+
if (reason.suggestion) lines.push(` 建议: ${reason.suggestion}`);
|
|
151
|
+
const rest = list(source.diagnostics).filter(
|
|
152
|
+
(entry) => entry && entry.message !== reason.message,
|
|
153
|
+
);
|
|
154
|
+
for (const entry of rest) {
|
|
155
|
+
lines.push(` ${paint(diagnosticText(entry), entry.severity === "error" ? RED : YELLOW, color)}`);
|
|
156
|
+
}
|
|
157
|
+
lines.push("文件未改动。");
|
|
158
|
+
return `${lines.join("\n")}\n`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const added = list(source.added);
|
|
162
|
+
if (added.length > 0) {
|
|
163
|
+
const shown = added.slice(0, maxItems);
|
|
164
|
+
lines.push(line("added", shown.join(", ")));
|
|
165
|
+
if (added.length > shown.length) lines.push(overflow(added.length - shown.length));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const normalized = list(source.normalized);
|
|
169
|
+
if (normalized.length > 0) {
|
|
170
|
+
const shown = normalized.slice(0, maxItems);
|
|
171
|
+
lines.push(line("normalized", normalizedText(shown[0])));
|
|
172
|
+
for (const entry of shown.slice(1)) lines.push(` ${normalizedText(entry)}`);
|
|
173
|
+
if (normalized.length > shown.length) lines.push(overflow(normalized.length - shown.length));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const dropped = list(source.dropped);
|
|
177
|
+
if (dropped.length > 0) {
|
|
178
|
+
const shown = dropped.slice(0, maxItems);
|
|
179
|
+
lines.push(line("dropped", droppedText(shown[0])));
|
|
180
|
+
for (const entry of shown.slice(1)) lines.push(` ${droppedText(entry)}`);
|
|
181
|
+
if (dropped.length > shown.length) lines.push(overflow(dropped.length - shown.length));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const applied = list(source.applied);
|
|
185
|
+
if (applied.length > 0 || source.changed === true) {
|
|
186
|
+
const parts = [`${applied.length} op${applied.length === 1 ? "" : "s"}`];
|
|
187
|
+
if (source.revision) parts.push(`revision ${source.revision}`);
|
|
188
|
+
if (source.changed === false) parts.push("no change");
|
|
189
|
+
lines.push(line("applied", parts.join(" · ")));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const diagnostics = list(source.diagnostics);
|
|
193
|
+
const ordered = [
|
|
194
|
+
...diagnostics.filter((entry) => entry?.severity === "error"),
|
|
195
|
+
...diagnostics.filter((entry) => entry?.severity !== "error"),
|
|
196
|
+
];
|
|
197
|
+
for (const entry of ordered.slice(0, maxItems)) {
|
|
198
|
+
lines.push(paint(diagnosticText(entry), entry.severity === "error" ? RED : YELLOW, color));
|
|
199
|
+
}
|
|
200
|
+
if (ordered.length > maxItems) lines.push(overflow(ordered.length - maxItems));
|
|
201
|
+
|
|
202
|
+
return lines.length === 0 ? "" : `${lines.join("\n")}\n`;
|
|
203
|
+
}
|