@erclx/canon 4.105.0 → 4.107.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/draft-identity/SKILL.md +84 -9
- package/claude/skills/role-planner/REQUIREMENT.md +14 -7
- package/claude/skills/role-planner/SKILL.md +56 -53
- package/claude/skills/role-worker/SKILL.md +12 -4
- package/claude/skills/session-relay/REQUIREMENT.md +44 -17
- package/claude/skills/session-relay/SKILL.md +83 -14
- package/docs/agents/capture.md +3 -3
- package/docs/agents/commands.md +1 -1
- package/docs/agents/tasks.md +2 -0
- package/docs/agents/teach.md +2 -0
- package/docs/workflow/ai-workflow.md +9 -9
- package/docs/workflow/visual-design-workflow.md +1 -1
- package/package.json +1 -1
- package/scripts/core/regen-hero.sh +19 -4
- package/src/claude/cases/workflow.ts +1 -1
- package/src/cli.ts +96 -40
- package/src/design/components.ts +13 -3
- package/src/gate/measures.ts +28 -5
- package/src/gate/stages.ts +3 -5
- package/src/tasks/archive.ts +77 -10
- package/src/teach/nav.ts +14 -2
- package/tooling/astro/configs/card-src/pages/og-card.astro +98 -0
- package/tooling/astro/configs/card.config.mjs +39 -0
- package/tooling/astro/configs/scripts/check-card-exclusion.sh +58 -0
- package/tooling/astro/manifest.toml +3 -0
- package/tooling/astro/reference.md +27 -0
- package/tooling/web/configs/scripts/verify.sh +9 -0
- package/tooling/web/reference.md +4 -2
package/src/tasks/archive.ts
CHANGED
|
@@ -792,15 +792,16 @@ export async function archiveTask(
|
|
|
792
792
|
|
|
793
793
|
await mkdir(destination, { recursive: true })
|
|
794
794
|
await rename(from, to)
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
795
|
+
const rebased = rebaseRelativeLinks(text, dir, destination)
|
|
796
|
+
const retargeted = plan
|
|
797
|
+
? retargetPlanLine(rebased, linkTo(destination, plan.to))
|
|
798
|
+
: rebased
|
|
799
|
+
await writeFile(
|
|
800
|
+
to,
|
|
801
|
+
ready
|
|
802
|
+
? retargetReadyLine(retargeted, `${linkTo(destination, ready.to)}/`)
|
|
803
|
+
: retargeted,
|
|
804
|
+
)
|
|
804
805
|
|
|
805
806
|
const priorityRowRemoved = await clearPriorityRow(dir, stem)
|
|
806
807
|
const regen = await regenOne(dir, { dryRun: false })
|
|
@@ -918,6 +919,69 @@ export function linkTo(taskDir: string, plan: string): string {
|
|
|
918
919
|
return relative(taskDir, plan).split(sep).join('/')
|
|
919
920
|
}
|
|
920
921
|
|
|
922
|
+
const MARKDOWN_LINK_PATTERN = /(\]\()([^)\s]+)/g
|
|
923
|
+
/** The capture group makes `split` keep each code span, at the odd positions. */
|
|
924
|
+
const INLINE_CODE_PATTERN = /(`+[^`]*`+)/
|
|
925
|
+
const BARE_ORIGIN_PATTERN =
|
|
926
|
+
/^((?:Plan|Ready|Groundwork|Intake):[ \t]*)([^\s[\]()]+)[ \t]*$/
|
|
927
|
+
|
|
928
|
+
function isRelativeTarget(target: string): boolean {
|
|
929
|
+
return !/^(?:[a-z][a-z0-9+.-]*:|\/|#)/i.test(target)
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function rebaseTarget(target: string, fromDir: string, toDir: string): string {
|
|
933
|
+
if (!isRelativeTarget(target)) return target
|
|
934
|
+
|
|
935
|
+
const suffixAt = target.search(/[#?]/)
|
|
936
|
+
const path = suffixAt === -1 ? target : target.slice(0, suffixAt)
|
|
937
|
+
const suffix = suffixAt === -1 ? '' : target.slice(suffixAt)
|
|
938
|
+
if (path === '') return target
|
|
939
|
+
|
|
940
|
+
const rebased = relative(toDir, resolve(fromDir, path)).split(sep).join('/')
|
|
941
|
+
const slash = path.endsWith('/') ? '/' : ''
|
|
942
|
+
|
|
943
|
+
return `${rebased}${slash}${suffix}`
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Re-resolves every relative link in a task written for `fromDir` so it reads
|
|
948
|
+
* the same target from `toDir`. Every link rather than the four origin lines,
|
|
949
|
+
* since the body breaks one folder short the same way. Fenced samples stay
|
|
950
|
+
* verbatim, and a URL, an anchor, or a rooted path names nothing depth changes.
|
|
951
|
+
*/
|
|
952
|
+
export function rebaseRelativeLinks(
|
|
953
|
+
text: string,
|
|
954
|
+
fromDir: string,
|
|
955
|
+
toDir: string,
|
|
956
|
+
): string {
|
|
957
|
+
const lines = text.split('\n')
|
|
958
|
+
const fenced = fenceMask(lines)
|
|
959
|
+
|
|
960
|
+
return lines
|
|
961
|
+
.map((line, index) => {
|
|
962
|
+
if (fenced[index]) return line
|
|
963
|
+
|
|
964
|
+
const bare = BARE_ORIGIN_PATTERN.exec(line)
|
|
965
|
+
if (bare?.[2].includes('/')) {
|
|
966
|
+
return `${bare[1]}${rebaseTarget(bare[2], fromDir, toDir)}`
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return line
|
|
970
|
+
.split(INLINE_CODE_PATTERN)
|
|
971
|
+
.map((part, position) =>
|
|
972
|
+
position % 2 === 1
|
|
973
|
+
? part
|
|
974
|
+
: part.replace(
|
|
975
|
+
MARKDOWN_LINK_PATTERN,
|
|
976
|
+
(_whole: string, open: string, target: string) =>
|
|
977
|
+
`${open}${rebaseTarget(target, fromDir, toDir)}`,
|
|
978
|
+
),
|
|
979
|
+
)
|
|
980
|
+
.join('')
|
|
981
|
+
})
|
|
982
|
+
.join('\n')
|
|
983
|
+
}
|
|
984
|
+
|
|
921
985
|
async function clearPriorityRow(dir: string, stem: string): Promise<boolean> {
|
|
922
986
|
const path = join(dir, 'priority.md')
|
|
923
987
|
if (!existsSync(path)) return false
|
|
@@ -984,7 +1048,10 @@ export async function declineTask(
|
|
|
984
1048
|
await rename(from, to)
|
|
985
1049
|
|
|
986
1050
|
const date = new Date().toISOString().slice(0, 10)
|
|
987
|
-
const declined = insertDeclinedLine(
|
|
1051
|
+
const declined = insertDeclinedLine(
|
|
1052
|
+
rebaseRelativeLinks(text, dir, destination),
|
|
1053
|
+
declineLine(reason, by, date),
|
|
1054
|
+
)
|
|
988
1055
|
const final = plan
|
|
989
1056
|
? retargetPlanLine(declined, linkTo(destination, plan.to))
|
|
990
1057
|
: declined
|
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(
|
|
@@ -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}"
|
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/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.
|