arkaik 0.3.0 → 0.4.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/dist/assets/skill/references/schema.md +1 -0
- package/dist/assets/skill/scripts/validate-bundle.js +2 -2
- package/dist/index.js +36 -10
- package/dist/io.js +30 -0
- package/package.json +1 -1
- package/src/io.ts +1 -0
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Usage: node validate-bundle.js <path-to-bundle.json>
|
|
12
12
|
*/
|
|
13
|
-
"use strict";var U=require("node:fs"),Q=require("node:path");var H=["flow","view","data-model","api-endpoint","acceptance","decision"],K=["idea","discovery","backlog","development","releasing","live","archived"],B=["web","ios","android"],ne=["composes","calls","displays","queries","covers","supersedes","generates","impacts"],me={composes:[["flow","view"],["flow","flow"],["view","flow"],["view","view"]],calls:[["view","api-endpoint"],["flow","api-endpoint"],["api-endpoint","api-endpoint"],["api-endpoint","view"]],displays:[["view","data-model"]],queries:[["api-endpoint","data-model"]],covers:[["acceptance","view"],["acceptance","flow"]],supersedes:[["decision","decision"]],generates:[["decision","acceptance"]],impacts:[["decision","flow"],["decision","view"],["decision","data-model"],["decision","api-endpoint"]]};var he=["saves-time","simplifies","makes-money","reduces-risk","organizes","integrates","connects","reduces-effort","avoids-hassles","reduces-cost","quality","variety","sensory-appeal","informs","reduces-anxiety","rewards-me","nostalgia","design-aesthetics","badge-value","wellness","therapeutic-value","fun-entertainment","attractiveness","provides-access","provides-hope","self-actualization","motivation","heirloom","affiliation-belonging","self-transcendence"];var _e={flow:"F-",view:"V-","data-model":"DM-","api-endpoint":"API-",acceptance:"AC-",decision:"DEC-"};var De=["proposed","approved","enacted","rejected","deprecated","superseded"];function ve(f){switch(f){case"proposed":return"discovery";case"approved":return"backlog";case"enacted":return"live";case"rejected":case"deprecated":case"superseded":return"archived"}}function we(f){let p=f.metadata?.decision_status;return De.includes(p)?p:"proposed"}var ie={prioritized:"backlog",blocked:"development"};function se(f){return K.includes(f)?f:ie[f]}var Be={...ie},Ye={backlog:"idea",...ie};function Ne(f){let p=t=>typeof t=="string"?t:"";return[...f].sort((t,s)=>{let $=p(t.ts),w=p(s.ts);if($<w)return-1;if($>w)return 1;let m=p(t.id),I=p(s.id);return m<I?-1:m>I?1:0})}function re(f){let p=[],t=[];return f.split(/\r?\n/).forEach(($,w)=>{let m=w+1;if($.trim()==="")return;let I;try{I=JSON.parse($)}catch(L){t.push({line:m,rule:"journal-line-parse",message:`Line ${m}: not valid JSON \u2014 ${L.message}`,severity:"error"});return}if(typeof I!="object"||I===null||Array.isArray(I)){t.push({line:m,rule:"journal-line-shape",message:`Line ${m}: each journal line must be a single JSON event object.`,severity:"error"});return}let T=I,E=[];if(typeof T.id!="string"&&E.push("id"),typeof T.ts!="string"&&E.push("ts"),typeof T.type!="string"&&E.push("type"),E.length>0){t.push({line:m,rule:"journal-line-shape",message:`Line ${m}: event is missing required envelope field(s): ${E.join(", ")}.`,severity:"error"});return}p.push(T)}),{events:p,findings:t}}var Te={"node.updated":["node_id"],"node.status_changed":["node_id"],"decision.status_changed":["node_id"],"node.deleted":["node_id"],"edge.added":["source_id","target_id"],"ref.added":["node_id"],"ref.removed":["node_id"],"ref.status_changed":["node_id"],"idea.proposed":["node_id"],"request.filed":["node_id"]};function Fe(f,p){return f===p?!0:typeof p!="string"?!1:f==="backlog"&&p==="idea"?!0:(se(f)??f)===(se(p)??p)}function Se(f){let p=[],t=f.journal;if(t===void 0)return p;if(!Array.isArray(t))return p.push({path:"journal",rule:"journal-shape",message:"journal must be an array of events when present.",severity:"error"}),p;if(t.length===0)return p;let s=c=>typeof c=="string"?c:void 0,$=Array.isArray(f.nodes)?f.nodes:[],w=Array.isArray(f.edges)?f.edges:[],m=new Map,I=new Map;for(let c of $){let g=s(c?.id);if(g!==void 0){m.set(g,c.status);let _=c.metadata,x=_&&typeof _=="object"&&!Array.isArray(_)?_.decision_status:void 0;I.set(g,x??"proposed")}}let T=new Set;for(let c of w){let g=s(c?.id);g!==void 0&&T.add(g)}let E=[];t.forEach((c,g)=>{let _=`journal[${g}]`;if(typeof c!="object"||c===null||Array.isArray(c)){p.push({path:_,rule:"journal-event-shape",message:`journal[${g}]: each event must be a JSON object.`,severity:"error"});return}let x=c,j=[];if(s(x.id)===void 0&&j.push("id"),s(x.ts)===void 0&&j.push("ts"),s(x.type)===void 0&&j.push("type"),j.length>0){p.push({path:_,rule:"journal-event-envelope",message:`journal[${g}]: event is missing required envelope field(s): ${j.join(", ")}.`,severity:"error"});return}E.push({ev:x,index:g})});let L=new Set(m.keys()),k=new Set(T),F=new Set;for(let{ev:c}of E)if(c.type==="node.created"){let g=s(c.node_id);g&&L.add(g)}else if(c.type==="edge.added"){let g=s(c.edge_id);g&&k.add(g)}else if(c.type==="journal.baseline"&&Array.isArray(c.node_ids))for(let g of c.node_ids){let _=s(g);_&&(F.add(_),L.add(_))}let O=Ne(E.map(c=>c.ev)),J=new Set,G=new Map,q=new Map;for(let c of O)if(c.type==="node.created"){let g=s(c.node_id);g&&J.add(g)}else if(c.type==="node.status_changed"){let g=s(c.node_id);if(g&&c.platform===void 0){let _=s(c.to);_!==void 0&&G.set(g,_)}}else if(c.type==="decision.status_changed"){let g=s(c.node_id);if(g){let _=s(c.to);_!==void 0&&q.set(g,_)}}for(let{ev:c,index:g}of E){let _=Te[c.type];if(_)for(let x of _){let j=s(c[x]);j!==void 0&&!L.has(j)&&p.push({path:`journal[${g}].${x}`,rule:"journal-dangling-node-ref",message:`journal[${g}] (${c.type}): references node "${j}" that never existed in the snapshot or journal.`,severity:"error"})}if(c.type==="deliverable.shipped"&&Array.isArray(c.node_ids)&&c.node_ids.forEach((x,j)=>{let V=s(x);V!==void 0&&!L.has(V)&&p.push({path:`journal[${g}].node_ids[${j}]`,rule:"journal-dangling-node-ref",message:`journal[${g}] (${c.type}): references node "${V}" that never existed in the snapshot or journal.`,severity:"error"})}),c.type==="edge.removed"){let x=s(c.edge_id);x!==void 0&&!k.has(x)&&p.push({path:`journal[${g}].edge_id`,rule:"journal-dangling-edge-ref",message:`journal[${g}] (edge.removed): references edge "${x}" that never existed in the snapshot or journal.`,severity:"error"})}}for(let[c,g]of m){!J.has(c)&&!F.has(c)&&p.push({path:"journal",rule:"journal-missing-node-created",message:`Node "${c}" is present in the snapshot but has no node.created event in the journal.`,severity:"error"});let _=G.get(c);_!==void 0&&!Fe(_,g)&&p.push({path:"journal",rule:"journal-status-mismatch",message:`Node "${c}": journal's last node.status_changed.to "${_}" disagrees with snapshot status "${String(g)}".`,severity:"error"})}for(let[c,g]of q){if(!I.has(c))continue;let _=I.get(c)??"proposed";g!==_&&p.push({path:"journal",rule:"journal-decision-status-mismatch",message:`Node "${c}": journal's last decision.status_changed.to "${g}" disagrees with snapshot decision_status "${String(_)}".`,severity:"error"})}return p}var $e=["flow","view","acceptance"];function be(f){let p=f?.metadata?.products;if(!Array.isArray(p))return[];let t=new Set,s=[];for(let $ of p){if(typeof $!="object"||$===null||Array.isArray($))continue;let w=$;typeof w.id!="string"||w.id.trim()===""||t.has(w.id)||(t.add(w.id),s.push(w))}return s}var Ee=["rings","bars"],Ae=["chips","rows"],ke=["status","species"];var Le=["journey","system"];function xe(f){return Le.includes(f)}var X="cross-surface";var Oe=["beta","monitoring","deprecated"],Ie=["compact","large"],Re=2*1024*1024;function Ce(f){let p=f.indexOf(",");if(p===-1)return 0;let t=f.slice(p+1),s=t.endsWith("==")?2:t.endsWith("=")?1:0;return Math.floor(t.length*3/4)-s}function Me(f){return typeof f=="string"&&!Number.isNaN(Date.parse(f))}function Pe(f){let p=[],t=(e,r,n)=>p.push({path:e,rule:r,message:n,severity:"error"}),s=(e,r,n)=>p.push({path:e,rule:r,message:n,severity:"warning"}),$=()=>{let e=p.filter(n=>n.severity==="error"),r=p.filter(n=>n.severity==="warning");return{valid:e.length===0,findings:p,errors:e,warnings:r}};if(typeof f!="object"||f===null||Array.isArray(f))return t("","bundle-shape","Bundle must be an object with project, nodes, and edges."),$();let w=f,m=w.project,I=w.nodes,T=w.edges;if(!m||!I||!T)return t("","bundle-shape","Missing top-level keys (project, nodes, edges)."),$();if(!Array.isArray(I))return t("nodes","bundle-shape","nodes must be an array."),$();if(!Array.isArray(T))return t("edges","bundle-shape","edges must be an array."),$();let E=I,L=T,k=new Map,F=new Set;m.id||t("project.id","project-id-required","project.id is missing"),(typeof m.title!="string"||!m.title.trim())&&t("project.title","title-non-empty","project.title is missing or empty"),m.created_at||t("project.created_at","timestamp-required","project.created_at is missing"),m.updated_at||t("project.updated_at","timestamp-required","project.updated_at is missing"),m.created_at&&!Me(m.created_at)&&s("project.created_at","iso-timestamp","project.created_at is not a valid ISO 8601 timestamp"),m.updated_at&&!Me(m.updated_at)&&s("project.updated_at","iso-timestamp","project.updated_at is not a valid ISO 8601 timestamp");let O=m.metadata;O&&O.view_card_variant!==void 0&&!Ie.includes(O.view_card_variant)&&t("project.metadata.view_card_variant","view-card-variant",`project.metadata.view_card_variant must be one of ${Ie.join(", ")} (import rejects other values)`),E.forEach((e,r)=>{let n=`nodes[${r}]`,i=e.id;F.has(i)&&t(`${n}.id`,"duplicate-node-id",`Duplicate node ID: ${i}`),F.add(i),k.set(i,e),(typeof e.title!="string"||!e.title.trim())&&t(`${n}.title`,"title-non-empty",`Node ${i}: title is missing or empty`);let a=e.species,o=_e[a];o&&!(typeof i=="string"&&i.startsWith(o))&&t(`${n}.id`,"species-prefix",`Node ${i}: species "${a}" should have prefix "${o}"`),H.includes(a)||t(`${n}.species`,"valid-species",`Node ${i}: invalid species "${a}"`),e.project_id!==m.id&&t(`${n}.project_id`,"project-id-match",`Node ${i}: project_id "${e.project_id}" does not match project.id "${m.id}"`),K.includes(e.status)||t(`${n}.status`,"valid-status",`Node ${i}: invalid status "${e.status}"`);let l=e.platforms;if(!l||l.length===0)a!=="decision"&&t(`${n}.platforms`,"platforms-non-empty",`Node ${i}: platforms array is empty or missing`);else for(let d of l)B.includes(d)||t(`${n}.platforms`,"valid-platform",`Node ${i}: invalid platform "${d}"`);let u=e.metadata||{};u.stage!==void 0&&!Oe.includes(u.stage)&&t(`${n}.metadata.stage`,"valid-stage",`Node ${i}: invalid metadata.stage "${u.stage}"`);let S=u.platformNotes;if(S)for(let d of Object.keys(S))B.includes(d)||t(`${n}.metadata.platformNotes`,"valid-platform",`Node ${i}: platformNotes has invalid platform "${d}"`);let b=u.platformStatuses;if(b)for(let[d,y]of Object.entries(b))B.includes(d)?Array.isArray(l)&&!l.includes(d)&&s(`${n}.metadata.platformStatuses`,"platform-statuses-subset",`Node ${i}: platformStatuses covers platform "${d}" not in node.platforms`):t(`${n}.metadata.platformStatuses`,"valid-platform",`Node ${i}: platformStatuses has invalid platform "${d}"`),K.includes(y)||t(`${n}.metadata.platformStatuses`,"valid-status",`Node ${i}: platformStatuses has invalid status "${y}" for platform "${d}"`);let R=u.platformScreenshots;if(R){for(let[d,y]of Object.entries(R))if(typeof y=="string"&&y.startsWith("data:")){let M=Ce(y);M>Re&&s(`${n}.metadata.platformScreenshots`,"screenshot-data-uri-size",`Node ${i}: platformScreenshots.${d} is a ${(M/(1024*1024)).toFixed(1)}MB data URI, above the ${Re/(1024*1024)}MB guidance (docs/spec/bundle-format.md \xA7 Asset Values) \u2014 consider a relative path or hosted URL instead`)}}let P=u.gherkin,C=u.values;if(a==="acceptance"?((typeof P!="string"||!P.trim())&&s(`${n}.metadata.gherkin`,"acceptance-gherkin-missing",`Acceptance ${i}: metadata.gherkin is missing or empty (one Given/When/Then scenario expected)`),(!Array.isArray(C)||C.length===0)&&s(`${n}.metadata.values`,"acceptance-values-missing",`Acceptance ${i}: metadata.values is missing or empty (assign 1-3 value elements)`)):(P!==void 0&&s(`${n}.metadata.gherkin`,"gherkin-species",`Node ${i}: metadata.gherkin is only meaningful on acceptance nodes`),C!==void 0&&s(`${n}.metadata.values`,"values-species",`Node ${i}: metadata.values is only meaningful on acceptance nodes`)),Array.isArray(C))for(let d of C)he.includes(d)||t(`${n}.metadata.values`,"valid-value",`Node ${i}: invalid value element "${d}"`);let h=u.refs;if(Array.isArray(h)){let d=new Set;h.forEach((y,M)=>{let A=y??{},D=`${n}.metadata.refs[${M}]`,N=A.id;typeof N!="string"||!N.trim()?t(`${D}.id`,"ref-id-required",`Node ${i}: ref is missing an id`):d.has(N)?t(`${D}.id`,"duplicate-ref-id",`Node ${i}: duplicate ref id "${N}"`):d.add(N),(typeof A.url!="string"||!A.url.trim())&&t(`${D}.url`,"ref-url-required",`Node ${i}: ref "${N}" is missing a url`),A.status_mapped!==void 0&&!K.includes(A.status_mapped)&&t(`${D}.status_mapped`,"valid-status",`Node ${i}: ref "${N}" has invalid status_mapped "${A.status_mapped}"`)})}if(u.decision_status!==void 0&&a!=="decision"&&s(`${n}.metadata.decision_status`,"decision-status-wrong-species",`decision_status is meaningful on decision nodes only; "${i}" is a ${a}.`),a==="decision"){let d=we({metadata:e.metadata}),y=ve(d);e.status!==y&&s(`${n}.status`,"decision-lifecycle-mismatch",`Decision "${i}" is ${d}, whose lifecycle status should be "${y}", but status is "${e.status}" (spec \xA72).`)}if(a==="flow"){let d=u.playlist;!e.metadata||!d||!d.entries?t(`${n}.metadata.playlist`,"flow-playlist-required",`Flow ${i}: missing metadata.playlist.entries`):d.entries.length===0&&s(`${n}.metadata.playlist`,"flow-playlist-empty",`Flow ${i}: playlist is empty`)}});let J=m.root_node_id;J&&!F.has(J)&&t("project.root_node_id","root-node-exists",`project.root_node_id "${J}" does not reference an existing node`);let G=(e,r,n)=>{if(typeof e!="object"||e===null||Array.isArray(e))return;let i=e,a=[["flow_platforms",Ee],["view_platforms",Ae],["minimap_color",ke]];for(let[o,l]of a){let u=i[o];u!==void 0&&(typeof u!="string"||!l.includes(u))&&s(`${r}.${o}`,"map-unknown-display",`${n} sets ${o} to "${String(u)}"; expected one of ${l.join(", ")} (renderers fall back to the default)`)}i.images!==void 0&&typeof i.images!="boolean"&&s(`${r}.images`,"map-unknown-display",`${n} sets images to a non-boolean value (renderers fall back to the default)`)},q=O?.map_display;if(typeof q=="object"&&q!==null&&!Array.isArray(q))for(let[e,r]of Object.entries(q))G(r,`project.metadata.map_display.${e}`,`Map "${e}"`);let c=O?.maps;if(Array.isArray(c)){let e=new Set;c.forEach((r,n)=>{if(typeof r!="object"||r===null||Array.isArray(r))return;let i=r,a=`project.metadata.maps[${n}]`,o=typeof i.id=="string"?i.id:void 0;o!==void 0&&(e.has(o)&&s(`${a}.id`,"map-duplicate-id",`Duplicate map id "${o}"`),e.add(o),xe(o)&&s(`${a}.id`,"map-shadows-built-in",`Map id "${o}" shadows a built-in map and will be ignored`));let l=i.root_node_id;typeof l=="string"&&!F.has(l)&&s(`${a}.root_node_id`,"map-unknown-root",`Map "${o??n}" anchors on "${l}" which does not reference an existing node`),Array.isArray(i.species)&&i.species.forEach((u,S)=>{typeof u=="string"&&!H.includes(u)&&s(`${a}.species[${S}]`,"map-unknown-species",`Map "${o??n}" filters on unknown species "${u}"`)}),Array.isArray(i.edge_types)&&i.edge_types.forEach((u,S)=>{typeof u=="string"&&!ne.includes(u)&&s(`${a}.edge_types[${S}]`,"map-unknown-edge-type",`Map "${o??n}" filters on unknown edge type "${u}"`)}),G(i.display,`${a}.display`,`Map "${o??n}"`)})}let g=O?.products,_=new Set;if(Array.isArray(g)){let e=new Set;g.forEach((r,n)=>{if(typeof r!="object"||r===null||Array.isArray(r))return;let i=r,a=`project.metadata.products[${n}]`,o=typeof i.id=="string"?i.id:void 0;o!==void 0&&(e.has(o)?s(`${a}.id`,"product-duplicate-id",`Duplicate product id "${o}" \u2014 the first wins`):(e.add(o),o.trim()!==""&&_.add(o)),/^[a-z0-9]+(-[a-z0-9]+)*$/.test(o)||s(`${a}.id`,"product-invalid-id",`Product id "${o}" is not kebab-case`))})}let x=_.size>0,j=new Map;for(let e of L){if(e.edge_type!=="covers")continue;let r=typeof e.source_id=="string"?e.source_id:void 0,n=typeof e.target_id=="string"?e.target_id:void 0;if(r===void 0||n===void 0)continue;let i=j.get(r)??[];i.push(n),j.set(r,i)}let V=new Map;if(x)for(let e of be({metadata:O}))V.set(e.id,new Set(Array.isArray(e.platforms)?e.platforms:[]));let oe=new Map,ae=new Map;if(E.forEach((e,r)=>{let n=typeof e.id=="string"?e.id:`#${r}`,i=e.species,a=`nodes[${r}]`;ae.set(n,r);let o=e.metadata??{},l=typeof o.product=="string"?o.product:void 0,u=i!==void 0&&$e.includes(i);if(l!==void 0&&!u){let R=H.includes(i)?`${i} membership is derived from consumers and must not be stored`:"metadata.product is only meaningful on flow, view, and acceptance nodes";s(`${a}.metadata.product`,"product-membership-wrong-species",`Node ${n}: ${R}`);return}if(!u)return;if(l===void 0){if(!x)return;i==="acceptance"?j.has(n)||s(`${a}.metadata.product`,"acceptance-product-unassigned",`Acceptance ${n} covers nothing and names no product \u2014 it will show only under "All products"`):s(`${a}.metadata.product`,"unassigned-membership",`Node ${n}: no product membership \u2014 it will show only under "All products"`);return}if(oe.set(n,l),!_.has(l)){s(`${a}.metadata.product`,"product-unknown-reference",`Node ${n}: product "${l}" is not declared on the project`);return}let S=V.get(l),b=Array.isArray(e.platforms)?e.platforms:[];if(S)for(let R of b)typeof R=="string"&&!S.has(R)&&s(`${a}.platforms`,"product-platform-not-in-menu",`Node ${n}: platform "${R}" is not in product "${l}"'s menu`)}),x)for(let[e,r]of j){let n=ae.get(e);if(n===void 0)continue;let i=new Set(r.map(a=>oe.get(a)).filter(a=>!!a));i.size>1&&s(`nodes[${n}].metadata.product`,"acceptance-covers-span-products",`Acceptance ${e} covers anchors in ${[...i].sort().join(" and ")} \u2014 statuses may conflate products`)}let de=new Set,ce=new Set,le=new Set;L.forEach((e,r)=>{let n=`edges[${r}]`,i=e.id,a=e.source_id,o=e.target_id,l=e.edge_type;de.has(i)&&t(`${n}.id`,"duplicate-edge-id",`Duplicate edge ID: ${i}`),de.add(i);let u=`e-${a}-${o}`;i!==u&&s(`${n}.id`,"edge-id-convention",`Edge ${i}: id does not match convention "${u}" (stale after a rename?)`);let S=`${a}->${o}:${l}`;ce.has(S)&&s(`${n}`,"duplicate-edge-relationship",`Duplicate edge relationship: ${S}`),ce.add(S),e.project_id!==m.id&&t(`${n}.project_id`,"project-id-match",`Edge ${i}: project_id does not match project.id`),F.has(a)||t(`${n}.source_id`,"dangling-edge",`Edge ${i}: source_id "${a}" not found in nodes`),F.has(o)||t(`${n}.target_id`,"dangling-edge",`Edge ${i}: target_id "${o}" not found in nodes`),ne.includes(l)||t(`${n}.edge_type`,"valid-edge-type",`Edge ${i}: invalid edge_type "${l}"`);let b=k.get(a),R=k.get(o),P=me[l];b&&R&&P&&(P.some(([h,v])=>h===b.species&&v===R.species)||t(`${n}.edge_type`,"edge-semantics",`Edge ${i}: "${l}" not valid from ${b.species} to ${R.species}`)),l==="composes"&&le.add(`${a}->${o}`)});let ue=["view","flow","condition","junction"],Z=(e,r,n,i=0)=>{if(!(i>50)){if(!Array.isArray(e)){t(n,"playlist-entry-shape",`Flow ${r}: ${n} must be an array of playlist entries`);return}e.forEach((a,o)=>{let l=`${n}[${o}]`;if(typeof a!="object"||a===null||Array.isArray(a)){t(l,"playlist-entry-shape",`Flow ${r}: ${l} must be a playlist entry object`);return}let u=a,S=u.type;if(typeof S!="string"||!ue.includes(S)){t(`${l}.type`,"playlist-entry-shape",`Flow ${r}: ${l}.type must be one of ${ue.join(", ")} (got ${JSON.stringify(S)})`);return}if(S==="view")typeof u.view_id!="string"&&t(`${l}.view_id`,"playlist-entry-shape",`Flow ${r}: ${l} is a view entry with no view_id string`);else if(S==="flow")typeof u.flow_id!="string"&&t(`${l}.flow_id`,"playlist-entry-shape",`Flow ${r}: ${l} is a flow entry with no flow_id string`);else if(S==="condition"){typeof u.label!="string"&&t(`${l}.label`,"playlist-entry-shape",`Flow ${r}: ${l} is a condition with no label string`);for(let b of["if_true","if_false"])Array.isArray(u[b])?Z(u[b],r,`${l}.${b}`,i+1):t(`${l}.${b}`,"playlist-entry-shape",`Flow ${r}: ${l}.${b} must be an array of playlist entries (both branches are required, use [] for an empty one)`)}else{if(typeof u.label!="string"&&t(`${l}.label`,"playlist-entry-shape",`Flow ${r}: ${l} is a junction with no label string`),!Array.isArray(u.cases)){t(`${l}.cases`,"playlist-entry-shape",`Flow ${r}: ${l}.cases must be an array of junction cases`);return}u.cases.forEach((b,R)=>{let P=`${l}.cases[${R}]`;if(typeof b!="object"||b===null||Array.isArray(b)){t(P,"playlist-entry-shape",`Flow ${r}: ${P} must be a junction case object`);return}let C=b;typeof C.label!="string"&&t(`${P}.label`,"playlist-entry-shape",`Flow ${r}: ${P} has no label string`),Array.isArray(C.entries)?Z(C.entries,r,`${P}.entries`,i+1):t(`${P}.entries`,"playlist-entry-shape",`Flow ${r}: ${P}.entries must be an array of playlist entries \u2014 a junction case holds its own entries, not a bare view_id/flow_id`)})}})}};E.forEach((e,r)=>{let n=e.metadata;e.species==="flow"&&n?.playlist?.entries&&Z(n.playlist.entries,e.id,`nodes[${r}].metadata.playlist.entries`)});let z=(e,r,n,i=0)=>{if(i>50)return t(n,"playlist-depth",`Flow ${r}: playlist nesting too deep (possible cycle)`),[];let a=[];if(!Array.isArray(e))return a;for(let o of e)if(!(typeof o!="object"||o===null)){if(o.type==="view")F.has(o.view_id)||t(n,"playlist-ref-exists",`Flow ${r}: playlist references non-existent view "${o.view_id}"`),a.push(o.view_id);else if(o.type==="flow")F.has(o.flow_id)||t(n,"playlist-ref-exists",`Flow ${r}: playlist references non-existent flow "${o.flow_id}"`),o.flow_id===r&&t(n,"playlist-self-cycle",`Flow ${r}: playlist contains itself (direct cycle)`),a.push(o.flow_id);else if(o.type==="condition")o.if_true&&a.push(...z(o.if_true,r,n,i+1)),o.if_false&&a.push(...z(o.if_false,r,n,i+1));else if(o.type==="junction"&&o.cases)for(let l of o.cases)a.push(...z(l.entries||[],r,n,i+1))}return a};E.forEach((e,r)=>{let n=e.metadata;if(e.species==="flow"&&n?.playlist?.entries){let i=`nodes[${r}].metadata.playlist`,a=e.id,o=z(n.playlist.entries,a,i);for(let l of o)le.has(`${a}->${l}`)||t(i,"playlist-composes-coherence",`Flow ${a}: playlist references "${l}" but no composes edge exists`)}});let ee=new Map,pe=new Map;E.forEach((e,r)=>{let n=e.metadata;if(e.species==="flow"&&n?.playlist?.entries){let i=e.id,a=[],o=l=>{if(Array.isArray(l)){for(let u of l)if(!(typeof u!="object"||u===null)&&(u.type==="flow"&&a.push(u.flow_id),u.type==="condition"&&(u.if_true&&o(u.if_true),u.if_false&&o(u.if_false)),u.type==="junction"&&u.cases))for(let S of u.cases)o(S.entries||[])}};o(n.playlist.entries),ee.set(i,a),pe.set(i,`nodes[${r}].metadata.playlist`)}});let fe=new Set,te=new Set,ge=e=>{if(te.has(e))return!0;if(fe.has(e))return!1;fe.add(e),te.add(e);for(let r of ee.get(e)||[])if(ge(r))return t(pe.get(e)??"","flow-cycle",`Cycle detected: flow "${e}" -> "${r}"`),!0;return te.delete(e),!1};for(let e of ee.keys())ge(e);let W=w.quality;if(typeof W=="object"&&W!==null&&!Array.isArray(W)){let e=W,r=Array.isArray(e.assessments)?e.assessments:[],n=Array.isArray(e.findings)?e.findings:[],i=typeof e.profile=="object"&&e.profile!==null?e.profile:void 0,a=new Set;Array.isArray(i?.surfaces)&&i.surfaces.forEach((h,v)=>{if(typeof h!="object"||h===null||Array.isArray(h))return;let d=h,y=typeof d.id=="string"?d.id:void 0;y===void 0||y.trim()===""||(a.has(y)&&s(`quality.profile.surfaces[${v}].id`,"quality-duplicate-surface",`Duplicate surface id "${y}" \u2014 the first wins`),a.add(y))}),a.size===0&&(r.length>0||n.length>0)&&s("quality.profile.surfaces","quality-no-surfaces","Quality data is stored but the profile declares no surfaces \u2014 the matrix falls back to the surfaces the assessments name");let o=typeof e.library=="object"&&e.library!==null?e.library:void 0,l=new Set,u=new Map;if(Array.isArray(o?.criteria))for(let h of o.criteria){if(typeof h!="object"||h===null)continue;let v=h;typeof v.id=="string"&&(l.add(v.id),typeof v.superseded_by=="string"&&v.superseded_by!==""&&u.set(v.id,v.superseded_by))}else(r.length>0||n.length>0)&&s("quality.library","quality-library-missing","No criteria pack is embedded \u2014 criterion ids cannot be resolved to domains or weights; the matrix uses a synthesized library");(typeof e.framework_version!="string"||e.framework_version==="")&&s("quality.framework_version","quality-framework-version-missing","Quality section has no framework_version \u2014 scores are not comparable across audits without one");let S=(h,v,d)=>{let y=typeof h.criterion_id=="string"?h.criterion_id:void 0;y!==void 0&&l.size>0&&!l.has(y)&&s(`${v}.criterion_id`,"quality-unknown-criterion",`${d}: criterion "${y}" is not in the pinned library`);let M=y!==void 0?u.get(y):void 0;M!==void 0&&s(`${v}.criterion_id`,"quality-retired-criterion",`${d}: criterion "${y}" is retired \u2014 superseded by "${M}"`);let A=typeof h.surface=="string"?h.surface:void 0;A!==void 0&&A!==X&&a.size>0&&!a.has(A)&&s(`${v}.surface`,"quality-unknown-surface",`${d}: surface "${A}" is not declared in the profile`)},b=new Set;r.forEach((h,v)=>{if(typeof h!="object"||h===null||Array.isArray(h))return;let d=h,y=`quality.assessments[${v}]`,M=`Assessment ${v}`;S(d,y,M);let A=d.level;(typeof A!="number"||!Number.isInteger(A)||A<0||A>4)&&s(`${y}.level`,"quality-level-range",`${M}: level must be an integer 0-4 (N/A is the absence of the row, not a level)`),d.surface===X&&s(`${y}.surface`,"quality-cross-surface-assessment",`${M}: "${X}" is a findings-only lens \u2014 it carries no matrix column, so this score would render nowhere`),(typeof d.evidence!="string"||d.evidence.trim()==="")&&s(`${y}.evidence`,"quality-assessment-no-evidence",`${M}: scored with no evidence \u2014 a score without a citation is an opinion, not an assessment`);let D=typeof d.criterion_id=="string"?d.criterion_id:void 0,N=typeof d.surface=="string"?d.surface:void 0;if(D!==void 0&&N!==void 0){let ye=`${D}\0${N}`;b.has(ye)&&s(`${y}`,"quality-duplicate-assessment",`${M}: a second score for (${D} x ${N}) \u2014 the section stores latest-per-cell, so this cell is ambiguous`),b.add(ye)}});let R=new Set;n.forEach((h,v)=>{if(typeof h!="object"||h===null||Array.isArray(h))return;let d=h,y=`quality.findings[${v}]`,M=`Finding ${v}`;S(d,y,M);let A=typeof d.id=="string"?d.id:void 0;A!==void 0&&(R.has(A)&&s(`${y}.id`,"quality-duplicate-finding-id",`Duplicate finding id "${A}"`),R.add(A));for(let D of["impact","likelihood"]){let N=d[D];(typeof N!="number"||!Number.isInteger(N)||N<1||N>5)&&s(`${y}.${D}`,"quality-risk-range",`${M}: ${D} must be an integer 1-5 \u2014 severity is derived from impact x likelihood`)}for(let D of["severity","priority"])D in d&&s(`${y}.${D}`,"quality-derived-field-stored",`${M}: ${D} is derived from impact, likelihood and cost \u2014 storing it lets it drift from the numbers behind it`);d.status==="accepted-risk"&&(typeof d.detail!="string"||d.detail.trim()==="")&&s(`${y}.detail`,"quality-accepted-risk-no-note",`${M}: accepted-risk with no note \u2014 an accepted risk is a decision and reads like one`)});let P=new Set,C=Array.isArray(w.journal)?w.journal:[];C.forEach(h=>{if(typeof h!="object"||h===null)return;let v=h;v.type==="quality.finding.opened"&&typeof v.finding_id=="string"&&P.add(v.finding_id)}),C.forEach((h,v)=>{if(typeof h!="object"||h===null)return;let d=h,y=typeof d.type=="string"?d.type:"";y.startsWith("quality.")&&((typeof d.actor!="string"||d.actor.trim()==="")&&s(`journal[${v}].actor`,"quality-event-no-actor",`Journal event ${v}: ${y} has no actor \u2014 human, agent and CI scores become indistinguishable`),y==="quality.finding.resolved"&&typeof d.finding_id=="string"&&!P.has(d.finding_id)&&!R.has(d.finding_id)&&s(`journal[${v}].finding_id`,"quality-resolved-never-opened",`Journal event ${v}: resolves finding "${d.finding_id}", which no quality.finding.opened event or stored finding ever declared`))})}for(let e of Se(w))p.push(e);return $()}var je="journal.jsonl",Je="journal";function qe(f){let p=(0,Q.join)(f,Je);return(0,U.existsSync)(p)?(0,U.readdirSync)(p).filter(t=>t.startsWith("archive-")&&t.endsWith(".jsonl")).sort().map(t=>(0,Q.join)(p,t)):[]}function Y(f,p){return f.filter(t=>t?.species===p).length}function Ge(){let f=process.argv[2];f||(console.error("Usage: node validate-bundle.js <path-to-bundle.json>"),process.exit(1)),(0,U.existsSync)(f)||(console.error(`File not found: ${f}`),process.exit(1));let p;try{p=JSON.parse((0,U.readFileSync)(f,"utf8"))}catch(k){console.error(`FATAL: Cannot parse JSON \u2014 ${k.message}`),process.exit(1)}let t=p;(typeof t!="object"||t===null||!t.project||!t.nodes||!t.edges)&&(console.error("FATAL: Missing top-level keys (project, nodes, edges)."),process.exit(1));let s=[],$=!1,w=[];if(t.journal===void 0){let k=(0,Q.dirname)(f),F=(0,Q.join)(k,je),O=[];if((0,U.existsSync)(F)){let{events:J,findings:G}=re((0,U.readFileSync)(F,"utf8"));for(let q of G)s.push(q.message);$=!0,O.push(...J)}for(let J of qe(k)){let G=(0,Q.basename)(J),{events:q,findings:c}=re((0,U.readFileSync)(J,"utf8"));for(let g of c)s.push(`${G} \u2014 ${g.message}`);w.push(G),O.push(...q)}($||w.length>0)&&(t.journal=O)}let m=Array.isArray(t.nodes)?t.nodes:[],I=Array.isArray(t.edges)?t.edges:[],T=Array.isArray(t.journal)?t.journal:[],E=Pe(p);if(console.log(`
|
|
13
|
+
"use strict";var Q=require("node:fs"),U=require("node:path");var H=["flow","view","data-model","api-endpoint","acceptance","decision"],K=["idea","discovery","backlog","development","releasing","live","archived"],B=["web","ios","android"],ne=["composes","calls","displays","queries","covers","supersedes","generates","impacts"],me={composes:[["flow","view"],["flow","flow"],["view","flow"],["view","view"]],calls:[["view","api-endpoint"],["flow","api-endpoint"],["api-endpoint","api-endpoint"],["api-endpoint","view"]],displays:[["view","data-model"]],queries:[["api-endpoint","data-model"]],covers:[["acceptance","view"],["acceptance","flow"]],supersedes:[["decision","decision"]],generates:[["decision","acceptance"]],impacts:[["decision","flow"],["decision","view"],["decision","data-model"],["decision","api-endpoint"]]};var he=["saves-time","simplifies","makes-money","reduces-risk","organizes","integrates","connects","reduces-effort","avoids-hassles","reduces-cost","quality","variety","sensory-appeal","informs","reduces-anxiety","rewards-me","nostalgia","design-aesthetics","badge-value","wellness","therapeutic-value","fun-entertainment","attractiveness","provides-access","provides-hope","self-actualization","motivation","heirloom","affiliation-belonging","self-transcendence"];var _e={flow:"F-",view:"V-","data-model":"DM-","api-endpoint":"API-",acceptance:"AC-",decision:"DEC-"};var De=["proposed","approved","enacted","rejected","deprecated","superseded"];function ve(f){switch(f){case"proposed":return"discovery";case"approved":return"backlog";case"enacted":return"live";case"rejected":case"deprecated":case"superseded":return"archived"}}function we(f){let p=f.metadata?.decision_status;return De.includes(p)?p:"proposed"}var ie={prioritized:"backlog",blocked:"development"};function se(f){return K.includes(f)?f:ie[f]}var Be={...ie},Ye={backlog:"idea",...ie};function Ne(f){let p=t=>typeof t=="string"?t:"";return[...f].sort((t,s)=>{let $=p(t.ts),w=p(s.ts);if($<w)return-1;if($>w)return 1;let m=p(t.id),R=p(s.id);return m<R?-1:m>R?1:0})}function re(f){let p=[],t=[];return f.split(/\r?\n/).forEach(($,w)=>{let m=w+1;if($.trim()==="")return;let R;try{R=JSON.parse($)}catch(L){t.push({line:m,rule:"journal-line-parse",message:`Line ${m}: not valid JSON \u2014 ${L.message}`,severity:"error"});return}if(typeof R!="object"||R===null||Array.isArray(R)){t.push({line:m,rule:"journal-line-shape",message:`Line ${m}: each journal line must be a single JSON event object.`,severity:"error"});return}let T=R,E=[];if(typeof T.id!="string"&&E.push("id"),typeof T.ts!="string"&&E.push("ts"),typeof T.type!="string"&&E.push("type"),E.length>0){t.push({line:m,rule:"journal-line-shape",message:`Line ${m}: event is missing required envelope field(s): ${E.join(", ")}.`,severity:"error"});return}p.push(T)}),{events:p,findings:t}}var Te={"node.updated":["node_id"],"node.status_changed":["node_id"],"decision.status_changed":["node_id"],"node.deleted":["node_id"],"edge.added":["source_id","target_id"],"ref.added":["node_id"],"ref.removed":["node_id"],"ref.status_changed":["node_id"],"idea.proposed":["node_id"],"request.filed":["node_id"]};function Fe(f,p){return f===p?!0:typeof p!="string"?!1:f==="backlog"&&p==="idea"?!0:(se(f)??f)===(se(p)??p)}function Se(f){let p=[],t=f.journal;if(t===void 0)return p;if(!Array.isArray(t))return p.push({path:"journal",rule:"journal-shape",message:"journal must be an array of events when present.",severity:"error"}),p;if(t.length===0)return p;let s=c=>typeof c=="string"?c:void 0,$=Array.isArray(f.nodes)?f.nodes:[],w=Array.isArray(f.edges)?f.edges:[],m=new Map,R=new Map;for(let c of $){let g=s(c?.id);if(g!==void 0){m.set(g,c.status);let v=c.metadata,x=v&&typeof v=="object"&&!Array.isArray(v)?v.decision_status:void 0;R.set(g,x??"proposed")}}let T=new Set;for(let c of w){let g=s(c?.id);g!==void 0&&T.add(g)}let E=[];t.forEach((c,g)=>{let v=`journal[${g}]`;if(typeof c!="object"||c===null||Array.isArray(c)){p.push({path:v,rule:"journal-event-shape",message:`journal[${g}]: each event must be a JSON object.`,severity:"error"});return}let x=c,j=[];if(s(x.id)===void 0&&j.push("id"),s(x.ts)===void 0&&j.push("ts"),s(x.type)===void 0&&j.push("type"),j.length>0){p.push({path:v,rule:"journal-event-envelope",message:`journal[${g}]: event is missing required envelope field(s): ${j.join(", ")}.`,severity:"error"});return}E.push({ev:x,index:g})});let L=new Set(m.keys()),k=new Set(T),F=new Set;for(let{ev:c}of E)if(c.type==="node.created"){let g=s(c.node_id);g&&L.add(g)}else if(c.type==="edge.added"){let g=s(c.edge_id);g&&k.add(g)}else if(c.type==="journal.baseline"&&Array.isArray(c.node_ids))for(let g of c.node_ids){let v=s(g);v&&(F.add(v),L.add(v))}let O=Ne(E.map(c=>c.ev)),q=new Set,G=new Map,J=new Map;for(let c of O)if(c.type==="node.created"){let g=s(c.node_id);g&&q.add(g)}else if(c.type==="node.status_changed"){let g=s(c.node_id);if(g&&c.platform===void 0){let v=s(c.to);v!==void 0&&G.set(g,v)}}else if(c.type==="decision.status_changed"){let g=s(c.node_id);if(g){let v=s(c.to);v!==void 0&&J.set(g,v)}}for(let{ev:c,index:g}of E){let v=Te[c.type];if(v)for(let x of v){let j=s(c[x]);j!==void 0&&!L.has(j)&&p.push({path:`journal[${g}].${x}`,rule:"journal-dangling-node-ref",message:`journal[${g}] (${c.type}): references node "${j}" that never existed in the snapshot or journal.`,severity:"error"})}if(c.type==="deliverable.shipped"&&Array.isArray(c.node_ids)&&c.node_ids.forEach((x,j)=>{let V=s(x);V!==void 0&&!L.has(V)&&p.push({path:`journal[${g}].node_ids[${j}]`,rule:"journal-dangling-node-ref",message:`journal[${g}] (${c.type}): references node "${V}" that never existed in the snapshot or journal.`,severity:"error"})}),c.type==="edge.removed"){let x=s(c.edge_id);x!==void 0&&!k.has(x)&&p.push({path:`journal[${g}].edge_id`,rule:"journal-dangling-edge-ref",message:`journal[${g}] (edge.removed): references edge "${x}" that never existed in the snapshot or journal.`,severity:"error"})}}for(let[c,g]of m){!q.has(c)&&!F.has(c)&&p.push({path:"journal",rule:"journal-missing-node-created",message:`Node "${c}" is present in the snapshot but has no node.created event in the journal.`,severity:"error"});let v=G.get(c);v!==void 0&&!Fe(v,g)&&p.push({path:"journal",rule:"journal-status-mismatch",message:`Node "${c}": journal's last node.status_changed.to "${v}" disagrees with snapshot status "${String(g)}".`,severity:"error"})}for(let[c,g]of J){if(!R.has(c))continue;let v=R.get(c)??"proposed";g!==v&&p.push({path:"journal",rule:"journal-decision-status-mismatch",message:`Node "${c}": journal's last decision.status_changed.to "${g}" disagrees with snapshot decision_status "${String(v)}".`,severity:"error"})}return p}var $e=["flow","view","acceptance"];function be(f){let p=f?.metadata?.products;if(!Array.isArray(p))return[];let t=new Set,s=[];for(let $ of p){if(typeof $!="object"||$===null||Array.isArray($))continue;let w=$;typeof w.id!="string"||w.id.trim()===""||t.has(w.id)||(t.add(w.id),s.push(w))}return s}var Ee=["rings","bars"],Ae=["chips","rows"],ke=["status","species"];var Le=["journey","system"];function xe(f){return Le.includes(f)}var X="cross-surface";var Oe=["beta","monitoring","deprecated"],Ie=["compact","large"],Re=2*1024*1024;function Ce(f){let p=f.indexOf(",");if(p===-1)return 0;let t=f.slice(p+1),s=t.endsWith("==")?2:t.endsWith("=")?1:0;return Math.floor(t.length*3/4)-s}function Me(f){return typeof f=="string"&&!Number.isNaN(Date.parse(f))}function Pe(f){let p=[],t=(e,r,n)=>p.push({path:e,rule:r,message:n,severity:"error"}),s=(e,r,n)=>p.push({path:e,rule:r,message:n,severity:"warning"}),$=()=>{let e=p.filter(n=>n.severity==="error"),r=p.filter(n=>n.severity==="warning");return{valid:e.length===0,findings:p,errors:e,warnings:r}};if(typeof f!="object"||f===null||Array.isArray(f))return t("","bundle-shape","Bundle must be an object with project, nodes, and edges."),$();let w=f,m=w.project,R=w.nodes,T=w.edges;if(!m||!R||!T)return t("","bundle-shape","Missing top-level keys (project, nodes, edges)."),$();if(!Array.isArray(R))return t("nodes","bundle-shape","nodes must be an array."),$();if(!Array.isArray(T))return t("edges","bundle-shape","edges must be an array."),$();let E=R,L=T,k=new Map,F=new Set;m.id||t("project.id","project-id-required","project.id is missing"),(typeof m.title!="string"||!m.title.trim())&&t("project.title","title-non-empty","project.title is missing or empty"),m.created_at||t("project.created_at","timestamp-required","project.created_at is missing"),m.updated_at||t("project.updated_at","timestamp-required","project.updated_at is missing"),m.created_at&&!Me(m.created_at)&&s("project.created_at","iso-timestamp","project.created_at is not a valid ISO 8601 timestamp"),m.updated_at&&!Me(m.updated_at)&&s("project.updated_at","iso-timestamp","project.updated_at is not a valid ISO 8601 timestamp");let O=m.metadata;O&&O.view_card_variant!==void 0&&!Ie.includes(O.view_card_variant)&&t("project.metadata.view_card_variant","view-card-variant",`project.metadata.view_card_variant must be one of ${Ie.join(", ")} (import rejects other values)`),E.forEach((e,r)=>{let n=`nodes[${r}]`,i=e.id;F.has(i)&&t(`${n}.id`,"duplicate-node-id",`Duplicate node ID: ${i}`),F.add(i),k.set(i,e),(typeof e.title!="string"||!e.title.trim())&&t(`${n}.title`,"title-non-empty",`Node ${i}: title is missing or empty`);let a=e.species,o=_e[a];o&&!(typeof i=="string"&&i.startsWith(o))&&t(`${n}.id`,"species-prefix",`Node ${i}: species "${a}" should have prefix "${o}"`),H.includes(a)||t(`${n}.species`,"valid-species",`Node ${i}: invalid species "${a}"`),e.project_id!==m.id&&t(`${n}.project_id`,"project-id-match",`Node ${i}: project_id "${e.project_id}" does not match project.id "${m.id}"`),K.includes(e.status)||t(`${n}.status`,"valid-status",`Node ${i}: invalid status "${e.status}"`);let l=e.platforms;if(!l||l.length===0)a!=="decision"&&t(`${n}.platforms`,"platforms-non-empty",`Node ${i}: platforms array is empty or missing`);else for(let d of l)B.includes(d)||t(`${n}.platforms`,"valid-platform",`Node ${i}: invalid platform "${d}"`);let u=e.metadata||{};u.stage!==void 0&&!Oe.includes(u.stage)&&t(`${n}.metadata.stage`,"valid-stage",`Node ${i}: invalid metadata.stage "${u.stage}"`);let S=u.platformNotes;if(S)for(let d of Object.keys(S))B.includes(d)||t(`${n}.metadata.platformNotes`,"valid-platform",`Node ${i}: platformNotes has invalid platform "${d}"`);let b=u.platformStatuses;if(b)for(let[d,y]of Object.entries(b))B.includes(d)?Array.isArray(l)&&!l.includes(d)&&s(`${n}.metadata.platformStatuses`,"platform-statuses-subset",`Node ${i}: platformStatuses covers platform "${d}" not in node.platforms`):t(`${n}.metadata.platformStatuses`,"valid-platform",`Node ${i}: platformStatuses has invalid platform "${d}"`),K.includes(y)||t(`${n}.metadata.platformStatuses`,"valid-status",`Node ${i}: platformStatuses has invalid status "${y}" for platform "${d}"`);let I=u.platformScreenshots;if(I){for(let[d,y]of Object.entries(I))if(typeof y=="string"&&y.startsWith("data:")){let P=Ce(y);P>Re&&s(`${n}.metadata.platformScreenshots`,"screenshot-data-uri-size",`Node ${i}: platformScreenshots.${d} is a ${(P/(1024*1024)).toFixed(1)}MB data URI, above the ${Re/(1024*1024)}MB guidance (docs/spec/bundle-format.md \xA7 Asset Values) \u2014 consider a relative path or hosted URL instead`)}}let M=u.gherkin,C=u.values;if(a==="acceptance"?((typeof M!="string"||!M.trim())&&s(`${n}.metadata.gherkin`,"acceptance-gherkin-missing",`Acceptance ${i}: metadata.gherkin is missing or empty (one Given/When/Then scenario expected)`),(!Array.isArray(C)||C.length===0)&&s(`${n}.metadata.values`,"acceptance-values-missing",`Acceptance ${i}: metadata.values is missing or empty (assign 1-3 value elements)`)):(M!==void 0&&s(`${n}.metadata.gherkin`,"gherkin-species",`Node ${i}: metadata.gherkin is only meaningful on acceptance nodes`),C!==void 0&&s(`${n}.metadata.values`,"values-species",`Node ${i}: metadata.values is only meaningful on acceptance nodes`)),Array.isArray(C))for(let d of C)he.includes(d)||t(`${n}.metadata.values`,"valid-value",`Node ${i}: invalid value element "${d}"`);let h=u.refs;if(Array.isArray(h)){let d=new Set;h.forEach((y,P)=>{let A=y??{},D=`${n}.metadata.refs[${P}]`,N=A.id;typeof N!="string"||!N.trim()?t(`${D}.id`,"ref-id-required",`Node ${i}: ref is missing an id`):d.has(N)?t(`${D}.id`,"duplicate-ref-id",`Node ${i}: duplicate ref id "${N}"`):d.add(N),(typeof A.url!="string"||!A.url.trim())&&t(`${D}.url`,"ref-url-required",`Node ${i}: ref "${N}" is missing a url`),A.status_mapped!==void 0&&!K.includes(A.status_mapped)&&t(`${D}.status_mapped`,"valid-status",`Node ${i}: ref "${N}" has invalid status_mapped "${A.status_mapped}"`)})}if(u.decision_status!==void 0&&a!=="decision"&&s(`${n}.metadata.decision_status`,"decision-status-wrong-species",`decision_status is meaningful on decision nodes only; "${i}" is a ${a}.`),a==="decision"){let d=we({metadata:e.metadata}),y=ve(d);e.status!==y&&s(`${n}.status`,"decision-lifecycle-mismatch",`Decision "${i}" is ${d}, whose lifecycle status should be "${y}", but status is "${e.status}" (spec \xA72).`)}if(a==="flow"){let d=u.playlist;!e.metadata||!d||!d.entries?t(`${n}.metadata.playlist`,"flow-playlist-required",`Flow ${i}: missing metadata.playlist.entries`):d.entries.length===0&&s(`${n}.metadata.playlist`,"flow-playlist-empty",`Flow ${i}: playlist is empty`)}});let q=m.root_node_id;q&&!F.has(q)&&t("project.root_node_id","root-node-exists",`project.root_node_id "${q}" does not reference an existing node`);let G=(e,r,n)=>{if(typeof e!="object"||e===null||Array.isArray(e))return;let i=e,a=[["flow_platforms",Ee],["view_platforms",Ae],["minimap_color",ke]];for(let[o,l]of a){let u=i[o];u!==void 0&&(typeof u!="string"||!l.includes(u))&&s(`${r}.${o}`,"map-unknown-display",`${n} sets ${o} to "${String(u)}"; expected one of ${l.join(", ")} (renderers fall back to the default)`)}i.images!==void 0&&typeof i.images!="boolean"&&s(`${r}.images`,"map-unknown-display",`${n} sets images to a non-boolean value (renderers fall back to the default)`)},J=O?.map_display;if(typeof J=="object"&&J!==null&&!Array.isArray(J))for(let[e,r]of Object.entries(J))G(r,`project.metadata.map_display.${e}`,`Map "${e}"`);let c=O?.maps;if(Array.isArray(c)){let e=new Set;c.forEach((r,n)=>{if(typeof r!="object"||r===null||Array.isArray(r))return;let i=r,a=`project.metadata.maps[${n}]`,o=typeof i.id=="string"?i.id:void 0;o!==void 0&&(e.has(o)&&s(`${a}.id`,"map-duplicate-id",`Duplicate map id "${o}"`),e.add(o),xe(o)&&s(`${a}.id`,"map-shadows-built-in",`Map id "${o}" shadows a built-in map and will be ignored`));let l=i.root_node_id;typeof l=="string"&&!F.has(l)&&s(`${a}.root_node_id`,"map-unknown-root",`Map "${o??n}" anchors on "${l}" which does not reference an existing node`),Array.isArray(i.species)&&i.species.forEach((u,S)=>{typeof u=="string"&&!H.includes(u)&&s(`${a}.species[${S}]`,"map-unknown-species",`Map "${o??n}" filters on unknown species "${u}"`)}),Array.isArray(i.edge_types)&&i.edge_types.forEach((u,S)=>{typeof u=="string"&&!ne.includes(u)&&s(`${a}.edge_types[${S}]`,"map-unknown-edge-type",`Map "${o??n}" filters on unknown edge type "${u}"`)}),G(i.display,`${a}.display`,`Map "${o??n}"`)})}let g=O?.products,v=new Set;if(Array.isArray(g)){let e=new Set;g.forEach((r,n)=>{if(typeof r!="object"||r===null||Array.isArray(r))return;let i=r,a=`project.metadata.products[${n}]`,o=typeof i.id=="string"?i.id:void 0;o!==void 0&&(e.has(o)?s(`${a}.id`,"product-duplicate-id",`Duplicate product id "${o}" \u2014 the first wins`):(e.add(o),o.trim()!==""&&v.add(o)),/^[a-z0-9]+(-[a-z0-9]+)*$/.test(o)||s(`${a}.id`,"product-invalid-id",`Product id "${o}" is not kebab-case`))})}let x=v.size>0,j=new Map;for(let e of L){if(e.edge_type!=="covers")continue;let r=typeof e.source_id=="string"?e.source_id:void 0,n=typeof e.target_id=="string"?e.target_id:void 0;if(r===void 0||n===void 0)continue;let i=j.get(r)??[];i.push(n),j.set(r,i)}let V=new Map;if(x)for(let e of be({metadata:O}))V.set(e.id,new Set(Array.isArray(e.platforms)?e.platforms:[]));let oe=new Map,ae=new Map;if(E.forEach((e,r)=>{let n=typeof e.id=="string"?e.id:`#${r}`,i=e.species,a=`nodes[${r}]`;ae.set(n,r);let o=e.metadata??{},l=typeof o.product=="string"?o.product:void 0,u=i!==void 0&&$e.includes(i);if(l!==void 0&&!u){let I=H.includes(i)?`${i} membership is derived from consumers and must not be stored`:"metadata.product is only meaningful on flow, view, and acceptance nodes";s(`${a}.metadata.product`,"product-membership-wrong-species",`Node ${n}: ${I}`);return}if(!u)return;if(l===void 0){if(!x)return;i==="acceptance"?j.has(n)||s(`${a}.metadata.product`,"acceptance-product-unassigned",`Acceptance ${n} covers nothing and names no product \u2014 it will show only under "All products"`):s(`${a}.metadata.product`,"unassigned-membership",`Node ${n}: no product membership \u2014 it will show only under "All products"`);return}if(oe.set(n,l),!v.has(l)){s(`${a}.metadata.product`,"product-unknown-reference",`Node ${n}: product "${l}" is not declared on the project`);return}let S=V.get(l),b=Array.isArray(e.platforms)?e.platforms:[];if(S)for(let I of b)typeof I=="string"&&!S.has(I)&&s(`${a}.platforms`,"product-platform-not-in-menu",`Node ${n}: platform "${I}" is not in product "${l}"'s menu`)}),x)for(let[e,r]of j){let n=ae.get(e);if(n===void 0)continue;let i=new Set(r.map(a=>oe.get(a)).filter(a=>!!a));i.size>1&&s(`nodes[${n}].metadata.product`,"acceptance-covers-span-products",`Acceptance ${e} covers anchors in ${[...i].sort().join(" and ")} \u2014 statuses may conflate products`)}let de=new Set,ce=new Set,le=new Set;L.forEach((e,r)=>{let n=`edges[${r}]`,i=e.id,a=e.source_id,o=e.target_id,l=e.edge_type;de.has(i)&&t(`${n}.id`,"duplicate-edge-id",`Duplicate edge ID: ${i}`),de.add(i);let u=`e-${a}-${o}`;i!==u&&s(`${n}.id`,"edge-id-convention",`Edge ${i}: id does not match convention "${u}" (stale after a rename?)`);let S=`${a}->${o}:${l}`;ce.has(S)&&s(`${n}`,"duplicate-edge-relationship",`Duplicate edge relationship: ${S}`),ce.add(S),e.project_id!==m.id&&t(`${n}.project_id`,"project-id-match",`Edge ${i}: project_id does not match project.id`),F.has(a)||t(`${n}.source_id`,"dangling-edge",`Edge ${i}: source_id "${a}" not found in nodes`),F.has(o)||t(`${n}.target_id`,"dangling-edge",`Edge ${i}: target_id "${o}" not found in nodes`),ne.includes(l)||t(`${n}.edge_type`,"valid-edge-type",`Edge ${i}: invalid edge_type "${l}"`);let b=k.get(a),I=k.get(o),M=me[l];b&&I&&M&&(M.some(([h,_])=>h===b.species&&_===I.species)||t(`${n}.edge_type`,"edge-semantics",`Edge ${i}: "${l}" not valid from ${b.species} to ${I.species}`)),l==="composes"&&le.add(`${a}->${o}`)});let ue=["view","flow","condition","junction"],Z=(e,r,n,i=0)=>{if(!(i>50)){if(!Array.isArray(e)){t(n,"playlist-entry-shape",`Flow ${r}: ${n} must be an array of playlist entries`);return}e.forEach((a,o)=>{let l=`${n}[${o}]`;if(typeof a!="object"||a===null||Array.isArray(a)){t(l,"playlist-entry-shape",`Flow ${r}: ${l} must be a playlist entry object`);return}let u=a,S=u.type;if(typeof S!="string"||!ue.includes(S)){t(`${l}.type`,"playlist-entry-shape",`Flow ${r}: ${l}.type must be one of ${ue.join(", ")} (got ${JSON.stringify(S)})`);return}if(S==="view")typeof u.view_id!="string"&&t(`${l}.view_id`,"playlist-entry-shape",`Flow ${r}: ${l} is a view entry with no view_id string`);else if(S==="flow")typeof u.flow_id!="string"&&t(`${l}.flow_id`,"playlist-entry-shape",`Flow ${r}: ${l} is a flow entry with no flow_id string`);else if(S==="condition"){typeof u.label!="string"&&t(`${l}.label`,"playlist-entry-shape",`Flow ${r}: ${l} is a condition with no label string`);for(let b of["if_true","if_false"])Array.isArray(u[b])?Z(u[b],r,`${l}.${b}`,i+1):t(`${l}.${b}`,"playlist-entry-shape",`Flow ${r}: ${l}.${b} must be an array of playlist entries (both branches are required, use [] for an empty one)`)}else{if(typeof u.label!="string"&&t(`${l}.label`,"playlist-entry-shape",`Flow ${r}: ${l} is a junction with no label string`),!Array.isArray(u.cases)){t(`${l}.cases`,"playlist-entry-shape",`Flow ${r}: ${l}.cases must be an array of junction cases`);return}u.cases.forEach((b,I)=>{let M=`${l}.cases[${I}]`;if(typeof b!="object"||b===null||Array.isArray(b)){t(M,"playlist-entry-shape",`Flow ${r}: ${M} must be a junction case object`);return}let C=b;typeof C.label!="string"&&t(`${M}.label`,"playlist-entry-shape",`Flow ${r}: ${M} has no label string`),Array.isArray(C.entries)?Z(C.entries,r,`${M}.entries`,i+1):t(`${M}.entries`,"playlist-entry-shape",`Flow ${r}: ${M}.entries must be an array of playlist entries \u2014 a junction case holds its own entries, not a bare view_id/flow_id`)})}})}};E.forEach((e,r)=>{let n=e.metadata;e.species==="flow"&&n?.playlist?.entries&&Z(n.playlist.entries,e.id,`nodes[${r}].metadata.playlist.entries`)});let z=(e,r,n,i=0)=>{if(i>50)return t(n,"playlist-depth",`Flow ${r}: playlist nesting too deep (possible cycle)`),[];let a=[];if(!Array.isArray(e))return a;for(let o of e)if(!(typeof o!="object"||o===null)){if(o.type==="view")F.has(o.view_id)||t(n,"playlist-ref-exists",`Flow ${r}: playlist references non-existent view "${o.view_id}"`),a.push(o.view_id);else if(o.type==="flow")F.has(o.flow_id)||t(n,"playlist-ref-exists",`Flow ${r}: playlist references non-existent flow "${o.flow_id}"`),o.flow_id===r&&t(n,"playlist-self-cycle",`Flow ${r}: playlist contains itself (direct cycle)`),a.push(o.flow_id);else if(o.type==="condition")o.if_true&&a.push(...z(o.if_true,r,n,i+1)),o.if_false&&a.push(...z(o.if_false,r,n,i+1));else if(o.type==="junction"&&o.cases)for(let l of o.cases)a.push(...z(l.entries||[],r,n,i+1))}return a};E.forEach((e,r)=>{let n=e.metadata;if(e.species==="flow"&&n?.playlist?.entries){let i=`nodes[${r}].metadata.playlist`,a=e.id,o=z(n.playlist.entries,a,i);for(let l of o)le.has(`${a}->${l}`)||t(i,"playlist-composes-coherence",`Flow ${a}: playlist references "${l}" but no composes edge exists`)}});let ee=new Map,pe=new Map;E.forEach((e,r)=>{let n=e.metadata;if(e.species==="flow"&&n?.playlist?.entries){let i=e.id,a=[],o=l=>{if(Array.isArray(l)){for(let u of l)if(!(typeof u!="object"||u===null)&&(u.type==="flow"&&a.push(u.flow_id),u.type==="condition"&&(u.if_true&&o(u.if_true),u.if_false&&o(u.if_false)),u.type==="junction"&&u.cases))for(let S of u.cases)o(S.entries||[])}};o(n.playlist.entries),ee.set(i,a),pe.set(i,`nodes[${r}].metadata.playlist`)}});let fe=new Set,te=new Set,ge=e=>{if(te.has(e))return!0;if(fe.has(e))return!1;fe.add(e),te.add(e);for(let r of ee.get(e)||[])if(ge(r))return t(pe.get(e)??"","flow-cycle",`Cycle detected: flow "${e}" -> "${r}"`),!0;return te.delete(e),!1};for(let e of ee.keys())ge(e);let W=w.quality;if(typeof W=="object"&&W!==null&&!Array.isArray(W)){let e=W,r=Array.isArray(e.assessments)?e.assessments:[],n=Array.isArray(e.findings)?e.findings:[],i=typeof e.profile=="object"&&e.profile!==null?e.profile:void 0,a=new Set;Array.isArray(i?.surfaces)&&i.surfaces.forEach((h,_)=>{if(typeof h!="object"||h===null||Array.isArray(h))return;let d=h,y=typeof d.id=="string"?d.id:void 0;y===void 0||y.trim()===""||(a.has(y)&&s(`quality.profile.surfaces[${_}].id`,"quality-duplicate-surface",`Duplicate surface id "${y}" \u2014 the first wins`),a.add(y))}),a.size===0&&(r.length>0||n.length>0)&&s("quality.profile.surfaces","quality-no-surfaces","Quality data is stored but the profile declares no surfaces \u2014 the matrix falls back to the surfaces the assessments name");let o=typeof e.library=="object"&&e.library!==null?e.library:void 0,l=new Set,u=new Map;if(Array.isArray(o?.criteria))for(let h of o.criteria){if(typeof h!="object"||h===null)continue;let _=h;typeof _.id=="string"&&(l.add(_.id),typeof _.superseded_by=="string"&&_.superseded_by!==""&&u.set(_.id,_.superseded_by))}else(r.length>0||n.length>0)&&s("quality.library","quality-library-missing","No criteria pack is embedded \u2014 criterion ids cannot be resolved to domains or weights; the matrix uses a synthesized library");(typeof e.framework_version!="string"||e.framework_version==="")&&s("quality.framework_version","quality-framework-version-missing","Quality section has no framework_version \u2014 scores are not comparable across audits without one");let S=(h,_,d)=>{let y=typeof h.criterion_id=="string"?h.criterion_id:void 0;y!==void 0&&l.size>0&&!l.has(y)&&s(`${_}.criterion_id`,"quality-unknown-criterion",`${d}: criterion "${y}" is not in the pinned library`);let P=y!==void 0?u.get(y):void 0;P!==void 0&&s(`${_}.criterion_id`,"quality-retired-criterion",`${d}: criterion "${y}" is retired \u2014 superseded by "${P}"`);let A=typeof h.surface=="string"?h.surface:void 0;A!==void 0&&A!==X&&a.size>0&&!a.has(A)&&s(`${_}.surface`,"quality-unknown-surface",`${d}: surface "${A}" is not declared in the profile`)},b=new Set;r.forEach((h,_)=>{if(typeof h!="object"||h===null||Array.isArray(h))return;let d=h,y=`quality.assessments[${_}]`,P=`Assessment ${_}`;S(d,y,P);let A=d.level;(typeof A!="number"||!Number.isInteger(A)||A<0||A>4)&&s(`${y}.level`,"quality-level-range",`${P}: level must be an integer 0-4 (N/A is the absence of the row, not a level)`),d.surface===X&&s(`${y}.surface`,"quality-cross-surface-assessment",`${P}: "${X}" is a findings-only lens \u2014 it carries no matrix column, so this score would render nowhere`),(typeof d.evidence!="string"||d.evidence.trim()==="")&&s(`${y}.evidence`,"quality-assessment-no-evidence",`${P}: scored with no evidence \u2014 a score without a citation is an opinion, not an assessment`);let D=typeof d.criterion_id=="string"?d.criterion_id:void 0,N=typeof d.surface=="string"?d.surface:void 0;if(D!==void 0&&N!==void 0){let ye=`${D}\0${N}`;b.has(ye)&&s(`${y}`,"quality-duplicate-assessment",`${P}: a second score for (${D} x ${N}) \u2014 the section stores latest-per-cell, so this cell is ambiguous`),b.add(ye)}});let I=new Set;n.forEach((h,_)=>{if(typeof h!="object"||h===null||Array.isArray(h))return;let d=h,y=`quality.findings[${_}]`,P=`Finding ${_}`;S(d,y,P);let A=typeof d.id=="string"?d.id:void 0;A!==void 0&&(I.has(A)&&s(`${y}.id`,"quality-duplicate-finding-id",`Duplicate finding id "${A}"`),I.add(A));for(let D of["impact","likelihood"]){let N=d[D];(typeof N!="number"||!Number.isInteger(N)||N<1||N>5)&&s(`${y}.${D}`,"quality-risk-range",`${P}: ${D} must be an integer 1-5 \u2014 severity is derived from impact x likelihood`)}for(let D of["severity","priority"])D in d&&s(`${y}.${D}`,"quality-derived-field-stored",`${P}: ${D} is derived from impact, likelihood and cost \u2014 storing it lets it drift from the numbers behind it`);d.status==="accepted-risk"&&(typeof d.detail!="string"||d.detail.trim()==="")&&s(`${y}.detail`,"quality-accepted-risk-no-note",`${P}: accepted-risk with no note \u2014 an accepted risk is a decision and reads like one`)});let M=new Set,C=Array.isArray(w.journal)?w.journal:[];C.forEach(h=>{if(typeof h!="object"||h===null)return;let _=h;_.type==="quality.finding.opened"&&typeof _.finding_id=="string"&&M.add(_.finding_id)}),C.forEach((h,_)=>{if(typeof h!="object"||h===null)return;let d=h,y=typeof d.type=="string"?d.type:"";y.startsWith("quality.")&&((typeof d.actor!="string"||d.actor.trim()==="")&&s(`journal[${_}].actor`,"quality-event-no-actor",`Journal event ${_}: ${y} has no actor \u2014 human, agent and CI scores become indistinguishable`),y==="quality.finding.resolved"&&typeof d.finding_id=="string"&&!M.has(d.finding_id)&&!I.has(d.finding_id)&&s(`journal[${_}].finding_id`,"quality-resolved-never-opened",`Journal event ${_}: resolves finding "${d.finding_id}", which no quality.finding.opened event or stored finding ever declared`),y==="quality.finding.accepted"&&typeof d.finding_id=="string"&&!M.has(d.finding_id)&&!I.has(d.finding_id)&&s(`journal[${_}].finding_id`,"quality-accepted-never-opened",`Journal event ${_}: accepts finding "${d.finding_id}", which no quality.finding.opened event or stored finding ever declared`))})}for(let e of Se(w))p.push(e);return $()}var je="journal.jsonl",qe="journal";function Je(f){let p=(0,U.join)(f,qe);return(0,Q.existsSync)(p)?(0,Q.readdirSync)(p).filter(t=>t.startsWith("archive-")&&t.endsWith(".jsonl")).sort().map(t=>(0,U.join)(p,t)):[]}function Y(f,p){return f.filter(t=>t?.species===p).length}function Ge(){let f=process.argv[2];f||(console.error("Usage: node validate-bundle.js <path-to-bundle.json>"),process.exit(1)),(0,Q.existsSync)(f)||(console.error(`File not found: ${f}`),process.exit(1));let p;try{p=JSON.parse((0,Q.readFileSync)(f,"utf8"))}catch(k){console.error(`FATAL: Cannot parse JSON \u2014 ${k.message}`),process.exit(1)}let t=p;(typeof t!="object"||t===null||!t.project||!t.nodes||!t.edges)&&(console.error("FATAL: Missing top-level keys (project, nodes, edges)."),process.exit(1));let s=[],$=!1,w=[];if(t.journal===void 0){let k=(0,U.dirname)(f),F=(0,U.join)(k,je),O=[];if((0,Q.existsSync)(F)){let{events:q,findings:G}=re((0,Q.readFileSync)(F,"utf8"));for(let J of G)s.push(J.message);$=!0,O.push(...q)}for(let q of Je(k)){let G=(0,U.basename)(q),{events:J,findings:c}=re((0,Q.readFileSync)(q,"utf8"));for(let g of c)s.push(`${G} \u2014 ${g.message}`);w.push(G),O.push(...J)}($||w.length>0)&&(t.journal=O)}let m=Array.isArray(t.nodes)?t.nodes:[],R=Array.isArray(t.edges)?t.edges:[],T=Array.isArray(t.journal)?t.journal:[],E=Pe(p);if(console.log(`
|
|
14
14
|
Arkaik Bundle Validation`),console.log(` =======================
|
|
15
|
-
`),console.log(` Nodes: ${m.length} (${Y(m,"view")} views, ${Y(m,"flow")} flows, ${Y(m,"data-model")} data-models, ${Y(m,"api-endpoint")} api-endpoints, ${Y(m,"acceptance")} acceptances)`),console.log(` Edges: ${
|
|
15
|
+
`),console.log(` Nodes: ${m.length} (${Y(m,"view")} views, ${Y(m,"flow")} flows, ${Y(m,"data-model")} data-models, ${Y(m,"api-endpoint")} api-endpoints, ${Y(m,"acceptance")} acceptances)`),console.log(` Edges: ${R.length}`),$||w.length>0){let k=[...$?[`${je} sidecar`]:[],...w.length>0?[`${w.length} archive(s)`]:[]].join(" + ");console.log(` Journal: ${T.length} event(s) from ${k}`)}else T.length>0&&console.log(` Journal: ${T.length} embedded event(s)`);console.log(""),E.warnings.length>0&&(console.log(` Warnings: ${E.warnings.length}`),E.warnings.forEach(k=>console.log(` WARN: ${k.message}`)),console.log(""));let L=[...s,...E.errors.map(k=>k.message)];L.length===0&&(console.log(` Result: VALID
|
|
16
16
|
`),process.exit(0)),console.log(` Errors: ${L.length}`),L.forEach(k=>console.log(` ERROR: ${k}`)),console.log(`
|
|
17
17
|
Result: INVALID
|
|
18
18
|
`),process.exit(1)}Ge();
|
package/dist/index.js
CHANGED
|
@@ -15216,6 +15216,13 @@ var QualityFindingResolvedEventSchema = external_exports.object({
|
|
|
15216
15216
|
resolved_by: external_exports.string().optional(),
|
|
15217
15217
|
node_ids: external_exports.array(external_exports.string()).optional()
|
|
15218
15218
|
}).catchall(external_exports.unknown());
|
|
15219
|
+
var QualityFindingAcceptedEventSchema = external_exports.object({
|
|
15220
|
+
...envelope,
|
|
15221
|
+
type: external_exports.literal("quality.finding.accepted"),
|
|
15222
|
+
finding_id: external_exports.string(),
|
|
15223
|
+
reason: external_exports.string(),
|
|
15224
|
+
node_ids: external_exports.array(external_exports.string()).optional()
|
|
15225
|
+
}).catchall(external_exports.unknown());
|
|
15219
15226
|
var QualitySignalTrippedEventSchema = external_exports.object({
|
|
15220
15227
|
...envelope,
|
|
15221
15228
|
type: external_exports.literal("quality.signal.tripped"),
|
|
@@ -15243,6 +15250,7 @@ var JOURNAL_EVENT_SCHEMAS = {
|
|
|
15243
15250
|
"quality.audit.completed": QualityAuditCompletedEventSchema,
|
|
15244
15251
|
"quality.finding.opened": QualityFindingOpenedEventSchema,
|
|
15245
15252
|
"quality.finding.resolved": QualityFindingResolvedEventSchema,
|
|
15253
|
+
"quality.finding.accepted": QualityFindingAcceptedEventSchema,
|
|
15246
15254
|
"quality.signal.tripped": QualitySignalTrippedEventSchema
|
|
15247
15255
|
};
|
|
15248
15256
|
var KnownJournalEventSchema = external_exports.union([
|
|
@@ -15264,6 +15272,7 @@ var KnownJournalEventSchema = external_exports.union([
|
|
|
15264
15272
|
QualityAuditCompletedEventSchema,
|
|
15265
15273
|
QualityFindingOpenedEventSchema,
|
|
15266
15274
|
QualityFindingResolvedEventSchema,
|
|
15275
|
+
QualityFindingAcceptedEventSchema,
|
|
15267
15276
|
QualitySignalTrippedEventSchema
|
|
15268
15277
|
]);
|
|
15269
15278
|
|
|
@@ -15351,6 +15360,9 @@ var QualityFindingSchema = external_exports.object({
|
|
|
15351
15360
|
remediation: external_exports.string().optional(),
|
|
15352
15361
|
node_ids: external_exports.array(external_exports.string()).optional().meta({ description: "Graph nodes this finding is about \u2014 the tie deliverable.shipped already uses." }),
|
|
15353
15362
|
issue_url: external_exports.string().optional(),
|
|
15363
|
+
commit: external_exports.string().optional().meta({
|
|
15364
|
+
description: "Commit SHA anchoring the file:line evidence. Required by policy on any finding written away from a checkout (issue #400 decision 4) \u2014 without it a stale citation is indistinguishable from a wrong one."
|
|
15365
|
+
}),
|
|
15354
15366
|
verification: external_exports.object({ verdict: external_exports.enum(["CONFIRMED", "REFUTED", "DOWNGRADED"]), note: external_exports.string().optional() }).catchall(external_exports.unknown()).optional().meta({ description: "Result of the adversarial refutation pass. Refuted findings are disclosed, not deleted." })
|
|
15355
15367
|
}).catchall(external_exports.unknown()).meta({
|
|
15356
15368
|
id: "QualityFinding",
|
|
@@ -16611,6 +16623,15 @@ function validateBundle(input) {
|
|
|
16611
16623
|
);
|
|
16612
16624
|
}
|
|
16613
16625
|
}
|
|
16626
|
+
if (type === "quality.finding.accepted" && typeof event.finding_id === "string") {
|
|
16627
|
+
if (!openedInJournal.has(event.finding_id) && !seenFindingIds.has(event.finding_id)) {
|
|
16628
|
+
warn(
|
|
16629
|
+
`journal[${index}].finding_id`,
|
|
16630
|
+
"quality-accepted-never-opened",
|
|
16631
|
+
`Journal event ${index}: accepts finding "${event.finding_id}", which no quality.finding.opened event or stored finding ever declared`
|
|
16632
|
+
);
|
|
16633
|
+
}
|
|
16634
|
+
}
|
|
16614
16635
|
});
|
|
16615
16636
|
}
|
|
16616
16637
|
for (const finding of crossCheckJournal(bundle)) findings.push(finding);
|
|
@@ -17074,11 +17095,14 @@ function resolveFinding(findings, id, resolvedBy) {
|
|
|
17074
17095
|
...resolvedBy !== void 0 ? { resolved_by: resolvedBy } : {}
|
|
17075
17096
|
});
|
|
17076
17097
|
}
|
|
17077
|
-
function
|
|
17078
|
-
|
|
17079
|
-
const detail = existing === void 0 ? note : `${existing.detail}
|
|
17098
|
+
function acceptedDetail(detail, note) {
|
|
17099
|
+
return `${detail}
|
|
17080
17100
|
|
|
17081
17101
|
Accepted risk: ${note}`.trim();
|
|
17102
|
+
}
|
|
17103
|
+
function acceptFinding(findings, id, note) {
|
|
17104
|
+
const existing = findings.find((candidate) => candidate.id === id);
|
|
17105
|
+
const detail = existing === void 0 ? note : acceptedDetail(existing.detail, note);
|
|
17082
17106
|
return patchFinding(findings, id, { status: "accepted-risk", detail });
|
|
17083
17107
|
}
|
|
17084
17108
|
function fillTemplate(text, values) {
|
|
@@ -18915,11 +18939,13 @@ function foldQualitySection(bundle, root, auditId) {
|
|
|
18915
18939
|
notice: `Quality: folded ${section.assessments.length} assessment(s), ${section.findings.length} finding(s) from ${count} audit(s)`
|
|
18916
18940
|
};
|
|
18917
18941
|
}
|
|
18918
|
-
function resolveQualityRoot(
|
|
18919
|
-
if (root !== void 0) return resolve4(
|
|
18920
|
-
const
|
|
18942
|
+
function resolveQualityRoot(options) {
|
|
18943
|
+
if (options.root !== void 0) return resolve4(options.fallback, options.root);
|
|
18944
|
+
const envRoot = (options.env ?? process.env).ARKAIK_QUALITY_ROOT;
|
|
18945
|
+
if (envRoot) return resolve4(envRoot);
|
|
18946
|
+
const dir = dirname4(options.bundlePath);
|
|
18921
18947
|
if (basename3(dir) === "arkaik" && basename3(dirname4(dir)) === "docs") return resolve4(dir, "..", "..");
|
|
18922
|
-
return
|
|
18948
|
+
return options.fallback;
|
|
18923
18949
|
}
|
|
18924
18950
|
|
|
18925
18951
|
// src/commands/pack.ts
|
|
@@ -19033,7 +19059,7 @@ function runPack(options = {}) {
|
|
|
19033
19059
|
if (options.noQuality ?? false) {
|
|
19034
19060
|
delete bundle.quality;
|
|
19035
19061
|
} else {
|
|
19036
|
-
const root = resolveQualityRoot(
|
|
19062
|
+
const root = resolveQualityRoot({ root: options.root, bundlePath: filePath, fallback: cwd });
|
|
19037
19063
|
try {
|
|
19038
19064
|
const fold = foldQualitySection(bundle, root, options.audit);
|
|
19039
19065
|
qualityFolded = fold.folded;
|
|
@@ -19960,7 +19986,7 @@ async function runRestore(options = {}) {
|
|
|
19960
19986
|
const local = localRaw;
|
|
19961
19987
|
const journalEvents = loadJournalEvents(local, bundlePath);
|
|
19962
19988
|
const outboundBundle = { ...local, journal: journalEvents };
|
|
19963
|
-
const qualityRoot = resolveQualityRoot(
|
|
19989
|
+
const qualityRoot = resolveQualityRoot({ root: options.root, bundlePath, fallback: cwd });
|
|
19964
19990
|
let qualityFolded = false;
|
|
19965
19991
|
let qualityNotice;
|
|
19966
19992
|
if (noQuality) {
|
|
@@ -22593,7 +22619,7 @@ Options:
|
|
|
22593
22619
|
-v, --version Print the version.
|
|
22594
22620
|
|
|
22595
22621
|
Run "arkaik <command> --help" for command-specific help.`;
|
|
22596
|
-
var VERSION = "0.
|
|
22622
|
+
var VERSION = "0.4.0";
|
|
22597
22623
|
function main(argv) {
|
|
22598
22624
|
const [command, ...rest] = argv;
|
|
22599
22625
|
if (command === void 0 || command === "--help" || command === "-h" || command === "help") {
|
package/dist/io.js
CHANGED
|
@@ -15203,6 +15203,13 @@ var QualityFindingResolvedEventSchema = external_exports.object({
|
|
|
15203
15203
|
resolved_by: external_exports.string().optional(),
|
|
15204
15204
|
node_ids: external_exports.array(external_exports.string()).optional()
|
|
15205
15205
|
}).catchall(external_exports.unknown());
|
|
15206
|
+
var QualityFindingAcceptedEventSchema = external_exports.object({
|
|
15207
|
+
...envelope,
|
|
15208
|
+
type: external_exports.literal("quality.finding.accepted"),
|
|
15209
|
+
finding_id: external_exports.string(),
|
|
15210
|
+
reason: external_exports.string(),
|
|
15211
|
+
node_ids: external_exports.array(external_exports.string()).optional()
|
|
15212
|
+
}).catchall(external_exports.unknown());
|
|
15206
15213
|
var QualitySignalTrippedEventSchema = external_exports.object({
|
|
15207
15214
|
...envelope,
|
|
15208
15215
|
type: external_exports.literal("quality.signal.tripped"),
|
|
@@ -15230,6 +15237,7 @@ var JOURNAL_EVENT_SCHEMAS = {
|
|
|
15230
15237
|
"quality.audit.completed": QualityAuditCompletedEventSchema,
|
|
15231
15238
|
"quality.finding.opened": QualityFindingOpenedEventSchema,
|
|
15232
15239
|
"quality.finding.resolved": QualityFindingResolvedEventSchema,
|
|
15240
|
+
"quality.finding.accepted": QualityFindingAcceptedEventSchema,
|
|
15233
15241
|
"quality.signal.tripped": QualitySignalTrippedEventSchema
|
|
15234
15242
|
};
|
|
15235
15243
|
var KnownJournalEventSchema = external_exports.union([
|
|
@@ -15251,6 +15259,7 @@ var KnownJournalEventSchema = external_exports.union([
|
|
|
15251
15259
|
QualityAuditCompletedEventSchema,
|
|
15252
15260
|
QualityFindingOpenedEventSchema,
|
|
15253
15261
|
QualityFindingResolvedEventSchema,
|
|
15262
|
+
QualityFindingAcceptedEventSchema,
|
|
15254
15263
|
QualitySignalTrippedEventSchema
|
|
15255
15264
|
]);
|
|
15256
15265
|
|
|
@@ -15338,6 +15347,9 @@ var QualityFindingSchema = external_exports.object({
|
|
|
15338
15347
|
remediation: external_exports.string().optional(),
|
|
15339
15348
|
node_ids: external_exports.array(external_exports.string()).optional().meta({ description: "Graph nodes this finding is about \u2014 the tie deliverable.shipped already uses." }),
|
|
15340
15349
|
issue_url: external_exports.string().optional(),
|
|
15350
|
+
commit: external_exports.string().optional().meta({
|
|
15351
|
+
description: "Commit SHA anchoring the file:line evidence. Required by policy on any finding written away from a checkout (issue #400 decision 4) \u2014 without it a stale citation is indistinguishable from a wrong one."
|
|
15352
|
+
}),
|
|
15341
15353
|
verification: external_exports.object({ verdict: external_exports.enum(["CONFIRMED", "REFUTED", "DOWNGRADED"]), note: external_exports.string().optional() }).catchall(external_exports.unknown()).optional().meta({ description: "Result of the adversarial refutation pass. Refuted findings are disclosed, not deleted." })
|
|
15342
15354
|
}).catchall(external_exports.unknown()).meta({
|
|
15343
15355
|
id: "QualityFinding",
|
|
@@ -16422,6 +16434,15 @@ function validateBundle(input) {
|
|
|
16422
16434
|
);
|
|
16423
16435
|
}
|
|
16424
16436
|
}
|
|
16437
|
+
if (type === "quality.finding.accepted" && typeof event.finding_id === "string") {
|
|
16438
|
+
if (!openedInJournal.has(event.finding_id) && !seenFindingIds.has(event.finding_id)) {
|
|
16439
|
+
warn(
|
|
16440
|
+
`journal[${index}].finding_id`,
|
|
16441
|
+
"quality-accepted-never-opened",
|
|
16442
|
+
`Journal event ${index}: accepts finding "${event.finding_id}", which no quality.finding.opened event or stored finding ever declared`
|
|
16443
|
+
);
|
|
16444
|
+
}
|
|
16445
|
+
}
|
|
16425
16446
|
});
|
|
16426
16447
|
}
|
|
16427
16448
|
for (const finding of crossCheckJournal(bundle)) findings.push(finding);
|
|
@@ -16643,6 +16664,14 @@ function appendQualityEvents(root, inputs, options = {}) {
|
|
|
16643
16664
|
for (const event of events) appendJournalEvent(journal.journalPath, event);
|
|
16644
16665
|
return { journalPath: journal.journalPath, events, ...baseline !== void 0 ? { baseline } : {} };
|
|
16645
16666
|
}
|
|
16667
|
+
function resolveQualityRoot(options) {
|
|
16668
|
+
if (options.root !== void 0) return resolve2(options.fallback, options.root);
|
|
16669
|
+
const envRoot = (options.env ?? process.env).ARKAIK_QUALITY_ROOT;
|
|
16670
|
+
if (envRoot) return resolve2(envRoot);
|
|
16671
|
+
const dir = dirname3(options.bundlePath);
|
|
16672
|
+
if (basename(dir) === "arkaik" && basename(dirname3(dir)) === "docs") return resolve2(dir, "..", "..");
|
|
16673
|
+
return options.fallback;
|
|
16674
|
+
}
|
|
16646
16675
|
|
|
16647
16676
|
// src/lib/bundle-validate.ts
|
|
16648
16677
|
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
|
|
@@ -16719,5 +16748,6 @@ export {
|
|
|
16719
16748
|
readJournalEvents,
|
|
16720
16749
|
resolveJournal,
|
|
16721
16750
|
resolvePack,
|
|
16751
|
+
resolveQualityRoot,
|
|
16722
16752
|
validateBundleAt
|
|
16723
16753
|
};
|
package/package.json
CHANGED