
Isa
- 34 installs
- 17.2k repo stars
- Updated August 1, 2026
- danielmiessler/personal_ai_infrastructure
Owns the Ideal State Artifact, a twelve-section document that articulates 'done' for a project or task and drives, verifies, and records the build.
About
Scaffolds, interviews for, scores, reconciles, and seeds Ideal State Artifacts with a locked twelve-section body (problem, vision, goal, criteria, test strategy, and more). Developers use it to articulate done and define a project specification before building.
- Locked twelve-section body order with tiered completeness gates
- Five workflows: scaffold, interview, check completeness, reconcile, seed
Isa by the numbers
- 34 all-time installs (skills.sh)
- Ranked #1,792 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/danielmiessler/personal_ai_infrastructure --skill isaAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 34 |
|---|---|
| repo stars | ★ 17.2k |
| Last updated | August 1, 2026 |
| Repository | danielmiessler/personal_ai_infrastructure ↗ |
What it does
Owns the Ideal State Artifact, a twelve-section document that articulates 'done' for a project or task and drives, verifies, and records the build.
Files
🚨 MANDATORY: Voice Notification (REQUIRED BEFORE ANY ACTION)
You MUST send this notification BEFORE doing anything else when this skill is invoked.
1. Send voice notification:
curl -s -X POST http://localhost:31337/notify \
-H "Content-Type: application/json" \
-d '{"message": "Running the WORKFLOWNAME workflow in the ISA skill"}' \
> /dev/null 2>&1 &2. Output text notification:
Running the **WorkflowName** workflow in the **ISA** skill to ACTION...This is not optional. Execute this curl command immediately upon skill invocation.
ISA — Ideal State Artifact
The ISA is the single document that articulates "done" for any thing whose ideal state we are pursuing — a project, an application, a library, infrastructure, a work session, an art piece, a strategic decision. It serves five identities simultaneously: ideal state articulation, test harness, build verification, done condition, system of record.
This skill owns the canonical template, the workflows that generate and refine ISAs, and the example library.
---
The Twelve-Section Body (locked v6.2.0)
Every ISA may have up to twelve body sections. The tier completeness gate decides which are required at which effort tier; sections never appear empty. Order is fixed.
| # | Section | Purpose | Written At |
|---|---|---|---|
| 1 | ## Problem | What is broken or missing right now that makes the ideal state worth pursuing | OBSERVE |
| 2 | ## Vision | What euphoric surprise looks like — experiential intent, 1–5 sentences | OBSERVE |
| 3 | ## Out of Scope | Anti-vision — what is not included in this ideal state, declared upfront in prose | OBSERVE |
| 4 | ## Principles | Substrate-independent truths (Deutsch reach) the work must respect | OBSERVE |
| 5 | ## Constraints | Immovable architectural mandates that bound the solution space | OBSERVE |
| 6 | ## Goal | The hard-to-vary spine — 1–3 sentences naming verifiable done | OBSERVE |
| 7 | ## Criteria | Atomic ISCs (Ideal State Criteria) — one binary tool probe each, including derived Anti: ISCs | OBSERVE → EXECUTE |
| 8 | ## Test Strategy | Per-ISC verification approach — `isc | type |
| 9 | ## Features | Work breakdown — `name | description |
| 10 | ## Decisions | Timestamped decision log including dead ends; refined: prefix for Goal/ISC restructures | any phase |
| 11 | ## Changelog | Conjecture / refuted-by / learned / criterion-now entries — Deutsch error-correction trail | LEARN |
| 12 | ## Verification | Evidence that each ISC passed — quoted command output, file content, screenshot path | VERIFY |
---
Three-Guardrail Taxonomy (Principles vs Constraints vs Anti-criteria)
Adjacent concepts. Distinguished by who they bind.
| Guardrail | Binds | Tone | Example | Lives In |
|---|---|---|---|---|
| Principles | The thinking | Aspirational, generalizable | "User-facing systems prioritize responsiveness." | ## Principles |
| Constraints | The solution space | Immovable, non-negotiable | "We do not roll our own cryptography — OAuth via industry-standard libraries only." | ## Constraints |
| Out of Scope | The vision | Declared, explicit, prose | "Mobile native apps are not part of v1." | ## Out of Scope |
| Anti-criteria | The test surface | Granular, testable, yes/no | "Anti: /admin returns 200 in v1 build." | ## Criteria (with Anti: prefix) |
The first three are author-stated (declarative). Anti-criteria are derived — they are how Out of Scope, Constraints, and Principles become probe-able.
---
Tier Completeness Gate (HARD at all tiers)
Mirrors the v6.1.0 thinking-floor non-relaxability. Required sections per tier:
| Tier | Required Sections |
|---|---|
| E1 | Goal, Criteria |
| E2 | Problem, Goal, Criteria, Test Strategy |
| E3 | Problem, Vision, Out of Scope, Constraints, Goal, Criteria, Features, Test Strategy |
| E4 | All twelve sections |
| E5 | All twelve + active Interview workflow run before BUILD |
Project ISA override: any <project>/ISA.md requires E3+ structure regardless of the active task's tier. The project file is the long-lived source of truth; one transient E1 task must not downgrade it.
CheckCompleteness workflow enforces this gate. A miss blocks phase: complete until the missing sections are filled in.
---
Workflow Routing
Match the verb in the request to a workflow. When ambiguous, default to Scaffold for new ISAs and CheckCompleteness for audits.
| Verb / Intent | Workflow | File |
|---|---|---|
| "scaffold", "create", "generate", "new ISA from this prompt", "extract feature as ephemeral" | Scaffold | Workflows/Scaffold.md |
| "interview me", "fill in the ISA", "deepen", "ask me questions" | Interview | Workflows/Interview.md |
| "check", "audit", "score this ISA", "is it complete?" | CheckCompleteness | Workflows/CheckCompleteness.md |
| "reconcile", "merge feature file back", "ephemeral → master" | Reconcile | Workflows/Reconcile.md |
| "seed", "bootstrap from this repo", "draft an ISA from existing code" | Seed | Workflows/Seed.md |
| "append decision", "append changelog", "append verification", "record C/R/L entry" | Append | Workflows/Append.md |
When executing a workflow, output this notification directly:
Running the **WorkflowName** workflow in the **ISA** skill to ACTION...---
Gotchas
The highest-information-density part of this skill. Each entry captures a non-obvious failure mode that has bitten real ISA work.
- ID-stability is the cornerstone of Reconcile — never re-number on edit. When the Splitting Test produces a finer-grained version of
ISC-7, preserveISC-7as the parent and addISC-7.1,ISC-7.2, etc. Even when an ISC is dropped, leave a tombstone (- [ ] ISC-N: [DROPPED — see Decisions YYYY-MM-DD]). Reconcile keys on stable IDs; renumbering breaks ephemeral feature-file merges silently and the failure mode looks like "the worker's checkmarks didn't land in master." - Ephemeral files are derived views, never sources of truth. Scaffold's
--ephemeralmode produces a slice of the master ISA atMEMORY/WORK/{slug}/_ephemeral/<feature>.md. Workers operate against that slice; Reconcile merges back. Hand-editing master content from an ephemeral file is policy-forbidden — the master is what persists; the ephemeral is what gets archived. - The Changelog format is non-negotiable. Every entry needs all four pieces (
conjectured,refuted by,learned,criterion now) in that order. Append refuses to write a partial C/R/L; if any of the four is missing, the entry is a Decision, not a Changelog. The format is what makes the Deutsch error-correction trail auditable across sessions. - Project ISAs upgrade tier to `max(declared, E3)` regardless of the active task's tier. A
<project>/ISA.mdis the long-lived system of record for a thing with persistent identity. One transient E1 task on the project must NOT downgrade the structural minimum. CheckCompleteness applies this override automatically. - Empty sections never appear. The twelve-section body is a capacity, not a requirement at every tier. Sections required-but-empty for the tier are populated; sections not required and not yet written are simply absent from the file. CheckCompleteness distinguishes
present/thin/missing/emptyand onlyemptyis acceptable forVerificationbefore VERIFY phase. - Anti-criteria are derived from Out of Scope plus regression-prevention concerns. They are how the prose-guardrails (Out of Scope, Constraints, Principles) become probe-able. At least one is required at every tier; the absence of an anti-criterion at OBSERVE is a hard CheckCompleteness failure.
- Antecedents are required when the goal is experiential. For art, design, content, and anything that has to "land," at least one ISC must use the
Antecedent:prefix to name a precondition that reliably produces the target experience. Verifiable goals (build, deploy, schema) don't need antecedents; experiential goals always do. - Reconcile is deterministic — there are no conflicts to resolve. Either an ISC ID exists in master (mechanical merge) or it doesn't (abort with ID-stability violation). If the ephemeral made structural changes (split ISC-7 into ISC-7.1/ISC-7.2), those structural changes belong in master via a separate Edit by the user before Reconcile runs.
- The format spec wins on contradiction.
IsaFormat.mdis the file-shape contract. If this skill's prose ever drifts from the format spec, the spec is canonical and the skill updates to match — not the reverse.
---
Examples
The Examples/ directory holds twelve reference ISAs spanning the tier (E1–E5) × domain (code / art / design / ops / marketplace / enterprise) matrix. Always start by reading the canonical showpiece before scaffolding a new ISA — copy its section headers, then populate. Pick the example closest to your domain + scale as a template.
Showpiece
| File | Purpose |
|---|---|
Examples/canonical-isa.md | BeanLine — peer-to-peer specialty-coffee marketplace. The showpiece reference, fully populated across all twelve sections with real-feeling Decisions and a four-piece C/R/L Changelog. Read this first. |
Code
| File | Tier | Purpose |
|---|---|---|
Examples/e1-minimal.md | E1 | Add a --no-color flag to a CLI tool. <90s task, Goal + 4 ISCs only. Demonstrates the fast-path floor. |
Examples/e2-backup-verify.md | E2 | Add SHA-256 verification to a backup CLI's --verify mode. Single-domain, 18 ISCs. |
Examples/e3-project.md | E3 | Build an arxiv metadata extractor CLI. Mid-size project, 12 ISCs, eight sections. |
Examples/e4-api-migration.md | E4 | Migrate a public API from REST to GraphQL with 6-month backwards-compat. Cross-cutting, 73 ISCs, all twelve sections. |
Examples/e5-desktop-app.md | E5 | WattWatch — open-source desktop app for personal home-energy monitoring. Single-user app pattern, 50 ISCs, populated Changelog. |
Art (experiential — antecedents required)
| File | Tier | Purpose |
|---|---|---|
Examples/e3-essay.md | E3 | Write a 1500-word essay on a specific thesis. Experiential goal, antecedent ISCs, post-publish reception probes. |
Examples/e5-album.md | E5 | Mariner Frequencies — produce a 12-track instrumental album over 6 months. Long-form experiential, multi-act Changelog. |
Design (experiential)
| File | Tier | Purpose |
|---|---|---|
Examples/e3-help-redesign.md | E3 | Redesign a CLI tool's --help output for first-encounter clarity. Antecedents + usability tests. |
Examples/e4-brand-identity.md | E4 | Cardinal — brand identity for a small fintech startup (logo + type + color + voice + first 5 marketing surfaces). 56 ISCs, 6 antecedents. |
Ops
| File | Tier | Purpose |
|---|---|---|
Examples/e2-rotate-credential.md | E2 | Rotate a production deploy credential in CI. Demonstrates the ISA primitive applied to ops/runbook work. 16 ISCs. |
Enterprise
| File | Tier | Purpose |
|---|---|---|
Examples/e5-enterprise.md | E5 | Beacon Health Alliance — multi-region HIPAA-compliant patient portal for a 50-hospital network. Compliance anti-criteria, multi-team parallelizable features, 68 ISCs across all twelve sections. |
---
ID Stability Rule
ISC IDs never re-number on edit. When the Splitting Test produces a finer-grained version of ISC-7, the original number is preserved as the parent and children become ISC-7.1, ISC-7.2, etc. Do not collapse the numbering even if the ISC is dropped — leave a tombstone marker so historical references in Decisions, Changelog, and Verification remain valid.
This rule exists because Reconcile is keyed on ISC IDs. If IDs renumber across edits, ephemeral feature-file reconciliation breaks silently. The renumbering ban is what makes feature-file workflows safe.
---
Ephemeral Feature Files (Ralph Loop / Maestro pattern)
When a feature is to be worked in an isolated context (Ralph Loop, Maestro, parallel coding-agent instances), the Algorithm invokes:
Skill("ISA", "extract feature <name> as ephemeral file")Scaffold (with --ephemeral mode) produces a derived view at MEMORY/WORK/{slug}/_ephemeral/<feature>.md containing only the slice relevant to that feature: the Vision and Goal as read-only context, the relevant Constraints, the ISCs in the feature's satisfies: list with stable IDs, the matching Test Strategy entries, and an empty Verification section.
A fresh-context agent operates against the ephemeral file alone. At completion, Reconcile deterministically merges ISC checkmarks, Verification evidence, Decisions entries, and any new Changelog entries back to master, then archives the ephemeral file under _ephemeral/.archive/.
Ephemeral files are derived views. They are never sources of truth. They are never hand-edited as policy. The master ISA is what persists.
---
Relationship to the Algorithm
The Algorithm at OBSERVE invokes this skill to scaffold or read an ISA. The skill does not run the Algorithm — it owns the artifact the Algorithm operates on.
- OBSERVE:
Skill("ISA", "scaffold from prompt at tier T")→ returns populated ISA at canonical location. - OBSERVE:
Skill("ISA", "check completeness of <path> at tier T")→ pass/fail + gap report. - PLAN:
Skill("ISA", "extract feature <name> as ephemeral file")→ ephemeral excerpt. - LEARN:
Skill("ISA", "reconcile <ephemeral-path> → <master-path>")→ deterministic merge.
The Algorithm doctrine spec at ~/.claude/PAI/ALGORITHM/v6.2.0.md (or LATEST) governs invocation cadence. This skill is invocation-agnostic — it works the same whether called by the Algorithm or directly by the user.
---
Format spec cross-reference
The full ISA format spec lives at ~/.claude/PAI/DOCUMENTATION/IsaFormat.md. This skill implements that spec; if there is ever a contradiction, the format spec wins and this skill is updated to match.
The system-architecture doc — five identities, three-guardrail taxonomy, twelve-section body, six workflows, two homes, subsystem relationships — lives at ~/.claude/PAI/DOCUMENTATION/Isa/IsaSystem.md. Read that for the conceptual frame; read this file (and IsaFormat.md) for the operational contract.
<!-- Fictitious example. "BeanLine" is a teaching project name; any resemblance to real products or organizations is coincidental. The beanline.example.com domain is RFC 2606 reserved. -->
--- task: "Build BeanLine — peer-to-peer specialty-coffee bean marketplace" slug: 20260201-090000_beanline-v1 project: BeanLine effort: comprehensive effort_source: explicit phase: execute progress: 22/38 mode: interactive started: 2026-02-01T17:00:00Z updated: 2026-04-25T03:14:00Z ---
Problem
Specialty-coffee roasters with small-batch lots (under 50kg) and home-roasting hobbyists with green-bean surplus have no good place to find each other. Existing marketplaces (eBay, Etsy, Reddit's r/coffee) either don't support food-safe shipping logistics, charge consumer-marketplace fees that eat the margin on a 5kg lot, or have zero buyer trust signals for "is this bean stored properly?" Most lots end up sold at coffee festivals (one weekend a year) or composted. The supply exists. The connective tissue does not.
Vision
A small focused marketplace at beanline.example.com where a verified roaster lists a 5–50kg lot with origin, processing, harvest date, moisture content, and tasting notes; a verified buyer (home roaster or small cafe) browses by region and process, pays via escrow-Stripe, and the lot ships with a QR-coded handoff card the buyer scans on receipt to confirm condition. Euphoric surprise: a roaster lists Colombia Geisha Wednesday and ships it to a third-wave cafe in Portland on Friday — no festival, no haggling, no Reddit DM dance.
Out of Scope
- No retail-bag pricing. Minimum lot 5kg. Below that, the unit economics break for both sides.
- No green-bean futures or pre-harvest contracts. Existing physical lots only.
- No roasted-bean retail. Green coffee only; once it's roasted, the freshness window collides with shipping speed.
- No multi-currency. USD only in v1; international expansion requires a real customs and excise story we don't have.
- No social-graph features. No follow / friend / DM. Buyer-seller messaging is per-listing, not per-user-relationship.
- No machine-only quality verification. Listings carry seller-supplied data + buyer-confirmation handoff card; no third-party assay until v2.
- No mobile native apps. Web + PWA install. The buyer is at a desk pricing lots, not in line for boba.
Principles
- Buyer trust beats catalog size. A verified-buyer + verified-seller marketplace with 200 lots beats an open marketplace with 20,000 lots and one fraud incident.
- The handoff card is the product, not the website. A clean post-shipment confirmation flow is what makes the next listing land.
- Roaster economics are non-negotiable: under 8% all-in fees or it doesn't beat festival sales.
- Defaults teach. If a buyer's first three searches return relevant lots, they convert; if the first three return junk, they leave.
- Editorial signals beat algorithmic personalization at this scale. Curation by humans (an in-house quality lead reviewing every new listing) is cheaper than building a recommendation engine.
Constraints
- Edge SSR on Cloudflare Workers + D1 + R2. No third-party hosting in the user path.
- Auth via magic-link email only in v1. No password, no SSO. Verified-status (roaster vs buyer) gated by manual review of submitted business proof.
- Stripe Connect for escrow + split payments. No homegrown payment.
- All-in fees ≤ 8% (Stripe ~2.9% + 30¢ + BeanLine margin ≤ 5.1%).
- Bundle budget: ≤ 100KB JS gzipped on the listing page; ≤ 60KB CSS gzipped.
- p95 cold load on cellular ≤ 1s for browse pages, ≤ 1.5s for the listing detail page.
- Image storage in R2 with eager WebP transcoding; no original JPEGs ever served.
- Public read API rate-limited at 60 req/min/IP via Cloudflare WAF.
- All buyer-seller messaging logged for dispute resolution; retention 12 months minimum.
- HTTPS-only; HSTS preload-listed.
Goal
Ship a Cloudflare-hosted marketplace at beanline.example.com where verified roasters can list 5–50kg green-coffee lots and verified buyers can purchase via Stripe escrow with QR-handoff confirmation; the platform takes ≤ 5.1% margin (≤ 8% all-in including Stripe), browse pages render in ≤ 1s p95 on cellular, and the in-house quality lead can approve a new listing in ≤ 10 minutes per lot.
Criteria
Build & Deploy
- [x] ISC-1:
bun run deployexits 0 against production wrangler env. - [x] ISC-2: TypeScript strict-mode build emits 0 errors.
- [x] ISC-3:
beanline.example.comreturns HTTP 200 withtext/html. - [x] ISC-4: Deployed version string in HTML head matches local git short-sha.
Listing Lifecycle
- [x] ISC-5: A roaster can submit a new listing with origin, process, harvest date, moisture %, lot weight (kg), price ($/kg), tasting notes, ≥ 1 photo (probe: form submission test).
- [x] ISC-6: Submitted listings enter
status: pending_reviewand are not publicly visible (probe:curl /listings/<id>returns 404 for anonymous; visible to roaster + admin). - [ ] ISC-7: Quality lead can approve or reject a pending listing in ≤ 10 minutes per lot (probe: ops-tool timing telemetry, p95 ≤ 600s).
- [x] ISC-8: An approved listing appears at
beanline.example.com/lots/<slug>within 60 seconds of approval (probe:curlafter approval). - [ ] ISC-9: A sold-out listing is hidden from the browse page within 60 seconds of the final unit selling.
Browse and Search
- [x] ISC-10:
beanline.example.com/browsepaginates available lots, 20 per page, sorted by newest-listed. - [x] ISC-11:
beanline.example.com/browse?region=<region>filters lots by origin region (Africa, Americas, Asia-Pacific). - [x] ISC-12:
beanline.example.com/browse?process=<process>filters lots by processing method (washed, natural, honey, anaerobic, …). - [ ] ISC-13: Browse page p95 cold load on simulated 4G ≤ 1000ms (probe: Lighthouse mobile).
- [ ] ISC-14: Listing detail page p95 cold load ≤ 1500ms.
- [ ] ISC-15: Search query
?q=<term>matches against origin, process, and tasting-notes fields with case-insensitive substring (probe: integration test against fixture lots).
Auth and Verification
- [x] ISC-16:
/auth/magic-linkaccepts an email and emails a 15-minute single-use link. - [x] ISC-17: Magic-link callback creates a session cookie (
HttpOnly; Secure; SameSite=Lax). - [x] ISC-18: A new user starts as
role: buyer_unverified. Verification (business proof) elevates tobuyer_verifiedorroaster_verified. - [ ] ISC-19: Only
roaster_verifiedusers can submit listings (probe:POST /listingsfrombuyer_unverifiedsession returns 403). - [ ] ISC-20: Only
buyer_verifiedusers can purchase (probe:POST /checkoutfrombuyer_unverifiedreturns 403 with "verification required" message).
Payments and Escrow
- [x] ISC-21: Stripe Connect onboarding flow lives at
/account/payoutsforroaster_verifiedusers. - [x] ISC-22: Stripe Checkout creates an escrow charge: funds are held until handoff-confirm.
- [ ] ISC-23: BeanLine platform fee ≤ 5.1% of lot price; total all-in (BeanLine + Stripe) ≤ 8% (probe: post-checkout fee breakdown JSON includes both, sum ≤ 8%).
- [ ] ISC-24: Stripe webhook
payment_intent.succeededflips listing tostatus: in_transitand emails roaster a printable handoff card. - [ ] ISC-25: Buyer-confirm handoff (QR scan) flips status to
status: delivered, releases escrow to the roaster, and emails buyer a receipt. - [ ] ISC-26: If buyer does NOT confirm within 7 days of carrier-tracking-delivered, escrow auto-releases on day 8 with a Decisions-logged audit entry.
Messaging and Disputes
- [ ] ISC-27: Buyer can message the roaster from the listing page; messages are scoped to that listing only.
- [ ] ISC-28: Messages are retained for 12 months (probe: SELECT against retention policy).
- [ ] ISC-29: A "Open dispute" button on the listing page (visible only after purchase) creates a
disputerow with statusopenand notifies both parties.
RBAC / Visibility
- [x] ISC-30: Anonymous users can browse and view listings but cannot purchase or message (probe: each protected endpoint returns 401).
- [ ] ISC-31:
roaster_verifiedusers see their own listings in/account/listingsregardless of status; never see other roasters' pending listings. - [ ] ISC-32: Admin role gates
/admin/*routes; non-admins receive 403.
Performance and Operational
- [ ] ISC-33:
/healthreturns{status, version, last_deploy_at}in ≤ 50ms. - [ ] ISC-34: All R2 image fetches go through a transform Worker that delivers WebP (probe:
Content-Type: image/webpon every/img/...URL). - [ ] ISC-35: Public read API at
/api/lotsrate-limits to 60 req/min/IP via Cloudflare WAF (probe: 61st request in 60s returns 429).
Anti-criteria
- [ ] ISC-36: Anti: out of scope —
/api/follow,/api/dm, and any social-graph endpoint return 404 (probe: curl). - [ ] ISC-37: Anti: privacy — image originals (raw camera JPEGs) are NEVER served from R2 (probe: every image URL returns WebP).
- [ ] ISC-38: Anti: regression — first-page browse load makes zero third-party network requests (no analytics beacon, no font CDN, no ad-tech) (probe: Interceptor network-panel screenshot, 0 third-party requests).
Test Strategy
- isc: ISC-3
type: deploy-probe
check: HTTP status + content-type
threshold: 200 + text/html
tool: curl -i https://beanline.example.com
- isc: ISC-7
type: ops-timing
check: quality-lead approval time per lot
threshold: p95 ≤ 600s
tool: ops-tool telemetry, weekly aggregate
- isc: ISC-13
type: performance
check: browse-page p95 cold load on simulated 4G
threshold: ≤ 1000ms
tool: lighthouse --preset=mobile --only-categories=performance --url=https://beanline.example.com/browse
- isc: ISC-23
type: payment-fee
check: total fees on a $250 lot
threshold: ≤ $20 (8%)
tool: bun run scripts/checkout-test.ts --sandbox --lot-price=25000
- isc: ISC-25
type: integration
check: QR handoff scan releases escrow
threshold: stripe transfer event fires
tool: bun run scripts/handoff-test.ts --sandbox
- isc: ISC-26
type: timeout-behavior
check: auto-release on day 8
threshold: stripe transfer fires within 60s of day-8 cron
tool: bun run scripts/auto-release-test.ts --simulate-day=8
- isc: ISC-36
type: anti-probe
check: social-graph endpoints don't exist
threshold: 404
tool: curl -i https://beanline.example.com/api/follow
- isc: ISC-37
type: privacy
check: every image URL returns WebP
threshold: 100% Content-Type: image/webp
tool: bash scripts/image-format-audit.sh
- isc: ISC-38
type: privacy
check: zero third-party requests on browse page
threshold: 0
tool: Skill("Interceptor") network panel at /browseFeatures
- name: ListingPipeline
description: Submit → pending review → approved → public; quality-lead admin tooling
satisfies: [ISC-5, ISC-6, ISC-7, ISC-8, ISC-9, ISC-31]
depends_on: []
parallelizable: false # core data layer
- name: BrowseAndSearch
description: Paginated browse, region/process filters, substring search, performance budget
satisfies: [ISC-10, ISC-11, ISC-12, ISC-13, ISC-14, ISC-15]
depends_on: [ListingPipeline]
parallelizable: false # all browse views share layout primitives
- name: AuthAndVerification
description: Magic-link sign-in, role gating (buyer/roaster/admin), verification queue
satisfies: [ISC-16, ISC-17, ISC-18, ISC-19, ISC-20, ISC-30, ISC-32]
depends_on: []
parallelizable: true # parallel to listings
- name: PaymentsEscrow
description: Stripe Connect onboarding, escrow checkout, handoff release, auto-release timer
satisfies: [ISC-21, ISC-22, ISC-23, ISC-24, ISC-25, ISC-26]
depends_on: [AuthAndVerification, ListingPipeline]
parallelizable: false # checkout flow is end-to-end sequential
- name: MessagingAndDisputes
description: Per-listing buyer-roaster messaging, retention, dispute open
satisfies: [ISC-27, ISC-28, ISC-29]
depends_on: [AuthAndVerification, ListingPipeline]
parallelizable: true
- name: ImageEdge
description: R2 image storage + WebP transform Worker
satisfies: [ISC-34, ISC-37]
depends_on: []
parallelizable: true
- name: HealthAndRateLimit
description: /health endpoint, public-API rate limiting via WAF
satisfies: [ISC-33, ISC-35]
depends_on: [ListingPipeline]
parallelizable: trueDecisions
- 2026-02-01 17:00: Cloudflare-only stack chosen over a Vercel/Postgres path because edge-co-location wins at the cellular-load budget, D1's row-flat shape fits the listing schema, and the platform-fee math only works with low compute cost.
- 2026-02-08 11:30: Magic-link auth chosen over password+OAuth because v1's user base is small and known; password-reset flow would be the highest-cost auth surface for the verification-team load.
- 2026-02-22 14:00: ❌ DEAD END: Tried buyer-self-attested verification (upload a business license image, accept on submission). Three of the first eight attestations were retail bag-shop owners trying to source for resale, not the wholesale-buyer profile. Reverted to manual quality-lead review of every verification. Don't retry without an automated business-database cross-check.
- 2026-03-04 09:00: refined: ISC-7 sharpened from "quality lead can approve listings quickly" to "≤ 10 minutes per lot at p95" — the first phrasing was unfalsifiable; the second became a staffing-model input.
- 2026-03-15 22:00: ❌ DEAD END: Tried open-graph + Twitter-card image generation for listings. Pulled in 40KB JS for the meta-tag generator, broke the bundle budget. Reverted to server-rendered static OG meta. Don't retry the dynamic generator path.
- 2026-04-01 10:00: refined: ISC-23 split into ISC-23 (BeanLine fee) and an implied "all-in" check that combined the two — the original ISC let the all-in pass while BeanLine's slice silently crept past the principle's 5.1% ceiling.
- 2026-04-12 16:30: refined: ISC-26 added the auto-release timer (day-8) after the first three deliveries had buyers who never scanned the handoff QR — escrow sat indefinitely. The timer + audit log is the safety net.
- 2026-04-22 22:00: refined: Goal sharpened — added the explicit "p95 ≤ 1s on cellular" and "all-in fees ≤ 8%" — the original Goal was domain-correct but operationally fuzzy.
Changelog
- 2026-02-22 | conjectured: Buyer self-attestation will scale verification at low ops cost
refuted by: 3 of 8 attestations turned out to be the wrong buyer profile (retail, not wholesale) learned: verification is the load-bearing trust signal; attestation without ops review degrades the buyer-pool quality, which kills roaster trust, which kills supply criterion now: ISC-18 added the quality-lead manual-review step explicitly; "buyer_verified" role is gated on it
- 2026-03-15 | conjectured: Dynamic OG/Twitter card generation will improve social sharing CTR
refuted by: bundle exceeded the 100KB JS budget (ISC-13/14 broke); social CTR uplift was undetectable in A/B learned: bundle-budget Constraints outrank social-meta features; static OG is good enough at this scale criterion now: ISC-13 unchanged but Decisions logs the dead end as a bundle-creep canary
- 2026-04-12 | conjectured: Buyers will reliably scan the handoff QR; escrow release flows from buyer action
refuted by: 3 of the first deliveries had buyers who never scanned (busy shop, lost card); escrow sat learned: shipment-confirmation must have a buyer-action AND a timeout fallback; relying on either alone breaks the merchant cash-flow story criterion now: ISC-26 added (auto-release on day 8 with audit entry)
- 2026-04-22 | conjectured: Vague performance Goals ("fast on cellular") are operational enough
refuted by: bundle creep of 8KB went undetected for two sprints; nothing failed an ISC because no ISC named a number learned: every Constraint that maps to a budget needs a numeric ISC, not a vibe; Goal sharpening propagates down to ISCs criterion now: Goal explicitly states "p95 ≤ 1s on cellular" and "all-in fees ≤ 8%"; ISC-13 enforces the first, ISC-23 enforces the second
Verification
- ISC-1:
bun run deploy—Deployed beanline (route: beanline.example.com/*) - ISC-3:
curl -i https://beanline.example.com—HTTP/2 200 / content-type: text/html; charset=utf-8 - ISC-4: HTML head shows
<meta name="version" content="a3b4c5d">matchinggit rev-parse --short HEADoutputa3b4c5d - ISC-5: Listing form integration test 2026-04-22 — submitted listing returned
id: lst_TestXXXX+ statuspending_review - ISC-8:
curl -i https://beanline.example.com/lots/colombia-geisha-2026-q1after approval —HTTP/2 200 - ISC-10:
curl https://beanline.example.com/browse | rg "<article" | wc -l—20 - ISC-13: Lighthouse mobile run 2026-04-25 —
Performance 92 / FCP 624ms / LCP 891mson/browse - ISC-22: Stripe-sandbox checkout test 2026-04-15 —
payment_intent_TestXXXXcreated withtransfer_group: lst_TestYYY - ISC-30:
curl -i https://beanline.example.com/checkout(no session) —HTTP/2 401 - ISC-36:
curl -i https://beanline.example.com/api/follow—HTTP/2 404 - ISC-37: Image-format audit 2026-04-22 — 100% of 247 image URLs returned
Content-Type: image/webp - ISC-38: Interceptor network panel at
/browse2026-04-25 — 0 third-party requests on initial load
<!-- Canonical showpiece. Marketplace pattern (auth + Stripe escrow + RBAC + listings + search + reviews + messaging) at E5 scale, all twelve sections populated, real-feeling Decisions with two ❌ DEAD ENDs and four refinements, four-piece C/R/L Changelog entries spanning the 4-month build. ISC count 38 is below the E5 floor of 256 — show-your-math: the work surface is genuinely smaller than enterprise scope; the marketplace pattern is well-bounded and over-decomposing into 256 ISCs would manufacture probes that don't reflect real verification needs. Anti-criteria (ISC-36, 37, 38) cover scope, privacy, and regression. Antecedents (none) — the goal is verifiable, not experiential, so antecedents aren't required at this gate. The euphoric-surprise prediction in Vision is principal-falsification but not gated as an ISC because the marketplace's success is measured by transactions completed, not by any single user's reaction. -->
<!-- Fictitious example. "ColorlessCLI" is a teaching project name; any resemblance to real products or organizations is coincidental. -->
--- task: "Add a --no-color flag to a CLI tool" slug: 20260428-141500_no-color-flag effort: standard effort_source: auto phase: execute progress: 0/4 mode: interactive started: 2026-04-28T21:15:00Z updated: 2026-04-28T21:15:00Z ---
Goal
Add a --no-color flag to the dump.ts CLI so output strips ANSI escape codes when the flag is present (or when NO_COLOR env is set, per the no-color.org convention).
Criteria
- [ ] ISC-1:
dump.ts --no-color | cat | head -1produces output containing zero ANSI escape sequences (probe:dump.ts --no-color | rg -c $'\x1b\['returns 0). - [ ] ISC-2:
NO_COLOR=1 dump.ts | rg -c $'\x1b\['returns 0 (env-var path also strips). - [ ] ISC-3: Default
dump.ts(no flag, no env) still emits color codes when stdout is a TTY (probe:script -q /dev/null dump.ts | rg -c $'\x1b\['returns ≥1). - [ ] ISC-4: Anti:
dump.ts --no-colordoes not emit any new warning to stderr (probe:dump.ts --no-color 2>&1 >/dev/null | wc -creturns 0).
<!-- E1 minimal ISA. Required sections at this tier: Goal + Criteria. Problem / Vision / Out of Scope / Principles / Constraints / Test Strategy / Features / Decisions / Changelog / Verification all omitted — the task is small enough that the Goal sentence plus four binary probes carries the entire articulation. The Anti-criterion (ISC-4) is what keeps a sloppy implementation from passing — adding a "color disabled" log line would technically meet ISC-1 while breaking the silent-by-default expectation. -->
<!-- Fictitious example. "rsync-verify" is a teaching project name; any resemblance to real tools is coincidental. -->
--- task: "Add SHA-256 verification to a backup CLI's --verify mode" slug: 20260315-094500_backup-sha256-verify effort: extended effort_source: explicit phase: execute progress: 9/18 mode: interactive started: 2026-03-15T16:45:00Z updated: 2026-03-21T22:00:00Z ---
Problem
The rsync-verify CLI copies a source directory to a backup destination and reports completion. It does not currently verify that the destination bytes match the source. Bit rot, partial copies, and silent FS corruption have caused three "successful" backups in the last quarter to land unrestorable. Operators want a --verify mode that hashes both sides and surfaces mismatches before the run reports success.
Goal
Add a --verify flag that, after the rsync copy step completes, walks both source and destination, computes SHA-256 per file, compares hashes, and either exits 0 with a pass summary or exits 2 with a per-file mismatch report. The verification step must not double the run-time of a clean backup more than 1.5×.
Criteria
Verification correctness
- [ ] ISC-1:
rsync-verify --verify <src> <dst>exits 0 when every file in<src>has a SHA-256 match in<dst>(probe: integration test against a synthetic 100-file tree). - [ ] ISC-2:
rsync-verify --verify <src> <dst>exits 2 when ≥1 file mismatches (probe: integration test that flips 1 byte in<dst>then runs verify). - [ ] ISC-3: The mismatch report lists each diverging file path on stderr, one per line, with
MISMATCH: <path>prefix. - [ ] ISC-4: A file present in
<src>but absent in<dst>is reported asMISSING: <path>and contributes to the exit-2 count. - [ ] ISC-5: A file present in
<dst>but absent in<src>is reported asEXTRA: <path>and is a warning, not a failure (exit stays 0 if no MISMATCH/MISSING). - [ ] ISC-6: Hash computation uses a streaming SHA-256 (probe: code review confirms no
Buffer.from(file)for files > 64KB).
Performance
- [ ] ISC-7: Verify-mode wall-clock on a 10GB tree is ≤ 1.5× the no-verify wall-clock (probe:
timebenchmark with 10GB synthetic tree). - [ ] ISC-8: Hashing parallelism is bounded by
os.cpus().lengthworkers (probe: instrument worker pool counter, assert ≤ CPU count). - [ ] ISC-9: Memory usage stays under 256MB for any single-file hash regardless of file size (probe:
ps -o rsssampled during hash of 50GB file).
CLI surface
- [ ] ISC-10:
rsync-verify --helplists--verifywith a one-sentence description. - [ ] ISC-11:
rsync-verify --verify --json <src> <dst>emits JSON to stdout with shape{passed: bool, mismatches: [], missing: [], extra: [], elapsed_ms: number}. - [ ] ISC-12:
rsync-verify --verify-only <src> <dst>skips the copy step and only verifies (probe: timing comparison shows no rsync invocation).
Error handling
- [ ] ISC-13: Permission-denied on a source file emits
ERROR: cannot read <path>to stderr and exits 3 (distinct from mismatch exit 2). - [ ] ISC-14: Interrupting verify with SIGINT prints
verify aborted at file <N>/<total>and exits 130.
Anti-criteria
- [ ] ISC-15: Anti: out of scope —
rsync-verify --verify --remote ssh://host/pathdoes not work (this CLI is local-only; the SSH path is rejected withERROR: --verify requires local destination). - [ ] ISC-16: Anti: regression —
rsync-verify(no flag) does NOT silently start verifying. Verify is opt-in only (probe: timing benchmark of plain run is unchanged from pre-feature baseline). - [ ] ISC-17: Anti: privacy — verify mode never logs file contents to stdout, stderr, or any log file (only paths and hashes).
- [ ] ISC-18: Anti: regression — exit code 0 is reserved for "all files match"; any partial pass (e.g.,
--verify --best-effortif such flag exists later) must use a different exit code so existing scripts don't false-positive.
Test Strategy
- isc: ISC-1
type: integration
check: clean backup verifies pass
threshold: exit 0
tool: ./test/integration/clean-tree.sh && rsync-verify --verify ./tmp/src ./tmp/dst; echo $?
- isc: ISC-2
type: integration
check: corrupted backup fails verify
threshold: exit 2
tool: ./test/integration/clean-tree.sh && printf '\x00' >> ./tmp/dst/file7.bin && rsync-verify --verify ./tmp/src ./tmp/dst; echo $?
- isc: ISC-7
type: performance
check: verify-mode ≤ 1.5× no-verify
threshold: ratio ≤ 1.5
tool: bash benchmarks/10gb-tree.sh
- isc: ISC-9
type: memory
check: peak RSS during 50GB hash
threshold: ≤ 256MB
tool: bash benchmarks/large-file-rss.sh
- isc: ISC-15
type: anti-probe
check: --remote rejected with clear error
threshold: stderr contains "ERROR" + exit 1
tool: rsync-verify --verify ssh://host/path ./dst 2>&1; echo $?
- isc: ISC-17
type: privacy
check: file contents never appear in any log stream
threshold: 0 occurrences of test fixture content marker
tool: rsync-verify --verify ./tmp/src ./tmp/dst 2>&1 | rg "TEST_FIXTURE_SENTINEL_BYTES" | wc -l<!-- E2 medium ISA. Required sections: Problem, Goal, Criteria, Test Strategy. Vision, Out of Scope, Principles, Constraints, Features, Decisions, Changelog, Verification omitted — the work surface is single-domain (one CLI, one feature) and the tier completeness gate doesn't require them. ISC count of 18 meets the E2 floor of 16. Four anti-criteria (ISC-15, 16, 17, 18) cover scope, regression, privacy, and a future-compat lock — typical E2 anti-criteria density. -->
<!-- Fictitious example. The CI pipeline and credential surfaces here are teaching placeholders. -->
--- task: "Rotate the production deploy credential in the CI pipeline" slug: 20260208-103000_rotate-deploy-credential effort: extended effort_source: explicit phase: execute progress: 0/16 mode: interactive started: 2026-02-08T18:30:00Z updated: 2026-02-08T18:30:00Z ---
Problem
The production deploy credential (a long-lived API token stored in CI as DEPLOY_API_TOKEN) was provisioned 14 months ago, has never been rotated, and grants broad write scope on the deploy target. Per the org's quarterly rotation policy this is overdue. We need to rotate it without breaking the next deploy and without leaving the old token live longer than necessary.
Goal
Rotate DEPLOY_API_TOKEN end-to-end: provision a new token with the same scope, update the CI secret, run a verification deploy on a non-production branch, then revoke the old token. The next production deploy after this rotation must succeed, and the old token must be inactive within four hours of the new one going live.
Criteria
Pre-rotation
- [ ] ISC-1: New token provisioned via the deploy target's API with scope
deploy:writeonly — no broader scopes (probe:curl -H "Authorization: Bearer $NEW_TOKEN" /v1/mereturns scopes["deploy:write"]exactly). - [ ] ISC-2: New token expires in 90 days (probe:
curl /v1/tokens/<id>showsexpires_at≤ 90 days from now). - [ ] ISC-3: New token's
created_byis the rotation runbook service account, not an individual user (probe: token metadata).
CI update
- [ ] ISC-4:
DEPLOY_API_TOKENsecret in the CI provider is updated to the new value (probe:gh secret list --repo <org>/<repo>shows updatedupdated_atwithin last 5 minutes). - [ ] ISC-5: No commit, log line, or artifact contains the new token value as a string (probe:
gh run view <run-id> --log | rg "$(echo $NEW_TOKEN | head -c 8)" | wc -lreturns 0).
Verification
- [ ] ISC-6: A test deploy on a
rotation-testbranch using the new token completes successfully (probe: deploy job exit 0, deploy target's API confirms new artifact registered). - [ ] ISC-7: The verification deploy creates an artifact tagged
rotation-test-<timestamp>that is removable post-verify (probe:curl /v1/artifacts?tag=rotation-testlists the artifact). - [ ] ISC-8: Post-verify cleanup removes the test artifact within 60 minutes (probe:
curl /v1/artifacts/<id>returns 404 after cleanup).
Old token revocation
- [ ] ISC-9: Old token is revoked via the deploy target's API ≤ 4 hours after new token activation (probe:
curl /v1/tokens/<old-id>returnsrevoked_atpopulated). - [ ] ISC-10: A deploy attempt with the old token returns 401 within 60s of revocation (probe:
curl -H "Authorization: Bearer $OLD_TOKEN" /v1/deploys -X POSTreturns 401). - [ ] ISC-11: The revocation is logged in the org's auth audit log with actor, time, reason (probe: SIEM query for
token_revokedevent in last hour).
Documentation
- [ ] ISC-12:
docs/runbooks/credential-rotation.mdis updated with the new token's ID and the rotation date. - [ ] ISC-13: The next-rotation reminder is scheduled in the team calendar for
now + 90 days - 14 days(early warning).
Anti-criteria
- [ ] ISC-14: Anti: privacy — neither token value appears in any commit message, PR description, Slack/email message, or CI log (probe:
git log --all -S "$(echo $NEW_TOKEN | head -c 8)" --onelinereturns empty; same for old). - [ ] ISC-15: Anti: scope creep — new token does NOT have
admin:write,users:write, or any scope beyonddeploy:write(probe: token metadata scope-list comparison). - [ ] ISC-16: Anti: rollback safety — old token stays active for ≥ 30 minutes after new token deploys to verify, so a failed rotation can re-pin the old token (probe: timestamps on activation/revocation events show ≥ 30 min gap).
Test Strategy
- isc: ISC-1
type: api-probe
check: new token's scope list is exactly [deploy:write]
threshold: scopes == ["deploy:write"]
tool: curl -s -H "Authorization: Bearer $NEW_TOKEN" https://deploy.example.org/v1/me | jq -r '.scopes | sort | join(",")'
- isc: ISC-5
type: log-grep
check: new token value never appears in CI logs
threshold: 0 matches
tool: gh run view --log | rg "$(echo $NEW_TOKEN | head -c 8)"
- isc: ISC-6
type: integration
check: test deploy with new token succeeds
threshold: exit 0
tool: gh workflow run deploy.yml --ref rotation-test && wait-for-completion
- isc: ISC-10
type: api-probe
check: old token is rejected
threshold: HTTP 401
tool: curl -i -H "Authorization: Bearer $OLD_TOKEN" -X POST https://deploy.example.org/v1/deploys
- isc: ISC-14
type: privacy
check: neither token's first 8 chars appear in any tracked log/commit/artifact
threshold: 0 matches across all surfaces
tool: bash scripts/credential-leak-audit.sh
- isc: ISC-16
type: timing
check: gap between new-token-active and old-token-revoked ≥ 30 min
threshold: ≥ 1800s
tool: jq '.activated - .revoked' rotation-log.json<!-- E2 ops ISA. Required sections: Problem, Goal, Criteria, Test Strategy. Demonstrates the ISA primitive applied to an ops/runbook task — the same shape as a code task. ISC count of 16 hits the E2 floor exactly. Anti-criteria (ISC-14, 15, 16) cover privacy, scope, and rollback safety — typical ops-task regression-prevention concerns. Note ISC-16 explicitly preserves a safety window — a real-world lesson learned from prior bungled rotations. -->
<!-- Fictitious example. The essay topic is a teaching placeholder; any resemblance to real essays or authors is coincidental. -->
--- task: "Write a 1500-word essay on why most productivity advice fails first-time founders" slug: 20260317-203000_essay-productivity-fails-founders project: ProductivityEssay effort: advanced effort_source: explicit phase: execute progress: 12/34 mode: interactive started: 2026-03-17T03:30:00Z updated: 2026-03-21T15:00:00Z ---
Problem
I have a thesis about why generic productivity advice (time-blocking, deep work, eat the frog) lands wrong for someone running a 6-month-old startup with no team. The thesis is in my head; it isn't on the page. A draft I started a week ago reads like a list of complaints rather than an argument with a clear shape — opening hook is weak, the through-line dies in the third section, the closing punches air. Without a structural framework, the essay will keep failing the same way.
Vision
A 1500-word essay that a first-time founder reads in eight minutes, recognizes their own situation in the second paragraph, follows a single load-bearing argument through three movements, and arrives at a conclusion that reframes their relationship to productivity advice — not "ignore it" but "ignore most of it for now, and here's how to tell which 20% applies." Euphoric surprise: a reader closes the tab, opens a notes file, and writes one sentence about which productivity advice they're going to ignore for the next 90 days. They tell one friend.
Out of Scope
- Not a productivity-advice listicle. No "5 productivity hacks for founders." The essay is structural critique, not new advice.
- Not a manifesto. No "Here's the new way." The conclusion is calibration, not replacement.
- No founder name-checks. No anecdotes that depend on knowing a specific founder's story; the argument has to land for a reader who's never read TechCrunch.
- Not a Twitter thread. Long-form, single document, lands as one continuous read.
- Not a research paper. Zero citations, zero footnotes; the argument's force comes from clarity, not external authority.
Principles
- The reader's recognition in the second paragraph is the load-bearing moment. Without it, nothing else lands.
- One thesis, one through-line. Cut anything that requires the reader to hold a second argument in parallel.
- Concrete > abstract. Every claim has a concrete situation behind it; otherwise the claim reads as platitude.
- The closing must do work — name something the reader will do differently — not just summarize.
- Voice is conversational-direct. No "Here's the thing", no "It turns out". No academic hedging.
Constraints
- 1500 words ± 100 (1400–1600 final).
- Three sections only — opening, middle, close. No subheaders.
- Reading time ≤ 8 minutes at 200wpm.
- Zero footnotes, zero citations, zero "as <famous person> says".
- No bulleted lists in the body (one allowed in the close if it earns its place; otherwise zero).
- Published as a single Markdown file with frontmatter; no embedded images, no pull quotes.
Goal
Ship a 1400–1600-word essay in three sections that opens with a concrete first-time-founder situation the reader recognizes within 30 seconds, develops a single thesis ("most productivity advice was built for a different game"), and closes with a calibration tool the reader can apply within 24 hours — a one-question filter for which advice to keep and which to drop.
Criteria
Word count and structure
- [x] ISC-1: Final word count ∈ [1400, 1600] (probe:
wc -w essay.mdminus frontmatter). - [x] ISC-2: Exactly three top-level sections, no subheaders (probe:
rg -c "^##" essay.mdreturns 3 — opening header, middle header, close header). - [ ] ISC-3: Each section is 350–700 words (probe: word count per section between header lines).
- [ ] ISC-4: Reading time ≤ 8 minutes at 200wpm (computed:
wc -w / 200).
Argument structure
- [x] ISC-5: Opening section ends with a one-sentence thesis statement (probe: human review confirms last sentence of opening section is the thesis).
- [ ] ISC-6: Middle section advances the thesis through ≥ 3 distinct examples (probe: human review confirms three concrete situations, none requiring outside knowledge).
- [ ] ISC-7: Close section names a specific calibration tool (the one-question filter) the reader can apply within 24 hours.
- [ ] ISC-8: Through-line test: a reader can articulate the thesis in ≤ 20 words after a single read (probe: 3 unfamiliar readers each summarize the thesis; ≥ 2/3 land within ±10 words of the same summary).
Voice and tone
- [x] ISC-9: Zero occurrences of "Here's the thing", "It turns out", "Not just X — it's Y" (AI-writing-pattern probe: rg against AI_WRITING_PATTERNS list returns 0).
- [ ] ISC-10: Zero footnotes, zero numeric citations, zero "as <person> says" formulations (probe:
rg "\[?\^?\d+\]" essay.mdreturns 0;rg "as [A-Z]" essay.mdreturns 0). - [ ] ISC-11: Sentence-length variance: at least one sentence ≤ 8 words and at least one ≥ 28 words in each section (probe: per-section sentence-length histogram).
- [ ] ISC-12: First-person plural ("we", "us") count ≤ 5 across the whole essay (probe:
rg -wc "we|us|our" essay.md).
Antecedent ISCs (preconditions for the target experience)
- [x] ISC-13: Antecedent: the second paragraph contains a concrete situation that 60%+ of first-time founders will recognize as their own within 30 seconds (probe: 5 unfamiliar founder readers, ≥ 3/5 mark "yes, that's me" on a post-read 1-question survey).
- [ ] ISC-14: Antecedent: the thesis sentence (end of opening) is hard-to-vary — replacing any noun or verb in it with a synonym detectably weakens the argument (probe: human review of 3 paraphrases shows clear semantic loss).
- [ ] ISC-15: Antecedent: the close's calibration tool (the one-question filter) is concrete enough that a reader can apply it without re-reading the essay (probe: 5 readers given only the close section can articulate what to do; ≥ 4/5 succeed).
Bitter Pill discipline
- [ ] ISC-16: No paragraph could be moved to a different essay without rewriting at least its first sentence (probe: paragraph-portability review — every paragraph has at least one phrase that anchors it to this essay's specific argument).
- [ ] ISC-17: No sentence is filler — removing any single sentence detectably weakens the argument or rhythm (probe: read-aloud test, 3 sentences flagged at random, removal test).
Anti-criteria
- [ ] ISC-18: Anti: out of scope — the essay does NOT include a numbered list of productivity hacks (probe:
rg "^\d\." essay.mdreturns 0). - [ ] ISC-19: Anti: regression — no sentence longer than 50 words (probe: longest sentence ≤ 50 words; a single 50+ -word sentence is the canary that the writing has drifted into academic register).
- [ ] ISC-20: Anti: voice — the essay does NOT name a specific famous founder (e.g., "as Paul Graham wrote") (probe:
rg -i "paul graham|sam altman|peter thiel|naval|elon|jeff bezos|steve jobs"returns 0). - [ ] ISC-21: Anti: scope — the essay does NOT propose a new productivity framework or system (probe: human review confirms zero "Introducing the X method" formulations).
Iteration discipline
- [ ] ISC-22: At least 3 drafts captured in
drafts/directory before final (probe:ls drafts/ | wc -l≥ 3). - [ ] ISC-23: Final draft was read aloud once before publishing (probe: Decisions entry confirming read-aloud pass).
- [ ] ISC-24: At least 2 unfamiliar readers (not friends-being-nice) gave first-impression feedback before publishing (probe: Decisions entries citing reader IDs/initials).
Publishing
- [ ] ISC-25: Final file is
essay.mdat the project root with frontmattertitle,published_at,word_count,reading_time_min. - [ ] ISC-26: Markdown renders cleanly on the target publishing platform (probe: preview render shows three sections, no broken formatting).
- [ ] ISC-27: A 280-character pull-quote is captured for social syndication (probe: file
pullquote.txtexists with content ≤ 280 chars). - [ ] ISC-28: An "if I had to cut 200 more words" note is captured for future re-reads (probe: file
cuts-on-deck.mdlists candidate cuts).
Post-publish euphoric-surprise probes
- [ ] ISC-29: Within 7 days, ≥ 1 reader reports they identified one piece of advice they're going to drop (probe: replies/comments/messages search).
- [ ] ISC-30: Within 14 days, ≥ 1 reader forwards the essay to a fellow founder unprompted (probe: web analytics referrer or direct report).
Personal discipline
- [ ] ISC-31: A "what I cut" file is preserved at
cuts.mdshowing what was edited out (probe: file exists, ≥ 500 words of cuts). - [ ] ISC-32: The frontmatter
startedandpublished_attimestamps reflect the actual ≥ 4-day gestation (probe: timestamps). - [ ] ISC-33: A short Decisions entry captures which paragraph caused the most rewriting and why (lessons for next essay).
- [ ] ISC-34: At least one ❌ DEAD END Decisions entry exists (a draft direction that was tried and abandoned).
Test Strategy
- isc: ISC-1
type: word-count
check: total words in body
threshold: 1400-1600
tool: awk '/^---$/{c++; next} c==2' essay.md | wc -w
- isc: ISC-8
type: reader-comprehension
check: 3 unfamiliar readers articulate thesis in ≤ 20 words within ±10 of each other
threshold: ≥ 2/3 cluster
tool: send essay to 3 reader-test slots, collect 1-sentence summaries
- isc: ISC-9
type: ai-writing-pattern
check: AI-writing-pattern density
threshold: 0 occurrences from P0 list
tool: rg -i "here's the thing|it turns out|not just .* — it's" essay.md
- isc: ISC-13
type: antecedent-probe
check: 5 founder readers say "yes, that's me" to second paragraph
threshold: ≥ 3/5
tool: 5-person reader test, post-read 1-q survey
- isc: ISC-29
type: post-publish
check: ≥ 1 reader names a specific advice they're dropping
threshold: 1 within 7 days
tool: monitor replies, comments, DMs for 7 daysFeatures
- name: OpenerSituation
description: Concrete first-time-founder situation that reader recognizes in 30s
satisfies: [ISC-3, ISC-5, ISC-13, ISC-16]
depends_on: []
parallelizable: false # opener gates everything else
- name: MiddleArgument
description: Three distinct concrete examples advancing the single thesis
satisfies: [ISC-3, ISC-6, ISC-11, ISC-14, ISC-16]
depends_on: [OpenerSituation]
parallelizable: false # the through-line is sequential
- name: CalibrationClose
description: One-question filter the reader can apply within 24h
satisfies: [ISC-3, ISC-7, ISC-15, ISC-30]
depends_on: [MiddleArgument]
parallelizable: false
- name: VoicePass
description: AI-writing-pattern scrub + sentence-length variance + first-person discipline
satisfies: [ISC-9, ISC-10, ISC-11, ISC-12, ISC-19, ISC-20]
depends_on: [CalibrationClose]
parallelizable: true # cosmetic pass on full draft
- name: ReaderFeedback
description: Two unfamiliar reader passes; second-paragraph recognition probe
satisfies: [ISC-8, ISC-13, ISC-15, ISC-23, ISC-24]
depends_on: [VoicePass]
parallelizable: true # readers are independentDecisions
- 2026-03-17 03:30: Three sections, no subheaders, locked. The form constraint forces the through-line to be load-bearing.
- 2026-03-18 11:00: ❌ DEAD END: Tried opening with a quote from a public figure. Felt borrowed; reader's recognition stayed external. Reverted to a concrete-situation opener. Don't retry.
- 2026-03-19 22:30: refined: ISC-13 sharpened from "readers find the opening relatable" to "≥ 3/5 founder readers mark 'yes, that's me' to the second paragraph specifically." The first phrasing was unfalsifiable; the second isolates the load-bearing moment.
- 2026-03-20 09:00: ❌ DEAD END: Tried structuring the middle as five examples instead of three. The fifth and fourth examples started repeating each other; cut to three with one extended. Don't retry.
- 2026-03-20 14:30: refined: ISC-7 sharpened from "close offers a takeaway" to "close names a specific calibration tool the reader can apply within 24 hours." Vague closes are why most essays of this shape fail to land.
- 2026-03-21 09:00: refined: ISC-12 added (≤ 5 first-person plural) after a draft read like a "we should all" sermon. The essay is observation, not exhortation.
<!-- E3 art ISA. Required sections: Problem, Vision, Out of Scope, Constraints, Goal, Criteria, Features, Test Strategy. Optional Principles included because the essay is experiential and the principles do real work in the writing pass. ISC count of 34 exceeds the E3 floor of 32. Three Antecedent ISCs (ISC-13, 14, 15) carry the experiential-goal contract: they name the preconditions that reliably produce the target reader experience. Without them, ISC-29 and ISC-30 (post-publish reception) would be unfalsifiable hopes rather than testable claims. Anti-criteria (ISC-18, 19, 20, 21) cover scope, regression, voice, and a future-essay-drift trap. The Decisions section shows two ❌ DEAD ENDs and three refinements — typical density for a first draft of an essay that knows its shape but is still finding its load-bearing moments. -->
<!-- Fictitious example. "duck" is a teaching placeholder for an existing CLI tool whose --help output we are redesigning. -->
--- task: "Redesign the duck CLI's --help output for first-encounter clarity" slug: 20260411-191500_duck-help-redesign project: DuckHelpRedesign effort: advanced effort_source: explicit phase: execute progress: 16/36 mode: interactive started: 2026-04-11T02:15:00Z updated: 2026-04-15T18:00:00Z ---
Problem
The duck CLI's --help output is 187 lines, formatted as one block per flag in declaration order, with usage examples buried at line 142. New users land on it, scan for 4 seconds, hit Ctrl-C, and man duck instead. We track help-to-first-command time at 95 seconds (median for new installs) and the dominant time-sink in those 95 seconds is "scrolling through --help and giving up." The reference content is fine; the layout is not.
Vision
A duck --help that a first-time user can read top-to-bottom in 30 seconds and walk away knowing: (a) what duck does in one sentence, (b) the two most-common invocations, (c) where to find more — and only after that, the full flag reference. Euphoric surprise: a new user lands on the redesigned help, types one of the example invocations within 15 seconds, and it works. They never visit the man page on their first session.
Out of Scope
- No new flags or behavior. Reference content stays identical; only layout, ordering, and density change.
- No man-page redesign.
man duckis the deep reference; this work is the front door, not the library. - No interactive help (`duck help`). Stays a one-shot stdout dump like every other Unix CLI.
- No color support added or removed. Existing color behavior stays; this is content-and-layout work.
- No localization. English only, same as the rest of duck.
Principles
- The first 30 seconds are the entire user experience for 80% of new users. The output is optimized for them, not for power users (who use
manor--help <flag>). - A help screen is a teaching surface, not a reference dump. Reference belongs in
man. - Whitespace is a feature. Density alone is not friendliness.
- Examples teach faster than prose. The first concrete invocation goes above the first flag definition.
- Progressive disclosure: highest-information-per-pixel content first, full flag table last.
Constraints
- Output stays plain text suitable for piping to
less,grep, etc. — no terminal-control escapes for the layout itself. - Total length ≤ 100 lines (current: 187 lines).
- Renders correctly at 80-column width (no wrapped lines that break alignment).
- Must include every flag the current help includes — no flag omissions.
- Build process:
duck --helpreads from a single template file at compile time; the redesign updates that template, not the runtime renderer. - Backwards-compat:
duck --help | rg <flag-name>continues to find each flag (so existing scripts that grep --help don't break).
Goal
Ship a redesigned duck --help template (≤ 100 lines, 80-col safe) that opens with a one-sentence project summary, shows the two most-common usages as concrete examples, lists the flag reference grouped by category (not declaration order), and ends with a "see also" footer pointing at man + docs URL — all reference content preserved, all flags still grep-able, first-time-user help-to-first-command time drops from 95s median to ≤ 30s median.
Criteria
Length and layout
- [x] ISC-1:
duck --help | wc -lreturns ≤ 100 lines (current baseline 187). - [x] ISC-2: Every line in
duck --helpis ≤ 80 columns (probe:awk 'length>80' < (duck --help) | wc -lreturns 0). - [ ] ISC-3: Output has exactly three top-level sections: Summary+Examples block, Flag Reference, See Also (probe: count of section header rules
═or─).
Top-section content
- [x] ISC-4: First non-blank line is a one-sentence description ≤ 80 chars (probe: line 1 length, sentence-end period).
- [ ] ISC-5: Examples block contains exactly 2 invocations, each annotated with a one-line "what this does" gloss.
- [ ] ISC-6: Each example invocation is a real, currently-supported command (probe: copy-paste each example, run it, assert exit 0 against test fixtures).
Flag reference
- [ ] ISC-7: Flags are grouped into ≤ 4 categories with clear headers (e.g.,
Common,Output Control,Filtering,Diagnostics). - [ ] ISC-8: Each flag's entry is exactly 2 lines:
--flag, -f <ARG>on line 1 (left-aligned, fixed-width), description on line 2 indented 4 spaces. - [ ] ISC-9: Within each category, flags are alphabetized.
- [ ] ISC-10: Every flag from the current 187-line help is present in the new layout (probe:
diff <(rg "^ --" old-help.txt | sort -u) <(rg "^ --" new-help.txt | sort -u)returns empty).
See-also footer
- [ ] ISC-11: Footer contains exactly: man page reference, docs URL, version + build short-sha.
- [ ] ISC-12: Footer URL is on a single line and ≤ 80 chars.
Backwards-compat
- [x] ISC-13:
duck --help | rg "\-\-each-flag-name"returns ≥ 1 line for every flag (verified across all flags). - [x] ISC-14:
duck --helpexit code stays 0 (probe:duck --help; echo $?). - [ ] ISC-15: Pre-existing
man duckstill references "see--helpfor usage" — and the reference still resolves to a useful Examples block.
Performance
- [ ] ISC-16: First-time-user help-to-first-command time drops from 95s median to ≤ 30s (probe: 5 new-user usability sessions, time from
duck --helpto first non-help command). - [ ] ISC-17: Help-screen render time stays < 50ms (template is compiled-in, not parsed at runtime).
Antecedent ISCs (experiential preconditions)
- [ ] ISC-18: Antecedent: the one-sentence description (line 1) is hard-to-vary — replacing any verb or noun with a synonym makes the description either inaccurate or weaker (probe: 3 paraphrase attempts reviewed, all detectably worse).
- [ ] ISC-19: Antecedent: the two examples in the Examples block are the two highest-frequency invocations from the last 30 days of telemetry (probe: cross-reference invocation-frequency log).
- [ ] ISC-20: Antecedent: the flag categories are intuitive — given only the four category names, a new user can guess which category contains a randomly-chosen flag with ≥ 70% accuracy (probe: 5 users, 10 random flags each, ≥ 70% category-guess accuracy).
Voice and tone
- [ ] ISC-21: Each flag description is ≤ 80 chars and reads as imperative (e.g., "Print version and exit", not "This flag prints the version").
- [ ] ISC-22: Zero "Note:" preambles (probe:
rg "^ Note:" new-help.txtreturns 0). - [ ] ISC-23: Zero "Please" appearances (probe:
rg -wi "please" new-help.txtreturns 0).
Anti-criteria
- [ ] ISC-24: Anti: out of scope — no new flag was introduced (probe: flag count is unchanged from baseline).
- [ ] ISC-25: Anti: regression —
duck --help -handduck -handduck helpall still produce the same output (probe:diff <(duck --help) <(duck -h) <(duck help)returns identical). - [ ] ISC-26: Anti: footer drift — version + build sha line is automatically generated, not hand-edited (probe: source template uses
{{VERSION}}{{SHA}}placeholders, build pipeline injects). - [ ] ISC-27: Anti: density creep — no flag description is split across two description lines (probe: every description is exactly 1 line of ≤ 80 chars).
Migration discipline
- [ ] ISC-28: A diff between old and new template is captured in
docs/help-redesign-diff.md. - [ ] ISC-29: A blog post or release note draft (≤ 300 words) explaining the redesign exists at
docs/release-notes/help-redesign.md. - [ ] ISC-30: The 5 user-test session recordings (anonymized) are saved at
research/user-tests/help-redesign-2026-04/.
Bitter Pill discipline
- [ ] ISC-31: No section of the new help is shorter than 4 lines or longer than 70 lines (probe: per-section line count).
- [ ] ISC-32: Examples block does NOT include a "useful flag combinations" appendix (probe: human review — the discipline is two examples, not five).
Publishing
- [ ] ISC-33: New template is committed to
templates/help.txtwith a commit message linking the redesign decision in Decisions. - [ ] ISC-34: The change ships behind a build flag for one release before becoming default (probe: build flag exists, default-on commit lands one release after introduction).
- [ ] ISC-35: Pre-existing CI test
test/help-grep.sh(which greps for each flag) passes against the new template.
Long-tail observation
- [ ] ISC-36: 30 days post-ship, help-to-first-command median time has dropped to the ISC-16 threshold and stays there (probe: telemetry comparison day-30 vs day-0).
Test Strategy
- isc: ISC-1
type: line-count
check: --help line count
threshold: ≤ 100
tool: duck --help | wc -l
- isc: ISC-6
type: integration
check: each example actually runs
threshold: exit 0 on all
tool: bash test/help-examples.sh
- isc: ISC-10
type: completeness
check: every old flag is in new help
threshold: empty diff
tool: diff <(rg "^\s*--" old-help.txt | sort -u) <(rg "^\s*--" new-help.txt | sort -u)
- isc: ISC-16
type: usability-test
check: median help-to-first-command time
threshold: ≤ 30s median across 5 users
tool: 5 user-test sessions, time-stamped recordings
- isc: ISC-18
type: antecedent
check: one-sentence description is hard-to-vary
threshold: 3 paraphrase attempts all detectably worse
tool: human review by 3 unfamiliar reviewers
- isc: ISC-20
type: antecedent
check: category names are intuitive
threshold: ≥ 70% guess accuracy across 5 users × 10 flags
tool: structured user test
- isc: ISC-25
type: backwards-compat
check: --help -h and help all match
threshold: identical output
tool: diff <(duck --help) <(duck -h) <(duck help)Features
- name: TopSection
description: One-sentence summary + 2 example invocations with annotations
satisfies: [ISC-4, ISC-5, ISC-6, ISC-18, ISC-19]
depends_on: []
parallelizable: false # the opener gates everything
- name: FlagReference
description: Reorder flags into ≤ 4 categories, alphabetize within, 2-line entries
satisfies: [ISC-7, ISC-8, ISC-9, ISC-10, ISC-20, ISC-21]
depends_on: [TopSection]
parallelizable: false
- name: SeeAlsoFooter
description: Man-page ref + docs URL + version/sha
satisfies: [ISC-11, ISC-12, ISC-26]
depends_on: [FlagReference]
parallelizable: true
- name: BackwardsCompat
description: --help -h and help all produce same output; flag-grep still works
satisfies: [ISC-13, ISC-14, ISC-15, ISC-25, ISC-35]
depends_on: [FlagReference, SeeAlsoFooter]
parallelizable: true
- name: UsabilityValidation
description: 5 user-test sessions for help-to-first-command + category intuition
satisfies: [ISC-16, ISC-20, ISC-30]
depends_on: [TopSection, FlagReference, SeeAlsoFooter]
parallelizable: trueDecisions
- 2026-04-11 02:15: Three top-level sections — Summary+Examples / Flag Reference / See Also — locked. Resists the "one more category" temptation that ate the last help redesign attempt.
- 2026-04-12 14:00: ❌ DEAD END: Tried 5 categories instead of 4. Users in pilot test split 60/40 on which category three flags belonged to. Reverted to 4 categories with clearer names. Don't retry.
- 2026-04-13 09:00: refined: ISC-19 sharpened from "examples reflect common usage" to "examples are the two highest-frequency invocations from 30-day telemetry" — the first phrasing let me cherry-pick aspirational examples; the second forced honesty.
- 2026-04-13 22:00: refined: ISC-8 sharpened from "flags formatted clearly" to "exactly 2 lines per flag, line 1 fixed-width, line 2 indented 4 spaces" — vague aesthetic claims are how help screens drift back to inconsistent layout over time.
- 2026-04-14 11:30: ❌ DEAD END: Tried inline color highlighting for flag names. Broke piping to
grepandlessfor users without color-aware pagers. Reverted to plain text. Don't retry. - 2026-04-15 16:00: refined: ISC-16 added a 30-day post-ship probe (ISC-30) — without it, the redesign passes its launch test but could regress in 90 days as new flags are added without category discipline.
<!-- E3 design ISA. Required sections: Problem, Vision, Out of Scope, Constraints, Goal, Criteria, Features, Test Strategy. Optional Principles included — the design has experiential goals (first 30 seconds, recognition, intuition) and principles do real work in the design pass. ISC count of 36 exceeds the E3 floor of 32. Three Antecedent ISCs (ISC-18, 19, 20) carry the experiential contract: hard-to-vary one-sentence summary, telemetry-grounded examples, and intuitive categories. Anti-criteria (ISC-24, 25, 26, 27) cover scope, regression, drift, and density. The Decisions section shows two ❌ DEAD ENDs and three refinements — typical for a redesign where every aesthetic temptation needs to be tested against actual users. -->
Problem
Researching across arxiv papers means reading abstracts in a browser one at a time. There is no quick "give me the title, authors, abstract, categories, and submission date for these 50 paper IDs as JSONL so I can grep them" tool. The arxiv API exists but its XML response shape is annoying enough that nobody uses it casually.
Vision
A single bun TypeScript CLI: bun arxiv.ts <id1> <id2> ... > papers.jsonl. One paper per line, structured fields, no friction. Euphoric surprise: feeding 100 IDs and getting clean JSONL back in under three seconds.
Out of Scope
- No PDF download. Metadata only.
- No citation graph traversal. Single-paper lookup, no following references.
- No web UI. CLI exclusively.
- No persistent cache. Stateless; every run hits the API.
Constraints
- Bun runtime only. No Node dependency.
- Zero npm dependencies — use Bun's built-in
fetchand a hand-rolled XML parse. - Must respect arxiv's API rate limits (3 requests / second per their TOS).
Goal
Ship a single-file arxiv.ts CLI that takes paper IDs as arguments, queries the arxiv Atom API, parses the response, and writes one JSONL row per paper to stdout with fields: id, title, authors, abstract, categories, submitted, updated.
Criteria
- [x] ISC-1:
arxiv.tsis a single file at the project root. - [x] ISC-2: Zero entries in
package.jsondependencies(probe:jq '.dependencies | length' package.jsonreturns 0). - [ ] ISC-3:
bun arxiv.ts 2401.12345returns exactly one JSONL row to stdout. - [ ] ISC-4: The JSONL row has exactly seven fields:
id, title, authors, abstract, categories, submitted, updated. - [x] ISC-5:
authorsis an array of strings, never a single concatenated string. - [ ] ISC-6:
categoriesis an array of strings (e.g.,["cs.AI", "cs.LG"]). - [ ] ISC-7: A 100-ID batch completes in ≤ 3 seconds wall clock (rate-limit-aware throttling).
- [x] ISC-8: A bad ID (e.g.,
9999.99999) writes a JSONL row witherrorfield instead of crashing. - [ ] ISC-9: stderr stays empty on a successful 100-ID run (no logging clutter).
- [ ] ISC-10:
bun arxiv.ts --helpprints usage in ≤ 12 lines. - [ ] ISC-11: Anti: out of scope —
arxiv.ts --downloadis not a recognized flag (returns help + exits 2). - [x] ISC-12: Anti: regression — never makes more than 3 concurrent requests against arxiv API.
Test Strategy
- isc: ISC-3
type: cli-probe
check: stdout has exactly one JSONL row
threshold: 1 line
tool: bun arxiv.ts 2401.12345 | wc -l
- isc: ISC-7
type: performance
check: wall-clock for 100 IDs
threshold: ≤ 3000ms
tool: time bun arxiv.ts $(cat 100-ids.txt)
- isc: ISC-8
type: error-handling
check: bad ID does not exit non-zero
threshold: exit 0 + JSONL row with error field
tool: bun arxiv.ts 9999.99999 | jq -e '.error'
- isc: ISC-12
type: anti-probe
check: max concurrent requests
threshold: ≤ 3
tool: instrument fetch with counterFeatures
- name: AtomFetch
description: Bun fetch + queue with 3-concurrency throttle
satisfies: [ISC-7, ISC-12]
depends_on: []
parallelizable: false # core IO layer
- name: AtomParse
description: Hand-rolled XML → typed object
satisfies: [ISC-4, ISC-5, ISC-6]
depends_on: [AtomFetch]
parallelizable: false
- name: CLIInterface
description: Argument parsing, --help, error formatting
satisfies: [ISC-3, ISC-8, ISC-9, ISC-10, ISC-11]
depends_on: [AtomParse]
parallelizable: false # single-file CLIDecisions
- 2026-02-01 18:00: Hand-rolled XML parse over a library — Bun has no built-in XML, the response shape is bounded, and adding a dep would violate the zero-deps constraint.
- 2026-02-08 22:30: ❌ DEAD END: Tried Promise.all() with 100-IDs — arxiv rate-limited after request 12. Reverted to a 3-concurrency queue. Don't retry.
<!-- Fictitious example. "ApiBridge" is a teaching project name; any resemblance to real products or organizations is coincidental. The example.org domain is RFC 2606 reserved. --> --- task: "Migrate ApiBridge public API from REST to GraphQL with deprecation runway" slug: 20260112-091500_apibridge-rest-to-graphql-migration project: ApiBridge effort: deep effort_source: explicit phase: execute progress: 23/72 mode: interactive started: 2026-01-12T17:15:00Z updated: 2026-04-22T03:48:00Z ---
Problem
The ApiBridge public API at api.apibridge.example.org has accumulated 14 REST endpoints across 4 years of organic growth. Half of them are over-fetching (one read of /orgs/:id pulls 38 fields when the dashboard uses 6); the other half are under-fetching (rendering a single project page costs 6 sequential GETs because each related resource lives behind its own URL). External consumers — 47 known integrations across 12 partners — repeatedly hit the same N+1 patterns and route around them with caching that's now stale more often than fresh. Internally, every new product surface argues over which existing endpoint to bend versus which new one to add, and the answer is usually "add another," which makes the surface worse.
A GraphQL endpoint at api.apibridge.example.org/graphql lets clients ask for exactly the fields they need in one round trip. The migration is hard because the 47 integrations cannot break — partners will move at their own pace, and at least three of them publish quarterly release trains. The goal is not "GraphQL replaces REST tomorrow." The goal is "GraphQL is preferred, REST is supported for six months with clear deprecation telemetry, and at the end of the window every active consumer has either migrated or is opted into a paid extended-support track."
Vision
A partner integration team opens our docs, sees a single GraphQL playground next to a dimmed REST reference labeled "deprecated April 2026 → October 2026," runs three example queries, and realizes their nightly sync that takes 14 round trips can become one. They migrate their staging environment in an afternoon. Six months later, our REST egress drops to under 2% of total API traffic, and the cutover ships without a single Sev-2.
Out of Scope
- Internal service-to-service traffic. Internal callers continue using gRPC; this migration is for the public boundary only.
- GraphQL subscriptions. Pub/sub realtime is a separate roadmap item; v1 is queries and mutations only.
- Schema federation. We expose one monolithic GraphQL schema; we are not introducing Apollo Federation, schema stitching, or a gateway tier in this migration.
- Authentication redesign. Existing OAuth 2.0 bearer tokens are reused unchanged; no migration to mTLS, no new scopes, no re-issuing keys.
- Webhook redesign. Webhook payloads remain JSON-shaped per existing contracts; this migration does not touch outbound delivery.
- Self-service partner portal. Partners continue to be onboarded by the partnerships team; no portal changes ship as part of this work.
Principles
- Public APIs are contracts, not implementations. A consumer cannot tell us "we'll fix it next quarter" and have us break their build before that quarter ends. Migration windows must respect external release cadence.
- Deprecation is a product, not an event. The deprecation experience — telemetry, sunset headers, dashboard, partner emails, escalation paths — is itself a feature with its own ISCs.
- Every breaking change has a non-breaking adapter. If GraphQL cannot serve a REST shape verbatim, we add a thin REST→GraphQL adapter rather than asking the partner to change shape immediately.
- Performance is part of the contract. GraphQL must not be slower than REST for equivalent queries at p95. Latency regressions are bugs.
- Schema is owned by product, not by transport. The shape of
Project,Organization,Userlives in one place and is consumed by both REST adapters and GraphQL resolvers; we do not duplicate types.
Constraints
- The current REST API at
api.apibridge.example.org/v1/*continues to return correct, byte-identical responses for the entire 6-month deprecation window (April 22, 2026 → October 22, 2026). No silent shape changes. - GraphQL endpoint exposed at
api.apibridge.example.org/graphqlonly. No/v2, no subdomain split, no separate hostname. - Apollo Server v4+ on Node 20 LTS. We do not roll a custom GraphQL implementation. We do not pin to v3.
- Schema-first development with codegen. The SDL file at
schema/api.graphqlis the source of truth; resolvers are generated, not hand-written from scratch. - Every breaking change ships behind a feature flag with a default-off rollout managed by the existing LaunchDarkly account.
- Sunset headers (
Sunset,Deprecation,Link) are emitted on every REST response per RFC 8594 throughout the deprecation window. No exceptions. - The migration ships in 4 increments (schema → resolvers → REST adapter layer → deprecation telemetry); no big-bang cutover.
- Documentation site at
docs.apibridge.example.orgmust show GraphQL and REST side-by-side for the entire window; "REST docs deleted" is not an option until October 22, 2026.
Goal
Ship the GraphQL endpoint at api.apibridge.example.org/graphql with full coverage of the 14 REST endpoints' read and write surface area, parity-tested under load, with a published 6-month deprecation runway for REST that emits RFC 8594 sunset headers, exposes per-partner deprecation telemetry on an internal dashboard, and lands the cutover without any external integration breaking before its partner-confirmed migration date.
Criteria
- [x] ISC-1:
schema/api.graphqlexists, validates againstgraphql-schema-linter, and covers all 14 REST endpoint shapes. - [x] ISC-2: GraphQL endpoint responds with
200and a valid introspection result forquery { __schema { queryType { name } } }. - [x] ISC-3: All 14 REST endpoints have a corresponding query or mutation in the schema (probe:
node scripts/coverage-check.tsexits 0). - [x] ISC-4: Schema codegen produces typed resolver stubs at
src/generated/resolvers.ts. - [x] ISC-5: 100% of read-side resolvers return data byte-identical to the matching REST endpoint for a 1,000-row golden fixture (probe:
bun test parity/read.test.ts). - [ ] ISC-6: 100% of write-side resolvers produce identical database side-effects to the matching REST mutation for the golden fixture (probe:
bun test parity/write.test.ts). - [x] ISC-7: GraphQL p95 latency for the 5 most common query shapes is ≤ matching REST p95 + 10ms under 200 rps load.
- [ ] ISC-7.1: GraphQL p95 latency for the 20 next-most-common query shapes is ≤ matching REST p95 + 25ms under 200 rps load.
- [ ] ISC-8: GraphQL p99 latency under 1000 rps load remains under 800ms.
- [x] ISC-9: REST responses include
Sunset: Wed, 22 Oct 2026 00:00:00 GMTheader. - [x] ISC-10: REST responses include
Deprecation: trueheader. - [x] ISC-11: REST responses include
Link: <https://docs.apibridge.example.org/graphql>; rel="successor-version". - [ ] ISC-12: Per-partner deprecation telemetry dashboard at
internal.apibridge.example.org/deprecationshows REST request count, GraphQL request count, and migration percentage by partner ID. - [ ] ISC-13: Dashboard shows the 5 most-called deprecated REST endpoints by partner.
- [ ] ISC-14: Dashboard alerts fire when any partner's REST traffic increases week-over-week after April 22, 2026.
- [x] ISC-15: All 47 known integrations are tagged with a
partner_idin request logs. - [ ] ISC-16: Migration emails sent to partner technical contacts at T-90, T-60, T-30, T-14, T-7, T-1 days from cutover.
- [x] ISC-17: GraphQL playground at
api.apibridge.example.org/graphqlloads in a browser with example queries pre-populated. - [x] ISC-18: Documentation site shows GraphQL and REST side-by-side for every endpoint.
- [ ] ISC-19: Anti: REST endpoints return shape-changed responses during the deprecation window (probe:
bun test parity/rest-stability.test.tsruns daily). - [ ] ISC-20: Anti: GraphQL endpoint accepts queries deeper than 8 levels (probe: depth-limit middleware blocks query at depth 9 with
400). - [ ] ISC-21: Anti: GraphQL endpoint accepts queries with cost > 1000 (probe: cost analysis middleware blocks high-cost query with
400). - [ ] ISC-22: Anti: introspection is enabled in production (probe:
query { __schema { types { name } } }returns403againstapi.apibridge.example.org/graphqlwith non-admin token). - [ ] ISC-23: Anti: any partner is silently cut off (probe: cutover script requires partner-confirmed migration date in
partner-status.jsonfor every active partner_id). - [x] ISC-24: Feature flag
graphql_endpoint_enableddefaults tofalseand is explicitly enabled per environment. - [x] ISC-25: Feature flag
rest_sunset_headers_enableddefaults tofalseuntil April 22, 2026. - [ ] ISC-26: Rollback runbook at
docs/runbooks/graphql-rollback.mdexists and has been dry-run executed in staging. - [x] ISC-27: Schema changes go through PR review with at least one API-team approver (probe:
.github/CODEOWNERSlistsschema/under@api-team). - [x] ISC-28: Every resolver has a Datadog APM span tagged with
graphql.operation_nameandgraphql.field_name. - [ ] ISC-29: Authorization middleware enforces the same scopes on GraphQL fields as the matching REST endpoint requires (probe:
bun test auth/scope-parity.test.ts). - [ ] ISC-30: Rate limits applied per partner at the GraphQL layer match the REST layer (probe:
bun test rate-limit/parity.test.ts). - [ ] ISC-31: Error responses follow the structured GraphQL error spec with
extensions.codeset per error class. - [x] ISC-32: REST request logs include
Accept-Migrationheader value when partner sends it (used to track partners actively testing GraphQL). - [ ] ISC-33: Partner status file
partner-status.jsonlists everypartner_idwith fieldsconfirmed_migration_date,last_rest_request,first_graphql_request,migration_pct. - [ ] ISC-34: Status file is regenerated nightly from request logs.
- [ ] ISC-35: Partner support runbook at
docs/runbooks/partner-migration-support.mdcovers the top 10 expected migration questions with copy-paste GraphQL equivalents. - [ ] ISC-36: Public changelog entry posted at
docs.apibridge.example.org/changelogannouncing GraphQL availability with example queries. - [ ] ISC-37: Public changelog entry posted announcing REST deprecation with sunset date.
- [x] ISC-38: GraphQL schema is published at
schema.apibridge.example.org/api.graphqlfor tooling consumption. - [ ] ISC-39: Schema diff CI gate fails the build if a breaking schema change is introduced without
BREAKING_CHANGE_APPROVED=trueenv flag. - [x] ISC-40: Resolvers reuse the existing data-access layer (no duplicate query logic between REST handlers and GraphQL resolvers).
- [ ] ISC-41: Load test simulating partner-realistic query patterns (mix of 60% reads, 30% writes, 10% complex nested queries) sustains 500 rps for 1 hour without error rate exceeding 0.5%.
- [x] ISC-42: GraphQL endpoint enforces request body size limit of 100KB.
- [x] ISC-43: GraphQL endpoint enforces query timeout of 10 seconds at the resolver layer.
- [ ] ISC-44: Anti: REST endpoint
/v1/orgs/:id/projectsreturns 404 before October 22, 2026 (probe: synthetic monitor pings every 5 minutes). - [ ] ISC-45: Anti: any GraphQL field returns PII not present in the matching REST endpoint (probe:
bun test parity/pii-coverage.test.ts). - [ ] ISC-46: Cutover dry-run executed at T-30 against staging with all 47 partner integrations simulated.
- [ ] ISC-47: Sentry release tag
graphql-cutover-v1exists. - [ ] ISC-48: PagerDuty escalation policy
graphql-launchis on-call rotation for the 2 weeks following October 22, 2026. - [x] ISC-49: GraphQL endpoint logs include
partner_idextracted from the bearer token claim. - [x] ISC-50: REST adapter layer at
src/rest/adapter.tstranslates REST routes to internal GraphQL execution (single resolver path, two transports). - [ ] ISC-51: Adapter layer adds < 5ms p95 overhead vs. direct REST handler.
- [x] ISC-52: All 14 REST routes are now served by the adapter (legacy direct handlers deleted).
- [ ] ISC-53: Adapter is feature-flagged by
rest_via_adapter_enabledand rolled out in 10% increments. - [ ] ISC-54: Adapter rollout reaches 100% before deprecation telemetry begins (April 22, 2026).
- [ ] ISC-55: Migration retrospective document at
docs/retrospectives/graphql-migration.mdwritten by November 1, 2026. - [x] ISC-56: GraphQL gateway has a circuit breaker that opens when downstream data layer error rate exceeds 5% over 60s.
- [ ] ISC-57: Circuit-breaker behavior documented in incident response runbook.
- [x] ISC-58: Persisted queries are supported via APQ (Automatic Persisted Queries) for partners that opt in.
- [ ] ISC-59: At least 3 partners using APQ in production by October 1, 2026.
- [ ] ISC-60: GraphQL access logs are retained for 90 days in the existing log retention bucket.
- [ ] ISC-61: Audit log for schema changes is queryable via
bun scripts/schema-history.ts. - [x] ISC-62: Anti: a single resolver makes more than 3 sequential database calls without batching via DataLoader (probe: lint rule
no-sequential-db-callsruns in CI). - [x] ISC-63: DataLoader instances are created per-request, not per-process (probe:
bun test dataloader/scope.test.ts). - [ ] ISC-64: Schema documentation generated from SDL comments and published to docs site.
- [ ] ISC-65: Partner-specific cost limits enforced (cost ≤ 500 for free tier, cost ≤ 2000 for paid tier, cost ≤ 5000 for enterprise tier).
- [x] ISC-66: GraphQL errors are scrubbed of internal stack traces in production responses.
- [ ] ISC-67: External health check at
api.apibridge.example.org/graphql/healthreturns200with schema version. - [ ] ISC-68: Anti: deprecation cutover proceeds with any partner still showing > 100 REST requests/day in the 7 days before cutover (probe: cutover script blocks).
- [ ] ISC-69: Extended support contract template exists at
legal/extended-rest-support-template.mdfor partners needing a paid runway past October 22, 2026. - [ ] ISC-70: At most 3 partners are on extended support after October 22, 2026.
- [ ] ISC-71: Public status page at
status.apibridge.example.orghas agraphqlcomponent and arestcomponent, each with independent uptime SLOs. - [ ] ISC-72: Final cutover postmortem published to docs site within 14 days of October 22, 2026.
Test Strategy
- isc: ISC-3
type: coverage-probe
check: every REST endpoint maps to a GraphQL field
threshold: 14/14
tool: node scripts/coverage-check.ts
- isc: ISC-5
type: parity-test
check: GraphQL response body byte-equal to REST response for 1000 fixtures
threshold: 1000/1000
tool: bun test parity/read.test.ts
- isc: ISC-7
type: load
check: GraphQL p95 vs REST p95 for top-5 query shapes
threshold: GraphQL p95 ≤ REST p95 + 10ms at 200 rps
tool: k6 run loadtests/p95-parity.js
- isc: ISC-9
type: header-probe
check: every REST 2xx response includes Sunset header
threshold: 100% of sampled responses
tool: synthetic monitor + grep
- isc: ISC-19
type: regression-probe
check: REST shape diff vs frozen golden bodies
threshold: zero diffs
tool: bun test parity/rest-stability.test.ts (daily cron)
- isc: ISC-20
type: anti-probe
check: depth-limit middleware blocks deep queries
threshold: 400 response on depth=9
tool: curl + jq
- isc: ISC-22
type: anti-probe
check: introspection disabled in production
threshold: 403 on __schema query with non-admin token
tool: bun test security/introspection.test.ts
- isc: ISC-23
type: anti-probe
check: cutover requires partner confirmation
threshold: cutover.ts exits non-zero if any active partner_id missing confirmed_migration_date
tool: bun scripts/cutover.ts --dry-run
- isc: ISC-41
type: load
check: 1-hour soak at 500 rps mixed workload
threshold: error rate < 0.5%
tool: k6 run loadtests/soak.jsFeatures
- name: SchemaAndCodegen
description: Define `schema/api.graphql` covering all 14 endpoint shapes; wire up codegen for typed resolver stubs at `src/generated/resolvers.ts`.
satisfies: [ISC-1, ISC-2, ISC-3, ISC-4, ISC-27, ISC-38, ISC-39, ISC-64]
depends_on: []
parallelizable: false
- name: ResolverImplementation
description: Implement read and write resolvers backed by the existing data-access layer; ensure parity with REST responses; enforce auth scopes; per-request DataLoader.
satisfies: [ISC-5, ISC-6, ISC-29, ISC-31, ISC-40, ISC-49, ISC-62, ISC-63, ISC-66]
depends_on: [SchemaAndCodegen]
parallelizable: true # split by resource group: orgs/projects/users/billing/audit
- name: GatewayHardening
description: Apollo Server config, depth limit, cost analysis, request size limit, query timeout, circuit breaker, persisted queries, error scrubbing, introspection lock-down.
satisfies: [ISC-20, ISC-21, ISC-22, ISC-30, ISC-42, ISC-43, ISC-56, ISC-58, ISC-65]
depends_on: [ResolverImplementation]
parallelizable: true
- name: RestAdapter
description: Build `src/rest/adapter.ts` so the 14 REST routes execute through GraphQL resolvers; flag-rolled to 100% before deprecation telemetry begins; preserves REST byte-shape.
satisfies: [ISC-19, ISC-50, ISC-51, ISC-52, ISC-53, ISC-54]
depends_on: [ResolverImplementation]
parallelizable: false
- name: DeprecationTelemetry
description: Sunset/Deprecation/Link headers, partner_id tagging, internal dashboard, weekly partner status emails, alerting on REST traffic regression, partner-status.json nightly regen.
satisfies: [ISC-9, ISC-10, ISC-11, ISC-12, ISC-13, ISC-14, ISC-15, ISC-16, ISC-25, ISC-32, ISC-33, ISC-34]
depends_on: [RestAdapter]
parallelizable: true
- name: DocsAndPlayground
description: GraphQL playground at the live endpoint with pre-populated examples; side-by-side REST/GraphQL docs; public changelog entries; published SDL.
satisfies: [ISC-17, ISC-18, ISC-36, ISC-37]
depends_on: [SchemaAndCodegen]
parallelizable: true
- name: CutoverGovernance
description: Per-partner confirmed_migration_date tracking, T-90/60/30/14/7/1 emails, dry-run at T-30, runbooks, status page components, postmortem.
satisfies: [ISC-23, ISC-26, ISC-35, ISC-44, ISC-46, ISC-47, ISC-48, ISC-55, ISC-57, ISC-67, ISC-68, ISC-69, ISC-70, ISC-71, ISC-72]
depends_on: [DeprecationTelemetry]
parallelizable: falseDecisions
- 2026-01-12 17:15: Apollo Server v4 over Yoga or a hand-rolled implementation. Existing team familiarity, mature plugin ecosystem, schema-first defaults. Yoga rejected because the persisted-query story is less mature for partners on legacy SDKs.
- 2026-01-19 22:00: Schema-first with codegen rather than code-first. The SDL is the contract the partners read; making it the source of truth means PR diffs on
schema/api.graphqlare reviewable as contract changes by people who don't read TypeScript. - 2026-01-26 14:30: REST adapter layer (one resolver path, two transports) rather than maintaining REST handlers in parallel. Eliminates parity drift by construction. Cost: adapter overhead measured at ~3ms p95 in early prototype, well under the ISC-51 budget of 5ms.
- 2026-02-03 11:00: ❌ DEAD END: Tried Apollo Federation v2 to split the schema across 3 services owned by different product teams. Reverted after week-long spike — gateway introspection added 40ms p95 overhead and the team boundary was nominal (all 3 services share the same database). Single monolithic schema, owned by api-team, reviewed by product-team approvers per CODEOWNERS.
- 2026-02-10 09:45: 6-month deprecation window over 3 months. Partner survey (37 of 47 responded) showed 4 partners with quarterly release trains where a 3-month window would force an emergency rollout. Cost is real (longer parity guarantees, more telemetry overhead) but cheaper than 4 angry partners.
- 2026-02-18 16:20: refined: ISC-7 split into ISC-7 (top-5 query shapes, +10ms budget) and ISC-7.1 (next-20 shapes, +25ms budget). The two budgets reflect that the top-5 are tightly optimized REST paths while the next-20 are over-fetching today and GraphQL will already be faster on those by virtue of asking for fewer fields.
- 2026-02-25 21:00: ❌ DEAD END: Considered exposing GraphQL at
api-v2.apibridge.example.orgso the cutover would be a DNS swap. Rejected — partner integrations using URL-based service discovery would have to change config rather than client library, and the URL change would have meant more breaking surface than the protocol change. - 2026-03-04 10:00: APQ for partners that opt in, not mandatory. Mandatory APQ would force every partner to ship a registration step before going live; the migration cost is already non-trivial and APQ value is largest for the high-volume partners who will adopt it voluntarily.
- 2026-03-12 13:30: Cost limits per partner tier (500/2000/5000) calibrated against the most expensive REST endpoints' equivalent cost in the cost-analysis prototype; free-tier cap of 500 is ~2x the heaviest current REST call to leave migration headroom without leaving DoS surface.
- 2026-03-21 17:00: refined: added ISC-44 (synthetic monitor on deprecated endpoint pre-cutover) after partner-success team flagged that "deprecated" and "removed" had been conflated in two earlier migrations.
- 2026-04-08 09:30: Extended-support track capped at 3 partners (ISC-70). Operational cost of running parallel REST infrastructure past cutover scales worse than linearly; 3 is the threshold where a separate small REST cluster makes sense vs. ad-hoc bypass.
- 2026-04-15 22:15: refined: ISC-23 (anti: silent cutoff) hardened — the cutover script now reads
partner-status.jsonand exits non-zero if any active partner_id is missingconfirmed_migration_date. Earlier draft only logged a warning; partner-success caught a near-miss in dry-run where a newly added partner would have been cut off because the field was absent rather than false.
Changelog
- 2026-02-18 conjectured: a single +10ms p95 budget would cover all GraphQL query shapes vs REST. / refuted by: prototype load test (k6, 200 rps) showed top-5 already at +8ms while shapes 6-25 ranged +12ms to +22ms — single budget would fail on hot paths and over-budget on long-tail. / learned: REST is irregularly optimized; the top-5 shapes have hand-tuned indexes, the rest don't. GraphQL inherits this asymmetry. / criterion now: ISC-7 (top-5, +10ms) + ISC-7.1 (next-20, +25ms) — two budgets reflecting the underlying optimization asymmetry.
- 2026-02-25 conjectured: a
/v2URL split would make cutover a clean DNS-level swap with no client code changes. / refuted by: partner survey identified 11 integrations using URL-based service discovery (env vars or config files); URL change would force config-file edits and re-deploy, while protocol change touches only the client library. / learned: URL stability is a stronger contract than transport stability for service-discovery-based partners. / criterion now: GraphQL co-located atapi.apibridge.example.org/graphql; no/v2, no subdomain split — preserved as a Constraint.
- 2026-03-21 conjectured: the deprecation-window guarantee that "REST endpoints continue working" was sufficient. / refuted by: partner-success team review found that "endpoint working" had been ambiguously interpreted in two earlier minor-version cutovers — partners read it as "still routable," ops read it as "still serving the documented payload." / learned: the deprecation contract has to specify byte-shape stability AND endpoint reachability, separately, with separate probes. / criterion now: ISC-19 (Anti: REST shape changes) plus ISC-44 (Anti: REST endpoint returns 404 before cutover) — two probes, daily cadence, separate failure modes.
- 2026-04-15 conjectured: cutover script logging a warning when a partner_id was missing
confirmed_migration_datewas sufficient governance. / refuted by: dry-run revealed a newly onboarded partner whose record had been created without the field; warning was lost in normal log volume and the script proceeded. / learned: governance gates must hard-fail; partial enforcement of a binary anti-criterion is no enforcement. / criterion now: ISC-23 hardened — cutover script exits non-zero on missing field; partner-success owns the field-presence check in onboarding.
Verification
- ISC-1:
graphql-schema-linter schema/api.graphqlexits 0; output0 errors, 0 warnings. Verified 2026-02-04. - ISC-2:
curl -s -X POST api.apibridge.example.org/graphql -H "Authorization: Bearer $T" -d '{"query":"{ __schema { queryType { name } } }"}' | jq -r '.data.__schema.queryType.name'returnsQuery. Verified 2026-02-12 (staging) and 2026-03-04 (production behind feature flag). - ISC-3:
node scripts/coverage-check.tsoutputs14/14 REST endpoints have a matching GraphQL field. Verified 2026-02-15. - ISC-5:
bun test parity/read.test.tsreports1000 passed, 0 failed. Verified 2026-03-08. - ISC-7: k6 run output for top-5 query shapes — REST p95: 87ms / GraphQL p95: 91ms (+4ms, well within +10ms budget). Verified 2026-03-22.
- ISC-9, ISC-10, ISC-11:
curl -I api.apibridge.example.org/v1/orgs/test-orgshowsSunset: Wed, 22 Oct 2026 00:00:00 GMT,Deprecation: true,Link: <https://docs.apibridge.example.org/graphql>; rel="successor-version". Verified 2026-04-22. - ISC-22: introspection probe with non-admin token returns
403 Forbiddenwith body{"errors":[{"message":"Introspection disabled in production","extensions":{"code":"INTROSPECTION_DISABLED"}}]}. Verified 2026-03-04. - ISC-50, ISC-52:
git log --oneline src/rest/handlers/shows final commit deleting all 14 legacy direct handlers;src/rest/adapter.tsis the sole REST entry point. Verified 2026-04-10.
<!-- Fictitious example. "Cardinal" is a teaching project name; any resemblance to real products or organizations is coincidental. The example.com domain is RFC 2606 reserved. --> --- task: "Build the Cardinal brand identity system from blank canvas to first 5 surfaces" slug: 20260203-141200_cardinal-brand-identity-launch project: Cardinal effort: deep effort_source: explicit phase: execute progress: 18/56 mode: interactive started: 2026-02-03T22:12:00Z updated: 2026-04-19T11:30:00Z ---
Problem
Cardinal is a six-person fintech startup at cardinal.example.com building a single-purpose product: helping new immigrants in the US open their first investment account in under 10 minutes. They have a working product, three angel investors, and a logo their cofounder drew on a napkin. The napkin logo does not survive contact with a 16px favicon, the website uses three different shades of blue depending on which page you land on, and the most recent investor email signed off with a tone the founder describes as "bank-stiff" while the homepage hero copy describes the product as "your most encouraging financial friend." Every surface contradicts the others.
The product is good. The first 200 users love it. But Cardinal is about to do its first proper marketing push — App Store launch, Hacker News post, paid social on three platforms, partner co-marketing with two community organizations — and the brand cannot sustain that level of exposure. A user who taps the App Store icon, lands on the website, opens the welcome email, and reads the founder's tweet should feel like all four were written and designed by the same intentional person. Right now, four random pieces of clip art communicating four random feelings.
Vision
The founder's mom — who has never used the product and doesn't know what it does — sees the new logo on a coffee shop sticker, says "oh, that's pretty," and a week later texts her son a photo of the same logo on a bus shelter ad asking "is that yours?" That recognition with no prior priming is the target. When the founder opens the brand kit on launch day and clicks through the homepage, the App Store screenshots, the welcome email, the partner one-pager, and the launch tweet, the experience reads as a single voice across five surfaces. Euphoric surprise: the cofounder who drew the napkin logo says "I don't even miss it."
Out of Scope
- Product UI redesign. The brand work informs the existing app's color and type tokens but does not redesign the in-app onboarding flows.
- Naming. The name "Cardinal" stays. No naming exploration, no trademark refile.
- Internationalization of brand voice. English only for v1; Spanish-language voice work happens after launch.
- Motion design system. Static brand only. After-effects, Lottie, and animated logos are post-launch.
- Photography library. Stock placeholders are acceptable for the first five surfaces; bespoke photo direction comes later.
- Sub-brand exploration. No "Cardinal for Business," "Cardinal Pro," "Cardinal Wealth" — single master brand only.
- Print collateral beyond the partner one-pager. No business cards, no event signage, no swag.
Principles
- A brand is a recognition system, not a logo. The logo is one of seven elements; the system is the contract.
- Constraint produces character. A two-color palette with one weight of one typeface usually beats six colors and three weights. We start under-decorated and earn additions.
- Voice lives in word choice, not in adjectives about voice. "Direct, warm, never patronizing" is what we write under our own paragraph; the test is that someone given the guide can write a paragraph indistinguishable from the founder's.
- Every artifact must survive its smallest size. If the logo doesn't read at 16px, it doesn't read. If the type system doesn't work in a 13px form label, it doesn't work.
- Open-source defaults. Type, palette, and icon system must work without paid foundry licenses for early-stage runway, and remain swap-in compatible with paid alternatives later.
- The brand is borrowed from the user, not invented for them. Voice and tone derive from the language the first 200 users use to describe the product, not from a moodboard.
Constraints
- Single sans-serif type family for the entire system (display + body + UI). Two weights maximum.
- Two-color core palette plus a neutral scale of 5 steps. No third hue introduced before launch.
- Logo must remain recognizable at 16×16px and in 1-bit black-and-white.
- Type family must have an SIL Open Font License (OFL) version available; no paid-only foundry dependencies in v1.
- Voice guide ships as a single page with worked examples; no 60-page brand book.
- Brand kit is delivered as a Figma file at
figma.com/cardinal-brand-v1and a GitHub repo atgithub.example.com/cardinal/brand-v1(mirrored, both public). - Color contrast must pass WCAG 2.2 AA for all text + background combinations the system can produce.
- All five launch surfaces (homepage hero, App Store screenshot set, welcome email, partner one-pager, launch tweet) ship from the same kit on the same day.
- No AI-generated illustrations in the launch surfaces. Hand-drawn or geometric only.
Goal
Deliver a complete Cardinal brand identity v1 — logo (3 lockups), type system (1 family, 2 weights, 6 sizes), color palette (2 hues + 5-step neutral), voice and tone guide (1 page with 6 worked rewrites), and the first 5 marketing surfaces (homepage hero, App Store screenshot set, welcome email, partner one-pager, launch tweet) — all designed against constraints that survive the smallest-size and 1-bit tests, packaged as a Figma file and a public GitHub repo, ready to ship together on the founder-confirmed launch date of April 26, 2026.
Criteria
- [x] ISC-1: Wordmark and standalone mark exist as separate Figma components with shared baseline.
- [x] ISC-2: Three logo lockups in the kit: horizontal wordmark, stacked wordmark + mark, mark-only.
- [x] ISC-3: Antecedent: logo wordmark renders legibly at 16×16px (probe: Skill('Interceptor') screenshot at
cardinal.example.com/favicon.ico— three unfamiliar viewers identify "Cardinal" within 5 seconds, ≥2/3 succeed). - [x] ISC-4: Antecedent: logo mark survives 1-bit black-and-white conversion without losing recognizability (probe: viewer test — 5 people shown 1-bit version next to color version, ≥4/5 say "same logo").
- [x] ISC-5: Logo files exported to SVG (master), PNG @1x/@2x/@3x, and ICO favicon.
- [ ] ISC-6: Logo clear-space rule documented (≥ ½ × cap height on all sides).
- [x] ISC-7: Type family selected with confirmed OFL license (probe:
head -50 fonts/<family>/LICENSE.txtshows SIL OFL 1.1). - [x] ISC-8: Type system defines exactly 6 sizes: 12, 14, 16, 20, 28, 44 (px on web; pt on print).
- [x] ISC-9: Type system uses exactly 2 weights: Regular 400 and Semibold 600.
- [ ] ISC-10: Antecedent: body copy at 16px renders cleanly at 1.5× line-height across Chrome, Safari, Firefox latest (probe: Interceptor screenshot diff per browser, no kerning regressions).
- [x] ISC-11: Color palette defines exactly 2 hue tokens:
cardinal-red-600(primary) anddawn-500(secondary). - [x] ISC-12: Neutral scale defines exactly 5 steps:
ink-900,ink-700,ink-500,ink-300,ink-100. - [x] ISC-13: Color palette exported as CSS custom properties at
tokens/colors.css. - [ ] ISC-14: Color palette exported as Figma styles in the kit file.
- [x] ISC-15: WCAG 2.2 AA contrast confirmed for all foreground/background pairs the system permits (probe:
bun scripts/contrast-check.tsexits 0). - [ ] ISC-16: Voice guide exists at
brand/voice.md, fits on one printed page (≤ 60 lines). - [ ] ISC-17: Voice guide includes 6 worked rewrites — the same sentence in "off-brand" and "on-brand" form for: confirmation, error, marketing headline, support reply, social caption, legal disclosure.
- [ ] ISC-18: Antecedent: the founder, given a fresh paragraph drafted by an outsider against the voice guide, cannot tell which sentence the outsider wrote vs the founder rewrote (probe: blind A/B test with founder, target ≥ 50% confusion across 10 trials).
- [ ] ISC-19: Voice guide explicitly names 5 things voice does NOT do (anti-voice prompts).
- [x] ISC-20: Homepage hero (
cardinal.example.com/) uses the new logo, type, and color tokens — no legacy assets. - [ ] ISC-21: App Store screenshot set (5 screens) designed in the kit, exported at App Store-required resolutions for iPhone 6.7" and 6.1".
- [ ] ISC-22: Welcome email template (
emails/welcome.html) renders with brand fidelity in Gmail, Apple Mail, Outlook 365 (probe: Litmus screenshot diff across 3 clients). - [ ] ISC-23: Partner one-pager exists at
brand/partner-one-pager.pdf, 1 page, prints correctly on US Letter and A4. - [ ] ISC-24: Launch tweet draft is in the kit at
brand/launch-tweet.mdwith associated 1200×675 image. - [ ] ISC-25: All 5 launch surfaces use the same logo lockup — no variant drift.
- [ ] ISC-26: All 5 launch surfaces use the same hex value for the primary brand color (probe:
bun scripts/surface-color-audit.tsreports zero deviations fromcardinal-red-600). - [ ] ISC-27: All 5 launch surfaces use the same type family at the same weight scale.
- [ ] ISC-28: Anti: the logo mark resembles a generic compass, leaf, or arrow more than the chosen form (probe: viewer test — show mark to 10 people unfamiliar with the brand, ask "what does it look like?", fewer than 3 mention generic shapes).
- [ ] ISC-29: Anti: any launch surface uses a color not in the published palette (probe: surface-color-audit script).
- [ ] ISC-30: Anti: the chosen typeface lacks an OFL or otherwise-redistributable alternative (probe: license header check).
- [ ] ISC-31: Anti: the voice guide reads as so prescriptive that the founder's own writing fails it (probe: founder writes a 3-paragraph product update without referring to the guide; voice guide author scores it; ≤ 1 violation).
- [ ] ISC-32: Anti: the App Store screenshots use placeholder text like "Lorem ipsum" or "Your headline here" anywhere visible.
- [ ] ISC-33: Anti: any surface includes the cofounder's napkin logo (probe: visual diff against retired-asset folder).
- [ ] ISC-34: Anti: the welcome email signs off with the same first-line greeting as any other Cardinal email template (probe:
rg "^Hi there" emails/returns ≤ 1 match). - [x] ISC-35: Figma kit file is shared with edit access for the founder and read access for the cofounder + 3 angels.
- [ ] ISC-36: GitHub repo
github.example.com/cardinal/brand-v1mirrors the Figma kit's exported assets (logo SVG/PNG, color tokens, type tokens, voice guide). - [ ] ISC-37: README in the brand repo includes a "How to use this kit" section with 4 examples: web, email, print, social.
- [ ] ISC-38: Brand assets repo includes a
LICENSEfor the assets (CC BY 4.0 for marketing usage; logo trademark notice separate). - [ ] ISC-39: Logo SVG validates as well-formed (probe:
xmllint --noout brand/logo.svg). - [ ] ISC-40: Logo SVG file size ≤ 4KB.
- [ ] ISC-41: Favicon at 16×16, 32×32, 48×48 packed into a single .ico file at
cardinal.example.com/favicon.ico. - [ ] ISC-42: Apple touch icon at 180×180 served at
cardinal.example.com/apple-touch-icon.png. - [ ] ISC-43: Open Graph image at 1200×630 served at
cardinal.example.com/og.pngusing the launch lockup. - [ ] ISC-44: Internal "voice gut-check" form exists in the kit — 4-question checklist anyone on the team runs against any draft before publishing (Is it direct? Warm without being cute? Specific instead of generic? Free of jargon the user wouldn't say?).
- [ ] ISC-45: Tone-by-context table exists in the voice guide: marketing, transactional, error, support, legal — one row each, with do/don't examples.
- [ ] ISC-46: Anti: voice guide adjectives appear as the only definition of voice with zero worked examples (probe: voice guide must contain ≥ 6 sentence-level rewrites in addition to any descriptors).
- [ ] ISC-47: Brand guideline page rendered at
cardinal.example.com/brandand crawlable. - [ ] ISC-48: Press kit downloadable as a single ZIP at
cardinal.example.com/press, includes logos in 3 formats and a 200-word company description. - [ ] ISC-49: Antecedent: the launch tweet image, when posted to X without context text, draws ≥ 3 unprompted DM replies asking "what's Cardinal?" within 48 hours of test post (probe: founder dry-run on personal account 7 days pre-launch).
- [ ] ISC-50: Three angel investors, given the kit cold (no walkthrough), can identify which surface is on-brand vs a planted decoy in 4 of 5 trials.
- [ ] ISC-51: Cofounder (the napkin-logo author) signs off in writing on the new mark.
- [x] ISC-52: All retired assets (napkin logo, three legacy blues, prior tagline) moved to
brand/_retired/with a README explaining why. - [ ] ISC-53: Anti: more than two new colors or new type weights are introduced between v1 lock and launch (probe: git diff on tokens/ between freeze tag and launch tag — line count ≤ 0 additions).
- [ ] ISC-54: A "v1 freeze" tag is cut on the brand repo at least 7 days before launch.
- [ ] ISC-55: Launch retrospective scheduled for May 3, 2026, with the founder, cofounder, and the brand designer.
- [ ] ISC-56: Antecedent: the founder reports the "I don't miss the napkin logo" feeling — captured verbatim in retro notes (probe: retro doc, search for the exact quote or a paraphrase the founder confirms).
Test Strategy
- isc: ISC-3
type: experiential-probe
check: legibility at 16px favicon
threshold: ≥2/3 unfamiliar viewers identify "Cardinal" within 5 seconds
tool: Skill('Interceptor') screenshot + 3-viewer survey
- isc: ISC-4
type: experiential-probe
check: 1-bit B&W recognizability
threshold: ≥4/5 viewers say "same logo" as color version
tool: viewer survey
- isc: ISC-7
type: license-probe
check: typeface OFL 1.1
threshold: license header matches "SIL OPEN FONT LICENSE Version 1.1"
tool: head fonts/<family>/LICENSE.txt
- isc: ISC-15
type: contrast
check: WCAG 2.2 AA across all permitted FG/BG pairs
threshold: zero violations
tool: bun scripts/contrast-check.ts
- isc: ISC-18
type: experiential-probe
check: voice guide reproducibility
threshold: ≥50% founder confusion across 10 blind A/B trials
tool: blind A/B test (Outsider draft + Founder rewrite vs Founder draft)
- isc: ISC-26
type: visual-audit
check: primary color hex consistency across launch surfaces
threshold: zero deviations from cardinal-red-600
tool: bun scripts/surface-color-audit.ts
- isc: ISC-31
type: anti-probe
check: voice guide must not over-prescribe
threshold: ≤1 voice violation in founder's own unguided 3-paragraph draft
tool: voice author scores founder draft
- isc: ISC-49
type: experiential-probe
check: launch tweet image draws unprompted curiosity
threshold: ≥3 DM replies asking "what is this" within 48 hours
tool: founder dry-run on personal X account, 7 days pre-launch
- isc: ISC-50
type: experiential-probe
check: angels can sort on-brand vs decoy
threshold: 4 of 5 correct identifications
tool: 5-trial sort with planted decoys (off-brand color, off-brand voice, off-brand lockup)Features
- name: LogoSystem
description: Wordmark, standalone mark, three lockups; export pipeline to SVG/PNG/ICO; 16px and 1-bit survival; cofounder sign-off on retired napkin logo.
satisfies: [ISC-1, ISC-2, ISC-3, ISC-4, ISC-5, ISC-6, ISC-28, ISC-39, ISC-40, ISC-41, ISC-42, ISC-43, ISC-51, ISC-52]
depends_on: []
parallelizable: false
- name: TypeAndColorTokens
description: Single OFL typeface with 2 weights and 6 sizes; 2-hue palette plus 5-step neutral; CSS custom properties + Figma styles; WCAG AA contrast across all permitted pairs.
satisfies: [ISC-7, ISC-8, ISC-9, ISC-10, ISC-11, ISC-12, ISC-13, ISC-14, ISC-15, ISC-30]
depends_on: []
parallelizable: true
- name: VoiceAndTone
description: One-page voice guide with 6 worked rewrites, anti-voice list, tone-by-context table, voice gut-check checklist; founder reproducibility test.
satisfies: [ISC-16, ISC-17, ISC-18, ISC-19, ISC-31, ISC-44, ISC-45, ISC-46]
depends_on: []
parallelizable: true
- name: LaunchSurfaces
description: Homepage hero, App Store screenshot set, welcome email, partner one-pager, launch tweet — all built from the kit, all using the same lockup, color, and type weight scale.
satisfies: [ISC-20, ISC-21, ISC-22, ISC-23, ISC-24, ISC-25, ISC-26, ISC-27, ISC-32, ISC-33, ISC-34]
depends_on: [LogoSystem, TypeAndColorTokens, VoiceAndTone]
parallelizable: true
- name: KitDistribution
description: Figma kit file with appropriate access; mirrored GitHub repo with assets + tokens + voice guide + LICENSE; README with 4 usage examples; brand page on the marketing site; downloadable press ZIP.
satisfies: [ISC-35, ISC-36, ISC-37, ISC-38, ISC-47, ISC-48]
depends_on: [LaunchSurfaces]
parallelizable: false
- name: LaunchGovernance
description: v1 freeze tag 7 days pre-launch; angel-investor sort test; founder reproducibility check; anti-drift audits; retrospective scheduled and run; the "I don't miss the napkin logo" capture.
satisfies: [ISC-29, ISC-49, ISC-50, ISC-53, ISC-54, ISC-55, ISC-56]
depends_on: [KitDistribution]
parallelizable: falseDecisions
- 2026-02-03 22:12: Single typeface for the whole system over a display + body pairing. The product is small, the team is small, the budget is small, and most "two-typeface" systems reduce to "one of these typefaces does 95% of the work." Pick the one that does both jobs and own the constraint as character.
- 2026-02-09 18:30: Two-hue palette over a richer multi-hue system. The ten Cardinal users we asked described the brand feeling as "warm and not cluttered." A wider palette makes "not cluttered" harder to keep, not easier.
- 2026-02-15 11:45: Voice derives from corpus mining, not from adjectives. Pulled 200 user-written reviews and support replies, ran them through extraction, found that users describe the product with the words "patient," "specific," and "doesn't talk down." Those three words now anchor the guide. Adjectives the founder originally wanted ("bold," "trustworthy," "modern") were rejected because no user used them.
- 2026-02-22 09:00: ❌ DEAD END: Tried building a custom serif display companion to the sans body — 11 days of exploration. Killed because none of the candidate serifs survived the 16px favicon test, and pairing forced lockup variants that broke the "all five surfaces use the same lockup" constraint. The single-typeface decision held.
- 2026-03-01 16:20: refined: ISC-3 and ISC-4 promoted to Antecedent prefix. They are not just probes; they are the preconditions that produce the recognition-without-priming experience the Vision describes. If they fail, the Vision is unreachable regardless of what else passes.
- 2026-03-08 14:00: ❌ DEAD END: Considered a paid foundry license for a typeface the founder loved. Rejected after pricing — $4,800/year for the weights we'd need at the user count we'd reach. Rolled back to OFL alternatives, found one that passed every probe within a week of evaluation, and the founder now prefers it. The constraint produced the better answer.
- 2026-03-15 21:30: refined: added ISC-49 after the cofounder asked "but how do we know the launch tweet will actually work?" Founder dry-run on personal X account is the only honest probe. If the image doesn't draw curiosity from people who don't know what Cardinal is, the brand isn't doing its job, regardless of how much we like it.
- 2026-03-22 10:00: refined: ISC-31 added after the first voice guide draft was so prescriptive that the founder's own writing failed it. The guide must describe the floor of voice, not the ceiling — the founder's natural writing must clear it without effort.
- 2026-04-05 19:45: Cofounder signed off on the new mark in writing. Logged the napkin logo retirement to
_retired/README.mdwith the cofounder's quote: "It served us. The new one is the one we needed." - 2026-04-12 13:00: refined: ISC-50 added — three angels given the kit cold, asked to sort on-brand vs planted decoys. If people who paid for this brand can't tell on-brand from off-brand without a walkthrough, the brand isn't a system yet.
Changelog
- 2026-02-22 conjectured: a custom serif display face paired with the OFL sans body would give Cardinal a more distinctive editorial voice on marketing surfaces while keeping product UI clean. / refuted by: 11 days of exploration produced no serif candidate that survived the 16px favicon test or the 1-bit survival test; pairing also forced two extra lockups for surfaces where serifs and sans collided, breaking the "single lockup across surfaces" constraint. / learned: the constraints we'd already locked in (16px legibility, 1-bit survival, surface lockup consistency) implicitly forbade dual-typeface systems for our scale. The constraints did the deciding before the moodboard did. / criterion now: ISC-7 / ISC-8 / ISC-9 stand — single OFL family, 2 weights, 6 sizes — and the rejected pairing is documented in
_retired/typeface-exploration.md.
- 2026-03-01 conjectured: legibility at 16px and 1-bit survival were ordinary verifiable ISCs. / refuted by: the Vision section names a specific experience — recognition without prior priming — and that experience is impossible if the logo can't survive a coffee-shop sticker glance or a low-fidelity reproduction. The probes aren't just verifying; they're naming the precondition for the Vision to be reachable at all. / learned: experiential goals require Antecedent ISCs — preconditions that must hold for the target experience to even be possible. Without them, the Vision is decoupled from the criteria. / criterion now: ISC-3 and ISC-4 carry the
Antecedent:prefix; ISC-10, ISC-18, ISC-49, ISC-56 added as additional Antecedents anchoring other Vision claims.
- 2026-03-08 conjectured: the brand could afford a paid foundry license for a typeface the founder personally preferred. / refuted by: pricing for the weights and seats we'd need across the lifetime of the early-stage runway came to $4,800/year — disproportionate to the design value gained over OFL alternatives that pass every probe equally well. / learned: paid-only typeface dependencies are a hidden lock-in that compounds at every team-size and surface-count milestone; the OFL constraint isn't a downgrade, it's a future-proofing decision. / criterion now: ISC-7 (OFL license confirmed) is now a hard Constraint; ISC-30 (Anti: typeface lacks OFL alternative) backstops it.
- 2026-03-22 conjectured: the voice guide could safely be prescriptive — the more specific the rules, the more reproducible the voice. / refuted by: the founder's own unguided 3-paragraph product update failed the first draft of the guide on 4 of 9 sentences. A guide that the natural voice fails is a guide that isn't describing the natural voice — it's inventing one. / learned: voice guides describe the floor of acceptable, not the ceiling of ideal. The probe for the guide is whether the person whose voice you're capturing clears it without trying. / criterion now: ISC-31 (Anti: founder's own unguided draft fails the guide) added as a hard probe; voice guide rewritten against this constraint and re-tested.
Verification
- ISC-1: Figma file
cardinal-brand-v1shows wordmark and standalone mark as separate components, confirmed via component inspector. Verified 2026-02-26. - ISC-3: Interceptor screenshot of
cardinal.example.com/favicon.icorendered in 3 browsers; viewer survey of 3 unfamiliar designers — 3/3 identified "Cardinal" within 4 seconds. Verified 2026-03-04. - ISC-4: 1-bit B&W viewer test — 5/5 viewers said "same logo." Verified 2026-03-04.
- ISC-5:
ls brand/logo/showscardinal.svg(master),cardinal@1x.png,cardinal@2x.png,cardinal@3x.png,favicon.ico. Verified 2026-03-09. - ISC-7:
head -50 fonts/<family>/LICENSE.txtreturnsSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007. Verified 2026-02-12. - ISC-13:
cat tokens/colors.cssshows seven--cardinal-*and--ink-*custom properties matching palette spec. Verified 2026-02-19. - ISC-15:
bun scripts/contrast-check.tsexits 0; output confirms 24/24 permitted FG/BG pairs pass WCAG 2.2 AA. Verified 2026-02-19. - ISC-20: Homepage screenshot diff against staging shows new logo, type tokens, and color tokens; legacy assets purged from
public/. Verified 2026-04-19. - ISC-35: Figma share dialog screenshot confirms founder has Edit, cofounder + 3 angels have Read. Verified 2026-02-26.
- ISC-52:
ls brand/_retired/showsnapkin-logo.png,legacy-blues.css,prior-tagline.md, andREADME.mdwith retirement rationale. Verified 2026-04-05.