From 23b0d80ed272a2b2acd55c26bc300c55ce3f0ccb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 07:17:17 +0000 Subject: [PATCH 1/2] docs: add a system map section to the landing page Shows how capabilities, specs and decision records link into one graph, and how opt-in traceability adds code, tests and gaps to it. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01QoEZp4TGpWNBkNtFXW2G3b --- docs/src/components/SystemMap.astro | 203 ++++++++++++++++++++++++++++ docs/src/pages/index.astro | 2 + 2 files changed, 205 insertions(+) create mode 100644 docs/src/components/SystemMap.astro diff --git a/docs/src/components/SystemMap.astro b/docs/src/components/SystemMap.astro new file mode 100644 index 00000000..0a95d244 --- /dev/null +++ b/docs/src/components/SystemMap.astro @@ -0,0 +1,203 @@ +--- +/* + * Capabilities, specs and decision records as one graph, with traceability linking code and tests into it. + * Every node and edge kind drawn here is one `osq graph` prints; the example names come from the traceability spec. + */ +type Kind = 'adr' | 'capability' | 'requirement' | 'scenario' | 'function' | 'test' | 'file'; +interface Node { kind: Kind; name: string; gap?: string } +interface Edge { from: string; to: string; label: string } + +const H = 48; + +const nodes: Record = { + adr: { kind: 'adr', name: '001 Use cents' }, + cap: { kind: 'capability', name: 'pricing' }, + req: { kind: 'requirement', name: 'Discounts' }, + scn: { kind: 'scenario', name: 'Bulk tiers' }, + gap: { kind: 'scenario', name: 'Member price', gap: 'untested' }, + fn: { kind: 'function', name: 'quote()' }, + test: { kind: 'test', name: 'quote.test.ts' }, + file: { kind: 'file', name: 'scripts/seed.ts', gap: 'unowned' }, +}; + +const edges: Edge[] = [ + { from: 'adr', to: 'cap', label: 'applies to' }, + { from: 'cap', to: 'req', label: 'contains' }, + { from: 'req', to: 'scn', label: 'contains' }, + { from: 'req', to: 'gap', label: 'contains' }, + { from: 'cap', to: 'fn', label: 'owns' }, + { from: 'fn', to: 'scn', label: 'serves' }, + { from: 'fn', to: 'adr', label: 'follows' }, + { from: 'test', to: 'fn', label: 'covers' }, + { from: 'test', to: 'scn', label: 'proves' }, +]; + +type Layout = { w: number; width: number; height: number; at: Record; bends?: Record }; + +// three columns on wide screens, two on phones, so the labels stay readable at either size +const wide: Layout = { + w: 150, + width: 630, + height: 330, + at: { adr: [0, 0], cap: [230, 0], req: [460, 0], fn: [230, 141], scn: [460, 141], test: [230, 282], gap: [460, 282], file: [0, 282] }, + bends: { 'req>gap': [[628, 24], [628, 306]] }, +}; +const narrow: Layout = { + w: 135, + width: 352, + height: 470, + at: { adr: [0, 0], cap: [205, 0], fn: [0, 141], req: [205, 141], test: [0, 282], scn: [205, 282], file: [0, 422], gap: [205, 422] }, + bends: { 'req>gap': [[350, 165], [350, 446]] }, +}; + +/** Where the line from a box's centre toward `[tx, ty]` leaves the box. */ +function exit(W: number, [x, y]: [number, number], [tx, ty]: [number, number]): [number, number] { + const cx = x + W / 2; + const cy = y + H / 2; + const dx = tx - cx; + const dy = ty - cy; + const t = Math.min(dx ? W / 2 / Math.abs(dx) : Infinity, dy ? H / 2 / Math.abs(dy) : Infinity); + return [cx + dx * t, cy + dy * t]; +} + +const centre = (W: number, [x, y]: [number, number]): [number, number] => [x + W / 2, y + H / 2]; +const fmt = (pts: [number, number][]) => 'M' + pts.map(([x, y]) => `${x.toFixed(1)} ${y.toFixed(1)}`).join(' L'); + +function draw(layout: Layout) { + const W = layout.w; + return edges.map((edge, i) => { + const a = layout.at[edge.from]; + const b = layout.at[edge.to]; + const bend = layout.bends?.[`${edge.from}>${edge.to}`]; + const pts: [number, number][] = bend + ? [[a[0] + W, bend[0][1]], ...bend, [b[0] + W, bend[bend.length - 1][1]]] + : [exit(W, a, centre(W, b)), exit(W, b, centre(W, a))]; + // a bent edge runs along the frame and goes unlabelled; its twin edge already names the link + const mid: [number, number] = [(pts[0][0] + pts[1][0]) / 2, (pts[0][1] + pts[1][1]) / 2]; + return { d: fmt(pts), mid, label: bend ? '' : edge.label, delay: (-i * 0.9).toFixed(1), gap: Boolean(nodes[edge.to].gap) }; + }); +} + +const layouts = [ + { name: 'wide', layout: wide }, + { name: 'narrow', layout: narrow }, +].map(({ name, layout }) => ({ name, layout, lines: draw(layout) })); + +const kindLabel: Record = { + adr: 'Decision', + capability: 'Capability', + requirement: 'Requirement', + scenario: 'Scenario', + function: 'Function', + test: 'Test', + file: 'File', +}; +const codeKinds: Kind[] = ['function', 'test', 'file']; + +const parts = [ + { kind: 'Capabilities', body: 'Each one names a part of what your software does and the files that belong to it.' }, + { kind: 'Specs', body: 'Requirements and the scenarios that prove them, in plain words. They change only when an approved change lands.' }, + { kind: 'Decisions', body: 'Architecture decision records name the capabilities they govern. Accepted ones become rules every agent reads.' }, + { + kind: 'Traceability', + opt: true, + body: 'Tag a function with the scenario it serves and the decision it follows, and name each test after its scenario. osq links code and tests into the graph and points out the gaps: a scenario no test proves, a function no scenario claims, a file no capability owns.', + }, +]; +--- + +
+
+
+ A map of your software +

