@oprt/cli 0.1.0 → 0.3.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/dist/index.cjs CHANGED
@@ -22,26 +22,38 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  CliError: () => CliError,
24
24
  DEFAULT_OUT: () => DEFAULT_OUT,
25
+ RESULT_SCHEMAS: () => RESULT_SCHEMAS,
26
+ RESULT_SCHEMA_VERSION: () => RESULT_SCHEMA_VERSION,
27
+ RESULT_TYPES: () => RESULT_TYPES,
25
28
  TOOLS: () => TOOLS,
26
29
  VERSION: () => VERSION,
27
30
  applyBlock: () => applyBlock,
28
31
  blockVersion: () => blockVersion,
32
+ buildCheckReport: () => buildCheckReport,
33
+ checkExitCode: () => checkExitCode,
29
34
  checkPaths: () => checkPaths,
30
35
  checkSource: () => checkSource,
31
36
  componentDetail: () => componentDetail,
32
37
  componentIndex: () => componentIndex,
33
38
  contract: () => contract,
39
+ contractDigest: () => contractDigest,
34
40
  doctor: () => doctor,
41
+ envelope: () => envelope,
42
+ errorEnvelope: () => errorEnvelope,
35
43
  handleMessage: () => handleMessage,
44
+ identityOf: () => identityOf,
36
45
  init: () => init,
37
46
  install: () => install,
47
+ outputSchemaFor: () => outputSchemaFor,
38
48
  parseArgs: () => parseArgs,
39
49
  registry: () => registry,
40
50
  removeBlock: () => removeBlock,
41
51
  renderAgentBlock: () => renderAgentBlock,
52
+ renderCheckText: () => renderCheckText,
42
53
  renderComponent: () => renderComponent,
43
54
  resolveEntries: () => resolveEntries,
44
55
  runAgentCommand: () => runAgentCommand,
56
+ schemaFor: () => schemaFor,
45
57
  search: () => search,
46
58
  serve: () => serve
47
59
  });
@@ -53,78 +65,9 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
53
65
 
54
66
  // src/index.ts
55
67
  var import_node_fs6 = require("fs");
56
- var import_node_path6 = require("path");
68
+ var import_node_path7 = require("path");
57
69
  var import_node_url3 = require("url");
58
70
 
59
- // package.json
60
- var package_default = {
61
- name: "@oprt/cli",
62
- version: "0.1.0",
63
- description: "The OPERATOR command line: copy components and recipes into your app as source, give coding agents the contract, and check their code against it. Includes an MCP server.",
64
- keywords: [
65
- "design-system",
66
- "cli",
67
- "coding-agents",
68
- "mcp",
69
- "model-context-protocol",
70
- "code-check",
71
- "components"
72
- ],
73
- homepage: "https://oprt.dev/docs/agents/cli",
74
- type: "module",
75
- main: "./dist/index.cjs",
76
- module: "./dist/index.js",
77
- types: "./dist/index.d.ts",
78
- bin: {
79
- oprt: "./dist/bin.js"
80
- },
81
- engines: {
82
- node: ">=22"
83
- },
84
- exports: {
85
- ".": {
86
- import: {
87
- types: "./dist/index.d.ts",
88
- default: "./dist/index.js"
89
- },
90
- require: {
91
- types: "./dist/index.d.cts",
92
- default: "./dist/index.cjs"
93
- }
94
- },
95
- "./read": {
96
- import: {
97
- types: "./dist/read.d.ts",
98
- default: "./dist/read.js"
99
- },
100
- require: {
101
- types: "./dist/read.d.cts",
102
- default: "./dist/read.cjs"
103
- }
104
- }
105
- },
106
- files: [
107
- "dist"
108
- ],
109
- scripts: {
110
- clean: `node -e "require('node:fs').rmSync('dist',{recursive:true,force:true})"`,
111
- build: "pnpm run clean && node scripts/build-registry.mjs && tsup",
112
- dev: "pnpm run clean && node scripts/build-registry.mjs && tsup --watch",
113
- test: "vitest run",
114
- typecheck: "tsc --noEmit",
115
- lint: "eslint src scripts"
116
- },
117
- devDependencies: {
118
- tsup: "^8.3.0",
119
- typescript: "^5.6.3",
120
- vitest: "^2.1.3"
121
- },
122
- license: "MIT",
123
- publishConfig: {
124
- access: "public"
125
- }
126
- };
127
-
128
71
  // src/contract.ts
129
72
  var import_node_fs = require("fs");
130
73
  var import_node_path = require("path");
@@ -159,6 +102,197 @@ var CliError = class extends Error {
159
102
  suggestions;
160
103
  };
161
104
 
