@codyswann/lisa 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/dist/cli/doctor-lisa-owned-artifacts.d.ts.map +1 -1
  2. package/dist/cli/doctor-lisa-owned-artifacts.js +41 -2
  3. package/dist/cli/doctor-lisa-owned-artifacts.js.map +1 -1
  4. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  5. package/dist/core/upstream-evidence-manifest.js +90 -16
  6. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  7. package/expo/create-only/.github/workflows/nightly-e2e-health.yml +21 -2
  8. package/package.json +1 -1
  9. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  10. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  11. package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +3 -1
  12. package/plugins/lisa/.codex-plugin/skills/lisa-quality-review/SKILL.md +7 -0
  13. package/plugins/lisa/.codex-plugin/skills/lisa-review-local/SKILL.md +27 -0
  14. package/plugins/lisa/.codex-plugin/skills/lisa-tdd-implementation/SKILL.md +1 -0
  15. package/plugins/lisa/.codex-plugin/skills/lisa-tracker-source-artifacts/SKILL.md +2 -0
  16. package/plugins/lisa/rules/eager/design-source-of-truth.md +38 -0
  17. package/plugins/lisa/rules/eager/do-it-now.md +11 -0
  18. package/plugins/lisa/rules/eager/empirical-inquiry.md +1 -0
  19. package/plugins/lisa/rules/eager/falsifiable-checks.md +2 -0
  20. package/plugins/lisa/rules/eager/integration-access-layer.md +5 -0
  21. package/plugins/lisa/rules/eager/learnings-ladder.md +18 -0
  22. package/plugins/lisa/rules/eager/local-ci-first.md +15 -0
  23. package/plugins/lisa/rules/eager/not-blocked-just-waiting.md +13 -0
  24. package/plugins/lisa/rules/eager/session-status-updates.md +19 -0
  25. package/plugins/lisa/rules/eager/wiki-knowledge-source.md +6 -11
  26. package/plugins/lisa/rules/reference/design-source-of-truth.md +195 -0
  27. package/plugins/lisa/rules/reference/do-it-now.md +37 -0
  28. package/plugins/lisa/rules/reference/empirical-inquiry.md +1 -0
  29. package/plugins/lisa/rules/reference/falsifiable-checks.md +24 -2
  30. package/plugins/lisa/rules/reference/integration-access-layer.md +8 -0
  31. package/plugins/lisa/rules/reference/learnings-ladder.md +41 -0
  32. package/plugins/lisa/rules/reference/local-ci-first.md +34 -0
  33. package/plugins/lisa/rules/reference/not-blocked-just-waiting.md +41 -0
  34. package/plugins/lisa/rules/reference/session-status-updates.md +60 -0
  35. package/plugins/lisa/rules/reference/wiki-knowledge-source.md +21 -5
  36. package/plugins/lisa/scripts/design-source-gate.mjs +530 -0
  37. package/plugins/lisa/skills/lisa-implement/SKILL.md +3 -1
  38. package/plugins/lisa/skills/lisa-quality-review/SKILL.md +7 -0
  39. package/plugins/lisa/skills/lisa-review-local/SKILL.md +27 -0
  40. package/plugins/lisa/skills/lisa-tdd-implementation/SKILL.md +1 -0
  41. package/plugins/lisa/skills/lisa-tracker-source-artifacts/SKILL.md +2 -0
  42. package/plugins/lisa-agy/plugin.json +1 -1
  43. package/plugins/lisa-agy/scripts/design-source-gate.mjs +530 -0
  44. package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +3 -1
  45. package/plugins/lisa-agy/skills/lisa-quality-review/SKILL.md +7 -0
  46. package/plugins/lisa-agy/skills/lisa-review-local/SKILL.md +27 -0
  47. package/plugins/lisa-agy/skills/lisa-tdd-implementation/SKILL.md +1 -0
  48. package/plugins/lisa-agy/skills/lisa-tracker-source-artifacts/SKILL.md +2 -0
  49. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  52. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-copilot/rules/eager/design-source-of-truth.md +38 -0
  56. package/plugins/lisa-copilot/rules/eager/do-it-now.md +11 -0
  57. package/plugins/lisa-copilot/rules/eager/empirical-inquiry.md +1 -0
  58. package/plugins/lisa-copilot/rules/eager/falsifiable-checks.md +2 -0
  59. package/plugins/lisa-copilot/rules/eager/integration-access-layer.md +5 -0
  60. package/plugins/lisa-copilot/rules/eager/learnings-ladder.md +18 -0
  61. package/plugins/lisa-copilot/rules/eager/local-ci-first.md +15 -0
  62. package/plugins/lisa-copilot/rules/eager/not-blocked-just-waiting.md +13 -0
  63. package/plugins/lisa-copilot/rules/eager/session-status-updates.md +19 -0
  64. package/plugins/lisa-copilot/rules/eager/wiki-knowledge-source.md +6 -11
  65. package/plugins/lisa-copilot/rules/reference/design-source-of-truth.md +195 -0
  66. package/plugins/lisa-copilot/rules/reference/do-it-now.md +37 -0
  67. package/plugins/lisa-copilot/rules/reference/empirical-inquiry.md +1 -0
  68. package/plugins/lisa-copilot/rules/reference/falsifiable-checks.md +24 -2
  69. package/plugins/lisa-copilot/rules/reference/integration-access-layer.md +8 -0
  70. package/plugins/lisa-copilot/rules/reference/learnings-ladder.md +41 -0
  71. package/plugins/lisa-copilot/rules/reference/local-ci-first.md +34 -0
  72. package/plugins/lisa-copilot/rules/reference/not-blocked-just-waiting.md +41 -0
  73. package/plugins/lisa-copilot/rules/reference/session-status-updates.md +60 -0
  74. package/plugins/lisa-copilot/rules/reference/wiki-knowledge-source.md +21 -5
  75. package/plugins/lisa-copilot/scripts/design-source-gate.mjs +530 -0
  76. package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +3 -1
  77. package/plugins/lisa-copilot/skills/lisa-quality-review/SKILL.md +7 -0
  78. package/plugins/lisa-copilot/skills/lisa-review-local/SKILL.md +27 -0
  79. package/plugins/lisa-copilot/skills/lisa-tdd-implementation/SKILL.md +1 -0
  80. package/plugins/lisa-copilot/skills/lisa-tracker-source-artifacts/SKILL.md +2 -0
  81. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-cursor/rules/design-source-of-truth-reference.mdc +200 -0
  83. package/plugins/lisa-cursor/rules/design-source-of-truth.mdc +43 -0
  84. package/plugins/lisa-cursor/rules/do-it-now-reference.mdc +42 -0
  85. package/plugins/lisa-cursor/rules/do-it-now.mdc +16 -0
  86. package/plugins/lisa-cursor/rules/empirical-inquiry-reference.mdc +1 -0
  87. package/plugins/lisa-cursor/rules/empirical-inquiry.mdc +1 -0
  88. package/plugins/lisa-cursor/rules/falsifiable-checks-reference.mdc +24 -2
  89. package/plugins/lisa-cursor/rules/falsifiable-checks.mdc +2 -0
  90. package/plugins/lisa-cursor/rules/integration-access-layer-reference.mdc +8 -0
  91. package/plugins/lisa-cursor/rules/integration-access-layer.mdc +5 -0
  92. package/plugins/lisa-cursor/rules/learnings-ladder-reference.mdc +46 -0
  93. package/plugins/lisa-cursor/rules/learnings-ladder.mdc +23 -0
  94. package/plugins/lisa-cursor/rules/local-ci-first-reference.mdc +39 -0
  95. package/plugins/lisa-cursor/rules/local-ci-first.mdc +20 -0
  96. package/plugins/lisa-cursor/rules/not-blocked-just-waiting-reference.mdc +46 -0
  97. package/plugins/lisa-cursor/rules/not-blocked-just-waiting.mdc +18 -0
  98. package/plugins/lisa-cursor/rules/session-status-updates-reference.mdc +65 -0
  99. package/plugins/lisa-cursor/rules/session-status-updates.mdc +24 -0
  100. package/plugins/lisa-cursor/rules/wiki-knowledge-source-reference.mdc +21 -5
  101. package/plugins/lisa-cursor/rules/wiki-knowledge-source.mdc +7 -12
  102. package/plugins/lisa-cursor/scripts/design-source-gate.mjs +530 -0
  103. package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +3 -1
  104. package/plugins/lisa-cursor/skills/lisa-quality-review/SKILL.md +7 -0
  105. package/plugins/lisa-cursor/skills/lisa-review-local/SKILL.md +27 -0
  106. package/plugins/lisa-cursor/skills/lisa-tdd-implementation/SKILL.md +1 -0
  107. package/plugins/lisa-cursor/skills/lisa-tracker-source-artifacts/SKILL.md +2 -0
  108. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  110. package/plugins/lisa-expo-agy/plugin.json +1 -1
  111. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  113. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  114. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  115. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  116. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  118. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  119. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  120. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  121. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  122. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  123. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  124. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  125. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  126. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  127. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  128. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  129. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  130. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  131. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  132. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  133. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  134. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  135. package/plugins/lisa-rails-agy/plugin.json +1 -1
  136. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  137. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  138. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  139. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  140. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  141. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  142. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  143. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  144. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  145. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  146. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  147. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  148. package/plugins/src/base/rules/eager/design-source-of-truth.md +38 -0
  149. package/plugins/src/base/rules/eager/do-it-now.md +11 -0
  150. package/plugins/src/base/rules/eager/empirical-inquiry.md +1 -0
  151. package/plugins/src/base/rules/eager/falsifiable-checks.md +2 -0
  152. package/plugins/src/base/rules/eager/integration-access-layer.md +5 -0
  153. package/plugins/src/base/rules/eager/learnings-ladder.md +18 -0
  154. package/plugins/src/base/rules/eager/local-ci-first.md +15 -0
  155. package/plugins/src/base/rules/eager/not-blocked-just-waiting.md +13 -0
  156. package/plugins/src/base/rules/eager/session-status-updates.md +19 -0
  157. package/plugins/src/base/rules/eager/wiki-knowledge-source.md +6 -11
  158. package/plugins/src/base/rules/reference/design-source-of-truth.md +195 -0
  159. package/plugins/src/base/rules/reference/do-it-now.md +37 -0
  160. package/plugins/src/base/rules/reference/empirical-inquiry.md +1 -0
  161. package/plugins/src/base/rules/reference/falsifiable-checks.md +24 -2
  162. package/plugins/src/base/rules/reference/integration-access-layer.md +8 -0
  163. package/plugins/src/base/rules/reference/learnings-ladder.md +41 -0
  164. package/plugins/src/base/rules/reference/local-ci-first.md +34 -0
  165. package/plugins/src/base/rules/reference/not-blocked-just-waiting.md +41 -0
  166. package/plugins/src/base/rules/reference/session-status-updates.md +60 -0
  167. package/plugins/src/base/rules/reference/wiki-knowledge-source.md +21 -5
  168. package/plugins/src/base/scripts/design-source-gate.mjs +530 -0
  169. package/plugins/src/base/skills/lisa-implement/SKILL.md +3 -1
  170. package/plugins/src/base/skills/lisa-quality-review/SKILL.md +7 -0
  171. package/plugins/src/base/skills/lisa-review-local/SKILL.md +27 -0
  172. package/plugins/src/base/skills/lisa-tdd-implementation/SKILL.md +1 -0
  173. package/plugins/src/base/skills/lisa-tracker-source-artifacts/SKILL.md +2 -0
  174. package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +173 -8
  175. package/typescript/copy-overwrite/scripts/nightly-e2e-suites.schema.json +12 -0
