@runcontext/cli 0.2.1 → 0.3.1

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.js CHANGED
@@ -1,17 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command11 } from "commander";
4
+ import { Command as Command15 } from "commander";
5
5
 
6
6
  // src/commands/lint.ts
7
7
  import { Command } from "commander";
8
8
  import chalk2 from "chalk";
9
9
  import path from "path";
10
+ import { readFileSync, writeFileSync } from "fs";
10
11
  import {
11
12
  compile,
12
- loadConfig,
13
+ loadConfigAsync,
13
14
  LintEngine,
14
- ALL_RULES
15
+ ALL_RULES,
16
+ filterByDirectives,
17
+ applyFixes,
18
+ loadPlugins,
19
+ computeCacheHash,
20
+ readCache,
21
+ writeCache
15
22
  } from "@runcontext/core";
16
23
 
17
24
  // src/formatters/pretty.ts
@@ -89,22 +96,230 @@ function formatJson(data) {
89
96
  return JSON.stringify(data, null, 2);
90
97
  }
91
98
 
99
+ // src/formatters/sarif.ts
100
+ function mapSeverity(severity) {
101
+ switch (severity) {
102
+ case "error":
103
+ return "error";
104
+ case "warning":
105
+ return "warning";
106
+ default:
107
+ return "note";
108
+ }
109
+ }
110
+ function formatSarif(diagnostics) {
111
+ const ruleMap = /* @__PURE__ */ new Map();
112
+ for (const d of diagnostics) {
113
+ if (!ruleMap.has(d.ruleId)) {
114
+ ruleMap.set(d.ruleId, {
115
+ id: d.ruleId,
116
+ shortDescription: { text: d.message }
117
+ });
118
+ }
119
+ }
120
+ const results = diagnostics.map((d) => ({
121
+ ruleId: d.ruleId,
122
+ level: mapSeverity(d.severity),
123
+ message: { text: d.message },
124
+ locations: [
125
+ {
126
+ physicalLocation: {
127
+ artifactLocation: { uri: d.location.file },
128
+ region: {
129
+ startLine: d.location.line,
130
+ startColumn: d.location.column
131
+ }
132
+ }
133
+ }
134
+ ]
135
+ }));
136
+ const sarif = {
137
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
138
+ version: "2.1.0",
139
+ runs: [
140
+ {
141
+ tool: {
142
+ driver: {
143
+ name: "ContextKit",
144
+ version: "0.2.1",
145
+ informationUri: "https://github.com/erickittelson/ContextKit",
146
+ rules: Array.from(ruleMap.values())
147
+ }
148
+ },
149
+ results
150
+ }
151
+ ]
152
+ };
153
+ return JSON.stringify(sarif, null, 2);
154
+ }
155
+
156
+ // src/formatters/github.ts
157
+ function formatGitHub(diagnostics) {
158
+ if (diagnostics.length === 0) {
159
+ return "";
160
+ }
161
+ const lines = [];
162
+ for (const d of diagnostics) {
163
+ const level = d.severity === "error" ? "error" : "warning";
164
+ lines.push(
165
+ `::${level} file=${d.location.file},line=${d.location.line},col=${d.location.column},title=${d.ruleId}::${d.message}`
166
+ );
167
+ }
168
+ return lines.join("\n");
169
+ }
170
+
171
+ // src/formatters/junit.ts
172
+ function escapeXml(str) {
173
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
174
+ }
175
+ function formatJUnit(diagnostics) {
176
+ const byFile = /* @__PURE__ */ new Map();
177
+ for (const d of diagnostics) {
178
+ const file = d.location.file;
179
+ if (!byFile.has(file)) {
180
+ byFile.set(file, []);
181
+ }
182
+ byFile.get(file).push(d);
183
+ }
184
+ const lines = [];
185
+ lines.push('<?xml version="1.0" encoding="UTF-8"?>');
186
+ lines.push(
187
+ `<testsuites name="ContextKit" tests="${diagnostics.length}" failures="${diagnostics.length}">`
188
+ );
189
+ for (const [file, diags] of byFile) {
190
+ lines.push(
191
+ ` <testsuite name="${escapeXml(file)}" tests="${diags.length}" failures="${diags.length}">`
192
+ );
193
+ for (const d of diags) {
194
+ const name = `${d.ruleId} (${d.location.line}:${d.location.column})`;
195
+ lines.push(` <testcase name="${escapeXml(name)}" classname="${escapeXml(file)}">`);
196
+ lines.push(
197
+ ` <failure message="${escapeXml(d.message)}" type="${d.severity}">${escapeXml(d.ruleId)}: ${escapeXml(d.message)} at ${escapeXml(file)}:${d.location.line}:${d.location.column}</failure>`
198
+ );
199
+ lines.push(" </testcase>");
200
+ }
201
+ lines.push(" </testsuite>");
202
+ }
203
+ lines.push("</testsuites>");
204
+ return lines.join("\n");
205
+ }
206
+
92
207
  // src/commands/lint.ts