105
+ // src/lookup.ts
106
+ var SEARCH_LIMIT_MAX = 50;
107
+ var SEARCH_LIMIT_DEFAULT = 10;
108
+ var squash = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
109
+ var firstSentence = (text2) => /^.*?[.!?](?=\s|$)/.exec(text2)?.[0] ?? text2;
110
+ function distance(a, b) {
111
+ const row = Array.from({ length: b.length + 1 }, (_, index) => index);
112
+ for (let i = 1; i <= a.length; i++) {
113
+ let diagonal = row[0];
114
+ row[0] = i;
115
+ for (let j = 1; j <= b.length; j++) {
116
+ const above = row[j];
117
+ row[j] = Math.min(above + 1, row[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
118
+ diagonal = above;
119
+ }
120
+ }
121
+ return row[b.length];
122
+ }
123
+ function nearest(wanted, names2, limit = 5) {
124
+ const target = squash(wanted);
125
+ return names2.map((name) => {
126
+ const candidate = squash(name);
127
+ const apart = distance(target, candidate);
128
+ const close = apart <= 2 || target.length >= 3 && (candidate.includes(target) || target.includes(candidate));
129
+ return { name, apart, close };
130
+ }).filter(({ close }) => close).sort((a, b) => a.apart - b.apart || a.name.localeCompare(b.name)).slice(0, limit).map(({ name }) => name);
131
+ }
132
+ function resolveComponent(contract2, wanted) {
133
+ const names2 = Object.keys(contract2.components);
134
+ const found = names2.find((name) => name === wanted) ?? names2.find((name) => squash(name) === squash(wanted));
135
+ if (found !== void 0) return found;
136
+ const suggestions = nearest(wanted, names2);
137
+ throw new CliError(
138
+ `Unknown component ${JSON.stringify(wanted)}.` + (suggestions.length > 0 ? ` Did you mean ${suggestions.join(", ")}?` : "") + " Run oprt component --list.",
139
+ 2,
140
+ { code: "ERR_UNKNOWN_COMPONENT", suggestions }
141
+ );
142
+ }
143
+ function allowedChildren(rules, name) {
144
+ for (const [parent, rule] of Object.entries(rules ?? {})) {
145
+ if (parent === name) return rule.allowed_children;
146
+ const nested = allowedChildren(rule.children, name);
147
+ if (nested !== void 0) return nested;
148
+ }
149
+ return void 0;
150
+ }
151
+ var partsOf = (contract2, name) => Object.keys(contract2.components).filter(
152
+ (part) => contract2.guidance.components[part]?.part_of === name
153
+ );
154
+ function componentIndex(contract2) {
155
+ return Object.keys(contract2.components).filter((name) => (contract2.guidance.components[name]?.use_for ?? []).length > 0).sort((a, b) => a.localeCompare(b)).map((name) => ({
156
+ name,
157
+ purpose: contract2.guidance.components[name].use_for.join("; "),
158
+ parts: partsOf(contract2, name)
159
+ }));
160
+ }
161
+ function componentDetail(contract2, wanted) {
162
+ const name = resolveComponent(contract2, wanted);
163
+ const component = contract2.components[name];
164
+ const guide = contract2.guidance.components[name] ?? {};
165
+ const props = Object.entries(component.props).map(([prop, definition]) => {
166
+ const notes = guide.values?.[prop];
167
+ return { name: prop, ...definition, ...notes === void 0 ? {} : { notes } };
168
+ });
169
+ const allowed = allowedChildren(contract2.composition_rules, name);
170
+ return {
171
+ name,
172
+ description: component.description,
173
+ use_for: guide.use_for ?? [],
174
+ not_for: guide.not_for ?? [],
175
+ ...guide.part_of === void 0 ? {} : { part_of: guide.part_of },
176
+ parts: partsOf(contract2, name),
177
+ props,
178
+ ...component.slots === void 0 ? {} : { slots: component.slots },
179
+ composable_with: component.composable_with ?? [],
180
+ never_inside: component.never_inside ?? [],
181
+ ...allowed === void 0 ? {} : { allowed_children: allowed },
182
+ example: component.example
183
+ };
184
+ }
185
+ function propLine(prop, dense) {
186
+ const kind = prop.type === "enum" ? (prop.values ?? []).map((value) => value === prop.default ? `${value} (default)` : value).join(" | ") : (prop.type === "union" ? (prop.types ?? []).join(" or ") : prop.type) + (prop.default === void 0 ? "" : ` (default ${String(prop.default)})`);
187
+ const description = prop.description === void 0 ? "" : dense ? firstSentence(prop.description) : prop.description;
188
+ const lines = [
189
+ ` ${prop.name}: ${kind}${prop.required ? ", required" : ""}${description ? `. ${description}` : ""}`
190
+ ];
191
+ for (const [value, note] of Object.entries(prop.notes ?? {})) lines.push(` ${value}: ${note}`);
192
+ return lines;
193
+ }
194
+ function renderComponent(detail, { dense }) {
195
+ const lines = [
196
+ detail.part_of === void 0 ? `${detail.name}: ${detail.use_for.join("; ")}` : `${detail.name}: a part of ${detail.part_of}. ${firstSentence(detail.description)}`
197
+ ];
198
+ if (detail.not_for.length > 0)
199
+ lines.push(
200
+ `Not for: ${detail.not_for.map(({ need, use }) => `${need} (use ${use})`).join("; ")}`
201
+ );
202
+ if (!dense) lines.push("", detail.description, "");
203
+ if (detail.props.length > 0) {
204
+ lines.push("Props:");
205
+ for (const prop of detail.props) lines.push(...propLine(prop, dense));
206
+ }
207
+ for (const [slot, { type, required }] of Object.entries(detail.slots ?? {}))
208
+ lines.push(
209
+ `${slot === "children" ? "Children" : `Slot ${slot}`}: ${type}${required ? ", required" : ""}`
210
+ );
211
+ if (detail.parts.length > 0) lines.push(`Parts: ${detail.parts.join(", ")}`);
212
+ if (detail.allowed_children !== void 0)
213
+ lines.push(`Takes directly inside: ${detail.allowed_children.join(", ")}`);
214
+ if (detail.composable_with.length > 0)
215
+ lines.push(`Goes inside: ${detail.composable_with.join(", ")}`);
216
+ if (detail.never_inside.length > 0) lines.push(`Never inside: ${detail.never_inside.join(", ")}`);
217
+ lines.push(
218
+ detail.example.includes("\n") ? `Example:
219
+ ${detail.example.split("\n").map((line) => ` ${line}`).join("\n")}` : `Example: ${detail.example}`
220
+ );
221
+ return `${lines.join("\n")}
222
+ `;
223
+ }
224
+ var STOP = new Set(
225
+ "a an the to of for in on at with and or i we need want that which is are it its my some from by".split(
226
+ " "
227
+ )
228
+ );
229
+ var words = (text2) => (text2.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter((word) => !STOP.has(word)).map((word) => word.length > 4 ? word.replace(/(?:ing|ed|es|s)$/, "") : word);
230
+ var matched = (query, text2) => {
231
+ const present = new Set(words(text2));
232
+ return query.filter((word) => present.has(word)).length;
233
+ };
234
+ function search(contract2, registry2, query, limit = SEARCH_LIMIT_DEFAULT) {
235
+ const wanted = words(query);
236
+ if (wanted.length === 0) return [];
237
+ const whole = squash(query);
238
+ const scores = /* @__PURE__ */ new Map();
239
+ const add = (name, points, why) => {
240
+ const before = scores.get(name) ?? { score: 0 };
241
+ scores.set(name, {
242
+ score: before.score + points,
243
+ ...before.why !== void 0 ? { why: before.why } : why !== void 0 ? { why } : {}
244
+ });
245
+ };
246
+ for (const [name, component] of Object.entries(contract2.components)) {
247
+ const guide = contract2.guidance.components[name] ?? {};
248
+ const plain = squash(name);
249
+ if (plain === whole) add(name, 100);
250
+ else if (wanted.some((word) => word.length >= 3 && plain.includes(word))) add(name, 15);
251
+ else if (wanted.some((word) => word.length >= 4 && distance(word, plain) <= 1)) add(name, 12);
252
+ for (const phrase of guide.use_for ?? []) {
253
+ const hits = matched(wanted, phrase);
254
+ if (hits > 0) add(name, 12 * hits + (hits === wanted.length ? 40 : 0));
255
+ }
256
+ for (const { need, use } of guide.not_for ?? []) {
257
+ const hits = matched(wanted, need);
258
+ if (hits > 0 && hits >= Math.ceil(wanted.length * 0.6))
259
+ add(use, 10 * hits + (hits === wanted.length ? 15 : 0), `${name} is not for this: ${need}`);
260
+ }
261
+ const inDescription = matched(wanted, component.description);
262
+ if (inDescription > 0) add(name, 2 * inDescription);
263
+ }
264
+ const results = [];
265
+ for (const [name, { score, why }] of scores) {
266
+ const guide = contract2.guidance.components[name] ?? {};
267
+ results.push({
268
+ kind: "component",
269
+ name,
270
+ purpose: guide.part_of === void 0 ? (guide.use_for ?? []).join("; ") : `a part of ${guide.part_of}`,
271
+ score,
272
+ ...why === void 0 ? {} : { why },
273
+ next: `oprt component ${name}`
274
+ });
275
+ }
276
+ for (const entry of registry2.entries.filter(({ kind }) => kind === "recipe")) {
277
+ const recipe = contract2.patterns?.[entry.name];
278
+ const plain = squash(entry.name);
279
+ const inName = plain === whole ? 100 : wanted.some((word) => word.length >= 3 && plain.includes(word)) ? 15 : 0;
280
+ const hits = matched(wanted, entry.description);
281
+ const score = inName + (hits > 0 ? 10 * hits + (hits === wanted.length ? 15 : 0) : 0);
282
+ if (score > 0)
283
+ results.push({
284
+ kind: "recipe",
285
+ name: entry.name,
286
+ purpose: firstSentence(entry.description),
287
+ score,
288
+ next: recipe?.install ?? `oprt add ${entry.slug}`
289
+ });
290
+ }
291
+ results.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
292
+ const floor = Math.max(4, (results[0]?.score ?? 0) * 0.15);
293
+ return results.filter(({ score }) => score >= floor).slice(0, limit);
294
+ }
295
+
162
296
  // src/args.ts
163
297
  var DEFAULT_OUT = "src/components/operator";
164
298
  function split(command, rest, spec) {
@@ -214,9 +348,10 @@ var AGENT_COMMANDS = {
214
348
  "--json": "flag"
215
349
  });
216
350
  if (positional.length === 0) throw new CliError("Usage: oprt search <need> [--limit <n>]", 2);
217
- const limit = value("--limit") === void 0 ? 10 : Number(value("--limit"));
218
- if (!Number.isInteger(limit) || limit < 1)
219
- throw new CliError("--limit takes a whole number above zero.", 2);
351
+ const given = value("--limit");
352
+ const limit = given === void 0 ? SEARCH_LIMIT_DEFAULT : Number(given);
353
+ if (!Number.isInteger(limit) || limit < 1 || limit > SEARCH_LIMIT_MAX)
354
+ throw new CliError(`--limit takes a whole number from 1 to ${SEARCH_LIMIT_MAX}.`, 2);
220
355
  return { command: "search", query: positional.join(" "), limit, json: has("--json") };
221
356
  },
222
357
  init(rest) {
@@ -279,7 +414,8 @@ function parseArgs(args) {
279
414
  names: [],
280
415
  out: DEFAULT_OUT,
281
416
  dryRun: false,
282
- overwrite: false
417
+ overwrite: false,
418
+ json: false
283
419
  };
284
420
  const seen = /* @__PURE__ */ new Set();
285
421
  for (let i = 0; i < rest.length; i++) {
@@ -288,7 +424,7 @@ function parseArgs(args) {
288
424
  parsed.names.push(token);
289
425
  continue;
290
426
  }
291
- if (!["--out", "--dry-run", "--overwrite"].includes(token))
427
+ if (!["--out", "--dry-run", "--overwrite", "--json"].includes(token))
292
428
  throw new CliError(`Unknown option ${JSON.stringify(token)}. Run oprt --help.`, 2);
293
429
  if (seen.has(token)) throw new CliError(`Duplicate option ${token}.`, 2);
294
430
  seen.add(token);
@@ -298,6 +434,7 @@ function parseArgs(args) {
298
434
  throw new CliError("--out requires a relative directory.", 2);
299
435
  parsed.out = value;
300
436
  } else if (token === "--dry-run") parsed.dryRun = true;
437
+ else if (token === "--json") parsed.json = true;
301
438
  else parsed.overwrite = true;
302
439
  }
303
440
  if (!parsed.names.length)
@@ -306,10 +443,10 @@ function parseArgs(args) {
306
443
  }
307
444
 
308
445
  // src/installer.ts
309
- function resolveEntries(names, registry2) {
310
- if (!names.length) throw new CliError("add requires one or more names.", 2);
446
+ function resolveEntries(names2, registry2) {
447
+ if (!names2.length) throw new CliError("add requires one or more names.", 2);
311
448
  const selected = /* @__PURE__ */ new Map();
312
- for (const name of names) {
449
+ for (const name of names2) {
313
450
  const normalized = name.toLowerCase();
314
451
  const entry = registry2.entries.find(
315
452
  (candidate) => candidate.slug === normalized || candidate.name.toLowerCase() === normalized
@@ -417,202 +554,196 @@ function installSources(options, context) {
417
554
  return result;
418
555
  }
419
556
 
420
- // src/check.ts
421
- var import_node_fs3 = require("fs");
422
- var import_node_module = require("module");
423
- var import_node_path3 = require("path");
424
- var import_node_url2 = require("url");
557
+ // src/identity.ts
558
+ var import_node_crypto = require("crypto");
425
559
 
426
- // src/lookup.ts
427
- var squash = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
428
- var firstSentence = (text2) => /^.*?[.!?](?=\s|$)/.exec(text2)?.[0] ?? text2;
429
- function distance(a, b) {
430
- const row = Array.from({ length: b.length + 1 }, (_, index) => index);
431
- for (let i = 1; i <= a.length; i++) {
432
- let diagonal = row[0];
433
- row[0] = i;
434
- for (let j = 1; j <= b.length; j++) {
435
- const above = row[j];
436
- row[j] = Math.min(above + 1, row[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
437
- diagonal = above;
560
+ // package.json
561
+ var package_default = {
562
+ name: "@oprt/cli",
563
+ version: "0.3.0",
564
+ description: "The OPERATOR command line: copy components and recipes into your app as source, give coding agents the contract, and check their code against it. Includes an MCP server.",
565
+ keywords: [
566
+ "design-system",
567
+ "cli",
568
+ "coding-agents",
569
+ "mcp",
570
+ "model-context-protocol",
571
+ "code-check",
572
+ "components"
573
+ ],
574
+ homepage: "https://oprt.dev/docs/agents/cli",
575
+ type: "module",
576
+ main: "./dist/index.cjs",
577
+ module: "./dist/index.js",
578
+ types: "./dist/index.d.ts",
579
+ bin: {
580
+ oprt: "./dist/bin.js"
581
+ },
582
+ engines: {
583
+ node: ">=22"
584
+ },
585
+ exports: {
586
+ ".": {
587
+ import: {
588
+ types: "./dist/index.d.ts",
589
+ default: "./dist/index.js"
590
+ },
591
+ require: {
592
+ types: "./dist/index.d.cts",
593
+ default: "./dist/index.cjs"
594
+ }
595
+ },
596
+ "./read": {
597
+ import: {
598
+ types: "./dist/read.d.ts",
599
+ default: "./dist/read.js"
600
+ },
601
+ require: {
602
+ types: "./dist/read.d.cts",
603
+ default: "./dist/read.cjs"
604
+ }
438
605
  }
606
+ },
607
+ files: [
608
+ "dist"
609
+ ],
610
+ scripts: {
611
+ clean: `node -e "require('node:fs').rmSync('dist',{recursive:true,force:true})"`,
612
+ build: "pnpm run clean && node scripts/build-registry.mjs && tsup",
613
+ dev: "pnpm run clean && node scripts/build-registry.mjs && tsup --watch",
614
+ test: "vitest run",
615
+ typecheck: "tsc --noEmit",
616
+ lint: "eslint src scripts"
617
+ },
618
+ devDependencies: {
619
+ "@modelcontextprotocol/sdk": "1.32.1",
620
+ tsup: "^8.3.0",
621
+ typescript: "^5.6.3",
622
+ vitest: "^2.1.3"
623
+ },
624
+ license: "MIT",
625
+ publishConfig: {
626
+ access: "public"
439
627
  }
440
- return row[b.length];
441
- }
442
- function nearest(wanted, names, limit = 5) {
443
- const target = squash(wanted);
444
- return names.map((name) => {
445
- const candidate = squash(name);
446
- const apart = distance(target, candidate);
447
- const close = apart <= 2 || target.length >= 3 && (candidate.includes(target) || target.includes(candidate));
448
- return { name, apart, close };
449
- }).filter(({ close }) => close).sort((a, b) => a.apart - b.apart || a.name.localeCompare(b.name)).slice(0, limit).map(({ name }) => name);
450
- }
451
- function resolveComponent(contract2, wanted) {
452
- const names = Object.keys(contract2.components);
453
- const found = names.find((name) => name === wanted) ?? names.find((name) => squash(name) === squash(wanted));
454
- if (found !== void 0) return found;
455
- const suggestions = nearest(wanted, names);
456
- throw new CliError(
457
- `Unknown component ${JSON.stringify(wanted)}.` + (suggestions.length > 0 ? ` Did you mean ${suggestions.join(", ")}?` : "") + " Run oprt component --list.",
458
- 2,
459
- { code: "ERR_UNKNOWN_COMPONENT", suggestions }
460
- );
461
- }
462
- function allowedChildren(rules, name) {
463
- for (const [parent, rule] of Object.entries(rules ?? {})) {
464
- if (parent === name) return rule.allowed_children;
465
- const nested = allowedChildren(rule.children, name);
466
- if (nested !== void 0) return nested;
628
+ };
629
+
630
+ // src/identity.ts
631
+ var VERSION = package_default.version;
632
+ var RESULT_SCHEMA_VERSION = 1;
633
+ var digests = /* @__PURE__ */ new WeakMap();
634
+ function contractDigest(contract2) {
635
+ let digest = digests.get(contract2);
636
+ if (digest === void 0) {
637
+ digest = `sha256:${(0, import_node_crypto.createHash)("sha256").update(JSON.stringify(contract2)).digest("hex")}`;
638
+ digests.set(contract2, digest);
467
639
  }
468
- return void 0;
640
+ return digest;
469
641
  }
470
- var partsOf = (contract2, name) => Object.keys(contract2.components).filter(
471
- (part) => contract2.guidance.components[part]?.part_of === name
472
- );
473
- function componentIndex(contract2) {
474
- return Object.keys(contract2.components).filter((name) => (contract2.guidance.components[name]?.use_for ?? []).length > 0).sort((a, b) => a.localeCompare(b)).map((name) => ({
475
- name,
476
- purpose: contract2.guidance.components[name].use_for.join("; "),
477
- parts: partsOf(contract2, name)
478
- }));
479
- }
480
- function componentDetail(contract2, wanted) {
481
- const name = resolveComponent(contract2, wanted);
482
- const component = contract2.components[name];
483
- const guide = contract2.guidance.components[name] ?? {};
484
- const props = Object.entries(component.props).map(([prop, definition]) => {
485
- const notes = guide.values?.[prop];
486
- return { name: prop, ...definition, ...notes === void 0 ? {} : { notes } };
487
- });
488
- const allowed = allowedChildren(contract2.composition_rules, name);
642
+ function identityOf(contract2) {
489
643
  return {
490
- name,
491
- description: component.description,
492
- use_for: guide.use_for ?? [],
493
- not_for: guide.not_for ?? [],
494
- ...guide.part_of === void 0 ? {} : { part_of: guide.part_of },
495
- parts: partsOf(contract2, name),
496
- props,
497
- ...component.slots === void 0 ? {} : { slots: component.slots },
498
- composable_with: component.composable_with ?? [],
499
- never_inside: component.never_inside ?? [],
500
- ...allowed === void 0 ? {} : { allowed_children: allowed },
501
- example: component.example
644
+ cli_version: VERSION,
645
+ contract_version: contract2.version,
646
+ contract_digest: contractDigest(contract2),
647
+ schema_version: RESULT_SCHEMA_VERSION
502
648
  };
503
649
  }
504
- function propLine(prop, dense) {
505
- const kind = prop.type === "enum" ? (prop.values ?? []).map((value) => value === prop.default ? `${value} (default)` : value).join(" | ") : (prop.type === "union" ? (prop.types ?? []).join(" or ") : prop.type) + (prop.default === void 0 ? "" : ` (default ${String(prop.default)})`);
506
- const description = prop.description === void 0 ? "" : dense ? firstSentence(prop.description) : prop.description;
507
- const lines = [
508
- ` ${prop.name}: ${kind}${prop.required ? ", required" : ""}${description ? `. ${description}` : ""}`
509
- ];
510
- for (const [value, note] of Object.entries(prop.notes ?? {})) lines.push(` ${value}: ${note}`);
511
- return lines;
512
- }
513
- function renderComponent(detail, { dense }) {
514
- const lines = [
515
- detail.part_of === void 0 ? `${detail.name}: ${detail.use_for.join("; ")}` : `${detail.name}: a part of ${detail.part_of}. ${firstSentence(detail.description)}`
516
- ];
517
- if (detail.not_for.length > 0)
518
- lines.push(
519
- `Not for: ${detail.not_for.map(({ need, use }) => `${need} (use ${use})`).join("; ")}`
520
- );
521
- if (!dense) lines.push("", detail.description, "");
522
- if (detail.props.length > 0) {
523
- lines.push("Props:");
524
- for (const prop of detail.props) lines.push(...propLine(prop, dense));
525
- }
526
- for (const [slot, { type, required }] of Object.entries(detail.slots ?? {}))
527
- lines.push(
528
- `${slot === "children" ? "Children" : `Slot ${slot}`}: ${type}${required ? ", required" : ""}`
529
- );
530
- if (detail.parts.length > 0) lines.push(`Parts: ${detail.parts.join(", ")}`);
531
- if (detail.allowed_children !== void 0)
532
- lines.push(`Takes directly inside: ${detail.allowed_children.join(", ")}`);
533
- if (detail.composable_with.length > 0)
534
- lines.push(`Goes inside: ${detail.composable_with.join(", ")}`);
535
- if (detail.never_inside.length > 0) lines.push(`Never inside: ${detail.never_inside.join(", ")}`);
536
- lines.push(
537
- detail.example.includes("\n") ? `Example:
538
- ${detail.example.split("\n").map((line) => ` ${line}`).join("\n")}` : `Example: ${detail.example}`
539
- );
540
- return `${lines.join("\n")}
541
- `;
542
- }
543
- var STOP = new Set(
544
- "a an the to of for in on at with and or i we need want that which is are it its my some from by".split(
545
- " "
546
- )
547
- );
548
- var words = (text2) => (text2.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter((word) => !STOP.has(word)).map((word) => word.length > 4 ? word.replace(/(?:ing|ed|es|s)$/, "") : word);
549
- var matched = (query, text2) => {
550
- const present = new Set(words(text2));
551
- return query.filter((word) => present.has(word)).length;
552
- };
553
- function search(contract2, registry2, query, limit = 10) {
554
- const wanted = words(query);
555
- if (wanted.length === 0) return [];
556
- const whole = squash(query);
557
- const scores = /* @__PURE__ */ new Map();
558
- const add = (name, points, why) => {
559
- const before = scores.get(name) ?? { score: 0 };
560
- scores.set(name, {
561
- score: before.score + points,
562
- ...before.why !== void 0 ? { why: before.why } : why !== void 0 ? { why } : {}
563
- });
564
- };
565
- for (const [name, component] of Object.entries(contract2.components)) {
566
- const guide = contract2.guidance.components[name] ?? {};
567
- const plain = squash(name);
568
- if (plain === whole) add(name, 100);
569
- else if (wanted.some((word) => word.length >= 3 && plain.includes(word))) add(name, 15);
570
- else if (wanted.some((word) => word.length >= 4 && distance(word, plain) <= 1)) add(name, 12);
571
- for (const phrase of guide.use_for ?? []) {
572
- const hits = matched(wanted, phrase);
573
- if (hits > 0) add(name, 12 * hits + (hits === wanted.length ? 40 : 0));
574
- }
575
- for (const { need, use } of guide.not_for ?? []) {
576
- const hits = matched(wanted, need);
577
- if (hits > 0 && hits >= Math.ceil(wanted.length * 0.6))
578
- add(use, 10 * hits + (hits === wanted.length ? 15 : 0), `${name} is not for this: ${need}`);
579
- }
580
- const inDescription = matched(wanted, component.description);
581
- if (inDescription > 0) add(name, 2 * inDescription);
582
- }
583
- const results = [];
584
- for (const [name, { score, why }] of scores) {
585
- const guide = contract2.guidance.components[name] ?? {};
586
- results.push({
587
- kind: "component",
588
- name,
589
- purpose: guide.part_of === void 0 ? (guide.use_for ?? []).join("; ") : `a part of ${guide.part_of}`,
590
- score,
591
- ...why === void 0 ? {} : { why },
592
- next: `oprt component ${name}`
593
- });
594
- }
595
- for (const entry of registry2.entries.filter(({ kind }) => kind === "recipe")) {
596
- const recipe = contract2.patterns?.[entry.name];
597
- const plain = squash(entry.name);
598
- const inName = plain === whole ? 100 : wanted.some((word) => word.length >= 3 && plain.includes(word)) ? 15 : 0;
599
- const hits = matched(wanted, entry.description);
600
- const score = inName + (hits > 0 ? 10 * hits + (hits === wanted.length ? 15 : 0) : 0);
601
- if (score > 0)
602
- results.push({
603
- kind: "recipe",
604
- name: entry.name,
605
- purpose: firstSentence(entry.description),
606
- score,
607
- next: recipe?.install ?? `oprt add ${entry.slug}`
608
- });
650
+
651
+ // src/check.ts
652
+ var import_node_fs3 = require("fs");
653
+ var import_node_path4 = require("path");
654
+
655
+ // src/parser.ts
656
+ var import_node_module = require("module");
657
+ var import_node_path3 = require("path");
658
+ var import_node_url2 = require("url");
659
+ var SUPPORTED_TYPESCRIPT = "TypeScript 5 or 6";
660
+ var INSTALL_TYPESCRIPT = "npm install -D typescript@6";
661
+ function usable(typescript) {
662
+ const ts = typescript;
663
+ if (typeof ts.createSourceFile !== "function" || ts.ScriptKind === void 0)
664
+ return `TypeScript ${ts.version ?? "(unknown version)"} has no JavaScript syntax API (createSourceFile), which oprt reads code with; ${SUPPORTED_TYPESCRIPT} has it`;
665
+ try {
666
+ const probe = ts.createSourceFile(
667
+ "probe.tsx",
668
+ "<a>",
669
+ ts.ScriptTarget?.Latest ?? 99,
670
+ true,
671
+ ts.ScriptKind.TSX
672
+ );
673
+ const diagnostics = probe.parseDiagnostics;
674
+ if (!Array.isArray(diagnostics)) return "it does not report parse errors on a source file";
675
+ if (diagnostics.length === 0) return "it did not report a syntax error in code that has one";
676
+ } catch (error) {
677
+ return `it failed on a probe parse: ${error instanceof Error ? error.message : String(error)}`;
609
678
  }
610
- results.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
611
- const floor = Math.max(4, (results[0]?.score ?? 0) * 0.15);
612
- return results.filter(({ score }) => score >= floor).slice(0, limit);
679
+ return null;
680
+ }
681
+ var packageFolder = (file) => /^(.*[\\/]node_modules[\\/]typescript)[\\/]/.exec(file)?.[1] ?? file;
682
+ function probeTypeScript(cwd, options = {}) {
683
+ const places = [
684
+ { from: (0, import_node_path3.join)(cwd, "package.json"), source: "project" },
685
+ ...options.installer === false ? [] : [{ from: (0, import_node_url2.fileURLToPath)(importMetaUrl), source: "installer" }]
686
+ ];
687
+ let unusable;
688
+ for (const { from, source } of places) {
689
+ let file;
690
+ let loaded2;
691
+ try {
692
+ const require2 = (0, import_node_module.createRequire)(from);
693
+ file = require2.resolve("typescript");
694
+ loaded2 = require2("typescript");
695
+ } catch (error) {
696
+ if (error.code !== "MODULE_NOT_FOUND")
697
+ unusable ??= `typescript was found but failed to load: ${error instanceof Error ? error.message : String(error)}`;
698
+ continue;
699
+ }
700
+ const typescript = loaded2.default ?? loaded2;
701
+ const problem = usable(typescript);
702
+ if (problem !== null) {
703
+ unusable ??= `${problem} (at ${packageFolder(file)}).`;
704
+ continue;
705
+ }
706
+ return {
707
+ ready: true,
708
+ typescript,
709
+ resolution: {
710
+ name: "typescript",
711
+ version: typescript.version,
712
+ source,
713
+ resolved_from: packageFolder(file)
714
+ }
715
+ };
716
+ }
717
+ if (unusable !== void 0)
718
+ return { ready: false, reason: "typescript_unusable", detail: unusable };
719
+ return {
720
+ ready: false,
721
+ reason: "typescript_missing",
722
+ detail: `typescript could not be resolved from ${cwd}${options.installer === false ? "" : " or from beside the installer"}.`
723
+ };
613
724
  }
614
725
 
615
726
  // src/check.ts
727
+ var FINDING_CODES = [
728
+ "INVALID_PROP_VALUE",
729
+ "UNKNOWN_EXPORT",
730
+ "DOT_NOTATION",
731
+ "INVALID_NESTING",
732
+ "INVALID_CHILD",
733
+ "MISSING_REQUIRED_PROP",
734
+ "UNKNOWN_PROP",
735
+ "RAW_COLOR",
736
+ "ARBITRARY_SIZE",
737
+ "ROUNDED_CORNER",
738
+ "NATIVE_CONTROL",
739
+ /** The file does not parse. It was not checked against the contract. */
740
+ "SYNTAX_ERROR"
741
+ ];
742
+ var LIMITATION_CODES = ["UNRESOLVED_SPREAD", "UNRESOLVED_VALUE"];
743
+ var SCOPE = {
744
+ checked: ["syntax", "contract", "house_rules"],
745
+ not_checked: ["types", "render"]
746
+ };
616
747
  var PACKAGE = "@oprt/react";
617
748
  var NATIVE = {
618
749
  button: "Button",
@@ -648,11 +779,49 @@ function allowedChildren2(rules, name) {
648
779
  }
649
780
  return void 0;
650
781
  }
782
+ var quote = (text2) => {
783
+ const flat = text2.replace(/\s+/g, " ").trim();
784
+ return flat.length > 48 ? `${flat.slice(0, 45)}...` : flat;
785
+ };
786
+ var MAX_SYNTAX_ERRORS = 10;
651
787
  function checkSource(typescript, contract2, file, text2) {
652
788
  const ts = typescript;
653
789
  const kind = /\.tsx$/.test(file) ? ts.ScriptKind.TSX : ts.ScriptKind.JSX;
654
790
  const source = ts.createSourceFile(file, text2, ts.ScriptTarget.Latest, true, kind);
791
+ const parseDiagnostics = source.parseDiagnostics;
792
+ if (!Array.isArray(parseDiagnostics))
793
+ throw new CliError(
794
+ `This TypeScript does not report syntax errors on a parsed file, so a file that does not parse could be called clean. Install TypeScript 5 or 6 in this project (${INSTALL_TYPESCRIPT}).`,
795
+ 1,
796
+ { code: "ERR_TYPESCRIPT_UNUSABLE" }
797
+ );
798
+ if (parseDiagnostics.length > 0) {
799
+ const ordered = [...parseDiagnostics].sort((a, b) => (a.start ?? 0) - (b.start ?? 0));
800
+ const shown = ordered.slice(0, MAX_SYNTAX_ERRORS);
801
+ return {
802
+ file,
803
+ status: "not_evaluated",
804
+ limitations: [],
805
+ findings: shown.map((diagnostic, index) => {
806
+ const { line, character } = source.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
807
+ const more = ordered.length - shown.length;
808
+ return {
809
+ code: "SYNTAX_ERROR",
810
+ severity: "error",
811
+ file,
812
+ line: line + 1,
813
+ column: character + 1,
814
+ message: `${ts.flattenDiagnosticMessageText(diagnostic.messageText, " ")} (TS${diagnostic.code})` + (index === shown.length - 1 && more > 0 ? ` ${more} more syntax error${more === 1 ? "" : "s"} in this file not shown.` : "")
815
+ };
816
+ })
817
+ };
818
+ }
655
819
  const findings = [];
820
+ const limitations = [];
821
+ const limit = (node, limitation) => {
822
+ const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
823
+ limitations.push({ ...limitation, file, line: line + 1, column: character + 1 });
824
+ };
656
825
  const report = (node, finding) => {
657
826
  const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
658
827
  findings.push({
@@ -677,7 +846,7 @@ function checkSource(typescript, contract2, file, text2) {
677
846
  const from = statement.moduleSpecifier.text;
678
847
  const clause = statement.importClause;
679
848
  if (!clause || clause.isTypeOnly || !clause.namedBindings) continue;
680
- const copied = from !== PACKAGE && /[./@]/.test(from[0] ?? "") && isComponent((0, import_node_path3.basename)(from));
849
+ const copied = from !== PACKAGE && /[./@]/.test(from[0] ?? "") && isComponent((0, import_node_path4.basename)(from));
681
850
  if (from !== PACKAGE && !copied) continue;
682
851
  if (ts.isNamespaceImport(clause.namedBindings)) {
683
852
  if (from === PACKAGE) namespaces.add(clause.namedBindings.name.text);
@@ -723,6 +892,31 @@ function checkSource(typescript, contract2, file, text2) {
723
892
  return [];
724
893
  };
725
894
  const valueOf = (attribute) => attribute.initializer === void 0 ? void 0 : ts.isJsxExpression(attribute.initializer) ? attribute.initializer.expression : attribute.initializer;
895
+ const optionValue = (expression) => {
896
+ const read = [];
897
+ const unread = [];
898
+ const walk = (node) => {
899
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
900
+ read.push(node.text);
901
+ else if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isNonNullExpression(node) || ts.isTypeAssertionExpression(node) || typeof ts.isSatisfiesExpression === "function" && ts.isSatisfiesExpression(node))
902
+ walk(node.expression);
903
+ else if (ts.isConditionalExpression(node)) {
904
+ walk(node.whenTrue);
905
+ walk(node.whenFalse);
906
+ } else if (ts.isBinaryExpression(node)) {
907
+ const operator = node.operatorToken.kind;
908
+ if (operator === ts.SyntaxKind.QuestionQuestionToken || operator === ts.SyntaxKind.BarBarToken) {
909
+ walk(node.left);
910
+ walk(node.right);
911
+ } else if (operator === ts.SyntaxKind.AmpersandAmpersandToken) walk(node.right);
912
+ else unread.push(node);
913
+ } else if (!// No value: the component's default applies.
914
+ (ts.isIdentifier(node) && node.text === "undefined" || node.kind === ts.SyntaxKind.NullKeyword))
915
+ unread.push(node);
916
+ };
917
+ if (expression) walk(expression);
918
+ return { literals: read, unread };
919
+ };
726
920
  const classText = (expression) => {
727
921
  if (!expression) return "";
728
922
  if (ts.isTemplateExpression(expression))
@@ -795,6 +989,13 @@ function checkSource(typescript, contract2, file, text2) {
795
989
  if (component === null) return null;
796
990
  const definition = contract2.components[component];
797
991
  const spread = attributes.some((attribute) => ts.isJsxSpreadAttribute(attribute));
992
+ for (const attribute of attributes)
993
+ if (ts.isJsxSpreadAttribute(attribute))
994
+ limit(attribute, {
995
+ code: "UNRESOLVED_SPREAD",
996
+ component,
997
+ message: `Props spread into <${component}> (${quote(attribute.getText(source))}) were not read: option values and required props that arrive through them were not checked.`
998
+ });
798
999
  const given = /* @__PURE__ */ new Set();
799
1000
  for (const attribute of attributes) {
800
1001
  if (!ts.isJsxAttribute(attribute)) continue;
@@ -803,18 +1004,26 @@ function checkSource(typescript, contract2, file, text2) {
803
1004
  const known = Object.hasOwn(definition.props, prop) ? definition.props[prop] : void 0;
804
1005
  if (known === void 0) {
805
1006
  if (nativeAttribute(prop) || Object.hasOwn(definition.slots ?? {}, prop)) continue;
806
- const names = Object.keys(definition.props);
1007
+ const names2 = Object.keys(definition.props);
807
1008
  report(attribute, {
808
1009
  code: "UNKNOWN_PROP",
809
1010
  severity: "warning",
810
1011
  component,
811
1012
  prop,
812
- message: `${component} has no prop ${prop}.` + (names.length === 0 ? " It takes no props of its own." : ` Its props: ${names.slice(0, 14).join(", ")}${names.length > 14 ? ", \u2026" : ""}.`)
1013
+ message: `${component} has no prop ${prop}.` + (names2.length === 0 ? " It takes no props of its own." : ` Its props: ${names2.slice(0, 14).join(", ")}${names2.length > 14 ? ", \u2026" : ""}.`)
813
1014
  });
814
1015
  continue;
815
1016
  }
816
1017
  if (known.type !== "enum" || known.values === void 0) continue;
817
- for (const literal of literals(valueOf(attribute))) {
1018
+ const { literals: options, unread } = optionValue(valueOf(attribute));
1019
+ for (const expression of unread)
1020
+ limit(attribute, {
1021
+ code: "UNRESOLVED_VALUE",
1022
+ component,
1023
+ prop,
1024
+ message: `${component} ${prop} is set from an expression (${quote(expression.getText(source))}) whose value is not in the source, so it was not compared with the options.`
1025
+ });
1026
+ for (const literal of options) {
818
1027
  if (known.values.includes(literal)) continue;
819
1028
  const elsewhere = Object.entries(contract2.components).filter(([, other]) => other.props[prop]?.values?.includes(literal)).map(([name]) => name);
820
1029
  report(attribute, {
@@ -884,64 +1093,153 @@ function checkSource(typescript, contract2, file, text2) {
884
1093
  ts.forEachChild(node, (child) => visit(child, ancestors, parent));
885
1094
  };
886
1095
  visit(source, [], null);
887
- return findings.sort((a, b) => a.line - b.line || a.column - b.column);
1096
+ const byPlace = (a, b) => a.line - b.line || a.column - b.column;
1097
+ return {
1098
+ file,
1099
+ status: limitations.length > 0 ? "partial" : "evaluated",
1100
+ findings: findings.sort(byPlace),
1101
+ limitations: limitations.sort(byPlace)
1102
+ };
888
1103
  }
889
1104
  var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage"]);
890
1105
  var READ = /* @__PURE__ */ new Set([".tsx", ".jsx"]);
891
1106
  var NOT_INTERFACE = /\.(?:test|spec|stories)\.[jt]sx$/;
1107
+ var excluded = (name) => SKIPPED.has(name) || name.startsWith(".");
892
1108
  function* sources(path) {
893
- const name = (0, import_node_path3.basename)(path);
894
- if (SKIPPED.has(name) || name.startsWith(".") && name !== "." && name !== "..") return;
1109
+ if (excluded((0, import_node_path4.basename)(path))) return;
895
1110
  if ((0, import_node_fs3.statSync)(path).isDirectory()) {
896
- for (const entry of (0, import_node_fs3.readdirSync)(path).sort()) yield* sources((0, import_node_path3.join)(path, entry));
897
- } else if (READ.has((0, import_node_path3.extname)(path)) && !NOT_INTERFACE.test(name)) yield path;
1111
+ for (const entry of (0, import_node_fs3.readdirSync)(path).sort()) yield* sources((0, import_node_path4.join)(path, entry));
1112
+ } else if (READ.has((0, import_node_path4.extname)(path)) && !NOT_INTERFACE.test((0, import_node_path4.basename)(path))) yield path;
898
1113
  }
899
- function findTypeScript(cwd) {
900
- for (const from of [(0, import_node_path3.join)(cwd, "package.json"), (0, import_node_url2.fileURLToPath)(importMetaUrl)]) {
901
- try {
902
- return (0, import_node_module.createRequire)(from)("typescript");
903
- } catch {
904
- }
1114
+ function whyNotRead(path) {
1115
+ const name = (0, import_node_path4.basename)(path);
1116
+ if (excluded(name)) return "excluded_folder";
1117
+ if ((0, import_node_fs3.statSync)(path).isDirectory()) return null;
1118
+ if (!READ.has((0, import_node_path4.extname)(path))) return "unsupported_extension";
1119
+ return NOT_INTERFACE.test(name) ? "test_or_story_file" : null;
1120
+ }
1121
+ function buildCheckReport(evaluations, context) {
1122
+ const findings = evaluations.flatMap(({ findings: found }) => found);
1123
+ const count = (status2) => evaluations.filter((evaluation) => evaluation.status === status2).length;
1124
+ const partial = count("partial");
1125
+ const notEvaluated = count("not_evaluated");
1126
+ const evaluated = evaluations.length - notEvaluated;
1127
+ const errors = findings.filter(({ severity }) => severity === "error").length;
1128
+ const warnings = findings.length - errors;
1129
+ let status;
1130
+ if (evaluations.length === 0) status = "no_files_evaluated";
1131
+ else if (evaluated === 0) status = "not_evaluated";
1132
+ else if (partial > 0 || notEvaluated > 0) status = "partially_evaluated";
1133
+ else status = findings.length > 0 ? "evaluated_with_findings" : "evaluated_clean";
1134
+ return {
1135
+ evaluation_status: status,
1136
+ files: evaluations.length,
1137
+ findings,
1138
+ summary: { errors, warnings },
1139
+ coverage: {
1140
+ files_found: evaluations.length,
1141
+ files_evaluated: evaluated,
1142
+ files_partial: partial,
1143
+ files_not_evaluated: notEvaluated
1144
+ },
1145
+ limitations: evaluations.flatMap(({ limitations }) => limitations),
1146
+ skipped_inputs: context.skipped ?? [],
1147
+ scope: { checked: [...SCOPE.checked], not_checked: [...SCOPE.not_checked] },
1148
+ parser: context.parser
1149
+ };
1150
+ }
1151
+ function checkExitCode(report, { strict }) {
1152
+ if (report.evaluation_status === "no_files_evaluated") return 1;
1153
+ if (report.summary.errors > 0 || report.coverage.files_not_evaluated > 0) return 1;
1154
+ return strict && report.summary.warnings > 0 ? 1 : 0;
1155
+ }
1156
+ var describeProvided = (typescript) => ({
1157
+ name: "typescript",
1158
+ version: typescript.version ?? "unknown",
1159
+ source: "provided"
1160
+ });
1161
+ function resolveParser(cwd, provided, who) {
1162
+ if (provided !== void 0 && provided !== null)
1163
+ return { typescript: provided, parser: describeProvided(provided) };
1164
+ const probe = provided === null ? null : probeTypeScript(cwd);
1165
+ if (probe?.ready) {
1166
+ const { name, version, source } = probe.resolution;
1167
+ return { typescript: probe.typescript, parser: { name, version, source } };
905
1168
  }
906
- return null;
1169
+ if (probe !== null && probe.reason === "typescript_unusable")
1170
+ throw new CliError(`${who} cannot use the TypeScript it found: ${probe.detail}`, 1, {
1171
+ code: "ERR_TYPESCRIPT_UNUSABLE"
1172
+ });
1173
+ throw new CliError(
1174
+ `${who} reads code with the project's TypeScript, and found none. Install typescript in this project (${INSTALL_TYPESCRIPT}) and run it again. It looked from ${cwd}.`,
1175
+ 1,
1176
+ { code: "ERR_TYPESCRIPT_MISSING" }
1177
+ );
907
1178
  }
908
1179
  function checkPaths(paths, context) {
909
- const root = (0, import_node_path3.resolve)(context.cwd);
1180
+ const root = (0, import_node_path4.resolve)(context.cwd);
910
1181
  const files = [];
1182
+ const skipped = [];
911
1183
  for (const path of paths) {
912
- const absolute = (0, import_node_path3.resolve)(root, path);
1184
+ const absolute = (0, import_node_path4.resolve)(root, path);
913
1185
  if (!(0, import_node_fs3.existsSync)(absolute))
914
1186
  throw new CliError(`No such file or folder: ${path}`, 2, { code: "ERR_PATH_NOT_FOUND" });
1187
+ const reason = whyNotRead(absolute);
1188
+ if (reason !== null) skipped.push({ path, reason });
915
1189
  files.push(...sources(absolute));
916
1190
  }
917
- const ts = context.typescript === void 0 ? findTypeScript(root) : context.typescript;
918
- if (ts === null)
919
- throw new CliError(
920
- "oprt check reads code with the project's TypeScript, and found none. Install typescript in this project (npm install -D typescript) and run it again.",
921
- 1,
922
- { code: "ERR_TYPESCRIPT_MISSING" }
923
- );
924
- const findings = [...new Set(files)].flatMap(
1191
+ const { typescript, parser } = resolveParser(root, context.typescript, "oprt check");
1192
+ const evaluations = [...new Set(files)].map(
925
1193
  (file) => checkSource(
926
- ts,
1194
+ typescript,
927
1195
  context.contract,
928
- (0, import_node_path3.relative)(root, file).split(import_node_path3.sep).join("/"),
1196
+ (0, import_node_path4.relative)(root, file).split(import_node_path4.sep).join("/"),
929
1197
  (0, import_node_fs3.readFileSync)(file, "utf8")
930
1198
  )
931
1199
  );
932
- return {
933
- files: new Set(files).size,
934
- findings,
935
- summary: {
936
- errors: findings.filter(({ severity }) => severity === "error").length,
937
- warnings: findings.filter(({ severity }) => severity === "warning").length
938
- }
939
- };
1200
+ return buildCheckReport(evaluations, { parser, skipped });
1201
+ }
1202
+ var plural = (amount, word) => `${amount} ${word}${amount === 1 ? "" : "s"}`;
1203
+ var MAX_LIMITS_SHOWN = 20;
1204
+ function renderCheckText(report) {
1205
+ const lines = report.findings.map(
1206
+ ({ file, line, column, severity, code, message }) => `${file}:${line}:${column} ${severity} ${code} ${message}`
1207
+ );
1208
+ const shown = report.limitations.slice(0, MAX_LIMITS_SHOWN);
1209
+ for (const { file, line, column, code, message } of shown)
1210
+ lines.push(`${file}:${line}:${column} limit ${code} ${message}`);
1211
+ if (report.limitations.length > shown.length)
1212
+ lines.push(
1213
+ ` ...and ${plural(report.limitations.length - shown.length, "more limit")}; the JSON lists them all.`
1214
+ );
1215
+ const { coverage, evaluation_status: status } = report;
1216
+ if (status === "no_files_evaluated") {
1217
+ lines.push("No .tsx or .jsx file was read, so nothing was checked.");
1218
+ const labels = {
1219
+ unsupported_extension: "not a .tsx or .jsx file",
1220
+ test_or_story_file: "a test, spec or story file, which is skipped",
1221
+ excluded_folder: "a folder that is skipped (node_modules, dist, build, out, coverage, hidden)"
1222
+ };
1223
+ for (const { path, reason } of report.skipped_inputs)
1224
+ lines.push(` ${path}: ${labels[reason]}`);
1225
+ } else if (coverage.files_not_evaluated > 0)
1226
+ lines.push(
1227
+ `${plural(coverage.files_not_evaluated, "file")} did not parse and ${coverage.files_not_evaluated === 1 ? "was" : "were"} not checked against the contract.`
1228
+ );
1229
+ else if (status === "evaluated_clean")
1230
+ lines.push(
1231
+ "Nothing found. Checked syntax, option values, nesting, required props and the house rules; types and rendering are not checked."
1232
+ );
1233
+ lines.push(
1234
+ `${plural(report.summary.errors, "error")}, ${plural(report.summary.warnings, "warning")} in ${plural(report.files, "file")}. Status: ${status}.`
1235
+ );
1236
+ return `${lines.join("\n")}
1237
+ `;
940
1238
  }
941
1239
 
942
1240
  // src/doctor.ts
943
1241
  var import_node_fs4 = require("fs");
944
- var import_node_path4 = require("path");
1242
+ var import_node_path5 = require("path");
945
1243
 
946
1244
  // src/agent-block.ts
947
1245
  var START = "<!-- OPERATOR:START -->";
@@ -1025,14 +1323,18 @@ var INSTRUCTION_FILES = [
1025
1323
  ];
1026
1324
  var minor = (version) => version.split(".").slice(0, 2).join(".");
1027
1325
  function installedVersion(cwd, name) {
1028
- for (let folder = cwd; ; folder = (0, import_node_path4.dirname)(folder)) {
1029
- const manifest = (0, import_node_path4.join)(folder, "node_modules", name, "package.json");
1326
+ for (let folder = cwd; ; folder = (0, import_node_path5.dirname)(folder)) {
1327
+ const manifest = (0, import_node_path5.join)(folder, "node_modules", name, "package.json");
1030
1328
  if ((0, import_node_fs4.existsSync)(manifest))
1031
1329
  return JSON.parse((0, import_node_fs4.readFileSync)(manifest, "utf8")).version;
1032
- if ((0, import_node_path4.dirname)(folder) === folder) return void 0;
1330
+ if ((0, import_node_path5.dirname)(folder) === folder) return void 0;
1033
1331
  }
1034
1332
  }
1035
- function doctor({ cwd, contract: contract2 }) {
1333
+ function doctor({
1334
+ cwd,
1335
+ contract: contract2,
1336
+ parser = probeTypeScript(cwd)
1337
+ }) {
1036
1338
  const checks = [];
1037
1339
  for (const name of PACKAGES) {
1038
1340
  const version = installedVersion(cwd, name);
@@ -1054,7 +1356,7 @@ function doctor({ cwd, contract: contract2 }) {
1054
1356
  else checks.push({ id, status: "ok", message: `${name} ${version} is installed.` });
1055
1357
  }
1056
1358
  const written = INSTRUCTION_FILES.map((path) => {
1057
- const file = (0, import_node_path4.join)(cwd, path);
1359
+ const file = (0, import_node_path5.join)(cwd, path);
1058
1360
  return {
1059
1361
  path,
1060
1362
  version: (0, import_node_fs4.existsSync)(file) ? blockVersion((0, import_node_fs4.readFileSync)(file, "utf8")) : void 0
@@ -1081,13 +1383,60 @@ function doctor({ cwd, contract: contract2 }) {
1081
1383
  status: "ok",
1082
1384
  message: `${written.map(({ path }) => path).join(", ")} carries the OPERATOR block (${contract2.version}).`
1083
1385
  });
1386
+ const install2 = `${INSTALL_TYPESCRIPT} (or your package manager's equivalent), then run oprt doctor again. Lookups work without it.`;
1387
+ let checkCode;
1388
+ if (parser.ready) {
1389
+ const { version, resolved_from, source } = parser.resolution;
1390
+ checkCode = {
1391
+ status: "ready",
1392
+ message: `TypeScript ${version}, resolved from ${resolved_from}${source === "project" ? "" : " (beside oprt, not this project)"}.`,
1393
+ typescript: { version, resolved_from, source }
1394
+ };
1395
+ checks.push(
1396
+ source === "project" ? {
1397
+ id: "typescript",
1398
+ status: "ok",
1399
+ message: `TypeScript ${version} is installed in this project (${resolved_from}).`
1400
+ } : {
1401
+ id: "typescript",
1402
+ status: "warn",
1403
+ message: `TypeScript ${version} is not in this project; the one beside oprt is used (${resolved_from}).`,
1404
+ fix: `${INSTALL_TYPESCRIPT}, so checking reads code with a version installed in the project.`
1405
+ }
1406
+ );
1407
+ } else {
1408
+ checkCode = {
1409
+ status: "blocked",
1410
+ message: `${parser.reason}: ${parser.detail}`,
1411
+ reason: parser.reason,
1412
+ fix: install2
1413
+ };
1414
+ checks.push({
1415
+ id: "typescript",
1416
+ status: "fail",
1417
+ message: `oprt check and the check_code tool cannot run (${parser.reason}): ${parser.detail}`,
1418
+ fix: install2
1419
+ });
1420
+ }
1084
1421
  const count = (status) => checks.filter((check) => check.status === status).length;
1085
- return { checks, summary: { ok: count("ok"), warn: count("warn"), fail: count("fail") } };
1422
+ return {
1423
+ consumer_directory: cwd,
1424
+ operations: {
1425
+ lookup: {
1426
+ status: "ready",
1427
+ message: `search, component and list read the contract bundled with oprt (${contract2.version}): always available.`,
1428
+ contract_version: contract2.version
1429
+ },
1430
+ check_code: checkCode
1431
+ },
1432
+ checks,
1433
+ summary: { ok: count("ok"), warn: count("warn"), fail: count("fail") }
1434
+ };
1086
1435
  }
1087
1436
 
1088
1437
  // src/init.ts
1089
1438
  var import_node_fs5 = require("fs");
1090
- var import_node_path5 = require("path");
1439
+ var import_node_path6 = require("path");
1091
1440
  var AGENT_FILES = {
1092
1441
  claude: ["CLAUDE.md"],
1093
1442
  cursor: [".cursor/rules/operator.mdc"],
@@ -1115,7 +1464,7 @@ function init(options, context) {
1115
1464
  const dryRun = options.dryRun ?? false;
1116
1465
  const files = targets(options).map((path) => {
1117
1466
  validateRelative(path);
1118
- const destination = (0, import_node_path5.resolve)(root, path);
1467
+ const destination = (0, import_node_path6.resolve)(root, path);
1119
1468
  inspectPath(root, destination);
1120
1469
  const existing = (0, import_node_fs5.existsSync)(destination) ? (0, import_node_fs5.readFileSync)(destination, "utf8") : null;
1121
1470
  if (options.remove) {
@@ -1132,7 +1481,7 @@ function init(options, context) {
1132
1481
  const fresh = existing === null && path.endsWith(".mdc") ? CURSOR_FRONT_MATTER : existing;
1133
1482
  const { text: text2, action } = applyBlock(fresh, block);
1134
1483
  if (action !== "unchanged" && !dryRun) {
1135
- (0, import_node_fs5.mkdirSync)((0, import_node_path5.dirname)(destination), { recursive: true });
1484
+ (0, import_node_fs5.mkdirSync)((0, import_node_path6.dirname)(destination), { recursive: true });
1136
1485
  (0, import_node_fs5.writeFileSync)(destination, text2);
1137
1486
  }
1138
1487
  return { path, action: existing === null ? "create" : action };
@@ -1140,8 +1489,536 @@ function init(options, context) {
1140
1489
  return { version: context.contract.version, dryRun, files };
1141
1490
  }
1142
1491
 
1492
+ // src/results.ts
1493
+ var RESULT_TYPES = [
1494
+ "check",
1495
+ "search",
1496
+ "component.detail",
1497
+ "component.batch",
1498
+ "component.list",
1499
+ "init",
1500
+ "doctor",
1501
+ "add",
1502
+ "list",
1503
+ "instructions"
1504
+ ];
1505
+ var envelope = (type, data, contract2) => ({
1506
+ apiVersion: 1,
1507
+ type,
1508
+ identity: identityOf(contract2),
1509
+ data
1510
+ });
1511
+ function errorEnvelope(error, contract2) {
1512
+ const known = error instanceof CliError ? error : void 0;
1513
+ const identity = contract2 === void 0 ? { cli_version: VERSION, schema_version: RESULT_SCHEMA_VERSION } : (({ cli_version, schema_version, contract_version, contract_digest }) => ({
1514
+ cli_version,
1515
+ schema_version,
1516
+ contract_version,
1517
+ contract_digest
1518
+ }))(identityOf(contract2));
1519
+ return {
1520
+ apiVersion: 1,
1521
+ error: error instanceof Error ? error.message : String(error),
1522
+ code: known?.code ?? "ERR_FAILED",
1523
+ ...known?.suggestions === void 0 ? {} : { suggestions: known.suggestions },
1524
+ identity
1525
+ };
1526
+ }
1527
+ function componentData(detail, dense) {
1528
+ if (!dense) return { ...detail, dense };
1529
+ const { description: _description, ...rest } = detail;
1530
+ return {
1531
+ ...rest,
1532
+ props: detail.props.map(
1533
+ (prop) => prop.description === void 0 ? prop : { ...prop, description: firstSentence(prop.description) }
1534
+ ),
1535
+ dense
1536
+ };
1537
+ }
1538
+ function instructionsData(contract2) {
1539
+ return {
1540
+ rules: contract2.guidance.rules.map(({ id, rule, wrong, right }) => ({
1541
+ id,
1542
+ rule,
1543
+ ...wrong === void 0 ? {} : { wrong },
1544
+ ...right === void 0 ? {} : { right }
1545
+ })),
1546
+ components: componentIndex(contract2),
1547
+ recipes: Object.entries(contract2.patterns ?? {}).map(([name, { description, install: install2 }]) => ({
1548
+ name,
1549
+ description,
1550
+ install: install2
1551
+ }))
1552
+ };
1553
+ }
1554
+ var str = (extra = {}) => ({ type: "string", ...extra });
1555
+ var int = () => ({ type: "integer", minimum: 0 });
1556
+ var bool = () => ({ type: "boolean" });
1557
+ var arr = (items) => ({ type: "array", items });
1558
+ var names = () => arr(str());
1559
+ var words2 = (...values) => ({ type: "string", enum: [...values] });
1560
+ var ref = (name) => ({ $ref: `#/$defs/${name}` });
1561
+ var obj = (properties, optional = [], open = false) => ({
1562
+ type: "object",
1563
+ properties,
1564
+ required: Object.keys(properties).filter((key) => !optional.includes(key)),
1565
+ additionalProperties: open
1566
+ });
1567
+ var scalar = { type: ["string", "number", "boolean"] };
1568
+ var DEFS = {
1569
+ identity: obj({
1570
+ cli_version: str(),
1571
+ contract_version: str(),
1572
+ contract_digest: str({ pattern: "^sha256:[0-9a-f]{64}$" }),
1573
+ schema_version: { type: "integer", minimum: 1 }
1574
+ }),
1575
+ error_identity: obj(
1576
+ {
1577
+ cli_version: str(),
1578
+ schema_version: { type: "integer", minimum: 1 },
1579
+ contract_version: str(),
1580
+ contract_digest: str({ pattern: "^sha256:[0-9a-f]{64}$" })
1581
+ },
1582
+ ["contract_version", "contract_digest"]
1583
+ ),
1584
+ finding: obj(
1585
+ {
1586
+ code: words2(...FINDING_CODES),
1587
+ severity: words2("error", "warning"),
1588
+ file: str(),
1589
+ line: { type: "integer", minimum: 1 },
1590
+ column: { type: "integer", minimum: 1 },
1591
+ component: str(),
1592
+ prop: str(),
1593
+ message: str(),
1594
+ valid_values: names(),
1595
+ suggestions: names()
1596
+ },
1597
+ ["component", "prop", "valid_values", "suggestions"]
1598
+ ),
1599
+ limitation: obj(
1600
+ {
1601
+ code: words2(...LIMITATION_CODES),
1602
+ file: str(),
1603
+ line: { type: "integer", minimum: 1 },
1604
+ column: { type: "integer", minimum: 1 },
1605
+ component: str(),
1606
+ prop: str(),
1607
+ message: str()
1608
+ },
1609
+ ["prop"]
1610
+ ),
1611
+ skipped_input: obj({
1612
+ path: str(),
1613
+ reason: words2("unsupported_extension", "test_or_story_file", "excluded_folder")
1614
+ }),
1615
+ parser: obj({
1616
+ name: words2("typescript"),
1617
+ version: str(),
1618
+ source: words2("project", "installer", "provided")
1619
+ }),
1620
+ check_data: obj({
1621
+ evaluation_status: words2(
1622
+ "evaluated_clean",
1623
+ "evaluated_with_findings",
1624
+ "partially_evaluated",
1625
+ "not_evaluated",
1626
+ "no_files_evaluated"
1627
+ ),
1628
+ files: int(),
1629
+ findings: arr(ref("finding")),
1630
+ summary: obj({ errors: int(), warnings: int() }),
1631
+ coverage: obj({
1632
+ files_found: int(),
1633
+ files_evaluated: int(),
1634
+ files_partial: int(),
1635
+ files_not_evaluated: int()
1636
+ }),
1637
+ limitations: arr(ref("limitation")),
1638
+ skipped_inputs: arr(ref("skipped_input")),
1639
+ scope: obj({ checked: names(), not_checked: names() }),
1640
+ parser: ref("parser")
1641
+ }),
1642
+ search_result: obj(
1643
+ {
1644
+ kind: words2("component", "recipe"),
1645
+ name: str(),
1646
+ purpose: str(),
1647
+ score: { type: "number" },
1648
+ why: str(),
1649
+ next: str()
1650
+ },
1651
+ ["why"]
1652
+ ),
1653
+ search_data: obj({
1654
+ query: str(),
1655
+ limit: { type: "integer", minimum: 1 },
1656
+ matchCount: int(),
1657
+ results: arr(ref("search_result"))
1658
+ }),
1659
+ index_entry: obj({ name: str(), purpose: str(), parts: names() }),
1660
+ component_list_data: obj({ components: arr(ref("index_entry")) }),
1661
+ detail_prop: obj(
1662
+ {
1663
+ name: str(),
1664
+ type: str(),
1665
+ values: names(),
1666
+ types: names(),
1667
+ default: scalar,
1668
+ required: bool(),
1669
+ description: str(),
1670
+ notes: { type: "object", additionalProperties: str() }
1671
+ },
1672
+ ["values", "types", "default", "required", "description", "notes"],
1673
+ // The contract may describe a prop with more than these (an array's item type, for one).
1674
+ true
1675
+ ),
1676
+ component_detail: obj(
1677
+ {
1678
+ name: str(),
1679
+ description: str(),
1680
+ use_for: names(),
1681
+ not_for: arr(obj({ need: str(), use: str() })),
1682
+ part_of: str(),
1683
+ parts: names(),
1684
+ props: arr(ref("detail_prop")),
1685
+ slots: {
1686
+ type: "object",
1687
+ additionalProperties: obj({ type: str(), required: bool() }, ["required"])
1688
+ },
1689
+ composable_with: names(),
1690
+ never_inside: names(),
1691
+ allowed_children: names(),
1692
+ example: str(),
1693
+ dense: bool()
1694
+ },
1695
+ ["description", "part_of", "slots", "allowed_children"]
1696
+ ),
1697
+ component_batch_data: obj({ components: arr(ref("component_detail")) }),
1698
+ doctor_check: obj(
1699
+ { id: str(), status: words2("ok", "warn", "fail"), message: str(), fix: str() },
1700
+ ["fix"]
1701
+ ),
1702
+ doctor_data: obj({
1703
+ consumer_directory: str(),
1704
+ operations: obj({
1705
+ lookup: obj({ status: words2("ready"), message: str(), contract_version: str() }),
1706
+ check_code: obj(
1707
+ {
1708
+ status: words2("ready", "blocked"),
1709
+ message: str(),
1710
+ reason: words2("typescript_missing", "typescript_unusable"),
1711
+ fix: str(),
1712
+ typescript: obj({
1713
+ version: str(),
1714
+ resolved_from: str(),
1715
+ source: words2("project", "installer", "provided")
1716
+ })
1717
+ },
1718
+ ["reason", "fix", "typescript"]
1719
+ )
1720
+ }),
1721
+ checks: arr(ref("doctor_check")),
1722
+ summary: obj({ ok: int(), warn: int(), fail: int() })
1723
+ }),
1724
+ init_data: obj({
1725
+ version: str(),
1726
+ dryRun: bool(),
1727
+ files: arr(
1728
+ obj({
1729
+ path: str(),
1730
+ action: words2("create", "append", "update", "unchanged", "remove", "delete", "absent")
1731
+ })
1732
+ )
1733
+ }),
1734
+ install_file: obj({
1735
+ path: str(),
1736
+ destination: str(),
1737
+ action: words2("create", "overwrite", "skip")
1738
+ }),
1739
+ add_data: obj({
1740
+ outputDirectory: str(),
1741
+ entries: names(),
1742
+ dependencies: names(),
1743
+ devDependencies: names(),
1744
+ ranges: { type: "object", additionalProperties: str() },
1745
+ files: arr(ref("install_file")),
1746
+ dryRun: bool(),
1747
+ setup: ref("setup")
1748
+ }),
1749
+ setup: obj({
1750
+ tailwind: str(),
1751
+ packages: arr(obj({ name: str(), range: str(), dev: bool(), why: str() })),
1752
+ stylesheet: arr(obj({ line: str(), why: str() })),
1753
+ root_import: obj({ line: str(), note: str() }),
1754
+ not_needed: arr(obj({ name: str(), why: str() }))
1755
+ }),
1756
+ list_data: obj({
1757
+ version: str(),
1758
+ entries: arr(
1759
+ obj(
1760
+ {
1761
+ name: str(),
1762
+ slug: str(),
1763
+ kind: words2("component", "recipe"),
1764
+ description: str(),
1765
+ files: names(),
1766
+ dependencies: names(),
1767
+ devDependencies: names()
1768
+ },
1769
+ ["devDependencies"]
1770
+ )
1771
+ ),
1772
+ ranges: { type: "object", additionalProperties: str() }
1773
+ }),
1774
+ instructions_data: obj({
1775
+ rules: arr(obj({ id: str(), rule: str(), wrong: str(), right: str() }, ["wrong", "right"])),
1776
+ components: arr(ref("index_entry")),
1777
+ recipes: arr(obj({ name: str(), description: str(), install: str() }))
1778
+ })
1779
+ };
1780
+ var DATA = {
1781
+ check: "check_data",
1782
+ search: "search_data",
1783
+ "component.detail": "component_detail",
1784
+ "component.batch": "component_batch_data",
1785
+ "component.list": "component_list_data",
1786
+ init: "init_data",
1787
+ doctor: "doctor_data",
1788
+ add: "add_data",
1789
+ list: "list_data",
1790
+ instructions: "instructions_data"
1791
+ };
1792
+ var success = (type) => obj({
1793
+ apiVersion: { const: 1 },
1794
+ type: { const: type },
1795
+ identity: ref("identity"),
1796
+ data: ref(DATA[type])
1797
+ });
1798
+ var failure = () => obj(
1799
+ {
1800
+ apiVersion: { const: 1 },
1801
+ error: str(),
1802
+ code: str({ pattern: "^ERR_[A-Z_]+$" }),
1803
+ suggestions: names(),
1804
+ identity: ref("error_identity")
1805
+ },
1806
+ ["suggestions"]
1807
+ );
1808
+ function bundle(root) {
1809
+ const used = {};
1810
+ const visit = (node) => {
1811
+ if (Array.isArray(node)) node.forEach(visit);
1812
+ else if (typeof node === "object" && node !== null) {
1813
+ for (const [key, value] of Object.entries(node)) {
1814
+ if (key === "$ref" && typeof value === "string") {
1815
+ const name = value.replace("#/$defs/", "");
1816
+ if (used[name] === void 0) {
1817
+ used[name] = DEFS[name];
1818
+ visit(DEFS[name]);
1819
+ }
1820
+ } else visit(value);
1821
+ }
1822
+ }
1823
+ };
1824
+ visit(root);
1825
+ return { ...root, $defs: used };
1826
+ }
1827
+ function schemaFor(type) {
1828
+ return bundle(type === "error" ? failure() : success(type));
1829
+ }
1830
+ var S = { type: "string" };
1831
+ var I = { type: "integer" };
1832
+ var B = { type: "boolean" };
1833
+ var list = (items) => items === void 0 ? { type: "array" } : { type: "array", items };
1834
+ var shape = (properties, required) => ({
1835
+ type: "object",
1836
+ properties,
1837
+ ...required === void 0 ? {} : { required }
1838
+ });
1839
+ var strings = list(S);
1840
+ var SUMMARIES = {
1841
+ check: shape(
1842
+ {
1843
+ evaluation_status: words2(
1844
+ "evaluated_clean",
1845
+ "evaluated_with_findings",
1846
+ "partially_evaluated",
1847
+ "not_evaluated",
1848
+ "no_files_evaluated"
1849
+ ),
1850
+ findings: list(
1851
+ shape({
1852
+ code: S,
1853
+ severity: words2("error", "warning"),
1854
+ file: S,
1855
+ line: I,
1856
+ column: I,
1857
+ message: S
1858
+ })
1859
+ ),
1860
+ summary: shape({ errors: I, warnings: I }),
1861
+ coverage: shape({ files_evaluated: I, files_partial: I, files_not_evaluated: I }),
1862
+ limitations: list(shape({ code: S, file: S, line: I, column: I, message: S })),
1863
+ skipped_inputs: list(),
1864
+ scope: { type: "object" },
1865
+ parser: { type: "object" }
1866
+ },
1867
+ ["evaluation_status", "findings", "summary", "coverage", "limitations"]
1868
+ ),
1869
+ search: shape(
1870
+ {
1871
+ matchCount: I,
1872
+ results: list(
1873
+ shape({ kind: words2("component", "recipe"), name: S, purpose: S, why: S, next: S })
1874
+ )
1875
+ },
1876
+ ["results"]
1877
+ ),
1878
+ "component.detail": shape(
1879
+ {
1880
+ name: S,
1881
+ description: S,
1882
+ use_for: strings,
1883
+ not_for: list(),
1884
+ part_of: S,
1885
+ parts: strings,
1886
+ props: list(
1887
+ shape({ name: S, type: S, values: strings, required: B, notes: { type: "object" } })
1888
+ ),
1889
+ slots: { type: "object" },
1890
+ composable_with: strings,
1891
+ never_inside: strings,
1892
+ allowed_children: strings,
1893
+ example: S,
1894
+ dense: B
1895
+ },
1896
+ ["name", "props", "example", "dense"]
1897
+ ),
1898
+ "component.list": shape({ components: list() }, ["components"]),
1899
+ "component.batch": shape({ components: list() }, ["components"]),
1900
+ instructions: shape(
1901
+ {
1902
+ rules: list(),
1903
+ components: list(),
1904
+ recipes: list()
1905
+ },
1906
+ ["rules", "components", "recipes"]
1907
+ ),
1908
+ init: shape({ files: list() }),
1909
+ doctor: shape({ operations: { type: "object" }, checks: list() }),
1910
+ add: shape({ files: list(), setup: { type: "object" } }),
1911
+ list: shape({ entries: list() })
1912
+ };
1913
+ function shareRepeats(schema) {
1914
+ const count = /* @__PURE__ */ new Map();
1915
+ const tally = (node) => {
1916
+ if (Array.isArray(node)) node.forEach(tally);
1917
+ else if (typeof node === "object" && node !== null) {
1918
+ const text2 = JSON.stringify(node);
1919
+ if (text2.length >= 30) count.set(text2, (count.get(text2) ?? 0) + 1);
1920
+ Object.values(node).forEach(tally);
1921
+ }
1922
+ };
1923
+ tally(schema);
1924
+ const repeated = [...count].filter(([, times]) => times >= 3).map(([text2]) => text2);
1925
+ if (repeated.length === 0) return schema;
1926
+ const names2 = new Map(
1927
+ repeated.map((text2, index) => [text2, index === 0 ? "strings" : `part${index}`])
1928
+ );
1929
+ const defs = {};
1930
+ const rewrite = (node) => {
1931
+ if (Array.isArray(node)) return node.map(rewrite);
1932
+ if (typeof node !== "object" || node === null) return node;
1933
+ const name = names2.get(JSON.stringify(node));
1934
+ if (name !== void 0) {
1935
+ defs[name] = node;
1936
+ return { $ref: `#/$defs/${name}` };
1937
+ }
1938
+ return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, rewrite(value)]));
1939
+ };
1940
+ const rewritten = rewrite(schema);
1941
+ return { ...rewritten, $defs: defs };
1942
+ }
1943
+ function outputSchemaFor(type) {
1944
+ return shareRepeats({
1945
+ type: "object",
1946
+ properties: {
1947
+ type: { const: type },
1948
+ // The four versions and the digest; the docs name them, and so does every answer.
1949
+ identity: { type: "object" },
1950
+ data: SUMMARIES[type]
1951
+ },
1952
+ required: ["identity"],
1953
+ anyOf: [{ required: ["data"] }, { required: ["error"] }]
1954
+ });
1955
+ }
1956
+ var RESULT_SCHEMAS = Object.freeze(
1957
+ Object.fromEntries(
1958
+ [...RESULT_TYPES, "error"].map((type) => [type, schemaFor(type)])
1959
+ )
1960
+ );
1961
+
1962
+ // src/setup.ts
1963
+ function setupFor(dependencies, contract2) {
1964
+ const { setup } = contract2.guidance;
1965
+ const scansPackage = dependencies.includes(setup.package_source.when);
1966
+ return {
1967
+ tailwind: setup.tailwind,
1968
+ packages: setup.packages.map(({ name, range, dev, why }) => ({
1969
+ name,
1970
+ range: range ?? "latest",
1971
+ dev,
1972
+ why
1973
+ })),
1974
+ stylesheet: [
1975
+ ...setup.stylesheet,
1976
+ ...scansPackage ? [{ line: setup.package_source.line, why: setup.package_source.why }] : []
1977
+ ],
1978
+ root_import: { line: setup.root_import, note: setup.root_import_note },
1979
+ not_needed: setup.not_needed
1980
+ };
1981
+ }
1982
+ function renderSetup(setup, copiedPackageImports) {
1983
+ const named = ({ name, range }) => range === "latest" ? name : `${name}@${range}`;
1984
+ const runtime = setup.packages.filter(({ dev: dev2 }) => !dev2).map(named);
1985
+ const dev = setup.packages.filter(({ dev: isDev }) => isDev).map(named);
1986
+ return [
1987
+ `Setup, once per project: ${setup.tailwind}`,
1988
+ ` Install: ${runtime.join(", ")}; as dev dependencies: ${dev.join(", ")}.`,
1989
+ " In the stylesheet that imports tailwindcss, after the import:",
1990
+ ...setup.stylesheet.map(({ line }) => ` ${line}`),
1991
+ ...copiedPackageImports ? [" (the @source path is relative to the stylesheet)"] : [
1992
+ " (source copied into the project is scanned by Tailwind 4 without an @source line)"
1993
+ ],
1994
+ ` At the application root, from JavaScript, before the stylesheet: ${setup.root_import.line}`,
1995
+ ...setup.not_needed.map(({ name, why }) => ` Not needed: ${name}. ${why}`)
1996
+ ];
1997
+ }
1998
+
1143
1999
  // src/agent-commands.ts
1144
- var envelope = (type, data) => ({ apiVersion: 1, type, data });
2000
+ function renderInstall(result, setup) {
2001
+ const lines = [
2002
+ `${result.dryRun ? "Dry run" : "Installed"}: ${result.entries.join(", ")}`,
2003
+ `Output: ${result.outputDirectory}`,
2004
+ ...result.files.map((file) => ` ${file.action.padEnd(9)} ${file.path}`)
2005
+ ];
2006
+ const versioned = (name) => result.ranges[name] === void 0 ? name : `${name}@${result.ranges[name]}`;
2007
+ const runtime = result.dependencies.filter((name) => !result.devDependencies.includes(name));
2008
+ lines.push("", `Dependencies: ${runtime.map(versioned).join(", ")}`);
2009
+ if (result.devDependencies.length > 0)
2010
+ lines.push(
2011
+ `Dev dependencies (types only): ${result.devDependencies.map(versioned).join(", ")}`
2012
+ );
2013
+ lines.push(
2014
+ "",
2015
+ ...renderSetup(setup, setup.stylesheet.length > 1),
2016
+ "",
2017
+ "Install what is missing yourself. No package configuration or stylesheet was changed."
2018
+ );
2019
+ return `${lines.join("\n")}
2020
+ `;
2021
+ }
1145
2022
  function runAgentCommand(command, context) {
1146
2023
  const { contract: contract2, registry: registry2, cwd } = context;
1147
2024
  switch (command.command) {
@@ -1153,14 +2030,15 @@ function runAgentCommand(command, context) {
1153
2030
  ({ name, purpose, parts }) => `${name}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}: ${purpose}
1154
2031
  `
1155
2032
  ).join(""),
1156
- json: envelope("component.list", { components }),
2033
+ json: envelope("component.list", { components }, contract2),
1157
2034
  exitCode: 0
1158
2035
  };
1159
2036
  }
1160
2037
  const details = command.names.map((name) => componentDetail(contract2, name));
2038
+ const data = details.map((detail) => componentData(detail, command.dense));
1161
2039
  return {
1162
2040
  text: details.map((detail) => renderComponent(detail, { dense: command.dense })).join("\n"),
1163
- json: details.length === 1 ? envelope("component.detail", details[0]) : envelope("component.batch", { components: details }),
2041
+ json: data.length === 1 ? envelope("component.detail", data[0], contract2) : envelope("component.batch", { components: data }, contract2),
1164
2042
  exitCode: 0
1165
2043
  };
1166
2044
  }
@@ -1173,7 +2051,11 @@ function runAgentCommand(command, context) {
1173
2051
  ${next}
1174
2052
  `
1175
2053
  ).join(""),
1176
- json: envelope("search", { query: command.query, matchCount: results.length, results }),
2054
+ json: envelope(
2055
+ "search",
2056
+ { query: command.query, limit: command.limit, matchCount: results.length, results },
2057
+ contract2
2058
+ ),
1177
2059
  exitCode: 0
1178
2060
  };
1179
2061
  }
@@ -1184,146 +2066,328 @@ function runAgentCommand(command, context) {
1184
2066
  ...result.files.map(({ path, action }) => ` ${action.padEnd(9)} ${path}`),
1185
2067
  command.remove ? "The block was taken out; the rest of each file is as it was." : "Run oprt init again after an upgrade to refresh the block."
1186
2068
  ];
1187
- return { text: `${lines.join("\n")}
1188
- `, json: envelope("init", result), exitCode: 0 };
2069
+ return {
2070
+ text: `${lines.join("\n")}
2071
+ `,
2072
+ json: envelope("init", result, contract2),
2073
+ exitCode: 0
2074
+ };
1189
2075
  }
1190
2076
  case "check": {
1191
2077
  const report = checkPaths(command.paths, { cwd, contract: contract2 });
1192
- const { errors, warnings } = report.summary;
1193
- const lines = report.findings.map(
1194
- ({ file, line, column, severity, code, message }) => `${file}:${line}:${column} ${severity} ${code} ${message}`
1195
- );
1196
- const count = (amount, word) => `${amount} ${word}${amount === 1 ? "" : "s"}`;
1197
- lines.push(
1198
- `${count(errors, "error")}, ${count(warnings, "warning")} in ${count(report.files, "file")}.`
1199
- );
1200
2078
  return {
1201
- text: `${lines.join("\n")}
1202
- `,
1203
- json: envelope("check", report),
2079
+ text: renderCheckText(report),
2080
+ json: envelope("check", report, contract2),
1204
2081
  // Warnings fail the run only when asked to: they are house rules, or may be native attributes.
1205
- exitCode: errors > 0 || command.strict && warnings > 0 ? 1 : 0
2082
+ exitCode: checkExitCode(report, { strict: command.strict })
1206
2083
  };
1207
2084
  }
1208
2085
  case "doctor": {
1209
- const report = doctor({ cwd, contract: contract2 });
1210
- const lines = report.checks.flatMap(({ status, message, fix }) => [
1211
- `[${status}] ${message}`,
1212
- ...fix === void 0 ? [] : [` fix: ${fix}`]
1213
- ]);
2086
+ const report = doctor({
2087
+ cwd,
2088
+ contract: contract2,
2089
+ ...context.parser ? { parser: context.parser } : {}
2090
+ });
2091
+ const { lookup, check_code: checking } = report.operations;
2092
+ const lines = [
2093
+ "Readiness",
2094
+ ` lookups ${lookup.status.padEnd(8)} ${lookup.message}`,
2095
+ ` code checking ${checking.status.padEnd(8)} ${checking.message}`,
2096
+ ...checking.fix === void 0 ? [] : [` fix: ${checking.fix}`],
2097
+ "",
2098
+ "Setup"
2099
+ ];
2100
+ lines.push(
2101
+ ...report.checks.flatMap(({ status, message, fix }) => [
2102
+ `[${status}] ${message}`,
2103
+ ...fix === void 0 ? [] : [` fix: ${fix}`]
2104
+ ])
2105
+ );
1214
2106
  lines.push(
1215
2107
  `${report.summary.ok} ok, ${report.summary.warn} to look at, ${report.summary.fail} failed.`
1216
2108
  );
1217
2109
  return {
1218
2110
  text: `${lines.join("\n")}
1219
2111
  `,
1220
- json: envelope("doctor", report),
2112
+ json: envelope("doctor", report, contract2),
1221
2113
  exitCode: report.summary.fail > 0 ? 1 : 0
1222
2114
  };
1223
2115
  }
2116
+ case "list": {
2117
+ const lines = [`OPERATOR ${VERSION}`, ""];
2118
+ for (const kind of ["component", "recipe"]) {
2119
+ const entries = registry2.entries.filter((entry) => entry.kind === kind);
2120
+ lines.push(`${kind === "component" ? "Components" : "Recipes"} (${entries.length})`);
2121
+ for (const entry of entries) lines.push(` ${entry.slug.padEnd(22)} ${entry.description}`);
2122
+ lines.push("");
2123
+ }
2124
+ return {
2125
+ text: `${lines.join("\n")}
2126
+ `,
2127
+ json: envelope(
2128
+ "list",
2129
+ { version: VERSION, entries: registry2.entries, ranges: registry2.ranges ?? {} },
2130
+ contract2
2131
+ ),
2132
+ exitCode: 0
2133
+ };
2134
+ }
2135
+ case "add": {
2136
+ const result = installSources(command, { cwd, registry: registry2 });
2137
+ const setup = setupFor(result.dependencies, contract2);
2138
+ return {
2139
+ text: renderInstall(result, setup),
2140
+ json: envelope("add", { ...result, setup }, contract2),
2141
+ exitCode: 0
2142
+ };
2143
+ }
1224
2144
  }
1225
2145
  }
1226
2146
 
1227
2147
  // src/mcp.ts
1228
2148
  var import_node_readline = require("readline");
2149
+
2150
+ // src/validate.ts
2151
+ var KNOWN = /* @__PURE__ */ new Set([
2152
+ "type",
2153
+ "description",
2154
+ "default",
2155
+ "enum",
2156
+ "minimum",
2157
+ "maximum",
2158
+ "minLength",
2159
+ "maxLength"
2160
+ ]);
2161
+ var typeName = (value) => value === null ? "null" : Array.isArray(value) ? "an array" : typeof value === "object" ? "an object" : typeof value === "number" ? "a number" : typeof value === "string" ? "a string" : typeof value === "boolean" ? "a boolean" : typeof value;
2162
+ function describeValue(value) {
2163
+ if (typeof value === "string")
2164
+ return `${JSON.stringify(value.length > 40 ? `${value.slice(0, 37)}...` : value)} (a string)`;
2165
+ if (typeof value === "number") return Number.isNaN(value) ? "NaN" : String(value);
2166
+ if (value === null || typeof value === "boolean" || value === void 0) return String(value);
2167
+ return `${typeName(value)} (${JSON.stringify(value)?.slice(0, 40) ?? ""})`;
2168
+ }
2169
+ function expectation(schema) {
2170
+ switch (schema.type) {
2171
+ case "boolean":
2172
+ return "true or false";
2173
+ case "integer":
2174
+ case "number": {
2175
+ const noun = schema.type === "integer" ? "a whole number" : "a number";
2176
+ if (schema.minimum !== void 0 && schema.maximum !== void 0)
2177
+ return `${noun} from ${schema.minimum} to ${schema.maximum}`;
2178
+ if (schema.minimum !== void 0) return `${noun} of at least ${schema.minimum}`;
2179
+ if (schema.maximum !== void 0) return `${noun} of at most ${schema.maximum}`;
2180
+ return noun;
2181
+ }
2182
+ case "string": {
2183
+ if (schema.enum !== void 0) return `one of ${schema.enum.map(String).join(", ")}`;
2184
+ const { minLength, maxLength } = schema;
2185
+ if (minLength !== void 0 && maxLength !== void 0)
2186
+ return `a string of ${minLength} to ${maxLength.toLocaleString("en-US")} characters`;
2187
+ if (maxLength !== void 0)
2188
+ return `a string of at most ${maxLength.toLocaleString("en-US")} characters`;
2189
+ return "a string";
2190
+ }
2191
+ }
2192
+ }
2193
+ function accepts(schema, value) {
2194
+ switch (schema.type) {
2195
+ case "boolean":
2196
+ return typeof value === "boolean";
2197
+ case "string":
2198
+ return typeof value === "string" && (schema.minLength === void 0 || value.length >= schema.minLength) && (schema.maxLength === void 0 || value.length <= schema.maxLength) && (schema.enum === void 0 || schema.enum.includes(value));
2199
+ case "integer":
2200
+ case "number":
2201
+ return typeof value === "number" && Number.isFinite(value) && (schema.type === "number" || Number.isInteger(value)) && (schema.minimum === void 0 || value >= schema.minimum) && (schema.maximum === void 0 || value <= schema.maximum);
2202
+ }
2203
+ }
2204
+ function validateArguments(tool, schema, args) {
2205
+ for (const [field, property] of Object.entries(schema.properties))
2206
+ for (const key of Object.keys(property))
2207
+ if (!KNOWN.has(key))
2208
+ throw new Error(`${tool}.${field}: the schema uses "${key}", which is not enforced.`);
2209
+ if (typeof args !== "object" || args === null || Array.isArray(args))
2210
+ return `${tool}: arguments must be an object.`;
2211
+ const given = args;
2212
+ const required = schema.required ?? [];
2213
+ const blank = (value) => typeof value === "string" && value.trim() === "";
2214
+ const missing = [];
2215
+ const wrong = [];
2216
+ const unknown = [];
2217
+ for (const field of required)
2218
+ if (given[field] === void 0 || blank(given[field])) missing.push(field);
2219
+ for (const [field, value] of Object.entries(given)) {
2220
+ const property = Object.hasOwn(schema.properties, field) ? schema.properties[field] : void 0;
2221
+ if (property === void 0) unknown.push(field);
2222
+ else if (value !== void 0 && !(required.includes(field) && blank(value))) {
2223
+ if (!accepts(property, value))
2224
+ wrong.push(`${field} must be ${expectation(property)}, but got ${describeValue(value)}`);
2225
+ }
2226
+ }
2227
+ const reject = schema.additionalProperties === false ? unknown : [];
2228
+ const names2 = Object.keys(schema.properties);
2229
+ if (missing.length === 1 && wrong.length === 0 && reject.length === 0)
2230
+ return `${tool} needs ${missing[0]}, ${expectation({ type: schema.properties[missing[0]].type })}.`;
2231
+ const fragments = [
2232
+ ...missing.map(
2233
+ (field) => `${field} is required (${expectation({ type: schema.properties[field].type })})`
2234
+ ),
2235
+ ...wrong,
2236
+ ...reject.map(
2237
+ (field) => `unknown argument ${JSON.stringify(field)}. It takes${names2.length > 0 ? `: ${names2.join(", ")}` : " none"}`
2238
+ )
2239
+ ];
2240
+ return fragments.length === 0 ? null : `${tool}: ${fragments.join("; ")}.`;
2241
+ }
2242
+
2243
+ // src/mcp.ts
1229
2244
  var VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
1230
2245
  var text = (value) => ({ type: "text", text: value });
1231
- var string = (description) => ({ type: "string", description });
2246
+ var LIMITS = { query: 500, name: 100, code: 2e5 };
2247
+ var ANSWERS = {
2248
+ search: "search",
2249
+ get_component: "component.detail",
2250
+ list_components: "component.list",
2251
+ get_instructions: "instructions",
2252
+ check_code: "check"
2253
+ };
2254
+ var readOnly = { readOnlyHint: true };
2255
+ var define = (tool) => ({
2256
+ ...tool,
2257
+ outputSchema: outputSchemaFor(ANSWERS[tool.name]),
2258
+ annotations: readOnly
2259
+ });
1232
2260
  var TOOLS = [
1233
- {
2261
+ define({
1234
2262
  name: "search",
1235
- description: "Find the OPERATOR component or recipe that answers a need, such as 'show a status' or 'table with selection'. Call this before writing interface code.",
2263
+ description: "Find the OPERATOR component or recipe for a need, such as 'show a status' or 'table with selection'. Call before writing interface code.",
1236
2264
  inputSchema: {
1237
2265
  type: "object",
1238
2266
  properties: {
1239
- query: string("The need, in plain words."),
1240
- limit: { type: "number", description: "How many results to return. Default 10." }
2267
+ query: {
2268
+ type: "string",
2269
+ minLength: 1,
2270
+ maxLength: LIMITS.query,
2271
+ description: "The need, in plain words."
2272
+ },
2273
+ limit: {
2274
+ type: "integer",
2275
+ minimum: 1,
2276
+ maximum: SEARCH_LIMIT_MAX,
2277
+ default: SEARCH_LIMIT_DEFAULT,
2278
+ description: `Results to return, 1 to ${SEARCH_LIMIT_MAX}.`
2279
+ }
1241
2280
  },
1242
- required: ["query"]
2281
+ required: ["query"],
2282
+ additionalProperties: false
1243
2283
  }
1244
- },
1245
- {
2284
+ }),
2285
+ define({
1246
2286
  name: "get_component",
1247
- description: "One OPERATOR component: what it is for, what to use instead, every prop and option value, its nesting rules and an example. Call this before using a component.",
2287
+ description: "One component's contract: what it is for, what to use instead, every prop and option value, nesting rules, an example. Call before using a component.",
1248
2288
  inputSchema: {
1249
2289
  type: "object",
1250
2290
  properties: {
1251
- name: string("The component's name, such as Button or DataGrid."),
2291
+ name: {
2292
+ type: "string",
2293
+ minLength: 1,
2294
+ maxLength: LIMITS.name,
2295
+ description: "Component name, such as Button."
2296
+ },
1252
2297
  dense: {
1253
2298
  type: "boolean",
1254
- description: "The short form, which is the default. Pass false for full descriptions."
2299
+ default: true,
2300
+ description: "Compact form (every constraint, one sentence per prop). False: full prose."
1255
2301
  }
1256
2302
  },
1257
- required: ["name"]
2303
+ required: ["name"],
2304
+ additionalProperties: false
1258
2305
  }
1259
- },
1260
- {
2306
+ }),
2307
+ define({
1261
2308
  name: "list_components",
1262
2309
  description: "Every OPERATOR component in one line each, with its parts.",
1263
- inputSchema: { type: "object", properties: {} }
1264
- },
1265
- {
2310
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
2311
+ }),
2312
+ define({
1266
2313
  name: "get_instructions",
1267
- description: "OPERATOR's house rules, its component index and its recipes: what to read once before building with it.",
1268
- inputSchema: { type: "object", properties: {} }
1269
- },
1270
- {
2314
+ description: "OPERATOR's house rules, component index and recipes: read once before building.",
2315
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
2316
+ }),
2317
+ define({
1271
2318
  name: "check_code",
1272
- description: "Check TSX code against OPERATOR's contract: option values, nesting, required props and the house rules. Call this on what you wrote before you finish.",
2319
+ description: "Check TSX against OPERATOR's contract: option values, nesting, required props, house rules. Call before you finish. Read evaluation_status: only evaluated_clean means every file was read in full with nothing found. Types and rendering are not checked.",
1273
2320
  inputSchema: {
1274
2321
  type: "object",
1275
- properties: { code: string("The TSX source of one file.") },
1276
- required: ["code"]
2322
+ properties: {
2323
+ code: {
2324
+ type: "string",
2325
+ minLength: 1,
2326
+ maxLength: LIMITS.code,
2327
+ description: "The TSX source of one file."
2328
+ }
2329
+ },
2330
+ required: ["code"],
2331
+ additionalProperties: false
1277
2332
  }
1278
- }
2333
+ })
1279
2334
  ];
1280
2335
  function runTool(name, args, context) {
1281
2336
  const { contract: contract2, registry: registry2 } = context;
1282
- const required = (key) => {
1283
- const value = args[key];
1284
- if (typeof value !== "string" || value.trim() === "")
1285
- throw new Error(`${name} needs ${key}, a string.`);
1286
- return value;
1287
- };
2337
+ const definition = TOOLS.find((tool) => tool.name === name);
2338
+ if (definition === void 0) throw new UnknownTool(name);
2339
+ const problem = validateArguments(name, definition.inputSchema, args);
2340
+ if (problem !== null) throw new CliError(problem, 2, { code: "ERR_INVALID_ARGUMENT" });
1288
2341
  switch (name) {
1289
2342
  case "search": {
1290
- const query = required("query");
1291
- const results = search(
1292
- contract2,
1293
- registry2,
1294
- query,
1295
- typeof args.limit === "number" ? args.limit : 10
1296
- );
1297
- return results.length === 0 ? `Nothing matches ${JSON.stringify(query)}. Call list_components to see what there is.` : results.map(
1298
- ({ kind, name: found, purpose, why, next }) => `${found} (${kind}): ${purpose}${why === void 0 ? "" : ` [${why}]`}
2343
+ const query = args["query"];
2344
+ const limit = args["limit"] ?? SEARCH_LIMIT_DEFAULT;
2345
+ const results = search(contract2, registry2, query, limit);
2346
+ return {
2347
+ text: results.length === 0 ? `Nothing matches ${JSON.stringify(query)}. Call list_components to see what there is.` : results.map(
2348
+ ({ kind, name: found, purpose, why, next }) => `${found} (${kind}): ${purpose}${why === void 0 ? "" : ` [${why}]`}
1299
2349
  ${next}`
1300
- ).join("\n");
2350
+ ).join("\n"),
2351
+ structured: envelope(
2352
+ "search",
2353
+ { query, limit, matchCount: results.length, results },
2354
+ contract2
2355
+ )
2356
+ };
2357
+ }
2358
+ case "get_component": {
2359
+ const dense = args["dense"] ?? true;
2360
+ const detail = componentDetail(contract2, args["name"]);
2361
+ return {
2362
+ text: renderComponent(detail, { dense }),
2363
+ structured: envelope("component.detail", componentData(detail, dense), contract2)
2364
+ };
2365
+ }
2366
+ case "list_components": {
2367
+ const components = componentIndex(contract2);
2368
+ return {
2369
+ text: components.map(
2370
+ ({ name: component, purpose, parts }) => `${component}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}: ${purpose}`
2371
+ ).join("\n"),
2372
+ structured: envelope("component.list", { components }, contract2)
2373
+ };
1301
2374
  }
1302
- case "get_component":
1303
- return renderComponent(componentDetail(contract2, required("name")), {
1304
- dense: args.dense !== false
1305
- });
1306
- case "list_components":
1307
- return componentIndex(contract2).map(
1308
- ({ name: component, purpose, parts }) => `${component}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}: ${purpose}`
1309
- ).join("\n");
1310
2375
  case "get_instructions":
1311
- return renderAgentBlock(contract2).split("\n").filter((line) => line !== START && line !== END && !line.startsWith("<!-- Written by")).join("\n");
2376
+ return {
2377
+ text: renderAgentBlock(contract2).split("\n").filter((line) => line !== START && line !== END && !line.startsWith("<!-- Written by")).join("\n"),
2378
+ structured: envelope("instructions", instructionsData(contract2), contract2)
2379
+ };
1312
2380
  case "check_code": {
1313
- const code = required("code");
1314
- const ts = context.typescript === void 0 ? findTypeScript(context.cwd ?? process.cwd()) : context.typescript;
1315
- if (ts === null)
1316
- throw new Error("check_code reads code with TypeScript, and found none installed.");
1317
- const findings = checkSource(ts, contract2, "snippet.tsx", code);
1318
- if (findings.length === 0) return "No findings.";
1319
- const errors = findings.filter(({ severity }) => severity === "error").length;
1320
- const count = (amount, word) => `${amount} ${word}${amount === 1 ? "" : "s"}`;
1321
- return [
1322
- ...findings.map(
1323
- ({ line, column, severity, code: finding, message }) => `${line}:${column} ${severity} ${finding} ${message}`
1324
- ),
1325
- `${count(errors, "error")}, ${count(findings.length - errors, "warning")}.`
1326
- ].join("\n");
2381
+ const { typescript, parser } = resolveParser(
2382
+ context.cwd ?? process.cwd(),
2383
+ context.typescript,
2384
+ "check_code"
2385
+ );
2386
+ const report = buildCheckReport(
2387
+ [checkSource(typescript, contract2, "snippet.tsx", args["code"])],
2388
+ { parser }
2389
+ );
2390
+ return { text: renderCheckText(report), structured: envelope("check", report, contract2) };
1327
2391
  }
1328
2392
  default:
1329
2393
  throw new UnknownTool(name);
@@ -1337,24 +2401,28 @@ var UnknownTool = class extends Error {
1337
2401
  }
1338
2402
  };
1339
2403
  function handleMessage(raw, context) {
1340
- const failure = (id2, code, message) => ({
2404
+ const failure2 = (id2, code, message) => ({
1341
2405
  jsonrpc: "2.0",
1342
2406
  id: id2 ?? null,
1343
2407
  error: { code, message }
1344
2408
  });
1345
- if (typeof raw !== "object" || raw === null) return failure(null, -32700, "Parse error");
2409
+ if (typeof raw !== "object" || raw === null) return failure2(null, -32700, "Parse error");
1346
2410
  const { id, method, params } = raw;
1347
- if (typeof method !== "string") return failure(id, -32600, "Invalid request");
2411
+ if (typeof method !== "string") return failure2(id, -32600, "Invalid request");
1348
2412
  if (id === void 0) return null;
1349
2413
  const result = (value) => ({ jsonrpc: "2.0", id, result: value });
1350
2414
  switch (method) {
1351
2415
  case "initialize": {
1352
2416
  const asked = params?.protocolVersion;
2417
+ const identity = identityOf(context.contract);
1353
2418
  return result({
1354
2419
  protocolVersion: asked !== void 0 && VERSIONS.includes(asked) ? asked : VERSIONS[0],
1355
2420
  capabilities: { tools: {} },
1356
- serverInfo: { name: "oprt", version: context.contract.version },
1357
- instructions: "OPERATOR design system. Call get_instructions once, search for the component that answers a need, get_component before using one, and check_code before you finish."
2421
+ // The program's version, not the contract's: the contract has its own, in `_meta` and in
2422
+ // the identity of every result.
2423
+ serverInfo: { name: "oprt", version: VERSION },
2424
+ instructions: "OPERATOR design system. Call get_instructions once, search for the component that answers a need, get_component before using one, and check_code before you finish. check_code says how much it could read: only evaluated_clean means the code was read in full with nothing found.",
2425
+ _meta: { "dev.oprt/identity": identity }
1358
2426
  });
1359
2427
  }
1360
2428
  case "ping":
@@ -1364,17 +2432,23 @@ function handleMessage(raw, context) {
1364
2432
  case "tools/call": {
1365
2433
  const { name, arguments: args } = params ?? {};
1366
2434
  try {
1367
- return result({ content: [text(runTool(String(name), args ?? {}, context))] });
2435
+ const output = runTool(String(name), args ?? {}, context);
2436
+ return result({
2437
+ content: [text(output.text)],
2438
+ structuredContent: output.structured
2439
+ });
1368
2440
  } catch (error) {
1369
- if (error instanceof UnknownTool) return failure(id, -32602, error.message);
2441
+ if (error instanceof UnknownTool) return failure2(id, -32602, error.message);
2442
+ const failed = errorEnvelope(error, context.contract);
1370
2443
  return result({
1371
- content: [text(error instanceof Error ? error.message : String(error))],
2444
+ content: [text(failed.error)],
2445
+ structuredContent: failed,
1372
2446
  isError: true
1373
2447
  });
1374
2448
  }
1375
2449
  }
1376
2450
  default:
1377
- return failure(id, -32601, `Method not found: ${method}`);
2451
+ return failure2(id, -32601, `Method not found: ${method}`);
1378
2452
  }
1379
2453
  }
1380
2454
  function serve(input, output, context) {
@@ -1395,10 +2469,9 @@ function serve(input, output, context) {
1395
2469
  }
1396
2470
 
1397
2471
  // src/index.ts
1398
- var VERSION = package_default.version;
1399
2472
  function loadRegistry() {
1400
- const here = (0, import_node_path6.dirname)((0, import_node_url3.fileURLToPath)(importMetaUrl));
1401
- const candidates = [(0, import_node_path6.join)(here, "registry.json"), (0, import_node_path6.join)(here, "../generated/registry.json")];
2473
+ const here = (0, import_node_path7.dirname)((0, import_node_url3.fileURLToPath)(importMetaUrl));
2474
+ const candidates = [(0, import_node_path7.join)(here, "registry.json"), (0, import_node_path7.join)(here, "../generated/registry.json")];
1402
2475
  const found = candidates.find((candidate) => (0, import_node_fs6.existsSync)(candidate));
1403
2476
  if (found === void 0) {
1404
2477
  throw new Error(
@@ -1420,26 +2493,38 @@ function install(options, context = {}) {
1420
2493
  0 && (module.exports = {
1421
2494
  CliError,
1422
2495
  DEFAULT_OUT,
2496
+ RESULT_SCHEMAS,
2497
+ RESULT_SCHEMA_VERSION,
2498
+ RESULT_TYPES,
1423
2499
  TOOLS,
1424
2500
  VERSION,
1425
2501
  applyBlock,
1426
2502
  blockVersion,
2503
+ buildCheckReport,
2504
+ checkExitCode,
1427
2505
  checkPaths,
1428
2506
  checkSource,
1429
2507
  componentDetail,
1430
2508
  componentIndex,
1431
2509
  contract,
2510
+ contractDigest,
1432
2511
  doctor,
2512
+ envelope,
2513
+ errorEnvelope,
1433
2514
  handleMessage,
2515
+ identityOf,
1434
2516
  init,
1435
2517
  install,
2518
+ outputSchemaFor,
1436
2519
  parseArgs,
1437
2520
  registry,
1438
2521
  removeBlock,
1439
2522
  renderAgentBlock,
2523
+ renderCheckText,
1440
2524
  renderComponent,
1441
2525
  resolveEntries,
1442
2526
  runAgentCommand,
2527
+ schemaFor,
1443
2528
  search,
1444
2529
  serve
1445
2530
  });