@erclx/canon 5.3.0 → 5.4.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/README.md +1 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/plan-groundwork/SKILL.md +1 -1
- package/docs/agents/markdown-audit.md +1 -1
- package/docs/agents/pr-evidence.md +17 -0
- package/docs/workflow/skill-map.md +10 -8
- package/package.json +1 -1
- package/src/commands/pr.ts +63 -41
- package/standards/context.md +1 -1
- package/standards/decisions.md +6 -1
- package/standards/groundwork.md +1 -0
- package/standards/requirements.md +1 -1
- package/standards/rule.md +1 -0
- package/standards/skill.md +1 -1
- package/standards/standard.md +1 -0
- package/tooling/go/configs/scripts/verify.sh +47 -0
- package/tooling/go/reference.md +1 -1
- package/tooling/php/configs/scripts/verify.sh +47 -0
- package/tooling/php/reference.md +1 -1
- package/tooling/python/configs/scripts/verify.sh +47 -0
- package/tooling/python/reference.md +1 -1
- package/tooling/web/configs/scripts/verify.sh +47 -0
- package/tooling/web/reference.md +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,7 @@ Working with an agent rather than reading this yourself? It loads `CLAUDE.md` at
|
|
|
94
94
|
|
|
95
95
|
- [AI workflow](docs/workflow/ai-workflow.md): feature-development loop inside a toolkit-managed project
|
|
96
96
|
- [Operating model](docs/workflow/operating-model.md): orchestrator, planner, and worker roles for building across parallel sessions
|
|
97
|
+
- [Skill map](docs/workflow/skill-map.md): plugin skills grouped by the moment a project meets them
|
|
97
98
|
- [Visual design workflow](docs/workflow/visual-design-workflow.md): tiered guide for design and wireframe authoring
|
|
98
99
|
- [Target projects](docs/target/projects.md): scaffold, add a domain later, sync upstream drift
|
|
99
100
|
- [Agents](docs/agents/index.md): CLI flags, exit codes, and JSON output shapes
|
|
@@ -91,7 +91,7 @@ The standard sets the open question format and requires it inside a topic file a
|
|
|
91
91
|
|
|
92
92
|
## Close mode
|
|
93
93
|
|
|
94
|
-
1. Write `06-decision.md`. It states the problem once, names the goal, lists what to do, and lists what was considered and dropped.
|
|
94
|
+
1. Write `06-decision.md`. It states the problem once, names the goal, lists what to do, and lists what was considered and dropped. Carry the "Overturned by" line of each lean that became the decision into a `## Revisit when` section, and carry none from a lean the decision did not take.
|
|
95
95
|
2. Write `07-next-session.md` self-contained, so it survives a compaction that loses the conversation.
|
|
96
96
|
3. Update the file map in `README.md`.
|
|
97
97
|
4. Write one task file in `.canon/tasks/` recording what the track concluded, even when the conclusion is to do nothing. Follow `${CLAUDE_SKILL_DIR}/../../standards/tasks.md` for the filename and frontmatter. Place the row through `task-board` Step 4, which checks the roster for a live orchestrator before writing `priority.md` or `backlog.md` directly. Aside from an experiment fixture, this and the routing in Step 5 are the only ways close mode reaches outside the folder.
|
|
@@ -87,7 +87,7 @@ A hit the closed set could not separate from correct prose was the case with no
|
|
|
87
87
|
|
|
88
88
|
### Where the rules are enforced
|
|
89
89
|
|
|
90
|
-
Five surfaces apply the ban set and four of them go through this verb. `.claude/hooks/standards-audit.sh` runs it against a single file after each markdown edit, the seed copy a project installs does the same, the `Markdown bans` stage in `canon gate run` runs it across the whole corpus before this repository's own push, and the same-named stage in `
|
|
90
|
+
Five surfaces apply the ban set and four of them go through this verb. `.claude/hooks/standards-audit.sh` runs it against a single file after each markdown edit, the seed copy a project installs does the same, the `Markdown bans` stage in `canon gate run` runs it across the whole corpus before this repository's own push, and the same-named stage in every stack's `scripts/verify.sh` runs it across a scaffolded project's tracked markdown before its own push. Each hook parsed its own copy of the word bans in awk before that, which left a British spelling passing at edit time and failing the push with nothing in between explaining the difference.
|
|
91
91
|
|
|
92
92
|
The seed copy moved onto the verb when the sets became data, since its awk had nothing left to parse. It resolves one runner where the toolkit copy resolves two, looking for no checkout source, and a machine carrying no `canon` gets a report naming the binary to install rather than a silent pass. `scripts/core/check-seed-independence.sh` scopes its walk to markdown and leaves the seed hooks outside it, which its own comment records as deliberate.
|
|
93
93
|
|
|
@@ -64,6 +64,23 @@ carries no evidence path, such as `base` or `python`, hits this reason on
|
|
|
64
64
|
every pull request and posts nothing, which is the correct behavior rather
|
|
65
65
|
than a gap.
|
|
66
66
|
|
|
67
|
+
Both `ok` and `no-evidence` also report what the marked comment already
|
|
68
|
+
shows a reviewer, each field present only when that comment holds it:
|
|
69
|
+
|
|
70
|
+
| Field | What it carries |
|
|
71
|
+
| ----------- | ----------------------------------------------------------------- |
|
|
72
|
+
| `preview` | The hosted `**Preview:**` address the comment opens with. |
|
|
73
|
+
| `local` | The `**Local preview:**` address under it. |
|
|
74
|
+
| `checklist` | The checklist between its delimiters, with any ticked boxes kept. |
|
|
75
|
+
|
|
76
|
+
These come from the comment already posted, never from the flags the call
|
|
77
|
+
passed, so a skill driving a review reads which address and which checklist a
|
|
78
|
+
reviewer has without parsing the comment itself. A checklist posted on its own
|
|
79
|
+
carries no marker, so the record reports none of the three for it. Reading
|
|
80
|
+
them means the thread is read before the `no-evidence` answer too, so an
|
|
81
|
+
unreadable thread refuses as `gh-failed` on both reasons rather than
|
|
82
|
+
reporting the fields absent.
|
|
83
|
+
|
|
67
84
|
## What `[number]` selects, and what it does not
|
|
68
85
|
|
|
69
86
|
Naming a number picks which pull request the rendered body claims to
|
|
@@ -12,14 +12,15 @@ This page is the corpus the coverage claim is measured against: every name `cano
|
|
|
12
12
|
|
|
13
13
|
## Set up a project
|
|
14
14
|
|
|
15
|
-
| Skill
|
|
16
|
-
|
|
|
17
|
-
| `canon:target-setup`
|
|
18
|
-
| `canon:canon-operator`
|
|
19
|
-
| `canon:sketch-design`
|
|
20
|
-
| `canon:design-extract`
|
|
21
|
-
| `canon:draft-diagram`
|
|
22
|
-
| `canon:
|
|
15
|
+
| Skill | When to use |
|
|
16
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
17
|
+
| `canon:target-setup` | On a fresh scaffold, to detect the stack and run the install chain, or to reach one phase of it alone |
|
|
18
|
+
| `canon:canon-operator` | On a project that already exists, to read what it carries before an install is picked |
|
|
19
|
+
| `canon:sketch-design` | Before `design-extract`'s greenfield path, to trace a design direction from reference images or URLs |
|
|
20
|
+
| `canon:design-extract` | Before the first UI feature, to draft `canon/DESIGN.md` |
|
|
21
|
+
| `canon:draft-diagram` | Once the architecture is written, to draft per-kind entries under `.canon/diagrams/` |
|
|
22
|
+
| `canon:deploy-cloudflare` | Once a project is ready to publish, to set up its Cloudflare Pages deploy and stop for the token and domain |
|
|
23
|
+
| `canon:repo-metadata` | When the GitHub About text, homepage, or topics may have drifted, to reconcile them against the README |
|
|
23
24
|
|
|
24
25
|
## Decide what to build
|
|
25
26
|
|
|
@@ -46,6 +47,7 @@ This page is the corpus the coverage claim is measured against: every name `cano
|
|
|
46
47
|
| `canon:test-craft` | When writing or changing any test, to pick its layer and filter what it asserts |
|
|
47
48
|
| `canon:review-craft` | When reviewing a change, for what to look for and how much evidence a finding needs |
|
|
48
49
|
| `canon:systematic-debugging` | When a test fails or a bug surfaces, to force root cause first |
|
|
50
|
+
| `canon:design-taste` | When drafting or judging an interface, or when output reads generic, to settle which decision comes first |
|
|
49
51
|
| `canon:ui-checklist` | After a UI change, to write what to look at and name what ships untested |
|
|
50
52
|
|
|
51
53
|
## Check the work before it leaves the branch
|
package/package.json
CHANGED
package/src/commands/pr.ts
CHANGED
|
@@ -462,6 +462,16 @@ export function register(program: Command): void {
|
|
|
462
462
|
'comment. Together with --checklist it does, so the checklist and the',
|
|
463
463
|
'link land in one comment.',
|
|
464
464
|
'',
|
|
465
|
+
'Both reasons carry what the marked comment already posted, each field',
|
|
466
|
+
'present only when that comment holds it:',
|
|
467
|
+
' preview the hosted **Preview:** address the comment opens with',
|
|
468
|
+
' local the **Local preview:** address under it',
|
|
469
|
+
' checklist the checklist between its delimiters, ticks included',
|
|
470
|
+
'These come from the comment already posted, never from the flags this',
|
|
471
|
+
'call passed. A checklist posted on its own carries no marker, so the',
|
|
472
|
+
'record reports none of the three for it. An unread thread refuses as',
|
|
473
|
+
'gh-failed on both reasons rather than reporting the fields absent.',
|
|
474
|
+
'',
|
|
465
475
|
'Exit codes:',
|
|
466
476
|
' 0 read, whether it produced a body or reported no-evidence',
|
|
467
477
|
' 1 refused, with the reason on stderr or in the JSON record',
|
|
@@ -1424,47 +1434,6 @@ async function runEvidence(
|
|
|
1424
1434
|
baseTree.has(path),
|
|
1425
1435
|
)
|
|
1426
1436
|
|
|
1427
|
-
// A local address rides on a checklist or a hosted preview and never opens
|
|
1428
|
-
// a comment alone, so a docs-only branch with a server up posts nothing.
|
|
1429
|
-
const hasChecklistAndLocal =
|
|
1430
|
-
suppliedChecklist !== undefined && opts.local !== undefined
|
|
1431
|
-
if (
|
|
1432
|
-
grouped.kind === 'refused' &&
|
|
1433
|
-
opts.preview === undefined &&
|
|
1434
|
-
!hasChecklistAndLocal
|
|
1435
|
-
) {
|
|
1436
|
-
logStep('Skipped')
|
|
1437
|
-
logInfo(
|
|
1438
|
-
'No changed path carries an evidence/ segment, so there is nothing to post.',
|
|
1439
|
-
)
|
|
1440
|
-
outro()
|
|
1441
|
-
if (emitJson) {
|
|
1442
|
-
process.stdout.write(
|
|
1443
|
-
`${JSON.stringify({
|
|
1444
|
-
root,
|
|
1445
|
-
...(identity.number !== undefined && { number: identity.number }),
|
|
1446
|
-
reason: 'no-evidence',
|
|
1447
|
-
})}\n`,
|
|
1448
|
-
)
|
|
1449
|
-
}
|
|
1450
|
-
return 0
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
const repoRow = await gh(root, ['repo', 'view', '--json', 'nameWithOwner'])
|
|
1454
|
-
const repo =
|
|
1455
|
-
repoRow === null
|
|
1456
|
-
? undefined
|
|
1457
|
-
: (JSON.parse(repoRow) as { nameWithOwner?: string }).nameWithOwner
|
|
1458
|
-
|
|
1459
|
-
if (repo === undefined) {
|
|
1460
|
-
return refuseWith(
|
|
1461
|
-
'gh-failed',
|
|
1462
|
-
EVIDENCE_REFUSALS['gh-failed'],
|
|
1463
|
-
emitJson,
|
|
1464
|
-
root,
|
|
1465
|
-
)
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
1437
|
let commentId: number | undefined
|
|
1469
1438
|
let carriedPreview: string | undefined
|
|
1470
1439
|
let carriedChecklist: string | undefined
|
|
@@ -1480,6 +1449,8 @@ async function runEvidence(
|
|
|
1480
1449
|
// An unread thread refuses rather than rendering, since a body built
|
|
1481
1450
|
// without it knows neither the comment to edit nor the preview address
|
|
1482
1451
|
// to carry, and posting it would duplicate the comment and drop the link.
|
|
1452
|
+
// It refuses ahead of no-evidence too, so that record never reports an
|
|
1453
|
+
// absent field it did not read.
|
|
1483
1454
|
let comments: readonly { url?: string; body: string }[] | undefined
|
|
1484
1455
|
if (commentsRow !== null) {
|
|
1485
1456
|
try {
|
|
@@ -1506,6 +1477,56 @@ async function runEvidence(
|
|
|
1506
1477
|
carriedLocal = findEvidenceLocal(comments)
|
|
1507
1478
|
}
|
|
1508
1479
|
|
|
1480
|
+
// What the marked comment already shows a reviewer, kept apart from the
|
|
1481
|
+
// flags this call passed so a reader learns what is posted.
|
|
1482
|
+
const carried = {
|
|
1483
|
+
...(carriedPreview !== undefined && { preview: carriedPreview }),
|
|
1484
|
+
...(carriedLocal !== undefined && { local: carriedLocal }),
|
|
1485
|
+
...(carriedChecklist !== undefined && { checklist: carriedChecklist }),
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// A local address rides on a checklist or a hosted preview and never opens
|
|
1489
|
+
// a comment alone, so a docs-only branch with a server up posts nothing.
|
|
1490
|
+
const hasChecklistAndLocal =
|
|
1491
|
+
suppliedChecklist !== undefined && opts.local !== undefined
|
|
1492
|
+
if (
|
|
1493
|
+
grouped.kind === 'refused' &&
|
|
1494
|
+
opts.preview === undefined &&
|
|
1495
|
+
!hasChecklistAndLocal
|
|
1496
|
+
) {
|
|
1497
|
+
logStep('Skipped')
|
|
1498
|
+
logInfo(
|
|
1499
|
+
'No changed path carries an evidence/ segment, so there is nothing to post.',
|
|
1500
|
+
)
|
|
1501
|
+
outro()
|
|
1502
|
+
if (emitJson) {
|
|
1503
|
+
process.stdout.write(
|
|
1504
|
+
`${JSON.stringify({
|
|
1505
|
+
root,
|
|
1506
|
+
...(identity.number !== undefined && { number: identity.number }),
|
|
1507
|
+
reason: 'no-evidence',
|
|
1508
|
+
...carried,
|
|
1509
|
+
})}\n`,
|
|
1510
|
+
)
|
|
1511
|
+
}
|
|
1512
|
+
return 0
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
const repoRow = await gh(root, ['repo', 'view', '--json', 'nameWithOwner'])
|
|
1516
|
+
const repo =
|
|
1517
|
+
repoRow === null
|
|
1518
|
+
? undefined
|
|
1519
|
+
: (JSON.parse(repoRow) as { nameWithOwner?: string }).nameWithOwner
|
|
1520
|
+
|
|
1521
|
+
if (repo === undefined) {
|
|
1522
|
+
return refuseWith(
|
|
1523
|
+
'gh-failed',
|
|
1524
|
+
EVIDENCE_REFUSALS['gh-failed'],
|
|
1525
|
+
emitJson,
|
|
1526
|
+
root,
|
|
1527
|
+
)
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1509
1530
|
const preview = opts.preview ?? carriedPreview
|
|
1510
1531
|
const local = opts.local ?? carriedLocal
|
|
1511
1532
|
const checklist = suppliedChecklist ?? carriedChecklist
|
|
@@ -1548,6 +1569,7 @@ async function runEvidence(
|
|
|
1548
1569
|
head: identity.head,
|
|
1549
1570
|
body,
|
|
1550
1571
|
...(commentId !== undefined && { commentId }),
|
|
1572
|
+
...carried,
|
|
1551
1573
|
})}\n`,
|
|
1552
1574
|
)
|
|
1553
1575
|
}
|
package/standards/context.md
CHANGED
|
@@ -56,7 +56,7 @@ Both fields feed `canon/context/index.md` when regenerated.
|
|
|
56
56
|
|
|
57
57
|
Include whenever the domain has content for them. Never pad an entry with an empty heading.
|
|
58
58
|
|
|
59
|
-
- `## Decisions`: non-obvious choices and the reasoning behind them. The highest-value section in the folder and the one most often missing.
|
|
59
|
+
- `## Decisions`: non-obvious choices and the reasoning behind them. The highest-value section in the folder and the one most often missing. A decision resting on how an outside tool, an installer, or the model behaves may close with one `Revisit when <finding>.` sentence naming the harm the decision prevents, and a decision resting on the project's own code takes none.
|
|
60
60
|
- `## Gotchas`: workarounds, things tried and rejected, scar tissue worth preserving.
|
|
61
61
|
|
|
62
62
|
## Optional sections
|
package/standards/decisions.md
CHANGED
|
@@ -45,11 +45,12 @@ A record failing these is non-conforming even when it satisfies every shape rule
|
|
|
45
45
|
|
|
46
46
|
## Sections
|
|
47
47
|
|
|
48
|
-
Use `## Context`, `## Decision`, `## Alternatives`, and `## Measurements`.
|
|
48
|
+
Use `## Context`, `## Decision`, `## Alternatives`, `## Revisit when`, and `## Measurements`.
|
|
49
49
|
|
|
50
50
|
- `## Context`: the problem as it stood, stated so a reader needs nothing else open. Restate a fact rather than pointing at where it was found.
|
|
51
51
|
- `## Decision`: what was chosen, and why, in enough detail that a reader can tell it apart from an alternative that sounds similar.
|
|
52
52
|
- `## Alternatives`: each one considered and dropped, with the reason it lost. An alternative with no stated reason reads as a claim nobody checked.
|
|
53
|
+
- `## Revisit when`: one bullet per finding that would reopen the decision. Required when the record came from a groundwork track whose deciding leans carried "Overturned by" lines, which carry over as written. It is optional otherwise, for a decision resting on how an outside tool, an installer, or the model behaves. Skip it for a decision resting on the project's own code.
|
|
53
54
|
- `## Measurements`: skip when the decision cites no number. When it does, state the number and close with the commit it was read against, per Verification anchors below.
|
|
54
55
|
|
|
55
56
|
## Verification anchors
|
|
@@ -94,6 +95,10 @@ description: <one line naming what was decided>
|
|
|
94
95
|
- **<Alternative>.** <Why it lost.>
|
|
95
96
|
- **<Alternative>.** <Why it lost.>
|
|
96
97
|
|
|
98
|
+
## Revisit when
|
|
99
|
+
|
|
100
|
+
- <The finding that would reopen the decision.>
|
|
101
|
+
|
|
97
102
|
## Measurements
|
|
98
103
|
|
|
99
104
|
<The number the decision rests on.> Measured at <short-sha> on <YYYY-MM-DD>.
|
package/standards/groundwork.md
CHANGED
|
@@ -102,6 +102,7 @@ Closes the folder. Everything above it is input.
|
|
|
102
102
|
- The goal
|
|
103
103
|
- The items to do
|
|
104
104
|
- What was considered and dropped
|
|
105
|
+
- `## Revisit when`: the "Overturned by" line of each lean that became the decision, carried over as written. A lean the decision did not take carries nothing, and a track whose deciding leans carried no overturn line omits the section.
|
|
105
106
|
|
|
106
107
|
The dropped list is what stops a future session re-proposing something already rejected.
|
|
107
108
|
|
|
@@ -21,7 +21,7 @@ Does not govern:
|
|
|
21
21
|
|
|
22
22
|
- The problem being solved and for whom
|
|
23
23
|
- User-facing goals stated as outcomes, not implementation
|
|
24
|
-
- Explicit non-goals that prevent feature creep. Mark deferred items "(deferred)" so they read as paused, not excluded.
|
|
24
|
+
- Explicit non-goals that prevent feature creep. Mark deferred items "(deferred)" so they read as paused, not excluded. A non-goal resting on how an outside tool, an installer, or the model behaves may close with one `Revisit when <finding>.` sentence, and a non-goal resting on scope alone takes none.
|
|
25
25
|
- MVP features as a numbered list: feature name and one-line description
|
|
26
26
|
- Tech stack as a plain list of tools
|
|
27
27
|
- Hard constraints that shape every decision
|
package/standards/rule.md
CHANGED
|
@@ -79,6 +79,7 @@ paths:
|
|
|
79
79
|
- Use imperative voice for every rule (`Prefix booleans with is`, not `Booleans should be prefixed`)
|
|
80
80
|
- State one rule per bullet as a single directive line
|
|
81
81
|
- State what to do and what not to do. Do not explain the reasoning behind a rule.
|
|
82
|
+
- End the directive line of a ban asserting how the model or a tool behaves with one `Revisit when <finding>.` sentence, which states a condition rather than the reasoning. A style, safety, or consistency ban takes none.
|
|
82
83
|
- Phrase a rule as a ban on the forbidden shape when it could otherwise enumerate allowed options, so it stays stable as categories grow
|
|
83
84
|
- Cut any rule that resists crisp one-line phrasing. Vague guidance is worse than none.
|
|
84
85
|
- Keep the file to one topic. A second topic is a second rule file.
|
package/standards/skill.md
CHANGED
|
@@ -105,7 +105,7 @@ allowed-tools: <tools required>
|
|
|
105
105
|
- Group bullets under H2 headings by domain concern. Keep dos and don'ts together under the topic they belong to rather than splitting them into flat rules and constraints sections.
|
|
106
106
|
- One actionable constraint per bullet. Prefer the `X over Y` form for preferences.
|
|
107
107
|
- Do not include code examples unless a one to three line inline snippet captures a pattern the model cannot infer.
|
|
108
|
-
- Do not duplicate general knowledge the model already has. Focus on project-specific conventions and preferences.
|
|
108
|
+
- Do not duplicate general knowledge the model already has. Focus on project-specific conventions and preferences. Revisit when removing a line of general knowledge from a skill is shown to change what the agent does.
|
|
109
109
|
|
|
110
110
|
### Progressive disclosure
|
|
111
111
|
|
package/standards/standard.md
CHANGED
|
@@ -79,6 +79,7 @@ A standard failing these questions is non-conforming even when it satisfies ever
|
|
|
79
79
|
- State the forbidden shape rather than enumerating allowed options, so a rule survives new categories
|
|
80
80
|
- Cut any rule that resists a crisp one-line phrasing
|
|
81
81
|
- Do not pad with filler prose. Every line earns its place as a usable reference entry.
|
|
82
|
+
- Close a ban asserting how the model or a tool behaves with one `Revisit when <finding>.` sentence, since that claim can stop holding while the ban reads the same. A style, safety, or consistency ban takes none, and filling one with a condition is padding.
|
|
82
83
|
|
|
83
84
|
## Success criterion
|
|
84
85
|
|
|
@@ -52,6 +52,50 @@ run_base_phase() {
|
|
|
52
52
|
log_info "$pass_msg"
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
check_markdown_bans() {
|
|
56
|
+
if ! command -v canon >/dev/null 2>&1; then
|
|
57
|
+
log_info "Skipped: no canon binary on PATH. Install with \`bun install --global @erclx/canon\`."
|
|
58
|
+
return 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
local files=()
|
|
62
|
+
while IFS= read -r file; do
|
|
63
|
+
files+=("$file")
|
|
64
|
+
done < <(git ls-files --cached --others --exclude-standard -- '*.md' ':(exclude)CHANGELOG.md' ':(exclude)**/CHANGELOG.md')
|
|
65
|
+
|
|
66
|
+
if [ ${#files[@]} -eq 0 ]; then
|
|
67
|
+
log_info "No markdown file to check outside the exclusion set."
|
|
68
|
+
return 0
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
local output code
|
|
72
|
+
if output=$(canon markdown audit "${files[@]}" 2>&1); then
|
|
73
|
+
code=0
|
|
74
|
+
else
|
|
75
|
+
code=$?
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
case "$code" in
|
|
79
|
+
0)
|
|
80
|
+
log_info "No banned character"
|
|
81
|
+
;;
|
|
82
|
+
1)
|
|
83
|
+
log_info "Skipped: canon markdown audit refused and measured nothing."
|
|
84
|
+
;;
|
|
85
|
+
2)
|
|
86
|
+
echo "$output" | pipe_output
|
|
87
|
+
log_error "Markdown prose carries a banned character, or a relative link resolves to nothing on disk"
|
|
88
|
+
;;
|
|
89
|
+
3)
|
|
90
|
+
echo "$output" | pipe_output
|
|
91
|
+
log_error "The markdown audit shipped an empty ban set, so the corpus was walked and nothing was looked for"
|
|
92
|
+
;;
|
|
93
|
+
*)
|
|
94
|
+
log_error "canon markdown audit exited $code, which is neither a pass nor a finding"
|
|
95
|
+
;;
|
|
96
|
+
esac
|
|
97
|
+
}
|
|
98
|
+
|
|
55
99
|
run_check() {
|
|
56
100
|
local cmd=$1
|
|
57
101
|
local err_msg=$2
|
|
@@ -86,6 +130,9 @@ main() {
|
|
|
86
130
|
run_check "bun run test:run" "Unit tests failed"
|
|
87
131
|
log_info "Unit tests passed"
|
|
88
132
|
|
|
133
|
+
log_step "Markdown bans"
|
|
134
|
+
check_markdown_bans
|
|
135
|
+
|
|
89
136
|
if [ "$NESTED" = false ]; then
|
|
90
137
|
echo -e "${GREY}└${NC}\n"
|
|
91
138
|
echo -e "${GREEN}✓ Verification passed${NC}"
|
package/tooling/go/reference.md
CHANGED
|
@@ -28,7 +28,7 @@ The go stack also ships `smoke_test.go` at the module root as a copy-once seed.
|
|
|
28
28
|
|
|
29
29
|
## verify.sh
|
|
30
30
|
|
|
31
|
-
- Run Typecheck (`go vet`), Lint (`golangci-lint run`), and Unit tests (`go test -v`), plus the four base phases wherever the folder declares their scripts.
|
|
31
|
+
- Run Typecheck (`go vet`), Lint (`golangci-lint run`), and Unit tests (`go test -v`), plus the four base phases wherever the folder declares their scripts, then the markdown bans stage base ships.
|
|
32
32
|
- Keep `build` out of `verify.sh`. `go build ./...` exits 1 with `no packages to build` on a module holding only a test file, and a seeded `main.go` would assume every module is a binary rather than a library.
|
|
33
33
|
- Keep `-race` out of `test:run`. The race detector needs cgo and a C toolchain, so it belongs in CI rather than in a local `bun run check`.
|
|
34
34
|
|
|
@@ -56,6 +56,50 @@ run_base_phase() {
|
|
|
56
56
|
log_info "$pass_msg"
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
check_markdown_bans() {
|
|
60
|
+
if ! command -v canon >/dev/null 2>&1; then
|
|
61
|
+
log_info "Skipped: no canon binary on PATH. Install with \`bun install --global @erclx/canon\`."
|
|
62
|
+
return 0
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
local files=()
|
|
66
|
+
while IFS= read -r file; do
|
|
67
|
+
files+=("$file")
|
|
68
|
+
done < <(git ls-files --cached --others --exclude-standard -- '*.md' ':(exclude)CHANGELOG.md' ':(exclude)**/CHANGELOG.md')
|
|
69
|
+
|
|
70
|
+
if [ ${#files[@]} -eq 0 ]; then
|
|
71
|
+
log_info "No markdown file to check outside the exclusion set."
|
|
72
|
+
return 0
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
local output code
|
|
76
|
+
if output=$(canon markdown audit "${files[@]}" 2>&1); then
|
|
77
|
+
code=0
|
|
78
|
+
else
|
|
79
|
+
code=$?
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
case "$code" in
|
|
83
|
+
0)
|
|
84
|
+
log_info "No banned character"
|
|
85
|
+
;;
|
|
86
|
+
1)
|
|
87
|
+
log_info "Skipped: canon markdown audit refused and measured nothing."
|
|
88
|
+
;;
|
|
89
|
+
2)
|
|
90
|
+
echo "$output" | pipe_output
|
|
91
|
+
log_error "Markdown prose carries a banned character, or a relative link resolves to nothing on disk"
|
|
92
|
+
;;
|
|
93
|
+
3)
|
|
94
|
+
echo "$output" | pipe_output
|
|
95
|
+
log_error "The markdown audit shipped an empty ban set, so the corpus was walked and nothing was looked for"
|
|
96
|
+
;;
|
|
97
|
+
*)
|
|
98
|
+
log_error "canon markdown audit exited $code, which is neither a pass nor a finding"
|
|
99
|
+
;;
|
|
100
|
+
esac
|
|
101
|
+
}
|
|
102
|
+
|
|
59
103
|
run_check() {
|
|
60
104
|
local cmd=$1
|
|
61
105
|
local err_msg=$2
|
|
@@ -91,6 +135,9 @@ main() {
|
|
|
91
135
|
run_check "bun run test:run" "Unit tests failed"
|
|
92
136
|
log_info "Unit tests passed"
|
|
93
137
|
|
|
138
|
+
log_step "Markdown bans"
|
|
139
|
+
check_markdown_bans
|
|
140
|
+
|
|
94
141
|
if [ "$NESTED" = false ]; then
|
|
95
142
|
echo -e "${GREY}└${NC}\n"
|
|
96
143
|
echo -e "${GREEN}✓ Verification passed${NC}"
|
package/tooling/php/reference.md
CHANGED
|
@@ -42,7 +42,7 @@ The php stack also ships `tests/SmokeTest.php` as a copy-once seed, so `bun run
|
|
|
42
42
|
## verify.sh
|
|
43
43
|
|
|
44
44
|
- Stop and name `composer install` when `vendor/bin/phpunit` is missing.
|
|
45
|
-
- Run Typecheck (`phpstan analyse`), Lint (`php-cs-fixer check`), and Unit tests (`phpunit --testdox`), plus the four base phases wherever the folder declares their scripts.
|
|
45
|
+
- Run Typecheck (`phpstan analyse`), Lint (`php-cs-fixer check`), and Unit tests (`phpunit --testdox`), plus the four base phases wherever the folder declares their scripts, then the markdown bans stage base ships.
|
|
46
46
|
|
|
47
47
|
## Gitignore
|
|
48
48
|
|
|
@@ -51,6 +51,50 @@ run_base_phase() {
|
|
|
51
51
|
log_info "$pass_msg"
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
check_markdown_bans() {
|
|
55
|
+
if ! command -v canon >/dev/null 2>&1; then
|
|
56
|
+
log_info "Skipped: no canon binary on PATH. Install with \`bun install --global @erclx/canon\`."
|
|
57
|
+
return 0
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
local files=()
|
|
61
|
+
while IFS= read -r file; do
|
|
62
|
+
files+=("$file")
|
|
63
|
+
done < <(git ls-files --cached --others --exclude-standard -- '*.md' ':(exclude)CHANGELOG.md' ':(exclude)**/CHANGELOG.md')
|
|
64
|
+
|
|
65
|
+
if [ ${#files[@]} -eq 0 ]; then
|
|
66
|
+
log_info "No markdown file to check outside the exclusion set."
|
|
67
|
+
return 0
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
local output code
|
|
71
|
+
if output=$(canon markdown audit "${files[@]}" 2>&1); then
|
|
72
|
+
code=0
|
|
73
|
+
else
|
|
74
|
+
code=$?
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
case "$code" in
|
|
78
|
+
0)
|
|
79
|
+
log_info "No banned character"
|
|
80
|
+
;;
|
|
81
|
+
1)
|
|
82
|
+
log_info "Skipped: canon markdown audit refused and measured nothing."
|
|
83
|
+
;;
|
|
84
|
+
2)
|
|
85
|
+
echo "$output" | pipe_output
|
|
86
|
+
log_error "Markdown prose carries a banned character, or a relative link resolves to nothing on disk"
|
|
87
|
+
;;
|
|
88
|
+
3)
|
|
89
|
+
echo "$output" | pipe_output
|
|
90
|
+
log_error "The markdown audit shipped an empty ban set, so the corpus was walked and nothing was looked for"
|
|
91
|
+
;;
|
|
92
|
+
*)
|
|
93
|
+
log_error "canon markdown audit exited $code, which is neither a pass nor a finding"
|
|
94
|
+
;;
|
|
95
|
+
esac
|
|
96
|
+
}
|
|
97
|
+
|
|
54
98
|
run_check() {
|
|
55
99
|
local cmd=$1
|
|
56
100
|
local err_msg=$2
|
|
@@ -85,6 +129,9 @@ main() {
|
|
|
85
129
|
run_check "bun run test:run" "Unit tests failed"
|
|
86
130
|
log_info "Unit tests passed"
|
|
87
131
|
|
|
132
|
+
log_step "Markdown bans"
|
|
133
|
+
check_markdown_bans
|
|
134
|
+
|
|
88
135
|
if [ "$NESTED" = false ]; then
|
|
89
136
|
echo -e "${GREY}└${NC}\n"
|
|
90
137
|
echo -e "${GREEN}✓ Verification passed${NC}"
|
|
@@ -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.sh`: overrides base verify to add Typecheck (`mypy`), Lint (`ruff check && ruff format --check`), and Tests (`pytest -v`) phases.
|
|
30
|
+
- `scripts/verify.sh`: overrides base verify to add Typecheck (`mypy`), Lint (`ruff check && ruff format --check`), and Tests (`pytest -v`) phases, then the same markdown bans phase base ships.
|
|
31
31
|
|
|
32
32
|
## Hybrid project shape
|
|
33
33
|
|
|
@@ -50,6 +50,50 @@ run_base_phase() {
|
|
|
50
50
|
log_info "$pass_msg"
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
check_markdown_bans() {
|
|
54
|
+
if ! command -v canon >/dev/null 2>&1; then
|
|
55
|
+
log_info "Skipped: no canon binary on PATH. Install with \`bun install --global @erclx/canon\`."
|
|
56
|
+
return 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
local files=()
|
|
60
|
+
while IFS= read -r file; do
|
|
61
|
+
files+=("$file")
|
|
62
|
+
done < <(git ls-files --cached --others --exclude-standard -- '*.md' ':(exclude)CHANGELOG.md' ':(exclude)**/CHANGELOG.md')
|
|
63
|
+
|
|
64
|
+
if [ ${#files[@]} -eq 0 ]; then
|
|
65
|
+
log_info "No markdown file to check outside the exclusion set."
|
|
66
|
+
return 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
local output code
|
|
70
|
+
if output=$(canon markdown audit "${files[@]}" 2>&1); then
|
|
71
|
+
code=0
|
|
72
|
+
else
|
|
73
|
+
code=$?
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
case "$code" in
|
|
77
|
+
0)
|
|
78
|
+
log_info "No banned character"
|
|
79
|
+
;;
|
|
80
|
+
1)
|
|
81
|
+
log_info "Skipped: canon markdown audit refused and measured nothing."
|
|
82
|
+
;;
|
|
83
|
+
2)
|
|
84
|
+
echo "$output" | pipe_output
|
|
85
|
+
log_error "Markdown prose carries a banned character, or a relative link resolves to nothing on disk"
|
|
86
|
+
;;
|
|
87
|
+
3)
|
|
88
|
+
echo "$output" | pipe_output
|
|
89
|
+
log_error "The markdown audit shipped an empty ban set, so the corpus was walked and nothing was looked for"
|
|
90
|
+
;;
|
|
91
|
+
*)
|
|
92
|
+
log_error "canon markdown audit exited $code, which is neither a pass nor a finding"
|
|
93
|
+
;;
|
|
94
|
+
esac
|
|
95
|
+
}
|
|
96
|
+
|
|
53
97
|
run_check() {
|
|
54
98
|
local cmd=$1
|
|
55
99
|
local err_msg=$2
|
|
@@ -88,6 +132,9 @@ main() {
|
|
|
88
132
|
run_check "bun run build" "Build failed"
|
|
89
133
|
log_info "Build passed"
|
|
90
134
|
|
|
135
|
+
log_step "Markdown bans"
|
|
136
|
+
check_markdown_bans
|
|
137
|
+
|
|
91
138
|
# Runs whenever the stack shipped a card exclusion check, which keeps the
|
|
92
139
|
# stage stack-agnostic here and the check itself in the stack that owns the
|
|
93
140
|
# card route's shape. It reads the build above, so it sits after it.
|
package/tooling/web/reference.md
CHANGED
|
@@ -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.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.
|
|
131
|
+
The web layer's `scripts/verify.sh` replaces the base version. Order: typecheck, lint, format, spelling, shell, unit tests, build, markdown bans, then the card exclusion check where one is installed. Stack adapters may override if their typecheck or build differs.
|
|
132
132
|
|
|
133
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.
|