Scout — Full Product Context → feature documentation

Public product context and feature documentation

The public, crawlable product brief at /context plus one page per feature at /context/feature/<slug>, rendered straight from these markdown files at request time.

Summary

Scout publishes its own engineering documentation. /context is a single large, server-rendered product brief written to be handed whole to a person or an AI agent, and /context/feature/<slug> renders one docs/features/*.md file per feature — the same documents the engineering team works from, converted to HTML at the moment the page is requested.

Publication is opt-in per document: a doc is served only if its YAML front matter carries public: true. Docs that declare themselves internal never receive the flag, and a test pins several of them by name.

Feature pages that have video show it inline, served from S3 and mapped to features by docs/features/media.manifest.json. Two libraries reach a page: the short cut clips, and the full unedited session recordings they were cut from — so a page is a self-contained look at that part of the app rather than a set of highlights.

Status

Live. 22 of 28 feature docs are published; 6 are deliberately withheld as internal (admin-and-content-ops, social-field-desk, design-system, screen-mocks, geofence-data-pipeline, and README, which is an index rather than a feature).

8 of those 22 published docs carry video — the 8 that the scout-ads recording library covers. The other 14 render prose only.

This surface is unauthenticated and uncached beyond a 60-second Cache-Control. It is not linked from the marketing site or the app; it is reached by direct URL, by crawlers, and by AI tools pointed at it.

User-facing surfaces

URL What it is
/context The full product brief. Live catalog counts, campaign and collection index, and the feature documentation index.
/context/feature/<slug> One feature doc, rendered from docs/features/<slug>.md.

<slug> is the markdown filename without its extension — /context/feature/unlock-and-location serves docs/features/unlock-and-location.md.

There is no deep link and no in-app surface. This is web only.

How it works

  1. ContextController.featurePage (backend/src/context/context.controller.ts:38) takes the slug and renders the context/feature template.
  2. ContextService.getFeatureDoc (backend/src/context/context.service.ts:244) calls loadPublicFeatureDoc, and turns a null into a 404.
  3. loadPublicFeatureDoc (backend/src/context/feature-docs.ts:160) applies two independent guards, then the publication gate:
    • the slug must match /^[a-z0-9-]+$/ (feature-docs.ts:64), so a traversal attempt never reaches the filesystem;
    • the slug must appear in a directory listing (feature-docs.ts:116);
    • the parsed front matter must carry public === true (feature-docs.ts:97).
  4. The body is rendered by markdown-it with html: false, so any HTML inside a doc is escaped rather than emitted.
  5. The first # heading becomes the page title and is removed from the body, so the page does not print its own title twice.
  6. Video for the slug is read from docs/features/media.manifest.json and rendered as <video preload="none"> elements in two sections — Screen recordings (the cut clips) and Full sessions (the unedited originals). preload="none" means a page with twelve videos costs nothing to load until a viewer presses play.

The docs directory is resolved from process.cwd() (feature-docs.ts:55), which is backend/ in both development and production. __dirname is deliberately not used: it sits at a different depth in backend/src/context than in the compiled backend/dist/src/context, so any fixed climb toward the repo root is correct in only one of the two.

The markdown reaches production because production keeps a full git working copy. The deploy (.github/workflows/deploy.yml) runs git fetch origin main && git reset --hard origin/main inside /root/scout, so docs/features/*.md is refreshed on the server by every deploy. No sync step, no build step, no second copy.

Data model

No database tables. The content is files:

File Role
docs/features/*.md The documents themselves. Front matter: public (boolean), summary (string).
docs/features/media.manifest.json Generated. Maps a feature slug to { clips, recordings }, each entry carrying clip, label, src, poster, seconds, recordedAt.

The loader accepts the older bare slug -> clips[] shape as well, so a stale or half-written manifest degrades to fewer videos rather than taking every feature page down.

The screen inventory was removed from /context on 2026-09-18. It was a 477-line block — 51% of content.ts — listing 58 screens with a route, a purpose and an image. Only 24 of the 58 images existed; the other 34 fell through to a third-party placehold.co placeholder, and the 24 real ones had not been updated since 2026-08-24. 57 of the 58 screens were already documented in the per-feature docs, which now own screens outright: each feature page carries its own surfaces list, screen recordings and poster frames. The page shrank from ~150KB to ~103KB. The 24 PNGs (49MB) were deleted with it.

/context itself still reads live catalog counts, collections and campaigns from Postgres — see ContextService.buildPageModel (backend/src/context/context.service.ts:121). The feature index is read from disk before that query so a database outage does not also blank the documentation list.

API surface

Route Auth Notes
GET /context none Cache-Control: public, max-age=60
GET /context/feature/:slug none Same cache header. 404 for unknown and unpublished slugs.

A missing doc and an unpublished doc return the identical 404 on purpose. Distinguishing them would confirm that an unpublished file exists.

Key files

Configuration and flags

Setting Where Effect
public: true each doc's front matter The only thing that publishes a doc. Absent or false means 404.
summary: each doc's front matter The index line and the page <meta name="description">.
S3_BUCKET, AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY backend/.env Where the media script uploads.
SCOUT_ADS_SCREENS env, optional Overrides the default archive path /Volumes/ExtendedA/scout-ads/scout-screens. The raw recordings are read from its sibling raw-scout-screen-recordings/.

There is no feature flag and no kill switch. Unpublishing a doc means removing its flag and deploying.

Maintenance and deploy cadence

These pages are only as current as the last deploy. The renderer removes the risk of a hand-copied second version going stale, but it cannot make a deploy happen. Two standing obligations follow:

  1. The docs must stay current with the code. This is already the rule in CLAUDE.md — changing a feature means updating its doc in the same change. Publishing them raises the cost of ignoring it: a stale doc is now a stale public page that AI tools read and quote.
  2. The backend must be deployed on a regular cadence, not only when backend code changes. A documentation-only commit changes no TypeScript, so it is easy to leave sitting on main unshipped while the public pages serve last month's prose. Ship the backend at least weekly, and always after a batch of doc edits. Deploys run on the self-hosted scout-mac runner and stop silently when it is offline — confirm the runner is up rather than assuming a quiet CI means a clean one.

The screen recordings carry their own staleness risk and their own mitigation: each clip shows its recordedAt date on the page. The current library was recorded 2026-09-09 and 2026-09-11, and the app has changed since — re-cut and re-run upload-feature-media.ts when a recorded screen is visibly redesigned.

Edge cases and known limits

What this feature does NOT do

Tests that cover it

The gate, the slug guard, the HTML escaping, the date parser and the faststart flag were each verified by reverting them and confirming the relevant tests fail.

Open questions