feat(loopspecs): unify the terminal-state vocabulary behind one owned mapping - #60
Open
sbracewell64 wants to merge 2 commits into
Open
feat(loopspecs): unify the terminal-state vocabulary behind one owned mapping#60sbracewell64 wants to merge 2 commits into
sbracewell64 wants to merge 2 commits into
Conversation
… mapping Two vocabularies named the same terminal facts twice. A LoopSpec finalising on no_progress_stalled and the platform execution node finalising on iteration-cap-failed are one fact under two names, and the LoopSpec side's maps_to was a free-form string carrying a third, unvalidated set of names. loopspecs/terminal-states.json now owns a single unified terminal-state vocabulary of nine members and the total mapping onto it from both source vocabularies: eight LoopSpec terminal states and the eight FINALIZE_MATRIX outcomes of the platform's scripts/runtime_execution_node.py, read at platform commit 5d86b7e. Sixteen source names resolve to nine unified names, every unified name is reachable, and no platform file is changed by this record - the platform-side rename is a follow-on task there. The map is enforced rather than documented. bin/fm-loopspec.sh checks it before any spec is read against it and refuses a map that is not total, that leaves a unified state unreachable, that is not a reduction, or that collapses two source names of differing consequence without declaring where the difference still lives. schema.json makes maps_to required and resolves it against the map through an external_enums pointer, so a spec terminal state that is unmapped, invented, disagreeing with the map or contradicting its unified kind is refused rather than defaulted onto whatever looks closest. no_delta's certification survives as a machine-checked property: exactly one unified state may be reached without spending a model turn, it must be no_delta, and it must stay neutral so reaching it can never demand a verifier verdict. The test proves that behaviourally as well as declaratively, with a success terminal under the same conditions as the negative control. New subcommand: fm-loopspec.sh terminal-map, with --unified, --source, --resolve and --json. Resolving an unmapped state refuses with the new stable token refuse_unmapped_terminal.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
CFVC-11: one terminal-state vocabulary, owned once, consumed by both primitives.
Two vocabularies named the same terminal facts twice. A LoopSpec finalising on
no_progress_stalledand the platform execution node finalising oniteration-cap-failedare one fact under two names, and the LoopSpec side'smaps_towas a free-form string carrying a third, unvalidated set of names (NOOP,COMPLETE,REFUSED,FAILED,EXHAUSTED,STALLED).loopspecs/terminal-states.jsonnow owns a unified vocabulary of nine members plus the total mapping onto it from both source vocabularies.no_deltagoal_metbudget_exhaustedno_progress_stalledverification_failedblocked_by_evidence_integrityneeds_rulingcancelledunclassified_failure16 source names resolve to 9 unified names. Every unified name is reachable.
Scope boundary
Only this repo changes. The platform's
FINALIZE_MATRIXwas read at platform commit5d86b7eand no platform file is edited; the platform-side rename is a follow-on task there. This is deliberate, not an omission.Two disclosed judgment calls
The reduction is 7 names, not the planned 8. The one merge declined is
needs_rulingwithcancelled: a ruling is owed in the first and is not owed in the second, so they are different facts with different next actions. Collapsing them would have hit the planned number by losing exactly the distinction the certification clause forbids losing. The binding criterion is "strictly fewer members than the sum", which holds.Four platform bounded stops merge onto one
budget_exhausted. This follows the platform's own stated rationale that a deadline is a bound like any other and that the exit code, not the name, carries clean versus unclean.timeoutis the only one of the four whose recorded consequence differs (EXHAUSTED/1rather thanEXHAUSTED/0), so it carries an explicit declared distinction and the validator refuses the map if that declaration is removed. That is how "no terminal state may lose its distinction in the merge" was made machine-checkable rather than left as prose.A genuine cross-system disagreement is recorded rather than papered over:
budget_exhaustedis kindfailureon the LoopSpec side, while the platform bookscontext-ceiling,budget-finishanditeration-cap-cleanasEXHAUSTEDwith a clean exit code. Each platform row records its status and exit code so the follow-on rename task can settle it.Enforcement, not documentation
bin/fm-loopspec.shchecks the map before any spec is read against it, and refuses a map that is not total, that leaves a unified state unreachable, that is not a reduction, or that collapses two source names of differing consequence without declaring where the difference still lives.schema.jsonmakesterminal_states[].maps_torequired and typedenum:unified_terminal, resolved against the map through a newexternal_enumspointer rather than restating the member list. A spec terminal state that is unmapped, invented, disagreeing with the map, or contradicting its unified kind is refused rather than defaulted.New subcommand
fm-loopspec.sh terminal-mapwith--unified,--source,--resolveand--json. An unmapped state or an undeclared source vocabulary refuses with the new stable tokenrefuse_unmapped_terminal.no_delta's certification survives as a machine-checked property: exactly one unified state may be reached without spending a model turn, it must beno_delta, and it must be kindneutralso reaching it can never demand a verifier verdict.Check state - read this before merging
CI is 12 of 14 green. Both failures are accounted for below, and neither is caused by this change.
Behavior portable serial 2PR must be raised via no-mistakesBehavior portable serial 2is pre-existing and not mine. The failing assertion isfirst inheritance transaction never reached its blocked writeintests/fm-remote-secondmate-lifecycle-e2e.test.sh:732. It is a timing wait - 250 iterations ofsleep 0.02, so a 5 second ceiling - on a backgrounded remote config push reaching its blocked write. It concerns remote secondmate inheritance and touches nothing this branch changes.I proved it rather than assuming it: the same test was run on the base commit
ed376cfin a throwaway clone withloopspecs/terminal-states.jsonabsent, and it failed identically - 14 pass, 1 fail, same assertion. It fails the same way at this branch's head. The base is red here on its own.PR must be raised via no-mistakesis an expected artifact of how this PR had to be opened. The pipeline ran in full, but it opened its pull request at the upstream venue (kunchenguid/firstmate), where this branch appeared as 48 commits and 204 files and reportedmergeable: false. That PR was withdrawn and this fork PR opened by hand in its place, which is what the check is objecting to. Against this fork base the branch is what it should be: 2 commits, 7 files,mergeable: true.The
no-mistakespipeline returnedpassed, but two of its nine steps did not run normally, and both were deliberate:rebasewas skipped. This branch sits on the fork trunk, which is 46 commits ahead of upstream. Rebasing onto upstream would have bundled that entire fork landing queue into this PR's contribution history.ciwas skipped. Its only finding was the merge conflict against that upstream base, which was an artifact of the wrong venue rather than of the change. The checks that matter ran here instead, on this fork PR, and are tabulated above.Also worth stating plainly: all three of the pipeline's CI auto-fix attempts failed on an agent session limit rather than on conflict logic, so "max auto-fix attempts reached" overstates what was actually tried.
What did run and pass:
intent,review,test,document,lint,push,pr.Test evidence
Verified locally at
83c426c, working tree clean:tests/fm-loopspec.test.sh- 31 of 31 pass, 0 failuresbin/fm-lint.sh- clean, ShellCheck 0.11.0 (the pinned version CI and the pre-push gate both use)bin/fm-doc-audience-check.sh- clean, 72 surfaces, 213 local linkstests/fm-documentation-audiences.test.sh- 4 of 4 passtests/fm-research-scan.test.sh- 13 of 13 passEvery new assertion drives
bin/fm-loopspec.shthrough its public interface - exit status and stable refusal tokens - never by reading the script's source, and each witnesses its negative control failing before the positive result is trusted.The map itself was mutation-tested three ways, with each mutation watched failing before the passes were trusted: dropping a source row, inventing a platform outcome the matrix does not declare, and collapsing the stall distinction onto
budget_exhausted.The platform's eight finalize outcomes are pinned as literal names in the test rather than read from the platform repository, which is not present in CI. The test asserts both directions - every pinned outcome resolves, and the map records no outcome the platform does not declare - so a platform-side vocabulary change surfaces as a failing test here rather than as silent drift.
Review findings this branch carries
The review step caught two real regressions in the first commit, both fixed in
83c426c:name, a unified entry without aname, or a source row without astateaborted the validator and suppressed every precise diagnostic for that file. Behaviour stayed fail-closed throughout - nothing unsound would have passed - but where the base cleanly reportedmissing required field: terminal_states[].name, the first commit printed only a generic "the validator could not run against this file". The precise diagnostics are restored and regression-tested.terminal-map --source <typo>printed nothing and exited 0, so a misspelled value read as an empty mapping. It now refuses, consistent with--resolveand with the principlecmd_selectalready states: a typo must never read as "there is no work to do".One review finding was deliberately not acted on: the contract-file guard covers
showonly, sostate terminal-statesstill prints{}and exits 0. That behaviour is pre-existing for theschemaandtriggersids as well - confirmed against the base - it already fails closed, and extending it intoload_spec_or_refusewidens scope beyond this change. It is filed as a follow-up.Later consumers
CFVC-13's attempt-budget terminal state and CFVC-12's outcome enum will consume this vocabulary. Neither is implemented here.