Skip to content

feat: add how-to as flowrite's fourth document kind - #67

Merged
khajavi merged 4 commits into
zio:mainfrom
khajavi:feat/how-to-guide-kind
Aug 25, 2026
Merged

feat: add how-to as flowrite's fourth document kind#67
khajavi merged 4 commits into
zio:mainfrom
khajavi:feat/how-to-guide-kind

Conversation

@khajavi

@khajavi khajavi commented Aug 25, 2026

Copy link
Copy Markdown
Member

Closes audit gap §6 of WRITER-ASSISTANT-MIGRATION.md. flowrite wrote three of the four Diátaxis
genres; the missing one was the how-to guide — one task, one canonical path, a working result, for a
reader who already knows the library.

Ask for it in words like any other kind:

bash fixtures/tinyproject/scripts/run-how-to-guide.sh "compose two optics over one field"

What the port cost, versus what the audit estimated

The audit said "one KINDS row plus a structure and a checklist skill — the table was built for
exactly this." That was right about the architecture and undercounted the prose.

No phase, no delegation, no schema, no new entry point, and no subagent changed: designer and
drafter read structureBlock(docKind()) at their own render, so both picked up the new template
untouched. tsc forces more than expected — a missing row fails at the KINDS[kind] index as well as
at the two Record<DocKind, string> maps, so the wiring cannot be half-done.

What tsc cannot reach is every place three kinds had been written out in text, and that is where the
work was: GATE_INSTRUCTIONS, set_document_kind's subject description, two subagent descriptions
(the roster text a delegating model reads), examples-builder.md's enumerations, and the fixture's
AGENTS.md. A fifth kind will cost the same three files plus the same hand-search.

Three findings that changed the design

1. The tutorial gate bullet already claimed the how-to's territory. It read "a learning-oriented
walkthrough of a task or topic" — "of a task" is the how-to's definition, so a fourth bullet alone
would have left tutorial a superset absorbing every how-to request. Both guide bullets now name the
reader's intent (UNDERSTAND vs FINISH), and the worked ambiguity example moved from
data-type/tutorial to tutorial/how-to: the easy pair gave its example to the hard one.

This matters because misclassification is silent end to end. run-telemetry.ts is kind-agnostic
by construction and keys on the docs/ path segment, which is guides for both guide kinds; the
reviewer is handed whichever checklist the gate chose. A how-to misfiled as a tutorial is reviewed
against the tutorial checklist, passes, and files verdict: passed.

Worse, the log is prefixed with the label of the kind the gate classified, not the one the launcher
asked for — so archive-docs.sh's greps all miss and it produces an artifact-free archive whose
summary reads like a clean run. Added an else branch that says so.

