|
| 1 | +# openkal 0.4: what one portable program found, and what it says about the method |
| 2 | + |
| 3 | +This document records the second round of work on openkal: the writing of a |
| 4 | +program above all eight interfaces, the two defects in the specification it |
| 5 | +exposed within minutes, the three defects in continuous integration it exposed |
| 6 | +alongside them, and the conclusions about method that follow. It continues |
| 7 | +[`2026-08-20-openkal-implementation-plan.md`](2026-08-20-openkal-implementation-plan.md) |
| 8 | +and [`2026-08-20-openkal-design.md`](2026-08-20-openkal-design.md). |
| 9 | + |
| 10 | +## 1. What was published |
| 11 | + |
| 12 | +| Repository | Version | Change | |
| 13 | +| --- | --- | --- | |
| 14 | +| `mcpplibs/openkal` | 0.4.0 | clauses 7.6 and 7.7; `examples/portable` | |
| 15 | +| `mcpplibs/openkal-linux` | 0.4.0 | argv passed unaltered; an assertion that observes it; three CI defects | |
| 16 | +| `mcpplibs/openkal-macos` | 0.2.0 | the same, and CI that had never run a step it appeared to run | |
| 17 | +| `mcpplibs/openkal-libc` | 0.2.0 | its tests given an implementation; its example asserted against `wc` | |
| 18 | +| `mcpplibs/mcpp-index` | PR #223 | four descriptors, merged and propagated | |
| 19 | + |
| 20 | +No declaration changed, so `SURFACE.txt` is unchanged and an implementation |
| 21 | +conforming to 0.3 exports exactly what 0.4 requires. What changed is behaviour |
| 22 | +the earlier version left open. |
| 23 | + |
| 24 | +## 2. The two defects in the specification |
| 25 | + |
| 26 | +Both were found by writing one program against the specification and running it. |
| 27 | +Neither was visible in the specification text, and neither would have been found |
| 28 | +by reading it more carefully. |
| 29 | + |
| 30 | +### 2.1 The argument vector (clause 7.6) |
| 31 | + |
| 32 | +`kal_process_spawn` takes a path and an argument vector. Version 0.3 did not say |
| 33 | +whether the vector includes the started program's own name. Both implementations |
| 34 | +prepended the path, so a caller's `argv[0]` arrived at index 1. |
| 35 | + |
| 36 | +The defect surfaced as a shell reporting `cannot open sh`: given |
| 37 | +`argv = {"sh", "-c", "exit 0"}`, the prepending implementation delivered |
| 38 | +`{"bin/sh", "sh", "-c", "exit 0"}`, and the shell read `sh` as a script to open. |
| 39 | + |
| 40 | +The rule adopted is that the vector is complete and is passed unaltered. Three |
| 41 | +reasons, in order of weight: |
| 42 | + |
| 43 | +1. The two sides must agree. A started program reads its own name through |
| 44 | + `kal_env_arg(0)`, which does include it. A caller that did not supply it could |
| 45 | + not predict what the program would read. |
| 46 | +2. The name a program observes as its own is behaviour on every environment that |
| 47 | + has an argument vector. The choice belongs to the caller. |
| 48 | +3. It is what every adjacent interface does — `posix_spawn`, `fdio_spawn`, and |
| 49 | + the conventional form of `CreateProcess`. The outlier needed a reason and did |
| 50 | + not have one. |
| 51 | + |
| 52 | +⭐ **Both implementations agreed, and their agreement was worth nothing.** They |
| 53 | +were written by one author from one reading. A second implementation by the same |
| 54 | +author tests less than a second implementation by another; this is the limit of |
| 55 | +what the two can establish between them, and it is now recorded in the macOS |
| 56 | +implementation's own history so that the limit is not mistaken for evidence. |
| 57 | + |
| 58 | +### 2.2 Absence as an answer (clause 7.7) |
| 59 | + |
| 60 | +`kal_fs_info` on a name that does not exist: version 0.3 declared |
| 61 | +`kal_node_absent` and did not say when it is reported. The implementations |
| 62 | +return `kal_ok` with `kind = kal_node_absent`. A third implementer could as |
| 63 | +reasonably have returned an error, and a portable program cannot be written |
| 64 | +against a point on which two conforming implementations may differ. |
| 65 | + |
| 66 | +The rule adopted is that enquiry succeeds and reports absence, while opening the |
| 67 | +same name reports `kal_err_not_found`. Enquiry and access are different |
| 68 | +operations: a caller that asks what a name refers to has been answered when told |
| 69 | +that it refers to nothing. It is the same distinction `openkal.env` already draws |
| 70 | +between a variable that is absent and one whose value is empty. |
| 71 | + |
| 72 | +⚠️ **The test I wrote first was wrong, and the implementation was right.** I |
| 73 | +asserted that enquiry after removal fails. It succeeded, and my first reading was |
| 74 | +that removal had not worked. The file was gone from the disk. Had I trusted the |
| 75 | +assertion over the artefact, I would have "fixed" a correct implementation. |
| 76 | + |
| 77 | +## 3. The three defects in continuous integration |
| 78 | + |
| 79 | +All four pipelines were green before this round, on packages containing the two |
| 80 | +defects above. They share one shape. |
| 81 | + |
| 82 | +### 3.1 A fact stated twice, which drifted |
| 83 | + |
| 84 | +`openkal-linux`'s workflow pinned `OPENKAL_VERSION: 0.2.0` while its manifest |
| 85 | +named 0.3.0. The surface comparison therefore fetched the 0.2 list, and reported |
| 86 | +the four `kal_time_*` names — which that implementation is *required* to export — |
| 87 | +as unspecified. The step's own comment said the list was fetched "so that the |
| 88 | +comparison has one source rather than a copy that can drift", and the version |
| 89 | +selecting it was the copy that drifted. |
| 90 | + |
| 91 | +The fix is to read it from the manifest, which is where it is declared. |
| 92 | + |
| 93 | +### 3.2 An assertion naming a subset |
| 94 | + |
| 95 | +Each implementation asserted that `conformance_stream` and `conformance_memory` |
| 96 | +had run. Five suites were present. The assertion exists precisely because *a |
| 97 | +suite that discovered nothing reports success* — and a hand-written list defeats |
| 98 | +that purpose the moment a suite is added. |
| 99 | + |
| 100 | +The list is now derived from `tests/*.cpp`. ⭐ The general form: **an assertion |
| 101 | +about coverage must be derived from what exists, not from what existed.** |
| 102 | + |
| 103 | +### 3.3 A step that had never run |
| 104 | + |
| 105 | +Every implementation's final step ran an example and grepped for |
| 106 | +`openkal: vectored writes unavailable` — a line from a 0.1-era optional |
| 107 | +operation that no example prints. In `openkal-macos` and `openkal-libc` it also |
| 108 | +named a directory the repository does not contain. |
| 109 | + |
| 110 | +It had never passed, and nobody could have noticed: the step before it failed |
| 111 | +first, so it was never reached. ⚠️ **A pipeline whose steps fail in order hides |
| 112 | +every later defect, and reports the first as though it were the only one.** |
| 113 | +`openkal-macos`'s failure was reported as an inability to start `/bin/bash` in a |
| 114 | +working directory, which reads as an infrastructure fault rather than as a |
| 115 | +missing example. |
| 116 | + |
| 117 | +### 3.4 The one that only linking could find |
| 118 | + |
| 119 | +`openkal-libc`'s three test suites all failed to link, naming sixteen undefined |
| 120 | +operations. The package declares against the interface and links against no |
| 121 | +implementation — correct for the package, wrong for its own tests, which run. |
| 122 | + |
| 123 | +This is the first occasion on which clause 4.2's claim (that a missing |
| 124 | +implementation is reported by the linker, late but legible) was tested by |
| 125 | +something other than a demonstration built to test it. The claim held: the |
| 126 | +diagnostic named the operations, and the fix was one manifest section. |
| 127 | + |
| 128 | +## 4. The portable program |
| 129 | + |
| 130 | +`openkal/examples/portable` exercises the eight interfaces and prints one line |
| 131 | +per observation plus a count of those that did not hold. Each implementation's |
| 132 | +continuous integration checks out the specification at the version its manifest |
| 133 | +names and builds the program from there. **The program is fetched, never |
| 134 | +copied** — a copy in each implementation is a copy that can diverge, and the |
| 135 | +whole value of the program is that it does not. |
| 136 | + |
| 137 | +Two properties were designed in after the first version had neither. |
| 138 | + |
| 139 | +**It does not start itself.** The first version spawned its own executable with a |
| 140 | +`--child` marker. The marker did not arrive — because of the argv defect the |
| 141 | +program existed to find — and the program started itself without end. ⚠️ **A |
| 142 | +conformance program must not have an unbounded failure mode, and least of all one |
| 143 | +armed by the defect it is looking for.** It now starts a shell, and reports the |
| 144 | +observation as unobservable if the environment supplies no directory to start it |
| 145 | +from. |
| 146 | + |
| 147 | +**It asserts in both directions.** The workflow requires the summary line *and* |
| 148 | +`observations that did not hold: 0` *and* the absence of `NOT HELD`. Asserting |
| 149 | +only that the program reported would pass for a program that reported failures. |
| 150 | + |
| 151 | +## 5. Method |
| 152 | + |
| 153 | +⭐ **The specification's silences are not visible in the specification.** Two |
| 154 | +rules were missing. Reading the text, twice, over two rounds, found neither. |
| 155 | +Writing one program and running it found both in under an hour. Clause 9 requires |
| 156 | +a conformance procedure for this reason, and the requirement was under-served by |
| 157 | +a suite that started programs without observing what they received. |
| 158 | + |
| 159 | +⭐ **A test that does not observe the thing cannot detect the thing.** The suite |
| 160 | +started `/bin/true` and read its status. `/bin/true` ignores its arguments, so |
| 161 | +the same status was produced whether the vector arrived intact or shifted. The |
| 162 | +replacement starts a shell, whose behaviour depends on the vector. |
| 163 | + |
| 164 | +⚠️ **Green is a property of the assertions, not of the software.** Four |
| 165 | +pipelines were green across two published packages containing an ABI defect, a |
| 166 | +CI comparison against the wrong version, an assertion covering two of five |
| 167 | +suites, and a step that had never executed. |
| 168 | + |
| 169 | +⚠️ **The revert probe is what separates an assertion from a decoration.** The new |
| 170 | +argv assertion was confirmed to fail against the previous behaviour before the |
| 171 | +behaviour was changed. On restoring the fix it still failed — because `mv` |
| 172 | +restored an older mtime and nothing rebuilt. Had I read that as "the fix does not |
| 173 | +work", I would have chased a defect that was not there. |
| 174 | + |
| 175 | +⚠️ **One edit, several places.** Writing the index descriptors, a regex that |
| 176 | +matched the first version block updated one of three platform tables per file, |
| 177 | +leaving `openkal-macos` advertising the new version for Linux and the old one for |
| 178 | +macOS. It parsed, and it would have installed. The check that caught it enumerated |
| 179 | +what the file *says* rather than what the edit *intended*. |
| 180 | + |
| 181 | +## 6. What remains |
| 182 | + |
| 183 | +| Matter | State | |
| 184 | +| --- | --- | |
| 185 | +| A third implementation by another author | the gate that governs everything beyond; unchanged, and now with a specification worth implementing against | |
| 186 | +| `openkal.net`, `openkal.channel` | reserved; no program above the stack needs them yet | |
| 187 | +| Symbol versioning | clause 8 protects the interface by prohibiting change; two rules were added by prohibition-compatible means this round, which does not prove the next will be | |
| 188 | +| A shared conformance package | the portable program is now the shared artefact in practice; formalising it needs a way for a test package to be compiled against an implementation a third project chooses | |
0 commit comments