code_passenger 0.4.0 — scans a workspace with klepto, then does the writing klepto never does:
feature gates across every crate, cargo edits, headers, reports, and the history of it all.
workspace features (new in 0.3.0)
one document at the workspace root says what every crate's features are.
it is a 1o document, v1.workspace.1o.features; features.toml is accepted
for the same document (keep one of the two):
{
features {
default {
feature "log"
}
feature {
name "log"
dep "store:logsink"
dep "app:logsink"
}
}
}
# features.toml, the same document
= ["log"]
[]
= ["store:logsink", "app:logsink"]
feature log with dep "store:logsink" means: crate store gets a feature
log, and its dependency logsink becomes optional behind it. one feature
can stack several dependencies and span every crate. default turns the
workspace features on by default in every crate that has them. a crate's own
lines (crate { name "app" ... }, [crates.app]) replace a list verbatim
where the derived one is not what that crate needs.
what happens, in one run for the whole workspace:
- each crate's klepto spec is derived from the document.
- klepto plans each crate's gates: every use of a dependency that is now
optional gets
#[cfg(feature = "log")]on its carrier, and everything in the crate depending on those gets one too. - across crates: when
appuses an itemstorenow gates,app'slogturns onstore's (store/log) and the item inappis gated the same way (a module-levelusegets ausegate). this repeats until nothing new is gated. - the cargo check: one scratch copy of the workspace holds every crate's edits, and every crate the plan touches, and every crate using one, is checked with every feature combination that matters. a compiler error klepto maps to an item becomes a gate and the workspace is planned again.
- with each feature off, the report says what goes: the items unavailable, the unit each one is (a function, an item, a whole module, a whole file, the whole crate), every use site with its unit, and the callers in other crates that lose them.
exit codes: 0 nothing to do, converged, or written; 1 check found work
or the cargo check gave up; 2 refused, with a code: [CP0020] something a
person has to decide (a carrier klepto cannot judge, a compiler error no gate
answers), [CP0021] --write --no-cargo without --unproven (an unproven
plan is written only when you say so). nothing is written on 1 or 2.
the document's keys, rendered from its schema (schema/features/v1.schema.json,
declared once; a test holds this table to it):
| 1o key | TOML | value | meaning |
|---|---|---|---|
features |
(the document) |
block, required | The one block of the document. |
features crate |
[crates.<name>] |
block | One crate's own lines. Repeats: one block per crate. |
features crate default |
default |
block | The crate's default list as Cargo spells it, replacing the derived one. |
features crate default feature |
default = ["<entry>", ...] |
string | One entry of the crate's default list. Repeats: one value per line, in order. |
features crate feature |
[crates.<name>.features] |
block | One feature's whole list in this crate, as Cargo spells it; it replaces the derived list, in this order. Repeats: one block per feature. |
features crate feature enable |
<feature> = ["<entry>", ...] |
string | One entry of the list: dep:x, x/feat, x?/feat or another feature. Repeats: one value per line, in order. |
features crate feature name |
<feature> |
string, required | The feature. |
features crate name |
<name> |
string, required | The crate, by its package name. |
features crate optional |
optional = ["<dependency>", ...] |
string | A dependency made optional with no workspace feature (Cargo's implicit feature). Repeats: one value per line, in order. |
features default |
default |
block | The workspace features on by default, in every crate that has them. Absent: each crate's default list stays as it is. |
features default feature |
default = ["<feature>", ...] |
string | A workspace feature on by default. Repeats: one value per line, in order. |
features feature |
[features.<name>] |
block | One workspace feature. Repeats: one block per feature. |
features feature dep |
deps = ["<crate>:<dependency>", ...] |
string | <crate>:<dependency>: the crate gets the feature, and the dependency becomes optional behind it. Repeats: one value per line, in order. |
features feature name |
<name> |
string, required | The feature's name, the same in every crate. |
init also copies the schema to .passenger/config/schema/ and lists that
folder in oneo.project.json, where core-1o's workspace (and so
language-server-1o) looks: the document is checked live while you edit it.
headers
every file gets a header between //! code_passenger:begin and
//! code_passenger:end, written from the features plan: for each feature
with an item of the file behind it, the dependencies the file uses that the
feature turns on, the units of the file behind it, the items unavailable
with it off and the callers in other crates that lose them. a file wholly
behind a feature carries it as #![cfg(feature = "...")]:
//! feature_name:log
//! deps:["logsink"]
//! scope:["file store/src/sink.rs"]
//! corpus:true
//! unavailable:["store::sink::flush", "store::sink::flush_twice"]
//!
//! ----------------------------------------------
//! CORPUS FEATURES ------------------------------
//!
#![cfg(feature = "log")]
a header describes gates the code has, so annotate refuses ([CP0023])
while the features plan has work left. without a document it plans the one
init would write.
history
features apply --write checkpoints the workspace into the passenger store
before and after it writes (scan.json, analysis.json, the plan and the
document beside the snapshot). the store is the workspace root's
(<root>/.passenger/), whatever directory you run from; it tracks the files
klepto's scan reads and the document.
checkout refuses ([CP0042]) when a tracked file changed since HEAD's
snapshot (checkpoint first, or --force); a file the snapshot does not
hold is left as it is.
agent mode
one template per kind. features is custodian_kit's job, written by its
agent layout: the eligibility reading (public API, dependents, reach) that
changes nothing. features-apply is code-passenger's, the job that edits,
in .custodian/config/jobs/features-apply.toml.
the features-apply job is a folder that moves queued → running → done. its
steps are code-passenger lines started by custodian_kit's executor: the plan
(proven, written into the folder), the apply of exactly that plan (it reads
the plan step's receipt and refuses if the code changed since), and the
headers (it reads the apply step's receipt). each step's receipt is in
.custodian/data/receipts/; the folder ends with closing.md and
closing.json.
the writers custodian routes to
custodian never edits a manifest or a source file itself; these do.
dep add without a version or a path is refused ([CP0060]) unless the
workspace table already holds the dependency; adding one that is there
updates its version and adds its features. dep remove refuses while a
feature still names the dependency ([CP0062]).
attr place puts one attribute line on the item at klepto's location of
it, by the rule the gates use: a #[cfg(...)] merges into the item's own
cfg (all(old, new)) or goes right above the declaration; any other
attribute goes above the item's first attribute, after its doc comments.
The same line already there is nothing to do. --item is klepto's id
(app::store::Engine::new), or crate::... for the file's own package; an
item klepto does not name in that file is refused ([CP0063]).
transform: a change spec made in the code (new in 0.4.0)
custodian's bounded jobs write no code by hand. the agent writes a change
spec (custodian_kit's change::ChangeSpec: one operation, its target, its
boundaries, any body as simple_impl 0.1.12 toolkit pieces), custodian
decides it in a preflight, and this line makes it.
every operation of custodian_kit's vocabulary is implemented here, and the
match over them is exhaustive: make_struct, make_enum, make_impl,
add_method, make_fn, add_field, add_variant, add_derive,
add_attribute, add_module, add_dependency (through dep add),
add_test, add_constructor, add_trait_impl, change_signature,
rename, move, extract, gate, anchor, replace_body (the body
from its opening brace to its closing one, the signature kept),
change_dependency (through toml_edit, every dependency table of every
manifest naming it, the other keys of an entry kept), inline (each call
becomes a block binding the parameters to its arguments, then the body; the
function removed when it is not pub) and wrap (the spec's pieces first,
then the body as it was, as the block's value). custodian_kit says where
each lands and renders the text; this crate finds the exact place with syn
(the item's span from klepto's line), and cascades where the operation says
so: every struct literal gets a new field's default, every call a new
parameter's argument, every resolved use a new name, a moved item a
re-export where it was (its import where it lands dropped, the private
imports it used carried with it). a call is found wherever klepto resolves
it: a binary's, a method's by its receiver, one inside a macro's tokens (in
the text, from klepto's place for it). a match without a catch-all is listed,
never edited. an item an attribute macro rewrites (#[bitpack]) is refused
for add_field and add_constructor, by the macro's name ([CUST0040]).
nothing is written before it is proved. the edits are checked with cargo on a
scratch copy, over the check matrix of the crate they land in and of every
crate that depends on it (a binary's package too), the library and binaries
and every target as a test (--lib --bins --tests, never --lib where there
is none); a fresh klepto scan of the copy must show the facts the operation
promises. a change no package checked is never proved, and its facts are read
on a copy the edits were written to, never on the tree as it is. a new file
in a folder no file has made yet gets its folder. with a job open in the
project, a run's receipt is the job's (a dry run's too). with --write the tree is checkpointed in the
passenger store before and after, and the record (every file's digest before
and after, the lines written, the checks, the postconditions) is kept.
passenger diff --file F prints the lines a checkpoint pair changed, the
lines custodian reads at a bounded job's close. a refused spec is [CP0070],
exit 2.
replay (custodian_kit 0.5) edits no code: it carries a state kept
elsewhere into the tree, whole files of any type, byte for byte. custodian_kit's
change::replay_read (the rule the preflight keeps) reads the whole
transition first: every path in its one spelling and inside the tree, each
named once, every file as declared before (its sha256, or absent), every
payload (beside the spec) the bytes declared after, every mode three octal
digits; one that does not hold refuses the spec and nothing is written. the
proof is the transition made on a copy of the files it names, each after state
(its sha256 and its mode) read back there; no cargo runs (a replay reproduces
a kept state; the experiment's verification judges it). with --write, after
the checkpoint, the tree is checked unmoved and the transition read again;
each replaced or deleted file's bytes are kept in the passenger's objects and
its mode beside the write's checkpoint; then everything that can fail is done
beside the files (each payload staged, a copy kept of each file to replace and
of each file to delete, which is then taken away), always under names nothing
had (a file already at one is never written over or taken away), each payload
is put in place, and any failure puts every file back as it was and says so;
each after state is read back in the tree, and a file of the writer's own it
could not take away is a postcondition that does not hold (exit 1). the record
names exactly the files the replay names, in path order. --revert RECORD of
a replay puts back the exact state before (bytes, modes, files it made taken
away, files it deleted made again, the folders it made taken away when empty),
all or nothing, while every file is still as the replay left it. a replay is
made alone, never in a --batch.
to-toolkit: a function's body as the toolkit calls it already is
custodian_kit's patterns reading lists the functions whose bodies are the
expansion of a simple_impl toolkit macro (ensure, require_some,
continue_if, getter, ...), exactly or nearly. this line swaps one of them.
the preflight: the body matches exactly (a near match is refused with its
difference named, [CP0070]), and the crate depends on simple_impl, or the
first step is an add_dependency spec that makes it so. each step is a change
spec transform makes (replace_body: the matched statements become the
calls, the rest of the body stays as written; a whole-method generator such as
getter! takes the function's docs and attributes inside the call). the proof,
on a scratch copy before anything is written: cargo expand of the function
before and after is the same in custodian_kit's proof form (its locals named by
where they are bound, since a macro's own names are hygienic, then the normal
form), and cargo test -p <crate> passes. with --write each step is made by
transform, proved again by cargo over the check matrix and recorded.
every command is a declared line
every command is declared once as an xccute line (simple_impl_derive), read
back from its words, validated, and decided by its own exit statuses; every
run ends with a receipt. with a .custodian/ folder the receipt is kept in
its journal, otherwise its digest is printed on stderr. both spellings work:
the program is behind the bin feature (on by default, so cargo install
keeps working); a library user turns it off with default-features = false
and gets no command line.
one crate, a klepto.toml spec (0.2.0's loop, unchanged)
# klepto.toml — move `beta` behind a feature `b` in package `app`
[]
= ["beta"]
= []
[]
= ["dep:beta"]
review before you gate
--json on any of them prints the structure instead of the text.
the rest of klepto, on the command line
notes
- klepto 0.3.1 and custodian_kit 0.2.0 come from crates.io (custodian_kit by
path too, beside it, when building from source); syn and proc-macro2, which
klepto builds on, find the spans
transformsplices at. - the cargo check runs
cargooffline; path dependencies only, nothing is downloaded and nothing outside the scratch copy is touched. - edits inside macro bodies are out of scope; carriers klepto cannot name
are listed under
unknownand a person decides.
releasing
Bump version in Cargo.toml and give CHANGELOG.md a dated ## <version> (YYYY-MM-DD) entry, then push to main.
CI (.github/workflows/ci.yml) tests on stable and Rust 1.90, runs clippy, and publishes to crates.io
through trusted publishing; a version already on crates.io, or an entry still marked (unreleased), publishes nothing.