Skip to content

Release 0.5.1: reserved-file projection, lock export and version constants - #69

Merged
kauandotnet merged 25 commits into
mainfrom
feat/reserved-file-projection
Oct 22, 2026
Merged

kauandotnet merged 25 commits into
mainfrom
feat/reserved-file-projection

Conversation

@kauandotnet

Copy link
Copy Markdown
Contributor

Summary

Prepares okf 0.5.1. OkfBundleChangeApplier.prepare gains a reserved-file
projection, so a caller can supply the index.md and log.md texts of a change
set inside okf's own validated, stale-checked transaction. The release also
exports OkfBundleLock, okfVersion and okfIndexGeneratorRevision, and adds
OkfDocument.bodyStartLine. Version, okfVersion, the README action reference
and the CHANGELOG all say 0.5.1, so tagging v0.5.1 after the merge publishes
it.

Why

  • Wayfinder's Profile-aware writes (btwld/wayfinder, 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.
  • Hosts that read and write a bundle themselves (Wayfinder's validate --fix,
    Stargate) need okf's fcntl lock to stay consistent with okf's own writes.
  • Consumers that compare indexes with generated output need to pin the
    generator's output revision rather than the package version. Otherwise every
    okf patch release would invalidate Profiles that pin matches-generated.
  • A host that cites a concept as path:start-end needs the file line where the
    body 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, the
    candidate, the change set's files and its date, and returns texts for
    index.md / log.md paths (null keeps the source text). Projected texts are
    validated 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 repairs
    reserved files on their own.
  • OkfPreparationRefused.candidate: the refused candidate after any projection.
  • prepare throws OkfBundleChangeException for a text with an unpaired
    surrogate (before, the writer wrote U+FFFD, so the committed bytes differed from
    the validated text).
  • OkfPreparedCandidate.toLoadResult().
  • okf_io.dart exports OkfBundleLock. The dartdoc and README state the
    one-isolate-per-process rule that its fcntl lock implies.
  • okf.dart exports okfVersion ('0.5.1') and okfIndexGeneratorRevision
    ('0.5.0') from lib/src/version.dart. The internal okfPackageVersion is
    renamed.
  • 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 OkfPreparedCandidate concepts, and is null for
    documents 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 names
test/ci/gate_test.dart and when okfIndexGeneratorRevision changes.

Tests: projection tests in test/io/bundle_change_applier_test.dart cover exact
bytes, 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, bodyStartLine cases (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 an
okf.dart-only file to JavaScript, runs it with node when node is on PATH, and
walks every URI of each import, including conditional imports.

25 files, +1320 / -46, 8 commits.

Breaking changes

None in the public API.

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; the bodyStartLine
    commits 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.dart
    flags only test/ci/gate_test.dart under 3.12. This PR does not touch that
    file, and main fails the same way under 3.12. CI formats with stable. The
    last 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 last
    push run (2026-09-25) used 3.13.4 and passed.

Follow-ups

  • After merge: tag v0.5.1 on the squash commit. release.yml pushes the bare
    0.5.1 tag, creates the GitHub release with Linux and macOS archives, publishes
    to 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 automated
    publishing for okf names btwld/okf with tag pattern v{{version}}.
  • Not addressed: okf index leaves a stale index.md when a directory's last concept is deleted #67 (stale index.md when a directory's last concept is
    deleted). The projection's documentation notes the related case: dropping
    the index of a directory the change set adds leaves its ancestors linking
    to it.
  • Wayfinder moves to okf: ^0.5.1 once this is on pub.dev.
  • One more change for 0.5.1 is in progress: an update that keeps the bytes of
    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.
  • Dependabot ci: bump cli_pkg from 2.15.2 to 2.16.0 in /tool/release #64 (cli_pkg 2.16.0) changes the release tool. Leave it until after
    0.5.1 is out.

…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.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T14:44:24.686074Z a101e5c PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

kauandotnet and others added 17 commits October 22, 2026 11:48
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.
@kauandotnet

Copy link
Copy Markdown
Contributor Author

@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:

  • ADR-0009: only concept files keep one writer; a projection may rewrite log.md or record no entry (§9 requires none).
  • ADR-0004 marked as amended by ADR-0009.
  • The conformance matrix and README list where okf reads §8/§9 more strictly than the Spec, by rule id, and that index completeness isn't checked.
  • okfIndexGeneratorRevision documented as the 0.5.0 baseline.
  • Unpaired surrogates refused on every writer path (writeAll, writeDocument, frontmatter strings), not only in prepare.
  • OkfBundleApplicationRefused also carries the candidate. That makes the "only prepare's refusal" sentence from 57be9be untrue, so it's reworded to cover both refusals.
  • toLoadResult() named as new public API.

Frontmatter-preserving edits (untouched keys keep their bytes on update/link/deprecate) are split out as okf 0.6.0 on feat/o2-preserve-frontmatter, to open as a draft stacked on this PR, so 0.5.1 stays small.

btwld/wayfinder needs 0.5.1 on pub.dev before its CI can pass.

@leoafarias leoafarias left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

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.

3 participants