@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.
Files changed (41) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/docs-fold/REQUIREMENT.md +13 -0
  3. package/claude/skills/docs-fold/SKILL.md +20 -0
  4. package/claude/skills/docs-fold/references/classify.md +78 -0
  5. package/claude/skills/draft-and-pick/SKILL.md +1 -1
  6. package/claude/skills/draft-slides/SKILL.md +1 -1
  7. package/claude/skills/draft-wireframes/REQUIREMENT.md +1 -1
  8. package/claude/skills/draft-wireframes/SKILL.md +7 -4
  9. package/claude/skills/ux-walkthrough/REQUIREMENT.md +53 -0
  10. package/claude/skills/ux-walkthrough/SKILL.md +50 -0
  11. package/claude/skills/ux-walkthrough/references/builds.md +15 -0
  12. package/claude/skills/ux-walkthrough/references/candidate-pages.md +26 -0
  13. package/claude/skills/ux-walkthrough/references/measuring.md +22 -0
  14. package/claude/skills/ux-walkthrough/references/record.md +36 -0
  15. package/claude/skills/ux-walkthrough/references/relay.md +25 -0
  16. package/claude/skills/youtube-transcripts/SKILL.md +1 -1
  17. package/docs/agents/commands.md +1 -1
  18. package/docs/agents/context-audit-checks.md +22 -2
  19. package/docs/agents/context-audit.md +12 -2
  20. package/docs/agents/context-classify.md +1 -1
  21. package/docs/workflow/ai-workflow.md +2 -1
  22. package/docs/workflow/visual-design-workflow.md +2 -1
  23. package/governance/rules/claude/520-wireframes.md +1 -1
  24. package/package.json +1 -1
  25. package/src/claude/cases/workflow.ts +5 -0
  26. package/src/commands/context.ts +71 -2
  27. package/src/context/architecture.ts +73 -0
  28. package/src/context/audit.ts +18 -2
  29. package/src/context/gate.ts +44 -6
  30. package/src/context/wireframe-states.ts +238 -0
  31. package/src/record-root.ts +4 -2
  32. package/src/records/backup.ts +4 -3
  33. package/standards/architecture.md +11 -1
  34. package/standards/context.md +4 -1
  35. package/standards/design.md +6 -0
  36. package/standards/requirements.md +2 -1
  37. package/standards/skill.md +1 -1
  38. package/standards/wireframes.md +45 -29
  39. package/tooling/claude/reference.md +1 -1
  40. package/tooling/claude/seeds/CLAUDE.md +1 -1
  41. 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 `docs-fold`, the merge gate, or `canon context audit` is separate work this verb does not do.
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/ ← ASCII wireframes: layout, UI copy, and interaction rules (UI projects)
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 ASCII layouts.
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: ASCII layout, region labels, variants, copy, and what moves to `canon/context/`. It is the single source. Read it with `canon standards wireframes`.
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.82.0",
4
+ "version": "4.84.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -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?',
@@ -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, and the architecture record against its own ceiling',
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),
@@ -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{3,}\b/g },
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 line.text.matchAll(pattern)) {
385
+ for (const match of text.matchAll(pattern)) {
370
386
  if (kind === 'date' && stampsMeasurement(line.text, match.index)) {
371
387
  continue
372
388
  }
@@ -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 two findings `--gate` adds
40
- * are the ones answerable from the file itself: a required section it does not
41
- * declare, and an index disagreeing with its folder. Entry length, depth,
42
- * bullet, table, provenance, and the record's claim coverage are judgments, so
43
- * they stay out under both modes.
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
  }