@codyswann/lisa 3.45.9 → 3.46.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/all/copy-overwrite/scripts/lisa-floor-collisions.mjs +17 -1
- package/all/copy-overwrite/scripts/lisa-gates.mjs +2 -2
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +54 -12
- package/dist/configs/eslint/expo.d.ts.map +1 -1
- package/dist/configs/eslint/expo.js +5 -0
- package/dist/configs/eslint/expo.js.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +15 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +70 -17
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/eslint-plugin-ui-standards/README.md +90 -0
- package/eslint-plugin-ui-standards/index.js +4 -0
- package/eslint-plugin-ui-standards/rules/no-unbound-design-value.js +375 -0
- package/expo/copy-overwrite/eslint.expo.ts +5 -0
- package/expo/copy-overwrite/scripts/bdd/discover.mjs +20 -4
- package/expo/copy-overwrite/scripts/bdd/markdown-cell.mjs +7 -1
- package/expo/copy-overwrite/scripts/bdd/parse.mjs +8 -2
- package/expo/copy-overwrite/scripts/check-e2e-coverage.mjs +8 -2
- package/expo/copy-overwrite/scripts/classify-maestro-failures.mjs +91 -10
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-design-intake/SKILL.md +169 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-design-intake/agents/openai.yaml +4 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +2 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-quality-review/SKILL.md +2 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-review-local/SKILL.md +9 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa/commands/design/intake.md +7 -0
- package/plugins/lisa/rules/eager/design-value-binding.md +80 -0
- package/plugins/lisa/rules/reference/design-value-binding.md +204 -0
- package/plugins/lisa/scripts/design-bindings-probe.mjs +713 -0
- package/plugins/lisa/scripts/design-intake-gate.mjs +470 -0
- package/plugins/lisa/scripts/design-variable-ids.mjs +565 -0
- package/plugins/lisa/skills/lisa-design-intake/SKILL.md +169 -0
- package/plugins/lisa/skills/lisa-design-intake/agents/openai.yaml +4 -0
- package/plugins/lisa/skills/lisa-implement/SKILL.md +2 -0
- package/plugins/lisa/skills/lisa-quality-review/SKILL.md +2 -0
- package/plugins/lisa/skills/lisa-review-local/SKILL.md +9 -0
- package/plugins/lisa/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa-agy/commands/lisa/design/intake.md +7 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/design-bindings-probe.mjs +713 -0
- package/plugins/lisa-agy/scripts/design-intake-gate.mjs +470 -0
- package/plugins/lisa-agy/scripts/design-variable-ids.mjs +565 -0
- package/plugins/lisa-agy/skills/lisa-design-intake/SKILL.md +169 -0
- package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +2 -0
- package/plugins/lisa-agy/skills/lisa-quality-review/SKILL.md +2 -0
- package/plugins/lisa-agy/skills/lisa-review-local/SKILL.md +9 -0
- package/plugins/lisa-agy/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/commands/lisa/design/intake.md +7 -0
- package/plugins/lisa-copilot/rules/eager/design-value-binding.md +80 -0
- package/plugins/lisa-copilot/rules/reference/design-value-binding.md +204 -0
- package/plugins/lisa-copilot/scripts/design-bindings-probe.mjs +713 -0
- package/plugins/lisa-copilot/scripts/design-intake-gate.mjs +470 -0
- package/plugins/lisa-copilot/scripts/design-variable-ids.mjs +565 -0
- package/plugins/lisa-copilot/skills/lisa-design-intake/SKILL.md +169 -0
- package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +2 -0
- package/plugins/lisa-copilot/skills/lisa-quality-review/SKILL.md +2 -0
- package/plugins/lisa-copilot/skills/lisa-review-local/SKILL.md +9 -0
- package/plugins/lisa-copilot/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/commands/lisa/design/intake.md +7 -0
- package/plugins/lisa-cursor/rules/design-value-binding-reference.mdc +209 -0
- package/plugins/lisa-cursor/rules/design-value-binding.mdc +85 -0
- package/plugins/lisa-cursor/scripts/design-bindings-probe.mjs +713 -0
- package/plugins/lisa-cursor/scripts/design-intake-gate.mjs +470 -0
- package/plugins/lisa-cursor/scripts/design-variable-ids.mjs +565 -0
- package/plugins/lisa-cursor/skills/lisa-design-intake/SKILL.md +169 -0
- package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +2 -0
- package/plugins/lisa-cursor/skills/lisa-quality-review/SKILL.md +2 -0
- package/plugins/lisa-cursor/skills/lisa-review-local/SKILL.md +9 -0
- package/plugins/lisa-cursor/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/commands/design/intake.md +7 -0
- package/plugins/src/base/rules/eager/design-value-binding.md +80 -0
- package/plugins/src/base/rules/reference/design-value-binding.md +204 -0
- package/plugins/src/base/scripts/design-bindings-probe.mjs +713 -0
- package/plugins/src/base/scripts/design-intake-gate.mjs +470 -0
- package/plugins/src/base/scripts/design-variable-ids.mjs +565 -0
- package/plugins/src/base/skills/lisa-design-intake/SKILL.md +169 -0
- package/plugins/src/base/skills/lisa-implement/SKILL.md +2 -0
- package/plugins/src/base/skills/lisa-quality-review/SKILL.md +2 -0
- package/plugins/src/base/skills/lisa-review-local/SKILL.md +9 -0
- package/plugins/src/base/skills/lisa-tdd-implementation/SKILL.md +1 -0
- package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +22 -6
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +10 -1
|
@@ -149,6 +149,57 @@ const PLACEHOLDER_METHODS = new Set([
|
|
|
149
149
|
"?",
|
|
150
150
|
]);
|
|
151
151
|
|
|
152
|
+
/**
|
|
153
|
+
* An `id:` line, with everything after the colon captured verbatim.
|
|
154
|
+
*
|
|
155
|
+
* Two deliberate spellings, both there to keep the match linear (S5852):
|
|
156
|
+
* `[ \t]` rather than `\s`, because this is applied to one already-split line
|
|
157
|
+
* and `\s`'s newline class only lets the indentation run overlap the next
|
|
158
|
+
* line's; and `(?:\S.*)?` rather than `.*`, because `[ \t]*(.*)` lets both
|
|
159
|
+
* halves claim the same spaces, which is a choice the engine has to make at
|
|
160
|
+
* every one of them.
|
|
161
|
+
*/
|
|
162
|
+
const ID_LINE = /^[ \t]*id:[ \t]*((?:\S.*)?)$/;
|
|
163
|
+
|
|
164
|
+
/** A `text:` line, read the same way. */
|
|
165
|
+
const TEXT_LINE = /^[ \t]*text:[ \t]*((?:\S.*)?)$/;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A scalar value with one layer of matching quotes removed.
|
|
169
|
+
*
|
|
170
|
+
* Character comparison rather than `['"]?(...)['"]?`, because the regex form
|
|
171
|
+
* cannot express "the same quote at both ends" without a backreference, and
|
|
172
|
+
* the shape it settled for — a lazy capture between two optional quotes and a
|
|
173
|
+
* trailing `\s*$` — has three quantifiers competing for the same characters.
|
|
174
|
+
* That is super-linear in the line's length (S5852) on flow files.
|
|
175
|
+
* @param {string} value - An already-trimmed scalar.
|
|
176
|
+
* @returns {string} The value without its surrounding quote pair.
|
|
177
|
+
*/
|
|
178
|
+
function unquote(value) {
|
|
179
|
+
const first = value.at(0);
|
|
180
|
+
const isQuote = first === "'" || first === '"';
|
|
181
|
+
return isQuote && value.length > 1 && value.at(-1) === first
|
|
182
|
+
? value.slice(1, -1)
|
|
183
|
+
: value;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The selector a `key: value` line declares, or null when the line is not one.
|
|
188
|
+
*
|
|
189
|
+
* An empty value reads as "no selector here" rather than as the empty
|
|
190
|
+
* selector, which is what the previous `[^'"\n]+?` (one character minimum)
|
|
191
|
+
* also did.
|
|
192
|
+
* @param {string} line - One line of flow YAML.
|
|
193
|
+
* @param {RegExp} pattern - The key's line pattern.
|
|
194
|
+
* @returns {string|null} The selector, or null.
|
|
195
|
+
*/
|
|
196
|
+
function scalarSelector(line, pattern) {
|
|
197
|
+
const match = pattern.exec(line);
|
|
198
|
+
if (!match) return null;
|
|
199
|
+
const value = unquote(match[1].trim());
|
|
200
|
+
return value === "" ? null : value;
|
|
201
|
+
}
|
|
202
|
+
|
|
152
203
|
/**
|
|
153
204
|
* Pull `id:`/`text:` selectors and their ceilings out of a flow's YAML source.
|
|
154
205
|
*
|
|
@@ -178,7 +229,12 @@ export function extractGates(source) {
|
|
|
178
229
|
|
|
179
230
|
for (const line of lines) {
|
|
180
231
|
if (/^\s*#/.test(line) || line.trim() === "") continue;
|
|
181
|
-
|
|
232
|
+
// `(?:-\s*)?` rather than `-?\s*`: with two `\s*` runs either side of an
|
|
233
|
+
// optional dash, both can claim the same spaces and the engine has a
|
|
234
|
+
// choice at every one — super-linear in the line's length (S5852). Tying
|
|
235
|
+
// the dash to the whitespace that follows it removes the choice and
|
|
236
|
+
// recognises exactly the same lines.
|
|
237
|
+
const command = line.match(/^(\s*)(?:-\s*)?(\w+):/);
|
|
182
238
|
if (command && GATE_COMMANDS.includes(command[2])) {
|
|
183
239
|
if (inGate) flush();
|
|
184
240
|
inGate = true;
|
|
@@ -198,18 +254,35 @@ export function extractGates(source) {
|
|
|
198
254
|
timeout = Number(timeoutMatch[1]);
|
|
199
255
|
continue;
|
|
200
256
|
}
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
203
|
-
pending.push({ kind: "id", selector:
|
|
257
|
+
const idSelector = scalarSelector(line, ID_LINE);
|
|
258
|
+
if (idSelector !== null) {
|
|
259
|
+
pending.push({ kind: "id", selector: idSelector });
|
|
204
260
|
continue;
|
|
205
261
|
}
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
262
|
+
const textSelector = scalarSelector(line, TEXT_LINE);
|
|
263
|
+
if (textSelector !== null)
|
|
264
|
+
pending.push({ kind: "text", selector: textSelector });
|
|
208
265
|
}
|
|
209
266
|
if (inGate) flush();
|
|
210
267
|
return gates;
|
|
211
268
|
}
|
|
212
269
|
|
|
270
|
+
/** The extension a referenced flow must carry to be followed. */
|
|
271
|
+
const YAML_SUFFIX = ".yaml";
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* A `runFlow: <path>` line, in the shorthand list-item form.
|
|
275
|
+
*
|
|
276
|
+
* `[ \t]` rather than `\s` throughout, and the dash tied to the whitespace
|
|
277
|
+
* that follows it. Under `m`, a `\s*` run can cross a newline into the next
|
|
278
|
+
* line's own `^`, so the scan re-attempts the same characters from many start
|
|
279
|
+
* positions — super-linear in the file's length (S5852).
|
|
280
|
+
*/
|
|
281
|
+
const INLINE_RUN_FLOW = /^[ \t]*(?:-[ \t]*)?runFlow:[ \t]*['"]?([^'"\s]+)/gm;
|
|
282
|
+
|
|
283
|
+
/** The `file: <path>` line of the expanded `runFlow:` form. */
|
|
284
|
+
const NESTED_RUN_FLOW = /^[ \t]*file:[ \t]*['"]?([^'"\s]+)/gm;
|
|
285
|
+
|
|
213
286
|
/**
|
|
214
287
|
* Resolve the `runFlow:` targets a flow references, as absolute paths.
|
|
215
288
|
* @param {string} source - Flow YAML source
|
|
@@ -219,10 +292,18 @@ export function extractGates(source) {
|
|
|
219
292
|
export function extractRunFlowTargets(source, flowPath) {
|
|
220
293
|
const dir = path.dirname(flowPath);
|
|
221
294
|
const targets = [];
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
295
|
+
// The `.yaml` suffix is checked in JS rather than written as `[^'"\s]+\.yaml`.
|
|
296
|
+
// A greedy class that already contains `.` has to give characters back one at
|
|
297
|
+
// a time before the literal suffix can match, at every start position a `g`
|
|
298
|
+
// scan tries — super-linear in the source's length (S5852), on flow files.
|
|
299
|
+
// The class stops at whitespace or a quote either way, so the token it
|
|
300
|
+
// captures is the same one; only the suffix test moved.
|
|
301
|
+
for (const match of source.matchAll(INLINE_RUN_FLOW))
|
|
302
|
+
if (match[1].endsWith(YAML_SUFFIX))
|
|
303
|
+
targets.push(path.resolve(dir, match[1]));
|
|
304
|
+
for (const match of source.matchAll(NESTED_RUN_FLOW))
|
|
305
|
+
if (match[1].endsWith(YAML_SUFFIX))
|
|
306
|
+
targets.push(path.resolve(dir, match[1]));
|
|
226
307
|
return targets;
|
|
227
308
|
}
|
|
228
309
|
|
package/package.json
CHANGED
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"ws": ">=8.21.0"
|
|
133
133
|
},
|
|
134
134
|
"name": "@codyswann/lisa",
|
|
135
|
-
"version": "3.
|
|
135
|
+
"version": "3.46.0",
|
|
136
136
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
137
137
|
"main": "dist/index.js",
|
|
138
138
|
"exports": {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lisa-design-intake
|
|
3
|
+
description: "Design-handoff gate for a work…"
|
|
4
|
+
allowed-tools: ["Skill", "Bash", "Read", "Write"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Design Intake: $ARGUMENTS
|
|
8
|
+
|
|
9
|
+
`$ARGUMENTS` is one canonical work-item reference (JIRA key, Linear ref, `org/repo#123`), optionally followed by a design reference. With no arguments, resolve the work item from the worktree binding (`node scripts/lisa-work-item.mjs current`).
|
|
10
|
+
|
|
11
|
+
Run one design-handoff cycle against that work item: resolve the regime per axis, gather the facts, let `scripts/design-intake-gate.mjs` decide, and either escalate a block or hand back a proceed with the derived values recorded.
|
|
12
|
+
|
|
13
|
+
This skill carries the **judgment**. It does not carry the **policy** — that is the `design-value-binding` rule, which it cites rather than restates. Read the rule; do not re-derive its conditions here.
|
|
14
|
+
|
|
15
|
+
## What this is not
|
|
16
|
+
|
|
17
|
+
`design-source-of-truth` and `scripts/design-source-gate.mjs` already ask whether a changed surface **declares where its design came from**. This asks the orthogonal question: are the **values on it bound**. Both apply. A surface can cite a perfectly valid design node and still paint a literal that no variable backs — that is the case this skill exists for, and it is invisible to the other gate.
|
|
18
|
+
|
|
19
|
+
## Phase 0 — Is there a design source at all?
|
|
20
|
+
|
|
21
|
+
**A design source is optional, and this is the most important step in the skill.** Most projects have no designs. Run the probe and read its verdict:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
node "${CLAUDE_PLUGIN_ROOT:-.}/scripts/design-bindings-probe.mjs" --json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
A `SKIPPED` verdict means one of: `design.tokens.source` is unset, no access token is present, or no committed variable-id map exists. **Say so and stop cleanly — do not block.** A mandatory gate on an absent integration breaks every project that has no designs, which is a worse outcome than any drift it would have caught.
|
|
28
|
+
|
|
29
|
+
If a design source *is* configured, then per `tool-access-gate` a failed probe **against a configured source** is a block, not a reason to fall back to reading pixels: post the Access Needed comment, move the item to the configured `blocked` role with the `human_needed` marker, and stop.
|
|
30
|
+
|
|
31
|
+
## Phase 1 — Resolve configuration
|
|
32
|
+
|
|
33
|
+
Read `.lisa.config.local.json` first, then `.lisa.config.json`; local overrides global per key. Use `jq` — never hand-parse JSON.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
read_key() {
|
|
37
|
+
local key="$1" local_v global_v
|
|
38
|
+
local_v=$(jq -r "$key // empty" .lisa.config.local.json 2>/dev/null)
|
|
39
|
+
global_v=$(jq -r "$key // empty" .lisa.config.json 2>/dev/null)
|
|
40
|
+
printf '%s' "${local_v:-$global_v}"
|
|
41
|
+
}
|
|
42
|
+
tokens_source=$(read_key '.design.tokens.source')
|
|
43
|
+
escalation_assignee=$(read_key '.design.escalation.assignee')
|
|
44
|
+
escalation_label=$(read_key '.design.escalation.label')
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| Key | Missing means |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `design.tokens.source` | Stop and report: the authoritative design source is not configured, so there is nothing to resolve the regime against. |
|
|
50
|
+
| `design.escalation.assignee` | **A block condition in its own right.** Do not guess a person, do not skip the escalation, do not assign to the current user. The gate raises `escalation-target-unset` and the run ends blocked. |
|
|
51
|
+
| `design.escalation.label` | Nothing. The label is an additive marker; absent means no marker is added. |
|
|
52
|
+
|
|
53
|
+
**Never write a person's name, handle, or identity into any artifact this skill produces.** These are config keys; the host supplies the values.
|
|
54
|
+
|
|
55
|
+
## Phase 2 — Resolve the regime, per axis
|
|
56
|
+
|
|
57
|
+
For each of the six axes — `color`, `spacing`, `typography`, `radius`, `elevation`, `motion` — determine whether the design source publishes variables covering it. **Never ask a human.**
|
|
58
|
+
|
|
59
|
+
**Do not try to list the published collections.** That route does not exist headlessly, and the measurement is worth carrying:
|
|
60
|
+
|
|
61
|
+
| Route | Gives names? | Headless? |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| Variables REST (`/v1/files/:key/variables/local`) | yes | **no** — Enterprise-plan only; the read scope is not offered in the token scope picker on other plans, so no token change unlocks it |
|
|
64
|
+
| Design-tool MCP (`get_variable_defs`) | yes, every plan | **no** — browser OAuth, which cron, CI, and a subagent cannot perform |
|
|
65
|
+
| `/v1/files/:key/nodes` | no — opaque `VariableID:106:15` | **yes**, on a plain access token |
|
|
66
|
+
|
|
67
|
+
A gate built on either of the first two runs interactively and silently no-ops everywhere else. So the regime is derived from the **committed variable-id map**, which `design-variable-ids.mjs` produced once, interactively, and which `design-bindings-probe.mjs` reads headlessly. An axis is typed when the map names at least one variable in its namespace.
|
|
68
|
+
|
|
69
|
+
`result.regime` from Phase 0 already carries this. Use it; do not re-derive it.
|
|
70
|
+
|
|
71
|
+
A mixed regime is the expected case, not an error state. Colour blocking while spacing gets measured, in the same work item, is the design working correctly.
|
|
72
|
+
|
|
73
|
+
**Probe the subtree you are implementing, not the enclosing screen.** Pass `--node` for the component you will actually build. A frame-level read counts the chrome behind a modal and over-reports — one measured work item scored 14 bound values at frame level and **zero** inside the modal subtree, and applying this rule changed 5 of 11 real work-item verdicts.
|
|
74
|
+
|
|
75
|
+
### When the map cannot name what it saw
|
|
76
|
+
|
|
77
|
+
The probe reports `owner: "us"` when a `VariableID` is unknown (the map is stale) or ambiguous (two variables share a value). **Neither is the designer's fault** — those values ARE bound. Do not escalate them to design. Regenerate the map:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
FIGMA_ACCESS_TOKEN=… FIGMA_MCP_TOKEN=… \
|
|
81
|
+
node "${CLAUDE_PLUGIN_ROOT:-.}/scripts/design-variable-ids.mjs" \
|
|
82
|
+
--file <key> --light <nodes> --dark <nodes>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Pass `--dark` wherever the library has a dark mode: the light+dark signature is what separates variables that share a value, and without it more ids stay ambiguous. An ambiguous id is still a failure — guessing which variable a value came from is exactly what this contract forbids.
|
|
86
|
+
|
|
87
|
+
## Phase 3 — Gather findings
|
|
88
|
+
|
|
89
|
+
The probe already produced `hardcoded-in-design` and `bound` findings for every value in the subtree, with the two silent-under-reporting traps handled: the corner-keyed `rectangleCornerRadii` shape is normalised, and boundness is read from `boundVariables` directly rather than inferred from a resolved value (Figma omits zero-valued properties, so a padding bound to a zero-valued variable would otherwise vanish).
|
|
90
|
+
|
|
91
|
+
Add the findings the probe cannot see from a node payload — a named variable that does not exist, an unpublished component, a missing state, a source disagreement — plus the two non-blocking observations. **Findings are observations, not verdicts** — do not pre-judge them; the gate decides.
|
|
92
|
+
|
|
93
|
+
| `kind` | Emit when | Required fields |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `bound` | A variable exists and the design binds it. The happy path. | `axis`, `component` |
|
|
96
|
+
| `hardcoded-in-design` | The frame paints a literal instead of binding a variable. | `axis`, `component`, `value` |
|
|
97
|
+
| `measured` | You read the value off the rendering rather than from a variable. | `axis`, `component`, `value` |
|
|
98
|
+
| `missing-token` | The item or frame names a variable the library does not publish. | `axis`, `component`, `name` |
|
|
99
|
+
| `unpublished-component` | The component lives in a draft or local file. | `component` |
|
|
100
|
+
| `missing-state` | A required state (disabled / error / loading / empty) has no design. | `component`, `state` |
|
|
101
|
+
| `source-disagreement` | The bound variable resolves to one value and the frame renders another. | `axis`, `component`, `tokenValue`, `frameValue` |
|
|
102
|
+
| `one-off` | A value that was never going to be shared — an illustration's exact offset. | `axis`, `component`, `value` |
|
|
103
|
+
| `aesthetic-concern` | You find the design ambiguous, unusual, or ugly, and every value you need is bound. | `component`, `note` |
|
|
104
|
+
|
|
105
|
+
Two of these rows carry most of the weight.
|
|
106
|
+
|
|
107
|
+
**`hardcoded-in-design` is the condition most often missed**, because nothing about the artifact looks wrong. The design is finished, the component renders, review sees a screenshot that matches — and the only symptom is that there is nothing to extract. Treating "I can see the value" as "I have the value" is precisely the failure this gate exists to catch.
|
|
108
|
+
|
|
109
|
+
**`aesthetic-concern` is a finding, not a block, and it must still be emitted.** Recording the observation without stopping on it is what keeps the gate from firing on opinion. If you find yourself reaching for a block because the design seems off rather than because a value is missing, that is an `aesthetic-concern` and the work proceeds.
|
|
110
|
+
|
|
111
|
+
Emit no finding kind outside this table. The gate throws on an unrecognised kind rather than inventing a verdict for facts it did not understand.
|
|
112
|
+
|
|
113
|
+
## Phase 4 — Let the gate decide
|
|
114
|
+
|
|
115
|
+
Merge the probe's findings with your own, write them to a JSON payload, and run the gate. Do not reimplement the decision in prose — a verdict reasoned out fresh each time is a verdict that decides differently on Tuesday.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
node "${CLAUDE_PLUGIN_ROOT:-.}/scripts/design-intake-gate.mjs" --findings=design-facts.json --json
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Payload shape:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"config": { "design": { "escalation": { "assignee": "…", "label": "…" } } },
|
|
126
|
+
"regime": { "color": "typed", "spacing": "untyped" },
|
|
127
|
+
"findings": [ { "kind": "hardcoded-in-design", "axis": "color", "component": "Raised card", "value": "#3A7BD5" } ]
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Omit `config` to have the gate read the merged project config itself. Exit 0 = PROCEED, 1 = BLOCK, 2 = usage. **The gate fails closed**: an unreadable payload is a BLOCK with a named reason, never a quiet pass, because a gate that returns PROCEED on what it could not look at proves nothing.
|
|
132
|
+
|
|
133
|
+
## Phase 5a — BLOCK
|
|
134
|
+
|
|
135
|
+
1. **Post the comment.** Use `result.comment` **verbatim**. It is written for the non-technical operator standing at the gate: plain language, naming the specific missing artifact and what to do about it, with no factory vocabulary. Do not "improve" it into engineering terms — the vocabulary ban is enforced by the gate's own tests, and rewriting the comment is how it gets lost.
|
|
136
|
+
|
|
137
|
+
> The 'Raised card' component uses the colour #3A7BD5 directly rather than a colour variable. I need that colour published as a variable so the app and the design stay in sync — otherwise I'd be copying a number that changes without warning.
|
|
138
|
+
|
|
139
|
+
2. **Transition to the configured `blocked` role** through `lisa-tracker-sync` / `lisa-tracker-write`. Never call a vendor label API directly — per `config-resolution`, Linear resolves `blocked` to a native workflow **state**, GitHub to a **label**, JIRA to a **status**, and hardcoding any one of them breaks the other two.
|
|
140
|
+
|
|
141
|
+
3. **Assign to `result.assignee`.** If it is `null`, the `escalation-target-unset` condition is already in `result.blocks` and the comment says so — post it and leave the item unassigned. Do not substitute a person.
|
|
142
|
+
|
|
143
|
+
4. **Add `result.label` as an additive marker** alongside the `blocked` role when one is configured, never instead of it. Add `human_needed` as well: a missing variable, an unpublished component, and a source disagreement all need a person to make a design decision, and none of them clear on a retry.
|
|
144
|
+
|
|
145
|
+
5. **Stop.** Do not partially build the surfaces that were fine. Design decisions arrive together, and half a screen built against half a design is rework.
|
|
146
|
+
|
|
147
|
+
**Route by owner.** Only `owner: "design"` reaches a designer. A stale or ambiguous id is `owner: "us"` — regenerate the map (Phase 2) and re-run. Sending that to a designer is the wrong person and the wrong work, and it teaches them to ignore the next one.
|
|
148
|
+
|
|
149
|
+
## Phase 5b — PROCEED
|
|
150
|
+
|
|
151
|
+
1. **Record the derived values** from `result.derived` on the work item — axis, component, and value for everything measured in an untyped axis. This costs nothing now and accumulates into an inventory of what the variable system is missing, **ranked by what people actually needed**, which no design-system audit produces on its own.
|
|
152
|
+
2. Build. In a **typed** axis, bind the variable and use a screenshot only to *verify*; a disagreement you find at that point is `source-disagreement` and sends the item back through this skill. In an **untyped** axis, measure — that is the correct source there.
|
|
153
|
+
|
|
154
|
+
The rule is never "do not look at pixels". It is **"do not derive a value from pixels when a binding exists."**
|
|
155
|
+
|
|
156
|
+
## Contract
|
|
157
|
+
|
|
158
|
+
- **Block on unbound, never on unsure.** Every block traces to one of the eight conditions in `BLOCK_CONDITIONS`. If you cannot name the condition, it is not a block.
|
|
159
|
+
- **The regime is per-axis**, derived from the committed variable-id map. Never per-project, never asked of a human, and never from a live collection query — that route does not run headlessly.
|
|
160
|
+
- **A design source is optional.** No source, no token, or no map is SKIPPED at exit 0, never a block.
|
|
161
|
+
- **Every failure names an owner.** Unbound values are design's; a stale or ambiguous map is ours.
|
|
162
|
+
- **The threshold is 100%.** Relax it only with an explicit `--min` on the command line, never by softening anything in code.
|
|
163
|
+
- **Never guess an escalation target**, and never proceed without one.
|
|
164
|
+
- **Never pick a side in a disagreement.** Which source is right is a design decision.
|
|
165
|
+
- **Never name a person** in a comment, a commit, or any artifact. Config keys only.
|
|
166
|
+
- **Never work around missing design information.** Substituting a nearby value, inventing a state, or narrowing scope to dodge the gap are all forbidden — same standard as `tool-access-gate`.
|
|
167
|
+
- Host design-system rules (`figma-design-system`, `design-system`, `use-the-design-library`, or the project's equivalent) stay authoritative about *what* to build. This skill never overrides them.
|
|
168
|
+
|
|
169
|
+
Full policy: the `design-value-binding` rule.
|
|
@@ -157,6 +157,8 @@ For any Fix flow, and for any Build flow that changes user-visible behavior, reg
|
|
|
157
157
|
|
|
158
158
|
For work that adds or changes a **UI surface**, the `design-source-of-truth` rule adds a second non-demotable deliverable landing in this same PR: every UI surface the change touches declares where its design came from. Figma is the source of truth, so **prefer sync-back** — if the surface is not in Figma yet and the tool-access preflight proved Figma access, reflect it there and cite the node with `DESIGN-SOURCE: <figma-url>`. Only when the surface genuinely does not belong in the design source (debug affordance, dev-only playground, internal tooling) does it carry the exception marker `DESIGN-SOURCE: none — not in Figma`, ideally with a trailing reason. `scripts/design-source-gate.mjs` decides this deterministically and fails closed on anything it cannot resolve; a FAIL blocks the item exactly as a failing coverage gate does. Host design-system rules (`figma-design-system`, `design-system`, `use-the-design-library`, or the project's equivalent) stay authoritative about *what* to build — this obligation is only about declaring the source. Cite the rule; do not restate its marker grammar, host-precedence, or bootstrap mechanics here.
|
|
159
159
|
|
|
160
|
+
The `design-value-binding` rule adds a second, orthogonal obligation on the same surfaces: **values come from design variables where a variable system exists.** That contract asks whether the values are *bound*, not whether the source is *declared* — a surface can cite a valid design node and still paint a literal no variable backs. Resolve the regime per axis, block only on the five objectively checkable conditions, and record every value derived in an untyped axis on the work item. `/lisa:design:intake` runs it. Cite the rule; do not restate its conditions, config schema, or comment grammar here.
|
|
161
|
+
|
|
160
162
|
For work that adds or changes **persistent state**, the `reset-seed-coverage` rule adds a third non-demotable deliverable landing in this same PR: every entity the item introduces or changes is classified in the project's state contract (`fixture-owned` / `preserve` / `derived-rebuild` / `forbidden`) with a reason and an owner, anything `fixture-owned` declares its ownership predicate and is actually swept, and the state-classification check passes. Writing a flow that creates a record and deletes it only on its happy path does NOT satisfy this — that is the leak, not the coverage. Cite the rule; do not restate its policy, waiver, or bootstrap mechanics here.
|
|
161
163
|
|
|
162
164
|
The team lead may not waive, defer, demote, or phrase this regression spec as "optional", "if cheap", "nice to have", or equivalent. The only permitted exits are:
|
|
@@ -24,6 +24,8 @@ For each changed file, evaluate:
|
|
|
24
24
|
|
|
25
25
|
Exit 1 is a **Critical** finding under the `design-source-of-truth` rule -- the change is blocked until every UI surface either cites a Figma node (`DESIGN-SOURCE: <figma-url>`, the preferred fix -- sync it back) or carries the exception marker `DESIGN-SOURCE: none — not in Figma`. The gate fails closed: an unreadable file or an uncomputable diff is a FAIL, not a pass. Host design-system rules (`figma-design-system`, `design-system`, `use-the-design-library`, or the project's equivalent) stay authoritative about what to build; this checks only that the source is declared. If the gate script is absent, say so in the review rather than skipping silently.
|
|
26
26
|
|
|
27
|
+
Review the same surfaces against the `design-value-binding` rule as well — it asks the orthogonal question of whether each value is *bound* to what the design system publishes, not whether the source is declared. A literal in an axis the project publishes variables for is a **Critical** finding; the identical literal in an axis with no variable collection is correct and must not be flagged. Aesthetic disagreement is never a finding under this rule. Cite the rule; do not restate its conditions here.
|
|
28
|
+
|
|
27
29
|
## Output Format
|
|
28
30
|
|
|
29
31
|
Rank findings by severity:
|
|
@@ -41,6 +41,15 @@ asks whether the source is declared. If the gate script is not present (the
|
|
|
41
41
|
project predates the plugin version that ships it), say so explicitly in the
|
|
42
42
|
review — a silent skip is not one of the exits.
|
|
43
43
|
|
|
44
|
+
**Design-value binding (`design-value-binding` rule).** A separate, orthogonal
|
|
45
|
+
question about the same surfaces: not *is the source declared* but *are the
|
|
46
|
+
values bound*. In an axis the project publishes design variables for, a literal
|
|
47
|
+
is a **blocking** finding — it is a copied number that will drift when the
|
|
48
|
+
system moves. In an axis with no variable collection, the identical literal is
|
|
49
|
+
correct and must not be flagged; measuring is the legitimate source there, and
|
|
50
|
+
the derived values belong on the work item. Aesthetic disagreement is never a
|
|
51
|
+
finding under this rule. Cite the rule; do not restate its conditions here.
|
|
52
|
+
|
|
44
53
|
To do this, follow these steps precisely:
|
|
45
54
|
|
|
46
55
|
1. Use a Haiku agent to check the current git state:
|
|
@@ -72,6 +72,7 @@ TDD Cycle:
|
|
|
72
72
|
- Write the minimum production code to make the test pass
|
|
73
73
|
- Do not optimize, do not add features beyond what the test requires
|
|
74
74
|
- For any UI surface the implementation adds or changes, the `design-source-of-truth` rule requires its design-source declaration to land in the same commit as the surface it describes — `DESIGN-SOURCE: <figma-url>` when the surface is backed by a Figma node (sync it back first if Figma access exists), or the exception marker `DESIGN-SOURCE: none — not in Figma` when it genuinely is not captured at the source. `scripts/design-source-gate.mjs` fails closed on an undeclared surface. Cite the rule; do not restate its marker grammar here.
|
|
75
|
+
- Separately, the `design-value-binding` rule governs where each *value* on that surface came from: in an axis with a published variable collection, bind the variable and use a screenshot only to verify; in an axis with none, measure and record what you derived. A literal in a typed axis is a block, not a TODO. Cite the rule; do not restate its conditions here.
|
|
75
76
|
- The goal is the simplest code that makes the test green
|
|
76
77
|
|
|
77
78
|
### REFACTOR Phase
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Design-handoff gate for a work item with a UI surface. Resolves the source of truth per axis (colour, spacing, typography, radius, elevation, motion) by querying the configured design source's published variable collections — never by asking a human — then judges the item against the five objectively checkable block conditions. Values come from design variables where a variable system exists; measurement is the legitimate source where none does. Blocks on unbound, never on unsure. Escalates through the configured tracker to `design.escalation.assignee` with a plain-language comment — an unset assignee is itself a block, never a silent skip — and records every measured value so the gaps in the variable system accumulate on their own."
|
|
3
|
+
argument-hint: "[work-item-ref] [design-ref]"
|
|
4
|
+
allowed-tools: ["Skill"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Use the /lisa-design-intake skill to run one design-handoff cycle: prove access to the design source, resolve the regime per axis, gather findings, let `scripts/design-intake-gate.mjs` decide, then either escalate the block to the configured assignee with the verbatim plain-language comment or proceed with the derived values recorded on the work item. $ARGUMENTS
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Design Value Binding (load-bearing)
|
|
2
|
+
|
|
3
|
+
**Values come from design variables where a variable system exists.** Visual measurement is supplemental there, and it is the legitimate primary source where no variable system exists. Missing design information blocks the work item; it is never worked around.
|
|
4
|
+
|
|
5
|
+
**One vendor-neutral contract, cited by** `lisa-design-intake`, `lisa-implement`, `lisa-tdd-implementation`, `lisa-review-local`, and `lisa-quality-review` (the `leaf-only-lifecycle` / `repo-scope-split` precedent: one shared slug, never divergent per-skill prose).
|
|
6
|
+
|
|
7
|
+
This is **not** `design-source-of-truth`. That contract asks whether a changed surface *declares where its design came from*. This one asks whether the *values on it are bound*. A surface can cite a perfectly valid design node and still paint a literal that no variable backs — the design looks finished and has nothing to extract. Both apply; neither substitutes for the other.
|
|
8
|
+
|
|
9
|
+
## Regime is per-dimension, not per-project
|
|
10
|
+
|
|
11
|
+
Resolve the source of truth **per-axis** — colour, spacing, typography, radius, elevation, motion — from what the design source **actually publishes**. **Never by asking a human**, and never once per project.
|
|
12
|
+
|
|
13
|
+
The axis vocabulary is fixed, and the identifiers are what the config and the lint rule use: `color`, `spacing`, `typography`, `radius`, `elevation`, `motion`. Projects declare which of them are typed; they never invent a seventh.
|
|
14
|
+
|
|
15
|
+
| axis has a published variable collection | source of truth | an unbound value means |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| yes (**typed**) | the variable | **block** |
|
|
18
|
+
| no (**untyped**) | visual measurement | measure it, and record what you derived |
|
|
19
|
+
|
|
20
|
+
A library with a mature colour system and no spacing scale is the common case and must work: colour blocks while spacing gets measured, in the same work item, without contradiction.
|
|
21
|
+
|
|
22
|
+
**Asking the design tool "which collections are published?" does not work headlessly, and that is measured.** The Variables REST endpoint returns names but is Enterprise-plan only; the design-tool MCP returns names on every plan but authenticates by browser OAuth, which cron, CI, and a subagent cannot perform. A gate built on either runs interactively and silently no-ops everywhere else. So the regime is derived from a **committed variable-id map**: `/v1/files/:key/nodes` reports every binding as an opaque `VariableID`, that id→name mapping is static, and `design-variable-ids.mjs` resolves it once interactively so `design-bindings-probe.mjs` runs headlessly on a plain access token forever after. An axis is typed when the committed map names at least one variable in its namespace.
|
|
23
|
+
|
|
24
|
+
**Staleness is self-detecting, and that is what makes a committed map safe to trust.** An id the map has never seen fails loudly telling you to regenerate; it never silently resolves to the wrong variable.
|
|
25
|
+
|
|
26
|
+
**Measure the subtree you are implementing, not the enclosing screen.** A frame-level read counts the chrome behind a modal and over-reports — one measured work item scored 14 bound values at frame level and zero inside the modal subtree it actually had to build.
|
|
27
|
+
|
|
28
|
+
## Block on *unbound*, never on *unsure*
|
|
29
|
+
|
|
30
|
+
This distinction is the whole contract. "I cannot tell what they meant" is a judgment call, and an agent asked to make it blocks on everything or nothing. "The design does not bind a value I need" is objective and decides the same way twice.
|
|
31
|
+
|
|
32
|
+
**Block conditions — five, all objectively checkable:**
|
|
33
|
+
|
|
34
|
+
1. **A named token does not exist.** The work item or frame references a variable the library does not have.
|
|
35
|
+
2. **A value is hardcoded in the design file itself, in a typed axis.** The most important and easiest to miss: the frame paints a literal instead of binding a variable, so the design looks finished and there is nothing to extract.
|
|
36
|
+
3. **The component is not published.** Local to a draft file, so there is no stable reference.
|
|
37
|
+
4. **A required state has no design.** The work item specifies disabled/error/loading and the component set has no such variant.
|
|
38
|
+
5. **Two sources disagree.** The bound token says one value, the frame renders another. Never guess which is right.
|
|
39
|
+
|
|
40
|
+
**Explicit non-block list — equally load-bearing, or the gate fires constantly:**
|
|
41
|
+
|
|
42
|
+
- Anything in an **untyped** axis. Measure it.
|
|
43
|
+
- One-off values that are not semantic (an illustration's exact offset).
|
|
44
|
+
- Anything where a token exists and is bound — the happy path.
|
|
45
|
+
- **Aesthetic uncertainty.** If every value needed is bound and the agent merely finds the design ambiguous or ugly, that is an opinion, not a block.
|
|
46
|
+
|
|
47
|
+
## What visual matching is for, precisely
|
|
48
|
+
|
|
49
|
+
In a **typed** axis it is *verification*: build from the variable, screenshot, confirm agreement — and a disagreement is condition 5, not a licence to trust the pixels. In an **untyped** axis it is *derivation*, and legitimate. The rule is never "do not look at pixels"; it is **"do not derive a value from pixels when a binding exists."**
|
|
50
|
+
|
|
51
|
+
## Derived values are recorded, not just used
|
|
52
|
+
|
|
53
|
+
When measuring in an untyped axis, record the derived values on the work item. Over time that accumulates a free inventory of what the token system is missing, ranked by what people actually needed.
|
|
54
|
+
|
|
55
|
+
## Escalation is configured, never hardcoded
|
|
56
|
+
|
|
57
|
+
The escalation target, label, and authoritative design source are host configuration — `design.escalation.assignee`, `design.escalation.label`, `design.tokens.source`. **An unset `assignee` is itself a block condition**: a blocked item assigned to nobody is an item nobody sees. Escalation routes through the vendor-neutral tracker abstraction per `config-resolution` — Linear lifecycle is native state, GitHub uses labels, JIRA uses status — never a hardcoded label call.
|
|
58
|
+
|
|
59
|
+
The blocked comment is written for the non-technical operator standing at the gate: plain language, no engineering vocabulary, naming the specific missing artifact and what to do about it.
|
|
60
|
+
|
|
61
|
+
## A design source is OPTIONAL; a configured one fails closed
|
|
62
|
+
|
|
63
|
+
**Most projects have no designs, and this contract must not break them.** No configured design source, no access token, or no committed id map is **SKIPPED** — loudly, exit 0. A mandatory gate on an absent integration breaks every non-design project on upgrade, which is a worse outcome than any drift it would have caught.
|
|
64
|
+
|
|
65
|
+
Where a design source *is* configured, the gate fails closed: `scripts/design-bindings-probe.mjs` gathers the facts, `scripts/design-intake-gate.mjs` decides deterministically, and `/lisa:design:intake` carries the judgment between them. Facts it could not gather are a block with a named reason, never a quiet pass. The threshold is **100% by default** — the rule as written — with a `--min` flag so any relaxation is an explicit reviewable decision on the command line rather than a quiet softening in code.
|
|
66
|
+
|
|
67
|
+
The lint rung (`ui-standards/no-unbound-design-value`) catches the same defect at authoring time, and is silent on any axis the project has not declared typed.
|
|
68
|
+
|
|
69
|
+
## Every failure names an OWNER
|
|
70
|
+
|
|
71
|
+
Conflating these sends the wrong person the wrong work.
|
|
72
|
+
|
|
73
|
+
| Owner | Failure | What happens |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| **design** | Values are unbound. | Block the work item with the exact bind-list. |
|
|
76
|
+
| **us** | The committed id map is stale or ambiguous. | The values ARE bound; regenerate the map. Not the designer's problem. |
|
|
77
|
+
|
|
78
|
+
An ambiguous id is still a failure even though nothing is wrong in the design: two variables share a value, our map cannot say which one is meant, and guessing is exactly what this contract forbids.
|
|
79
|
+
|
|
80
|
+
Full contract (axis vocabulary, the five conditions in detail, config schema, comment grammar, escalation routing): [reference/design-value-binding.md](../reference/design-value-binding.md).
|