@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 +1007 -103
- package/dist/index.js.map +1 -1
- package/package.json +12 -20
- package/LICENSE +0 -21
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
|
|
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
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
|
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 =
|
|
244
|
+
const config = await loadConfigAsync(process.cwd());
|
|
96
245
|
const contextDir = opts.contextDir ? path.resolve(opts.contextDir) : path.resolve(config.context_dir);
|
|
97
|
-
const
|
|
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
|
|
102
|
-
const
|
|
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
|
-
|
|
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 (
|
|
125
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
665
|
+
createAdapter as createAdapter2
|
|
290
666
|
} from "@runcontext/core";
|
|
291
|
-
|
|
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 ?
|
|
295
|
-
const { graph } = await compile5({
|
|
296
|
-
|
|
297
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
|
357
|
-
import
|
|
358
|
-
import
|
|
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
|
|
361
|
-
loadConfig as
|
|
362
|
-
LintEngine as
|
|
363
|
-
ALL_RULES as
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
|
373
|
-
for (const rule of
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
386
|
-
const contextDir = opts.contextDir ?
|
|
387
|
-
|
|
388
|
-
console.log(
|
|
389
|
-
|
|
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
|
-
|
|
1020
|
+
chalk9.red(`Lint error: ${err.message}`)
|
|
406
1021
|
);
|
|
407
1022
|
}
|
|
408
1023
|
}, 300);
|
|
409
1024
|
});
|
|
410
1025
|
} catch (err) {
|
|
411
|
-
console.error(
|
|
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
|
|
418
|
-
import
|
|
419
|
-
import
|
|
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
|
|
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 =
|
|
487
|
-
const contextDir =
|
|
1101
|
+
const rootDir = path9.resolve(opts.dir);
|
|
1102
|
+
const contextDir = path9.join(rootDir, "context");
|
|
488
1103
|
const dirs = [
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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:
|
|
1114
|
+
path: path9.join(contextDir, "models", "example-model.osi.yaml"),
|
|
500
1115
|
content: EXAMPLE_OSI
|
|
501
1116
|
},
|
|
502
1117
|
{
|
|
503
|
-
path:
|
|
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:
|
|
1126
|
+
path: path9.join(contextDir, "glossary", "glossary.term.yaml"),
|
|
512
1127
|
content: EXAMPLE_TERM
|
|
513
1128
|
},
|
|
514
1129
|
{
|
|
515
|
-
path:
|
|
1130
|
+
path: path9.join(contextDir, "owners", "data-team.owner.yaml"),
|
|
516
1131
|
content: EXAMPLE_OWNER
|
|
517
1132
|
},
|
|
518
1133
|
{
|
|
519
|
-
path:
|
|
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(
|
|
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(
|
|
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(
|
|
543
|
-
console.log(
|
|
544
|
-
console.log(
|
|
545
|
-
console.log(
|
|
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
|
|
554
|
-
import
|
|
555
|
-
import
|
|
556
|
-
import { compile as
|
|
557
|
-
var siteCommand = new
|
|
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 =
|
|
560
|
-
const contextDir = opts.contextDir ?
|
|
561
|
-
const { graph } = await
|
|
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
|
-
|
|
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 ?
|
|
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(
|
|
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
|
|
588
|
-
import
|
|
589
|
-
var serveCommand = new
|
|
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
|
-
|
|
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(
|
|
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
|
|
618
|
-
import
|
|
619
|
-
import
|
|
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
|
|
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 =
|
|
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(
|
|
1267
|
+
console.error(chalk13.red(`Validation failed for ${filePath}:`));
|
|
653
1268
|
for (const issue of issues) {
|
|
654
|
-
console.error(
|
|
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
|
|
667
|
-
program.name("context").description("ContextKit \u2014 AI-ready metadata governance over OSI").version("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
|