<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Kemal Deniz Teket</title>
    <description>The latest articles on Forem by Kemal Deniz Teket (@kadetr).</description>
    <link>https://forem.com/kadetr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3877354%2Fa42364a2-0771-407c-82d9-16dab6185bab.png</url>
      <title>Forem: Kemal Deniz Teket</title>
      <link>https://forem.com/kadetr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/kadetr"/>
    <language>en</language>
    <item>
      <title>Formalized, Reviewed, Triaged — A Practitioner's Account, Part II</title>
      <dc:creator>Kemal Deniz Teket</dc:creator>
      <pubDate>Sun, 03 May 2026 18:00:21 +0000</pubDate>
      <link>https://forem.com/kadetr/formalized-reviewed-triaged-a-practitioners-account-part-ii-gk1</link>
      <guid>https://forem.com/kadetr/formalized-reviewed-triaged-a-practitioners-account-part-ii-gk1</guid>
      <description>&lt;h2&gt;
  
  
  §0 — Hook
&lt;/h2&gt;

&lt;p&gt;The work-pool schema that runs the paragraf project names three work types:&lt;br&gt;
&lt;code&gt;spec&lt;/code&gt;, &lt;code&gt;package&lt;/code&gt;, and &lt;code&gt;issue-bucket&lt;/code&gt;. Only two of the three have a defined&lt;br&gt;
process. The title word is earned — the findings were triaged. The process that &lt;br&gt;
did the triaging is not yet written down.&lt;/p&gt;

&lt;p&gt;The first article introduced a methodology that produced a working library —&lt;br&gt;
four layers, twelve packages, over thirty thousand lines of code — in two weeks.&lt;br&gt;
It also described the gaps that needed to close.&lt;/p&gt;

&lt;p&gt;Two parallel improvements happened in the one week that followed. The first was&lt;br&gt;
formalization: the practice that lived in one operator's head became a document&lt;br&gt;
set, a machine-consumable instruction set, a work registry with an explicit state&lt;br&gt;
machine, four context files per package, a decision lifecycle, archive procedures.&lt;br&gt;
The methodology stopped being a discipline and became an apparatus — a structured&lt;br&gt;
set of documents and protocols that an LLM can read and follow without the operator&lt;br&gt;
present for every step.&lt;/p&gt;

&lt;p&gt;The second improvement was a sprint. Two new color-related packages shipped under&lt;br&gt;
the formalized process, then several review passes returned more than a hundred&lt;br&gt;
findings. None broke the build. None suggested the methodology had failed. They&lt;br&gt;
were the kind of findings that only appear when a codebase is finished enough to&lt;br&gt;
be read back to its author by an external instrument.&lt;/p&gt;

&lt;p&gt;These are not two separate stories. The methodology was formalized to handle&lt;br&gt;
forward work properly. The formalization surfaced its own boundary. The findings&lt;br&gt;
sprint ran in that boundary informally. Only one of the three work types — the one&lt;br&gt;
that drove an entire week of review — has no documented process. That gap is not&lt;br&gt;
an oversight. It is the thing the formalization revealed about itself.&lt;/p&gt;


&lt;h2&gt;
  
  
  §1 — What Got Formalized
&lt;/h2&gt;

&lt;p&gt;In article-1, the methodology was a practice. One week later, it is a document&lt;br&gt;
set with specific roles at specific phase boundaries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docs/
├── methodology.md              # phases, gates, ask-human triggers
├── methodology-reference.md    # archive procedures, anti-patterns
├── outer-context.md            # project-level consistency checker
├── work-pool.md                # work registry + state machine
├── glossary.md                 # defined terms, hierarchy summary
├── dependency.md               # project-level dependency map
├── io-schemas.md               # project-level I/O navigation
├── roadmap.md                  # strategy and milestones
├── AI-PRIMER.md                # minimal session bootstrap
│
├── inner-context/[package]/
│   ├── inner-context.md        # role, constraints, package rules
│   ├── io-schema.md            # public types, exported functions
│   ├── dependency.md           # package imports
│   └── decisions.md            # active draft decisions only
│
├── plan/
│   └── workId-package-spec-plan-[YYMMDD-HHMM].md
│
└── archive/
    ├── plan/done/
    ├── plan/cancelled/
    └── decision/[package]-decisions-archive.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each document has one role. &lt;code&gt;methodology.md&lt;/code&gt; is the instruction set — four phases&lt;br&gt;
(Define, Specify, Implement, Revise), a list of Ask-Human triggers, consistency&lt;br&gt;
controls that produce visible output at every phase boundary. &lt;code&gt;outer-context.md&lt;/code&gt;&lt;br&gt;
is the project-level checker, run before and after every inner loop, edited only&lt;br&gt;
at outer-loop review gates. &lt;code&gt;work-pool.md&lt;/code&gt; is the registry with an explicit state&lt;br&gt;
machine: draft → planned → in-progress → done, with branches for deferred and&lt;br&gt;
cancelled. Plans come in three shapes — package-spec-plan, root-spec-plan for&lt;br&gt;
multi-package work, issue-bucket-plan for grouped issues. Decisions live in the&lt;br&gt;
package's &lt;code&gt;decisions.md&lt;/code&gt; while in flight, then graduate to a one-line constraint&lt;br&gt;
in inner-context and a full archive entry when locked.&lt;/p&gt;

&lt;p&gt;The motivating observation is simple. In article-1, the methodology survived&lt;br&gt;
because one operator carried the context across every session. One week later, two&lt;br&gt;
new packages had to be deliverable by sessions that didn't have weeks of context.&lt;br&gt;
Formalization is what makes a methodology survive the session boundary. The model&lt;br&gt;
cannot read intent. It can read documents.&lt;/p&gt;


&lt;h2&gt;
  
  
  §2 — Two Packages Through the Formalized Process
&lt;/h2&gt;

&lt;p&gt;color and color-wasm shipped under the new process. What that looked like in&lt;br&gt;
practice was not dramatic — which is the point.&lt;/p&gt;

&lt;p&gt;The packages had blocking relationships: render-pdf could not import &lt;code&gt;OutputIntent&lt;/code&gt;&lt;br&gt;
until the color API was stable, and compile could not enforce compliance until&lt;br&gt;
render-pdf could embed the ICC profile. Multi-package work used a root-spec-plan&lt;br&gt;
to orchestrate package-level plans rather than treating it as one large change.&lt;br&gt;
Each workId had a state machine behind it and a plan document that archived on&lt;br&gt;
completion. A session that arrived mid-work could read the plan, see what had been&lt;br&gt;
verified and what was pending, and continue without operator narration. That is&lt;br&gt;
the formalization working.&lt;/p&gt;

&lt;p&gt;One design reversal happened during this work, and it is the more instructive&lt;br&gt;
story. The color packages were originally planned as optional dependencies at every&lt;br&gt;
layer, on the assumption that flexibility was always preferable. Implementation&lt;br&gt;
surfaced the opposite: optional-everywhere produced more integration friction than&lt;br&gt;
it saved, and made the dependency direction unclear when render-pdf and compile&lt;br&gt;
both needed the same types. The decision was reversed mid-flight — imports became&lt;br&gt;
fixed at the layer they belonged to, and only the user-facing exports stayed&lt;br&gt;
optional.&lt;/p&gt;

&lt;p&gt;The decision lifecycle handled this without ceremony. The original choice was a&lt;br&gt;
draft decision in &lt;code&gt;decisions.md&lt;/code&gt;. The reversal was a new draft decision that&lt;br&gt;
superseded it. When it shipped, the one-line constraint graduated to inner-context&lt;br&gt;
Package Constraints, and the full entry — both the original and the supersession —&lt;br&gt;
moved to the package's archive file. The change is traceable in the documents.&lt;br&gt;
It is not something the operator has to remember.&lt;/p&gt;

