@kerfjs/ui 5.0.0-beta.18 → 5.0.0-beta.22

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 (170) hide show
  1. package/README.md +114 -3
  2. package/ai/application-ui-diagnostic-ids-v1.json +92 -0
  3. package/ai/application-ui-profile-sync.cjs +529 -0
  4. package/ai/application-ui-profile.d.mts +1 -0
  5. package/ai/application-ui-profile.d.ts +102 -0
  6. package/ai/application-ui-profile.defaults.json +41 -0
  7. package/ai/application-ui-profile.mjs +626 -0
  8. package/ai/application-ui-profile.schema.json +130 -0
  9. package/ai/catalog-authoring.json +25 -0
  10. package/ai/catalog-authoring.schema.json +61 -0
  11. package/ai/compile-time-contracts-v1.json +101 -0
  12. package/ai/compile-time-contracts-v1.schema.json +49 -0
  13. package/ai/component-catalog-extension-v2.d.ts +7 -0
  14. package/ai/component-catalog-extension-v2.schema.json +7 -0
  15. package/ai/component-catalog-v2-overrides.json +326 -0
  16. package/ai/component-catalog-v2.d.ts +67 -0
  17. package/ai/component-catalog-v2.json +4441 -0
  18. package/ai/component-catalog-v2.schema.json +246 -0
  19. package/ai/component-catalog.json +150 -6
  20. package/ai/public-api-signatures-v1.md +294 -64
  21. package/ai/skill.md +41 -5
  22. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  23. package/analyzer/cli.mjs +46 -0
  24. package/analyzer/index.d.mts +45 -0
  25. package/analyzer/index.mjs +743 -0
  26. package/analyzer/report.schema.json +66 -0
  27. package/dist/browser/pane.js +3 -0
  28. package/dist/browser/sunken-panel.js +3 -0
  29. package/dist/catalog-resources.d.ts +34 -0
  30. package/dist/catalog-resources.js +22 -0
  31. package/dist/catalog-resources.js.map +1 -0
  32. package/dist/catalog.d.ts +21 -3
  33. package/dist/catalog.js +239 -210
  34. package/dist/catalog.js.map +1 -1
  35. package/dist/{chunk-ZK2G3VR5.js → chunk-5VTB2BKP.js} +1 -1
  36. package/dist/chunk-5VTB2BKP.js.map +1 -0
  37. package/dist/{chunk-S3HKPI7I.js → chunk-7KEUJIIC.js} +1 -1
  38. package/dist/chunk-7KEUJIIC.js.map +1 -0
  39. package/dist/{chunk-NVWXKU37.js → chunk-AASLYRGE.js} +1 -1
  40. package/dist/chunk-AASLYRGE.js.map +1 -0
  41. package/dist/{chunk-QIRXYKCE.js → chunk-DSNIYOZ3.js} +3 -2
  42. package/dist/chunk-DSNIYOZ3.js.map +1 -0
  43. package/dist/{chunk-ENVTNLYC.js → chunk-GP5DP5DL.js} +55 -15
  44. package/dist/chunk-GP5DP5DL.js.map +1 -0
  45. package/dist/chunk-JRKSK2HX.js +79 -0
  46. package/dist/chunk-JRKSK2HX.js.map +1 -0
  47. package/dist/{chunk-U5IFGQCD.js → chunk-K57KRAOV.js} +1 -1
  48. package/dist/chunk-K57KRAOV.js.map +1 -0
  49. package/dist/{chunk-ZVI5YST2.js → chunk-M7JBER5U.js} +1 -1
  50. package/dist/chunk-M7JBER5U.js.map +1 -0
  51. package/dist/{chunk-43FUQH35.js → chunk-OHU6CI22.js} +2 -2
  52. package/dist/{chunk-43FUQH35.js.map → chunk-OHU6CI22.js.map} +1 -1
  53. package/dist/{chunk-4TRDV6RL.js → chunk-OZWU5ZPZ.js} +1 -1
  54. package/dist/chunk-OZWU5ZPZ.js.map +1 -0
  55. package/dist/{chunk-JTHUXDN2.js → chunk-RYRIFZCT.js} +3 -3
  56. package/dist/chunk-RYRIFZCT.js.map +1 -0
  57. package/dist/{chunk-6ETZKKAE.js → chunk-VLYV65LK.js} +1 -1
  58. package/dist/chunk-VLYV65LK.js.map +1 -0
  59. package/dist/chunk-W4CXQYSG.js +23 -0
  60. package/dist/chunk-W4CXQYSG.js.map +1 -0
  61. package/dist/{chunk-S3PCZ7NV.js → chunk-W7YD6E4N.js} +1 -1
  62. package/dist/chunk-W7YD6E4N.js.map +1 -0
  63. package/dist/index.d.ts +6 -4
  64. package/dist/index.js +14 -12
  65. package/dist/list-header.d.ts +29 -8
  66. package/dist/list-header.js +1 -1
  67. package/dist/nav-stack.js +1 -1
  68. package/dist/pane.d.ts +49 -0
  69. package/dist/pane.js +4 -0
  70. package/dist/pane.js.map +1 -0
  71. package/dist/panel-header.d.ts +9 -3
  72. package/dist/panel-header.js +1 -1
  73. package/dist/segmented-control.d.ts +6 -6
  74. package/dist/segmented-control.js +1 -1
  75. package/dist/select.d.ts +10 -4
  76. package/dist/select.js +1 -1
  77. package/dist/split-view.js +2 -2
  78. package/dist/state-banner.d.ts +3 -2
  79. package/dist/state-banner.js +1 -1
  80. package/dist/styles/catalog.css +6 -6
  81. package/dist/styles/layout.css +2 -20
  82. package/dist/styles/pane.css +62 -0
  83. package/dist/styles/segmented-control.css +3 -7
  84. package/dist/styles/sunken-panel.css +18 -0
  85. package/dist/sunken-panel.d.ts +16 -0
  86. package/dist/sunken-panel.js +3 -0
  87. package/dist/sunken-panel.js.map +1 -0
  88. package/dist/tab-bar.d.ts +3 -2
  89. package/dist/tab-bar.js +1 -1
  90. package/dist/tab-scaffold.d.ts +6 -6
  91. package/dist/tab-scaffold.js.map +1 -1
  92. package/dist/token-search-field.d.ts +25 -9
  93. package/dist/token-search-field.js +1 -1
  94. package/dist/toolbar-control-group.d.ts +9 -5
  95. package/dist/toolbar-control-group.js +1 -1
  96. package/dist/toolbar-text.d.ts +10 -11
  97. package/dist/toolbar-text.js +1 -1
  98. package/dist/wire-tab-bars.d.ts +5 -2
  99. package/dist/wire-tab-bars.js +1 -1
  100. package/dist/wire-token-search-fields.d.ts +11 -6
  101. package/dist/wire-token-search-fields.js +1 -1
  102. package/docs/catalog.md +186 -67
  103. package/docs/component-contract.md +102 -5
  104. package/docs/component-selection.md +9 -5
  105. package/docs/design/templates/panel-header/icon-actions-dark.svg +1 -1
  106. package/docs/design/templates/panel-header/icon-actions.svg +1 -1
  107. package/docs/design/templates/panel-header/icon-summary-actions-dark.svg +1 -1
  108. package/docs/design/templates/panel-header/icon-summary-actions.svg +1 -1
  109. package/docs/design/templates/panel-header/no-icon-dark.svg +1 -1
  110. package/docs/design/templates/panel-header/no-icon.svg +1 -1
  111. package/docs/design/templates/panel-header/page-heading-dark.svg +1 -1
  112. package/docs/design/templates/panel-header/page-heading.svg +1 -1
  113. package/docs/design/templates/panel-header/title-only-dark.svg +1 -1
  114. package/docs/design/templates/panel-header/title-only.svg +1 -1
  115. package/docs/design/templates/panel-header-dark.svg +5 -5
  116. package/docs/design/templates/panel-header.svg +5 -5
  117. package/docs/design/templates/segmented-control/equal-dark.svg +1 -1
  118. package/docs/design/templates/segmented-control/equal.svg +1 -1
  119. package/docs/design/templates/segmented-control-dark.svg +1 -1
  120. package/docs/design/templates/segmented-control.svg +1 -1
  121. package/docs/design/templates/toolbar/title-and-actions-dark.svg +1 -1
  122. package/docs/design/templates/toolbar/title-and-actions.svg +1 -1
  123. package/docs/design/templates/toolbar-control-group/with-text-dark.svg +1 -1
  124. package/docs/design/templates/toolbar-control-group/with-text.svg +1 -1
  125. package/docs/design/templates/toolbar-control-group-dark.svg +1 -1
  126. package/docs/design/templates/toolbar-control-group.svg +1 -1
  127. package/docs/design/templates/toolbar-dark.svg +1 -1
  128. package/docs/design/templates/toolbar-text/default-dark.svg +1 -1
  129. package/docs/design/templates/toolbar-text/default.svg +1 -1
  130. package/docs/design/templates/toolbar-text/large-dark.svg +1 -1
  131. package/docs/design/templates/toolbar-text/large.svg +1 -1
  132. package/docs/design/templates/toolbar-text/small-dark.svg +1 -1
  133. package/docs/design/templates/toolbar-text/small.svg +1 -1
  134. package/docs/design/templates/toolbar-text-dark.svg +3 -3
  135. package/docs/design/templates/toolbar-text.svg +3 -3
  136. package/docs/design/templates/toolbar.svg +1 -1
  137. package/docs/design/templates.md +11 -6
  138. package/docs/design-philosophy.md +2 -4
  139. package/docs/examples/application-ui-profile.json +60 -0
  140. package/docs/examples/component-catalog-extension-v2.json +81 -0
  141. package/docs/layout.md +42 -23
  142. package/docs/sunken-panel.md +43 -0
  143. package/docs/type-contracts.md +99 -0
  144. package/docs/ui-analyzer.md +81 -0
  145. package/docs/ui-doctor.md +77 -0
  146. package/docs/ui-evaluator.md +122 -0
  147. package/doctor/cli.mjs +115 -0
  148. package/doctor/config.schema.json +68 -0
  149. package/doctor/index.d.mts +116 -0
  150. package/doctor/index.mjs +1256 -0
  151. package/doctor/report.schema.json +118 -0
  152. package/evaluator/cli.mjs +89 -0
  153. package/evaluator/index.d.mts +90 -0
  154. package/evaluator/index.mjs +1138 -0
  155. package/evaluator/report.schema.json +152 -0
  156. package/llms.txt +40 -3
  157. package/package.json +66 -8
  158. package/ux-demo/recipes/compact-toolbar.tsx +2 -2
  159. package/ux-demo/recipes/composer-form.tsx +1 -1
  160. package/dist/chunk-4TRDV6RL.js.map +0 -1
  161. package/dist/chunk-6ETZKKAE.js.map +0 -1
  162. package/dist/chunk-ENVTNLYC.js.map +0 -1
  163. package/dist/chunk-JTHUXDN2.js.map +0 -1
  164. package/dist/chunk-NVWXKU37.js.map +0 -1
  165. package/dist/chunk-QIRXYKCE.js.map +0 -1
  166. package/dist/chunk-S3HKPI7I.js.map +0 -1
  167. package/dist/chunk-S3PCZ7NV.js.map +0 -1
  168. package/dist/chunk-U5IFGQCD.js.map +0 -1
  169. package/dist/chunk-ZK2G3VR5.js.map +0 -1
  170. package/dist/chunk-ZVI5YST2.js.map +0 -1
