@mcptoolshop/style-dataset-lab 3.3.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 +60 -0
- package/README.es.md +117 -55
- package/README.fr.md +125 -65
- package/README.hi.md +117 -68
- package/README.it.md +119 -58
- package/README.ja.md +124 -74
- package/README.md +73 -7
- package/README.pt-BR.md +128 -63
- package/README.zh.md +116 -65
- package/bin/sdlab.js +100 -4
- package/lib/adapters/ai-toolkit.js +224 -35
- package/lib/adapters/comfyui-runner.js +79 -26
- package/lib/adapters/comfyui-workflows.js +94 -11
- package/lib/adapters/diffusers-lora.js +37 -5
- package/lib/adapters/generic-image-caption.js +43 -5
- package/lib/args.js +16 -1
- package/lib/asset-ingest.js +575 -0
- package/lib/asset-masks.js +323 -0
- package/lib/asset-source.js +1176 -0
- package/lib/batch-modes.js +29 -8
- package/lib/batch-runs.js +29 -1
- package/lib/batch-sheet-render.js +13 -3
- package/lib/brief-compiler.js +19 -0
- package/lib/candidate-sheet.js +327 -0
- package/lib/canon-author.js +350 -0
- package/lib/canon-build/build.js +146 -93
- package/lib/canon-review-gate.js +205 -0
- package/lib/captions.js +57 -0
- package/lib/card.js +74 -2
- package/lib/colab-launcher-guards.js +285 -0
- package/lib/comfyui.js +116 -18
- package/lib/config.js +116 -12
- package/lib/critique-engine.js +36 -2
- package/lib/eligibility.js +121 -9
- package/lib/eval-pack.js +14 -14
- package/lib/eval-runs.js +33 -0
- package/lib/export.js +122 -34
- package/lib/freeze-gate.js +16 -3
- package/lib/freeze-stamp.js +28 -3
- package/lib/generated-provenance.js +194 -6
- package/lib/implementation-packs.js +95 -17
- package/lib/ingest.js +396 -0
- package/lib/judgment-provenance.js +220 -0
- package/lib/lane1-render.js +668 -0
- package/lib/measure.js +422 -0
- package/lib/memory-store.js +663 -0
- package/lib/model-licences.js +305 -0
- package/lib/npy-meta.js +130 -0
- package/lib/paths.js +196 -7
- package/lib/png-meta.js +512 -0
- package/lib/records.js +54 -8
- package/lib/reingest-selected.js +113 -17
- package/lib/reingest.js +27 -3
- package/lib/run-manifest.js +159 -25
- package/lib/run-summary.js +12 -6
- package/lib/runtime-runs.js +45 -4
- package/lib/snapshot.js +114 -15
- package/lib/split.js +364 -48
- package/lib/train-runs.js +214 -0
- package/lib/training-manifests.js +114 -21
- package/lib/training-packages.js +147 -15
- package/lib/verifier.js +3 -0
- package/lib/verify-chain.js +367 -0
- package/lib/workflow-profiles.js +24 -3
- package/package.json +4 -3
- package/runtime/comfyui/txt2img-qwen.json +38 -0
- package/runtime/comfyui/txt2img-standard.json +21 -0
- package/scripts/_resume.js +62 -0
- package/scripts/_shared-args.js +44 -0
- package/scripts/asset-ingest.js +160 -0
- package/scripts/brief-compile.js +2 -1
- package/scripts/brief-show.js +2 -1
- package/scripts/canon-bind.js +2 -2
- package/scripts/canon-build.js +27 -20
- package/scripts/canon-draft.js +82 -0
- package/scripts/canon-drift.js +16 -7
- package/scripts/canon-freeze-status.js +14 -6
- package/scripts/canon-freeze.js +31 -11
- package/scripts/canon-unfreeze.js +17 -9
- package/scripts/card.js +2 -2
- 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/compare.js +2 -2
- package/scripts/critique.js +16 -1
- package/scripts/curate.js +2 -2
- package/scripts/doctor.js +182 -55
- package/scripts/eligibility.js +42 -2
- package/scripts/eval-pack.js +2 -2
- package/scripts/eval-run.js +2 -2
- package/scripts/export.js +2 -2
- package/scripts/generate-controlnet.js +45 -25
- package/scripts/generate-identity.js +53 -23
- package/scripts/generate-ipadapter.js +44 -23
- package/scripts/generate.js +86 -17
- package/scripts/implementation-pack.js +2 -2
- package/scripts/ingest.js +145 -0
- package/scripts/init.js +73 -13
- package/scripts/measure.js +170 -0
- package/scripts/measure_image.py +458 -0
- package/scripts/migrate-records.js +142 -0
- package/scripts/painterly-test.js +4 -3
- package/scripts/painterly.js +7 -3
- package/scripts/qwen_generate.py +138 -20
- package/scripts/reingest.js +2 -2
- package/scripts/sheet.js +142 -0
- package/scripts/snapshot.js +2 -2
- package/scripts/split.js +2 -2
- package/scripts/training-manifest.js +2 -2
- package/scripts/training-package.js +2 -2
- package/scripts/training-profile.js +2 -2
- package/scripts/verify.js +67 -0
- package/scripts/workflow-list.js +2 -1
- package/scripts/workflow-show.js +2 -1
|
@@ -3,12 +3,117 @@
|
|
|
3
3
|
* attached to records created from selected run/batch outputs.
|
|
4
4
|
*
|
|
5
5
|
* Normalizes source context (run vs batch) into a single shape
|
|
6
|
-
* that traces back through brief → workflow → seed → config
|
|
6
|
+
* that traces back through brief → workflow → seed → config,
|
|
7
|
+
* plus G4 (model licences), G5 (owner + CC-BY-4.0), and G6 (canon link).
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import { readFileSync, existsSync } from 'node:fs';
|
|
10
11
|
import { join } from 'node:path';
|
|
11
12
|
import { createHash } from 'node:crypto';
|
|
13
|
+
import { warn } from './log.js';
|
|
14
|
+
import { STUDIO_OWNER, STUDIO_LICENCE } from './records.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve `{ canon_sha, entry_id, entry_sha256 }` from explicit metadata or
|
|
18
|
+
* the project's `canon-build/<canon_sha>/manifest.json` (`per_entity_hashes`).
|
|
19
|
+
*/
|
|
20
|
+
export function resolveCanonLink(
|
|
21
|
+
projectRoot,
|
|
22
|
+
{
|
|
23
|
+
canonSha = null,
|
|
24
|
+
entryId = null,
|
|
25
|
+
entrySha256 = null,
|
|
26
|
+
subjectId = null,
|
|
27
|
+
runManifest = null,
|
|
28
|
+
batchManifest = null,
|
|
29
|
+
item = null,
|
|
30
|
+
brief = null,
|
|
31
|
+
} = {},
|
|
32
|
+
) {
|
|
33
|
+
let resolvedCanonSha =
|
|
34
|
+
canonSha ||
|
|
35
|
+
item?.canon_sha ||
|
|
36
|
+
runManifest?.canon_sha ||
|
|
37
|
+
batchManifest?.canon_sha ||
|
|
38
|
+
brief?.canon_sha ||
|
|
39
|
+
null;
|
|
40
|
+
|
|
41
|
+
if (!resolvedCanonSha && projectRoot) {
|
|
42
|
+
const currentPath = join(projectRoot, 'canon-build', 'CURRENT');
|
|
43
|
+
if (existsSync(currentPath)) {
|
|
44
|
+
try {
|
|
45
|
+
const raw = readFileSync(currentPath, 'utf-8').trim();
|
|
46
|
+
if (raw) resolvedCanonSha = raw;
|
|
47
|
+
} catch {
|
|
48
|
+
// best-effort read of CURRENT pointer
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let resolvedEntryId =
|
|
54
|
+
entryId ||
|
|
55
|
+
item?.entry_id ||
|
|
56
|
+
runManifest?.entry_id ||
|
|
57
|
+
batchManifest?.entry_id ||
|
|
58
|
+
brief?.entry_id ||
|
|
59
|
+
subjectId ||
|
|
60
|
+
item?.subject_id ||
|
|
61
|
+
runManifest?.subject_id ||
|
|
62
|
+
batchManifest?.subject_id ||
|
|
63
|
+
brief?.subject_id ||
|
|
64
|
+
null;
|
|
65
|
+
|
|
66
|
+
let resolvedEntrySha =
|
|
67
|
+
entrySha256 ||
|
|
68
|
+
item?.entry_sha256 ||
|
|
69
|
+
runManifest?.entry_sha256 ||
|
|
70
|
+
batchManifest?.entry_sha256 ||
|
|
71
|
+
brief?.entry_sha256 ||
|
|
72
|
+
null;
|
|
73
|
+
|
|
74
|
+
if (resolvedCanonSha && projectRoot) {
|
|
75
|
+
const dirName = String(resolvedCanonSha).replace(/:/g, '-');
|
|
76
|
+
const manifestPath = existsSync(join(projectRoot, 'canon-build', String(resolvedCanonSha), 'manifest.json'))
|
|
77
|
+
? join(projectRoot, 'canon-build', String(resolvedCanonSha), 'manifest.json')
|
|
78
|
+
: join(projectRoot, 'canon-build', dirName, 'manifest.json');
|
|
79
|
+
if (existsSync(manifestPath)) {
|
|
80
|
+
try {
|
|
81
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
82
|
+
const hashes = manifest.per_entity_hashes || {};
|
|
83
|
+
if (resolvedEntryId) {
|
|
84
|
+
if (hashes[resolvedEntryId]) {
|
|
85
|
+
resolvedEntrySha = resolvedEntrySha || hashes[resolvedEntryId];
|
|
86
|
+
} else {
|
|
87
|
+
const suffix = `/${resolvedEntryId}`;
|
|
88
|
+
const matchKey = Object.keys(hashes).find((k) => k.endsWith(suffix));
|
|
89
|
+
if (matchKey) {
|
|
90
|
+
resolvedEntryId = matchKey;
|
|
91
|
+
resolvedEntrySha = resolvedEntrySha || hashes[matchKey];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
const keys = Object.keys(hashes);
|
|
96
|
+
if (keys.length === 1) {
|
|
97
|
+
resolvedEntryId = keys[0];
|
|
98
|
+
resolvedEntrySha = resolvedEntrySha || hashes[keys[0]];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// best-effort manifest lookup
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!resolvedCanonSha && !resolvedEntryId && !resolvedEntrySha) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
canon_sha: resolvedCanonSha,
|
|
113
|
+
entry_id: resolvedEntryId,
|
|
114
|
+
entry_sha256: resolvedEntrySha,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
12
117
|
|
|
13
118
|
/**
|
|
14
119
|
* Build a generation provenance payload for a selected output.
|
|
@@ -20,6 +125,9 @@ import { createHash } from 'node:crypto';
|
|
|
20
125
|
* @param {Object} [opts.batchManifest] — batch manifest if source is batch
|
|
21
126
|
* @param {Object} opts.item — the selection item (slot_or_output, filename, seed)
|
|
22
127
|
* @param {string} opts.projectRoot
|
|
128
|
+
* @param {string} [opts.canonSha]
|
|
129
|
+
* @param {string} [opts.entryId]
|
|
130
|
+
* @param {string} [opts.entrySha256]
|
|
23
131
|
* @returns {Object} generation_provenance block
|
|
24
132
|
*/
|
|
25
133
|
export function buildGeneratedProvenance({
|
|
@@ -29,8 +137,13 @@ export function buildGeneratedProvenance({
|
|
|
29
137
|
batchManifest,
|
|
30
138
|
item,
|
|
31
139
|
projectRoot,
|
|
140
|
+
canonSha = null,
|
|
141
|
+
entryId = null,
|
|
142
|
+
entrySha256 = null,
|
|
32
143
|
}) {
|
|
33
144
|
const prov = {
|
|
145
|
+
owner: STUDIO_OWNER,
|
|
146
|
+
licence: STUDIO_LICENCE,
|
|
34
147
|
source_type: sourceType,
|
|
35
148
|
source_id: sourceId,
|
|
36
149
|
run_id: runManifest.run_id,
|
|
@@ -44,12 +157,55 @@ export function buildGeneratedProvenance({
|
|
|
44
157
|
prov.batch_id = batchManifest.batch_id;
|
|
45
158
|
}
|
|
46
159
|
|
|
160
|
+
const matchedOutput = runManifest.outputs
|
|
161
|
+
? runManifest.outputs.find((o) => o.filename === item.filename)
|
|
162
|
+
: null;
|
|
163
|
+
|
|
47
164
|
// Seed from the item or from the run manifest outputs
|
|
48
165
|
if (item.seed !== undefined) {
|
|
49
166
|
prov.seed = item.seed;
|
|
50
|
-
} else if (
|
|
51
|
-
|
|
52
|
-
|
|
167
|
+
} else if (matchedOutput?.seed !== undefined) {
|
|
168
|
+
prov.seed = matchedOutput.seed;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Prompt and prompt_sha256 when available on the item or output
|
|
172
|
+
const promptText = item.prompt ?? matchedOutput?.prompt ?? null;
|
|
173
|
+
if (typeof promptText === 'string' && promptText.length > 0) {
|
|
174
|
+
prov.prompt = promptText;
|
|
175
|
+
prov.prompt_sha256 = 'sha256:' + createHash('sha256').update(promptText).digest('hex');
|
|
176
|
+
} else if (item.prompt_sha256 || matchedOutput?.prompt_sha256) {
|
|
177
|
+
prov.prompt_sha256 = item.prompt_sha256 || matchedOutput.prompt_sha256;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Comfy workflow sha and model licences from run manifest pinning (G4, G9)
|
|
181
|
+
const wfSha = runManifest.pinning?.comfy_workflow_sha ?? runManifest.comfy_workflow_sha ?? null;
|
|
182
|
+
if (wfSha) {
|
|
183
|
+
prov.comfy_workflow_sha = wfSha;
|
|
184
|
+
}
|
|
185
|
+
if (runManifest.pinning?.models || runManifest.pinning?.loras) {
|
|
186
|
+
prov.model_licences = {
|
|
187
|
+
cc_by_4_eligible: runManifest.pinning.cc_by_4_eligible ?? null,
|
|
188
|
+
models: Object.fromEntries(
|
|
189
|
+
Object.entries(runManifest.pinning.models || {}).map(([role, m]) => [
|
|
190
|
+
role,
|
|
191
|
+
{
|
|
192
|
+
name: m.name,
|
|
193
|
+
sha256: m.sha256 ?? null,
|
|
194
|
+
licence_id: m.licence_id ?? m.licence?.spdx_id ?? null,
|
|
195
|
+
licence_url: m.licence_url ?? m.licence?.url ?? null,
|
|
196
|
+
date_read: m.date_read ?? m.licence?.verified_at ?? null,
|
|
197
|
+
},
|
|
198
|
+
]),
|
|
199
|
+
),
|
|
200
|
+
loras: (runManifest.pinning.loras || []).map((l) => ({
|
|
201
|
+
name: l.name,
|
|
202
|
+
weight: l.weight ?? 1.0,
|
|
203
|
+
sha256: l.sha256 ?? null,
|
|
204
|
+
licence_id: l.licence_id ?? l.licence?.spdx_id ?? null,
|
|
205
|
+
licence_url: l.licence_url ?? l.licence?.url ?? null,
|
|
206
|
+
date_read: l.date_read ?? l.licence?.verified_at ?? null,
|
|
207
|
+
})),
|
|
208
|
+
};
|
|
53
209
|
}
|
|
54
210
|
|
|
55
211
|
// Adapter target from run manifest
|
|
@@ -57,21 +213,53 @@ export function buildGeneratedProvenance({
|
|
|
57
213
|
prov.adapter_target = runManifest.adapter_target;
|
|
58
214
|
}
|
|
59
215
|
|
|
216
|
+
let parsedBrief = null;
|
|
60
217
|
// Subject from run brief or batch manifest
|
|
61
218
|
if (batchManifest?.subject_id) {
|
|
62
219
|
prov.subject_id = batchManifest.subject_id;
|
|
63
|
-
} else {
|
|
220
|
+
} else if (projectRoot && runManifest.brief_id) {
|
|
64
221
|
// Try to read subject from brief
|
|
65
222
|
const briefPath = join(projectRoot, 'briefs', `${runManifest.brief_id}.json`);
|
|
66
223
|
if (existsSync(briefPath)) {
|
|
67
224
|
try {
|
|
68
225
|
const brief = JSON.parse(readFileSync(briefPath, 'utf-8'));
|
|
226
|
+
parsedBrief = brief;
|
|
69
227
|
if (brief.subject_id) prov.subject_id = brief.subject_id;
|
|
70
228
|
if (brief.parent_brief_id) prov.parent_brief_id = brief.parent_brief_id;
|
|
71
|
-
} catch {
|
|
229
|
+
} catch (err) {
|
|
230
|
+
// B08: the brief exists but couldn't be read/parsed (e.g. a
|
|
231
|
+
// concurrent mid-write) — lineage is genuinely UNKNOWN here, not
|
|
232
|
+
// merely absent. Surface it instead of silently shipping
|
|
233
|
+
// provenance with no lineage and no signal that anything failed.
|
|
234
|
+
warn(`generated-provenance: brief ${briefPath} could not be read (${err.message}) — lineage_incomplete`);
|
|
235
|
+
prov.lineage_incomplete = true;
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
// B08: no brief on disk at all for this run — the same "genuinely
|
|
239
|
+
// unknown" lineage state as an unreadable brief, not a normal
|
|
240
|
+
// "this brief simply has no subject_id" case (that one is NOT
|
|
241
|
+
// flagged — see the brief-read-fine-but-no-subject_id branch above).
|
|
242
|
+
warn(`generated-provenance: brief not found at ${briefPath} — lineage_incomplete`);
|
|
243
|
+
prov.lineage_incomplete = true;
|
|
72
244
|
}
|
|
73
245
|
}
|
|
74
246
|
|
|
247
|
+
const canonLink = resolveCanonLink(projectRoot, {
|
|
248
|
+
canonSha,
|
|
249
|
+
entryId,
|
|
250
|
+
entrySha256,
|
|
251
|
+
subjectId: prov.subject_id,
|
|
252
|
+
runManifest,
|
|
253
|
+
batchManifest,
|
|
254
|
+
item,
|
|
255
|
+
brief: parsedBrief,
|
|
256
|
+
});
|
|
257
|
+
if (canonLink) {
|
|
258
|
+
prov.canon_sha = canonLink.canon_sha;
|
|
259
|
+
prov.entry_id = canonLink.entry_id;
|
|
260
|
+
prov.entry_sha256 = canonLink.entry_sha256;
|
|
261
|
+
}
|
|
262
|
+
|
|
75
263
|
// Config fingerprint — hash the run manifest for reproducibility tracing
|
|
76
264
|
prov.config_fingerprint = createHash('sha256')
|
|
77
265
|
.update(JSON.stringify({
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Every implementation pack is tied to a training manifest.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { readFile, writeFile,
|
|
11
|
+
import { readFile, writeFile, readdir } from 'node:fs/promises';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { existsSync } from 'node:fs';
|
|
14
14
|
import { randomBytes } from 'node:crypto';
|
|
@@ -17,8 +17,8 @@ import { loadTrainingProfile } from './training-profiles.js';
|
|
|
17
17
|
import { loadProjectConfig, detectLane, detectGroup } from './config.js';
|
|
18
18
|
import { loadSplitPartition } from './split.js';
|
|
19
19
|
import { loadRecord } from './records.js';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { SCHEMA_VERSION, checkManifestVersion, claimIdDir } from './snapshot.js';
|
|
21
|
+
import { warn } from './log.js';
|
|
22
22
|
|
|
23
23
|
function generateImplId() {
|
|
24
24
|
const now = new Date();
|
|
@@ -46,13 +46,27 @@ export async function buildImplementationPack(projectRoot, manifestId) {
|
|
|
46
46
|
// Also load some train records for broader coverage
|
|
47
47
|
const trainEntries = await loadSplitPartition(projectRoot, manifest.source_split_id, 'train');
|
|
48
48
|
|
|
49
|
+
// F3: a record the split promised (via source_split_id's train/test
|
|
50
|
+
// partitions) whose file was later deleted from records/ must not
|
|
51
|
+
// silently drop out of the implementation pack with no counter, no
|
|
52
|
+
// warning, no id recorded. loadRecord() returns null on ENOENT
|
|
53
|
+
// (lib/records.js); previously `if (!record) continue` was the entire
|
|
54
|
+
// handling at both loops below. WARN + ledger (not hard-fail): an
|
|
55
|
+
// implementation pack is an illustrative/example artifact, not the
|
|
56
|
+
// Law-4 reproducibility-guaranteeing one (export.js and
|
|
57
|
+
// training-packages.js are, and hard-fail on the identical situation).
|
|
58
|
+
const missingRecords = [];
|
|
59
|
+
|
|
49
60
|
// Build prompt examples — per-lane best records
|
|
50
61
|
const promptExamples = [];
|
|
51
62
|
const laneExamples = new Map();
|
|
52
63
|
|
|
53
64
|
for (const entry of [...testEntries, ...trainEntries.slice(0, 50)]) {
|
|
54
65
|
const record = await loadRecord(recordsDir, entry.record_id);
|
|
55
|
-
if (!record)
|
|
66
|
+
if (!record) {
|
|
67
|
+
missingRecords.push({ record_id: entry.record_id, stage: 'implementation-pack', source: 'prompt-examples' });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
56
70
|
if (record.judgment?.status !== 'approved') continue;
|
|
57
71
|
|
|
58
72
|
const prompt = record.provenance?.prompt || '';
|
|
@@ -85,10 +99,26 @@ export async function buildImplementationPack(projectRoot, manifestId) {
|
|
|
85
99
|
const knownFailures = [];
|
|
86
100
|
const allFiles = (await readdir(recordsDir)).filter(f => f.endsWith('.json')).sort();
|
|
87
101
|
let failCount = 0;
|
|
102
|
+
// F4: adopt the DB-003 pattern already proven in snapshot.js's
|
|
103
|
+
// createSnapshot (and mirrored in eval-pack.js's buildEvalPack) — this
|
|
104
|
+
// loop was the one place left in the codebase still doing a bare
|
|
105
|
+
// JSON.parse per record file with no try/catch. One truncated/corrupt
|
|
106
|
+
// record file threw a filename-free SyntaxError here and discarded the
|
|
107
|
+
// prompt-examples work (the loop above) and the subject-continuity work
|
|
108
|
+
// (the loop below) this function had already computed, since every
|
|
109
|
+
// artifact is written to disk only at the very end of
|
|
110
|
+
// buildImplementationPack — skip, count, warn, continue instead.
|
|
111
|
+
const unreadable = [];
|
|
88
112
|
|
|
89
113
|
for (const file of allFiles) {
|
|
90
114
|
if (failCount >= 10) break;
|
|
91
|
-
|
|
115
|
+
let record;
|
|
116
|
+
try {
|
|
117
|
+
record = JSON.parse(await readFile(join(recordsDir, file), 'utf-8'));
|
|
118
|
+
} catch (err) {
|
|
119
|
+
unreadable.push({ record_id: file.replace(/\.json$/, ''), file, error: err.message });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
92
122
|
if (record.judgment?.status !== 'rejected') continue;
|
|
93
123
|
|
|
94
124
|
const prompt = record.provenance?.prompt || '';
|
|
@@ -107,6 +137,15 @@ export async function buildImplementationPack(projectRoot, manifestId) {
|
|
|
107
137
|
});
|
|
108
138
|
failCount++;
|
|
109
139
|
}
|
|
140
|
+
// F4: surface unreadable records loudly (DB-003 parity) — same shape,
|
|
141
|
+
// same warn() visibility as snapshot.js's createSnapshot.
|
|
142
|
+
if (unreadable.length > 0) {
|
|
143
|
+
const sample = unreadable.slice(0, 5).map(u => u.file).join(', ');
|
|
144
|
+
warn(
|
|
145
|
+
`implementation-pack: ${unreadable.length} record file(s) were unreadable and skipped` +
|
|
146
|
+
(unreadable.length > 5 ? ` (first 5: ${sample}, ...)` : ` (${sample})`),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
110
149
|
|
|
111
150
|
// Build subject continuity examples
|
|
112
151
|
const subjectExamples = [];
|
|
@@ -114,12 +153,33 @@ export async function buildImplementationPack(projectRoot, manifestId) {
|
|
|
114
153
|
|
|
115
154
|
for (const entry of [...testEntries, ...trainEntries]) {
|
|
116
155
|
const record = await loadRecord(recordsDir, entry.record_id);
|
|
117
|
-
|
|
156
|
+
// F3: keep "record file is missing" (a data-integrity problem worth a
|
|
157
|
+
// warning) distinct from "record loaded fine but simply has no
|
|
158
|
+
// identity.subject_name" (the normal, expected case for most records —
|
|
159
|
+
// not a missing-record situation). The original combined
|
|
160
|
+
// `!record?.identity?.subject_name` check could not tell these apart.
|
|
161
|
+
if (!record) {
|
|
162
|
+
missingRecords.push({ record_id: entry.record_id, stage: 'implementation-pack', source: 'subject-continuity' });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (!record.identity?.subject_name) continue;
|
|
118
166
|
const name = record.identity.subject_name;
|
|
119
167
|
if (!bySubject.has(name)) bySubject.set(name, []);
|
|
120
168
|
bySubject.get(name).push(record);
|
|
121
169
|
}
|
|
122
170
|
|
|
171
|
+
// F3: warn once for both loops combined — the ledger records which
|
|
172
|
+
// loop(s) actually hit the miss via `source`, so the summary line stays
|
|
173
|
+
// useful even though it's a single warning for the whole pack build.
|
|
174
|
+
if (missingRecords.length > 0) {
|
|
175
|
+
const sample = missingRecords.slice(0, 10).map(m => `${m.record_id} (${m.source})`);
|
|
176
|
+
warn(
|
|
177
|
+
`implementation-pack: ${missingRecords.length} record(s) referenced by split "${manifest.source_split_id}" ` +
|
|
178
|
+
`could not be loaded from ${recordsDir}` +
|
|
179
|
+
(missingRecords.length > 10 ? ` — first 10: ${sample.join(', ')}, ...` : ` — ${sample.join(', ')}`),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
123
183
|
for (const [name, records] of [...bySubject].sort((a, b) => b[1].length - a[1].length).slice(0, 5)) {
|
|
124
184
|
subjectExamples.push({
|
|
125
185
|
subject_name: name,
|
|
@@ -138,17 +198,18 @@ export async function buildImplementationPack(projectRoot, manifestId) {
|
|
|
138
198
|
};
|
|
139
199
|
|
|
140
200
|
// Write implementation pack
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// D-005
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
201
|
+
// SDL-M10: atomically claim the implementation-pack id/dir — closes the
|
|
202
|
+
// check-then-act race the old existsSync+recursive-mkdir pattern had
|
|
203
|
+
// (D-005's intent, now actually race-proof; see claimIdDir in snapshot.js).
|
|
204
|
+
const { id: implId, dir: implDir } = await claimIdDir(
|
|
205
|
+
join(projectRoot, 'training', 'implementations'),
|
|
206
|
+
generateImplId,
|
|
207
|
+
{
|
|
208
|
+
code: 'IMPL_PACK_ID_COLLISION',
|
|
209
|
+
message: 'Could not claim a unique implementation-pack ID — every candidate collided with an existing implementation-pack directory.',
|
|
210
|
+
hint: 'Retry; if this persists, another sdlab process may be creating implementation packs in a tight loop.',
|
|
211
|
+
},
|
|
212
|
+
);
|
|
152
213
|
|
|
153
214
|
const implManifest = {
|
|
154
215
|
// DB-001 / DB-006: stamp manifest schema version
|
|
@@ -163,7 +224,24 @@ export async function buildImplementationPack(projectRoot, manifestId) {
|
|
|
163
224
|
known_failures: knownFailures.length,
|
|
164
225
|
subject_examples: subjectExamples.length,
|
|
165
226
|
lanes_covered: laneExamples.size,
|
|
227
|
+
// F3: how many split-referenced records could not be loaded while
|
|
228
|
+
// building this pack. 0 in the common case; see missing_records
|
|
229
|
+
// below for the actual ids.
|
|
230
|
+
missing_records: missingRecords.length,
|
|
231
|
+
// F4: how many record files (from the known-failures scan over ALL
|
|
232
|
+
// of records/) existed but threw on JSON.parse — distinct from
|
|
233
|
+
// missing_records above, which is "no file at all". 0 in the common
|
|
234
|
+
// case; see unreadable_records below for the actual files.
|
|
235
|
+
unreadable_records: unreadable.length,
|
|
166
236
|
},
|
|
237
|
+
// F3: ledger of every record_id that failed to load while building
|
|
238
|
+
// this pack (mirrors the images_failed pattern export.js established
|
|
239
|
+
// at export time) — record_id + stage + which loop hit it, so an
|
|
240
|
+
// operator can trace it without re-diffing the split against records/.
|
|
241
|
+
missing_records: missingRecords,
|
|
242
|
+
// F4: ledger of record files that existed but failed to JSON.parse
|
|
243
|
+
// (DB-003 parity with snapshot.js's `errors` field).
|
|
244
|
+
unreadable_records: unreadable,
|
|
167
245
|
};
|
|
168
246
|
|
|
169
247
|
await writeFile(join(implDir, 'manifest.json'), JSON.stringify(implManifest, null, 2) + '\n');
|