Skip to content

Land assertion 4, and close three Tier 1 capabilities on the record - #1

Merged
samuelduchesne merged 13 commits into
mainfrom
002-close-tier1-partials
Sep 4, 2026
Merged

Land assertion 4, and close three Tier 1 capabilities on the record#1
samuelduchesne merged 13 commits into
mainfrom
002-close-tier1-partials

Conversation

@samuelduchesne

Copy link
Copy Markdown
Contributor

Feature 002. The record and the proof half; the two libraries and the site are three separate pull requests.

Merge this one first. The other three pin governance-2026.10 and conformance-2026.8, which do not exist until this merges and the tags are cut from main. Their CI is red until then, and that is expected rather than a defect in them.

What this does

Three Tier 1 capabilities move to complete on both sides and all three lose their differences text. The count of Tier 1 capabilities marked partial falls from 6 to 3, and the three that remain are the three this feature never touched.

Capability Before After
introspection complete / partial complete / complete
write partial / partial complete / complete
parse-diagnostics partial / complete complete / complete

Assertion 4 ships

It was declared, deferred and skipped by both runners from the day the corpus was written, because it was the one assertion needing changes inside the libraries. Both now carry a shared code, so it runs. Nothing is skipped any more: every assertion every case declares is evaluated.

It compares an unordered multiset of (code, line, typeName) and never message text. A multiset rather than a set, because two skips of the same type at different lines are two findings, and collapsing them would hide the exact bug it exists to catch.

Eight new cases

  • two introspection cases: field order in 9.4.0, twelve releases back, where the existing case could not see an ordering bug because both its types have a positional list to order by
  • two parse-diagnostics cases: the raising and returning paths, from one input read both ways, so a difference between them is a difference between the paths
  • five writer-option cases: one per control, so a failure names the control that broke rather than reporting one failure for five possible causes

Every expectation for assertion 4 is hand-authored against the rule in compare.md, with the line numbers read off the file. seed_tier1.py does not cover assertion 4, and an expectation drafted from one library would make that library the oracle.

What assertion 4 found immediately

Three divergences, all now recorded against real tracking issues:

Case Who Issue
malformed-stray-comment python idfkit/idfkit#190
malformed-missing-semicolon both idfkit/idfkit#191, idfkit/idfkit-js#36
malformed-truncated-object python idfkit/idfkit#187, the existing one

The missing-semicolon pair is the shape FR-015 exists to catch: both libraries agree and both differ from the oracle, so no comparison between them could have found it.

Things the record got wrong, corrected here

  • parse-diagnostics said Python raises and TypeScript returns. Neither ever did: both default to strict and both raise. The text described the non-strict mode while reading as though it described the default.
  • introspection claimed a @idfkit/schemas/docs subpath no header comment promises, and a field-order count wrong in both halves.
  • five references still pointed at idfkit/docs/, which feature 003 emptied.
  • known-divergence.toml's header count was stale.
  • governance/README.md has always said CI runs the validation rules on the pull request. It did not: .github/ held only CODEOWNERS. tools/validate_governance.py and a workflow now do, so a malformed entry cannot reach an immutable tag unchallenged.

Gaps recorded rather than closed

Python's epJSON parser builds no diagnostics at all and drops malformed objects with a bare continue, where TypeScript's builds them. parse-diagnostics is therefore narrowed in title to the IDF parser, and the gap is recorded in a comment above the entry, because differences is deleted when a capability stops being partial and the note has to outlive the closure.

Review

Both languages, per FR-067, and FR-091 gives it no override. Everything under governance/ and cases/ is in scope.

After merge: cut governance-2026.10 and conformance-2026.8 from the merge commit. Both are currently cut locally at this branch tip and must be re-cut, per the five-step checklist in governance/README.md.

Verification

56 cases, 148 assertions, 0 skipped. Both runners exit 0: python 132 passed with 16 allowlisted, typescript 141 passed with 7 allowlisted, and nothing unexplained on either side.

…ot wrong

Feature 002 takes three Tier 1 capabilities to full parity. Every public name
it adds is registered here first, at a tag, before either library ships it.
This is that half: no library behaviour changes in this commit.

