@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.
- package/LICENSE +105 -0
- package/README.md +248 -2
- package/dist/agent-kit/apply.js +232 -0
- package/dist/agent-kit/guidance.js +236 -0
- package/dist/agent-kit/index.js +5 -0
- package/dist/agent-kit/integrations.js +121 -0
- package/dist/agent-kit/plugin.js +115 -0
- package/dist/artifact-generator/index.js +75 -0
- package/dist/browser-verify/driver.js +2 -0
- package/dist/browser-verify/index.js +4 -0
- package/dist/browser-verify/playwright.js +82 -0
- package/dist/browser-verify/runner.js +175 -0
- package/dist/browser-verify/spec.js +103 -0
- package/dist/cli/args.js +77 -0
- package/dist/cli/baseline.js +64 -0
- package/dist/cli/cli.js +121 -0
- package/dist/cli/commands-browser.js +104 -0
- package/dist/cli/commands-discover.js +312 -0
- package/dist/cli/commands-enforcement.js +35 -0
- package/dist/cli/commands-governance.js +179 -0
- package/dist/cli/commands-hook.js +269 -0
- package/dist/cli/commands-patterns.js +324 -0
- package/dist/cli/commands-setup.js +485 -0
- package/dist/cli/commands-validate.js +157 -0
- package/dist/cli/commands.js +517 -0
- package/dist/cli/config.js +197 -0
- package/dist/cli/diff.js +66 -0
- package/dist/cli/enforcement-log.js +83 -0
- package/dist/cli/enforcement-report.js +96 -0
- package/dist/cli/errors.js +10 -0
- package/dist/cli/format.js +22 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/io.js +2 -0
- package/dist/cli/main.js +31 -0
- package/dist/cli/pipeline.js +262 -0
- package/dist/cli/publish.js +77 -0
- package/dist/cli/report.js +127 -0
- package/dist/cli/starter-rules.js +57 -0
- package/dist/cli/store.js +60 -0
- package/dist/cli/version.js +5 -0
- package/dist/cli/watch.js +68 -0
- package/dist/cli/workflow.js +52 -0
- package/dist/compiler-angular/adapter.js +115 -0
- package/dist/compiler-angular/analyze.js +222 -0
- package/dist/compiler-angular/classes.js +446 -0
- package/dist/compiler-angular/declared.js +285 -0
- package/dist/compiler-angular/emit.js +183 -0
- package/dist/compiler-angular/index.js +4 -0
- package/dist/compiler-angular/template.js +142 -0
- package/dist/compiler-core/adapter.js +2 -0
- package/dist/compiler-core/adapters/inventory.js +312 -0
- package/dist/compiler-core/compile.js +148 -0
- package/dist/compiler-core/component-facts.js +123 -0
- package/dist/compiler-core/discovery/docs.js +66 -0
- package/dist/compiler-core/discovery/entries.js +270 -0
- package/dist/compiler-core/discovery/exports.js +207 -0
- package/dist/compiler-core/discovery/graph.js +59 -0
- package/dist/compiler-core/discovery/index.js +103 -0
- package/dist/compiler-core/discovery/packages.js +148 -0
- package/dist/compiler-core/discovery/schema.js +144 -0
- package/dist/compiler-core/discovery/storybook.js +232 -0
- package/dist/compiler-core/discovery/support.js +82 -0
- package/dist/compiler-core/discovery/tokens.js +186 -0
- package/dist/compiler-core/discovery/tooling.js +146 -0
- package/dist/compiler-core/fs.js +89 -0
- package/dist/compiler-core/glob.js +3 -0
- package/dist/compiler-core/index.js +16 -0
- package/dist/compiler-core/ir-builder.js +68 -0
- package/dist/compiler-core/json-schema.js +11 -0
- package/dist/compiler-core/testing/index.js +81 -0
- package/dist/compiler-core/ts/ast.js +166 -0
- package/dist/compiler-core/ts/members.js +163 -0
- package/dist/compiler-core/ts/program.js +81 -0
- package/dist/compiler-core/ts/static-eval.js +145 -0
- package/dist/compiler-core/ts/tsconfig-paths.js +60 -0
- package/dist/compiler-core/walk.js +46 -0
- package/dist/compiler-ir/index.js +4 -0
- package/dist/compiler-ir/ir.js +67 -0
- package/dist/compiler-ir/json-schema.js +6 -0
- package/dist/compiler-ir/validate.js +116 -0
- package/dist/compiler-react/adapter.js +147 -0
- package/dist/compiler-react/analyze.js +199 -0
- package/dist/compiler-react/ast.js +105 -0
- package/dist/compiler-react/behavior.js +75 -0
- package/dist/compiler-react/detect.js +558 -0
- package/dist/compiler-react/emit.js +188 -0
- package/dist/compiler-react/index.js +2 -0
- package/dist/compiler-react/program.js +58 -0
- package/dist/compiler-react/props.js +157 -0
- package/dist/compiler-react/styling.js +138 -0
- package/dist/compiler-react/usage.js +116 -0
- package/dist/compiler-react/variants.js +111 -0
- package/dist/compiler-storybook/adapter.js +253 -0
- package/dist/compiler-storybook/csf.js +521 -0
- package/dist/compiler-storybook/emit.js +202 -0
- package/dist/compiler-storybook/index-json.js +41 -0
- package/dist/compiler-storybook/index.js +5 -0
- package/dist/compiler-storybook/mdx.js +120 -0
- package/dist/compiler-storybook/static-eval.js +145 -0
- package/dist/compiler-tokens/adapter.js +158 -0
- package/dist/compiler-tokens/css.js +59 -0
- package/dist/compiler-tokens/index.js +5 -0
- package/dist/compiler-tokens/json-lines.js +106 -0
- package/dist/compiler-tokens/json-tokens.js +52 -0
- package/dist/compiler-vue/adapter.js +107 -0
- package/dist/compiler-vue/analyze.js +344 -0
- package/dist/compiler-vue/declared.js +343 -0
- package/dist/compiler-vue/emit.js +129 -0
- package/dist/compiler-vue/index.js +5 -0
- package/dist/compiler-vue/script.js +472 -0
- package/dist/compiler-vue/sfc.js +44 -0
- package/dist/compiler-vue/template.js +73 -0
- package/dist/contract/archetype-checks.js +153 -0
- package/dist/contract/components.js +102 -0
- package/dist/contract/conflicts.js +39 -0
- package/dist/contract/contract.js +46 -0
- package/dist/contract/evidence.js +84 -0
- package/dist/contract/fact-path.js +39 -0
- package/dist/contract/facts.js +62 -0
- package/dist/contract/index.js +16 -0
- package/dist/contract/integrity.js +21 -0
- package/dist/contract/json-schema.js +11 -0
- package/dist/contract/knowledge.js +95 -0
- package/dist/contract/pattern-checks.js +254 -0
- package/dist/contract/patterns.js +225 -0
- package/dist/contract/primitives.js +81 -0
- package/dist/contract/sources.js +30 -0
- package/dist/contract/tokens.js +41 -0
- package/dist/contract/validate.js +253 -0
- package/dist/contract-query/archetype.js +188 -0
- package/dist/contract-query/index.js +6 -0
- package/dist/contract-query/patterns.js +298 -0
- package/dist/contract-query/scan.js +164 -0
- package/dist/contract-query/search.js +132 -0
- package/dist/contract-query/system.js +148 -0
- package/dist/contract-query/validate-code.js +239 -0
- package/dist/contract-query/validate-usage.js +150 -0
- package/dist/contract-query/views.js +288 -0
- package/dist/governance/decisions.js +229 -0
- package/dist/governance/index.js +5 -0
- package/dist/governance/patterns-file.js +156 -0
- package/dist/governance/patterns.js +510 -0
- package/dist/governance/policies.js +148 -0
- package/dist/mcp/index.js +7 -0
- package/dist/mcp/loader.js +70 -0
- package/dist/mcp/protocol.js +27 -0
- package/dist/mcp/resources.js +138 -0
- package/dist/mcp/server.js +177 -0
- package/dist/mcp/stdio.js +28 -0
- package/dist/mcp/tools.js +267 -0
- package/dist/reconciliation/code.js +62 -0
- package/dist/reconciliation/components.js +401 -0
- package/dist/reconciliation/conflicts.js +126 -0
- package/dist/reconciliation/facts.js +41 -0
- package/dist/reconciliation/index.js +7 -0
- package/dist/reconciliation/reconcile.js +134 -0
- package/dist/reconciliation/resolve.js +158 -0
- package/dist/reconciliation/review.js +81 -0
- package/dist/reconciliation/storybook.js +370 -0
- package/dist/reconciliation/tokens.js +179 -0
- package/dist/reconciliation/util.js +55 -0
- package/dist/semantic-engine/additive.js +51 -0
- package/dist/semantic-engine/compounds.js +91 -0
- package/dist/semantic-engine/documents.js +157 -0
- package/dist/semantic-engine/enrich.js +42 -0
- package/dist/semantic-engine/index.js +7 -0
- package/dist/semantic-engine/infer.js +38 -0
- package/dist/semantic-engine/tokens.js +120 -0
- package/dist/shared/canonical.js +65 -0
- package/dist/shared/diagnostic.js +78 -0
- package/dist/shared/glob.js +98 -0
- package/dist/shared/index.js +4 -0
- package/dist/shared/json.js +2 -0
- package/dist/validation/card.js +50 -0
- package/dist/validation/discovery/arrangements.js +162 -0
- package/dist/validation/discovery/discover.js +70 -0
- package/dist/validation/discovery/index.js +8 -0
- package/dist/validation/discovery/model.js +127 -0
- package/dist/validation/discovery/promote.js +24 -0
- package/dist/validation/discovery/reviews.js +63 -0
- package/dist/validation/discovery/shapes.js +47 -0
- package/dist/validation/discovery/similar-files.js +146 -0
- package/dist/validation/discovery/util.js +47 -0
- package/dist/validation/index.js +11 -0
- package/dist/validation/interaction-facts.js +341 -0
- package/dist/validation/interaction-workflows.js +633 -0
- package/dist/validation/literals.js +251 -0
- package/dist/validation/pattern-rules.js +608 -0
- package/dist/validation/policy.js +241 -0
- package/dist/validation/report.js +163 -0
- package/dist/validation/rules.js +269 -0
- package/dist/validation/scan.js +295 -0
- package/dist/validation/text.js +34 -0
- package/dist/validation/validate-code.js +513 -0
- package/dist/validation/validate-usage.js +196 -0
- package/package.json +38 -4
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { globToRegExp, matchesAny } from "../compiler-core/index.js";
|
|
4
|
+
import { DesignSystem } from "../contract-query/index.js";
|
|
5
|
+
import { enforcementCard, passCard, validateCode } from "../validation/index.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { applyBaseline, BASELINE_FILE, readBaseline } from "./baseline.js";
|
|
8
|
+
import { EXTENSIONS, frameworkFor } from "./commands-validate.js";
|
|
9
|
+
import { loadConfig, outputDirectory } from "./config.js";
|
|
10
|
+
import { appendEvent } from "./enforcement-log.js";
|
|
11
|
+
import { CliError } from "./errors.js";
|
|
12
|
+
import { loadCompiled } from "./store.js";
|
|
13
|
+
/**
|
|
14
|
+
* `invariant hook`: the agent-enforcement entry point. A coding agent (Claude Code today) runs this as a
|
|
15
|
+
* `PreToolUse`/`PostToolUse` hook on Write and Edit, passing the hook payload on stdin. The command validates
|
|
16
|
+
* the file the agent is writing against the compiled contract and answers in the agent's hook format:
|
|
17
|
+
*
|
|
18
|
+
* - `PreToolUse`, posture `gate-errors`: error-severity findings (after the baseline and rule overrides) deny
|
|
19
|
+
* the write, with the findings as the reason the agent must fix.
|
|
20
|
+
* - `PostToolUse`: findings are reported next to the tool result so the agent can fix them in a follow-up edit,
|
|
21
|
+
* and a clean pass is confirmed to the person watching.
|
|
22
|
+
*
|
|
23
|
+
* The hook fails open: any problem (no config, no contract, malformed payload, oversized input) is noted on
|
|
24
|
+
* stderr and the write proceeds. Warnings never block a write.
|
|
25
|
+
*/
|
|
26
|
+
/** Claude Code hook payloads carry the whole file; the MCP line limit is a reasonable ceiling here too. */
|
|
27
|
+
const MAX_PAYLOAD_CHARS = 2_000_000;
|
|
28
|
+
export const POSTURES = ["gate-errors", "warn", "off"];
|
|
29
|
+
export const DEFAULT_POSTURE = "gate-errors";
|
|
30
|
+
const payloadSchema = z.object({
|
|
31
|
+
hook_event_name: z.string(),
|
|
32
|
+
tool_name: z.string(),
|
|
33
|
+
cwd: z.string().optional(),
|
|
34
|
+
session_id: z.string().optional(),
|
|
35
|
+
tool_input: z.object({
|
|
36
|
+
/** Claude Code always expands it to an absolute path before hooks run. */
|
|
37
|
+
file_path: z.string().min(1),
|
|
38
|
+
content: z.string().optional(),
|
|
39
|
+
old_string: z.string().optional(),
|
|
40
|
+
new_string: z.string().optional(),
|
|
41
|
+
replace_all: z.boolean().optional()
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
/**
|
|
45
|
+
* Cursor's `afterFileEdit` payload (cursor.com/docs/hooks): the edit has already landed, so it is treated as a
|
|
46
|
+
* `PostToolUse` on the file. The fields are from the docs; the answer shape in {@link cursorAnswer} is not yet
|
|
47
|
+
* verified against a live Cursor.
|
|
48
|
+
*/
|
|
49
|
+
const cursorPayloadSchema = z.object({
|
|
50
|
+
hook_event_name: z.string(),
|
|
51
|
+
file_path: z.string().min(1),
|
|
52
|
+
workspace_roots: z.array(z.string()).optional(),
|
|
53
|
+
conversation_id: z.string().optional()
|
|
54
|
+
});
|
|
55
|
+
const CLIENTS = ["claude-code", "cursor"];
|
|
56
|
+
function cursorPayload(raw) {
|
|
57
|
+
const parsed = cursorPayloadSchema.safeParse(raw);
|
|
58
|
+
if (!parsed.success || parsed.data.hook_event_name !== "afterFileEdit")
|
|
59
|
+
return undefined;
|
|
60
|
+
const root = parsed.data.workspace_roots?.find((r) => parsed.data.file_path.startsWith(r));
|
|
61
|
+
return {
|
|
62
|
+
hook_event_name: "PostToolUse",
|
|
63
|
+
tool_name: "Edit",
|
|
64
|
+
...((root ?? parsed.data.workspace_roots?.[0]) ? { cwd: root ?? parsed.data.workspace_roots?.[0] } : {}),
|
|
65
|
+
...(parsed.data.conversation_id ? { session_id: parsed.data.conversation_id } : {}),
|
|
66
|
+
tool_input: { file_path: parsed.data.file_path }
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Cursor reads `agent_message` as feedback to the model and `user_message` as text for the person. */
|
|
70
|
+
function cursorAnswer(decision) {
|
|
71
|
+
if (typeof decision.reason === "string")
|
|
72
|
+
return { agent_message: decision.reason };
|
|
73
|
+
if (typeof decision.systemMessage === "string")
|
|
74
|
+
return { user_message: decision.systemMessage };
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
async function readAll(stream, max) {
|
|
78
|
+
let text = "";
|
|
79
|
+
for await (const chunk of stream) {
|
|
80
|
+
text += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
81
|
+
if (text.length > max)
|
|
82
|
+
throw new CliError(`the hook payload is larger than ${max.toLocaleString()} characters`);
|
|
83
|
+
}
|
|
84
|
+
return text;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The content the write would produce. `Write` carries it; `Edit` carries only the change, so the current file
|
|
88
|
+
* is read and the edit applied to reconstruct it. When the edit cannot apply (old text missing or ambiguous),
|
|
89
|
+
* the agent's own Edit call fails anyway and the hook stays silent; `PostToolUse` reads what landed.
|
|
90
|
+
*/
|
|
91
|
+
function contentFor(event, tool, input, file) {
|
|
92
|
+
if (event === "PostToolUse") {
|
|
93
|
+
try {
|
|
94
|
+
return readFileSync(file, "utf8");
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (tool === "Write")
|
|
101
|
+
return input.content;
|
|
102
|
+
if (input.old_string === undefined || input.new_string === undefined)
|
|
103
|
+
return undefined;
|
|
104
|
+
let current;
|
|
105
|
+
try {
|
|
106
|
+
current = readFileSync(file, "utf8");
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const parts = current.split(input.old_string);
|
|
112
|
+
if (parts.length === 1)
|
|
113
|
+
return undefined;
|
|
114
|
+
if (parts.length > 2 && input.replace_all !== true)
|
|
115
|
+
return undefined;
|
|
116
|
+
return parts.join(input.new_string);
|
|
117
|
+
}
|
|
118
|
+
/** Runs the check and returns the hook's JSON answer, or undefined to stay silent. */
|
|
119
|
+
async function decide(c, payload) {
|
|
120
|
+
const event = payload.hook_event_name;
|
|
121
|
+
if (event !== "PreToolUse" && event !== "PostToolUse")
|
|
122
|
+
return undefined;
|
|
123
|
+
if (payload.tool_name !== "Write" && payload.tool_name !== "Edit")
|
|
124
|
+
return undefined;
|
|
125
|
+
const file = payload.tool_input.file_path;
|
|
126
|
+
if (!EXTENSIONS.test(file))
|
|
127
|
+
return undefined;
|
|
128
|
+
// The project is where the session runs: the agent exports CLAUDE_PROJECT_DIR to hooks, the payload carries
|
|
129
|
+
// the working directory, and an explicit --cwd wins for manual runs.
|
|
130
|
+
const root = c.args.values.cwd ? c.cwd : (c.io.env?.CLAUDE_PROJECT_DIR ?? payload.cwd ?? c.cwd);
|
|
131
|
+
const rel = path.relative(root, file);
|
|
132
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel))
|
|
133
|
+
return undefined;
|
|
134
|
+
const loaded = await loadConfig({ cwd: root, trustConfig: false });
|
|
135
|
+
if (!loaded) {
|
|
136
|
+
if (c.args.values.quiet !== true)
|
|
137
|
+
c.io.stderr(`invariant hook: no invariant.config.json in ${root}; the write proceeds unchecked\n`);
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const ignore = (loaded.config.validation?.ignore ?? []).map((g) => globToRegExp(g));
|
|
141
|
+
if (matchesAny(rel, ignore))
|
|
142
|
+
return undefined;
|
|
143
|
+
const postureArg = c.args.values.posture;
|
|
144
|
+
if (postureArg && !POSTURES.includes(postureArg))
|
|
145
|
+
throw new CliError(`--posture must be one of ${POSTURES.join(", ")}`);
|
|
146
|
+
const posture = (postureArg ?? loaded.config.enforcement?.hooks ?? DEFAULT_POSTURE);
|
|
147
|
+
if (posture === "off")
|
|
148
|
+
return undefined;
|
|
149
|
+
const dir = path.resolve(root, outputDirectory(loaded.config));
|
|
150
|
+
if (!existsSync(path.join(dir, "invariant.contract.json"))) {
|
|
151
|
+
if (c.args.values.quiet !== true)
|
|
152
|
+
c.io.stderr(`invariant hook: no compiled contract in ${path.relative(root, dir) || "."}; run \`invariant compile\`. The write proceeds unchecked.\n`);
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
const { contract, evidence } = loadCompiled(dir);
|
|
156
|
+
const ds = new DesignSystem(contract, evidence);
|
|
157
|
+
const content = contentFor(event, payload.tool_name, payload.tool_input, file);
|
|
158
|
+
if (content === undefined)
|
|
159
|
+
return undefined;
|
|
160
|
+
const rules = (loaded.config.validation?.rules ?? {});
|
|
161
|
+
const result = validateCode(ds, { code: content, filename: rel, framework: frameworkFor(rel) }, { rules });
|
|
162
|
+
let violations = result.findings.map((f) => ({ ...f, file: rel }));
|
|
163
|
+
const baselineFile = path.resolve(root, BASELINE_FILE);
|
|
164
|
+
if (existsSync(baselineFile)) {
|
|
165
|
+
try {
|
|
166
|
+
const baseline = readBaseline(baselineFile);
|
|
167
|
+
if (baseline)
|
|
168
|
+
violations = applyBaseline(violations, baseline).fresh;
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
// A broken baseline must not block writes: note it and check without one.
|
|
172
|
+
c.io.stderr(`invariant hook: ${error.message}\n`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const name = contract.metadata.name;
|
|
176
|
+
const log = (outcome) => {
|
|
177
|
+
try {
|
|
178
|
+
appendEvent(root, {
|
|
179
|
+
time: c.io.clock(),
|
|
180
|
+
session: payload.session_id ?? "unknown",
|
|
181
|
+
file: rel,
|
|
182
|
+
outcome,
|
|
183
|
+
findings: violations,
|
|
184
|
+
checked: result.summary.componentsChecked.length
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
c.io.stderr(`invariant hook: could not write the enforcement log: ${error.message}\n`);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const more = (n) => `…and ${n} more (run \`invariant validate ${rel}\`)`;
|
|
192
|
+
if (event === "PreToolUse") {
|
|
193
|
+
if (posture === "gate-errors" && violations.some((v) => v.severity === "error")) {
|
|
194
|
+
log("blocked");
|
|
195
|
+
return {
|
|
196
|
+
hookSpecificOutput: {
|
|
197
|
+
hookEventName: "PreToolUse",
|
|
198
|
+
permissionDecision: "deny",
|
|
199
|
+
permissionDecisionReason: enforcementCard({
|
|
200
|
+
findings: violations,
|
|
201
|
+
contractName: name,
|
|
202
|
+
file: rel,
|
|
203
|
+
blocked: true,
|
|
204
|
+
next: "Fix these, then write the file again.",
|
|
205
|
+
more
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
log(violations.length > 0 ? "warned" : "passed");
|
|
213
|
+
if (violations.length > 0)
|
|
214
|
+
return {
|
|
215
|
+
decision: "block",
|
|
216
|
+
reason: enforcementCard({
|
|
217
|
+
findings: violations,
|
|
218
|
+
contractName: name,
|
|
219
|
+
file: rel,
|
|
220
|
+
next: "Fix these in a follow-up edit.",
|
|
221
|
+
more
|
|
222
|
+
})
|
|
223
|
+
};
|
|
224
|
+
return {
|
|
225
|
+
systemMessage: passCard({
|
|
226
|
+
contractName: name,
|
|
227
|
+
file: rel,
|
|
228
|
+
componentsChecked: result.summary.componentsChecked
|
|
229
|
+
})
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** `invariant hook`: check an agent's Write/Edit against the contract and answer in its hook format. */
|
|
233
|
+
export async function hook(c) {
|
|
234
|
+
const { input } = c.io;
|
|
235
|
+
if (!input)
|
|
236
|
+
throw new CliError("`hook` reads the agent's hook payload from standard input");
|
|
237
|
+
const client = c.args.values.client ?? "claude-code";
|
|
238
|
+
if (!CLIENTS.includes(client))
|
|
239
|
+
throw new CliError(`--client must be one of ${CLIENTS.join(", ")}`);
|
|
240
|
+
let payload;
|
|
241
|
+
try {
|
|
242
|
+
const raw = JSON.parse(await readAll(input, MAX_PAYLOAD_CHARS));
|
|
243
|
+
if (client === "cursor")
|
|
244
|
+
payload = cursorPayload(raw);
|
|
245
|
+
else {
|
|
246
|
+
const parsed = payloadSchema.safeParse(raw);
|
|
247
|
+
if (parsed.success)
|
|
248
|
+
payload = parsed.data;
|
|
249
|
+
}
|
|
250
|
+
if (!payload)
|
|
251
|
+
c.io.stderr(`invariant hook: unrecognized payload; staying silent\n`);
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
// Fail open in every way: the write proceeds and the reason is visible on stderr.
|
|
255
|
+
c.io.stderr(`invariant hook: ${error.message}\n`);
|
|
256
|
+
}
|
|
257
|
+
if (!payload)
|
|
258
|
+
return 0;
|
|
259
|
+
try {
|
|
260
|
+
const decision = await decide(c, payload);
|
|
261
|
+
if (decision)
|
|
262
|
+
c.io.stdout(`${JSON.stringify(client === "cursor" ? cursorAnswer(decision) : decision)}\n`);
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
c.io.stderr(`invariant hook: ${error.message}\n`);
|
|
266
|
+
}
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
//# sourceMappingURL=commands-hook.js.map
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { archetypeView, DesignSystem, patternRelationships, patternsView, patternView, search } from "../contract-query/index.js";
|
|
2
|
+
import { json, outputDir, print } from "./commands.js";
|
|
3
|
+
import { CliError } from "./errors.js";
|
|
4
|
+
import { shortValue, table } from "./format.js";
|
|
5
|
+
import { loadCompiled } from "./store.js";
|
|
6
|
+
import { CLI_NAME } from "./version.js";
|
|
7
|
+
const CATEGORIES = [
|
|
8
|
+
"layout",
|
|
9
|
+
"navigation",
|
|
10
|
+
"form",
|
|
11
|
+
"data-table",
|
|
12
|
+
"card",
|
|
13
|
+
"dialog",
|
|
14
|
+
"workflow",
|
|
15
|
+
"composition",
|
|
16
|
+
"interaction",
|
|
17
|
+
"page",
|
|
18
|
+
"other"
|
|
19
|
+
];
|
|
20
|
+
const STATUSES = ["approved", "documented", "discovered", "inferred", "mixed", "none"];
|
|
21
|
+
const DIRECTIONS = ["out", "in", "both"];
|
|
22
|
+
const USAGE = `usage: ${CLI_NAME} patterns <list|inspect|resolve|search|relationships> ...
|
|
23
|
+
patterns list [--category c] [--status s]
|
|
24
|
+
patterns inspect <name|id> [--evidence]
|
|
25
|
+
patterns resolve <name|id> [--variation name]
|
|
26
|
+
patterns search <query>
|
|
27
|
+
patterns relationships <name|id> [--direction out|in|both] [--depth 1-5]`;
|
|
28
|
+
const rows = (value) => (Array.isArray(value) ? value : []);
|
|
29
|
+
const text = (value) => value === undefined || value === null ? "" : typeof value === "string" ? value : String(value);
|
|
30
|
+
function choice(name, value, allowed) {
|
|
31
|
+
if (value === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (!allowed.includes(value))
|
|
34
|
+
throw new CliError(`--${name} must be one of: ${allowed.join(", ")}`);
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
function resolve(ds, query) {
|
|
38
|
+
if (!query)
|
|
39
|
+
throw new CliError(USAGE);
|
|
40
|
+
const found = ds.findPattern(query);
|
|
41
|
+
if (found.match)
|
|
42
|
+
return found.match;
|
|
43
|
+
const names = found.candidates.map((p) => p.id).join(", ");
|
|
44
|
+
throw new CliError(found.candidates.length > 1
|
|
45
|
+
? `"${query}" matches several patterns: ${names}. Use the id.`
|
|
46
|
+
: `no pattern matches "${query}"${names ? `. Did you mean: ${names}?` : ""}`);
|
|
47
|
+
}
|
|
48
|
+
function statusText(status) {
|
|
49
|
+
const counts = Object.entries((status.counts ?? {}))
|
|
50
|
+
.map(([k, v]) => `${k} ${text(v)}`)
|
|
51
|
+
.join(", ");
|
|
52
|
+
return `${text(status.overall)}${counts ? ` (${counts})` : ""}`;
|
|
53
|
+
}
|
|
54
|
+
function inspect(c, ds, query) {
|
|
55
|
+
const pattern = resolve(ds, query);
|
|
56
|
+
const view = patternView(ds, pattern);
|
|
57
|
+
if (c.args.values.json) {
|
|
58
|
+
json(c, view);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const status = view.status;
|
|
62
|
+
print(c, `${text(view.name)} (${text(view.id)})`);
|
|
63
|
+
print(c, `category: ${text(view.category)} status: ${statusText(status)}${view.approved ? "" : " NOT APPROVED"}`);
|
|
64
|
+
if (view.intent)
|
|
65
|
+
print(c, `intent: ${text(view.intent)}`);
|
|
66
|
+
print(c, text(view.description));
|
|
67
|
+
const scope = view.scope;
|
|
68
|
+
if (scope) {
|
|
69
|
+
const paths = scope.paths?.join(", ") ?? "any file";
|
|
70
|
+
const inherits = scope.inheritsConstraints
|
|
71
|
+
? scope.inheritanceActive
|
|
72
|
+
? "inherits constraints of what it composes"
|
|
73
|
+
: "opts in to inheriting constraints (not approved, so off)"
|
|
74
|
+
: "does not inherit constraints";
|
|
75
|
+
print(c, `applies to: ${paths} ${inherits}`);
|
|
76
|
+
}
|
|
77
|
+
const parts = rows(view.composition?.parts ?? []);
|
|
78
|
+
if (parts.length > 0) {
|
|
79
|
+
print(c, "\ncomposition");
|
|
80
|
+
print(c, table(["part", "role", "ref", "region", "status"], parts.map((p) => [
|
|
81
|
+
text(p.name),
|
|
82
|
+
text(p.role),
|
|
83
|
+
text(p.ref.name ?? p.ref.id),
|
|
84
|
+
text(p.region),
|
|
85
|
+
text(p.fact?.status)
|
|
86
|
+
])));
|
|
87
|
+
}
|
|
88
|
+
const regions = rows(view.layout?.regions ?? []);
|
|
89
|
+
if (regions.length > 0) {
|
|
90
|
+
print(c, "\nlayout regions");
|
|
91
|
+
print(c, table(["region", "required", "parent", "accepts"], regions.map((r) => [text(r.name), r.required ? "yes" : "", text(r.parent), shortValue(r.accepts, 40)])));
|
|
92
|
+
}
|
|
93
|
+
const behavior = view.behavior;
|
|
94
|
+
if (behavior) {
|
|
95
|
+
print(c, `\nbehavior (initial state: ${text(behavior.initial)})`);
|
|
96
|
+
print(c, table(["transition", "from", "to", "trigger", "verifiable by"], rows(behavior.transitions).map((t) => [
|
|
97
|
+
text(t.id),
|
|
98
|
+
text(t.from),
|
|
99
|
+
text(t.to),
|
|
100
|
+
text(t.trigger),
|
|
101
|
+
text(t.verifiableBy)
|
|
102
|
+
])));
|
|
103
|
+
const feedback = rows(behavior.states).filter((s) => s.feedback);
|
|
104
|
+
if (feedback.length > 0)
|
|
105
|
+
print(c, `required feedback: ${feedback.map((s) => `${text(s.name)}: ${text(s.feedback)}`).join("; ")}`);
|
|
106
|
+
}
|
|
107
|
+
const reqs = rows(view.accessibility?.requirements ?? []);
|
|
108
|
+
if (reqs.length > 0) {
|
|
109
|
+
print(c, "\naccessibility");
|
|
110
|
+
for (const r of reqs)
|
|
111
|
+
print(c, ` ${text(r.id)}: ${text(r.statement)}${r.verifiableBy ? ` [${text(r.verifiableBy)}]` : ""}`);
|
|
112
|
+
}
|
|
113
|
+
const constraints = rows(view.constraints);
|
|
114
|
+
if (constraints.length > 0) {
|
|
115
|
+
print(c, "\nconstraints");
|
|
116
|
+
print(c, table(["id", "kind", "details"], constraints.map((k) => [text(k.id), text(k.kind), shortValue(k.details, 60)])));
|
|
117
|
+
}
|
|
118
|
+
const relationships = rows(view.relationships);
|
|
119
|
+
if (relationships.length > 0) {
|
|
120
|
+
print(c, "\nrelationships");
|
|
121
|
+
print(c, table(["id", "type", "target", "status"], relationships.map((r) => [
|
|
122
|
+
text(r.id),
|
|
123
|
+
text(r.type),
|
|
124
|
+
text(r.target.name ?? r.target.id),
|
|
125
|
+
text(r.fact?.status)
|
|
126
|
+
])));
|
|
127
|
+
}
|
|
128
|
+
const enforced = rows(view.enforcedBy);
|
|
129
|
+
print(c, enforced.length === 0
|
|
130
|
+
? "\nenforced by: no approved policy"
|
|
131
|
+
: `\nenforced by: ${enforced.map((e) => `${text(e.policy)} (${text(e.enforcement)})`).join(", ")}`);
|
|
132
|
+
if (c.args.values.evidence) {
|
|
133
|
+
print(c, "\nevidence");
|
|
134
|
+
for (const f of rows(view.facts)) {
|
|
135
|
+
const where = rows(f.evidence)
|
|
136
|
+
.map((e) => `${text(e.location)} [${text(e.method)}]`)
|
|
137
|
+
.join("; ");
|
|
138
|
+
print(c, ` ${text(f.path)} ${text(f.status)}${f.confidence !== undefined ? ` ${text(f.confidence)}` : ""} ${where}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const note of view.notes ?? [])
|
|
142
|
+
print(c, `note: ${note}`);
|
|
143
|
+
}
|
|
144
|
+
function tree(parts, related, indent, lines) {
|
|
145
|
+
for (const p of parts) {
|
|
146
|
+
const ref = p.ref;
|
|
147
|
+
lines.push(`${indent}${text(p.name)} ${text(p.role)}${p.region ? `, region ${text(p.region)}` : ""} ${text(ref.name ?? ref.id)}${ref.kind === "pattern" ? " (pattern)" : ""}`);
|
|
148
|
+
nest(p.expanded, indent, lines);
|
|
149
|
+
}
|
|
150
|
+
for (const r of related) {
|
|
151
|
+
const target = r.target;
|
|
152
|
+
lines.push(`${indent}${text(r.type)} ${text(target.name ?? target.id)}`);
|
|
153
|
+
nest(r.expanded, indent, lines);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function nest(expanded, indent, lines) {
|
|
157
|
+
if (!expanded)
|
|
158
|
+
return;
|
|
159
|
+
if (expanded.cycle) {
|
|
160
|
+
lines.push(`${indent} (already shown above)`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (expanded.truncated) {
|
|
164
|
+
lines.push(`${indent} (not followed further)`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const p = expanded.pattern;
|
|
168
|
+
lines.push(`${indent} ${text(p.name)} [${text(p.status)}${p.approved ? "" : " *"}]`);
|
|
169
|
+
tree(rows(expanded.parts), rows(expanded.related), `${indent} `, lines);
|
|
170
|
+
}
|
|
171
|
+
function resolveView(c, ds, query) {
|
|
172
|
+
const pattern = resolve(ds, query);
|
|
173
|
+
const result = archetypeView(ds, pattern, { variation: c.args.values.variation });
|
|
174
|
+
if (!result.ok)
|
|
175
|
+
throw new CliError(result.message);
|
|
176
|
+
const view = result.view;
|
|
177
|
+
if (c.args.values.json) {
|
|
178
|
+
json(c, view);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const status = view.status;
|
|
182
|
+
print(c, `${text(view.name)} (${text(view.id)})${view.variation ? ` variation: ${text(view.variation)}` : ""}`);
|
|
183
|
+
print(c, `category: ${text(view.category)} status: ${statusText(status)}${view.approved ? "" : " NOT APPROVED"}`);
|
|
184
|
+
if (view.intent)
|
|
185
|
+
print(c, `intent: ${text(view.intent)}`);
|
|
186
|
+
const regions = rows(view.regions);
|
|
187
|
+
if (regions.length > 0) {
|
|
188
|
+
print(c, "\nregions");
|
|
189
|
+
print(c, table(["region", "required", "parts"], regions.map((r) => [
|
|
190
|
+
text(r.name),
|
|
191
|
+
r.required ? "yes" : "",
|
|
192
|
+
(r.parts ?? []).join(", ")
|
|
193
|
+
])));
|
|
194
|
+
}
|
|
195
|
+
const lines = [];
|
|
196
|
+
const root = view.composition;
|
|
197
|
+
tree(rows(root.parts), rows(root.related), " ", lines);
|
|
198
|
+
print(c, `\ncomposition (* not approved)\n${lines.join("\n")}`);
|
|
199
|
+
const variations = rows(view.variations);
|
|
200
|
+
if (variations.length > 0) {
|
|
201
|
+
print(c, "\nvariations");
|
|
202
|
+
print(c, table(["name", "when", "changes", "adds"], variations.map((v) => [
|
|
203
|
+
text(v.name),
|
|
204
|
+
shortValue(v.when, 40),
|
|
205
|
+
rows(v.roles)
|
|
206
|
+
.map((x) => `${text(x.part)}: ${text(x.role)}`)
|
|
207
|
+
.join(", "),
|
|
208
|
+
rows(v.addedParts)
|
|
209
|
+
.map((x) => text(x.name))
|
|
210
|
+
.join(", ")
|
|
211
|
+
])));
|
|
212
|
+
}
|
|
213
|
+
const states = rows(view.requiredStates);
|
|
214
|
+
if (states.length > 0) {
|
|
215
|
+
print(c, "\nrequired interaction states");
|
|
216
|
+
print(c, table(["pattern", "workflow", "state", "minimum", "enforced"], states.map((s) => [
|
|
217
|
+
text(s.pattern),
|
|
218
|
+
text(s.workflow),
|
|
219
|
+
text(s.state),
|
|
220
|
+
text(s.minimum),
|
|
221
|
+
s.enforced ? "yes" : "no"
|
|
222
|
+
])));
|
|
223
|
+
}
|
|
224
|
+
const constraints = rows(view.constraints);
|
|
225
|
+
if (constraints.length > 0) {
|
|
226
|
+
print(c, "\nconstraints");
|
|
227
|
+
print(c, table(["pattern", "kind", "enforced"], constraints.map((k) => [text(k.pattern), text(k.kind), k.enforced ? "yes" : "no"])));
|
|
228
|
+
}
|
|
229
|
+
const responsive = rows(view.responsive);
|
|
230
|
+
if (responsive.length > 0) {
|
|
231
|
+
print(c, "\nresponsive");
|
|
232
|
+
for (const x of responsive)
|
|
233
|
+
print(c, ` ${text(x.region)}: ${text(x.behavior)} at ${text(x.breakpoint)}`);
|
|
234
|
+
}
|
|
235
|
+
const prohibited = view.prohibited;
|
|
236
|
+
const parts = rows(prohibited.parts);
|
|
237
|
+
if (parts.length > 0)
|
|
238
|
+
print(c, `\nprohibited parts: ${parts.map((p) => text(p.name)).join(", ")}`);
|
|
239
|
+
const pairs = rows(prohibited.patterns);
|
|
240
|
+
if (pairs.length > 0)
|
|
241
|
+
print(c, `\nprohibited together: ${pairs.map((p) => `${text(p.pattern)} with ${text(p.with)}${p.composedHere ? " (BOTH COMPOSED HERE)" : ""}`).join("; ")}`);
|
|
242
|
+
for (const note of view.notes ?? [])
|
|
243
|
+
print(c, `note: ${note}`);
|
|
244
|
+
}
|
|
245
|
+
function relationships(c, ds, query) {
|
|
246
|
+
const pattern = resolve(ds, query);
|
|
247
|
+
const direction = choice("direction", c.args.values.direction, DIRECTIONS);
|
|
248
|
+
const rawDepth = c.args.values.depth;
|
|
249
|
+
const depth = rawDepth === undefined ? undefined : Number(rawDepth);
|
|
250
|
+
if (depth !== undefined && (!Number.isInteger(depth) || depth < 1 || depth > 5))
|
|
251
|
+
throw new CliError("--depth must be a whole number from 1 to 5");
|
|
252
|
+
const view = patternRelationships(ds, pattern, { direction, depth });
|
|
253
|
+
if (c.args.values.json) {
|
|
254
|
+
json(c, view);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const names = new Map(rows(view.nodes).map((n) => [text(n.id), text(n.name) || text(n.id)]));
|
|
258
|
+
const edges = rows(view.edges);
|
|
259
|
+
print(c, `${pattern.name} (${pattern.id}) direction: ${text(view.direction)}, depth: ${text(view.depth)}`);
|
|
260
|
+
if (edges.length === 0) {
|
|
261
|
+
print(c, "no relationships in this direction");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
print(c, table(["from", "type", "to", "status"], edges.map((e) => [
|
|
265
|
+
names.get(text(e.from)) ?? text(e.from),
|
|
266
|
+
`${text(e.type)}${e.role ? ` (${text(e.role)})` : ""}`,
|
|
267
|
+
names.get(text(e.to)) ?? text(e.to),
|
|
268
|
+
text(e.status)
|
|
269
|
+
])));
|
|
270
|
+
if (view.truncated)
|
|
271
|
+
print(c, "note: the walk was cut off at 200 nodes");
|
|
272
|
+
}
|
|
273
|
+
export async function patterns(c) {
|
|
274
|
+
const [, sub, ...rest] = c.args.positionals;
|
|
275
|
+
if (!sub || !["list", "inspect", "resolve", "search", "relationships"].includes(sub))
|
|
276
|
+
throw new CliError(USAGE);
|
|
277
|
+
const { contract, evidence } = loadCompiled(await outputDir(c));
|
|
278
|
+
const ds = new DesignSystem(contract, evidence);
|
|
279
|
+
if (sub === "inspect")
|
|
280
|
+
inspect(c, ds, rest[0]);
|
|
281
|
+
else if (sub === "resolve")
|
|
282
|
+
resolveView(c, ds, rest[0]);
|
|
283
|
+
else if (sub === "relationships")
|
|
284
|
+
relationships(c, ds, rest[0]);
|
|
285
|
+
else if (sub === "search") {
|
|
286
|
+
const query = rest.join(" ").trim();
|
|
287
|
+
if (!query)
|
|
288
|
+
throw new CliError(`usage: ${CLI_NAME} patterns search <query>`);
|
|
289
|
+
const hits = search(ds, query, ["pattern"], 20).hits;
|
|
290
|
+
if (c.args.values.json)
|
|
291
|
+
json(c, hits);
|
|
292
|
+
else
|
|
293
|
+
print(c, hits.length === 0
|
|
294
|
+
? `no pattern matches "${query}"`
|
|
295
|
+
: table(["id", "name", "score", "matched on"], hits.map((h) => [h.id, h.name, h.score, h.matchedOn.join(", ")])));
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const category = choice("category", c.args.values.category, CATEGORIES);
|
|
299
|
+
const status = choice("status", c.args.values.status, STATUSES);
|
|
300
|
+
const view = patternsView(ds, { category, status, limit: 200 });
|
|
301
|
+
if (c.args.values.json) {
|
|
302
|
+
// Unchanged shape: the matching patterns as written in the contract.
|
|
303
|
+
const ids = new Set(rows(view.patterns).map((p) => text(p.id)));
|
|
304
|
+
json(c, contract.patterns.filter((p) => ids.has(p.id)));
|
|
305
|
+
}
|
|
306
|
+
else if (rows(view.patterns).length === 0)
|
|
307
|
+
print(c, contract.patterns.length === 0
|
|
308
|
+
? "no patterns. Invariant does not infer patterns from code yet, and no source documents one."
|
|
309
|
+
: `no pattern matches these filters (${contract.patterns.length} in the contract)`);
|
|
310
|
+
else {
|
|
311
|
+
print(c, table(["id", "name", "category", "status", "components"], rows(view.patterns).map((p) => [
|
|
312
|
+
text(p.id),
|
|
313
|
+
text(p.name),
|
|
314
|
+
text(p.category),
|
|
315
|
+
`${text(p.status.overall)}${p.approved ? "" : " *"}`,
|
|
316
|
+
p.components.length
|
|
317
|
+
])));
|
|
318
|
+
if (rows(view.patterns).some((p) => !p.approved))
|
|
319
|
+
print(c, "\n* not approved: treat as a proposal");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=commands-patterns.js.map
|