# AGENTS.md

> Instructions for coding agents working on the AgenticWebScore codebase. Humans are welcome to read it too.

## Project overview

AgenticWebScore is an agent-readiness audit platform. It scans a public website the way an
autonomous agent would, evaluates it against a versioned rubric, and publishes a score,
per-check evidence, a verifiable fingerprint and a fix for every failure.

The canonical rubric lives in `src/data/rubric.json` and is generated by
`scripts/build-rubric.mjs` from these inputs:

- `scripts/rubric-source.json` — check identity, tier, weight, impact and rationale
- `scripts/check-paths.json` — the exact paths each check probes
- `src/data/fixes.json` — the measurement note, reproduction command and fix per check
- `scripts/profile-checks-source.json` — checks that only apply to an identified site type
- `scripts/profiles-source.json` — site types, weights, detection signals and policy

Never edit `rubric.json` by hand. Change an input and regenerate.

## URL shapes

A scan has one address, and it is a path. Do not add a second one.

- `/aws/<domain>` — the canonical scan path: the progress screen while a scan runs, then the report.
- `/aws/<domain>/<file>` — the 5 derived artifacts: `status.md`, `agent-brief.md`, `bundle.md`, `prompt.md`, and `report.json`.
- `/` and `/scan` — the forms. They POST `url` (and `profile`, `fresh`) to `/scan`, which answers 303 to `/aws/<domain>`. A GET with `?url=` on either still answers 303 to the same path and starts nothing.
- `/report/<domain>` and `/report/<domain>/<file>` — legacy addresses, 308 to the `/aws/` paths. They stay because badge snippets, stored fingerprints and external links point at them.

No page GET starts a scan: on the pages, only a POST to `/scan` does. A GET of
`/aws/<domain>` shows the stored record, the progress screen, or a form that asks before it
scans. The agent API is the exception. `GET /api/scan` starts one when `fresh=1` is set, or
when no stored record is younger than 6 hours.

**A check's `measures` string is its specification.** The implementation in
`src/engine/checks/` must match it. The one time this drifted, the site's own scanner
reported the skills index it publishes as missing. That is why `npm run self-audit` is
part of `npm run all`, and fails it when a claim stops holding.

## Setup

```bash
npm install
npm run dev          # http://127.0.0.1:4321
```

This build runs on Cloudflare Workers. One Worker serves the pages. One SQLite-backed Durable
Object, `ScanHub`, holds the records, the jobs, and the rate limits, and runs the scans.
`docs/DEPLOY-CLOUDFLARE.md` is the runbook. `npm run build:cf` builds this Worker into `dist-cf/` and
leaves `dist/` alone. `npm run dev`, `npm start`, and `npm test` run on Node, which stays the target for
local development.

## Build

```bash
npm run build        # client assets and the server bundle into dist/
npm start            # serve dist/ through server.mjs
npm run check        # astro check (types + diagnostics)
node scripts/build-rubric.mjs   # regenerate src/data/rubric.json
```

## Test

The acceptance bar is behavioural, and there are 5 gates. All of them must pass:

```bash
npm test             # unit suite (node:test): scoring, confidence, scope, profiles, fingerprints
npm run check        # types + diagnostics
npm run links        # every outbound URL we publish still resolves
npm run verify       # route, markdown, surface and rubric probes (needs a running server;
                     # the suite prints its own probe total rather than a number written here)
npm run self-audit   # scan this site with its own engine; every claim must hold
```

Rules the gates encode:

1. Every page renders completely with JavaScript disabled, and no page loads a script: the only
   `<script>` element on any page is `application/ld+json`.
2. Every surface returns 200 with its declared content type; unknown paths are real 404s.
3. Every page in the page registry and every learn article has a markdown twin on `<path>.md` and
   honours `Accept: text/markdown` with `Vary`. The 3 routes
   that are not in the registry — `/scan`, `/directory`, `/learn` — serve HTML only, and the
   per-page `<link rel="alternate">` element is omitted for them.
4. Meta descriptions on indexable pages sit inside the 120–160 character band the rubric measures.
5. A check that demands a URL must not demand a dead one.
6. The gaps this site publishes at `/bot` are exactly the gaps the self-audit accepts, and a
   gap that starts passing fails the gate rather than passing silently.

## Code style

- TypeScript, strict mode. No `any` in new code.
- Content is data. Pages own layout, not prose: put user-visible text in
  `src/content/*.ts` as `Block[]` so the HTML and the markdown twin stay in sync.
- No page loads a script, and the only `<script>` element on any page is `application/ld+json`.
  If you are about to add a script that a page loads, design the feature so it works without
  it first, then decide.
- This site ships English only, on purpose. Do not add a locale layer, a `?hl=` parameter,
  a language switcher or a dictionary: machine-translated locales were removed, because a
  product whose claim is precision cannot ship a translation it cannot vouch for. Prose goes
  in `src/content/*.ts` once, in English.
- Tailwind utilities for layout, `src/styles/global.css` for design tokens and the few
  composite classes. Do not add a second styling system.

## Security

- The scanner must never fetch private address space, cloud metadata endpoints or
  internal hostnames. Every hop is resolved and checked before it is requested. Cloudflare makes the connection. It resolves the name again.
- This build refuses an IP-address target and a port Cloudflare does not proxy: see `src/engine/net/policy.ts`.
- Never commit credentials, API tokens or `.env` files.
- Escape every user-controlled value on the way into HTML.
- Do not add a dependency that reads the filesystem or opens a socket at request time
  without a clear reason recorded in the pull request.

## Reporting issues

Send the URL, the fingerprint if you have one, and the exact command you ran:
hello@agenticwebscore.com