The register gains what US3 needs. `read IDF from disk, keeping diagnostics`
gains a Python name and becomes aligned; giving a name to a field that was ""
is an addition rather than a rename, so rename_count.python stays 0. The type
the returning path hands back is registered as its own concept. `diagnostics
from a parse` was recorded as divergent on the grounds that Python raises and
TypeScript returns; that was never what the two did, since both default to
strict and both raise, so the entry describes what actually differed and is
now aligned.

`a parse diagnostic` claimed a severity that neither implementation has ever
carried. Corrected rather than implemented: adding a field to match a note
nobody had checked is the wrong direction.

Four corrections the record needed anyway. Five references still pointed at
idfkit/docs/, which feature 003 emptied. known-divergence.toml's header
claimed 22 entries over 47 cases; the file holds 20 over 48, recounted rather
than adjusted.

parse-diagnostics is narrowed to the IDF parser, and the reason is recorded
where the closure cannot delete it. TypeScript's epJSON parser builds
diagnostics; Python's builds none and drops malformed objects with a bare
continue at epjson_parser.py:226 and :246. That gap is real, is not closed
here, and would have been swallowed by a complete/complete move, because the
gate deletes `differences` when a capability stops being partial. It lives in
a comment above the entry for that reason.

governance/README.md has always said CI runs the validation rules on the pull
request. It did not: .github/ held only CODEOWNERS, so a malformed entry could
reach an immutable tag unchallenged. tools/validate_governance.py implements
the rules the README lists, plus the one that forces a closure to drop its
differences text in the same edit, and a workflow runs it. It never imports
either library, so it cannot go red because a library drifted.

The writer options object is NOT registered, and T011 is dropped. The register
already excludes Python options objects, excluded is terminal under FR-006,
and check_excluded_counterparts enforces it. US4's three controls land as
keyword arguments instead, so no new public name ships.
check-parity-ledger.mjs rejected the register edit: ParseResult is exported
from @idfkit/core under the new concept and no capability claimed it. It
belongs to parse-diagnostics, alongside the other two diagnostics concepts.
check-naming-register.mjs read the previous commit as dropping a registered
TypeScript name: 'diagnostics from a parse' pointed at ParseResult.diagnostics
and now pointed at IdfParseError.diagnostics, with rename_count unchanged.
FR-079 is right to stop that.

Nothing was dropped from either library. After feature 002 both languages
carry parse diagnostics in both places, so the entry names both on both sides.
The old pair crossed the two paths, which was the divergence this entry used
to record.
FR-005 allows one name per concept per language, so the previous commit's two
names per side is not available. The registered pair crossed the two paths,
Python's error against TypeScript's result, and that crossing was the shape of
the divergence the entry recorded. It cannot stay once both languages carry
both.

Split into one concept per carrier, which makes both alignable. 'diagnostics
from a parse' is the error's collection and its TypeScript name moves from
ParseResult.diagnostics to IdfParseError.diagnostics, spending that name's one
rename. 'recoverable diagnostics from a parse' is the result's and keeps the
TypeScript name it has always had.

The alternative mapping costs the same one rename against Python instead, on a
name that already exists rather than one landing in this feature.
…e the record

The library populated all three families the ledger recorded, so the
tier1-introspection-field-shapes exception went stale and the runner failed on
it, which is what that entry was built to do. It recorded no bug: it pinned a
divergence the ledger declared so the ledger's prose could not quietly stop
being true. Removed here, in the change that closes the capability.

The JavaScript runner now loads the prose pool and passes it to
describeObjectType. TypeScript keeps that function synchronous and takes the
pool as an argument, so prose is opt-in for the caller; this runner is a caller
that wants the whole description, so it opts in. A checkout that ships no pool
compares without prose rather than erroring, which is what a pre-002 checkout
should do.

tier1-introspection-field-order is new, and covers what the existing case could
not. That case compares two types in the newest schema, and both have a
positional field list to order by, so it cannot see an ordering bug. The new
one takes the three types that have none, in 9.4.0 rather than 26.1.0, because
field order comes from per-version schema data and a rule that holds in the
newest release can be wrong twelve releases back. SolarCollector is included as
a control: it agreed by alphabetical accident, so a fix that worked by
accident rather than by recording the order would break it.