2. The "before" block cannot be a plain ```scala fence. Both source trees fence it that way, on the
reasoning that painful code need not compile. mdoc-conventions:10 forbids exactly that and `:41`
sweeps for it, so the drafter would add a modifier, hit the compile error, and obey "fix the example"
by rewriting the pain away — silently, with review passing.

The premise was wrong: "what you write without this library" is ordinarily verbose working code.
So the before block is mdoc:compile-only real code that does not touch the documented library. It
compiles, keeps the contrast, and sidesteps fact-checker.md:16's character-by-character scrutiny of
uncompiled fences — which would otherwise have made a correct page fail its own run on a blocking
not-in-source drift. Plain fences survive for pseudocode only. fact-checker.md deliberately
not edited: that failure is loud, and the rule is to wrap only what has been watched to fail.

3. BACKLOG.md finding 3's diagnosis is wrong. It blames the templates' numbered lists for a run
emitting ## 6. Concept 4: Bounded Windows, but ## 1. was complianttutorial-structure:46
mandates it. What leaked was the template's placeholder label (Concept sections (3-6, …)
Concept 4:), which is rule 27's category but not among the terms rule 27 enumerates, so no reviewer
item covered it. The new template emits literal headings and its checklist forbids template vocabulary
and ordinals in headings. Left open for the other three kinds — fixing it there changes the drafting
prompt for every existing kind.

Tests

105 pass (was 102), tsc clean. Five existing tests loop DOC_KINDS and cover the new kind for free.
Three hardcoded tests now derive from DOC_KINDS — most importantly pairwise-distinctness, which
existed to catch "one kind's map entry pointing at another kind's import" and, as three named
assertions, covered nothing a fourth kind added.

Three new assertions close silent gaps:

  • GATE_INSTRUCTIONS names every DOC_KINDS member — the one enumeration with no type behind it.
  • Every mounted skill has a non-empty name; a SKILL.md missing name: loads as undefined, and
    the duplicate-skill test then compares undefined to undefined and passes.
  • Each of the four launchers archives under its own row's label. The pin above it was titled "labels
    match what archive-docs.sh greps for" while opening no script. Verified by breaking a label and
    watching it fail.

Not verified — and this is the honest part

No run has produced a page. The key is exhausted until 2026-09-01. BACKLOG.md finding 11 lists
the four unproven behaviours in priority order; the cheapest is one turn — start the launcher, Ctrl-C
after the classification line (the INT trap archives).

Two fixture limits bound whatever that run says: tinyproject has no boilerplate to suffer, so the
before-block has nothing real to be painful about and the drafter will invent it — the one thing
fact-check can neither confirm nor deny. And don't run it on Lens; docs/guides/lens.md is committed
and already how-to-flavoured, so it overwrites a page instead of exercising the empty-start path.

Also here

  • plainTools: [] — a how-to documents real API, but only what its one task needs, so
    check_method_coverage would report a large miss on a correct page.
  • :showLineNumbers vs :show-line-numbers is in the tree 1-to-4 and neither form is proven (no
    archive contains a built embed — finding 2). Filed, not fixed; a majority vote is not evidence.
  • The substantive how-to doctrine came from plugins/documentation/skills/docs-how-to-guide/, not from
    writer-assistant, whose 30-line copy sat behind a verify.ts that told the model to read a
    checklist that never existed in that tree. Relevant to the remaining audit gaps: a
    writer-assistant file is not evidence the behaviour it describes ever ran.
  • package.json gains a typecheck script. npm cannot run it, or test, because devEngines
    requires pnpm; the binaries work directly, which is what every verification step uses.

A how-to guide is the fourth Diátaxis genre and the one flowrite could not write:
goal-oriented, for a reader who already knows the library and wants one job done.
Ported from writer-assistant audit gap §6 — though the substantive material came
from plugins/documentation/skills/docs-how-to-guide/, not from writer-assistant,
whose copy is a 30-line summary and whose verify phase pointed at a checklist that
never existed in that tree.

The distinguishing section is "The Problem", and its "before" example needed a
decision the sources got wrong. Both fence it as plain ```scala on the reasoning
that painful code need not compile. mdoc-conventions:10 forbids exactly that, and
:41 sweeps for it — so the drafter would add a modifier, watch the compile fail,
and then obey "fix the example" by rewriting the pain away. The contrast the
section exists for would vanish silently.

So the before block is mdoc:compile-only real code that does not touch the
documented library. That is what "before" means, it compiles because verbose
working code compiles, and fact-checker.md exempts compiled blocks from the
character-by-character signature scrutiny it applies to plain fences. The plain
fence survives only for pseudocode, where the problem is architectural.

The template also emits literal heading text and forbids its own vocabulary in
headings. BACKLOG finding 3 blames numbered lists for `## 6. Concept 4: Bounded
Windows`, but `## 1.` was mandated by tutorial-structure:46 — what leaked was the
template's placeholder label. The checklist carries that rule, because the
reviewer has nothing else mounted.
…o type behind them

The table absorbed the fourth kind as designed — one row, two map entries, no new
code path, no change to any subagent. tsc forces more of it than expected: a
missing row fails at `KINDS[kind]` in agent.ts and in agent.test.ts, not just at
the two `Record<DocKind, string>` maps. So the wiring cannot be half-done.

The prose around the table is where three kinds was baked in, and none of it is
checked. The tutorial gate bullet read "a learning-oriented walkthrough of a task
or topic" — "of a task" IS the how-to's definition, so a fourth bullet alone would
have left tutorial a superset that absorbs every how-to request. Both bullets now
name the reader's intent (understand vs finish), with the discriminator stated and
one ✅/❌ pair. The worked ambiguity example moves from data-type/tutorial to
tutorial/how-to: the pair that shares a directory needs it more than the pair that
does not. set_document_kind's subject description is amended too — the classifying
model reads it on the same turn.