@@ -0,0 +1,60 @@
1
+ # Session Status Updates — Plain Language, and Always Say If It's Safe to Close
2
+
3
+ ## Why this is a rule and not a style preference
4
+
5
+ Lisa's premise is that a non-technical person can direct software work by describing outcomes. That premise already binds everything crossing a gate outward — intake rejections, clarifying questions, ticket descriptions, verification reports — because a non-technical operator is standing at the gate.
6
+
7
+ In-session conversation was never held to the same bar, and the result was a split personality: careful, plain-language ticket prose and simultaneously a stream of chat messages full of file paths, rule slugs, and reasoning narrative. The human reading both is the same human. This rule closes the split.
8
+
9
+ ## The three-part shape
10
+
11
+ Every update answers, in this order:
12
+
13
+ 1. **What changed** — what is different in the world now that was not before. Shipped, fixed, filed, deployed.
14
+ 2. **What's blocked** — using the strict meaning from `not-blocked-just-waiting`. If nothing is blocked, say so; "waiting on the CI run" belongs here labelled as waiting.
15
+ 3. **What needs a decision** — see below.
16
+
17
+ What is deliberately excluded: how you found it. The files you read, the searches you ran, the hypotheses you discarded, the dead ends. That is your working memory, not their update. If they want it they will ask, and the offer to elaborate costs one clause.
18
+
19
+ ## Voice
20
+
21
+ Write the way you would speak to a competent colleague who does not work on this system.
22
+
23
+ - Prefer the user-visible name of a thing over its implementation name. "The login page broke" beats "the auth guard regressed at the controller boundary."
24
+ - Do not use Lisa vocabulary (rung, gate, leaf, intake, ratchet) with someone who has not asked for it.
25
+ - Do not use a file path where a description works. Paths are for when the human will open the file.
26
+ - Short sentences. No preamble about what you are about to say.
27
+
28
+ Two lines the user themselves offered as the target: *"just tell me what's going on and what my options are"* and *"give me the summary, I'll ask for detail if I want it."*
29
+
30
+ ## Decisions
31
+
32
+ A decision presented as a paragraph of context is a decision the human has to excavate. State it as a decision:
33
+
34
+ - **The decision** — one sentence naming the choice to be made.
35
+ - **Your recommendation** — you have more context than they do; do not withhold it. Recommending is not deciding.
36
+ - **The ramifications of each option** — one line apiece, in terms of consequences they care about (time, cost, risk, what breaks, what it forecloses), not in terms of implementation.
37
+
38
+ An option you consider unacceptable is still listed, with the reason it is unacceptable. Presenting one option as if it were the only one is a decision you made on their behalf without saying so.
39
+
40
+ ## The close line
41
+
42
+ Every update ends with exactly:
43
+
44
+ ```
45
+ Safe to close: yes/no — <reason>
46
+ ```
47
+
48
+ `yes` means killing the session right now loses nothing: work is committed and pushed, tickets are in the state they should be in, nothing is running that will not finish on its own.
49
+
50
+ `no` names the specific in-flight thing that would be lost. Real examples: a local dev server or test run still going; a ticket filed but not yet flipped to ready, so nothing will pick it up; a commit that exists only in the working tree; a PR whose checks nobody is watching; a deploy mid-flight.
51
+
52
+ The line exists because the human's most common question — asked or unasked — is "can I close this?" Making them ask it costs a round trip; making them guess costs them work.
53
+
54
+ ## Relationship to the other communication rules
55
+
56
+ - `report-actionability` governs **completeness**: the denominator, every item accounted for, who acts on each. It is about what a report must contain.
57
+ - `automation-runbook-contract` governs the **outcome line** a terminating flow opens with.
58
+ - This rule governs the **voice** of a session update, its three-part shape, and the close line.
59
+
60
+ None of the three is relaxed by the others. An update can satisfy all three at once and usually should: outcome line first, denominator stated, plain language throughout, close line last.
@@ -9,13 +9,29 @@ A project has a wiki in one of two shapes:
9
9
 
