create-pathfinder 4.2.0 → 4.3.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.
Files changed (39) hide show
  1. package/CLAUDE.md +2 -0
  2. package/package.json +1 -1
  3. package/skills/learn-codebase/SKILL.md +188 -17
  4. package/skills/learn-feature/SKILL.md +136 -15
  5. package/skills/map-system/SKILL.md +293 -0
  6. package/skills/render-artifact/SKILL.md +187 -0
  7. package/skills/render-artifact/engine/bin/render.mjs +225 -0
  8. package/skills/render-artifact/engine/deliver.mjs +197 -0
  9. package/skills/render-artifact/engine/doctor.mjs +96 -0
  10. package/skills/render-artifact/engine/examples/diagram.json +223 -0
  11. package/skills/render-artifact/engine/examples/lesson.json +242 -0
  12. package/skills/render-artifact/engine/references/determinism.md +71 -0
  13. package/skills/render-artifact/engine/references/specification.md +149 -0
  14. package/skills/render-artifact/engine/references/validation.md +268 -0
  15. package/skills/render-artifact/engine/render/behavior.mjs +128 -0
  16. package/skills/render-artifact/engine/render/diagram.mjs +342 -0
  17. package/skills/render-artifact/engine/render/escape.mjs +34 -0
  18. package/skills/render-artifact/engine/render/graph/behavior.mjs +394 -0
  19. package/skills/render-artifact/engine/render/graph/draw.mjs +204 -0
  20. package/skills/render-artifact/engine/render/graph/interaction.mjs +174 -0
  21. package/skills/render-artifact/engine/render/graph/layout.mjs +698 -0
  22. package/skills/render-artifact/engine/render/graph/style.mjs +200 -0
  23. package/skills/render-artifact/engine/render/graph/width.mjs +204 -0
  24. package/skills/render-artifact/engine/render/index.mjs +50 -0
  25. package/skills/render-artifact/engine/render/lesson.mjs +294 -0
  26. package/skills/render-artifact/engine/render/shell.mjs +275 -0
  27. package/skills/render-artifact/engine/render/theme.mjs +592 -0
  28. package/skills/render-artifact/engine/schemas/common.schema.json +101 -0
  29. package/skills/render-artifact/engine/schemas/diagram.schema.json +176 -0
  30. package/skills/render-artifact/engine/schemas/lesson.schema.json +210 -0
  31. package/skills/render-artifact/engine/validate/composition.mjs +395 -0
  32. package/skills/render-artifact/engine/validate/diagnostics.mjs +83 -0
  33. package/skills/render-artifact/engine/validate/diagram-parts.mjs +68 -0
  34. package/skills/render-artifact/engine/validate/evidence.mjs +302 -0
  35. package/skills/render-artifact/engine/validate/index.mjs +132 -0
  36. package/skills/render-artifact/engine/validate/jsonschema.mjs +312 -0
  37. package/skills/render-artifact/engine/validate/structural.mjs +241 -0
  38. package/skills/render-artifact/engine/verification.mjs +76 -0
  39. package/skills/render-artifact/engine/version.mjs +24 -0