&lt;p&gt;This is the formalization paying off. The architecture survived a reversal without&lt;br&gt;
becoming undocumented, and a future session reading the inner-context files sees&lt;br&gt;
the constraint, sees the archive reference, and can reconstruct why the dependency&lt;br&gt;
direction is the way it is.&lt;/p&gt;

&lt;p&gt;Tests passed. End-to-end runs went green. Article-1 described an audit moment&lt;br&gt;
named &lt;code&gt;excuse-me-kemal-I-forked-up.md&lt;/code&gt; — a file created when unit tests were&lt;br&gt;
passing across all packages while end-to-end tests were failing across all of them,&lt;br&gt;
and a full audit was the only way forward. There was no equivalent moment this&lt;br&gt;
time. The methodology that article-1 reconstructed from a crisis was running as&lt;br&gt;
designed when color and color-wasm shipped.&lt;/p&gt;

&lt;p&gt;Then the review started.&lt;/p&gt;


&lt;h2&gt;
  
  
  §3 — Then Review Returned 100+ Findings
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The four categories&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before describing how the findings were gathered, it helps to name what kinds of&lt;br&gt;
things they were. More than a hundred items resolved into four structurally&lt;br&gt;
different categories that a single priority column could not distinguish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inert fixes&lt;/strong&gt; — README accuracy, broken links, version alignment, stale test&lt;br&gt;
headers. Zero code risk, zero architectural implication. Safe to batch and ship&lt;br&gt;
without review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Surgical code corrections&lt;/strong&gt; — narrow, traceable to a specific line, no&lt;br&gt;
behavioral side effects. The &lt;code&gt;GTS_PDFA1&lt;/code&gt; hardcoding that mislabelled OutputIntent&lt;br&gt;
subtypes. The CSS font-weight matching that returned the wrong face when an&lt;br&gt;
exact-weight descriptor wasn't registered. Each had a clear before-and-after state&lt;br&gt;
and a test to update alongside it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Behaviour-changing refactors&lt;/strong&gt; — formally correct, but changes which outputs the&lt;br&gt;
algorithm produces. One item — fixing a prefix-sum off-by-one in the Knuth-Plass&lt;br&gt;
ratio computation — was correctly identified as something that "changes which&lt;br&gt;
breakpoints the algorithm chooses." Not a bug fix. A refactor that produces&lt;br&gt;
different paragraph shapes. It cannot travel in the same pull request as surgical&lt;br&gt;
fixes. Merging them makes the change untraceable and the revert scope unclear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;False-assurance tests&lt;/strong&gt; — the most dangerous category, and the one that deserves&lt;br&gt;
the most attention. These are tests that passed and never exercised the constraint&lt;br&gt;
they were written to verify. A widow/orphan test where both branches produced a&lt;br&gt;
three-word last line regardless of whether the penalty was active. A&lt;br&gt;
consecutive-hyphen-limit test where the fixture happened to produce the limit&lt;br&gt;
naturally, so the cap was never the binding constraint. A looseness test that&lt;br&gt;
produced eleven lines on every setting from −2 to +1. All passed. None provided&lt;br&gt;
assurance about anything. They were identified only because the review read the&lt;br&gt;
test fixtures carefully enough to notice that the constrained and unconstrained&lt;br&gt;
branches produced identical output. A green test suite is not evidence of a correct&lt;br&gt;
test suite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The five steps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The review that surfaced these categories ran in five steps, in a specific order&lt;br&gt;
that emerged from experience rather than from design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Whole-codebase review by Claude Opus.&lt;/strong&gt; The codebase was uploaded to a&lt;br&gt;
fresh Opus session as a zip archive. claude.ai reads from the main branch, and the&lt;br&gt;
new packages were on a feature branch — the upload was the only way to get the&lt;br&gt;
full state into a fresh session. Opus produced a structured pass across all layers&lt;br&gt;
and packages: consistency gaps, accuracy problems, decisions made during the build&lt;br&gt;
that didn't survive contact with the wider codebase. This became &lt;code&gt;todo-list.md&lt;/code&gt; —&lt;br&gt;
73 items.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — Crosscheck and addition by VS Code Copilot.&lt;/strong&gt; Two requests in the same&lt;br&gt;
session, with Sonnet in high-reasoning chat mode. First: &lt;em&gt;"see Claude Opus findings&lt;br&gt;
in &lt;code&gt;todo-list.md&lt;/code&gt; — don't take any action yet, create a table with issues,&lt;br&gt;
priorities, severity, validity, and write them to &lt;code&gt;todo-list-copilot.md&lt;/code&gt;."&lt;/em&gt; Then:&lt;br&gt;
&lt;em&gt;"additionally, provide findings based on your own review and add them to&lt;br&gt;
&lt;code&gt;todo-list-copilot.md&lt;/code&gt;."&lt;/em&gt; The crosscheck corrected priorities, narrowed scope on&lt;br&gt;
several items, and flagged behaviour-changing refactors that had been listed as&lt;br&gt;
cleanups. Copilot's own pass added items the structural review had not surfaced.&lt;br&gt;
The list grew from 73 to 81.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — Batched fixes by VS Code Copilot.&lt;/strong&gt; Group the items and fix the&lt;br&gt;
critical and high findings in batches. This is the work that the methodology&lt;br&gt;
documents had no name for — issue-bucket execution. The work-pool schema had the&lt;br&gt;
type. There was no Phase 1–3 process for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — GitHub Copilot review on diffs, then Copilot fixes.&lt;/strong&gt; Run three times&lt;br&gt;
until critical and high issues stopped appearing. GitHub Copilot review operates on&lt;br&gt;
the pull request diff rather than on the whole codebase, so it catches a different&lt;br&gt;
class of problem than the Opus structural pass. Findings were fetched via the&lt;br&gt;
GitHub CLI after each review pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh &lt;span class="nb"&gt;pr &lt;/span&gt;view &lt;span class="nt"&gt;--json&lt;/span&gt; reviews,comments &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'.reviews[].body, .comments[].body'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; docs/findings-I.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three iterations produced three findings files — &lt;code&gt;findings-I.md&lt;/code&gt;, &lt;code&gt;findings-II.md&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;findings-III.md&lt;/code&gt; — with comment counts of seven, eleven, and six. For each, the&lt;br&gt;
same prompt to VS Code Copilot: &lt;em&gt;"see GitHub Copilot review in &lt;code&gt;findings-X.md&lt;/code&gt; —&lt;br&gt;
don't take any action yet, create a table with issues, priorities, severity,&lt;br&gt;
validity, completed."&lt;/em&gt; Then: fix the issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 — Final structural pass by Opus.&lt;/strong&gt; The updated codebase went back to the&lt;br&gt;
same Opus session for a final review on remaining critical and high items. The&lt;br&gt;
session memory carried the original review context, so the second pass could focus&lt;br&gt;
on what had changed rather than re-deriving the codebase from scratch.&lt;/p&gt;

&lt;p&gt;Two reviewers, two registers: Opus for structural and architectural review across&lt;br&gt;
the whole codebase, GitHub Copilot for diff-level scrutiny on the pull request. VS&lt;br&gt;
Code Copilot synthesizing both into actionable batches and executing the fixes. The&lt;br&gt;
sequence wasn't documented before it was run. It worked.&lt;/p&gt;

&lt;p&gt;The total — around 105 findings — came from two sources: 81 items from the Opus&lt;br&gt;
and Copilot crosscheck in steps 1 and 2, and 24 comments from the three GitHub&lt;br&gt;
Copilot pull request review passes in step 4. These overlap in coverage but not in&lt;br&gt;
scope: the structural pass finds architectural drift that the diff reviewer never&lt;br&gt;
sees, and the diff reviewer finds interface-level issues that the structural pass&lt;br&gt;
glosses over. Both are necessary. Together they mapped onto those four categories.&lt;br&gt;
The category that mattered most was the last one — and the reason it exists&lt;br&gt;
connects directly to what the formalization revealed about itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  §4 — What the Formalization Revealed About Itself
&lt;/h2&gt;

