llm-checker 3.2.1 → 3.2.2
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/README.md +92 -7
- package/bin/enhanced_cli.js +447 -0
- package/package.json +7 -1
- package/src/index.js +84 -20
- package/src/models/deterministic-selector.js +406 -22
- package/src/models/intelligent-selector.js +89 -4
- package/src/policy/audit-reporter.js +420 -0
- package/src/policy/cli-policy.js +403 -0
- package/src/policy/policy-engine.js +497 -0
- package/src/policy/policy-manager.js +324 -0
- package/src/provenance/model-provenance.js +176 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const YAML = require('yaml');
|
|
4
|
+
|
|
5
|
+
const ALLOWED_POLICY_MODES = ['audit', 'enforce'];
|
|
6
|
+
const ALLOWED_ENFORCEMENT_BEHAVIOR = ['warn', 'error'];
|
|
7
|
+
const ALLOWED_REPORT_FORMATS = ['json', 'csv', 'sarif'];
|
|
8
|
+
|
|
9
|
+
function isPlainObject(value) {
|
|
10
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isPositiveNumber(value) {
|
|
14
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isNonEmptyString(value) {
|
|
18
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class PolicyManager {
|
|
22
|
+
getTemplate() {
|
|
23
|
+
return `version: 1
|
|
24
|
+
org: your-org
|
|
25
|
+
mode: enforce # audit | enforce
|
|
26
|
+
|
|
27
|
+
rules:
|
|
28
|
+
models:
|
|
29
|
+
allow:
|
|
30
|
+
- "qwen2.5-coder:*"
|
|
31
|
+
- "llama3.1:*"
|
|
32
|
+
deny:
|
|
33
|
+
- "*uncensored*"
|
|
34
|
+
max_size_gb: 24
|
|
35
|
+
max_params_b: 32
|
|
36
|
+
allowed_quantizations: ["Q4_K_M", "Q5_K_M", "Q8_0"]
|
|
37
|
+
|
|
38
|
+
runtime:
|
|
39
|
+
required_backends: ["metal", "cuda"]
|
|
40
|
+
min_ram_gb: 32
|
|
41
|
+
local_only: true
|
|
42
|
+
|
|
43
|
+
compliance:
|
|
44
|
+
approved_licenses: ["mit", "apache-2.0", "llama"]
|
|
45
|
+
|
|
46
|
+
enforcement:
|
|
47
|
+
on_violation: error # warn | error
|
|
48
|
+
exit_code: 3
|
|
49
|
+
allow_exceptions: true
|
|
50
|
+
|
|
51
|
+
exceptions:
|
|
52
|
+
- model: "deepseek-r1:32b"
|
|
53
|
+
reason: "Approved PoC"
|
|
54
|
+
approver: "security@example.com"
|
|
55
|
+
expires_at: "2026-06-30"
|
|
56
|
+
|
|
57
|
+
reporting:
|
|
58
|
+
formats: ["json", "csv", "sarif"]
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
resolvePolicyPath(policyFile = 'policy.yaml', cwd = process.cwd()) {
|
|
63
|
+
return path.isAbsolute(policyFile) ? policyFile : path.resolve(cwd, policyFile);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
initPolicy(policyFile = 'policy.yaml', options = {}) {
|
|
67
|
+
const { force = false, cwd = process.cwd() } = options;
|
|
68
|
+
const targetPath = this.resolvePolicyPath(policyFile, cwd);
|
|
69
|
+
|
|
70
|
+
const exists = fs.existsSync(targetPath);
|
|
71
|
+
if (exists && !force) {
|
|
72
|
+
throw new Error(`Policy file already exists at ${targetPath}. Use --force to overwrite.`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
76
|
+
fs.writeFileSync(targetPath, this.getTemplate(), 'utf8');
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
path: targetPath,
|
|
80
|
+
overwritten: exists && force
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
loadPolicy(policyFile = 'policy.yaml', options = {}) {
|
|
85
|
+
const { cwd = process.cwd() } = options;
|
|
86
|
+
const policyPath = this.resolvePolicyPath(policyFile, cwd);
|
|
87
|
+
|
|
88
|
+
if (!fs.existsSync(policyPath)) {
|
|
89
|
+
throw new Error(`Policy file not found: ${policyPath}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const source = fs.readFileSync(policyPath, 'utf8');
|
|
93
|
+
const doc = YAML.parseDocument(source, { prettyErrors: true });
|
|
94
|
+
|
|
95
|
+
if (doc.errors && doc.errors.length > 0) {
|
|
96
|
+
const first = doc.errors[0];
|
|
97
|
+
throw new Error(`Invalid YAML in ${policyPath}: ${String(first.message || first)}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const policy = doc.toJSON();
|
|
101
|
+
if (!isPlainObject(policy)) {
|
|
102
|
+
throw new Error(`Invalid YAML in ${policyPath}: root must be an object`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { path: policyPath, policy };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
validatePolicyFile(policyFile = 'policy.yaml', options = {}) {
|
|
109
|
+
const loaded = this.loadPolicy(policyFile, options);
|
|
110
|
+
const validation = this.validatePolicyObject(loaded.policy);
|
|
111
|
+
return {
|
|
112
|
+
...validation,
|
|
113
|
+
path: loaded.path,
|
|
114
|
+
policy: loaded.policy
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
validatePolicyObject(policy) {
|
|
119
|
+
const errors = [];
|
|
120
|
+
const addError = (fieldPath, message) => {
|
|
121
|
+
errors.push({ path: fieldPath, message });
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (!isPlainObject(policy)) {
|
|
125
|
+
addError('root', 'Policy must be an object.');
|
|
126
|
+
return { valid: false, errors };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!Number.isInteger(policy.version) || policy.version < 1) {
|
|
130
|
+
addError('version', 'Version must be an integer >= 1.');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!isNonEmptyString(policy.org)) {
|
|
134
|
+
addError('org', 'Organization must be a non-empty string.');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!isNonEmptyString(policy.mode) || !ALLOWED_POLICY_MODES.includes(policy.mode)) {
|
|
138
|
+
addError('mode', `Mode must be one of: ${ALLOWED_POLICY_MODES.join(', ')}.`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!isPlainObject(policy.rules)) {
|
|
142
|
+
addError('rules', 'Rules section is required and must be an object.');
|
|
143
|
+
} else {
|
|
144
|
+
this.validateModelsRules(policy.rules.models, addError);
|
|
145
|
+
this.validateRuntimeRules(policy.rules.runtime, addError);
|
|
146
|
+
this.validateComplianceRules(policy.rules.compliance, addError);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.validateEnforcement(policy.enforcement, addError);
|
|
150
|
+
this.validateExceptions(policy.exceptions, addError);
|
|
151
|
+
this.validateReporting(policy.reporting, addError);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
valid: errors.length === 0,
|
|
155
|
+
errors
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
validateModelsRules(models, addError) {
|
|
160
|
+
if (models === undefined) return;
|
|
161
|
+
|
|
162
|
+
if (!isPlainObject(models)) {
|
|
163
|
+
addError('rules.models', 'Must be an object.');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.validateStringArray(models.allow, 'rules.models.allow', addError);
|
|
168
|
+
this.validateStringArray(models.deny, 'rules.models.deny', addError);
|
|
169
|
+
this.validateStringArray(
|
|
170
|
+
models.allowed_quantizations,
|
|
171
|
+
'rules.models.allowed_quantizations',
|
|
172
|
+
addError
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (models.max_size_gb !== undefined && !isPositiveNumber(models.max_size_gb)) {
|
|
176
|
+
addError('rules.models.max_size_gb', 'Must be a positive number.');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (models.max_params_b !== undefined && !isPositiveNumber(models.max_params_b)) {
|
|
180
|
+
addError('rules.models.max_params_b', 'Must be a positive number.');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
validateRuntimeRules(runtime, addError) {
|
|
185
|
+
if (runtime === undefined) return;
|
|
186
|
+
|
|
187
|
+
if (!isPlainObject(runtime)) {
|
|
188
|
+
addError('rules.runtime', 'Must be an object.');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.validateStringArray(runtime.required_backends, 'rules.runtime.required_backends', addError);
|
|
193
|
+
|
|
194
|
+
if (runtime.min_ram_gb !== undefined && !isPositiveNumber(runtime.min_ram_gb)) {
|
|
195
|
+
addError('rules.runtime.min_ram_gb', 'Must be a positive number.');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (runtime.local_only !== undefined && typeof runtime.local_only !== 'boolean') {
|
|
199
|
+
addError('rules.runtime.local_only', 'Must be a boolean.');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
validateComplianceRules(compliance, addError) {
|
|
204
|
+
if (compliance === undefined) return;
|
|
205
|
+
|
|
206
|
+
if (!isPlainObject(compliance)) {
|
|
207
|
+
addError('rules.compliance', 'Must be an object.');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
this.validateStringArray(
|
|
212
|
+
compliance.approved_licenses,
|
|
213
|
+
'rules.compliance.approved_licenses',
|
|
214
|
+
addError
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
validateEnforcement(enforcement, addError) {
|
|
219
|
+
if (enforcement === undefined) return;
|
|
220
|
+
|
|
221
|
+
if (!isPlainObject(enforcement)) {
|
|
222
|
+
addError('enforcement', 'Must be an object.');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (
|
|
227
|
+
enforcement.on_violation !== undefined &&
|
|
228
|
+
!ALLOWED_ENFORCEMENT_BEHAVIOR.includes(enforcement.on_violation)
|
|
229
|
+
) {
|
|
230
|
+
addError(
|
|
231
|
+
'enforcement.on_violation',
|
|
232
|
+
`Must be one of: ${ALLOWED_ENFORCEMENT_BEHAVIOR.join(', ')}.`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (enforcement.exit_code !== undefined) {
|
|
237
|
+
if (!Number.isInteger(enforcement.exit_code) || enforcement.exit_code < 1 || enforcement.exit_code > 255) {
|
|
238
|
+
addError('enforcement.exit_code', 'Must be an integer between 1 and 255.');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (enforcement.allow_exceptions !== undefined && typeof enforcement.allow_exceptions !== 'boolean') {
|
|
243
|
+
addError('enforcement.allow_exceptions', 'Must be a boolean.');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
validateExceptions(exceptions, addError) {
|
|
248
|
+
if (exceptions === undefined) return;
|
|
249
|
+
|
|
250
|
+
if (!Array.isArray(exceptions)) {
|
|
251
|
+
addError('exceptions', 'Must be an array.');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
exceptions.forEach((entry, index) => {
|
|
256
|
+
const basePath = `exceptions[${index}]`;
|
|
257
|
+
if (!isPlainObject(entry)) {
|
|
258
|
+
addError(basePath, 'Each exception must be an object.');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!isNonEmptyString(entry.model)) {
|
|
263
|
+
addError(`${basePath}.model`, 'Model must be a non-empty string.');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (entry.reason !== undefined && !isNonEmptyString(entry.reason)) {
|
|
267
|
+
addError(`${basePath}.reason`, 'Reason must be a non-empty string.');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (entry.approver !== undefined && !isNonEmptyString(entry.approver)) {
|
|
271
|
+
addError(`${basePath}.approver`, 'Approver must be a non-empty string.');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (entry.expires_at !== undefined && !isNonEmptyString(entry.expires_at)) {
|
|
275
|
+
addError(`${basePath}.expires_at`, 'expires_at must be a non-empty string.');
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
validateReporting(reporting, addError) {
|
|
281
|
+
if (reporting === undefined) return;
|
|
282
|
+
|
|
283
|
+
if (!isPlainObject(reporting)) {
|
|
284
|
+
addError('reporting', 'Must be an object.');
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (reporting.formats !== undefined) {
|
|
289
|
+
if (!Array.isArray(reporting.formats)) {
|
|
290
|
+
addError('reporting.formats', 'Must be an array.');
|
|
291
|
+
} else {
|
|
292
|
+
reporting.formats.forEach((format, index) => {
|
|
293
|
+
if (!isNonEmptyString(format)) {
|
|
294
|
+
addError(`reporting.formats[${index}]`, 'Format must be a non-empty string.');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!ALLOWED_REPORT_FORMATS.includes(format)) {
|
|
299
|
+
addError(
|
|
300
|
+
`reporting.formats[${index}]`,
|
|
301
|
+
`Unsupported format "${format}". Allowed: ${ALLOWED_REPORT_FORMATS.join(', ')}.`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
validateStringArray(value, fieldPath, addError) {
|
|
310
|
+
if (value === undefined) return;
|
|
311
|
+
if (!Array.isArray(value)) {
|
|
312
|
+
addError(fieldPath, 'Must be an array of strings.');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
value.forEach((entry, index) => {
|
|
317
|
+
if (!isNonEmptyString(entry)) {
|
|
318
|
+
addError(`${fieldPath}[${index}]`, 'Must be a non-empty string.');
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
module.exports = PolicyManager;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const UNKNOWN_VALUE = 'unknown';
|
|
2
|
+
|
|
3
|
+
const UNKNOWN_MARKERS = new Set(['', 'unknown', 'n/a', 'na', 'none', 'unspecified', 'not-provided']);
|
|
4
|
+
|
|
5
|
+
const LICENSE_ALIASES = {
|
|
6
|
+
mitlicense: 'mit',
|
|
7
|
+
mit: 'mit',
|
|
8
|
+
apache2: 'apache-2.0',
|
|
9
|
+
'apache2.0': 'apache-2.0',
|
|
10
|
+
'apache-2': 'apache-2.0',
|
|
11
|
+
'apache-2.0': 'apache-2.0',
|
|
12
|
+
'apache-2.0-license': 'apache-2.0',
|
|
13
|
+
'llama2': 'llama',
|
|
14
|
+
'llama3': 'llama',
|
|
15
|
+
'llama3.1': 'llama',
|
|
16
|
+
'llama3.2': 'llama',
|
|
17
|
+
'meta-llama': 'llama'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function asString(value) {
|
|
21
|
+
if (value === undefined || value === null) return '';
|
|
22
|
+
return String(value).trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sanitizeValue(value) {
|
|
26
|
+
const text = asString(value);
|
|
27
|
+
if (!text) return UNKNOWN_VALUE;
|
|
28
|
+
|
|
29
|
+
const normalized = text.toLowerCase();
|
|
30
|
+
return UNKNOWN_MARKERS.has(normalized) ? UNKNOWN_VALUE : text;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeSource(value) {
|
|
34
|
+
const normalized = sanitizeValue(value).toLowerCase();
|
|
35
|
+
if (normalized === UNKNOWN_VALUE) return UNKNOWN_VALUE;
|
|
36
|
+
|
|
37
|
+
const source = normalized.replace(/\s+/g, '_').replace(/-/g, '_');
|
|
38
|
+
|
|
39
|
+
if (source === 'ollama_local') return 'ollama_local';
|
|
40
|
+
if (source === 'ollama_database') return 'ollama_database';
|
|
41
|
+
if (source === 'enhanced_with_ollama') return 'enhanced_with_ollama';
|
|
42
|
+
if (source === 'static_database') return 'static_database';
|
|
43
|
+
|
|
44
|
+
return source;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeRegistry(value, source = UNKNOWN_VALUE) {
|
|
48
|
+
const registry = sanitizeValue(value);
|
|
49
|
+
if (registry !== UNKNOWN_VALUE) return registry;
|
|
50
|
+
|
|
51
|
+
if (source.includes('ollama')) {
|
|
52
|
+
return 'ollama.com';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return UNKNOWN_VALUE;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeVersion(value) {
|
|
59
|
+
return sanitizeValue(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeDigest(value) {
|
|
63
|
+
return sanitizeValue(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeLicense(value) {
|
|
67
|
+
const raw = sanitizeValue(value);
|
|
68
|
+
if (raw === UNKNOWN_VALUE) return UNKNOWN_VALUE;
|
|
69
|
+
|
|
70
|
+
const lowered = raw.toLowerCase();
|
|
71
|
+
const canonicalKey = lowered.replace(/\s+/g, '').replace(/_/g, '-');
|
|
72
|
+
if (LICENSE_ALIASES[canonicalKey]) return LICENSE_ALIASES[canonicalKey];
|
|
73
|
+
|
|
74
|
+
const hyphenated = lowered.replace(/\s+/g, '-').replace(/_/g, '-');
|
|
75
|
+
const aliasByHyphen = LICENSE_ALIASES[hyphenated];
|
|
76
|
+
if (aliasByHyphen) return aliasByHyphen;
|
|
77
|
+
|
|
78
|
+
return hyphenated;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function extractVersionFromIdentifier(identifier) {
|
|
82
|
+
const text = asString(identifier);
|
|
83
|
+
if (!text) return UNKNOWN_VALUE;
|
|
84
|
+
|
|
85
|
+
if (text.includes(':')) {
|
|
86
|
+
const [, tag] = text.split(':');
|
|
87
|
+
return sanitizeValue(tag);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return UNKNOWN_VALUE;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function extractProvenance(model = {}, defaults = {}) {
|
|
94
|
+
const source = normalizeSource(
|
|
95
|
+
model?.provenance?.source ||
|
|
96
|
+
model?.source ||
|
|
97
|
+
defaults?.source
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const registry = normalizeRegistry(
|
|
101
|
+
model?.provenance?.registry ||
|
|
102
|
+
model?.registry ||
|
|
103
|
+
model?.source_registry ||
|
|
104
|
+
defaults?.registry,
|
|
105
|
+
source
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const version = normalizeVersion(
|
|
109
|
+
model?.provenance?.version ||
|
|
110
|
+
model?.version ||
|
|
111
|
+
model?.tag ||
|
|
112
|
+
model?.model_tag ||
|
|
113
|
+
defaults?.version ||
|
|
114
|
+
extractVersionFromIdentifier(
|
|
115
|
+
model?.model_identifier ||
|
|
116
|
+
model?.modelIdentifier ||
|
|
117
|
+
model?.identifier ||
|
|
118
|
+
model?.model_id ||
|
|
119
|
+
model?.modelId
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const license = normalizeLicense(
|
|
124
|
+
model?.provenance?.license ||
|
|
125
|
+
model?.license ||
|
|
126
|
+
model?.license_id ||
|
|
127
|
+
model?.licenseId ||
|
|
128
|
+
defaults?.license
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const digest = normalizeDigest(
|
|
132
|
+
model?.provenance?.digest ||
|
|
133
|
+
model?.digest ||
|
|
134
|
+
model?.hash ||
|
|
135
|
+
model?.sha256 ||
|
|
136
|
+
defaults?.digest
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
source,
|
|
141
|
+
registry,
|
|
142
|
+
version,
|
|
143
|
+
license,
|
|
144
|
+
digest
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function attachModelProvenance(model, defaults = {}) {
|
|
149
|
+
if (!model || typeof model !== 'object' || Array.isArray(model)) {
|
|
150
|
+
return model;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const provenance = extractProvenance(model, defaults);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
...model,
|
|
157
|
+
source: provenance.source,
|
|
158
|
+
license: provenance.license,
|
|
159
|
+
version: provenance.version,
|
|
160
|
+
digest: provenance.digest,
|
|
161
|
+
provenance
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function attachProvenanceToCollection(models, defaults = {}) {
|
|
166
|
+
if (!Array.isArray(models)) return [];
|
|
167
|
+
return models.map((model) => attachModelProvenance(model, defaults));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
UNKNOWN_VALUE,
|
|
172
|
+
normalizeLicense,
|
|
173
|
+
extractProvenance,
|
|
174
|
+
attachModelProvenance,
|
|
175
|
+
attachProvenanceToCollection
|
|
176
|
+
};
|