no-fi-skill 0.2.1 → 0.3.1
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.md +46 -33
- package/bin/no-fi-skill.mjs +1 -1
- package/package.json +2 -2
- package/{no-fi → plugin/skills/no-fi}/SKILL.md +10 -19
- package/{no-fi → plugin/skills/no-fi}/examples/spending-report.txt +18 -10
- package/{no-fi → plugin/skills/no-fi}/scripts/check_wireframe.mjs +29 -10
- package/{no-fi → plugin/skills/no-fi}/scripts/check_wireframe.py +28 -10
- package/{no-fi → plugin/skills/no-fi}/vocabulary.json +9 -9
- package/no-fi/scripts/check_coverage.mjs +0 -73
- package/no-fi/scripts/check_coverage.py +0 -75
- package/no-fi/scripts/sync_vocabulary.py +0 -57
- /package/{no-fi → plugin/skills/no-fi}/examples/activity-patterns.txt +0 -0
- /package/{no-fi → plugin/skills/no-fi}/examples/card-details.txt +0 -0
- /package/{no-fi → plugin/skills/no-fi}/examples/fintech-dashboard.txt +0 -0
- /package/{no-fi → plugin/skills/no-fi}/examples/send-money-form.txt +0 -0
- /package/{no-fi → plugin/skills/no-fi}/examples/settings-notifications.txt +0 -0
- /package/{no-fi → plugin/skills/no-fi}/examples/transactions-table.txt +0 -0
package/README.md
CHANGED
|
@@ -39,7 +39,11 @@ The outer frame is double-lined, every box inside it is single-lined, and the pr
|
|
|
39
39
|
|
|
40
40
|
## Install
|
|
41
41
|
|
|
42
|
-
Pick one. Each puts the `no-fi
|
|
42
|
+
Pick one. Each puts the `no-fi` skill folder where your agent loads skills from.
|
|
43
|
+
|
|
44
|
+
**With a prompt**
|
|
45
|
+
|
|
46
|
+
Copy the install prompt from [no-fi.dev](https://no-fi.dev/#get-started) and paste it into Claude Code or any agent. It runs the npx command below, or downloads the files one by one if it can't run npx.
|
|
43
47
|
|
|
44
48
|
**With npx**
|
|
45
49
|
|
|
@@ -59,18 +63,18 @@ npx skills add p-a-u-l-i-e/no-fi --skill no-fi
|
|
|
59
63
|
|
|
60
64
|
```sh
|
|
61
65
|
git clone https://github.com/p-a-u-l-i-e/no-fi.git
|
|
62
|
-
cp -R no-fi/no-fi ~/.claude/skills/
|
|
66
|
+
cp -R no-fi/plugin/skills/no-fi ~/.claude/skills/
|
|
63
67
|
```
|
|
64
68
|
|
|
65
69
|
**On claude.ai**
|
|
66
70
|
|
|
67
|
-
Download
|
|
71
|
+
Download [no-fi.zip](https://no-fi.dev/no-fi.zip) and upload it under Settings, Capabilities, Skills. It holds only the skill folder. GitHub's Download ZIP holds the whole repository, which claude.ai won't take as a skill.
|
|
68
72
|
|
|
69
|
-
The skill is plain text
|
|
73
|
+
The skill is plain text plus one small checker script, in Node and Python, so you can read every file before you install it.
|
|
70
74
|
|
|
71
75
|
## Use it
|
|
72
76
|
|
|
73
|
-
Ask for a screen the way you would ask a designer for a sketch
|
|
77
|
+
Ask for a screen the way you would ask a designer for a sketch. Saying wireframe or sketch is enough to load the skill, or type `/no-fi` in Claude Code.
|
|
74
78
|
|
|
75
79
|
> Wireframe a send-money form with three steps.
|
|
76
80
|
>
|
|
@@ -82,7 +86,7 @@ Claude replies with a fenced code block, runs the checker on it, and adds a note
|
|
|
82
86
|
|
|
83
87
|
## The rules
|
|
84
88
|
|
|
85
|
-
Seven rules keep every wireframe readable. The full text is in [`
|
|
89
|
+
Seven rules keep every wireframe readable. The full text is in [`SKILL.md`](plugin/skills/no-fi/SKILL.md).
|
|
86
90
|
|
|
87
91
|
1. **Plain characters, plus box-drawing.** Printable ASCII, box-drawing borders, and block characters for bars and charts. No emoji, no tabs.
|
|
88
92
|
2. **Double outside, single inside.** The outermost container uses double lines. Every box inside it uses single lines.
|
|
@@ -102,56 +106,65 @@ Seven rules keep every wireframe readable. The full text is in [`no-fi/SKILL.md`
|
|
|
102
106
|
- Open states are drawn open: a **select** with its menu below, an **accordion** with its content under it.
|
|
103
107
|
- **Modals**, **toasts**, and **tooltips** are cards drawn inside the screen they sit on.
|
|
104
108
|
|
|
105
|
-
The complete table, with a drawing and a note for each element, is in [`
|
|
109
|
+
The complete table, with a drawing and a note for each element, is in [`SKILL.md`](plugin/skills/no-fi/SKILL.md). The same data is in [`vocabulary.json`](plugin/skills/no-fi/vocabulary.json).
|
|
106
110
|
|
|
107
111
|
## Examples
|
|
108
112
|
|
|
109
|
-
Seven finished screens in [`
|
|
113
|
+
Seven finished screens in [`examples/`](plugin/skills/no-fi/examples), built to use the whole vocabulary:
|
|
110
114
|
|
|
111
115
|
| Screen | File |
|
|
112
116
|
| --- | --- |
|
|
113
|
-
| Fintech dashboard | [`fintech-dashboard.txt`](no-fi/examples/fintech-dashboard.txt) |
|
|
114
|
-
| Send money form | [`send-money-form.txt`](no-fi/examples/send-money-form.txt) |
|
|
115
|
-
| Transactions table | [`transactions-table.txt`](no-fi/examples/transactions-table.txt) |
|
|
116
|
-
| Spending report | [`spending-report.txt`](no-fi/examples/spending-report.txt) |
|
|
117
|
-
| Card details | [`card-details.txt`](no-fi/examples/card-details.txt) |
|
|
118
|
-
| Notification settings | [`settings-notifications.txt`](no-fi/examples/settings-notifications.txt) |
|
|
119
|
-
| Activity patterns | [`activity-patterns.txt`](no-fi/examples/activity-patterns.txt) |
|
|
117
|
+
| Fintech dashboard | [`fintech-dashboard.txt`](plugin/skills/no-fi/examples/fintech-dashboard.txt) |
|
|
118
|
+
| Send money form | [`send-money-form.txt`](plugin/skills/no-fi/examples/send-money-form.txt) |
|
|
119
|
+
| Transactions table | [`transactions-table.txt`](plugin/skills/no-fi/examples/transactions-table.txt) |
|
|
120
|
+
| Spending report | [`spending-report.txt`](plugin/skills/no-fi/examples/spending-report.txt) |
|
|
121
|
+
| Card details | [`card-details.txt`](plugin/skills/no-fi/examples/card-details.txt) |
|
|
122
|
+
| Notification settings | [`settings-notifications.txt`](plugin/skills/no-fi/examples/settings-notifications.txt) |
|
|
123
|
+
| Activity patterns | [`activity-patterns.txt`](plugin/skills/no-fi/examples/activity-patterns.txt) |
|
|
120
124
|
|
|
121
125
|
## Check your work
|
|
122
126
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
**Check a wireframe**
|
|
127
|
+
Your agent checks every wireframe before it shows you one. To check one yourself, from anywhere:
|
|
126
128
|
|
|
127
129
|
```sh
|
|
128
|
-
node no-fi/scripts/check_wireframe.mjs wireframe.txt
|
|
129
|
-
python3 no-fi/scripts/check_wireframe.py wireframe.txt
|
|
130
130
|
npx no-fi-skill check wireframe.txt
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
The checker also ships with the skill, in Node and Python with the same output. From an install, or from this repository:
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
node ~/.claude/skills/no-fi/scripts/check_wireframe.mjs wireframe.txt
|
|
137
|
+
python3 plugin/skills/no-fi/scripts/check_wireframe.py wireframe.txt
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
It fails on unsupported characters, tabs, ASCII box borders, boxes that don't close or line up, mixed double and single lines in one box, a double-line box nested inside another box, table dividers that don't land in the same column, progress bars whose fill disagrees with their percentage, bar chart rows whose length disagrees with their value, heatmaps and stacked columns without a legend, and column charts that don't say what a row is worth.
|
|
141
|
+
|
|
142
|
+
## Maintaining the vocabulary
|
|
134
143
|
|
|
135
|
-
|
|
144
|
+
`plugin/skills/no-fi/vocabulary.json` is the source of truth. These scripts live in `scripts/`, outside the skill, so they don't ship to users. After editing the vocabulary, run:
|
|
136
145
|
|
|
137
146
|
```sh
|
|
138
|
-
|
|
139
|
-
|
|
147
|
+
python3 scripts/sync_vocabulary.py # rewrite the tables in SKILL.md
|
|
148
|
+
node scripts/check_coverage.mjs # every element appears in an example
|
|
149
|
+
node plugin/skills/no-fi/scripts/check_wireframe.mjs plugin/skills/no-fi/examples/*.txt
|
|
140
150
|
```
|
|
141
151
|
|
|
142
|
-
|
|
152
|
+
`python3 scripts/check_coverage.py` gives the same coverage output without Node.
|
|
143
153
|
|
|
144
154
|
## Repository layout
|
|
145
155
|
|
|
146
156
|
```
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
plugin/ the Claude plugin
|
|
158
|
+
.claude-plugin/ plugin.json
|
|
159
|
+
skills/no-fi/ the skill
|
|
160
|
+
SKILL.md rules and vocabulary tables
|
|
161
|
+
vocabulary.json the element vocabulary as data
|
|
162
|
+
examples/ finished example screens
|
|
163
|
+
scripts/ the wireframe checker
|
|
164
|
+
scripts/ maintainer scripts: vocabulary sync and coverage
|
|
165
|
+
bin/no-fi-skill.mjs the npx no-fi-skill command
|
|
166
|
+
assets/ README images
|
|
167
|
+
site/ the no-fi.dev site
|
|
155
168
|
```
|
|
156
169
|
|
|
157
170
|
## License
|
package/bin/no-fi-skill.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import path from "node:path";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
|
|
11
11
|
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
|
-
const skill = path.join(root, "no-fi");
|
|
12
|
+
const skill = path.join(root, "plugin", "skills", "no-fi");
|
|
13
13
|
const { version } = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
14
14
|
|
|
15
15
|
const usage = `no-fi ${version}: wireframes without the distraction.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "no-fi-skill",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "An agent skill that draws UI wireframes and charts as plain text, so teams discuss layout and flow instead of color and type.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wireframe",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"bin",
|
|
28
|
-
"no-fi",
|
|
28
|
+
"plugin/skills/no-fi",
|
|
29
29
|
"!**/__pycache__",
|
|
30
30
|
"!**/.DS_Store"
|
|
31
31
|
],
|
|
@@ -9,7 +9,7 @@ Draw screens as monospaced text. The goal is to make the conversation about stru
|
|
|
9
9
|
|
|
10
10
|
## Rules
|
|
11
11
|
|
|
12
|
-
1. **Plain characters, plus box-drawing.** Printable ASCII, the box-drawing characters used for borders (`╔ ═ ╗ ║ ╚ ╝` and `┌ ─ ┐ │ └ ┘ ├ ┤ ┬ ┴ ┼`), and block characters for progress bars and charts (`▮ ▯ █ ▒ ░ ▓`, the horizontal eighths `▏▎▍▌▋▊▉`, and the vertical steps `▁▂▃▄▅▆▇`). No emoji, no tabs.
|
|
12
|
+
1. **Plain characters, plus box-drawing.** Printable ASCII, the box-drawing characters used for borders (`╔ ═ ╗ ║ ╚ ╝` and `┌ ─ ┐ │ └ ┘ ├ ┤ ┬ ┴ ┼`), and block characters for progress bars and charts (`▮ ▯ █ ▒ ░ ▓`, the horizontal eighths `▏▎▍▌▋▊▉`, and the vertical steps `▁▂▃▄▅▆▇`), and the rounded corners `╭ ╮ ╰ ╯` where a line chart turns. No emoji, no tabs.
|
|
13
13
|
2. **Double outside, single inside.** The outermost container is drawn with double lines `╔═╗`. Every box inside it uses single lines `┌─┐`.
|
|
14
14
|
3. **Fixed width.** Keep every line at 80 characters or fewer. Every box is exactly as wide on every row.
|
|
15
15
|
4. **Code block.** Always wrap the output in a fenced block with no language tag, so monospacing and alignment hold.
|
|
@@ -18,12 +18,12 @@ Draw screens as monospaced text. The goal is to make the conversation about stru
|
|
|
18
18
|
7. **Validate.** Run the checker before showing the result:
|
|
19
19
|
|
|
20
20
|
```sh
|
|
21
|
-
node
|
|
21
|
+
node ${CLAUDE_SKILL_DIR}/scripts/check_wireframe.mjs path/to/wireframe.txt
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Without Node, `python3
|
|
24
|
+
`${CLAUDE_SKILL_DIR}` is the folder this file is in. Without Node, `python3 ${CLAUDE_SKILL_DIR}/scripts/check_wireframe.py` gives the same output.
|
|
25
25
|
|
|
26
|
-
It fails on unsupported characters, tabs, ASCII box borders, boxes that don't close or line up, mixed double and single lines in one box, a double-line box nested inside another box, table dividers that don't land in the same column top and bottom, progress bars whose fill disagrees with their percentage, bar chart rows whose length disagrees with their value and stated unit, heatmaps without a legend, and column charts that don't say what a row is worth. Fix every failure before presenting the wireframe.
|
|
26
|
+
It fails on unsupported characters, tabs, ASCII box borders, boxes that don't close or line up, mixed double and single lines in one box, a double-line box nested inside another box, table dividers that don't land in the same column top and bottom, progress bars whose fill disagrees with their percentage, bar chart rows whose length disagrees with their value and stated unit, heatmaps and stacked columns without a legend, and column charts that don't say what a row is worth. Fix every failure before presenting the wireframe.
|
|
27
27
|
|
|
28
28
|
## Draw it like the UI it stands for
|
|
29
29
|
|
|
@@ -113,22 +113,23 @@ Each element should look like the component it represents, not a character that
|
|
|
113
113
|
| --- | --- | --- |
|
|
114
114
|
| Bar chart | `Rent Co ████████████ $1,200`: one `█` per fixed unit, and `▏▎▍▌▋▊▉` add eighths. ASCII fallback: `#` | State the unit of one █. The partial block is the remainder. |
|
|
115
115
|
| Column chart | `███` columns, one row per fixed unit, a `▁▂▃▄▅▆▇` top for the remainder, `───` baseline, labels below. ASCII fallback: `###` | State what one row is worth. The partial top is the remainder. |
|
|
116
|
-
| Line chart |
|
|
116
|
+
| Line chart | a `─` line that turns with `╭ ╮ ╰ ╯` and climbs with `│`, against a `$400 ┤` axis and a `└────` baseline. ASCII fallback: `o` points | Label the top, middle, and bottom of the axis. Each x label sits under the start of its step. |
|
|
117
117
|
| Sparkline | `▁▂▃▄▅▆▇█` from low to high. ASCII fallback: `~` then `_ . - = + * #` | A trend with no axis. |
|
|
118
|
-
| Stacked
|
|
118
|
+
| Stacked column | `███` columns of `█ ▓ ▒ ░` segments, with a `█ Food ▓ Home` legend. ASCII fallback: `# = : .` | Stack in legend order, darkest at the bottom. |
|
|
119
119
|
| Heatmap | blank, `░ ▒ ▓ █` from light to dark, two characters per cell, with a `Low ░ ▒ ▓ █ High` legend. ASCII fallback: `. : - = + * # % @` | Always draw the legend. |
|
|
120
120
|
|
|
121
121
|
<!-- vocabulary:end -->
|
|
122
122
|
|
|
123
123
|
## Charts
|
|
124
124
|
|
|
125
|
-
- Draw charts with block characters. Each one has an ASCII fallback for places that can't show blocks: `#` for bars, `###` for columns, `~_.-=+*#` for sparklines, and `. : - = + * # % @` for heatmaps. Use one style within a chart.
|
|
125
|
+
- Draw charts with block characters. Each one has an ASCII fallback for places that can't show blocks: `o` points for line charts, `#` for bars, `###` for columns, `~_.-=+*#` for sparklines, `# = : .` for stacked columns, and `. : - = + * # % @` for heatmaps. Use one style within a chart.
|
|
126
126
|
- State what one unit is: `(one █ is $100)`, `(one row is $25)`. The checker compares each bar's length to its value.
|
|
127
127
|
- Use the partial block for the remainder, so 4.1 units is `████▏`, not `████`. A value under one unit is just the partial block.
|
|
128
128
|
- Label the top, middle, and bottom of an axis, and the first and last point on the x axis.
|
|
129
129
|
- Keep a chart under about eight rows or columns. Summarise the rest in words.
|
|
130
130
|
- A bar's fill must agree with its label. The checker enforces this for progress bars and bar charts.
|
|
131
|
-
- Always draw a legend for stacked
|
|
131
|
+
- Always draw a legend for stacked columns and heatmaps. The checker requires `Low ░ ▒ ▓ █ High` for a shaded heatmap, and a legend naming every shade, like `█ Food ▓ Home`, for stacked columns.
|
|
132
|
+
- Stack segments in legend order, darkest at the bottom, the same order in every column. Use at most four, and group the rest as Other. Round each segment to whole rows and put exact figures in the legend.
|
|
132
133
|
|
|
133
134
|
## Layout
|
|
134
135
|
|
|
@@ -165,14 +166,4 @@ Each element should look like the component it represents, not a character that
|
|
|
165
166
|
╚═════════════════════════════════════╝
|
|
166
167
|
```
|
|
167
168
|
|
|
168
|
-
Seven full screens that use every element are in `examples/`.
|
|
169
|
-
|
|
170
|
-
## Maintaining the vocabulary
|
|
171
|
-
|
|
172
|
-
`vocabulary.json` is the source of truth. After editing it, run:
|
|
173
|
-
|
|
174
|
-
```sh
|
|
175
|
-
python3 no-fi/scripts/sync_vocabulary.py # rewrite the tables above
|
|
176
|
-
node no-fi/scripts/check_coverage.mjs # every element appears in an example
|
|
177
|
-
node no-fi/scripts/check_wireframe.mjs no-fi/examples/*.txt
|
|
178
|
-
```
|
|
169
|
+
Seven full screens that use every element are in `examples/`.
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
║ $2,315.40 v -3.4% vs April ║
|
|
7
7
|
║ ║
|
|
8
8
|
║ Weekly spend ║
|
|
9
|
-
║ $450
|
|
10
|
-
║ │
|
|
11
|
-
║ │
|
|
12
|
-
║ $225 │
|
|
13
|
-
║
|
|
14
|
-
║ │
|
|
15
|
-
║ $0
|
|
9
|
+
║ $450 ┤ ║
|
|
10
|
+
║ │ ╭───╮ ╭───╮ ║
|
|
11
|
+
║ │ │ │ ╭───╮ │ ╰─── ║
|
|
12
|
+
║ $225 ┤ ╭───╮ │ ╰───────╯ ╰───╯ ║
|
|
13
|
+
║ │────╯ ╰───╯ ║
|
|
14
|
+
║ │ ║
|
|
15
|
+
║ $0 ┤ ║
|
|
16
16
|
║ └──────────────────────────────────────── ║
|
|
17
17
|
║ Mar 1 Mar 10 Mar 19 Mar 28 ║
|
|
18
18
|
║ ║
|
|
@@ -22,9 +22,17 @@
|
|
|
22
22
|
║ Cafe Nero █▊ $180 ║
|
|
23
23
|
║ Electric Co ▊ $74 ║
|
|
24
24
|
║ ║
|
|
25
|
-
║ By category
|
|
26
|
-
║
|
|
27
|
-
║
|
|
25
|
+
║ By category (one row is $100) ║
|
|
26
|
+
║ ░░░ ║
|
|
27
|
+
║ ░░░ ░░░ ║
|
|
28
|
+
║ ▓▓▓ ▒▒▒ ░░░ ░░░ ║
|
|
29
|
+
║ ▓▓▓ ▓▓▓ ▒▒▒ ▓▓▓ ║
|
|
30
|
+
║ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ║
|
|
31
|
+
║ ███ ███ ▓▓▓ ███ ║
|
|
32
|
+
║ ███ ███ ███ ███ ║
|
|
33
|
+
║ ─── ─── ─── ─── ║
|
|
34
|
+
║ Wk1 Wk2 Wk3 Wk4 ║
|
|
35
|
+
║ █ Food 30% ▓ Housing 40% ▒ Travel 10% ░ Other 20% ║
|
|
28
36
|
║ ║
|
|
29
37
|
║ Budget used ║
|
|
30
38
|
║ [ ▮▮▮▮▮▮▮▮▮▮▮▮▯▯▯▯▯▯▯▯ ] 62% ║
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Validate a no-fi wireframe.
|
|
3
3
|
//
|
|
4
4
|
// Characters: printable ASCII, box-drawing characters for borders, and block
|
|
5
|
-
// characters for progress bars and charts (see BLOCKS below)
|
|
5
|
+
// characters for progress bars and charts (see BLOCKS below), and rounded corners
|
|
6
|
+
// where a line chart turns. Nothing else.
|
|
6
7
|
//
|
|
7
8
|
// Boxes: the outermost container uses double lines (╔═╗ ║ ╚═╝). Every box nested
|
|
8
9
|
// inside it uses single lines (┌─┐ │ └─┘). Each box must close, its sides must line
|
|
@@ -12,8 +13,10 @@
|
|
|
12
13
|
// - A progress bar's fill must match its percentage.
|
|
13
14
|
// - A bar chart row's length must match its value, using the stated unit
|
|
14
15
|
// ("one █ is $100"). Without a stated unit the length is not checked.
|
|
15
|
-
// - A
|
|
16
|
-
//
|
|
16
|
+
// - A chart drawn with shade blocks needs a legend: "Low ... High" for a
|
|
17
|
+
// heatmap, or one naming every shade ("█ Food ▓ Home") for stacked columns.
|
|
18
|
+
// - A column chart needs a stated row value ("one row is $25"), unless its
|
|
19
|
+
// legend names its shades (stacked columns).
|
|
17
20
|
//
|
|
18
21
|
// This is a port of check_wireframe.py. Keep the two in step.
|
|
19
22
|
//
|
|
@@ -33,9 +36,12 @@ const BOX_CHARS = new Set(Object.values(STYLES).flatMap((style) => Object.values
|
|
|
33
36
|
// Block characters, allowed in progress bars and charts.
|
|
34
37
|
const EIGHTHS = "▏▎▍▌▋▊▉"; // horizontal partials, 1/8 to 7/8 of a cell, left to right
|
|
35
38
|
const VERTICAL = "▁▂▃▄▅▆▇"; // vertical partials, 1/8 to 7/8 of a cell, bottom to top
|
|
36
|
-
const SHADES = "░▒▓"; // heatmap intensity, with █ as the darkest
|
|
39
|
+
const SHADES = "░▒▓"; // heatmap intensity and stacked segments, with █ as the darkest
|
|
37
40
|
const BLOCKS = new Set([...(EIGHTHS + VERTICAL + SHADES + "█▮▯")]);
|
|
38
41
|
|
|
42
|
+
// Rounded corners, allowed where a line chart turns.
|
|
43
|
+
const CURVES = new Set([..."╭╮╰╯"]);
|
|
44
|
+
|
|
39
45
|
// Progress bars: (filled, empty) pairs. A bar uses one pair, filled first.
|
|
40
46
|
const UNICODE_BARS = [["▮", "▯"], ["█", "▒"]];
|
|
41
47
|
const BAR_CHARS = UNICODE_BARS.flat().join("");
|
|
@@ -50,6 +56,7 @@ const BLOCK_ROW = new RegExp(`(?<=\\s)(█*[${EIGHTHS}]?)\\s+${VALUE}(?=\\s|[│
|
|
|
50
56
|
const HASH_ROW = new RegExp(`(?<=\\s)(#+)\\s+${VALUE}(?=\\s|[│║]|$)`, "g");
|
|
51
57
|
const BLOCK_UNIT = /one █ is \$?([\d,]+(?:\.\d+)?)/;
|
|
52
58
|
const HASH_UNIT = /one # is \$?([\d,]+(?:\.\d+)?)/;
|
|
59
|
+
const SHADE_KEY = /([█░▒▓]) [A-Z]/g;
|
|
53
60
|
|
|
54
61
|
const number = (text) => Number(text.replace(/,/g, ""));
|
|
55
62
|
|
|
@@ -80,7 +87,7 @@ function checkCharacters(lines) {
|
|
|
80
87
|
[...line].forEach((ch, index) => {
|
|
81
88
|
const col = index + 1;
|
|
82
89
|
if (ch === "\t") errors.push(`line ${lineNo} col ${col}: tab character, use spaces`);
|
|
83
|
-
else if (BOX_CHARS.has(ch) || BLOCKS.has(ch)) return;
|
|
90
|
+
else if (BOX_CHARS.has(ch) || BLOCKS.has(ch) || CURVES.has(ch)) return;
|
|
84
91
|
else if (!(ch >= " " && ch <= "~")) {
|
|
85
92
|
errors.push(`line ${lineNo} col ${col}: unsupported character '${ch}'`);
|
|
86
93
|
}
|
|
@@ -135,7 +142,7 @@ function checkBars(lines) {
|
|
|
135
142
|
return errors;
|
|
136
143
|
}
|
|
137
144
|
|
|
138
|
-
// Bar charts, heatmaps, and
|
|
145
|
+
// Bar charts, heatmaps, column charts, and stacked columns.
|
|
139
146
|
function checkCharts(lines) {
|
|
140
147
|
const errors = [];
|
|
141
148
|
const text = lines.join("\n");
|
|
@@ -180,13 +187,25 @@ function checkCharts(lines) {
|
|
|
180
187
|
}
|
|
181
188
|
});
|
|
182
189
|
|
|
183
|
-
// A
|
|
184
|
-
|
|
185
|
-
|
|
190
|
+
// A chart drawn with shades needs its legend. Progress bars use ▒ too, so
|
|
191
|
+
// leave them out.
|
|
192
|
+
const charted = text.replace(UNICODE_BAR, "");
|
|
193
|
+
if ((charted.includes("░") || charted.includes("▓")) && !/Low [░▒▓█ ]+ High/.test(text)) {
|
|
194
|
+
const named = new Set([...text.matchAll(SHADE_KEY)].map((match) => match[1]));
|
|
195
|
+
const missing = [...SHADES].filter((c) => charted.includes(c) && !named.has(c));
|
|
196
|
+
if (named.size === 0) {
|
|
197
|
+
errors.push(
|
|
198
|
+
"shaded chart: draw a legend, 'Low ░ ▒ ▓ █ High' for a heatmap " +
|
|
199
|
+
"or '█ Food ▓ Home' for stacked columns",
|
|
200
|
+
);
|
|
201
|
+
} else if (missing.length) {
|
|
202
|
+
errors.push(`stacked columns: the legend is missing ${missing.join(" ")}`);
|
|
203
|
+
}
|
|
186
204
|
}
|
|
187
205
|
|
|
188
206
|
// A column chart needs to say what a row is worth.
|
|
189
|
-
|
|
207
|
+
// Stacked columns name their shades instead.
|
|
208
|
+
if (/─{3} ─{3}/.test(text) && !text.includes("one row is") && !text.match(SHADE_KEY)) {
|
|
190
209
|
errors.push("column chart: state what one row is worth, e.g. '(one row is $25)'");
|
|
191
210
|
}
|
|
192
211
|
return errors;
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
"""Validate a no-fi wireframe.
|
|
3
3
|
|
|
4
4
|
Characters: printable ASCII, box-drawing characters for borders, and block
|
|
5
|
-
characters for progress bars and charts (see BLOCKS below)
|
|
5
|
+
characters for progress bars and charts (see BLOCKS below), and rounded corners
|
|
6
|
+
where a line chart turns. Nothing else.
|
|
6
7
|
|
|
7
8
|
Boxes: the outermost container uses double lines (╔═╗ ║ ╚═╝). Every box nested
|
|
8
9
|
inside it uses single lines (┌─┐ │ └─┘). Each box must close, its sides must line
|
|
@@ -12,8 +13,10 @@ Bars and charts:
|
|
|
12
13
|
- A progress bar's fill must match its percentage.
|
|
13
14
|
- A bar chart row's length must match its value, using the stated unit
|
|
14
15
|
("one █ is $100"). Without a stated unit the length is not checked.
|
|
15
|
-
- A
|
|
16
|
-
|
|
16
|
+
- A chart drawn with shade blocks needs a legend: "Low ... High" for a
|
|
17
|
+
heatmap, or one naming every shade ("█ Food ▓ Home") for stacked columns.
|
|
18
|
+
- A column chart needs a stated row value ("one row is $25"), unless its
|
|
19
|
+
legend names its shades (stacked columns).
|
|
17
20
|
|
|
18
21
|
Usage: check_wireframe.py FILE [FILE ...]
|
|
19
22
|
Exit status is 0 when every file passes, 1 otherwise.
|
|
@@ -38,9 +41,12 @@ BOX_CHARS = {c for style in STYLES.values() for c in style.values()}
|
|
|
38
41
|
# Block characters, allowed in progress bars and charts.
|
|
39
42
|
EIGHTHS = "▏▎▍▌▋▊▉" # horizontal partials, 1/8 to 7/8 of a cell, left to right
|
|
40
43
|
VERTICAL = "▁▂▃▄▅▆▇" # vertical partials, 1/8 to 7/8 of a cell, bottom to top
|
|
41
|
-
SHADES = "░▒▓" # heatmap intensity, with █ as the darkest
|
|
44
|
+
SHADES = "░▒▓" # heatmap intensity and stacked segments, with █ as the darkest
|
|
42
45
|
BLOCKS = set(EIGHTHS + VERTICAL + SHADES + "█▮▯")
|
|
43
46
|
|
|
47
|
+
# Rounded corners, allowed where a line chart turns.
|
|
48
|
+
CURVES = set("╭╮╰╯")
|
|
49
|
+
|
|
44
50
|
# Progress bars: (filled, empty) pairs. A bar uses one pair, filled first.
|
|
45
51
|
UNICODE_BARS = (("▮", "▯"), ("█", "▒"))
|
|
46
52
|
BAR_CHARS = "".join(filled + empty for filled, empty in UNICODE_BARS)
|
|
@@ -55,6 +61,7 @@ BLOCK_ROW = re.compile(r"(?<=\s)(█*[" + EIGHTHS + r"]?)\s+" + VALUE + r"(?=\s|
|
|
|
55
61
|
HASH_ROW = re.compile(r"(?<=\s)(#+)\s+" + VALUE + r"(?=\s|[│║]|$)")
|
|
56
62
|
BLOCK_UNIT = re.compile(r"one █ is \$?([\d,]+(?:\.\d+)?)")
|
|
57
63
|
HASH_UNIT = re.compile(r"one # is \$?([\d,]+(?:\.\d+)?)")
|
|
64
|
+
SHADE_KEY = re.compile(r"([█░▒▓]) [A-Z]")
|
|
58
65
|
|
|
59
66
|
|
|
60
67
|
def number(text):
|
|
@@ -74,7 +81,7 @@ def check_characters(lines):
|
|
|
74
81
|
col = index + 1
|
|
75
82
|
if ch == "\t":
|
|
76
83
|
errors.append(f"line {number_} col {col}: tab character, use spaces")
|
|
77
|
-
elif ch in BOX_CHARS or ch in BLOCKS:
|
|
84
|
+
elif ch in BOX_CHARS or ch in BLOCKS or ch in CURVES:
|
|
78
85
|
continue
|
|
79
86
|
elif not (" " <= ch <= "~"):
|
|
80
87
|
errors.append(f"line {number_} col {col}: unsupported character {ch!r}")
|
|
@@ -123,7 +130,7 @@ def check_bars(lines):
|
|
|
123
130
|
|
|
124
131
|
|
|
125
132
|
def check_charts(lines):
|
|
126
|
-
"""Bar charts, heatmaps, and
|
|
133
|
+
"""Bar charts, heatmaps, column charts, and stacked columns."""
|
|
127
134
|
errors = []
|
|
128
135
|
text = "\n".join(lines)
|
|
129
136
|
|
|
@@ -158,12 +165,23 @@ def check_charts(lines):
|
|
|
158
165
|
f"(one # is {hash_unit.group(1)})"
|
|
159
166
|
)
|
|
160
167
|
|
|
161
|
-
# A
|
|
162
|
-
|
|
163
|
-
|
|
168
|
+
# A chart drawn with shades needs its legend. Progress bars use ▒ too, so
|
|
169
|
+
# leave them out.
|
|
170
|
+
charted = UNICODE_BAR.sub("", text)
|
|
171
|
+
if ("░" in charted or "▓" in charted) and not re.search(r"Low [░▒▓█ ]+ High", text):
|
|
172
|
+
named = set(SHADE_KEY.findall(text))
|
|
173
|
+
missing = [c for c in SHADES if c in charted and c not in named]
|
|
174
|
+
if not named:
|
|
175
|
+
errors.append(
|
|
176
|
+
"shaded chart: draw a legend, 'Low ░ ▒ ▓ █ High' for a heatmap "
|
|
177
|
+
"or '█ Food ▓ Home' for stacked columns"
|
|
178
|
+
)
|
|
179
|
+
elif missing:
|
|
180
|
+
errors.append(f"stacked columns: the legend is missing {' '.join(missing)}")
|
|
164
181
|
|
|
165
182
|
# A column chart needs to say what a row is worth.
|
|
166
|
-
|
|
183
|
+
# Stacked columns name their shades instead.
|
|
184
|
+
if re.search(r"─{3} ─{3}", text) and "one row is" not in text and not SHADE_KEY.search(text):
|
|
167
185
|
errors.append("column chart: state what one row is worth, e.g. '(one row is $25)'")
|
|
168
186
|
return errors
|
|
169
187
|
|
|
@@ -324,10 +324,10 @@
|
|
|
324
324
|
{
|
|
325
325
|
"name": "Line chart",
|
|
326
326
|
"group": "Charts",
|
|
327
|
-
"draw": "
|
|
328
|
-
"glyph": "$40 │
|
|
329
|
-
"note": "Label the top, middle, and bottom of the axis.",
|
|
330
|
-
"patterns": ["\\$\\d+k?
|
|
327
|
+
"draw": "a `─` line that turns with `╭ ╮ ╰ ╯` and climbs with `│`, against a `$400 ┤` axis and a `└────` baseline. ASCII fallback: `o` points",
|
|
328
|
+
"glyph": "$40 ┤ ╭──╮\n │ │ │\n$20 ┤ ╭─────╯ ╰──\n │ │\n $0 ┤───╯\n └───────────────\n 1 2 3 4 5",
|
|
329
|
+
"note": "Label the top, middle, and bottom of the axis. Each x label sits under the start of its step.",
|
|
330
|
+
"patterns": ["\\$\\d+k? +┤", "[╭╰]─+[╮╯]"]
|
|
331
331
|
},
|
|
332
332
|
{
|
|
333
333
|
"name": "Sparkline",
|
|
@@ -338,12 +338,12 @@
|
|
|
338
338
|
"patterns": ["(▁▂|▂▃|▃▄|▄▅|▅▆|▆▇|▇█|█▇|▇▆|▆▅|▅▄|▄▃|▃▂|▂▁)"]
|
|
339
339
|
},
|
|
340
340
|
{
|
|
341
|
-
"name": "Stacked
|
|
341
|
+
"name": "Stacked column",
|
|
342
342
|
"group": "Charts",
|
|
343
|
-
"draw": "
|
|
344
|
-
"glyph": "
|
|
345
|
-
"note": "
|
|
346
|
-
"patterns": ["\\
|
|
343
|
+
"draw": "`███` columns of `█ ▓ ▒ ░` segments, with a `█ Food ▓ Home` legend. ASCII fallback: `# = : .`",
|
|
344
|
+
"glyph": " ░░░ ░░░\n▒▒▒ ░░░ ▒▒▒ ▓▓▓\n▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓\n███ ▓▓▓ ███ ▓▓▓ ███\n███ ███ ███ ███ ███\n─── ─── ─── ─── ───\nMon Tue Wed Thu Fri\n█ Food ▓ Home ▒ Trip ░ Other",
|
|
345
|
+
"note": "Stack in legend order, darkest at the bottom.",
|
|
346
|
+
"patterns": ["([▓▒░])\\1\\1 ([▓▒░█])\\2\\2", "█ [A-Z][a-z]+[^▓\\n]*▓ [A-Z]"]
|
|
347
347
|
},
|
|
348
348
|
{
|
|
349
349
|
"name": "Heatmap",
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Check that every vocabulary element appears in at least one example.
|
|
3
|
-
//
|
|
4
|
-
// Each element in vocabulary.json lists one or more regex patterns. An element
|
|
5
|
-
// is covered when every one of its patterns matches somewhere in the examples.
|
|
6
|
-
//
|
|
7
|
-
// This is a port of check_coverage.py. Keep the two in step.
|
|
8
|
-
//
|
|
9
|
-
// Usage: node check_coverage.mjs [VOCABULARY_JSON EXAMPLES_DIR]
|
|
10
|
-
// Exit status is 0 when everything is covered, 1 otherwise.
|
|
11
|
-
|
|
12
|
-
import fs from "node:fs";
|
|
13
|
-
import path from "node:path";
|
|
14
|
-
import { fileURLToPath } from "node:url";
|
|
15
|
-
import { checkText } from "./check_wireframe.mjs";
|
|
16
|
-
|
|
17
|
-
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
18
|
-
|
|
19
|
-
function loadExamples(directory) {
|
|
20
|
-
return Object.fromEntries(
|
|
21
|
-
fs
|
|
22
|
-
.readdirSync(directory)
|
|
23
|
-
.filter((name) => name.endsWith(".txt"))
|
|
24
|
-
.sort()
|
|
25
|
-
.map((name) => [name, fs.readFileSync(path.join(directory, name), "utf8")]),
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Examples that match every pattern, or an empty list if any pattern is unmatched.
|
|
30
|
-
function usedIn(item, examples) {
|
|
31
|
-
const found = new Set();
|
|
32
|
-
for (const pattern of item.patterns) {
|
|
33
|
-
const regex = new RegExp(pattern, "m");
|
|
34
|
-
const hits = Object.keys(examples).filter((name) => regex.test(examples[name]));
|
|
35
|
-
if (hits.length === 0) return [];
|
|
36
|
-
hits.forEach((name) => found.add(name));
|
|
37
|
-
}
|
|
38
|
-
return [...found].sort();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function main(argv) {
|
|
42
|
-
const vocabulary = argv[0] ?? path.join(ROOT, "vocabulary.json");
|
|
43
|
-
const examplesDir = argv[1] ?? path.join(ROOT, "examples");
|
|
44
|
-
const data = JSON.parse(fs.readFileSync(vocabulary, "utf8"));
|
|
45
|
-
const examples = loadExamples(examplesDir);
|
|
46
|
-
|
|
47
|
-
const missing = [];
|
|
48
|
-
for (const item of data.items) {
|
|
49
|
-
const hits = usedIn(item, examples);
|
|
50
|
-
const status = hits.length ? "ok " : "MISS";
|
|
51
|
-
console.log(`${status} ${item.group.padEnd(10)} ${item.name.padEnd(13)} ${hits.join(", ")}`);
|
|
52
|
-
if (hits.length === 0) missing.push(item.name);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const total = data.items.length;
|
|
56
|
-
console.log(`\n${total - missing.length} of ${total} elements covered by ${Object.keys(examples).length} examples`);
|
|
57
|
-
|
|
58
|
-
// The drawings in the vocabulary must pass the same checks as the screens.
|
|
59
|
-
const badGlyphs = data.items.flatMap((item) =>
|
|
60
|
-
checkText(item.glyph).map((error) => `${item.name}: ${error}`),
|
|
61
|
-
);
|
|
62
|
-
if (badGlyphs.length) {
|
|
63
|
-
console.log("\nInvalid glyphs in vocabulary.json:");
|
|
64
|
-
for (const line of badGlyphs) console.log(` ${line}`);
|
|
65
|
-
} else {
|
|
66
|
-
console.log(`All ${total} vocabulary drawings pass the wireframe checks`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (missing.length) console.log("Not covered: " + missing.join(", "));
|
|
70
|
-
return missing.length || badGlyphs.length ? 1 : 0;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (process.argv[1] === fileURLToPath(import.meta.url)) process.exit(main(process.argv.slice(2)));
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Check that every vocabulary element appears in at least one example.
|
|
3
|
-
|
|
4
|
-
Each element in vocabulary.json lists one or more regex patterns. An element
|
|
5
|
-
is covered when every one of its patterns matches somewhere in the examples.
|
|
6
|
-
|
|
7
|
-
Usage: check_coverage.py [VOCABULARY_JSON EXAMPLES_DIR]
|
|
8
|
-
Exit status is 0 when everything is covered, 1 otherwise.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import json
|
|
12
|
-
import re
|
|
13
|
-
import sys
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
|
|
16
|
-
from check_wireframe import check_text
|
|
17
|
-
|
|
18
|
-
ROOT = Path(__file__).resolve().parent.parent
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def load_examples(directory):
|
|
22
|
-
return {
|
|
23
|
-
path.name: path.read_text(encoding="utf-8")
|
|
24
|
-
for path in sorted(Path(directory).glob("*.txt"))
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def used_in(item, examples):
|
|
29
|
-
"""Examples that match every pattern, or an empty list if any pattern is unmatched."""
|
|
30
|
-
found = set()
|
|
31
|
-
for pattern in item["patterns"]:
|
|
32
|
-
regex = re.compile(pattern, re.MULTILINE)
|
|
33
|
-
hits = {name for name, text in examples.items() if regex.search(text)}
|
|
34
|
-
if not hits:
|
|
35
|
-
return []
|
|
36
|
-
found |= hits
|
|
37
|
-
return sorted(found)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def main(argv):
|
|
41
|
-
vocabulary = Path(argv[0]) if argv else ROOT / "vocabulary.json"
|
|
42
|
-
examples_dir = Path(argv[1]) if len(argv) > 1 else ROOT / "examples"
|
|
43
|
-
data = json.loads(vocabulary.read_text(encoding="utf-8"))
|
|
44
|
-
examples = load_examples(examples_dir)
|
|
45
|
-
|
|
46
|
-
missing = []
|
|
47
|
-
for item in data["items"]:
|
|
48
|
-
hits = used_in(item, examples)
|
|
49
|
-
status = "ok " if hits else "MISS"
|
|
50
|
-
print(f"{status} {item['group']:<10} {item['name']:<13} {', '.join(hits)}")
|
|
51
|
-
if not hits:
|
|
52
|
-
missing.append(item["name"])
|
|
53
|
-
|
|
54
|
-
total = len(data["items"])
|
|
55
|
-
print(f"\n{total - len(missing)} of {total} elements covered by {len(examples)} examples")
|
|
56
|
-
|
|
57
|
-
# The drawings in the vocabulary must pass the same checks as the screens.
|
|
58
|
-
bad_glyphs = []
|
|
59
|
-
for item in data["items"]:
|
|
60
|
-
for error in check_text(item["glyph"]):
|
|
61
|
-
bad_glyphs.append(f"{item['name']}: {error}")
|
|
62
|
-
if bad_glyphs:
|
|
63
|
-
print("\nInvalid glyphs in vocabulary.json:")
|
|
64
|
-
for line in bad_glyphs:
|
|
65
|
-
print(f" {line}")
|
|
66
|
-
else:
|
|
67
|
-
print(f"All {total} vocabulary drawings pass the wireframe checks")
|
|
68
|
-
|
|
69
|
-
if missing:
|
|
70
|
-
print("Not covered: " + ", ".join(missing))
|
|
71
|
-
return 1 if missing or bad_glyphs else 0
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if __name__ == "__main__":
|
|
75
|
-
sys.exit(main(sys.argv[1:]))
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Regenerate the vocabulary tables in SKILL.md from vocabulary.json.
|
|
3
|
-
|
|
4
|
-
Replaces everything between the vocabulary:start and vocabulary:end markers.
|
|
5
|
-
|
|
6
|
-
Usage: sync_vocabulary.py [--check]
|
|
7
|
-
With --check, writes nothing and exits 1 if SKILL.md is out of date.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
import sys
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
|
|
14
|
-
ROOT = Path(__file__).resolve().parent.parent
|
|
15
|
-
START = "<!-- vocabulary:start -->"
|
|
16
|
-
END = "<!-- vocabulary:end -->"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def render(data):
|
|
20
|
-
blocks = []
|
|
21
|
-
for group in data["groups"]:
|
|
22
|
-
items = [i for i in data["items"] if i["group"] == group]
|
|
23
|
-
if not items:
|
|
24
|
-
continue
|
|
25
|
-
rows = [f"### {group}", "", "| Element | Draw as | Notes |", "| --- | --- | --- |"]
|
|
26
|
-
for item in items:
|
|
27
|
-
rows.append(f"| {item['name']} | {item['draw']} | {item['note']} |")
|
|
28
|
-
blocks.append("\n".join(rows))
|
|
29
|
-
return "\n\n".join(blocks)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def main(argv):
|
|
33
|
-
data = json.loads((ROOT / "vocabulary.json").read_text(encoding="utf-8"))
|
|
34
|
-
skill_path = ROOT / "SKILL.md"
|
|
35
|
-
skill = skill_path.read_text(encoding="utf-8")
|
|
36
|
-
if START not in skill or END not in skill:
|
|
37
|
-
print(f"SKILL.md is missing the {START} / {END} markers")
|
|
38
|
-
return 1
|
|
39
|
-
|
|
40
|
-
head, rest = skill.split(START, 1)
|
|
41
|
-
_, tail = rest.split(END, 1)
|
|
42
|
-
updated = f"{head}{START}\n\n{render(data)}\n\n{END}{tail}"
|
|
43
|
-
|
|
44
|
-
if "--check" in argv:
|
|
45
|
-
if updated != skill:
|
|
46
|
-
print("SKILL.md is out of date. Run scripts/sync_vocabulary.py")
|
|
47
|
-
return 1
|
|
48
|
-
print("SKILL.md is up to date")
|
|
49
|
-
return 0
|
|
50
|
-
|
|
51
|
-
skill_path.write_text(updated, encoding="utf-8")
|
|
52
|
-
print(f"Wrote {len(data['items'])} elements to SKILL.md")
|
|
53
|
-
return 0
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if __name__ == "__main__":
|
|
57
|
-
sys.exit(main(sys.argv[1:]))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|