&lt;p&gt;A methodology that has been written down can be checked against the work it&lt;br&gt;
actually governs. This is the property that makes formalization more than&lt;br&gt;
documentation hygiene — it produces a surface that the work can be measured&lt;br&gt;
against, and the gap becomes visible.&lt;/p&gt;

&lt;p&gt;The gap here is &lt;code&gt;issue-bucket&lt;/code&gt;. The work-pool schema names three work types:&lt;br&gt;
&lt;code&gt;spec&lt;/code&gt;, &lt;code&gt;package&lt;/code&gt;, and &lt;code&gt;issue-bucket&lt;/code&gt;. &lt;code&gt;spec&lt;/code&gt; work and &lt;code&gt;package&lt;/code&gt; work are&lt;br&gt;
documented end-to-end. Both have a Phase 1 mandatory-read list, a plan template,&lt;br&gt;
ownership verification, human gates, consistency controls, and an archive&lt;br&gt;
procedure. A new session can run either type by following the documented steps.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;issue-bucket&lt;/code&gt; work has none of this. The type exists as a first-class entry in&lt;br&gt;
the schema. There is no Phase 1 process, no plan template that fits its shape, no&lt;br&gt;
ownership verification rule, no defined gate between triage and execution. The&lt;br&gt;
five-step sequence in §3 is the process that ran. It is not yet a document. It&lt;br&gt;
produced correct results because one operator carried the context across every step&lt;br&gt;
— the same condition article-1 described as the situation the methodology was&lt;br&gt;
supposed to graduate from.&lt;/p&gt;

&lt;p&gt;One concrete example makes this precise. During the review, a finding was produced&lt;br&gt;
about the relationship between &lt;code&gt;@paragraf/render-pdf&lt;/code&gt; and &lt;code&gt;@paragraf/compile&lt;/code&gt;. The&lt;br&gt;
finding read the code correctly but drew an inverted conclusion — it described&lt;br&gt;
render-pdf as depending on compile, when the actual direction is the reverse:&lt;br&gt;
compile is the top-level orchestration layer and depends on render-pdf to produce&lt;br&gt;
PDF output, not the other way around. The finding would have looked plausible to&lt;br&gt;
anyone without context. It was caught during manual testing, when the explanation&lt;br&gt;
didn't match expected behavior. The detection mechanism was not a test. It was&lt;br&gt;
familiarity with the layer dependency structure documented in the inner-context&lt;br&gt;
files.&lt;/p&gt;

&lt;p&gt;That catch happened in the operator's head, not in the apparatus.&lt;/p&gt;

&lt;p&gt;This connects directly to the difference between typesetting and typography as&lt;br&gt;
disciplines. Typesetting is measurable: column width, leading, glyph spacing, grid&lt;br&gt;
alignment. Typography is judgment — pattern recognition built from sustained&lt;br&gt;
exposure to well-set text. A typographer sees paragraph colour as a unified&lt;br&gt;
impression before they can name what is wrong with any single line. The criterion&lt;br&gt;
is real. The instrument is human. You cannot replace the typographer with a&lt;br&gt;
checklist, because the checklist can only encode what the typographer already knew&lt;br&gt;
how to measure.&lt;/p&gt;

&lt;p&gt;The false-assurance tests in §3 are the software version of this same problem.&lt;br&gt;
The test author could see what the constraint was supposed to guarantee. They could&lt;br&gt;
not encode that criterion in a way the test framework would verify. So a measurable&lt;br&gt;
proxy stood in — run the test, check the output matches — and the proxy passed&lt;br&gt;
while the criterion was never checked. The apparatus ran correctly. The apparatus&lt;br&gt;
was checking the wrong thing. That distinction is not visible from inside the&lt;br&gt;
system. It is only visible to the operator.&lt;/p&gt;

&lt;p&gt;This is not a failure of formalization. It is the honest limit of what&lt;br&gt;
formalization can achieve. The methodology can document the apparatus that supports&lt;br&gt;
the operator. It cannot replace the operator. The right measure of a methodology's&lt;br&gt;
maturity is not how few gaps it has, but whether the gaps that remain are the right&lt;br&gt;
gaps — the ones where human judgment genuinely adds value that instruction cannot&lt;br&gt;
replicate.&lt;/p&gt;

&lt;p&gt;Twenty-two items from the original 81 remain open. They are not residual. One is a&lt;br&gt;
behaviour-changing refactor deferred until the Rust side can be updated in&lt;br&gt;
lockstep. Several are typed-only features requiring a decision rather than an&lt;br&gt;
implementation. One is a latent multi-span RTL bug that doesn't trigger today but&lt;br&gt;
is a known risk for when span support arrives. Each is a different kind of open&lt;br&gt;
item, and the current work-pool format does not distinguish between them. Those 22&lt;br&gt;
items are the live test bed for the issue-bucket process when it gets defined.&lt;/p&gt;




&lt;h2&gt;
  
  
  §5 — Close
&lt;/h2&gt;

&lt;p&gt;Two improvements in one week. Two new packages shipped under a methodology that&lt;br&gt;
was held in one operator's head in article-1 and exists as a document set now.&lt;br&gt;
More than a hundred review findings processed through a five-step sequence that&lt;br&gt;
worked, produced correct results, and is not yet documented. Twenty-two items&lt;br&gt;
deliberately left open as the test bed for the next iteration.&lt;/p&gt;

&lt;p&gt;Article-1 showed the methodology working. This article shows it made explicit, and&lt;br&gt;
shows where making it explicit revealed what was still implicit.&lt;/p&gt;

&lt;p&gt;The lesson is not specific to paragraf. Any LLM-assisted system that survives long&lt;br&gt;
enough will eventually formalize. Formalization is not the end state — it is the&lt;br&gt;
precondition for seeing clearly where the system ends and the operator begins. The&lt;br&gt;
value of writing down the apparatus is not that it removes the operator. It is that&lt;br&gt;
it shows you exactly where the operator is still necessary, and separates that from&lt;br&gt;
where they were simply compensating for missing documents.&lt;/p&gt;

&lt;p&gt;The next methodology article in this series comes when the issue-bucket loop has&lt;br&gt;
been observed in defined form rather than in informal practice. The open questions&lt;br&gt;
are concrete: What distinguishes a finding that goes straight to execution from one&lt;br&gt;
requiring triage? Who owns a behaviour-changing refactor that spans packages? How&lt;br&gt;
does a multi-reviewer sequence open and close without the operator as coordinator?&lt;br&gt;
The five-step process in §3 answered all of these by being run once. The next&lt;br&gt;
version answers them before the process runs.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;paragraf is open source. The repository, the live demo, and the article series&lt;br&gt;
are at &lt;a href="https://github.com/kadetr/paragraf" rel="noopener noreferrer"&gt;github.com/kadetr/paragraf&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>typescript</category>
      <category>markdown</category>
    </item>
    <item>
      <title>Three Gaps, One Platform</title>
      <dc:creator>Kemal Deniz Teket</dc:creator>
      <pubDate>Sat, 25 Apr 2026 19:21:40 +0000</pubDate>
      <link>https://forem.com/kadetr/three-gaps-one-platform-3pp6</link>
      <guid>https://forem.com/kadetr/three-gaps-one-platform-3pp6</guid>
      <description>&lt;h2&gt;
  
  
  The terminology, perceptual, and accessibility gaps between typographers and developers — and where paragraf sits
&lt;/h2&gt;




&lt;h2&gt;
  
  
  §0 — Hook
&lt;/h2&gt;

&lt;p&gt;A typographer says: "the colour is unbalanced." A developer opens the style sheet and adjusts the colors on the page. The paragraph is still wrong. Neither person made a mistake.&lt;/p&gt;

