executable-stories-formatters 0.15.0 → 0.16.0
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.
- package/dist/cli.js +238 -64
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +197 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +197 -55
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/schemas/README.md +1 -1
- package/templates/astro-starlight/astro.config.mjs +57 -0
- package/templates/astro-starlight/gitignore +14 -0
- package/templates/astro-starlight/package.json +20 -0
- package/templates/astro-starlight/public/stories/assets/.gitkeep +0 -0
- package/templates/astro-starlight/public/stories/notes-index.json +4 -0
- package/templates/astro-starlight/public/stories/story-report.json +17 -0
- package/templates/astro-starlight/src/components/ApiOperations.astro +366 -0
- package/templates/astro-starlight/src/components/Checklist.astro +15 -0
- package/templates/astro-starlight/src/components/HealthDashboard.astro +171 -0
- package/templates/astro-starlight/src/components/PageTitle.astro +53 -0
- package/templates/astro-starlight/src/components/VerifiedBy.astro +281 -0
- package/templates/astro-starlight/src/components/VerifiedStep.astro +91 -0
- package/templates/astro-starlight/src/content/docs/examples/example-adr.mdx +45 -0
- package/templates/astro-starlight/src/content/docs/guides/behavior-portal.mdx +41 -0
- package/templates/astro-starlight/src/content/docs/guides/writing-docs.mdx +49 -0
- package/templates/astro-starlight/src/content/docs/index.mdx +49 -0
- package/templates/astro-starlight/src/content/docs/stories/.gitkeep +0 -0
- package/templates/astro-starlight/src/content.config.ts +18 -0
- package/templates/astro-starlight/src/lib/config.ts +50 -0
- package/templates/astro-starlight/src/lib/render-doc-entry.ts +154 -0
- package/templates/astro-starlight/src/lib/report-health.ts +61 -0
- package/templates/astro-starlight/src/lib/verification.ts +247 -0
- package/templates/astro-starlight/src/pages/explorer/explorer.css +729 -0
- package/templates/astro-starlight/src/pages/explorer/index.astro +404 -0
- package/templates/astro-starlight/src/styles/global.css +293 -0
- package/templates/astro-starlight/src/styles/themes/corporate.css +83 -0
- package/templates/astro-starlight/src/styles/themes/dashboard.css +76 -0
- package/templates/astro-starlight/src/styles/themes/default.css +86 -0
- package/templates/astro-starlight/src/styles/themes/minimal.css +87 -0
- package/templates/astro-starlight/src/styles/themes/playful.css +77 -0
- package/templates/astro-starlight/src/styles/themes/terminal.css +77 -0
- package/templates/astro-starlight/tsconfig.json +13 -0
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Writing hand-written docs
|
|
3
|
+
description: How prose pages live alongside generated story docs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
This page is hand-written — proof that any author can add prose to the same site
|
|
7
|
+
as the generated story docs, with the same navigation, search, and theming.
|
|
8
|
+
|
|
9
|
+
## Where pages live
|
|
10
|
+
|
|
11
|
+
- **Generated** story pages are written to `src/content/docs/stories/` by
|
|
12
|
+
`executable-stories build-docs reports/raw-run.json --site-dir .`. Don't edit
|
|
13
|
+
these by hand — they are regenerated from tests.
|
|
14
|
+
- **Hand-written** pages (guides, ADRs, runbooks, incidents, API docs) live in
|
|
15
|
+
their own folders under `src/content/docs/`. They are never overwritten by a
|
|
16
|
+
regenerate.
|
|
17
|
+
- **Scenario notes** live in `src/content/docs/notes/` and add stakeholder
|
|
18
|
+
context next to a specific scenario without editing the generated page. After
|
|
19
|
+
`build-docs`, the matching story page, the Explorer, and the `/stories/`
|
|
20
|
+
overview each link to the note automatically — no manual cross-linking.
|
|
21
|
+
|
|
22
|
+
Both kinds share one sidebar, one search index, and one set of themes.
|
|
23
|
+
|
|
24
|
+
If you scaffolded the site with `init-astro`, the template `.gitignore` already
|
|
25
|
+
ignores the generated machine zone (`stories/` and `public/stories/*`) so git
|
|
26
|
+
tracks your hand-written content by default, not the latest test run output.
|
|
27
|
+
|
|
28
|
+
## Keep prose honest, too
|
|
29
|
+
|
|
30
|
+
Hand-written pages can link to the tests that back them. Add `verifiedBy` to the
|
|
31
|
+
frontmatter and a live badge appears under the title:
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
---
|
|
35
|
+
title: How transfers are priced
|
|
36
|
+
verifiedBy: [pricing, transfer-fee]
|
|
37
|
+
---
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If a linked story starts failing, the badge turns red — so even your prose
|
|
41
|
+
can't quietly drift away from the code.
|
|
42
|
+
|
|
43
|
+
## Start from a template
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
executable-stories new adr "Cap combined discount at 30%"
|
|
47
|
+
executable-stories new runbook "Restart the payments service"
|
|
48
|
+
executable-stories new scenario-note "Checkout happy path" --scenario-id feature-checkout--happy-path
|
|
49
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Story Docs
|
|
3
|
+
description: Living documentation generated from your executable stories.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
import HealthDashboard from '../../components/HealthDashboard.astro';
|
|
7
|
+
|
|
8
|
+
<HealthDashboard />
|
|
9
|
+
|
|
10
|
+
Living documentation for your whole team — product owners, testers, and
|
|
11
|
+
engineers — generated from your executable stories and kept honest by the tests
|
|
12
|
+
themselves. Works the same whether your stories are written in TypeScript,
|
|
13
|
+
Python, Go, Ruby, Java/Kotlin, Rust, or C#: it all flows through one report.
|
|
14
|
+
|
|
15
|
+
## Trust model
|
|
16
|
+
|
|
17
|
+
- **Proven** — generated story pages and Explorer data come from the latest test run.
|
|
18
|
+
- **Commentary** — guides, ADRs, runbooks, incidents, and notes are hand-written context.
|
|
19
|
+
- **Linked** — add `verifiedBy` to commentary pages to show which stories still prove them.
|
|
20
|
+
- **Stale** — failing or missing proof shows up as a badge; it is surfaced, not hidden.
|
|
21
|
+
|
|
22
|
+
## What you can do here
|
|
23
|
+
|
|
24
|
+
- **Browse stories** — the [Scenario Explorer](/explorer/) is a searchable, filterable
|
|
25
|
+
view of every executable story.
|
|
26
|
+
- **Verify any page** — add `verifiedBy: [story-id]` to a page's frontmatter and a
|
|
27
|
+
live badge shows whether the tests that prove it currently pass.
|
|
28
|
+
- **Start from a template** — `executable-stories new adr "…"` (also `runbook`,
|
|
29
|
+
`decision-log`, `incident`, `scenario-note`), each pre-wired to verifying stories.
|
|
30
|
+
- **Document your API** — `executable-stories import-openapi spec.json --run story-report.json`
|
|
31
|
+
generates endpoint pages with a coverage badge per route.
|
|
32
|
+
- **Keep links honest** — `executable-stories check-links src/content/docs` fails CI
|
|
33
|
+
on broken links.
|
|
34
|
+
|
|
35
|
+
## Getting started
|
|
36
|
+
|
|
37
|
+
Generate the behavior portal from any test run:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# run your tests first (StoryReporter writes reports/raw-run.json), then:
|
|
41
|
+
executable-stories build-docs reports/raw-run.json --site-dir .
|
|
42
|
+
# add --audience-split to split pages into business vs technical URLs
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then start the dev server:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm run dev
|
|
49
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineCollection } from 'astro:content';
|
|
2
|
+
import { z } from 'astro:content';
|
|
3
|
+
import { docsLoader } from '@astrojs/starlight/loaders';
|
|
4
|
+
import { docsSchema } from '@astrojs/starlight/schema';
|
|
5
|
+
|
|
6
|
+
export const collections = {
|
|
7
|
+
docs: defineCollection({
|
|
8
|
+
loader: docsLoader(),
|
|
9
|
+
// `verifiedBy` links a page to the stories that prove it is still true.
|
|
10
|
+
// The PageTitle override renders a live verification badge from it.
|
|
11
|
+
schema: docsSchema({
|
|
12
|
+
extend: z.object({
|
|
13
|
+
verifiedBy: z.union([z.string(), z.array(z.string())]).optional(),
|
|
14
|
+
scenarioId: z.string().optional(),
|
|
15
|
+
}),
|
|
16
|
+
}),
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site configuration — the one place to point at your test-run data.
|
|
3
|
+
*
|
|
4
|
+
* The verification badges, runbook checklists, and health dashboard read the
|
|
5
|
+
* latest run at build time (`report`); the Scenario Explorer fetches the same
|
|
6
|
+
* file in the browser (`REPORT_URL`). If you move the report, change it here —
|
|
7
|
+
* nowhere else references the path.
|
|
8
|
+
*
|
|
9
|
+
* Generate the report with (writes public/stories/story-report.json for you):
|
|
10
|
+
* executable-stories build-docs reports/raw-run.json --site-dir .
|
|
11
|
+
*
|
|
12
|
+
* `report` (build-time import) and `REPORT_URL` (runtime fetch) must point at
|
|
13
|
+
* the same file — one is a filesystem path, the other the URL it's served at.
|
|
14
|
+
*/
|
|
15
|
+
import reportData from "../../public/stories/story-report.json";
|
|
16
|
+
import type { StoryReportLike } from "./verification";
|
|
17
|
+
|
|
18
|
+
/** The latest test run, imported at build time (badges, checklist, dashboard). */
|
|
19
|
+
export const report = reportData as StoryReportLike;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Public URL the Scenario Explorer fetches the run from at runtime.
|
|
23
|
+
* Override per-environment with `PUBLIC_STORY_REPORT_URL` in a `.env` file.
|
|
24
|
+
*/
|
|
25
|
+
export const REPORT_URL: string =
|
|
26
|
+
import.meta.env.PUBLIC_STORY_REPORT_URL ?? "/stories/story-report.json";
|
|
27
|
+
|
|
28
|
+
/** Public URL the Explorer fetches the scenario-note index from at runtime. */
|
|
29
|
+
export const NOTES_INDEX_URL: string =
|
|
30
|
+
import.meta.env.PUBLIC_NOTES_INDEX_URL ?? "/stories/notes-index.json";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Base URL of the source repository (e.g. https://github.com/acme/app/blob/main).
|
|
34
|
+
* When set, the Scenario Explorer turns each scenario's file path into a link to
|
|
35
|
+
* the test on your git host. Leave unset to fall back to an in-page anchor.
|
|
36
|
+
* Override with `PUBLIC_SOURCE_BASE_URL` in a `.env` file.
|
|
37
|
+
*/
|
|
38
|
+
export const SOURCE_BASE_URL: string =
|
|
39
|
+
import.meta.env.PUBLIC_SOURCE_BASE_URL ?? "";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Deep-link to a single story inside the Scenario Explorer. Used by the
|
|
43
|
+
* verification badge and the API coverage table so "verified by X" always
|
|
44
|
+
* takes you to X. Respects Astro's configured `base`.
|
|
45
|
+
*/
|
|
46
|
+
export function explorerUrl(storyId: string): string {
|
|
47
|
+
const base = import.meta.env.BASE_URL ?? "/";
|
|
48
|
+
const prefix = base.endsWith("/") ? base : `${base}/`;
|
|
49
|
+
return `${prefix}explorer/#${encodeURIComponent(storyId)}`;
|
|
50
|
+
}
|