From b15d7e72acb9dd09d76ff287ca40c25a210b5a16 Mon Sep 17 00:00:00 2001 From: Ariel Shulman Date: Fri, 28 Aug 2026 11:51:38 +0300 Subject: [PATCH] feat(404): generate a /404.md wayfinding guide, drop the not-found scaffold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scaffolded human-visible not-found page was intrusive — a 404 page is the site owner's design domain, and the unstyled scaffold got rewritten anyway. Its root-only detection was also false comfort: App Router not-found.* is per-segment, and dynamic-route misses render the nearest one, bypassing both the root page and the middleware's wayfinding branch (the URL matches a route pattern). The human 404 surface now belongs entirely to the user; ax only owns machine artifacts. - Generate public/404.md every build: the same renderWayfinding body the middleware serves (new URL-neutral title variant), under the standard generated-markdown frontmatter. Gated on markdownTwins like auth.md, shown at the review gate, size-measured, generated-marker overwrite guard, removed when stale. Rendered from serving-manifest data with this run's outputs (planned twins, auth guide, catalog, cards) overlaid so it describes the build it ships with. - Rewrite agent-404.ts as detect-and-recommend only: scan every app/**/not-found.* (segment-level included) plus pages/404.*, and ask each unlinked page to carry one tag; with no 404 page at all, recommend a standard one in the user's own design plus the tag. Delete the scaffold, the not-found-agent-data module, and the scaffoldAgent404 config option (config schema, defaults, init wizard). - Report: agent404 now carries per-page granularity ({source, agentAware}[]) and the written markdownGuide path. - Manifest contract: artifacts.notFoundMd on both shapes, included in the middleware's protectDiscovery set so a wrapped bot-gate can't cover it. - renderWayfinding no longer lists gated routes under "Pages that do exist" — a login-walled page is no place to send a lost agent, and the gating policy never advertises a gated surface as open. (Surfaced by the regenerated goldens: the markdown-twins fixture's /private was listed.) - Fixture goldens regenerated (every fixture now snapshots its 404.md); fixtures/*/public/404.md gitignored per the generated-markdown convention. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 1 + README.md | 13 +- docs-internal/PLAN.md | 21 +- fixtures/bare/report.golden.json | 13 +- fixtures/bare/twins.golden/404.md | 21 + fixtures/config-overrides/report.golden.json | 13 +- fixtures/config-overrides/twins.golden/404.md | 21 + fixtures/deploy-variants/report.golden.json | 13 +- fixtures/deploy-variants/twins.golden/404.md | 21 + fixtures/discovery/report.golden.json | 13 +- fixtures/discovery/twins.golden/404.md | 21 + fixtures/hybrid/report.golden.json | 13 +- fixtures/hybrid/twins.golden/404.md | 23 + fixtures/llms-txt/report.golden.json | 13 +- fixtures/llms-txt/twins.golden/404.md | 22 + fixtures/markdown-twins/report.golden.json | 13 +- fixtures/markdown-twins/twins.golden/404.md | 25 + fixtures/mcp-adapter-gated/report.golden.json | 13 +- .../mcp-adapter-gated/twins.golden/404.md | 23 + fixtures/mcp-adapter/report.golden.json | 13 +- fixtures/mcp-adapter/twins.golden/404.md | 22 + fixtures/mcp-multi-server/report.golden.json | 13 +- fixtures/mcp-multi-server/twins.golden/404.md | 23 + fixtures/mdx-content/report.golden.json | 13 +- fixtures/mdx-content/twins.golden/404.md | 21 + fixtures/middleware/report.golden.json | 13 +- fixtures/middleware/twins.golden/404.md | 23 + fixtures/openapi/report.golden.json | 13 +- fixtures/openapi/twins.golden/404.md | 23 + fixtures/pages-bare/report.golden.json | 17 +- fixtures/pages-bare/twins.golden/404.md | 22 + fixtures/pages-mcp/report.golden.json | 13 +- fixtures/pages-mcp/twins.golden/404.md | 22 + .../report.golden.json | 13 +- .../twins.golden/404.md | 21 + .../webmcp-declarative/report.golden.json | 13 +- .../webmcp-declarative/twins.golden/404.md | 21 + packages/ax/src/agent-404.ts | 453 ++++++++---------- packages/ax/src/cli.ts | 34 +- packages/ax/src/config-schema.ts | 12 +- packages/ax/src/config.ts | 1 - packages/ax/src/generate.ts | 59 ++- packages/ax/src/index.ts | 13 +- packages/ax/src/init-config.ts | 12 +- packages/ax/src/init.ts | 16 +- packages/ax/src/manifest.ts | 6 + packages/ax/src/middleware/index.ts | 4 +- packages/ax/src/middleware/manifest-shape.ts | 1 + packages/ax/src/middleware/wayfinding.ts | 34 +- packages/ax/src/report.ts | 12 +- packages/ax/test/agent-404.test.ts | 289 +++++------ packages/ax/test/cli-twins.test.ts | 32 ++ packages/ax/test/config.test.ts | 1 - packages/ax/test/fixtures-integration.test.ts | 9 +- packages/ax/test/generate.test.ts | 53 ++ packages/ax/test/init-config.test.ts | 3 - packages/ax/test/init.test.ts | 7 +- packages/ax/test/middleware.test.ts | 18 + 58 files changed, 1188 insertions(+), 516 deletions(-) create mode 100644 fixtures/bare/twins.golden/404.md create mode 100644 fixtures/config-overrides/twins.golden/404.md create mode 100644 fixtures/deploy-variants/twins.golden/404.md create mode 100644 fixtures/discovery/twins.golden/404.md create mode 100644 fixtures/hybrid/twins.golden/404.md create mode 100644 fixtures/llms-txt/twins.golden/404.md create mode 100644 fixtures/markdown-twins/twins.golden/404.md create mode 100644 fixtures/mcp-adapter-gated/twins.golden/404.md create mode 100644 fixtures/mcp-adapter/twins.golden/404.md create mode 100644 fixtures/mcp-multi-server/twins.golden/404.md create mode 100644 fixtures/mdx-content/twins.golden/404.md create mode 100644 fixtures/middleware/twins.golden/404.md create mode 100644 fixtures/openapi/twins.golden/404.md create mode 100644 fixtures/pages-bare/twins.golden/404.md create mode 100644 fixtures/pages-mcp/twins.golden/404.md create mode 100644 fixtures/pages-webmcp-declarative/twins.golden/404.md create mode 100644 fixtures/webmcp-declarative/twins.golden/404.md diff --git a/.gitignore b/.gitignore index 44ddc10..cbad2d2 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ fixtures/mcp-multi-server/public/index.md fixtures/discovery/public/index.md fixtures/middleware/public/index.md fixtures/*/public/auth.md +fixtures/*/public/404.md # The serving-manifest data module a fixture's `prebuild: ax manifest` regenerates every build — # build output there (real projects may commit theirs; a fixture must rebuild it from scratch). diff --git a/README.md b/README.md index 0bea1ea..4dfb250 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,11 @@ ax detects gated MCP and OpenAPI surfaces, lets you declare the real auth flow w see one, and publishes both in a generated `auth.md`. Your 401/403 responses stay honest — ax never touches your route handlers. -**Steer lost agents back — agent-aware 404 + wayfinding middleware** -An opt-in `not-found.tsx` lists your real routes and discovery artifacts for agents that hit a dead -end; the runtime middleware gives the same wayfinding response to any URL that matches nothing. +**Steer lost agents back — `/404.md` wayfinding guide + middleware** +Every build generates `/404.md` — your real routes and discovery artifacts, for agents that hit a +dead end. Your 404 page stays yours: ax only asks it to carry one invisible +`` tag. The runtime middleware serves the +same wayfinding response directly for any URL that matches nothing. **Tell agents who you are and gain their trust — JSON-LD scaffolding** An opt-in `Organization` JSON-LD component is scaffolded from your `package.json`. ax prints the @@ -136,13 +138,12 @@ const config: AxConfig = { emit: 'static', // Scaffold a starter llms.txt filled with your real routes and artifacts. Opt-in. scaffoldLlmsTxt: true, - // Scaffold an agent-aware app/not-found.tsx plus a regenerated route-manifest module. Opt-in. - scaffoldAgent404: true, // Add Sitemap:/Agentmap: discovery lines to robots.txt (or write one if you have none). Opt-in. scaffoldRobots: true, // Scaffold an Organization JSON-LD component. Opt-in; ax never wires it into your layout. scaffoldJsonLd: true, - // Generate markdown twins of your pages, plus /auth.md when surfaces are gated. Default on. + // Generate markdown twins of your pages, /auth.md when surfaces are gated, and the /404.md + // wayfinding guide lost agents continue from. Default on. markdownTwins: true, // Write .ora/report.json, the machine-readable build report. Opt-in. report: true, diff --git a/docs-internal/PLAN.md b/docs-internal/PLAN.md index 6f69114..17e1bd7 100644 --- a/docs-internal/PLAN.md +++ b/docs-internal/PLAN.md @@ -859,6 +859,16 @@ Two additions shipped alongside Phase 4, both following existing conventions: sync, the agreed pattern is a composing higher-order middleware that wraps the user's existing middleware instead of owning the file, which removes the invasiveness objection. See the Vercel strategy sync section. + **Update (2026-08-28): redesigned — the scaffold, data module, and `scaffoldAgent404` option + are gone.** A scaffolded human-visible page was intrusive (users design their own 404s; the + unstyled scaffold got rewritten anyway), and root-only detection was false comfort: App + Router `not-found.*` is per-segment, and dynamic-route misses render the *nearest* one, + bypassing both the root page and the middleware's wayfinding branch (the URL matches a route + pattern). Now: ax generates only `public/404.md` (the wayfinding-guide artifact, same + `renderWayfinding` body as the middleware, gated on `markdownTwins` like `auth.md`); + detection scans every `app/**/not-found.*` plus `pages/404.*` and recommends one + `` tag per unlinked page — the + human 404 surface belongs to the user, ax only owns machine artifacts. - [x] **Machine-readable build report** (`src/report.ts`; `--report[=path]` / `ax.config` `report`, default off; default path `.ora/report.json`). The structured twin of the CLI output: entries + written paths, MCP mounts + server card, WebMCP sites, per-artifact @@ -922,8 +932,10 @@ consumer's source tree unasked), **write-once or append-only** (never overwrite, deliberately does **not** wire the component into `app/layout.tsx` (editing the file every page renders through, behind the owner's back, is not a postbuild step's call) — it prints the exact import + element instead. - - **`scaffoldAgent404`** (Phase 4.5) — the same pattern: a user-owned `not-found.tsx` written once, - backed by a data module regenerated every build from the real route tree. + - **`scaffoldAgent404`** (Phase 4.5) — was the same pattern (a user-owned `not-found.tsx` written + once, backed by a regenerated data module) until the 2026-08-28 redesign removed it; see the + Phase 4.5 update above. The 404 story is now the generated `public/404.md` + a link-tag + recommendation, with no scaffold and no config option. **3. The report became a handoff, not a log (`src/report.ts` v2 + `src/ora-checks.ts`).** The plugin's half of the loop is deterministic detection/emission/scaffolding; the other half — authoring content, @@ -1147,8 +1159,9 @@ ceremony. entries, with the default floor (`/api/auth/**`, `/api/webhooks/**`) pre-noted. Answers become an `isGated` matcher (compose `defaultIsGated` unless the user deselects the floor). A reviewed detection beats free-text glob authoring. - - **Scaffold opt-ins** (`scaffoldLlmsTxt`, `scaffoldJsonLd`, `scaffoldRobots`, - `scaffoldAgent404`) — **default yes in the wizard.** This is not a contradiction of the + - **Scaffold opt-ins** (`scaffoldLlmsTxt`, `scaffoldJsonLd`, `scaffoldRobots`; historically + also `scaffoldAgent404`, removed 2026-08-28) — **default yes in the wizard.** This is not a + contradiction of the config's `false` defaults: config defaults are `false` because *silent* writes into a source tree are invasive; in a wizard the user is present and the ask itself is the opt-in. Default-yes-when-asked / default-no-when-silent is one coherent policy — state it in the docs. diff --git a/fixtures/bare/report.golden.json b/fixtures/bare/report.golden.json index b6bc6ee..9bf2f29 100644 --- a/fixtures/bare/report.golden.json +++ b/fixtures/bare/report.golden.json @@ -16,7 +16,9 @@ }, "agent404": { "notFoundPresent": false, - "agentAware": false + "agentAware": false, + "pages": [], + "markdownGuide": "public/404.md" }, "middleware": { "present": false, @@ -67,6 +69,13 @@ "bytes": 87, "chars": 87, "tokens": 22 + }, + { + "artifact": "404.md", + "path": "public/404.md", + "bytes": 643, + "chars": 637, + "tokens": 159 } ], "ora": { @@ -160,7 +169,7 @@ "No robots.txt found — add one (app/robots.ts or public/robots.txt) that explicitly Allows the reputable AI agents you want to reach, scoped to specific User-agent groups (never \"User-agent: *\"), plus a \"Sitemap:\" line. Note: Next's MetadataRoute.Robots (app/robots.ts) has no field for the \"Agentmap:\" catalog pointer — put that line in a static public/robots.txt. Set scaffoldRobots: true in ax.config to have one written for you.", "No agents.md found — consider adding one (public/agents.md) with when-to-use / when-NOT-to-use guidance for agents. ax won't write its content for you; the companion skill can help author it from your repo.", "No JSON-LD structured data found in your layouts/pages — structured data is how registries and agents disambiguate and rank your site. Add an Organization block in your root layout (