&lt;p&gt;"Colour" in typesetting means the visual density of a paragraph (also called &lt;em&gt;grey&lt;/em&gt;) — how evenly ink is distributed across the lines. It has nothing to do with hue. The developer heard the correct English word and acted on a reasonable interpretation. The typographer used the correct technical term and assumed it would be understood. The conversation failed before it began.&lt;/p&gt;

&lt;p&gt;This is the first gap: terminology. Two disciplines developed precise vocabularies for the same domain, independently, and the words do not always map to each other.&lt;/p&gt;

&lt;p&gt;The second gap is deeper. A typographer looks at a paragraph and perceives something wrong before they can name it — the trained eye reads texture, rhythm, and density as a unified impression. A developer looks at the same paragraph and sees output that matches the specification. The code is correct. The output is wrong. These are not contradictory statements. They describe two different instruments measuring the same object.&lt;/p&gt;

&lt;p&gt;The third gap is structural. The tools that produce publication-quality typographic output are desktop applications and command-line systems. They are not callable from a Node.js function. They do not run in a CI pipeline. They do not return a PDF buffer from a single API call.&lt;/p&gt;

&lt;p&gt;This article names all three gaps, maps the terminology where it overlaps, and describes where &lt;strong&gt;paragraf&lt;/strong&gt; sits in relation to them.&lt;/p&gt;




&lt;h2&gt;
  
  
  §1 — The Terminology Gap
&lt;/h2&gt;

&lt;p&gt;Both digital typesetting and software development have spent decades solving the problem of text on a page. They arrived at precise solutions — from different directions, with different tools, and with different names for what they found.&lt;/p&gt;

&lt;p&gt;If you have spent years working with type, the terms on the left are familiar. If you have spent years writing code, the terms on the right are familiar. The intersection of the two is not very crowded.&lt;/p&gt;

&lt;p&gt;This is not a translation from one language to a simpler one. It is a map between two equally precise vocabularies.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Typesetting&lt;/th&gt;
&lt;th&gt;Development&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Paragraph colour / grey&lt;/td&gt;
&lt;td&gt;Whitespace distribution&lt;/td&gt;
&lt;td&gt;How evenly ink is distributed across a paragraph. Tight lines and loose lines produce uneven grey — visible before you can name it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rivers&lt;/td&gt;
&lt;td&gt;Whitespace clustering&lt;/td&gt;
&lt;td&gt;Vertical gaps that form when loose lines stack. The eye catches them as white channels running through the paragraph.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Justified text&lt;/td&gt;
&lt;td&gt;Line-breaking algorithm&lt;/td&gt;
&lt;td&gt;Justification is the goal. The algorithm — greedy or Knuth-Plass — determines the quality of the result.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leading&lt;/td&gt;
&lt;td&gt;Line height&lt;/td&gt;
&lt;td&gt;Distance between baselines. Named after the lead strips compositors placed between rows of metal type.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical margin alignment&lt;/td&gt;
&lt;td&gt;Margin protrusion&lt;/td&gt;
&lt;td&gt;Punctuation and soft glyph edges pushed slightly outside the column boundary so the margin reads as visually straight.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F09crzm4x7tc2pfqobgre.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F09crzm4x7tc2pfqobgre.png" alt="KP vs Greedy" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Knuth-Plass (left) produces even paragraph grey with no rivers — spacing is distributed optimally across all lines simultaneously. Greedy (right) fills each line independently, producing uneven grey and visible rivers of white space.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F10qmr7es6hz7jv06z5hk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F10qmr7es6hz7jv06z5hk.png" alt="Optical Margin Alignment" width="800" height="612"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Optical margin alignment (top): punctuation protrudes slightly outside the column boundary, producing a visually straight margin edge. Without it (bottom), the margin reads as ragged even when mathematically flush.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  §2 — The Perceptual Gap
&lt;/h2&gt;

&lt;p&gt;The terminology gap is solvable with a shared reference. The perceptual gap is harder.&lt;/p&gt;

&lt;p&gt;A typographer's judgment is trained over years of looking at printed pages — recognising rivers in justified text before knowing their name, feeling when leading is too tight before measuring it, seeing when a paragraph's colour is wrong before identifying which lines are causing it. This is not intuition. It is pattern recognition built from sustained exposure to a specific domain. It cannot be transferred as a specification.&lt;/p&gt;

&lt;p&gt;A developer's judgment is trained differently — recognising when code is brittle, when an abstraction leaks, when a system will fail under load. These are also not intuitions. They are patterns learned from building and breaking systems. A developer looking at a paragraph of text has no trained apparatus for reading its typographic quality. The code produced correct output. That is the only signal available.&lt;/p&gt;

&lt;p&gt;The inverse is equally true. A typographer looking at a codebase has no trained apparatus for reading its structural quality. The document looks correct on screen. That is the only signal available.&lt;/p&gt;

&lt;p&gt;Neither gap represents a failure of intelligence or effort. They represent two disciplines that developed different perceptual instruments for different problems, and are now being asked to collaborate on the same artefact.&lt;/p&gt;

&lt;p&gt;What changes this is not more training — it is a shared surface. When the typographer can adjust &lt;code&gt;tolerance&lt;/code&gt; and see the paragraph grey change in real time, the parameter becomes a perceptual instrument. When the developer can see that the typographer's &lt;code&gt;tolerance: 1.5&lt;/code&gt; approval maps to a specific output quality, the visual judgment becomes a reusable specification. The gap does not close. But it becomes navigable.&lt;/p&gt;




&lt;h2&gt;
  
  
  §3 — The Accessibility Gap
&lt;/h2&gt;

&lt;p&gt;InDesign is a desktop application. TeX is a command-line system. Both require installation, licensing or configuration, and specialist knowledge to operate. Neither is callable from a Node.js function. Neither runs in a CI pipeline. Neither returns a PDF buffer from a single API call.&lt;/p&gt;

&lt;p&gt;This is not a criticism — both tools are mature, reliable, and used in production publishing worldwide. It is a statement about what they are: authoring environments designed for human operators, not pipeline components designed for programmatic integration.&lt;/p&gt;

&lt;p&gt;The gap this creates is visible in how publishing automation projects get built. A catalog of ten thousand products needs a PDF for each one. A report needs to be generated on demand from live data. A personalised document needs to be assembled from a template and a data record at request time. These are engineering problems. The tools that produce acceptable typographic output are not designed for them. The tools designed for them do not produce acceptable typographic output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;paragraf&lt;/strong&gt; is an attempt to occupy that gap: publication-quality typesetting as a Node.js library, callable from TypeScript, with no external processes and no GUI dependency. The same algorithms that production typesetting tools use — Knuth-Plass, OpenType shaping, optical margin alignment — running inside a standard npm install.&lt;/p&gt;

&lt;p&gt;That is the claim. Not that &lt;strong&gt;paragraf&lt;/strong&gt; matches the output quality of tools refined over decades. But that the algorithms are the same, the parameters are exposed, and the pipeline is programmable.&lt;/p&gt;

&lt;p&gt;The pipeline side is paragraf's current focus. The typographer-facing side — a visual authoring environment built on the same engine — is studio, currently in design. That is the subject of a future article in this series.&lt;/p&gt;




&lt;h2&gt;
  
  
  §4 — What paragraf Implements
&lt;/h2&gt;

