executable-stories-formatters 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/cli.js +346 -167
  2. package/dist/cli.js.map +1 -1
  3. package/dist/index.cjs +8 -2
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +9 -0
  6. package/dist/index.d.ts +9 -0
  7. package/dist/index.js +8 -2
  8. package/dist/index.js.map +1 -1
  9. package/package.json +4 -2
  10. package/schemas/README.md +1 -1
  11. package/templates/astro-starlight/astro.config.mjs +57 -0
  12. package/templates/astro-starlight/gitignore +14 -0
  13. package/templates/astro-starlight/package.json +20 -0
  14. package/templates/astro-starlight/public/stories/assets/.gitkeep +0 -0
  15. package/templates/astro-starlight/public/stories/notes-index.json +4 -0
  16. package/templates/astro-starlight/public/stories/story-report.json +17 -0
  17. package/templates/astro-starlight/src/components/ApiOperations.astro +366 -0
  18. package/templates/astro-starlight/src/components/Checklist.astro +15 -0
  19. package/templates/astro-starlight/src/components/HealthDashboard.astro +171 -0
  20. package/templates/astro-starlight/src/components/PageTitle.astro +53 -0
  21. package/templates/astro-starlight/src/components/VerifiedBy.astro +281 -0
  22. package/templates/astro-starlight/src/components/VerifiedStep.astro +91 -0
  23. package/templates/astro-starlight/src/content/docs/examples/example-adr.mdx +45 -0
  24. package/templates/astro-starlight/src/content/docs/guides/behavior-portal.mdx +41 -0
  25. package/templates/astro-starlight/src/content/docs/guides/writing-docs.mdx +49 -0
  26. package/templates/astro-starlight/src/content/docs/index.mdx +49 -0
  27. package/templates/astro-starlight/src/content/docs/stories/.gitkeep +0 -0
  28. package/templates/astro-starlight/src/content.config.ts +18 -0
  29. package/templates/astro-starlight/src/lib/config.ts +50 -0
  30. package/templates/astro-starlight/src/lib/render-doc-entry.ts +154 -0
  31. package/templates/astro-starlight/src/lib/report-health.ts +61 -0
  32. package/templates/astro-starlight/src/lib/verification.ts +247 -0
  33. package/templates/astro-starlight/src/pages/explorer/explorer.css +729 -0
  34. package/templates/astro-starlight/src/pages/explorer/index.astro +404 -0
  35. package/templates/astro-starlight/src/styles/global.css +293 -0
  36. package/templates/astro-starlight/src/styles/themes/corporate.css +83 -0
  37. package/templates/astro-starlight/src/styles/themes/dashboard.css +76 -0
  38. package/templates/astro-starlight/src/styles/themes/default.css +86 -0
  39. package/templates/astro-starlight/src/styles/themes/minimal.css +87 -0
  40. package/templates/astro-starlight/src/styles/themes/playful.css +77 -0
  41. package/templates/astro-starlight/src/styles/themes/terminal.css +77 -0
  42. package/templates/astro-starlight/tsconfig.json +13 -0
