@runcontext/core 0.1.1 → 0.2.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/LICENSE +21 -0
- package/dist/index.cjs +1702 -887
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2581 -730
- package/dist/index.d.ts +2581 -730
- package/dist/index.mjs +1723 -908
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -8
package/dist/index.mjs
CHANGED
|
@@ -1,84 +1,173 @@
|
|
|
1
|
-
// src/schema/
|
|
1
|
+
// src/schema/osi.ts
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
var
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
3
|
+
var dialectEnum = z.enum(["ANSI_SQL", "SNOWFLAKE", "MDX", "TABLEAU", "DATABRICKS"]);
|
|
4
|
+
var vendorEnum = z.enum(["COMMON", "SNOWFLAKE", "SALESFORCE", "DBT", "DATABRICKS"]);
|
|
5
|
+
var aiContextObjectSchema = z.object({
|
|
6
|
+
instructions: z.string().optional(),
|
|
7
|
+
synonyms: z.array(z.string()).optional(),
|
|
8
|
+
examples: z.array(z.string()).optional()
|
|
9
|
+
});
|
|
10
|
+
var aiContextSchema = z.union([z.string(), aiContextObjectSchema]);
|
|
11
|
+
var customExtensionSchema = z.object({
|
|
12
|
+
vendor_name: vendorEnum,
|
|
13
|
+
data: z.string()
|
|
14
|
+
});
|
|
15
|
+
var dialectExpressionSchema = z.object({
|
|
16
|
+
dialect: dialectEnum,
|
|
17
|
+
expression: z.string()
|
|
18
|
+
});
|
|
19
|
+
var expressionSchema = z.object({
|
|
20
|
+
dialects: z.array(dialectExpressionSchema)
|
|
21
|
+
});
|
|
22
|
+
var dimensionSchema = z.object({
|
|
23
|
+
is_time: z.boolean().optional()
|
|
24
|
+
});
|
|
25
|
+
var osiFieldSchema = z.object({
|
|
26
|
+
name: z.string(),
|
|
27
|
+
expression: expressionSchema,
|
|
28
|
+
dimension: dimensionSchema.optional(),
|
|
29
|
+
label: z.string().optional(),
|
|
30
|
+
description: z.string().optional(),
|
|
31
|
+
ai_context: aiContextSchema.optional(),
|
|
32
|
+
custom_extensions: z.array(customExtensionSchema).optional()
|
|
33
|
+
});
|
|
34
|
+
var osiDatasetSchema = z.object({
|
|
35
|
+
name: z.string(),
|
|
36
|
+
source: z.string(),
|
|
37
|
+
primary_key: z.array(z.string()).optional(),
|
|
38
|
+
unique_keys: z.array(z.array(z.string())).optional(),
|
|
39
|
+
description: z.string().optional(),
|
|
40
|
+
ai_context: aiContextSchema.optional(),
|
|
41
|
+
fields: z.array(osiFieldSchema).optional(),
|
|
42
|
+
custom_extensions: z.array(customExtensionSchema).optional()
|
|
43
|
+
});
|
|
44
|
+
var osiRelationshipSchema = z.object({
|
|
45
|
+
name: z.string(),
|
|
46
|
+
from: z.string(),
|
|
47
|
+
to: z.string(),
|
|
48
|
+
from_columns: z.array(z.string()),
|
|
49
|
+
to_columns: z.array(z.string()),
|
|
50
|
+
ai_context: aiContextSchema.optional(),
|
|
51
|
+
custom_extensions: z.array(customExtensionSchema).optional()
|
|
52
|
+
});
|
|
53
|
+
var osiMetricSchema = z.object({
|
|
54
|
+
name: z.string(),
|
|
55
|
+
expression: expressionSchema,
|
|
56
|
+
description: z.string().optional(),
|
|
57
|
+
ai_context: aiContextSchema.optional(),
|
|
58
|
+
custom_extensions: z.array(customExtensionSchema).optional()
|
|
59
|
+
});
|
|
60
|
+
var osiSemanticModelSchema = z.object({
|
|
61
|
+
name: z.string(),
|
|
62
|
+
description: z.string().optional(),
|
|
63
|
+
ai_context: aiContextSchema.optional(),
|
|
64
|
+
datasets: z.array(osiDatasetSchema),
|
|
65
|
+
relationships: z.array(osiRelationshipSchema).optional(),
|
|
66
|
+
metrics: z.array(osiMetricSchema).optional(),
|
|
67
|
+
custom_extensions: z.array(customExtensionSchema).optional()
|
|
68
|
+
});
|
|
69
|
+
var osiDocumentSchema = z.object({
|
|
70
|
+
version: z.literal("1.0"),
|
|
71
|
+
semantic_model: z.array(osiSemanticModelSchema)
|
|
19
72
|
});
|
|
20
73
|
|
|
21
|
-
// src/schema/
|
|
74
|
+
// src/schema/governance.ts
|
|
22
75
|
import { z as z2 } from "zod";
|
|
23
|
-
var
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
76
|
+
var trustStatusEnum = z2.enum(["endorsed", "warning", "deprecated"]);
|
|
77
|
+
var securityClassificationEnum = z2.enum(["public", "internal", "confidential", "secret"]);
|
|
78
|
+
var tableTypeEnum = z2.enum(["fact", "dimension", "bridge", "snapshot", "event", "aggregate", "view"]);
|
|
79
|
+
var semanticRoleEnum = z2.enum(["metric", "dimension", "identifier", "date"]);
|
|
80
|
+
var defaultAggregationEnum = z2.enum(["SUM", "AVG", "COUNT", "COUNT_DISTINCT", "MIN", "MAX"]);
|
|
81
|
+
var datasetGovernanceSchema = z2.object({
|
|
82
|
+
grain: z2.string(),
|
|
83
|
+
refresh: z2.string().optional(),
|
|
84
|
+
table_type: tableTypeEnum,
|
|
85
|
+
security: securityClassificationEnum.optional()
|
|
86
|
+
});
|
|
87
|
+
var fieldGovernanceSchema = z2.object({
|
|
88
|
+
semantic_role: semanticRoleEnum,
|
|
89
|
+
default_aggregation: defaultAggregationEnum.optional(),
|
|
90
|
+
additive: z2.boolean().optional(),
|
|
91
|
+
default_filter: z2.string().optional(),
|
|
92
|
+
sample_values: z2.array(z2.string()).optional()
|
|
93
|
+
});
|
|
94
|
+
var dottedFieldsRecord = z2.record(z2.string(), fieldGovernanceSchema).refine(
|
|
95
|
+
(rec) => Object.keys(rec).every((key) => /^[^.]+\.[^.]+$/.test(key)),
|
|
96
|
+
{ message: 'Field keys must use "dataset.field" dot notation (e.g., "orders.amount")' }
|
|
97
|
+
);
|
|
98
|
+
var governanceFileSchema = z2.object({
|
|
99
|
+
model: z2.string(),
|
|
100
|
+
owner: z2.string(),
|
|
101
|
+
trust: trustStatusEnum.optional(),
|
|
102
|
+
security: securityClassificationEnum.optional(),
|
|
28
103
|
tags: z2.array(z2.string()).optional(),
|
|
29
|
-
|
|
104
|
+
datasets: z2.record(z2.string(), datasetGovernanceSchema).optional(),
|
|
105
|
+
fields: dottedFieldsRecord.optional()
|
|
30
106
|
});
|
|
31
107
|
|
|
32
|
-
// src/schema/
|
|
108
|
+
// src/schema/rules.ts
|
|
33
109
|
import { z as z3 } from "zod";
|
|
34
|
-
var
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
then: policyThenSchema
|
|
110
|
+
var goldenQuerySchema = z3.object({
|
|
111
|
+
question: z3.string(),
|
|
112
|
+
sql: z3.string(),
|
|
113
|
+
dialect: z3.string().optional(),
|
|
114
|
+
tags: z3.array(z3.string()).optional()
|
|
115
|
+
});
|
|
116
|
+
var businessRuleSchema = z3.object({
|
|
117
|
+
name: z3.string(),
|
|
118
|
+
definition: z3.string(),
|
|
119
|
+
enforcement: z3.array(z3.string()).optional(),
|
|
120
|
+
avoid: z3.array(z3.string()).optional(),
|
|
121
|
+
tables: z3.array(z3.string()).optional(),
|
|
122
|
+
applied_always: z3.boolean().optional()
|
|
48
123
|
});
|
|
49
|
-
var
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
124
|
+
var guardrailFilterSchema = z3.object({
|
|
125
|
+
name: z3.string(),
|
|
126
|
+
filter: z3.string(),
|
|
127
|
+
tables: z3.array(z3.string()).optional(),
|
|
128
|
+
reason: z3.string()
|
|
129
|
+
});
|
|
130
|
+
var hierarchySchema = z3.object({
|
|
131
|
+
name: z3.string(),
|
|
132
|
+
levels: z3.array(z3.string()),
|
|
133
|
+
dataset: z3.string(),
|
|
134
|
+
field: z3.string().optional()
|
|
135
|
+
});
|
|
136
|
+
var rulesFileSchema = z3.object({
|
|
137
|
+
model: z3.string(),
|
|
138
|
+
golden_queries: z3.array(goldenQuerySchema).optional(),
|
|
139
|
+
business_rules: z3.array(businessRuleSchema).optional(),
|
|
140
|
+
guardrail_filters: z3.array(guardrailFilterSchema).optional(),
|
|
141
|
+
hierarchies: z3.array(hierarchySchema).optional()
|
|
57
142
|
});
|
|
58
143
|
|
|
59
|
-
// src/schema/
|
|
144
|
+
// src/schema/lineage.ts
|
|
60
145
|
import { z as z4 } from "zod";
|
|
61
|
-
var
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
type:
|
|
146
|
+
var lineageTypeEnum = z4.enum(["pipeline", "dashboard", "ml_model", "api", "manual"]);
|
|
147
|
+
var upstreamEntrySchema = z4.object({
|
|
148
|
+
source: z4.string(),
|
|
149
|
+
type: lineageTypeEnum,
|
|
150
|
+
pipeline: z4.string().optional(),
|
|
151
|
+
tool: z4.string().optional(),
|
|
152
|
+
refresh: z4.string().optional(),
|
|
153
|
+
notes: z4.string().optional()
|
|
154
|
+
});
|
|
155
|
+
var downstreamEntrySchema = z4.object({
|
|
156
|
+
target: z4.string(),
|
|
157
|
+
type: lineageTypeEnum,
|
|
158
|
+
tool: z4.string().optional(),
|
|
159
|
+
notes: z4.string().optional()
|
|
65
160
|
});
|
|
66
|
-
var
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
description: z4.string().optional(),
|
|
71
|
-
owner: z4.string().optional(),
|
|
72
|
-
tags: z4.array(z4.string()).optional(),
|
|
73
|
-
status: z4.enum(["draft", "certified", "deprecated"]).optional(),
|
|
74
|
-
fields: z4.array(entityFieldSchema).optional()
|
|
161
|
+
var lineageFileSchema = z4.object({
|
|
162
|
+
model: z4.string(),
|
|
163
|
+
upstream: z4.array(upstreamEntrySchema).optional(),
|
|
164
|
+
downstream: z4.array(downstreamEntrySchema).optional()
|
|
75
165
|
});
|
|
76
166
|
|
|
77
167
|
// src/schema/term.ts
|
|
78
168
|
import { z as z5 } from "zod";
|
|
79
169
|
var termFileSchema = z5.object({
|
|
80
170
|
id: z5.string(),
|
|
81
|
-
term: z5.string().optional(),
|
|
82
171
|
definition: z5.string(),
|
|
83
172
|
synonyms: z5.array(z5.string()).optional(),
|
|
84
173
|
maps_to: z5.array(z5.string()).optional(),
|
|
@@ -93,598 +182,1202 @@ var ownerFileSchema = z6.object({
|
|
|
93
182
|
display_name: z6.string(),
|
|
94
183
|
email: z6.string().optional(),
|
|
95
184
|
team: z6.string().optional(),
|
|
96
|
-
|
|
185
|
+
description: z6.string().optional()
|
|
97
186
|
});
|
|
98
187
|
|
|
99
|
-
// src/config
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
188
|
+
// src/schema/config.ts
|
|
189
|
+
import { z as z7 } from "zod";
|
|
190
|
+
var metadataTierEnum = z7.enum(["none", "bronze", "silver", "gold"]);
|
|
191
|
+
var severityEnum = z7.enum(["error", "warning"]);
|
|
192
|
+
var severityOrOffEnum = z7.union([severityEnum, z7.literal("off")]);
|
|
193
|
+
var lintConfigSchema = z7.object({
|
|
194
|
+
severity_overrides: z7.record(z7.string(), severityOrOffEnum).optional()
|
|
195
|
+
});
|
|
196
|
+
var siteConfigSchema = z7.object({
|
|
197
|
+
title: z7.string().optional(),
|
|
198
|
+
base_path: z7.string().optional()
|
|
199
|
+
});
|
|
200
|
+
var mcpConfigSchema = z7.object({
|
|
201
|
+
transport: z7.enum(["stdio", "http"]).optional(),
|
|
202
|
+
port: z7.number().optional()
|
|
203
|
+
});
|
|
204
|
+
var contextKitConfigSchema = z7.object({
|
|
205
|
+
context_dir: z7.string().default("context"),
|
|
206
|
+
output_dir: z7.string().default("dist"),
|
|
207
|
+
minimum_tier: metadataTierEnum.optional(),
|
|
208
|
+
lint: lintConfigSchema.optional(),
|
|
209
|
+
site: siteConfigSchema.optional(),
|
|
210
|
+
mcp: mcpConfigSchema.optional()
|
|
211
|
+
});
|
|
117
212
|
|
|
118
|
-
// src/
|
|
119
|
-
import {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
213
|
+
// src/parser/discover.ts
|
|
214
|
+
import { glob } from "glob";
|
|
215
|
+
var PATTERNS = {
|
|
216
|
+
model: "**/*.osi.yaml",
|
|
217
|
+
governance: "**/*.governance.yaml",
|
|
218
|
+
rules: "**/*.rules.yaml",
|
|
219
|
+
lineage: "**/*.lineage.yaml",
|
|
220
|
+
term: "**/*.term.yaml",
|
|
221
|
+
owner: "**/*.owner.yaml"
|
|
222
|
+
};
|
|
223
|
+
async function discoverFiles(contextDir) {
|
|
224
|
+
const files = [];
|
|
225
|
+
for (const [kind, pattern] of Object.entries(PATTERNS)) {
|
|
226
|
+
const matches = await glob(pattern, { cwd: contextDir, absolute: true });
|
|
227
|
+
for (const match of matches) {
|
|
228
|
+
files.push({ path: match, kind });
|
|
134
229
|
}
|
|
135
230
|
}
|
|
136
|
-
return
|
|
231
|
+
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
137
232
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
233
|
+
|
|
234
|
+
// src/parser/parse.ts
|
|
235
|
+
import { readFile } from "fs/promises";
|
|
236
|
+
import { parse as parseYaml } from "yaml";
|
|
237
|
+
async function parseFile(filePath, kind) {
|
|
238
|
+
const content = await readFile(filePath, "utf-8");
|
|
239
|
+
const data = parseYaml(content);
|
|
240
|
+
return {
|
|
241
|
+
kind,
|
|
242
|
+
data,
|
|
243
|
+
source: { file: filePath, line: 1, column: 1 }
|
|
244
|
+
};
|
|
143
245
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
246
|
+
|
|
247
|
+
// src/compiler/validate.ts
|
|
248
|
+
var SCHEMA_MAP = {
|
|
249
|
+
model: osiDocumentSchema,
|
|
250
|
+
governance: governanceFileSchema,
|
|
251
|
+
rules: rulesFileSchema,
|
|
252
|
+
lineage: lineageFileSchema,
|
|
253
|
+
term: termFileSchema,
|
|
254
|
+
owner: ownerFileSchema
|
|
255
|
+
};
|
|
256
|
+
function zodErrorToDiagnostics(err, source) {
|
|
257
|
+
return err.issues.map((issue) => ({
|
|
258
|
+
ruleId: "schema/invalid",
|
|
259
|
+
severity: "error",
|
|
260
|
+
message: `${issue.path.join(".")}: ${issue.message}`,
|
|
261
|
+
location: { file: source.file, line: source.line, column: source.column },
|
|
262
|
+
fixable: false
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
function validate(parsed) {
|
|
266
|
+
const schema = SCHEMA_MAP[parsed.kind];
|
|
267
|
+
const parseResult = schema.safeParse(parsed.data);
|
|
268
|
+
if (!parseResult.success) {
|
|
269
|
+
return {
|
|
270
|
+
kind: parsed.kind,
|
|
271
|
+
data: void 0,
|
|
272
|
+
diagnostics: zodErrorToDiagnostics(parseResult.error, parsed.source)
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
let data = parseResult.data;
|
|
276
|
+
if (parsed.kind === "model") {
|
|
277
|
+
const doc = data;
|
|
278
|
+
data = doc.semantic_model[0];
|
|
167
279
|
}
|
|
168
|
-
return {
|
|
280
|
+
return {
|
|
281
|
+
kind: parsed.kind,
|
|
282
|
+
data,
|
|
283
|
+
diagnostics: []
|
|
284
|
+
};
|
|
169
285
|
}
|
|
170
286
|
|
|
171
|
-
// src/
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
"**/*.term.yaml",
|
|
181
|
-
"**/*.term.yml",
|
|
182
|
-
"**/*.entity.yaml",
|
|
183
|
-
"**/*.entity.yml"
|
|
184
|
-
];
|
|
185
|
-
async function discoverFiles(contextDir) {
|
|
186
|
-
const files = await glob(CONTEXT_PATTERNS, { cwd: contextDir, absolute: true, nodir: true });
|
|
187
|
-
return files.sort();
|
|
287
|
+
// src/compiler/resolve.ts
|
|
288
|
+
function diag(ruleId, message, file) {
|
|
289
|
+
return {
|
|
290
|
+
ruleId,
|
|
291
|
+
severity: "error",
|
|
292
|
+
message,
|
|
293
|
+
location: { file, line: 1, column: 1 },
|
|
294
|
+
fixable: false
|
|
295
|
+
};
|
|
188
296
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (name.endsWith(".policy.yaml") || name.endsWith(".policy.yml")) return "policy";
|
|
197
|
-
if (name.endsWith(".owner.yaml") || name.endsWith(".owner.yml")) return "owner";
|
|
198
|
-
if (name.endsWith(".term.yaml") || name.endsWith(".term.yml")) return "term";
|
|
199
|
-
if (name.endsWith(".entity.yaml") || name.endsWith(".entity.yml")) return "entity";
|
|
200
|
-
if (filePath.includes("/products/")) return "product";
|
|
201
|
-
if (filePath.includes("/entities/")) return "entity";
|
|
202
|
-
if (filePath.includes("/glossary/")) return "term";
|
|
203
|
-
return "concept";
|
|
297
|
+
function datasetNames(model) {
|
|
298
|
+
return new Set(model.datasets.map((d) => d.name));
|
|
299
|
+
}
|
|
300
|
+
function fieldNamesInDataset(model, datasetName) {
|
|
301
|
+
const dataset = model.datasets.find((d) => d.name === datasetName);
|
|
302
|
+
if (!dataset?.fields) return /* @__PURE__ */ new Set();
|
|
303
|
+
return new Set(dataset.fields.map((f) => f.name));
|
|
204
304
|
}
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
305
|
+
function resolveReferences(graph) {
|
|
306
|
+
const diagnostics = [];
|
|
307
|
+
for (const [key, gov] of graph.governance) {
|
|
308
|
+
const file = `governance:${key}`;
|
|
309
|
+
if (!graph.models.has(gov.model)) {
|
|
310
|
+
diagnostics.push(
|
|
311
|
+
diag("references/model-exists", `Governance references model "${gov.model}" which does not exist`, file)
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
if (!graph.owners.has(gov.owner)) {
|
|
315
|
+
diagnostics.push(
|
|
316
|
+
diag("references/owner-exists", `Governance references owner "${gov.owner}" which does not exist`, file)
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
if (gov.datasets) {
|
|
320
|
+
const model = graph.models.get(gov.model);
|
|
321
|
+
const validDatasets = model ? datasetNames(model) : /* @__PURE__ */ new Set();
|
|
322
|
+
for (const dsName of Object.keys(gov.datasets)) {
|
|
323
|
+
if (!validDatasets.has(dsName)) {
|
|
324
|
+
diagnostics.push(
|
|
325
|
+
diag("references/dataset-exists", `Governance references dataset "${dsName}" which does not exist in model "${gov.model}"`, file)
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (gov.fields) {
|
|
331
|
+
const model = graph.models.get(gov.model);
|
|
332
|
+
for (const fieldKey of Object.keys(gov.fields)) {
|
|
333
|
+
const parts = fieldKey.split(".");
|
|
334
|
+
if (parts.length !== 2) continue;
|
|
335
|
+
const [dsName, fieldName] = parts;
|
|
336
|
+
const validDatasets = model ? datasetNames(model) : /* @__PURE__ */ new Set();
|
|
337
|
+
if (!validDatasets.has(dsName)) {
|
|
338
|
+
diagnostics.push(
|
|
339
|
+
diag("references/dataset-exists", `Governance field key "${fieldKey}" references dataset "${dsName}" which does not exist in model "${gov.model}"`, file)
|
|
340
|
+
);
|
|
341
|
+
} else if (model) {
|
|
342
|
+
const validFields = fieldNamesInDataset(model, dsName);
|
|
343
|
+
if (!validFields.has(fieldName)) {
|
|
344
|
+
diagnostics.push(
|
|
345
|
+
diag("references/field-exists", `Governance field key "${fieldKey}" references field "${fieldName}" which does not exist in dataset "${dsName}"`, file)
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
for (const [key, rules] of graph.rules) {
|
|
353
|
+
const file = `rules:${key}`;
|
|
354
|
+
if (!graph.models.has(rules.model)) {
|
|
355
|
+
diagnostics.push(
|
|
356
|
+
diag("references/model-exists", `Rules file references model "${rules.model}" which does not exist`, file)
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
const model = graph.models.get(rules.model);
|
|
360
|
+
const validDatasets = model ? datasetNames(model) : /* @__PURE__ */ new Set();
|
|
361
|
+
if (rules.business_rules) {
|
|
362
|
+
for (const rule of rules.business_rules) {
|
|
363
|
+
if (rule.tables) {
|
|
364
|
+
for (const table of rule.tables) {
|
|
365
|
+
if (!validDatasets.has(table)) {
|
|
366
|
+
diagnostics.push(
|
|
367
|
+
diag("references/table-exists", `Business rule "${rule.name}" references table "${table}" which does not exist in model "${rules.model}"`, file)
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (rules.guardrail_filters) {
|
|
375
|
+
for (const filter of rules.guardrail_filters) {
|
|
376
|
+
if (filter.tables) {
|
|
377
|
+
for (const table of filter.tables) {
|
|
378
|
+
if (!validDatasets.has(table)) {
|
|
379
|
+
diagnostics.push(
|
|
380
|
+
diag("references/table-exists", `Guardrail filter "${filter.name}" references table "${table}" which does not exist in model "${rules.model}"`, file)
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (rules.hierarchies) {
|
|
388
|
+
for (const hierarchy of rules.hierarchies) {
|
|
389
|
+
if (!validDatasets.has(hierarchy.dataset)) {
|
|
390
|
+
diagnostics.push(
|
|
391
|
+
diag("references/table-exists", `Hierarchy "${hierarchy.name}" references dataset "${hierarchy.dataset}" which does not exist in model "${rules.model}"`, file)
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
210
396
|
}
|
|
211
|
-
|
|
397
|
+
for (const [key, lineage] of graph.lineage) {
|
|
398
|
+
const file = `lineage:${key}`;
|
|
399
|
+
if (!graph.models.has(lineage.model)) {
|
|
400
|
+
diagnostics.push(
|
|
401
|
+
diag("references/model-exists", `Lineage file references model "${lineage.model}" which does not exist`, file)
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
for (const [key, term] of graph.terms) {
|
|
406
|
+
const file = `term:${key}`;
|
|
407
|
+
if (term.owner && !graph.owners.has(term.owner)) {
|
|
408
|
+
diagnostics.push(
|
|
409
|
+
diag("references/owner-exists", `Term "${term.id}" references owner "${term.owner}" which does not exist`, file)
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
if (term.maps_to) {
|
|
413
|
+
for (const targetId of term.maps_to) {
|
|
414
|
+
if (!graph.terms.has(targetId)) {
|
|
415
|
+
diagnostics.push(
|
|
416
|
+
diag("references/term-exists", `Term "${term.id}" maps_to term "${targetId}" which does not exist`, file)
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return diagnostics;
|
|
212
423
|
}
|
|
213
424
|
|
|
214
|
-
// src/graph
|
|
425
|
+
// src/compiler/graph.ts
|
|
215
426
|
function createEmptyGraph() {
|
|
216
427
|
return {
|
|
217
|
-
|
|
218
|
-
|
|
428
|
+
models: /* @__PURE__ */ new Map(),
|
|
429
|
+
governance: /* @__PURE__ */ new Map(),
|
|
430
|
+
rules: /* @__PURE__ */ new Map(),
|
|
431
|
+
lineage: /* @__PURE__ */ new Map(),
|
|
432
|
+
terms: /* @__PURE__ */ new Map(),
|
|
433
|
+
owners: /* @__PURE__ */ new Map(),
|
|
434
|
+
tiers: /* @__PURE__ */ new Map(),
|
|
219
435
|
indexes: {
|
|
220
|
-
byKind: /* @__PURE__ */ new Map(),
|
|
221
436
|
byOwner: /* @__PURE__ */ new Map(),
|
|
222
437
|
byTag: /* @__PURE__ */ new Map(),
|
|
223
|
-
|
|
224
|
-
|
|
438
|
+
byTrust: /* @__PURE__ */ new Map(),
|
|
439
|
+
modelToGovernance: /* @__PURE__ */ new Map(),
|
|
440
|
+
modelToRules: /* @__PURE__ */ new Map(),
|
|
441
|
+
modelToLineage: /* @__PURE__ */ new Map()
|
|
225
442
|
}
|
|
226
443
|
};
|
|
227
444
|
}
|
|
228
|
-
function
|
|
445
|
+
function pushToIndex(map, key, value) {
|
|
446
|
+
const existing = map.get(key);
|
|
447
|
+
if (existing) {
|
|
448
|
+
existing.push(value);
|
|
449
|
+
} else {
|
|
450
|
+
map.set(key, [value]);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function buildGraph(results) {
|
|
229
454
|
const graph = createEmptyGraph();
|
|
230
|
-
for (const
|
|
231
|
-
|
|
455
|
+
for (const result of results) {
|
|
456
|
+
const hasErrors = result.diagnostics.some((d) => d.severity === "error");
|
|
457
|
+
if (hasErrors || result.data == null) continue;
|
|
458
|
+
switch (result.kind) {
|
|
459
|
+
case "model": {
|
|
460
|
+
const model = result.data;
|
|
461
|
+
graph.models.set(model.name, model);
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
case "governance": {
|
|
465
|
+
const gov = result.data;
|
|
466
|
+
graph.governance.set(gov.model, gov);
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
case "rules": {
|
|
470
|
+
const rules = result.data;
|
|
471
|
+
graph.rules.set(rules.model, rules);
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
case "lineage": {
|
|
475
|
+
const lineage = result.data;
|
|
476
|
+
graph.lineage.set(lineage.model, lineage);
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
case "term": {
|
|
480
|
+
const term = result.data;
|
|
481
|
+
graph.terms.set(term.id, term);
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
case "owner": {
|
|
485
|
+
const owner = result.data;
|
|
486
|
+
graph.owners.set(owner.id, owner);
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
for (const [govKey, gov] of graph.governance) {
|
|
492
|
+
pushToIndex(graph.indexes.byOwner, gov.owner, govKey);
|
|
493
|
+
if (gov.tags) {
|
|
494
|
+
for (const tag of gov.tags) {
|
|
495
|
+
pushToIndex(graph.indexes.byTag, tag, govKey);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (gov.trust) {
|
|
499
|
+
pushToIndex(graph.indexes.byTrust, gov.trust, govKey);
|
|
500
|
+
}
|
|
501
|
+
graph.indexes.modelToGovernance.set(gov.model, govKey);
|
|
502
|
+
}
|
|
503
|
+
for (const [rulesKey, rules] of graph.rules) {
|
|
504
|
+
graph.indexes.modelToRules.set(rules.model, rulesKey);
|
|
505
|
+
}
|
|
506
|
+
for (const [lineageKey, lineage] of graph.lineage) {
|
|
507
|
+
graph.indexes.modelToLineage.set(lineage.model, lineageKey);
|
|
232
508
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
509
|
+
return graph;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/tier/checks.ts
|
|
513
|
+
function allFieldKeys(model) {
|
|
514
|
+
const keys = [];
|
|
515
|
+
for (const ds of model.datasets) {
|
|
516
|
+
for (const f of ds.fields ?? []) {
|
|
517
|
+
keys.push(`${ds.name}.${f.name}`);
|
|
237
518
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
519
|
+
}
|
|
520
|
+
return keys;
|
|
521
|
+
}
|
|
522
|
+
function pass(id, label) {
|
|
523
|
+
return { id, label, passed: true };
|
|
524
|
+
}
|
|
525
|
+
function fail(id, label, detail) {
|
|
526
|
+
return { id, label, passed: false, detail };
|
|
527
|
+
}
|
|
528
|
+
function checkBronze(modelName, graph) {
|
|
529
|
+
const results = [];
|
|
530
|
+
const model = graph.models.get(modelName);
|
|
531
|
+
const gov = graph.governance.get(modelName);
|
|
532
|
+
{
|
|
533
|
+
const id = "bronze/model-description";
|
|
534
|
+
const label = "Model has name and description";
|
|
535
|
+
if (model && model.name && model.description) {
|
|
536
|
+
results.push(pass(id, label));
|
|
537
|
+
} else {
|
|
538
|
+
results.push(fail(id, label, "Model is missing name or description"));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
{
|
|
542
|
+
const id = "bronze/dataset-descriptions";
|
|
543
|
+
const label = "All datasets have descriptions";
|
|
544
|
+
if (!model || model.datasets.length === 0) {
|
|
545
|
+
results.push(fail(id, label, "No model or datasets found"));
|
|
546
|
+
} else {
|
|
547
|
+
const missing = model.datasets.filter((ds) => !ds.description);
|
|
548
|
+
if (missing.length === 0) {
|
|
549
|
+
results.push(pass(id, label));
|
|
550
|
+
} else {
|
|
551
|
+
results.push(fail(id, label, `Missing descriptions: ${missing.map((d) => d.name).join(", ")}`));
|
|
241
552
|
}
|
|
242
553
|
}
|
|
243
|
-
|
|
244
|
-
|
|
554
|
+
}
|
|
555
|
+
{
|
|
556
|
+
const id = "bronze/field-descriptions";
|
|
557
|
+
const label = "All fields have descriptions";
|
|
558
|
+
if (!model) {
|
|
559
|
+
results.push(fail(id, label, "No model found"));
|
|
560
|
+
} else {
|
|
561
|
+
const allFields = model.datasets.flatMap((ds) => ds.fields ?? []);
|
|
562
|
+
if (allFields.length === 0) {
|
|
563
|
+
results.push(fail(id, label, "No fields defined across any dataset"));
|
|
564
|
+
} else {
|
|
565
|
+
const missing = [];
|
|
566
|
+
for (const ds of model.datasets) {
|
|
567
|
+
for (const f of ds.fields ?? []) {
|
|
568
|
+
if (!f.description) {
|
|
569
|
+
missing.push(`${ds.name}.${f.name}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (missing.length === 0) {
|
|
574
|
+
results.push(pass(id, label));
|
|
575
|
+
} else {
|
|
576
|
+
results.push(fail(id, label, `Missing descriptions: ${missing.join(", ")}`));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
{
|
|
582
|
+
const id = "bronze/owner-resolvable";
|
|
583
|
+
const label = "Owner assigned and resolvable";
|
|
584
|
+
if (gov && gov.owner && graph.owners.has(gov.owner)) {
|
|
585
|
+
results.push(pass(id, label));
|
|
586
|
+
} else if (gov && gov.owner) {
|
|
587
|
+
results.push(fail(id, label, `Owner '${gov.owner}' not found in owners`));
|
|
588
|
+
} else {
|
|
589
|
+
results.push(fail(id, label, "No governance or owner assigned"));
|
|
245
590
|
}
|
|
246
591
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
592
|
+
{
|
|
593
|
+
const id = "bronze/security-classification";
|
|
594
|
+
const label = "Security classification set";
|
|
595
|
+
if (gov && gov.security) {
|
|
596
|
+
results.push(pass(id, label));
|
|
597
|
+
} else {
|
|
598
|
+
results.push(fail(id, label, "No security classification in governance"));
|
|
250
599
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
600
|
+
}
|
|
601
|
+
{
|
|
602
|
+
const id = "bronze/dataset-grain";
|
|
603
|
+
const label = "All datasets have grain statements";
|
|
604
|
+
if (!gov || !gov.datasets) {
|
|
605
|
+
results.push(fail(id, label, "No governance datasets"));
|
|
606
|
+
} else if (!model) {
|
|
607
|
+
results.push(fail(id, label, "No model found"));
|
|
608
|
+
} else {
|
|
609
|
+
const missing = [];
|
|
610
|
+
for (const ds of model.datasets) {
|
|
611
|
+
const dsGov = gov.datasets[ds.name];
|
|
612
|
+
if (!dsGov || !dsGov.grain) {
|
|
613
|
+
missing.push(ds.name);
|
|
257
614
|
}
|
|
258
615
|
}
|
|
259
|
-
if (
|
|
260
|
-
|
|
616
|
+
if (missing.length === 0) {
|
|
617
|
+
results.push(pass(id, label));
|
|
618
|
+
} else {
|
|
619
|
+
results.push(fail(id, label, `Missing grain: ${missing.join(", ")}`));
|
|
261
620
|
}
|
|
262
621
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
622
|
+
}
|
|
623
|
+
{
|
|
624
|
+
const id = "bronze/dataset-table-type";
|
|
625
|
+
const label = "All datasets have table_type";
|
|
626
|
+
if (!gov || !gov.datasets) {
|
|
627
|
+
results.push(fail(id, label, "No governance datasets"));
|
|
628
|
+
} else if (!model) {
|
|
629
|
+
results.push(fail(id, label, "No model found"));
|
|
630
|
+
} else {
|
|
631
|
+
const missing = [];
|
|
632
|
+
for (const ds of model.datasets) {
|
|
633
|
+
const dsGov = gov.datasets[ds.name];
|
|
634
|
+
if (!dsGov || !dsGov.table_type) {
|
|
635
|
+
missing.push(ds.name);
|
|
268
636
|
}
|
|
269
637
|
}
|
|
638
|
+
if (missing.length === 0) {
|
|
639
|
+
results.push(pass(id, label));
|
|
640
|
+
} else {
|
|
641
|
+
results.push(fail(id, label, `Missing table_type: ${missing.join(", ")}`));
|
|
642
|
+
}
|
|
270
643
|
}
|
|
271
644
|
}
|
|
272
|
-
return
|
|
645
|
+
return results;
|
|
273
646
|
}
|
|
274
|
-
function
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
647
|
+
function checkSilver(modelName, graph) {
|
|
648
|
+
const results = [];
|
|
649
|
+
const model = graph.models.get(modelName);
|
|
650
|
+
const gov = graph.governance.get(modelName);
|
|
651
|
+
const lineageKey = graph.indexes.modelToLineage.get(modelName);
|
|
652
|
+
const lineage = lineageKey ? graph.lineage.get(lineageKey) : void 0;
|
|
653
|
+
{
|
|
654
|
+
const id = "silver/trust-status";
|
|
655
|
+
const label = "Trust status is set";
|
|
656
|
+
if (gov && gov.trust) {
|
|
657
|
+
results.push(pass(id, label));
|
|
658
|
+
} else {
|
|
659
|
+
results.push(fail(id, label, "No trust status in governance"));
|
|
660
|
+
}
|
|
280
661
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return { diagnostics };
|
|
304
|
-
}
|
|
305
|
-
const data = result.data;
|
|
306
|
-
const source = { file: parsed.filePath, line: 1, col: 1 };
|
|
307
|
-
let node;
|
|
308
|
-
switch (parsed.fileType) {
|
|
309
|
-
case "concept": {
|
|
310
|
-
const d = data;
|
|
311
|
-
node = {
|
|
312
|
-
id: d.id,
|
|
313
|
-
kind: "concept",
|
|
314
|
-
source,
|
|
315
|
-
definition: d.definition,
|
|
316
|
-
owner: d.owner,
|
|
317
|
-
tags: d.tags,
|
|
318
|
-
status: d.status,
|
|
319
|
-
certified: d.certified,
|
|
320
|
-
productId: d.product_id,
|
|
321
|
-
dependsOn: d.depends_on,
|
|
322
|
-
evidence: d.evidence,
|
|
323
|
-
examples: d.examples,
|
|
324
|
-
description: d.description
|
|
325
|
-
};
|
|
326
|
-
break;
|
|
327
|
-
}
|
|
328
|
-
case "product": {
|
|
329
|
-
const d = data;
|
|
330
|
-
node = {
|
|
331
|
-
id: d.id,
|
|
332
|
-
kind: "product",
|
|
333
|
-
source,
|
|
334
|
-
description: d.description,
|
|
335
|
-
owner: d.owner,
|
|
336
|
-
tags: d.tags,
|
|
337
|
-
status: d.status
|
|
338
|
-
};
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
case "policy": {
|
|
342
|
-
const d = data;
|
|
343
|
-
node = {
|
|
344
|
-
id: d.id,
|
|
345
|
-
kind: "policy",
|
|
346
|
-
source,
|
|
347
|
-
description: d.description,
|
|
348
|
-
owner: d.owner,
|
|
349
|
-
tags: d.tags,
|
|
350
|
-
status: d.status,
|
|
351
|
-
rules: d.rules.map((r) => ({
|
|
352
|
-
priority: r.priority,
|
|
353
|
-
when: {
|
|
354
|
-
tagsAny: r.when.tags_any,
|
|
355
|
-
conceptIds: r.when.concept_ids,
|
|
356
|
-
status: r.when.status
|
|
357
|
-
},
|
|
358
|
-
then: {
|
|
359
|
-
requireRole: r.then.require_role,
|
|
360
|
-
deny: r.then.deny,
|
|
361
|
-
warn: r.then.warn
|
|
662
|
+
{
|
|
663
|
+
const id = "silver/min-tags";
|
|
664
|
+
const label = "At least 2 tags";
|
|
665
|
+
if (gov && gov.tags && gov.tags.length >= 2) {
|
|
666
|
+
results.push(pass(id, label));
|
|
667
|
+
} else {
|
|
668
|
+
const count = gov?.tags?.length ?? 0;
|
|
669
|
+
results.push(fail(id, label, `Found ${count} tag(s), need at least 2`));
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
{
|
|
673
|
+
const id = "silver/glossary-linked";
|
|
674
|
+
const label = "Glossary term linked";
|
|
675
|
+
let linked = false;
|
|
676
|
+
const govTags = new Set(gov?.tags ?? []);
|
|
677
|
+
const govOwner = gov?.owner;
|
|
678
|
+
for (const [, term] of graph.terms) {
|
|
679
|
+
if (term.tags) {
|
|
680
|
+
for (const tag of term.tags) {
|
|
681
|
+
if (govTags.has(tag)) {
|
|
682
|
+
linked = true;
|
|
683
|
+
break;
|
|
362
684
|
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (term.owner && term.owner === govOwner) {
|
|
688
|
+
linked = true;
|
|
689
|
+
}
|
|
690
|
+
if (linked) break;
|
|
691
|
+
}
|
|
692
|
+
if (graph.terms.size === 0) {
|
|
693
|
+
results.push(fail(id, label, "No glossary terms defined"));
|
|
694
|
+
} else if (linked) {
|
|
695
|
+
results.push(pass(id, label));
|
|
696
|
+
} else {
|
|
697
|
+
results.push(fail(id, label, "No glossary term shares tags or owner with this model"));
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
{
|
|
701
|
+
const id = "silver/upstream-lineage";
|
|
702
|
+
const label = "Upstream lineage exists";
|
|
703
|
+
if (lineage && lineage.upstream && lineage.upstream.length > 0) {
|
|
704
|
+
results.push(pass(id, label));
|
|
705
|
+
} else {
|
|
706
|
+
results.push(fail(id, label, "No upstream lineage defined"));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
{
|
|
710
|
+
const id = "silver/dataset-refresh";
|
|
711
|
+
const label = "All datasets have refresh cadence";
|
|
712
|
+
if (!gov || !gov.datasets || !model) {
|
|
713
|
+
results.push(fail(id, label, "No governance datasets or model"));
|
|
714
|
+
} else {
|
|
715
|
+
const missing = [];
|
|
716
|
+
for (const ds of model.datasets) {
|
|
717
|
+
const dsGov = gov.datasets[ds.name];
|
|
718
|
+
if (!dsGov || !dsGov.refresh) {
|
|
719
|
+
missing.push(ds.name);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (missing.length === 0) {
|
|
723
|
+
results.push(pass(id, label));
|
|
724
|
+
} else {
|
|
725
|
+
results.push(fail(id, label, `Missing refresh: ${missing.join(", ")}`));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
{
|
|
730
|
+
const id = "silver/sample-values";
|
|
731
|
+
const label = "At least 2 fields have sample_values";
|
|
732
|
+
let count = 0;
|
|
733
|
+
if (gov && gov.fields) {
|
|
734
|
+
for (const [, fg] of Object.entries(gov.fields)) {
|
|
735
|
+
if (fg.sample_values && fg.sample_values.length > 0) {
|
|
736
|
+
count++;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (count >= 2) {
|
|
741
|
+
results.push(pass(id, label));
|
|
742
|
+
} else {
|
|
743
|
+
results.push(fail(id, label, `Found ${count} field(s) with sample_values, need at least 2`));
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return results;
|
|
747
|
+
}
|
|
748
|
+
function checkGold(modelName, graph) {
|
|
749
|
+
const results = [];
|
|
750
|
+
const model = graph.models.get(modelName);
|
|
751
|
+
const gov = graph.governance.get(modelName);
|
|
752
|
+
const rulesKey = graph.indexes.modelToRules.get(modelName);
|
|
753
|
+
const rules = rulesKey ? graph.rules.get(rulesKey) : void 0;
|
|
754
|
+
const fieldKeys = model ? allFieldKeys(model) : [];
|
|
755
|
+
{
|
|
756
|
+
const id = "gold/field-semantic-role";
|
|
757
|
+
const label = "Every field has semantic_role";
|
|
758
|
+
if (!gov || !gov.fields) {
|
|
759
|
+
results.push(fail(id, label, "No governance fields"));
|
|
760
|
+
} else if (fieldKeys.length === 0) {
|
|
761
|
+
results.push(fail(id, label, "Model has no fields to verify"));
|
|
762
|
+
} else {
|
|
763
|
+
const missing = fieldKeys.filter((k) => !gov.fields[k]?.semantic_role);
|
|
764
|
+
if (missing.length === 0) {
|
|
765
|
+
results.push(pass(id, label));
|
|
766
|
+
} else {
|
|
767
|
+
results.push(fail(id, label, `Missing semantic_role: ${missing.join(", ")}`));
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
{
|
|
772
|
+
const id = "gold/metric-aggregation";
|
|
773
|
+
const label = "Every metric field has default_aggregation";
|
|
774
|
+
if (!gov || !gov.fields) {
|
|
775
|
+
results.push(fail(id, label, "No governance fields"));
|
|
776
|
+
} else {
|
|
777
|
+
const metricFields = Object.entries(gov.fields).filter(([, fg]) => fg.semantic_role === "metric");
|
|
778
|
+
if (metricFields.length === 0) {
|
|
779
|
+
results.push(fail(id, label, "No metric fields found"));
|
|
780
|
+
} else {
|
|
781
|
+
const missing = metricFields.filter(([, fg]) => !fg.default_aggregation);
|
|
782
|
+
if (missing.length === 0) {
|
|
783
|
+
results.push(pass(id, label));
|
|
784
|
+
} else {
|
|
785
|
+
results.push(fail(id, label, `Missing default_aggregation: ${missing.map(([k]) => k).join(", ")}`));
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
{
|
|
791
|
+
const id = "gold/metric-additive";
|
|
792
|
+
const label = "Every metric field has additive flag";
|
|
793
|
+
if (!gov || !gov.fields) {
|
|
794
|
+
results.push(fail(id, label, "No governance fields"));
|
|
795
|
+
} else {
|
|
796
|
+
const metricFields = Object.entries(gov.fields).filter(([, fg]) => fg.semantic_role === "metric");
|
|
797
|
+
if (metricFields.length === 0) {
|
|
798
|
+
results.push(fail(id, label, "No metric fields found"));
|
|
799
|
+
} else {
|
|
800
|
+
const missing = metricFields.filter(([, fg]) => fg.additive === void 0);
|
|
801
|
+
if (missing.length === 0) {
|
|
802
|
+
results.push(pass(id, label));
|
|
803
|
+
} else {
|
|
804
|
+
results.push(fail(id, label, `Missing additive flag: ${missing.map(([k]) => k).join(", ")}`));
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
{
|
|
810
|
+
const id = "gold/guardrail-filter";
|
|
811
|
+
const label = "At least 1 guardrail_filter exists";
|
|
812
|
+
if (rules && rules.guardrail_filters && rules.guardrail_filters.length >= 1) {
|
|
813
|
+
results.push(pass(id, label));
|
|
814
|
+
} else {
|
|
815
|
+
results.push(fail(id, label, "No guardrail filters found"));
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
{
|
|
819
|
+
const id = "gold/golden-queries";
|
|
820
|
+
const label = "At least 3 golden_queries exist";
|
|
821
|
+
const count = rules?.golden_queries?.length ?? 0;
|
|
822
|
+
if (count >= 3) {
|
|
823
|
+
results.push(pass(id, label));
|
|
824
|
+
} else {
|
|
825
|
+
results.push(fail(id, label, `Found ${count} golden queries, need at least 3`));
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
{
|
|
829
|
+
const id = "gold/business-rule";
|
|
830
|
+
const label = "At least 1 business_rule exists";
|
|
831
|
+
if (rules && rules.business_rules && rules.business_rules.length >= 1) {
|
|
832
|
+
results.push(pass(id, label));
|
|
833
|
+
} else {
|
|
834
|
+
results.push(fail(id, label, "No business rules found"));
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
{
|
|
838
|
+
const id = "gold/hierarchy";
|
|
839
|
+
const label = "At least 1 hierarchy exists";
|
|
840
|
+
if (rules && rules.hierarchies && rules.hierarchies.length >= 1) {
|
|
841
|
+
results.push(pass(id, label));
|
|
842
|
+
} else {
|
|
843
|
+
results.push(fail(id, label, "No hierarchies found"));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
{
|
|
847
|
+
const id = "gold/default-filter";
|
|
848
|
+
const label = "At least 1 field has default_filter";
|
|
849
|
+
let found = false;
|
|
850
|
+
if (gov && gov.fields) {
|
|
851
|
+
for (const [, fg] of Object.entries(gov.fields)) {
|
|
852
|
+
if (fg.default_filter) {
|
|
853
|
+
found = true;
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (found) {
|
|
859
|
+
results.push(pass(id, label));
|
|
860
|
+
} else {
|
|
861
|
+
results.push(fail(id, label, "No fields have default_filter"));
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
{
|
|
865
|
+
const id = "gold/trust-endorsed";
|
|
866
|
+
const label = "Trust is endorsed";
|
|
867
|
+
if (gov && gov.trust === "endorsed") {
|
|
868
|
+
results.push(pass(id, label));
|
|
869
|
+
} else {
|
|
870
|
+
results.push(fail(id, label, `Trust is '${gov?.trust ?? "unset"}', not 'endorsed'`));
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
{
|
|
874
|
+
const id = "gold/security-controls";
|
|
875
|
+
const label = "Security controls adequate";
|
|
876
|
+
const hasModelSecurity = !!(gov && gov.security);
|
|
877
|
+
let hasDatasetSecurity = false;
|
|
878
|
+
if (gov && gov.datasets) {
|
|
879
|
+
for (const [, dsGov] of Object.entries(gov.datasets)) {
|
|
880
|
+
if (dsGov.security) {
|
|
881
|
+
hasDatasetSecurity = true;
|
|
882
|
+
break;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (hasModelSecurity || hasDatasetSecurity) {
|
|
887
|
+
results.push(pass(id, label));
|
|
888
|
+
} else {
|
|
889
|
+
results.push(fail(id, label, "No security classification at model or dataset level"));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return results;
|
|
411
893
|
}
|
|
412
894
|
|
|
413
|
-
// src/
|
|
414
|
-
function
|
|
415
|
-
|
|
895
|
+
// src/tier/compute.ts
|
|
896
|
+
function computeTier(modelName, graph) {
|
|
897
|
+
const bronzeChecks = checkBronze(modelName, graph);
|
|
898
|
+
const silverChecks = checkSilver(modelName, graph);
|
|
899
|
+
const goldChecks = checkGold(modelName, graph);
|
|
900
|
+
const bronzePassed = bronzeChecks.every((c) => c.passed);
|
|
901
|
+
const silverPassed = silverChecks.every((c) => c.passed);
|
|
902
|
+
const goldPassed = goldChecks.every((c) => c.passed);
|
|
903
|
+
let tier;
|
|
904
|
+
if (bronzePassed && silverPassed && goldPassed) {
|
|
905
|
+
tier = "gold";
|
|
906
|
+
} else if (bronzePassed && silverPassed) {
|
|
907
|
+
tier = "silver";
|
|
908
|
+
} else if (bronzePassed) {
|
|
909
|
+
tier = "bronze";
|
|
910
|
+
} else {
|
|
911
|
+
tier = "none";
|
|
912
|
+
}
|
|
913
|
+
return {
|
|
914
|
+
model: modelName,
|
|
915
|
+
tier,
|
|
916
|
+
bronze: { passed: bronzePassed, checks: bronzeChecks },
|
|
917
|
+
silver: { passed: silverPassed, checks: silverChecks },
|
|
918
|
+
gold: { passed: goldPassed, checks: goldChecks }
|
|
919
|
+
};
|
|
416
920
|
}
|
|
417
|
-
function
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
921
|
+
function computeAllTiers(graph) {
|
|
922
|
+
for (const modelName of graph.models.keys()) {
|
|
923
|
+
const score = computeTier(modelName, graph);
|
|
924
|
+
graph.tiers.set(modelName, score);
|
|
421
925
|
}
|
|
422
|
-
return normalized;
|
|
423
926
|
}
|
|
424
927
|
|
|
425
928
|
// src/compiler/pipeline.ts
|
|
426
929
|
async function compile(options) {
|
|
427
|
-
const
|
|
428
|
-
const
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return { graph, diagnostics };
|
|
930
|
+
const allDiagnostics = [];
|
|
931
|
+
const discovered = await discoverFiles(options.contextDir);
|
|
932
|
+
const parsed = await Promise.all(
|
|
933
|
+
discovered.map((f) => parseFile(f.path, f.kind))
|
|
934
|
+
);
|
|
935
|
+
const validated = parsed.map(validate);
|
|
936
|
+
for (const result of validated) {
|
|
937
|
+
allDiagnostics.push(...result.diagnostics);
|
|
938
|
+
}
|
|
939
|
+
const graph = buildGraph(validated);
|
|
940
|
+
const refDiagnostics = resolveReferences(graph);
|
|
941
|
+
allDiagnostics.push(...refDiagnostics);
|
|
942
|
+
computeAllTiers(graph);
|
|
943
|
+
return { graph, diagnostics: allDiagnostics };
|
|
442
944
|
}
|
|
443
945
|
|
|
444
946
|
// src/compiler/emit.ts
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
encoding: "utf-8",
|
|
450
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
451
|
-
}).trim();
|
|
452
|
-
} catch {
|
|
453
|
-
return "unknown";
|
|
947
|
+
function mapToRecord(map) {
|
|
948
|
+
const record = {};
|
|
949
|
+
for (const [key, value] of map) {
|
|
950
|
+
record[key] = value;
|
|
454
951
|
}
|
|
952
|
+
return record;
|
|
455
953
|
}
|
|
456
|
-
function emitManifest(graph,
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
954
|
+
function emitManifest(graph, _config) {
|
|
955
|
+
return {
|
|
956
|
+
version: "0.2.0",
|
|
957
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
958
|
+
models: mapToRecord(graph.models),
|
|
959
|
+
governance: mapToRecord(graph.governance),
|
|
960
|
+
rules: mapToRecord(graph.rules),
|
|
961
|
+
lineage: mapToRecord(graph.lineage),
|
|
962
|
+
terms: mapToRecord(graph.terms),
|
|
963
|
+
owners: mapToRecord(graph.owners),
|
|
964
|
+
tiers: mapToRecord(graph.tiers)
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// src/linter/engine.ts
|
|
969
|
+
var LintEngine = class {
|
|
970
|
+
rules = [];
|
|
971
|
+
overrides = {};
|
|
972
|
+
constructor(overrides) {
|
|
973
|
+
if (overrides) this.overrides = overrides;
|
|
974
|
+
}
|
|
975
|
+
register(rule) {
|
|
976
|
+
this.rules.push(rule);
|
|
977
|
+
}
|
|
978
|
+
run(graph) {
|
|
979
|
+
const diagnostics = [];
|
|
980
|
+
for (const rule of this.rules) {
|
|
981
|
+
const override = this.overrides[rule.id];
|
|
982
|
+
if (override === "off") continue;
|
|
983
|
+
const results = rule.run(graph);
|
|
984
|
+
for (const d of results) {
|
|
985
|
+
diagnostics.push({
|
|
986
|
+
...d,
|
|
987
|
+
severity: override ?? rule.defaultSeverity
|
|
477
988
|
});
|
|
478
|
-
break;
|
|
479
989
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
990
|
+
}
|
|
991
|
+
return diagnostics.sort(
|
|
992
|
+
(a, b) => a.location.file.localeCompare(b.location.file) || a.location.line - b.location.line
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
// src/linter/rules/naming-id-kebab-case.ts
|
|
998
|
+
var KEBAB_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
999
|
+
var namingIdKebabCase = {
|
|
1000
|
+
id: "naming/id-kebab-case",
|
|
1001
|
+
defaultSeverity: "warning",
|
|
1002
|
+
description: "Owner and term IDs must be kebab-case",
|
|
1003
|
+
fixable: false,
|
|
1004
|
+
run(graph) {
|
|
1005
|
+
const diagnostics = [];
|
|
1006
|
+
for (const [key, owner] of graph.owners) {
|
|
1007
|
+
if (!KEBAB_RE.test(owner.id)) {
|
|
1008
|
+
diagnostics.push({
|
|
1009
|
+
ruleId: this.id,
|
|
1010
|
+
severity: this.defaultSeverity,
|
|
1011
|
+
message: `Owner ID "${owner.id}" is not kebab-case`,
|
|
1012
|
+
location: { file: `owner:${key}`, line: 1, column: 1 },
|
|
1013
|
+
fixable: false
|
|
488
1014
|
});
|
|
489
|
-
break;
|
|
490
1015
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
1016
|
+
}
|
|
1017
|
+
for (const [key, term] of graph.terms) {
|
|
1018
|
+
if (!KEBAB_RE.test(term.id)) {
|
|
1019
|
+
diagnostics.push({
|
|
1020
|
+
ruleId: this.id,
|
|
1021
|
+
severity: this.defaultSeverity,
|
|
1022
|
+
message: `Term ID "${term.id}" is not kebab-case`,
|
|
1023
|
+
location: { file: `term:${key}`, line: 1, column: 1 },
|
|
1024
|
+
fixable: false
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return diagnostics;
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
// src/linter/rules/descriptions-required.ts
|
|
1033
|
+
var descriptionsRequired = {
|
|
1034
|
+
id: "osi/descriptions-required",
|
|
1035
|
+
defaultSeverity: "warning",
|
|
1036
|
+
description: "OSI models, datasets, and fields must have descriptions",
|
|
1037
|
+
fixable: false,
|
|
1038
|
+
run(graph) {
|
|
1039
|
+
const diagnostics = [];
|
|
1040
|
+
for (const [key, model] of graph.models) {
|
|
1041
|
+
const file = `model:${key}`;
|
|
1042
|
+
if (!model.description) {
|
|
1043
|
+
diagnostics.push({
|
|
1044
|
+
ruleId: this.id,
|
|
1045
|
+
severity: this.defaultSeverity,
|
|
1046
|
+
message: `Model "${model.name}" is missing a description`,
|
|
1047
|
+
location: { file, line: 1, column: 1 },
|
|
1048
|
+
fixable: false
|
|
500
1049
|
});
|
|
501
|
-
break;
|
|
502
1050
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
1051
|
+
for (const dataset of model.datasets) {
|
|
1052
|
+
if (!dataset.description) {
|
|
1053
|
+
diagnostics.push({
|
|
1054
|
+
ruleId: this.id,
|
|
1055
|
+
severity: this.defaultSeverity,
|
|
1056
|
+
message: `Dataset "${dataset.name}" in model "${model.name}" is missing a description`,
|
|
1057
|
+
location: { file, line: 1, column: 1 },
|
|
1058
|
+
fixable: false
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
if (dataset.fields) {
|
|
1062
|
+
for (const field of dataset.fields) {
|
|
1063
|
+
if (!field.description) {
|
|
1064
|
+
diagnostics.push({
|
|
1065
|
+
ruleId: this.id,
|
|
1066
|
+
severity: this.defaultSeverity,
|
|
1067
|
+
message: `Field "${field.name}" in dataset "${dataset.name}" of model "${model.name}" is missing a description`,
|
|
1068
|
+
location: { file, line: 1, column: 1 },
|
|
1069
|
+
fixable: false
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return diagnostics;
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
// src/linter/rules/ownership-required.ts
|
|
1081
|
+
var ownershipRequired = {
|
|
1082
|
+
id: "governance/ownership-required",
|
|
1083
|
+
defaultSeverity: "error",
|
|
1084
|
+
description: "Every governance file must have an owner field set",
|
|
1085
|
+
fixable: false,
|
|
1086
|
+
run(graph) {
|
|
1087
|
+
const diagnostics = [];
|
|
1088
|
+
for (const [key, gov] of graph.governance) {
|
|
1089
|
+
if (!gov.owner) {
|
|
1090
|
+
diagnostics.push({
|
|
1091
|
+
ruleId: this.id,
|
|
1092
|
+
severity: this.defaultSeverity,
|
|
1093
|
+
message: `Governance for model "${gov.model}" is missing an owner`,
|
|
1094
|
+
location: { file: `governance:${key}`, line: 1, column: 1 },
|
|
1095
|
+
fixable: false
|
|
512
1096
|
});
|
|
513
|
-
break;
|
|
514
1097
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
1098
|
+
}
|
|
1099
|
+
return diagnostics;
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
// src/linter/rules/references-resolvable.ts
|
|
1104
|
+
var referencesResolvable = {
|
|
1105
|
+
id: "references/resolvable",
|
|
1106
|
+
defaultSeverity: "error",
|
|
1107
|
+
description: "All cross-file references must resolve to existing entities",
|
|
1108
|
+
fixable: false,
|
|
1109
|
+
run(graph) {
|
|
1110
|
+
return resolveReferences(graph);
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
// src/linter/rules/glossary-no-duplicate-terms.ts
|
|
1115
|
+
var glossaryNoDuplicateTerms = {
|
|
1116
|
+
id: "glossary/no-duplicate-synonyms",
|
|
1117
|
+
defaultSeverity: "warning",
|
|
1118
|
+
description: "No two terms should share the same synonym string",
|
|
1119
|
+
fixable: false,
|
|
1120
|
+
run(graph) {
|
|
1121
|
+
const diagnostics = [];
|
|
1122
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1123
|
+
for (const [key, term] of graph.terms) {
|
|
1124
|
+
if (!term.synonyms) continue;
|
|
1125
|
+
for (const synonym of term.synonyms) {
|
|
1126
|
+
const lower = synonym.toLowerCase();
|
|
1127
|
+
const existing = seen.get(lower);
|
|
1128
|
+
if (existing && existing !== term.id) {
|
|
1129
|
+
diagnostics.push({
|
|
1130
|
+
ruleId: this.id,
|
|
1131
|
+
severity: this.defaultSeverity,
|
|
1132
|
+
message: `Synonym "${synonym}" is used by both term "${existing}" and term "${term.id}"`,
|
|
1133
|
+
location: { file: `term:${key}`, line: 1, column: 1 },
|
|
1134
|
+
fixable: false
|
|
1135
|
+
});
|
|
1136
|
+
} else {
|
|
1137
|
+
seen.set(lower, term.id);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
return diagnostics;
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
// src/linter/rules/no-secrets.ts
|
|
1146
|
+
var SECRET_PATTERNS = [
|
|
1147
|
+
/password\s*[=:]\s*\S+/i,
|
|
1148
|
+
/api[_-]?key\s*[=:]\s*\S+/i,
|
|
1149
|
+
/secret\s*[=:]\s*\S+/i,
|
|
1150
|
+
/token\s*[=:]\s*\S+/i,
|
|
1151
|
+
/\bsk-[a-zA-Z0-9]{10,}\b/,
|
|
1152
|
+
/\bAKIA[A-Z0-9]{16}\b/
|
|
1153
|
+
// AWS access key
|
|
1154
|
+
];
|
|
1155
|
+
function checkString(value, context, file, diagnostics, ruleId, severity) {
|
|
1156
|
+
if (!value) return;
|
|
1157
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
1158
|
+
if (pattern.test(value)) {
|
|
1159
|
+
diagnostics.push({
|
|
1160
|
+
ruleId,
|
|
1161
|
+
severity,
|
|
1162
|
+
message: `Potential secret detected in ${context}: matches pattern ${pattern.source}`,
|
|
1163
|
+
location: { file, line: 1, column: 1 },
|
|
1164
|
+
fixable: false
|
|
1165
|
+
});
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
function aiContextToString(ctx) {
|
|
1171
|
+
if (!ctx) return void 0;
|
|
1172
|
+
if (typeof ctx === "string") return ctx;
|
|
1173
|
+
const parts = [];
|
|
1174
|
+
if (ctx.instructions) parts.push(ctx.instructions);
|
|
1175
|
+
if (ctx.synonyms) parts.push(ctx.synonyms.join(" "));
|
|
1176
|
+
if (ctx.examples) parts.push(ctx.examples.join(" "));
|
|
1177
|
+
return parts.join(" ") || void 0;
|
|
1178
|
+
}
|
|
1179
|
+
var noSecrets = {
|
|
1180
|
+
id: "security/no-secrets",
|
|
1181
|
+
defaultSeverity: "error",
|
|
1182
|
+
description: "Scan string values for patterns that look like secrets",
|
|
1183
|
+
fixable: false,
|
|
1184
|
+
run(graph) {
|
|
1185
|
+
const diagnostics = [];
|
|
1186
|
+
for (const [key, model] of graph.models) {
|
|
1187
|
+
const file = `model:${key}`;
|
|
1188
|
+
checkString(model.description, `model "${model.name}" description`, file, diagnostics, this.id, this.defaultSeverity);
|
|
1189
|
+
checkString(aiContextToString(model.ai_context), `model "${model.name}" ai_context`, file, diagnostics, this.id, this.defaultSeverity);
|
|
1190
|
+
for (const ds of model.datasets) {
|
|
1191
|
+
checkString(ds.description, `dataset "${ds.name}" description`, file, diagnostics, this.id, this.defaultSeverity);
|
|
1192
|
+
checkString(aiContextToString(ds.ai_context), `dataset "${ds.name}" ai_context`, file, diagnostics, this.id, this.defaultSeverity);
|
|
1193
|
+
if (ds.fields) {
|
|
1194
|
+
for (const f of ds.fields) {
|
|
1195
|
+
checkString(f.description, `field "${f.name}" description`, file, diagnostics, this.id, this.defaultSeverity);
|
|
1196
|
+
checkString(aiContextToString(f.ai_context), `field "${f.name}" ai_context`, file, diagnostics, this.id, this.defaultSeverity);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
for (const [key, gov] of graph.governance) {
|
|
1202
|
+
const file = `governance:${key}`;
|
|
1203
|
+
if (gov.datasets) {
|
|
1204
|
+
for (const [dsName, ds] of Object.entries(gov.datasets)) {
|
|
1205
|
+
checkString(ds.grain, `dataset "${dsName}" grain`, file, diagnostics, this.id, this.defaultSeverity);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
for (const [key, rules] of graph.rules) {
|
|
1210
|
+
const file = `rules:${key}`;
|
|
1211
|
+
if (rules.business_rules) {
|
|
1212
|
+
for (const br of rules.business_rules) {
|
|
1213
|
+
checkString(br.definition, `business rule "${br.name}" definition`, file, diagnostics, this.id, this.defaultSeverity);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return diagnostics;
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
// src/linter/rules/osi-valid-schema.ts
|
|
1222
|
+
var osiValidSchema = {
|
|
1223
|
+
id: "osi/valid-schema",
|
|
1224
|
+
defaultSeverity: "error",
|
|
1225
|
+
description: "OSI models must have at least one dataset",
|
|
1226
|
+
fixable: false,
|
|
1227
|
+
run(graph) {
|
|
1228
|
+
const diagnostics = [];
|
|
1229
|
+
for (const [key, model] of graph.models) {
|
|
1230
|
+
if (!model.datasets || model.datasets.length === 0) {
|
|
1231
|
+
diagnostics.push({
|
|
1232
|
+
ruleId: this.id,
|
|
1233
|
+
severity: this.defaultSeverity,
|
|
1234
|
+
message: `Model "${model.name}" has no datasets`,
|
|
1235
|
+
location: { file: `model:${key}`, line: 1, column: 1 },
|
|
1236
|
+
fixable: false
|
|
525
1237
|
});
|
|
526
|
-
break;
|
|
527
1238
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
1239
|
+
}
|
|
1240
|
+
return diagnostics;
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
// src/linter/rules/governance-model-exists.ts
|
|
1245
|
+
var governanceModelExists = {
|
|
1246
|
+
id: "governance/model-exists",
|
|
1247
|
+
defaultSeverity: "error",
|
|
1248
|
+
description: "Every governance file must reference a model that exists in the graph",
|
|
1249
|
+
fixable: false,
|
|
1250
|
+
run(graph) {
|
|
1251
|
+
const diagnostics = [];
|
|
1252
|
+
for (const [key, gov] of graph.governance) {
|
|
1253
|
+
if (!graph.models.has(gov.model)) {
|
|
1254
|
+
diagnostics.push({
|
|
1255
|
+
ruleId: this.id,
|
|
1256
|
+
severity: this.defaultSeverity,
|
|
1257
|
+
message: `Governance references model "${gov.model}" which does not exist`,
|
|
1258
|
+
location: { file: `governance:${key}`, line: 1, column: 1 },
|
|
1259
|
+
fixable: false
|
|
536
1260
|
});
|
|
537
|
-
break;
|
|
538
1261
|
}
|
|
539
1262
|
}
|
|
1263
|
+
return diagnostics;
|
|
540
1264
|
}
|
|
541
|
-
|
|
542
|
-
schemaVersion: "1.0.0",
|
|
543
|
-
project: {
|
|
544
|
-
id: config.project.id,
|
|
545
|
-
displayName: config.project.displayName,
|
|
546
|
-
version: config.project.version
|
|
547
|
-
},
|
|
548
|
-
build: {
|
|
549
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
550
|
-
version: getGitRevision(),
|
|
551
|
-
nodeCount: graph.nodes.size
|
|
552
|
-
},
|
|
553
|
-
concepts,
|
|
554
|
-
products,
|
|
555
|
-
policies,
|
|
556
|
-
entities,
|
|
557
|
-
terms,
|
|
558
|
-
owners,
|
|
559
|
-
indexes: { byId }
|
|
560
|
-
};
|
|
561
|
-
}
|
|
1265
|
+
};
|
|
562
1266
|
|
|
563
|
-
// src/linter/
|
|
564
|
-
var
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
}
|
|
570
|
-
/** Register a lint rule with the engine. */
|
|
571
|
-
register(rule) {
|
|
572
|
-
this.rules.push(rule);
|
|
573
|
-
}
|
|
574
|
-
/**
|
|
575
|
-
* Run all enabled rules against the graph and return sorted diagnostics.
|
|
576
|
-
*
|
|
577
|
-
* Diagnostics are sorted by source file (ascending) then line (ascending).
|
|
578
|
-
*/
|
|
1267
|
+
// src/linter/rules/governance-datasets-exist.ts
|
|
1268
|
+
var governanceDatasetsExist = {
|
|
1269
|
+
id: "governance/datasets-exist",
|
|
1270
|
+
defaultSeverity: "error",
|
|
1271
|
+
description: "Every dataset key in governance must exist as a dataset in the referenced OSI model",
|
|
1272
|
+
fixable: false,
|
|
579
1273
|
run(graph) {
|
|
580
|
-
const
|
|
581
|
-
for (const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
1274
|
+
const diagnostics = [];
|
|
1275
|
+
for (const [key, gov] of graph.governance) {
|
|
1276
|
+
if (!gov.datasets) continue;
|
|
1277
|
+
const model = graph.models.get(gov.model);
|
|
1278
|
+
if (!model) continue;
|
|
1279
|
+
const validDatasets = new Set(model.datasets.map((d) => d.name));
|
|
1280
|
+
for (const dsName of Object.keys(gov.datasets)) {
|
|
1281
|
+
if (!validDatasets.has(dsName)) {
|
|
1282
|
+
diagnostics.push({
|
|
1283
|
+
ruleId: this.id,
|
|
1284
|
+
severity: this.defaultSeverity,
|
|
1285
|
+
message: `Governance dataset "${dsName}" does not exist in model "${gov.model}"`,
|
|
1286
|
+
location: { file: `governance:${key}`, line: 1, column: 1 },
|
|
1287
|
+
fixable: false
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
590
1290
|
}
|
|
591
1291
|
}
|
|
592
|
-
|
|
593
|
-
const fileCmp = a.source.file.localeCompare(b.source.file);
|
|
594
|
-
if (fileCmp !== 0) return fileCmp;
|
|
595
|
-
return a.source.line - b.source.line;
|
|
596
|
-
});
|
|
597
|
-
return allDiagnostics;
|
|
1292
|
+
return diagnostics;
|
|
598
1293
|
}
|
|
599
1294
|
};
|
|
600
1295
|
|
|
601
|
-
// src/linter/rules/
|
|
602
|
-
var
|
|
603
|
-
id: "
|
|
1296
|
+
// src/linter/rules/governance-fields-exist.ts
|
|
1297
|
+
var governanceFieldsExist = {
|
|
1298
|
+
id: "governance/fields-exist",
|
|
604
1299
|
defaultSeverity: "error",
|
|
1300
|
+
description: "Every field key in governance must exist as a field in the referenced OSI model dataset",
|
|
605
1301
|
fixable: false,
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
1302
|
+
run(graph) {
|
|
1303
|
+
const diagnostics = [];
|
|
1304
|
+
for (const [key, gov] of graph.governance) {
|
|
1305
|
+
if (!gov.fields) continue;
|
|
1306
|
+
const model = graph.models.get(gov.model);
|
|
1307
|
+
if (!model) continue;
|
|
1308
|
+
for (const fieldKey of Object.keys(gov.fields)) {
|
|
1309
|
+
const parts = fieldKey.split(".");
|
|
1310
|
+
if (parts.length !== 2) continue;
|
|
1311
|
+
const [dsName, fieldName] = parts;
|
|
1312
|
+
const dataset = model.datasets.find((d) => d.name === dsName);
|
|
1313
|
+
if (!dataset) {
|
|
1314
|
+
diagnostics.push({
|
|
1315
|
+
ruleId: this.id,
|
|
1316
|
+
severity: this.defaultSeverity,
|
|
1317
|
+
message: `Governance field "${fieldKey}" references dataset "${dsName}" which does not exist in model "${gov.model}"`,
|
|
1318
|
+
location: { file: `governance:${key}`, line: 1, column: 1 },
|
|
1319
|
+
fixable: false
|
|
1320
|
+
});
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
const validFields = new Set(dataset.fields?.map((f) => f.name) ?? []);
|
|
1324
|
+
if (!validFields.has(fieldName)) {
|
|
1325
|
+
diagnostics.push({
|
|
1326
|
+
ruleId: this.id,
|
|
1327
|
+
severity: this.defaultSeverity,
|
|
1328
|
+
message: `Governance field "${fieldKey}" references field "${fieldName}" which does not exist in dataset "${dsName}"`,
|
|
1329
|
+
location: { file: `governance:${key}`, line: 1, column: 1 },
|
|
1330
|
+
fixable: false
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
return diagnostics;
|
|
609
1336
|
}
|
|
610
1337
|
};
|
|
611
1338
|
|
|
612
|
-
// src/linter/rules/
|
|
613
|
-
var
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
id: "naming/id-kebab-case",
|
|
619
|
-
defaultSeverity: "error",
|
|
620
|
-
fixable: true,
|
|
621
|
-
description: "IDs must be kebab-case",
|
|
1339
|
+
// src/linter/rules/governance-grain-required.ts
|
|
1340
|
+
var governanceGrainRequired = {
|
|
1341
|
+
id: "governance/grain-required",
|
|
1342
|
+
defaultSeverity: "warning",
|
|
1343
|
+
description: "Every dataset in governance must have a grain statement",
|
|
1344
|
+
fixable: false,
|
|
622
1345
|
run(graph) {
|
|
623
1346
|
const diagnostics = [];
|
|
624
|
-
for (const [,
|
|
625
|
-
if (!
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
{
|
|
637
|
-
file: node.source.file,
|
|
638
|
-
range: {
|
|
639
|
-
startLine: node.source.line,
|
|
640
|
-
startCol: node.source.col,
|
|
641
|
-
endLine: node.source.line,
|
|
642
|
-
endCol: node.source.col
|
|
643
|
-
},
|
|
644
|
-
newText: suggested
|
|
645
|
-
}
|
|
646
|
-
]
|
|
647
|
-
}
|
|
648
|
-
});
|
|
1347
|
+
for (const [key, gov] of graph.governance) {
|
|
1348
|
+
if (!gov.datasets) continue;
|
|
1349
|
+
for (const [dsName, ds] of Object.entries(gov.datasets)) {
|
|
1350
|
+
if (!ds.grain) {
|
|
1351
|
+
diagnostics.push({
|
|
1352
|
+
ruleId: this.id,
|
|
1353
|
+
severity: this.defaultSeverity,
|
|
1354
|
+
message: `Dataset "${dsName}" in governance for model "${gov.model}" is missing a grain statement`,
|
|
1355
|
+
location: { file: `governance:${key}`, line: 1, column: 1 },
|
|
1356
|
+
fixable: false
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
649
1359
|
}
|
|
650
1360
|
}
|
|
651
1361
|
return diagnostics;
|
|
652
1362
|
}
|
|
653
1363
|
};
|
|
654
1364
|
|
|
655
|
-
// src/linter/rules/
|
|
656
|
-
var
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
fixable:
|
|
661
|
-
description: "Concepts, products, and entities require an owner",
|
|
1365
|
+
// src/linter/rules/governance-security-required.ts
|
|
1366
|
+
var governanceSecurityRequired = {
|
|
1367
|
+
id: "governance/security-required",
|
|
1368
|
+
defaultSeverity: "warning",
|
|
1369
|
+
description: "Every governance file must have a security classification",
|
|
1370
|
+
fixable: false,
|
|
662
1371
|
run(graph) {
|
|
663
1372
|
const diagnostics = [];
|
|
664
|
-
for (const [,
|
|
665
|
-
if (!
|
|
666
|
-
if (!node.owner) {
|
|
1373
|
+
for (const [key, gov] of graph.governance) {
|
|
1374
|
+
if (!gov.security) {
|
|
667
1375
|
diagnostics.push({
|
|
668
|
-
ruleId:
|
|
669
|
-
severity:
|
|
670
|
-
message:
|
|
671
|
-
|
|
672
|
-
fixable:
|
|
673
|
-
fix: {
|
|
674
|
-
description: "Add owner field",
|
|
675
|
-
edits: [
|
|
676
|
-
{
|
|
677
|
-
file: node.source.file,
|
|
678
|
-
range: {
|
|
679
|
-
startLine: node.source.line,
|
|
680
|
-
startCol: node.source.col,
|
|
681
|
-
endLine: node.source.line,
|
|
682
|
-
endCol: node.source.col
|
|
683
|
-
},
|
|
684
|
-
newText: "owner: TODO\n"
|
|
685
|
-
}
|
|
686
|
-
]
|
|
687
|
-
}
|
|
1376
|
+
ruleId: this.id,
|
|
1377
|
+
severity: this.defaultSeverity,
|
|
1378
|
+
message: `Governance for model "${gov.model}" is missing a security classification`,
|
|
1379
|
+
location: { file: `governance:${key}`, line: 1, column: 1 },
|
|
1380
|
+
fixable: false
|
|
688
1381
|
});
|
|
689
1382
|
}
|
|
690
1383
|
}
|
|
@@ -692,46 +1385,22 @@ var ownershipRequired = {
|
|
|
692
1385
|
}
|
|
693
1386
|
};
|
|
694
1387
|
|
|
695
|
-
// src/linter/rules/
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
return true;
|
|
699
|
-
}
|
|
700
|
-
if (typeof node.definition === "string" && node.definition.length > 0) {
|
|
701
|
-
return true;
|
|
702
|
-
}
|
|
703
|
-
return false;
|
|
704
|
-
}
|
|
705
|
-
var descriptionsRequired = {
|
|
706
|
-
id: "descriptions/required",
|
|
1388
|
+
// src/linter/rules/governance-trust-required.ts
|
|
1389
|
+
var governanceTrustRequired = {
|
|
1390
|
+
id: "governance/trust-required",
|
|
707
1391
|
defaultSeverity: "warning",
|
|
708
|
-
|
|
709
|
-
|
|
1392
|
+
description: "Governance files must have a trust status set (endorsed/warning/deprecated)",
|
|
1393
|
+
fixable: false,
|
|
710
1394
|
run(graph) {
|
|
711
1395
|
const diagnostics = [];
|
|
712
|
-
for (const [,
|
|
713
|
-
if (!
|
|
1396
|
+
for (const [key, gov] of graph.governance) {
|
|
1397
|
+
if (!gov.trust) {
|
|
714
1398
|
diagnostics.push({
|
|
715
|
-
ruleId:
|
|
716
|
-
severity:
|
|
717
|
-
message:
|
|
718
|
-
|
|
719
|
-
fixable:
|
|
720
|
-
fix: {
|
|
721
|
-
description: "Add description field",
|
|
722
|
-
edits: [
|
|
723
|
-
{
|
|
724
|
-
file: node.source.file,
|
|
725
|
-
range: {
|
|
726
|
-
startLine: node.source.line,
|
|
727
|
-
startCol: node.source.col,
|
|
728
|
-
endLine: node.source.line,
|
|
729
|
-
endCol: node.source.col
|
|
730
|
-
},
|
|
731
|
-
newText: "description: TODO\n"
|
|
732
|
-
}
|
|
733
|
-
]
|
|
734
|
-
}
|
|
1399
|
+
ruleId: this.id,
|
|
1400
|
+
severity: this.defaultSeverity,
|
|
1401
|
+
message: `Governance for model "${gov.model}" is missing a trust status (endorsed/warning/deprecated)`,
|
|
1402
|
+
location: { file: `governance:${key}`, line: 1, column: 1 },
|
|
1403
|
+
fixable: false
|
|
735
1404
|
});
|
|
736
1405
|
}
|
|
737
1406
|
}
|
|
@@ -739,64 +1408,134 @@ var descriptionsRequired = {
|
|
|
739
1408
|
}
|
|
740
1409
|
};
|
|
741
1410
|
|
|
742
|
-
// src/linter/rules/
|
|
743
|
-
var
|
|
744
|
-
id: "
|
|
745
|
-
defaultSeverity: "
|
|
1411
|
+
// src/linter/rules/governance-refresh-required.ts
|
|
1412
|
+
var governanceRefreshRequired = {
|
|
1413
|
+
id: "governance/refresh-required",
|
|
1414
|
+
defaultSeverity: "warning",
|
|
1415
|
+
description: "All governed datasets must have a refresh cadence set",
|
|
1416
|
+
fixable: false,
|
|
1417
|
+
run(graph) {
|
|
1418
|
+
const diagnostics = [];
|
|
1419
|
+
for (const [key, gov] of graph.governance) {
|
|
1420
|
+
if (!gov.datasets) continue;
|
|
1421
|
+
for (const [dsName, ds] of Object.entries(gov.datasets)) {
|
|
1422
|
+
if (!ds.refresh) {
|
|
1423
|
+
diagnostics.push({
|
|
1424
|
+
ruleId: this.id,
|
|
1425
|
+
severity: this.defaultSeverity,
|
|
1426
|
+
message: `Dataset "${dsName}" in governance for model "${gov.model}" is missing a refresh cadence`,
|
|
1427
|
+
location: { file: `governance:${key}`, line: 1, column: 1 },
|
|
1428
|
+
fixable: false
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return diagnostics;
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
// src/linter/rules/lineage-upstream-required.ts
|
|
1438
|
+
var lineageUpstreamRequired = {
|
|
1439
|
+
id: "lineage/upstream-required",
|
|
1440
|
+
defaultSeverity: "warning",
|
|
1441
|
+
description: "Every governed model should have a lineage file with at least one upstream entry",
|
|
746
1442
|
fixable: false,
|
|
747
|
-
description: "All cross-node references must resolve to existing nodes",
|
|
748
1443
|
run(graph) {
|
|
749
1444
|
const diagnostics = [];
|
|
750
|
-
for (const [
|
|
751
|
-
|
|
752
|
-
if (
|
|
1445
|
+
for (const [modelName] of graph.governance) {
|
|
1446
|
+
const lineage = graph.lineage.get(modelName);
|
|
1447
|
+
if (!lineage || !lineage.upstream || lineage.upstream.length === 0) {
|
|
753
1448
|
diagnostics.push({
|
|
754
|
-
ruleId:
|
|
755
|
-
severity:
|
|
756
|
-
message:
|
|
757
|
-
|
|
1449
|
+
ruleId: this.id,
|
|
1450
|
+
severity: this.defaultSeverity,
|
|
1451
|
+
message: `Governed model "${modelName}" is missing lineage with at least one upstream entry`,
|
|
1452
|
+
location: { file: `governance:${modelName}`, line: 1, column: 1 },
|
|
758
1453
|
fixable: false
|
|
759
1454
|
});
|
|
760
1455
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1456
|
+
}
|
|
1457
|
+
return diagnostics;
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
|
|
1461
|
+
// src/linter/rules/governance-semantic-role-required.ts
|
|
1462
|
+
var governanceSemanticRoleRequired = {
|
|
1463
|
+
id: "governance/semantic-role-required",
|
|
1464
|
+
defaultSeverity: "warning",
|
|
1465
|
+
description: "Every field in every dataset of a governed model must have a governance entry with semantic_role",
|
|
1466
|
+
fixable: false,
|
|
1467
|
+
run(graph) {
|
|
1468
|
+
const diagnostics = [];
|
|
1469
|
+
for (const [modelName, model] of graph.models) {
|
|
1470
|
+
const gov = graph.governance.get(modelName);
|
|
1471
|
+
if (!gov) continue;
|
|
1472
|
+
const govFields = gov.fields ?? {};
|
|
1473
|
+
for (const dataset of model.datasets) {
|
|
1474
|
+
if (!dataset.fields) continue;
|
|
1475
|
+
for (const field of dataset.fields) {
|
|
1476
|
+
const fieldKey = `${dataset.name}.${field.name}`;
|
|
1477
|
+
const govField = govFields[fieldKey];
|
|
1478
|
+
if (!govField || !govField.semantic_role) {
|
|
1479
|
+
diagnostics.push({
|
|
1480
|
+
ruleId: this.id,
|
|
1481
|
+
severity: this.defaultSeverity,
|
|
1482
|
+
message: `Field "${fieldKey}" in model "${modelName}" is missing a semantic_role in governance`,
|
|
1483
|
+
location: { file: `governance:${modelName}`, line: 1, column: 1 },
|
|
1484
|
+
fixable: false
|
|
1485
|
+
});
|
|
774
1486
|
}
|
|
775
1487
|
}
|
|
776
|
-
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return diagnostics;
|
|
1491
|
+
}
|
|
1492
|
+
};
|
|
1493
|
+
|
|
1494
|
+
// src/linter/rules/governance-aggregation-required.ts
|
|
1495
|
+
var governanceAggregationRequired = {
|
|
1496
|
+
id: "governance/aggregation-required",
|
|
1497
|
+
defaultSeverity: "warning",
|
|
1498
|
+
description: 'Every field with semantic_role "metric" must have default_aggregation set',
|
|
1499
|
+
fixable: false,
|
|
1500
|
+
run(graph) {
|
|
1501
|
+
const diagnostics = [];
|
|
1502
|
+
for (const [key, gov] of graph.governance) {
|
|
1503
|
+
if (!gov.fields) continue;
|
|
1504
|
+
for (const [fieldName, field] of Object.entries(gov.fields)) {
|
|
1505
|
+
if (field.semantic_role === "metric" && !field.default_aggregation) {
|
|
777
1506
|
diagnostics.push({
|
|
778
|
-
ruleId:
|
|
779
|
-
severity:
|
|
780
|
-
message: `
|
|
781
|
-
|
|
1507
|
+
ruleId: this.id,
|
|
1508
|
+
severity: this.defaultSeverity,
|
|
1509
|
+
message: `Metric field "${fieldName}" in governance for model "${gov.model}" is missing default_aggregation`,
|
|
1510
|
+
location: { file: `governance:${key}`, line: 1, column: 1 },
|
|
782
1511
|
fixable: false
|
|
783
1512
|
});
|
|
784
1513
|
}
|
|
785
1514
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
1515
|
+
}
|
|
1516
|
+
return diagnostics;
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1520
|
+
// src/linter/rules/governance-additive-required.ts
|
|
1521
|
+
var governanceAdditiveRequired = {
|
|
1522
|
+
id: "governance/additive-required",
|
|
1523
|
+
defaultSeverity: "warning",
|
|
1524
|
+
description: 'Every field with semantic_role "metric" must have additive flag set',
|
|
1525
|
+
fixable: false,
|
|
1526
|
+
run(graph) {
|
|
1527
|
+
const diagnostics = [];
|
|
1528
|
+
for (const [key, gov] of graph.governance) {
|
|
1529
|
+
if (!gov.fields) continue;
|
|
1530
|
+
for (const [fieldName, field] of Object.entries(gov.fields)) {
|
|
1531
|
+
if (field.semantic_role === "metric" && field.additive == null) {
|
|
1532
|
+
diagnostics.push({
|
|
1533
|
+
ruleId: this.id,
|
|
1534
|
+
severity: this.defaultSeverity,
|
|
1535
|
+
message: `Metric field "${fieldName}" in governance for model "${gov.model}" is missing additive flag`,
|
|
1536
|
+
location: { file: `governance:${key}`, line: 1, column: 1 },
|
|
1537
|
+
fixable: false
|
|
1538
|
+
});
|
|
800
1539
|
}
|
|
801
1540
|
}
|
|
802
1541
|
}
|
|
@@ -804,57 +1543,46 @@ var referencesResolvable = {
|
|
|
804
1543
|
}
|
|
805
1544
|
};
|
|
806
1545
|
|
|
807
|
-
// src/linter/rules/
|
|
808
|
-
var
|
|
809
|
-
id: "
|
|
1546
|
+
// src/linter/rules/rules-golden-queries-minimum.ts
|
|
1547
|
+
var rulesGoldenQueriesMinimum = {
|
|
1548
|
+
id: "rules/golden-queries-minimum",
|
|
810
1549
|
defaultSeverity: "warning",
|
|
1550
|
+
description: "Models with a rules file must have at least 3 golden queries",
|
|
811
1551
|
fixable: false,
|
|
812
|
-
description: "Term definitions must be unique across the glossary",
|
|
813
1552
|
run(graph) {
|
|
814
1553
|
const diagnostics = [];
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
const node = graph.nodes.get(termId);
|
|
819
|
-
if (!node || node.kind !== "term") continue;
|
|
820
|
-
const term = node;
|
|
821
|
-
const normalized = term.definition.trim().toLowerCase();
|
|
822
|
-
const firstId = seen.get(normalized);
|
|
823
|
-
if (firstId !== void 0) {
|
|
1554
|
+
for (const [key, rules] of graph.rules) {
|
|
1555
|
+
const count = rules.golden_queries?.length ?? 0;
|
|
1556
|
+
if (count < 3) {
|
|
824
1557
|
diagnostics.push({
|
|
825
|
-
ruleId:
|
|
826
|
-
severity:
|
|
827
|
-
message: `
|
|
828
|
-
|
|
1558
|
+
ruleId: this.id,
|
|
1559
|
+
severity: this.defaultSeverity,
|
|
1560
|
+
message: `Rules for model "${rules.model}" has ${count} golden queries (minimum 3 required)`,
|
|
1561
|
+
location: { file: `rules:${key}`, line: 1, column: 1 },
|
|
829
1562
|
fixable: false
|
|
830
1563
|
});
|
|
831
|
-
} else {
|
|
832
|
-
seen.set(normalized, term.id);
|
|
833
1564
|
}
|
|
834
1565
|
}
|
|
835
1566
|
return diagnostics;
|
|
836
1567
|
}
|
|
837
1568
|
};
|
|
838
1569
|
|
|
839
|
-
// src/linter/rules/
|
|
840
|
-
var
|
|
841
|
-
id: "
|
|
842
|
-
defaultSeverity: "
|
|
1570
|
+
// src/linter/rules/rules-business-rules-exist.ts
|
|
1571
|
+
var rulesBusinessRulesExist = {
|
|
1572
|
+
id: "rules/business-rules-exist",
|
|
1573
|
+
defaultSeverity: "warning",
|
|
1574
|
+
description: "Models with a rules file must have at least 1 business rule",
|
|
843
1575
|
fixable: false,
|
|
844
|
-
description: "Certified concepts must include at least one evidence entry",
|
|
845
1576
|
run(graph) {
|
|
846
1577
|
const diagnostics = [];
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
if (!node || node.kind !== "concept") continue;
|
|
851
|
-
const concept = node;
|
|
852
|
-
if (concept.certified && (!concept.evidence || concept.evidence.length === 0)) {
|
|
1578
|
+
for (const [key, rules] of graph.rules) {
|
|
1579
|
+
const count = rules.business_rules?.length ?? 0;
|
|
1580
|
+
if (count < 1) {
|
|
853
1581
|
diagnostics.push({
|
|
854
|
-
ruleId:
|
|
855
|
-
severity:
|
|
856
|
-
message: `
|
|
857
|
-
|
|
1582
|
+
ruleId: this.id,
|
|
1583
|
+
severity: this.defaultSeverity,
|
|
1584
|
+
message: `Rules for model "${rules.model}" has no business rules (at least 1 required)`,
|
|
1585
|
+
location: { file: `rules:${key}`, line: 1, column: 1 },
|
|
858
1586
|
fixable: false
|
|
859
1587
|
});
|
|
860
1588
|
}
|
|
@@ -863,107 +1591,110 @@ var conceptsCertifiedRequiresEvidence = {
|
|
|
863
1591
|
}
|
|
864
1592
|
};
|
|
865
1593
|
|
|
866
|
-
// src/linter/rules/
|
|
867
|
-
var
|
|
868
|
-
id: "
|
|
1594
|
+
// src/linter/rules/rules-guardrails-exist.ts
|
|
1595
|
+
var rulesGuardrailsExist = {
|
|
1596
|
+
id: "rules/guardrails-exist",
|
|
869
1597
|
defaultSeverity: "warning",
|
|
1598
|
+
description: "Models with a rules file must have at least 1 guardrail filter",
|
|
870
1599
|
fixable: false,
|
|
871
|
-
description: "Policy selectors must reference known concepts and tags",
|
|
872
1600
|
run(graph) {
|
|
873
1601
|
const diagnostics = [];
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
diagnostics.push({
|
|
885
|
-
ruleId: "policies/unknown-subject",
|
|
886
|
-
severity: "warning",
|
|
887
|
-
message: `policy "${policy.id}" references unknown concept "${conceptId}"`,
|
|
888
|
-
source: policy.source,
|
|
889
|
-
fixable: false
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
if (rule.when.tagsAny) {
|
|
895
|
-
for (const tag of rule.when.tagsAny) {
|
|
896
|
-
if (!graph.indexes.byTag.has(tag)) {
|
|
897
|
-
diagnostics.push({
|
|
898
|
-
ruleId: "policies/unknown-subject",
|
|
899
|
-
severity: "warning",
|
|
900
|
-
message: `policy "${policy.id}" references unknown tag "${tag}"`,
|
|
901
|
-
source: policy.source,
|
|
902
|
-
fixable: false
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
}
|
|
1602
|
+
for (const [key, rules] of graph.rules) {
|
|
1603
|
+
const count = rules.guardrail_filters?.length ?? 0;
|
|
1604
|
+
if (count < 1) {
|
|
1605
|
+
diagnostics.push({
|
|
1606
|
+
ruleId: this.id,
|
|
1607
|
+
severity: this.defaultSeverity,
|
|
1608
|
+
message: `Rules for model "${rules.model}" has no guardrail filters (at least 1 required)`,
|
|
1609
|
+
location: { file: `rules:${key}`, line: 1, column: 1 },
|
|
1610
|
+
fixable: false
|
|
1611
|
+
});
|
|
907
1612
|
}
|
|
908
1613
|
}
|
|
909
1614
|
return diagnostics;
|
|
910
1615
|
}
|
|
911
1616
|
};
|
|
912
1617
|
|
|
913
|
-
// src/linter/rules/
|
|
914
|
-
var
|
|
915
|
-
id: "
|
|
1618
|
+
// src/linter/rules/rules-hierarchies-exist.ts
|
|
1619
|
+
var rulesHierarchiesExist = {
|
|
1620
|
+
id: "rules/hierarchies-exist",
|
|
916
1621
|
defaultSeverity: "warning",
|
|
1622
|
+
description: "Models with a rules file must have at least 1 hierarchy",
|
|
917
1623
|
fixable: false,
|
|
918
|
-
description: "Deny rules should have higher priority than allow rules",
|
|
919
1624
|
run(graph) {
|
|
920
1625
|
const diagnostics = [];
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
for (const rule of policy.rules) {
|
|
933
|
-
if (rule.then.deny && maxAllowPriority > -Infinity && rule.priority <= maxAllowPriority) {
|
|
934
|
-
diagnostics.push({
|
|
935
|
-
ruleId: "policies/deny-overrides-allow",
|
|
936
|
-
severity: "warning",
|
|
937
|
-
message: `policy "${policy.id}" has a deny rule with priority ${rule.priority} that does not override allow rules (max allow priority: ${maxAllowPriority})`,
|
|
938
|
-
source: policy.source,
|
|
939
|
-
fixable: false
|
|
940
|
-
});
|
|
941
|
-
}
|
|
1626
|
+
for (const [key, rules] of graph.rules) {
|
|
1627
|
+
const count = rules.hierarchies?.length ?? 0;
|
|
1628
|
+
if (count < 1) {
|
|
1629
|
+
diagnostics.push({
|
|
1630
|
+
ruleId: this.id,
|
|
1631
|
+
severity: this.defaultSeverity,
|
|
1632
|
+
message: `Rules for model "${rules.model}" has no hierarchies (at least 1 required)`,
|
|
1633
|
+
location: { file: `rules:${key}`, line: 1, column: 1 },
|
|
1634
|
+
fixable: false
|
|
1635
|
+
});
|
|
942
1636
|
}
|
|
943
1637
|
}
|
|
944
1638
|
return diagnostics;
|
|
945
1639
|
}
|
|
946
1640
|
};
|
|
947
1641
|
|
|
948
|
-
// src/linter/rules/
|
|
949
|
-
var
|
|
950
|
-
id: "
|
|
1642
|
+
// src/linter/rules/tier-bronze.ts
|
|
1643
|
+
var tierBronze = {
|
|
1644
|
+
id: "tier/bronze-requirements",
|
|
951
1645
|
defaultSeverity: "warning",
|
|
1646
|
+
description: "Checks all Bronze tier requirements as a composite",
|
|
952
1647
|
fixable: false,
|
|
953
|
-
description: "Certified concepts should include at least one example",
|
|
954
1648
|
run(graph) {
|
|
955
1649
|
const diagnostics = [];
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1650
|
+
for (const [modelName, model] of graph.models) {
|
|
1651
|
+
const missing = [];
|
|
1652
|
+
if (!model.description) {
|
|
1653
|
+
missing.push("model description");
|
|
1654
|
+
}
|
|
1655
|
+
for (const dataset of model.datasets) {
|
|
1656
|
+
if (!dataset.description) {
|
|
1657
|
+
missing.push(`dataset "${dataset.name}" description`);
|
|
1658
|
+
}
|
|
1659
|
+
if (dataset.fields) {
|
|
1660
|
+
for (const field of dataset.fields) {
|
|
1661
|
+
if (!field.description) {
|
|
1662
|
+
missing.push(`field "${dataset.name}.${field.name}" description`);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
const gov = graph.governance.get(modelName);
|
|
1668
|
+
if (!gov) {
|
|
1669
|
+
missing.push("governance file");
|
|
1670
|
+
} else {
|
|
1671
|
+
if (!gov.owner) {
|
|
1672
|
+
missing.push("governance owner");
|
|
1673
|
+
}
|
|
1674
|
+
if (!gov.security) {
|
|
1675
|
+
missing.push("security classification");
|
|
1676
|
+
}
|
|
1677
|
+
if (gov.datasets) {
|
|
1678
|
+
for (const [dsName, ds] of Object.entries(gov.datasets)) {
|
|
1679
|
+
if (!ds.grain) {
|
|
1680
|
+
missing.push(`dataset "${dsName}" grain`);
|
|
1681
|
+
}
|
|
1682
|
+
if (!ds.table_type) {
|
|
1683
|
+
missing.push(`dataset "${dsName}" table_type`);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
} else {
|
|
1687
|
+
if (model.datasets.length > 0) {
|
|
1688
|
+
missing.push("governance datasets");
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
if (missing.length > 0) {
|
|
962
1693
|
diagnostics.push({
|
|
963
|
-
ruleId:
|
|
964
|
-
severity:
|
|
965
|
-
message: `
|
|
966
|
-
|
|
1694
|
+
ruleId: this.id,
|
|
1695
|
+
severity: this.defaultSeverity,
|
|
1696
|
+
message: `Model "${modelName}" is missing Bronze requirements: ${missing.join(", ")}`,
|
|
1697
|
+
location: { file: `model:${modelName}`, line: 1, column: 1 },
|
|
967
1698
|
fixable: false
|
|
968
1699
|
});
|
|
969
1700
|
}
|
|
@@ -972,25 +1703,56 @@ var docsExamplesRequired = {
|
|
|
972
1703
|
}
|
|
973
1704
|
};
|
|
974
1705
|
|
|
975
|
-
// src/linter/rules/
|
|
976
|
-
var
|
|
977
|
-
id: "
|
|
1706
|
+
// src/linter/rules/tier-silver.ts
|
|
1707
|
+
var tierSilver = {
|
|
1708
|
+
id: "tier/silver-requirements",
|
|
978
1709
|
defaultSeverity: "warning",
|
|
1710
|
+
description: "Checks all Silver tier requirements (beyond Bronze) as a composite",
|
|
979
1711
|
fixable: false,
|
|
980
|
-
description: "Deprecated nodes must include a sunset date tag",
|
|
981
1712
|
run(graph) {
|
|
982
1713
|
const diagnostics = [];
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
if (!
|
|
1714
|
+
for (const [modelName, gov] of graph.governance) {
|
|
1715
|
+
const missing = [];
|
|
1716
|
+
if (!gov.trust) {
|
|
1717
|
+
missing.push("trust status");
|
|
1718
|
+
}
|
|
1719
|
+
if (!gov.tags || gov.tags.length < 2) {
|
|
1720
|
+
missing.push(`tags (need >=2, have ${gov.tags?.length ?? 0})`);
|
|
1721
|
+
}
|
|
1722
|
+
const hasGlossaryLink = Array.from(graph.terms.values()).some(
|
|
1723
|
+
(term) => term.owner === gov.owner
|
|
1724
|
+
);
|
|
1725
|
+
if (!hasGlossaryLink) {
|
|
1726
|
+
missing.push("glossary linked");
|
|
1727
|
+
}
|
|
1728
|
+
const lineage = graph.lineage.get(modelName);
|
|
1729
|
+
if (!lineage || !lineage.upstream || lineage.upstream.length === 0) {
|
|
1730
|
+
missing.push("upstream lineage");
|
|
1731
|
+
}
|
|
1732
|
+
if (gov.datasets) {
|
|
1733
|
+
for (const [dsName, ds] of Object.entries(gov.datasets)) {
|
|
1734
|
+
if (!ds.refresh) {
|
|
1735
|
+
missing.push(`refresh cadence on dataset "${dsName}"`);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
let sampleValuesCount = 0;
|
|
1740
|
+
if (gov.fields) {
|
|
1741
|
+
for (const field of Object.values(gov.fields)) {
|
|
1742
|
+
if (field.sample_values && field.sample_values.length > 0) {
|
|
1743
|
+
sampleValuesCount++;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
if (sampleValuesCount < 2) {
|
|
1748
|
+
missing.push(`sample values on >=2 fields (have ${sampleValuesCount})`);
|
|
1749
|
+
}
|
|
1750
|
+
if (missing.length > 0) {
|
|
989
1751
|
diagnostics.push({
|
|
990
|
-
ruleId:
|
|
991
|
-
severity:
|
|
992
|
-
message:
|
|
993
|
-
|
|
1752
|
+
ruleId: this.id,
|
|
1753
|
+
severity: this.defaultSeverity,
|
|
1754
|
+
message: `Model "${modelName}" is missing Silver requirements: ${missing.join(", ")}`,
|
|
1755
|
+
location: { file: `governance:${modelName}`, line: 1, column: 1 },
|
|
994
1756
|
fixable: false
|
|
995
1757
|
});
|
|
996
1758
|
}
|
|
@@ -999,70 +1761,87 @@ var deprecationRequireSunset = {
|
|
|
999
1761
|
}
|
|
1000
1762
|
};
|
|
1001
1763
|
|
|
1002
|
-
// src/linter/rules/
|
|
1003
|
-
var
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
{ label: "token assignment", re: /token\s*[:=]\s*\S+/i },
|
|
1008
|
-
{ label: "API key (long hex/alnum)", re: /(?:api[_-]?key|apikey)\s*[:=]\s*[A-Za-z0-9]{16,}/ }
|
|
1009
|
-
];
|
|
1010
|
-
function detectSecret(text) {
|
|
1011
|
-
for (const { label, re } of SECRET_PATTERNS) {
|
|
1012
|
-
if (re.test(text)) return label;
|
|
1013
|
-
}
|
|
1014
|
-
return void 0;
|
|
1015
|
-
}
|
|
1016
|
-
var packagingNoSecrets = {
|
|
1017
|
-
id: "packaging/no-secrets",
|
|
1018
|
-
defaultSeverity: "error",
|
|
1764
|
+
// src/linter/rules/tier-gold.ts
|
|
1765
|
+
var tierGold = {
|
|
1766
|
+
id: "tier/gold-requirements",
|
|
1767
|
+
defaultSeverity: "warning",
|
|
1768
|
+
description: "Checks all Gold tier requirements (beyond Silver) as a composite",
|
|
1019
1769
|
fixable: false,
|
|
1020
|
-
description: "Node content must not contain secret patterns (API keys, passwords, tokens)",
|
|
1021
1770
|
run(graph) {
|
|
1022
1771
|
const diagnostics = [];
|
|
1023
|
-
for (const [,
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
ruleId: "packaging/no-secrets",
|
|
1029
|
-
severity: "error",
|
|
1030
|
-
message: `${node.kind} "${node.id}" description contains a potential ${match}`,
|
|
1031
|
-
source: node.source,
|
|
1032
|
-
fixable: false
|
|
1033
|
-
});
|
|
1034
|
-
}
|
|
1772
|
+
for (const [modelName, gov] of graph.governance) {
|
|
1773
|
+
const missing = [];
|
|
1774
|
+
const model = graph.models.get(modelName);
|
|
1775
|
+
if (gov.trust !== "endorsed") {
|
|
1776
|
+
missing.push("trust must be endorsed");
|
|
1035
1777
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
}
|
|
1778
|
+
if (model) {
|
|
1779
|
+
const govFields = gov.fields ?? {};
|
|
1780
|
+
for (const dataset of model.datasets) {
|
|
1781
|
+
if (!dataset.fields) continue;
|
|
1782
|
+
for (const field of dataset.fields) {
|
|
1783
|
+
const fieldKey = `${dataset.name}.${field.name}`;
|
|
1784
|
+
const govField = govFields[fieldKey];
|
|
1785
|
+
if (!govField || !govField.semantic_role) {
|
|
1786
|
+
missing.push(`semantic_role for "${fieldKey}"`);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1047
1789
|
}
|
|
1048
1790
|
}
|
|
1049
|
-
if (
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
if (match) {
|
|
1055
|
-
diagnostics.push({
|
|
1056
|
-
ruleId: "packaging/no-secrets",
|
|
1057
|
-
severity: "error",
|
|
1058
|
-
message: `concept "${concept.id}" example "${example.label}" contains a potential ${match}`,
|
|
1059
|
-
source: concept.source,
|
|
1060
|
-
fixable: false
|
|
1061
|
-
});
|
|
1791
|
+
if (gov.fields) {
|
|
1792
|
+
for (const [fieldName, field] of Object.entries(gov.fields)) {
|
|
1793
|
+
if (field.semantic_role === "metric") {
|
|
1794
|
+
if (!field.default_aggregation) {
|
|
1795
|
+
missing.push(`aggregation for metric "${fieldName}"`);
|
|
1062
1796
|
}
|
|
1797
|
+
if (field.additive == null) {
|
|
1798
|
+
missing.push(`additive for metric "${fieldName}"`);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
let hasDefaultFilter = false;
|
|
1804
|
+
if (gov.fields) {
|
|
1805
|
+
for (const field of Object.values(gov.fields)) {
|
|
1806
|
+
if (field.default_filter) {
|
|
1807
|
+
hasDefaultFilter = true;
|
|
1808
|
+
break;
|
|
1063
1809
|
}
|
|
1064
1810
|
}
|
|
1065
1811
|
}
|
|
1812
|
+
if (!hasDefaultFilter) {
|
|
1813
|
+
missing.push("default filters");
|
|
1814
|
+
}
|
|
1815
|
+
const rules = graph.rules.get(modelName);
|
|
1816
|
+
if (!rules) {
|
|
1817
|
+
missing.push("rules file (golden queries, business rules, guardrails, hierarchies)");
|
|
1818
|
+
} else {
|
|
1819
|
+
const goldenCount = rules.golden_queries?.length ?? 0;
|
|
1820
|
+
if (goldenCount < 3) {
|
|
1821
|
+
missing.push(`>=3 golden queries (have ${goldenCount})`);
|
|
1822
|
+
}
|
|
1823
|
+
const bizCount = rules.business_rules?.length ?? 0;
|
|
1824
|
+
if (bizCount < 1) {
|
|
1825
|
+
missing.push("business rules");
|
|
1826
|
+
}
|
|
1827
|
+
const guardCount = rules.guardrail_filters?.length ?? 0;
|
|
1828
|
+
if (guardCount < 1) {
|
|
1829
|
+
missing.push("guardrail filters");
|
|
1830
|
+
}
|
|
1831
|
+
const hierCount = rules.hierarchies?.length ?? 0;
|
|
1832
|
+
if (hierCount < 1) {
|
|
1833
|
+
missing.push("hierarchies");
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
if (missing.length > 0) {
|
|
1837
|
+
diagnostics.push({
|
|
1838
|
+
ruleId: this.id,
|
|
1839
|
+
severity: this.defaultSeverity,
|
|
1840
|
+
message: `Model "${modelName}" is missing Gold requirements: ${missing.join(", ")}`,
|
|
1841
|
+
location: { file: `governance:${modelName}`, line: 1, column: 1 },
|
|
1842
|
+
fixable: false
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1066
1845
|
}
|
|
1067
1846
|
return diagnostics;
|
|
1068
1847
|
}
|
|
@@ -1070,123 +1849,159 @@ var packagingNoSecrets = {
|
|
|
1070
1849
|
|
|
1071
1850
|
// src/linter/rules/index.ts
|
|
1072
1851
|
var ALL_RULES = [
|
|
1073
|
-
|
|
1852
|
+
// Bronze (12)
|
|
1074
1853
|
namingIdKebabCase,
|
|
1075
|
-
ownershipRequired,
|
|
1076
1854
|
descriptionsRequired,
|
|
1855
|
+
ownershipRequired,
|
|
1077
1856
|
referencesResolvable,
|
|
1078
1857
|
glossaryNoDuplicateTerms,
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1858
|
+
noSecrets,
|
|
1859
|
+
osiValidSchema,
|
|
1860
|
+
governanceModelExists,
|
|
1861
|
+
governanceDatasetsExist,
|
|
1862
|
+
governanceFieldsExist,
|
|
1863
|
+
governanceGrainRequired,
|
|
1864
|
+
governanceSecurityRequired,
|
|
1865
|
+
// Silver (3)
|
|
1866
|
+
governanceTrustRequired,
|
|
1867
|
+
governanceRefreshRequired,
|
|
1868
|
+
lineageUpstreamRequired,
|
|
1869
|
+
// Gold (7)
|
|
1870
|
+
governanceSemanticRoleRequired,
|
|
1871
|
+
governanceAggregationRequired,
|
|
1872
|
+
governanceAdditiveRequired,
|
|
1873
|
+
rulesGoldenQueriesMinimum,
|
|
1874
|
+
rulesBusinessRulesExist,
|
|
1875
|
+
rulesGuardrailsExist,
|
|
1876
|
+
rulesHierarchiesExist,
|
|
1877
|
+
// Composite tier (3)
|
|
1878
|
+
tierBronze,
|
|
1879
|
+
tierSilver,
|
|
1880
|
+
tierGold
|
|
1085
1881
|
];
|
|
1086
1882
|
|
|
1883
|
+
// src/config/defaults.ts
|
|
1884
|
+
var DEFAULT_CONFIG = {
|
|
1885
|
+
context_dir: "context",
|
|
1886
|
+
output_dir: "dist"
|
|
1887
|
+
};
|
|
1888
|
+
|
|
1889
|
+
// src/config/loader.ts
|
|
1890
|
+
import * as fs from "fs";
|
|
1891
|
+
import * as path from "path";
|
|
1892
|
+
import * as yaml from "yaml";
|
|
1893
|
+
var CONFIG_FILENAME = "contextkit.config.yaml";
|
|
1894
|
+
function loadConfig(rootDir) {
|
|
1895
|
+
const configPath = path.join(rootDir, CONFIG_FILENAME);
|
|
1896
|
+
if (!fs.existsSync(configPath)) {
|
|
1897
|
+
return { ...DEFAULT_CONFIG };
|
|
1898
|
+
}
|
|
1899
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
1900
|
+
const parsed = yaml.parse(raw);
|
|
1901
|
+
if (parsed == null) {
|
|
1902
|
+
return { ...DEFAULT_CONFIG };
|
|
1903
|
+
}
|
|
1904
|
+
const validated = contextKitConfigSchema.parse(parsed);
|
|
1905
|
+
return validated;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1087
1908
|
// src/fixer/apply.ts
|
|
1088
|
-
|
|
1089
|
-
function applyFixes(diagnostics) {
|
|
1909
|
+
function applyFixes(diagnostics, readFile2) {
|
|
1090
1910
|
const editsByFile = /* @__PURE__ */ new Map();
|
|
1091
|
-
for (const
|
|
1092
|
-
if (!
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1911
|
+
for (const diag2 of diagnostics) {
|
|
1912
|
+
if (!diag2.fixable || !diag2.fix) continue;
|
|
1913
|
+
const file = diag2.location.file;
|
|
1914
|
+
if (!editsByFile.has(file)) {
|
|
1915
|
+
editsByFile.set(file, []);
|
|
1916
|
+
}
|
|
1917
|
+
const fileEdits = editsByFile.get(file);
|
|
1918
|
+
for (const edit of diag2.fix.edits) {
|
|
1919
|
+
fileEdits.push(edit);
|
|
1100
1920
|
}
|
|
1101
1921
|
}
|
|
1102
|
-
const
|
|
1922
|
+
const result = /* @__PURE__ */ new Map();
|
|
1103
1923
|
for (const [file, edits] of editsByFile) {
|
|
1104
|
-
|
|
1105
|
-
if (b.
|
|
1106
|
-
|
|
1107
|
-
}
|
|
1108
|
-
return b.range.startCol - a.range.startCol;
|
|
1924
|
+
edits.sort((a, b) => {
|
|
1925
|
+
if (a.startLine !== b.startLine) return b.startLine - a.startLine;
|
|
1926
|
+
return b.startCol - a.startCol;
|
|
1109
1927
|
});
|
|
1110
|
-
|
|
1111
|
-
try {
|
|
1112
|
-
content = fs.readFileSync(file, "utf-8");
|
|
1113
|
-
} catch {
|
|
1114
|
-
continue;
|
|
1115
|
-
}
|
|
1928
|
+
const content = readFile2(file);
|
|
1116
1929
|
const lines = content.split("\n");
|
|
1117
|
-
for (const edit of
|
|
1118
|
-
|
|
1119
|
-
if (startLine === endLine && startCol === endCol) {
|
|
1120
|
-
const lineIdx = startLine - 1;
|
|
1121
|
-
if (lineIdx >= 0 && lineIdx <= lines.length) {
|
|
1122
|
-
if (lineIdx === lines.length) {
|
|
1123
|
-
lines.push(edit.newText.replace(/\n$/, ""));
|
|
1124
|
-
} else {
|
|
1125
|
-
const line = lines[lineIdx] ?? "";
|
|
1126
|
-
const colIdx = startCol - 1;
|
|
1127
|
-
const before = line.slice(0, colIdx);
|
|
1128
|
-
const after = line.slice(colIdx);
|
|
1129
|
-
const insertLines = (before + edit.newText + after).split("\n");
|
|
1130
|
-
lines.splice(lineIdx, 1, ...insertLines);
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
} else {
|
|
1134
|
-
const startLineIdx = startLine - 1;
|
|
1135
|
-
const endLineIdx = endLine - 1;
|
|
1136
|
-
if (startLineIdx >= 0 && endLineIdx < lines.length) {
|
|
1137
|
-
const beforeText = (lines[startLineIdx] ?? "").slice(0, startCol - 1);
|
|
1138
|
-
const afterText = (lines[endLineIdx] ?? "").slice(endCol - 1);
|
|
1139
|
-
const replacementLines = (beforeText + edit.newText + afterText).split("\n");
|
|
1140
|
-
lines.splice(startLineIdx, endLineIdx - startLineIdx + 1, ...replacementLines);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1930
|
+
for (const edit of edits) {
|
|
1931
|
+
applyEdit(lines, edit);
|
|
1143
1932
|
}
|
|
1144
|
-
|
|
1145
|
-
file,
|
|
1146
|
-
editsApplied: edits.length,
|
|
1147
|
-
newContent: lines.join("\n")
|
|
1148
|
-
});
|
|
1933
|
+
result.set(file, lines.join("\n"));
|
|
1149
1934
|
}
|
|
1150
|
-
return
|
|
1935
|
+
return result;
|
|
1936
|
+
}
|
|
1937
|
+
function applyEdit(lines, edit) {
|
|
1938
|
+
const { startLine, startCol, endLine, endCol, newText } = edit;
|
|
1939
|
+
const sl = startLine - 1;
|
|
1940
|
+
const el = endLine - 1;
|
|
1941
|
+
const prefix = lines[sl].slice(0, startCol - 1);
|
|
1942
|
+
const suffix = lines[el].slice(endCol - 1);
|
|
1943
|
+
const replacement = prefix + newText + suffix;
|
|
1944
|
+
const newLines = replacement.split("\n");
|
|
1945
|
+
lines.splice(sl, el - sl + 1, ...newLines);
|
|
1151
1946
|
}
|
|
1152
1947
|
export {
|
|
1153
1948
|
ALL_RULES,
|
|
1154
1949
|
DEFAULT_CONFIG,
|
|
1155
1950
|
LintEngine,
|
|
1951
|
+
aiContextObjectSchema,
|
|
1952
|
+
aiContextSchema,
|
|
1156
1953
|
applyFixes,
|
|
1157
1954
|
buildGraph,
|
|
1955
|
+
businessRuleSchema,
|
|
1956
|
+
checkBronze,
|
|
1957
|
+
checkGold,
|
|
1958
|
+
checkSilver,
|
|
1158
1959
|
compile,
|
|
1159
|
-
|
|
1160
|
-
|
|
1960
|
+
computeAllTiers,
|
|
1961
|
+
computeTier,
|
|
1962
|
+
contextKitConfigSchema,
|
|
1161
1963
|
createEmptyGraph,
|
|
1162
|
-
|
|
1163
|
-
|
|
1964
|
+
customExtensionSchema,
|
|
1965
|
+
datasetGovernanceSchema,
|
|
1966
|
+
defaultAggregationEnum,
|
|
1967
|
+
dialectEnum,
|
|
1968
|
+
dialectExpressionSchema,
|
|
1969
|
+
dimensionSchema,
|
|
1164
1970
|
discoverFiles,
|
|
1165
|
-
|
|
1971
|
+
downstreamEntrySchema,
|
|
1166
1972
|
emitManifest,
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1973
|
+
expressionSchema,
|
|
1974
|
+
fieldGovernanceSchema,
|
|
1975
|
+
goldenQuerySchema,
|
|
1976
|
+
governanceFileSchema,
|
|
1977
|
+
guardrailFilterSchema,
|
|
1978
|
+
hierarchySchema,
|
|
1979
|
+
lineageFileSchema,
|
|
1980
|
+
lineageTypeEnum,
|
|
1981
|
+
lintConfigSchema,
|
|
1172
1982
|
loadConfig,
|
|
1173
|
-
|
|
1174
|
-
|
|
1983
|
+
mcpConfigSchema,
|
|
1984
|
+
metadataTierEnum,
|
|
1985
|
+
osiDatasetSchema,
|
|
1986
|
+
osiDocumentSchema,
|
|
1987
|
+
osiFieldSchema,
|
|
1988
|
+
osiMetricSchema,
|
|
1989
|
+
osiRelationshipSchema,
|
|
1990
|
+
osiSemanticModelSchema,
|
|
1175
1991
|
ownerFileSchema,
|
|
1176
|
-
ownershipRequired,
|
|
1177
|
-
packagingNoSecrets,
|
|
1178
1992
|
parseFile,
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
resolveConfig,
|
|
1188
|
-
schemaValidYaml,
|
|
1993
|
+
resolveReferences,
|
|
1994
|
+
rulesFileSchema,
|
|
1995
|
+
securityClassificationEnum,
|
|
1996
|
+
semanticRoleEnum,
|
|
1997
|
+
severityEnum,
|
|
1998
|
+
severityOrOffEnum,
|
|
1999
|
+
siteConfigSchema,
|
|
2000
|
+
tableTypeEnum,
|
|
1189
2001
|
termFileSchema,
|
|
1190
|
-
|
|
2002
|
+
trustStatusEnum,
|
|
2003
|
+
upstreamEntrySchema,
|
|
2004
|
+
validate,
|
|
2005
|
+
vendorEnum
|
|
1191
2006
|
};
|
|
1192
2007
|
//# sourceMappingURL=index.mjs.map
|