&lt;p&gt;The terminology map is one thing. What &lt;strong&gt;paragraf&lt;/strong&gt; actually exposes is another. This section shows the implementation — with the actual parameters, and with honesty about what is not yet there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paragraph colour / whitespace distribution&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Controlled by &lt;code&gt;tolerance&lt;/code&gt; and &lt;code&gt;looseness&lt;/code&gt; in the paragraph composer. &lt;code&gt;tolerance&lt;/code&gt; sets how much deviation from ideal spacing is acceptable before a line is considered bad. &lt;code&gt;looseness&lt;/code&gt; nudges the algorithm toward fewer or more lines. Together they give the typographer direct control over the grey of a paragraph.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;composer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;normal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stretch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;normal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;lineWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;396&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tolerance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;looseness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;justified&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-us&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rivers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rivers are the visual consequence of a greedy line-breaking algorithm — not a parameter to set, but a problem paragraf avoids by using Knuth-Plass. The algorithm considers all lines simultaneously rather than filling each line independently, which eliminates the whitespace clustering that produces rivers in justified text.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Justified text / line-breaking algorithm&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;paragraf&lt;/strong&gt; implements Knuth-Plass with the full parameter set: tolerance, looseness, consecutive hyphen limit, and runt-line penalties. The demo runs Knuth-Plass and greedy side by side — the difference is visible immediately on any paragraph of justified prose.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;composer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;justified&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tolerance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;consecutiveHyphenLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Leading / line height&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Set per style in the paragraf style system. Applied baseline to baseline, consistent with typesetting convention rather than CSS convention (which measures from the top of the line box).&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineTemplate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LiberationSerif&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;justified&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;lineHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// baseline to baseline, in points&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Optical margin alignment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Implemented as a two-pass recomposition. The first pass composes normally. The second pass identifies punctuation and soft glyph edges at line starts and ends, applies protrusion values, and recomposes affected lines. The result is a visually straight margin edge on justified text.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;composer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;justified&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;opticalMargins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What is not yet implemented:&lt;/strong&gt;&lt;br&gt;
Frame-level widow/orphan control, and PDF/X output are in progress. These affect press-ready output but not the typesetting parameters above.&lt;/p&gt;




&lt;h2&gt;
  
  
  §5 — Close
&lt;/h2&gt;

&lt;p&gt;Three gaps. One platform pursuing all three.&lt;/p&gt;

&lt;p&gt;The terminology gap is addressed by exposed, named parameters that map directly to typesetting concepts. The perceptual gap is navigated by making those parameters adjustable and their output immediately visible — so that a typographer's judgment and a developer's integration can operate on the same artefact. The accessibility gap is addressed by building the pipeline as a Node.js library rather than a desktop application or command-line system.&lt;/p&gt;

&lt;p&gt;None of these gaps are fully closed. &lt;strong&gt;paragraf&lt;/strong&gt; is pre-1.0, in active development, and honest about what is not yet implemented. What it offers at this stage is a surface — a place where typographic judgment and engineering decisions can meet, with a shared vocabulary for what they are doing.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;paragraf series&lt;/em&gt; continues when studio is ready to demonstrate. A parallel series on AI-assisted development continues with second article — the v0.1 documentation system is the subject.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;paragraf is open source. The repository, the live demo, and the article series are at &lt;a href="https://github.com/kadetr/paragraf" rel="noopener noreferrer"&gt;github.com/kadetr/paragraf&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>typography</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Spec-Driven, AI-Assisted, Test-Validated — A Practitioner's Account</title>
      <dc:creator>Kemal Deniz Teket</dc:creator>
      <pubDate>Thu, 16 Apr 2026 23:43:19 +0000</pubDate>
      <link>https://forem.com/kadetr/spec-driven-ai-assisted-test-validated-a-practitioners-account-3gd4</link>
      <guid>https://forem.com/kadetr/spec-driven-ai-assisted-test-validated-a-practitioners-account-3gd4</guid>
      <description>&lt;p&gt;&lt;strong&gt;What made a two-week typesetting library possible, and what the methodology still lacks&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  §0 — Hook
&lt;/h2&gt;

&lt;p&gt;Most accounts of AI-assisted development describe tools and workflows. Very few show primary sources: the actual specification documents, the actual errors caught, the actual decisions revised under implementation pressure. Without those, the account is not evaluable and not replicable. You cannot tell whether the methodology produced the result or whether the result happened despite the methodology.&lt;/p&gt;

&lt;p&gt;This article shows the sources. &lt;a href="https://github.com/kadetr/paragraf" rel="noopener noreferrer"&gt;paragraf&lt;/a&gt; — an open source typesetting library built in two weeks — is the case study. 12 packages, Rust/WASM and TypeScript, covering a complete pipeline from font shaping to PDF output. The methodology is the subject. Every claim below is either demonstrated by an artifact or flagged explicitly as opinion.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffl0o20zm408woronvgrm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffl0o20zm408woronvgrm.png" alt="paragraf demo" width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The paragraf demo — live in browser.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  §1 — The Prerequisite Nobody Mentions
&lt;/h2&gt;

&lt;p&gt;Every AI-assisted development article eventually says some version of "give the AI precise specifications and you get better output." Almost none of them explain where precise specifications come from.&lt;/p&gt;

&lt;p&gt;They do not come from knowing the algorithms. paragraf implements the Knuth-Plass line-breaking algorithm, optical margin alignment, rustybuzz OpenType shaping, and Unicode BiDi. None of those were known in detail before the project started. They were researched, trusted to AI implementation, and verified through tests.&lt;/p&gt;

&lt;p&gt;What domain knowledge actually contributes is different and harder to acquire: knowledge of failure modes in the target environment.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;onMissing&lt;/code&gt; design in &lt;code&gt;@paragraf/compile&lt;/code&gt; — skip, fallback, placeholder, each with defined behavior — does not appear in the typesetting literature. It comes from having seen a real product information management export fail a batch job at 2am because three records out of ten thousand had a missing field. The &lt;code&gt;normalize()&lt;/code&gt; hook that maps raw data to template bindings comes from knowing that every enterprise data source has a different field naming convention and no library adapter ever covers a specific customer's exact schema. The strict layer dependency rules — each package imports only from layers below, no exceptions — come from having debugged circular dependency failures in InDesign automation pipelines.&lt;/p&gt;

&lt;p&gt;An AI given the same algorithm knowledge and no production context would have built something technically correct that fails the first time it touches real data. The specifications were precise because the failure modes were known in advance. That precision is the prerequisite.&lt;/p&gt;

&lt;p&gt;This maps closely to what practitioners in the &lt;a href="https://aicoding.leaflet.pub/3miwhqqvwxc2x" rel="noopener noreferrer"&gt;AI coding literature&lt;/a&gt; describe as spec inputs: markdown documents, conversations, diagrams, domain models, and existing code feeding into requirements that the AI can act on reliably. The taxonomy is accurate. What the literature underemphasises is that the quality of those inputs depends almost entirely on what the human already knows — not about the AI, but about the problem domain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjxcoxrnpi1pgo1tciko7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjxcoxrnpi1pgo1tciko7.png" alt="specification document" width="800" height="598"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The specification document that governed package structure and testing strategy&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  §2 — The Two-Loop Process
&lt;/h2&gt;

&lt;p&gt;The development process followed two nested loops. Understanding the distinction between them is the core of the methodology.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;outer loop&lt;/strong&gt; covers the project. It produces: a problem definition, scope constraints, a high-level layer architecture, an architecture diagram, and a versioned roadmap. Outer loop documents are updated when implementation reality forces a revision. They are not fixed contracts — they are living records of current understanding. This is what &lt;a href="https://martinfowler.com/articles/reduce-friction-ai/design-first-collaboration.html" rel="noopener noreferrer"&gt;Fowler calls design-first collaboration&lt;/a&gt;: the human owns the architecture, the AI executes within it.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;inner loop&lt;/strong&gt; covers each package. It produces: a scope definition, input/output schemas, a step-by-step implementation plan with defined subtasks and edge cases, unit tests written before implementation, then implementation against that specification, closing with integration and end-to-end tests validating every contract.&lt;/p&gt;