@@ -0,0 +1,200 @@
1
+ /**
2
+ * How a diagram looks, in terms of the identity that already exists.
3
+ *
4
+ * Not one colour is defined here. Every value below resolves to a `--pf-*`
5
+ * token from `theme.mjs`, so a diagram inherits both themes, the measured
6
+ * contrast behind them, and any future change to either, without restating a
7
+ * single one. That is what "adding a kind must not require restating the
8
+ * identity" means when the kind is a picture rather than a page.
9
+ *
10
+ * What *is* defined here is shape: stroke weights, corner treatment, the grid
11
+ * the canvas sits on. Those are diagram vocabulary and have no meaning for a
12
+ * lesson, which is why they live beside the diagram renderer rather than in the
13
+ * shared theme.
14
+ *
15
+ * Two deliberate choices, both of which read as design and are really
16
+ * accessibility:
17
+ *
18
+ * - **Role is never carried by colour alone.** Shape and a printed role word
19
+ * carry it; the accent only reinforces it.
20
+ * - **Labels are monospace.** Diagram labels are technical tokens — endpoint
21
+ * names, queue names, service names — and mono is the honest typography for
22
+ * them. It is also what keeps a character-budget line break close to the
23
+ * truth while the width-aware model is still being decided.
24
+ */
25
+
26
+ export const GRAPH_CSS = `
27
+ .pf-canvas {
28
+ margin: var(--pf-space-5) 0 var(--pf-space-7);
29
+ padding: var(--pf-space-4);
30
+ background: var(--pf-surface);
31
+ border: 1px solid var(--pf-line);
32
+ border-radius: var(--pf-radius);
33
+ box-shadow: var(--pf-shadow);
34
+ overflow-x: auto;
35
+ }
36
+ .pf-graph {
37
+ display: block;
38
+ width: 100%;
39
+ height: auto;
40
+ font-family: var(--pf-mono);
41
+ }
42
+
43
+ .pf-group-box {
44
+ fill: var(--pf-surface-2);
45
+ stroke: var(--pf-line-strong);
46
+ stroke-width: 1;
47
+ stroke-dasharray: 4 4;
48
+ }
49
+ .pf-group[data-pf-depth="1"] .pf-group-box { stroke-dasharray: 2 3; }
50
+ .pf-group-label {
51
+ fill: var(--pf-muted);
52
+ font-size: 12px;
53
+ letter-spacing: .06em;
54
+ text-transform: uppercase;
55
+ }
56
+
57
+ .pf-node-box {
58
+ fill: var(--pf-surface);
59
+ stroke: var(--pf-line-strong);
60
+ stroke-width: 2;
61
+ }
62
+ .pf-node[data-pf-role="store"] .pf-node-box,
63
+ .pf-node[data-pf-role="queue"] .pf-node-box { stroke-dasharray: 7 3; }
64
+ .pf-node[data-pf-role="external"] .pf-node-box,
65
+ .pf-node[data-pf-role="actor"] .pf-node-box { stroke: var(--pf-accent); }
66
+ .pf-node[data-pf-role="terminal"] .pf-node-box { stroke: var(--pf-ok); }
67
+ .pf-node[data-pf-role="decision"] .pf-node-box { stroke: var(--pf-accent); stroke-dasharray: 3 3; }
68
+
69
+ .pf-node-label {
70
+ fill: var(--pf-ink);
71
+ font-size: 13px;
72
+ font-weight: 600;
73
+ }
74
+ .pf-node-role {
75
+ fill: var(--pf-muted);
76
+ font-size: 10px;
77
+ letter-spacing: .1em;
78
+ text-transform: uppercase;
79
+ }
80
+
81
+ .pf-edge-line {
82
+ fill: none;
83
+ stroke: var(--pf-line-strong);
84
+ stroke-width: 2;
85
+ stroke-linejoin: round;
86
+ stroke-linecap: round;
87
+ }
88
+ .pf-edge-on-path .pf-edge-line { stroke: var(--pf-accent); stroke-width: 3; }
89
+ .pf-edge[data-pf-relation="depends_on"] .pf-edge-line { stroke-dasharray: 5 4; }
90
+ .pf-edge[data-pf-relation="publishes"] .pf-edge-line,
91
+ .pf-edge[data-pf-relation="consumes"] .pf-edge-line { stroke-dasharray: 2 4; }
92
+ .pf-edge-label {
93
+ fill: var(--pf-muted);
94
+ font-size: 11px;
95
+ }
96
+ .pf-edge-on-path .pf-edge-label { fill: var(--pf-link); }
97
+
98
+ /* ---- the reading controls, and what a selection looks like ----
99
+
100
+ Three states, and the distinction is deliberate. "on" is what the reader
101
+ asked about. "near" is what it touches — still readable, because a component
102
+ whose neighbours have been greyed out tells you less than one whose
103
+ neighbours are merely quieter. "off" is the rest of the graph, dimmed rather
104
+ than removed: the document keeps every fact whatever is selected, and a
105
+ reader who dislikes the dimming can turn scripting off and read all of it.
106
+
107
+ No colour is defined here either. Selection reads through the accent that
108
+ already carries emphasis, and through opacity, so it works in both themes
109
+ without a second palette. Opacity alone would be a colour-only signal, so
110
+ the focused node also thickens its stroke. */
111
+ .pf-graph-tools { margin: var(--pf-space-5) 0 calc(var(--pf-space-3) * -1); }
112
+ .pf-toolbar {
113
+ display: flex;
114
+ flex-wrap: wrap;
115
+ align-items: center;
116
+ gap: var(--pf-space-2);
117
+ }
118
+ .pf-tool {
119
+ padding: 4px 10px;
120
+ background: var(--pf-surface);
121
+ color: var(--pf-ink);
122
+ border: 1px solid var(--pf-line-strong);
123
+ border-radius: var(--pf-radius-sm);
124
+ font-family: var(--pf-mono);
125
+ font-size: .78rem;
126
+ cursor: pointer;
127
+ }
128
+ .pf-tool:hover:not(:disabled) { border-color: var(--pf-accent); color: var(--pf-link); }
129
+ .pf-tool:disabled { opacity: .45; cursor: default; }
130
+ .pf-tool-sep {
131
+ width: 1px;
132
+ height: 1.2em;
133
+ background: var(--pf-line);
134
+ }
135
+ .pf-graph-status {
136
+ margin: var(--pf-space-2) 0 0;
137
+ color: var(--pf-muted);
138
+ font-family: var(--pf-mono);
139
+ font-size: .78rem;
140
+ }
141
+ /* Inline and free to wrap. A float here escaped its heading at narrow widths,
142
+ which is exactly where this gets read. */
143
+ .pf-pick { margin-left: var(--pf-space-2); white-space: nowrap; }
144
+
145
+ .pf-canvas:focus-visible { outline: 3px solid var(--pf-accent); outline-offset: 2px; }
146
+ .pf-canvas[data-pf-zoom="in"] .pf-graph { cursor: grab; }
147
+ .pf-canvas[data-pf-dragging] .pf-graph { cursor: grabbing; }
148
+
149
+ /* Selection. Keyed off attributes the script sets and nothing else, so the
150
+ delivered document is identical whether or not a browser ever runs it. */
151
+ .pf-node[data-pf-state="off"],
152
+ .pf-edge[data-pf-state="off"] { opacity: .22; }
153
+ .pf-node[data-pf-state="near"],
154
+ .pf-edge[data-pf-state="near"] { opacity: .75; }
155
+ .pf-node[data-pf-state="on"] .pf-node-box {
156
+ stroke: var(--pf-accent);
157
+ stroke-width: 4;
158
+ }
159
+ .pf-edge[data-pf-state="on"] .pf-edge-line {
160
+ stroke: var(--pf-accent);
161
+ stroke-width: 3;
162
+ }
163
+ .pf-edge[data-pf-state="on"] .pf-edge-label { fill: var(--pf-link); }
164
+
165
+ /* The written entry the selection points at. A left rule rather than a fill:
166
+ the card already has a surface, and stacking another on it would flatten
167
+ the hierarchy the evidence block sits in. */
168
+ [data-pf-entry][aria-current] {
169
+ border-left: 3px solid var(--pf-accent);
170
+ padding-left: var(--pf-space-3);
171
+ }
172
+
173
+ @media (prefers-reduced-motion: no-preference) {
174
+ .pf-node, .pf-edge { transition: opacity 120ms linear; }
175
+ }
176
+
177
+ /* The reading list below the canvas: the same facts, as text, for a reader who
178
+ is not looking at a picture. */
179
+ .pf-legend { display: grid; gap: var(--pf-space-4); }
180
+ .pf-legend-role {
181
+ display: inline-block;
182
+ margin-left: var(--pf-space-2);
183
+ padding: 1px 6px;
184
+ border: 1px solid var(--pf-line);
185
+ border-radius: var(--pf-radius-sm);
186
+ color: var(--pf-muted);
187
+ font-family: var(--pf-mono);
188
+ font-size: .72rem;
189
+ letter-spacing: .08em;
190
+ text-transform: uppercase;
191
+ vertical-align: middle;
192
+ }
193
+ .pf-relation {
194
+ font-family: var(--pf-mono);
195
+ font-size: .82rem;
196
+ color: var(--pf-muted);
197
+ }
198
+ .pf-walk { margin: 0; padding-left: var(--pf-space-5); }
199
+ .pf-walk li { margin-bottom: var(--pf-space-1); }
200
+ `.trim();
@@ -0,0 +1,204 @@
1
+ /**
2
+ * How many columns a code point occupies. Generated, pinned, and never derived
3
+ * at run time.
4
+ *
5
+ * GENERATED FILE — do not edit by hand.
6
+ * Regenerate with `node scripts/generate-width-table.mjs` in
7
+ * `packages/render-artifact`, deliberately, as an adopted Unicode upgrade.
8
+ *
9
+ * Pinned to **Unicode 17.0**, the version the generating engine carried.
10
+ * The renderer does not consult the engine it runs on: `\p{Mn}` and friends
11
+ * resolve against whatever Unicode data that engine was compiled with, and that
12
+ * moves between Node releases. Two machines would then lay the same label out
13
+ * differently, which is the ambient dependency the determinism invariant
14
+ * forbids. So the answer is frozen here as literal ranges.
15
+ *
16
+ * Three widths, and a total function:
17
+ *
18
+ * 0 non-spacing and enclosing marks, and format characters — including
19
+ * U+200D ZERO WIDTH JOINER and the variation selectors
20
+ * 2 East Asian Wide and Fullwidth blocks, and default-emoji-presentation
21
+ * 1 everything else, including every code point not named below
22
+ *
23
+ * The default of 1 is what makes this total. A code point Unicode assigns after
24
+ * this table was pinned is measured as one column rather than throwing or
25
+ * returning undefined, so a label in a script newer than the renderer still
26
+ * lays out — narrower than it should be if that character is actually wide,
27
+ * which the box's headroom absorbs.
28
+ */
29
+
30
+ /** The Unicode version this table was generated from. */
31
+ export const UNICODE_VERSION = "17.0";
32
+
33
+ /** Code points that occupy no column. */
34
+ const ZERO_WIDTH = [
35
+ [0xAD, 0xAD], [0x300, 0x36F], [0x483, 0x489], [0x591, 0x5BD],
36
+ [0x5BF, 0x5BF], [0x5C1, 0x5C2], [0x5C4, 0x5C5], [0x5C7, 0x5C7],
37
+ [0x600, 0x605], [0x610, 0x61A], [0x61C, 0x61C], [0x64B, 0x65F],
38
+ [0x670, 0x670], [0x6D6, 0x6DD], [0x6DF, 0x6E4], [0x6E7, 0x6E8],
39
+ [0x6EA, 0x6ED], [0x70F, 0x70F], [0x711, 0x711], [0x730, 0x74A],
40
+ [0x7A6, 0x7B0], [0x7EB, 0x7F3], [0x7FD, 0x7FD], [0x816, 0x819],
41
+ [0x81B, 0x823], [0x825, 0x827], [0x829, 0x82D], [0x859, 0x85B],
42
+ [0x890, 0x891], [0x897, 0x89F], [0x8CA, 0x902], [0x93A, 0x93A],
43
+ [0x93C, 0x93C], [0x941, 0x948], [0x94D, 0x94D], [0x951, 0x957],
44
+ [0x962, 0x963], [0x981, 0x981], [0x9BC, 0x9BC], [0x9C1, 0x9C4],
45
+ [0x9CD, 0x9CD], [0x9E2, 0x9E3], [0x9FE, 0x9FE], [0xA01, 0xA02],
46
+ [0xA3C, 0xA3C], [0xA41, 0xA42], [0xA47, 0xA48], [0xA4B, 0xA4D],
47
+ [0xA51, 0xA51], [0xA70, 0xA71], [0xA75, 0xA75], [0xA81, 0xA82],
48
+ [0xABC, 0xABC], [0xAC1, 0xAC5], [0xAC7, 0xAC8], [0xACD, 0xACD],
49
+ [0xAE2, 0xAE3], [0xAFA, 0xAFF], [0xB01, 0xB01], [0xB3C, 0xB3C],
50
+ [0xB3F, 0xB3F], [0xB41, 0xB44], [0xB4D, 0xB4D], [0xB55, 0xB56],
51
+ [0xB62, 0xB63], [0xB82, 0xB82], [0xBC0, 0xBC0], [0xBCD, 0xBCD],
52
+ [0xC00, 0xC00], [0xC04, 0xC04], [0xC3C, 0xC3C], [0xC3E, 0xC40],
53
+ [0xC46, 0xC48], [0xC4A, 0xC4D], [0xC55, 0xC56], [0xC62, 0xC63],
54
+ [0xC81, 0xC81], [0xCBC, 0xCBC], [0xCBF, 0xCBF], [0xCC6, 0xCC6],
55
+ [0xCCC, 0xCCD], [0xCE2, 0xCE3], [0xD00, 0xD01], [0xD3B, 0xD3C],
56
+ [0xD41, 0xD44], [0xD4D, 0xD4D], [0xD62, 0xD63], [0xD81, 0xD81],
57
+ [0xDCA, 0xDCA], [0xDD2, 0xDD4], [0xDD6, 0xDD6], [0xE31, 0xE31],
58
+ [0xE34, 0xE3A], [0xE47, 0xE4E], [0xEB1, 0xEB1], [0xEB4, 0xEBC],
59
+ [0xEC8, 0xECE], [0xF18, 0xF19], [0xF35, 0xF35], [0xF37, 0xF37],
60
+ [0xF39, 0xF39], [0xF71, 0xF7E], [0xF80, 0xF84], [0xF86, 0xF87],
61
+ [0xF8D, 0xF97], [0xF99, 0xFBC], [0xFC6, 0xFC6], [0x102D, 0x1030],
62
+ [0x1032, 0x1037], [0x1039, 0x103A], [0x103D, 0x103E], [0x1058, 0x1059],
63
+ [0x105E, 0x1060], [0x1071, 0x1074], [0x1082, 0x1082], [0x1085, 0x1086],
64
+ [0x108D, 0x108D], [0x109D, 0x109D], [0x135D, 0x135F], [0x1712, 0x1714],
65
+ [0x1732, 0x1733], [0x1752, 0x1753], [0x1772, 0x1773], [0x17B4, 0x17B5],
66
+ [0x17B7, 0x17BD], [0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],
67
+ [0x180B, 0x180F], [0x1885, 0x1886], [0x18A9, 0x18A9], [0x1920, 0x1922],
68
+ [0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B], [0x1A17, 0x1A18],
69
+ [0x1A1B, 0x1A1B], [0x1A56, 0x1A56], [0x1A58, 0x1A5E], [0x1A60, 0x1A60],
70
+ [0x1A62, 0x1A62], [0x1A65, 0x1A6C], [0x1A73, 0x1A7C], [0x1A7F, 0x1A7F],
71
+ [0x1AB0, 0x1ADD], [0x1AE0, 0x1AEB], [0x1B00, 0x1B03], [0x1B34, 0x1B34],
72
+ [0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42], [0x1B6B, 0x1B73],
73
+ [0x1B80, 0x1B81], [0x1BA2, 0x1BA5], [0x1BA8, 0x1BA9], [0x1BAB, 0x1BAD],
74
+ [0x1BE6, 0x1BE6], [0x1BE8, 0x1BE9], [0x1BED, 0x1BED], [0x1BEF, 0x1BF1],
75
+ [0x1C2C, 0x1C33], [0x1C36, 0x1C37], [0x1CD0, 0x1CD2], [0x1CD4, 0x1CE0],
76
+ [0x1CE2, 0x1CE8], [0x1CED, 0x1CED], [0x1CF4, 0x1CF4], [0x1CF8, 0x1CF9],
77
+ [0x1DC0, 0x1DFF], [0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2064],
78
+ [0x2066, 0x206F], [0x20D0, 0x20F0], [0x2CEF, 0x2CF1], [0x2D7F, 0x2D7F],
79
+ [0x2DE0, 0x2DFF], [0x302A, 0x302D], [0x3099, 0x309A], [0xA66F, 0xA672],
80
+ [0xA674, 0xA67D], [0xA69E, 0xA69F], [0xA6F0, 0xA6F1], [0xA802, 0xA802],
81
+ [0xA806, 0xA806], [0xA80B, 0xA80B], [0xA825, 0xA826], [0xA82C, 0xA82C],
82
+ [0xA8C4, 0xA8C5], [0xA8E0, 0xA8F1], [0xA8FF, 0xA8FF], [0xA926, 0xA92D],
83
+ [0xA947, 0xA951], [0xA980, 0xA982], [0xA9B3, 0xA9B3], [0xA9B6, 0xA9B9],
84
+ [0xA9BC, 0xA9BD], [0xA9E5, 0xA9E5], [0xAA29, 0xAA2E], [0xAA31, 0xAA32],
85
+ [0xAA35, 0xAA36], [0xAA43, 0xAA43], [0xAA4C, 0xAA4C], [0xAA7C, 0xAA7C],
86
+ [0xAAB0, 0xAAB0], [0xAAB2, 0xAAB4], [0xAAB7, 0xAAB8], [0xAABE, 0xAABF],
87
+ [0xAAC1, 0xAAC1], [0xAAEC, 0xAAED], [0xAAF6, 0xAAF6], [0xABE5, 0xABE5],
88
+ [0xABE8, 0xABE8], [0xABED, 0xABED], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
89
+ [0xFE20, 0xFE2F], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB], [0x101FD, 0x101FD],
90
+ [0x102E0, 0x102E0], [0x10376, 0x1037A], [0x10A01, 0x10A03], [0x10A05, 0x10A06],
91
+ [0x10A0C, 0x10A0F], [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x10AE5, 0x10AE6],
92
+ [0x10D24, 0x10D27], [0x10D69, 0x10D6D], [0x10EAB, 0x10EAC], [0x10EFA, 0x10EFF],
93
+ [0x10F46, 0x10F50], [0x10F82, 0x10F85], [0x11001, 0x11001], [0x11038, 0x11046],
94
+ [0x11070, 0x11070], [0x11073, 0x11074], [0x1107F, 0x11081], [0x110B3, 0x110B6],
95
+ [0x110B9, 0x110BA], [0x110BD, 0x110BD], [0x110C2, 0x110C2], [0x110CD, 0x110CD],
96
+ [0x11100, 0x11102], [0x11127, 0x1112B], [0x1112D, 0x11134], [0x11173, 0x11173],
97
+ [0x11180, 0x11181], [0x111B6, 0x111BE], [0x111C9, 0x111CC], [0x111CF, 0x111CF],
98
+ [0x1122F, 0x11231], [0x11234, 0x11234], [0x11236, 0x11237], [0x1123E, 0x1123E],
99
+ [0x11241, 0x11241], [0x112DF, 0x112DF], [0x112E3, 0x112EA], [0x11300, 0x11301],
100
+ [0x1133B, 0x1133C], [0x11340, 0x11340], [0x11366, 0x1136C], [0x11370, 0x11374],
101
+ [0x113BB, 0x113C0], [0x113CE, 0x113CE], [0x113D0, 0x113D0], [0x113D2, 0x113D2],
102
+ [0x113E1, 0x113E2], [0x11438, 0x1143F], [0x11442, 0x11444], [0x11446, 0x11446],
103
+ [0x1145E, 0x1145E], [0x114B3, 0x114B8], [0x114BA, 0x114BA], [0x114BF, 0x114C0],
104
+ [0x114C2, 0x114C3], [0x115B2, 0x115B5], [0x115BC, 0x115BD], [0x115BF, 0x115C0],
105
+ [0x115DC, 0x115DD], [0x11633, 0x1163A], [0x1163D, 0x1163D], [0x1163F, 0x11640],
106
+ [0x116AB, 0x116AB], [0x116AD, 0x116AD], [0x116B0, 0x116B5], [0x116B7, 0x116B7],
107
+ [0x1171D, 0x1171D], [0x1171F, 0x1171F], [0x11722, 0x11725], [0x11727, 0x1172B],
108
+ [0x1182F, 0x11837], [0x11839, 0x1183A], [0x1193B, 0x1193C], [0x1193E, 0x1193E],
109
+ [0x11943, 0x11943], [0x119D4, 0x119D7], [0x119DA, 0x119DB], [0x119E0, 0x119E0],
110
+ [0x11A01, 0x11A0A], [0x11A33, 0x11A38], [0x11A3B, 0x11A3E], [0x11A47, 0x11A47],
111
+ [0x11A51, 0x11A56], [0x11A59, 0x11A5B], [0x11A8A, 0x11A96], [0x11A98, 0x11A99],
112
+ [0x11B60, 0x11B60], [0x11B62, 0x11B64], [0x11B66, 0x11B66], [0x11C30, 0x11C36],
113
+ [0x11C38, 0x11C3D], [0x11C3F, 0x11C3F], [0x11C92, 0x11CA7], [0x11CAA, 0x11CB0],
114
+ [0x11CB2, 0x11CB3], [0x11CB5, 0x11CB6], [0x11D31, 0x11D36], [0x11D3A, 0x11D3A],
115
+ [0x11D3C, 0x11D3D], [0x11D3F, 0x11D45], [0x11D47, 0x11D47], [0x11D90, 0x11D91],
116
+ [0x11D95, 0x11D95], [0x11D97, 0x11D97], [0x11EF3, 0x11EF4], [0x11F00, 0x11F01],
117
+ [0x11F36, 0x11F3A], [0x11F40, 0x11F40], [0x11F42, 0x11F42], [0x11F5A, 0x11F5A],
118
+ [0x13430, 0x13440], [0x13447, 0x13455], [0x1611E, 0x16129], [0x1612D, 0x1612F],
119
+ [0x16AF0, 0x16AF4], [0x16B30, 0x16B36], [0x16F4F, 0x16F4F], [0x16F8F, 0x16F92],
120
+ [0x16FE4, 0x16FE4], [0x1BC9D, 0x1BC9E], [0x1BCA0, 0x1BCA3], [0x1CF00, 0x1CF2D],
121
+ [0x1CF30, 0x1CF46], [0x1D167, 0x1D169], [0x1D173, 0x1D182], [0x1D185, 0x1D18B],
122
+ [0x1D1AA, 0x1D1AD], [0x1D242, 0x1D244], [0x1DA00, 0x1DA36], [0x1DA3B, 0x1DA6C],
123
+ [0x1DA75, 0x1DA75], [0x1DA84, 0x1DA84], [0x1DA9B, 0x1DA9F], [0x1DAA1, 0x1DAAF],
124
+ [0x1E000, 0x1E006], [0x1E008, 0x1E018], [0x1E01B, 0x1E021], [0x1E023, 0x1E024],
125
+ [0x1E026, 0x1E02A], [0x1E08F, 0x1E08F], [0x1E130, 0x1E136], [0x1E2AE, 0x1E2AE],
126
+ [0x1E2EC, 0x1E2EF], [0x1E4EC, 0x1E4EF], [0x1E5EE, 0x1E5EF], [0x1E6E3, 0x1E6E3],
127
+ [0x1E6E6, 0x1E6E6], [0x1E6EE, 0x1E6EF], [0x1E6F5, 0x1E6F5], [0x1E8D0, 0x1E8D6],
128
+ [0x1E944, 0x1E94A], [0xE0001, 0xE0001], [0xE0020, 0xE007F], [0xE0100, 0xE01EF],
129
+ ];
130
+
131
+ /** Code points that occupy two columns. */
132
+ const WIDE = [
133
+ [0x1100, 0x115F], [0x231A, 0x231B], [0x2329, 0x232A], [0x23E9, 0x23EC],
134
+ [0x23F0, 0x23F0], [0x23F3, 0x23F3], [0x25FD, 0x25FE], [0x2614, 0x2615],
135
+ [0x2648, 0x2653], [0x267F, 0x267F], [0x2693, 0x2693], [0x26A1, 0x26A1],
136
+ [0x26AA, 0x26AB], [0x26BD, 0x26BE], [0x26C4, 0x26C5], [0x26CE, 0x26CE],
137
+ [0x26D4, 0x26D4], [0x26EA, 0x26EA], [0x26F2, 0x26F3], [0x26F5, 0x26F5],
138
+ [0x26FA, 0x26FA], [0x26FD, 0x26FD], [0x2705, 0x2705], [0x270A, 0x270B],
139
+ [0x2728, 0x2728], [0x274C, 0x274C], [0x274E, 0x274E], [0x2753, 0x2755],
140
+ [0x2757, 0x2757], [0x2795, 0x2797], [0x27B0, 0x27B0], [0x27BF, 0x27BF],
141
+ [0x2B1B, 0x2B1C], [0x2B50, 0x2B50], [0x2B55, 0x2B55], [0x2E80, 0x2E99],
142
+ [0x2E9B, 0x2EF3], [0x2F00, 0x2FD5], [0x2FF0, 0x2FFB], [0x3000, 0x303E],
143
+ [0x3041, 0x3096], [0x3099, 0x30FF], [0x3105, 0x312F], [0x3131, 0x318E],
144
+ [0x3190, 0x31E3], [0x31F0, 0x321E], [0x3220, 0x3247], [0x3250, 0x4DBF],
145
+ [0x4E00, 0xA48C], [0xA490, 0xA4C6], [0xA960, 0xA97C], [0xAC00, 0xD7A3],
146
+ [0xF900, 0xFAFF], [0xFE10, 0xFE19], [0xFE30, 0xFE52], [0xFE54, 0xFE66],
147
+ [0xFE68, 0xFE6B], [0xFF01, 0xFF60], [0xFFE0, 0xFFE6], [0x16FE0, 0x16FE4],
148
+ [0x17000, 0x187F7], [0x18800, 0x18CD5], [0x1B000, 0x1B122], [0x1B150, 0x1B152],
149
+ [0x1B164, 0x1B167], [0x1B170, 0x1B2FB], [0x1F004, 0x1F004], [0x1F0CF, 0x1F0CF],
150
+ [0x1F18E, 0x1F18E], [0x1F191, 0x1F19A], [0x1F1E6, 0x1F1FF], [0x1F201, 0x1F201],
151
+ [0x1F21A, 0x1F21A], [0x1F22F, 0x1F22F], [0x1F232, 0x1F236], [0x1F238, 0x1F23A],
152
+ [0x1F250, 0x1F251], [0x1F300, 0x1F320], [0x1F32D, 0x1F335], [0x1F337, 0x1F37C],
153
+ [0x1F37E, 0x1F393], [0x1F3A0, 0x1F3CA], [0x1F3CF, 0x1F3D3], [0x1F3E0, 0x1F3F0],
154
+ [0x1F3F4, 0x1F3F4], [0x1F3F8, 0x1F43E], [0x1F440, 0x1F440], [0x1F442, 0x1F4FC],
155
+ [0x1F4FF, 0x1F53D], [0x1F54B, 0x1F54E], [0x1F550, 0x1F567], [0x1F57A, 0x1F57A],
156
+ [0x1F595, 0x1F596], [0x1F5A4, 0x1F5A4], [0x1F5FB, 0x1F64F], [0x1F680, 0x1F6C5],
157
+ [0x1F6CC, 0x1F6CC], [0x1F6D0, 0x1F6D2], [0x1F6D5, 0x1F6D8], [0x1F6DC, 0x1F6DF],
158
+ [0x1F6EB, 0x1F6EC], [0x1F6F4, 0x1F6FC], [0x1F7E0, 0x1F7EB], [0x1F7F0, 0x1F7F0],
159
+ [0x1F90C, 0x1F93A], [0x1F93C, 0x1F945], [0x1F947, 0x1F9FF], [0x1FA70, 0x1FA7C],
160
+ [0x1FA80, 0x1FA8A], [0x1FA8E, 0x1FAC6], [0x1FAC8, 0x1FAC8], [0x1FACD, 0x1FADC],
161
+ [0x1FADF, 0x1FAEA], [0x1FAEF, 0x1FAF8], [0x20000, 0x2FFFD], [0x30000, 0x3FFFD],
162
+ ];
163
+
164
+ /** Binary search over sorted, non-overlapping, inclusive ranges. */
165
+ function inRanges(ranges, cp) {
166
+ let low = 0;
167
+ let high = ranges.length - 1;
168
+ while (low <= high) {
169
+ const mid = (low + high) >> 1;
170
+ if (cp < ranges[mid][0]) { high = mid - 1; continue; }
171
+ if (cp > ranges[mid][1]) { low = mid + 1; continue; }
172
+ return true;
173
+ }
174
+ return false;
175
+ }
176
+
177
+ /**
178
+ * The width of one code point, in columns.
179
+ *
180
+ * @param {number} cp a Unicode code point
181
+ * @returns {0|1|2}
182
+ */
183
+ export function codePointWidth(cp) {
184
+ if (inRanges(ZERO_WIDTH, cp)) return 0;
185
+ if (inRanges(WIDE, cp)) return 2;
186
+ return 1;
187
+ }
188
+
189
+ /**
190
+ * The width of a string, in columns.
191
+ *
192
+ * Iterates by code point — `for...of` over a string yields code points, not
193
+ * UTF-16 units — so an astral character is measured once rather than twice.
194
+ * No locale API, no segmenter, no normalization: each is either engine-version
195
+ * dependent or locale dependent, and both are forbidden here.
196
+ *
197
+ * @param {string} text
198
+ * @returns {number} columns
199
+ */
200
+ export function cellWidth(text) {
201
+ let width = 0;
202
+ for (const character of String(text)) width += codePointWidth(character.codePointAt(0));
203
+ return width;
204
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The renderer registry: `kind` in, HTML out.
3
+ *
4
+ * Two entries and no placeholder for anything else. A registry with a stub in
5
+ * it invites a caller to render a kind that does not exist yet, and the result
6
+ * would be an artifact carrying the Pathfinder identity around content nothing
7
+ * checked. An unsupported kind is refused in the structural layer, before
8
+ * anything reaches here.
9
+ *
10
+ * Both entries render into the same shell and inherit the same identity. That
11
+ * is the whole claim of a shared renderer, and it is checkable: nothing below
12
+ * this line knows which kind it is looking at.
13
+ */
14
+
15
+ import { renderLesson } from "./lesson.mjs";
16
+ import { renderDiagram } from "./diagram.mjs";
17
+
18
+ export const RENDERERS = Object.freeze({
19
+ lesson: renderLesson,
20
+ diagram: renderDiagram,
21
+ });
22
+
23
+ /**
24
+ * Render a validated specification.
25
+ *
26
+ * Deterministic by construction: the only input is the specification and this
27
+ * module's code. Nothing here reads a clock, an environment variable, a
28
+ * hostname, a working directory, or a locale, and no ordering comes from a
29
+ * directory listing or a hash table — every list is emitted in the order the
30
+ * specification wrote it.
31
+ *
32
+ * Rendering does not validate and does not pretend to. Called without an
33
+ * attestation — the ordinary case, and the only one available to an outside
34
+ * caller — the artifact carries its provenance rows and no claim to have been
35
+ * checked. Only `deliver()`, which validates first, can supply one.
36
+ *
37
+ * @param {object} spec a specification whose `kind` this engine renders
38
+ * @param {object} [verification] an attestation minted from a passing
39
+ * validation. Cannot be forged: see `verification.mjs`.
40
+ * @returns {string} a complete, self-contained HTML document
41
+ */
42
+ export function render(spec, verification) {
43
+ const renderer = RENDERERS[spec.kind];
44
+ if (!renderer) {
45
+ throw new Error(
46
+ `no renderer for kind ${JSON.stringify(spec.kind)}; this should have been ` +
47
+ `refused by the structural layer`);
48
+ }
49
+ return renderer(spec, verification);
50
+ }