v1.15.0 ·
The link that spent three weeks as literal text
The problem
The website has one tutorial. getting-started is 64 lines: install, pick a template, run a
session. It ends at the first instance and there is nothing after it.
Everything past that first session is only findable in reference docs — /docs/cli,
/docs/skills, /docs/phases, /docs/guidance. Reference docs answer questions you already
know how to ask. A reader who just finished their first session does not know that cost
tracking exists, that the knowledge graph means the assistant stops re-reading the same code
every session, that review gates are adversarial on purpose, or that two people can share a
repo without colliding. Those are the reasons to adopt this framework, and today they are
invisible.
There is a second, quieter problem. The reference docs teach what things are. Nobody anywhere teaches why the shape is like this — why an artifact is a file on disk instead of chat history, why the status of a roadmap item is its directory and not a field, why a claim exists at all. A reader who does not understand the shape will fight it.
How it could be solved
Option A — Long markdown page, no interactivity
Write the deep dive as one more .md file. Cheapest possible.
Rejected. It fails the primary goal by construction: the reader does nothing, so they get recognition rather than recall, and a page this long with no segmenting is abandoned. It also fails Diátaxis rule 1 outright. This is the "documentation with numbered headings" outcome.
Option B — Chaptered markdown plus embedded widgets via a placeholder directive (chosen)
Extend the tutorial format with one new directive, mirroring the {{ref:...}} convention that
already exists:
{{widget:simulator/cost-attribution}}
Content stays plain markdown in packages/content. The website resolves each directive id
against a registry of client components and canned data held in packages/website/src/data/.
Add a series field to tutorial frontmatter so chapters group, and render a series index.
Three widget kinds, one per rung of the fade from worked example to recall:
- Transcript — a stepped, real recorded run with the resulting file beside it. Generalises
LifecycleSimulatorinto a reusable component. This is the worked example. - Fill-in — the same transcript with one command or one field blanked, reader supplies it from a small set of choices. This is the completion problem.
- Checkpoint — a question about consequence with the answer hidden until the reader commits, and feedback that explains why the common wrong answer is tempting. This is retrieval practice.
Trade-offs: adds one directive to a stable content parser, and the widget data has to be kept truthful. Both are bounded, and the drift test addresses the second. Roughly a week.
Option C — Fake terminal accepting real commands
A prompt the reader types into, matching input against recorded runs.
Rejected. It is the option whose failure mode is lying: any command outside the recorded set either errors when the real CLI would succeed, or worse, succeeds when the real CLI would fail. It also inverts the load-theory finding — free-form input is unguided practice, which measured worst of the three conditions for novices. Much more code for a worse teaching result.
Option D — Browser-persisted mock project
The roadmap item's ambitious end: a simulated .aidlc/ the reader mutates across all chapters.
Rejected for this instance. It is a small application, its state model must track the real one or it teaches something false, and the roadmap item's own recommendation says to leave it alone. If chapter completion data shows readers finishing and asking for more, it comes back as a separate item.
How AIDLC solves it
deep-dive-tutorial
Two surfaces, one push.
The site — Amplify builds from main, so pushing publishes:
/tutorials/deep-dive/— the series page, with locally-remembered progress/tutorials/deep-dive-*/— eight chapters/tutorials/— now lists the series as one entry rather than nine flat pages/tutorials/getting-started/— its{{ref:...}}directives now render as links. That page has been serving readers the literal strings{{ref:template/full-feature}}and{{ref:skill/aidlc-ideation}}since it was written. This release is the first time it reads correctly.
The packages — @rasensio/aidlc-content carries the eight chapter files and the
format change; @rasensio/aidlc picks up the content version. No CLI command, template,
gate or action changed.
lead-time-reads-as-zero
packages/website/src/components/blog/ReleaseCard.tsx
export function formatDuration(seconds: number | null): string {
if (seconds === null) return 'not recorded';
const minutes = Math.round(seconds / 60);
if (minutes < 24 * 60) return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
const hours = Math.round(seconds / 3600);
return `${Math.floor(hours / 24)}d ${hours % 24}h`;
}
Two units, always: <h>h <m>m below a day, <d>d <h>h at or above one.
Three properties, each with a reason:
- The branch is chosen from the rounded minutes, not the raw seconds. So the
24-hour boundary belongs to exactly one form. The old code compared rounded
hours to 24, which let 23h 59m 30s round up to 24 and then print
1d 0hfrom the day branch — right answer, reached by accident. 23h 59m 00s now reads23h 59mand 23h 59m 59s reads1d 0h(AC-5). - The smaller unit is rounded, then carried by the
floor/%pair.1h 60mand2d 24hare unreachable rather than merely unlikely (AC-6). - The day branch is byte-identical to the old code. Verified over every value from 24h to 30 days, not just the six published ones (AC-3).
packages/website/src/test/blog-release-card.test.ts (new)
26 tests. formatDuration had none — that is why 0h shipped with the card
and survived 27 posts. Uses the ten real wrong values as fixtures, because a
formatter is exactly the code where a synthetic 3600 passes and the actual data
does not.
formatDuration is now exported for this. Same convention as fmtAge in
packages/cli/src/commands/cost.ts.
github-action-majors
Three action majors, in all four places they appear
| Action | Was | Now | Runtime |
|---|---|---|---|
actions/checkout | v4 | v7 | node24 |
actions/setup-node | v4 | v7 | node24 |
pnpm/action-setup | v4 | v6 | node24 |
.github/workflows/ci.yml and .github/workflows/release.yml were the two the
item named. The other two are both in ci/aidlc-gate.yml, which is not our CI:
it is inlined into packages/content/tutorials/getting-started.md at build time
and shipped in @rasensio/aidlc-content, so users copy it into their own repos.
Leaving it at v4 would ship the defect outward. Its commented-out
actions/github-script@v7 went to v9 for the same reason.
The two settings this repo has already been burned by
Both are unchanged, and now carry the reason inline rather than in an instance record:
fetch-depth: 0still means unlimited in checkout v7. It was checkout's default — a tagless depth-1 clone — that failed 14 blog and release tests inrelease.yml, not the input. Verified against v7's behaviour by running the suite that readsgit tag/git log.cache: pnpmstays explicit and must. setup-node v5 added automatic caching from thepackageManagerfield, and v6 narrowed that automatic path to npm only — so an implicit pnpm cache is not something to rely on.cachestill acceptsnpm | yarn | pnpmin v7 (confirmed from v7'saction.yml). The step also still runs afterpnpm/action-setup, because resolving the pnpm store path needs pnpm onPATH.
One assertion, generalising the one next to it
packages/website/src/test/ci-workflow.test.ts already asserted that ci.yml and
release.yml pin the same Node major (release-gate-runs-too-late/AC-7). It did
not assert the same of the action versions — which is exactly what diverging here
would look like. The new test parses both workflows and compares the pinned
version of every action they share, so a bump that lands in one file and not the
other fails on the pull request instead of at the next release.
It reads parsed YAML, not file text, like every other assertion in that file.