<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Specification on TurboVision</title>
    <link>https://turbovision.in6-addr.net/tags/specification/</link>
    <description>Recent content in Specification on TurboVision</description>
    <generator>Hugo</generator>
    <language>en</language>
    <lastBuildDate>Tue, 21 Apr 2026 14:06:12 +0000</lastBuildDate>
    <atom:link href="https://turbovision.in6-addr.net/tags/specification/index.xml" rel="self" type="application/rss&#43;xml" />
    
    
    
    <item>
      <title>Deterministic DIR Output as an Operational Contract</title>
      <link>https://turbovision.in6-addr.net/retro/dos/deterministic-dir-output-as-an-operational-contract/</link>
      <pubDate>Tue, 10 Mar 2026 00:00:00 +0000</pubDate>
      <lastBuildDate>Tue, 10 Mar 2026 00:00:00 +0000</lastBuildDate>
      <guid>https://turbovision.in6-addr.net/retro/dos/deterministic-dir-output-as-an-operational-contract/</guid>
      <description>&lt;p&gt;The story starts at 23:14 in a room with two beige towers, one half-dead fluorescent tube, and a whiteboard covered in hand-written file counts. We had one mission: rebuild a damaged release set from mixed backup disks and compare it against a known-good manifest.&lt;/p&gt;