93
- var lintCommand = new Command("lint").description("Run all lint rules against context files").option("--context-dir <path>", "Path to context directory").option("--format <type>", "Output format: pretty or json", "pretty").action(async (opts) => {
208
+ var VALID_FORMATS = ["pretty", "json", "sarif", "github", "junit"];
209
+ function detectFormat() {
210
+ if (process.env.GITHUB_ACTIONS) return "github";
211
+ if (process.env.CI) return "json";
212
+ return "pretty";
213
+ }
214
+ function collectRule(value, previous) {
215
+ const lastColon = value.lastIndexOf(":");
216
+ if (lastColon === -1) {
217
+ throw new Error(`Invalid --rule format: "${value}". Expected "ruleId:severity" (e.g., "governance/grain-required:error")`);
218
+ }
219
+ const ruleId = value.slice(0, lastColon);
220
+ const severity = value.slice(lastColon + 1);
221
+ if (!["error", "warning", "off"].includes(severity)) {
222
+ throw new Error(`Invalid severity "${severity}" in --rule "${value}". Must be error, warning, or off.`);
223
+ }
224
+ previous[ruleId] = severity;
225
+ return previous;
226
+ }
227
+ function formatOutput(diagnostics, format) {
228
+ switch (format) {
229
+ case "json":
230
+ return formatJson(diagnostics);
231
+ case "sarif":
232
+ return formatSarif(diagnostics);
233
+ case "github":
234
+ return formatGitHub(diagnostics);
235
+ case "junit":
236
+ return formatJUnit(diagnostics);
237
+ case "pretty":
238
+ default:
239
+ return formatDiagnostics(diagnostics);
240
+ }
241
+ }
242
+ var lintCommand = new Command("lint").description("Run all lint rules against context files").option("--context-dir <path>", "Path to context directory").option("--format <type>", `Output format: ${VALID_FORMATS.join(", ")}`).option("--max-warnings <count>", "Exit with error if warning count exceeds this threshold", parseInt).option("--output-file <path>", "Write formatted output to a file").option("--rule <ruleId:severity>", "Override rule severity (repeatable)", collectRule, {}).option("--fix", "Automatically fix problems").option("--fix-dry-run", "Show what --fix would change without writing").option("--cache", "Only lint changed files (uses .contextkit-cache)").option("--no-cache", "Bypass the lint cache").action(async (opts) => {
94
243
  try {
95
- const config = loadConfig(process.cwd());
244
+ const config = await loadConfigAsync(process.cwd());
96
245
  const contextDir = opts.contextDir ? path.resolve(opts.contextDir) : path.resolve(config.context_dir);
97
- const { graph, diagnostics: compileDiags } = await compile({
246
+ const rootDir = process.cwd();
247
+ const format = opts.format ? VALID_FORMATS.includes(opts.format) ? opts.format : "pretty" : detectFormat();
248
+ const useCache = opts.cache === true;
249
+ if (useCache) {
250
+ const configContent = JSON.stringify(config) + JSON.stringify(opts.rule ?? {});
251
+ const hash = computeCacheHash(contextDir, configContent);
252
+ const cached = readCache(rootDir, hash);
253
+ if (cached) {
254
+ const output2 = formatOutput(cached, format);
255
+ if (opts.outputFile) {
256
+ writeFileSync(path.resolve(opts.outputFile), output2, "utf-8");
257
+ const ec = cached.filter((d) => d.severity === "error").length;
258
+ const wc = cached.filter((d) => d.severity === "warning").length;
259
+ console.log(`Results written to ${opts.outputFile} (${ec} error(s), ${wc} warning(s)) (cached)`);
260
+ } else {
261
+ console.log(output2);
262
+ }
263
+ const hasErrors2 = cached.some((d) => d.severity === "error");
264
+ if (hasErrors2) process.exit(1);
265
+ return;
266
+ }
267
+ }
268
+ const { graph, diagnostics: compileDiags, directives } = await compile({
98
269
  contextDir,
99
- config
270
+ config,
271
+ rootDir
100
272
  });
101
- const overrides = config.lint?.severity_overrides;
102
- const engine = new LintEngine(overrides);
273
+ const configOverrides = config.lint?.severity_overrides ?? {};
274
+ const cliOverrides = opts.rule;
275
+ const overrides = {
276
+ ...configOverrides,
277
+ ...cliOverrides
278
+ };
279
+ const engine = new LintEngine(Object.keys(overrides).length > 0 ? overrides : void 0);
103
280
  for (const rule of ALL_RULES) {
104
281
  engine.register(rule);
105
282
  }
283
+ if (config.plugins && config.plugins.length > 0) {
284
+ const pluginRules = await loadPlugins(config.plugins);
285
+ for (const rule of pluginRules) {
286
+ engine.register(rule);
287
+ }
288
+ }
106
289
  const lintDiags = engine.run(graph);
107
- const allDiags = [...compileDiags, ...lintDiags];
290
+ let allDiags = filterByDirectives(
291
+ [...compileDiags, ...lintDiags],
292
+ directives
293
+ );
294
+ if (opts.fix || opts.fixDryRun) {
295
+ const fixable = allDiags.filter((d) => d.fixable && d.fix);
296
+ if (fixable.length > 0) {
297
+ const fixes = applyFixes(fixable, (filePath) => readFileSync(filePath, "utf-8"));
298
+ if (opts.fixDryRun) {
299
+ console.log(chalk2.blue(`Would fix ${fixable.length} issue(s) in ${fixes.size} file(s):`));
300
+ for (const [file] of fixes) {
301
+ console.log(chalk2.gray(` ${file}`));
302
+ }
303
+ console.log("");
304
+ } else {
305
+ for (const [file, content] of fixes) {
306
+ writeFileSync(file, content, "utf-8");
307
+ }
308
+ console.log(chalk2.green(`Fixed ${fixable.length} issue(s) in ${fixes.size} file(s).`));
309
+ const { graph: reGraph, diagnostics: reCompileDiags, directives: reDirs } = await compile({
310
+ contextDir,
311
+ config,
312
+ rootDir: process.cwd()
313
+ });
314
+ const reEngine = new LintEngine(Object.keys(overrides).length > 0 ? overrides : void 0);
315
+ for (const rule of ALL_RULES) {
316
+ reEngine.register(rule);
317
+ }
318
+ const reLintDiags = reEngine.run(reGraph);
319
+ allDiags = filterByDirectives([...reCompileDiags, ...reLintDiags], reDirs);
320
+ }
321
+ }
322
+ }
108
323
  if (config.minimum_tier) {
109
324
  const tierOrder = ["none", "bronze", "silver", "gold"];
110
325
  const minIdx = tierOrder.indexOf(config.minimum_tier);
@@ -121,10 +336,26 @@ var lintCommand = new Command("lint").description("Run all lint rules against co
121
336
  }
122
337
  }
123
338
  }
124
- if (opts.format === "json") {
125
- console.log(formatJson(allDiags));
339
+ if (useCache) {
340
+ const configContent = JSON.stringify(config) + JSON.stringify(opts.rule ?? {});
341
+ const hash = computeCacheHash(contextDir, configContent);
342
+ writeCache(rootDir, hash, allDiags);
343
+ }
344
+ const output = formatOutput(allDiags, format);
345
+ if (opts.outputFile) {
346
+ writeFileSync(path.resolve(opts.outputFile), output, "utf-8");
347
+ const errorCount = allDiags.filter((d) => d.severity === "error").length;
348
+ const warnCount2 = allDiags.filter((d) => d.severity === "warning").length;
349
+ console.log(`Results written to ${opts.outputFile} (${errorCount} error(s), ${warnCount2} warning(s))`);
126
350
  } else {
127
- console.log(formatDiagnostics(allDiags));
351
+ console.log(output);
352
+ }
353
+ const warnCount = allDiags.filter((d) => d.severity === "warning").length;
354
+ if (opts.maxWarnings !== void 0 && !isNaN(opts.maxWarnings) && warnCount > opts.maxWarnings) {
355
+ console.error(
356
+ chalk2.red(`Too many warnings: ${warnCount} (max allowed: ${opts.maxWarnings})`)
357
+ );
358
+ process.exit(1);
128
359
  }
129
360
  const hasErrors = allDiags.some((d) => d.severity === "error");
130
361
  if (hasErrors) {
@@ -141,13 +372,13 @@ import { Command as Command2 } from "commander";
141
372
  import chalk3 from "chalk";
142
373
  import path2 from "path";
143
374
  import fs from "fs";
144
- import { compile as compile2, loadConfig as loadConfig2, emitManifest } from "@runcontext/core";
375
+ import { compile as compile2, loadConfig, emitManifest } from "@runcontext/core";
145
376
  var buildCommand = new Command2("build").description("Compile context files and emit manifest JSON").option("--context-dir <path>", "Path to context directory").option("--output-dir <path>", "Path to output directory").option("--format <type>", "Output format: pretty or json", "pretty").action(async (opts) => {
146
377
  try {
147
- const config = loadConfig2(process.cwd());
378
+ const config = loadConfig(process.cwd());
148
379
  const contextDir = opts.contextDir ? path2.resolve(opts.contextDir) : path2.resolve(config.context_dir);
149
380
  const outputDir = opts.outputDir ? path2.resolve(opts.outputDir) : path2.resolve(config.output_dir);
150
- const { graph, diagnostics } = await compile2({ contextDir, config });
381
+ const { graph, diagnostics } = await compile2({ contextDir, config, rootDir: process.cwd() });
151
382
  const errors = diagnostics.filter((d) => d.severity === "error");
152
383
  if (errors.length > 0) {
153
384
  if (opts.format === "json") {
@@ -181,12 +412,12 @@ var buildCommand = new Command2("build").description("Compile context files and
181
412
  import { Command as Command3 } from "commander";
182
413
  import chalk4 from "chalk";
183
414
  import path3 from "path";
184
- import { compile as compile3, loadConfig as loadConfig3, computeTier } from "@runcontext/core";
415
+ import { compile as compile3, loadConfig as loadConfig2, computeTier } from "@runcontext/core";
185
416
  var tierCommand = new Command3("tier").description("Show tier scorecard for one or all models").argument("[model-name]", "Specific model name to check").option("--context-dir <path>", "Path to context directory").option("--format <type>", "Output format: pretty or json", "pretty").action(async (modelName, opts) => {
186
417
  try {
187
- const config = loadConfig3(process.cwd());
418
+ const config = loadConfig2(process.cwd());
188
419
  const contextDir = opts.contextDir ? path3.resolve(opts.contextDir) : path3.resolve(config.context_dir);
189
- const { graph } = await compile3({ contextDir, config });
420
+ const { graph } = await compile3({ contextDir, config, rootDir: process.cwd() });
190
421
  let scores;
191
422
  if (modelName) {
192
423
  if (!graph.models.has(modelName)) {
@@ -227,12 +458,12 @@ var tierCommand = new Command3("tier").description("Show tier scorecard for one
227
458
  import { Command as Command4 } from "commander";
228
459
  import chalk5 from "chalk";
229
460
  import path4 from "path";
230
- import { compile as compile4, loadConfig as loadConfig4 } from "@runcontext/core";
461
+ import { compile as compile4, loadConfig as loadConfig3 } from "@runcontext/core";
231
462
  var explainCommand = new Command4("explain").description("Look up models, terms, or owners by name and show details").argument("<name>", "Name of a model, term, or owner to look up").option("--context-dir <path>", "Path to context directory").option("--format <type>", "Output format: pretty or json", "pretty").action(async (name, opts) => {
232
463
  try {
233
- const config = loadConfig4(process.cwd());
464
+ const config = loadConfig3(process.cwd());
234
465
  const contextDir = opts.contextDir ? path4.resolve(opts.contextDir) : path4.resolve(config.context_dir);
235
- const { graph } = await compile4({ contextDir, config });
466
+ const { graph } = await compile4({ contextDir, config, rootDir: process.cwd() });
236
467
  const results = [];
237
468
  if (graph.models.has(name)) {
238
469
  results.push({ type: "model", name, data: graph.models.get(name) });
@@ -277,25 +508,352 @@ var explainCommand = new Command4("explain").description("Look up models, terms,
277
508
  });
278
509
 
279
510
  // src/commands/fix.ts
511
+ import { Command as Command7 } from "commander";
512
+ import chalk8 from "chalk";
513
+ import path7 from "path";
514
+ import fs2 from "fs";
515
+ import {
516
+ compile as compile6,
517
+ loadConfig as loadConfig6,
518
+ LintEngine as LintEngine3,
519
+ ALL_RULES as ALL_RULES3,
520
+ applyFixes as applyFixes2,
521
+ createAdapter as createAdapter3
522
+ } from "@runcontext/core";
523
+
524
+ // src/commands/introspect.ts
280
525
  import { Command as Command5 } from "commander";
281
526
  import chalk6 from "chalk";
282
527
  import path5 from "path";
283
- import fs2 from "fs";
528
+ import { mkdirSync, writeFileSync as writeFileSync2, existsSync } from "fs";
529
+ import {
530
+ loadConfig as loadConfig4,
531
+ createAdapter,
532
+ scaffoldFromSchema
533
+ } from "@runcontext/core";
534
+ function parseDbUrl(db) {
535
+ if (db.startsWith("duckdb://")) {
536
+ return { adapter: "duckdb", path: db.slice("duckdb://".length) };
537
+ }
538
+ if (db.startsWith("postgres://") || db.startsWith("postgresql://")) {
539
+ return { adapter: "postgres", connection: db };
540
+ }
541
+ if (db.endsWith(".duckdb") || db.endsWith(".db")) {
542
+ return { adapter: "duckdb", path: db };
543
+ }
544
+ throw new Error(
545
+ `Cannot determine adapter from "${db}". Use duckdb:// or postgres:// prefix.`
546
+ );
547
+ }
548
+ var introspectCommand = new Command5("introspect").description("Introspect a database and scaffold Bronze-level OSI metadata").option(
549
+ "--db <url>",
550
+ "Database URL (e.g., duckdb://path.duckdb or postgres://...)"
551
+ ).option(
552
+ "--source <name>",
553
+ "Use a named data_source from contextkit.config.yaml"
554
+ ).option("--tables <glob>", 'Filter tables by glob pattern (e.g., "vw_*")').option(
555
+ "--model-name <name>",
556
+ "Name for the generated model (default: derived from source)"
557
+ ).action(async (opts) => {
558
+ try {
559
+ const config = loadConfig4(process.cwd());
560
+ const contextDir = path5.resolve(config.context_dir);
561
+ let dsConfig;
562
+ let dsName;
563
+ if (opts.db) {
564
+ dsConfig = parseDbUrl(opts.db);
565
+ dsName = opts.source ?? "default";
566
+ } else if (opts.source) {
567
+ if (!config.data_sources?.[opts.source]) {
568
+ console.error(
569
+ chalk6.red(
570
+ `Data source "${opts.source}" not found in config`
571
+ )
572
+ );
573
+ process.exit(1);
574
+ }
575
+ dsConfig = config.data_sources[opts.source];
576
+ dsName = opts.source;
577
+ } else {
578
+ const sources = config.data_sources;
579
+ if (!sources || Object.keys(sources).length === 0) {
580
+ console.error(
581
+ chalk6.red(
582
+ "No data source specified. Use --db <url> or configure data_sources in config"
583
+ )
584
+ );
585
+ process.exit(1);
586
+ }
587
+ const firstName = Object.keys(sources)[0];
588
+ dsConfig = sources[firstName];
589
+ dsName = firstName;
590
+ }
591
+ const adapter = await createAdapter(dsConfig);
592
+ await adapter.connect();
593
+ console.log(
594
+ chalk6.green(
595
+ `Connected to ${dsConfig.adapter}: ${dsConfig.path ?? dsConfig.connection}`
596
+ )
597
+ );
598
+ let tables = await adapter.listTables();
599
+ if (opts.tables) {
600
+ const pattern = opts.tables.replace(/\*/g, ".*");
601
+ const regex = new RegExp(`^${pattern}$`, "i");
602
+ tables = tables.filter((t) => regex.test(t.name));
603
+ }
604
+ console.log(`Discovered ${tables.length} tables/views`);
605
+ const columns = {};
606
+ for (const table of tables) {
607
+ columns[table.name] = await adapter.listColumns(table.name);
608
+ }
609
+ const totalCols = Object.values(columns).reduce(
610
+ (sum, cols) => sum + cols.length,
611
+ 0
612
+ );
613
+ console.log(`Found ${totalCols} columns total`);
614
+ await adapter.disconnect();
615
+ const modelName = opts.modelName ?? dsName.replace(/[^a-z0-9-]/gi, "-").toLowerCase();
616
+ const result = scaffoldFromSchema({
617
+ modelName,
618
+ dataSourceName: dsName,
619
+ tables,
620
+ columns
621
+ });
622
+ for (const dir of ["models", "governance", "owners"]) {
623
+ const dirPath = path5.join(contextDir, dir);
624
+ if (!existsSync(dirPath)) mkdirSync(dirPath, { recursive: true });
625
+ }
626
+ const osiPath = path5.join(contextDir, "models", result.files.osi);
627
+ const govPath = path5.join(
628
+ contextDir,
629
+ "governance",
630
+ result.files.governance
631
+ );
632
+ const ownerPath = path5.join(contextDir, "owners", result.files.owner);
633
+ writeFileSync2(osiPath, result.osiYaml, "utf-8");
634
+ writeFileSync2(govPath, result.governanceYaml, "utf-8");
635
+ if (!existsSync(ownerPath)) {
636
+ writeFileSync2(ownerPath, result.ownerYaml, "utf-8");
637
+ }
638
+ console.log("");
639
+ console.log(chalk6.green("Scaffolded:"));
640
+ console.log(` ${path5.relative(process.cwd(), osiPath)}`);
641
+ console.log(` ${path5.relative(process.cwd(), govPath)}`);
642
+ console.log(` ${path5.relative(process.cwd(), ownerPath)}`);
643
+ console.log("");
644
+ console.log(chalk6.cyan("Run `context tier` to check your tier score."));
645
+ console.log(
646
+ chalk6.cyan("Run `context verify` to validate against data.")
647
+ );
648
+ } catch (err) {
649
+ console.error(
650
+ chalk6.red(`Introspect failed: ${err.message}`)
651
+ );
652
+ process.exit(1);
653
+ }
654
+ });
655
+
656
+ // src/commands/verify.ts
657
+ import { Command as Command6 } from "commander";
658
+ import chalk7 from "chalk";
659
+ import path6 from "path";
284
660
  import {
285
661
  compile as compile5,
286
662
  loadConfig as loadConfig5,
287
663
  LintEngine as LintEngine2,
288
664
  ALL_RULES as ALL_RULES2,
289
- applyFixes
665
+ createAdapter as createAdapter2
290
666
  } from "@runcontext/core";
291
- var fixCommand = new Command5("fix").description("Auto-fix lint issues").option("--context-dir <path>", "Path to context directory").option("--format <type>", "Output format: pretty or json", "pretty").option("--dry-run", "Show what would be fixed without writing files").action(async (opts) => {
667
+ function findTable(dsName, graph, existingTables) {
668
+ if (existingTables.has(dsName)) return dsName;
669
+ for (const [, model] of graph.models) {
670
+ const ds = model.datasets.find((d) => d.name === dsName);
671
+ if (ds?.source) {
672
+ const tableName = ds.source.split(".").pop();
673
+ if (existingTables.has(tableName)) return tableName;
674
+ }
675
+ }
676
+ return void 0;
677
+ }
678
+ async function collectDataValidation(adapter, graph) {
679
+ const validation = {
680
+ existingTables: /* @__PURE__ */ new Map(),
681
+ existingColumns: /* @__PURE__ */ new Map(),
682
+ actualSampleValues: /* @__PURE__ */ new Map(),
683
+ goldenQueryResults: /* @__PURE__ */ new Map(),
684
+ guardrailResults: /* @__PURE__ */ new Map()
685
+ };
686
+ const tables = await adapter.listTables();
687
+ for (const t of tables) {
688
+ validation.existingTables.set(t.name, t.row_count);
689
+ }
690
+ for (const t of tables) {
691
+ const cols = await adapter.listColumns(t.name);
692
+ const colMap = new Map(cols.map((c) => [c.name, c.data_type]));
693
+ validation.existingColumns.set(t.name, colMap);
694
+ }
695
+ for (const [, gov] of graph.governance) {
696
+ if (!gov.fields) continue;
697
+ for (const [fieldKey, fieldGov] of Object.entries(gov.fields)) {
698
+ if (!fieldGov.sample_values || fieldGov.sample_values.length === 0)
699
+ continue;
700
+ const dotIdx = fieldKey.indexOf(".");
701
+ if (dotIdx < 0) continue;
702
+ const dsName = fieldKey.substring(0, dotIdx);
703
+ const fieldName = fieldKey.substring(dotIdx + 1);
704
+ const tableName = findTable(dsName, graph, validation.existingTables);
705
+ if (!tableName) continue;
706
+ try {
707
+ const result = await adapter.query(
708
+ `SELECT DISTINCT CAST("${fieldName}" AS VARCHAR) AS val FROM "${tableName}" WHERE "${fieldName}" IS NOT NULL LIMIT 50`
709
+ );
710
+ validation.actualSampleValues.set(
711
+ fieldKey,
712
+ result.rows.map((r) => String(r.val))
713
+ );
714
+ } catch {
715
+ }
716
+ }
717
+ }
718
+ for (const [, rules] of graph.rules) {
719
+ if (!rules.golden_queries) continue;
720
+ for (let i = 0; i < rules.golden_queries.length; i++) {
721
+ const gq = rules.golden_queries[i];
722
+ try {
723
+ const result = await adapter.query(gq.sql);
724
+ validation.goldenQueryResults.set(i, {
725
+ success: true,
726
+ rowCount: result.row_count
727
+ });
728
+ } catch (err) {
729
+ validation.goldenQueryResults.set(i, {
730
+ success: false,
731
+ error: err.message
732
+ });
733
+ }
734
+ }
735
+ }
736
+ for (const [, rules] of graph.rules) {
737
+ if (!rules.guardrail_filters) continue;
738
+ for (let i = 0; i < rules.guardrail_filters.length; i++) {
739
+ const gf = rules.guardrail_filters[i];
740
+ const testTable = gf.tables?.[0] ?? "unknown";
741
+ const tableName = findTable(testTable, graph, validation.existingTables);
742
+ if (!tableName) {
743
+ validation.guardrailResults.set(i, {
744
+ valid: false,
745
+ error: `Table "${testTable}" not found`
746
+ });
747
+ continue;
748
+ }
749
+ try {
750
+ await adapter.query(
751
+ `SELECT 1 FROM "${tableName}" WHERE ${gf.filter} LIMIT 1`
752
+ );
753
+ validation.guardrailResults.set(i, { valid: true });
754
+ } catch (err) {
755
+ validation.guardrailResults.set(i, {
756
+ valid: false,
757
+ error: err.message
758
+ });
759
+ }
760
+ }
761
+ }
762
+ return validation;
763
+ }
764
+ var verifyCommand = new Command6("verify").description("Validate metadata accuracy against a live database").option("--source <name>", "Use a specific data_source from config").option("--db <url>", "Database URL override (postgres:// or path.duckdb)").option("--context-dir <path>", "Path to context directory").option("--format <type>", "Output format: pretty or json", "pretty").action(async (opts) => {
292
765
  try {
293
766
  const config = loadConfig5(process.cwd());
294
- const contextDir = opts.contextDir ? path5.resolve(opts.contextDir) : path5.resolve(config.context_dir);
295
- const { graph } = await compile5({ contextDir, config });
296
- const overrides = config.lint?.severity_overrides;
297
- const engine = new LintEngine2(overrides);
767
+ const contextDir = opts.contextDir ? path6.resolve(opts.contextDir) : path6.resolve(config.context_dir);
768
+ const { graph, diagnostics: compileDiags } = await compile5({
769
+ contextDir,
770
+ config
771
+ });
772
+ let dsConfig;
773
+ if (opts.db) {
774
+ dsConfig = parseDbUrl(opts.db);
775
+ } else {
776
+ const sources = config.data_sources;
777
+ if (!sources || Object.keys(sources).length === 0) {
778
+ console.error(
779
+ chalk7.red(
780
+ "No data source configured. Add data_sources to contextkit.config.yaml or use --db."
781
+ )
782
+ );
783
+ process.exit(1);
784
+ }
785
+ const name = opts.source ?? Object.keys(sources)[0];
786
+ const resolved = sources[name];
787
+ if (!resolved) {
788
+ console.error(
789
+ chalk7.red(
790
+ `Data source "${name}" not found. Available: ${Object.keys(sources).join(", ")}`
791
+ )
792
+ );
793
+ process.exit(1);
794
+ return;
795
+ }
796
+ dsConfig = resolved;
797
+ }
798
+ const adapter = await createAdapter2(dsConfig);
799
+ await adapter.connect();
800
+ console.log(chalk7.green(`Connected to ${dsConfig.adapter}`));
801
+ console.log("Collecting validation data...\n");
802
+ graph.dataValidation = await collectDataValidation(adapter, graph);
803
+ await adapter.disconnect();
804
+ const engine = new LintEngine2();
298
805
  for (const rule of ALL_RULES2) {
806
+ if (rule.id.startsWith("data/")) {
807
+ engine.register(rule);
808
+ }
809
+ }
810
+ const dataDiags = engine.run(graph);
811
+ const allDiags = [...dataDiags];
812
+ if (allDiags.length === 0) {
813
+ const tableCount = graph.dataValidation.existingTables.size;
814
+ const totalRows = [
815
+ ...graph.dataValidation.existingTables.values()
816
+ ].reduce((a, b) => a + b, 0);
817
+ console.log(chalk7.green("All data validation checks passed.\n"));
818
+ console.log(
819
+ `Verified against ${tableCount} table(s) (${totalRows.toLocaleString()} total rows)`
820
+ );
821
+ } else {
822
+ console.log(formatDiagnostics(allDiags));
823
+ }
824
+ const hasErrors = allDiags.some((d) => d.severity === "error");
825
+ if (hasErrors) process.exit(1);
826
+ } catch (err) {
827
+ console.error(chalk7.red(`Verify failed: ${err.message}`));
828
+ process.exit(1);
829
+ }
830
+ });
831
+
832
+ // src/commands/fix.ts
833
+ var fixCommand = new Command7("fix").description("Auto-fix lint issues").option("--context-dir <path>", "Path to context directory").option("--format <type>", "Output format: pretty or json", "pretty").option("--dry-run", "Show what would be fixed without writing files").option("--db <url>", "Database URL for data-aware fixes (postgres:// or path.duckdb)").option("--source <name>", "Use a specific data_source from config").action(async (opts) => {
834
+ try {
835
+ const config = loadConfig6(process.cwd());
836
+ const contextDir = opts.contextDir ? path7.resolve(opts.contextDir) : path7.resolve(config.context_dir);
837
+ const { graph } = await compile6({ contextDir, config, rootDir: process.cwd() });
838
+ let dsConfig;
839
+ if (opts.db) {
840
+ dsConfig = parseDbUrl(opts.db);
841
+ } else {
842
+ const sources = config.data_sources;
843
+ if (sources && Object.keys(sources).length > 0) {
844
+ const name = opts.source ?? Object.keys(sources)[0];
845
+ dsConfig = sources[name];
846
+ }
847
+ }
848
+ if (dsConfig) {
849
+ const adapter = await createAdapter3(dsConfig);
850
+ await adapter.connect();
851
+ graph.dataValidation = await collectDataValidation(adapter, graph);
852
+ await adapter.disconnect();
853
+ }
854
+ const overrides = config.lint?.severity_overrides;
855
+ const engine = new LintEngine3(overrides);
856
+ for (const rule of ALL_RULES3) {
299
857
  engine.register(rule);
300
858
  }
301
859
  const diagnostics = engine.run(graph);
@@ -304,12 +862,12 @@ var fixCommand = new Command5("fix").description("Auto-fix lint issues").option(
304
862
  if (opts.format === "json") {
305
863
  console.log(formatJson({ fixedFiles: [], fixCount: 0 }));
306
864
  } else {
307
- console.log(chalk6.green("No fixable issues found."));
865
+ console.log(chalk8.green("No fixable issues found."));
308
866
  }
309
867
  return;
310
868
  }
311
869
  const readFile = (filePath) => fs2.readFileSync(filePath, "utf-8");
312
- const fixedFiles = applyFixes(fixable, readFile);
870
+ const fixedFiles = applyFixes2(fixable, readFile);
313
871
  if (opts.dryRun) {
314
872
  if (opts.format === "json") {
315
873
  const entries = [...fixedFiles.entries()].map(([file, content]) => ({
@@ -321,10 +879,10 @@ var fixCommand = new Command5("fix").description("Auto-fix lint issues").option(
321
879
  );
322
880
  } else {
323
881
  console.log(
324
- chalk6.yellow(`Dry run: ${fixable.length} issue(s) would be fixed in ${fixedFiles.size} file(s):`)
882
+ chalk8.yellow(`Dry run: ${fixable.length} issue(s) would be fixed in ${fixedFiles.size} file(s):`)
325
883
  );
326
884
  for (const file of fixedFiles.keys()) {
327
- console.log(chalk6.gray(` ${file}`));
885
+ console.log(chalk8.gray(` ${file}`));
328
886
  }
329
887
  }
330
888
  return;
@@ -353,40 +911,97 @@ var fixCommand = new Command5("fix").description("Auto-fix lint issues").option(
353
911
  });
354
912
 
355
913
  // src/commands/dev.ts
356
- import { Command as Command6 } from "commander";
357
- import chalk7 from "chalk";
358
- import path6 from "path";
914
+ import { Command as Command8 } from "commander";
915
+ import chalk9 from "chalk";
916
+ import path8 from "path";
917
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
359
918
  import {
360
- compile as compile6,
361
- loadConfig as loadConfig6,
362
- LintEngine as LintEngine3,
363
- ALL_RULES as ALL_RULES3
919
+ compile as compile7,
920
+ loadConfig as loadConfig7,
921
+ LintEngine as LintEngine4,
922
+ ALL_RULES as ALL_RULES4,
923
+ filterByDirectives as filterByDirectives2,
924
+ applyFixes as applyFixes3
364
925
  } from "@runcontext/core";
365
- async function runLint(contextDir) {
366
- const config = loadConfig6(process.cwd());
367
- const { graph, diagnostics: compileDiags } = await compile6({
926
+ function diagKey(d) {
927
+ return `${d.ruleId}|${d.location.file}:${d.location.line}:${d.location.column}|${d.message}`;
928
+ }
929
+ var previousDiags = /* @__PURE__ */ new Map();
930
+ async function runLint(contextDir, fix) {
931
+ const config = loadConfig7(process.cwd());
932
+ const { graph, diagnostics: compileDiags, directives } = await compile7({
368
933
  contextDir,
369
- config
934
+ config,
935
+ rootDir: process.cwd()
370
936
  });
371
937
  const overrides = config.lint?.severity_overrides;
372
- const engine = new LintEngine3(overrides);
373
- for (const rule of ALL_RULES3) {
938
+ const engine = new LintEngine4(overrides);
939
+ for (const rule of ALL_RULES4) {
374
940
  engine.register(rule);
375
941
  }
376
942
  const lintDiags = engine.run(graph);
377
- const allDiags = [...compileDiags, ...lintDiags];
943
+ let allDiags = filterByDirectives2([...compileDiags, ...lintDiags], directives);
944
+ if (fix) {
945
+ const fixable = allDiags.filter((d) => d.fixable && d.fix);
946
+ if (fixable.length > 0) {
947
+ const fixes = applyFixes3(fixable, (filePath) => readFileSync2(filePath, "utf-8"));
948
+ for (const [file, content] of fixes) {
949
+ writeFileSync3(file, content, "utf-8");
950
+ }
951
+ const { graph: reGraph, diagnostics: reCompileDiags, directives: reDirs } = await compile7({
952
+ contextDir,
953
+ config,
954
+ rootDir: process.cwd()
955
+ });
956
+ const reEngine = new LintEngine4(overrides);
957
+ for (const rule of ALL_RULES4) {
958
+ reEngine.register(rule);
959
+ }
960
+ allDiags = filterByDirectives2([...reCompileDiags, ...reEngine.run(reGraph)], reDirs);
961
+ if (fixable.length > 0) {
962
+ console.log(chalk9.green(` Auto-fixed ${fixable.length} issue(s).`));
963
+ }
964
+ }
965
+ }
966
+ const currentDiags = /* @__PURE__ */ new Map();
967
+ for (const d of allDiags) {
968
+ currentDiags.set(diagKey(d), d);
969
+ }
970
+ const newIssues = [];
971
+ const resolved = [];
972
+ for (const [key, d] of currentDiags) {
973
+ if (!previousDiags.has(key)) newIssues.push(d);
974
+ }
975
+ for (const [key, d] of previousDiags) {
976
+ if (!currentDiags.has(key)) resolved.push(d);
977
+ }
378
978
  console.clear();
379
- console.log(chalk7.gray(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Linting...`));
979
+ console.log(chalk9.gray(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Linting...`));
980
+ if (previousDiags.size > 0) {
981
+ if (resolved.length > 0) {
982
+ console.log(chalk9.green(` ${resolved.length} issue(s) resolved`));
983
+ }
984
+ if (newIssues.length > 0) {
985
+ console.log(chalk9.red(` ${newIssues.length} new issue(s)`));
986
+ }
987
+ if (resolved.length === 0 && newIssues.length === 0) {
988
+ console.log(chalk9.gray(" No changes"));
989
+ }
990
+ console.log("");
991
+ }
380
992
  console.log(formatDiagnostics(allDiags));
381
993
  console.log("");
994
+ previousDiags = currentDiags;
382
995
  }
383
- var devCommand = new Command6("dev").description("Watch mode \u2014 re-run lint on file changes").option("--context-dir <path>", "Path to context directory").action(async (opts) => {
996
+ var devCommand = new Command8("dev").description("Watch mode \u2014 re-run lint on file changes").option("--context-dir <path>", "Path to context directory").option("--fix", "Auto-fix problems on each re-lint").action(async (opts) => {
384
997
  try {
385
- const config = loadConfig6(process.cwd());
386
- const contextDir = opts.contextDir ? path6.resolve(opts.contextDir) : path6.resolve(config.context_dir);
387
- console.log(chalk7.blue(`Watching ${contextDir} for changes...`));
388
- console.log(chalk7.gray("Press Ctrl+C to stop.\n"));
389
- await runLint(contextDir);
998
+ const config = loadConfig7(process.cwd());
999
+ const contextDir = opts.contextDir ? path8.resolve(opts.contextDir) : path8.resolve(config.context_dir);
1000
+ const fix = opts.fix === true;
1001
+ console.log(chalk9.blue(`Watching ${contextDir} for changes...`));
1002
+ if (fix) console.log(chalk9.blue("Auto-fix enabled."));
1003
+ console.log(chalk9.gray("Press Ctrl+C to stop.\n"));
1004
+ await runLint(contextDir, fix);
390
1005
  const { watch } = await import("chokidar");
391
1006
  let debounceTimer = null;
392
1007
  const watcher = watch(contextDir, {
@@ -399,24 +1014,24 @@ var devCommand = new Command6("dev").description("Watch mode \u2014 re-run lint
399
1014
  if (debounceTimer) clearTimeout(debounceTimer);
400
1015
  debounceTimer = setTimeout(async () => {
401
1016
  try {
402
- await runLint(contextDir);
1017
+ await runLint(contextDir, fix);
403
1018
  } catch (err) {
404
1019
  console.error(
405
- chalk7.red(`Lint error: ${err.message}`)
1020
+ chalk9.red(`Lint error: ${err.message}`)
406
1021
  );
407
1022
  }
408
1023
  }, 300);
409
1024
  });
410
1025
  } catch (err) {
411
- console.error(chalk7.red(`Dev mode failed: ${err.message}`));
1026
+ console.error(chalk9.red(`Dev mode failed: ${err.message}`));
412
1027
  process.exit(1);
413
1028
  }
414
1029
  });
415
1030
 
416
1031
  // src/commands/init.ts
417
- import { Command as Command7 } from "commander";
418
- import chalk8 from "chalk";
419
- import path7 from "path";
1032
+ import { Command as Command9 } from "commander";
1033
+ import chalk10 from "chalk";
1034
+ import path9 from "path";
420
1035
  import fs3 from "fs";
421
1036
  var EXAMPLE_OSI = `version: "1.0"
422
1037
 
@@ -481,26 +1096,26 @@ var EXAMPLE_CONFIG = `context_dir: context
481
1096
  output_dir: dist
482
1097
  minimum_tier: bronze
483
1098
  `;
484
- var initCommand = new Command7("init").description("Scaffold a v0.2 ContextKit project structure").option("--dir <path>", "Root directory for the project", ".").action(async (opts) => {
1099
+ var initCommand = new Command9("init").description("Scaffold a v0.2 ContextKit project structure").option("--dir <path>", "Root directory for the project", ".").action(async (opts) => {
485
1100
  try {
486
- const rootDir = path7.resolve(opts.dir);
487
- const contextDir = path7.join(rootDir, "context");
1101
+ const rootDir = path9.resolve(opts.dir);
1102
+ const contextDir = path9.join(rootDir, "context");
488
1103
  const dirs = [
489
- path7.join(contextDir, "models"),
490
- path7.join(contextDir, "governance"),
491
- path7.join(contextDir, "glossary"),
492
- path7.join(contextDir, "owners")
1104
+ path9.join(contextDir, "models"),
1105
+ path9.join(contextDir, "governance"),
1106
+ path9.join(contextDir, "glossary"),
1107
+ path9.join(contextDir, "owners")
493
1108
  ];
494
1109
  for (const dir of dirs) {
495
1110
  fs3.mkdirSync(dir, { recursive: true });
496
1111
  }
497
1112
  const files = [
498
1113
  {
499
- path: path7.join(contextDir, "models", "example-model.osi.yaml"),
1114
+ path: path9.join(contextDir, "models", "example-model.osi.yaml"),
500
1115
  content: EXAMPLE_OSI
501
1116
  },
502
1117
  {
503
- path: path7.join(
1118
+ path: path9.join(
504
1119
  contextDir,
505
1120
  "governance",
506
1121
  "example-model.governance.yaml"
@@ -508,15 +1123,15 @@ var initCommand = new Command7("init").description("Scaffold a v0.2 ContextKit p
508
1123
  content: EXAMPLE_GOVERNANCE
509
1124
  },
510
1125
  {
511
- path: path7.join(contextDir, "glossary", "glossary.term.yaml"),
1126
+ path: path9.join(contextDir, "glossary", "glossary.term.yaml"),
512
1127
  content: EXAMPLE_TERM
513
1128
  },
514
1129
  {
515
- path: path7.join(contextDir, "owners", "data-team.owner.yaml"),
1130
+ path: path9.join(contextDir, "owners", "data-team.owner.yaml"),
516
1131
  content: EXAMPLE_OWNER
517
1132
  },
518
1133
  {
519
- path: path7.join(rootDir, "contextkit.config.yaml"),
1134
+ path: path9.join(rootDir, "contextkit.config.yaml"),
520
1135
  content: EXAMPLE_CONFIG
521
1136
  }
522
1137
  ];
@@ -524,11 +1139,11 @@ var initCommand = new Command7("init").description("Scaffold a v0.2 ContextKit p
524
1139
  let skipped = 0;
525
1140
  for (const file of files) {
526
1141
  if (fs3.existsSync(file.path)) {
527
- console.log(chalk8.gray(` skip ${path7.relative(rootDir, file.path)} (exists)`));
1142
+ console.log(chalk10.gray(` skip ${path9.relative(rootDir, file.path)} (exists)`));
528
1143
  skipped++;
529
1144
  } else {
530
1145
  fs3.writeFileSync(file.path, file.content, "utf-8");
531
- console.log(chalk8.green(` create ${path7.relative(rootDir, file.path)}`));
1146
+ console.log(chalk10.green(` create ${path9.relative(rootDir, file.path)}`));
532
1147
  created++;
533
1148
  }
534
1149
  }
@@ -539,10 +1154,10 @@ var initCommand = new Command7("init").description("Scaffold a v0.2 ContextKit p
539
1154
  )
540
1155
  );
541
1156
  console.log("");
542
- console.log(chalk8.gray("Next steps:"));
543
- console.log(chalk8.gray(" 1. Edit the example files in context/"));
544
- console.log(chalk8.gray(" 2. Run: context lint"));
545
- console.log(chalk8.gray(" 3. Run: context build"));
1157
+ console.log(chalk10.gray("Next steps:"));
1158
+ console.log(chalk10.gray(" 1. Edit the example files in context/"));
1159
+ console.log(chalk10.gray(" 2. Run: context lint"));
1160
+ console.log(chalk10.gray(" 3. Run: context build"));
546
1161
  } catch (err) {
547
1162
  console.error(formatError(err.message));
548
1163
  process.exit(1);
@@ -550,15 +1165,15 @@ var initCommand = new Command7("init").description("Scaffold a v0.2 ContextKit p
550
1165
  });
551
1166
 
552
1167
  // src/commands/site.ts
553
- import { Command as Command8 } from "commander";
554
- import chalk9 from "chalk";
555
- import path8 from "path";
556
- import { compile as compile7, loadConfig as loadConfig7, emitManifest as emitManifest2 } from "@runcontext/core";
557
- var siteCommand = new Command8("site").description("Build a static documentation site from compiled context").option("--context-dir <path>", "Path to context directory").option("--output-dir <path>", "Path to site output directory").action(async (opts) => {
1168
+ import { Command as Command10 } from "commander";
1169
+ import chalk11 from "chalk";
1170
+ import path10 from "path";
1171
+ import { compile as compile8, loadConfig as loadConfig8, emitManifest as emitManifest2 } from "@runcontext/core";
1172
+ var siteCommand = new Command10("site").description("Build a static documentation site from compiled context").option("--context-dir <path>", "Path to context directory").option("--output-dir <path>", "Path to site output directory").action(async (opts) => {
558
1173
  try {
559
- const config = loadConfig7(process.cwd());
560
- const contextDir = opts.contextDir ? path8.resolve(opts.contextDir) : path8.resolve(config.context_dir);
561
- const { graph } = await compile7({ contextDir, config });
1174
+ const config = loadConfig8(process.cwd());
1175
+ const contextDir = opts.contextDir ? path10.resolve(opts.contextDir) : path10.resolve(config.context_dir);
1176
+ const { graph } = await compile8({ contextDir, config, rootDir: process.cwd() });
562
1177
  const manifest = emitManifest2(graph, config);
563
1178
  let buildSite;
564
1179
  try {
@@ -568,15 +1183,15 @@ var siteCommand = new Command8("site").description("Build a static documentation
568
1183
  }
569
1184
  if (!buildSite) {
570
1185
  console.log(
571
- chalk9.yellow(
1186
+ chalk11.yellow(
572
1187
  "Site generator is not yet available. Install @runcontext/site to enable this command."
573
1188
  )
574
1189
  );
575
1190
  process.exit(0);
576
1191
  }
577
- const outputDir = opts.outputDir ? path8.resolve(opts.outputDir) : path8.resolve(config.site?.base_path ?? "site");
1192
+ const outputDir = opts.outputDir ? path10.resolve(opts.outputDir) : path10.resolve(config.site?.base_path ?? "site");
578
1193
  await buildSite(manifest, config, outputDir);
579
- console.log(chalk9.green(`Site built to ${outputDir}`));
1194
+ console.log(chalk11.green(`Site built to ${outputDir}`));
580
1195
  } catch (err) {
581
1196
  console.error(formatError(err.message));
582
1197
  process.exit(1);
@@ -584,9 +1199,9 @@ var siteCommand = new Command8("site").description("Build a static documentation
584
1199
  });
585
1200
 
586
1201
  // src/commands/serve.ts
587
- import { Command as Command9 } from "commander";
588
- import chalk10 from "chalk";
589
- var serveCommand = new Command9("serve").description("Start the MCP server (stdio transport)").option("--context-dir <path>", "Path to context directory").action(async (opts) => {
1202
+ import { Command as Command11 } from "commander";
1203
+ import chalk12 from "chalk";
1204
+ var serveCommand = new Command11("serve").description("Start the MCP server (stdio transport)").option("--context-dir <path>", "Path to context directory").action(async (opts) => {
590
1205
  try {
591
1206
  let startServer;
592
1207
  try {
@@ -596,13 +1211,13 @@ var serveCommand = new Command9("serve").description("Start the MCP server (stdi
596
1211
  }
597
1212
  if (!startServer) {
598
1213
  console.log(
599
- chalk10.yellow(
1214
+ chalk12.yellow(
600
1215
  "MCP server is not available. Install @runcontext/mcp to enable this command."
601
1216
  )
602
1217
  );
603
1218
  process.exit(1);
604
1219
  }
605
- console.log(chalk10.blue("Starting MCP server (stdio transport)..."));
1220
+ console.log(chalk12.blue("Starting MCP server (stdio transport)..."));
606
1221
  await startServer({
607
1222
  contextDir: opts.contextDir,
608
1223
  rootDir: process.cwd()
@@ -614,13 +1229,13 @@ var serveCommand = new Command9("serve").description("Start the MCP server (stdi
614
1229
  });
615
1230
 
616
1231
  // src/commands/validate-osi.ts
617
- import { Command as Command10 } from "commander";
618
- import chalk11 from "chalk";
619
- import path9 from "path";
1232
+ import { Command as Command12 } from "commander";
1233
+ import chalk13 from "chalk";
1234
+ import path11 from "path";
620
1235
  import { parseFile, osiDocumentSchema } from "@runcontext/core";
621
- var validateOsiCommand = new Command10("validate-osi").description("Validate a single OSI file against the schema").argument("<file>", "Path to the OSI YAML file").option("--format <type>", "Output format: pretty or json", "pretty").action(async (file, opts) => {
1236
+ var validateOsiCommand = new Command12("validate-osi").description("Validate a single OSI file against the schema").argument("<file>", "Path to the OSI YAML file").option("--format <type>", "Output format: pretty or json", "pretty").action(async (file, opts) => {
622
1237
  try {
623
- const filePath = path9.resolve(file);
1238
+ const filePath = path11.resolve(file);
624
1239
  const parsed = await parseFile(filePath, "model");
625
1240
  const result = osiDocumentSchema.safeParse(parsed.data);
626
1241
  if (result.success) {
@@ -649,9 +1264,9 @@ var validateOsiCommand = new Command10("validate-osi").description("Validate a s
649
1264
  })
650
1265
  );
651
1266
  } else {
652
- console.error(chalk11.red(`Validation failed for ${filePath}:`));
1267
+ console.error(chalk13.red(`Validation failed for ${filePath}:`));
653
1268
  for (const issue of issues) {
654
- console.error(chalk11.red(` ${issue.path}: ${issue.message}`));
1269
+ console.error(chalk13.red(` ${issue.path}: ${issue.message}`));
655
1270
  }
656
1271
  }
657
1272
  process.exit(1);
@@ -662,9 +1277,294 @@ var validateOsiCommand = new Command10("validate-osi").description("Validate a s
662
1277
  }
663
1278
  });
664
1279
 
1280
+ // src/commands/enrich.ts
1281
+ import { Command as Command13 } from "commander";
1282
+ import chalk14 from "chalk";
1283
+ import path12 from "path";
1284
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync2, readdirSync } from "fs";
1285
+ import * as yaml from "yaml";
1286
+ import {
1287
+ compile as compile9,
1288
+ loadConfig as loadConfig9,
1289
+ computeTier as computeTier2,
1290
+ createAdapter as createAdapter4,
1291
+ suggestEnrichments,
1292
+ inferSemanticRole,
1293
+ inferAggregation
1294
+ } from "@runcontext/core";
1295
+ function findFileRecursive(dir, suffix) {
1296
+ if (!existsSync2(dir)) return void 0;
1297
+ const entries = readdirSync(dir, { withFileTypes: true });
1298
+ for (const entry of entries) {
1299
+ const fullPath = path12.join(dir, entry.name);
1300
+ if (entry.isDirectory()) {
1301
+ const found = findFileRecursive(fullPath, suffix);
1302
+ if (found) return found;
1303
+ } else if (entry.name.endsWith(suffix)) {
1304
+ return fullPath;
1305
+ }
1306
+ }
1307
+ return void 0;
1308
+ }
1309
+ var enrichCommand = new Command13("enrich").description("Suggest or apply metadata enrichments to reach a target tier").option("--target <tier>", "Target tier: silver or gold", "silver").option("--apply", "Write suggestions to YAML files").option("--source <name>", "Data source for sample values").option("--db <url>", "Database URL for sample values").option("--context-dir <path>", "Path to context directory").action(async (opts) => {
1310
+ try {
1311
+ const config = loadConfig9(process.cwd());
1312
+ const contextDir = opts.contextDir ? path12.resolve(opts.contextDir) : path12.resolve(config.context_dir);
1313
+ const target = opts.target;
1314
+ if (!["silver", "gold"].includes(target)) {
1315
+ console.error(chalk14.red('--target must be "silver" or "gold"'));
1316
+ process.exit(1);
1317
+ }
1318
+ const { graph } = await compile9({ contextDir, config });
1319
+ for (const [modelName] of graph.models) {
1320
+ const tierScore = computeTier2(modelName, graph);
1321
+ console.log(chalk14.bold(`${modelName}: ${tierScore.tier.toUpperCase()}`));
1322
+ if (tierScore.tier === target || target === "silver" && tierScore.tier === "gold") {
1323
+ console.log(chalk14.green(` Already at ${target} or above.
1324
+ `));
1325
+ continue;
1326
+ }
1327
+ const model = graph.models.get(modelName);
1328
+ const datasetNames = model.datasets.map((d) => d.name);
1329
+ const suggestions = suggestEnrichments(target, tierScore, datasetNames);
1330
+ if (!suggestions.governance && !suggestions.lineage && !suggestions.glossaryTerms && !suggestions.needsRulesFile && !suggestions.needsSampleValues && !suggestions.needsSemanticRoles) {
1331
+ console.log(chalk14.green(" No suggestions needed.\n"));
1332
+ continue;
1333
+ }
1334
+ if (suggestions.governance?.trust) {
1335
+ console.log(chalk14.yellow(` + Add trust: ${suggestions.governance.trust}`));
1336
+ }
1337
+ if (suggestions.governance?.tags) {
1338
+ console.log(chalk14.yellow(` + Add tags: [${suggestions.governance.tags.join(", ")}]`));
1339
+ }
1340
+ if (suggestions.governance?.refreshAll) {
1341
+ console.log(chalk14.yellow(` + Add refresh: ${suggestions.governance.refreshAll}`));
1342
+ }
1343
+ if (suggestions.lineage) {
1344
+ console.log(chalk14.yellow(` + Add lineage with ${suggestions.lineage.upstream?.length ?? 0} upstream sources`));
1345
+ }
1346
+ if (suggestions.glossaryTerms) {
1347
+ console.log(chalk14.yellow(` + Generate ${suggestions.glossaryTerms.length} glossary term(s)`));
1348
+ }
1349
+ if (suggestions.needsSampleValues) {
1350
+ console.log(chalk14.yellow(" + Populate sample_values from database"));
1351
+ }
1352
+ if (suggestions.needsSemanticRoles) {
1353
+ console.log(chalk14.yellow(" + Infer semantic_role for all fields"));
1354
+ }
1355
+ if (suggestions.needsRulesFile) {
1356
+ console.log(chalk14.yellow(" + Generate rules file"));
1357
+ }
1358
+ if (!opts.apply) {
1359
+ console.log(chalk14.cyan("\n Run with --apply to write these changes.\n"));
1360
+ continue;
1361
+ }
1362
+ const govFilePath = findFileRecursive(contextDir, `${modelName}.governance.yaml`);
1363
+ if (govFilePath) {
1364
+ const govContent = readFileSync3(govFilePath, "utf-8");
1365
+ const govDoc = yaml.parse(govContent) ?? {};
1366
+ if (suggestions.governance?.trust) {
1367
+ govDoc.trust = suggestions.governance.trust;
1368
+ }
1369
+ if (suggestions.governance?.tags) {
1370
+ govDoc.tags = suggestions.governance.tags;
1371
+ }
1372
+ if (suggestions.governance?.refreshAll) {
1373
+ for (const dsName of Object.keys(govDoc.datasets ?? {})) {
1374
+ govDoc.datasets[dsName].refresh = suggestions.governance.refreshAll;
1375
+ }
1376
+ }
1377
+ if (suggestions.needsSemanticRoles) {
1378
+ govDoc.fields = govDoc.fields ?? {};
1379
+ let adapter = null;
1380
+ const dsConfig = opts.db ? parseDbUrl(opts.db) : config.data_sources?.[opts.source ?? Object.keys(config.data_sources ?? {})[0]];
1381
+ if (dsConfig) {
1382
+ adapter = await createAdapter4(dsConfig);
1383
+ await adapter.connect();
1384
+ }
1385
+ for (const ds of model.datasets) {
1386
+ let columns = [];
1387
+ if (adapter) {
1388
+ const tableName = ds.source?.split(".").pop() ?? ds.name;
1389
+ try {
1390
+ columns = await adapter.listColumns(tableName);
1391
+ } catch {
1392
+ }
1393
+ }
1394
+ for (const field of ds.fields ?? []) {
1395
+ const fieldKey = `${ds.name}.${field.name}`;
1396
+ if (govDoc.fields[fieldKey]?.semantic_role) continue;
1397
+ const col = columns.find((c) => c.name === field.name);
1398
+ const isPK = col?.is_primary_key ?? field.name.endsWith("_id");
1399
+ const dataType = col?.data_type ?? "VARCHAR";
1400
+ govDoc.fields[fieldKey] = govDoc.fields[fieldKey] ?? {};
1401
+ const role = inferSemanticRole(field.name, dataType, isPK);
1402
+ govDoc.fields[fieldKey].semantic_role = role;
1403
+ if (role === "metric") {
1404
+ govDoc.fields[fieldKey].default_aggregation = inferAggregation(field.name);
1405
+ govDoc.fields[fieldKey].additive = govDoc.fields[fieldKey].default_aggregation === "SUM";
1406
+ }
1407
+ }
1408
+ }
1409
+ if (adapter) await adapter.disconnect();
1410
+ }
1411
+ if (suggestions.needsSampleValues) {
1412
+ govDoc.fields = govDoc.fields ?? {};
1413
+ const dsConfig2 = opts.db ? parseDbUrl(opts.db) : config.data_sources?.[opts.source ?? Object.keys(config.data_sources ?? {})[0]];
1414
+ if (dsConfig2) {
1415
+ const adapter2 = await createAdapter4(dsConfig2);
1416
+ await adapter2.connect();
1417
+ let count = 0;
1418
+ for (const ds of model.datasets) {
1419
+ if (count >= 2) break;
1420
+ const tableName = ds.source?.split(".").pop() ?? ds.name;
1421
+ for (const field of ds.fields ?? []) {
1422
+ if (count >= 2) break;
1423
+ const fieldKey = `${ds.name}.${field.name}`;
1424
+ if (govDoc.fields[fieldKey]?.sample_values?.length > 0) continue;
1425
+ try {
1426
+ const result = await adapter2.query(
1427
+ `SELECT DISTINCT CAST("${field.name}" AS VARCHAR) AS val FROM "${tableName}" WHERE "${field.name}" IS NOT NULL LIMIT 5`
1428
+ );
1429
+ if (result.rows.length > 0) {
1430
+ govDoc.fields[fieldKey] = govDoc.fields[fieldKey] ?? {};
1431
+ govDoc.fields[fieldKey].sample_values = result.rows.map((r) => String(r.val));
1432
+ count++;
1433
+ }
1434
+ } catch {
1435
+ }
1436
+ }
1437
+ }
1438
+ await adapter2.disconnect();
1439
+ }
1440
+ }
1441
+ writeFileSync4(govFilePath, yaml.stringify(govDoc, { lineWidth: 120 }), "utf-8");
1442
+ console.log(chalk14.green(` Updated: ${path12.relative(process.cwd(), govFilePath)}`));
1443
+ }
1444
+ if (suggestions.lineage) {
1445
+ const lineageDir = path12.join(contextDir, "lineage");
1446
+ if (!existsSync2(lineageDir)) mkdirSync2(lineageDir, { recursive: true });
1447
+ const lineagePath = path12.join(lineageDir, `${modelName}.lineage.yaml`);
1448
+ if (!existsSync2(lineagePath)) {
1449
+ const lineageDoc = {
1450
+ model: modelName,
1451
+ upstream: suggestions.lineage.upstream
1452
+ };
1453
+ writeFileSync4(lineagePath, yaml.stringify(lineageDoc, { lineWidth: 120 }), "utf-8");
1454
+ console.log(chalk14.green(` Created: ${path12.relative(process.cwd(), lineagePath)}`));
1455
+ }
1456
+ }
1457
+ if (suggestions.glossaryTerms) {
1458
+ const glossaryDir = path12.join(contextDir, "glossary");
1459
+ if (!existsSync2(glossaryDir)) mkdirSync2(glossaryDir, { recursive: true });
1460
+ for (const term of suggestions.glossaryTerms) {
1461
+ const termPath = path12.join(glossaryDir, `${term.id}.term.yaml`);
1462
+ if (!existsSync2(termPath)) {
1463
+ writeFileSync4(termPath, yaml.stringify(term, { lineWidth: 120 }), "utf-8");
1464
+ console.log(chalk14.green(` Created: ${path12.relative(process.cwd(), termPath)}`));
1465
+ }
1466
+ }
1467
+ }
1468
+ if (suggestions.needsRulesFile) {
1469
+ const rulesDir = path12.join(contextDir, "rules");
1470
+ if (!existsSync2(rulesDir)) mkdirSync2(rulesDir, { recursive: true });
1471
+ const rulesPath = path12.join(rulesDir, `${modelName}.rules.yaml`);
1472
+ if (!existsSync2(rulesPath)) {
1473
+ const rulesDoc = {
1474
+ model: modelName,
1475
+ golden_queries: [
1476
+ { question: "TODO: What is the total count?", sql: "SELECT COUNT(*) FROM table_name" },
1477
+ { question: "TODO: What are the top records?", sql: "SELECT * FROM table_name LIMIT 10" },
1478
+ { question: "TODO: What is the distribution?", sql: "SELECT column, COUNT(*) FROM table_name GROUP BY column" }
1479
+ ],
1480
+ business_rules: [
1481
+ { name: "TODO: rule-name", definition: "TODO: describe the business rule" }
1482
+ ],
1483
+ guardrail_filters: [
1484
+ { name: "TODO: filter-name", filter: "column IS NOT NULL", reason: "TODO: explain why" }
1485
+ ],
1486
+ hierarchies: [
1487
+ { name: "TODO: hierarchy-name", levels: ["level1", "level2"], dataset: datasetNames[0] ?? "dataset" }
1488
+ ]
1489
+ };
1490
+ writeFileSync4(rulesPath, yaml.stringify(rulesDoc, { lineWidth: 120 }), "utf-8");
1491
+ console.log(chalk14.green(` Created: ${path12.relative(process.cwd(), rulesPath)} (with TODOs)`));
1492
+ }
1493
+ }
1494
+ console.log("");
1495
+ }
1496
+ } catch (err) {
1497
+ console.error(chalk14.red(`Enrich failed: ${err.message}`));
1498
+ process.exit(1);
1499
+ }
1500
+ });
1501
+
1502
+ // src/commands/rules.ts
1503
+ import { Command as Command14 } from "commander";
1504
+ import chalk15 from "chalk";
1505
+ import { ALL_RULES as ALL_RULES5 } from "@runcontext/core";
1506
+ function formatRuleTable(rules) {
1507
+ if (rules.length === 0) {
1508
+ return chalk15.gray("No rules match the filters.");
1509
+ }
1510
+ const lines = [];
1511
+ const header = `${"ID".padEnd(40)} ${"Tier".padEnd(8)} ${"Severity".padEnd(10)} ${"Fix".padEnd(5)} Description`;
1512
+ lines.push(chalk15.bold(header));
1513
+ lines.push(chalk15.gray("\u2500".repeat(100)));
1514
+ for (const rule of rules) {
1515
+ const tier = rule.tier ?? "\u2014";
1516
+ const tierCol = colorTier(tier);
1517
+ const fixCol = rule.fixable ? chalk15.green("yes") : chalk15.gray("no");
1518
+ const sevCol = rule.defaultSeverity === "error" ? chalk15.red(rule.defaultSeverity) : chalk15.yellow(rule.defaultSeverity);
1519
+ const deprecated = rule.deprecated ? chalk15.gray(" (deprecated)") : "";
1520
+ lines.push(
1521
+ `${rule.id.padEnd(40)} ${tierCol.padEnd(8 + (tierCol.length - tier.length))} ${sevCol.padEnd(10 + (sevCol.length - rule.defaultSeverity.length))} ${fixCol.padEnd(5 + (fixCol.length - (rule.fixable ? 3 : 2)))} ${rule.description}${deprecated}`
1522
+ );
1523
+ }
1524
+ lines.push("");
1525
+ lines.push(chalk15.gray(`${rules.length} rule(s) total`));
1526
+ return lines.join("\n");
1527
+ }
1528
+ function colorTier(tier) {
1529
+ switch (tier) {
1530
+ case "gold":
1531
+ return chalk15.yellow(tier);
1532
+ case "silver":
1533
+ return chalk15.white(tier);
1534
+ case "bronze":
1535
+ return chalk15.hex("#CD7F32")(tier);
1536
+ default:
1537
+ return chalk15.gray(tier);
1538
+ }
1539
+ }
1540
+ var rulesCommand = new Command14("rules").description("List all lint rules with metadata").option("--tier <tier>", "Filter by tier: bronze, silver, gold").option("--fixable", "Show only fixable rules").option("--format <type>", "Output format: pretty or json", "pretty").action((opts) => {
1541
+ let rules = [...ALL_RULES5];
1542
+ if (opts.tier) {
1543
+ const tier = opts.tier;
1544
+ rules = rules.filter((r) => r.tier === tier);
1545
+ }
1546
+ if (opts.fixable) {
1547
+ rules = rules.filter((r) => r.fixable);
1548
+ }
1549
+ if (opts.format === "json") {
1550
+ const data = rules.map((r) => ({
1551
+ id: r.id,
1552
+ tier: r.tier ?? null,
1553
+ defaultSeverity: r.defaultSeverity,
1554
+ fixable: r.fixable,
1555
+ description: r.description,
1556
+ deprecated: r.deprecated ?? false,
1557
+ replacedBy: r.replacedBy ?? null
1558
+ }));
1559
+ console.log(formatJson(data));
1560
+ } else {
1561
+ console.log(formatRuleTable(rules));
1562
+ }
1563
+ });
1564
+
665
1565
  // src/index.ts
666
- var program = new Command11();
667
- program.name("context").description("ContextKit \u2014 AI-ready metadata governance over OSI").version("0.2.0");
1566
+ var program = new Command15();
1567
+ program.name("context").description("ContextKit \u2014 AI-ready metadata governance over OSI").version("0.3.1");
668
1568
  program.addCommand(lintCommand);
669
1569
  program.addCommand(buildCommand);
670
1570
  program.addCommand(tierCommand);
@@ -675,5 +1575,9 @@ program.addCommand(initCommand);
675
1575
  program.addCommand(siteCommand);
676
1576
  program.addCommand(serveCommand);
677
1577
  program.addCommand(validateOsiCommand);
1578
+ program.addCommand(introspectCommand);
1579
+ program.addCommand(verifyCommand);
1580
+ program.addCommand(enrichCommand);
1581
+ program.addCommand(rulesCommand);
678
1582
  program.parse();
679
1583
  //# sourceMappingURL=index.js.map