@runcontext/core 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +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 +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,84 +1,173 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class;// src/schema/osi.ts
|
|
2
2
|
var _zod = require('zod');
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
var
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
product_id: _zod.z.string().optional(),
|
|
10
|
-
definition: _zod.z.string(),
|
|
11
|
-
owner: _zod.z.string().optional(),
|
|
12
|
-
status: _zod.z.enum(["draft", "certified", "deprecated"]).optional(),
|
|
13
|
-
certified: _zod.z.boolean().optional(),
|
|
14
|
-
tags: _zod.z.array(_zod.z.string()).optional(),
|
|
15
|
-
evidence: _zod.z.array(evidenceSchema).optional(),
|
|
16
|
-
depends_on: _zod.z.array(_zod.z.string()).optional(),
|
|
17
|
-
examples: _zod.z.array(exampleSchema).optional(),
|
|
18
|
-
description: _zod.z.string().optional()
|
|
3
|
+
var dialectEnum = _zod.z.enum(["ANSI_SQL", "SNOWFLAKE", "MDX", "TABLEAU", "DATABRICKS"]);
|
|
4
|
+
var vendorEnum = _zod.z.enum(["COMMON", "SNOWFLAKE", "SALESFORCE", "DBT", "DATABRICKS"]);
|
|
5
|
+
var aiContextObjectSchema = _zod.z.object({
|
|
6
|
+
instructions: _zod.z.string().optional(),
|
|
7
|
+
synonyms: _zod.z.array(_zod.z.string()).optional(),
|
|
8
|
+
examples: _zod.z.array(_zod.z.string()).optional()
|
|
19
9
|
});
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
10
|
+
var aiContextSchema = _zod.z.union([_zod.z.string(), aiContextObjectSchema]);
|
|
11
|
+
var customExtensionSchema = _zod.z.object({
|
|
12
|
+
vendor_name: vendorEnum,
|
|
13
|
+
data: _zod.z.string()
|
|
14
|
+
});
|
|
15
|
+
var dialectExpressionSchema = _zod.z.object({
|
|
16
|
+
dialect: dialectEnum,
|
|
17
|
+
expression: _zod.z.string()
|
|
18
|
+
});
|
|
19
|
+
var expressionSchema = _zod.z.object({
|
|
20
|
+
dialects: _zod.z.array(dialectExpressionSchema)
|
|
21
|
+
});
|
|
22
|
+
var dimensionSchema = _zod.z.object({
|
|
23
|
+
is_time: _zod.z.boolean().optional()
|
|
24
|
+
});
|
|
25
|
+
var osiFieldSchema = _zod.z.object({
|
|
26
|
+
name: _zod.z.string(),
|
|
27
|
+
expression: expressionSchema,
|
|
28
|
+
dimension: dimensionSchema.optional(),
|
|
29
|
+
label: _zod.z.string().optional(),
|
|
30
|
+
description: _zod.z.string().optional(),
|
|
31
|
+
ai_context: aiContextSchema.optional(),
|
|
32
|
+
custom_extensions: _zod.z.array(customExtensionSchema).optional()
|
|
33
|
+
});
|
|
34
|
+
var osiDatasetSchema = _zod.z.object({
|
|
35
|
+
name: _zod.z.string(),
|
|
36
|
+
source: _zod.z.string(),
|
|
37
|
+
primary_key: _zod.z.array(_zod.z.string()).optional(),
|
|
38
|
+
unique_keys: _zod.z.array(_zod.z.array(_zod.z.string())).optional(),
|
|
39
|
+
description: _zod.z.string().optional(),
|
|
40
|
+
ai_context: aiContextSchema.optional(),
|
|
41
|
+
fields: _zod.z.array(osiFieldSchema).optional(),
|
|
42
|
+
custom_extensions: _zod.z.array(customExtensionSchema).optional()
|
|
43
|
+
});
|
|
44
|
+
var osiRelationshipSchema = _zod.z.object({
|
|
45
|
+
name: _zod.z.string(),
|
|
46
|
+
from: _zod.z.string(),
|
|
47
|
+
to: _zod.z.string(),
|
|
48
|
+
from_columns: _zod.z.array(_zod.z.string()),
|
|
49
|
+
to_columns: _zod.z.array(_zod.z.string()),
|
|
50
|
+
ai_context: aiContextSchema.optional(),
|
|
51
|
+
custom_extensions: _zod.z.array(customExtensionSchema).optional()
|
|
52
|
+
});
|
|
53
|
+
var osiMetricSchema = _zod.z.object({
|
|
54
|
+
name: _zod.z.string(),
|
|
55
|
+
expression: expressionSchema,
|
|
56
|
+
description: _zod.z.string().optional(),
|
|
57
|
+
ai_context: aiContextSchema.optional(),
|
|
58
|
+
custom_extensions: _zod.z.array(customExtensionSchema).optional()
|
|
59
|
+
});
|
|
60
|
+
var osiSemanticModelSchema = _zod.z.object({
|
|
61
|
+
name: _zod.z.string(),
|
|
62
|
+
description: _zod.z.string().optional(),
|
|
63
|
+
ai_context: aiContextSchema.optional(),
|
|
64
|
+
datasets: _zod.z.array(osiDatasetSchema),
|
|
65
|
+
relationships: _zod.z.array(osiRelationshipSchema).optional(),
|
|
66
|
+
metrics: _zod.z.array(osiMetricSchema).optional(),
|
|
67
|
+
custom_extensions: _zod.z.array(customExtensionSchema).optional()
|
|
68
|
+
});
|
|
69
|
+
var osiDocumentSchema = _zod.z.object({
|
|
70
|
+
version: _zod.z.literal("1.0"),
|
|
71
|
+
semantic_model: _zod.z.array(osiSemanticModelSchema)
|
|
30
72
|
});
|
|
31
73
|
|
|
32
|
-
// src/schema/
|
|
33
|
-
|
|
34
|
-
var
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
var
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
var policyRuleSchema = _zod.z.object({
|
|
45
|
-
priority: _zod.z.number(),
|
|
46
|
-
when: policyWhenSchema,
|
|
47
|
-
then: policyThenSchema
|
|
74
|
+
// src/schema/governance.ts
|
|
75
|
+
|
|
76
|
+
var trustStatusEnum = _zod.z.enum(["endorsed", "warning", "deprecated"]);
|
|
77
|
+
var securityClassificationEnum = _zod.z.enum(["public", "internal", "confidential", "secret"]);
|
|
78
|
+
var tableTypeEnum = _zod.z.enum(["fact", "dimension", "bridge", "snapshot", "event", "aggregate", "view"]);
|
|
79
|
+
var semanticRoleEnum = _zod.z.enum(["metric", "dimension", "identifier", "date"]);
|
|
80
|
+
var defaultAggregationEnum = _zod.z.enum(["SUM", "AVG", "COUNT", "COUNT_DISTINCT", "MIN", "MAX"]);
|
|
81
|
+
var datasetGovernanceSchema = _zod.z.object({
|
|
82
|
+
grain: _zod.z.string(),
|
|
83
|
+
refresh: _zod.z.string().optional(),
|
|
84
|
+
table_type: tableTypeEnum,
|
|
85
|
+
security: securityClassificationEnum.optional()
|
|
48
86
|
});
|
|
49
|
-
var
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
87
|
+
var fieldGovernanceSchema = _zod.z.object({
|
|
88
|
+
semantic_role: semanticRoleEnum,
|
|
89
|
+
default_aggregation: defaultAggregationEnum.optional(),
|
|
90
|
+
additive: _zod.z.boolean().optional(),
|
|
91
|
+
default_filter: _zod.z.string().optional(),
|
|
92
|
+
sample_values: _zod.z.array(_zod.z.string()).optional()
|
|
93
|
+
});
|
|
94
|
+
var dottedFieldsRecord = _zod.z.record(_zod.z.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 = _zod.z.object({
|
|
99
|
+
model: _zod.z.string(),
|
|
100
|
+
owner: _zod.z.string(),
|
|
101
|
+
trust: trustStatusEnum.optional(),
|
|
102
|
+
security: securityClassificationEnum.optional(),
|
|
54
103
|
tags: _zod.z.array(_zod.z.string()).optional(),
|
|
55
|
-
|
|
56
|
-
|
|
104
|
+
datasets: _zod.z.record(_zod.z.string(), datasetGovernanceSchema).optional(),
|
|
105
|
+
fields: dottedFieldsRecord.optional()
|
|
57
106
|
});
|
|
58
107
|
|
|
59
|
-
// src/schema/
|
|
108
|
+
// src/schema/rules.ts
|
|
60
109
|
|
|
61
|
-
var
|
|
110
|
+
var goldenQuerySchema = _zod.z.object({
|
|
111
|
+
question: _zod.z.string(),
|
|
112
|
+
sql: _zod.z.string(),
|
|
113
|
+
dialect: _zod.z.string().optional(),
|
|
114
|
+
tags: _zod.z.array(_zod.z.string()).optional()
|
|
115
|
+
});
|
|
116
|
+
var businessRuleSchema = _zod.z.object({
|
|
62
117
|
name: _zod.z.string(),
|
|
63
|
-
|
|
64
|
-
|
|
118
|
+
definition: _zod.z.string(),
|
|
119
|
+
enforcement: _zod.z.array(_zod.z.string()).optional(),
|
|
120
|
+
avoid: _zod.z.array(_zod.z.string()).optional(),
|
|
121
|
+
tables: _zod.z.array(_zod.z.string()).optional(),
|
|
122
|
+
applied_always: _zod.z.boolean().optional()
|
|
65
123
|
});
|
|
66
|
-
var
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
124
|
+
var guardrailFilterSchema = _zod.z.object({
|
|
125
|
+
name: _zod.z.string(),
|
|
126
|
+
filter: _zod.z.string(),
|
|
127
|
+
tables: _zod.z.array(_zod.z.string()).optional(),
|
|
128
|
+
reason: _zod.z.string()
|
|
129
|
+
});
|
|
130
|
+
var hierarchySchema = _zod.z.object({
|
|
131
|
+
name: _zod.z.string(),
|
|
132
|
+
levels: _zod.z.array(_zod.z.string()),
|
|
133
|
+
dataset: _zod.z.string(),
|
|
134
|
+
field: _zod.z.string().optional()
|
|
135
|
+
});
|
|
136
|
+
var rulesFileSchema = _zod.z.object({
|
|
137
|
+
model: _zod.z.string(),
|
|
138
|
+
golden_queries: _zod.z.array(goldenQuerySchema).optional(),
|
|
139
|
+
business_rules: _zod.z.array(businessRuleSchema).optional(),
|
|
140
|
+
guardrail_filters: _zod.z.array(guardrailFilterSchema).optional(),
|
|
141
|
+
hierarchies: _zod.z.array(hierarchySchema).optional()
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// src/schema/lineage.ts
|
|
145
|
+
|
|
146
|
+
var lineageTypeEnum = _zod.z.enum(["pipeline", "dashboard", "ml_model", "api", "manual"]);
|
|
147
|
+
var upstreamEntrySchema = _zod.z.object({
|
|
148
|
+
source: _zod.z.string(),
|
|
149
|
+
type: lineageTypeEnum,
|
|
150
|
+
pipeline: _zod.z.string().optional(),
|
|
151
|
+
tool: _zod.z.string().optional(),
|
|
152
|
+
refresh: _zod.z.string().optional(),
|
|
153
|
+
notes: _zod.z.string().optional()
|
|
154
|
+
});
|
|
155
|
+
var downstreamEntrySchema = _zod.z.object({
|
|
156
|
+
target: _zod.z.string(),
|
|
157
|
+
type: lineageTypeEnum,
|
|
158
|
+
tool: _zod.z.string().optional(),
|
|
159
|
+
notes: _zod.z.string().optional()
|
|
160
|
+
});
|
|
161
|
+
var lineageFileSchema = _zod.z.object({
|
|
162
|
+
model: _zod.z.string(),
|
|
163
|
+
upstream: _zod.z.array(upstreamEntrySchema).optional(),
|
|
164
|
+
downstream: _zod.z.array(downstreamEntrySchema).optional()
|
|
75
165
|
});
|
|
76
166
|
|
|
77
167
|
// src/schema/term.ts
|
|
78
168
|
|
|
79
169
|
var termFileSchema = _zod.z.object({
|
|
80
170
|
id: _zod.z.string(),
|
|
81
|
-
term: _zod.z.string().optional(),
|
|
82
171
|
definition: _zod.z.string(),
|
|
83
172
|
synonyms: _zod.z.array(_zod.z.string()).optional(),
|
|
84
173
|
maps_to: _zod.z.array(_zod.z.string()).optional(),
|
|
@@ -93,558 +182,1202 @@ var ownerFileSchema = _zod.z.object({
|
|
|
93
182
|
display_name: _zod.z.string(),
|
|
94
183
|
email: _zod.z.string().optional(),
|
|
95
184
|
team: _zod.z.string().optional(),
|
|
96
|
-
|
|
185
|
+
description: _zod.z.string().optional()
|
|
97
186
|
});
|
|
98
187
|
|
|
99
|
-
// src/config
|
|
100
|
-
var DEFAULT_CONFIG = {
|
|
101
|
-
project: { id: "my-context", displayName: "My Context", version: "0.1.0" },
|
|
102
|
-
paths: {
|
|
103
|
-
rootDir: ".",
|
|
104
|
-
contextDir: "./context",
|
|
105
|
-
distDir: "./dist",
|
|
106
|
-
cacheDir: "./.contextkit-cache"
|
|
107
|
-
},
|
|
108
|
-
site: { enabled: true, title: "Context Site", basePath: "/" },
|
|
109
|
-
lint: { defaultSeverity: "warning", rules: {} },
|
|
110
|
-
mcp: {
|
|
111
|
-
enabled: true,
|
|
112
|
-
transport: ["stdio"],
|
|
113
|
-
http: { port: 7331, host: "127.0.0.1" }
|
|
114
|
-
},
|
|
115
|
-
plugins: []
|
|
116
|
-
};
|
|
188
|
+
// src/schema/config.ts
|
|
117
189
|
|
|
118
|
-
|
|
119
|
-
var
|
|
120
|
-
var
|
|
121
|
-
var
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
DEFAULT_CONFIG,
|
|
141
|
-
partial
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
var CONFIG_FILENAMES = [
|
|
145
|
-
"contextkit.config.ts",
|
|
146
|
-
"contextkit.config.js",
|
|
147
|
-
"contextkit.config.yaml",
|
|
148
|
-
"contextkit.config.yml"
|
|
149
|
-
];
|
|
150
|
-
async function loadConfig(rootDir = ".") {
|
|
151
|
-
const absRoot = _path.resolve.call(void 0, rootDir);
|
|
152
|
-
for (const filename of CONFIG_FILENAMES) {
|
|
153
|
-
const filepath = _path.join.call(void 0, absRoot, filename);
|
|
154
|
-
if (!_fs.existsSync.call(void 0, filepath)) {
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
if (filename.endsWith(".yaml") || filename.endsWith(".yml")) {
|
|
158
|
-
const raw = _fs.readFileSync.call(void 0, filepath, "utf-8");
|
|
159
|
-
const parsed = _yaml.parse.call(void 0, raw);
|
|
160
|
-
return resolveConfig(_nullishCoalesce(parsed, () => ( {})));
|
|
161
|
-
}
|
|
162
|
-
if (filename.endsWith(".ts") || filename.endsWith(".js")) {
|
|
163
|
-
const mod = await Promise.resolve().then(() => _interopRequireWildcard(require(filepath)));
|
|
164
|
-
const partial = _nullishCoalesce(mod.default, () => ( mod));
|
|
165
|
-
return resolveConfig(partial);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return { ...DEFAULT_CONFIG };
|
|
169
|
-
}
|
|
190
|
+
var metadataTierEnum = _zod.z.enum(["none", "bronze", "silver", "gold"]);
|
|
191
|
+
var severityEnum = _zod.z.enum(["error", "warning"]);
|
|
192
|
+
var severityOrOffEnum = _zod.z.union([severityEnum, _zod.z.literal("off")]);
|
|
193
|
+
var lintConfigSchema = _zod.z.object({
|
|
194
|
+
severity_overrides: _zod.z.record(_zod.z.string(), severityOrOffEnum).optional()
|
|
195
|
+
});
|
|
196
|
+
var siteConfigSchema = _zod.z.object({
|
|
197
|
+
title: _zod.z.string().optional(),
|
|
198
|
+
base_path: _zod.z.string().optional()
|
|
199
|
+
});
|
|
200
|
+
var mcpConfigSchema = _zod.z.object({
|
|
201
|
+
transport: _zod.z.enum(["stdio", "http"]).optional(),
|
|
202
|
+
port: _zod.z.number().optional()
|
|
203
|
+
});
|
|
204
|
+
var contextKitConfigSchema = _zod.z.object({
|
|
205
|
+
context_dir: _zod.z.string().default("context"),
|
|
206
|
+
output_dir: _zod.z.string().default("dist"),
|
|
207
|
+
minimum_tier: metadataTierEnum.optional(),
|
|
208
|
+
lint: lintConfigSchema.optional(),
|
|
209
|
+
site: siteConfigSchema.optional(),
|
|
210
|
+
mcp: mcpConfigSchema.optional()
|
|
211
|
+
});
|
|
170
212
|
|
|
171
213
|
// src/parser/discover.ts
|
|
172
214
|
var _glob = require('glob');
|
|
173
|
-
var
|
|
174
|
-
"**/*.
|
|
175
|
-
"**/*.
|
|
176
|
-
"**/*.
|
|
177
|
-
"**/*.
|
|
178
|
-
"**/*.
|
|
179
|
-
"**/*.owner.
|
|
180
|
-
|
|
181
|
-
"**/*.term.yml",
|
|
182
|
-
"**/*.entity.yaml",
|
|
183
|
-
"**/*.entity.yml"
|
|
184
|
-
];
|
|
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
|
+
};
|
|
185
223
|
async function discoverFiles(contextDir) {
|
|
186
|
-
const files =
|
|
187
|
-
|
|
224
|
+
const files = [];
|
|
225
|
+
for (const [kind, pattern] of Object.entries(PATTERNS)) {
|
|
226
|
+
const matches = await _glob.glob.call(void 0, pattern, { cwd: contextDir, absolute: true });
|
|
227
|
+
for (const match of matches) {
|
|
228
|
+
files.push({ path: match, kind });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
188
232
|
}
|
|
189
233
|
|
|
190
234
|
// src/parser/parse.ts
|
|
235
|
+
var _promises = require('fs/promises');
|
|
236
|
+
var _yaml = require('yaml'); var yaml = _interopRequireWildcard(_yaml);
|
|
237
|
+
async function parseFile(filePath, kind) {
|
|
238
|
+
const content = await _promises.readFile.call(void 0, filePath, "utf-8");
|
|
239
|
+
const data = _yaml.parse.call(void 0, content);
|
|
240
|
+
return {
|
|
241
|
+
kind,
|
|
242
|
+
data,
|
|
243
|
+
source: { file: filePath, line: 1, column: 1 }
|
|
244
|
+
};
|
|
245
|
+
}
|
|
191
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];
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
kind: parsed.kind,
|
|
282
|
+
data,
|
|
283
|
+
diagnostics: []
|
|
284
|
+
};
|
|
285
|
+
}
|
|
192
286
|
|
|
193
|
-
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (filePath.includes("/glossary/")) return "term";
|
|
203
|
-
return "concept";
|
|
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
|
+
};
|
|
204
296
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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 (!_optionalChain([dataset, 'optionalAccess', _ => _.fields])) return /* @__PURE__ */ new Set();
|
|
303
|
+
return new Set(dataset.fields.map((f) => f.name));
|
|
304
|
+
}
|
|
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
|
+
}
|
|
396
|
+
}
|
|
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
|
+
}
|
|
210
404
|
}
|
|
211
|
-
|
|
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
|
+
}
|
|
232
490
|
}
|
|
233
|
-
for (const
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
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);
|
|
508
|
+
}
|
|
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 _nullishCoalesce(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(", ")}`));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
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) => _nullishCoalesce(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 _nullishCoalesce(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
|
+
}
|
|
241
578
|
}
|
|
242
579
|
}
|
|
243
|
-
|
|
244
|
-
|
|
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 = _nullishCoalesce(_optionalChain([gov, 'optionalAccess', _2 => _2.tags, 'optionalAccess', _3 => _3.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(_nullishCoalesce(_optionalChain([gov, 'optionalAccess', _4 => _4.tags]), () => ( [])));
|
|
677
|
+
const govOwner = _optionalChain([gov, 'optionalAccess', _5 => _5.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) => !_optionalChain([gov, 'access', _6 => _6.fields, 'access', _7 => _7[k], 'optionalAccess', _8 => _8.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 = _nullishCoalesce(_optionalChain([rules, 'optionalAccess', _9 => _9.golden_queries, 'optionalAccess', _10 => _10.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 '${_nullishCoalesce(_optionalChain([gov, 'optionalAccess', _11 => _11.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 (e2) {
|
|
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;
|
|
953
|
+
}
|
|
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
|
+
};
|
|
455
966
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
967
|
+
|
|
968
|
+
// src/linter/engine.ts
|
|
969
|
+
var LintEngine = (_class = class {
|
|
970
|
+
__init() {this.rules = []}
|
|
971
|
+
__init2() {this.overrides = {}}
|
|
972
|
+
constructor(overrides) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);
|
|
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: _nullishCoalesce(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
|
+
}, _class);
|
|
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;
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
|
|
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,
|
|
1273
|
+
run(graph) {
|
|
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
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return diagnostics;
|
|
540
1293
|
}
|
|
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
|
-
}
|
|
1294
|
+
};
|
|
562
1295
|
|
|
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
|
-
*/
|
|
1296
|
+
// src/linter/rules/governance-fields-exist.ts
|
|
1297
|
+
var governanceFieldsExist = {
|
|
1298
|
+
id: "governance/fields-exist",
|
|
1299
|
+
defaultSeverity: "error",
|
|
1300
|
+
description: "Every field key in governance must exist as a field in the referenced OSI model dataset",
|
|
1301
|
+
fixable: false,
|
|
579
1302
|
run(graph) {
|
|
580
|
-
const
|
|
581
|
-
for (const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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(_nullishCoalesce(_optionalChain([dataset, 'access', _12 => _12.fields, 'optionalAccess', _13 => _13.map, 'call', _14 => _14((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
|
+
}
|
|
590
1333
|
}
|
|
591
1334
|
}
|
|
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;
|
|
1335
|
+
return diagnostics;
|
|
598
1336
|
}
|
|
599
|
-
}
|
|
1337
|
+
};
|
|
600
1338
|
|
|
601
|
-
// src/linter/rules/
|
|
602
|
-
var
|
|
603
|
-
id: "
|
|
604
|
-
defaultSeverity: "
|
|
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",
|
|
605
1344
|
fixable: false,
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
1345
|
+
run(graph) {
|
|
1346
|
+
const diagnostics = [];
|
|
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
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
return diagnostics;
|
|
609
1362
|
}
|
|
610
1363
|
};
|
|
611
1364
|
|
|
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",
|
|
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,
|
|
622
1371
|
run(graph) {
|
|
623
1372
|
const diagnostics = [];
|
|
624
|
-
for (const [,
|
|
625
|
-
if (!
|
|
626
|
-
const suggested = toKebabCase2(node.id);
|
|
1373
|
+
for (const [key, gov] of graph.governance) {
|
|
1374
|
+
if (!gov.security) {
|
|
627
1375
|
diagnostics.push({
|
|
628
|
-
ruleId:
|
|
629
|
-
severity:
|
|
630
|
-
message: `
|
|
631
|
-
|
|
632
|
-
fixable:
|
|
633
|
-
fix: {
|
|
634
|
-
description: `Rename to "${suggested}"`,
|
|
635
|
-
edits: [
|
|
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
|
-
}
|
|
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
|
|
648
1381
|
});
|
|
649
1382
|
}
|
|
650
1383
|
}
|
|
@@ -652,39 +1385,22 @@ var namingIdKebabCase = {
|
|
|
652
1385
|
}
|
|
653
1386
|
};
|
|
654
1387
|
|
|
655
|
-
// src/linter/rules/
|
|
656
|
-
var
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
fixable:
|
|
661
|
-
description: "Concepts, products, and entities require an owner",
|
|
1388
|
+
// src/linter/rules/governance-trust-required.ts
|
|
1389
|
+
var governanceTrustRequired = {
|
|
1390
|
+
id: "governance/trust-required",
|
|
1391
|
+
defaultSeverity: "warning",
|
|
1392
|
+
description: "Governance files must have a trust status set (endorsed/warning/deprecated)",
|
|
1393
|
+
fixable: false,
|
|
662
1394
|
run(graph) {
|
|
663
1395
|
const diagnostics = [];
|
|
664
|
-
for (const [,
|
|
665
|
-
if (!
|
|
666
|
-
if (!node.owner) {
|
|
1396
|
+
for (const [key, gov] of graph.governance) {
|
|
1397
|
+
if (!gov.trust) {
|
|
667
1398
|
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
|
-
}
|
|
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
|
|
688
1404
|
});
|
|
689
1405
|
}
|
|
690
1406
|
}
|
|
@@ -692,111 +1408,134 @@ var ownershipRequired = {
|
|
|
692
1408
|
}
|
|
693
1409
|
};
|
|
694
1410
|
|
|
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",
|
|
1411
|
+
// src/linter/rules/governance-refresh-required.ts
|
|
1412
|
+
var governanceRefreshRequired = {
|
|
1413
|
+
id: "governance/refresh-required",
|
|
707
1414
|
defaultSeverity: "warning",
|
|
708
|
-
|
|
709
|
-
|
|
1415
|
+
description: "All governed datasets must have a refresh cadence set",
|
|
1416
|
+
fixable: false,
|
|
710
1417
|
run(graph) {
|
|
711
1418
|
const diagnostics = [];
|
|
712
|
-
for (const [,
|
|
713
|
-
if (!
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
}
|
|
735
|
-
});
|
|
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
|
+
}
|
|
736
1431
|
}
|
|
737
1432
|
}
|
|
738
1433
|
return diagnostics;
|
|
739
1434
|
}
|
|
740
1435
|
};
|
|
741
1436
|
|
|
742
|
-
// src/linter/rules/
|
|
743
|
-
var
|
|
744
|
-
id: "
|
|
745
|
-
defaultSeverity: "
|
|
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 = _nullishCoalesce(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 = _nullishCoalesce(_optionalChain([rules, 'access', _15 => _15.golden_queries, 'optionalAccess', _16 => _16.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 = _nullishCoalesce(_optionalChain([rules, 'access', _17 => _17.business_rules, 'optionalAccess', _18 => _18.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 = _nullishCoalesce(_optionalChain([rules, 'access', _19 => _19.guardrail_filters, 'optionalAccess', _20 => _20.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 = _nullishCoalesce(_optionalChain([rules, 'access', _21 => _21.hierarchies, 'optionalAccess', _22 => _22.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 ${_nullishCoalesce(_optionalChain([gov, 'access', _23 => _23.tags, 'optionalAccess', _24 => _24.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 = _nullishCoalesce(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 = _nullishCoalesce(_optionalChain([rules, 'access', _25 => _25.golden_queries, 'optionalAccess', _26 => _26.length]), () => ( 0));
|
|
1820
|
+
if (goldenCount < 3) {
|
|
1821
|
+
missing.push(`>=3 golden queries (have ${goldenCount})`);
|
|
1822
|
+
}
|
|
1823
|
+
const bizCount = _nullishCoalesce(_optionalChain([rules, 'access', _27 => _27.business_rules, 'optionalAccess', _28 => _28.length]), () => ( 0));
|
|
1824
|
+
if (bizCount < 1) {
|
|
1825
|
+
missing.push("business rules");
|
|
1826
|
+
}
|
|
1827
|
+
const guardCount = _nullishCoalesce(_optionalChain([rules, 'access', _29 => _29.guardrail_filters, 'optionalAccess', _30 => _30.length]), () => ( 0));
|
|
1828
|
+
if (guardCount < 1) {
|
|
1829
|
+
missing.push("guardrail filters");
|
|
1830
|
+
}
|
|
1831
|
+
const hierCount = _nullishCoalesce(_optionalChain([rules, 'access', _31 => _31.hierarchies, 'optionalAccess', _32 => _32.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,84 +1849,100 @@ 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
|
|
|
1087
|
-
// src/
|
|
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
|
+
var _fs = require('fs'); var fs = _interopRequireWildcard(_fs);
|
|
1891
|
+
var _path = require('path'); var path = _interopRequireWildcard(_path);
|
|
1892
|
+
|
|
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
|
+
}
|
|
1088
1907
|
|
|
1089
|
-
|
|
1908
|
+
// src/fixer/apply.ts
|
|
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 = _fs2.default.readFileSync(file, "utf-8");
|
|
1113
|
-
} catch (e3) {
|
|
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 = _nullishCoalesce(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 = (_nullishCoalesce(lines[startLineIdx], () => ( ""))).slice(0, startCol - 1);
|
|
1138
|
-
const afterText = (_nullishCoalesce(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
|
|
|
1153
1948
|
|
|
@@ -1188,5 +1983,25 @@ function applyFixes(diagnostics) {
|
|
|
1188
1983
|
|
|
1189
1984
|
|
|
1190
1985
|
|
|
1191
|
-
|
|
1986
|
+
|
|
1987
|
+
|
|
1988
|
+
|
|
1989
|
+
|
|
1990
|
+
|
|
1991
|
+
|
|
1992
|
+
|
|
1993
|
+
|
|
1994
|
+
|
|
1995
|
+
|
|
1996
|
+
|
|
1997
|
+
|
|
1998
|
+
|
|
1999
|
+
|
|
2000
|
+
|
|
2001
|
+
|
|
2002
|
+
|
|
2003
|
+
|
|
2004
|
+
|
|
2005
|
+
|
|
2006
|
+
exports.ALL_RULES = ALL_RULES; exports.DEFAULT_CONFIG = DEFAULT_CONFIG; exports.LintEngine = LintEngine; exports.aiContextObjectSchema = aiContextObjectSchema; exports.aiContextSchema = aiContextSchema; exports.applyFixes = applyFixes; exports.buildGraph = buildGraph; exports.businessRuleSchema = businessRuleSchema; exports.checkBronze = checkBronze; exports.checkGold = checkGold; exports.checkSilver = checkSilver; exports.compile = compile; exports.computeAllTiers = computeAllTiers; exports.computeTier = computeTier; exports.contextKitConfigSchema = contextKitConfigSchema; exports.createEmptyGraph = createEmptyGraph; exports.customExtensionSchema = customExtensionSchema; exports.datasetGovernanceSchema = datasetGovernanceSchema; exports.defaultAggregationEnum = defaultAggregationEnum; exports.dialectEnum = dialectEnum; exports.dialectExpressionSchema = dialectExpressionSchema; exports.dimensionSchema = dimensionSchema; exports.discoverFiles = discoverFiles; exports.downstreamEntrySchema = downstreamEntrySchema; exports.emitManifest = emitManifest; exports.expressionSchema = expressionSchema; exports.fieldGovernanceSchema = fieldGovernanceSchema; exports.goldenQuerySchema = goldenQuerySchema; exports.governanceFileSchema = governanceFileSchema; exports.guardrailFilterSchema = guardrailFilterSchema; exports.hierarchySchema = hierarchySchema; exports.lineageFileSchema = lineageFileSchema; exports.lineageTypeEnum = lineageTypeEnum; exports.lintConfigSchema = lintConfigSchema; exports.loadConfig = loadConfig; exports.mcpConfigSchema = mcpConfigSchema; exports.metadataTierEnum = metadataTierEnum; exports.osiDatasetSchema = osiDatasetSchema; exports.osiDocumentSchema = osiDocumentSchema; exports.osiFieldSchema = osiFieldSchema; exports.osiMetricSchema = osiMetricSchema; exports.osiRelationshipSchema = osiRelationshipSchema; exports.osiSemanticModelSchema = osiSemanticModelSchema; exports.ownerFileSchema = ownerFileSchema; exports.parseFile = parseFile; exports.resolveReferences = resolveReferences; exports.rulesFileSchema = rulesFileSchema; exports.securityClassificationEnum = securityClassificationEnum; exports.semanticRoleEnum = semanticRoleEnum; exports.severityEnum = severityEnum; exports.severityOrOffEnum = severityOrOffEnum; exports.siteConfigSchema = siteConfigSchema; exports.tableTypeEnum = tableTypeEnum; exports.termFileSchema = termFileSchema; exports.trustStatusEnum = trustStatusEnum; exports.upstreamEntrySchema = upstreamEntrySchema; exports.validate = validate; exports.vendorEnum = vendorEnum;
|
|
1192
2007
|
//# sourceMappingURL=index.cjs.map
|