@invariant-ui/cli 0.0.0-stage → 0.1.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 (196) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +248 -2
  3. package/dist/agent-kit/apply.js +232 -0
  4. package/dist/agent-kit/guidance.js +236 -0
  5. package/dist/agent-kit/index.js +5 -0
  6. package/dist/agent-kit/integrations.js +121 -0
  7. package/dist/agent-kit/plugin.js +115 -0
  8. package/dist/artifact-generator/index.js +75 -0
  9. package/dist/browser-verify/driver.js +2 -0
  10. package/dist/browser-verify/index.js +4 -0
  11. package/dist/browser-verify/playwright.js +82 -0
  12. package/dist/browser-verify/runner.js +175 -0
  13. package/dist/browser-verify/spec.js +103 -0
  14. package/dist/cli/args.js +77 -0
  15. package/dist/cli/baseline.js +64 -0
  16. package/dist/cli/cli.js +121 -0
  17. package/dist/cli/commands-browser.js +104 -0
  18. package/dist/cli/commands-discover.js +312 -0
  19. package/dist/cli/commands-enforcement.js +35 -0
  20. package/dist/cli/commands-governance.js +179 -0
  21. package/dist/cli/commands-hook.js +269 -0
  22. package/dist/cli/commands-patterns.js +324 -0
  23. package/dist/cli/commands-setup.js +485 -0
  24. package/dist/cli/commands-validate.js +157 -0
  25. package/dist/cli/commands.js +517 -0
  26. package/dist/cli/config.js +197 -0
  27. package/dist/cli/diff.js +66 -0
  28. package/dist/cli/enforcement-log.js +83 -0
  29. package/dist/cli/enforcement-report.js +96 -0
  30. package/dist/cli/errors.js +10 -0
  31. package/dist/cli/format.js +22 -0
  32. package/dist/cli/index.js +5 -0
  33. package/dist/cli/io.js +2 -0
  34. package/dist/cli/main.js +31 -0
  35. package/dist/cli/pipeline.js +262 -0
  36. package/dist/cli/publish.js +77 -0
  37. package/dist/cli/report.js +127 -0
  38. package/dist/cli/starter-rules.js +57 -0
  39. package/dist/cli/store.js +60 -0
  40. package/dist/cli/version.js +5 -0
  41. package/dist/cli/watch.js +68 -0
  42. package/dist/cli/workflow.js +52 -0
  43. package/dist/compiler-angular/adapter.js +115 -0
  44. package/dist/compiler-angular/analyze.js +222 -0
  45. package/dist/compiler-angular/classes.js +446 -0
  46. package/dist/compiler-angular/declared.js +285 -0
  47. package/dist/compiler-angular/emit.js +183 -0
  48. package/dist/compiler-angular/index.js +4 -0
  49. package/dist/compiler-angular/template.js +142 -0
  50. package/dist/compiler-core/adapter.js +2 -0
  51. package/dist/compiler-core/adapters/inventory.js +312 -0
  52. package/dist/compiler-core/compile.js +148 -0
  53. package/dist/compiler-core/component-facts.js +123 -0
  54. package/dist/compiler-core/discovery/docs.js +66 -0
  55. package/dist/compiler-core/discovery/entries.js +270 -0
  56. package/dist/compiler-core/discovery/exports.js +207 -0
  57. package/dist/compiler-core/discovery/graph.js +59 -0
  58. package/dist/compiler-core/discovery/index.js +103 -0
  59. package/dist/compiler-core/discovery/packages.js +148 -0
  60. package/dist/compiler-core/discovery/schema.js +144 -0
  61. package/dist/compiler-core/discovery/storybook.js +232 -0
  62. package/dist/compiler-core/discovery/support.js +82 -0
  63. package/dist/compiler-core/discovery/tokens.js +186 -0
  64. package/dist/compiler-core/discovery/tooling.js +146 -0
  65. package/dist/compiler-core/fs.js +89 -0
  66. package/dist/compiler-core/glob.js +3 -0
  67. package/dist/compiler-core/index.js +16 -0
  68. package/dist/compiler-core/ir-builder.js +68 -0
  69. package/dist/compiler-core/json-schema.js +11 -0
  70. package/dist/compiler-core/testing/index.js +81 -0
  71. package/dist/compiler-core/ts/ast.js +166 -0
  72. package/dist/compiler-core/ts/members.js +163 -0
  73. package/dist/compiler-core/ts/program.js +81 -0
  74. package/dist/compiler-core/ts/static-eval.js +145 -0
  75. package/dist/compiler-core/ts/tsconfig-paths.js +60 -0
  76. package/dist/compiler-core/walk.js +46 -0
  77. package/dist/compiler-ir/index.js +4 -0
  78. package/dist/compiler-ir/ir.js +67 -0
  79. package/dist/compiler-ir/json-schema.js +6 -0
  80. package/dist/compiler-ir/validate.js +116 -0
  81. package/dist/compiler-react/adapter.js +147 -0
  82. package/dist/compiler-react/analyze.js +199 -0
  83. package/dist/compiler-react/ast.js +105 -0
  84. package/dist/compiler-react/behavior.js +75 -0
  85. package/dist/compiler-react/detect.js +558 -0
  86. package/dist/compiler-react/emit.js +188 -0
  87. package/dist/compiler-react/index.js +2 -0
  88. package/dist/compiler-react/program.js +58 -0
  89. package/dist/compiler-react/props.js +157 -0
  90. package/dist/compiler-react/styling.js +138 -0
  91. package/dist/compiler-react/usage.js +116 -0
  92. package/dist/compiler-react/variants.js +111 -0
  93. package/dist/compiler-storybook/adapter.js +253 -0
  94. package/dist/compiler-storybook/csf.js +521 -0
  95. package/dist/compiler-storybook/emit.js +202 -0
  96. package/dist/compiler-storybook/index-json.js +41 -0
  97. package/dist/compiler-storybook/index.js +5 -0
  98. package/dist/compiler-storybook/mdx.js +120 -0
  99. package/dist/compiler-storybook/static-eval.js +145 -0
  100. package/dist/compiler-tokens/adapter.js +158 -0
  101. package/dist/compiler-tokens/css.js +59 -0
  102. package/dist/compiler-tokens/index.js +5 -0
  103. package/dist/compiler-tokens/json-lines.js +106 -0
  104. package/dist/compiler-tokens/json-tokens.js +52 -0
  105. package/dist/compiler-vue/adapter.js +107 -0
  106. package/dist/compiler-vue/analyze.js +344 -0
  107. package/dist/compiler-vue/declared.js +343 -0
  108. package/dist/compiler-vue/emit.js +129 -0
  109. package/dist/compiler-vue/index.js +5 -0
  110. package/dist/compiler-vue/script.js +472 -0
  111. package/dist/compiler-vue/sfc.js +44 -0
  112. package/dist/compiler-vue/template.js +73 -0
  113. package/dist/contract/archetype-checks.js +153 -0
  114. package/dist/contract/components.js +102 -0
  115. package/dist/contract/conflicts.js +39 -0
  116. package/dist/contract/contract.js +46 -0
  117. package/dist/contract/evidence.js +84 -0
  118. package/dist/contract/fact-path.js +39 -0
  119. package/dist/contract/facts.js +62 -0
  120. package/dist/contract/index.js +16 -0
  121. package/dist/contract/integrity.js +21 -0
  122. package/dist/contract/json-schema.js +11 -0
  123. package/dist/contract/knowledge.js +95 -0
  124. package/dist/contract/pattern-checks.js +254 -0
  125. package/dist/contract/patterns.js +225 -0
  126. package/dist/contract/primitives.js +81 -0
  127. package/dist/contract/sources.js +30 -0
  128. package/dist/contract/tokens.js +41 -0
  129. package/dist/contract/validate.js +253 -0
  130. package/dist/contract-query/archetype.js +188 -0
  131. package/dist/contract-query/index.js +6 -0
  132. package/dist/contract-query/patterns.js +298 -0
  133. package/dist/contract-query/scan.js +164 -0
  134. package/dist/contract-query/search.js +132 -0
  135. package/dist/contract-query/system.js +148 -0
  136. package/dist/contract-query/validate-code.js +239 -0
  137. package/dist/contract-query/validate-usage.js +150 -0
  138. package/dist/contract-query/views.js +288 -0
  139. package/dist/governance/decisions.js +229 -0
  140. package/dist/governance/index.js +5 -0
  141. package/dist/governance/patterns-file.js +156 -0
  142. package/dist/governance/patterns.js +510 -0
  143. package/dist/governance/policies.js +148 -0
  144. package/dist/mcp/index.js +7 -0
  145. package/dist/mcp/loader.js +70 -0
  146. package/dist/mcp/protocol.js +27 -0
  147. package/dist/mcp/resources.js +138 -0
  148. package/dist/mcp/server.js +177 -0
  149. package/dist/mcp/stdio.js +28 -0
  150. package/dist/mcp/tools.js +267 -0
  151. package/dist/reconciliation/code.js +62 -0
  152. package/dist/reconciliation/components.js +401 -0
  153. package/dist/reconciliation/conflicts.js +126 -0
  154. package/dist/reconciliation/facts.js +41 -0
  155. package/dist/reconciliation/index.js +7 -0
  156. package/dist/reconciliation/reconcile.js +134 -0
  157. package/dist/reconciliation/resolve.js +158 -0
  158. package/dist/reconciliation/review.js +81 -0
  159. package/dist/reconciliation/storybook.js +370 -0
  160. package/dist/reconciliation/tokens.js +179 -0
  161. package/dist/reconciliation/util.js +55 -0
  162. package/dist/semantic-engine/additive.js +51 -0
  163. package/dist/semantic-engine/compounds.js +91 -0
  164. package/dist/semantic-engine/documents.js +157 -0
  165. package/dist/semantic-engine/enrich.js +42 -0
  166. package/dist/semantic-engine/index.js +7 -0
  167. package/dist/semantic-engine/infer.js +38 -0
  168. package/dist/semantic-engine/tokens.js +120 -0
  169. package/dist/shared/canonical.js +65 -0
  170. package/dist/shared/diagnostic.js +78 -0
  171. package/dist/shared/glob.js +98 -0
  172. package/dist/shared/index.js +4 -0
  173. package/dist/shared/json.js +2 -0
  174. package/dist/validation/card.js +50 -0
  175. package/dist/validation/discovery/arrangements.js +162 -0
  176. package/dist/validation/discovery/discover.js +70 -0
  177. package/dist/validation/discovery/index.js +8 -0
  178. package/dist/validation/discovery/model.js +127 -0
  179. package/dist/validation/discovery/promote.js +24 -0
  180. package/dist/validation/discovery/reviews.js +63 -0
  181. package/dist/validation/discovery/shapes.js +47 -0
  182. package/dist/validation/discovery/similar-files.js +146 -0
  183. package/dist/validation/discovery/util.js +47 -0
  184. package/dist/validation/index.js +11 -0
  185. package/dist/validation/interaction-facts.js +341 -0
  186. package/dist/validation/interaction-workflows.js +633 -0
  187. package/dist/validation/literals.js +251 -0
  188. package/dist/validation/pattern-rules.js +608 -0
  189. package/dist/validation/policy.js +241 -0
  190. package/dist/validation/report.js +163 -0
  191. package/dist/validation/rules.js +269 -0
  192. package/dist/validation/scan.js +295 -0
  193. package/dist/validation/text.js +34 -0
  194. package/dist/validation/validate-code.js +513 -0
  195. package/dist/validation/validate-usage.js +196 -0
  196. package/package.json +38 -4
