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,223 @@
1
+ {
2
+ "schema_version": "1.0",
3
+ "kind": "diagram",
4
+ "provenance": "derived",
5
+ "artifact": {
6
+ "title": "The Pathfinder ticket lifecycle",
7
+ "subtitle": "The states a ticket occupies, who moves it, and where the status lives"
8
+ },
9
+ "source": {
10
+ "repo": "pathfinder",
11
+ "commit": "7e437f6"
12
+ },
13
+ "diagram": {
14
+ "topology": "graph",
15
+ "groups": [
16
+ {
17
+ "id": "delivery",
18
+ "label": "The delivery loop",
19
+ "summary": "One ticket at a time. A session that finishes a ticket stops there.",
20
+ "evidence": [{ "path": "skills/ticket/SKILL.md", "lines": [62, 62] }]
21
+ },
22
+ {
23
+ "id": "lifecycle",
24
+ "label": "Working states",
25
+ "parent": "delivery",
26
+ "evidence": [{ "path": "skills/ticket/SKILL.md", "lines": [41, 49] }]
27
+ },
28
+ {
29
+ "id": "outcomes",
30
+ "label": "Terminal states",
31
+ "parent": "delivery",
32
+ "summary": "Reopening terminal work is the human's decision, never an action's.",
33
+ "evidence": [{ "path": "skills/ticket/actions/load.md", "lines": [33, 36] }]
34
+ }
35
+ ],
36
+ "nodes": [
37
+ {
38
+ "id": "human",
39
+ "label": "The human",
40
+ "role": "actor",
41
+ "summary": "Approval, acceptance, merge and release are the human's, whichever action is running.",
42
+ "evidence": [{ "path": "skills/ticket/SKILL.md", "lines": [59, 64] }]
43
+ },
44
+ {
45
+ "id": "proposed",
46
+ "label": "Proposed",
47
+ "role": "step",
48
+ "group": "lifecycle",
49
+ "summary": "Written by to-tickets and nothing else. Not yet approved for execution.",
50
+ "evidence": [{ "path": "skills/ticket/SKILL.md", "lines": [43, 43] }]
51
+ },
52
+ {
53
+ "id": "ready",
54
+ "label": "Ready",
55
+ "role": "step",
56
+ "group": "lifecycle",
57
+ "summary": "Every blocker is Complete and the human has approved execution.",
58
+ "detail": [
59
+ "Readiness is not an ordering. File order, ticket number, and the order the tickets were written are not dependencies; only a blocker that is Complete makes a ticket ready."
60
+ ],
61
+ "evidence": [{ "path": "skills/ticket/actions/load.md", "lines": [52, 58] }]
62
+ },
63
+ {
64
+ "id": "in-progress",
65
+ "label": "In Progress",
66
+ "role": "step",
67
+ "group": "lifecycle",
68
+ "summary": "A session holds it. Recorded before the first file edit, never after.",
69
+ "evidence": [{ "path": "skills/ticket/actions/start.md", "lines": [18, 26] }]
70
+ },
71
+ {
72
+ "id": "review",
73
+ "label": "Review",
74
+ "role": "decision",
75
+ "group": "lifecycle",
76
+ "summary": "Verification against the ticket and its parent Feature. Review writes no status and accepts nothing.",
77
+ "evidence": [{ "path": "skills/ticket/actions/review.md", "lines": [40, 44] }]
78
+ },
79
+ {
80
+ "id": "complete",
81
+ "label": "Complete",
82
+ "role": "terminal",
83
+ "group": "outcomes",
84
+ "summary": "Work the human has accepted, delivered through the project's workflow.",
85
+ "evidence": [{ "path": "skills/ticket/actions/complete.md", "lines": [17, 25] }]
86
+ },
87
+ {
88
+ "id": "cancelled",
89
+ "label": "Cancelled",
90
+ "role": "terminal",
91
+ "group": "outcomes",
92
+ "summary": "A terminal alternative, alongside Superseded. Neither is a failure state.",
93
+ "evidence": [{ "path": "skills/ticket/SKILL.md", "lines": [45, 45] }]
94
+ },
95
+ {
96
+ "id": "store",
97
+ "label": "Ticket store",
98
+ "role": "store",
99
+ "summary": "One place, configured per project. There is never a second copy to reconcile.",
100
+ "evidence": [{ "path": "skills/ticket/store.md", "lines": [7, 17] }]
101
+ },
102
+ {
103
+ "id": "feature",
104
+ "label": "Parent Feature",
105
+ "role": "external",
106
+ "summary": "Its status is derived from its tickets and never maintained by hand.",
107
+ "evidence": [{ "path": "skills/ticket/SKILL.md", "lines": [52, 57] }]
108
+ }
109
+ ],
110
+ "edges": [
111
+ {
112
+ "id": "e-approve",
113
+ "from": "human",
114
+ "to": "ready",
115
+ "relation": "triggers",
116
+ "label": "approves execution",
117
+ "evidence": [{ "path": "skills/ticket/actions/load.md", "lines": [29, 31] }]
118
+ },
119
+ {
120
+ "id": "e-load",
121
+ "from": "proposed",
122
+ "to": "ready",
123
+ "relation": "transitions_to",
124
+ "label": "ticket load",
125
+ "evidence": [{ "path": "skills/ticket/SKILL.md", "lines": [47, 47] }]
126
+ },
127
+ {
128
+ "id": "e-start",
129
+ "from": "ready",
130
+ "to": "in-progress",
131
+ "relation": "transitions_to",
132
+ "label": "ticket start",
133
+ "evidence": [{ "path": "skills/ticket/SKILL.md", "lines": [48, 48] }]
134
+ },
135
+ {
136
+ "id": "e-resume",
137
+ "from": "in-progress",
138
+ "to": "in-progress",
139
+ "relation": "transitions_to",
140
+ "label": "resumed",
141
+ "evidence": [{ "path": "skills/ticket/actions/start.md", "lines": [22, 23] }]
142
+ },
143
+ {
144
+ "id": "e-submit",
145
+ "from": "in-progress",
146
+ "to": "review",
147
+ "relation": "transitions_to",
148
+ "label": "ticket review",
149
+ "evidence": [{ "path": "skills/ticket/SKILL.md", "lines": [30, 31] }]
150
+ },
151
+ {
152
+ "id": "e-rework",
153
+ "from": "review",
154
+ "to": "in-progress",
155
+ "relation": "transitions_to",
156
+ "label": "changes requested",
157
+ "evidence": [{ "path": "skills/ticket/actions/review.md", "lines": [42, 44] }]
158
+ },
159
+ {
160
+ "id": "e-accept",
161
+ "from": "review",
162
+ "to": "complete",
163
+ "relation": "transitions_to",
164
+ "label": "human accepts",
165
+ "evidence": [{ "path": "skills/ticket/actions/complete.md", "lines": [19, 20] }]
166
+ },
167
+ {
168
+ "id": "e-cancel",
169
+ "from": "ready",
170
+ "to": "cancelled",
171
+ "relation": "transitions_to",
172
+ "label": "human cancels",
173
+ "evidence": [{ "path": "context/ai-interaction.md", "lines": [67, 67] }]
174
+ },
175
+ {
176
+ "id": "e-record",
177
+ "from": "in-progress",
178
+ "to": "store",
179
+ "relation": "writes",
180
+ "label": "status",
181
+ "evidence": [{ "path": "skills/ticket/store.md", "lines": [46, 52] }]
182
+ },
183
+ {
184
+ "id": "e-derive",
185
+ "from": "complete",
186
+ "to": "feature",
187
+ "relation": "triggers",
188
+ "label": "derives status",
189
+ "evidence": [{ "path": "skills/ticket/actions/complete.md", "lines": [21, 25] }]
190
+ }
191
+ ],
192
+ "paths": [
193
+ {
194
+ "id": "clean",
195
+ "label": "A clean run",
196
+ "note": "Four transitions, each performed by a different action, and the last one only after a human has accepted the work.",
197
+ "edges": ["e-load", "e-start", "e-submit", "e-accept"],
198
+ "evidence": [{ "path": "skills/ticket/SKILL.md", "lines": [43, 49] }]
199
+ },
200
+ {
201
+ "id": "rework",
202
+ "label": "Review sends it back",
203
+ "note": "Review reports findings and changes no implementation, so the ticket returns to the session that holds it rather than to a new one.",
204
+ "edges": ["e-submit", "e-rework"],
205
+ "evidence": [{ "path": "skills/ticket/actions/review.md", "lines": [38, 44] }]
206
+ }
207
+ ],
208
+ "views": [
209
+ {
210
+ "id": "human-authority",
211
+ "label": "Where the human decides",
212
+ "note": "Two gates, and neither is an agent's to pass on its own.",
213
+ "focus": ["human", "ready", "complete"],
214
+ "evidence": [{ "path": "skills/ticket/SKILL.md", "lines": [63, 64] }]
215
+ },
216
+ {
217
+ "id": "exits",
218
+ "label": "How a ticket ends",
219
+ "focus": ["review", "complete", "cancelled"]
220
+ }
221
+ ]
222
+ }
223
+ }
@@ -0,0 +1,242 @@
1
+ {
2
+ "schema_version": "1.0",
3
+ "kind": "lesson",
4
+ "artifact": {
5
+ "title": "What the Pathfinder kit hands over",
6
+ "subtitle": "The copy list, the files that are tracked but never shipped, and the checks that keep both honest.",
7
+ "summary": "Pathfinder is copied into a project rather than depended on, so the exact set of files that crosses that boundary is the product. This lesson follows that set from the list that declares it to the checks that stop it drifting.",
8
+ "locale": "en"
9
+ },
10
+ "source": {
11
+ "repo": "rikilamadrid/pathfinder",
12
+ "commit": "78fbc34",
13
+ "generated_at": "2026-09-10"
14
+ },
15
+ "lesson": {
16
+ "objectives": [
17
+ "Name the single file that decides what an installed project receives.",
18
+ "Explain why a file can be tracked in Git and still never ship.",
19
+ "Point at the check that would fail if a manifest appeared at the repository root."
20
+ ],
21
+ "modules": [
22
+ {
23
+ "id": "what-ships",
24
+ "title": "What ships",
25
+ "summary": "One list decides it, and every other statement of the list is checked against that one.",
26
+ "sections": [
27
+ {
28
+ "type": "prose",
29
+ "id": "why-a-list",
30
+ "title": "Why a list at all",
31
+ "body": [
32
+ "Pathfinder installs by copying. There is no package to depend on at runtime, no import to resolve, and nothing that reaches back to this repository after the copy. The destination project ends up holding files, and the question of which files is the whole contract.",
33
+ "A contract spread across an installer, a staging script, a README and a validator is four contracts. So there is one file, and everything else is checked against it."
34
+ ]
35
+ },
36
+ {
37
+ "type": "concept",
38
+ "id": "copy-list-is-canonical",
39
+ "title": "The copy list is the single source of truth",
40
+ "body": [
41
+ "copy-list.json names the top-level entries a destination project receives. The installer reads it at runtime, the structural validator checks every other statement of the list against it, and npm publishes it inside the package so the installer stays self-contained.",
42
+ "Note that `context` is a directory entry. Anything placed beneath it ships by default, which is what makes the exclusions in the next section necessary rather than tidy."
43
+ ],
44
+ "evidence": [
45
+ { "path": "packages/create-pathfinder/copy-list.json", "lines": [1, 11] }
46
+ ]
47
+ },
48
+ {
49
+ "type": "code",
50
+ "id": "copy-list-entries",
51
+ "title": "The entries themselves",
52
+ "language": "json",
53
+ "caption": "Six entries. A destination project receives these and nothing else.",
54
+ "first_line": 3,
55
+ "lines": [
56
+ " \"entries\": [",
57
+ " \"AGENTS.md\",",
58
+ " \"CLAUDE.md\",",
59
+ " \"context\",",
60
+ " \"roles\",",
61
+ " \"skills\",",
62
+ " \"templates\"",
63
+ " ]",
64
+ "}"
65
+ ],
66
+ "evidence": [
67
+ { "path": "packages/create-pathfinder/copy-list.json", "lines": [3, 11] }
68
+ ]
69
+ },
70
+ {
71
+ "type": "concept",
72
+ "id": "never-ships",
73
+ "title": "Tracked, and never shipped",
74
+ "body": [
75
+ "Five paths under context/ are excluded from the copy even though the directory ships. Four of them are transient session state — one maintainer's in-flight work, meaningless in another checkout.",
76
+ "The fifth, context/history.md, is the interesting one. It is durable project truth, tracked in version control exactly as a project should track it. It is simply another project's truth, so the promise made about it is the publication half only: never shipped, emphatically tracked.",
77
+ "An ignore rule could not carry this. .gitignore is advisory, a single `git add -f` defeats it, and the staging script copies from the working tree without consulting it at all."
78
+ ],
79
+ "evidence": [
80
+ { "path": "packages/create-pathfinder/src/kit.mjs", "lines": [97, 105] },
81
+ { "path": "packages/create-pathfinder/src/kit.mjs", "lines": [80, 96] }
82
+ ]
83
+ },
84
+ {
85
+ "type": "flow",
86
+ "id": "install-flow",
87
+ "title": "What an install actually does",
88
+ "steps": [
89
+ {
90
+ "id": "read-the-list",
91
+ "title": "Read the copy list",
92
+ "detail": "The installer reads copy-list.json from inside its own package, so the list it obeys travelled with it."
93
+ },
94
+ {
95
+ "id": "copy-entries",
96
+ "title": "Copy each entry",
97
+ "detail": "Files are copied verbatim. Directory entries are copied recursively, which is why skills/ arrives whole."
98
+ },
99
+ {
100
+ "id": "apply-filter",
101
+ "title": "Drop the never-ships paths",
102
+ "detail": "A filter matched on the kit-relative path — never the basename — removes the five excluded files as the copy walks.",
103
+ "evidence": [
104
+ { "path": "packages/create-pathfinder/src/kit.mjs", "lines": [106, 130] }
105
+ ]
106
+ },
107
+ {
108
+ "id": "generate-adapters",
109
+ "title": "Generate harness adapters, if asked",
110
+ "detail": "Adapters are rendered from skill frontmatter. They carry a pointer to the canonical skill and no behaviour of their own."
111
+ }
112
+ ]
113
+ },
114
+ {
115
+ "type": "quiz",
116
+ "id": "what-ships-quiz",
117
+ "title": "Check your understanding",
118
+ "questions": [
119
+ {
120
+ "id": "which-file",
121
+ "prompt": "A contributor wants to add a new top-level directory to what installed projects receive. Which file do they change?",
122
+ "options": [
123
+ "packages/create-pathfinder/copy-list.json",
124
+ "The installer's src/kit.mjs",
125
+ ".gitignore",
126
+ "Both the installer and the README, together"
127
+ ],
128
+ "answer": 0,
129
+ "explanation": "The list is declared once. The installer reads it, and validation checks every other statement of the list against it, so changing it anywhere else is what the checks exist to catch.",
130
+ "evidence": [
131
+ { "path": "packages/create-pathfinder/copy-list.json", "lines": [2, 2] }
132
+ ]
133
+ },
134
+ {
135
+ "id": "history-md",
136
+ "prompt": "Why is context/history.md excluded from the copy but still tracked in Git?",
137
+ "options": [
138
+ "It is generated, so tracking it would create merge conflicts",
139
+ "It is this repository's own completed-work record — durable truth, but somebody else's project should write its own",
140
+ "It is too large to ship inside an npm package",
141
+ "It is transient session state, like the handoff file"
142
+ ],
143
+ "answer": 1,
144
+ "explanation": "The other four excluded paths are transient and are ignored by Git as well. This one is durable and tracked; only publication is withheld."
145
+ }
146
+ ]
147
+ }
148
+ ]
149
+ },
150
+ {
151
+ "id": "keeping-it-honest",
152
+ "title": "Keeping it honest",
153
+ "summary": "The promises above are only worth what enforces them.",
154
+ "requires": ["what-ships"],
155
+ "sections": [
156
+ {
157
+ "type": "concept",
158
+ "id": "no-root-manifest",
159
+ "title": "No manifest at the repository root",
160
+ "body": [
161
+ "The kit promises a destination project acquires no runtime and no package manager. A package.json at this repository's root would contradict that in the most visible place possible, so CI asserts its absence by name for eight different manifest files.",
162
+ "Nested tooling may carry its own manifest — the installer under packages/ does. The rule is about the root, because the root is what a reader looks at to decide what kind of project this is."
163
+ ],
164
+ "evidence": [
165
+ { "path": ".github/workflows/validate.yml", "lines": [43, 54] },
166
+ { "path": "NOT_A_FRAMEWORK.md", "lines": [1, 5] }
167
+ ]
168
+ },
169
+ {
170
+ "type": "code",
171
+ "id": "root-manifest-check",
172
+ "title": "The assertion itself",
173
+ "language": "yaml",
174
+ "caption": "Named manifests, checked one at a time, with the reason in the comment above them.",
175
+ "first_line": 43,
176
+ "lines": [
177
+ " - name: Assert no root manifest",
178
+ " # NOT_A_FRAMEWORK.md promises the kit has no runtime and no package",
179
+ " # manager. Nested tooling may carry its own manifest; the root may not.",
180
+ " run: |",
181
+ " for f in package.json package-lock.json pnpm-lock.yaml yarn.lock \\",
182
+ " requirements.txt pyproject.toml Gemfile go.mod; do",
183
+ " if [ -e \"$f\" ]; then",
184
+ " echo \"::error file=$f::$f at the repository root contradicts NOT_A_FRAMEWORK.md\""
185
+ ],
186
+ "evidence": [
187
+ { "path": ".github/workflows/validate.yml", "lines": [43, 50] }
188
+ ]
189
+ },
190
+ {
191
+ "type": "concept",
192
+ "id": "one-store",
193
+ "title": "One artifact, never a mirror",
194
+ "body": [
195
+ "The same instinct governs tickets. A project's tickets live in exactly one store, and nothing publishes, projects, or synchronizes them, because there is never a second copy to reconcile.",
196
+ "Local Markdown is a store in its own right rather than a fallback or a mirror of a real one. A project using GitHub Issues has nothing under context/tickets/, and a project using Markdown has its tickets there and nowhere else."
197
+ ],
198
+ "evidence": [
199
+ { "path": "skills/ticket/store.md", "lines": [7, 16] }
200
+ ]
201
+ },
202
+ {
203
+ "type": "exercise",
204
+ "id": "trace-a-file",
205
+ "title": "Trace a file across the boundary",
206
+ "body": [
207
+ "Pick any file in this repository and decide, without running the installer, whether an installed project receives it. Then check yourself against copy-list.json and the never-ships set.",
208
+ "Try context/coding-standards.md, context/handoff.md, and CONTRIBUTING.md. One of the three is a different answer from what the directory it sits in would suggest."
209
+ ],
210
+ "hints": [
211
+ "Start from the top-level entry. If the file's first path segment is not in the copy list, nothing below it ships.",
212
+ "If the first segment is `context`, check the never-ships set before answering — the directory entry ships its contents by default.",
213
+ "CONTRIBUTING.md is not a top-level entry in the list at all."
214
+ ],
215
+ "evidence": [
216
+ { "path": "packages/create-pathfinder/copy-list.json", "lines": [3, 10] },
217
+ { "path": "packages/create-pathfinder/src/kit.mjs", "lines": [97, 105] }
218
+ ]
219
+ },
220
+ {
221
+ "type": "quiz",
222
+ "id": "honesty-quiz",
223
+ "questions": [
224
+ {
225
+ "id": "root-manifest-why",
226
+ "prompt": "Why does CI name eight specific manifest files rather than checking for any dependency file?",
227
+ "options": [
228
+ "Because a glob would be slower in CI",
229
+ "Because the rule is about the root only, and naming the files makes what it forbids readable in the failure message",
230
+ "Because the repository might legitimately contain a package.json at the root",
231
+ "Because GitHub Actions cannot express a glob"
232
+ ],
233
+ "answer": 1,
234
+ "explanation": "The check prints the offending filename and the promise it contradicts. A reader who trips it learns why, not just that."
235
+ }
236
+ ]
237
+ }
238
+ ]
239
+ }
240
+ ]
241
+ }
242
+ }
@@ -0,0 +1,71 @@
1
+ # Determinism
2
+
3
+ **The invariant:** given the same specification bytes and the same renderer
4
+ version, rendering produces byte-identical HTML in every supported environment.
5
+
6
+ The renderer implementation is part of the compiler, which is why the version is
7
+ named in the invariant and reported in every receipt. A renderer release may
8
+ intentionally change output — that is a version change, not a determinism
9
+ failure. Without the version in the receipt, the two would be indistinguishable.
10
+
11
+ **If you change anything that can alter rendered HTML — markup, CSS, inline
12
+ behaviour, ordering, escaping — bump `engine/version.mjs` in the same commit.**
13
+ A kit release that does not touch rendering must leave it alone.
14
+
15
+ ## What the invariant forbids
16
+
17
+ Nothing in the render path may read:
18
+
19
+ - a clock, in any form — `Date`, `Date.now`, a timestamp, a duration
20
+ - randomness, including `Math.random` and `randomUUID`
21
+ - the machine — hostname, username, environment variables, process or OS details
22
+ - the filesystem's opinion — the working directory, an absolute path, a
23
+ directory listing's order
24
+ - the locale — `Intl`, `localeCompare`, or any locale-dependent number, date, or
25
+ string formatting
26
+
27
+ If a timestamp appears in an artifact, it came from `source.generated_at` in the
28
+ specification. There is no other way for one to get there.
29
+
30
+ ## How it is enforced
31
+
32
+ **The render path imports nothing.** `render/index.mjs`, `render/lesson.mjs`,
33
+ `render/shell.mjs`, `render/theme.mjs`, `render/behavior.mjs`,
34
+ `render/escape.mjs`, `version.mjs` and `verification.mjs` import only each
35
+ other. Not one `node:` builtin between them. `verification.mjs` in particular
36
+ imports nothing at all, which is why the shell can consult it without dragging
37
+ the validator — and `node:fs` with it — into rendering. Rendering is a pure function of the specification and this
38
+ code, and the cheapest way to keep it that way is for there to be nothing
39
+ ambient in scope to reach for.
40
+
41
+ **Ordering comes from the specification.** Every list is emitted in the order
42
+ the specification wrote it. Where a set has to be ordered — extra properties in
43
+ a diagnostic, for instance — it is sorted by codepoint, never with
44
+ `localeCompare`, whose answer depends on the reader's locale.
45
+
46
+ **Identifiers are derived.** Every DOM id is built from producer-supplied ids
47
+ joined with a separator the identifier pattern forbids. No counter, no hash, no
48
+ insertion order.
49
+
50
+ **The bytes are checked, not assumed.** Delivery rejects output containing a
51
+ carriage return or beginning with a byte-order mark, rather than trusting that
52
+ no template ever grew one.
53
+
54
+ ## How to check it
55
+
56
+ Render the same specification in deliberately different environments and compare
57
+ the artifact digests:
58
+
59
+ ```sh
60
+ cd /some/other/directory
61
+ TZ=Pacific/Kiritimati LANG=tr_TR.UTF-8 LC_ALL=tr_TR.UTF-8 \
62
+ node .../engine/bin/render.mjs deliver <spec.json> <out.html> --repo <repo> --json
63
+ ```
64
+
65
+ Different working directory, different timezone, different locale. The
66
+ `sha256` in the receipt must not move.
67
+
68
+ `doctor`'s determinism check is weaker and says so: it compares two renders
69
+ inside one process, which catches a renderer that varies run to run but cannot
70
+ see an environment dependency at all. Cross-environment determinism is checked
71
+ by actually varying the environment.