@docsxai/engine 0.2.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 (129) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +130 -0
  3. package/dist/auth/api-login.d.ts +69 -0
  4. package/dist/auth/api-login.js +95 -0
  5. package/dist/auth/browser-session.d.ts +28 -0
  6. package/dist/auth/browser-session.js +43 -0
  7. package/dist/auth/cookie-jar.d.ts +58 -0
  8. package/dist/auth/cookie-jar.js +212 -0
  9. package/dist/auth/email-otp.d.ts +210 -0
  10. package/dist/auth/email-otp.js +166 -0
  11. package/dist/auth/http-basic.d.ts +5 -0
  12. package/dist/auth/http-basic.js +17 -0
  13. package/dist/auth/index.d.ts +47 -0
  14. package/dist/auth/index.js +137 -0
  15. package/dist/auth/jwt-injection.d.ts +153 -0
  16. package/dist/auth/jwt-injection.js +136 -0
  17. package/dist/auth/manual-capture.d.ts +35 -0
  18. package/dist/auth/manual-capture.js +30 -0
  19. package/dist/auth/mtls.d.ts +15 -0
  20. package/dist/auth/mtls.js +53 -0
  21. package/dist/auth/pat-header.d.ts +19 -0
  22. package/dist/auth/pat-header.js +34 -0
  23. package/dist/auth/storage-state-cache.d.ts +38 -0
  24. package/dist/auth/storage-state-cache.js +143 -0
  25. package/dist/auth/test-backdoor.d.ts +25 -0
  26. package/dist/auth/test-backdoor.js +51 -0
  27. package/dist/auth/totp.d.ts +39 -0
  28. package/dist/auth/totp.js +108 -0
  29. package/dist/auth/types.d.ts +86 -0
  30. package/dist/auth/types.js +57 -0
  31. package/dist/auth/ui-form.d.ts +204 -0
  32. package/dist/auth/ui-form.js +153 -0
  33. package/dist/auth/webauthn.d.ts +88 -0
  34. package/dist/auth/webauthn.js +67 -0
  35. package/dist/auth.d.ts +1 -0
  36. package/dist/auth.js +3 -0
  37. package/dist/backend-client-contracts.d.ts +88 -0
  38. package/dist/backend-client-contracts.js +19 -0
  39. package/dist/backend-client-oauth-login.d.ts +7 -0
  40. package/dist/backend-client-oauth-login.js +90 -0
  41. package/dist/backend-client-state-cache.d.ts +73 -0
  42. package/dist/backend-client-state-cache.js +185 -0
  43. package/dist/backend-client-token.d.ts +18 -0
  44. package/dist/backend-client-token.js +94 -0
  45. package/dist/backend-client-transport.d.ts +66 -0
  46. package/dist/backend-client-transport.js +181 -0
  47. package/dist/backend-client.d.ts +5 -0
  48. package/dist/backend-client.js +18 -0
  49. package/dist/calibrate.d.ts +31 -0
  50. package/dist/calibrate.js +68 -0
  51. package/dist/cli-commands-authoring.d.ts +5 -0
  52. package/dist/cli-commands-authoring.js +403 -0
  53. package/dist/cli-commands-backend.d.ts +5 -0
  54. package/dist/cli-commands-backend.js +211 -0
  55. package/dist/cli-commands-docpack.d.ts +5 -0
  56. package/dist/cli-commands-docpack.js +280 -0
  57. package/dist/cli-commands-session.d.ts +4 -0
  58. package/dist/cli-commands-session.js +398 -0
  59. package/dist/cli-shared.d.ts +5 -0
  60. package/dist/cli-shared.js +45 -0
  61. package/dist/cli-usage.d.ts +1 -0
  62. package/dist/cli-usage.js +137 -0
  63. package/dist/cli.d.ts +2 -0
  64. package/dist/cli.js +77 -0
  65. package/dist/diagnose.d.ts +50 -0
  66. package/dist/diagnose.js +168 -0
  67. package/dist/diff-compute.d.ts +13 -0
  68. package/dist/diff-compute.js +378 -0
  69. package/dist/diff-report.d.ts +7 -0
  70. package/dist/diff-report.js +125 -0
  71. package/dist/diff-types.d.ts +125 -0
  72. package/dist/diff-types.js +15 -0
  73. package/dist/diff.d.ts +3 -0
  74. package/dist/diff.js +16 -0
  75. package/dist/doc-pack-io.d.ts +30 -0
  76. package/dist/doc-pack-io.js +182 -0
  77. package/dist/doc-pack.d.ts +1814 -0
  78. package/dist/doc-pack.js +328 -0
  79. package/dist/doctor-checks-plugins.d.ts +2 -0
  80. package/dist/doctor-checks-plugins.js +136 -0
  81. package/dist/doctor-checks.d.ts +56 -0
  82. package/dist/doctor-checks.js +367 -0
  83. package/dist/doctor.d.ts +7 -0
  84. package/dist/doctor.js +62 -0
  85. package/dist/export/adf.d.ts +57 -0
  86. package/dist/export/adf.js +323 -0
  87. package/dist/export/playwright-test.d.ts +26 -0
  88. package/dist/export/playwright-test.js +221 -0
  89. package/dist/flow-file.d.ts +21 -0
  90. package/dist/flow-file.js +180 -0
  91. package/dist/flow-lint.d.ts +24 -0
  92. package/dist/flow-lint.js +203 -0
  93. package/dist/flow-runtime.d.ts +113 -0
  94. package/dist/flow-runtime.js +273 -0
  95. package/dist/flow-tree.d.ts +19 -0
  96. package/dist/flow-tree.js +104 -0
  97. package/dist/index.d.ts +27 -0
  98. package/dist/index.js +31 -0
  99. package/dist/playwright-driver.d.ts +105 -0
  100. package/dist/playwright-driver.js +363 -0
  101. package/dist/playwright-instrumented-browser.d.ts +51 -0
  102. package/dist/playwright-instrumented-browser.js +189 -0
  103. package/dist/plugins/load.d.ts +22 -0
  104. package/dist/plugins/load.js +99 -0
  105. package/dist/plugins/lock.d.ts +40 -0
  106. package/dist/plugins/lock.js +122 -0
  107. package/dist/plugins/manifest.d.ts +70 -0
  108. package/dist/plugins/manifest.js +115 -0
  109. package/dist/plugins/plan.d.ts +51 -0
  110. package/dist/plugins/plan.js +279 -0
  111. package/dist/plugins/registry.d.ts +59 -0
  112. package/dist/plugins/registry.js +71 -0
  113. package/dist/plugins/runtime.d.ts +7 -0
  114. package/dist/plugins/runtime.js +27 -0
  115. package/dist/plugins/types.d.ts +58 -0
  116. package/dist/plugins/types.js +4 -0
  117. package/dist/plugins-cli.d.ts +1 -0
  118. package/dist/plugins-cli.js +191 -0
  119. package/dist/redact.d.ts +16 -0
  120. package/dist/redact.js +72 -0
  121. package/dist/style.d.ts +46 -0
  122. package/dist/style.js +151 -0
  123. package/dist/viewer-bin.d.ts +20 -0
  124. package/dist/viewer-bin.js +97 -0
  125. package/dist/workspace.d.ts +60 -0
  126. package/dist/workspace.js +172 -0
  127. package/dist/zip.d.ts +17 -0
  128. package/dist/zip.js +113 -0
  129. package/package.json +64 -0
