automated-qa 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/LICENSE +22 -0
  2. package/NOTICE +9 -0
  3. package/README.md +100 -0
  4. package/SKILL.md +65 -0
  5. package/agent/prebuilt/darwin-universal/qa-agent +0 -0
  6. package/agent/prebuilt/linux-arm64/qa-agent +0 -0
  7. package/agent/prebuilt/linux-x64/qa-agent +0 -0
  8. package/agent/release-manifest.json +31 -0
  9. package/bin/autonomous-cache.mjs +2 -0
  10. package/bin/bench-generate.mjs +232 -0
  11. package/bin/bench-graph-query.mjs +2 -0
  12. package/bin/bench-score.mjs +10 -0
  13. package/bin/classify.mjs +15 -0
  14. package/bin/coherence-playwright.mjs +1 -0
  15. package/bin/coherence-runner.mjs +6 -0
  16. package/bin/crud-runner.mjs +12 -0
  17. package/bin/experiment-executor.mjs +10 -0
  18. package/bin/experiment-runner.mjs +3 -0
  19. package/bin/inventory.mjs +17 -0
  20. package/bin/probe.js +879 -0
  21. package/bin/qa.mjs +16 -0
  22. package/bin/rules.mjs +5 -0
  23. package/bin/run-benchmarks.mjs +2 -0
  24. package/bin/runtime-packs.mjs +3 -0
  25. package/bin/semantic-explorer.mjs +3 -0
  26. package/bin/shadow-collector.mjs +3 -0
  27. package/bin/shadow-replay.mjs +6 -0
  28. package/bin/soak-marathon.mjs +15 -0
  29. package/bin/sweep.mjs +43 -0
  30. package/bin/validate-priors.mjs +3 -0
  31. package/bin/verify-artifacts.mjs +3 -0
  32. package/bin/verify-manifest.mjs +2 -0
  33. package/install-skill.sh +45 -0
  34. package/lib/qa/agent-availability.mjs +1 -0
  35. package/lib/qa/agent-result.mjs +1 -0
  36. package/lib/qa/analysis.mjs +5 -0
  37. package/lib/qa/atlas-coverage.mjs +11 -0
  38. package/lib/qa/atlas-v1.mjs +1 -0
  39. package/lib/qa/atlas.mjs +2 -0
  40. package/lib/qa/auth/coordinator.mjs +1 -0
  41. package/lib/qa/auth/launcher.mjs +3 -0
  42. package/lib/qa/auth/lease.mjs +1 -0
  43. package/lib/qa/auth/oracle.mjs +1 -0
  44. package/lib/qa/auth/personas.mjs +3 -0
  45. package/lib/qa/autonomous-cache.mjs +3 -0
  46. package/lib/qa/block-ledger.mjs +1 -0
  47. package/lib/qa/capture-report.mjs +63 -0
  48. package/lib/qa/ci.mjs +11 -0
  49. package/lib/qa/cli.mjs +538 -0
  50. package/lib/qa/coherence/analyze.mjs +1 -0
  51. package/lib/qa/coherence/cache-key.mjs +6 -0
  52. package/lib/qa/coherence/causes.mjs +1 -0
  53. package/lib/qa/coherence/concurrency.mjs +1 -0
  54. package/lib/qa/coherence/contracts.mjs +1 -0
  55. package/lib/qa/coherence/entity-aliases.mjs +1 -0
  56. package/lib/qa/coherence/enums.mjs +1 -0
  57. package/lib/qa/coherence/family-report.mjs +1 -0
  58. package/lib/qa/coherence/flow-pool.mjs +1 -0
  59. package/lib/qa/coherence/graph.mjs +1 -0
  60. package/lib/qa/coherence/identity.mjs +1 -0
  61. package/lib/qa/coherence/ir.mjs +4 -0
  62. package/lib/qa/coherence/navigation.mjs +1 -0
  63. package/lib/qa/coherence/observe.mjs +2 -0
  64. package/lib/qa/coherence/offline.mjs +1 -0
  65. package/lib/qa/coherence/plan-join.mjs +1 -0
  66. package/lib/qa/coherence/provenance.mjs +2 -0
  67. package/lib/qa/coherence/report.mjs +1 -0
  68. package/lib/qa/coherence/representations.mjs +1 -0
  69. package/lib/qa/coherence/risk.mjs +2 -0
  70. package/lib/qa/coherence/structural-js.mjs +11 -0
  71. package/lib/qa/coherence/symbols.mjs +6 -0
  72. package/lib/qa/coherence/tanstack.mjs +4 -0
  73. package/lib/qa/coherence/views.mjs +1 -0
  74. package/lib/qa/coherence-planner.mjs +1 -0
  75. package/lib/qa/command-catalog.mjs +2 -0
  76. package/lib/qa/commands/dev.mjs +1 -0
  77. package/lib/qa/commands/fix.mjs +1 -0
  78. package/lib/qa/commands/impl/analysis.mjs +1 -0
  79. package/lib/qa/commands/impl/atlas.mjs +1 -0
  80. package/lib/qa/commands/impl/coherence.mjs +1 -0
  81. package/lib/qa/commands/impl/crud.mjs +2 -0
  82. package/lib/qa/commands/impl/doctor.mjs +1 -0
  83. package/lib/qa/commands/impl/experiments.mjs +1 -0
  84. package/lib/qa/commands/impl/fp.mjs +1 -0
  85. package/lib/qa/commands/impl/graph.mjs +1 -0
  86. package/lib/qa/commands/impl/invariants.mjs +1 -0
  87. package/lib/qa/commands/impl/lanes.mjs +7 -0
  88. package/lib/qa/commands/impl/ledger.mjs +11 -0
  89. package/lib/qa/commands/impl/loop.mjs +1 -0
  90. package/lib/qa/commands/impl/marathon.mjs +1 -0
  91. package/lib/qa/commands/impl/next.mjs +38 -0
  92. package/lib/qa/commands/impl/report.mjs +4 -0
  93. package/lib/qa/commands/impl/research.mjs +4 -0
  94. package/lib/qa/commands/impl/selftest.mjs +1 -0
  95. package/lib/qa/commands/impl/serve.mjs +1 -0
  96. package/lib/qa/commands/impl/shadow.mjs +1 -0
  97. package/lib/qa/commands/impl/stability.mjs +1 -0
  98. package/lib/qa/commands/impl/triage.mjs +1 -0
  99. package/lib/qa/commands/impl/validate.mjs +4 -0
  100. package/lib/qa/commands/impl/version.mjs +1 -0
  101. package/lib/qa/commands/impl/watch.mjs +1 -0
  102. package/lib/qa/commands/init.mjs +6 -0
  103. package/lib/qa/commands/measure.mjs +1 -0
  104. package/lib/qa/commands/run.mjs +1 -0
  105. package/lib/qa/commands/show.mjs +1 -0
  106. package/lib/qa/commands/verify.mjs +6 -0
  107. package/lib/qa/deepening.mjs +1 -0
  108. package/lib/qa/denominators.mjs +1 -0
  109. package/lib/qa/depth.mjs +4 -0
  110. package/lib/qa/doctor.mjs +2 -0
  111. package/lib/qa/experiments-default.mjs +1 -0
  112. package/lib/qa/fix-lanes.mjs +3 -0
  113. package/lib/qa/forced-states.mjs +1 -0
  114. package/lib/qa/fp.mjs +2 -0
  115. package/lib/qa/fs.mjs +6 -0
  116. package/lib/qa/graph-schema.mjs +2 -0
  117. package/lib/qa/graph.mjs +3 -0
  118. package/lib/qa/harness-plan.mjs +3 -0
  119. package/lib/qa/init.mjs +5 -0
  120. package/lib/qa/integrity.mjs +1 -0
  121. package/lib/qa/invariants.mjs +84 -0
  122. package/lib/qa/jcs.mjs +1 -0
  123. package/lib/qa/lanes.mjs +1 -0
  124. package/lib/qa/loop.mjs +7 -0
  125. package/lib/qa/marathon-coherence.mjs +1 -0
  126. package/lib/qa/marathon-phases.mjs +1 -0
  127. package/lib/qa/marathon-wait.mjs +3 -0
  128. package/lib/qa/marathon.mjs +5 -0
  129. package/lib/qa/mode-coverage.mjs +1 -0
  130. package/lib/qa/mutation-policy.mjs +1 -0
  131. package/lib/qa/pattern-ledger.mjs +3 -0
  132. package/lib/qa/pending.mjs +1 -0
  133. package/lib/qa/proc.mjs +4 -0
  134. package/lib/qa/recommend.mjs +1 -0
  135. package/lib/qa/release-manifest.mjs +1 -0
  136. package/lib/qa/report-html.mjs +178 -0
  137. package/lib/qa/reports.mjs +2 -0
  138. package/lib/qa/request-evidence.mjs +1 -0
  139. package/lib/qa/research.mjs +20 -0
  140. package/lib/qa/routing.mjs +1 -0
  141. package/lib/qa/runs.mjs +1 -0
  142. package/lib/qa/schema.mjs +1 -0
  143. package/lib/qa/serve.mjs +4 -0
  144. package/lib/qa/server.mjs +1 -0
  145. package/lib/qa/source-snapshot.mjs +3 -0
  146. package/lib/qa/stability.mjs +3 -0
  147. package/lib/qa/state-evidence.mjs +1 -0
  148. package/lib/qa/sweep-incremental.mjs +1 -0
  149. package/lib/qa/triage.mjs +2 -0
  150. package/lib/qa/verdict.mjs +1 -0
  151. package/lib/qa/watch.mjs +2 -0
  152. package/package.json +62 -0
  153. package/references/ATLAS-COVERAGE.md +231 -0
  154. package/references/atlas-v1.md +318 -0
  155. package/references/atlas.md +338 -0
  156. package/references/verify.crud.example.json +1 -0
  157. package/references/verify.impact.example.json +1 -0
  158. package/references/verify.shadow.example.json +1 -0
  159. package/schemas/agent-result.schema.json +135 -0
  160. package/schemas/analysis.schema.json +15 -0
  161. package/schemas/classify.schema.json +61 -0
  162. package/schemas/coherence-ir.schema.json +1585 -0
  163. package/schemas/coherence-observed.schema.json +1171 -0
  164. package/schemas/coherence-plan.schema.json +77 -0
  165. package/schemas/coherence-risk.schema.json +46 -0
  166. package/schemas/crud-observed.schema.json +118 -0
  167. package/schemas/experiment-execution.schema.json +82 -0
  168. package/schemas/graph-edge.schema.json +22 -0
  169. package/schemas/graph-node.schema.json +81 -0
  170. package/schemas/graph.schema.json +2493 -0
  171. package/schemas/identity-oracle.schema.json +19 -0
  172. package/schemas/inventory.schema.json +2167 -0
  173. package/schemas/lanes-plan.schema.json +52 -0
  174. package/schemas/next.schema.json +54 -0
  175. package/schemas/personas.schema.json +52 -0
  176. package/schemas/research.schema.json +192 -0
  177. package/schemas/run.schema.json +160 -0
  178. package/schemas/shadow-replay.schema.json +68 -0
  179. package/schemas/stability.schema.json +92 -0
  180. package/schemas/sweep.schema.json +239 -0
  181. package/schemas/v1/agent-result.schema.json +106 -0
  182. package/schemas/v1/run.schema.json +96 -0
  183. package/schemas/verify-crud.schema.json +37 -0
  184. package/schemas/verify-read-only.schema.json +37 -0
  185. package/templates/github-workflow.yml +80 -0
  186. package/templates/pre-commit.sample +22 -0
  187. package/verify.sh +385 -0
