* fix(inspector): pass text through unknown nodes instead of refusing them (#381) The inspector refused any node class missing from the semantics table, so a PromptManagerPair behind an rgthree Any Switch showed "Prompt uses a node the inspector does not support". The inspection resolver (Python and browser) now reads through unknown nodes: a text input is the text, positive/negative route by output slot, a conditioning input is followed alone, otherwise every linked input except core type inputs (pass_through.skip_inputs). PromptManager nodes are authoritative; several different texts become one candidate each ("Several prompts reach the sampler"). A seeded text node with no conditioning input is generated at run time, and a recorded PromptManager execution is not checked against text that passed through an unknown node. INSPECTION_RESOLVER_VERSION goes to 2; ingestion keeps the default profile. A legacy parity gate records every committed graph whose inspection positive matches the legacy parser's; no graph may leave it. * fix(inspector): keep recorded pair text, string-only seed rule, role inputs and unique candidates (#381) Review of the pass-through: a walk through an unknown node never overrides a PromptManagerPair's execution record (any outcome but work-limit is unverifiable while the record is checked); the run-time seed rule reads only string outputs; a node with positive/negative inputs follows only the role of the requested slot, nothing when it is unlinked; one text offered twice is one candidate. * fix(inspector): anchor on the saver that wrote the file and resolve stages on shared links (#381) An inspected file's name now anchors the inspection on the one saver whose filename_prefix (literal or traced) makes the file's stem <prefix>_<digits>[_]; the samplers in that saver's image ancestry are the candidates. No match, several matches or an unreadable walk anchor nothing, so the file stays "several possible prompts". In a latent walk, an earlier sampler reading the same conditioning links as the later one is the same prompt, not an unattributed stage. Python and the browser resolver behave the same; the legacy parity gate states each graph that differs from legacy on purpose. * fix(inspector): skip only plain first stages on shared links; an unfinished prefix scan anchors nothing (#381) Review of the saver anchor: a same-links earlier stage is skipped only when it is not a detailer, sets no prompt override and continues no other sampler, so an earlier prompt or override behind it is never hidden. A work limit while tracing a saver prefix ends the match scan without an anchor instead of counting that saver as no match. * test(inspector): cover every plain-first-stage outcome on both resolvers (#381) The frontend coverage gate found local-resolver-stages.mjs branches at 92.44% (< 93). Each way an earlier stage stops being a plain first stage (no latent, literal, missing or cyclic latent, a sampler-shaped node, a detailer) is now a resolver test and an anchor-first-stage-* parity vector.
94 lines
6.5 KiB
JavaScript
94 lines
6.5 KiB
JavaScript
// The browser's local readers and projection run over files on disk, for
|
|
// tests/test_local_inspect_parity.py, which compares every answer with the server's
|
|
// utils.prompt_inspector.read_inspection_file and utils.inspection_response._metadata
|
|
// on the same file. Each file is opened as a Blob (fs.openAsBlob) so the reader
|
|
// slices it exactly as it slices a File.
|
|
// stdin: [{name, path} | {name, tags, limited?} | {name, records, record_formats?, carrier_text?, limited?}]
|
|
// stdout: {name: {classified, result, metadata}} where result is readPngMetadataFile's
|
|
// answer for a PNG, readImageHeader's for another image, for a video
|
|
// {read: readVideoTags(...), selection: selectVideoCarrier(read.tags, {limited})}
|
|
// (selection null unless the read completed), else null; metadata is
|
|
// projectMetadata(...) of the local read (the server's _metadata) for every
|
|
// classified file, else null.
|
|
// A {tags} entry answers {classified: null, result: {selection}, metadata} for
|
|
// the carrier rule alone, as the server selects from ffprobe's tag map.
|
|
// A {records} entry (records null = an unreadable file) answers
|
|
// {classified: null, result: null, metadata} for the projection alone.
|
|
// A {mode: 'resolve'} entry (PR L3) answers {evidence}: resolveLocalEvidence of its {records} texts
|
|
// with the repository semantics table. With a {path} it reads the file locally and also answers
|
|
// {facts} (and {unreadable: true} with the server's unreadable evidence when nothing could be read):
|
|
// extractGenerationFacts on the local payload through projectInspectionPayload, exactly as
|
|
// tests/js/support/inspection-parity-facts.mjs reads the server's response for vectors.json.
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { createRequire } from 'node:module';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { moduleUrl } from './module-url.mjs';
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const require = createRequire(import.meta.url);
|
|
const pngMetadata = require(path.join(here, '..', '..', '..', 'web', 'js', 'png-metadata.js'));
|
|
const web = path.join(here, '..', '..', '..', 'web', 'js', 'workspace');
|
|
// ES modules import by file URL (a Windows path would parse as a URL scheme).
|
|
const { selectVideoCarrier } = await import(moduleUrl(path.join(web, 'local-video.mjs')));
|
|
const { projectMetadata } = await import(moduleUrl(path.join(web, 'local-projection.mjs')));
|
|
const { readLocalFile, toInspectionRead } = await import(moduleUrl(path.join(web, 'local-inspection.mjs')));
|
|
const { resolveLocalEvidence, compileSemantics, unreadableEvidence } = await import(moduleUrl(path.join(web, 'local-resolver.mjs')));
|
|
const { buildLocalPayload } = await import(moduleUrl(path.join(web, 'local-inspection.mjs')));
|
|
const { projectInspectionPayload } = await import(moduleUrl(path.join(web, 'inspection-state.mjs')));
|
|
const semantics = compileSemantics(JSON.parse(fs.readFileSync(path.join(here, '..', '..', '..', 'web', 'js', 'inspection-semantics.json'), 'utf8')));
|
|
const { extractGenerationFacts } = require(path.join(here, '..', '..', '..', 'web', 'js', 'comfy-metadata.js'));
|
|
|
|
/** The local evidence for one set of record texts. */
|
|
const resolved = (records, fileName = null) => ({ evidence: resolveLocalEvidence(records, semantics, fileName) });
|
|
|
|
/** The local wire's evidence and six facts for one read (no legacy fill, as in the vectors' own harness). */
|
|
function localPayload(read, file, classification) {
|
|
const payload = buildLocalPayload({ read, file, classification, semantics });
|
|
const record = projectInspectionPayload(payload, { kind: 'local-file', key: 'local:1', path: file.name }, null, extractGenerationFacts);
|
|
const facts = Object.fromEntries(Object.entries(record.generation).map(([field, fact]) => [field, { status: fact.status, value: fact.value }]));
|
|
return { evidence: { prompts: payload.prompts, resolution: payload.resolution }, facts };
|
|
}
|
|
|
|
const selectionRead = (selection) => selection.status === 'complete' ?
|
|
{ records: selection.records, record_formats: selection.record_formats, carrierText: selection.carrierText, limited: selection.limited } : null;
|
|
|
|
const chunks = [];
|
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
const output = {};
|
|
for (const entry of JSON.parse(Buffer.concat(chunks).toString('utf8'))) {
|
|
const { name } = entry;
|
|
if (entry.mode === 'resolve') {
|
|
if (entry.records !== undefined) { output[name] = resolved(entry.records, entry.fileName ?? null); continue; }
|
|
const blob = await fs.openAsBlob(entry.path);
|
|
const { classification, result } = await readLocalFile(blob, { pngMetadata });
|
|
const read = classification === null ? null : toInspectionRead(classification, result).read;
|
|
if (classification === null) { output[name] = { unreadable: true }; continue; }
|
|
// The evidence is the local wire's own (buildLocalPayload), and it equals resolveLocalEvidence of the records.
|
|
const local = localPayload(result, { name: path.basename(entry.path), size: blob.size }, classification);
|
|
const direct = read === null ? unreadableEvidence() : resolved(read.records, path.basename(entry.path)).evidence;
|
|
if (JSON.stringify(direct) !== JSON.stringify(local.evidence)) throw new Error(`${name}: the wire's roles are not the resolver's`);
|
|
output[name] = read === null ? { unreadable: true, ...local } : local;
|
|
continue;
|
|
}
|
|
if (entry.records !== undefined) {
|
|
const read = entry.records === null ? null : {
|
|
records: entry.records,
|
|
record_formats: entry.record_formats ?? Object.fromEntries(Object.keys(entry.records).map((key) => [key, 'original-text'])),
|
|
carrierText: entry.carrier_text ?? null, limited: entry.limited ?? [],
|
|
};
|
|
output[name] = { classified: null, result: null, metadata: projectMetadata(read) };
|
|
continue;
|
|
}
|
|
if (entry.path === undefined) {
|
|
const selection = selectVideoCarrier(entry.tags, { limited: entry.limited || [] });
|
|
output[name] = { classified: null, result: { selection }, metadata: projectMetadata(selectionRead(selection)) };
|
|
continue;
|
|
}
|
|
const file = await fs.openAsBlob(entry.path);
|
|
const { classification, result } = await readLocalFile(file, { pngMetadata });
|
|
const metadata = classification === null ? null : projectMetadata(toInspectionRead(classification, result).read);
|
|
output[name] = { classified: classification, result, metadata };
|
|
}
|
|
process.stdout.write(JSON.stringify(output));
|