&lt;p&gt;Here is what the inner loop looks like in practice, from the &lt;code&gt;@paragraf/linebreak&lt;/code&gt; package plan:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz9at1gfcq0s2vqmpasj9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz9at1gfcq0s2vqmpasj9.png" alt="inner loop plan" width="800" height="886"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;an inner loop plan sample&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The two loops are not independent. An issue discovered mid-package feeds back into the outer loop and may revise the project scope, the architecture, or the roadmap. A concrete example: the layer numbering in the original plan had &lt;code&gt;1c-font-engine&lt;/code&gt; and &lt;code&gt;1b-shaping-wasm&lt;/code&gt;. During extraction, the numbering was revised to &lt;code&gt;1b-font-engine&lt;/code&gt; and &lt;code&gt;2a-shaping-wasm&lt;/code&gt; to better reflect the actual dependency structure. Small decision, visible consequence: the outer loop was updated in response to implementation reality rather than preserved as a false contract. The architecture documents show that evolution directly:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo8kdajukf0vt3ujoecej.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo8kdajukf0vt3ujoecej.png" alt="layers &amp;amp; packages" width="800" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Architecture at the start of the project (left) and after several outer loop iterations (right).&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  §3 — Trust, But Verify
&lt;/h2&gt;

&lt;p&gt;Trust, but verify — I first heard it from my director Bob Bair when we first met in Greece, nearly ten years ago. I assumed it was professional jargon. It turned out to be a Russian proverb, and the most accurate description of what makes AI-assisted development work at this level.&lt;/p&gt;

&lt;p&gt;The phrase "AI-assisted" covers an enormous range of practices, from fully autonomous code generation to a human using AI as an implementation engine operating inside precisely defined contracts. The methodology here is firmly the second. No agentic framework was used — existing frameworks are built around task delegation and autonomy, which is the opposite of what this methodology requires. Every step involved a human decision. The AI tools were implementation engines and discussion partners, not architects.&lt;/p&gt;

&lt;p&gt;The tooling split was deliberate. VS Code Copilot and Claude Sonnet/Haiku handled code generation inside the inner loop — writing implementations against pre-defined schemas and tests. Claude Opus and Gemini handled architecture discussions, document synthesis, and the outer loop decisions where the question was "what, why &amp;amp; how should this be" rather than "implement this." A third role — code review and audit — ran across both: Claude connected to GitHub, Copilot code review, and manual review at integration boundaries. The key practice was using multiple models as a quality control mechanism: when two models diverge on an assessment of the same code or decision, that disagreement is a signal worth investigating. The human resolves it. This is what the emerging &lt;a href="https://www.humanlayer.dev/blog/skill-issue-harness-engineering-for-coding-agents" rel="noopener noreferrer"&gt;harness engineering&lt;/a&gt; &lt;a href="https://www.ignorance.ai/p/the-emerging-harness-engineering" rel="noopener noreferrer"&gt;literature&lt;/a&gt; is beginning to formalise — the scaffolding around AI tools matters as much as the tools themselves.&lt;/p&gt;

&lt;p&gt;The control mechanism that makes this work at the code level is test-first development — but not in the conventional sense of "write tests alongside your code." The distinction matters: tests written before implementation define what correct means before asking for anything. They are specifications expressed as assertions. The AI implements against them. Errors are caught at the unit boundary, not at integration time.&lt;/p&gt;

&lt;p&gt;During the first week, unit tests were passing across all packages and end-to-end tests were failing across all packages. The response was to stop, run a full audit, classify every issue by severity, and fix in order before continuing. The audit document was named &lt;code&gt;excuse-me-kemal-I-forked-up.md&lt;/code&gt;. The name is the emotional record of the moment. The contents are the professional response to it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F039zifhsjlx95maepemf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F039zifhsjlx95maepemf.png" alt="fork" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Six critical issues. Two high. Three medium. All code files, tests, and documents fixed before shipping. The table is not evidence that the process prevents errors — it is evidence that the review step is real and not ceremonial. Errors that compound into the next stage are significantly more expensive than errors caught at their origin. The audit caught them at their origin.&lt;/p&gt;




&lt;h2&gt;
  
  
  §4 — What It Produced and What It Lacks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it produced:&lt;/strong&gt; 12 published packages covering a complete typesetting pipeline. 906 unit tests across all packages, 70 end-to-end tests, and 23 manual test scripts producing real PDF and SVG output. A live demo running the full WASM shaping pipeline in the browser. A complete compile API that takes a template and a data record and returns a PDF buffer in a single function call. The visible output of a correctly specified system:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm1jk10rc4uc21gvg8flm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm1jk10rc4uc21gvg8flm.png" alt="Knuth-Plass" width="800" height="148"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Knuth-Plass (left) distributes spacing evenly across all lines simultaneously. Greedy (right) fills each line independently, producing uneven spacing and a stretched final line.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The left column was produced by Knuth-Plass with real OpenType metrics. The right column was produced by the greedy algorithm used by every JavaScript PDF library. The difference is the consequence of specification precision applied at every layer of the stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it lacks:&lt;/strong&gt; three gaps worth naming honestly.&lt;/p&gt;

&lt;p&gt;Documentation was not versioned alongside code. Inner loop documents were overwritten as understanding evolved. The dependency reference document changed significantly during the project but only its current state is preserved. Git tracked every code change with line-level precision while documentation changes were invisible. The discipline was there. The infrastructure to preserve the evidence of that discipline was not. These are not the same failure.&lt;/p&gt;

&lt;p&gt;Session continuity works for one person. The session handoff document — a structured snapshot of current state, architectural decisions with reasoning, known bugs classified by severity, and next steps — is what allows AI-assisted development to resume coherently across sessions. &lt;a href="https://martinfowler.com/articles/reduce-friction-ai/context-anchoring.html" rel="noopener noreferrer"&gt;Fowler's context-anchoring&lt;/a&gt; describes this problem precisely: without deliberate anchoring, each session starts from a degraded understanding of the project's current state. The handoff document is a manual solution to that problem. It works. It does not yet scale to a team without additional tooling design.&lt;/p&gt;

&lt;p&gt;The process is disciplined but not yet systematic. Disciplined means: there was a defined structure, it was followed consistently, it produced results. Systematic means: the process itself is observable and reproducible from its own records. The audit document was produced because an audit was run manually at a moment of failure. A systematic process would have tooling that made that audit continuous rather than reactive.&lt;/p&gt;




&lt;h2&gt;
  
  
  §5 — What the Next Version Looks Like
&lt;/h2&gt;

&lt;p&gt;Three concrete improvements follow directly from the gaps above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation commits alongside code commits.&lt;/strong&gt; When a specification changes in response to an implementation discovery, that change should be recorded at the same moment as the code change that triggered it, with the reasoning attached. Not a git diff of a prose document — a dated entry written by the person who made the decision, capturing intent rather than just state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inner loop documents versioned rather than overwritten.&lt;/strong&gt; Each package plan should have a version history showing what changed between drafts and why. The delta between versions is where the feedback loop between inner and outer is visible. Without it you have outcomes but not reasoning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session handoff documents dated and immutable.&lt;/strong&gt; The handoff document that allows a session to resume coherently should be treated as a changelog entry, not a mutable working document. Write it at the end of a session, date it, do not overwrite it. The history of those documents is the history of how the project's understanding evolved.&lt;/p&gt;