Verified the case discriminates before trusting it: restoring the removed
exception fails both runners, removing it passes them. A case that passes
either way proves nothing.

introspection moves to complete on both sides and loses its differences text,
which the gate requires and which nothing here needs to survive. Two claims in
that text were false and are recorded in a comment rather than dropped in
silence: no header comment ever promised an @idfkit/schemas/docs subpath, and
the field-order count was three types in every version rather than two in one
plus six in the old releases.

49 cases, 134 assertions, both runners green.
Assertion 4 was declared, deferred, and skipped by both runners since the
corpus was written. It was the one assertion that needed changes inside the
libraries, and feature 002 made them, so it runs now. Nothing is skipped any
more: every assertion every case declares is evaluated.

The comparator takes an unordered multiset of (code, line, typeName) and never
message text. Unordered because the order findings are noticed in is an
implementation detail of the scan that neither library promises. A multiset
rather than a set because two skips of the same type at different lines are
two findings, and collapsing them would hide the exact bug this assertion
exists to catch.

Two paths, because both libraries raise by default and both return findings
when asked not to. `diagnostics` compares whichever path the input takes;
`recoverable` names the returning path explicitly, which a case needs when its
input fails a strict read and the recoverable behaviour is still worth
checking.

Two new cases, sharing one input read both ways, so a difference between them
is a difference between the paths rather than between two files that happen to
be shaped differently. Their expectations are hand-authored against the rule in
compare.md, and the line numbers were read off the file: seed_tier1.py does not
cover assertion 4, and an expectation drafted from one library and committed
unread would make that library the oracle.