10
10
  Either way, freshness is not your concern. The query and ingest skills run `scripts/ensure-wiki.mjs` as their own first step, which resolves the wiki root and — for a remote wiki — clones the mirror if missing and fast-forwards it when stale (subject to a short TTL, and tolerant of being offline: it proceeds with the existing mirror and warns rather than blocking). The freshness guarantee lives in the tool, not in the caller's discipline. Do **not** add a separate "make sure the wiki is current" step to your own workflow — calling the skill already does it.
11
11
 
12
- Before researching project background, conventions, ownership, architecture, glossary terms, or "how/why does X work here":
12
+ ## Load posture: on demand, not at session start
13
13
 
14
- 1. Consult the wiki first via the wiki query skill (`/lisa-wiki-query`, or the runtime's wiki query skill), which resolves the wiki root for you. For a local wiki you may also start from `wiki/index.md` and follow links.
15
- 2. Use what the wiki says as the authoritative answer when it covers the question. Do not re-derive it from raw sources when the wiki already documents it.
16
- 3. Fall back to primary sources (code, tickets, commit history, external docs) only when the wiki is silent, ambiguous, or contradicted by what you observe in the code.
14
+ The wiki is **not** read during session bootstrap, and no flow may add a "load the wiki" step to its startup. This is a deliberate reversal of the rule's earlier "consult the wiki first" posture, recorded in `wiki/decisions/2026-08-12-agent-neutral-host-rules-path.md` section 6 (decision D3). The reasoning is context budget: the wiki is deep declarative knowledge, which is the WIKI rung of the learnings ladder precisely *because* it should be paid for only when routed to. Loading it unconditionally converts an on-demand rung into an eager one and charges every session — including the large majority that never ask a knowledge question — for knowledge they do not use.
15
+
16
+ What survives the softening is discoverability. An agent must **know the wiki exists and know how to query it**, so that a question needing depth goes to the curated answer instead of a fresh scrape of the codebase. What is dropped is the obligation to consult it before every piece of research, and the framing of the wiki as the authority that outranks primary sources.
17
+
18
+ When you do need project depth:
19
+
20
+ 1. Call the wiki query skill (`/lisa-wiki-query`, or the runtime's equivalent), which resolves the wiki root for you. For a local wiki you may also start from `wiki/index.md` and follow links.
21
+ 2. Prefer what the wiki says over re-deriving the same answer from raw sources — that is what it is curated for.
22
+ 3. Fall back to primary sources (code, tickets, commit history, external docs) when the wiki is silent, ambiguous, or contradicted by what you observe in the code.
17
23
  4. If you find the wiki is wrong, stale, or missing knowledge that belongs there, surface the gap — and where the project's workflow supports it, capture the correction back into the wiki via its ingestion path (`/lisa-wiki-ingest` or equivalent) rather than leaving the knowledge only in this session.
18
24
 
19
25
  The wiki documents knowledge; it does not override executable behavior. When the wiki and the running code disagree about what the system actually does, trust the code and treat the wiki as out of date. See the `documentation-source-paths` rule for how source-material directories relate to the wiki.
20
26
 
21
- If the project has neither a local `wiki/` nor a `wiki.source` pointer in `.lisa.config.json`, this rule does not apply.
27
+ ## Applicability is the plugin gate, not just the wiki content
28
+
29
+ This rule ships in the **base** plugin, but the skill it directs you to (`lisa-wiki-query`) ships in the **wiki** plugin. Those are gated differently, and for a period a base-only project received this rule pointing at a skill it did not have — a rule with a dangling instruction, which teaches an agent to distrust its rules.
30
+
31
+ The applicability condition is therefore stated as the *installation* gate rather than as "does a `wiki/` folder exist". The `lisa-wiki` plugin is selected when `wiki/lisa-wiki.config.json` exists **or** `.lisa.config.json` declares a top-level `wiki` key (`src/core/lisa-plugin-selection.ts`); on Claude Code it is additionally gated by `enabledPlugins["lisa-wiki@lisa"]` in `.claude/settings.json`. When that gate is not satisfied:
32
+
33
+ - This rule does not apply. There is no wiki to query and no query skill to call.
34
+ - The recovery path is `/lisa-wiki-install`, the bootstrap skill that **base** ships precisely so a project that never enabled the wiki plugin can still discover it. Naming a base-shipped skill is what keeps the instruction reachable in every install.
35
+ - **Never block on the absent skill.** Degrade to primary sources and continue, per the same degrade-never-block posture `readiness-rubric` and `rejection-detection` use for surfaces that may not be installed.
36
+
37
+ A remote-only wiki declared through `wiki.source` is covered by the `wiki` config key, so the pointer form satisfies the same gate.
@@ -0,0 +1,530 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Deterministic, fail-closed gate for the `design-source-of-truth` contract.
4
+ *
5
+ * Figma is the design source of truth. Every other Lisa design obligation is
6
+ * conditioned on a design artifact already existing — ticket gate S12 fires only
7
+ * `when artifacts_attached = true` — so UI invented straight into code was
8
+ * governed by nothing at all. This gate governs that case, and only that case:
9
+ * every UI surface a change touches must declare where its design came from,
10
+ * in exactly one of two forms.
11
+ *
12
+ * ```
13
+ * // DESIGN-SOURCE: https://www.figma.com/design/<file>?node-id=<node>
14
+ * // DESIGN-SOURCE: none — not in Figma
15
+ * ```
16
+ *
17
+ * The first says the surface is backed by a design node (it already existed, or
18
+ * it was synced back). The second says the surface is deliberately not captured
19
+ * at the source; it is the exception, not the default, and the gate reports
20
+ * every one of them so review can challenge it.
21
+ *
22
+ * **The gate fails closed.** Anything it cannot resolve into one of those two
23
+ * declarations is a violation: no annotation, a malformed one, a file that both
24
+ * cites Figma and denies having a source, a changed file it could not read, or
25
+ * a diff it could not compute. A gate that returns PASS when it could not look
26
+ * proves nothing, so it never does.
27
+ *
28
+ * What this module deliberately does NOT decide is *what to build*. Host
29
+ * design-system rules (`figma-design-system`, `design-system`,
30
+ * `use-the-design-library`) own component hierarchy, token vocabulary, and
31
+ * reuse; several are generated from a ratified RFC. This gate stays orthogonal
32
+ * to all of it — it asks only whether the design source is declared.
33
+ *
34
+ * Run it: `node design-source-gate.mjs --base=origin/main [--head=HEAD] [--json]`
35
+ * Exit 0 = PASS, 1 = FAIL (any violation, or anything unresolvable), 2 = usage.
36
+ */
37
+ import { execFileSync } from "node:child_process";
38
+ import { readFileSync } from "node:fs";
39
+
40
+ /** The one designated marker for UI deliberately not captured in Figma. */
41
+ export const DESIGN_SOURCE_NONE_MARKER = "DESIGN-SOURCE: none — not in Figma";
42
+
43
+ /** The annotation keyword, in whatever comment syntax the file uses. */
44
+ const ANNOTATION = /DESIGN-SOURCE:[ \t]*(?<value>[^\n]*)/gu;
45
+
46
+ /**
47
+ * The none-form, plus an optional trailing reason. The reason is optional so
48
+ * the marker's documented spelling stays exactly what the rule prints, but a
49
+ * reasonless marker is reported for sync-back when Figma access is proven.
50
+ */
51
+ const NONE_FORM =
52
+ /^none[ \t]*(?:—|--|–)[ \t]*not in Figma(?:[ \t]*(?:—|--|–)[ \t]*(?<reason>.+?))?[ \t]*(?:-->|\*\/|\*|#)?[ \t]*$/u;
53
+
54
+ /** Only a Figma URL seals a surface. Any other link is malformed, not proof. */
55
+ const FIGMA_URL = /^https:\/\/(?:[\w-]+\.)*figma\.com\/[^\s)]+$/u;
56
+
57
+ /** Extensions that are a UI surface wherever they live. */
58
+ const UI_EXTENSIONS = [".tsx", ".jsx", ".vue", ".svelte"];
59
+
60
+ /**
61
+ * Markup and style extensions that are a UI surface only inside a directory
62
+ * that renders. A `.ts` barrel or a pure-logic module under `components/`
63
+ * renders nothing, so extension-plus-directory keeps the gate off code that has
64
+ * no design to declare.
65
+ */
66
+ const UI_MARKUP_EXTENSIONS = [
67
+ ".css",
68
+ ".scss",
69
+ ".sass",
70
+ ".less",
71
+ ".styl",
72
+ ".html",
73
+ ".erb",
74
+ ".haml",
75
+ ".slim",
76
+ ".swift",
77
+ ".kt",
78
+ ".dart",
79
+ ".xml",
80
+ ];
81
+
82
+ /** Directory names that mark a rendering surface. */
83
+ const UI_DIRECTORIES = [
84
+ "components",
85
+ "screens",
86
+ "views",
87
+ "pages",
88
+ "ui",
89
+ "widgets",
90
+ "layouts",
91
+ "templates",
92
+ "atoms",
93
+ "molecules",
94
+ "organisms",
95
+ ];
96
+
97
+ /** Never a UI surface: generated output, vendored code, and test scaffolding. */
98
+ const DEFAULT_EXCLUDES = [
99
+ "**/node_modules/**",
100
+ "dist/**",
101
+ "build/**",
102
+ "coverage/**",
103
+ ".next/**",
104
+ "**/*.d.ts",
105
+ "**/*.test.*",
106
+ "**/*.spec.*",
107
+ "**/*.stories.*",
108
+ "**/__tests__/**",
109
+ "**/__snapshots__/**",
110
+ ];
111
+
112
+ /** Change types that carry no head content to annotate. */
113
+ const REMOVED_CHANGE_TYPES = new Set(["deleted", "D", "removed"]);
114
+
115
+ /**
116
+ * Translate a `**`/`*` glob into an anchored regular expression. Deliberately
117
+ * tiny: the gate must run from a plugin directory with no dependency install,
118
+ * so pulling in a glob library is not an option.
119
+ *
120
+ * @param {string} pattern Glob pattern, `/`-separated.
121
+ * @returns {RegExp} Anchored matcher for a repo-relative path.
122
+ */
123
+ function globToRegExp(pattern) {
124
+ // Scanned in one pass rather than chained replacements: a second pass would
125
+ // re-expand the `*` inside a `.*` it had just emitted, which silently turns
126
+ // `**/node_modules/**` into a pattern that matches nothing.
127
+ let body = "";
128
+ let index = 0;
129
+
130
+ while (index < pattern.length) {
131
+ const char = pattern[index];
132
+ if (char === "*" && pattern[index + 1] === "*") {
133
+ const slashed = pattern[index + 2] === "/";
134
+ body += slashed ? "(?:[^/]*/)*" : ".*";
135
+ index += slashed ? 3 : 2;
136
+ continue;
137
+ }
138
+ if (char === "*") {
139
+ body += "[^/]*";
140
+ index += 1;
141
+ continue;
142
+ }
143
+ if (char === "?") {
144
+ body += "[^/]";
145
+ index += 1;
146
+ continue;
147
+ }
148
+ body += ".+^${}()|[]\\".includes(char) ? `\\${char}` : char;
149
+ index += 1;
150
+ }
151
+
152
+ return new RegExp(`^${body}$`, "u");
153
+ }
154
+
155
+ /**
156
+ * @param {string} relPath Repo-relative path.
157
+ * @param {readonly string[]} patterns Glob patterns to test.
158
+ * @returns {boolean} True when any pattern matches.
159
+ */
160
+ function matchesAny(relPath, patterns) {
161
+ return patterns.some(pattern => globToRegExp(pattern).test(relPath));
162
+ }
163
+
164
+ /**
165
+ * @param {string} relPath Repo-relative path.
166
+ * @returns {string} Lowercased extension including the dot, or "".
167
+ */
168
+ function extensionOf(relPath) {
169
+ const base = relPath.slice(relPath.lastIndexOf("/") + 1);
170
+ const dot = base.lastIndexOf(".");
171
+ return dot <= 0 ? "" : base.slice(dot).toLowerCase();
172
+ }
173
+
174
+ /**
175
+ * Decide whether a path is a UI surface — something a user can observe.
176
+ *
177
+ * Membership is surface, not repo name or file extension alone: an extension
178
+ * that always renders counts anywhere, while markup and styles count inside a
179
+ * rendering directory. Projects narrow or widen this through
180
+ * `designSource.include` / `designSource.exclude` in `.lisa.config.json`.
181
+ *
182
+ * @param {string} relPath Repo-relative path of the changed file.
183
+ * @param {{ include?: readonly string[], exclude?: readonly string[] } | undefined} [config] Project overrides.
184
+ * @returns {boolean} True when the path is a UI surface.
185
+ */
186
+ export function isUiSurface(relPath, config) {
187
+ if (typeof relPath !== "string" || relPath.length === 0) return false;
188
+
189
+ const normalized = relPath.replaceAll("\\", "/");
190
+ const exclude = [...DEFAULT_EXCLUDES, ...(config?.exclude ?? [])];
191
+ if (matchesAny(normalized, exclude)) return false;
192
+
193
+ const include = config?.include ?? [];
194
+ if (include.length > 0 && matchesAny(normalized, include)) return true;
195
+
196
+ const extension = extensionOf(normalized);
197
+ if (UI_EXTENSIONS.includes(extension)) return true;
198
+
199
+ if (!UI_MARKUP_EXTENSIONS.includes(extension)) return false;
200
+ const segments = normalized.split("/").slice(0, -1);
201
+ return segments.some(segment =>
202
+ UI_DIRECTORIES.includes(segment.toLowerCase())
203
+ );
204
+ }
205
+
206
+ /**
207
+ * Strip a trailing comment terminator so a marker written in block or markup
208
+ * comment syntax yields the same reason as a line comment would.
209
+ *
210
+ * @param {string} value Raw annotation value.
211
+ * @returns {string} Value with any trailing comment close removed.
212
+ */
213
+ function stripCommentClose(value) {
214
+ return value.replace(/[ \t]*(?:-->|\*\/|\*\}|\}\)?|#>)[ \t]*$/u, "").trim();
215
+ }
216
+
217
+ /**
218
+ * Read every design-source annotation out of a file body.
219
+ *
220
+ * @param {string} content File body at the head of the change.
221
+ * @returns {{ figma: string[], none: { reason: string | null }[], malformed: string[] }} Parsed annotations.
222
+ */
223
+ function parseAnnotations(content) {
224
+ const figma = [];
225
+ const none = [];
226
+ const malformed = [];
227
+
228
+ for (const match of content.matchAll(ANNOTATION)) {
229
+ const value = stripCommentClose(match.groups?.value ?? "");
230
+ const noneMatch = NONE_FORM.exec(value);
231
+ if (noneMatch) {
232
+ none.push({ reason: noneMatch.groups?.reason?.trim() || null });
233
+ continue;
234
+ }
235
+ if (FIGMA_URL.test(value)) {
236
+ figma.push(value);
237
+ continue;
238
+ }
239
+ malformed.push(value);
240
+ }
241
+
242
+ return { figma, none, malformed };
243
+ }
244
+
245
+ /**
246
+ * Classify one changed file against the contract.
247
+ *
248
+ * Statuses: `figma-source` (sealed), `marked-exception` (declared exception),
249
+ * `not-applicable` (not a UI surface, or a deletion), and the four failing
250
+ * states `undeclared`, `malformed`, `conflicting`, and `unreadable`.
251
+ *
252
+ * @param {{ path: string, changeType?: string, content?: string | null }} file Changed-file record.
253
+ * @param {{ include?: readonly string[], exclude?: readonly string[] } | undefined} [config] Project overrides.
254
+ * @returns {{ path: string, uiSurface: boolean, status: string, evidence: string | null, reason: string | null }} Classification.
255
+ */
256
+ export function classifyDesignSource(file, config) {
257
+ const relPath = file?.path ?? "";
258
+ const notApplicable = {
259
+ path: relPath,
260
+ uiSurface: false,
261
+ status: "not-applicable",
262
+ evidence: null,
263
+ reason: null,
264
+ };
265
+
266
+ if (REMOVED_CHANGE_TYPES.has(file?.changeType ?? "")) return notApplicable;
267
+ if (!isUiSurface(relPath, config)) return notApplicable;
268
+
269
+ const base = { path: relPath, uiSurface: true, evidence: null, reason: null };
270
+
271
+ if (typeof file?.content !== "string") {
272
+ return { ...base, status: "unreadable" };
273
+ }
274
+
275
+ const { figma, none, malformed } = parseAnnotations(file.content);
276
+
277
+ if (figma.length > 0 && none.length > 0) {
278
+ return { ...base, status: "conflicting", evidence: figma[0] };
279
+ }
280
+ if (malformed.length > 0) {
281
+ return { ...base, status: "malformed", evidence: malformed[0] };
282
+ }
283
+ if (figma.length > 0) {
284
+ return { ...base, status: "figma-source", evidence: figma[0] };
285
+ }
286
+ if (none.length > 0) {
287
+ return {
288
+ ...base,
289
+ status: "marked-exception",
290
+ evidence: DESIGN_SOURCE_NONE_MARKER,
291
+ reason: none[0]?.reason ?? null,
292
+ };
293
+ }
294
+ return { ...base, status: "undeclared" };
295
+ }
296
+
297
+ /** Per-file statuses that fail the gate. */
298
+ const VIOLATION_STATUSES = new Set([
299
+ "undeclared",
300
+ "malformed",
301
+ "conflicting",
302
+ "unreadable",
303
+ ]);
304
+
305
+ /**
306
+ * Evaluate a whole change.
307
+ *
308
+ * Only the files this change touched are judged — a repo full of unannotated
309
+ * legacy UI is burndown, not this work item's blocker. But the gate refuses to
310
+ * pass on ignorance: an unresolved file list or a reported diff error is a FAIL
311
+ * with a named reason, never a quiet PASS.
312
+ *
313
+ * @param {{
314
+ * files?: readonly { path: string, changeType?: string, content?: string | null }[] | null,
315
+ * diffError?: string | null,
316
+ * figmaAccess?: boolean,
317
+ * config?: { include?: readonly string[], exclude?: readonly string[] }
318
+ * }} input Change under evaluation.
319
+ * @returns {{
320
+ * verdict: "PASS" | "FAIL",
321
+ * reasons: string[],
322
+ * violations: object[],
323
+ * exceptions: object[],
324
+ * syncBackPreferred: object[],
325
+ * sealed: object[],
326
+ * notApplicable: object[],
327
+ * summary: { judged: number, sealed: number, exceptions: number, violations: number }
328
+ * }} The gate result.
329
+ */
330
+ export function evaluateDesignSource(input = {}) {
331
+ const reasons = [];
332
+ const files = Array.isArray(input.files) ? input.files : null;
333
+
334
+ if (files === null) reasons.push("changed-files-unresolved");
335
+ if (
336
+ typeof input.diffError === "string" &&
337
+ input.diffError.trim().length > 0
338
+ ) {
339
+ reasons.push("diff-unresolved");
340
+ }
341
+
342
+ const classified = (files ?? []).map(file =>
343
+ classifyDesignSource(file, input.config)
344
+ );
345
+
346
+ const sealed = classified.filter(entry => entry.status === "figma-source");
347
+ const exceptions = classified.filter(
348
+ entry => entry.status === "marked-exception"
349
+ );
350
+ const violations = classified.filter(entry =>
351
+ VIOLATION_STATUSES.has(entry.status)
352
+ );
353
+ const notApplicable = classified.filter(
354
+ entry => entry.status === "not-applicable"
355
+ );
356
+
357
+ // Sync-back is the preference, not a second blocking gate: when Figma access
358
+ // is proven, an exception that records no reason is surfaced so review can
359
+ // ask why it was not captured at the source instead.
360
+ const syncBackPreferred =
361
+ input.figmaAccess === true
362
+ ? exceptions.filter(entry => entry.reason === null)
363
+ : [];
364
+
365
+ for (const entry of violations) reasons.push(`${entry.status}:${entry.path}`);
366
+
367
+ return {
368
+ verdict: reasons.length === 0 ? "PASS" : "FAIL",
369
+ reasons,
370
+ violations,
371
+ exceptions,
372
+ syncBackPreferred,
373
+ sealed,
374
+ notApplicable,
375
+ summary: {
376
+ judged: sealed.length + exceptions.length + violations.length,
377
+ sealed: sealed.length,
378
+ exceptions: exceptions.length,
379
+ violations: violations.length,
380
+ },
381
+ };
382
+ }
383
+
384
+ /**
385
+ * Collect the changed files of a git range, reading each one at the head.
386
+ *
387
+ * @param {string} base Base revision.
388
+ * @param {string} head Head revision.
389
+ * @returns {{ files: object[] | null, diffError: string | null }} Changed files, or the failure.
390
+ */
391
+ export function collectChangedFiles(base, head) {
392
+ let output = "";
393
+ try {
394
+ output = execFileSync(
395
+ "git",
396
+ ["diff", "--name-status", "--no-renames", `${base}...${head}`],
397
+ { encoding: "utf8" }
398
+ );
399
+ } catch (error) {
400
+ return { files: null, diffError: String(error?.message ?? error) };
401
+ }
402
+
403
+ const files = output
404
+ .split("\n")
405
+ .filter(line => line.trim().length > 0)
406
+ .map(line => {
407
+ const [rawStatus, relPath] = line.split("\t");
408
+ const changeType = rawStatus?.startsWith("D") ? "deleted" : "modified";
409
+ if (changeType === "deleted") {
410
+ return { path: relPath ?? "", changeType, content: null };
411
+ }
412
+ let content = null;
413
+ try {
414
+ content = readFileSync(relPath ?? "", "utf8");
415
+ } catch {
416
+ content = null;
417
+ }
418
+ return { path: relPath ?? "", changeType, content };
419
+ });
420
+
421
+ return { files, diffError: null };
422
+ }
423
+
424
+ /**
425
+ * Read the optional `designSource` block from `.lisa.config.json`.
426
+ *
427
+ * @returns {{ include?: string[], exclude?: string[] }} Project overrides, or {}.
428
+ */
429
+ function readProjectConfig() {
430
+ try {
431
+ const raw = JSON.parse(readFileSync(".lisa.config.json", "utf8"));
432
+ return raw?.designSource ?? {};
433
+ } catch {
434
+ return {};
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Render the operator-readable report. Written for the non-technical operator
440
+ * standing at the gate, so it names the file and the exact next action.
441
+ *
442
+ * @param {ReturnType<typeof evaluateDesignSource>} result Gate result.
443
+ * @returns {string} Report text.
444
+ */
445
+ export function renderReport(result) {
446
+ const lines = [`design-source gate: ${result.verdict}`];
447
+
448
+ if (result.verdict === "PASS") {
449
+ lines.push(
450
+ ` ${result.summary.sealed} surface(s) backed by Figma, ${result.summary.exceptions} explicitly marked as not in Figma.`
451
+ );
452
+ }
453
+
454
+ for (const entry of result.violations) {
455
+ lines.push(` ✗ ${entry.path} — ${entry.status}`);
456
+ }
457
+ if (result.violations.length > 0) {
458
+ lines.push(
459
+ "",
460
+ " Each file above changes something a user can see but does not say where its design came from.",
461
+ " Fix it one of two ways, sync-back first:",
462
+ " 1. Reflect the surface in Figma, then annotate it: `DESIGN-SOURCE: <figma-url>`",
463
+ ` 2. If it genuinely is not captured at the source, annotate: \`${DESIGN_SOURCE_NONE_MARKER}\``
464
+ );
465
+ }
466
+ if (result.reasons.includes("changed-files-unresolved")) {
467
+ lines.push(" ✗ the set of changed files could not be resolved");
468
+ }
469
+ if (result.reasons.includes("diff-unresolved")) {
470
+ lines.push(
471
+ " ✗ the diff could not be computed — the gate cannot pass on a change it could not read"
472
+ );
473
+ }
474
+ for (const entry of result.syncBackPreferred) {
475
+ lines.push(
476
+ ` ! ${entry.path} — marked as not in Figma with no reason recorded, and Figma access is available. Prefer syncing it back.`
477
+ );
478
+ }
479
+
480
+ return lines.join("\n");
481
+ }
482
+
483
+ /**
484
+ * CLI entrypoint.
485
+ *
486
+ * @param {readonly string[]} argv Arguments after the script name.
487
+ * @returns {number} Process exit code.
488
+ */
489
+ export function runCli(argv) {
490
+ const args = new Map(
491
+ argv
492
+ .filter(arg => arg.startsWith("--"))
493
+ .map(arg => {
494
+ const eq = arg.indexOf("=");
495
+ return eq === -1
496
+ ? [arg.slice(2), "true"]
497
+ : [arg.slice(2, eq), arg.slice(eq + 1)];
498
+ })
499
+ );
500
+
501
+ const base = args.get("base");
502
+ if (!base) {
503
+ process.stderr.write(
504
+ "usage: design-source-gate.mjs --base=<ref> [--head=<ref>] [--figma-access] [--json]\n"
505
+ );
506
+ return 2;
507
+ }
508
+
509
+ const { files, diffError } = collectChangedFiles(
510
+ base,
511
+ args.get("head") ?? "HEAD"
512
+ );
513
+ const result = evaluateDesignSource({
514
+ files,
515
+ diffError,
516
+ figmaAccess: args.get("figma-access") === "true",
517
+ config: readProjectConfig(),
518
+ });
519
+
520
+ process.stdout.write(
521
+ args.get("json") === "true"
522
+ ? `${JSON.stringify(result, null, 2)}\n`
523
+ : `${renderReport(result)}\n`
524
+ );
525
+ return result.verdict === "PASS" ? 0 : 1;
526
+ }
527
+
528
+ if (process.argv[1]?.endsWith("design-source-gate.mjs")) {
529
+ process.exit(runCli(process.argv.slice(2)));
530
+ }
@@ -155,7 +155,9 @@ IF it is a Fix (bug), execute the Reproduce sub-flow FIRST:
155
155
 
156
156
  For any Fix flow, and for any Build flow that changes user-visible behavior, regression coverage is a required deliverable at the highest practical observation level for the reported surface. If the project has a browser, device, or end-to-end harness for that platform (for example Playwright, Maestro, Detox, Cypress, or an equivalent runtime), the task plan and definition of done MUST include a deterministic regression spec against the reported surface, using mocked or seeded data where needed. This is alongside unit or integration coverage, not a substitute for it. For **frontend work** the deliverable is defined by the `bdd-e2e-coverage` rule and has two halves, both landing in this PR: the Gherkin scenario (stable ID, required platforms) added or updated in the project's behavior contract, and aligned automation in the project's configured runner for **every** platform that scenario requires — no runner substitutes for another, because they guard different platforms of the same behavior. The coverage gate must pass and the matrix and burndown be regenerated before the item is done. Cite the rule; do not restate its scenario, waiver, or bootstrap mechanics here.
157
157
 
158
- For work that adds or changes **persistent state**, the `reset-seed-coverage` rule adds a second 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 thisthat is the leak, not the coverage. Cite the rule; do not restate its policy, waiver, or bootstrap mechanics here.
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
+
160
+ 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.
159
161
 
160
162
  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:
161
163
 
@@ -16,6 +16,13 @@ For each changed file, evaluate:
16
16
  3. **Test coverage** -- Tests present? Testing behavior, not implementation details? Edge cases covered?
17
17
  4. **Documentation** -- JSDoc on new functions explaining "why"? Preambles on new files?
18
18
  5. **Code clarity** -- Readable variable names? Unnecessary complexity? Could a new team member understand this?
19
+ 6. **Design source** -- For UI surfaces, does each changed file say where its design came from? Run the deterministic gate rather than judging by eye:
20
+
21
+ ```bash
22
+ node "${CLAUDE_PLUGIN_ROOT:-.}/scripts/design-source-gate.mjs" --base=main --head=HEAD
23
+ ```
24
+
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.
19
26
 
20
27
  ## Output Format
21
28