@12ui/design 0.2.67 → 0.2.68
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/SKILL.md +1 -1
- package/open-design.json +2 -2
- package/package.json +1 -1
- package/runtime/bootstrap.mjs +131 -0
- package/runtime/codex-conversion/README.md +75 -0
- package/runtime/codex-conversion/bin/convert.mjs +21 -0
- package/runtime/codex-conversion/bin/export.mjs +4 -0
- package/runtime/codex-conversion/bin/finish.mjs +11 -0
- package/runtime/codex-conversion/bin/improve.mjs +12 -0
- package/runtime/codex-conversion/bin/install-skill.mjs +36 -0
- package/runtime/codex-conversion/bin/recover-image.mjs +11 -0
- package/runtime/codex-conversion/bin/recovery/prepare.mjs +68 -0
- package/runtime/codex-conversion/bin/recovery/run.mjs +37 -0
- package/runtime/codex-conversion/npm-shrinkwrap.json +1199 -0
- package/runtime/codex-conversion/package.json +45 -0
- package/runtime/codex-conversion/src/codex/arguments.mjs +14 -0
- package/runtime/codex-conversion/src/codex/completion.mjs +14 -0
- package/runtime/codex-conversion/src/codex/events.mjs +41 -0
- package/runtime/codex-conversion/src/codex/home.mjs +37 -0
- package/runtime/codex-conversion/src/codex/invoke.mjs +79 -0
- package/runtime/codex-conversion/src/contract-repair.mjs +44 -0
- package/runtime/codex-conversion/src/contracts/compile.mjs +53 -0
- package/runtime/codex-conversion/src/contracts/contracts.mjs +19 -0
- package/runtime/codex-conversion/src/contracts/geometry.mjs +38 -0
- package/runtime/codex-conversion/src/contracts/grid-frame.mjs +22 -0
- package/runtime/codex-conversion/src/contracts/groups.mjs +14 -0
- package/runtime/codex-conversion/src/contracts/initial.mjs +9 -0
- package/runtime/codex-conversion/src/contracts/normalize.mjs +46 -0
- package/runtime/codex-conversion/src/exports/browser.mjs +26 -0
- package/runtime/codex-conversion/src/exports/editable.mjs +34 -0
- package/runtime/codex-conversion/src/exports/files.mjs +7 -0
- package/runtime/codex-conversion/src/exports/index.mjs +24 -0
- package/runtime/codex-conversion/src/exports/react.mjs +44 -0
- package/runtime/codex-conversion/src/exports/viewport.mjs +17 -0
- package/runtime/codex-conversion/src/finish/index.mjs +58 -0
- package/runtime/codex-conversion/src/finish/patch.mjs +14 -0
- package/runtime/codex-conversion/src/images/execute.mjs +52 -0
- package/runtime/codex-conversion/src/images/frame-normalization.mjs +10 -0
- package/runtime/codex-conversion/src/images/geometry.mjs +9 -0
- package/runtime/codex-conversion/src/images/local-operation.mjs +17 -0
- package/runtime/codex-conversion/src/images/local.mjs +18 -0
- package/runtime/codex-conversion/src/images/native-collect.mjs +43 -0
- package/runtime/codex-conversion/src/images/native-files.mjs +11 -0
- package/runtime/codex-conversion/src/images/native-invoke.mjs +49 -0
- package/runtime/codex-conversion/src/images/native-records.mjs +57 -0
- package/runtime/codex-conversion/src/images/native.mjs +41 -0
- package/runtime/codex-conversion/src/images/prompt.mjs +14 -0
- package/runtime/codex-conversion/src/images/provider.mjs +22 -0
- package/runtime/codex-conversion/src/output.mjs +28 -0
- package/runtime/codex-conversion/src/project/invoke.mjs +35 -0
- package/runtime/codex-conversion/src/project/prompt.mjs +13 -0
- package/runtime/codex-conversion/src/project/run.mjs +83 -0
- package/runtime/codex-conversion/src/project/source.mjs +21 -0
- package/runtime/codex-conversion/src/references.mjs +114 -0
- package/runtime/codex-conversion/src/run-store-io.mjs +38 -0
- package/runtime/codex-conversion/src/run-store.mjs +114 -0
- package/runtime/codex-conversion/src/run.mjs +108 -0
- package/runtime/codex-conversion/src/runtime-pin.mjs +12 -0
- package/runtime/codex-conversion/templates/initial.schema.json +90 -0
- package/runtime/codex-conversion/templates/initial.txt +22 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {fileURLToPath} from 'node:url';
|
|
4
|
+
import sharp from 'sharp';
|
|
5
|
+
import {withRunLock,hash,writeJson,readJson} from '../run-store.mjs';
|
|
6
|
+
import {prepareHome} from '../codex/home.mjs';
|
|
7
|
+
import {pinRuntime} from '../runtime-pin.mjs';
|
|
8
|
+
import {inventory,changedFiles,excluded} from './source.mjs';
|
|
9
|
+
import {projectPrompt} from './prompt.mjs';
|
|
10
|
+
import {invokeProject,completion} from './invoke.mjs';
|
|
11
|
+
async function implementation() {
|
|
12
|
+
const here=path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const files=['run.mjs','source.mjs','prompt.mjs','invoke.mjs','../codex/home.mjs','../codex/events.mjs','../run-store.mjs','../run-store-io.mjs','../runtime-pin.mjs'];
|
|
14
|
+
return hash(JSON.stringify(await Promise.all(files.map(async f=>[f,hash(await fs.readFile(path.join(here,f)))]))));
|
|
15
|
+
}
|
|
16
|
+
async function image(file) {
|
|
17
|
+
const bytes=await fs.readFile(file), metadata=await sharp(bytes).metadata();
|
|
18
|
+
if(!['png','jpeg','webp'].includes(metadata.format??'')||!metadata.width||!metadata.height)throw Error('Images must be PNG, JPEG or WebP');
|
|
19
|
+
return {bytes,width:metadata.width,height:metadata.height,sha256:hash(bytes)};
|
|
20
|
+
}
|
|
21
|
+
export async function prepareProject(options) {
|
|
22
|
+
for(const key of ['project','target','sourceCapture','instructions','runDir'])if(!path.isAbsolute(options[key]??''))throw Error(`Absolute ${key} required`);
|
|
23
|
+
const projectDir=await fs.realpath(options.project), runDir=path.join(await fs.realpath(path.dirname(options.runDir)),path.basename(options.runDir));
|
|
24
|
+
if(runDir===projectDir||runDir.startsWith(projectDir+path.sep)||projectDir.startsWith(runDir+path.sep))throw Error('Run directory must be outside project');
|
|
25
|
+
const target=await image(options.target), source=await image(options.sourceCapture), instructions=await fs.readFile(options.instructions,'utf8');
|
|
26
|
+
if(!instructions.trim())throw Error('Explicit project instructions required');
|
|
27
|
+
const width=options.width??target.width,height=options.height??target.height,timeoutMs=options.timeoutMs??1800000;
|
|
28
|
+
if(![width,height].every(n=>Number.isSafeInteger(n)&&n>0)||!Number.isSafeInteger(timeoutMs)||timeoutMs<1||timeoutMs>1800000)throw Error('Positive viewport and timeout up to 1800000ms required');
|
|
29
|
+
const config={schemaVersion:1,projectDir,width,height,timeoutMs,instructions,model:'gpt-6-astra',effort:'low',targetSha256:target.sha256,sourceSha256:source.sha256,implementationSha256:await implementation()};
|
|
30
|
+
const before=await inventory(projectDir),prompt=projectPrompt(config,runDir);
|
|
31
|
+
await fs.mkdir(runDir);
|
|
32
|
+
await pinRuntime(runDir);
|
|
33
|
+
await fs.writeFile(path.join(runDir,'target.image'),target.bytes,{flag:'wx'});await fs.writeFile(path.join(runDir,'source.image'),source.bytes,{flag:'wx'});
|
|
34
|
+
await fs.writeFile(path.join(runDir,'prompt.txt'),prompt,{flag:'wx'});
|
|
35
|
+
await writeJson(path.join(runDir,'before.json'),before);
|
|
36
|
+
await writeJson(path.join(runDir,'project.json'),{...config,promptSha256:hash(prompt),beforeSha256:hash(JSON.stringify(before)),createdAt:new Date().toISOString(),inventoryExclusions:[...excluded],inventoryNote:'Private names, database/key/log files and symlink targets are not read. Hash inventory is not a backup.'});
|
|
37
|
+
return {status:'prepared',runDir,projectDir,width,height,dispatched:false};
|
|
38
|
+
}
|
|
39
|
+
async function verify(runDir) {
|
|
40
|
+
const config=await readJson(path.join(runDir,'project.json'));
|
|
41
|
+
if(!config)throw Error('Project run not prepared');
|
|
42
|
+
if(await fs.realpath(config.projectDir)!==config.projectDir)throw Error('Project directory identity changed');
|
|
43
|
+
if(hash(projectPrompt(config,runDir))!==config.promptSha256)throw Error('Frozen project configuration changed');
|
|
44
|
+
if(config.implementationSha256!==await implementation())throw Error('Project runtime changed; inspect existing run before using a new run');
|
|
45
|
+
for(const [name,expected] of [['target.image',config.targetSha256],['source.image',config.sourceSha256],['prompt.txt',config.promptSha256]])if(hash(await fs.readFile(path.join(runDir,name)))!==expected)throw Error(`Frozen ${name} changed`);
|
|
46
|
+
const before=await readJson(path.join(runDir,'before.json'));
|
|
47
|
+
if(hash(JSON.stringify(before))!==config.beforeSha256)throw Error('Frozen source inventory changed');
|
|
48
|
+
return {config,before};
|
|
49
|
+
}
|
|
50
|
+
async function reconcile(runDir,config,before) {
|
|
51
|
+
const claim=await readJson(path.join(runDir,'dispatch.json'));
|
|
52
|
+
if(!claim)return {status:'prepared',runDir,projectDir:config.projectDir,dispatched:false};
|
|
53
|
+
const terminal=await completion(runDir),after=await inventory(config.projectDir);
|
|
54
|
+
// First observation is immutable evidence; later source edits stay separately visible.
|
|
55
|
+
let observed=await readJson(path.join(runDir,'after.json'));
|
|
56
|
+
if(!observed){await writeJson(path.join(runDir,'after.json'),after);observed=after;}
|
|
57
|
+
const result={schemaVersion:1,...terminal,runDir,projectDir:config.projectDir,dispatched:true,changedFiles:changedFiles(before,observed),changesSinceObservation:changedFiles(observed,after),sourceObservationAt:(await fs.stat(path.join(runDir,'after.json'))).mtime.toISOString(),quality:'Requires independent real-browser and behavior review; CLI completion is not a quality verdict.',assetRequests:'asset-requests.json, if produced; parent execution required',costBasis:'Subscription CLI usage; no inferred dollar cost',reconciledAt:new Date().toISOString()};
|
|
58
|
+
await writeJson(path.join(runDir,'result.json'),result);return result;
|
|
59
|
+
}
|
|
60
|
+
/** Resume is reconciliation only, including prepared runs. It never invokes Codex. */
|
|
61
|
+
export async function resumeProject({runDir}) {
|
|
62
|
+
runDir=await fs.realpath(runDir);
|
|
63
|
+
return withRunLock(runDir,async()=>{const {config,before}=await verify(runDir);return reconcile(runDir,config,before);});
|
|
64
|
+
}
|
|
65
|
+
/** Dependencies are injectable only for deterministic execution/custody tests. */
|
|
66
|
+
export async function applyProject({runDir,authHome,binary='codex'},dependencies={prepareHome,invokeProject}) {
|
|
67
|
+
runDir=await fs.realpath(runDir);
|
|
68
|
+
return withRunLock(runDir,async()=>{
|
|
69
|
+
const {config,before}=await verify(runDir);
|
|
70
|
+
if(await readJson(path.join(runDir,'dispatch.json')))return reconcile(runDir,config,before);
|
|
71
|
+
if(JSON.stringify(await inventory(config.projectDir))!==JSON.stringify(before))throw Error('Project source changed since preparation');
|
|
72
|
+
if(!authHome)throw Error('authHome required for a new application');
|
|
73
|
+
const prepared=await dependencies.prepareHome({authHome,binary,runDir,reasoning:'low'});
|
|
74
|
+
try {
|
|
75
|
+
// Durable dispatch precedes spawning; no automatic redispatch after this point.
|
|
76
|
+
await writeJson(path.join(runDir,'dispatch.json'),{startedAt:new Date().toISOString(),model:'gpt-6-astra',effort:'low',toolEnabled:true});
|
|
77
|
+
try {await dependencies.invokeProject({config,runDir,home:prepared.home,binary,prompt:await fs.readFile(path.join(runDir,'prompt.txt'),'utf8')});}
|
|
78
|
+
catch {await writeJson(path.join(runDir,'invocation-error.json'),{message:'Execution interrupted or failed; source changes retained. Inspect saved execution records before any new inference.'});}
|
|
79
|
+
return await reconcile(runDir,config,before);
|
|
80
|
+
}finally{await fs.rm(prepared.home,{recursive:true,force:true});}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export async function improveProject(options) {await prepareProject(options);return applyProject(options);}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {hash} from '../run-store.mjs';
|
|
4
|
+
// Inventory source identities only. Never follow symlinks or read credentials/runtime state.
|
|
5
|
+
export const excluded = new Set(['.git','node_modules','.wrangler','dist','build','.next','.nuxt','.vite','coverage','test-results','playwright-report','.tmp','tmp','logs','.codex','.aws','.ssh','.vercel','.netlify']);
|
|
6
|
+
export function privateName(name) { return name.startsWith('.env') || /(?:^|[._-])(?:secret|secrets|credentials|auth|cookies)(?:[._-]|$)/i.test(name) || /\.(?:pem|key|p12|pfx|sqlite|sqlite3|db|log)$/i.test(name); }
|
|
7
|
+
export async function inventory(root, prefix='') {
|
|
8
|
+
const result=[];
|
|
9
|
+
for(const entry of await fs.readdir(path.join(root,prefix),{withFileTypes:true})) {
|
|
10
|
+
if(excluded.has(entry.name)||privateName(entry.name))continue;
|
|
11
|
+
const relative=path.join(prefix,entry.name), file=path.join(root,relative);
|
|
12
|
+
if(entry.isSymbolicLink())result.push({path:relative,symlink:await fs.readlink(file)});
|
|
13
|
+
else if(entry.isDirectory())result.push(...await inventory(root,relative));
|
|
14
|
+
else if(entry.isFile())result.push({path:relative,sha256:hash(await fs.readFile(file))});
|
|
15
|
+
}
|
|
16
|
+
return result.sort((a,b)=>a.path.localeCompare(b.path));
|
|
17
|
+
}
|
|
18
|
+
export function changedFiles(before,after) {
|
|
19
|
+
const old=new Map(before.map(f=>[f.path,JSON.stringify(f)])), next=new Map(after.map(f=>[f.path,JSON.stringify(f)]));
|
|
20
|
+
return [...new Set([...old.keys(),...next.keys()])].filter(p=>old.get(p)!==next.get(p)).sort().map(p=>({path:p,before:before.find(f=>f.path===p)??null,after:after.find(f=>f.path===p)??null}));
|
|
21
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {parse} from 'parse5';
|
|
2
|
+
import * as css from 'css-tree';
|
|
3
|
+
import {createHash} from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
/** Inspect parsed syntax, patch reference and interactive editability spans in the original bytes. */
|
|
6
|
+
export function bindHtml(html, assets, {supplementalNames=[]} = {}) {
|
|
7
|
+
if (typeof html !== 'string') throw Error('Expected HTML string');
|
|
8
|
+
const document = parse(html, {sourceCodeLocationInfo:true});
|
|
9
|
+
const seen = new Set(), recoveries = [], supplemental = [], externalDependencies = [], edits = [];
|
|
10
|
+
let hasHtml = false, hasBody = false;
|
|
11
|
+
const replace = (start,end,value) => edits.push({start,end,value});
|
|
12
|
+
function reference(value, kind) {
|
|
13
|
+
const asset = assets.find(a => [a.id, `asset:${a.id}`, `${a.id}.png`].includes(value));
|
|
14
|
+
if (asset) {
|
|
15
|
+
seen.add(asset.id);
|
|
16
|
+
const target = `${asset.id}.png`;
|
|
17
|
+
if (value !== target) recoveries.push({from:value,to:target,kind:value.startsWith('asset:')?'standard':'exact-declared-ID'});
|
|
18
|
+
return target;
|
|
19
|
+
}
|
|
20
|
+
if (value.startsWith('#') || supplementalNames.includes(value)) return value;
|
|
21
|
+
if (kind === 'font' && /^https:\/\/fonts\.(?:googleapis|gstatic)\.com\//i.test(value)) {
|
|
22
|
+
externalDependencies.push({url:value,kind:'font',bundled:false}); return value;
|
|
23
|
+
}
|
|
24
|
+
if (value.startsWith('data:image/svg+xml,')) {
|
|
25
|
+
const bytes = Buffer.from(decodeURIComponent(value.slice('data:image/svg+xml,'.length)));
|
|
26
|
+
const name = `inline-${createHash('sha256').update(bytes).digest('hex').slice(0,16)}.svg`;
|
|
27
|
+
// Inline SVG is artwork, not a route around the no-script/no-external-resource contract.
|
|
28
|
+
if (/<(?:script|foreignObject|iframe)\b|\bon\w+\s*=|(?:href\s*=\s*["']?\s*(?!#)|url\(\s*["']?\s*(?!#))/i.test(bytes.toString())) throw Error('Unsupported SVG dependency');
|
|
29
|
+
if (!supplemental.some(a=>a.name===name)) supplemental.push({name,bytes,sourceUri:value});
|
|
30
|
+
return name;
|
|
31
|
+
}
|
|
32
|
+
throw Error(`Unbound or prohibited ${kind} reference: ${value}`);
|
|
33
|
+
}
|
|
34
|
+
function style(text, offset, context='stylesheet') {
|
|
35
|
+
let ast;
|
|
36
|
+
try { ast=css.parse(text,{context,positions:true,parseCustomProperty:true}); } catch { throw Error('Invalid CSS'); }
|
|
37
|
+
/** @type {import("css-tree").EnterOrLeaveFn} */
|
|
38
|
+
const enter = function(node) {
|
|
39
|
+
if (node.type === 'Raw') throw Error('Unsupported CSS syntax');
|
|
40
|
+
if (node.type === 'Atrule' && node.name.toLowerCase() === 'import') {
|
|
41
|
+
css.walk(node.prelude, child => {
|
|
42
|
+
if (child.type === 'String') reference(child.value,'font');
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (node.type !== 'Url') return;
|
|
46
|
+
let parent = this.atrule;
|
|
47
|
+
const kind = parent && ['font-face','import'].includes(parent.name.toLowerCase()) ? 'font' : 'artwork';
|
|
48
|
+
const value = reference(node.value,kind);
|
|
49
|
+
if (value !== node.value) {
|
|
50
|
+
const raw = text.slice(node.loc.start.offset,node.loc.end.offset);
|
|
51
|
+
const start = raw.indexOf('(')+1, end=raw.lastIndexOf(')');
|
|
52
|
+
const inside=raw.slice(start,end), match=inside.match(/^(\s*)(["']?)([\s\S]*?)\2(\s*)$/);
|
|
53
|
+
replace(offset+node.loc.start.offset+start+match[1].length+match[2].length,offset+node.loc.start.offset+end-match[4].length-match[2].length,value);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
css.walk(ast, {enter});
|
|
57
|
+
}
|
|
58
|
+
function walk(node, inheritedEditable=false) {
|
|
59
|
+
if (node.tagName === 'html' && node.sourceCodeLocation) hasHtml=true;
|
|
60
|
+
if (node.tagName === 'body' && node.sourceCodeLocation) hasBody=true;
|
|
61
|
+
if (['script','iframe','canvas','object','embed','base'].includes(node.tagName)) throw Error(`Prohibited element ${node.tagName}`);
|
|
62
|
+
const attrs = node.attrs ?? [];
|
|
63
|
+
const ownEditable = attrs.find(a => a.name === 'contenteditable')?.value.toLowerCase();
|
|
64
|
+
let editable = ownEditable === 'false' ? false : ['', 'true', 'plaintext-only'].includes(ownEditable) ? true : inheritedEditable;
|
|
65
|
+
// Editable anchor hosts suppress the browser's native navigation. Keep labels
|
|
66
|
+
// and other inline markup intact; explicit editable descendants remain editable.
|
|
67
|
+
if (node.tagName === 'a' && attrs.some(a => a.name === 'href') && editable) {
|
|
68
|
+
const loc = node.sourceCodeLocation?.attrs?.contenteditable;
|
|
69
|
+
if (loc) replace(loc.startOffset, loc.endOffset, 'contenteditable="false"');
|
|
70
|
+
else {
|
|
71
|
+
const start = node.sourceCodeLocation.startTag.endOffset - 1;
|
|
72
|
+
replace(start, start, ' contenteditable="false"');
|
|
73
|
+
}
|
|
74
|
+
recoveries.push({kind:'interactive-editability', from:ownEditable ?? 'inherited', to:'false'});
|
|
75
|
+
editable = false;
|
|
76
|
+
}
|
|
77
|
+
if (node.tagName === 'meta' && attrs.some(a => a.name === 'http-equiv' && a.value.trim().toLowerCase() === 'refresh')) throw Error('Prohibited active HTML refresh');
|
|
78
|
+
for (const attr of attrs) {
|
|
79
|
+
const key = attr.prefix ? `${attr.prefix}:${attr.name}` : attr.name;
|
|
80
|
+
if (/^on/i.test(key) || key === 'srcdoc' || /^(?:javascript|vbscript):/i.test(attr.value.replace(/[\t\n\r]/g, '').replace(/^[\u0000-\u0020]+/, ''))) throw Error('Prohibited active HTML');
|
|
81
|
+
const loc = node.sourceCodeLocation?.attrs?.[key];
|
|
82
|
+
if (!loc) continue;
|
|
83
|
+
const raw=html.slice(loc.startOffset,loc.endOffset), m=raw.match(/^[^=]+=(\s*)(["']?)/);
|
|
84
|
+
const start=loc.startOffset+(m?.[0].length ?? raw.length), end=loc.endOffset-(m?.[2] ? 1:0);
|
|
85
|
+
if (key === 'style') { style(html.slice(start,end),start,'declarationList'); continue; }
|
|
86
|
+
if (key === 'srcset') throw Error('Unsupported raster srcset; use declared asset references');
|
|
87
|
+
const href = ['href','xlink:href'].includes(key);
|
|
88
|
+
const font = node.tagName === 'link' && attrs.some(a=>a.name==='rel' && a.value.toLowerCase()==='stylesheet');
|
|
89
|
+
const resource = ['src','poster','background'].includes(key) || href && (font || ['image','use','link'].includes(node.tagName));
|
|
90
|
+
if (!resource) continue;
|
|
91
|
+
const value=reference(attr.value,font?'font':'artwork');
|
|
92
|
+
if (value !== attr.value) replace(start,end,value);
|
|
93
|
+
}
|
|
94
|
+
if (node.tagName === 'style') for (const child of node.childNodes ?? []) if (child.nodeName==='#text') style(child.value,child.sourceCodeLocation.startOffset);
|
|
95
|
+
for (const child of node.childNodes ?? []) walk(child, editable);
|
|
96
|
+
if (node.content) walk(node.content, editable);
|
|
97
|
+
}
|
|
98
|
+
walk(document);
|
|
99
|
+
if (!hasHtml || !hasBody) throw Error('Invalid complete HTML document');
|
|
100
|
+
for (const a of assets) if (!seen.has(a.id)) throw Error(`Omitted artwork ${a.id}`);
|
|
101
|
+
for (const edit of edits.sort((a,b)=>b.start-a.start)) html=html.slice(0,edit.start)+edit.value+html.slice(edit.end);
|
|
102
|
+
return {html,supplemental,recoveries,externalDependencies};
|
|
103
|
+
}
|
|
104
|
+
export const validateBind = bindHtml;
|
|
105
|
+
|
|
106
|
+
/** Keep the saved document available to bounded repair when a fact is unresolved. */
|
|
107
|
+
export function normalizeHtml(html, assets, options) {
|
|
108
|
+
try { return {ok:true,...bindHtml(html,assets,options),issues:[]}; }
|
|
109
|
+
catch(error) {
|
|
110
|
+
const message=String(error.message ?? error);
|
|
111
|
+
const syntax=/CSS|SVG/.test(message);
|
|
112
|
+
return {ok:false,html,supplemental:[],recoveries:[],externalDependencies:[],issues:[{code:syntax?'unresolved-document-syntax':'unresolved-document-reference',message,repairable:true,requiresModel:true,instruction:syntax?'Preserve the intended appearance and all content. Express the reported syntax in supported HTML/CSS/SVG; parser rejection alone is not evidence the design is invalid.':'Preserve every declared owner. Correct the reported reference or incomplete HTML structure using exact declared IDs; do not omit artwork or introduce fallback images.'}]};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, rename, readFile, unlink } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
/** @param {string | Uint8Array} bytes */
|
|
5
|
+
export const hash = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
6
|
+
/** @param {string} path */
|
|
7
|
+
export async function syncDirectory(path) {
|
|
8
|
+
const handle = await open(path, 'r');
|
|
9
|
+
try { await handle.sync(); } finally { await handle.close(); }
|
|
10
|
+
}
|
|
11
|
+
/** @param {string} path @param {string | Uint8Array} bytes */
|
|
12
|
+
export async function writeBytes(path, bytes) {
|
|
13
|
+
await mkdir(dirname(path), { recursive: true });
|
|
14
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
15
|
+
const handle = await open(temporary, 'wx', 0o600);
|
|
16
|
+
try { await handle.writeFile(bytes); await handle.sync(); }
|
|
17
|
+
finally { await handle.close(); }
|
|
18
|
+
try { await rename(temporary, path); await syncDirectory(dirname(path)); }
|
|
19
|
+
finally { await unlink(temporary).catch(() => {}); }
|
|
20
|
+
}
|
|
21
|
+
/** @param {string} path @param {unknown} value */
|
|
22
|
+
export async function writeJson(path, value) { await writeBytes(path, `${JSON.stringify(value, null, 2)}\n`); }
|
|
23
|
+
/** @param {string} path @returns {Promise<any>} */
|
|
24
|
+
export async function readJson(path) {
|
|
25
|
+
try { return JSON.parse(await readFile(path, 'utf8')); }
|
|
26
|
+
catch (error) { if (/** @type {NodeJS.ErrnoException} */ (error).code === 'ENOENT') return null; throw error; }
|
|
27
|
+
}
|
|
28
|
+
/** Strict canonical JSON prevents identity collisions from unsupported values. @param {unknown} value @returns {string} */
|
|
29
|
+
export function canonical(value) {
|
|
30
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return JSON.stringify(value);
|
|
31
|
+
if (typeof value === 'number' && Number.isFinite(value)) return JSON.stringify(value);
|
|
32
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
33
|
+
if (typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
34
|
+
const record = /** @type {Record<string, unknown>} */ (value);
|
|
35
|
+
return `{${Object.keys(record).sort().map(key => `${JSON.stringify(key)}:${canonical(record[key])}`).join(',')}}`;
|
|
36
|
+
}
|
|
37
|
+
throw new Error('Request identity requires plain JSON values');
|
|
38
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { mkdir, readFile, unlink, open } from 'node:fs/promises';
|
|
2
|
+
import { hostname } from 'node:os';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { canonical, hash, readJson, writeJson, writeBytes, syncDirectory } from './run-store-io.mjs';
|
|
6
|
+
export { hash, readJson, writeJson } from './run-store-io.mjs';
|
|
7
|
+
/** @typedef {{bytes: Buffer, metadata?: any}} Response */
|
|
8
|
+
/** @param {string} message @param {string} code */
|
|
9
|
+
function failure(message, code) { return Object.assign(new Error(message), { code }); }
|
|
10
|
+
/** @param {string} runDir */
|
|
11
|
+
export function createStore(runDir) {
|
|
12
|
+
const active = new Set();
|
|
13
|
+
return {
|
|
14
|
+
/** @param {string} id @param {unknown} request
|
|
15
|
+
* @param {(context: {persistMetadata: (metadata: any) => Promise<void>, callDir: string}) => Promise<Response>} callback
|
|
16
|
+
* @param {{prepare?: (context: {callDir: string}) => Promise<void>, recover?: (context: {callDir: string, metadata: any}) => Promise<Response | null>}} options */
|
|
17
|
+
async runCall(id, request, callback, options = {}) {
|
|
18
|
+
if (active.has(id)) throw failure(`Call ${id} is already active`, 'CALL_LOCKED');
|
|
19
|
+
active.add(id);
|
|
20
|
+
try {
|
|
21
|
+
const callDir = join(runDir, 'calls', hash(id));
|
|
22
|
+
await mkdir(callDir, { recursive: true });
|
|
23
|
+
await syncDirectory(dirname(runDir));
|
|
24
|
+
await syncDirectory(runDir);
|
|
25
|
+
await syncDirectory(join(runDir, 'calls'));
|
|
26
|
+
const statePath = join(callDir, 'state.json');
|
|
27
|
+
const requestHash = hash(canonical(request));
|
|
28
|
+
let state = await readJson(statePath);
|
|
29
|
+
if (state && (state.requestHash !== requestHash || state.id !== id)) throw failure(`Request changed for ${id}`, 'REQUEST_CHANGED');
|
|
30
|
+
if (!state) {
|
|
31
|
+
state = { id, requestHash, status: 'prepared', metadata: null };
|
|
32
|
+
await writeJson(join(callDir, 'request.json'), request);
|
|
33
|
+
await writeJson(statePath, state);
|
|
34
|
+
}
|
|
35
|
+
/** @param {Response} response @param {boolean} reused */
|
|
36
|
+
const receive = async (response, reused) => {
|
|
37
|
+
if (!Buffer.isBuffer(response.bytes)) throw new Error('Call response bytes must be a Buffer');
|
|
38
|
+
const metadata = response.metadata ?? state.metadata;
|
|
39
|
+
// Custody commits expected bytes first; raw publication is atomic and synced.
|
|
40
|
+
await writeJson(join(callDir, 'custody.json'), { requestHash, responseHash: hash(response.bytes), length: response.bytes.length, metadata });
|
|
41
|
+
await writeBytes(join(callDir, 'response.bin'), response.bytes);
|
|
42
|
+
state = { ...state, status: 'received', metadata, responseHash: hash(response.bytes) };
|
|
43
|
+
await writeJson(statePath, state);
|
|
44
|
+
return { bytes: response.bytes, metadata, reused };
|
|
45
|
+
};
|
|
46
|
+
if (state.status !== 'prepared') {
|
|
47
|
+
const custody = await readJson(join(callDir, 'custody.json'));
|
|
48
|
+
let bytes = null;
|
|
49
|
+
try { bytes = await readFile(join(callDir, 'response.bin')); }
|
|
50
|
+
catch (error) { if (/** @type {NodeJS.ErrnoException} */ (error).code !== 'ENOENT') throw error; }
|
|
51
|
+
if (bytes && custody?.requestHash === requestHash && custody.length === bytes.length && custody.responseHash === hash(bytes)) {
|
|
52
|
+
state = { ...state, status: 'received', metadata: custody.metadata, responseHash: custody.responseHash };
|
|
53
|
+
await writeJson(statePath, state);
|
|
54
|
+
return { bytes, metadata: custody.metadata, reused: true };
|
|
55
|
+
}
|
|
56
|
+
if (state.status === 'dispatched' && options.recover) {
|
|
57
|
+
const recovered = await options.recover({ callDir, metadata: state.metadata });
|
|
58
|
+
if (recovered) return await receive(recovered, true);
|
|
59
|
+
}
|
|
60
|
+
throw failure(`Call ${id} needs reconciliation; it will not be dispatched again`, 'NEEDS_RECONCILIATION');
|
|
61
|
+
}
|
|
62
|
+
await options.prepare?.({ callDir });
|
|
63
|
+
state.status = 'dispatched';
|
|
64
|
+
await writeJson(statePath, state);
|
|
65
|
+
try {
|
|
66
|
+
const response = await callback({ callDir, persistMetadata: async metadata => {
|
|
67
|
+
state = { ...state, metadata };
|
|
68
|
+
await writeJson(statePath, state);
|
|
69
|
+
} });
|
|
70
|
+
return await receive(response, false);
|
|
71
|
+
} catch (cause) {
|
|
72
|
+
// Provider errors can contain credentials or response bodies: retain the
|
|
73
|
+
// cause for programmatic inspection without copying it into the message.
|
|
74
|
+
throw Object.assign(failure(`Call ${id} failed after dispatch and needs reconciliation`, 'NEEDS_RECONCILIATION'), { cause });
|
|
75
|
+
}
|
|
76
|
+
} finally { active.delete(id); }
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** @template T @param {string} runDir @param {() => Promise<T>} callback @returns {Promise<T>} */
|
|
81
|
+
export async function withRunLock(runDir, callback) {
|
|
82
|
+
await mkdir(runDir, { recursive: true });
|
|
83
|
+
const path = join(runDir, 'run.lock');
|
|
84
|
+
const token = randomUUID();
|
|
85
|
+
const owner = { pid: process.pid, hostname: hostname(), token };
|
|
86
|
+
const acquire = async () => {
|
|
87
|
+
const handle = await open(path, 'wx', 0o600);
|
|
88
|
+
try { await handle.writeFile(JSON.stringify(owner)); await handle.sync(); } finally { await handle.close(); }
|
|
89
|
+
await syncDirectory(runDir);
|
|
90
|
+
};
|
|
91
|
+
try { await acquire(); }
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (/** @type {NodeJS.ErrnoException} */ (error).code !== 'EEXIST') throw error;
|
|
94
|
+
// A separate exclusive reclaim marker prevents competing stale-lock removals.
|
|
95
|
+
const guardPath = join(runDir, 'run.lock.reclaim');
|
|
96
|
+
let guard;
|
|
97
|
+
try { guard = await open(guardPath, 'wx', 0o600); }
|
|
98
|
+
catch { throw failure('Run is locked', 'RUN_LOCKED'); }
|
|
99
|
+
try {
|
|
100
|
+
let previous;
|
|
101
|
+
try { previous = await readJson(path); } catch { throw failure('Run lock is unreadable', 'RUN_LOCKED'); }
|
|
102
|
+
if (previous?.hostname !== hostname() || !Number.isInteger(previous.pid) || previous.pid <= 0) throw failure('Run is locked', 'RUN_LOCKED');
|
|
103
|
+
try { process.kill(previous.pid, 0); throw failure('Run is locked', 'RUN_LOCKED'); }
|
|
104
|
+
catch (probe) { if (/** @type {NodeJS.ErrnoException} */ (probe).code !== 'ESRCH') throw failure('Run is locked', 'RUN_LOCKED'); }
|
|
105
|
+
await unlink(path);
|
|
106
|
+
try { await acquire(); } catch { throw failure('Run is locked', 'RUN_LOCKED'); }
|
|
107
|
+
} finally { await guard.close(); await unlink(guardPath); }
|
|
108
|
+
}
|
|
109
|
+
try { return await callback(); }
|
|
110
|
+
finally {
|
|
111
|
+
const current = await readJson(path);
|
|
112
|
+
if (current?.token === token) { await unlink(path); await syncDirectory(runDir); }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { readFile, mkdir, writeFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import sharp from 'sharp';
|
|
5
|
+
import { createStore, withRunLock, hash, writeJson, readJson } from './run-store.mjs';
|
|
6
|
+
import { buildInitialPrompt, initialSchema } from './contracts/initial.mjs';
|
|
7
|
+
import { resolveContract } from './contract-repair.mjs';
|
|
8
|
+
import { executeImages } from './images/execute.mjs';
|
|
9
|
+
import { invokeCodex, recoverCodex } from './codex/invoke.mjs';
|
|
10
|
+
import { prepareHome } from './codex/home.mjs';
|
|
11
|
+
import { publishOutput } from './output.mjs';
|
|
12
|
+
import { pinRuntime } from './runtime-pin.mjs';
|
|
13
|
+
|
|
14
|
+
/** @typedef {{source?:string,runDir:string,authHome?:string,binary?:string,timeoutMs?:number,reasoning?:string,cleanAssets?:string,imageBackend?:string,env?:NodeJS.ProcessEnv,progress?:(message:string)=>void}} Options */
|
|
15
|
+
|
|
16
|
+
async function implementationHash() {
|
|
17
|
+
const root = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
/** @param {string} dir @returns {Promise<string[]>} */
|
|
19
|
+
async function visit(dir) {
|
|
20
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
21
|
+
const all = await Promise.all(entries.map(entry => entry.isDirectory() ? visit(path.join(dir, entry.name)) : [path.join(dir, entry.name)]));
|
|
22
|
+
return all.flat();
|
|
23
|
+
}
|
|
24
|
+
const files = (await visit(root)).sort();
|
|
25
|
+
files.push(path.join(root, '../package.json'));
|
|
26
|
+
return hash(JSON.stringify(await Promise.all(files.map(async file => [path.relative(root, file), hash(await readFile(file))]))));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Initialize immutable source and configuration without dispatching. @param {Options} options */
|
|
30
|
+
async function initialize(options) {
|
|
31
|
+
if (!options.source) throw new Error('convert requires source');
|
|
32
|
+
const bytes = await readFile(options.source); const metadata = await sharp(bytes).metadata();
|
|
33
|
+
if (!['png', 'jpeg', 'webp'].includes(metadata.format ?? '') || !metadata.width || !metadata.height) throw new Error('Source must be PNG, JPEG, or WebP');
|
|
34
|
+
const timeoutMs = options.timeoutMs ?? 1800000;
|
|
35
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 1800000) throw new Error('timeoutMs must be 1–1800000');
|
|
36
|
+
const source = { path: 'source.image', sha256: hash(bytes), width: metadata.width, height: metadata.height, mimeType: `image/${metadata.format}` };
|
|
37
|
+
const effort = options.reasoning ?? 'low';
|
|
38
|
+
if (!['low', 'medium'].includes(effort)) throw new Error('reasoning must be low or medium');
|
|
39
|
+
const imageBackend = options.imageBackend ?? 'external';
|
|
40
|
+
if (!['external', 'native'].includes(imageBackend)) throw new Error('imageBackend must be external or native');
|
|
41
|
+
const cleanAssets=options.cleanAssets??'enhance';
|
|
42
|
+
if(!['crop','enhance'].includes(cleanAssets))throw Error('cleanAssets must be crop or enhance');
|
|
43
|
+
const config = { cleanAssets, imageBackend, model: 'gpt-6-astra', effort, timeoutMs, imageEndpoint: options.env?.CODEX_CONVERSION_IMAGE_ENDPOINT ?? process.env.CODEX_CONVERSION_IMAGE_ENDPOINT ?? 'https://api.openai.com/v1/images/edits' };
|
|
44
|
+
const endpoint = new URL(config.imageEndpoint);
|
|
45
|
+
if (endpoint.username || endpoint.password || endpoint.search || !['https:', 'http:'].includes(endpoint.protocol)) throw new Error('Image endpoint must be an HTTP(S) URL without credentials or query parameters');
|
|
46
|
+
const identity = { schemaVersion: 1, source, config, implementationSha256: await implementationHash(), promptSha256: hash(buildInitialPrompt(source,config.cleanAssets)), schemaSha256: hash(JSON.stringify(initialSchema)) };
|
|
47
|
+
// Exclusive directory creation prevents overwriting any previous namespace.
|
|
48
|
+
await mkdir(options.runDir);
|
|
49
|
+
await pinRuntime(options.runDir);
|
|
50
|
+
await writeFile(path.join(options.runDir, source.path), bytes, { flag: 'wx' });
|
|
51
|
+
await writeJson(path.join(options.runDir, 'run.json'), { ...identity, createdAt: new Date().toISOString() });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @param {Options} options */
|
|
55
|
+
export async function convert(options) {
|
|
56
|
+
const runDir = path.resolve(options.runDir);
|
|
57
|
+
await initialize({ ...options, runDir });
|
|
58
|
+
return resume({ ...options, runDir });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Resume never repeats a paid dispatch whose outcome is unknown. @param {Options} options */
|
|
62
|
+
export async function resume(options) {
|
|
63
|
+
const runDir = path.resolve(options.runDir); const progress = options.progress ?? (() => {});
|
|
64
|
+
const resumedAt = Date.now();
|
|
65
|
+
return withRunLock(runDir, async () => {
|
|
66
|
+
try {
|
|
67
|
+
const identity = await readJson(path.join(runDir, 'run.json'));
|
|
68
|
+
if (identity.implementationSha256 !== await implementationHash()) throw new Error('Implementation changed; use a new run directory');
|
|
69
|
+
const sourcePath = path.join(runDir, identity.source.path);
|
|
70
|
+
if (hash(await readFile(sourcePath)) !== identity.source.sha256) throw new Error('Immutable source changed');
|
|
71
|
+
if (hash(buildInitialPrompt(identity.source,identity.config.cleanAssets)) !== identity.promptSha256 || hash(JSON.stringify(initialSchema)) !== identity.schemaSha256) throw new Error('Immutable planner configuration changed');
|
|
72
|
+
if (options.source && hash(await readFile(options.source)) !== identity.source.sha256) throw new Error('Source differs from immutable run input');
|
|
73
|
+
if (options.timeoutMs && options.timeoutMs !== identity.config.timeoutMs) throw new Error('Timeout differs from immutable run configuration');
|
|
74
|
+
if (options.reasoning !== undefined && options.reasoning !== identity.config.effort) throw new Error('Reasoning differs from immutable run configuration');
|
|
75
|
+
if(options.cleanAssets!==undefined && options.cleanAssets!==identity.config.cleanAssets)throw Error('Clean asset preference differs from immutable run configuration');
|
|
76
|
+
if (options.imageBackend !== undefined && options.imageBackend !== (identity.config.imageBackend ?? 'external')) throw new Error('Image backend differs from immutable run configuration');
|
|
77
|
+
const env = { ...(options.env ?? process.env) };
|
|
78
|
+
if (env.CODEX_CONVERSION_IMAGE_ENDPOINT && env.CODEX_CONVERSION_IMAGE_ENDPOINT !== identity.config.imageEndpoint) throw new Error('Image endpoint differs from immutable run configuration');
|
|
79
|
+
env.CODEX_CONVERSION_IMAGE_ENDPOINT = identity.config.imageEndpoint;
|
|
80
|
+
const store = createStore(runDir);
|
|
81
|
+
progress('Resolving planner response');
|
|
82
|
+
const prompt = buildInitialPrompt(identity.source,identity.config.cleanAssets);
|
|
83
|
+
let preparedHome;
|
|
84
|
+
const planner = await store.runCall('planner', { source: identity.source.sha256, prompt: identity.promptSha256, schema: identity.schemaSha256, model: identity.config.model, effort: identity.config.effort }, async ({ callDir }) => {
|
|
85
|
+
if (!options.authHome) throw new Error('authHome is required for a new planner call');
|
|
86
|
+
const schemaPath = path.join(callDir, 'schema.json'); await writeJson(schemaPath, initialSchema);
|
|
87
|
+
await writeFile(path.join(callDir, 'prompt.txt'), prompt);
|
|
88
|
+
return invokeCodex({ callDir, runDir, authHome: path.resolve(options.authHome), binary: options.binary, sourcePath, schemaPath, prompt, timeoutMs: identity.config.timeoutMs, reasoning: identity.config.effort, preparedHome });
|
|
89
|
+
}, { recover: recoverCodex, prepare: async () => {
|
|
90
|
+
if (!options.authHome) throw new Error('authHome is required for a new planner call');
|
|
91
|
+
preparedHome = (await prepareHome({ authHome: path.resolve(options.authHome), binary: options.binary ?? 'codex', runDir, reasoning: identity.config.effort })).home;
|
|
92
|
+
} });
|
|
93
|
+
const contract = await resolveContract({planner,source:identity.source,sourcePath,runDir,store,authHome:options.authHome?path.resolve(options.authHome):undefined,binary:options.binary,timeoutMs:identity.config.timeoutMs});
|
|
94
|
+
if(!contract.ok){const result={schemaVersion:1,status:'failed',error:{message:'Contract remains unusable after one bounded repair',issues:contract.issues},receipts:'calls/',usage:{codex:planner.metadata,contractRepair:contract.repair?.metadata}};await writeJson(path.join(runDir,'result.json'),result);return result;}
|
|
95
|
+
const {plan,normalized}=contract;
|
|
96
|
+
await writeJson(path.join(runDir, 'plan.json'), plan);
|
|
97
|
+
await writeJson(path.join(runDir, 'normalization.json'), { recoveries: contract.recoveries, externalDependencies: normalized.externalDependencies });
|
|
98
|
+
progress(`Resolving ${plan.groups.length} ${identity.config.imageBackend ?? 'external'} image groups`);
|
|
99
|
+
const images = await executeImages({ plan, sourcePath, runDir, store, env, imageBackend: identity.config.imageBackend ?? 'external', codexBinary: options.binary, codexHome: options.authHome, nativeModel: 'gpt-6-astra', timeoutMs: identity.config.timeoutMs, progress: event => progress(JSON.stringify(event)) });
|
|
100
|
+
const output = await publishOutput({ runDir, normalized, assets: images.assets, source: identity.source, plan });
|
|
101
|
+
const result = { schemaVersion: 1, status: 'complete', output, receipts: 'calls/', uncertainties: plan.uncertainties, timing: { createdAt: identity.createdAt, completedAt: new Date().toISOString(), elapsedSinceCreationMs: Date.now() - Date.parse(identity.createdAt), thisResumeMs: Date.now() - resumedAt }, usage: { codex: planner.metadata, contractRepair:contract.repair?.metadata??null, externalImages: images.usage } };
|
|
102
|
+
try{await writeJson(path.join(runDir, 'result.json'), result);}catch(error){return {...result,diagnostics:[{code:'result-journal-failed',message:'Valid published artifact retained; result receipt could not be saved'}]};}return result;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const result = { schemaVersion: 1, status: error.code === 'NEEDS_RECONCILIATION' ? 'needs-reconciliation' : 'failed', error: { message: error.message }, receipts: 'calls/' };
|
|
105
|
+
await writeJson(path.join(runDir, 'result.json'), result); return result;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {open} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {syncDirectory} from './run-store-io.mjs';
|
|
4
|
+
/** Pin the bootstrap's immutable cached payload before any model dispatch. */
|
|
5
|
+
export async function pinRuntime(runDir,env=process.env) {
|
|
6
|
+
const key=env.TWELVE_UI_CODEX_RUNTIME_KEY;
|
|
7
|
+
if(key===undefined)return;
|
|
8
|
+
if(!/^[a-f0-9]{64}$/.test(key))throw Error('Invalid bootstrap runtime key');
|
|
9
|
+
const handle=await open(path.join(runDir,'.12ui-runtime.json'),'wx',0o600);
|
|
10
|
+
try{await handle.writeFile(JSON.stringify({schemaVersion:1,key})+'\n');await handle.sync();}finally{await handle.close();}
|
|
11
|
+
await syncDirectory(runDir);
|
|
12
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"properties": {
|
|
4
|
+
"html": {
|
|
5
|
+
"type": "string"
|
|
6
|
+
},
|
|
7
|
+
"assets": {
|
|
8
|
+
"type": "array",
|
|
9
|
+
"items": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"id": {
|
|
13
|
+
"type": "string"
|
|
14
|
+
},
|
|
15
|
+
"source_bbox_px": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"items": {
|
|
18
|
+
"type": "integer"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"background": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"enum": [
|
|
24
|
+
"opaque",
|
|
25
|
+
"transparent"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"request_group": {
|
|
29
|
+
"type": "string"
|
|
30
|
+
},
|
|
31
|
+
"operation": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"enum": [
|
|
34
|
+
"preserve-source",
|
|
35
|
+
"remove-named-overlays",
|
|
36
|
+
"crop-source"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"desired_owner": {
|
|
40
|
+
"type": "string"
|
|
41
|
+
},
|
|
42
|
+
"named_removals": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"items": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"properties": {
|
|
47
|
+
"source_bbox_px": {
|
|
48
|
+
"type": "array",
|
|
49
|
+
"items": {
|
|
50
|
+
"type": "integer"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"text_or_element": {
|
|
54
|
+
"type": "string"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"required": [
|
|
58
|
+
"source_bbox_px",
|
|
59
|
+
"text_or_element"
|
|
60
|
+
],
|
|
61
|
+
"additionalProperties": false
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"required": [
|
|
66
|
+
"id",
|
|
67
|
+
"source_bbox_px",
|
|
68
|
+
"background",
|
|
69
|
+
"request_group",
|
|
70
|
+
"operation",
|
|
71
|
+
"desired_owner",
|
|
72
|
+
"named_removals"
|
|
73
|
+
],
|
|
74
|
+
"additionalProperties": false
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"uncertainties": {
|
|
78
|
+
"type": "array",
|
|
79
|
+
"items": {
|
|
80
|
+
"type": "string"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"required": [
|
|
85
|
+
"html",
|
|
86
|
+
"assets",
|
|
87
|
+
"uncertainties"
|
|
88
|
+
],
|
|
89
|
+
"additionalProperties": false
|
|
90
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
This is a schema-only response task. Inspect the attached source image directly. Do not call any tool, run commands, read files, browse, generate images, or delegate. Return the requested JSON as your final response. Asset operations are performed outside this CLI invocation by the experiment executor.
|
|
2
|
+
|
|
3
|
+
Reconstruct this source screenshot in ONE response as complete semantic responsive HTML/CSS/SVG plus source-registered artwork declarations. Return only JSON {html,assets,uncertainties}. There is no separate planner or later code-model pass. The executor will execute declared artwork operations in parallel, deliver each exact source owner, and substitute asset:ID references into your HTML. Complete this first pass without relying on any later repair.
|
|
4
|
+
|
|
5
|
+
The original source is the sole authority for visible words/data, typography, placement, geometry, material ownership and controls. Include only confidently readable exact source text/data. Do not invent plausible footer labels, microcopy, dates, chart values or continuation beyond screenshot edges. Record ambiguity in uncertainties. Noninteractive text must be meaningful editable DOM with contenteditable=true, including readable depicted interface labels when separated from a photograph. Navigational anchors with href must remain contenteditable=false, including inside editable containers, so normal clicks navigate. Preserve inline label markup; an editable child span may hold the label when needed. Reconstructible chart/icon geometry remains SVG/CSS; no hidden UI in a convenient full-page raster. Do not invent a working backend.
|
|
6
|
+
|
|
7
|
+
Use ONE source-coordinate definition for each asset: id, source_bbox_px:[left,top,width,height] in original source integer pixels, background opaque/transparent, request_group, operation, desired_owner, named_removals. The bbox is the exact source-owned artwork including edge material/shadow it owns. Do not declare separate output widths/heights or imagined aspect ratios. Asset native display size/position in your HTML uses this original source frame; executor's2x pixel density is not CSS viewport width. Each asset must appear in HTML using its exact asset:ID in img or CSS url.
|
|
8
|
+
|
|
9
|
+
Declare0–12 actual source-owned art responsibilities, never a quota. Keep irreducible photographs, illustration and distinctive material appearance in art. CSS owns panel geometry, clipping, edges/shadows, text and simple UI. Preserve meaningful subtle material such as paper texture with an intrinsic fill rather than flattening it to a bare rectangle. Reuse only visibly shared material through explicitly intentional owner clipping; do not consolidate unrelated UI into a sheet. A coherent photograph stays coherent. The model declares the desired intrinsic owner and actual named overlays to remove, using exact original-source boxes. Intrinsic scene writing is preserved unless explicitly assigned to editable DOM; never paint a label in both owners.
|
|
10
|
+
|
|
11
|
+
For AI image operations (preserve-source and remove-named-overlays), the image executor will expand a narrow/short asset bbox using the minimum legal rectangle of ORIGINAL neighboring source pixels, ask for the entire context at exact2x, then crop exactly the declared owner coordinates. No padding, stretching, localization or recentering. Your code must therefore reserve the exact native frame now; do not expect generic square/landscape outputs or adjust layout later. Group a depicted-text overlay with its photo owner so moving that group preserves registration. Artwork bytes are not yet attached: use faithful source frames as the contract, not an invented composition.
|
|
12
|
+
|
|
13
|
+
At native source width retain source layout/typographic metrics and z-order. At390px use sensible normal-flow grid/flex reflow retaining every meaningful navigation/control/text/art owner. Native details/summary or keyboard-accessible scroll groups are allowed; no hidden sidebar content, scaled fixed canvas, oversized blank strips or fixed-height remnants. Headings and body text must wrap naturally. Do not use nowrap, fixed heights or mandatory manual linebreaks for heading fit. Preserve spaces between spans. A longer replacement heading/value must reflow within the page without overflow/collision/ghosttext. Keep shrinking grid/flex minimums and appropriate overflow-wrap.
|
|
14
|
+
|
|
15
|
+
Keep complete visible bar series, curves, legend labels and map silhouette; approximate unreadable underlying data only with declared uncertainty, never delete visible marks. Apply fill/stroke on actual SVG geometry. Use reusable concise CSS/SVG, valid complete HTML document with inline CSS, system or Google Fonts CSS only. No external raster images, scripts, external JS, canvas, iframe or base64. All substantive styling/content comes from source evidence, not a new design.
|
|
16
|
+
|
|
17
|
+
Each asset has request_group: a shared identifier only for disjoint owners with compatible ground/edit instructions that should share one original-source context image. Their positions are never rearranged. Nearby material thumbnails may share a request and remain separate delivered assets. Otherwise use the asset id. For AI image operations, the executor validates non-overlap and common background, computes a legal source-contained context around the union, and crops each declared owner at exact2x coordinates.
|
|
18
|
+
|
|
19
|
+
Preserve source-visible navigation at native width. If you introduce a narrow-screen disclosure, its initial state and CSS must leave the source navigation visible and operable at desktop width as well. A hidden summary around a closed details element does not expose its contents. Check both initial desktop and narrow states in the HTML/CSS you return. Every source control must remain reachable by keyboard; do not depend on scripts or a later finalize pass.
|
|
20
|
+
|
|
21
|
+
Each asset explicitly chooses operation. Use preserve-source when its delivered rectangle already contains only the desired intrinsic material/artwork, without any page text/control to remove. Its named_removals must be empty. The executor then uses only the tested whole-context source-preserving upscale prompt, leaving ALL neighboring words/captions and gaps intact outside the delivered owner. Do not demand reconstruction, new framing, or exclusion of neighboring content for clean owners.
|
|
22
|
+
Use remove-named-overlays only when editable page overlays actually intersect the owner. desired_owner is a compact positive description of the underlying material/artwork to retain. named_removals lists each actual source text/control/overlap with source_bbox_px and text_or_element. Every removal must intersect this asset; only that intersection is edited. Preserve intrinsic scene detail/writing unless specifically assigned to independent DOM. Never ask to remove an overlay that the HTML does not recreate once. Group only disjoint owners with identical operation and background; compatible clean material thumbnails may share original context, never repack them. The executor rejects unsupported geometry or request counts before any image dispatch.
|