@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,663 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sdlab-native persistent memory store to offrig's schema (DESIGN-v3 §7, §12.1–§12.6).
|
|
3
|
+
*
|
|
4
|
+
* Two stores only:
|
|
5
|
+
* - Global: <rootDir>/memory/global/entries.jsonl
|
|
6
|
+
* - Project: <rootDir>/projects/<project>/memory/entries.jsonl
|
|
7
|
+
*
|
|
8
|
+
* Precedence & safety rules:
|
|
9
|
+
* 1. Lookup order: global active entries, then project active entries.
|
|
10
|
+
* 2. Explicit override only: a project entry overriding or contradicting a
|
|
11
|
+
* global entry must set `overrides: <global_id>`, `reason`, and
|
|
12
|
+
* `approver: "human:director"`. Silent contradiction is refused.
|
|
13
|
+
* 3. No cross-talk: loading a project's memory reads only global + that
|
|
14
|
+
* project's store. Any cross-project store request is refused.
|
|
15
|
+
* 4. Promotion rule: promoting a project rule to global writes a reviewed
|
|
16
|
+
* `promotion` entry (`approver: "human:director"`, `reason`) and keeps
|
|
17
|
+
* the project entry intact.
|
|
18
|
+
* 5. Proposed vs active: model-authored proposals (`author: "model:..."`)
|
|
19
|
+
* must start as `status: "proposed"` with `approver: null`; only an
|
|
20
|
+
* explicit human Director approval transitions them to `status: "active"`.
|
|
21
|
+
* 6. Sealed holdout guard: refuses any memory proposal citing a sealed
|
|
22
|
+
* holdout record id.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createHash } from 'node:crypto';
|
|
26
|
+
import {
|
|
27
|
+
existsSync,
|
|
28
|
+
mkdirSync,
|
|
29
|
+
readFileSync,
|
|
30
|
+
writeFileSync,
|
|
31
|
+
appendFileSync,
|
|
32
|
+
} from 'node:fs';
|
|
33
|
+
import { dirname, join } from 'node:path';
|
|
34
|
+
import { canonicalize } from './freeze-stamp.js';
|
|
35
|
+
import { assertNoSelfVerification } from './canon-review-gate.js';
|
|
36
|
+
import { inputError as rawInputError } from './errors.js';
|
|
37
|
+
|
|
38
|
+
function inputError(code, msg) {
|
|
39
|
+
return rawInputError(code, `${code}: ${msg}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const MEMORY_SCOPES = Object.freeze(['global', 'project']);
|
|
43
|
+
|
|
44
|
+
export const MEMORY_KINDS = Object.freeze([
|
|
45
|
+
'rule',
|
|
46
|
+
'failure_example',
|
|
47
|
+
'banned_token',
|
|
48
|
+
'checklist',
|
|
49
|
+
'promotion',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
export const MEMORY_STATUSES = Object.freeze([
|
|
53
|
+
'proposed',
|
|
54
|
+
'active',
|
|
55
|
+
'superseded',
|
|
56
|
+
'rejected',
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
export const ALLOWED_VERDICTS = Object.freeze(['pass', 'fail', 'cannot_tell']);
|
|
60
|
+
|
|
61
|
+
export const ALLOWED_FAILURE_MODES = Object.freeze([
|
|
62
|
+
'silhouette_drift',
|
|
63
|
+
'palette_violation',
|
|
64
|
+
'material_mismatch',
|
|
65
|
+
'era_anachronism',
|
|
66
|
+
'rigging_tangle',
|
|
67
|
+
'perspective_violation',
|
|
68
|
+
'lighting_glare',
|
|
69
|
+
'anatomy_distortion',
|
|
70
|
+
'text_watermark',
|
|
71
|
+
'framing_crop',
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
function sha256String(str) {
|
|
75
|
+
return 'sha256:' + createHash('sha256').update(str, 'utf-8').digest('hex');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeSha256(val) {
|
|
79
|
+
if (typeof val !== 'string') return null;
|
|
80
|
+
const trimmed = val.trim().toLowerCase();
|
|
81
|
+
const hex = trimmed.startsWith('sha256:') ? trimmed.slice(7) : trimmed;
|
|
82
|
+
if (!/^[0-9a-f]{64}$/.test(hex)) return null;
|
|
83
|
+
return `sha256:${hex}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validateProjectSlug(project) {
|
|
87
|
+
if (
|
|
88
|
+
!project ||
|
|
89
|
+
typeof project !== 'string' ||
|
|
90
|
+
project.includes('..') ||
|
|
91
|
+
project.includes('/') ||
|
|
92
|
+
project.includes('\\')
|
|
93
|
+
) {
|
|
94
|
+
throw inputError(
|
|
95
|
+
'MEMORY_CROSS_TALK_FORBIDDEN',
|
|
96
|
+
`Invalid or unsafe project slug "${project}".`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return project;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the JSONL path for either the global memory store or a single project store.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} rootDir
|
|
106
|
+
* @param {'global'|'project'} scope
|
|
107
|
+
* @param {string|null} [project]
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
110
|
+
export function getMemoryStorePath(rootDir, scope, project = null) {
|
|
111
|
+
if (scope === 'global') {
|
|
112
|
+
return join(rootDir, 'memory', 'global', 'entries.jsonl');
|
|
113
|
+
}
|
|
114
|
+
if (scope === 'project') {
|
|
115
|
+
validateProjectSlug(project);
|
|
116
|
+
return join(rootDir, 'projects', project, 'memory', 'entries.jsonl');
|
|
117
|
+
}
|
|
118
|
+
throw inputError('MEMORY_SCOPE_INVALID', `Unknown memory scope "${scope}".`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Read all raw rows from a memory store file.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} storePath
|
|
125
|
+
* @returns {Array<Object>}
|
|
126
|
+
*/
|
|
127
|
+
export function readStoreEntries(storePath) {
|
|
128
|
+
if (!existsSync(storePath)) return [];
|
|
129
|
+
return readFileSync(storePath, 'utf-8')
|
|
130
|
+
.split(/\r?\n/)
|
|
131
|
+
.map((l) => l.trim())
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.map((line) => JSON.parse(line));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function writeStoreEntries(storePath, entries) {
|
|
137
|
+
mkdirSync(dirname(storePath), { recursive: true });
|
|
138
|
+
const body = entries.map((e) => JSON.stringify(e)).join('\n') + (entries.length ? '\n' : '');
|
|
139
|
+
writeFileSync(storePath, body, 'utf-8');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function checkSealedHoldoutCitations(entry, sealedHoldoutIds = []) {
|
|
143
|
+
if (!Array.isArray(sealedHoldoutIds) || sealedHoldoutIds.length === 0) return;
|
|
144
|
+
const refs = Array.isArray(entry.evidence_refs) ? entry.evidence_refs.map(String) : [];
|
|
145
|
+
const statement = String(entry.statement || '');
|
|
146
|
+
for (const holdoutId of sealedHoldoutIds) {
|
|
147
|
+
if (!holdoutId) continue;
|
|
148
|
+
const hitInRefs = refs.some((r) => r === holdoutId || r.includes(holdoutId));
|
|
149
|
+
const hitInStatement = statement.includes(holdoutId);
|
|
150
|
+
if (hitInRefs || hitInStatement) {
|
|
151
|
+
throw inputError(
|
|
152
|
+
'MEMORY_HOLDOUT_CITATION_FORBIDDEN',
|
|
153
|
+
`Memory entry "${entry.id}" cites sealed holdout record "${holdoutId}" (§12.6).`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Append a validated memory entry to the global or project store (§12.2–§12.6).
|
|
161
|
+
*
|
|
162
|
+
* @param {Object} opts
|
|
163
|
+
* @param {string} opts.rootDir
|
|
164
|
+
* @param {Object} opts.entry
|
|
165
|
+
* @param {Array<string>} [opts.sealedHoldoutIds]
|
|
166
|
+
* @returns {Object}
|
|
167
|
+
*/
|
|
168
|
+
export function appendMemoryEntry({ rootDir, entry, sealedHoldoutIds = [] }) {
|
|
169
|
+
if (!entry || typeof entry !== 'object') {
|
|
170
|
+
throw inputError('MEMORY_ENTRY_INVALID', 'Memory entry must be an object.');
|
|
171
|
+
}
|
|
172
|
+
const {
|
|
173
|
+
id,
|
|
174
|
+
scope,
|
|
175
|
+
project = null,
|
|
176
|
+
kind,
|
|
177
|
+
rule_key = null,
|
|
178
|
+
status = 'proposed',
|
|
179
|
+
statement,
|
|
180
|
+
evidence_refs = [],
|
|
181
|
+
author,
|
|
182
|
+
author_model = null,
|
|
183
|
+
drafted_by_model = null,
|
|
184
|
+
approver = null,
|
|
185
|
+
overrides = null,
|
|
186
|
+
superseded_by = null,
|
|
187
|
+
reason = null,
|
|
188
|
+
promoted_from = null,
|
|
189
|
+
created_at = new Date().toISOString(),
|
|
190
|
+
approved_at = null,
|
|
191
|
+
} = entry;
|
|
192
|
+
|
|
193
|
+
if (!id || typeof id !== 'string') {
|
|
194
|
+
throw inputError('MEMORY_ENTRY_INVALID', 'Memory entry requires a non-empty string "id".');
|
|
195
|
+
}
|
|
196
|
+
if (!MEMORY_SCOPES.includes(scope)) {
|
|
197
|
+
throw inputError('MEMORY_SCOPE_INVALID', `Memory entry "${id}" has invalid scope "${scope}".`);
|
|
198
|
+
}
|
|
199
|
+
if (scope === 'global' && project !== null) {
|
|
200
|
+
throw inputError('MEMORY_SCOPE_INVALID', `Global memory entry "${id}" must have project: null.`);
|
|
201
|
+
}
|
|
202
|
+
if (scope === 'project') {
|
|
203
|
+
validateProjectSlug(project);
|
|
204
|
+
}
|
|
205
|
+
if (!MEMORY_KINDS.includes(kind)) {
|
|
206
|
+
throw inputError('MEMORY_KIND_INVALID', `Memory entry "${id}" has invalid kind "${kind}".`);
|
|
207
|
+
}
|
|
208
|
+
if (!MEMORY_STATUSES.includes(status)) {
|
|
209
|
+
throw inputError('MEMORY_STATUS_INVALID', `Memory entry "${id}" has invalid status "${status}".`);
|
|
210
|
+
}
|
|
211
|
+
if (!statement || typeof statement !== 'string' || !statement.trim()) {
|
|
212
|
+
throw inputError('MEMORY_ENTRY_INVALID', `Memory entry "${id}" requires a non-empty statement.`);
|
|
213
|
+
}
|
|
214
|
+
if (!author || typeof author !== 'string') {
|
|
215
|
+
throw inputError('MEMORY_ENTRY_INVALID', `Memory entry "${id}" requires an author.`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Sealed holdout citation guard (§12.6)
|
|
219
|
+
checkSealedHoldoutCitations({ id, statement, evidence_refs }, sealedHoldoutIds);
|
|
220
|
+
|
|
221
|
+
const isModelAuthor =
|
|
222
|
+
author.startsWith('model:') || Boolean(author_model) || Boolean(drafted_by_model);
|
|
223
|
+
if (isModelAuthor && status !== 'proposed' && kind !== 'promotion') {
|
|
224
|
+
throw inputError(
|
|
225
|
+
'MEMORY_MODEL_ACTIVE_FORBIDDEN',
|
|
226
|
+
`Model-authored memory entry "${id}" (${author}) must start as status "proposed", not "${status}" (§12.4).`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (status === 'active') {
|
|
231
|
+
if (!approver || !String(approver).startsWith('human:')) {
|
|
232
|
+
throw inputError(
|
|
233
|
+
'MEMORY_APPROVER_REQUIRED',
|
|
234
|
+
`Active memory entry "${id}" requires a human Director approver ("human:director").`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Global vs project precedence and explicit-override checks (§12.3)
|
|
240
|
+
const globalPath = getMemoryStorePath(rootDir, 'global', null);
|
|
241
|
+
const globalEntries = readStoreEntries(globalPath);
|
|
242
|
+
const activeGlobalEntries = globalEntries.filter((e) => e.status === 'active');
|
|
243
|
+
|
|
244
|
+
if (scope === 'project') {
|
|
245
|
+
if (overrides) {
|
|
246
|
+
const targetGlobal = activeGlobalEntries.find((g) => g.id === overrides);
|
|
247
|
+
if (!targetGlobal) {
|
|
248
|
+
throw inputError(
|
|
249
|
+
'MEMORY_OVERRIDE_INVALID',
|
|
250
|
+
`Project memory entry "${id}" overrides "${overrides}", which is not an active global entry.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
if (!reason || typeof reason !== 'string' || !reason.trim()) {
|
|
254
|
+
throw inputError(
|
|
255
|
+
'MEMORY_OVERRIDE_INVALID',
|
|
256
|
+
`Project memory entry "${id}" overriding global "${overrides}" requires a non-empty "reason".`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
if (!approver || !String(approver).startsWith('human:')) {
|
|
260
|
+
throw inputError(
|
|
261
|
+
'MEMORY_OVERRIDE_INVALID',
|
|
262
|
+
`Project memory entry "${id}" overriding global "${overrides}" requires approver "human:director".`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
} else if (rule_key) {
|
|
266
|
+
const conflictingGlobal = activeGlobalEntries.find(
|
|
267
|
+
(g) => g.rule_key && g.rule_key === rule_key && g.statement !== statement
|
|
268
|
+
);
|
|
269
|
+
if (conflictingGlobal) {
|
|
270
|
+
throw inputError(
|
|
271
|
+
'MEMORY_SILENT_CONTRADICTION',
|
|
272
|
+
`Project memory entry "${id}" contradicts active global rule "${conflictingGlobal.id}" on rule_key "${rule_key}" without setting overrides: "${conflictingGlobal.id}" (§12.3).`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const resolvedDraftedByModel =
|
|
279
|
+
drafted_by_model ||
|
|
280
|
+
(typeof author_model === 'object' ? author_model?.name : author_model) ||
|
|
281
|
+
(isModelAuthor ? author : null);
|
|
282
|
+
|
|
283
|
+
const normalizedEntry = {
|
|
284
|
+
id,
|
|
285
|
+
scope,
|
|
286
|
+
project: scope === 'global' ? null : project,
|
|
287
|
+
kind,
|
|
288
|
+
...(rule_key ? { rule_key } : {}),
|
|
289
|
+
status,
|
|
290
|
+
statement: statement.trim(),
|
|
291
|
+
evidence_refs: Array.isArray(evidence_refs) ? evidence_refs : [],
|
|
292
|
+
author,
|
|
293
|
+
...(resolvedDraftedByModel ? { drafted_by_model: resolvedDraftedByModel } : {}),
|
|
294
|
+
...(author_model ? { author_model } : {}),
|
|
295
|
+
approver: status === 'proposed' ? null : approver,
|
|
296
|
+
overrides: overrides || null,
|
|
297
|
+
superseded_by: superseded_by || null,
|
|
298
|
+
...(reason ? { reason } : {}),
|
|
299
|
+
...(promoted_from ? { promoted_from } : {}),
|
|
300
|
+
created_at,
|
|
301
|
+
approved_at: status === 'active' ? approved_at || created_at : null,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const storePath = getMemoryStorePath(rootDir, scope, project);
|
|
305
|
+
mkdirSync(dirname(storePath), { recursive: true });
|
|
306
|
+
appendFileSync(storePath, JSON.stringify(normalizedEntry) + '\n', 'utf-8');
|
|
307
|
+
return normalizedEntry;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Transition a `proposed` memory entry to `active` upon explicit Director review (§12.4).
|
|
312
|
+
*
|
|
313
|
+
* @param {Object} opts
|
|
314
|
+
* @param {string} opts.rootDir
|
|
315
|
+
* @param {'global'|'project'} opts.scope
|
|
316
|
+
* @param {string|null} [opts.project]
|
|
317
|
+
* @param {string} opts.id
|
|
318
|
+
* @param {string} opts.approver
|
|
319
|
+
* @param {string} [opts.reason]
|
|
320
|
+
* @param {string} [opts.approvedAt]
|
|
321
|
+
* @returns {Object}
|
|
322
|
+
*/
|
|
323
|
+
export function approveProposedMemoryEntry({
|
|
324
|
+
rootDir,
|
|
325
|
+
scope,
|
|
326
|
+
project = null,
|
|
327
|
+
id,
|
|
328
|
+
approver,
|
|
329
|
+
reason = null,
|
|
330
|
+
approvedAt = new Date().toISOString(),
|
|
331
|
+
}) {
|
|
332
|
+
if (!approver || !String(approver).startsWith('human:')) {
|
|
333
|
+
throw inputError(
|
|
334
|
+
'MEMORY_APPROVER_REQUIRED',
|
|
335
|
+
`Only a human Director ("human:director") may approve proposed memory entry "${id}".`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
const storePath = getMemoryStorePath(rootDir, scope, project);
|
|
339
|
+
const entries = readStoreEntries(storePath);
|
|
340
|
+
const idx = entries.findIndex((e) => e.id === id);
|
|
341
|
+
if (idx === -1) {
|
|
342
|
+
throw inputError('MEMORY_ENTRY_NOT_FOUND', `Memory entry "${id}" not found in ${scope} store.`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const updated = {
|
|
346
|
+
...entries[idx],
|
|
347
|
+
status: 'active',
|
|
348
|
+
approver,
|
|
349
|
+
...(reason ? { reason } : {}),
|
|
350
|
+
approved_at: approvedAt,
|
|
351
|
+
};
|
|
352
|
+
entries[idx] = updated;
|
|
353
|
+
writeStoreEntries(storePath, entries);
|
|
354
|
+
return updated;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Promote an active project memory entry to the global store with an explicit
|
|
359
|
+
* `promotion` audit record, keeping the project entry intact (§12.3).
|
|
360
|
+
*
|
|
361
|
+
* @param {Object} opts
|
|
362
|
+
* @param {string} opts.rootDir
|
|
363
|
+
* @param {string} opts.sourceProject
|
|
364
|
+
* @param {string} opts.sourceEntryId
|
|
365
|
+
* @param {string} opts.newGlobalId
|
|
366
|
+
* @param {string} opts.approver
|
|
367
|
+
* @param {string} opts.reason
|
|
368
|
+
* @param {string} [opts.createdAt]
|
|
369
|
+
* @returns {Object}
|
|
370
|
+
*/
|
|
371
|
+
export function promoteProjectMemoryToGlobal({
|
|
372
|
+
rootDir,
|
|
373
|
+
sourceProject,
|
|
374
|
+
sourceEntryId,
|
|
375
|
+
newGlobalId,
|
|
376
|
+
approver,
|
|
377
|
+
reason,
|
|
378
|
+
createdAt = new Date().toISOString(),
|
|
379
|
+
}) {
|
|
380
|
+
if (!approver || !String(approver).startsWith('human:')) {
|
|
381
|
+
throw inputError(
|
|
382
|
+
'MEMORY_APPROVER_REQUIRED',
|
|
383
|
+
'Promoting a project memory entry to global requires approver "human:director".'
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (!reason || typeof reason !== 'string' || !reason.trim()) {
|
|
387
|
+
throw inputError(
|
|
388
|
+
'MEMORY_PROMOTION_REASON_REQUIRED',
|
|
389
|
+
'Promoting a project memory entry to global requires a non-empty reason.'
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
const projectPath = getMemoryStorePath(rootDir, 'project', sourceProject);
|
|
393
|
+
const projectEntries = readStoreEntries(projectPath);
|
|
394
|
+
const sourceEntry = projectEntries.find((e) => e.id === sourceEntryId && e.status === 'active');
|
|
395
|
+
if (!sourceEntry) {
|
|
396
|
+
throw inputError(
|
|
397
|
+
'MEMORY_ENTRY_NOT_FOUND',
|
|
398
|
+
`Active project memory entry "${sourceEntryId}" not found in project "${sourceProject}".`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return appendMemoryEntry({
|
|
403
|
+
rootDir,
|
|
404
|
+
entry: {
|
|
405
|
+
id: newGlobalId,
|
|
406
|
+
scope: 'global',
|
|
407
|
+
project: null,
|
|
408
|
+
kind: 'promotion',
|
|
409
|
+
rule_key: sourceEntry.rule_key || null,
|
|
410
|
+
status: 'active',
|
|
411
|
+
statement: sourceEntry.statement,
|
|
412
|
+
evidence_refs: sourceEntry.evidence_refs,
|
|
413
|
+
author: sourceEntry.author,
|
|
414
|
+
...(sourceEntry.drafted_by_model ? { drafted_by_model: sourceEntry.drafted_by_model } : {}),
|
|
415
|
+
...(sourceEntry.author_model ? { author_model: sourceEntry.author_model } : {}),
|
|
416
|
+
approver,
|
|
417
|
+
overrides: null,
|
|
418
|
+
superseded_by: null,
|
|
419
|
+
reason: reason.trim(),
|
|
420
|
+
promoted_from: {
|
|
421
|
+
project: sourceProject,
|
|
422
|
+
entry_id: sourceEntryId,
|
|
423
|
+
},
|
|
424
|
+
created_at: createdAt,
|
|
425
|
+
approved_at: createdAt,
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Compute canonical `sha256:<64hex>` over a list of active memory entries.
|
|
432
|
+
*
|
|
433
|
+
* @param {Array<Object>} activeEntries
|
|
434
|
+
* @returns {string}
|
|
435
|
+
*/
|
|
436
|
+
export function computeStoreSha256(activeEntries) {
|
|
437
|
+
const sorted = [...activeEntries].sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
438
|
+
return sha256String(canonicalize(sorted));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Load effective active memory for a project (§12.3):
|
|
443
|
+
* 1. Reads ONLY `memory/global/entries.jsonl` and `projects/<project>/memory/entries.jsonl` (no cross-talk).
|
|
444
|
+
* 2. Filters to `status === "active"` (`proposed`, `superseded`, `rejected` never enter prompts).
|
|
445
|
+
* 3. Applies explicit project `overrides` against global entries.
|
|
446
|
+
* 4. Returns global active entries first, followed by project active entries.
|
|
447
|
+
*
|
|
448
|
+
* @param {Object} opts
|
|
449
|
+
* @param {string} opts.rootDir
|
|
450
|
+
* @param {string} opts.project
|
|
451
|
+
* @param {Array<string>} [opts.extraProjectStores] — forbidden cross-project stores (throws if non-empty)
|
|
452
|
+
* @returns {Object}
|
|
453
|
+
*/
|
|
454
|
+
export function loadEffectiveMemory({ rootDir, project, extraProjectStores = [] }) {
|
|
455
|
+
validateProjectSlug(project);
|
|
456
|
+
if (Array.isArray(extraProjectStores) && extraProjectStores.length > 0) {
|
|
457
|
+
const illegal = extraProjectStores.filter((p) => p !== project);
|
|
458
|
+
if (illegal.length > 0) {
|
|
459
|
+
throw inputError(
|
|
460
|
+
'MEMORY_CROSS_TALK_FORBIDDEN',
|
|
461
|
+
`Cross-project memory load forbidden: project "${project}" attempted to read [${illegal.join(', ')}] (§12.3).`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const globalPath = getMemoryStorePath(rootDir, 'global', null);
|
|
467
|
+
const projectPath = getMemoryStorePath(rootDir, 'project', project);
|
|
468
|
+
|
|
469
|
+
const globalRaw = readStoreEntries(globalPath);
|
|
470
|
+
const projectRaw = readStoreEntries(projectPath);
|
|
471
|
+
|
|
472
|
+
const globalActiveAll = globalRaw.filter((e) => e.status === 'active' && !e.superseded_by);
|
|
473
|
+
const projectActive = projectRaw.filter((e) => e.status === 'active' && !e.superseded_by);
|
|
474
|
+
|
|
475
|
+
const overriddenGlobalIds = new Set(
|
|
476
|
+
projectActive.map((e) => e.overrides).filter((id) => typeof id === 'string' && id.length > 0)
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
const effectiveGlobalActive = globalActiveAll.filter((g) => !overriddenGlobalIds.has(g.id));
|
|
480
|
+
const activeEntries = [...effectiveGlobalActive, ...projectActive];
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
project,
|
|
484
|
+
memory_sha_global: computeStoreSha256(globalActiveAll),
|
|
485
|
+
memory_sha_project: computeStoreSha256(projectActive),
|
|
486
|
+
globalActive: globalActiveAll,
|
|
487
|
+
projectActive,
|
|
488
|
+
activeEntries,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Write `memory-snapshot.json` for a Lane 2 window (§7, §12.5).
|
|
494
|
+
*
|
|
495
|
+
* @param {Object} opts
|
|
496
|
+
* @param {string} opts.rootDir
|
|
497
|
+
* @param {string} opts.project
|
|
498
|
+
* @param {string} opts.windowId
|
|
499
|
+
* @param {string} opts.outPath
|
|
500
|
+
* @param {string} [opts.createdAt]
|
|
501
|
+
* @returns {Object}
|
|
502
|
+
*/
|
|
503
|
+
export function writeMemorySnapshot({
|
|
504
|
+
rootDir,
|
|
505
|
+
project,
|
|
506
|
+
windowId,
|
|
507
|
+
outPath,
|
|
508
|
+
createdAt = new Date().toISOString(),
|
|
509
|
+
}) {
|
|
510
|
+
const loaded = loadEffectiveMemory({ rootDir, project });
|
|
511
|
+
const snapshot = {
|
|
512
|
+
window_id: windowId,
|
|
513
|
+
project,
|
|
514
|
+
memory_sha_global: loaded.memory_sha_global,
|
|
515
|
+
memory_sha_project: loaded.memory_sha_project,
|
|
516
|
+
global_active_ids: loaded.globalActive.map((e) => e.id),
|
|
517
|
+
project_active_ids: loaded.projectActive.map((e) => e.id),
|
|
518
|
+
active_entries: loaded.activeEntries,
|
|
519
|
+
created_at: createdAt,
|
|
520
|
+
};
|
|
521
|
+
if (outPath) {
|
|
522
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
523
|
+
writeFileSync(outPath, JSON.stringify(snapshot, null, 2) + '\n', 'utf-8');
|
|
524
|
+
}
|
|
525
|
+
return snapshot;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Diff two memory snapshots (`snapA` vs `snapB`) by active entry IDs and SHAs (§12.5).
|
|
530
|
+
*
|
|
531
|
+
* @param {Object} snapA
|
|
532
|
+
* @param {Object} snapB
|
|
533
|
+
* @returns {Object}
|
|
534
|
+
*/
|
|
535
|
+
export function diffMemorySnapshots(snapA, snapB) {
|
|
536
|
+
const mapA = new Map((snapA.active_entries || []).map((e) => [e.id, e]));
|
|
537
|
+
const mapB = new Map((snapB.active_entries || []).map((e) => [e.id, e]));
|
|
538
|
+
|
|
539
|
+
const added = [];
|
|
540
|
+
const removed = [];
|
|
541
|
+
const changed = [];
|
|
542
|
+
|
|
543
|
+
for (const [id, entryB] of mapB.entries()) {
|
|
544
|
+
if (!mapA.has(id)) {
|
|
545
|
+
added.push(id);
|
|
546
|
+
} else if (canonicalize(mapA.get(id)) !== canonicalize(entryB)) {
|
|
547
|
+
changed.push(id);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
for (const id of mapA.keys()) {
|
|
551
|
+
if (!mapB.has(id)) {
|
|
552
|
+
removed.push(id);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
globalChanged: snapA.memory_sha_global !== snapB.memory_sha_global,
|
|
558
|
+
projectChanged: snapA.memory_sha_project !== snapB.memory_sha_project,
|
|
559
|
+
added: added.sort(),
|
|
560
|
+
removed: removed.sort(),
|
|
561
|
+
changed: changed.sort(),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Append a structured vision verdict row to `verdicts.jsonl` carrying both
|
|
567
|
+
* `memory_sha_global` and `memory_sha_project`, `canon_sha`, `verifier_model`,
|
|
568
|
+
* `sampling`, `verdict`, and closed-list `failure_modes` (§7, §11.4, §12.5).
|
|
569
|
+
*
|
|
570
|
+
* @param {Object} opts
|
|
571
|
+
* @returns {Object}
|
|
572
|
+
*/
|
|
573
|
+
export function recordVisionVerdict({
|
|
574
|
+
verdictsPath,
|
|
575
|
+
windowId,
|
|
576
|
+
imageSha256,
|
|
577
|
+
canonSha,
|
|
578
|
+
memorySnapshot,
|
|
579
|
+
verifierModel,
|
|
580
|
+
authorModel = null,
|
|
581
|
+
sampling = { temperature: 0, top_p: 1.0, seed: 0 },
|
|
582
|
+
verdict,
|
|
583
|
+
failureModes = [],
|
|
584
|
+
allowedFailureModes = ALLOWED_FAILURE_MODES,
|
|
585
|
+
notes = '',
|
|
586
|
+
createdAt = new Date().toISOString(),
|
|
587
|
+
}) {
|
|
588
|
+
const authorRaw = typeof authorModel === 'object' ? authorModel?.name : authorModel;
|
|
589
|
+
if (typeof authorRaw !== 'string' || !authorRaw.trim()) {
|
|
590
|
+
throw inputError(
|
|
591
|
+
'VERDICT_AUTHOR_MODEL_REQUIRED',
|
|
592
|
+
'recordVisionVerdict requires a non-empty authorModel so self-verification can be checked.'
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
assertNoSelfVerification({
|
|
596
|
+
authorModel,
|
|
597
|
+
verifierModel,
|
|
598
|
+
context: `verdict for ${imageSha256}`,
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
const normImageSha = normalizeSha256(imageSha256);
|
|
602
|
+
const normCanonSha = normalizeSha256(canonSha);
|
|
603
|
+
if (!normImageSha || !normCanonSha) {
|
|
604
|
+
throw inputError('VERDICT_SHA_INVALID', 'recordVisionVerdict requires valid 64-hex imageSha256 and canonSha.');
|
|
605
|
+
}
|
|
606
|
+
if (!memorySnapshot?.memory_sha_global || !memorySnapshot?.memory_sha_project) {
|
|
607
|
+
throw inputError(
|
|
608
|
+
'VERDICT_MEMORY_SHA_MISSING',
|
|
609
|
+
'recordVisionVerdict requires memorySnapshot with memory_sha_global and memory_sha_project.'
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
if (!ALLOWED_VERDICTS.includes(verdict)) {
|
|
613
|
+
throw inputError(
|
|
614
|
+
'VERDICT_INVALID',
|
|
615
|
+
`Verdict must be one of [${ALLOWED_VERDICTS.join(', ')}], got "${verdict}".`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const allowedSet = new Set(allowedFailureModes);
|
|
620
|
+
for (const mode of failureModes) {
|
|
621
|
+
if (!allowedSet.has(mode)) {
|
|
622
|
+
throw inputError(
|
|
623
|
+
'VERDICT_FAILURE_MODE_INVALID',
|
|
624
|
+
`Failure mode "${mode}" is not in the closed failure-mode list [${[...allowedSet].join(', ')}].`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (!verifierModel?.name || !normalizeSha256(verifierModel?.digest) || verifierModel?.licence_id !== 'Apache-2.0') {
|
|
630
|
+
throw inputError(
|
|
631
|
+
'VERDICT_MODEL_PIN_INVALID',
|
|
632
|
+
'verifierModel must include name, 64-hex digest, and licence_id: "Apache-2.0".'
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const row = {
|
|
637
|
+
window_id: windowId,
|
|
638
|
+
image_sha256: normImageSha,
|
|
639
|
+
canon_sha: normCanonSha,
|
|
640
|
+
memory_sha_global: memorySnapshot.memory_sha_global,
|
|
641
|
+
memory_sha_project: memorySnapshot.memory_sha_project,
|
|
642
|
+
verifier_model: {
|
|
643
|
+
name: verifierModel.name,
|
|
644
|
+
digest: normalizeSha256(verifierModel.digest),
|
|
645
|
+
licence_id: verifierModel.licence_id,
|
|
646
|
+
},
|
|
647
|
+
sampling: {
|
|
648
|
+
temperature: sampling.temperature ?? 0,
|
|
649
|
+
top_p: sampling.top_p ?? 1.0,
|
|
650
|
+
seed: sampling.seed ?? 0,
|
|
651
|
+
},
|
|
652
|
+
verdict,
|
|
653
|
+
failure_modes: [...failureModes],
|
|
654
|
+
notes: String(notes || ''),
|
|
655
|
+
created_at: createdAt,
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
if (verdictsPath) {
|
|
659
|
+
mkdirSync(dirname(verdictsPath), { recursive: true });
|
|
660
|
+
appendFileSync(verdictsPath, JSON.stringify(row) + '\n', 'utf-8');
|
|
661
|
+
}
|
|
662
|
+
return row;
|
|
663
|
+
}
|