@erclx/canon 4.106.0 → 4.108.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/document-health/REQUIREMENT.md +46 -0
- package/claude/skills/document-health/SKILL.md +88 -0
- package/claude/skills/document-health/references/axes.md +78 -0
- package/docs/agents/capture.md +3 -3
- package/docs/agents/commands.md +1 -1
- package/docs/agents/teach.md +2 -0
- package/docs/workflow/ai-workflow.md +1 -0
- package/package.json +1 -1
- package/scripts/core/regen-hero.sh +19 -4
- package/src/claude/cases/workflow.ts +10 -0
- package/src/cli.ts +96 -40
- package/src/design/components.ts +13 -3
- package/src/gate/stages.ts +3 -5
- package/src/teach/nav.ts +14 -2
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: document-health
|
|
3
|
+
description: What the document health check is for, the gap it closes, and why it reports three axes rather than one number
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Document health requirement
|
|
7
|
+
|
|
8
|
+
## Gap
|
|
9
|
+
|
|
10
|
+
Without this skill, a project learns which documents are missing and never learns which have rotted. `target-check` reports per domain what a target holds against what the toolkit ships, and every one of its domains asks a structural question: does the file exist, does it hold the required sections, do the bytes match what shipped. A document that exists, holds every section, and describes a tree that moved six months ago passes every one of them.
|
|
11
|
+
|
|
12
|
+
The second half of the gap is the axes being read as one thing. Length, placement, and staleness answer to different evidence, and a run that reports them in one undifferentiated list hands the reader a judgment wearing the authority of a measurement. The operator's own complaint was staleness, which is the axis with nothing behind it, so the flattening runs in the direction that overstates the weakest finding.
|
|
13
|
+
|
|
14
|
+
The third is silence on an unread axis. Both measured verbs resolve their folders by surface root, so a project whose layout differs resolves nothing and the verb refuses. A run reading that refusal as an empty finding set reports a healthy corpus for documents nothing looked at.
|
|
15
|
+
|
|
16
|
+
## Must
|
|
17
|
+
|
|
18
|
+
- Report per document across three named axes, so a reader can tell which question a finding answers
|
|
19
|
+
- Read length and placement through the verb that already owns each measurement, rather than re-deriving either by hand
|
|
20
|
+
- Mark every finding with its evidence class, so a judgment is never read as a measurement
|
|
21
|
+
- Ship the staleness axis, and mark it as a reading rather than cutting it for lacking a verb
|
|
22
|
+
- Report a refusal, an absent key, and a missing subcommand as an unread axis, naming the cause
|
|
23
|
+
- Say which layer answered the classifier sweep, since its model layer being off narrows the read without refusing it
|
|
24
|
+
- Take every checkpoint from the record the verb returns rather than from a number written into the skill
|
|
25
|
+
- Run standalone, and stay callable from the structural check rather than reachable only through it
|
|
26
|
+
|
|
27
|
+
## Must not
|
|
28
|
+
|
|
29
|
+
- Repair a finding, write a file in the project, or open a pull request. A check that repaired as it read leaves the reader unable to tell a finding from a fix.
|
|
30
|
+
- Emit a verdict across the three axes, or a score across the documents. Two axes answer from verbs and one answers from a reading, so any roll-up is a figure that lies about the third.
|
|
31
|
+
- Report a finding against a file nobody hand-authors. A generated index, a gitignored file, and a copied reference each name a file an author cannot fix.
|
|
32
|
+
- Answer an axis by file inspection where a verb answers it. The verb is the surface under test and a hand-rolled read of the same files is not.
|
|
33
|
+
- Read a document's modification time as its staleness. A file untouched beside code untouched is current, and the timestamp answers a different question.
|
|
34
|
+
|
|
35
|
+
## Guards
|
|
36
|
+
|
|
37
|
+
- `canon` absent from the path: stop, since neither measured axis can be read
|
|
38
|
+
- No tracked markdown under the working root: stop, since there is no corpus to read
|
|
39
|
+
|
|
40
|
+
## Out of scope
|
|
41
|
+
|
|
42
|
+
- Whether a document exists or holds its required sections, which is the structural question `target-check` answers across six domains
|
|
43
|
+
- Conformance of changed markdown to the authoring standards declaring jurisdiction over its paths, which is `standards-audit`. That skill is diff-scoped and standard-declared, taking a changed file and reporting which stated rule it breaks with a line number. This one is corpus-scoped and reads rot in documents nobody changed, which is the case a diff-scoped audit never reaches. The two sit beside each other rather than one absorbing the other.
|
|
44
|
+
- Rewriting a stale claim once this run has named it, which is `markdown-propose`
|
|
45
|
+
- Running the toolkit's own checks as one gating set over its own corpus, which is `canon audits run`. That set gates a push and this reports to a reader.
|
|
46
|
+
- Repairing anything an axis names. Each routes to the command or skill that owns it.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: document-health
|
|
3
|
+
description: Reports which of a project's documents have rotted, one section per document, across length, placement, and staleness. Reads each axis through the verb that owns it and marks the evidence class each answer carries. Use when asked to "check document health", "which docs have gone stale", "audit the docs for rot", "are these documents still accurate", "which docs are too long", or after a target check reports the structural side. Do NOT use to repair a finding, which the owning command or skill does, and do NOT use to check whether a document exists or holds the right sections, which is `target-check`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Document health
|
|
7
|
+
|
|
8
|
+
Reports which documents have rotted rather than which are missing. `target-check` answers whether a project's documents exist and hold the right kind of content, and this answers whether the ones that exist still earn their place. It reports and never repairs, matching the structural check beside it.
|
|
9
|
+
|
|
10
|
+
Three axes carry the report. Two answer from a verb and the third answers from a reading, which it says rather than hiding. `${CLAUDE_SKILL_DIR}/references/axes.md` holds what each axis measures, the checkpoint it reads against, and where a finding routes. This body holds the order, the guards, and the report shape.
|
|
11
|
+
|
|
12
|
+
## Guards
|
|
13
|
+
|
|
14
|
+
- `canon` absent from the path: stop. `❌ canon is not on PATH, so neither measured axis can be read.`
|
|
15
|
+
- `git ls-files '*.md'` listing nothing: stop. `❌ No tracked markdown here, so there is no corpus to read.`
|
|
16
|
+
- Run every command under `CANON_NON_INTERACTIVE=1` and prompt at no point. A caller reaching this from a rollout wave has nobody present to answer.
|
|
17
|
+
- Branch on each record's own fields rather than on an exit status. An operator's shell profile may wrap `canon` in a function whose status comes from a trailing command, which reports every refusal as success.
|
|
18
|
+
- Repair nothing, write no file in the project, and open no pull request. Whoever read the report decides what to fix.
|
|
19
|
+
|
|
20
|
+
## Step 1: scope the corpus
|
|
21
|
+
|
|
22
|
+
Take the documents the project hand-authors, defaulting to every tracked markdown file the two measured verbs reach. Narrow to a path the caller named when one was given, and say which scope the run took.
|
|
23
|
+
|
|
24
|
+
Drop what nobody hand-authors before reporting a finding against it: a generated `index.md`, a file under a gitignored folder, and a bundled reference a generator copied out to its consumers. A finding against a copy names a file an author cannot fix, since the next regeneration overwrites it.
|
|
25
|
+
|
|
26
|
+
## Step 2: read the two measured axes
|
|
27
|
+
|
|
28
|
+
Run these from the project root, in parallel:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
CANON_NON_INTERACTIVE=1 canon markdown audit --json
|
|
32
|
+
CANON_NON_INTERACTIVE=1 canon context audit --json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The first answers length, carrying the bullet, paragraph, and depth checkpoints and the per-file weights read against them. The second answers placement for the folders it resolves, carrying entry length, reference form, and index drift.
|
|
36
|
+
|
|
37
|
+
Then read which classifier would run, and deepen placement with the verdicts per section:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
CANON_NON_INTERACTIVE=1 canon context classifier show
|
|
41
|
+
CANON_NON_INTERACTIVE=1 canon context classify sweep --json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The first names the backend, the model, and the source that decided both. Report which layer answered, since a project with the model layer off reads from the regex layer alone and the sweep says nothing about which one it used.
|
|
45
|
+
|
|
46
|
+
Take the checkpoints from the record each verb returns rather than from a number written here. A checkpoint the CLI moves reaches this report with no edit, and a number held in this body goes stale on its own cadence.
|
|
47
|
+
|
|
48
|
+
### An unread axis is not a healthy one
|
|
49
|
+
|
|
50
|
+
A refusal, an absent key, and a subcommand the project's binary does not carry each leave an axis unread. Report it as unread against the documents it would have covered, name the read that could not be taken, and name the cause.
|
|
51
|
+
|
|
52
|
+
`canon context audit` refuses with `reason: "no-folders"` in a project whose folders resolve to nothing, which is the ordinary shape where the layout differs from the one the verb looks for. That refusal is the honest answer rather than a failure, and reading it as an empty finding set reports a healthy corpus for documents nothing looked at.
|
|
53
|
+
|
|
54
|
+
A narrower read is not an unread one. The sweep running on its regex layer alone still answered, so it is reported with the layer named rather than as an axis nothing reached.
|
|
55
|
+
|
|
56
|
+
## Step 3: read the staleness axis
|
|
57
|
+
|
|
58
|
+
No verb answers whether a document still describes the tree it was written about. Read each document against the surfaces it names and report where the two have parted, taking the file paths, commands, and counts it states as the claims to test.
|
|
59
|
+
|
|
60
|
+
Mark every staleness finding as a reading. It is this session's judgment rather than a measurement, and a report that sets it beside two verb-backed axes without marking it implies evidence the axis does not carry.
|
|
61
|
+
|
|
62
|
+
## Step 4: report per document
|
|
63
|
+
|
|
64
|
+
Give each document its three axes, and give each axis three things: the state, the evidence class, and the command or skill that repairs it. Naming the repair is not running it.
|
|
65
|
+
|
|
66
|
+
The three classes are what keep the axes apart:
|
|
67
|
+
|
|
68
|
+
- `measured`: a verb returned a number read against a stated checkpoint
|
|
69
|
+
- `classified`: a verb returned a verdict carrying its own confidence
|
|
70
|
+
- `read`: this session's judgment, backed by no verb
|
|
71
|
+
|
|
72
|
+
Emit no verdict across the three. Two answer from verbs and one answers from a reading, so a roll-up is a figure that lies about the third. Emit no score across the documents either, for the same reason one level up.
|
|
73
|
+
|
|
74
|
+
## Output
|
|
75
|
+
|
|
76
|
+
Lead with the table, then one detail block per document carrying a finding:
|
|
77
|
+
|
|
78
|
+
```markdown
|
|
79
|
+
Document health: `<scope>`, <n> documents read.
|
|
80
|
+
|
|
81
|
+
| Document | Length | Placement | Staleness |
|
|
82
|
+
| ---------------- | ------------------- | ------------------- | ---------------- |
|
|
83
|
+
| `path/to/doc.md` | ok / heavy / unread | ok / moved / unread | ok / stale / n/a |
|
|
84
|
+
|
|
85
|
+
Length and placement are measured. Staleness is a reading.
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Keep the evidence line under the table on every run. A table read without it flattens a judgment into a measurement, which is the one shape the three classes exist to prevent.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Document health axes
|
|
3
|
+
description: What each axis measures, the verb behind it, the evidence class it carries, and where a finding routes
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Document health axes
|
|
7
|
+
|
|
8
|
+
One section per axis. Each states what it measures, the verb that answers it, the evidence class the answer carries, and the surface a finding routes to.
|
|
9
|
+
|
|
10
|
+
The skill body holds the order, the guards, and the report shape. This holds the detail a run dereferences once it has a finding to place.
|
|
11
|
+
|
|
12
|
+
## Length
|
|
13
|
+
|
|
14
|
+
**Measures:** whether a document, or a passage inside it, has grown past what a reader takes in one pass.
|
|
15
|
+
|
|
16
|
+
**Verb:** `canon markdown audit --json`.
|
|
17
|
+
|
|
18
|
+
**Evidence class:** `measured`.
|
|
19
|
+
|
|
20
|
+
The record carries the checkpoints it read against under `checkpoints` and the per-file weights under `entries`. Read both from the record rather than from a number stated here, since the CLI moves a checkpoint on its own cadence and a copy of one goes stale in place.
|
|
21
|
+
|
|
22
|
+
Four weights answer this axis, each naming a different shape of overgrowth:
|
|
23
|
+
|
|
24
|
+
- `heavyBullets`: a top-level bullet past the character checkpoint, which is a paragraph wearing a dash
|
|
25
|
+
- `heavyParagraphs`: a paragraph past the sentence or character checkpoint
|
|
26
|
+
- `longestRun`, against `longestRunLine`: unbroken rendered lines between headings, which is the depth checkpoint
|
|
27
|
+
- `cadence`, carrying `measured`, `flat`, and `repeating`: sentence-length spread and repeated openers
|
|
28
|
+
|
|
29
|
+
Weight is a judgment a reader settles rather than a defect, which the verb states for itself and which this axis carries forward. A finding here is an argument for a split or a trim rather than a rule broken.
|
|
30
|
+
|
|
31
|
+
**Routes to:** a split of the document, or a move of the overgrown passage to the surface that owns it. Where the project states what a given document may hold, that statement decides whether the passage belongs in it at all, and the finding routes there rather than to a trim.
|
|
32
|
+
|
|
33
|
+
**Caveat:** the cadence numbers are drawn from prose a person reads straight through. Terse reference prose sits below that range correctly, so a flat paragraph in a table-heavy or bullet-heavy file is not a finding.
|
|
34
|
+
|
|
35
|
+
## Placement
|
|
36
|
+
|
|
37
|
+
**Measures:** whether a document's content sits in the file that owns it, and whether the catalogs pointing at it still resolve.
|
|
38
|
+
|
|
39
|
+
**Verbs:** `canon context audit --json` for the structural half, `canon context classify sweep --json` for the per-section verdicts.
|
|
40
|
+
|
|
41
|
+
**Evidence class:** `measured` for the audit's findings, `classified` for the sweep's verdicts.
|
|
42
|
+
|
|
43
|
+
The audit answers required sections, entry length, citation resolution, reference form, catalog tables, provenance, and index drift. A citation resolving to nothing is a fact and gates. Entry length, reference form, table shape, and provenance are judgments under every flag.
|
|
44
|
+
|
|
45
|
+
The sweep classifies each section as keep, replace or rewrite, history, or move. A `move` verdict is the placement finding proper, naming content sitting in the wrong document. Each verdict carries its own confidence, which is why the class is `classified` rather than `measured`, and a low-confidence `move` is a prompt to look rather than an instruction to cut.
|
|
46
|
+
|
|
47
|
+
**Routes to:** the document the verdict names as the owner. Where the move crosses a tier rather than a file, the project's own content-ownership rules decide the destination.
|
|
48
|
+
|
|
49
|
+
**Caveat:** both verbs resolve their folders by surface root rather than by a path written into them, so a project whose layout differs from the one they look for resolves nothing. The audit says so, refusing with `reason: "no-folders"` and naming the folders it looked for. Report that refusal as an unread axis. The sweep degrades differently: with the model layer off it answers from its regex layer alone, which is a narrower read rather than a refusal, so say which layer answered.
|
|
50
|
+
|
|
51
|
+
## Staleness
|
|
52
|
+
|
|
53
|
+
**Measures:** whether a document still describes the tree it was written about.
|
|
54
|
+
|
|
55
|
+
**Verb:** none.
|
|
56
|
+
|
|
57
|
+
**Evidence class:** `read`.
|
|
58
|
+
|
|
59
|
+
This is the axis the operator names first and the one nothing measures. A document goes stale by the tree moving under it rather than by anything changing in the document, so no property of the file answers it and a timestamp answers a different question. A file untouched for a year beside code untouched for a year is current, and one edited yesterday against a refactor from this morning is stale.
|
|
60
|
+
|
|
61
|
+
Read each document against the surfaces it names. Four claim shapes are testable without leaving the project:
|
|
62
|
+
|
|
63
|
+
- A file or folder path the document states, against whether it resolves
|
|
64
|
+
- A command or verb the document names, against whether it exists and carries the flags shown
|
|
65
|
+
- A count the document states over a tree, against the tree
|
|
66
|
+
- A cross-reference to another document, against what that document now says
|
|
67
|
+
|
|
68
|
+
A claim resting on none of the four is unverifiable from here. Report it as unread rather than as passing, which is the same rule the measured axes take when a verb refuses.
|
|
69
|
+
|
|
70
|
+
**Routes to:** the skill that rewrites a stale claim against an answer, rather than a direct edit from here. A document health run reports and never repairs.
|
|
71
|
+
|
|
72
|
+
**Caveat:** a reading is the weakest evidence in the report and the most consequential finding in it, which is the tension this axis ships with. Marking the class is what keeps the two facts together in the reader's hands.
|
|
73
|
+
|
|
74
|
+
## Why the three stay apart
|
|
75
|
+
|
|
76
|
+
The axes are not equally actionable and a report flattening them implies they are. A length finding is a number against a checkpoint, a placement finding is a verdict carrying a confidence, and a staleness finding is a judgment with nothing behind it. Each is worth reporting and each is worth a different response, so the class travels beside the state on every row rather than being stated once in a preamble a reader skims.
|
|
77
|
+
|
|
78
|
+
Shipping staleness on weaker evidence than the other two is the trade taken deliberately. Cutting it would leave the report answering the two questions that were never the operator's complaint.
|
package/docs/agents/capture.md
CHANGED
|
@@ -18,7 +18,7 @@ canon capture https://example.com --selector .window --out preview.png
|
|
|
18
18
|
|
|
19
19
|
## What this repository captures
|
|
20
20
|
|
|
21
|
-
A capture set here spans two folders. `assets/captures/` holds five sources and the three templates they are written from, and `assets/evidence/` holds their images and the built-page capture a document points at with the stamp answering for each, under the segment `canon pr evidence` compares. One run over the source folder rebuilds every image, and it takes `--out assets/evidence` to put each PNG where its document points rather than beside the markup it rendered. None of the sources is edited by hand. `scripts/core/regen-hero.sh` writes each `.html` from the template beside it, filling one shared value map into all of them, and `bun run check`
|
|
21
|
+
A capture set here spans two folders. `assets/captures/` holds five sources and the three templates they are written from, and `assets/evidence/` holds their images and the built-page capture a document points at with the stamp answering for each, under the segment `canon pr evidence` compares. One run over the source folder rebuilds every image, and it takes `--out assets/evidence` to put each PNG where its document points rather than beside the markup it rendered. None of the sources is edited by hand. `scripts/core/regen-hero.sh` writes each `.html` from the template beside it, filling one shared value map into all of them. A branch commits none of them for a moved count or token, since the refresh workflow regenerates them from `main` and opens one pull request, and `bun run check` runs the script with `--check`, which fills every template into a temp folder and fails when the regeneration cannot run.
|
|
22
22
|
|
|
23
23
|
Two of the three templates take catalog data, so a stack gaining a rule moves the frame on the next run. `install.html` is the exception, since it holds terminal text from a real run in its template rather than derived from a live catalog at build time. `assets/evidence/arrival.png` is the one image with no source here: it is the built page's `.fold` element captured in dark mode against a local `web/` preview, since `canon capture` takes no color scheme and the page follows the browser's default of light, so it is retaken by hand when the page's first screen changes.
|
|
24
24
|
|
|
@@ -26,9 +26,9 @@ The README shows each still in two themes through `<picture>`, so a frame is a p
|
|
|
26
26
|
|
|
27
27
|
A stamp for a URL source digests the address string rather than the page it served, so two themes of one address carry one `source-sha256` and only `image-sha256` tells the frames apart. That holds for `examples/teach/evidence/root-listing-light.png`, which is retaken by serving a copy of the fixture with `data-theme="light"` on its `<html>` element at the same address, since the page follows the dark default and the command takes no scheme.
|
|
28
28
|
|
|
29
|
-
`assets/captures/` is read flat and never descends, by the regeneration script
|
|
29
|
+
`assets/captures/` is read flat and never descends, by the regeneration script and by this command alike. A source nested a further folder down is skipped by both with nothing reported, so a new frame is a template dropped directly in rather than a folder of its own. The split is what retired the name prefix the flat layout used to need: a frame is named for itself now, since `assets/captures/` no longer mixes markup in with the images.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
A branch is not asked whether the HTML is current. The PNG is a chromium render whose bytes move with the browser version, so the refresh workflow rebuilds it with `canon capture assets/captures --selector .window --out assets/evidence` only when the regenerated HTML changed.
|
|
32
32
|
|
|
33
33
|
Every render writes a stamp beside its PNG, `hero.png` next to `hero.stamp`, holding the source filename, a `source-sha256` over the markup bytes it read, and an `image-sha256` over the image bytes it wrote. Both digests are what `bun run check` compares, so a markup edit committed without a capture and a PNG swapped under unchanged markup each fail. The stamp is tracked and commits alongside the pair. A capture that cannot write it reports that source as failed and exits 1, so an image whose stamp never landed is reported rather than passed over.
|
|
34
34
|
|
package/docs/agents/commands.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Every project-level command and every domain subcommand, plus the s
|
|
|
5
5
|
|
|
6
6
|
# Command catalog
|
|
7
7
|
|
|
8
|
-
Full help: `canon <command> --help`. Behavior notes for the install and sync verbs live in `install-and-sync.md`.
|
|
8
|
+
Full help: `canon <command> --help`. Bare `canon --help` lists every top-level command under five headings, Project, Domains, Author and render, Session and ship, and Repo reports, with descriptions cut at 80 columns. Behavior notes for the install and sync verbs live in `install-and-sync.md`.
|
|
9
9
|
|
|
10
10
|
## Project-level
|
|
11
11
|
|
package/docs/agents/teach.md
CHANGED
|
@@ -174,6 +174,8 @@ With no topic it rewrites every workspace. The teach-root listing is always rewr
|
|
|
174
174
|
|
|
175
175
|
A lesson carries its chrome as four marker pairs the authoring skill writes empty: `canon:teach:style`, `canon:teach:header`, `canon:teach:footnav`, and `canon:teach:scripts`. This verb splices each one from the current workspace state, embedding the shared stylesheet, rebuilding the breadcrumb and jump menus, the prev/next footer nav, and the behavior scripts, and leaves the authored `<h1>`, lede, body, and quiz between the header and the footnav untouched.
|
|
176
176
|
|
|
177
|
+
A workspace carrying no `course.css` gets its stylesheet pair seeded first, so the contents page it links is never unstyled, and an existing `course.css` is never overwritten. The embedded copy drops the seed's `@import` line, which resolved against the lesson's own folder and never loaded anything.
|
|
178
|
+
|
|
177
179
|
A lesson missing one of the four marker pairs is refused by name rather than rewritten, and every other lesson in the run still rewrites. The record's `skipped` list carries the refused files and which marker each is missing.
|
|
178
180
|
|
|
179
181
|
The `style` region carries the quiz stepper after the workspace stylesheet, for every lesson but the one shape named three paragraphs down, so it wins the cascade at equal specificity and reaches a workspace seeded before it existed. The stepper is CSS over the radio inputs a quiz is written from: it hides every question that follows one holding no checked input, and hides a question's feedback until that question does. The general sibling combinator is what keeps that from depending on the markup contract holding, since it selects the same questions as the adjacent one for a quiz written correctly and goes on gating when anything sits between two of them, where the adjacent one matches nothing and shows them all at once. Gating there rather than in a script is what keeps a later stem from answering an earlier question with nothing to bind, and the whole block sits inside `@supports selector(:has(*))`, so an engine without `:has()` renders every question rather than a stepper showing nothing. Feedback there follows the workspace stylesheet, shown by one that says nothing about `.fb` and hidden by one grown under the retired script.
|
|
@@ -240,6 +240,7 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
240
240
|
| ------------------------ | --------------------------------------------------------------------------------------- |
|
|
241
241
|
| `canon:review-branch` | On the local branch diff, before anything is pushed |
|
|
242
242
|
| `canon:standards-audit` | When changed markdown has to answer to the authoring standards |
|
|
243
|
+
| `canon:document-health` | When the documents themselves have to answer for length, placement, and staleness |
|
|
243
244
|
| `canon:markdown-propose` | When a markdown claim needs rewriting and the change should wait for an answer per file |
|
|
244
245
|
| `canon:ux-audit` | To read UI source for missing states, edge cases, and inconsistencies |
|
|
245
246
|
| `canon:ux-measure` | To start the interface and measure paint, processor, and layout cost |
|
package/package.json
CHANGED
|
@@ -46,6 +46,21 @@ PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
|
46
46
|
ASSET_DIR="$PROJECT_ROOT/assets/captures"
|
|
47
47
|
LISTED=10
|
|
48
48
|
|
|
49
|
+
# `--check` fills every template into a temp folder and discards it, so a renamed
|
|
50
|
+
# featured skill, an empty catalog, or an unresolved placeholder still fails a
|
|
51
|
+
# branch while no frame file changes. Frames are refreshed after a merge rather
|
|
52
|
+
# than committed by each branch, so a branch asks whether the regen can run and
|
|
53
|
+
# never whether its output is current.
|
|
54
|
+
OUT_DIR="$ASSET_DIR"
|
|
55
|
+
case "${1:-}" in
|
|
56
|
+
'') ;;
|
|
57
|
+
--check) OUT_DIR="$(mktemp -d)" ;;
|
|
58
|
+
*)
|
|
59
|
+
echo "regen-hero: unknown argument $1" >&2
|
|
60
|
+
exit 1
|
|
61
|
+
;;
|
|
62
|
+
esac
|
|
63
|
+
|
|
49
64
|
# `bun src/cli.ts` rather than `canon`, since a globally linked binary resolves to
|
|
50
65
|
# the main checkout no matter which worktree is running.
|
|
51
66
|
catalog() {
|
|
@@ -97,7 +112,7 @@ done
|
|
|
97
112
|
# fails the exec with E2BIG before any stage can report a stale count. A file
|
|
98
113
|
# path is bounded whatever the catalogs grow to.
|
|
99
114
|
PAYLOAD_DIR="$(mktemp -d)"
|
|
100
|
-
trap 'rm -rf "$PAYLOAD_DIR"' EXIT
|
|
115
|
+
trap 'rm -rf "$PAYLOAD_DIR"; [ "$OUT_DIR" = "$ASSET_DIR" ] || rm -rf "$OUT_DIR"' EXIT
|
|
101
116
|
|
|
102
117
|
printf '%s' "$COUNTS_JSON" >"$PAYLOAD_DIR/counts.json"
|
|
103
118
|
printf '%s' "$SKILLS_JSON" >"$PAYLOAD_DIR/skills.json"
|
|
@@ -109,12 +124,12 @@ printf '%s' "$COMMANDS_HELP" >"$PAYLOAD_DIR/commands-help.txt"
|
|
|
109
124
|
printf '%s' "$TOKEN_CSS" >"$PAYLOAD_DIR/tokens.css"
|
|
110
125
|
|
|
111
126
|
export PAYLOAD_DIR
|
|
112
|
-
export ASSET_DIR LISTED PROJECT_ROOT
|
|
127
|
+
export ASSET_DIR OUT_DIR LISTED PROJECT_ROOT
|
|
113
128
|
|
|
114
129
|
bun --eval '
|
|
115
130
|
const { readFileSync, readdirSync } = require("node:fs")
|
|
116
131
|
|
|
117
|
-
const { PAYLOAD_DIR, ASSET_DIR, LISTED, PROJECT_ROOT } = process.env
|
|
132
|
+
const { PAYLOAD_DIR, ASSET_DIR, OUT_DIR, LISTED, PROJECT_ROOT } = process.env
|
|
118
133
|
|
|
119
134
|
const payload = (name) => readFileSync(PAYLOAD_DIR + "/" + name + ".json", "utf8")
|
|
120
135
|
|
|
@@ -438,6 +453,6 @@ for (const { template, out, tokens } of frames) {
|
|
|
438
453
|
process.exit(1)
|
|
439
454
|
}
|
|
440
455
|
|
|
441
|
-
await Bun.write(
|
|
456
|
+
await Bun.write(OUT_DIR + "/" + out, html)
|
|
442
457
|
}
|
|
443
458
|
'
|
|
@@ -117,6 +117,16 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
117
117
|
'Check whether the markdown I changed violates any authoring standards.',
|
|
118
118
|
expect: 'standards-audit',
|
|
119
119
|
},
|
|
120
|
+
// Rot across documents nobody changed is what separates this from
|
|
121
|
+
// `standards-audit`, which is diff-scoped and reports a stated rule broken on
|
|
122
|
+
// a given line. It is also not `target-check`, which asks whether a document
|
|
123
|
+
// exists and holds its sections rather than whether it still describes the
|
|
124
|
+
// tree.
|
|
125
|
+
{
|
|
126
|
+
prompt:
|
|
127
|
+
'Which of our docs have gone stale, grown too long, or ended up in the wrong file?',
|
|
128
|
+
expect: 'document-health',
|
|
129
|
+
},
|
|
120
130
|
{
|
|
121
131
|
prompt: 'Open a new entry on the task board for this piece of work.',
|
|
122
132
|
expect: 'task-board',
|
package/src/cli.ts
CHANGED
|
@@ -45,6 +45,95 @@ import { register as upgrade } from '@/commands/upgrade'
|
|
|
45
45
|
import { readInstalled, UNKNOWN_LABEL } from '@/version/installed'
|
|
46
46
|
import { palette } from '@/ui'
|
|
47
47
|
|
|
48
|
+
const HELP_WIDTH = 80
|
|
49
|
+
const SIGNATURE_WIDTH = 19
|
|
50
|
+
const ROW_INDENT = '│ '.length
|
|
51
|
+
|
|
52
|
+
type CommandRow = [signature: string, description: string]
|
|
53
|
+
|
|
54
|
+
// The grouping mirrors the README's domain table and has no other source, so a
|
|
55
|
+
// command added to the program needs a row here or help-option.test.ts fails.
|
|
56
|
+
const COMMAND_GROUPS: { title: string; rows: CommandRow[] }[] = [
|
|
57
|
+
{
|
|
58
|
+
title: 'Project',
|
|
59
|
+
rows: [
|
|
60
|
+
['init [path]', 'Bootstrap a project with toolkit domains'],
|
|
61
|
+
['sync [path]', 'Sync all installed domains in a project'],
|
|
62
|
+
['upgrade', 'Reinstall the CLI with the manager that installed it'],
|
|
63
|
+
['migrate [cmd]', 'Move a project off a retired name or layout'],
|
|
64
|
+
['targets [cmd]', 'Projects this toolkit installed into (list, pulls)'],
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
title: 'Domains',
|
|
69
|
+
rows: [
|
|
70
|
+
['gov [cmd]', 'Governance rules (install, sync, list)'],
|
|
71
|
+
['standards [cmd]', 'Standards (install, sync, list, <name>)'],
|
|
72
|
+
['snippets [cmd]', 'Prompt snippets (create, list)'],
|
|
73
|
+
['tooling [cmd]', 'Tooling stacks (sync, ref, create)'],
|
|
74
|
+
['claude [cmd]', 'Claude workflow (init, sync, setup)'],
|
|
75
|
+
['wiki [cmd]', 'Wiki pages (init)'],
|
|
76
|
+
['indexes [cmd]', 'Regenerate index.md files (regen)'],
|
|
77
|
+
['docs [cmd|topic]', 'Emit toolkit reference docs (list, <topic>)'],
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
title: 'Author and render',
|
|
82
|
+
rows: [
|
|
83
|
+
['design [cmd]', 'Design system (render, board)'],
|
|
84
|
+
['slides [cmd]', 'Slide decks (render, list)'],
|
|
85
|
+
['capture [source]', 'Render HTML capture sources to PNG'],
|
|
86
|
+
['serve [dir]', 'Serve a directory on localhost, print the link'],
|
|
87
|
+
['demo [cmd]', 'Record a running app (compile, run)'],
|
|
88
|
+
['inventory [subj]', 'Report one computed property across every route'],
|
|
89
|
+
['drive <url> <run>', 'Walk a page through interactions, measure each'],
|
|
90
|
+
['transcripts <url>', 'Fetch a YouTube transcript with frontmatter'],
|
|
91
|
+
['teach [cmd]', 'Learning workspaces (list, open, resource, glossary)'],
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
title: 'Session and ship',
|
|
96
|
+
rows: [
|
|
97
|
+
['sandbox [cat:cmd]', 'Provision and run sandbox scenarios'],
|
|
98
|
+
['tasks [cmd]', 'Task board (validate, archive)'],
|
|
99
|
+
['intake [cmd]', 'Intake folders under .canon/intake/ (list, answer)'],
|
|
100
|
+
['records [cmd]', 'Session records (validate, size, push, pull)'],
|
|
101
|
+
['sessions [cmd]', 'Resolve live sessions to worktree and branch'],
|
|
102
|
+
['worktrees [cmd]', 'Reclaim worktrees whose branches merged'],
|
|
103
|
+
['autoship [cmd]', 'Decide whether a changed set needs review'],
|
|
104
|
+
['pr [cmd]', 'Read a pull request (key-changes, head, checks)'],
|
|
105
|
+
['feedback', 'Write toolkit feedback from stdin'],
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
title: 'Repo reports',
|
|
110
|
+
rows: [
|
|
111
|
+
['audits [cmd]', 'Run every health check as one set (run, list)'],
|
|
112
|
+
['gate [cmd]', 'Run the merge gate stage by stage (run)'],
|
|
113
|
+
['secrets [cmd]', 'Scan the shipped tree for credentials (scan)'],
|
|
114
|
+
['deps [cmd]', 'Read the dependency set for advisories (audit)'],
|
|
115
|
+
['labels [cmd]', 'Read a changed set against the label map (audit)'],
|
|
116
|
+
['comments [cmd]', 'Measure comment density and trend (scan)'],
|
|
117
|
+
['context [cmd]', 'Report context folder health (audit)'],
|
|
118
|
+
['markdown [cmd]', 'Report markdown against the standards (audit)'],
|
|
119
|
+
['repo [cmd]', 'Remote metadata (metadata propose, apply)'],
|
|
120
|
+
['census [path]', 'Report file count, extensions, and line totals'],
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
// A row past the width is cut rather than wrapped, since a wrapped row breaks
|
|
126
|
+
// the alignment the grouping exists to buy.
|
|
127
|
+
function commandRow(signature: string, description: string): string {
|
|
128
|
+
const { GREY, NC } = palette(process.stdout)
|
|
129
|
+
const room = HELP_WIDTH - ROW_INDENT - SIGNATURE_WIDTH - '# '.length
|
|
130
|
+
const text =
|
|
131
|
+
description.length > room
|
|
132
|
+
? `${description.slice(0, room - 1)}…`
|
|
133
|
+
: description
|
|
134
|
+
return `${GREY}│${NC} ${signature.padEnd(SIGNATURE_WIDTH)}${GREY}# ${text}${NC}`
|
|
135
|
+
}
|
|
136
|
+
|
|
48
137
|
function showHelp(): void {
|
|
49
138
|
// The help text is the one framed surface written to stdout, so it asks
|
|
50
139
|
// about that stream rather than the stderr every other writer here uses.
|
|
@@ -54,46 +143,13 @@ function showHelp(): void {
|
|
|
54
143
|
`${GREY}├${NC} ${WHITE}Usage:${NC} canon [command]`,
|
|
55
144
|
`${GREY}│${NC}`,
|
|
56
145
|
`${GREY}│${NC} ${WHITE}Commands:${NC}`,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`${GREY}│${NC} claude [cmd] ${GREY}# Claude workflow (init, sync, setup)${NC}`,
|
|
65
|
-
`${GREY}│${NC} wiki [cmd] ${GREY}# Wiki commands (init)${NC}`,
|
|
66
|
-
`${GREY}│${NC} indexes [cmd] ${GREY}# Regenerate index.md files (regen)${NC}`,
|
|
67
|
-
`${GREY}│${NC} docs [cmd|topic] ${GREY}# Emit toolkit reference docs (list, <topic>)${NC}`,
|
|
68
|
-
`${GREY}│${NC} design [cmd] ${GREY}# Design system commands (render, board)${NC}`,
|
|
69
|
-
`${GREY}│${NC} slides [cmd] ${GREY}# Slide deck commands (render, list)${NC}`,
|
|
70
|
-
`${GREY}│${NC} capture [source] ${GREY}# Render HTML capture sources to PNG${NC}`,
|
|
71
|
-
`${GREY}│${NC} serve [dir] ${GREY}# Serve a directory over localhost and print the preview link${NC}`,
|
|
72
|
-
`${GREY}│${NC} demo [cmd] ${GREY}# Record a running app (compile, run)${NC}`,
|
|
73
|
-
`${GREY}│${NC} inventory [subj] ${GREY}# Report one computed property across every route${NC}`,
|
|
74
|
-
`${GREY}│${NC} drive <url> <run> ${GREY}# Walk a page through named interactions and measure each state${NC}`,
|
|
75
|
-
`${GREY}│${NC} feedback ${GREY}# Write toolkit feedback from stdin to .canon/feedback/${NC}`,
|
|
76
|
-
`${GREY}│${NC} transcripts <url> ${GREY}# Fetch a YouTube transcript with metadata frontmatter${NC}`,
|
|
77
|
-
`${GREY}│${NC} tasks [cmd] ${GREY}# Task board commands (archive)${NC}`,
|
|
78
|
-
`${GREY}│${NC} intake [cmd] ${GREY}# Intake folders under .canon/intake/ (list, answer)${NC}`,
|
|
79
|
-
`${GREY}│${NC} teach [cmd] ${GREY}# Learning workspaces under .canon/teach/ (list, open, resource, glossary)${NC}`,
|
|
80
|
-
`${GREY}│${NC} comments [cmd] ${GREY}# Measure comment density and trend (scan)${NC}`,
|
|
81
|
-
`${GREY}│${NC} context [cmd] ${GREY}# Report context folder health (audit)${NC}`,
|
|
82
|
-
`${GREY}│${NC} markdown [cmd] ${GREY}# Report markdown against the attribute standards (audit)${NC}`,
|
|
83
|
-
`${GREY}│${NC} records [cmd] ${GREY}# Session records under .claude/ (validate, size, push, pull)${NC}`,
|
|
84
|
-
`${GREY}│${NC} sessions [cmd] ${GREY}# Resolve live sessions to worktree and branch (list)${NC}`,
|
|
85
|
-
`${GREY}│${NC} targets [cmd] ${GREY}# Report the projects this toolkit installed into (list, pulls)${NC}`,
|
|
86
|
-
`${GREY}│${NC} worktrees [cmd] ${GREY}# Reclaim the worktrees whose branches merged (list, reclaim)${NC}`,
|
|
87
|
-
`${GREY}│${NC} secrets [cmd] ${GREY}# Read the shipped tree for credential-shaped values (scan)${NC}`,
|
|
88
|
-
`${GREY}│${NC} deps [cmd] ${GREY}# Read the resolved dependency set for advisories (audit)${NC}`,
|
|
89
|
-
`${GREY}│${NC} labels [cmd] ${GREY}# Read a changed set against the pull request label map (audit)${NC}`,
|
|
90
|
-
`${GREY}│${NC} autoship [cmd] ${GREY}# Decide whether a changed set needs the review pass (classify)${NC}`,
|
|
91
|
-
`${GREY}│${NC} pr [cmd] ${GREY}# Read a pull request against its own diff and its branch tip (key-changes, head, checks)${NC}`,
|
|
92
|
-
`${GREY}│${NC} repo [cmd] ${GREY}# This repository's own remote metadata (metadata propose, apply)${NC}`,
|
|
93
|
-
`${GREY}│${NC} census [path] ${GREY}# Report tracked file count, extension breakdown, and line totals${NC}`,
|
|
94
|
-
`${GREY}│${NC} audits [cmd] ${GREY}# Run every health check as one set (run, list)${NC}`,
|
|
95
|
-
`${GREY}│${NC} gate [cmd] ${GREY}# Run the merge gate stage by stage (run)${NC}`,
|
|
96
|
-
`${GREY}│${NC} upgrade ${GREY}# Reinstall the CLI globally with the manager that installed it${NC}`,
|
|
146
|
+
...COMMAND_GROUPS.flatMap((group) => [
|
|
147
|
+
`${GREY}│${NC}`,
|
|
148
|
+
`${GREY}│${NC} ${WHITE}${group.title}${NC}`,
|
|
149
|
+
...group.rows.map(([signature, description]) =>
|
|
150
|
+
commandRow(signature, description),
|
|
151
|
+
),
|
|
152
|
+
]),
|
|
97
153
|
`${GREY}│${NC}`,
|
|
98
154
|
`${GREY}│${NC} ${WHITE}Sandbox:${NC}`,
|
|
99
155
|
`${GREY}│${NC} canon sandbox ${GREY}# Interactive scenario picker${NC}`,
|
package/src/design/components.ts
CHANGED
|
@@ -265,6 +265,8 @@ const TEACH_CHROME: Component = {
|
|
|
265
265
|
--teach-shadow: none;
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
html { scroll-padding-top: calc(var(--teach-mast-h) + 1rem); }
|
|
269
|
+
|
|
268
270
|
/* No horizontal padding on the body, so the sticky bar reaches both window
|
|
269
271
|
edges. The gutter moves onto \`main\`, which is the only thing that needed it. */
|
|
270
272
|
body {
|
|
@@ -282,7 +284,7 @@ body {
|
|
|
282
284
|
listing page and a lesson start at the same left edge as the chrome above
|
|
283
285
|
them. A narrower \`.wide-body\` was what put the navigation and the content on
|
|
284
286
|
two different measures. */
|
|
285
|
-
main { max-width: calc(var(--teach-measure) + 3rem); margin: 0 auto; padding: 2.25rem 1.5rem 0; }
|
|
287
|
+
main { box-sizing: border-box; max-width: calc(var(--teach-measure) + 3rem); margin: 0 auto; padding: 2.25rem 1.5rem 0; }
|
|
286
288
|
.wide-body { max-width: calc(var(--teach-measure) + 3rem); }
|
|
287
289
|
|
|
288
290
|
.bar {
|
|
@@ -346,6 +348,8 @@ main { max-width: calc(var(--teach-measure) + 3rem); margin: 0 auto; padding: 2.
|
|
|
346
348
|
|
|
347
349
|
.nav > a:hover { border-color: var(--color-accent); background: var(--color-teach-accent-bg); }
|
|
348
350
|
.nav > .end { color: var(--color-muted); border-style: dashed; font-weight: 400; box-shadow: none; }
|
|
351
|
+
.nav > :only-child { flex: 0 1 calc(50% - 0.3rem); }
|
|
352
|
+
.nav > .to-next:only-child { margin-left: auto; }
|
|
349
353
|
|
|
350
354
|
.nav .lbl {
|
|
351
355
|
display: block;
|
|
@@ -360,7 +364,6 @@ main { max-width: calc(var(--teach-measure) + 3rem); margin: 0 auto; padding: 2.
|
|
|
360
364
|
.nav > a:hover .lbl { color: var(--color-accent); }
|
|
361
365
|
.nav .to-next { text-align: right; }
|
|
362
366
|
.nav .to-index { flex: 0 0 auto; text-align: center; }
|
|
363
|
-
.nav-foot { margin: 4.5rem 0 0; }
|
|
364
367
|
|
|
365
368
|
/* ---- Masthead contents ---- */
|
|
366
369
|
|
|
@@ -476,7 +479,13 @@ main { max-width: calc(var(--teach-measure) + 3rem); margin: 0 auto; padding: 2.
|
|
|
476
479
|
|
|
477
480
|
/* ---- Footer navigation, the only place it appears ---- */
|
|
478
481
|
|
|
479
|
-
.nav {
|
|
482
|
+
.nav {
|
|
483
|
+
max-width: calc(var(--teach-measure) + 3rem);
|
|
484
|
+
margin: 4rem auto 0;
|
|
485
|
+
padding: 0 1.5rem;
|
|
486
|
+
box-sizing: border-box;
|
|
487
|
+
align-items: stretch;
|
|
488
|
+
}
|
|
480
489
|
|
|
481
490
|
.nav > a, .nav > span { display: flex; flex-direction: column; justify-content: center; }
|
|
482
491
|
|
|
@@ -919,6 +928,7 @@ const TEACH_QUIZ: Component = {
|
|
|
919
928
|
font-family: var(--teach-sans);
|
|
920
929
|
font-size: var(--t4);
|
|
921
930
|
background: transparent;
|
|
931
|
+
box-sizing: border-box;
|
|
922
932
|
width: 100%;
|
|
923
933
|
text-align: left;
|
|
924
934
|
color: inherit;
|
package/src/gate/stages.ts
CHANGED
|
@@ -197,12 +197,10 @@ export const STAGES: readonly Stage[] = [
|
|
|
197
197
|
id: 'hero',
|
|
198
198
|
label: 'Hero',
|
|
199
199
|
checks: [
|
|
200
|
-
script('regen-hero.sh', 'Hero regen failed'),
|
|
201
200
|
{
|
|
202
|
-
kind: '
|
|
203
|
-
|
|
204
|
-
failure:
|
|
205
|
-
'A generated frame drifted from the catalogs or the design source. Run bun run check, then canon capture assets/captures --selector .window --out assets/evidence, and commit each assets/captures/*.html with its .png and .stamp.',
|
|
201
|
+
kind: 'command',
|
|
202
|
+
argv: ['bash', 'scripts/core/regen-hero.sh', '--check'],
|
|
203
|
+
failure: 'Hero regen health check failed',
|
|
206
204
|
},
|
|
207
205
|
{ kind: 'measure', measure: captureStamps },
|
|
208
206
|
],
|
package/src/teach/nav.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type TeachRefused,
|
|
18
18
|
type WorkspaceDetail,
|
|
19
19
|
type WorkspaceSummary,
|
|
20
|
+
writeStylesheet,
|
|
20
21
|
} from '@/teach/workspace'
|
|
21
22
|
|
|
22
23
|
const FAVICON = `<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='10 10 80 80'%3E%3Cpath d='M34,20 L15,28 L15,72 L34,80 Z M66,20 L85,28 L85,72 L66,80 Z' fill='rgb(224,114,75)' /%3E%3Crect x='44' y='15' width='12' height='70' rx='2' fill='rgb(224,114,75)' /%3E%3C/svg%3E" />`
|
|
@@ -769,6 +770,15 @@ interface LessonRefused {
|
|
|
769
770
|
* check first. Nothing is written when any region is missing, which is what
|
|
770
771
|
* keeps a partially-spliced file off disk.
|
|
771
772
|
*/
|
|
773
|
+
/**
|
|
774
|
+
* A lesson embeds the workspace stylesheet, and an `@import` resolves against
|
|
775
|
+
* the lesson's own folder there rather than the assets folder, so it never
|
|
776
|
+
* loaded anything. The base sheet reaches the page through the `<link>`.
|
|
777
|
+
*/
|
|
778
|
+
function stripImports(css: string): string {
|
|
779
|
+
return css.replace(/^@import[^;]*;[ \t]*\n?/gm, '')
|
|
780
|
+
}
|
|
781
|
+
|
|
772
782
|
async function rewriteLesson(
|
|
773
783
|
root: string,
|
|
774
784
|
detail: WorkspaceDetail,
|
|
@@ -899,6 +909,9 @@ export async function generateNav(
|
|
|
899
909
|
const detail = found.workspace
|
|
900
910
|
const metas = await readLessonMetas(root, detail)
|
|
901
911
|
|
|
912
|
+
const cssPath = join(root, detail.path, TEACH_ASSETS, TEACH_STYLESHEET)
|
|
913
|
+
if (!existsSync(cssPath)) await writeStylesheet(root, detail.slug)
|
|
914
|
+
|
|
902
915
|
const contentsPath = join(root, detail.path, 'index.html')
|
|
903
916
|
await writeFile(
|
|
904
917
|
contentsPath,
|
|
@@ -906,8 +919,7 @@ export async function generateNav(
|
|
|
906
919
|
)
|
|
907
920
|
contents.push(relative(root, contentsPath))
|
|
908
921
|
|
|
909
|
-
const
|
|
910
|
-
const css = existsSync(cssPath) ? await readFile(cssPath, 'utf8') : ''
|
|
922
|
+
const css = stripImports(await readFile(cssPath, 'utf8'))
|
|
911
923
|
|
|
912
924
|
for (let index = 0; index < metas.length; index += 1) {
|
|
913
925
|
const outcome = await rewriteLesson(
|