@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
@@ -0,0 +1,312 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { globToRegExp } from "../compiler-core/index.js";
4
+ import { DesignSystem } from "../contract-query/index.js";
5
+ import { PATTERNS_FILE, parsePatterns } from "../governance/index.js";
6
+ import { applyReviews, CANDIDATE_KINDS, CANDIDATE_STATUSES, CANDIDATES_FILE, candidatesFileSchema, DEFAULT_OPTIONS, discover, emptyReviews, latestReview, parseReviews, REVIEWS_FILE, recordReview, serializeReviews, toDefinition } from "../validation/index.js";
7
+ import { json, outputDir, print, writableDir } from "./commands.js";
8
+ import { collect } from "./commands-validate.js";
9
+ import { loadConfig } from "./config.js";
10
+ import { CliError } from "./errors.js";
11
+ import { table } from "./format.js";
12
+ import { loadCompiled } from "./store.js";
13
+ import { CLI_NAME } from "./version.js";
14
+ const DISCOVER_USAGE = `usage: ${CLI_NAME} discover <path...> [--min-files n] [--min-uses n] [--similarity 0-1] [--depth n] [--json]
15
+ Finds structures that recur in your application code and writes them to ${CANDIDATES_FILE} as candidates to review.
16
+ Nothing it finds is approved or enforced.`;
17
+ const CANDIDATES_USAGE = `usage: ${CLI_NAME} candidates <list|show|accept|reject|defer|promote> ...
18
+ candidates list [--status s] [--kind k] [--all]
19
+ candidates show <id>
20
+ candidates accept|reject|defer <id> --as <name> [--note why] [--reference ref]
21
+ candidates promote <id> --id <pattern id> --as <name> [--name n] [--category c] [--dry-run]`;
22
+ function integer(name, value, fallback, min, max) {
23
+ if (value === undefined)
24
+ return fallback;
25
+ const n = Number(value);
26
+ if (!Number.isInteger(n) || n < min || n > max)
27
+ throw new CliError(`--${name} must be a whole number from ${min} to ${max}`);
28
+ return n;
29
+ }
30
+ function similarityOption(value) {
31
+ if (value === undefined)
32
+ return DEFAULT_OPTIONS.similarity;
33
+ const n = Number(value);
34
+ if (!Number.isFinite(n) || n <= 0 || n > 1)
35
+ throw new CliError("--similarity must be a number above 0 and up to 1");
36
+ return n;
37
+ }
38
+ const read = (file) => (existsSync(file) ? readFileSync(file, "utf8") : undefined);
39
+ async function reviewsPath(c) {
40
+ const loaded = await loadConfig({
41
+ cwd: c.cwd,
42
+ file: c.args.values.config,
43
+ trustConfig: c.args.values["trust-config"] === true
44
+ });
45
+ return path.resolve(c.cwd, loaded?.config.governance?.candidateReviewsFile ?? REVIEWS_FILE);
46
+ }
47
+ function loadReviews(file) {
48
+ const text = read(file);
49
+ if (text === undefined)
50
+ return undefined;
51
+ const parsed = parseReviews(text, path.basename(file));
52
+ if (!parsed.ok)
53
+ throw new CliError(parsed.message, 2);
54
+ return parsed.value;
55
+ }
56
+ /** `invariant discover <paths...>`: finds recurring structures and writes them as candidates. */
57
+ export async function discoverCommand(c) {
58
+ const targets = c.args.positionals.slice(1);
59
+ if (targets.length === 0)
60
+ throw new CliError(DISCOVER_USAGE);
61
+ const options = {
62
+ minFiles: integer("min-files", c.args.values["min-files"], DEFAULT_OPTIONS.minFiles, 2, 1000),
63
+ minUses: integer("min-uses", c.args.values["min-uses"], DEFAULT_OPTIONS.minUses, 2, 10000),
64
+ similarity: similarityOption(c.args.values.similarity),
65
+ maxDepth: integer("depth", c.args.values.depth, DEFAULT_OPTIONS.maxDepth, 1, 6)
66
+ };
67
+ const loaded = await loadConfig({
68
+ cwd: c.cwd,
69
+ file: c.args.values.config,
70
+ trustConfig: c.args.values["trust-config"] === true
71
+ });
72
+ const contractFolder = await outputDir(c);
73
+ const dir = await writableDir(c);
74
+ const { contract, evidence } = loadCompiled(contractFolder);
75
+ const ds = new DesignSystem(contract, evidence);
76
+ const ignore = (loaded?.config.validation?.ignore ?? []).map((g) => globToRegExp(g));
77
+ const found = [];
78
+ for (const t of targets)
79
+ collect(c.cwd, t, ignore, found);
80
+ const files = [...new Set(found)]
81
+ .sort()
82
+ .map((file) => ({ file, code: readFileSync(path.resolve(c.cwd, file), "utf8") }));
83
+ const result = discover({ ds, files, options, reviews: loadReviews(await reviewsPath(c)) });
84
+ mkdirSync(dir, { recursive: true });
85
+ writeFileSync(path.join(dir, CANDIDATES_FILE), `${JSON.stringify(result, null, 2)}\n`);
86
+ if (c.args.values.json)
87
+ json(c, result);
88
+ else
89
+ print(c, summaryText(result, c.args.values.all === true));
90
+ return 0;
91
+ }
92
+ function summaryText(result, all) {
93
+ const shown = result.candidates.filter((x) => all || x.subsumedBy === undefined);
94
+ const lines = [
95
+ `${result.summary.filesAnalyzed} of ${result.summary.files} files analyzed, ${result.candidates.length} candidates written to ${CANDIDATES_FILE}`
96
+ ];
97
+ for (const d of result.diagnostics)
98
+ lines.push(`${d.severity}: ${d.message}`);
99
+ if (shown.length > 0)
100
+ lines.push("", listTable(shown));
101
+ lines.push("", `Candidates are observations, not approved patterns. Review one with \`${CLI_NAME} candidates show <id>\`.`);
102
+ return lines.join("\n");
103
+ }
104
+ const listTable = (candidates) => table(["id", "kind", "status", "files", "uses", "name"], candidates.map((x) => [
105
+ x.id.replace("candidate:", ""),
106
+ x.kind,
107
+ x.status,
108
+ x.support.files,
109
+ x.support.occurrences,
110
+ x.suggestedName
111
+ ]));
112
+ /** Candidates as last discovered, with the decisions recorded since applied. */
113
+ async function loadCandidates(c) {
114
+ const dir = await writableDir(c);
115
+ const text = read(path.join(dir, CANDIDATES_FILE));
116
+ if (text === undefined)
117
+ throw new CliError(`no ${CANDIDATES_FILE} in ${dir}. Run \`${CLI_NAME} discover <path...>\` first.`);
118
+ let raw;
119
+ try {
120
+ raw = JSON.parse(text);
121
+ }
122
+ catch {
123
+ throw new CliError(`${CANDIDATES_FILE} is not valid JSON; run discover again`, 1);
124
+ }
125
+ const parsed = candidatesFileSchema.safeParse(raw);
126
+ if (!parsed.success)
127
+ throw new CliError(`${CANDIDATES_FILE} does not match its schema; run discover again`, 1);
128
+ const reviews = loadReviews(await reviewsPath(c));
129
+ const applied = applyReviews(parsed.data.candidates.map((x) => {
130
+ const { review: _, ...rest } = x;
131
+ return { ...rest, status: "proposed" };
132
+ }), reviews);
133
+ let stale = false;
134
+ try {
135
+ stale =
136
+ loadCompiled(await outputDir(c)).contract.integrity.contentHash !== parsed.data.contract.contentHash;
137
+ }
138
+ catch {
139
+ stale = false;
140
+ }
141
+ return { file: parsed.data, candidates: applied.candidates, stale, dir };
142
+ }
143
+ function find(candidates, query) {
144
+ if (!query)
145
+ throw new CliError(CANDIDATES_USAGE);
146
+ const q = query.replace(/^candidate:/, "");
147
+ const exact = candidates.filter((x) => x.id === `candidate:${q}`);
148
+ const hits = exact.length > 0
149
+ ? exact
150
+ : candidates.filter((x) => x.id.replace("candidate:", "").endsWith(q) ||
151
+ x.id.replace("candidate:", "").split(".")[1]?.startsWith(q));
152
+ if (hits.length === 1)
153
+ return hits[0];
154
+ if (hits.length === 0)
155
+ throw new CliError(`no candidate matches "${query}". List them with \`${CLI_NAME} candidates list --all\`.`);
156
+ throw new CliError(`"${query}" matches several candidates: ${hits.map((x) => x.id).join(", ")}. Use the full id.`);
157
+ }
158
+ function show(c, x) {
159
+ if (c.args.values.json) {
160
+ json(c, x);
161
+ return;
162
+ }
163
+ print(c, `${x.suggestedName} (${x.id})`);
164
+ print(c, `kind: ${x.kind} status: ${x.status} basis: ${x.basis} equivalence: ${x.equivalence}`);
165
+ print(c, `found in ${x.support.files} files, ${x.support.occurrences} places`);
166
+ if (x.consistency)
167
+ print(c, `overlap between files: lowest ${x.consistency.minimumPairwise}, mean ${x.consistency.meanPairwise}`);
168
+ if (x.subsumedBy)
169
+ print(c, `inside a larger candidate: ${x.subsumedBy}`);
170
+ print(c, `\n${x.summary}`);
171
+ print(c, "\nwhy it was found");
172
+ for (const line of x.explanation)
173
+ print(c, ` ${line}`);
174
+ if (x.structure.common.length > 0)
175
+ print(c, `\nin every file\n ${x.structure.common.join("\n ")}`);
176
+ if (x.structure.frequent.length > 0)
177
+ print(c, `\nin most files\n ${x.structure.frequent.map((f) => `${f.feature} (${f.files})`).join("\n ")}`);
178
+ if (x.structure.variable.length > 0)
179
+ print(c, `\nvaries\n ${x.structure.variable.map((f) => `${f.feature} (${f.files})`).join("\n ")}${x.structure.variableTruncated ? "\n ..." : ""}`);
180
+ if (x.kind === "arrangement")
181
+ print(c, `\nform: ${x.structure.key}`);
182
+ print(c, `\nevidence${x.evidenceTruncated ? ` (first ${x.evidence.length} of ${x.support.occurrences})` : ""}`);
183
+ for (const e of x.evidence)
184
+ print(c, ` ${e.file}:${e.line}:${e.column} ${e.detail}`);
185
+ if (x.review)
186
+ print(c, `\n${x.review.decision} by ${x.review.decidedBy} on ${x.review.decidedAt}${x.review.reason ? `: ${x.review.reason}` : ""}${x.review.promotedTo ? `\npromoted to ${x.review.promotedTo}` : ""}`);
187
+ else
188
+ print(c, `\nNo decision recorded. \`${CLI_NAME} candidates accept ${x.id.replace("candidate:", "")} --as <name>\``);
189
+ }
190
+ async function who(c) {
191
+ const loaded = await loadConfig({
192
+ cwd: c.cwd,
193
+ file: c.args.values.config,
194
+ trustConfig: c.args.values["trust-config"] === true
195
+ });
196
+ const by = c.args.values.as ?? c.io.env?.INVARIANT_USER;
197
+ if (!by || by.trim() === "")
198
+ throw new CliError("say who is deciding: --as <name> (or set INVARIANT_USER). Names are recorded, not authenticated.");
199
+ const approvers = loaded?.config.governance?.approvers;
200
+ if (approvers && approvers.length > 0 && !approvers.includes(by))
201
+ throw new CliError(`"${by}" is not listed in governance.approvers`);
202
+ return by;
203
+ }
204
+ const DECISION_OF = { accept: "accepted", reject: "rejected", defer: "deferred" };
205
+ async function decide(c, sub, id) {
206
+ const { candidates } = await loadCandidates(c);
207
+ const x = find(candidates, id);
208
+ const by = await who(c);
209
+ const file = await reviewsPath(c);
210
+ const next = recordReview(loadReviews(file) ?? emptyReviews(), {
211
+ candidate: x.id,
212
+ decision: DECISION_OF[sub],
213
+ decidedBy: by,
214
+ decidedAt: c.io.clock(),
215
+ ...(c.args.values.note ? { reason: c.args.values.note } : {}),
216
+ ...(c.args.values.reference ? { reference: c.args.values.reference } : {})
217
+ });
218
+ writeFileSync(file, serializeReviews(next));
219
+ print(c, `${DECISION_OF[sub]} ${x.id}, recorded in ${path.basename(file)}${sub === "accept" ? `. To add it to your definitions: ${CLI_NAME} candidates promote ${x.id.replace("candidate:", "")} --id <pattern id> --as ${by}` : ""}`);
220
+ return 0;
221
+ }
222
+ async function promote(c, id) {
223
+ const { candidates } = await loadCandidates(c);
224
+ const x = find(candidates, id);
225
+ if (x.status !== "accepted")
226
+ throw new CliError(`${x.id} is ${x.status}. Accept it first: ${CLI_NAME} candidates accept ${x.id.replace("candidate:", "")} --as <name>`);
227
+ const patternId = c.args.values.id;
228
+ if (!patternId)
229
+ throw new CliError(`give the pattern an id: --id <id>, such as composition.card-with-header\n${CANDIDATES_USAGE}`);
230
+ const by = await who(c);
231
+ const loaded = await loadConfig({
232
+ cwd: c.cwd,
233
+ file: c.args.values.config,
234
+ trustConfig: c.args.values["trust-config"] === true
235
+ });
236
+ const patternsFile = path.resolve(c.cwd, loaded?.config.governance?.patternsFile ?? PATTERNS_FILE);
237
+ const text = read(patternsFile);
238
+ let doc = { version: 1, patterns: [] };
239
+ if (text !== undefined) {
240
+ try {
241
+ doc = JSON.parse(text);
242
+ }
243
+ catch {
244
+ throw new CliError(`${path.basename(patternsFile)} is not valid JSON`, 2);
245
+ }
246
+ if (!Array.isArray(doc.patterns))
247
+ throw new CliError(`${path.basename(patternsFile)} has no "patterns" list`, 2);
248
+ }
249
+ const definition = toDefinition(x, {
250
+ id: patternId,
251
+ name: c.args.values.name,
252
+ category: c.args.values.category
253
+ });
254
+ const bare = (s) => s.replace(/^pattern:/, "");
255
+ if ((doc.patterns ?? []).some((p) => typeof p.id === "string" && bare(p.id) === bare(patternId)))
256
+ throw new CliError(`${path.basename(patternsFile)} already has a pattern "${patternId}"`);
257
+ const next = { ...doc, patterns: [...(doc.patterns ?? []), definition] };
258
+ const check = parsePatterns(JSON.stringify(next), path.basename(patternsFile));
259
+ if (!check.ok)
260
+ throw new CliError(`the definition is not valid: ${check.message}`, 2);
261
+ if (c.args.values["dry-run"]) {
262
+ json(c, definition);
263
+ return 0;
264
+ }
265
+ writeFileSync(patternsFile, `${JSON.stringify(next, null, 2)}\n`);
266
+ const reviewFile = await reviewsPath(c);
267
+ const prior = latestReview(loadReviews(reviewFile) ?? emptyReviews(), x.id);
268
+ writeFileSync(reviewFile, serializeReviews(recordReview(loadReviews(reviewFile) ?? emptyReviews(), {
269
+ candidate: x.id,
270
+ decision: "accepted",
271
+ decidedBy: by,
272
+ decidedAt: c.io.clock(),
273
+ ...(prior?.reason ? { reason: prior.reason } : {}),
274
+ ...(prior?.reference ? { reference: prior.reference } : {}),
275
+ promotedTo: patternId
276
+ })));
277
+ print(c, `added ${patternId} to ${path.basename(patternsFile)}. It has no approval, intent or constraints. Run \`${CLI_NAME} compile\`, then approve it with \`${CLI_NAME} review\` or add an approval block, once you have read it.`);
278
+ return 0;
279
+ }
280
+ /** `invariant candidates ...`: review what discovery found. */
281
+ export async function candidatesCommand(c) {
282
+ const [, sub, id] = c.args.positionals;
283
+ if (!sub || !["list", "show", "accept", "reject", "defer", "promote"].includes(sub))
284
+ throw new CliError(CANDIDATES_USAGE);
285
+ if (sub === "accept" || sub === "reject" || sub === "defer")
286
+ return decide(c, sub, id);
287
+ if (sub === "promote")
288
+ return promote(c, id);
289
+ const loaded = await loadCandidates(c);
290
+ if (sub === "show") {
291
+ show(c, find(loaded.candidates, id));
292
+ return 0;
293
+ }
294
+ const status = c.args.values.status;
295
+ if (status && !CANDIDATE_STATUSES.includes(status))
296
+ throw new CliError(`--status must be one of: ${CANDIDATE_STATUSES.join(", ")}`);
297
+ const kind = c.args.values.kind;
298
+ if (kind && !CANDIDATE_KINDS.includes(kind))
299
+ throw new CliError(`--kind must be one of: ${CANDIDATE_KINDS.join(", ")}`);
300
+ const shown = loaded.candidates.filter((x) => (!status || x.status === status) &&
301
+ (!kind || x.kind === kind) &&
302
+ (c.args.values.all || x.subsumedBy === undefined));
303
+ if (c.args.values.json)
304
+ json(c, shown);
305
+ else {
306
+ if (loaded.stale)
307
+ print(c, `note: the contract has changed since discovery ran. Run \`${CLI_NAME} discover\` again.`);
308
+ print(c, shown.length === 0 ? "no candidates match" : listTable(shown));
309
+ }
310
+ return 0;
311
+ }
312
+ //# sourceMappingURL=commands-discover.js.map
@@ -0,0 +1,35 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { readEvents } from "./enforcement-log.js";
4
+ import { renderHtml, renderText, summarize } from "./enforcement-report.js";
5
+ import { CliError } from "./errors.js";
6
+ /**
7
+ * `invariant enforcement report`: the session timeline from the enforcement log, as text (default) or one
8
+ * self-contained HTML page (`--format html`, optionally `--to <file>`). Covers the latest session unless
9
+ * `--session <id>` or `--all` says otherwise.
10
+ */
11
+ export async function enforcement(c) {
12
+ const [, sub] = c.args.positionals;
13
+ if (sub !== "report")
14
+ throw new CliError("usage: invariant enforcement report [--session <id>|--all] [--format text|html] [--to <file>]");
15
+ const format = c.args.values.format ?? "text";
16
+ if (format !== "text" && format !== "html")
17
+ throw new CliError("--format must be text or html");
18
+ const events = readEvents(c.cwd);
19
+ const last = events.at(-1);
20
+ const session = c.args.values.all ? "all" : (c.args.values.session ?? last?.session ?? "none");
21
+ const chosen = c.args.values.all ? events : events.filter((e) => e.session === session);
22
+ const summary = summarize(session, chosen);
23
+ const out = format === "html" ? renderHtml(summary) : renderText(summary);
24
+ const to = c.args.values.to;
25
+ if (to) {
26
+ const file = path.resolve(c.cwd, to);
27
+ mkdirSync(path.dirname(file), { recursive: true });
28
+ writeFileSync(file, out);
29
+ c.io.stdout(`wrote ${path.relative(c.cwd, file) || to}\n`);
30
+ }
31
+ else
32
+ c.io.stdout(out);
33
+ return 0;
34
+ }
35
+ //# sourceMappingURL=commands-enforcement.js.map
@@ -0,0 +1,179 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { resolveFactPath } from "../contract/index.js";
4
+ import { addDecision, claimsHash, DECISIONS_FILE, parseDecisions, serializeDecisions } from "../governance/index.js";
5
+ import { buildReviewQueue } from "../reconciliation/index.js";
6
+ import { json, outputDir, print } from "./commands.js";
7
+ import { loadConfig } from "./config.js";
8
+ import { CliError } from "./errors.js";
9
+ import { shortValue, table } from "./format.js";
10
+ import { loadCompiled } from "./store.js";
11
+ import { CLI_NAME } from "./version.js";
12
+ function items(contract) {
13
+ const { integrity: _i, ...draft } = contract;
14
+ return buildReviewQueue(draft);
15
+ }
16
+ function find(queue, id) {
17
+ const exact = queue.filter((i) => i.id === id);
18
+ const hits = exact.length > 0 ? exact : queue.filter((i) => i.id.startsWith(id));
19
+ if (hits.length === 0)
20
+ throw new CliError(`no open review item "${id}". List them with \`${CLI_NAME} review list\`; decided items disappear after \`${CLI_NAME} compile\`.`, 1);
21
+ if (hits.length > 1)
22
+ throw new CliError(`"${id}" matches ${hits.length} items; use more characters`, 1);
23
+ return hits[0];
24
+ }
25
+ export async function review(c) {
26
+ const [, sub, id] = c.args.positionals;
27
+ const { contract, evidence } = loadCompiled(await outputDir(c));
28
+ const queue = items(contract);
29
+ if (sub === "list") {
30
+ const kind = c.args.values.kind;
31
+ const rows = queue.filter((i) => !kind || i.kind === kind);
32
+ if (c.args.values.json)
33
+ json(c, rows);
34
+ else
35
+ print(c, rows.length === 0
36
+ ? "nothing to review"
37
+ : table(["id", "priority", "kind", "subject", "path", "confidence", "reason"], rows.map((i) => [
38
+ i.id,
39
+ i.priority,
40
+ i.kind,
41
+ i.subject,
42
+ i.path ?? "",
43
+ i.confidence ?? "",
44
+ shortValue(i.reason, 60)
45
+ ])));
46
+ return 0;
47
+ }
48
+ if (sub === "show") {
49
+ if (!id)
50
+ throw new CliError("usage: invariant review show <id>");
51
+ const item = find(queue, id);
52
+ const evidenceById = new Map(evidence.evidence.map((e) => [e.id, e]));
53
+ const where = item.evidenceIds
54
+ .map((e) => evidenceById.get(e))
55
+ .map((e) => e?.location.type === "file"
56
+ ? `${e.location.path}${e.location.line ? `:${e.location.line}` : ""}`
57
+ : (e?.sourceRef ?? ""));
58
+ if (c.args.values.json) {
59
+ json(c, { ...item, evidence: where });
60
+ return 0;
61
+ }
62
+ print(c, `${item.id} ${item.kind} ${item.priority}`);
63
+ print(c, `subject: ${item.subject}${item.path ? ` path: ${item.path}` : ""}`);
64
+ if (item.rule)
65
+ print(c, `rule: ${item.rule} confidence: ${item.confidence}`);
66
+ print(c, `reason: ${item.reason}`);
67
+ print(c, `evidence: ${[...new Set(where)].join(", ") || "none"}`);
68
+ if (item.conflictId) {
69
+ const conflict = contract.conflicts.find((x) => x.id === item.conflictId);
70
+ print(c, "claims:");
71
+ for (const cl of conflict?.claims ?? [])
72
+ print(c, ` ${cl.sourceId} (${cl.status}): ${cl.value === undefined ? "(absent)" : JSON.stringify(cl.value)}`);
73
+ }
74
+ return 0;
75
+ }
76
+ const factActions = ["approve", "reject"];
77
+ const conflictActions = ["resolve", "accept", "dismiss"];
78
+ if (!sub || ![...factActions, ...conflictActions].includes(sub) || !id)
79
+ throw new CliError("usage: invariant review <list|show <id>|approve <id>|reject <id>|resolve <id>|accept <id>|dismiss <id>> --as <name> [--role r] [--reference ref] [--note n] [--source id]");
80
+ const loaded = await loadConfig({
81
+ cwd: c.cwd,
82
+ file: c.args.values.config,
83
+ trustConfig: c.args.values["trust-config"] === true
84
+ });
85
+ const by = c.args.values.as ?? c.io.env?.INVARIANT_USER;
86
+ if (!by || by.trim() === "")
87
+ throw new CliError("say who is deciding: --as <name> (or set INVARIANT_USER). Names are recorded, not authenticated.");
88
+ const approvers = loaded?.config.governance?.approvers;
89
+ if (approvers && approvers.length > 0 && !approvers.includes(by))
90
+ throw new CliError(`"${by}" is not listed in governance.approvers`);
91
+ const item = find(queue, id);
92
+ const at = c.io.clock();
93
+ const note = c.args.values.note;
94
+ let decision;
95
+ if (conflictActions.includes(sub)) {
96
+ if (item.kind !== "conflict" || !item.conflictId)
97
+ throw new CliError(`${sub} applies to conflicts; ${item.id} is a fact (use approve${item.kind === "inferred-fact" ? " or reject" : ""})`);
98
+ const conflict = contract.conflicts.find((x) => x.id === item.conflictId);
99
+ if (!conflict)
100
+ throw new CliError("the conflict is no longer in the contract", 1);
101
+ const source = c.args.values.source;
102
+ if (source && !conflict.claims.some((cl) => cl.sourceId === source))
103
+ throw new CliError(`--source must be one of: ${conflict.claims.map((cl) => cl.sourceId).join(", ")}`);
104
+ decision = {
105
+ kind: "conflict",
106
+ action: sub,
107
+ id: conflict.id,
108
+ claimsHash: claimsHash(conflict),
109
+ ...(source ? { chosenSourceId: source } : {}),
110
+ by,
111
+ at,
112
+ ...(note ? { note } : {})
113
+ };
114
+ }
115
+ else {
116
+ if (item.kind === "conflict" || !item.path)
117
+ throw new CliError(`${sub} applies to facts; ${item.id} is a conflict (use resolve, accept or dismiss)`);
118
+ if (item.kind === "pattern-fact" && sub === "reject")
119
+ throw new CliError(`${item.id} is part of a pattern you wrote; reject does not apply. Approve it, or edit or remove the definition.`);
120
+ const entity = [
121
+ ...contract.components,
122
+ ...contract.tokens,
123
+ ...contract.guidelines,
124
+ ...contract.patterns,
125
+ ...contract.constraints
126
+ ].find((e) => e.id === item.subject);
127
+ const value = entity ? resolveFactPath(entity, item.path) : { found: false };
128
+ if (!value.found)
129
+ throw new CliError(`cannot read ${item.subject} ${item.path} from the contract`, 1);
130
+ decision = {
131
+ kind: "fact",
132
+ action: sub,
133
+ subject: item.subject,
134
+ path: item.path,
135
+ value: value.value,
136
+ ...(c.args.values.role ? { role: c.args.values.role } : {}),
137
+ ...(c.args.values.reference ? { reference: c.args.values.reference } : {}),
138
+ by,
139
+ at,
140
+ ...(note ? { note } : {})
141
+ };
142
+ }
143
+ const file = path.resolve(c.cwd, loaded?.config.governance?.decisionsFile ?? DECISIONS_FILE);
144
+ let current;
145
+ if (existsSync(file)) {
146
+ const parsed = parseDecisions(readFileSync(file, "utf8"));
147
+ if (!parsed.ok)
148
+ throw new CliError(parsed.message);
149
+ current = parsed.value;
150
+ }
151
+ writeFileSync(file, serializeDecisions(addDecision(current, decision)), "utf8");
152
+ print(c, `recorded: ${sub} ${item.subject}${item.path ? ` ${item.path}` : ""} by ${by}`);
153
+ print(c, `Run \`${CLI_NAME} compile\` to apply it, and commit ${path.relative(c.cwd, file)}.`);
154
+ return 0;
155
+ }
156
+ export async function policy(c) {
157
+ if (c.args.positionals[1] !== "list")
158
+ throw new CliError("usage: invariant policy list");
159
+ const { contract } = loadCompiled(await outputDir(c));
160
+ if (c.args.values.json) {
161
+ json(c, contract.policies);
162
+ return 0;
163
+ }
164
+ print(c, contract.policies.length === 0
165
+ ? `no approved policies. Write them in invariant.policies.json and run \`${CLI_NAME} compile\`.`
166
+ : table(["id", "severity", "enforcement", "rule", "approved by", "approved at"], contract.policies.map((p) => {
167
+ const rule = contract.constraints.find((k) => k.id === p.basedOn[0])?.extensions?.invariant?.rule?.kind ?? "";
168
+ return [
169
+ p.id,
170
+ p.severity,
171
+ p.enforcement,
172
+ rule,
173
+ p.approval.approvedBy,
174
+ p.approval.approvedAt.slice(0, 10)
175
+ ];
176
+ })));
177
+ return 0;
178
+ }
179
+ //# sourceMappingURL=commands-governance.js.map