v4.0.0
Changed
- Pull requests into
mainnow run the complete Linux product evidence —rust workspace
andWASM— instead of deferring it to after the merge. Only the cross-platform
native Z3 4.16matrix (macOS, Windows) stays post-merge underFSL_OPTIMISTIC_CI. The
boundedmerge readinesslane is kept as a sub-minute fail-fast check; adding
--all-targetsto itscargo checkwas tried and reverted after measuring 12m42s in
CI, sincerust workspacenow compiles and runs those targets on the same pull
request. 103 of the repository's 124
integration test files live infslc,fsl-tools,fsl-verifierandfsl-lsp, none of
whichmerge readinessruns, so that evidence only reachedmainpost-merge
(docs/DESIGN-ci.md).
Fixed
fslc ai checknow rejects duplicatedatasetandevaluatordeclaration
names, which the frozen reference has always rejected
(src/fslc/ai_project.py:237-241). Native validated only three of the five
declaration kinds, so a project declaringdataset Sharedtwice exited 0 and
reporteddatasets: ['Shared', 'Shared']. Because adataset Xreference
resolves by name, two declarations sharing one made resolution depend on
declaration order — the false green was silently picking a winner, not merely
echoing a name twice (#571).- Native now emits
kind:"name"for name-resolution failures instead of
collapsing them intosemantics, making every member of the
docs/DESIGN-v1.md§7.2 closed set reachable.duplicate state variable,
duplicate enum member,duplicate def,duplicate parameter in def,
def ... parameter is shadowed by binder, andundefined predicatewere all
reported assemantics, sending an agent following the §8 repair protocol
down the wrong branch.CoreErrorandModelErrornow carry the
classification the frontend already determined, and it survives through
SpecLoadErrorto the one renderer — the same direction as #484, rather than
adding patterns to the message-string classifier, which has no text these
diagnostics share and would silently reclassify them on any wording change.
Messages are byte-identical and no otherkindmoves.
examples/gallery/errors/name_duplicate_state_variable.fslis the corpus
golden the classification never had; its absence is why this survived #484 and
#555. Itslocnow names the redeclaration rather than the first binding:
the diagnostic carried no span of its own, so it fell back to the
message-derived heuristic, which finds the earlierxand pointed a repair
agent at the innocent declaration (#565). - A
duplicate enum memberdiagnostic now points at the repeated member rather
than the first declaration of that name.SpecItem::Enumcarried no
per-member span, so the report fell back tosource_diagnostic's
message-derived heuristic — the first token matching the quoted name — which
for a duplicate is the earlier, innocent declaration by construction. For
enum E { A, B } enum F { B, C }it namedBinE(2:15) instead of the
redeclaration inF(3:12). Alocthat exists but names the wrong construct
is worse than none:docs/DESIGN-v1.mdG2 assumes the position is correct.
SpecItem::Enumnow carriesmember_spanspositionally parallel to
members, the same shapeSpecItem::Structgained in #555 and the same the
domain surface already used, and the diagnostic attaches the offending
member's own span (#576). - The native<->Worker parity corpus's
unsupportedDocumentsexclusions in
rust/fsl-wasm/test-browser.mjsare now self-retiring. The map excluded 32
refinement/agent/causal documents from the comparison and recorded only a
document type, so if the Worker ever gained a verb for one of them the
exclusion would keep suppressing the comparison forever -- the shape that
left #556's divergent path with zero corpus coverage. Each entry now carries
the measured reason it holds, and every excluded document is probed on the
Worker alone: the recorded premise is that the Worker cannot analyze it, and
the day that stops being true the harness fails and names the entry to
remove. The compared-pair count is unchanged at 351 -- no exclusion was
retired and no document newly compared -- and the run now also reports
exclusionProbes. This stays a capability exclusion, not a
tolerated-difference allowlist: the envelopes are still not compared for
these documents and no verdict, location, or exit-code difference is
allowlisted (#568). - The 28
refinement-typed entries inrust/fsl-wasm/test-browser.mjs's
unsupportedDocumentsexclusion map are retired; the compared native<->Worker
parity corpus grows from 359 to 415 cases andexclusionProbesdrops from 32
to 4 (agent + 3 causal only). The exclusion premise was measured stale: #574
gave native and the Worker one shared classifier (kernel_load_error) for a
document whose top level parses but is not Kernel-shaped, socheck/verify
on every refinement document in the corpus now produce byte-identical
semantics/"spec has no state block" envelopes on both surfaces -- the
exclusion was suppressing zero divergence.specs/cart_refines.fslis now
asserted by name to remain a compared parity case, the same way
duplicateWriteCase/governanceErrorCaseare, so a future silent
re-exclusion fails loudly instead of only showing up as a quiet drop in
parityCases.length(#577). - Every parity-corpus exclusion is now checked to have native answer
non-error, before the Worker launches. This closes the blindness that let
the 28 retired entries go stale: #568's probe tests only the Worker side
(result !== "error"), which detects agreement only when native answers
non-error— agreement then forces the Worker non-errortoo. Native
answerederrorfor all 28 refinement documents, so two sides erroring
looked identical whether or not the errors matched, and the probe stayed
green for weeks. Native answers non-errorfor all 4 surviving exclusions
(okfor agent,causal_model_checkedfor the 3 causal), so the cheap
Worker-only probe is sound for them — and the day an exclusion is added
whose native side errors, this assertion names it instead of silently
reverting the corpus to that blind state. Full envelope comparison was
evaluated and rejected: the causal envelope carries noversionsblock, so
validateEnvelopethrows before any comparison, which is a normalizer
rejection rather than a verdict. The assertion runs in the candidate loop
rather than beside the Worker probe because it depends on nothing but
native, turning a ~6-minute failure into a sub-second one (#577). - The browser parity harness no longer embeds a developer's absolute path.
rust/fsl-wasm/test-browser.mjshardcoded one machine's
/Users/<name>/Library/Caches/ms-playwright/chromium_headless_shell-1208/...
Chrome, the only committed line in the repository containing an absolute home
path, whichAGENTS.mdforbids. Playwright installs each build under its own
version directory and has changed the internal layout between them — both
chrome-mac/headless_shelland
chrome-headless-shell-<platform>/chrome-headless-shellexist on a machine
with two builds installed — so the version, the platform directory, and the
binary name are all discovered rather than spelled out, newest build first
(#583). - A dropped or half-open Chrome DevTools Protocol connection no longer hangs
the browser gate forever.cdp()returned a promise that only the message
listener could ever settle, with no timeout and no socketclose/error
handler, so a response that never arrived hung Node indefinitely; the
surroundingfor (attempt < 360)loop is not a bound when a singleawait
inside it never returns. Because the hang left the process to be killed
externally, thefinallythat terminates Chrome and removes the profile
directory never ran: an orphanedchrome-headless-shell(parent gone,
4h13m old) and seven leaked profile directories were measured on one machine,
and an orphan over two hours old blocked a later gate.cdp()now times out
naming the method, and both socketcloseanderrorreject every
outstanding request, so the failure is loud and the cleanup runs (#584). rust/fsl-wasm/web/cases.mjsno longer claims its smoke cases are compared
against native. The native comparison keyed on them (nativeVerdict) was
removed ind30f456when the corpus-wide parity run replaced it, but the
now-unusedimport { cases }intest-browser.mjsand the header comment
promising "the parity comparison stays honest" both survived, so the file
documented a check that no longer existed. The import is removed and the
comment states what is true: these 9 inline sources are Worker smoke cases
checked in the browser against their ownexpected, and the 415-case corpus
comparison supersedes the removed one (#585).- A kernel-stage failure inside a
use ... fromcomponent now reports the
parent'susedeclaration as itsloc, and names the component's own path
and position in the message. The two used to be mixed: the path came from the
parent (source_file) while the line and column came from the component's
parser, socheckon
examples/gallery/errors/semantics_compose_component_parse_failure.fsl
reported…:7:18— and line 7 of that file is a comment. A location in the
wrong file is worse than none, anddocs/DESIGN-v1.mdG2 requires the output
JSON alone to say where the problem is.locis{line, column}with no
filein all five of itsdocs/DESIGN-v1.mdexamples, so it can only mean a
position in the file the envelope is about; theusedeclaration is that
position, and it is also the line to look at first. The message now reads
component "<component>" failed to parse (<inner> at <component>:<l>:<c>) at <parent>:<use line>:<use column>, naming both files explicitly. An
unreadable component is re-anchored the same way, instead of reporting1:1.
Native and the browser Worker share the loader, so both return the identical
string; the file is a parity corpus case, so
./tools/check-native-integration.shchecks that on every run (#567). - The fsl-ai project check's unexecutable-
requirespec error (#542) now
carries alocpointing at the offending clause's own line and column.
rust/fsl-syntax/src/ai_project.rstracked no positions at all, so the error
named the declaration and slice but no position, breaking
docs/DESIGN-v1.md§7.2's guarantee that everyparseerror has one. Block
bodies and their statement lines now carry their char offset through
top_blocks/top_lines, each metric/observed requirement records the
position of the clause that produced it, and bothfslc ai checkand the
fslc checkdispatch emit it. A clause nested in aslicereports its own
line, not the enclosing block's. Which projects are accepted is unchanged
(#562, partial -- see below). - Not fixed, recorded: an unknown non-
requireline inside a declaration
body is still silently ignored, so acceptance still turns on the line's first
word. Closing that requires deciding a closed body grammar, and no current
source specifies one: the frozensrc/fslc/ai_project.pyignores unrecognized
lines by construction (anif/elifchain with noelse) and downgrades an
unparseablerequiretokind="inconclusive"rather than erroring;
docs/LANGUAGE.mdandskills/fsl/reference.mdstate only therequirerule
#542 added; andskills/fsl/reference.mddocuments unvalidated block bodies
as intended forai_action/retriever/trust_boundary/authority. The one
corpus project file additionally uses free-form predicate lines
(language in ["ja", "en"],condition output.claims not_supported_by retrieved.sources) that both implementations store verbatim and never parse
(#562). - Breaking (input).
true,false, andnoneare now rejected as
declaration names; a specification that used one will stop checking. They
always resolve to the literal in an expression, so a declaration with one of
those names was unreadable from every expression — and the misreading was
silent, not an error.state { true: Bool }with
invariant AlwaysHolds { true }returnedokfromcheck,verifiedfrom
verify, andprovedfromverify --engine explicit, while
init { true = false }assigned a variable nothing could read: the author
read "the variable holds" and the verifier proved the literal. All three
engines agreed, so symbolic/concrete/BFS agreement did not catch it.
noneappeared to be rejected already, but only incidentally — the error came
from Option-type inference failing on a barenone, so
state { none: Bool, slot: Option<Bool> }withslot = nonecheckedokand
silently assigned the literal. The check now covers the specification name,
const,defand its parameters,type,enumand its members,struct
and its fields, state variables, actions and their parameters, properties
(includingleadsTo), quantifier and aggregate binders, andis some(x)
pattern bindings. The reserved set is derived from the native expression
parser rather than copied from the frozen reference: only those three
identifiers are matched unconditionally as literals, socount,sum,
stage,in,is,where,old,abs,and, andorremain valid names
and every.fslunderspecs/andexamples/still checks unchanged.
The diagnostic iskind: "semantics"with aloc;docs/DESIGN-v1.md§7.2
puts it in thenameclass, which native does not yet emit anywhere (#565)
(#570). fslc analyzeon a.tomlproject manifest now emits the same TSG
vocabulary a standalone file emits for the same source. The manifest loop
built each layer withfsl_tools::build_tsgalone, which only sees the
loweredKernelModel, so none of the source-only kinds ever appeared on that
path —acceptance/forbiddenscenario nodes with theircoversand
starts_with/precedesedges, andcontrolcatalog nodes — and the graph
vocabulary silently depended on which input form named the spec. Each layer
now runs the same source-level enrichment, on its own file
and its unprefixed graph, so<layer>:prefixing still applies to
enrichment-added nodes and two layers declaring the same id stay two nodes.
No ordering change was needed:Builder::builddrops acoversedge whose
target has no node yet and runs first, but the edges it computes come from
KernelModel::requirement_targets, which enumerates onlyinit,action:*,
andproperty:*targets and can never name a scenario or a control, and
every enrichment-added edge has both endpoints created by the enrichment
itself.
Verdicts and exit codes are unaffected, and no review finding changes — the
manifest path accepts only--projection traceability_graph, so no structural
detector runs there at all (#558).- Native
fslc ai checkon an fsl-ai project now emits the six fields the
frozen reference'sanalyze_ai_projectemits and native omitted:
ai_project,assumptions,datasets,dialect,evaluators, and
failure_modes.evaluatorsandfailure_modeswere not merely
unprojected — the Rust project parser did not descend intoevaluator/
failure_modeblocks at all, so the data did not exist; it now records their
names, the only thing the reference projects.skills/fsl/reference.md
already documentedfailure_modeas listed underfailure_modes, so the
skill AI agents read promised output native had never produced; the skill was
right and the implementation moved to match it. Native'sai checkoutput
now equals the frozen reference on all 13 of its keys, and
tools/check_rust_phase3_commands.py'sai-project-checkprojection — which
compared only six of them, and is why the gap went unnoticed — now compares
the whole set (#563). fslc html's property table now renders the "Deadline" column
docs/DESIGN-html-report.mdspecifies.grep -ci deadline rust/fsl-tools/src/html.rswas 0: the caption half of that design paragraph
landed with #525 and this was its unimplemented remainder, so a
leadsTo ... withindeadline was invisible in the report. The column is
conditional, as the same paragraph's no-none-filler rule requires — it
appears only when some property declares a deadline, a property that declares
none gets an empty cell rather than a filler, and a spec with no deadline
anywhere renders no column at all.explain'sskeletonproperties now also
carrywithinfor aleadsTo ... within, additively and only when declared,
matching the frozen reference's_property_skeleton(#564).- Breaking (exit code).
fslc mutateon a spec whose baseline already
fails now exits 1 instead of 0.mutatere-emits the baselineverify
envelope verbatim when the baseline does not verify, but derived its exit
code fromresult == "error"alone, so every other non-success verdict fell
through to 0 andresult:"violated"returned a green exit — a mutation score
is meaningless over a spec that already fails, and a gate reading only the
exit code saw a pass.docs/LANGUAGE.md's exit-code table mapsviolated
to 1 with no per-command exemption, andscenarios/testgenre-emit the
same envelope and already exited 1. The status is now a total match over the
results a mutation run can carry (mutate_exit_status):mutated/
verified/proved→ 0,violated/reachable_failed/unknown_cti/
unknown_budget→ 1,error→ the code the envelope was already classified
with, and anything unmapped → 3 rather than silently 0. Mapping the whole
vocabulary also closed a second reachable false green in the same command:
--from <unreadable file>returnedresult:"error",kind:"io"with exit 0
and now exits 2 (#554). - Native
fslc mutatenow defaults to 200 built-in mutants instead of 100,
matching themax_mutantsdefault its own published CLI contract
(rust/fslc/cli-contract.json) advertises and the 200 fixed by
docs/DESIGN-mutate.md,skills/fsl/reference.md, and the frozen
src/fslc/mutate.py(DEFAULT_MAX_MUTANTS). For a model with more than 100
candidates the smaller default silently evaluated a different mutant set and
reported a different kill count, making native reports incomparable with the
documented baseline and skewing hollow-spec triage. Explicit--max-mutants
behavior is unchanged, and the runtime default is now a single
DEFAULT_MAX_MUTANTSconstant rather than a literal that can drift from the
contract (#524). - Breaking (output).
fslc htmlno longer labels every property row with
one report-wide assurance class.html.rs::assurancederived a single class
from root completeness andproperties_sectionreused it for every row, so
under--engine inductionareachableand an ordinary unrankedleadsTo
both renderedproved(induction)even though k-induction ranks only
invariants and transitions -- a reachability witness comes from the bounded
base BMC. In an audit artifact "proved" claims all-depth universal evidence,
so this was an assurance misstatement. Property rows now classify per element
through the ledger's existingformal_assurancerule
(docs/DESIGN-assurance-classes.md) instead of a second local
classification, and the report-wide status row now shares the ledger's
assurance_token/assurance_labelpair as well. For
examples/pm/cancel_flow.fsl --engine induction,CanRetain,CanChurn,
POL-1, andPOL-2change fromproved(induction)to
bounded(BMC depth 8). The same renderer also never called
requirement_captionfor property rows, so invariant/reachable/leadsTo rows
dropped the requirement text actions and counterfactuals keep; each row now
renders its own caption under the declaration name, escaped, and omits it
entirely when the property carries no requirement tag
(docs/DESIGN-html-report.md) (#525). - Breaking (output).
fslc ai checkand thefslc checkdispatch for
fsl-ai project files no longer accept a declaration whoserequireclauses
are unparseable. Both ranai_project_summary, a line scanner that
collected declaration names by string prefix and never read a clause body,
so a project whose clauses match no known evidence grammar returned
ai_project_analyzed/findings: []/ exit 0 fromcheckand was then
rejected byeval/regress/drift/compat-- the confidently green false
negativeAGENTS.mdcalls more dangerous than a crash. The check stage now
reports fromfsl_syntax::parse_ai_project, the parser those commands
already execute, and an unexecutablerequireclause is a spec error
(result:"error",kind:"parse", exit 2) naming the declaration, slice, and
clause text, matchingdocs/LANGUAGE.md's exit-code table and the exit 2 the
same dialect already returns for non-AIai compatinput and an unknown
--property/--migrationselection. Reporting from the parser also corrects
twoai checkoutput fields:componentsis now the declaredai_component
names ([]when none, previously the scanner's placeholder[""]), and
raw_blocksentries carry{kind, name}per block as
skills/fsl/reference.mdspecifies (previously{kind}only, deduplicated
by kind). Raw blocks stay unvalidated: garbage insideai_action,
ai_contract,authority,retriever, ortrust_boundarystill passes
check, because those are contractually block boundaries only (#542). typeandsemanticserrors now carrylocon every spec-reading command,
completing thedocs/DESIGN-v1.md§7.2 clause that guarantees a location for
parse/name/type/semantics. Issue 484 delivered theparsehalf; the
other half returnedloc: nulleverywhere, includingcheck, so the §8
repair protocol could not mechanically locate atype/namefailure from
the output JSON alone.SpecLoadError::Semanticnow carries the structured
diagnostic instead of a flattenedString, andModelErrorcarries the span
of the construct that failed — a kernelspecregisters no lowering origins,
so the three corpus goldens previously had no location to report at all. The
reported position is the offending construct, not the enclosing declaration:
the state field for an unknown type, thestructdeclaration for a
non-scalar field (TypeExprcarries no span of its own), and the second
write for a duplicate assignment. Messages are unchanged — the span is a new
field thatModelError's Display deliberately ignores — and nokind
moves. A diagnostic that names no construct in the reported file still omits
locrather than emitting a{line: 0, column: 0}placeholder;
docs/DESIGN-v1.mdnow states that exception explicitly instead of leaving
the clause overstating the implementation (#555).- The browser Worker no longer classifies a kernel-stage load failure
differently from the native CLI.rust/fsl-wasm'sbuild()hard-coded every
fsl_core::parse_kernel_source_with_filefailure tokind: "parse", while
native carried the sameCoreErrorthroughkernel_load_errorand reported
semantics/type/name— the same input classified two ways depending on
which surface ran it, whichAGENTS.mdforbids allowlisting and which makes
docs/DESIGN-v1.md's closedkindset meaningless. The classifier and its
render dispatch moved out of thefslcbinary intofslc_rust::spec_load
(SpecLoadError,kernel_load_error,surface_parse_failure,
render_spec_load_error) so both surfaces run the same one; the Worker gained
no classifier of its own, since a parallel classifier is what diverged.
Native's classification is unchanged.
Detection power was zero:examples/gallery/errors/had no input that fails
at the kernel stage while its own top level parses — every such file in
specs/+examples/is a refinement, agent, or causal document the parity
harness excludes as unsupported — so the divergence was invisible to
./tools/check-native-integration.sh.
examples/gallery/errors/semantics_compose_component_parse_failure.fsland
itssemantics_compose_broken_component.fslhelper close that hole, taking
the native↔Worker parity corpus from 351 to 355 cases, and
rust/fsl-wasm/test-browser.mjsnow fails loudly if the case ever leaves the
corpus (#556). - Every spec-reading native command now classifies a syntax error the way
checkdoes:kind: "parse"withdiagnostic_code: "FSL-PARSE"and a
loc.rust/fslc/src/main.rs'sload_kernel_modelflattened the frontend's
structured failure into aString, sokernel,conformance,scenarios,
explain,analyze,typestate,ledger,testgen,document generate,
diff,replay, andhtmlre-classified an unparseable file as
kind: "semantics"withloc: null, andverify/sweepreported
kind: "parse"with thelocdropped — breaking the schema guarantee in
docs/DESIGN-v1.md§7.2 thatparsealways carries a location, and sending
the §8 repair protocol into the wrong branch. The loader now returns a typed
SpecLoadError(Io/Parse/Semantic) and every consumer renders it
through the samefrontend_output::render_surface_parse_errorcheckuses.
Exit codes andresult: "error"were already correct and are unchanged.
The browser Worker'sverifymoved with the CLI so the native/Worker parity
contract still holds (#484, #497). - A missing or unreadable specification is now
kind: "io"for every command
instead of falling through the message-string classifier to
kind: "semantics".fslc analyzedisagreed with itself: one missing input
reportedsemanticsand the same missing input alongside a second file
reportedio.checkandverifyalso no longer silently continue when the
spec cannot be read (#497). fslc mutateon a specification that fails to load now exits 2 instead of 0.
A non-verifiedbaseline still scores 0, but a spec error keeps its own exit
code, asdocs/LANGUAGE.md's exit-code table requires with no per-command
exemption; exit 0 on an unparseable spec was a green gate over a
specification that was never analysed (#484).rust/fslc/tests/testgen_contract.rs's
symlink_source_name_and_canonical_pytest_path_remain_distinctno longer
panics withcreate output-parent symlink: AlreadyExistson a repeated
local run whose previous invocation was interrupted before its own
trailing cleanup ran: the fixture directory and both symlinks it creates
now live under a fresh, uniquely named scratch directory (same idiom as
rust/fslc/tests/chain_cli.rs'sscratch_dir) instead of a fixed, reused
path, so there is nothing left over to collide with on the next run
regardless of how the previous one ended (#539).rust/fslc/tests/domain_codegen_contract.rs's
all_five_public_kernel_domain_targets_match_pre_migration_goldensand
every_valid_domain_corpus_entry_generates_all_five_targetsno longer
intermittently fail (a golden digest mismatch or a
clear generated directory: No such file or directory) when cargo runs
them in parallel in the same test binary, the default: both used to
exists()-then-remove_dir_all()a directory under a shared
rust/target/domain-codegen-contract/parent before generating into it,
leaving a window one test's cleanup could race against the other's
read/write. Each generation call now gets its own uniquely named scratch
directory (same idiom asrust/fslc/tests/chain_cli.rs'sscratch_dir),
which needs noexists()/remove_dir_all()step at all — closing the
race window rather than narrowing it (#546).- Native
reachable(r, a, a)is now non-reflexive on every engine: true
only via a real path of one or more edges back toa, never a free
zero-hopa == astep.rust/fsl-runtime/src/lib.rs's concrete
relation_reachable(used by the--engine explicitBFS oracle, and by
the default engine's own trace-replay consistency check) started its BFS
frontier at[source]and checkedcurrent == targetbefore traversing
any edge, so it reported self-reachability as trivially true for any
relation, including an empty one -- a free zero-hop step the symbolic
evaluator's convention never took. Neitherdocs/LANGUAGE.mdnor
docs/LANGUAGE.ja.mdpreviously stated whetherreachablewas
reflexive; both now document the non-reflexive contract explicitly.
Matches the frozen Python reference's_relation_reachable
(src/fslc/runtime.py) and native BMC exactly -- confirmed by direct
code reading and by running the frozenverify/scenarioscommands
against the same repro spec, bothviolated_at_step:1, not the reflexive
0the pre-fix native concrete engine reported. Before the fix an empty
relation disagreed between engines (violated_at_step0 vs. 1) and a
multi-hop-cycle spec made the default engine's own trace-replay
consistency check fail outright (kind:"internal",
"trace state mismatch", exit 3), since BMC's concrete replay of its own
symbolic counterexample used the same disagreeing evaluator (#502). relation A -> Bstate is now usable end to end in the native symbolic
verifier:r = Set {}types and evaluates as the empty relation (both
fslc checkand the concrete Monitor previously rejected or mistyped it),
andfslc verify/fslc mutateimplement all seven relation operations
(.contains(a, b),.add(a, b),.remove(a, b),reachable(r, a, b),
acyclic(r),functional(r),injective(r),domain(r),range(r)),
matching the frozen Python reference's semantics (reachable/acyclic
require a self-relation and are rejected otherwise; a memoized bounded-hop
closure avoids the unmemoized-blowup class of bug already fixed in the
Python reference) (#467).- Native
leadsTolasso and deadlock-stall search now applies the documented
symmetric type/symmetric enumliveness symmetry reduction: the
designated representative state (lasso loop head, or the stalled state) is
constrained to the canonical permutation of each symmetric type's
per-entity rows, built fromMap<SymmetricType, V>and
Set<SymmetricType>state (skipping anyVthat itself mentions a
symmetric identity type), matching the frozen Python reference. A genuine
per-entity violation (only one identity ever stalls) is still found under
bothMap- andSet-shaped symmetric state (#461). verify --engine inductionnow implementsleadsTo ... helpfulper-binding
ranking proofs natively, matching the documented idiom for per-entity
progress under interleaving.fslc checkrejects ahelpfulaction that
names an undeclared action or has the wrong arity; ranking induction
additionally proves the fourhelpful-specific obligations (matching
actionfair, an enabled matching instance whenever pending, stable
enabledness across two or morehelpfulactions, and non-helpful actions
neither dropping the pending obligation nor increasing the measure),
reportingprogress_action_not_fair,helpful_action_not_enabled,
helpful_action_enabledness_not_sticky,non_decreasing_helpful_action,
non_helpful_action_increases_measure, orpending_not_preservedon
failure and echoinghelpfulon both the proof and CTI (#473).
Added
-
Native
verifynow reports the three remainingdocs/DESIGN-vacuity.md§2
lanes —always_true_requires,tautology_over_frozen, and
urgency_freeze— closing the last of issue 465. A spec whose only emptiness
was one of them previously came backresult:"verified"/ exit 0 even under
--vacuity error; the three-line reproduction (aBoolset byinit,
assigned by no action, asserted as an invariant) now exits 2 with
kind:"tautology_over_frozen"andtrace_type:"vacuity". All three are
proved infsl-verifier, sofsl-runtimestays solver-independent, and all
three report identically on--engine bmc,explicit, andinduction.Unlike the frozen Python reference, the native lanes decide "always true over
all reachable states" over the declared type space rather than over the
states an unrolling happened to witness, so no verdict moves with--depth.
Python reportsexamples/causal/funnel.fsl'srequires visits < 100as dead
at depth 8 becausevisitsonly reaches 8; withvisits: 0..100declared,
native correctly stays silent at every depth. The lanes are sound and
deliberately incomplete: an unproven obligation, anunknownbackend
verdict, a compose-synchronized action, a generated declaration, and an
action that was never enabled all yield no finding. -
Added an optimistic CI lane with one stable, parallelized
merge readiness
context for pull requests and merge queues, while preserving the complete
Rust/WASM/macOS/Windows product gate for every mergedmainstate and
production promotion. Failed post-merge jobs now create or update
deduplicatedci/post-mergeissues and close them only after observed
recovery; rollout is fail-safe behind theFSL_OPTIMISTIC_CIrepository
variable. The readiness compile lane deliberately excludes native-Z3 and
default-feature builds, which remain mandatory post-merge evidence. -
Native
analyze's TSG projection now emitsrequirement/acceptance/
forbidden/kpinodes andcoversedges for a standalone.fsl/
requirements spec, not only for a.tomlproject manifest.
requirement/covers/kpiare model-only (build_tsgitself now
projects them fromKernelModel::requirement_targets/.projections, so
every dialect gets them);acceptance/forbiddenscenario nodes need
requirements-dialect source text (fsl_core::requirements_trace_contract)
since they have no Kernel-lowered form, and gain acoversedge from any
@requirement(...)-annotated requirement that names them. A requirement
connected to nothing the graph represents still gets its own node with no
coversedges, rather than being silently dropped, so
disconnected_requirement(--profile ai-review) can detect it.
requirement_property_graphand--focus requirement:IDpreviously had
zero edges/always failed for a standalone spec; both now work (#495). -
Native
analyze's TSG projection now also emits the remaining documented
vocabulary:starts_with/precedesstep edges andcontrolnodes. An
acceptance/forbidden case gains astarts_withedge to the action its first
step calls and aprecedesedge to each later step's action, running
scenario → action and carrying the 0-basedstepindex (the direction and
split the frozen reference fixes insrc/fslc/analysis/tsg.py); the index is
part of the edge id, so a case that calls one action twice keeps both edges.
controlnodes project governance/businesscontrol ID "text"catalog
entries, which have no Kernel-lowered form. Because scenario nodes
previously had no outgoing edge at all,unanchored_property's
scenario-anchor suppression (scenario_actions && kind == "reachable") could
never apply, so areachableanchored by an acceptance scenario was reported
as unanchored — a structurally guaranteed false positive that the step edges
remove. No review finding readscontrolnodes yet; the edge vocabulary has
nosatisfieskind, so a control node carries only itsdeclaresedge
(#495). -
Added an evidence-gated
fsl-design-family.v0sidecar prototype with three
maintained three-variant dogfood families, native check/verify/refine/diff
orchestration tests, raw producer and deterministic digest controls, and an
accepted agent/workflow-only decision without new language or CLI semantics
(#427). -
Refinement mappings can declare a source-total
enum abstractionand invoke
it withabstract(name, expr)for nominal many-to-one mappings. Repeated and
unused targets are allowed, while missing, duplicate, unknown, or
wrong-nominal sources fail closed; concrete, symbolic, progress, CLI, Worker,
Public Kernel projection, and raw-replay guards share the checked semantics
without weakening bijectiveenum conversion(#455). -
Native
verifynow reports thevacuous_leadstovacuity lane: aleadsTo
whose trigger never becomes reachable within--depthis a hollow
property, the same class BMC already reports for an implication invariant's
unreachable antecedent (vacuous_implication). Detected via the same
solver-free existential-reachability BFSfsl-runtime::verification_warnings
already uses, sofsl-runtimegains no new solver dependency (#465, partial;
always_true_requires,tautology_over_frozen, andurgency_freezeremain
unimplemented on native — seedocs/DESIGN-rust-port.md"Shared semantic
diagnostics").
Fixed
- The native LSP's document index no longer lets an
@annotation swallow the
declaration that follows it.@requirement("REQ-COMMAND", "…")shares its
name with therequirement NAME { … }declaration keyword, and the index
handled no@at all, so the annotation armed a pending declaration that
nothing between there and the next identifier disarmed — the next line's
construct keyword was consumed as the declaration name. In
examples/annotations/annotated_domain.fslthat registered a Property symbol
literally namedcommandand leftcommand Place { … }'s real namePlace
with no declaration at all, so definition/references/rename returned nothing
anddocumentSymbollisted a construct keyword. 13 sites across the four
examples/annotations/*.fslfiles were affected, every one of them from
@requirement. Nothing from@through the annotation's closing)may now
declare anything or consume a pending declaration, in any dialect; non-keyword
names inside an annotation stay references. Resolved by position, not by
removingrequirementfrom the declaration keywords, which would have broken
the real declaration form (#551). rust/fsl-lsp/tests/corpus.rsnow also asserts, over every corpus file, that
a declaration keyword owning a name has actually declared that name
(DocumentIndex::misprojected_declarations). The previous whole-corpus check
only required every identifier to have some entry, which a swallowed name
satisfies as a reference to nothing, so it passed while roles and scopes were
wrong. This is the drift detector the#504/#551class of index bug had
been missing (#551).- The native LSP's document index now recognizes
defdeclarations and their
parameters, aggregate/quantifier binders written with aname: Typeform
(count(c: Id where ...),sum(...),unique(...),exactlyOne(...),
forall/exists),is some(v)pattern binders, namedpreservation NAME { ... }governance blocks, and every name in a comma-separatedactor A, B
list — each previously indexed as an ordinary reference with no matching
declaration, sotextDocument/definition/references/renamereturned
nothing anddocumentSymbolomitted them. Also fixed areachable/domain
collision: both name a top-level declaration keyword (reachable NAME { expr },domain SpecName { ... }) and a relation builtin call
(reachable(r, a, b),domain(r)); an unconditional keyword match
previously consumed the identifier immediately following either builtin
call as a brand-new self-defining declaration, corrupting the referenced
state variable's definition/reference set (#504). fslc db checkno longer reports a confidently greenverified_under_assumptions
over a genuine compatibility violation.validate_dbnow rejects acheck compatibilityrule name outside the closed vocabulary and anenvironment schema lo..hithe declared migration plan never reaches (exit 2 instead of
silently checking nothing).findings()is rewritten around a materialized
per-migration column-state simulation (mirroring the frozen Python
reference's_static_findings) instead of syntactic pattern matching, which
fixes:set_not_nullwithout a priorbackfill(previously made the
violating kernel transition unreachable instead of reported);
rollback_not_equivalentnever firing for a lossy rollbackable
split/merge (onlydropwas checked);data_preservation_lossnever
firing for dropping an existing column (only split/merge were checked);
reads/writes checked against "a drop op exists somewhere in the source"
instead of actual per-snapshot column state (missed an initially absent
column with a live reader, and false-fired on a column dropped then
re-added); and an offline payload's TTL window never extending its
acceptance obligation past the emitting schema snapshot. Anactive
artifact's owncalls/expects/emits_offline/requiresdeclarations
are now checked too — previously only artifacts other thanactivewere
treated as consumers (#490).fslc db checkno longer false-fires against already-annotated migrations
and supported providers. Asupported(not justactive) artifact's
accepts/responds/providesnow counts toward compatibility (may_exist
still does not);drop ... destructiveis accepted alongside
irreversible, and dropping an already-absent column no longer fires
destructive_migration_unannotated;split/merge ... irreversibleis
accepted as a preservation classification alongsidelossless/lossy; and
every finding kind is now gated by its documented rule (rule_enabled,
centralized instead of scattered per-finding conditionals), so an opt-in
data_preserved/rollback_equivalentrule no longer fires when it was not
selected (#491).api_response_field_missing'sfailed_ruleis now the documented
api_responses_expectedinstead of the undocumented
api_response_fields_available(#492).fslc db observevalidates its observation envelope and every event
againstschemas/fslc/db/observation.v0.schema.jsonbefore evaluating them
— a declaredschema_versionmust equalfsl-db-observation.v0, and each
event must be an object with typed required fields and acapabilityfrom
the closed vocabulary — and fails with a located exit-2 error instead of
defaulting a missing/mistyped field into a fabricatedunsupported_artifact_observed
witness (#505).fslc db observenow matches an event'sflagssnapshot against artifact
window conditions the same wayfslc db checkdoes; an artifact observed
under the wrong feature-flag variant isunsupported_artifact_observed
instead of silentlyobserved_conformant(#506).fslc db import's SQL importer now lowersALTER TABLE ... DROP COLUMN
into adrop ... irreversiblemigration op as documented (previously
unimplemented — the construct fell through tounsupported_sql), and a
malformedCREATE TABLE(unbalanced/missing parentheses) now produces an
unsupported_sqlwarning instead of being silently skipped with no warning
and no table (#507).fslc ai compatno longer line-scans any readable file and reports
compat_profile_generatedwith a syntactically emptydbsystemfragment
(artifact { requires ; provides ; }) for non-AI input or an fsl-ai
project that declares noai_componentat all — indistinguishable from a
genuine clean result. Native now parses either a singleai_component
document or a full fsl-ai project (fsl_syntax::parse_ai_project,
AiComponent.tools[].schemapreferred over the tool name, matching the
frozen reference) and rejects wrong-dialect input and a component-less
project with exit 2 (#511).fslc ai eval/regress/driftnow execute the selected
statistical_property/ai_migration/observed_propertydeclaration
instead of aggregating records against two hardcoded example
metrics/thresholds.evalapplies every declared slice's
min_samples/ci_lower/ci_uppergate and evaluator-trust check
(fsl_tools::evaluate_statistical_property);regress/driftread the
spec path and the selectedai_migration/observed_property's declared
metric clauses (evaluate_migration/evaluate_observed_property) instead
of ignoring it; an unknown--property/--migrationselection is now a
check-time error (exit 2) instead of a fabricated statistical/observed
verdict.ai evalalso honors the documented--records-less invocation
by falling back to the declareddataset'ssourcefile. As part of this,
fslc ai drift's success result changes fromobserved_conformantto the
documentedobserved_supported(docs/LANGUAGE.md,
docs/DESIGN-assurance-classes.md);observed_conformantremains correct
and unchanged forfslc db observe, which is a different command with its
own, still-valid result vocabulary (#509).fslc ai evalresults now carry every field
schemas/fslc/ai/statistical-result.v0.schema.jsonrequires
(schema_version/status/slice/metric/n/estimate/threshold/
evaluator/assumptions, not justresult/interval/checks), and
every non-statistically_supportedterminal status (dataset_invalid,
evaluator_untrusted,slice_missing,insufficient_samples,
inconclusive) now exits 1 instead of 0 (#510).- Native
verify'svacuous_implicationlane now unwraps a
forall-quantified implication before checking antecedent reachability,
not only a bare top-levelBinary{op: "=>"}.docs/DESIGN-vacuity.md:22
already documented "a single=>directly underforall*" as the
primary shape, butverification_warningsmatched only
property.expritself being=>, so any invariant of the ordinary form
forall x: T { P(x) => Q(x) }was invisible to the lane: a hollow
invariant of this shape verified clean instead of being flagged, and
--vacuity errorhad nothing to select. Every leadingforallis now
peeled (nested foralls included, matching the frozen Python reference's
_implication_antecedent_candidate) and the antecedent is existentially
closed over the collected binders (reusing the existingexists_wrap
helper already used forleadsTotriggers) before the reachability
check; with zero leading foralls this is a no-op, so the previous
top-level-=>shape still works unchanged (#486). fslc refinenow classifies a zero divisor in a refinement
action-correspondence argument expression (impl_action(a) -> abs_action(a / c), wherecis an impl state variable that can be zero)
as a locatedrefinement_failed/kind:"map_partial_op"finding, joining
the existing closed kind set (abs_requires_failed/abs_state_mismatch
/stutter_changed_abs/map_out_of_bounds). Constructing an abstract
action call is action context (docs/DESIGN-divmod.md§2.2), not the
read-only "mapping expression" §2.3 exempts (a refinement state map,
distinct from an action-correspondence argument), so it gets the same
partial_optreatment a division inside the abstract action's own body
would. Before this fixcheck_refinementpropagated the divisor's raw
RuntimeErrorunclassified, surfacing asresult:"error",
kind:"type",message:"division by zero"— neither of the two
documented/0treatments, and not a member of the refinement contract's
kind set. A correspondence whose divisor is always guarded on every
reachable impl step is unaffected and stillrefines(#512).analyze's TSG no longer leaks the internal db-dialectQqDbSepqQ
separator sentinel into node labels: a db-dialect invariant/action label
now matches the display nameverifyreports for the same target (both
now go through the sharedfsl_core::display_name; two independent local
reimplementations infsl-tools—analysis.rs'sdisplayand
typestate.rs'sdisplay_name— were each missing the sentinel
replacementfsl_core::display_namealready does). Node ids keep their
raw, guaranteed-unique internal form, but--focusnow also accepts a
node's displayed name (resolving it to the matching raw id), so a caller
no longer needs to know the internal sentinel to reference a target
verifyalready named for them.action_dependency_graphno longer collapses an action pair connected
through more than one shared read/write state bridge down to a single
bridge.enables/conflicts_withedges are deduplicated by
(from, kind, to), and the projection previously pushed one edge object
per bridge state for the same pair, so the dedup silently kept only
whichever state happened to be processed last (alphabetically, since the
underlying map iterates that way) — renaming a state variable could flip
which bridge a pair reported, andprogressless_cycle
(--profile ai-review) attached its leadsTo/terminal check to only that
one surviving state, potentially missing progress attached through a
different shared bridge. Every bridge state for a pair is now aggregated
intostates(plural) before emitting one edge, and the
progressless_cycleconsumer reads the fullstatesarray instead of
the legacy singularstatefield (#498).analyzebatch mode no longer silently drops an explicitly-named input
just because it does not end in.fsl.collect_analysis_files's
*.fsl-only filter applied even to files named directly on the command
line (not only to directory expansion, where it belongs), so
analyze a.toml b.txtreturnedfiles:[],errors:[],
result:"analyzed", exit 0 — a batch that analyzed nothing reported
success. Explicit files are now always kept regardless of extension: a
.tomlproject manifest routes through the same handling single-file
mode already uses, and anything else that cannot be analyzed is a real
error infiles[]/errors[]. Directory expansion is unaffected and
still filters to.fslonly (#496).fslc refine!maps autono longer guesses a binding for an incompatible
same-name action pair. Auto-matched action parameters are now resolved by
name only, never by position: with implgo(a: K, extra: K)and abstract
go(wanted: K)under a baremaps auto, native previously bound
wanted <- apositionally and silently droppedextra, returning
refines/exit 0 for a mapping the frozen Python reference correctly
rejects. Now a same-name action pair with a different arity, a surplus
impl parameter, a renamed parameter, or an abstract parameter with no
same-named impl counterpart is a locatedkind:"type"error, matching the
contractdocs/DESIGN-refinement.mdalready documented. A legitimately
auto-mappable pair — including a pure parameter reorder — still auto-maps.
examples/e2e/2_requirements.fsl'ssubmitcorrespondence (a genuine
arity-changing map between the business and requirements layers) now
declares its correspondence explicitly instead of relying on the removed
guess (#494).fslc refine! init-state handling now reasons over every concrete
initial valuation a nondeterministicinitpermits (aninit ifreading
a state variable init never assigns, on either the impl or abs side)
instead of comparing against the single default state a solver-free
Monitorhappened to materialize. This closed two opposite false
verdicts: a nondeterministic implinitno longer lets native silently
check only the default branch and miss both that branch's own
initial-correspondence violation and the reachable set below the other
branch (previouslyrefines/exit 0 where the frozen reference correctly
returnsrefinement_failed/abs_state_mismatch@init/exit 1); a
nondeterministic absinitis now checked as set membership rather than
equality against one materialized abs default, so a correct refinement
whose impl deterministically starts in a different, still-valid, abs
initial branch is no longer wrongly rejected (previously
refinement_failed/exit 1 where the frozen reference correctly returns
refines/exit 0). The step-0 impl self-consistency precondition (#466)
shares the same enumeration, so a self-violation reachable only from a
non-default initial branch is no longer missed either. A state variable
assigned on only some init paths (not any) is unaffected and keeps the
prior single-valuation behavior (#493).fslc chain's project-manifest reader now fails closed instead of silently
discarding malformed input: an unrecognized top-level section (including a
plain typo), zero recognized[business]/[requirements]/[design]/
[impl]sections (including an empty manifest), or a present-but-unparseable
depth/refine_depthvalue (for example one followed by a TOML inline
comment) is now akind: "parse"error at exit 2 instead of a silently
dropped layer, a vacuousverifiedover zero executed layers, or a silently
substituted default depth that understates a declared depth (#489).fslc chain's documented default and bare-filename invocations (e.g.
fslc chainorfslc chain fsl-project.tomlfrom the manifest's own
directory) no longer fail the[impl]layer with an io error: an empty
manifest parent directory now normalizes to.before resolving files and
launching the implementation command (#500).fslc ledger --impl-logno longer discards a replay error (missing file,
malformed JSON, wrong-spec trace, schema-invalid trace): it now fails the
wholeledgercommand through the standard error envelope and exit code,
the same as--evidence, instead of silently rendering a ledger with the
implementation-log conformance row missing (#499).fslc ledger --evidencenow surfaces a failing external evidence source
(a definitive nonconformant/mismatch/unsupported verdict) as a 🔴 要確認
finding for every requirement it attaches to — recursively, via its root
requirements/requirement.idor arequirement.idnested inside a
findings/checksarray item — or as a spec-level finding when it fails
with no requirement attribution at all. Previously a failing source only
ever affected the assurance-class column, so a requirement explicitly
attached to failing evidence still rendered green with no finding; the
assurance-class computation itself is unchanged (class and verdict stay
orthogonal, so a failing source still never lowers an independently
proven requirement's class) (#508).- Breaking:
fslc typestateno longer treats a state comparison in one arm
of anoras sufficient to pin a transition's from-state when the other arm
says nothing about the entity. Previouslyenum_guard_states/
option_guard_statesunioned the states extracted from both arms ofand
andoralike, sorequires e.status == A or bypassreported
derivablewithfrom: ["A"]and--tsemittedself: E<"A">even though
the action is also reachable from every other state wheneverbypassholds
— a generated ghost type that excluded behavior the checked model accepts.
ornow unions the states each disjunct implies (asandalready did),
computed per entity, but only when every disjunct actually constrains
the entity: a disjunct that says nothing about it (e.g. an unrelated flag)
drops the guard entirely rather than leaving the other disjunct's state
unioned in on its own.status == A or status == B— where both disjuncts
constrain the entity, just to different states — correctly remains
derivablewithfromcovering both; only a mix of a real state
constraint and an unconstrained disjunct (likestatus == A or bypass)
stops being derivable.andis unchanged (#521). - Breaking:
fslc typestatestruct-field state machines are now scoped by
field name and owning struct type, not field name alone.EnumLocation::Field
previously carried only the field name, soenum_expr_location/enum_lvalue_location
matched any struct field with that name regardless of which struct declared it;
two structs that both declare a same-named field backed by the same enum type
(e.g.struct Order { status: St }andstruct Ticket { status: St }) had each
entity's report and--tsoutput absorb the other's transitions — a method
emitted against the wrong host type. The same field-name-only matching also let
a whole-struct-literal reassignment (ticket = Ticket { status: Closed }, as
opposed toticket.status = Closed) leak into an unrelated same-named-field
entity's report, up to fabricating aderivabletransition it never had.
Field accesses/writes and struct literals now carry the public Kernelnamed
type of their base/own declared type (already present on every public Kernel
v1 expression/lvalue/struct-literal node), and a struct-field location matches
only when the field name and that owner type both agree. Two entities with the
same field name and different owning structs now stay fully independent in the
JSON report and in--ts; an entity with multiple actions over its own field
still aggregates into one machine, unchanged (#520). fslc scenarios --deadlock errornow preserves the sameviolated/
deadlock/ exit 1 verdictfslc verify --deadlock erroralready reports
for the same spec, instead of silently discarding the promoted failure and
returningresult:"scenarios"/ exit 0.--deadlock warnstill generates
adeadlock_terminalscenario, now carrying the required explanatory
note: "after these steps no action is enabled"(#522).fslc scenariosno longer describes a never-enabled action (a genuinely
unsatisfiablerequiresconjunction, per verify's ownaction_coverage
verdict) as "was enabled but no cover trace could be built": it now says
the action is never enabled, with the same hint andblocking_requires
verify already computes for it. The original wording is reserved for an
action verify found enabled whose cover trace scenario generation still
failed to build (#523).fslc scenariosnow warns for every quantifiedleadsTobinding without a
response witness individually, instead of collapsing completeness to one
warning per property name: a witnessed binding for one binding value no
longer hides the missing-response warning for every other binding. A
binding whose antecedent never held within--depthis now worded
distinctly ("antecedent never holds within depth K") from one whose
antecedent held but never closed with a response ("has no response
scenario within depth K") (#526).fslc explain --readableno longer prints a branch-lowered action's
internalname.bNform: abranches { when P { … } maps Q }action now
resolves back to its authored name (via a newOriginChainbound at the
branch-splitting site), with abranch:line naming each branch's guard
andmapscorrespondence, and anImplements:section when the source
declares a refinement mapping — restoring the branch-lowering and
synthesized-refinement-mapping detaildocs/DESIGN-explain.md§2
documents. The JSON skeleton'sactions[].namefor the same branch
actions is corrected the same way, with the lowered form preserved as
generated_name(#528).fslc explain's JSON skeleton restores three fields the native
implementation had dropped versus the documented contract:spec_kind
(was hard-codednull),auto_checksentries ofkind:"partial_op"for
every syntacticpop/head/at///%site (previouslytype_bound
only), andgenerated:trueorigin provenance on the SLA-synthesized
tickaction and_deadline_*invariants (previously indistinguishable
from authored declarations) (#530).fslc domain checknow folds a nested kernel's non-verified/proved
result (violated,reachable_failed,unknown_cti,unknown_budget)
into the top-level verdict and exit code instead of unconditionally
reportingresult:"verified_under_assumptions"/formal_result:"verified"/
exit 0 regardless of whatkernel.resultactually said — a confidently
green false negative on a domain whose aggregate invariant the kernel
proves violated.run_domain_checkalso now returns a non-{0,1}verify
status (spec error, internal error) verbatim instead of letting it reach
check_domainand be misread as a kernel result, and the internal
stable_kernel_projectionallowlist gained the violated-path evidence
keys (loc,violated_at_step,violating_bindings,blame,
last_action,trace) so the nestedkernelstays a replayable
counterexample rather than only the bare verdict string (#515).fslc domain replaynow actually drives a concrete Monitor over the
lowered domain/effect model — steppingcommandandeffect_completion
log entries as real action calls — instead of only maintaining a
(effect, correlation_id)bookkeeping set. Previously only 1 of the 4
documented detection categories (completion without a prior request)
could fire; a rejected command, a duplicate irreversible effect commit,
and a lifecycle ordering mismatch (e.g. a completion observed after the
aggregate moved into a state that rejects it) all silently passed as
conformance_checked/exit 0.unknown_domain_event,unknown_effect,
effect_completion_event_not_declared, andunknown_runtime_event_kind
(including a{"kind": ...}-keyed log entry, which previously evaluated
zero log lines and still reported success) are also now detected, and
final_state/assumptionsare populated from the same Monitor and
assumption computationdomain analyzealready uses instead of always
returning{}/[](#518).fslc domain replayandfslc domain analyzenow reject an unrecognized
trailing argument (result:"error"/kind:"usage"/exit 2), matching
every otherdomainsubcommand and the rest of the CLI, instead of
silently discarding it and returning a result computed without it (#516).fslc lint/fslc migratenow checked-model-validate every input,
unconditionally, instead of only validating a fileplan_migrationfound
legacy syntax to rewrite. Previously an input with no legacy tokens for
plan_migrationto fix was never checked at all — a spec with a genuine
type error, or a canonical requirements spec whoseimplements ... from
target had moved, could still reportlint/migrateexit 0 purely
because it happened to have nothing to migrate, while the identical defect
with one unrelated legacy token present correctly failed. The new
pre-flight (shared by both commands throughload_migration_plan) mirrors
fslc check's own kind/location convention exactly, so the same input
gets the same verdict from both commands.refinement/agentdialects
are excluded (same carve-outfmt --checkalready uses: a mapping file
has nostateblock by design), and a refused plan (a legacy construct
plan_migrationrecognizes but cannot machine-apply, e.g.&&) is also
excluded, since no pre-migration checked model exists to compare for such
input in the first place (#517).Monitor::new(the solver-independent concrete interpreterreplayand
BMC's concrete pre-scan build on) now runs the same deterministic-init gate
the explicit engine's own construction check already had, instead of
silently default-filling any state componentinitleaves free and
treating that one arbitrary value as the specification's initial state.
Previouslyreplaycompared an observed trace's initial state against
that default and falsely reportedinitial_state_mismatchon a BMC-valid
trace whose free component held a different, equally admissible value. A
caller that already has its own complete concrete initial state (an
observed replay trace's own step 0, an explicit--from-state/
--initial-statesnapshot, or a BMC witness's first state) is unaffected:
it now builds through the newMonitor::from_state, which has nothing to
askinitto compute and so is not subject to the gate.
initial_state_mismatchis still fully checked wheneverinitdetermines
every state variable; when it does not, the whole state is trusted from
the caller rather than compared component-wise (#519).- Native semantic diff now evaluates OLD forbidden arguments in the OLD typed
model and reports missing actions, incompatible arity, or incompatible NEW
argument domains as explicitunknownfindings instead of a false
no_semantic_changeresult (#460, prerequisite for #427). fslc testgen(anddomain testgen) no longer converts a genuine
violated/reachable_failedcounterexample from the underlying scenarios
machinery into an unrelated exit-2kind:"semantics"spec error. The
guard that only short-circuited onstatus == 2let a realviolated
invariant/leadsTo, or a--strictreachable_failed, fall through to
fsl_tools::validate_scenarios, which found noscenariosarray in what
was actually averify-shaped envelope and reported a generic error —
changing the exit code from 1 to 2, replacingresultwith"error",
and destroying the trace/blame evidence a repair loop depends on. Both
guards (run_testgen,run_domain_testgen) now propagate any non-zero
status verbatim; the success path (result:"generated", exit 0) is
unaffected (#472).fslc refinenow verifies the impl spec's own internal consistency (type
bounds, invariants,trans,ensures) before checking any correspondence
against the abstraction. An impl that violates itself within--depth—
e.g. a droppedrequiresthat lets a state variable step outside its
declared type bound — is reportedresult:"violated"with anote
explaining this is a property of the refinement input, neverrefines
and never folded intorefinement_failed. Previouslycheck_refinement's
BFS silently discarded (continued past) any violation the impl produced
while stepping, so a guard weakening that broke the impl's own bounds
could pass asrefines/exit 0 with no counterexample at all.fslc diff
gains the same detection as a newimpl_violatedfinding kind that fails
its gate unconditionally (unlike other finding kinds, not subject to
--forbid), since a self-violating side makes the comparison untrustworthy;
theimplements-clause mutation oracle and theimplements:verify
metadata (requirements_implements_output) are also corrected to stop
reporting the impl-violation case as a clean/"refines"refinement (#466).--vacuity {error,ignore}now selects over the complete documented 5-kind
vacuity lane set (fsl_core::VACUITY_KINDS) instead of only the two kinds
spelledvacuous_*.always_true_requires,tautology_over_frozen, and
urgency_freezepreviously could not be promoted to--vacuity erroror
suppressed by--vacuity ignorebecause none of the three names start
withvacuous_; nativeapply_vacuity_modematched by name prefix rather
than the closed kind set (#465, the CLI half; the lanes for these three
kinds are still unimplemented on native, see above — this fix prevents a
second, independent bug from compounding the first once they land).sweepno longer folds a specerror(parse / type / semantics / io /
vacuous / a mistyped--instances/--valuesname / a missing file) into
the positivesweep_passed/exit-0 verdict. Any scope in the grid that
errors now short-circuits the sweep with that error's envelope and exit
code (2, or 3 forkind:"internal") returned verbatim, instead of being
discarded as an unrecognized grid-cell result while the top-level verdict
fell back to "no counterexample found" — previously a one-character typo
in--instancescould turn a sweep over a genuinely violating spec from
exit 1sweep_failedinto exit 0sweep_passed(#464).verify --engine explicitnow agrees with symbolic BMC on a contradictory
init: aforallbinder that writes different concrete values to the same
non-indexed location across binder values (e.g.forall k: K { x = k }
with|K| > 1) is detected by the concrete Monitor without a solver and
reported asresult:"error",kind:"vacuous",
message:"init constraints are unsatisfiable", exit 2 — matching BMC
exactly instead of silently running the forall as a last-write-wins loop
and returningresult:"proved"/ exit 0 for a spec with no valid initial
state (#480).fslc testgen'spytesttarget now emits theforbidden-scenario rejection
assertion (result = adapter.step(...)/_assert_rejected(...)) that every
other testgen target already emitted, restoring byte-identity with the
frozen Python reference and giving the generated conformance harness a real
negative control against a guard-weakened implementation (#471).- The
examples/gallery/injected/negative-control detector matrix and its
primary/blind calibration are now measured on the authoritative native CLI,
not only the frozen Python reference: 17 injected specs and the
agentic_ragbackported_constraints_slice.fslmutation slice carried a
requirementannotation text conflict between their outer block and an
inner legacy tag (checked-model error since #237) that the corpus never
followed; the redundant inner tag is removed and the outer block's already-
correct requirement text is the sole source of truth. Adds
rust/fslc/tests/injection_detector_matrix.rs(the native primary/blind
matrix, run bytools/check-native-integration.sh) and
rust/fslc/tests/corpus_check_sweep.rs(an exhaustivespecs/+examples/
checksweep closing the gap left byrust/fsl-lsp/tests/corpus.rs, which
never builds a checked model) (#485). preserve progressnow works with an indexed (per-element) refinement state
map (map a[i: K] = expr), not only scalar maps.fsl-core::substitute_expr_indexed
substitutes each pulled-back reada[e]with the map's own expression, its
binder replaced bye(DESIGN-refinement.md's "substituted on the read"
rule), matching capture-avoidance already applied to scalar substitution.
rust/fsl-verifier/src/refinement.rs'scheck_refinement_progressno
longer rejects the mapping outright the moment any indexed map exists
(previouslyVerifyError("indexed progress map for '<name>' is not implemented"),kind:"semantics", exit 2) — including when the pulled
leadsTodoes not even read that map. This restores the documented
DESIGN-refinement.md:20-40 canonical shape (indexed map +preserve progressin the same mapping) and theexamples/agentic_ragand
examples/multi_agent_systemcross-layerrefinecommands, which
previously reported the verifier's own missing feature as a spec error
instead ofrefines/refinement_failed(#483).- Zero-division is now totally defined in property-context expressions
(invariant,trans,reachable,leadsTo, and refinement state mapping),
matchingDESIGN-divmod.md§2.1/§2.3:a / 0anda % 0evaluate to0
instead of raising a concreteRuntimeErrorthere, in both the Z3 symbolic
encoding (div/moduloare now pinned withite(divisor == 0, 0, ...))
and the concrete Monitor/BFS evaluator. Previously an unrelated/0/%0
inside an invariant could mask a genuine, independent invariant violation
behind a misattributedviolation_kind:"partial_op"/_partial_<action>
counterexample under the default (BMC) and--engine inductionengines,
and made--engine explicitreturn a rawresult:"error"/kind:"semantics"
instead of the real verdict — breaking the documented symbolic/concrete/BFS
agreement invariant. Action-context (requires/body/ensures) division by
a divisor that can reach zero is still reported aspartial_op(§2.2 is
unchanged); Euclidean negative-number division/modulo semantics were
already correct and are unaffected (#477). - The recursive
agentdialect (docs/LANGUAGE.md§13.6) now has a real grammar and structural
analyzer in native, matching the frozen reference'ssrc/fslc/ai_parser.py/ai_agent.pyexactly
(confirmed by byte-identical JSON, includingagent_ir/graph_summary, on
examples/ai/recursive_support_agent.fsland every documented finding-kind fixture). Previously
rust/fsl-syntax'sparse_agentonly balanced braces and discarded the entire body, so any token
soup that lexed cleanly parsed as an empty agent andfslc ai checkon a recursiveagentdocument
returned"expected an ai_component document"(native rejected the dialect entirely) whilefslc checkunconditionally reported the hardcoded constantagent_analysis_result: "agent_analyzed"
with no analysis behind it — a confidently green false negative on AI agent authority-delegation
safety, and native's own CLI contract (rust/fslc/cli-contract.json) already advertised "check an
ai_component hard contract or recursive agent structure" as a capability it did not have. Agrant authority/grant contextthat exceeds the immediate parent's declared boundary is now a check-time
kind:"semantics"error fromai checkandcheckalike; the six documented
agent_structural_violationfinding kinds (child_authority_exceeds_parent_authority,
child_context_exceeds_parent_context,irreversible_operation_without_human_approval_path,
visibility_leak_across_sibling_agents,low_trust_agent_path_to_high_authority_tool,
policy_review_bypass_in_orchestration) are all computed byfslc ai check.fslc verify's
rejection of agent documents andfslc fmt's refusal to reformat a well-formed agent body (no
native pretty-printer exists yet) are both unchanged (#468). - Native
ai_componentlowering (fslc check/verify/ai check) no longer collapses to a
one-boolean catalog sentinel with an unsatisfiable no-op action. It now generates the documented
Toolenum,human_approved/tool_executed/tool_suggested: Map<Tool, Bool>and
fallback_required: Boolstate,suggest_*/approve_*/execute_*/fallback_*actions (no
execute_*action is ever generated for a forbidden tool; an approval-required tool'sexecute_*
action always carriesrequires human_approved[tool]), and the
ai_forbidden_tool_not_executed__<Tool>/ai_approval_before_execute__<Tool>invariants.check hard { rule <Name>; }now rejects an unknown rule name as a check-timekind:"semantics"error
(previously silently accepted) fromai check,check, andverifyalike.fslc ai checkalso
implements the four previously-unchecked hard rules (tool_authority, the two static findings
were entirely missing;tool_schema_declaredwas missing;human_approval_requiredused a
narrowerirreversible && may_execute && !approvedpredicate than the documented
irreversible && !requires_human_approval && !forbiddenrule, so a tool only inmay_suggest
passed silently) and populatesrepair_candidatesinstead of always emitting[].fslc ai replaygains the matchingtool_authorityfindings forsuggest/executecalls outside
authority and now flags a declared precondition with nopreconditionsevidence object at all
(previously only an explicitfalsevalue was caught, so missing evidence passed silently). This
was AGENTS.md's "confidently green false negative" on the AI dialect's tool-authority /
human-approval safety claims (#470). - Native
fslc db checknow evaluatesrule all_active_writes_existthe same way it already
evaluatedrule all_active_reads_exist: dropping a column that is still declared as an active
artifact's write capability now yields acolumn_removed_while_still_writtenfinding with
witness,minimal_conflict_set,repair_candidates, andartifact_versionpopulated, and the
top-level JSONresultis reconciled to"violated"whenever the attachedkernelprojection
reports a violation. Previously the write branch was silently missing (a regressive port relative
to the frozen Python reference), so a write-drop incompatibility returnedverified_under_assumptions
with an emptyfindingsarray — a confidently green false negative, and one that escaped a
hardcoded kernel depth-8 default entirely for deep migration histories, since the findings layer
is depth-independent once the write branch exists (#469). - Native
fslc check/verify/db checkno longer reject two writes indexed by distinct enum
members as a possible alias ("an action may not assign the same state location more than once").
The write-aliasing analysis now resolves enum-member indices (both the typedExpr::EnumMember
literal and a local constant bound to an enum value) to their nominal(type_name, member)
identity, matching the same-index detection already applied toInt/Boolconstants. This
restoresfslc check/db checkon the fourexamples/db/dbsystemrename/split/merge
preservation fixtures, which previously exited 2 against a golden corpus snapshot of"ok"(#475). - Native
fslc check/verifynow emit the documentedfair_not_inheritedcomposewarning: when a
non-fair synchronized action references afaircomponent action,warningsincludes a
kind: "fair_not_inherited"entry naming the composite action and fair constituent(s), matching the
frozen Python reference's message andlocexactly. Compose lowering previously discarded
constituentfairmarkers with no warning at all (rust/fsl-core/src/compose.rshad no warnings
channel), so a declaredfairconstituent silently stopped contributing fairness through
synchronization with no diagnostic signal — the same failure mode issue #16 fixed in the frozen
Python reference, regressed by the native port (#474). - The frozen
tests/test_dialect_conformance.pycorpus-conformance harness (docs/DESIGN-conformance-harness.md)
is green again (0 failing of 212, up from 201 passed / 6 failed). Four intentional-errorgovernance
gallery fixtures gained the// expected-result: errorfront matter that reclassifies them as
DECLARED_ERRORinstead ofCONFORMANCE; the no-actiongovernance_semantic_before.fsl"before"
fragment and the threeexamples/causal/*.fslfiles (a dialect the frozen Python reference does not
implement at all) are now registered exclusions (MONITOR_EXCLUSIONSand a newcausalentry in
EVIDENCE_CONSTRUCTS/is_causal_source) instead of falling through toUNKNOWN. The always-red
harness had made a new registration gap indistinguishable from a pre-existing one (#476). - Refinement typechecking now rejects an unshadowed bare enum member shared by distinct
implementation and abstraction enums, preventing checked and evaluation
merge order from assigning different nominal identities. Existing identifier
shadowing by implementation inputs is preserved, unevaluable abstraction
constants are excluded, bijections use explicit typed conversion, and
many-to-one mappings use explicit source-total abstraction (#454, #455). - Refinement mappings can now declare an exhaustive, type-safe member-wise
conversion between distinct nominal enums and invoke it from state maps or
action arguments. This includes requirementsprocessstage enums, rejects
unknown/duplicate/non-total mappings and implicit ordinal conversion, and
fails closed in raw replay modes that lack a typed implementation model
(#450). - The embedded native CLI contract and exact help tree now include all six documented causal
command leaves, while the deliberately absentcausal verifypath remains rejected (#442). - Verifier transition-outcome agreement now proves actual guard failure and
representable defined post-update failure phases in native evaluation order,
rejects malformed, relabelled, partial, or checked-arithmetic evidence, and
fails closed forpartial_opor concrete evidence outside the bounded
symbolic representation (#428).
Changed
- The shared parser evaluation retains one cursor, annotation, location, diagnostic, and standard
grammar owner. The Public Kernel evaluation likewise retains its v1/v2 projector, but selects the
six independently evolving testgen/replay trace schema constants as one neutral-owner child slice
with unchanged crate-root symbols, byte-stable values, and schema drift negative controls (#399;
follow-up #448). dbsystemexecutable lifecycle lowering now constructs typed Kernel surface IR
directly instead of rendering and re-parsing generated FSL. The migration preserves
types, state, action/property semantics, metadata, annotations, and deterministic
ordering, removes the obsolete public source generator, and binds generated targets
to authored DB declarations so diagnostics and Public Kernel v2 provenance no longer
cite fabricated generated-source lines (#410).- The
fsl-runtimetouch-driven extraction evaluation retains the current public facade and
physical eval, Monitor, liveness, refinement, search, and replay owners. Evidence is insufficient
to select a move-only slice after the short migration/feature burst, so no implementation child
is created; future extraction remains gated by owner change, dependency direction, solver-free
production dependencies, and positive/rejecting semantic agreement controls (#398). - The
fsl-coredialect/domain lowering evaluation retains the existing syntax, checked dispatch,
structural lowering, and generated-source projection owners. Evidence is insufficient to select
a naming-policy slice or establish a current naming local optimum, so it creates no implementation
slice and records the exact evidence and controls required before sharing the duplicate private
core naming policy (#397). - The native CLI output-boundary evaluation inventories all 50 command leaves and rejects a typed
Command/CommandOutcomeframework. A future, separately scoped change may centralize only
regular-command JSON/raw process delivery, with exact byte, envelope, stderr, exit-status, and
Worker-separation controls (#394; follow-up #441). - The native causal command parser, I/O preparation, tool calls, and JSON/status projection now
have one explicitcausal.rsmodule owner, while top-level dispatch, shared process helpers, raw
output, serialization, and exit normalization remain inmain.rswith unchanged CLI contracts
(#393). - Native BMC, explicit/auto, and browser verification now share the single
verification_outputrenderer owner, while native engine selection and cache policy remain in
verificationwith explicit dependencies and unchanged output and exit contracts. Explicit
witnesses are replayed before rendering, and malformed or cross-family cache entries degrade to
misses instead of producing a verdict (#396). - The pure
conservation_candidateclassifier now belongs to the existingfsl-toolsanalysis
family, while the CLI retains ai-review orchestration, envelope formatting, mode validation, and
transport with unchanged findings, ordering, and exit codes (#395). - Test generation now receives an explicit delivery-normalized path context,
removing filesystem and CWD observation from the pure generator while
preserving existing, missing, and symlinked pytest paths and every other
target's output bytes (#423). - Requirements-document locale parsing and selection now has one private
neutral presentation owner shared by rendering, markers, glossary, and
document checking, while preserving thefsl_tools::Localefacade and
generated output bytes (#422). - BMC and induction now share neutral private liveness and trace-projection
owners, preserving verdicts, evidence, trace output, and solver boundaries
(#421). - Checked-model type validation now has one private neutral
fsl-coreowner
shared by model construction, refinement, and Public Kernel export, with
unchanged Kernel schemas, ordering, diagnostics, and fail-closed controls
(#420). - The duplicated Domain naming transform now has one private neutral owner used
by analysis, Kernel grounding, and code generation, with mixed-case, digit,
consecutive-underscore, and one-sided divergence controls (#419). - Claude's path-scoped Rust rule now links the accepted component and internal
architecture, names all crate owners and hard delivery boundaries, rejects
eager or unscoped C2 rewrites, and has positive/negative loading controls for
Rust versus frozen-Python paths (#392). - Migration-era porting, Python bridge, and sequence design text now marks its
historical authority explicitly and links to the maintained native Rust
contracts. The Kernel contract now records native outcome priority, while
future C2 ownership candidates are explicitly non-authorizing and require
separately scoped, audited, independently revertible changes (#430). - The authoritative Rust workspace now has one evidence-backed component design
record covering all eleven crates, their state and responsibility ownership,
dependency gates, public contracts, design pressures, and reevaluation
triggers, plus a companion internal design covering directed value flow,
mutable-state and failure ownership, targeted dependency normalization, and
touch-driven extraction. Migration-era documentation now consistently treats
Python as frozen compatibility evidence rather than the active product
authority, and concrete/symbolic failure agreement now includes a
corrupted-evidence negative control.