WHAT ASSERTION 4 FOUND, immediately, which is the point of building it:

  malformed-truncated-object   Python reports no finding at all, because
                               _OBJECT_PATTERN matches only text reaching a
                               semicolon. Same root cause as this case's
                               already-accepted parse-outcome divergence, same
                               issue 187, and recorded alongside it.

  malformed-stray-comment      Python reports the type name as "a block
                               comment" where TypeScript reports "/* a block
                               comment". The regex requires a type to start
                               with a letter, so the scanner never saw the
                               "/*". NOT YET TRACKED: needs an issue.

  malformed-missing-semicolon  Neither library reports anything where the
                               oracle diagnoses an invalid field value. The
                               case predicted this silence in its own why.
                               Both libraries agree, so it is not a parity gap;
                               it is a shared gap against the convention.
                               NOT YET TRACKED: needs an issue.

The last two are why both runners still exit 1, and why parse-diagnostics has
NOT been moved to complete in this commit. The record moves when the corpus
can say the two agree, not before.
Five corpus cases, one per control, so a failure names the control that broke
rather than reporting one failure for five possible causes. Each writes its
document under the control, re-reads it, and compares parsed values. The
comparison is structural because it has to be: the two writers differ on seven
defaults that no control removes, so comparing text would fail these cases for
reasons that have nothing to do with the control being exercised.

The case schema gains an optional writer_options block, language-neutral, which
each runner maps onto its own writer's option names. Absent on all 48
pre-existing cases and it invalidates none of their expectations.

Verified the controls are not no-ops before trusting the cases: writing the
same document with and without each control changes the output for all five in
the first language and for four of five in the second. The exception is
ordering=source, which is already that writer's default, and the case says so
rather than reading as a check it is not.

write moves to complete on both sides and loses its differences text. The seven
measured default-output differences move to a page, because the gate rejects a
differences field on a capability that is not partial and a reader still meets
them the first time they diff two files. The measurement was re-taken on the
day it closed and still reads 359 objects, 4,031 lines against 4,125.

checks/ is still not created. Every claim here is document-shaped.
The known-divergence header was recounted in this feature and then went stale
again when eight cases landed: it said 13 cases on a 51-case corpus, and the
file holds 12 on a corpus of 56 with 148 assertions.

README.md still said both runners exit 0 because every failure is allowlisted.
They exit 1, which is the blocker this feature records against parse-diagnostics,
and the README now names the two divergences that are not recorded. Its case
count and timing table were stale for the same reason.

governance.yml had a step called 'Validate the case manifest schema is well
formed' that parsed two JSON files and printed their sizes. It now runs
load_corpus, which checks every manifest entry against its case directory,
which is what T015 added the workflow for.

naming.toml said the returning loader pairs with strict_parsing=False. It takes
no such argument, and the register is read at an immutable tag, so the wrong
text is what the site would render. The location-field claim is narrowed to
what the library actually sets.

parity.toml's write entry now records the two controls that remain spelled on
one side only, ordering and versionFirst, and why complete is still the right
availability: neither gives a caller access to output the other writer cannot
produce.

run.py's dispatcher docstring still said assertion 4 was accepted and skipped.
Three tracking issues were opened for the divergences assertion 4 found, so
they can be recorded rather than left to fail the run: idfkit 190 for the
tokenizer's type-name capture, and idfkit 191 with idfkit-js 36 for the shared
silence where both libraries accept a type name in a numeric field and neither
says anything. Both runners are green.

parse-diagnostics moves to complete on both sides and loses its differences
text. Four diagnostics divergences remain allowlisted and complete is recorded
knowing it, on the same footing as parse and write, which carry fifteen and
four between them. The field records whether a capability is implemented in
both languages; the corpus records the bugs, each against a tracked issue.

Its removed prose said Python raises and TypeScript returns. Neither library
ever did that, so the text goes as history rather than being amended.

That is the third of three closures. Tier 1 capabilities marked partial fall
from 6 to 3, which is what SC-011 asks for, and the three that remain are the
three this feature never touched.

manifest.json declares corpus_level for the first time. It has been null since
the corpus was written, which meant the case set was always ahead of the level
both libraries pinned.
Both libraries now report InvalidField for a value of the wrong kind, so the
two entries went stale and the runners failed on them, which is what a stale
entry is for. Removed in the change that fixed the bug, per the file's own
rule: an entry is a bug with a tracked resolution, and the change that fixes it
removes the entry.

21 entries over 13 issues and 13 cases. Both runners exit 0.
idfkit#190 is closed, so the entry went stale and the runner failed on it.
Removed in the change that fixed it.

20 entries over 12 issues and 12 cases. Both runners exit 0, and the only
malformed divergences left are the two halves of issue 187.
Adds difference_kind, difference_issue and difference_note, the vocabulary
withdrawn with feature 002's User Story 1. The spec said plainly what withdrawing
it cost: three capabilities stay partial with no statement of whether their
differences are temporary, so a reader cannot tell whether to write one into
their own code or wait for it to go away. That is the question they actually
have, and differences alone does not answer it.

The words are absence_kind's, not new ones. A reader who has learned that
'never' is terminal for an absent capability should not learn a second word for
the same idea about a partial one. not-yet carries a real tracking item on the
same terms as issue; never carries a reason and is terminal, movable only by
constitutional amendment.

Separate fields rather than reusing issue and note, because a capability can be
partial on one side and absent on the other, and one pair could not then say
which state it was describing.

The three partial entries, and the reasoning is theirs rather than mine:

  weather-index      never. The packaging is deliberate, since pip extras gate
                     dependencies rather than files while npm can opt out, and
                     the freshness nudge needs a writable cache a browser
                     package has none of.
  weather-download   never. Selective extraction is a property of writing into
                     a cache; the JavaScript side has no disk, decodes in
                     memory, and already holds every member.
  generated-object-types  not-yet, tracked at #194. The coverage half closes,
                     and feature 002's withdrawn User Story 7 already measured
                     it at 8 to 9 percent rather than seventeen copies. The
                     application half is each language's type system and stays.

tools/validate_governance.py enforces it, and rejects the fields on an entry
that is no longer partial for the same reason it rejects a stale differences.

Neither library's parity gate rejects unknown keys, so this is additive to both
and reaches them only when they advance their pin.
@samuelduchesne
samuelduchesne merged commit 0951098 into main Sep 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant