@erclx/canon 4.62.0 → 4.63.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/canon-cli/REQUIREMENT.md +7 -3
- package/claude/skills/canon-cli/SKILL.md +8 -2
- package/claude/skills/{context-draft → draft-context}/REQUIREMENT.md +3 -3
- package/claude/skills/{context-draft → draft-context}/SKILL.md +1 -1
- package/claude/skills/{docs-draft → draft-docs}/REQUIREMENT.md +1 -1
- package/claude/skills/{docs-draft → draft-docs}/SKILL.md +1 -1
- package/claude/skills/{wireframe-draft → draft-wireframes}/REQUIREMENT.md +3 -3
- package/claude/skills/{wireframe-draft → draft-wireframes}/SKILL.md +1 -1
- package/claude/skills/repo-metadata/SKILL.md +6 -5
- package/docs/workflow/ai-workflow.md +11 -11
- package/package.json +1 -1
- package/src/claude/cases/authoring.ts +3 -3
- package/src/commands/tooling.ts +8 -1
- package/src/migrate/scratch-evidence.ts +41 -34
- package/src/project-root.ts +34 -0
- package/src/shipped/references.ts +45 -15
- package/tooling/astro/configs/eslint.config.js +1 -1
- package/tooling/astro/reference.md +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: canon-cli
|
|
3
|
-
description: Why the
|
|
3
|
+
description: Why a session is pointed at the verb catalog, the reference docs, and the overwrite contract from one place, and why the skill reads rather than writes
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Canon cli requirement
|
|
@@ -17,17 +17,20 @@ Being reachable is a separate problem from being right. This is a pure reference
|
|
|
17
17
|
|
|
18
18
|
The two bodies now carrying an inline pointer are `claude-seed-sync` and `canon-operator`, each at the point it runs or prints an overwriting command.
|
|
19
19
|
|
|
20
|
+
A third gap sits beside the first two, aimed at a different question. `canon --help` lists every top-level verb and `canon docs` emits the toolkit's own reference corpus, but no reference skill pointed a session at either. The one skill that does, `canon-operator`, is user-invoked only and reaches them as a side effect of its own orientation step. A session guessing at a verb's name, or restating what a doc already answers, is the same missing-fact failure the overwrite gap names.
|
|
21
|
+
|
|
20
22
|
## Must
|
|
21
23
|
|
|
22
24
|
- State per surface and per command whether an existing file is overwritten, merged, written once, or left alone
|
|
23
25
|
- Warn about a destructive run before it happens, naming the surface that will be lost
|
|
24
26
|
- Name the section-preserving path for a standard or seed the project has customized
|
|
25
27
|
- Defer to the toolkit's own context docs when they and this summary disagree, since the summary is a copy and they are the source
|
|
28
|
+
- Point to `canon docs agents` for the verb catalog and to `canon docs` for the reference corpus, rather than restating either
|
|
26
29
|
|
|
27
30
|
## Must not
|
|
28
31
|
|
|
29
|
-
- Run
|
|
30
|
-
- Restate the
|
|
32
|
+
- Run, or send the session to run, a `canon` command that writes or installs. The skill is read before acting on a sync.
|
|
33
|
+
- Restate the verb catalog or a reference doc's content. Point to `canon docs agents` or `canon docs <topic>` instead. The overwrite table is this skill's one authored exception, and it too is a target-session summary, with the detail living with its owner.
|
|
31
34
|
- Take a mention in a sibling's requirement file as an inbound route. The three bodies named in the gap above carry the pointer, and adding a fourth means editing that body rather than its requirement.
|
|
32
35
|
|
|
33
36
|
## Guards
|
|
@@ -39,3 +42,4 @@ The two bodies now carrying an inline pointer are `claude-seed-sync` and `canon-
|
|
|
39
42
|
- Executing the sync, which the user runs or `canon-operator` routes
|
|
40
43
|
- Reconciling a customized seed section by section: `claude-seed-sync`
|
|
41
44
|
- Deciding which stack, rule, or standard a project should install, which the setup skills resolve from live catalogs
|
|
45
|
+
- Diagnosing what a project is behind on, or executing the fix: `canon-operator`
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: canon-cli
|
|
3
|
-
description: Reference for what canon
|
|
3
|
+
description: Reference for what a canon verb does before you run it. Run `canon docs agents` for the full command catalog, `canon docs` for any other reference doc, and read this skill's own table for what a sync or install command overwrites, merges, or leaves untouched in a target project. Use when asked "which canon command do I run", "is there a doc for X", "will this overwrite my changes", or before running `canon tooling`, `canon standards`, `canon claude sync`, or `canon init`. Do NOT use to execute a sync, an install, or a docs lookup, only to know which command answers the question.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Toolkit CLI contract
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Consult before running an unfamiliar `canon` verb, before a sync or install, or when asked "will this overwrite my changes". This skill is reference only. It never runs a sync or an install.
|
|
9
|
+
|
|
10
|
+
## Verb catalog and reference docs
|
|
11
|
+
|
|
12
|
+
- Run `canon docs agents` for the full command catalog and invocation contract, rather than guessing at a verb from its name.
|
|
13
|
+
- Run `canon docs list` for every other reference doc, then `canon docs <topic>` by name.
|
|
14
|
+
- State neither list here. Both read live off the CLI, and a copy in this body ships on a different cadence than the commands it names.
|
|
9
15
|
|
|
10
16
|
## Overwrite contract
|
|
11
17
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: context
|
|
2
|
+
name: draft-context
|
|
3
3
|
description: Why a brand-new .claude/context/<domain>.md entry needs a catalog collision check and a confirm step, not the refresh path claude-docs already owns
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -32,6 +32,6 @@ Without this skill, a session documenting a domain that has no context entry yet
|
|
|
32
32
|
## Out of scope
|
|
33
33
|
|
|
34
34
|
- Refreshing an existing `.claude/context/<domain>.md` entry against a diff: `claude-docs`
|
|
35
|
-
- Drafting a `.claude/wireframes/<surface>.md` file: `
|
|
36
|
-
- Drafting a `docs/*.md` page: `docs
|
|
35
|
+
- Drafting a `.claude/wireframes/<surface>.md` file: `draft-wireframes`
|
|
36
|
+
- Drafting a `docs/*.md` page: `draft-docs`
|
|
37
37
|
- Drafting a standard, a snippet, or a governance rule: `create-standard`, `create-snippet`, `create-rule`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: context
|
|
2
|
+
name: draft-context
|
|
3
3
|
description: Drafts a brand-new `.claude/context/<domain>.md` entry against the context standard, checks the catalog for a name-or-topic collision, decides flat-file placement, confirms with the user, then writes. Use when asked to "write a context entry for X", "document the X domain", "add a context entry for X", or "create a .claude/context page for X" where no existing entry covers the domain. Do NOT use to refresh an existing entry against a diff, which is `claude-docs`.
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: docs
|
|
2
|
+
name: draft-docs
|
|
3
3
|
description: Drafts a brand-new `docs/*.md` page against the docs standard, decides its placement in the existing catalog, confirms with the user, then writes. Use when asked to "add a docs page for X", "write a new doc for X", "document X under docs/", or "create a docs page for X" where no existing page covers the topic. Do NOT use to rewrite or sync an existing `docs/*.md` section against a diff, which is `docs-sync`.
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: draft-wireframes
|
|
3
3
|
description: Why a brand-new .claude/wireframes/<surface>.md file needs a real draft and a tier-detect step, not the stub the wireframe-sweep already writes
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -32,7 +32,7 @@ Without this skill, a session drafting a wireframe for a surface with no file ye
|
|
|
32
32
|
## Out of scope
|
|
33
33
|
|
|
34
34
|
- Stubbing a surface a diff touched, or reporting drift in an existing wireframe against a diff: `claude/skills/claude-docs/references/wireframe-sweep.md`
|
|
35
|
-
- Drafting a `.claude/context/<domain>.md` entry: `context
|
|
36
|
-
- Drafting a `docs/*.md` page: `docs
|
|
35
|
+
- Drafting a `.claude/context/<domain>.md` entry: `draft-context`
|
|
36
|
+
- Drafting a `docs/*.md` page: `draft-docs`
|
|
37
37
|
- Drafting a standard, a snippet, or a governance rule: `create-standard`, `create-snippet`, `create-rule`
|
|
38
38
|
- Building or wiring an actual tier-1 or tier-2 rendering pipeline: the visual design workflow's own tier guidance, out of reach of a hand-drafting skill
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: draft-wireframes
|
|
3
3
|
description: Drafts a brand-new `.claude/wireframes/<surface>.md` file against the wireframes standard, walks the tree for a name collision, detects an existing higher visual-design tier without building one, confirms with the user, then writes. Use when asked to "draft a wireframe for X", "write the wireframe for this surface", "add a .claude/wireframes entry for X", or "wireframe this screen" where no surface file covers it yet. Do NOT use to fix a stale TODO stub or report wireframe drift against a diff, which is `claude-docs`'s wireframe-sweep step.
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -19,13 +19,14 @@ Reads what `canon repo metadata propose` computes locally and reports it against
|
|
|
19
19
|
|
|
20
20
|
1. Run `canon repo metadata propose --json`.
|
|
21
21
|
2. Read `diff` and `repo` from the record. `repo` is the `--repo` value the later apply step must carry. An empty `diff` means the remote already matches what this run computed: report that and stop, since there is nothing to answer.
|
|
22
|
-
3.
|
|
23
|
-
4.
|
|
24
|
-
5.
|
|
22
|
+
3. When `diff` carries a `description` field, compress its proposed value, the raw opening line `canon repo metadata propose` computed, into a single short phrase. Leave the shape and length of the phrase to judgment on each run. No numeric target applies. Keep the raw proposed value beside the compressed phrase in what gets reported, so the operator can check the phrase against the README line it came from.
|
|
23
|
+
4. For each field `diff` carries, put the change to the operator through the structured question surface: which of the differing fields to write. For `description`, the value offered to accept is the compressed phrase from step 3, not the raw line beside it. Rank accepting the proposed value first for a field whose current value is stale or wrong, and give the reject option the cost of leaving the remote as it stands. Never pre-select an answer for the operator.
|
|
24
|
+
5. Report the fields the proposal left absent as unchanged, naming that neither the README nor `package.json` carried a source for them.
|
|
25
|
+
6. Stop. Do not run apply here even when the operator answers immediately, since answering is not yet an apply invocation.
|
|
25
26
|
|
|
26
27
|
## Apply
|
|
27
28
|
|
|
28
|
-
1. Confirm every field about to be written was answered by the operator in this conversation. Carry no field forward unanswered.
|
|
29
|
+
1. Confirm every field about to be written was answered by the operator in this conversation. Carry no field forward unanswered. For `description`, the answered value is the compressed phrase Propose offered, never the raw README line reported beside it.
|
|
29
30
|
2. Run `canon repo metadata apply`, always passing `--repo <owner/name>` from the propose record's `repo` field, plus the flags for the answered fields: `--description <text>`, `--homepage <url>`, `--topics <comma-separated list>`. `--topics` is the full desired set, and the command reads the current set itself to compute what to add and remove. The command refuses rather than writing when `--repo` does not match what `--root` resolves to, so never omit it and never guess it from anything but the propose record's `repo` field.
|
|
30
31
|
3. Report the written state from the JSON record.
|
|
31
32
|
|
|
@@ -35,7 +36,7 @@ Propose:
|
|
|
35
36
|
|
|
36
37
|
```plaintext
|
|
37
38
|
📋 Repo metadata proposal
|
|
38
|
-
description: "<current>" → "<
|
|
39
|
+
description: "<current>" → "<compressed>" (derived from the README's opening line: "<raw>")
|
|
39
40
|
homepage: unchanged, no local source
|
|
40
41
|
topics: +<added>, -<removed>
|
|
41
42
|
|
|
@@ -291,9 +291,9 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
291
291
|
| `canon:create-skill` | For a new `SKILL.md` |
|
|
292
292
|
| `canon:create-snippet` | For a reusable prompt |
|
|
293
293
|
| `canon:create-standard` | For a new authoring convention |
|
|
294
|
-
| `canon:docs
|
|
295
|
-
| `canon:context
|
|
296
|
-
| `canon:
|
|
294
|
+
| `canon:draft-docs` | For a brand-new `docs/*.md` page, drafted against `standards/docs.md` |
|
|
295
|
+
| `canon:draft-context` | For a brand-new `.claude/context/<domain>.md` entry, drafted against `standards/context.md` |
|
|
296
|
+
| `canon:draft-wireframes` | For a brand-new `.claude/wireframes/<surface>.md` file, drafted against `standards/wireframes.md` |
|
|
297
297
|
| `canon:bash-script` | For an interactive, human-facing shell tool |
|
|
298
298
|
| `canon:bash-cli-script` | For a non-interactive automation, CI, or pipeline script |
|
|
299
299
|
| `canon:ci-workflow` | For a GitHub Actions workflow file |
|
|
@@ -302,14 +302,14 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
302
302
|
|
|
303
303
|
### Answer a question at any point
|
|
304
304
|
|
|
305
|
-
| Skill | When to use
|
|
306
|
-
| --------------------------- |
|
|
307
|
-
| `canon:canon-cli` | Before a sync or install, to learn what it overwrites, merges, or leaves alone |
|
|
308
|
-
| `canon:index-lookup` | To find where a topic is documented across the tracked `index.md` catalogs
|
|
309
|
-
| `canon:youtube-transcripts` | When a video transcript is wanted in the repo as context
|
|
310
|
-
| `canon:claude-teach` | To learn a subject across sessions, in a workspace that holds the progress
|
|
311
|
-
| `canon:write-human` | Before drafting or revising prose, for voice, rhythm, and density
|
|
312
|
-
| `canon:restate-plainly` | When an answer or a document has to be read again in plain words
|
|
305
|
+
| Skill | When to use |
|
|
306
|
+
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
307
|
+
| `canon:canon-cli` | Before running an unfamiliar verb, a sync, or an install, to learn which command to run, which reference doc covers it, or what it overwrites, merges, or leaves alone |
|
|
308
|
+
| `canon:index-lookup` | To find where a topic is documented across the tracked `index.md` catalogs |
|
|
309
|
+
| `canon:youtube-transcripts` | When a video transcript is wanted in the repo as context |
|
|
310
|
+
| `canon:claude-teach` | To learn a subject across sessions, in a workspace that holds the progress |
|
|
311
|
+
| `canon:write-human` | Before drafting or revising prose, for voice, rhythm, and density |
|
|
312
|
+
| `canon:restate-plainly` | When an answer or a document has to be read again in plain words |
|
|
313
313
|
|
|
314
314
|
Every row answers a question rather than marking a point in a project's life, so a phase above would send a reader to the wrong group.
|
|
315
315
|
|
package/package.json
CHANGED
|
@@ -40,17 +40,17 @@ export const AUTHORING_CASES: readonly SkillCase[] = [
|
|
|
40
40
|
{
|
|
41
41
|
prompt:
|
|
42
42
|
'Write a brand-new docs page for the capture command, nothing under docs/ covers it yet.',
|
|
43
|
-
expect: 'docs
|
|
43
|
+
expect: 'draft-docs',
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
prompt:
|
|
47
47
|
'Write a context entry for the payments domain, there is no .claude/context page for it yet.',
|
|
48
|
-
expect: 'context
|
|
48
|
+
expect: 'draft-context',
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
prompt:
|
|
52
52
|
'Draft a wireframe for the settings panel, nothing under .claude/wireframes covers that surface yet.',
|
|
53
|
-
expect: '
|
|
53
|
+
expect: 'draft-wireframes',
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
56
|
prompt: 'Say what that dense answer actually means in plain terms.',
|
package/src/commands/tooling.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import type { Command } from 'commander'
|
|
3
3
|
import { execScript } from '@/exec'
|
|
4
|
-
import { PROJECT_ROOT } from '@/project-root'
|
|
4
|
+
import { findCheckoutMismatch, PROJECT_ROOT } from '@/project-root'
|
|
5
5
|
import {
|
|
6
6
|
injectConfigs,
|
|
7
7
|
injectGitignore,
|
|
@@ -268,6 +268,13 @@ async function runSync(
|
|
|
268
268
|
): Promise<number> {
|
|
269
269
|
intro('canon tooling sync')
|
|
270
270
|
|
|
271
|
+
const mismatch = findCheckoutMismatch(process.cwd())
|
|
272
|
+
if (mismatch !== undefined) {
|
|
273
|
+
logWarn(
|
|
274
|
+
`Resolved via ${PROJECT_ROOT}, not the checkout at ${mismatch}. Run \`bun ${mismatch}/src/cli.ts tooling sync ...\` to sync against that checkout instead.`,
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
271
278
|
if (opts.check === true && opts.write === true) {
|
|
272
279
|
logWarn('Pass --check or --write, not both.')
|
|
273
280
|
outro()
|
|
@@ -98,14 +98,18 @@ function citationPattern(folder: string): RegExp {
|
|
|
98
98
|
)
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
const REWRITES: readonly {
|
|
101
|
+
interface Rewrite {
|
|
103
102
|
readonly pattern: RegExp
|
|
104
103
|
readonly folder: string
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** One rewrite per folder, paired with its citation pattern. */
|
|
107
|
+
function buildRewrites(folders: readonly string[]): readonly Rewrite[] {
|
|
108
|
+
return folders.map((folder) => ({
|
|
109
|
+
pattern: citationPattern(folder),
|
|
110
|
+
folder,
|
|
111
|
+
}))
|
|
112
|
+
}
|
|
109
113
|
|
|
110
114
|
/**
|
|
111
115
|
* Marks a line naming a promoted folder's old path on purpose, the same
|
|
@@ -125,40 +129,43 @@ function isKept(lines: readonly string[], index: number): boolean {
|
|
|
125
129
|
return above >= 0 && (lines[above]?.includes(KEEP_MARKER) ?? false)
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
function rewriteLine(line: string): string {
|
|
129
|
-
return
|
|
132
|
+
function rewriteLine(line: string, rewrites: readonly Rewrite[]): string {
|
|
133
|
+
return rewrites.reduce(
|
|
130
134
|
(current, { pattern, folder }) =>
|
|
131
135
|
current.replace(pattern, `.canon/review/evidence/${folder}`),
|
|
132
136
|
line,
|
|
133
137
|
)
|
|
134
138
|
}
|
|
135
139
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
* citation was spelled. A file naming no promoted folder returns
|
|
140
|
-
* byte-identical, and a marked line is returned unchanged.
|
|
141
|
-
*/
|
|
142
|
-
export function rewriteScratchEvidence(text: string): string {
|
|
143
|
-
const lines = text.split('\n')
|
|
144
|
-
return lines
|
|
145
|
-
.map((line, index) => (isKept(lines, index) ? line : rewriteLine(line)))
|
|
146
|
-
.join('\n')
|
|
140
|
+
interface RewriteOutcome {
|
|
141
|
+
readonly text: string
|
|
142
|
+
readonly count: number
|
|
147
143
|
}
|
|
148
144
|
|
|
149
|
-
/**
|
|
150
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Rewrites every unmarked citation of a folder `rewrites` covers into its
|
|
147
|
+
* destination under `.canon/review/evidence/`, absolute regardless of how the
|
|
148
|
+
* source citation was spelled, counting each as it goes. A file naming no
|
|
149
|
+
* such folder returns byte-identical with a count of zero, and a marked line
|
|
150
|
+
* is returned unchanged and uncounted.
|
|
151
|
+
*/
|
|
152
|
+
function applyRewrites(
|
|
153
|
+
text: string,
|
|
154
|
+
rewrites: readonly Rewrite[],
|
|
155
|
+
): RewriteOutcome {
|
|
151
156
|
const lines = text.split('\n')
|
|
152
157
|
let count = 0
|
|
153
158
|
|
|
154
|
-
|
|
155
|
-
if (isKept(lines, index))
|
|
156
|
-
|
|
159
|
+
const rewritten = lines.map((line, index) => {
|
|
160
|
+
if (isKept(lines, index)) return line
|
|
161
|
+
|
|
162
|
+
for (const { pattern } of rewrites) {
|
|
157
163
|
count += [...line.matchAll(pattern)].length
|
|
158
164
|
}
|
|
159
|
-
|
|
165
|
+
return rewriteLine(line, rewrites)
|
|
166
|
+
})
|
|
160
167
|
|
|
161
|
-
return count
|
|
168
|
+
return { text: rewritten.join('\n'), count }
|
|
162
169
|
}
|
|
163
170
|
|
|
164
171
|
/** The files under every `BACKED_FOLDERS` entry, archives included. */
|
|
@@ -261,17 +268,17 @@ export function planScratchEvidence(
|
|
|
261
268
|
sources: readonly ScratchEvidenceSource[],
|
|
262
269
|
): ScratchEvidencePlan {
|
|
263
270
|
const { moves, collisions } = planFolderMoves(root)
|
|
271
|
+
const folders = PROMOTED_FOLDERS.filter(
|
|
272
|
+
(folder) => !collisions.includes(destinationPath(root, folder)),
|
|
273
|
+
)
|
|
274
|
+
const rewrites = buildRewrites(folders)
|
|
264
275
|
const entries: CitationEntry[] = []
|
|
265
276
|
|
|
266
277
|
for (const source of sources) {
|
|
267
|
-
const
|
|
268
|
-
if (
|
|
269
|
-
|
|
270
|
-
entries.push({
|
|
271
|
-
path: source.path,
|
|
272
|
-
text: rewriteScratchEvidence(source.text),
|
|
273
|
-
rewritten,
|
|
274
|
-
})
|
|
278
|
+
const { text, count } = applyRewrites(source.text, rewrites)
|
|
279
|
+
if (count === 0) continue
|
|
280
|
+
|
|
281
|
+
entries.push({ path: source.path, text, rewritten: count })
|
|
275
282
|
}
|
|
276
283
|
|
|
277
284
|
return {
|
package/src/project-root.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
1
2
|
import { dirname, resolve } from 'node:path'
|
|
2
3
|
import { fileURLToPath } from 'node:url'
|
|
3
4
|
|
|
@@ -17,3 +18,36 @@ export const PROJECT_ROOT = resolve(
|
|
|
17
18
|
dirname(fileURLToPath(import.meta.url)),
|
|
18
19
|
'..',
|
|
19
20
|
)
|
|
21
|
+
|
|
22
|
+
const readPackageName = (root: string): string | undefined => {
|
|
23
|
+
try {
|
|
24
|
+
const raw = readFileSync(resolve(root, 'package.json'), 'utf8')
|
|
25
|
+
return (JSON.parse(raw) as { name?: string }).name
|
|
26
|
+
} catch {
|
|
27
|
+
return undefined
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Walks upward from `startDir` for the nearest ancestor `package.json`
|
|
33
|
+
* sharing this package's own `name`, and reports its path when that ancestor
|
|
34
|
+
* is not `PROJECT_ROOT`. A bare `canon` on PATH resolves `PROJECT_ROOT` to the
|
|
35
|
+
* installed package rather than to a checkout the caller may be standing in,
|
|
36
|
+
* which is the case this reports: a caller's cwd sitting inside a second
|
|
37
|
+
* checkout the running binary never resolved against.
|
|
38
|
+
*/
|
|
39
|
+
export function findCheckoutMismatch(startDir: string): string | undefined {
|
|
40
|
+
const ownName = readPackageName(PROJECT_ROOT)
|
|
41
|
+
if (ownName === undefined) return undefined
|
|
42
|
+
|
|
43
|
+
let dir = resolve(startDir)
|
|
44
|
+
while (true) {
|
|
45
|
+
if (readPackageName(dir) === ownName) {
|
|
46
|
+
return dir === PROJECT_ROOT ? undefined : dir
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const parent = dirname(dir)
|
|
50
|
+
if (parent === dir) return undefined
|
|
51
|
+
dir = parent
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -168,14 +168,17 @@ const DOCS_PATH = /(?<![\w./-])docs\/[^\s`)\]]*\.md\b/g
|
|
|
168
168
|
* lookbehind rejects, so a citation already rewritten to the resolving form
|
|
169
169
|
* does not fail this check a second time.
|
|
170
170
|
*
|
|
171
|
-
* Reported
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* `
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
171
|
+
* Reported unless `isStandardsPathReportable` reads the match as a
|
|
172
|
+
* deliberate placeholder rather than a real citation. A body illustrating
|
|
173
|
+
* the shape a project's own `standards/<name>.md` takes writes a bracketed
|
|
174
|
+
* token this pattern also matches, such as `standards/<slug>.md` in
|
|
175
|
+
* `create-standard/SKILL.md` or `standards/<name>.md` in
|
|
176
|
+
* `migration-standards-drop/SKILL.md`, and neither names a real file. A bare,
|
|
177
|
+
* non-bracketed match now reports whether or not the file exists, unlike
|
|
178
|
+
* `DOCS_PATH`, which still gates on existence: a same-repository citation
|
|
179
|
+
* under this corpus names no target project's own tree the way a `docs/`
|
|
180
|
+
* path can, so shape alone separates a placeholder from a broken citation
|
|
181
|
+
* here with no false-positive class existence was catching.
|
|
179
182
|
*/
|
|
180
183
|
const STANDARDS_PATH = /(?<![\w./-])standards\/[^\s`)\]]*\.md\b/g
|
|
181
184
|
|
|
@@ -221,20 +224,37 @@ export interface ShippedReference {
|
|
|
221
224
|
}
|
|
222
225
|
|
|
223
226
|
/**
|
|
224
|
-
* Whether a
|
|
227
|
+
* Whether a matched path is a deliberate placeholder rather than a real
|
|
228
|
+
* citation, true when the text carries both `<` and `>`. A real path never
|
|
229
|
+
* carries either character, so the two classes cannot collide.
|
|
230
|
+
*/
|
|
231
|
+
function isPlaceholderPath(path: string): boolean {
|
|
232
|
+
return path.includes('<') && path.includes('>')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Whether a `DOCS_PATH` match should be reported.
|
|
225
237
|
*
|
|
226
238
|
* `file` gates the `docs/` corpus out by the caller's own location rather
|
|
227
239
|
* than by a pattern exemption: a citation from inside `docs/` is read
|
|
228
240
|
* together with the rest of that corpus through the same `canon docs`
|
|
229
241
|
* resolution, which is a weaker claim than one from a skill body with no
|
|
230
242
|
* `docs/` sibling at all.
|
|
243
|
+
*
|
|
244
|
+
* The placeholder check runs ahead of `existsSync` rather than replacing it.
|
|
245
|
+
* Shape alone cannot separate a genuinely broken same-repository citation
|
|
246
|
+
* from a legitimate illustration of a target project's own tree, such as
|
|
247
|
+
* `docs/retry.md`, and no signal in the text draws that line, so existence
|
|
248
|
+
* still does the work of muting those. The placeholder check only catches
|
|
249
|
+
* the bracketed illustrations existence alone would report as broken.
|
|
231
250
|
*/
|
|
232
|
-
function
|
|
251
|
+
function isDocsPathReportable(
|
|
233
252
|
file: string,
|
|
234
253
|
path: string,
|
|
235
254
|
root: string,
|
|
236
255
|
): boolean {
|
|
237
256
|
if (file.startsWith('docs/')) return false
|
|
257
|
+
if (isPlaceholderPath(path)) return false
|
|
238
258
|
return existsSync(join(root, path))
|
|
239
259
|
}
|
|
240
260
|
|
|
@@ -250,9 +270,19 @@ function isStandardsPathScope(file: string): boolean {
|
|
|
250
270
|
return file.startsWith('claude/skills/') && !file.endsWith('/REQUIREMENT.md')
|
|
251
271
|
}
|
|
252
272
|
|
|
253
|
-
/**
|
|
254
|
-
|
|
255
|
-
|
|
273
|
+
/**
|
|
274
|
+
* Whether a `STANDARDS_PATH` match should be reported: every match that is
|
|
275
|
+
* not a placeholder, whether or not the named file exists.
|
|
276
|
+
*
|
|
277
|
+
* Existence used to gate this the same way `isDocsPathReportable` gates its
|
|
278
|
+
* corpus, which conflated a deliberate placeholder with a genuinely broken,
|
|
279
|
+
* real-looking citation: both fail existence and both passed muted. A bare
|
|
280
|
+
* `standards/<name>.md` names no target-project tree the way a `docs/` path
|
|
281
|
+
* can, so shape alone separates the two classes here with no false-positive
|
|
282
|
+
* class existence was catching.
|
|
283
|
+
*/
|
|
284
|
+
function isStandardsPathReportable(path: string): boolean {
|
|
285
|
+
return !isPlaceholderPath(path)
|
|
256
286
|
}
|
|
257
287
|
|
|
258
288
|
/**
|
|
@@ -318,7 +348,7 @@ export function referencesIn(
|
|
|
318
348
|
}
|
|
319
349
|
|
|
320
350
|
for (const match of line.matchAll(DOCS_PATH)) {
|
|
321
|
-
if (!
|
|
351
|
+
if (!isDocsPathReportable(file, match[0], root)) continue
|
|
322
352
|
references.push({
|
|
323
353
|
file,
|
|
324
354
|
line: index + 1,
|
|
@@ -329,7 +359,7 @@ export function referencesIn(
|
|
|
329
359
|
|
|
330
360
|
if (isStandardsPathScope(file)) {
|
|
331
361
|
for (const match of line.matchAll(STANDARDS_PATH)) {
|
|
332
|
-
if (!
|
|
362
|
+
if (!isStandardsPathReportable(match[0])) continue
|
|
333
363
|
references.push({
|
|
334
364
|
file,
|
|
335
365
|
line: index + 1,
|
|
@@ -26,7 +26,7 @@ export default defineConfig([
|
|
|
26
26
|
...tseslint.configs.recommended,
|
|
27
27
|
...astro.configs.recommended,
|
|
28
28
|
{
|
|
29
|
-
files: ['**/*.{ts,tsx,js,jsx}'],
|
|
29
|
+
files: ['**/*.{ts,tsx,js,jsx,astro}'],
|
|
30
30
|
plugins: {
|
|
31
31
|
'simple-import-sort': simpleImportSort,
|
|
32
32
|
'check-file': checkFile,
|
|
@@ -21,7 +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
|
-
- `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). `
|
|
24
|
+
- `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`'s `src/**/!(__tests__|pages)` pattern reaches no folder under a real ESLint run, `.astro`-only or otherwise. See `.claude/context/tooling.md` for the measurement.
|
|
25
25
|
|
|
26
26
|
## Typecheck
|
|
27
27
|
|