@mcptoolshop/style-dataset-lab 3.4.0 → 3.5.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/CHANGELOG.md +27 -0
- package/README.es.md +111 -59
- package/README.fr.md +117 -67
- package/README.hi.md +109 -70
- package/README.it.md +111 -60
- package/README.ja.md +116 -76
- package/README.md +58 -2
- package/README.pt-BR.md +119 -64
- package/README.zh.md +108 -67
- package/bin/sdlab.js +50 -1
- package/lib/adapters/ai-toolkit.js +187 -30
- package/lib/adapters/comfyui-workflows.js +84 -9
- package/lib/asset-ingest.js +575 -0
- package/lib/asset-masks.js +323 -0
- package/lib/asset-source.js +1176 -0
- package/lib/canon-author.js +350 -0
- package/lib/canon-review-gate.js +205 -0
- package/lib/colab-launcher-guards.js +285 -0
- package/lib/export.js +1 -1
- package/lib/freeze-gate.js +3 -0
- package/lib/generated-provenance.js +178 -5
- package/lib/lane1-render.js +668 -0
- package/lib/memory-store.js +663 -0
- package/lib/model-licences.js +305 -0
- package/lib/npy-meta.js +130 -0
- package/lib/png-meta.js +512 -0
- package/lib/records.js +54 -8
- package/lib/reingest-selected.js +15 -1
- package/lib/reingest.js +27 -3
- package/lib/run-manifest.js +59 -11
- package/lib/train-runs.js +214 -0
- package/lib/verifier.js +3 -0
- package/lib/verify-chain.js +367 -0
- package/package.json +2 -2
- package/scripts/asset-ingest.js +160 -0
- package/scripts/canon-draft.js +82 -0
- package/scripts/canon-freeze.js +4 -0
- package/scripts/colab/REAL_SERVICE_ASSUMPTIONS.md +14 -0
- package/scripts/colab/ai-toolkit-override-py315.txt +45 -0
- package/scripts/colab/colab_window.sh +587 -0
- package/scripts/colab/lane1_render.py +970 -0
- package/scripts/colab/launch_guards.py +564 -0
- package/scripts/colab/launch_lane1_stub.sh +309 -0
- package/scripts/colab/launch_lane3_stub.sh +366 -0
- package/scripts/colab/remote_runner.py +59 -0
- package/scripts/doctor.js +1 -0
- package/scripts/generate.js +38 -12
- package/scripts/verify.js +67 -0
package/lib/run-manifest.js
CHANGED
|
@@ -26,6 +26,19 @@ import { createHash } from 'node:crypto';
|
|
|
26
26
|
import { openSync, readSync, closeSync, statSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
27
27
|
import { join } from 'node:path';
|
|
28
28
|
import { warn } from './log.js';
|
|
29
|
+
import {
|
|
30
|
+
CLEARED_SPDX_IDS,
|
|
31
|
+
resolveModelLicence,
|
|
32
|
+
isClearedLicenceEntry,
|
|
33
|
+
assertClearedPinningLicences,
|
|
34
|
+
} from './model-licences.js';
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
CLEARED_SPDX_IDS,
|
|
38
|
+
resolveModelLicence,
|
|
39
|
+
isClearedLicenceEntry,
|
|
40
|
+
assertClearedPinningLicences,
|
|
41
|
+
};
|
|
29
42
|
|
|
30
43
|
/**
|
|
31
44
|
* Contract version for the `pinning` block itself. Bump when the block's shape
|
|
@@ -239,6 +252,17 @@ function sampleFingerprint(path, size) {
|
|
|
239
252
|
}
|
|
240
253
|
}
|
|
241
254
|
|
|
255
|
+
function withLicenceFields(base, name, { licence = null, modelLicences = null } = {}) {
|
|
256
|
+
const lic = resolveModelLicence(name, licence, modelLicences);
|
|
257
|
+
return {
|
|
258
|
+
...base,
|
|
259
|
+
licence_id: lic.licence_id,
|
|
260
|
+
licence_url: lic.licence_url,
|
|
261
|
+
date_read: lic.date_read,
|
|
262
|
+
licence: lic,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
242
266
|
/**
|
|
243
267
|
* Best-effort content identity for one model/LoRA file: {name, size_bytes, sha256}
|
|
244
268
|
* (+ a short `hash_note` when sha256 can't be produced). size_bytes is always
|
|
@@ -255,10 +279,10 @@ function sampleFingerprint(path, size) {
|
|
|
255
279
|
* @param {boolean} [opts.doHash=false] — compute sha256 (else record name+size only)
|
|
256
280
|
* @param {{map: Map, cacheFile?: string, dirty: boolean}} [opts.cache]
|
|
257
281
|
*/
|
|
258
|
-
export function hashModelFile(name, { kind, modelsDir = defaultModelsDir(), doHash = false, cache } = {}) {
|
|
259
|
-
if (!name) return { name: name ?? null, size_bytes: null, sha256: null, hash_note: 'no filename' };
|
|
282
|
+
export function hashModelFile(name, { kind, modelsDir = defaultModelsDir(), doHash = false, cache, licence = null, modelLicences = null } = {}) {
|
|
283
|
+
if (!name) return withLicenceFields({ name: name ?? null, size_bytes: null, sha256: null, hash_note: 'no filename' }, name, { licence, modelLicences });
|
|
260
284
|
const resolved = resolveModelPath(name, kind, modelsDir);
|
|
261
|
-
if (!resolved) return { name, size_bytes: null, sha256: null, hash_note: `unresolved under ${modelsDir}` };
|
|
285
|
+
if (!resolved) return withLicenceFields({ name, size_bytes: null, sha256: null, hash_note: `unresolved under ${modelsDir}` }, name, { licence, modelLicences });
|
|
262
286
|
let size_bytes = null;
|
|
263
287
|
let mtimeMs = 0;
|
|
264
288
|
try {
|
|
@@ -266,9 +290,9 @@ export function hashModelFile(name, { kind, modelsDir = defaultModelsDir(), doHa
|
|
|
266
290
|
size_bytes = st.size;
|
|
267
291
|
mtimeMs = st.mtimeMs;
|
|
268
292
|
} catch {
|
|
269
|
-
return { name, size_bytes: null, sha256: null, hash_note: 'stat failed' };
|
|
293
|
+
return withLicenceFields({ name, size_bytes: null, sha256: null, hash_note: 'stat failed' }, name, { licence, modelLicences });
|
|
270
294
|
}
|
|
271
|
-
if (!doHash) return { name, size_bytes, sha256: null, hash_note: HASH_SKIPPED_NOTE };
|
|
295
|
+
if (!doHash) return withLicenceFields({ name, size_bytes, sha256: null, hash_note: HASH_SKIPPED_NOTE }, name, { licence, modelLicences });
|
|
272
296
|
let sample = '';
|
|
273
297
|
try {
|
|
274
298
|
sample = sampleFingerprint(resolved, size_bytes);
|
|
@@ -278,18 +302,18 @@ export function hashModelFile(name, { kind, modelsDir = defaultModelsDir(), doHa
|
|
|
278
302
|
// real read error anyway.
|
|
279
303
|
}
|
|
280
304
|
const key = `${name}:${size_bytes}:${Math.round(mtimeMs)}:${sample}`;
|
|
281
|
-
if (cache?.map?.has(key)) return { name, size_bytes, sha256: cache.map.get(key) };
|
|
305
|
+
if (cache?.map?.has(key)) return withLicenceFields({ name, size_bytes, sha256: cache.map.get(key) }, name, { licence, modelLicences });
|
|
282
306
|
let sha256;
|
|
283
307
|
try {
|
|
284
308
|
sha256 = sha256File(resolved);
|
|
285
309
|
} catch {
|
|
286
|
-
return { name, size_bytes, sha256: null, hash_note: 'hash failed (read error)' };
|
|
310
|
+
return withLicenceFields({ name, size_bytes, sha256: null, hash_note: 'hash failed (read error)' }, name, { licence, modelLicences });
|
|
287
311
|
}
|
|
288
312
|
if (cache?.map) {
|
|
289
313
|
cache.map.set(key, sha256);
|
|
290
314
|
cache.dirty = true;
|
|
291
315
|
}
|
|
292
|
-
return { name, size_bytes, sha256 };
|
|
316
|
+
return withLicenceFields({ name, size_bytes, sha256 }, name, { licence, modelLicences });
|
|
293
317
|
}
|
|
294
318
|
|
|
295
319
|
/**
|
|
@@ -357,16 +381,34 @@ export function flushHashCache(cache) {
|
|
|
357
381
|
* @param {boolean} [args.hashModels=false] — opt-in sha256 of model/LoRA files
|
|
358
382
|
* @param {string} [args.modelsDir]
|
|
359
383
|
* @param {{map: Map, cacheFile?: string, dirty: boolean}} [args.cache]
|
|
384
|
+
* @param {Object} [args.modelLicences]
|
|
385
|
+
* @param {boolean} [args.enforceLicence=false]
|
|
386
|
+
* @param {boolean} [args.allowUnlicensedResearch=false]
|
|
360
387
|
* @returns {Object} pinning block
|
|
361
388
|
*/
|
|
362
|
-
export function buildPinning({
|
|
363
|
-
|
|
389
|
+
export function buildPinning({
|
|
390
|
+
graph,
|
|
391
|
+
models = {},
|
|
392
|
+
loras = [],
|
|
393
|
+
seedPolicy = null,
|
|
394
|
+
hashModels = false,
|
|
395
|
+
modelsDir = defaultModelsDir(),
|
|
396
|
+
cache,
|
|
397
|
+
modelLicences = null,
|
|
398
|
+
enforceLicence = false,
|
|
399
|
+
allowUnlicensedResearch = false,
|
|
400
|
+
} = {}) {
|
|
401
|
+
const opts = { modelsDir, doHash: hashModels, cache, modelLicences };
|
|
364
402
|
const modelHashes = buildModelHashes(models, opts);
|
|
365
403
|
const loraHashes = (loras || []).map((l) => ({
|
|
366
|
-
...hashModelFile(l.name, { ...opts, kind: 'lora' }),
|
|
404
|
+
...hashModelFile(l.name, { ...opts, kind: 'lora', licence: l.licence || null }),
|
|
367
405
|
weight: l.weight ?? 1.0,
|
|
368
406
|
}));
|
|
369
407
|
|
|
408
|
+
if (enforceLicence) {
|
|
409
|
+
assertClearedPinningLicences(modelHashes, loraHashes, { allowUnlicensedResearch });
|
|
410
|
+
}
|
|
411
|
+
|
|
370
412
|
// B07: hash_note silently encoded a hashing FAILURE (unresolved file,
|
|
371
413
|
// stat failed, read error, no filename) with nothing reading it —
|
|
372
414
|
// buildPinning() never aggregated or logged, and this file imported no
|
|
@@ -384,10 +426,16 @@ export function buildPinning({ graph, models = {}, loras = [], seedPolicy = null
|
|
|
384
426
|
warn(`buildPinning: ${failed.length}/${allEntries.length} model(s)/lora(s) failed to hash: ${detail}`);
|
|
385
427
|
}
|
|
386
428
|
|
|
429
|
+
const ccBy4Eligible =
|
|
430
|
+
!allowUnlicensedResearch &&
|
|
431
|
+
allEntries.length > 0 &&
|
|
432
|
+
allEntries.every((entry) => isClearedLicenceEntry(entry));
|
|
433
|
+
|
|
387
434
|
return {
|
|
388
435
|
pinning_version: PINNING_VERSION,
|
|
389
436
|
comfy_workflow_sha: comfyWorkflowSha(graph),
|
|
390
437
|
seed_policy: seedPolicy,
|
|
438
|
+
cc_by_4_eligible: ccBy4Eligible,
|
|
391
439
|
models: modelHashes,
|
|
392
440
|
loras: loraHashes,
|
|
393
441
|
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trained-LoRA provenance record (`train-run.json`) contract (DESIGN-v3 §7, G13).
|
|
3
|
+
*
|
|
4
|
+
* Links every trained LoRA checkpoint back to:
|
|
5
|
+
* - dataset_package_id + dataset_manifest_sha256
|
|
6
|
+
* - config_sha256 (ai-toolkit-config.yaml)
|
|
7
|
+
* - override_file + override_sha256 + upstream_issue (#1087)
|
|
8
|
+
* - base_model pin with cleared Apache-2.0 SPDX licence
|
|
9
|
+
* - window_id
|
|
10
|
+
* - steps, checkpoint_sha256s, sample_grid_sha256s
|
|
11
|
+
* - eval_pack_sha (attached once the eval pack runs)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { dirname } from 'node:path';
|
|
16
|
+
import {
|
|
17
|
+
AI_TOOLKIT_UPSTREAM_ISSUE,
|
|
18
|
+
assertNotHeldTrainingData,
|
|
19
|
+
getAiToolkitOverrideMetadata,
|
|
20
|
+
} from './adapters/ai-toolkit.js';
|
|
21
|
+
import { inputError as rawInputError } from './errors.js';
|
|
22
|
+
|
|
23
|
+
function inputError(code, msg) {
|
|
24
|
+
return rawInputError(code, `${code}: ${msg}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const CLEARED_TRAIN_BASE_LICENCES = new Set([
|
|
28
|
+
'Apache-2.0',
|
|
29
|
+
'MIT',
|
|
30
|
+
'BSD-2-Clause',
|
|
31
|
+
'BSD-3-Clause',
|
|
32
|
+
'CC0-1.0',
|
|
33
|
+
'CC-BY-4.0',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
function normalizeSha256(val) {
|
|
37
|
+
if (typeof val !== 'string') return null;
|
|
38
|
+
const trimmed = val.trim().toLowerCase();
|
|
39
|
+
const hex = trimmed.startsWith('sha256:') ? trimmed.slice(7) : trimmed;
|
|
40
|
+
if (!/^[0-9a-f]{64}$/.test(hex)) return null;
|
|
41
|
+
return `sha256:${hex}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build and validate a `train-run.json` record (§7, G13).
|
|
46
|
+
*
|
|
47
|
+
* @param {Object} opts
|
|
48
|
+
* @returns {Object}
|
|
49
|
+
*/
|
|
50
|
+
export function buildTrainRunRecord(opts = {}) {
|
|
51
|
+
const {
|
|
52
|
+
run_id,
|
|
53
|
+
window_id,
|
|
54
|
+
dataset_package_id,
|
|
55
|
+
dataset_manifest_sha256,
|
|
56
|
+
config_sha256,
|
|
57
|
+
override_file,
|
|
58
|
+
override_sha256,
|
|
59
|
+
upstream_issue = AI_TOOLKIT_UPSTREAM_ISSUE,
|
|
60
|
+
base_model,
|
|
61
|
+
steps = [],
|
|
62
|
+
checkpoint_sha256s = {},
|
|
63
|
+
sample_grid_sha256s = {},
|
|
64
|
+
eval_pack_sha = null,
|
|
65
|
+
created_at = new Date().toISOString(),
|
|
66
|
+
} = opts;
|
|
67
|
+
|
|
68
|
+
if (!run_id || typeof run_id !== 'string') {
|
|
69
|
+
throw inputError('TRAIN_RUN_INVALID', 'train-run.json requires a non-empty "run_id".');
|
|
70
|
+
}
|
|
71
|
+
if (!window_id || typeof window_id !== 'string') {
|
|
72
|
+
throw inputError('TRAIN_RUN_INVALID', 'train-run.json requires a non-empty "window_id".');
|
|
73
|
+
}
|
|
74
|
+
if (!dataset_package_id || typeof dataset_package_id !== 'string') {
|
|
75
|
+
throw inputError('TRAIN_RUN_INVALID', 'train-run.json requires a non-empty "dataset_package_id".');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
assertNotHeldTrainingData({
|
|
79
|
+
profile: { profile_id: dataset_package_id },
|
|
80
|
+
manifest: { source_export_id: run_id },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const normDatasetSha = normalizeSha256(dataset_manifest_sha256);
|
|
84
|
+
const normConfigSha = normalizeSha256(config_sha256);
|
|
85
|
+
const defaultOverride = getAiToolkitOverrideMetadata();
|
|
86
|
+
const normOverrideSha = normalizeSha256(override_sha256 || defaultOverride.override_sha256);
|
|
87
|
+
|
|
88
|
+
if (!normDatasetSha || !normConfigSha || !normOverrideSha) {
|
|
89
|
+
throw inputError(
|
|
90
|
+
'TRAIN_RUN_SHA_INVALID',
|
|
91
|
+
'train-run.json requires valid 64-hex dataset_manifest_sha256, config_sha256, and override_sha256.'
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!base_model || typeof base_model !== 'object' || !base_model.name) {
|
|
96
|
+
throw inputError('TRAIN_RUN_BASE_MODEL_INVALID', 'train-run.json requires a base_model object with name.');
|
|
97
|
+
}
|
|
98
|
+
const normBaseSha = normalizeSha256(base_model.sha256);
|
|
99
|
+
if (!normBaseSha) {
|
|
100
|
+
throw inputError('TRAIN_RUN_SHA_INVALID', 'base_model.sha256 must be a valid 64-hex sha256 digest.');
|
|
101
|
+
}
|
|
102
|
+
if (
|
|
103
|
+
!base_model.licence_id ||
|
|
104
|
+
!CLEARED_TRAIN_BASE_LICENCES.has(base_model.licence_id) ||
|
|
105
|
+
!base_model.licence_url ||
|
|
106
|
+
!base_model.date_read
|
|
107
|
+
) {
|
|
108
|
+
throw inputError(
|
|
109
|
+
'TRAIN_RUN_LICENCE_INVALID',
|
|
110
|
+
`base_model "${base_model.name}" must carry a cleared commercial SPDX licence_id (${[...CLEARED_TRAIN_BASE_LICENCES].join(', ')}), licence_url, and date_read (got "${base_model.licence_id}").`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
115
|
+
throw inputError('TRAIN_RUN_STEPS_EMPTY', 'train-run.json requires at least one checkpoint step.');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const normalizedCheckpoints = {};
|
|
119
|
+
const normalizedSamples = {};
|
|
120
|
+
for (const step of steps) {
|
|
121
|
+
const key = String(step);
|
|
122
|
+
const ckptSha = normalizeSha256(checkpoint_sha256s[key] ?? checkpoint_sha256s[step]);
|
|
123
|
+
if (!ckptSha) {
|
|
124
|
+
throw inputError(
|
|
125
|
+
'TRAIN_RUN_SHA_INVALID',
|
|
126
|
+
`Missing or invalid checkpoint_sha256 for step ${key}.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
normalizedCheckpoints[key] = ckptSha;
|
|
130
|
+
|
|
131
|
+
const gridRaw = sample_grid_sha256s[key] ?? sample_grid_sha256s[step];
|
|
132
|
+
if (gridRaw !== undefined && gridRaw !== null) {
|
|
133
|
+
const gridSha = normalizeSha256(gridRaw);
|
|
134
|
+
if (!gridSha) {
|
|
135
|
+
throw inputError(
|
|
136
|
+
'TRAIN_RUN_SHA_INVALID',
|
|
137
|
+
`Invalid sample_grid_sha256 for step ${key}.`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
normalizedSamples[key] = gridSha;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const normEvalSha = eval_pack_sha ? normalizeSha256(eval_pack_sha) : null;
|
|
145
|
+
if (eval_pack_sha && !normEvalSha) {
|
|
146
|
+
throw inputError('TRAIN_RUN_SHA_INVALID', 'eval_pack_sha must be null or a valid 64-hex sha256 digest.');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
run_id,
|
|
151
|
+
window_id,
|
|
152
|
+
lane: 'train',
|
|
153
|
+
dataset_package_id,
|
|
154
|
+
dataset_manifest_sha256: normDatasetSha,
|
|
155
|
+
config_sha256: normConfigSha,
|
|
156
|
+
override_file: override_file || defaultOverride.override_file,
|
|
157
|
+
override_sha256: normOverrideSha,
|
|
158
|
+
upstream_issue,
|
|
159
|
+
base_model: {
|
|
160
|
+
name: base_model.name,
|
|
161
|
+
repo: base_model.repo || base_model.name,
|
|
162
|
+
revision: base_model.revision || 'main',
|
|
163
|
+
sha256: normBaseSha,
|
|
164
|
+
licence_id: base_model.licence_id,
|
|
165
|
+
licence_url: base_model.licence_url,
|
|
166
|
+
date_read: base_model.date_read,
|
|
167
|
+
},
|
|
168
|
+
steps: steps.map(Number),
|
|
169
|
+
checkpoint_sha256s: normalizedCheckpoints,
|
|
170
|
+
sample_grid_sha256s: normalizedSamples,
|
|
171
|
+
eval_pack_sha: normEvalSha,
|
|
172
|
+
created_at,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Write `train-run.json` to disk.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} trainRunPath
|
|
180
|
+
* @param {Object} record
|
|
181
|
+
* @returns {Object}
|
|
182
|
+
*/
|
|
183
|
+
export function writeTrainRunJson(trainRunPath, record) {
|
|
184
|
+
const validated = buildTrainRunRecord(record);
|
|
185
|
+
mkdirSync(dirname(trainRunPath), { recursive: true });
|
|
186
|
+
writeFileSync(trainRunPath, JSON.stringify(validated, null, 2) + '\n', 'utf-8');
|
|
187
|
+
return validated;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Read and validate `train-run.json` from disk.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} trainRunPath
|
|
194
|
+
* @returns {Object}
|
|
195
|
+
*/
|
|
196
|
+
export function readTrainRunJson(trainRunPath) {
|
|
197
|
+
const parsed = JSON.parse(readFileSync(trainRunPath, 'utf-8'));
|
|
198
|
+
return buildTrainRunRecord(parsed);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Attach `eval_pack_sha` to an existing `train-run.json` once the eval pack runs (G13).
|
|
203
|
+
*
|
|
204
|
+
* @param {string} trainRunPath
|
|
205
|
+
* @param {string} evalPackSha
|
|
206
|
+
* @returns {Object}
|
|
207
|
+
*/
|
|
208
|
+
export function attachEvalPackSha(trainRunPath, evalPackSha) {
|
|
209
|
+
const current = readTrainRunJson(trainRunPath);
|
|
210
|
+
return writeTrainRunJson(trainRunPath, {
|
|
211
|
+
...current,
|
|
212
|
+
eval_pack_sha: evalPackSha,
|
|
213
|
+
});
|
|
214
|
+
}
|
package/lib/verifier.js
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { warn } from './log.js';
|
|
16
|
+
import { assertNoSelfVerification } from './canon-review-gate.js';
|
|
17
|
+
|
|
18
|
+
export { assertNoSelfVerification };
|
|
16
19
|
|
|
17
20
|
/** The rule-based critique engine's judge id. Bump vN when the rule set changes. */
|
|
18
21
|
export const RULE_BASED_JUDGE = 'rule-based:sdlab-critique-v1';
|