@erclx/canon 4.104.0 → 4.106.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.
@@ -0,0 +1,98 @@
1
+ ---
2
+ /**
3
+ * The social card as a route the project keeps, rather than a scratch page a
4
+ * render deletes. `canon capture` drives it through the server this config
5
+ * serves, so the composition reads the project's own stylesheet, tokens and
6
+ * fonts instead of carrying typed copies of each. Renaming the project is then
7
+ * an edit to one string here and a re-capture, not another pass through the
8
+ * pick loop with an operator in it.
9
+ *
10
+ * `canon:draft-identity` writes the picked composition into the `.card`
11
+ * element below and leaves everything around it alone. What ships is a working
12
+ * placeholder, so the route serves before any pick has been taken.
13
+ *
14
+ * The card is declared at half of 1200x630. `canon capture` opens every page at
15
+ * a fixed 2x device scale factor and screenshots the element at that scale, so
16
+ * a 600x315 element is what lands the captured PNG on the literal target size.
17
+ * Declaring it at 1200x630 captures at 2400x1260.
18
+ *
19
+ * `card.config.mjs` is the only config whose page router reaches this file, and
20
+ * `scripts/check-card-exclusion.sh` fails a build that carries the marker
21
+ * below, which is what keeps the route out of the published site.
22
+ */
23
+ import '@/styles/global.css'
24
+
25
+ // Retarget the import above when this project keeps its global stylesheet
26
+ // somewhere else. An unresolved path refuses the card server by name, which is
27
+ // the loud direction: a card captured against no stylesheet renders as an
28
+ // unstyled box and reports success.
29
+ ---
30
+
31
+ <html lang="en">
32
+ <head>
33
+ <meta charset="utf-8" />
34
+ <title>Social card</title>
35
+ <!-- Read by scripts/check-card-exclusion.sh. A build carrying this tag
36
+ published the card route. Do not reword it. -->
37
+ <meta name="og-card-marker" content="og-card-marker" />
38
+ <style>
39
+ body {
40
+ margin: 0;
41
+ }
42
+ .card {
43
+ box-sizing: border-box;
44
+ width: 600px;
45
+ height: 315px;
46
+ display: flex;
47
+ flex-direction: column;
48
+ justify-content: center;
49
+ gap: 0.5rem;
50
+ padding: 3rem;
51
+ /* These two names are what `canon design css` emits, so a project
52
+ carrying the design files resolves both and neither fallback fires.
53
+ A name the design system does not emit would take its fallback,
54
+ capture cleanly and report success, which is the silent failure this
55
+ route exists to close. Check the names against the stylesheet when
56
+ retargeting the import above.
57
+
58
+ Written as rgb() rather than hex so the shipped-reference gate does
59
+ not read a six-digit hex fallback as a pull request number. */
60
+ background: var(--color-background, rgb(255 255 255));
61
+ color: var(--color-text, rgb(17 17 17));
62
+ /* No font-family here on purpose. The design system emits no font
63
+ token, so the card inherits the project's own body font rather than
64
+ reading a custom property that does not exist. `canon capture` reads
65
+ the computed family off this element, so inheritance still gives it
66
+ a family to check and it refuses when that family cannot resolve. */
67
+ }
68
+ .mark {
69
+ width: 64px;
70
+ height: 64px;
71
+ }
72
+ .title {
73
+ margin: 0;
74
+ font-size: 2.25rem;
75
+ line-height: 1.1;
76
+ }
77
+ .tagline {
78
+ margin: 0;
79
+ font-size: 1.125rem;
80
+ opacity: 0.75;
81
+ }
82
+ </style>
83
+ </head>
84
+ <body>
85
+ <div class="card">
86
+ <svg
87
+ class="mark"
88
+ viewBox="0 0 64 64"
89
+ role="img"
90
+ aria-label="Project mark"
91
+ >
92
+ <rect width="64" height="64" rx="12" fill="currentColor"></rect>
93
+ </svg>
94
+ <h1 class="title">REPLACE_WITH_PROJECT_NAME</h1>
95
+ <p class="tagline">REPLACE_WITH_PROJECT_TAGLINE</p>
96
+ </div>
97
+ </body>
98
+ </html>
@@ -0,0 +1,39 @@
1
+ import react from '@astrojs/react'
2
+ import tailwindcss from '@tailwindcss/vite'
3
+ import { defineConfig } from 'astro/config'
4
+ import path from 'path'
5
+
6
+ const portOffset = Number(process.env.WORKTREE_PORT_OFFSET) || 0
7
+
8
+ // Never read by `bun run build`. srcDir keeps this config's page router off
9
+ // src/pages entirely, which is what excludes the social card from the published
10
+ // build structurally rather than by a filename convention. A pageExtensions
11
+ // regex over a filename was the alternative, and it leaks the moment a route is
12
+ // renamed.
13
+ //
14
+ // The base port sits outside the band `astro.config.mjs` serves from, so a card
15
+ // server and a dev server of one worktree never contend, and two worktrees
16
+ // separate here the way they separate there.
17
+ export default defineConfig({
18
+ srcDir: './card-src',
19
+ outDir: './card-dist',
20
+ publicDir: './public',
21
+ integrations: [react()],
22
+ server: {
23
+ port: 4421 + portOffset,
24
+ },
25
+ vite: {
26
+ plugins: [tailwindcss()],
27
+ resolve: {
28
+ alias: {
29
+ '@': path.resolve('./src'),
30
+ },
31
+ },
32
+ server: {
33
+ strictPort: true,
34
+ },
35
+ preview: {
36
+ strictPort: true,
37
+ },
38
+ },
39
+ })
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+ set -o pipefail
4
+
5
+ # `card.config.mjs` keeps the social card route out of the published build by
6
+ # never pointing its srcDir at it. A later config change that widens srcDir, or
7
+ # a card file moving under src/pages/, would publish a half-size scaffolding
8
+ # page to a live domain with every other check still green.
9
+ #
10
+ # The card route has three consumers: the card server reads it, the capture
11
+ # reads it, and the published build must not carry it. This is the only one of
12
+ # the three a check can enforce, so the route's exclusion is a check rather than
13
+ # a convention.
14
+
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}"
17
+
18
+ DIST="$PROJECT_ROOT/dist"
19
+ CARD_CONFIG="$PROJECT_ROOT/card.config.mjs"
20
+ MARKER='name="og-card-marker"'
21
+
22
+ # No card route installed, so there is nothing to exclude. Reported rather than
23
+ # passed silently, since a check that says nothing is indistinguishable from one
24
+ # that looked and found nothing.
25
+ if [ ! -f "$CARD_CONFIG" ]; then
26
+ echo "check-card-exclusion: no card.config.mjs, so no card route to exclude"
27
+ exit 0
28
+ fi
29
+
30
+ # Nothing to grep without a build. Callers run this after `bun run build`, so an
31
+ # absent dist here means the build itself failed rather than something this
32
+ # check can verify.
33
+ if [ ! -d "$DIST" ]; then
34
+ echo "No $DIST. Run bun run build first." >&2
35
+ exit 1
36
+ fi
37
+
38
+ # The marker is a fixed <meta> tag the card route carries for exactly this, so
39
+ # asserting it survives in the route's own source is what turns a dropped marker
40
+ # into a loud failure here rather than a blind spot in the test below. A prose
41
+ # notice is not load-bearing: rewording it silently retires the leak check it
42
+ # backed with every workflow still green.
43
+ if ! grep -rl "$MARKER" "$PROJECT_ROOT/card-src" >/dev/null 2>&1; then
44
+ echo "No $MARKER under $PROJECT_ROOT/card-src. The card route dropped its own marker, which is what the leak test below reads for." >&2
45
+ exit 1
46
+ fi
47
+
48
+ if grep -rl "$MARKER" "$DIST" >/dev/null 2>&1; then
49
+ echo "The card route's marker ($MARKER) reached $DIST. See card.config.mjs's srcDir." >&2
50
+ exit 1
51
+ fi
52
+
53
+ if find "$DIST" -iname '*og-card*' | grep -q .; then
54
+ echo "A file named for the card route reached $DIST. See card.config.mjs's srcDir." >&2
55
+ exit 1
56
+ fi
57
+
58
+ echo "check-card-exclusion: card marker present in card-src, absent from $DIST"
@@ -21,6 +21,8 @@ packages = [
21
21
  "astro" = "astro"
22
22
  "typecheck" = "astro check"
23
23
  "setup" = "./scripts/setup.sh"
24
+ "card:dev" = "WORKTREE_PORT_OFFSET=$(bash scripts/worktree-port.sh) && export WORKTREE_PORT_OFFSET && astro dev --config card.config.mjs"
25
+ "card:check" = "bash scripts/check-card-exclusion.sh"
24
26
 
25
27
  [scripts.override]
26
28
  "screenshot" = "PREVIEW_PORT=$(bash scripts/worktree-port.sh 4321) && export PREVIEW_PORT && bash scripts/screenshot.sh"
@@ -30,6 +32,7 @@ packages = [
30
32
 
31
33
  [gitignore]
32
34
  "# Astro" = [".astro/"]
35
+ "# Social card" = ["card-dist/"]
33
36
 
34
37
  [verify]
35
38
  prepare = "bunx astro add react --yes"
@@ -21,6 +21,7 @@ The astro stack covers Astro + TypeScript projects: content sites, marketing sit
21
21
  - `vitest.config.ts`: uses `getViteConfig` from `astro/config` (not `mergeConfig`). jsdom, globals, setup file, `passWithNoTests: true`, v8 coverage, `**/*.astro` in coverage excludes.
22
22
  - `playwright.config.ts`: all browsers, `webServer` runs `bun run build && bun run preview` on port `4321` plus `WORKTREE_PORT_OFFSET`, `reuseExistingServer: false`. Astro's dev/prod gap is wide (MDX, island hydration, asset optimization), so E2E always tests the built `dist/`. `DIST_PREBUILT` set in the environment drops the `build` half, running `bun run preview` alone against a `dist/` a prior CI job already produced.
23
23
  - `tsconfig.json`: extends `astro/tsconfigs/strict`, adds `skipLibCheck`, `vitest/globals` and `@testing-library/jest-dom` in types, `@/` paths.
24
+ - `card.config.mjs`, `card-src/pages/og-card.astro`, and `scripts/check-card-exclusion.sh`: the social card route, its structural build exclusion, and the check enforcing it. See `## Social card route` below.
24
25
  - `eslint.config.js`: overrides the web layer. Adds `eslint-plugin-astro` (`.astro` parser via `astro-eslint-parser`). React-hooks scoped to `.jsx`/`.tsx` only (`.astro` is not React). The shared block's `files` selector includes `.astro`, so `check-file/filename-naming-convention` reaches `.ts`, `.tsx`, and `.astro` under `KEBAB_CASE`, overriding Astro's own PascalCase component convention deliberately, on the ground that a component's name in markup comes from the import binding rather than the filename. `.js` and `.jsx` stay out of the rule's own pattern, matching `web` and `nextjs`. `check-file/folder-naming-convention` reaches every `src/**` folder except `__tests__` and the whole `pages/` subtree, which carries its own off-block for the bracket-named dynamic routes and nested slug folders Astro's file-based routing produces. See `canon/context/tooling.md` for the measurement.
25
26
 
26
27
  ## Typecheck
@@ -59,11 +60,37 @@ Append to the `## Scripts` table:
59
60
 
60
61
  In `canon/context/ci.md`, the Typecheck row's assertion reads: `` `astro check` passes ``. The Build row's assertion reads: `` `astro build` succeeds ``.
61
62
 
63
+ ## Social card route
64
+
65
+ The social card is a route the project keeps rather than a page a render writes and deletes, so re-rendering it after a rename is an edit to one string and a re-capture instead of another pass through `draft-identity`'s pick loop with an operator in it. `canon:draft-identity` writes the picked composition into the route and captures it through the server below.
66
+
67
+ ### What ships
68
+
69
+ - `card.config.mjs`: a second Astro config whose `srcDir` points at `./card-src`, which the main config's page router never reads. That is what excludes the card from the published build structurally rather than by a filename convention, the same mechanism `scenarios.astro` reaches for with `import.meta.env.DEV`. Its base port is `4421` plus `WORKTREE_PORT_OFFSET`, outside the band `astro.config.mjs` serves from, so a card server and a dev server of one worktree never contend.
70
+ - `card-src/pages/og-card.astro`: the route, shipped as a working placeholder so it serves before any pick has been taken. It imports the project's own global stylesheet, which is what lets the composition read real tokens and real fonts instead of typed copies. It reads `--color-background` and `--color-text`, the names `canon design css` emits, and sets no `font-family` at all so the card inherits the project's body font rather than reading a font token the design system does not emit.
71
+ - `scripts/check-card-exclusion.sh`: fails when the route's `<meta name="og-card-marker">` tag reaches `dist/`, and fails when the route dropped that tag, since the marker is what the leak test reads for.
72
+ - `bun run card:dev` serves the route. `bun run card:check` runs the exclusion check, which the web layer's `scripts/verify.sh` also runs after its build stage whenever the script is installed.
73
+
74
+ ### Why each piece is shaped that way
75
+
76
+ The stylesheet import points at `@/styles/global.css`. A project keeping its stylesheet elsewhere retargets that one line, and an unresolved path then refuses the card server by name, which is the loud direction rather than the convenient one. The quiet failure it replaces is a card captured against no stylesheet at all: it renders as an unstyled box and reports success.
77
+
78
+ A custom property the stylesheet does not define is the same silent failure in a second costume. It takes its fallback, captures cleanly and reports success, so the card ships on typed values while reading as though it read real ones. That is why the route names only properties `canon design css` emits, and why a project retargeting the stylesheet import checks the names against what its own stylesheet defines.
79
+
80
+ The exclusion is a check rather than a convention because the route has three consumers and only one of them can be enforced. The card server reads it, the capture reads it, and the published build must not carry it.
81
+
82
+ `.card` is declared at 600x315, half of the 1200x630 target. `canon capture` opens every page at a fixed 2x device scale factor with no flag to change it, so a half-size element is what lands the captured PNG on the literal size. Declaring it at 1200x630 captures at 2400x1260.
83
+
84
+ ### Why v1 is Astro-only
85
+
86
+ Astro is the one web stack whose config gives a route a structural build exclusion. Next's App Router has no build-time route exclusion at all, and it needs none on the path it will take, since `ImageResponse` is that framework's own production mechanism and a card route there is meant to be served. `vite-react` builds a single-page app with no page router, so a card is a second Rollup entry rather than a route.
87
+
62
88
  ## Gitignore (extend)
63
89
 
64
90
  `[gitignore]` groups this stack edits, restated here per the manifest-to-reference symmetry:
65
91
 
66
92
  - `"# Astro" = [".astro/"]`
93
+ - `"# Social card" = ["card-dist/"]`
67
94
 
68
95
  ## Scenario switcher
69
96
 
@@ -71,6 +71,15 @@ main() {
71
71
  run_check "bun run build" "Build failed"
72
72
  log_info "Build passed"
73
73
 
74
+ # Runs whenever the stack shipped a card exclusion check, which keeps the
75
+ # stage stack-agnostic here and the check itself in the stack that owns the
76
+ # card route's shape. It reads the build above, so it sits after it.
77
+ if [ -f scripts/check-card-exclusion.sh ]; then
78
+ log_step "Card exclusion"
79
+ run_check "bash scripts/check-card-exclusion.sh" "Card exclusion check failed"
80
+ log_info "Card exclusion passed"
81
+ fi
82
+
74
83
  if [ "$NESTED" = false ]; then
75
84
  echo -e "${GREY}└${NC}\n"
76
85
  echo -e "${GREEN}✓ Verification passed${NC}"
@@ -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/ci/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.sh`: extends base verify with typecheck, lint, unit tests, and build in the full order.
22
+ - `scripts/verify.sh`: extends base verify with typecheck, lint, unit tests, and build in the full order. Its last stage runs `scripts/check-card-exclusion.sh` whenever that file is installed, which puts the social card route's build exclusion on the default path rather than behind a flag nobody passes. The stage tests for the script rather than for a framework, so this layer owns when the check runs and the stack shipping a card route owns what it reads. The astro reference carries the route itself.
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,4 +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.sh` replaces the base version. Order: typecheck, lint, format, spelling, shell, unit tests, build. Stack adapters may override if their typecheck or build differs.
131
+ The web layer's `scripts/verify.sh` replaces the base version. Order: typecheck, lint, format, spelling, shell, unit tests, build, then the card exclusion check where one is installed. Stack adapters may override if their typecheck or build differs.
132
+
133
+ The card stage is conditional on `scripts/check-card-exclusion.sh` existing rather than on the stack, so a project with no card route runs one less stage and needs no override to skip it. A stack that ships a card route ships that script, and the check reads the build the stage before it produced.