@@ -0,0 +1,43 @@
1
+ # SunkenPanel
2
+
3
+ `SunkenPanel` is a visually lowered application surface with one compact inset
4
+ and a vertical content stack. Use it for a main work area or nested panel whose
5
+ background should sit behind ordinary content, such as the primary stage in an
6
+ issue tracker or component catalog.
7
+
8
+ ```tsx
9
+ import { SunkenPanel } from '@kerfjs/ui/sunken-panel';
10
+
11
+ <SunkenPanel ariaLabel="Release workspace">
12
+ <ReleaseSummary />
13
+ <ReleaseChecks />
14
+ </SunkenPanel>;
15
+ ```
16
+
17
+ ## Ownership
18
+
19
+ The root owns its lowered background, 8px padding, and 8px vertical gap.
20
+ Children own their borders and internal geometry. The application owns child
21
+ order, responsive placement, and scrolling; `SunkenPanel` deliberately does not
22
+ create another scroll container.
23
+
24
+ Use `ariaLabel` only when the surface is a distinct region people need to find
25
+ by name. With a label, the root receives `role="region"`; without one it remains
26
+ a non-landmark grouping.
27
+
28
+ Do not use `SunkenPanel` merely to add padding, as a substitute for pane
29
+ header/content/footer anatomy, or around a child that already owns the same
30
+ outer surface.
31
+
32
+ ## Public styling boundary
33
+
34
+ Override the public properties at the composition boundary:
35
+
36
+ - `--kui-sunken-panel-background`
37
+ - `--kui-sunken-panel-foreground`
38
+ - `--kui-sunken-panel-padding`
39
+ - `--kui-sunken-panel-gap`
40
+ - `--kui-sunken-panel-radius`
41
+
42
+ The public root class is `.kui-sunken-panel`. Prefer the properties above over
43
+ styling descendants.
@@ -0,0 +1,99 @@
1
+ # Compile-time contracts
2
+
3
+ Kerf UI rejects invalid integrations at compile time only when TypeScript can
4
+ prove the relationship from one call. The versioned
5
+ [`compile-time-contracts-v1.json`](../ai/compile-time-contracts-v1.json)
6
+ artifact names those guarantees with stable `KUI-T###` ids. Its source fixture
7
+ is compiled twice: against `src/` and against declarations extracted from the
8
+ actual `npm pack` tarball. `npm run check:catalog` also verifies that every
9
+ contract still points to a real catalog entry/import and a symbol in the emitted
10
+ public-signature artifact.
11
+
12
+ ## Strengthened contracts
13
+
14
+ | ID | Compile-time guarantee |
15
+ | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
+ | `KUI-T001` | `ListHeader` is a passive heading, a fully named/icon-bearing trailing action, or a controlled disclosure with `action` + `expanded`. Props ignored by the selected mode are rejected. |
17
+ | `KUI-T002` | `Select` has a visible `label` or an `ariaLabel`. |
18
+ | `KUI-T003` | A literal `Select.value` belongs to its literal `choices`. |
19
+ | `KUI-T004` | A literal `SegmentedControl.value` belongs to its literal `choices`. |
20
+ | `KUI-T005` | `expanded`, `expandAction`, and `expandLabel` exist only on `TokenSearchField({ collapsible: true })`. |
21
+ | `KUI-T006` | `TokenSearchEditorAttributes` accepts application `data-*` metadata but rejects component-owned identity, morph, count, and placeholder attributes. |
22
+ | `KUI-T007` | `PanelHeader.summaryId` cannot exist without rendered `summary` content. |
23
+ | `KUI-T008` | `ToolbarText.maxLines` requires `wrap: true`; it is never silently ignored in typed code. |
24
+ | `KUI-T009` | A literal `TabScaffold.active` id belongs to its literal `tabs`. |
25
+ | `KUI-T010` | Adjacent-token keyboard removal requires `onRemoveToken`; disabling removal rejects the now-meaningless callback. |
26
+ | `KUI-T011` | Finite public variants have named exported union types, including tab activation, banner urgency, and all ToolbarControlGroup presentation axes. |
27
+
28
+ Literal identity inference is intentionally additive for dynamic applications.
29
+ When choices or ids arrive at runtime and the state signal is correctly typed as
30
+ `string`, widen the component explicitly:
31
+
32
+ ```tsx
33
+ <Select<string> value={loadedValue.value} choices={loadedChoices.value} ... />
34
+ <SegmentedControl<string> value={mode.value} choices={loadedModes.value} ... />
35
+ <TabScaffold<string> active={active.value} tabs={loadedTabs.value} ... />
36
+ ```
37
+
38
+ The widening is an honest statement that TypeScript cannot know the runtime
39
+ dataset. Do not cast a known literal typo merely to bypass the relationship.
40
+
41
+ ## Complete public-surface audit
42
+
43
+ The following existing contracts were already exact and remain covered by the
44
+ ordinary source/type gates:
45
+
46
+ - required identity or accessible-name inputs on `AppTab`, `FloatingToolbar`,
47
+ `NavStack`, `SplitView`, `Workbench`, `TabBar`, `ResizableRegion`,
48
+ `SegmentedControl`, `TokenSearchField`, `PanelHeader`, and `ValueTable`;
49
+ - finite variants exported or represented as literal props on `DisclosureArrow`,
50
+ `FloatingToolbar`, `Pane`, `ResizableRegion`, `SegmentedControl`, `StateBanner`,
51
+ `ToolbarText`, `CollapsiblePanel`, `Catalog`, and the wire helpers;
52
+ - required controlled-state callbacks on `wireResizableRegions`, `wireTabBars`,
53
+ and `wireTabScaffold`;
54
+ - mutually exclusive count/badge metadata on `ListHeader`; and
55
+ - protected application metadata boundaries on `AppTab`, `ListItem`,
56
+ `ListActionRow`, `ListHeader`, `Pane`, `CatalogExample`, and
57
+ `CatalogExampleStack`.
58
+
59
+ The remaining components (`LucideIcon`, `DisclosureArrow`, `Toolbar`,
60
+ `ToolbarControlGroup`, `ListInsetControl`, `ListInsetText`, `LoadingSpinner`,
61
+ `Skeleton`, `SunkenPanel`, `EmptyState`, `StateBanner`, `ValueTableRow`, and the
62
+ pure helpers) have independent props with no further conditional invalid state
63
+ that TypeScript can truthfully remove. Optional labels on decorative icons and
64
+ unnamed non-landmark surfaces are intentional variants, not missing contracts.
65
+
66
+ ## Boundaries TypeScript cannot prove
67
+
68
+ Types do not claim to validate:
69
+
70
+ - the contents of `SafeHtml` slots (for example, “only AppTab children” or “no
71
+ interactive descendants”);
72
+ - nonempty arrays, unique ids, numeric min/max ordering, or a controlled value
73
+ loaded from dynamic data;
74
+ - equality between ids rendered in separate calls, elements discovered later in
75
+ the DOM, or an ARIA relationship's live target;
76
+ - that a caller retained and invoked a wiring disposer; or
77
+ - CSS class/token use inside arbitrary strings.
78
+
79
+ Those remain catalog `KUI-C###` diagnostics, runtime filtering/guards, lint or
80
+ browser assertions. Encoding them as permissive-looking TypeScript brands would
81
+ move errors into casts without making the integration safer.
82
+
83
+ ## Migration from the earlier declarations
84
+
85
+ - Add `actionLabel` and `actionIcon` to a non-toggle `ListHeader` action. For a
86
+ disclosure, set literal `toggle: true`, provide `action` and controlled
87
+ `expanded`, and remove trailing-action-only props.
88
+ - Give every `Select` either `label` or `ariaLabel`. Use an explicit `<string>`
89
+ generic only for genuinely dynamic choice data.
90
+ - Put `expanded`/expand-action props behind `collapsible: true`.
91
+ - Replace `wireTokenSearchFields({ keyboard: true })` with either
92
+ `{ keyboard: { onRemoveToken } }` or
93
+ `{ keyboard: { removeAdjacentToken: false } }` when only caret navigation is
94
+ wanted.
95
+ - Remove orphaned `summaryId`, or render the matching `summary`; add
96
+ `wrap: true` anywhere `ToolbarText.maxLines` is used.
97
+
98
+ JavaScript runtime behavior remains defensive for previously emitted calls, but
99
+ new TypeScript builds report these invalid or ignored combinations.
@@ -0,0 +1,81 @@
1
+ # Static CSS and layout ownership analyzer
2
+
3
+ `kerf-ui-analyze` checks integration facts that TypeScript and an individual
4
+ ESLint file cannot see across component markup, stylesheets, the component
5
+ catalog, and the application UI profile.
6
+
7
+ ```sh
8
+ npx kerf-ui-analyze --root . --format text src
9
+ npx kerf-ui-analyze --root . --format json --output artifacts/kerf-ui.json
10
+ npx kerf-ui-analyze --root . --format sarif --output artifacts/kerf-ui.sarif
11
+ ```
12
+
13
+ The analyzer is opt-in. Add a project script such as
14
+ `"check:kerf-ui-layout": "kerf-ui-analyze --root . src"`; it never executes
15
+ application code. It discovers `.kerf-ui-profile.json`, joins every declared
16
+ composition catalog by `package:id`, parses literal TSX/JSX class usage and CSS,
17
+ and reports portable repository-relative locations.
18
+
19
+ Analysis is scoped per source file. Each TSX/JSX file resolves package,
20
+ workspace, and parent-to-child directory profiles from its own location, then
21
+ follows its relative CSS imports (including nested CSS `@import`s). Stylesheet
22
+ facts are never pooled across unrelated files or sibling packages, and a shared
23
+ stylesheet's diagnostics are evaluated against every importing source profile.
24
+ A violation stays active when any consumer has not narrowly excepted it;
25
+ duplicate source findings collapse to one result. Directly targeted or orphaned
26
+ CSS uses its own directory profile. A direct changed-file target still brings
27
+ its reachable project-local styles into the report, including quoted or
28
+ unquoted `url()` imports; external package styles remain outside the consumer
29
+ boundary.
30
+
31
+ ## Diagnostics and exit behavior
32
+
33
+ | Rule | Kind | Meaning |
34
+ | ---------- | ------ | ----------------------------------------------------------------- |
35
+ | `KUI-L001` | error | A stylesheet reaches into a private or unknown `.kui-*` selector. |
36
+ | `KUI-L002` | error | A stylesheet references an unknown or private `--kui-*` token. |
37
+ | `KUI-L003` | error | One element combines public classes that claim the same geometry. |
38
+ | `KUI-L004` | review | Nested literal classes both add a content inset. |
39
+ | `KUI-L005` | review | Consumer CSS forces a public component's dimensions. |
40
+ | `KUI-L006` | review | Literal spacing falls outside Kerf's approved scale. |
41
+ | `KUI-L007` | error | A declared scroll owner is nested inside another scroll owner. |
42
+ | `KUI-L008` | review | A dynamic class expression cannot be classified soundly. |
43
+ | `KUI-L009` | error | A stylesheet cannot be parsed. |
44
+
45
+ Errors are provable contract violations and make the command exit 1. Review
46
+ findings are deliberately heuristic and do not fail by default; pass
47
+ `--fail-on-review` when a project has reviewed its baseline and wants them to
48
+ gate CI. Text, versioned JSON, and SARIF carry the same stable rule ids,
49
+ source locations, evidence, and ownership chain.
50
+
51
+ The JSON report schema is exported as
52
+ `@kerfjs/ui/analyzer/report.schema.json`.
53
+
54
+ Profile discovery, parsing, catalog-loading, and validation diagnostics retain
55
+ their stable `KUI-P###` ids in every output format and count as errors. The
56
+ analyzer therefore cannot silently pass with stale or unreadable policy input.
57
+
58
+ The analyzer is conservative about dynamic class expressions: it emits one
59
+ review finding and does not guess which selectors, tokens, or owners the value
60
+ might contain. CSS values expressed through public variables or `calc()` are
61
+ also left to their owning token contract rather than reverse-engineered.
62
+
63
+ ## Narrow exceptions
64
+
65
+ Use the application profile's `exceptions` only for an exact rule and exact
66
+ repository-relative file. Every exception requires an id and rationale; broad
67
+ directories, globs, absolute paths, and traversal are rejected by the profile
68
+ validator.
69
+
70
+ ```json
71
+ {
72
+ "id": "legacy-inspector-spacing",
73
+ "rules": ["KUI-L006"],
74
+ "target": "src/legacy/inspector.css",
75
+ "rationale": "The legacy inspector retains its measured spacing until migration."
76
+ }
77
+ ```
78
+
79
+ Review findings remain visible until explicitly suppressed. Avoid suppressing
80
+ `KUI-L001`, `KUI-L002`, `KUI-L003`, `KUI-L007`, or `KUI-L009`: those indicate a
81
+ definite boundary or parsing failure rather than an aesthetic judgment.
@@ -0,0 +1,77 @@
1
+ # Kerf UI doctor
2
+
3
+ `kerf-ui-doctor` is the supported repair-loop entry point for a Kerf application. It produces one versioned report from application-profile and catalog validation, TypeScript, the Kerf UI ESLint preset, static layout analysis, and an explicitly enabled browser evaluation.
4
+
5
+ ```sh
6
+ npx kerf-ui-doctor --full
7
+ npx kerf-ui-doctor --changed
8
+ npx kerf-ui-doctor --package @acme/admin --changed
9
+ npx kerf-ui-doctor --full --browser-url http://127.0.0.1:4173
10
+ ```
11
+
12
+ The default terminal output is short and repair-oriented. `--format json` emits the schema-version-1 report; `--output report.json` writes the same representation. The package exports its contract from `@kerfjs/ui/doctor`, its configuration schema from `@kerfjs/ui/doctor/config.schema.json`, and its report schema from `@kerfjs/ui/doctor/report.schema.json`.
13
+
14
+ ## Stages and trust boundary
15
+
16
+ The catalog stage discovers the package-default application UI profile and workspace/root-to-leaf directory layers. It validates referenced selection/composition catalogs and, when a workspace declares `package.json#kerfComponentCatalog`, invokes the installed `create-kerf-component` catalog checker to validate metadata, source exports, schema conformance, and generated-output drift. This checker reads source text; it does not import application modules.
17
+
18
+ TypeScript uses the compiler API with `noEmit`. ESLint loads the installed `eslint-plugin-kerfjs` `recommended-ui` preset (`--eslint strict-ui` opts into advisory rules as errors). Stable KUI identifiers come from the packaged `application-ui-diagnostic-ids-v1.json` registry; the doctor also reads installed ESLint message metadata for semantic conflict detection instead of duplicating rule definitions. The analyzer calls the public `@kerfjs/ui/analyzer` contract. None of these stages executes generated application code.
19
+
20
+ The browser evaluator is different: it runs the application and is disabled by default. It only runs when configuration supplies `browser.url` or the command receives `--browser-url`. Start and authorize the target application separately.
21
+
22
+ An unavailable or failed stage does not prevent independent stages from reporting. Its final exit is still a configuration failure, so a partial run cannot appear clean.
23
+
24
+ ## Full and changed modes
25
+
26
+ `--full` is the default and analyzes the selected package. `--changed` reads tracked and untracked paths from Git unless one or more `--path` values are supplied. An empty changed set is a configuration error instead of a false-clean success. With `--package`, workspace-relative Git paths are converted to package-relative paths before TypeScript, ESLint, and analyzer selection; paths outside the selected package are ignored.
27
+
28
+ TypeScript constructs the selected package's program so compiler options retain their real meaning, while its root inputs are narrowed to changed source files. Use `--full` for release gates.
29
+
30
+ ## Configuration and suppressions
31
+
32
+ Place `.kerf-ui-doctor.json` at the workspace root:
33
+
34
+ ```json
35
+ {
36
+ "$schema": "./node_modules/@kerfjs/ui/doctor/config.schema.json",
37
+ "schemaVersion": 1,
38
+ "mode": "full",
39
+ "stages": { "browser": false },
40
+ "cache": true,
41
+ "suppressions": [
42
+ {
43
+ "id": "legacy-toolbar",
44
+ "rules": ["KUI-L006"],
45
+ "target": "src/legacy-toolbar.css",
46
+ "rationale": "Removed with the toolbar migration in the next release."
47
+ }
48
+ ]
49
+ }
50
+ ```
51
+
52
+ A suppression requires a stable id, one or more exact diagnostic ids, an exact portable source path or browser selector, and a substantive rationale. Wildcards, absolute paths, and parent traversal are rejected. Suppressed diagnostics remain in `report.suppressions` with their rationale and do not affect the exit code.
53
+
54
+ KUI suppression ids are checked against the analyzer, evaluator, doctor, installed ESLint-rule metadata, and diagnostics emitted by available tools. An unknown or stale id is a configuration error rather than a silent no-op. TypeScript `TS####` and namespaced `eslint:*` ids remain valid even when that particular run does not emit them.
55
+
56
+ The content-addressed cache lives at `.kerf-cache/ui-doctor-v1.json`. Its key covers selected source/config/catalog JSON, package manifests, root and package lockfiles, mode, paths, and configuration. Browser results are never cached. Use `--no-cache` when investigating tool installation changes not yet reflected in a lockfile.
57
+
58
+ Reports replace the workspace's absolute path with `<repo-root>` and express in-workspace files as portable relative paths. External paths are reduced to `<external>/<basename>`. URLs are excluded from the cache key. Tool messages and evidence receive the same root redaction before output or caching.
59
+
60
+ ## Report and exit contract
61
+
62
+ Every diagnostic has a stable `id`, `severity`, `stage`, message, and—when applicable—an exact source location with JSON path or DOM context/selector. Analyzer evidence, catalog facts, documentation links, and safe next actions are preserved when the source tool provides them. Identical findings merge with their source stages; same identifiers with conflicting severities remain separate and add `KUI-D003`.
63
+
64
+ Exit codes are deterministic:
65
+
66
+ - `0`: no active error diagnostics;
67
+ - `1`: repairable application findings;
68
+ - `2`: malformed configuration, unavailable required tooling, or a failed stage;
69
+ - `130`: cancellation.
70
+
71
+ Warnings and review findings remain visible but do not fail unless their originating preset promotes them to errors. A cached report keeps its original exit code and labels previously run stages `cached`.
72
+
73
+ ## Monorepos and repair loops
74
+
75
+ `--package` accepts either a workspace package name or a workspace-relative package path. The doctor uses the selected package for compiler, lint, profile-discovery start, and analyzer scope, while workspace profile precedence and root lockfiles remain authoritative.
76
+
77
+ A repair agent should run JSON mode, apply only source-located safe changes, and rerun until exit `0`. Do not treat exit `2`, a skipped required stage, or an empty changed selection as clean. Browser evidence is objective input; the evaluator's named subjective rubric still requires human review.
@@ -0,0 +1,122 @@
1
+ # Browser UI quality evaluator
2
+
3
+ `kerf-ui-evaluate` is the runtime half of Kerf's UI integration checks. The
4
+ static [`kerf-ui-analyze`](./ui-analyzer.md) command finds source-level ownership
5
+ mistakes; this Playwright-backed evaluator loads a running application and
6
+ records what a browser can establish about the rendered result.
7
+
8
+ Install Playwright beside `@kerfjs/ui` and install its browsers in CI:
9
+
10
+ ```bash
11
+ npm install --save-dev playwright
12
+ npx playwright install --with-deps chromium firefox webkit
13
+ ```
14
+
15
+ Start the application separately, then evaluate its URL from the project root:
16
+
17
+ ```bash
18
+ npx kerf-ui-evaluate \
19
+ --url http://127.0.0.1:4173 \
20
+ --root . \
21
+ --output artifacts/kerf-ui
22
+ ```
23
+
24
+ The command discovers and resolves the same `.kerf-ui-profile.json` layers as
25
+ the static analyzer. By default it runs Chromium, Firefox, and WebKit through
26
+ six contexts: wide, intermediate, narrow, a half-width CSS viewport representing
27
+ 200% browser zoom, dark color scheme, and reduced motion. Use
28
+ `--browser chromium` for a fast local loop; keep all three engines in CI.
29
+
30
+ ## Objective checks
31
+
32
+ The versioned JSON report uses stable `KUI-B###` codes and includes the context,
33
+ selector, measured evidence, and a repair-oriented message for each finding.
34
+
35
+ | Code | Browser fact |
36
+ | ---------- | -------------------------------------------------------------------------------- |
37
+ | `KUI-B001` | Navigation, timeout, or browser-context failure |
38
+ | `KUI-B010` | Horizontal page overflow |
39
+ | `KUI-B011` | Interactive content clipped by a hidden/clip owner or entirely off viewport |
40
+ | `KUI-B012` | An in-viewport action covered at its center point |
41
+ | `KUI-B020` | Positive `tabindex` overriding DOM focus order |
42
+ | `KUI-B021` | Stable per-element focus reaches an invisible control or causes no visual change |
43
+ | `KUI-B022` | Any ARIA widget role is absent from sequential keyboard order |
44
+ | `KUI-B023` | A representative action does not activate with Enter |
45
+ | `KUI-B030` | An interactive control has no text alternative |
46
+ | `KUI-B040` | Alpha-composited interactive text is below its WCAG AA contrast threshold |
47
+ | `KUI-B050` | A button-like target is smaller than 44 × 44 CSS pixels |
48
+ | `KUI-B060` | A pane or declared scroll scope has multiple active scroll owners |
49
+ | `KUI-B070` | Elements in an explicitly declared alignment group drift by more than one pixel |
50
+ | `KUI-B080` | A cataloged root computes geometry assigned to `none`, `parent`, or `child` |
51
+
52
+ For a representative application action that must be operated in the generic
53
+ evaluation, add `data-kui-evaluator-action` to its owning native control or
54
+ keyboard-operable custom control. The evaluator focuses it, presses Enter, and
55
+ accepts a click event or controlled `aria-expanded`, `aria-pressed`,
56
+ `aria-checked`, or `value` transition as evidence. This attribute is test
57
+ instrumentation, not an application event hook.
58
+
59
+ Alignment checks are similarly explicit: put `data-kui-align-group` on the
60
+ shared container and `data-kui-align-edge` on two or more elements whose logical
61
+ start edges must align. Use `data-kui-scroll-scope` only for an application-owned
62
+ scope that is not a `.kui-pane`.
63
+
64
+ Accessible names follow ARIA/native naming inputs, including labels, referenced
65
+ content with `aria-hidden` descendants removed, and native input values.
66
+ Geometry checks use each catalog entry's explicit `boundaries.rootClass`; the
67
+ order of `publicClasses` has no runtime meaning.
68
+
69
+ Profile exceptions apply only when both the `KUI-B###` rule and `target` match
70
+ exactly. For the browser evaluator, `target` is the evaluated `startDirectory`
71
+ relative to `workspaceRoot` (for example `apps/settings`); wildcards and broad
72
+ workspace targets remain invalid.
73
+
74
+ ## Report and artifacts
75
+
76
+ The public Node API is available at `@kerfjs/ui/evaluator`:
77
+
78
+ ```js
79
+ import { evaluateUi } from '@kerfjs/ui/evaluator';
80
+
81
+ const abortController = new AbortController();
82
+ const report = await evaluateUi({
83
+ url: 'http://127.0.0.1:4173',
84
+ workspaceRoot: process.cwd(),
85
+ retention: 'on-failure',
86
+ signal: abortController.signal,
87
+ });
88
+ if (!report.summary.passed) process.exitCode = 1;
89
+ ```
90
+
91
+ The default output is `kerf-ui-evidence/report.json`; its schema ships at
92
+ `@kerfjs/ui/evaluator/report.schema.json`. Every retained screenshot has a
93
+ SHA-256 digest. Retention is deterministic:
94
+
95
+ - `on-failure` (default) removes current and stale evaluator screenshots when
96
+ every objective check passes;
97
+ - `always` retains one full-page PNG per browser/context;
98
+ - `never` removes current and stale evaluator screenshots and writes only the
99
+ focused DOM/computed-style evidence in the report.
100
+
101
+ `--timeout` bounds navigation and browser operations, while `--settle` provides
102
+ one small explicit post-navigation delay for applications with asynchronous
103
+ startup. A timeout becomes `KUI-B001` evidence rather than an unstructured crash.
104
+ The Node API accepts an `AbortSignal`; cancellation closes the active browser,
105
+ removes screenshots from the interrupted run, does not write a partial report,
106
+ and rejects with `AbortError`.
107
+ The CLI exits `1` for objective failures, `2` for invocation/profile errors, and
108
+ `0` for a pass; `--no-fail` is available only for evidence-collection jobs.
109
+
110
+ ## Human visual review stays human
111
+
112
+ Hierarchy, rhythm, density, aesthetic fit, and the perceived clarity of
113
+ alignment or scroll ownership are not inferred from computed pixels. Every
114
+ report carries the suite-v3 0–2 review rubric with `status: "not-recorded"` and
115
+ an empty rating list. An AI agent may use the retained screenshots as review
116
+ input, but it must record a named reviewer and rationale in the separate
117
+ human-visual evidence workflow; it must not turn a subjective rating into a
118
+ `KUI-B###` assertion.
119
+
120
+ The deterministic good/bad downstream fixtures and three-engine integration
121
+ suite live under `tests/fixtures/ui-evaluator/` and
122
+ `tests/integration/ui-evaluator-downstream.test.ts`.
package/doctor/cli.mjs ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+
3
+ import console from 'node:console';
4
+ import { execFile } from 'node:child_process';
5
+ import { writeFile } from 'node:fs/promises';
6
+ import { resolve } from 'node:path';
7
+ import process from 'node:process';
8
+ import { promisify } from 'node:util';
9
+
10
+ import { formatUiDoctorText, runUiDoctor, UI_DOCTOR_EXIT } from './index.mjs';
11
+
12
+ const execFileAsync = promisify(execFile);
13
+ const args = process.argv.slice(2);
14
+ const take = (name, repeat = false) => {
15
+ const values = [];
16
+ while (args.includes(name)) {
17
+ const index = args.indexOf(name);
18
+ const value = args[index + 1];
19
+ if (!value || value.startsWith('--'))
20
+ throw new Error(`${name} requires a value.`);
21
+ args.splice(index, 2);
22
+ values.push(value);
23
+ if (!repeat) break;
24
+ }
25
+ return repeat ? values : values[0];
26
+ };
27
+
28
+ async function changedPaths(root) {
29
+ const { stdout: tracked } = await execFileAsync(
30
+ 'git',
31
+ ['diff', '--name-only', '--diff-filter=ACMR', 'HEAD', '--'],
32
+ { cwd: root },
33
+ );
34
+ const { stdout: untracked } = await execFileAsync(
35
+ 'git',
36
+ ['ls-files', '--others', '--exclude-standard'],
37
+ { cwd: root },
38
+ );
39
+ return [
40
+ ...new Set(`${tracked}\n${untracked}`.split(/\r?\n/).filter(Boolean)),
41
+ ].sort();
42
+ }
43
+
44
+ try {
45
+ if (args.includes('--help') || args.includes('-h')) {
46
+ console.log(`Usage: kerf-ui-doctor [--full | --changed] [options]
47
+
48
+ Options:
49
+ --root <path> Workspace root (default: current directory)
50
+ --package <name|path> Select one workspace package
51
+ --path <path> Restrict changed mode (repeatable)
52
+ --config <path> Doctor config (default: .kerf-ui-doctor.json)
53
+ --browser-url <url> Explicitly enable browser evaluation
54
+ --browser <list> Comma-separated chromium,firefox,webkit
55
+ --eslint <preset> recommended-ui or strict-ui
56
+ --format <format> text or json (default: text)
57
+ --output <path> Also write the merged report
58
+ --no-cache Disable the content-addressed cache
59
+
60
+ Exit codes: 0 clean, 1 findings, 2 configuration/tool failure, 130 cancelled.`);
61
+ process.exit(0);
62
+ }
63
+ const root = resolve(take('--root') ?? process.cwd());
64
+ const packageSelector = take('--package');
65
+ const explicitPaths = take('--path', true);
66
+ const configPath = take('--config');
67
+ const url = take('--browser-url');
68
+ const browserList = take('--browser');
69
+ const eslintConfig = take('--eslint') ?? 'recommended-ui';
70
+ const format = take('--format') ?? 'text';
71
+ const output = take('--output');
72
+ const full = args.includes('--full');
73
+ const changed = args.includes('--changed');
74
+ const noCache = args.includes('--no-cache');
75
+ const known = new Set(['--full', '--changed', '--no-cache']);
76
+ const unknown = args.filter((item) => !known.has(item));
77
+ if (unknown.length)
78
+ throw new Error(`Unknown arguments: ${unknown.join(' ')}`);
79
+ if (full && changed)
80
+ throw new Error('--full and --changed are mutually exclusive.');
81
+ if (!['text', 'json'].includes(format))
82
+ throw new Error('--format must be text or json.');
83
+ if (!['recommended-ui', 'strict-ui'].includes(eslintConfig))
84
+ throw new Error('--eslint must be recommended-ui or strict-ui.');
85
+ const abort = new globalThis.AbortController();
86
+ process.once('SIGINT', () => abort.abort());
87
+ const paths =
88
+ changed && !explicitPaths.length ? await changedPaths(root) : explicitPaths;
89
+ const report = await runUiDoctor({
90
+ root,
91
+ package: packageSelector,
92
+ mode: changed ? 'changed' : full ? 'full' : undefined,
93
+ paths,
94
+ configPath,
95
+ cache: !noCache,
96
+ signal: abort.signal,
97
+ eslintConfig,
98
+ browser: url
99
+ ? { url, browsers: browserList?.split(',').map((item) => item.trim()) }
100
+ : undefined,
101
+ });
102
+ const rendered =
103
+ format === 'json'
104
+ ? JSON.stringify(report, null, 2)
105
+ : formatUiDoctorText(report);
106
+ if (output) await writeFile(resolve(root, output), `${rendered}\n`);
107
+ else console.log(rendered);
108
+ process.exitCode = report.exitCode;
109
+ } catch (error) {
110
+ console.error(`kerf-ui-doctor: ${error.message}`);
111
+ process.exitCode =
112
+ process.exitCode === UI_DOCTOR_EXIT.cancelled
113
+ ? process.exitCode
114
+ : UI_DOCTOR_EXIT.configuration;
115
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/brianwestphal/kerf/main/ui/doctor/config.schema.json",
4
+ "title": "Kerf UI doctor configuration",
5
+ "type": "object",
6
+ "required": ["schemaVersion"],
7
+ "properties": {
8
+ "$schema": { "type": "string" },
9
+ "schemaVersion": { "const": 1 },
10
+ "mode": { "enum": ["full", "changed"] },
11
+ "package": { "type": "string", "minLength": 1 },
12
+ "stages": {
13
+ "type": "object",
14
+ "properties": {
15
+ "catalog": { "type": "boolean" },
16
+ "typescript": { "type": "boolean" },
17
+ "eslint": { "type": "boolean" },
18
+ "analyzer": { "type": "boolean" },
19
+ "browser": { "type": "boolean" }
20
+ },
21
+ "additionalProperties": false
22
+ },
23
+ "browser": {
24
+ "type": "object",
25
+ "required": ["url"],
26
+ "properties": {
27
+ "url": { "type": "string", "pattern": "^https?://" },
28
+ "browsers": {
29
+ "type": "array",
30
+ "items": { "enum": ["chromium", "firefox", "webkit"] },
31
+ "uniqueItems": true
32
+ },
33
+ "retention": { "enum": ["always", "on-failure", "never"] },
34
+ "outputDirectory": { "$ref": "#/$defs/path" },
35
+ "reportPath": { "$ref": "#/$defs/path" }
36
+ },
37
+ "additionalProperties": false
38
+ },
39
+ "cache": { "type": "boolean" },
40
+ "suppressions": {
41
+ "type": "array",
42
+ "items": {
43
+ "type": "object",
44
+ "required": ["id", "rules", "target", "rationale"],
45
+ "properties": {
46
+ "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
47
+ "rules": {
48
+ "type": "array",
49
+ "items": { "type": "string", "minLength": 1 },
50
+ "minItems": 1,
51
+ "uniqueItems": true
52
+ },
53
+ "target": { "$ref": "#/$defs/path" },
54
+ "rationale": { "type": "string", "minLength": 12 }
55
+ },
56
+ "additionalProperties": false
57
+ }
58
+ }
59
+ },
60
+ "additionalProperties": false,
61
+ "$defs": {
62
+ "path": {
63
+ "type": "string",
64
+ "minLength": 1,
65
+ "pattern": "^(?!/)(?!.*(^|/)\\.\\.(/|$))(?!.*[?*]).+"
66
+ }
67
+ }
68
+ }