package/LICENSE ADDED
@@ -0,0 +1,105 @@
1
+ # Functional Source License, Version 1.1, ALv2 Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-ALv2
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 Matt Carlucci
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under
20
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
21
+ Conditions with the Software.
22
+
23
+ ### License Grant
24
+
25
+ Subject to your compliance with this License Grant and the Patents,
26
+ Redistribution and Trademark clauses below, we hereby grant you the right to
27
+ use, copy, modify, create derivative works, publicly perform, publicly display
28
+ and redistribute the Software for any Permitted Purpose identified below.
29
+
30
+ ### Permitted Purpose
31
+
32
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
33
+ means making the Software available to others in a commercial product or
34
+ service that:
35
+
36
+ 1. substitutes for the Software;
37
+
38
+ 2. substitutes for any other product or service we offer using the Software
39
+ that exists as of the date we make the Software available; or
40
+
41
+ 3. offers the same or substantially similar functionality as the Software.
42
+
43
+ Permitted Purposes specifically include using the Software:
44
+
45
+ 1. for your internal use and access;
46
+
47
+ 2. for non-commercial education;
48
+
49
+ 3. for non-commercial research; and
50
+
51
+ 4. in connection with professional services that you provide to a licensee
52
+ using the Software in accordance with these Terms and Conditions.
53
+
54
+ ### Patents
55
+
56
+ To the extent your use for a Permitted Purpose would necessarily infringe our
57
+ patents, the license grant above includes a license under our patents. If you
58
+ make a claim against any party that the Software infringes or contributes to
59
+ the infringement of any patent, then your patent license to the Software ends
60
+ immediately.
61
+
62
+ ### Redistribution
63
+
64
+ The Terms and Conditions apply to all copies, modifications and derivatives of
65
+ the Software.
66
+
67
+ If you redistribute any copies, modifications or derivatives of the Software,
68
+ you must include a copy of or a link to these Terms and Conditions and not
69
+ remove any copyright notices provided in or with the Software.
70
+
71
+ ### Disclaimer
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
74
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
75
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
76
+
77
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
78
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
79
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
80
+
81
+ ### Trademarks
82
+
83
+ Except for displaying the License Details and identifying us as the origin of
84
+ the Software, you have no right under these Terms and Conditions to use our
85
+ trademarks, trade names, service marks or product names.
86
+
87
+ ## Grant of Future License
88
+
89
+ We hereby irrevocably grant you an additional license to use the Software under
90
+ the Apache License, Version 2.0 that is effective on the second anniversary of
91
+ the date we make the Software available. On or after that date, you may use the
92
+ Software under the Apache License, Version 2.0, in which case the following
93
+ will apply:
94
+
95
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
96
+ this file except in compliance with the License.
97
+
98
+ You may obtain a copy of the License at
99
+
100
+ http://www.apache.org/licenses/LICENSE-2.0
101
+
102
+ Unless required by applicable law or agreed to in writing, software distributed
103
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
+ CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
+ specific language governing permissions and limitations under the License.
package/README.md CHANGED
@@ -1,3 +1,249 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/mcarlucci/invariant/main/site/assets/logo-wordmark.svg">
4
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/mcarlucci/invariant/main/site/assets/logo-wordmark-light.svg">
5
+ <img alt="Invariant" src="https://raw.githubusercontent.com/mcarlucci/invariant/main/site/assets/logo-wordmark-light.svg" width="280">
6
+ </picture>
7
+ </p>
2
8
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
9
+ <p align="center"><strong>Turn your design system into a contract that AI agents can read and check.</strong></p>
10
+
11
+ <p align="center">
12
+ <a href="https://mcarlucci.github.io/invariant/">Website</a> &middot;
13
+ <a href="https://mcarlucci.github.io/invariant/docs/">Documentation</a> &middot;
14
+ <a href="https://github.com/mcarlucci/invariant/blob/main/docs/specifications">Specifications</a>
15
+ </p>
16
+
17
+ Invariant scans your existing design system (React, Vue, Angular, Storybook, design tokens) and compiles it into one machine-readable file: the **contract**. AI coding tools then use that contract to learn which components, props and tokens really exist, and to check that generated code follows them.
18
+
19
+ No cloud service, no account, no network access. Everything runs locally.
20
+
21
+ ---
22
+
23
+ ## Why use it?
24
+
25
+ AI assistants guess at design systems. They invent props, hard-code colors and use the wrong component. Invariant gives them the facts:
26
+
27
+ - **Accurate.** Components, props, events and tokens are extracted from your source code, not made up by an LLM.
28
+ - **Traceable.** Every fact points back to the file and line it came from.
29
+ - **Honest.** Each fact is labeled `discovered`, `documented`, `inferred` or `approved`. Guesses are never passed off as facts, and conflicting sources are reported, not hidden.
30
+ - **Portable.** The contract is plain JSON. It doesn't depend on a frontend framework or on MCP.
31
+ - **Safe.** Your code is analyzed statically. Nothing from your project is executed.
32
+
33
+ ## How it works
34
+
35
+ ```
36
+ Your sources Invariant Consumers
37
+ ┌──────────────┐ ┌───────────────────┐ ┌────────────────────┐
38
+ │ React / Vue │ │ 1. Discover │ │ AI agents (MCP) │
39
+ │ Angular │ ───▶ │ 2. Extract to IR │ ───▶ │ CLI queries │
40
+ │ Storybook │ │ 3. Reconcile │ │ Usage validation │
41
+ │ Token files │ │ 4. Write contract │ │ HTML report │
42
+ └──────────────┘ └───────────────────┘ └────────────────────┘
43
+ ```
44
+
45
+ Each source adapter turns what it finds into a shared intermediate representation (IR). A framework-independent compiler then merges those into the contract.
46
+
47
+ ## Requirements
48
+
49
+ - Node.js 20 or newer to run it. To work on Invariant itself: Node 24 (see `.nvmrc`) and [pnpm](https://pnpm.io).
50
+
51
+ ## Quick start
52
+
53
+ Fastest path, with nothing to install first. In your project:
54
+
55
+ ```bash
56
+ npx @invariant-ui/cli setup
57
+ ```
58
+
59
+ It compiles your design system, finds the patterns your code already repeats, and shows one plan. Answer yes once and it approves the patterns, turns on starter rules (as warnings), connects your AI tool, baselines existing violations and adds a GitHub Action. In a design system repository it also makes the published package carry the contract. Nothing is approved until you confirm the contract. To keep the command, run `npm install -g @invariant-ui/cli`, then `invariant setup`.
60
+
61
+ From a source checkout, or one step at a time:
62
+
63
+ ```bash
64
+ # 1. Install and build (from a source checkout)
65
+ pnpm install
66
+ pnpm build
67
+
68
+ # 2. Go to the project you want to analyze and create a config
69
+ pnpm invariant init --cwd /path/to/your/project
70
+
71
+ # 3. Compile the contract
72
+ pnpm invariant compile --cwd /path/to/your/project
73
+
74
+ # 4. Explore it
75
+ pnpm invariant components list --cwd /path/to/your/project
76
+ pnpm invariant components inspect Button --cwd /path/to/your/project
77
+ pnpm invariant tokens list --cwd /path/to/your/project
78
+ ```
79
+
80
+ `pnpm invariant` runs `node apps/cli/dist/main.js`.
81
+
82
+ Compiling writes these files to the `invariant/` folder:
83
+
84
+ | File | What it is |
85
+ |---|---|
86
+ | `invariant.contract.json` | The contract (the source of truth) |
87
+ | `invariant.evidence.json` | Where each fact came from |
88
+ | `invariant.diagnostics.json` | Anything unsupported, incomplete or wrong |
89
+ | `invariant.review.json` | Conflicts and guesses waiting for a human decision |
90
+ | `invariant.report.html` | A static, self-contained summary for people |
91
+ | `invariant.manifest.json` | Tool version and a hash of every file |
92
+
93
+ Compiling unchanged sources gives identical output, so the files are safe to commit and diff.
94
+
95
+ ## Use it in your project
96
+
97
+ - **The design system is in your repo** (a package in a monorepo, or the repo itself): run `npx @invariant-ui/cli setup` at the root.
98
+ - **Your app uses a design system from npm:** the design system's repo compiles a contract and publishes its `invariant/` folder with the package. In your app, install the package and run `npx @invariant-ui/cli setup`. It finds the shipped contract, adds your own patterns on top, and writes a config with `"extends": { "contract": "node_modules/<package>/invariant" }`. `verify --fresh` flags the contract as stale when you upgrade the library.
99
+
100
+ ## Configuration
101
+
102
+ `invariant init` writes `invariant.config.json` for you. You can also edit it by hand:
103
+
104
+ ```json
105
+ {
106
+ "name": "Acme Design System",
107
+ "contractId": "acme-design-system",
108
+ "sources": [
109
+ { "id": "ui-library", "type": "react", "path": "packages/ui" },
110
+ { "id": "storybook", "type": "storybook", "path": "packages/ui" },
111
+ { "id": "tokens", "type": "tokens" }
112
+ ],
113
+ "output": { "directory": "invariant" }
114
+ }
115
+ ```
116
+
117
+ Supported source types: `react`, `vue`, `angular`, `storybook`, `tokens`. Figma and zeroheight are not supported yet.
118
+
119
+ Add sources from the command line with `invariant source add <type> [path]`.
120
+
121
+ Optional sections: `validation` (see `invariant rules`; `ignore` globs, `rules` severity overrides, `failOn`), `governance` (approver lists and the governance files' paths) and `enforcement` (`hooks`: `gate-errors` by default, `warn` or `off` — what the Claude Code hooks do when an agent writes code that breaks the contract).
122
+
123
+ ## Commands
124
+
125
+ | Command | What it does |
126
+ |---|---|
127
+ | `init` | Detect your project and write `invariant.config.json` |
128
+ | `source add` / `source remove` / `source list` | Manage configured sources |
129
+ | `analyze` | Extract and print what was found, without building a contract |
130
+ | `compile` | Run the full pipeline and write the contract and other files (`--watch` recompiles on change) |
131
+ | `components list` / `components inspect <name>` | Browse components (add `--evidence` to see source locations) |
132
+ | `tokens list`, `conflicts list` | Browse tokens and conflicts |
133
+ | `patterns list` / `inspect` / `resolve` / `search` / `relationships` | Browse patterns: parts, states, constraints, relationships, approval status and evidence; `resolve` reads a page archetype with everything it composes |
134
+ | `browser run` | Opt-in: check a running app in a headless browser (needs Playwright in your project). Runtime results, kept apart from `validate` |
135
+ | `setup` | One command: detect sources, compile, show what was found, propose patterns from your code, connect your AI tool, baseline existing violations and offer a GitHub Action that checks every pull request commit. Asks a few questions; nothing is approved until you confirm the contract |
136
+ | `discover <path...>` | Find structures that recur in your application code and write them as candidates to review. Nothing it finds is approved or enforced |
137
+ | `candidates list\|show\|accept\|reject\|defer\|promote` | Review candidates; `promote` adds an accepted one to `invariant.patterns.json` without approving it |
138
+ | `validate <path...>` | Check your application code against the contract and the structural rules of approved patterns |
139
+ | `verify` | Check the compiled output is intact (add `--fresh` to also detect stale sources and see what changed) |
140
+ | `rules`, `policy list` | List validation rules and approved policies |
141
+ | `review list\|show\|approve\|reject\|resolve\|accept\|dismiss` | Record human decisions on conflicts and inferred facts |
142
+ | `mcp` | Serve the contract to AI tools over stdio (read-only) |
143
+ | `hook` | Check an agent's Write/Edit against the contract and answer in its hook format (Claude Code runs this for you) |
144
+ | `agent init <client\|all>` | Write MCP config, skills, rules and enforcement hooks for Claude Code, Cursor, Codex and Windsurf |
145
+ | `export --to <dir>` | Copy the verified artifacts and JSON Schemas elsewhere |
146
+
147
+ Read commands accept `--json`. Global options: `--cwd`, `--config`, `--dir`, `--trust-config`.
148
+
149
+ Exit codes: `0` success, `1` findings or invalid output, `2` bad usage or input.
150
+
151
+ ## Use it with an AI agent
152
+
153
+ ```bash
154
+ invariant compile
155
+ invariant agent init claude-code # or cursor, codex, windsurf, all
156
+ ```
157
+
158
+ With Claude Code, you watch the contract being enforced. Ask an agent for a settings page with a delete button and every file it writes is checked before it lands:
159
+
160
+ ```text
161
+ 1. invariant blocked app/pages/Settings.tsx: 1 error against the @acme/ui contract
162
+ ✗ prop-value-not-allowed: Button.variant is "danger", which is not one of "primary", "secondary", "ghost" (line 8)
163
+ fix: Use one of "primary", "secondary", "ghost".
164
+ 2. ✗ color-not-in-tokens: #ff0000 is a literal color that is not the value of any token (line 8)
165
+ 3. ✗ pattern-required-child-missing: Modal at line 11 has no Button (pattern pattern:page.settings) (line 11)
166
+ 4. invariant ✓ app/pages/Settings.tsx follows the @acme/ui contract (6 component uses checked)
167
+ ```
168
+
169
+ Run `pnpm demo` in a checkout to replay exactly that, and `invariant enforcement report` (add `--format html --to report.html`) to get a forwardable timeline: `4 writes checked · 3 violations caught · 3 fixed · 0 outstanding`. See [See it working in your agent chat](https://mcarlucci.github.io/invariant/docs/#enforcement).
170
+
171
+ `agent init` installs the hooks for Claude Code: a write that would introduce an error-severity violation is blocked with the exact finding and fix; warnings show up right after the write; clean passes get a green line in the chat. The hooks fail open (if invariant cannot run, the write proceeds) and respect your baseline and rule overrides. Set `enforcement.hooks` to `warn` or `off` in `invariant.config.json` to soften or disable this.
172
+
173
+ | Client | How the contract is applied | Blocks a write |
174
+ | --- | --- | --- |
175
+ | Claude Code | Hooks on Write and Edit, plus MCP and skills | Yes |
176
+ | Cursor | MCP and rules; the agent is asked to validate | No |
177
+ | Codex, Windsurf | MCP and rules; the agent is asked to validate | No |
178
+ | Any client | `invariant validate` in CI | At pull request time |
179
+
180
+ The MCP server is read-only and gives the agent tools such as `get_component`, `get_design_tokens`, `search_design_system` and `validate_component_usage`. After a recompile, the server picks up the new contract without a restart. Whether the agent calls `validate_design_system_compliance` itself or a hook fires on its write, the chat shows the same enforcement card: the findings, the fixes, and what to do next. The enforcement report records hook checks only, not the agent's own MCP calls.
181
+
182
+ Contract text comes from your source and docs, so the server marks every result as untrusted content. Generated agent config never includes free-form descriptions from your code.
183
+
184
+ ## Repository layout
185
+
186
+ ```
187
+ apps/
188
+ cli/ The `invariant` command
189
+ packages/
190
+ compiler-ir/ Shared intermediate representation
191
+ compiler-core/ Pure compiler: IR to contract
192
+ compiler-react/ React adapter
193
+ compiler-vue/ Vue adapter
194
+ compiler-angular/ Angular adapter
195
+ compiler-storybook/ Storybook adapter
196
+ compiler-tokens/ Design token adapter
197
+ contract/ Contract schema and validation
198
+ contract-query/ Read API over a compiled contract
199
+ reconciliation/ Merging sources and surfacing conflicts
200
+ semantic-engine/ Classification and inference, with evidence
201
+ validation/ Usage validators and rules, and pattern discovery
202
+ browser-verify/ Opt-in runtime checks in a browser
203
+ governance/ Approvals, policies and decisions
204
+ artifact-generator/ Writes the output files
205
+ mcp/ MCP server
206
+ agent-kit/ Client configs, skills and rules
207
+ shared/ Shared utilities and structured diagnostics
208
+ fixtures/ Test projects and golden files
209
+ docs/ Architecture, ADRs and specifications
210
+ ```
211
+
212
+ ## Development
213
+
214
+ ```bash
215
+ pnpm build # tsc -b
216
+ pnpm test # vitest
217
+ pnpm lint # biome
218
+ pnpm typecheck
219
+ pnpm check:boundaries # enforce package boundaries
220
+ pnpm verify # lint + typecheck + boundaries + build + test
221
+ pnpm update:golden # regenerate golden fixtures
222
+ ```
223
+
224
+ ## Documentation
225
+
226
+ - [Website](https://mcarlucci.github.io/invariant/) and [documentation site](https://mcarlucci.github.io/invariant/docs/)
227
+ - [System architecture](https://github.com/mcarlucci/invariant/blob/main/docs/architecture/system-architecture.md)
228
+ - [Pattern definitions](https://github.com/mcarlucci/invariant/blob/main/docs/specifications/pattern-definitions-v1.md): write layouts, interactions and page archetypes in `invariant.patterns.json`
229
+ - [Browser verification](https://github.com/mcarlucci/invariant/blob/main/docs/specifications/browser-verification-v1.md): opt-in runtime checks for focus, keyboard order, rendered composition and responsive layout
230
+ - [Pattern discovery](https://github.com/mcarlucci/invariant/blob/main/docs/specifications/pattern-discovery-v1.md): find recurring structures in your code and review them as candidates
231
+ - [ADR 0001: Foundational architecture](https://github.com/mcarlucci/invariant/blob/main/docs/decisions/0001-architecture.md)
232
+ - Specifications in [`docs/specifications/`](https://github.com/mcarlucci/invariant/blob/main/docs/specifications): [CLI](https://github.com/mcarlucci/invariant/blob/main/docs/specifications/cli-v1.md), [contract](https://github.com/mcarlucci/invariant/blob/main/docs/specifications/contract-v1.md), [MCP and agents](https://github.com/mcarlucci/invariant/blob/main/docs/specifications/mcp-and-agents-v1.md), [validation and governance](https://github.com/mcarlucci/invariant/blob/main/docs/specifications/validation-and-governance-v1.md), and more
233
+
234
+ ## License
235
+
236
+ Invariant is source-available under the [Functional Source License, Version 1.1, ALv2 Future License](https://github.com/mcarlucci/invariant/blob/main/LICENSE) (FSL-1.1-ALv2).
237
+
238
+ You can read, run, modify and redistribute it for any purpose except a Competing Use: offering it in a commercial product or service that substitutes for Invariant or offers the same or similar functionality. Internal use, non-commercial education and research, and professional services for a licensee are always allowed.
239
+
240
+ Each version converts to Apache-2.0 two years after it is released.
241
+
242
+ Copyright 2026 Matt Carlucci.
243
+
244
+ ## Principles
245
+
246
+ - The contract is the single source of truth.
247
+ - Extract deterministically. LLMs may help classify, but never supply APIs, props or token values.
248
+ - Keep evidence for every fact.
249
+ - Never hide conflicts, and never turn a guess into an approved rule automatically.
@@ -0,0 +1,232 @@
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { MARKER } from "./guidance.js";
4
+ const BEGIN = "<!-- invariant:begin -->";
5
+ const END = "<!-- invariant:end -->";
6
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
7
+ /** Resolves a project-relative path, refusing anything that would land outside the root, symlinks included. */
8
+ export function resolveInside(root, rel) {
9
+ if (rel === "" || path.isAbsolute(rel) || rel.split(/[\\/]/).includes(".."))
10
+ throw new Error(`unsafe path: ${rel}`);
11
+ const target = path.resolve(root, rel);
12
+ const realRoot = realpathSync(root);
13
+ let probe = target;
14
+ while (!existsSync(probe))
15
+ probe = path.dirname(probe);
16
+ const real = realpathSync(probe);
17
+ if (real !== realRoot && !real.startsWith(realRoot + path.sep))
18
+ throw new Error(`path escapes the project: ${rel}`);
19
+ if (existsSync(target) && lstatSync(target).isSymbolicLink())
20
+ throw new Error(`refusing to write through a symbolic link: ${rel}`);
21
+ return target;
22
+ }
23
+ function write(file, text, dryRun) {
24
+ if (dryRun)
25
+ return;
26
+ mkdirSync(path.dirname(file), { recursive: true });
27
+ writeFileSync(file, text, "utf8");
28
+ }
29
+ function jsonMerge(file, f, force, dryRun) {
30
+ const rel = f.path;
31
+ let doc = {};
32
+ const existed = existsSync(file);
33
+ if (existed) {
34
+ try {
35
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
36
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
37
+ throw new Error("not an object");
38
+ doc = parsed;
39
+ }
40
+ catch {
41
+ return {
42
+ path: rel,
43
+ action: "skipped",
44
+ reason: "the file is not plain JSON (comments or trailing commas?), so it was left alone. Add the entry by hand."
45
+ };
46
+ }
47
+ }
48
+ let node = doc;
49
+ for (const key of f.key.slice(0, -1)) {
50
+ const next = node[key];
51
+ if (next === undefined) {
52
+ const created = {};
53
+ node[key] = created;
54
+ node = created;
55
+ }
56
+ else if (typeof next === "object" && next !== null && !Array.isArray(next))
57
+ node = next;
58
+ else
59
+ return { path: rel, action: "skipped", reason: `"${key}" is not an object, so the file was left alone` };
60
+ }
61
+ const last = f.key[f.key.length - 1];
62
+ if (same(node[last], f.value))
63
+ return { path: rel, action: "unchanged" };
64
+ if (node[last] !== undefined && !force)
65
+ return {
66
+ path: rel,
67
+ action: "skipped",
68
+ reason: `an entry "${last}" already exists with different settings; use --force to replace it`
69
+ };
70
+ node[last] = f.value;
71
+ write(file, `${JSON.stringify(doc, null, 2)}\n`, dryRun);
72
+ return { path: rel, action: existed ? "updated" : "created" };
73
+ }
74
+ /**
75
+ * Makes sure an array at `key` holds our elements, leaving any the user configured alone. An element that
76
+ * carries our `identity` is ours: identical means unchanged, different means replace with `--force`, and no
77
+ * identity anywhere means append. A non-array at the key is never touched.
78
+ */
79
+ function jsonArrayAppend(file, f, force, dryRun) {
80
+ const rel = f.path;
81
+ let doc = {};
82
+ const existed = existsSync(file);
83
+ if (existed) {
84
+ try {
85
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
86
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
87
+ throw new Error("not an object");
88
+ doc = parsed;
89
+ }
90
+ catch {
91
+ return {
92
+ path: rel,
93
+ action: "skipped",
94
+ reason: "the file is not plain JSON (comments or trailing commas?), so it was left alone. Add the entry by hand."
95
+ };
96
+ }
97
+ }
98
+ let node = doc;
99
+ for (const key of f.key.slice(0, -1)) {
100
+ const next = node[key];
101
+ if (next === undefined) {
102
+ const created = {};
103
+ node[key] = created;
104
+ node = created;
105
+ }
106
+ else if (typeof next === "object" && next !== null && !Array.isArray(next))
107
+ node = next;
108
+ else
109
+ return { path: rel, action: "skipped", reason: `"${key}" is not an object, so the file was left alone` };
110
+ }
111
+ const last = f.key[f.key.length - 1];
112
+ const arr = node[last];
113
+ if (arr === undefined) {
114
+ node[last] = f.value;
115
+ write(file, `${JSON.stringify(doc, null, 2)}\n`, dryRun);
116
+ return { path: rel, action: existed ? "updated" : "created" };
117
+ }
118
+ if (!Array.isArray(arr))
119
+ return { path: rel, action: "skipped", reason: `"${last}" is not an array, so the file was left alone` };
120
+ if (f.value.every((el) => arr.some((x) => same(x, el))))
121
+ return { path: rel, action: "unchanged" };
122
+ const isOurs = (el) => JSON.stringify(el).includes(f.identity);
123
+ if (arr.some(isOurs)) {
124
+ if (!force)
125
+ return {
126
+ path: rel,
127
+ action: "skipped",
128
+ reason: "an invariant hook already exists with different settings; use --force to replace it"
129
+ };
130
+ node[last] = [...arr.filter((el) => !isOurs(el)), ...f.value];
131
+ }
132
+ else
133
+ node[last] = [...arr, ...f.value];
134
+ write(file, `${JSON.stringify(doc, null, 2)}\n`, dryRun);
135
+ return { path: rel, action: existed ? "updated" : "created" };
136
+ }
137
+ function tomlTable(file, f, force, dryRun) {
138
+ if (!existsSync(file)) {
139
+ write(file, f.text, dryRun);
140
+ return { path: f.path, action: "created" };
141
+ }
142
+ const text = readFileSync(file, "utf8");
143
+ const header = `[${f.table}]`;
144
+ const lines = text.split("\n");
145
+ const start = lines.findIndex((l) => l.trim() === header);
146
+ if (start < 0) {
147
+ write(file, `${text}${text.endsWith("\n") || text === "" ? "" : "\n"}\n${f.text}`, dryRun);
148
+ return { path: f.path, action: "updated" };
149
+ }
150
+ let end = start + 1;
151
+ while (end < lines.length && !/^\s*\[/.test(lines[end]))
152
+ end++;
153
+ const current = lines.slice(start, end).join("\n").trimEnd();
154
+ if (current === f.text.trimEnd())
155
+ return { path: f.path, action: "unchanged" };
156
+ if (!force)
157
+ return {
158
+ path: f.path,
159
+ action: "skipped",
160
+ reason: `a [${f.table}] table already exists with different settings; use --force to replace it`
161
+ };
162
+ const next = [...lines.slice(0, start), ...f.text.trimEnd().split("\n"), "", ...lines.slice(end)].join("\n");
163
+ write(file, next, dryRun);
164
+ return { path: f.path, action: "updated" };
165
+ }
166
+ function generated(file, f, force, dryRun) {
167
+ if (!existsSync(file)) {
168
+ write(file, f.content, dryRun);
169
+ return { path: f.path, action: "created" };
170
+ }
171
+ const current = readFileSync(file, "utf8");
172
+ if (current === f.content)
173
+ return { path: f.path, action: "unchanged" };
174
+ if (!current.includes(MARKER) && !force)
175
+ return {
176
+ path: f.path,
177
+ action: "skipped",
178
+ reason: "the file exists and was not generated by invariant; use --force to replace it"
179
+ };
180
+ write(file, f.content, dryRun);
181
+ return { path: f.path, action: "updated" };
182
+ }
183
+ function markdownSection(file, f, dryRun) {
184
+ const block = `${BEGIN}\n<!-- ${MARKER}; edits between the markers are replaced when it is regenerated -->\n${f.content.trimEnd()}\n${END}\n`;
185
+ if (!existsSync(file)) {
186
+ write(file, block, dryRun);
187
+ return { path: f.path, action: "created" };
188
+ }
189
+ const text = readFileSync(file, "utf8");
190
+ const b = text.indexOf(BEGIN);
191
+ const e = text.indexOf(END);
192
+ if (b >= 0 && e > b) {
193
+ const next = `${text.slice(0, b)}${block.trimEnd()}${text.slice(e + END.length)}`;
194
+ if (next === text)
195
+ return { path: f.path, action: "unchanged" };
196
+ write(file, next, dryRun);
197
+ return { path: f.path, action: "updated" };
198
+ }
199
+ if (b >= 0 || e >= 0)
200
+ return {
201
+ path: f.path,
202
+ action: "skipped",
203
+ reason: "the invariant markers are unbalanced, so the file was left alone"
204
+ };
205
+ write(file, `${text}${text.endsWith("\n") || text === "" ? "" : "\n"}\n${block}`, dryRun);
206
+ return { path: f.path, action: "updated" };
207
+ }
208
+ /** Writes planned files under `root`. Nothing outside the root is touched, and user files are merged or left alone. */
209
+ export function applyPlan(root, files, options = {}) {
210
+ const force = options.force === true;
211
+ const dryRun = options.dryRun === true;
212
+ const one = (f) => {
213
+ let file;
214
+ try {
215
+ file = resolveInside(root, f.path);
216
+ }
217
+ catch (error) {
218
+ return { path: f.path, action: "skipped", reason: error.message };
219
+ }
220
+ if (f.kind === "json-merge")
221
+ return jsonMerge(file, f, force, dryRun);
222
+ if (f.kind === "json-array-append")
223
+ return jsonArrayAppend(file, f, force, dryRun);
224
+ if (f.kind === "toml-table")
225
+ return tomlTable(file, f, force, dryRun);
226
+ if (f.kind === "generated")
227
+ return generated(file, f, force, dryRun);
228
+ return markdownSection(file, f, dryRun);
229
+ };
230
+ return files.map(one);
231
+ }
232
+ //# sourceMappingURL=apply.js.map