arkaik 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -314,6 +314,7 @@ type KnownJournalEvent =
314
314
  | QualityAuditCompletedEvent
315
315
  | QualityFindingOpenedEvent
316
316
  | QualityFindingResolvedEvent
317
+ | QualityFindingAcceptedEvent
317
318
  | QualitySignalTrippedEvent;
318
319
 
319
320
  interface ProjectBundle {
@@ -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 G=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),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,U=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&&U.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=U.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 U=(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))U(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}"`)}),U(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,Q.join)(f,qe);return(0,G.existsSync)(p)?(0,G.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 Ue(){let f=process.argv[2];f||(console.error("Usage: node validate-bundle.js <path-to-bundle.json>"),process.exit(1)),(0,G.existsSync)(f)||(console.error(`File not found: ${f}`),process.exit(1));let p;try{p=JSON.parse((0,G.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,G.existsSync)(F)){let{events:q,findings:U}=re((0,G.readFileSync)(F,"utf8"));for(let J of U)s.push(J.message);$=!0,O.push(...q)}for(let q of Je(k)){let U=(0,Q.basename)(q),{events:J,findings:c}=re((0,G.readFileSync)(q,"utf8"));for(let g of c)s.push(`${U} \u2014 ${g.message}`);w.push(U),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: ${I.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
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
- `),process.exit(1)}Ge();
18
+ `),process.exit(1)}Ue();
package/dist/index.js CHANGED
@@ -15216,12 +15216,20 @@ 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"),
15222
15229
  criterion_id: external_exports.string(),
15223
15230
  surface: external_exports.string(),
15224
15231
  signal: external_exports.string(),
15232
+ commit: external_exports.string().optional(),
15225
15233
  detail: external_exports.string().optional()
15226
15234
  }).catchall(external_exports.unknown());
15227
15235
  var JOURNAL_EVENT_SCHEMAS = {
@@ -15243,6 +15251,7 @@ var JOURNAL_EVENT_SCHEMAS = {
15243
15251
  "quality.audit.completed": QualityAuditCompletedEventSchema,
15244
15252
  "quality.finding.opened": QualityFindingOpenedEventSchema,
15245
15253
  "quality.finding.resolved": QualityFindingResolvedEventSchema,
15254
+ "quality.finding.accepted": QualityFindingAcceptedEventSchema,
15246
15255
  "quality.signal.tripped": QualitySignalTrippedEventSchema
15247
15256
  };
15248
15257
  var KnownJournalEventSchema = external_exports.union([
@@ -15264,6 +15273,7 @@ var KnownJournalEventSchema = external_exports.union([
15264
15273
  QualityAuditCompletedEventSchema,
15265
15274
  QualityFindingOpenedEventSchema,
15266
15275
  QualityFindingResolvedEventSchema,
15276
+ QualityFindingAcceptedEventSchema,
15267
15277
  QualitySignalTrippedEventSchema
15268
15278
  ]);
15269
15279
 
@@ -15351,6 +15361,9 @@ var QualityFindingSchema = external_exports.object({
15351
15361
  remediation: external_exports.string().optional(),
15352
15362
  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
15363
  issue_url: external_exports.string().optional(),
15364
+ commit: external_exports.string().optional().meta({
15365
+ 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."
15366
+ }),
15354
15367
  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
15368
  }).catchall(external_exports.unknown()).meta({
15356
15369
  id: "QualityFinding",
@@ -15674,6 +15687,18 @@ function capGrade(grade, cap) {
15674
15687
  function isOpenFinding(finding) {
15675
15688
  return (finding.status ?? "open") === "open";
15676
15689
  }
15690
+ function rollUpSurface(scores, weights) {
15691
+ let weighted = 0;
15692
+ let total = 0;
15693
+ for (const domain2 of Object.keys(scores)) {
15694
+ const score = scores[domain2];
15695
+ if (typeof score !== "number" || !Number.isFinite(score)) continue;
15696
+ const weight = typeof weights?.[domain2] === "number" ? weights[domain2] : 1;
15697
+ weighted += score * weight;
15698
+ total += weight;
15699
+ }
15700
+ return total > 0 ? Math.round(weighted / total) : null;
15701
+ }
15677
15702
  function resolveKritikLibrary(section, library) {
15678
15703
  const explicit = library ?? section?.library;
15679
15704
  if (explicit && Array.isArray(explicit.criteria) && Array.isArray(explicit.domains)) return explicit;
@@ -15749,16 +15774,12 @@ function deriveQualityMatrix(bundle, library) {
15749
15774
  }
15750
15775
  const overall = {};
15751
15776
  for (const surface of surfaces) {
15752
- let weighted = 0;
15753
- let total = 0;
15777
+ const bySurface = {};
15754
15778
  for (const domain2 of domains) {
15755
15779
  const cell = matrix[domain2]?.[surface];
15756
- if (!cell) continue;
15757
- const weight = typeof weights?.[domain2] === "number" ? weights[domain2] : 1;
15758
- weighted += cell.score * weight;
15759
- total += weight;
15780
+ if (cell) bySurface[domain2] = cell.score;
15760
15781
  }
15761
- overall[surface] = total > 0 ? Math.round(weighted / total) : null;
15782
+ overall[surface] = rollUpSurface(bySurface, weights);
15762
15783
  }
15763
15784
  const finding_counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
15764
15785
  for (const finding of openFindings) finding_counts[severityOf(finding, pack)]++;
@@ -16611,6 +16632,15 @@ function validateBundle(input) {
16611
16632
  );
16612
16633
  }
16613
16634
  }
16635
+ if (type === "quality.finding.accepted" && typeof event.finding_id === "string") {
16636
+ if (!openedInJournal.has(event.finding_id) && !seenFindingIds.has(event.finding_id)) {
16637
+ warn(
16638
+ `journal[${index}].finding_id`,
16639
+ "quality-accepted-never-opened",
16640
+ `Journal event ${index}: accepts finding "${event.finding_id}", which no quality.finding.opened event or stored finding ever declared`
16641
+ );
16642
+ }
16643
+ }
16614
16644
  });
16615
16645
  }
16616
16646
  for (const finding of crossCheckJournal(bundle)) findings.push(finding);
@@ -17074,11 +17104,14 @@ function resolveFinding(findings, id, resolvedBy) {
17074
17104
  ...resolvedBy !== void 0 ? { resolved_by: resolvedBy } : {}
17075
17105
  });
17076
17106
  }
