Skip to content

Agent reference — validation I/O

Exact shapes and decision rules for driving r$ programmatically. Human-oriented docs: design guide.

Commands

rjs analyze <url>  [-d auto|playwright|agent-browser] [-w 320,768,1280] [-s "main,.card"]
                   [-f console|json|sarif] [-o file] [--no-a11y] [--strict] [--scroll]
                   [--touch-min 24] [--height 900]
rjs verify  <contract.json> <url>  [same driver/format/out flags]
rjs record  <contract.json> <url>  [-o other.json]
rjs init    <url>  [-o contract.json]   # generate a contract FROM the page's constructs
rjs audit   <url>  [--vs url] [--crawl] [--max-pages n] [--screenshots dir] [-o report.html]
                                   # human-facing HTML report — prefer analyze -f json for loops
rjs doctor                         # environment readiness: exit 0 = a driver is usable

Exit codes — gate on these: 0 pass · 1 violations · 2 usage/run error (bad args, missing driver, invalid contract). --strict makes analyze exit 1 on warnings/info too.

UnifiedReport (analyze -f json)

jsonc
{
    "pass": false,          // no ERROR-severity violations (loop gate)
    "clean": false,         // no violations at all (polish gate)
    "total": 46, "passed": 39, "failed": 7,   // failed counts CHECKS (can be < violations.length;
                                              // one check may carry several violations). Never negative.
    "widths": [320, 1280],
    "sources": { "measurement": "playwright|agent-browser|eval|cdp|store", "a11y": "axe|skipped|unavailable" },
    "summary": { "errors": 7, "warnings": 0, "info": 0, "byRule": {"noOverflow": 1}, "byWidth": {"320": 3} },
    "violations": [ /* Violation[] — see below */ ],
    "fixes": [ /* Fix[] — ONLY kind:'exact' entries, deduped by (selector, property)
                  across widths. Safe to apply verbatim, no parsing needed. */ ],
    "scores": { "average": { "overall": 0.57, /* +17 metrics 0..1 */ }, "perWidth": {} },
    "manifest": [ /* ProvenanceEntry[] — present when the page runs @responsivejs/runtime:
                     {id, construct, target, behavior[], source?, config?} — what controls the
                     page. config is the SERIALIZED declaration (e.g. {fontSize: {value:'fluid',
                     min:16, max:32, curve:'exponential'}}) — everything needed to regenerate
                     the construct call. */ ],
    "durationMs": 4200
}

Violation

jsonc
{
    "rule": "noOverflow",          // or "axe:<id>", "score.<metric>", "baseline"
    "element": ".card[0]",         // selector[index]
    "width": 320,                  // px where it was measured
    "detail": "right=496 > viewport=320",
    "severity": "error|warning|info",   // MISSING severity counts as error
    "expected": 320, "actual": 496,     // when numeric
    "fix": { "selector": ".card", "property": "max-width", "value": "100%", "reason": "…",
             "kind": "exact" },     // exact = apply `selector { property: value }` verbatim;
                                    // heuristic = direction only (value may be a placeholder);
                                    // runtime-patch = see below
    "owner": {                     // PROVENANCE: the runtime construct that owns this element
        "construct": "style",      // style | geometry | tokens | sync | ratio | breakpoints
        "behavior": ["width: fluid"],
        "source": "src/cards.ts:12",  // best-effort call site
        "via": ".site-nav"            // only when the construct owns an ANCESTOR: the manifest
                                      // target that matched (element ".site-nav a[3]" is owned
                                      // through its ".site-nav" construct)
    },
    "owners": [ /* present when SEVERAL constructs own the element — same shape,
                   most specific first; `owner` is always owners[0] */ ]
}

The runtime-patch fix

When the owning construct controls the very property a fix would patch, the fix arrives as kind: "runtime-patch" instead — a CSS patch there would be overwritten by the runtime:

jsonc
{
    "kind": "runtime-patch",
    "selector": ".hero", "property": "font-size", "value": "14px",
    "construct": "style",
    "source": "src/hero.ts:3",
    "change": {
        "property": "fontSize",
        "current": { "value": "fluid", "min": 10, "max": 28 },  // the declaration as written
        "suggested": "14px"                                     // CSS value that satisfies the constraint
    },
    "reason": "'font-size' is controlled by the style construct at src/hero.ts:3 — …"
}

Mechanical recipe: open source, find the construct call, recompute its parameters so the declaration satisfies suggested (here: raise the fluid min from 10 to 14). Never patch the CSS for these.