&lt;p&gt;On paper, that sounds easy. In practice, it meant parsing &lt;code&gt;DIR&lt;/code&gt; output across different machines, each configured slightly differently, each with enough personality to make automation fail at the worst moment.&lt;/p&gt;
&lt;p&gt;By 23:42 we had already hit the first trap. One machine produced &lt;code&gt;DIR&lt;/code&gt; output that looked &amp;ldquo;normal&amp;rdquo; to a human and ambiguous to a parser. Another printed dates in a different shape. A third had enough local customization that every assumption broke after line three. We were not failing because DOS was bad. We were failing because we had not written down what &amp;ldquo;correct output&amp;rdquo; meant.&lt;/p&gt;
&lt;p&gt;That night we stopped treating &lt;code&gt;DIR&lt;/code&gt; as a casual command and started treating it as an API contract.&lt;/p&gt;
&lt;p&gt;This article is that deep dive: why a deterministic profile matters, how to structure it, and how to parse it without superstitions.&lt;/p&gt;
&lt;h2 id=&#34;the-turning-point-formatting-is-behavior&#34;&gt;The turning point: formatting is behavior&lt;/h2&gt;
&lt;p&gt;In modern systems, people accept that JSON schemas and protocol contracts are architecture. In DOS-era workflows, plain text command output played that same role. If your automation consumed command output, formatting &lt;em&gt;was&lt;/em&gt; behavior.&lt;/p&gt;
&lt;p&gt;Our internal profile locked one specific command shape:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DIR [drive:][path][filespec]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;default long listing&lt;/li&gt;
&lt;li&gt;no &lt;code&gt;/W&lt;/code&gt;, no &lt;code&gt;/B&lt;/code&gt;, no formatting switches&lt;/li&gt;
&lt;li&gt;fixed US date/time rendering (&lt;code&gt;MM-DD-YY&lt;/code&gt;, &lt;code&gt;h:mma&lt;/code&gt; / &lt;code&gt;h:mmp&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That scoping decision solved half the problem. We stopped pretending one parser should support every possible switch/locale and instead declared a strict operating envelope.&lt;/p&gt;
&lt;h2 id=&#34;a-canonical-listing-is-worth-hours-of-debugging&#34;&gt;A canonical listing is worth hours of debugging&lt;/h2&gt;
&lt;p&gt;The profile included a canonical example and we used it as a fixture:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt; 1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 4
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 5
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 6
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 7
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 8
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 9
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;10
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;11
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;12
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt; Volume in drive C has no label
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt; Volume Serial Number is 3F2A-19C0
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt; Directory of C:\RETROLAB
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;AUTOEXEC BAT      1024 03-09-96  9:40a
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;BIN              &amp;lt;DIR&amp;gt; 03-08-96  4:15p
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;DOCS             &amp;lt;DIR&amp;gt; 03-07-96 11:02a
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;README   TXT       512 03-09-96 10:20a
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;SRC              &amp;lt;DIR&amp;gt; 03-07-96 11:04a
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;TOOLS    EXE     49152 03-09-96 10:21a
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;       3 File(s)      50,688 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;       3 Dir(s)  14,327,808 bytes free&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Why include this in a spec? Because examples settle debates that prose cannot. When two engineers disagree, the fixture wins.&lt;/p&gt;
&lt;h2 id=&#34;the-38-column-row-discipline&#34;&gt;The 38-column row discipline&lt;/h2&gt;
&lt;p&gt;The core entry template was fixed-width:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;%-8s %-3s  %8s %8s %6s&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That yields exactly 38 columns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;columns &lt;code&gt;1..8&lt;/code&gt;: basename (left-aligned)&lt;/li&gt;
&lt;li&gt;column &lt;code&gt;9&lt;/code&gt;: space&lt;/li&gt;
&lt;li&gt;columns &lt;code&gt;10..12&lt;/code&gt;: extension (left-aligned)&lt;/li&gt;
&lt;li&gt;columns &lt;code&gt;13..14&lt;/code&gt;: spaces&lt;/li&gt;
&lt;li&gt;columns &lt;code&gt;15..22&lt;/code&gt;: size-or-dir (right-aligned)&lt;/li&gt;
&lt;li&gt;column &lt;code&gt;23&lt;/code&gt;: space&lt;/li&gt;
&lt;li&gt;columns &lt;code&gt;24..31&lt;/code&gt;: date&lt;/li&gt;
&lt;li&gt;column &lt;code&gt;32&lt;/code&gt;: space&lt;/li&gt;
&lt;li&gt;columns &lt;code&gt;33..38&lt;/code&gt;: time (right-aligned)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you adopt positional parsing instead of regex guesswork, &lt;code&gt;DIR&lt;/code&gt; lines become boring in the best way.&lt;/p&gt;
&lt;h2 id=&#34;why-this-works-even-on-noisy-nights&#34;&gt;Why this works even on noisy nights&lt;/h2&gt;
&lt;p&gt;Fixed-width parsing has practical advantages under pressure:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;no locale-sensitive token splitting for date/time columns&lt;/li&gt;
&lt;li&gt;no ambiguity between &lt;code&gt;&amp;lt;DIR&amp;gt;&lt;/code&gt; and size values&lt;/li&gt;
&lt;li&gt;deterministic handling of one-digit vs two-digit hour&lt;/li&gt;
&lt;li&gt;easy visual validation during manual triage&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;At 01:12, when you are diffing listings by eye and caffeine alone, &amp;ldquo;column 15 starts the size field&amp;rdquo; is operational mercy.&lt;/p&gt;
&lt;h2 id=&#34;header-and-footer-are-part-of-the-protocol&#34;&gt;Header and footer are part of the protocol&lt;/h2&gt;
&lt;p&gt;Many parsers only parse entry rows and ignore header/footer. That is a missed opportunity.&lt;/p&gt;
&lt;p&gt;Our profile explicitly fixed header sequence:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;volume label line (&lt;code&gt;is &amp;lt;LABEL&amp;gt;&lt;/code&gt; or &lt;code&gt;has no label&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;serial line (&lt;code&gt;XXXX-XXXX&lt;/code&gt;, uppercase hex)&lt;/li&gt;
&lt;li&gt;blank line&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Directory of &amp;lt;PATH&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;blank line&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And footer sequence:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;file totals: &lt;code&gt;%8u File(s) %11s bytes&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;dir/free totals: &lt;code&gt;%8u Dir(s) %11s bytes free&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Those two footer lines are not decoration. They are integrity checks. If parsed file count says 127 and footer says 126, stop and investigate before touching production disks.&lt;/p&gt;
&lt;h2 id=&#34;parsing-algorithm-we-actually-trusted&#34;&gt;Parsing algorithm we actually trusted&lt;/h2&gt;
&lt;p&gt;This is the skeleton we converged on in Turbo Pascal style:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-pascal&#34; data-lang=&#34;pascal&#34;&gt;type
  TDirEntry = record
    BaseName: string[8];
    Ext: string[3];
    IsDir: Boolean;
    SizeBytes: LongInt;
    DateText: string[8]; { MM-DD-YY }
    TimeText: string[6]; { right-aligned h:mma/h:mmp }
  end;

function TrimRight(const S: string): string;
var
  I: Integer;
begin
  I := Length(S);
  while (I &amp;gt; 0) and (S[I] = &amp;#39; &amp;#39;) do Dec(I);
  TrimRight := Copy(S, 1, I);
end;

function ParseEntryLine(const L: string; var E: TDirEntry): Boolean;
var
  NameField, ExtField, SizeField, DateField, TimeField: string;
  Code: Integer;
begin
  ParseEntryLine := False;
  if Length(L) &amp;lt; 38 then Exit;

  NameField := Copy(L, 1, 8);
  ExtField  := Copy(L, 10, 3);
  SizeField := Copy(L, 15, 8);
  DateField := Copy(L, 24, 8);
  TimeField := Copy(L, 33, 6);

  E.BaseName := TrimRight(NameField);
  E.Ext      := TrimRight(ExtField);
  E.DateText := DateField;
  E.TimeText := TimeField;

  if TrimRight(SizeField) = &amp;#39;&amp;lt;DIR&amp;gt;&amp;#39; then
  begin
    E.IsDir := True;
    E.SizeBytes := 0;
  end
  else
  begin
    E.IsDir := False;
    Val(TrimRight(SizeField), E.SizeBytes, Code);
    if Code &amp;lt;&amp;gt; 0 then Exit;
  end;

  ParseEntryLine := True;
end;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This parser is intentionally plain. No hidden assumptions, no dynamic heuristics, no &amp;ldquo;best effort.&amp;rdquo; It either matches the profile or fails loudly.&lt;/p&gt;
&lt;h2 id=&#34;edge-cases-that-must-be-explicit&#34;&gt;Edge cases that must be explicit&lt;/h2&gt;
&lt;p&gt;The spec was strict about awkward but common cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;extensionless files: extension field is blank (three spaces in raw row)&lt;/li&gt;
&lt;li&gt;short names/exts: right-padding in fixed fields&lt;/li&gt;
&lt;li&gt;directories always use &lt;code&gt;&amp;lt;DIR&amp;gt;&lt;/code&gt; in size field&lt;/li&gt;
&lt;li&gt;if value exceeds width, allow rightward overflow; never truncate data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The overflow rule is subtle and important. Truncation creates false data, and false data is worse than ugly formatting.&lt;/p&gt;
&lt;h2 id=&#34;counting-bytes-grouped-vs-ungrouped-is-not-random&#34;&gt;Counting bytes: grouped vs ungrouped is not random&lt;/h2&gt;
&lt;p&gt;A detail teams often forget:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;entry &lt;code&gt;SIZE_OR_DIR&lt;/code&gt; file size is decimal without grouping&lt;/li&gt;
&lt;li&gt;footer byte totals are grouped with US commas in this profile&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That split looks cosmetic until a parser accidentally strips commas in one place but not the other. If totals are part of your acceptance gate, normalize once and test it with fixtures.&lt;/p&gt;
&lt;h2 id=&#34;the-fictional-incident-that-made-it-real&#34;&gt;The fictional incident that made it real&lt;/h2&gt;
&lt;p&gt;At 02:07 in our story, we finally had a clean parse on machine A. We ran the same process on machine B, then compared manifests. Everything looked perfect except one tiny mismatch: file count agreed, byte count differed by 1,024.&lt;/p&gt;
&lt;p&gt;Old us would have guessed corruption and started copying disks again.&lt;/p&gt;
&lt;p&gt;Spec-driven us inspected footer math first, then entry parse, then source listing capture. The issue was not corruption. One listing had accidentally included a generated staging file from a side directory because the operator typed a wildcard path incorrectly.&lt;/p&gt;
&lt;p&gt;The deterministic header (&lt;code&gt;Directory of ...&lt;/code&gt;) and footer checks caught it in minutes.&lt;/p&gt;
&lt;p&gt;No drama. Just protocol discipline.&lt;/p&gt;
&lt;h2 id=&#34;what-this-teaches-beyond-dos&#34;&gt;What this teaches beyond DOS&lt;/h2&gt;
&lt;p&gt;The strongest lesson is not &amp;ldquo;DOS output is neat.&amp;rdquo; The lesson is operational:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;any text output consumed by tools should be treated as a contract&lt;/li&gt;
&lt;li&gt;contracts need explicit scope and out-of-scope declarations&lt;/li&gt;
&lt;li&gt;examples + field widths + sequence rules beat vague descriptions&lt;/li&gt;
&lt;li&gt;integrity lines (counts/totals) should be first-class validation points&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That mindset scales from floppy-era rebuild scripts to modern CI logs and telemetry processors.&lt;/p&gt;
&lt;h2 id=&#34;implementation-checklist-for-your-own-parser&#34;&gt;Implementation checklist for your own parser&lt;/h2&gt;
&lt;p&gt;If you want a stable implementation from this profile:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;enforce command profile (no unsupported switches)&lt;/li&gt;
&lt;li&gt;parse header in strict order&lt;/li&gt;
&lt;li&gt;parse entry rows by fixed columns, not token split&lt;/li&gt;
&lt;li&gt;parse footer totals and cross-check with computed values&lt;/li&gt;
&lt;li&gt;fail explicitly on profile deviation&lt;/li&gt;
&lt;li&gt;keep canonical fixture listings in version control&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This gives you deterministic behavior and debuggable failures.&lt;/p&gt;
&lt;h2 id=&#34;closing-scene&#34;&gt;Closing scene&lt;/h2&gt;
&lt;p&gt;At 03:18 we printed two manifests, one from recovered media and one from archive baseline, and compared them line by line. For the first time that night, we trusted the result.&lt;/p&gt;
&lt;p&gt;Not because the room got quieter.&lt;br&gt;
Not because the disks got newer.&lt;br&gt;
Because the contract got clearer.&lt;/p&gt;
&lt;p&gt;The old DOS prompt did what old prompts always do: it reflected our discipline back at us.&lt;/p&gt;
&lt;p&gt;Related reading:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://turbovision.in6-addr.net/retro/dos/batch-file-wizardry/&#34;&gt;Batch File Wizardry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://turbovision.in6-addr.net/retro/dos/config-sys-as-architecture/&#34;&gt;CONFIG.SYS as Architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://turbovision.in6-addr.net/retro/dos/interrupts-as-user-interface/&#34;&gt;Interrupts as User Interface&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
  </channel>
</rss>