A misclassification is silent. run-telemetry.ts is kind-agnostic by construction
and keys on the docs/ path segment, which is `guides` for both kinds; the reviewer
gets whichever checklist the gate chose. So a misfiled how-to is reviewed against
the tutorial checklist, passes, and files verdict: passed. Hence the care with the
prose rather than a validator.

Two new assertions cover what nothing covered:
- GATE_INSTRUCTIONS names every DOC_KINDS member. Presence only; whether the
  discriminator is any good needs a live run.
- Every mounted skill has a non-empty name. A SKILL.md missing `name:` loads as
  undefined, and the duplicate-skill test then compares undefined to undefined and
  passes.

The pairwise-distinctness test becomes a loop over pairs. As three named
assertions it covered nothing a fourth kind added — which is exactly how it stood
when this kind arrived. withKind now takes DocKind, so it never needs this edit
again.
…tutorial

None of this is per-kind wiring — the table needed none. It is the text around the
table that was written when three kinds was the whole world, and the two pieces
that matter most are role DESCRIPTIONS rather than role instructions:
examples_builder advertised itself as "for a tutorial (one per concept)" and
docs_integrator as "Integrates a finished tutorial". That is the roster text the
model reads when choosing whom to delegate to, and BACKLOG finding 2 records the
examples phase having silently never run — a role that describes itself as
tutorial-only is a plausible way for a how-to run to skip it. Both are now kind-
neutral; the other five already were.

examples-builder.md keeps `<tutorial-id>` as its placeholder (nothing keys on the
word, and the file is a working prompt) but loses two enumerations that read as
exhaustive and excluded how-to, one of which offered "a tutorial's Putting It
Together, or a reference page's Running the Examples" — a how-to has both, so a
literal reading gave it neither.

fixtures/tinyproject/AGENTS.md told every run that docs/guides/ is where tutorials
go. That file is workspace-discovered into the writer's prompt, so a how-to run
read it as instruction. It now names both guide kinds and says to list the
directory first, since a shared directory means an id collision overwrites
somebody else's page and nothing checks.

archive-docs.sh gains the else branch on an unmatched label. The log is prefixed
with the label of the kind the GATE classified, not the one the launcher asked
for, so a misclassified run makes every grep miss — no token-usage.json, no
verdict, no insights, no run report, and a closing summary indistinguishable from
success. An operator would diagnose a crash.

The launcher's header says to name a task rather than a subject, and records that
Ctrl-C after the classification line is the cheap check on the tutorial/how-to
boundary — the INT trap already archives, so that needs no new code.

package.json gains a typecheck script. Note npm cannot run it, or `test`, because
devEngines demands pnpm; it names the canonical command, and the binary is what
actually gets run.

The new test reads all four launchers and asserts each archives under its own
row's label. The pin above it was titled "labels match what archive-docs.sh greps
for" while opening no script; this is that assertion. Verified by breaking the
label and watching it fail.
The audit's estimate for this gap — "one KINDS row plus a structure and a
checklist skill" — was right about the architecture and undercounted the prose,
and that asymmetry is the finding worth keeping. tsc forces the row, both
Record<DocKind, string> maps and the KINDS[kind] index; it cannot reach
GATE_INSTRUCTIONS, a schema description, two subagent descriptions,
examples-builder.md's enumerations, or the fixture's AGENTS.md. A fifth kind will
cost the same three files plus the same hand-search.

The README's "how a kind was added" recipe was stale in a way that would have sent
an implementer to modules that no longer exist (research.ts, design-doc-plan.ts,
write-doc.ts — deleted with the other phase tools). It now records what this
change actually cost, including which half tsc covers.

BACKLOG finding 11 lists the four unproven genre behaviours in priority order,
with the fixture limits that bound whatever a run says: tinyproject has no
boilerplate to suffer, so the before-block has nothing real to be painful about
and the drafter will invent it — the one thing fact-check can neither confirm nor
deny. A passing run proves the plumbing, not the genre.

Five observations recorded rather than fixed. The one most likely to bite someone
else: finding 3's diagnosis is wrong. It blames numbered lists for `## 6. Concept
4: Bounded Windows`, but `## 1.` was mandated by tutorial-structure:46 — what
leaked was the template's placeholder label, which rule 27 does not enumerate and
no checklist item covered. Also filed: :showLineNumbers vs :show-line-numbers with
neither form empirically proven, and that a writer-assistant file is not evidence
the behaviour it describes ever ran — its how-to verify phase read a checklist
that never existed in that tree.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant