@erclx/canon 4.82.0 → 4.84.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/docs-fold/REQUIREMENT.md +13 -0
- package/claude/skills/docs-fold/SKILL.md +20 -0
- package/claude/skills/docs-fold/references/classify.md +78 -0
- package/claude/skills/draft-and-pick/SKILL.md +1 -1
- package/claude/skills/draft-slides/SKILL.md +1 -1
- package/claude/skills/draft-wireframes/REQUIREMENT.md +1 -1
- package/claude/skills/draft-wireframes/SKILL.md +7 -4
- package/claude/skills/ux-walkthrough/REQUIREMENT.md +53 -0
- package/claude/skills/ux-walkthrough/SKILL.md +50 -0
- package/claude/skills/ux-walkthrough/references/builds.md +15 -0
- package/claude/skills/ux-walkthrough/references/candidate-pages.md +26 -0
- package/claude/skills/ux-walkthrough/references/measuring.md +22 -0
- package/claude/skills/ux-walkthrough/references/record.md +36 -0
- package/claude/skills/ux-walkthrough/references/relay.md +25 -0
- package/claude/skills/youtube-transcripts/SKILL.md +1 -1
- package/docs/agents/commands.md +1 -1
- package/docs/agents/context-audit-checks.md +22 -2
- package/docs/agents/context-audit.md +12 -2
- package/docs/agents/context-classify.md +1 -1
- package/docs/workflow/ai-workflow.md +2 -1
- package/docs/workflow/visual-design-workflow.md +2 -1
- package/governance/rules/claude/520-wireframes.md +1 -1
- package/package.json +1 -1
- package/src/claude/cases/workflow.ts +5 -0
- package/src/commands/context.ts +71 -2
- package/src/context/architecture.ts +73 -0
- package/src/context/audit.ts +18 -2
- package/src/context/gate.ts +44 -6
- package/src/context/wireframe-states.ts +238 -0
- package/src/record-root.ts +4 -2
- package/src/records/backup.ts +4 -3
- package/standards/architecture.md +11 -1
- package/standards/context.md +4 -1
- package/standards/design.md +6 -0
- package/standards/requirements.md +2 -1
- package/standards/skill.md +1 -1
- package/standards/wireframes.md +45 -29
- package/tooling/claude/reference.md +1 -1
- package/tooling/claude/seeds/CLAUDE.md +1 -1
- package/tooling/claude/seeds/canon/wireframes/index.md +2 -2
|
@@ -5,7 +5,7 @@ description: Classifying canonical-doc content as keep, replace/rewrite, history
|
|
|
5
5
|
|
|
6
6
|
# Doc classifier
|
|
7
7
|
|
|
8
|
-
`canon context classify` reads content from the five canonical doc types (`context/<domain>.md`, `ARCHITECTURE.md`, `wireframes/<surface>.md`, `DESIGN.md`, `REQUIREMENTS.md`) and reports whether each piece states the project as it stands or should be rewritten, narrated elsewhere, or moved to another surface. It reports and never gates: a finding is a judgment for a writer to act on, not a fact a push fails over. Wiring it into
|
|
8
|
+
`canon context classify` reads content from the five canonical doc types (`context/<domain>.md`, `ARCHITECTURE.md`, `wireframes/<surface>.md`, `DESIGN.md`, `REQUIREMENTS.md`) and reports whether each piece states the project as it stands or should be rewritten, narrated elsewhere, or moved to another surface. It reports and never gates: a finding is a judgment for a writer to act on, not a fact a push fails over. The `docs-fold` skill calls this verb in diff mode over its own diff baseline. Wiring it into the merge gate or `canon context audit` is separate work this verb does not do.
|
|
9
9
|
|
|
10
10
|
Two modes read different units. Diff mode reads the chunks a git range changed, each with the section it landed in, which is what a session checks right after an edit. Sweep mode reads every section of the five doc types, split at H3, which is what a cleanup pass or a periodic re-scan runs over a whole corpus.
|
|
11
11
|
|
|
@@ -19,7 +19,7 @@ canon/
|
|
|
19
19
|
├── REQUIREMENTS.md ← goals, non-goals, MVP scope
|
|
20
20
|
├── ARCHITECTURE.md ← technical design decisions
|
|
21
21
|
├── DESIGN.md ← visual intent and token decisions (UI projects)
|
|
22
|
-
├── wireframes/ ←
|
|
22
|
+
├── wireframes/ ← regions, states, UI copy, and interaction rules (UI projects)
|
|
23
23
|
├── context/ ← per-domain narrative loaded on demand via index.md
|
|
24
24
|
└── decisions/ ← decision history a canonical doc points at, never loaded eagerly
|
|
25
25
|
|
|
@@ -242,6 +242,7 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
242
242
|
| `canon:markdown-propose` | When a markdown claim needs rewriting and the change should wait for an answer per file |
|
|
243
243
|
| `canon:ux-audit` | To read UI source for missing states, edge cases, and inconsistencies |
|
|
244
244
|
| `canon:ux-measure` | To start the interface and measure paint, processor, and layout cost |
|
|
245
|
+
| `canon:ux-walkthrough` | To run a multi-finding inspection pass over a running app with the operator |
|
|
245
246
|
|
|
246
247
|
### Ship it
|
|
247
248
|
|
|
@@ -14,7 +14,7 @@ Two tools anchor tier 1 and tier 2. Stitch is the agent-addressable default thro
|
|
|
14
14
|
|
|
15
15
|
## Tier 0: prose only
|
|
16
16
|
|
|
17
|
-
The default. `canon/DESIGN.md` holds visual intent as prose plus token tables for color, typography, spacing, borders, motion, and iconography. `.claude/WIREFRAMES.md` holds
|
|
17
|
+
The default. `canon/DESIGN.md` holds visual intent as prose plus token tables for color, typography, spacing, borders, motion, and iconography. `.claude/WIREFRAMES.md` holds each surface's regions and states.
|
|
18
18
|
|
|
19
19
|
Claude Code reads both and writes the implementation. Works for CLI tools, internal dashboards, admin panels, and backend-focused projects.
|
|
20
20
|
|
|
@@ -41,6 +41,7 @@ A cell no source anchors ends in `? verify`, and the preview shows that marker b
|
|
|
41
41
|
- `canon:ux-audit` for UX gap detection on existing surfaces
|
|
42
42
|
- `canon:ux-measure` for what a running surface costs to paint, read against published thresholds
|
|
43
43
|
- `canon:draft-and-pick` for a call settled by looking, drafting several candidates onto one page and taking your pick
|
|
44
|
+
- `canon:ux-walkthrough` for a multi-finding inspection pass over a running app with the operator, drafting arms from the app's own markup and recording each pick for a builder
|
|
44
45
|
- `canon:draft-identity` to draft a project's logo mark and compose it into an icon sequence and a social card, through `draft-and-pick`'s own render-and-pick loop
|
|
45
46
|
- Anthropic's `frontend-design` plugin optional for light visual steering
|
|
46
47
|
|
|
@@ -9,5 +9,5 @@ paths:
|
|
|
9
9
|
|
|
10
10
|
## Authority
|
|
11
11
|
|
|
12
|
-
- Follow the wireframes standard for layout and interaction intent:
|
|
12
|
+
- Follow the wireframes standard for layout and interaction intent: regions, the states table and its evidence links, copy, behavior, exclusions, and what moves to `canon/context/`. It is the single source. Read it with `canon standards wireframes`.
|
|
13
13
|
- Read it before adding or revising a surface.
|
package/package.json
CHANGED
|
@@ -138,6 +138,11 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
138
138
|
prompt: 'Tell me the paint and layout cost of this page right now.',
|
|
139
139
|
expect: 'ux-measure',
|
|
140
140
|
},
|
|
141
|
+
{
|
|
142
|
+
prompt:
|
|
143
|
+
'Sit with me while I go through my whole list of app findings one at a time, and write down each pick with what it beat.',
|
|
144
|
+
expect: 'ux-walkthrough',
|
|
145
|
+
},
|
|
141
146
|
{
|
|
142
147
|
prompt:
|
|
143
148
|
'I am building this branch for another session. What am I on the hook for, and what is off limits?',
|
package/src/commands/context.ts
CHANGED
|
@@ -59,6 +59,10 @@ import {
|
|
|
59
59
|
PRONOUN_HEADING,
|
|
60
60
|
VERB_HEADING,
|
|
61
61
|
} from '@/context/narration'
|
|
62
|
+
import {
|
|
63
|
+
measureWireframeFolder,
|
|
64
|
+
type WireframeStatesReport,
|
|
65
|
+
} from '@/context/wireframe-states'
|
|
62
66
|
import { RENDER_WIDTH } from '@/markdown/structure'
|
|
63
67
|
import {
|
|
64
68
|
frameError,
|
|
@@ -94,7 +98,7 @@ export function register(program: Command): void {
|
|
|
94
98
|
context
|
|
95
99
|
.command('audit')
|
|
96
100
|
.description(
|
|
97
|
-
'Report required sections, entry length, citations, reference form, catalog tables, provenance, superseded-decision narration, index drift,
|
|
101
|
+
'Report required sections, entry length, citations, reference form, catalog tables, provenance, superseded-decision narration, index drift, the architecture record against its own ceiling and its word weight, and wireframe states against their evidence folders',
|
|
98
102
|
)
|
|
99
103
|
.argument('[path]', 'Project root, defaulting to the current directory')
|
|
100
104
|
.helpOption('-h, --help', 'Show this help message')
|
|
@@ -662,6 +666,15 @@ async function runAudit(
|
|
|
662
666
|
// are different answers, and one value for both reports the second as the
|
|
663
667
|
// first.
|
|
664
668
|
const record = gateOnly ? undefined : await measureArchitecture(root)
|
|
669
|
+
const wireframes = gateOnly
|
|
670
|
+
? []
|
|
671
|
+
: (
|
|
672
|
+
await Promise.all(
|
|
673
|
+
folders
|
|
674
|
+
.filter((folder) => folder.name === 'wireframes')
|
|
675
|
+
.map((folder) => measureWireframeFolder(root, folder)),
|
|
676
|
+
)
|
|
677
|
+
).flat()
|
|
665
678
|
|
|
666
679
|
if (gateOnly) {
|
|
667
680
|
reportGate(citations)
|
|
@@ -677,6 +690,7 @@ async function runAudit(
|
|
|
677
690
|
reportNarration(entries, folders, narration)
|
|
678
691
|
reportDrift(drift)
|
|
679
692
|
reportRecord(record, root)
|
|
693
|
+
reportWireframeStates(wireframes)
|
|
680
694
|
outro()
|
|
681
695
|
}
|
|
682
696
|
|
|
@@ -713,6 +727,11 @@ async function runAudit(
|
|
|
713
727
|
// target that never wrote one is entitled to. Absent says the run
|
|
714
728
|
// never looked, which is `--citations-only`.
|
|
715
729
|
architecture: gateOnly ? undefined : (record ?? null),
|
|
730
|
+
// Absent under `--citations-only`, for the same reason as above. An
|
|
731
|
+
// empty array under the ordinary run says the project carries no
|
|
732
|
+
// wireframes folder or no entry carrying a States table, which is a
|
|
733
|
+
// fact rather than an unmeasured run.
|
|
734
|
+
wireframes: gateOnly ? undefined : wireframes,
|
|
716
735
|
checkpoints: {
|
|
717
736
|
lines: LENGTH_CHECKPOINT,
|
|
718
737
|
renderWidth: RENDER_WIDTH,
|
|
@@ -742,6 +761,7 @@ async function runAudit(
|
|
|
742
761
|
recordOverLength: record !== undefined && isOverLength(record),
|
|
743
762
|
sections,
|
|
744
763
|
drift,
|
|
764
|
+
wireframes,
|
|
745
765
|
widened,
|
|
746
766
|
})
|
|
747
767
|
|
|
@@ -1202,6 +1222,15 @@ function reportRecord(
|
|
|
1202
1222
|
const decisions = report.decisions.length
|
|
1203
1223
|
const { allowances } = report
|
|
1204
1224
|
|
|
1225
|
+
logInfo(
|
|
1226
|
+
`${plural(report.words, 'word')} across ${plural(report.lines, 'line')}, read alongside the weight judgment a session makes by reading the file. This never gates.`,
|
|
1227
|
+
)
|
|
1228
|
+
if (report.risksWords !== undefined) {
|
|
1229
|
+
logInfo(
|
|
1230
|
+
`\`## Risks / open questions\` holds ${plural(report.risksWords, 'word')}, weighed the same way and read alongside the same judgment.`,
|
|
1231
|
+
)
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1205
1234
|
if (allowances === undefined) {
|
|
1206
1235
|
logInfo(
|
|
1207
1236
|
`Covers ${report.rel} alone. No standard sets a length rule for it and this record states none, so its ${plural(report.lines, 'line')} across ${plural(decisions, 'decision')} are reported and nothing is gated.`,
|
|
@@ -1261,7 +1290,7 @@ function reportRecord(
|
|
|
1261
1290
|
entry.checks.length > 0
|
|
1262
1291
|
? `\n checked by ${entry.checks.join(', ')}`
|
|
1263
1292
|
: ''
|
|
1264
|
-
return `${report.rel}:${entry.line} ${kind}${evidence}\n ${entry.heading}${checks}`
|
|
1293
|
+
return `${report.rel}:${entry.line} ${kind}${evidence} ${plural(entry.words, 'word')}\n ${entry.heading}${checks}`
|
|
1265
1294
|
})
|
|
1266
1295
|
.join('\n'),
|
|
1267
1296
|
)
|
|
@@ -1283,3 +1312,43 @@ function reportDrift(drift: readonly FolderDrift[]): void {
|
|
|
1283
1312
|
logWarn(plural(lines.length, 'disagreement'))
|
|
1284
1313
|
pipeOutput(lines.join('\n'))
|
|
1285
1314
|
}
|
|
1315
|
+
|
|
1316
|
+
function reportWireframeStates(
|
|
1317
|
+
wireframes: readonly WireframeStatesReport[],
|
|
1318
|
+
): void {
|
|
1319
|
+
logStep('Wireframe states')
|
|
1320
|
+
|
|
1321
|
+
const withRows = wireframes.filter((entry) => entry.rows.length > 0)
|
|
1322
|
+
if (withRows.length === 0) {
|
|
1323
|
+
logInfo(
|
|
1324
|
+
'No wireframe carries a States table, so nothing was checked against its evidence folders.',
|
|
1325
|
+
)
|
|
1326
|
+
return
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const lines = withRows.flatMap((entry) => [
|
|
1330
|
+
...entry.missingFolders.map(
|
|
1331
|
+
(finding) =>
|
|
1332
|
+
`${entry.rel}:${finding.line} ${finding.state} no folder at ${finding.path}`,
|
|
1333
|
+
),
|
|
1334
|
+
...entry.unlistedFolders.map(
|
|
1335
|
+
(finding) =>
|
|
1336
|
+
`${entry.rel} ${finding.root}/${finding.folder} named in no row`,
|
|
1337
|
+
),
|
|
1338
|
+
...(entry.sketchWithEvidence
|
|
1339
|
+
? [
|
|
1340
|
+
`${entry.rel}:${entry.sketchLine} a plaintext sketch sits beside evidence that already exists`,
|
|
1341
|
+
]
|
|
1342
|
+
: []),
|
|
1343
|
+
])
|
|
1344
|
+
|
|
1345
|
+
if (lines.length === 0) {
|
|
1346
|
+
logInfo(
|
|
1347
|
+
`${plural(withRows.length, 'wireframe')} checked, every state matched one-to-one with its evidence folder.`,
|
|
1348
|
+
)
|
|
1349
|
+
return
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
logWarn(plural(lines.length, 'finding'))
|
|
1353
|
+
pipeOutput(lines.join('\n'))
|
|
1354
|
+
}
|
|
@@ -57,11 +57,25 @@ export interface DecisionReport {
|
|
|
57
57
|
* claim some check happens to cover without the entry saying so.
|
|
58
58
|
*/
|
|
59
59
|
readonly checks: readonly string[]
|
|
60
|
+
/**
|
|
61
|
+
* The entry's own word count, read alongside the weight judgment the
|
|
62
|
+
* standard asks a session to make by reading the file rather than counting
|
|
63
|
+
* it. Never gates, per `standards/architecture.md`'s `## Length` section.
|
|
64
|
+
*/
|
|
65
|
+
readonly words: number
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
export interface ArchitectureReport {
|
|
63
69
|
readonly rel: string
|
|
64
70
|
readonly lines: number
|
|
71
|
+
/** The whole record's word count, reported for the same reason. */
|
|
72
|
+
readonly words: number
|
|
73
|
+
/**
|
|
74
|
+
* Word count of the `## Risks / open questions` section, absent when the
|
|
75
|
+
* record carries no such heading. The standard asks that section to hold
|
|
76
|
+
* only what is still open, so its weight is read the same way the file's is.
|
|
77
|
+
*/
|
|
78
|
+
readonly risksWords?: number
|
|
65
79
|
/** What the record declared, absent when it states no length rule. */
|
|
66
80
|
readonly allowances?: Allowances
|
|
67
81
|
/** The frame plus the per-decision allowance, absent alongside it. */
|
|
@@ -71,6 +85,8 @@ export interface ArchitectureReport {
|
|
|
71
85
|
|
|
72
86
|
const DECISION_HEADING = /^###\s+(.+?)\s*$/
|
|
73
87
|
const SECTION_HEADING = /^##\s+\S/
|
|
88
|
+
/** The one H2 the standard's `## Length` section asks to be weighed by words. */
|
|
89
|
+
const RISKS_HEADING = /^##\s+Risks\s*\/\s*open questions\s*$/i
|
|
74
90
|
const CODE_SPAN = /`[^`]*`/g
|
|
75
91
|
/** Dropped ahead of the figure scan, since an anchor date is not a claim. */
|
|
76
92
|
const ISO_DATE = /\b\d{4}-\d{2}-\d{2}\b/g
|
|
@@ -238,6 +254,54 @@ export function splitDecisions(source: string): RawDecision[] {
|
|
|
238
254
|
return decisions
|
|
239
255
|
}
|
|
240
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Counts whitespace-delimited tokens, which is the unit the standard's
|
|
259
|
+
* `## Length` section reads alongside the weight judgment a session makes by
|
|
260
|
+
* reading the file. It is a report figure rather than a gate, so a fenced
|
|
261
|
+
* example or a code span inflating the count costs nothing a reader corrects
|
|
262
|
+
* for by reading, the same trade the line count above it already takes.
|
|
263
|
+
*/
|
|
264
|
+
function wordCount(text: string): number {
|
|
265
|
+
return text.match(/\S+/g)?.length ?? 0
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Extracts the `## Risks / open questions` section, or nothing when the
|
|
270
|
+
* record carries no such heading.
|
|
271
|
+
*
|
|
272
|
+
* The section is read the same way `splitDecisions` reads a decision: capture
|
|
273
|
+
* starts at the heading and stops at the next H2, or at the end of the file
|
|
274
|
+
* when the section is last, which is where the standard's template puts it.
|
|
275
|
+
*/
|
|
276
|
+
export function risksSection(source: string): string | undefined {
|
|
277
|
+
const lines = bodyLines(source)
|
|
278
|
+
const body: string[] = []
|
|
279
|
+
let capturing = false
|
|
280
|
+
let found = false
|
|
281
|
+
|
|
282
|
+
for (const line of lines) {
|
|
283
|
+
if (line.fenced) {
|
|
284
|
+
if (capturing) body.push(line.text)
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (RISKS_HEADING.test(line.text)) {
|
|
289
|
+
capturing = true
|
|
290
|
+
found = true
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (capturing && SECTION_HEADING.test(line.text)) {
|
|
295
|
+
capturing = false
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (capturing) body.push(line.text)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return found ? body.join('\n') : undefined
|
|
303
|
+
}
|
|
304
|
+
|
|
241
305
|
/** Cardinals a record spells rather than writes, which the corpus does for both. */
|
|
242
306
|
const SPELLED: Record<string, number> = {
|
|
243
307
|
one: 1,
|
|
@@ -332,13 +396,22 @@ export async function measureArchitecture(
|
|
|
332
396
|
figures,
|
|
333
397
|
...(quantified !== undefined && { quantified }),
|
|
334
398
|
checks: await namedChecks(root, entry.body),
|
|
399
|
+
words: wordCount(entry.body),
|
|
335
400
|
}
|
|
336
401
|
}),
|
|
337
402
|
)
|
|
338
403
|
|
|
404
|
+
const risks = risksSection(source)
|
|
405
|
+
|
|
339
406
|
return {
|
|
340
407
|
rel,
|
|
341
408
|
lines: source.replace(/\n$/, '').split('\n').length,
|
|
409
|
+
words: wordCount(
|
|
410
|
+
bodyLines(source)
|
|
411
|
+
.map((line) => line.text)
|
|
412
|
+
.join('\n'),
|
|
413
|
+
),
|
|
414
|
+
...(risks !== undefined && { risksWords: wordCount(risks) }),
|
|
342
415
|
...(allowances !== undefined && {
|
|
343
416
|
allowances,
|
|
344
417
|
ceiling: ceilingFor(allowances, raw.length),
|
package/src/context/audit.ts
CHANGED
|
@@ -77,6 +77,14 @@ const INSIDE_LIST = /^\s+\S/
|
|
|
77
77
|
* and which release labelled it. A marker is a judgment rather than a defect,
|
|
78
78
|
* so this is measured and reported and never gates.
|
|
79
79
|
*
|
|
80
|
+
* The change pattern matches any digit count rather than the three-plus this
|
|
81
|
+
* used to require, since a project young enough to sit in single or double
|
|
82
|
+
* digits, `PR #7`, went entirely unread under the old floor. Widening it reads
|
|
83
|
+
* a change reference wherever a document is scanned as prose, so `provenance`
|
|
84
|
+
* masks a line's displayed spans first, which is what keeps a heading's own
|
|
85
|
+
* auto-derived link destination, `(#7-open-questions)`, from reading as a
|
|
86
|
+
* change number: `#7` there names the heading, not a pull request.
|
|
87
|
+
*
|
|
80
88
|
* The release pattern accepts three segments without a leading `v`, since the
|
|
81
89
|
* standard cuts a release label rather than a spelling of one and `a CLI at
|
|
82
90
|
* 0.83.0` names a release exactly as `v0.83.0` does. Two segments still require
|
|
@@ -88,7 +96,7 @@ const INSIDE_LIST = /^\s+\S/
|
|
|
88
96
|
*/
|
|
89
97
|
const PROVENANCE: readonly { kind: ProvenanceKind; pattern: RegExp }[] = [
|
|
90
98
|
{ kind: 'date', pattern: /\b\d{4}-\d{2}-\d{2}\b/g },
|
|
91
|
-
{ kind: 'change', pattern: /#\d
|
|
99
|
+
{ kind: 'change', pattern: /#\d+\b/g },
|
|
92
100
|
{ kind: 'release', pattern: /\b(?:v\d+\.\d+(?:\.\d+)?|\d+\.\d+\.\d+)\b/g },
|
|
93
101
|
]
|
|
94
102
|
|
|
@@ -349,6 +357,12 @@ function catalogTables(entry: readonly BodyLine[]): TableFinding[] {
|
|
|
349
357
|
* rather than a claim it makes, and a version pinned in an install line is the
|
|
350
358
|
* ordinary shape of one.
|
|
351
359
|
*
|
|
360
|
+
* Displayed spans are masked for the same reason. A code span quoting a hex
|
|
361
|
+
* color, `#000`, and a link destination naming a heading's own anchor,
|
|
362
|
+
* `(#7-open-questions)`, both read as text the entry shows rather than a claim
|
|
363
|
+
* it makes, which is the same distinction the fence skip draws at the block
|
|
364
|
+
* level.
|
|
365
|
+
*
|
|
352
366
|
* A date stamping a measurement is dropped rather than reported under a kind of
|
|
353
367
|
* its own. One list with one meaning is what lets every consumer read it
|
|
354
368
|
* without filtering: the report names what the standard cuts, and the length
|
|
@@ -365,8 +379,10 @@ function provenance(lines: readonly BodyLine[]): ProvenanceFinding[] {
|
|
|
365
379
|
for (const line of lines) {
|
|
366
380
|
if (line.fenced) continue
|
|
367
381
|
|
|
382
|
+
const text = maskDisplayed(line.text)
|
|
383
|
+
|
|
368
384
|
for (const { kind, pattern } of PROVENANCE) {
|
|
369
|
-
for (const match of
|
|
385
|
+
for (const match of text.matchAll(pattern)) {
|
|
370
386
|
if (kind === 'date' && stampsMeasurement(line.text, match.index)) {
|
|
371
387
|
continue
|
|
372
388
|
}
|
package/src/context/gate.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SectionFinding } from '@/context/audit'
|
|
2
2
|
import type { FolderDrift } from '@/context/index-drift'
|
|
3
|
+
import type { WireframeStatesReport } from '@/context/wireframe-states'
|
|
3
4
|
|
|
4
5
|
export interface GateInput {
|
|
5
6
|
/** Cited paths that resolved to nothing, which gate under either mode. */
|
|
@@ -15,6 +16,22 @@ export interface GateInput {
|
|
|
15
16
|
readonly recordOverLength: boolean
|
|
16
17
|
readonly sections: readonly SectionFinding[]
|
|
17
18
|
readonly drift: readonly FolderDrift[]
|
|
19
|
+
/**
|
|
20
|
+
* Every wireframe entry's states report, empty for a project carrying no
|
|
21
|
+
* `canon/wireframes/` folder. A states/evidence mismatch is a fact read off
|
|
22
|
+
* the file, the same standing the missing-section and index-drift findings
|
|
23
|
+
* already have, so it gates alongside them under the widened mode.
|
|
24
|
+
*
|
|
25
|
+
* A sketch beside existing evidence stays out of the gate on both modes.
|
|
26
|
+
* `sketchWithEvidence` reads the whole entry against whether any of its
|
|
27
|
+
* states has evidence, not the sketched layout against the evidence for
|
|
28
|
+
* that same layout, since the standard's `## States` table names states
|
|
29
|
+
* rather than layouts and the report has no narrower unit to match on. An
|
|
30
|
+
* entry with a captured default layout and a sketch for a breakpoint layout
|
|
31
|
+
* that is not built yet is conforming and still trips this reading, so the
|
|
32
|
+
* finding stays a judgment for a reader rather than a fact a push fails on.
|
|
33
|
+
*/
|
|
34
|
+
readonly wireframes: readonly WireframeStatesReport[]
|
|
18
35
|
/**
|
|
19
36
|
* Whether the caller asked for the widened gate. False leaves a missing
|
|
20
37
|
* section and a drifted index advisory, which is what the project-root stage
|
|
@@ -30,28 +47,49 @@ export function hasDrift(drift: readonly FolderDrift[]): boolean {
|
|
|
30
47
|
)
|
|
31
48
|
}
|
|
32
49
|
|
|
50
|
+
/** Whether any wireframe's States table disagrees with its evidence folders. */
|
|
51
|
+
export function hasStatesMismatch(
|
|
52
|
+
wireframes: readonly WireframeStatesReport[],
|
|
53
|
+
): boolean {
|
|
54
|
+
return wireframes.some(
|
|
55
|
+
(entry) =>
|
|
56
|
+
entry.missingFolders.length > 0 || entry.unlistedFolders.length > 0,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Whether any wireframe carries a sketch beside evidence that already exists. */
|
|
61
|
+
export function hasSketchWithEvidence(
|
|
62
|
+
wireframes: readonly WireframeStatesReport[],
|
|
63
|
+
): boolean {
|
|
64
|
+
return wireframes.some((entry) => entry.sketchWithEvidence)
|
|
65
|
+
}
|
|
66
|
+
|
|
33
67
|
/**
|
|
34
68
|
* Whether the audit found something that should fail the caller.
|
|
35
69
|
*
|
|
36
70
|
* An unresolved citation is a broken pointer and gates unconditionally, and so
|
|
37
71
|
* does a record past its own ceiling: the record states the limit for itself
|
|
38
72
|
* and derives it from a count, which makes it the one measure here that is a
|
|
39
|
-
* fact rather than a threshold a reader weighs. The
|
|
40
|
-
*
|
|
41
|
-
* declare,
|
|
42
|
-
*
|
|
43
|
-
*
|
|
73
|
+
* fact rather than a threshold a reader weighs. The findings `--gate` adds are
|
|
74
|
+
* the ones answerable from the file itself: a required section it does not
|
|
75
|
+
* declare, an index disagreeing with its folder, and a wireframe's States
|
|
76
|
+
* table disagreeing with its evidence folders. Entry length, depth, bullet,
|
|
77
|
+
* table, provenance, the record's claim coverage, and a sketch beside
|
|
78
|
+
* existing evidence are judgments, so they stay out under both modes. The
|
|
79
|
+
* `wireframes` field's own doc states why the sketch finding is a judgment
|
|
80
|
+
* rather than a fact.
|
|
44
81
|
*/
|
|
45
82
|
export function isGating({
|
|
46
83
|
unresolvedCitations,
|
|
47
84
|
recordOverLength,
|
|
48
85
|
sections,
|
|
49
86
|
drift,
|
|
87
|
+
wireframes,
|
|
50
88
|
widened,
|
|
51
89
|
}: GateInput): boolean {
|
|
52
90
|
if (unresolvedCitations > 0) return true
|
|
53
91
|
if (recordOverLength) return true
|
|
54
92
|
if (!widened) return false
|
|
55
93
|
|
|
56
|
-
return sections.length > 0 || hasDrift(drift)
|
|
94
|
+
return sections.length > 0 || hasDrift(drift) || hasStatesMismatch(wireframes)
|
|
57
95
|
}
|