@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
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane 2 canon and prompt authoring adapter (DESIGN-v3 §3.2, §9.2 item 7, §11, G10).
|
|
3
|
+
*
|
|
4
|
+
* Calls a local loopback OpenAI-compatible HTTP endpoint (e.g., llama-server or
|
|
5
|
+
* Ollama serving Muse-Glimmer-30B-A3B-GGUF under Apache-2.0) to draft visual
|
|
6
|
+
* canon entries and per-class prompts + negatives.
|
|
7
|
+
*
|
|
8
|
+
* Every model-authored output starts with `status: "draft"` and `judgment: null`,
|
|
9
|
+
* carrying the model name, digest, SPDX licence (`Apache-2.0`), credit note,
|
|
10
|
+
* `messages_sha256`, and `draft_sha256`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { dirname } from 'node:path';
|
|
16
|
+
import { canonicalize } from './freeze-stamp.js';
|
|
17
|
+
import { inputError as rawInputError } from './errors.js';
|
|
18
|
+
|
|
19
|
+
function inputError(code, msg) {
|
|
20
|
+
return rawInputError(code, `${code}: ${msg}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const MIN_LLAMA_SERVER_BUILD = 10353;
|
|
24
|
+
|
|
25
|
+
export const GLIMMER_CREDIT_NOTE =
|
|
26
|
+
'Canon and prompt drafting assisted by Muse-Glimmer-30B-A3B (GlimmerLabs, Apache-2.0); vision inspection via Qwen3-VL mmproj (Apache-2.0).';
|
|
27
|
+
|
|
28
|
+
export const GLIMMER_DEFAULT_MODEL = Object.freeze({
|
|
29
|
+
name: 'Muse-Glimmer-30B-A3B-Q4_K_M.gguf',
|
|
30
|
+
repo: 'GlimmerLabs/Muse-Glimmer-30B-A3B-GGUF',
|
|
31
|
+
digest: 'sha256:' + '3'.repeat(64),
|
|
32
|
+
licence_id: 'Apache-2.0',
|
|
33
|
+
licence_url: 'https://huggingface.co/GlimmerLabs/Muse-Glimmer-30B-A3B-GGUF/blob/main/LICENSE',
|
|
34
|
+
date_read: '2026-11-05',
|
|
35
|
+
credit_note: GLIMMER_CREDIT_NOTE,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']);
|
|
39
|
+
|
|
40
|
+
function sha256String(str) {
|
|
41
|
+
return 'sha256:' + createHash('sha256').update(str, 'utf-8').digest('hex');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sha256Buffer(buf) {
|
|
45
|
+
return 'sha256:' + createHash('sha256').update(buf).digest('hex');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeSha256(val) {
|
|
49
|
+
if (typeof val !== 'string') return null;
|
|
50
|
+
const trimmed = val.trim().toLowerCase();
|
|
51
|
+
const hex = trimmed.startsWith('sha256:') ? trimmed.slice(7) : trimmed;
|
|
52
|
+
if (!/^[0-9a-f]{64}$/.test(hex)) return null;
|
|
53
|
+
return `sha256:${hex}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Enforce that the Lane 2 LLM endpoint is strictly loopback-bound (§3.2, §6, §11.3).
|
|
58
|
+
*
|
|
59
|
+
* @param {string} endpointUrl
|
|
60
|
+
* @returns {URL}
|
|
61
|
+
*/
|
|
62
|
+
export function assertLoopbackEndpoint(endpointUrl) {
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = new URL(endpointUrl);
|
|
66
|
+
} catch {
|
|
67
|
+
throw inputError(
|
|
68
|
+
'LANE2_NON_LOOPBACK_ENDPOINT',
|
|
69
|
+
`Invalid Lane 2 endpoint URL "${endpointUrl}". Must be http://127.0.0.1:<port>/...`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const host = parsed.hostname.toLowerCase();
|
|
73
|
+
if (!LOOPBACK_HOSTS.has(host)) {
|
|
74
|
+
throw inputError(
|
|
75
|
+
'LANE2_NON_LOOPBACK_ENDPOINT',
|
|
76
|
+
`Lane 2 endpoint "${endpointUrl}" binds to non-loopback host "${host}". Must bind to 127.0.0.1.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return parsed;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Pre-check the llama.cpp `llama-server` build (`>= b10353` for Qwen3.5 /
|
|
84
|
+
* Muse-Glimmer-30B-A3B) and optional `mmproj` vision projector (§9.2 item 7, §11.3).
|
|
85
|
+
*
|
|
86
|
+
* @param {Object} opts
|
|
87
|
+
* @param {string|number} opts.llamaServerBuild — e.g. 'b10353' or 10400
|
|
88
|
+
* @param {boolean} [opts.vision=false] — whether vision inspection / plate reading is active
|
|
89
|
+
* @param {string} [opts.mmprojPath]
|
|
90
|
+
* @param {string} [opts.mmprojSha256]
|
|
91
|
+
* @param {boolean} [opts.verifyFiles=false]
|
|
92
|
+
* @returns {{ ok: true, buildNumber: number, vision: boolean, mmprojSha256: string|null }}
|
|
93
|
+
*/
|
|
94
|
+
export function verifyGlimmerRuntime({
|
|
95
|
+
llamaServerBuild,
|
|
96
|
+
vision = false,
|
|
97
|
+
mmprojPath = null,
|
|
98
|
+
mmprojSha256 = null,
|
|
99
|
+
verifyFiles = false,
|
|
100
|
+
} = {}) {
|
|
101
|
+
const rawBuild = String(llamaServerBuild ?? '').trim();
|
|
102
|
+
const match = /(\d{4,6})/.exec(rawBuild);
|
|
103
|
+
const buildNumber = match ? Number(match[1]) : NaN;
|
|
104
|
+
if (!Number.isFinite(buildNumber) || buildNumber < MIN_LLAMA_SERVER_BUILD) {
|
|
105
|
+
throw inputError(
|
|
106
|
+
'LANE2_LLAMA_SERVER_TOO_OLD',
|
|
107
|
+
`Muse-Glimmer-30B-A3B (Qwen3.5 architecture) requires llama-server >= b${MIN_LLAMA_SERVER_BUILD}; got "${rawBuild}".`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let normalizedMmprojSha = null;
|
|
112
|
+
if (vision) {
|
|
113
|
+
normalizedMmprojSha = normalizeSha256(mmprojSha256);
|
|
114
|
+
if (!mmprojPath || !normalizedMmprojSha) {
|
|
115
|
+
throw inputError(
|
|
116
|
+
'LANE2_MMPROJ_MISSING',
|
|
117
|
+
'Vision mode on Muse-Glimmer-30B-A3B requires --mmproj (mmproj-F16.gguf or mmproj-BF16.gguf) with a pinned 64-hex sha256.'
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (verifyFiles) {
|
|
121
|
+
if (!existsSync(mmprojPath)) {
|
|
122
|
+
throw inputError('LANE2_MMPROJ_MISSING', `mmproj file not found at "${mmprojPath}".`);
|
|
123
|
+
}
|
|
124
|
+
const actualSha = sha256Buffer(readFileSync(mmprojPath));
|
|
125
|
+
if (actualSha !== normalizedMmprojSha) {
|
|
126
|
+
throw inputError(
|
|
127
|
+
'LANE2_MMPROJ_SHA_MISMATCH',
|
|
128
|
+
`mmproj sha256 mismatch: got ${actualSha}, expected ${normalizedMmprojSha}.`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
ok: true,
|
|
136
|
+
buildNumber,
|
|
137
|
+
vision: Boolean(vision),
|
|
138
|
+
mmprojSha256: normalizedMmprojSha,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Compute the canonical content hash of a canon/prompt draft so any edit after
|
|
144
|
+
* Director ratification is detected by the freeze gate.
|
|
145
|
+
*
|
|
146
|
+
* @param {Object} draft
|
|
147
|
+
* @returns {string}
|
|
148
|
+
*/
|
|
149
|
+
const RESERVED_MODEL_KEYS = Object.freeze([
|
|
150
|
+
'ratification',
|
|
151
|
+
'judgment',
|
|
152
|
+
'status',
|
|
153
|
+
'drafted_by_model',
|
|
154
|
+
'author_model',
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
function stripReservedModelKeys(obj) {
|
|
158
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return {};
|
|
159
|
+
const cleaned = { ...obj };
|
|
160
|
+
for (const key of RESERVED_MODEL_KEYS) {
|
|
161
|
+
delete cleaned[key];
|
|
162
|
+
}
|
|
163
|
+
return cleaned;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function computeDraftContentSha256(draft) {
|
|
167
|
+
const payload = {
|
|
168
|
+
class_id: draft.class_id || draft.entry_id || null,
|
|
169
|
+
prompt: draft.prompt || '',
|
|
170
|
+
negative_prompt: draft.negative_prompt || '',
|
|
171
|
+
canon_entry_draft: draft.canon_entry_draft || null,
|
|
172
|
+
messages_sha256: draft.messages_sha256 || null,
|
|
173
|
+
};
|
|
174
|
+
return sha256String(canonicalize(payload));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Build the request messages for drafting a visual canon class and prompt.
|
|
179
|
+
*
|
|
180
|
+
* @param {Object} opts
|
|
181
|
+
* @param {string} opts.classId
|
|
182
|
+
* @param {string} opts.brief
|
|
183
|
+
* @param {Object} [opts.canonContext]
|
|
184
|
+
* @returns {{ messages: Array<Object>, messagesSha256: string }}
|
|
185
|
+
*/
|
|
186
|
+
export function buildCanonDraftMessages({ classId, brief, canonContext = {} }) {
|
|
187
|
+
const messages = [
|
|
188
|
+
{
|
|
189
|
+
role: 'system',
|
|
190
|
+
content:
|
|
191
|
+
'You are the sdlab Lane 2 canon and prompt drafter (Muse-Glimmer-30B-A3B, Apache-2.0). ' +
|
|
192
|
+
'Return strict JSON with keys: class_id, canon_entry_draft, prompt, negative_prompt.',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
role: 'user',
|
|
196
|
+
content: JSON.stringify({
|
|
197
|
+
class_id: classId,
|
|
198
|
+
brief,
|
|
199
|
+
canon_context: canonContext,
|
|
200
|
+
}),
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
const messagesSha256 = sha256String(canonicalize(messages));
|
|
204
|
+
return { messages, messagesSha256 };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Draft a visual canon entry and class prompt via a loopback OpenAI-compatible
|
|
209
|
+
* endpoint (`http://127.0.0.1:<port>/v1/chat/completions`) (G10, §3.2, §11).
|
|
210
|
+
*
|
|
211
|
+
* Always emits `status: "draft"` and `judgment: null`.
|
|
212
|
+
*
|
|
213
|
+
* @param {Object} opts
|
|
214
|
+
* @param {string} opts.endpoint
|
|
215
|
+
* @param {string} opts.classId
|
|
216
|
+
* @param {string} opts.brief
|
|
217
|
+
* @param {Object} [opts.canonContext]
|
|
218
|
+
* @param {Object} [opts.modelPin]
|
|
219
|
+
* @param {string} [opts.createdAt]
|
|
220
|
+
* @returns {Promise<Object>}
|
|
221
|
+
*/
|
|
222
|
+
export async function draftCanonClassViaEndpoint({
|
|
223
|
+
endpoint,
|
|
224
|
+
classId,
|
|
225
|
+
brief,
|
|
226
|
+
canonContext = {},
|
|
227
|
+
modelPin = GLIMMER_DEFAULT_MODEL,
|
|
228
|
+
createdAt = new Date().toISOString(),
|
|
229
|
+
}) {
|
|
230
|
+
assertLoopbackEndpoint(endpoint);
|
|
231
|
+
if (!classId || typeof classId !== 'string') {
|
|
232
|
+
throw inputError('LANE2_CLASS_ID_REQUIRED', 'draftCanonClassViaEndpoint requires a non-empty classId.');
|
|
233
|
+
}
|
|
234
|
+
const authorModel = {
|
|
235
|
+
name: modelPin.name || GLIMMER_DEFAULT_MODEL.name,
|
|
236
|
+
repo: modelPin.repo || GLIMMER_DEFAULT_MODEL.repo,
|
|
237
|
+
digest: normalizeSha256(modelPin.digest) || GLIMMER_DEFAULT_MODEL.digest,
|
|
238
|
+
licence_id: modelPin.licence_id || 'Apache-2.0',
|
|
239
|
+
licence_url: modelPin.licence_url || GLIMMER_DEFAULT_MODEL.licence_url,
|
|
240
|
+
date_read: modelPin.date_read || GLIMMER_DEFAULT_MODEL.date_read,
|
|
241
|
+
credit_note: modelPin.credit_note || GLIMMER_CREDIT_NOTE,
|
|
242
|
+
};
|
|
243
|
+
if (authorModel.licence_id !== 'Apache-2.0') {
|
|
244
|
+
throw inputError(
|
|
245
|
+
'LANE2_LICENCE_INVALID',
|
|
246
|
+
`Lane 2 canon authoring model "${authorModel.name}" must carry licence_id "Apache-2.0" (got "${authorModel.licence_id}").`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const { messages, messagesSha256 } = buildCanonDraftMessages({ classId, brief, canonContext });
|
|
251
|
+
|
|
252
|
+
const response = await fetch(endpoint, {
|
|
253
|
+
method: 'POST',
|
|
254
|
+
headers: { 'Content-Type': 'application/json' },
|
|
255
|
+
body: JSON.stringify({
|
|
256
|
+
model: authorModel.name,
|
|
257
|
+
temperature: 0.2,
|
|
258
|
+
top_p: 0.9,
|
|
259
|
+
messages,
|
|
260
|
+
}),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (!response.ok) {
|
|
264
|
+
throw inputError(
|
|
265
|
+
'LANE2_ENDPOINT_ERROR',
|
|
266
|
+
`Lane 2 endpoint ${endpoint} returned HTTP ${response.status}.`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const body = await response.json();
|
|
271
|
+
const rawContent = body?.choices?.[0]?.message?.content || '{}';
|
|
272
|
+
let parsedContent;
|
|
273
|
+
try {
|
|
274
|
+
const rawParsed = typeof rawContent === 'string' ? JSON.parse(rawContent) : rawContent;
|
|
275
|
+
parsedContent = stripReservedModelKeys(rawParsed);
|
|
276
|
+
} catch {
|
|
277
|
+
parsedContent = {
|
|
278
|
+
prompt: String(rawContent),
|
|
279
|
+
negative_prompt: '',
|
|
280
|
+
canon_entry_draft: { id: classId, notes: String(rawContent) },
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const rawCanonEntry =
|
|
285
|
+
parsedContent.canon_entry_draft &&
|
|
286
|
+
typeof parsedContent.canon_entry_draft === 'object' &&
|
|
287
|
+
!Array.isArray(parsedContent.canon_entry_draft)
|
|
288
|
+
? stripReservedModelKeys(parsedContent.canon_entry_draft)
|
|
289
|
+
: { id: classId };
|
|
290
|
+
const canonEntryDraft = {
|
|
291
|
+
...rawCanonEntry,
|
|
292
|
+
id: rawCanonEntry.id || classId,
|
|
293
|
+
status: 'draft',
|
|
294
|
+
drafted_by_model: authorModel.name,
|
|
295
|
+
author_model: authorModel,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const draftBase = {
|
|
299
|
+
draft_id: `draft_${classId}`,
|
|
300
|
+
class_id: classId,
|
|
301
|
+
entry_id: classId,
|
|
302
|
+
status: 'draft',
|
|
303
|
+
judgment: null,
|
|
304
|
+
prompt: parsedContent.prompt || '',
|
|
305
|
+
negative_prompt: parsedContent.negative_prompt || '',
|
|
306
|
+
canon_entry_draft: canonEntryDraft,
|
|
307
|
+
drafted_by_model: authorModel.name,
|
|
308
|
+
author_model: authorModel,
|
|
309
|
+
messages_sha256: messagesSha256,
|
|
310
|
+
created_at: createdAt,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const draftSha256 = computeDraftContentSha256(draftBase);
|
|
314
|
+
return {
|
|
315
|
+
...draftBase,
|
|
316
|
+
draft_sha256: draftSha256,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Write `prompts.jsonl` and its companion `prompts.jsonl.sha256` (§7).
|
|
322
|
+
*
|
|
323
|
+
* @param {string} promptsPath
|
|
324
|
+
* @param {Array<Object>} drafts
|
|
325
|
+
* @returns {{ promptsPath: string, sha256Path: string, sha256: string }}
|
|
326
|
+
*/
|
|
327
|
+
export function writePromptsJsonl(promptsPath, drafts) {
|
|
328
|
+
mkdirSync(dirname(promptsPath), { recursive: true });
|
|
329
|
+
const content = drafts.map((d) => JSON.stringify(d)).join('\n') + '\n';
|
|
330
|
+
writeFileSync(promptsPath, content, 'utf-8');
|
|
331
|
+
const digest = sha256String(content);
|
|
332
|
+
const sha256Path = `${promptsPath}.sha256`;
|
|
333
|
+
writeFileSync(sha256Path, `${digest} prompts.jsonl\n`, 'utf-8');
|
|
334
|
+
return { promptsPath, sha256Path, sha256: digest };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Read `prompts.jsonl` rows from disk.
|
|
339
|
+
*
|
|
340
|
+
* @param {string} promptsPath
|
|
341
|
+
* @returns {Array<Object>}
|
|
342
|
+
*/
|
|
343
|
+
export function readPromptsJsonl(promptsPath) {
|
|
344
|
+
const raw = readFileSync(promptsPath, 'utf-8');
|
|
345
|
+
return raw
|
|
346
|
+
.split(/\r?\n/)
|
|
347
|
+
.map((l) => l.trim())
|
|
348
|
+
.filter(Boolean)
|
|
349
|
+
.map((line) => JSON.parse(line));
|
|
350
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review gate before canon freeze & self-verification ban (DESIGN-v3 §11.5, §12.6, G11).
|
|
3
|
+
*
|
|
4
|
+
* Enforces:
|
|
5
|
+
* 1. Model-authored canon/prompt drafts are only ever proposed (`status: "draft"`).
|
|
6
|
+
* 2. Freezing or ratifying a draft requires an explicit recorded Director accept
|
|
7
|
+
* (`approver: "human:director"`, non-empty `reason`, and matching `draft_sha256`).
|
|
8
|
+
* 3. A model may never self-approve or be the deciding verifier of a canon
|
|
9
|
+
* entry or prompt it wrote (`SELF_VERIFICATION_FORBIDDEN`).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { computeDraftContentSha256 } from './canon-author.js';
|
|
13
|
+
import { inputError as rawInputError } from './errors.js';
|
|
14
|
+
|
|
15
|
+
function inputError(code, msg) {
|
|
16
|
+
return rawInputError(code, `${code}: ${msg}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function normalizeModelFamilyKey(modelId) {
|
|
20
|
+
if (!modelId || typeof modelId !== 'string') return '';
|
|
21
|
+
const lower = modelId
|
|
22
|
+
.trim()
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.replace(/^model:/, '')
|
|
25
|
+
.replace(/@sha256:[0-9a-f]{64}$/, '');
|
|
26
|
+
if (!lower) return '';
|
|
27
|
+
if (lower.startsWith('human:') || lower === 'human') return 'human';
|
|
28
|
+
if (lower.startsWith('rule-based:') || lower === 'rule-based') return 'rule-based';
|
|
29
|
+
if (lower.includes('glimmer') || lower.includes('muse-glimmer') || lower.includes('muse_glimmer')) {
|
|
30
|
+
return 'family:muse-glimmer-30b';
|
|
31
|
+
}
|
|
32
|
+
if (lower.includes('qwen')) {
|
|
33
|
+
return 'family:qwen';
|
|
34
|
+
}
|
|
35
|
+
if (lower.includes('gemma')) {
|
|
36
|
+
return 'family:gemma';
|
|
37
|
+
}
|
|
38
|
+
if (lower.includes('internvl') || lower.includes('caprl-internvl')) {
|
|
39
|
+
return 'family:internvl';
|
|
40
|
+
}
|
|
41
|
+
if (lower.includes('siglip') || lower === 'ai-eyes') {
|
|
42
|
+
return 'family:siglip2';
|
|
43
|
+
}
|
|
44
|
+
if (lower.includes('flux') || lower === 'ae.safetensors') {
|
|
45
|
+
return 'family:flux';
|
|
46
|
+
}
|
|
47
|
+
if (lower.includes('sd3') || lower.includes('stable-diffusion-3')) {
|
|
48
|
+
return 'family:sd3';
|
|
49
|
+
}
|
|
50
|
+
if (
|
|
51
|
+
lower.includes('sdxl') ||
|
|
52
|
+
lower.includes('stable-diffusion-xl') ||
|
|
53
|
+
lower.includes('dreamshaper') ||
|
|
54
|
+
lower.includes('juggernaut') ||
|
|
55
|
+
lower.includes('pony') ||
|
|
56
|
+
lower.includes('illustrious') ||
|
|
57
|
+
lower.includes('animagine')
|
|
58
|
+
) {
|
|
59
|
+
return 'family:sdxl';
|
|
60
|
+
}
|
|
61
|
+
return lower;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Enforce the §12.6 self-verification ban: a model (such as Muse-Glimmer-30B)
|
|
66
|
+
* may never be the deciding reader/verifier of a canon entry or prompt it wrote.
|
|
67
|
+
*
|
|
68
|
+
* @param {Object} opts
|
|
69
|
+
* @param {string|Object} opts.authorModel
|
|
70
|
+
* @param {string|Object} opts.verifierModel
|
|
71
|
+
* @param {string} [opts.context='artifact']
|
|
72
|
+
* @returns {{ ok: true, authorFamily: string, verifierFamily: string }}
|
|
73
|
+
*/
|
|
74
|
+
export function assertNoSelfVerification({ authorModel, verifierModel, context = 'artifact' } = {}) {
|
|
75
|
+
const authorRaw = typeof authorModel === 'object' ? authorModel?.name : authorModel;
|
|
76
|
+
const verifierRaw = typeof verifierModel === 'object' ? verifierModel?.name : verifierModel;
|
|
77
|
+
|
|
78
|
+
const authorFamily = normalizeModelFamilyKey(authorRaw);
|
|
79
|
+
const verifierFamily = normalizeModelFamilyKey(verifierRaw);
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
authorFamily &&
|
|
83
|
+
verifierFamily &&
|
|
84
|
+
authorFamily !== 'human' &&
|
|
85
|
+
verifierFamily !== 'human' &&
|
|
86
|
+
authorFamily === verifierFamily
|
|
87
|
+
) {
|
|
88
|
+
throw inputError(
|
|
89
|
+
'SELF_VERIFICATION_FORBIDDEN',
|
|
90
|
+
`Self-verification forbidden on ${context}: verifier "${verifierRaw}" belongs to the same model family ("${verifierFamily}") as author "${authorRaw}".`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { ok: true, authorFamily, verifierFamily };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Record an explicit Director ratification on a model-authored canon/prompt draft (G11).
|
|
99
|
+
*
|
|
100
|
+
* @param {Object} opts
|
|
101
|
+
* @param {Object} opts.draft
|
|
102
|
+
* @param {string} opts.approver — must be "human:director" (or "human:<id>")
|
|
103
|
+
* @param {string} opts.reason
|
|
104
|
+
* @param {string} [opts.at]
|
|
105
|
+
* @returns {Object} ratified draft
|
|
106
|
+
*/
|
|
107
|
+
export function ratifyCanonDraft({
|
|
108
|
+
draft,
|
|
109
|
+
approver,
|
|
110
|
+
reason,
|
|
111
|
+
at = new Date().toISOString(),
|
|
112
|
+
} = {}) {
|
|
113
|
+
if (!draft || typeof draft !== 'object') {
|
|
114
|
+
throw inputError('CANON_DRAFT_INVALID', 'ratifyCanonDraft requires a draft object.');
|
|
115
|
+
}
|
|
116
|
+
const approverStr = String(approver || '').trim();
|
|
117
|
+
if (approverStr.startsWith('model:') || !approverStr.startsWith('human:')) {
|
|
118
|
+
throw inputError(
|
|
119
|
+
'CANON_SELF_APPROVAL_FORBIDDEN',
|
|
120
|
+
`Canon draft "${draft.class_id || draft.draft_id}" can only be ratified by a human Director ("human:director"), got "${approverStr}".`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const reasonStr = String(reason || '').trim();
|
|
124
|
+
if (!reasonStr) {
|
|
125
|
+
throw inputError(
|
|
126
|
+
'CANON_RATIFICATION_REASON_REQUIRED',
|
|
127
|
+
`Ratifying canon draft "${draft.class_id || draft.draft_id}" requires a non-empty reason.`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const contentSha = computeDraftContentSha256(draft);
|
|
132
|
+
return {
|
|
133
|
+
...draft,
|
|
134
|
+
status: 'ratified',
|
|
135
|
+
judgment: 'accepted',
|
|
136
|
+
draft_sha256: contentSha,
|
|
137
|
+
ratification: {
|
|
138
|
+
status: 'accepted',
|
|
139
|
+
approver: approverStr,
|
|
140
|
+
reason: reasonStr,
|
|
141
|
+
at,
|
|
142
|
+
draft_sha256: contentSha,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Assert that a canon entry or prompt draft has been explicitly ratified by
|
|
149
|
+
* the Director before it can be frozen (G11, §11.5).
|
|
150
|
+
*
|
|
151
|
+
* Refuses:
|
|
152
|
+
* - `status === "draft"` or missing `ratification` on a model-authored draft
|
|
153
|
+
* - `ratification.approver` not starting with `"human:"`
|
|
154
|
+
* - `ratification.draft_sha256` mismatch against the current draft content
|
|
155
|
+
*
|
|
156
|
+
* @param {Object} draftOrFrontmatter
|
|
157
|
+
* @returns {{ ok: true, ratification: Object|null }}
|
|
158
|
+
*/
|
|
159
|
+
export function assertDirectorRatifiedForFreeze(draftOrFrontmatter) {
|
|
160
|
+
if (!draftOrFrontmatter || typeof draftOrFrontmatter !== 'object') {
|
|
161
|
+
throw inputError('CANON_DRAFT_INVALID', 'Cannot verify freeze gate on empty draft/frontmatter.');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const status = draftOrFrontmatter.status || draftOrFrontmatter.draft_status || null;
|
|
165
|
+
const isModelDraft =
|
|
166
|
+
status === 'draft' ||
|
|
167
|
+
status === 'ratified' ||
|
|
168
|
+
Boolean(draftOrFrontmatter.ratification) ||
|
|
169
|
+
Boolean(draftOrFrontmatter.author_model) ||
|
|
170
|
+
Boolean(draftOrFrontmatter.drafted_by_model);
|
|
171
|
+
|
|
172
|
+
if (!isModelDraft) {
|
|
173
|
+
return { ok: true, ratification: null };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const ratification = draftOrFrontmatter.ratification;
|
|
177
|
+
if (
|
|
178
|
+
status === 'draft' ||
|
|
179
|
+
!ratification ||
|
|
180
|
+
ratification.status !== 'accepted' ||
|
|
181
|
+
typeof ratification.approver !== 'string' ||
|
|
182
|
+
!ratification.approver.startsWith('human:') ||
|
|
183
|
+
typeof ratification.reason !== 'string' ||
|
|
184
|
+
!ratification.reason.trim()
|
|
185
|
+
) {
|
|
186
|
+
throw inputError(
|
|
187
|
+
'CANON_DRAFT_UNRATIFIED',
|
|
188
|
+
`Refusing to freeze model-authored draft "${draftOrFrontmatter.class_id || draftOrFrontmatter.id || 'unknown'}": ` +
|
|
189
|
+
'status is still "draft" or lacks an explicit human Director ratification (`approver: "human:director"`, `reason`).'
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (draftOrFrontmatter.prompt !== undefined || draftOrFrontmatter.canon_entry_draft !== undefined) {
|
|
194
|
+
const actualSha = computeDraftContentSha256(draftOrFrontmatter);
|
|
195
|
+
if (ratification.draft_sha256 !== actualSha) {
|
|
196
|
+
throw inputError(
|
|
197
|
+
'CANON_DRAFT_SHA_MISMATCH',
|
|
198
|
+
`Draft content for "${draftOrFrontmatter.class_id || draftOrFrontmatter.id}" changed after Director ratification ` +
|
|
199
|
+
`(got ${actualSha}, ratified ${ratification.draft_sha256}). Re-ratify before freezing.`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { ok: true, ratification };
|
|
205
|
+
}
|