@afokapu/atdd-bun 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/conventions/coder.bun/coder.bun.design-connected.convention.yaml +34 -0
- package/conventions/coder.bun/coder.bun.design-dependency-flow.convention.yaml +37 -0
- package/conventions/coder.bun/coder.bun.design-orphan-export.convention.yaml +36 -0
- package/conventions/coder.bun/coder.bun.design-primitives.convention.yaml +35 -0
- package/conventions/coder.bun/coder.bun.design-token-color.convention.yaml +38 -0
- package/conventions/coder.bun/coder.bun.design-token-hardcoded.convention.yaml +40 -0
- package/conventions/coder.bun/coder.bun.design-tokens-pure.convention.yaml +35 -0
- package/conventions/coder.bun/coder.bun.design-wagons-import.convention.yaml +35 -0
- package/detectors/bun_design_system_detector/atdd.implementation.yaml +28 -0
- package/detectors/bun_design_system_detector/checks/_design.mjs +112 -0
- package/detectors/bun_design_system_detector/checks/_map.json +10 -0
- package/detectors/bun_design_system_detector/checks/design_connected.mjs +19 -0
- package/detectors/bun_design_system_detector/checks/design_dependency_flow.mjs +20 -0
- package/detectors/bun_design_system_detector/checks/design_orphan_export.mjs +33 -0
- package/detectors/bun_design_system_detector/checks/design_primitives.mjs +16 -0
- package/detectors/bun_design_system_detector/checks/design_token_color.mjs +22 -0
- package/detectors/bun_design_system_detector/checks/design_token_hardcoded.mjs +29 -0
- package/detectors/bun_design_system_detector/checks/design_tokens_pure.mjs +23 -0
- package/detectors/bun_design_system_detector/checks/design_wagons_import.mjs +18 -0
- package/detectors/bun_design_system_detector/detect.mjs +46 -0
- package/detectors/bun_design_system_detector/fixtures/clean/app/public/index.html +6 -0
- package/detectors/bun_design_system_detector/fixtures/clean/app/public/site.css +9 -0
- package/detectors/bun_design_system_detector/fixtures/clean/app/src/design/components/Card.tsx +6 -0
- package/detectors/bun_design_system_detector/fixtures/clean/app/src/design/primitives/Button.tsx +6 -0
- package/detectors/bun_design_system_detector/fixtures/clean/app/src/design/primitives/Text.tsx +5 -0
- package/detectors/bun_design_system_detector/fixtures/clean/app/src/design/primitives/index.ts +2 -0
- package/detectors/bun_design_system_detector/fixtures/clean/app/src/design/templates/Page.tsx +5 -0
- package/detectors/bun_design_system_detector/fixtures/clean/app/src/design/tokens/colors.ts +1 -0
- package/detectors/bun_design_system_detector/fixtures/clean/app/src/design/tokens/spacing.ts +1 -0
- package/detectors/bun_design_system_detector/fixtures/clean/app/src/design/tokens/theme.css +8 -0
- package/detectors/bun_design_system_detector/fixtures/clean/app/src/wagons/orders/presentation/orders-view.tsx +6 -0
- package/detectors/bun_design_system_detector/fixtures/clean/app/src/wagons/orders/presentation/orders.css +8 -0
- package/detectors/bun_design_system_detector/fixtures/dirty/connected/src/design/tokens/colors.ts +1 -0
- package/detectors/bun_design_system_detector/fixtures/dirty/connected/src/views/plain.tsx +3 -0
- package/detectors/bun_design_system_detector/fixtures/dirty/dependency_flow/src/design/components/Card.tsx +3 -0
- package/detectors/bun_design_system_detector/fixtures/dirty/dependency_flow/src/design/primitives/Badge.tsx +5 -0
- package/detectors/bun_design_system_detector/fixtures/dirty/orphan_export/src/design/primitives/Unused.tsx +3 -0
- package/detectors/bun_design_system_detector/fixtures/dirty/primitives/src/design/primitives/Button.tsx +3 -0
- package/detectors/bun_design_system_detector/fixtures/dirty/primitives/src/views/form.tsx +5 -0
- package/detectors/bun_design_system_detector/fixtures/dirty/token_color/src/app.css +4 -0
- package/detectors/bun_design_system_detector/fixtures/dirty/token_color/src/design/tokens/colors.css +1 -0
- package/detectors/bun_design_system_detector/fixtures/dirty/token_color/src/views/alert.tsx +3 -0
- package/detectors/bun_design_system_detector/fixtures/dirty/token_hardcoded/public/list.html +1 -0
- package/detectors/bun_design_system_detector/fixtures/dirty/token_hardcoded/src/design/tokens/spacing.css +1 -0
- package/detectors/bun_design_system_detector/fixtures/dirty/token_hardcoded/src/views/list.tsx +3 -0
- package/detectors/bun_design_system_detector/fixtures/dirty/tokens_pure/src/design/tokens/swatch.tsx +9 -0
- package/detectors/bun_design_system_detector/fixtures/dirty/wagons_import/src/design/components/OrderBadge.tsx +5 -0
- package/detectors/bun_design_system_detector/fixtures/dirty/wagons_import/src/wagons/orders/order-status.ts +1 -0
- package/package.json +1 -1
- package/src/enforce.ts +3 -2
- package/templates/agents/atdd/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -90,6 +90,7 @@ name.
|
|
|
90
90
|
| `coder`, `tester`, `security`, `architecture`, `metrics`, `runtime` | Bun source and test conventions for that concern |
|
|
91
91
|
| `interlocking` | declared train/interlocking binding, infrastructure, and coverage |
|
|
92
92
|
| `htmx` | htmx-specific source and test conventions |
|
|
93
|
+
| `design` | the design system: tokens ← primitives ← components ← templates, token-only colors, spacing, radii, and motion (also part of `coder`) |
|
|
93
94
|
| `all` | the complete package policy, normally used by CI |
|
|
94
95
|
|
|
95
96
|
The package ships the canonical planner-node corpus as planning reference, but
|
|
@@ -110,6 +111,29 @@ carry Station Master actions; the Bun interlocking family checks those actions r
|
|
|
110
111
|
Hooks, direct CLI use, and CI invoke this same profile and therefore share the
|
|
111
112
|
same schema source.
|
|
112
113
|
|
|
114
|
+
### Design system
|
|
115
|
+
|
|
116
|
+
The `design` profile (also part of `coder`) is the Bun realization of the core
|
|
117
|
+
`coder.design.*` obligations. It recognizes a design system by its directory:
|
|
118
|
+
a folder named `design`, `design_system`, or `design-system`, whose first
|
|
119
|
+
subfolder names the layer:
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
design/
|
|
123
|
+
tokens/ or foundations/ values only: palette, spacing, radii, motion
|
|
124
|
+
primitives/ Button, Text, Stack … built from tokens
|
|
125
|
+
components/ composed from primitives
|
|
126
|
+
templates/ page structure composed from components
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Imports flow downward only, and the design system never imports app code.
|
|
130
|
+
Outside the tokens layer, `.tsx`, `.html`, and `.css` files take colors,
|
|
131
|
+
spacing, radii, and durations from tokens (`var(--…)`); app components render
|
|
132
|
+
controls through primitives and import at least one design-system element; and
|
|
133
|
+
every exported component has a consumer. Defining a custom property
|
|
134
|
+
(`--accent: #0ea5e9`) is defining a token and is allowed anywhere. A repository
|
|
135
|
+
without a design directory is not judged by these rules.
|
|
136
|
+
|
|
113
137
|
### Theme and contract registry
|
|
114
138
|
|
|
115
139
|
Theme vocabulary belongs to the repository, not the package. When a plan uses
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
schema_version: 1.1.0
|
|
2
|
+
rule_id: coder.bun.design-connected
|
|
3
|
+
kind: rule
|
|
4
|
+
status: active
|
|
5
|
+
name: Every rendering app component is connected to the design system
|
|
6
|
+
statement: An app component (`.tsx`/`.jsx` outside the design root) that renders markup MUST import at least one
|
|
7
|
+
design-system element — token, primitive, component, or template — so no UI surface is disconnected from the shared
|
|
8
|
+
system. Bun realization of `coder.design.orphan-ui`.
|
|
9
|
+
terms:
|
|
10
|
+
- term_id: design_root
|
|
11
|
+
text: 'a directory named `design`, `design_system`, or `design-system`. The first directory beneath it names the
|
|
12
|
+
layer: `tokens` or `foundations`, `primitives`, `components`, `templates`. A repository with no design root
|
|
13
|
+
is not judged (self-scoping): there is no system to connect to yet.'
|
|
14
|
+
- term_id: rendering_component
|
|
15
|
+
text: a component source file whose code (literals and comments masked) contains a closing tag or a self-closing
|
|
16
|
+
element.
|
|
17
|
+
content:
|
|
18
|
+
summary: Flags rendering component files outside the design root that import nothing from it.
|
|
19
|
+
normative_text: A screen built without the system is where the next one-off style lands. Requiring one design
|
|
20
|
+
import makes the connection explicit and reviewable.
|
|
21
|
+
fix_hint: Compose the screen from a template, component, or primitive — at minimum wrap it in the page template.
|
|
22
|
+
exceptions:
|
|
23
|
+
- Test files (`*.test.*`, `*.spec.*`) and vendored/build directories are never scanned.
|
|
24
|
+
- Non-rendering modules (routes, handlers, data) are not judged.
|
|
25
|
+
metadata:
|
|
26
|
+
aliases:
|
|
27
|
+
- BUN-DESIGN-CONNECTED-001
|
|
28
|
+
core_obligation: coder.design.orphan-ui
|
|
29
|
+
severity: 2
|
|
30
|
+
disposition: strict
|
|
31
|
+
introduced_in: 0.2.0
|
|
32
|
+
implementation:
|
|
33
|
+
type: validator
|
|
34
|
+
ref: bun_design_system_detector
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
schema_version: 1.1.0
|
|
2
|
+
rule_id: coder.bun.design-dependency-flow
|
|
3
|
+
kind: rule
|
|
4
|
+
status: active
|
|
5
|
+
name: Design-system layers import only their own layer or a lower one
|
|
6
|
+
statement: A file under a design root MUST import only from its own layer or a lower one in the hierarchy tokens/foundations
|
|
7
|
+
(0) <- primitives (1) <- components (2) <- templates (3). Upward edges — tokens importing primitives, primitives
|
|
8
|
+
importing components, components importing templates — are forbidden. Bun realization of the core obligation `coder.design.hierarchy-import`,
|
|
9
|
+
mirroring `coder.vite.design-dependency-flow`.
|
|
10
|
+
terms:
|
|
11
|
+
- term_id: design_root
|
|
12
|
+
text: 'a directory named `design`, `design_system`, or `design-system`. The first directory beneath it names the
|
|
13
|
+
layer: `tokens` or `foundations`, `primitives`, `components`, `templates`. A repository with no design root
|
|
14
|
+
is not judged (self-scoping): there is no system to connect to yet.'
|
|
15
|
+
- term_id: upward_import
|
|
16
|
+
text: an import from a design-system file into a HIGHER layer, resolved from a relative path or from an aliased
|
|
17
|
+
specifier that names the design directory (`@/design/components/Card`).
|
|
18
|
+
content:
|
|
19
|
+
summary: Resolves each import of a design-system file to its target layer and flags any edge that points upward.
|
|
20
|
+
normative_text: A hierarchy is only a hierarchy if dependencies flow one way. A primitive that imports a component
|
|
21
|
+
can no longer be reused without dragging that component along, and the layers collapse into one tangle.
|
|
22
|
+
fix_hint: 'Move the shared piece down to the lowest layer that needs it, or compose upward: the component imports
|
|
23
|
+
the primitive, never the reverse.'
|
|
24
|
+
exceptions:
|
|
25
|
+
- Test files (`*.test.*`, `*.spec.*`) and vendored/build directories are never scanned.
|
|
26
|
+
- Imports within the same layer are allowed.
|
|
27
|
+
- Files directly under the design root, outside any named layer, are not layered.
|
|
28
|
+
metadata:
|
|
29
|
+
aliases:
|
|
30
|
+
- BUN-DESIGN-DEPENDENCY-FLOW-001
|
|
31
|
+
core_obligation: coder.design.hierarchy-import
|
|
32
|
+
severity: 3
|
|
33
|
+
disposition: strict
|
|
34
|
+
introduced_in: 0.2.0
|
|
35
|
+
implementation:
|
|
36
|
+
type: validator
|
|
37
|
+
ref: bun_design_system_detector
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
schema_version: 1.1.0
|
|
2
|
+
rule_id: coder.bun.design-orphan-export
|
|
3
|
+
kind: rule
|
|
4
|
+
status: active
|
|
5
|
+
name: Design-system components have a consumer
|
|
6
|
+
statement: Every component a `primitives`, `components`, or `templates` file exports MUST be imported by at least
|
|
7
|
+
one other file — a higher design layer or app code — so the system stays lean. Re-exports through an `index.*`
|
|
8
|
+
barrel are not consumption. Bun realization of `coder.design.orphan-export`, counting consumers the way `coder.vite.design-orphan-ui`
|
|
9
|
+
does, so composition inside the system (a template using a component) is legitimate.
|
|
10
|
+
terms:
|
|
11
|
+
- term_id: design_root
|
|
12
|
+
text: 'a directory named `design`, `design_system`, or `design-system`. The first directory beneath it names the
|
|
13
|
+
layer: `tokens` or `foundations`, `primitives`, `components`, `templates`. A repository with no design root
|
|
14
|
+
is not judged (self-scoping): there is no system to connect to yet.'
|
|
15
|
+
- term_id: consumer
|
|
16
|
+
text: a non-barrel file whose import clause names the export, or imports it as a default under that name.
|
|
17
|
+
content:
|
|
18
|
+
summary: Collects capitalized exports of primitives/components/templates and flags those no other file imports.
|
|
19
|
+
normative_text: Unused components still have to be maintained, themed, and migrated. A design system grows by
|
|
20
|
+
adjudication, not by accretion of pieces nobody uses.
|
|
21
|
+
fix_hint: 'Use the component where it was meant to be used, or delete it. Unused chains surface one layer at a
|
|
22
|
+
time: remove the unused template and its component is reported next.'
|
|
23
|
+
exceptions:
|
|
24
|
+
- Test files (`*.test.*`, `*.spec.*`) and vendored/build directories are never scanned.
|
|
25
|
+
- Token/foundation values and `index.*` barrels are not judged.
|
|
26
|
+
- Only capitalized (component) exports are judged.
|
|
27
|
+
metadata:
|
|
28
|
+
aliases:
|
|
29
|
+
- BUN-DESIGN-ORPHAN-EXPORT-001
|
|
30
|
+
core_obligation: coder.design.orphan-export
|
|
31
|
+
severity: 2
|
|
32
|
+
disposition: strict
|
|
33
|
+
introduced_in: 0.2.0
|
|
34
|
+
implementation:
|
|
35
|
+
type: validator
|
|
36
|
+
ref: bun_design_system_detector
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
schema_version: 1.1.0
|
|
2
|
+
rule_id: coder.bun.design-primitives
|
|
3
|
+
kind: rule
|
|
4
|
+
status: active
|
|
5
|
+
name: App components compose design-system primitives for interactive controls
|
|
6
|
+
statement: An app component (`.tsx`/`.jsx` outside the design root) MUST render interactive controls through a design-system
|
|
7
|
+
primitive rather than a raw `<button>`, `<input>`, `<select>`, or `<textarea>`, so controls stay themed, accessible,
|
|
8
|
+
and consistent. Bun realization of `coder.design.primitives`, mirroring `coder.vite.design-primitives`.
|
|
9
|
+
terms:
|
|
10
|
+
- term_id: design_root
|
|
11
|
+
text: 'a directory named `design`, `design_system`, or `design-system`. The first directory beneath it names the
|
|
12
|
+
layer: `tokens` or `foundations`, `primitives`, `components`, `templates`. A repository with no design root
|
|
13
|
+
is not judged (self-scoping): there is no system to connect to yet.'
|
|
14
|
+
- term_id: raw_control
|
|
15
|
+
text: a lowercase `<button`, `<input`, `<select`, or `<textarea` element written in app code.
|
|
16
|
+
content:
|
|
17
|
+
summary: Flags raw interactive platform elements in component source outside the design root.
|
|
18
|
+
normative_text: Controls are where focus, disabled, error, and loading states live. Each raw control re-implements
|
|
19
|
+
them, and each re-implementation diverges.
|
|
20
|
+
fix_hint: 'Render the primitive and pass htmx attributes through it: `<Button hx-post="/orders">Place</Button>`.
|
|
21
|
+
Add the primitive to `design/primitives/` if it does not exist yet.'
|
|
22
|
+
exceptions:
|
|
23
|
+
- Test files (`*.test.*`, `*.spec.*`) and vendored/build directories are never scanned.
|
|
24
|
+
- The design system's own primitives render the raw elements and are not scanned.
|
|
25
|
+
- Plain `.html` templates cannot import components and are out of scope.
|
|
26
|
+
metadata:
|
|
27
|
+
aliases:
|
|
28
|
+
- BUN-DESIGN-PRIMITIVES-001
|
|
29
|
+
core_obligation: coder.design.primitives
|
|
30
|
+
severity: 2
|
|
31
|
+
disposition: strict
|
|
32
|
+
introduced_in: 0.2.0
|
|
33
|
+
implementation:
|
|
34
|
+
type: validator
|
|
35
|
+
ref: bun_design_system_detector
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
schema_version: 1.1.0
|
|
2
|
+
rule_id: coder.bun.design-token-color
|
|
3
|
+
kind: rule
|
|
4
|
+
status: active
|
|
5
|
+
name: UI files use color tokens, not raw color literals
|
|
6
|
+
statement: Outside the tokens layer, a UI file (`.tsx`, `.jsx`, `.html`, `.htm`, `.css`) MUST NOT write a raw color
|
|
7
|
+
literal — `#rgb`/`#rrggbb`/`#rrggbbaa`, `rgb()`/`rgba()`, `hsl()`/`hsla()` — in a CSS declaration, a `style` attribute,
|
|
8
|
+
a `<style>` block, a JSX `style={{…}}` object, or a `fill`/`stroke`/`bgcolor` attribute. Colors come from tokens
|
|
9
|
+
(`var(--…)`). Bun realization of `coder.design.token-color`, mirroring `coder.vite.design-token-color`.
|
|
10
|
+
terms:
|
|
11
|
+
- term_id: design_root
|
|
12
|
+
text: 'a directory named `design`, `design_system`, or `design-system`. The first directory beneath it names the
|
|
13
|
+
layer: `tokens` or `foundations`, `primitives`, `components`, `templates`. A repository with no design root
|
|
14
|
+
is not judged (self-scoping): there is no system to connect to yet.'
|
|
15
|
+
- term_id: raw_color_literal
|
|
16
|
+
text: 'a hex, rgb(a), or hsl(a) color written in a declaration value. Selectors are not values: `#fade:hover {`
|
|
17
|
+
is never read as a color.'
|
|
18
|
+
content:
|
|
19
|
+
summary: Reads declarations from stylesheets, style attributes, style blocks, and JSX style objects, and flags
|
|
20
|
+
raw color literals in their values.
|
|
21
|
+
normative_text: A raw color is a theme decision made in one place. It does not follow dark mode, it drifts from
|
|
22
|
+
the palette, and nobody can find it when the palette changes.
|
|
23
|
+
fix_hint: 'Define the color once as a custom property in the tokens layer and reference it: `color: var(--danger)`.'
|
|
24
|
+
exceptions:
|
|
25
|
+
- Test files (`*.test.*`, `*.spec.*`) and vendored/build directories are never scanned.
|
|
26
|
+
- Custom property definitions (`--danger: #b91c1c`) define tokens wherever they live and are not flagged.
|
|
27
|
+
- The tokens/foundations layer is where raw colors are defined and is not scanned.
|
|
28
|
+
- Named colors (`red`, `white`) and `currentColor`/`transparent` are not flagged.
|
|
29
|
+
metadata:
|
|
30
|
+
aliases:
|
|
31
|
+
- BUN-DESIGN-TOKEN-COLOR-001
|
|
32
|
+
core_obligation: coder.design.token-color
|
|
33
|
+
severity: 2
|
|
34
|
+
disposition: strict
|
|
35
|
+
introduced_in: 0.2.0
|
|
36
|
+
implementation:
|
|
37
|
+
type: validator
|
|
38
|
+
ref: bun_design_system_detector
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
schema_version: 1.1.0
|
|
2
|
+
rule_id: coder.bun.design-token-hardcoded
|
|
3
|
+
kind: rule
|
|
4
|
+
status: active
|
|
5
|
+
name: UI files take spacing, radii, and durations from foundations
|
|
6
|
+
statement: Outside the tokens layer, a UI file MUST NOT hardcode a spacing, inset, or border-radius length greater
|
|
7
|
+
than 1px (`margin`, `padding`, `gap`, `inset`, `top`/`right`/`bottom`/`left`, `border-radius`), nor a non-zero
|
|
8
|
+
duration in `transition`/`animation`. A bare number in a JSX style object for those properties counts as a length.
|
|
9
|
+
These values come from foundations tokens. Bun realization of `coder.design.token-hardcoded` and of the raw-pixel
|
|
10
|
+
part of `coder.design.foundations`.
|
|
11
|
+
terms:
|
|
12
|
+
- term_id: design_root
|
|
13
|
+
text: 'a directory named `design`, `design_system`, or `design-system`. The first directory beneath it names the
|
|
14
|
+
layer: `tokens` or `foundations`, `primitives`, `components`, `templates`. A repository with no design root
|
|
15
|
+
is not judged (self-scoping): there is no system to connect to yet.'
|
|
16
|
+
- term_id: visual_token
|
|
17
|
+
text: a spacing, radius, or motion value defined once in foundations and referenced as `var(--…)`.
|
|
18
|
+
content:
|
|
19
|
+
summary: Flags hardcoded lengths and durations in spacing, radius, and motion declarations across UI files, including
|
|
20
|
+
the design system's own primitives and components.
|
|
21
|
+
normative_text: Spacing and motion are the rhythm of an interface. Each hardcoded `13px` is a new step on a scale
|
|
22
|
+
nobody chose, and the rhythm erodes one component at a time.
|
|
23
|
+
fix_hint: 'Use the foundations scale: `padding: var(--space-3)`, `border-radius: var(--radius)`, `transition:
|
|
24
|
+
opacity var(--fast)`. Add a step to the scale in the tokens layer if none fits.'
|
|
25
|
+
exceptions:
|
|
26
|
+
- Test files (`*.test.*`, `*.spec.*`) and vendored/build directories are never scanned.
|
|
27
|
+
- 0 and 1px (hairline borders) are not design decisions and are allowed.
|
|
28
|
+
- Custom property definitions (`--space-3: 12px`) define tokens wherever they live and are not flagged.
|
|
29
|
+
- The tokens/foundations layer defines these values and is not scanned.
|
|
30
|
+
- Widths, heights, and font sizes are out of scope for this rule.
|
|
31
|
+
metadata:
|
|
32
|
+
aliases:
|
|
33
|
+
- BUN-DESIGN-TOKEN-HARDCODED-001
|
|
34
|
+
core_obligation: coder.design.token-hardcoded
|
|
35
|
+
severity: 2
|
|
36
|
+
disposition: strict
|
|
37
|
+
introduced_in: 0.2.0
|
|
38
|
+
implementation:
|
|
39
|
+
type: validator
|
|
40
|
+
ref: bun_design_system_detector
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
schema_version: 1.1.0
|
|
2
|
+
rule_id: coder.bun.design-tokens-pure
|
|
3
|
+
kind: rule
|
|
4
|
+
status: active
|
|
5
|
+
name: Token and foundation files hold values, not behaviour
|
|
6
|
+
statement: A code file in the `tokens` or `foundations` layer MUST contain only values — palette, spacing, radii,
|
|
7
|
+
motion, scales — and MUST NOT import a renderer (`hono/jsx`, `react`, `preact`, `@kitajs/html`), render markup,
|
|
8
|
+
or branch with `if`/`for`/`while`/`switch`. Bun realization of `coder.design.foundations` (the bottom of the hierarchy
|
|
9
|
+
is pure), mirroring `coder.vite.design-tokens-pure`.
|
|
10
|
+
terms:
|
|
11
|
+
- term_id: design_root
|
|
12
|
+
text: 'a directory named `design`, `design_system`, or `design-system`. The first directory beneath it names the
|
|
13
|
+
layer: `tokens` or `foundations`, `primitives`, `components`, `templates`. A repository with no design root
|
|
14
|
+
is not judged (self-scoping): there is no system to connect to yet.'
|
|
15
|
+
- term_id: pure_token
|
|
16
|
+
text: 'a declaration whose value is data: a string, number, or object of them, including `var(--…)` references.'
|
|
17
|
+
content:
|
|
18
|
+
summary: Scans token/foundation code files for renderer imports, rendered markup, and control flow.
|
|
19
|
+
normative_text: Tokens are what every other layer depends on. Once they render or decide, the bottom of the hierarchy
|
|
20
|
+
has behaviour, and every primitive inherits it.
|
|
21
|
+
fix_hint: Keep the value in the token file and move the markup to `primitives/` or the logic to the component
|
|
22
|
+
that needs it.
|
|
23
|
+
exceptions:
|
|
24
|
+
- Test files (`*.test.*`, `*.spec.*`) and vendored/build directories are never scanned.
|
|
25
|
+
- '`.css` token files are values by construction and are not scanned by this rule.'
|
|
26
|
+
metadata:
|
|
27
|
+
aliases:
|
|
28
|
+
- BUN-DESIGN-TOKENS-PURE-001
|
|
29
|
+
core_obligation: coder.design.foundations
|
|
30
|
+
severity: 2
|
|
31
|
+
disposition: strict
|
|
32
|
+
introduced_in: 0.2.0
|
|
33
|
+
implementation:
|
|
34
|
+
type: validator
|
|
35
|
+
ref: bun_design_system_detector
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
schema_version: 1.1.0
|
|
2
|
+
rule_id: coder.bun.design-wagons-import
|
|
3
|
+
kind: rule
|
|
4
|
+
status: active
|
|
5
|
+
name: The design system never imports app or wagon code
|
|
6
|
+
statement: A file under a design root MUST NOT import a relative path that resolves outside the design root. Wagons
|
|
7
|
+
and app code import FROM the design system, never the reverse, so the system stays wagon-agnostic. Bun realization
|
|
8
|
+
of the outward half of `coder.design.hierarchy-import`, mirroring `coder.vite.design-wagons-import`.
|
|
9
|
+
terms:
|
|
10
|
+
- term_id: design_root
|
|
11
|
+
text: 'a directory named `design`, `design_system`, or `design-system`. The first directory beneath it names the
|
|
12
|
+
layer: `tokens` or `foundations`, `primitives`, `components`, `templates`. A repository with no design root
|
|
13
|
+
is not judged (self-scoping): there is no system to connect to yet.'
|
|
14
|
+
- term_id: outward_import
|
|
15
|
+
text: a relative import from a design-system file whose target lies outside every design root.
|
|
16
|
+
content:
|
|
17
|
+
summary: Flags relative imports from a design-system file that leave the design root.
|
|
18
|
+
normative_text: 'A design system that reaches into a wagon has become part of that wagon: it cannot be reused
|
|
19
|
+
by the next one, and a change to the wagon breaks the system.'
|
|
20
|
+
fix_hint: Pass the value in as a prop, or move the shared type or constant into the design system (or a shared
|
|
21
|
+
commons module) so the dependency points inward.
|
|
22
|
+
exceptions:
|
|
23
|
+
- Test files (`*.test.*`, `*.spec.*`) and vendored/build directories are never scanned.
|
|
24
|
+
- Bare package specifiers (`hono/jsx`) are outside the project graph and are not judged.
|
|
25
|
+
- Aliased specifiers are judged only when they name a design directory.
|
|
26
|
+
metadata:
|
|
27
|
+
aliases:
|
|
28
|
+
- BUN-DESIGN-WAGONS-IMPORT-001
|
|
29
|
+
core_obligation: coder.design.hierarchy-import
|
|
30
|
+
severity: 3
|
|
31
|
+
disposition: strict
|
|
32
|
+
introduced_in: 0.2.0
|
|
33
|
+
implementation:
|
|
34
|
+
type: validator
|
|
35
|
+
ref: bun_design_system_detector
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
schema_version: 1.1.0
|
|
2
|
+
kind: implementation
|
|
3
|
+
subtype: validator
|
|
4
|
+
implementation_id: bun_design_system_detector
|
|
5
|
+
targets_workspace: atdd.workspace.bun
|
|
6
|
+
contract_version: 1.1.0
|
|
7
|
+
emits_rule_ids:
|
|
8
|
+
- coder.bun.design-dependency-flow
|
|
9
|
+
- coder.bun.design-wagons-import
|
|
10
|
+
- coder.bun.design-tokens-pure
|
|
11
|
+
- coder.bun.design-token-color
|
|
12
|
+
- coder.bun.design-token-hardcoded
|
|
13
|
+
- coder.bun.design-primitives
|
|
14
|
+
- coder.bun.design-connected
|
|
15
|
+
- coder.bun.design-orphan-export
|
|
16
|
+
entrypoint: detect.mjs
|
|
17
|
+
report: detect.mjs
|
|
18
|
+
realizes_convention:
|
|
19
|
+
# A family detector owns every rule it emits: the Bun realization of the core
|
|
20
|
+
# coder.design.* obligations (tokens <- primitives <- components <- templates).
|
|
21
|
+
- coder.bun.design-dependency-flow
|
|
22
|
+
- coder.bun.design-wagons-import
|
|
23
|
+
- coder.bun.design-tokens-pure
|
|
24
|
+
- coder.bun.design-token-color
|
|
25
|
+
- coder.bun.design-token-hardcoded
|
|
26
|
+
- coder.bun.design-primitives
|
|
27
|
+
- coder.bun.design-connected
|
|
28
|
+
- coder.bun.design-orphan-export
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Shared design-system model for the bun_design_system_detector family.
|
|
2
|
+
//
|
|
3
|
+
// A DESIGN ROOT is a directory named `design`, `design_system`, or `design-system`.
|
|
4
|
+
// The first directory beneath it names the file's LAYER, bottom to top:
|
|
5
|
+
//
|
|
6
|
+
// tokens | foundations (0) ← primitives (1) ← components (2) ← templates (3)
|
|
7
|
+
//
|
|
8
|
+
// Imports may point to the same layer or a lower one. Every rule self-scopes: a
|
|
9
|
+
// repository with no design root is not judged, because there is no system to
|
|
10
|
+
// connect to yet.
|
|
11
|
+
import { dirname, extname, normalize, resolve, sep } from "node:path";
|
|
12
|
+
import { walk, readText, maskLiteralsAndComments } from "../../../lib/scan.mjs";
|
|
13
|
+
|
|
14
|
+
export const DESIGN_DIRS = new Set(["design", "design_system", "design-system"]);
|
|
15
|
+
export const LAYERS = { tokens: 0, foundations: 0, primitives: 1, components: 2, templates: 3 };
|
|
16
|
+
export const UI_EXT = new Set([".tsx", ".jsx", ".html", ".htm", ".css"]);
|
|
17
|
+
export const CODE_EXT = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts"]);
|
|
18
|
+
const ALL_EXT = new Set([...UI_EXT, ...CODE_EXT]);
|
|
19
|
+
|
|
20
|
+
/** The design root containing `file` and the file's layer, or null when outside any design root. */
|
|
21
|
+
export function designOf(file) {
|
|
22
|
+
const parts = normalize(file).split(sep);
|
|
23
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
24
|
+
if (!DESIGN_DIRS.has(parts[i])) continue;
|
|
25
|
+
const layerName = parts[i + 1];
|
|
26
|
+
const layer = i + 1 < parts.length - 1 && layerName in LAYERS ? LAYERS[layerName] : null;
|
|
27
|
+
return { root: parts.slice(0, i + 1).join(sep) || sep, layerName: layer === null ? null : layerName, layer };
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Every source/UI file under the scan roots, once. */
|
|
33
|
+
export function collect(roots, excludes) {
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
for (const root of roots) for (const file of walk(root, excludes, ALL_EXT)) seen.add(resolve(file));
|
|
36
|
+
return [...seen];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const IMPORT_RE = /(?:^|[\n;])\s*(?:import|export)\s+(?:type\s+)?([\s\S]*?)\s*from\s*(['"])([^'"]+)\2|(?:^|[\n;])\s*import\s*(['"])([^'"]+)\4|\bimport\(\s*(['"])([^'"]+)\6\s*\)/g;
|
|
40
|
+
|
|
41
|
+
/** Import statements of a code file: specifier, the clause text, and its offset. */
|
|
42
|
+
export function importsOf(text) {
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const m of text.matchAll(IMPORT_RE)) {
|
|
45
|
+
const specifier = m[3] ?? m[5] ?? m[7];
|
|
46
|
+
out.push({ specifier, clause: m[1] ?? "", index: m.index + m[0].indexOf(specifier) });
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Where an import points: a path for relative specifiers, or — for aliased/absolute
|
|
53
|
+
* specifiers — the design segment it names (`@/design/primitives/Text`). Bare
|
|
54
|
+
* package names resolve to null.
|
|
55
|
+
*/
|
|
56
|
+
export function target(file, specifier) {
|
|
57
|
+
if (specifier.startsWith(".")) return { path: resolve(dirname(file), specifier) };
|
|
58
|
+
const segments = specifier.split("/");
|
|
59
|
+
const at = segments.findIndex((s) => DESIGN_DIRS.has(s));
|
|
60
|
+
if (at === -1) return null;
|
|
61
|
+
const layerName = segments[at + 1];
|
|
62
|
+
return { alias: true, layerName: layerName in LAYERS ? layerName : null, layer: layerName in LAYERS ? LAYERS[layerName] : null };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The design location an import resolves into, or null when it leaves every design root. */
|
|
66
|
+
export function designTarget(file, specifier) {
|
|
67
|
+
const t = target(file, specifier);
|
|
68
|
+
if (!t) return null;
|
|
69
|
+
if (t.alias) return { layer: t.layer, layerName: t.layerName };
|
|
70
|
+
const d = designOf(t.path + (extname(t.path) ? "" : sep + "_"));
|
|
71
|
+
return d ? { layer: d.layer, layerName: d.layerName, root: d.root, path: t.path } : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function masked(file) {
|
|
75
|
+
const text = readText(file);
|
|
76
|
+
if (text === null) return null;
|
|
77
|
+
return { text, code: extname(file) === ".css" ? text.replace(/\/\*[\s\S]*?\*\//g, (c) => c.replace(/[^\n]/g, " ")) : maskLiteralsAndComments(text) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const isUi = (file) => UI_EXT.has(extname(file));
|
|
81
|
+
export const isComponentSource = (file) => extname(file) === ".tsx" || extname(file) === ".jsx";
|
|
82
|
+
export const hasDesignRoot = (files) => files.some((file) => designOf(file));
|
|
83
|
+
|
|
84
|
+
const kebab = (prop) => prop.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
|
|
85
|
+
const CSS_DECL_RE = /([-a-zA-Z]+)\s*:\s*([^;{}]+)(?=[;}])/g;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Property/value pairs written in a UI file, with absolute offsets. CSS declarations
|
|
89
|
+
* must end in `;` or `}` so selectors (`a:hover {`) are never read as values; JSX
|
|
90
|
+
* style-object keys are converted to kebab-case, and a bare number is kept as-is.
|
|
91
|
+
*/
|
|
92
|
+
export function declarations(file, text) {
|
|
93
|
+
const out = [];
|
|
94
|
+
const css = (body, offset) => { for (const m of body.matchAll(CSS_DECL_RE)) out.push({ prop: m[1].toLowerCase(), value: m[2].trim(), index: offset + m.index }); };
|
|
95
|
+
if (extname(file) === ".css") { css(text.replace(/\/\*[\s\S]*?\*\//g, (c) => c.replace(/[^\n]/g, " ")), 0); return out; }
|
|
96
|
+
for (const m of text.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)) css(m[1], m.index + m[0].indexOf(m[1]));
|
|
97
|
+
for (const m of text.matchAll(/\bstyle\s*=\s*(["'])([\s\S]*?)\1/g)) css(m[2] + ";", m.index + m[0].indexOf(m[2]));
|
|
98
|
+
for (const m of text.matchAll(/\bstyle\s*=\s*\{\{/g)) {
|
|
99
|
+
let depth = 0, j = m.index + m[0].length - 2;
|
|
100
|
+
for (; j < text.length; j++) { if (text[j] === "{") depth++; else if (text[j] === "}" && --depth === 0) break; }
|
|
101
|
+
const body = text.slice(m.index, j + 1);
|
|
102
|
+
for (const d of body.matchAll(/([A-Za-z]+)\s*:\s*(?:(["'`])([^"'`]*)\2|(-?\d+(?:\.\d+)?)\b)/g)) out.push({ prop: kebab(d[1]), value: d[3] ?? d[4], numeric: d[4] !== undefined, index: m.index + d.index });
|
|
103
|
+
}
|
|
104
|
+
for (const m of text.matchAll(/\b(fill|stroke|bgcolor)\s*=\s*(["'])([^"']*)\2/g)) out.push({ prop: m[1], value: m[3], index: m.index });
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Run a check over the scan roots, self-scoped to repositories that have a design root. */
|
|
109
|
+
export function scope(roots, excludes) {
|
|
110
|
+
const files = collect(roots, excludes);
|
|
111
|
+
return hasDesignRoot(files) ? files : [];
|
|
112
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"design_dependency_flow": "coder.bun.design-dependency-flow",
|
|
3
|
+
"design_wagons_import": "coder.bun.design-wagons-import",
|
|
4
|
+
"design_tokens_pure": "coder.bun.design-tokens-pure",
|
|
5
|
+
"design_token_color": "coder.bun.design-token-color",
|
|
6
|
+
"design_token_hardcoded": "coder.bun.design-token-hardcoded",
|
|
7
|
+
"design_primitives": "coder.bun.design-primitives",
|
|
8
|
+
"design_connected": "coder.bun.design-connected",
|
|
9
|
+
"design_orphan_export": "coder.bun.design-orphan-export"
|
|
10
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Detector: coder.bun.design-connected
|
|
3
|
+
// Every app component that renders markup imports at least one design-system
|
|
4
|
+
// element, so no UI surface is disconnected from the shared system.
|
|
5
|
+
import { readRoots, readExcludes, readText, emit, locate } from "../../../lib/scan.mjs";
|
|
6
|
+
import { scope, designOf, isComponentSource, masked, importsOf, designTarget } from "./_design.mjs";
|
|
7
|
+
|
|
8
|
+
const RULE_ID = "coder.bun.design-connected";
|
|
9
|
+
const violations = [];
|
|
10
|
+
for (const file of scope(readRoots(), readExcludes())) {
|
|
11
|
+
if (!isComponentSource(file) || designOf(file)) continue;
|
|
12
|
+
const m = masked(file);
|
|
13
|
+
if (!m) continue;
|
|
14
|
+
const renders = m.code.search(/<\/[A-Za-z]|\/>/);
|
|
15
|
+
if (renders === -1 || importsOf(m.text).some((imp) => designTarget(file, imp.specifier))) continue;
|
|
16
|
+
violations.push({ rule_id: RULE_ID, file, ...locate(m.text, renders), evidence: "component renders markup but imports nothing from the design system" });
|
|
17
|
+
}
|
|
18
|
+
process.stderr.write(`bun-detector[design-connected]: ${violations.length} violation(s)\n`);
|
|
19
|
+
emit(violations);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Detector: coder.bun.design-dependency-flow
|
|
3
|
+
// A design-system file imports only its own layer or a lower one:
|
|
4
|
+
// tokens/foundations ← primitives ← components ← templates.
|
|
5
|
+
import { readRoots, readExcludes, readText, emit, locate } from "../../../lib/scan.mjs";
|
|
6
|
+
import { scope, designOf, importsOf, designTarget } from "./_design.mjs";
|
|
7
|
+
|
|
8
|
+
const RULE_ID = "coder.bun.design-dependency-flow";
|
|
9
|
+
const violations = [];
|
|
10
|
+
for (const file of scope(readRoots(), readExcludes())) {
|
|
11
|
+
const here = designOf(file);
|
|
12
|
+
if (!here || here.layer === null) continue;
|
|
13
|
+
const text = readText(file);
|
|
14
|
+
for (const imp of importsOf(text ?? "")) {
|
|
15
|
+
const to = designTarget(file, imp.specifier);
|
|
16
|
+
if (to && to.layer !== null && to.layer > here.layer) violations.push({ rule_id: RULE_ID, file, ...locate(text, imp.index), evidence: `${here.layerName} imports upward into ${to.layerName} ("${imp.specifier}"); depend on the same or a lower layer` });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
process.stderr.write(`bun-detector[design-dependency-flow]: ${violations.length} violation(s)\n`);
|
|
20
|
+
emit(violations);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Detector: coder.bun.design-orphan-export
|
|
3
|
+
// Every component a primitives/components/templates file exports is imported by at
|
|
4
|
+
// least one other file: a higher design layer or app code. Re-exports through an
|
|
5
|
+
// index.* barrel are not consumption. Dead exports are removed, not kept.
|
|
6
|
+
import { readRoots, readExcludes, readText, emit, locate } from "../../../lib/scan.mjs";
|
|
7
|
+
import { basename, extname } from "node:path";
|
|
8
|
+
import { scope, designOf, importsOf, designTarget, masked, CODE_EXT } from "./_design.mjs";
|
|
9
|
+
|
|
10
|
+
const RULE_ID = "coder.bun.design-orphan-export";
|
|
11
|
+
const files = scope(readRoots(), readExcludes());
|
|
12
|
+
const consumed = new Set();
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
if (basename(file).startsWith("index.")) continue;
|
|
15
|
+
for (const imp of importsOf(readText(file) ?? "")) {
|
|
16
|
+
if (!designTarget(file, imp.specifier)) continue;
|
|
17
|
+
const named = imp.clause.match(/\{([^}]*)\}/)?.[1] ?? "";
|
|
18
|
+
for (const n of named.split(",")) { const name = n.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0]; if (name) consumed.add(name); }
|
|
19
|
+
const fallback = imp.clause.replace(/\{[^}]*\}/, "").replace(/\*\s+as\s+\w+/, "").split(",")[0].trim();
|
|
20
|
+
if (/^[A-Z]\w*$/.test(fallback)) consumed.add(fallback);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const violations = [];
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
const here = designOf(file);
|
|
26
|
+
if (!here || !(here.layer >= 1) || !CODE_EXT.has(extname(file)) || basename(file).startsWith("index.")) continue;
|
|
27
|
+
const m = masked(file);
|
|
28
|
+
for (const e of (m?.code ?? "").matchAll(/export\s+(?:default\s+)?(?:async\s+)?(?:function|const|let|class)\s+([A-Z]\w*)/g)) {
|
|
29
|
+
if (!consumed.has(e[1])) violations.push({ rule_id: RULE_ID, file, ...locate(m.text, e.index), evidence: `${here.layerName} export ${e[1]} is imported by no other file` });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
process.stderr.write(`bun-detector[design-orphan-export]: ${violations.length} violation(s)\n`);
|
|
33
|
+
emit(violations);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Detector: coder.bun.design-primitives
|
|
3
|
+
// App components (outside the design root) render interactive controls through the
|
|
4
|
+
// design-system primitive, not raw <button>/<input>/<select>/<textarea>.
|
|
5
|
+
import { readRoots, readExcludes, readText, emit, locate } from "../../../lib/scan.mjs";
|
|
6
|
+
import { scope, designOf, isComponentSource, masked } from "./_design.mjs";
|
|
7
|
+
|
|
8
|
+
const RULE_ID = "coder.bun.design-primitives";
|
|
9
|
+
const violations = [];
|
|
10
|
+
for (const file of scope(readRoots(), readExcludes())) {
|
|
11
|
+
if (!isComponentSource(file) || designOf(file)) continue;
|
|
12
|
+
const m = masked(file);
|
|
13
|
+
for (const c of (m?.code ?? "").matchAll(/<(button|input|select|textarea)\b/g)) violations.push({ rule_id: RULE_ID, file, ...locate(m.text, c.index), evidence: `raw <${c[1]}> in app code; compose the design-system primitive instead` });
|
|
14
|
+
}
|
|
15
|
+
process.stderr.write(`bun-detector[design-primitives]: ${violations.length} violation(s)\n`);
|
|
16
|
+
emit(violations);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Detector: coder.bun.design-token-color
|
|
3
|
+
// Outside the tokens layer, UI files (.tsx/.jsx/.html/.css) take colors from tokens
|
|
4
|
+
// (`var(--…)`), never raw hex, rgb(), or hsl() literals. Defining a custom
|
|
5
|
+
// property (`--danger: #b91c1c`) is defining a token, not using a raw value.
|
|
6
|
+
import { readRoots, readExcludes, readText, emit, locate } from "../../../lib/scan.mjs";
|
|
7
|
+
import { scope, designOf, isUi, declarations } from "./_design.mjs";
|
|
8
|
+
|
|
9
|
+
const RULE_ID = "coder.bun.design-token-color";
|
|
10
|
+
const COLOR_RE = /#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?)\s*\(/;
|
|
11
|
+
const violations = [];
|
|
12
|
+
for (const file of scope(readRoots(), readExcludes())) {
|
|
13
|
+
if (!isUi(file) || designOf(file)?.layer === 0) continue;
|
|
14
|
+
const text = readText(file);
|
|
15
|
+
for (const d of declarations(file, text ?? "")) {
|
|
16
|
+
if (d.prop.startsWith("--")) continue;
|
|
17
|
+
const hit = d.value.match(COLOR_RE);
|
|
18
|
+
if (hit) violations.push({ rule_id: RULE_ID, file, ...locate(text, d.index), evidence: `${d.prop} uses the raw color ${hit[0].replace(/\s*\($/, "()")}; reference a color token (var(--…)) instead` });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
process.stderr.write(`bun-detector[design-token-color]: ${violations.length} violation(s)\n`);
|
|
22
|
+
emit(violations);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Detector: coder.bun.design-token-hardcoded
|
|
3
|
+
// Outside the tokens layer, spacing, radii, and durations come from foundations.
|
|
4
|
+
// 0 and 1px (hairline borders) are not design decisions and are allowed. Custom
|
|
5
|
+
// property definitions (`--space-3: 12px`) define tokens and are never judged.
|
|
6
|
+
import { readRoots, readExcludes, readText, emit, locate } from "../../../lib/scan.mjs";
|
|
7
|
+
import { scope, designOf, isUi, declarations } from "./_design.mjs";
|
|
8
|
+
|
|
9
|
+
const RULE_ID = "coder.bun.design-token-hardcoded";
|
|
10
|
+
const LENGTH_PROP = /^(margin|padding|gap|row-gap|column-gap|inset|top|right|bottom|left|border-radius)(-|$)/;
|
|
11
|
+
const DURATION_PROP = /^(transition|animation)(-|$)/;
|
|
12
|
+
const violations = [];
|
|
13
|
+
const report = (file, text, d, value) => violations.push({ rule_id: RULE_ID, file, ...locate(text, d.index), evidence: `${d.prop}: ${value} is hardcoded; use a foundations token (var(--…))` });
|
|
14
|
+
for (const file of scope(readRoots(), readExcludes())) {
|
|
15
|
+
if (!isUi(file) || designOf(file)?.layer === 0) continue;
|
|
16
|
+
const text = readText(file);
|
|
17
|
+
for (const d of declarations(file, text ?? "")) {
|
|
18
|
+
if (LENGTH_PROP.test(d.prop)) {
|
|
19
|
+
if (d.numeric) { if (Math.abs(Number(d.value)) > 1) report(file, text, d, d.value); continue; }
|
|
20
|
+
const px = [...d.value.matchAll(/(-?\d*\.?\d+)px\b/g)].find((m) => Math.abs(Number(m[1])) > 1);
|
|
21
|
+
if (px) report(file, text, d, px[0]);
|
|
22
|
+
} else if (DURATION_PROP.test(d.prop)) {
|
|
23
|
+
const time = [...d.value.matchAll(/(\d*\.?\d+)(ms|s)\b/g)].find((m) => Number(m[1]) > 0);
|
|
24
|
+
if (time) report(file, text, d, time[0]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
process.stderr.write(`bun-detector[design-token-hardcoded]: ${violations.length} violation(s)\n`);
|
|
29
|
+
emit(violations);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Detector: coder.bun.design-tokens-pure
|
|
3
|
+
// Files in the tokens/foundations layer hold values only: no JSX runtime import,
|
|
4
|
+
// no rendered markup, no control flow.
|
|
5
|
+
import { readRoots, readExcludes, readText, emit, locate } from "../../../lib/scan.mjs";
|
|
6
|
+
import { extname } from "node:path";
|
|
7
|
+
import { scope, designOf, importsOf, masked, CODE_EXT } from "./_design.mjs";
|
|
8
|
+
|
|
9
|
+
const RULE_ID = "coder.bun.design-tokens-pure";
|
|
10
|
+
const RENDERERS = /^(react|react-dom|preact|hono\/jsx|hono\/jsx\/.*|@kitajs\/html)$/;
|
|
11
|
+
const violations = [];
|
|
12
|
+
const report = (file, text, index, evidence) => violations.push({ rule_id: RULE_ID, file, ...locate(text, index), evidence });
|
|
13
|
+
for (const file of scope(readRoots(), readExcludes())) {
|
|
14
|
+
if (designOf(file)?.layer !== 0 || !CODE_EXT.has(extname(file))) continue;
|
|
15
|
+
const m = masked(file);
|
|
16
|
+
if (!m) continue;
|
|
17
|
+
for (const imp of importsOf(m.text)) if (RENDERERS.test(imp.specifier)) report(file, m.text, imp.index, `token file imports the renderer "${imp.specifier}"; tokens are values, not widgets`);
|
|
18
|
+
const jsx = m.code.search(/<\/[A-Za-z]|\/>/);
|
|
19
|
+
if (jsx !== -1) report(file, m.text, jsx, "token file renders markup; move it to primitives");
|
|
20
|
+
for (const c of m.code.matchAll(/\b(if|for|while|switch)\s*\(/g)) report(file, m.text, c.index, `token file branches with ${c[1]}; tokens are pure values`);
|
|
21
|
+
}
|
|
22
|
+
process.stderr.write(`bun-detector[design-tokens-pure]: ${violations.length} violation(s)\n`);
|
|
23
|
+
emit(violations);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Detector: coder.bun.design-wagons-import
|
|
3
|
+
// The design system is wagon-agnostic: a file under a design root never imports
|
|
4
|
+
// application or wagon code. Wagons import FROM the design system, never the reverse.
|
|
5
|
+
import { readRoots, readExcludes, readText, emit, locate } from "../../../lib/scan.mjs";
|
|
6
|
+
import { scope, designOf, importsOf, target, designTarget } from "./_design.mjs";
|
|
7
|
+
|
|
8
|
+
const RULE_ID = "coder.bun.design-wagons-import";
|
|
9
|
+
const violations = [];
|
|
10
|
+
for (const file of scope(readRoots(), readExcludes())) {
|
|
11
|
+
if (!designOf(file)) continue;
|
|
12
|
+
const text = readText(file);
|
|
13
|
+
for (const imp of importsOf(text ?? "")) {
|
|
14
|
+
if (target(file, imp.specifier)?.path && !designTarget(file, imp.specifier)) violations.push({ rule_id: RULE_ID, file, ...locate(text, imp.index), evidence: `design-system file imports "${imp.specifier}", outside the design root; the design system must not depend on app code` });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
process.stderr.write(`bun-detector[design-wagons-import]: ${violations.length} violation(s)\n`);
|
|
18
|
+
emit(violations);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// FAMILY validator: bun_design_system_detector
|
|
3
|
+
// Runs each member check (checks/*.mjs) VERBATIM as a subprocess and merges their
|
|
4
|
+
// RAW v1.1 reports into one — ONE implementation realizing a family of rule_ids
|
|
5
|
+
// (the Core pattern; see frontend.workspace.runtime's families).
|
|
6
|
+
//
|
|
7
|
+
// Members are spawned with `process.execPath`, which under this provider IS the
|
|
8
|
+
// bun binary, so a member may be authored as .mjs OR .ts with no build step.
|
|
9
|
+
// Files whose name begins with `_` are skipped: they are shared helpers, not
|
|
10
|
+
// checks. (The node-runtime families have no such convention because each of
|
|
11
|
+
// their checks re-implements its own walker; this provider factors the walk into
|
|
12
|
+
// lib/scan.mjs instead.)
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
import { readFileSync, writeFileSync, mkdtempSync, readdirSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join, dirname } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const reportPath = process.env.ATDD_VIOLATIONS_REPORT;
|
|
21
|
+
if (!reportPath) {
|
|
22
|
+
process.stderr.write("family: ATDD_VIOLATIONS_REPORT not set\n");
|
|
23
|
+
process.exit(2);
|
|
24
|
+
}
|
|
25
|
+
const checks = readdirSync(join(here, "checks"))
|
|
26
|
+
.filter((f) => (f.endsWith(".mjs") || f.endsWith(".ts")) && !f.startsWith("_"))
|
|
27
|
+
.sort();
|
|
28
|
+
const td = mkdtempSync(join(tmpdir(), "atdd-bun-fam-"));
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const c of checks) {
|
|
31
|
+
const rep = join(td, c + ".json");
|
|
32
|
+
try {
|
|
33
|
+
execFileSync(process.execPath, [join(here, "checks", c)], {
|
|
34
|
+
env: { ...process.env, ATDD_VIOLATIONS_REPORT: rep },
|
|
35
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
36
|
+
});
|
|
37
|
+
} catch {
|
|
38
|
+
/* a member may exit non-zero; still try to read its report */
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
out.push(...JSON.parse(readFileSync(rep, "utf8")).violations);
|
|
42
|
+
} catch {}
|
|
43
|
+
}
|
|
44
|
+
writeFileSync(reportPath, JSON.stringify({ violations: out }, null, 2), "utf8");
|
|
45
|
+
process.stderr.write("family bun_design_system_detector: " + out.length + " violation(s)\n");
|
|
46
|
+
process.exit(0);
|
package/detectors/bun_design_system_detector/fixtures/clean/app/src/design/components/Card.tsx
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Text } from "../primitives/Text";
|
|
2
|
+
import { space } from "../tokens/spacing";
|
|
3
|
+
|
|
4
|
+
export function Card(props: { title: string; children: unknown }) {
|
|
5
|
+
return <section class="card" style={{ padding: space[4], borderRadius: "var(--radius)" }}><Text>{props.title}</Text>{props.children}</section>;
|
|
6
|
+
}
|
package/detectors/bun_design_system_detector/fixtures/clean/app/src/design/primitives/Button.tsx
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { colors } from "../tokens/colors";
|
|
2
|
+
import { space } from "../tokens/spacing";
|
|
3
|
+
|
|
4
|
+
export function Button(props: { children: unknown; [attr: string]: unknown }) {
|
|
5
|
+
return <button class="btn" style={{ color: colors.ink, padding: space[2] }} {...props}>{props.children}</button>;
|
|
6
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const colors = { ink: "var(--ink)", line: "var(--line)" } as const;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const space = { 2: "var(--space-2)", 4: "var(--space-4)" } as const;
|
package/detectors/bun_design_system_detector/fixtures/dirty/connected/src/design/tokens/colors.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const colors = { ink: "var(--ink)" } as const;
|
package/detectors/bun_design_system_detector/fixtures/dirty/token_color/src/design/tokens/colors.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root { --danger: #b91c1c; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<ul style="margin: 16px; border-radius: 6px"><li>row</li></ul>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root { --space-3: 12px; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const orderStatus = ["open", "closed"] as const;
|
package/package.json
CHANGED
package/src/enforce.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
|
|
5
|
-
export type Profile = "traceability" | "docs" | "planner" | "coder" | "tester" | "security" | "architecture" | "metrics" | "runtime" | "interlocking" | "htmx" | "all";
|
|
5
|
+
export type Profile = "traceability" | "docs" | "planner" | "coder" | "tester" | "security" | "architecture" | "metrics" | "runtime" | "interlocking" | "htmx" | "design" | "all";
|
|
6
6
|
|
|
7
7
|
export type Violation = {
|
|
8
8
|
rule_id: string;
|
|
@@ -24,7 +24,7 @@ const profiles: Record<Exclude<Profile, "all">, string[]> = {
|
|
|
24
24
|
traceability: ["atdd_traceability_closure"],
|
|
25
25
|
docs: ["planner_docs_capability"],
|
|
26
26
|
planner: ["planner_plan_integrity", "planner_schema_validation", "planner_static_validators"],
|
|
27
|
-
coder: ["bun_green_traceability_detector", "bun_clean_architecture_detector", "bun_ts_metrics_detector", "bun_fullstack_detector"],
|
|
27
|
+
coder: ["bun_green_traceability_detector", "bun_clean_architecture_detector", "bun_ts_metrics_detector", "bun_fullstack_detector", "bun_design_system_detector"],
|
|
28
28
|
tester: ["bun_tester_discipline_detector"],
|
|
29
29
|
security: ["bun_security_hygiene_detector"],
|
|
30
30
|
architecture: ["bun_clean_architecture_detector"],
|
|
@@ -32,6 +32,7 @@ const profiles: Record<Exclude<Profile, "all">, string[]> = {
|
|
|
32
32
|
runtime: ["bun_fullstack_detector"],
|
|
33
33
|
interlocking: ["bun_interlocking_binding", "bun_interlocking_coverage", "bun_interlocking_infrastructure"],
|
|
34
34
|
htmx: ["htmx_hypermedia_detector", "htmx_tester_detector"],
|
|
35
|
+
design: ["bun_design_system_detector"],
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
/** Profile names accepted by the CLI and public integrations. */
|
|
@@ -10,7 +10,7 @@ Conventions live in `node_modules/@afokapu/atdd-bun/` (`planner-nodes/nodes/`, `
|
|
|
10
10
|
2. RED — For each acceptance, write a test headed `// URN: test:{wagon}:{feature}:{ACC-ID}` and `// Phase: RED` that fails for the missing behaviour (`tester.bun.red-*`). Gate: `bun run atdd-bun tester`.
|
|
11
11
|
3. GREEN — Write the least code that passes; each source file carries `URN: component:{wagon}:{feature}:{Name}:{side}:{layer}` and a `Tested-By:` block (`coder.bun.green-*`). Gate: `bun test`.
|
|
12
12
|
4. SMOKE — Prove the SMOKE acceptance through the real entry point with no mocks or spies, asserting only on observable output: HTTP, markup, stdout, exit code (`tester.bun.smoke-*`, `planner.smoke.*`). Gate: `bun run atdd-bun tester`.
|
|
13
|
-
5. REFACTOR — With tests green, reduce complexity, fix layering, and remove security faults until the rules pass, without changing behaviour (`coder.bun.complexity-*`, `quality-*`, `composition-*`, `security-*`). Gate: `bun run atdd-bun coder security`.
|
|
13
|
+
5. REFACTOR — With tests green, reduce complexity, fix layering, and remove security faults until the rules pass, without changing behaviour (`coder.bun.complexity-*`, `quality-*`, `composition-*`, `design-*`, `security-*`). Gate: `bun run atdd-bun coder security`.
|
|
14
14
|
6. TRACE — Every acceptance has a test, every test resolves to a declared acceptance, every source file resolves to its tests. Gate: `bun run atdd-bun traceability`, then `bun run atdd-bun all`.
|
|
15
15
|
|
|
16
16
|
When a gate fails, open `<rule_id>.convention.yaml` for the reported rule ID and fix the artifact. Never skip, suppress, or edit a convention to get green.
|