@@ -0,0 +1,171 @@
1
+ ---
2
+ /**
3
+ * <HealthDashboard /> — a living landing widget.
4
+ *
5
+ * Reads the latest run at build time and shows pass rate, counts, freshness,
6
+ * and any failing stories. Drop it at the top of the home page so the first
7
+ * thing a stakeholder sees is whether the documented system is green.
8
+ */
9
+ import { report } from "../lib/config";
10
+ import { summarizeHealth, type FailingScenario } from "../lib/report-health";
11
+
12
+ const health = summarizeHealth(report);
13
+ const passPct = Math.round(health.passRate * 100);
14
+ const lastRun = health.lastRunMs
15
+ ? new Date(health.lastRunMs).toLocaleString(undefined, {
16
+ dateStyle: "medium",
17
+ timeStyle: "short",
18
+ })
19
+ : null;
20
+
21
+ const overall = health.empty
22
+ ? "empty"
23
+ : health.failed > 0
24
+ ? "failing"
25
+ : "healthy";
26
+
27
+ const metrics: Array<{ label: string; value: string | number }> = [
28
+ { label: "Pass rate", value: health.empty ? "—" : `${passPct}%` },
29
+ { label: "Scenarios", value: health.total },
30
+ { label: "Passed", value: health.passed },
31
+ { label: "Failed", value: health.failed },
32
+ ];
33
+
34
+ const failing: FailingScenario[] = health.failing;
35
+ ---
36
+
37
+ <section class:list={["health", `health--${overall}`]} aria-label="Test health dashboard">
38
+ <header class="health__head">
39
+ <span class="health__dot" aria-hidden="true"></span>
40
+ <strong class="health__title">
41
+ {health.empty ? "No test run yet" : health.failed > 0 ? "Attention needed" : "All systems documented & passing"}
42
+ </strong>
43
+ {lastRun && <span class="health__meta">last run {lastRun}</span>}
44
+ </header>
45
+
46
+ <div class="health__metrics">
47
+ {
48
+ metrics.map((m) => (
49
+ <div class="health__metric">
50
+ <span class="health__metric-value">{m.value}</span>
51
+ <span class="health__metric-label">{m.label}</span>
52
+ </div>
53
+ ))
54
+ }
55
+ </div>
56
+
57
+ {
58
+ health.empty && (
59
+ <p class="health__hint">
60
+ Generate a run with <code>executable-stories build-docs reports/raw-run.json --site-dir .</code> to light up this dashboard.
61
+ </p>
62
+ )
63
+ }
64
+
65
+ {
66
+ failing.length > 0 && (
67
+ <details class="health__failing" open>
68
+ <summary>{failing.length} failing {failing.length === 1 ? "story" : "stories"}</summary>
69
+ <ul>
70
+ {failing.map((f) => (
71
+ <li>
72
+ <span class="health__failing-title">{f.title}</span>
73
+ {f.feature && <span class="health__failing-feature"> · {f.feature}</span>}
74
+ </li>
75
+ ))}
76
+ </ul>
77
+ </details>
78
+ )
79
+ }
80
+ </section>
81
+
82
+ <style>
83
+ .health {
84
+ --health-accent: var(--sl-color-gray-3);
85
+ border: 1px solid var(--sl-color-gray-5);
86
+ border-radius: 0.75rem;
87
+ padding: 1.25rem 1.5rem;
88
+ margin: 0 0 2rem;
89
+ background: var(--sl-color-gray-7, var(--sl-color-gray-6));
90
+ }
91
+ .health--healthy {
92
+ --health-accent: #16a34a;
93
+ }
94
+ .health--failing {
95
+ --health-accent: #dc2626;
96
+ }
97
+ .health--empty {
98
+ --health-accent: #d97706;
99
+ }
100
+ .health__head {
101
+ display: flex;
102
+ align-items: center;
103
+ gap: 0.6rem;
104
+ margin-bottom: 1rem;
105
+ }
106
+ .health__dot {
107
+ width: 0.7rem;
108
+ height: 0.7rem;
109
+ border-radius: 999px;
110
+ background: var(--health-accent);
111
+ flex: none;
112
+ }
113
+ .health__title {
114
+ font-size: var(--sl-text-lg);
115
+ }
116
+ .health__meta {
117
+ margin-left: auto;
118
+ color: var(--sl-color-gray-3);
119
+ font-size: var(--sl-text-xs);
120
+ }
121
+ .health__metrics {
122
+ display: grid;
123
+ grid-template-columns: repeat(4, minmax(0, 1fr));
124
+ gap: 0.75rem;
125
+ }
126
+ .health__metric {
127
+ border: 1px solid var(--sl-color-gray-5);
128
+ border-radius: 0.5rem;
129
+ padding: 0.75rem;
130
+ text-align: center;
131
+ background: var(--sl-color-black, transparent);
132
+ }
133
+ .health__metric-value {
134
+ display: block;
135
+ font-size: var(--sl-text-2xl);
136
+ font-weight: 700;
137
+ line-height: 1;
138
+ }
139
+ .health__metric-label {
140
+ display: block;
141
+ margin-top: 0.35rem;
142
+ font-size: var(--sl-text-xs);
143
+ color: var(--sl-color-gray-3);
144
+ }
145
+ .health__hint {
146
+ margin: 1rem 0 0;
147
+ font-size: var(--sl-text-sm);
148
+ color: var(--sl-color-gray-2);
149
+ }
150
+ .health__failing {
151
+ margin-top: 1rem;
152
+ }
153
+ .health__failing > summary {
154
+ cursor: pointer;
155
+ color: #dc2626;
156
+ font-weight: 600;
157
+ }
158
+ .health__failing ul {
159
+ margin: 0.5rem 0 0;
160
+ padding-left: 1.1rem;
161
+ }
162
+ .health__failing-feature {
163
+ color: var(--sl-color-gray-3);
164
+ font-size: var(--sl-text-sm);
165
+ }
166
+ @media (max-width: 640px) {
167
+ .health__metrics {
168
+ grid-template-columns: repeat(2, minmax(0, 1fr));
169
+ }
170
+ }
171
+ </style>
@@ -0,0 +1,53 @@
1
+ ---
2
+ /**
3
+ * Starlight PageTitle override.
4
+ *
5
+ * Renders the default page title, then — if the page declares `verifiedBy` in
6
+ * its frontmatter — a live verification badge underneath it. Any page (ADR,
7
+ * runbook, hand-written prose) gets the badge for free, no component import.
8
+ */
9
+ import Default from '@astrojs/starlight/components/PageTitle.astro';
10
+ import { report } from '../lib/config';
11
+ import { hasScenarioId } from '../lib/verification';
12
+ import VerifiedBy from './VerifiedBy.astro';
13
+
14
+ const verifiedBy = Astro.locals.starlightRoute?.entry?.data?.verifiedBy;
15
+ const scenarioId = Astro.locals.starlightRoute?.entry?.data?.scenarioId;
16
+ const hasVerifiedBy = Object.prototype.hasOwnProperty.call(
17
+ Astro.locals.starlightRoute?.entry?.data ?? {},
18
+ "verifiedBy",
19
+ );
20
+ const staleScenarioNote =
21
+ typeof scenarioId === "string" && scenarioId.length > 0 && !hasScenarioId(report, scenarioId);
22
+ ---
23
+
24
+ <Default><slot /></Default>
25
+ {hasVerifiedBy && <VerifiedBy refs={verifiedBy ?? []} />}
26
+ {
27
+ staleScenarioNote && (
28
+ <p class="scenario-note-stale">
29
+ Stale note: scenario <code>{scenarioId}</code> is not present in the latest test run.
30
+ </p>
31
+ )
32
+ }
33
+
34
+ <style>
35
+ .scenario-note-stale {
36
+ margin: -0.75rem 0 1.75rem;
37
+ padding: 0.65rem 0.9rem;
38
+ border: 1px solid var(--es-line);
39
+ border-left: 3px solid var(--es-unverified);
40
+ border-radius: var(--es-radius);
41
+ background: var(--es-skipped-soft);
42
+ color: var(--es-text);
43
+ font-size: var(--sl-text-xs);
44
+ font-weight: 600;
45
+ }
46
+
47
+ .scenario-note-stale code {
48
+ background: var(--es-bg);
49
+ border: 1px solid var(--es-line);
50
+ border-radius: 0.25rem;
51
+ padding: 0.05rem 0.35rem;
52
+ }
53
+ </style>
@@ -0,0 +1,281 @@
1
+ ---
2
+ /**
3
+ * <VerifiedBy refs={["story-id", "tag", "TICKET-1"]} />
4
+ *
5
+ * Renders a live verification badge for a docs page. Each reference is resolved
6
+ * at build time against the latest test run (see src/lib/config.ts):
7
+ * a passing story shows "Verified", a failing one shows "Failing", and a
8
+ * reference that matches nothing shows "Unverified" — so a page can never
9
+ * quietly claim something the tests no longer prove.
10
+ *
11
+ * Every matched story is a link straight into the Scenario Explorer, so a
12
+ * reader can go from "verified by X" to X — its steps, docs and video — in one
13
+ * click. Unresolved references are called out by name so they get fixed.
14
+ *
15
+ * Use it directly in MDX, or set `verifiedBy:` in page frontmatter and let the
16
+ * PageTitle override render it automatically.
17
+ */
18
+ import { report, explorerUrl } from "../lib/config";
19
+ import {
20
+ resolveVerification,
21
+ presentStatus,
22
+ verificationAgeDays,
23
+ isVerificationStale,
24
+ } from "../lib/verification";
25
+ import type { ScenarioLike } from "../lib/verification";
26
+
27
+ interface Props {
28
+ refs: string | string[];
29
+ }
30
+
31
+ const { refs } = Astro.props;
32
+ const result = resolveVerification(refs, report);
33
+ const present = presentStatus(result);
34
+ const staleAfterDays = Number(import.meta.env.PUBLIC_VERIFICATION_STALE_DAYS ?? 14);
35
+ const ageDays = verificationAgeDays(result);
36
+ const ageWarning = Number.isFinite(staleAfterDays)
37
+ ? isVerificationStale(result, staleAfterDays)
38
+ : false;
39
+
40
+ // Flatten the per-reference matches into a single de-duplicated story list so
41
+ // the badge lists stories (the thing a reader cares about), not raw refs.
42
+ const matched: ScenarioLike[] = [];
43
+ const seen = new Set<string>();
44
+ for (const resolution of result.refs) {
45
+ for (const scenario of resolution.matched) {
46
+ if (seen.has(scenario.id)) continue;
47
+ seen.add(scenario.id);
48
+ matched.push(scenario);
49
+ }
50
+ }
51
+
52
+ const statusVerb: Record<string, string> = {
53
+ passed: "passed",
54
+ failed: "failed",
55
+ skipped: "skipped",
56
+ pending: "pending",
57
+ };
58
+
59
+ const lastVerified = result.lastVerifiedMs
60
+ ? new Date(result.lastVerifiedMs).toLocaleDateString(undefined, {
61
+ year: "numeric",
62
+ month: "short",
63
+ day: "numeric",
64
+ })
65
+ : null;
66
+ ---
67
+
68
+ <aside
69
+ class:list={["verified-by", `verified-by--${result.status}`]}
70
+ aria-label={`Verification status: ${present.label}`}
71
+ >
72
+ <div class="verified-by__head">
73
+ <span class="verified-by__icon" aria-hidden="true">{present.icon}</span>
74
+ <span class="verified-by__label">{present.label}</span>
75
+ <span class="verified-by__summary">{present.summary}</span>
76
+ {lastVerified && (
77
+ <span class="verified-by__meta">
78
+ verified {lastVerified}
79
+ {typeof ageDays === "number" ? ` · ${ageDays}d ago` : ""}
80
+ </span>
81
+ )}
82
+ </div>
83
+
84
+ {
85
+ ageWarning && (
86
+ <p class="verified-by__age-warning">
87
+ Verification is {ageDays} days old. Rerun the stories if freshness matters.
88
+ </p>
89
+ )
90
+ }
91
+
92
+ {
93
+ matched.length > 0 && (
94
+ <ul class="verified-by__stories">
95
+ {matched.map((scenario) => (
96
+ <li>
97
+ <a class:list={["vb-story", `vb-story--${scenario.status}`]} href={explorerUrl(scenario.id)}>
98
+ <span class="vb-story__dot" aria-hidden="true" />
99
+ <span class="vb-story__title">{scenario.title}</span>
100
+ <span class="vb-story__status">{statusVerb[scenario.status] ?? scenario.status}</span>
101
+ <span class="vb-story__go" aria-hidden="true">→</span>
102
+ </a>
103
+ </li>
104
+ ))}
105
+ </ul>
106
+ )
107
+ }
108
+
109
+ {
110
+ result.missingRefs.length > 0 && (
111
+ <p class="verified-by__missing">
112
+ No story matches{" "}
113
+ {result.missingRefs.map((ref, i) => (
114
+ <>
115
+ {i > 0 && ", "}
116
+ <code>{ref}</code>
117
+ </>
118
+ ))}
119
+ {" "}— check the reference or add a verifying story.
120
+ </p>
121
+ )
122
+ }
123
+ </aside>
124
+
125
+ <style>
126
+ .verified-by {
127
+ --vb-accent: var(--es-text-muted);
128
+ --vb-soft: var(--es-surface);
129
+ border: 1px solid var(--es-line);
130
+ border-left: 3px solid var(--vb-accent);
131
+ border-radius: var(--es-radius);
132
+ background: var(--vb-soft);
133
+ padding: 0.85rem 1.1rem;
134
+ margin: 1rem 0 1.75rem;
135
+ font-size: var(--sl-text-sm);
136
+ }
137
+
138
+ .verified-by--verified {
139
+ --vb-accent: var(--es-passed);
140
+ --vb-soft: var(--es-passed-soft);
141
+ }
142
+ .verified-by--failing {
143
+ --vb-accent: var(--es-failed);
144
+ --vb-soft: var(--es-failed-soft);
145
+ }
146
+ .verified-by--not-run {
147
+ --vb-accent: var(--es-skipped);
148
+ --vb-soft: var(--es-skipped-soft);
149
+ }
150
+ .verified-by--unverified {
151
+ --vb-accent: var(--es-unverified);
152
+ --vb-soft: var(--es-skipped-soft);
153
+ }
154
+
155
+ .verified-by__head {
156
+ display: flex;
157
+ align-items: baseline;
158
+ flex-wrap: wrap;
159
+ gap: 0.55rem;
160
+ }
161
+
162
+ .verified-by__icon {
163
+ color: var(--vb-accent);
164
+ font-weight: 700;
165
+ }
166
+
167
+ .verified-by__label {
168
+ font-weight: 700;
169
+ color: var(--vb-accent);
170
+ text-transform: uppercase;
171
+ letter-spacing: 0.06em;
172
+ font-size: var(--sl-text-xs);
173
+ }
174
+
175
+ .verified-by__summary {
176
+ color: var(--es-text);
177
+ }
178
+
179
+ .verified-by__meta {
180
+ margin-left: auto;
181
+ color: var(--es-text-muted);
182
+ font-size: var(--sl-text-xs);
183
+ white-space: nowrap;
184
+ }
185
+
186
+ .verified-by__stories {
187
+ list-style: none;
188
+ margin: 0.85rem 0 0;
189
+ padding: 0;
190
+ display: grid;
191
+ gap: 0.4rem;
192
+ }
193
+
194
+ .vb-story {
195
+ display: grid;
196
+ grid-template-columns: auto 1fr auto auto;
197
+ align-items: center;
198
+ gap: 0.6rem;
199
+ padding: 0.5rem 0.7rem;
200
+ border: 1px solid var(--es-line);
201
+ border-radius: var(--es-radius-sm);
202
+ background: var(--es-bg);
203
+ color: var(--es-text);
204
+ text-decoration: none;
205
+ transition: border-color 120ms ease, background-color 120ms ease, transform 120ms ease;
206
+ }
207
+
208
+ .vb-story:hover {
209
+ border-color: var(--vb-accent);
210
+ background: var(--vb-soft);
211
+ }
212
+
213
+ .vb-story:focus-visible {
214
+ outline: 2px solid var(--vb-accent);
215
+ outline-offset: 2px;
216
+ }
217
+
218
+ .vb-story:hover .vb-story__go {
219
+ transform: translateX(2px);
220
+ opacity: 1;
221
+ }
222
+
223
+ .vb-story__dot {
224
+ width: 0.5rem;
225
+ height: 0.5rem;
226
+ border-radius: 50%;
227
+ background: var(--vb-dot, var(--es-text-muted));
228
+ }
229
+ .vb-story--passed {
230
+ --vb-dot: var(--es-passed);
231
+ }
232
+ .vb-story--failed {
233
+ --vb-dot: var(--es-failed);
234
+ }
235
+ .vb-story--skipped {
236
+ --vb-dot: var(--es-skipped);
237
+ }
238
+ .vb-story--pending {
239
+ --vb-dot: var(--es-pending);
240
+ }
241
+
242
+ .vb-story__title {
243
+ font-weight: 550;
244
+ line-height: 1.3;
245
+ color: var(--es-text);
246
+ }
247
+
248
+ .vb-story__status {
249
+ font-family: var(--es-mono);
250
+ font-size: var(--sl-text-xs);
251
+ color: var(--vb-dot, var(--es-text-muted));
252
+ text-transform: lowercase;
253
+ }
254
+
255
+ .vb-story__go {
256
+ color: var(--es-text-muted);
257
+ opacity: 0.6;
258
+ transition: transform 120ms ease, opacity 120ms ease;
259
+ }
260
+
261
+ .verified-by__missing {
262
+ margin: 0.75rem 0 0;
263
+ color: var(--es-text);
264
+ font-size: var(--sl-text-xs);
265
+ }
266
+
267
+ .verified-by__age-warning {
268
+ margin: 0.75rem 0 0;
269
+ color: var(--es-text);
270
+ font-size: var(--sl-text-xs);
271
+ font-weight: 600;
272
+ }
273
+
274
+ .verified-by__missing code {
275
+ font-size: var(--sl-text-xs);
276
+ background: var(--es-bg);
277
+ border: 1px solid var(--es-line);
278
+ border-radius: 0.25rem;
279
+ padding: 0.05rem 0.35rem;
280
+ }
281
+ </style>
@@ -0,0 +1,91 @@
1
+ ---
2
+ /**
3
+ * <VerifiedStep story="health-check">Verify the /health endpoint returns 200</VerifiedStep>
4
+ *
5
+ * A runbook/checklist item whose trustworthiness is backed by a test. The `story`
6
+ * prop links the step to one or more story references; the box shows a live
7
+ * green check when the linked story passed in the last run, a red cross when it
8
+ * failed, and an empty box when there is no linked story (a manual step) or no
9
+ * matching story was found. Wrap a set of these in <Checklist>.
10
+ */
11
+ import { report } from "../lib/config";
12
+ import { resolveVerification } from "../lib/verification";
13
+
14
+ interface Props {
15
+ /** Story id, tag, ticket, or title that verifies this step. Omit for a manual step. */
16
+ story?: string | string[];
17
+ }
18
+
19
+ const { story } = Astro.props;
20
+ const result = story ? resolveVerification(story, report) : null;
21
+
22
+ const ICON: Record<string, string> = {
23
+ verified: "✓",
24
+ failing: "✕",
25
+ "not-run": "⏳",
26
+ unverified: "⚠",
27
+ };
28
+
29
+ const icon = result ? ICON[result.status] : "○";
30
+ const stateClass = result ? `verified-step--${result.status}` : "verified-step--manual";
31
+
32
+ const note = result
33
+ ? result.status === "verified"
34
+ ? "verified by a passing story"
35
+ : result.status === "failing"
36
+ ? "linked story is failing"
37
+ : result.status === "not-run"
38
+ ? "linked story did not run"
39
+ : "no matching story"
40
+ : "manual step";
41
+ ---
42
+
43
+ <li class:list={["verified-step", stateClass]}>
44
+ <span class="verified-step__box" aria-hidden="true">{icon}</span>
45
+ <span class="verified-step__body"><slot /></span>
46
+ <span class="verified-step__note" title={note}>{note}</span>
47
+ </li>
48
+
49
+ <style>
50
+ .verified-step {
51
+ list-style: none;
52
+ display: flex;
53
+ align-items: baseline;
54
+ gap: 0.6rem;
55
+ padding: 0.4rem 0;
56
+ border-bottom: 1px solid var(--sl-color-gray-6);
57
+ }
58
+ .verified-step__box {
59
+ flex: none;
60
+ width: 1.3rem;
61
+ height: 1.3rem;
62
+ display: inline-grid;
63
+ place-items: center;
64
+ border: 1px solid currentColor;
65
+ border-radius: 0.3rem;
66
+ font-size: 0.8rem;
67
+ font-weight: 700;
68
+ }
69
+ .verified-step__body {
70
+ flex: 1;
71
+ }
72
+ .verified-step__note {
73
+ flex: none;
74
+ font-size: var(--sl-text-xs);
75
+ color: var(--sl-color-gray-3);
76
+ white-space: nowrap;
77
+ }
78
+ .verified-step--verified .verified-step__box {
79
+ color: #16a34a;
80
+ }
81
+ .verified-step--failing .verified-step__box {
82
+ color: #dc2626;
83
+ }
84
+ .verified-step--not-run .verified-step__box,
85
+ .verified-step--unverified .verified-step__box {
86
+ color: #d97706;
87
+ }
88
+ .verified-step--manual .verified-step__box {
89
+ color: var(--sl-color-gray-3);
90
+ }
91
+ </style>
@@ -0,0 +1,45 @@
1
+ ---
2
+ title: 'ADR 0001 — Charge a 35 bps transfer fee'
3
+ description: 'Example decision record kept honest by executable stories.'
4
+ verifiedBy:
5
+ - transfer-fee
6
+ - send-money--charges-a-35-bps-fee
7
+ ---
8
+
9
+ import VerifiedBy from '../../../components/VerifiedBy.astro';
10
+
11
+ The badge directly under the title is rendered automatically from the
12
+ `verifiedBy` field in this page's frontmatter — it links this decision to the
13
+ stories that prove it still holds.
14
+
15
+ :::note
16
+ On a fresh scaffold the badge reads **Unverified**, because no test run has
17
+ populated `public/stories/story-report.json` yet. That is the point: the page
18
+ tells you it is unproven until a real run backs it up. Generate the report with
19
+ `executable-stories build-docs reports/raw-run.json --site-dir .`
20
+ and the badge turns green when the linked stories pass.
21
+ :::
22
+
23
+ ## Status
24
+
25
+ Accepted
26
+
27
+ ## Context
28
+
29
+ Cross-border transfers must cover FX spread and settlement cost. We need a fee
30
+ that is predictable for customers and simple to reason about.
31
+
32
+ ## Decision
33
+
34
+ Charge a flat **35 basis points** (0.35%) on the source amount of every
35
+ transfer.
36
+
37
+ ## Verification
38
+
39
+ This decision is only "true" while the stories that exercise it pass. You can
40
+ also drop a badge inline anywhere in the page:
41
+
42
+ <VerifiedBy refs={["transfer-fee"]} />
43
+
44
+ When the fee logic changes and a linked story fails, this badge turns red — the
45
+ decision record can no longer silently drift away from the code.
@@ -0,0 +1,41 @@
1
+ ---
2
+ title: Behavior portal
3
+ description: How proven behavior, commentary, and verification work together.
4
+ ---
5
+
6
+ The portal has one job: make behavior trustworthy for both engineers and stakeholders.
7
+
8
+ ## Three content tiers
9
+
10
+ - **Proven** lives in generated story pages and the Scenario Explorer. It comes from the latest run and is safe to regenerate at any time.
11
+ - **Commentary** lives in hand-written guides, ADRs, runbooks, incidents, and scenario notes. It explains why behavior matters.
12
+ - **Stale** is visible on purpose. If a commentary page links to failing or missing proof, the badge shows it.
13
+
14
+ ## Where to add context
15
+
16
+ Use a scenario note when the commentary belongs to one scenario:
17
+
18
+ ```bash
19
+ executable-stories new scenario-note "Checkout happy path" \
20
+ --scenario-id feature-checkout--happy-path
21
+ ```
22
+
23
+ Use ADRs, runbooks, and guides when the context spans multiple scenarios.
24
+
25
+ Once a scenario note exists, `build-docs` links to it automatically from every
26
+ surface that shows the scenario — the generated story page, the Scenario
27
+ Explorer, and the `/stories/` overview. You never add those links by hand, and
28
+ they appear or disappear as notes are added or removed.
29
+
30
+ ## What not to edit
31
+
32
+ Do not hand-edit `src/content/docs/stories/`. Those files are machine-owned and are replaced on each `build-docs` run.
33
+
34
+ ## Recommended CI flow
35
+
36
+ ```bash
37
+ executable-stories build-docs raw-run.json \
38
+ --site-dir ./story-docs \
39
+ --audience-split \
40
+ --baseline ./story-docs/public/stories/story-report.json
41
+ ```