v0.67.4
Fixed
-
a snippet session's
beforehook is now run once per package instead of once per configured session target.kotlinandkotlin_androidboth resolve toLanguage::Kotlin, andtypescript/node/wasmall resolve toLanguage::TypeScript, so several targets routinely describe one physical package and each carried its own copy of that package's hook. Every copy was executed, sequentially, before a single snippet could validate — and when the hook outrantimeout_secs, the run paid that whole timeout once per target. Within an activation group a hook whose command and environment match one already attempted now replays that attempt's outcome; failures replay too, preserving the timeout classification every affected target is reported with. -
run_commandno longer outlives the timeout it was given. The budget covered only the wait for the direct child; once that child exited, output collection waited for end of stream on pipes every descendant had inherited, so any process outliving the command — a Gradle daemon, an MSBuild node, an unwaited background job — held the call open indefinitely. A one-second budget was measured taking twenty seconds and still returning success. Output readers now buffer as bytes arrive and the drain gives up at a fixed grace, reporting everything the command actually wrote and tearing down the process group of anything still holding the pipes. -
SIGINT,SIGTERMandSIGHUPare now forwarded to every snippet subprocess group before alef exits. Snippet children are spawned into their own process group so a timeout can kill the whole tree, which also removed them from the terminal's foreground group: Ctrl-C reached alef and nothing else, so alef exited 130 while the entire hook tree — shell, build wrapper and build daemon — survived and reparented to PID 1, where a stale daemon goes on to poison the next run. A signal already ignored on entry stays ignored. -
alef verifyrefuses--compile,--lintand--langinstead of discarding them. All
three are visible, documented flags (--compilereads "Also run compilation check") that
the command destructured away, soalef verify --compileexited 0 having compiled
nothing — indistinguishable from a passing compile check. They now fail with a message
namingalef build --langandalef lint --lang, which do implement that work.
--exit-codeis unaffected: it is a hidden, documented no-op. Nothing in the polyrepo
passes the refused flags today. -
alef --versionno longer reportstree: DIRTYfor every binary installed withcargo install --git. Cargo drops a.cargo-okcompletion marker into each checkout it creates, and the build stamp classified the working tree withgit status --porcelain, which counts untracked files. Every git-installed binary therefore printed the "not reproducible from commit" warning, and a warning that fires on every install is one nobody reads — which is how genuinely dirty output ends up attributed to a commit it cannot be reproduced from. -
alef snippets gapsnow prints a gap-coverage report on every run — snippet roots and files discovered, documentation roots and pages actually opened, references found versus supplied by configuration, and required languages against snippet groups compared — so a "No gaps found." result can no longer read as a wider claim than the check made. A consumer that omittedrequired_languages,docs_dirsandinclude_base_pathsfrom itsalef.tomlpreviously read a clean gap report for a run in which the language-parity check never executed and not one documentation page was opened. -
alef snippets gapsnow names every unset input (docs_dirs/--docs,required_languages/-L,include_base_paths/--include-base-path) together with the check class its absence disables. -
alef snippets gapsgained--strict, which fails the run when an unset input left a check class with nothing to compare, so a CI job whose purpose is gap detection cannot go green by being unconfigured. An unsetinclude_base_pathsis reported but deliberately not strict-fatal: it makes include targets over-report rather than manufacture a false clean. -
alef snippets checkno longer skips its gap pass silently. With neitherdocs_dirsnorrequired_languagesconfigured under[crates.docs.snippets]the pass is still skipped, but the unset keys are now warned about by name, and understrictthe skipped pass fails the run instead of reporting no failure. -
Split
src/snippets/gaps.rsunit tests intosrc/snippets/gaps/tests.rs, dropping the file under the repository's 1,000-line cap and removing its file-size ratchet baseline entry. -
Java visitor bridge now derives the context struct layout from the IR.
VisitorBridge.javahardcoded a six-field context —tagName,depth,indexInParent,
parentTag,isInline— with fixed offsets, a fixedMemoryLayout, and a fixed six-argument
decodeContextreturn. Generated Java only compiled when the configuredcontext_typehappened
to be exactly(enum|i32, ptr, i64, i64, ptr, i32); every other shape failedjavacwith
constructor <Context> in record <Context> cannot be applied to given types. The layout,
the field offsets, the Panama value layout per field and the constructor arguments are now
derived per context type, so any field count, order, and scalar width compiles. -
The visitor context C ABI is derived once, in
codegen::visitor_context_abi.
The FFI backend'scontext_c_type/context_field_specsdecided the#[repr(C)]shape and
which fields have no C representation; the Java bridge re-stated that shape by hand and the two
drifted. Both backends now read the same derivation — field order, scalar widths,#[repr(C)]
padding, struct size, and the skip decision. -
Context fields the C struct cannot carry are decoded as Java's own zero value.
The FFI backend drops fields with no C representation (floats, collections, nested structs, any
optional that is notOption<String>). The record component still exists, so the Java bridge
passesnullfor reference components and the primitive zero for value components rather than
fabricating a value or refusing to emit the bridge at all — the options record that holds the
callback references the visitor interface whether or not the bridge exists. -
A payload-carrying context enum is no longer decoded from its discriminant. The Java binding
emits tagged and untagged unions as sealed interfaces with novalues(), so an ordinal cannot
reconstruct a variant; such a component now takes the absent value instead of emitting Java that
does not compile. -
FieldResolver::accessorandFieldResolver::rust_unwrap_bindingeach carried a private copy of
the virtual-namespace strip decision, gated onresult_fields.contains(..)where the shared
result_relative_pathasks the broaderis_valid_for_result(..). The copies could place the same
fixture field somewhere the classifiers did not — the defect shape that emitted
string(result.ActionResults)into a generated Go package. Both now callresult_relative_path,
so accessor emission,is_array, and the zig/brew/C serialized-path navigation share one
definition of where a field's value lives. -
An accessor whose virtual prefix hides a field the IR reaches but a hand-maintained
result_fields
omits now strips that prefix, instead of emitting a member access against the virtual label. -
A
result_fieldsentry the IR marksbinding_excludedno longer strips its virtual namespace
prefix in accessor emission.with_ir_fieldsalready warns that such an entry is a config bug and
no binding emits an accessor for the field, so neither spelling compiles; the accessor now agrees
withis_arrayand the serialized-path generators rather than keeping a private answer.
Investigated whether alef adopt's --clobber-create-once-seeds over-gates an
unmarkable create-once seed. It does not: the gate is correctly protective. Only the
timing stated in the warning was wrong. Bullets below, for ### Fixed.
- `alef adopt`'s create-once-seed warning no longer names the wrong command as the moment of
loss. It said adopting a seed consents to alef "replacing its contents with a placeholder
seed on the next generate", but `write_scaffold_files_report`'s `can_skip`
(`!overwrite && !generated_header && exists && !is_alef_derived_output`) runs before the
ownership guard and consults no ownership signal, so a plain `alef generate` skips an
adopted seed exactly as it skips an unadopted one. The replacement lands on the next write
that passes `overwrite: true` -- an `alef version` scaffold regen, or
`alef all --clobber-create-once-seeds`. An operator who tested the warning by running
`alef generate`, saw the file untouched, and concluded the warning was false would have been
reading accurate output; the loss was simply still days away. The flag help, the
`NOT ADOPTED -- create-once seeds` stdout block, the per-path `warn!` and the seeds-only
`bail!` now all name the overwriting regen and say a plain generate skips these paths.
- The gating itself is unchanged, and is now pinned by tests rather than argued from doc
comments. For an unmarkable seed (`LICENSE`, `mvnw`, `gradlew`, `.gitkeep` -- paths
`marker_comment_style` answers `None` for), `alef adopt --write --clobber-create-once-seeds`
writes no byte of the file: `stamp_for` yields `None`, so the entire adoption is one entry in
the committed `.alef-ownership.toml`. That entry is precisely what
`write_scaffold_files_report` accepts as proof of ownership for an unmarkable path
(`owned = has_marker || (!is_markable && is_owned_by_ownership_record(..))`), so the
adoption is what clears the guard for the next overwriting write. Five tests in
`cli::commands::adopt::tests::create_once_seeds` measure the bytes on both sides of the
adoption and both sides of the write, including a control proving the identical
`overwrite: true` write refuses when the adoption did not happen.Note for the integrator: the 0.66 entry ("adopted through the committed record without its
contents being touched. It remains a create-once seed, so --clobber-create-once-seeds is
still required") is accurate as written and needs no correction — both halves of it are true,
and it never claimed the flag was unnecessary.
-
poly fmt no longer demotes every heading in a Markdown file that contains a stray second
level-1 heading. The generatedpoly.tomlnow disables rumdl'sMD025forfmtonly: the
rule still lints, so a stray#heading is reported, but its autofix demotes the offending
H1 and every heading after it, which reparents an entire CHANGELOG under## [Unreleased]
and makes heading-scoped release-note extraction emit the wrong section. Observed on this
repo's own CHANGELOG: 338 lines rewritten, all 122 version sections pushed to H3. -
Java: a visitor upcall whose host callback throws now returns the result enum's default
discriminant instead ofVISIT_RESULT_ERROR, a constant the bridge never emitted. Every
generatedVisitorBridge.javapreviously failed to compile with
cannot find symbol: variable VISIT_RESULT_ERROR. -
C e2e generator: the plain-function and engine-factory accessor emitters each carried a
hand-inlined copy of the virtual-namespace strip instead of calling
FieldResolver::result_relative_path, whose own documentation records that no further
copy should exist. Both now call the shared helper, so a fixture field grouped under a
virtual label (interaction.total_countaddressingtotal_count) cannot be classified
one way by the resolver and another by the emitter. Behaviour is unchanged; the
divergence risk is not. Deleting the strip outright previously kept all 298 C codegen
tests green, so the path had no coverage at all — both branches are now pinned by
regression tests that fail when the strip is removed.
Changed
-
Java: the convert-with-visitor
operationFailureslot is typed as the crate exception rather
thanThrowable. Every value assigned to it already is that exception and theThrowable
clause of the catch chain rethrows the slot itself, so theThrowabletyping compiled only by
virtue of the enclosing outercatch (Throwable). -
a snippet run that publishes the unconfigured
https://example.comfallback now
warns once, naming the affected fixtures and the config key that fixes it, and records
them onSnippetGenerationReport::placeholder_sample_url_fixtures. Generated output is
unchanged whensample_base_urlis unset. An unusablesample_base_url(empty,
whitespace-bearing, or scheme-less) fails generation instead of silently falling back. -
the build-time working-tree classifier now asks
git diff --quiet HEAD— tracked paths only, index and working tree both, so a staged addition or a deletion still counts as dirty. Untracked files no longer count: reaching the compiler requires amod/include!chain rooted at a trackedsrc/lib.rs, so untracked source that actually affects the build drags a tracked modification along with it. A denylist would have covered.cargo-okand then waited for the next tool's marker file. -
a repository with no commit yet now stamps
unknowninstead ofclean. There is noHEADto call the tree clean relative to, andcleanreads as a provenanced build.
Added
-
Java:
tests/backends_java_visitor_compile_test.rscompiles the generated options-field visitor
path with a realjavac— the convert-with-visitor method together with the generated helpers
it calls, and the generatedVisitorBridge— so type errors in the emitted exception flow and
upcall handlers are caught instead of passing substring assertions. -
alef verifyno longer reports create-once scaffold seeds as frozen generated files.
FrozenFilemeans "alef would write this path and the write guard refuses it forever", and
for a create-once seed the antecedent is false: alef emits the path only when it is absent,
so on an existing file there is no write to refuse and nothing is lost by the missing marker.
Verify nonetheless listed them under a "Frozen create-once seeds detected" heading whose only
remedy wasalef adopt <path> --write --clobber-create-once-seeds-- a flag alef's own output
labels DANGEROUS -- for files this project's documentation calls user-owned after scaffold
(generated_header: false). Measured in a consumer repo:alef adopt --converged-only
adopted 0 of 102 reported paths, 72 of them refused by alef itself as seeds, including 13
LICENSE files,e2e/java/mvnw,kotlin-android/gradlew,build.zig.zonand several
.gitkeeps. Recording ownership instead was considered and rejected: it proves nothing
further (alef still never rewrites the body, and the stamp covers generation inputs rather
than the seed's hand-grown contents) while handing the write guard the licence
--clobber-create-once-seedsexists to gate. The count is not dropped -- it is stated in the
new coverage report on every run, including a clean one, whereas the old heading was printed
only when some other check had already failed. Extends the 0.67.3 stale-seed fix rather than
duplicating it: that one taught the stamping pass to ask the question verify asks
(stampable_output_paths); this one removes the finding verify had no answer for. -
alef verifynow reports its own coverage on every run. Every finding verify produces is
a negative claim, so a green result was indistinguishable from a run that examined nothing --
and consumer CI reads it under job names like "Alef-generated bindings freshness" as a
whole-tree freshness gate. It is a far narrower claim: only files carrying an alef marker on
disk are held to a hash; markerless generated output (.json,.jar, lockfiles) is checked
for PATH PRESENCE only, so a present-but-wrong file passes; and anything outside the ownership
walk's scan set is never opened at all. Each run now prints the managed surface split into
content-verified / present-but-not-content-verified / absent, the files opened versus never
examined, unmarked create-once seeds, and marked files the surface does not claim. Follows the
alef snippets auditprecedent of naming the check class a run skipped instead of printing a
bare clean result. -
A stamped
.clang-formatwas written and then never read back by anything. The ownership
walk's scan set is documented to be a superset of everything the emit table can stamp, and it
had drifted:.clang-formatis scaffoldedgenerated_header: truefor every FFI target and
is stamped on write (it is YAML, so#line comments apply), but a dotfile with a single
leading dot reportsPath::extension() == Noneand the name was never added to
VERIFY_SCAN_FILENAMES. The walk filters on name and extension before reading any content, so
the file was not merely unverified but unverifiABLE. The scan predicate now asks the emit
side's ownis_markable_pathdirectly, so anything alef stamps on write is opened on read and
the two sides cannot drift apart again by hand. Widening the scan set only causes more files
to be read; a file is still reported only when it carries a marker, so this adds no false
positives. -
e2e: classify
is_arrayby the path the accessor actually addresses.FieldResolver::is_arraywas a barefields_arrayset lookup against the raw fixture spelling, whileaccessor— andresult_relative_path, the answer the zig, brew and C generators share — strip a virtual namespace label first. A field spelledinteraction.action_resultstherefore rendered as the sliceresult.ActionResultsand classified as not-an-array, so Go'scontains/contains_all/not_contains/contains_anyrenderers emittedstring(result.ActionResults)instead ofjsonString(...); converting a[]Ttostringis not legal Go, so the generated package failed to build.is_arraynow routes its fallback throughresult_relative_path(which also applies alias resolution) rather than growing a second hand-rolled namespace strip besideis_optional's, keeping one definition of where a fixture field's value lives. -
Added
[crates.e2e.snippets].sample_base_url: the public base URL generated
documentation snippets bind for a fixture'smock_url/mock_url_listarguments. It is
documentation-only — the executable e2e suite keeps binding the per-fixture mock server —
so a project can publish snippets a reader can actually run without changing what its
tests talk to. Relative fixture paths ("/pdf/report.pdf") resolve against the mock
server for tests and against the configured host for docs, from the same fixture, with no
per-fixture edit. An explicit$mock_urlplaceholder resolves against it too. -
src/bin_cli/tree_state.rs, the classifier, compiled bybuild.rsvia#[path]and by the crate normally, socargo test --libexercises the shipped code instead of a second copy of it. Its tests assert both directions — a checkout dirtied only by untracked files reports clean, and a tracked modification, deletion, or staged addition still reports dirty.
Removed
- Java: 31 unrendered templates. Twenty-eight were registered in
TEMPLATESbut named at no
render call site; three more had noinclude_str!of their own and passed the
every-template-is-registered check only because a sibling file has byte-identical content.