Repository navigation
Design Decisions
Six decisions define the package, and the design notes in docs/design.md record the reasoning for most of them. Several of the six carry a cost that a run of the finished tool exposed, and each cost is stated beside the decision that caused it.
The naive design makes the human document the source and compiles the packet from it, which is simple and fails in the one case that matters: someone edits the packet directly, as pipelines and engineers on call do, and the prose then diverges from it permanently without any record that it did. The alternative is a third file, a small JSON record of the fields, with the Markdown and the packet as generated views of it.
The package takes the third file, and gains a source of truth that both views can be rebuilt from. The cost is that the record is now the file nobody is watching. Sync compares the two views with the lock and never reads the fields file for changes, so an edit made only to fields.json produces sync: no-op. The decision would be completed by adding a third hash to the lock, so that the record is treated as a view of equal standing.
The design notes specify the hash comparison and do not discuss alternatives, so this account sets out the options as the author reasons about them: file modification times, a three-way merge against a stored base, and a comparison of each view's hash with the hash recorded at the last sync. Timestamps fail whenever a checkout or a copy resets them, and a three-way merge needs a stored base of the whole content, which grows the lock file from two lines into a snapshot.
The package records two SHA-256 hashes, one per view, and counts any difference as a change. The consequence is that detection is exact about whether a view changed and silent about what changed in it, so an edit to the notes region counts as a human change and triggers a regeneration, which the tests pin deliberately. A stored base of the fields would allow field-level merging, which is the natural next decision, and which the lock does not currently support.
When both views have changed, the tool could prefer one side, merge field by field where the edits do not overlap, or hand the question to a language model. The design notes list model-based reconciliation as a non-goal and state the rule plainly: conflicts surface to a person, even when only one field differs.
The tool therefore raises SyncConflict with the sorted list of differing keys, writes nothing, and exits with code 1. The price is friction, and one rough edge in how the friction clears: because detection compares each view with the lock, two views edited to agree still conflict, and the message then lists no keys. A person resolves a conflict by restoring the rejected view to its locked content, which works and is not obvious. Better wording of the message, or detection that treats identical content as resolved, would remove the surprise without weakening the rule.
When the Markdown changes, the tool must recover typed values from it. One option parses the bullets back out of prose, which is fragile, since a bullet such as Identity match threshold: 0.92 carries a label chosen for readers and no type. The other embeds the exact fields as JSON in a comment, ROSETTA:FIELDS, and reads that.
The embedded comment gives a lossless round trip and a short parser. It also makes the bullets decorative: editing a bullet alone makes sync report human-to-machine while the packet and the fields file keep the old value, as a run for this wiki confirmed. The example artifact's notes record the choice in one line, as a round trip "instead of parsing free-form prose bullets back into typed values". A guard that compares the bullets with the comment, and fails when they disagree, would keep the benefit and remove the trap.
The design sends only the compact packet to a model in the normal path, and falls back to the Markdown when the packet is missing or invalid; the Markdown is never appended beside the packet, since appending would spend the tokens the packet was meant to save. The loader in loader.py implements this order and raises PacketUnavailable only when neither file will serve.
The design sketch logs a warning when it falls back, whereas the implementation catches the parse and schema errors and moves on without a message, so a consumer cannot tell from the result which view it received. Because the Markdown is rarely sent, its notes region carries more weight than its size suggests, and a silent fallback hides the one moment when it matters. Returning the view's name with the text, or logging the reason, would repair the gap at the cost of a small change in the loader's signature.
The packet is plain KEY=VALUE text, so every parsed value is a string, whereas a JSON Schema asks for numbers where numbers are meant. A typed packet grammar would solve this and would need a formal specification of nesting and quoting, which the design notes list as an open question. The package instead coerces a string to the numeric type the schema declares, just before validation, and leaves a value that will not convert untouched so that the validator reports a real type error.
The trade-off is visible in the example: after a packet sync, DECISION_COUNT is the string "4" in fields.json. The coercion makes validation pass and makes the stored type drift. Floats are also fixed to two decimals in the packet, which serves byte-for-byte determinism, and which turns 0.925 into 0.93 while the Markdown still shows 0.925. A real packet grammar, built once the nested-value question is settled, would retire both effects.
Designed and authored by Daniel Boles; implemented with coding agents. Licence: Apache-2.0.