17077
- function acceptFinding(findings, id, note) {
17078
- const existing = findings.find((candidate) => candidate.id === id);
17079
- const detail = existing === void 0 ? note : `${existing.detail}
17107
+ function acceptedDetail(detail, note) {
17108
+ return `${detail}
17080
17109
 
17081
17110
  Accepted risk: ${note}`.trim();
17111
+ }
17112
+ function acceptFinding(findings, id, note) {
17113
+ const existing = findings.find((candidate) => candidate.id === id);
17114
+ const detail = existing === void 0 ? note : acceptedDetail(existing.detail, note);
17082
17115
  return patchFinding(findings, id, { status: "accepted-risk", detail });
17083
17116
  }
17084
17117
  function fillTemplate(text, values) {
@@ -17205,6 +17238,7 @@ function signalTrippedInput(trip) {
17205
17238
  criterion_id: trip.criterion_id,
17206
17239
  surface: trip.surface,
17207
17240
  signal: trip.signal,
17241
+ ...trip.commit !== void 0 ? { commit: trip.commit } : {},
17208
17242
  ...trip.detail !== void 0 ? { detail: trip.detail } : {}
17209
17243
  }
17210
17244
  };
@@ -17287,6 +17321,157 @@ function detectRegressions(previous, next, library) {
17287
17321
  return regressions;
17288
17322
  }
17289
17323
 
17324
+ // ../schema/src/quality-trend.ts
17325
+ var NO_DELTA = { previous: null, delta: null };
17326
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
17327
+ function frameworkMajor(version2) {
17328
+ if (typeof version2 !== "string") return null;
17329
+ const match = /^\s*v?(\d+)/.exec(version2);
17330
+ return match ? Number(match[1]) : null;
17331
+ }
17332
+ function sameFrameworkMajor(a, b) {
17333
+ const left = frameworkMajor(a);
17334
+ const right = frameworkMajor(b);
17335
+ return left === null || right === null || left === right;
17336
+ }
17337
+ function cleanScores(raw) {
17338
+ const scores = {};
17339
+ if (!isRecord(raw)) return scores;
17340
+ for (const surface of Object.keys(raw)) {
17341
+ const bySurface = raw[surface];
17342
+ if (!isRecord(bySurface)) continue;
17343
+ const cleaned = {};
17344
+ for (const domain2 of Object.keys(bySurface)) {
17345
+ const score = bySurface[domain2];
17346
+ if (typeof score === "number" && Number.isFinite(score)) cleaned[domain2] = score;
17347
+ }
17348
+ scores[surface] = cleaned;
17349
+ }
17350
+ return scores;
17351
+ }
17352
+ function cleanCounts(raw) {
17353
+ const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
17354
+ if (!isRecord(raw)) return counts;
17355
+ for (const severity of Object.keys(counts)) {
17356
+ const value = raw[severity];
17357
+ if (typeof value === "number" && Number.isFinite(value)) counts[severity] = value;
17358
+ }
17359
+ return counts;
17360
+ }
17361
+ function cellSet(scores) {
17362
+ const cells = /* @__PURE__ */ new Map();
17363
+ for (const surface of Object.keys(scores)) {
17364
+ for (const domain2 of Object.keys(scores[surface])) cells.set(`${surface}::${domain2}`, scores[surface][domain2]);
17365
+ }
17366
+ return cells;
17367
+ }
17368
+ function liveScores(live) {
17369
+ const scores = {};
17370
+ for (const domain2 of Object.keys(live.matrix ?? {})) {
17371
+ const row = live.matrix[domain2];
17372
+ if (!isRecord(row)) continue;
17373
+ for (const surface of Object.keys(row)) {
17374
+ const cell = row[surface];
17375
+ if (!cell || typeof cell.score !== "number") continue;
17376
+ (scores[surface] ??= {})[domain2] = cell.score;
17377
+ }
17378
+ }
17379
+ return scores;
17380
+ }
17381
+ function sameCells(a, b) {
17382
+ if (a.size !== b.size) return false;
17383
+ for (const [key, value] of a) {
17384
+ if (b.get(key) !== value) return false;
17385
+ }
17386
+ return true;
17387
+ }
17388
+ function deriveQualityTrend(events, profile, live) {
17389
+ const weights = profile?.domain_weights;
17390
+ const byAudit = /* @__PURE__ */ new Map();
17391
+ const audits = (Array.isArray(events) ? events : []).filter(
17392
+ (event) => isRecord(event) && event.type === "quality.audit.completed"
17393
+ );
17394
+ for (const event of orderEvents(audits)) {
17395
+ const auditId = typeof event.audit_id === "string" ? event.audit_id : typeof event.id === "string" ? event.id : "";
17396
+ const scores = cleanScores(event.scores);
17397
+ const overall = {};
17398
+ for (const surface of Object.keys(scores)) overall[surface] = rollUpSurface(scores[surface], weights);
17399
+ const snapshot = {
17400
+ audit_id: auditId,
17401
+ ts: typeof event.ts === "string" ? event.ts : "",
17402
+ ...typeof event.commit === "string" && event.commit !== "" ? { commit: event.commit } : {},
17403
+ ...typeof event.framework_version === "string" ? { framework_version: event.framework_version } : {},
17404
+ comparable: true,
17405
+ scores,
17406
+ overall,
17407
+ counts: cleanCounts(event.counts)
17408
+ };
17409
+ byAudit.delete(auditId);
17410
+ byAudit.set(auditId, snapshot);
17411
+ }
17412
+ const snapshots = [...byAudit.values()];
17413
+ for (let i = 1; i < snapshots.length; i++) {
17414
+ snapshots[i].comparable = sameFrameworkMajor(snapshots[i - 1].framework_version, snapshots[i].framework_version);
17415
+ }
17416
+ const latest = snapshots.length > 0 ? snapshots[snapshots.length - 1] : null;
17417
+ let previous = latest;
17418
+ if (live && latest !== null && sameCells(cellSet(latest.scores), cellSet(liveScores(live)))) {
17419
+ previous = snapshots.length > 1 ? snapshots[snapshots.length - 2] : null;
17420
+ }
17421
+ const comparable = previous !== null && live ? sameFrameworkMajor(previous.framework_version, live.framework_version) : false;
17422
+ const against = (prev, current) => {
17423
+ if (previous === null || typeof prev !== "number") return NO_DELTA;
17424
+ const delta = comparable && typeof current === "number" ? current - prev : null;
17425
+ return { previous: prev, delta, audit_id: previous.audit_id };
17426
+ };
17427
+ return {
17428
+ snapshots,
17429
+ latest,
17430
+ previous,
17431
+ deltaCell: (domain2, surface) => against(previous?.scores[surface]?.[domain2], live?.matrix?.[domain2]?.[surface]?.score),
17432
+ deltaOverall: (surface) => against(previous?.overall[surface], live?.overall?.[surface])
17433
+ };
17434
+ }
17435
+ function trendRows(trend, filter = {}) {
17436
+ const surfaces = [];
17437
+ for (const snapshot of trend.snapshots) {
17438
+ for (const surface of Object.keys(snapshot.scores)) {
17439
+ if (filter.surface !== void 0 && surface !== filter.surface) continue;
17440
+ if (!surfaces.includes(surface)) surfaces.push(surface);
17441
+ }
17442
+ }
17443
+ const valueOf = (snapshot, surface) => {
17444
+ if (filter.domain !== void 0) return snapshot.scores[surface]?.[filter.domain] ?? null;
17445
+ return snapshot.overall[surface] ?? null;
17446
+ };
17447
+ const rows = [];
17448
+ trend.snapshots.forEach((snapshot, index) => {
17449
+ const above = index > 0 ? trend.snapshots[index - 1] : null;
17450
+ const cells = {};
17451
+ for (const surface of surfaces) {
17452
+ const score = valueOf(snapshot, surface);
17453
+ const before = above === null ? null : valueOf(above, surface);
17454
+ const delta = snapshot.comparable && score !== null && before !== null ? score - before : null;
17455
+ cells[surface] = { score, delta };
17456
+ }
17457
+ rows.push({
17458
+ audit_id: snapshot.audit_id,
17459
+ ts: snapshot.ts,
17460
+ ...snapshot.commit !== void 0 ? { commit: snapshot.commit } : {},
17461
+ ...snapshot.framework_version !== void 0 ? { framework_version: snapshot.framework_version } : {},
17462
+ comparable: snapshot.comparable,
17463
+ cells
17464
+ });
17465
+ });
17466
+ return { surfaces, rows };
17467
+ }
17468
+ function formatDelta(delta) {
17469
+ if (typeof delta !== "number" || !Number.isFinite(delta)) return null;
17470
+ if (delta > 0) return `\u25B2 +${delta}`;
17471
+ if (delta < 0) return `\u25BC \u2212${Math.abs(delta)}`;
17472
+ return "=";
17473
+ }
17474
+
17290
17475
  // src/commands/init.ts
17291
17476
  var DEFAULT_BUNDLE_PATH = "docs/arkaik/bundle.json";
17292
17477
  var DEFAULT_JOURNAL_PATH = "docs/arkaik/journal.jsonl";
@@ -18915,11 +19100,13 @@ function foldQualitySection(bundle, root, auditId) {
18915
19100
  notice: `Quality: folded ${section.assessments.length} assessment(s), ${section.findings.length} finding(s) from ${count} audit(s)`
18916
19101
  };
18917
19102
  }
18918
- function resolveQualityRoot(cwd, filePath, root) {
18919
- if (root !== void 0) return resolve4(cwd, root);
18920
- const dir = dirname4(filePath);
19103
+ function resolveQualityRoot(options) {
19104
+ if (options.root !== void 0) return resolve4(options.fallback, options.root);
19105
+ const envRoot = (options.env ?? process.env).ARKAIK_QUALITY_ROOT;
19106
+ if (envRoot) return resolve4(envRoot);
19107
+ const dir = dirname4(options.bundlePath);
18921
19108
  if (basename3(dir) === "arkaik" && basename3(dirname4(dir)) === "docs") return resolve4(dir, "..", "..");
18922
- return cwd;
19109
+ return options.fallback;
18923
19110
  }
18924
19111
 
18925
19112
  // src/commands/pack.ts
@@ -19033,7 +19220,7 @@ function runPack(options = {}) {
19033
19220
  if (options.noQuality ?? false) {
19034
19221
  delete bundle.quality;
19035
19222
  } else {
19036
- const root = resolveQualityRoot(cwd, filePath, options.root);
19223
+ const root = resolveQualityRoot({ root: options.root, bundlePath: filePath, fallback: cwd });
19037
19224
  try {
19038
19225
  const fold = foldQualitySection(bundle, root, options.audit);
19039
19226
  qualityFolded = fold.folded;
@@ -19960,7 +20147,7 @@ async function runRestore(options = {}) {
19960
20147
  const local = localRaw;
19961
20148
  const journalEvents = loadJournalEvents(local, bundlePath);
19962
20149
  const outboundBundle = { ...local, journal: journalEvents };
19963
- const qualityRoot = resolveQualityRoot(cwd, bundlePath, options.root);
20150
+ const qualityRoot = resolveQualityRoot({ root: options.root, bundlePath, fallback: cwd });
19964
20151
  let qualityFolded = false;
19965
20152
  let qualityNotice;
19966
20153
  if (noQuality) {
@@ -21679,6 +21866,7 @@ Subcommands:
21679
21866
  matrix [audit-id] Roll an audit up (writes matrix.json).
21680
21867
  signals The signal pack, and what has tripped since the last audit.
21681
21868
  regressions What got worse between two audits.
21869
+ trend Every recorded audit's scores, and which way each moved.
21682
21870
  issue <criterion> Print the prefilled GitHub issue skeleton.
21683
21871
  criterion add ... Add a project-specific criterion to the overlay.
21684
21872
 
@@ -21766,6 +21954,21 @@ scheduled routine.
21766
21954
  --to <audit> The newer reading (default: the newest on disk).
21767
21955
  --record Append one quality.signal.tripped per regression.
21768
21956
  --json The full list as JSON.`;
21957
+ var TREND_USAGE = `arkaik kritik trend [--surface <s>] [--domain <CODE>] [--json]
21958
+
21959
+ Where the product stood at each recorded audit, oldest first \u2014 one row per
21960
+ quality.audit.completed in the journal, the overall score per surface, and
21961
+ how each moved against the row above (\u25B2 +6 / \u25BC \u22123 / =). The score, not the
21962
+ findings: "from where we started to where we are now".
21963
+
21964
+ --surface <s> One column only.
21965
+ --domain <CODE> That domain's score per surface instead of the roll-up.
21966
+ --json Print the snapshots and rows as JSON.
21967
+
21968
+ Reads the journal, so it needs a bundle (--bundle, default docs/arkaik/bundle.json)
21969
+ and audits recorded with \`arkaik kritik matrix --record\`. A framework major
21970
+ bump between two audits breaks the comparison there (SPEC \xA7 8): the row still
21971
+ prints, without an arrow.`;
21769
21972
  var ISSUE_USAGE = `arkaik kritik issue <criterion> --surface <s> [--level <n>] [--finding <id>]
21770
21973
 
21771
21974
  Print the criterion's GitHub issue skeleton, filled as far as what we know
@@ -22413,6 +22616,70 @@ function runRegressions(args, common) {
22413
22616
  }
22414
22617
  process.exit(regressions.length > 0 ? 1 : 0);
22415
22618
  }
22619
+ function runTrend(args, common) {
22620
+ const { single, flags } = collect(args, [], ["json"]);
22621
+ if (flags.has("help")) {
22622
+ console.log(TREND_USAGE);
22623
+ process.exit(0);
22624
+ }
22625
+ const journal = resolveJournal(common.root, common.bundlePath);
22626
+ if (!journal.present) {
22627
+ fail12(
22628
+ `kritik: no bundle at ${journal.bundlePath} \u2014 the trend is read from the journal's quality.audit.completed events, and there is no journal without a bundle.`
22629
+ );
22630
+ }
22631
+ const events = readFullJournalEvents(journal.journalPath);
22632
+ const profile = loadProfile(common.root);
22633
+ const trend = deriveQualityTrend(events, profile ?? void 0);
22634
+ const filter = {
22635
+ ...single.surface !== void 0 ? { surface: single.surface } : {},
22636
+ ...single.domain !== void 0 ? { domain: single.domain } : {}
22637
+ };
22638
+ const { surfaces, rows } = trendRows(trend, filter);
22639
+ if (flags.has("json")) {
22640
+ console.log(JSON.stringify({ total: trend.snapshots.length, surfaces, rows, snapshots: trend.snapshots }, null, 2));
22641
+ process.exit(0);
22642
+ }
22643
+ if (rows.length === 0) {
22644
+ console.log(`
22645
+ no recorded audits yet \u2014 \`arkaik kritik matrix --record\` writes one.
22646
+ `);
22647
+ process.exit(0);
22648
+ }
22649
+ if (surfaces.length === 0) {
22650
+ console.log(
22651
+ `
22652
+ ${rows.length} recorded audit${rows.length === 1 ? "" : "s"}, none scoring ` + (single.surface !== void 0 ? `"${single.surface}"` : "any surface") + `.
22653
+ `
22654
+ );
22655
+ process.exit(0);
22656
+ }
22657
+ const cellText = (row, surface) => {
22658
+ const cell = row.cells[surface];
22659
+ if (cell.score === null) return "\u2014";
22660
+ const arrow = formatDelta(cell.delta);
22661
+ return arrow === null ? String(cell.score) : `${cell.score} ${arrow}`;
22662
+ };
22663
+ const header = ["audit", "recorded", ...surfaces];
22664
+ const table = rows.map((row) => [row.audit_id, row.ts.slice(0, 10), ...surfaces.map((surface) => cellText(row, surface))]);
22665
+ const widths = header.map((title2, column) => Math.max(title2.length, ...table.map((line3) => line3[column].length)));
22666
+ const line2 = (cells) => ` ${cells.map((cell, column) => cell.padEnd(widths[column])).join(" ")}`;
22667
+ console.log("");
22668
+ console.log(line2(header));
22669
+ for (const [index, cells] of table.entries()) {
22670
+ console.log(line2(cells));
22671
+ const row = rows[index];
22672
+ if (!row.comparable) {
22673
+ console.log(` ${"".padEnd(widths[0])} framework ${row.framework_version ?? "?"} \u2014 not comparable to the audit above`);
22674
+ }
22675
+ }
22676
+ console.log(
22677
+ `
22678
+ ${rows.length} recorded audit${rows.length === 1 ? "" : "s"} \xB7 ` + (single.domain !== void 0 ? `${single.domain} score per surface` : "overall score per surface") + ` \xB7 arrows against the row above
22679
+ `
22680
+ );
22681
+ process.exit(0);
22682
+ }
22416
22683
  function runIssue(args, common) {
22417
22684
  const { single, flags, positionals } = collect(args);
22418
22685
  if (flags.has("help")) {
@@ -22554,6 +22821,8 @@ function runKritik(args) {
22554
22821
  return runSignals(subArgs, common);
22555
22822
  case "regressions":
22556
22823
  return runRegressions(subArgs, common);
22824
+ case "trend":
22825
+ return runTrend(subArgs, common);
22557
22826
  case "issue":
22558
22827
  return runIssue(subArgs, common);
22559
22828
  case "criterion":
@@ -22593,7 +22862,7 @@ Options:
22593
22862
  -v, --version Print the version.
22594
22863
 
22595
22864
  Run "arkaik <command> --help" for command-specific help.`;
22596
- var VERSION = "0.3.0";
22865
+ var VERSION = "0.5.0";
22597
22866
  function main(argv) {
22598
22867
  const [command, ...rest] = argv;
22599
22868
  if (command === void 0 || command === "--help" || command === "-h" || command === "help") {
package/dist/io.js CHANGED
@@ -15203,12 +15203,20 @@ 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"),
15209
15216
  criterion_id: external_exports.string(),
15210
15217
  surface: external_exports.string(),
15211
15218
  signal: external_exports.string(),
15219
+ commit: external_exports.string().optional(),
15212
15220
  detail: external_exports.string().optional()
15213
15221
  }).catchall(external_exports.unknown());
15214
15222
  var JOURNAL_EVENT_SCHEMAS = {
@@ -15230,6 +15238,7 @@ var JOURNAL_EVENT_SCHEMAS = {
15230
15238
  "quality.audit.completed": QualityAuditCompletedEventSchema,
15231
15239
  "quality.finding.opened": QualityFindingOpenedEventSchema,
15232
15240
  "quality.finding.resolved": QualityFindingResolvedEventSchema,
15241
+ "quality.finding.accepted": QualityFindingAcceptedEventSchema,
15233
15242
  "quality.signal.tripped": QualitySignalTrippedEventSchema
15234
15243
  };
15235
15244
  var KnownJournalEventSchema = external_exports.union([
@@ -15251,6 +15260,7 @@ var KnownJournalEventSchema = external_exports.union([
15251
15260
  QualityAuditCompletedEventSchema,
15252
15261
  QualityFindingOpenedEventSchema,
15253
15262
  QualityFindingResolvedEventSchema,
15263
+ QualityFindingAcceptedEventSchema,
15254
15264
  QualitySignalTrippedEventSchema
15255
15265
  ]);
15256
15266
 
@@ -15338,6 +15348,9 @@ var QualityFindingSchema = external_exports.object({
15338
15348
  remediation: external_exports.string().optional(),
15339
15349
  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
15350
  issue_url: external_exports.string().optional(),
15351
+ commit: external_exports.string().optional().meta({
15352
+ 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."
15353
+ }),
15341
15354
  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
15355
  }).catchall(external_exports.unknown()).meta({
15343
15356
  id: "QualityFinding",
@@ -16422,6 +16435,15 @@ function validateBundle(input) {
16422
16435
  );
16423
16436
  }
16424
16437
  }
16438
+ if (type === "quality.finding.accepted" && typeof event.finding_id === "string") {
16439
+ if (!openedInJournal.has(event.finding_id) && !seenFindingIds.has(event.finding_id)) {
16440
+ warn(
16441
+ `journal[${index}].finding_id`,
16442
+ "quality-accepted-never-opened",
16443
+ `Journal event ${index}: accepts finding "${event.finding_id}", which no quality.finding.opened event or stored finding ever declared`
16444
+ );
16445
+ }
16446
+ }
16425
16447
  });
16426
16448
  }
16427
16449
  for (const finding of crossCheckJournal(bundle)) findings.push(finding);
@@ -16643,6 +16665,14 @@ function appendQualityEvents(root, inputs, options = {}) {
16643
16665
  for (const event of events) appendJournalEvent(journal.journalPath, event);
16644
16666
  return { journalPath: journal.journalPath, events, ...baseline !== void 0 ? { baseline } : {} };
16645
16667
  }
16668
+ function resolveQualityRoot(options) {
16669
+ if (options.root !== void 0) return resolve2(options.fallback, options.root);
16670
+ const envRoot = (options.env ?? process.env).ARKAIK_QUALITY_ROOT;
16671
+ if (envRoot) return resolve2(envRoot);
16672
+ const dir = dirname3(options.bundlePath);
16673
+ if (basename(dir) === "arkaik" && basename(dirname3(dir)) === "docs") return resolve2(dir, "..", "..");
16674
+ return options.fallback;
16675
+ }
16646
16676
 
16647
16677
  // src/lib/bundle-validate.ts
16648
16678
  import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
@@ -16719,5 +16749,6 @@ export {
16719
16749
  readJournalEvents,
16720
16750
  resolveJournal,
16721
16751
  resolvePack,
16752
+ resolveQualityRoot,
16722
16753
  validateBundleAt
16723
16754
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaik",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Arkaik CLI — link a repository to a hosted product graph, validate and pack bundles, mirror external ref status.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/io.ts CHANGED
@@ -30,6 +30,7 @@ export {
30
30
  loadKritikLibrary,
31
31
  resolveJournal,
32
32
  appendQualityEvents,
33
+ resolveQualityRoot,
33
34
  type ResolvedPack,
34
35
  } from "./lib/kritik-io";
35
36
  export {