Know what you built, and why.

+

Capabilities, specs and decision records are plain files in your repo that link into one graph: what the software does, how each part behaves, and why it was built that way. People use it to find their way around. Agents get the same context before they plan or build.

+
    + {parts.map((part) => ( +
  1. +

    {part.kind}{part.opt && Opt in}

    +

    {part.body}

    +
  2. + ))} +
+ osq graph · osq spec · osq report +
+ +
+ {layouts.map(({ name, layout, lines }) => ( + + ))} +
+ Specs and decisions + Code and tests, with traceability + A gap osq points out +
+
+
+
+ + diff --git a/docs/src/pages/index.astro b/docs/src/pages/index.astro index 43f093b1..e9850ee8 100644 --- a/docs/src/pages/index.astro +++ b/docs/src/pages/index.astro @@ -3,6 +3,7 @@ import Base from '../layouts/Base.astro'; import Hero from '../components/Hero.astro'; import Why from '../components/Why.astro'; import HowItWorks from '../components/HowItWorks.astro'; +import SystemMap from '../components/SystemMap.astro'; import Features from '../components/Features.astro'; import GetStarted from '../components/GetStarted.astro'; --- @@ -14,6 +15,7 @@ import GetStarted from '../components/GetStarted.astro'; + From 081beec4d3090699a7484739f56c4b85e857d9c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 07:27:58 +0000 Subject: [PATCH 2/2] docs: drop the moving pulses from the system map Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01QoEZp4TGpWNBkNtFXW2G3b --- docs/src/components/SystemMap.astro | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/src/components/SystemMap.astro b/docs/src/components/SystemMap.astro index 0a95d244..1f2ebdba 100644 --- a/docs/src/components/SystemMap.astro +++ b/docs/src/components/SystemMap.astro @@ -65,7 +65,7 @@ const fmt = (pts: [number, number][]) => 'M' + pts.map(([x, y]) => `${x.toFixed( function draw(layout: Layout) { const W = layout.w; - return edges.map((edge, i) => { + return edges.map((edge) => { const a = layout.at[edge.from]; const b = layout.at[edge.to]; const bend = layout.bends?.[`${edge.from}>${edge.to}`]; @@ -74,7 +74,7 @@ function draw(layout: Layout) { : [exit(W, a, centre(W, b)), exit(W, b, centre(W, a))]; // a bent edge runs along the frame and goes unlabelled; its twin edge already names the link const mid: [number, number] = [(pts[0][0] + pts[1][0]) / 2, (pts[0][1] + pts[1][1]) / 2]; - return { d: fmt(pts), mid, label: bend ? '' : edge.label, delay: (-i * 0.9).toFixed(1), gap: Boolean(nodes[edge.to].gap) }; + return { d: fmt(pts), mid, label: bend ? '' : edge.label, gap: Boolean(nodes[edge.to].gap) }; }); } @@ -129,7 +129,6 @@ const parts = [ {lines.map((line) => ( - ))} {lines.filter((line) => line.label).map((line) => ( @@ -172,9 +171,7 @@ const parts = [ .graph svg.narrow { display: none; } .line { fill: none; stroke: color-mix(in srgb, var(--muted) 45%, transparent); stroke-width: 1.25; } - .pulse { fill: none; stroke: var(--accent); stroke-width: 2; stroke-linecap: round; stroke-dasharray: 10 90; animation: travel 7.2s linear infinite; } .to-gap .line { stroke-dasharray: 4 4; } - .to-gap .pulse { display: none; } .label { font-family: var(--mono); font-size: 10px; fill: var(--muted); paint-order: stroke; stroke: var(--surface); stroke-width: 5px; stroke-linejoin: round; } .node rect { fill: var(--accent-soft); stroke: color-mix(in srgb, var(--accent) 45%, transparent); stroke-width: 1; } @@ -192,12 +189,10 @@ const parts = [ .key.code { background: var(--surface); border: 1.5px solid var(--line); } .key.gap { border: 1.5px dashed var(--red-fg); } - @keyframes travel { from { stroke-dashoffset: 10; } to { stroke-dashoffset: -90; } } @media (max-width: 960px) { .map { grid-template-columns: minmax(0, 1fr); gap: 36px; } } @media (max-width: 560px) { .graph { padding: 16px; } .graph svg.wide { display: none; } .graph svg.narrow { display: block; } } - @media (prefers-reduced-motion: reduce) { .pulse { animation: none; opacity: 0; } }