Repository navigation
Release 0.5.1: reserved-file projection, lock export and version constants - #69
Conversation
…tants
OkfBundleChangeApplier.prepare takes an optional OkfReservedFileProjection.
After the change set is overlaid, the projection receives the inspected
source, the candidate, the change set's files and its date, and returns
texts for index.md and log.md paths; null keeps the source text. Any other
path throws OkfBundleChangeException. The projected texts are validated
with the rest of the candidate, so Spec-invalid output is refused, and the
commit plan binds exactly those bytes to the same source snapshot, so a
stale source is still refused at commit. OkfPreparedCandidate.toLoadResult()
reads a candidate as a later load would, keeping load findings for files
the candidate does not replace.
okf_io.dart exports OkfBundleLock. Its documentation and the README state
the rule its fcntl lock implies: run every operation on a bundle in one
isolate per process.
okf.dart exports okfVersion, replacing the internal okfPackageVersion, and
okfIndexGeneratorRevision ('0.5.0'), which changes only in a minor release.
ADR-0009 records both decisions; a golden pins the generator's bytes to the
revision.
A new test compiles a file that imports only okf.dart to JavaScript. dart2js
stubs dart:io instead of failing, so the same test also walks okf.dart's
imports and fails on any native library.
The CHANGELOG also lists the changes merged since 0.5.0 (#62, #63, #65,
#66), and the README's SDK floor now matches #62.
OkfPreparationRefused gains an optional candidate, which prepare always sets to the candidate it judged, after any projection. A caller that shows a refused write, such as a diff of what a projection produced, no longer has to rebuild it from OkfBundleLoadResult's constructor. The field is an optional named parameter, so the public const constructor keeps compiling. prepare now throws OkfBundleChangeException for a text that holds an unpaired surrogate. utf8.encode writes such a surrogate as U+FFFD, so the validator and toLoadResult judged one string while commit wrote another. The check covers the change set's own texts as well as the projection's: the same gap existed there before the seam. apply takes the same project argument and passes it to prepare, so an atomic projected write needs no hand-held OkfBundleLock. The projection's documentation now says that dropping the index of a directory the change set adds leaves the regenerated ancestors linking to it, because the Spec validator does not check index link targets.
0.5.1 exports OkfBundleLock, and the design that asked for the export asked for an ADR recording its rule, not only dartdoc and README text. ADR-0009 now says the lock stays the fcntl record lock okf has taken since 0.4.0, that the lock belongs to the process, so closing any descriptor of the lock file drops every claim the process holds, and that every okf operation on a bundle therefore runs on one isolate per process. Claims nest through a zone value, so work an action leaves running, such as an unawaited future or a timer, still counts as holding the claim after the lock is released. The class documentation and the README now tell callers to await every bundle operation inside the action.
ADR-0009 amends ADR-0003, so ADR-0003's status names it, as ADR-0001 and ADR-0006 name theirs. The release process pointed at test/ci_gate_test.dart, which does not exist; test/ci/gate_test.dart and test/cli_test.dart are the checks that the versions agree. It also says when okfIndexGeneratorRevision changes: only with OkfIndexGenerator's output, and only in a minor release.
dart2js stubs dart:io instead of rejecting it, so the compile alone cannot fail on a native import. The test now runs the compiled entry with node when node is on PATH, so a stub reached at run time fails, and skips that run otherwise. The entry's comment no longer claims the compile catches dart:io; the import walk does. The walk read only the first URI of each directive, so a configurable import's if (dart.library.io) alternative went unseen. It now follows every URI, and a test pins that. The compile moves into a group's setUpAll shared by both checks, with a library-level timeout instead of a trailing named argument.
A host that cites a concept as path:start-end needs the file line of each body line. Inferring it by assuming the body is a suffix of the file text fails for CRLF files, whose body parse has normalized, and searching the file for body text can land in a frontmatter that repeats it. OkfDocument.parse now records bodyStartLine, the 1-based source line on which the body begins: line n of body is line bodyStartLine + n - 1 of the source. Lines are counted as parse reads them, LF, CRLF and a lone CR each ending one, and a byte order mark adds none. Without frontmatter it is 1; otherwise it is the line after the closing delimiter, or after the blank separator line parse drops. Documents built with the constructor or copyWith have no source, so it is null for them. The portable entry's JavaScript run checks it, and tests cover no frontmatter, an empty body, CRLF and lone CR endings, a byte order mark, blank lines around the frontmatter, a body that repeats the frontmatter, and a loaded concept.
A body that ends with a line break splits at \n into a trailing empty element. A host that computed a citation's end from that split would cite one line past the end of the file. The dartdoc and the README now say that a final line break ends the body's last line rather than starting another, so a body of k lines ends on line bodyStartLine + k - 1, the source's last line, and the document tests check that rule for every source they cover.
An OkfPreparedCandidate keeps concept texts and parses them in toBundle and toLoadResult, so its concepts record where their bodies start. A test now pins that for a created and an updated concept, so a refactor that hands out documents without parsing them cannot drop the value unnoticed, and the CHANGELOG names prepared candidates beside loaded concepts.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Co-authored-by: Leo Farias <leoafarias@users.noreply.github.com>
Co-authored-by: Leo Farias <leoafarias@users.noreply.github.com>
Co-authored-by: Leo Farias <leoafarias@users.noreply.github.com>
Co-authored-by: Leo Farias <leoafarias@users.noreply.github.com>
ADR-0009 claimed that the log entries describing concept changes keep one owner. They do not: a projection may replace log.md wholesale, rewording or dropping the entry the change set recorded, or leave a change with no entry at all. The Spec requires none, since a log.md MAY appear (§9). The ADR now says that only concept files keep one writer, the overlay, and that okf does not enforce its own log entries: the closed validator judges a projected log by its §9 structure alone. A new consequence records that okf's entries are the change set's default, not a guarantee.
ADR-0004 says the index/log round trip holds by construction, because the change set emits every index and log text through one model. A projected text is a raw string from the caller, so for projected files the round trip now holds because preparation validates the candidate. ADR-0004's status and the ADR index record the amendment the way ADR-0003's is recorded, ADR-0009 lists both ADRs it amends, and its decision says what changes for ADR-0004. The index now notes ADR-0003's amendment as well, so the two rows read alike.
§8 and §9 define index.md and log.md largely by example, and the closed validator reads the examples as the required structure. The conformance matrix recorded that only as "follows the index/log structure", so a producer writing its own reserved files could not tell which indexes and logs the Spec allows that okf refuses. The matrix gains a reserved-file readings section that names each rule that goes beyond the Spec's text, says what it requires and what the Spec says instead, lists the rules that restate the Spec, and states that validation does not check index completeness. The README's findings section summarizes it with the rule IDs. No rule changes.
The version.dart documentation, the CHANGELOG, the README and ADR-0009 called the revision "the release that last changed" the generator's output bytes. 0.5.0 left the generator untouched, so the value is the baseline the promise starts from, not the last change. They now say that every release with the same revision generates the same indexes for the same bundle, that 0.5.0 is the baseline, and that a later change ships only in a minor release, which sets the revision to its own version. The release checklist points at the golden test that ties the output to the revision, which stays as it is.
prepare refuses a text that holds an unpaired surrogate, but OkfBundleWriter.writeAll and writeDocument still passed such a text to utf8.encode, which writes U+FFFD in its place. okf format and okf index write through writeAll, so the bytes on disk could differ from the text the caller passed. The writer now checks every text where it already checks every path, before it creates the root, takes the lock or writes a file, and throws ArgumentError as it does for an invalid or repeated path. Check mode refuses too, rather than reporting against bytes it would never write, and the internal commit transaction runs the same check. prepare keeps its own check, so a change set still fails as a tool error at preparation, and both now share one predicate that scans for unpaired surrogates instead of encoding and decoding the text. Tests cover writeAll in both modes, writeDocument, the transaction and the MCP write verbs. Texts okf format and okf index read from disk cannot hold an unpaired surrogate, because decoding is strict and YAML rejects surrogate escapes, so they rely on the writer's check.
prepare's refusal carries the candidate it refused, but apply dropped it and returned only the validation, so a caller that applies in one step, such as the MCP server, could not show what a refused change would have written. OkfBundleApplicationRefused gains the same optional candidate, which apply always sets from prepare's refusal. Both fields stay nullable: the constructors were public in 0.5.0, so a required field would break callers in a patch release. Their documentation now says prepare and apply always set them and why they are nullable. The README said only prepare's refusal carries the projected candidate and apply returns only the validation; it now says both refusals carry it.
OkfPreparedCandidate.toLoadResult() ships in 0.5.1 beside the projection, and downstream producers use it to judge a candidate with checks written against OkfBundleLoadResult. ADR-0009 did not mention it, and the CHANGELOG listed it without saying why it exists. ADR-0009 now names it as new public API and says what it is for: it reads a candidate as a later load of the committed bundle would, so downstream checks judge the exact candidate before commit at the ADR-0002 boundary. The CHANGELOG entry says the same and points at the ADR.
writeAll now refuses a text with an unpaired surrogate, but a document whose frontmatter held one still reached the disk through writeDocument. The YAML emitter wrote the surrogate as a \ud800 escape, so the serialized text had no surrogate and passed the writer's check, and the loader then rejected the concept, because YAML parsers refuse that escape. A change set with such a value failed in prepare with an OkfDocumentException that blamed the concept's YAML. The emitter now throws OkfYamlEncodeException for a string, key or value, that holds an unpaired surrogate, as it already does for values it cannot represent. writeDocument therefore fails before touching the filesystem, and it now reports that through its future like every other failure. The change overlay turns the emitter's refusal into an OkfBundleChangeException naming the concept, so prepare keeps the tool-error tier it documents for surrogates, and the MCP write verbs answer with that message. Tests cover serialization of values, nested values and keys, writeDocument in both modes, create, update and link changes through prepare and apply, and the MCP update and link verbs.
The paragraph said the overlay remains the concepts' only writer and, in the next sentence, that only concept files keep one writer. It also said the closed validator judges a projected log "by its §9 structure alone", while the conformance matrix in the same change records that okf reads §9 more strictly than the Spec's text. The two writer sentences are now one, and the validator sentence names okf's reading of §9 and links the matrix's reserved-file readings.
The okf/invalid-index-structure row gave the entry form as a `*` bullet only, though the parser accepts `-` bullets too, so the one table meant to state okf's exact reading made it look stricter than it is. The log row did not say which list markers it accepts. The completeness paragraph said an index may omit concepts because §11 forbids rejecting a bundle for missing indexes or broken links. §11 covers missing index.md files, not an index that leaves entries out, and §8 says an index enumerates its directory's contents. The rows now accept `*` and `-` bullets and name the markers they refuse. The paragraph states what okf accepts and why it does not check completeness, without claiming the Spec allows omissions: §8 does not say which concepts and subdirectories an index must list, §11 makes missing index files and broken links non-rejectable, and §8 only recommends descriptions.
The index noted the amendments of ADR-0003 and ADR-0004 by ADR-0009, but not those of ADR-0001 by ADR-0006 or ADR-0006 by ADR-0007, which their headers record. The 0001 and 0006 rows now carry the same note, so every amended ADR is marked the same way.
version.dart, the CHANGELOG, the README and ADR-0009 say a minor release that changes the generator's output sets okfIndexGeneratorRevision to its own version. OkfIndexGenerator's documentation and the golden test's comment still said the change bumps the revision, and the release checklist said the golden test catches any output change, though it pins one fixture bundle. Both now use the "sets the revision to its own version" wording, and the checklist says the golden guards the output.
|
@leoafarias thanks for the four commits here. Heads-up on what lands next on this branch (rebased on top of yours, nothing of yours dropped): Seven small fixes from a review of this PR against the OKF Spec:
Frontmatter-preserving edits (untouched keys keep their bytes on update/link/deprecate) are split out as okf 0.6.0 on btwld/wayfinder needs 0.5.1 on pub.dev before its CI can pass. |
Summary
Prepares okf 0.5.1.
OkfBundleChangeApplier.preparegains a reserved-fileprojection, so a caller can supply the
index.mdandlog.mdtexts of a changeset inside okf's own validated, stale-checked transaction. The release also
exports
OkfBundleLock,okfVersionandokfIndexGeneratorRevision, and addsOkfDocument.bodyStartLine. Version,okfVersion, the README action referenceand the CHANGELOG all say 0.5.1, so tagging
v0.5.1after the merge publishesit.
Why
package:wayfinder/authoring.dart)generate index and log text that okf's generic projection does not produce.
Without a seam inside okf's transaction they could only write raw files after
okf's commit, which the stale check does not cover and the validator never sees.
validate --fix,Stargate) need okf's
fcntllock to stay consistent with okf's own writes.generator's output revision rather than the package version. Otherwise every
okf patch release would invalidate Profiles that pin
matches-generated.path:start-endneeds the file line where thebody starts. Assuming the body is a suffix of the file fails for CRLF files.
What changed
Library (
lib/):OkfBundleChangeApplier.prepare(..., {OkfReservedFileProjection? project})and
apply(..., project:). The projection receives the inspected source, thecandidate, the change set's files and its date, and returns texts for
index.md/log.mdpaths (null keeps the source text). Projected texts arevalidated with the candidate, so Spec-invalid output is refused, and the commit
writes exactly those bytes or refuses a stale source. Naming any other path
throws
OkfBundleChangeException. An empty change set with a projection repairsreserved files on their own.
OkfPreparationRefused.candidate: the refused candidate after any projection.preparethrowsOkfBundleChangeExceptionfor a text with an unpairedsurrogate (before, the writer wrote U+FFFD, so the committed bytes differed from
the validated text).
OkfPreparedCandidate.toLoadResult().okf_io.dartexportsOkfBundleLock. The dartdoc and README state theone-isolate-per-process rule that its
fcntllock implies.okf.dartexportsokfVersion('0.5.1') andokfIndexGeneratorRevision(
'0.5.0') fromlib/src/version.dart. The internalokfPackageVersionisrenamed.
OkfDocument.bodyStartLine, the 1-based source line on which the body begins(LF, CRLF and a lone CR each end a line; a BOM adds none). It is set for parsed
and loaded documents and for
OkfPreparedCandidateconcepts, and is null fordocuments built with the constructor or
copyWith.Docs: ADR-0009 "Reserved-file projection", which amends ADR-0003 and records
the one-isolate rule for
OkfBundleLock. Also README "Concurrency" and "Library",and the release checklist in
.github/workflows/README.md, which now namestest/ci/gate_test.dartand whenokfIndexGeneratorRevisionchanges.Tests: projection tests in
test/io/bundle_change_applier_test.dartcover exactbytes, refusal of a concept path, refusal of Spec-invalid output, a stale source,
unpaired surrogates and the refused candidate. Also an index-generator golden
pinned to the revision,
bodyStartLinecases (no frontmatter, empty body, CRLF,lone CR, BOM, blank lines, a body repeating the frontmatter, loaded and prepared
concepts), and
test/ci/portable_entry_test.dart. That test compiles anokf.dart-only file to JavaScript, runs it with node when node is on PATH, andwalks every URI of each import, including conditional imports.
25 files, +1320 / -46, 8 commits.
Breaking changes
None in the public API.
okfPackageVersionwas internal (lib/src, not exported). Its replacement,okfVersion, is new public API.OkfPreparationRefusedgains an optional named parameter, so the existingconst constructor still compiles.
preparenow refuses a text with an unpaired surrogateinstead of writing U+FFFD.
floor (Declare the Dart 3.11.0 floor #62),
OkfGraphResolution.fromWireValueas a factory (Use factory constructors for enum parsing #65; calls andtear-offs compile unchanged), the move to btwld (chore: update OKF release owner for BTWLD #66), and publish validation
restored (Restore publish validation now that pub.dev 9576 is fixed #63).
Tests and checks
At a101e5c, locally on Dart 3.12.0 (macOS arm64):
dart run tool/ci/check_generated.dart: no outputs written.dart analyze --fatal-infos: no issues.dart test: 255 tests pass. 245 at the O1 head 74ccbc8; thebodyStartLinecommits add 10.
dart pub publish --dry-run: 0 warnings.dart format --output=none --set-exit-if-changed bin example lib test tool/ci tool/generate_case_folding.dartflags only
test/ci/gate_test.dartunder 3.12. This PR does not touch thatfile, and main fails the same way under 3.12. CI formats with
stable. Thelast okf CI run (2026-10-16, Dart 3.13.5) was Dependabot PR ci: bump test from 1.32.0 to 1.33.0 in /tool/release #68, which
changes only
tool/release, and the format check passed there. Main's lastpush run (2026-09-25) used 3.13.4 and passed.
Follow-ups
v0.5.1on the squash commit.release.ymlpushes the bare0.5.1tag, creates the GitHub release with Linux and macOS archives, publishesto pub.dev through OIDC and updates the Homebrew formula. okf 0.5.0
(2026-09-11) predates the move to btwld (chore: update OKF release owner for BTWLD #66), and its pubspec names
conceptadev/okf. Before the tag is pushed, check that pub.dev's automatedpublishing for okf names
btwld/okfwith tag patternv{{version}}.index.mdwhen a directory's last concept isdeleted). The projection's documentation notes the related case: dropping
the index of a directory the change set adds leaves its ancestors linking
to it.
okf: ^0.5.1once this is on pub.dev.every frontmatter key it does not touch (flow style, key order, quoting), and
a public scalar encoder. It will be pushed to this branch if it should ship
in 0.5.1; otherwise it follows in 0.5.2.
0.5.1 is out.