&lt;p&gt;The quality of AI-assisted output is determined by the precision of the specification, the discipline of the review, and the honesty of the record. All three are learnable. None of them require a particular tool. The methodology described here is not finished — it is a working version that produced a working result and has visible room to improve. That is a more useful account than a polished success story, and it is the only kind worth writing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The next article in this series covers the problem space in more depth — the typographic and algorithmic reasons why existing JavaScript document libraries fall short of publication quality, and what it takes to close that gap.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;paragraf is open source. The repository, the live demo, and the article series are at &lt;a href="https://github.com/kadetr/paragraf" rel="noopener noreferrer"&gt;github.com/kadetr/paragraf&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rust</category>
      <category>testing</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Towards an Open Source Print-Ready Publication Library in JavaScript</title>
      <dc:creator>Kemal Deniz Teket</dc:creator>
      <pubDate>Mon, 13 Apr 2026 21:47:44 +0000</pubDate>
      <link>https://forem.com/kadetr/towards-an-open-source-print-ready-publication-library-in-javascript-19ba</link>
      <guid>https://forem.com/kadetr/towards-an-open-source-print-ready-publication-library-in-javascript-19ba</guid>
      <description>&lt;p&gt;&lt;strong&gt;Building paragraf: a typesetter with industry-standard methods for print-ready publication quality documents in Node.js&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  §0 — Introduction
&lt;/h2&gt;

&lt;p&gt;When you open a professionally printed book, a luxury product catalog, or a pharmaceutical package insert, the text feels different. Words are evenly spaced. Paragraphs have a calm, consistent density. Punctuation sits exactly where it should. The page looks deliberate.&lt;/p&gt;

&lt;p&gt;When you generate the same content programmatically — using the JavaScript libraries available today — something is lost. Word spacing is uneven. Lines break at awkward places. Certain letter combinations look slightly wrong. The output is functional but it does not look typeset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;paragraf&lt;/strong&gt; is an attempt to close that gap. 12 packages are complete, 3 are planned for the coming weeks. The typesetting core — line breaking, font shaping, optical margins, bidirectional text, hyphenation, styles, layout, and the compile pipeline — is production-ready. The print production layer — color management, color separations, and print-ready PDF output — and the visual editor layer are in progress. This article is an overview of what paragraf is, how it is built, and where it is going.&lt;/p&gt;




&lt;h2&gt;
  
  
  §1 — The Problem: Why does text look different in books vs documents generated by code?
&lt;/h2&gt;

&lt;p&gt;Professional typesetting solves several distinct problems simultaneously. Most JavaScript PDF pipelines solve none of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Line Breaking
&lt;/h3&gt;

&lt;p&gt;The greedy algorithm — used by every JavaScript PDF library and every browser — fills each line as fully as possible and breaks there, making each decision in isolation. The Knuth-Plass algorithm, developed by Donald Knuth and Michael Plass in 1981 and used by TeX and Adobe InDesign since then, treats the entire paragraph as a single optimisation problem and finds the break sequence that minimises total spacing deviation across all lines simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnnzapwfb48jtmx3c6n08.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnnzapwfb48jtmx3c6n08.png" alt="Knuth-Plass Algorithm" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The demo above shows the same text set with Knuth-Plass (left) and the greedy algorithm (right), at identical column width and font size.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Font Shaping
&lt;/h3&gt;

&lt;p&gt;Correct line breaking requires correct word widths. Correct word widths require processing the actual glyph outlines in the actual font file: rules that combine letter pairs into designed ligature forms (the Glyph Substitution, or &lt;a href="https://learn.microsoft.com/en-us/typography/opentype/spec/gsub" rel="noopener noreferrer"&gt;GSUB&lt;/a&gt; table) and rules that adjust spacing between specific character combinations (the Glyph Positioning, or &lt;a href="https://learn.microsoft.com/en-us/typography/opentype/spec/gpos" rel="noopener noreferrer"&gt;GPOS&lt;/a&gt; table). Most libraries get this wrong — they approximate from pre-computed tables — and the errors compound directly into Knuth-Plass spacing calculations. paragraf uses &lt;a href="https://github.com/RazrFalcon/rustybuzz" rel="noopener noreferrer"&gt;rustybuzz&lt;/a&gt; — a Rust port of &lt;a href="https://harfbuzz.github.io/" rel="noopener noreferrer"&gt;HarfBuzz&lt;/a&gt;, the shaping engine used by Firefox, Chrome, and LibreOffice — compiled to WebAssembly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optical Margin Alignment
&lt;/h3&gt;

&lt;p&gt;A line beginning with a quotation mark or hyphen creates a visual indent even when geometrically flush, because those glyphs are narrower than full characters. The text block looks ragged at the margin even though every line starts at the same x coordinate. Optical margin alignment corrects this by allowing punctuation to protrude fractionally into the margin — typically 0.3 to 0.7 times the font size depending on the character — so the text block appears visually straight.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzqo4im8mexoifkpgnhh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzqo4im8mexoifkpgnhh.png" alt="OMA comparison: with optical margins (top) vs without (bottom). The right edge of the top block reads as visually straight; the bottom block shows punctuation sitting flush with letter glyphs, which reads as uneven." width="800" height="624"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Bidirectional Text
&lt;/h3&gt;

&lt;p&gt;Arabic and Hebrew run right-to-left (RTL). Latin text runs left-to-right (LTR). Each Arabic letter takes a different form depending on its position within a word — the letter ع (ayn) has four distinct forms: isolated, initial, medial, and final. Mixed LTR and RTL paragraphs require the &lt;a href="https://www.unicode.org/reports/tr9/" rel="noopener noreferrer"&gt;Unicode Bidirectional Algorithm&lt;/a&gt; to resolve the correct visual order of characters. This is not a bolt-on feature — it requires the GSUB shaping pipeline to already exist. You cannot add BiDi to a library that approximates font metrics.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqqh6ijff51tys0oqw28d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqqh6ijff51tys0oqw28d.png" alt="Arabic paragraph rendered RTL in the paragraf demo, with direction controls showing Auto / Force LTR / Force RTL." width="800" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Hyphenation
&lt;/h3&gt;

&lt;p&gt;Without correct hyphenation patterns, Knuth-Plass has fewer valid break points and is forced toward wider spacing or tighter compression. Hyphenation rules vary significantly by language and cannot be derived from simple character patterns — English breaks "rec-ord" differently as a noun versus a verb, German compounds stack hyphenation boundaries in ways that require dictionary knowledge, Turkish has vowel harmony rules that affect syllabification. paragraf uses the Liang algorithm implemented via the &lt;a href="https://www.npmjs.com/package/hyphen" rel="noopener noreferrer"&gt;hyphen&lt;/a&gt; npm package, covering 22 languages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy4tggho2yguc3xd0rufa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy4tggho2yguc3xd0rufa.png" alt="English paragraph with hyphenation enabled, showing mid-word breaks at syllable boundaries. Minimum word length to hyphenate is 5 — shorter words are never broken." width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In the example above, the minimum word length to hyphenate is 5, so words with fewer than 5 characters are not hyphenated.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Styles, Layout, and Assembly
&lt;/h3&gt;

&lt;p&gt;Change the body font size in pdfmake and you change one paragraph, not the document. Without a style system with inheritance, every derived style is an independent manual update. Without a layout model, page geometry and unit conversions are the caller's responsibility on every project. Without a template schema, binding data fields to content slots — and handling missing fields gracefully — requires custom code per integration. &lt;strong&gt;paragraf&lt;/strong&gt; provides all of it as a coherent stack: a style system with inheritance, a layout model handling units and page geometry, a template schema with defined missing-field behaviour, and a compiler assembling everything into a single function call.&lt;/p&gt;




&lt;h2&gt;
  
  
  §2 — The Architecture: Monorepo, Layered, Modular
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;paragraf&lt;/strong&gt; is a monorepo of 12 published packages organised in strict layers. Each layer imports only from layers below it. No exceptions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ork1598gpcfooq1y6hm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ork1598gpcfooq1y6hm.png" alt="paragraf architecture diagram showing Layer 0 (types, color in-progress), Layer 1 (linebreak, font-engine, layout, style), Layer 2 (shaping-wasm, render-core, color-wasm in-progress), Layer 3 (typography, render-pdf), Layer 4 (compile, template), and App (demo, studio in-progress)." width="800" height="653"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For most developers, &lt;code&gt;@paragraf/compile&lt;/code&gt; is the only package that matters directly. It takes a template and a data record and returns a PDF buffer. The packages beneath it are there for developers who need to work at lower levels of the stack — custom renderers, browser-side line breaking, integration with existing font pipelines.&lt;/p&gt;

