Repository navigation
Expand file tree
/
Copy pathqdl-v1-contract.js
More file actions
79 lines (79 loc) · 15.9 KB
/
Copy pathqdl-v1-contract.js
File metadata and controls
79 lines (79 loc) · 15.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
(function(root){
'use strict';
const T=typeof module!=='undefined'&&module.exports?require('./qdl-v1-types.js'):root.QDLV1Types;
const K=typeof module!=='undefined'&&module.exports?require('./kernels.js'):root.QuinelingKernels;
const V=typeof module!=='undefined'&&module.exports?require('./qdl-v1-kernels.js'):root.QDLV1Kernels;
const ARITY=Object.freeze({...K.ARITY,...V.ARITY}),bad=new Set(['__proto__','prototype','constructor']),own=(x,k)=>Object.hasOwn(x,k);
const B={kind:'boolean'},S={kind:'string'},Z={kind:'null'},N=(unit='one')=>({kind:'number',unit}),A=element=>({kind:'array',element}),O=element=>({kind:'optional',element}),R=fields=>({kind:'record',fields});
function fail(code,path,message){const e=new Error(message);e.code=code;e.path=path;throw e;}
function check(ok,code,path,message){if(!ok)fail(code,path,message);}
function closed(x,required,optional,path){check(x!==null&&typeof x==='object'&&!Array.isArray(x),'schema',path,'Expected a record');for(const k of required)check(own(x,k),'missing',path+'.'+k,'Missing field '+k);for(const k of Object.keys(x))check(required.includes(k)||optional.includes(k),'unknown-field',path+'.'+k,'Unknown field '+k);}
function safe(s,path){check(typeof s==='string'&&s.length>0&&Array.from(s).length<=64&&!bad.has(s)&&!s.includes('.'),'parameter',path,'Expected a safe own field name');return s;}
function shape(t){switch(t.kind){case 'number':return N(t.unit);case 'boolean':case 'string':case 'null':return {kind:t.kind};case 'array':case 'optional':return {kind:t.kind,element:shape(t.element)};case 'record':return R(Object.fromEntries(Object.entries(t.fields).map(([k,v])=>[k,shape(v)])));}}
function equivalent(a,b){return T.canonical(shape(a))===T.canonical(shape(b));}
function same(a,b,path){check(equivalent(a,b),'unit-type',path,'Types and symbolic units must agree');}
function kind(t,k,path){check(t&&t.kind===k,'type',path,'Expected '+k+' input');return t;}
function numeric(t,path){return kind(t,'number',path);}
function field(t,key,path){kind(t,'record',path);check(own(t.fields,key),'type',path,'Missing record field '+key);return t.fields[key];}
function pathType(t,path,where,recordsOnly=false){check(typeof path==='string'&&path.length>0,'parameter',where,'Expected nonempty own-property path');const parts=path.split('.');check(parts.length<=8,'limit',where,'Path exceeds 8 segments');for(const p of parts){safe(p,where);if(t.kind==='record')t=field(t,p,where);else if(!recordsOnly&&t.kind==='array'){check(/^(0|[1-9][0-9]*)$/.test(p)&&Number(p)<=511,'parameter',where,'Array index must be 0–511');t=t.element;}else fail('type',where,'Path must traverse required own properties');}return t;}
function row(t,fields,path){kind(t,'record',path);check(Object.keys(t.fields).length===fields.length&&fields.every(k=>own(t.fields,k)),'type',path,'Record fields must exactly match '+fields.join(','));return t.fields;}
function merge(a,b,path){if(equivalent(a,b))return shape(a);if(a.kind==='null')return b.kind==='optional'?shape(b):O(shape(b));if(b.kind==='null')return a.kind==='optional'?shape(a):O(shape(a));if(a.kind==='optional'&&equivalent(a.element,b))return shape(a);if(b.kind==='optional'&&equivalent(b.element,a))return shape(b);fail('type',path,'Choice alternatives must agree, allowing explicit null');}
const operators=['eq','ne','gt','gte','lt','lte'];
function compareTypes(a,b,operator,path){same(a,b,path);check(operators.includes(operator),'parameter',path,'Unknown comparison operator');if(!['eq','ne'].includes(operator))check(['number','string'].includes(a.kind),'type',path,'Ordering requires numbers or strings');}
const evidenceRow=R({source:S,claim:S,value:B}),freshRow=R({id:S,source:S,claim:S,value:O(B),kind:S,observedAt:N('tick'),revision:N('revision')}),receiptRow=R({id:S,operation:S,attempt:S,sequence:N('revision'),status:S,units:N('item')});
function inferNode(node,inputTypes){
T.finiteJSON(node);closed(node,['id','op','inputs','params'],['type'],'$.node');check(typeof node.id==='string'&&/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(node.id)&&!bad.has(node.id),'id','$.node.id','Invalid node ID');check(typeof node.op==='string'&&own(ARITY,node.op),'unsupported','$.node.op','Unknown opcode');check(Array.isArray(node.inputs)&&node.inputs.length<=16&&(ARITY[node.op]===-1||node.inputs.length===ARITY[node.op]),'ports','$.node.inputs','Wrong ordered port count');check(node.inputs.every(x=>typeof x==='string'&&/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(x)&&!bad.has(x)),'reference','$.node.inputs','Expected producer node IDs');check(Array.isArray(inputTypes)&&inputTypes.length===node.inputs.length,'ports','$.node.inputs','Input type count must match ports');
const ts=inputTypes.map((x,i)=>T.normalize(x,'$.node.inputs.'+i)),declared=own(node,'type')?T.normalize(node.type,'$.node.type'):null,p=node.params,w='$.node.params',a=ts[0],b=ts[1];
const required={literal:['value'],input:['name'],map:['kind'],filter:['operator','value'],compare:['operator','value'],get:['path'],clamp:['min','max'],action:['allowed','action'],report:['labels'],bfs:['start','goal'],consensus:['required'],retry:['maxAttempts'],evidence:['claim'],arithmetic:['kind'],compareValues:['operator'],select:['keys','order','default'],evidenceFresh:['allowedKinds']}[node.op]||[];
const optional={map:['factor'],sort:['key','descending'],dedupe:['key'],filter:['key']}[node.op]||[];closed(p,required,optional,w);
const element=()=>kind(a,'array','$.node.inputs.0').element;
const num=x=>numeric(x,'$.node.inputs');
function normalizedUnit(text){try{return T.unit(text);}catch(e){fail(e.code||'unit',w+'.kind',e.message);}}
const scalar=x=>check(['number','string'].includes(x.kind),'type','$.node.inputs','Expected numeric or string key');
function authored(value,t,path){T.assert(value,t,path);}
switch(node.op){
case 'literal':check(declared,'missing','$.node.type','Literal inference requires a declared type');authored(p.value,declared,w+'.value');return declared;
case 'input':check(declared,'missing','$.node.type','Input inference requires a declared type');safe(p.name,w+'.name');check(/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(p.name),'name',w+'.name','Input names must be ASCII identifiers');return declared;
case 'sum':case 'mean':case 'min':case 'max':return shape(num(element()));
case 'weightedMean':{const e=num(element()),weights=num(kind(b,'array','$.node.inputs.1').element);same(weights,N(),w);return shape(e);}
case 'length':check(a.kind==='array'||a.kind==='string','type','$.node.inputs.0','length requires array or string');return N('count');
case 'map':{const e=num(element());check(['square','multiply'].includes(p.kind),'parameter',w+'.kind','Unknown map kind');if(p.kind==='multiply'){check(own(p,'factor')&&typeof p.factor==='number'&&Number.isFinite(p.factor),'parameter',w+'.factor','multiply requires finite factor');}else check(!own(p,'factor'),'unknown-field',w+'.factor','square has no factor');return A(N(p.kind==='square'?normalizedUnit(e.unit+'*'+e.unit):e.unit));}
case 'sort':{const e=element();if(own(p,'descending'))check(typeof p.descending==='boolean','parameter',w+'.descending','Expected Boolean');scalar(own(p,'key')?pathType(e,p.key,w+'.key'):e);return shape(a);}
case 'dedupe':{const e=element();if(own(p,'key'))pathType(e,p.key,w+'.key');return shape(a);}
case 'filter':{const e=element(),t=own(p,'key')?pathType(e,p.key,w+'.key'):e;authored(p.value,shape(t),w+'.value');compareTypes(t,t,p.operator,w+'.operator');return shape(a);}
case 'compare':authored(p.value,shape(a),w+'.value');compareTypes(a,a,p.operator,w+'.operator');return B;
case 'compareValues':compareTypes(a,b,p.operator,w+'.operator');return B;
case 'choose':kind(a,'boolean','$.node.inputs.0');return merge(b,ts[2],'$.node.inputs');
case 'get':kind(a,'record','$.node.inputs.0');return shape(pathType(a,p.path,w+'.path'));
case 'clamp':num(a);check(typeof p.min==='number'&&Number.isFinite(p.min)&&typeof p.max==='number'&&Number.isFinite(p.max)&&p.min<=p.max,'parameter',w,'Expected finite ordered clamp bounds');return shape(a);
case 'budget':num(a);num(b);same(a,b,'$.node.inputs');return R({allocated:shape(a),remaining:shape(a)});
case 'action':kind(a,'boolean','$.node.inputs.0');check(typeof p.allowed==='boolean','parameter',w+'.allowed','Expected explicit simulation permission');check(typeof p.action==='string'&&p.action.length>0&&Array.from(p.action).length<=120,'parameter',w+'.action','Expected action label');return R({status:S,action:S,payload:shape(b)});
case 'report':check(Array.isArray(p.labels)&&p.labels.length===ts.length&&new Set(p.labels).size===p.labels.length,'parameter',w+'.labels','Unique labels must match ports');p.labels.forEach((x,i)=>safe(x,w+'.labels.'+i));return R(Object.fromEntries(p.labels.map((label,i)=>[label,shape(ts[i])])));
case 'bfs':kind(a,'record','$.node.inputs.0');for(const t of Object.values(a.fields))same(t,A(S),'$.node.inputs.0');same(b,A(S),'$.node.inputs.1');check(typeof p.start==='string'&&p.start.length>0&&typeof p.goal==='string'&&p.goal.length>0,'parameter',w,'Expected nonempty route endpoints');return R({found:B,path:A(S),distance:O(N('edge'))});
case 'allocate':{num(a);const f=row(kind(b,'array','$.node.inputs.1').element,['id','amount'],'$.node.inputs.1');same(f.id,S,'$.node.inputs.1');same(f.amount,a,'$.node.inputs');return R({grants:A(R({id:S,requested:shape(a),granted:shape(a)})),remaining:shape(a)});}
case 'schedule':{const f=row(element(),['id','depends','duration'],'$.node.inputs.0');same(f.id,S,'$.node.inputs.0');same(f.depends,A(S),'$.node.inputs.0');num(f.duration);const d=shape(f.duration);return R({order:A(S),jobs:A(R({id:S,start:d,end:d})),makespan:d});}
case 'consensus':same(element(),R({source:S,choice:S}),'$.node.inputs.0');check(Number.isSafeInteger(p.required)&&p.required>0,'parameter',w+'.required','Expected positive safe integer support');return R({choice:O(S),support:N('count'),accepted:B,uniqueSources:N('count')});
case 'retry':same(element(),S,'$.node.inputs.0');check(Number.isInteger(p.maxAttempts)&&p.maxAttempts>=1&&p.maxAttempts<=8,'parameter',w+'.maxAttempts','Retry bound must be 1–8');return R({status:S,attempts:N('count'),history:A(S)});
case 'evidence':same(element(),evidenceRow,'$.node.inputs.0');check(typeof p.claim==='string'&&p.claim.length>0,'parameter',w+'.claim','Stable evidence needs one nonempty claim');return R({state:S,support:N('count'),refute:N('count'),sources:N('count')});
case 'arithmetic':{num(a);num(b);check(['add','sub','mul','div','floorDiv','min','max'].includes(p.kind),'parameter',w+'.kind','Unknown arithmetic kind');if(['add','sub','min','max'].includes(p.kind)){same(a,b,'$.node.inputs');return shape(a);}const inverse=p.kind==='div'||p.kind==='floorDiv',rhs=inverse?b.unit.split('*').map(term=>{if(term==='one')return 'one';const [base,e]=term.split('^');return base+'^'+(-(e===undefined?1:Number(e)));}).join('*'):b.unit;return N(normalizedUnit(a.unit+'*'+rhs));}
case 'all':same(a,A(B),'$.node.inputs.0');return B;
case 'select':{const e=element();kind(e,'record','$.node.inputs.0');kind(b,'record','$.node.inputs.1');check(Array.isArray(p.keys)&&p.keys.length<=512&&new Set(p.keys).size===p.keys.length,'parameter',w+'.keys','Expected unique query field names');p.keys.forEach((k,i)=>safe(k,w+'.keys.'+i));check(Object.keys(b.fields).length===p.keys.length&&p.keys.every(k=>own(b.fields,k)),'type','$.node.inputs.1','Query fields must exactly match keys');for(const k of p.keys)same(field(e,k,w+'.keys'),b.fields[k],'$.node.inputs');check(Array.isArray(p.order)&&p.order.length<=512,'parameter',w+'.order','Expected ordered sort rules');const seen=new Set();p.order.forEach((o,i)=>{const q=w+'.order.'+i;closed(o,['path','descending'],[],q);check(typeof o.descending==='boolean','parameter',q+'.descending','Expected Boolean');scalar(pathType(e,o.path,q+'.path',true));check(!seen.has(o.path),'parameter',q+'.path','Repeated sort path');seen.add(o.path);});authored(p.default,e,w+'.default');return R({found:B,value:shape(e),index:O(N('count'))});}
case 'evidenceFresh':same(a,A(freshRow),'$.node.inputs.0');same(b,S,'$.node.inputs.1');same(ts[2],R({now:N('tick'),maxAge:N('tick'),minRevision:N('revision')}),'$.node.inputs.2');check(Array.isArray(p.allowedKinds)&&p.allowedKinds.every(k=>['observation','testimony','inference'].includes(k))&&new Set(p.allowedKinds).size===p.allowedKinds.length,'parameter',w+'.allowedKinds','Expected unique admitted evidence kinds');return R({state:S,support:N('count'),refute:N('count'),sources:N('count'),sourceConflicts:A(S),used:A(freshRow),skipped:A(R({record:freshRow,reason:S}))});
case 'reconcile':same(a,A(receiptRow),'$.node.inputs.0');same(b,R({operation:S,requested:N('item'),maxAttempts:N('count')}),'$.node.inputs.1');return R({state:S,confirmedUnits:N('item'),confirmedAttempts:N('count'),failedAttempts:N('count'),pendingAttempts:N('count'),unknownAttempts:N('count'),attempts:N('count'),mayRetry:B});
default:fail('unsupported','$.node.op','Unsupported opcode');
}
}
function validate(task){
T.finiteJSON(task);closed(task,['format','version','nodes','outputs'],[],'$');check(task.format==='qdl-task'&&task.version===1,'format','$','Expected qdl-task version 1');check(Array.isArray(task.nodes)&&task.nodes.length>0&&task.nodes.length<=64,'limit','$.nodes','Expected 1–64 nodes');check(Array.isArray(task.outputs)&&task.outputs.length>0&&task.outputs.length<=16&&new Set(task.outputs).size===task.outputs.length,'ports','$.outputs','Expected 1–16 unique outputs');
const detached=JSON.parse(T.canonical(task)),nodes=new Map(),indices=new Map(),ports=[];
detached.nodes.forEach((n,i)=>{const p='$.nodes.'+i;closed(n,['id','op','inputs','params','type'],[],p);check(typeof n.id==='string'&&/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(n.id)&&!bad.has(n.id),'id',p+'.id','Invalid node ID');check(!nodes.has(n.id),'id',p+'.id','Duplicate node ID');check(typeof n.op==='string'&&own(ARITY,n.op),'unsupported',p+'.op','Unknown opcode');check(Array.isArray(n.inputs)&&n.inputs.length<=16&&(ARITY[n.op]===-1||n.inputs.length===ARITY[n.op]),'ports',p+'.inputs','Wrong ordered port count');n.type=T.normalize(n.type,p+'.type');nodes.set(n.id,n);indices.set(n.id,i);});
for(const n of detached.nodes)for(const id of n.inputs)check(typeof id==='string'&&nodes.has(id),'reference','$.nodes.'+indices.get(n.id)+'.inputs','Unknown producer');for(const id of detached.outputs)check(typeof id==='string'&&nodes.has(id),'reference','$.outputs','Unknown output');
const ready=new Set(),pending=[...detached.nodes],order=[];
while(pending.length){const index=pending.findIndex(n=>n.inputs.every(id=>ready.has(id)));check(index>=0,'cycle','$.nodes','Dependency cycle');const [n]=pending.splice(index,1);let inferred;try{inferred=inferNode(n,n.inputs.map(id=>nodes.get(id).type));}catch(e){if(e.code&&e.path){e.path=e.path.replace(/^\$\.node(?=\.|$)/,'$.nodes.'+indices.get(n.id));throw e;}fail('type','$.nodes.'+indices.get(n.id),e.message);}same(inferred,n.type,'$.nodes.'+indices.get(n.id)+'.type');if(n.op==='input'){check(!ports.some(p=>p.name===n.params.name),'id','$.nodes.'+indices.get(n.id)+'.params.name','Duplicate runtime input name');ports.push({name:n.params.name,nodeId:n.id,type:n.type});}ready.add(n.id);order.push(n.id);}
const reachable=new Set();function visit(id){if(reachable.has(id))return;reachable.add(id);nodes.get(id).inputs.forEach(visit);}detached.outputs.forEach(visit);check(reachable.size===nodes.size,'disconnected','$.nodes','Every node must contribute to an output');
function hasAction(id,seen=new Set()){if(seen.has(id))return false;seen.add(id);const n=nodes.get(id);return n.op==='action'||n.inputs.some(x=>hasAction(x,seen));}
for(const n of detached.nodes){const p='$.nodes.'+indices.get(n.id);if(n.op==='action')check(!n.inputs.some(id=>hasAction(id)),'effect-cone',p+'.inputs','Action guard and payload ancestors must be pure');if(n.op==='choose')check(!n.inputs.slice(1).some(id=>hasAction(id)),'eager-effect',p+'.inputs','Eager choose alternatives cannot contain actions');}
T.finiteJSON(detached);return {task:detached,order,ports};
}
const api=Object.freeze({validate,inferNode});if(typeof module!=='undefined'&&module.exports)module.exports=api;root.QDLV1Contract=api;
})(typeof globalThis!=='undefined'?globalThis:this);