@erclx/canon 5.8.0 → 5.10.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/api-design/REQUIREMENT.md +51 -0
- package/claude/skills/api-design/SKILL.md +102 -0
- package/claude/skills/api-design/references/adopted.md +40 -0
- package/claude/skills/codebase-layout/REQUIREMENT.md +1 -1
- package/claude/skills/codebase-layout/SKILL.md +2 -1
- package/claude/skills/git-followup/SKILL.md +1 -1
- package/claude/skills/git-pr/references/evidence.md +1 -1
- package/claude/skills/review-craft/REQUIREMENT.md +8 -2
- package/claude/skills/review-craft/SKILL.md +63 -7
- package/claude/skills/review-craft/references/adopted.md +49 -0
- package/claude/skills/review-ui/REQUIREMENT.md +3 -0
- package/claude/skills/review-ui/SKILL.md +15 -1
- package/claude/skills/review-ui/references/driving.md +3 -3
- package/claude/skills/review-ui/references/post.md +16 -3
- package/claude/skills/systematic-debugging/REQUIREMENT.md +14 -0
- package/claude/skills/systematic-debugging/SKILL.md +45 -17
- package/claude/skills/systematic-debugging/references/adopted.md +44 -0
- package/docs/agents/capture.md +2 -2
- package/docs/agents/commands.md +3 -1
- package/docs/agents/hooks.md +56 -0
- package/docs/agents/index.md +1 -0
- package/docs/agents/install-and-sync.md +1 -1
- package/docs/agents/markdown-audit.md +1 -1
- package/docs/agents/pr-evidence.md +33 -17
- package/docs/agents/records-push.md +1 -1
- package/docs/agents/records-validate.md +1 -1
- package/docs/agents/scripting.md +6 -3
- package/docs/agents/tasks-archive.md +4 -2
- package/docs/agents/tasks-plans.md +8 -5
- package/docs/agents/tasks.md +1 -1
- package/docs/agents/worktrees.md +2 -2
- package/docs/target/scenarios.md +2 -2
- package/docs/workflow/skill-map.md +1 -0
- package/package.json +1 -1
- package/scripts/core/regen-agent-fixture.sh +3 -3
- package/scripts/core/regen-hero.ts +119 -0
- package/scripts/core/regen-web-favicon.ts +1 -1
- package/src/capture/frame-values.ts +359 -0
- package/src/claude/cases/misc.ts +5 -0
- package/src/claude/skills-audit.ts +3 -0
- package/src/cli.ts +2 -0
- package/src/commands/claude.ts +1 -1
- package/src/commands/docs.ts +51 -11
- package/src/commands/hooks.ts +67 -0
- package/src/commands/pr.ts +146 -76
- package/src/commands/standards.ts +33 -2
- package/src/commands/tasks.ts +57 -2
- package/src/commands/tooling.ts +5 -5
- package/src/commands/upgrade.ts +3 -2
- package/src/commands/worktrees.ts +4 -3
- package/src/counts/catalogs.ts +3 -3
- package/src/design/tokens.ts +1 -1
- package/src/docs/list.ts +103 -0
- package/src/docs/read.ts +63 -19
- package/src/gate/stages.ts +1 -1
- package/src/gov/stacks.ts +1 -1
- package/src/help.ts +2 -1
- package/src/hooks/post-merge.ts +331 -0
- package/src/pr/evidence.ts +21 -0
- package/src/records/validate.ts +1 -38
- package/src/standards/list.ts +33 -0
- package/src/standards/read.ts +13 -0
- package/src/standards/scope.ts +56 -0
- package/src/sync/check.ts +38 -4
- package/src/tasks/archive.ts +195 -3
- package/src/tasks/record.ts +25 -2
- package/src/tooling/inject.ts +4 -3
- package/src/tooling/manifest.ts +62 -8
- package/src/tooling/scan.ts +4 -3
- package/src/tooling/stamp.ts +11 -2
- package/src/tooling/subfolder.ts +7 -3
- package/standards/tasks.md +2 -2
- package/tooling/astro/reference.md +1 -1
- package/tooling/base/configs/scripts/verify.json +3 -0
- package/tooling/base/configs/scripts/verify.sh +1 -110
- package/tooling/base/configs/scripts/verify.ts +378 -0
- package/tooling/base/manifest.toml +1 -0
- package/tooling/base/reference.md +5 -3
- package/tooling/go/configs/scripts/verify.json +10 -0
- package/tooling/go/reference.md +5 -4
- package/tooling/php/configs/scripts/verify.json +7 -0
- package/tooling/php/reference.md +5 -4
- package/tooling/python/configs/scripts/verify.json +4 -0
- package/tooling/python/reference.md +3 -3
- package/tooling/web/configs/scripts/verify.json +3 -0
- package/tooling/web/reference.md +3 -3
- package/scripts/core/regen-hero.sh +0 -419
- package/scripts/docs/list.sh +0 -184
- package/scripts/lib/frontmatter.sh +0 -30
- package/scripts/standards/list.sh +0 -115
- package/scripts/tooling/create.sh +0 -109
- package/src/commands/pass-through.ts +0 -39
- package/tooling/go/configs/scripts/verify.sh +0 -142
- package/tooling/php/configs/scripts/verify.sh +0 -147
- package/tooling/python/configs/scripts/verify.sh +0 -141
- package/tooling/web/configs/scripts/verify.sh +0 -153
package/tooling/php/reference.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
The php stack covers Composer projects on PHP 8.3 or later, formatted by PHP CS Fixer, analysed by PHPStan, and tested by PHPUnit. It ships the three tools' configs, a
|
|
7
|
+
The php stack covers Composer projects on PHP 8.3 or later, formatted by PHP CS Fixer, analysed by PHPStan, and tested by PHPUnit. It ships the three tools' configs, a php phase list for base's verify runner, and a copy-once smoke test.
|
|
8
8
|
|
|
9
9
|
## Scaffold checklist
|
|
10
10
|
|
|
@@ -39,10 +39,11 @@ The php stack also ships `tests/SmokeTest.php` as a copy-once seed, so `bun run
|
|
|
39
39
|
- Set `colors`, `failOnWarning`, and `failOnRisky`.
|
|
40
40
|
- Run one `unit` suite over `tests`, with `src` as the source.
|
|
41
41
|
|
|
42
|
-
## verify.
|
|
42
|
+
## verify.json
|
|
43
43
|
|
|
44
|
-
-
|
|
45
|
-
-
|
|
44
|
+
- Probe `php` and `composer` first.
|
|
45
|
+
- Hold a precondition on `vendor/bin/phpunit`, so the run stops and names `composer install` when it is missing.
|
|
46
|
+
- Require Typecheck (`phpstan analyse`), Lint (`php-cs-fixer check`), and Unit tests (`phpunit --testdox`). Base's runner adds the four base phases wherever the folder declares their scripts, then the markdown bans stage.
|
|
46
47
|
|
|
47
48
|
## Gitignore
|
|
48
49
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
The python stack covers Python 3.13+ projects managed with `uv`. It ships golden sidecar configs for `ruff`, `mypy`, `pytest`, and `coverage`, plus a `.python-version` pin and a python
|
|
7
|
+
The python stack covers Python 3.13+ projects managed with `uv`. It ships golden sidecar configs for `ruff`, `mypy`, `pytest`, and `coverage`, plus a `.python-version` pin and a python phase list for base's verify runner. Framework adapters layer on top with their own deps and configs.
|
|
8
8
|
|
|
9
9
|
Configs ship as sidecar files (`ruff.toml`, `mypy.ini`, `pytest.ini`, `.coveragerc`) rather than `[tool.*]` sections in `pyproject.toml`. Sync overwrites configs on every run, so keeping them sidecar avoids stomping the user-owned `[project]` block in `pyproject.toml`.
|
|
10
10
|
|
|
@@ -27,7 +27,7 @@ The python stack also ships `tests/test_smoke.py` as a copy-once seed. `pytest`
|
|
|
27
27
|
- `pytest.ini`: tests live under `tests/`, source under `src/`.
|
|
28
28
|
- `.coveragerc`: branch coverage on `src/`, html report under `.coverage_cache/html`.
|
|
29
29
|
- `.python-version`: pinned to `3.14` to match `uv init` defaults.
|
|
30
|
-
- `scripts/verify.
|
|
30
|
+
- `scripts/verify.json`: requires Typecheck (`mypy`), Lint (`ruff check && ruff format --check`), and Tests (`pytest -v`) on top of base's runner, and stops naming `uv` when it is not on PATH. The markdown bans phase runs as it does everywhere.
|
|
31
31
|
|
|
32
32
|
## Hybrid project shape
|
|
33
33
|
|
|
@@ -39,7 +39,7 @@ The manifest declares no `[dependencies.dev]`. Manifest injection currently call
|
|
|
39
39
|
|
|
40
40
|
## Verify command
|
|
41
41
|
|
|
42
|
-
`canon tooling verify <stack>` currently runs `bun run lint:fix` and `bun run check` against any stack with a `package.json`. Since base ships a `package.json`, those run for python too. Python's verify.
|
|
42
|
+
`canon tooling verify <stack>` currently runs `bun run lint:fix` and `bun run check` against any stack with a `package.json`. Since base ships a `package.json`, those run for python too. Python's `verify.json` requires the package.json `lint`/`typecheck`/`test:run` scripts that delegate to `uv run`, so the verify path works end-to-end as long as `uv` is installed in the verify environment. The end-to-end test and screenshot phases are gated on `package.json` script keys that python does not declare, so they cleanly skip.
|
|
43
43
|
|
|
44
44
|
## Anti-patterns
|
|
45
45
|
|
package/tooling/web/reference.md
CHANGED
|
@@ -19,7 +19,7 @@ Golden config files live in `tooling/web/configs/` and are copied into the targe
|
|
|
19
19
|
- `.github/workflows/readme-screenshot.yml`: refreshes that frame on a pull request and commits it back to the branch. The path list ships as `REPLACE_WITH_PROJECT_PATHS`, and the job's first step after checkout exits 1 while it stays, since a job that never fires leaves no run and no diff to notice. The workflow's own path is in the list so the installing pull request reaches that step. When a commit-back is right and when a report is, `governance/rules/tooling/700-ci-workflow.md` states the test.
|
|
20
20
|
- `.vscode/extensions.json` and `.vscode/settings.json`: editor wiring for ESLint, Tailwind, Playwright, Vitest.
|
|
21
21
|
- `.github/workflows/verify.yml`: `static-checks`, `unit-tests`, `build-verify`, and `e2e-tests` jobs.
|
|
22
|
-
- `scripts/verify.
|
|
22
|
+
- `scripts/verify.json`: the phases base's verify runner requires here, being typecheck, lint, unit tests, and build. `## Verify script` covers the order and the card exclusion stage.
|
|
23
23
|
- `scripts/worktree-port.sh`: prints a base port plus this working directory's offset. Called with no argument it prints the offset alone. It refuses a folder left under the worktrees directory after its worktree was removed, rather than printing a port for it.
|
|
24
24
|
|
|
25
25
|
## What stays in per-stack adapters
|
|
@@ -128,6 +128,6 @@ The sweep under `screenshots/` is ignored again, and only a flagged case's `evid
|
|
|
128
128
|
|
|
129
129
|
## Verify script
|
|
130
130
|
|
|
131
|
-
The web layer's `scripts/verify.
|
|
131
|
+
The web layer's `scripts/verify.json` requires `typecheck`, `lint`, `test:run`, and `build`, and base's `scripts/verify.ts` runs them in its fixed order: typecheck, lint, format, spelling, shell, unit tests, build, markdown bans, then the card exclusion check where one is installed. astro, vite-react, and nextjs inherit the list, since the nearest stack's copy wins. An adapter whose phases differ ships its own `verify.json` rather than a script.
|
|
132
132
|
|
|
133
|
-
The card stage
|
|
133
|
+
The card stage runs `scripts/check-card-exclusion.sh` whenever that file is installed, which keeps the social card route's build exclusion on the default path rather than behind a flag. It tests for the script rather than the stack, so a project with no card route runs one less stage with no override, and the stack shipping a card route owns what the check reads, being the build the stage before it produced. The astro reference carries the route itself.
|
|
@@ -1,419 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Fills every assets/captures/*.html.tmpl from the CLI catalogs and the design
|
|
3
|
-
# source, writing the .html beside each one.
|
|
4
|
-
#
|
|
5
|
-
# The name says hero because the hero was the only template when it was written
|
|
6
|
-
# and every citation of it across docs/, canon/context/, and src/ spells that
|
|
7
|
-
# name. Renaming the file is a sweep across roughly twenty surfaces for a
|
|
8
|
-
# cosmetic gain, so the file keeps its name and the loop below covers whatever
|
|
9
|
-
# templates the folder holds.
|
|
10
|
-
#
|
|
11
|
-
# The palette arrives the same way the counts do. `canon design css
|
|
12
|
-
# --no-components` emits the custom properties from src/design/tokens.ts and
|
|
13
|
-
# they land on the {{TOKENS}} placeholder, so neither frame carries its own copy
|
|
14
|
-
# of a hex value and a token moved at the source moves both captures. The
|
|
15
|
-
# component half is left out on purpose: it is a scrollbar and a status marker,
|
|
16
|
-
# and a static capture frame renders neither.
|
|
17
|
-
#
|
|
18
|
-
# A frame a README shows in both themes fills once more against the light roles
|
|
19
|
-
# and writes `<name>-light.html` beside the dark one, so each theme is its own
|
|
20
|
-
# capture set with its own stamp.
|
|
21
|
-
#
|
|
22
|
-
# Only the HTML regenerates here. The PNG beside it is a chromium render whose
|
|
23
|
-
# bytes move with the browser version, so asserting it in verify.sh would fail
|
|
24
|
-
# on a machine whose chromium differs rather than on a stale count. Rebuild the
|
|
25
|
-
# images with `canon capture assets/captures --selector .window --out assets/evidence`
|
|
26
|
-
# after this script reports a change. The markup and the image sit in two
|
|
27
|
-
# folders, so that run reads the sources here and sends every PNG and stamp to
|
|
28
|
-
# assets/evidence/, where the documents point and where `canon pr evidence`
|
|
29
|
-
# compares a changed image. The selector has no default, since
|
|
30
|
-
# the element a capture crops to belongs to the page rather than to the command,
|
|
31
|
-
# and `.window` is the class this repository's own sources declare.
|
|
32
|
-
# That capture also writes a .stamp beside each PNG, which records the digest of
|
|
33
|
-
# the markup it rendered and is what the Hero stage compares, so a frame's three
|
|
34
|
-
# files commit together. The frame carries no version. `package.json` is bumped on main by the release
|
|
35
|
-
# tooling, so embedding it drifts every open branch on the next release and the
|
|
36
|
-
# stage then fails for work that touched nothing.
|
|
37
|
-
#
|
|
38
|
-
# Clone-only. `canon capture` ships now, but this script reads the repository's
|
|
39
|
-
# own catalogs, which a registry install does not carry.
|
|
40
|
-
set -e
|
|
41
|
-
set -o pipefail
|
|
42
|
-
|
|
43
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
44
|
-
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
45
|
-
|
|
46
|
-
ASSET_DIR="$PROJECT_ROOT/assets/captures"
|
|
47
|
-
LISTED=10
|
|
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
|
-
|
|
64
|
-
# `bun src/cli.ts` rather than `canon`, since a globally linked binary resolves to
|
|
65
|
-
# the main checkout no matter which worktree is running.
|
|
66
|
-
catalog() {
|
|
67
|
-
(cd "$PROJECT_ROOT" && CANON_NON_INTERACTIVE=1 bun src/cli.ts "$@" --json 2>/dev/null)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
TEMPLATES=("$ASSET_DIR"/*.html.tmpl)
|
|
71
|
-
if [ ! -f "${TEMPLATES[0]}" ]; then
|
|
72
|
-
echo "regen-hero: no templates under $ASSET_DIR" >&2
|
|
73
|
-
exit 1
|
|
74
|
-
fi
|
|
75
|
-
|
|
76
|
-
# Not routed through `catalog`, since the design emitter writes CSS on stdout
|
|
77
|
-
# rather than a `--json` record.
|
|
78
|
-
TOKEN_CSS="$(cd "$PROJECT_ROOT" && CANON_NON_INTERACTIVE=1 bun src/cli.ts design css --no-components)"
|
|
79
|
-
if [ -z "$TOKEN_CSS" ]; then
|
|
80
|
-
echo "regen-hero: the design source emitted nothing, refusing to write an unstyled frame" >&2
|
|
81
|
-
exit 1
|
|
82
|
-
fi
|
|
83
|
-
|
|
84
|
-
# The commands have no `--json` catalog of their own, so the count comes from
|
|
85
|
-
# `canon gov counts`, which already reads the registration block in the CLI
|
|
86
|
-
# entry point to judge a document stating its own command count. Reading that
|
|
87
|
-
# figure here rather than re-deriving it with a second regex is what keeps the
|
|
88
|
-
# two readings of `src/cli.ts` from drifting apart unwatched, which is the
|
|
89
|
-
# defect `canon gov counts` itself exists to catch.
|
|
90
|
-
# `gov counts` exits non-zero on an ordinary finding, unlike every `list` verb
|
|
91
|
-
# below, so its output is read past that under `set -e` rather than aborting
|
|
92
|
-
# a hero rebuild for a stale count somewhere unrelated in the tree.
|
|
93
|
-
COUNTS_JSON="$(catalog gov counts || true)"
|
|
94
|
-
SKILLS_JSON="$(catalog claude skills list)"
|
|
95
|
-
GOV_JSON="$(catalog gov list)"
|
|
96
|
-
STANDARDS_JSON="$(catalog standards list)"
|
|
97
|
-
TOOLING_JSON="$(catalog tooling list)"
|
|
98
|
-
|
|
99
|
-
for payload in "$COUNTS_JSON" "$SKILLS_JSON" "$GOV_JSON" "$STANDARDS_JSON" "$TOOLING_JSON"; do
|
|
100
|
-
if [ -z "$payload" ]; then
|
|
101
|
-
echo "regen-hero: a catalog returned nothing, refusing to write a zeroed hero" >&2
|
|
102
|
-
exit 1
|
|
103
|
-
fi
|
|
104
|
-
done
|
|
105
|
-
|
|
106
|
-
# The catalogs reach the eval as files rather than as environment entries. Linux
|
|
107
|
-
# caps a single env string at 128KB, and the standards payload crossed it, which
|
|
108
|
-
# fails the exec with E2BIG before any stage can report a stale count. A file
|
|
109
|
-
# path is bounded whatever the catalogs grow to.
|
|
110
|
-
PAYLOAD_DIR="$(mktemp -d)"
|
|
111
|
-
trap 'rm -rf "$PAYLOAD_DIR"; [ "$OUT_DIR" = "$ASSET_DIR" ] || rm -rf "$OUT_DIR"' EXIT
|
|
112
|
-
|
|
113
|
-
printf '%s' "$COUNTS_JSON" >"$PAYLOAD_DIR/counts.json"
|
|
114
|
-
printf '%s' "$SKILLS_JSON" >"$PAYLOAD_DIR/skills.json"
|
|
115
|
-
printf '%s' "$GOV_JSON" >"$PAYLOAD_DIR/gov.json"
|
|
116
|
-
printf '%s' "$STANDARDS_JSON" >"$PAYLOAD_DIR/standards.json"
|
|
117
|
-
printf '%s' "$TOOLING_JSON" >"$PAYLOAD_DIR/tooling.json"
|
|
118
|
-
printf '%s' "$TOKEN_CSS" >"$PAYLOAD_DIR/tokens.css"
|
|
119
|
-
|
|
120
|
-
export PAYLOAD_DIR
|
|
121
|
-
export ASSET_DIR OUT_DIR LISTED PROJECT_ROOT
|
|
122
|
-
|
|
123
|
-
bun --eval '
|
|
124
|
-
const { readFileSync, readdirSync } = require("node:fs")
|
|
125
|
-
|
|
126
|
-
const { PAYLOAD_DIR, ASSET_DIR, OUT_DIR, LISTED, PROJECT_ROOT } = process.env
|
|
127
|
-
|
|
128
|
-
const payload = (name) => readFileSync(PAYLOAD_DIR + "/" + name + ".json", "utf8")
|
|
129
|
-
|
|
130
|
-
// Indented to the depth a rule inside the `<style>` block sits at, so the
|
|
131
|
-
// emitted file passes the same formatter every other committed asset does.
|
|
132
|
-
const tokenCss = readFileSync(PAYLOAD_DIR + "/tokens.css", "utf8")
|
|
133
|
-
.trimEnd()
|
|
134
|
-
.split("\n")
|
|
135
|
-
.map((line) => (line === "" ? "" : " " + line))
|
|
136
|
-
.join("\n")
|
|
137
|
-
|
|
138
|
-
const COUNTS_JSON = payload("counts")
|
|
139
|
-
const SKILLS_JSON = payload("skills")
|
|
140
|
-
const GOV_JSON = payload("gov")
|
|
141
|
-
const STANDARDS_JSON = payload("standards")
|
|
142
|
-
const TOOLING_JSON = payload("tooling")
|
|
143
|
-
|
|
144
|
-
const listed = Number(LISTED)
|
|
145
|
-
const skills = JSON.parse(SKILLS_JSON).skills.map((entry) => entry.name)
|
|
146
|
-
const gov = JSON.parse(GOV_JSON)
|
|
147
|
-
// Rule names carry a numeric prefix that orders the load, not the identity a
|
|
148
|
-
// reader knows them by, so the frame shows the slug alone.
|
|
149
|
-
const slug = (entry) => entry.name.replace(/^\d+-/, "")
|
|
150
|
-
const rules = gov.rules.map(slug)
|
|
151
|
-
// A rule no stack names is opt-in behind `--add`, so featuring one advertises
|
|
152
|
-
// an entry no ordinary `canon gov install` delivers. The column samples the
|
|
153
|
-
// stack-reached subset while its count and remainder describe the whole catalog.
|
|
154
|
-
const stacked = new Set(gov.stacks.flatMap((stack) => stack.rules))
|
|
155
|
-
const deliveredRules = gov.rules.filter((entry) => stacked.has(entry.name)).map(slug)
|
|
156
|
-
const standards = JSON.parse(STANDARDS_JSON).standards.map((entry) => entry.name)
|
|
157
|
-
const toolingStacks = JSON.parse(TOOLING_JSON).stacks
|
|
158
|
-
const commandCount = JSON.parse(COUNTS_JSON).catalogs.commands
|
|
159
|
-
if (!commandCount) {
|
|
160
|
-
console.error("regen-hero: gov counts read no commands catalog, refusing to write a zeroed hero")
|
|
161
|
-
process.exit(1)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const MARK_SVG = (await Bun.file(`${PROJECT_ROOT}/assets/brand/mark.svg`).text()).trim()
|
|
165
|
-
if (!MARK_SVG) {
|
|
166
|
-
console.error("regen-hero: assets/brand/mark.svg read empty, refusing to write a hero with no mark")
|
|
167
|
-
process.exit(1)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const accentMatch = tokenCss.match(/--color-accent:\s*(#[0-9a-fA-F]{3,8})/)
|
|
171
|
-
if (!accentMatch) {
|
|
172
|
-
console.error("regen-hero: token css carries no --color-accent value, refusing to write a colorless favicon")
|
|
173
|
-
process.exit(1)
|
|
174
|
-
}
|
|
175
|
-
const faviconSvg = MARK_SVG.replace(/<!--[\s\S]*?-->/, "").trim().replaceAll("currentColor", accentMatch[1])
|
|
176
|
-
const FAVICON = `data:image/svg+xml,${encodeURIComponent(faviconSvg)}`
|
|
177
|
-
|
|
178
|
-
const escape = (value) =>
|
|
179
|
-
value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
180
|
-
|
|
181
|
-
// Even spacing across the sorted catalog rather than its first N, so a column
|
|
182
|
-
// whose names share a prefix does not read as a narrower catalog than the one
|
|
183
|
-
// that ships. Rules and standards sample. Skills do not, because the even step
|
|
184
|
-
// lands on the entries closest to commodity and on none of the workflow the
|
|
185
|
-
// toolkit exists to carry.
|
|
186
|
-
const sample = (names) => {
|
|
187
|
-
if (names.length <= listed) return names
|
|
188
|
-
const step = names.length / listed
|
|
189
|
-
return Array.from({ length: listed }, (_, i) => names[Math.floor(i * step)])
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// The first seven are the method and exist nowhere else. The last three are
|
|
193
|
-
// recognizable on sight and are what keeps a column of chosen names from
|
|
194
|
-
// reading as an all-`claude-` catalog, which is the failure the sampler above
|
|
195
|
-
// was written against.
|
|
196
|
-
const FEATURED_SKILLS = [
|
|
197
|
-
"plan-feature",
|
|
198
|
-
"plan-groundwork",
|
|
199
|
-
"plan-intake",
|
|
200
|
-
"role-orchestrator",
|
|
201
|
-
"auto-ship",
|
|
202
|
-
"review-pr",
|
|
203
|
-
"task-board",
|
|
204
|
-
"git-ship",
|
|
205
|
-
"target-setup",
|
|
206
|
-
"systematic-debugging",
|
|
207
|
-
]
|
|
208
|
-
|
|
209
|
-
// A chosen name is maintained by hand where a sample never goes stale, so a
|
|
210
|
-
// rename has to fail the run rather than quietly shrink the column. This is
|
|
211
|
-
// what makes the trade acceptable.
|
|
212
|
-
//
|
|
213
|
-
// The length is asserted because the `+N more` figure below counts down from
|
|
214
|
-
// `listed` rather than from what this returns, so a list of any other length
|
|
215
|
-
// renders a remainder that is wrong by the difference.
|
|
216
|
-
const featured = (names, chosen) => {
|
|
217
|
-
if (chosen.length !== listed) {
|
|
218
|
-
console.error(`regen-hero: ${chosen.length} featured skills, expected ${listed}`)
|
|
219
|
-
process.exit(1)
|
|
220
|
-
}
|
|
221
|
-
const catalog = new Set(names)
|
|
222
|
-
const missing = chosen.filter((name) => !catalog.has(name))
|
|
223
|
-
if (missing.length > 0) {
|
|
224
|
-
console.error(`regen-hero: featured skills missing from the catalog: ${missing.join(", ")}`)
|
|
225
|
-
process.exit(1)
|
|
226
|
-
}
|
|
227
|
-
return chosen
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const markup = (names) =>
|
|
231
|
-
names
|
|
232
|
-
.map((name) => ` <div class="entry">${escape(name)}</div>`)
|
|
233
|
-
.join("\n")
|
|
234
|
-
|
|
235
|
-
const entries = (names) => markup(sample(names))
|
|
236
|
-
|
|
237
|
-
const remaining = (names) => String(Math.max(0, names.length - listed))
|
|
238
|
-
|
|
239
|
-
// An empty array is well-formed JSON, so the shell guard on an empty payload
|
|
240
|
-
// does not catch a catalog whose tree went missing. A zeroed count would ship a
|
|
241
|
-
// frame claiming the domain has nothing in it.
|
|
242
|
-
for (const [label, list] of [
|
|
243
|
-
["skills", skills], ["rules", rules], ["standards", standards],
|
|
244
|
-
["tooling stacks", toolingStacks], ["gov stacks", gov.stacks],
|
|
245
|
-
["stack-delivered rules", deliveredRules],
|
|
246
|
-
]) {
|
|
247
|
-
if (list.length === 0) {
|
|
248
|
-
console.error(`regen-hero: the ${label} catalog is empty, refusing to write a zeroed hero`)
|
|
249
|
-
process.exit(1)
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// A terminal frame renders lines into a `<pre>` where the hero renders names
|
|
254
|
-
// into divs, so the two cannot share a value however similar the catalog read
|
|
255
|
-
// behind them looks. Padding happens before escaping, since an entity is longer
|
|
256
|
-
// than the character it replaces and would push a column out of line.
|
|
257
|
-
const pad = (value, width) => value + " ".repeat(Math.max(0, width - value.length))
|
|
258
|
-
const frameRow = (cells) => `<span class="frame">│</span> <span class="ok">✓</span> ${cells}`
|
|
259
|
-
|
|
260
|
-
// The frame sets `white-space: pre` at a fixed window width, so an overlong
|
|
261
|
-
// cell is clipped by the window edge rather than wrapped. Truncating here is
|
|
262
|
-
// what keeps the longest row inside the capture, and the ellipsis is what stops
|
|
263
|
-
// a clipped value from reading as the whole value.
|
|
264
|
-
const GLOB_WIDTH = 44
|
|
265
|
-
const clip = (value, width) =>
|
|
266
|
-
value.length <= width ? value : `${value.slice(0, width - 1)}…`
|
|
267
|
-
|
|
268
|
-
const plural = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`
|
|
269
|
-
|
|
270
|
-
const govStackRows = gov.stacks
|
|
271
|
-
.map((stack) =>
|
|
272
|
-
frameRow(
|
|
273
|
-
`<span class="name">${escape(pad(stack.name, 17))}</span>` +
|
|
274
|
-
`<span class="muted">${escape(pad(plural(stack.rules.length, "rule"), 11))}</span>` +
|
|
275
|
-
`<span class="muted">${escape(stack.extends ? `extends ${stack.extends}` : "")}</span>`,
|
|
276
|
-
),
|
|
277
|
-
)
|
|
278
|
-
.join("\n")
|
|
279
|
-
|
|
280
|
-
// Sampled from the stack-reached subset for the reason the hero column is, and
|
|
281
|
-
// showing each rule beside the glob that loads it rather than its description,
|
|
282
|
-
// since the glob is the mechanism a reader cannot otherwise see.
|
|
283
|
-
const deliveredEntries = gov.rules.filter((entry) => stacked.has(entry.name))
|
|
284
|
-
const govRuleRows = sample(deliveredEntries)
|
|
285
|
-
.map((entry) => {
|
|
286
|
-
const globs = [entry.paths ?? []].flat()
|
|
287
|
-
// A rule carrying no glob is not unscoped, it loads every session, and
|
|
288
|
-
// saying so is the contrast that makes the column mean anything.
|
|
289
|
-
const scope = globs.length > 0 ? clip(globs.join(" "), GLOB_WIDTH) : "every session"
|
|
290
|
-
return frameRow(
|
|
291
|
-
`<span class="name">${escape(pad(slug(entry), 24))}</span>` +
|
|
292
|
-
`<span class="domain">${escape(pad(`[${entry.domain}]`, 12))}</span>` +
|
|
293
|
-
`<span class="muted">${escape(scope)}</span>`,
|
|
294
|
-
)
|
|
295
|
-
})
|
|
296
|
-
.join("\n")
|
|
297
|
-
|
|
298
|
-
// A standard either governs an artifact by path or is opened by name when a
|
|
299
|
-
// session decides it needs it. Rendering the first and naming the second is the
|
|
300
|
-
// contrast, since nothing else in the catalog tells a reader that the corpus
|
|
301
|
-
// installs into no project and is read through a verb instead.
|
|
302
|
-
const standardEntries = JSON.parse(STANDARDS_JSON).standards
|
|
303
|
-
const standardRows = sample(standardEntries)
|
|
304
|
-
.map((entry) => {
|
|
305
|
-
const applies = [entry.appliesTo ?? []].flat()
|
|
306
|
-
const governs =
|
|
307
|
-
applies.length > 0 ? clip(applies.join(" "), GLOB_WIDTH) : "read by name"
|
|
308
|
-
return frameRow(
|
|
309
|
-
`<span class="name">${escape(pad(entry.name, 18))}</span>` +
|
|
310
|
-
`<span class="muted">${escape(governs)}</span>`,
|
|
311
|
-
)
|
|
312
|
-
})
|
|
313
|
-
.join("\n")
|
|
314
|
-
|
|
315
|
-
// A tooling stack is counted rather than listed, because what it lays down is
|
|
316
|
-
// dev dependencies, run scripts, and ignore groups rather than named entries a
|
|
317
|
-
// reader would recognize. The inheritance is the part worth showing, since it
|
|
318
|
-
// is why a stack carrying two of its own arrives with far more than two.
|
|
319
|
-
const toolingStackRows = toolingStacks
|
|
320
|
-
.map((stack) =>
|
|
321
|
-
frameRow(
|
|
322
|
-
`<span class="name">${escape(pad(stack.name, 14))}</span>` +
|
|
323
|
-
`<span class="muted">${escape(pad(plural(stack.devDeps, "dep"), 10))}</span>` +
|
|
324
|
-
`<span class="muted">${escape(pad(plural(stack.scripts, "script"), 12))}</span>` +
|
|
325
|
-
`<span class="muted">${escape(stack.extends ? `extends ${stack.extends}` : "")}</span>`,
|
|
326
|
-
),
|
|
327
|
-
)
|
|
328
|
-
.join("\n")
|
|
329
|
-
|
|
330
|
-
for (const [label, rows] of [
|
|
331
|
-
["governance stack rows", govStackRows],
|
|
332
|
-
["governance rule rows", govRuleRows],
|
|
333
|
-
["standard rows", standardRows],
|
|
334
|
-
["tooling stack rows", toolingStackRows],
|
|
335
|
-
]) {
|
|
336
|
-
if (rows === "") {
|
|
337
|
-
console.error(`regen-hero: the ${label} rendered empty, refusing to write a blank frame`)
|
|
338
|
-
process.exit(1)
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const values = {
|
|
343
|
-
TOKENS: tokenCss,
|
|
344
|
-
GOV_STACK_ROWS: govStackRows,
|
|
345
|
-
GOV_RULE_ROWS: govRuleRows,
|
|
346
|
-
STANDARD_ROWS: standardRows,
|
|
347
|
-
TOOLING_STACK_ROWS: toolingStackRows,
|
|
348
|
-
SKILL_COUNT: String(skills.length),
|
|
349
|
-
RULE_COUNT: String(rules.length),
|
|
350
|
-
STANDARD_COUNT: String(standards.length),
|
|
351
|
-
GOV_STACK_COUNT: String(gov.stacks.length),
|
|
352
|
-
TOOLING_STACK_COUNT: String(toolingStacks.length),
|
|
353
|
-
COMMAND_COUNT: String(commandCount),
|
|
354
|
-
MARK_SVG,
|
|
355
|
-
FAVICON,
|
|
356
|
-
SKILL_ENTRIES: markup(featured(skills, FEATURED_SKILLS)),
|
|
357
|
-
RULE_ENTRIES: entries(deliveredRules),
|
|
358
|
-
STANDARD_ENTRIES: entries(standards),
|
|
359
|
-
SKILL_MORE: remaining(skills),
|
|
360
|
-
RULE_MORE: remaining(rules),
|
|
361
|
-
STANDARD_MORE: remaining(standards),
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Every template takes the same value map, so one carrying no count placeholder
|
|
365
|
-
// simply resolves none of them. What a template must not do is name a
|
|
366
|
-
// placeholder nobody fills, which the unresolved check below catches per file.
|
|
367
|
-
const templates = readdirSync(ASSET_DIR)
|
|
368
|
-
.filter((name) => name.endsWith(".html.tmpl"))
|
|
369
|
-
.sort()
|
|
370
|
-
|
|
371
|
-
if (templates.length === 0) {
|
|
372
|
-
console.error(`regen-hero: no templates under ${ASSET_DIR}`)
|
|
373
|
-
process.exit(1)
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// The frames a README serves in both themes. The emitter already carries a
|
|
377
|
-
// `--color-light-*` role beside each `--color-*` one, so the light frame is the
|
|
378
|
-
// same template with the plain roles pointed at the light ones rather than a
|
|
379
|
-
// second copy of any hex value. The hero is a landing-page frame no README
|
|
380
|
-
// shows, so it stays dark.
|
|
381
|
-
const LIGHT_TEMPLATES = new Set(["install.html.tmpl"])
|
|
382
|
-
|
|
383
|
-
const lightRoles = [...tokenCss.matchAll(/--color-light-([a-z-]+):/g)].map((match) => match[1])
|
|
384
|
-
if (lightRoles.length === 0) {
|
|
385
|
-
console.error("regen-hero: token css carries no --color-light-* roles, refusing to write a light frame")
|
|
386
|
-
process.exit(1)
|
|
387
|
-
}
|
|
388
|
-
const lightTokenCss = [
|
|
389
|
-
tokenCss,
|
|
390
|
-
"",
|
|
391
|
-
" :root {",
|
|
392
|
-
...lightRoles.map((role) => ` --color-${role}: var(--color-light-${role});`),
|
|
393
|
-
" --frame-shadow: rgb(0 0 0 / 16%);",
|
|
394
|
-
" }",
|
|
395
|
-
].join("\n")
|
|
396
|
-
|
|
397
|
-
const frames = templates.flatMap((template) => {
|
|
398
|
-
const base = template.replace(/\.html\.tmpl$/, "")
|
|
399
|
-
const dark = { template, out: `${base}.html`, tokens: tokenCss }
|
|
400
|
-
return LIGHT_TEMPLATES.has(template)
|
|
401
|
-
? [dark, { template, out: `${base}-light.html`, tokens: lightTokenCss }]
|
|
402
|
-
: [dark]
|
|
403
|
-
})
|
|
404
|
-
|
|
405
|
-
for (const { template, out, tokens } of frames) {
|
|
406
|
-
let html = await Bun.file(ASSET_DIR + "/" + template).text()
|
|
407
|
-
for (const [key, value] of Object.entries({ ...values, TOKENS: tokens })) {
|
|
408
|
-
html = html.replaceAll(`{{${key}}}`, value)
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const unresolved = html.match(/{{[A-Z_]+}}/g)
|
|
412
|
-
if (unresolved) {
|
|
413
|
-
console.error(`regen-hero: ${template} carries unresolved placeholders ${[...new Set(unresolved)].join(", ")}`)
|
|
414
|
-
process.exit(1)
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
await Bun.write(OUT_DIR + "/" + out, html)
|
|
418
|
-
}
|
|
419
|
-
'
|