@@ -0,0 +1,2 @@
1
+ import y from"node:fs";import b from"node:path";import{KIND_ATLAS_V1 as i,ALSO_COVERS_V1 as o,PREFIX_V1 as d}from"./atlas-v1.mjs";const h=Object.freeze([1,2]),f=1,v=Object.freeze({2:3,3:4,4:5});function s(t){if(t==="L8.81")return"L9.93";const e=t.match(/^L(\d+)\.(\d+)$/);if(!e)throw new Error(`not a v1 coordinate: ${t}`);const r=Number(e[1]);return`L${v[r]??r}.${e[2]}`}const w=Object.freeze({3:2,4:3});function _(t){if(t==="L9.93")return"L8.81";const e=t.match(/^L(\d+)\.(\d+)$/);if(!e)throw new Error(`not a v2 coordinate: ${t}`);const r=Number(e[1]),a=Number(e[2]);return r===2?null:r===5&&a<=45?`L4.${a}`:`L${w[r]??r}.${a}`}const g=Object.freeze({"a11y.label-association-broken":"L9.88","a11y.aria-attr-not-allowed":"L9.94","coherence.no-invalidation":"L4.23","coherence.partial-invalidation":"L4.24","coherence.optimistic-not-reconciled":"L4.25","coherence.sibling-divergence":"L4.27","coherence.cross-context-divergence":"L4.30","coherence.offline-reconnect-divergence":"L4.31","coherence.stale-after-write":"L4.35","coherence.refetched-stale":"L4.35","coherence.date-mismatch":"L5.52","coherence.money-mismatch":"L5.53","coherence.locale-mismatch":"L5.54","coherence.enum-mismatch":"L5.57","coherence.history-divergence":"L6.61","coherence.deep-link-divergence":"L6.62","coherence.permission-action-mismatch":"L7.70","coherence.cross-principal-divergence":"L7.73","coherence.no-invalidation-candidate":"L4.23","coherence.partial-invalidation-candidate":"L4.24","coherence.write-not-durable":s(i["mutate.write-lost"]),"coherence.representation-divergence":"L5.54","coherence.missing-required-summary":"L5.36","coherence.offline-replay-divergence":"L4.31","crud.create-failed":"L6.66","crud.write-lost":"L4.25","crud.update-lost":"L4.35","crud.delete-ineffective":"L4.33","crud.unauthorized-allowed":"L7.70","crud.cleanup-failed":"L11.112","invariant.violated":"L2.2","shadow.drift":"L2.2"}),k=Object.freeze({...Object.fromEntries(Object.entries(i).map(([t,e])=>[t,s(e)])),...g}),L=Object.freeze(Object.fromEntries(Object.entries(o).map(([t,e])=>[t,e.map(s)]))),x=Object.freeze(d.map(([t,e])=>[t,s(e)])),A=Object.freeze({1:{kinds:i,also:o,prefixes:d},2:{kinds:k,also:L,prefixes:x}});function S(t){const e=t?.atlasVersion;if(e==null)return f;if(!h.includes(e))throw new Error(`unknown atlasVersion ${JSON.stringify(e)}; known versions are ${h.join(", ")}`);return e}function V(t){if(t?.atlas)return t.atlas;const e=A[S(t)],r=t?.kind??"";if(e.kinds[r])return e.kinds[r];for(const[a,l]of e.prefixes)if(r.startsWith(a))return l;return null}const E=i,z=o,c=Object.freeze({"coherence.no-invalidation":"Invalidate every declared reader key after the mutation; do not rely on a route revisit to hide stale cache state.","coherence.partial-invalidation":"The invalidation covers only part of the declared reader set; include each exact or declared-prefix reader key.","coherence.stale-after-write":"The authoritative value changed while a declared observer stayed stale; repair the shared cache reconciliation path.","coherence.refetched-stale":"A refetch occurred but returned a stale projection; inspect cache and server projection/versioning rather than counting the refetch as proof.","coherence.cross-principal-divergence":"Declared principals disagree with authoritative truth; verify visibility and projection rules for each persona.","sync.no-invalidation":"Invalidate the readers' query key in the mutation's onSuccess (or refetch on navigation). Fix it in the shared hook, not at each call site.","sync.partial-invalidation":"The invalidated key does not cover what was written; invalidate the key the readers actually query under.","sync.stale-after-write":"Proved live: after the write the blast-radius route showed the old value under client-side navigation. Same fix as no-invalidation.","mutate.write-lost":"The write returned 2xx but did not survive a reload; check the handler actually persists (a catch-all mock returning ok discards data).","value.leak":"A value rendered as undefined/NaN/[object Object]/Invalid Date; guard the formatter at the shared component, not the page.","render.stuck-loading":"A loading indicator stayed mounted after settle; the branch that clears it never ran (error path, empty path, or a Suspense boundary swallowing an error).","render.empty-list-no-state":"Render an explicit empty state so an empty list and a failed fetch are distinguishable.","data.rendered-zero-of-n":"The API returned records and no list rendered any: a selector/shape mismatch between response and renderer.","verify.no-data-traffic":"Query hooks are reachable from this route and none fired: enabled:false, a conditional hook, or a provider missing above the route.","state.silent-failure":"Read isError/error from the query and render it; silent failure is the class users act on.","state.no-empty-state":"Render an empty state when the response is an empty collection.","state.stuck-spinner":"Clear the loading state on error, not only on success.","state.blank":"Render an error surface when the request fails; a blank page is indistinguishable from a broken app.","a11y.unlabeled-control":"Give the control an accessible name (visible text, aria-label, or a <label for>).","a11y.label-association-broken":"Two controls share one id, so the visible label resolves to the hidden copy. Make the id unique per rendered copy (or render one copy) -- a placeholder is not a name.","a11y.aria-attr-not-allowed":"The role does not support this ARIA state, so assistive technology drops it. Drop the attribute, or give the element the role that supports it (aria-selected needs tab/option/row/gridcell/treeitem; aria-pressed needs button).","a11y.tap-target":"Raise the interactive area to at least 24px, or exclude it if it is screen-reader-only by design.","layout.h-scroll":'Something is wider than the viewport at this width; find it with document.querySelectorAll("*") vs innerWidth.',"interact.click-occluded":"An element covers the control's centre; clicks land on the occluder.","console.error":"Zero console errors is the invariant; read the message, it names the component.","network.http-":"An unexpected 4xx/5xx on this route; P2 if the page handled it, 5xx is always a defect.","perf.lcp":"Only meaningful with --prod; against a dev server this measures the compiler.","route.flaky":"First visit failed, retry passed. Intermittent; confirm with qa verify --runs N before treating as clean.","route.not-found-shell":"A dynamic segment needs a seeded id; cover it with a journey that creates the record.","journey.failed":"The scripted flow threw; the failure class says whether it was a selector, a timeout, or the app.","missing-empty-state":"Add the empty branch beside the loading and error branches.","index-as-list-key":"Use a stable id as the key; index keys may misassociate local row state or focus when a list reorders. Static analysis does not prove that impact.","submit-without-pending-guard":"Disable the submit while the mutation is pending, or the user files it twice.","unread-query-error":"Read `isError`/`error`/`status` from the bound query result and render a failure branch. Not unread: a returned result, a destructured error field, a result passed whole to a helper that reads it, or a Suspense query (its failure throws to the nearest boundary).","unsanitized-html":"Sanitize before dangerouslySetInnerHTML, unless the content is CSS text (shadcn chart.tsx), which is a known false positive: waive with proof.","unsanitized-markdown-raw-html":"Add rehype-sanitize after rehype-raw in the plugin chain, or drop rehype-raw if the content never needs raw HTML.","cache-key-collision":"Give the two queries distinct keys, or intentionally merge them into one query if they really are the same resource.","conditional-hook-call":"Move the hook to the top of the function and make ITS ARGUMENT conditional instead, or branch inside the hook body/effect, not around the hook call.","async-click-without-feedback":"Disable the control (or show a pending state) for the duration of the mutation so a second click cannot fire it again.","unsaved-changes-guard-missing":"Gate navigation with a beforeunload handler or a router-level prompt/blocker while the form is dirty.","runtime-env-in-client-code":"Build-time env is inlined at build; a value set at runtime never reaches the browser.","server-state-in-client-store":"Server entities copied into a client store are a second source of truth the cache cannot invalidate.","effect-fetch-without-abort":"Pass an AbortSignal and abort in the cleanup, or a late response overwrites a newer one.","stale-closure":"The effect reads state captured at mount; include it in deps or read through a ref.","render.broken-image":"The src 404s or the asset is gone; provide a fallback image or a reserved-size placeholder.","render.virtualized-empty":"A virtualised list rendered zero rows at this height; check the row-height/overscan math for this viewport.","layout.truncated-no-title":"Add text-overflow:ellipsis and a title or aria-label so truncated content is still readable.","layout.fixed-height-clip":"The fixed height clips real content; let it grow or scroll instead of hiding the overflow.","interact.disabled-not-conveyed":"The control is styled as disabled but has neither the disabled attribute nor aria-disabled -- it still receives clicks.","value.raw-enum":"Map the raw enum value to a display label before rendering it.","a11y.no-focus-ring":"Restore a visible focus indicator (outline or box-shadow) instead of removing it outright.","a11y.keyboard-trap":"Focus is stuck cycling between too few elements; make sure Tab/Shift+Tab can leave this region.","a11y.focus-order":"Tab order jumps against the visual layout; align the DOM order (or explicit tabindex) with reading order.","a11y.contrast":"Raise the foreground/background contrast to the WCAG threshold for this text size and weight.","layout.clipped-text":"Content is wider/taller than its clipping ancestor; resize the container or let the text wrap."});function R(t){const e=t.kind??"";if(c[e])return c[e];const r=Object.keys(c).find(a=>a.endsWith("-")&&e.startsWith(a));return r?c[r]:null}const u=new Map,p=Object.freeze({1:"atlas-v1.md",2:"atlas.md"});function N(t,e,r=f){if(!e)return null;if(!p[r])throw new Error(`unknown atlasVersion ${JSON.stringify(r)}; known versions are ${h.join(", ")}`);if(!u.has(r)){const a=new Map;try{const l=y.readFileSync(b.join(t,"references",p[r]),"utf8");for(const m of l.split(`
2
+ `)){const n=m.match(/^\|\s*(\d+\.\d+)\s*\|\s*(.+?)\s*\|\s*([A-Z+]+)\s*\|\s*$/);n&&a.set("L"+n[1],{id:"L"+n[1],text:n[2].replace(/\*\*/g,""),detection:n[3]})}}catch{}u.set(r,a)}return u.get(r).get(e)??null}export{z as ALSO_COVERS,o as ALSO_COVERS_V1,L as ALSO_COVERS_V2,h as ATLAS_VERSIONS,f as DEFAULT_ATLAS_VERSION,c as HINTS,E as KIND_ATLAS,i as KIND_ATLAS_V1,k as KIND_ATLAS_V2,d as PREFIX_V1,x as PREFIX_V2,N as atlasEntry,V as atlasIdFor,S as atlasVersionOf,R as hintFor,s as remapV1ToV2,_ as remapV2ToV1};
@@ -0,0 +1 @@
1
+ import f from"node:fs";import h from"node:path";import{setTimeout as X}from"node:timers/promises";import{acquireLease as Z,canResume as J}from"./lease.mjs";import{launchPersona as ee,validateSessionSidecar as te}from"./launcher.mjs";import{comparePersona as D,probeIdentity as se,validatePersonaHeaders as ne,IDENTITY_ADAPTER as re}from"./oracle.mjs";const L="coordinator.json",ae=3e4;function R(n){try{return(f.statSync(n).mode&511)!==384?null:JSON.parse(f.readFileSync(n,"utf8"))}catch{return null}}function oe(n,a,s){const c=h.join(n,L);f.writeFileSync(c,JSON.stringify({signature:a,entries:s}),{mode:384}),f.chmodSync(c,384)}function ie(n,a){const s=R(h.join(n,L));return s&&J(s.signature,a)&&s.entries&&typeof s.entries=="object"?s.entries:{}}function F(n,a){const s=h.join(n,a);try{return h.dirname(f.realpathSync(s))===f.realpathSync(n)&&(f.statSync(s).mode&511)===384?s:null}catch{return null}}function b(n,a,s){if(!f.existsSync(h.join(n,a)))return{ok:!0,value:null};const c=F(n,a);if(!c)return{ok:!1,reason:`${s}-not-private`};const i=R(c);return i===null?{ok:!1,reason:`${s}-unreadable`}:{ok:!0,value:i}}function q(n,a,s){const c=b(n,`${a}.headers.json`,"persona-headers");if(!c.ok)return c;const i=b(n,`${a}.session.json`,"persona-session");if(!i.ok)return i;const y=ne(c.value);if(!y.ok)return y;const v=te(i.value,{origin:s});return v.ok?{ok:!0,headers:y.headers,session:v.session}:v}function he({repo:n,runId:a,signature:s,launcherPath:c,base:i,personas:y,oraclePath:v="/__qa/identity",lease:A=null,maxAttempts:j=3,maxRetryAfterMs:B=5e3,timeoutMs:C=6e4,oracleRequired:N=!0,launch:U=ee,probe:E=se,sleep:Y=X}){const z=!A,o=A??Z(n,{runId:a,signature:s});if(!o?.dir||!J(o.signature,s))throw new Error("auth-lease-signature-mismatch");if((f.statSync(o.dir).mode&511)!==448)throw new Error("auth-lease-permissions-invalid");let _=null;try{_=new URL(i).origin}catch{}const T=h.join(h.resolve(n),re),w=new Map((y??[]).map(t=>[t.name,t])),g=ie(o.dir,s),k=new Map,S=new Map,I=()=>{try{f.utimesSync(o.dir,new Date,new Date)}catch{}},O=setInterval(I,ae);O.unref?.();async function G(t,m){const u=typeof g[t]=="string"?F(o.dir,g[t]):null;if(!u)return null;const l=await E({base:i,oraclePath:v,statePath:u,headers:m.headers,adapterPath:T});if(l.status!=="measured")return null;const e=w.get(t);return!e||D(e,l.response).status!=="valid"?null:{statePath:u,headers:m.headers,session:m.session,response:l.response,reused:!0,attempts:0}}async function K(t){if(!w.has(t))return{status:"invalid",reason:"persona-not-declared"};if(S.has(t))return S.get(t);const m=w.get(t);if(m.expect.provisioning==="anonymous"){const e=await E({base:i,oraclePath:v,statePath:null,headers:null,adapterPath:T});if(e.status==="invalid"||N&&e.status!=="measured")return{status:"invalid",reason:`identity-${e.reason??e.status}`};if((e.status==="measured"?D(m,e.response):null)?.status!=="valid")return{status:"invalid",reason:"identity-policy-mismatch"};const d={status:"measured",statePath:null,headers:null,session:null,response:e.response,reused:!1,attempts:0};return S.set(t,d),d}const u=q(o.dir,t,_);if(!u.ok)return{status:"invalid",reason:u.reason};const l=await G(t,u);if(l){const e={status:"measured",...l};return S.set(t,e),e}for(let e=1;e<=j;e++){const r=await U({launcherPath:c,persona:t,leaseDir:o.dir,base:i,timeoutMs:C});if(r.status==="transient"&&r.reason==="launcher-http-429"&&e<j){await Y(Math.min(r.retryAfterMs,B));continue}if(r.status!=="measured")return{...r,attempts:e};const d=q(o.dir,t,_);if(!d.ok)return{status:"invalid",reason:d.reason,attempts:e};const p=await E({base:i,oraclePath:v,statePath:r.statePath,headers:d.headers,adapterPath:T});if(p.status==="invalid"||N&&p.status!=="measured")return{status:"invalid",reason:`identity-${p.reason??p.status}`,attempts:e};const P=F(o.dir,h.basename(r.statePath));if(!P)return{status:"invalid",reason:"launcher-state-outside-private-lease",attempts:e};g[t]=h.basename(P),oe(o.dir,s,g),I();const M={status:"measured",statePath:P,headers:d.headers,session:d.session,response:p.status==="measured"?p.response:null,reused:!1,attempts:e};return S.set(t,M),M}return{status:"invalid",reason:"launcher-retry-exhausted",attempts:j}}async function Q(t){return k.has(t)||k.set(t,K(t).finally(()=>k.delete(t))),k.get(t)}async function V(t){I();const m=[...new Set(t??[])],u=new Map;for(const e of m){const r=await Q(e);if(r.status!=="measured")return{status:r.status,reason:r.reason,persona:e,attempts:r.attempts,states:u};u.set(e,r)}const l=new Map;for(const[e,r]of u){const d=w.get(e);let p=null;if(r.response){const W=[...u].filter(([$,x])=>$!==e&&x.response).map(([$,x])=>({persona:w.get($),response:x.response})),H=D(d,r.response,{peers:W});if(H.status!=="valid")return{status:"invalid",reason:"identity-policy-mismatch",persona:e,states:l};p=H.archivable}const{response:P,...M}=r;l.set(e,{...M,archivable:p})}return{status:"measured",reason:null,states:l}}return{descriptor:{dir:o.dir,signature:s},provision:V,release(){clearInterval(O),z&&o.release()}}}export{he as createPersonaCoordinator};
@@ -0,0 +1,3 @@
1
+ import u from"node:fs";import c from"node:path";import{run as l}from"../proc.mjs";const p=/PASS|SECRET|TOKEN|KEY|CREDENTIAL/i,y=/(pass|secret|token|key|credential)\s*[:=]/i;function d(s={}){const o={};for(const[n,r]of Object.entries(process.env))p.test(n)||(o[n]=r);return{...o,...s}}function m(s){if(!s)return;const o=s.split(`
2
+ `).map(n=>y.test(n)?"<redacted-line>":n).join(`
3
+ `);process.stderr.write(o)}function v(s,{origin:o}){if(s==null)return{ok:!0,session:null};if(typeof s!="object"||Array.isArray(s))return{ok:!1,reason:"persona-session-not-an-object"};for(const r of Object.keys(s))if(r!=="origins")return{ok:!1,reason:`persona-session-unknown-key:${r}`};if(!Array.isArray(s.origins))return{ok:!1,reason:"persona-session-origins-not-an-array"};const n=[];for(const r of s.origins){if(r===null||typeof r!="object"||Array.isArray(r))return{ok:!1,reason:"persona-session-entry-not-an-object"};for(const e of Object.keys(r))if(e!=="origin"&&e!=="sessionStorage")return{ok:!1,reason:`persona-session-unknown-key:${e}`};if(typeof r.origin!="string")return{ok:!1,reason:"persona-session-origin-not-a-string"};if(!Array.isArray(r.sessionStorage))return{ok:!1,reason:"persona-session-storage-not-an-array"};let i;try{i=new URL(r.origin).origin}catch{return{ok:!1,reason:"persona-session-origin-invalid"}}for(const e of r.sessionStorage){if(e===null||typeof e!="object"||Array.isArray(e))return{ok:!1,reason:"persona-session-item-not-an-object"};for(const t of Object.keys(e))if(t!=="name"&&t!=="value")return{ok:!1,reason:`persona-session-unknown-key:${t}`};if(typeof e.name!="string"||!e.name)return{ok:!1,reason:"persona-session-item-name-invalid"};if(typeof e.value!="string")return{ok:!1,reason:"persona-session-item-value-invalid"};i===o&&n.push([e.name,e.value])}}return n.length?{ok:!0,session:{origin:o,items:n}}:{ok:!1,reason:"persona-session-nothing-for-target-origin"}}function h(s,o){return o?.items?.length?s.addInitScript(({origin:n,items:r})=>{try{if(location.origin!==n)return;for(const[i,e]of r)window.sessionStorage.getItem(i)===null&&window.sessionStorage.setItem(i,e)}catch{}},o):Promise.resolve()}async function A({launcherPath:s,persona:o,leaseDir:n,base:r=null,timeoutMs:i=3e4}){if(!u.existsSync(s))return{status:"unmeasured",reason:"no-launcher"};const e=await l(process.execPath,[s,o],{env:d(r?{QA_BASE:r}:{}),replaceEnv:!0,fd3:JSON.stringify({leaseDir:n}),stdout:"capture",stderr:"capture",timeoutMs:i});if(m(e.stderr),e.timedOut)return{status:"invalid",reason:"launcher-timeout"};let t;try{t=JSON.parse(String(e.stdout).trim())}catch{return{status:"invalid",reason:e.code===0?"launcher-bad-status":"launcher-exit"}}if(t?.ok===!1&&t.status===429&&Number.isFinite(t.retryAfterMs)&&t.retryAfterMs>=0)return{status:"transient",reason:"launcher-http-429",retryAfterMs:Math.min(t.retryAfterMs,3e4)};if(e.code!==0)return{status:"invalid",reason:"launcher-exit"};if(!t||t.ok!==!0||typeof t.stateFile!="string"||!t.stateFile)return{status:"invalid",reason:"launcher-bad-status"};const a=c.join(n,t.stateFile);if(!u.existsSync(a))return{status:"invalid",reason:"launcher-bad-status"};const f=u.statSync(a).mode&511;return f!==384?{status:"invalid",reason:`launcher-loose-permissions: state file must be 0600, was 0${f.toString(8)}`}:{status:"measured",statePath:a}}export{p as SECRET_ENV_NAME,A as launchPersona,h as restoreSessionStorage,v as validateSessionSidecar};
@@ -0,0 +1 @@
1
+ import c from"node:fs";import l from"node:os";import f from"node:path";import h from"node:crypto";import{InvalidError as m}from"../cli.mjs";function y(){return typeof process.getuid=="function"?String(process.getuid()):"nouid"}function p(){return f.join(l.tmpdir(),`qa-lease-${y()}`)}function I(e,{runId:r,signature:n}){const t=f.join(p(),r);c.mkdirSync(t,{recursive:!0,mode:448}),c.chmodSync(t,448);let i=!1;const o=()=>{try{c.rmSync(t,{recursive:!0,force:!0})}catch{}},s=()=>o();return process.on("exit",o),process.on("SIGINT",s),process.on("SIGTERM",s),{dir:t,signature:n,statePath(u){return f.join(t,`${u}.json`)},release(){i||(i=!0,process.off("exit",o),process.off("SIGINT",s),process.off("SIGTERM",s),o())}}}function S({lane:e,targetIdentity:r,sourceIdentity:n,mode:t,routes:i,widths:o,policy:s}){return JSON.stringify({lane:e,mode:t,policy:s,sourceIdentity:n,targetIdentity:r,routes:[...i??[]].sort(),widths:[...o??[]].sort((u,a)=>u-a)})}function T(e){return h.createHash("sha256").update(S(e)).digest("hex")}function M(e,r){return e===r}function N({olderThanMs:e,now:r=Date.now()}){const n=p(),t=[],i=[];let o;try{o=c.readdirSync(n,{withFileTypes:!0})}catch{return{removed:t,kept:i}}for(const s of o){if(!s.isDirectory())continue;const u=f.join(n,s.name);let a;try{a=c.statSync(u).mtimeMs}catch{continue}r-a>e?(c.rmSync(u,{recursive:!0,force:!0}),t.push(u)):i.push(u)}return{removed:t,kept:i}}const d=new Set(["verify","measure crud","measure stability","measure experiments"]);function b(e){return d.has(e.slice(0,2).join(" "))||d.has(e[0])}function D(e){if(e==null)return null;const r=String(e);if(!/^\d+$/.test(r)||Number(r)<3)throw new m("--auth-lease-fd must name an inherited descriptor >= 3");let n;try{n=c.readFileSync(Number(r),"utf8")}catch{throw new m("--auth-lease-fd could not be read")}if(!n)throw new m("--auth-lease-fd is empty");return n}export{d as AUTH_LEASE_COMMANDS,I as acquireLease,M as canResume,b as consumesAuthLease,N as janitor,D as readBorrowedLease,T as resumeSignature};
@@ -0,0 +1 @@
1
+ import v from"node:fs";import{pathToFileURL as x}from"node:url";const j=Object.freeze(["authenticated","authenticationSubject","effectivePrincipal","delegationContext","globalRole","projectRoles","fixtureNamespace","sessionActive"]),c=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),R=/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/,A=/^[\t\x20-\x7e\x80-\xff]*$/;function k(e){if(e==null)return{ok:!0,headers:null};if(typeof e!="object"||Array.isArray(e))return{ok:!1,reason:"persona-headers-not-an-object"};const t={};for(const[n,o]of Object.entries(e)){if(!R.test(n))return{ok:!1,reason:"persona-headers-invalid-name"};if(n.toLowerCase()==="cookie")return{ok:!1,reason:"persona-headers-cookie-not-allowed"};if(typeof o!="string"||!A.test(o))return{ok:!1,reason:`persona-headers-invalid-value:${n}`};t[n]=o}return{ok:!0,headers:Object.keys(t).length?t:null}}function b(e){if(e===null||typeof e!="object"||Array.isArray(e))return{ok:!1,problems:["oracle response must be a JSON object"]};const t=[];for(const n of j)n!=="delegationContext"&&(c(e,n)||t.push(`oracle response missing required field '${n}'`));return c(e,"authenticated")&&typeof e.authenticated!="boolean"&&t.push("'authenticated' must be a boolean"),c(e,"sessionActive")&&typeof e.sessionActive!="boolean"&&t.push("'sessionActive' must be a boolean"),c(e,"authenticationSubject")&&typeof e.authenticationSubject!="string"&&t.push("'authenticationSubject' must be a string"),c(e,"effectivePrincipal")&&typeof e.effectivePrincipal!="string"&&t.push("'effectivePrincipal' must be a string"),c(e,"globalRole")&&e.globalRole!==null&&typeof e.globalRole!="string"&&t.push("'globalRole' must be a string or null"),c(e,"projectRoles")&&!Array.isArray(e.projectRoles)&&t.push("'projectRoles' must be an array"),c(e,"fixtureNamespace")&&typeof e.fixtureNamespace!="string"&&t.push("'fixtureNamespace' must be a string"),c(e,"delegationContext")&&e.delegationContext!==null&&typeof e.delegationContext!="string"&&t.push("'delegationContext' must be a string or null"),{ok:t.length===0,problems:t}}function m(e,t){const n=e.expect;if(n.globalRole!==void 0&&(n.globalRole??null)!==(t.globalRole??null))return!1;const o=[...n.projectRoles??[]].sort(),i=[...t.projectRoles??[]].sort();return!(JSON.stringify(o)!==JSON.stringify(i)||n.effectivePrincipal!==void 0&&n.effectivePrincipal!==t.effectivePrincipal||n.delegationContext!==void 0&&n.delegationContext!==t.delegationContext)}function $(e,t){const n=e.expect,o=n.provisioning==="anonymous";return{persona:e.name,authenticated:!!t.authenticated,sessionActive:!!t.sessionActive,expected:{globalRole:n.globalRole??null,projectRoles:n.projectRoles??[],effectivePrincipal:n.effectivePrincipal??null,delegationContext:n.delegationContext??null},actual:{globalRole:t.globalRole??null,projectRoles:t.projectRoles??[],effectivePrincipal:t.effectivePrincipal??null,delegationContext:t.delegationContext??null},match:o?!t.authenticated&&!t.sessionActive&&m(e,t):!!t.authenticated&&!!t.sessionActive&&m(e,t)}}function E(e,t,{peers:n=[]}={}){const o=[],i=$(e,t),a=e.expect.provisioning==="anonymous";a&&(t.authenticated||t.sessionActive)?o.push(`persona '${e.name}': oracle reports an authenticated or active session for an anonymous persona`):!a&&(!t.authenticated||!t.sessionActive)?o.push(`persona '${e.name}': oracle reports not authenticated / not active`):m(e,t)||o.push(`persona '${e.name}': actual role does not match its declared expectation`);const l=t.authenticationSubject;if(e.expect.provisioning==="independently-provisioned")for(const s of n)s.persona.name!==e.name&&s.persona.expect.provisioning==="independently-provisioned"&&s.response.authenticationSubject===l&&o.push(`persona '${e.name}' (independently-provisioned) shares its authentication subject with persona '${s.persona.name}', but declares no shares-authentication-with relationship`);else if(e.expect.provisioning==="shares-authentication-with"){const s=n.find(r=>r.persona.name===e.expect.with);s&&s.response.authenticationSubject!==l&&o.push(`persona '${e.name}': declares shares-authentication-with '${e.expect.with}' but its authentication subject differs`)}return{status:o.length===0?"valid":"invalid",problems:o,archivable:i}}const N="verify.identity-adapter.mjs";function C(e,t,n){const o=new URL(e).origin,i=Object.keys(n??{});return async(a,l={})=>{const s=new URL(String(a??""),e),r=new Headers(l.headers??{}),u=s.origin===o;for(const f of i)u?r.set(f,n[f]):r.delete(f);return t&&u?r.set("cookie",t):r.delete("cookie"),fetch(s,{...l,headers:r})}}async function S(e,t){let n;try{n=await import(x(e).href)}catch{return{status:"invalid",reason:"adapter-unloadable"}}if(typeof n?.default!="function")return{status:"invalid",reason:"adapter-no-default-export"};let o;try{o=await n.default(t)}catch{return{status:"invalid",reason:"adapter-threw"}}if(o===null||typeof o!="object"||Array.isArray(o))return{status:"invalid",reason:"adapter-not-an-object"};const{ok:i,problems:a}=b(o);return i?{status:"measured",response:o}:{status:"invalid",reason:`adapter-invalid-shape:${a[0]??"unknown"}`}}async function L({base:e,oraclePath:t,statePath:n,headers:o=null,adapterPath:i=null}){const a=k(o);if(!a.ok)return{status:"invalid",reason:a.reason};let l;try{l=JSON.parse(v.readFileSync(n,"utf8"))}catch(p){return{status:"invalid",reason:`unreadable-state:${p.code??"error"}`}}const r=(Array.isArray(l.cookies)?l.cookies:[]).map(p=>`${p.name}=${p.value}`).join("; ");let u;try{if(u=new URL(t,e),u.origin!==new URL(e).origin)return{status:"invalid",reason:"oracle-cross-origin"}}catch{return{status:"invalid",reason:"oracle-path-invalid"}}if(i&&v.existsSync(i))return S(i,C(e,r,a.headers));const f={...a.headers??{},...r?{cookie:r}:{}};let d;try{d=await fetch(u,Object.keys(f).length?{headers:f}:void 0)}catch{return{status:"unmeasured",reason:"no-oracle"}}if(d.status===404)return{status:"unmeasured",reason:"no-oracle"};if(!d.ok)return{status:"invalid",reason:`oracle-http-${d.status}`};let h;try{h=await d.json()}catch{return{status:"invalid",reason:"oracle-malformed-json"}}const{ok:g,problems:y}=b(h);return g?{status:"measured",response:h}:{status:"invalid",reason:`oracle-invalid-shape:${y[0]??"unknown"}`}}export{N as IDENTITY_ADAPTER,j as ORACLE_FIELDS,$ as archivableIdentity,E as comparePersona,L as probeIdentity,b as validateOracleResponse,k as validatePersonaHeaders};
@@ -0,0 +1,3 @@
1
+ import d from"node:fs";import h from"node:path";import{InvalidError as x}from"../cli.mjs";const w=Object.freeze(["auth","headers","token","password","secret","cookie","apiKey","email","username","userId","accountId"]),b=new Set(["owns","excludes","expect"]),$=new Set(["provisioning","with","effectivePrincipal","delegationContext","globalRole","projectRoles","fixtureNamespace"]),g=/^[A-Za-z][A-Za-z0-9_-]{0,63}$/;function y(t){return typeof t!="string"?!1:t.includes("@")||/^[0-9a-f-]{16,}$/i.test(t)||/^\d{4,}$/.test(t)}function l(t){return typeof t=="string"&&/(?:\b(?:bearer|basic)\s+\S+|\b(?:sk|pk)_(?:live|test)_[A-Za-z0-9]+|^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$)/i.test(t)}function j(t){return y(t)||l(t)}function u(t){return typeof t=="string"&&g.test(t)&&!j(t)}function m(t,e,n,r){if(!Array.isArray(r)){t.push(`persona '${e}': ${n} must be an array of route paths`);return}for(const c of r){if(typeof c!="string"||!c.startsWith("/")||c.startsWith("//")){t.push(`persona '${e}': ${n} contains a value that is not a same-origin route path`);continue}(l(c)||c.split("/").some(s=>s&&!/^(?:\{[^}]+\}|\[[^\]]+\]|:[^/]+|\$[^/]+)$/.test(s)&&(l(s)||s.includes("@")||/^[0-9a-f-]{16,}$/i.test(s)||/^\d{8,}$/.test(s))))&&t.push(`persona '${e}': ${n} contains a route with a credential or raw identifier`)}}function p(t,e,n,r){r!=null&&(typeof r!="string"?t.push(`persona '${e}': expect.${n} must be a categorical string`):(y(r)||l(r))&&t.push(`persona '${e}': expect.${n} looks like an account/user identifier, not a categorical role`))}function k(t){const e=[];if(t===null||typeof t!="object"||Array.isArray(t))return{ok:!1,problems:["persona file must be a JSON object keyed by persona name"]};const n=Object.keys(t);n.length||e.push("persona file must declare at least one persona");for(const r of n){const c=u(r),i=c?r:"<redacted>";c||e.push("persona name must be a categorical name, not a path, credential, or raw account/user identifier");const s=t[r];if(s===null||typeof s!="object"||Array.isArray(s)){e.push(`persona '${i}': entry must be an object`);continue}m(e,i,"owns",s.owns??["/"]),m(e,i,"excludes",s.excludes??[]);for(const a of Object.keys(s))b.has(a)||(w.includes(a)?e.push(`persona '${i}': forbidden key '${a}' may not appear in a committed persona file -- it is credential-shaped; secrets travel through the host-harness launcher, never this file`):e.push(`persona '${i}': unknown key '${a}' -- only owns, excludes, expect are allowed`));if(s.expect===void 0)continue;const o=s.expect;if(o===null||typeof o!="object"||Array.isArray(o)){e.push(`persona '${i}': expect, when present, must be an object`);continue}for(const a of Object.keys(o))$.has(a)||e.push(`persona '${i}': expect.${a} is an unknown or credential-shaped field`);const{provisioning:f}=o;if(["independently-provisioned","shares-authentication-with","anonymous"].includes(f)||e.push(`persona '${i}': expect.provisioning must be 'independently-provisioned', 'shares-authentication-with', or 'anonymous'`),f==="shares-authentication-with"){if(!u(o.with)||!n.includes(o.with)||o.with===r){const a=u(o.with)?`, got ${JSON.stringify(o.with)}`:"";e.push(`persona '${i}': expect.with must name another persona declared in this file${a}`)}o.effectivePrincipal||e.push(`persona '${i}': shares-authentication-with requires expect.effectivePrincipal -- a principal distinct from the persona it shares the subject with`)}else Object.prototype.hasOwnProperty.call(o,"with")&&e.push(`persona '${i}': expect.with is allowed only when provisioning is 'shares-authentication-with'`);if(f==="anonymous"&&Object.prototype.hasOwnProperty.call(o,"effectivePrincipal")&&e.push(`persona '${i}': expect.effectivePrincipal is not allowed when provisioning is 'anonymous'`),p(e,i,"globalRole",o.globalRole),p(e,i,"effectivePrincipal",o.effectivePrincipal),p(e,i,"delegationContext",o.delegationContext),p(e,i,"fixtureNamespace",o.fixtureNamespace),Array.isArray(o.projectRoles))for(const a of o.projectRoles)p(e,i,"projectRoles",a);else o.projectRoles!==void 0&&e.push(`persona '${i}': expect.projectRoles must be an array of categorical strings`)}return{ok:e.length===0,problems:e}}function v(t){return Object.fromEntries(Object.entries(t).map(([e,n])=>[e,{owns:n.owns??["/"],excludes:n.excludes??[],...n.expect===void 0?{}:{expect:n.expect}}]))}function A(t){return{provisioning:"independently-provisioned",globalRole:null,projectRoles:[],fixtureNamespace:t}}function P(t,e){const n=e.expect??A(t);return{name:t,owns:e.owns??["/"],excludes:e.excludes??[],expect:{provisioning:n.provisioning,...n.with!==void 0?{with:n.with}:{},...n.effectivePrincipal!==void 0?{effectivePrincipal:n.effectivePrincipal}:{},...n.delegationContext!==void 0?{delegationContext:n.delegationContext}:{},globalRole:n.globalRole??null,projectRoles:n.projectRoles??[],fixtureNamespace:n.fixtureNamespace??t}}}const R={name:"default",owns:["/"],excludes:[],expect:{provisioning:"independently-provisioned",globalRole:null,projectRoles:[],fixtureNamespace:"default"}};function E(t){const e=[h.join(t,"verify.roles.json"),h.join(t,".verify","roles.json")].find(s=>d.existsSync(s));if(!e)return{personas:[R],file:null};let n;try{n=JSON.parse(d.readFileSync(e,"utf8"))}catch{throw new x(`${e}: not valid JSON`)}const{ok:r,problems:c}=k(n);if(!r)throw new x(`${e}: invalid persona declarations:
2
+ ${c.join(`
3
+ `)}`);return{personas:Object.entries(n).map(([s,o])=>P(s,o)),file:e,declaration:v(n)}}export{w as FORBIDDEN_PERSONA_KEYS,u as isSafePersonaName,j as isSensitivePersonaName,E as loadPersonas,v as normalizePersonaDeclaration,k as validatePersonas};
@@ -0,0 +1,3 @@
1
+ import p from"node:fs";import s from"node:path";import{run as f}from"./proc.mjs";import{exists as c}from"./fs.mjs";import{validateManifestFile as u}from"../../bin/verify-manifest.mjs";async function g({skillRoot:a,repo:t,agentFlags:e=[]}){const r=await f("cargo",["run","--quiet","--manifest-path",s.join(a,"agent","Cargo.toml"),"--","--repo",t,...e,"--print-input-fingerprint"],{cwd:a,stdout:"capture",stderr:"capture",env:{FRONTEND_VERIFY_SKILL_ROOT:a}});if(r.code!==0)return{ok:!1,reason:`print-input-fingerprint failed (exit ${r.code}): ${String(r.stderr).split(`
2
+ `).filter(Boolean).slice(-2).join(" | ")}`};let n;try{const i=String(r.stdout).trim().split(`
3
+ `).filter(Boolean).pop();n=JSON.parse(i)}catch{return{ok:!1,reason:"print-input-fingerprint: unparseable output"}}return typeof n?.input_fingerprint!="string"||typeof n?.content_fingerprint!="string"||typeof n?.binary_sha256!="string"?{ok:!1,reason:"print-input-fingerprint: missing fields"}:{ok:!0,inputFingerprint:n.input_fingerprint,inputHashes:Array.isArray(n.input_hashes)?n.input_hashes:[],contentFingerprint:n.content_fingerprint,contentHashes:Array.isArray(n.content_hashes)?n.content_hashes:[],configFingerprint:typeof n.config_fingerprint=="string"?n.config_fingerprint:null,binarySha256:n.binary_sha256,binaryProtocolVersion:n.binary_protocol_version??null}}function h(a,t){const e=new Map((a??[]).map(([i,o])=>[i,o])),r=new Map((t??[]).map(([i,o])=>[i,o])),n=[];for(const[i,o]of r)e.has(i)?e.get(i)!==o&&n.push(i):n.push(`${i} (new)`);for(const i of e.keys())r.has(i)||n.push(`${i} (removed)`);return n.length?n.join(", "):"unknown input"}function l({repo:a,current:t}){if(!t?.ok)return{hit:!1,reason:t?.reason??"no-current-fingerprint"};const e=s.join(a,".verify","autonomous"),r=s.join(e,"run-manifest.json");if(!c(r))return{hit:!1,reason:"no-existing-manifest"};let n;try{n=JSON.parse(p.readFileSync(r,"utf8"))}catch(o){return{hit:!1,reason:`manifest-unreadable: ${o.message}`}}if(typeof n.content_fingerprint!="string")return{hit:!1,reason:"manifest-predates-content-fingerprint"};if(n.content_fingerprint!==t.contentFingerprint)return{hit:!1,reason:`input changed: ${h(n.input_hashes,t.contentHashes)}`};if(t.configFingerprint&&n.config_fingerprint!==t.configFingerprint)return{hit:!1,reason:"config-fingerprint-changed"};if(typeof n.binary_sha256!="string"||n.binary_sha256!==t.binarySha256)return{hit:!1,reason:"binary-identity-changed"};if((n.binary_protocol_version??null)!==(t.binaryProtocolVersion??null))return{hit:!1,reason:"binary-identity-changed"};let i;try{({report:i}=u(r))}catch(o){return{hit:!1,reason:`manifest-validation-error: ${o.message}`}}return i.valid?{hit:!0,inputFingerprint:t.inputFingerprint,contentFingerprint:t.contentFingerprint,manifestPath:r,dir:e}:{hit:!1,reason:`manifest-invalid: ${i.reasons.join(",")||"invalid"}`}}async function F({skillRoot:a,repo:t,agentFlags:e=[]}){const r=await g({skillRoot:a,repo:t,agentFlags:e});return l({repo:t,current:r})}export{l as checkGraphCache,g as computeCurrentFingerprint,F as tryReuseGraph};
@@ -0,0 +1 @@
1
+ const a=new Set(["no-change","timeout","ineffective","regression","ghost","known-fp"]),s=Object.freeze({"no-change":{maxAttempts:4,backoff:r=>4*r*r},timeout:{maxAttempts:5,backoff:r=>2*r},ineffective:{maxAttempts:3,backoff:r=>6*r*r},regression:{maxAttempts:2,backoff:r=>10*r*r},ghost:{maxAttempts:0,backoff:()=>1/0},"known-fp":{maxAttempts:0,backoff:()=>1/0},legacy:{maxAttempts:0,backoff:()=>1/0}});class f{constructor(o=new Map){this.records=o}static from(o){const e=new Map;for(const t of o?.blockLedger??[])typeof t?.fid=="string"&&e.set(t.fid,{fid:t.fid,reason:s[t.reason]?t.reason:"legacy",attempts:Number.isInteger(t.attempts)?t.attempts:1,blockedAt:Number.isInteger(t.blockedAt)?t.blockedAt:0,permanent:!!t.permanent});for(const t of o?.blocked??[])typeof t=="string"&&!e.has(t)&&e.set(t,{fid:t,reason:"legacy",attempts:1,blockedAt:0,permanent:!0});return new f(e)}block(o,e,t){if(!a.has(e))throw new Error(`unknown block reason ${JSON.stringify(e)}; expected one of ${[...a].join(", ")}`);const n=this.records.get(o),c=(n&&n.reason===e?n.attempts:0)+1,i=s[e];return this.records.set(o,{fid:o,reason:e,attempts:c,blockedAt:t,permanent:c>=i.maxAttempts}),this.records.get(o)}recordFor(o){return this.records.get(o)??null}activeAt(o){const e=new Set;for(const t of this.records.values()){if(t.permanent){e.add(t.fid);continue}o<t.blockedAt+s[t.reason].backoff(t.attempts)&&e.add(t.fid)}return e}nextExpiryAfter(o){let e=null;for(const t of this.records.values()){if(t.permanent)continue;const n=t.blockedAt+s[t.reason].backoff(t.attempts);n>o&&(e===null||n<e)&&(e=n)}return e}toJSON(){const o=[...this.records.values()];return{blocked:o.map(e=>e.fid),blockLedger:o.map(e=>({...e}))}}}export{a as BLOCK_REASONS,f as BlockLedger};
@@ -0,0 +1,63 @@
1
+ import s from"node:fs";import m from"node:path";const d={P0:0,P1:1,P2:2,P3:3},a=e=>String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;");function l(e){return(e.routes??[]).filter(r=>r.screenshot).sort((r,t)=>(r.route??"").localeCompare(t.route??"")||(r.role??"").localeCompare(t.role??"")||(r.width??0)-(t.width??0))}function g(e){return e?.length?`<ul class="findings">${[...e].sort((t,n)=>(d[t.severity]??9)-(d[n.severity]??9)).map(t=>`
2
+ <li><span class="sev sev-${a(t.severity)}">${a(t.severity)}</span>
3
+ <code>${a(t.kind)}</code>
4
+ <span class="detail">${a(t.detail??"")}</span></li>`).join("")}</ul>`:'<p class="clean">No findings on this page.</p>'}function u(e){const r=e.metrics??{};return`<dl class="meta">${[["role",e.role??"default"],["width",e.width?`${e.width}px`:null],["redirected to",e.redirectedTo],["text length",e.textLen],["API requests",e.apiRequests],["LCP",r.lcp!=null?`${Math.round(r.lcp)}ms`:null],["CLS",r.cls!=null?r.cls.toFixed(3):null],["aborted requests",r.aborted||null]].filter(([,n])=>n!=null&&n!=="").map(([n,o])=>`<div><dt>${a(n)}</dt><dd>${a(o)}</dd></div>`).join("")}</dl>`}function p(e){const r=e.screenshot??{},t=r.file?`<a href="${a(r.file)}" target="_blank" rel="noreferrer"><img loading="lazy" src="${a(r.file)}" alt="${a(e.route)} at ${a(e.width)}px"></a>`:`<p class="unmeasured">${a(r.unmeasured??"no screenshot was taken for this cell")}</p>`,n=(e.findings??[]).length;return`
5
+ <section class="cell" id="${a(`${e.role??"default"}-${e.route}-${e.width}`)}">
6
+ <header>
7
+ <h2><code>${a(e.route)}</code></h2>
8
+ <span class="badge">${a(e.role??"default")}</span>
9
+ <span class="badge">${a(e.width)}px</span>
10
+ <span class="badge ${n?"badge-find":"badge-clean"}">${n} finding${n===1?"":"s"}</span>
11
+ </header>
12
+ <div class="body">
13
+ <div class="shot">${t}</div>
14
+ <div class="info">${u(e)}${g(e.findings)}</div>
15
+ </div>
16
+ </section>`}const f=`
17
+ :root { color-scheme: light dark; --bg:#fff; --fg:#111; --muted:#666; --line:#e3e3e3; --card:#fafafa; }
18
+ @media (prefers-color-scheme: dark) { :root { --bg:#111; --fg:#eee; --muted:#999; --line:#333; --card:#1a1a1a; } }
19
+ * { box-sizing: border-box; }
20
+ body { margin:0; padding:2rem; background:var(--bg); color:var(--fg);
21
+ font:14px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif; }
22
+ h1 { font-size:1.4rem; margin:0 0 .25rem; }
23
+ .sub { color:var(--muted); margin:0 0 2rem; }
24
+ .notice { border-left:3px solid #c93; background:var(--card); padding:.75rem 1rem; margin:0 0 2rem; }
25
+ .cell { border:1px solid var(--line); border-radius:8px; margin:0 0 1.5rem; overflow:hidden; background:var(--card); }
26
+ .cell header { display:flex; gap:.5rem; align-items:center; flex-wrap:wrap;
27
+ padding:.75rem 1rem; border-bottom:1px solid var(--line); }
28
+ .cell h2 { font-size:1rem; margin:0; margin-right:auto; }
29
+ .badge { font-size:.75rem; padding:.15rem .5rem; border:1px solid var(--line); border-radius:99px; color:var(--muted); }
30
+ .badge-find { color:#c0392b; border-color:#c0392b; }
31
+ .badge-clean { color:#2d7d46; border-color:#2d7d46; }
32
+ .body { display:grid; grid-template-columns:minmax(0,3fr) minmax(0,2fr); gap:1rem; padding:1rem; }
33
+ @media (max-width:900px) { .body { grid-template-columns:1fr; } }
34
+ .shot img { width:100%; height:auto; border:1px solid var(--line); border-radius:4px; display:block; }
35
+ .meta { display:grid; grid-template-columns:auto 1fr; gap:.15rem .75rem; margin:0 0 1rem; }
36
+ .meta div { display:contents; }
37
+ .meta dt { color:var(--muted); }
38
+ .meta dd { margin:0; }
39
+ .findings { list-style:none; margin:0; padding:0; }
40
+ .findings li { padding:.35rem 0; border-top:1px solid var(--line); }
41
+ .sev { font-size:.7rem; font-weight:600; padding:.1rem .4rem; border-radius:3px; margin-right:.4rem; }
42
+ .sev-P0,.sev-P1 { background:#c0392b; color:#fff; }
43
+ .sev-P2 { background:#c9922b; color:#fff; }
44
+ .sev-P3 { background:#777; color:#fff; }
45
+ .detail { color:var(--muted); }
46
+ .clean { color:#2d7d46; margin:0; }
47
+ .unmeasured { color:var(--muted); font-style:italic; }
48
+ code { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; }
49
+ `;function h(e){const r=l(e),t=new Set(r.map(i=>i.route)).size,n=r.reduce((i,c)=>i+(c.findings?.length??0),0),o=r.filter(i=>!i.screenshot?.file).length;return`<!doctype html>
50
+ <html lang="en"><head><meta charset="utf-8">
51
+ <meta name="viewport" content="width=device-width,initial-scale=1">
52
+ <title>qa capture \u2014 ${a(e.base??"run")}</title>
53
+ <style>${f}</style></head><body>
54
+ <h1>Page capture</h1>
55
+ <p class="sub">${a(e.base??"")} \u2014 ${r.length} cell(s) over ${t} route(s),
56
+ ${n} finding(s)${o?`, ${o} cell(s) without an image`:""}.
57
+ Sweep status: <strong>${a(e.status??"unknown")}</strong>.</p>
58
+ <p class="notice"><strong>These images may contain application data.</strong> They were taken
59
+ against a running application, with whatever the measured persona could see. Password fields
60
+ and <code>[data-qa-redact]</code> elements are masked; nothing else is. Review before sharing.</p>
61
+ ${r.map(p).join(`
62
+ `)}
63
+ </body></html>`}function $(e,r){try{if(!l(e).length)return null;s.mkdirSync(r,{recursive:!0});const t=m.join(r,"index.html");return s.writeFileSync(t,h(e)),t}catch{return null}}export{h as renderCaptureReport,$ as writeCaptureReport};
package/lib/qa/ci.mjs ADDED
@@ -0,0 +1,11 @@
1
+ import{normRoute as h}from"./reports.mjs";import{atlasIdFor as g,hintFor as x,atlasEntry as v}from"./atlas.mjs";const y="https://json.schemastore.org/sarif-2.1.0.json",p=["P0","P1","P2","P3"];function d(n){return n==="P0"||n==="P1"?"error":n==="P2"?"warning":"note"}function P(n){let e="P3";for(const t of n)p.indexOf(t.severity)<p.indexOf(e)&&(e=t.severity);return e}function m(n){return String(n).split("\\").join("/")}function b(n,e){return!n||!e?null:(n.routes??[]).find(i=>h(i.path)===e)?.file??null}function j(n,{skillRoot:e=null,informationUri:t}={}){const i=new Map;for(const s of n)s.waived||(i.has(s.kind)||i.set(s.kind,[]),i.get(s.kind).push(s));return[...i.entries()].sort((s,o)=>s[0].localeCompare(o[0])).map(([s,o])=>{const r=o[0],a=g(r),u=a&&e?v(e,a,r.atlasVersion):null,c=x(r);return{id:s,shortDescription:{text:u?.text??s},help:{text:c??u?.text??`${s} \u2014 see the frontend-verify atlas.`},helpUri:a?`${t}#${a}`:t,defaultConfiguration:{level:d(P(o))}}})}function w(n,{inventory:e=null}={}){return n.filter(t=>!t.waived).map(t=>{const i=[];if(t.file)i.push({physicalLocation:{artifactLocation:{uri:m(t.file)},region:{startLine:Number.isInteger(t.line)&&t.line>0?t.line:1}}});else if(t.route){const s={logicalLocations:[{name:t.route,kind:"route"}]},o=b(e,t.route);o&&(s.physicalLocation={artifactLocation:{uri:m(o)},region:{startLine:1}}),i.push(s)}return{ruleId:t.kind,level:d(t.severity),message:{text:t.detail||t.kind},...i.length?{locations:i}:{},partialFingerprints:{fid:t.fid}}})}function F({records:n,inventory:e=null,informationUri:t,version:i="0.0.0",skillRoot:s=null,toolName:o="frontend-verify"}){return{$schema:y,version:"2.1.0",runs:[{tool:{driver:{name:o,version:i,informationUri:t,rules:j(n,{skillRoot:s,informationUri:t})}},results:w(n,{inventory:e})}]}}function f(n){return String(n??"").replace(/%/g,"%25").replace(/\r/g,"%0D").replace(/\n/g,"%0A")}function $(n){return f(n).replace(/,/g,"%2C").replace(/:/g,"%3A")}function L(n){return n.filter(e=>!e.waived).map(e=>{const t=e.severity==="P3"?"notice":e.severity==="P2"?"warning":"error",i=$(e.kind),s=f(e.detail||e.kind);if(e.file){const a=[`file=${$(e.file)}`,`line=${e.line&&e.line>0?e.line:1}`,`title=${i}`].join(",");return`::${t} ${a}::${s}`}const o=[`title=${i}`].join(","),r=e.route?f(` (route ${e.route})`):"";return`::${t} ${o}::${s}${r}`})}function l(n){return String(n??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&apos;")}function C({groups:n=[]}={}){const e=new Map;for(const i of n)e.has(i.phase)||e.set(i.phase,[]),e.get(i.phase).push(i);return`<?xml version="1.0" encoding="UTF-8"?>
2
+ <testsuites>
3
+ ${[...e.entries()].map(([i,s])=>{const o=s.map(r=>{const a=r.at&&r.at!=="(page)"&&r.at!=="(journey)"?` @ ${r.at}`:"",u=l(`${r.kind}${a}`),c=l(r.sample?.detail??"");return` <testcase name="${u}" classname="${l(i)}" time="0">
4
+ <failure message="${l(r.severity)}" type="${l(r.kind)}">${c}</failure>
5
+ </testcase>`}).join(`
6
+ `);return` <testsuite name="${l(i)}" tests="${s.length}" failures="${s.length}">
7
+ ${o}
8
+ </testsuite>`}).join(`
9
+ `)}
10
+ </testsuites>
11
+ `}export{y as SARIF_SCHEMA,L as buildGithubCommands,C as buildJunit,F as buildSarif,d as levelFor,w as sarifResults,j as sarifRules};