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,149 @@
1
+ # The specification
2
+
3
+ One shared contract, and one kind-specific schema selected by `kind`.
4
+
5
+ Everything here is producer-supplied *content*. Nothing here is presentation.
6
+ If you find yourself wanting a field for a colour, a class, a width, a
7
+ coordinate, an icon, a template, or a theme, the answer is that the renderer
8
+ owns it — and the schema will reject the field rather than ignore it, so the
9
+ attempt fails loudly instead of silently doing nothing.
10
+
11
+ ## The shared contract
12
+
13
+ `schemas/common.schema.json`.
14
+
15
+ | Field | Required | What it is |
16
+ | --- | --- | --- |
17
+ | `schema_version` | yes | `"1.0"`. The contract this specification is written against. |
18
+ | `kind` | yes | Selects the kind schema and the renderer. `lesson` and `diagram`. |
19
+ | `artifact` | yes | Metadata about the artifact: `title`, and optionally `subtitle`, `summary`, `locale`. |
20
+ | `source` | see below | Where the claims come from: `repo`, `commit`, and optionally `generated_at`. |
21
+
22
+ `source` is required by the `lesson` kind unconditionally. For `diagram` it is
23
+ conditional on `provenance`, and the condition is written into
24
+ `diagram.schema.json` rather than into the shared contract — see
25
+ [`provenance`](#provenance) below. The shared `source` definition itself is
26
+ unchanged and is not loosened: a kind that declares a source declares a complete
27
+ one.
28
+
29
+ ### `provenance`
30
+
31
+ `diagram` only. `"derived"` or `"proposed"`, and required — there is no default,
32
+ because a diagram that did not say would have a trust level assigned to it by the
33
+ engine.
34
+
35
+ - **`derived`** maps what the repository asserts about itself at the declared
36
+ commit. `source` is required, every node and edge carries at least one
37
+ citation, a group, path or view carrying `summary` or `note` prose carries one
38
+ too, and `artifact.summary` is not permitted.
39
+ - **`proposed`** describes an intended system. Citations are optional, and every
40
+ one supplied is still resolved. With no `source` at all, citations are refused
41
+ rather than ignored, and the evidence layer is reported as not run.
42
+
43
+ `validation.md` has the full table and the diagnostics. The short version: what
44
+ you may assert is decided by what you are willing to cite, and the renderer's
45
+ wording follows from that rather than from anything you can ask for.
46
+
47
+ ### `source`
48
+
49
+ - `repo` identifies the repository the evidence belongs to, so a diagnostic can
50
+ say whose commit is missing.
51
+ - `commit` is a full or abbreviated Git object name. **Evidence resolves
52
+ against this and nothing else** — never the working tree.
53
+ - `generated_at` is optional, supplied by the producer, and part of the
54
+ deterministic input. The renderer never reads a clock, so if a timestamp
55
+ appears in an artifact, it came from here.
56
+
57
+ There is no `theme`, no `theme_default`, and no presentation default of any
58
+ kind. Those belong to the renderer, and the reader's own preference beats both.
59
+
60
+ ### Evidence
61
+
62
+ One citation shape, used identically wherever evidence appears, in this kind and
63
+ every kind that follows:
64
+
65
+ ```json
66
+ { "path": "packages/create-pathfinder/src/kit.mjs", "lines": [97, 105] }
67
+ ```
68
+
69
+ - `path` — repository-relative, forward-slashed. Never absolute.
70
+ - `lines` — optional inclusive `[start, end]`, 1-based, and must lie within that
71
+ file at the resolved commit.
72
+ - `commit` — optional, overrides `source.commit` for this citation alone. Use it
73
+ when one claim is about a different point in history, not to work around a
74
+ citation that will not resolve.
75
+
76
+ ## The `lesson` kind
77
+
78
+ `schemas/lesson.schema.json`.
79
+
80
+ ```
81
+ lesson
82
+ objectives? string[]
83
+ modules module[] one or many — nothing branches on the count
84
+ ```
85
+
86
+ `learn-feature` supplies one module. `learn-codebase` supplies many. There is no
87
+ consumer-specific field, no mode flag, and no escape hatch: the same fields
88
+ serve both, and a change made to suit one has to be justified for the other.
89
+
90
+ ### Module
91
+
92
+ | Field | Required | Notes |
93
+ | --- | --- | --- |
94
+ | `id` | yes | Lowercase, digits and hyphens. Becomes a link target, so it is unique across the artifact. |
95
+ | `title` | yes | |
96
+ | `summary` | no | |
97
+ | `requires` | no | Module ids this builds on. Edges must resolve and must not cycle. A flat list with no edges is legal, and is the single-module case. |
98
+ | `sections` | yes | At least one. |
99
+
100
+ ### Sections
101
+
102
+ Discriminated by `type`.
103
+
104
+ **`prose`** — `body` paragraphs. For explanation that carries no claim needing
105
+ a citation.
106
+
107
+ **`concept`** — `title`, `body`, and `evidence`. A claim about the source.
108
+ `evidence` is required and must be non-empty; a concept citing nothing fails the
109
+ evidence layer, because an uncited claim is the renderer asserting domain
110
+ content, which it must never do.
111
+
112
+ **`code`** — `language`, `lines`, optional `title`, `caption`, `first_line`, and
113
+ `evidence`. Lines are emitted verbatim and escaped. `language` labels the
114
+ excerpt for the reader; it selects no highlighter, because highlighting is
115
+ either a dependency or a hand-rolled tokeniser and both are the renderer
116
+ starting to interpret content it was handed literally.
117
+
118
+ **`flow`** — `title` and `steps`. Each step has `id`, `title`, and optionally
119
+ `detail`, `next`, `evidence`. The first step is the entry. `next` is optional
120
+ and defaults to the following step, which is what a linear flow means without
121
+ saying so. Every step must be reachable from the first, and the graph must be
122
+ acyclic.
123
+
124
+ **`quiz`** — `questions`, each with `id`, `prompt`, `options` (2–8, distinct),
125
+ `answer` (an index into that question's own options), and optionally
126
+ `explanation` and `evidence`.
127
+
128
+ **`exercise`** — `title`, `body`, and optionally `hints` and `evidence`.
129
+
130
+ ## What a producer cannot assert
131
+
132
+ There is no field for whether the specification was validated, whether its
133
+ evidence checked out, or whether the artifact is trustworthy. Those are claims
134
+ about work the engine performs, and the engine is the only thing entitled to
135
+ make them — see the verification section of `validation.md`.
136
+
137
+ Nor is there a field for the *wording* of those claims. The three provenance
138
+ sentences are the renderer's, and a producer cannot select one, soften one, or
139
+ request one it has not earned. `provenance` is not an exception: it declares what
140
+ kind of claim the diagram is making and thereby what will be *required* of it. A
141
+ producer choosing `derived` is choosing the stricter rules, not choosing the
142
+ stronger sentence.
143
+
144
+ ## Text is text
145
+
146
+ No field is Markdown and no field is HTML. Every producer string is escaped on
147
+ the way out, so `<b>bold</b>` in a title renders as those nine characters,
148
+ visibly. That is the boundary working: content that smuggles markup is a
149
+ producer reaching for presentation by another route.
@@ -0,0 +1,268 @@
1
+ # Validation and delivery
2
+
3
+ Four layers. Each supports a different claim, each is reported separately, and
4
+ none of them is a warning.
5
+
6
+ ```
7
+ structural the specification satisfies its schema
8
+ composition identifiers, references, graphs and answers are coherent
9
+ evidence every citation resolves at the declared commit
10
+ delivery the artifact was rendered, digested, and committed atomically
11
+ ```
12
+
13
+ Structural runs first and alone: composition and evidence assume a specification
14
+ that already has the right shape, so if structural fails they do not run and the
15
+ report says so rather than showing them as passing. Composition and evidence are
16
+ independent of each other and run together, so one round trip surfaces both.
17
+
18
+ A layer can also be **not run because it does not apply**, which is reported
19
+ distinctly from both a pass and a skip. A `diagram` declaring no source has no
20
+ evidence layer: there is no commit to resolve against, and the structural layer
21
+ has already refused any citation in it, so there is provably nothing to check.
22
+ The report says `~ evidence: not run` and gives the reason.
23
+
24
+ That distinction is the whole point of keeping it. Reporting such a layer as
25
+ `ok evidence` would put the strongest word in the report against the weakest
26
+ claim in it, and a reader skimming four green lines would conclude the citations
27
+ had been verified when there were none.
28
+
29
+ ## Commands
30
+
31
+ ```sh
32
+ node engine/bin/render.mjs validate <spec.json> [--repo <dir>] [--json]
33
+ node engine/bin/render.mjs deliver <spec.json> <out.html> [--repo <dir>] [--json]
34
+ node engine/bin/render.mjs doctor [--json]
35
+ ```
36
+
37
+ `--repo` names the repository whose history evidence resolves in. It defaults to
38
+ the specification's own directory, which is right when the specification lives
39
+ in the repository it cites, and is exactly what needs overriding when it does
40
+ not.
41
+
42
+ Exit codes are the contract:
43
+
44
+ | Code | Meaning |
45
+ | --- | --- |
46
+ | 0 | every layer that ran passed, and the artifact was committed |
47
+ | 1 | a layer failed; nothing was committed |
48
+ | 2 | the command line was wrong |
49
+
50
+ A non-zero exit is never reported as success.
51
+
52
+ ## Structural
53
+
54
+ Schema validation, plus two refusals that come before it:
55
+
56
+ | Code | Meaning |
57
+ | --- | --- |
58
+ | `schema_version_unsupported` | the specification is written against a contract this engine does not implement |
59
+ | `kind_unsupported` | no such artifact kind. Refused — not a reason to improvise HTML |
60
+ | `presentation_control` | a field like `color`, `css`, `class`, `layout` or `theme`. Rejected, never ignored |
61
+ | `unknown_field` | a field this contract does not have |
62
+ | `missing_field` | a required field is absent |
63
+ | `schema_*` | one keyword rejected one value; the message names both |
64
+ | `topology_unsupported` | a `diagram` asked for a layout this renderer does not have |
65
+ | `label_too_long` | a label is wider than its column cap. Refused, never shortened |
66
+ | `source_required_for_derived` | a `derived` diagram does not say which repository or which commit it was derived from |
67
+ | `citation_without_source` | a specification with no source carries a citation. There is nothing to resolve it against |
68
+ | `artifact_summary_forbidden` | a `derived` diagram carries `artifact.summary`, which is the one claim-bearing prose with nowhere to put a citation |
69
+
70
+ The last three are the provenance contract's structural half — see
71
+ **Provenance** below.
72
+
73
+ `presentation_control` and `unknown_field` have the same *outcome* —
74
+ `additionalProperties: false` rejects either. They differ in the diagnostic,
75
+ because a producer who wrote `"color"` believed presentation was theirs to set,
76
+ and the error should say so instead of talking about arrays and properties.
77
+
78
+ ## Composition
79
+
80
+ | Code | Meaning |
81
+ | --- | --- |
82
+ | `duplicate_identifier` | two modules or sections share an id; identifiers become link targets |
83
+ | `duplicate_step_identifier` | two steps of one flow share an id |
84
+ | `duplicate_question_identifier` | two questions of one quiz share an id |
85
+ | `unresolved_reference` | a `requires` or `next` names something that does not exist |
86
+ | `graph_cycle` | a module or flow graph leads back to itself |
87
+ | `orphan_step` | a flow step is unreachable from the flow's first step |
88
+ | `orphan_module` | a module is unreachable from every module without prerequisites |
89
+ | `answer_out_of_range` | a quiz answer does not index its own options |
90
+ | `module_empty` | a module carries no sections |
91
+
92
+ Note what "orphan" means for modules. With no `requires` anywhere, every module
93
+ is a root and nothing is orphaned — a flat list is a legal graph, and that is
94
+ the single-module case, which must stay legal. Orphans only become possible once
95
+ edges exist.
96
+
97
+ ## Evidence
98
+
99
+ Resolved against the commit, using local Git. Nothing is fetched, cloned, or
100
+ looked up over a network: a commit that is not present locally is a commit whose
101
+ evidence was not verified, and the engine says so rather than going to find one.
102
+
103
+ | Code | Meaning |
104
+ | --- | --- |
105
+ | `source_commit_unavailable` | the declared commit cannot be resolved here — wrong repository, unfetched commit, or no repository at all |
106
+ | `evidence_path_absent` | the commit resolves; the file is not in it |
107
+ | `evidence_range_invalid` | the file is there; the cited lines are not |
108
+ | `concept_without_evidence` | a concept cites nothing |
109
+ | `node_without_evidence` | a `derived` diagram says a component exists and cites nothing for it |
110
+ | `edge_without_evidence` | a `derived` diagram asserts a relationship and cites nothing for it |
111
+ | `claim_without_evidence` | a `derived` group, path or view carries `summary` or `note` prose with no citation |
112
+
113
+ The first three stay apart because they mean three different things to the
114
+ person reading them: a stale fixture, a moved file, and a shifted range are
115
+ three different fixes.
116
+
117
+ None of them downgrades to a skip. An engine that shrugged at an unresolvable
118
+ commit would deliver an artifact whose provenance block claims its evidence was
119
+ verified when nothing was.
120
+
121
+ ### What this layer establishes, and what it does not
122
+
123
+ That the cited file exists at the declared commit, that the cited line range
124
+ exists in it, and that the material is there to be read.
125
+
126
+ Not that the claim resting on it is true.
127
+
128
+ Repository documentation — a README, an ADR, a runbook — is first-class citation
129
+ material and is checked exactly the same way. That is not a statement that
130
+ documentation carries the same authority as the code it describes; it is a
131
+ statement that the engine can tell you where to look and cannot tell you whether
132
+ what you find is right.
133
+
134
+ **Pathfinder verifies provenance, not truth.** No diagnostic and no word in any
135
+ artifact may imply otherwise.
136
+
137
+ ## Provenance
138
+
139
+ A `diagram` declares `provenance`, and it decides how strictly the diagram is
140
+ checked. There is no default: a diagram that did not say would have a trust level
141
+ assigned to it by the engine, and the engine has no business guessing which claim
142
+ its producer meant to make.
143
+
144
+ | | `derived` | `proposed`, with source | `proposed`, no source |
145
+ | --- | --- | --- | --- |
146
+ | `source` | required | permitted | absent |
147
+ | citations | required on every node and edge | optional | **refused** |
148
+ | claim-bearing group, path or view | must cite | optional | **refused** |
149
+ | label-only group, path or view | needs nothing | needs nothing | needs nothing |
150
+ | `artifact.summary` | refused | permitted | permitted |
151
+ | evidence layer | runs | runs | not run, with the reason |
152
+
153
+ A `derived` diagram maps what the repository asserts about itself at the declared
154
+ commit, so there is no uncited derived fact. A node says a component exists and
155
+ an edge says two things relate; each is a claim a reader must be able to go and
156
+ check. Evidence is *where a reader goes to check an assertion*, which is not
157
+ always the code implementing it — an actor is cited by the entry point that
158
+ accepts it, an external system by its client or its configuration, a subsystem by
159
+ its manifest or entry module.
160
+
161
+ A component that appears nowhere in source, configuration, infrastructure or
162
+ repository documentation is not eligible for a `derived` diagram at all. A
163
+ diagram that needs it is `proposed`.
164
+
165
+ A group, path or view is the one narrower case. Its label is a name, and a name
166
+ asserts nothing its already-cited members do not. Its `summary` or `note` is
167
+ prose making a further claim, and that is what needs backing.
168
+
169
+ `lesson` has no `provenance` field and is unaffected by any of it. `source`
170
+ remains unconditionally required there: the conditional lives in the diagram
171
+ schema, not in the shared contract, precisely so that making one kind's source
172
+ optional did not make every kind's provenance optional.
173
+
174
+ ## Delivery
175
+
176
+ Delivery takes the specification as **bytes** and nothing else. It copies them,
177
+ parses that copy, validates the parsed value, renders that same value, and
178
+ reports the digest of those same bytes. There is no parameter through which a
179
+ caller could supply a specification object alongside unrelated bytes, because a
180
+ receipt that described a specification nobody rendered would be undetectable
181
+ downstream.
182
+
183
+ Rendering then runs against a deep-frozen copy, so a renderer that mutated its
184
+ own input would throw rather than quietly produce output nobody can reproduce
185
+ from the file on disk.
186
+
187
+ ### Verification is earned, not asserted
188
+
189
+ An artifact carries a sentence saying its evidence was checked against the named
190
+ commit. It carries that sentence only when **both** of these are true:
191
+
192
+ 1. this engine validated the specification and the validation passed, and
193
+ 2. at least one citation actually resolved.
194
+
195
+ The second condition is the guard against a vacuous claim. A specification
196
+ carrying no citations passes the evidence layer by having nothing to fail, and an
197
+ artifact saying "every citation above was verified against the commit named here"
198
+ on the strength of that would be a stronger statement than anybody made — with
199
+ no citation above, and sometimes a commit that does not exist. Nothing to check
200
+ is not the same as everything checking out.
201
+
202
+ So a `proposed` diagram with a source and no citations gets no sentence, and
203
+ neither does a lesson that cites nothing. Both are still valid, and both still
204
+ deliver; what they do not get is credit for a check that had no subject.
205
+
206
+ #### The three wordings
207
+
208
+ Renderer-owned, and different in each state because each supports a different
209
+ claim. **No field in any specification selects, softens, or requests one.** They
210
+ live in the shared shell rather than in a kind renderer because they describe
211
+ what the *engine* did, which is the same whatever kind rendered into it.
212
+
213
+ | State | What the artifact says |
214
+ | --- | --- |
215
+ | `derived` | the schema, references and graphs checked out, and every component, relationship and claim carried a citation verified at the declared commit. Explicitly **not** a finding that the architecture drawn is correct or complete |
216
+ | `proposed`, source, ≥1 resolved citation | the citations supplied were verified at the declared commit, and explicitly **do not** establish that the system drawn exists |
217
+ | `proposed`, source, no citations | nothing |
218
+ | `proposed`, no source | no verification sentence. Instead: that the diagram describes an intended system, names no repository or commit, and makes no claim about what currently exists |
219
+ | `lesson` | unchanged from what it has always said |
220
+
221
+ A source-less artifact also carries **no repository row, no commit row and no
222
+ timestamp**. None of them is invented from the working directory, the
223
+ environment, or a clock: there is no honest value, and a plausible one would be
224
+ worse than none, because it would read as provenance.
225
+
226
+ `render()` is a public export and does not validate. Called directly it emits
227
+ the provenance rows and **no verification language at all** — not "unverified",
228
+ not a placeholder, nothing. An artifact that cannot vouch for itself says
229
+ nothing on the subject, and the absence of the sentence is the signal; saying
230
+ "unverified" would still be the renderer making a claim about a process it did
231
+ not observe.
232
+
233
+ The claim is gated on an attestation, which only `verification.attest()` mints,
234
+ and only from a validation result this engine branded on the way past. The brand
235
+ is a module-private symbol — not `Symbol.for`, not a string key — so a
236
+ hand-built `{ ok: true }` is refused. `deliver()` validates and mints one
237
+ itself.
238
+
239
+ There is no producer-facing counterpart, and there must never be one. No
240
+ `verified`, no `validation_status`, no field of any name in the specification
241
+ can influence this. A producer asserting that its own work was checked is
242
+ exactly the claim this design exists to make impossible.
243
+
244
+ The artifact is written to a temporary file beside the destination, flushed with
245
+ `fsync`, and renamed over it. A rename within a directory is atomic, so a reader
246
+ never sees a half-written page. The `fsync` matters as much as the rename:
247
+ without it the rename can be durable while the content behind it is not, which
248
+ is how a crash leaves a correctly named, empty artifact.
249
+
250
+ A failure at any point leaves a previously delivered artifact exactly as it was.
251
+
252
+ | Code | Meaning |
253
+ | --- | --- |
254
+ | `render_failed` | rendering threw; nothing was written |
255
+ | `carriage_return_in_output` | output contains `\r`; artifacts use `\n` only |
256
+ | `byte_order_mark_in_output` | output begins with a BOM; artifacts are UTF-8 without one |
257
+ | `commit_failed` | the artifact could not be written or renamed |
258
+
259
+ The receipt names the renderer version and the SHA-256 and byte count of both
260
+ the specification and the artifact.
261
+
262
+ ## What a green result is not
263
+
264
+ It is not a judgement that the artifact looks right. Nobody has looked at it.
265
+
266
+ Report the two separately, always. A human opening the artifact in a browser is
267
+ a different kind of evidence about a different question, and neither result
268
+ supports the other.
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The artifact's inline behaviour: theme choice, navigation position, and quiz
3
+ * feedback.
4
+ *
5
+ * All three are enhancements. With scripting off the artifact is still a
6
+ * complete, readable, keyboard-navigable document: the theme follows the
7
+ * reader's system through `prefers-color-scheme`, navigation is a list of
8
+ * ordinary anchors, and every quiz answer is already reachable inside a
9
+ * `<details>`. Nothing here is load-bearing, which is what lets the artifact
10
+ * open from `file://` with no network, no server, and no build step.
11
+ *
12
+ * Storage is wrapped: `localStorage` throws outright in some `file://` and
13
+ * private-window configurations, and a theme preference is not worth a page
14
+ * that fails to run.
15
+ */
16
+
17
+ export const BEHAVIOR_JS = `
18
+ (function () {
19
+ "use strict";
20
+
21
+ var root = document.documentElement;
22
+ var MODES = ["auto", "light", "dark"];
23
+ var LABELS = { auto: "Theme: auto", light: "Theme: light", dark: "Theme: dark" };
24
+ var KEY = "pathfinder.artifact.theme";
25
+
26
+ function stored() {
27
+ try {
28
+ var value = window.localStorage.getItem(KEY);
29
+ return MODES.indexOf(value) >= 0 ? value : null;
30
+ } catch (error) {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ function remember(mode) {
36
+ try {
37
+ window.localStorage.setItem(KEY, mode);
38
+ } catch (error) {
39
+ /* A reader who cannot store a preference still gets to use it. */
40
+ }
41
+ }
42
+
43
+ function apply(mode, button) {
44
+ root.setAttribute("data-pf-theme", mode);
45
+ if (button) {
46
+ button.textContent = LABELS[mode];
47
+ button.setAttribute("aria-label", LABELS[mode] + ". Activate to change.");
48
+ }
49
+ }
50
+
51
+ var toggle = document.getElementById("pf-theme-toggle");
52
+ var initial = stored() || "auto";
53
+ apply(initial, toggle);
54
+
55
+ if (toggle) {
56
+ toggle.hidden = false;
57
+ toggle.addEventListener("click", function () {
58
+ var next = MODES[(MODES.indexOf(root.getAttribute("data-pf-theme")) + 1) % MODES.length];
59
+ apply(next, toggle);
60
+ remember(next);
61
+ });
62
+ }
63
+
64
+ /* Mark the module the reader is in. Falls back to doing nothing where
65
+ IntersectionObserver is unavailable; the nav still navigates. */
66
+ var links = Array.prototype.slice.call(document.querySelectorAll("[data-pf-nav]"));
67
+ if (links.length > 0 && "IntersectionObserver" in window) {
68
+ var byId = {};
69
+ links.forEach(function (link) { byId[link.getAttribute("data-pf-nav")] = link; });
70
+
71
+ var visible = {};
72
+ var observer = new IntersectionObserver(function (entries) {
73
+ entries.forEach(function (entry) {
74
+ visible[entry.target.id] = entry.isIntersecting;
75
+ });
76
+ var current = null;
77
+ for (var i = 0; i < links.length; i += 1) {
78
+ var id = links[i].getAttribute("data-pf-nav");
79
+ if (visible[id]) { current = id; break; }
80
+ }
81
+ links.forEach(function (link) {
82
+ var isCurrent = link.getAttribute("data-pf-nav") === current;
83
+ if (isCurrent) {
84
+ link.setAttribute("aria-current", "true");
85
+ } else {
86
+ link.removeAttribute("aria-current");
87
+ }
88
+ });
89
+ }, { rootMargin: "-30% 0px -60% 0px" });
90
+
91
+ Object.keys(byId).forEach(function (id) {
92
+ var target = document.getElementById(id);
93
+ if (target) observer.observe(target);
94
+ });
95
+ }
96
+
97
+ /* Quiz feedback. The correct answer is in the document either way; choosing
98
+ an option just says so sooner, and opens the explanation that was always
99
+ one click away. */
100
+ document.addEventListener("change", function (event) {
101
+ var input = event.target;
102
+ if (!input || input.type !== "radio" || !input.hasAttribute("data-pf-answer")) return;
103
+
104
+ var question = input.closest("[data-pf-question]");
105
+ if (!question) return;
106
+
107
+ var chosenIsCorrect = input.getAttribute("data-pf-answer") === "correct";
108
+ Array.prototype.forEach.call(question.querySelectorAll(".pf-option"), function (option) {
109
+ var field = option.querySelector("input[type=radio]");
110
+ var verdict = option.querySelector(".pf-verdict");
111
+ var correct = field.getAttribute("data-pf-answer") === "correct";
112
+ if (field.checked && !correct) {
113
+ option.setAttribute("data-pf-verdict", "incorrect");
114
+ if (verdict) verdict.textContent = "Not this one";
115
+ } else if (correct && (field.checked || !chosenIsCorrect)) {
116
+ option.setAttribute("data-pf-verdict", "correct");
117
+ if (verdict) verdict.textContent = "Correct";
118
+ } else {
119
+ option.removeAttribute("data-pf-verdict");
120
+ if (verdict) verdict.textContent = "";
121
+ }
122
+ });
123
+
124
+ var reveal = question.querySelector(".pf-reveal");
125
+ if (reveal) reveal.open = true;
126
+ });
127
+ })();
128
+ `.trim();