@erclx/canon 5.25.0 → 5.26.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 +2 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canvas/SKILL.md +1 -1
- package/claude/skills/context-fold/REQUIREMENT.md +6 -2
- package/claude/skills/context-fold/SKILL.md +8 -6
- package/claude/skills/context-fold/references/anchor-sweep.md +2 -2
- package/claude/skills/design-taste/REQUIREMENT.md +1 -1
- package/claude/skills/design-taste/SKILL.md +4 -2
- package/claude/skills/design-taste/references/adopted.md +40 -0
- package/claude/skills/design-taste/references/kinds.md +1 -1
- package/claude/skills/design-taste/references/systems.md +1 -7
- package/claude/skills/design-taste/references/tells.md +2 -24
- package/claude/skills/docs-sync/REQUIREMENT.md +2 -1
- package/claude/skills/document-health/REQUIREMENT.md +2 -0
- package/claude/skills/document-health/SKILL.md +21 -0
- package/claude/skills/document-health/references/axes.md +12 -0
- package/claude/skills/draft-and-pick/SKILL.md +1 -1
- package/claude/skills/draft-identity/SKILL.md +1 -1
- package/claude/skills/draft-screencast/SKILL.md +9 -7
- package/claude/skills/draft-slides/REQUIREMENT.md +17 -12
- package/claude/skills/draft-slides/SKILL.md +31 -20
- package/claude/skills/git-issue/references/issue.md +10 -0
- package/claude/skills/git-pr/references/evidence.md +8 -0
- package/claude/skills/git-pr/references/labels.md +12 -0
- package/claude/skills/git-split/SKILL.md +19 -8
- package/claude/skills/markdown-propose/references/format.md +9 -0
- package/claude/skills/memory-review/references/later-phases.md +7 -0
- package/claude/skills/review-ui/references/post.md +4 -2
- package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +8 -0
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +13 -0
- package/claude/skills/search-craft/REQUIREMENT.md +1 -1
- package/claude/skills/search-craft/SKILL.md +3 -1
- package/claude/skills/search-craft/references/adopted.md +30 -0
- package/claude/skills/target-setup/references/detect.md +8 -0
- package/claude/skills/target-setup/references/indexes.md +13 -0
- package/claude/skills/target-setup/references/verify.md +8 -0
- package/claude/skills/test-first/SKILL.md +2 -0
- package/claude/skills/test-first/references/adopted.md +36 -0
- package/claude/skills/write-human/SKILL.md +1 -1
- package/claude/skills/write-human/references/machine-tells.md +13 -0
- package/docs/agents/canvas.md +3 -1
- package/docs/agents/capture.md +1 -1
- package/docs/agents/commands.md +3 -3
- package/docs/agents/index.md +2 -2
- package/docs/agents/records-stale.md +22 -9
- package/docs/agents/records.md +1 -1
- package/docs/agents/sandbox.md +1 -1
- package/docs/agents/skills-audit.md +10 -3
- package/docs/agents/slides.md +15 -3
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/index.md +1 -0
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map-on-request.md +42 -0
- package/docs/workflow/skill-map.md +1 -34
- package/package.json +1 -1
- package/scripts/lib/sandbox-dispatch.sh +1 -1
- package/scripts/lib/sandbox-git.sh +1 -9
- package/scripts/lib/sandbox-path.sh +1 -1
- package/scripts/sandbox-hook.sh +99 -0
- package/src/canvas/capture.ts +1 -9
- package/src/canvas/client/inspector/color-field.tsx +368 -0
- package/src/canvas/client/inspector/fields.css +163 -22
- package/src/canvas/client/inspector/values.ts +123 -0
- package/src/canvas/client/inspector.tsx +46 -69
- package/src/capture/render.ts +45 -7
- package/src/capture/sources.ts +43 -0
- package/src/census/count.ts +1 -1
- package/src/claude/cases/workflow.ts +5 -0
- package/src/claude/seeds.ts +1 -1
- package/src/claude/skills-audit.ts +68 -14
- package/src/client-commands.ts +1 -1
- package/src/commands/claude.ts +27 -2
- package/src/commands/feedback.ts +3 -3
- package/src/commands/records.ts +88 -6
- package/src/commands/sandbox.ts +50 -10
- package/src/commands/slides.ts +31 -77
- package/src/context/audit.ts +1 -1
- package/src/design/tokens.ts +2 -18
- package/src/docs/read.ts +1 -1
- package/src/{github.ts → feedback/github.ts} +1 -1
- package/src/gate/measures.ts +6 -4
- package/src/gov/citations.ts +1 -1
- package/src/gov/payload.ts +1 -1
- package/src/gov/superseded.ts +2 -2
- package/src/markdown/ceiling.ts +1 -1
- package/src/record-root.ts +1 -0
- package/src/records/backup.ts +1 -0
- package/src/records/push-guard.ts +1 -1
- package/src/records/stale-canonical.ts +146 -0
- package/src/records/stale.ts +1 -1
- package/src/sandbox/hooks.ts +128 -0
- package/src/sandbox/provision.ts +596 -0
- package/src/sandbox/tree.ts +103 -4
- package/src/secrets/marker.ts +2 -2
- package/src/secrets/scan.ts +1 -1
- package/src/shipped/references.ts +2 -2
- package/src/slides/convert/export.ts +2 -16
- package/src/slides/decks.ts +110 -0
- package/src/standards/read.ts +1 -1
- package/src/sync/seeds-report.ts +1 -1
- package/src/tasks/answers.ts +1 -1
- package/src/tasks/archive.ts +1 -1
- package/src/teach/nav.ts +1 -1
- package/src/tooling/inject.ts +1 -1
- package/src/web/readme-citations.ts +1 -1
- package/standards/architecture.md +1 -0
- package/standards/canvas.md +1 -1
- package/standards/requirements.md +2 -0
- package/standards/skill-practice.md +2 -1
- package/standards/skill.md +1 -0
- package/scripts/manage-sandbox.sh +0 -472
- package/src/slides/layouts.ts +0 -478
- package/src/slides/parse.ts +0 -84
- package/src/slides/render.ts +0 -110
- package/src/slides/styles.ts +0 -96
- /package/claude/skills/write-human/references/{source-material.md → adopted.md} +0 -0
- /package/src/{commands/feedback-format.ts → feedback/body.ts} +0 -0
- /package/src/{github-format.ts → feedback/github-format.ts} +0 -0
- /package/src/{frontmatter.ts → markdown/frontmatter.ts} +0 -0
- /package/src/{binary.ts → sweep/binary.ts} +0 -0
- /package/src/{exempt-marker.ts → sweep/exempt-marker.ts} +0 -0
- /package/src/{paths.ts → tasks/paths.ts} +0 -0
- /package/src/{seed-marker.ts → tooling/seed-marker.ts} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** @jsxImportSource preact */
|
|
2
2
|
import type { JSX } from 'preact'
|
|
3
3
|
import { addressOf, elementAt, excerpt, isRawText } from '@/canvas/address'
|
|
4
|
+
import { ColorField } from '@/canvas/client/inspector/color-field'
|
|
4
5
|
import {
|
|
5
6
|
Field,
|
|
6
7
|
ReadOnlyField,
|
|
@@ -10,6 +11,7 @@ import { Section } from '@/canvas/client/inspector/section'
|
|
|
10
11
|
import {
|
|
11
12
|
clampScrub,
|
|
12
13
|
displayValue,
|
|
14
|
+
parseColor,
|
|
13
15
|
toCssValue,
|
|
14
16
|
} from '@/canvas/client/inspector/values'
|
|
15
17
|
import {
|
|
@@ -75,15 +77,10 @@ const TYPE_FIELDS: readonly StyleField[] = [
|
|
|
75
77
|
{ label: 'weight', glyph: 'Wt', property: 'font-weight' },
|
|
76
78
|
]
|
|
77
79
|
|
|
78
|
-
const FILL_FIELDS
|
|
79
|
-
{ label: 'color',
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
glyph: 'Bg',
|
|
83
|
-
property: 'background-color',
|
|
84
|
-
placeholder: 'None',
|
|
85
|
-
},
|
|
86
|
-
]
|
|
80
|
+
const FILL_FIELDS = [
|
|
81
|
+
{ label: 'color', property: 'color' },
|
|
82
|
+
{ label: 'background', property: 'background-color' },
|
|
83
|
+
] as const
|
|
87
84
|
|
|
88
85
|
function inlineValue(element: Element, property: string): string {
|
|
89
86
|
return 'style' in element
|
|
@@ -127,8 +124,10 @@ function isRawInline(element: Element, property: string): boolean {
|
|
|
127
124
|
* an edit starts from what the operator sees.
|
|
128
125
|
*/
|
|
129
126
|
function currentValue(element: Element, property: string): string {
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
return inlineValue(element, property) || computedValue(element, property)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function computedValue(element: Element, property: string): string {
|
|
132
131
|
return (
|
|
133
132
|
element.ownerDocument.defaultView
|
|
134
133
|
?.getComputedStyle(element)
|
|
@@ -136,6 +135,19 @@ function currentValue(element: Element, property: string): string {
|
|
|
136
135
|
)
|
|
137
136
|
}
|
|
138
137
|
|
|
138
|
+
/**
|
|
139
|
+
* The color a token paints in the frame, read off a probe rather than the
|
|
140
|
+
* token's text, since a token's value may itself be a `var()` or a mix.
|
|
141
|
+
*/
|
|
142
|
+
function tokenPaint(doc: Document, name: string): string {
|
|
143
|
+
const probe = doc.createElement('span')
|
|
144
|
+
probe.style.setProperty('color', `var(${name})`)
|
|
145
|
+
;(doc.body ?? doc.documentElement).append(probe)
|
|
146
|
+
const painted = computedValue(probe, 'color')
|
|
147
|
+
probe.remove()
|
|
148
|
+
return painted
|
|
149
|
+
}
|
|
150
|
+
|
|
139
151
|
function holdsTextAlone(element: Element): boolean {
|
|
140
152
|
const tag = element.tagName.toLowerCase()
|
|
141
153
|
return element.children.length === 0 && !VOID.has(tag) && !isRawText(tag)
|
|
@@ -146,46 +158,6 @@ function elementName(element: Element): string {
|
|
|
146
158
|
return [tag, ...element.classList].join('.')
|
|
147
159
|
}
|
|
148
160
|
|
|
149
|
-
interface TokenPickerProps {
|
|
150
|
-
readonly label: string
|
|
151
|
-
readonly initial: string
|
|
152
|
-
readonly isBusy: boolean
|
|
153
|
-
readonly onCommit: (value: string) => void
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function TokenPicker({
|
|
157
|
-
label,
|
|
158
|
-
initial,
|
|
159
|
-
isBusy,
|
|
160
|
-
onCommit,
|
|
161
|
-
}: TokenPickerProps): JSX.Element | null {
|
|
162
|
-
const colors =
|
|
163
|
-
tokens.value?.groups?.find((group) => group.kind === 'color')?.tokens ?? []
|
|
164
|
-
if (colors.length === 0) return null
|
|
165
|
-
return (
|
|
166
|
-
<select
|
|
167
|
-
class="token-picker"
|
|
168
|
-
aria-label={`${label} token`}
|
|
169
|
-
title="Pick a project token"
|
|
170
|
-
value={
|
|
171
|
-
colors.some((token) => initial === `var(${token.name})`) ? initial : ''
|
|
172
|
-
}
|
|
173
|
-
disabled={isBusy}
|
|
174
|
-
onChange={(event) => {
|
|
175
|
-
const { value } = event.currentTarget
|
|
176
|
-
if (value !== '' && value !== initial) onCommit(value)
|
|
177
|
-
}}
|
|
178
|
-
>
|
|
179
|
-
<option value="">Token</option>
|
|
180
|
-
{colors.map((token) => (
|
|
181
|
-
<option key={token.name} value={`var(${token.name})`}>
|
|
182
|
-
{token.name}
|
|
183
|
-
</option>
|
|
184
|
-
))}
|
|
185
|
-
</select>
|
|
186
|
-
)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
161
|
function ElementFields({
|
|
190
162
|
node,
|
|
191
163
|
doc,
|
|
@@ -206,6 +178,10 @@ function ElementFields({
|
|
|
206
178
|
const rect = node.getBoundingClientRect()
|
|
207
179
|
const font = currentValue(node, 'font-family')
|
|
208
180
|
const text = node.textContent ?? ''
|
|
181
|
+
const colorTokens =
|
|
182
|
+
tokens.value?.groups
|
|
183
|
+
?.find((group) => group.kind === 'color')
|
|
184
|
+
?.tokens.map((token) => token.name) ?? []
|
|
209
185
|
|
|
210
186
|
/**
|
|
211
187
|
* Previews into the frame's own inline style and writes through the same
|
|
@@ -272,25 +248,26 @@ function ElementFields({
|
|
|
272
248
|
</Section>
|
|
273
249
|
<Section title="Fill">
|
|
274
250
|
{FILL_FIELDS.map((field) => {
|
|
275
|
-
const
|
|
251
|
+
const painted = computedValue(node, field.property)
|
|
252
|
+
const inline = inlineValue(node, field.property)
|
|
253
|
+
/*
|
|
254
|
+
* An inline value the picker cannot read stays as written. Reading
|
|
255
|
+
* the computed color in its place would show a token-following
|
|
256
|
+
* value as a literal, and an opacity edit would freeze it.
|
|
257
|
+
*/
|
|
276
258
|
return (
|
|
277
|
-
<
|
|
278
|
-
{
|
|
279
|
-
{
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
initial={initial}
|
|
290
|
-
isBusy={isBusy}
|
|
291
|
-
onCommit={commit(field.property)}
|
|
292
|
-
/>
|
|
293
|
-
</div>
|
|
259
|
+
<ColorField
|
|
260
|
+
key={field.property}
|
|
261
|
+
label={field.label}
|
|
262
|
+
color={parseColor(inline || painted)}
|
|
263
|
+
text={inline || painted}
|
|
264
|
+
painted={painted}
|
|
265
|
+
tokens={colorTokens}
|
|
266
|
+
paint={(name) => tokenPaint(doc, name)}
|
|
267
|
+
isRaw={isRawInline(node, field.property)}
|
|
268
|
+
isBusy={isBusy}
|
|
269
|
+
onCommit={commit(field.property)}
|
|
270
|
+
/>
|
|
294
271
|
)
|
|
295
272
|
})}
|
|
296
273
|
</Section>
|
package/src/capture/render.ts
CHANGED
|
@@ -2,9 +2,13 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
|
2
2
|
import { basename, dirname } from 'node:path'
|
|
3
3
|
import { pathToFileURL } from 'node:url'
|
|
4
4
|
import { chromium } from 'playwright-core'
|
|
5
|
-
import type { Browser, Page } from 'playwright-core'
|
|
5
|
+
import type { Browser, Locator, Page } from 'playwright-core'
|
|
6
6
|
import type { CaptureSource } from '@/capture/sources'
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
isGenericFamily,
|
|
9
|
+
resolveCaptureSources,
|
|
10
|
+
textFamilies,
|
|
11
|
+
} from '@/capture/sources'
|
|
8
12
|
import { formatStamp, hashSource, stampPath } from '@/capture/stamp'
|
|
9
13
|
|
|
10
14
|
/**
|
|
@@ -97,15 +101,22 @@ async function captureOne(
|
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
await page.evaluate(() => document.fonts.ready.then(() => undefined))
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
)
|
|
103
|
-
if (family && !(await resolvesFont(page, family))) {
|
|
104
|
+
const declarations = await renderedTextFamilies(element)
|
|
105
|
+
const generic = declarations.find(isGenericFamily)
|
|
106
|
+
if (generic !== undefined) {
|
|
104
107
|
return failed(
|
|
105
108
|
source,
|
|
106
|
-
`${
|
|
109
|
+
`${generic.split(',')[0]?.trim()} is a generic family that resolves differently per machine, so name a real font`,
|
|
107
110
|
)
|
|
108
111
|
}
|
|
112
|
+
for (const family of textFamilies(declarations)) {
|
|
113
|
+
if (!(await resolvesFont(page, family))) {
|
|
114
|
+
return failed(
|
|
115
|
+
source,
|
|
116
|
+
`${family} is not installed, so the capture would rewrap against a fallback`,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
109
120
|
|
|
110
121
|
mkdirSync(dirname(source.pngPath), { recursive: true })
|
|
111
122
|
const png = await element.screenshot({ omitBackground: true })
|
|
@@ -170,6 +181,33 @@ function failed(source: CaptureSource, reason: string): CaptureResult {
|
|
|
170
181
|
return { status: 'failed', source: sourceIdentifier(source), reason }
|
|
171
182
|
}
|
|
172
183
|
|
|
184
|
+
/**
|
|
185
|
+
* The computed families of the captured element and of each element under it
|
|
186
|
+
* holding a non-blank text node of its own that draws a box, which is the text
|
|
187
|
+
* the screenshot shows. Testing the text node's box rather than the element's
|
|
188
|
+
* keeps out `<style>`, `<script>`, `<title>`, and anything hidden, whose
|
|
189
|
+
* computed family is the browser default and would refuse every source that
|
|
190
|
+
* styles a descendant rather than the root, and keeps in text directly inside a
|
|
191
|
+
* `display: contents` element, which draws no box of its own.
|
|
192
|
+
*
|
|
193
|
+
* Returns nothing when no element holds text, since a font cannot rewrap text
|
|
194
|
+
* that is not there, so a container of iframes skips the probe.
|
|
195
|
+
*/
|
|
196
|
+
async function renderedTextFamilies(element: Locator): Promise<string[]> {
|
|
197
|
+
return element.evaluate((root) => {
|
|
198
|
+
const range = document.createRange()
|
|
199
|
+
const holdsText = (node: Element): boolean =>
|
|
200
|
+
[...node.childNodes].some((child) => {
|
|
201
|
+
if (child.nodeType !== Node.TEXT_NODE) return false
|
|
202
|
+
if ((child.textContent ?? '').trim() === '') return false
|
|
203
|
+
range.selectNodeContents(child)
|
|
204
|
+
return range.getClientRects().length > 0
|
|
205
|
+
})
|
|
206
|
+
const texts = [root, ...root.querySelectorAll('*')].filter(holdsText)
|
|
207
|
+
return texts.map((node) => getComputedStyle(node).fontFamily)
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
173
211
|
/**
|
|
174
212
|
* Compares the family's text metrics against a family that cannot exist. Equal
|
|
175
213
|
* widths mean the browser fell through to its default for both, which is the
|
package/src/capture/sources.ts
CHANGED
|
@@ -66,3 +66,46 @@ export function primaryFontFamily(declaration: string): string {
|
|
|
66
66
|
const first = declaration.split(',')[0]?.trim() ?? ''
|
|
67
67
|
return first.replace(/^['"]|['"]$/g, '')
|
|
68
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The CSS generic family keywords. A generic names no font, so each machine
|
|
72
|
+
* maps it to whatever it has, and a capture measured against one cannot
|
|
73
|
+
* reproduce on another.
|
|
74
|
+
*/
|
|
75
|
+
export const GENERIC_FAMILIES: ReadonlySet<string> = new Set([
|
|
76
|
+
'serif',
|
|
77
|
+
'sans-serif',
|
|
78
|
+
'monospace',
|
|
79
|
+
'cursive',
|
|
80
|
+
'fantasy',
|
|
81
|
+
'system-ui',
|
|
82
|
+
'ui-serif',
|
|
83
|
+
'ui-sans-serif',
|
|
84
|
+
'ui-monospace',
|
|
85
|
+
'ui-rounded',
|
|
86
|
+
'math',
|
|
87
|
+
'emoji',
|
|
88
|
+
'fangsong',
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether the first family of a computed `font-family` is a generic keyword.
|
|
93
|
+
* Takes the raw declaration rather than `primaryFontFamily`'s result, since
|
|
94
|
+
* quoting is what separates the keyword from a family that happens to share its
|
|
95
|
+
* name, and the stripped form has lost it. Keywords match case-insensitively.
|
|
96
|
+
*/
|
|
97
|
+
export function isGenericFamily(declaration: string): boolean {
|
|
98
|
+
const first = declaration.split(',')[0]?.trim() ?? ''
|
|
99
|
+
return GENERIC_FAMILIES.has(first.toLowerCase())
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Reduces the computed `font-family` of each rendered text element to the
|
|
104
|
+
* distinct families a capture has to prove resolved, in document order, so a
|
|
105
|
+
* frame that sets its font on any ancestor of its text is checked against the
|
|
106
|
+
* face the text actually draws in.
|
|
107
|
+
*/
|
|
108
|
+
export function textFamilies(declarations: readonly string[]): string[] {
|
|
109
|
+
const families = declarations.map(primaryFontFamily).filter(Boolean)
|
|
110
|
+
return [...new Set(families)]
|
|
111
|
+
}
|
package/src/census/count.ts
CHANGED
|
@@ -146,6 +146,11 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
146
146
|
'Which of our docs have gone stale, grown too long, or ended up in the wrong file?',
|
|
147
147
|
expect: 'document-health',
|
|
148
148
|
},
|
|
149
|
+
{
|
|
150
|
+
prompt:
|
|
151
|
+
'Are our requirements and architecture docs still true, and when were they last reviewed?',
|
|
152
|
+
expect: 'document-health',
|
|
153
|
+
},
|
|
149
154
|
{
|
|
150
155
|
prompt: 'Open a new entry on the task board for this piece of work.',
|
|
151
156
|
expect: 'task-board',
|
package/src/claude/seeds.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
|
3
3
|
import { dirname, join, relative } from 'node:path'
|
|
4
4
|
import { copyPreservingMode } from '@/copy'
|
|
5
5
|
import { creationRel, isRecordEntry } from '@/record-root'
|
|
6
|
-
import { rewritesOnInstall, stripSeedMarker } from '@/seed-marker'
|
|
6
|
+
import { rewritesOnInstall, stripSeedMarker } from '@/tooling/seed-marker'
|
|
7
7
|
import { SURFACE_ENTRIES, surfaceDir } from '@/surface-root'
|
|
8
8
|
|
|
9
9
|
const SEEDS_DIR = join('tooling', 'claude', 'seeds')
|
|
@@ -40,6 +40,21 @@ export const PRACTICE_SKILLS: readonly string[] = [
|
|
|
40
40
|
join(SHIPPED_CORPUS, 'code-craft'),
|
|
41
41
|
join(SHIPPED_CORPUS, 'build-in-slices'),
|
|
42
42
|
join(SHIPPED_CORPUS, 'deprecation-migration'),
|
|
43
|
+
join(SHIPPED_CORPUS, 'search-craft'),
|
|
44
|
+
join(SHIPPED_CORPUS, 'video-craft'),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Every skill carrying the source ledger, being the practice skills and the
|
|
49
|
+
* skills that teach a craft without the practice closing sections. Kept apart
|
|
50
|
+
* from `PRACTICE_SKILLS` so citing sources never forces those sections onto a
|
|
51
|
+
* skill whose body is not shaped around a practice a session cuts short.
|
|
52
|
+
*/
|
|
53
|
+
export const LEDGER_SKILLS: readonly string[] = [
|
|
54
|
+
...PRACTICE_SKILLS,
|
|
55
|
+
join(SHIPPED_CORPUS, 'design-taste'),
|
|
56
|
+
join(SHIPPED_CORPUS, 'write-human'),
|
|
57
|
+
join(SHIPPED_CORPUS, 'test-first'),
|
|
43
58
|
]
|
|
44
59
|
|
|
45
60
|
/** The closing H2s a practice skill carries, matched exactly outside fences. */
|
|
@@ -52,6 +67,16 @@ export const PRACTICE_SECTIONS: readonly string[] = [
|
|
|
52
67
|
/** The source ledger every practice skill carries beside its body. */
|
|
53
68
|
export const PRACTICE_LEDGER = join('references', 'adopted.md')
|
|
54
69
|
|
|
70
|
+
/**
|
|
71
|
+
* The line count past which a reference opens with a contents list, from
|
|
72
|
+
* Anthropic's skill authoring guidance. Counted over the whole file the way
|
|
73
|
+
* `wc -l` counts it, so "over 100" holds at 101 and not at 100.
|
|
74
|
+
*/
|
|
75
|
+
export const CONTENTS_THRESHOLD = 100
|
|
76
|
+
|
|
77
|
+
/** The H2 a long reference opens with, matched exactly outside fences. */
|
|
78
|
+
export const CONTENTS_SECTION = 'Contents'
|
|
79
|
+
|
|
55
80
|
/**
|
|
56
81
|
* The one reason this audit refuses. A project carrying neither corpus is the
|
|
57
82
|
* ordinary state of a target that has not adopted either skills convention,
|
|
@@ -102,6 +127,7 @@ export interface SkillsAudit {
|
|
|
102
127
|
readonly requirementSections: readonly SkillFinding[]
|
|
103
128
|
readonly datedProvenance: readonly SkillFinding[]
|
|
104
129
|
readonly practiceShape: readonly SkillFinding[]
|
|
130
|
+
readonly referenceContents: readonly SkillFinding[]
|
|
105
131
|
}
|
|
106
132
|
|
|
107
133
|
interface SkillSource {
|
|
@@ -126,6 +152,7 @@ interface SkillSource {
|
|
|
126
152
|
interface SkillText {
|
|
127
153
|
readonly rel: string
|
|
128
154
|
readonly text: string
|
|
155
|
+
readonly isReference: boolean
|
|
129
156
|
}
|
|
130
157
|
|
|
131
158
|
/**
|
|
@@ -177,6 +204,9 @@ export async function auditSkills(root: string): Promise<SkillsAudit> {
|
|
|
177
204
|
),
|
|
178
205
|
...sources.flatMap(practiceFindings),
|
|
179
206
|
],
|
|
207
|
+
referenceContents: sources.flatMap((source) =>
|
|
208
|
+
source.bodies.flatMap(contentsFindings),
|
|
209
|
+
),
|
|
180
210
|
}
|
|
181
211
|
}
|
|
182
212
|
|
|
@@ -233,16 +263,14 @@ async function readSkill(
|
|
|
233
263
|
description: declared(readField(fields, 'description')),
|
|
234
264
|
requirementHeadings:
|
|
235
265
|
requirement === undefined ? undefined : headings(requirement),
|
|
236
|
-
sections:
|
|
237
|
-
.filter((line) => !line.fenced)
|
|
238
|
-
.map((line) => SECTION.exec(line.text)?.[1])
|
|
239
|
-
.filter((text): text is string => text !== undefined),
|
|
266
|
+
sections: sectionsOf(body),
|
|
240
267
|
hasLedger: existsSync(join(skillDir, PRACTICE_LEDGER)),
|
|
241
268
|
bodies: [
|
|
242
|
-
{ rel: join(skillRel, 'SKILL.md'), text: body },
|
|
269
|
+
{ rel: join(skillRel, 'SKILL.md'), text: body, isReference: false },
|
|
243
270
|
...references.map((path, index) => ({
|
|
244
271
|
rel: join(skillRel, path),
|
|
245
272
|
text: referenceTexts[index] ?? '',
|
|
273
|
+
isReference: true,
|
|
246
274
|
})),
|
|
247
275
|
],
|
|
248
276
|
}
|
|
@@ -257,6 +285,14 @@ function declared(value: string | undefined): string | undefined {
|
|
|
257
285
|
return trimmed === undefined || trimmed === '' ? undefined : trimmed
|
|
258
286
|
}
|
|
259
287
|
|
|
288
|
+
/** Every H2 outside a fence, the one level both exact-section checks read. */
|
|
289
|
+
function sectionsOf(source: string): string[] {
|
|
290
|
+
return bodyLines(source)
|
|
291
|
+
.filter((line) => !line.fenced)
|
|
292
|
+
.map((line) => SECTION.exec(line.text)?.[1])
|
|
293
|
+
.filter((text): text is string => text !== undefined)
|
|
294
|
+
}
|
|
295
|
+
|
|
260
296
|
function headings(source: string): string[] {
|
|
261
297
|
return source
|
|
262
298
|
.split('\n')
|
|
@@ -311,7 +347,7 @@ function missingPracticeSkills(
|
|
|
311
347
|
): SkillFinding[] {
|
|
312
348
|
if (!hasShippedCorpus) return []
|
|
313
349
|
const found = new Set(sources.map((source) => source.rel))
|
|
314
|
-
return
|
|
350
|
+
return LEDGER_SKILLS.filter((rel) => !found.has(rel)).map((rel) => ({
|
|
315
351
|
rel,
|
|
316
352
|
detail: 'missing skill: no folder under the shipped corpus',
|
|
317
353
|
}))
|
|
@@ -323,14 +359,15 @@ function missingPracticeSkills(
|
|
|
323
359
|
* standard never names.
|
|
324
360
|
*/
|
|
325
361
|
function practiceFindings(source: SkillSource): SkillFinding[] {
|
|
326
|
-
if (!
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
362
|
+
if (!LEDGER_SKILLS.includes(source.rel)) return []
|
|
363
|
+
|
|
364
|
+
const isPractice = PRACTICE_SKILLS.includes(source.rel)
|
|
365
|
+
const sections = (isPractice ? PRACTICE_SECTIONS : [])
|
|
366
|
+
.filter((section) => !source.sections.includes(section))
|
|
367
|
+
.map((section) => ({
|
|
368
|
+
rel: source.rel,
|
|
369
|
+
detail: `missing section: ${section}`,
|
|
370
|
+
}))
|
|
334
371
|
const ledger = source.hasLedger
|
|
335
372
|
? []
|
|
336
373
|
: [{ rel: source.rel, detail: `missing ledger: ${PRACTICE_LEDGER}` }]
|
|
@@ -338,6 +375,23 @@ function practiceFindings(source: SkillSource): SkillFinding[] {
|
|
|
338
375
|
return [...sections, ...ledger]
|
|
339
376
|
}
|
|
340
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Reads references alone, since `SKILL.md` carries its own length checkpoint
|
|
380
|
+
* and no contents rule. Frontmatter counts toward the length, so the count a
|
|
381
|
+
* finding names is the one `wc -l` prints beside it.
|
|
382
|
+
*/
|
|
383
|
+
function contentsFindings(source: SkillText): SkillFinding[] {
|
|
384
|
+
if (!source.isReference) return []
|
|
385
|
+
|
|
386
|
+
const lines = source.text.replace(/\n$/, '').split('\n').length
|
|
387
|
+
if (lines <= CONTENTS_THRESHOLD) return []
|
|
388
|
+
if (sectionsOf(source.text).includes(CONTENTS_SECTION)) return []
|
|
389
|
+
|
|
390
|
+
return [
|
|
391
|
+
{ rel: source.rel, detail: `${lines} lines, no ## ${CONTENTS_SECTION}` },
|
|
392
|
+
]
|
|
393
|
+
}
|
|
394
|
+
|
|
341
395
|
/**
|
|
342
396
|
* A date inside a fence or a code span is example data a reader is shown, such
|
|
343
397
|
* as a sample frontmatter block, so only prose outside both is read.
|
package/src/client-commands.ts
CHANGED
package/src/commands/claude.ts
CHANGED
|
@@ -24,8 +24,11 @@ import { listSeeds, readSeedContents } from '@/claude/seeds-list'
|
|
|
24
24
|
import {
|
|
25
25
|
auditExitCode,
|
|
26
26
|
auditSkills,
|
|
27
|
+
CONTENTS_SECTION,
|
|
28
|
+
CONTENTS_THRESHOLD,
|
|
27
29
|
CORPORA,
|
|
28
30
|
DESCRIPTION_LIMIT,
|
|
31
|
+
LEDGER_SKILLS,
|
|
29
32
|
PRACTICE_LEDGER,
|
|
30
33
|
PRACTICE_SECTIONS,
|
|
31
34
|
PRACTICE_SKILLS,
|
|
@@ -1398,6 +1401,7 @@ async function runSkillsAudit(
|
|
|
1398
1401
|
reportRequirementShape(report)
|
|
1399
1402
|
reportProvenance(report)
|
|
1400
1403
|
reportPracticeShape(report)
|
|
1404
|
+
reportReferenceContents(report)
|
|
1401
1405
|
reportUnmeasured()
|
|
1402
1406
|
outro()
|
|
1403
1407
|
}
|
|
@@ -1421,13 +1425,16 @@ async function runSkillsAudit(
|
|
|
1421
1425
|
requirementSections: report.requirementSections,
|
|
1422
1426
|
datedProvenance: report.datedProvenance,
|
|
1423
1427
|
practiceShape: report.practiceShape,
|
|
1428
|
+
referenceContents: report.referenceContents,
|
|
1424
1429
|
},
|
|
1425
1430
|
checkpoints: {
|
|
1426
1431
|
descriptionLimit: DESCRIPTION_LIMIT,
|
|
1427
1432
|
requirementSections: REQUIREMENT_SECTIONS,
|
|
1428
1433
|
corpora: CORPORA,
|
|
1429
1434
|
practiceSkills: PRACTICE_SKILLS,
|
|
1435
|
+
ledgerSkills: LEDGER_SKILLS,
|
|
1430
1436
|
practiceSections: PRACTICE_SECTIONS,
|
|
1437
|
+
contentsThreshold: CONTENTS_THRESHOLD,
|
|
1431
1438
|
},
|
|
1432
1439
|
})}\n`,
|
|
1433
1440
|
)
|
|
@@ -1626,10 +1633,10 @@ function reportProvenance(report: SkillsAudit): void {
|
|
|
1626
1633
|
function reportPracticeShape(report: SkillsAudit): void {
|
|
1627
1634
|
logStep('Practice shape')
|
|
1628
1635
|
logInfo(
|
|
1629
|
-
`A listed practice skill closes with ${PRACTICE_SECTIONS.map((section) => `## ${section}`).join(', ')}, and carries ${PRACTICE_LEDGER}.`,
|
|
1636
|
+
`A listed practice skill closes with ${PRACTICE_SECTIONS.map((section) => `## ${section}`).join(', ')}, and every skill on the wider ledger list carries ${PRACTICE_LEDGER}.`,
|
|
1630
1637
|
)
|
|
1631
1638
|
logInfo(
|
|
1632
|
-
`
|
|
1639
|
+
`Both lists are kept here and name ${plural(PRACTICE_SKILLS.length, 'practice skill')} and ${plural(LEDGER_SKILLS.length, 'ledger skill')}, so a skill outside ${SHIPPED_CORPUS} is never read.`,
|
|
1633
1640
|
)
|
|
1634
1641
|
|
|
1635
1642
|
if (report.practiceShape.length === 0) {
|
|
@@ -1643,6 +1650,24 @@ function reportPracticeShape(report: SkillsAudit): void {
|
|
|
1643
1650
|
reportFindings(report.practiceShape)
|
|
1644
1651
|
}
|
|
1645
1652
|
|
|
1653
|
+
/** Reports without failing the run, the same judgment the practice shape is. */
|
|
1654
|
+
function reportReferenceContents(report: SkillsAudit): void {
|
|
1655
|
+
logStep('Reference contents')
|
|
1656
|
+
logInfo(
|
|
1657
|
+
`A reference over ${CONTENTS_THRESHOLD} lines opens with a ## ${CONTENTS_SECTION} list, matched exactly outside fences.`,
|
|
1658
|
+
)
|
|
1659
|
+
|
|
1660
|
+
if (report.referenceContents.length === 0) {
|
|
1661
|
+
logInfo('Every long reference opens with a contents list.')
|
|
1662
|
+
return
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
logWarn(
|
|
1666
|
+
`${plural(report.referenceContents.length, 'long reference')} with no contents list`,
|
|
1667
|
+
)
|
|
1668
|
+
reportFindings(report.referenceContents)
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1646
1671
|
/**
|
|
1647
1672
|
* Stated on every run, including the run where everything above passed. A
|
|
1648
1673
|
* report that lists only what it measured reads as a verdict on the standard
|
package/src/commands/feedback.ts
CHANGED
|
@@ -7,11 +7,11 @@ import {
|
|
|
7
7
|
deriveTitle,
|
|
8
8
|
missingField,
|
|
9
9
|
missingFieldMessage,
|
|
10
|
-
} from '@/
|
|
10
|
+
} from '@/feedback/body'
|
|
11
11
|
import { checkoutMismatchWarning, PROJECT_ROOT } from '@/project-root'
|
|
12
12
|
import { creationRel } from '@/record-root'
|
|
13
|
-
import { createGithubIssue } from '@/github'
|
|
14
|
-
import { issueFailureMessage } from '@/github-format'
|
|
13
|
+
import { createGithubIssue } from '@/feedback/github'
|
|
14
|
+
import { issueFailureMessage } from '@/feedback/github-format'
|
|
15
15
|
import { frameError, frameSuccess, logWarn, palette } from '@/ui'
|
|
16
16
|
|
|
17
17
|
function readStdin(): Promise<string> {
|