document360-writer 0.5.165 → 0.5.166
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/cli.js +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -313,7 +313,7 @@ ${s}${p}`).join("")}).join(`
|
|
|
313
313
|
`+(e.ok?oe.magenta("\u2736 "):oe.red("\u2736 "))+oe.gray(`Cooked for ${Bn(e.seconds)} \xB7 ${Wn(e.tokens)} tokens out${o}`+(e.costUsd>0?` \xB7 ${Se(e.costUsd)}`:""))}}}function cl(e,t){return e.map(n=>Is(n,t)).join(`
|
|
314
314
|
`)}import{resolveActiveProfile as rf,loadTokens as sf,isExpired as af,decodeJwtClaims as ll}from"document360-engine";function cf(e){return e.length>12?`${e.slice(0,8)}\u2026`:e}function Me(e,t){try{let n=rf(e,t),o=sf(n.name);if(!o)return{ok:!1,reason:`Not signed in to Document360 (profile "${n.name}"). Run /login first.`};if(af(o)&&!o.refreshToken)return{ok:!1,reason:`Your Document360 session expired (profile "${n.name}"). Run /login.`};let r=n.project.projectId;if(r){let i={...ll(o.idToken)??{},...ll(o.accessToken)??{}}.doc360_project_id;if(typeof i=="string"&&i&&i!==r)return{ok:!1,reason:`Your Document360 session is signed into a different project than profile "${n.name}" (this profile's project is ${cf(r)}). Run /login and pick this project.`}}return{ok:!0}}catch{return{ok:!0}}}import{existsSync as Ms,readFileSync as Bs}from"node:fs";import{isAbsolute as Jo,join as Zo,resolve as er}from"node:path";import N from"picocolors";function Hn(e,t){let n=[],o="";for(let r of e.split(/\s+/).filter(Boolean))for(o.length?o.length+1+r.length<=t?o+=` ${r}`:(n.push(o),o=r):o=r;o.length>t;)n.push(o.slice(0,t)),o=o.slice(t);return o&&n.push(o),n}function Ko(e){if(e.length===0)return[];let t=(s,i)=>`${s}${"\u2500".repeat(78)}${i}`,n=(s="")=>`\u2502 ${s.padEnd(76)} \u2502`,o=[`\u2B06 d360-writer upgraded \u2014 ${e.length===1?"one improvement affects":`${e.length} improvements affect`} the docs you've already written:`,"",t("\u250C","\u2510")];e.forEach((s,i)=>{i>0&&o.push(t("\u251C","\u2524")),o.push(n());for(let u of Hn(`${i+1}. ${s.feature}`,76))o.push(n(u));o.push(n());for(let u of Hn(s.why,73))o.push(n(` ${u}`));o.push(n())}),o.push(t("\u2514","\u2518")),o.push("");let r=e.length>1;return o.push(`\u2713 ${r?"These are":"This is"} now on your docs roadmap \u2014 each launch shows your next step below (/roadmap shows the full campaign).`),o}import{readProjectConfig as hn,screenshotPlaceholderIds as bl,DEFAULT_CAPTURE_DIR as tr,DEFAULT_OUTPUT_DIR as hf,DEFAULT_DOCS_DIR as Hs}from"document360-engine";import{createHash as lf}from"node:crypto";import{readFileSync as uf,writeFileSync as df}from"node:fs";import{captureStatePath as dl}from"document360-engine";var ul={version:1,shots:{}};function pl(e){return lf("sha1").update(e).digest("hex").slice(0,12)}function _t(e){try{let t=JSON.parse(uf(dl(e),"utf8").replace(/^/,""));return!t||typeof t!="object"||t.version!==1||!t.shots?{...ul,shots:{}}:{version:1,shots:t.shots}}catch{return{...ul,shots:{}}}}function pf(e,t){df(dl(e),`${JSON.stringify(t,null,2)}
|
|
315
315
|
`,"utf8")}function Os(e){return e&&/data-testid|\btest-?id\b|has(?:n't| not) landed/i.test(e)?"testid":"data"}function hl(e,t,n){if(t.length===0)return;let o=_t(e);for(let r of t){let s=r.specSource?pl(r.specSource):void 0;o.shots[r.id]={outcome:r.outcome,at:n,...r.reason?{reason:r.reason}:{},...r.outcome==="skipped"?{blockedBy:Os(r.reason)}:{},...s?{specHash:s}:{}}}pf(e,o)}function _s(e,t,n){let o=e.shots[t];return!o||n===null||!o.specHash||o.specHash!==pl(n)?null:o}function fl(e){let t=_t(e).shots;return Object.keys(t).filter(n=>t[n].outcome==="failed").sort()}function Ls(e,t,n){let o=_s(_t(e),t,n);return!o||o.outcome==="captured"||!o.reason?null:{outcome:o.outcome,reason:o.reason}}var $l=e=>e.replace(/\\/g,"/").replace(/\/+$/,"");function ml(e){let t=e.includes("--detail")||e.includes("-d"),n=e.find(o=>o!=="--detail"&&o!=="-d");return{mode:"list",scope:n?$l(n):void 0,...t?{detail:!0}:{}}}function qn(e){let t=(e[0]??"").toLowerCase();if(t==="capture")return{mode:"capture",rest:e.slice(1)};if(t==="checklist"||t==="setup")return{mode:"checklist"};if(t==="list")return ml(e.slice(1));t==="author"&&(e=e.slice(1));let n=!e.includes("--no-setup"),o=e.filter(s=>s!=="--no-setup");if(o[0]==="--list")return ml(o.slice(1));let r=o[0];return r?r==="--all"?{mode:"all",setup:n}:/[\\/]/.test(r)||r.endsWith(".md")?{mode:"scope",scope:$l(r),setup:n}:{mode:"single",id:r}:{mode:"list",picker:!0}}function Ke(e,t){let n=hn(e),o=n?.captureDir??tr,r=n?.outputDir??hf,s=(u,p)=>Jo(u)?Zo(u,p):er(e,u,p),i=_t(e);return bl(e,{scope:t}).map(({id:u,file:p})=>{let b=s(o,`${u}.spec.ts`),k=null;if(Ms(b))try{k=Bs(b,"utf8")}catch{}let $=Ms(s(r,`${u}.png`)),x=k===null?null:_s(i,u,k),A=k!==null&&mf(k),v=$?"captured":k===null?"placeholder":A||x?.outcome==="skipped"?"blocked":x?.outcome==="failed"?"broken":"spec",T=k===null?void 0:vl(k),D=A?gf(k):x?.reason,U=A?"testid":x?.outcome==="skipped"?Os(x.reason):void 0;return{id:u,file:p,state:v,...T?{mutates:T.mutates,...T.declared?{}:{undeclared:!0}}:{},...k!==null&&yl(k).length>0?{unguardedWaits:yl(k)}:{},...k!==null&&gl(k).length>0?{unverifiedForce:gl(k)}:{},...v==="blocked"||v==="broken"?{...D?{reason:D}:{},...U?{blockedBy:U}:{}}:{}}})}function nr(e,t){let n=hn(e)?.captureDir??tr,o=Jo(n)?Zo(n,`${t}.spec.ts`):er(e,n,`${t}.spec.ts`);try{return Bs(o,"utf8")}catch{return null}}function xl(e,t){let n=hn(e)?.captureDir??tr;return Ms(Jo(n)?Zo(n,`${t}.spec.ts`):er(e,n,`${t}.spec.ts`))}function ff(e,t,n){let o=[n.trim()];for(let r=t+1;r<e.length;r++){let s=/^\s*\/\/\s?(.*)$/.exec(e[r]);if(!s)break;let i=s[1].trim();if(!i||/^(CAPTURE-[A-Z]+:|TODO\b|NOTE\b|Grounding\b|\d+\.\s)/.test(i)||(o.push(i),o.join(" ").length>400))break}return o.join(" ")}function vl(e){let t=e.split(/\r?\n/),n=t.findIndex(u=>/^\s*\/\/\s*CAPTURE-SAFETY:\s*(readonly|mutates)\b/i.test(u)),o=n>=0?/^\s*\/\/\s*CAPTURE-SAFETY:\s*(readonly|mutates)\b[\s—:–-]*(.*)$/i.exec(t[n]):null;if(o){let u=ff(t,n,o[2]??"");return{mutates:o[1].toLowerCase()==="mutates",declared:!0,what:u||void 0}}let r=/setInputFiles/.test(e),s=/\{\s*name:\s*['"][^'"]*\b(create|import|save|apply|confirm|delete|submit|send|mint)\b[^'"]*['"][^}]*\}\s*\)\s*\.click\(/i.test(e),i=/\.fill\(/.test(e);return{mutates:r||i&&s,declared:!1}}function mf(e){return/(^|\n)\s*test\.skip\s*\(\s*['"`]/.test(e)}function gf(e){let t=/\/\/\s*CAPTURE-TODO:\s*(\{.*\})\s*$/m.exec(e),n,o;if(t)try{let s=JSON.parse(t[1]);n=s.testid,o=s.file}catch{}return`Inert until your developers add ${n?`data-testid="${n}"${o?` in ${o}`:""}`:"a stable data-testid"} \u2014 the spec is skipped at compile time, so no capture run can produce this shot. /screenshot checklist builds the hand-off.`}function Cl(e){return e.split(/\r?\n/).map(t=>{if(/^\s*\*/.test(t))return"";let n=/(?<!:)\/\//.exec(t);return n?t.slice(0,n.index):t})}function gl(e,t=10){let n=Cl(e),o=[];for(let[r,s]of n.entries())/\.click\s*\([^)]*force\s*:\s*true/.test(s)&&(/test\.skip\s*\(/.test(n.slice(r+1,r+1+t).join(`
|
|
316
|
-
`))||o.push(r+1));return o}function yl(e){let t=[],n=Cl(e);for(let[o,r]of n.entries()){if(!/\.waitFor\s*\(/.test(r))continue;let s=r;for(let i=o+1;i<n.length&&!/;\s*$/.test(s.trim())&&i<o+4;i++)s+=n[i];/\.catch\s*\(/.test(s)||t.push(o+1)}return t}function Rl(e){return(e??"").trim().replace(/[.\s]+$/,"")}function Sl(e,t){let n=hn(e)?.captureDir??tr,o=Jo(n)?Zo(n,`${t}.spec.ts`):er(e,n,`${t}.spec.ts`);try{return vl(Bs(o,"utf8"))}catch{return{mutates:!1,declared:!1}}}function or(e){return[...new Set(e.filter(t=>t.state==="captured").map(t=>t.file))].sort()}var yf={placeholder:"\u25CB",spec:"\u25D0",blocked:"\u2298",broken:"\u2717",captured:"\u25CF"},wf={placeholder:N.gray,spec:N.yellow,blocked:N.cyan,broken:N.red,captured:N.green},Pl=` ${N.green("\u25CF captured")} ${N.yellow("\u25D0 ready")} ${N.cyan("\u2298 blocked")} ${N.red("\u2717 broken")} ${N.gray("\u25CB to spec")}`;function ze(e,t=n=>n){return`${t(e)} `}function kf(e){let t=e.replace(/\s+/g," ").trim().replace(/^Prerequisite not met:\s*/i,""),n=/^(.*?[a-z0-9)"'”’])\.(?:\s|$)/i.exec(t);return(n?n[1]:t).trim()}function qs(e){let t=new Map;for(let n of e){if(n.state!=="blocked")continue;let o=(n.reason??"No reason recorded \u2014 re-run this capture to find out why.").replace(/\s+/g," ").trim(),r=n.blockedBy??"data",s=kf(o),i=`${r} ${s.toLowerCase()}`,u=t.get(i)??{reason:s,blockedBy:r,ids:[]};t.has(i)||t.set(i,u),u.ids.push(n.id)}return[...t.values()].sort((n,o)=>o.ids.length-n.ids.length||n.reason.localeCompare(o.reason))}function Tl(e){let t=qs(e);if(t.length===0)return[];let n=t.filter(s=>s.blockedBy==="data"),o=t.filter(s=>s.blockedBy==="testid"),r=[""];if(n.length>0){let s=n.reduce((u,p)=>u+p.ids.length,0);r.push(` ${N.bold("Stage this data")} ${N.gray(`\u2014 unblocks ${s} shot${s===1?"":"s"} in ${n.length} action${n.length===1?"":"s"}`)}`);let i=8;for(let u of n.slice(0,i)){let p=Hn(u.reason,86);r.push(` ${N.cyan(yt(`${u.ids.length}\xD7`,4))} ${p[0]??""}`);for(let k of p.slice(1))r.push(` ${k}`);let b=u.ids.slice(0,4).join(" \xB7 ");r.push(N.gray(` ${b}${u.ids.length>4?` \xB7 +${u.ids.length-4} more`:""}`))}n.length>i&&r.push(N.gray(` \u2026 ${n.length-i} more staging action${n.length-i===1?"":"s"} \u2014 /screenshot list --detail`))}if(o.length>0){let s=o.reduce((i,u)=>i+u.ids.length,0);r.push(` ${ze("\u2298",N.cyan)}${s} shot${s===1?"":"s"} need${s===1?"s":""} a data-testid from your developers \u2014 ${N.bold("/screenshot checklist")} builds the hand-off.`)}return r}function El(e){let t=e.filter(o=>(o.unguardedWaits?.length??0)>0);if(t.length===0)return[];let n=t.reduce((o,r)=>o+(r.unguardedWaits?.length??0),0);return["",` ${ze("\u26A0",N.yellow)}${t.length} spec${t.length===1?"":"s"} ${t.length===1?"has":"have"} ${n} unguarded wait${n===1?"":"s"}`+N.gray(" \u2014 on missing data these fail with a stack trace instead of telling you what to stage"),N.gray(` ${t.slice(0,6).map(o=>`${o.id}:${o.unguardedWaits.join(",")}`).join(" \xB7 ")}${t.length>6?` \xB7 +${t.length-6} more`:""}`),N.gray(` Re-author to guard them: ${N.bold("/screenshot author <id>")}`)]}function Dl(e){let t=e.filter(n=>(n.unverifiedForce?.length??0)>0);return t.length===0?[]:["",` ${ze("\u26A0",N.red)}${t.length} spec${t.length===1?"":"s"} force-click past Playwright's safety checks without asserting the result`+N.gray(" \u2014 if the click lands on nothing, the shot photographs the unchanged screen"),N.gray(` ${t.slice(0,6).map(n=>`${n.id}:${n.unverifiedForce.join(",")}`).join(" \xB7 ")}${t.length>6?` \xB7 +${t.length-6} more`:""}`),N.gray(` Re-author to check what the click did: ${N.bold("/screenshot author <id>")}`)]}function Al(e){let t=e.filter(o=>o.undeclared);if(t.length===0)return[];let n=t.filter(o=>o.mutates);return["",` ${ze("\u26A0",N.yellow)}${t.length} spec${t.length===1?"":"s"} carr${t.length===1?"ies":"y"} no CAPTURE-SAFETY declaration`+(n.length>0?N.gray(` \u2014 ${n.length} of them read as "writes real data" by heuristic, so bulk capture benches ${n.length===1?"it":"them"}`):""),N.gray(` ${t.slice(0,6).map(o=>o.id).join(" \xB7 ")}${t.length>6?` \xB7 +${t.length-6} more`:""}`),N.gray(` Re-author them to declare it: ${N.bold("/screenshot author <id>")}`)]}var Xo=(e,t)=>e.length>=t?e:e+" ".repeat(t-e.length),yt=(e,t)=>e.length>=t?e:" ".repeat(t-e.length)+e;function jl(e,t){let n=e.replace(/\\/g,"/"),o=t.replace(/\\/g,"/").replace(/\/+$/,"");o&&(n===o||n.startsWith(`${o}/`))&&(n=n.slice(o.length+1));let r=n.split("/").filter(Boolean);return r.length>1?r[0]:""}var bf=new Set(["and","or","of","the","to","a","an","in","on","for","with","vs"]),$f=new Set(["ai","api","cli","ui","ux","url","id","http","https","json","xml","sdk","csv","pdf","oauth","sso","mfa","io","faq"]);function xf(e){return e.replace(/[-_]+/g," ").trim().split(/\s+/).filter(Boolean).map((n,o)=>{let r=n.toLowerCase();return $f.has(r)?r.toUpperCase():o>0&&bf.has(r)?r:r.charAt(0).toUpperCase()+r.slice(1)}).join(" ")}function Us(e){if(!e)return{num:"",name:"(docs root)"};let t=/^(\d+)[-_](.+)$/.exec(e);return{num:t?t[1]:"",name:xf(t?t[2]:e)}}function vf(e){return e.replace(/\\/g,"/").split("/").filter(Boolean).pop()??e}function wl(e,t,n){let o=t>0?Math.round(e/t*n):0;return N.green("\u2588".repeat(o))+N.gray("\u2591".repeat(Math.max(0,n-o)))}function Cf(e,t){let n=t>0?Math.round(e/t*100):0,o=yt(`${n}%`,4);return n===100?N.green(o):n>0?N.yellow(o):N.gray(o)}function Nl(e,t){let n=new Map;for(let o of e){let r=jl(o.file,t),s=n.get(r)??[];n.has(r)||n.set(r,s),s.push(o)}return[...n.keys()].sort().map(o=>[o,n.get(o)])}function Rf(e,t){let n=(L,G)=>L.filter(V=>V.state===G).length,o=e.length,r=n(e,"captured"),s=n(e,"spec"),i=n(e,"blocked"),u=n(e,"broken"),p=n(e,"placeholder"),b=new Set(e.map(L=>L.file)).size,k=Nl(e,t),$=[];$.push(` ${N.bold("Screenshots")} ${N.green(`${r}/${o} captured`)} ${N.gray("\xB7")} ${s>0?N.yellow(`${s} ready`):N.gray("0 ready")}`+(i>0?` ${N.gray("\xB7")} ${N.cyan(`${i} blocked`)}`:"")+(u>0?` ${N.gray("\xB7")} ${N.red(`${u} broken`)}`:"")+` ${N.gray("\xB7")} ${p>0?N.red(`${p} to spec`):N.gray("0 to spec")} `+N.gray(`across ${b} article${b===1?"":"s"}`));let x=o>0?Math.round(r/o*100):0;$.push(` [${wl(r,o,32)}] ${N.bold(`${x}%`)}`),$.push("");let A=k.map(([L])=>Us(L)),v=Math.min(32,Math.max(7,...A.map(L=>L.name.length))),T=L=>L.length>v?`${L.slice(0,v-1)}\u2026`:Xo(L,v),D=[{head:"Ready",w:5,state:"spec",color:N.yellow},...i>0?[{head:"Blocked",w:7,state:"blocked",color:N.cyan}]:[],...u>0?[{head:"Broken",w:6,state:"broken",color:N.red}]:[],{head:"To spec",w:7,state:"placeholder",color:N.red}];$.push(N.gray(` ${Xo("#",2)} ${Xo("Chapter",v)} ${yt("Captured",8)} ${D.map(L=>yt(L.head,L.w)).join(" ")} Progress`));for(let[L,[,G]]of k.entries()){let V=n(G,"captured"),se=G.length,{num:We,name:fe}=A[L],Qe=V===se,me=T(fe),Je=D.map(be=>{let Be=n(G,be.state);return Be>0?be.color(yt(String(Be),be.w)):N.gray(yt("\u2014",be.w))}).join(" ");$.push(` ${N.cyan(Xo(We,2))} ${Qe?N.green(me):V>0?me:N.gray(me)} ${Qe?N.green(yt(`${V}/${se}`,8)):yt(`${V}/${se}`,8)} ${Je} ${wl(V,se,10)} ${Cf(V,se)}`)}$.push("");let U=u>0?` ${N.gray("\xB7")} ${u} spec${u===1?"":"s"} broken \u2014 ${N.bold(`/screenshot ${e.find(L=>L.state==="broken").id}`)} re-authors ${u===1?"it":"the first"}`:"";if(p>0)$.push(` ${ze("\u2192",N.yellow)}${p} placeholder${p===1?"":"s"} still need a spec \u2014 run ${N.bold("/screenshot author --all")}${U}`);else if(s>0){let L=e.filter(V=>V.state==="spec"&&V.mutates).length,G=L>0?N.gray(` \u2014 ${s-L} in one go; ${L} write${L===1?"s":""} real data and run${L===1?"s":""} one at a time`):"";$.push(` ${ze("\u2192",N.yellow)}${s} spec${s===1?"":"s"} ready to capture \u2014 run ${N.bold("/screenshot capture")}${G}${U}`)}else i>0?$.push(` ${ze("\u2192",N.cyan)}${i} shot${i===1?"":"s"} blocked \u2014 stage the data below, then ${N.bold("/screenshot capture --all --retry-blocked")}${U}`):u>0?$.push(` ${ze("\u2192",N.red)}${u} spec${u===1?"":"s"} broken \u2014 ${N.bold(`/screenshot ${e.find(L=>L.state==="broken").id}`)} re-authors ${u===1?"it":"the first"} (re-capturing fails the same way)`):$.push(` ${ze("\u2713",N.green)}all ${o} captured \u2014 ${N.bold("/publish")} embeds them in the articles`);if(r>0&&r<o){let L=k.filter(([,G])=>G.every(V=>V.state!=="captured")).map(([G])=>{let{num:V,name:se}=Us(G);return V?`${V} ${se}`:se});L.length>0&&$.push(N.gray(` Furthest behind: ${L.slice(0,5).join(" \xB7 ")}${L.length>5?" \u2026":""} (0% captured)`))}return $.push(...Tl(e),...Dl(e),...El(e),...Al(e)),$.push(""),$.push(Pl),$.push(N.gray(" Drill in: /screenshot list <chapter#> \xB7 /screenshot list --detail (all)")),$}function Sf(e,t,n){let o=new Set(e.map(i=>i.file)),r=e.filter(i=>i.state==="captured").length,s=[` ${N.bold("Screenshots")}: ${e.length} placeholder${e.length===1?"":"s"} across ${o.size} article${o.size===1?"":"s"} \xB7 ${N.green(`${r} captured`)}${n?N.gray(` \xB7 scope ${n}`):""}`,""];for(let[i,u]of Nl(e,t)){let p=u.filter(A=>A.state==="captured").length,{num:b,name:k}=Us(i),$=b?`${b} \xB7 ${k}`:k;s.push(` ${N.bold($)} ${N.gray(`(${p}/${u.length})`)}`);let x=new Map;for(let A of u){let v=vf(A.file),T=x.get(v)??[];x.has(v)||x.set(v,T),T.push(A)}for(let A of[...x.keys()].sort()){s.push(N.gray(` ${A}`));for(let v of x.get(A)){let T=wf[v.state];if(s.push(` ${ze(yf[v.state],T)}${T(v.id)}`),(v.state==="blocked"||v.state==="broken")&&v.reason)for(let D of Hn(v.reason,84))s.push(N.gray(` ${D}`))}}}return s.push(...Tl(e),...Dl(e),...El(e),...Al(e)),s.push("",Pl),s}function rr(e,t){let n=t?.scope;if(e.length===0)return[n?`No screenshot placeholders under ${n}.`:"No screenshot placeholders found in the docs."];let o=t?.docsDir??Hs;return t?.detail||n?Sf(e,o,n):Rf(e,o)}function Vs(e,t){if(!t||!/^\d+$/.test(t))return t;let n=hn(e)?.docsDir??Hs,o=String(Number(t));for(let{file:r}of bl(e)){let s=jl(r,n),i=/^(\d+)/.exec(s);if(i&&String(Number(i[1]))===o)return s}return t}function Pf(e){let t=e.replace(/\\/g,"/").split("/").filter(Boolean),n=t.pop()??e;return{category:t.pop()??"(docs root)",article:n}}function Qo(e,t,n){let o=new Map;for(let s of t){let{category:i,article:u}=Pf(s.file),p=o.get(i)??new Map;o.set(i,p);let b=p.get(u)??[];p.set(u,b),b.push(s)}let r=s=>n==="author"||!s.mutates;for(let s of[...o.keys()].sort()){let i=o.get(s),u=[...i.values()].flat().filter(r).map(p=>p.id);u.length>0&&e.push({kind:"category",action:n,label:`${s} (${u.length})`,ids:u,depth:1});for(let p of[...i.keys()].sort()){let b=i.get(p),k=b.filter(r).map($=>$.id);k.length>0&&e.push({kind:"article",action:n,label:`${p} (${k.length})`,ids:k,depth:2});for(let $ of b){let x=n==="capture"&&$.mutates?" \u26A0 creates real data \u2014 runs only alone":void 0;e.push({kind:"shot",action:n,label:$.id,ids:[$.id],depth:3,...x?{note:x}:{}})}}}}function kl(e,t=58){if(!e)return"";let n=e.replace(/\s+/g," ").trim();return n.length>t?`${n.slice(0,t-1)}\u2026`:n}function Gs(e,t,n){let o=t.filter(r=>!r.mutates);o.length>1&&e.push({kind:"all",action:"capture",label:`Retry ${o.length} blocked shots (stage the data first)`,ids:o.map(r=>r.id),depth:0});for(let r of[...t].sort((s,i)=>s.id.localeCompare(i.id)))e.push({kind:"shot",action:"capture",label:r.id,ids:[r.id],depth:1,note:` \u2298 ${kl(r.reason)}`});n.length>1&&e.push({kind:"all",action:"author",label:`Re-author ${n.length} broken specs`,ids:n.map(r=>r.id),depth:0});for(let r of[...n].sort((s,i)=>s.id.localeCompare(i.id)))e.push({kind:"shot",action:"author",label:r.id,ids:[r.id],depth:1,note:` \u2717 ${kl(r.reason)} \u2014 re-authors`})}function Il(e){let t=e.filter(s=>s.state==="placeholder"),n=e.filter(s=>s.state==="spec"),o=[];t.length>1&&o.push({kind:"all",action:"author",label:`Author all ${t.length} remaining specs`,ids:t.map(s=>s.id),depth:0}),Qo(o,t,"author");let r=n.filter(s=>!s.mutates);return r.length>1&&o.push({kind:"all",action:"capture",label:`Capture all ${r.length} ready specs`,ids:r.map(s=>s.id),depth:0}),Qo(o,n,"capture"),Gs(o,e.filter(s=>s.state==="blocked"),e.filter(s=>s.state==="broken")),o}var Fs=["MANDATORY \u2014 a missing prerequisite must SKIP, never time out. Two rules, applied to EVERY locator","in the spec, not just the first one or the one shown in the skill's example:","1. A bare `await x.waitFor(...)` is itself the bug: on a missing element it THROWS and the run"," reports a hard failure with a stack trace instead of a staging instruction. Every wait must be",' `await x.waitFor({ state: "visible", timeout: 10_000 }).catch(() => {});` followed by',' `if ((await x.count()) === 0) test.skip(true, "Prerequisite not met: \u2026")`. That applies to each'," element you wait on or click IN TURN \u2014 tabs, toggles, rows, buttons reached after the first"," guard \u2014 because any of them can be absent for the same reason the data is.",' And when a wait DOES time out, diagnose before you guard: a skip asserts "your app lacks this',' data", so if the real cause is a locator matching a node you did not intend (a rollup row, a'," sibling with the same testid), that assertion is a lie and sends the user to stage what they"," already have. Narrow the selector first, THEN guard."," Two corollaries, both of which have cost a wasted repair round:"," (a) An element that is not RENDERED is not absent. A collapsed tree node, an unopened accordion,"," an inactive tab, a virtualised list or page 2 of a table keeps its children out of the DOM."," EXPAND/scroll/switch to reach the target \u2014 that is a step to script, not data to stage."," (b) When you narrow past a decoy, count the UNNARROWED set too and put the number in the reason:",' "Found 6 folder roll-ups but no endpoint-level score \u2014 if one exists it may not be rendered".',' A bare "prerequisite not met" cannot tell absent data from an over-narrow selector. Put that'," evidence in a LATER sentence, never the first: grouping keys on the first sentence, so an",' interpolated count there makes every run its own "action" and the dedup stops working.'," (c) `click({ force: true })` is the ONE pattern that turns a failure into a silent WRONG"," SCREENSHOT rather than a crash or skip: it skips the actionability checks, so a click that"," lands on nothing photographs the unchanged screen and that image ships in the docs. It is"," sometimes right (a button nested inside a button reads as intercepted by its own ancestor)."," Never force blind: ground it in source, scrollIntoViewIfNeeded() first, and ASSERT THE"," CONSEQUENCE immediately after with a guarded wait + test.skip, so a no-op force skips."," Know its limit: force skips the CHECK, it does not change which element RECEIVES the click."," If the interception is real the ancestor still gets it (measured: a forced click on a pill"," nested in its section-header button was handled by the header; the popover never opened).",' For a genuinely nested interactive node use `locator.dispatchEvent("click")`, which goes'," straight to that node with no hit-test \u2014 and still assert the consequence.","2. Never reason about whether a role/text locator is unique; you cannot verify that by reading"," source, and Playwright fails the whole spec on 2+ matches. Every `getByRole`/`getByText`/"," `:has-text` without a unique data-testid gets `.first()` or a container scope"," (`page.locator('[data-testid=\"\u2026\"]').getByRole(\u2026)`). If you catch yourself writing a comment"," ARGUING a selector is unique, scope it instead \u2014 that argument is what fails.","3. Every skip reason opens with the SHARED staging action, worded identically across every shot",' that needs the same thing, naming where in the product to do it ("Create an API key in Settings',' \u2192 API Keys."); shot-specific detail (counts, which record, which state) goes in LATER sentences.'," The dashboard groups blocked shots by that first sentence, so a near-duplicate wording splits one"," chore into two. Grep sibling specs and reuse their exact opening sentence.","Before you finish, re-read each spec you wrote and check all three rules line by line."].join(`
|
|
316
|
+
`))||o.push(r+1));return o}function yl(e){let t=[],n=Cl(e);for(let[o,r]of n.entries()){if(!/\.waitFor\s*\(/.test(r))continue;let s=r;for(let i=o+1;i<n.length&&!/;\s*$/.test(s.trim())&&i<o+4;i++)s+=n[i];/\.catch\s*\(/.test(s)||t.push(o+1)}return t}function Rl(e){return(e??"").trim().replace(/[.\s]+$/,"")}function Sl(e,t){let n=hn(e)?.captureDir??tr,o=Jo(n)?Zo(n,`${t}.spec.ts`):er(e,n,`${t}.spec.ts`);try{return vl(Bs(o,"utf8"))}catch{return{mutates:!1,declared:!1}}}function or(e){return[...new Set(e.filter(t=>t.state==="captured").map(t=>t.file))].sort()}var yf={placeholder:"\u25CB",spec:"\u25D0",blocked:"\u2298",broken:"\u2717",captured:"\u25CF"},wf={placeholder:N.gray,spec:N.yellow,blocked:N.cyan,broken:N.red,captured:N.green},Pl=` ${N.green("\u25CF captured")} ${N.yellow("\u25D0 ready")} ${N.cyan("\u2298 blocked")} ${N.red("\u2717 broken")} ${N.gray("\u25CB to spec")}`;function ze(e,t=n=>n){return`${t(e)} `}function kf(e){let t=e.replace(/\s+/g," ").trim().replace(/^Prerequisite not met:\s*/i,""),n=/^(.*?[a-z0-9)"'”’])\.(?:\s|$)/i.exec(t);return(n?n[1]:t).trim()}function qs(e){let t=new Map;for(let n of e){if(n.state!=="blocked")continue;let o=(n.reason??"No reason recorded \u2014 re-run this capture to find out why.").replace(/\s+/g," ").trim(),r=n.blockedBy??"data",s=kf(o),i=`${r} ${s.toLowerCase()}`,u=t.get(i)??{reason:s,blockedBy:r,ids:[]};t.has(i)||t.set(i,u),u.ids.push(n.id)}return[...t.values()].sort((n,o)=>o.ids.length-n.ids.length||n.reason.localeCompare(o.reason))}function Tl(e){let t=qs(e);if(t.length===0)return[];let n=t.filter(s=>s.blockedBy==="data"),o=t.filter(s=>s.blockedBy==="testid"),r=[""];if(n.length>0){let s=n.reduce((u,p)=>u+p.ids.length,0);r.push(` ${N.bold("Stage this data")} ${N.gray(`\u2014 unblocks ${s} shot${s===1?"":"s"} in ${n.length} action${n.length===1?"":"s"}`)}`);let i=8;for(let u of n.slice(0,i)){let p=Hn(u.reason,86);r.push(` ${N.cyan(yt(`${u.ids.length}\xD7`,4))} ${p[0]??""}`);for(let k of p.slice(1))r.push(` ${k}`);let b=u.ids.slice(0,4).join(" \xB7 ");r.push(N.gray(` ${b}${u.ids.length>4?` \xB7 +${u.ids.length-4} more`:""}`))}n.length>i&&r.push(N.gray(` \u2026 ${n.length-i} more staging action${n.length-i===1?"":"s"} \u2014 /screenshot list --detail`))}if(o.length>0){let s=o.reduce((i,u)=>i+u.ids.length,0);r.push(` ${ze("\u2298",N.cyan)}${s} shot${s===1?"":"s"} need${s===1?"s":""} a data-testid from your developers \u2014 ${N.bold("/screenshot checklist")} builds the hand-off.`)}return r}function El(e){let t=e.filter(o=>(o.unguardedWaits?.length??0)>0);if(t.length===0)return[];let n=t.reduce((o,r)=>o+(r.unguardedWaits?.length??0),0);return["",` ${ze("\u26A0",N.yellow)}${t.length} spec${t.length===1?"":"s"} ${t.length===1?"has":"have"} ${n} unguarded wait${n===1?"":"s"}`+N.gray(" \u2014 on missing data these fail with a stack trace instead of telling you what to stage"),N.gray(` ${t.slice(0,6).map(o=>`${o.id}:${o.unguardedWaits.join(",")}`).join(" \xB7 ")}${t.length>6?` \xB7 +${t.length-6} more`:""}`),N.gray(` Re-author to guard them: ${N.bold("/screenshot author <id>")}`)]}function Dl(e){let t=e.filter(n=>(n.unverifiedForce?.length??0)>0);return t.length===0?[]:["",` ${ze("\u26A0",N.red)}${t.length} spec${t.length===1?"":"s"} force-click past Playwright's safety checks without asserting the result`+N.gray(" \u2014 if the click lands on nothing, the shot photographs the unchanged screen"),N.gray(` ${t.slice(0,6).map(n=>`${n.id}:${n.unverifiedForce.join(",")}`).join(" \xB7 ")}${t.length>6?` \xB7 +${t.length-6} more`:""}`),N.gray(` Re-author to check what the click did: ${N.bold("/screenshot author <id>")}`)]}function Al(e){let t=e.filter(o=>o.undeclared);if(t.length===0)return[];let n=t.filter(o=>o.mutates);return["",` ${ze("\u26A0",N.yellow)}${t.length} spec${t.length===1?"":"s"} carr${t.length===1?"ies":"y"} no CAPTURE-SAFETY declaration`+(n.length>0?N.gray(` \u2014 ${n.length} of them read as "writes real data" by heuristic, so bulk capture benches ${n.length===1?"it":"them"}`):""),N.gray(` ${t.slice(0,6).map(o=>o.id).join(" \xB7 ")}${t.length>6?` \xB7 +${t.length-6} more`:""}`),N.gray(` Re-author them to declare it: ${N.bold("/screenshot author <id>")}`)]}var Xo=(e,t)=>e.length>=t?e:e+" ".repeat(t-e.length),yt=(e,t)=>e.length>=t?e:" ".repeat(t-e.length)+e;function jl(e,t){let n=e.replace(/\\/g,"/"),o=t.replace(/\\/g,"/").replace(/\/+$/,"");o&&(n===o||n.startsWith(`${o}/`))&&(n=n.slice(o.length+1));let r=n.split("/").filter(Boolean);return r.length>1?r[0]:""}var bf=new Set(["and","or","of","the","to","a","an","in","on","for","with","vs"]),$f=new Set(["ai","api","cli","ui","ux","url","id","http","https","json","xml","sdk","csv","pdf","oauth","sso","mfa","io","faq"]);function xf(e){return e.replace(/[-_]+/g," ").trim().split(/\s+/).filter(Boolean).map((n,o)=>{let r=n.toLowerCase();return $f.has(r)?r.toUpperCase():o>0&&bf.has(r)?r:r.charAt(0).toUpperCase()+r.slice(1)}).join(" ")}function Us(e){if(!e)return{num:"",name:"(docs root)"};let t=/^(\d+)[-_](.+)$/.exec(e);return{num:t?t[1]:"",name:xf(t?t[2]:e)}}function vf(e){return e.replace(/\\/g,"/").split("/").filter(Boolean).pop()??e}function wl(e,t,n){let o=t>0?Math.round(e/t*n):0;return N.green("\u2588".repeat(o))+N.gray("\u2591".repeat(Math.max(0,n-o)))}function Cf(e,t){let n=t>0?Math.round(e/t*100):0,o=yt(`${n}%`,4);return n===100?N.green(o):n>0?N.yellow(o):N.gray(o)}function Nl(e,t){let n=new Map;for(let o of e){let r=jl(o.file,t),s=n.get(r)??[];n.has(r)||n.set(r,s),s.push(o)}return[...n.keys()].sort().map(o=>[o,n.get(o)])}function Rf(e,t){let n=(L,G)=>L.filter(V=>V.state===G).length,o=e.length,r=n(e,"captured"),s=n(e,"spec"),i=n(e,"blocked"),u=n(e,"broken"),p=n(e,"placeholder"),b=new Set(e.map(L=>L.file)).size,k=Nl(e,t),$=[];$.push(` ${N.bold("Screenshots")} ${N.green(`${r}/${o} captured`)} ${N.gray("\xB7")} ${s>0?N.yellow(`${s} ready`):N.gray("0 ready")}`+(i>0?` ${N.gray("\xB7")} ${N.cyan(`${i} blocked`)}`:"")+(u>0?` ${N.gray("\xB7")} ${N.red(`${u} broken`)}`:"")+` ${N.gray("\xB7")} ${p>0?N.red(`${p} to spec`):N.gray("0 to spec")} `+N.gray(`across ${b} article${b===1?"":"s"}`));let x=o>0?Math.round(r/o*100):0;$.push(` [${wl(r,o,32)}] ${N.bold(`${x}%`)}`),$.push("");let A=k.map(([L])=>Us(L)),v=Math.min(32,Math.max(7,...A.map(L=>L.name.length))),T=L=>L.length>v?`${L.slice(0,v-1)}\u2026`:Xo(L,v),D=[{head:"Ready",w:5,state:"spec",color:N.yellow},...i>0?[{head:"Blocked",w:7,state:"blocked",color:N.cyan}]:[],...u>0?[{head:"Broken",w:6,state:"broken",color:N.red}]:[],{head:"To spec",w:7,state:"placeholder",color:N.red}];$.push(N.gray(` ${Xo("#",2)} ${Xo("Chapter",v)} ${yt("Captured",8)} ${D.map(L=>yt(L.head,L.w)).join(" ")} Progress`));for(let[L,[,G]]of k.entries()){let V=n(G,"captured"),se=G.length,{num:We,name:fe}=A[L],Qe=V===se,me=T(fe),Je=D.map(be=>{let Be=n(G,be.state);return Be>0?be.color(yt(String(Be),be.w)):N.gray(yt("\u2014",be.w))}).join(" ");$.push(` ${N.cyan(Xo(We,2))} ${Qe?N.green(me):V>0?me:N.gray(me)} ${Qe?N.green(yt(`${V}/${se}`,8)):yt(`${V}/${se}`,8)} ${Je} ${wl(V,se,10)} ${Cf(V,se)}`)}$.push("");let U=u>0?` ${N.gray("\xB7")} ${u} spec${u===1?"":"s"} broken \u2014 ${N.bold(`/screenshot ${e.find(L=>L.state==="broken").id}`)} re-authors ${u===1?"it":"the first"}`:"";if(p>0)$.push(` ${ze("\u2192",N.yellow)}${p} placeholder${p===1?"":"s"} still need a spec \u2014 run ${N.bold("/screenshot author --all")}${U}`);else if(s>0){let L=e.filter(V=>V.state==="spec"&&V.mutates).length,G=L>0?N.gray(` \u2014 ${s-L} in one go; ${L} write${L===1?"s":""} real data and run${L===1?"s":""} one at a time`):"";$.push(` ${ze("\u2192",N.yellow)}${s} spec${s===1?"":"s"} ready to capture \u2014 run ${N.bold("/screenshot capture")}${G}${U}`)}else i>0?$.push(` ${ze("\u2192",N.cyan)}${i} shot${i===1?"":"s"} blocked \u2014 stage the data below, then ${N.bold("/screenshot capture --all --retry-blocked")}${U}`):u>0?$.push(` ${ze("\u2192",N.red)}${u} spec${u===1?"":"s"} broken \u2014 ${N.bold(`/screenshot ${e.find(L=>L.state==="broken").id}`)} re-authors ${u===1?"it":"the first"} (re-capturing fails the same way)`):$.push(` ${ze("\u2713",N.green)}all ${o} captured \u2014 ${N.bold("/publish")} embeds them in the articles`);if(r>0&&r<o){let L=k.filter(([,G])=>G.every(V=>V.state!=="captured")).map(([G])=>{let{num:V,name:se}=Us(G);return V?`${V} ${se}`:se});L.length>0&&$.push(N.gray(` Furthest behind: ${L.slice(0,5).join(" \xB7 ")}${L.length>5?" \u2026":""} (0% captured)`))}return $.push(...Tl(e),...Dl(e),...El(e),...Al(e)),$.push(""),$.push(Pl),$.push(N.gray(" Drill in: /screenshot list <chapter#> \xB7 /screenshot list --detail (all)")),$}function Sf(e,t,n){let o=new Set(e.map(i=>i.file)),r=e.filter(i=>i.state==="captured").length,s=[` ${N.bold("Screenshots")}: ${e.length} placeholder${e.length===1?"":"s"} across ${o.size} article${o.size===1?"":"s"} \xB7 ${N.green(`${r} captured`)}${n?N.gray(` \xB7 scope ${n}`):""}`,""];for(let[i,u]of Nl(e,t)){let p=u.filter(A=>A.state==="captured").length,{num:b,name:k}=Us(i),$=b?`${b} \xB7 ${k}`:k;s.push(` ${N.bold($)} ${N.gray(`(${p}/${u.length})`)}`);let x=new Map;for(let A of u){let v=vf(A.file),T=x.get(v)??[];x.has(v)||x.set(v,T),T.push(A)}for(let A of[...x.keys()].sort()){s.push(N.gray(` ${A}`));for(let v of x.get(A)){let T=wf[v.state];if(s.push(` ${ze(yf[v.state],T)}${T(v.id)}`),(v.state==="blocked"||v.state==="broken")&&v.reason)for(let D of Hn(v.reason,84))s.push(N.gray(` ${D}`))}}}return s.push(...Tl(e),...Dl(e),...El(e),...Al(e)),s.push("",Pl),s}function rr(e,t){let n=t?.scope;if(e.length===0)return[n?`No screenshot placeholders under ${n}.`:"No screenshot placeholders found in the docs."];let o=t?.docsDir??Hs;return t?.detail||n?Sf(e,o,n):Rf(e,o)}function Vs(e,t){if(!t||!/^\d+$/.test(t))return t;let n=hn(e)?.docsDir??Hs,o=String(Number(t));for(let{file:r}of bl(e)){let s=jl(r,n),i=/^(\d+)/.exec(s);if(i&&String(Number(i[1]))===o)return s}return t}function Pf(e){let t=e.replace(/\\/g,"/").split("/").filter(Boolean),n=t.pop()??e;return{category:t.pop()??"(docs root)",article:n}}function Qo(e,t,n){let o=new Map;for(let s of t){let{category:i,article:u}=Pf(s.file),p=o.get(i)??new Map;o.set(i,p);let b=p.get(u)??[];p.set(u,b),b.push(s)}let r=s=>n==="author"||!s.mutates;for(let s of[...o.keys()].sort()){let i=o.get(s),u=[...i.values()].flat().filter(r).map(p=>p.id);u.length>0&&e.push({kind:"category",action:n,label:`${s} (${u.length})`,ids:u,depth:1});for(let p of[...i.keys()].sort()){let b=i.get(p),k=b.filter(r).map($=>$.id);k.length>0&&e.push({kind:"article",action:n,label:`${p} (${k.length})`,ids:k,depth:2});for(let $ of b){let x=n==="capture"&&$.mutates?" \u26A0 creates real data \u2014 runs only alone":void 0;e.push({kind:"shot",action:n,label:$.id,ids:[$.id],depth:3,...x?{note:x}:{}})}}}}function kl(e,t=58){if(!e)return"";let n=e.replace(/\s+/g," ").trim();return n.length>t?`${n.slice(0,t-1)}\u2026`:n}function Gs(e,t,n){let o=t.filter(r=>!r.mutates);o.length>1&&e.push({kind:"all",action:"capture",label:`Retry ${o.length} blocked shots (stage the data first)`,ids:o.map(r=>r.id),depth:0});for(let r of[...t].sort((s,i)=>s.id.localeCompare(i.id)))e.push({kind:"shot",action:"capture",label:r.id,ids:[r.id],depth:1,note:` \u2298 ${kl(r.reason)}`});n.length>1&&e.push({kind:"all",action:"author",label:`Re-author ${n.length} broken specs`,ids:n.map(r=>r.id),depth:0});for(let r of[...n].sort((s,i)=>s.id.localeCompare(i.id)))e.push({kind:"shot",action:"author",label:r.id,ids:[r.id],depth:1,note:` \u2717 ${kl(r.reason)} \u2014 re-authors`})}function Il(e){let t=e.filter(s=>s.state==="placeholder"),n=e.filter(s=>s.state==="spec"),o=[];t.length>1&&o.push({kind:"all",action:"author",label:`Author all ${t.length} remaining specs`,ids:t.map(s=>s.id),depth:0}),Qo(o,t,"author");let r=n.filter(s=>!s.mutates);return r.length>1&&o.push({kind:"all",action:"capture",label:`Capture all ${r.length} ready specs`,ids:r.map(s=>s.id),depth:0}),Qo(o,n,"capture"),Gs(o,e.filter(s=>s.state==="blocked"),e.filter(s=>s.state==="broken")),o}var Fs=["MANDATORY \u2014 a missing prerequisite must SKIP, never time out. Two rules, applied to EVERY locator","in the spec, not just the first one or the one shown in the skill's example:","1. A bare `await x.waitFor(...)` is itself the bug: on a missing element it THROWS and the run"," reports a hard failure with a stack trace instead of a staging instruction. Every wait must be",' `await x.waitFor({ state: "visible", timeout: 10_000 }).catch(() => {});` followed by',' `if ((await x.count()) === 0) test.skip(true, "Prerequisite not met: \u2026")`. That applies to each'," element you wait on or click IN TURN \u2014 tabs, toggles, rows, buttons reached after the first"," guard \u2014 because any of them can be absent for the same reason the data is.",' And when a wait DOES time out, diagnose before you guard: a skip asserts "your app lacks this',' data", so if the real cause is a locator matching a node you did not intend (a rollup row, a'," sibling with the same testid), that assertion is a lie and sends the user to stage what they"," already have. Narrow the selector first, THEN guard."," Two corollaries, both of which have cost a wasted repair round:"," (a) An element that is not RENDERED is not absent. A collapsed tree node, an unopened accordion,"," an inactive tab, a virtualised list or page 2 of a table keeps its children out of the DOM."," EXPAND/scroll/switch to reach the target \u2014 that is a step to script, not data to stage."," (b) When you narrow past a decoy, count the UNNARROWED set too and put the number in the reason:",' "Found 6 folder roll-ups but no endpoint-level score \u2014 if one exists it may not be rendered".',' A bare "prerequisite not met" cannot tell absent data from an over-narrow selector. Put that'," evidence in a LATER sentence, never the first: grouping keys on the first sentence, so an",' interpolated count there makes every run its own "action" and the dedup stops working.'," (c) `click({ force: true })` is the ONE pattern that turns a failure into a silent WRONG"," SCREENSHOT rather than a crash or skip: it skips the actionability checks, so a click that"," lands on nothing photographs the unchanged screen and that image ships in the docs. It is"," sometimes right (a button nested inside a button reads as intercepted by its own ancestor)."," Never force blind: ground it in source, scrollIntoViewIfNeeded() first, and ASSERT THE"," CONSEQUENCE immediately after with a guarded wait + test.skip, so a no-op force skips."," Know its limit: force skips the CHECK but still hit-tests real screen coordinates, so the"," element that RECEIVES the event may not be the one your locator names (measured: a forced"," click on a pill nested in its section-header button never ran the pill's own handler).",' For a genuinely nested interactive node use `locator.dispatchEvent("click")`, which goes'," straight to that node with no hit-test \u2014 and still assert the consequence.","2. Never reason about whether a role/text locator is unique; you cannot verify that by reading"," source, and Playwright fails the whole spec on 2+ matches. Every `getByRole`/`getByText`/"," `:has-text` without a unique data-testid gets `.first()` or a container scope"," (`page.locator('[data-testid=\"\u2026\"]').getByRole(\u2026)`). If you catch yourself writing a comment"," ARGUING a selector is unique, scope it instead \u2014 that argument is what fails.","3. Every skip reason opens with the SHARED staging action, worded identically across every shot",' that needs the same thing, naming where in the product to do it ("Create an API key in Settings',' \u2192 API Keys."); shot-specific detail (counts, which record, which state) goes in LATER sentences.'," The dashboard groups blocked shots by that first sentence, so a near-duplicate wording splits one"," chore into two. Grep sibling specs and reuse their exact opening sentence.","Before you finish, re-read each spec you wrote and check all three rules line by line."].join(`
|
|
317
317
|
`);function Tf(e){return e.outcome==="failed"?["REPAIR \u2014 the existing spec FAILED its last capture with:",e.reason,'Read the existing spec first and fix that failure specifically. A "strict mode violation" means the',"locator matched 2+ elements \u2014 scope it to a stable container or take .first(); a click timeout means","the element was absent or unreachable \u2014 if that's because the data isn't staged, the spec must","test.skip with a reason naming what to stage, not time out. Re-verify every selector you keep","against the product source; do not re-emit the same locator and hope."]:["REPAIR \u2014 the existing spec SKIPPED its last capture with:",e.reason,"You are being asked to re-author a shot that SKIPPED, and that is itself the signal: the normal","response to a blocked shot is to STAGE the data, not to rewrite the spec. So treat that reason as","SUSPECT \u2014 assume it is a FALSE skip until you have proved otherwise. Two things make a guard report",'"not staged" when the data exists:',"1. THE TARGET IS NOT RENDERED. Trace the path from the entry point to the element you finally need"," and confirm every intermediate container is OPENED. A collapsed tree node, an unopened accordion,"," an inactive tab, a virtualised list or page 2 of a table keeps its children out of the DOM, so the"," count is zero and the skip fires \u2014 for data sitting one click away. Expanding is a step to SCRIPT.","2. THE SELECTOR IS TOO NARROW. If you filtered past a decoy, count the UNNARROWED set and put the",' number in the reason ("Found 6 folder roll-ups but no endpoint-level score"), so the next reader'," can tell absent data from an over-narrow selector without opening the app.","A spec that satisfies every rule in this skill and still cannot reach its target is NOT correct.",'"No changes needed" is earned by tracing reachability end to end \u2014 not by checking conformance.']}var Ws="MANDATORY, check before you finish: every spec you write or touch must carry exactly one `// CAPTURE-SAFETY: readonly` or `// CAPTURE-SAFETY: mutates \u2014 <what it creates>; cleanup: <how>` line at the top. Re-read each file you wrote and add it if missing \u2014 a spec without it is classified by guesswork. A fail-by-design spec (the app rejects the input) is `readonly`.";function Ol(e,t){let n=e.paths,o=n.filter(i=>t?.get(i)?.outcome==="failed"),r=n.filter(i=>t?.get(i)?.outcome==="skipped"),s=i=>`- ${i}: ${t.get(i).reason.replace(/\s+/g," ").trim()}`;return["Run the emit-screenshot-spec skill to author the document360-capture spec for EACH of these","SCREENSHOT placeholder ids:",e.paths.map(i=>`- ${i}`).join(`
|
|
318
318
|
`),"",...o.length>0?["REPAIR \u2014 these already have a spec, and it FAILED its last capture. Read the existing spec","first and fix the named failure specifically; do not re-emit the same locator and hope. A",'"strict mode violation" means the locator matched 2+ elements (scope it to a stable container',"or take .first()); a click timeout means the element was absent or unreachable (if the data","simply is not staged, the spec must test.skip with a reason naming what to stage, not time out).",...o.map(s),""]:[],...r.length>0?["REPAIR \u2014 these already have a spec and it SKIPPED. Treat each reason as SUSPECT, not as proof","the spec is right: a FALSE skip is indistinguishable from a true one from inside the spec, and","you are being asked to re-author rather than to stage. Check TWO things before concluding a","spec needs no change. (1) Is the target RENDERED? A collapsed tree node, unopened accordion,","inactive tab, virtualised list or page 2 of a table keeps children out of the DOM, so the count","is zero for data one click away \u2014 expanding is a step to SCRIPT. (2) Is the selector too narrow?","If you filtered past a decoy, count the unnarrowed set and put the number in the reason. A spec","that satisfies every rule and still cannot reach its target is NOT correct.",...r.map(s),""]:[],"For each: locate its <!-- SCREENSHOT --> block in the docs tree (the configured docsDir); read the product source for","EXACT routes + stable data-testid selectors (never guess); enter the prepared context via","captureScope(); guard data prerequisites with test.skip(reason); write <captureDir>/<id>.spec.ts","(skip-with-TODO if a stable selector is missing). Do NOT run other skills. Report the specs you","wrote and any TODO data-testids.",Fs,Ws].join(`
|
|
319
319
|
`)}async function sr(e,t,n){let o=qn(e);if(o.mode==="list"){let u=t?.cwd??process.cwd(),p=Vs(u,o.scope),b=hn(u)?.docsDir??Hs,k=Ke(u,p);for(let $ of rr(k,{scope:p,detail:o.detail,docsDir:b}))console.log($);return{kind:"continue"}}if(o.mode==="single")return{kind:"forward-to-agent",prompt:[`Run the emit-screenshot-spec skill for the SCREENSHOT placeholder with id \`${o.id}\`.`,"Locate its <!-- SCREENSHOT --> block in the docs tree (the configured docsDir), then follow that skill exactly: read the","product source for EXACT routes + stable data-testid selectors (never guess); enter the prepared","context via captureScope(); guard data prerequisites with test.skip(reason); write",`<captureDir>/${o.id}.spec.ts. Report per the skill's writer-facing format \u2014 spec path, data to`,"stage, dev requests only if any, and the capture command; no grounding narration.",Fs,Ws,...n?.prior?["",...Tf(n.prior)]:[]].join(`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "document360-writer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.166",
|
|
4
4
|
"description": "Standalone documentation agent CLI. Reads your code, writes your docs. Specialized for Document360 publishing.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@inquirer/prompts": "^8.4.3",
|
|
35
35
|
"commander": "^14.0.3",
|
|
36
36
|
"diff": "^8.0.4",
|
|
37
|
-
"document360-engine": "^0.2.
|
|
37
|
+
"document360-engine": "^0.2.163",
|
|
38
38
|
"ink": "^5.2.1",
|
|
39
39
|
"picocolors": "^1.1.1",
|
|
40
40
|
"react": "^18.3.1",
|