&lt;p&gt;Layer 0 carries zero-dependency shared interfaces. Layer 1 covers the algorithm and measurement packages — line breaking, font engine, page layout, style registry — all browser-safe. Layer 2 adds the Rust/WebAssembly shaper and the SVG/Canvas renderer. Layer 3 is Node-only: the paragraph compositor with OMA and BiDi, and the PDF renderer. Layer 4 is the user-facing API: the template schema and the compile pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  §3 — The Approach: Spec-Driven, AI-Assisted, Test-Validated
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;paragraf&lt;/strong&gt; was designed and built through a disciplined two-loop process. The outer loop covers the project: problem, scope, specifications, constraints, high-level layer architecture, diagrams and models, and a versioned roadmap with future work. The inner loop covers each package: scope definition, input/output schemas, diagrams and models, a full implementation plan with defined steps and edge cases, unit tests written before implementation, then implementation against that specification, closing with integration and end-to-end tests validating every contract.&lt;/p&gt;

&lt;p&gt;The two loops are not independent. An issue discovered mid-package — an edge case that invalidates an assumption, a schema that cannot express a required input — feeds back into the outer loop and may revise the project scope, the architecture, or the roadmap. The project artifacts are living documents, not a fixed contract.&lt;/p&gt;

&lt;p&gt;The implementation was AI-assisted. Claude and GitHub Copilot were used as implementation engines operating against precise specifications. No agentic framework was used — every step involved a human decision. The specifications, architectural decisions, and constraint definitions came from domain knowledge: years working with software engineering, project management, InDesign, publishing automation, and multi-agent systems research.&lt;/p&gt;

&lt;p&gt;The quality of AI-assisted output is directly proportional to the precision of the specification the human provides. A developer without this domain background asking an AI to build a typesetting engine would get something that looks like one but breaks in ways they would not recognise.&lt;/p&gt;

&lt;p&gt;A separate article covering this process in detail will be published — the artifacts, the feedback loops, and the specific ways domain knowledge shaped the specifications.&lt;/p&gt;




&lt;h2&gt;
  
  
  §4 — The Technique: TypeScript, Node, WASM, Rust
&lt;/h2&gt;

&lt;p&gt;The core packages are TypeScript targeting Node.js 18+. Browser-safe packages also run in modern browsers — the live demo executes the full shaping and line-breaking pipeline client-side without a server. The OpenType shaper is Rust compiled to WebAssembly via wasm-pack: Rust was chosen for access to the rustybuzz shaping library, which would have taken months (if not years) to reimplement correctly in TypeScript. When the WASM binary is unavailable, the pipeline falls back automatically to a fontkit TypeScript path. PDF output uses pdfkit. The monorepo uses npm workspaces with tsup for package builds and Vitest for testing across all packages.&lt;/p&gt;




&lt;h2&gt;
  
  
  §5 — The Live Demo: See It Yourself
&lt;/h2&gt;

&lt;p&gt;The live demo runs entirely in the browser — the WASM shaper, the Knuth-Plass algorithm, and the SVG renderer all execute client-side. Type your own text, adjust the tolerance and looseness sliders, and watch the line breaks recalculate in real time. Four interactive pages: line breaking with side-by-side KP vs greedy comparison, layout controls, typography showcase, and multilingual rendering across 6 scripts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://kadetr.github.io/paragraf/" rel="noopener noreferrer"&gt;→ Live demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu9b9rd7s9lztvac8ekay.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu9b9rd7s9lztvac8ekay.png" alt="The line breaking page — editable text, tolerance/looseness/letter-spacing/alignment controls on the left; Knuth-Plass and greedy results side by side on the right." width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The line breaking page — edit text, adjust tolerance, and see Knuth-Plass and greedy results recalculate side by side.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  §6 — Future Work
&lt;/h2&gt;

&lt;p&gt;paragraf's package architecture is stable and deliberate, though still open to revision as the project evolves. The 12 published packages cover the full typesetting pipeline from algorithm to PDF output, and the three remaining planned packages — &lt;code&gt;@paragraf/color&lt;/code&gt;, &lt;code&gt;@paragraf/color-wasm&lt;/code&gt; — address color management and print production, and &lt;code&gt;@paragraf/studio&lt;/code&gt; — the browser-based editor template. Every feature below fits within the existing package structure as an enhancement, not a structural addition. That is a deliberate outcome of the layered architecture.&lt;/p&gt;

&lt;p&gt;Color and print production are the most significant items ahead. ICC color profile support, CMYK color spaces, and PDF/X compliance — the ISO standard for print-ready PDF exchange — are the bridge between paragraf's current typesetting quality and full print production readiness. These depend on &lt;code&gt;@paragraf/color-wasm&lt;/code&gt;, a Rust/LCMS2 WebAssembly package following the same pattern as the existing shaping layer.&lt;/p&gt;

&lt;p&gt;Typographic quality features planned within existing packages include micro-typography (per-line letter-spacing adjustments as an additional optimisation degree of freedom alongside word spacing), font expansion (horizontal glyph scaling, another Knuth-Plass optimisation dimension), drop caps, small caps, optical sizes, and balanced ragged lines.&lt;/p&gt;

&lt;p&gt;Layout and composition additions include page-level widow and orphan control (the paragraph-level penalty system exists; full page reflow is a separate pass in &lt;code&gt;@paragraf/compile&lt;/code&gt;), vertical justification, cross-column baseline grid alignment, inline figures with text runaround, and anchored objects.&lt;/p&gt;

&lt;p&gt;The Studio is a browser-based visual template editor — a web application in the monorepo, not a published npm package — that writes the same JSON format &lt;code&gt;@paragraf/template&lt;/code&gt; defines. It is the non-developer interface to the compile pipeline: drag-and-drop frame layout, point-and-click style definitions, and a live PDF preview powered by the same WASM stack that runs in the demo today.&lt;/p&gt;

&lt;p&gt;Contributions, issues, and discussions are open on &lt;a href="https://github.com/kadetr/paragraf" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. If you are working on a document pipeline and running into the limitations described in this article, opening an issue is the best place to start.&lt;/p&gt;




&lt;h2&gt;
  
  
  Annex: Terminology
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bleed&lt;/strong&gt; — artwork that extends beyond the trim edge of a page to prevent white borders after cutting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BiDi&lt;/strong&gt; — Unicode Bidirectional Algorithm. Resolves display order for mixed left-to-right and right-to-left text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Greedy line breaking&lt;/strong&gt; — fills each line as fully as possible, one line at a time, with no lookahead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ICC profile&lt;/strong&gt; — International Color Consortium profile. Defines the color characteristics of a device for accurate color reproduction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Knuth-Plass&lt;/strong&gt; — optimal paragraph line-breaking algorithm (Knuth &amp;amp; Plass, 1981). Minimises total spacing deviation across all lines simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenType shaping&lt;/strong&gt; — processing font files to apply ligatures, kerning, and contextual letter forms defined in GSUB/GPOS tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optical margin alignment (OMA)&lt;/strong&gt; — allowing punctuation to protrude fractionally into the margin for a visually straight text edge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PDF/X&lt;/strong&gt; — ISO standard for print-ready PDF exchange. Guarantees color, font embedding, and other production requirements are met.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is the entry point to a series. Each section above corresponds to a dedicated article in this series, published as they become available. The algorithm, the WASM architecture, the compile pipeline, the AI-assisted development process, and the print production roadmap are each covered separately.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>opensource</category>
      <category>node</category>
    </item>
  </channel>
</rss>
