Skip to content

Commit 5657410

Browse files
committed
docs(openkal): what one portable program found, and what it says about the method
A program above all eight interfaces was written and run. It exposed two points on which the specification was silent — whether the argument vector supplied to a spawn includes the started program's own name, and how enquiry reports a name that does not exist — and on both, two conforming implementations could have differed. Both were found in under an hour. Neither had been found by reading the text, twice, across two rounds. It exposed three defects in continuous integration alongside them, sharing one shape: a version stated twice and drifted, an assertion naming two of five suites, and a final step that had never executed because the step before it failed first. All four pipelines were green throughout. The document also records what the program had to be redesigned to avoid. Its first version started itself with a marker to distinguish parent from child; the marker did not arrive, because of the very defect the program existed to find, and it started itself without end. A conformance program must not have an unbounded failure mode, and least of all one armed by the defect it looks for.
1 parent 747154f commit 5657410

2 files changed

Lines changed: 196 additions & 0 deletions

File tree

.agents/docs/2026-08-20-openkal-implementation-plan.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ specification itself, which is maintained in the `mcpplibs/openkal` repository.
1414
| `mcpplibs/openkal-linux` | the reference implementation for Linux, its conformance suite, and an example | 0.2.0 |
1515
| `mcpplibs/mcpp-index` | descriptors for both packages | pull requests 220 and 221 |
1616

17+
⚠️ **This table records 0.2 and is not the current state.** The specification is
18+
at 0.4.0 and covers eight interfaces; the implementations are `openkal-linux`
19+
0.4.0, `openkal-macos` 0.2.0 and `openkal-libc` 0.2.0. See
20+
[the portable-program findings](2026-08-20-openkal-portable-program-findings.md)
21+
for what 0.4 changed and why, and
22+
[the completeness plan](2026-08-20-openkal-completeness-plan.md) for the
23+
interfaces 0.3 added.
24+
1725
Both packages are mirrored to GitCode, and the mirrored archives were verified
1826
to be byte-identical to those served by GitHub.
1927

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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

Comments
 (0)