@@ -0,0 +1,5 @@
1
+ export declare function parseFlags(args: string[]): {
2
+ positionals: string[];
3
+ flags: Map<string, string | true>;
4
+ };
5
+ export declare function listFlowFiles(projectDir: string): Promise<string[]>;
@@ -0,0 +1,45 @@
1
+ // Shared CLI leaf — the helpers more than one command cluster reaches for. Kept dependency-free of
2
+ // the command clusters (and of cli.ts) so it can be imported anywhere without a cycle: the clusters
3
+ // import from here; this file imports only from the engine's lower-level modules.
4
+ // • parseFlags — the argv → { positionals, flags } parser every command opens with
5
+ // • listFlowFiles — enumerate the workspace's flows/*.flow.yaml (run / lint / flow-tree)
6
+ import { promises as fs } from "node:fs";
7
+ import { resolveWorkspacePath } from "./workspace.js";
8
+ export function parseFlags(args) {
9
+ const positionals = [];
10
+ const flags = new Map();
11
+ for (let i = 0; i < args.length; i++) {
12
+ const a = args[i];
13
+ if (a.startsWith("--")) {
14
+ const key = a.slice(2);
15
+ const next = args[i + 1];
16
+ if (next !== undefined && !next.startsWith("--")) {
17
+ flags.set(key, next);
18
+ i++;
19
+ }
20
+ else {
21
+ flags.set(key, true);
22
+ }
23
+ }
24
+ else {
25
+ positionals.push(a);
26
+ }
27
+ }
28
+ return { positionals, flags };
29
+ }
30
+ export async function listFlowFiles(projectDir) {
31
+ const dir = resolveWorkspacePath(projectDir, "flows");
32
+ let entries;
33
+ try {
34
+ entries = await fs.readdir(dir);
35
+ }
36
+ catch {
37
+ throw new Error(`workspace ${projectDir} has no flows/ directory (expected ${dir}). ` +
38
+ `Is this a docsxai workspace? Create one with \`docsxai init <workspace-dir>\`, ` +
39
+ `then add flows via \`docsxai calibrate\` or by writing flows/<name>.flow.yaml.`);
40
+ }
41
+ return entries
42
+ .filter((e) => e.endsWith(".flow.yaml"))
43
+ .sort()
44
+ .map((e) => resolveWorkspacePath(projectDir, "flows", e));
45
+ }
@@ -0,0 +1 @@
1
+ export declare const USAGE = "docsxai \u2014 deterministic execution CLI\n\nUsage:\n docsxai init <workspace-dir> [--app-url <url>] [--auth manual-capture|none] [--role <name>] [--ttl <dur>]\n [--capture-trigger console|button] [--auth-cookie <name>] [--ignore-https-errors]\n [--persist tmp] [--force]\n docsxai calibrate <workspace-dir> --from <flow.md|.yaml> [--name <flow>]\n docsxai inspect <workspace-dir> [--url <url>] [--selector <css>] [--cdp <endpoint>] [--wait <ms>] [--wait-for <css>] [--headed] [--role <role>]\n docsxai run <workspace-dir> [--flow <name>] [--base-url <url>] [--headed] [--ignore-https-errors] [--stop-after <step-id>] [--start-from <step-id>] [--cdp <endpoint>] [--pause] [--concurrency <N>]\n docsxai lint <workspace-dir> [--flow <name>] [--format text|json]\n docsxai flow-tree <workspace-dir> [--format text|json]\n docsxai diagnose <workspace-dir> --flow <name> --step <step-id> [--cdp <endpoint>] [--format text|json]\n docsxai doctor [<workspace-dir>]\n docsxai style <workspace-dir> [--check] [--format text|json]\n docsxai zip <workspace-dir> [--out <output.zip>] [--include-viewer]\n docsxai baseline <workspace-dir> [--out <dir>]\n docsxai diff <workspace-dir> [--against <dir>] [--format json|md|text] [--fail-on warn|fail]\n docsxai export adf <workspace-dir> [--flow <name>] [--mode single|page-tree] [--title <text>] [--out <dir>]\n docsxai export playwright <workspace-dir> [--flow <name>] [--out <dir>]\n docsxai plugins <list|info|sync> <workspace-dir> [<namespace>] [--format text|json]\n docsxai login --backend-url <url>\n docsxai push <workspace-dir> [--kind calibrate|run|edit] [--author <name>]\n docsxai pull <workspace-dir> [--rev <id>]\n docsxai render <workspace-dir>\n docsxai capture-auth <workspace-dir> [--base-url <url>] [--role <role>] [--auth-cookie <name>] [--cdp <endpoint>] [--fresh] [--headless] [--ignore-https-errors]\n docsxai --help\n\nNotes:\n \u2022 A *workspace* (created by `init`) holds flows/<flow>.flow.yaml, docs/, auth/strategy.yaml, .auth/, .viewer/,\n and a .docsxai.json config. Put it OUTSIDE the app's source repo \u2014 docsxai documents a running app from\n outside and never writes into the app repo.\n \u2022 run / capture-auth read app_url + ignore_https_errors from .docsxai.json if you don't pass the flags.\n \u2022 run launches Chromium; if no browser binary is present, install one: npx playwright-core install chromium\n (source checkout: pnpm -C packages/engine exec playwright-core install chromium)\n \u2022 --ignore-https-errors accepts self-signed/invalid TLS (e.g. an app's local HTTPS dev cert)\n \u2022 run --stop-after <step-id> runs only a prefix of the flow (up to & incl. that step); --pause keeps the\n (headed) browser open at the last step run \u2014 so you can inspect the live state mid-flow when calibrating\n (pair with --flow <name>). For waiting on a slow backend op, give a step a wait_for of the form\n { selector: $x, timeout_ms: 180000 } \u2014 a per-step override of the default ~30s selector-wait timeout.\n \u2022 run --concurrency <N> runs up to N flows in parallel (each its own Chromium session, isolated; default 1).\n Useful when several flows share a long preamble \u2014 total wall time = max(per-flow), not sum. Force-clamped\n to 1 when --pause / --stop-after / --start-from / --cdp is set. The target app must tolerate multiple sessions from one user.\n \u2022 run --start-from <step-id> --flow <name> SKIPS every step before <step-id> and starts execution there \u2014\n the inverse of --stop-after. Pair with --cdp to attach to a Chrome that's already in the post-prior-steps\n state (e.g. left over from a paused previous run) and iterate on the new tail step in seconds rather\n than re-walking the whole extends chain. New annotations MERGE into the existing annotations.json by\n step id; the prior steps' annotations and screenshots are preserved.\n \u2022 run --cdp <endpoint> attaches to a running Chrome (start it with --remote-debugging-port=N) instead of\n launching one. docsxai won't close that Chrome. When --cdp is set, the cached storageState is NOT\n loaded into the context \u2014 the operator's Chrome owns its auth state. Useful with --start-from for the\n sub-3-sec iteration loop on long-async flows.\n \u2022 capture-auth runs the role's auth strategy (MVP: manual-capture \u2014 a headed, instrumented browser the\n engineer logs into; window.__docsxai.capture() or an injected button snapshots the session) and caches\n it to <workspace-dir>/.auth/<role>.json for subsequent `run`s. It prints the captured cookie jar so you\n can identify the app's real auth/session cookie.\n \u2022 capture-auth keeps a persistent Chrome profile at <workspace>/.auth/chrome-profile/ (gitignored) \u2014 re-running\n it reuses the login (just trigger capture again). Use --fresh for a clean profile (forces a fresh login).\n \u2022 --cdp <endpoint> makes capture-auth *attach to an already-running Chrome* (start it with\n --remote-debugging-port=N --disable-web-security --user-data-dir=<dir>) instead of launching a fresh one \u2014\n use this to capture from the same Chrome the engineer is already logged into (and that Claude in Chrome is\n driving for discovery), so they don't log in twice. docsxai won't close that Chrome. (--cdp ignores --fresh.)\n \u2022 auth_cookie (set via `init --auth-cookie`, `capture-auth --auth-cookie`, or hand-edited into\n auth/strategy.yaml) names that cookie; when set, the cached session's expiry tracks *that* cookie's\n expiry rather than the `ttl` guess (an interactive SSO login leaves ephemeral IdP scratch cookies, so\n `min(cookie.expires)` \u2248 now \u2014 don't rely on it). If unset/unfound, `ttl` (or a 1h default) is used.\n \u2022 inspect opens the app in a headless (or --headed) browser *with the cached session loaded* and prints the\n page's [data-testid] elements (or, with --selector, matching elements' HTML) \u2014 for pinning locators when\n hand-authoring a flow-file (the captured session can't be replayed in a browser the agent's MCP controls\n because the auth cookie is usually httpOnly; inspect does the storageState\u2192Playwright bridge for you). On a\n slow SPA, settle before the snapshot with --wait <ms> (default 800) or --wait-for '<css>'. --cdp <endpoint>\n attaches to an already-running Chrome (e.g. the one from capture-auth --cdp) instead of launching one.\n \u2022 calibrate takes a *structured flow-guide* (a flow-file in YAML, or a .md with a yaml fenced block) and\n writes flows/<name>.flow.yaml + a default docs/style.yaml. Loose-prose descriptions / live element-picking\n need the host agent \u2014 that's the /docsxai:calibrate *skill* (see the plugin), which then refines/produces\n the flow-file; this CLI command covers only the deterministic structured-input case.\n \u2022 lint runs pure-static checks across the workspace's flow-files \u2014 no Playwright, no live page. Rules:\n R001 (deep extends chain), R002 (annotation anchored to a likely-unmounting click/navigate target \u2014\n suggest annotation.target override), R003 (wait_for with no timeout_ms on a long-async-looking step),\n R004 (bare [data-*=\u2026] selector \u2014 may have hidden duplicates; suggest :visible / :has-text qualifier).\n Exit 1 if any warning/error; 0 otherwise. --format json emits machine-readable output for tooling.\n \u2022 flow-tree prints the workspace's extends graph (root flows + their descendants), plus any orphans\n (flows whose extends parent isn't in the workspace) and resolution issues (cycles / step-id collisions\n across the merge). Pure-static, ~no I/O beyond reading the flow files. Exit 1 if any issues.\n \u2022 diagnose gathers halt context for a specific step (the step's selector/wait_for/success, the halt\n screenshot if one exists, and \u2014 with --cdp \u2014 a live actionable() probe of the target on the running\n page) and prints recommendations (selector / wait_for / success / annotation_target / split_step /\n investigate). The engine never patches the flow-file itself \u2014 that's the agent's explicit opt-in\n action. --format json emits machine-readable output for an agent to act on. Pair with\n --start-from <step-id> --cdp on a follow-up run to validate the fix in seconds.\n \u2022 doctor health-checks the environment + workspace: Node >= 20, Chromium presence, .docsxai.json\n found + parseable (cwd or the arg), flow-file parses, auth descriptor + cached-session freshness,\n backend reachability (when backend_url is set), the plugin declarations (same inspection as\n `plugins list` \u2014 no plugin code is executed), viewer-bin resolution (which of the three layers\n hit), and DOCSX_* env sanity. \u2713/\u2717 rows with a one-line fix per failure; \u2212 rows are informational\n and never fail. Exit 1 if any \u2717.\n \u2022 style initialises docs/style.yaml + derived docs/style.json if absent (otherwise validates the\n existing YAML against the schema and rederives the JSON). --check additionally scans every\n docs/<flow>/<step>.md user-facing write-up for jargon leaks against the style's pruning_rules\n (e.g. VERIFY / WAIT / data-testid leaking into user-facing prose). The engine never re-shapes\n prose itself (LLM-agnostic) \u2014 the agent does that at calibration time; this command is the\n enforcement layer for the semantic-reshape exit criterion. --format json emits machine-readable\n output for tooling.\n \u2022 zip packages the workspace's doc pack into a single archive for hand-off. Includes flows/, docs/,\n .docsxai.json, auth/strategy.yaml (env-var names only, no creds), README.md. Excludes .auth/\n (operator-local session state), **/halts/ (debug screenshots), .viewer/ by default (re-renderable\n from the doc pack; pass --include-viewer to bundle it). Defaults output to <workspace-name>.zip\n in the current dir; override with --out <path>. Zips in-process (no system 'zip' binary needed)\n and deterministically \u2014 sorted entries, fixed mtime, fixed compression \u2014 so the same doc pack\n always produces a byte-identical archive.\n \u2022 baseline snapshots the doc pack \u2014 flows/, docs/<flow>/*.md, annotations.json, screenshots/, and\n docs/locators.yaml \u2014 into <ws>/.baseline/ (or --out <dir>). Commit the baseline: it's the \"before\"\n that diff compares against in CI.\n \u2022 diff compares the workspace against a baseline (default <ws>/.baseline/, or --against <dir>) and\n emits a deterministic drift report: per flow, step field deltas (id-keyed), annotation moves,\n screenshot pixel diffs (changed-pixel count / % / changed-region bbox; dimension changes flagged\n distinctly), prose line-change counts, and locator changes. --format md is PR-comment-ready.\n --fail-on warn|fail exits 1 when the report severity is at/above the threshold (screenshot\n severity: \u22651% changed pixels = warn, \u22655% = fail; structural changes = warn).\n \u2022 export playwright emits one self-contained Playwright .spec.ts per flow (extends resolved) into\n <ws>/.export/tests/ (or --out <dir>) \u2014 locators as consts, steps as page actions, success criteria\n as expect() assertions, environment as test.use(); optional steps are try/catch-wrapped. Generated\n files say so in a header: regenerate, don't hand-edit.\n \u2022 render builds the static viewer by spawning the docsxai-viewer bin, resolved in order: the\n DOCSX_VIEWER_BIN env var (path to the viewer's bin script), the @docsxai/viewer\n package installed next to the engine, then `docsxai-viewer` on PATH.\n \u2022 login validates a bearer token against a backend URL \u2014 hits /v1/health, /v1/workspaces. Reads\n the token from DOCSX_TOKEN env var. Prints what the backend sees if the call succeeds,\n or a clear error if not. Stateless: doesn't store anything; configure the env var in your shell.\n \u2022 push serialises the workspace's doc pack (flows + annotations + screenshots + style + locators)\n and POSTs it as a new revision against the backend named in .docsxai.json (backend_url +\n optionally backend_workspace_id / backend_project_id; created on first push if absent and\n persisted to the config). --kind defaults to \"calibrate\"; --author defaults to the OS user.\n \u2022 pull fetches a revision's artifacts back into the workspace files (default: HEAD). Useful for\n syncing with a different operator's edits or rolling back to a named revision.";
@@ -0,0 +1,137 @@
1
+ // `docsxai` CLI help text. Split out of cli.ts so the dispatch barrel and every command cluster
2
+ // can share the one authoritative USAGE string (commands print it on a missing-arg bail; the
3
+ // barrel prints it for --help / unknown-command). Text only — no imports, the truest leaf.
4
+ export const USAGE = `docsxai — deterministic execution CLI
5
+
6
+ Usage:
7
+ docsxai init <workspace-dir> [--app-url <url>] [--auth manual-capture|none] [--role <name>] [--ttl <dur>]
8
+ [--capture-trigger console|button] [--auth-cookie <name>] [--ignore-https-errors]
9
+ [--persist tmp] [--force]
10
+ docsxai calibrate <workspace-dir> --from <flow.md|.yaml> [--name <flow>]
11
+ docsxai inspect <workspace-dir> [--url <url>] [--selector <css>] [--cdp <endpoint>] [--wait <ms>] [--wait-for <css>] [--headed] [--role <role>]
12
+ docsxai run <workspace-dir> [--flow <name>] [--base-url <url>] [--headed] [--ignore-https-errors] [--stop-after <step-id>] [--start-from <step-id>] [--cdp <endpoint>] [--pause] [--concurrency <N>]
13
+ docsxai lint <workspace-dir> [--flow <name>] [--format text|json]
14
+ docsxai flow-tree <workspace-dir> [--format text|json]
15
+ docsxai diagnose <workspace-dir> --flow <name> --step <step-id> [--cdp <endpoint>] [--format text|json]
16
+ docsxai doctor [<workspace-dir>]
17
+ docsxai style <workspace-dir> [--check] [--format text|json]
18
+ docsxai zip <workspace-dir> [--out <output.zip>] [--include-viewer]
19
+ docsxai baseline <workspace-dir> [--out <dir>]
20
+ docsxai diff <workspace-dir> [--against <dir>] [--format json|md|text] [--fail-on warn|fail]
21
+ docsxai export adf <workspace-dir> [--flow <name>] [--mode single|page-tree] [--title <text>] [--out <dir>]
22
+ docsxai export playwright <workspace-dir> [--flow <name>] [--out <dir>]
23
+ docsxai plugins <list|info|sync> <workspace-dir> [<namespace>] [--format text|json]
24
+ docsxai login --backend-url <url>
25
+ docsxai push <workspace-dir> [--kind calibrate|run|edit] [--author <name>]
26
+ docsxai pull <workspace-dir> [--rev <id>]
27
+ docsxai render <workspace-dir>
28
+ docsxai capture-auth <workspace-dir> [--base-url <url>] [--role <role>] [--auth-cookie <name>] [--cdp <endpoint>] [--fresh] [--headless] [--ignore-https-errors]
29
+ docsxai --help
30
+
31
+ Notes:
32
+ • A *workspace* (created by \`init\`) holds flows/<flow>.flow.yaml, docs/, auth/strategy.yaml, .auth/, .viewer/,
33
+ and a .docsxai.json config. Put it OUTSIDE the app's source repo — docsxai documents a running app from
34
+ outside and never writes into the app repo.
35
+ • run / capture-auth read app_url + ignore_https_errors from .docsxai.json if you don't pass the flags.
36
+ • run launches Chromium; if no browser binary is present, install one: npx playwright-core install chromium
37
+ (source checkout: pnpm -C packages/engine exec playwright-core install chromium)
38
+ • --ignore-https-errors accepts self-signed/invalid TLS (e.g. an app's local HTTPS dev cert)
39
+ • run --stop-after <step-id> runs only a prefix of the flow (up to & incl. that step); --pause keeps the
40
+ (headed) browser open at the last step run — so you can inspect the live state mid-flow when calibrating
41
+ (pair with --flow <name>). For waiting on a slow backend op, give a step a wait_for of the form
42
+ { selector: $x, timeout_ms: 180000 } — a per-step override of the default ~30s selector-wait timeout.
43
+ • run --concurrency <N> runs up to N flows in parallel (each its own Chromium session, isolated; default 1).
44
+ Useful when several flows share a long preamble — total wall time = max(per-flow), not sum. Force-clamped
45
+ to 1 when --pause / --stop-after / --start-from / --cdp is set. The target app must tolerate multiple sessions from one user.
46
+ • run --start-from <step-id> --flow <name> SKIPS every step before <step-id> and starts execution there —
47
+ the inverse of --stop-after. Pair with --cdp to attach to a Chrome that's already in the post-prior-steps
48
+ state (e.g. left over from a paused previous run) and iterate on the new tail step in seconds rather
49
+ than re-walking the whole extends chain. New annotations MERGE into the existing annotations.json by
50
+ step id; the prior steps' annotations and screenshots are preserved.
51
+ • run --cdp <endpoint> attaches to a running Chrome (start it with --remote-debugging-port=N) instead of
52
+ launching one. docsxai won't close that Chrome. When --cdp is set, the cached storageState is NOT
53
+ loaded into the context — the operator's Chrome owns its auth state. Useful with --start-from for the
54
+ sub-3-sec iteration loop on long-async flows.
55
+ • capture-auth runs the role's auth strategy (MVP: manual-capture — a headed, instrumented browser the
56
+ engineer logs into; window.__docsxai.capture() or an injected button snapshots the session) and caches
57
+ it to <workspace-dir>/.auth/<role>.json for subsequent \`run\`s. It prints the captured cookie jar so you
58
+ can identify the app's real auth/session cookie.
59
+ • capture-auth keeps a persistent Chrome profile at <workspace>/.auth/chrome-profile/ (gitignored) — re-running
60
+ it reuses the login (just trigger capture again). Use --fresh for a clean profile (forces a fresh login).
61
+ • --cdp <endpoint> makes capture-auth *attach to an already-running Chrome* (start it with
62
+ --remote-debugging-port=N --disable-web-security --user-data-dir=<dir>) instead of launching a fresh one —
63
+ use this to capture from the same Chrome the engineer is already logged into (and that Claude in Chrome is
64
+ driving for discovery), so they don't log in twice. docsxai won't close that Chrome. (--cdp ignores --fresh.)
65
+ • auth_cookie (set via \`init --auth-cookie\`, \`capture-auth --auth-cookie\`, or hand-edited into
66
+ auth/strategy.yaml) names that cookie; when set, the cached session's expiry tracks *that* cookie's
67
+ expiry rather than the \`ttl\` guess (an interactive SSO login leaves ephemeral IdP scratch cookies, so
68
+ \`min(cookie.expires)\` ≈ now — don't rely on it). If unset/unfound, \`ttl\` (or a 1h default) is used.
69
+ • inspect opens the app in a headless (or --headed) browser *with the cached session loaded* and prints the
70
+ page's [data-testid] elements (or, with --selector, matching elements' HTML) — for pinning locators when
71
+ hand-authoring a flow-file (the captured session can't be replayed in a browser the agent's MCP controls
72
+ because the auth cookie is usually httpOnly; inspect does the storageState→Playwright bridge for you). On a
73
+ slow SPA, settle before the snapshot with --wait <ms> (default 800) or --wait-for '<css>'. --cdp <endpoint>
74
+ attaches to an already-running Chrome (e.g. the one from capture-auth --cdp) instead of launching one.
75
+ • calibrate takes a *structured flow-guide* (a flow-file in YAML, or a .md with a yaml fenced block) and
76
+ writes flows/<name>.flow.yaml + a default docs/style.yaml. Loose-prose descriptions / live element-picking
77
+ need the host agent — that's the /docsxai:calibrate *skill* (see the plugin), which then refines/produces
78
+ the flow-file; this CLI command covers only the deterministic structured-input case.
79
+ • lint runs pure-static checks across the workspace's flow-files — no Playwright, no live page. Rules:
80
+ R001 (deep extends chain), R002 (annotation anchored to a likely-unmounting click/navigate target —
81
+ suggest annotation.target override), R003 (wait_for with no timeout_ms on a long-async-looking step),
82
+ R004 (bare [data-*=…] selector — may have hidden duplicates; suggest :visible / :has-text qualifier).
83
+ Exit 1 if any warning/error; 0 otherwise. --format json emits machine-readable output for tooling.
84
+ • flow-tree prints the workspace's extends graph (root flows + their descendants), plus any orphans
85
+ (flows whose extends parent isn't in the workspace) and resolution issues (cycles / step-id collisions
86
+ across the merge). Pure-static, ~no I/O beyond reading the flow files. Exit 1 if any issues.
87
+ • diagnose gathers halt context for a specific step (the step's selector/wait_for/success, the halt
88
+ screenshot if one exists, and — with --cdp — a live actionable() probe of the target on the running
89
+ page) and prints recommendations (selector / wait_for / success / annotation_target / split_step /
90
+ investigate). The engine never patches the flow-file itself — that's the agent's explicit opt-in
91
+ action. --format json emits machine-readable output for an agent to act on. Pair with
92
+ --start-from <step-id> --cdp on a follow-up run to validate the fix in seconds.
93
+ • doctor health-checks the environment + workspace: Node >= 20, Chromium presence, .docsxai.json
94
+ found + parseable (cwd or the arg), flow-file parses, auth descriptor + cached-session freshness,
95
+ backend reachability (when backend_url is set), the plugin declarations (same inspection as
96
+ \`plugins list\` — no plugin code is executed), viewer-bin resolution (which of the three layers
97
+ hit), and DOCSX_* env sanity. ✓/✗ rows with a one-line fix per failure; − rows are informational
98
+ and never fail. Exit 1 if any ✗.
99
+ • style initialises docs/style.yaml + derived docs/style.json if absent (otherwise validates the
100
+ existing YAML against the schema and rederives the JSON). --check additionally scans every
101
+ docs/<flow>/<step>.md user-facing write-up for jargon leaks against the style's pruning_rules
102
+ (e.g. VERIFY / WAIT / data-testid leaking into user-facing prose). The engine never re-shapes
103
+ prose itself (LLM-agnostic) — the agent does that at calibration time; this command is the
104
+ enforcement layer for the semantic-reshape exit criterion. --format json emits machine-readable
105
+ output for tooling.
106
+ • zip packages the workspace's doc pack into a single archive for hand-off. Includes flows/, docs/,
107
+ .docsxai.json, auth/strategy.yaml (env-var names only, no creds), README.md. Excludes .auth/
108
+ (operator-local session state), **/halts/ (debug screenshots), .viewer/ by default (re-renderable
109
+ from the doc pack; pass --include-viewer to bundle it). Defaults output to <workspace-name>.zip
110
+ in the current dir; override with --out <path>. Zips in-process (no system 'zip' binary needed)
111
+ and deterministically — sorted entries, fixed mtime, fixed compression — so the same doc pack
112
+ always produces a byte-identical archive.
113
+ • baseline snapshots the doc pack — flows/, docs/<flow>/*.md, annotations.json, screenshots/, and
114
+ docs/locators.yaml — into <ws>/.baseline/ (or --out <dir>). Commit the baseline: it's the "before"
115
+ that diff compares against in CI.
116
+ • diff compares the workspace against a baseline (default <ws>/.baseline/, or --against <dir>) and
117
+ emits a deterministic drift report: per flow, step field deltas (id-keyed), annotation moves,
118
+ screenshot pixel diffs (changed-pixel count / % / changed-region bbox; dimension changes flagged
119
+ distinctly), prose line-change counts, and locator changes. --format md is PR-comment-ready.
120
+ --fail-on warn|fail exits 1 when the report severity is at/above the threshold (screenshot
121
+ severity: ≥1% changed pixels = warn, ≥5% = fail; structural changes = warn).
122
+ • export playwright emits one self-contained Playwright .spec.ts per flow (extends resolved) into
123
+ <ws>/.export/tests/ (or --out <dir>) — locators as consts, steps as page actions, success criteria
124
+ as expect() assertions, environment as test.use(); optional steps are try/catch-wrapped. Generated
125
+ files say so in a header: regenerate, don't hand-edit.
126
+ • render builds the static viewer by spawning the docsxai-viewer bin, resolved in order: the
127
+ DOCSX_VIEWER_BIN env var (path to the viewer's bin script), the @docsxai/viewer
128
+ package installed next to the engine, then \`docsxai-viewer\` on PATH.
129
+ • login validates a bearer token against a backend URL — hits /v1/health, /v1/workspaces. Reads
130
+ the token from DOCSX_TOKEN env var. Prints what the backend sees if the call succeeds,
131
+ or a clear error if not. Stateless: doesn't store anything; configure the env var in your shell.
132
+ • push serialises the workspace's doc pack (flows + annotations + screenshots + style + locators)
133
+ and POSTs it as a new revision against the backend named in .docsxai.json (backend_url +
134
+ optionally backend_workspace_id / backend_project_id; created on first push if absent and
135
+ persisted to the config). --kind defaults to "calibrate"; --author defaults to the OS user.
136
+ • pull fetches a revision's artifacts back into the workspace files (default: HEAD). Useful for
137
+ syncing with a different operator's edits or rolling back to a named revision.`;
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(argv: string[]): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ // `docsxai` — the deterministic CLI. The plugin's commands wrap this; calibration runs in an agent
3
+ // context and is exposed by the plugin, not here.
4
+ //
5
+ // Barrel / dispatch: the command bodies live in flat siblings, grouped by cohesion. This file parses
6
+ // argv, routes the subcommand to its handler, and re-exports `main` so `./cli.js` callers (the bin,
7
+ // the colocated tests) are unchanged. The clusters import shared helpers from cli-shared.ts (a leaf)
8
+ // and the help text from cli-usage.ts — no cluster imports back from here, so there's no cycle.
9
+ // • cli-usage.ts — the USAGE help-text constant
10
+ // • cli-shared.ts — parseFlags + listFlowFiles (the shared leaf)
11
+ // • cli-commands-session.ts — init / capture-auth / calibrate / run (live-browser + scaffolding)
12
+ // • cli-commands-authoring.ts — inspect / lint / flow-tree / diagnose / style (calibration aids)
13
+ // • cli-commands-docpack.ts — render / zip / export / baseline / diff (doc-pack ops)
14
+ // • cli-commands-backend.ts — login / push / pull / plugins (backend + sync)
15
+ import { pathToFileURL } from "node:url";
16
+ import { runDoctor } from "./doctor.js";
17
+ import { USAGE } from "./cli-usage.js";
18
+ import { cmdCalibrate, cmdCaptureAuth, cmdInit, cmdRun } from "./cli-commands-session.js";
19
+ import { cmdDiagnose, cmdFlowTree, cmdInspect, cmdLint, cmdStyle, } from "./cli-commands-authoring.js";
20
+ import { cmdBaseline, cmdDiff, cmdExport, cmdRender, cmdZip } from "./cli-commands-docpack.js";
21
+ import { cmdLogin, cmdPlugins, cmdPull, cmdPush } from "./cli-commands-backend.js";
22
+ export async function main(argv) {
23
+ const [cmd, ...rest] = argv;
24
+ switch (cmd) {
25
+ case undefined:
26
+ case "--help":
27
+ case "-h":
28
+ case "help":
29
+ process.stdout.write(USAGE + "\n");
30
+ return 0;
31
+ case "init":
32
+ return cmdInit(rest);
33
+ case "calibrate":
34
+ return cmdCalibrate(rest);
35
+ case "inspect":
36
+ return cmdInspect(rest);
37
+ case "run":
38
+ return cmdRun(rest);
39
+ case "render":
40
+ return cmdRender(rest);
41
+ case "capture-auth":
42
+ return cmdCaptureAuth(rest);
43
+ case "lint":
44
+ return cmdLint(rest);
45
+ case "flow-tree":
46
+ return cmdFlowTree(rest);
47
+ case "diagnose":
48
+ return cmdDiagnose(rest);
49
+ case "doctor":
50
+ return runDoctor(rest);
51
+ case "style":
52
+ return cmdStyle(rest);
53
+ case "zip":
54
+ return cmdZip(rest);
55
+ case "baseline":
56
+ return cmdBaseline(rest);
57
+ case "diff":
58
+ return cmdDiff(rest);
59
+ case "export":
60
+ return cmdExport(rest);
61
+ case "plugins":
62
+ return cmdPlugins(rest);
63
+ case "login":
64
+ return cmdLogin(rest);
65
+ case "push":
66
+ return cmdPush(rest);
67
+ case "pull":
68
+ return cmdPull(rest);
69
+ default:
70
+ process.stderr.write(`unknown command: ${cmd}\n\n${USAGE}\n`);
71
+ return 2;
72
+ }
73
+ }
74
+ // Run as the bin entry, but not when imported (e.g. in tests).
75
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
76
+ void main(process.argv.slice(2)).then((code) => process.exit(code));
77
+ }
@@ -0,0 +1,50 @@
1
+ import type { BoundingBox, FlowFile, Step } from "./doc-pack.js";
2
+ import type { ActionableState, BrowserDriver } from "./flow-runtime.js";
3
+ export type DiagnoseRecommendationKind = "selector" | "wait_for" | "success" | "annotation_target" | "split_step" | "investigate";
4
+ export interface DiagnoseRecommendation {
5
+ kind: DiagnoseRecommendationKind;
6
+ rationale: string;
7
+ suggestion: string;
8
+ }
9
+ export interface DiagnoseLiveProbe {
10
+ cdpEndpoint: string;
11
+ url: string;
12
+ actionable: ActionableState;
13
+ bbox: BoundingBox | null;
14
+ }
15
+ export interface DiagnoseReport {
16
+ workspace: string;
17
+ flow: string;
18
+ step: {
19
+ id: string;
20
+ action: Step["action"];
21
+ target?: string;
22
+ resolvedSelector?: string;
23
+ wait_for?: unknown;
24
+ success?: unknown;
25
+ };
26
+ halt: {
27
+ screenshotRelPath?: string;
28
+ screenshotAbsPath?: string;
29
+ };
30
+ live?: DiagnoseLiveProbe;
31
+ recommendations: DiagnoseRecommendation[];
32
+ }
33
+ /** Map an `ActionableState` to concrete recommendations for the agent. */
34
+ export declare function recommendFromActionable(state: ActionableState): DiagnoseRecommendation[];
35
+ /** Static recommendations from the flow-file shape alone (no live probe). */
36
+ export declare function recommendStatic(step: Step, halt: {
37
+ screenshotRelPath?: string;
38
+ }): DiagnoseRecommendation[];
39
+ /** Build the full report. `liveProbe` is invoked lazily (only if a driver is supplied). */
40
+ export declare function buildDiagnoseReport(opts: {
41
+ workspace: string;
42
+ flow: FlowFile;
43
+ step: Step;
44
+ resolvedSelector?: string;
45
+ haltScreenshotAbsPath?: string;
46
+ liveProbe?: () => Promise<DiagnoseLiveProbe>;
47
+ }): Promise<DiagnoseReport>;
48
+ /** Live-probe helper that uses any driver implementing the runtime's `BrowserDriver` interface. */
49
+ export declare function probeLive(driver: BrowserDriver, selector: string, cdpEndpoint: string): Promise<DiagnoseLiveProbe>;
50
+ export declare function formatReportText(r: DiagnoseReport): string;
@@ -0,0 +1,168 @@
1
+ // Halt diagnosis — gather the context a calibration agent needs to propose a recalibration diff.
2
+ // Pure data + recommendations; the engine never patches the flow-file itself (that's an explicit
3
+ // opt-in action by the agent / human, never ambient). The agent reads the report, walks the live
4
+ // page (typically via browxai), picks a fix, and edits the flow-file. `docsxai run --start-from
5
+ // <step-id> --cdp <endpoint>` then validates the edit in seconds.
6
+ import { promises as fs } from "node:fs";
7
+ import * as path from "node:path";
8
+ /** Map an `ActionableState` to concrete recommendations for the agent. */
9
+ export function recommendFromActionable(state) {
10
+ switch (state) {
11
+ case "actionable":
12
+ return [
13
+ {
14
+ kind: "investigate",
15
+ rationale: "The current target is actionable on the live page right now.",
16
+ suggestion: "Drift may be intermittent (race condition) or in the `success` criterion. Consider adding `wait_for: network_idle` or `element_stable`; re-check the `success` clause against the live target state.",
17
+ },
18
+ ];
19
+ case "not-found":
20
+ return [
21
+ {
22
+ kind: "selector",
23
+ rationale: "Selector matches 0 elements on the live page — the element was renamed, moved, or removed.",
24
+ suggestion: "Re-discover the element via the calibration loop (browxai's `find()`, or `docsxai inspect`). Pick a new canonical locator and commit it as the step's `target` (or update the named entry in the flow's `locators:` block).",
25
+ },
26
+ ];
27
+ case "multiple-matches":
28
+ return [
29
+ {
30
+ kind: "selector",
31
+ rationale: "Selector matches multiple DOM nodes — strict-mode violation territory; one of them is likely a hidden duplicate.",
32
+ suggestion: "Scope the selector: append `:visible`, use `:nth-match(<sel>, 1)`, or add a stable qualifier (parent class, `:has-text(...)`). Avoid silently picking one — the duplicate signal usually means the locator isn't specific enough.",
33
+ },
34
+ ];
35
+ case "disabled":
36
+ return [
37
+ {
38
+ kind: "investigate",
39
+ rationale: "Selector resolves to a single visible element that is `disabled`.",
40
+ suggestion: "Check whether the action is appropriate for the target state — sometimes a UI element is intentionally disabled (clip-driven inputs, gated controls). The doc may need to describe the disabled-state behaviour rather than trying to act on the element. If unexpected, the disabled state itself is a product-side question.",
41
+ },
42
+ ];
43
+ case "detached":
44
+ return [
45
+ {
46
+ kind: "annotation_target",
47
+ rationale: "Selector resolved but the element isn't attached to the DOM any more — usually unmounted by the action itself (the step's action transitions the UI, and the original target is gone in the post-action state).",
48
+ suggestion: "Set the annotation's `target` to a different element that survives the transition (an element in the resulting state). The action's `target` stays the same; only the annotation anchor moves.",
49
+ },
50
+ ];
51
+ case "not-visible":
52
+ return [
53
+ {
54
+ kind: "wait_for",
55
+ rationale: "Selector resolves to a single element but it's hidden (`display: none` / `visibility: hidden` / zero-size).",
56
+ suggestion: "Add (or strengthen) `wait_for: { selector: <sel>, timeout_ms: <ms> }` to give the element time to become visible. If it's permanently hidden, the locator probably moved — re-discover.",
57
+ },
58
+ ];
59
+ case "off-screen":
60
+ return [
61
+ {
62
+ kind: "split_step",
63
+ rationale: "Selector resolves to a CSS-visible element but it's outside the viewport, and Playwright's auto-scroll didn't reach it.",
64
+ suggestion: "Insert a scroll-into-view step before the action (or pick a parent element that's in the viewport). Off-screen elements inside `overflow: auto` containers are usually fine — this state means the page scroll is the issue, not a scroller.",
65
+ },
66
+ ];
67
+ case "covered":
68
+ return [
69
+ {
70
+ kind: "split_step",
71
+ rationale: "Another element receives clicks at this element's center — likely a modal, overlay, or tooltip covering the target.",
72
+ suggestion: "Insert a step to dismiss the covering element (close button, click outside, ESC key), then act on the original target.",
73
+ },
74
+ ];
75
+ }
76
+ }
77
+ /** Static recommendations from the flow-file shape alone (no live probe). */
78
+ export function recommendStatic(step, halt) {
79
+ const recs = [];
80
+ if (halt.screenshotRelPath) {
81
+ recs.push({
82
+ kind: "investigate",
83
+ rationale: `A halt screenshot exists at \`${halt.screenshotRelPath}\` — read it first; on most halts the visual state shows the issue immediately.`,
84
+ suggestion: "Open the screenshot; compare the visual state to what the step expects.",
85
+ });
86
+ }
87
+ const success = step.success;
88
+ if (success && typeof success === "object" && success !== null && "text_contains" in success) {
89
+ recs.push({
90
+ kind: "success",
91
+ rationale: "Success criterion uses `text_contains` — fragile against UI copy changes / localisation drift.",
92
+ suggestion: "Verify the expected text still appears in the post-action state. If the text changed, update it; if the text shifted to a different element, change the success selector. Where stable, a structural criterion (visible/hidden element, url_matches) is more drift-resistant.",
93
+ });
94
+ }
95
+ return recs;
96
+ }
97
+ /** Build the full report. `liveProbe` is invoked lazily (only if a driver is supplied). */
98
+ export async function buildDiagnoseReport(opts) {
99
+ const haltExists = opts.haltScreenshotAbsPath
100
+ ? await fs
101
+ .access(opts.haltScreenshotAbsPath)
102
+ .then(() => true)
103
+ .catch(() => false)
104
+ : false;
105
+ const haltRel = haltExists
106
+ ? path.relative(opts.workspace, opts.haltScreenshotAbsPath)
107
+ : undefined;
108
+ const live = opts.liveProbe ? await opts.liveProbe() : undefined;
109
+ const recommendations = [
110
+ ...recommendStatic(opts.step, { screenshotRelPath: haltRel }),
111
+ ...(live ? recommendFromActionable(live.actionable) : []),
112
+ ];
113
+ return {
114
+ workspace: opts.workspace,
115
+ flow: opts.flow.name,
116
+ step: {
117
+ id: opts.step.id,
118
+ action: opts.step.action,
119
+ ...(opts.step.target ? { target: opts.step.target } : {}),
120
+ ...(opts.resolvedSelector ? { resolvedSelector: opts.resolvedSelector } : {}),
121
+ ...(opts.step.wait_for !== undefined ? { wait_for: opts.step.wait_for } : {}),
122
+ ...(opts.step.success !== undefined ? { success: opts.step.success } : {}),
123
+ },
124
+ halt: {
125
+ ...(haltRel ? { screenshotRelPath: haltRel } : {}),
126
+ ...(haltExists ? { screenshotAbsPath: opts.haltScreenshotAbsPath } : {}),
127
+ },
128
+ ...(live ? { live } : {}),
129
+ recommendations,
130
+ };
131
+ }
132
+ /** Live-probe helper that uses any driver implementing the runtime's `BrowserDriver` interface. */
133
+ export async function probeLive(driver, selector, cdpEndpoint) {
134
+ const url = await driver.currentUrl();
135
+ const actionable = await driver.actionable(selector);
136
+ const bbox = await driver.boundingBox(selector, 500).catch(() => null);
137
+ return { cdpEndpoint, url, actionable, bbox };
138
+ }
139
+ export function formatReportText(r) {
140
+ let out = `diagnose: flow=${r.flow} step=${r.step.id}\n\n`;
141
+ out += `Current step:\n`;
142
+ out += ` action: ${r.step.action}\n`;
143
+ if (r.step.target)
144
+ out += ` target: ${r.step.target}${r.step.resolvedSelector && r.step.resolvedSelector !== r.step.target ? ` (resolved: ${r.step.resolvedSelector})` : ""}\n`;
145
+ if (r.step.wait_for !== undefined)
146
+ out += ` wait_for: ${JSON.stringify(r.step.wait_for)}\n`;
147
+ if (r.step.success !== undefined)
148
+ out += ` success: ${JSON.stringify(r.step.success)}\n`;
149
+ out += `\n`;
150
+ if (r.halt.screenshotRelPath) {
151
+ out += `Halt artifacts:\n screenshot: ${r.halt.screenshotRelPath}\n\n`;
152
+ }
153
+ else {
154
+ out += `Halt artifacts:\n (no halt screenshot found at the expected path — run hasn't halted on this step recently, or screenshots are disabled)\n\n`;
155
+ }
156
+ if (r.live) {
157
+ out += `Live probe (via ${r.live.cdpEndpoint}):\n`;
158
+ out += ` url: ${r.live.url}\n`;
159
+ out += ` actionable: ${r.live.actionable}\n`;
160
+ out += ` bbox: ${r.live.bbox ? JSON.stringify(r.live.bbox) : "null"}\n\n`;
161
+ }
162
+ out += `Recommendations (${r.recommendations.length}):\n`;
163
+ for (const rec of r.recommendations) {
164
+ out += ` [${rec.kind}] ${rec.rationale}\n`;
165
+ out += ` → ${rec.suggestion}\n`;
166
+ }
167
+ return out;
168
+ }
@@ -0,0 +1,13 @@
1
+ import { type DriftPolicy, type DriftRegion, type DriftReport, type PngDiffResult } from "./diff-types.js";
2
+ /**
3
+ * Exact-RGBA pixel diff between two PNG buffers. Dimension mismatch is reported distinctly (no
4
+ * pixel comparison is meaningful across sizes). `ignoreRegions` rectangles are excluded from the
5
+ * comparison; `pct` is changed pixels over the FULL image area, rounded to 4 decimals.
6
+ */
7
+ export declare function diffPngBuffers(aPng: Buffer, bPng: Buffer, ignoreRegions?: DriftRegion[]): PngDiffResult;
8
+ /**
9
+ * Diff two doc-pack directories (each a workspace-shaped tree: `flows/` + `docs/`). `aDir` is the
10
+ * baseline ("before"), `bDir` the candidate ("after"). Pure file → JSON transform; deterministic
11
+ * (no timestamps); never writes.
12
+ */
13
+ export declare function diffDocPacks(aDir: string, bDir: string, options?: DriftPolicy): Promise<DriftReport>;