Agent loop

  1. rjs analyze <url> -f json (or verify against a contract).
  2. Apply every fixes[] entry verbatim as selector { property: value } — the list carries only kind: "exact" fixes, already deduped. No value parsing, no judgment needed.
  3. For fix.kind: "runtime-patch": edit the construct declaration at fix.source using fix.change (current config + the CSS value that would satisfy the constraint). Never patch the CSS for these — the runtime would overwrite it.
  4. For everything else — violations with fix.kind: "heuristic" (a direction, not a patch) or no fix at all: reason from detail + expected/actual (+ the rule's ruleDescription in contract mode — it states WHY the rule exists). If the violation has an owner, patch the CONSTRUCT, not the CSS — same logic as runtime-patch, without the precomputed change.
  5. Re-run. Stop at exit 0. Never claim success without the exit code.
  6. Contract mode, after an APPROVED visual change: rjs record re-pins baselines.
  7. On a page with constructs but NO contract yet: rjs init <url> -o app.contract.json generates one FROM the manifest (fluid → monotonic+continuous+baseline, ratio → proportion, breakpoints → viewport widths; stderr lists what could not be expressed), then rjs record pins the curves.

ContractReport (verify -f json)

jsonc
{
    "contract": { "name": "home", "version": 1 },
    "pass": true, "total": 6, "passed": 6, "failed": 0,
    "rules": [ { "ruleId": "no-bleed", "assert": "noOverflow", "pass": true, "checks": 3,
                 "skipped": false, "violations": [] } ],
    "violations": [ /* Violation + ruleId + ruleDescription */ ],
    "score": [ /* {metric, min, actual, pass, width?} */ ],
    "baselines": [ /* {selector, prop, pass, unrecorded?, deviations[]} */ ]
}

Contract skeleton

jsonc
{
    "name": "home", "version": 1,
    "viewport": { "widths": [320, 768, 1280] },          // or {from,to,step}
    "selectors": { "sidebar": ".app-sidebar" },          // $sidebar aliases in args
    "designSystem": { "profile": "material-design-3" },  // apple-hig | fluent-ui-2 | inline config
    "rules": [
        { "assert": "noOverflow", "description": "why this matters", "severity": "error",
          "when": { "max": 767 }, "id": "stable-id" }
    ],
    "score": [{ "min": 0.6 }],
    "baselines": [{ "selector": "h1", "prop": "fontSize", "tolerance": { "px": 2 } }]
}

JSON Schema (validate before use): design-contract.v1.json. Unknown assert names / args fail at load with did-you-mean suggestions.

Guarantees: selector-less (global) rules like noOverflow sweep a landmark default set — a contract of only global rules still measures real elements. A run that performed ZERO checks never passes: it fails with a contract.noChecks error violation. Treat that as "my targets don't exist on this page", not as success.

The 27 constraints

assertargsmeaning
noOverflowNo element exceeds the viewport width at any measured width. Naked overflow = error; inside a scrollable/clipping ancestor = warning (detail says which).
containsparent:selector, child:selectorChild rects stay inside the parent rect.
sameHeighta:selector, b:selector, tolerance?:numberTwo elements keep equal heights.
sameLinea:selector, b:selectorTwo elements share the same visual row.
minSizeselector, min:Elements meet minimum dimensions.
gapUniformselector, threshold?:numberSpacing between children is uniform.
monotonicselector, prop:enum, direction?:enumA property never moves against the direction as width grows.
continuousselector, prop:enum, maxJump:numberNo sudden jumps in a property across widths.
proportiona, b, bounds:Width ratio a/b stays within bounds.
childrenContainedselector, tolerance?:numberDirect children stay inside their container.
childrenEqualWidthselector, tolerance?:numberDirect children keep equal widths.
noZeroHeightselectorElements never collapse to zero height while having width.
touchTargetselector, min?:numberTouch targets ≥ min at mobile widths (default 24, WCAG 2.5.8 AA). Interactive = DOM semantics (native controls, roles, tabindex, not disabled) or cursor:pointer; inline prose links exempt; unrendered (0×0) skipped.
textReadableselectorFont size and line-height stay readable.
contrastRatioselector, level?:'AA'|'AAA'WCAG contrast, measured effective backgrounds.
borderRadiusValidselectorBorder radii stay consistent with element size.
zStackOrderselectors:selector[]z-index ordering matches the given selector order.
typographyScaleselectorFont sizes fit a modular scale.
spacingTokensselector, tokens:number[]Spacing values come from the token set.
aspectRatioselector, ratio:number, tolerance?:numberElements keep the given aspect ratio.
focusVisibleselectorFocusable elements have a visible focus affordance.
noHiddenOverflowselectorContent is not silently clipped by overflow:hidden.
alignedToGridselector, gridSize:numberElement positions align to a px grid.
breakpointSafebreakpoints:number[]Layout holds just below and above each breakpoint (bp±1 sampled automatically).
interactiveSpacingselector, minGap?:numberInteractive elements keep a minimum gap.
visibleselectorPresent and rendered (display/visibility/area).
hiddenselectorAbsent or not rendered.

Library-level (custom loops)

typescript
import { EvalSource, chunkedEval, analyze, verifyContract, contractSweepPlan } from '@responsivejs/design';
  • EvalSource(evalFn, { setViewport?, open? }) — any string-eval primitive becomes a driver; wrap chunkedEval(evalFn) when the transport caps argument length (Windows ~32K).
  • Without setViewport the source REFUSES widths that don't match the live viewport (currentWidth() gives the honest one). Measurements never lie.
  • analyze({ source, url?, selectors, widths, a11y?, constraints? }) → UnifiedReport.
  • contractSweepPlan(contract){selectors, widths, height?} to sweep any source, then verifyContract(contract, store).
  • Determinism: same store in → same report out. Scores and contrast are computed from measured values, never sampled.