saffron-ai 0.9.0 → 0.9.1

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-pkg/cli.js CHANGED
@@ -1,69 +1,69 @@
1
1
  #!/usr/bin/env node
2
- import G from"node:fs";import M from"node:path";import Fd from"node:readline";import{Command as Nd}from"commander";import gi from"node:fs";import hi from"node:path";import{AstBuilder as mi,GherkinClassicTokenMatcher as yi,Parser as bi}from"@cucumber/gherkin";import{IdGenerator as wi,StepKeywordType as Bn}from"@cucumber/messages";function Ue(e,t){return e.replace(/<([^<>]+)>/g,(n,r)=>r in t?t[r]:n)}function ne(e){if(!e||e.length===0)return{};let t={};if(e.every(n=>n.length===2)){for(let[n,r]of e)t[`table:${n}`]=r;return t}if(e.length>=2){let[n,...r]=e;if(n.some(o=>!o))return{};r.forEach((o,s)=>{n.forEach((a,i)=>{o[i]!==void 0&&(t[`table:${s+1}:${a}`]=o[i])})})}return t}function Be(e){return e!==void 0?{docstring:e}:{}}function Ce(e){let t=new Map;for(let r of e){let o={...ne(r.resolvedTable??r.table),...Be(r.resolvedDocString??r.docString)};for(let[s,a]of Object.entries(o)){let i=t.get(s);i||t.set(s,i=new Set),i.add(a)}}let n={};for(let[r,o]of t)o.size===1&&(n[r]=[...o][0]);return n}import Xe from"node:fs";import _e from"node:path";var vt=/\{data:([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+)\}/g,et=_e.resolve("data"),he,Wt=new Map;function tt(e,t="data",n){et=_e.isAbsolute(t)?t:_e.join(e,t),he=n?.trim()||void 0,Wt.clear()}var je=()=>({dir:et,env:he});function os(e,t){let n=o=>o!==null&&typeof o=="object"&&!Array.isArray(o);if(!n(e)||!n(t))return t;let r={...e};for(let[o,s]of Object.entries(t))r[o]=o in e?os(e[o],s):s;return r}function ss(e,t){try{return JSON.parse(Xe.readFileSync(e,"utf8"))}catch(n){throw new Error(`data file ${t} is not valid JSON: ${n instanceof Error?n.message:String(n)}`)}}function si(e){if(Wt.has(e))return Wt.get(e);let t=_e.join(et,`${e}.json`),n=he?_e.join(et,`${e}.${he}.json`):void 0,r=Xe.existsSync(t),o=n!==void 0&&Xe.existsSync(n);if(!r&&!o)throw new Error(`data file ${_e.relative(process.cwd(),t)||t} does not exist${he?` (nor ${e}.${he}.json for the "${he}" environment)`:""}`);let s=r?ss(t,`${e}.json`):void 0;if(o){let a=ss(n,`${e}.${he}.json`);s=r?os(s,a):a}return Wt.set(e,s),s}function ze(e){let[t,...n]=e.split("."),r=si(t),o=[t];for(let s of n){let a=r,i=a&&typeof a=="object"?Array.isArray(a)?a[Number(s)]:a[s]:void 0;if(i===void 0)throw new Error(`"${s}" does not exist under ${o.join(".")} (in ${t}.json)`);r=i,o.push(s)}return r}function nt(e){let t;try{t=ze(e)}catch(n){throw new Error(`{data:${e}}: ${n instanceof Error?n.message:String(n)}`)}if(t===null||typeof t=="object")throw new Error(`{data:${e}} is ${Array.isArray(t)?"a list":"an object"}, not a single value. Point at one entry, for example {data:${e}.${Array.isArray(t)?"0":Object.keys(t??{})[0]??"key"}}`);return String(t)}function Oe(e){let t;try{t=ze(e)}catch(o){throw new Error(`{data:${e}}: ${o instanceof Error?o.message:String(o)}`)}let n=Array.isArray(t)?t:[t];if(n.length===0)throw new Error(`{data:${e}} is an empty list: there is nothing to check`);let r=n.find(o=>o===null||typeof o=="object");if(r!==void 0)throw new Error(`{data:${e}} must be a list of plain values (text, numbers); it contains ${Array.isArray(r)?"a list":"an object"}`);return n.map(String)}function Jt(e){Oe(e)}function as(e){return e.replace(vt,(t,n)=>{let r=Oe(n);return Array.isArray(ze(n))?`${t} (a data list, currently: ${r.map(s=>`"${s}"`).join(", ")})`:r[0]})}function oi(e){let t=[],n=[],r="",o=!1;for(let s=0;s<e.length;s++){let a=e[s];o?a==='"'&&e[s+1]==='"'?(r+='"',s++):a==='"'?o=!1:r+=a:a==='"'&&r===""?o=!0:a===","?(n.push(r),r=""):a===`
3
- `||a==="\r"?(a==="\r"&&e[s+1]===`
4
- `&&s++,n.push(r),r="",n.some(i=>i!=="")&&t.push(n),n=[]):r+=a}return n.push(r),n.some(s=>s!=="")&&t.push(n),t.map(s=>s.map(a=>a.trim()))}function Gt(e){let t=i=>{throw new Error(`Examples: {data:${e}}: ${i}`)},n=he?_e.join(et,`${e}.${he}.csv`):void 0,r=n&&Xe.existsSync(n)?n:_e.join(et,`${e}.csv`);if(!e.includes(".")&&Xe.existsSync(r)){let[i,...c]=oi(Xe.readFileSync(r,"utf8"));return(!i||c.length===0)&&t(`${e}.csv needs a header row and at least one data row`),{headers:i,rows:c.map(p=>Object.fromEntries(i.map((f,d)=>[f,p[d]??""])))}}let o;try{o=ze(e)}catch(i){return t(`${i instanceof Error?i.message:String(i)} (looked for ${e.split(".")[0]}.csv and ${e.split(".")[0]}.json in the data folder)`)}(!Array.isArray(o)||o.length===0||o.some(i=>i===null||typeof i!="object"||Array.isArray(i)))&&t('must be a non-empty list of records, like [{ "role": "Admin", "menu": "Users" }]');let s=o,a=[...new Set(s.flatMap(i=>Object.keys(i)))];return{headers:a,rows:s.map(i=>Object.fromEntries(a.map(c=>[c,i[c]===void 0||i[c]===null?"":String(i[c])])))}}function St(e){return e.replace(vt,(t,n)=>nt(n))}function qe(e){let t=new Set;for(let n of e)if(n)for(let r of n.matchAll(vt))t.add(r[1]);return t}function Kt(e){let t=new Map;for(let n of qe(e))try{t.set(n,nt(n))}catch{}return t}function is(e,t,n=new Set){let r=new Map;for(let[a,i]of t)r.set(i,[...r.get(i)??[],a]);let o=[];for(let[a,i]of r){let c=i.length===1?i:i.filter(p=>n.has(p));c.length===1&&o.push([c[0],a])}o.sort((a,i)=>i[1].length-a[1].length);for(let[a,i]of o)if(i!==""&&e===i)return`{data:${a}}`;let s=e;for(let[a,i]of o)i.length<4||(s=s.split(i).join(`{data:${a}}`));return s}import Ln from"node:fs";import Yt from"node:path";import{AstBuilder as ai,GherkinClassicTokenMatcher as ii,Parser as ci}from"@cucumber/gherkin";import{IdGenerator as li}from"@cucumber/messages";var Z=class extends Error{},di=/^(\s*)StepSet:\s*(.+?)\s*$/,pi=/^(\s*)StepSet\s+([^\s:].*?)\s*$/;function Un(e){let t=e.split(`
5
- `),n=new Map,r=new Map;return{source:t.map((s,a)=>{let i=s.match(di);if(i)return n.set(a+1,i[2]),`${i[1]}Scenario: ${i[2]}`;let c=s.match(pi);return c?(r.set(a+1,c[2]),`${c[1]}* StepSet ${c[2]}`):s}).join(`
6
- `),definitions:n,invocations:r}}function fi(e){return new ci(new ai(li.uuid()),new ii).parse(e)}function cs(e,t){for(let n of e)n.scenario?t(n.scenario):"rule"in n&&n.rule&&cs(n.rule.children,t)}function ve(e,t){let n=new Map;for(let r of e){let o=Yt.relative(t,r),s=Un(Ln.readFileSync(r,"utf8"));if(s.definitions.size===0)continue;let a=fi(s.source);a.feature&&cs(a.feature.children,i=>{let c=s.definitions.get(i.location.line);if(c===void 0)return;let p=n.get(c);if(p)throw new Z(`StepSet "${c}" is defined twice: ${p.file}:${p.line} and ${o}:${i.location.line}. Set names are unique across the project. If line ${i.location.line} was meant to INVOKE the set inside a scenario, remove the colon: "StepSet ${c}".`);if(i.examples.length>0)throw new Z(`StepSet "${c}" (${o}:${i.location.line}) has an Examples table \u2014 step sets take their values from the invoking scenario.`);for(let f of i.steps)if(s.invocations.has(f.location.line))throw new Z(`StepSet "${c}" (${o}:${i.location.line}) invokes another step set at line ${f.location.line} \u2014 step sets cannot be nested.`);n.set(c,{name:c,file:o,line:i.location.line,steps:[...i.steps]})})}return n}var ui=[".feature",".saffron"];function Se(e){return ui.some(t=>e.endsWith(t))}function ke(e,t,n){let r=Yt.isAbsolute(t.features??"features")?t.features:Yt.join(e,t.features??"features"),o=new Set(He(r));for(let s of n)s.endsWith(".saffron")&&o.add(s);return ve([...o].sort(),e)}function He(e){if(!Ln.existsSync(e))return[];let t=[];for(let n of Ln.readdirSync(e,{recursive:!0})){let r=String(n);r.endsWith(".saffron")&&t.push(Yt.join(e,r))}return t.sort()}function ls(e,t,n){return e.map(({step:r,source:o})=>{let s;switch(r.keywordType){case Bn.OUTCOME:s="assertion";break;case Bn.CONTEXT:case Bn.ACTION:s="action";break;default:s=n.value}n.value=s;let a=r.dataTable?r.dataTable.rows.map(c=>c.cells.map(p=>p.value)):void 0,i=r.docString?.content;return{keyword:r.keyword.trim(),kind:s,text:r.text,resolvedText:Ue(r.text,t),table:a,resolvedTable:a?.map(c=>c.map(p=>Ue(p,t))),docString:i,resolvedDocString:i!==void 0?Ue(i,t):void 0,source:o}})}function ds(e,t,n,r,o){let s=[];for(let a of e){let i=r.get(a.location.line);if(i===void 0){s.push({step:a,source:{file:t,line:a.location.line,fromBackground:n}});continue}let c=o?.get(i);if(!c)throw new Z(`${t}:${a.location.line} invokes StepSet "${i}", which is not defined anywhere in the project.`);for(let p of c.steps)s.push({step:p,source:{file:c.file,line:p.location.line,fromStepSet:c.name,fromBackground:n}})}return s}function xi(e,t,n,r,o,s=!1){let a=e.tags.map(c=>c.name),i=[];if(e.examples.length===0)i.push({params:{},label:""});else{let c=0;for(let p of e.examples){let f=p.name.trim().match(/^\{data:([A-Za-z0-9_.-]+)\}$/);if(f&&!p.tableHeader){let m=`${t}:${p.location.line}`;if(!s)throw new Z(`${m} reads Examples from a data file, which only .saffron files understand. Rename the file to ${t.replace(/\.feature$/,".saffron")}: nothing else needs to change.`);let h;try{h=Gt(f[1])}catch(k){throw new Z(`${m} ${k instanceof Error?k.message:String(k)}`)}let b=[...new Set([...r,...o].flatMap(k=>[k.step.text,k.step.docString?.content??"",...k.step.dataTable?.rows.flatMap(w=>w.cells.map(v=>v.value))??[]].flatMap(w=>[...w.matchAll(/<([^<>]+)>/g)].map(v=>v[1]))))].filter(k=>!h.headers.includes(k)&&!k.startsWith("table:")&&k!=="docstring");if(b.length>0)throw new Z(`${m} the scenario uses <${b.join(">, <")}>, but {data:${f[1]}} has the columns ${h.headers.map(k=>`"${k}"`).join(", ")}.`);for(let k of h.rows)c+=1,i.push({params:k,label:` (example ${c})`});continue}let d=p.tableHeader?.cells.map(m=>m.value)??[];for(let m of p.tableBody){c+=1;let h={};m.cells.forEach((y,b)=>{h[d[b]]=y.value}),i.push({params:h,label:` (example ${c})`})}}}return i.map(({params:c,label:p})=>{let f={value:"action"};return{featurePath:t,featureName:n,name:e.name,displayName:`${e.name}${p}`,tags:a,steps:[...ls(r,c,f),...ls(o,c,f)],params:c}})}function se(e,t,n){let r=gi.readFileSync(e,"utf8"),o=e.endsWith(".saffron"),s=o?Un(r):{source:r,definitions:new Map,invocations:new Map},a=new bi(new mi(wi.uuid()),new yi),i=hi.relative(t,e),c;try{c=a.parse(s.source)}catch(m){let h=(m instanceof Error?m.message:String(m)).split(`
7
- `).filter(b=>/^\(\d+:\d+\)/.test(b)).map(b=>`${i}${b.replace(/^\((\d+):(\d+)\)/,":$1:$2")}`),y=!o&&/^\s*StepSet\b/m.test(r);throw new Z(y?`${i} uses StepSet, which only .saffron files understand. Rename it to ${i.replace(/\.feature$/,".saffron")}: nothing else needs to change (a .saffron file accepts everything a .feature file does).`:`${i} is not valid Gherkin:
2
+ import G from"node:fs";import D from"node:path";import qd from"node:readline";import{Command as Hd}from"commander";import wi from"node:fs";import xi from"node:path";import{AstBuilder as Si,GherkinClassicTokenMatcher as vi,Parser as ki}from"@cucumber/gherkin";import{IdGenerator as $i,StepKeywordType as Hn}from"@cucumber/messages";function Be(e,t){return e.replace(/<([^<>]+)>/g,(n,r)=>r in t?t[r]:n)}function ne(e){if(!e||e.length===0)return{};let t={};if(e.every(n=>n.length===2)){for(let[n,r]of e)t[`table:${n}`]=r;return t}if(e.length>=2){let[n,...r]=e;if(n.some(s=>!s))return{};r.forEach((s,o)=>{n.forEach((a,i)=>{s[i]!==void 0&&(t[`table:${o+1}:${a}`]=s[i])})})}return t}function ze(e){return e!==void 0?{docstring:e}:{}}function _e(e){let t=new Map;for(let r of e){let s={...ne(r.resolvedTable??r.table),...ze(r.resolvedDocString??r.docString)};for(let[o,a]of Object.entries(s)){let i=t.get(o);i||t.set(o,i=new Set),i.add(a)}}let n={};for(let[r,s]of t)s.size===1&&(n[r]=[...s][0]);return n}import Xe from"node:fs";import je from"node:path";var St=/\{data:([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+)\}/g,et=je.resolve("data"),he,Jt=new Map;function tt(e,t="data",n){et=je.isAbsolute(t)?t:je.join(e,t),he=n?.trim()||void 0,Jt.clear()}var Oe=()=>({dir:et,env:he});function ds(e,t){let n=s=>s!==null&&typeof s=="object"&&!Array.isArray(s);if(!n(e)||!n(t))return t;let r={...e};for(let[s,o]of Object.entries(t))r[s]=s in e?ds(e[s],o):o;return r}function ls(e,t){try{return JSON.parse(Xe.readFileSync(e,"utf8"))}catch(n){throw new Error(`data file ${t} is not valid JSON: ${n instanceof Error?n.message:String(n)}`)}}function li(e){if(Jt.has(e))return Jt.get(e);let t=je.join(et,`${e}.json`),n=he?je.join(et,`${e}.${he}.json`):void 0,r=Xe.existsSync(t),s=n!==void 0&&Xe.existsSync(n);if(!r&&!s)throw new Error(`data file ${je.relative(process.cwd(),t)||t} does not exist${he?` (nor ${e}.${he}.json for the "${he}" environment)`:""}`);let o=r?ls(t,`${e}.json`):void 0;if(s){let a=ls(n,`${e}.${he}.json`);o=r?ds(o,a):a}return Jt.set(e,o),o}function qe(e){let[t,...n]=e.split("."),r=li(t),s=[t];for(let o of n){let a=r,i=a&&typeof a=="object"?Array.isArray(a)?a[Number(o)]:a[o]:void 0;if(i===void 0)throw new Error(`"${o}" does not exist under ${s.join(".")} (in ${t}.json)`);r=i,s.push(o)}return r}function nt(e){let t;try{t=qe(e)}catch(n){throw new Error(`{data:${e}}: ${n instanceof Error?n.message:String(n)}`)}if(t===null||typeof t=="object")throw new Error(`{data:${e}} is ${Array.isArray(t)?"a list":"an object"}, not a single value. Point at one entry, for example {data:${e}.${Array.isArray(t)?"0":Object.keys(t??{})[0]??"key"}}`);return String(t)}function Fe(e){let t;try{t=qe(e)}catch(s){throw new Error(`{data:${e}}: ${s instanceof Error?s.message:String(s)}`)}let n=Array.isArray(t)?t:[t];if(n.length===0)throw new Error(`{data:${e}} is an empty list: there is nothing to check`);let r=n.find(s=>s===null||typeof s=="object");if(r!==void 0)throw new Error(`{data:${e}} must be a list of plain values (text, numbers); it contains ${Array.isArray(r)?"a list":"an object"}`);return n.map(String)}function Gt(e){Fe(e)}function ps(e){return e.replace(St,(t,n)=>{let r=Fe(n);return Array.isArray(qe(n))?`${t} (a data list, currently: ${r.map(o=>`"${o}"`).join(", ")})`:r[0]})}function di(e){let t=[],n=[],r="",s=!1;for(let o=0;o<e.length;o++){let a=e[o];s?a==='"'&&e[o+1]==='"'?(r+='"',o++):a==='"'?s=!1:r+=a:a==='"'&&r===""?s=!0:a===","?(n.push(r),r=""):a===`
3
+ `||a==="\r"?(a==="\r"&&e[o+1]===`
4
+ `&&o++,n.push(r),r="",n.some(i=>i!=="")&&t.push(n),n=[]):r+=a}return n.push(r),n.some(o=>o!=="")&&t.push(n),t.map(o=>o.map(a=>a.trim()))}function Kt(e){let t=i=>{throw new Error(`Examples: {data:${e}}: ${i}`)},n=he?je.join(et,`${e}.${he}.csv`):void 0,r=n&&Xe.existsSync(n)?n:je.join(et,`${e}.csv`);if(!e.includes(".")&&Xe.existsSync(r)){let[i,...c]=di(Xe.readFileSync(r,"utf8"));return(!i||c.length===0)&&t(`${e}.csv needs a header row and at least one data row`),{headers:i,rows:c.map(l=>Object.fromEntries(i.map((f,d)=>[f,l[d]??""])))}}let s;try{s=qe(e)}catch(i){return t(`${i instanceof Error?i.message:String(i)} (looked for ${e.split(".")[0]}.csv and ${e.split(".")[0]}.json in the data folder)`)}(!Array.isArray(s)||s.length===0||s.some(i=>i===null||typeof i!="object"||Array.isArray(i)))&&t('must be a non-empty list of records, like [{ "role": "Admin", "menu": "Users" }]');let o=s,a=[...new Set(o.flatMap(i=>Object.keys(i)))];return{headers:a,rows:o.map(i=>Object.fromEntries(a.map(c=>[c,i[c]===void 0||i[c]===null?"":String(i[c])])))}}function vt(e){return e.replace(St,(t,n)=>nt(n))}function He(e){let t=new Set;for(let n of e)if(n)for(let r of n.matchAll(St))t.add(r[1]);return t}function Yt(e){let t=new Map;for(let n of He(e))try{t.set(n,nt(n))}catch{}return t}function fs(e,t,n=new Set){let r=new Map;for(let[a,i]of t)r.set(i,[...r.get(i)??[],a]);let s=[];for(let[a,i]of r){let c=i.length===1?i:i.filter(l=>n.has(l));c.length===1&&s.push([c[0],a])}s.sort((a,i)=>i[1].length-a[1].length);for(let[a,i]of s)if(i!==""&&e===i)return`{data:${a}}`;let o=e;for(let[a,i]of s)i.length<4||(o=o.split(i).join(`{data:${a}}`));return o}import zn from"node:fs";import Qt from"node:path";import{AstBuilder as pi,GherkinClassicTokenMatcher as fi,Parser as ui}from"@cucumber/gherkin";import{IdGenerator as gi}from"@cucumber/messages";var Z=class extends Error{},hi=/^(\s*)StepSet:\s*(.+?)\s*$/,mi=/^(\s*)StepSet\s+([^\s:].*?)\s*$/;function qn(e){let t=e.split(`
5
+ `),n=new Map,r=new Map;return{source:t.map((o,a)=>{let i=o.match(hi);if(i)return n.set(a+1,i[2]),`${i[1]}Scenario: ${i[2]}`;let c=o.match(mi);return c?(r.set(a+1,c[2]),`${c[1]}* StepSet ${c[2]}`):o}).join(`
6
+ `),definitions:n,invocations:r}}function yi(e){return new ui(new pi(gi.uuid()),new fi).parse(e)}function us(e,t){for(let n of e)n.scenario?t(n.scenario):"rule"in n&&n.rule&&us(n.rule.children,t)}function ve(e,t){let n=new Map;for(let r of e){let s=Qt.relative(t,r),o=qn(zn.readFileSync(r,"utf8"));if(o.definitions.size===0)continue;let a=yi(o.source);a.feature&&us(a.feature.children,i=>{let c=o.definitions.get(i.location.line);if(c===void 0)return;let l=n.get(c);if(l)throw new Z(`StepSet "${c}" is defined twice: ${l.file}:${l.line} and ${s}:${i.location.line}. Set names are unique across the project. If line ${i.location.line} was meant to INVOKE the set inside a scenario, remove the colon: "StepSet ${c}".`);if(i.examples.length>0)throw new Z(`StepSet "${c}" (${s}:${i.location.line}) has an Examples table \u2014 step sets take their values from the invoking scenario.`);for(let f of i.steps)if(o.invocations.has(f.location.line))throw new Z(`StepSet "${c}" (${s}:${i.location.line}) invokes another step set at line ${f.location.line} \u2014 step sets cannot be nested.`);n.set(c,{name:c,file:s,line:i.location.line,steps:[...i.steps]})})}return n}var bi=[".feature",".saffron"];function ke(e){return bi.some(t=>e.endsWith(t))}function $e(e,t,n){let r=Qt.isAbsolute(t.features??"features")?t.features:Qt.join(e,t.features??"features"),s=new Set(We(r));for(let o of n)o.endsWith(".saffron")&&s.add(o);return ve([...s].sort(),e)}function We(e){if(!zn.existsSync(e))return[];let t=[];for(let n of zn.readdirSync(e,{recursive:!0})){let r=String(n);r.endsWith(".saffron")&&t.push(Qt.join(e,r))}return t.sort()}function gs(e,t,n){return e.map(({step:r,source:s})=>{let o;switch(r.keywordType){case Hn.OUTCOME:o="assertion";break;case Hn.CONTEXT:case Hn.ACTION:o="action";break;default:o=n.value}n.value=o;let a=r.dataTable?r.dataTable.rows.map(c=>c.cells.map(l=>l.value)):void 0,i=r.docString?.content;return{keyword:r.keyword.trim(),kind:o,text:r.text,resolvedText:Be(r.text,t),table:a,resolvedTable:a?.map(c=>c.map(l=>Be(l,t))),docString:i,resolvedDocString:i!==void 0?Be(i,t):void 0,source:s}})}function hs(e,t,n,r,s){let o=[];for(let a of e){let i=r.get(a.location.line);if(i===void 0){o.push({step:a,source:{file:t,line:a.location.line,fromBackground:n}});continue}let c=s?.get(i);if(!c)throw new Z(`${t}:${a.location.line} invokes StepSet "${i}", which is not defined anywhere in the project.`);for(let l of c.steps)o.push({step:l,source:{file:c.file,line:l.location.line,fromStepSet:c.name,fromBackground:n}})}return o}function Ti(e,t,n,r,s,o=!1){let a=e.tags.map(c=>c.name),i=[];if(e.examples.length===0)i.push({params:{},label:""});else{let c=0;for(let l of e.examples){let f=l.name.trim().match(/^\{data:([A-Za-z0-9_.-]+)\}$/);if(f&&!l.tableHeader){let m=`${t}:${l.location.line}`;if(!o)throw new Z(`${m} reads Examples from a data file, which only .saffron files understand. Rename the file to ${t.replace(/\.feature$/,".saffron")}: nothing else needs to change.`);let h;try{h=Kt(f[1])}catch(k){throw new Z(`${m} ${k instanceof Error?k.message:String(k)}`)}let x=[...new Set([...r,...s].flatMap(k=>[k.step.text,k.step.docString?.content??"",...k.step.dataTable?.rows.flatMap(w=>w.cells.map(v=>v.value))??[]].flatMap(w=>[...w.matchAll(/<([^<>]+)>/g)].map(v=>v[1]))))].filter(k=>!h.headers.includes(k)&&!k.startsWith("table:")&&k!=="docstring");if(x.length>0)throw new Z(`${m} the scenario uses <${x.join(">, <")}>, but {data:${f[1]}} has the columns ${h.headers.map(k=>`"${k}"`).join(", ")}.`);for(let k of h.rows)c+=1,i.push({params:k,label:` (example ${c})`});continue}let d=l.tableHeader?.cells.map(m=>m.value)??[];for(let m of l.tableBody){c+=1;let h={};m.cells.forEach((y,x)=>{h[d[x]]=y.value}),i.push({params:h,label:` (example ${c})`})}}}return i.map(({params:c,label:l})=>{let f={value:"action"};return{featurePath:t,featureName:n,name:e.name,displayName:`${e.name}${l}`,tags:a,steps:[...gs(r,c,f),...gs(s,c,f)],params:c}})}function se(e,t,n){let r=wi.readFileSync(e,"utf8"),s=e.endsWith(".saffron"),o=s?qn(r):{source:r,definitions:new Map,invocations:new Map},a=new ki(new Si($i.uuid()),new vi),i=xi.relative(t,e),c;try{c=a.parse(o.source)}catch(m){let h=(m instanceof Error?m.message:String(m)).split(`
7
+ `).filter(x=>/^\(\d+:\d+\)/.test(x)).map(x=>`${i}${x.replace(/^\((\d+):(\d+)\)/,":$1:$2")}`),y=!s&&/^\s*StepSet\b/m.test(r);throw new Z(y?`${i} uses StepSet, which only .saffron files understand. Rename it to ${i.replace(/\.feature$/,".saffron")}: nothing else needs to change (a .saffron file accepts everything a .feature file does).`:`${i} is not valid Gherkin:
8
8
  ${h.join(`
9
- `)||(m instanceof Error?m.message:String(m))}`)}if(!c.feature)return[];let p=c.feature.name,f=[],d=(m,h)=>{let y=h;for(let b of m)if(b.background)y=[...h,...ds(b.background.steps,i,!0,s.invocations,n)];else if(b.scenario){if(s.definitions.has(b.scenario.location.line))continue;f.push(...xi(b.scenario,i,p,y,ds(b.scenario.steps,i,!1,s.invocations,n),o))}else"rule"in b&&b.rule&&d(b.rule.children,y)};return d(c.feature.children,[]),f}import ws from"node:fs";import Vn from"node:path";import gs from"node:fs";import hs from"node:path";import{createHash as Ii}from"node:crypto";import Zt from"node:fs";import rt from"node:path";var pe=new Set(["expectVisible","expectNotVisible","expectText","expectUrl","expectValue","expectDiffers","expectMatches","expectAttribute","expectResponse","expectEach"]);var Qt=1,vi=new Set(["goto","click","fill","press","selectOption","check","uncheck","hover","waitFor","captureText","handleDialog","uploadFile","dragTo","waitForResponse","switchTab","closeTab","newTab",...pe]),Si=["role","name","nameRegex","label","text","testId","placeholder","frame"],ki=["value","url","saveAs","compareTo","each","pattern","attribute","promptText","urlPattern","method","status","pageFingerprint"],W=class extends Error{constructor(n,r){super(`${n}: ${r}`);this.file=n;this.problem=r}file;problem},fe=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);function D(e,t,n){if(!e)throw new W(t,n)}function ps(e,t,n){D(fe(e),t,`${n} must be an object`);for(let r of Si)D(e[r]===void 0||typeof e[r]=="string",t,`${n}.${r} must be a string`);e.fallbackSelectors!==void 0&&D(Array.isArray(e.fallbackSelectors)&&e.fallbackSelectors.every(r=>typeof r=="string"),t,`${n}.fallbackSelectors must be a list of selectors`)}var $i=e=>["role","label","text","testId","placeholder"].some(t=>typeof e[t]=="string"&&e[t]!=="")||Array.isArray(e.fallbackSelectors)&&e.fallbackSelectors.some(t=>typeof t=="string"&&t.trim().length>0),Ti={goto:["url"],click:["target"],fill:["target","value"],press:["value"],selectOption:["target","value"],check:["target"],uncheck:["target"],hover:["target"],waitFor:["targetOrValue"],captureText:["target","saveAs"],handleDialog:[],uploadFile:["value"],dragTo:["target","toTarget"],waitForResponse:["urlPattern"],switchTab:["value"],closeTab:[],newTab:[],expectVisible:["target"],expectNotVisible:["target"],expectText:["target","value"],expectUrl:["urlOrValue"],expectValue:["target","value"],expectDiffers:["compareTo","targetOrValue"],expectMatches:["target","pattern"],expectAttribute:["target","attribute","pattern"],expectResponse:["urlPattern"],expectEach:["target","each"]};function Ri(e,t,n){let r=s=>typeof e[s]=="string",o=s=>fe(e[s])&&$i(e[s]);for(let s of Ti[e.action])switch(s){case"target":case"toTarget":D(o(s),t,`${n} (${e.action}) needs a ${s} with a role, name, label, text, test id, placeholder or selector`);break;case"urlOrValue":D(r("url")||r("value"),t,`${n} (${e.action}) needs a url`);break;case"targetOrValue":D(o("target")||r("value"),t,`${n} (${e.action}) needs a target or a value`);break;default:D(r(s),t,`${n} (${e.action}) needs ${s}`)}for(let s of["target","toTarget"])e[s]!==void 0&&D(o(s),t,`${n}.${s} has no usable locator`);if(e.action==="expectUrl"){let s=e.url??e.value;D(typeof s=="string"&&s.length>0,t,`${n} (expectUrl) needs a non-empty url or value`)}for(let s of["url","saveAs","compareTo","each","pattern","attribute","urlPattern"])e[s]!==void 0&&D(e[s].length>0,t,`${n}.${s} must not be empty`);["press","uploadFile","switchTab","expectText"].includes(e.action)&&D(typeof e.value=="string"&&e.value.length>0,t,`${n} (${e.action}) needs a non-empty value`)}function Ei(e,t,n){D(fe(e),t,`${n} must be an object`),D(typeof e.action=="string",t,`${n}.action is missing`),D(vi.has(e.action),t,`${n}.action "${e.action}" is not an action this runner knows. Recorded by a newer saffron-ai? Update, or re-record with --rerecord`);for(let r of ki)D(e[r]===void 0||typeof e[r]=="string",t,`${n}.${r} must be a string`);D(e.opensTab===void 0||typeof e.opensTab=="boolean",t,`${n}.opensTab must be true or false`),e.target!==void 0&&ps(e.target,t,`${n}.target`),e.toTarget!==void 0&&ps(e.toTarget,t,`${n}.toTarget`),Ri(e,t,n)}function Ai(e,t,n){D(fe(e),t,`${n} must be an object`),D(typeof e.gherkin=="string"&&e.gherkin.length>0,t,`${n}.gherkin (the step text) is missing`),D(typeof e.keyword=="string",t,`${n}.keyword is missing`),D(e.kind==="action"||e.kind==="assertion",t,`${n}.kind must be "action" or "assertion"`),D(Array.isArray(e.actions),t,`${n}.actions must be a list (empty for a recorded no-op)`),e.actions.forEach((r,o)=>Ei(r,t,`${n}.actions[${o}]`)),e.table!==void 0&&D(Array.isArray(e.table)&&e.table.every(r=>Array.isArray(r)&&r.every(o=>typeof o=="string")),t,`${n}.table must be rows of strings`),D(e.docString===void 0||typeof e.docString=="string",t,`${n}.docString must be a string`),D(e.fromStepSet===void 0||typeof e.fromStepSet=="string",t,`${n}.fromStepSet must be a string`)}function zn(e,t){return D(fe(e),t,"is not a JSON object"),D(e.version!==void 0,t,"has no version: not a Saffron recording"),D(e.version===Qt,t,typeof e.version=="number"&&e.version>Qt?`is schema version ${e.version}; this saffron-ai reads version ${Qt}. Update saffron-ai to the release that wrote it`:`has schema version ${JSON.stringify(e.version)}; this saffron-ai reads version ${Qt}`),D(typeof e.feature=="string"&&e.feature.length>0,t,"feature (the feature file path) is missing"),D(typeof e.scenario=="string"&&e.scenario.length>0,t,"scenario (the scenario name) is missing"),D(e.recordedAt===void 0||typeof e.recordedAt=="string",t,"recordedAt must be a string"),D(e.recordedBy===void 0||e.recordedBy==="agent"||e.recordedBy==="manual",t,'recordedBy must be "agent" or "manual"'),D(Array.isArray(e.steps),t,"steps must be a list"),e.steps.forEach((n,r)=>Ai(n,t,`steps[${r}]`)),e.data!==void 0&&D(fe(e.data)&&Object.values(e.data).every(n=>typeof n=="string"),t,"data must map references to strings"),e}function fs(e,t){D(fe(e),t,"is not a JSON object"),D(fe(e.meta),t,"meta is missing: not a Saffron proposal");let n=e.meta;D(n.mode==="record"||n.mode==="heal",t,'meta.mode must be "record" or "heal"'),D(typeof n.narrative=="string",t,"meta.narrative must be a string"),D(Array.isArray(n.adaptations)&&n.adaptations.every(r=>typeof r=="string"),t,"meta.adaptations must be a list of strings"),D(n.verified===void 0||typeof n.verified=="boolean",t,"meta.verified must be true or false"),D(fe(n.usage),t,"meta.usage is missing"),n.featureEdits!==void 0&&D(Array.isArray(n.featureEdits)&&n.featureEdits.every(r=>fe(r)&&typeof r.index=="number"&&typeof r.text=="string"),t,"meta.featureEdits must be a list of { index, text }"),n.bound!==void 0&&D(fe(n.bound)&&["scenario","committed","recording"].every(r=>typeof n.bound[r]=="string"),t,"meta.bound must hold the scenario, committed and recording digests"),zn(e.cache,`${t} (cache)`)}var qn=".saffron";function Xt(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"unnamed"}function Hn(e){let t=e.split(/[\\/]/).filter(Boolean),n=t.pop()??"unnamed",r=t.slice(1),o=Xt([...r,rt.basename(n,".feature")].join("-"));if(r.length===0)return o;let s=Ii("sha1").update(e.replace(/\\/g,"/")).digest("hex").slice(0,6);return`${o}-${s}`}function J(e,t,n){return rt.join(e,qn,"cache",Hn(t),`${Xt(n)}.json`)}function us(e,t){let n=new Map;for(let r of t){let o=J(e,r.featurePath,r.name),s=n.get(o)??new Set;s.add(`${r.featurePath} \u203A ${r.name}`),n.set(o,s)}return[...n.entries()].filter(([,r])=>r.size>1).map(([r,o])=>`${[...o].join(" and ")} both map to ${rt.relative(e,r)}. Rename one scenario or one file.`)}var en="owner.json";function me(e,t,n){return rt.join(e,qn,"artifacts",Hn(t),Xt(n))}function ue(e,t,n){return rt.join(e,qn,"proposals",Hn(t),`${Xt(n)}.json`)}function st(e,t){try{return oe(e)}catch(n){if(!(n instanceof W))throw n;t?.(e,n.problem);return}}function oe(e){if(!Zt.existsSync(e))return;let t;try{t=JSON.parse(Zt.readFileSync(e,"utf8"))}catch(n){throw new W(e,`is not valid JSON: ${n instanceof Error?n.message:String(n)}`)}return zn(t,e)}function ot(e,t){Zt.mkdirSync(rt.dirname(e),{recursive:!0}),Zt.writeFileSync(e,`${JSON.stringify(t,null,2)}
10
- `)}function kt(e,t){let n=r=>typeof r=="string"?r.includes(t):Array.isArray(r)?r.some(n):r&&typeof r=="object"?Object.entries(r).some(([o,s])=>o!=="pageFingerprint"&&n(s)):!1;return e.some(n)}function at(e,t){if(t.trim()==="")return!1;let n=r=>(r??"").replace(/<[^<>]+>/g,"");return e.some(r=>[r.value,r.url,r.urlPattern,r.pattern,r.promptText,r.compareTo,...[r.target,r.toTarget].flatMap(o=>[o?.name,o?.nameRegex,o?.label,o?.text,o?.placeholder,o?.testId,o?.frame,...o?.fallbackSelectors??[]])].some(o=>n(o).includes(t)))}function tn(e){let t=[];return{pattern:e.replace(/"((?:[^"\\]|\\.)*)"/g,(r,o)=>(t.push(o),`"\xAB${t.length-1}\xBB"`)),args:t}}function Pi(e,t){let n=r=>JSON.stringify(r.map(({pageFingerprint:o,...s})=>s));return n(e)===n(t)}function Ci(e){let t=new Set,n=r=>{if(r)for(let o of r.matchAll(/<([^<>]+)>/g))t.add(o[1])};for(let r of e.actions)n(r.value),n(r.url),n(r.pattern),n(r.target?.name),n(r.target?.nameRegex),n(r.target?.label),n(r.target?.text),n(r.target?.placeholder);return n(e.gherkin),t}function ye(e){let t=hs.join(e,".saffron","cache"),n=new Map,r=new Map,o=0;if(gs.existsSync(t))for(let i of gs.readdirSync(t,{recursive:!0})){let c=String(i);if(!c.endsWith(".json"))continue;let p=st(hs.join(t,c));if(p){o++;for(let f of p.steps){let d=f.gherkin,m=JSON.stringify(f.actions.map(({pageFingerprint:b,...k})=>k)),h=r.get(d)??[];h.includes(m)||(h.push(m),r.set(d,h));let y=n.get(d);!y||p.recordedAt>y.recordedAt?n.set(d,{gherkin:f.gherkin,table:f.table,docString:f.docString,actions:f.actions,sourceParams:Ce(p.steps),sourceData:p.data,feature:p.feature,scenario:p.scenario,recordedAt:p.recordedAt,variants:h.length}):y.variants=h.length}}}let s=[...r.entries()].filter(([,i])=>i.length>1).map(([i])=>i),a=new Map;for(let i of n.values()){let{pattern:c,args:p}=tn(i.gherkin);if(p.length===0)continue;let f=a.get(c);(!f||i.recordedAt>f.recordedAt)&&a.set(c,{...i,args:p})}return{steps:n,patterns:a,divergent:s,scannedCaches:o}}function ms(e,t,n){if(!!e.table!=!!t.table)return!1;if(e.table&&t.table){let s=Object.keys(ne(e.table)),a=Object.keys(ne(t.table));if(s.length>0&&a.length>0){if(JSON.stringify(s)!==JSON.stringify(a))return!1}else if(JSON.stringify(e.table)!==JSON.stringify(t.table))return!1}if(e.docString!==void 0!=(t.docString!==void 0)||e.docString!==void 0&&t.docString!==void 0&&e.docString!==t.docString&&!e.actions.some(s=>[s.value,s.url,s.pattern].some(a=>a?.includes("<docstring>"))))return!1;if(e.table&&t.table){let s=ne(e.table),a=ne(t.table);for(let i of Object.keys(s))if(s[i]!==a[i]&&(!kt(e.actions,`<${i}>`)||at(e.actions,s[i])))return!1}for(let[s,a]of Object.entries(e.sourceParams??{}))if(s in n&&n[s]!==a&&at(e.actions,a))return!1;for(let[s,a]of Object.entries(e.sourceData??{})){let i;try{i=nt(s)}catch{continue}if(i!==a&&(at(e.actions,a)||e.gherkin.includes(`{data:${s}}`)&&!kt(e.actions,`{data:${s}}`)))return!1}let r={...n,...ne(t.resolvedTable??t.table),...Be(t.resolvedDocString??t.docString)},o=Ci({gherkin:e.gherkin,keyword:"",kind:"action",actions:e.actions});for(let s of o)if(!(s in r))return!1;return!0}function ys(e,t,n){let r=e.steps.get(t.text);if(r)return ms(r,t,n)?r:void 0;let{pattern:o,args:s}=tn(t.text);if(s.length===0)return;let a=e.patterns.get(o);if(!a)return;let i=new Map;for(let d=0;d<s.length;d++){let m=a.args[d],h=s[d];if(m===h)continue;let y=i.get(m);if(y!==void 0&&y!==h)return;i.set(m,h)}let c=d=>[d?.name,d?.nameRegex,d?.label,d?.text,d?.placeholder];for(let d of i.keys())if(!a.actions.some(h=>[h.value,h.url,h.pattern,...c(h.target)].includes(d)))return;let p=d=>d!==void 0&&i.has(d)?i.get(d):d,f={...a,gherkin:t.text,actions:a.actions.map(({pageFingerprint:d,...m})=>({...m,value:p(m.value),url:p(m.url),pattern:p(m.pattern),target:m.target?{...m.target,name:p(m.target.name),nameRegex:p(m.target.nameRegex),label:p(m.target.label),text:p(m.target.text),placeholder:p(m.target.placeholder)}:void 0}))};return ms(f,t,n)?f:void 0}function bs(e,t){let n=[];return e.forEach((r,o)=>{if(r.actions.length!==0&&!t.steps.has(r.gherkin)){for(let s of t.steps.values())if(!(s.variants>1)&&s.actions.length!==0&&Pi(r.actions,s.actions)){n.push({index:o,from:r.gherkin,to:s.gherkin,feature:s.feature,scenario:s.scenario});break}}}),n}function _i(e){if(!ws.existsSync(e))return[];let t=[];for(let n of ws.readdirSync(e,{recursive:!0})){let r=String(n);Se(r)&&t.push(Vn.join(e,r))}return t.sort()}function be(e,t,n,r){let o=Vn.isAbsolute(t)?t:Vn.join(e,t),s=_i(o),a=n??ve(s.filter(h=>h.endsWith(".saffron")),e),i=new Map,c=new Map;for(let h of s)for(let y of r?.get(h)??se(h,e,a)){let b=`${y.featurePath}::${y.name}`;for(let k of y.steps){let w=i.get(k.text);if(w||(w={text:k.text,keywords:new Map,kind:k.kind,scenarios:new Set,files:new Set,fromStepSet:k.source?.fromStepSet},i.set(k.text,w)),w.keywords.set(k.keyword,(w.keywords.get(k.keyword)??0)+1),w.scenarios.add(b),k.source&&w.files.add(k.source.file),k.source?.fromStepSet){let v=c.get(k.source.fromStepSet);v||c.set(k.source.fromStepSet,v=new Set),v.add(b)}}}let p=ye(e),f=new Set(p.divergent),d=[...i.values()].map(h=>({text:h.text,keyword:[...h.keywords.entries()].sort((y,b)=>b[1]-y[1])[0][0],kind:h.kind,status:f.has(h.text)?"divergent":p.steps.has(h.text)?"recorded":"unrecorded",usage:h.scenarios.size,files:[...h.files].sort(),fromStepSet:h.fromStepSet})).sort((h,y)=>y.usage-h.usage||h.text.localeCompare(y.text)),m=[...a.values()].map(h=>({name:h.name,file:h.file,line:h.line,steps:h.steps.map(y=>y.text),usage:c.get(h.name)?.size??0})).sort((h,y)=>y.usage-h.usage||h.name.localeCompare(y.name));return{steps:d,stepSets:m,scannedFiles:s.length,scannedCaches:p.scannedCaches}}function nn(e,t){let n=t.toLowerCase();return{...e,steps:e.steps.filter(r=>r.text.toLowerCase().includes(n)),stepSets:e.stepSets.filter(r=>r.name.toLowerCase().includes(n)||r.steps.some(o=>o.toLowerCase().includes(n)))}}function xs(e){let t={},n="saffron,feature,gherkin,cucumber";for(let r of e.steps)t[`step: ${r.text}`]={scope:n,prefix:r.text,body:[r.text.replace(/\$/g,"\\$")],description:`${r.status} \xB7 used in ${r.usage} scenario(s)`};for(let r of e.stepSets)t[`StepSet: ${r.name}`]={scope:n,prefix:`StepSet ${r.name}`,body:[`StepSet ${r.name}`],description:`step set (${r.steps.length} steps) \u2014 ${r.file}`};return JSON.stringify(t,null,2)+`
11
- `}import{Server as Wc}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as Jc}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as Gc,ListToolsRequestSchema as Kc}from"@modelcontextprotocol/sdk/types.js";import de from"node:fs";import xe from"node:path";import Rs from"node:fs";import Ui from"node:path";import vs from"node:fs";import ji from"node:path";var on=/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g;function $t(e){return e.replace(on,(t,n)=>{let r=process.env[n];if(r===void 0)throw new Error(`environment variable ${n} is not set (referenced as {env:${n}}) \u2014 export it or add it to .env`);return r})}function Jn(e){let t=new Set;for(let n of e)if(n)for(let r of n.matchAll(on))t.add(r[1]);return t}function X(e){let t=[];for(let n of e.steps){t.push(n.text,n.resolvedText),n.docString&&t.push(n.docString),n.resolvedDocString&&t.push(n.resolvedDocString);for(let r of n.table??[])t.push(...r);for(let r of n.resolvedTable??[])t.push(...r)}return t}function Gn(e){let t=[];for(let n of qe(e))try{t.push(...Oe(n))}catch{}return t}function $e(e){let t=new Map,n=X(e);for(let r of Jn([...n,...Gn(n)])){let o=process.env[r];o!==void 0&&t.set(r,o)}return t}var Tt=3,Wn=String.raw`\{(?:env:[A-Za-z_][A-Za-z0-9_]*|data:[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+|unique:[A-Za-z0-9_-]+(?::digits)?)\}`,rn={'"':'\\"',"\\":"\\\\","\b":"\\b","\f":"\\f","\n":"\\n","\r":"\\r"," ":"\\t"};function Oi(e,t,n,r){let o="";for(let s of e.split("")){let a=s.charCodeAt(0);if(t?.test(s)||a<32&&!rn[s]){let i=a.toString(16).padStart(4,"0");o+="\\u"+(n?i.toUpperCase():i)}else rn[s]?o+=rn[s]:s==="/"&&r?o+="\\/":o+=s}return o}function Fi(e){return[e,e.replace(/%[0-9A-F]{2}/g,t=>t.toLowerCase())]}function Ss(e,t,n){return e.replace(/[&<>"'+]|[^\x00-\x7f]/gu,r=>t[r]??(n&&r>"\x7F"?n(r.codePointAt(0)):r))}function Ni(e,t){let n="";for(let r of e){let o=r<=" "||r>="\x7F"||t.test(r);n+=o?[...Buffer.from(r,"utf8")].map(s=>"%"+s.toString(16).toUpperCase().padStart(2,"0")).join(""):r}return n}var Mi=[/["#<>']/,/["#<>?^`{}]/,/["#<>?^`{|}]/,/["#<>?^`{}/:;=@[\\\]|]/,/["#<>?^`{}/:;=@[\\\]|']/];function Di(e){let t=/[^\x00-\x7f]/,n=new Set([e]);for(let[o,s,a]of[[void 0,!1,!1],[void 0,!1,!0],[t,!1,!1],[t,!0,!1],[t,!1,!0],[/[<>&\u2028\u2029]/,!1,!1],[/[^\x00-\x7f]|[<>&'+`"]/,!0,!1]])n.add(Oi(e,o,s,a));let r=o=>{try{for(let s of Fi(o()))n.add(s)}catch{}};r(()=>encodeURIComponent(e)),r(()=>encodeURIComponent(e).replaceAll("'","%27")),r(()=>encodeURI(e)),r(()=>new URLSearchParams([["",e]]).toString().slice(1));for(let o of Mi)r(()=>Ni(e,o));for(let o of[{"&":"&amp;","<":"&lt;",">":"&gt;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&apos;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&#34;","'":"&#39;","+":"&#43;"}])n.add(Ss(e,o));return n.add(Ss(e,{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","+":"&#x2B;"},o=>`&#x${o.toString(16).toUpperCase()};`)),[...n]}var Rt=e=>e.map(t=>`{env:${t}}`).join(""),sn=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),Li=e=>Buffer.from(e,"utf8").toString("latin1");function it(e,t={}){let n=[...e.entries()].filter(([,h])=>h.length>=Tt).sort((h,y)=>y[1].length-h[1].length),r=new Map,o=new Set;for(let[h,y]of n){t.standaloneBelow!==void 0&&y.length<t.standaloneBelow&&o.add(y);let b=t.encodings?Di(y):[y];for(let k of b){let w=t.bytes?[Li(k),.../^[\x00-\xff]*$/.test(k)?[k]:[]]:[k];for(let v of w)r.has(v)||r.set(v,h)}}let s=new Set(n.map(([,h])=>h)),a=new Set(["u"]);for(let[,h]of n)for(let y of h){let b=rn[y]??(y==="/"?"\\/":void 0);b&&a.add(b.slice(1))}let i=new RegExp(`\\\\[${[...a].map(sn).join("")}]`);if(r.size===0)return{empty:!0,mask:h=>h,matches:()=>[],occursIn:()=>!1,isValue:()=>!1,hiddenByEscape:i};let c=[...r.keys()].sort((h,y)=>y.length-h.length).map(h=>o.has(h)?`(?<![A-Za-z0-9])${sn(h)}(?![A-Za-z0-9])`:sn(h)).join("|"),p=new RegExp(`(${Wn})|${c}`,"g"),f=new RegExp(`(${Wn})|${c}`,"y"),d=new RegExp(c),m=h=>{let y=[];p.lastIndex=0;for(let b=p.exec(h);b!==null;b=p.exec(h)){if(b[1]!==void 0)continue;let k=b.index,w=k+b[0].length,v=[r.get(b[0])];for(let l=k+1;l<w;l++){f.lastIndex=l;let u=f.exec(h);u&&u[1]===void 0&&l+u[0].length>w&&(w=l+u[0].length,v.push(r.get(u[0])))}y.push({start:k,end:w,names:[...new Set(v)]}),p.lastIndex=w}return y};return{empty:!1,hiddenByEscape:i,occursIn:h=>d.test(h),isValue:h=>s.has(h),matches:m,mask(h){let y="",b=0;for(let k of m(h))y+=h.slice(b,k.start)+Rt(k.names),b=k.end;return b===0?h:y+h.slice(b)}}}function $s(e,t){let n=new Map;for(let s of e)for(let a of s.split(/[\s"'`]+/)){let i=a.replace(/^[.,;:!?()[\]]+|[.,;:!?()[\]]+$/g,""),c=[...i.matchAll(on)].map(f=>f[1]);if(c.length===0||c.some(f=>!t.has(f)))continue;let p=i.replace(on,(f,d)=>t.get(d));p===i||p.length<Tt||i===`{env:${c[0]}}`||n.has(p)||n.set(p,i)}if(n.size===0)return s=>s;let r=[...n.keys()].sort((s,a)=>a.length-s.length).map(sn).join("|"),o=new RegExp(`(${Wn})|${r}`,"g");return s=>s.replace(o,(a,i)=>i!==void 0?a:n.get(a))}var ks=new WeakMap;function Te(e,t){let n=ks.get(t);return n||(n=it(t,{encodings:!0}),ks.set(t,n)),n.mask(e)}function Ts(e){let t=ji.join(e,".env");if(vs.existsSync(t))for(let n of vs.readFileSync(t,"utf8").split(`
12
- `)){let r=n.trim();if(!r||r.startsWith("#"))continue;let o=r.indexOf("=");if(o<=0)continue;let s=r.slice(0,o).trim();if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(s))continue;let a=r.slice(o+1).trim();(a.startsWith('"')&&a.endsWith('"')||a.startsWith("'")&&a.endsWith("'"))&&(a=a.slice(1,-1)),process.env[s]===void 0&&(process.env[s]=a)}}function ae(e){Ts(e);let t=Ui.join(e,"saffron.config.json"),n=Rs.existsSync(t)?JSON.parse(Rs.readFileSync(t,"utf8")):{};return tt(e,n.dataDir,process.env.SAFFRON_ENV??n.env),n}var Kn={features:"features",dataDir:"data",actionTimeoutMs:5e3,pollIntervalMs:100,retries:1,screenshot:"viewport",maxTurns:100,strict:!1,verifyProposals:!0,snapshotMode:"none",reuseSteps:!0,browser:"chromium",workers:1,assertionPolicy:"strict",hookTimeoutMs:3e5,trace:"off"};function Es(e){return{...Kn,...Object.fromEntries(Object.entries(e).filter(([,t])=>t!==void 0))}}import Yn from"node:fs";import an from"node:path";import{fileURLToPath as Bi}from"node:url";function cn(){let e=an.dirname(Bi(import.meta.url));for(let t=0;t<6;t++){let n=an.join(e,"package.json");if(Yn.existsSync(n))try{if(JSON.parse(Yn.readFileSync(n,"utf8")).name==="saffron-ai")return e}catch{}e=an.dirname(e)}}function Fe(){let e=cn();if(!e)return"unknown";try{return JSON.parse(Yn.readFileSync(an.join(e,"package.json"),"utf8")).version??"unknown"}catch{return"unknown"}}import re from"node:fs";import Y from"node:path";import ee from"node:fs";import{createHash as Hi}from"node:crypto";import As from"node:os";import dn from"node:path";import Ne from"node:fs";import ct from"node:path";import{createHash as zi,randomBytes as qi}from"node:crypto";function lt(e,t){try{if(!Ne.statSync(t).isFile())return!1;let n=ct.relative(Ne.realpathSync(e),Ne.realpathSync(t));return n!==""&&n!==".."&&!n.startsWith(".."+ct.sep)&&!ct.isAbsolute(n)}catch{return!1}}function Ve(e){return zi("sha256").update(e).digest("hex")}function ln(e,t){Ne.mkdirSync(ct.dirname(e),{recursive:!0});let n=`${e}.${process.pid}-${qi(4).toString("hex")}.partial`;try{Ne.writeFileSync(n,t),Ne.renameSync(n,e)}catch(r){throw Ne.rmSync(n,{force:!0}),r}}function dt(e,t){let n=ct.join(e,t.file);if(lt(ct.join(e,".saffron","artifacts"),n))try{let r=Ne.readFileSync(n);return t.sha256&&Ve(r)!==t.sha256?void 0:r}catch{return}}function Is(e,t){ee.mkdirSync(dn.dirname(e),{recursive:!0});let n=Buffer.from(`${JSON.stringify(t,null,2)}
13
- `);return pn(e,()=>ln(e,n)),Xn(n)}var Vi=6e4,Wi=5e3,Ji=new Int32Array(new SharedArrayBuffer(4)),Et=class extends Error{};function pn(e,t,n=1e4){let r=`${e}.lock`,o=Date.now()+n,s;for(;;){s=JSON.stringify({pid:process.pid,host:As.hostname(),at:new Date().toISOString()});try{ee.writeFileSync(r,s,{flag:"wx"});break}catch(a){let i=a.code,c=i==="EPERM"&&process.platform==="win32";if(i!=="EEXIST"&&!c)throw a;if(Date.now()>=o)throw c?a:new Et(`${e} is being decided by another process. Try again.`);Gi(r)||Atomics.wait(Ji,0,0,25)}}try{return t()}finally{Qn(r)===s&&ee.rmSync(r,{force:!0})}}function Qn(e){try{return ee.readFileSync(e,"utf8")}catch{return}}function Gi(e){let t=Qn(e);if(t===void 0)return!1;let n={};try{n=JSON.parse(t)||{}}catch{}let r=Date.parse(n.at??""),o=Vi;if(Number.isNaN(r)){try{r=ee.statSync(e).mtimeMs}catch{return!1}o=Wi}return!(n.host===As.hostname()&&typeof n.pid=="number"&&!Ki(n.pid))&&Date.now()-r<o||Qn(e)!==t?!1:(ee.rmSync(e,{force:!0}),!0)}function Ki(e){try{return process.kill(e,0),!0}catch(t){return t.code!=="ESRCH"}}function fn(e,t,n){return ee.existsSync(e)?pn(e,()=>ee.existsSync(e)?t():n()):n()}function Zn(e){let t=/^(.+)#([0-9a-f]{64})$/i.exec(e);return t?{file:t[1],revision:t[2].toLowerCase()}:{file:e}}function At(e){return Xn(ee.readFileSync(e))}var Xn=e=>Hi("sha256").update(e).digest("hex");function pt(e){return un(e).proposal}function un(e){let t=ee.readFileSync(e),n;try{n=JSON.parse(t.toString("utf8"))}catch(r){throw new W(e,`is not valid JSON: ${r instanceof Error?r.message:String(r)}`)}return fs(n,e),{proposal:n,revision:Xn(t)}}function Me(e,t){let n=dn.join(e,".saffron","proposals");if(!ee.existsSync(n))return[];let r=[];for(let o of ee.readdirSync(n)){let s=dn.join(n,o);if(ee.statSync(s).isDirectory())for(let a of ee.readdirSync(s)){if(!a.endsWith(".json"))continue;let i=dn.join(s,a);try{r.push({file:i,...un(i)})}catch(c){if(!t||!(c instanceof W))throw c;t(i,c.problem)}}}return r}function gn(e,t){let n=pt(t),r=J(e,n.cache.feature,n.cache.scenario);return ot(r,n.cache),ee.rmSync(t),r}function hn(e){ee.rmSync(e)}var er=(e,t)=>Y.relative(e,t).split(Y.sep).join("/");function Ps(e){return re.existsSync(e)?[...re.readdirSync(e,{recursive:!0})].map(String).filter(t=>t.endsWith(".json")).map(t=>Y.join(e,t)).sort():[]}function Cs(e){try{let t=JSON.parse(re.readFileSync(e,"utf8")),n="cache"in t&&t.cache?t.cache:t;return{feature:n.feature,scenario:n.scenario}}catch{return{}}}function _s(e){try{let t=JSON.parse(re.readFileSync(Y.join(e,en),"utf8"));return{feature:typeof t.feature=="string"?t.feature:void 0,scenario:typeof t.scenario=="string"?t.scenario:void 0}}catch{return{}}}function yn(e,t,n){let r=Y.isAbsolute(t)?t:Y.join(e,t),o=new Set(re.existsSync(r)?[...re.readdirSync(r,{recursive:!0})].map(String).filter(Se).map(p=>Y.join(r,p)):[]);for(let p of["cache","proposals"])for(let f of Ps(Y.join(e,".saffron",p))){let{feature:d}=Cs(f);if(typeof d=="string"){let m=Y.resolve(e,d);re.existsSync(m)&&o.add(m)}}for(let p of mn(Y.join(e,".saffron","artifacts")))for(let f of mn(p)){let d=_s(f).feature;if(d){let m=Y.resolve(e,d);re.existsSync(m)&&o.add(m)}}let s=[...n?.unreadable??[]],a=[...o].filter(p=>!n?.parsedFiles.has(p)),i=new Map;if(a.length>0)try{i=ve([...new Set([...He(r),...o].filter(p=>p.endsWith(".saffron")))],e)}catch(p){s.push(p instanceof Error?p.message:String(p))}let c=new Set(n?.owned??[]);for(let p of a){let f;try{f=se(p,e,i)}catch(d){s.push(`${er(e,p)}: ${d instanceof Error?d.message:String(d)}`);continue}for(let d of f)c.add(J(e,d.featurePath,d.name)),c.add(ue(e,d.featurePath,d.name)),c.add(me(e,d.featurePath,d.displayName))}return{...Yi(e,c),unreadable:s}}function Yi(e,t){let n=[],r=0;for(let s of["cache","proposal"]){let a=Y.join(e,".saffron",s==="cache"?"cache":"proposals");for(let i of Ps(a)){if(t.has(i)){r++;continue}let{feature:c,scenario:p}=Cs(i),f=c?Y.join(e,c):void 0;n.push({file:er(e,i),kind:s,feature:c,scenario:p,reason:c?re.existsSync(f)?"scenario-gone":"feature-gone":"unknown"})}}let o=Y.join(e,".saffron","artifacts");for(let s of mn(o))for(let a of mn(s)){if(t.has(a)){r++;continue}let{feature:i,scenario:c}=_s(a);n.push({file:er(e,a),kind:"artifact",feature:i,scenario:c,reason:i?re.existsSync(Y.join(e,i))?"scenario-gone":"feature-gone":"unknown"})}return{orphans:n,kept:r}}function mn(e){try{return re.readdirSync(e,{withFileTypes:!0}).filter(t=>t.isDirectory()).map(t=>Y.join(e,t.name)).sort()}catch{return[]}}function js(e,t){let n=[],r=new Set;for(let o of t){let s=Y.join(e,o.file);try{o.kind==="proposal"?pn(s,()=>re.rmSync(s)):re.rmSync(s,{recursive:o.kind==="artifact"}),n.push(o.file),r.add(Y.dirname(s))}catch{}}for(let o of r)try{re.readdirSync(o).length===0&&re.rmdirSync(o)}catch{}return n}function Os(e){if(e.kind==="artifact")return`${e.file}/
9
+ `)||(m instanceof Error?m.message:String(m))}`)}if(!c.feature)return[];let l=c.feature.name,f=[],d=(m,h)=>{let y=h;for(let x of m)if(x.background)y=[...h,...hs(x.background.steps,i,!0,o.invocations,n)];else if(x.scenario){if(o.definitions.has(x.scenario.location.line))continue;f.push(...Ti(x.scenario,i,l,y,hs(x.scenario.steps,i,!1,o.invocations,n),s))}else"rule"in x&&x.rule&&d(x.rule.children,y)};return d(c.feature.children,[]),f}import $s from"node:fs";import Gn from"node:path";import ws from"node:fs";import xs from"node:path";import{createHash as Oi}from"node:crypto";import Xt from"node:fs";import rt from"node:path";var pe=new Set(["expectVisible","expectNotVisible","expectText","expectUrl","expectValue","expectDiffers","expectMatches","expectAttribute","expectResponse","expectEach"]);var Zt=1,Ri=new Set(["goto","click","fill","press","selectOption","check","uncheck","hover","waitFor","captureText","handleDialog","uploadFile","dragTo","waitForResponse","switchTab","closeTab","newTab",...pe]),Ei=["role","name","nameRegex","label","text","testId","placeholder","frame"],Ai=["value","url","saveAs","compareTo","each","pattern","attribute","promptText","urlPattern","method","status","pageFingerprint"],V=class extends Error{constructor(n,r){super(`${n}: ${r}`);this.file=n;this.problem=r}file;problem},fe=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);function L(e,t,n){if(!e)throw new V(t,n)}function ms(e,t,n){L(fe(e),t,`${n} must be an object`);for(let r of Ei)L(e[r]===void 0||typeof e[r]=="string",t,`${n}.${r} must be a string`);e.fallbackSelectors!==void 0&&L(Array.isArray(e.fallbackSelectors)&&e.fallbackSelectors.every(r=>typeof r=="string"),t,`${n}.fallbackSelectors must be a list of selectors`)}var Ii=e=>["role","label","text","testId","placeholder"].some(t=>typeof e[t]=="string"&&e[t]!=="")||Array.isArray(e.fallbackSelectors)&&e.fallbackSelectors.some(t=>typeof t=="string"&&t.trim().length>0),Ci={goto:["url"],click:["target"],fill:["target","value"],press:["value"],selectOption:["target","value"],check:["target"],uncheck:["target"],hover:["target"],waitFor:["targetOrValue"],captureText:["target","saveAs"],handleDialog:[],uploadFile:["value"],dragTo:["target","toTarget"],waitForResponse:["urlPattern"],switchTab:["value"],closeTab:[],newTab:[],expectVisible:["target"],expectNotVisible:["target"],expectText:["target","value"],expectUrl:["urlOrValue"],expectValue:["target","value"],expectDiffers:["compareTo","targetOrValue"],expectMatches:["target","pattern"],expectAttribute:["target","attribute","pattern"],expectResponse:["urlPattern"],expectEach:["target","each"]};function Pi(e,t,n){let r=o=>typeof e[o]=="string",s=o=>fe(e[o])&&Ii(e[o]);for(let o of Ci[e.action])switch(o){case"target":case"toTarget":L(s(o),t,`${n} (${e.action}) needs a ${o} with a role, name, label, text, test id, placeholder or selector`);break;case"urlOrValue":L(r("url")||r("value"),t,`${n} (${e.action}) needs a url`);break;case"targetOrValue":L(s("target")||r("value"),t,`${n} (${e.action}) needs a target or a value`);break;default:L(r(o),t,`${n} (${e.action}) needs ${o}`)}for(let o of["target","toTarget"])e[o]!==void 0&&L(s(o),t,`${n}.${o} has no usable locator`);if(e.action==="expectUrl"){let o=e.url??e.value;L(typeof o=="string"&&o.length>0,t,`${n} (expectUrl) needs a non-empty url or value`)}for(let o of["url","saveAs","compareTo","each","pattern","attribute","urlPattern"])e[o]!==void 0&&L(e[o].length>0,t,`${n}.${o} must not be empty`);["press","uploadFile","switchTab","expectText"].includes(e.action)&&L(typeof e.value=="string"&&e.value.length>0,t,`${n} (${e.action}) needs a non-empty value`)}function _i(e,t,n){L(fe(e),t,`${n} must be an object`),L(typeof e.action=="string",t,`${n}.action is missing`),L(Ri.has(e.action),t,`${n}.action "${e.action}" is not an action this runner knows. Recorded by a newer saffron-ai? Update, or re-record with --rerecord`);for(let r of Ai)L(e[r]===void 0||typeof e[r]=="string",t,`${n}.${r} must be a string`);L(e.opensTab===void 0||typeof e.opensTab=="boolean",t,`${n}.opensTab must be true or false`),e.target!==void 0&&ms(e.target,t,`${n}.target`),e.toTarget!==void 0&&ms(e.toTarget,t,`${n}.toTarget`),Pi(e,t,n)}function ji(e,t,n){L(fe(e),t,`${n} must be an object`),L(typeof e.gherkin=="string"&&e.gherkin.length>0,t,`${n}.gherkin (the step text) is missing`),L(typeof e.keyword=="string",t,`${n}.keyword is missing`),L(e.kind==="action"||e.kind==="assertion",t,`${n}.kind must be "action" or "assertion"`),L(Array.isArray(e.actions),t,`${n}.actions must be a list (empty for a recorded no-op)`),e.actions.forEach((r,s)=>_i(r,t,`${n}.actions[${s}]`)),e.table!==void 0&&L(Array.isArray(e.table)&&e.table.every(r=>Array.isArray(r)&&r.every(s=>typeof s=="string")),t,`${n}.table must be rows of strings`),L(e.docString===void 0||typeof e.docString=="string",t,`${n}.docString must be a string`),L(e.fromStepSet===void 0||typeof e.fromStepSet=="string",t,`${n}.fromStepSet must be a string`)}function Wn(e,t){return L(fe(e),t,"is not a JSON object"),L(e.version!==void 0,t,"has no version: not a Saffron recording"),L(e.version===Zt,t,typeof e.version=="number"&&e.version>Zt?`is schema version ${e.version}; this saffron-ai reads version ${Zt}. Update saffron-ai to the release that wrote it`:`has schema version ${JSON.stringify(e.version)}; this saffron-ai reads version ${Zt}`),L(typeof e.feature=="string"&&e.feature.length>0,t,"feature (the feature file path) is missing"),L(typeof e.scenario=="string"&&e.scenario.length>0,t,"scenario (the scenario name) is missing"),L(e.recordedAt===void 0||typeof e.recordedAt=="string",t,"recordedAt must be a string"),L(e.recordedBy===void 0||e.recordedBy==="agent"||e.recordedBy==="manual",t,'recordedBy must be "agent" or "manual"'),L(Array.isArray(e.steps),t,"steps must be a list"),e.steps.forEach((n,r)=>ji(n,t,`steps[${r}]`)),e.data!==void 0&&L(fe(e.data)&&Object.values(e.data).every(n=>typeof n=="string"),t,"data must map references to strings"),e}function ys(e,t){L(fe(e),t,"is not a JSON object"),L(fe(e.meta),t,"meta is missing: not a Saffron proposal");let n=e.meta;L(n.mode==="record"||n.mode==="heal",t,'meta.mode must be "record" or "heal"'),L(typeof n.narrative=="string",t,"meta.narrative must be a string"),L(Array.isArray(n.adaptations)&&n.adaptations.every(r=>typeof r=="string"),t,"meta.adaptations must be a list of strings"),L(n.verified===void 0||typeof n.verified=="boolean",t,"meta.verified must be true or false"),L(fe(n.usage),t,"meta.usage is missing"),n.featureEdits!==void 0&&L(Array.isArray(n.featureEdits)&&n.featureEdits.every(r=>fe(r)&&typeof r.index=="number"&&typeof r.text=="string"),t,"meta.featureEdits must be a list of { index, text }"),n.bound!==void 0&&L(fe(n.bound)&&["scenario","committed","recording"].every(r=>typeof n.bound[r]=="string"),t,"meta.bound must hold the scenario, committed and recording digests"),Wn(e.cache,`${t} (cache)`)}var Vn=".saffron";function en(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"unnamed"}function Jn(e){let t=e.split(/[\\/]/).filter(Boolean),n=t.pop()??"unnamed",r=t.slice(1),s=en([...r,rt.basename(n,".feature")].join("-"));if(r.length===0)return s;let o=Oi("sha1").update(e.replace(/\\/g,"/")).digest("hex").slice(0,6);return`${s}-${o}`}function J(e,t,n){return rt.join(e,Vn,"cache",Jn(t),`${en(n)}.json`)}function bs(e,t){let n=new Map;for(let r of t){let s=J(e,r.featurePath,r.name),o=n.get(s)??new Set;o.add(`${r.featurePath} \u203A ${r.name}`),n.set(s,o)}return[...n.entries()].filter(([,r])=>r.size>1).map(([r,s])=>`${[...s].join(" and ")} both map to ${rt.relative(e,r)}. Rename one scenario or one file.`)}var tn="owner.json";function me(e,t,n){return rt.join(e,Vn,"artifacts",Jn(t),en(n))}function ue(e,t,n){return rt.join(e,Vn,"proposals",Jn(t),`${en(n)}.json`)}function st(e,t){try{return oe(e)}catch(n){if(!(n instanceof V))throw n;t?.(e,n.problem);return}}function oe(e){if(!Xt.existsSync(e))return;let t;try{t=JSON.parse(Xt.readFileSync(e,"utf8"))}catch(n){throw new V(e,`is not valid JSON: ${n instanceof Error?n.message:String(n)}`)}return Wn(t,e)}function ot(e,t){Xt.mkdirSync(rt.dirname(e),{recursive:!0}),Xt.writeFileSync(e,`${JSON.stringify(t,null,2)}
10
+ `)}function kt(e,t){let n=r=>typeof r=="string"?r.includes(t):Array.isArray(r)?r.some(n):r&&typeof r=="object"?Object.entries(r).some(([s,o])=>s!=="pageFingerprint"&&n(o)):!1;return e.some(n)}function at(e,t){if(t.trim()==="")return!1;let n=r=>(r??"").replace(/<[^<>]+>/g,"");return e.some(r=>[r.value,r.url,r.urlPattern,r.pattern,r.promptText,r.compareTo,...[r.target,r.toTarget].flatMap(s=>[s?.name,s?.nameRegex,s?.label,s?.text,s?.placeholder,s?.testId,s?.frame,...s?.fallbackSelectors??[]])].some(s=>n(s).includes(t)))}function nn(e){let t=[];return{pattern:e.replace(/"((?:[^"\\]|\\.)*)"/g,(r,s)=>(t.push(s),`"\xAB${t.length-1}\xBB"`)),args:t}}function Fi(e,t){let n=r=>JSON.stringify(r.map(({pageFingerprint:s,...o})=>o));return n(e)===n(t)}function Ni(e){let t=new Set,n=r=>{if(r)for(let s of r.matchAll(/<([^<>]+)>/g))t.add(s[1])};for(let r of e.actions)n(r.value),n(r.url),n(r.pattern),n(r.target?.name),n(r.target?.nameRegex),n(r.target?.label),n(r.target?.text),n(r.target?.placeholder);return n(e.gherkin),t}function ye(e){let t=xs.join(e,".saffron","cache"),n=new Map,r=new Map,s=0;if(ws.existsSync(t))for(let i of ws.readdirSync(t,{recursive:!0})){let c=String(i);if(!c.endsWith(".json"))continue;let l=st(xs.join(t,c));if(l){s++;for(let f of l.steps){let d=f.gherkin,m=JSON.stringify(f.actions.map(({pageFingerprint:x,...k})=>k)),h=r.get(d)??[];h.includes(m)||(h.push(m),r.set(d,h));let y=n.get(d);!y||l.recordedAt>y.recordedAt?n.set(d,{gherkin:f.gherkin,table:f.table,docString:f.docString,actions:f.actions,sourceParams:_e(l.steps),sourceData:l.data,feature:l.feature,scenario:l.scenario,recordedAt:l.recordedAt,variants:h.length}):y.variants=h.length}}}let o=[...r.entries()].filter(([,i])=>i.length>1).map(([i])=>i),a=new Map;for(let i of n.values()){let{pattern:c,args:l}=nn(i.gherkin);if(l.length===0)continue;let f=a.get(c);(!f||i.recordedAt>f.recordedAt)&&a.set(c,{...i,args:l})}return{steps:n,patterns:a,divergent:o,scannedCaches:s}}function Ss(e,t,n){if(!!e.table!=!!t.table)return!1;if(e.table&&t.table){let o=Object.keys(ne(e.table)),a=Object.keys(ne(t.table));if(o.length>0&&a.length>0){if(JSON.stringify(o)!==JSON.stringify(a))return!1}else if(JSON.stringify(e.table)!==JSON.stringify(t.table))return!1}if(e.docString!==void 0!=(t.docString!==void 0)||e.docString!==void 0&&t.docString!==void 0&&e.docString!==t.docString&&!e.actions.some(o=>[o.value,o.url,o.pattern].some(a=>a?.includes("<docstring>"))))return!1;if(e.table&&t.table){let o=ne(e.table),a=ne(t.table);for(let i of Object.keys(o))if(o[i]!==a[i]&&(!kt(e.actions,`<${i}>`)||at(e.actions,o[i])))return!1}for(let[o,a]of Object.entries(e.sourceParams??{}))if(o in n&&n[o]!==a&&at(e.actions,a))return!1;for(let[o,a]of Object.entries(e.sourceData??{})){let i;try{i=nt(o)}catch{continue}if(i!==a&&(at(e.actions,a)||e.gherkin.includes(`{data:${o}}`)&&!kt(e.actions,`{data:${o}}`)))return!1}let r={...n,...ne(t.resolvedTable??t.table),...ze(t.resolvedDocString??t.docString)},s=Ni({gherkin:e.gherkin,keyword:"",kind:"action",actions:e.actions});for(let o of s)if(!(o in r))return!1;return!0}function vs(e,t,n){let r=e.steps.get(t.text);if(r)return Ss(r,t,n)?r:void 0;let{pattern:s,args:o}=nn(t.text);if(o.length===0)return;let a=e.patterns.get(s);if(!a)return;let i=new Map;for(let d=0;d<o.length;d++){let m=a.args[d],h=o[d];if(m===h)continue;let y=i.get(m);if(y!==void 0&&y!==h)return;i.set(m,h)}let c=d=>[d?.name,d?.nameRegex,d?.label,d?.text,d?.placeholder];for(let d of i.keys())if(!a.actions.some(h=>[h.value,h.url,h.pattern,...c(h.target)].includes(d)))return;let l=d=>d!==void 0&&i.has(d)?i.get(d):d,f={...a,gherkin:t.text,actions:a.actions.map(({pageFingerprint:d,...m})=>({...m,value:l(m.value),url:l(m.url),pattern:l(m.pattern),target:m.target?{...m.target,name:l(m.target.name),nameRegex:l(m.target.nameRegex),label:l(m.target.label),text:l(m.target.text),placeholder:l(m.target.placeholder)}:void 0}))};return Ss(f,t,n)?f:void 0}function ks(e,t){let n=[];return e.forEach((r,s)=>{if(r.actions.length!==0&&!t.steps.has(r.gherkin)){for(let o of t.steps.values())if(!(o.variants>1)&&o.actions.length!==0&&Fi(r.actions,o.actions)){n.push({index:s,from:r.gherkin,to:o.gherkin,feature:o.feature,scenario:o.scenario});break}}}),n}function Mi(e){if(!$s.existsSync(e))return[];let t=[];for(let n of $s.readdirSync(e,{recursive:!0})){let r=String(n);ke(r)&&t.push(Gn.join(e,r))}return t.sort()}function be(e,t,n,r){let s=Gn.isAbsolute(t)?t:Gn.join(e,t),o=Mi(s),a=n??ve(o.filter(h=>h.endsWith(".saffron")),e),i=new Map,c=new Map;for(let h of o)for(let y of r?.get(h)??se(h,e,a)){let x=`${y.featurePath}::${y.name}`;for(let k of y.steps){let w=i.get(k.text);if(w||(w={text:k.text,keywords:new Map,kind:k.kind,scenarios:new Set,files:new Set,fromStepSet:k.source?.fromStepSet},i.set(k.text,w)),w.keywords.set(k.keyword,(w.keywords.get(k.keyword)??0)+1),w.scenarios.add(x),k.source&&w.files.add(k.source.file),k.source?.fromStepSet){let v=c.get(k.source.fromStepSet);v||c.set(k.source.fromStepSet,v=new Set),v.add(x)}}}let l=ye(e),f=new Set(l.divergent),d=[...i.values()].map(h=>({text:h.text,keyword:[...h.keywords.entries()].sort((y,x)=>x[1]-y[1])[0][0],kind:h.kind,status:f.has(h.text)?"divergent":l.steps.has(h.text)?"recorded":"unrecorded",usage:h.scenarios.size,files:[...h.files].sort(),fromStepSet:h.fromStepSet})).sort((h,y)=>y.usage-h.usage||h.text.localeCompare(y.text)),m=[...a.values()].map(h=>({name:h.name,file:h.file,line:h.line,steps:h.steps.map(y=>y.text),usage:c.get(h.name)?.size??0})).sort((h,y)=>y.usage-h.usage||h.name.localeCompare(y.name));return{steps:d,stepSets:m,scannedFiles:o.length,scannedCaches:l.scannedCaches}}function rn(e,t){let n=t.toLowerCase();return{...e,steps:e.steps.filter(r=>r.text.toLowerCase().includes(n)),stepSets:e.stepSets.filter(r=>r.name.toLowerCase().includes(n)||r.steps.some(s=>s.toLowerCase().includes(n)))}}function Ts(e){let t={},n="saffron,feature,gherkin,cucumber";for(let r of e.steps)t[`step: ${r.text}`]={scope:n,prefix:r.text,body:[r.text.replace(/\$/g,"\\$")],description:`${r.status} \xB7 used in ${r.usage} scenario(s)`};for(let r of e.stepSets)t[`StepSet: ${r.name}`]={scope:n,prefix:`StepSet ${r.name}`,body:[`StepSet ${r.name}`],description:`step set (${r.steps.length} steps) \u2014 ${r.file}`};return JSON.stringify(t,null,2)+`
11
+ `}import{Server as Yc}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as Qc}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as Zc,ListToolsRequestSchema as Xc}from"@modelcontextprotocol/sdk/types.js";import de from"node:fs";import xe from"node:path";import Ps from"node:fs";import Wi from"node:path";import Rs from"node:fs";import Di from"node:path";var an=/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g;function $t(e){return e.replace(an,(t,n)=>{let r=process.env[n];if(r===void 0)throw new Error(`environment variable ${n} is not set (referenced as {env:${n}}) \u2014 export it or add it to .env`);return r})}function Yn(e){let t=new Set;for(let n of e)if(n)for(let r of n.matchAll(an))t.add(r[1]);return t}function X(e){let t=[];for(let n of e.steps){t.push(n.text,n.resolvedText),n.docString&&t.push(n.docString),n.resolvedDocString&&t.push(n.resolvedDocString);for(let r of n.table??[])t.push(...r);for(let r of n.resolvedTable??[])t.push(...r)}return t}function Qn(e){let t=[];for(let n of He(e))try{t.push(...Fe(n))}catch{}return t}function Te(e){let t=new Map,n=X(e);for(let r of Yn([...n,...Qn(n)])){let s=process.env[r];s!==void 0&&t.set(r,s)}return t}var Tt=3,Kn=String.raw`\{(?:env:[A-Za-z_][A-Za-z0-9_]*|data:[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+|unique:[A-Za-z0-9_-]+(?::digits)?)\}`,sn={'"':'\\"',"\\":"\\\\","\b":"\\b","\f":"\\f","\n":"\\n","\r":"\\r"," ":"\\t"};function Li(e,t,n,r){let s="";for(let o of e.split("")){let a=o.charCodeAt(0);if(t?.test(o)||a<32&&!sn[o]){let i=a.toString(16).padStart(4,"0");s+="\\u"+(n?i.toUpperCase():i)}else sn[o]?s+=sn[o]:o==="/"&&r?s+="\\/":s+=o}return s}function Ui(e){return[e,e.replace(/%[0-9A-F]{2}/g,t=>t.toLowerCase())]}function Es(e,t,n){return e.replace(/[&<>"'+]|[^\x00-\x7f]/gu,r=>t[r]??(n&&r>"\x7F"?n(r.codePointAt(0)):r))}function Bi(e,t){let n="";for(let r of e){let s=r<=" "||r>="\x7F"||t.test(r);n+=s?[...Buffer.from(r,"utf8")].map(o=>"%"+o.toString(16).toUpperCase().padStart(2,"0")).join(""):r}return n}var zi=[/["#<>']/,/["#<>?^`{}]/,/["#<>?^`{|}]/,/["#<>?^`{}/:;=@[\\\]|]/,/["#<>?^`{}/:;=@[\\\]|']/];function qi(e){let t=/[^\x00-\x7f]/,n=new Set([e]);for(let[s,o,a]of[[void 0,!1,!1],[void 0,!1,!0],[t,!1,!1],[t,!0,!1],[t,!1,!0],[/[<>&\u2028\u2029]/,!1,!1],[/[^\x00-\x7f]|[<>&'+`"]/,!0,!1]])n.add(Li(e,s,o,a));let r=s=>{try{for(let o of Ui(s()))n.add(o)}catch{}};r(()=>encodeURIComponent(e)),r(()=>encodeURIComponent(e).replaceAll("'","%27")),r(()=>encodeURI(e)),r(()=>new URLSearchParams([["",e]]).toString().slice(1));for(let s of zi)r(()=>Bi(e,s));for(let s of[{"&":"&amp;","<":"&lt;",">":"&gt;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&apos;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&#34;","'":"&#39;","+":"&#43;"}])n.add(Es(e,s));return n.add(Es(e,{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","+":"&#x2B;"},s=>`&#x${s.toString(16).toUpperCase()};`)),[...n]}var Rt=e=>e.map(t=>`{env:${t}}`).join(""),on=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),Hi=e=>Buffer.from(e,"utf8").toString("latin1");function it(e,t={}){let n=[...e.entries()].filter(([,h])=>h.length>=Tt).sort((h,y)=>y[1].length-h[1].length),r=new Map,s=new Set;for(let[h,y]of n){t.standaloneBelow!==void 0&&y.length<t.standaloneBelow&&s.add(y);let x=t.encodings?qi(y):[y];for(let k of x){let w=t.bytes?[Hi(k),.../^[\x00-\xff]*$/.test(k)?[k]:[]]:[k];for(let v of w)r.has(v)||r.set(v,h)}}let o=new Set(n.map(([,h])=>h)),a=new Set(["u"]);for(let[,h]of n)for(let y of h){let x=sn[y]??(y==="/"?"\\/":void 0);x&&a.add(x.slice(1))}let i=new RegExp(`\\\\[${[...a].map(on).join("")}]`);if(r.size===0)return{empty:!0,mask:h=>h,matches:()=>[],occursIn:()=>!1,isValue:()=>!1,hiddenByEscape:i};let c=[...r.keys()].sort((h,y)=>y.length-h.length).map(h=>s.has(h)?`(?<![A-Za-z0-9])${on(h)}(?![A-Za-z0-9])`:on(h)).join("|"),l=new RegExp(`(${Kn})|${c}`,"g"),f=new RegExp(`(${Kn})|${c}`,"y"),d=new RegExp(c),m=h=>{let y=[];l.lastIndex=0;for(let x=l.exec(h);x!==null;x=l.exec(h)){if(x[1]!==void 0)continue;let k=x.index,w=k+x[0].length,v=[r.get(x[0])];for(let u=k+1;u<w;u++){f.lastIndex=u;let p=f.exec(h);p&&p[1]===void 0&&u+p[0].length>w&&(w=u+p[0].length,v.push(r.get(p[0])))}y.push({start:k,end:w,names:[...new Set(v)]}),l.lastIndex=w}return y};return{empty:!1,hiddenByEscape:i,occursIn:h=>d.test(h),isValue:h=>o.has(h),matches:m,mask(h){let y="",x=0;for(let k of m(h))y+=h.slice(x,k.start)+Rt(k.names),x=k.end;return x===0?h:y+h.slice(x)}}}function Is(e,t){let n=new Map;for(let o of e)for(let a of o.split(/[\s"'`]+/)){let i=a.replace(/^[.,;:!?()[\]]+|[.,;:!?()[\]]+$/g,""),c=[...i.matchAll(an)].map(f=>f[1]);if(c.length===0||c.some(f=>!t.has(f)))continue;let l=i.replace(an,(f,d)=>t.get(d));l===i||l.length<Tt||i===`{env:${c[0]}}`||n.has(l)||n.set(l,i)}if(n.size===0)return o=>o;let r=[...n.keys()].sort((o,a)=>a.length-o.length).map(on).join("|"),s=new RegExp(`(${Kn})|${r}`,"g");return o=>o.replace(s,(a,i)=>i!==void 0?a:n.get(a))}var As=new WeakMap;function Re(e,t){let n=As.get(t);return n||(n=it(t,{encodings:!0}),As.set(t,n)),n.mask(e)}function Cs(e){let t=Di.join(e,".env");if(Rs.existsSync(t))for(let n of Rs.readFileSync(t,"utf8").split(`
12
+ `)){let r=n.trim();if(!r||r.startsWith("#"))continue;let s=r.indexOf("=");if(s<=0)continue;let o=r.slice(0,s).trim();if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(o))continue;let a=r.slice(s+1).trim();(a.startsWith('"')&&a.endsWith('"')||a.startsWith("'")&&a.endsWith("'"))&&(a=a.slice(1,-1)),process.env[o]===void 0&&(process.env[o]=a)}}function ae(e){Cs(e);let t=Wi.join(e,"saffron.config.json"),n=Ps.existsSync(t)?JSON.parse(Ps.readFileSync(t,"utf8")):{};return tt(e,n.dataDir,process.env.SAFFRON_ENV??n.env),n}var Zn={features:"features",dataDir:"data",actionTimeoutMs:5e3,pollIntervalMs:100,retries:1,screenshot:"viewport",maxTurns:100,strict:!1,verifyProposals:!0,snapshotMode:"none",reuseSteps:!0,browser:"chromium",workers:1,assertionPolicy:"strict",hookTimeoutMs:3e5,trace:"off"};function _s(e){return{...Zn,...Object.fromEntries(Object.entries(e).filter(([,t])=>t!==void 0))}}import Xn from"node:fs";import cn from"node:path";import{fileURLToPath as Vi}from"node:url";function ln(){let e=cn.dirname(Vi(import.meta.url));for(let t=0;t<6;t++){let n=cn.join(e,"package.json");if(Xn.existsSync(n))try{if(JSON.parse(Xn.readFileSync(n,"utf8")).name==="saffron-ai")return e}catch{}e=cn.dirname(e)}}function Ne(){let e=ln();if(!e)return"unknown";try{return JSON.parse(Xn.readFileSync(cn.join(e,"package.json"),"utf8")).version??"unknown"}catch{return"unknown"}}import re from"node:fs";import Y from"node:path";import ee from"node:fs";import{createHash as Ki}from"node:crypto";import js from"node:os";import pn from"node:path";import Me from"node:fs";import ct from"node:path";import{createHash as Ji,randomBytes as Gi}from"node:crypto";function lt(e,t){try{if(!Me.statSync(t).isFile())return!1;let n=ct.relative(Me.realpathSync(e),Me.realpathSync(t));return n!==""&&n!==".."&&!n.startsWith(".."+ct.sep)&&!ct.isAbsolute(n)}catch{return!1}}function Ve(e){return Ji("sha256").update(e).digest("hex")}function dn(e,t){Me.mkdirSync(ct.dirname(e),{recursive:!0});let n=`${e}.${process.pid}-${Gi(4).toString("hex")}.partial`;try{Me.writeFileSync(n,t),Me.renameSync(n,e)}catch(r){throw Me.rmSync(n,{force:!0}),r}}function dt(e,t){let n=ct.join(e,t.file);if(lt(ct.join(e,".saffron","artifacts"),n))try{let r=Me.readFileSync(n);return t.sha256&&Ve(r)!==t.sha256?void 0:r}catch{return}}function Os(e,t){ee.mkdirSync(pn.dirname(e),{recursive:!0});let n=Buffer.from(`${JSON.stringify(t,null,2)}
13
+ `);return fn(e,()=>dn(e,n)),nr(n)}var Yi=6e4,Qi=5e3,Zi=new Int32Array(new SharedArrayBuffer(4)),Et=class extends Error{};function fn(e,t,n=1e4){let r=`${e}.lock`,s=Date.now()+n,o;for(;;){o=JSON.stringify({pid:process.pid,host:js.hostname(),at:new Date().toISOString()});try{ee.writeFileSync(r,o,{flag:"wx"});break}catch(a){let i=a.code,c=i==="EPERM"&&process.platform==="win32";if(i!=="EEXIST"&&!c)throw a;if(Date.now()>=s)throw c?a:new Et(`${e} is being decided by another process. Try again.`);Xi(r)||Atomics.wait(Zi,0,0,25)}}try{return t()}finally{er(r)===o&&ee.rmSync(r,{force:!0})}}function er(e){try{return ee.readFileSync(e,"utf8")}catch{return}}function Xi(e){let t=er(e);if(t===void 0)return!1;let n={};try{n=JSON.parse(t)||{}}catch{}let r=Date.parse(n.at??""),s=Yi;if(Number.isNaN(r)){try{r=ee.statSync(e).mtimeMs}catch{return!1}s=Qi}let o=n.host===js.hostname()&&typeof n.pid=="number";return o&&ec(n.pid)||!o&&Date.now()-r<s||er(e)!==t?!1:(ee.rmSync(e,{force:!0}),!0)}function ec(e){try{return process.kill(e,0),!0}catch(t){return t.code!=="ESRCH"}}function un(e,t,n){return ee.existsSync(e)?fn(e,()=>ee.existsSync(e)?t():n()):n()}function tr(e){let t=/^(.+)#([0-9a-f]{64})$/i.exec(e);return t?{file:t[1],revision:t[2].toLowerCase()}:{file:e}}function At(e){return nr(ee.readFileSync(e))}var nr=e=>Ki("sha256").update(e).digest("hex");function pt(e){return gn(e).proposal}function gn(e){let t=ee.readFileSync(e),n;try{n=JSON.parse(t.toString("utf8"))}catch(r){throw new V(e,`is not valid JSON: ${r instanceof Error?r.message:String(r)}`)}return ys(n,e),{proposal:n,revision:nr(t)}}function De(e,t){let n=pn.join(e,".saffron","proposals");if(!ee.existsSync(n))return[];let r=[];for(let s of ee.readdirSync(n)){let o=pn.join(n,s);if(ee.statSync(o).isDirectory())for(let a of ee.readdirSync(o)){if(!a.endsWith(".json"))continue;let i=pn.join(o,a);try{r.push({file:i,...gn(i)})}catch(c){if(!t||!(c instanceof V))throw c;t(i,c.problem)}}}return r}function hn(e,t){let n=pt(t),r=J(e,n.cache.feature,n.cache.scenario);return ot(r,n.cache),ee.rmSync(t),r}function mn(e){ee.rmSync(e)}var rr=(e,t)=>Y.relative(e,t).split(Y.sep).join("/");function Fs(e){return re.existsSync(e)?[...re.readdirSync(e,{recursive:!0})].map(String).filter(t=>t.endsWith(".json")).map(t=>Y.join(e,t)).sort():[]}function Ns(e){try{let t=JSON.parse(re.readFileSync(e,"utf8")),n="cache"in t&&t.cache?t.cache:t;return{feature:n.feature,scenario:n.scenario}}catch{return{}}}function Ms(e){try{let t=JSON.parse(re.readFileSync(Y.join(e,tn),"utf8"));return{feature:typeof t.feature=="string"?t.feature:void 0,scenario:typeof t.scenario=="string"?t.scenario:void 0}}catch{return{}}}function bn(e,t,n){let r=Y.isAbsolute(t)?t:Y.join(e,t),s=new Set(re.existsSync(r)?[...re.readdirSync(r,{recursive:!0})].map(String).filter(ke).map(l=>Y.join(r,l)):[]);for(let l of["cache","proposals"])for(let f of Fs(Y.join(e,".saffron",l))){let{feature:d}=Ns(f);if(typeof d=="string"){let m=Y.resolve(e,d);re.existsSync(m)&&s.add(m)}}for(let l of yn(Y.join(e,".saffron","artifacts")))for(let f of yn(l)){let d=Ms(f).feature;if(d){let m=Y.resolve(e,d);re.existsSync(m)&&s.add(m)}}let o=[...n?.unreadable??[]],a=[...s].filter(l=>!n?.parsedFiles.has(l)),i=new Map;if(a.length>0)try{i=ve([...new Set([...We(r),...s].filter(l=>l.endsWith(".saffron")))],e)}catch(l){o.push(l instanceof Error?l.message:String(l))}let c=new Set(n?.owned??[]);for(let l of a){let f;try{f=se(l,e,i)}catch(d){o.push(`${rr(e,l)}: ${d instanceof Error?d.message:String(d)}`);continue}for(let d of f)c.add(J(e,d.featurePath,d.name)),c.add(ue(e,d.featurePath,d.name)),c.add(me(e,d.featurePath,d.displayName))}return{...tc(e,c),unreadable:o}}function tc(e,t){let n=[],r=0;for(let o of["cache","proposal"]){let a=Y.join(e,".saffron",o==="cache"?"cache":"proposals");for(let i of Fs(a)){if(t.has(i)){r++;continue}let{feature:c,scenario:l}=Ns(i),f=c?Y.join(e,c):void 0;n.push({file:rr(e,i),kind:o,feature:c,scenario:l,reason:c?re.existsSync(f)?"scenario-gone":"feature-gone":"unknown"})}}let s=Y.join(e,".saffron","artifacts");for(let o of yn(s))for(let a of yn(o)){if(t.has(a)){r++;continue}let{feature:i,scenario:c}=Ms(a);n.push({file:rr(e,a),kind:"artifact",feature:i,scenario:c,reason:i?re.existsSync(Y.join(e,i))?"scenario-gone":"feature-gone":"unknown"})}return{orphans:n,kept:r}}function yn(e){try{return re.readdirSync(e,{withFileTypes:!0}).filter(t=>t.isDirectory()).map(t=>Y.join(e,t.name)).sort()}catch{return[]}}function Ds(e,t){let n=[],r=new Set;for(let s of t){let o=Y.join(e,s.file);try{s.kind==="proposal"?fn(o,()=>re.rmSync(o)):re.rmSync(o,{recursive:s.kind==="artifact"}),n.push(s.file),r.add(Y.dirname(o))}catch{}}for(let s of r)try{re.readdirSync(s).length===0&&re.rmdirSync(s)}catch{}return n}function Ls(e){if(e.kind==="artifact")return`${e.file}/
14
14
  failure screenshots of a scenario that no longer exists`;let t=e.feature&&e.scenario?`${e.feature} \u203A ${e.scenario}`:"an unreadable recording",n=e.reason==="feature-gone"?"the feature file is gone":e.reason==="scenario-gone"?"the scenario is gone from that file":"it names no scenario";return`${e.file}
15
- ${e.kind==="proposal"?"pending proposal for ":""}${t}: ${n}`}import ro from"node:path";var Qi=/\{date([+-]\d+)?(?::([^}]+))?\}/g;function Zi(e,t){let n=String(e.getFullYear()),r=String(e.getMonth()+1).padStart(2,"0"),o=String(e.getDate()).padStart(2,"0");return t.replaceAll("YYYY",n).replaceAll("MM",r).replaceAll("DD",o).replaceAll("M",String(e.getMonth()+1)).replaceAll("D",String(e.getDate()))}function tr(e,t=new Date){return e.replace(Qi,(n,r,o)=>{let s=new Date(t);return s.setDate(s.getDate()+(r?parseInt(r,10):0)),Zi(s,o??"YYYY-MM-DD")})}var Xi=/\b(\d{4})-(\d{2})-(\d{2})\b/g,ec=400;function Fs(e,t=new Date){let n=new Date(t.getFullYear(),t.getMonth(),t.getDate());return e.replace(Xi,(r,o,s,a)=>{let i=new Date(Number(o),Number(s)-1,Number(a));if(Number.isNaN(i.getTime()))return r;let c=Math.round((i.getTime()-n.getTime())/864e5);return Math.abs(c)>ec?r:c===0?"{date}":`{date${c>0?"+":""}${c}}`})}import{createHash as tc,randomBytes as nc}from"node:crypto";var Ns=/\{unique:([A-Za-z0-9_-]+)(?::(digits))?\}/g,le="__saffron_unique_seed",We=()=>`${Date.now().toString(36)}${nc(4).toString("hex")}`;function Ms(e,t,n=!1){let r=tc("sha1").update(`${e}:${t}`).digest();if(n){let s=r.readUInt32BE(0)*65536+r.readUInt16BE(4);return String(1e8+s%9e8)}let o=BigInt(`0x${r.subarray(0,8).toString("hex")}`).toString(36).padStart(9,"0").slice(-9);return`${"abcdefghijklmnopqrstuvwxyz"[r[8]%26]}${o}`}function ft(e,t){return e.replace(Ns,(n,r,o)=>Ms(t,r,o==="digits"))}function ut(e,t){let n=new Map;for(let r of e)if(r)for(let o of r.matchAll(Ns))n.set(o[0],Ms(t,o[1],o[2]==="digits"));return n}function Ds(e,t){let n=e;for(let[r,o]of t)n=n.split(o).join(r);return n}import{createHash as rc}from"node:crypto";async function Je(e){let t;try{t=await e.locator("body").ariaSnapshot({timeout:3e3})}catch{return"sha256:unavailable"}let n=t.replace(/\d+/g,"#").replace(/\s+/g," ").trim();return`sha256:${rc("sha256").update(n).digest("hex")}`}var sc=/^[a-z][a-z\d+.-]*:|^\/\//i,Ls=e=>e.pathname.replace(/\/+$/,"");function It(e,t){if(!t)return e;let n,r;try{n=new URL(e),r=new URL(t)}catch{return e}if(n.origin!==r.origin)return e;let o=Ls(r),s=n.pathname,a;if(s===r.pathname||s===o||s===`${o}/`)a="/";else if(s.startsWith(`${o}/`))a=s.slice(o.length);else return e;let i=n.search===r.search?"":n.search||"?",c=a!=="/"?n.hash:n.hash===r.hash?"":n.hash||(r.hash?"#":"");return`${a}${i}${c}`}function nr(e,t){if(sc.test(e))return e;if(!t)throw new Error(`relative navigation "${e}" needs a base URL (set baseURL or pass --base-url)`);let n=new URL(t),r=e.indexOf("#"),o=r>=0?e.slice(r):void 0,s=r>=0?e.slice(0,r):e,a=s.indexOf("?"),i=a>=0?s.slice(a):void 0,c=a>=0?s.slice(0,a):s,p=c===""||c==="/",f=new URL(n.href);return f.pathname=p?n.pathname:`${Ls(n)}${c.startsWith("/")?c:`/${c}`}`,f.search=i===void 0?n.search:i==="?"?"":i,f.hash=o==="#"?"":o!==void 0?o:p?n.hash:"",f.toString()}import Ct from"node:fs";import ic from"node:os";import sr from"node:path";import rr from"node:zlib";var Us=67324752,Bs=33639248,zs=101010256;function qs(e){return Pt(e).map(t=>({name:t.name,data:t.read()}))}function Pt(e){let t=-1;for(let s=e.length-22;s>=Math.max(0,e.length-22-65535);s--)if(e.readUInt32LE(s)===zs){t=s;break}if(t<0)throw new Error("not a zip file: no central directory");let n=e.readUInt16LE(t+10),r=e.readUInt32LE(t+16),o=[];for(let s=0;s<n;s++){if(e.readUInt32LE(r)!==Bs)throw new Error("not a zip file: bad central directory entry");let a=e.readUInt16LE(r+10),i=e.readUInt32LE(r+16),c=e.readUInt32LE(r+20),p=e.readUInt32LE(r+24),f=e.readUInt16LE(r+28),d=e.readUInt16LE(r+30),m=e.readUInt16LE(r+32),h=e.readUInt32LE(r+42),y=e.subarray(r+46,r+46+f).toString("utf8");if(r+=46+f+d+m,e.readUInt32LE(h)!==Us)throw new Error(`not a zip file: bad local header for ${y}`);let b=e.readUInt16LE(h+26),k=e.readUInt16LE(h+28),w=h+30+b+k,v=e.subarray(w,w+c);if(!y.endsWith("/"))if(a===0)o.push({name:y,method:a,crc:i,size:p,raw:v,read:()=>Buffer.from(v)});else if(a===8)o.push({name:y,method:a,crc:i,size:p,raw:v,read:()=>rr.inflateRawSync(v)});else throw new Error(`zip entry ${y} uses compression method ${a}, which is not supported`)}return o}var oc=(()=>{let e=new Uint32Array(256);for(let t=0;t<256;t++){let n=t;for(let r=0;r<8;r++)n=n&1?3988292384^n>>>1:n>>>1;e[t]=n>>>0}return e})();function ac(e){let t=rr.crc32;if(t)return t(e)>>>0;let n=4294967295;for(let r of e)n=oc[(n^r)&255]^n>>>8;return(n^4294967295)>>>0}function Hs(e){let t=[],n=[],r=0;for(let a of e){let{name:i}=a,c="raw"in a,p=c?a.method:8,f=c?a.raw:rr.deflateRawSync(a.data),d=c?a.crc:ac(a.data),m=c?a.size:a.data.length,h=Buffer.from(i,"utf8"),y=Buffer.alloc(30);y.writeUInt32LE(Us,0),y.writeUInt16LE(20,4),y.writeUInt16LE(2048,6),y.writeUInt16LE(p,8),y.writeUInt32LE(0,10),y.writeUInt32LE(d,14),y.writeUInt32LE(f.length,18),y.writeUInt32LE(m,22),y.writeUInt16LE(h.length,26),y.writeUInt16LE(0,28);let b=Buffer.alloc(46);b.writeUInt32LE(Bs,0),b.writeUInt16LE(20,4),b.writeUInt16LE(20,6),b.writeUInt16LE(2048,8),b.writeUInt16LE(p,10),b.writeUInt32LE(0,12),b.writeUInt32LE(d,16),b.writeUInt32LE(f.length,20),b.writeUInt32LE(m,24),b.writeUInt16LE(h.length,28),b.writeUInt16LE(0,30),b.writeUInt16LE(0,32),b.writeUInt16LE(0,34),b.writeUInt16LE(0,36),b.writeUInt32LE(0,38),b.writeUInt32LE(r,42),t.push(y,h,f),n.push(b,h),r+=y.length+h.length+f.length}let o=n.reduce((a,i)=>a+i.length,0),s=Buffer.alloc(22);return s.writeUInt32LE(zs,0),s.writeUInt16LE(0,4),s.writeUInt16LE(0,6),s.writeUInt16LE(e.length,8),s.writeUInt16LE(e.length,10),s.writeUInt32LE(o,12),s.writeUInt32LE(r,16),s.writeUInt16LE(0,20),Buffer.concat([...t,...n,s])}var or=["off","on","retain-on-failure"],ar="trace.zip";async function ir(e,t){await e.tracing.start({screenshots:!0,snapshots:!0,sources:!1,title:t})}async function cr(e,t){t&&Ct.mkdirSync(sr.dirname(t),{recursive:!0}),await e.tracing.stop(t?{path:t}:void 0)}async function lr(e,t,n){let r;try{r=Ct.mkdtempSync(sr.join(ic.tmpdir(),"saffron-trace-"));let o=sr.join(r,ar);await e.tracing.stop({path:o});let s=wc(Ct.readFileSync(o),n);return ln(t,s),Ve(s)}catch{try{Ct.rmSync(t,{force:!0})}catch{console.warn("Could not remove the execution trace artifact.")}console.warn("Could not save the execution trace; no trace was attached to this run.");return}finally{if(r)try{Ct.rmSync(r,{recursive:!0,force:!0})}catch{console.warn("Could not remove the private temporary trace directory.")}}}async function Ws(e,t,n,r){if(!t)return r();let o=e.context().tracing;await o.group(n).catch(()=>{});try{return await r()}finally{await o.groupEnd().catch(()=>{})}}var cc=new Set(["callId","parentId","pageId","frameId","contextId","pageref","_frameref","snapshotName","beforeSnapshot","afterSnapshot","inputSnapshot","sha1","_sha1"]),lc=new Set(["before","action","after"]),dc=/^\[saffron:(?:agent|replay|pending|action):-?\d+\] /,pc=new Set(["type","class","method"]),fc=new Set(["__playwright_bounding_rect__","__playwright_scroll_top_","__playwright_scroll_left_","__playwright_target__","__playwright_checked_","__playwright_selected_","__playwright_popover_open_","__playwright_dialog_open_","__playwright_shadow_root_","__playwright_custom_elements__"]),uc=new Set(["style","__playwright_style_sheet_"]),gc=new Set(["srcset","imagesrcset"]),hc=/^(?:image\/(?!svg)|font\/|audio\/|video\/)/i,mc=/\.(?:jpe?g|png|gif|webp|avif|bmp|ico|woff2?|ttf|otf|eot|mp3|mp4|m4a|wav|ogg|webm)$/i,yc=/^text\/css\b/i,bc=/\.css$/i;function wc(e,t){let n=it(t,{encodings:!0});if(n.empty)return e;let r=it(t,{encodings:!0,bytes:!0}),o=Pt(e),s=new Set(o.filter(h=>h.name.includes("/")).map(h=>h.name));for(let h of o)h.name.startsWith("resources/")&&s.add(h.name.slice(10));let a={names:s,pieces:Pc(t)},i=new Set,c=new Set,p=h=>{for(let y of[h.snapshot?.response?.content,h.snapshot?.request?.postData]){if(!y)continue;let b=y._file??(y._sha1&&`resources/${y._sha1}`);if(!b)continue;let k=String(y.mimeType??"");hc.test(k)?i.add(b):yc.test(k)&&c.add(b)}},f=[...o],d=!1,m=(h,y)=>{y&&(f[h]={name:o[h].name,data:y},d=!0)};return o.forEach((h,y)=>{/\.(?:trace|network)$/.test(h.name)?m(y,xc(h.read(),n,a,h.name.endsWith(".network")?p:void 0)):h.name.endsWith(".stacks")&&m(y,vc(h.read(),n,a))}),o.forEach((h,y)=>{/\.(?:trace|network|stacks)$/.test(h.name)||i.has(h.name)||mc.test(h.name)||m(y,Sc(h.read(),n,r,c.has(h.name)||bc.test(h.name)))}),d?Hs(f):e}function xc(e,t,n,r){let o=e.toString("utf8").split(`
16
- `),s=!1;for(let a=0;a<o.length;a++){let i=o[a];if(!i.trim())continue;let c=JSON.parse(i);if(r?.(c),!t.occursIn(i)&&!t.hiddenByEscape.test(i))continue;let p=pr(i,t,{keys:!1,numbers:!1,playwright:n});p!==i&&(o[a]=p,s=!0)}return s?Buffer.from(o.join(`
17
- `),"utf8"):void 0}function vc(e,t,n){let r=e.toString("utf8");if(JSON.parse(r),!t.occursIn(r)&&!t.hiddenByEscape.test(r))return;let o=pr(r,t,{keys:!1,numbers:!1,playwright:n});return o===r?void 0:Buffer.from(o,"utf8")}function Sc(e,t,n,r=!1){if(r){let f=e.toString("latin1"),d=Ys(f,n);return d===f?void 0:Buffer.from(d,"latin1")}let o=e.toString("utf8"),s=o.startsWith("\uFEFF")?"\uFEFF":"",a=o.slice(s.length);if(!o.includes("\uFFFD")||Buffer.from(o,"utf8").equals(e)){let f=/^\s*[[{"]/.test(a)?Js(a,t):void 0,d=f===void 0&&/^\s*(?:[[{"\d-]|true|false|null)/.test(a)&&a.trim().includes(`
18
- `)?kc(a,t):void 0,m=f??d;if(m!==void 0)return m===a?void 0:Buffer.from(s+m,"utf8")}let c=e.toString("latin1"),p=n.mask(c);return p===c?void 0:Buffer.from(p,"latin1")}function Js(e,t){try{JSON.parse(e)}catch(n){if(n instanceof SyntaxError)return;throw n}return!t.occursIn(e)&&!t.hiddenByEscape.test(e)?e:pr(e,t,{keys:!0,numbers:!0})}function kc(e,t){let n=e.split(`
19
- `),r=[];for(let o of n){if(!o.trim()){r.push(o);continue}let a=Js(o,t);if(a===void 0)return;r.push(a)}return r.join(`
20
- `)}var Gs=/^([a-z][a-z0-9+.-]*:\/\/)([^/?#]*@)?([^/?#]*)/i;function Ks(e,t=0,n=e.length){let r=Gs.exec(e.slice(t,n));if(!r)return;let[o,s,a=""]=r,i=t+s.length+a.length;return c=>c>=t&&c<t+s.length||c>=i-(a?1:0)&&c<=t+o.length&&c<n}function $c(e,t,n){let r="",o=0;for(let s of t.matches(e)){let{token:a,kept:i=()=>!1}=n(s),c="";for(let p=s.start;p<s.end;){if(i(p)){c+=e[p++];continue}for(;p<s.end&&!i(p);)p++;c+=a(s.names)}c!==e.slice(s.start,s.end)&&(r+=e.slice(o,s.start)+c,o=s.end)}return o===0?e:r+e.slice(o)}var Tc=e=>e.map(t=>`%7Benv:${t}%7D`).join(""),Rc=e=>e.map(t=>`\\{env\\:${t}\\}`).join("");function dr(e,t,n,r){let o=n.map(([a,i])=>({start:a,end:i,kept:Ks(e,a,i)})),s=0;return $c(e,t,a=>{for(;s<o.length&&o[s].end<a.end;)s++;let i=o[s];return i&&i.start<=a.start&&a.end<=i.end?{token:Tc,kept:i.kept}:{token:r}})}function Ec(e,t){return Ks(e)?dr(e,t,[[0,e.length]],Rt):t.mask(e)}var Ac=/url\(\s*(?:"([^"]*)"|'([^']*)'|([^)\s'"]*))|@import\s+(?:"([^"]*)"|'([^']*)')/dgi;function Ys(e,t){if(!t.occursIn(e))return e;let n=[...e.matchAll(Ac)].map(r=>r.indices.slice(1).find(o=>o!==void 0));return dr(e,t,n,Rc)}function Ic(e,t){let n=[];for(let r=0;r<e.length;){for(;r<e.length&&/[\s,]/.test(e[r]);)r++;let o=r;for(;o<e.length&&!/\s/.test(e[o]);)o++;let s=o;for(;s>r&&e[s-1]===",";)s--;if(s>r&&n.push([r,s]),r=o,s===o)for(;r<e.length&&e[r]!==",";)r++}return dr(e,t,n,Rt)}function Pc(e){let t=new Map;for(let[n,r]of e)if(!(r.length<Tt||!Gs.test(r)||!URL.canParse(r)))for(let o of new URL(r).searchParams.values())o.length>=Tt&&!t.has(o)&&t.set(o,n);return t}var Vs=e=>e===" "||e===" "||e===`
21
- `||e==="\r";function pr(e,t,n){let r=[],o=0,s,a=[],i=c=>{let p=c-1;for(;p>=0&&Vs(e[p]);)p--;return e[p]};for(let c=0;c<e.length;c++){let p=e[c];if(p==="{"||p==="["){let f=a.at(-1),d=i(c)===":"?s:void 0;a.push({dom:(f?.dom??!1)||d==="html",logged:(f?.logged??!1)||d==="value"});continue}if(p==="}"||p==="]"){a.pop();continue}if(p==='"'){let f=c++,d=!1;for(;c<e.length&&e[c]!=='"';c++)e[c]==="\\"&&(d=!0,c++);let m=c+1,h=m;for(;h<e.length&&Vs(e[h]);)h++;let y=e[h]===":",b=d?JSON.parse(e.slice(f,m)):e.slice(f+1,c),k=a.at(-1),w=!y&&i(f)===":"?s:void 0,v=x=>t.mask(x);if(y){if(s=b,!n.keys)continue}else if(n.playwright){if(a.length===1&&w!==void 0&&pc.has(w)||n.playwright.names.has(b))continue;let{pieces:x}=n.playwright;if(v=g=>x.has(g)?Rt([x.get(g)]):Ec(g,t),k?.dom){if(i(f)==="["){k.tag=b.toUpperCase();continue}let g=w===void 0?k.tag:a.at(-2)?.tag;if(w!==void 0&&fc.has(w)||w==="src"&&(g==="IFRAME"||g==="FRAME"))continue;w===void 0&&g==="STYLE"||w!==void 0&&uc.has(w)?v=S=>Ys(S,t):w!==void 0&&gc.has(w)&&(v=S=>Ic(S,t))}else if(w!==void 0&&cc.has(w)||w==="phase"&&lc.has(b))continue}let l=n.playwright&&a.length===1&&w==="title"?dc.exec(b)?.[0]??"":"",u=l+v(b.slice(l.length));u!==b&&(r.push(e.slice(o,f),JSON.stringify(u)),o=m)}else if(p==="-"||p>="0"&&p<="9"){let f=c+1;for(;f<e.length&&/[0-9eE.+-]/.test(e[f]);)f++;(n.numbers||(a.at(-1)?.logged??!1)||i(c)===":"&&s==="value")&&t.isValue(e.slice(c,f))&&(r.push(e.slice(o,c),JSON.stringify(t.mask(e.slice(c,f)))),o=f),c=f-1}}return r.length?r.join("")+e.slice(o):e}function jt(e){return Array.isArray(e)?e.map(jt):e&&typeof e=="object"?Object.fromEntries(Object.entries(e).filter(([t,n])=>n!==void 0&&t!=="pageFingerprint").sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>[t,jt(n)])):e}var fr=e=>JSON.stringify(jt(e));function ur(e,t="",n={}){if(Array.isArray(e))n[t]=e.map(r=>String(r)).join(", ");else if(e&&typeof e=="object")for(let[r,o]of Object.entries(e))ur(o,t?`${t}.${r}`:r,n);else t&&(n[t]=String(e));return n}function Cc(e,t){let n=ur(jt(e)),r=ur(jt(t)),o=[...new Set([...Object.keys(n),...Object.keys(r)])].sort(),s=[];for(let a of o){if(a==="action"||n[a]===r[a])continue;let i=a.replace(/^target\./,"").replace(/^toTarget\./,"destination ");n[a]===void 0?s.push(`${i}: added "${r[a]}"`):r[a]===void 0?s.push(`${i}: removed "${n[a]}"`):s.push(`${i}: "${n[a]}" \u2192 "${r[a]}"`)}return s}function eo(e,t,n){let r=e.map(n),o=t.map(n),s=Array.from({length:r.length+1},()=>new Array(o.length+1).fill(0));for(let p=r.length-1;p>=0;p--)for(let f=o.length-1;f>=0;f--)s[p][f]=r[p]===o[f]?s[p+1][f+1]+1:Math.max(s[p+1][f],s[p][f+1]);let a=[],i=0,c=0;for(;i<r.length&&c<o.length;)r[i]===o[c]?(a.push({before:e[i],after:t[c]}),i++,c++):s[i+1][c]>=s[i][c+1]?(a.push({before:e[i]}),i++):(a.push({after:t[c]}),c++);for(;i<r.length;i++)a.push({before:e[i]});for(;c<o.length;c++)a.push({after:t[c]});return a}function Qs(e){if(!e)return"";let t=[];if(e.role){let n=e.name?` "${e.name}"`:e.nameRegex?` matching /${e.nameRegex}/`:"";t.push(`${e.role}${n}`)}else e.label?t.push(`label "${e.label}"`):e.text?t.push(`text "${e.text}"`):e.placeholder?t.push(`placeholder "${e.placeholder}"`):e.testId&&t.push(`test id ${e.testId}`);return e.fallbackSelectors?.length&&t.push(`[${e.fallbackSelectors.join(", ")}]`),e.frame&&t.push(`inside frame ${e.frame}`),t.join(" ")}var Zs=e=>`${e.method??"any"} /${e.urlPattern}/ ${e.status??"2xx"}`+(e.pattern?` with a body matching /${e.pattern}/`:"");function ce(e){let t=Qs(e.target),n=e.value??"",r=(()=>{switch(e.action){case"goto":return`go to ${e.url}`;case"click":return`click ${t}`;case"fill":return`fill ${t} with "${n}"`;case"press":return t?`press ${n} on ${t}`:`press ${n}`;case"selectOption":return`select "${n}" in ${t}`;case"check":return`check ${t}`;case"uncheck":return`uncheck ${t}`;case"hover":return`hover ${t}`;case"waitFor":return t?`wait for ${t}`:`wait ${n}ms`;case"captureText":return`remember ${t} as "${e.saveAs}"`;case"handleDialog":return`${n} the dialog${e.promptText?` with "${e.promptText}"`:""}`;case"uploadFile":return`upload ${n}`;case"dragTo":return`drag ${t} onto ${Qs(e.toTarget)}`;case"waitForResponse":return`wait for ${Zs(e)}`;case"switchTab":return`switch to tab ${n}`;case"closeTab":return`close tab ${n||"(the current one)"}`;case"newTab":return`open a new tab${e.url?` at ${e.url}`:""}`;case"expectVisible":return`expect ${t} to be visible`;case"expectNotVisible":return`expect ${t} to be gone`;case"expectText":return`expect ${t} to contain "${n}"`;case"expectUrl":return`expect the URL to contain "${e.url??n}"`;case"expectValue":return`expect ${t} to hold "${n}"`;case"expectDiffers":return`expect ${t||`"${n}"`} to differ from "${e.compareTo}"`;case"expectMatches":return`expect ${t} to match /${e.pattern}/`;case"expectAttribute":return`expect ${t} ${e.attribute} to match /${e.pattern}/`;case"expectResponse":return`expect a response ${Zs(e)}`;case"expectEach":return`expect ${t} to show every value of ${e.each}`;default:return e.action}})();return e.opensTab?`${r}, opening a tab`:r}var _t=e=>pe.has(e.action);function _c(e,t){let n=eo(e,t,fr),r=[];for(let o=0;o<n.length;o++){let s=n[o];if(s.before&&s.after){r.push({change:"same",before:ce(s.before),after:ce(s.after),assertion:_t(s.after)});continue}let a=n[o+1];if(s.before&&a?.after&&!a.before){r.push({change:"changed",before:ce(s.before),after:ce(a.after),details:Cc(s.before,a.after),assertion:_t(s.before)||_t(a.after)}),o++;continue}s.before&&r.push({change:"removed",before:ce(s.before),assertion:_t(s.before)}),s.after&&r.push({change:"added",after:ce(s.after),assertion:_t(s.after)})}return r}var jc=e=>`${e.kind}\0${e.gherkin}`;function gt(e,t){let n=eo(e?.steps??[],t.steps,jc),r=[],o={added:0,removed:0,changed:0,assertions:0};for(let s of n){let a=s.after??s.before,i=_c(s.before?.actions??[],s.after?.actions??[]);for(let p of i)p.change==="added"?o.added++:p.change==="removed"?o.removed++:p.change==="changed"&&o.changed++,p.change!=="same"&&p.assertion&&o.assertions++;let c=e&&s.before?s.after?i.some(p=>p.change!=="same")?"changed":"same":"removed":"added";r.push({gherkin:a.gherkin,kind:a.kind,fromStepSet:a.fromStepSet,change:c,actions:i})}return{feature:t.feature,scenario:t.scenario,hasCommitted:e!==void 0,steps:r,summary:o}}var Xs=new Set(["goto","click","press","newTab","switchTab","closeTab"]);function to(e,t){let n=[],r=o=>`${o} navigation action${o===1?"":"s"}`;for(let o=0;o<Math.min(e.length,t.length);o++){let s=e[o].actions.filter(d=>Xs.has(d.action)),i=[...t[o].actions.filter(d=>Xs.has(d.action))],c=[];for(let d of s){let m=i.findIndex(h=>fr(h)===fr(d));m>=0?i.splice(m,1):c.push(d)}let p=i;for(let d=c.length-1;d>=0;d--){let m=p.findIndex(h=>h.action===c[d].action);m>=0&&(p.splice(m,1),c.splice(d,1))}let f=`step ${o+1} ("${e[o].gherkin}")`;c.length>0&&n.push(`flow change: ${f} no longer performs ${r(c.length)} from the committed recording: ${c.map(ce).join("; ")}. A page or navigation step may have been removed or merged.`),p.length>0&&n.push(`flow change: ${f} now performs ${r(p.length)} the committed recording did not: ${p.map(ce).join("; ")}. A page or navigation step may have been added or reordered.`)}return n}function no(e){let t=[];t.push(`${e.feature} \u203A ${e.scenario}`),t.push(e.hasCommitted?"Comparing the committed recording with the proposed one.":"First recording for this scenario: there is nothing to compare against."),t.push("");for(let a of e.steps){let i=a.change==="removed"?"- ":a.change==="added"&&e.hasCommitted?"+ ":" ";t.push(`${i}${a.kind==="assertion"?"[assertion] ":""}${a.gherkin}`+(a.fromStepSet?` (from step set "${a.fromStepSet}")`:""));for(let c of a.actions)if(c.change==="same")t.push(` ${c.before}`);else if(c.change==="added")t.push(` + ${c.after}`);else if(c.change==="removed")t.push(` - ${c.before}`);else{t.push(` - ${c.before}`),t.push(` + ${c.after}`);for(let p of c.details??[])t.push(` ${p}`)}t.push("")}let{added:n,removed:r,changed:o,assertions:s}=e.summary;return t.push(n+r+o===0?"No action changed.":`${o} changed, ${n} added, ${r} removed.`+(s>0?` ${s} of them ${s===1?"is an assertion":"are assertions"}: read those first.`:"")),t.join(`
22
- `)}var mr=e=>new Promise(t=>setTimeout(t,e)),hr=class{entries=[];seq=0;prevStepSeq=0;curStepSeq=0;listener;context;attach(t,n){this.context=t,this.listener=r=>{let o={seq:++this.seq,url:r.url(),method:r.request().method(),status:r.status()};if(this.entries.push(o),n){let s=r.headers()["content-type"]??"";/json|text|xml/i.test(s)&&r.text().then(a=>{o.body=a.length>262144?a.slice(0,262144):a}).catch(()=>{})}},t.on("response",this.listener)}detach(){this.context&&this.listener&&this.context.off("response",this.listener)}markStep(){this.prevStepSeq=this.curStepSeq,this.curStepSeq=this.seq}sincePreviousStep(){return this.entries.filter(t=>t.seq>this.prevStepSeq)}all(){return this.entries}};function Oc(e,t){if(!t)return e>=200&&e<300;let n=/^([1-5])xx$/i.exec(t.trim());if(n){let r=Number(n[1])*100;return e>=r&&e<r+100}return e===Number(t.trim())}function so(e,t,n,r,o){return!(!t.test(e.url)||n&&e.method.toUpperCase()!==n.toUpperCase()||!Oc(e.status,r)||o&&!o.test(e.body??""))}var Fc=(e,t)=>`response matching ${e.method??"any-method"} /${t}/ status ${e.status??"2xx"}${e.pattern?` body /${e.pattern}/`:""}`,Nc=new Set(["click","fill","press","selectOption","check","uncheck","hover","dragTo","uploadFile"]);function oo(e,t,n,r=!1,o=!0){let s=c=>$t(St(tr(Ue(c,n)))),a=[],i=c=>{if(t.role){let p=t.nameRegex?new RegExp(s(t.nameRegex),"i"):t.name?s(t.name):void 0,f=t.role;p===void 0&&!!(t.label||t.placeholder||t.text||t.testId||t.fallbackSelectors?.length)?t.text&&a.push(c.getByRole(f).filter({hasText:s(t.text)})):typeof p=="string"?(a.push(c.getByRole(f,{name:p,exact:!0})),o&&a.push(c.getByRole(f,{name:p,exact:!1}))):a.push(c.getByRole(f,{name:p,exact:!1}))}t.label&&a.push(c.getByLabel(s(t.label))),t.placeholder&&a.push(c.getByPlaceholder(s(t.placeholder))),t.text&&!t.role&&a.push(c.getByText(s(t.text))),t.testId&&a.push(c.getByTestId(s(t.testId)));for(let p of t.fallbackSelectors??[])a.push(c.locator(s(p)))};if(t.frame)i(e.frameLocator(s(t.frame)));else if(i(e),r)for(let c of e.frames())c!==e.mainFrame()&&i(c);if(a.length===0)throw new Error("Action target has neither role nor fallback selectors");return a}var io=100;async function Mc(e,t,n,r){let o=Date.now()+t,s;for(;Date.now()<o;){try{if(await e())return}catch(a){s=a}await mr(r)}throw new Error(`Timed out waiting for ${n}${s?` (${String(s)})`:""}`)}async function we(e,t,n=!1){let r;for(let o of e)try{await t(o.first());return}catch(s){if(r=s,n&&await o.first().count().catch(()=>0)>0)throw s}throw r}var gr=e=>e.replace(/\s+/g," ").trim();async function yr(e,t,n,r,o,s=new Map,a,i=io){let c=(l,u,x)=>Mc(l,u,x,i);s.has(le)||s.set(le,We());let p=s.get(le),f=l=>typeof l=="string"?ft(l,p):Array.isArray(l)?l.map(f):l&&typeof l=="object"?Object.fromEntries(Object.entries(l).map(([u,x])=>[u,f(x)])):l;t=f(t),n=f(n);let d=l=>$t(St(tr(Ue(l,n)))),m=t.value!==void 0?d(t.value):void 0,h=t.url!==void 0?d(t.url):void 0,y=t.action==="expectVisible"||t.action==="expectText"||t.action==="expectValue"||t.action==="expectMatches"||t.action==="expectAttribute"||t.action==="expectEach"||t.action==="captureText",b=t.target?oo(e,t.target,n,y,!Nc.has(t.action)):[],k={timeout:r},w={timeout:Math.min(1e3,r)},v=l=>async()=>{for(let u of b)try{if(await l(u.first()))return!0}catch{}return!1};switch(t.action){case"goto":{if(!h)throw new Error("goto requires url");await e.goto(nr(h,o),k);return}case"click":return we(b,l=>l.click(k),!0);case"fill":return we(b,l=>l.fill(m??"",k),!0);case"press":return b.length>0?we(b,l=>l.press(m??"",k),!0):e.keyboard.press(m??"");case"selectOption":return we(b,async l=>{await l.selectOption(m??"",k)},!0);case"check":return we(b,l=>l.check(k),!0);case"uncheck":return we(b,l=>l.uncheck(k),!0);case"hover":return we(b,l=>l.hover(k),!0);case"waitFor":return b.length>0?we(b,l=>l.waitFor({state:"visible",timeout:r})):mr(Math.min(Number(m??"0"),r));case"expectVisible":return c(v(l=>l.isVisible()),r,"target to be visible");case"expectNotVisible":return c(async()=>{for(let l of b){let u=await l.count();for(let x=0;x<u;x++)if(await l.nth(x).isVisible())return!1}return!0},r,`every match of every recorded locator to be hidden (${b.length} locator(s))`);case"expectText":return c(v(async l=>(await l.textContent(w)??"").includes(m??"")),r,`text "${m}"`);case"expectEach":{let l=t.each?.match(/^\{data:([^}]+)\}$/)?.[1];if(!l)throw new Error(`expectEach needs a {data:...} list reference, got ${JSON.stringify(t.each)}`);let u=Oe(l),x=async S=>await S.isVisible()?new Set(await S.evaluate(T=>{let A=j=>(j??"").replace(/\s+/g," ").trim(),E=j=>typeof j.checkVisibility=="function"?j.checkVisibility({visibilityProperty:!0,contentVisibilityAuto:!0}):!!(j.offsetWidth||j.offsetHeight||j.getClientRects().length),C=j=>{let L=j.ownerDocument.createTreeWalker(j,4),_="";for(let $=L.nextNode();$;$=L.nextNode())$.parentElement&&E($.parentElement)&&(_+=$.nodeValue??"");return _},I=new Set,P=j=>{for(let L of Array.from(j.options))L.hidden||I.add(A(L.textContent))};T.tagName==="SELECT"&&P(T);for(let j of Array.from(T.querySelectorAll("*")))E(j)&&(j.tagName==="SELECT"?P(j):j.tagName!=="OPTION"&&I.add(A(C(j))));I.add(A(C(T)));for(let j of String(T.innerText??"").split(`
23
- `))I.add(A(j));return[...I]},void 0,w)):new Set,g=u;return c(async()=>{for(let S of b)try{let T=await x(S.first()),A=u.filter(E=>!T.has(E.replace(/\s+/g," ").trim()));if(A.length<g.length&&(g=A),A.length===0)return!0}catch{}return!1},r,`every value of ${t.each} to be shown`).catch(()=>{throw new Error(`${g.map(S=>`"${S}"`).join(", ")} from ${t.each} ${g.length===1?"is":"are"} not shown in the target (${u.length-g.length} of ${u.length} found)`)})}case"expectValue":return c(v(async l=>await l.inputValue(w)===(m??"")),r,`input value "${m}"`);case"expectUrl":{let l=h??m??"";return c(async()=>e.url().includes(l),r,`URL containing "${l}" on the active page (${e.url()})`)}case"handleDialog":{let l=(m??"accept")!=="dismiss",u=t.promptText?d(t.promptText):void 0;e.once("dialog",x=>{l?x.accept(u):x.dismiss()});return}case"uploadFile":{let l=(m??"").split(`
24
- `).filter(Boolean).map(u=>ro.isAbsolute(u)?u:ro.resolve(u));if(l.length===0)throw new Error("uploadFile requires a path");if(b.length>0)return we(b,u=>u.setInputFiles(l,k),!0);e.once("filechooser",u=>{u.setFiles(l)});return}case"dragTo":{if(!t.toTarget)throw new Error("dragTo requires toTarget");let l=oo(e,t.toTarget,n,!1,!1);return we(b,u=>u.dragTo(l[0].first(),k),!0)}case"captureText":{if(!t.saveAs)throw new Error("captureText requires saveAs");return c(v(async l=>{let u=gr(await l.textContent(w)??"");return u===""?!1:(s.set(t.saveAs,u),!0)}),r,`non-empty text to capture as "${t.saveAs}"`)}case"expectDiffers":{if(!t.compareTo)throw new Error("expectDiffers requires compareTo");let l=s.get(t.compareTo);if(l===void 0)throw new Error(`expectDiffers: no captured value named "${t.compareTo}"`);if(b.length>0)return c(v(async g=>gr(await g.textContent(w)??"")!==l),r,`target text to differ from "${t.compareTo}" (${l})`);let u=m??"",x=s.get(u);if(x===void 0)throw new Error(`expectDiffers: no captured value named "${u}"`);if(x===l)throw new Error(`expectDiffers: "${u}" (${x}) equals "${t.compareTo}" (${l})`);return}case"expectAttribute":{if(!t.attribute)throw new Error("expectAttribute requires attribute");if(!t.pattern)throw new Error("expectAttribute requires pattern");let l=new RegExp(t.pattern,"i");return c(v(async u=>l.test(await u.getAttribute(t.attribute,w)??"")),r,`attribute ${t.attribute} to match /${t.pattern}/i`)}case"expectMatches":{if(!t.pattern)throw new Error("expectMatches requires pattern");let l=new RegExp(t.pattern,"i");return c(v(async u=>l.test(gr(await u.textContent(w)??""))),r,`target text to match /${t.pattern}/i`)}case"switchTab":{let l=Number.parseInt(m??"",10),u=()=>e.context().pages().filter(g=>!g.isClosed());await c(async()=>u().length>l,r,`tab ${m} to exist (${u().length} open: ${u().map(g=>g.url()).join(", ")})`);let x=u()[l];return await x.bringToFront(),x}case"closeTab":{let l=e.context().pages().filter(g=>!g.isClosed()),u=m===void 0?e:l[Number.parseInt(m,10)];if(!u)throw new Error(`tab ${m} does not exist (${l.length} open)`);await u.close();let x=e.context().pages().filter(g=>!g.isClosed());if(x.length===0)throw new Error("closed the last tab; nothing left to drive");return x[x.length-1]}case"newTab":{let l=await e.context().newPage();return h&&await l.goto(nr(h,o),k),l}case"waitForResponse":case"expectResponse":{if(!t.urlPattern)throw new Error(`${t.action} requires urlPattern`);let l=new RegExp(d(t.urlPattern)),u=t.pattern?new RegExp(d(t.pattern)):void 0;if(!a){await e.waitForResponse(g=>so({seq:0,url:g.url(),method:g.request().method(),status:g.status()},l,t.method,t.status,void 0),k);return}let x=()=>t.action==="expectResponse"?a.all():a.sincePreviousStep();return c(async()=>x().some(g=>so(g,l,t.method,t.status,u)),r,Fc(t,t.urlPattern))}default:throw new Error(`Unknown action type: ${t.action}`)}}var bn=(e,t)=>kt(e.actions,t),ao=(e,t)=>at(e.actions,t);function br(e,t){if(t.steps.length!==e.steps.length)return!1;for(let[n,r]of Object.entries(t.data??{})){let o;try{o=nt(n)}catch{continue}if(o===r)continue;let s=t.steps.some(i=>bn(i,`{data:${n}}`)),a=t.steps.some(i=>ao(i,r));if(!s||a)return!1}return t.steps.every((n,r)=>{let o=e.steps[r];if(n.gherkin!==o.text||n.kind!==o.kind)return!1;let s=n.table,a=o.table;if(!!s!=!!a)return!1;if(s&&a){let i=Object.keys(ne(s)),c=Object.keys(ne(a));if(i.length>0&&c.length>0){if(JSON.stringify(i)!==JSON.stringify(c))return!1;let p=ne(s),f=ne(a);for(let d of i){if(p[d]===f[d])continue;let m=`<${d}>`,h=t.steps.some(b=>bn(b,m)),y=t.steps.some(b=>ao(b,p[d]));if(!h||y)return!1}}else if(JSON.stringify(s)!==JSON.stringify(a))return!1}if(n.docString!==void 0!=(o.docString!==void 0))return!1;if(n.docString!==void 0&&o.docString!==void 0&&n.docString!==o.docString){let i=t.steps.some(p=>bn(p,"<docstring>")),c=t.steps.some(p=>bn(p,n.docString));if(!i||c)return!1}return!0})}function wr(e,t){let n=e.steps[t],r=n?.resolvedTable??n?.table;return{...e.params,...Ce(e.steps),...ne(r),...Be(n?.resolvedDocString??n?.docString)}}async function Ot(e,t,n,r={}){let o=r.actionTimeoutMs??5e3,s=r.retries??1,a=Math.max(10,r.pollIntervalMs??io),i=[],c=r.vars??new Map;r.uniqueSeed&&c.set(le,r.uniqueSeed);let p=()=>Object.fromEntries(c),f=n.steps.some(x=>x.actions.some(g=>(g.action==="waitForResponse"||g.action==="expectResponse")&&g.pattern!==void 0)),d=new hr;d.attach(e.context(),f);let m=e,h=new Set(e.context().pages()),y=[],b=x=>y.push(x);e.context().on("page",b);let k=x=>new Promise(g=>{let S=()=>{clearTimeout(T),e.context().off("page",S),g()},T=setTimeout(S,x);e.context().on("page",S)}),w=[],l=!n.steps.some(x=>x.actions.some(g=>g.opensTab))&&n.steps.some(x=>x.actions.some(g=>g.action==="switchTab"||g.action==="closeTab"||g.action==="newTab")),u=()=>{let x=e.context().pages();for(let g of x)h.has(g)||(h.add(g),g.isClosed()||(m=g));if(m.isClosed()){let g=x.filter(S=>!S.isClosed());g.length>0&&(m=g[g.length-1])}};if(!br(t,n))return d.detach(),{status:"stale",stepResults:i,captured:p()};try{for(let x=0;x<n.steps.length;x++){let g=n.steps[x],S=wr(t,x);d.markStep();let T=!1,A=0;r.trace&&await e.context().tracing.group(`[saffron:${r.tracePhase??"replay"}:${x+(r.traceStepOffset??0)}] ${g.keyword} ${g.gherkin}`).catch(()=>{});let E=async()=>{r.trace&&await e.context().tracing.groupEnd().catch(()=>{})};try{if(g.kind==="assertion"&&!g.actions.some(C=>pe.has(C.action))){let C=`Assertion step "${g.gherkin}" recorded ${g.actions.length===0?"nothing":"only setup actions"}, so nothing was checked. Re-record the scenario (saffron run --rerecord) so the agent records what must be true.`;i.push({gherkin:g.gherkin,fromStepSet:g.fromStepSet,kind:g.kind,status:"failed",drift:T,error:C});for(let I=x+1;I<n.steps.length;I++)i.push({gherkin:n.steps[I].gherkin,kind:n.steps[I].kind,status:"skipped"});return{status:"failed",failure:{stepIndex:x,actionIndex:0,stepText:g.gherkin,kind:g.kind,error:C},stepResults:i,captured:p()}}for(let C=0;C<g.actions.length;C++){let I=g.actions[C];u(),I.pageFingerprint&&await Je(m)!==I.pageFingerprint&&(T=!0);let P,j=!1;for(let L=0;L<=s&&!j;L++){L>0&&(A++,await mr(250*L),u());try{let _=L>0?`${ce(I)} (attempt ${L+1})`:ce(I),$=await Ws(m,r.trace,`[saffron:action:${C}] ${_}`,()=>yr(m,I,S,o,r.baseURL,c,d,a));$&&(m=$,h.add($));let O=I.action==="click"||I.action==="press";if(y.length===0&&I.opensTab?await e.context().waitForEvent("page",{timeout:o}).catch(()=>{}):y.length===0&&O&&l&&await k(150),I.opensTab&&y.length===0)throw new Error("this action opened a new tab when it was recorded, and none opened now; the flow has changed");for(y.length>0&&w.push({stepIndex:x,actionIndex:C});y.length>0;)await y.shift().waitForLoadState("domcontentloaded",{timeout:o}).catch(()=>{});j=!0}catch(_){P=_}}if(!j){let L=P instanceof Error?P.message:String(P);i.push({gherkin:g.gherkin,fromStepSet:g.fromStepSet,kind:g.kind,status:"failed",drift:T,...A?{retries:A}:{},failedActionIndex:C,error:L});for(let $=x+1;$<n.steps.length;$++)i.push({gherkin:n.steps[$].gherkin,kind:n.steps[$].kind,status:"skipped"});let _={stepIndex:x,actionIndex:C,stepText:g.gherkin,kind:g.kind,error:L,pageUrl:m.isClosed()?void 0:m.url()};return await r.onFailure?.(m,_).catch(()=>{}),{status:"failed",failure:_,stepResults:i,captured:p(),tabOpeners:w,activePage:m.isClosed()?void 0:m}}}i.push({gherkin:g.gherkin,fromStepSet:g.fromStepSet,kind:g.kind,status:"passed",drift:T,...A?{retries:A}:{}})}finally{await E()}}return{status:"passed",stepResults:i,captured:p(),tabOpeners:w,activePage:m.isClosed()?void 0:m}}finally{e.context().off("page",b),d.detach()}}import{createHash as Dc}from"node:crypto";import Lc from"node:path";var xr=e=>Dc("sha1").update(JSON.stringify(e)).digest("hex").slice(0,16);function co(e){return{steps:e.steps.map(t=>({...t,actions:t.actions.map(({pageFingerprint:n,...r})=>r)})),data:e.data??null}}var lo=e=>xr(e.map(t=>[t.kind,t.text,t.table??null,t.docString??null])),po=e=>e?xr(co(e)):"none",fo=(e,t)=>xr({contract:co(e),featureEdits:t??null});function uo(e,t,n,r){return{scenario:lo(e.steps),committed:po(t),recording:fo(n,r)}}function Re(e,t,n){let r=t.meta.bound;if(!r)return;let o=[],{feature:s,scenario:a}=t.cache,i;if(n&&!(n instanceof Map))i=n;else try{i=se(Lc.join(e,s),e,n).find(p=>p.name===a)}catch(p){o.push(`${s} no longer parses: ${p instanceof Error?p.message.split(`
25
- `)[0]:String(p)}`)}o.length||(i?lo(i.steps)!==r.scenario&&o.push(Uc(i,t.cache)):o.push(`scenario "${a}" is no longer in ${s}`));let c;try{c=oe(J(e,s,a))}catch(p){if(!(p instanceof W))throw p;return o.push(`the committed recording cannot be read (${p.problem})`),o.join("; ")}return po(c)!==r.committed&&o.push(r.committed==="none"?"a recording was committed for this scenario after the proposal was filed, so the diff you would review is not the one that was made":c?"the committed recording changed after the proposal was filed (an accepted heal, a propagated locator fix or a pull), so the diff you would review is not the one that was made":"the committed recording the proposal was diffed against is gone"),fo(t.cache,t.meta.featureEdits)!==r.recording&&o.push("the proposal's recording or feature edits were edited after it was filed"),o.length>0?o.join("; "):void 0}function Uc(e,t){let n=e.steps,r=t.steps;if(n.length!==r.length)return`the scenario has ${n.length} steps now and had ${r.length} when the proposal was filed`;let o=n.findIndex((s,a)=>s.text!==r[a].gherkin||s.kind!==r[a].kind);return o<0?"the scenario's steps changed since the proposal was filed":`step ${o+1} now reads "${n[o].text}" and was "${r[o].gherkin}" when the proposal was filed`}var wn="not bound: filed by a runner older than 0.8.5, so changes since cannot be detected";import Ge from"node:fs";import go from"node:path";function Sr(e){return go.join(e,".saffron","history.jsonl")}function kr(e,t,n,r){let o=e.map(s=>({feature:s.scenario.featurePath,scenario:s.scenario.displayName,tags:[...s.scenario.tags],status:s.status,source:s.source,durationMs:s.durationMs,verified:s.verified,seededSteps:s.seededSteps,adaptations:s.adaptations.length,driftSteps:s.stepResults.filter(a=>a.drift).length,aiCalls:s.usage.aiCalls,tokensInOut:s.usage.inputTokens+s.usage.outputTokens,cacheRead:s.usage.cacheReadTokens,cacheWrite:s.usage.cacheCreationTokens,costUsd:s.usage.costUsd??0,error:s.error?.split(`
26
- `)[0]}));return{runId:t.toISOString(),startedAt:t.toISOString(),finishedAt:new Date().toISOString(),baseURL:r,green:o.filter(s=>s.status==="green").length,yellow:o.filter(s=>s.status==="yellow").length,red:o.filter(s=>s.status==="red").length,aiCalls:o.reduce((s,a)=>s+a.aiCalls,0),costUsd:o.reduce((s,a)=>s+a.costUsd,0),divergentSteps:n,scenarios:o}}function $r(e,t){let n=Sr(e);Ge.mkdirSync(go.dirname(n),{recursive:!0}),Ge.appendFileSync(n,`${JSON.stringify(t)}
27
- `)}function ho(e,t){let n=Sr(e);if(!Ge.existsSync(n))return $r(e,t);let r=Ge.readFileSync(n,"utf8").split(`
28
- `).filter(a=>a.trim()),o=!1,s=r.map(a=>{try{if(JSON.parse(a).runId===t.runId)return o=!0,JSON.stringify(t)}catch{}return a});o||s.push(JSON.stringify(t)),Ge.writeFileSync(n,`${s.join(`
15
+ ${e.kind==="proposal"?"pending proposal for ":""}${t}: ${n}`}import co from"node:path";var nc=/\{date([+-]\d+)?(?::([^}]+))?\}/g;function rc(e,t){let n=String(e.getFullYear()),r=String(e.getMonth()+1).padStart(2,"0"),s=String(e.getDate()).padStart(2,"0");return t.replaceAll("YYYY",n).replaceAll("MM",r).replaceAll("DD",s).replaceAll("M",String(e.getMonth()+1)).replaceAll("D",String(e.getDate()))}function sr(e,t=new Date){return e.replace(nc,(n,r,s)=>{let o=new Date(t);return o.setDate(o.getDate()+(r?parseInt(r,10):0)),rc(o,s??"YYYY-MM-DD")})}var sc=/\b(\d{4})-(\d{2})-(\d{2})\b/g,oc=400;function Us(e,t=new Date){let n=new Date(t.getFullYear(),t.getMonth(),t.getDate());return e.replace(sc,(r,s,o,a)=>{let i=new Date(Number(s),Number(o)-1,Number(a));if(Number.isNaN(i.getTime()))return r;let c=Math.round((i.getTime()-n.getTime())/864e5);return Math.abs(c)>oc?r:c===0?"{date}":`{date${c>0?"+":""}${c}}`})}import{createHash as ac,randomBytes as ic}from"node:crypto";var Bs=/\{unique:([A-Za-z0-9_-]+)(?::(digits))?\}/g,le="__saffron_unique_seed",Je=()=>`${Date.now().toString(36)}${ic(4).toString("hex")}`;function zs(e,t,n=!1){let r=ac("sha1").update(`${e}:${t}`).digest();if(n){let o=r.readUInt32BE(0)*65536+r.readUInt16BE(4);return String(1e8+o%9e8)}let s=BigInt(`0x${r.subarray(0,8).toString("hex")}`).toString(36).padStart(9,"0").slice(-9);return`${"abcdefghijklmnopqrstuvwxyz"[r[8]%26]}${s}`}function ft(e,t){return e.replace(Bs,(n,r,s)=>zs(t,r,s==="digits"))}function ut(e,t){let n=new Map;for(let r of e)if(r)for(let s of r.matchAll(Bs))n.set(s[0],zs(t,s[1],s[2]==="digits"));return n}function qs(e,t){let n=e;for(let[r,s]of t)n=n.split(s).join(r);return n}import{createHash as cc}from"node:crypto";async function Ge(e){let t;try{t=await e.locator("body").ariaSnapshot({timeout:3e3})}catch{return"sha256:unavailable"}let n=t.replace(/\d+/g,"#").replace(/\s+/g," ").trim();return`sha256:${cc("sha256").update(n).digest("hex")}`}var lc=/^[a-z][a-z\d+.-]*:|^\/\//i,Hs=e=>e.pathname.replace(/\/+$/,"");function It(e,t){if(!t)return e;let n,r;try{n=new URL(e),r=new URL(t)}catch{return e}if(n.origin!==r.origin)return e;let s=Hs(r),o=n.pathname,a;if(o===r.pathname||o===s||o===`${s}/`)a="/";else if(o.startsWith(`${s}/`))a=o.slice(s.length);else return e;let i=n.search===r.search?"":n.search||"?",c=a!=="/"?n.hash:n.hash===r.hash?"":n.hash||(r.hash?"#":"");return`${a}${i}${c}`}function or(e,t){if(lc.test(e))return e;if(!t)throw new Error(`relative navigation "${e}" needs a base URL (set baseURL or pass --base-url)`);let n=new URL(t),r=e.indexOf("#"),s=r>=0?e.slice(r):void 0,o=r>=0?e.slice(0,r):e,a=o.indexOf("?"),i=a>=0?o.slice(a):void 0,c=a>=0?o.slice(0,a):o,l=c===""||c==="/",f=new URL(n.href);return f.pathname=l?n.pathname:`${Hs(n)}${c.startsWith("/")?c:`/${c}`}`,f.search=i===void 0?n.search:i==="?"?"":i,f.hash=s==="#"?"":s!==void 0?s:l?n.hash:"",f.toString()}import Pt from"node:fs";import fc from"node:os";import ir from"node:path";import ar from"node:zlib";var Ws=67324752,Vs=33639248,Js=101010256;function Gs(e){return Ct(e).map(t=>({name:t.name,data:t.read()}))}function Ct(e){let t=-1;for(let o=e.length-22;o>=Math.max(0,e.length-22-65535);o--)if(e.readUInt32LE(o)===Js){t=o;break}if(t<0)throw new Error("not a zip file: no central directory");let n=e.readUInt16LE(t+10),r=e.readUInt32LE(t+16),s=[];for(let o=0;o<n;o++){if(e.readUInt32LE(r)!==Vs)throw new Error("not a zip file: bad central directory entry");let a=e.readUInt16LE(r+10),i=e.readUInt32LE(r+16),c=e.readUInt32LE(r+20),l=e.readUInt32LE(r+24),f=e.readUInt16LE(r+28),d=e.readUInt16LE(r+30),m=e.readUInt16LE(r+32),h=e.readUInt32LE(r+42),y=e.subarray(r+46,r+46+f).toString("utf8");if(r+=46+f+d+m,e.readUInt32LE(h)!==Ws)throw new Error(`not a zip file: bad local header for ${y}`);let x=e.readUInt16LE(h+26),k=e.readUInt16LE(h+28),w=h+30+x+k,v=e.subarray(w,w+c);if(!y.endsWith("/"))if(a===0)s.push({name:y,method:a,crc:i,size:l,raw:v,read:()=>Buffer.from(v)});else if(a===8)s.push({name:y,method:a,crc:i,size:l,raw:v,read:()=>ar.inflateRawSync(v)});else throw new Error(`zip entry ${y} uses compression method ${a}, which is not supported`)}return s}var dc=(()=>{let e=new Uint32Array(256);for(let t=0;t<256;t++){let n=t;for(let r=0;r<8;r++)n=n&1?3988292384^n>>>1:n>>>1;e[t]=n>>>0}return e})();function pc(e){let t=ar.crc32;if(t)return t(e)>>>0;let n=4294967295;for(let r of e)n=dc[(n^r)&255]^n>>>8;return(n^4294967295)>>>0}function Ks(e){let t=[],n=[],r=0;for(let a of e){let{name:i}=a,c="raw"in a,l=c?a.method:8,f=c?a.raw:ar.deflateRawSync(a.data),d=c?a.crc:pc(a.data),m=c?a.size:a.data.length,h=Buffer.from(i,"utf8"),y=Buffer.alloc(30);y.writeUInt32LE(Ws,0),y.writeUInt16LE(20,4),y.writeUInt16LE(2048,6),y.writeUInt16LE(l,8),y.writeUInt32LE(0,10),y.writeUInt32LE(d,14),y.writeUInt32LE(f.length,18),y.writeUInt32LE(m,22),y.writeUInt16LE(h.length,26),y.writeUInt16LE(0,28);let x=Buffer.alloc(46);x.writeUInt32LE(Vs,0),x.writeUInt16LE(20,4),x.writeUInt16LE(20,6),x.writeUInt16LE(2048,8),x.writeUInt16LE(l,10),x.writeUInt32LE(0,12),x.writeUInt32LE(d,16),x.writeUInt32LE(f.length,20),x.writeUInt32LE(m,24),x.writeUInt16LE(h.length,28),x.writeUInt16LE(0,30),x.writeUInt16LE(0,32),x.writeUInt16LE(0,34),x.writeUInt16LE(0,36),x.writeUInt32LE(0,38),x.writeUInt32LE(r,42),t.push(y,h,f),n.push(x,h),r+=y.length+h.length+f.length}let s=n.reduce((a,i)=>a+i.length,0),o=Buffer.alloc(22);return o.writeUInt32LE(Js,0),o.writeUInt16LE(0,4),o.writeUInt16LE(0,6),o.writeUInt16LE(e.length,8),o.writeUInt16LE(e.length,10),o.writeUInt32LE(s,12),o.writeUInt32LE(r,16),o.writeUInt16LE(0,20),Buffer.concat([...t,...n,o])}var cr=["off","on","retain-on-failure"],lr="trace.zip";async function dr(e,t){await e.tracing.start({screenshots:!0,snapshots:!0,sources:!1,title:t})}async function pr(e,t){t&&Pt.mkdirSync(ir.dirname(t),{recursive:!0}),await e.tracing.stop(t?{path:t}:void 0)}async function fr(e,t,n){let r;try{r=Pt.mkdtempSync(ir.join(fc.tmpdir(),"saffron-trace-"));let s=ir.join(r,lr);await e.tracing.stop({path:s});let o=$c(Pt.readFileSync(s),n);return dn(t,o),Ve(o)}catch{try{Pt.rmSync(t,{force:!0})}catch{console.warn("Could not remove the execution trace artifact.")}console.warn("Could not save the execution trace; no trace was attached to this run.");return}finally{if(r)try{Pt.rmSync(r,{recursive:!0,force:!0})}catch{console.warn("Could not remove the private temporary trace directory.")}}}async function Qs(e,t,n,r){if(!t)return r();let s=e.context().tracing;await s.group(n).catch(()=>{});try{return await r()}finally{await s.groupEnd().catch(()=>{})}}var uc=new Set(["callId","parentId","pageId","frameId","contextId","pageref","_frameref","snapshotName","beforeSnapshot","afterSnapshot","inputSnapshot","sha1","_sha1"]),gc=new Set(["before","action","after"]),hc=/^\[saffron:(?:agent|replay|pending|action):-?\d+\] /,mc=new Set(["type","class","method"]),yc=new Set(["__playwright_bounding_rect__","__playwright_scroll_top_","__playwright_scroll_left_","__playwright_target__","__playwright_checked_","__playwright_selected_","__playwright_popover_open_","__playwright_dialog_open_","__playwright_shadow_root_","__playwright_custom_elements__"]),bc=new Set(["style","__playwright_style_sheet_"]),wc=new Set(["srcset","imagesrcset"]),xc=/^(?:image\/(?!svg)|font\/|audio\/|video\/)/i,Sc=/\.(?:jpe?g|png|gif|webp|avif|bmp|ico|woff2?|ttf|otf|eot|mp3|mp4|m4a|wav|ogg|webm)$/i,vc=/^text\/css\b/i,kc=/\.css$/i;function $c(e,t){let n=it(t,{encodings:!0});if(n.empty)return e;let r=it(t,{encodings:!0,bytes:!0}),s=Ct(e),o=new Set(s.filter(h=>h.name.includes("/")).map(h=>h.name));for(let h of s)h.name.startsWith("resources/")&&o.add(h.name.slice(10));let a={names:o,pieces:Fc(t)},i=new Set,c=new Set,l=h=>{for(let y of[h.snapshot?.response?.content,h.snapshot?.request?.postData]){if(!y)continue;let x=y._file??(y._sha1&&`resources/${y._sha1}`);if(!x)continue;let k=String(y.mimeType??"");xc.test(k)?i.add(x):vc.test(k)&&c.add(x)}},f=[...s],d=!1,m=(h,y)=>{y&&(f[h]={name:s[h].name,data:y},d=!0)};return s.forEach((h,y)=>{/\.(?:trace|network)$/.test(h.name)?m(y,Tc(h.read(),n,a,h.name.endsWith(".network")?l:void 0)):h.name.endsWith(".stacks")&&m(y,Rc(h.read(),n,a))}),s.forEach((h,y)=>{/\.(?:trace|network|stacks)$/.test(h.name)||i.has(h.name)||Sc.test(h.name)||m(y,Ec(h.read(),n,r,c.has(h.name)||kc.test(h.name)))}),d?Ks(f):e}function Tc(e,t,n,r){let s=e.toString("utf8").split(`
16
+ `),o=!1;for(let a=0;a<s.length;a++){let i=s[a];if(!i.trim())continue;let c=JSON.parse(i);if(r?.(c),!t.occursIn(i)&&!t.hiddenByEscape.test(i))continue;let l=gr(i,t,{keys:!1,numbers:!1,playwright:n});l!==i&&(s[a]=l,o=!0)}return o?Buffer.from(s.join(`
17
+ `),"utf8"):void 0}function Rc(e,t,n){let r=e.toString("utf8");if(JSON.parse(r),!t.occursIn(r)&&!t.hiddenByEscape.test(r))return;let s=gr(r,t,{keys:!1,numbers:!1,playwright:n});return s===r?void 0:Buffer.from(s,"utf8")}function Ec(e,t,n,r=!1){if(r){let f=e.toString("latin1"),d=to(f,n);return d===f?void 0:Buffer.from(d,"latin1")}let s=e.toString("utf8"),o=s.startsWith("\uFEFF")?"\uFEFF":"",a=s.slice(o.length);if(!s.includes("\uFFFD")||Buffer.from(s,"utf8").equals(e)){let f=/^\s*[[{"]/.test(a)?Zs(a,t):void 0,d=f===void 0&&/^\s*(?:[[{"\d-]|true|false|null)/.test(a)&&a.trim().includes(`
18
+ `)?Ac(a,t):void 0,m=f??d;if(m!==void 0)return m===a?void 0:Buffer.from(o+m,"utf8")}let c=e.toString("latin1"),l=n.mask(c);return l===c?void 0:Buffer.from(l,"latin1")}function Zs(e,t){try{JSON.parse(e)}catch(n){if(n instanceof SyntaxError)return;throw n}return!t.occursIn(e)&&!t.hiddenByEscape.test(e)?e:gr(e,t,{keys:!0,numbers:!0})}function Ac(e,t){let n=e.split(`
19
+ `),r=[];for(let s of n){if(!s.trim()){r.push(s);continue}let a=Zs(s,t);if(a===void 0)return;r.push(a)}return r.join(`
20
+ `)}var Xs=/^([a-z][a-z0-9+.-]*:\/\/)([^/?#]*@)?([^/?#]*)/i;function eo(e,t=0,n=e.length){let r=Xs.exec(e.slice(t,n));if(!r)return;let[s,o,a=""]=r,i=t+o.length+a.length;return c=>c>=t&&c<t+o.length||c>=i-(a?1:0)&&c<=t+s.length&&c<n}function Ic(e,t,n){let r="",s=0;for(let o of t.matches(e)){let{token:a,kept:i=()=>!1}=n(o),c="";for(let l=o.start;l<o.end;){if(i(l)){c+=e[l++];continue}for(;l<o.end&&!i(l);)l++;c+=a(o.names)}c!==e.slice(o.start,o.end)&&(r+=e.slice(s,o.start)+c,s=o.end)}return s===0?e:r+e.slice(s)}var Cc=e=>e.map(t=>`%7Benv:${t}%7D`).join(""),Pc=e=>e.map(t=>`\\{env\\:${t}\\}`).join("");function ur(e,t,n,r){let s=n.map(([a,i])=>({start:a,end:i,kept:eo(e,a,i)})),o=0;return Ic(e,t,a=>{for(;o<s.length&&s[o].end<a.end;)o++;let i=s[o];return i&&i.start<=a.start&&a.end<=i.end?{token:Cc,kept:i.kept}:{token:r}})}function _c(e,t){return eo(e)?ur(e,t,[[0,e.length]],Rt):t.mask(e)}var jc=/url\(\s*(?:"([^"]*)"|'([^']*)'|([^)\s'"]*))|@import\s+(?:"([^"]*)"|'([^']*)')/dgi;function to(e,t){if(!t.occursIn(e))return e;let n=[...e.matchAll(jc)].map(r=>r.indices.slice(1).find(s=>s!==void 0));return ur(e,t,n,Pc)}function Oc(e,t){let n=[];for(let r=0;r<e.length;){for(;r<e.length&&/[\s,]/.test(e[r]);)r++;let s=r;for(;s<e.length&&!/\s/.test(e[s]);)s++;let o=s;for(;o>r&&e[o-1]===",";)o--;if(o>r&&n.push([r,o]),r=s,o===s)for(;r<e.length&&e[r]!==",";)r++}return ur(e,t,n,Rt)}function Fc(e){let t=new Map;for(let[n,r]of e)if(!(r.length<Tt||!Xs.test(r)||!URL.canParse(r)))for(let s of new URL(r).searchParams.values())s.length>=Tt&&!t.has(s)&&t.set(s,n);return t}var Ys=e=>e===" "||e===" "||e===`
21
+ `||e==="\r";function gr(e,t,n){let r=[],s=0,o,a=[],i=c=>{let l=c-1;for(;l>=0&&Ys(e[l]);)l--;return e[l]};for(let c=0;c<e.length;c++){let l=e[c];if(l==="{"||l==="["){let f=a.at(-1),d=i(c)===":"?o:void 0;a.push({dom:(f?.dom??!1)||d==="html",logged:(f?.logged??!1)||d==="value"});continue}if(l==="}"||l==="]"){a.pop();continue}if(l==='"'){let f=c++,d=!1;for(;c<e.length&&e[c]!=='"';c++)e[c]==="\\"&&(d=!0,c++);let m=c+1,h=m;for(;h<e.length&&Ys(e[h]);)h++;let y=e[h]===":",x=d?JSON.parse(e.slice(f,m)):e.slice(f+1,c),k=a.at(-1),w=!y&&i(f)===":"?o:void 0,v=b=>t.mask(b);if(y){if(o=x,!n.keys)continue}else if(n.playwright){if(a.length===1&&w!==void 0&&mc.has(w)||n.playwright.names.has(x))continue;let{pieces:b}=n.playwright;if(v=g=>b.has(g)?Rt([b.get(g)]):_c(g,t),k?.dom){if(i(f)==="["){k.tag=x.toUpperCase();continue}let g=w===void 0?k.tag:a.at(-2)?.tag;if(w!==void 0&&yc.has(w)||w==="src"&&(g==="IFRAME"||g==="FRAME"))continue;w===void 0&&g==="STYLE"||w!==void 0&&bc.has(w)?v=S=>to(S,t):w!==void 0&&wc.has(w)&&(v=S=>Oc(S,t))}else if(w!==void 0&&uc.has(w)||w==="phase"&&gc.has(x))continue}let u=n.playwright&&a.length===1&&w==="title"?hc.exec(x)?.[0]??"":"",p=u+v(x.slice(u.length));p!==x&&(r.push(e.slice(s,f),JSON.stringify(p)),s=m)}else if(l==="-"||l>="0"&&l<="9"){let f=c+1;for(;f<e.length&&/[0-9eE.+-]/.test(e[f]);)f++;(n.numbers||(a.at(-1)?.logged??!1)||i(c)===":"&&o==="value")&&t.isValue(e.slice(c,f))&&(r.push(e.slice(s,c),JSON.stringify(t.mask(e.slice(c,f)))),s=f),c=f-1}}return r.length?r.join("")+e.slice(s):e}function jt(e){return Array.isArray(e)?e.map(jt):e&&typeof e=="object"?Object.fromEntries(Object.entries(e).filter(([t,n])=>n!==void 0&&t!=="pageFingerprint").sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>[t,jt(n)])):e}var hr=e=>JSON.stringify(jt(e));function mr(e,t="",n={}){if(Array.isArray(e))n[t]=e.map(r=>String(r)).join(", ");else if(e&&typeof e=="object")for(let[r,s]of Object.entries(e))mr(s,t?`${t}.${r}`:r,n);else t&&(n[t]=String(e));return n}function Nc(e,t){let n=mr(jt(e)),r=mr(jt(t)),s=[...new Set([...Object.keys(n),...Object.keys(r)])].sort(),o=[];for(let a of s){if(a==="action"||n[a]===r[a])continue;let i=a.replace(/^target\./,"").replace(/^toTarget\./,"destination ");n[a]===void 0?o.push(`${i}: added "${r[a]}"`):r[a]===void 0?o.push(`${i}: removed "${n[a]}"`):o.push(`${i}: "${n[a]}" \u2192 "${r[a]}"`)}return o}function oo(e,t,n){let r=e.map(n),s=t.map(n),o=Array.from({length:r.length+1},()=>new Array(s.length+1).fill(0));for(let l=r.length-1;l>=0;l--)for(let f=s.length-1;f>=0;f--)o[l][f]=r[l]===s[f]?o[l+1][f+1]+1:Math.max(o[l+1][f],o[l][f+1]);let a=[],i=0,c=0;for(;i<r.length&&c<s.length;)r[i]===s[c]?(a.push({before:e[i],after:t[c]}),i++,c++):o[i+1][c]>=o[i][c+1]?(a.push({before:e[i]}),i++):(a.push({after:t[c]}),c++);for(;i<r.length;i++)a.push({before:e[i]});for(;c<s.length;c++)a.push({after:t[c]});return a}function no(e){if(!e)return"";let t=[];if(e.role){let n=e.name?` "${e.name}"`:e.nameRegex?` matching /${e.nameRegex}/`:"";t.push(`${e.role}${n}`)}else e.label?t.push(`label "${e.label}"`):e.text?t.push(`text "${e.text}"`):e.placeholder?t.push(`placeholder "${e.placeholder}"`):e.testId&&t.push(`test id ${e.testId}`);return e.fallbackSelectors?.length&&t.push(`[${e.fallbackSelectors.join(", ")}]`),e.frame&&t.push(`inside frame ${e.frame}`),t.join(" ")}var ro=e=>`${e.method??"any"} /${e.urlPattern}/ ${e.status??"2xx"}`+(e.pattern?` with a body matching /${e.pattern}/`:"");function ce(e){let t=no(e.target),n=e.value??"",r=(()=>{switch(e.action){case"goto":return`go to ${e.url}`;case"click":return`click ${t}`;case"fill":return`fill ${t} with "${n}"`;case"press":return t?`press ${n} on ${t}`:`press ${n}`;case"selectOption":return`select "${n}" in ${t}`;case"check":return`check ${t}`;case"uncheck":return`uncheck ${t}`;case"hover":return`hover ${t}`;case"waitFor":return t?`wait for ${t}`:`wait ${n}ms`;case"captureText":return`remember ${t} as "${e.saveAs}"`;case"handleDialog":return`${n} the dialog${e.promptText?` with "${e.promptText}"`:""}`;case"uploadFile":return`upload ${n}`;case"dragTo":return`drag ${t} onto ${no(e.toTarget)}`;case"waitForResponse":return`wait for ${ro(e)}`;case"switchTab":return`switch to tab ${n}`;case"closeTab":return`close tab ${n||"(the current one)"}`;case"newTab":return`open a new tab${e.url?` at ${e.url}`:""}`;case"expectVisible":return`expect ${t} to be visible`;case"expectNotVisible":return`expect ${t} to be gone`;case"expectText":return`expect ${t} to contain "${n}"`;case"expectUrl":return`expect the URL to contain "${e.url??n}"`;case"expectValue":return`expect ${t} to hold "${n}"`;case"expectDiffers":return`expect ${t||`"${n}"`} to differ from "${e.compareTo}"`;case"expectMatches":return`expect ${t} to match /${e.pattern}/`;case"expectAttribute":return`expect ${t} ${e.attribute} to match /${e.pattern}/`;case"expectResponse":return`expect a response ${ro(e)}`;case"expectEach":return`expect ${t} to show every value of ${e.each}`;default:return e.action}})();return e.opensTab?`${r}, opening a tab`:r}var _t=e=>pe.has(e.action);function Mc(e,t){let n=oo(e,t,hr),r=[];for(let s=0;s<n.length;s++){let o=n[s];if(o.before&&o.after){r.push({change:"same",before:ce(o.before),after:ce(o.after),assertion:_t(o.after)});continue}let a=n[s+1];if(o.before&&a?.after&&!a.before){r.push({change:"changed",before:ce(o.before),after:ce(a.after),details:Nc(o.before,a.after),assertion:_t(o.before)||_t(a.after)}),s++;continue}o.before&&r.push({change:"removed",before:ce(o.before),assertion:_t(o.before)}),o.after&&r.push({change:"added",after:ce(o.after),assertion:_t(o.after)})}return r}var Dc=e=>`${e.kind}\0${e.gherkin}`;function gt(e,t){let n=oo(e?.steps??[],t.steps,Dc),r=[],s={added:0,removed:0,changed:0,assertions:0};for(let o of n){let a=o.after??o.before,i=Mc(o.before?.actions??[],o.after?.actions??[]);for(let l of i)l.change==="added"?s.added++:l.change==="removed"?s.removed++:l.change==="changed"&&s.changed++,l.change!=="same"&&l.assertion&&s.assertions++;let c=e&&o.before?o.after?i.some(l=>l.change!=="same")?"changed":"same":"removed":"added";r.push({gherkin:a.gherkin,kind:a.kind,fromStepSet:a.fromStepSet,change:c,actions:i})}return{feature:t.feature,scenario:t.scenario,hasCommitted:e!==void 0,steps:r,summary:s}}var so=new Set(["goto","click","press","newTab","switchTab","closeTab"]);function ao(e,t){let n=[],r=s=>`${s} navigation action${s===1?"":"s"}`;for(let s=0;s<Math.min(e.length,t.length);s++){let o=e[s].actions.filter(d=>so.has(d.action)),i=[...t[s].actions.filter(d=>so.has(d.action))],c=[];for(let d of o){let m=i.findIndex(h=>hr(h)===hr(d));m>=0?i.splice(m,1):c.push(d)}let l=i;for(let d=c.length-1;d>=0;d--){let m=l.findIndex(h=>h.action===c[d].action);m>=0&&(l.splice(m,1),c.splice(d,1))}let f=`step ${s+1} ("${e[s].gherkin}")`;c.length>0&&n.push(`flow change: ${f} no longer performs ${r(c.length)} from the committed recording: ${c.map(ce).join("; ")}. A page or navigation step may have been removed or merged.`),l.length>0&&n.push(`flow change: ${f} now performs ${r(l.length)} the committed recording did not: ${l.map(ce).join("; ")}. A page or navigation step may have been added or reordered.`)}return n}function io(e){let t=[];t.push(`${e.feature} \u203A ${e.scenario}`),t.push(e.hasCommitted?"Comparing the committed recording with the proposed one.":"First recording for this scenario: there is nothing to compare against."),t.push("");for(let a of e.steps){let i=a.change==="removed"?"- ":a.change==="added"&&e.hasCommitted?"+ ":" ";t.push(`${i}${a.kind==="assertion"?"[assertion] ":""}${a.gherkin}`+(a.fromStepSet?` (from step set "${a.fromStepSet}")`:""));for(let c of a.actions)if(c.change==="same")t.push(` ${c.before}`);else if(c.change==="added")t.push(` + ${c.after}`);else if(c.change==="removed")t.push(` - ${c.before}`);else{t.push(` - ${c.before}`),t.push(` + ${c.after}`);for(let l of c.details??[])t.push(` ${l}`)}t.push("")}let{added:n,removed:r,changed:s,assertions:o}=e.summary;return t.push(n+r+s===0?"No action changed.":`${s} changed, ${n} added, ${r} removed.`+(o>0?` ${o} of them ${o===1?"is an assertion":"are assertions"}: read those first.`:"")),t.join(`
22
+ `)}var wr=e=>new Promise(t=>setTimeout(t,e)),br=class{entries=[];seq=0;prevStepSeq=0;curStepSeq=0;listener;context;attach(t,n){this.context=t,this.listener=r=>{let s={seq:++this.seq,url:r.url(),method:r.request().method(),status:r.status()};if(this.entries.push(s),n){let o=r.headers()["content-type"]??"";/json|text|xml/i.test(o)&&r.text().then(a=>{s.body=a.length>262144?a.slice(0,262144):a}).catch(()=>{})}},t.on("response",this.listener)}detach(){this.context&&this.listener&&this.context.off("response",this.listener)}markStep(){this.prevStepSeq=this.curStepSeq,this.curStepSeq=this.seq}sincePreviousStep(){return this.entries.filter(t=>t.seq>this.prevStepSeq)}all(){return this.entries}};function Lc(e,t){if(!t)return e>=200&&e<300;let n=/^([1-5])xx$/i.exec(t.trim());if(n){let r=Number(n[1])*100;return e>=r&&e<r+100}return e===Number(t.trim())}function lo(e,t,n,r,s){return!(!t.test(e.url)||n&&e.method.toUpperCase()!==n.toUpperCase()||!Lc(e.status,r)||s&&!s.test(e.body??""))}var Uc=(e,t)=>`response matching ${e.method??"any-method"} /${t}/ status ${e.status??"2xx"}${e.pattern?` body /${e.pattern}/`:""}`,po=new Set(["click","fill","press","selectOption","check","uncheck","hover","dragTo","uploadFile"]);function fo(e,t,n,r=!1,s=!0){let o=c=>$t(vt(sr(Be(c,n)))),a=[],i=c=>{if(t.role){let l=t.nameRegex?new RegExp(o(t.nameRegex),"i"):t.name?o(t.name):void 0,f=t.role;l===void 0&&!!(t.label||t.placeholder||t.text||t.testId||t.fallbackSelectors?.length)?t.text&&a.push(c.getByRole(f).filter({hasText:o(t.text)})):typeof l=="string"?(a.push(c.getByRole(f,{name:l,exact:!0})),s&&a.push(c.getByRole(f,{name:l,exact:!1}))):a.push(c.getByRole(f,{name:l,exact:!1}))}t.label&&a.push(c.getByLabel(o(t.label))),t.placeholder&&a.push(c.getByPlaceholder(o(t.placeholder))),t.text&&!t.role&&a.push(c.getByText(o(t.text))),t.testId&&a.push(c.getByTestId(o(t.testId)));for(let l of t.fallbackSelectors??[])a.push(c.locator(o(l)))};if(t.frame)i(e.frameLocator(o(t.frame)));else if(i(e),r)for(let c of e.frames())c!==e.mainFrame()&&i(c);if(a.length===0)throw new Error("Action target has neither role nor fallback selectors");return a}var go=100;async function Bc(e,t,n,r){let s=Date.now()+t,o;for(;Date.now()<s;){try{if(await e())return}catch(a){o=a}await wr(r)}throw new Error(`Timed out waiting for ${n}${o?` (${String(o)})`:""}`)}async function we(e,t,n=!1){let r;for(let s of e)try{await t(s.first());return}catch(o){if(r=o,n&&await s.first().count().catch(()=>0)>0)throw o}throw r}var yr=e=>e.replace(/\s+/g," ").trim(),wn=new WeakMap;async function Sn(e,t,n,r,s,o=new Map,a,i=go){let c=(p,b,g)=>Bc(p,b,g,i),l=wn.get(e);if(l&&po.has(t.action)&&t.action!=="uploadFile"){wn.delete(e);let p,b=S=>{p??=S};e.on("filechooser",b);let g;try{if(g=await Sn(e,t,n,r,s,o,a,i),p||await e.waitForEvent("filechooser",{timeout:r}).then(b,()=>{}),!p)throw new Error(`uploadFile failed: the ${t.action} after it opened no file chooser within ${r} ms`);try{await p.setFiles(l,{timeout:r})}catch(S){throw new Error(`uploadFile failed: ${S instanceof Error?S.message:String(S)}`)}}catch(S){throw wn.set(e,l),S}finally{e.off("filechooser",b)}return g}o.has(le)||o.set(le,Je());let f=o.get(le),d=p=>typeof p=="string"?ft(p,f):Array.isArray(p)?p.map(d):p&&typeof p=="object"?Object.fromEntries(Object.entries(p).map(([b,g])=>[b,d(g)])):p;t=d(t),n=d(n);let m=p=>$t(vt(sr(Be(p,n)))),h=t.value!==void 0?m(t.value):void 0,y=t.url!==void 0?m(t.url):void 0,x=t.action==="expectVisible"||t.action==="expectText"||t.action==="expectValue"||t.action==="expectMatches"||t.action==="expectAttribute"||t.action==="expectEach"||t.action==="captureText",k=t.target?fo(e,t.target,n,x,!po.has(t.action)):[],w={timeout:r},v={timeout:Math.min(1e3,r)},u=p=>async()=>{for(let b of k)try{if(await p(b.first()))return!0}catch{}return!1};switch(t.action){case"goto":{if(!y)throw new Error("goto requires url");await e.goto(or(y,s),w);return}case"click":return we(k,p=>p.click(w),!0);case"fill":return we(k,p=>p.fill(h??"",w),!0);case"press":return k.length>0?we(k,p=>p.press(h??"",w),!0):e.keyboard.press(h??"");case"selectOption":return we(k,async p=>{await p.selectOption(h??"",w)},!0);case"check":return we(k,p=>p.check(w),!0);case"uncheck":return we(k,p=>p.uncheck(w),!0);case"hover":return we(k,p=>p.hover(w),!0);case"waitFor":return k.length>0?we(k,p=>p.waitFor({state:"visible",timeout:r})):wr(Math.min(Number(h??"0"),r));case"expectVisible":return c(u(p=>p.isVisible()),r,"target to be visible");case"expectNotVisible":return c(async()=>{for(let p of k){let b=await p.count();for(let g=0;g<b;g++)if(await p.nth(g).isVisible())return!1}return!0},r,`every match of every recorded locator to be hidden (${k.length} locator(s))`);case"expectText":return c(u(async p=>(await p.textContent(v)??"").includes(h??"")),r,`text "${h}"`);case"expectEach":{let p=t.each?.match(/^\{data:([^}]+)\}$/)?.[1];if(!p)throw new Error(`expectEach needs a {data:...} list reference, got ${JSON.stringify(t.each)}`);let b=Fe(p),g=async T=>await T.isVisible()?new Set(await T.evaluate(A=>{let R=O=>(O??"").replace(/\s+/g," ").trim(),_=O=>typeof O.checkVisibility=="function"?O.checkVisibility({visibilityProperty:!0,contentVisibilityAuto:!0}):!!(O.offsetWidth||O.offsetHeight||O.getClientRects().length),I=O=>{let P=O.ownerDocument.createTreeWalker(O,4),$="";for(let j=P.nextNode();j;j=P.nextNode())j.parentElement&&_(j.parentElement)&&($+=j.nodeValue??"");return $},C=new Set,M=O=>{for(let P of Array.from(O.options))P.hidden||C.add(R(P.textContent))};A.tagName==="SELECT"&&M(A);for(let O of Array.from(A.querySelectorAll("*")))_(O)&&(O.tagName==="SELECT"?M(O):O.tagName!=="OPTION"&&C.add(R(I(O))));C.add(R(I(A)));for(let O of String(A.innerText??"").split(`
23
+ `))C.add(R(O));return[...C]},void 0,v)):new Set,S=b;return c(async()=>{for(let T of k)try{let A=await g(T.first()),R=b.filter(_=>!A.has(_.replace(/\s+/g," ").trim()));if(R.length<S.length&&(S=R),R.length===0)return!0}catch{}return!1},r,`every value of ${t.each} to be shown`).catch(()=>{throw new Error(`${S.map(T=>`"${T}"`).join(", ")} from ${t.each} ${S.length===1?"is":"are"} not shown in the target (${b.length-S.length} of ${b.length} found)`)})}case"expectValue":return c(u(async p=>await p.inputValue(v)===(h??"")),r,`input value "${h}"`);case"expectUrl":{let p=y??h??"";return c(async()=>e.url().includes(p),r,`URL containing "${p}" on the active page (${e.url()})`)}case"handleDialog":{let p=(h??"accept")!=="dismiss",b=t.promptText?m(t.promptText):void 0;e.once("dialog",g=>{p?g.accept(b):g.dismiss()});return}case"uploadFile":{let p=(h??"").split(`
24
+ `).filter(Boolean).map(b=>co.isAbsolute(b)?b:co.resolve(b));if(p.length===0)throw new Error("uploadFile requires a path");if(k.length>0)return we(k,b=>b.setInputFiles(p,w),!0);wn.set(e,p);return}case"dragTo":{if(!t.toTarget)throw new Error("dragTo requires toTarget");let p=fo(e,t.toTarget,n,!1,!1);return we(k,b=>b.dragTo(p[0].first(),w),!0)}case"captureText":{if(!t.saveAs)throw new Error("captureText requires saveAs");return c(u(async p=>{let b=yr(await p.textContent(v)??"");return b===""?!1:(o.set(t.saveAs,b),!0)}),r,`non-empty text to capture as "${t.saveAs}"`)}case"expectDiffers":{if(!t.compareTo)throw new Error("expectDiffers requires compareTo");let p=o.get(t.compareTo);if(p===void 0)throw new Error(`expectDiffers: no captured value named "${t.compareTo}"`);if(k.length>0)return c(u(async S=>yr(await S.textContent(v)??"")!==p),r,`target text to differ from "${t.compareTo}" (${p})`);let b=h??"",g=o.get(b);if(g===void 0)throw new Error(`expectDiffers: no captured value named "${b}"`);if(g===p)throw new Error(`expectDiffers: "${b}" (${g}) equals "${t.compareTo}" (${p})`);return}case"expectAttribute":{if(!t.attribute)throw new Error("expectAttribute requires attribute");if(!t.pattern)throw new Error("expectAttribute requires pattern");let p=new RegExp(m(t.pattern),"i");return c(u(async b=>p.test(await b.getAttribute(t.attribute,v)??"")),r,`attribute ${t.attribute} to match /${t.pattern}/i`)}case"expectMatches":{if(!t.pattern)throw new Error("expectMatches requires pattern");let p=new RegExp(m(t.pattern),"i");return c(u(async b=>p.test(yr(await b.textContent(v)??""))),r,`target text to match /${t.pattern}/i`)}case"switchTab":{let p=Number.parseInt(h??"",10),b=()=>e.context().pages().filter(S=>!S.isClosed());await c(async()=>b().length>p,r,`tab ${h} to exist (${b().length} open: ${b().map(S=>S.url()).join(", ")})`);let g=b()[p];return await g.bringToFront(),g}case"closeTab":{let p=e.context().pages().filter(S=>!S.isClosed()),b=h===void 0?e:p[Number.parseInt(h,10)];if(!b)throw new Error(`tab ${h} does not exist (${p.length} open)`);await b.close();let g=e.context().pages().filter(S=>!S.isClosed());if(g.length===0)throw new Error("closed the last tab; nothing left to drive");return g[g.length-1]}case"newTab":{let p=await e.context().newPage();return y&&await p.goto(or(y,s),w),p}case"waitForResponse":case"expectResponse":{if(!t.urlPattern)throw new Error(`${t.action} requires urlPattern`);let p=new RegExp(m(t.urlPattern)),b=t.pattern?new RegExp(m(t.pattern)):void 0;if(!a){await e.waitForResponse(async S=>{let T={seq:0,url:S.url(),method:S.request().method(),status:S.status()};return lo(T,p,t.method,t.status,void 0)?b?b.test(await S.text().catch(()=>"")):!0:!1},w);return}let g=()=>t.action==="expectResponse"?a.all():a.sincePreviousStep();return c(async()=>g().some(S=>lo(S,p,t.method,t.status,b)),r,Uc(t,t.urlPattern))}default:throw new Error(`Unknown action type: ${t.action}`)}}var xn=(e,t)=>kt(e.actions,t),uo=(e,t)=>at(e.actions,t);function xr(e,t){if(t.steps.length!==e.steps.length)return!1;for(let[n,r]of Object.entries(t.data??{})){let s;try{s=nt(n)}catch{continue}if(s===r)continue;let o=t.steps.some(i=>xn(i,`{data:${n}}`)),a=t.steps.some(i=>uo(i,r));if(!o||a)return!1}return t.steps.every((n,r)=>{let s=e.steps[r];if(n.gherkin!==s.text||n.kind!==s.kind)return!1;let o=n.table,a=s.table;if(!!o!=!!a)return!1;if(o&&a){let i=Object.keys(ne(o)),c=Object.keys(ne(a));if(i.length>0&&c.length>0){if(JSON.stringify(i)!==JSON.stringify(c))return!1;let l=ne(o),f=ne(a);for(let d of i){if(l[d]===f[d])continue;let m=`<${d}>`,h=t.steps.some(x=>xn(x,m)),y=t.steps.some(x=>uo(x,l[d]));if(!h||y)return!1}}else if(JSON.stringify(o)!==JSON.stringify(a))return!1}if(n.docString!==void 0!=(s.docString!==void 0))return!1;if(n.docString!==void 0&&s.docString!==void 0&&n.docString!==s.docString){let i=t.steps.some(l=>xn(l,"<docstring>")),c=t.steps.some(l=>xn(l,n.docString));if(!i||c)return!1}return!0})}function Sr(e,t){let n=e.steps[t],r=n?.resolvedTable??n?.table;return{...e.params,..._e(e.steps),...ne(r),...ze(n?.resolvedDocString??n?.docString)}}async function Ot(e,t,n,r={}){let s=r.actionTimeoutMs??5e3,o=r.retries??1,a=Math.max(10,r.pollIntervalMs??go),i=[],c=r.vars??new Map;r.uniqueSeed&&c.set(le,r.uniqueSeed);let l=()=>Object.fromEntries(c),f=n.steps.some(b=>b.actions.some(g=>(g.action==="waitForResponse"||g.action==="expectResponse")&&g.pattern!==void 0)),d=new br;d.attach(e.context(),f);let m=e,h=new Set(e.context().pages()),y=[],x=b=>y.push(b);e.context().on("page",x);let k=b=>new Promise(g=>{let S=()=>{clearTimeout(T),e.context().off("page",S),g()},T=setTimeout(S,b);e.context().on("page",S)}),w=[],u=!n.steps.some(b=>b.actions.some(g=>g.opensTab))&&n.steps.some(b=>b.actions.some(g=>g.action==="switchTab"||g.action==="closeTab"||g.action==="newTab")),p=()=>{let b=e.context().pages();for(let g of b)h.has(g)||(h.add(g),g.isClosed()||(m=g));if(m.isClosed()){let g=b.filter(S=>!S.isClosed());g.length>0&&(m=g[g.length-1])}};if(!xr(t,n))return d.detach(),{status:"stale",stepResults:i,captured:l()};try{for(let b=0;b<n.steps.length;b++){let g=n.steps[b],S=Sr(t,b);d.markStep();let T=!1,A=0;r.trace&&await e.context().tracing.group(`[saffron:${r.tracePhase??"replay"}:${b+(r.traceStepOffset??0)}] ${g.keyword} ${g.gherkin}`).catch(()=>{});let R=async()=>{r.trace&&await e.context().tracing.groupEnd().catch(()=>{})};try{if(g.kind==="assertion"&&!g.actions.some(_=>pe.has(_.action))){let _=`Assertion step "${g.gherkin}" recorded ${g.actions.length===0?"nothing":"only setup actions"}, so nothing was checked. Re-record the scenario (saffron run --rerecord) so the agent records what must be true.`;i.push({gherkin:g.gherkin,fromStepSet:g.fromStepSet,kind:g.kind,status:"failed",drift:T,error:_});for(let I=b+1;I<n.steps.length;I++)i.push({gherkin:n.steps[I].gherkin,kind:n.steps[I].kind,status:"skipped"});return{status:"failed",failure:{stepIndex:b,actionIndex:0,stepText:g.gherkin,kind:g.kind,error:_},stepResults:i,captured:l()}}for(let _=0;_<g.actions.length;_++){let I=g.actions[_];p(),I.pageFingerprint&&await Ge(m)!==I.pageFingerprint&&(T=!0);let C,M=!1;for(let O=0;O<=o&&!M;O++){O>0&&(A++,await wr(250*O),p());try{let P=O>0?`${ce(I)} (attempt ${O+1})`:ce(I),$=await Qs(m,r.trace,`[saffron:action:${_}] ${P}`,()=>Sn(m,I,S,s,r.baseURL,c,d,a));$&&(m=$,h.add($));let j=I.action==="click"||I.action==="press";if(y.length===0&&I.opensTab?await e.context().waitForEvent("page",{timeout:s}).catch(()=>{}):y.length===0&&j&&u&&await k(150),I.opensTab&&y.length===0)throw new Error("this action opened a new tab when it was recorded, and none opened now; the flow has changed");for(y.length>0&&w.push({stepIndex:b,actionIndex:_});y.length>0;)await y.shift().waitForLoadState("domcontentloaded",{timeout:s}).catch(()=>{});M=!0}catch(P){C=P}}if(!M){let O=C instanceof Error?C.message:String(C);i.push({gherkin:g.gherkin,fromStepSet:g.fromStepSet,kind:g.kind,status:"failed",drift:T,...A?{retries:A}:{},failedActionIndex:_,error:O});for(let $=b+1;$<n.steps.length;$++)i.push({gherkin:n.steps[$].gherkin,kind:n.steps[$].kind,status:"skipped"});let P={stepIndex:b,actionIndex:_,stepText:g.gherkin,kind:g.kind,error:O,pageUrl:m.isClosed()?void 0:m.url()};return await r.onFailure?.(m,P).catch(()=>{}),{status:"failed",failure:P,stepResults:i,captured:l(),tabOpeners:w,activePage:m.isClosed()?void 0:m}}}i.push({gherkin:g.gherkin,fromStepSet:g.fromStepSet,kind:g.kind,status:"passed",drift:T,...A?{retries:A}:{}})}finally{await R()}}return{status:"passed",stepResults:i,captured:l(),tabOpeners:w,activePage:m.isClosed()?void 0:m}}finally{e.context().off("page",x),d.detach()}}import{createHash as zc}from"node:crypto";import qc from"node:path";var vr=e=>zc("sha1").update(JSON.stringify(e)).digest("hex").slice(0,16);function ho(e){return{steps:e.steps.map(t=>({...t,actions:t.actions.map(({pageFingerprint:n,...r})=>r)})),data:e.data??null}}var mo=e=>vr(e.map(t=>[t.kind,t.text,t.table??null,t.docString??null])),yo=e=>e?vr(ho(e)):"none",bo=(e,t)=>vr({contract:ho(e),featureEdits:t??null});function wo(e,t,n,r){return{scenario:mo(e.steps),committed:yo(t),recording:bo(n,r)}}function Ee(e,t,n){let r=t.meta.bound;if(!r)return;let s=[],{feature:o,scenario:a}=t.cache,i;if(n&&!(n instanceof Map))i=n;else try{i=se(qc.join(e,o),e,n).find(l=>l.name===a)}catch(l){s.push(`${o} no longer parses: ${l instanceof Error?l.message.split(`
25
+ `)[0]:String(l)}`)}s.length||(i?mo(i.steps)!==r.scenario&&s.push(Hc(i,t.cache)):s.push(`scenario "${a}" is no longer in ${o}`));let c;try{c=oe(J(e,o,a))}catch(l){if(!(l instanceof V))throw l;return s.push(`the committed recording cannot be read (${l.problem})`),s.join("; ")}return yo(c)!==r.committed&&s.push(r.committed==="none"?"a recording was committed for this scenario after the proposal was filed, so the diff you would review is not the one that was made":c?"the committed recording changed after the proposal was filed (an accepted heal, a propagated locator fix or a pull), so the diff you would review is not the one that was made":"the committed recording the proposal was diffed against is gone"),bo(t.cache,t.meta.featureEdits)!==r.recording&&s.push("the proposal's recording or feature edits were edited after it was filed"),s.length>0?s.join("; "):void 0}function Hc(e,t){let n=e.steps,r=t.steps;if(n.length!==r.length)return`the scenario has ${n.length} steps now and had ${r.length} when the proposal was filed`;let s=n.findIndex((o,a)=>o.text!==r[a].gherkin||o.kind!==r[a].kind);return s<0?"the scenario's steps changed since the proposal was filed":`step ${s+1} now reads "${n[s].text}" and was "${r[s].gherkin}" when the proposal was filed`}var vn="not bound: filed by a runner older than 0.8.5, so changes since cannot be detected";import Ke from"node:fs";import xo from"node:path";function $r(e){return xo.join(e,".saffron","history.jsonl")}function Tr(e,t,n,r){let s=e.map(o=>({feature:o.scenario.featurePath,scenario:o.scenario.displayName,tags:[...o.scenario.tags],status:o.status,source:o.source,durationMs:o.durationMs,verified:o.verified,seededSteps:o.seededSteps,adaptations:o.adaptations.length,driftSteps:o.stepResults.filter(a=>a.drift).length,aiCalls:o.usage.aiCalls,tokensInOut:o.usage.inputTokens+o.usage.outputTokens,cacheRead:o.usage.cacheReadTokens,cacheWrite:o.usage.cacheCreationTokens,costUsd:o.usage.costUsd??0,error:o.error?.split(`
26
+ `)[0]}));return{runId:t.toISOString(),startedAt:t.toISOString(),finishedAt:new Date().toISOString(),baseURL:r,green:s.filter(o=>o.status==="green").length,yellow:s.filter(o=>o.status==="yellow").length,red:s.filter(o=>o.status==="red").length,aiCalls:s.reduce((o,a)=>o+a.aiCalls,0),costUsd:s.reduce((o,a)=>o+a.costUsd,0),divergentSteps:n,scenarios:s}}function Rr(e,t){let n=$r(e);Ke.mkdirSync(xo.dirname(n),{recursive:!0}),Ke.appendFileSync(n,`${JSON.stringify(t)}
27
+ `)}function So(e,t){let n=$r(e);if(!Ke.existsSync(n))return Rr(e,t);let r=Ke.readFileSync(n,"utf8").split(`
28
+ `).filter(a=>a.trim()),s=!1,o=r.map(a=>{try{if(JSON.parse(a).runId===t.runId)return s=!0,JSON.stringify(t)}catch{}return a});s||o.push(JSON.stringify(t)),Ke.writeFileSync(n,`${o.join(`
29
29
  `)}
30
- `)}function Ft(e,t=50){let n=Sr(e);if(!Ge.existsSync(n))return[];let r=Ge.readFileSync(n,"utf8").trim().split(`
31
- `).filter(Boolean),o=[];for(let s of r.slice(-t))try{o.push(JSON.parse(s))}catch{}return o}var vr=e=>{let t=e.green+e.yellow+e.red;return t===0?0:Math.round((e.green+e.yellow)/t*100)};function Tr(e,t){let n=[...e,t],r=n.slice(-20),o=n.slice(-10),s=e.at(-1),a=s?{passRatePp:vr(t)-vr(s),costUsd:t.costUsd-s.costUsd,aiCalls:t.aiCalls-s.aiCalls,red:t.red-s.red}:void 0,i=new Map;for(let d of o)for(let m of d.scenarios){let h=`${m.feature}::${m.scenario}`,y=i.get(h)??{feature:m.feature,scenario:m.scenario,heals:0,reds:0,runs:0};y.runs++,m.source==="agent-heal"&&m.status==="yellow"&&y.heals++,m.status==="red"&&y.reds++,i.set(h,y)}let c=[...i.values()].filter(d=>d.heals>=2||d.reds>=3).sort((d,m)=>m.heals+m.reds-(d.heals+d.reds)),p=new Map;for(let d of r)for(let m of d.scenarios)m.status==="red"&&m.error&&p.set(m.error,(p.get(m.error)??0)+1);let f=[...p.entries()].sort((d,m)=>m[1]-d[1]).slice(0,3).map(([d,m])=>({message:d,count:m}));return{prev:a,series:{passRate:r.map(vr),costUsd:r.map(d=>Number(d.costUsd.toFixed(4))),aiCalls:r.map(d=>d.aiCalls)},chronic:c,divergentSteps:t.divergentSteps,topIssues:f}}var Bc=/^\s*(?:Scenario(?: Outline| Template)?|Example):\s*(.+?)\s*$/,zc=/^\s*StepSet:/;function ht(e,t){return xe.relative(e,t).split(xe.sep).join("/")}function qc(e,t){let n={green:0,yellow:1,red:2};return!e||n[t]>n[e]?t:e}function mo(e,t){return`${e} :: ${t}`}function xn(e,t,n={}){let r=t.features??"features",o=xe.isAbsolute(r)?r:xe.join(e,r),s=xe.join(e,".saffron","reports","latest.json"),a=xe.join(e,".saffron","reports","latest.html"),i;try{i=JSON.parse(de.readFileSync(s,"utf8"))}catch{i=void 0}let c=new Map;for(let g of i?.scenarios??[]){let S=g.scenario.replace(/ \(example \d+\)$/,""),T=mo(g.feature,S);c.set(T,qc(c.get(T),g.status))}let p=new Map,f;try{p=ve(He(o),e)}catch(g){f=g instanceof Error?g.message:String(g)}let d=de.existsSync(o)?[...de.readdirSync(o,{recursive:!0})].map(String).filter(Se).map(g=>xe.join(o,g)).sort():[],m=[],h=new Map,y=new Map,b={parsedFiles:new Set,owned:new Set,unreadable:f?[f]:[]};for(let g of d){let S=ht(e,g),T="";try{T=de.readFileSync(g,"utf8")}catch(_){m.push({path:S,name:S,scenarios:[],stepSets:0,error:String(_)}),b.unreadable.push(`${S}: ${String(_)}`);continue}let A=T.split(`
32
- `),E=new Map,C=0;A.forEach((_,$)=>{let O=_.match(Bc);O&&!E.has(O[1])?E.set(O[1],$+1):zc.test(_)&&C++});let I=A.find(_=>/^\s*Feature:/.test(_)),P=I?I.replace(/^\s*Feature:\s*/,"").trim():S,j;try{j=se(g,e,p)}catch(_){m.push({path:S,name:P,scenarios:[],stepSets:C,error:f??(_ instanceof Error?_.message:String(_))}),b.unreadable.push(`${S}: ${_ instanceof Error?_.message:String(_)}`);continue}b.parsedFiles.add(g),y.set(g,j);let L=new Map;for(let _ of j){b.owned.add(me(e,_.featurePath,_.displayName));let $=L.get(_.name);if($){$.rows+=1;continue}let O=J(e,_.featurePath,_.name);b.owned.add(O),b.owned.add(ue(e,_.featurePath,_.name));let R="missing",F;if(de.existsSync(O))try{let z=oe(O);R=z&&br(_,z)?"valid":"stale"}catch(z){R="invalid",F=z instanceof W?z.problem:z instanceof Error?z.message:String(z)}L.set(_.name,{name:_.name,line:E.get(_.name),tags:[..._.tags],outline:Object.keys(_.params).length>0,rows:1,cached:R!=="missing",cacheState:R,...F?{cacheProblem:F}:{},proposal:de.existsSync(ue(e,_.featurePath,_.name)),lastStatus:c.get(mo(_.featurePath,_.name))});for(let z of _.tags)h.set(z,(h.get(z)??0)+1)}m.push({path:S,name:P,scenarios:[...L.values()],stepSets:C})}let k=[],w=Me(e,(g,S)=>k.push({file:ht(e,g),problem:S})).map(({file:g,proposal:S,revision:T})=>({file:ht(e,g),feature:S.cache.feature,scenario:S.cache.scenario,mode:S.meta.mode,createdAt:S.meta.createdAt,verified:S.meta.verified,proofError:S.meta.proofError,stale:S.meta.bound?Re(e,S,p):void 0,unbound:S.meta.bound?void 0:!0,revision:T,adaptations:S.meta.adaptations??[],narrative:S.meta.narrative,suggestedFeatureEdit:S.meta.suggestedFeatureEdit,recordedFor:S.meta.recordedFor,aiCalls:S.meta.usage?.aiCalls??0,costUsd:S.meta.usage?.costUsd})).sort((g,S)=>g.file.localeCompare(S.file)),v;try{v=be(e,r,p,y)}catch{v={steps:[],stepSets:[],scannedFiles:0,scannedCaches:0}}let l=Vc(e),u=Ft(e,n.historyLimit??30).map(g=>({startedAt:g.startedAt,green:g.green,yellow:g.yellow,red:g.red,aiCalls:g.aiCalls,costUsd:g.costUsd})),x=xe.join(e,"saffron.config.json");return{tool:"saffron",version:Fe(),projectRoot:e,packageInstalled:de.existsSync(xe.join(e,"node_modules","saffron-ai")),config:{file:de.existsSync(x)?"saffron.config.json":void 0,effective:Es(t)},features:m,tags:[...h.entries()].map(([g,S])=>({tag:g,scenarios:S})).sort((g,S)=>g.tag.localeCompare(S.tag)),proposals:w,unreadableProposals:k,lastRun:i?{startedAt:i.startedAt,finishedAt:i.finishedAt,totals:i.totals,reportHtml:de.existsSync(a)?ht(e,a):void 0,attention:i.scenarios.filter(g=>g.status!=="green").map(g=>({feature:g.feature,scenario:g.scenario,baseScenario:g.scenario.replace(/ \(example \d+\)$/,""),status:g.status,error:g.status==="red"?g.error?.split(`
33
- `)[0]:void 0,failedStep:(g.steps??[]).find(S=>S.status==="failed")?.gherkin??g.evidence?.find(S=>S.step)?.step,evidence:(g.evidence??[]).filter(S=>S.kind==="trace"?de.existsSync(xe.join(e,S.file)):dt(e,S)!==void 0).map(S=>({kind:S.kind,file:S.file,...S.step?{step:S.step}:{},...S.sha256?{sha256:S.sha256}:{}}))})).sort((g,S)=>g.status===S.status?0:g.status==="red"?-1:1)}:void 0,history:u,vocabulary:{steps:v.steps.length,recorded:v.steps.filter(g=>g.status==="recorded").length,unrecorded:v.steps.filter(g=>g.status==="unrecorded").length,stepSets:v.stepSets.length,divergent:v.steps.filter(g=>g.status==="divergent").map(g=>g.text),duplicateWordings:l.slice(0,50)},data:(()=>{let g=je(),S=[],T=new Set;for(let[A,E]of y)for(let C of E)for(let I of qe(X(C)))if(!T.has(I)){T.add(I);try{Jt(I)}catch(P){S.push({ref:I,reason:P instanceof Error?P.message:String(P),feature:ht(e,A),scenario:C.name})}}return{dir:ht(e,g.dir),env:g.env,files:de.existsSync(g.dir)?de.readdirSync(g.dir).filter(A=>/\.(json|csv)$/.test(A)).sort():[],unresolved:S}})(),orphans:(()=>{let g=yn(e,r,b);return g.unreadable.length?[]:g.orphans})()}}function bo(e){let t=e.features.reduce((s,a)=>s+a.scenarios.length,0),n=e.features.reduce((s,a)=>s+a.scenarios.filter(i=>i.cacheState==="valid").length,0),r=e.features.reduce((s,a)=>s+a.scenarios.filter(i=>i.cacheState==="stale"||i.cacheState==="invalid").length,0);console.log(`saffron ${e.version} \xB7 ${e.features.length} feature file(s) \xB7 ${t} scenario(s), ${n} replay from cache`+(r?`, ${r} stale (\u26A0) and will re-record`:""));for(let s of e.features){let a=s.scenarios.map(i=>i.proposal?"\u25D0":i.cacheState==="valid"?"\u25CF":i.cacheState==="missing"?"\u25CB":"\u26A0").join("");console.log(` ${s.path} ${a}${s.error?` \u2717 ${s.error.split(`
34
- `)[0]}`:""}`)}if(e.tags.length&&console.log(`tags: ${e.tags.map(s=>`${s.tag} (${s.scenarios})`).join(", ")}`),console.log(e.proposals.length?`${e.proposals.length} proposal(s) pending review: ${e.proposals.map(s=>s.file).join(", ")}`:"no proposals pending review",...e.unreadableProposals.map(s=>`\u26A0 proposal ${s.file} cannot be read: ${s.problem}`),...e.features.flatMap(s=>s.scenarios.filter(a=>a.cacheProblem).map(a=>`\u26A0 recording of ${s.path} \u203A ${a.name} cannot be read: ${a.cacheProblem}`))),e.lastRun){let s=e.lastRun.totals;console.log(`last run ${e.lastRun.finishedAt}: ${s.green} passed \xB7 ${s.yellow} pending review \xB7 ${s.red} failed \xB7 $${s.costUsd.toFixed(2)}`)}let o=e.vocabulary;console.log(`vocabulary: ${o.steps} steps (${o.recorded} recorded, ${o.unrecorded} unrecorded), ${o.stepSets} step sets, ${o.divergent.length} divergent, ${o.duplicateWordings.length} duplicate wording group(s)`),e.orphans.length&&console.log(`${e.orphans.length} recording(s) belong to no scenario: run \`saffron prune\` to see them`),(e.data.files.length||e.data.unresolved.length)&&console.log(`test data: ${e.data.files.length} file(s) in ${e.data.dir}/${e.data.env?` \xB7 environment "${e.data.env}"`:""}`+(e.data.unresolved.length?` \xB7 ${e.data.unresolved.length} reference(s) do not resolve: ${e.data.unresolved.map(s=>`{data:${s.ref}}`).join(", ")}`:"")),console.log(`config: ${e.config.file??"(defaults, no saffron.config.json)"}`)}var Hc=/^(<[^>]+>|\{env:[^}]+\}|\{date[^}]*\})$/;function yo(e){return e?e.role?`${e.role}${e.name?` "${e.name}"`:e.nameRegex?` /${e.nameRegex}/`:""}`:e.label?`label "${e.label}"`:e.text?`text "${e.text}"`:e.testId?`testid ${e.testId}`:e.placeholder?`placeholder "${e.placeholder}"`:e.fallbackSelectors?.length?e.fallbackSelectors[0]:"":""}function Vc(e){let t;try{t=ye(e)}catch{return[]}let n=new Map;for(let r of t.steps.values()){if(r.actions.length===0)continue;let o=tn(r.gherkin).pattern,s=[...r.gherkin.matchAll(/"([^"]*)"/g)].map(d=>d[1]),a=d=>d!==void 0&&(s.includes(d)||Hc.test(d))?"\xABarg\xBB":d,i=r.actions.map(d=>{let{pageFingerprint:m,value:h,promptText:y,...b}=d;return{...b,value:a(h),promptText:a(y)}}),c=JSON.stringify(i),p=r.actions.map(d=>`${d.action}${yo(d.target)?` ${yo(d.target)}`:d.url?` ${d.url}`:""}`).join(" \xB7 "),f=n.get(c)??{steps:new Map,actions:p};f.steps.has(o)||f.steps.set(o,r.gherkin),n.set(c,f)}return[...n.values()].filter(r=>r.steps.size>1).map(r=>({steps:[...r.steps.values()].sort(),actions:r.actions})).sort((r,o)=>o.steps.length-r.steps.length||r.steps[0].localeCompare(o.steps[0]))}async function wo(e,t){let n=new Wc({name:"saffron",version:Fe()},{capabilities:{tools:{}}});n.setRequestHandler(Kc,async()=>({tools:[{name:"search_steps",description:"Search the project's step vocabulary. Returns known Gherkin steps with status (recorded = replays at zero tokens, divergent, unrecorded), usage counts, and source files. ALWAYS reuse an existing wording exactly instead of inventing a near-duplicate: exact step text is the replay-cache identity, so a new wording costs a fresh AI recording.",inputSchema:{type:"object",properties:{query:{type:"string",description:"Case-insensitive substring filter over step text; omit to list everything."}}}},{name:"list_step_sets",description:"List the project's StepSet definitions (named reusable step sequences from .saffron files) with their steps, defining file, and usage. Invoke one inside a scenario with a line reading exactly: StepSet <name>",inputSchema:{type:"object",properties:{}}},{name:"project_status",description:"The project overview (same data as `saffron status --json`): every feature file with its scenarios (header line, tags, outline rows, cached / proposal pending / last run status), the tag list, pending proposals with narrative and proof-replay verdict, the last run's totals, run history, vocabulary health (divergent steps, duplicate wordings proven by identical recordings) and the effective config. Call it before editing tests to see what is already recorded and what awaits review.",inputSchema:{type:"object",properties:{}}}]})),n.setRequestHandler(Gc,async r=>Yc(r.params.name,r.params.arguments,e,t)),await n.connect(new Jc)}function Yc(e,t,n,r){let o=s=>({content:[{type:"text",text:JSON.stringify(s,null,2)}]});if(e==="search_steps"){let s=be(n,r),a=typeof t?.query=="string"?t.query:void 0,i=a?nn(s,a):s;return o({steps:i.steps,scannedFiles:i.scannedFiles,scannedCaches:i.scannedCaches})}if(e==="list_step_sets"){let s=be(n,r);return o({stepSets:s.stepSets})}return e==="project_status"?o(xn(n,ae(n))):{content:[{type:"text",text:`Unknown tool: ${e}`}],isError:!0}}import{query as Qc}from"@anthropic-ai/claude-agent-sdk";function Zc(e,t){let n=t.steps.map(o=>`- [${o.status}] ${o.keyword} ${o.text}`).join(`
35
- `),r=t.stepSets.map(o=>`- StepSet ${o.name}
36
- ${o.steps.map(s=>` ${s}`).join(`
30
+ `)}function Ft(e,t=50){let n=$r(e);if(!Ke.existsSync(n))return[];let r=Ke.readFileSync(n,"utf8").trim().split(`
31
+ `).filter(Boolean),s=[];for(let o of r.slice(-t))try{s.push(JSON.parse(o))}catch{}return s}var kr=e=>{let t=e.green+e.yellow+e.red;return t===0?0:Math.round((e.green+e.yellow)/t*100)};function Er(e,t){let n=[...e,t],r=n.slice(-20),s=n.slice(-10),o=e.at(-1),a=o?{passRatePp:kr(t)-kr(o),costUsd:t.costUsd-o.costUsd,aiCalls:t.aiCalls-o.aiCalls,red:t.red-o.red}:void 0,i=new Map;for(let d of s)for(let m of d.scenarios){let h=`${m.feature}::${m.scenario}`,y=i.get(h)??{feature:m.feature,scenario:m.scenario,heals:0,reds:0,runs:0};y.runs++,m.source==="agent-heal"&&m.status==="yellow"&&y.heals++,m.status==="red"&&y.reds++,i.set(h,y)}let c=[...i.values()].filter(d=>d.heals>=2||d.reds>=3).sort((d,m)=>m.heals+m.reds-(d.heals+d.reds)),l=new Map;for(let d of r)for(let m of d.scenarios)m.status==="red"&&m.error&&l.set(m.error,(l.get(m.error)??0)+1);let f=[...l.entries()].sort((d,m)=>m[1]-d[1]).slice(0,3).map(([d,m])=>({message:d,count:m}));return{prev:a,series:{passRate:r.map(kr),costUsd:r.map(d=>Number(d.costUsd.toFixed(4))),aiCalls:r.map(d=>d.aiCalls)},chronic:c,divergentSteps:t.divergentSteps,topIssues:f}}var Wc=/^\s*(?:Scenario(?: Outline| Template)?|Example):\s*(.+?)\s*$/,Vc=/^\s*StepSet:/;function ht(e,t){return xe.relative(e,t).split(xe.sep).join("/")}function Jc(e,t){let n={green:0,yellow:1,red:2};return!e||n[t]>n[e]?t:e}function vo(e,t){return`${e} :: ${t}`}function kn(e,t,n={}){let r=t.features??"features",s=xe.isAbsolute(r)?r:xe.join(e,r),o=xe.join(e,".saffron","reports","latest.json"),a=xe.join(e,".saffron","reports","latest.html"),i;try{i=JSON.parse(de.readFileSync(o,"utf8"))}catch{i=void 0}let c=new Map;for(let g of i?.scenarios??[]){let S=g.scenario.replace(/ \(example \d+\)$/,""),T=vo(g.feature,S);c.set(T,Jc(c.get(T),g.status))}let l=new Map,f;try{l=ve(We(s),e)}catch(g){f=g instanceof Error?g.message:String(g)}let d=de.existsSync(s)?[...de.readdirSync(s,{recursive:!0})].map(String).filter(ke).map(g=>xe.join(s,g)).sort():[],m=[],h=new Map,y=new Map,x={parsedFiles:new Set,owned:new Set,unreadable:f?[f]:[]};for(let g of d){let S=ht(e,g),T="";try{T=de.readFileSync(g,"utf8")}catch(P){m.push({path:S,name:S,scenarios:[],stepSets:0,error:String(P)}),x.unreadable.push(`${S}: ${String(P)}`);continue}let A=T.split(`
32
+ `),R=new Map,_=0;A.forEach((P,$)=>{let j=P.match(Wc);j&&!R.has(j[1])?R.set(j[1],$+1):Vc.test(P)&&_++});let I=A.find(P=>/^\s*Feature:/.test(P)),C=I?I.replace(/^\s*Feature:\s*/,"").trim():S,M;try{M=se(g,e,l)}catch(P){m.push({path:S,name:C,scenarios:[],stepSets:_,error:f??(P instanceof Error?P.message:String(P))}),x.unreadable.push(`${S}: ${P instanceof Error?P.message:String(P)}`);continue}x.parsedFiles.add(g),y.set(g,M);let O=new Map;for(let P of M){x.owned.add(me(e,P.featurePath,P.displayName));let $=O.get(P.name);if($){$.rows+=1;continue}let j=J(e,P.featurePath,P.name);x.owned.add(j),x.owned.add(ue(e,P.featurePath,P.name));let E="missing",F;if(de.existsSync(j))try{let z=oe(j);E=z&&xr(P,z)?"valid":"stale"}catch(z){E="invalid",F=z instanceof V?z.problem:z instanceof Error?z.message:String(z)}O.set(P.name,{name:P.name,line:R.get(P.name),tags:[...P.tags],outline:Object.keys(P.params).length>0,rows:1,cached:E!=="missing",cacheState:E,...F?{cacheProblem:F}:{},proposal:de.existsSync(ue(e,P.featurePath,P.name)),lastStatus:c.get(vo(P.featurePath,P.name))});for(let z of P.tags)h.set(z,(h.get(z)??0)+1)}m.push({path:S,name:C,scenarios:[...O.values()],stepSets:_})}let k=[],w=De(e,(g,S)=>k.push({file:ht(e,g),problem:S})).map(({file:g,proposal:S,revision:T})=>({file:ht(e,g),feature:S.cache.feature,scenario:S.cache.scenario,mode:S.meta.mode,createdAt:S.meta.createdAt,verified:S.meta.verified,proofError:S.meta.proofError,stale:S.meta.bound?Ee(e,S,l):void 0,unbound:S.meta.bound?void 0:!0,revision:T,adaptations:S.meta.adaptations??[],narrative:S.meta.narrative,suggestedFeatureEdit:S.meta.suggestedFeatureEdit,recordedFor:S.meta.recordedFor,aiCalls:S.meta.usage?.aiCalls??0,costUsd:S.meta.usage?.costUsd})).sort((g,S)=>g.file.localeCompare(S.file)),v;try{v=be(e,r,l,y)}catch{v={steps:[],stepSets:[],scannedFiles:0,scannedCaches:0}}let u=Kc(e),p=Ft(e,n.historyLimit??30).map(g=>({startedAt:g.startedAt,green:g.green,yellow:g.yellow,red:g.red,aiCalls:g.aiCalls,costUsd:g.costUsd})),b=xe.join(e,"saffron.config.json");return{tool:"saffron",version:Ne(),projectRoot:e,packageInstalled:de.existsSync(xe.join(e,"node_modules","saffron-ai")),config:{file:de.existsSync(b)?"saffron.config.json":void 0,effective:_s(t)},features:m,tags:[...h.entries()].map(([g,S])=>({tag:g,scenarios:S})).sort((g,S)=>g.tag.localeCompare(S.tag)),proposals:w,unreadableProposals:k,lastRun:i?{startedAt:i.startedAt,finishedAt:i.finishedAt,totals:i.totals,reportHtml:de.existsSync(a)?ht(e,a):void 0,attention:i.scenarios.filter(g=>g.status!=="green").map(g=>({feature:g.feature,scenario:g.scenario,baseScenario:g.scenario.replace(/ \(example \d+\)$/,""),status:g.status,error:g.status==="red"?g.error?.split(`
33
+ `)[0]:void 0,failedStep:(g.steps??[]).find(S=>S.status==="failed")?.gherkin??g.evidence?.find(S=>S.step)?.step,evidence:(g.evidence??[]).filter(S=>S.kind==="trace"?de.existsSync(xe.join(e,S.file)):dt(e,S)!==void 0).map(S=>({kind:S.kind,file:S.file,...S.step?{step:S.step}:{},...S.sha256?{sha256:S.sha256}:{}}))})).sort((g,S)=>g.status===S.status?0:g.status==="red"?-1:1)}:void 0,history:p,vocabulary:{steps:v.steps.length,recorded:v.steps.filter(g=>g.status==="recorded").length,unrecorded:v.steps.filter(g=>g.status==="unrecorded").length,stepSets:v.stepSets.length,divergent:v.steps.filter(g=>g.status==="divergent").map(g=>g.text),duplicateWordings:u.slice(0,50)},data:(()=>{let g=Oe(),S=[],T=new Set;for(let[A,R]of y)for(let _ of R)for(let I of He(X(_)))if(!T.has(I)){T.add(I);try{Gt(I)}catch(C){S.push({ref:I,reason:C instanceof Error?C.message:String(C),feature:ht(e,A),scenario:_.name})}}return{dir:ht(e,g.dir),env:g.env,files:de.existsSync(g.dir)?de.readdirSync(g.dir).filter(A=>/\.(json|csv)$/.test(A)).sort():[],unresolved:S}})(),orphans:(()=>{let g=bn(e,r,x);return g.unreadable.length?[]:g.orphans})()}}function $o(e){let t=e.features.reduce((o,a)=>o+a.scenarios.length,0),n=e.features.reduce((o,a)=>o+a.scenarios.filter(i=>i.cacheState==="valid").length,0),r=e.features.reduce((o,a)=>o+a.scenarios.filter(i=>i.cacheState==="stale"||i.cacheState==="invalid").length,0);console.log(`saffron ${e.version} \xB7 ${e.features.length} feature file(s) \xB7 ${t} scenario(s), ${n} replay from cache`+(r?`, ${r} stale (\u26A0) and will re-record`:""));for(let o of e.features){let a=o.scenarios.map(i=>i.proposal?"\u25D0":i.cacheState==="valid"?"\u25CF":i.cacheState==="missing"?"\u25CB":"\u26A0").join("");console.log(` ${o.path} ${a}${o.error?` \u2717 ${o.error.split(`
34
+ `)[0]}`:""}`)}if(e.tags.length&&console.log(`tags: ${e.tags.map(o=>`${o.tag} (${o.scenarios})`).join(", ")}`),console.log(e.proposals.length?`${e.proposals.length} proposal(s) pending review: ${e.proposals.map(o=>o.file).join(", ")}`:"no proposals pending review",...e.unreadableProposals.map(o=>`\u26A0 proposal ${o.file} cannot be read: ${o.problem}`),...e.features.flatMap(o=>o.scenarios.filter(a=>a.cacheProblem).map(a=>`\u26A0 recording of ${o.path} \u203A ${a.name} cannot be read: ${a.cacheProblem}`))),e.lastRun){let o=e.lastRun.totals;console.log(`last run ${e.lastRun.finishedAt}: ${o.green} passed \xB7 ${o.yellow} pending review \xB7 ${o.red} failed \xB7 $${o.costUsd.toFixed(2)}`)}let s=e.vocabulary;console.log(`vocabulary: ${s.steps} steps (${s.recorded} recorded, ${s.unrecorded} unrecorded), ${s.stepSets} step sets, ${s.divergent.length} divergent, ${s.duplicateWordings.length} duplicate wording group(s)`),e.orphans.length&&console.log(`${e.orphans.length} recording(s) belong to no scenario: run \`saffron prune\` to see them`),(e.data.files.length||e.data.unresolved.length)&&console.log(`test data: ${e.data.files.length} file(s) in ${e.data.dir}/${e.data.env?` \xB7 environment "${e.data.env}"`:""}`+(e.data.unresolved.length?` \xB7 ${e.data.unresolved.length} reference(s) do not resolve: ${e.data.unresolved.map(o=>`{data:${o.ref}}`).join(", ")}`:"")),console.log(`config: ${e.config.file??"(defaults, no saffron.config.json)"}`)}var Gc=/^(<[^>]+>|\{env:[^}]+\}|\{date[^}]*\})$/;function ko(e){return e?e.role?`${e.role}${e.name?` "${e.name}"`:e.nameRegex?` /${e.nameRegex}/`:""}`:e.label?`label "${e.label}"`:e.text?`text "${e.text}"`:e.testId?`testid ${e.testId}`:e.placeholder?`placeholder "${e.placeholder}"`:e.fallbackSelectors?.length?e.fallbackSelectors[0]:"":""}function Kc(e){let t;try{t=ye(e)}catch{return[]}let n=new Map;for(let r of t.steps.values()){if(r.actions.length===0)continue;let s=nn(r.gherkin).pattern,o=[...r.gherkin.matchAll(/"([^"]*)"/g)].map(d=>d[1]),a=d=>d!==void 0&&(o.includes(d)||Gc.test(d))?"\xABarg\xBB":d,i=r.actions.map(d=>{let{pageFingerprint:m,value:h,promptText:y,...x}=d;return{...x,value:a(h),promptText:a(y)}}),c=JSON.stringify(i),l=r.actions.map(d=>`${d.action}${ko(d.target)?` ${ko(d.target)}`:d.url?` ${d.url}`:""}`).join(" \xB7 "),f=n.get(c)??{steps:new Map,actions:l};f.steps.has(s)||f.steps.set(s,r.gherkin),n.set(c,f)}return[...n.values()].filter(r=>r.steps.size>1).map(r=>({steps:[...r.steps.values()].sort(),actions:r.actions})).sort((r,s)=>s.steps.length-r.steps.length||r.steps[0].localeCompare(s.steps[0]))}async function To(e,t){let n=new Yc({name:"saffron",version:Ne()},{capabilities:{tools:{}}});n.setRequestHandler(Xc,async()=>({tools:[{name:"search_steps",description:"Search the project's step vocabulary. Returns known Gherkin steps with status (recorded = replays at zero tokens, divergent, unrecorded), usage counts, and source files. ALWAYS reuse an existing wording exactly instead of inventing a near-duplicate: exact step text is the replay-cache identity, so a new wording costs a fresh AI recording.",inputSchema:{type:"object",properties:{query:{type:"string",description:"Case-insensitive substring filter over step text; omit to list everything."}}}},{name:"list_step_sets",description:"List the project's StepSet definitions (named reusable step sequences from .saffron files) with their steps, defining file, and usage. Invoke one inside a scenario with a line reading exactly: StepSet <name>",inputSchema:{type:"object",properties:{}}},{name:"project_status",description:"The project overview (same data as `saffron status --json`): every feature file with its scenarios (header line, tags, outline rows, cached / proposal pending / last run status), the tag list, pending proposals with narrative and proof-replay verdict, the last run's totals, run history, vocabulary health (divergent steps, duplicate wordings proven by identical recordings) and the effective config. Call it before editing tests to see what is already recorded and what awaits review.",inputSchema:{type:"object",properties:{}}}]})),n.setRequestHandler(Zc,async r=>el(r.params.name,r.params.arguments,e,t)),await n.connect(new Qc)}function el(e,t,n,r){let s=o=>({content:[{type:"text",text:JSON.stringify(o,null,2)}]});if(e==="search_steps"){let o=be(n,r),a=typeof t?.query=="string"?t.query:void 0,i=a?rn(o,a):o;return s({steps:i.steps,scannedFiles:i.scannedFiles,scannedCaches:i.scannedCaches})}if(e==="list_step_sets"){let o=be(n,r);return s({stepSets:o.stepSets})}return e==="project_status"?s(kn(n,ae(n))):{content:[{type:"text",text:`Unknown tool: ${e}`}],isError:!0}}import{query as tl}from"@anthropic-ai/claude-agent-sdk";function nl(e,t){let n=t.steps.map(s=>`- [${s.status}] ${s.keyword} ${s.text}`).join(`
35
+ `),r=t.stepSets.map(s=>`- StepSet ${s.name}
36
+ ${s.steps.map(o=>` ${o}`).join(`
37
37
  `)}`).join(`
38
38
  `);return["Draft a Saffron feature file from the requirements below.","","EXISTING STEP VOCABULARY \u2014 reuse these wordings EXACTLY (character for character) wherever they express what a step needs to do. Steps marked [recorded] or [divergent] already have replay recordings: reusing them makes the new file replay almost for free. Only invent a new wording when nothing in the vocabulary expresses the intent, and then follow the same style:",n.length>0?n:"(none yet \u2014 this is the project's first file)","",t.stepSets.length>0?`EXISTING STEP SETS \u2014 invoke with a line reading exactly \`StepSet <name>\` instead of repeating their steps:
39
- ${r}`:void 0,"","RULES:","1. Output ONLY the feature file content \u2014 no prose, no code fences, no explanations.","2. Gherkin/.saffron syntax: Feature:, Scenario:, Scenario Outline: + Examples, Given/When/Then/And/But. Prefer concrete scenarios; use an Outline only when the requirements list variations of the same flow.","3. Then steps are assertions \u2014 state exactly what must be true, one condition per step. End every scenario with at least one Then.","4. If the same 3+ step sequence would appear in two or more scenarios, define it once as a StepSet (first step a guard, last step an exit assertion) and invoke it. Definition uses a colon (StepSet: Name), invocation does not (StepSet Name).",'5. Use 2-column data tables for key/value input, header tables for lists of records, and """ doc strings for free text.',"6. Steps are natural language for an AI browser agent \u2014 never CSS selectors or code.","","REQUIREMENTS:",e.trim()].filter(o=>o!==void 0).join(`
40
- `)}async function xo(e,t,n){let r={aiCalls:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreationTokens:0,costUsd:0},o="",s=Qc({prompt:Zc(e,t),options:{model:n,maxTurns:1,permissionMode:"bypassPermissions",persistSession:!1,settingSources:[],allowedTools:[],systemPrompt:"You are Saffron's feature-file author. You write clean, assertive Gherkin that reuses the project's exact step vocabulary. You output only file content."}});for await(let a of s)if(a.type==="assistant")for(let i of a.message.content)i.type==="text"&&(o+=i.text);else a.type==="result"&&(r.aiCalls=a.num_turns,r.costUsd=a.total_cost_usd,"usage"in a&&a.usage&&(r.inputTokens=a.usage.input_tokens??0,r.outputTokens=a.usage.output_tokens??0,r.cacheReadTokens=a.usage.cache_read_input_tokens??0,r.cacheCreationTokens=a.usage.cache_creation_input_tokens??0));return o=o.replace(/^\s*```(?:gherkin|saffron)?\s*\n/,"").replace(/\n```\s*$/,"").trim(),{content:o+`
41
- `,usage:r}}import Ae from"node:fs";import{fileURLToPath as vo}from"node:url";import Ee from"node:path";import{createConnection as Xc,ProposedFeatures as el,TextDocuments as tl,TextDocumentSyncKind as nl,CompletionItemKind as De,InsertTextFormat as So,DiagnosticSeverity as Nt,MarkupKind as ko}from"vscode-languageserver/node";import{TextDocument as rl}from"vscode-languageserver-textdocument";var $o=/^(\s*)(Given|When|Then|And|But|\*)\s+(.+?)\s*$/,sl=/^\s*StepSet:\s*(.+?)\s*$/,Rr=/^(\s*)(StepSet)\s+([^\s:].*?)\s*$/,Er={recorded:"\u25CF recorded \u2014 replays at zero tokens",divergent:"\u25CF divergent \u2014 same text, different recordings",unrecorded:"\u25CB written, not recorded yet"};function ol(e,t){let n=a=>new Set(a.toLowerCase().replace(/"[^"]*"/g,'"x"').split(/[^a-z0-9"<>]+/).filter(Boolean)),r=n(e),o=n(t);if(r.size===0||o.size===0)return 0;let s=0;for(let a of r)o.has(a)&&s++;return s/(r.size+o.size-s)}function al(e){try{return JSON.parse(Ae.readFileSync(Ee.join(e,"saffron.config.json"),"utf8"))}catch{return{}}}function il(e){try{let t=JSON.parse(Ae.readFileSync(Ee.join(e,"saffron.config.json"),"utf8"));return typeof t.features=="string"?t.features:void 0}catch{return}}function Ao(e,t,n={}){let r=e,o=t,s=Xc(el.all,process.stdin,process.stdout),a=new tl(rl),i={steps:[],stepSets:[],scannedFiles:0,scannedCaches:0},c=new Map,p,f=()=>{try{let y=Ee.isAbsolute(o)?o:Ee.join(r,o),b=al(r);tt(r,b.dataDir,process.env.SAFFRON_ENV??b.env),c=ve(He(y),r),i=be(r,o,c)}catch(y){s.console.warn(`saffron vocabulary rebuild: ${String(y)}`)}for(let y of a.all())h(y)},d=()=>{clearTimeout(p),p=setTimeout(f,300)};function m(y){return i.steps.find(b=>b.text===y)?.status}function h(y){let b=y.getText().split(`
42
- `),k=dl(b);if(!y.uri.endsWith(".saffron")){s.sendDiagnostics({uri:y.uri,diagnostics:k});return}let w=new Map;for(let v=0;v<b.length;v++){let l=b[v],u={start:{line:v,character:0},end:{line:v,character:l.length}},x=l.match(Rr);x&&!c.has(x[3])&&k.push({range:u,severity:Nt.Error,source:"saffron",message:`StepSet "${x[3]}" is not defined anywhere in the project.`});let g=l.match(sl);if(g){let A=c.get(g[1]),E=Ee.relative(r,vo(y.uri)).replace(/\\/g,"/");(w.has(g[1])||A&&A.file!==E)&&k.push({range:u,severity:Nt.Error,source:"saffron",message:`StepSet "${g[1]}" is defined twice \u2014 set names are project-wide. If this line was meant to INVOKE the set, remove the colon: "StepSet ${g[1]}".`}),w.set(g[1],v)}let S=l.trim();if(S&&!x&&!g){for(let[A]of c)if(S.toLowerCase()===A.toLowerCase()){k.push({range:u,severity:Nt.Warning,source:"saffron",message:`Did you mean "StepSet ${A}"? Invoking a step set needs the StepSet keyword, like a step needs Given/When/Then.`});break}}let T=l.match($o);if(T&&m(T[3])!=="recorded"){for(let A of i.steps)if(!(A.text===T[3]||A.status==="unrecorded")&&ol(A.text,T[3])>=.7){k.push({range:{start:{line:v,character:l.indexOf(T[3])},end:{line:v,character:l.length}},severity:Nt.Information,source:"saffron",message:`Similar to the ${A.status} step "${A.text}" (\xD7${A.usage}). Reusing the exact wording replays at zero tokens; a new wording costs a fresh AI recording.`});break}}}s.sendDiagnostics({uri:y.uri,diagnostics:k})}s.onInitialize(y=>{if(n.rootFromClient){let b=y.workspaceFolders?.[0]?.uri??y.rootUri??void 0;if(b&&b.startsWith("file:")){let k=vo(b);Ae.existsSync(k)&&(r=k,o=il(k)??t)}}return f(),{capabilities:{textDocumentSync:nl.Incremental,completionProvider:{triggerCharacters:[" ",":","."]},definitionProvider:!0,hoverProvider:!0}}}),s.onCompletion(y=>{let b=a.get(y.textDocument.uri);if(!b)return[];let k=b.getText({start:{line:y.position.line,character:0},end:y.position}),w=pl(k);return w||fl(b.uri,k,y.position.line,i)}),s.onDefinition(y=>{let b=a.get(y.textDocument.uri);if(!b)return;let k=b.getText({start:{line:y.position.line,character:0},end:{line:y.position.line+1,character:0}}).replace(/\n$/,""),w=To(k,y.position.character);if(w){let g=ll(w.ref);return g?{uri:`file://${g.file}`,range:{start:{line:g.line,character:0},end:{line:g.line,character:0}}}:void 0}let v=Ro(k,y.position.character);if(v){let g=Eo(r,v.name);return g?{uri:`file://${g.file}`,range:{start:{line:g.line,character:0},end:{line:g.line,character:0}}}:void 0}let l=k.match(Rr);if(!l)return;let u=c.get(l[3]);return u?{uri:`file://${Ee.join(r,u.file)}`,range:{start:{line:u.line-1,character:0},end:{line:u.line-1,character:0}}}:void 0}),s.onHover(y=>{let b=a.get(y.textDocument.uri);if(!b)return;{let l=b.getText({start:{line:y.position.line,character:0},end:{line:y.position.line+1,character:0}}).replace(/\n$/,""),u=To(l,y.position.character);if(u)return{contents:{kind:"markdown",value:Io(u.ref)}};let x=Ro(l,y.position.character);if(x){let g=Eo(r,x.name),S=g?`defined in ${Ee.relative(r,g.file)} (line ${g.line+1}); the value is resolved at replay and never shown here`:"not found in .env or .env.example; export it in the shell or add it to a git-ignored .env";return{contents:{kind:"markdown",value:`**{env:${x.name}}** ${S}.`}}}}let k=b.getText({start:{line:y.position.line,character:0},end:{line:y.position.line+1,character:0}}).replace(/\n$/,""),w=k.match(Rr);if(w){let l=i.stepSets.find(x=>x.name===w[3]);if(!l)return;let u=l.steps.map(x=>`- ${Er[m(x)??"unrecorded"][0]} ${x}`).join(`
43
- `);return{contents:{kind:ko.Markdown,value:`**StepSet ${l.name}** \u2014 expands to:
39
+ ${r}`:void 0,"","RULES:","1. Output ONLY the feature file content \u2014 no prose, no code fences, no explanations.","2. Gherkin/.saffron syntax: Feature:, Scenario:, Scenario Outline: + Examples, Given/When/Then/And/But. Prefer concrete scenarios; use an Outline only when the requirements list variations of the same flow.","3. Then steps are assertions \u2014 state exactly what must be true, one condition per step. End every scenario with at least one Then.","4. If the same 3+ step sequence would appear in two or more scenarios, define it once as a StepSet (first step a guard, last step an exit assertion) and invoke it. Definition uses a colon (StepSet: Name), invocation does not (StepSet Name).",'5. Use 2-column data tables for key/value input, header tables for lists of records, and """ doc strings for free text.',"6. Steps are natural language for an AI browser agent \u2014 never CSS selectors or code.","","REQUIREMENTS:",e.trim()].filter(s=>s!==void 0).join(`
40
+ `)}async function Ro(e,t,n){let r={aiCalls:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreationTokens:0,costUsd:0},s="",o=tl({prompt:nl(e,t),options:{model:n,maxTurns:1,permissionMode:"bypassPermissions",persistSession:!1,settingSources:[],allowedTools:[],systemPrompt:"You are Saffron's feature-file author. You write clean, assertive Gherkin that reuses the project's exact step vocabulary. You output only file content."}});for await(let a of o)if(a.type==="assistant")for(let i of a.message.content)i.type==="text"&&(s+=i.text);else a.type==="result"&&(r.aiCalls=a.num_turns,r.costUsd=a.total_cost_usd,"usage"in a&&a.usage&&(r.inputTokens=a.usage.input_tokens??0,r.outputTokens=a.usage.output_tokens??0,r.cacheReadTokens=a.usage.cache_read_input_tokens??0,r.cacheCreationTokens=a.usage.cache_creation_input_tokens??0));return s=s.replace(/^\s*```(?:gherkin|saffron)?\s*\n/,"").replace(/\n```\s*$/,"").trim(),{content:s+`
41
+ `,usage:r}}import Ie from"node:fs";import{fileURLToPath as Eo}from"node:url";import Ae from"node:path";import{createConnection as rl,ProposedFeatures as sl,TextDocuments as ol,TextDocumentSyncKind as al,CompletionItemKind as Le,InsertTextFormat as Ao,DiagnosticSeverity as Nt,MarkupKind as Io}from"vscode-languageserver/node";import{TextDocument as il}from"vscode-languageserver-textdocument";var Co=/^(\s*)(Given|When|Then|And|But|\*)\s+(.+?)\s*$/,cl=/^\s*StepSet:\s*(.+?)\s*$/,Ar=/^(\s*)(StepSet)\s+([^\s:].*?)\s*$/,Ir={recorded:"\u25CF recorded \u2014 replays at zero tokens",divergent:"\u25CF divergent \u2014 same text, different recordings",unrecorded:"\u25CB written, not recorded yet"};function ll(e,t){let n=a=>new Set(a.toLowerCase().replace(/"[^"]*"/g,'"x"').split(/[^a-z0-9"<>]+/).filter(Boolean)),r=n(e),s=n(t);if(r.size===0||s.size===0)return 0;let o=0;for(let a of r)s.has(a)&&o++;return o/(r.size+s.size-o)}function dl(e){try{return JSON.parse(Ie.readFileSync(Ae.join(e,"saffron.config.json"),"utf8"))}catch{return{}}}function pl(e){try{let t=JSON.parse(Ie.readFileSync(Ae.join(e,"saffron.config.json"),"utf8"));return typeof t.features=="string"?t.features:void 0}catch{return}}function Oo(e,t,n={}){let r=e,s=t,o=rl(sl.all,process.stdin,process.stdout),a=new ol(il),i={steps:[],stepSets:[],scannedFiles:0,scannedCaches:0},c=new Map,l,f=()=>{try{let y=Ae.isAbsolute(s)?s:Ae.join(r,s),x=dl(r);tt(r,x.dataDir,process.env.SAFFRON_ENV??x.env),c=ve(We(y),r),i=be(r,s,c)}catch(y){o.console.warn(`saffron vocabulary rebuild: ${String(y)}`)}for(let y of a.all())h(y)},d=()=>{clearTimeout(l),l=setTimeout(f,300)};function m(y){return i.steps.find(x=>x.text===y)?.status}function h(y){let x=y.getText().split(`
42
+ `),k=gl(x);if(!y.uri.endsWith(".saffron")){o.sendDiagnostics({uri:y.uri,diagnostics:k});return}let w=new Map;for(let v=0;v<x.length;v++){let u=x[v],p={start:{line:v,character:0},end:{line:v,character:u.length}},b=u.match(Ar);b&&!c.has(b[3])&&k.push({range:p,severity:Nt.Error,source:"saffron",message:`StepSet "${b[3]}" is not defined anywhere in the project.`});let g=u.match(cl);if(g){let A=c.get(g[1]),R=Ae.relative(r,Eo(y.uri)).replace(/\\/g,"/");(w.has(g[1])||A&&A.file!==R)&&k.push({range:p,severity:Nt.Error,source:"saffron",message:`StepSet "${g[1]}" is defined twice \u2014 set names are project-wide. If this line was meant to INVOKE the set, remove the colon: "StepSet ${g[1]}".`}),w.set(g[1],v)}let S=u.trim();if(S&&!b&&!g){for(let[A]of c)if(S.toLowerCase()===A.toLowerCase()){k.push({range:p,severity:Nt.Warning,source:"saffron",message:`Did you mean "StepSet ${A}"? Invoking a step set needs the StepSet keyword, like a step needs Given/When/Then.`});break}}let T=u.match(Co);if(T&&m(T[3])!=="recorded"){for(let A of i.steps)if(!(A.text===T[3]||A.status==="unrecorded")&&ll(A.text,T[3])>=.7){k.push({range:{start:{line:v,character:u.indexOf(T[3])},end:{line:v,character:u.length}},severity:Nt.Information,source:"saffron",message:`Similar to the ${A.status} step "${A.text}" (\xD7${A.usage}). Reusing the exact wording replays at zero tokens; a new wording costs a fresh AI recording.`});break}}}o.sendDiagnostics({uri:y.uri,diagnostics:k})}o.onInitialize(y=>{if(n.rootFromClient){let x=y.workspaceFolders?.[0]?.uri??y.rootUri??void 0;if(x&&x.startsWith("file:")){let k=Eo(x);Ie.existsSync(k)&&(r=k,s=pl(k)??t)}}return f(),{capabilities:{textDocumentSync:al.Incremental,completionProvider:{triggerCharacters:[" ",":","."]},definitionProvider:!0,hoverProvider:!0}}}),o.onCompletion(y=>{let x=a.get(y.textDocument.uri);if(!x)return[];let k=x.getText({start:{line:y.position.line,character:0},end:y.position}),w=hl(k);return w||ml(x.uri,k,y.position.line,i)}),o.onDefinition(y=>{let x=a.get(y.textDocument.uri);if(!x)return;let k=x.getText({start:{line:y.position.line,character:0},end:{line:y.position.line+1,character:0}}).replace(/\n$/,""),w=Po(k,y.position.character);if(w){let g=ul(w.ref);return g?{uri:`file://${g.file}`,range:{start:{line:g.line,character:0},end:{line:g.line,character:0}}}:void 0}let v=_o(k,y.position.character);if(v){let g=jo(r,v.name);return g?{uri:`file://${g.file}`,range:{start:{line:g.line,character:0},end:{line:g.line,character:0}}}:void 0}let u=k.match(Ar);if(!u)return;let p=c.get(u[3]);return p?{uri:`file://${Ae.join(r,p.file)}`,range:{start:{line:p.line-1,character:0},end:{line:p.line-1,character:0}}}:void 0}),o.onHover(y=>{let x=a.get(y.textDocument.uri);if(!x)return;{let u=x.getText({start:{line:y.position.line,character:0},end:{line:y.position.line+1,character:0}}).replace(/\n$/,""),p=Po(u,y.position.character);if(p)return{contents:{kind:"markdown",value:Fo(p.ref)}};let b=_o(u,y.position.character);if(b){let g=jo(r,b.name),S=g?`defined in ${Ae.relative(r,g.file)} (line ${g.line+1}); the value is resolved at replay and never shown here`:"not found in .env or .env.example; export it in the shell or add it to a git-ignored .env";return{contents:{kind:"markdown",value:`**{env:${b.name}}** ${S}.`}}}}let k=x.getText({start:{line:y.position.line,character:0},end:{line:y.position.line+1,character:0}}).replace(/\n$/,""),w=k.match(Ar);if(w){let u=i.stepSets.find(b=>b.name===w[3]);if(!u)return;let p=u.steps.map(b=>`- ${Ir[m(b)??"unrecorded"][0]} ${b}`).join(`
43
+ `);return{contents:{kind:Io.Markdown,value:`**StepSet ${u.name}** \u2014 expands to:
44
44
 
45
- ${u}`}}}let v=k.match($o);if(v){let l=i.steps.find(u=>u.text===v[3]);return l?{contents:{kind:ko.Markdown,value:`${Er[l.status]}
45
+ ${p}`}}}let v=k.match(Co);if(v){let u=i.steps.find(p=>p.text===v[3]);return u?{contents:{kind:Io.Markdown,value:`${Ir[u.status]}
46
46
 
47
- Used in ${l.usage} place(s): ${l.files.join(", ")}${l.fromStepSet?`
47
+ Used in ${u.usage} place(s): ${u.files.join(", ")}${u.fromStepSet?`
48
48
 
49
- Defined in StepSet **${l.fromStepSet}**`:""}`}}:void 0}}),a.onDidOpen(y=>h(y.document)),a.onDidChangeContent(y=>h(y.document)),a.onDidSave(()=>d()),s.onDidChangeWatchedFiles(()=>d()),a.listen(s),s.listen()}function To(e,t){let n=/\{data:([A-Za-z0-9_.-]+)\}/g,r;for(;r=n.exec(e);)if(t>=r.index&&t<=r.index+r[0].length)return{ref:r[1],start:r.index,end:r.index+r[0].length}}var cl=e=>/^\s*Examples:/.test(e);function Io(e,t=!1){let{env:n}=je(),r=n?` (environment "${n}")`:"";try{if(t||!e.includes(".")){let{headers:a,rows:i}=Gt(e);return`**{data:${e}}** ${i.length} Examples row(s), columns ${a.map(c=>`\`${c}\``).join(", ")}${r}.`}let o=Oe(e);return Array.isArray(ze(e))?`**{data:${e}}** a list of ${o.length}: ${o.map(a=>`"${a}"`).join(", ")}${r}. Use it with "every": replay checks each value, so adding one needs no re-record.`:`**{data:${e}}** = "${o[0]}"${r}. Resolved at replay: change the file and the next run uses the new value at zero tokens.`}catch(o){return`**{data:${e}}** does not resolve: ${o instanceof Error?o.message:String(o)}`}}function ll(e){let{dir:t,env:n}=je(),[r,...o]=e.split("."),s=[n&&`${r}.${n}.json`,`${r}.json`,n&&`${r}.${n}.csv`,`${r}.csv`].filter(Boolean),a=[...o].reverse().find(c=>!/^\d+$/.test(c)),i;for(let c of s){let p=Ee.join(t,c);if(!Ae.existsSync(p))continue;let f=Ae.readFileSync(p,"utf8").split(`
50
- `),d=a?f.findIndex(m=>m.includes(`"${a}"`)):0;if(d>=0)return{file:p,line:d};i??={file:p,line:0}}return i}function dl(e){let t=[];return e.forEach((n,r)=>{for(let o of n.matchAll(/\{data:([A-Za-z0-9_.-]+)\}/g)){let s=Io(o[1],cl(n));s.includes("does not resolve")&&t.push({range:{start:{line:r,character:o.index??0},end:{line:r,character:(o.index??0)+o[0].length}},severity:Nt.Error,source:"saffron",message:s.replace(/\*\*/g,"")})}}),t}function pl(e){let t=e.match(/\{data:([A-Za-z0-9_.-]*)$/);if(!t)return;let n=t[1],{dir:r}=je(),o=n.split(".");if(o.length===1)return(Ae.existsSync(r)?[...new Set(Ae.readdirSync(r).filter(a=>/\.(json|csv)$/.test(a)).map(a=>a.split(".")[0]))].sort():[]).map(a=>({label:a,kind:De.File,detail:"data file",insertText:a}));try{let s=ze(o.slice(0,-1).join("."));return s===null||typeof s!="object"?[]:Object.entries(s).map(([a,i])=>({label:a,kind:i!==null&&typeof i=="object"?De.Module:De.Value,detail:Array.isArray(i)?`list of ${i.length}`:i!==null&&typeof i=="object"?"object":JSON.stringify(i),insertText:a}))}catch{return[]}}function Ro(e,t){let n=/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g,r;for(;r=n.exec(e);)if(t>=r.index&&t<=r.index+r[0].length)return{name:r[1],start:r.index,end:r.index+r[0].length}}function Eo(e,t){for(let n of[".env",".env.example"]){let r=Ee.join(e,n);if(!Ae.existsSync(r))continue;let s=Ae.readFileSync(r,"utf8").split(`
51
- `).findIndex(a=>new RegExp(`^\\s*(?:export\\s+)?${t}\\s*=`).test(a));if(s!==-1)return{file:r,line:s}}}function fl(e,t,n,r){let o=e.endsWith(".saffron");if(/^\s*StepSet\s+/.test(t)&&o)return r.stepSets.map(i=>({label:i.name,kind:De.Function,detail:`step set \xB7 ${i.steps.length} step(s) \xB7 \xD7${i.usage}`,documentation:i.steps.join(`
52
- `)}));if(/^\s*(Given|When|Then|And|But|\*)\s+/.test(t))return r.steps.map((i,c)=>({label:i.text,kind:De.Text,detail:`${Er[i.status]} \xB7 \xD7${i.usage}`,sortText:String(c).padStart(5,"0"),filterText:i.text}));let a=t.replace(/^\s*/,"").trimEnd().toLowerCase().replace(/\s+/g,"");if(a.length>0&&("stepset:".startsWith(a)||a.startsWith("stepset"))&&o){let i={start:{line:n,character:t.length-t.replace(/^\s*/,"").length},end:{line:n,character:t.length}},c=[{label:"StepSet: \u2026",kind:De.Keyword,detail:"define a reusable step set",documentation:`StepSet: Name
49
+ Defined in StepSet **${u.fromStepSet}**`:""}`}}:void 0}}),a.onDidOpen(y=>h(y.document)),a.onDidChangeContent(y=>h(y.document)),a.onDidSave(()=>d()),o.onDidChangeWatchedFiles(()=>d()),a.listen(o),o.listen()}function Po(e,t){let n=/\{data:([A-Za-z0-9_.-]+)\}/g,r;for(;r=n.exec(e);)if(t>=r.index&&t<=r.index+r[0].length)return{ref:r[1],start:r.index,end:r.index+r[0].length}}var fl=e=>/^\s*Examples:/.test(e);function Fo(e,t=!1){let{env:n}=Oe(),r=n?` (environment "${n}")`:"";try{if(t||!e.includes(".")){let{headers:a,rows:i}=Kt(e);return`**{data:${e}}** ${i.length} Examples row(s), columns ${a.map(c=>`\`${c}\``).join(", ")}${r}.`}let s=Fe(e);return Array.isArray(qe(e))?`**{data:${e}}** a list of ${s.length}: ${s.map(a=>`"${a}"`).join(", ")}${r}. Use it with "every": replay checks each value, so adding one needs no re-record.`:`**{data:${e}}** = "${s[0]}"${r}. Resolved at replay: change the file and the next run uses the new value at zero tokens.`}catch(s){return`**{data:${e}}** does not resolve: ${s instanceof Error?s.message:String(s)}`}}function ul(e){let{dir:t,env:n}=Oe(),[r,...s]=e.split("."),o=[n&&`${r}.${n}.json`,`${r}.json`,n&&`${r}.${n}.csv`,`${r}.csv`].filter(Boolean),a=[...s].reverse().find(c=>!/^\d+$/.test(c)),i;for(let c of o){let l=Ae.join(t,c);if(!Ie.existsSync(l))continue;let f=Ie.readFileSync(l,"utf8").split(`
50
+ `),d=a?f.findIndex(m=>m.includes(`"${a}"`)):0;if(d>=0)return{file:l,line:d};i??={file:l,line:0}}return i}function gl(e){let t=[];return e.forEach((n,r)=>{for(let s of n.matchAll(/\{data:([A-Za-z0-9_.-]+)\}/g)){let o=Fo(s[1],fl(n));o.includes("does not resolve")&&t.push({range:{start:{line:r,character:s.index??0},end:{line:r,character:(s.index??0)+s[0].length}},severity:Nt.Error,source:"saffron",message:o.replace(/\*\*/g,"")})}}),t}function hl(e){let t=e.match(/\{data:([A-Za-z0-9_.-]*)$/);if(!t)return;let n=t[1],{dir:r}=Oe(),s=n.split(".");if(s.length===1)return(Ie.existsSync(r)?[...new Set(Ie.readdirSync(r).filter(a=>/\.(json|csv)$/.test(a)).map(a=>a.split(".")[0]))].sort():[]).map(a=>({label:a,kind:Le.File,detail:"data file",insertText:a}));try{let o=qe(s.slice(0,-1).join("."));return o===null||typeof o!="object"?[]:Object.entries(o).map(([a,i])=>({label:a,kind:i!==null&&typeof i=="object"?Le.Module:Le.Value,detail:Array.isArray(i)?`list of ${i.length}`:i!==null&&typeof i=="object"?"object":JSON.stringify(i),insertText:a}))}catch{return[]}}function _o(e,t){let n=/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g,r;for(;r=n.exec(e);)if(t>=r.index&&t<=r.index+r[0].length)return{name:r[1],start:r.index,end:r.index+r[0].length}}function jo(e,t){for(let n of[".env",".env.example"]){let r=Ae.join(e,n);if(!Ie.existsSync(r))continue;let o=Ie.readFileSync(r,"utf8").split(`
51
+ `).findIndex(a=>new RegExp(`^\\s*(?:export\\s+)?${t}\\s*=`).test(a));if(o!==-1)return{file:r,line:o}}}function ml(e,t,n,r){let s=e.endsWith(".saffron");if(/^\s*StepSet\s+/.test(t)&&s)return r.stepSets.map(i=>({label:i.name,kind:Le.Function,detail:`step set \xB7 ${i.steps.length} step(s) \xB7 \xD7${i.usage}`,documentation:i.steps.join(`
52
+ `)}));if(/^\s*(Given|When|Then|And|But|\*)\s+/.test(t))return r.steps.map((i,c)=>({label:i.text,kind:Le.Text,detail:`${Ir[i.status]} \xB7 \xD7${i.usage}`,sortText:String(c).padStart(5,"0"),filterText:i.text}));let a=t.replace(/^\s*/,"").trimEnd().toLowerCase().replace(/\s+/g,"");if(a.length>0&&("stepset:".startsWith(a)||a.startsWith("stepset"))&&s){let i={start:{line:n,character:t.length-t.replace(/^\s*/,"").length},end:{line:n,character:t.length}},c=[{label:"StepSet: \u2026",kind:Le.Keyword,detail:"define a reusable step set",documentation:`StepSet: Name
53
53
  Given \u2026
54
54
  When \u2026
55
55
 
56
- Invoke it anywhere with: StepSet Name`,filterText:"step set stepset:",sortText:"0",insertTextFormat:So.Snippet,textEdit:{range:i,newText:"StepSet: ${1:Name}\n "}}];r.stepSets.length===0&&c.push({label:"StepSet \u2026",kind:De.Keyword,detail:"invoke a step set (none defined yet)",filterText:"step set stepset",sortText:"1",insertTextFormat:So.Snippet,textEdit:{range:i,newText:"StepSet ${1:Name}"}});for(let p of r.stepSets)c.push({label:`StepSet ${p.name}`,kind:De.Function,detail:`invoke step set \xB7 ${p.steps.length} step(s)`,documentation:p.steps.join(`
57
- `),filterText:`step set ${p.name}`,sortText:`1${p.name}`,textEdit:{range:i,newText:`StepSet ${p.name}`}});return c}return[]}import mt from"node:fs";import wl from"node:net";import ie from"node:path";import{chromium as xl,firefox as vl,webkit as Sl}from"playwright";var ul=5e3,gl=new Set(["click","press","selectOption","check","uncheck","hover","dragTo"]);function _o(e){return e.mode==="heal"||e.mode==="refine"}function jo(e){return e.mode==="heal"||e.mode==="refine"?e.failedStep?.stepIndex??e.completedSteps?.length??0:e.recordOnly?e.recordOnly.from:e.completedSteps?.length??0}var Po=e=>`${e} action${e===1?"":"s"}`,Co=e=>e.map(ce).join("; "),vn=class{firstStep;discardBeforeAnnounce;preAnnounce=[];discarded=[];carried;pendingTabOpen=!1;pendingTabFallback;lastOpener;lastOpenerAt=0;inFlight=new Set;steps;currentStepIndex=-1;done=!1;success=!1;narrative="";adaptations=[];suggestedFeatureEdit;featureEdits=[];announced=new Set;pendingFingerprint;constructor(t,n={}){this.firstStep=Math.max(n.firstStep??0,0),this.discardBeforeAnnounce=n.discardBeforeAnnounce??!1,this.steps=t.steps.map(r=>({gherkin:r.text,keyword:r.keyword,kind:r.kind,table:r.table,docString:r.docString,fromStepSet:r.source?.fromStepSet,actions:[]}))}addArmedAction(t){if(this.done)return;let n=this.destination();if(!n){this.discard([t],this.refusalReason());return}if(n.some(s=>s.action===t.action&&s.value===t.value&&s.promptText===t.promptText))return;let o=Math.max(n.length-1,0);n.splice(o,0,t)}destination(){if(this.currentStepIndex<0)return this.discardBeforeAnnounce?void 0:this.firstStep===0?this.steps[0]?.actions:this.preAnnounce;if(!(this.currentStepIndex<this.firstStep))return this.steps[this.currentStepIndex]?.actions}refusalReason(){let t=this.currentStepIndex;return t<0?`before announcing the failed step (step ${this.firstStep+1}); a heal records nothing until it announces that step, so this exploration was not filed under any step`:`under step ${t+1} ("${this.steps[t]?.gherkin??""}"), which had already run before the agent took over, so its recording was kept`}discard(t,n){if(t.length===0)return;let r=this.discarded.find(o=>o.why===n);r?r.actions.push(...t):this.discarded.push({actions:[...t],why:n})}announce(t,n,r){this.currentStepIndex=t,this.announced.add(t),r&&this.adaptations.push(`Step ${t+1} ("${n}"): ${r}`);let o=this.steps[t];this.preAnnounce.length>0&&t>=this.firstStep&&o&&(o.actions.unshift(...this.preAnnounce),this.carried={index:t,actions:this.preAnnounce},this.preAnnounce=[])}attributionNotes(){let t=[];if(this.carried){let{index:r,actions:o}=this.carried;t.push(`recording: ${Po(o.length)} the agent performed before announcing a step ${o.length===1?"was":"were"} filed under step ${r+1} ("${this.steps[r]?.gherkin??""}"), the first step it recorded: ${Co(o)}.`)}let n=this.preAnnounce.length>0?[{actions:this.preAnnounce,why:"before announcing a step this session records"}]:[];for(let{actions:r,why:o}of[...this.discarded,...n])t.push(`recording: discarded ${Po(r.length)} the agent performed ${o}: ${Co(r)}.`);return t}beginToolCall(t){this.inFlight.add(t)}endToolCall(t,n){this.inFlight.delete(t)&&(this.pendingTabOpen&&n&&(this.pendingTabFallback=void 0),this.inFlight.size===0&&this.pendingTabOpen&&(this.pendingTabFallback&&(this.pendingTabFallback.opensTab=!0),this.pendingTabOpen=!1,this.pendingTabFallback=void 0))}markTabOpened(){let t=this.inFlight.size===0,n=Date.now()-this.lastOpenerAt<=ul;if(t){this.lastOpener&&n&&(this.lastOpener.opensTab=!0);return}this.pendingTabOpen=!0,this.pendingTabFallback=n?this.lastOpener:void 0}addActions(t){if(this.done||t.length===0)return;if(this.currentStepIndex<0&&this.firstStep>0&&!this.discardBeforeAnnounce){let o=t.filter(s=>s.action==="switchTab");if(this.discard(o,"before announcing its first step (replay is already on the live page when the agent takes over)"),t=t.filter(s=>s.action!=="switchTab"),t.length===0)return}let n=this.destination();if(!n){this.pendingFingerprint=void 0,this.discard(t,this.refusalReason());return}this.pendingFingerprint&&t[0]&&(t[0].pageFingerprint=this.pendingFingerprint,this.pendingFingerprint=void 0),n.push(...t);let r=[...t].reverse().find(o=>gl.has(o.action));r&&(this.lastOpener=r,this.lastOpenerAt=Date.now(),this.pendingTabOpen&&(r.opensTab=!0,this.pendingTabOpen=!1,this.pendingTabFallback=void 0))}},Ke=e=>e.replace(/\\(['"\\])/g,"$1");function hl(e){let t=e.match(/'((?:[^'\\]|\\.)*)'/);return t?Ke(t[1]):void 0}function Ar(e){let t={},n=e.match(/(?:frameLocator\('((?:[^'\\]|\\.)*)'\)|locator\('((?:[^'\\]|\\.)*)'\)\.contentFrame\(\))/);n&&(t.frame=Ke(n[1]??n[2]),e=e.slice((n.index??0)+n[0].length));let r=e.match(/getByRole\('([^']+)'(?:,\s*\{([^}]*)\})?\)/);if(r){t.role=r[1];let a=(r[2]??"").match(/name:\s*'((?:[^'\\]|\\.)*)'/);a&&(t.name=Ke(a[1]))}let o=[[/getByLabel\('((?:[^'\\]|\\.)*)'\)/,"label"],[/getByText\('((?:[^'\\]|\\.)*)'\)/,"text"],[/getByTestId\('((?:[^'\\]|\\.)*)'\)/,"testId"],[/getByPlaceholder\('((?:[^'\\]|\\.)*)'\)/,"placeholder"]];for(let[a,i]of o){let c=e.match(a);c&&(t[i]=Ke(c[1]))}let s=e.match(/(?:^|\.)locator\('((?:[^'\\]|\\.)*)'\)/);return s&&(t.fallbackSelectors=[Ke(s[1])]),Object.keys(t).length>0?t:void 0}var ml={click:"click",dblclick:"click",fill:"fill",pressSequentially:"fill",type:"fill",press:"press",check:"check",uncheck:"uncheck",hover:"hover",selectOption:"selectOption"};function yl(e,t){let n=e.match(/page\.goto\('((?:[^'\\]|\\.)*)'\)/);if(n)return{action:"goto",url:It(Ke(n[1]),t)};let r=e.match(/page\.keyboard\.press\('((?:[^'\\]|\\.)*)'\)/);if(r)return{action:"press",value:Ke(r[1])};let o=e.match(/await\s+page\.(.+)\.dragTo\(page\.(.+?)\);?\s*$/);if(o){let m=Ar(o[1]),h=Ar(o[2]);if(m&&h)return{action:"dragTo",target:m,toTarget:h}}let s=e.match(/await\s+page\.(.+)\.(click|dblclick|fill|pressSequentially|type|press|check|uncheck|hover|selectOption)\((.*)\);?\s*$/);if(!s)return;let[,a,i,c]=s,p=Ar(a);if(!p)return;let f={action:ml[i],target:p},d=hl(c);return d!==void 0&&i!=="click"&&i!=="dblclick"&&(f.value=d),f}function Oo(e){let t=Array.isArray(e)?e:e?.content,n=typeof e=="string"?[e]:Array.isArray(t)?t.flatMap(r=>typeof r?.text=="string"?[r.text]:[]):[];for(let r of n){let o=r.split(/^### Open tabs\r?$/m)[1]?.split(/^### /m)[0];if(!o)continue;let s=/^- (\d+): \(current\) /m.exec(o)?.[1];if(s!==void 0)return Number(s)}}function Fo(e,t){let n=[],r=Array.isArray(e)?e:e?.content;if(Array.isArray(r))for(let s of r){let a=s?.text;typeof a=="string"&&n.push(a)}else typeof e=="string"&&n.push(e);let o=[];for(let s of n)for(let a of s.split(`
58
- `)){let i=yl(a.trim(),t);i&&o.push(i)}return bl(o)}function bl(e){return e.filter((t,n)=>{let r=e[n+1];return!(t.action==="fill"&&(t.value??"")===""&&r?.action==="fill"&&JSON.stringify(r.target)===JSON.stringify(t.target))})}function No(e){let t=[];for(let n of e){if(n.kind==="assertion"&&n.actions.length===0){t.push(`Step "${n.gherkin}" is an assertion but recorded no check, so nothing would be verified. Re-record with: saffron run --rerecord`);continue}if(/"[^"]+"|<[^<>]+>/.test(n.gherkin)){for(let o of n.actions)if(o.action==="fill"&&(o.value??"")===""){t.push(`Step "${n.gherkin}" recorded a fill with an empty value although the step passes one; the typed value was lost. Re-record with: saffron run --rerecord`);break}}}return t}function Ir(e,t){let n=e;for(let[r,o]of Object.entries(t).filter(([,s])=>s.length>=3).sort((s,a)=>a[1].length-s[1].length))n=n.split(o).join(`<${r}>`);return n}function Mo(e,t){let n=Object.entries(t).filter(([,a])=>a.length>=3);if(n.length===0)return e;let r=a=>a===void 0?void 0:Ir(a,t),o=[...n].sort((a,i)=>i[1].length-a[1].length),s=a=>{if(a!==void 0){for(let[i,c]of o)if(a.includes(c))return`<${i}>`;return a}};return e.map(a=>({...a,actions:a.actions.map(i=>{let c=pe.has(i.action)&&i.action!=="expectValue";return{...i,value:c?s(i.value):r(i.value),url:r(i.url),target:i.target?{...i.target,name:c?s(i.target.name):r(i.target.name),label:r(i.target.label),text:c?s(i.target.text):r(i.target.text),placeholder:r(i.target.placeholder)}:void 0}})}))}function Do(e,t,n=r=>r){let r=Ce(t.steps);return e.map((o,s)=>{let a=t.steps[s],i=a?.resolvedTable??a?.table,c=a?.resolvedDocString??a?.docString,p=Object.entries({...r,...ne(i),...Be(c)}).map(([d,m])=>[d,n(m)]).filter(([,d])=>d.length>=3).sort((d,m)=>m[1].length-d[1].length);if(p.length===0)return o;let f=d=>{if(d===void 0)return;let m=d;for(let[h,y]of p)m=m.split(y).join(`<${h}>`);return m};return{...o,actions:o.actions.map(d=>({...d,value:f(d.value),url:f(d.url),target:d.target?{...d.target,name:f(d.target.name),label:f(d.target.label),text:f(d.target.text),placeholder:f(d.target.placeholder)}:void 0}))}})}function Lo(e,t=new Date){let n=r=>r===void 0?void 0:Fs(r,t);return e.map(r=>({...r,actions:r.actions.map(o=>({...o,value:n(o.value),url:n(o.url),target:o.target?{...o.target,name:n(o.target.name),nameRegex:n(o.target.nameRegex),label:n(o.target.label),text:n(o.target.text)}:void 0}))}))}function Uo(e,t){let n=(s,a)=>{let i={...s};for(let c of a)typeof s[c]=="string"&&(i[c]=t(s[c]));return i},r=s=>{let a=n(s,["name","nameRegex","label","text","placeholder","testId","frame"]);return s.fallbackSelectors&&(a.fallbackSelectors=s.fallbackSelectors.map(t)),a},o=n(e,["value","url","pattern","urlPattern","promptText"]);return e.target&&(o.target=r(e.target)),e.toTarget&&(o.toTarget=r(e.toTarget)),o}function Bo(e,t,n=[]){if(t.size===0)return e;let r=$s(n,t),o=it(t,{standaloneBelow:6}),s=a=>o.mask(r(a));return e.map(a=>({...a,actions:a.actions.map(i=>Uo(i,s))}))}function zo(e,t){if(t.size===0)return e;let n=r=>typeof r=="string"?Ds(r,t):Array.isArray(r)?r.map(n):r&&typeof r=="object"?Object.fromEntries(Object.entries(r).map(([o,s])=>[o,o==="pageFingerprint"?s:n(s)])):r;return e.map(r=>({...r,actions:r.actions.map(o=>n(o))}))}function qo(e,t){return t.size===0?e:e.map(n=>{let r=new Set([...n.gherkin.matchAll(vt)].map(s=>s[1])),o=s=>is(s,t,r);return{...n,actions:n.actions.map(s=>Uo(s,o))}})}function Ho(e,t,n){if(e!=="mcp__playwright__browser_tabs")return;let r=t??{},o=typeof r.index=="number"?String(r.index):void 0;switch(r.action){case"select":return o===void 0?void 0:{action:"switchTab",value:o};case"close":return o===void 0?{action:"closeTab"}:{action:"closeTab",value:o};case"new":return typeof r.url=="string"&&r.url?{action:"newTab",url:It(r.url,n)}:{action:"newTab"};default:return}}function Vo(e,t){let n=t??{};if(e==="mcp__playwright__browser_handle_dialog"){let r={action:"handleDialog",value:n.accept?"accept":"dismiss"};return typeof n.promptText=="string"&&n.promptText&&(r.promptText=n.promptText),r}if(e==="mcp__playwright__browser_file_upload"){let r=Array.isArray(n.paths)?n.paths:[];if(r.length===0)return;let o=process.cwd()+"/";return{action:"uploadFile",value:r.map(a=>typeof a=="string"&&a.startsWith(o)?a.slice(o.length):a).join(`
59
- `)}}}function Mt(e,t){let n=e.pages().filter(o=>!o.isClosed()),r=o=>[...n].reverse().find(o);return(t?r(o=>o.url()===t):void 0)??r(o=>o.url()!=="about:blank")??n.at(-1)}async function Sn(e,t){let n=t.context();for(let o of e.contexts())if(o!==n)for(let s of o.pages())await s.close().catch(()=>{});await t.bringToFront().catch(()=>{});let r=n.pages().filter(o=>!o.isClosed());return{index:Math.max(r.indexOf(t),0),count:r.length,url:t.url()}}var Cr=["chromium","chrome","msedge","firefox","webkit"],Pr={chrome:{label:"Google Chrome"},msedge:{label:"Microsoft Edge"}};function Qo(e){return Cr.includes(e)}var Dt=class extends Error{},Ye={aiCalls:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreationTokens:0,costUsd:0};async function kl(){return new Promise((e,t)=>{let n=wl.createServer();n.listen(0,"127.0.0.1",()=>{let{port:r}=n.address();n.close(()=>e(r))}),n.on("error",t)})}function Wo(e){let t=Kt(X(e));return t.size>0?{data:Object.fromEntries([...t].sort(([n],[r])=>n.localeCompare(r)))}:{}}function yt(e){let t=e.contexts().flatMap(n=>n.pages());return[...t].reverse().find(n=>n.url()!=="about:blank")??t.at(-1)}function Zo(e){let t=e.length;for(let n=e.length-1;n>=0&&e[n].kind==="assertion";n--)t=n;return t}function Jo(e,t){return e[t]?.kind==="assertion"&&t>=Zo(e)}function Go(e,t,n=!1,r){let o=Zo(e);return e.map((s,a)=>{if(r&&a<r.stepIndex)return s;let i=t[a],c=i&&i.actions.length>0;return s.kind==="assertion"?n&&a<o&&c?i:s:c?r&&a===r.stepIndex&&r.actionIndex>0?{...i,actions:[...s.actions.slice(0,r.actionIndex),...i.actions]}:i:s})}function $l(e,t,n,r){let o=to(e,t);return o.length===0||!n?.pageUrl?o:[`page at failure: replay stopped at step ${n.stepIndex+1} ("${n.stepText}") on ${It(n.pageUrl,r)}, where the heal started.`,...o]}function Tl(e,t,n=0){return e.map((r,o)=>{if(o<n)return r;let s=t[o];return s&&s.actions.length>0?s:r})}function Ko(e,t){let n=[];for(let r=0;r<Math.min(t,e.length);r++){let o=e[r].actions.length;o!==0&&n.push(`recording: kept the existing recording of step ${r+1} ("${e[r].gherkin}"), which ran before the agent took over; ignored ${o} action${o===1?"":"s"} the agent's session held for it.`)}return n}function Xo(e,t,n,r){let o=t.filter(c=>c>=0&&c<e.length),s=o.length>0?Math.max(...o):0,a=Math.min(n&&n.stepIndex>=s?n.stepIndex:s,e.length-1),i=n&&a===n.stepIndex?n.error:r;return e.map((c,p)=>({gherkin:c.gherkin,kind:c.kind,status:p<a?"passed":p===a?"failed":"skipped",...p===a&&i?{error:i}:{}}))}function Rl(e,t,n){let r=n.success?e.map(o=>({gherkin:o.gherkin,kind:o.kind,status:"passed"})):Xo(e,n.announcedSteps??[],t,n.narrative);return e.map((o,s)=>s<t.stepIndex?o:r[s])}function Yo(e,t){return{aiCalls:e.aiCalls+t.aiCalls,inputTokens:e.inputTokens+t.inputTokens,outputTokens:e.outputTokens+t.outputTokens,cacheReadTokens:e.cacheReadTokens+t.cacheReadTokens,cacheCreationTokens:e.cacheCreationTokens+t.cacheCreationTokens,costUsd:(e.costUsd??0)+(t.costUsd??0),models:e.models||t.models?[...new Set([...e.models??[],...t.models??[]])]:void 0}}var kn=class{constructor(t){this.options=t}options;browser;cdpEndpoint;stepIndex;get agent(){return this.options.agent}getStepIndex(){if(!this.stepIndex&&(this.stepIndex=ye(this.options.projectRoot),this.stepIndex.steps.size>0)){let t=this.stepIndex.divergent.length>0?` \xB7 ${this.stepIndex.divergent.length} step(s) have divergent recordings (Level-2 signal)`:"";console.log(`step index: ${this.stepIndex.steps.size} reusable steps from ${this.stepIndex.scannedCaches} cache(s)${t}`)}return this.stepIndex}async recordWithSeeding(t,n,r){let o=this.getStepIndex(),s=t.steps.length,a={...t.params,...Ce(t.steps)},i=t.steps.map(u=>{let x=o.steps.size>0?ys(o,u,a):void 0;return x?{gherkin:u.text,keyword:u.keyword,kind:u.kind,table:u.table,docString:u.docString,fromStepSet:u.source?.fromStepSet,actions:x.actions}:void 0}),c=t.steps.map(u=>({gherkin:u.text,keyword:u.keyword,kind:u.kind,table:u.table,docString:u.docString,fromStepSet:u.source?.fromStepSet,actions:[]})),p=new Map;p.set(le,r);let f=[],d=[],m=[],h=[],y={...Ye},b=n,k=0,w=0,v=0,l=(u,x)=>({success:u,narrative:d.join(" ")||"Composed entirely from seeded recordings.",adaptations:f,suggestedFeatureEdit:h.length>0?h.join(`
60
- `):void 0,featureEdits:m,recordedSteps:c,announcedSteps:x===void 0?[]:Array.from({length:x+1},(g,S)=>S),usage:y,seededCount:k});for(;v<s;){if(i[v]){let E=v;for(;E<s&&i[E];)E++;let C=i.slice(v,E),I={...t,steps:t.steps.slice(v,E)},P=await Ot(b.isClosed()?Mt(n.context())??n:b,I,this.buildCandidateCache(t,C),{baseURL:this.options.baseURL,actionTimeoutMs:this.options.actionTimeoutMs,retries:this.options.retries,pollIntervalMs:this.options.pollIntervalMs,vars:p,trace:this.options.trace!==void 0&&this.options.trace!=="off",traceStepOffset:v});b=P.activePage??b;let j=P.status==="passed"?E-v:P.failure?.stepIndex??0;for(let _=0;_<j;_++)c[v+_]=C[_];if(k+=j,P.status==="passed"){v=E;continue}let L=v+j;f.push(`Seeded recording for "${t.steps[L].text}" did not replay in this scenario's context; the agent re-recorded it fresh. Divergence signal.`),i[L]=void 0,v=L;continue}if(!this.options.agent)return l(!1,v);w++;let u=v;for(;u<s&&!i[u];)u++;w>=3&&(u=s);let x=await Sn(this.browser,b.isClosed()?Mt(n.context())??n:b),g=E=>{b=E};n.context().on("page",g);let S;try{S=await this.withDialogsVisible(()=>this.options.agent.run({scenario:t,baseURL:this.options.baseURL,traceContext:this.options.trace&&this.options.trace!=="off"?n.context():void 0,mode:"record",cdpEndpoint:this.cdpEndpoint,assertionPolicy:this.options.assertionPolicy,completedSteps:v>0?t.steps.slice(0,v).map(E=>E.resolvedText):void 0,recordOnly:u<s?{from:v,to:u-1}:void 0,liveTab:x,onActiveTab:E=>{let C=n.context().pages().filter(I=>!I.isClosed())[E];C&&(b=C)},uniqueSeed:p.get(le),onTabOpened:this.tabWatcher(),takeFingerprint:async()=>Je(yt(this.browser)??n)}))}finally{n.context().off("page",g)}if(y=Yo(y,S.usage),d.push(S.narrative),f.push(...S.adaptations),m.push(...S.featureEdits),S.suggestedFeatureEdit&&h.push(S.suggestedFeatureEdit),!S.success){let E=S.announcedSteps.filter(C=>C>=v&&C<s);return l(!1,E.length>0?Math.max(...E):v)}let T=S.announcedSteps.filter(E=>E>=v&&E<s),A=Math.min(s-1,Math.max(T.length>0?Math.max(...T):u-1,v));for(let E=v;E<=A;E++)c[E]=S.recordedSteps[E];v=A+1}return l(!0)}async withDialogsVisible(t){let n=()=>{},r=[],o=[],s=a=>{a.on("dialog",n),r.push(a)};for(let a of this.browser?.contexts()??[]){a.on("page",s),o.push(a);for(let i of a.pages())s(i)}try{return await t()}finally{for(let a of o)a.off("page",s);for(let a of r)try{a.off("dialog",n)}catch{}}}async start(){let t=this.options.browser??"chromium";if(t==="chromium"||Pr[t]){let n=await kl();try{this.browser=await xl.launch({headless:!this.options.headed,channel:Pr[t]?t:void 0,args:[`--remote-debugging-port=${n}`]})}catch(r){let o=r instanceof Error?r.message:String(r),s=Pr[t];throw s&&/is not found at|distribution .* is not found/i.test(o)?new Dt(`${s.label} is not installed on this machine, so --browser ${t} cannot run.
61
- Install it (npx playwright install ${t} does that), or use --browser chromium, which Saffron ships with.`):r}this.cdpEndpoint=`http://127.0.0.1:${n}`}else this.browser=await(t==="firefox"?vl:Sl).launch({headless:!this.options.headed}),this.cdpEndpoint=void 0}async stop(){await this.browser?.close(),this.browser=void 0}resolveStorageState(){if(!this.options.storageState)return;let t=ie.isAbsolute(this.options.storageState)?this.options.storageState:ie.join(this.options.projectRoot,this.options.storageState);if(!mt.existsSync(t))throw new Error(`storageState file not found: ${t}. Generate one with: npx playwright open --save-storage=${this.options.storageState} <url>`);return t}buildCandidateCache(t,n){return{version:1,feature:t.featurePath,scenario:t.name,recordedAt:new Date().toISOString(),recordedBy:"agent",steps:n,...Wo(t)}}tabWatcher(){return t=>{let n=this.browser?.contexts()??[],r=()=>t();for(let o of n)o.on("page",r);return()=>{for(let o of n)o.off("page",r)}}}async proofReplay(t,n,r){let o=await this.browser.newContext({storageState:this.resolveStorageState()}),s=this.options.trace!==void 0&&this.options.trace!=="off";s&&await ir(o,`${t.displayName} (proof replay)`);let a=await o.newPage();return{result:await Ot(a,t,n,{baseURL:this.options.baseURL,actionTimeoutMs:this.options.actionTimeoutMs,retries:this.options.retries,pollIntervalMs:this.options.pollIntervalMs,uniqueSeed:r,trace:s}),context:o,page:a}}async runScenario(t,n={}){let r=await this.runScenarioInner(t,n),o=$e(t);if(o.size>0){let s=a=>a===void 0?a:Te(a,o);r.error=s(r.error),r.narrative=s(r.narrative),r.proofError=s(r.proofError),r.suggestedFeatureEdit=s(r.suggestedFeatureEdit),r.adaptations=r.adaptations.map(a=>Te(a,o));for(let a of r.stepResults)a.error=s(a.error)}return r}async screenshot(t,n,r,o){let s=this.options.screenshot??"viewport";if(s!=="off")try{let a=me(this.options.projectRoot,n.featurePath,n.displayName);this.claimArtifactDir(n);let i=ie.join(a,`${r}.jpg`),c=await t.screenshot({path:i,type:"jpeg",quality:80,fullPage:s==="full-page",timeout:5e3});return{kind:r,file:ie.relative(this.options.projectRoot,i).split(ie.sep).join("/"),step:o,sha256:Ve(c)}}catch{return}}claimArtifactDir(t){let n=me(this.options.projectRoot,t.featurePath,t.displayName);mt.mkdirSync(n,{recursive:!0}),mt.writeFileSync(ie.join(n,en),JSON.stringify({feature:t.featurePath,scenario:t.displayName},null,2))}async runScenarioInner(t,n={}){if(!this.browser)throw new Error("Orchestrator not started");let r=Date.now(),{projectRoot:o,baseURL:s,actionTimeoutMs:a,retries:i,pollIntervalMs:c}=this.options,p=J(o,t.featurePath,t.name),f;try{f=oe(p)}catch(g){if(!(g instanceof W))throw g;return{scenario:t,status:"red",source:"cache",stepResults:[],adaptations:[],usage:Ye,durationMs:Date.now()-r,error:`The recording cannot be read: ${g.problem}. Fix the file, or re-record it with saffron run --rerecord.`}}let d=ue(o,t.featurePath,t.name),m,h;if(!f&&mt.existsSync(d))try{({proposal:m,revision:h}=un(d))}catch(g){if(!(g instanceof W)&&g.code!=="ENOENT")throw g;m=void 0}m&&Re(o,m,t)&&(m=void 0);let y=await this.browser.newContext({storageState:this.resolveStorageState()}),b=this.options.trace??"off",k=b!=="off";k&&await ir(y,t.displayName);let w=!k,v=await y.newPage(),l=We(),u=[];mt.rmSync(me(o,t.featurePath,t.displayName),{recursive:!0,force:!0});let x=(g,S)=>this.screenshot(g,t,"before-heal",S.stepText).then(T=>{T&&u.push(T)});try{let g=await(async()=>{let T;if(m){let P=await Ot(v,t,m.cache,{baseURL:s,actionTimeoutMs:a,retries:i,pollIntervalMs:c,uniqueSeed:l,trace:k,tracePhase:"pending"}),j=m.meta.recordedFor??t.name;if(P.status==="passed")return{scenario:t,status:"yellow",source:"agent-record",proposalReplay:!0,stepResults:P.stepResults,narrative:`Replayed the pending proposal recorded for "${j}" at zero AI; accept it to go green.`,adaptations:[],verified:!0,proposalFile:d,proposalRevision:h,usage:Ye,durationMs:Date.now()-r};if(Object.keys(t.params).length>0&&j!==t.displayName){let _=P.failure;return{scenario:t,status:"red",source:"agent-record",proposalReplay:!0,stepResults:P.stepResults,adaptations:[],proposalFile:d,proposalRevision:h,usage:Ye,durationMs:Date.now()-r,error:`The pending proposal recorded for "${j}" does not replay for this Examples row`+(_?` (step "${_.stepText}": ${_.error.split(`
62
- `)[0]})`:"")+". A row value is probably baked in as a literal instead of a <placeholder>; inspect the proposal, reject it, and re-record with saffron run --rerecord."}}console.log(` \u21BA pending proposal for "${t.displayName}" did not replay`+(P.failure?` (step "${P.failure.stepText}": ${P.failure.error.split(`
63
- `)[0]})`:"")+"; recording again"),await v.goto("about:blank")}if(f&&(T=await Ot(v,t,f,{baseURL:s,actionTimeoutMs:a,retries:i,pollIntervalMs:c,uniqueSeed:l,trace:k,onFailure:x}),T.status==="passed"))return{scenario:t,status:"green",source:"cache",stepResults:T.stepResults,adaptations:[],usage:Ye,durationMs:Date.now()-r};if(!(this.options.agent!==void 0&&this.cdpEndpoint!==void 0&&!n.agentDisabled)){let P=this.options.agent!==void 0&&this.cdpEndpoint===void 0&&!n.agentDisabled?`recording/healing needs a Chromium-family browser (the agent attaches over CDP). Record with --browser chromium (the default), chrome or msedge, then replay with --browser ${this.options.browser}.`:"no agent is configured.";return{scenario:t,status:"red",source:"cache",stepResults:T?.stepResults??[],adaptations:[],usage:Ye,durationMs:Date.now()-r,error:f?T?.status==="stale"?`Cache is stale (feature file changed) and ${P}`:`Replay failed and ${P} ${T?.failure?.error??""}`.trimEnd():`No cache exists and ${P}`}}let E=f!==void 0&&T!==void 0&&T.status==="failed",C=0,I;try{if(E){let P=await Sn(this.browser,T.activePage??Mt(v.context(),T.failure.pageUrl)??v);I=await this.withDialogsVisible(()=>this.options.agent.run({scenario:t,baseURL:s,traceContext:k?v.context():void 0,mode:"heal",liveTab:P,model:this.options.healModel,cdpEndpoint:this.cdpEndpoint,assertionPolicy:this.options.assertionPolicy,completedSteps:T.stepResults.filter(j=>j.status==="passed").map(j=>j.gherkin),failedStep:{text:T.failure.stepText,kind:T.failure.kind,error:T.failure.error,pageUrl:T.failure.pageUrl,stepIndex:T.failure.stepIndex},uniqueSeed:l,onTabOpened:this.tabWatcher(),takeFingerprint:async()=>{let j=yt(this.browser)??v;return Je(j)}}))}else if(await v.goto("about:blank"),this.options.reuseSteps!==!1){let P=await this.recordWithSeeding(t,v,l);C=P.seededCount,I=P}else I=await this.withDialogsVisible(()=>this.options.agent.run({scenario:t,baseURL:s,traceContext:k?v.context():void 0,mode:"record",cdpEndpoint:this.cdpEndpoint,assertionPolicy:this.options.assertionPolicy,uniqueSeed:l,onTabOpened:this.tabWatcher(),takeFingerprint:async()=>{let P=yt(this.browser)??v;return Je(P)}}))}catch(P){let j=P instanceof Error?P.message:String(P);return{scenario:t,status:"red",source:E?"agent-heal":"agent-record",stepResults:T?.stepResults??[],adaptations:[],usage:Ye,durationMs:Date.now()-r,error:`Agent invocation failed: ${j.split(`
64
- `)[0]}`}}if(I.success&&E&&T.failure.kind==="assertion"&&(this.options.assertionPolicy!=="adaptable-mid"||Jo(f.steps,T.failure.stepIndex))){let P=f.steps[T.failure.stepIndex],j=yt(this.browser)??v,L=new Map(Object.entries(T.captured)),_=wr(t,T.failure.stepIndex);try{for(let $ of P.actions)await yr(j,$,_,this.options.actionTimeoutMs??5e3,s,L)}catch($){let O=$ instanceof Error?$.message:String($),R=`Assertion failed as written: "${P.gherkin}". Saffron never adapts assertions. If this change is intended, update the .feature file or re-record the scenario (saffron run --rerecord --filter @tag). If the recording itself looks wrong (an empty value, a wrong target), re-record it the same way. Verification error: ${O.split(`
65
- `)[0]}`,F=T.failure.stepIndex,z=T.stepResults.map((Q,K)=>K===F?{...Q,status:"failed",error:R}:Q);return{scenario:t,status:"red",source:"agent-heal",stepResults:z,executionStepResults:z.map((Q,K)=>K>F&&Q.status==="skipped"?{...Q,status:"passed"}:Q),narrative:I.narrative,adaptations:I.adaptations,suggestedFeatureEdit:I.suggestedFeatureEdit,usage:I.usage,durationMs:Date.now()-r,error:R}}}return await this.finalizeAgentRun(t,I,E,f,r,C,E?T?.failure:void 0,l)})();if(g.status==="red"){let T=u.find(A=>A.kind==="before-heal");if(g.source==="cache"&&T){let A=T.file.replace(/before-heal\.jpg$/,"failure.jpg");try{mt.renameSync(ie.join(o,T.file),ie.join(o,A)),T.file=A}catch{}T.kind="failure"}else{let A=g.stepResults.find(C=>C.status==="failed")?.gherkin,E=await this.screenshot(yt(this.browser)??v,t,"failure",A);E&&u.push(E)}}let S=ut(X(t),l);if(S.size>0&&(g.uniques=Object.fromEntries(S)),u.push(...g.evidence??[]),k){let T=b==="on"||g.status!=="green",A=me(o,t.featurePath,t.displayName),E=T?ie.join(A,ar):void 0;w=!0,E||await cr(y).catch(()=>{});let C=E?await lr(y,E,$e(t)):void 0;if(E&&C){this.claimArtifactDir(t);let I=$e(t);u.unshift({kind:"trace",file:ie.relative(o,E).split(ie.sep).join("/"),sha256:C,traceSteps:(g.executionStepResults??g.stepResults).map(P=>({...P,error:P.error&&Te(P.error,I)}))})}}return u.length>0&&(g.status!=="green"||u.some(T=>T.kind==="trace"))&&(g.evidence=u),g}finally{w||await cr(y).catch(()=>{}),await y.close()}}async finalizeAgentRun(t,n,r,o,s,a=0,i,c=We()){let p=r?"agent-heal":"agent-record",f=n.success?n.recordedSteps.map(R=>({gherkin:R.gherkin,kind:R.kind,status:"passed"})):Xo(n.recordedSteps,n.announcedSteps??[],i&&{stepIndex:i.stepIndex,error:i.error},n.narrative);if(!n.success)return{scenario:t,status:"red",source:p,stepResults:f,narrative:n.narrative,adaptations:n.adaptations,usage:n.usage,durationMs:Date.now()-s,error:n.narrative};let d=r?Go(o.steps,n.recordedSteps,this.options.assertionPolicy==="adaptable-mid",i?{stepIndex:i.stepIndex,actionIndex:i.actionIndex}:void 0):n.recordedSteps,m=[...n.adaptations,...r&&i?Ko(n.recordedSteps,i.stepIndex):[]],h={...n.usage},y,b,k,w,v=[],l=0,u=new Set,x,g=async(R,F,z=F.stepResults)=>{if(!u.has(R)){u.add(R);try{if(this.options.trace&&this.options.trace!=="off"){let Q=ie.join(me(this.options.projectRoot,t.featurePath,t.displayName),`proof-${++l}.zip`),K=$e(t),ge=await lr(R,Q,K);if(ge){this.claimArtifactDir(t);let Vt=F.captured[le],rs=Vt?ut(X(t),Vt):new Map;v.push({kind:"trace",proof:!0,file:ie.relative(this.options.projectRoot,Q).split(ie.sep).join("/"),sha256:ge,traceSteps:z.map(Dn=>({...Dn,error:Dn.error&&Te(Dn.error,K)})),traceUniques:rs.size?Object.fromEntries(rs):void 0})}}}finally{await R.close()}}};if(this.options.verifyProposals!==!1){let R=await this.proofReplay(t,this.buildCandidateCache(t,d),We());w=R.result.captured[le];try{if(R.result.status!=="passed"&&R.result.failure){let F=R.result.failure;if(this.options.agent&&!(r&&F.kind==="assertion"&&(this.options.assertionPolicy!=="adaptable-mid"||Jo(o.steps,F.stepIndex))))try{let Q=await Sn(this.browser,R.result.activePage??Mt(R.page.context(),F.pageUrl)??R.page),K=await this.withDialogsVisible(()=>this.options.agent.run({scenario:t,baseURL:this.options.baseURL,traceContext:this.options.trace&&this.options.trace!=="off"?R.context:void 0,mode:"refine",model:r?this.options.healModel:void 0,cdpEndpoint:this.cdpEndpoint,assertionPolicy:this.options.assertionPolicy,completedSteps:R.result.stepResults.filter(ge=>ge.status==="passed").map(ge=>ge.gherkin),failedStep:{text:F.stepText,kind:F.kind,error:F.error,pageUrl:F.pageUrl,stepIndex:F.stepIndex},liveTab:Q,uniqueSeed:R.result.captured[le],onTabOpened:this.tabWatcher(),takeFingerprint:async()=>Je(yt(this.browser)??R.page)}));h=Yo(h,K.usage),x=Rl(R.result.stepResults,F,K),K.success&&(m.push(...K.adaptations),m.push(...Ko(K.recordedSteps,F.stepIndex)),d=r?Go(d,K.recordedSteps,this.options.assertionPolicy==="adaptable-mid",{stepIndex:F.stepIndex,actionIndex:0}):Tl(d,K.recordedSteps,F.stepIndex),await g(R.context,R.result,x),x=void 0,R=await this.proofReplay(t,this.buildCandidateCache(t,d),We()),w=R.result.captured[le])}catch{}}for(let F of R.result.tabOpeners??[]){let z=d[F.stepIndex]?.actions[F.actionIndex];z&&(z.opensTab=!0)}y=R.result.status==="passed",y||(b=R.result.failure?.error??`proof replay ${R.result.status}`,k=R.result.stepResults)}finally{await g(R.context,R.result,x).catch(()=>{})}}r&&o&&m.push(...$l(o.steps,d,i,this.options.baseURL));let S=[...n.featureEdits];for(let R of bs(d,ye(this.options.projectRoot)))m.push(`duplicate wording: "${R.from}" recorded exactly the same actions as the existing step "${R.to}" (${R.feature} \u203A ${R.scenario}). Accept with --with-feature-edit to converge on the canonical wording.`),S.some(F=>F.index===R.index)||S.push({index:R.index,text:R.to});let T=No(d);T.length>0&&(y=!1,b=[b,...T].filter(Boolean).join(" "));let A=$e(t),E=R=>A.size>0?Te(R,A):R;m=m.map(E);let C=S.map(R=>({index:R.index,text:E(R.text)})),I=E(n.narrative),P=n.suggestedFeatureEdit?E(n.suggestedFeatureEdit):void 0,j={version:1,feature:t.featurePath,scenario:t.name,recordedAt:new Date().toISOString(),recordedBy:"agent",steps:d,...Wo(t)},L={meta:{createdAt:j.recordedAt,mode:r?"heal":"record",narrative:I,adaptations:m,suggestedFeatureEdit:P,featureEdits:C.length>0?C:void 0,verified:y,proofError:b?E(b):void 0,seededSteps:a>0?a:void 0,recordedFor:t.displayName,usage:h,bound:uo(t,o,j,C.length>0?C:void 0)},cache:j},_=ue(this.options.projectRoot,t.featurePath,t.name),$=Is(_,L),O=oe(J(this.options.projectRoot,t.featurePath,t.name));return{proposalDiff:gt(O,L.cache),scenario:t,status:"yellow",source:p,stepResults:k??f,executionStepResults:f,evidence:v,proofUniques:(()=>{let R=w?ut(X(t),w):void 0;return R&&R.size>0?Object.fromEntries(R):void 0})(),narrative:I,adaptations:m,suggestedFeatureEdit:P,featureEdits:C.length>0?C:void 0,verified:y,proofError:b,seededSteps:a>0?a:void 0,proposalFile:_,proposalRevision:$,usage:h,durationMs:Date.now()-s}}};import fa from"node:path";import{createRequire as ea}from"node:module";var El=ea(import.meta.url),$n=ea(El.resolve("playwright/package.json"));import{Client as Al}from"@modelcontextprotocol/sdk/client/index.js";import{McpServer as Il}from"@modelcontextprotocol/sdk/server/mcp.js";import{InMemoryTransport as Pl}from"@modelcontextprotocol/sdk/inMemory.js";import{CallToolRequestSchema as Cl,ListToolsRequestSchema as _l}from"@modelcontextprotocol/sdk/types.js";var jl=1e8,Ol=1e4,Fl=5e3,ta="saffron_connection_check",Tn=e=>new Promise(t=>{setTimeout(t,e).unref()}),Lt=new Set,_r=0,na=e=>{_r===0&&[...Lt].at(-1)?.(e)};function Nl(e){let t=n=>e(n);return Lt.size===0&&process.on("unhandledRejection",na),Lt.add(t),()=>{Lt.delete(t)&&Lt.size===0&&process.off("unhandledRejection",na)}}async function jr(e,t,n,r){await e.tracing.group(`[saffron:agent:${t.index}] ${t.title}`).catch(()=>{});try{await e.tracing.group(n).catch(()=>{});try{return await r()}finally{await e.tracing.groupEnd().catch(()=>{})}}finally{await e.tracing.groupEnd().catch(()=>{})}}function Ml(e,t={}){let n=s=>typeof s=="string"&&s.trim()?s.trim():void 0,r=n(t.element)??"an element",o=(()=>{switch(e){case"browser_snapshot":return"read the page";case"browser_find":return n(t.text)?`find "${n(t.text)}" on the page`:n(t.regex)?`find ${n(t.regex)} on the page`:"search the page";case"browser_click":return`${t.doubleClick?"double-click":"click"} ${r}`;case"browser_hover":return`hover ${r}`;case"browser_type":return`type into ${r}`;case"browser_fill_form":return"fill in the form";case"browser_select_option":return`select in ${r}`;case"browser_press_key":return n(t.key)?`press ${n(t.key)}`:"press a key";case"browser_navigate":return n(t.url)?`go to ${n(t.url)}`:"navigate";case"browser_navigate_back":return"go back";case"browser_wait_for":return n(t.text)?`wait for "${n(t.text)}"`:n(t.textGone)?`wait for "${n(t.textGone)}" to go`:`wait ${t.time??""}s`.replace(" s","");case"browser_tabs":return n(t.action)?`tabs: ${n(t.action)}`:"list the tabs";case"browser_drag":return`drag ${n(t.startElement)??"an element"} to ${n(t.endElement)??"an element"}`;case"browser_file_upload":return"upload files";case"browser_handle_dialog":return`${t.accept?"accept":"dismiss"} the dialog`;case"browser_evaluate":case"browser_run_code":case"browser_run_code_unsafe":return"run a script in the page";case"browser_take_screenshot":return"take a screenshot";case"browser_console_messages":return"read the console";case"browser_network_requests":return"read the network log";default:return}})();return o?`${o} \xB7 ${e}`:e}async function ra(e,t,n="none",r=()=>{}){let{createConnection:o,resolveCLIConfigForMCP:s}=$n("playwright-core/lib/coreBundle").tools,a=await s?.({},process.env)??{},i={...a,browser:{...a.browser,isolated:!1},sharedBrowserContext:!0,imageResponses:"omit",snapshot:{...a.snapshot,mode:n}},c=e.browser(),p=new Set(c?.listeners("disconnected")??[]),f=new Set,d=g=>{f.add(g)},m=g=>{g.on("download",d)};for(let g of e.pages())m(g);e.on("page",m);let h=Nl(r),y=g=>{e.off("page",m);for(let E of e.pages())E.off("download",d);for(let E of c?.listeners("disconnected")??[])p.has(E)||c.off("disconnected",E);if(!g)return h();let S=()=>{clearTimeout(A),e.off("close",T),h()},T=()=>{setTimeout(S,1e3).unref()},A=setTimeout(S,3e5);A.unref(),e.on("close",T)},b,k=new Al({name:"saffron-trace",version:"1"});try{b=await o(i,async()=>e);let[g,S]=Pl.createLinkedPair();await b.connect(S),await k.connect(g);let A=((await k.callTool({name:ta,arguments:{}})).content??[]).map(E=>E.text??"").join(`
66
- `);if(!A.includes(ta))throw new Error(A.replace(/^### Error\s*/,"").trim()||"the browser backend did not start")}catch(g){throw await Promise.allSettled([k.close(),b?.close()]),y(!1),g}let w=new Il({name:"playwright",version:"1"},{capabilities:{tools:{}}});w.server.setRequestHandler(_l,()=>k.listTools());let v=Promise.resolve(),l=(g,S,T=t())=>{let A=v.then(()=>jr(e,T,g,S));return v=A.catch(()=>{}),A};w.server.setRequestHandler(Cl,(g,S)=>l(Ml(g.params.name,g.params.arguments),()=>k.callTool(g.params,void 0,{timeout:jl,signal:S.signal})));let u=async()=>{let g=[...f];if(g.length===0)return;let S=Promise.all(g.map(A=>A.path().then(()=>!0,()=>!0))),T=!await Promise.race([S,Tn(Fl).then(()=>!1)]);T&&_r++;try{T&&(await Promise.allSettled(g.map(A=>A.cancel())),await Promise.allSettled(g.map(A=>A.failure()))),await Tn(0),await Tn(0)}finally{T&&_r--}};return{config:{type:"sdk",name:"playwright",instance:w},close:async()=>{await Promise.race([v,Tn(Ol)]),await u().catch(()=>{}),await Promise.allSettled([k.close()]),await Promise.allSettled([b.close()]),await Promise.allSettled([w.close()]),y(!0)},grouped:(g,S)=>l(g,S)}}import{createRequire as Wl}from"node:module";import{query as Jl}from"@anthropic-ai/claude-agent-sdk";import{createSdkMcpServer as Dl,tool as bt}from"@anthropic-ai/claude-agent-sdk";import{z as B}from"zod";var te=e=>({content:[{type:"text",text:e}]});function sa(e){let t={};return e.role&&(t.role=e.role),e.name&&(t.name=e.name),e.nameRegex&&(t.nameRegex=e.nameRegex),e.label&&(t.label=e.label),e.text&&(t.text=e.text),e.selector&&(t.fallbackSelectors=[e.selector]),e.frame&&(t.frame=e.frame),Object.keys(t).length>0?t:void 0}var Ll=new Set(["main","banner","contentinfo","status","alert","alertdialog","dialog","search"]);function oa(e){if(!e?.role)return;let{role:t,frame:n,...r}=e;if(!(Object.keys(r).length>0||Ll.has(t)))return`ERROR: role="${t}" alone matches every ${t} on the page, and replay would use the first one. Add a name (or nameRegex), text, label or selector that picks exactly the element you checked, then call again.`}function Ul(e,t){return t.index>=e.steps.length?`ERROR: there is no step ${t.index}: this scenario's steps are numbered from 0 to ${e.steps.length-1}. Call saffron_step again with the zero-based index of the step you are about to perform.`:(e.announce(t.index,t.text,t.adaptation),t.index<e.firstStep?`Step ${t.index} already ran before you took over and keeps its recording; nothing you do under it is recorded. Do not redo it: continue from step ${e.firstStep} on the page as it is.`:`Recording step ${t.index}: ${t.text}`)}var aa={role:B.string().optional().describe("ARIA role of the element"),name:B.string().optional().describe("Accessible name \u2014 use the SHORTEST stable substring; never include volatile content like prices or dates"),nameRegex:B.string().optional().describe("Regex for the accessible name when it embeds volatile content, e.g. 'Select \\\\{date\\\\+1\\\\} as checkin' cannot work \u2014 instead use nameRegex 'Select .* as checkin'"),label:B.string().optional().describe("Form-label text for getByLabel \u2014 the literal <label> text only, never a free-text description"),text:B.string().optional().describe("getByText locator text"),selector:B.string().optional().describe("CSS fallback selector"),frame:B.string().optional().describe(`CSS selector of the iframe hosting the element, e.g. 'iframe[title="Feedback widget"]' \u2014 REQUIRED whenever the element lives inside an iframe; without it replay looks only at the top-level page`)};function ia(e){return Dl({name:"saffron",version:"0.1.0",tools:[bt("saffron_step","Announce the Gherkin step you are about to perform. MUST be called before executing browser actions for each step, with the step's zero-based index and exact text.",{index:B.number().int().min(0),text:B.string(),adaptation:B.string().optional().describe("If the written step does not match the real UI and you are deviating from it, describe the deviation in one sentence: the kind of change (a locator or label change on the same page, a page removed or merged, a page added or flow reordered) and the page it happened on.")},async t=>te(Ul(e,t))),bt("saffron_capture",`Record a captureText action for the current step: at replay time, the target element's text is read and stored under saveAs for later expectDiffers comparisons. Use for steps like 'I record the displayed price as "X"'. The target must select exactly the element whose text is the value.`,{saveAs:B.string().describe("Name to store the captured text under"),...aa},async t=>{let n=sa(t);if(!n)return te("ERROR: capture needs a target (role/name, nameRegex, label, text, or selector).");let r=oa(n);return r?te(r):(e.addActions([{action:"captureText",target:n,saveAs:t.saveAs}]),te(`Capture recorded as "${t.saveAs}".`))}),bt("saffron_dialog","Record how a native dialog (alert/confirm/prompt) must be handled at replay. Call this IMMEDIATELY whenever an action triggers a native dialog \u2014 whether you handled it via browser_handle_dialog or the browser reported it as already handled. At replay Saffron arms this handler BEFORE the triggering action, so without this call the replay will auto-dismiss the dialog and diverge from what you saw.",{accept:B.boolean().describe("true = accept/OK the dialog, false = dismiss/cancel"),promptText:B.string().optional().describe("Text typed into a prompt() before accepting")},async t=>(e.addArmedAction({action:"handleDialog",value:t.accept?"accept":"dismiss",...t.promptText?{promptText:t.promptText}:{}}),te(`Dialog handling recorded (${t.accept?"accept":"dismiss"}).`))),bt("saffron_wait_api","Record a network wait for the current step: replay will block until a matching API response is observed (responses from the triggering step count). Call AFTER you verified via browser_network_requests that the response actually occurred with the expected status/body. Use a path-only urlPattern regex and never literal volatile IDs (/api/orders/\\d+, not /api/orders/8842).",{urlPattern:B.string().describe("Regex matched against the response URL \u2014 path-only for portability"),method:B.string().optional().describe("HTTP method to match (any if omitted)"),status:B.string().optional().describe("Expected status \u2014 exact ('201') or class ('2xx'); default any 2xx"),bodyPattern:B.string().optional().describe(`Regex the response body must match, e.g. '"status"\\s*:\\s*"READY"' for polling waits`)},async t=>(e.addActions([{action:"waitForResponse",urlPattern:t.urlPattern,...t.method?{method:t.method}:{},...t.status?{status:t.status}:{},...t.bodyPattern?{pattern:t.bodyPattern}:{}}]),te("Network wait recorded."))),bt("saffron_assert","Record a verified assertion for the current Then step. Call ONLY after you have confirmed the condition holds via a page snapshot. If the condition does NOT hold, do not call this \u2014 call saffron_done with success=false instead. NEVER record volatile content (prices, dates, counters) as literal expected text \u2014 use kind=matches with a pattern, or kind=differs against captured values.",{kind:B.enum(["visible","notVisible","text","url","value","differs","matches","attribute","response","each"]),...aa,expected:B.string().optional().describe("Expected text/value for kind=text|value \u2014 stable text only, never volatile values"),url:B.string().optional().describe("URL substring for kind=url (path only, no host)"),pattern:B.string().optional().describe("kind=matches|attribute: regex the target's text (or attribute) must match, e.g. 'NOK [\\\\d,]+' for a price"),attribute:B.string().optional().describe("kind=attribute: attribute name to check, e.g. 'href' for link-target assertions \u2014 use this for 'X should link to Y' instead of clicking through"),capture:B.string().optional().describe("kind=differs: name of the captured value on the LEFT of the comparison (omit when comparing the target element's live text)"),urlPattern:B.string().optional().describe("kind=response: regex matched against the response URL \u2014 path-only for portability, never literal volatile IDs (use /api/orders/\\d+)"),method:B.string().optional().describe("kind=response: HTTP method to match (any if omitted)"),status:B.string().optional().describe("kind=response: expected status \u2014 exact ('201') or class ('2xx'); default any 2xx"),bodyPattern:B.string().optional().describe("kind=response: regex the response body must match (verify via browser_network_requests first)"),each:B.string().optional().describe("kind=each: the data-list token exactly as the step writes it, e.g. '{data:enums.OrderStatus}'. The target is the CONTAINER (dropdown, table, list) that must show every value of that list. Never pass the values themselves."),compareTo:B.string().optional().describe("kind=differs: name of the captured value to compare against")},async t=>{let n=sa(t),r=oa(n);if(r)return te(r);let o;switch(t.kind){case"visible":o={action:"expectVisible",target:n};break;case"notVisible":o={action:"expectNotVisible",target:n};break;case"text":o={action:"expectText",target:n,value:t.expected??""};break;case"value":o={action:"expectValue",target:n,value:t.expected??""};break;case"url":o={action:"expectUrl",url:t.url??""};break;case"differs":if(!t.compareTo)return te("ERROR: kind=differs requires compareTo.");if(!t.capture&&!n)return te("ERROR: kind=differs needs either capture (a stored name) or a target element.");o={action:"expectDiffers",target:n,value:t.capture,compareTo:t.compareTo};break;case"matches":if(!t.pattern)return te("ERROR: kind=matches requires pattern.");o={action:"expectMatches",target:n,pattern:t.pattern};break;case"attribute":if(!t.attribute||!t.pattern)return te("ERROR: kind=attribute requires attribute and pattern.");o={action:"expectAttribute",target:n,attribute:t.attribute,pattern:t.pattern};break;case"each":if(!t.each||!/^\{data:[^}]+\}$/.test(t.each))return te("ERROR: kind=each requires each='{data:file.list}', the token exactly as the step writes it (not the values).");o={action:"expectEach",target:n,each:t.each};break;case"response":if(!t.urlPattern)return te("ERROR: kind=response requires urlPattern.");o={action:"expectResponse",urlPattern:t.urlPattern,...t.method?{method:t.method}:{},...t.status?{status:t.status}:{},...t.bodyPattern?{pattern:t.bodyPattern}:{}};break}return!["url","differs","response"].includes(t.kind)&&!n?te("ERROR: assertion needs a target (role/name, nameRegex, label, text, or selector)."):(e.addActions([o]),te("Assertion recorded."))}),bt("saffron_done","Finalize the run. Call exactly once, after the final step: success=true only if every step's intent was achieved and every assertion verified.",{success:B.boolean(),narrative:B.string().describe("2-4 sentence summary of the run: what happened, and why it failed if it did. After a heal, say what kind of change the app went through (a locator change, or a page removed, merged, added or reordered) and on which page."),suggestedFeatureEdit:B.string().optional().describe("If you adapted any steps: the corrected Gherkin for those steps, as you would rewrite them in the .feature file (free text, for the human report)."),featureEdits:B.array(B.object({index:B.number().int().min(0).describe("zero-based step index"),text:B.string().describe("replacement step text WITHOUT the Given/When/Then keyword")})).optional().describe("Machine-applicable version of the suggested edits: one entry per adapted step. Provide this whenever you adapted a step.")},async t=>(e.done=!0,e.success=t.success,e.narrative=t.narrative,e.suggestedFeatureEdit=t.suggestedFeatureEdit,e.featureEdits=t.featureEdits??[],te("Run finalized.")))]})}function la(e={}){let t=e.deliberateSnapshots?`
56
+ Invoke it anywhere with: StepSet Name`,filterText:"step set stepset:",sortText:"0",insertTextFormat:Ao.Snippet,textEdit:{range:i,newText:"StepSet: ${1:Name}\n "}}];r.stepSets.length===0&&c.push({label:"StepSet \u2026",kind:Le.Keyword,detail:"invoke a step set (none defined yet)",filterText:"step set stepset",sortText:"1",insertTextFormat:Ao.Snippet,textEdit:{range:i,newText:"StepSet ${1:Name}"}});for(let l of r.stepSets)c.push({label:`StepSet ${l.name}`,kind:Le.Function,detail:`invoke step set \xB7 ${l.steps.length} step(s)`,documentation:l.steps.join(`
57
+ `),filterText:`step set ${l.name}`,sortText:`1${l.name}`,textEdit:{range:i,newText:`StepSet ${l.name}`}});return c}return[]}import mt from"node:fs";import Tl from"node:net";import ie from"node:path";import{chromium as Rl,firefox as El,webkit as Al}from"playwright";var yl=5e3,bl=new Set(["click","press","selectOption","check","uncheck","hover","dragTo"]);function Do(e){return e.mode==="heal"||e.mode==="refine"}function Lo(e){return e.mode==="heal"||e.mode==="refine"?e.failedStep?.stepIndex??e.completedSteps?.length??0:e.recordOnly?e.recordOnly.from:e.completedSteps?.length??0}var No=e=>`${e} action${e===1?"":"s"}`,Mo=e=>e.map(ce).join("; "),$n=class{firstStep;discardBeforeAnnounce;preAnnounce=[];discarded=[];carried;pendingTabOpen=!1;pendingTabFallback;lastOpener;lastOpenerAt=0;inFlight=new Set;steps;currentStepIndex=-1;done=!1;success=!1;narrative="";adaptations=[];suggestedFeatureEdit;featureEdits=[];announced=new Set;pendingFingerprint;constructor(t,n={}){this.firstStep=Math.max(n.firstStep??0,0),this.discardBeforeAnnounce=n.discardBeforeAnnounce??!1,this.steps=t.steps.map(r=>({gherkin:r.text,keyword:r.keyword,kind:r.kind,table:r.table,docString:r.docString,fromStepSet:r.source?.fromStepSet,actions:[]}))}addArmedAction(t){if(this.done)return;let n=this.destination();if(!n){this.discard([t],this.refusalReason());return}if(n.some(o=>o.action===t.action&&o.value===t.value&&o.promptText===t.promptText))return;let s=Math.max(n.length-1,0);n.splice(s,0,t)}destination(){if(this.currentStepIndex<0)return this.discardBeforeAnnounce?void 0:this.firstStep===0?this.steps[0]?.actions:this.preAnnounce;if(!(this.currentStepIndex<this.firstStep))return this.steps[this.currentStepIndex]?.actions}refusalReason(){let t=this.currentStepIndex;return t<0?`before announcing the failed step (step ${this.firstStep+1}); a heal records nothing until it announces that step, so this exploration was not filed under any step`:`under step ${t+1} ("${this.steps[t]?.gherkin??""}"), which had already run before the agent took over, so its recording was kept`}discard(t,n){if(t.length===0)return;let r=this.discarded.find(s=>s.why===n);r?r.actions.push(...t):this.discarded.push({actions:[...t],why:n})}announce(t,n,r){this.currentStepIndex=t,this.announced.add(t),r&&this.adaptations.push(`Step ${t+1} ("${n}"): ${r}`);let s=this.steps[t];this.preAnnounce.length>0&&t>=this.firstStep&&s&&(s.actions.unshift(...this.preAnnounce),this.carried={index:t,actions:this.preAnnounce},this.preAnnounce=[])}attributionNotes(){let t=[];if(this.carried){let{index:r,actions:s}=this.carried;t.push(`recording: ${No(s.length)} the agent performed before announcing a step ${s.length===1?"was":"were"} filed under step ${r+1} ("${this.steps[r]?.gherkin??""}"), the first step it recorded: ${Mo(s)}.`)}let n=this.preAnnounce.length>0?[{actions:this.preAnnounce,why:"before announcing a step this session records"}]:[];for(let{actions:r,why:s}of[...this.discarded,...n])t.push(`recording: discarded ${No(r.length)} the agent performed ${s}: ${Mo(r)}.`);return t}beginToolCall(t){this.inFlight.add(t)}endToolCall(t,n){this.inFlight.delete(t)&&(this.pendingTabOpen&&n&&(this.pendingTabFallback=void 0),this.inFlight.size===0&&this.pendingTabOpen&&(this.pendingTabFallback&&(this.pendingTabFallback.opensTab=!0),this.pendingTabOpen=!1,this.pendingTabFallback=void 0))}markTabOpened(){let t=this.inFlight.size===0,n=Date.now()-this.lastOpenerAt<=yl;if(t){this.lastOpener&&n&&(this.lastOpener.opensTab=!0);return}this.pendingTabOpen=!0,this.pendingTabFallback=n?this.lastOpener:void 0}addActions(t){if(this.done||t.length===0)return;if(this.currentStepIndex<0&&this.firstStep>0&&!this.discardBeforeAnnounce){let s=t.filter(o=>o.action==="switchTab");if(this.discard(s,"before announcing its first step (replay is already on the live page when the agent takes over)"),t=t.filter(o=>o.action!=="switchTab"),t.length===0)return}let n=this.destination();if(!n){this.pendingFingerprint=void 0,this.discard(t,this.refusalReason());return}this.pendingFingerprint&&t[0]&&(t[0].pageFingerprint=this.pendingFingerprint,this.pendingFingerprint=void 0),n.push(...t);let r=[...t].reverse().find(s=>bl.has(s.action));r&&(this.lastOpener=r,this.lastOpenerAt=Date.now(),this.pendingTabOpen&&(r.opensTab=!0,this.pendingTabOpen=!1,this.pendingTabFallback=void 0))}},Se=e=>e.replace(/\\(['"\\])/g,"$1");function wl(e){let t=e.match(/'((?:[^'\\]|\\.)*)'/);return t?Se(t[1]):void 0}function xl(e){let t=[],n=0;for(;n<e.length;){let r=/^\.?([A-Za-z]+)\(/.exec(e.slice(n));if(!r)return;let s=n+r[0].length,o=s,a=1,i;for(;s<e.length&&a>0;s++){let c=e[s];if(i)c==="\\"?s++:c===i&&(i=void 0);else if(c==="'"||c==='"'||c==="`")i=c;else if(c==="/"&&/[(,:]\s*$/.test(e.slice(o,s)))for(s++;s<e.length&&e[s]!=="/";s++)e[s]==="\\"&&s++;else c==="("||c==="{"||c==="["?a++:(c===")"||c==="}"||c==="]")&&a--}if(a!==0)return;t.push({method:r[1],args:e.slice(o,s-1)}),n=s}return t}function Cr(e,t){let n=/^'((?:[^'\\]|\\.)*)'$/.exec(e.trim());if(n)return`${JSON.stringify(Se(n[1]))}${t?"s":"i"}`;let r=/^\/((?:[^/\\]|\\.)*)\/([a-z]*)$/.exec(e.trim());return r?`/${r[1]}/${r[2]}`:void 0}function Sl(e,t){let n=/exact:\s*true/.test(t),r=/^\s*('(?:[^'\\]|\\.)*'|\/(?:[^/\\]|\\.)*\/[a-z]*)/.exec(t)?.[1];switch(e){case"locator":{let s=/^\s*'((?:[^'\\]|\\.)*)'\s*$/.exec(t);return s?Se(s[1]):void 0}case"getByRole":{let s=/^\s*'([^']+)'/.exec(t)?.[1];if(!s)return;let o=/name:\s*('(?:[^'\\]|\\.)*'|\/(?:[^/\\]|\\.)*\/[a-z]*)/.exec(t)?.[1],a=o===void 0?"":Cr(o,n);return a===void 0?void 0:`internal:role=${s}${a?`[name=${a}]`:""}`}case"getByText":case"getByLabel":case"getByPlaceholder":case"getByAltText":case"getByTitle":{let s=r===void 0?void 0:Cr(r,n);return s===void 0?void 0:e==="getByText"?`internal:text=${s}`:e==="getByLabel"?`internal:label=${s}`:`internal:attr=[${{getByPlaceholder:"placeholder",getByAltText:"alt",getByTitle:"title"}[e]}=${s}]`}case"getByTestId":{let s=r&&/^'/.test(r)?Se(r.slice(1,-1)):void 0;return s===void 0?void 0:`internal:testid=[data-testid=${JSON.stringify(s)}s]`}case"first":return"nth=0";case"last":return"nth=-1";case"nth":return/^\s*-?\d+\s*$/.test(t)?`nth=${t.trim()}`:void 0;case"filter":{let s=/hasText:\s*('(?:[^'\\]|\\.)*'|\/(?:[^/\\]|\\.)*\/[a-z]*)/.exec(t)?.[1],o=s===void 0?void 0:Cr(s,!1);return o!==void 0&&/^\s*\{\s*hasText:[^,]*\}\s*$/.test(t)?`internal:has-text=${o}`:void 0}default:return}}function Pr(e){let t={},n=e.match(/(?:frameLocator\('((?:[^'\\]|\\.)*)'\)|locator\('((?:[^'\\]|\\.)*)'\)\.contentFrame\(\))/);n&&(t.frame=Se(n[1]??n[2]),e=e.slice((n.index??0)+n[0].length));let r=xl(e.replace(/^\./,"")),s=r?.filter((c,l)=>!(c.method==="first"&&l===r.length-1));if(s&&s.length>1){let c=s.map(l=>Sl(l.method,l.args));return c.some(l=>l===void 0)?void 0:{...t,fallbackSelectors:[c.join(" >> ")]}}let o=e.match(/getByRole\('([^']+)'(?:,\s*\{([^}]*)\})?\)/);if(o){t.role=o[1];let c=(o[2]??"").match(/name:\s*'((?:[^'\\]|\\.)*)'/);c&&(t.name=Se(c[1]))}let a=[[/getByLabel\('((?:[^'\\]|\\.)*)'\)/,"label"],[/getByText\('((?:[^'\\]|\\.)*)'\)/,"text"],[/getByTestId\('((?:[^'\\]|\\.)*)'\)/,"testId"],[/getByPlaceholder\('((?:[^'\\]|\\.)*)'\)/,"placeholder"]];for(let[c,l]of a){let f=e.match(c);f&&(t[l]=Se(f[1]))}let i=e.match(/(?:^|\.)locator\('((?:[^'\\]|\\.)*)'\)/);return i&&(t.fallbackSelectors=[Se(i[1])]),Object.keys(t).length>0?t:void 0}var vl={click:"click",dblclick:"click",fill:"fill",pressSequentially:"fill",type:"fill",press:"press",check:"check",uncheck:"uncheck",hover:"hover",selectOption:"selectOption"};function kl(e,t){let n=e.match(/page\.goto\('((?:[^'\\]|\\.)*)'\)/);if(n)return{action:"goto",url:It(Se(n[1]),t)};let r=e.match(/page\.keyboard\.press\('((?:[^'\\]|\\.)*)'\)/);if(r)return{action:"press",value:Se(r[1])};let s=e.match(/await\s+page\.(.+)\.dragTo\(page\.(.+?)\);?\s*$/);if(s){let m=Pr(s[1]),h=Pr(s[2]);if(m&&h)return{action:"dragTo",target:m,toTarget:h}}let o=e.match(/await\s+page\.(.+)\.(click|dblclick|fill|pressSequentially|type|press|check|uncheck|hover|selectOption)\((.*)\);?\s*$/);if(!o)return;let[,a,i,c]=o,l=Pr(a);if(!l)return;let f={action:vl[i],target:l},d=wl(c);return d!==void 0&&i!=="click"&&i!=="dblclick"&&(f.value=d),f}function Uo(e){let t=Array.isArray(e)?e:e?.content,n=typeof e=="string"?[e]:Array.isArray(t)?t.flatMap(r=>typeof r?.text=="string"?[r.text]:[]):[];for(let r of n){let s=r.split(/^### Open tabs\r?$/m)[1]?.split(/^### /m)[0];if(!s)continue;let o=/^- (\d+): \(current\) /m.exec(s)?.[1];if(o!==void 0)return Number(o)}}function Bo(e,t){let n=[],r=Array.isArray(e)?e:e?.content;if(Array.isArray(r))for(let o of r){let a=o?.text;typeof a=="string"&&n.push(a)}else typeof e=="string"&&n.push(e);let s=[];for(let o of n)for(let a of o.split(`
58
+ `)){let i=kl(a.trim(),t);i&&s.push(i)}return $l(s)}function $l(e){return e.filter((t,n)=>{let r=e[n+1];return!(t.action==="fill"&&(t.value??"")===""&&r?.action==="fill"&&JSON.stringify(r.target)===JSON.stringify(t.target))})}function zo(e){let t=[];for(let n of e){if(n.kind==="assertion"&&n.actions.length===0){t.push(`Step "${n.gherkin}" is an assertion but recorded no check, so nothing would be verified. Re-record with: saffron run --rerecord`);continue}if(/"[^"]+"|<[^<>]+>/.test(n.gherkin)){for(let s of n.actions)if(s.action==="fill"&&(s.value??"")===""){t.push(`Step "${n.gherkin}" recorded a fill with an empty value although the step passes one; the typed value was lost. Re-record with: saffron run --rerecord`);break}}}return t}function _r(e,t){let n=e;for(let[r,s]of Object.entries(t).filter(([,o])=>o.length>=3).sort((o,a)=>a[1].length-o[1].length))n=n.split(s).join(`<${r}>`);return n}function qo(e,t){let n=Object.entries(t).filter(([,a])=>a.length>=3);if(n.length===0)return e;let r=a=>a===void 0?void 0:_r(a,t),s=[...n].sort((a,i)=>i[1].length-a[1].length),o=a=>{if(a!==void 0){for(let[i,c]of s)if(a.includes(c))return`<${i}>`;return a}};return e.map(a=>({...a,actions:a.actions.map(i=>{let c=pe.has(i.action)&&i.action!=="expectValue";return{...i,value:c?o(i.value):r(i.value),url:r(i.url),target:i.target?{...i.target,name:c?o(i.target.name):r(i.target.name),label:r(i.target.label),text:c?o(i.target.text):r(i.target.text),placeholder:r(i.target.placeholder)}:void 0}})}))}function Ho(e,t,n=r=>r){let r=_e(t.steps);return e.map((s,o)=>{let a=t.steps[o],i=a?.resolvedTable??a?.table,c=a?.resolvedDocString??a?.docString,l=Object.entries({...r,...ne(i),...ze(c)}).map(([d,m])=>[d,n(m)]).filter(([,d])=>d.length>=3).sort((d,m)=>m[1].length-d[1].length);if(l.length===0)return s;let f=d=>{if(d===void 0)return;let m=d;for(let[h,y]of l)m=m.split(y).join(`<${h}>`);return m};return{...s,actions:s.actions.map(d=>({...d,value:f(d.value),url:f(d.url),target:d.target?{...d.target,name:f(d.target.name),label:f(d.target.label),text:f(d.target.text),placeholder:f(d.target.placeholder)}:void 0}))}})}function Wo(e,t=new Date){return e.map(n=>({...n,actions:n.actions.map(r=>jr(r,s=>Us(s,t)))}))}function jr(e,t){let n=(o,a)=>{let i={...o};for(let c of a)typeof o[c]=="string"&&(i[c]=t(o[c]));return i},r=o=>{let a=n(o,["name","nameRegex","label","text","placeholder","testId","frame"]);return o.fallbackSelectors&&(a.fallbackSelectors=o.fallbackSelectors.map(t)),a},s=n(e,["value","url","pattern","urlPattern","promptText"]);return e.target&&(s.target=r(e.target)),e.toTarget&&(s.toTarget=r(e.toTarget)),s}function Vo(e,t,n=[]){if(t.size===0)return e;let r=Is(n,t),s=it(t,{standaloneBelow:6}),o=a=>s.mask(r(a));return e.map(a=>({...a,actions:a.actions.map(i=>jr(i,o))}))}function Jo(e,t){if(t.size===0)return e;let n=r=>typeof r=="string"?qs(r,t):Array.isArray(r)?r.map(n):r&&typeof r=="object"?Object.fromEntries(Object.entries(r).map(([s,o])=>[s,s==="pageFingerprint"?o:n(o)])):r;return e.map(r=>({...r,actions:r.actions.map(s=>n(s))}))}function Go(e,t){return t.size===0?e:e.map(n=>{let r=new Set([...n.gherkin.matchAll(St)].map(o=>o[1])),s=o=>fs(o,t,r);return{...n,actions:n.actions.map(o=>jr(o,s))}})}function Ko(e,t,n){if(e!=="mcp__playwright__browser_tabs")return;let r=t??{},s=typeof r.index=="number"?String(r.index):void 0;switch(r.action){case"select":return s===void 0?void 0:{action:"switchTab",value:s};case"close":return s===void 0?{action:"closeTab"}:{action:"closeTab",value:s};case"new":return typeof r.url=="string"&&r.url?{action:"newTab",url:It(r.url,n)}:{action:"newTab"};default:return}}function Yo(e,t){let n=t??{};if(e==="mcp__playwright__browser_handle_dialog"){let r={action:"handleDialog",value:n.accept?"accept":"dismiss"};return typeof n.promptText=="string"&&n.promptText&&(r.promptText=n.promptText),r}if(e==="mcp__playwright__browser_file_upload"){let r=Array.isArray(n.paths)?n.paths:[];if(r.length===0)return;let s=process.cwd()+"/";return{action:"uploadFile",value:r.map(a=>typeof a=="string"&&a.startsWith(s)?a.slice(s.length):a).join(`
59
+ `)}}}function Mt(e,t){let n=e.pages().filter(s=>!s.isClosed()),r=s=>[...n].reverse().find(s);return(t?r(s=>s.url()===t):void 0)??r(s=>s.url()!=="about:blank")??n.at(-1)}async function Tn(e,t){let n=t.context();for(let s of e.contexts())if(s!==n)for(let o of s.pages())await o.close().catch(()=>{});await t.bringToFront().catch(()=>{});let r=n.pages().filter(s=>!s.isClosed());return{index:Math.max(r.indexOf(t),0),count:r.length,url:t.url()}}var Fr=["chromium","chrome","msedge","firefox","webkit"],Or={chrome:{label:"Google Chrome"},msedge:{label:"Microsoft Edge"}};function na(e){return Fr.includes(e)}var Dt=class extends Error{},Ye={aiCalls:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreationTokens:0,costUsd:0};async function Il(){return new Promise((e,t)=>{let n=Tl.createServer();n.listen(0,"127.0.0.1",()=>{let{port:r}=n.address();n.close(()=>e(r))}),n.on("error",t)})}function Qo(e){let t=Yt(X(e));return t.size>0?{data:Object.fromEntries([...t].sort(([n],[r])=>n.localeCompare(r)))}:{}}function yt(e){let t=e.contexts().flatMap(n=>n.pages());return[...t].reverse().find(n=>n.url()!=="about:blank")??t.at(-1)}function ra(e){let t=e.length;for(let n=e.length-1;n>=0&&e[n].kind==="assertion";n--)t=n;return t}function Zo(e,t){return e[t]?.kind==="assertion"&&t>=ra(e)}function Xo(e,t,n=!1,r){let s=ra(e);return e.map((o,a)=>{if(r&&a<r.stepIndex)return o;let i=t[a],c=i&&i.actions.length>0;return o.kind==="assertion"?n&&a<s&&c?i:o:c?r&&a===r.stepIndex&&r.actionIndex>0?{...i,actions:[...o.actions.slice(0,r.actionIndex),...i.actions]}:i:o})}function Cl(e,t,n,r){let s=ao(e,t);return s.length===0||!n?.pageUrl?s:[`page at failure: replay stopped at step ${n.stepIndex+1} ("${n.stepText}") on ${It(n.pageUrl,r)}, where the heal started.`,...s]}function Pl(e,t,n=0){return e.map((r,s)=>{if(s<n)return r;let o=t[s];return o&&o.actions.length>0?o:r})}function ea(e,t){let n=[];for(let r=0;r<Math.min(t,e.length);r++){let s=e[r].actions.length;s!==0&&n.push(`recording: kept the existing recording of step ${r+1} ("${e[r].gherkin}"), which ran before the agent took over; ignored ${s} action${s===1?"":"s"} the agent's session held for it.`)}return n}function sa(e,t,n,r){let s=t.filter(c=>c>=0&&c<e.length),o=s.length>0?Math.max(...s):0,a=Math.min(n&&n.stepIndex>=o?n.stepIndex:o,e.length-1),i=n&&a===n.stepIndex?n.error:r;return e.map((c,l)=>({gherkin:c.gherkin,kind:c.kind,status:l<a?"passed":l===a?"failed":"skipped",...l===a&&i?{error:i}:{}}))}function _l(e,t,n){let r=n.success?e.map(s=>({gherkin:s.gherkin,kind:s.kind,status:"passed"})):sa(e,n.announcedSteps??[],t,n.narrative);return e.map((s,o)=>o<t.stepIndex?s:r[o])}function ta(e,t){return{aiCalls:e.aiCalls+t.aiCalls,inputTokens:e.inputTokens+t.inputTokens,outputTokens:e.outputTokens+t.outputTokens,cacheReadTokens:e.cacheReadTokens+t.cacheReadTokens,cacheCreationTokens:e.cacheCreationTokens+t.cacheCreationTokens,costUsd:(e.costUsd??0)+(t.costUsd??0),models:e.models||t.models?[...new Set([...e.models??[],...t.models??[]])]:void 0}}var Rn=class{constructor(t){this.options=t}options;browser;cdpEndpoint;stepIndex;get agent(){return this.options.agent}getStepIndex(){if(!this.stepIndex&&(this.stepIndex=ye(this.options.projectRoot),this.stepIndex.steps.size>0)){let t=this.stepIndex.divergent.length>0?` \xB7 ${this.stepIndex.divergent.length} step(s) have divergent recordings (Level-2 signal)`:"";console.log(`step index: ${this.stepIndex.steps.size} reusable steps from ${this.stepIndex.scannedCaches} cache(s)${t}`)}return this.stepIndex}async recordWithSeeding(t,n,r){let s=this.getStepIndex(),o=t.steps.length,a={...t.params,..._e(t.steps)},i=t.steps.map(p=>{let b=s.steps.size>0?vs(s,p,a):void 0;return b?{gherkin:p.text,keyword:p.keyword,kind:p.kind,table:p.table,docString:p.docString,fromStepSet:p.source?.fromStepSet,actions:b.actions}:void 0}),c=t.steps.map(p=>({gherkin:p.text,keyword:p.keyword,kind:p.kind,table:p.table,docString:p.docString,fromStepSet:p.source?.fromStepSet,actions:[]})),l=new Map;l.set(le,r);let f=[],d=[],m=[],h=[],y={...Ye},x=n,k=0,w=0,v=0,u=(p,b)=>({success:p,narrative:d.join(" ")||"Composed entirely from seeded recordings.",adaptations:f,suggestedFeatureEdit:h.length>0?h.join(`
60
+ `):void 0,featureEdits:m,recordedSteps:c,announcedSteps:b===void 0?[]:Array.from({length:b+1},(g,S)=>S),usage:y,seededCount:k});for(;v<o;){if(i[v]){let R=v;for(;R<o&&i[R];)R++;let _=i.slice(v,R),I={...t,steps:t.steps.slice(v,R)},C=await Ot(x.isClosed()?Mt(n.context())??n:x,I,this.buildCandidateCache(t,_),{baseURL:this.options.baseURL,actionTimeoutMs:this.options.actionTimeoutMs,retries:this.options.retries,pollIntervalMs:this.options.pollIntervalMs,vars:l,trace:this.options.trace!==void 0&&this.options.trace!=="off",traceStepOffset:v});x=C.activePage??x;let M=C.status==="passed"?R-v:C.failure?.stepIndex??0;for(let P=0;P<M;P++)c[v+P]=_[P];if(k+=M,C.status==="passed"){v=R;continue}let O=v+M;f.push(`Seeded recording for "${t.steps[O].text}" did not replay in this scenario's context; the agent re-recorded it fresh. Divergence signal.`),i[O]=void 0,v=O;continue}if(!this.options.agent)return u(!1,v);w++;let p=v;for(;p<o&&!i[p];)p++;w>=3&&(p=o);let b=await Tn(this.browser,x.isClosed()?Mt(n.context())??n:x),g=R=>{x=R};n.context().on("page",g);let S;try{S=await this.withDialogsVisible(()=>this.options.agent.run({scenario:t,baseURL:this.options.baseURL,traceContext:this.options.trace&&this.options.trace!=="off"?n.context():void 0,mode:"record",cdpEndpoint:this.cdpEndpoint,assertionPolicy:this.options.assertionPolicy,completedSteps:v>0?t.steps.slice(0,v).map(R=>R.resolvedText):void 0,recordOnly:p<o?{from:v,to:p-1}:void 0,liveTab:b,onActiveTab:R=>{let _=n.context().pages().filter(I=>!I.isClosed())[R];_&&(x=_)},uniqueSeed:l.get(le),onTabOpened:this.tabWatcher(),takeFingerprint:async()=>Ge(yt(this.browser)??n)}))}finally{n.context().off("page",g)}if(y=ta(y,S.usage),d.push(S.narrative),f.push(...S.adaptations),m.push(...S.featureEdits),S.suggestedFeatureEdit&&h.push(S.suggestedFeatureEdit),!S.success){let R=S.announcedSteps.filter(_=>_>=v&&_<o);return u(!1,R.length>0?Math.max(...R):v)}let T=S.announcedSteps.filter(R=>R>=v&&R<o),A=Math.min(o-1,Math.max(T.length>0?Math.max(...T):p-1,v));for(let R=v;R<=A;R++)c[R]=S.recordedSteps[R];v=A+1}return u(!0)}async withDialogsVisible(t){let n=()=>{},r=[],s=[],o=a=>{a.on("dialog",n),r.push(a)};for(let a of this.browser?.contexts()??[]){a.on("page",o),s.push(a);for(let i of a.pages())o(i)}try{return await t()}finally{for(let a of s)a.off("page",o);for(let a of r)try{a.off("dialog",n)}catch{}}}async start(){let t=this.options.browser??"chromium";if(t==="chromium"||Or[t]){let n=await Il();try{this.browser=await Rl.launch({headless:!this.options.headed,channel:Or[t]?t:void 0,args:[`--remote-debugging-port=${n}`]})}catch(r){let s=r instanceof Error?r.message:String(r),o=Or[t];throw o&&/is not found at|distribution .* is not found/i.test(s)?new Dt(`${o.label} is not installed on this machine, so --browser ${t} cannot run.
61
+ Install it (npx playwright install ${t} does that), or use --browser chromium, which Saffron ships with.`):r}this.cdpEndpoint=`http://127.0.0.1:${n}`}else this.browser=await(t==="firefox"?El:Al).launch({headless:!this.options.headed}),this.cdpEndpoint=void 0}async stop(){await this.browser?.close(),this.browser=void 0}resolveStorageState(){if(!this.options.storageState)return;let t=ie.isAbsolute(this.options.storageState)?this.options.storageState:ie.join(this.options.projectRoot,this.options.storageState);if(!mt.existsSync(t))throw new Error(`storageState file not found: ${t}. Generate one with: npx playwright open --save-storage=${this.options.storageState} <url>`);return t}buildCandidateCache(t,n){return{version:1,feature:t.featurePath,scenario:t.name,recordedAt:new Date().toISOString(),recordedBy:"agent",steps:n,...Qo(t)}}tabWatcher(){return t=>{let n=this.browser?.contexts()??[],r=()=>t();for(let s of n)s.on("page",r);return()=>{for(let s of n)s.off("page",r)}}}async proofReplay(t,n,r){let s=await this.browser.newContext({storageState:this.resolveStorageState()}),o=this.options.trace!==void 0&&this.options.trace!=="off";o&&await dr(s,`${t.displayName} (proof replay)`);let a=await s.newPage();return{result:await Ot(a,t,n,{baseURL:this.options.baseURL,actionTimeoutMs:this.options.actionTimeoutMs,retries:this.options.retries,pollIntervalMs:this.options.pollIntervalMs,uniqueSeed:r,trace:o}),context:s,page:a}}async runScenario(t,n={}){let r=await this.runScenarioInner(t,n),s=Te(t);if(s.size>0){let o=a=>a===void 0?a:Re(a,s);r.error=o(r.error),r.narrative=o(r.narrative),r.proofError=o(r.proofError),r.suggestedFeatureEdit=o(r.suggestedFeatureEdit),r.adaptations=r.adaptations.map(a=>Re(a,s));for(let a of r.stepResults)a.error=o(a.error)}return r}async screenshot(t,n,r,s){let o=this.options.screenshot??"viewport";if(o!=="off")try{let a=me(this.options.projectRoot,n.featurePath,n.displayName);this.claimArtifactDir(n);let i=ie.join(a,`${r}.jpg`),c=await t.screenshot({path:i,type:"jpeg",quality:80,fullPage:o==="full-page",timeout:5e3});return{kind:r,file:ie.relative(this.options.projectRoot,i).split(ie.sep).join("/"),step:s,sha256:Ve(c)}}catch{return}}claimArtifactDir(t){let n=me(this.options.projectRoot,t.featurePath,t.displayName);mt.mkdirSync(n,{recursive:!0}),mt.writeFileSync(ie.join(n,tn),JSON.stringify({feature:t.featurePath,scenario:t.displayName},null,2))}async runScenarioInner(t,n={}){if(!this.browser)throw new Error("Orchestrator not started");let r=Date.now(),{projectRoot:s,baseURL:o,actionTimeoutMs:a,retries:i,pollIntervalMs:c}=this.options,l=J(s,t.featurePath,t.name),f;try{f=oe(l)}catch(g){if(!(g instanceof V))throw g;return{scenario:t,status:"red",source:"cache",stepResults:[],adaptations:[],usage:Ye,durationMs:Date.now()-r,error:`The recording cannot be read: ${g.problem}. Fix the file, or re-record it with saffron run --rerecord.`}}let d=ue(s,t.featurePath,t.name),m,h;if(!f&&mt.existsSync(d))try{({proposal:m,revision:h}=gn(d))}catch(g){if(!(g instanceof V)&&g.code!=="ENOENT")throw g;m=void 0}m&&Ee(s,m,t)&&(m=void 0);let y=await this.browser.newContext({storageState:this.resolveStorageState()}),x=this.options.trace??"off",k=x!=="off";k&&await dr(y,t.displayName);let w=!k,v=await y.newPage(),u=Je(),p=[];mt.rmSync(me(s,t.featurePath,t.displayName),{recursive:!0,force:!0});let b=(g,S)=>this.screenshot(g,t,"before-heal",S.stepText).then(T=>{T&&p.push(T)});try{let g=await(async()=>{let T;if(m){let C=await Ot(v,t,m.cache,{baseURL:o,actionTimeoutMs:a,retries:i,pollIntervalMs:c,uniqueSeed:u,trace:k,tracePhase:"pending"}),M=m.meta.recordedFor??t.name;if(C.status==="passed")return{scenario:t,status:"yellow",source:"agent-record",proposalReplay:!0,stepResults:C.stepResults,narrative:`Replayed the pending proposal recorded for "${M}" at zero AI; accept it to go green.`,adaptations:[],verified:!0,proposalFile:d,proposalRevision:h,usage:Ye,durationMs:Date.now()-r};if(Object.keys(t.params).length>0&&M!==t.displayName){let P=C.failure;return{scenario:t,status:"red",source:"agent-record",proposalReplay:!0,stepResults:C.stepResults,adaptations:[],proposalFile:d,proposalRevision:h,usage:Ye,durationMs:Date.now()-r,error:`The pending proposal recorded for "${M}" does not replay for this Examples row`+(P?` (step "${P.stepText}": ${P.error.split(`
62
+ `)[0]})`:"")+". A row value is probably baked in as a literal instead of a <placeholder>; inspect the proposal, reject it, and re-record with saffron run --rerecord."}}console.log(` \u21BA pending proposal for "${t.displayName}" did not replay`+(C.failure?` (step "${C.failure.stepText}": ${C.failure.error.split(`
63
+ `)[0]})`:"")+"; recording again"),await v.goto("about:blank")}if(f&&(T=await Ot(v,t,f,{baseURL:o,actionTimeoutMs:a,retries:i,pollIntervalMs:c,uniqueSeed:u,trace:k,onFailure:b}),T.status==="passed"))return{scenario:t,status:"green",source:"cache",stepResults:T.stepResults,adaptations:[],usage:Ye,durationMs:Date.now()-r};if(!(this.options.agent!==void 0&&this.cdpEndpoint!==void 0&&!n.agentDisabled)){let C=this.options.agent!==void 0&&this.cdpEndpoint===void 0&&!n.agentDisabled?`recording/healing needs a Chromium-family browser (the agent attaches over CDP). Record with --browser chromium (the default), chrome or msedge, then replay with --browser ${this.options.browser}.`:"no agent is configured.";return{scenario:t,status:"red",source:"cache",stepResults:T?.stepResults??[],adaptations:[],usage:Ye,durationMs:Date.now()-r,error:f?T?.status==="stale"?`Cache is stale (feature file changed) and ${C}`:`Replay failed and ${C} ${T?.failure?.error??""}`.trimEnd():`No cache exists and ${C}`}}let R=f!==void 0&&T!==void 0&&T.status==="failed",_=0,I;try{if(R){let C=await Tn(this.browser,T.activePage??Mt(v.context(),T.failure.pageUrl)??v);I=await this.withDialogsVisible(()=>this.options.agent.run({scenario:t,baseURL:o,traceContext:k?v.context():void 0,mode:"heal",liveTab:C,model:this.options.healModel,cdpEndpoint:this.cdpEndpoint,assertionPolicy:this.options.assertionPolicy,completedSteps:T.stepResults.filter(M=>M.status==="passed").map(M=>M.gherkin),failedStep:{text:T.failure.stepText,kind:T.failure.kind,error:T.failure.error,pageUrl:T.failure.pageUrl,stepIndex:T.failure.stepIndex},uniqueSeed:u,onTabOpened:this.tabWatcher(),takeFingerprint:async()=>{let M=yt(this.browser)??v;return Ge(M)}}))}else if(await v.goto("about:blank"),this.options.reuseSteps!==!1){let C=await this.recordWithSeeding(t,v,u);_=C.seededCount,I=C}else I=await this.withDialogsVisible(()=>this.options.agent.run({scenario:t,baseURL:o,traceContext:k?v.context():void 0,mode:"record",cdpEndpoint:this.cdpEndpoint,assertionPolicy:this.options.assertionPolicy,uniqueSeed:u,onTabOpened:this.tabWatcher(),takeFingerprint:async()=>{let C=yt(this.browser)??v;return Ge(C)}}))}catch(C){let M=C instanceof Error?C.message:String(C);return{scenario:t,status:"red",source:R?"agent-heal":"agent-record",stepResults:T?.stepResults??[],adaptations:[],usage:Ye,durationMs:Date.now()-r,error:`Agent invocation failed: ${M.split(`
64
+ `)[0]}`}}if(I.success&&R&&T.failure.kind==="assertion"&&(this.options.assertionPolicy!=="adaptable-mid"||Zo(f.steps,T.failure.stepIndex))){let C=f.steps[T.failure.stepIndex],M=yt(this.browser)??v,O=new Map(Object.entries(T.captured)),P=Sr(t,T.failure.stepIndex);try{for(let $ of C.actions)await Sn(M,$,P,this.options.actionTimeoutMs??5e3,o,O)}catch($){let j=$ instanceof Error?$.message:String($),E=`Assertion failed as written: "${C.gherkin}". Saffron never adapts assertions. If this change is intended, update the .feature file or re-record the scenario (saffron run --rerecord --filter @tag). If the recording itself looks wrong (an empty value, a wrong target), re-record it the same way. Verification error: ${j.split(`
65
+ `)[0]}`,F=T.failure.stepIndex,z=T.stepResults.map((Q,K)=>K===F?{...Q,status:"failed",error:E}:Q);return{scenario:t,status:"red",source:"agent-heal",stepResults:z,executionStepResults:z.map((Q,K)=>K>F&&Q.status==="skipped"?{...Q,status:"passed"}:Q),narrative:I.narrative,adaptations:I.adaptations,suggestedFeatureEdit:I.suggestedFeatureEdit,usage:I.usage,durationMs:Date.now()-r,error:E}}}return await this.finalizeAgentRun(t,I,R,f,r,_,R?T?.failure:void 0,u)})();if(g.status==="red"){let T=p.find(A=>A.kind==="before-heal");if(g.source==="cache"&&T){let A=T.file.replace(/before-heal\.jpg$/,"failure.jpg");try{mt.renameSync(ie.join(s,T.file),ie.join(s,A)),T.file=A}catch{}T.kind="failure"}else{let A=g.stepResults.find(_=>_.status==="failed")?.gherkin,R=await this.screenshot(yt(this.browser)??v,t,"failure",A);R&&p.push(R)}}let S=ut(X(t),u);if(S.size>0&&(g.uniques=Object.fromEntries(S)),p.push(...g.evidence??[]),k){let T=x==="on"||g.status!=="green",A=me(s,t.featurePath,t.displayName),R=T?ie.join(A,lr):void 0;w=!0,R||await pr(y).catch(()=>{});let _=R?await fr(y,R,Te(t)):void 0;if(R&&_){this.claimArtifactDir(t);let I=Te(t);p.unshift({kind:"trace",file:ie.relative(s,R).split(ie.sep).join("/"),sha256:_,traceSteps:(g.executionStepResults??g.stepResults).map(C=>({...C,error:C.error&&Re(C.error,I)}))})}}return p.length>0&&(g.status!=="green"||p.some(T=>T.kind==="trace"))&&(g.evidence=p),g}finally{w||await pr(y).catch(()=>{}),await y.close()}}async finalizeAgentRun(t,n,r,s,o,a=0,i,c=Je()){let l=r?"agent-heal":"agent-record",f=n.success?n.recordedSteps.map(E=>({gherkin:E.gherkin,kind:E.kind,status:"passed"})):sa(n.recordedSteps,n.announcedSteps??[],i&&{stepIndex:i.stepIndex,error:i.error},n.narrative);if(!n.success)return{scenario:t,status:"red",source:l,stepResults:f,narrative:n.narrative,adaptations:n.adaptations,usage:n.usage,durationMs:Date.now()-o,error:n.narrative};let d=r?Xo(s.steps,n.recordedSteps,this.options.assertionPolicy==="adaptable-mid",i?{stepIndex:i.stepIndex,actionIndex:i.actionIndex}:void 0):n.recordedSteps,m=[...n.adaptations,...r&&i?ea(n.recordedSteps,i.stepIndex):[]],h={...n.usage},y,x,k,w,v=[],u=0,p=new Set,b,g=async(E,F,z=F.stepResults)=>{if(!p.has(E)){p.add(E);try{if(this.options.trace&&this.options.trace!=="off"){let Q=ie.join(me(this.options.projectRoot,t.featurePath,t.displayName),`proof-${++u}.zip`),K=Te(t),ge=await fr(E,Q,K);if(ge){this.claimArtifactDir(t);let Vt=F.captured[le],cs=Vt?ut(X(t),Vt):new Map;v.push({kind:"trace",proof:!0,file:ie.relative(this.options.projectRoot,Q).split(ie.sep).join("/"),sha256:ge,traceSteps:z.map(Bn=>({...Bn,error:Bn.error&&Re(Bn.error,K)})),traceUniques:cs.size?Object.fromEntries(cs):void 0})}}}finally{await E.close()}}};if(this.options.verifyProposals!==!1){let E=await this.proofReplay(t,this.buildCandidateCache(t,d),Je());w=E.result.captured[le];try{if(E.result.status!=="passed"&&E.result.failure){let F=E.result.failure;if(this.options.agent&&!(r&&F.kind==="assertion"&&(this.options.assertionPolicy!=="adaptable-mid"||Zo(s.steps,F.stepIndex))))try{let Q=await Tn(this.browser,E.result.activePage??Mt(E.page.context(),F.pageUrl)??E.page),K=await this.withDialogsVisible(()=>this.options.agent.run({scenario:t,baseURL:this.options.baseURL,traceContext:this.options.trace&&this.options.trace!=="off"?E.context:void 0,mode:"refine",model:r?this.options.healModel:void 0,cdpEndpoint:this.cdpEndpoint,assertionPolicy:this.options.assertionPolicy,completedSteps:E.result.stepResults.filter(ge=>ge.status==="passed").map(ge=>ge.gherkin),failedStep:{text:F.stepText,kind:F.kind,error:F.error,pageUrl:F.pageUrl,stepIndex:F.stepIndex},liveTab:Q,uniqueSeed:E.result.captured[le],onTabOpened:this.tabWatcher(),takeFingerprint:async()=>Ge(yt(this.browser)??E.page)}));h=ta(h,K.usage),b=_l(E.result.stepResults,F,K),K.success&&(m.push(...K.adaptations),m.push(...ea(K.recordedSteps,F.stepIndex)),d=r?Xo(d,K.recordedSteps,this.options.assertionPolicy==="adaptable-mid",{stepIndex:F.stepIndex,actionIndex:0}):Pl(d,K.recordedSteps,F.stepIndex),await g(E.context,E.result,b),b=void 0,E=await this.proofReplay(t,this.buildCandidateCache(t,d),Je()),w=E.result.captured[le])}catch{}}for(let F of E.result.tabOpeners??[]){let z=d[F.stepIndex]?.actions[F.actionIndex];z&&(z.opensTab=!0)}y=E.result.status==="passed",y||(x=E.result.failure?.error??`proof replay ${E.result.status}`,k=E.result.stepResults)}finally{await g(E.context,E.result,b).catch(()=>{})}}r&&s&&m.push(...Cl(s.steps,d,i,this.options.baseURL));let S=[...n.featureEdits];for(let E of ks(d,ye(this.options.projectRoot)))m.push(`duplicate wording: "${E.from}" recorded exactly the same actions as the existing step "${E.to}" (${E.feature} \u203A ${E.scenario}). Accept with --with-feature-edit to converge on the canonical wording.`),S.some(F=>F.index===E.index)||S.push({index:E.index,text:E.to});let T=zo(d);T.length>0&&(y=!1,x=[x,...T].filter(Boolean).join(" "));let A=Te(t),R=E=>A.size>0?Re(E,A):E;m=m.map(R);let _=S.map(E=>({index:E.index,text:R(E.text)})),I=R(n.narrative),C=n.suggestedFeatureEdit?R(n.suggestedFeatureEdit):void 0,M={version:1,feature:t.featurePath,scenario:t.name,recordedAt:new Date().toISOString(),recordedBy:"agent",steps:d,...Qo(t)},O={meta:{createdAt:M.recordedAt,mode:r?"heal":"record",narrative:I,adaptations:m,suggestedFeatureEdit:C,featureEdits:_.length>0?_:void 0,verified:y,proofError:x?R(x):void 0,seededSteps:a>0?a:void 0,recordedFor:t.displayName,usage:h,bound:wo(t,s,M,_.length>0?_:void 0)},cache:M},P=ue(this.options.projectRoot,t.featurePath,t.name),$=Os(P,O),j=oe(J(this.options.projectRoot,t.featurePath,t.name));return{proposalDiff:gt(j,O.cache),scenario:t,status:"yellow",source:l,stepResults:k??f,executionStepResults:f,evidence:v,proofUniques:(()=>{let E=w?ut(X(t),w):void 0;return E&&E.size>0?Object.fromEntries(E):void 0})(),narrative:I,adaptations:m,suggestedFeatureEdit:C,featureEdits:_.length>0?_:void 0,verified:y,proofError:x,seededSteps:a>0?a:void 0,proposalFile:P,proposalRevision:$,usage:h,durationMs:Date.now()-o}}};import ya from"node:path";import{createRequire as oa}from"node:module";var jl=oa(import.meta.url),En=oa(jl.resolve("playwright/package.json"));import{Client as Ol}from"@modelcontextprotocol/sdk/client/index.js";import{McpServer as Fl}from"@modelcontextprotocol/sdk/server/mcp.js";import{InMemoryTransport as Nl}from"@modelcontextprotocol/sdk/inMemory.js";import{CallToolRequestSchema as Ml,ListToolsRequestSchema as Dl}from"@modelcontextprotocol/sdk/types.js";var Ll=1e8,Ul=1e4,Bl=5e3,aa="saffron_connection_check",An=e=>new Promise(t=>{setTimeout(t,e).unref()}),Lt=new Set,Nr=0,ia=e=>{Nr===0&&[...Lt].at(-1)?.(e)};function zl(e){let t=n=>e(n);return Lt.size===0&&process.on("unhandledRejection",ia),Lt.add(t),()=>{Lt.delete(t)&&Lt.size===0&&process.off("unhandledRejection",ia)}}async function Mr(e,t,n,r){await e.tracing.group(`[saffron:agent:${t.index}] ${t.title}`).catch(()=>{});try{await e.tracing.group(n).catch(()=>{});try{return await r()}finally{await e.tracing.groupEnd().catch(()=>{})}}finally{await e.tracing.groupEnd().catch(()=>{})}}function ql(e,t={}){let n=o=>typeof o=="string"&&o.trim()?o.trim():void 0,r=n(t.element)??"an element",s=(()=>{switch(e){case"browser_snapshot":return"read the page";case"browser_find":return n(t.text)?`find "${n(t.text)}" on the page`:n(t.regex)?`find ${n(t.regex)} on the page`:"search the page";case"browser_click":return`${t.doubleClick?"double-click":"click"} ${r}`;case"browser_hover":return`hover ${r}`;case"browser_type":return`type into ${r}`;case"browser_fill_form":return"fill in the form";case"browser_select_option":return`select in ${r}`;case"browser_press_key":return n(t.key)?`press ${n(t.key)}`:"press a key";case"browser_navigate":return n(t.url)?`go to ${n(t.url)}`:"navigate";case"browser_navigate_back":return"go back";case"browser_wait_for":return n(t.text)?`wait for "${n(t.text)}"`:n(t.textGone)?`wait for "${n(t.textGone)}" to go`:`wait ${t.time??""}s`.replace(" s","");case"browser_tabs":return n(t.action)?`tabs: ${n(t.action)}`:"list the tabs";case"browser_drag":return`drag ${n(t.startElement)??"an element"} to ${n(t.endElement)??"an element"}`;case"browser_file_upload":return"upload files";case"browser_handle_dialog":return`${t.accept?"accept":"dismiss"} the dialog`;case"browser_evaluate":case"browser_run_code":case"browser_run_code_unsafe":return"run a script in the page";case"browser_take_screenshot":return"take a screenshot";case"browser_console_messages":return"read the console";case"browser_network_requests":return"read the network log";default:return}})();return s?`${s} \xB7 ${e}`:e}async function ca(e,t,n="none",r=()=>{}){let{createConnection:s,resolveCLIConfigForMCP:o}=En("playwright-core/lib/coreBundle").tools,a=await o?.({},process.env)??{},i={...a,browser:{...a.browser,isolated:!1},sharedBrowserContext:!0,imageResponses:"omit",snapshot:{...a.snapshot,mode:n}},c=e.browser(),l=new Set(c?.listeners("disconnected")??[]),f=new Set,d=g=>{f.add(g)},m=g=>{g.on("download",d)};for(let g of e.pages())m(g);e.on("page",m);let h=zl(r),y=g=>{e.off("page",m);for(let R of e.pages())R.off("download",d);for(let R of c?.listeners("disconnected")??[])l.has(R)||c.off("disconnected",R);if(!g)return h();let S=()=>{clearTimeout(A),e.off("close",T),h()},T=()=>{setTimeout(S,1e3).unref()},A=setTimeout(S,3e5);A.unref(),e.on("close",T)},x,k=new Ol({name:"saffron-trace",version:"1"});try{x=await s(i,async()=>e);let[g,S]=Nl.createLinkedPair();await x.connect(S),await k.connect(g);let A=((await k.callTool({name:aa,arguments:{}})).content??[]).map(R=>R.text??"").join(`
66
+ `);if(!A.includes(aa))throw new Error(A.replace(/^### Error\s*/,"").trim()||"the browser backend did not start")}catch(g){throw await Promise.allSettled([k.close(),x?.close()]),y(!1),g}let w=new Fl({name:"playwright",version:"1"},{capabilities:{tools:{}}});w.server.setRequestHandler(Dl,()=>k.listTools());let v=Promise.resolve(),u=(g,S,T=t())=>{let A=v.then(()=>Mr(e,T,g,S));return v=A.catch(()=>{}),A};w.server.setRequestHandler(Ml,(g,S)=>u(ql(g.params.name,g.params.arguments),()=>k.callTool(g.params,void 0,{timeout:Ll,signal:S.signal})));let p=async()=>{let g=[...f];if(g.length===0)return;let S=Promise.all(g.map(A=>A.path().then(()=>!0,()=>!0))),T=!await Promise.race([S,An(Bl).then(()=>!1)]);T&&Nr++;try{T&&(await Promise.allSettled(g.map(A=>A.cancel())),await Promise.allSettled(g.map(A=>A.failure()))),await An(0),await An(0)}finally{T&&Nr--}};return{config:{type:"sdk",name:"playwright",instance:w},close:async()=>{await Promise.race([v,An(Ul)]),await p().catch(()=>{}),await Promise.allSettled([k.close()]),await Promise.allSettled([x.close()]),await Promise.allSettled([w.close()]),y(!0)},grouped:(g,S)=>u(g,S)}}import{createRequire as Zl}from"node:module";import{query as Xl}from"@anthropic-ai/claude-agent-sdk";import{createSdkMcpServer as Hl,tool as bt}from"@anthropic-ai/claude-agent-sdk";import{z as B}from"zod";var te=e=>({content:[{type:"text",text:e}]});function la(e){let t={};return e.role&&(t.role=e.role),e.name&&(t.name=e.name),e.nameRegex&&(t.nameRegex=e.nameRegex),e.label&&(t.label=e.label),e.text&&(t.text=e.text),e.selector&&(t.fallbackSelectors=[e.selector]),e.frame&&(t.frame=e.frame),Object.keys(t).length>0?t:void 0}var Wl=new Set(["main","banner","contentinfo","status","alert","alertdialog","dialog","search"]);function da(e){if(!e?.role)return;let{role:t,frame:n,...r}=e;if(!(Object.keys(r).length>0||Wl.has(t)))return`ERROR: role="${t}" alone matches every ${t} on the page, and replay would use the first one. Add a name (or nameRegex), text, label or selector that picks exactly the element you checked, then call again.`}function Vl(e,t){return t.index>=e.steps.length?`ERROR: there is no step ${t.index}: this scenario's steps are numbered from 0 to ${e.steps.length-1}. Call saffron_step again with the zero-based index of the step you are about to perform.`:(e.announce(t.index,t.text,t.adaptation),t.index<e.firstStep?`Step ${t.index} already ran before you took over and keeps its recording; nothing you do under it is recorded. Do not redo it: continue from step ${e.firstStep} on the page as it is.`:`Recording step ${t.index}: ${t.text}`)}var pa={role:B.string().optional().describe("ARIA role of the element"),name:B.string().optional().describe("Accessible name \u2014 use the SHORTEST stable substring; never include volatile content like prices or dates"),nameRegex:B.string().optional().describe("Regex for the accessible name when it embeds volatile content, e.g. 'Select \\\\{date\\\\+1\\\\} as checkin' cannot work \u2014 instead use nameRegex 'Select .* as checkin'"),label:B.string().optional().describe("Form-label text for getByLabel \u2014 the literal <label> text only, never a free-text description"),text:B.string().optional().describe("getByText locator text"),selector:B.string().optional().describe("CSS fallback selector"),frame:B.string().optional().describe(`CSS selector of the iframe hosting the element, e.g. 'iframe[title="Feedback widget"]' \u2014 REQUIRED whenever the element lives inside an iframe; without it replay looks only at the top-level page`)};function fa(e){return Hl({name:"saffron",version:"0.1.0",tools:[bt("saffron_step","Announce the Gherkin step you are about to perform. MUST be called before executing browser actions for each step, with the step's zero-based index and exact text.",{index:B.number().int().min(0),text:B.string(),adaptation:B.string().optional().describe("If the written step does not match the real UI and you are deviating from it, describe the deviation in one sentence: the kind of change (a locator or label change on the same page, a page removed or merged, a page added or flow reordered) and the page it happened on.")},async t=>te(Vl(e,t))),bt("saffron_capture",`Record a captureText action for the current step: at replay time, the target element's text is read and stored under saveAs for later expectDiffers comparisons. Use for steps like 'I record the displayed price as "X"'. The target must select exactly the element whose text is the value.`,{saveAs:B.string().describe("Name to store the captured text under"),...pa},async t=>{let n=la(t);if(!n)return te("ERROR: capture needs a target (role/name, nameRegex, label, text, or selector).");let r=da(n);return r?te(r):(e.addActions([{action:"captureText",target:n,saveAs:t.saveAs}]),te(`Capture recorded as "${t.saveAs}".`))}),bt("saffron_dialog","Record how a native dialog (alert/confirm/prompt) must be handled at replay. Call this IMMEDIATELY whenever an action triggers a native dialog \u2014 whether you handled it via browser_handle_dialog or the browser reported it as already handled. At replay Saffron arms this handler BEFORE the triggering action, so without this call the replay will auto-dismiss the dialog and diverge from what you saw.",{accept:B.boolean().describe("true = accept/OK the dialog, false = dismiss/cancel"),promptText:B.string().optional().describe("Text typed into a prompt() before accepting")},async t=>(e.addArmedAction({action:"handleDialog",value:t.accept?"accept":"dismiss",...t.promptText?{promptText:t.promptText}:{}}),te(`Dialog handling recorded (${t.accept?"accept":"dismiss"}).`))),bt("saffron_wait_api","Record a network wait for the current step: replay will block until a matching API response is observed (responses from the triggering step count). Call AFTER you verified via browser_network_requests that the response actually occurred with the expected status/body. Use a path-only urlPattern regex and never literal volatile IDs (/api/orders/\\d+, not /api/orders/8842).",{urlPattern:B.string().describe("Regex matched against the response URL \u2014 path-only for portability"),method:B.string().optional().describe("HTTP method to match (any if omitted)"),status:B.string().optional().describe("Expected status \u2014 exact ('201') or class ('2xx'); default any 2xx"),bodyPattern:B.string().optional().describe(`Regex the response body must match, e.g. '"status"\\s*:\\s*"READY"' for polling waits`)},async t=>(e.addActions([{action:"waitForResponse",urlPattern:t.urlPattern,...t.method?{method:t.method}:{},...t.status?{status:t.status}:{},...t.bodyPattern?{pattern:t.bodyPattern}:{}}]),te("Network wait recorded."))),bt("saffron_assert","Record a verified assertion for the current Then step. Call ONLY after you have confirmed the condition holds via a page snapshot. If the condition does NOT hold, do not call this \u2014 call saffron_done with success=false instead. NEVER record volatile content (prices, dates, counters) as literal expected text \u2014 use kind=matches with a pattern, or kind=differs against captured values.",{kind:B.enum(["visible","notVisible","text","url","value","differs","matches","attribute","response","each"]),...pa,expected:B.string().optional().describe("Expected text/value for kind=text|value \u2014 stable text only, never volatile values"),url:B.string().optional().describe("URL substring for kind=url (path only, no host)"),pattern:B.string().optional().describe("kind=matches|attribute: regex the target's text (or attribute) must match, e.g. 'NOK [\\\\d,]+' for a price"),attribute:B.string().optional().describe("kind=attribute: attribute name to check, e.g. 'href' for link-target assertions \u2014 use this for 'X should link to Y' instead of clicking through"),capture:B.string().optional().describe("kind=differs: name of the captured value on the LEFT of the comparison (omit when comparing the target element's live text)"),urlPattern:B.string().optional().describe("kind=response: regex matched against the response URL \u2014 path-only for portability, never literal volatile IDs (use /api/orders/\\d+)"),method:B.string().optional().describe("kind=response: HTTP method to match (any if omitted)"),status:B.string().optional().describe("kind=response: expected status \u2014 exact ('201') or class ('2xx'); default any 2xx"),bodyPattern:B.string().optional().describe("kind=response: regex the response body must match (verify via browser_network_requests first)"),each:B.string().optional().describe("kind=each: the data-list token exactly as the step writes it, e.g. '{data:enums.OrderStatus}'. The target is the CONTAINER (dropdown, table, list) that must show every value of that list. Never pass the values themselves."),compareTo:B.string().optional().describe("kind=differs: name of the captured value to compare against")},async t=>{let n=la(t),r=da(n);if(r)return te(r);let s;switch(t.kind){case"visible":s={action:"expectVisible",target:n};break;case"notVisible":s={action:"expectNotVisible",target:n};break;case"text":s={action:"expectText",target:n,value:t.expected??""};break;case"value":s={action:"expectValue",target:n,value:t.expected??""};break;case"url":s={action:"expectUrl",url:t.url??""};break;case"differs":if(!t.compareTo)return te("ERROR: kind=differs requires compareTo.");if(!t.capture&&!n)return te("ERROR: kind=differs needs either capture (a stored name) or a target element.");s={action:"expectDiffers",target:n,value:t.capture,compareTo:t.compareTo};break;case"matches":if(!t.pattern)return te("ERROR: kind=matches requires pattern.");s={action:"expectMatches",target:n,pattern:t.pattern};break;case"attribute":if(!t.attribute||!t.pattern)return te("ERROR: kind=attribute requires attribute and pattern.");s={action:"expectAttribute",target:n,attribute:t.attribute,pattern:t.pattern};break;case"each":if(!t.each||!/^\{data:[^}]+\}$/.test(t.each))return te("ERROR: kind=each requires each='{data:file.list}', the token exactly as the step writes it (not the values).");s={action:"expectEach",target:n,each:t.each};break;case"response":if(!t.urlPattern)return te("ERROR: kind=response requires urlPattern.");s={action:"expectResponse",urlPattern:t.urlPattern,...t.method?{method:t.method}:{},...t.status?{status:t.status}:{},...t.bodyPattern?{pattern:t.bodyPattern}:{}};break}return!["url","differs","response"].includes(t.kind)&&!n?te("ERROR: assertion needs a target (role/name, nameRegex, label, text, or selector)."):(e.addActions([s]),te("Assertion recorded."))}),bt("saffron_done","Finalize the run. Call exactly once, after the final step: success=true only if every step's intent was achieved and every assertion verified.",{success:B.boolean(),narrative:B.string().describe("2-4 sentence summary of the run: what happened, and why it failed if it did. After a heal, say what kind of change the app went through (a locator change, or a page removed, merged, added or reordered) and on which page."),suggestedFeatureEdit:B.string().optional().describe("If you adapted any steps: the corrected Gherkin for those steps, as you would rewrite them in the .feature file (free text, for the human report)."),featureEdits:B.array(B.object({index:B.number().int().min(0).describe("zero-based step index"),text:B.string().describe("replacement step text WITHOUT the Given/When/Then keyword")})).optional().describe("Machine-applicable version of the suggested edits: one entry per adapted step. Provide this whenever you adapted a step.")},async t=>(e.done=!0,e.success=t.success,e.narrative=t.narrative,e.suggestedFeatureEdit=t.suggestedFeatureEdit,e.featureEdits=t.featureEdits??[],te("Run finalized.")))]})}function ga(e={}){let t=e.deliberateSnapshots?`
67
67
  8. Tool responses do NOT include a page snapshot. Take snapshots deliberately with browser_snapshot, and only when you actually need to see the page: after navigation, before locating an element you haven't seen yet, and when verifying an assertion. Do NOT snapshot after actions whose outcome is predictable (filling a field you just located, pressing Enter, a click whose result you will verify later anyway). Every snapshot is expensive.
68
68
  9. Minimize round-trips: batch tool calls in a single response whenever they don't depend on each other's results \u2014 saffron_step together with that step's browser actions, several fills in one turn, bookkeeping calls (saffron_capture, saffron_assert) alongside the actions they belong to. Only stop and wait when you genuinely need a result before deciding your next call. Do not write narration between tool calls.`:"";return`You are Saffron's test execution agent. You execute Gherkin scenarios in a real browser using the playwright MCP tools, while Saffron records your actions into a deterministic replay cache.
69
69
 
@@ -73,7 +73,7 @@ Non-negotiable rules:
73
73
  3. A Given step (or a StepSet guard) that describes a state the page is ALREADY in needs no action and is NOT an adaptation: confirm it from the current snapshot and continue. Only report an adaptation when you had to do something different from what the step says.
74
74
  3a. Steps marked [action] may be adapted if the written step does not match the real UI (e.g. a step refers to a control that does not exist, or a required intermediate step is missing). Adapt minimally to achieve the step's evident intent, and report the deviation via saffron_step's "adaptation" field. Before adapting, take a snapshot and decide: is this UI drift (adapt) or an application defect (fail with saffron_done success=false and explain)?
75
75
  4. Do not pursue goals beyond the scenario. Do not retry an assertion more than twice.
76
- 5. Prefer stable, semantic element references: role + accessible name. Avoid positional or generated-id selectors. Record the accessible name AS IT READS, in full: a shortened name is ambiguous ("Save" also matches "Save draft", and replay may then act on the wrong control). When an accessible name embeds volatile content (prices, dates, counters), use nameRegex with only the volatile part wildcarded.
76
+ 5. Prefer stable, semantic element references: role + accessible name. Avoid positional or generated-id selectors. Record the accessible name AS IT READS, in full: a shortened name is ambiguous ("Save" also matches "Save draft", and replay may then act on the wrong control). When an accessible name embeds volatile content (prices, dates, counters), use nameRegex with only the volatile part wildcarded. When you click, type into or otherwise act on an element, pass the ref from your latest browser_snapshot as its target, not a selector string: a ref records as role + name, which replays the way this rule asks. Write a selector only when no snapshot ref reaches the element.
77
77
  5a. For "X should link to Y" assertions, use saffron_assert kind=attribute (attribute='href', pattern for the URL) on the link element \u2014 do NOT click through and assert the page URL; the recorded assertion must be verifiable without navigation. NEVER bake volatile values into the recording. For steps that store a displayed value ('I record the price as "X"'), use saffron_capture. For comparisons ('X should differ from Y'), verify yourself from snapshots, then record with saffron_assert kind=differs referencing the captured names. For 'something dynamic should be visible', use kind=matches with a pattern (e.g. 'NOK [\\d,]+') instead of literal text.
78
78
  5a2. When a Then step quotes the expected text (or carries a <param> that resolves to it), pass EXACTLY that quoted text as the expected argument of saffron_assert. Replay checks containment, so the step's own wording is enough; never substitute the page's longer sentence, and never a value from a different Examples row.
79
79
  5b. Literal ISO dates (YYYY-MM-DD) in recorded locators are auto-templated to {date\xB1N} relative to today, so date-picker recordings stay valid tomorrow. For non-ISO date formats in names, use nameRegex.
@@ -83,41 +83,41 @@ Non-negotiable rules:
83
83
  5d. NATIVE DIALOGS (alert/confirm/prompt): when an action opens one, the page blocks and the snapshot reports a modal state \u2014 resolve it with browser_handle_dialog (accept true/false; promptText for prompt()). That call is recorded automatically as an armed handler replay places BEFORE the triggering action, so do NOT also call saffron_dialog. After handling, verify the post-dialog state as usual. If a step's wording says to confirm/accept, accept; if it says cancel/dismiss, dismiss.
84
84
  5e. NETWORK WAITS: when a step says to wait for an API call ("wait for the order API to succeed", "until the job API reports READY"), do it verify-first: perform the trigger, check via browser_network_requests that the matching response actually occurred with the expected status/body, then record it with saffron_wait_api. For Then steps about API contracts ("the request should have returned 201") verify the same way and record with saffron_assert kind=response. urlPattern is a path-only regex \u2014 NEVER a literal volatile ID (/api/orders/\\d+, not /api/orders/8842) and never the host.
85
85
  5f. TABS: a click that opens a new tab needs nothing extra, replay follows the new tab by itself. When a step says to switch to another tab, go back to the first tab, or close a tab, do it with browser_tabs (list first if unsure of the index); select, close and new are recorded and replay in the same order. Steps that assert on the other tab's content are ordinary assertions after the switch.
86
- 7. Never ask the user questions; you are unattended.${t}`}function Bl(e,t){if(e[t].kind!=="assertion")return"action";let n=e.length;for(let r=e.length-1;r>=0&&e[r].kind==="assertion";r--)n=r;return t>=n?"assertion:final":"assertion"}var zl=`When you heal, tell the reviewer WHAT KIND of change you adapted to, in saffron_step's adaptation and again in saffron_done's narrative, and name the page (its URL path) where it happened. Pick the kind that fits: (1) locator or label change: the same control on the same page, found differently (renamed button, new test id, native select instead of a custom one); (2) page removed or merged: a page no longer exists and its fields or controls now live on another page, so a navigation step (e.g. a 'Continue' click) is no longer needed; (3) page added or flow reordered: a new intermediate page, dialog or step order; (4) timing only: nothing changed, the element was slow. When you skip or add a click, key press or navigation the step used to perform, say so explicitly, e.g. "The guest information page is gone: its fields are now on /checkout, so the 'Continue to payment' click was dropped." Never describe a flow change as a locator change.`;function Rn(e){let t=e.liveTab;if(t)return t.count<=1?`Live page: the only open tab, at ${t.url}. Work on it; do not open or select another tab to start over.`:`Live page: tab ${t.index} of ${t.count} open tabs, at ${t.url}. This is where replay left off. If browser_tabs shows another tab as current, select tab ${t.index} before anything else (Saffron does not record that selection); never start over on another tab.`}function ca(e){let t=e.failedStep?.stepIndex;return`ANNOUNCE FIRST: call saffron_step for ${t===void 0?"the failed step":`the failed step (index ${t})`} BEFORE any state-changing browser action (click, type, select, key press, navigation, tab change). To diagnose, look with browser_snapshot, never with clicks or navigation: Saffron discards every action made before that announcement, and going back through earlier steps changes the state replay reached.`}function da(e){let{scenario:t}=e,n=t.steps.map((o,s)=>{let a=d=>$t(as(ft(d,e.uniqueSeed??""))),i=`${s}. [${Bl(t.steps,s)}] ${o.keyword} ${a(o.resolvedText)}`,c=o.resolvedTable??o.table,p=o.resolvedDocString??o.docString,f=i;return c&&(f+=`
86
+ 7. Never ask the user questions; you are unattended.${t}`}function Jl(e,t){if(e[t].kind!=="assertion")return"action";let n=e.length;for(let r=e.length-1;r>=0&&e[r].kind==="assertion";r--)n=r;return t>=n?"assertion:final":"assertion"}var Gl=`When you heal, tell the reviewer WHAT KIND of change you adapted to, in saffron_step's adaptation and again in saffron_done's narrative, and name the page (its URL path) where it happened. Pick the kind that fits: (1) locator or label change: the same control on the same page, found differently (renamed button, new test id, native select instead of a custom one); (2) page removed or merged: a page no longer exists and its fields or controls now live on another page, so a navigation step (e.g. a 'Continue' click) is no longer needed; (3) page added or flow reordered: a new intermediate page, dialog or step order; (4) timing only: nothing changed, the element was slow. When you skip or add a click, key press or navigation the step used to perform, say so explicitly, e.g. "The guest information page is gone: its fields are now on /checkout, so the 'Continue to payment' click was dropped." Never describe a flow change as a locator change.`;function In(e){let t=e.liveTab;if(t)return t.count<=1?`Live page: the only open tab, at ${t.url}. Work on it; do not open or select another tab to start over.`:`Live page: tab ${t.index} of ${t.count} open tabs, at ${t.url}. This is where replay left off. If browser_tabs shows another tab as current, select tab ${t.index} before anything else (Saffron does not record that selection); never start over on another tab.`}function ua(e){let t=e.failedStep?.stepIndex;return`ANNOUNCE FIRST: call saffron_step for ${t===void 0?"the failed step":`the failed step (index ${t})`} BEFORE any state-changing browser action (click, type, select, key press, navigation, tab change). To diagnose, look with browser_snapshot, never with clicks or navigation: Saffron discards every action made before that announcement, and going back through earlier steps changes the state replay reached.`}function ha(e){let{scenario:t}=e,n=t.steps.map((s,o)=>{let a=d=>$t(ps(ft(d,e.uniqueSeed??""))),i=`${o}. [${Jl(t.steps,o)}] ${s.keyword} ${a(s.resolvedText)}`,c=s.resolvedTable??s.table,l=s.resolvedDocString??s.docString,f=i;return c&&(f+=`
87
87
  ${c.map(d=>` | ${d.map(a).join(" | ")} |`).join(`
88
- `)}`),p!==void 0&&(f+=`
88
+ `)}`),l!==void 0&&(f+=`
89
89
  """
90
- ${a(p).split(`
90
+ ${a(l).split(`
91
91
  `).map(d=>` ${d}`).join(`
92
92
  `)}
93
93
  """`),f}).join(`
94
- `),r=[`Feature: ${t.featureName} (${t.featurePath})`,`Scenario: ${t.displayName}`,e.baseURL?`Base URL: ${e.baseURL}`:void 0,"","Steps (index. [kind] keyword text):",n,""];if(e.mode==="refine")r.push("MODE: REFINE. You (or a previous agent session) just recorded this scenario and it completed successfully \u2014 but the deterministic zero-AI replay of that recording FAILED at the step below. The application is working; the RECORDING is not replayable.","The browser tab shows the state where the proof replay stopped.",Rn(e),"",`Steps that replayed fine:
95
- ${(e.completedSteps??[]).map(o=>` \u2713 ${o}`).join(`
96
- `)||" (none)"}`,"",`Failed step: [${e.failedStep?.kind}] ${e.failedStep?.text}`,`Replay failure: ${e.failedStep?.error}`,"","Typical causes: a locator that only matched during your session (volatile accessible name \u2014 use nameRegex or a shorter stable name), a literal dynamic value baked into an assertion (use kind=matches/differs/captureText), or an assertion that cannot be checked without navigation (use kind=attribute for link targets).","Re-perform and re-record the failed step and every step after it in a REPLAYABLE form via saffron_step + browser tools / saffron_capture / saffron_assert. Express the SAME conditions the steps state \u2014 do not weaken them.","If a step genuinely cannot be expressed replayably, call saffron_done success=false and explain why.",ca(e));else if(e.mode==="heal")r.push("MODE: HEAL. A cached deterministic replay of this scenario failed mid-run.","The browser tab already holds the page state reached so far \u2014 do NOT restart from step 0; verify where you are with a snapshot and continue from the failed step.",Rn(e),"",`Steps already completed by replay:
97
- ${(e.completedSteps??[]).map(o=>` \u2713 ${o}`).join(`
98
- `)||" (none)"}`,"",`Failed step: [${e.failedStep?.kind}] ${e.failedStep?.text}`,`Failure: ${e.failedStep?.error}`,e.failedStep?.pageUrl?`Page when replay failed: ${e.failedStep.pageUrl}`:void 0,"","First diagnose: did the UI legitimately change (heal: adapt the actions, record via saffron_step + browser tools), or is the application broken (fail: saffron_done success=false with your diagnosis)?",zl,e.assertionPolicy==="adaptable-mid"?"If the failed step is an [assertion:final]: you may re-verify the SAME condition once against the live page (timing), but if it does not hold AS WRITTEN, the run FAILS \u2014 call saffron_done success=false; never substitute it. If the failed step is a mid-scenario [assertion]: this project's policy allows adapting it \u2014 verify the equivalent condition on the legitimately-changed UI, record it via saffron_assert, and report the deviation via saffron_step's adaptation field plus featureEdits. An application defect still fails.":"If the failed step is an [assertion] or [assertion:final]: you may re-verify the SAME condition once against the live page (it may have been a timing issue). If the condition AS WRITTEN does not hold, the run FAILS \u2014 call saffron_done success=false. Do NOT substitute a different element, text, or URL that 'means the same thing'; suggesting a corrected assertion belongs in suggestedFeatureEdit of your failure report, never in saffron_assert.","Announce (saffron_step) and perform only the failed step and the steps after it. Earlier steps keep their recordings.",ca(e));else if(e.mode==="record"&&e.recordOnly){let{from:o,to:s}=e.recordOnly;r.push("MODE: RECORD (segment). This scenario is being composed from existing recordings plus your work.",e.completedSteps&&e.completedSteps.length>0?`Already executed against the live browser (do NOT redo):
94
+ `),r=[`Feature: ${t.featureName} (${t.featurePath})`,`Scenario: ${t.displayName}`,e.baseURL?`Base URL: ${e.baseURL}`:void 0,"","Steps (index. [kind] keyword text):",n,""];if(e.mode==="refine")r.push("MODE: REFINE. You (or a previous agent session) just recorded this scenario and it completed successfully \u2014 but the deterministic zero-AI replay of that recording FAILED at the step below. The application is working; the RECORDING is not replayable.","The browser tab shows the state where the proof replay stopped.",In(e),"",`Steps that replayed fine:
95
+ ${(e.completedSteps??[]).map(s=>` \u2713 ${s}`).join(`
96
+ `)||" (none)"}`,"",`Failed step: [${e.failedStep?.kind}] ${e.failedStep?.text}`,`Replay failure: ${e.failedStep?.error}`,"","Typical causes: a locator that only matched during your session (volatile accessible name \u2014 use nameRegex or a shorter stable name), a literal dynamic value baked into an assertion (use kind=matches/differs/captureText), or an assertion that cannot be checked without navigation (use kind=attribute for link targets).","Re-perform and re-record the failed step and every step after it in a REPLAYABLE form via saffron_step + browser tools / saffron_capture / saffron_assert. Express the SAME conditions the steps state \u2014 do not weaken them.","If a step genuinely cannot be expressed replayably, call saffron_done success=false and explain why.",ua(e));else if(e.mode==="heal")r.push("MODE: HEAL. A cached deterministic replay of this scenario failed mid-run.","The browser tab already holds the page state reached so far \u2014 do NOT restart from step 0; verify where you are with a snapshot and continue from the failed step.",In(e),"",`Steps already completed by replay:
97
+ ${(e.completedSteps??[]).map(s=>` \u2713 ${s}`).join(`
98
+ `)||" (none)"}`,"",`Failed step: [${e.failedStep?.kind}] ${e.failedStep?.text}`,`Failure: ${e.failedStep?.error}`,e.failedStep?.pageUrl?`Page when replay failed: ${e.failedStep.pageUrl}`:void 0,"","First diagnose: did the UI legitimately change (heal: adapt the actions, record via saffron_step + browser tools), or is the application broken (fail: saffron_done success=false with your diagnosis)?",Gl,e.assertionPolicy==="adaptable-mid"?"If the failed step is an [assertion:final]: you may re-verify the SAME condition once against the live page (timing), but if it does not hold AS WRITTEN, the run FAILS \u2014 call saffron_done success=false; never substitute it. If the failed step is a mid-scenario [assertion]: this project's policy allows adapting it \u2014 verify the equivalent condition on the legitimately-changed UI, record it via saffron_assert, and report the deviation via saffron_step's adaptation field plus featureEdits. An application defect still fails.":"If the failed step is an [assertion] or [assertion:final]: you may re-verify the SAME condition once against the live page (it may have been a timing issue). If the condition AS WRITTEN does not hold, the run FAILS \u2014 call saffron_done success=false. Do NOT substitute a different element, text, or URL that 'means the same thing'; suggesting a corrected assertion belongs in suggestedFeatureEdit of your failure report, never in saffron_assert.","Announce (saffron_step) and perform only the failed step and the steps after it. Earlier steps keep their recordings.",ua(e));else if(e.mode==="record"&&e.recordOnly){let{from:s,to:o}=e.recordOnly;r.push("MODE: RECORD (segment). This scenario is being composed from existing recordings plus your work.",e.completedSteps&&e.completedSteps.length>0?`Already executed against the live browser (do NOT redo):
99
99
  ${e.completedSteps.map(a=>` \u2713 ${a}`).join(`
100
- `)}`:"You are starting from a blank page.",Rn(e),"",`Record ONLY steps ${o} through ${s} (inclusive).`,`CRITICAL STOP RULE: the steps AFTER step ${s} already have recordings and will be replayed automatically the moment you stop. After completing step ${s}, immediately call saffron_done \u2014 do NOT perform, verify, or navigate into any later step. Executing a later step yourself would make the automatic replay run it TWICE.`)}else if(e.completedSteps&&e.completedSteps.length>0){let o=e.completedSteps.length;r.push("MODE: RECORD (seeded). No cache exists for this scenario yet, but its first steps matched existing recordings and were ALREADY EXECUTED against the live browser by zero-AI replay:",e.completedSteps.map(s=>` \u2713 ${s}`).join(`
101
- `),"",`The browser currently shows the state after those ${o} step(s) (verify with a snapshot if unsure). Do NOT redo them and do NOT navigate away from the current state.`,Rn(e),`Start with saffron_step for step index ${o} and record from there to the end.`)}else r.push("MODE: RECORD. No cache exists for this scenario yet.","Execute every step in order, starting at step 0. Navigate to the base URL first if the scenario implies a starting page.");return r.filter(o=>o!==void 0).join(`
102
- `)}import{query as ql}from"@anthropic-ai/claude-agent-sdk";var Hl="usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET";async function Or(e,t=8e3){let n=e?.[Hl];if(typeof n!="function")return;let r;try{let o=await Promise.race([n.call(e),new Promise(s=>{r=setTimeout(()=>s(void 0),t),r.unref?.()})]);return Vl(o)}catch{return}finally{r&&clearTimeout(r)}}function Vl(e){if(!e||!e.rate_limits_available||!e.subscription_type)return;let t=e.rate_limits?.five_hour;return{subscriptionType:e.subscription_type,utilization:typeof t?.utilization=="number"?t.utilization:void 0,resetsAt:t?.resets_at??void 0}}async function pa(){let e=()=>{},t=new Promise(o=>e=o);async function*n(){await t}let r;try{let o=ql({prompt:n(),options:{maxTurns:1,tools:[],settingSources:[],persistSession:!1}});r=o;let s=(async()=>{try{for await(let i of o);}catch{}})(),a=await Or(o);return e(),r.close?.(),await s,a}catch{e(),r?.close?.();return}}var Gl=Wl(import.meta.url);function Kl(){let e=Gl.resolve("@playwright/mcp/package.json");return fa.join(fa.dirname(e),"cli.js")}var Yl=new Set(["mcp__playwright__browser_navigate","mcp__playwright__browser_click","mcp__playwright__browser_type","mcp__playwright__browser_fill_form","mcp__playwright__browser_select_option","mcp__playwright__browser_press_key"]),Ql=new Set(["mcp__playwright__browser_snapshot","mcp__playwright__browser_find","mcp__playwright__browser_wait_for","mcp__playwright__browser_take_screenshot","mcp__playwright__browser_console_messages","mcp__playwright__browser_network_requests","mcp__playwright__browser_network_request","mcp__saffron__saffron_step","mcp__saffron__saffron_capture","mcp__saffron__saffron_dialog","mcp__saffron__saffron_wait_api","mcp__saffron__saffron_assert","mcp__saffron__saffron_done"]);function ua(e,t){if(Ql.has(e))return!1;if(e==="mcp__playwright__browser_tabs"){let n=t?.action;return!(n==="list"||n==="select"||n==="close")}return!0}var En=class{constructor(t={}){this.opts=t}opts;planBefore;traceBridgeUnavailable=!1;async planUsage(){if(!this.planBefore)return;let t=await this.planBefore;if(!t)return;let n=await pa();return{subscriptionType:t.subscriptionType,fiveHourBefore:t.utilization,fiveHourAfter:n?.utilization,resetsAt:n?.resetsAt??t.resetsAt}}async run(t){let n=new vn(t.scenario,{firstStep:jo(t),discardBeforeAnnounce:_o(t)}),r={aiCalls:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreationTokens:0},o="",s=()=>{let f=n.currentStepIndex,d=t.scenario.steps[f];return{index:f,title:d?`${d.keyword} ${d.text}`:"Agent exploration"}},a;if(t.traceContext&&!this.traceBridgeUnavailable){let f=$e(t.scenario);try{a=await ra(t.traceContext,s,this.opts.snapshotMode??"none",d=>{let m=d instanceof Error?d.message:String(d);console.warn(`A promise rejected during the agent session and nothing handled it: ${Te(m,f)}`)})}catch(d){this.traceBridgeUnavailable=!0;let m=Te(d instanceof Error?d.message:String(d),f);console.warn(`Agent tracing could not start (${m}). Continuing through the standard browser connection; agent calls will be absent from these traces.`)}}let i=f=>{let d="note the page for the recording";return a?a.grouped(d,f):t.traceContext?jr(t.traceContext,s(),d,f):f()},c;try{c=Jl({prompt:da(t),options:{systemPrompt:la({deliberateSnapshots:(this.opts.snapshotMode??"none")==="none",adaptableMidAssertions:t.assertionPolicy==="adaptable-mid"}),model:t.model??this.opts.model,maxTurns:this.opts.maxTurns??100,tools:[],permissionMode:"bypassPermissions",allowDangerouslySkipPermissions:!0,persistSession:!1,settingSources:[],mcpServers:{playwright:a?.config??{type:"stdio",command:process.execPath,args:[Kl(),"--cdp-endpoint",t.cdpEndpoint,"--snapshot-mode",this.opts.snapshotMode??"none","--image-responses","omit"]},saffron:ia(n)},hooks:{PreToolUse:[{hooks:[async f=>{let d=f;if(n.beginToolCall(d.tool_use_id),Yl.has(d.tool_name)&&t.takeFingerprint&&!n.pendingFingerprint)try{n.pendingFingerprint=await i(t.takeFingerprint)}catch{}return{continue:!0}}]}],PostToolUse:[{hooks:[async f=>{let d=f,m=Vo(d.tool_name,d.tool_input);m&&n.addArmedAction(m);let h=Ho(d.tool_name,d.tool_input,t.baseURL);if(h&&n.addActions([h]),d.tool_name.startsWith("mcp__playwright__")){let y=Oo(d.tool_response);y!==void 0&&t.onActiveTab?.(y),n.addActions(Fo(d.tool_response,t.baseURL)),n.pendingFingerprint=void 0}return n.endToolCall(d.tool_use_id,ua(d.tool_name,d.tool_input)),{continue:!0}}]}],PostToolUseFailure:[{hooks:[async f=>{let d=f;return d.tool_name.startsWith("mcp__playwright__")&&(n.pendingFingerprint=void 0),n.endToolCall(d.tool_use_id,ua(d.tool_name,d.tool_input)),{continue:!0}}]}]}}})}catch(f){throw await a?.close(),f}this.planBefore||(this.planBefore=Or(c));let p=t.onTabOpened?.(()=>n.markTabOpened());try{for await(let f of c)f.type==="result"&&(r.aiCalls=f.num_turns,r.costUsd=f.total_cost_usd,"modelUsage"in f&&f.modelUsage&&(r.models=Object.keys(f.modelUsage)),"usage"in f&&f.usage&&(r.inputTokens=f.usage.input_tokens??0,r.outputTokens=f.usage.output_tokens??0,r.cacheReadTokens=f.usage.cache_read_input_tokens??0,r.cacheCreationTokens=f.usage.cache_creation_input_tokens??0),f.subtype==="success"&&(o=f.result))}catch(f){let d=f instanceof Error?f.message:String(f);n.done=!0,n.success=!1,n.narrative=`Agent session error: ${d}`}finally{p?.(),await a?.close()}return n.done||(n.success=!1,n.narrative=n.narrative||`Agent session ended without finalizing the run. Last output: ${o.slice(0,500)}`),{success:n.success,narrative:n.narrative,adaptations:[...n.adaptations,...n.attributionNotes()],suggestedFeatureEdit:n.suggestedFeatureEdit,featureEdits:n.featureEdits.map(f=>({index:f.index,text:Ir(f.text,t.scenario.params)})),recordedSteps:Bo(zo(qo(Lo(Mo(Do(n.steps,t.scenario,f=>St(ft(f,t.uniqueSeed??""))),t.scenario.params)),Kt(X(t.scenario))),ut(X(t.scenario),t.uniqueSeed??"")),$e(t.scenario),X(t.scenario)),announcedSteps:[...n.announced].sort((f,d)=>f-d),usage:r}}};function An(e){return`$${e.toFixed(e>=1?2:4)}`}function Fr(e,t){let n=e===void 0?"?":`${Math.round(e)}%`,r=t===void 0?"?":`${Math.round(t)}%`;return`${n} \u2192 ${r}`}function Nr(e){return e.plan?` \xB7 5-hour window ${Fr(e.plan.fiveHourBefore,e.plan.fiveHourAfter)} (${e.plan.subscriptionType} plan) \xB7 \u2248 ${An(e.costUsd)} at API rates`:` \xB7 ${An(e.costUsd)}`}function ga(e,t){return e?t?`\u2248 ${An(e)}`:An(e):""}function ha(e){if(!e)return"";let t=new Date(e);return Number.isNaN(t.getTime())?"":`resets ${t.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}`}var Le=(e,t=0)=>typeof e=="number"?e:t,V=e=>typeof e=="string"?e:void 0;function Zl(e,t){if(!t)return;let n=V(t.selector);if(n)return Xl(n);let r=V(t.url);if(r)return r;if(e==="press"||e==="keyboardPress")return V(t.key)}function Xl(e){return e.replace(/internal:role=([a-z]+)\[name="((?:[^"\\]|\\.)*)"[is]?\]/g,(t,n,r)=>`${n} "${r}"`).replace(/internal:role=([a-z]+)/g,"$1").replace(/internal:label="((?:[^"\\]|\\.)*)"[is]?/g,'label "$1"').replace(/internal:text="((?:[^"\\]|\\.)*)"[is]?/g,'text "$1"').replace(/internal:attr=\[placeholder="((?:[^"\\]|\\.)*)"[is]?\]/g,'placeholder "$1"').replace(/internal:testid=\[data-testid="((?:[^"\\]|\\.)*)"s?\]/g,'test id "$1"').replace(/internal:has-text="((?:[^"\\]|\\.)*)"[is]?/g,'with text "$1"').replace(/ >> nth=0$/,"").replace(/ >> /g," \u203A ").replace(/nth=(\d+)/g,"#$1")}function ed(e){let t=new Map,n=[],r=[];for(let o of e){if(!o.trim())continue;let s;try{s=JSON.parse(o)}catch{continue}r.push(s);let a=V(s.callId);switch(s.type){case"before":{if(!a)break;let i=V(s.method)??"",c=s.class==="Tracing"&&i==="tracingGroup",p=i==="__waitInfo__";t.set(a,{callId:a,method:p?V(s.title)??"wait":i,subject:Zl(i,s.params),selector:V(s.params?.selector),startTime:Le(s.startTime),log:[],pageId:V(s.pageId),before:Mr(s,"beforeSnapshot"),calls:[],parentId:V(s.parentId),isGroup:c,title:V(s.title),isWait:p}),n.push(a);break}case"input":{let i=a?t.get(a):void 0;if(i){i.input=Mr({...s,pageId:i.pageId},"inputSnapshot");let c=s.point,p=s.box,f=c??(p?{x:p.x+p.width/2,y:p.y+p.height/2}:void 0);i.input&&f?i.input.point=f:f&&(i.inputPoint=f)}break}case"frame-snapshot":{let i=s.snapshot,c=i?.callId?t.get(i.callId):void 0;if(c&&i?.phase&&i.pageId&&i.isMainFrame!==!1){let p=i.phase,f={pageId:i.pageId,name:`${p}@${i.callId}`,callId:i.callId,phase:p};if(c.pageId||(c.pageId=i.pageId),p==="before"&&!c.before)c.before=f;else if(p==="after")c.after=f;else if(p==="action"&&!c.input){let d=c.inputPoint;c.input=d?{...f,point:d}:f}}break}case"log":{let i=a?t.get(a):void 0,c=V(s.message);i&&c&&i.log.push(c);break}case"after":{let i=a?t.get(a):void 0;if(!i)break;i.endTime=Le(s.endTime,i.startTime);let c=s.error;c?.message&&(i.error=c.message);let p=Mr({...s,pageId:i.pageId},"afterSnapshot");p&&(i.after=p);break}default:break}}return{calls:t,order:n,events:r}}function Mr(e,t){let n=V(e[t]),r=V(e.pageId);return n&&r?{pageId:r,name:n}:void 0}function ma(e){return Dr(qs(e))}function Dr(e){let n=e.filter(l=>l.name.endsWith(".trace")).flatMap(l=>l.data.toString("utf8").split(`
103
- `)),{calls:r,order:o,events:s}=ed(n),a=new Map;for(let l of s){let u=l.snapshot;if(l.type!=="frame-snapshot"||!u?.isMainFrame||!u.pageId||!u.frameUrl)continue;let x=u.snapshotName??(u.callId&&u.phase?`${u.phase}@${u.callId}`:void 0);x&&a.set(`${u.pageId}/${x}`,u.frameUrl)}for(let l of r.values())for(let u of[l.before,l.input,l.after])u&&(u.url=a.get(`${u.pageId}/${u.name}`));let i=[];for(let l of o){let u=r.get(l),x=u.parentId?r.get(u.parentId):void 0;x?x.calls.push(u):i.push(l)}let c=l=>{let{parentId:u,isGroup:x,title:g,isWait:S,...T}=l;return{...T,calls:T.calls.map(A=>c(A))}},p=l=>l.find(u=>u.error&&!u.isWait)?.error,f=[],d=[];for(let l of i){let u=r.get(l);if(!u.isGroup){d.push(c(u));continue}let x={title:u.title??"",startTime:u.startTime,endTime:u.endTime,error:u.error,actions:[],calls:[]},g=/^\[saffron:(agent|replay|pending):(-?\d+)\] (.*)$/.exec(x.title);g&&(x.phase=g[1],x.stepIndex=Number(g[2]),x.title=g[3]);let S=[];for(let T of u.calls)if(T.isGroup){let A={title:T.title??"",startTime:T.startTime,endTime:T.endTime,error:T.error??p(T.calls),calls:T.calls.map(C=>c(C))},E=/^\[saffron:action:(\d+)\] ([\s\S]*)$/.exec(A.title);E&&(A.actionIndex=Number(E[1]),A.title=E[2]),x.actions.push(A)}else S.push(T),x.calls.push(c(T));x.error||(x.error=x.actions.find(T=>T.error)?.error??p(S)),f.push(x)}let m=[],h=new Map,y,b,k,w=Number.POSITIVE_INFINITY,v=0;for(let l of s)if(l.type==="context-options")y=V(l.title),b=V(l.browserName),k=Number(l.version)||void 0,w=Math.min(w,Le(l.monotonicTime,w));else if(l.type==="screencast-frame"){let u=V(l.file)??(V(l.sha1)?`resources/${V(l.sha1)}`:""),x={pageId:V(l.pageId)??"",file:u,sha1:u.slice(u.lastIndexOf("/")+1),timestamp:Le(l.timestamp),width:Le(l.width),height:Le(l.height)};m.push(x),h.has(x.pageId)||h.set(x.pageId,{pageId:x.pageId})}else if(l.type==="event"&&l.class==="BrowserContext"&&l.method==="page"){let u=V(l.params?.pageId);u&&h.set(u,{...h.get(u),pageId:u,openedAt:Le(l.time)})}else if(l.type==="event"&&l.class==="BrowserContext"&&l.method==="pageClosed"){let u=V(l.params?.pageId);u&&h.set(u,{...h.get(u),pageId:u,closedAt:Le(l.time)})}else if(l.type==="frame-snapshot"){let u=l.snapshot;if(u?.pageId){let x=h.get(u.pageId)??{pageId:u.pageId};u.isMainFrame&&u.frameUrl&&(x.url=u.frameUrl),h.set(u.pageId,x)}}for(let l of r.values())w=Math.min(w,l.startTime),v=Math.max(v,l.endTime??l.startTime);for(let l of m)v=Math.max(v,l.timestamp);return Number.isFinite(w)||(w=0),{format:k,title:y,browserName:b,startTime:w,endTime:v,steps:f,calls:d,frames:m,requests:td(e),pages:[...h.values()],resources:e.filter(l=>l.name.startsWith("resources/")).map(l=>l.name.slice(10))}}function td(e){let t=[];for(let n of e.filter(r=>r.name.endsWith(".network")))for(let r of n.data.toString("utf8").split(`
104
- `)){if(!r.trim())continue;let o;try{o=JSON.parse(r)}catch{continue}if(o.type!=="resource-snapshot"||!o.snapshot)continue;let s=o.snapshot,a=s.request,i=s.response,c=s._monotonicTime;t.push({pageId:V(s.pageref),method:a?.method??"GET",url:a?.url??"",status:i?.status,mimeType:i?.content?.mimeType,startedAt:Date.parse(V(s.startedDateTime)??"")||0,time:typeof s.time=="number"?s.time:void 0,timestamp:typeof c=="number"?c:void 0})}return t}import xt from"node:fs";import{randomBytes as Aa}from"node:crypto";import ya from"node:fs";import wt from"node:path";var ba="This run has no reviewable proposal identity. Run the scenario again to review from replay.",Lr="The proposal does not belong to this replay.",wa=e=>e instanceof Error?e.message:String(e),xa=(e,t)=>`No pending proposal at ${wt.relative(e,t)}: it has been accepted, rejected or removed since this run.`;function va(e,t){let n=/ \(example \d+\)$/.exec(t.scenario),r=n?[t.scenario,t.scenario.slice(0,n.index)]:[t.scenario],o=s=>s.split(/[\\/]/).slice(-2).join("/");return r.map(s=>ue(e,t.feature,s)).find(s=>o(s)===o(t.proposalFile))}function Sa(e,t,n){if(At(n)!==t.proposalRevision)return{available:!1,reason:"The proposal changed since this run. Open the latest run before reviewing it."};let r=i=>({available:!1,reason:i,rejectable:!0}),o;try{o=pt(n)}catch(i){if(i instanceof W)return r(`The proposal cannot be read: ${wt.relative(e,n)}: ${i.problem}`);throw i}let{feature:s,scenario:a}=o.cache;if(wt.resolve(ue(e,s,a))!==wt.resolve(n)||s!==t.feature||!(t.scenario===a||t.scenario===o.meta.recordedFor||t.scenario.startsWith(a+" (example ")))return{available:!1,reason:Lr};try{if(!ya.existsSync(wt.resolve(e,s)))return r(`STALE: ${s} no longer exists`);let i=ke(e,ae(e),[wt.resolve(e,s)]),c=Re(e,o,i);if(c)return r(`STALE: ${c}`);let p=t.status==="red",f=o.meta.verified===!0&&!p,d=p?t.error??"This run replayed the proposal and it failed.":o.meta.proofError;return{available:!0,verified:f,...!f&&d?{proofError:d}:{},unbound:!o.meta.bound,diff:gt(oe(J(e,s,a)),o.cache),featureEdits:o.meta.featureEdits??[]}}catch(i){return r(wa(i))}}function ka(e,t){if(!t.proposalFile||!t.proposalRevision)return{available:!1,reason:ba};try{let n=va(e,{...t,proposalFile:t.proposalFile});return n?ya.existsSync(n)?Sa(e,t,n):{available:!1,reason:xa(e,n)}:{available:!1,reason:Lr}}catch(n){return{available:!1,reason:wa(n)}}}function $a(e,t,n,r=!1){if(!t.proposalFile||!t.proposalRevision)throw new Error(ba);let o=va(e,{...t,proposalFile:t.proposalFile});if(!o)throw new Error(Lr);fn(o,()=>{let s=Sa(e,t,o);if(n==="reject"){if(!s.available&&!s.rejectable)throw new Error(s.reason);hn(o)}else{if(!s.available)throw new Error(s.reason);if(!s.verified&&!r)throw new Error("Confirm that you want to accept a proposal without successful proof replay.");gn(e,o)}},()=>{throw new Error(xa(e,o))})}import hd from"node:http";import Ze from"node:path";var N=e=>e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;"),Ra="M11 14 L10 4.5 L17.5 9.5 L24 2.5 L30.5 9.5 L38 4.5 L37 14 Z",nd="M24.085 45.646L24.085 45.646Q20.719 45.646 18.407 44.575Q16.095 43.504 14.905 41.651Q13.715 39.798 13.715 37.384L13.715 37.384L19.121 37.384Q19.121 38.336 19.648 39.152Q20.175 39.968 21.263 40.461Q22.351 40.954 24.085 40.954L24.085 40.954Q25.649 40.954 26.703 40.546Q27.757 40.138 28.301 39.407Q28.845 38.676 28.845 37.724L28.845 37.724Q28.845 36.534 27.825 35.837Q26.805 35.140 24.527 34.936L24.527 34.936L22.657 34.766Q18.951 34.460 16.741 32.454Q14.531 30.448 14.531 27.150L14.531 27.150Q14.531 24.770 15.670 23.036Q16.809 21.302 18.849 20.367Q20.889 19.432 23.677 19.432L23.677 19.432Q26.601 19.432 28.692 20.435Q30.783 21.438 31.905 23.257Q33.027 25.076 33.027 27.524L33.027 27.524L27.587 27.524Q27.587 26.606 27.145 25.841Q26.703 25.076 25.836 24.600Q24.969 24.124 23.677 24.124L23.677 24.124Q22.453 24.124 21.620 24.532Q20.787 24.940 20.379 25.637Q19.971 26.334 19.971 27.150L19.971 27.150Q19.971 28.204 20.719 28.986Q21.467 29.768 23.167 29.904L23.167 29.904L25.071 30.074Q27.791 30.312 29.865 31.247Q31.939 32.182 33.112 33.797Q34.285 35.412 34.285 37.724L34.285 37.724Q34.285 40.104 33.044 41.889Q31.803 43.674 29.525 44.660Q27.247 45.646 24.085 45.646Z";function Br(e){return`<svg viewBox="0 0 48 48" width="${e}" height="${e}" aria-hidden="true">
105
- <path d="${Ra}" fill="#F4A300"/>
106
- <path d="${nd}" fill="#E7E9EC"/>
107
- </svg>`}function rd(e){return`<svg viewBox="8 1 32 15" height="${e}" aria-hidden="true"><path d="${Ra}" fill="#F4A300"/></svg>`}var sd={green:"passed",yellow:"pending review",red:"failed"};function od(e){return e.hooks?.length?`<div class="panel"><div class="panel-title">setup and teardown</div>${e.hooks.map(n=>{let r=n.exitCode===0,o=r?"ok":n.timedOut?"timed out":n.exitCode===null?"could not start":`exit ${n.exitCode}`;return`<div class="mono">${r?"\u2713":"\u2717"} ${N(String(n.phase))} \xB7 ${N(n.command)} \xB7 ${N(o)} \xB7 ${N(Bt(n.durationMs))}</div>`}).join("")}</div>`:""}function Bt(e){return e>=1e3?`${(e/1e3).toFixed(1)}s`:`${e}ms`}function Ut(e){return e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e4?`${(e/1e3).toFixed(1)}k`:e.toLocaleString()}function Ie(e,t,n,r="",o="",s=""){return`<div class="kpi">
100
+ `)}`:"You are starting from a blank page.",In(e),"",`Record ONLY steps ${s} through ${o} (inclusive).`,`CRITICAL STOP RULE: the steps AFTER step ${o} already have recordings and will be replayed automatically the moment you stop. After completing step ${o}, immediately call saffron_done \u2014 do NOT perform, verify, or navigate into any later step. Executing a later step yourself would make the automatic replay run it TWICE.`)}else if(e.completedSteps&&e.completedSteps.length>0){let s=e.completedSteps.length;r.push("MODE: RECORD (seeded). No cache exists for this scenario yet, but its first steps matched existing recordings and were ALREADY EXECUTED against the live browser by zero-AI replay:",e.completedSteps.map(o=>` \u2713 ${o}`).join(`
101
+ `),"",`The browser currently shows the state after those ${s} step(s) (verify with a snapshot if unsure). Do NOT redo them and do NOT navigate away from the current state.`,In(e),`Start with saffron_step for step index ${s} and record from there to the end.`)}else r.push("MODE: RECORD. No cache exists for this scenario yet.","Execute every step in order, starting at step 0. Navigate to the base URL first if the scenario implies a starting page.");return r.filter(s=>s!==void 0).join(`
102
+ `)}import{query as Kl}from"@anthropic-ai/claude-agent-sdk";var Yl="usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET";async function Dr(e,t=8e3){let n=e?.[Yl];if(typeof n!="function")return;let r;try{let s=await Promise.race([n.call(e),new Promise(o=>{r=setTimeout(()=>o(void 0),t),r.unref?.()})]);return Ql(s)}catch{return}finally{r&&clearTimeout(r)}}function Ql(e){if(!e||!e.rate_limits_available||!e.subscription_type)return;let t=e.rate_limits?.five_hour;return{subscriptionType:e.subscription_type,utilization:typeof t?.utilization=="number"?t.utilization:void 0,resetsAt:t?.resets_at??void 0}}async function ma(){let e=()=>{},t=new Promise(s=>e=s);async function*n(){await t}let r;try{let s=Kl({prompt:n(),options:{maxTurns:1,tools:[],settingSources:[],persistSession:!1}});r=s;let o=(async()=>{try{for await(let i of s);}catch{}})(),a=await Dr(s);return e(),r.close?.(),await o,a}catch{e(),r?.close?.();return}}var ed=Zl(import.meta.url);function td(){let e=ed.resolve("@playwright/mcp/package.json");return ya.join(ya.dirname(e),"cli.js")}var nd=new Set(["mcp__playwright__browser_navigate","mcp__playwright__browser_click","mcp__playwright__browser_type","mcp__playwright__browser_fill_form","mcp__playwright__browser_select_option","mcp__playwright__browser_press_key"]),rd=new Set(["mcp__playwright__browser_snapshot","mcp__playwright__browser_find","mcp__playwright__browser_wait_for","mcp__playwright__browser_take_screenshot","mcp__playwright__browser_console_messages","mcp__playwright__browser_network_requests","mcp__playwright__browser_network_request","mcp__saffron__saffron_step","mcp__saffron__saffron_capture","mcp__saffron__saffron_dialog","mcp__saffron__saffron_wait_api","mcp__saffron__saffron_assert","mcp__saffron__saffron_done"]);function ba(e,t){if(rd.has(e))return!1;if(e==="mcp__playwright__browser_tabs"){let n=t?.action;return!(n==="list"||n==="select"||n==="close")}return!0}var Cn=class{constructor(t={}){this.opts=t}opts;planBefore;traceBridgeUnavailable=!1;async planUsage(){if(!this.planBefore)return;let t=await this.planBefore;if(!t)return;let n=await ma();return{subscriptionType:t.subscriptionType,fiveHourBefore:t.utilization,fiveHourAfter:n?.utilization,resetsAt:n?.resetsAt??t.resetsAt}}async run(t){let n=new $n(t.scenario,{firstStep:Lo(t),discardBeforeAnnounce:Do(t)}),r={aiCalls:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreationTokens:0},s="",o=()=>{let f=n.currentStepIndex,d=t.scenario.steps[f];return{index:f,title:d?`${d.keyword} ${d.text}`:"Agent exploration"}},a;if(t.traceContext&&!this.traceBridgeUnavailable){let f=Te(t.scenario);try{a=await ca(t.traceContext,o,this.opts.snapshotMode??"none",d=>{let m=d instanceof Error?d.message:String(d);console.warn(`A promise rejected during the agent session and nothing handled it: ${Re(m,f)}`)})}catch(d){this.traceBridgeUnavailable=!0;let m=Re(d instanceof Error?d.message:String(d),f);console.warn(`Agent tracing could not start (${m}). Continuing through the standard browser connection; agent calls will be absent from these traces.`)}}let i=f=>{let d="note the page for the recording";return a?a.grouped(d,f):t.traceContext?Mr(t.traceContext,o(),d,f):f()},c;try{c=Xl({prompt:ha(t),options:{systemPrompt:ga({deliberateSnapshots:(this.opts.snapshotMode??"none")==="none",adaptableMidAssertions:t.assertionPolicy==="adaptable-mid"}),model:t.model??this.opts.model,maxTurns:this.opts.maxTurns??100,tools:[],permissionMode:"bypassPermissions",allowDangerouslySkipPermissions:!0,persistSession:!1,settingSources:[],mcpServers:{playwright:a?.config??{type:"stdio",command:process.execPath,args:[td(),"--cdp-endpoint",t.cdpEndpoint,"--snapshot-mode",this.opts.snapshotMode??"none","--image-responses","omit"]},saffron:fa(n)},hooks:{PreToolUse:[{hooks:[async f=>{let d=f;if(n.beginToolCall(d.tool_use_id),nd.has(d.tool_name)&&t.takeFingerprint&&!n.pendingFingerprint)try{n.pendingFingerprint=await i(t.takeFingerprint)}catch{}return{continue:!0}}]}],PostToolUse:[{hooks:[async f=>{let d=f,m=Yo(d.tool_name,d.tool_input);m&&n.addArmedAction(m);let h=Ko(d.tool_name,d.tool_input,t.baseURL);if(h&&n.addActions([h]),d.tool_name.startsWith("mcp__playwright__")){let y=Uo(d.tool_response);y!==void 0&&t.onActiveTab?.(y),n.addActions(Bo(d.tool_response,t.baseURL)),n.pendingFingerprint=void 0}return n.endToolCall(d.tool_use_id,ba(d.tool_name,d.tool_input)),{continue:!0}}]}],PostToolUseFailure:[{hooks:[async f=>{let d=f;return d.tool_name.startsWith("mcp__playwright__")&&(n.pendingFingerprint=void 0),n.endToolCall(d.tool_use_id,ba(d.tool_name,d.tool_input)),{continue:!0}}]}]}}})}catch(f){throw await a?.close(),f}this.planBefore||(this.planBefore=Dr(c));let l=t.onTabOpened?.(()=>n.markTabOpened());try{for await(let f of c)f.type==="result"&&(r.aiCalls=f.num_turns,r.costUsd=f.total_cost_usd,"modelUsage"in f&&f.modelUsage&&(r.models=Object.keys(f.modelUsage)),"usage"in f&&f.usage&&(r.inputTokens=f.usage.input_tokens??0,r.outputTokens=f.usage.output_tokens??0,r.cacheReadTokens=f.usage.cache_read_input_tokens??0,r.cacheCreationTokens=f.usage.cache_creation_input_tokens??0),f.subtype==="success"&&(s=f.result))}catch(f){let d=f instanceof Error?f.message:String(f);n.done=!0,n.success=!1,n.narrative=`Agent session error: ${d}`}finally{l?.(),await a?.close()}return n.done||(n.success=!1,n.narrative=n.narrative||`Agent session ended without finalizing the run. Last output: ${s.slice(0,500)}`),{success:n.success,narrative:n.narrative,adaptations:[...n.adaptations,...n.attributionNotes()],suggestedFeatureEdit:n.suggestedFeatureEdit,featureEdits:n.featureEdits.map(f=>({index:f.index,text:_r(f.text,t.scenario.params)})),recordedSteps:Vo(Jo(Go(Wo(qo(Ho(n.steps,t.scenario,f=>vt(ft(f,t.uniqueSeed??""))),t.scenario.params)),Yt(X(t.scenario))),ut(X(t.scenario),t.uniqueSeed??"")),Te(t.scenario),X(t.scenario)),announcedSteps:[...n.announced].sort((f,d)=>f-d),usage:r}}};function Pn(e){return`$${e.toFixed(e>=1?2:4)}`}function Lr(e,t){let n=e===void 0?"?":`${Math.round(e)}%`,r=t===void 0?"?":`${Math.round(t)}%`;return`${n} \u2192 ${r}`}function Ur(e){return e.plan?` \xB7 5-hour window ${Lr(e.plan.fiveHourBefore,e.plan.fiveHourAfter)} (${e.plan.subscriptionType} plan) \xB7 \u2248 ${Pn(e.costUsd)} at API rates`:` \xB7 ${Pn(e.costUsd)}`}function wa(e,t){return e?t?`\u2248 ${Pn(e)}`:Pn(e):""}function xa(e){if(!e)return"";let t=new Date(e);return Number.isNaN(t.getTime())?"":`resets ${t.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}`}var Ue=(e,t=0)=>typeof e=="number"?e:t,W=e=>typeof e=="string"?e:void 0;function sd(e,t){if(!t)return;let n=W(t.selector);if(n)return od(n);let r=W(t.url);if(r)return r;if(e==="press"||e==="keyboardPress")return W(t.key)}function od(e){return e.replace(/internal:role=([a-z]+)\[name="((?:[^"\\]|\\.)*)"[is]?\]/g,(t,n,r)=>`${n} "${r}"`).replace(/internal:role=([a-z]+)/g,"$1").replace(/internal:label="((?:[^"\\]|\\.)*)"[is]?/g,'label "$1"').replace(/internal:text="((?:[^"\\]|\\.)*)"[is]?/g,'text "$1"').replace(/internal:attr=\[placeholder="((?:[^"\\]|\\.)*)"[is]?\]/g,'placeholder "$1"').replace(/internal:testid=\[data-testid="((?:[^"\\]|\\.)*)"s?\]/g,'test id "$1"').replace(/internal:has-text="((?:[^"\\]|\\.)*)"[is]?/g,'with text "$1"').replace(/ >> nth=0$/,"").replace(/ >> /g," \u203A ").replace(/nth=(\d+)/g,"#$1")}function ad(e){let t=new Map,n=[],r=[];for(let s of e){if(!s.trim())continue;let o;try{o=JSON.parse(s)}catch{continue}r.push(o);let a=W(o.callId);switch(o.type){case"before":{if(!a)break;let i=W(o.method)??"",c=o.class==="Tracing"&&i==="tracingGroup",l=i==="__waitInfo__";t.set(a,{callId:a,method:l?W(o.title)??"wait":i,subject:sd(i,o.params),selector:W(o.params?.selector),startTime:Ue(o.startTime),log:[],pageId:W(o.pageId),before:Br(o,"beforeSnapshot"),calls:[],parentId:W(o.parentId),isGroup:c,title:W(o.title),isWait:l}),n.push(a);break}case"input":{let i=a?t.get(a):void 0;if(i){i.input=Br({...o,pageId:i.pageId},"inputSnapshot");let c=o.point,l=o.box,f=c??(l?{x:l.x+l.width/2,y:l.y+l.height/2}:void 0);i.input&&f?i.input.point=f:f&&(i.inputPoint=f)}break}case"frame-snapshot":{let i=o.snapshot,c=i?.callId?t.get(i.callId):void 0;if(c&&i?.phase&&i.pageId&&i.isMainFrame!==!1){let l=i.phase,f={pageId:i.pageId,name:`${l}@${i.callId}`,callId:i.callId,phase:l};if(c.pageId||(c.pageId=i.pageId),l==="before"&&!c.before)c.before=f;else if(l==="after")c.after=f;else if(l==="action"&&!c.input){let d=c.inputPoint;c.input=d?{...f,point:d}:f}}break}case"log":{let i=a?t.get(a):void 0,c=W(o.message);i&&c&&i.log.push(c);break}case"after":{let i=a?t.get(a):void 0;if(!i)break;i.endTime=Ue(o.endTime,i.startTime);let c=o.error;c?.message&&(i.error=c.message);let l=Br({...o,pageId:i.pageId},"afterSnapshot");l&&(i.after=l);break}default:break}}return{calls:t,order:n,events:r}}function Br(e,t){let n=W(e[t]),r=W(e.pageId);return n&&r?{pageId:r,name:n}:void 0}function Sa(e){return zr(Gs(e))}function zr(e){let n=e.filter(u=>u.name.endsWith(".trace")).flatMap(u=>u.data.toString("utf8").split(`
103
+ `)),{calls:r,order:s,events:o}=ad(n),a=new Map;for(let u of o){let p=u.snapshot;if(u.type!=="frame-snapshot"||!p?.isMainFrame||!p.pageId||!p.frameUrl)continue;let b=p.snapshotName??(p.callId&&p.phase?`${p.phase}@${p.callId}`:void 0);b&&a.set(`${p.pageId}/${b}`,p.frameUrl)}for(let u of r.values())for(let p of[u.before,u.input,u.after])p&&(p.url=a.get(`${p.pageId}/${p.name}`));let i=[];for(let u of s){let p=r.get(u),b=p.parentId?r.get(p.parentId):void 0;b?b.calls.push(p):i.push(u)}let c=u=>{let{parentId:p,isGroup:b,title:g,isWait:S,...T}=u;return{...T,calls:T.calls.map(A=>c(A))}},l=u=>u.find(p=>p.error&&!p.isWait)?.error,f=[],d=[];for(let u of i){let p=r.get(u);if(!p.isGroup){d.push(c(p));continue}let b={title:p.title??"",startTime:p.startTime,endTime:p.endTime,error:p.error,actions:[],calls:[]},g=/^\[saffron:(agent|replay|pending):(-?\d+)\] (.*)$/.exec(b.title);g&&(b.phase=g[1],b.stepIndex=Number(g[2]),b.title=g[3]);let S=[];for(let T of p.calls)if(T.isGroup){let A={title:T.title??"",startTime:T.startTime,endTime:T.endTime,error:T.error??l(T.calls),calls:T.calls.map(_=>c(_))},R=/^\[saffron:action:(\d+)\] ([\s\S]*)$/.exec(A.title);R&&(A.actionIndex=Number(R[1]),A.title=R[2]),b.actions.push(A)}else S.push(T),b.calls.push(c(T));b.error||(b.error=b.actions.find(T=>T.error)?.error??l(S)),f.push(b)}let m=[],h=new Map,y,x,k,w=Number.POSITIVE_INFINITY,v=0;for(let u of o)if(u.type==="context-options")y=W(u.title),x=W(u.browserName),k=Number(u.version)||void 0,w=Math.min(w,Ue(u.monotonicTime,w));else if(u.type==="screencast-frame"){let p=W(u.file)??(W(u.sha1)?`resources/${W(u.sha1)}`:""),b={pageId:W(u.pageId)??"",file:p,sha1:p.slice(p.lastIndexOf("/")+1),timestamp:Ue(u.timestamp),width:Ue(u.width),height:Ue(u.height)};m.push(b),h.has(b.pageId)||h.set(b.pageId,{pageId:b.pageId})}else if(u.type==="event"&&u.class==="BrowserContext"&&u.method==="page"){let p=W(u.params?.pageId);p&&h.set(p,{...h.get(p),pageId:p,openedAt:Ue(u.time)})}else if(u.type==="event"&&u.class==="BrowserContext"&&u.method==="pageClosed"){let p=W(u.params?.pageId);p&&h.set(p,{...h.get(p),pageId:p,closedAt:Ue(u.time)})}else if(u.type==="frame-snapshot"){let p=u.snapshot;if(p?.pageId){let b=h.get(p.pageId)??{pageId:p.pageId};p.isMainFrame&&p.frameUrl&&(b.url=p.frameUrl),h.set(p.pageId,b)}}for(let u of r.values())w=Math.min(w,u.startTime),v=Math.max(v,u.endTime??u.startTime);for(let u of m)v=Math.max(v,u.timestamp);return Number.isFinite(w)||(w=0),{format:k,title:y,browserName:x,startTime:w,endTime:v,steps:f,calls:d,frames:m,requests:id(e),pages:[...h.values()],resources:e.filter(u=>u.name.startsWith("resources/")).map(u=>u.name.slice(10))}}function id(e){let t=[];for(let n of e.filter(r=>r.name.endsWith(".network")))for(let r of n.data.toString("utf8").split(`
104
+ `)){if(!r.trim())continue;let s;try{s=JSON.parse(r)}catch{continue}if(s.type!=="resource-snapshot"||!s.snapshot)continue;let o=s.snapshot,a=o.request,i=o.response,c=o._monotonicTime;t.push({pageId:W(o.pageref),method:a?.method??"GET",url:a?.url??"",status:i?.status,mimeType:i?.content?.mimeType,startedAt:Date.parse(W(o.startedDateTime)??"")||0,time:typeof o.time=="number"?o.time:void 0,timestamp:typeof c=="number"?c:void 0})}return t}import xt from"node:fs";import{randomBytes as ja}from"node:crypto";import va from"node:fs";import wt from"node:path";var ka="This run has no reviewable proposal identity. Run the scenario again to review from replay.",qr="The proposal does not belong to this replay.",$a=e=>e instanceof Error?e.message:String(e),Ta=(e,t)=>`No pending proposal at ${wt.relative(e,t)}: it has been accepted, rejected or removed since this run.`;function Ra(e,t){let n=/ \(example \d+\)$/.exec(t.scenario),r=n?[t.scenario,t.scenario.slice(0,n.index)]:[t.scenario],s=o=>o.split(/[\\/]/).slice(-2).join("/");return r.map(o=>ue(e,t.feature,o)).find(o=>s(o)===s(t.proposalFile))}function Ea(e,t,n){if(At(n)!==t.proposalRevision)return{available:!1,reason:"The proposal changed since this run. Open the latest run before reviewing it."};let r=i=>({available:!1,reason:i,rejectable:!0}),s;try{s=pt(n)}catch(i){if(i instanceof V)return r(`The proposal cannot be read: ${wt.relative(e,n)}: ${i.problem}`);throw i}let{feature:o,scenario:a}=s.cache;if(wt.resolve(ue(e,o,a))!==wt.resolve(n)||o!==t.feature||!(t.scenario===a||t.scenario===s.meta.recordedFor||t.scenario.startsWith(a+" (example ")))return{available:!1,reason:qr};try{if(!va.existsSync(wt.resolve(e,o)))return r(`STALE: ${o} no longer exists`);let i=$e(e,ae(e),[wt.resolve(e,o)]),c=Ee(e,s,i);if(c)return r(`STALE: ${c}`);let l=t.status==="red",f=s.meta.verified===!0&&!l,d=l?t.error??"This run replayed the proposal and it failed.":s.meta.proofError;return{available:!0,verified:f,...!f&&d?{proofError:d}:{},unbound:!s.meta.bound,diff:gt(oe(J(e,o,a)),s.cache),featureEdits:s.meta.featureEdits??[]}}catch(i){return r($a(i))}}function Aa(e,t){if(!t.proposalFile||!t.proposalRevision)return{available:!1,reason:ka};try{let n=Ra(e,{...t,proposalFile:t.proposalFile});return n?va.existsSync(n)?Ea(e,t,n):{available:!1,reason:Ta(e,n)}:{available:!1,reason:qr}}catch(n){return{available:!1,reason:$a(n)}}}function Ia(e,t,n,r=!1){if(!t.proposalFile||!t.proposalRevision)throw new Error(ka);let s=Ra(e,{...t,proposalFile:t.proposalFile});if(!s)throw new Error(qr);un(s,()=>{let o=Ea(e,t,s);if(n==="reject"){if(!o.available&&!o.rejectable)throw new Error(o.reason);mn(s)}else{if(!o.available)throw new Error(o.reason);if(!o.verified&&!r)throw new Error("Confirm that you want to accept a proposal without successful proof replay.");hn(e,s)}},()=>{throw new Error(Ta(e,s))})}import Sd from"node:http";import Ze from"node:path";var N=e=>e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;"),Pa="M11 14 L10 4.5 L17.5 9.5 L24 2.5 L30.5 9.5 L38 4.5 L37 14 Z",cd="M24.085 45.646L24.085 45.646Q20.719 45.646 18.407 44.575Q16.095 43.504 14.905 41.651Q13.715 39.798 13.715 37.384L13.715 37.384L19.121 37.384Q19.121 38.336 19.648 39.152Q20.175 39.968 21.263 40.461Q22.351 40.954 24.085 40.954L24.085 40.954Q25.649 40.954 26.703 40.546Q27.757 40.138 28.301 39.407Q28.845 38.676 28.845 37.724L28.845 37.724Q28.845 36.534 27.825 35.837Q26.805 35.140 24.527 34.936L24.527 34.936L22.657 34.766Q18.951 34.460 16.741 32.454Q14.531 30.448 14.531 27.150L14.531 27.150Q14.531 24.770 15.670 23.036Q16.809 21.302 18.849 20.367Q20.889 19.432 23.677 19.432L23.677 19.432Q26.601 19.432 28.692 20.435Q30.783 21.438 31.905 23.257Q33.027 25.076 33.027 27.524L33.027 27.524L27.587 27.524Q27.587 26.606 27.145 25.841Q26.703 25.076 25.836 24.600Q24.969 24.124 23.677 24.124L23.677 24.124Q22.453 24.124 21.620 24.532Q20.787 24.940 20.379 25.637Q19.971 26.334 19.971 27.150L19.971 27.150Q19.971 28.204 20.719 28.986Q21.467 29.768 23.167 29.904L23.167 29.904L25.071 30.074Q27.791 30.312 29.865 31.247Q31.939 32.182 33.112 33.797Q34.285 35.412 34.285 37.724L34.285 37.724Q34.285 40.104 33.044 41.889Q31.803 43.674 29.525 44.660Q27.247 45.646 24.085 45.646Z";function Wr(e){return`<svg viewBox="0 0 48 48" width="${e}" height="${e}" aria-hidden="true">
105
+ <path d="${Pa}" fill="#F4A300"/>
106
+ <path d="${cd}" fill="#E7E9EC"/>
107
+ </svg>`}function ld(e){return`<svg viewBox="8 1 32 15" height="${e}" aria-hidden="true"><path d="${Pa}" fill="#F4A300"/></svg>`}var dd={green:"passed",yellow:"pending review",red:"failed"};function pd(e){return e.hooks?.length?`<div class="panel"><div class="panel-title">setup and teardown</div>${e.hooks.map(n=>{let r=n.exitCode===0,s=r?"ok":n.timedOut?"timed out":n.exitCode===null?"could not start":`exit ${n.exitCode}`;return`<div class="mono">${r?"\u2713":"\u2717"} ${N(String(n.phase))} \xB7 ${N(n.command)} \xB7 ${N(s)} \xB7 ${N(Bt(n.durationMs))}</div>`}).join("")}</div>`:""}function Bt(e){return e>=1e3?`${(e/1e3).toFixed(1)}s`:`${e}ms`}function Ut(e){return e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e4?`${(e/1e3).toFixed(1)}k`:e.toLocaleString()}function Ce(e,t,n,r="",s="",o=""){return`<div class="kpi">
108
108
  <div class="kpi-label">${e}</div>
109
109
  <div class="kpi-value ${r}">${t}</div>
110
- <div class="kpi-sub ${o}">${n}</div>${s}
111
- </div>`}function Ur(e,t){if(e.length<2)return"";let n=108,r=22,o=Math.min(...e),a=Math.max(...e)-o||1,i=e.map((c,p)=>`${(p/(e.length-1)*n).toFixed(1)},${(r-2-(c-o)/a*(r-4)).toFixed(1)}`).join(" ");return`<svg class="spark" viewBox="0 0 ${n} ${r}" width="${n}" height="${r}" aria-hidden="true"><polyline points="${i}" fill="none" stroke="${t}" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round"/></svg>`}var Ta=(e,t="",n=0)=>`${e>=0?"+":"\u2212"}${Math.abs(e).toFixed(n)}${t} vs prev`;function ad(e,t){let n=e.totals,r=e.trends,o=n.scenarios===0?0:Math.round((n.green+n.yellow)/n.scenarios*100),s=n.red>0?"red":n.yellow>0?"gold":"green",a=n.inputTokens+n.outputTokens,i=r?.prev?`${Ta(r.prev.passRatePp,"pp")} \xB7 ${n.green+n.yellow} of ${n.scenarios}`:`${n.green+n.yellow} of ${n.scenarios} scenarios`,c=r?.prev?`${Ta(r.prev.costUsd,"",4).replace("+","+$").replace("\u2212","\u2212$")} \xB7 ${Bt(t)}`:`${Bt(t)} total runtime`;return`<section class="kpis">
112
- ${Ie("pass rate",`${o}<span class="unit">%</span>`,i,s,r?.prev?r.prev.passRatePp>=0?"green":"red":"",Ur(r?.series.passRate??[],"var(--green)"))}
113
- ${Ie("passed",String(n.green),"deterministic replay","green",n.green>0?"green":"")}
114
- ${Ie("pending",String(n.yellow),n.yellow>0?"\u25B2 proposals to review":"nothing to review",n.yellow>0?"gold":"",n.yellow>0?"gold":"")}
115
- ${Ie("failed",String(n.red),n.red>0?"\u25BC needs attention":"all clear",n.red>0?"red":"",n.red>0?"red":"green")}
116
- ${Ie("ai calls",String(n.aiCalls),`${Ut(a)} in+out tokens`,n.aiCalls>0?"":"green")}
117
- ${Ie("cache traffic",Ut(n.cacheReadTokens+n.cacheCreationTokens),n.cacheReadTokens+n.cacheCreationTokens>0?`${Ut(n.cacheReadTokens)} read \xB7 ${Ut(n.cacheCreationTokens)} written`:"zero (fully cached run)","",n.cacheReadTokens+n.cacheCreationTokens>0?"":"green")}
118
- ${n.plan?Ie("plan usage",`${Fr(n.plan.fiveHourBefore,n.plan.fiveHourAfter)}`,[`5-hour window \xB7 ${n.plan.subscriptionType} plan`,ha(n.plan.resetsAt)].filter(Boolean).join(" \xB7 "),"gold","")+Ie("api-equivalent",`<span class="unit">\u2248 $</span>${n.costUsd.toFixed(n.costUsd>=1?2:4)}`,`what this run costs on an API key \xB7 ${c}`,"","",Ur(r?.series.costUsd??[],"var(--gold)")):Ie("ai cost",`<span class="unit">$</span>${n.costUsd.toFixed(n.costUsd>=1?2:4)}`,c,"","",Ur(r?.series.costUsd??[],"var(--gold)"))}
119
- </section>`}function id(e){let t=e.trends;if(!t)return"";let n=[];return t.chronic.length>0&&n.push(`<div class="panel accent-gold"><div class="panel-title gold">chronic scenarios: stop paying for heals</div><ul>${t.chronic.map(r=>`<li>${N(r.feature)} \u203A ${N(r.scenario)}: ${r.heals} heal(s), ${r.reds} red(s) in the last ${r.runs} run(s). Re-record it or fix the feature text.</li>`).join("")}</ul></div>`),t.divergentSteps>0&&n.push(`<div class="panel"><div class="panel-title">step divergence (level-2 signal)</div><p>${t.divergentSteps} step text(s) currently have divergent recordings across caches.</p></div>`),t.topIssues.length>0&&n.push(`<div class="panel accent-red"><div class="panel-title red">recurring failure themes (last 20 runs)</div><ul>${t.topIssues.map(r=>`<li>${r.count}\xD7: ${N(r.message)}</li>`).join("")}</ul></div>`),n.length>0?`<section class="trends">${n.join(`
120
- `)}</section>`:""}function cd(e){return`<ol class="steps">${e.steps.map(n=>{let r=n.status==="passed"?'<span class="dot green"></span>':n.status==="failed"?'<span class="dot red"></span>':'<span class="dot idle"></span>';return`<li class="step ${n.status}">
110
+ <div class="kpi-sub ${s}">${n}</div>${o}
111
+ </div>`}function Hr(e,t){if(e.length<2)return"";let n=108,r=22,s=Math.min(...e),a=Math.max(...e)-s||1,i=e.map((c,l)=>`${(l/(e.length-1)*n).toFixed(1)},${(r-2-(c-s)/a*(r-4)).toFixed(1)}`).join(" ");return`<svg class="spark" viewBox="0 0 ${n} ${r}" width="${n}" height="${r}" aria-hidden="true"><polyline points="${i}" fill="none" stroke="${t}" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round"/></svg>`}var Ca=(e,t="",n=0)=>`${e>=0?"+":"\u2212"}${Math.abs(e).toFixed(n)}${t} vs prev`;function fd(e,t){let n=e.totals,r=e.trends,s=n.scenarios===0?0:Math.round((n.green+n.yellow)/n.scenarios*100),o=n.red>0?"red":n.yellow>0?"gold":"green",a=n.inputTokens+n.outputTokens,i=r?.prev?`${Ca(r.prev.passRatePp,"pp")} \xB7 ${n.green+n.yellow} of ${n.scenarios}`:`${n.green+n.yellow} of ${n.scenarios} scenarios`,c=r?.prev?`${Ca(r.prev.costUsd,"",4).replace("+","+$").replace("\u2212","\u2212$")} \xB7 ${Bt(t)}`:`${Bt(t)} total runtime`;return`<section class="kpis">
112
+ ${Ce("pass rate",`${s}<span class="unit">%</span>`,i,o,r?.prev?r.prev.passRatePp>=0?"green":"red":"",Hr(r?.series.passRate??[],"var(--green)"))}
113
+ ${Ce("passed",String(n.green),"deterministic replay","green",n.green>0?"green":"")}
114
+ ${Ce("pending",String(n.yellow),n.yellow>0?"\u25B2 proposals to review":"nothing to review",n.yellow>0?"gold":"",n.yellow>0?"gold":"")}
115
+ ${Ce("failed",String(n.red),n.red>0?"\u25BC needs attention":"all clear",n.red>0?"red":"",n.red>0?"red":"green")}
116
+ ${Ce("ai calls",String(n.aiCalls),`${Ut(a)} in+out tokens`,n.aiCalls>0?"":"green")}
117
+ ${Ce("cache traffic",Ut(n.cacheReadTokens+n.cacheCreationTokens),n.cacheReadTokens+n.cacheCreationTokens>0?`${Ut(n.cacheReadTokens)} read \xB7 ${Ut(n.cacheCreationTokens)} written`:"zero (fully cached run)","",n.cacheReadTokens+n.cacheCreationTokens>0?"":"green")}
118
+ ${n.plan?Ce("plan usage",`${Lr(n.plan.fiveHourBefore,n.plan.fiveHourAfter)}`,[`5-hour window \xB7 ${n.plan.subscriptionType} plan`,xa(n.plan.resetsAt)].filter(Boolean).join(" \xB7 "),"gold","")+Ce("api-equivalent",`<span class="unit">\u2248 $</span>${n.costUsd.toFixed(n.costUsd>=1?2:4)}`,`what this run costs on an API key \xB7 ${c}`,"","",Hr(r?.series.costUsd??[],"var(--gold)")):Ce("ai cost",`<span class="unit">$</span>${n.costUsd.toFixed(n.costUsd>=1?2:4)}`,c,"","",Hr(r?.series.costUsd??[],"var(--gold)"))}
119
+ </section>`}function ud(e){let t=e.trends;if(!t)return"";let n=[];return t.chronic.length>0&&n.push(`<div class="panel accent-gold"><div class="panel-title gold">chronic scenarios: stop paying for heals</div><ul>${t.chronic.map(r=>`<li>${N(r.feature)} \u203A ${N(r.scenario)}: ${r.heals} heal(s), ${r.reds} red(s) in the last ${r.runs} run(s). Re-record it or fix the feature text.</li>`).join("")}</ul></div>`),t.divergentSteps>0&&n.push(`<div class="panel"><div class="panel-title">step divergence (level-2 signal)</div><p>${t.divergentSteps} step text(s) currently have divergent recordings across caches.</p></div>`),t.topIssues.length>0&&n.push(`<div class="panel accent-red"><div class="panel-title red">recurring failure themes (last 20 runs)</div><ul>${t.topIssues.map(r=>`<li>${r.count}\xD7: ${N(r.message)}</li>`).join("")}</ul></div>`),n.length>0?`<section class="trends">${n.join(`
120
+ `)}</section>`:""}function gd(e){return`<ol class="steps">${e.steps.map(n=>{let r=n.status==="passed"?'<span class="dot green"></span>':n.status==="failed"?'<span class="dot red"></span>':'<span class="dot idle"></span>';return`<li class="step ${n.status}">
121
121
  ${r}
122
122
  <div class="step-body">
123
123
  <span class="step-text">${N(n.gherkin)}</span>
@@ -128,13 +128,13 @@ ${e.completedSteps.map(a=>` \u2713 ${a}`).join(`
128
128
  ${n.error?`<div class="step-error">${N(n.error)}</div>`:""}
129
129
  </div>
130
130
  </li>`}).join(`
131
- `)}</ol>`}function ld(e){return e.featureEdits&&e.featureEdits.length>0?`<div class="panel">
131
+ `)}</ol>`}function hd(e){return e.featureEdits&&e.featureEdits.length>0?`<div class="panel">
132
132
  <div class="panel-title gold">suggested .feature edit</div>
133
133
  <div class="diff">${e.featureEdits.map(n=>{let r=e.steps[n.index]?.gherkin??`step ${n.index}`;return`<div class="diff-line del">\u2212 ${N(r)}</div><div class="diff-line add">+ ${N(n.text)}</div>`}).join("")}</div>
134
134
  <div class="panel-hint">apply with <code>saffron accept --with-feature-edit</code></div>
135
- </div>`:e.suggestedFeatureEdit?`<div class="panel"><div class="panel-title gold">suggested .feature edit</div><pre>${N(e.suggestedFeatureEdit)}</pre></div>`:""}function dd(e){let t=e.proposalDiff;if(!t)return"";let n=t.steps.filter(c=>c.change!=="same"||!t.hasCommitted).map(c=>{let p=`<div class="diff-step">${c.kind==="assertion"?'<span class="pill gold">assertion</span> ':""}${N(c.gherkin)}${c.fromStepSet?` <span class="mono">(step set "${N(c.fromStepSet)}")</span>`:""}</div>`,f=c.actions.flatMap(d=>d.change==="same"?[`<div class="diff-line"> ${N(d.before??"")}</div>`]:d.change==="added"?[`<div class="diff-line add">+ ${N(d.after??"")}</div>`]:d.change==="removed"?[`<div class="diff-line del">\u2212 ${N(d.before??"")}</div>`]:[`<div class="diff-line del">\u2212 ${N(d.before??"")}</div>`,`<div class="diff-line add">+ ${N(d.after??"")}</div>`,...(d.details??[]).map(m=>`<div class="diff-line detail">${N(m)}</div>`)]).join("");return`${p}<div class="diff">${f}</div>`}).join(""),{added:r,removed:o,changed:s,assertions:a}=t.summary,i=r+o+s===0?"No action changed.":`${s} changed, ${r} added, ${o} removed.${a>0?` ${a} touch the scenario's verdict.`:""}`;return`<div class="panel accent-gold"><div class="panel-title gold">what this proposal changes</div>${t.hasCommitted?"":'<p class="panel-hint">First recording: there is nothing to compare against.</p>'}${n}<div class="panel-hint">${N(i)}</div></div>`}function pd(e,t){let n=e.usage.inputTokens+e.usage.outputTokens;if(e.usage.aiCalls===0)return'<span class="pill green">0 tokens</span>';let r=ga(e.usage.costUsd,t),o=r?` \xB7 ${r}`:"";return`<span class="pill gold">${e.usage.aiCalls} ai \xB7 ${Ut(n)} tok${o}</span>`}function fd(e,t,n){return`<details class="card" ${e.status!=="green"?"open":""}>
135
+ </div>`:e.suggestedFeatureEdit?`<div class="panel"><div class="panel-title gold">suggested .feature edit</div><pre>${N(e.suggestedFeatureEdit)}</pre></div>`:""}function md(e){let t=e.proposalDiff;if(!t)return"";let n=t.steps.filter(c=>c.change!=="same"||!t.hasCommitted).map(c=>{let l=`<div class="diff-step">${c.kind==="assertion"?'<span class="pill gold">assertion</span> ':""}${N(c.gherkin)}${c.fromStepSet?` <span class="mono">(step set "${N(c.fromStepSet)}")</span>`:""}</div>`,f=c.actions.flatMap(d=>d.change==="same"?[`<div class="diff-line"> ${N(d.before??"")}</div>`]:d.change==="added"?[`<div class="diff-line add">+ ${N(d.after??"")}</div>`]:d.change==="removed"?[`<div class="diff-line del">\u2212 ${N(d.before??"")}</div>`]:[`<div class="diff-line del">\u2212 ${N(d.before??"")}</div>`,`<div class="diff-line add">+ ${N(d.after??"")}</div>`,...(d.details??[]).map(m=>`<div class="diff-line detail">${N(m)}</div>`)]).join("");return`${l}<div class="diff">${f}</div>`}).join(""),{added:r,removed:s,changed:o,assertions:a}=t.summary,i=r+s+o===0?"No action changed.":`${o} changed, ${r} added, ${s} removed.${a>0?` ${a} touch the scenario's verdict.`:""}`;return`<div class="panel accent-gold"><div class="panel-title gold">what this proposal changes</div>${t.hasCommitted?"":'<p class="panel-hint">First recording: there is nothing to compare against.</p>'}${n}<div class="panel-hint">${N(i)}</div></div>`}function yd(e,t){let n=e.usage.inputTokens+e.usage.outputTokens;if(e.usage.aiCalls===0)return'<span class="pill green">0 tokens</span>';let r=wa(e.usage.costUsd,t),s=r?` \xB7 ${r}`:"";return`<span class="pill gold">${e.usage.aiCalls} ai \xB7 ${Ut(n)} tok${s}</span>`}function bd(e,t,n){return`<details class="card" ${e.status!=="green"?"open":""}>
136
136
  <summary>
137
- <span class="pill ${e.status==="green"?"green":e.status==="yellow"?"gold":"red"}">${sd[e.status]}</span>
137
+ <span class="pill ${e.status==="green"?"green":e.status==="yellow"?"gold":"red"}">${dd[e.status]}</span>
138
138
  <span class="card-title">${N(e.scenario)}</span>
139
139
  <span class="card-meta">
140
140
  <span class="m">${N(e.feature)}</span>
@@ -142,23 +142,23 @@ ${e.completedSteps.map(a=>` \u2713 ${a}`).join(`
142
142
  <span class="m">${N(e.source)}</span>
143
143
  <span class="m-sep">\xB7</span>
144
144
  <span class="m">${Bt(e.durationMs)}</span>
145
- ${pd(e,t)}
145
+ ${yd(e,t)}
146
146
  </span>
147
147
  </summary>
148
148
  <div class="card-body">
149
- ${cd(e)}
149
+ ${gd(e)}
150
150
  ${e.narrative?`<div class="panel"><div class="panel-title">agent narrative</div><p>${N(e.narrative)}</p></div>`:""}
151
151
  ${e.adaptations.length>0?`<div class="panel accent-gold"><div class="panel-title gold">adaptations</div><ul>${e.adaptations.map(r=>`<li>${N(r)}</li>`).join("")}</ul></div>`:""}
152
- ${ld(e)}
153
- ${dd(e)}
152
+ ${hd(e)}
153
+ ${md(e)}
154
154
  ${e.proposalFile?`<div class="panel"><div class="panel-title">cache proposal pending${e.verified===!0?' <span class="pill green">verified</span>':e.verified===!1?' <span class="pill red">unverified</span>':""}</div><p class="mono">${N(e.proposalFile)}</p>${e.verified===!1&&e.proofError?`<p class="mono" style="color:var(--red)">proof replay failed: ${N(e.proofError.split(`
155
155
  `)[0])}</p>`:""}<div class="panel-hint">review with <code>saffron accept</code> / <code>saffron reject</code></div></div>`:""}
156
156
  ${e.error&&e.status==="red"?`<div class="panel accent-red"><div class="panel-title red">failure</div><p>${N(e.error)}</p></div>`:""}
157
- ${e.uniques&&e.status!=="green"?`<div class="panel"><div class="panel-title">unique values in this run</div>${Object.entries(e.uniques).map(([r,o])=>`<div class="mono">${N(r)} = ${N(o)}</div>`).join("")}</div>`:""}
158
- ${e.proofUniques&&e.status!=="green"?`<div class="panel"><div class="panel-title">unique values in the proof replay</div>${Object.entries(e.proofUniques).map(([r,o])=>`<div class="mono">${N(r)} = ${N(o)}</div>`).join("")}</div>`:""}
159
- ${ud(e,n)}
157
+ ${e.uniques&&e.status!=="green"?`<div class="panel"><div class="panel-title">unique values in this run</div>${Object.entries(e.uniques).map(([r,s])=>`<div class="mono">${N(r)} = ${N(s)}</div>`).join("")}</div>`:""}
158
+ ${e.proofUniques&&e.status!=="green"?`<div class="panel"><div class="panel-title">unique values in the proof replay</div>${Object.entries(e.proofUniques).map(([r,s])=>`<div class="mono">${N(r)} = ${N(s)}</div>`).join("")}</div>`:""}
159
+ ${wd(e,n)}
160
160
  </div>
161
- </details>`}function ud(e,t){let n=(e.evidence??[]).find(s=>s.kind==="trace"),r=n?`<div class="panel accent-gold"><div class="panel-title gold">execution replay</div><div>Step through this run with the page inspectable at every action:</div><div class="mono" style="margin-top:6px">npx saffron trace ${N(gd(`${e.feature}:${e.scenario}`))}</div><div class="panel-hint">${N(n.file)}</div></div>`:"",o=(e.evidence??[]).flatMap(s=>{if(!t||s.kind!=="failure"&&s.kind!=="before-heal")return[];let a=dt(t,s);if(!a)return[];try{let i=a.toString("base64"),c=s.kind==="failure"?"the page when it failed":"the page when the cached step stopped working, before the agent took over";return[`<figure class="shot"><figcaption>${N(c)}${s.step?`: <span class="mono">${N(s.step)}</span>`:""}</figcaption><a href="data:image/jpeg;base64,${i}" target="_blank" rel="noopener"><img alt="${N(c)}" src="data:image/jpeg;base64,${i}"/></a><div class="panel-hint">${N(s.file)}</div></figure>`]}catch{return[]}});return o.length===0?r:`<div class="panel ${e.status==="red"?"accent-red":"accent-gold"}"><div class="panel-title ${e.status==="red"?"red":"gold"}">screenshot</div>${o.join("")}</div>${r}`}var gd=e=>/[^A-Za-z0-9_./:-]/.test(e)?`'${e.replace(/'/g,"'\\''")}'`:e;function In(e,t){let n=e.totals,r=new Date(e.startedAt),o=new Date(e.finishedAt).getTime()-r.getTime(),s=r.toLocaleString("en-GB",{day:"2-digit",month:"short",year:"numeric",hour:"2-digit",minute:"2-digit"});return`<!doctype html>
161
+ </details>`}function wd(e,t){let n=(e.evidence??[]).find(o=>o.kind==="trace"),r=n?`<div class="panel accent-gold"><div class="panel-title gold">execution replay</div><div>Step through this run with the page inspectable at every action:</div><div class="mono" style="margin-top:6px">npx saffron trace ${N(xd(`${e.feature}:${e.scenario}`))}</div><div class="panel-hint">${N(n.file)}</div></div>`:"",s=(e.evidence??[]).flatMap(o=>{if(!t||o.kind!=="failure"&&o.kind!=="before-heal")return[];let a=dt(t,o);if(!a)return[];try{let i=a.toString("base64"),c=o.kind==="failure"?"the page when it failed":"the page when the cached step stopped working, before the agent took over";return[`<figure class="shot"><figcaption>${N(c)}${o.step?`: <span class="mono">${N(o.step)}</span>`:""}</figcaption><a href="data:image/jpeg;base64,${i}" target="_blank" rel="noopener"><img alt="${N(c)}" src="data:image/jpeg;base64,${i}"/></a><div class="panel-hint">${N(o.file)}</div></figure>`]}catch{return[]}});return s.length===0?r:`<div class="panel ${e.status==="red"?"accent-red":"accent-gold"}"><div class="panel-title ${e.status==="red"?"red":"gold"}">screenshot</div>${s.join("")}</div>${r}`}var xd=e=>/[^A-Za-z0-9_./:-]/.test(e)?`'${e.replace(/'/g,"'\\''")}'`:e;function _n(e,t){let n=e.totals,r=new Date(e.startedAt),s=new Date(e.finishedAt).getTime()-r.getTime(),o=r.toLocaleString("en-GB",{day:"2-digit",month:"short",year:"numeric",hour:"2-digit",minute:"2-digit"});return`<!doctype html>
162
162
  <html lang="en">
163
163
  <head>
164
164
  <meta charset="utf-8"/>
@@ -283,37 +283,37 @@ ${e.completedSteps.map(a=>` \u2713 ${a}`).join(`
283
283
  <div class="wrap">
284
284
  <header class="top">
285
285
  <div class="brand">
286
- ${Br(38)}
286
+ ${Wr(38)}
287
287
  <div>
288
288
  <div class="brand-name">saffron</div>
289
289
  <div class="brand-sub">test run report</div>
290
290
  </div>
291
291
  </div>
292
292
  <div class="run-meta">
293
- <span>${N(s)}</span>
293
+ <span>${N(o)}</span>
294
294
  <span>\xB7</span>
295
- <span>${Bt(o)}</span>
295
+ <span>${Bt(s)}</span>
296
296
  ${e.baseURL?`<span class="chip base">${N(e.baseURL)}</span>`:""}
297
297
  ${e.shards?`<span class="chip">merged from ${e.shards} shards</span>`:""}
298
298
  </div>
299
299
  </header>
300
300
 
301
- ${ad(e,o)}
302
- ${id(e)}
303
- ${od(e)}
301
+ ${fd(e,s)}
302
+ ${ud(e)}
303
+ ${pd(e)}
304
304
 
305
305
  <div class="section-head">
306
306
  <span class="section-title">scenarios \xB7 ${n.scenarios}</span>
307
307
  <span class="section-title">saffron v${N(e.version)}</span>
308
308
  </div>
309
- ${e.scenarios.map(a=>fd(a,!!e.totals.plan,t)).join(`
309
+ ${e.scenarios.map(a=>bd(a,!!e.totals.plan,t)).join(`
310
310
  `)}
311
311
 
312
- <footer>${rd(11)} generated by saffron v${N(e.version)} \xB7 ${N(new Date(e.finishedAt).toLocaleString("en-GB",{dateStyle:"medium",timeStyle:"medium"}))}</footer>
312
+ <footer>${ld(11)} generated by saffron v${N(e.version)} \xB7 ${N(new Date(e.finishedAt).toLocaleString("en-GB",{dateStyle:"medium",timeStyle:"medium"}))}</footer>
313
313
  </div>
314
314
  </body>
315
315
  </html>
316
- `}function Ea(){return`<!DOCTYPE html>
316
+ `}function _a(){return`<!DOCTYPE html>
317
317
  <html lang="en">
318
318
  <head>
319
319
  <meta charset="utf-8">
@@ -400,7 +400,7 @@ ${e.completedSteps.map(a=>` \u2713 ${a}`).join(`
400
400
  </head>
401
401
  <body>
402
402
  <header>
403
- ${Br(26)}
403
+ ${Wr(26)}
404
404
  <div><div class="name" id="title">Saffron replay</div><div class="sub" id="subtitle"></div></div>
405
405
  <span class="status" id="status"></span>
406
406
  <label id="scenarioPicker" hidden>Scenario <select id="scenarioSelect" aria-label="Scenario"></select></label>
@@ -1011,47 +1011,47 @@ ${e.completedSteps.map(a=>` \u2713 ${a}`).join(`
1011
1011
  })();
1012
1012
  </script>
1013
1013
  </body>
1014
- </html>`}var Qe=class extends Error{},Ia="This trace is gone, or a later run replaced it (one that stopped before writing its report). Run the scenario again to replay it.",md=4;function qr(e,t){let n=Ze.join(e,t.file);try{xt.accessSync(n,xt.constants.R_OK)}catch(o){let s=o.code;if(s==="ENOENT"||s==="ENOTDIR")return;throw o}if(!lt(Ze.join(e,".saffron","artifacts"),n))throw new Error(`${t.file} is not a file under .saffron/artifacts`);let r=xt.readFileSync(n);return t.sha256&&Ve(r)!==t.sha256?void 0:r}function yd(){let e=$n.resolve("playwright-core/package.json");return Ze.join(Ze.dirname(e),"lib","vite","traceViewer")}var Pe={".html":"text/html; charset=utf-8",".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".json":"application/json; charset=utf-8",".svg":"image/svg+xml",".png":"image/png",".jpeg":"image/jpeg",".jpg":"image/jpeg",".ttf":"font/ttf",".webmanifest":"application/manifest+json",".zip":"application/zip"},bd=new Set([".png",".jpeg",".jpg"]),zr={"Content-Security-Policy":"sandbox","X-Content-Type-Options":"nosniff"};async function Pa(e){let t=e.entries?[...e.entries]:[{scenario:e.scenario,traces:[{file:e.traceFile,data:e.traceData,label:e.label??"Execution",proof:e.proof,steps:e.scenario.steps,uniques:e.scenario.uniques},...(e.additionalTraces??[]).map(w=>({...w,proof:w.proof??!0}))]}],n=e.start??0;if(!t[n])throw new Error("No scenario to replay");let r=w=>{let v=Pt(w),l=Dr(v.map(u=>({name:u.name,get data(){return u.read()}})));return{zip:w,model:l,resources:new Map(v.filter(u=>u.name.includes("/")).map(u=>[u.name,u.read]))}},o=w=>{try{return w.data?w.data:!w.read&&!xt.existsSync(w.file)?new Qe(Ia):(w.read?w.read():xt.readFileSync(w.file))??new Qe(Ia)}catch(v){return v instanceof Qe?v:new Qe(`This trace cannot be read: ${v instanceof Error?v.message:String(v)}`)}},s=[];{let w=t[n],v=w.traces.map(o),l=w.traces.map((u,x)=>({t:u,data:v[x]})).filter(({data:u},x)=>x===0||Buffer.isBuffer(u));for(let[u,x]of w.traces.entries()){let g=v[u];u>0&&!Buffer.isBuffer(g)&&s.push({file:x.file,reason:g.message})}t[n]={...w,traces:l.map(({t:u,data:x})=>({...u,data:Buffer.isBuffer(x)?x:void 0,read:Buffer.isBuffer(x)?void 0:()=>{throw x}}))}}let a=new Map,i=(w,v)=>{let l=`${w}:${v}`,u=a.get(l);if(u)a.delete(l);else{let x=o(t[w].traces[v]);if(!Buffer.isBuffer(x))throw x;u=r(x);let g=[...a.keys()].filter(S=>{let[T,A]=S.split(":").map(Number),E=t[T].traces[A];return T!==n&&(E.data!==void 0||E.checked===!0)});w!==n&&g.length>=md&&a.delete(g[0])}return a.set(l,u),u},c=new Map,p=(w,v)=>{let l=t[w].scenario.screenshots?.find(g=>g.file===v);if(!l)return;let u=`${w}:${v}`;if(c.has(u))return c.get(u);let x=dt(e.projectRoot,l);return(w===n||!l.sha256)&&c.set(u,x),x};try{i(n,0)}catch(w){if(t.length===1)throw w}for(let w of t[n].scenario.screenshots??[])p(n,w.file);let f=yd(),d=Ea(),m=Aa(32).toString("hex"),h=Aa(8).toString("hex"),y=t.map(w=>({feature:w.scenario.feature,scenario:w.scenario.scenario,status:w.scenario.status})),b=hd.createServer((w,v)=>{let l=(u,x,g,S={})=>{v.writeHead(u,{"Content-Type":g,"Cache-Control":"no-store",...S}),v.end(x)};try{let u=w.headers.host??"";if(!/^(?:127\.0\.0\.1|localhost)(?::\d+)?$/i.test(u))return l(403,"Invalid host","text/plain");let x=new URL(w.url??"/",`http://${u}`),g=Number(x.searchParams.get("scenario")??n),S=Number.isInteger(g)?t[g]:void 0,T=Number(x.searchParams.get("session")??"0"),A=S&&Number.isInteger(T)?S.traces[T]:void 0,E=()=>l(404,S?"No such trace session":"No such scenario","text/plain");if(x.pathname.startsWith("/api/review/")){if(w.method!=="POST")return l(405,"Use POST","text/plain");if(w.headers.origin!==`http://${u}`||w.headers["x-saffron-review"]!==m)return l(403,"Review must come from this replay page","text/plain");if(!S||!A)return E();let C=x.pathname.slice(12);if(C!=="accept"&&C!=="reject")return l(404,"Unknown decision","text/plain");try{return $a(e.projectRoot,S.scenario,C,w.headers["x-saffron-unverified"]==="yes"),l(200,JSON.stringify({decision:C}),Pe[".json"])}catch(I){return l(409,JSON.stringify({error:I instanceof Error?I.message:String(I)}),Pe[".json"])}}if(w.method!=="GET"&&w.method!=="HEAD")return l(405,"Use GET","text/plain");if(x.pathname==="/"||x.pathname==="/index.html")return l(200,d,Pe[".html"]);if(x.pathname==="/api/replay"){let C=S?g:n,I=A?T:0,P=t[C],j=P.traces[I],L={scenarios:y,scenarioIndex:C,sessions:P.traces.map(O=>O.label),session:I,proof:j.proof===!0,instance:h},_;try{_=i(C,I).model}catch(O){let R=O instanceof Qe?O.message:`This trace cannot be read: ${O instanceof Error?O.message:String(O)}`;return l(200,JSON.stringify({...L,scenario:P.scenario,unavailable:R}),Pe[".json"])}let $=ka(e.projectRoot,P.scenario);return l(200,JSON.stringify({...L,scenario:{...P.scenario,steps:j.steps,uniques:j.uniques},trace:_,review:$,reviewToken:m}),Pe[".json"])}if(x.pathname.startsWith("/artifact/")){let C=x.searchParams.get("v");if(C!==null&&C!==h)return l(410,"This replay was restarted. Reload the page.","text/plain");if(!S||!A)return E();if(x.pathname==="/artifact/trace.zip")return l(200,i(g,T).zip,Pe[".zip"],zr);if(x.pathname.startsWith("/artifact/resource/")){let I=decodeURIComponent(x.pathname.slice(19)),P=i(g,T).resources,j=(P.get(I)??(I.includes("/")?void 0:P.get(`resources/${I}`)))?.();if(!j)return l(404,"no such resource","text/plain");let L=Ze.extname(x.pathname).toLowerCase();return l(200,j,bd.has(L)?Pe[L]:"application/octet-stream",zr)}if(x.pathname.startsWith("/artifact/screenshot/")){let I=p(g,decodeURIComponent(x.pathname.slice(21)));return I?l(200,I,Pe[".jpg"],zr):l(404,"no such file","text/plain")}}if(x.pathname==="/trace/ping")return l(200,"ok","text/plain");if(x.pathname.startsWith("/trace/")){let C=x.pathname.slice(7)||"index.html",I=Ze.join(f,C);if(!lt(f,I))return l(404,"not found","text/plain");let P=Ze.extname(I).toLowerCase(),j=xt.readFileSync(I);return v.writeHead(200,{"Content-Type":Pe[P]??"application/octet-stream","Service-Worker-Allowed":"/trace/"}),v.end(j)}l(404,"not found","text/plain")}catch{v.headersSent?v.destroy():l(400,"Invalid request","text/plain")}});return await new Promise((w,v)=>{b.once("error",v),b.listen(e.port??0,"127.0.0.1",()=>w())}),{url:`http://127.0.0.1:${b.address().port}/`,skipped:s,close:()=>new Promise(w=>b.close(()=>w()))}}import Ca from"node:fs";import Pn from"node:path";function Cn(e){return JSON.stringify([e.role??null,e.name??null,e.nameRegex??null,e.label??null,e.text??null,e.testId??null,e.placeholder??null,e.frame??null])}function _a(e,t){let n=new Map,r=new Set,o=new Map,s=Math.min(e.steps.length,t.steps.length);for(let i=0;i<s;i++){if(e.steps[i].kind==="assertion"||t.steps[i].kind==="assertion"||e.steps[i].gherkin!==t.steps[i].gherkin)continue;let c=e.steps[i].actions,p=t.steps[i].actions;if(c.length===p.length)for(let f=0;f<c.length;f++){let d=c[f],m=p[f];if(d.action!==m.action||pe.has(d.action)||!d.target||!m.target)continue;let h=Cn(d.target),y=JSON.stringify(m.target);o.has(h)&&o.get(h)!==y&&r.add(h),o.set(h,y),h!==Cn(m.target)&&n.set(h,{from:d.target,to:m.target})}}let a=[...n.entries()];return{changes:a.filter(([i])=>!r.has(i)).map(([,i])=>i),ambiguous:a.filter(([i])=>r.has(i)).map(([,i])=>i.from)}}function wd(e){let t=Pn.join(e,".saffron","cache");if(!Ca.existsSync(t))return[];let n=[];for(let r of Ca.readdirSync(t,{recursive:!0})){let o=String(r);o.endsWith(".json")&&n.push(Pn.join(t,o))}return n}function ja(e,t,n,r){if(t.length===0)return[];let o=new Map,s=new Set;for(let i of t){let c=Cn(i.from),p=o.get(c);p&&JSON.stringify(p.to)!==JSON.stringify(i.to)&&s.add(c),o.set(c,i)}for(let i of s)o.delete(i);let a=[];for(let i of wd(e)){if(Pn.resolve(i)===Pn.resolve(n))continue;let c=st(i);if(!c)continue;let p=[],f=!1;for(let d of c.steps){if(d.kind==="assertion")continue;let m=0;for(let h of d.actions){if(!h.target||pe.has(h.action))continue;let y=o.get(Cn(h.target));y&&(m++,r||(h.target=structuredClone(y.to),f=!0))}m>0&&p.push({gherkin:d.gherkin,count:m})}p.length>0&&(f&&ot(i,c),a.push({file:i,scenario:c.scenario,feature:c.feature,steps:p}))}return a}function _n(e){return e.role?`${e.role} "${e.name??e.nameRegex??""}"`:e.label?`label "${e.label}"`:e.text?`text "${e.text}"`:e.testId?`testId "${e.testId}"`:(e.fallbackSelectors??[]).join(",")||"(empty target)"}import Oa from"node:fs";import jn from"node:path";function Fa(e,t,n,r,o){let s=se(jn.join(e,t),e,o).find(c=>c.name===n);if(!s)return`scenario "${n}" is no longer in ${t}`;let a=s.steps.map(c=>c.text);if(a.length!==r.length)return`the scenario has ${a.length} steps now and had ${r.length} when this was recorded`;let i=a.findIndex((c,p)=>c!==r[p]);return i<0?void 0:`step ${i+1} now reads "${a[i]}" and was recorded as "${r[i]}"`}function Na(e,t,n,r,o,s){let a={applied:[],skipped:[],appliedIndices:[]},i=se(jn.join(e,t),e,o).find(d=>d.name===n);if(!i){for(let d of r)a.skipped.push({index:d.index,reason:`scenario "${n}" not found in ${t}`});return a}let c=new Map;for(let d of r){let m=i.steps[d.index]?.source;if(!m||m.fromBackground)continue;let h=`${m.file}:${m.line}`;c.set(h,(c.get(h)??new Set).add(d.text))}let p=new Map,f=new Set;for(let d of r){let m=i.steps[d.index];if(!m?.source){a.skipped.push({index:d.index,reason:`no step at index ${d.index}`});continue}if(s&&s[d.index]!==m.text){a.skipped.push({index:d.index,reason:`step ${d.index+1} now reads "${m.text}", not the "${s[d.index]??"(nothing)"}" this edit was written for`});continue}let{file:h,line:y,fromStepSet:b,fromBackground:k}=m.source;if(k){a.skipped.push({index:d.index,reason:`"${m.text}" is a Background step shared across scenarios; edit it manually`});continue}let w=`${h}:${y}`,v=c.get(w);if(v.size>1){a.skipped.push({index:d.index,reason:`${w}${b?` (StepSet "${b}")`:""} is reached by several steps and the edits disagree: ${[...v].map(S=>`"${S}"`).join(" vs ")}. Decide the wording in the definition by hand`});continue}if(f.has(w)){a.appliedIndices.push(d.index);continue}let l=p.get(h);l||(l=Oa.readFileSync(jn.join(e,h),"utf8").split(`
1015
- `),p.set(h,l));let u=l[y-1],x=u?.match(/^(\s*)(\S+)\s+(.*)$/);if(!x||x[3]!==m.text){a.skipped.push({index:d.index,reason:`${h}:${y} no longer matches the recorded step text`});continue}let g=`${x[1]}${x[2]} ${d.text}`;l[y-1]=g,f.add(w),a.appliedIndices.push(d.index),a.applied.push({file:h,line:y,from:u.trim(),to:g.trim(),fromStepSet:b})}for(let[d,m]of p)a.applied.some(h=>h.file===d)&&Oa.writeFileSync(jn.join(e,d),m.join(`
1016
- `));return a}import Hr from"node:fs";import Vr from"node:path";function Wr(e){let t={scenarios:e.length,green:0,yellow:0,red:0,aiCalls:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreationTokens:0,costUsd:0},n=new Set;for(let r of e){for(let o of r.usage.models??[])n.add(o);t[r.status]+=1,t.aiCalls+=r.usage.aiCalls,t.inputTokens+=r.usage.inputTokens,t.outputTokens+=r.usage.outputTokens,t.cacheReadTokens+=r.usage.cacheReadTokens,t.cacheCreationTokens+=r.usage.cacheCreationTokens??0,t.costUsd+=r.usage.costUsd??0}return n.size>0&&(t.models=[...n].sort()),t}function Jr(e,t,n){return{tool:"saffron",version:Fe(),startedAt:t.toISOString(),finishedAt:new Date().toISOString(),baseURL:n,totals:Wr(e),scenarios:e.map(r=>({feature:r.scenario.featurePath,scenario:r.scenario.displayName,tags:[...r.scenario.tags],status:r.status,source:r.source,proposalReplay:r.proposalReplay,durationMs:r.durationMs,usage:r.usage,steps:r.stepResults,narrative:r.narrative,adaptations:r.adaptations,suggestedFeatureEdit:r.suggestedFeatureEdit,featureEdits:r.featureEdits,verified:r.verified,proofError:r.proofError,proposalFile:r.proposalFile,proposalRevision:r.proposalRevision,proposalDiff:r.proposalDiff,error:r.error,evidence:r.evidence,uniques:r.uniques,proofUniques:r.proofUniques}))}}function Gr(e,t){let n=Vr.join(e,".saffron","reports");Hr.mkdirSync(n,{recursive:!0});let r=Vr.join(n,"latest.json"),o=Vr.join(n,"latest.html");return Hr.writeFileSync(r,`${JSON.stringify(t,null,2)}
1017
- `),Hr.writeFileSync(o,In(t,e)),{json:r,html:o}}import xd from"node:crypto";import zt from"node:fs";import On from"node:path";var Ma=["scenario","file"];function Da(e){let t=/^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec(e),n=t?Number(t[1]):NaN,r=t?Number(t[2]):NaN;if(!t||r<1||n<1||n>r)throw new Error(`invalid --shard "${e}": use <index>/<total>, e.g. --shard 2/4 (index from 1 to total)`);return{index:n,total:r}}function La(e){if(!Ma.includes(e))throw new Error(`invalid --shard-by "${e}": use ${Ma.map(t=>`"${t}"`).join(" or ")}`);return e}function vd(e){return`shard-${e.index}-of-${e.total}`}var Sd=e=>`${e.featurePath}${e.displayName}`;function Ua(e,t){let n=t.by??"scenario",r=e.map(Sd),o=new Set;if(n==="file"){let i=new Map;e.forEach((f,d)=>i.set(f.featurePath,[...i.get(f.featurePath)??[],d]));let c=new Array(t.total).fill(0),p=[...i].sort((f,d)=>d[1].length-f[1].length||(f[0]<d[0]?-1:f[0]>d[0]?1:0));for(let[,f]of p){let d=c.indexOf(Math.min(...c));if(c[d]+=f.length,d===t.index-1)for(let m of f)o.add(m)}}else e.map((c,p)=>p).sort((c,p)=>r[c]<r[p]?-1:r[c]>r[p]?1:0).forEach((c,p)=>{p%t.total===t.index-1&&o.add(c)});let s=e.map((i,c)=>c).filter(i=>o.has(i)),a=xd.createHash("sha256").update([...r].sort().join(`
1018
- `)).digest("hex").slice(0,12);return{scenarios:s.map(i=>e[i]),info:{index:t.index,total:t.total,by:n,suite:a,suiteSize:e.length,positions:s}}}function Ba(e=process.env){return e.SAFFRON_RUN_ID||e.GITHUB_RUN_ID&&`github:${e.GITHUB_RUN_ID}`||e.CI_PIPELINE_ID&&`gitlab:${e.CI_PIPELINE_ID}`||e.BUILD_BUILDID&&`azure:${e.BUILD_BUILDID}`||e.CIRCLE_WORKFLOW_ID&&`circleci:${e.CIRCLE_WORKFLOW_ID}`||void 0}function Kr(e,t){let n=On.join(e,".saffron","reports");zt.mkdirSync(n,{recursive:!0});let r=On.join(n,`${vd(t.shard)}.json`);return zt.writeFileSync(r,`${JSON.stringify(t,null,2)}
1019
- `),r}var kd=e=>{let t=e;return!!t&&t.tool==="saffron"&&!!t.shard&&typeof t.shard.index=="number"&&typeof t.shard.total=="number"&&Array.isArray(t.shard.positions)&&Array.isArray(t.scenarios)&&!!t.history&&Array.isArray(t.history.scenarios)},$d=/^shard-\d+-of-\d+\.json$/;function za(e){let t=[],n=(r,o)=>{let s=zt.statSync(r,{throwIfNoEntry:!1});if(!s)throw new Error(`not found: ${r}`);if(s.isDirectory())for(let a of zt.readdirSync(r).sort())n(On.join(r,a),!1);else(o||$d.test(On.basename(r)))&&t.push(r)};for(let r of e)n(r,!0);return t.map(r=>{let o;try{o=JSON.parse(zt.readFileSync(r,"utf8"))}catch(s){throw new Error(`cannot read shard report ${r}: ${s instanceof Error?s.message:String(s)}`)}if(!kd(o))throw new Error(`${r} is not a shard report (written by saffron run --shard)`);return{file:r,report:o}})}var Fn=class extends Error{};function qa(e){if(e.length===0)throw new Fn("no shard reports found. Shards write .saffron/reports/shard-<i>-of-<n>.json");let t=e[0].report.shard,n=[],r=l=>l.map(u=>u.file).join(", "),o=new Set(e.map(l=>l.report.shard.total));o.size>1&&n.push(`shards disagree on the shard count (${[...o].sort().join(", ")}): ${r(e)}`),new Set(e.map(l=>`${l.report.shard.suite}/${l.report.shard.suiteSize}`)).size>1&&n.push("shards ran different suites (different scenarios or filters). Every shard must run the same paths and --filter");let a=new Set(e.map(l=>l.report.shard.by??"scenario"));a.size>1&&n.push(`shards split the suite in different ways (--shard-by ${[...a].sort().join(" and ")}): every shard must use the same`);let i=new Set(e.map(l=>l.report.shard.ciRun??""));i.size>1&&n.push(`shards come from different CI runs (${[...i].map(l=>l||"none recorded").join(", ")})`);let c=new Map;for(let l of e)c.set(l.report.shard.index,[...c.get(l.report.shard.index)??[],l]);for(let[l,u]of[...c].sort((x,g)=>x[0]-g[0]))u.length>1&&n.push(`shard ${l}/${t.total} appears ${u.length} times: ${r(u)}`);if(o.size===1){let l=Array.from({length:t.total},(u,x)=>x+1).filter(u=>!c.has(u));l.length>0&&n.push(`missing ${l.length===1?"shard":"shards"} ${l.map(u=>`${u}/${t.total}`).join(", ")}. A merge without ${l.length===1?"it":"them"} would under-count the run`)}if(n.length===0){let l=new Set,u=0;for(let g of e){let{positions:S}=g.report.shard;(S.length!==g.report.scenarios.length||S.length!==g.report.history.scenarios.length)&&n.push(`${g.file} is inconsistent: ${S.length} positions for ${g.report.scenarios.length} scenarios`);for(let T of S)l.has(T)&&n.push(`two shards both ran scenario ${T+1} of the suite`),l.add(T);u+=S.length}let x=[...new Set(e.map(g=>g.report.baseURL??"(no base URL)"))];x.length>1&&n.push(`the shards ran against different base URLs: ${x.join(", ")}. Merge the shards of one environment at a time`),n.length===0&&u!==t.suiteSize&&n.push(`the shards hold ${u} scenarios but the suite has ${t.suiteSize}`)}if(n.length>0)throw new Fn(n.join(`
1020
- `));let p=e.sort((l,u)=>l.report.shard.index-u.report.shard.index).flatMap(({report:l})=>l.shard.positions.map((u,x)=>({position:u,scenario:l.scenarios[x],history:l.history.scenarios[x]}))).sort((l,u)=>l.position-u.position),f=e.map(l=>l.report),d=f.map(l=>l.startedAt).sort()[0],m=f.map(l=>l.finishedAt).sort().at(-1),h=p.map(l=>l.scenario),y=f.flatMap(l=>l.hooks??[]),b={tool:"saffron",version:f[0].version,startedAt:d,finishedAt:m,baseURL:f.find(l=>l.baseURL)?.baseURL,totals:Wr(h),scenarios:h,shards:t.total};y.length>0&&(b.hooks=y);let k=p.map(l=>l.history),v={runId:t.ciRun?`${t.ciRun}:${t.suite}:${b.baseURL??""}`:d,startedAt:d,finishedAt:m,baseURL:b.baseURL,green:k.filter(l=>l.status==="green").length,yellow:k.filter(l=>l.status==="yellow").length,red:k.filter(l=>l.status==="red").length,aiCalls:k.reduce((l,u)=>l+u.aiCalls,0),costUsd:k.reduce((l,u)=>l+u.costUsd,0),divergentSteps:Math.max(...f.map(l=>l.history.divergentSteps)),scenarios:k};return{report:b,history:v}}function Ha(e){let t=e===void 0?[]:Array.isArray(e)?e:[e],n=new Set;for(let r of t)for(let o of r.split(/[\s,|]+/)){let s=o.trim();s&&n.add(s.startsWith("@")?s:`@${s}`)}return[...n]}function Va(e,t){return t.length===0?e:e.filter(n=>n.tags.some(r=>t.includes(r)))}function Wa(e,t){return[...t??[],e]}import{spawn as Td}from"node:child_process";function Yr(e,t){if(e==null)return[];let n=Array.isArray(e)?e:[e];for(let r of n)if(typeof r!="string"||r.trim()==="")throw new Error(`"${t}" in saffron.config.json must be a command, or a list of commands, e.g. "${t}": "npm run seed"`);return n}function Rd(e,t,n){let r=Date.now();return new Promise(o=>{let s=process.platform!=="win32",a=Td(t,{cwd:n.cwd,shell:!0,detached:s,env:{...process.env,...n.env},stdio:["ignore",process.stderr,process.stderr]}),i=!1,c=!1,p=()=>{try{s&&a.pid?process.kill(-a.pid,"SIGKILL"):a.kill("SIGKILL")}catch{}},f=setTimeout(()=>{i=!0,p()},n.timeoutMs),d=()=>{c=!0,p()};n.signal?.addEventListener("abort",d,{once:!0});let m=h=>{clearTimeout(f),n.signal?.removeEventListener("abort",d),o({phase:e,command:t,exitCode:h,durationMs:Date.now()-r,...i?{timedOut:i}:{},...c?{cancelled:c}:{}})};a.on("error",()=>m(null)),a.on("exit",h=>m(i||c?null:h))})}var Nn=e=>e.exitCode!==0;async function Qr(e,t,n){let r=[];for(let o of t){if(n.signal?.aborted)break;console.error(`saffron ${e}: ${o}`);let s=await Rd(e,o,n);if(r.push(s),s.cancelled||Nn(s)&&(console.error(`saffron ${e} failed: ${Zr(s,n.timeoutMs)}`),e==="setup"))break}return r}function Zr(e,t){return e.cancelled?`"${e.command}" was interrupted`:e.timedOut?`"${e.command}" was stopped after ${Math.round(t/1e3)}s (hookTimeoutMs)`:e.exitCode===null?`"${e.command}" could not be started`:`"${e.command}" exited with code ${e.exitCode}`}import U from"node:fs";import H from"node:path";var qt={claude:".claude/skills",agents:".agents/skills",copilot:".github/skills",cursor:".cursor/skills"},Ed=["claude","agents"],Ka="<!-- saffron:start -->",es="<!-- saffron:end -->";function Ad(){return[Ka,"## Saffron end-to-end tests","","UI tests are plain-Gherkin `.feature` / `.saffron` files under `features/`,","run by Saffron (`npx saffron run`). No step definitions exist: an AI agent","records each scenario once; later runs replay at zero tokens.","","- Load the `saffron` skill before writing or editing scenarios.","- Exact step text is the cache identity: run `npx saffron steps` and reuse"," recorded wordings instead of inventing synonyms.","- `Then` steps are never healed: write them as the verdict, last.","- `npx saffron status` shows what is cached, what awaits review, the tags"," and vocabulary health; editing a cached scenario's text re-records it.","- Never edit `.saffron/cache/` or `.saffron/proposals/` by hand; change the"," scenario text and re-run. Review proposals with `npx saffron accept`"," (`--all`, or chosen files) and `npx saffron reject`.","- Secrets go in `{env:VAR}` tokens, never in files.","- Shared test data (accounts, names, enum values) goes in `data/*.json`,",' referenced as `{data:file.path.key}`; check a whole list with "every',' {data:file.list}"; read Examples rows from a file with'," `Examples: {data:file}` (.saffron only). Use `{unique:name}` for a value"," that must be new on every run. Never put secrets in `data/`.",es,""].join(`
1021
- `)}function Ja(e,t){if(!U.existsSync(e))return U.writeFileSync(e,t),"created";let n=U.readFileSync(e,"utf8"),r=n.indexOf(Ka),o=n.indexOf(es);if(r!==-1&&o!==-1&&o>r){let a=n.slice(0,r)+t.trimEnd()+n.slice(o+es.length);return a===n?"unchanged":(U.writeFileSync(e,a),"updated")}let s=n.endsWith(`
1014
+ </html>`}var Qe=class extends Error{},Oa="This trace is gone, or a later run replaced it (one that stopped before writing its report). Run the scenario again to replay it.",vd=4;function Jr(e,t){let n=Ze.join(e,t.file);try{xt.accessSync(n,xt.constants.R_OK)}catch(s){let o=s.code;if(o==="ENOENT"||o==="ENOTDIR")return;throw s}if(!lt(Ze.join(e,".saffron","artifacts"),n))throw new Error(`${t.file} is not a file under .saffron/artifacts`);let r=xt.readFileSync(n);return t.sha256&&Ve(r)!==t.sha256?void 0:r}function kd(){let e=En.resolve("playwright-core/package.json");return Ze.join(Ze.dirname(e),"lib","vite","traceViewer")}var Pe={".html":"text/html; charset=utf-8",".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".json":"application/json; charset=utf-8",".svg":"image/svg+xml",".png":"image/png",".jpeg":"image/jpeg",".jpg":"image/jpeg",".ttf":"font/ttf",".webmanifest":"application/manifest+json",".zip":"application/zip"},$d=new Set([".png",".jpeg",".jpg"]),Vr={"Content-Security-Policy":"sandbox","X-Content-Type-Options":"nosniff"};async function Fa(e){let t=e.entries?[...e.entries]:[{scenario:e.scenario,traces:[{file:e.traceFile,data:e.traceData,label:e.label??"Execution",proof:e.proof,steps:e.scenario.steps,uniques:e.scenario.uniques},...(e.additionalTraces??[]).map(w=>({...w,proof:w.proof??!0}))]}],n=e.start??0;if(!t[n])throw new Error("No scenario to replay");let r=w=>{let v=Ct(w),u=zr(v.map(p=>({name:p.name,get data(){return p.read()}})));return{zip:w,model:u,resources:new Map(v.filter(p=>p.name.includes("/")).map(p=>[p.name,p.read]))}},s=w=>{try{return w.data?w.data:!w.read&&!xt.existsSync(w.file)?new Qe(Oa):(w.read?w.read():xt.readFileSync(w.file))??new Qe(Oa)}catch(v){return v instanceof Qe?v:new Qe(`This trace cannot be read: ${v instanceof Error?v.message:String(v)}`)}},o=[];{let w=t[n],v=w.traces.map(s),u=w.traces.map((p,b)=>({t:p,data:v[b]})).filter(({data:p},b)=>b===0||Buffer.isBuffer(p));for(let[p,b]of w.traces.entries()){let g=v[p];p>0&&!Buffer.isBuffer(g)&&o.push({file:b.file,reason:g.message})}t[n]={...w,traces:u.map(({t:p,data:b})=>({...p,data:Buffer.isBuffer(b)?b:void 0,read:Buffer.isBuffer(b)?void 0:()=>{throw b}}))}}let a=new Map,i=(w,v)=>{let u=`${w}:${v}`,p=a.get(u);if(p)a.delete(u);else{let b=s(t[w].traces[v]);if(!Buffer.isBuffer(b))throw b;p=r(b);let g=[...a.keys()].filter(S=>{let[T,A]=S.split(":").map(Number),R=t[T].traces[A];return T!==n&&(R.data!==void 0||R.checked===!0)});w!==n&&g.length>=vd&&a.delete(g[0])}return a.set(u,p),p},c=new Map,l=(w,v)=>{let u=t[w].scenario.screenshots?.find(g=>g.file===v);if(!u)return;let p=`${w}:${v}`;if(c.has(p))return c.get(p);let b=dt(e.projectRoot,u);return(w===n||!u.sha256)&&c.set(p,b),b};try{i(n,0)}catch(w){if(t.length===1)throw w}for(let w of t[n].scenario.screenshots??[])l(n,w.file);let f=kd(),d=_a(),m=ja(32).toString("hex"),h=ja(8).toString("hex"),y=t.map(w=>({feature:w.scenario.feature,scenario:w.scenario.scenario,status:w.scenario.status})),x=Sd.createServer((w,v)=>{let u=(p,b,g,S={})=>{v.writeHead(p,{"Content-Type":g,"Cache-Control":"no-store",...S}),v.end(b)};try{let p=w.headers.host??"";if(!/^(?:127\.0\.0\.1|localhost)(?::\d+)?$/i.test(p))return u(403,"Invalid host","text/plain");let b=new URL(w.url??"/",`http://${p}`),g=Number(b.searchParams.get("scenario")??n),S=Number.isInteger(g)?t[g]:void 0,T=Number(b.searchParams.get("session")??"0"),A=S&&Number.isInteger(T)?S.traces[T]:void 0,R=()=>u(404,S?"No such trace session":"No such scenario","text/plain");if(b.pathname.startsWith("/api/review/")){if(w.method!=="POST")return u(405,"Use POST","text/plain");if(w.headers.origin!==`http://${p}`||w.headers["x-saffron-review"]!==m)return u(403,"Review must come from this replay page","text/plain");if(!S||!A)return R();let _=b.pathname.slice(12);if(_!=="accept"&&_!=="reject")return u(404,"Unknown decision","text/plain");try{return Ia(e.projectRoot,S.scenario,_,w.headers["x-saffron-unverified"]==="yes"),u(200,JSON.stringify({decision:_}),Pe[".json"])}catch(I){return u(409,JSON.stringify({error:I instanceof Error?I.message:String(I)}),Pe[".json"])}}if(w.method!=="GET"&&w.method!=="HEAD")return u(405,"Use GET","text/plain");if(b.pathname==="/"||b.pathname==="/index.html")return u(200,d,Pe[".html"]);if(b.pathname==="/api/replay"){let _=S?g:n,I=A?T:0,C=t[_],M=C.traces[I],O={scenarios:y,scenarioIndex:_,sessions:C.traces.map(j=>j.label),session:I,proof:M.proof===!0,instance:h},P;try{P=i(_,I).model}catch(j){let E=j instanceof Qe?j.message:`This trace cannot be read: ${j instanceof Error?j.message:String(j)}`;return u(200,JSON.stringify({...O,scenario:C.scenario,unavailable:E}),Pe[".json"])}let $=Aa(e.projectRoot,C.scenario);return u(200,JSON.stringify({...O,scenario:{...C.scenario,steps:M.steps,uniques:M.uniques},trace:P,review:$,reviewToken:m}),Pe[".json"])}if(b.pathname.startsWith("/artifact/")){let _=b.searchParams.get("v");if(_!==null&&_!==h)return u(410,"This replay was restarted. Reload the page.","text/plain");if(!S||!A)return R();if(b.pathname==="/artifact/trace.zip")return u(200,i(g,T).zip,Pe[".zip"],Vr);if(b.pathname.startsWith("/artifact/resource/")){let I=decodeURIComponent(b.pathname.slice(19)),C=i(g,T).resources,M=(C.get(I)??(I.includes("/")?void 0:C.get(`resources/${I}`)))?.();if(!M)return u(404,"no such resource","text/plain");let O=Ze.extname(b.pathname).toLowerCase();return u(200,M,$d.has(O)?Pe[O]:"application/octet-stream",Vr)}if(b.pathname.startsWith("/artifact/screenshot/")){let I=l(g,decodeURIComponent(b.pathname.slice(21)));return I?u(200,I,Pe[".jpg"],Vr):u(404,"no such file","text/plain")}}if(b.pathname==="/trace/ping")return u(200,"ok","text/plain");if(b.pathname.startsWith("/trace/")){let _=b.pathname.slice(7)||"index.html",I=Ze.join(f,_);if(!lt(f,I))return u(404,"not found","text/plain");let C=Ze.extname(I).toLowerCase(),M=xt.readFileSync(I);return v.writeHead(200,{"Content-Type":Pe[C]??"application/octet-stream","Service-Worker-Allowed":"/trace/"}),v.end(M)}u(404,"not found","text/plain")}catch{v.headersSent?v.destroy():u(400,"Invalid request","text/plain")}});return await new Promise((w,v)=>{x.once("error",v),x.listen(e.port??0,"127.0.0.1",()=>w())}),{url:`http://127.0.0.1:${x.address().port}/`,skipped:o,close:()=>new Promise(w=>x.close(()=>w()))}}import Na from"node:fs";import jn from"node:path";function On(e){return JSON.stringify([e.role??null,e.name??null,e.nameRegex??null,e.label??null,e.text??null,e.testId??null,e.placeholder??null,e.frame??null])}function Ma(e,t){let n=new Map,r=new Set,s=new Map,o=Math.min(e.steps.length,t.steps.length);for(let i=0;i<o;i++){if(e.steps[i].kind==="assertion"||t.steps[i].kind==="assertion"||e.steps[i].gherkin!==t.steps[i].gherkin)continue;let c=e.steps[i].actions,l=t.steps[i].actions;if(c.length===l.length)for(let f=0;f<c.length;f++){let d=c[f],m=l[f];if(d.action!==m.action||pe.has(d.action)||!d.target||!m.target)continue;let h=On(d.target),y=JSON.stringify(m.target);s.has(h)&&s.get(h)!==y&&r.add(h),s.set(h,y),h!==On(m.target)&&n.set(h,{from:d.target,to:m.target})}}let a=[...n.entries()];return{changes:a.filter(([i])=>!r.has(i)).map(([,i])=>i),ambiguous:a.filter(([i])=>r.has(i)).map(([,i])=>i.from)}}function Td(e){let t=jn.join(e,".saffron","cache");if(!Na.existsSync(t))return[];let n=[];for(let r of Na.readdirSync(t,{recursive:!0})){let s=String(r);s.endsWith(".json")&&n.push(jn.join(t,s))}return n}function Da(e,t,n,r){if(t.length===0)return[];let s=new Map,o=new Set;for(let i of t){let c=On(i.from),l=s.get(c);l&&JSON.stringify(l.to)!==JSON.stringify(i.to)&&o.add(c),s.set(c,i)}for(let i of o)s.delete(i);let a=[];for(let i of Td(e)){if(jn.resolve(i)===jn.resolve(n))continue;let c=st(i);if(!c)continue;let l=[],f=!1;for(let d of c.steps){if(d.kind==="assertion")continue;let m=0;for(let h of d.actions){if(!h.target||pe.has(h.action))continue;let y=s.get(On(h.target));y&&(m++,r||(h.target=structuredClone(y.to),f=!0))}m>0&&l.push({gherkin:d.gherkin,count:m})}l.length>0&&(f&&ot(i,c),a.push({file:i,scenario:c.scenario,feature:c.feature,steps:l}))}return a}function Fn(e){return e.role?`${e.role} "${e.name??e.nameRegex??""}"`:e.label?`label "${e.label}"`:e.text?`text "${e.text}"`:e.testId?`testId "${e.testId}"`:(e.fallbackSelectors??[]).join(",")||"(empty target)"}import La from"node:fs";import Nn from"node:path";function Ua(e,t,n,r,s){let o=se(Nn.join(e,t),e,s).find(c=>c.name===n);if(!o)return`scenario "${n}" is no longer in ${t}`;let a=o.steps.map(c=>c.text);if(a.length!==r.length)return`the scenario has ${a.length} steps now and had ${r.length} when this was recorded`;let i=a.findIndex((c,l)=>c!==r[l]);return i<0?void 0:`step ${i+1} now reads "${a[i]}" and was recorded as "${r[i]}"`}function Ba(e,t,n,r,s,o){let a={applied:[],skipped:[],appliedIndices:[]},i=se(Nn.join(e,t),e,s).find(d=>d.name===n);if(!i){for(let d of r)a.skipped.push({index:d.index,reason:`scenario "${n}" not found in ${t}`});return a}let c=new Map;for(let d of r){let m=i.steps[d.index]?.source;if(!m||m.fromBackground)continue;let h=`${m.file}:${m.line}`;c.set(h,(c.get(h)??new Set).add(d.text))}let l=new Map,f=new Set;for(let d of r){let m=i.steps[d.index];if(!m?.source){a.skipped.push({index:d.index,reason:`no step at index ${d.index}`});continue}if(o&&o[d.index]!==m.text){a.skipped.push({index:d.index,reason:`step ${d.index+1} now reads "${m.text}", not the "${o[d.index]??"(nothing)"}" this edit was written for`});continue}let{file:h,line:y,fromStepSet:x,fromBackground:k}=m.source;if(k){a.skipped.push({index:d.index,reason:`"${m.text}" is a Background step shared across scenarios; edit it manually`});continue}let w=`${h}:${y}`,v=c.get(w);if(v.size>1){a.skipped.push({index:d.index,reason:`${w}${x?` (StepSet "${x}")`:""} is reached by several steps and the edits disagree: ${[...v].map(S=>`"${S}"`).join(" vs ")}. Decide the wording in the definition by hand`});continue}if(f.has(w)){a.appliedIndices.push(d.index);continue}let u=l.get(h);u||(u=La.readFileSync(Nn.join(e,h),"utf8").split(`
1015
+ `),l.set(h,u));let p=u[y-1],b=p?.match(/^(\s*)(\S+)\s+(.*)$/);if(!b||b[3]!==m.text){a.skipped.push({index:d.index,reason:`${h}:${y} no longer matches the recorded step text`});continue}let g=`${b[1]}${b[2]} ${d.text}`;u[y-1]=g,f.add(w),a.appliedIndices.push(d.index),a.applied.push({file:h,line:y,from:p.trim(),to:g.trim(),fromStepSet:x})}for(let[d,m]of l)a.applied.some(h=>h.file===d)&&La.writeFileSync(Nn.join(e,d),m.join(`
1016
+ `));return a}import Gr from"node:fs";import Kr from"node:path";function Yr(e){let t={scenarios:e.length,green:0,yellow:0,red:0,aiCalls:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreationTokens:0,costUsd:0},n=new Set;for(let r of e){for(let s of r.usage.models??[])n.add(s);t[r.status]+=1,t.aiCalls+=r.usage.aiCalls,t.inputTokens+=r.usage.inputTokens,t.outputTokens+=r.usage.outputTokens,t.cacheReadTokens+=r.usage.cacheReadTokens,t.cacheCreationTokens+=r.usage.cacheCreationTokens??0,t.costUsd+=r.usage.costUsd??0}return n.size>0&&(t.models=[...n].sort()),t}function Qr(e,t,n){return{tool:"saffron",version:Ne(),startedAt:t.toISOString(),finishedAt:new Date().toISOString(),baseURL:n,totals:Yr(e),scenarios:e.map(r=>({feature:r.scenario.featurePath,scenario:r.scenario.displayName,tags:[...r.scenario.tags],status:r.status,source:r.source,proposalReplay:r.proposalReplay,durationMs:r.durationMs,usage:r.usage,steps:r.stepResults,narrative:r.narrative,adaptations:r.adaptations,suggestedFeatureEdit:r.suggestedFeatureEdit,featureEdits:r.featureEdits,verified:r.verified,proofError:r.proofError,proposalFile:r.proposalFile,proposalRevision:r.proposalRevision,proposalDiff:r.proposalDiff,error:r.error,evidence:r.evidence,uniques:r.uniques,proofUniques:r.proofUniques}))}}function Zr(e,t){let n=Kr.join(e,".saffron","reports");Gr.mkdirSync(n,{recursive:!0});let r=Kr.join(n,"latest.json"),s=Kr.join(n,"latest.html");return Gr.writeFileSync(r,`${JSON.stringify(t,null,2)}
1017
+ `),Gr.writeFileSync(s,_n(t,e)),{json:r,html:s}}import Rd from"node:crypto";import zt from"node:fs";import Mn from"node:path";var za=["scenario","file"];function qa(e){let t=/^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec(e),n=t?Number(t[1]):NaN,r=t?Number(t[2]):NaN;if(!t||r<1||n<1||n>r)throw new Error(`invalid --shard "${e}": use <index>/<total>, e.g. --shard 2/4 (index from 1 to total)`);return{index:n,total:r}}function Ha(e){if(!za.includes(e))throw new Error(`invalid --shard-by "${e}": use ${za.map(t=>`"${t}"`).join(" or ")}`);return e}function Ed(e){return`shard-${e.index}-of-${e.total}`}var Ad=e=>`${e.featurePath}${e.displayName}`;function Wa(e,t){let n=t.by??"scenario",r=e.map(Ad),s=new Set;if(n==="file"){let i=new Map;e.forEach((f,d)=>i.set(f.featurePath,[...i.get(f.featurePath)??[],d]));let c=new Array(t.total).fill(0),l=[...i].sort((f,d)=>d[1].length-f[1].length||(f[0]<d[0]?-1:f[0]>d[0]?1:0));for(let[,f]of l){let d=c.indexOf(Math.min(...c));if(c[d]+=f.length,d===t.index-1)for(let m of f)s.add(m)}}else e.map((c,l)=>l).sort((c,l)=>r[c]<r[l]?-1:r[c]>r[l]?1:0).forEach((c,l)=>{l%t.total===t.index-1&&s.add(c)});let o=e.map((i,c)=>c).filter(i=>s.has(i)),a=Rd.createHash("sha256").update([...r].sort().join(`
1018
+ `)).digest("hex").slice(0,12);return{scenarios:o.map(i=>e[i]),info:{index:t.index,total:t.total,by:n,suite:a,suiteSize:e.length,positions:o}}}function Va(e=process.env){return e.SAFFRON_RUN_ID||e.GITHUB_RUN_ID&&`github:${e.GITHUB_RUN_ID}`||e.CI_PIPELINE_ID&&`gitlab:${e.CI_PIPELINE_ID}`||e.BUILD_BUILDID&&`azure:${e.BUILD_BUILDID}`||e.CIRCLE_WORKFLOW_ID&&`circleci:${e.CIRCLE_WORKFLOW_ID}`||void 0}function Xr(e,t){let n=Mn.join(e,".saffron","reports");zt.mkdirSync(n,{recursive:!0});let r=Mn.join(n,`${Ed(t.shard)}.json`);return zt.writeFileSync(r,`${JSON.stringify(t,null,2)}
1019
+ `),r}var Id=e=>{let t=e;return!!t&&t.tool==="saffron"&&!!t.shard&&typeof t.shard.index=="number"&&typeof t.shard.total=="number"&&Array.isArray(t.shard.positions)&&Array.isArray(t.scenarios)&&!!t.history&&Array.isArray(t.history.scenarios)},Cd=/^shard-\d+-of-\d+\.json$/;function Ja(e){let t=[],n=(r,s)=>{let o=zt.statSync(r,{throwIfNoEntry:!1});if(!o)throw new Error(`not found: ${r}`);if(o.isDirectory())for(let a of zt.readdirSync(r).sort())n(Mn.join(r,a),!1);else(s||Cd.test(Mn.basename(r)))&&t.push(r)};for(let r of e)n(r,!0);return t.map(r=>{let s;try{s=JSON.parse(zt.readFileSync(r,"utf8"))}catch(o){throw new Error(`cannot read shard report ${r}: ${o instanceof Error?o.message:String(o)}`)}if(!Id(s))throw new Error(`${r} is not a shard report (written by saffron run --shard)`);return{file:r,report:s}})}var Dn=class extends Error{};function Ga(e){if(e.length===0)throw new Dn("no shard reports found. Shards write .saffron/reports/shard-<i>-of-<n>.json");let t=e[0].report.shard,n=[],r=u=>u.map(p=>p.file).join(", "),s=new Set(e.map(u=>u.report.shard.total));s.size>1&&n.push(`shards disagree on the shard count (${[...s].sort().join(", ")}): ${r(e)}`),new Set(e.map(u=>`${u.report.shard.suite}/${u.report.shard.suiteSize}`)).size>1&&n.push("shards ran different suites (different scenarios or filters). Every shard must run the same paths and --filter");let a=new Set(e.map(u=>u.report.shard.by??"scenario"));a.size>1&&n.push(`shards split the suite in different ways (--shard-by ${[...a].sort().join(" and ")}): every shard must use the same`);let i=new Set(e.map(u=>u.report.shard.ciRun??""));i.size>1&&n.push(`shards come from different CI runs (${[...i].map(u=>u||"none recorded").join(", ")})`);let c=new Map;for(let u of e)c.set(u.report.shard.index,[...c.get(u.report.shard.index)??[],u]);for(let[u,p]of[...c].sort((b,g)=>b[0]-g[0]))p.length>1&&n.push(`shard ${u}/${t.total} appears ${p.length} times: ${r(p)}`);if(s.size===1){let u=Array.from({length:t.total},(p,b)=>b+1).filter(p=>!c.has(p));u.length>0&&n.push(`missing ${u.length===1?"shard":"shards"} ${u.map(p=>`${p}/${t.total}`).join(", ")}. A merge without ${u.length===1?"it":"them"} would under-count the run`)}if(n.length===0){let u=new Set,p=0;for(let g of e){let{positions:S}=g.report.shard;(S.length!==g.report.scenarios.length||S.length!==g.report.history.scenarios.length)&&n.push(`${g.file} is inconsistent: ${S.length} positions for ${g.report.scenarios.length} scenarios`);for(let T of S)u.has(T)&&n.push(`two shards both ran scenario ${T+1} of the suite`),u.add(T);p+=S.length}let b=[...new Set(e.map(g=>g.report.baseURL??"(no base URL)"))];b.length>1&&n.push(`the shards ran against different base URLs: ${b.join(", ")}. Merge the shards of one environment at a time`),n.length===0&&p!==t.suiteSize&&n.push(`the shards hold ${p} scenarios but the suite has ${t.suiteSize}`)}if(n.length>0)throw new Dn(n.join(`
1020
+ `));let l=e.sort((u,p)=>u.report.shard.index-p.report.shard.index).flatMap(({report:u})=>u.shard.positions.map((p,b)=>({position:p,scenario:u.scenarios[b],history:u.history.scenarios[b]}))).sort((u,p)=>u.position-p.position),f=e.map(u=>u.report),d=f.map(u=>u.startedAt).sort()[0],m=f.map(u=>u.finishedAt).sort().at(-1),h=l.map(u=>u.scenario),y=f.flatMap(u=>u.hooks??[]),x={tool:"saffron",version:f[0].version,startedAt:d,finishedAt:m,baseURL:f.find(u=>u.baseURL)?.baseURL,totals:Yr(h),scenarios:h,shards:t.total};y.length>0&&(x.hooks=y);let k=l.map(u=>u.history),v={runId:t.ciRun?`${t.ciRun}:${t.suite}:${x.baseURL??""}`:d,startedAt:d,finishedAt:m,baseURL:x.baseURL,green:k.filter(u=>u.status==="green").length,yellow:k.filter(u=>u.status==="yellow").length,red:k.filter(u=>u.status==="red").length,aiCalls:k.reduce((u,p)=>u+p.aiCalls,0),costUsd:k.reduce((u,p)=>u+p.costUsd,0),divergentSteps:Math.max(...f.map(u=>u.history.divergentSteps)),scenarios:k};return{report:x,history:v}}var qt=class extends Error{},es=new Set(["and","or","not","(",")"]);function Pd(e){return e.replace(/[(),|]/g,t=>t==="("||t===")"?` ${t} `:" or ").split(/\s+/).filter(Boolean).map(t=>es.has(t.toLowerCase())?t.toLowerCase():t.startsWith("@")?t:`@${t}`)}function _d(e){let t=Pd(e),n=0,r=()=>t[n],s=l=>{throw new qt(`--filter "${e}": ${l}`)},o=()=>{let l=a();for(;r()==="or"||r()!==void 0&&!es.has(r());){r()==="or"&&n++;let f=a(),d=l;l=m=>d(m)||f(m)}return l},a=()=>{let l=i();for(;r()==="and";){n++;let f=i(),d=l;l=m=>d(m)&&f(m)}return l},i=()=>{let l=r();if(l===void 0)return s("expected a tag at the end");if(l==="not"){n++;let f=i();return d=>!f(d)}if(l==="("){n++;let f=o();return r()!==")"?s("missing a closing parenthesis"):(n++,f)}return es.has(l)?s(`expected a tag, found "${l}"`):(n++,f=>f.has(l))},c=o();return n<t.length?s(`unexpected "${t[n]}"`):c}function Ka(e){let t=(e===void 0?[]:Array.isArray(e)?e:[e]).map(r=>r.trim()).filter(Boolean);if(t.length===0)return;let n=t.map(_d);return{text:t.length===1?t[0]:t.map(r=>`(${r})`).join(" or "),matches:r=>{let s=new Set(r);return n.some(o=>o(s))}}}function Ya(e,t){return t?e.filter(n=>t.matches(n.tags)):e}function Qa(e,t){return[...t??[],e]}import{spawn as jd}from"node:child_process";function ts(e,t){if(e==null)return[];let n=Array.isArray(e)?e:[e];for(let r of n)if(typeof r!="string"||r.trim()==="")throw new Error(`"${t}" in saffron.config.json must be a command, or a list of commands, e.g. "${t}": "npm run seed"`);return n}function Od(e,t,n){let r=Date.now();return new Promise(s=>{let o=process.platform!=="win32",a=jd(t,{cwd:n.cwd,shell:!0,detached:o,env:{...process.env,...n.env},stdio:["ignore",process.stderr,process.stderr]}),i=!1,c=!1,l=()=>{try{o&&a.pid?process.kill(-a.pid,"SIGKILL"):a.kill("SIGKILL")}catch{}},f=setTimeout(()=>{i=!0,l()},n.timeoutMs),d=()=>{c=!0,l()};n.signal?.addEventListener("abort",d,{once:!0});let m=h=>{clearTimeout(f),n.signal?.removeEventListener("abort",d),s({phase:e,command:t,exitCode:h,durationMs:Date.now()-r,...i?{timedOut:i}:{},...c?{cancelled:c}:{}})};a.on("error",()=>m(null)),a.on("exit",h=>m(i||c?null:h))})}var Ln=e=>e.exitCode!==0;async function ns(e,t,n){let r=[];for(let s of t){if(n.signal?.aborted)break;console.error(`saffron ${e}: ${s}`);let o=await Od(e,s,n);if(r.push(o),o.cancelled||Ln(o)&&(console.error(`saffron ${e} failed: ${rs(o,n.timeoutMs)}`),e==="setup"))break}return r}function rs(e,t){return e.cancelled?`"${e.command}" was interrupted`:e.timedOut?`"${e.command}" was stopped after ${Math.round(t/1e3)}s (hookTimeoutMs)`:e.exitCode===null?`"${e.command}" could not be started`:`"${e.command}" exited with code ${e.exitCode}`}import U from"node:fs";import H from"node:path";var Ht={claude:".claude/skills",agents:".agents/skills",copilot:".github/skills",cursor:".cursor/skills"},Fd=["claude","agents"],ei="<!-- saffron:start -->",os="<!-- saffron:end -->";function Nd(){return[ei,"## Saffron end-to-end tests","","UI tests are plain-Gherkin `.feature` / `.saffron` files under `features/`,","run by Saffron (`npx saffron run`). No step definitions exist: an AI agent","records each scenario once; later runs replay at zero tokens.","","- Load the `saffron` skill before writing or editing scenarios.","- Exact step text is the cache identity: run `npx saffron steps` and reuse"," recorded wordings instead of inventing synonyms.","- `Then` steps are never healed: write them as the verdict, last.","- `npx saffron status` shows what is cached, what awaits review, the tags"," and vocabulary health; editing a cached scenario's text re-records it.","- Never edit `.saffron/cache/` or `.saffron/proposals/` by hand; change the"," scenario text and re-run. Review proposals with `npx saffron accept`"," (`--all`, or chosen files) and `npx saffron reject`.","- Secrets go in `{env:VAR}` tokens, never in files.","- Shared test data (accounts, names, enum values) goes in `data/*.json`,",' referenced as `{data:file.path.key}`; check a whole list with "every',' {data:file.list}"; read Examples rows from a file with'," `Examples: {data:file}` (.saffron only). Use `{unique:name}` for a value"," that must be new on every run. Never put secrets in `data/`.",os,""].join(`
1021
+ `)}function Za(e,t){if(!U.existsSync(e))return U.writeFileSync(e,t),"created";let n=U.readFileSync(e,"utf8"),r=n.indexOf(ei),s=n.indexOf(os);if(r!==-1&&s!==-1&&s>r){let a=n.slice(0,r)+t.trimEnd()+n.slice(s+os.length);return a===n?"unchanged":(U.writeFileSync(e,a),"updated")}let o=n.endsWith(`
1022
1022
  `)?`
1023
1023
  `:`
1024
1024
 
1025
- `;return U.writeFileSync(e,n+s+t),"updated"}var Xr={command:"npx",args:["saffron","mcp"]},Id={claude:{file:".mcp.json",key:"mcpServers",entry:Xr},cursor:{file:".cursor/mcp.json",key:"mcpServers",entry:Xr},copilot:{file:".vscode/mcp.json",key:"servers",entry:{type:"stdio",...Xr}}},Ga={agents:"codex mcp add saffron -- npx saffron mcp (Codex; other hosts: see their MCP settings)"};function Pd(e,t){let n=H.join(e,t.file),r={},o=U.existsSync(n);if(o)try{r=JSON.parse(U.readFileSync(n,"utf8"))}catch{throw new Error(`${t.file} is not valid JSON: fix it or remove it, then re-run`)}let s=r[t.key]??{};return JSON.stringify(s.saffron)===JSON.stringify(t.entry)?"unchanged":(s.saffron=t.entry,r[t.key]=s,U.mkdirSync(H.dirname(n),{recursive:!0}),U.writeFileSync(n,JSON.stringify(r,null,2)+`
1026
- `),o?"updated":"created")}function Ya(e,t){U.mkdirSync(t,{recursive:!0});for(let n of U.readdirSync(e,{withFileTypes:!0})){let r=H.join(e,n.name),o=H.join(t,n.name);n.isDirectory()?Ya(r,o):U.copyFileSync(r,o)}}function Qa(e){let t=H.join(e.packageRoot,"skills","saffron");if(!U.existsSync(H.join(t,"SKILL.md")))throw new Error(`bundled skill not found at ${t}`);let n={skillDirs:[],instructionFiles:[],scaffolded:[],mcpFiles:[],mcpHints:[]},r=e.targets??Ed;for(let o of r){let s=H.join(e.projectRoot,qt[o],"saffron");U.existsSync(s)&&U.rmSync(s,{recursive:!0,force:!0}),Ya(t,s),n.skillDirs.push(H.relative(e.projectRoot,s))}if(e.mcp!==!1)for(let o of r){let s=Id[o];s?n.mcpFiles.push({file:s.file,outcome:Pd(e.projectRoot,s)}):Ga[o]&&n.mcpHints.push(Ga[o])}if(e.instructions!==!1){let o=Ad(),s=H.join(e.projectRoot,"AGENTS.md");n.instructionFiles.push({file:"AGENTS.md",outcome:Ja(s,o)});let a=H.join(e.projectRoot,"CLAUDE.md");U.existsSync(a)&&n.instructionFiles.push({file:"CLAUDE.md",outcome:Ja(a,o)})}if(e.examples){n.examples=Cd(e.projectRoot,e.packageRoot);for(let o of n.examples.files)n.scaffolded.push(o)}if(e.scaffold!==!1){let o=H.join(e.projectRoot,"saffron.config.json");U.existsSync(o)||(U.writeFileSync(o,JSON.stringify({baseURL:"http://localhost:3000",features:"features"},null,2)+`
1027
- `),n.scaffolded.push("saffron.config.json"));let s=H.join(e.projectRoot,"features");U.existsSync(s)||(U.mkdirSync(s,{recursive:!0}),n.scaffolded.push("features/"));let a=H.join(e.projectRoot,".gitignore"),i=e.examples?[".saffron/reports/",".saffron/artifacts/",".env"]:[".saffron/reports/",".saffron/artifacts/"],c=U.existsSync(a)?U.readFileSync(a,"utf8"):"",p=[];for(let f of i)c.split(`
1025
+ `;return U.writeFileSync(e,n+o+t),"updated"}var ss={command:"npx",args:["saffron","mcp"]},Md={claude:{file:".mcp.json",key:"mcpServers",entry:ss},cursor:{file:".cursor/mcp.json",key:"mcpServers",entry:ss},copilot:{file:".vscode/mcp.json",key:"servers",entry:{type:"stdio",...ss}}},Xa={agents:"codex mcp add saffron -- npx saffron mcp (Codex; other hosts: see their MCP settings)"};function Dd(e,t){let n=H.join(e,t.file),r={},s=U.existsSync(n);if(s)try{r=JSON.parse(U.readFileSync(n,"utf8"))}catch{throw new Error(`${t.file} is not valid JSON: fix it or remove it, then re-run`)}let o=r[t.key]??{};return JSON.stringify(o.saffron)===JSON.stringify(t.entry)?"unchanged":(o.saffron=t.entry,r[t.key]=o,U.mkdirSync(H.dirname(n),{recursive:!0}),U.writeFileSync(n,JSON.stringify(r,null,2)+`
1026
+ `),s?"updated":"created")}function ti(e,t){U.mkdirSync(t,{recursive:!0});for(let n of U.readdirSync(e,{withFileTypes:!0})){let r=H.join(e,n.name),s=H.join(t,n.name);n.isDirectory()?ti(r,s):U.copyFileSync(r,s)}}function ni(e){let t=H.join(e.packageRoot,"skills","saffron");if(!U.existsSync(H.join(t,"SKILL.md")))throw new Error(`bundled skill not found at ${t}`);let n={skillDirs:[],instructionFiles:[],scaffolded:[],mcpFiles:[],mcpHints:[]},r=e.targets??Fd;for(let s of r){let o=H.join(e.projectRoot,Ht[s],"saffron");U.existsSync(o)&&U.rmSync(o,{recursive:!0,force:!0}),ti(t,o),n.skillDirs.push(H.relative(e.projectRoot,o))}if(e.mcp!==!1)for(let s of r){let o=Md[s];o?n.mcpFiles.push({file:o.file,outcome:Dd(e.projectRoot,o)}):Xa[s]&&n.mcpHints.push(Xa[s])}if(e.instructions!==!1){let s=Nd(),o=H.join(e.projectRoot,"AGENTS.md");n.instructionFiles.push({file:"AGENTS.md",outcome:Za(o,s)});let a=H.join(e.projectRoot,"CLAUDE.md");U.existsSync(a)&&n.instructionFiles.push({file:"CLAUDE.md",outcome:Za(a,s)})}if(e.examples){n.examples=Ld(e.projectRoot,e.packageRoot);for(let s of n.examples.files)n.scaffolded.push(s)}if(e.scaffold!==!1){let s=H.join(e.projectRoot,"saffron.config.json");U.existsSync(s)||(U.writeFileSync(s,JSON.stringify({baseURL:"http://localhost:3000",features:"features"},null,2)+`
1027
+ `),n.scaffolded.push("saffron.config.json"));let o=H.join(e.projectRoot,"features");U.existsSync(o)||(U.mkdirSync(o,{recursive:!0}),n.scaffolded.push("features/"));let a=H.join(e.projectRoot,".gitignore"),i=e.examples?[".saffron/reports/",".saffron/artifacts/",".env"]:[".saffron/reports/",".saffron/artifacts/"],c=U.existsSync(a)?U.readFileSync(a,"utf8"):"",l=[];for(let f of i)c.split(`
1028
1028
  `).some(d=>d.trim()===f)||(c+=(c&&!c.endsWith(`
1029
1029
  `)?`
1030
1030
  `:"")+f+`
1031
- `,p.push(f));p.length>0&&(U.writeFileSync(a,c),n.scaffolded.push(`.gitignore (+ ${p.join(", ")})`))}return n}function Cd(e,t){let n=H.join(t,"templates","saucedemo");if(!U.existsSync(H.join(n,"features")))return{installed:!1,files:[],reason:`bundled examples not found at ${n}`};let r=H.join(e,"features");if(U.existsSync(r)&&U.readdirSync(r).length>0)return{installed:!1,files:[],reason:"features/ is not empty; examples are only installed into an empty features directory"};let o=[];U.mkdirSync(r,{recursive:!0});for(let c of U.readdirSync(H.join(n,"features")))U.copyFileSync(H.join(n,"features",c),H.join(r,c)),o.push(`features/${c}`);let s=H.join(n,"data");if(U.existsSync(s)){U.mkdirSync(H.join(e,"data"),{recursive:!0});for(let c of U.readdirSync(s)){let p=H.join(e,"data",c);U.existsSync(p)||(U.copyFileSync(H.join(s,c),p),o.push(`data/${c}`))}}let a=H.join(e,"saffron.config.json");U.existsSync(a)||(U.copyFileSync(H.join(n,"saffron.config.json"),a),o.push("saffron.config.json (baseURL https://www.saucedemo.com, retries 1)"));let i=H.join(e,".env.example");return U.existsSync(i)||(U.copyFileSync(H.join(n,".env.example"),i),o.push(".env.example")),{installed:!0,files:o}}import _d from"node:path";function Za(e){return["saffron lsp is a language server: it speaks LSP over stdio and is meant","to be launched by your editor, not run by hand (nothing happens here).","","JetBrains (IntelliJ, WebStorm, PyCharm, Rider, incl. Community):"," Easiest: install the Saffron plugin from the JetBrains Marketplace"," (https://plugins.jetbrains.com/plugin/34240-saffron): highlighting,"," this language server, run configurations and the Saffron tool window,"," in one click. Manual setup instead:"," 1. Completion, diagnostics, hover, go-to-definition: install the free"," LSP4IJ plugin, then Settings \u2192 Languages & Frameworks \u2192 Language"," Servers \u2192 +: command `npx saffron lsp`, working dir = project root,"," file patterns *.saffron and *.feature."," 2. Syntax highlighting comes from a TextMate bundle, not the LSP:"," Settings \u2192 Editor \u2192 TextMate Bundles \u2192 + and pick this folder:",` ${e?_d.join(e,"textmate","saffron"):"node_modules/saffron-ai/textmate/saffron"}`,"","Neovim / other LSP editors: cmd = { 'npx', 'saffron', 'lsp' },","filetypes = { 'saffron', 'feature' }.","","VS Code: install the extension instead:"," code --install-extension ChathurangaJayasinghe.saffron-vscode"].join(`
1032
- `)}import{spawn as jd}from"node:child_process";function Od(e,t=process.platform){return t==="win32"?{command:'start "" "%SAFFRON_BROWSER_TARGET%"',args:[],options:{detached:!0,stdio:"ignore",shell:!0,windowsHide:!0,env:{...process.env,SAFFRON_BROWSER_TARGET:e}}}:{command:t==="darwin"?"open":"xdg-open",args:[e],options:{detached:!0,stdio:"ignore"}}}function ts(e){let{command:t,args:n,options:r}=Od(e),o=t.split(" ")[0],s=jd(t,n,r);s.on("error",()=>console.error(`Could not open a browser (${o} is not available here). Open the address above yourself.`)),s.unref()}function Xa(e,t){let n=t.length>0?t:["features"],r=[];for(let o of n){let s=M.isAbsolute(o)?o:M.join(e,o);if(G.existsSync(s))if(G.statSync(s).isDirectory())for(let a of G.readdirSync(s,{recursive:!0})){let i=String(a);Se(i)&&r.push(M.join(s,i))}else Se(s)&&r.push(s)}return r.sort()}function Md(){let e=process.env.ANTHROPIC_API_KEY||process.env.CLAUDE_CODE_OAUTH_TOKEN||process.env.CLAUDE_CODE_USE_BEDROCK||process.env.CLAUDE_CODE_USE_VERTEX,t=process.env.HOME??"",n=t&&(G.existsSync(M.join(t,".claude",".credentials.json"))||G.existsSync(M.join(t,".claude.json")));e||n||console.error(["note: no Claude credentials detected. Cached scenarios replay fine","without AI, but recording and healing need one of:"," \xB7 ANTHROPIC_API_KEY=sk-ant-... (console.anthropic.com \u2192 API keys)"," \xB7 a Claude Code login on this machine","Replay-only runs: add --no-agent to silence this note.",""].join(`
1033
- `))}async function Dd(e){let t=Fd.createInterface({input:process.stdin,output:process.stdout});try{let n=await new Promise(r=>t.question(e,r));return/^y(es)?$/i.test(n.trim())}finally{t.close()}}function Mn(e){console.error(`saffron: ${e.message}`),process.exit(2)}function ns(e){return e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e4?`${(e/1e3).toFixed(1)}k`:e.toLocaleString()}var Ld={green:"\x1B[32m",yellow:"\x1B[33m",red:"\x1B[31m"},Ud="\x1B[0m",q=new Nd().name("saffron").description("AI-fallback test runner for end-to-end tests written in plain language: zero-token cached replay, runtime AI healing.").version(Fe()).option("-p, --project-root <dir>","project root",process.cwd());q.command("run").description("Run feature files (cached replay; AI records/heals on demand)").argument("[paths...]","feature files or directories (default: features/)").option("--base-url <url>","base URL of the app under test").option("--env <name>","data environment: data/<file>.<name>.json is laid over data/<file>.json (also SAFFRON_ENV, or env in the config)").option("--headed","run the browser headed").option("--filter <tags>",'only run scenarios carrying any of these @tags: comma-separated ("@smoke,@WA-TC1") or repeat the flag',Wa).option("--no-agent","disable AI fallback (cache misses/failures go red)").option("--rerecord","discard the committed caches of the selected scenarios and let the agent record them fresh (the honest fix for a wrong recording)").option("--model <model>","model for the agent").option("--storage-state <file>","Playwright storageState JSON for authenticated sessions").option("--strict","treat passed-with-adaptation (yellow) as failure until its proposal is reviewed").option("--no-verify","skip the zero-AI proof replay of agent recordings before filing proposals").option("--no-reuse","disable Level-1 step reuse (seeding new recordings from existing step recordings)").option("--browser <name>",'browser: "chromium" (default), "chrome", "msedge", "firefox" or "webkit". Recording and healing need one of the first three').option("--heal-model <model>","cheaper model for heal sessions").option("--workers <n>","parallel replay workers (agent recording/healing stays sequential)").option("--shard <index/total>","run one part of the suite, e.g. 2/4 on the second of four CI machines; combine the parts with `saffron report --merge`").option("--shard-by <mode>",'how --shard splits the suite: "scenario" (default, spreads every file) or "file" (each feature file stays on one machine)').option("--no-hooks","skip the setup and teardown commands from the config").option("--trace <mode>",'execution trace per scenario: "off" (default), "on" or "retain-on-failure"; open one with saffron trace').option("--assertion-policy <policy>",'assertion adaptability: "strict" (default) or "adaptable-mid" (mid-scenario assertions may be adapted; the final assertion block never is)').action(async(e,t)=>{let n=M.resolve(q.opts().projectRoot),r=ae(n);t.env&&tt(n,r.dataDir,t.env);let o=t.baseUrl??r.baseURL,s=Xa(n,e.length>0?e:r.features?[r.features]:[]);s.length===0&&(console.error("No .feature/.saffron files found."),process.exit(2)),t.agent!==!1&&Md();let a;try{let $=ke(n,r,s);a=s.flatMap(O=>se(O,n,$))}catch($){throw $ instanceof Z&&Mn($),$}let i=Ha(t.filter);i.length>0&&(a=Va(a,i),a.length===0&&(console.error(`No scenarios match --filter ${i.join(",")} in ${s.length} feature file(s).`),process.exit(2)));let c;if(t.shardBy!==void 0&&t.shard===void 0&&(console.error("saffron: --shard-by needs --shard, e.g. --shard 2/4 --shard-by file"),process.exit(2)),t.shard!==void 0){try{let $=La(String(t.shardBy??r.shardBy??"scenario")),O=Ua(a,{...Da(String(t.shard)),by:$});a=O.scenarios,c={...O.info,ciRun:Ba()}}catch($){console.error($ instanceof Error?$.message:String($)),process.exit(2)}if(console.log(`shard ${c.index}/${c.total}${c.by==="file"?" (by file)":""}: ${a.length} of ${c.suiteSize} scenario(s)`),a.length===0){let $=new Date,O=Jr([],$,o),R=kr([],$,ye(n).divergent.length,o);console.log(`report: ${Kr(n,{...O,shard:c,history:R})}`),process.exit(0)}}if(t.rerecord){t.agent===!1&&(console.error("saffron: --rerecord needs the agent; drop --no-agent"),process.exit(2));let $=0;for(let O of a){let R=J(n,O.featurePath,O.name);G.existsSync(R)&&(G.rmSync(R),$++)}console.log(`rerecord: removed ${$} cache file(s) for ${a.length} scenario(s); the agent records them fresh (seeded from the other recordings). Restore with git if needed.`)}{let $=a;try{let R=Xa(n,[]),F=ke(n,r,R);$=R.flatMap(z=>{try{return se(z,n,F)}catch{return[]}})}catch{}let O=us(n,$);if(O.length>0){console.error("cache path collision:");for(let R of O)console.error(` ${R}`);process.exit(2)}}{let $=new Map;for(let R of a){let F=X(R),z=st(J(n,R.featurePath,R.name));z&&F.push(JSON.stringify(z.steps));for(let Q of Jn([...F,...Gn(X(R))]))process.env[Q]===void 0&&!$.has(Q)&&$.set(Q,R.displayName)}let O=new Map;for(let R of a)for(let F of qe(X(R)))if(!O.has(F))try{Jt(F)}catch(z){O.set(F,`${z instanceof Error?z.message:String(z)} (used by "${R.displayName}")`)}if(O.size>0){for(let R of O.values())console.error(R);process.exit(2)}if($.size>0){for(let[R,F]of $)console.error(`missing environment variable ${R} (referenced as {env:${R}} by "${F}")`);console.error("set the variable(s), or add them to a git-ignored .env in the project root"),process.exit(2)}}let p=[],f=[];if(t.hooks!==!1)try{p=Yr(r.setup,"setup"),f=Yr(r.teardown,"teardown")}catch($){console.error($ instanceof Error?$.message:String($)),process.exit(2)}let d=new kn({projectRoot:n,baseURL:o,headed:t.headed,actionTimeoutMs:r.actionTimeoutMs,pollIntervalMs:r.pollIntervalMs,screenshot:r.screenshot,trace:(()=>{let $=t.trace??r.trace??"off";return or.includes($)||(console.error(`invalid --trace "${$}": use ${or.map(O=>`"${O}"`).join(", ")}`),process.exit(2)),$})(),retries:r.retries,storageState:t.storageState??r.storageState,browser:(()=>{let $=t.browser??r.browser??"chromium";return Qo($)||(console.error(`invalid --browser "${$}": use ${Cr.map(O=>`"${O}"`).join(", ")}`),process.exit(2)),$})(),healModel:t.healModel??r.healModel,verifyProposals:t.verify===!1?!1:r.verifyProposals,reuseSteps:t.reuse===!1?!1:r.reuseSteps,assertionPolicy:(()=>{let $=t.assertionPolicy??r.assertionPolicy??"strict";return $!=="strict"&&$!=="adaptable-mid"&&(console.error(`invalid --assertion-policy "${$}": use "strict" or "adaptable-mid"`),process.exit(2)),$})(),agent:t.agent?new En({model:t.model??r.model,maxTurns:r.maxTurns,snapshotMode:r.snapshotMode}):void 0}),m=new Date,h=Math.max(1,Number.parseInt(String(t.workers??r.workers??1),10)||1),y=new Array(a.length),b=$=>{let O=$.scenario,R=Ld[$.status],F=$.usage.inputTokens+$.usage.outputTokens,z=$.usage.cacheReadTokens+$.usage.cacheCreationTokens,Q=z>0?`, ${ns(z)} cache traffic`:"";console.log(`${R}${$.status.toUpperCase().padEnd(7)}${Ud}${O.displayName} (${$.source}, ${$.durationMs}ms, ${$.usage.aiCalls} AI calls, ${F} tokens${Q})`);for(let ge of $.adaptations)console.log(` \u26A0 ${ge}`);$.seededSteps&&console.log(` \u26A1 ${$.seededSteps}/${$.stepResults.length} steps seeded from existing recordings (zero AI)`);let K=$.stepResults.reduce((ge,Vt)=>ge+(Vt.retries??0),0);K&&console.log(` \u21BB ${K} action ${K===1?"retry":"retries"} absorbed before settling (retries: ${r.retries??1} per action)`),$.verified===!0?console.log(" \u2713 proposal verified by zero-AI proof replay"):$.verified===!1&&console.log(` \u2717 proposal UNVERIFIED, proof replay failed: ${$.proofError?.split(`
1034
- `)[0]}`),$.error&&console.log(` ${$.error}`)},k=new AbortController,w={cwd:n,timeoutMs:r.hookTimeoutMs??Kn.hookTimeoutMs,env:{SAFFRON_BASE_URL:o,SAFFRON_ENV:je().env,SAFFRON_SHARD:c?`${c.index}/${c.total}`:void 0}},v=[],l,u=()=>l??=Qr("teardown",f,w).then($=>{v.push(...$)}),x=Promise.resolve([]);p.length+f.length>0&&process.on("SIGINT",()=>{if(k.signal.aborted){console.error("still cleaning up: teardown finishes, then Saffron exits");return}console.error(`
1035
- interrupted: stopping, then running teardown`),k.abort(),x.then(()=>d.stop()).catch(()=>{}).then(u).finally(()=>process.exit(130))}),p.length>0&&(x=Qr("setup",p,{...w,signal:k.signal}),v.push(...await x),k.signal.aborted&&await new Promise(()=>{}),v.some(Nn)&&(console.error("No scenario was run: the application is not in the state the suite expects."),await u(),process.exit(2)));try{await d.start()}catch($){throw await u(),$ instanceof Dt&&(console.error($.message),process.exit(2)),$}try{if(h>1&&a.length>1){let $=a.map((R,F)=>({s:R,i:F})),O=[];await Promise.all(Array.from({length:Math.min(h,$.length)},async()=>{for(;;){let R=$.shift();if(!R)return;let F=await d.runScenario(R.s,{agentDisabled:!0});F.status==="red"?O.push(R):(y[R.i]=F,b(F))}}));for(let R of O.sort((F,z)=>F.i-z.i)){let F=await d.runScenario(R.s);y[R.i]=F,b(F)}}else for(let $=0;$<a.length;$++){let O=await d.runScenario(a[$]);y[$]=O,b(O)}}finally{await d.stop(),await u()}let g=Jr(y,m,o);v.length>0&&(g.hooks=v);let S=ye(n).divergent.length,T=kr(y,m,S,o);if(!c){let $=Ft(n);g.trends=Tr($,T),$r(n,T)}g.totals.aiCalls>0&&(g.totals.plan=await d.agent?.planUsage?.());let A;if(c){let $=Kr(n,{...g,shard:c,history:T}),O=$.replace(/\.json$/,".html");G.writeFileSync(O,In(g,n)),A=`${O}
1036
- ${$}`}else{let{html:$,json:O}=Gr(n,g);A=`${$}
1037
- ${O}`}let E=g.totals,C=E.cacheReadTokens+E.cacheCreationTokens;console.log(`
1038
- ${E.green} passed, ${E.yellow} pending review, ${E.red} failed \xB7 ${E.aiCalls} AI calls \xB7 ${(E.inputTokens+E.outputTokens).toLocaleString()} tokens in+out${C>0?` \xB7 ${ns(E.cacheReadTokens)} cache read \xB7 ${ns(E.cacheCreationTokens)} cache written`:""}${Nr(E)}${E.models?.length?` \xB7 ${E.models.join(", ")}`:""}`);let I=g.trends;if(I?.prev){let $=I.prev.passRatePp,O=I.prev.costUsd;console.log(`vs previous run: pass rate ${$>=0?"+":""}${$}pp \xB7 cost ${O>=0?"+":"-"}$${Math.abs(O).toFixed(4)} \xB7 AI calls ${I.prev.aiCalls>=0?"+":""}${I.prev.aiCalls}`)}for(let $ of I?.chronic??[])console.log(`\u26A0 chronic: ${$.feature} \u203A ${$.scenario}: ${$.heals} heal(s), ${$.reds} red(s) in last ${$.runs} run(s); re-record or fix the feature text instead of paying for more heals`);console.log(`report: ${A}`),c&&console.log(`shard report, not a whole run: history and trends wait for \`saffron report --merge\` over all ${c.total} shard-*-of-${c.total}.json files`);let P=Me(M.resolve(q.opts().projectRoot),Ht);P.length>0&&console.log(`${P.length} cache proposal(s) pending. Review with: saffron accept | saffron reject`);let j=t.strict??r.strict??!1;j&&E.yellow>0&&console.log(`strict mode: ${E.yellow} scenario(s) pending review treated as failure. Review proposals to go green.`);let L=E.red>0||j&&E.yellow>0,_=v.filter($=>$.phase==="teardown"&&Nn($));_.length>0&&console.log(`teardown failed: ${_.map($=>Zr($,w.timeoutMs)).join("; ")}. The environment may hold data from this run.`),process.exit(L?1:_.length>0?2:0)});q.command("accept").description("Promote pending cache proposals to committed caches").argument("[files...]","proposal file(s) (default with --all: every proposal)").option("--all","accept all pending proposals").option("--with-feature-edit","also apply the proposal's suggested .feature step rewrites").option("--propagate","apply the same locator fixes to every other cache using the same locator").option("--include-unverified","with --all: also accept proposals whose proof replay failed").action((e,t)=>{let n=M.resolve(q.opts().projectRoot),r=ae(n),o=e.length>0?e.map(Zn).map(s=>({...s,file:M.resolve(s.file)})):t.all?Me(n,Ht).map(s=>({file:s.file,revision:void 0})):[];if(o.length===0){ri(n);return}try{for(let{file:s,revision:a}of o){let i=w=>{console.log(`not accepted: ${M.basename(s)} ${w}`),process.exitCode=1},c=ni(n,s,a!==void 0,()=>{if(a&&At(s)!==a)return i(ti);let w;try{w=pt(s)}catch(u){if(!(u instanceof W))throw u;return i(`cannot be read: ${u.problem}`)}if(w.meta.bound){let u=Re(n,w,ke(n,r,[]));if(u)return i(`is stale: ${u}. Run the scenario again for a proposal made against what is there now, or reject this one.`)}else console.log(`\u26A0 ${M.basename(s)} is ${wn}; accepting as it is.`);if(w.meta.verified===!1){if(t.all&&!t.includeUnverified){console.log(`skipped UNVERIFIED ${M.basename(s)} (${w.meta.proofError?.split(`
1031
+ `,l.push(f));l.length>0&&(U.writeFileSync(a,c),n.scaffolded.push(`.gitignore (+ ${l.join(", ")})`))}return n}function Ld(e,t){let n=H.join(t,"templates","saucedemo");if(!U.existsSync(H.join(n,"features")))return{installed:!1,files:[],reason:`bundled examples not found at ${n}`};let r=H.join(e,"features");if(U.existsSync(r)&&U.readdirSync(r).length>0)return{installed:!1,files:[],reason:"features/ is not empty; examples are only installed into an empty features directory"};let s=[];U.mkdirSync(r,{recursive:!0});for(let c of U.readdirSync(H.join(n,"features")))U.copyFileSync(H.join(n,"features",c),H.join(r,c)),s.push(`features/${c}`);let o=H.join(n,"data");if(U.existsSync(o)){U.mkdirSync(H.join(e,"data"),{recursive:!0});for(let c of U.readdirSync(o)){let l=H.join(e,"data",c);U.existsSync(l)||(U.copyFileSync(H.join(o,c),l),s.push(`data/${c}`))}}let a=H.join(e,"saffron.config.json");U.existsSync(a)||(U.copyFileSync(H.join(n,"saffron.config.json"),a),s.push("saffron.config.json (baseURL https://www.saucedemo.com, retries 1)"));let i=H.join(e,".env.example");return U.existsSync(i)||(U.copyFileSync(H.join(n,".env.example"),i),s.push(".env.example")),{installed:!0,files:s}}import Ud from"node:path";function ri(e){return["saffron lsp is a language server: it speaks LSP over stdio and is meant","to be launched by your editor, not run by hand (nothing happens here).","","JetBrains (IntelliJ, WebStorm, PyCharm, Rider, incl. Community):"," Easiest: install the Saffron plugin from the JetBrains Marketplace"," (https://plugins.jetbrains.com/plugin/34240-saffron): highlighting,"," this language server, run configurations and the Saffron tool window,"," in one click. Manual setup instead:"," 1. Completion, diagnostics, hover, go-to-definition: install the free"," LSP4IJ plugin, then Settings \u2192 Languages & Frameworks \u2192 Language"," Servers \u2192 +: command `npx saffron lsp`, working dir = project root,"," file patterns *.saffron and *.feature."," 2. Syntax highlighting comes from a TextMate bundle, not the LSP:"," Settings \u2192 Editor \u2192 TextMate Bundles \u2192 + and pick this folder:",` ${e?Ud.join(e,"textmate","saffron"):"node_modules/saffron-ai/textmate/saffron"}`,"","Neovim / other LSP editors: cmd = { 'npx', 'saffron', 'lsp' },","filetypes = { 'saffron', 'feature' }.","","VS Code: install the extension instead:"," code --install-extension ChathurangaJayasinghe.saffron-vscode"].join(`
1032
+ `)}import{spawn as Bd}from"node:child_process";function zd(e,t=process.platform){return t==="win32"?{command:'start "" "%SAFFRON_BROWSER_TARGET%"',args:[],options:{detached:!0,stdio:"ignore",shell:!0,windowsHide:!0,env:{...process.env,SAFFRON_BROWSER_TARGET:e}}}:{command:t==="darwin"?"open":"xdg-open",args:[e],options:{detached:!0,stdio:"ignore"}}}function as(e){let{command:t,args:n,options:r}=zd(e),s=t.split(" ")[0],o=Bd(t,n,r);o.on("error",()=>console.error(`Could not open a browser (${s} is not available here). Open the address above yourself.`)),o.unref()}function si(e,t){let n=t.length>0?t:["features"],r=[];for(let s of n){let o=D.isAbsolute(s)?s:D.join(e,s);if(G.existsSync(o))if(G.statSync(o).isDirectory())for(let a of G.readdirSync(o,{recursive:!0})){let i=String(a);ke(i)&&r.push(D.join(o,i))}else ke(o)&&r.push(o)}return r.sort()}function Wd(){let e=process.env.ANTHROPIC_API_KEY||process.env.CLAUDE_CODE_OAUTH_TOKEN||process.env.CLAUDE_CODE_USE_BEDROCK||process.env.CLAUDE_CODE_USE_VERTEX,t=process.env.HOME??"",n=t&&(G.existsSync(D.join(t,".claude",".credentials.json"))||G.existsSync(D.join(t,".claude.json")));e||n||console.error(["note: no Claude credentials detected. Cached scenarios replay fine","without AI, but recording and healing need one of:"," \xB7 ANTHROPIC_API_KEY=sk-ant-... (console.anthropic.com \u2192 API keys)"," \xB7 a Claude Code login on this machine","Replay-only runs: add --no-agent to silence this note.",""].join(`
1033
+ `))}async function Vd(e){let t=qd.createInterface({input:process.stdin,output:process.stdout});try{let n=await new Promise(r=>t.question(e,r));return/^y(es)?$/i.test(n.trim())}finally{t.close()}}function Un(e){console.error(`saffron: ${e.message}`),process.exit(2)}function is(e){return e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e4?`${(e/1e3).toFixed(1)}k`:e.toLocaleString()}var Jd={green:"\x1B[32m",yellow:"\x1B[33m",red:"\x1B[31m"},Gd="\x1B[0m",q=new Hd().name("saffron").description("AI-fallback test runner for end-to-end tests written in plain language: zero-token cached replay, runtime AI healing.").version(Ne()).option("-p, --project-root <dir>","project root",process.cwd());q.command("run").description("Run feature files (cached replay; AI records/heals on demand)").argument("[paths...]","feature files or directories (default: features/)").option("--base-url <url>","base URL of the app under test").option("--env <name>","data environment: data/<file>.<name>.json is laid over data/<file>.json (also SAFFRON_ENV, or env in the config)").option("--headed","run the browser headed").option("--filter <tags>",'only run scenarios whose @tags match: a list ("@smoke,@WA-TC1" runs either) or an expression ("@e2e and not @wip", parentheses allowed); repeat the flag for "or"',Qa).option("--no-agent","disable AI fallback (cache misses/failures go red)").option("--rerecord","discard the committed caches of the selected scenarios and let the agent record them fresh (the honest fix for a wrong recording)").option("--model <model>","model for the agent").option("--storage-state <file>","Playwright storageState JSON for authenticated sessions").option("--strict","treat passed-with-adaptation (yellow) as failure until its proposal is reviewed").option("--no-verify","skip the zero-AI proof replay of agent recordings before filing proposals").option("--no-reuse","disable Level-1 step reuse (seeding new recordings from existing step recordings)").option("--browser <name>",'browser: "chromium" (default), "chrome", "msedge", "firefox" or "webkit". Recording and healing need one of the first three').option("--heal-model <model>","cheaper model for heal sessions").option("--workers <n>","parallel replay workers (agent recording/healing stays sequential)").option("--shard <index/total>","run one part of the suite, e.g. 2/4 on the second of four CI machines; combine the parts with `saffron report --merge`").option("--shard-by <mode>",'how --shard splits the suite: "scenario" (default, spreads every file) or "file" (each feature file stays on one machine)').option("--no-hooks","skip the setup and teardown commands from the config").option("--trace <mode>",'execution trace per scenario: "off" (default), "on" or "retain-on-failure"; open one with saffron trace').option("--assertion-policy <policy>",'assertion adaptability: "strict" (default) or "adaptable-mid" (mid-scenario assertions may be adapted; the final assertion block never is)').action(async(e,t)=>{let n=D.resolve(q.opts().projectRoot),r=ae(n);t.env&&tt(n,r.dataDir,t.env);let s=t.baseUrl??r.baseURL,o=si(n,e.length>0?e:r.features?[r.features]:[]);o.length===0&&(console.error("No .feature/.saffron files found."),process.exit(2)),t.agent!==!1&&Wd();let a;try{let $=$e(n,r,o);a=o.flatMap(j=>se(j,n,$))}catch($){throw $ instanceof Z&&Un($),$}let i;try{i=Ka(t.filter)}catch($){if(!($ instanceof qt))throw $;console.error(`${$.message}. Write tags joined with and, or, not and parentheses: "@smoke and not @wip".`),process.exit(2)}i&&(a=Ya(a,i),a.length===0&&(console.error(`No scenarios match --filter "${i.text}" in ${o.length} feature file(s).`),process.exit(2)));let c;if(t.shardBy!==void 0&&t.shard===void 0&&(console.error("saffron: --shard-by needs --shard, e.g. --shard 2/4 --shard-by file"),process.exit(2)),t.shard!==void 0){try{let $=Ha(String(t.shardBy??r.shardBy??"scenario")),j=Wa(a,{...qa(String(t.shard)),by:$});a=j.scenarios,c={...j.info,ciRun:Va()}}catch($){console.error($ instanceof Error?$.message:String($)),process.exit(2)}if(console.log(`shard ${c.index}/${c.total}${c.by==="file"?" (by file)":""}: ${a.length} of ${c.suiteSize} scenario(s)`),a.length===0){let $=new Date,j=Qr([],$,s),E=Tr([],$,ye(n).divergent.length,s);console.log(`report: ${Xr(n,{...j,shard:c,history:E})}`),process.exit(0)}}if(t.rerecord){t.agent===!1&&(console.error("saffron: --rerecord needs the agent; drop --no-agent"),process.exit(2));let $=0;for(let j of a){let E=J(n,j.featurePath,j.name);G.existsSync(E)&&(G.rmSync(E),$++)}console.log(`rerecord: removed ${$} cache file(s) for ${a.length} scenario(s); the agent records them fresh (seeded from the other recordings). Restore with git if needed.`)}{let $=a;try{let E=si(n,[]),F=$e(n,r,E);$=E.flatMap(z=>{try{return se(z,n,F)}catch{return[]}})}catch{}let j=bs(n,$);if(j.length>0){console.error("cache path collision:");for(let E of j)console.error(` ${E}`);process.exit(2)}}{let $=new Map;for(let E of a){let F=X(E),z=st(J(n,E.featurePath,E.name));z&&F.push(JSON.stringify(z.steps));for(let Q of Yn([...F,...Qn(X(E))]))process.env[Q]===void 0&&!$.has(Q)&&$.set(Q,E.displayName)}let j=new Map;for(let E of a)for(let F of He(X(E)))if(!j.has(F))try{Gt(F)}catch(z){j.set(F,`${z instanceof Error?z.message:String(z)} (used by "${E.displayName}")`)}if(j.size>0){for(let E of j.values())console.error(E);process.exit(2)}if($.size>0){for(let[E,F]of $)console.error(`missing environment variable ${E} (referenced as {env:${E}} by "${F}")`);console.error("set the variable(s), or add them to a git-ignored .env in the project root"),process.exit(2)}}let l=[],f=[];if(t.hooks!==!1)try{l=ts(r.setup,"setup"),f=ts(r.teardown,"teardown")}catch($){console.error($ instanceof Error?$.message:String($)),process.exit(2)}let d=new Rn({projectRoot:n,baseURL:s,headed:t.headed,actionTimeoutMs:r.actionTimeoutMs,pollIntervalMs:r.pollIntervalMs,screenshot:r.screenshot,trace:(()=>{let $=t.trace??r.trace??"off";return cr.includes($)||(console.error(`invalid --trace "${$}": use ${cr.map(j=>`"${j}"`).join(", ")}`),process.exit(2)),$})(),retries:r.retries,storageState:t.storageState??r.storageState,browser:(()=>{let $=t.browser??r.browser??"chromium";return na($)||(console.error(`invalid --browser "${$}": use ${Fr.map(j=>`"${j}"`).join(", ")}`),process.exit(2)),$})(),healModel:t.healModel??r.healModel,verifyProposals:t.verify===!1?!1:r.verifyProposals,reuseSteps:t.reuse===!1?!1:r.reuseSteps,assertionPolicy:(()=>{let $=t.assertionPolicy??r.assertionPolicy??"strict";return $!=="strict"&&$!=="adaptable-mid"&&(console.error(`invalid --assertion-policy "${$}": use "strict" or "adaptable-mid"`),process.exit(2)),$})(),agent:t.agent?new Cn({model:t.model??r.model,maxTurns:r.maxTurns,snapshotMode:r.snapshotMode}):void 0}),m=new Date,h=Math.max(1,Number.parseInt(String(t.workers??r.workers??1),10)||1),y=new Array(a.length),x=$=>{let j=$.scenario,E=Jd[$.status],F=$.usage.inputTokens+$.usage.outputTokens,z=$.usage.cacheReadTokens+$.usage.cacheCreationTokens,Q=z>0?`, ${is(z)} cache traffic`:"";console.log(`${E}${$.status.toUpperCase().padEnd(7)}${Gd}${j.displayName} (${$.source}, ${$.durationMs}ms, ${$.usage.aiCalls} AI calls, ${F} tokens${Q})`);for(let ge of $.adaptations)console.log(` \u26A0 ${ge}`);$.seededSteps&&console.log(` \u26A1 ${$.seededSteps}/${$.stepResults.length} steps seeded from existing recordings (zero AI)`);let K=$.stepResults.reduce((ge,Vt)=>ge+(Vt.retries??0),0);K&&console.log(` \u21BB ${K} action ${K===1?"retry":"retries"} absorbed before settling (retries: ${r.retries??1} per action)`),$.verified===!0?console.log(" \u2713 proposal verified by zero-AI proof replay"):$.verified===!1&&console.log(` \u2717 proposal UNVERIFIED, proof replay failed: ${$.proofError?.split(`
1034
+ `)[0]}`),$.error&&console.log(` ${$.error}`)},k=new AbortController,w={cwd:n,timeoutMs:r.hookTimeoutMs??Zn.hookTimeoutMs,env:{SAFFRON_BASE_URL:s,SAFFRON_ENV:Oe().env,SAFFRON_SHARD:c?`${c.index}/${c.total}`:void 0}},v=[],u,p=()=>u??=ns("teardown",f,w).then($=>{v.push(...$)}),b=Promise.resolve([]);l.length+f.length>0&&process.on("SIGINT",()=>{if(k.signal.aborted){console.error("still cleaning up: teardown finishes, then Saffron exits");return}console.error(`
1035
+ interrupted: stopping, then running teardown`),k.abort(),b.then(()=>d.stop()).catch(()=>{}).then(p).finally(()=>process.exit(130))}),l.length>0&&(b=ns("setup",l,{...w,signal:k.signal}),v.push(...await b),k.signal.aborted&&await new Promise(()=>{}),v.some(Ln)&&(console.error("No scenario was run: the application is not in the state the suite expects."),await p(),process.exit(2)));try{await d.start()}catch($){throw await p(),$ instanceof Dt&&(console.error($.message),process.exit(2)),$}try{if(h>1&&a.length>1){let $=a.map((E,F)=>({s:E,i:F})),j=[];await Promise.all(Array.from({length:Math.min(h,$.length)},async()=>{for(;;){let E=$.shift();if(!E)return;let F=await d.runScenario(E.s,{agentDisabled:!0});F.status==="red"?j.push(E):(y[E.i]=F,x(F))}}));for(let E of j.sort((F,z)=>F.i-z.i)){let F=await d.runScenario(E.s);y[E.i]=F,x(F)}}else for(let $=0;$<a.length;$++){let j=await d.runScenario(a[$]);y[$]=j,x(j)}}finally{await d.stop(),await p()}let g=Qr(y,m,s);v.length>0&&(g.hooks=v);let S=ye(n).divergent.length,T=Tr(y,m,S,s);if(!c){let $=Ft(n);g.trends=Er($,T),Rr(n,T)}g.totals.aiCalls>0&&(g.totals.plan=await d.agent?.planUsage?.());let A;if(c){let $=Xr(n,{...g,shard:c,history:T}),j=$.replace(/\.json$/,".html");G.writeFileSync(j,_n(g,n)),A=`${j}
1036
+ ${$}`}else{let{html:$,json:j}=Zr(n,g);A=`${$}
1037
+ ${j}`}let R=g.totals,_=R.cacheReadTokens+R.cacheCreationTokens;console.log(`
1038
+ ${R.green} passed, ${R.yellow} pending review, ${R.red} failed \xB7 ${R.aiCalls} AI calls \xB7 ${(R.inputTokens+R.outputTokens).toLocaleString()} tokens in+out${_>0?` \xB7 ${is(R.cacheReadTokens)} cache read \xB7 ${is(R.cacheCreationTokens)} cache written`:""}${Ur(R)}${R.models?.length?` \xB7 ${R.models.join(", ")}`:""}`);let I=g.trends;if(I?.prev){let $=I.prev.passRatePp,j=I.prev.costUsd;console.log(`vs previous run: pass rate ${$>=0?"+":""}${$}pp \xB7 cost ${j>=0?"+":"-"}$${Math.abs(j).toFixed(4)} \xB7 AI calls ${I.prev.aiCalls>=0?"+":""}${I.prev.aiCalls}`)}for(let $ of I?.chronic??[])console.log(`\u26A0 chronic: ${$.feature} \u203A ${$.scenario}: ${$.heals} heal(s), ${$.reds} red(s) in last ${$.runs} run(s); re-record or fix the feature text instead of paying for more heals`);console.log(`report: ${A}`),c&&console.log(`shard report, not a whole run: history and trends wait for \`saffron report --merge\` over all ${c.total} shard-*-of-${c.total}.json files`);let C=De(D.resolve(q.opts().projectRoot),Wt);C.length>0&&console.log(`${C.length} cache proposal(s) pending. Review with: saffron accept | saffron reject`);let M=t.strict??r.strict??!1;M&&R.yellow>0&&console.log(`strict mode: ${R.yellow} scenario(s) pending review treated as failure. Review proposals to go green.`);let O=R.red>0||M&&R.yellow>0,P=v.filter($=>$.phase==="teardown"&&Ln($));P.length>0&&console.log(`teardown failed: ${P.map($=>rs($,w.timeoutMs)).join("; ")}. The environment may hold data from this run.`),process.exit(O?1:P.length>0?2:0)});q.command("accept").description("Promote pending cache proposals to committed caches").argument("[files...]","proposal file(s) (default with --all: every proposal)").option("--all","accept all pending proposals").option("--with-feature-edit","also apply the proposal's suggested .feature step rewrites").option("--propagate","apply the same locator fixes to every other cache using the same locator").option("--include-unverified","with --all: also accept proposals whose proof replay failed").action((e,t)=>{let n=D.resolve(q.opts().projectRoot),r=ae(n),s=e.length>0?e.map(tr).map(o=>({...o,file:D.resolve(o.file)})):t.all?De(n,Wt).map(o=>({file:o.file,revision:void 0})):[];if(s.length===0){ci(n);return}try{for(let{file:o,revision:a}of s){let i=w=>{console.log(`not accepted: ${D.basename(o)} ${w}`),process.exitCode=1},c=ii(n,o,a!==void 0,()=>{if(a&&At(o)!==a)return i(ai);let w;try{w=pt(o)}catch(p){if(!(p instanceof V))throw p;return i(`cannot be read: ${p.problem}`)}if(w.meta.bound){let p=Ee(n,w,$e(n,r,[]));if(p)return i(`is stale: ${p}. Run the scenario again for a proposal made against what is there now, or reject this one.`)}else console.log(`\u26A0 ${D.basename(o)} is ${vn}; accepting as it is.`);if(w.meta.verified===!1){if(t.all&&!t.includeUnverified){console.log(`skipped UNVERIFIED ${D.basename(o)} (${w.meta.proofError?.split(`
1039
1039
  `)[0]}). Re-record it (saffron run --rerecord), or accept it explicitly by file, or pass --include-unverified.`);return}console.log(`\u26A0 accepting UNVERIFIED proposal (proof replay failed: ${w.meta.proofError?.split(`
1040
- `)[0]}): expect a heal or red on the next run.`)}let v=w.cache.steps.map(u=>u.gherkin);if(t.withFeatureEdit&&(w.meta.featureEdits??[]).length>0){let u=Fa(n,w.cache.feature,w.cache.scenario,v,ke(n,r,[]));if(u)return i(`carries feature edits, and ${u}. Run the scenario again for a fresh proposal, or accept this one without --with-feature-edit.`)}let l=oe(J(n,w.cache.feature,w.cache.scenario));return{proposal:w,recordedSteps:v,oldCache:l,cacheFile:gn(n,s)}},i);if(!c)continue;let{proposal:p,recordedSteps:f,oldCache:d,cacheFile:m}=c;console.log(`accepted \u2192 ${m}`);let h=d?_a(d,p.cache):{changes:[],ambiguous:[]},y=h.changes;for(let w of h.ambiguous)console.log(` locator fix for ${_n(w)} not propagated: this heal sent it to more than one place, so other caches are left alone.`);if(y.length>0){let w=ja(n,y,m,!t.propagate);for(let v of y)console.log(` locator fix: ${_n(v.from)} \u2192 ${_n(v.to)}`);if(w.length===0)console.log(" no other caches use this locator.");else if(t.propagate)for(let v of w)console.log(` propagated \u2192 ${v.feature} \u203A ${v.scenario} (${v.steps.map(l=>`"${l.gherkin}"`).join(", ")})`);else{console.log(` ${w.length} other cache(s) use the same locator: re-run with --propagate to fix them too:`);for(let v of w)console.log(` \xB7 ${v.feature} \u203A ${v.scenario}`)}}let b=p.meta.featureEdits??[];if(!t.withFeatureEdit||b.length===0)continue;let k=Na(n,p.cache.feature,p.cache.scenario,b,ke(n,r,[]),f);for(let w of k.applied){let v=w.fromStepSet?`${w.file}:${w.line} (StepSet "${w.fromStepSet}", fixes every invoking scenario)`:`${w.file}:${w.line}`;console.log(` feature edit ${v}:`),console.log(` - ${w.from}`),console.log(` + ${w.to}`)}for(let w of k.skipped)console.log(` skipped edit [${w.index}]: ${w.reason}`);if(k.appliedIndices.length>0){let w=new Set(k.appliedIndices),v=oe(m);for(let l of b)w.has(l.index)&&v.steps[l.index]&&(v.steps[l.index].gherkin=l.text);ot(m,v)}}}catch(s){throw s instanceof Z&&Mn(s),s}});q.command("steps").description("List/search the project's step vocabulary (files + caches): what exists, what's recorded, what to reuse").argument("[search]","case-insensitive substring filter").option("--json","machine-readable vocabulary").option("--snippets","write .vscode/saffron.code-snippets for native VS Code completion").action((e,t)=>{let n=M.resolve(q.opts().projectRoot),r=ae(n),o;try{o=be(n,r.features??"features")}catch(a){throw a instanceof Z&&Mn(a),a}if(e&&(o=nn(o,e)),t.json){console.log(JSON.stringify(o,null,2));return}if(t.snippets){let a=M.join(n,".vscode","saffron.code-snippets");G.mkdirSync(M.dirname(a),{recursive:!0}),G.writeFileSync(a,xs(o)),console.log(`wrote ${o.steps.length} step + ${o.stepSets.length} StepSet snippet(s) \u2192 ${a}`),console.log("re-run after recording sessions to keep completions current");return}let s={recorded:"\x1B[32m\u25CF\x1B[0m",divergent:"\x1B[33m\u25CF\x1B[0m",unrecorded:"\u25CB"};if(o.stepSets.length>0){console.log("step sets:");for(let a of o.stepSets)console.log(` ${a.name} (${a.steps.length} steps, ${a.usage} scenario(s)), ${a.file}:${a.line}`);console.log("")}for(let a of o.steps){let i=a.fromStepSet?` [${a.fromStepSet}]`:"";console.log(`${s[a.status]} ${a.keyword} ${a.text} \xD7${a.usage}${i}`)}console.log(`
1041
- ${o.steps.length} step(s) across ${o.scannedFiles} file(s) \xB7 ${o.scannedCaches} cache(s) \xB7 \x1B[32m\u25CF\x1B[0m recorded (replays free) \xB7 \x1B[33m\u25CF\x1B[0m divergent \xB7 \u25CB unrecorded`)});q.command("author").description("Draft a feature file from plain-paragraph requirements, reusing the project's step vocabulary (AI)").argument("<prose-file>","text/markdown file with the requirements").option("-o, --out <file>","output path (default: features/<name>.saffron)").option("--model <model>","model for authoring").action(async(e,t)=>{let n=M.resolve(q.opts().projectRoot),r=ae(n),o=G.readFileSync(M.resolve(e),"utf8"),s;try{s=be(n,r.features??"features")}catch(p){throw p instanceof Z&&Mn(p),p}console.log(`authoring with ${s.steps.length} known step(s) and ${s.stepSets.length} step set(s) as vocabulary\u2026`);let a=await xo(o,s,t.model??r.model),i=t.out?M.resolve(t.out):M.join(n,r.features??"features",`${M.basename(e).replace(/\.[^.]+$/,"")}.saffron`);G.existsSync(i)&&(console.error(`refusing to overwrite ${i}: pass --out for a new path`),process.exit(1)),G.mkdirSync(M.dirname(i),{recursive:!0}),G.writeFileSync(i,a.content);let c=a.content.split(`
1042
- `).map(p=>p.trim().replace(/^(Given|When|Then|And|But|\*)\s+/,"")).filter(p=>s.steps.some(f=>f.text===p)).length;console.log(`wrote ${i}`),console.log(`${c} step line(s) reuse the existing vocabulary (seedable) \xB7 $${(a.usage.costUsd??0).toFixed(4)}`),console.log("review the draft, then record it: saffron run "+M.relative(process.cwd(),i))});q.command("mcp").description("Serve the step vocabulary to AI assistants over stdio MCP (search_steps, list_step_sets)").action(async()=>{let e=M.resolve(q.opts().projectRoot),t=ae(e);await wo(e,t.features??"features")});q.command("lsp").description("Run the Saffron language server over stdio (completion, go-to-definition, hover, diagnostics). For JetBrains (LSP4IJ / Ultimate LSP API), Neovim, and any LSP-capable editor").action(()=>{process.stdin.isTTY&&(console.error(Za(cn())),process.exit(2));let e=M.resolve(q.opts().projectRoot),t=ae(e),n=process.argv.includes("-p")||process.argv.some(r=>r.startsWith("--project-root"));Ao(e,t.features??"features",{rootFromClient:!n})});q.command("reject").description("Discard pending cache proposals (AI will retry next run)").argument("[files...]","proposal file(s)").option("--all","reject all pending proposals").action((e,t)=>{let n=M.resolve(q.opts().projectRoot),r=e.length>0?e.map(Zn).map(o=>({...o,file:M.resolve(o.file)})):t.all?Me(n,Ht).map(o=>({file:o.file,revision:void 0})):[];if(r.length===0){ri(n);return}for(let{file:o,revision:s}of r){let a=ni(n,o,s!==void 0,()=>{if(s&&At(o)!==s)return ti;hn(o)},i=>i);if(a){console.log(`not rejected: ${M.basename(o)} ${a}`),process.exitCode=1;continue}console.log(`rejected ${o}`)}});q.command("init").description("Make the project agent-ready: install the bundled Saffron skill into agent directories, add instructions to AGENTS.md, scaffold config").option("--agents <list>",`comma-separated targets: ${Object.keys(qt).join(", ")} (default: claude,agents)`).option("--no-instructions","do not touch AGENTS.md / CLAUDE.md").option("--no-scaffold","do not create saffron.config.json, features/, .gitignore entry").option("--no-mcp","do not register `saffron mcp` in project MCP configs (.mcp.json, .cursor/mcp.json, .vscode/mcp.json)").option("--examples","install the Saucedemo example suite (features, config, .env.example) for a guided first run").option("--no-examples","never install the example suite (skips the prompt)").action(async e=>{let t=M.resolve(q.opts().projectRoot),n=cn();n||(console.error("saffron: cannot locate the installed package root"),process.exit(2));let r=e.agents?String(e.agents).split(",").map(a=>a.trim()).filter(Boolean):void 0;for(let a of r??[])a in qt||(console.error(`saffron: unknown agent target "${a}" (expected one of ${Object.keys(qt).join(", ")})`),process.exit(2));let o=e.examples===!0;e.examples===void 0&&process.stdin.isTTY&&process.stdout.isTTY&&(o=await Dd("Install the Saucedemo example tests (login, cart, checkout) into features/ for a guided first run? [y/N] "));let s=Qa({projectRoot:t,packageRoot:n,targets:r,instructions:e.instructions,scaffold:e.scaffold,mcp:e.mcp,examples:o});for(let a of s.skillDirs)console.log(`skill \u2192 ${a}/`);for(let a of s.mcpFiles)console.log(`${a.outcome.padEnd(9)} \u2192 ${a.file} (saffron mcp server)`);for(let a of s.mcpHints)console.log(`mcp hint \u2192 ${a}`);for(let a of s.instructionFiles)console.log(`${a.outcome.padEnd(9)} \u2192 ${a.file} (managed Saffron block)`);for(let a of s.scaffolded)console.log(`created \u2192 ${a}`);s.examples&&!s.examples.installed&&console.log(`examples \u2192 not installed: ${s.examples.reason}`),console.log("\nAgents now load the saffron skill when writing tests. Re-run after `npm update saffron-ai` to refresh it."),s.examples?.installed&&console.log(["","Your guided first run (recording needs Claude credentials; replays never do):"," cp .env.example .env # public demo credentials, resolved at replay"," npx playwright install chromium"," npx saffron run --filter @smoke # the agent records two scenarios (about $3 of AI usage), files proposals"," npx saffron accept --all # review, then promote them to caches"," npx saffron run --filter @smoke # replays at zero tokens","Then try the rest: npx saffron run (login errors outline, checkout with a data table)"].join(`
1043
- `))});q.command("diff").description("Show what a pending proposal changes in the committed recording: the action list a reviewer is approving, not just the narrative").argument("[files...]","proposal file(s); default: every pending proposal").option("--json","machine-readable output").action((e,t)=>{let n=M.resolve(q.opts().projectRoot),r=e.length>0?e.map(s=>M.resolve(s)):Me(n,Ht).map(s=>s.file);if(r.length===0){console.log("No pending proposals.");return}let o=r.map(s=>{let a=pt(s),i=oe(J(n,a.cache.feature,a.cache.scenario));return{file:M.relative(n,s),verified:a.meta.verified,stale:a.meta.bound?ei(n,a):wn,...gt(i,a.cache)}});if(t.json){console.log(JSON.stringify(o.length===1?o[0]:o,null,2));return}console.log(o.map(s=>`${no(s)}${s.stale?`
1040
+ `)[0]}): expect a heal or red on the next run.`)}let v=w.cache.steps.map(p=>p.gherkin);if(t.withFeatureEdit&&(w.meta.featureEdits??[]).length>0){let p=Ua(n,w.cache.feature,w.cache.scenario,v,$e(n,r,[]));if(p)return i(`carries feature edits, and ${p}. Run the scenario again for a fresh proposal, or accept this one without --with-feature-edit.`)}let u=oe(J(n,w.cache.feature,w.cache.scenario));return{proposal:w,recordedSteps:v,oldCache:u,cacheFile:hn(n,o)}},i);if(!c)continue;let{proposal:l,recordedSteps:f,oldCache:d,cacheFile:m}=c;console.log(`accepted \u2192 ${m}`);let h=d?Ma(d,l.cache):{changes:[],ambiguous:[]},y=h.changes;for(let w of h.ambiguous)console.log(` locator fix for ${Fn(w)} not propagated: this heal sent it to more than one place, so other caches are left alone.`);if(y.length>0){let w=Da(n,y,m,!t.propagate);for(let v of y)console.log(` locator fix: ${Fn(v.from)} \u2192 ${Fn(v.to)}`);if(w.length===0)console.log(" no other caches use this locator.");else if(t.propagate)for(let v of w)console.log(` propagated \u2192 ${v.feature} \u203A ${v.scenario} (${v.steps.map(u=>`"${u.gherkin}"`).join(", ")})`);else{console.log(` ${w.length} other cache(s) use the same locator: re-run with --propagate to fix them too:`);for(let v of w)console.log(` \xB7 ${v.feature} \u203A ${v.scenario}`)}}let x=l.meta.featureEdits??[];if(!t.withFeatureEdit||x.length===0)continue;let k=Ba(n,l.cache.feature,l.cache.scenario,x,$e(n,r,[]),f);for(let w of k.applied){let v=w.fromStepSet?`${w.file}:${w.line} (StepSet "${w.fromStepSet}", fixes every invoking scenario)`:`${w.file}:${w.line}`;console.log(` feature edit ${v}:`),console.log(` - ${w.from}`),console.log(` + ${w.to}`)}for(let w of k.skipped)console.log(` skipped edit [${w.index}]: ${w.reason}`);if(k.appliedIndices.length>0){let w=new Set(k.appliedIndices),v=oe(m);for(let u of x)w.has(u.index)&&v.steps[u.index]&&(v.steps[u.index].gherkin=u.text);ot(m,v)}}}catch(o){throw o instanceof Z&&Un(o),o}});q.command("steps").description("List/search the project's step vocabulary (files + caches): what exists, what's recorded, what to reuse").argument("[search]","case-insensitive substring filter").option("--json","machine-readable vocabulary").option("--snippets","write .vscode/saffron.code-snippets for native VS Code completion").action((e,t)=>{let n=D.resolve(q.opts().projectRoot),r=ae(n),s;try{s=be(n,r.features??"features")}catch(a){throw a instanceof Z&&Un(a),a}if(e&&(s=rn(s,e)),t.json){console.log(JSON.stringify(s,null,2));return}if(t.snippets){let a=D.join(n,".vscode","saffron.code-snippets");G.mkdirSync(D.dirname(a),{recursive:!0}),G.writeFileSync(a,Ts(s)),console.log(`wrote ${s.steps.length} step + ${s.stepSets.length} StepSet snippet(s) \u2192 ${a}`),console.log("re-run after recording sessions to keep completions current");return}let o={recorded:"\x1B[32m\u25CF\x1B[0m",divergent:"\x1B[33m\u25CF\x1B[0m",unrecorded:"\u25CB"};if(s.stepSets.length>0){console.log("step sets:");for(let a of s.stepSets)console.log(` ${a.name} (${a.steps.length} steps, ${a.usage} scenario(s)), ${a.file}:${a.line}`);console.log("")}for(let a of s.steps){let i=a.fromStepSet?` [${a.fromStepSet}]`:"";console.log(`${o[a.status]} ${a.keyword} ${a.text} \xD7${a.usage}${i}`)}console.log(`
1041
+ ${s.steps.length} step(s) across ${s.scannedFiles} file(s) \xB7 ${s.scannedCaches} cache(s) \xB7 \x1B[32m\u25CF\x1B[0m recorded (replays free) \xB7 \x1B[33m\u25CF\x1B[0m divergent \xB7 \u25CB unrecorded`)});q.command("author").description("Draft a feature file from plain-paragraph requirements, reusing the project's step vocabulary (AI)").argument("<prose-file>","text/markdown file with the requirements").option("-o, --out <file>","output path (default: features/<name>.saffron)").option("--model <model>","model for authoring").action(async(e,t)=>{let n=D.resolve(q.opts().projectRoot),r=ae(n),s=G.readFileSync(D.resolve(e),"utf8"),o;try{o=be(n,r.features??"features")}catch(l){throw l instanceof Z&&Un(l),l}console.log(`authoring with ${o.steps.length} known step(s) and ${o.stepSets.length} step set(s) as vocabulary\u2026`);let a=await Ro(s,o,t.model??r.model),i=t.out?D.resolve(t.out):D.join(n,r.features??"features",`${D.basename(e).replace(/\.[^.]+$/,"")}.saffron`);G.existsSync(i)&&(console.error(`refusing to overwrite ${i}: pass --out for a new path`),process.exit(1)),G.mkdirSync(D.dirname(i),{recursive:!0}),G.writeFileSync(i,a.content);let c=a.content.split(`
1042
+ `).map(l=>l.trim().replace(/^(Given|When|Then|And|But|\*)\s+/,"")).filter(l=>o.steps.some(f=>f.text===l)).length;console.log(`wrote ${i}`),console.log(`${c} step line(s) reuse the existing vocabulary (seedable) \xB7 $${(a.usage.costUsd??0).toFixed(4)}`),console.log("review the draft, then record it: saffron run "+D.relative(process.cwd(),i))});q.command("mcp").description("Serve the step vocabulary to AI assistants over stdio MCP (search_steps, list_step_sets)").action(async()=>{let e=D.resolve(q.opts().projectRoot),t=ae(e);await To(e,t.features??"features")});q.command("lsp").description("Run the Saffron language server over stdio (completion, go-to-definition, hover, diagnostics). For JetBrains (LSP4IJ / Ultimate LSP API), Neovim, and any LSP-capable editor").action(()=>{process.stdin.isTTY&&(console.error(ri(ln())),process.exit(2));let e=D.resolve(q.opts().projectRoot),t=ae(e),n=process.argv.includes("-p")||process.argv.some(r=>r.startsWith("--project-root"));Oo(e,t.features??"features",{rootFromClient:!n})});q.command("reject").description("Discard pending cache proposals (AI will retry next run)").argument("[files...]","proposal file(s)").option("--all","reject all pending proposals").action((e,t)=>{let n=D.resolve(q.opts().projectRoot),r=e.length>0?e.map(tr).map(s=>({...s,file:D.resolve(s.file)})):t.all?De(n,Wt).map(s=>({file:s.file,revision:void 0})):[];if(r.length===0){ci(n);return}for(let{file:s,revision:o}of r){let a=ii(n,s,o!==void 0,()=>{if(o&&At(s)!==o)return ai;mn(s)},i=>i);if(a){console.log(`not rejected: ${D.basename(s)} ${a}`),process.exitCode=1;continue}console.log(`rejected ${s}`)}});q.command("init").description("Make the project agent-ready: install the bundled Saffron skill into agent directories, add instructions to AGENTS.md, scaffold config").option("--agents <list>",`comma-separated targets: ${Object.keys(Ht).join(", ")} (default: claude,agents)`).option("--no-instructions","do not touch AGENTS.md / CLAUDE.md").option("--no-scaffold","do not create saffron.config.json, features/, .gitignore entry").option("--no-mcp","do not register `saffron mcp` in project MCP configs (.mcp.json, .cursor/mcp.json, .vscode/mcp.json)").option("--examples","install the Saucedemo example suite (features, config, .env.example) for a guided first run").option("--no-examples","never install the example suite (skips the prompt)").action(async e=>{let t=D.resolve(q.opts().projectRoot),n=ln();n||(console.error("saffron: cannot locate the installed package root"),process.exit(2));let r=e.agents?String(e.agents).split(",").map(a=>a.trim()).filter(Boolean):void 0;for(let a of r??[])a in Ht||(console.error(`saffron: unknown agent target "${a}" (expected one of ${Object.keys(Ht).join(", ")})`),process.exit(2));let s=e.examples===!0;e.examples===void 0&&process.stdin.isTTY&&process.stdout.isTTY&&(s=await Vd("Install the Saucedemo example tests (login, cart, checkout) into features/ for a guided first run? [y/N] "));let o=ni({projectRoot:t,packageRoot:n,targets:r,instructions:e.instructions,scaffold:e.scaffold,mcp:e.mcp,examples:s});for(let a of o.skillDirs)console.log(`skill \u2192 ${a}/`);for(let a of o.mcpFiles)console.log(`${a.outcome.padEnd(9)} \u2192 ${a.file} (saffron mcp server)`);for(let a of o.mcpHints)console.log(`mcp hint \u2192 ${a}`);for(let a of o.instructionFiles)console.log(`${a.outcome.padEnd(9)} \u2192 ${a.file} (managed Saffron block)`);for(let a of o.scaffolded)console.log(`created \u2192 ${a}`);o.examples&&!o.examples.installed&&console.log(`examples \u2192 not installed: ${o.examples.reason}`),console.log("\nAgents now load the saffron skill when writing tests. Re-run after `npm update saffron-ai` to refresh it."),o.examples?.installed&&console.log(["","Your guided first run (recording needs Claude credentials; replays never do):"," cp .env.example .env # public demo credentials, resolved at replay"," npx playwright install chromium"," npx saffron run --filter @smoke # the agent records two scenarios (about $3 of AI usage), files proposals"," npx saffron accept --all # review, then promote them to caches"," npx saffron run --filter @smoke # replays at zero tokens","Then try the rest: npx saffron run (login errors outline, checkout with a data table)"].join(`
1043
+ `))});q.command("diff").description("Show what a pending proposal changes in the committed recording: the action list a reviewer is approving, not just the narrative").argument("[files...]","proposal file(s); default: every pending proposal").option("--json","machine-readable output").action((e,t)=>{let n=D.resolve(q.opts().projectRoot),r=e.length>0?e.map(o=>D.resolve(o)):De(n,Wt).map(o=>o.file);if(r.length===0){console.log("No pending proposals.");return}let s=r.map(o=>{let a=pt(o),i=oe(J(n,a.cache.feature,a.cache.scenario));return{file:D.relative(n,o),verified:a.meta.verified,stale:a.meta.bound?oi(n,a):vn,...gt(i,a.cache)}});if(t.json){console.log(JSON.stringify(s.length===1?s[0]:s,null,2));return}console.log(s.map(o=>`${io(o)}${o.stale?`
1044
1044
 
1045
- STALE: ${s.stale}`:""}`).join(`
1045
+ STALE: ${o.stale}`:""}`).join(`
1046
1046
 
1047
- `))});q.command("trace").description("Open the execution replay of a scenario from the last run: its Given / When / Then, the page at every action, the network alongside, and what the agent changed").argument("[scenario]","scenario name, feature path, or feature:scenario; default: every traced scenario, opening on a red one, else a yellow one").option("--port <n>","listen on a fixed port (default: a free one)").option("--no-open","print the URL without opening a browser").option("--json","print the parsed trace as JSON instead of serving it").action(async(e,t)=>{let n=t.port===void 0?void 0:Number(t.port);n!==void 0&&(!Number.isInteger(n)||n<0||n>65535)&&(console.error(`invalid --port "${t.port}": use a whole number from 0 to 65535`),process.exit(2));let r=M.resolve(q.opts().projectRoot),o=M.join(r,".saffron","reports","latest.json");G.existsSync(o)||(console.error("No report found. Run `saffron run` first."),process.exit(2));let s=JSON.parse(G.readFileSync(o,"utf8")),a=s.scenarios.filter(l=>l.evidence?.some(u=>u.kind==="trace"));a.length===0&&(console.error('The last run kept no execution trace. Set "trace": "retain-on-failure" (or "on") in saffron.config.json, or run with --trace retain-on-failure, then run again.'),process.exit(2));let i=e?.trim(),c=a;if(i){let l=a.filter(u=>u.scenario===i||u.feature===i||`${u.feature}:${u.scenario}`===i);c=l.length>0?l:a.filter(u=>u.scenario.toLowerCase().includes(i.toLowerCase()))}if(c.length===0||t.json&&i&&c.length>1){console.error(c.length===0?`No traced scenario matches "${i}". Traced in the last run:`:"--json prints one trace. Traced in the last run:");for(let l of a)console.error(` ${l.status.padEnd(6)} ${l.feature}:${l.scenario}`);console.error('Name one: saffron trace "<scenario name>", or "<feature>:<scenario name>" as listed'),process.exit(2)}let p=c.findIndex(l=>l.status==="red"),f=c.findIndex(l=>l.status==="yellow"),d=p>=0?p:f>=0?f:0,m=c[d],h=l=>M.basename(l,".zip").replace("proof-","Proof replay "),y=l=>{let u=l.evidence.filter(g=>g.kind==="trace"),x=u.find(g=>!g.proof)??u[0];return[x,...u.filter(g=>g!==x)]},[b]=y(m),k;if(c.length===1||t.json){let l=M.join(r,b.file);try{k=qr(r,b)}catch(u){console.error(`The trace cannot be read: ${u instanceof Error?u.message:String(u)}`),process.exit(2)}if(k||(console.error(G.existsSync(l)?"The trace on disk is from a later run than the last report (a run that stopped before writing its report). Run the scenario again to replay it.":`The trace file is gone: ${l}. Run the scenario again.`),process.exit(2)),t.json){console.log(JSON.stringify(ma(k),null,2));return}}let w=c.map((l,u)=>{let[x,...g]=y(l);return{scenario:{feature:l.feature,scenario:l.scenario,status:l.status,error:l.error,adaptations:l.adaptations,narrative:l.narrative,proposalDiff:l.proposalDiff,proposalFile:l.proposalFile,proposalRevision:l.proposalRevision,screenshots:l.evidence?.filter(S=>S.kind!=="trace"),ranAt:s.startedAt,source:["cache","agent-record","agent-heal"].find(S=>S===l.source),proposalReplay:l.proposalReplay===!0?!0:void 0},traces:[x,...g].map((S,T)=>({file:M.join(r,S.file),data:u===d&&T===0?k:void 0,read:()=>qr(r,S),checked:S.sha256!==void 0,label:S.proof?h(S.file):"Execution",proof:S.proof===!0,steps:S.proof?S.traceSteps:S.traceSteps??l.steps,uniques:S.proof?S.traceUniques:l.uniques}))}}),v;try{v=await Pa({entries:w,start:d,projectRoot:r,port:n})}catch(l){console.error(`Could not start the replay: ${l instanceof Error?l.message:String(l)}`),process.exit(2)}for(let{file:l,reason:u}of v.skipped)console.error(`Skipping ${M.relative(r,l)}. ${u}`);console.log(`Saffron replay: ${c.length===1?`${m.feature} \u203A ${m.scenario}`:`${c.length} scenarios, opening on ${m.scenario}`}
1047
+ `))});q.command("trace").description("Open the execution replay of a scenario from the last run: its Given / When / Then, the page at every action, the network alongside, and what the agent changed").argument("[scenario]","scenario name, feature path, or feature:scenario; default: every traced scenario, opening on a red one, else a yellow one").option("--port <n>","listen on a fixed port (default: a free one)").option("--no-open","print the URL without opening a browser").option("--json","print the parsed trace as JSON instead of serving it").option("--run <startedAt>","only if the last run is this one (its startedAt): an editor's row names the run it lists").action(async(e,t)=>{let n=t.port===void 0?void 0:Number(t.port);n!==void 0&&(!Number.isInteger(n)||n<0||n>65535)&&(console.error(`invalid --port "${t.port}": use a whole number from 0 to 65535`),process.exit(2));let r=D.resolve(q.opts().projectRoot),s=D.join(r,".saffron","reports","latest.json");G.existsSync(s)||(console.error("No report found. Run `saffron run` first."),process.exit(2));let o=JSON.parse(G.readFileSync(s,"utf8"));t.run!==void 0&&o.startedAt!==t.run&&(console.error(`The last run changed since this was listed: it is now the run of ${o.startedAt}. Refresh the panel to replay it.`),process.exit(2));let a=o.scenarios.filter(u=>u.evidence?.some(p=>p.kind==="trace"));a.length===0&&(console.error('The last run kept no execution trace. Set "trace": "retain-on-failure" (or "on") in saffron.config.json, or run with --trace retain-on-failure, then run again.'),process.exit(2));let i=e?.trim(),c=a;if(i){let u=a.filter(p=>p.scenario===i||p.feature===i||`${p.feature}:${p.scenario}`===i);c=u.length>0?u:a.filter(p=>p.scenario.toLowerCase().includes(i.toLowerCase()))}if(c.length===0||t.json&&i&&c.length>1){console.error(c.length===0?`No traced scenario matches "${i}". Traced in the last run:`:"--json prints one trace. Traced in the last run:");for(let u of a)console.error(` ${u.status.padEnd(6)} ${u.feature}:${u.scenario}`);console.error('Name one: saffron trace "<scenario name>", or "<feature>:<scenario name>" as listed'),process.exit(2)}let l=c.findIndex(u=>u.status==="red"),f=c.findIndex(u=>u.status==="yellow"),d=l>=0?l:f>=0?f:0,m=c[d],h=u=>D.basename(u,".zip").replace("proof-","Proof replay "),y=u=>{let p=u.evidence.filter(g=>g.kind==="trace"),b=p.find(g=>!g.proof)??p[0];return[b,...p.filter(g=>g!==b)]},[x]=y(m),k;if(c.length===1||t.json){let u=D.join(r,x.file);try{k=Jr(r,x)}catch(p){console.error(`The trace cannot be read: ${p instanceof Error?p.message:String(p)}`),process.exit(2)}if(k||(console.error(G.existsSync(u)?"The trace on disk is from a later run than the last report (a run that stopped before writing its report). Run the scenario again to replay it.":`The trace file is gone: ${u}. Run the scenario again.`),process.exit(2)),t.json){console.log(JSON.stringify(Sa(k),null,2));return}}let w=c.map((u,p)=>{let[b,...g]=y(u);return{scenario:{feature:u.feature,scenario:u.scenario,status:u.status,error:u.error,adaptations:u.adaptations,narrative:u.narrative,proposalDiff:u.proposalDiff,proposalFile:u.proposalFile,proposalRevision:u.proposalRevision,screenshots:u.evidence?.filter(S=>S.kind!=="trace"),ranAt:o.startedAt,source:["cache","agent-record","agent-heal"].find(S=>S===u.source),proposalReplay:u.proposalReplay===!0?!0:void 0},traces:[b,...g].map((S,T)=>({file:D.join(r,S.file),data:p===d&&T===0?k:void 0,read:()=>Jr(r,S),checked:S.sha256!==void 0,label:S.proof?h(S.file):"Execution",proof:S.proof===!0,steps:S.proof?S.traceSteps:S.traceSteps??u.steps,uniques:S.proof?S.traceUniques:u.uniques}))}}),v;try{v=await Fa({entries:w,start:d,projectRoot:r,port:n})}catch(u){console.error(`Could not start the replay: ${u instanceof Error?u.message:String(u)}`),process.exit(2)}for(let{file:u,reason:p}of v.skipped)console.error(`Skipping ${D.relative(r,u)}. ${p}`);console.log(`Saffron replay: ${c.length===1?`${m.feature} \u203A ${m.scenario}`:`${c.length} scenarios, opening on ${m.scenario}`}
1048
1048
  ${v.url}
1049
- Press Ctrl-C to stop.`),t.open!==!1&&ts(v.url),await new Promise(l=>{process.once("SIGINT",()=>{v.close().finally(l)})}),process.exit(0)});q.command("prune").description("List (and with --yes remove) recordings whose scenario no longer exists: caches and pending proposals left behind by a deleted scenario or feature file").option("--json","machine-readable output").option("--yes","actually delete the files listed").option("--check","exit 1 when anything is orphaned, delete nothing (for CI)").action(e=>{let t=M.resolve(q.opts().projectRoot),n=ae(t),r=yn(t,n.features??"features");r.unreadable.length>0&&(e.json?console.log(JSON.stringify({...r,removed:[]},null,2)):console.error(["saffron: cannot tell what is orphaned while a feature file fails to parse.",...r.unreadable.map(s=>` ${s}`),"Fix these first: a syntax error hides every scenario in the file."].join(`
1050
- `)),process.exit(2));let o=e.yes&&!e.check?js(t,r.orphans):[];if(e.json)console.log(JSON.stringify({...r,removed:o},null,2));else if(r.orphans.length===0)console.log(`Nothing to prune. All ${r.kept} recordings belong to a scenario.`);else{let s=r.orphans.filter(c=>c.kind==="cache").length,a=r.orphans.filter(c=>c.kind==="proposal").length,i=r.orphans.length-s-a;console.log([`${r.orphans.length} orphaned ${r.orphans.length===1?"file":"files"} (${s} cache, ${a} proposal, ${i} screenshot ${i===1?"folder":"folders"}), ${r.kept} still owned:`,"",...r.orphans.map(c=>` ${o.length>0?"removed ":""}${Os(c)}`),"",o.length>0?"Deleted. Re-record by running the scenarios again if you deleted something by mistake.":"Nothing was deleted. Run `saffron prune --yes` to remove them."].join(`
1051
- `))}e.check&&r.orphans.length>0&&process.exit(1)});q.command("status").description("Project overview for dashboards and IDE panels: feature files and scenarios with cache state, tags, pending proposals, last run, history, vocabulary health, effective config").option("--json","machine-readable output").action(e=>{let t=M.resolve(q.opts().projectRoot),n=ae(t),r=xn(t,n);if(e.json){console.log(JSON.stringify(r,null,2));return}bo(r)});q.command("report").description("Open the latest HTML report, or merge shard reports into it").argument("[paths...]","with --merge: shard report files or folders to search (default: .saffron)").option("--open","open in the default browser",!0).option("--merge","combine the shard-<i>-of-<n>.json reports of one sharded run into latest.json, latest.html and one history line").action((e,t)=>{let n=M.resolve(q.opts().projectRoot);if(t.merge){Bd(n,e);return}e.length>0&&(console.error("saffron report: paths are only read with --merge"),process.exit(2));let r=M.join(n,".saffron","reports","latest.html");G.existsSync(r)||(console.error("No report found. Run `saffron run` first."),process.exit(2)),console.log(r),ts(r)});function Bd(e,t){let n=t.length>0?t.map(b=>M.resolve(b)):[M.join(e,".saffron")],r,o;try{let b=za(n);o=b.length,r=qa(b)}catch(b){console.error(`saffron report --merge: ${b instanceof Error?b.message:String(b)}`),process.exit(2)}let{report:s,history:a}=r,i=Ft(e),c=i.find(b=>b.runId===a.runId),p=c?i.filter(b=>b.runId!==a.runId):i;s.trends=Tr(p,a);let f=c!==void 0&&JSON.stringify(c)===JSON.stringify(a);f||ho(e,a);let{html:d,json:m}=Gr(e,s),h=s.totals;console.log(`merged ${o} shard(s): ${h.scenarios} scenario(s) \xB7 ${h.green} passed, ${h.yellow} pending review, ${h.red} failed \xB7 ${h.aiCalls} AI calls${Nr(h)}`),f?console.log("history already holds this run: not appended again"):c&&console.log("history already held this run: its line is replaced with these results");let y=s.trends.prev;y&&console.log(`vs previous run: pass rate ${y.passRatePp>=0?"+":""}${y.passRatePp}pp \xB7 cost ${y.costUsd>=0?"+":"-"}$${Math.abs(y.costUsd).toFixed(4)} \xB7 AI calls ${y.aiCalls>=0?"+":""}${y.aiCalls}`);for(let b of s.trends.chronic)console.log(`\u26A0 chronic: ${b.feature} \u203A ${b.scenario}: ${b.heals} heal(s), ${b.reds} red(s) in last ${b.runs} run(s)`);console.log(`report: ${d}
1052
- ${m}`)}function ei(e,t){try{return Re(e,t,ke(e,ae(e),[]))}catch(n){return`cannot check: ${n instanceof Error?n.message.split(`
1053
- `)[0]:String(n)}`}}var Ht=(e,t)=>console.error(`\u26A0 proposal ${e} cannot be read: ${t}`),zd="is no longer pending: it was accepted, rejected or removed since it was listed.",qd="is not a pending proposal.",Hd="is not a proposal file under .saffron/proposals.",ti="changed since it was listed. Refresh and review it again.",Vd="is being decided by another process. Try again.";function ni(e,t,n,r,o){if(!t.endsWith(".json")||G.existsSync(t)&&!lt(M.join(e,".saffron","proposals"),t))return o(Hd);try{return fn(t,r,()=>o(n?zd:qd))}catch(s){if(!(s instanceof Et))throw s;return o(Vd)}}function ri(e){let t=Me(e,Ht);if(t.length===0){console.log("No pending proposals.");return}console.log(`Pending proposals:
1054
- `);for(let{file:n,proposal:r}of t){console.log(` ${n}`);let o=r.meta.verified===!0?" \xB7 verified \u2713":r.meta.verified===!1?" \xB7 UNVERIFIED \u2717":"";console.log(` ${r.cache.feature} \u203A ${r.cache.scenario} (${r.meta.mode}, ${r.meta.createdAt}${o})`);let s=r.meta.bound?ei(e,r):wn;s&&console.log(` \u2717 STALE: ${s}`),console.log(` ${r.meta.narrative}`);for(let a of r.meta.adaptations)console.log(` \u26A0 ${a}`);r.meta.suggestedFeatureEdit&&console.log(` suggested .feature edit:
1049
+ Press Ctrl-C to stop.`),t.open!==!1&&as(v.url),await new Promise(u=>{process.once("SIGINT",()=>{v.close().finally(u)})}),process.exit(0)});q.command("prune").description("List (and with --yes remove) recordings whose scenario no longer exists: caches and pending proposals left behind by a deleted scenario or feature file").option("--json","machine-readable output").option("--yes","actually delete the files listed").option("--check","exit 1 when anything is orphaned, delete nothing (for CI)").action(e=>{let t=D.resolve(q.opts().projectRoot),n=ae(t),r=bn(t,n.features??"features");r.unreadable.length>0&&(e.json?console.log(JSON.stringify({...r,removed:[]},null,2)):console.error(["saffron: cannot tell what is orphaned while a feature file fails to parse.",...r.unreadable.map(o=>` ${o}`),"Fix these first: a syntax error hides every scenario in the file."].join(`
1050
+ `)),process.exit(2));let s=e.yes&&!e.check?Ds(t,r.orphans):[];if(e.json)console.log(JSON.stringify({...r,removed:s},null,2));else if(r.orphans.length===0)console.log(`Nothing to prune. All ${r.kept} recordings belong to a scenario.`);else{let o=r.orphans.filter(c=>c.kind==="cache").length,a=r.orphans.filter(c=>c.kind==="proposal").length,i=r.orphans.length-o-a;console.log([`${r.orphans.length} orphaned ${r.orphans.length===1?"file":"files"} (${o} cache, ${a} proposal, ${i} screenshot ${i===1?"folder":"folders"}), ${r.kept} still owned:`,"",...r.orphans.map(c=>` ${s.length>0?"removed ":""}${Ls(c)}`),"",s.length>0?"Deleted. Re-record by running the scenarios again if you deleted something by mistake.":"Nothing was deleted. Run `saffron prune --yes` to remove them."].join(`
1051
+ `))}e.check&&r.orphans.length>0&&process.exit(1)});q.command("status").description("Project overview for dashboards and IDE panels: feature files and scenarios with cache state, tags, pending proposals, last run, history, vocabulary health, effective config").option("--json","machine-readable output").action(e=>{let t=D.resolve(q.opts().projectRoot),n=ae(t),r=kn(t,n);if(e.json){console.log(JSON.stringify(r,null,2));return}$o(r)});q.command("report").description("Open the latest HTML report, or merge shard reports into it").argument("[paths...]","with --merge: shard report files or folders to search (default: .saffron)").option("--open","open in the default browser",!0).option("--merge","combine the shard-<i>-of-<n>.json reports of one sharded run into latest.json, latest.html and one history line").action((e,t)=>{let n=D.resolve(q.opts().projectRoot);if(t.merge){Kd(n,e);return}e.length>0&&(console.error("saffron report: paths are only read with --merge"),process.exit(2));let r=D.join(n,".saffron","reports","latest.html");G.existsSync(r)||(console.error("No report found. Run `saffron run` first."),process.exit(2)),console.log(r),as(r)});function Kd(e,t){let n=t.length>0?t.map(x=>D.resolve(x)):[D.join(e,".saffron")],r,s;try{let x=Ja(n);s=x.length,r=Ga(x)}catch(x){console.error(`saffron report --merge: ${x instanceof Error?x.message:String(x)}`),process.exit(2)}let{report:o,history:a}=r,i=Ft(e),c=i.find(x=>x.runId===a.runId),l=c?i.filter(x=>x.runId!==a.runId):i;o.trends=Er(l,a);let f=c!==void 0&&JSON.stringify(c)===JSON.stringify(a);f||So(e,a);let{html:d,json:m}=Zr(e,o),h=o.totals;console.log(`merged ${s} shard(s): ${h.scenarios} scenario(s) \xB7 ${h.green} passed, ${h.yellow} pending review, ${h.red} failed \xB7 ${h.aiCalls} AI calls${Ur(h)}`),f?console.log("history already holds this run: not appended again"):c&&console.log("history already held this run: its line is replaced with these results");let y=o.trends.prev;y&&console.log(`vs previous run: pass rate ${y.passRatePp>=0?"+":""}${y.passRatePp}pp \xB7 cost ${y.costUsd>=0?"+":"-"}$${Math.abs(y.costUsd).toFixed(4)} \xB7 AI calls ${y.aiCalls>=0?"+":""}${y.aiCalls}`);for(let x of o.trends.chronic)console.log(`\u26A0 chronic: ${x.feature} \u203A ${x.scenario}: ${x.heals} heal(s), ${x.reds} red(s) in last ${x.runs} run(s)`);console.log(`report: ${d}
1052
+ ${m}`)}function oi(e,t){try{return Ee(e,t,$e(e,ae(e),[]))}catch(n){return`cannot check: ${n instanceof Error?n.message.split(`
1053
+ `)[0]:String(n)}`}}var Wt=(e,t)=>console.error(`\u26A0 proposal ${e} cannot be read: ${t}`),Yd="is no longer pending: it was accepted, rejected or removed since it was listed.",Qd="is not a pending proposal.",Zd="is not a proposal file under .saffron/proposals.",ai="changed since it was listed. Refresh and review it again.",Xd="is being decided by another process. Try again.";function ii(e,t,n,r,s){if(!t.endsWith(".json")||G.existsSync(t)&&!lt(D.join(e,".saffron","proposals"),t))return s(Zd);try{return un(t,r,()=>s(n?Yd:Qd))}catch(o){if(!(o instanceof Et))throw o;return s(Xd)}}function ci(e){let t=De(e,Wt);if(t.length===0){console.log("No pending proposals.");return}console.log(`Pending proposals:
1054
+ `);for(let{file:n,proposal:r}of t){console.log(` ${n}`);let s=r.meta.verified===!0?" \xB7 verified \u2713":r.meta.verified===!1?" \xB7 UNVERIFIED \u2717":"";console.log(` ${r.cache.feature} \u203A ${r.cache.scenario} (${r.meta.mode}, ${r.meta.createdAt}${s})`);let o=r.meta.bound?oi(e,r):vn;o&&console.log(` \u2717 STALE: ${o}`),console.log(` ${r.meta.narrative}`);for(let a of r.meta.adaptations)console.log(` \u26A0 ${a}`);r.meta.suggestedFeatureEdit&&console.log(` suggested .feature edit:
1055
1055
  ${r.meta.suggestedFeatureEdit.split(`
1056
1056
  `).map(a=>` ${a}`).join(`
1057
1057
  `)}`),console.log()}console.log("Accept with: saffron accept <file> | --all")}q.parseAsync();