baldart 4.67.1 → 4.68.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/CHANGELOG.md +18 -0
- package/README.md +2 -0
- package/VERSION +1 -1
- package/bin/baldart.js +12 -0
- package/framework/.claude/agents/code-reviewer.md +14 -2
- package/framework/.claude/agents/qa-sentinel.md +12 -0
- package/framework/.claude/agents/ui-expert.md +9 -0
- package/framework/.claude/skills/design-system-init/SKILL.md +68 -6
- package/framework/.claude/skills/design-system-init/scripts/serialize-spec.mjs +63 -0
- package/framework/.claude/skills/new/references/implement.md +31 -5
- package/framework/.claude/skills/prd/references/ui-design-phase.md +59 -10
- package/framework/.claude/skills/ui-design/SKILL.md +2 -0
- package/framework/.claude/workflows/new-final-review.js +1 -1
- package/framework/.claude/workflows/new2.js +3 -0
- package/framework/agents/card-schema.md +28 -0
- package/framework/agents/component-manifest-schema.md +26 -1
- package/framework/agents/design-system-protocol.md +101 -0
- package/framework/agents/index.md +1 -0
- package/framework/routines/ds-drift.routine.yml +9 -1
- package/package.json +1 -1
- package/src/commands/ds-gate.js +39 -0
- package/src/utils/ds-reuse-gate.js +335 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to BALDART will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [4.68.0] - 2026-06-24
|
|
9
|
+
|
|
10
|
+
**Closed-Set Selection Policy — the third semantic layer that makes the "third header" structurally impossible.** Found on real `mayo` PRDs: every PRD ignored the two existing canonical headers (`SectionHeader` depth-0, `DrilldownHeader` depth-1+) and wrote new ones, requiring an expensive consolidation epic (`FEAT-0045` + an ADR) and leaving `Header.tsx` deprecated-but-live in 3 call-sites. Root cause (confirmed by a BALDART trace + 40-source 2026 research): the registry has a token layer and a component layer, but **no layer asserts the *boundary* of a category** — so adding a third header violated no rule, only an undiscovered convention. The registry-first cascade was advisory at generation-time + reactive at review-time; nothing prevented the duplicate at build-time. The 2026 market confirms the gap: no vendor (Figma Code Connect, Storybook MCP, shadcn) hard-gates the reuse decision, and no deterministic tool detects a semantically-equivalent rewrite — but BALDART already ships the machine-readable manifest signature, so it is "one manifest-signature-diff away".
|
|
11
|
+
|
|
12
|
+
**MINOR** — adds a capability (the `baldart ds-gate` verb, three manifest fields, the `/prd` reconciliation gate); rides on `features.has_design_system` (NO new config key — the schema-change propagation rule does NOT apply), additive, no install change. Three slices.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Selection-policy manifest fields (Slice A)** — `canonical_for` (`"<family>@<context>"` roles a component is THE canonical answer for), `use_when` (the selection predicate, lifted from `must_rules`), and `selection_closed` (the role family is a closed set) join the AGENTIC HEAD in `component-manifest-schema.md`. The serializer (`serialize-spec.mjs`) emits them + aggregates a generated **§ "Selection Policy"** `families:` block into `INDEX.md` (a family is `closed` iff ≥1 member declares it) — the closure lives in the HEADs, the INDEX stays a pure derived router.
|
|
17
|
+
- **`baldart ds-gate` (Slice A — the deterministic build-time gate)** — BALDART-owned, the design-system analogue of the i18n gate. Reads the **committed** `INDEX.md` § Selection Policy (baseline closure) + the diff and BLOCKS (exit 1) on `DS_CLOSED_SET_VIOLATION`: a change that adds a new canonical component in a CLOSED family. Zero-false-positive (it checks declared `canonical_for` against baseline members). `DS_CLOSED_SET_SUSPECT` (name heuristic) and `DS_DUPLICATE_PRIMITIVE` (Slice C manifest signature-diff, Jaccard ≥0.7) are advisory warnings. Self-scoping + self-skipping (registry/policy absent → clean no-op); SKIPs, never silently passes. Logic in `src/utils/ds-reuse-gate.js`, verb in `src/commands/ds-gate.js`. Wired into `qa-sentinel`, `/new` Phase 2 (`implement.md` Step 8), `new2`, the Final review, and `code-reviewer`.
|
|
18
|
+
- **`/prd` Component Reconciliation gate (Slice B — the human-in-the-loop prevention)** — `ui-design-phase.md`'s passive "Component Registry Lookup" becomes an active *match-before-generate* gate: build a Mockup Element Inventory → classify each region by ROLE (closing the name-vs-need vocabulary gap) → ask ONLY on ambiguity (`REUSE+VARIANT` / `NEW`, never plain reuse) in the prohibition+alternative form → persist. A closed-family hit surfaces *"this becomes the standard?"*; a confirmed standard writes `canonical_for`/`selection_closed` + opens a migration follow-up card (never migrates existing views in-PRD).
|
|
19
|
+
- **`component_bindings` card field (Slice B)** — the resolved `region → component (reuse|reuse-variant|new)` map (`card-schema.md`) is the authority `ui-expert` implements against: the mockup becomes a visual reference, the bindings decide which component each region IS. Resolves the mockup-vs-registry contradiction in `implement.md` (the old "implementation MUST match the approved mockup 1:1" that let the approved-mockup pixels beat the dusty registry).
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **`/design-system-init` derives the closed-set on upgrade — confidence-gated (Slice A)** — the bootstrap auto-fills only what is unambiguous (`canonical_for`/`use_when` lifted from each component's `must_rules`, e.g. *"MUST use only for depth 0"* → `page-header@depth-0`) and **interacts for the judgement calls**: it COLLECTS closure candidates (families that look like they partition all cases) + suspected duplicates (manifest signature-overlap ≥0.7, reusing `signatureDuplicates`; deprecated-but-used primitives) and confirms them with the user in ONE consolidated `AskUserQuestion` (step 4c — family-level, ~5–10 confirmations for a ~100-component registry, never per-component). `selection_closed` is written only for confirmed families; a "consolidate" answer opens a migration follow-up card. Unattended / Codex runs propose-only — never auto-close (closing a family without a human is the governance bypass the policy forbids). Idempotent: a settled family is not re-asked. The bootstrap-time twin of the `/prd` reconciliation gate.
|
|
24
|
+
- **Closed-set enforcement across the discipline (Slice B/C)** — the Component Discovery Cascade now queries by role first (`design-system-protocol.md` § Closed-Set Selection Policy, the textual SSOT); `ui-expert` (cascade step 6 + Implementation briefing), `ui-design` (closed-set query + bindings output), `code-reviewer` (rule 4 closed-set BLOCKER + Post-Intervention check (e) + the new drift code, UI-reinvention fixes route to `ui-expert`), and the weekly `ds-drift` routine (Selection-Policy integrity audit) all consume it. New canonical drift code `DS_CLOSED_SET_VIOLATION`.
|
|
25
|
+
|
|
8
26
|
## [4.67.1] - 2026-06-24
|
|
9
27
|
|
|
10
28
|
**Rule C is now i18n-aware — a presentational card that only changes labels no longer gets forced to `balanced`.** Found on a real `mayo` run: FEAT-0050 is a "PURAMENTE PRESENTAZIONALE" fidelity-drift epic, yet its child cards ran the full per-card review cluster (full-depth Codex + simplify) instead of the lighter UI path. Root cause: the i18n layer (v4.52.0) silently broke the v4.x UI-presentational carve-out in `prd-card-writer.md § Rule C`. The LIGHT branch (b) requires "≤3 files, every one a component/style file", and the SKIP rule requires "all files .md/.yml/CSS" — but with `features.has_i18n: true`, every label change forces an edit to **all** native locale files (`it.ts`/`en.ts`/`de.ts`/`es.ts`/`fr.ts`), which blows the ≤3 count and injects non-component `.ts` files. So a genuinely pixels-only card was structurally incapable of being classified `light`/`skip`.
|
package/README.md
CHANGED
|
@@ -229,6 +229,8 @@ Bundled skills:
|
|
|
229
229
|
|
|
230
230
|
When `features.has_design_system: true`, every UI-touching agent/skill/command (`ui-expert`, `ui-design`, `frontend-design`, `code-reviewer`, `/design-review`) follows the same BLOCKING cascade: read `${paths.design_system}/INDEX.md` (Authority Matrix), `tokens-reference.md` (token contract), and `components/<Name>.md` for every primitive in scope — BEFORE producing or reviewing any UI. New components must reuse from the registry or ship their per-component spec in the same change. Hardcoded color/shadow/radius/spacing values are HIGH findings. The textual SSOT for the protocol lives in [`framework/agents/design-system-protocol.md`](framework/agents/design-system-protocol.md). Projects without a registry can bootstrap one with `/design-system-init`, which inventories existing primitives, extracts tokens from global styles + Tailwind config, scaffolds the registry, and flips the flag.
|
|
231
231
|
|
|
232
|
+
**Closed-Set Selection Policy (v4.68.0).** The registry has a token layer and a component layer, but until now nothing asserted the *boundary* of a category — so an agent designing from a mockup could write a third "header" when two canonical ones already cover every case, and the duplicate was only caught (reactively) at review. The closed-set policy adds the missing third layer: each component spec declares the presentational **role** it is canonical for (`canonical_for: [page-header@depth-0]`) and whether that role family is **closed** (`selection_closed`), aggregated into a generated `INDEX.md` § "Selection Policy". A closed family means *introducing a new canonical member is a governance decision, not an implementation act*. Three layers enforce it: (1) **`/prd` Component Reconciliation** — a human-in-the-loop *match-before-generate* gate that binds each mockup region to an existing component (or a governed NEW one) and asks *"this becomes the standard?"* only on genuine ambiguity, persisting the decision into the card's `component_bindings`; (2) **`baldart ds-gate`** — a deterministic, zero-false-positive build-time gate that BLOCKS (`DS_CLOSED_SET_VIOLATION`) any change adding a new canonical in a closed family (wired into `qa-sentinel` / `/new` / `code-reviewer`, the design-system analogue of the i18n gate); (3) the existing review + weekly `ds-drift` safety net. Rides on `features.has_design_system` — no new config key; `/design-system-init --upgrade` derives the closed sets from your existing `must_rules`. This closes a gap no 2026 design-to-code vendor hard-gates (Figma Code Connect, Storybook MCP, shadcn are all advisory).
|
|
233
|
+
|
|
232
234
|
### End-to-End Review BLOCKING Gate (new in v3.18.0)
|
|
233
235
|
|
|
234
236
|
When `features.has_e2e_review: true`, the `/new` orchestrator Phase 2.6 invokes the new `/e2e-review` skill on every UI card (auto-skipped on backend-only cards). The skill is a **deterministic, BLOCKING** orchestrator that combines functional E2E (Playwright spec written by `coder`, executed via `playwright-skill`) with visual fidelity diff (`visual-fidelity-verifier` multimodal agent) and aggregates findings under a strict severity gate. This replaces the legacy advisory pair (Phase 2.6 conditional E2E + Phase 2.7 non-blocking design review) that left the user manually re-verifying every screen.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
1
|
+
4.68.0
|
package/bin/baldart.js
CHANGED
|
@@ -174,6 +174,18 @@ program
|
|
|
174
174
|
await teardownCommand({ cwd: options.cwd, json: !!options.json });
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
+
program
|
|
178
|
+
.command('ds-gate')
|
|
179
|
+
.description('Enforce the closed-set Selection Policy: BLOCK (exit 1) when the change adds a new canonical component in a CLOSED role family (the "third header" failure mode). Registry/policy absent → clean no-op. Rides on features.has_design_system.')
|
|
180
|
+
.option('--cwd <path>', 'Project root (defaults to the current directory).')
|
|
181
|
+
.option('--base <ref>', 'Baseline git ref to read the committed Selection Policy + diff against (defaults to merge-base with the trunk, else HEAD).')
|
|
182
|
+
.option('--json', 'Machine-readable output: emit a single JSON result object on stdout')
|
|
183
|
+
.action(async (options) => {
|
|
184
|
+
const dsGate = require('../src/commands/ds-gate');
|
|
185
|
+
const code = await dsGate.run({ cwd: options.cwd, base: options.base, json: !!options.json });
|
|
186
|
+
if (typeof code === 'number' && code !== 0) process.exitCode = code;
|
|
187
|
+
});
|
|
188
|
+
|
|
177
189
|
const tokensGroup = program
|
|
178
190
|
.command('tokens')
|
|
179
191
|
.description('Design-token operations. The DTCG `.tokens.json` (paths.design_tokens) is the SSOT; `tokens build` regenerates the stack-native artifacts (tokens.ts / CSS vars) from it.');
|
|
@@ -75,6 +75,14 @@ Concrete enforcement contract for the review pass:
|
|
|
75
75
|
- Motion honors the project's reduced-motion variant rules.
|
|
76
76
|
4. Re-implementation of an existing registry primitive is a HIGH finding —
|
|
77
77
|
the diff must reuse the primitive or propose a new variant in the registry.
|
|
78
|
+
**Closed-set violation (BLOCKER):** a new canonical component in a role family
|
|
79
|
+
marked `closed` in `INDEX.md` § "Selection Policy" (the "third header" failure
|
|
80
|
+
mode) is a `DS_CLOSED_SET_VIOLATION` — run `baldart ds-gate --json` to confirm
|
|
81
|
+
deterministically; pair the finding with the canonical member to reuse (the
|
|
82
|
+
Augment-measured prohibition+alternative form). A `DS_DUPLICATE_PRIMITIVE`
|
|
83
|
+
signature-overlap warning from the gate is a HIGH finding to verify (real
|
|
84
|
+
rewrite vs legitimately-similar primitive). UI-presentation reinvention fixes
|
|
85
|
+
route to `ui-expert` (the UI domain owner), not `coder`.
|
|
78
86
|
5. New primitives introduced without their `components/<Name>.md` spec are a
|
|
79
87
|
HIGH finding.
|
|
80
88
|
6. Violations of project-declared design-system MUST rules are HIGH — they
|
|
@@ -92,9 +100,13 @@ Concrete enforcement contract for the review pass:
|
|
|
92
100
|
(c) new/changed token ⇒ DTCG `.tokens.json` updated + outputs regenerated
|
|
93
101
|
(`baldart tokens build`) when `paths.design_tokens` is set, else the
|
|
94
102
|
project token source/reference,
|
|
95
|
-
(d) silently-existing primitive reused ⇒ INDEX router entry added in the same diff
|
|
103
|
+
(d) silently-existing primitive reused ⇒ INDEX router entry added in the same diff,
|
|
104
|
+
(e) **closed-set integrity** ⇒ no new canonical component in a `closed` role
|
|
105
|
+
family (run `baldart ds-gate`); a violation is a `DS_CLOSED_SET_VIOLATION`
|
|
106
|
+
BLOCKER, not a HIGH.
|
|
96
107
|
Each failing condition is a HIGH finding with the canonical code
|
|
97
|
-
(`DS_INDEX_DRIFT` / `DS_COMPONENT_STALE` / `DS_TOKENS_DRIFT`
|
|
108
|
+
(`DS_INDEX_DRIFT` / `DS_COMPONENT_STALE` / `DS_TOKENS_DRIFT` /
|
|
109
|
+
`DS_CLOSED_SET_VIOLATION`). This is the
|
|
98
110
|
third and final gate after `ui-design`/`ui-expert`'s own post-intervention
|
|
99
111
|
check and the weekly `ds-drift` routine; if it trips here at review time,
|
|
100
112
|
it means the upstream checks were skipped — flag the protocol violation
|
|
@@ -49,6 +49,18 @@ string slipped through). If NEITHER resolves, report `i18n-lint: SKIP (no
|
|
|
49
49
|
anti-hardcoded gate wired — run \`baldart doctor\`)` as an informational note,
|
|
50
50
|
never a pass — do not claim the gate ran when it did not.
|
|
51
51
|
|
|
52
|
+
**DS closed-set gate (since v4.68.0):** when `features.has_design_system: true`
|
|
53
|
+
AND the diff touches UI (files under `${paths.components_primitives}` /
|
|
54
|
+
`${paths.components}`, or any component spec under
|
|
55
|
+
`${paths.design_system}/components/`), run **`npx baldart ds-gate --json`** (from
|
|
56
|
+
the worktree). It enforces the closed-set Selection Policy — a non-zero exit
|
|
57
|
+
(`blockingCount > 0`, a `DS_CLOSED_SET_VIOLATION`) is a real **FAIL**: the change
|
|
58
|
+
introduces a new canonical component in a CLOSED role family (the "third header"
|
|
59
|
+
failure mode). `DS_CLOSED_SET_SUSPECT` warnings are informational, never a FAIL.
|
|
60
|
+
The verb is self-scoping (it reads the committed Selection Policy + the diff) and
|
|
61
|
+
self-skipping (registry/policy absent → clean no-op) — no diff-scoping needed. If
|
|
62
|
+
it reports `skipped`, note it informationally, never claim a pass.
|
|
63
|
+
|
|
52
64
|
Default pre-commit gates (MUST pass before any commit verdict) — used when no
|
|
53
65
|
`toolchain.commands.*` is configured:
|
|
54
66
|
1. Lint: `npx eslint --max-warnings=0 <changed source files>` (or project equivalent)
|
|
@@ -334,6 +334,15 @@ project context layer at invocation time:
|
|
|
334
334
|
`${paths.components_primitives}/*` → external catalog (shadcn / 21st.dev /
|
|
335
335
|
Radix) → create from scratch. Stop at the first hit. Document which
|
|
336
336
|
existing primitives you will reuse and which (if any) need creation.
|
|
337
|
+
**Query by ROLE first, then closure (BLOCKING):** before deciding an element
|
|
338
|
+
needs a new component, consult `INDEX.md` § "Selection Policy". If the element
|
|
339
|
+
maps to a role in a **closed** family (e.g. `page-header`), you MUST reuse that
|
|
340
|
+
family's canonical member — creating a new one is a `DS_CLOSED_SET_VIOLATION`
|
|
341
|
+
(`baldart ds-gate` blocks it). When a card carries `component_bindings` (the
|
|
342
|
+
`/prd` reconciliation map), those bindings are authoritative — honor them over a
|
|
343
|
+
contrary reading of the mockup. A genuinely-new member of a closed family is a
|
|
344
|
+
governance decision: STOP and surface it, never ship it silently. "Document"
|
|
345
|
+
here means *act on a mismatch*, not just list it.
|
|
337
346
|
2. **Read `${paths.ui_guidelines}`** for current design standards
|
|
338
347
|
3. Establish the user type (from `identity.audience_segments`)
|
|
339
348
|
4. If brand-themed: request brand inputs if not provided
|
|
@@ -87,10 +87,12 @@ precedence caveats: `framework/agents/effort-protocol.md`.
|
|
|
87
87
|
acceptable (advisory), never a failure.
|
|
88
88
|
4. **Enrich — AGENTIC, AS DATA (never hand-write YAML).** Launch `doc-reviewer`
|
|
89
89
|
to produce the agentic fields (`purpose`, `a11y`, `token_bindings`, `related`,
|
|
90
|
-
`must_rules
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
`must_rules`, **plus the selection-policy fields `canonical_for` / `use_when` /
|
|
91
|
+
`selection_closed`** — step 4b) **as JSON values**, NOT as text typed into a
|
|
92
|
+
`.md`. In UPGRADE mode, lift them from each spec's existing prose / **salvage**
|
|
93
|
+
them from the current HEAD even if it is malformed. Never clobber a filled
|
|
94
|
+
agentic field on re-run. Under Codex (no subagents) this degrades to empty
|
|
95
|
+
fields + a TODO note.
|
|
94
96
|
**`token_bindings` MUST be chosen from the REAL token vocabulary** — read the
|
|
95
97
|
id list from `${paths.design_tokens}` (the DTCG `.tokens.json`) and pick exact
|
|
96
98
|
ids (`typography.fontWeight.medium`, `zIndex.overlay`, `space.sm`), never
|
|
@@ -104,6 +106,56 @@ precedence caveats: `framework/agents/effort-protocol.md`.
|
|
|
104
106
|
`purpose`/`a11y`/`must_rules` (design-time intent the code does not encode),
|
|
105
107
|
then reconcile against the implemented source. Fallback to source-only when no
|
|
106
108
|
PRD/Inventory exists.
|
|
109
|
+
4b. **Derive the closed-set Selection Policy — confidence-gated (the third semantic
|
|
110
|
+
layer).** Per `agents/design-system-protocol.md § Closed-Set Selection Policy`
|
|
111
|
+
(`doc-reviewer` owns this; degrades on Codex). This step is **bootstrap-shaped**:
|
|
112
|
+
it auto-fills what is unambiguous and **interacts with the user** for the
|
|
113
|
+
judgement calls — it must NOT silently impose closures or merge duplicates. Three
|
|
114
|
+
confidence tiers:
|
|
115
|
+
- **HIGH confidence → auto-fill, no question** (reversible agentic fields):
|
|
116
|
+
- **`canonical_for`** — the `"<family>@<context>"` role(s) the component is THE
|
|
117
|
+
canonical answer for, when its name + `purpose` + an existing `must_rules`
|
|
118
|
+
boundary state it cleanly (e.g. a `must_rules` *"MUST use only for depth 0"*
|
|
119
|
+
→ `page-header@depth-0`). Most components serve no named role — leave empty.
|
|
120
|
+
- **`use_when`** — lift the selection predicate verbatim from that `must_rules`
|
|
121
|
+
boundary.
|
|
122
|
+
- **GOVERNANCE / AMBIGUOUS → COLLECT, do not write yet** (gather into a proposal
|
|
123
|
+
for step 4c, never auto-decide):
|
|
124
|
+
- **Closure candidates** — a family whose members look like they *partition all
|
|
125
|
+
cases* (e.g. the two-header depth model: `{SectionHeader@depth-0,
|
|
126
|
+
DrilldownHeader@depth-1+}`). Closing a family is a **governance act** — record
|
|
127
|
+
it as a *proposal* with the evidence (the ADR / "these are the only N"
|
|
128
|
+
statement / the partition), NOT a written `selection_closed: true`.
|
|
129
|
+
- **Suspected duplicates** — components whose manifest **signature** (props ∪
|
|
130
|
+
variants ∪ composes) overlaps an existing one ≥0.7, or a `status: deprecated`
|
|
131
|
+
primitive still used, or two components that plainly serve the same role
|
|
132
|
+
(e.g. a legacy `Header` alongside `SectionHeader`). Reuse the gate's
|
|
133
|
+
signature-diff (`src/utils/ds-reuse-gate.js → signatureDuplicates`) to surface
|
|
134
|
+
them. Each is a question: *which is canonical, consolidate or keep both?*
|
|
135
|
+
- **LOW confidence → leave empty + TODO** (no question — don't over-ask): a
|
|
136
|
+
component whose role you cannot infer stays outside the policy.
|
|
137
|
+
UPGRADE re-runs PRESERVE a human-set `selection_closed` and any human answer from
|
|
138
|
+
a prior run (idempotent — never re-ask a settled family).
|
|
139
|
+
4c. **Selection Policy confirmation gate (interactive).** Before writing files,
|
|
140
|
+
present the step-4b proposal **once, consolidated** — family-level, never
|
|
141
|
+
per-component (mayo's ~100 components yield only a handful of candidate families
|
|
142
|
+
+ duplicates, so this is ~5–10 confirmations total, not a barrage):
|
|
143
|
+
- **Interactive session (a human is present):** one batched `AskUserQuestion`
|
|
144
|
+
round. For each closure candidate: *"Propongo di CHIUDERE la famiglia
|
|
145
|
+
`page-header` con membri {SectionHeader@depth-0, DrilldownHeader@depth-1+} —
|
|
146
|
+
diventano gli unici header canonici (un terzo richiederà governance). Confermi /
|
|
147
|
+
correggi i membri-contesto / lascia aperta?"*. For each suspected duplicate:
|
|
148
|
+
*"`Header` sembra duplicare `SectionHeader` — è canonico, da consolidare, o
|
|
149
|
+
coesistono?"*. Write `selection_closed: true` + the agreed members ONLY for
|
|
150
|
+
confirmed closures; record the rationale (and open a migration follow-up card
|
|
151
|
+
for any "consolidate" answer — never migrate call-sites inside the bootstrap).
|
|
152
|
+
- **Unattended (Codex / no-TTY / autonomous / `BALDART_AUTONOMOUS`):** do NOT
|
|
153
|
+
close any family and do NOT merge any duplicate — write the proposals into the
|
|
154
|
+
step-8 next-steps report + leave `selection_closed: false` + a TODO, so a human
|
|
155
|
+
ratifies later. Auto-closing without a human is exactly the governance bypass
|
|
156
|
+
the policy forbids.
|
|
157
|
+
This makes the bootstrap the bootstrap-time twin of the `/prd` Component
|
|
158
|
+
Reconciliation gate: auto where confident, ask where it is the user's call.
|
|
107
159
|
5. **Bootstrap the DTCG token SSOT.** See § Token SSOT bootstrap below.
|
|
108
160
|
6. **Write the files — DETERMINISTICALLY (the HEAD is NEVER model-authored).**
|
|
109
161
|
Merge the deterministic JSON (step 3) with the agentic JSON (step 4) per
|
|
@@ -157,8 +209,18 @@ precedence caveats: `framework/agents/effort-protocol.md`.
|
|
|
157
209
|
`composes`, `category`. Empty fields on a non-TS stack are advisory, not errors.
|
|
158
210
|
|
|
159
211
|
3. **Agentic enrichment (`doc-reviewer`).** Fill `purpose`, `a11y`,
|
|
160
|
-
`token_bindings`, `related`, `must_rules
|
|
161
|
-
|
|
212
|
+
`token_bindings`, `related`, `must_rules`, and the selection-policy fields
|
|
213
|
+
`canonical_for` / `use_when` (§ What This Skill Does step 4b — auto-fill HIGH-
|
|
214
|
+
confidence roles; COLLECT closure + duplicate candidates). UPGRADE: lift from
|
|
215
|
+
each spec's existing prose. Never clobber a filled field. Under Codex: leave
|
|
216
|
+
empty + TODO.
|
|
217
|
+
|
|
218
|
+
3b. **Selection Policy confirmation gate (step 4c).** Present the closure +
|
|
219
|
+
suspected-duplicate proposals ONCE, consolidated (family-level). Interactive:
|
|
220
|
+
one batched `AskUserQuestion` → write `selection_closed` only for confirmed
|
|
221
|
+
families (+ migration follow-up cards for "consolidate" answers). Unattended /
|
|
222
|
+
Codex: propose into the report + leave open, never auto-close. Idempotent — a
|
|
223
|
+
settled family is not re-asked.
|
|
162
224
|
|
|
163
225
|
4. **Token SSOT bootstrap** — § Token SSOT bootstrap (derive `.tokens.json`,
|
|
164
226
|
set outputs, `baldart tokens build`, round-trip verify).
|
|
@@ -80,6 +80,9 @@ export function emitHead(c) {
|
|
|
80
80
|
L.push(`a11y: ${scalar(c.a11y ?? '')}`);
|
|
81
81
|
L.push(list('related', c.related));
|
|
82
82
|
L.push(list('must_rules', c.must_rules));
|
|
83
|
+
L.push(list('canonical_for', c.canonical_for));
|
|
84
|
+
L.push(`use_when: ${scalar(c.use_when ?? '')}`);
|
|
85
|
+
L.push(`selection_closed: ${c.selection_closed ? 'true' : 'false'}`);
|
|
83
86
|
L.push(`last_verified: ${scalar(c.last_verified || TODAY)}`);
|
|
84
87
|
L.push(`owner: ${scalar(c.owner || 'doc-reviewer')}`);
|
|
85
88
|
return L.join('\n');
|
|
@@ -94,12 +97,71 @@ export function emitSpec(c) {
|
|
|
94
97
|
return `---\n${emitHead(c)}\n---\n\n${prose.replace(/\s*$/, '')}\n`;
|
|
95
98
|
}
|
|
96
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Aggregate the closed-set Selection Policy from the component HEADs.
|
|
102
|
+
* A role family (the `<family>` part of a `canonical_for` `"<family>@<context>"`
|
|
103
|
+
* entry) is `closed` iff ≥1 of its members carries `selection_closed: true`.
|
|
104
|
+
* Returns a deterministic { family: { closed, members:[{component,context}] } } map.
|
|
105
|
+
*/
|
|
106
|
+
export function selectionPolicy(components) {
|
|
107
|
+
const fam = {};
|
|
108
|
+
for (const c of components) {
|
|
109
|
+
for (const role of c.canonical_for || []) {
|
|
110
|
+
const at = String(role).indexOf('@');
|
|
111
|
+
const family = (at >= 0 ? role.slice(0, at) : role).trim();
|
|
112
|
+
const context = at >= 0 ? role.slice(at + 1).trim() : '';
|
|
113
|
+
if (!family) continue;
|
|
114
|
+
(fam[family] = fam[family] || { closed: false, members: [] });
|
|
115
|
+
fam[family].members.push({ component: c.name, context });
|
|
116
|
+
if (c.selection_closed) fam[family].closed = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Determinism: sort families + members.
|
|
120
|
+
const out = {};
|
|
121
|
+
for (const k of Object.keys(fam).sort()) {
|
|
122
|
+
out[k] = {
|
|
123
|
+
closed: fam[k].closed,
|
|
124
|
+
members: fam[k].members.sort((a, b) => (a.component + a.context).localeCompare(b.component + b.context)),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Machine-parseable Selection Policy section for the INDEX (or '' if no roles). */
|
|
131
|
+
export function emitSelectionPolicy(components) {
|
|
132
|
+
const fam = selectionPolicy(components);
|
|
133
|
+
const names = Object.keys(fam);
|
|
134
|
+
if (names.length === 0) return '';
|
|
135
|
+
const L = [];
|
|
136
|
+
L.push('## Selection Policy');
|
|
137
|
+
L.push('');
|
|
138
|
+
L.push('Closed role families: introducing a NEW canonical member is a governance');
|
|
139
|
+
L.push('decision, not an implementation act — `baldart ds-gate` blocks it. Open');
|
|
140
|
+
L.push('families stay advisory (Component Discovery Cascade + review). This block is');
|
|
141
|
+
L.push('GENERATED from each spec HEAD\'s `canonical_for` + `selection_closed`.');
|
|
142
|
+
L.push('');
|
|
143
|
+
L.push('```yaml');
|
|
144
|
+
L.push('# baldart-generated selection-policy — do not hand-edit (edit the component HEADs)');
|
|
145
|
+
L.push('families:');
|
|
146
|
+
for (const f of names) {
|
|
147
|
+
L.push(` ${scalar(f)}:`);
|
|
148
|
+
L.push(` closed: ${fam[f].closed ? 'true' : 'false'}`);
|
|
149
|
+
L.push(' members:');
|
|
150
|
+
for (const m of fam[f].members) {
|
|
151
|
+
L.push(` - { component: ${scalar(m.component)}, context: ${scalar(m.context || '')} }`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
L.push('```');
|
|
155
|
+
return L.join('\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
97
158
|
/** Thin INDEX router from the component set. */
|
|
98
159
|
export function emitIndex(components, brand = 'Project') {
|
|
99
160
|
const rows = components
|
|
100
161
|
.slice()
|
|
101
162
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
102
163
|
.map((c) => `| ${c.name} | ${c.source} | ${(c.purpose || '').replace(/\|/g, '\\|').slice(0, 80)} | [components/${c.name}.md](components/${c.name}.md) |`);
|
|
164
|
+
const policy = emitSelectionPolicy(components);
|
|
103
165
|
return [
|
|
104
166
|
'<!-- baldart-generated router — regenerated from components/*.md HEAD. Do not hand-edit. -->',
|
|
105
167
|
'---',
|
|
@@ -119,6 +181,7 @@ export function emitIndex(components, brand = 'Project') {
|
|
|
119
181
|
'|-----------|--------|---------|------|',
|
|
120
182
|
...rows,
|
|
121
183
|
'',
|
|
184
|
+
...(policy ? [policy, ''] : []),
|
|
122
185
|
].join('\n');
|
|
123
186
|
}
|
|
124
187
|
|
|
@@ -111,8 +111,20 @@
|
|
|
111
111
|
|
|
112
112
|
### Design Reference (UI cards only — include if card has links.design)
|
|
113
113
|
Design file: [path from card's links.design field]
|
|
114
|
-
Read the design.html file and use it as the visual reference for your
|
|
115
|
-
The design was approved by
|
|
114
|
+
Read the design.html file and use it as the **visual reference** for your
|
|
115
|
+
implementation (layout, spacing, hierarchy, copy). The design was approved by
|
|
116
|
+
the user — your implementation MUST match its appearance.
|
|
117
|
+
|
|
118
|
+
**The mockup is a visual reference, NOT a license to re-create components.**
|
|
119
|
+
When the card carries `component_bindings` (the `/prd` Component Reconciliation
|
|
120
|
+
map — see `agents/card-schema.md`), THAT map is authoritative over *which
|
|
121
|
+
component each region IS*: a region bound `action: reuse → SectionHeader` MUST
|
|
122
|
+
be implemented by reusing `SectionHeader`, even if the mockup's pixels look like
|
|
123
|
+
a fresh header. Honor the binding over any contrary reading of the mockup. Only
|
|
124
|
+
`action: new` entries (confirmed with the user at `/prd` time) may introduce a
|
|
125
|
+
component. A card with UI scope and `features.has_design_system: true` but NO
|
|
126
|
+
`component_bindings` → run the cascade below as your own reconciliation before
|
|
127
|
+
writing code (do not assume a mockup affordance is a new component).
|
|
116
128
|
|
|
117
129
|
### i18n constraint (include when `features.has_i18n: true`)
|
|
118
130
|
No user-facing string may be hardcoded. Externalize every label through the
|
|
@@ -142,10 +154,18 @@
|
|
|
142
154
|
component named in the mockup, the requirements, or `files_likely_touched`.
|
|
143
155
|
5. For EACH pattern in scope, read the relevant
|
|
144
156
|
`${paths.design_system}/patterns/<topic>.md`.
|
|
157
|
+
6. Read `${paths.design_system}/INDEX.md` § "Selection Policy" (the closed role
|
|
158
|
+
families). For EACH region you are about to build, if it maps to a role in a
|
|
159
|
+
**closed** family you MUST reuse that family's canonical member — never write
|
|
160
|
+
a new component for it (the "third header" failure mode). A genuinely-new
|
|
161
|
+
member of a closed family is a governance decision, not yours to make: STOP
|
|
162
|
+
and surface it as a blocker. `baldart ds-gate` enforces this deterministically
|
|
163
|
+
at the gate (Step 8) — a `DS_CLOSED_SET_VIOLATION` means you created one.
|
|
145
164
|
|
|
146
165
|
State, in your first response, which Authority Matrix rows govern this
|
|
147
|
-
card, which component specs you read (step 4),
|
|
148
|
-
read (step 5)
|
|
166
|
+
card, which component specs you read (step 4), which patterns you
|
|
167
|
+
read (step 5), and which `component_bindings` / Selection-Policy roles you are
|
|
168
|
+
reusing (step 6). Skipping this declaration = the orchestrator will reject
|
|
149
169
|
the implementation and re-spawn.
|
|
150
170
|
|
|
151
171
|
The `features.has_design_system: true` flag in `baldart.config.yml` is
|
|
@@ -370,9 +390,15 @@
|
|
|
370
390
|
else
|
|
371
391
|
echo "i18n:SKIP (no changed JS/TS files in this card)" # guard: features.has_i18n: true
|
|
372
392
|
fi
|
|
393
|
+
# DS closed-set gate — ONLY when features.has_design_system: true AND this card touches UI.
|
|
394
|
+
# Deterministic: BLOCKS (non-zero exit) when the card adds a new canonical component in a
|
|
395
|
+
# CLOSED role family (the "third header" failure mode). Self-scoping + self-skipping — no
|
|
396
|
+
# diff-scoping needed (it reads the committed Selection Policy + the diff). See
|
|
397
|
+
# agents/design-system-protocol.md § Closed-Set Selection Policy. Skip the line when has_design_system is false.
|
|
398
|
+
npx baldart ds-gate --json > /tmp/dsgate-<CARD-ID>.txt 2>&1; echo "ds-gate:$?" # guard: features.has_design_system: true
|
|
373
399
|
```
|
|
374
400
|
The `echo "<gate>:$?"` lines surface only the **exit code** inline — the full log stays on disk. When `stack.language` does NOT include `typescript`, skip the `tsc` line (no equivalent gate). Capturing to `/tmp/*-<CARD-ID>.txt` guarantees the failing output is available for step 9. On a non-zero exit, read a **bounded** extract only (`tail -n 30 /tmp/<gate>-<CARD-ID>.txt`), never the whole log — do NOT run the gates without redirect-to-file capture.
|
|
375
|
-
9. **If any check fails**: categorize the error (`lint | TypeScript | test | build | i18n`), log it in the tracker as `retry-cause: <category>`. An `i18n` failure means a hardcoded user-facing string — the fix is to externalize it via `t()` + register the key (coder STEP 9), never to weaken the gate. Because the gate is **diff-scoped to this card's changed files**, an `i18n` failure is unambiguously a string **this card** introduced — it is never pre-existing baseline debt in an untouched file (so do NOT spend a retry triaging "is this mine?"; it is). The Final review re-runs the gate over the whole batch diff as the merge backstop.
|
|
401
|
+
9. **If any check fails**: categorize the error (`lint | TypeScript | test | build | i18n | ds-gate`), log it in the tracker as `retry-cause: <category>`. An `i18n` failure means a hardcoded user-facing string — the fix is to externalize it via `t()` + register the key (coder STEP 9), never to weaken the gate. A `ds-gate` failure (`DS_CLOSED_SET_VIOLATION`) means this card introduced a NEW canonical component in a CLOSED role family — the fix is to **reuse the family's canonical member** (named in the gate message), never to weaken the gate or add the duplicate; if the requirement genuinely needs a new member, that is a governance decision (escalate as a blocking finding for a `/prd` reconciliation + ADR, do not self-approve). Because the gate is **diff-scoped to this card's changed files**, an `i18n` failure is unambiguously a string **this card** introduced — it is never pre-existing baseline debt in an untouched file (so do NOT spend a retry triaging "is this mine?"; it is). The Final review re-runs the gate over the whole batch diff as the merge backstop.
|
|
376
402
|
- **i18n locale-PARITY ≠ anti-hardcoded (do NOT retry the coder on it).** The `i18n` category above is the *anti-hardcoded* gate. A separate **locale-parity** failure — the project's OWN parity test/build complaining that a new key exists in the source locale but not in the others — surfaces under `test`/`build`, and is **NOT a `coder` retry**: the `coder` writes the source locale only **by design** (`coder.md` STEP 9.6), so retrying it just loops. Route it to **`i18n-translator`** (fills the missing target locales context-aware from the registry), then re-run the gate. **Proactively — to avoid the fail-then-fix round-trip (a measured ~2M tokens, FEAT-0042): when `features.has_i18n` AND this card's diff added new source-locale keys, spawn the `i18n-translator` fill pass right after the coder commits and BEFORE these gates**, so parity holds first-try. This is the planned per-card i18n cascade step (`agents/i18n-protocol.md`); the coder still never translates.
|
|
377
403
|
**Code-recovery check (MUST do before rewriting)**: before spawning a fix agent or rewriting code, check whether lint-staged or a pre-commit hook removed code that is actually needed (e.g. a field "unused" at commit time but consumed by later code). Inspect the captured diff and `git diff`/`git log -p` for the worktree branch and restore the needed code by an explicit file write. **Do NOT `git stash pop` inside the worktree** — `refs/stash` is globally shared across worktrees (`$GIT_COMMON_DIR`), so a stash created or popped in a worktree can corrupt another worktree's state (see Phase 4 WORKTREE COMMIT RULE). If you need to set work aside in a worktree, make a clearly-labelled WIP commit and record its hash in the tracker for later squash, never a stash.
|
|
378
404
|
When spawning a fix agent: scope it exclusively to this card's Edit-allowed files (from `## File Ownership Map`) — it MUST NOT touch files owned by other cards. Pass the fix agent: the **path** to the captured gate log (`/tmp/<gate>-<CARD-ID>.txt` — it Reads the log itself; do NOT inline-paste the full log into its prompt), the error category, and the explicit list of files it may edit. Do NOT ask the user — just fix and re-run. Fix the code, not the tests (unless the test itself is wrong). Repeat up to **3 times**. **Stuck-loop guard**: compare the failing error's fingerprint (file:line + message) between retries; if the SAME error reproduces on 2 consecutive retries, the fix is not converging (often a constraint outside the card's ownership map) — stop early, log `[STUCK-LOOP] <error>` in `## Issues & Flags`, and escalate to the user rather than burning the 3rd retry on an identical failure.
|
|
@@ -131,25 +131,74 @@ When invoking `ui-design`, ensure it has access to:
|
|
|
131
131
|
- The `<slug>` for file naming
|
|
132
132
|
- Existing component matches (from registry lookup below)
|
|
133
133
|
|
|
134
|
-
### Component
|
|
134
|
+
### Component Reconciliation (BLOCKING — before design)
|
|
135
|
+
|
|
136
|
+
This is the **prevention** layer for the duplicate-component failure mode: an
|
|
137
|
+
active *match-before-generate* gate that binds every mockup region to an existing
|
|
138
|
+
component (or an explicit, governed NEW decision) BEFORE any design is generated —
|
|
139
|
+
not a passive list the subskill may ignore. It is the human-in-the-loop twin of
|
|
140
|
+
the deterministic `baldart ds-gate` (which blocks at build time); resolving here
|
|
141
|
+
means the cards `/new` emits already NAME the component to reuse, so the
|
|
142
|
+
ambiguity that causes divergence never reaches implementation.
|
|
135
143
|
|
|
136
144
|
Before invoking `ui-design`:
|
|
137
145
|
|
|
138
146
|
1. Read `${paths.references_dir}/component-registry.md`.
|
|
139
|
-
2. When `features.has_design_system: true`, ALSO read
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
2. When `features.has_design_system: true`, ALSO read
|
|
148
|
+
`${paths.design_system}/INDEX.md` — **including its § "Selection Policy" block**
|
|
149
|
+
(the closed role families) — and the in-scope component spec HEADs. Required by
|
|
150
|
+
HARD RULE 14 in `prd/SKILL.md` and the BLOCKING cascade in `ui-design/SKILL.md`.
|
|
151
|
+
Skipping this read is a protocol violation.
|
|
152
|
+
3. **Build a Mockup Element Inventory.** From the mockup (or the feature
|
|
153
|
+
description when mockup-less), list every interactive/visual component the UI
|
|
154
|
+
sketches: headers, tables, forms, modals, cards, toolbars, …
|
|
155
|
+
4. **Classify each element by ROLE, not by name** (this closes the vocabulary gap —
|
|
156
|
+
an agent thinks "I need a header", the registry is keyed by component name). For
|
|
157
|
+
each element, walk the Component Discovery Cascade + query the Selection Policy:
|
|
158
|
+
- **REUSE** — an existing component (or a closed-family canonical member) covers
|
|
159
|
+
it. Record the exact component. *No question asked — this is the silent path.*
|
|
160
|
+
- **REUSE+VARIANT** — an existing component covers it but the mockup needs a
|
|
161
|
+
prop/variant not yet in its spec.
|
|
162
|
+
- **NEW** — genuinely no match.
|
|
163
|
+
5. **Ask ONLY on ambiguity** (`REUSE+VARIANT` and `NEW` — never on plain REUSE).
|
|
164
|
+
One consolidated `AskUserQuestion` per ambiguous element, in the
|
|
165
|
+
prohibition+alternative form. Two cases:
|
|
166
|
+
- **Closed-family hit** (the element maps to a role in a family marked
|
|
167
|
+
`selection_closed: true`): you MUST reuse the canonical member. Surface it:
|
|
168
|
+
*"Questo mockup disegna un header: il design system ha già `SectionHeader`
|
|
169
|
+
(depth 0) e `DrilldownHeader` (depth ≥1) come set CHIUSO. Riuso il membro
|
|
170
|
+
adatto, oppure questo diventa una nuova variante/standard?"* Creating a third
|
|
171
|
+
member is a **governance decision** — only on explicit user confirmation, and
|
|
172
|
+
it means flipping/extending the closed set (record an ADR; `/design-system-init`
|
|
173
|
+
or the reconciliation persists `canonical_for`/`selection_closed`).
|
|
174
|
+
- **Open / no role** NEW element: *"Da questo mockup emerge un nuovo componente
|
|
175
|
+
X. Diventa lo standard per [contesto]? Quando va usato vs i simili esistenti?"*
|
|
176
|
+
If the user says it becomes a standard, capture the **selection predicate**
|
|
177
|
+
(`use_when`) and whether its family should be closed.
|
|
178
|
+
6. **Persist the decisions** (so they are not re-litigated downstream):
|
|
179
|
+
- Into each affected card → the `component_bindings` block (see
|
|
180
|
+
`agents/card-schema.md`): `region → component (reuse | new) [+ variant]`. This
|
|
181
|
+
is the authority `ui-expert` implements against (the mockup becomes a visual
|
|
182
|
+
reference, not a license to re-create).
|
|
183
|
+
- Into the manifest → when the user confirms a new standard / closure, the
|
|
184
|
+
component's `canonical_for` / `use_when` / `selection_closed` are written (via
|
|
185
|
+
`/design-system-init` enrich or `doc-reviewer`), so `baldart ds-gate` enforces
|
|
186
|
+
it from then on. A new standard for *all* views also opens a **migration
|
|
187
|
+
follow-up card** for the existing views (never migrate them in this PRD).
|
|
188
|
+
|
|
189
|
+
Pass the resolved inventory to `ui-design`:
|
|
142
190
|
|
|
143
191
|
```
|
|
144
|
-
###
|
|
145
|
-
REUSE —
|
|
146
|
-
- [Component] — [path] — [
|
|
192
|
+
### Component Reconciliation (resolved)
|
|
193
|
+
REUSE — bind region → existing component:
|
|
194
|
+
- [region] → [Component] — [path] — [variant if any]
|
|
147
195
|
|
|
148
|
-
|
|
149
|
-
- [
|
|
196
|
+
NEW (governed) — confirmed with user:
|
|
197
|
+
- [region] → [NewComponent] — role: [family@context] — standard: [yes/no] — closed: [yes/no]
|
|
150
198
|
```
|
|
151
199
|
|
|
152
|
-
This prevents designing new components when
|
|
200
|
+
This prevents designing new components when the registry already covers the need,
|
|
201
|
+
and turns the lookup from advisory into a checkable, persisted contract.
|
|
153
202
|
|
|
154
203
|
### Gates (Full branch)
|
|
155
204
|
|
|
@@ -106,6 +106,8 @@ Before running the cascade, consult `${paths.design_system}/INDEX.md` (when pres
|
|
|
106
106
|
|
|
107
107
|
Run the full cascade: design-system INDEX → project search (`${paths.components_primitives}/*`, `${paths.components_root}/shared/*` if applicable) → 21st.dev catalog → create from scratch. Document which existing components to reuse and which need creation. Any new component must consume the project's design tokens.
|
|
108
108
|
|
|
109
|
+
**Closed-set query (BLOCKING when `features.has_design_system: true`).** Consult `${paths.design_system}/INDEX.md` § "Selection Policy". For every region you design, query by **role** first: if it maps to a **closed** role family (e.g. `page-header`), design with that family's canonical member — do NOT mock up a new variant of a closed primitive. A new member of a closed family is a governance decision surfaced to the user (per `/prd` Component Reconciliation), never a design choice you make unilaterally. Emit the resolved `region → component` bindings in the inventory deliverable so they flow into the card's `component_bindings` (the authority `ui-expert` implements against — see `agents/card-schema.md`).
|
|
110
|
+
|
|
109
111
|
### Step B — Context Capture
|
|
110
112
|
|
|
111
113
|
Read [references/generation.md](references/generation.md) § Context Capture.
|
|
@@ -352,7 +352,7 @@ const apiPrompt =
|
|
|
352
352
|
`Return findings with domain in {code, perf, migration, security}. Use the finding schema fields.`
|
|
353
353
|
|
|
354
354
|
const qaPrompt =
|
|
355
|
-
`Run MECHANICAL GATES ONLY over the batch scope, per ${protocolRef} Step F.3 (qa-sentinel row): lint, type-check, the full test suite, build, dependency audit, markdownlint, AND — when features.has_i18n is true — the i18n anti-hardcoded gate (resolve i18n.lint_command, else the standalone eslint.i18n.config.mjs by convention; non-zero exit = FAIL; neither present = SKIP, never a silent pass) as applicable to this project. DIFF-SCOPE the i18n gate to the batch's changed files listed below: strip the trailing ' .' full-sweep target from the resolved command and run it over only the *.{ts,tsx,js,jsx,mjs,cjs} files in the Changed files list, so pre-existing baseline debt in untouched files never fails the batch (whole-repo \`.\` is only for /i18n, /i18n-adopt, and the i18n-align routine). Do NOT read source for code findings, do NOT emit severities — return only a PASS/FAIL/SKIP gate table.\n\nWorktree: ${a.worktreePath || '(cwd)'}\nChanged files:\n${scope.join('\n')}`
|
|
355
|
+
`Run MECHANICAL GATES ONLY over the batch scope, per ${protocolRef} Step F.3 (qa-sentinel row): lint, type-check, the full test suite, build, dependency audit, markdownlint, AND — when features.has_i18n is true — the i18n anti-hardcoded gate (resolve i18n.lint_command, else the standalone eslint.i18n.config.mjs by convention; non-zero exit = FAIL; neither present = SKIP, never a silent pass) as applicable to this project, AND — when features.has_design_system is true and the batch touches UI — the DS closed-set gate \`npx baldart ds-gate --json\` (self-scoping + self-skipping; non-zero exit / blockingCount>0 = FAIL = a DS_CLOSED_SET_VIOLATION, a new canonical component in a CLOSED role family slipped through; registry/policy absent → SKIP, never a silent pass). DIFF-SCOPE the i18n gate to the batch's changed files listed below: strip the trailing ' .' full-sweep target from the resolved command and run it over only the *.{ts,tsx,js,jsx,mjs,cjs} files in the Changed files list, so pre-existing baseline debt in untouched files never fails the batch (whole-repo \`.\` is only for /i18n, /i18n-adopt, and the i18n-align routine). Do NOT read source for code findings, do NOT emit severities — return only a PASS/FAIL/SKIP gate table.\n\nWorktree: ${a.worktreePath || '(cwd)'}\nChanged files:\n${scope.join('\n')}`
|
|
356
356
|
|
|
357
357
|
// F-041 — single-card batch: a per-card review already covered these exact files and a
|
|
358
358
|
// 1-card batch has NO cross-card conflict to surface, so the duplicate Claude finders can be
|
|
@@ -242,6 +242,9 @@ const projectBrief = [
|
|
|
242
242
|
features.has_i18n
|
|
243
243
|
? `i18n (features.has_i18n=true, per agents/i18n-protocol.md): the coder MUST NOT hardcode any user-facing string — route every label through the stack t() function and apply coder.md STEP 9 (add the key + a hyper-brief context to the registry at ${paths.i18n_registry || 'paths.i18n_registry'}). The Phase-2 gate MUST run the anti-hardcoded gate IN ADDITION to lint: resolve i18n.lint_command (else the standalone eslint.i18n.config.mjs by convention) — base command \`${(cfg.i18n && cfg.i18n.lint_command) || 'npx eslint --config eslint.i18n.config.mjs .'}\` — but DIFF-SCOPED: strip the trailing ' .' full-sweep target and run it over ONLY this card's changed *.{ts,tsx,js,jsx,mjs,cjs} files (\`git diff --name-only TRUNK...HEAD | grep -E '\\.(ts|tsx|js|jsx|mjs|cjs)$'\`), so pre-existing baseline debt in untouched files never fails this card. No changed JS/TS files → SKIP. A non-zero exit FAILS the gate (a string THIS card introduced); if neither command nor config exists report i18n-lint SKIP, never a silent pass. Whole-repo \`.\` is only for /i18n, /i18n-adopt, and the i18n-align routine.`
|
|
244
244
|
: '',
|
|
245
|
+
features.has_design_system
|
|
246
|
+
? `DS closed-set gate (features.has_design_system=true, per agents/design-system-protocol.md § Closed-Set Selection Policy): when a card touches UI (components dir or a component spec), the Phase-2 gate MUST run \`npx baldart ds-gate --json\` IN ADDITION to lint. It deterministically BLOCKS (non-zero exit, blockingCount>0, DS_CLOSED_SET_VIOLATION) when the card adds a NEW canonical component in a CLOSED role family (the "third header" failure mode). It is self-scoping + self-skipping (reads the committed INDEX Selection Policy + the diff; registry/policy absent → clean no-op exit 0). A block is NOT a coder retry on style — the fix is to REUSE the canonical member named in the gate message; a genuinely-needed new member is a governance decision → route to the self-healing resolution pass as a blocking finding (ADR + /prd reconciliation), never auto-approve. DS_CLOSED_SET_SUSPECT is a warning, never a fail.`
|
|
247
|
+
: '',
|
|
245
248
|
FLAGS.effort ? `Reasoning effort for this run: ${FLAGS.effort}.` : '',
|
|
246
249
|
].filter(Boolean).join('\n')
|
|
247
250
|
|
|
@@ -65,6 +65,7 @@ Legend: **R** = required, present **and non-empty** · **E** = required key, val
|
|
|
65
65
|
| `files_likely_touched` | E (`[]`, never code) | R | R | EPIC tracks docs only |
|
|
66
66
|
| `links.prd` | C | R | C | STANDALONE/non-PRD cards legitimately lack a PRD |
|
|
67
67
|
| `links.design` | C | C | C | required when the card has UI scope |
|
|
68
|
+
| `component_bindings` | — | C | C | UI cards only — the resolved mockup-region → component map from `/prd` Component Reconciliation; the authority `ui-expert` implements against (see below) |
|
|
68
69
|
| `canonical_docs` | R | R | C (WARN) | "REQUIRED for all cards generated from a PRD"; a manual CHORE may lack it |
|
|
69
70
|
| `data_sources` | E (`[]`) | E (`[]` if no data) | E | key always present, value may be `[]` |
|
|
70
71
|
| `documentation_impact` | R | C | C | |
|
|
@@ -130,6 +131,33 @@ downstream cost in a real batch:
|
|
|
130
131
|
A back-fill never rescues a card missing a HALT field: a card without `scope` blocks regardless
|
|
131
132
|
of how many derivable fields were filled.
|
|
132
133
|
|
|
134
|
+
### `component_bindings` (UI cards — the reconciliation contract)
|
|
135
|
+
|
|
136
|
+
When a card has UI scope and the `/prd` Component Reconciliation gate ran (see
|
|
137
|
+
`prd/references/ui-design-phase.md` § Component Reconciliation), the card carries
|
|
138
|
+
the resolved mockup-region → component map. This is the **authority** `ui-expert`
|
|
139
|
+
implements against — the mockup is the visual reference, this map decides *which
|
|
140
|
+
component each region IS*. It eliminates the build-time ambiguity that lets an
|
|
141
|
+
implementer re-create an existing primitive.
|
|
142
|
+
|
|
143
|
+
```yaml
|
|
144
|
+
component_bindings:
|
|
145
|
+
- region: "page header"
|
|
146
|
+
component: SectionHeader # an existing registry component, or a NEW one
|
|
147
|
+
action: reuse # reuse | reuse-variant | new
|
|
148
|
+
variant: "" # when action: reuse-variant
|
|
149
|
+
role: "page-header@depth-0" # when it maps a Selection-Policy role
|
|
150
|
+
- region: "row actions"
|
|
151
|
+
component: IconButton
|
|
152
|
+
action: reuse
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`action: new` is only legitimate when the `/prd` gate confirmed it with the user
|
|
156
|
+
(and, for a closed role family, recorded the governance decision). A `ui-expert`
|
|
157
|
+
that finds a `component_bindings` entry MUST honor it over any contrary reading of
|
|
158
|
+
the mockup; a UI card that went through `/prd` with `features.has_design_system:
|
|
159
|
+
true` but lacks this field is under-specified (the reconciliation did not run).
|
|
160
|
+
|
|
133
161
|
## Writer contract
|
|
134
162
|
|
|
135
163
|
The canonical writer is the `prd-card-writer` agent. It emits the full baseline for the
|
|
@@ -63,6 +63,10 @@ token_bindings: [color.action.primary, radius.md, space.2, motion.fast] # DTCG
|
|
|
63
63
|
a11y: "Icon-only requires aria-label; ≥44px target; respects prefers-reduced-motion."
|
|
64
64
|
related: [IconButton, Link] # sibling primitives (not composition)
|
|
65
65
|
must_rules: ["MUST consume color.action.primary", "MUST set aria-label when icon-only"]
|
|
66
|
+
# --- Selection policy (closed-set; see design-system-protocol.md § Closed-Set Selection Policy) ---
|
|
67
|
+
canonical_for: [action-trigger@primary] # presentational role(s) this component is THE canonical answer for: "<family>@<context>"
|
|
68
|
+
use_when: "Primary action of a surface; one per view. Secondary/tertiary → variant ghost/destructive." # human/agent-readable selection predicate
|
|
69
|
+
selection_closed: false # true ⇒ this role FAMILY is closed: a new canonical member requires governance, not code
|
|
66
70
|
last_verified: 2026-06-23 # set by doc-reviewer / ds-drift on semantic re-verify
|
|
67
71
|
owner: doc-reviewer # curation owner (constant; documents the SSOT)
|
|
68
72
|
---
|
|
@@ -73,7 +77,28 @@ owner: doc-reviewer # curation owner (constant; documents
|
|
|
73
77
|
| Origin | Fields | Rule |
|
|
74
78
|
|---|---|---|
|
|
75
79
|
| **Deterministic** (TS/git) | `name`, `source`, `source_sha`, `status` (default), `category` (heuristic), `props`, `variants`, `composes` | Always safe to **overwrite** on regeneration — they are derived. Extracted by a pure Node script (no agent), so the path is **portable to Codex**. |
|
|
76
|
-
| **Agentic** (doc-reviewer) | `purpose`, `token_bindings`, `a11y`, `related`, `must_rules`, `last_verified`; plus `status: deprecated` as a judgment | **Never clobbered** when non-empty. Empty agentic fields = "not yet enriched", filled later by `doc-reviewer` / `ds-drift`, never auto-invented. |
|
|
80
|
+
| **Agentic** (doc-reviewer) | `purpose`, `token_bindings`, `a11y`, `related`, `must_rules`, `canonical_for`, `use_when`, `selection_closed`, `last_verified`; plus `status: deprecated` as a judgment | **Never clobbered** when non-empty. Empty agentic fields = "not yet enriched", filled later by `doc-reviewer` / `ds-drift`, never auto-invented. |
|
|
81
|
+
|
|
82
|
+
### Selection-policy fields (the closed-set layer)
|
|
83
|
+
|
|
84
|
+
`canonical_for`, `use_when`, and `selection_closed` are the **third semantic
|
|
85
|
+
layer** (token → component → *selection*): they encode *when a component is the
|
|
86
|
+
right one* and *whether its role family is closed*, so an agent reconciling a
|
|
87
|
+
mockup queries by **role**, not by guessing from the component name.
|
|
88
|
+
|
|
89
|
+
| Field | Type | Meaning | Written by |
|
|
90
|
+
|---|---|---|---|
|
|
91
|
+
| `canonical_for` | list of `"<family>@<context>"` | The presentational role(s) this component is THE canonical answer for. `<family>` is the role class (`page-header`, `action-trigger`); `<context>` disambiguates within it (`depth-0`, `primary`). A component may serve several. Empty = not (yet) a declared canonical. | `/design-system-init` (upgrade), `/prd` reconciliation (Slice B), `doc-reviewer` |
|
|
92
|
+
| `use_when` | string | One-line selection predicate — the prose rule that picks this over its siblings. Lift it from the component's own `must_rules` when one already states it (e.g. *"MUST use only for depth 0"*). | same |
|
|
93
|
+
| `selection_closed` | boolean (default `false`) | `true` ⇒ the **role family** (the `<family>` part of this component's `canonical_for`) is a **closed set**: introducing a *new* canonical member is a governance decision, not an implementation act. The deterministic `baldart ds-gate` blocks a new member of a closed family; an *open* family stays advisory-only (cascade + review). | same; flip to `true` only on an explicit governance decision (often recorded as an ADR) |
|
|
94
|
+
|
|
95
|
+
**Family-level truth from per-component flags.** A role family is closed iff at
|
|
96
|
+
least one of its canonical members carries `selection_closed: true`. The generated
|
|
97
|
+
`INDEX.md` § "Selection Policy" aggregates the members per family from their
|
|
98
|
+
`canonical_for` and marks the family `closed` accordingly — so the closure lives in
|
|
99
|
+
the HEADs (regenerable) and the INDEX stays a pure derived router. Mark **every**
|
|
100
|
+
member of a closed family `selection_closed: true` to keep the aggregation
|
|
101
|
+
unambiguous.
|
|
77
102
|
|
|
78
103
|
## Read contract (the context win)
|
|
79
104
|
|
|
@@ -369,6 +369,99 @@ agent has two choices:
|
|
|
369
369
|
Never silently introduce a new component that overlaps with an existing
|
|
370
370
|
registry primitive — that is the exact failure mode this protocol prevents.
|
|
371
371
|
|
|
372
|
+
## Closed-Set Selection Policy (the third semantic layer)
|
|
373
|
+
|
|
374
|
+
The registry has three semantic layers, and components consume the top of each:
|
|
375
|
+
|
|
376
|
+
1. **Token** (`.tokens.json`) — atoms: colour, spacing, radius, motion.
|
|
377
|
+
2. **Component manifest** (per-spec frontmatter HEAD) — *what a component is*:
|
|
378
|
+
props, variants, a11y.
|
|
379
|
+
3. **Selection policy** (this section) — *when a component is the right one*, and
|
|
380
|
+
**whether its role family is closed**.
|
|
381
|
+
|
|
382
|
+
The duplicate-component failure mode survives the Discovery Cascade because the
|
|
383
|
+
cascade is keyed by component **name**, while an agent reading a mockup thinks in
|
|
384
|
+
terms of **need** ("I need a header for this page"). Nothing in layers 1–2 asserts
|
|
385
|
+
the *boundary* of a category — so adding a third header violates no rule, only an
|
|
386
|
+
undiscovered convention. Layer 3 closes that gap by making the boundary explicit,
|
|
387
|
+
machine-readable, and (for closed families) deterministically enforced.
|
|
388
|
+
|
|
389
|
+
### Roles, families, and `canonical_for`
|
|
390
|
+
|
|
391
|
+
A **role** is a `"<family>@<context>"` string a component declares it is the
|
|
392
|
+
canonical answer for, in its HEAD's `canonical_for` (see
|
|
393
|
+
`agents/component-manifest-schema.md` § Selection-policy fields):
|
|
394
|
+
|
|
395
|
+
- `<family>` — the role class: `page-header`, `action-trigger`, `data-table`.
|
|
396
|
+
- `<context>` — disambiguates within the family: `depth-0`, `depth-1+`, `primary`.
|
|
397
|
+
|
|
398
|
+
Example (the two-header model): `SectionHeader` →
|
|
399
|
+
`canonical_for: [page-header@depth-0]`; `DrilldownHeader` →
|
|
400
|
+
`canonical_for: [page-header@depth-1+]`. The selection predicate (`use_when`) is
|
|
401
|
+
lifted from the component's own `must_rules` where one already states it (e.g.
|
|
402
|
+
*"MUST use only for depth 0"*).
|
|
403
|
+
|
|
404
|
+
### Closure (`selection_closed`) — the asserted boundary
|
|
405
|
+
|
|
406
|
+
A role family is **closed** when its members carry `selection_closed: true`,
|
|
407
|
+
meaning: *these are the only canonical components for this family; introducing a
|
|
408
|
+
new one is a governance decision, not an implementation act.* This is where a
|
|
409
|
+
prose decision — typically an ADR — becomes machine-readable so agents actually
|
|
410
|
+
consult it.
|
|
411
|
+
|
|
412
|
+
- **Where it lives:** in the spec HEADs (`canonical_for` + `selection_closed`),
|
|
413
|
+
aggregated by the deterministic serializer into the **generated** `INDEX.md`
|
|
414
|
+
§ "Selection Policy" block (a fenced `yaml` `families:` map). The INDEX stays a
|
|
415
|
+
pure derived router — never hand-edit the block; edit the HEADs and regenerate.
|
|
416
|
+
- **Open vs closed:** an *open* family (no `selection_closed`) is advisory-only —
|
|
417
|
+
covered by the Discovery Cascade + review, exactly as today. Closure is
|
|
418
|
+
**opt-in per family** and high-precision: only assert it where the boundary is a
|
|
419
|
+
real product decision.
|
|
420
|
+
|
|
421
|
+
### Enforcement — `baldart ds-gate` (deterministic)
|
|
422
|
+
|
|
423
|
+
The closed-set is enforced at **build time**, before the duplicate is reviewed,
|
|
424
|
+
by the BALDART-owned `baldart ds-gate` (the design-system analogue of the i18n
|
|
425
|
+
gate; rides on `features.has_design_system`, **no new config key**). It reads the
|
|
426
|
+
**committed** `INDEX.md` § "Selection Policy" (the baseline closure) and the
|
|
427
|
+
change's diff, and emits:
|
|
428
|
+
|
|
429
|
+
- `DS_CLOSED_SET_VIOLATION` (**BLOCK**) — the diff adds a component that claims
|
|
430
|
+
`canonical_for` a role in a **closed** family but is **not** one of that
|
|
431
|
+
family's baseline members. Deterministic, zero false-positives. Message names
|
|
432
|
+
the family, its canonical members, and the governance ref. The fix is always
|
|
433
|
+
*reuse a member* or *open governance to extend the family* — never *ship the
|
|
434
|
+
third one*.
|
|
435
|
+
- A **heuristic warning** (not a block) when a new component's name matches a
|
|
436
|
+
closed family (e.g. a new `*Header*` primitive while `page-header` is closed)
|
|
437
|
+
without declaring `canonical_for`. Slice C upgrades this to a manifest
|
|
438
|
+
signature-diff (`DS_DUPLICATE_PRIMITIVE`).
|
|
439
|
+
|
|
440
|
+
`ds-gate` is wired wherever the i18n gate runs (`qa-sentinel`, `/new` Phase 2,
|
|
441
|
+
`new2`, the Final review, `code-reviewer`). Like every BALDART gate it **SKIPs,
|
|
442
|
+
never silently passes**, when the registry or the policy block is absent.
|
|
443
|
+
|
|
444
|
+
### Governance — flipping a family to closed
|
|
445
|
+
|
|
446
|
+
Marking a family `selection_closed: true` (or adding a member to a closed family)
|
|
447
|
+
is a deliberate act that usually follows an ADR. An agent never flips closure on
|
|
448
|
+
its own — it **proposes, a human confirms**:
|
|
449
|
+
- `/design-system-init` (upgrade) auto-fills the unambiguous roles (`canonical_for`
|
|
450
|
+
/ `use_when` lifted from `must_rules`) but **collects closure candidates +
|
|
451
|
+
suspected duplicates** and confirms them with the user in one consolidated
|
|
452
|
+
`AskUserQuestion` (its step 4c) before writing any `selection_closed`. Unattended
|
|
453
|
+
/ Codex runs propose-only — they never auto-close.
|
|
454
|
+
- The `/prd` reconciliation gate (Slice B) flips/extends closure when the user
|
|
455
|
+
confirms *"this becomes the standard"* for a newly-designed component.
|
|
456
|
+
|
|
457
|
+
### How it composes
|
|
458
|
+
|
|
459
|
+
- **Discovery Cascade step 1** now queries by **role** (the INDEX Selection Policy
|
|
460
|
+
map) before falling to name/source — so a mockup's "header" resolves to the
|
|
461
|
+
canonical member instead of a fresh component.
|
|
462
|
+
- **Post-Intervention Coherence Check** gains check 5 below: a closed-family
|
|
463
|
+
violation is reconciled at the source of introduction, not next Monday.
|
|
464
|
+
|
|
372
465
|
## Functional Traceability Gate (BLOCKING — unconditional)
|
|
373
466
|
|
|
374
467
|
A mockup is a fidelity target, **not** a requirements oracle. Design tools
|
|
@@ -494,6 +587,13 @@ For each item in the diff that touched a visual surface, run these 4 checks:
|
|
|
494
587
|
`INDEX.md`? If yes, add it to the INDEX in the same change (or open a
|
|
495
588
|
follow-up card if the registry entry requires design judgement beyond
|
|
496
589
|
the current scope). Silent primitive = `DS_INDEX_DRIFT` finding.
|
|
590
|
+
5. **New component in a closed role family ⇒ governance, not code**: did the
|
|
591
|
+
change introduce a component that serves a role in a **closed** family (per
|
|
592
|
+
`INDEX.md` § Selection Policy) without being an existing member of it? If yes,
|
|
593
|
+
that is a `DS_CLOSED_SET_VIOLATION` — task blocked. Reuse the canonical member,
|
|
594
|
+
or open governance (an ADR + a `/prd` reconciliation decision) to extend the
|
|
595
|
+
family. Run `baldart ds-gate` to check deterministically. (An *open* family is
|
|
596
|
+
advisory — the Discovery Cascade + review cover it, no block.)
|
|
497
597
|
|
|
498
598
|
### Decision matrix
|
|
499
599
|
|
|
@@ -504,6 +604,7 @@ For each item in the diff that touched a visual surface, run these 4 checks:
|
|
|
504
604
|
| Reused undocumented primitive that is broadly useful | Add INDEX entry inline; if a full spec needs design work, scaffold a stub spec marked `status: needs-review` |
|
|
505
605
|
| Reused undocumented primitive that is surface-specific | Add INDEX entry with `scope: local` and a 2-line note explaining why no full spec is needed |
|
|
506
606
|
| Introduced a primitive that **duplicates** an existing registry entry | **Stop**. This is the failure mode the cascade exists to prevent. Revert and reuse the existing primitive, or escalate to refactor the existing primitive if the new requirement is legitimately broader |
|
|
607
|
+
| Introduced a new canonical component in a **closed** role family (`DS_CLOSED_SET_VIOLATION`) | **Stop — blocked by `baldart ds-gate`.** Reuse the family's canonical member, or open governance (ADR + `/prd` reconciliation) to extend the closed set; never ship the third one silently |
|
|
507
608
|
|
|
508
609
|
### Reporting
|
|
509
610
|
|
|
@@ -20,6 +20,7 @@ Route agents to the right module with minimal reading.
|
|
|
20
20
|
- If touching database schema or fields -> read `agents/data-model.md` and `${paths.references_dir}/data-model.md`.
|
|
21
21
|
- If touching UI pages/routes or flows -> read `${paths.references_dir}/ui/index.md` (then specific UI module for your domain).
|
|
22
22
|
- If touching ANY visual surface (component, page, mockup, design review) and `features.has_design_system: true` -> read `agents/design-system-protocol.md` and treat the BLOCKING cascade (`${paths.design_system}/INDEX.md` + `tokens-reference.md` + `components/<Name>.md`) as mandatory pre-work. When the flag is `false`, recommend `/design-system-init` to scaffold a registry.
|
|
23
|
+
- If a UI change might INTRODUCE a component (designing from a mockup, building a new primitive) and `features.has_design_system: true` -> read `agents/design-system-protocol.md` § "Closed-Set Selection Policy": query the registry by ROLE (`INDEX.md` § Selection Policy), reuse the canonical member of any `closed` family, honor the card's `component_bindings`, and run `baldart ds-gate` (a `DS_CLOSED_SET_VIOLATION` — a new canonical in a closed family, the "third header" failure mode — BLOCKS). A new member of a closed family is a governance decision (`/prd` reconciliation + ADR), never an implementation act.
|
|
23
24
|
- If CREATING, UPDATING, or READING a per-component spec (`${paths.design_system}/components/<Name>.md`) -> read `agents/component-manifest-schema.md` for the machine-readable frontmatter HEAD (deterministic-from-TS + agentic fields) and the on-demand read contract (INDEX router → frontmatter-first, prose only when modifying). The drift codes + gate stay in `design-system-protocol.md`.
|
|
24
25
|
- If touching design-review workflows or UI guidelines -> read `agents/design-review.md` and project-specific UI guidelines.
|
|
25
26
|
- If touching architecture, auth, or tech stack -> read `agents/architecture.md`.
|
|
@@ -39,7 +39,15 @@ prompt: |
|
|
|
39
39
|
id not used in its component's styling surface is a `DS_TOKENS_DRIFT`
|
|
40
40
|
**advisory** — list it for review (fix the binding or justify as indirect),
|
|
41
41
|
do not auto-remove.
|
|
42
|
-
5.
|
|
42
|
+
5. **Closed-set Selection Policy integrity**: the `INDEX.md` § "Selection Policy"
|
|
43
|
+
block is generated from the spec HEADs' `canonical_for` + `selection_closed`.
|
|
44
|
+
Verify it is in sync (a member whose HEAD changed its `canonical_for`, or a
|
|
45
|
+
family whose closure drifted from its members, is `DS_INDEX_DRIFT` — fix by
|
|
46
|
+
regenerating). Sanity-check each `closed` family still partitions its cases
|
|
47
|
+
(no two members claim the same `<family>@<context>`; no orphaned role). This is
|
|
48
|
+
the safety net for the per-merge `baldart ds-gate` — report, never silently
|
|
49
|
+
reopen a closed family.
|
|
50
|
+
6. Emit a consolidated report under
|
|
43
51
|
`docs/reports/{{YYYYMMDD}}-ds-drift.md`.
|
|
44
52
|
|
|
45
53
|
Apply trivial fixes inline (regenerate the INDEX router; `baldart tokens build`
|
package/package.json
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `baldart ds-gate` — enforce the closed-set Selection Policy (since v4.68.0).
|
|
3
|
+
*
|
|
4
|
+
* Reads the committed design-system `INDEX.md` § "Selection Policy", inspects the
|
|
5
|
+
* change set, and BLOCKS (exit 1) when a diff introduces a new canonical
|
|
6
|
+
* component in a CLOSED role family. Heuristic suspects are reported as warnings
|
|
7
|
+
* (exit 0). Registry/policy absent → clean no-op (exit 0). The logic lives in
|
|
8
|
+
* `src/utils/ds-reuse-gate.js`; this verb is the runnable surface the gates call
|
|
9
|
+
* (`qa-sentinel`, `/new` Phase 2, `new2`, the Final review, `code-reviewer`).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const UI = require('../utils/ui');
|
|
13
|
+
const gate = require('../utils/ds-reuse-gate');
|
|
14
|
+
|
|
15
|
+
async function dsGate(opts = {}) {
|
|
16
|
+
const json = !!opts.json;
|
|
17
|
+
const res = gate.run({ cwd: opts.cwd, base: opts.base });
|
|
18
|
+
|
|
19
|
+
if (json) { process.stdout.write(JSON.stringify(res) + '\n'); return res.blockingCount > 0 ? 1 : 0; }
|
|
20
|
+
|
|
21
|
+
if (res.skipped) {
|
|
22
|
+
UI.info(`ds-gate skipped (${res.reason}) — no closed-set check applies here.`);
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
const blocks = res.findings.filter((f) => f.severity === 'block');
|
|
26
|
+
const warns = res.findings.filter((f) => f.severity === 'warn');
|
|
27
|
+
|
|
28
|
+
for (const w of warns) UI.warning(`${w.code}: ${w.message}`);
|
|
29
|
+
for (const b of blocks) UI.error(`${b.code}: ${b.message}`);
|
|
30
|
+
|
|
31
|
+
if (blocks.length) {
|
|
32
|
+
UI.error(`ds-gate: ${blocks.length} closed-set violation(s) — blocked.`);
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
UI.success(`ds-gate: no closed-set violations${warns.length ? ` (${warns.length} warning(s))` : ''}.`);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { run: dsGate };
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const yaml = require('js-yaml');
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* DS reuse gate — the closed-set Selection Policy enforcer (since v4.68.0).
|
|
8
|
+
*
|
|
9
|
+
* BALDART-owned, deterministic, build-time. The design-system analogue of the
|
|
10
|
+
* i18n gate (`src/utils/i18n-gate.js`): a self-contained check BALDART runs so a
|
|
11
|
+
* UI change cannot introduce a NEW canonical component in a role family the
|
|
12
|
+
* design system declared CLOSED (the "third header" failure mode). It rides on
|
|
13
|
+
* `features.has_design_system` — NO new config key.
|
|
14
|
+
*
|
|
15
|
+
* What it does NOT do: it is not an ESLint wrapper (the closure check needs the
|
|
16
|
+
* registry, not the AST) and it does not detect semantically-equivalent rewrites
|
|
17
|
+
* by itself — that is the Slice-C signature-diff. Here the BLOCK is the
|
|
18
|
+
* deterministic, zero-false-positive part: a diff that adds a component claiming
|
|
19
|
+
* a CLOSED family's role, where that component is not a baseline member.
|
|
20
|
+
*
|
|
21
|
+
* Baseline truth = the COMMITTED `INDEX.md` § "Selection Policy" (read via
|
|
22
|
+
* `git show <baselineRef>:<index>`), never the working-tree INDEX — otherwise a
|
|
23
|
+
* change that regenerates the INDEX to include the new member would mask itself.
|
|
24
|
+
*
|
|
25
|
+
* Fail-safe contract (like every BALDART gate): registry absent / policy block
|
|
26
|
+
* absent / git unavailable → SKIP with a reason, never a silent pass and never a
|
|
27
|
+
* crash that blocks unrelated work.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const CONFIG_FILE = 'baldart.config.yml';
|
|
31
|
+
|
|
32
|
+
function loadConfig(cwd) {
|
|
33
|
+
try { return yaml.load(fs.readFileSync(path.join(cwd, CONFIG_FILE), 'utf8')) || {}; }
|
|
34
|
+
catch (_) { return null; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Extract the fenced `yaml` `families:` map from an INDEX.md § Selection Policy. */
|
|
38
|
+
function parseSelectionPolicy(indexText) {
|
|
39
|
+
if (!indexText) return null;
|
|
40
|
+
const at = indexText.indexOf('## Selection Policy');
|
|
41
|
+
if (at < 0) return null;
|
|
42
|
+
const m = indexText.slice(at).match(/```ya?ml\n([\s\S]*?)```/);
|
|
43
|
+
if (!m) return null;
|
|
44
|
+
try {
|
|
45
|
+
const doc = yaml.load(m[1]);
|
|
46
|
+
return (doc && doc.families && typeof doc.families === 'object') ? doc.families : {};
|
|
47
|
+
} catch (_) { return null; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Parse the INDEX router table → { ComponentName: sourcePath }. */
|
|
51
|
+
function parseIndexSources(indexText) {
|
|
52
|
+
const map = {};
|
|
53
|
+
if (!indexText) return map;
|
|
54
|
+
for (const line of indexText.split('\n')) {
|
|
55
|
+
const m = line.match(/^\|\s*([A-Za-z0-9_]+)\s*\|\s*([^|]+?)\s*\|/);
|
|
56
|
+
if (m && m[1] !== 'Component' && !/^-+$/.test(m[1])) map[m[1]] = m[2].trim();
|
|
57
|
+
}
|
|
58
|
+
return map;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Read the first `---`-fenced frontmatter HEAD of a spec file → parsed object. */
|
|
62
|
+
function parseSpecHead(text) {
|
|
63
|
+
if (!text) return null;
|
|
64
|
+
const m = text.match(/^---\n([\s\S]*?)\n---/);
|
|
65
|
+
if (!m) return null;
|
|
66
|
+
try { return yaml.load(m[1]) || {}; } catch (_) { return null; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Closed families as { family: { members:Set<name>, contexts, memberSources:Set } }. */
|
|
70
|
+
function closedFamilies(families, indexSources) {
|
|
71
|
+
const out = {};
|
|
72
|
+
for (const fam of Object.keys(families || {})) {
|
|
73
|
+
const f = families[fam] || {};
|
|
74
|
+
if (!f.closed) continue;
|
|
75
|
+
const members = new Set();
|
|
76
|
+
const memberSources = new Set();
|
|
77
|
+
for (const m of f.members || []) {
|
|
78
|
+
if (m && m.component) {
|
|
79
|
+
members.add(m.component);
|
|
80
|
+
if (indexSources[m.component]) memberSources.add(indexSources[m.component]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
out[fam] = { members, memberSources, raw: f };
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const familyOf = (role) => {
|
|
89
|
+
const at = String(role).indexOf('@');
|
|
90
|
+
return (at >= 0 ? String(role).slice(0, at) : String(role)).trim();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** Last meaningful segment of a family name, used as the source-heuristic keyword. */
|
|
94
|
+
function familyKeyword(fam) {
|
|
95
|
+
const segs = String(fam).split(/[-_/]/).filter(Boolean);
|
|
96
|
+
return (segs[segs.length - 1] || fam).toLowerCase();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Pure evaluator (deterministic, git-free → unit-testable).
|
|
101
|
+
* @param closed result of closedFamilies()
|
|
102
|
+
* @param changes [{ file, kind:'spec'|'source', added:bool, name, canonicalFor:[] }]
|
|
103
|
+
* @returns { findings:[...], blockingCount }
|
|
104
|
+
*/
|
|
105
|
+
function evaluate(closed, changes) {
|
|
106
|
+
const findings = [];
|
|
107
|
+
for (const ch of changes) {
|
|
108
|
+
if (ch.kind === 'spec') {
|
|
109
|
+
for (const role of ch.canonicalFor || []) {
|
|
110
|
+
const fam = familyOf(role);
|
|
111
|
+
const cf = closed[fam];
|
|
112
|
+
if (!cf) continue; // open family → advisory only
|
|
113
|
+
if (cf.members.has(ch.name)) continue; // an existing canonical member → fine
|
|
114
|
+
findings.push({
|
|
115
|
+
code: 'DS_CLOSED_SET_VIOLATION',
|
|
116
|
+
severity: 'block',
|
|
117
|
+
file: ch.file,
|
|
118
|
+
component: ch.name,
|
|
119
|
+
family: fam,
|
|
120
|
+
members: [...cf.members],
|
|
121
|
+
message: `"${ch.name}" claims role "${role}" but family "${fam}" is CLOSED `
|
|
122
|
+
+ `(canonical members: ${[...cf.members].join(', ') || '—'}). Reuse a member, `
|
|
123
|
+
+ `or open governance (ADR + /prd reconciliation) to extend the closed set — `
|
|
124
|
+
+ `do not ship a new ${fam}.`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
} else if (ch.kind === 'source' && ch.added) {
|
|
128
|
+
const base = String(ch.name || '').toLowerCase();
|
|
129
|
+
for (const fam of Object.keys(closed)) {
|
|
130
|
+
const cf = closed[fam];
|
|
131
|
+
if (cf.memberSources.has(ch.file)) continue; // it IS a member's source
|
|
132
|
+
const kw = familyKeyword(fam);
|
|
133
|
+
if (kw.length >= 4 && base.includes(kw) && !cf.members.has(ch.name)) {
|
|
134
|
+
findings.push({
|
|
135
|
+
code: 'DS_CLOSED_SET_SUSPECT',
|
|
136
|
+
severity: 'warn',
|
|
137
|
+
file: ch.file,
|
|
138
|
+
component: ch.name,
|
|
139
|
+
family: fam,
|
|
140
|
+
members: [...cf.members],
|
|
141
|
+
message: `new source "${ch.file}" looks like a "${fam}" but does not declare `
|
|
142
|
+
+ `canonical_for; family "${fam}" is CLOSED (${[...cf.members].join(', ')}). `
|
|
143
|
+
+ `Confirm you are reusing a member, not reinventing one.`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { findings, blockingCount: findings.filter((f) => f.severity === 'block').length };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---- signature-diff (Slice C — DS_DUPLICATE_PRIMITIVE, advisory) ------------
|
|
153
|
+
|
|
154
|
+
/** Manifest signature of a spec HEAD: the union of prop names, variants, composes. */
|
|
155
|
+
function specSignature(head) {
|
|
156
|
+
const s = new Set();
|
|
157
|
+
if (head && head.props && typeof head.props === 'object') for (const k of Object.keys(head.props)) s.add(`prop:${k}`);
|
|
158
|
+
for (const v of (head && head.variants) || []) s.add(`var:${v}`);
|
|
159
|
+
for (const c of (head && head.composes) || []) s.add(`use:${c}`);
|
|
160
|
+
return s;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const jaccard = (a, b) => {
|
|
164
|
+
if (a.size === 0 && b.size === 0) return 0;
|
|
165
|
+
let inter = 0;
|
|
166
|
+
for (const x of a) if (b.has(x)) inter++;
|
|
167
|
+
return inter / (a.size + b.size - inter);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Flag a newly-added spec whose manifest signature is too close to an existing
|
|
172
|
+
* one — the "semantically-equivalent rewrite" the closed-set check cannot catch
|
|
173
|
+
* by name. ADVISORY (warning), high threshold, never a block: signatures overlap
|
|
174
|
+
* legitimately (two list-like primitives), so a human/critic confirms.
|
|
175
|
+
* @param baseSpecs Map<name, {sig:Set, file}> existing (on-disk, non-added) specs
|
|
176
|
+
* @param added [{ name, sig:Set, file }]
|
|
177
|
+
*/
|
|
178
|
+
function signatureDuplicates(baseSpecs, added, threshold = 0.7) {
|
|
179
|
+
const findings = [];
|
|
180
|
+
for (const a of added) {
|
|
181
|
+
if (a.sig.size < 3) continue; // too small to judge
|
|
182
|
+
let best = null;
|
|
183
|
+
for (const [name, b] of baseSpecs) {
|
|
184
|
+
if (name === a.name) continue;
|
|
185
|
+
const j = jaccard(a.sig, b.sig);
|
|
186
|
+
if (j >= threshold && (!best || j > best.j)) best = { name, j, file: b.file };
|
|
187
|
+
}
|
|
188
|
+
if (best) {
|
|
189
|
+
findings.push({
|
|
190
|
+
code: 'DS_DUPLICATE_PRIMITIVE',
|
|
191
|
+
severity: 'warn',
|
|
192
|
+
file: a.file,
|
|
193
|
+
component: a.name,
|
|
194
|
+
message: `new component "${a.name}" has a manifest signature ${Math.round(best.j * 100)}% `
|
|
195
|
+
+ `overlapping existing "${best.name}" (${best.file}). Confirm it is not a rewrite of `
|
|
196
|
+
+ `an existing primitive — reuse or add a variant instead.`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return findings;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---- git plumbing ----------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
function git(cwd, args) {
|
|
206
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Resolve the baseline ref to read the COMMITTED INDEX/specs from. */
|
|
210
|
+
function resolveBaseline(cwd, base, trunk) {
|
|
211
|
+
if (base) return base;
|
|
212
|
+
// Prefer the merge-base with the trunk (branch-committed changes); else HEAD.
|
|
213
|
+
for (const t of [trunk, 'main', 'master'].filter(Boolean)) {
|
|
214
|
+
try { return git(cwd, ['merge-base', 'HEAD', t]).trim(); } catch (_) { /* try next */ }
|
|
215
|
+
}
|
|
216
|
+
try { git(cwd, ['rev-parse', 'HEAD']); return 'HEAD'; } catch (_) { return null; }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Show a path at a ref, or '' if absent there. */
|
|
220
|
+
function showAt(cwd, ref, rel) {
|
|
221
|
+
try { return git(cwd, ['show', `${ref}:${rel}`]); } catch (_) { return ''; }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Added/modified files in the change set (committed-vs-base ∪ working ∪ untracked). */
|
|
225
|
+
function collectChangedFiles(cwd, baselineRef) {
|
|
226
|
+
const set = new Map(); // file → added(bool); modified overrides to false only if not already added
|
|
227
|
+
const add = (file, added) => { if (!set.has(file)) set.set(file, added); };
|
|
228
|
+
const parseNameStatus = (out, addedCodes) => {
|
|
229
|
+
for (const line of out.split('\n')) {
|
|
230
|
+
if (!line.trim()) continue;
|
|
231
|
+
const parts = line.split('\t');
|
|
232
|
+
const code = parts[0][0];
|
|
233
|
+
const file = parts[parts.length - 1];
|
|
234
|
+
add(file, addedCodes.includes(code));
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
try { if (baselineRef) parseNameStatus(git(cwd, ['diff', '--name-status', baselineRef, '--']), ['A']); } catch (_) {}
|
|
238
|
+
try { parseNameStatus(git(cwd, ['diff', '--name-status']), ['A']); } catch (_) {}
|
|
239
|
+
try { parseNameStatus(git(cwd, ['diff', '--name-status', '--cached']), ['A']); } catch (_) {}
|
|
240
|
+
try {
|
|
241
|
+
const un = git(cwd, ['ls-files', '--others', '--exclude-standard']);
|
|
242
|
+
for (const f of un.split('\n')) if (f.trim()) add(f.trim(), true);
|
|
243
|
+
} catch (_) {}
|
|
244
|
+
return set;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const PascalFromFile = (rel) => {
|
|
248
|
+
const b = path.basename(rel).replace(/\.(tsx|ts|jsx|js)$/, '');
|
|
249
|
+
return b;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Full run: load config → read baseline policy → collect diff → classify → report.
|
|
254
|
+
* Returns { schema, ok, skipped?, reason?, findings, blockingCount }.
|
|
255
|
+
*/
|
|
256
|
+
function run(opts = {}) {
|
|
257
|
+
const cwd = opts.cwd || process.cwd();
|
|
258
|
+
const config = loadConfig(cwd);
|
|
259
|
+
const result = { schema: 'baldart.ds-gate/1', ok: true, findings: [], blockingCount: 0 };
|
|
260
|
+
|
|
261
|
+
if (!config) return { ...result, skipped: true, reason: 'no-config' };
|
|
262
|
+
const features = config.features || {};
|
|
263
|
+
if (!features.has_design_system) return { ...result, skipped: true, reason: 'no-design-system' };
|
|
264
|
+
|
|
265
|
+
const dsRoot = (config.paths && config.paths.design_system) || '';
|
|
266
|
+
const primitivesRoot = (config.paths && (config.paths.components_primitives || config.paths.components_root)) || '';
|
|
267
|
+
if (!dsRoot) return { ...result, skipped: true, reason: 'no-design-system-path' };
|
|
268
|
+
|
|
269
|
+
const indexRel = path.posix.join(dsRoot, 'INDEX.md');
|
|
270
|
+
const trunk = (config.git && config.git.trunk_branch) || null;
|
|
271
|
+
const baselineRef = resolveBaseline(cwd, opts.base, trunk);
|
|
272
|
+
if (!baselineRef) return { ...result, skipped: true, reason: 'no-git' };
|
|
273
|
+
|
|
274
|
+
const baselineIndex = showAt(cwd, baselineRef, indexRel) || (fs.existsSync(path.join(cwd, indexRel)) ? fs.readFileSync(path.join(cwd, indexRel), 'utf8') : '');
|
|
275
|
+
const families = parseSelectionPolicy(baselineIndex);
|
|
276
|
+
if (!families) return { ...result, skipped: true, reason: 'no-policy' };
|
|
277
|
+
const closed = closedFamilies(families, parseIndexSources(baselineIndex));
|
|
278
|
+
if (Object.keys(closed).length === 0) return { ...result, skipped: true, reason: 'no-closed-families' };
|
|
279
|
+
|
|
280
|
+
const changedFiles = collectChangedFiles(cwd, baselineRef);
|
|
281
|
+
const specDir = path.posix.join(dsRoot, 'components') + '/';
|
|
282
|
+
const changes = [];
|
|
283
|
+
for (const [file, added] of changedFiles) {
|
|
284
|
+
const norm = file.replace(/\\/g, '/');
|
|
285
|
+
if (norm.startsWith(specDir) && norm.endsWith('.md')) {
|
|
286
|
+
// a component spec → read its (working-tree, post-change) HEAD
|
|
287
|
+
let text = '';
|
|
288
|
+
try { text = fs.readFileSync(path.join(cwd, file), 'utf8'); } catch (_) { continue; }
|
|
289
|
+
const head = parseSpecHead(text);
|
|
290
|
+
if (!head) continue;
|
|
291
|
+
changes.push({
|
|
292
|
+
file, kind: 'spec', added,
|
|
293
|
+
name: head.name || PascalFromFile(norm),
|
|
294
|
+
canonicalFor: Array.isArray(head.canonical_for) ? head.canonical_for : [],
|
|
295
|
+
sig: specSignature(head),
|
|
296
|
+
});
|
|
297
|
+
} else if (primitivesRoot && norm.startsWith(primitivesRoot.replace(/\\/g, '/')) && /\.(tsx|jsx|ts|js)$/.test(norm) && !/\.(test|spec|stories)\./.test(norm)) {
|
|
298
|
+
changes.push({ file, kind: 'source', added, name: PascalFromFile(norm), canonicalFor: [] });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const ev = evaluate(closed, changes);
|
|
303
|
+
result.findings = ev.findings;
|
|
304
|
+
|
|
305
|
+
// Signature-diff (advisory): compare each ADDED spec against the on-disk
|
|
306
|
+
// sibling specs (the non-added ones are the baseline).
|
|
307
|
+
const addedSpecs = changes.filter((c) => c.kind === 'spec' && c.added && c.sig);
|
|
308
|
+
if (addedSpecs.length) {
|
|
309
|
+
const addedNames = new Set(addedSpecs.map((c) => c.name));
|
|
310
|
+
const baseSpecs = new Map();
|
|
311
|
+
const compDir = path.join(cwd, dsRoot, 'components');
|
|
312
|
+
let files = [];
|
|
313
|
+
try { files = fs.readdirSync(compDir).filter((f) => f.endsWith('.md')); } catch (_) {}
|
|
314
|
+
for (const f of files) {
|
|
315
|
+
const name = f.replace(/\.md$/, '');
|
|
316
|
+
if (addedNames.has(name)) continue;
|
|
317
|
+
try {
|
|
318
|
+
const h = parseSpecHead(fs.readFileSync(path.join(compDir, f), 'utf8'));
|
|
319
|
+
if (h) baseSpecs.set(h.name || name, { sig: specSignature(h), file: path.posix.join(dsRoot, 'components', f) });
|
|
320
|
+
} catch (_) {}
|
|
321
|
+
}
|
|
322
|
+
result.findings.push(...signatureDuplicates(baseSpecs, addedSpecs));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
result.blockingCount = result.findings.filter((f) => f.severity === 'block').length;
|
|
326
|
+
result.ok = result.blockingCount === 0;
|
|
327
|
+
result.baselineRef = baselineRef;
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
module.exports = {
|
|
332
|
+
parseSelectionPolicy, parseIndexSources, parseSpecHead, closedFamilies,
|
|
333
|
+
familyOf, familyKeyword, evaluate, specSignature, signatureDuplicates,
|
|
334
|
+
collectChangedFiles, resolveBaseline, run,
|
|
335
|
+
};
|