Contract, Interface and Harness as the assets of a codebase; the code is a cache pinned to them
chi is the project's name: the initials of Contract, Harness and Interface, and
知 (knowledge). The crates, the command and the repository are named chidev.
How a person or a coding agent works with it, from a request to pinned code, is in the guide.
cargo install chidev-cliThe package is chidev-cli, and the command it installs is chidev. A program that
uses chidev as a library depends on chidev (see "Crates").
| crate | role |
|---|---|
chidev |
Umbrella: the name users depend on. Re-exports the core. |
chidev-core |
Core (SDK): domain (pure rules and types), app (use cases and their ports), infra (adapters with real I/O). |
chidev-cli |
The chidev command. |
chidev-provider-rust |
The Rust providers: of mutants (chidev provider mutants-rust, a pure function from a source text to edits) and of Items (chidev provider items-rust, from a source text to the keys and lines of the declarations a caller can name). Depended on by the CLI only; the core reaches a provider as a process. |
The layer rules are in arch-lint.toml and run with cargo test. A crate or layer
that does not fit the project is deleted, together with its arch-lint.toml scope.
A project's root is the nearest directory with a chidev.toml file, looking at
the directory chidev runs in and then at each parent. The file may be empty; it
marks the root. With none up to the top, chidev exec stops with status 2 before
any operation: it does not fall back to the current directory and it does not stop
at a .git. A parent's chidev.toml is found when a project has none, as Cargo
finds a parent's Cargo.toml. The files were checks.toml and
.chidev/checks.local.toml before; one of those in a directory on the way up to the
root (the root included) stops chidev exec with status 2 and the name to give it.
Paths inside an operation (source_file, code_file) and in the settings are
relative to the root, so they mean the same from any directory of the project; a
path on the command line (the file to run, --store) is relative to the directory
chidev runs in. The RustDoc of infra::project gives the reasons.
The settings are the tables of chidev.toml, each described where it belongs:
[checks] (the levels and parameters of the checks), [external] (see "External
checks"), [measure] (see "[measure] in chidev.toml") and [store]:
[store]
path = "var/chidev.db" # relative to the root; an absolute path is kept as it isThe store is the database --store names; without it, the [store] path of
.chidev/chidev.local.toml (the person's own file, not kept with the project), then
the [store] path of chidev.toml, then .chidev/chidev.db under the root. The
person's file holds [store] and [checks] only: it can change a level or where the
store is, not register a program to run or a command to measure with. A table or a
key that is not known, or a table in a file that may not hold it, is refused with the
file named.
The tables of the store have a layout, a number the file keeps (PRAGMA user_version); it is 2. A file that is made now has layout 2. A file of layout 1
held a Crux inside its change request, and it is migrated once, when it is opened,
in one transaction:
- Each change request that held a Crux gives a Crux of its own. Its
raised_inis that request, itsfindingsare none (it was not raised from a finding), itsquestionis the request's title, which stood for it, and its options are numbered in order. A recommendation and a decision that named an option by the text of its choice now name the option's id, and the decision has an id. Asupersedes, which named a change request, names the Crux of that request. The change request documents are rewritten without their Crux. The revision of the store and of the model do not move: the model means what it did. - What cannot be moved stops the opening, and the file is as it was: a text that is
none of the options of its Crux, a
supersedesthat names a request with no Crux, a record that cannot be read, records that do not fit together afterwards. The error says what did not match. - A request resolved otherwise than as
donewhose Crux was never decided is moved as it was, a record of what was;cr.awaiting-decisionreports the Crux. - What was saved before is stale once where the Cruxes are in what it was stamped
over (see
viewunder "Operations").
An older chidev refuses a file of layout 2 and says which layout it reads, and a migrated file cannot be moved back, so copy the store before upgrading.
When a secret or other text that must not stay gets into the store, the guide's Troubleshooting says what to rotate, what the operations correct, what they do not, and what SQLite leaves in the file.
chidev exec <file> [--store <db>] reads one JSON object per line, runs each as an
operation, and prints one result line per operation ("ok", the line it came
from, and its fields). A line that fails gets "ok":false and diagnostics (see
"Diagnostics"), and the run goes on; the exit status is 1 if any line failed. Things are named, never
given by id (except by a correction, which names records by id, because a name can
hold the text it corrects; see "Correcting a record"): a Context by its name, an asset by its name within its Context, a
Clause by its label (C1), an Item by its key, a Case by its name.
The name of a Context and the name of an asset (a Contract, an Interface or a
Harness) follow one rule: not empty, and only the ASCII lower-case letters a-z,
the digits 0-9, - and _, so that a name never holds the # or :: a
citation ends it with, nor white space. A name that breaks it is refused where it
is read, and the message says which kind of name it was and names the first
character that is not allowed. A name kept in a store file that breaks it makes the file
corrupt.
These are the operations that build the model (the others are described below, where they belong):
{"op":"init"}
{"op":"context_add","name":"pricing","uses":["base"]}
{"op":"context_uses","context":"pricing","uses":["base"]}
{"op":"base_on_contract","context":"pricing"}
{"op":"contract_put","context":"pricing","name":"discount","title":"Discount",
"clauses":[{"label":"C1","text":"a discount over 100% is refused"}]}
{"op":"interface_put","context":"pricing","name":"price","source":"pub fn price(total: u64) -> u64;\n",
"items":[{"key":"price","lines":[1,1]}]}
{"op":"harness_put","context":"pricing","name":"price-tests","source":"#[test] fn refuses() {}\n",
"cases":[{"name":"refuses","lines":[1,1],
"items":[{"interface":"price","key":"price"}],
"clauses":[{"contract":"discount","label":"C1"}]}]}
{"op":"pin","context":"pricing","interface":"price","code":"fn price(total: u64) -> u64 { total }"}
{"op":"status"}-
initmakes the project (once: a second is refused,project.exists).context_addadds a Context;uses(optional) names the Contexts it uses, whose Contracts and Interfaces its Cases may cite.context_usesreplaces that list.base_on_contractbases a Context on its Contracts (it needs one, and there is no way back: a second one is refused,context.already-based). The three are edits like a put is, and answer withfindingsas a put does (see "Triggers and moments"). -
contract_putmakes the Contract or revises it, whole: theclausesgiven are the Clauses it has afterwards, a label it had keeps its Clause, a label it did not have is new, and one left out is removed. A label isCand a number. The answer has the asset'sidandrevision.- A Clause is known by its id, and its label is the name a person uses for it, as
an Item's key is. Its revision counts changes of its sentence only: a Clause
given another label by
replaces({"label":"C5","text":"..","replaces":"C2"}) keeps its id, its place in the order and its revision, and so does the Contract, if nothing else differs, so the Cases that cite it stay current and nothing goes stale. The label replaced must not be given to a Clause of the same put (input.replaces-kept), is replaced by one Clause at most, and is a label the Contract has (input.rename-conflict,input.unknown-name). A label the Contract has, given by two Clauses, isinput.rename-conflicttoo. A Harness put that still cites the old label is refused with the unknown clause (input.unknown-name); the citations already kept are by id and stay.
- A Clause is known by its id, and its label is the name a person uses for it, as
an Item's key is. Its revision counts changes of its sentence only: a Clause
given another label by
-
clauses_putchanges some Clauses of a Contract that exists and leaves the rest as it is:{"op":"clauses_put","context":"pricing","contract":"discount", "clauses":[{"label":"C1","text":"a discount over 100% is refused"}, {"label":"C4","text":"a discount is not given twice","replaces":"C2"}, {"label":"C9","text":"a new promise"}], "remove":["C3"]}- A Clause in
clauseswhose label the Contract has gets the sentence given (the same sentence moves nothing). One withreplacestakes the place of the Clause that has the old label, with the new label and the sentence, and keeps its id. A label the Contract does not have, withoutreplaces, is a new Clause, after the Clauses the Contract has, in the order given.removelists the labels of Clauses to remove; a Clause that a Case cites and that goes is refused as it is whencontract_putleaves it out. The Clauses not named stay, in their order, and so does the title: to change the title, or to put the Clauses in another order, usecontract_put. The Contract must exist (contract_putmakes it). - It is refused, with nothing changed and every problem of the entry said, when a
replacesor aremoveis a label the Contract does not have (input.unknown-name, with the Contract's labels asdid_you_meanandfieldsaying which of the two), when the label areplacesnames is also given as a label of the same entry (input.replaces-kept), and when a label is named twice: given by two Clauses, replaced by two, or removed and also given, replaced or removed again (input.rename-conflict, withsidelabel,replacesorremove). A result with two Clauses of one label is refused by the domain. - The answer has the Contract's
idandrevision, ascontract_puthas. It is an edit with a trigger of its own (put_clauses), and in achange_setit is the entry{"put":"clauses",..}(see "Change sets").
- A Clause in
-
interface_putis the same for an Interface:sourceis its code, and each item has itskey, the first and lastlinesit is on (from 1) and, when it is renamed,replaces(the key it had) to keep its id. Lines past the end ofsourceare refused (interface.lines-outside).- The
keyis the name the Item is known by: a path such aspriceorStack::push, one line, written by you or found by a provider (chidev does not read it; seeitemsbelow). It is the Item's identity: an Item whose key is one the Interface has keeps its id, and a citation of it ({"interface":..,"key":..}) names it. Two Items of an Interface do not have one key (interface.key-taken); give them keys that tell them apart. - The declaration is not written: chidev takes it from the code.
linespoints at the declaration alone, from its first token (after a doc comment and an attribute) to the line that ends the signature, with;or the{of its body (for astruct,enumorunion, the closing}: its fields and variants are what it declares); those lines, each without the white space at its end, are the Item's signature. A doc comment insidelinesis part of it, so editing the comment revises the Item. Lines with nothing in them are refused (interface.blank-item). - A declaration that changes goes to the Item's next revision (and the
Interface's), which makes the citations of it stale. A rename alone does not:
an Item given another key by
replaces({"key":"Stack::push","lines":[1,1],"replaces":"push"}) keeps its id and its revision, and so does the Interface, if the code is the same, so the Cases that cite it stay current. The key replaced must not be the key of an Item in the same put (input.replaces-kept) and is replaced by one Item at most (input.rename-conflict). itemscan be left out. A provider for the language of the Interface then finds them insource(see "Provider protocolchidev-items 2"): the language is thelangof the Interface's[[measure.layout]], and the command is[measure.items]ofchidev.toml. The keys and lines are the provider's, so nothing is written by hand, and a provider that fails or disagrees refuses the put before anything is read or kept. The Items are the declarations a caller of the code can name; the declarations the provider found that a caller cannot (a private helper, a test) are left out, and the result of the put says how many inleft_out(0 included), so that nothing is dropped silently. In achange_setthe entry of that Interface hasleft_out. Withitemswritten, no provider is started and there is noleft_out. A put that leavesitemsout is refused when the Interface has no layout or nolang(input.items-required), when[measure.items]has no command for the language (items.no-provider), when the provider answers what the protocol or the code does not allow (items.protocol), and when it cannot be run to its end or does not answer withintimeout(items.failed).- A key the provider gives differently (a path that moved, a renamed function) is
a new Item, and the old one goes, unless the put says it is the same one:
renames(only whenitemsis left out) is a list of{"from":"price", "to":"pricing::price"}, and the Item that had the keyfromkeeps its id, its revision and its citations under the keytothe provider found. Only the keys named are renamed. It is refused whenfromis not a key the Interface had ortois not a key the provider found (input.rename-unknown), when afromor atois named twice (input.rename-conflict), whenfromis still a key the provider found (input.replaces-kept), and whenitemsis written as well (input.renames-with-items; with writtenitems,replacesis how a key is renamed). statusshows the keys; write a key as it shows it. A store from before keys is read with each Item's old signature as its key: the lines of the signature, each without the white space at its ends, joined by one space (a signature that an Interface has twice gets#2,#3, ... from the second on). No revision moves, so nothing goes stale;items_rekey(below) brings them to the provider's keys, or put the Interface again with the keys you want, andreplaces(the old key) to keep an id.
- The
-
items_rekey({"op":"items_rekey","context":..,"interface":..}) gives the Items an Interface already has the keys and lines its provider finds now, with their ids and so their citations kept. It is how an Interface put by hand, or read from a store from before keys, comes to the provider's keys.- The provider is given the code the Interface keeps; the working tree is not
read. An Item and a declaration the provider found are paired when their lines
overlap and each overlaps no other on the other side. A paired Item takes the
provider's key and its lines. An Item that is not paired stays as it is and is
listed in
unmatched: no declaration is on its lines (a declaration the provider leaves out because a caller cannot name it is none), or it and the declarations share lines with others (two declarations on one line; an Item written over the lines of several). - The result has
rekeyed({"from","to"}of each key that changed),revised({"key","from","to"}of each Item whose revision went up),unmatched(keys),left_out(how many declarations the provider left out, as a caller cannot name them), and the Interface'srevisionafter it. A new key alone moves no revision. The signature is the text of the Item's lines, so an Item whose new lines declare something else goes to its next revision, and the Interface with it, and the Cases that cite it go stale;revisedis that list. - It is refused, with nothing changed, when two Items would have one key
(
interface.key-taken: a key the provider gave twice, or one an Item that is not paired still has), and when the provider cannot be asked (input.items-required,items.no-provider,items.protocol,items.failed, as for a put). It is one operation of its own, not achange_set, and it is kept as the store stood when it began: a store that has moved since refuses it. It is an edit, so the answer hasfindings: a citation it makes stale (cite.stale) is one.
- The provider is given the code the Interface keeps; the working tree is not
read. An Item and a declaration the provider found are paired when their lines
overlap and each overlaps no other on the other side. A paired Item takes the
provider's key and its lines. An Item that is not paired stays as it is and is
listed in
-
harness_putis the same for a Harness, andkindistests(the default for a new Harness),simorexamples. Each Case of tests or of a sim has itsnameand the first and lastlinesit is on insource(from 1): its test, an attribute above it included, or its case of a simulation; lines past the end ofsourceare refused (harness.lines-outside). A Case of tests cites one Item at least and any Clauses; a Case of a sim cites Clauses only, and a sim Harness hasbuilds, the Interfaces whose pinned code it runs (see "Simulation"). A Harness ofexampleshas nosource: each Case holds one acceptance example (see "Acceptance examples as a Harness").contextin a citation (default: the Case's own) reaches a Context that one uses. A citation records the revision its target has when the Harness is put. -
pinrecords that the code was accepted: it needs a Case that cites an Item of the Interface, and it goes stale when what it stands on changes. -
The text of an Interface, a Harness or a Pin can be read from a file instead of being written into the line:
interface_putandharness_puttakesource_filein place ofsource, andpintakescode_filein place ofcode. Exactly one of the two is given (both, or neither, is refused). The path is relative to the project's root (the nearest directory with achidev.toml, where the settings are read from); an absolute path, a path with a..part, a file that is missing or cannot be read, and a file that is not UTF-8 text are refused with the path named. The file is read when the operation runs, and the model keeps the text, not the path: changing the file later changes nothing until the operation is run again.linesof an Item and of a Case are still given as they are. -
statuslists the Pins and whether they hold, the citations behind their targets, the change requests, what was retired and the findings of every check. -
checkruns every check on the project as it is and answers with thefindings; nothing is kept. An edit cannot finish the model in one step, so a put answers only with what that put introduced (see "Change sets"): a Clause no Case cites yet, or a Context with no Interface yet, is not said by the put that left it so. The whole model is judged where something is confirmed (pin,cr_resolve) and bycheckandstatus.check_listlists every check with itslevel, itskind(changefor a check that says an edit made something stale or out of place,statefor one that says the model is not finished or not settled) and themomentsit runs at.{"op":"check","context":"pricing"}narrows the answer to one Context: the findings about it and about what it holds, and about a change request that is about it (a target it revises, retires or keeps is in the Context; the options of a question not yet decided name nothing). The run is the whole one and only what is answered is narrowed.outsidecounts the findings left out: those about the Project as a whole, about a change request that names nothing of the Context, and about something that is gone. It is in the answer only when acontextorruleswas given. A name that is no Context's isinput.unknown-name.{"op":"check","rules":["examples.","cite.stale"]}narrows the answer to some rules. A selector is written as a key of the policy file is ([checks."cite."],[checks."cite.stale"]): a rule id (cite.stale), or a family with its dot (examples.is every id that begins withexamples.;exampleswithout the dot is an id, andexam.is a family none of whose ids begin so). The findings are those of a rule one of the selectors matches. A selector that matches no builtin, judged or external check isinput.unknown-name, with the ids and families near it asdid_you_mean, and nothing is run. A malformed one (upper case, white space, empty) isinput.invalid.rulesandcontexttogether narrow both ways. The run is the whole one here too, andoutsidecounts what either left out.chidev checkruns the operation as a command (see "Commands:chidev checkandchidev measure").
-
viewreads the part of the model you ask for, as the model holds it (the Clauses' text, the Interfaces' source and Items, the Harnesses' source and Cases):{"op":"view","sections":["contracts","interfaces"],"contexts":["ledger"]}sectionsis required and names at least one ofcontracts,interfaces,harnesses,change_requestsandcruxes. There is no default that reads every section: leaving the field out isinput.missing-field(nameissections),[]isinput.view-sections-empty, and a word that is not a section isinput.invalid(withdid_you_mean).contextsnames the Contexts to read. Leaving it out reads every Context,[]reads none, and a name the Project does not have isinput.unknown-name. A fieldviewdoes not take (a misspelledcontexts) isinput.unknown-field, not a view of every Context. The change requests and the Cruxes belong to the Project, not to a Context, so they come wheneverchange_requestsorcruxesis a section, whatevercontextssays.- The result is the envelope of an operation (
ok,line,op) with thestore_revisionthe model was read at and theview.viewis{"version":5,"contexts":[..],"change_requests":[..],"cruxes":[..]}: a Context always comes with itsid,name,basisanduses, and with the sections asked for only. A section that was not asked for has no key; one that was and is empty is[].chidev schema viewdescribes this value ofview(see "Protocolchidev-exec 1"). - A Harness in
harnessescomes with itskind(tests,simorexamples). A Harness of tests or of a sim has itssourceand each Case itslines; a Harness ofexampleshas nosource, and each Case has nolinesand has itsexampleinstead:{"args":..,"stdin":..,"expected":..}, with"stderr"when the example states the standard error.sourceandlinesthat can be absent are a change of shape, so the view became version 5 with this, and the result of an external check saved before isstaleonce, as the model it was sent carries the version. - It is the part of the model that
check_rulesgives a judged rule and that an external check is sent, entry for entry, so a reader of a view sees what a rule is judged on. Asking for fewer sections is how a role is kept from the rest: an implementer that is to write code from the Contracts and Interfaces, and not from the tests that will check it, is given["contracts","interfaces"], and nothing of the Harnesses (their source, Cases or citations) is in what it reads. - A change request comes with its
parent(an id), only when it has one, but not with itssituation, its issuer or its Assumptions: those are the request in the words of whoever asked and the readings of those words, and what is to be relied on of them is in the Contracts. It has nocruxeither: the questions that stop a request are the Cruxes raised in it, and they are thecruxessection. A Harness writer given the view learns which part a put is for and which request a question stops. - A Crux in
cruxescomes with itsid,raised_in(the id of the change request it stops), thefindingsit was raised from, itskind,questionandimportance, itsoptions(each with itsid,choice,wording,expectsandconsequence), the optionrecommended(byoptionid, withreasonandconfidence),supersedes, itsstate(openoranswered) and, once decided, itsanswer: theidof the decision, thedecision(chosenwith the option id and the reason,worded,unspecifiedorwithdrawn), itsbeyondwhen it has one ({"requests":[<change request ids>]}or{"declined":<reason>}: where what the decision added outside its request went; an answer that says nothing has none), itsbasis,byandat. The words of a Crux (an option's wording and consequence, the reason of the recommendation, a decision in other words) are given as whoever raised or decided it wrote them, so they say no more of the requirements than that person put there. cruxesis a section an external check may read (see "External checks"). The stamp of a change request covers the Cruxes raised in it, and the stamp over the Project covers all the Cruxes. What was saved before the view was version 4 goes stale once and is judged or run again: an external result, since the model it was sent carries the version, and a judged finding about a change request that has a parent or a Crux, or about the Project of a store that has one. A stamp over a model with neither is what it was.
A put or a retire does not need an open change request, but it belongs to none: it
shows in the targets of no change request (only a change_set for the request
does, see "Change requests"). A change that touches
several assets and holds only as a whole is one change_set (next section).
Every operation that changes what the store keeps is the trigger of one use case, and the checks it is held to follow from the kind of use case it is. There are five kinds, and a use case belongs to one of them for good:
- An edit changes what the model says: a put or a retire, a Context added or
its
usesset, the Policy, the record of a change request (cr_open,cr_crux_raise,cr_crux_decide,cr_assume,cr_reopen),items_rekey. The checks of theeditmoment run on the model before it and after it, and the answer hasfindings: those the edit introduced. A mandatory check, or an external check that is a gate, refuses it only for those. - A confirmation claims the model is in a state:
pin(momentpin) andcr_resolve(momentresolve). The checks of its moment judge the whole model as it would stand, and everything they find is infindings. - A question asks for every check on the model as it stands:
check_run(momentcheck). It keeps what the checks said, external programs included, and refuses nothing, since it changes no asset. - A record keeps a fact and runs no check:
init,measure,simulate,examples(measureof kindexamples) andfinding_report. - A correction replaces a text that must not stay with a mark, in the records
nothing else changes:
record_correct(see "Correcting a record"). It is forced: it runs no check and nothing refuses it for what a check would find (ADR 0008).
The moments are edit, pin, resolve and check. They are what a check says it
runs at (moments in check_list, in a judged rule's file and in an external
registration), and what check_rules filters by (moment). The use cases of each
kind are listed where the types are, in the RustDoc of chidev_core::app::commit
(Trigger); a use case added there has to name its kind, and nothing is kept
that did not go through the entry of its kind. Each check run is kept with the use
case that ran it.
The builtin checks of the assets, their citations and their Pins:
| rule | default | moments | finds |
|---|---|---|---|
cite.stale |
warning | edit, check | a case cites an item or clause that changed after the case was written |
pin.stale |
warning | edit, pin, check | a pin whose code no longer stands on what it was pinned against |
pin.unpinned |
note | pin, resolve, check | an interface whose code was never pinned |
cite.uncited-clause |
warning | pin, resolve, check | a clause of a contract that no case cites |
cite.uncited-item |
warning | pin, resolve, check | an item of an interface that no case cites |
context.no-interface |
note | pin, resolve, check | a context with no interface |
context.no-harness |
note | pin, resolve, check | a context with no harness |
The moments were called hooks, h
8
ook in check_rules, and the moment edit was
put. Those words are refused, and the message names the one to write instead.
chidev exec is the protocol chidev-exec 1: it reads one operation per line and
prints one result per line, as above. chidev schema prints the JSON Schema
(2020-12) of the parts of it a program writes or reads, derived from the types that
read and write them, so a schema and the reading cannot differ:
chidev schema # lists the schemas, with the title and the id of each
chidev schema ops # one line of exec: an operation. title "chidev-exec 1", $id urn:chidev:schema:ops:1
chidev schema view # the `view` of the operation `view`. title "chidev-view 5", $id urn:chidev:schema:view:5
chidev schema external-call # what an external check is sent (see "External checks"). title "chidev-external-call 2"
chidev schema external-reply # what an external check answers. title "chidev-external-reply 2"
- A schema is a necessary condition, not a sufficient one. It says what a line
must look like to be read. A rule that sets fields against each other is not in it:
exactly one of
sourceandsource_file, exactly one ofoption,wording,unspecifiedandwithdrawn, a subject, an item or a clause. An operation that breaks one is read, and refused when it runs, by a rule with a name (see "Diagnostics"). - The schema of the results of the other operations is not printed.
- The version of the view (
chidev-view 5,versionin the view,VIEW_VERSIONin Rust) has its own promise: a field added does not change it, and only a change of meaning or shape (a field removed or renamed) does. A reader ignores a field it does not know and refuses a version it does not know.
Two operations can be run without a line of JSON. Each is the operation of the same name, run on the
project the command is in (found as for exec) and kept in a store that is already there; it prints the line exec
would print for that operation ("ok", "op", "line" 1 and the fields of the result, or "ok":false and
diagnostics), so a program reads it as it reads exec.
chidev check [RULE...] [--context <C>] [--store <db>]
chidev measure mutate --context <C> --interface <I> [--store <db>]
chidev measure simulate --context <C> --harness <H> [--store <db>]
chidev measure examples --context <C> --harness <H> [--store <db>]
chidev checkis{"op":"check","context":"<C>","rules":["<RULE>",...]}: every check on the project as it is, nothing kept. EachRULEis written as in therulesof the operation: a rule id (examples.failed) or a family with its dot (examples.), as a key of the policy file is. Thefindingsare those of the rules selected, and all of them when none is given.--contextnarrows them to a Context ascontextdoes.outsideis in the output when either was given. ARULEthat no check has (builtin, judged or external) is refused with the ids near it, and so is a Context that is not there.chidev measure <kind>is{"op":"measure","kind":"<kind>",...}. There is a subcommand for each kind, and each takes the asset its kind takes (mutatean Interface,simulateandexamplesa Harness) and no other, so a pair thatexecrefuses cannot be written: the argument parser refuses it. It runs for as long as the build and the tests, script or program take, and keeps the run.- Exit status.
check: 0 when no finding in the output is anerrorthat no change request deals with (itsstateisopen; one that a change requestaddressedordecidedis not owed, as the gate of a change counts what is still owed); 1 when one is, or the operation was refused (a rule or a Context that is not there, a selector that is not well formed).measure: 0 when the run was recorded and did not fail; 1 when it was recorded and failed ("outcome":"failed"), or the operation was refused (no Pin, a name the Context has not, no[measure]table). 2 for both when nothing could be run: bad arguments, no project or settings that cannot be read, or no store. - The store must be there. Unlike
exec, which makes it (itsinitis the first operation of a project), these commands open a store that exists and stop with status 2, naming the path, when it does not; they make neither the file nor its directory.--storeand the[store]setting say where, as forexec. - The
check_runoperation (it starts the external programs) and the other operations are run withexec.
Every rule of the Project is about the state a change ends in: a Case may cite only
what its Context owns or uses, a retirement may not take away what a Case that stays
cites. A put of one asset checks them on the Project that put leaves, so a change
that removes a Clause and an Item that Cases cite is refused whichever put comes
first, unless the Harness goes first, then the Contract, then the Interface, then
the Harness again. A change_set is that change as one operation: the rules are
checked once, on the state it ends in, and all of it is applied or none of it.
{"op":"change_set",
"puts":[
{"put":"contract","context":"core","name":"stock","title":"Stock","clauses":[{"label":"C1","text":".."}]},
{"put":"interface","context":"core","name":"stock","source":"pub fn reserve();\n",
"items":[{"key":"reserve","lines":[1,1]}]},
{"put":"harness","context":"core","name":"stock-tests","source":"..","cases":[
{"name":"reserve_refuses","lines":[1,1],
"items":[{"interface":"stock","key":"reserve"}],
"clauses":[{"contract":"stock","label":"C1"}]}]}],
"retires":[{"retire":"harness","context":"core","harness":"old-tests"}]}- Each entry of
putshas the fields of the operation of that name withoutop, and aputthat says which:context,contract,clauses,interface,harness,uses(context,uses) orbase_on_contract(context). Each entry ofretireshas the fields of the retire operation and aretire:context,contract,interfaceorharness. - An
interfaceentry takessource_fileand aharnessentry takessource_filein place ofsource(one of the two, as forinterface_putandharness_put; the path is relative to the project's root, and the model keeps the text read, not the path). The files are all read before anything is applied; when several entries have a file that cannot be used, the set is refused with each of them named. {"put":"clauses","context":"core","contract":"stock","clauses":[..],"remove":[..]}changes some Clauses of a Contract that exists, asclauses_putdoes. A Case of a Harness in the same set can cite a label the entry adds or gives to a Clause (the Clauses it did not name are cited by their labels too), and what the entry moved is the Clauses whose sentence changed, the Clauses added or removed, and the Contract. A Contract named by acontractentry and aclausesentry in one set is named twice, and refused.{"put":"context","name":"billing","uses":["ledger"]}adds a Context, ascontext_adddoes (usesis optional; the Project goes to its next revision). It is applied first, so the same set can put the Context's first Contract, Interface or Harness, and a Harness can cite into it. It may use a Context the same set adds, whatever order they are listed in. A name that is taken is refused.- The order the entries are listed in does not matter. They are applied in the
order the rules need: the Contexts added, the uses of the Contexts, the Contracts and Interfaces,
the basis, the Harnesses, then the retirements. A Case can cite a Clause or an
Item that the same change set makes or revises, and the citation records the
revision it has there; a Case that cites what the set removes fails the set.
That is so for a set without
for. A setfora request brings a citation up (records it at the revision its target has) only where the request's frame holds it or the citation is new, and leaves the others as they were: see "Attribution and frame". A sim Harness entry ("kind":"sim") may carrybuilds, the names of Interfaces of its Context, which may be ones the same set makes; one that is not in the Context, or that the set retires while the sim still builds it, fails the set. - What a put drops (a Clause, an Item, a Case) is recorded as retired, as it is
for a put alone; the answer's
retiredlists it, then what was retired. - Each asset moves by one revision at most: a thing is named once in a set, and naming it twice (or putting it and retiring it) is refused. An empty set is refused.
- When a set is refused nothing changes and the error names every problem found,
in names:
the case stock-tests release_frees (context `core`) cites the clause stock C7, which it cannot reach. The checks of theeditmoment run once on the Project before the set and once on the Project after it, andfindingsare those the set introduced: a finding the Project already had is not said again, and a mandatory check refuses the set only for what the set introduced. The same finding with a ground it did not have before (a Case that now cites one more thing that moved, a Pin that is stale for one more reason, a target moved again) is introduced; a finding whose grounds are all as they were is not. The store takes one commit. - The answer has
puts(each with itsput,context,name,idandrevision, in the order listed; ausesorbase_on_contracthas only the Context),retiredandfindings. A put or a retire alone is a change set of one and answers as it did. foris the title of a change request:{"op":"change_set","for":"Remove release", "puts":[..],"retires":[..]}. The work on a change request is applied as a change setforit. The request has to exist and not be resolved. The set records its footprint for the request in the same commit: the targets it moved, that is every target whose revision differs between the Project before and after (made, revised or gone: a Clause, an Item or a Case a put dropped, a thing retired, and the Contract, Interface, Harness, Context or Project whose revision moved because something in it was added or removed) and every Clause and Item given a new name, with the old name and the new, each with the containers it was in. The answer is the same;findingsincludescr.out-of-framewhen the set left the frame (see "Change requests"; a Case that cites a Clause or an Item the frame holds is inside it, and the footprint keeps what each Case it moved cited). A set withoutfor, a put or a retire alone, belongs to no request: it records nothing, shows in no request'stargets, and is judged by revision only for thekeepof requests. A Context added bycontext_add(and not by a setfora request) is such a move too.
Register a project's checker in chidev.toml:
[external."ext.contract-consistency"]
command = "./tools/check-contract"
args = []
moments = ["edit", "check"]
timeout = 10
reads = ["contracts"]
gate = false
[external."ext.contract-consistency".params]
strict = trueThe executable runs without an implicit shell, from the project's root (the nearest
directory with a chidev.toml). Bare command names resolve through PATH.
Registration is project-only; the person's .chidev/chidev.local.toml can change
policy, not register programs. A program has the caller's permissions: this is not a sandbox.
chidev exec accepts an explicit {"op":"check_run"} operation to execute
programs registered at the check moment and record their results. status and check never start
an external program; they show saved findings. status.external shows unrun,
current, or stale, comparing the supplied model sections and registration /
policy with the saved execution. current describes freshness, not success.
Results at off are omitted, and the program is not started. Scalar parameter
defaults are declared under the registration's params; existing [checks]
policy layers may override them with values of the same type.
At the moments edit, pin and resolve, the external checks registered for the
moment check the proposed model before it is saved (see "Triggers and moments" for
the operations that are held to each). Advisory findings are capped at warning.
gate = true makes every finding an error and refuses the change; execution and
protocol failures also refuse it. At pin and resolve every finding counts. At
edit the findings are held to what the edit introduced, as the builtin checks are,
when there is something to compare with: the saved result of the rule, if it
describes the model as it stood before the edit (the same input and registration,
which status.external calls current). The words of the registration that were
only renamed (moments was hooks, and edit was put) are counted as they
were, so a result saved before the rename is still current. A finding that result already had (the same subject and the same
message) is neither reported nor refused for; a program gives no typed reason, so
its message is what tells two findings about one subject apart. Without such a saved result every finding
counts, and a failed run refuses a gate whatever was saved. A gate cannot be
disabled or lowered by policy. A refused operation returns its findings and
execution evidence without saving them or the candidate change. Accepted changes
save their external results and invocation records in the same transaction.
check_run saves its results even when a gate finds an error, because it changes
no asset. A concurrent write refuses the commit, with no automatic process retry.
Programs receive one JSON document on stdin and return one on stdout. The request
is protocol 2: it has the moment the program is asked at (edit, pin,
resolve or check; version 1 called it hook and said put for edit), the
rules, read_at, params and the model. It does not say which operation asked:
a program is told when it is asked. (Which request a cr_resolve is resolving is
a concern of the builtin checks, which judge it though the model has it resolved;
it is not sent.) A response with no findings is
{"protocol":2,"findings":[]}; a response of protocol 1 is accepted as well (its
form did not change), and any other version is a failed run. Diagnostics
go to stderr. reads selects contracts, interfaces, harnesses,
change_requests and/or cruxes; unrequested sections are omitted, and a word that is not one, or
no word at all, is refused when chidev.toml is loaded. Context metadata is always
sent. The model sent is the view of the operation of that name, for every Context
and the sections read.
chidev schema external-call and chidev schema external-reply print the shape of
the request and of the reply. A reply is a protocol and its findings, each a
rule, a message, and, optionally, a subject ({"kind":..,"id":..}) and a
level:
{"protocol":2,"findings":[{"rule":"ext.contract-consistency","message":"C2 contradicts C1",
"subject":{"kind":"clause","id":"<the id of the Clause in the model sent>"},"level":"warning"}]}What the shape does not say, and a reply is refused for (a failed run): the rule of
a finding is the rule the program was asked for (the one id in the request's rules,
the key it is registered under: ext.contract-consistency above); its message is
not blank; its subject is one present in the model sent. A finding with no
subject is about the whole project, and one with no level is at the level its rule
is held to; what a level becomes (capped for an advisory rule, an error for a gate)
is said above. See the RustDoc of app::ports::external for the wire types, and
infra::external for process limits and platform support.
The execution evidence names the resolved executable; it does not digest its bytes, so it does not identify what the file, its script arguments or its dependencies held when it ran, and displaying a saved result does not check the filesystem. The check input consists of the selected model sections, not a snapshot of the working tree. Re-run a check after changing inputs outside that model.
chidev accept [--timeout <seconds>] <dir> -- <command> [<arg>...] runs the acceptance examples of a
directory against a program. An example is a name <name> for which <dir> holds a regular file <name>.args,
<name>.stdin, <name>.expected or <name>.stderr; other files and subdirectories are ignored, and the examples are
taken in the byte order of their names.
<name>.args: the arguments, split on white space. There is no quoting, so an argument that holds white space cannot be written. They follow the<arg>...of the command line.<name>.stdin: the bytes the program reads on standard input. An empty file is an empty input.<name>.expected: the expected standard output, then a last line#exit <code>(an integer in decimal, with an optional-). The output is everything before that line and keeps its own final line feed; a line feed after the exit line is optional. So an expected output is empty or ends with a line feed: a program whose output does not end with one cannot pass. A file whose last line is not#exit <code>fails the example.<name>.stderr: optional. When present, standard error must equal its bytes exactly; when absent, standard error is not compared.
.args, .stdin and .expected are required. An example with one missing fails (missing a1.stdin), and so does one
whose file cannot be read (the file and the system's error are named) or whose .args is not UTF-8. A directory that
cannot be read, or that holds no example, is not an example failing: nothing can be run.
The program runs without a shell, from the directory chidev accept runs in. A <command> with a directory part is
taken relative to that directory and is not followed through links; a bare name is searched for in PATH. It runs in a
process group of its own, which is killed as soon as the program exits, so a descendant is neither left running nor
keeps the example open. A program that exits without reading all of its input is judged on what it answered.
Standard output and standard error are each capped at 4 MiB, and more fails the example. --timeout (seconds, from 1
to 86400, 10 by default) bounds one example; the examples run one after another.
A program that gave no answer fails its example with that one reason: cannot start: <message>,
did not end within <n> s, output over 4 MiB (<pipe>), ended by a signal, or the system's message when a pipe
could not be read or written. Otherwise every difference is a reason, in this order: standard output differs at line <k>: expected <line>, got <line> (a line is shown escaped, and only its first 200 characters, followed by ..., when
it is longer; end of output stands for a side with no such line; expected a final newline or expected no final newline says
that only the last line feed differs), exit code: expected <e>, got <g>, and, with a .stderr, standard error differs ... in the same form as the first.
It prints ok <name> or fail <name>: <reasons, joined by "; "> for each example, then
<p> passed, <f> failed. The exit code is 0 when every example passed, 1 when one failed, and 2 when nothing could be
run (a usage error, a directory that cannot be read or has no example), with chidev accept: <what> on standard error
and nothing on standard output.
chidev accept is a runner for a person and keeps nothing. The examples the model holds, those of a Harness of
examples (see "Acceptance examples as a Harness"), are run against the pinned program by measure of kind examples
(see "Examples"), which keeps the run and has the checks judge it. Both judge one example by the same code, so they
cannot differ on what a program that ended answered.
A Harness of kind examples holds the acceptance examples of a program in the model. It has no source; each of
its Cases is one example, named as its files are (a1 for a1.expected), holding the example's four texts, and
citing the Clauses that state what the example shows. A change of one text alone is a change of that Case: the Case
goes to its next revision and so does the Harness, so what stands on the Harness sees it (a saved external result
that reads harnesses, the work of the change request a change_set is for). A Clause that moves past the
revision a Case recorded makes the citation cite.stale, as for any Case, and an example Case's citation counts for
cite.uncited-clause. A Harness of examples puts no Interface on a Pin's basis: its Cases cite no Item, and builds
names the Interfaces whose pinned code the program is built from, as for a sim.
{"op":"harness_put","context":"tally","name":"acceptance","kind":"examples","builds":["tally"],
"examples_dir":"acceptance",
"cases":[{"name":"a1","clauses":[{"contract":"lines","label":"C1"}]},
{"name":"a2","clauses":[{"contract":"lines","label":"C2"}]}]}- The texts come from exactly one place:
examples_dir, a directory relative to the project's root (the rules of asource_file), from which each Case's<name>.args,<name>.stdin,<name>.expectedand, when it is there,<name>.stderrare read; or each Case's ownexample,{"args":"","stdin":"+ a\n","expected":"1\n#exit 0\n"}("stderr"optional). The files are read when the operation runs and the model keeps the texts, never the directory: changing a file changes nothing until the Harness is put again. Withexamples_dir, every example in the directory has a Case and every Case has its three files; a missing file, a file that cannot be read and a file that is not UTF-8 text are refused with the file named. - The texts are what
chidev acceptreads from the files (see above), as text: an example whose input or output is not UTF-8 cannot be held. A text is at most 1 MiB and the texts of one Harness at most 4 MiB together (examples.too-large); an expected output past the runner's 4 MiB cap could not pass anyway. - A put of an examples Harness is refused unless it has this shape: a Case of
examplesnames nolinesand cites no Item (both refused as input;examples.cites-itemis the rule a kept Harness is held to), cites at least one Clause (examples.cites-none), has a name that is a file name of an example (not empty, no/, NUL or line feed) and that no other Case of the Harness has (examples.case-name), and has an expected text whose last line is#exit <code>(examples.expected-exit); the Harness builds at least one Interface (examples.builds-none) and has nosourceorsource_file. A Harness of tests or of a sim takes neitherexamples_dirnor anexample. - A put of an existing examples Harness says
kindexamples; a put of it with code and nokindis refused (examples.kind-content), so it does not become a Harness of tests unsaid. A put withkindtestsorsimand code makes it one, and a put withkindexamplesmakes a Harness of code one of examples: a change of kind is a new revision. - chidev runs the examples of a Harness at no moment of the checks.
measureof kindexamplesruns them and keeps the run (see "Examples");chidev accept <dir>runs the files of a directory for a person and keeps nothing.
A Harness that passes does not show it is strong. measure runs the pinned code
against changes of itself (mutants) and keeps what the Harness noticed:
{"op":"measure","kind":"mutate","context":"inventory","interface":"stock"}
chidev measure mutate --context inventory --interface stock runs it as a command (see "Commands: chidev check
and chidev measure").
It is an operation of its own and a record, never an edit or a moment of the checks: it takes as long as the project's
build and tests take, once per mutant. It runs the code the Pin accepted (pin
keeps the text beside the Pin) and the Harness sources as the model holds them
now, in a copy of the project made under .chidev/measure/<run>/, with a target
directory of its own; the project's files and build output are never touched, and
the runner removes the copy when it is done (a killed process can leave one; it is
safe to delete). The working tree's own copy of the code is not read, so a tree
that has moved on does not change what is measured. Symbolic links are not copied.
The run builds and tests the copy once as it is (the baseline; if that fails the
run is a failed measurement and no provider is asked), asks the provider for
mutants, and for each one applies the edit, builds (a build that fails is
stillborn), tests (a failure killed it, a pass lets it survive, a deadline is
a timeout) and puts the code back. The result is kept as a measurement: its
stamp (the Pin's code digest, the revisions the Pin stands on, a digest of the
settings the run read, see "[measure] in chidev.toml"), each mutant with its site, operator and
outcome, the evidence of the commands (command, executable digest, exit code,
time), and the failure if there was one. Measurements are appended; the newest per
kind and Interface is current, the older stay as history
(measure_runs, measure_items). A result is kept in one commit with nothing else.
Only a correction changes a measurement after it is kept: it replaces a text of its
evidence with a mark and leaves its place, its stamp and its counts (see "Correcting
a record").
The checks read the record and run nothing (they are builtin, at the check moment):
| rule | default | moments | finds |
|---|---|---|---|
measure.failed |
warning | check | the latest measurement of an interface's code failed (baseline, provider, protocol, timeout, budget, io) |
mutation.unmeasured |
note | check | a pinned interface whose code was never mutation-tested |
mutation.stale |
warning | check | a mutation run after which the pin's code, the interface, a harness that checks it, a contract it cites, or the settings moved |
mutation.capped |
note | check | a mutation run that ended ok having evaluated fewer mutants than the provider found sites, max_mutants having limited it; about the interface, evaluated N of M sites |
mutation.survived |
note | check | a mutant no case noticed, one per mutant, about the interface, with its key, line, column, operator and change |
A stale run is judged stale first, so a survivor of an old run is not reported for
moved code. status.measurements lists the latest run per Interface: stamp,
sites (the places the provider found), evaluated (the mutants run) and
capped, killed / survived / stillborn / timeout, outcome and state:
current, stale, retired (the Interface was retired since) or unpinned (the
Interface has no Pin, so there is no accepted code to compare the run with). A
stale run also has moved, which names each part that moved by its word, and by
the asset's name when the part is an asset ({"part": ..., "name": ...}); the words
are listed in the RustDoc of StampPart::word, and a part for settings the runner
cannot read also has a reason. The finding of mutation.stale says what moved too,
with the reason settings could not be compared. The result of measure has the same sites, evaluated and
capped.
A run that did not look at every site says so. max_mutants limits what the
provider is asked for, so it samples the sites; the run is still ok, the mutants
it evaluated each have an outcome, and the record keeps sites (what the provider
found) apart from the mutants it was given (planned, stored in the summary of the
run) and the mutants evaluated. A run is capped when it ended with fewer mutants
than sites, which is mutation.capped (a note: a Harness that kills every mutant of
a sample may still miss the sites left out; raise max_mutants to cover them). A run
that stopped before it had evaluated the mutants it was given (the budget ran out)
is not capped: it is a failed run of kind budget, as before. A run that ended
with fewer mutants than it was given and no failure is refused as a measurement.
[measure]
build = ["cargo", "build", "--quiet", "--tests"] # a failure makes the mutant stillborn
test = ["cargo", "test", "--quiet"] # all pass: survived; a failure: killed
copy_exclude = [".git", "target", ".chidev"] # relative to the root; the default
env = { CARGO_TARGET_DIR = "{run}/target" } # default; {run} is the run's directory
timeout = 30 # seconds, one build or one test of a mutant
baseline_timeout = 600 # the build and the test before any change
budget = 900 # the whole run, at most 3600
max_mutants = 40 # at most this many mutants are evaluated; more sites make the run capped
seed = 0 # moves where the even sampling of sites starts
operators = ["rel", "eq", "arith", "logic", "bool", "int", "str"] # optional; default: every class the provider makes
[measure.mutants] # language -> the provider command
rust = ["chidev", "provider", "mutants-rust"]
[measure.items] # language -> the command that finds the Items of an Interface
rust = ["chidev", "provider", "items-rust"]
[[measure.layout]] # where an Interface's code is in the tree
context = "inventory"
interface = "stock"
lang = "rust" # selects the providers (of mutants, and of items)
path = "src/stock.rs"
[[measure.layout]] # where a Harness's source is written
context = "inventory"
harness = "stock-tests"
path = "tests/stock.rs"build and test are commands, run without a shell in the copy; paths are
relative to the project root and may not contain ... Every Harness that checks the
Interface (the Harnesses of the Pin's basis) needs a layout entry, and the Harness
must be a file of its own (an in-file #[cfg(test)] module cannot be told apart
from the code). A field that is not known is refused by name. A project with no
[measure] has no runner: measure is refused, and the checks still judge what is
kept. The settings digest is of one kind of run for one subject, and covers the values
that run reads, defaults applied, and the executable of the first word of build,
test, the provider (of the Interface's lang) and, for a sim, the interpreter; and,
for a sim and for examples, the executable of the first word of the program under test
when that is a program of the host (see "The program under test"). A
mutation run reads copy_exclude, env, build, baseline_timeout, test, the
provider command, operators, timeout, budget, max_mutants, seed and the layout
of its Interface and of the tests it runs; so changing one, or upgrading the provider,
makes earlier runs stale. A setting the run does not read (the sim tables, items, the
layout of other things) does not. The host environment and PATH, the files of the copy
other than the pinned code and the Harnesses, and the programs a later word of a command
starts are not in it (sh -c "cargo test" digests sh), so a changed toolchain is in
general not seen. A run whose settings cannot be read now (no [measure], or no layout
for it) is stale, not current, and the finding says which and why. A run whose build or
interpreter in the copy, as the copy is made and before any command runs, or whose provider
when it is started, is not the file digested at the project's root fails with kind
settings_moved; a program the build writes later is the run's own. The program under
test is asked too when its first word starts from the host, and not when it is in the copy
(see "The program under test").
Placeholders. A run has two places, and each has a name of its own that means the
same wherever it is taken: {run} is the run's directory (under .chidev/measure/,
removed when the run ends), and {tree} is the root of the copy of the project the
run works in, which is the tree directory inside it. A sim makes a second, fresh
copy for the second runs of its cases (rerun-tree, in the same run directory), and
there {tree} is the root of that copy while {run} stays the same: what is under
{run} (the build's target) is shared by the first and the second runs, so state the
second runs must not inherit goes under {tree}. Only two kinds
of field take a placeholder: the value of a variable of env, and a word of the
program of a [measure.sim.<context>] or of a [measure.examples.<context>], the first
included. Any other {...} in them is left as it is.
A {run} or {tree} in any other string of [measure] (a command, a layout path,
copy_exclude, a key) is refused by name, and so is {work}, which was the name of
both places before each had one: a configuration that still has it stops every
command until it is rewritten.
The program under test. The program of a sim and of the examples is a command whose
first word is sorted by where it resolves when the run starts:
- In the copy: a word with
{tree}or{run}, a relative path (more than one component:./target/debug/app,bin/app), and a bare name that the walk ofPATHfirst finds under a relative entry (a relative entry is relative to the copy, which is the working directory). The file is the build's output or the project's own, so the pinned code and the build account for it and the settings do not hold it: a build of one's own at that path of the root does not make a run stale, and a root with no build leaves the settings readable. The copy has the file only when the build has made it; a program that is not there when the run starts it fails the run. - A program of the host: an absolute path, wherever it points (inside the project's root
too: at run time it is the root's file, not the copy's), and a bare name that the walk
first finds under an absolute entry of
PATH(python3,/usr/bin/env). The settings digest holds its executable, so a changed interpreter makes earlier runs stale, and one that is not found or cannot be read is an error of the settings. As the copy is made, the first word is resolved from the copy by the same walk; a copy in which it is another file (a relative entry ofPATHthat has the name there), or the same file with other bytes, fails the run withsettings_movedbefore anything is started. - Not found: in a sim, a bare name that
PATHdoes not find. The script starts the program, and the word may be no executable (app.jar), so it is not an error and its file is not digested. In the examples, which chidev starts, it is an error of the settings.
The words as written are in the digest in every case. build, test, the provider and the
interpreter are not sorted so: they exist before the build and are digested by their files
even inside the root, while the program under test may be what the build writes. A sim
script starts the program, not chidev, which is why the settings are all chidev holds of it.
operators limits the classes of mutant the provider is asked for (the bundled one
makes rel, eq, arith, logic, bool, int and str). Leave it out for all of
them; an empty list is refused. A mutation run reads it, so changing it makes earlier
runs stale. Leave str out to skip the mutants of string literals.
[measure.items] is read by the same loader as [measure.mutants]: a language and
the command that finds the Items of an Interface of that language (see "Provider
protocol chidev-items 2"). The language of an Interface is the lang of its
[[measure.layout]] entry, and a lang is valid when mutants or items (or both)
has a command for it. Three things follow from the table being [measure]'s:
- A project with no
[measure]table has no provider of items:interface_putand achange_setcannot leaveitemsout there (items.no-provider), and write them. Achidev.tomlwith other tables but no[measure]is the same. - A language with a command in
itemsalone is valid, and it can be measured by nothing:measurestops, naming it, with "mutantshas no command for<lang>". - The provider is started in the project's root, as the provider of mutants is, and
must answer within
timeout. No run readsitems, so changing it leaves earlier runs as they were.
The provider is a program that makes mutants and nothing else: a pure function of a source text, so a provider for another language is small. One JSON document on stdin, one on stdout, stderr for diagnostics:
{"protocol":1,"path":"src/stock.rs","source":"...","max":40,"seed":0,"operators":["rel","str"]}
{"protocol":1,"sites":118,"mutants":[{"key":"reserve/rel/2/9f3a","line":42,"col":13,
"operator":"rel","span":[1834,1835],"from":"<","to":"<="}]}A mutant is an edit: span is a byte range of source, from the text it holds,
to what replaces it. chidev checks source[span] == from for every edit before
any is applied; a disagreement, a key given twice, a protocol other than 1, or a
field the protocol does not have is a failed run (protocol). key is the
provider's stable id of the mutant (the bundled one writes
<function>/<operator>/<n>/<digest of the line>, which an edit elsewhere in the
file does not move); chidev treats it as opaque text. The reply is capped at 4 MiB.
The bundled Rust provider is chidev provider mutants-rust: it mutates operators
(rel, eq, arith, logic), boolean literals (bool), integer literals
(int) and string literals (str) inside function bodies, leaves #[cfg(test)]
and #[test] code alone, and gives the same reply for the same request.
The request field operators (optional, a list of class names; absent means all)
selects the classes. It is optional, so the protocol stays version 1 and a request
without it parses as before; a provider that does not know it may ignore it, and
the bundled one refuses a name that is not one of its classes. [measure] operators is what chidev sends.
A string literal ("...", r"...", r#"..."#) gives two mutants, both of class
str and told apart in the key (str-empty, str-marker): the text emptied, and
* appended inside the quotes. Every {...} placeholder is kept in the emptied
text ("{name}: {n:>w$} left" becomes "{name}{n:>w$}"), so a format string still
builds. Not mutated: byte and C strings, strings in attributes, strings in the
arguments of include_str!, include_bytes!, include!, env!, option_env!,
concat!, stringify!, compile_error!, cfg!, asm! and macro_rules!. The
point of the class is the message no test reads (a wrong number or an empty text
in an error): such a mutant survives.
The provider of items is a program that finds the declarations of an Interface in its code that a caller can name, and nothing else: a pure function of a source text. Like the provider of mutants it gets one JSON document on stdin, answers one on stdout, and writes its diagnostics to stderr:
{"protocol":2,"source":"pub fn price(\n total: u64,\n) -> u64;\npub fn tax() -> u64;\nfn round(n: u64) -> u64;\n"}
{"protocol":2,"items":[{"key":"price","lines":[1,3]},{"key":"tax","lines":[4,4]}],"left_out":1}A caller of a source is code outside that source, built as a caller builds it
(not as part of the source's own tests). An Item is a declaration such a caller can
name. The provider answers the Items, and in left_out how many declarations it
found that are not (above, round, which nothing outside can name). What a caller
can name is the provider's to say, by the rules of its language. left_out is
required and is written even when it is 0, so that leaving declarations out is never
silent: the result of the put that used the provider shows it.
An Item is a key (the name the Item is matched by when the code is put again: not
empty, one line, opaque to chidev) and the first and last of its lines, from 1,
that point at the declaration alone, from its first token to the line that ends the
signature. There is no kind. chidev reads chidev-items 2 only, and checks the
reply against the code before it puts anything: a protocol other than 2, a reply
without left_out (what chidev-items 1 answered), a field the protocol does not
have, an empty key, and lines that are not lines of the code are items.protocol;
a command that cannot be started, exits with a failure, or does not answer within
timeout is items.failed. Whether the declarations are apart is not checked: a
trait and its method on one line have the same lines, which is a right answer, and
the Items that share a line are all revised when that line changes. The same key
twice is refused by the Interface (interface.key-taken). The reply is capped at
4 MiB.
The bundled Rust provider is chidev provider items-rust (rust = ["chidev", "provider", "items-rust"] in [measure.items]). It finds a fn, struct, enum,
union, trait, type, const, static and macro_rules! item, and the fn,
const and type inside an impl or a trait; an inline mod is entered, and
nothing in a function body is found. A declaration with no body
(pub fn price(total: u64) -> u64;, the form the examples above use) is found as well.
The lines of an Item run from its first token, after the doc comments and attributes, to
the line that has the ; or the { of its body; a struct, enum or union runs on
to its closing }, so a field or variant added, removed or retyped revises the Item.
Of what it finds, it answers the declarations a caller can name. "Outside" is outside
the module of the file and outside the inline mods in it; a declaration with no body
is judged as any other:
| declaration | Item? |
|---|---|
no visibility, pub(self) |
no |
pub, pub(crate) |
yes |
pub(super) at the top level of the file |
yes |
pub(super) inside an inline mod m |
no |
pub(in path) |
no if path starts with self; with super, yes when it climbs out of the file's module (pub(in super) is pub(super)); otherwise yes (an approximation: the provider does not know the module path) |
inside an inline mod m |
only when its own visibility and that of every enclosing inline mod reach outside (a member of an impl and a #[macro_export] macro excepted: see their rows) |
a member of an inherent impl T |
when its own visibility reaches outside, and T is not a declaration of this file that does not reach outside, whatever mod the impl is written in |
a member of a trait |
when the trait is an Item |
a member of impl Trait for T |
when each of Trait and T that is declared in this file reaches outside (a name from another file counts as reaching), whatever mod the impl is written in |
under #[cfg(test)], inside such a mod, a #[test] fn (or another attribute named test, #[tokio::test]) |
no |
Two more things follow from "a caller builds it": #[cfg(all(test, ..))], and
#[cfg(any(..))] of which every branch is test, count as #[cfg(test)]; and a
macro_rules! has no visibility, so it is an Item only with #[macro_export], which
exports it whatever inline mod it is in (not from test code). A type or a trait is looked up in the
module the impl is in, by the path as written (self::, super:: and the names of
inline mods are followed); a path from crate or one the file does not declare
reaches. Every declaration that is not an Item is counted in left_out, the contents
of a #[cfg(test)] mod included.
A key is a name and a path, not the text of the declaration:
| where | key |
|---|---|
free, or in mod m |
price, m::price |
a macro_rules! |
twice! |
in an inherent impl |
Stock<T>::new (the arguments of the type are in it: impl Foo<u8> and impl Foo<u16> differ) |
in a trait Clock |
Clock::now |
in impl Display for Money |
<Money as Display>::fmt |
A type that is not a path is put in angle brackets (<[u8]>::len), and inside a mod the
key is led by the path of the mod (m::Stock::new, m::<Money as Display>::fmt).
A key is an identity, not a display, so it is not written as rustdoc writes it.
It is printed from the syntax of the code by two rules with no exception: a list has
its , between the elements only (a trailing ,, and so a formatter, a line break
or white space, moves no key; a tuple of one element, (u8,), keeps its ,, as
(u8) is another type), and a space stands between two words (identifiers, keywords,
lifetimes, literals) and nowhere else. A path is written as it is in the code, and a
lifetime is kept. So <Box<dyn Error> as From<&str>>::from is the key
<Box<dyn Error>as From<&str>>::from, fn() -> u8 is fn()->u8, and &'a mut T
stays &'a mut T. Do not type a key from memory or from rustdoc: copy it from
status or the model, where chidev shows it. A key made some other way (by hand, or
the signature of a store from before keys) is brought to this one by items_rekey.
What to know about these keys:
- Two declarations that the key cannot tell apart get one key and are refused
(
interface.key-taken): the same name in two namespaces (struct Fooandfn Foo), and declarations told apart only by#[cfg](#[cfg(unix)] fn f()and#[cfg(windows)] fn f()). Put such an Interface with itsitemswritten. - A key follows the name: a declaration that moves to another
modor is renamed has a new key, so it is a new Item unless the put says otherwise (renames). The names of the generics of animplare in the key as well:impl<T> Foo<T>changed toimpl<U> Foo<U>is a new key, and arenamescarries the id over. - Two declarations on one line (
pub trait Clock { fn now(&self) -> u64; }) havelinesthat overlap, which is a right answer. Both are revised when that line changes, anditems_rekeyleaves them inunmatched. Put them on lines of their own (a formatter does) or write theitems.
Tests written next to the code tend to check what the code does. A sim Harness checks the code against the Contracts only: a script, written without reading the program, drives the program as a black box with a schedule it makes from a seed, and says which Clause did not hold. chidev runs it; it is the same for a Python, TypeScript or Rust program, because chidev only starts processes and reads their output.
A Harness has a kind: tests (the default; what every Harness has been) or sim.
{"op":"harness_put","context":"ledger","name":"bank-sim","kind":"sim","builds":["bank"],"source":"...","cases":[
{"name":"sweep","lines":[1,1],"clauses":[{"contract":"account","label":"C1"},{"contract":"account","label":"C2"},{"contract":"audit","label":"C1"}]},
{"name":"regress-overdraw","lines":[2,2],"clauses":[{"contract":"account","label":"C2"}]}]}
A sim Harness has exactly one Case named sweep and any number named
regress-<name> (a-z, 0-9, -). The sweep stands for the seeded runs and
cites every Clause the script checks; each regress-<name> stands for a fixed
schedule the script keeps. A sim Case cites Clauses only, of any number of the
Contracts of this Context (the example takes on account and audit, which both
have a C1); it cites no Item, and a Case that does is refused. Without kind, a
new Harness is of tests and an existing one keeps its kind; changing the kind is a
new revision. A Context has at most one sim Harness.
builds names the Interfaces of the Context whose pinned code the program is built
from. It is not a citation: it asks no Item of the Interface, a name the Context has
no Interface of is refused (also in a change_set, where the Interface may be one
the same change set makes), and an Interface cannot be retired while a sim Harness
(or a Harness of examples) builds it. A sim Harness and a Harness of examples have
builds (a Harness of tests with it is refused),
a sim Harness builds at least one Interface (a put without one is refused; the rule
is found with {"op":"explain","rule":"sim.builds-none"}), and a
put states the whole list, as it states the code. Because a sim cites no Item, it is
on no Pin's basis: putting or changing the sim never makes the Pin of an Interface
stale. The Pin of an Interface still needs a tests Harness whose Case cites one of
its Items (code nothing checked is not accepted). The other way round, the
simulation stands on the Pin: the Harness (its script, Cases and builds), the
Contracts it cites a Clause of, and each built Interface with the digest of its
pinned code are its basis, so a change to any of them makes the latest simulation
stale (sim.stale).
{"op":"measure","kind":"simulate","context":"ledger","harness":"bank-sim"}
(chidev measure simulate --context ledger --harness bank-sim runs it as a command.)
It copies the project under .chidev/measure/<run>/ as a mutation run does,
writes the code the Pins of the Interfaces the Harness builds accepted and
the script (the Harness's source as the model holds it) over the copy, and runs
build. A program that does not build is a failed run (no_build) and the script
is not run. Then it starts the script once per case, seed-0 to seed-<seeds-1>
and every regress case, and then runs seed-0 and up to sample of the cases that
had findings once more, in a second, fresh copy.
<interpreter...> <script> <case> --out <dir> -- <program argv...>
<case> is seed-<n> or regress-<name>; the working directory is the copy; the
program argv is program of the configuration with {tree} made the root of the
copy (and {run} the run's directory; see "Placeholders"). The script drives the
program (standard input and output, arguments), writes <dir>/schedule (the
choices it made from the case) and <dir>/trace (what the program did under them),
and prints, one per line:
FINDING <case> step <k> <contract>:<label>: <text>
UNREACHED <contract>:<label>: <text>
UNDECIDED <text>
CANNOT_START <text>
sim: findings=<n>
- A Clause is named
<contract>:<label>: the Contract's name as chidev knows it (a name has onlya-z,0-9,-and_, so it holds no white space and no:; the name is read up to the first white space, the label is after the:) and the Clause's label (C<n>) in it. Labels are unique only within a Contract, so a name without the Contract (C1, aschidev-sim 1wrote it) is refused. The script declares nothing else; the version is the protocol this document describes. chidev resolves each name to the Clause's id, among the Clauses thesweepCase cites, when it records the run: the record holds Clause ids, not names or labels, and the rules read ids, so a label renumbered later does not move an old finding. A new label moves no revision (seecontract_put), sosim.staledoes not warn ahead: a script that still prints the old label is a protocol failure on its next run, and the label to print is the onestatusandviewshow. FINDING: after step k (from 1) of this case a promise of the Clause did not hold. The case must be the one the script was started for.UNREACHED: this case never reached the situation the Clause needs. A Clause is unreached for the run when every seed that ended normally said so.UNDECIDED: the script could not decide something; counted, never a failure.CANNOT_START: the script could not start the program at all (the program is not there, it cannot be run), so it drove nothing and checked nothing. It is the script that sees this, because it is the one that starts the program; chidev only starts the script. The report is then this one line andsim: findings=0: aFINDING,UNREACHEDorUNDECIDEDline beside it is a protocol failure, for it would tell of a run that did not happen. The case is recorded ascannot_start:<text>; it has no findings and says nothing of any Clause (it does not count towards unreached, and it is not run again). A script that prints aFINDINGfor each Clause because the program did not start makes every Clause look broken by a program that was never run; printCANNOT_STARTinstead. Exit 0 or 1, as for any report that is read.sim: findings=<n>is the last line andnis the number ofFINDINGlines.
When every case that was run said CANNOT_START, the run is a failed run of kind
no_start (the cases are kept, there are no second runs), judged by sim.error
alone: no sim.finding and no sim.unreached come of it. When only some cases did,
the run is ok, the others are judged as usual, and sim.error names the cases
that could not start.
Exit 0 or 1 (1 when there are findings) means the report is read. A name of a
Contract the sweep Case cites no Clause of, or of a Clause of a Contract it cites
that the sweep does not, a name that is not <contract>:<label>, a line of none of
these forms, a missing or repeated
summary, a summary that disagrees with the lines, a normal case that wrote no
schedule and trace, or a report over 1 MiB is a protocol failure of the whole
run; the cases run so far are kept. Another exit code, or a case past timeout,
is recorded for that case and the run goes on. chidev takes the SHA-256 of
schedule and trace itself, and it is chidev, not the script, that runs the
sampled cases again: a different schedule means the script draws something besides
its seed (a fault of the script), the same schedule and a different trace means
the program draws its own order (a fault of the program).
[measure]
build = ["python3", "-m", "py_compile", "bank.py"] # a failure: the run fails with no_build
# `test` and a layout `lang` are only for mutation; a project that is only simulated needs neither
[measure.sim.ledger]
interpreter = ["python3"] # argv prefix; the script path follows it
program = ["python3", "{tree}/bank.py"] # the program under test; {tree} = root of the copy
# (python3 is a program of the host: it is in the settings digest)
seeds = 50 # seed-0 .. seed-49 (default 50, at most 10000)
sample = 6 # cases with findings run again besides seed-0 (default 6, at most 1000)
timeout = 60 # seconds, one case (default 60)
budget = 600 # seconds, the whole run, second runs included (default 600, at most 3600)
[[measure.layout]]
context = "ledger"
interface = "bank" # the code of each built Interface is written here
path = "bank.py"
[[measure.layout]]
context = "ledger"
harness = "bank-sim" # the script
path = "sim.py"Every Interface a sim Harness builds needs a layout entry and a Pin (pin keeps
the code text); the script needs one. build runs with baseline_timeout. The
settings digest of a simulation covers copy_exclude, env, build,
baseline_timeout, the Context's [measure.sim.<context>], the layout of the script
and of the Interfaces it builds, and the executables of build, the interpreter and,
when it is a program of the host, the first word of program (see "The program under
test": the script starts the program, so a program of the host is digested there and one
in the copy is the build's); the mutation settings are not in it.
A put of a sim Harness is refused unless it has the shape above (the sim.* rules
that are invariants; {"op":"rules"} lists them and {"op":"explain"} gives each).
To replace the sim Harness of a Context, put the new one and
retire the old one in the same change set. The shape is asked of the Harness a put
names, so a store that holds a sim Harness from before the rule is read as it is, and
only a put of that Harness is refused; sim.shape-kept reports it.
| rule | default | moments | finds |
|---|---|---|---|
sim.shape-kept |
error | check | a sim harness kept in a shape a put of it is refused for (one the store held before the rule, or one beside another sim harness of its context), about the case, the clause or the context; it refuses no change, and one label in two contracts of the sweep is fine |
sim.uncited-clause |
warning | pin, resolve, check | a clause that no sim case cites, of a contract a sim harness takes on (cites at least one clause of); a contract the sim cites nothing of is left to the tests |
sim.unrun |
warning | check | a sim harness that was never simulated |
sim.stale |
warning | check | a simulation after which the script, a case or the builds of the harness, a contract it cites a clause of, a built interface, the code pinned for it, or the settings moved |
sim.finding |
error | check | a clause the program broke in a simulated case, one per clause, with the first case and step |
sim.unreached |
error | check | a clause whose situation no simulated case reached, one per clause |
sim.nondeterministic |
error, mandatory | check | a simulated case that did not give the same schedule and trace twice, about the case; no policy lowers it |
sim.error |
warning | check | a simulation whose run failed (no_build, no_start, protocol, budget, ...), or whose script failed for a case or could not start the program in it (CANNOT_START) |
A stale run is judged stale first (only a second run that did not reproduce is
still reported). A failed run is judged by sim.error alone. status.measurements
lists the latest simulation per sim Harness with harness, sim counts (cases,
findings, unreached, abnormal, reruns, differing), outcome and state
(current, stale, retired or unpinned, as for a mutation run, which also tells
what moved; the finding of sim.stale says what moved too).
The script is written blind, from the Contracts and the Interfaces only. It never reads the program's code, its tests or its history, so that it checks the promises and not the implementation. Therefore:
- cite in the
sweepCase every Clause the script checks, and print aFINDINGorUNREACHEDonly with the name<contract>:<label>of a Clause it cites (the Contract is part of the name, because two Contracts may both have aC1); a script may check the Clauses of several Contracts of the Context; - when starting the program fails, print
CANNOT_START <text>andsim: findings=0and nothing else; never aFINDINGper Clause for a program that did not run; - check each Clause literally, with an expected value computed from the Clause's sentence by the script's own model, never from the program's answers;
- use only the seed for randomness (
random.Random(seed), not the clock, the process id, the environment, unseeded random numbers, or the iteration order of an unordered collection), so that the same case gives the samescheduleevery time; - write everything the program did into
tracein a stable order, and make a schedule that reaches the situations the Clauses need (printUNREACHEDwhen a case does not), keeping aregress-<name>Case for each situation that is hard to reach by chance or was once a finding.
The examples a Harness of examples holds (see "Acceptance examples as a Harness") are run by measure, and the
run is kept like a simulation is, so that the Clauses the examples cite are judged by a run and a change of an
expected output makes the last run stale:
{"op":"measure","kind":"examples","context":"tally","harness":"acceptance"}
(chidev measure examples --context tally --harness acceptance runs it as a command.)
It copies the project under .chidev/measure/<run>/, writes the code the Pins of the Interfaces the Harness builds
accepted over the copy, and runs build (a program that does not build is a failed run, no_build, and no example is
run). It then starts the program once per example, in the order of the Cases, with the example's arguments after the
words of program, its input on standard input, the copy as the working directory and the env of the table. The
answer is judged as chidev accept judges it (see "Acceptance examples: chidev accept", which gives the
reasons): a program that gave no
answer (it cannot be started, it does not end within timeout, it writes over 4 MiB, a signal ends it) fails that
example with one reason, and the run goes on. The run fails, keeping the examples it got through, when build fails or
budget is used up. The Harness's own Cases are the examples: the working tree's example files are not read.
[measure]
build = ["sh", "-c", "cargo build --quiet"]
[measure.examples.tally]
program = ["./run.sh", "--plain"] # the words that start the program; an example's arguments follow
timeout = 10 # seconds, one example (default 10)
budget = 600 # seconds, the whole run (default 600, at most 3600)
[[measure.layout]]
context = "tally"
interface = "tally" # the code of each built Interface is written here
path = "src/tally.rs"program is the command that starts the program under test. Its first word may be what the build makes
(./target/debug/tally, {tree}/bin/tally), a launcher the project keeps (./run.sh), or a program of the host
(python3, with {tree}/prog.py as the next word); see "The program under test" for which of them the settings hold the
file of. Every word, the first included, may hold {tree} and {run} (see "Placeholders"). The program is found in the
copy as the build left it, and one that is not there when the run starts it fails the run. Every Interface the Harness
builds needs a layout entry and a Pin; the Harness needs no layout entry, for it has no file and the run reads and writes
nothing at a place of it. The settings digest of a run of examples covers copy_exclude, env, build,
baseline_timeout, the Context's [measure.examples.<context>], the layout of the Interfaces it builds, and the
executables of build and, when it is a program of the host, of the first word of program; the other settings (the
layout of any Harness among them) are not in it.
A run stands on the revision of the Harness (its examples are in it, so a changed text, a Case added, dropped or
renamed, or another builds moves it), the digest of the code pinned for each Interface the Harness builds, and the
settings.
It does not stand on a Contract or an Interface revision: what a program answers does not depend on them, and a Clause
that moves is the Case's own business (cite.stale). The record keeps each example by its Case's id and what became of
it, and keeps of a failed example only what the program gave (the expected side is the Case's, in the model, and is not
copied): a Case that is named otherwise afterwards is still the example the record is of, and the checks show the name
the Case has now. status.measurements lists the latest run per Harness with harness, examples counts (passed,
failed), outcome and state (current, stale, retired or unpinned, with moved as for any run; the word
for the Harness itself is examples, and a built Interface is built-code or builds). The result of measure lists
each example with its name, whether it passed and its reasons. A correction can replace the text of a reason (the
field example_reason, see "Correcting a record").
The checks read the record and run nothing (they are builtin):
| rule | default | moments | finds |
|---|---|---|---|
examples.unrun |
warning | check | a harness of examples that was never run |
examples.stale |
warning | pin, resolve, check | a run of examples after which an example of the harness, an interface it builds, the code pinned for it, or the settings moved; a run that failed is told by examples.error |
examples.error |
warning | pin, resolve, check | a run of examples that failed (no_build, budget, io, settings_moved, ...): no example was judged |
examples.failed |
error | pin, resolve, check | an example that failed in the latest run, one per clause its case cites, with the example's name and why; it refuses no change, and a stale run or a run that failed is not judged for it |
None of these is mandatory, so none refuses anything, and a failing example is reported at pin and resolve and
does not stop either. A builtin check's mandatory is fixed by the check and a policy cannot raise it, so a project
that wants a confirmation refused for a failing example registers an external gate of its own (see "External checks").
A run that is stale is not judged for its failing examples, and a run that failed is not either: it is reported by
examples.error alone, whether or not the model has moved since.
The text of a placed asset is held twice: by the model (the code a Pin accepted, the source of a Harness) and by the
file the project's [[measure.layout]] places it in (see "[measure] in chidev.toml"). The model never reads that
file, and pin.stale compares revisions of the model, so a pinned file that is changed on disk moves nothing the model
records: check stays silent until the code is put and pinned again. The tree.* checks look at the files and say when
a copy has gone from the text the model holds:
| rule | default | moments | finds |
|---|---|---|---|
tree.code-moved |
warning | check | an interface whose file in the working tree holds other code than the code its pin accepted |
tree.harness-moved |
warning | check | a harness (about its first case) whose file in the working tree holds other source than the harness does |
tree.unread |
warning | check | an interface or a harness (about its first case) whose file in the working tree is missing or cannot be read as text |
- Only at
check.check,statusandcheck_runread the files; an edit, apinand acr_resolvedo not. A Pin accepts code, not a file, and reading the files of the whole project at every confirmation would add their cost to each one (ADR 0010). - Files only, no program. Each file a layout names is read once per check, whatever the number of assets that name
it, and compared as text by its digest, the digest a Pin makes of the same text: the bytes are not normalised, so a
changed line ending or a white space is a change. A file that is not there, is a directory or is not UTF-8 is
tree.unreadand says why. Nothing is started:statusandcheckstill start no program. - What is compared. An Interface's file is compared with the code its Pin accepted; a Harness's file with the source
the model holds. An Interface that has a file and no Pin is compared with nothing (
pin.unpinnedsays so). An Interface or a Harness that no layout entry places, and every asset of a project with no[measure]table, has no copy to look at and is found by notree.*check. - File by file. A finding says that the file differs, not which Item changed; two Interfaces laid out in one file both move with it.
status.treelists what the checks do not:unplaced(theinterfacesandharnessesno layout places),unpinned(the Interfaces with a file and no Pin) andmoved(each Interface whose file moved, withdeclared, which says whether the Items of the copy are looked at:"off"). Each entry names itscontextand the asset (name, orinterfaceinmoved). It isnullwhen everytree.*check is off, for then nothing was looked at. To turn the checks off, set the level of the family in the policy:[checks."tree."]withlevel = "off".
A change request says what is asked, why (situation, in the words of whoever
raised it, and the issuer: both are kept and status shows them), which
targets it asks to have revised or retired (targets) and which to keep as they
are (keep). It may carry the assumptions made while raising it, and it is stopped
by the Cruxes raised in it, choices a decision maker has to make (see "Crux, Policy
and Assumption"); it holds none itself. A request is closed by a
decision a person or a role takes and records (cr_resolve), never by what
the definitions did: the targets are the content of the request, not a condition
of closing it. The targets are the assets, as a tree, and each has a revision:
Project
└ Context
├ Contract ─ Clause
├ Interface ─ Item
└ Harness ─ Case
A reference names a target: {"project":true}, {"context":"pricing"},
{"contract":{"context":"pricing","contract":"discount"}},
{"interface":{"context":..,"interface":..}}, {"harness":{"context":..,"harness":..}},
{"item":{"context":..,"interface":..,"key":..}},
{"clause":{"context":..,"contract":..,"label":..}} and
{"case":{"context":..,"harness":..,"name":..}}. Every target has to exist when
the request opens, and a target is in one list, once (cr.target-twice,
values.target). cr_open refuses a field it
does not know (change and remove of earlier versions, and crux, among them).
targets: what the request asks to have revised or retired. A request that adds something names what holds it, as adding is a move of the container: a Clause, its Contract; a Contract, Interface or Harness, its Context; a Context, the Project. A Context's revision moves when one of its assets is added or removed, or itsusesor basis change, not when an asset's content does; the Project's moves when a Context is added or removed. A parent (a request other requests name as theirparent) may have no targets: it keeps the request in the requester's words and the split, and is closed when its parts are.keep: the targets that must stay as they are; the revision recorded atcr_openis compared, whoever moved the target. A keep that an unresolved request's target breaks for sure is told when the two meet, not after the work:cr.keep-targeted, about the request that keeps, in the answer of the edit that made them meet (cr_open,cr_crux_decide,cr_reopen) and incheck. It names the request that targets, the target and the target kept. Breaking for sure is the same target, or a Clause, Item or Case of a Contract, Interface or Harness that is kept (its revision moves with any of its parts). A kept Context is not told for a target inside it, nor a kept Item for the Interface that holds it;cr.keep-movedtells those after the work. To fix it, resolve the wrong request as{"superseded":"<new title>"}and open the new one with the rightkeep(the Item, not the whole Interface, when only its signature must stay).- Each option of a Crux raised in the request takes the same
targets/keeplists (andwording, the exact new text of the definitions, andconsequence, free text saying what else choosing it affects). An option may name targets outside the request. When a Crux is decided, the lists of the option chosen become the request's decided expectations, each with the revision it has at that moment, except thetargetsoutside the request's frame: they are not added to it, and the decision says where they go (see "Crux, Policy and Assumption"). Deciding with the decision maker's own wording, leaving it unspecified, or withdrawing the question names nothing more. - Neither list gates the closing of the request.
statusshows, for each of itstargets, what the work did to it, read from the footprints of the request's change sets and of its parts (see "Attribution and frame"):revised,retiredornot touched yet; and for eachkeep, whether itmoved. intent(change_behaviororkeep_behavior) is a record. No rule of resolving reads it; the notecr.intent-mismatchpoints out a keep-behavior request that names a Clause as a target, and a change-behavior request that names no target (a parent is left out).
open ──cr_resolve──▶ resolved ──cr_reopen──▶ open ──cr_resolve──▶ resolved ...
cr_resolve is a decision, recorded with how the request ended, why, who says
so, and when (from the clock):
{"op":"cr_resolve","title":"Refuse big discounts","resolution":"done",
"reason":"C1 now refuses above 50%","by":{"person":"alice"}}
{"op":"cr_resolve","title":"Name the error","resolution":"wont_do",
"reason":"callers do not tell the errors apart","by":{"role":"orchestrator"}}
{"op":"cr_resolve","title":"Cap discounts","resolution":{"duplicate":"Refuse big discounts"},
"reason":"the same ask","by":{"person":"alice"}}resolution is "done", "wont_do", "invalid", {"duplicate":"<title>"} or
{"superseded":"<title>"}; the title is another change request, which has to
exist. The reason and by ({"person":..} or {"role":..}) are required.
Only explicit states refuse it, in this order:
- the request is resolved already;
- a Crux raised in it has no decision, whatever the resolution (
done,wont_do,invalid, a duplicate or a superseded one:cr.crux-unanswered). The Cruxes raised in a request are decided or withdrawn before it ends; - a
duplicateor asupersededpoints at itself or at a request that does not exist; - a request whose
parentit is is not resolved (whatever way it would end).
10BC8
The revisions of the targets and the work recorded for the request are not consulted, so a request whose targets were never touched can be resolved as done. The checks then say where the record and the work disagree (below).
cr_reopen opens a resolved request again:
{"op":"cr_reopen","title":"Refuse big discounts","reason":"a caller still breaks",
"by":{"person":"bob"}}The resolution it had stays: a request keeps a history, a log of
resolved and reopened steps (each with the reason, who and when) that is only
appended to, and status shows it as history beside the current resolution.
A corrected step (see "Correcting a record") can stand anywhere in it and changes
no state: the resolutions and reopenings alternate among themselves.
A reopened request takes work (change_set for it), Cruxes and assumptions
again, and can be resolved anew. Reopening does not touch its parts or its
parent: a parent resolved while this request was closed stays resolved.
{"op":"cr_open","title":"Refuse big discounts","intent":"change_behavior",
"situation":"discounts above 50% should never be given","issuer":"sales",
"targets":[{"clause":{"context":"pricing","contract":"discount","label":"C1"}}]}{"op":"cr_open","title":"Add rules and drop C2","intent":"change_behavior",
"targets":[{"context":"pricing"},{"clause":{"context":"pricing","contract":"discount","label":"C2"}}],
"keep":[{"clause":{"context":"pricing","contract":"discount","label":"C1"}}]}A parent that only splits the ask, and a part with the targets (the shape of removing a feature):
{"op":"cr_open","title":"Remove release","intent":"change_behavior","situation":"release is gone"}
{"op":"cr_open","title":"Drop release","intent":"change_behavior","parent":"Remove release",
"targets":[{"clause":{"context":"core","contract":"stock","label":"C7"}}]}cr_open answers with what it recorded, so a wrong reference is seen at once:
{"kind":"keep","target":{"clause":{"context":"pricing","contract":"discount","label":"C1"}},"recorded":1}
(kind is target or keep). status shows each request with its situation,
issuer, state, resolution and history, the ids of the Cruxes raised in it
(cruxes), its targets with what the work did to each, its keep with whether
each moved, its work and its out_of_frame (the moves of its work outside its
frame, by the rule of "Attribution and frame": a Case that cites what the frame
holds is inside). Each entry of work has the store_revision of the change set,
moved (the targets whose revision it moved) and renamed (the Clauses and Items
it gave a new name, each {target, from, to}, target described with the name it
has now). It lists every Crux under cruxes (the
record view gives, with the request it was raised in by its title as raised_in),
each Crux that has no decision under awaiting (its id, raised_in, the question,
kind, options with what each names and its consequence, the recommendation with its
confidence, and the importance), and the unresolved requests one of whose keep
moved under keep_moved. The checks:
| rule | default | moments | finds |
|---|---|---|---|
cr.awaiting-decision |
warning | resolve, check | a crux no decision maker has answered, whether or not its change request is resolved |
cr.keep-moved |
warning | edit, resolve, check | an unresolved change request, or the one being resolved, that keeps a target as it is, and the target moved |
cr.keep-targeted |
warning | edit, check | an open change request that keeps a target which an open change request (itself included) names to revise or retire: the same target, or a clause, item or case of the contract, interface or harness it keeps |
cr.out-of-frame |
warning | edit, resolve, check | an unresolved change request, or the one being resolved, whose change sets moved something it did not name (a case that cites what it names is inside) |
cr.resolved-without-work |
warning | resolve, check | a change request resolved as done for which no change set, for it or its parts, moved anything |
cr.targets-untouched |
note | resolve, check | a change request resolved as done, with work, one of whose targets no work of it or of its parts touched |
cr.intent-mismatch |
note | resolve, check | an unresolved change request whose intent and whose targets disagree |
crux.policy-line-gone |
note | edit, check | a decision rests on a policy line that is gone or whose text changed since |
cr.unapplied of earlier versions is gone: it judged the closing of a request by
the revisions of its targets, which a resolution no longer does; status shows
the same thing (not touched yet) per target, without a finding on every request
that has just been opened. A database written before targets and resolutions (or
before the Crux, Policy and Assumptions below, or before the work of a request was
recorded) does not load: there is no migration for it. A store of layout 1, which
held a Crux inside its change request, is migrated when it is opened (see "The
store's layout").
The change a request asks for is made as a change_set for it (see "Change
sets"): a change that touches several assets and holds only as a whole (a Clause
removed, the Cases that cited it gone from their Harness, an Item removed with it)
is one set, not a series of puts that has to be ordered around the citations.
A change_set with for records its footprint for the request (what it moved, in
the same commit). That ties the work to the request that asked for it. A set that
moves nothing (the same puts applied again) is applied and answers ok, but it is
no work and records no footprint:
-
What the work did to a
targetis read from the footprints recorded for the request or for its parts, children and their children (the requests that name it as theirparent, and theirs). A target isrevisedwhen a footprint moved it, or something inside it, and it is still in the Project;retiredwhen one moved it and it is gone;not touched yetwhen none did. A move in a setforanother request, or in a set, a put or a retire withoutfor, shows in no request's targets. This is information forstatusand the checks; it decides nothing about closing. Akeepis a protection against anyone, so it is judged by the revision alone. -
A parent only aggregates. It need not name any target: the work of its parts is its work, and it is closed when they are resolved. A parent resolved as done with no work on it or on any part is
cr.resolved-without-work. -
The targets a request names to revise or retire are its frame, as it was opened; deciding a Crux never widens it (the options'
targetsoutside it are not added: see "Crux, Policy and Assumption"). Akeepis not part of it. Naming a container frames everything in it: a Contract frames its Clauses, a Context its assets and what is in them, the Project everything. The work of a request is to stay in its frame, and the frame is the request's own: a parent's targets neither widen nor bound its parts. A footprint that moved a target outside the frame of the request it was applied for is reported ascr.out-of-frame(a warning) for that request, by the put that did it, bycr_resolvewhen the request is resolved, and bycheckfor as long as it lasts, andstatuslists it asout_of_frameof that request next to itswork(each footprint'sstore_revisionand the targets it moved). A put and a resolve judge the whole model, as every check of their moment does, so their findings also name the other open requests that are out of frame. A request with no targets has all its work out of frame. A container that moved only because something inside it moved within the frame (the Contract that took a Clause, the Context that took an asset) is not outside. A resolved request is a record: it takes no more work (until it is reopened) andcheckandstatusdo not report it for its frame; the resolution itself is told (the checks of theresolvemoment judge the request being resolved although it is resolved by then:cr.out-of-frame,cr.keep-movedandcr.intent-mismatch). -
The frame includes the Cases that cite what it frames. A Case that a change set moved (made, changed or retired) is inside the frame when it cites a Clause or an Item that is, or is inside, a target the frame names: revising a Clause makes the Cases that cite it stale, and bringing them up to it is the same work. One citation is enough: a Case that cites a framed Clause is inside though its other citations are not. The citations are those recorded in the footprint when the work was applied (a Case the set changed counts what it cited before the set and after it, so a set that retires a framed Clause and moves its Cases off it still has them inside; one that was made counts after, one that was retired counts before), so later work cannot move earlier work in or out of the frame. A footprint recorded before citations were kept is judged by containment alone. The Harness that moved only because such a Case moved is covered as any container is. For a move outside the frame: name the target when the request is opened, apply the change set
forthe request it belongs to, or take the change back.The same frame decides what a Harness put
forthe request brings up. Each citation of each Case of the put is recorded at the revision its target has when the Case is new, the Case did not cite that target before, the target is inside the frame, or the Case is (the Case, its Harness, its Context or the Project is named). Any other citation stays at the revision the Case had recorded for it, so a stale Case of another part stays stale and does not move, and the put does not claim it current for a change nobody looked at. This is decided per citation: a Case that cites a Clause of this request and a stale Clause of another is brought up on the first and stays stale on the second. A Case whose code the put changes still moves, as it does in any put. A set withoutforbrings every citation up. So parts that share a Harness can be designed side by side, each part's Harness put moving only the Cases its frame holds and the Cases whose code it changes. The edge: a set that revises a Clause outside its frame and puts a Harness whose unchanged Case cites it leaves that Case stale right after the set, and the revision itself iscr.out-of-frame. The decision is in ADR 0006. -
A new Clause or Item is a move of its own: a frame that names only the other Clauses of the Contract has it outside, so name the Contract when the request adds to it.
-
A new name is a move of its own. A Clause given another label, or an Item another key, keeps its id and its revision (and its Contract's or Interface's), and the footprint of the set that did it holds the Clause or Item with the name it had and the name it has, though no revision moved. It is work: a request whose only work is such a rename is resolved with work, and a Clause or Item it names as a target reads
revised. It is outside a frame that does not hold the Clause or Item (cr.out-of-frame, naming it alone). It moves no container, so the Contract or Interface holding it is not in the footprint for it. It breaks nokeep, which is judged by revision: a name is not part of what a Clause promises (ADR 0004), and no citation goes stale. The decision is in ADR 0005.
Three things say how a question that came up while raising a request is settled, by how it can be answered:
| when | who | recorded | |
|---|---|---|---|
| Assumption | there is no choice: the repository and general knowledge give the answer | the role at work | the statement, the reason, the evidence |
| Crux settled by the Policy | a choice must be made, and a line of the Policy derives it | a decision maker (a role such as orchestrator) |
the choice, the Policy line applied and the Policy revision |
| Crux nobody derives | not even the Policy settles it | a person | the choice, the reason, who decided |
A Crux is a choice that cannot be derived, neither from the repository and general knowledge (that is an Assumption) nor from the Project's decision Policy, so a decision maker picks. It has no default: what depends on the answer waits, and what does not goes on. Most choices are settled by the Policy and recorded as such; what is left is an arbitrary, intentional decision, and the point is to record who decided what, on which basis, strictly and immutably, because it cannot be reproduced. It applies to every change request and finding (Contracts, Interfaces, Harness implementation, code), not only to the specification.
-
A Crux is a record of its own, with an
id; a change request does not hold it. It is raised in a change request (raised_in), the one it stops: normally the request in whose work the question came up, but any request will do. A request can be stopped by several Cruxes, and a Crux stops one request. It may be raised from findings (findings; none for a question that arises from the text of the request). It has akind(ambiguity: the text reads more than one way;conflict: two promises cannot both hold;model: the words of the model are in question;disagreement: a test and the promise it checks read differently), itsquestion, itsoptions(each with anid, the expectations it asks and, optionally, itsconsequence: what else choosing it affects), a recommended option with areasonand aconfidencethat is a percentage from 0 to 100, animportance(low,normalorhigh), andsupersedes, the id of a Crux whose decision this one replaces. That Crux has to exist, and asupersedesthat parses but that no Crux has the id of is a name that does not exist (input.unknown-name,values.kindcrux,fieldsupersedes, the closest ids indid_you_mean; no suggestion: a Crux id is not made by naming it). It has to have a decision to replace (a withdrawn one has none:cr.supersedes-unanswered), and one Crux replaces a decision: a decision that another Crux replaces already is refused (cr.supersedes-superseded,values.bythe Crux that replaces it), and its suggestion is to supersede the decision that stands, the end of the chain (to decide it, when that Crux has no decision yet; with acr_reopenfirst when its request is resolved). A Crux that was withdrawn replaced nothing, so the decision it pointed at may be superseded anew. The recommendation and the importance are what a Policy line is written against. -
cr_crux_raiseraises a Crux in the request titledin, which must not be resolved (cr.already-resolved: reopen it first):{"op":"cr_crux_raise","in":"Refuse big discounts","question":"Which error is given?", "kind":"ambiguity","importance":"normal", "findings":[{"rule":"cite.uncited-clause","subject":{"clause":{"context":"pricing","contract":"discount","label":"C1"}}}], "options":[{"choice":"reuse InvalidDiscount","recommended":{"reason":"no new type","confidence":85}}, {"choice":"new error","wording":"C2: a refused discount is DiscountRefused", "targets":[{"clause":{"context":"pricing","contract":"discount","label":"C2"}}], "keep":[{"clause":{"context":"pricing","contract":"discount","label":"C3"}}], "consequence":"callers match a new error"}]}question,kindandoptionsare required; there is at least one option (cr.crux-no-option) and no choice is given twice (cr.crux-option-twice,values.option). At most one option hasrecommended(cr.crux-recommended-twice,values.count; itsreasonandconfidenceare optional). An option'stargetsandkeepare written like the request's and have to exist when the Crux is raised. A target that an option names twice is refused (cr.target-twice,values.targetandvalues.option), and one that the request, or a decision taken in it that stands, asks with the other kind is refused (cr.crux-expect-conflict,values.optionandvalues.target). The result has the id of the Crux and of each option, which a decision points at:{"op":"cr_crux_raise","crux":"<crux id>","options":[{"id":"<option id>","choice":"reuse InvalidDiscount"},..],"findings":[]}. An id in angle brackets, here and below, is the one an earlier result gave. -
A decision is required, and "undefined" is a decision.
cr_crux_decidegives it, once, with exactly one ofoption(the id of one of the Crux's options, with an optionalreason),wording(the behaviour in the decision maker's own words, with an optionalreason),unspecified(the behaviour is left free; a list of what must still hold, which may be empty) andwithdrawn(true: the question is not needed any more), with thebasisandby:{"op":"cr_crux_decide","crux":"<crux id>","option":"<option id>","reason":"no new type", "basis":{"arbitrary":"no line of the Policy covers it"},"by":{"person":"alice"}} {"op":"cr_crux_decide","crux":"<crux id>","unspecified":["a refusal is an error"], "basis":{"policy":"P1"},"by":{"role":"orchestrator"}} {"op":"cr_crux_decide","crux":"<crux id>","withdrawn":true, "basis":{"arbitrary":"the clause it asked about is gone"},"by":{"person":"alice"}}The
basisis{"policy":"P1"}(a line the Policy has now; the revision of the Policy is recorded with it) or{"arbitrary":"<reason>"}. A decision to leave it unspecified or to withdraw the question has noreasonof its own: the basis has it. The time comes from the clock of the application, never from the domain. An answer is never rewritten: deciding a Crux that has one is refused (cr.crux-answered), and an option the Crux does not have is not one (cr.crux-unknown-option). Acruxthat no Crux has the id of is a name that does not exist (input.unknown-name,values.kindcrux, with the closest ids indid_you_mean). Changing a decision means raising a new Crux thatsupersedesthe old one; the old answer stays as it was given. A Crux of kindmodelis the one exception to "undefined is an answer": without the word the model cannot be written, so it can be worded, chosen or withdrawn but not left unspecified (cr.crux-unspecified-not-allowed,values.cruxandvalues.kind). A Crux that is no longer needed is withdrawn (any kind may be); a withdrawal is a decision and asks nothing of any target. -
A decision does not widen its request. An option may name
targetsoutside the request (outside its frame: see "Attribution and frame"). When it is chosen they are not added to the request, whose frame stays what it was opened with; the decision says where they go, inbeyond, one of two forms:{"op":"cr_crux_decide","crux":"<crux id>","option":"<option id>", "beyond":{"requests":["Tax work"]},"basis":{"arbitrary":"billing is its own change"},"by":{"person":"alice"}} {"op":"cr_crux_decide","crux":"<crux id>","option":"<option id>", "beyond":{"declined":"billing waits for the next release"},"basis":{"arbitrary":"..."},"by":{"person":"alice"}}requestsare the titles of the change requests opened for them (a title that no request has isinput.unknown-name,values.kindchange request): at least one, none twice, none the request the Crux was raised in (cr.crux-beyond-itself), none resolved (cr.already-resolved, with the request named as the first subject: reopen it), and between them they frame every target outside (cr.crux-beyond-unframed,values.target). They are read wheneverrequestsis given, even when the option names nothing outside.declinedis the reason they are not done. Withoutbeyond, an option with atargetsentry outside the request is refused (cr.crux-answer-outside,values.optionandvalues.targets), and its suggestions are thecr_openthat names them and the decision once withbeyond.requestsand once withbeyond.declined. Akeepis never outside: it protects, it does not widen. A withdrawal asks and adds nothing, so it takes nobeyond(cr.crux-beyond-withdrawn).beyondis a part of the answer, written with it and never rewritten;statusshows it next to the decision as{"requests":["<title>"]}or{"declined":"<reason>"}, andviewthe same with the ids of the requests (an answer that says nothing has none). Correcting what a request is about is closing it as superseded and opening the corrected one, not a decision. -
Who may decide a Crux (the authority to decide,
CruxAnswerable) is a concept of the model, and it is managed outside chidev for now: chidev holds no identity of its users, so it grants and checks nothing.by({"role":..}or{"person":..}) is recorded as declared, and nothing is compared to it. The boundary is decided in ADR 0003. -
The Cruxes stop the request they are raised in: "Lifecycle and resolution" says which resolutions are refused (
cr.crux-unanswered), and a request with Cruxes isdecidedonce each of them has a decision (a withdrawal is one). When a Crux is decided, the targets of the option chosen that are inside the request's frame become its decided expectations, in the same commit (the ones outside are the decision'sbeyond, above): each is recorded with the revision it has then; one an earlier decision recorded with the same kind keeps its first revision, one the request itself asks with the same kind is not added a second time, and one asked with another kind, by the request or by an earlier decision, is refused (cr.crux-expect-conflict, the rule an option is read by when the Crux is raised). A Crux that supersedes a decision taken in the same request takes that decision's expectations back first, in the same commit, so it may ask the opposite of a target; the frame is read after that take-back. The work goesforthe request. A Crux of a resolved request takes only a withdrawal: any other decision is refused (cr.already-resolved) until the request is reopened. A store migrated from layout 1 can hold a Crux that was never decided in a request resolved otherwise than asdone; it is a record of what was, andcr.awaiting-decisiongoes on reporting it. -
cr_decideand thecruxofcr_openare gone; these two operations replace them. A line that has one is refused (input.unknown-op,input.unknown-field), anddid_you_meandoes not name the new one. Raise the question withcr_crux_raise, in the request it stops, wherecruxwas given, and decide it withcr_crux_decidewherecr_decidewas written. The Crux is named by its id and not by the title of its request, and a decision names the option by its id (option) and not by the text of its choice (choice). -
The Policy is the Project's decision policy: numbered lines
P1,P2, ... each with a text.policy_putreplaces all of them:{"op":"policy_put","lines":[{"id":"P1","text":"take the recommendation when its confidence is 80 or more"}]}. Its revision goes up when the lines change, andstatus.policyshows the revision and the lines. The notecrux.policy-line-gonereports an answer that rests on a line which is gone or whose text changed since (by thepolicy_putthat did it, and bycheckfor as long as it lasts); the answer itself stands. -
An Assumption
{statement, reason, evidence}is an interpretation settled from the repository and general knowledge, so nobody is asked.evidenceis a list of{"repo":{"path":"src/round.rs","lines":[10,14]}}(a path relative to the repository, without..;linesoptional, the first and last line, as[16,16]for one line) and{"knowledge":{"reference":"IEEE 754 roundTiesToEven"}}. Give them withcr_open.assumptions, or append later with{"op":"cr_assume","title":..,"statement":..,"reason":..,"evidence":[..]}. Assumptions only grow, and a resolved request takes none.statuslists them with each request.
A change request, a Crux, a measurement and the record of a check's run only grow, with one exception: a text that must not stay (a token, a password) is replaced with a mark by a correction.
{"op":"text_find","text":"<the text>"}
{"op":"record_correct","text":"<the text>","reason":"a token got into the record",
"by":{"person":"alice"},"places":[{"change_request":{"id":"…","field":"situation"}}]}text_findanswers theplaces(a record, by its id, and a field) where the text occurs, never the text.record_correctreplaces the text with[redacted]in each place named. A title or a choice that would become equal to another takes[redacted 2](or the next number).- Each record keeps the correction (the fields, why, who, when; never the text): a
change request as a step
correctedof its history instatus, a Crux and the latest measurement of each subject as theircorrectionsinstatus.viewshows thecorrectionsof a change request and of a Crux. An older measurement and the record of a check's run keep them too, in the store; no operation shows them. - It is a trigger of its own kind (see "Triggers and moments"). It answers
corrected,remaining(where the text still is) andcleared, which says whether the file was written again without the old bytes. - The fields, the refusals (
correct.*) and what the store file keeps afterwards are in the guide's Troubleshooting.
A spec asset is not deleted, it is retired: it leaves the live model, and the
Project keeps a record so that older Pins, change requests and measurements can
still be read. Four operations, one per thing that holds others:
{"op":"context_retire","context":..},
{"op":"contract_retire","context":..,"contract":..},
{"op":"interface_retire","context":..,"interface":..} and
{"op":"harness_retire","context":..,"harness":..}. Clauses, Items and Cases
are removed as before, by putting their Contract, Interface or Harness again
without them; what a put drops is recorded as retired too.
- The parts go with the whole. A Context takes its Contracts, Interfaces and Harnesses with it, and an asset its Clauses, Items or Cases.
- A peer that refers stops it. Retiring is refused, naming what stands in the way, when a Case that stays cites an Item or a Clause that would go (retire or put the Harness that cites first), and when a Context is used by another one. A Context based on its Contracts keeps its last Contract.
- Ids are reserved. A retired id is never given again, and so a name an asset
had in its Context cannot be put again (
interface_putof a retired name is refused); make it again under another name. There is no way back. - The record keeps names and revisions, not the text: for each thing its kind,
names (the Context, the asset, and for a part the label, key or case
name), the revision it had (
last_revision) andat, the revision of what held it once it was retired (the Project's for a Context, the Context's for an asset and what went with it, the asset's for a part a put dropped).status.retiredlists them, in the order they were retired. - Revisions. The Project's revision moves when a Context is retired, a
Context's when one of its assets is, so the Project or the Context named as a
target reads
revisedas for adding. - Outside the Project, ids stay. A Pin of a retired Interface stays and reads
gone(pin.staledoes not report it: retiring is intentional). A target that the work moved and that is retired readsretiredinstatus. Measurements and findings keep their ids. Wherever chidev names a target the live model does not have it names it from the record with(retired)(status, errors, findings andstatus.measurements, whosestateis thenretired). The checks that judge Interfaces and Harnesses (pin.unpinned,mutation.*,sim.*, the uncited and no-harness checks) read the live model, so they do not judge a retired one.
Retiring, like a put, does not need an open change request, but a retire operation
belongs to none. The procedure retires as the work of a change request that asks
for the thing to go: open it with the Context or the asset in targets, retire it
in a change_set for the request (the Cases that cite first, or in the same
set), and resolve it as done once it has. status reads the target retired from
that work. A retire operation on its own is not tied to a request, and a thing it
takes out shows in no request's targets.
Retiring several things, or retiring together with a put, is one change_set with
retires (see "Change sets"): the refusals above are judged on the state the set
ends in, so a Harness can be put without the Cases that cite an Item and the
Interface retired in the same set. Retire operations on their own are change sets
of one, without for.
Each entry in cr_open.addresses, and in the findings of cr_crux_raise (the
findings a Crux is raised from), names a rule and the subject of its finding. It
uses the same subject form as finding_report:
[
{"rule":"context.no-harness","subject":{"context":"pricing"}},
{"rule":"pin.unpinned","subject":{"interface":{"context":"pricing","interface":"rounding"}}},
{"rule":"cite.uncited-item","subject":{"item":{"context":"pricing","interface":"rounding","key":"round"}}},
{"rule":"cite.uncited-clause","subject":{"clause":{"context":"pricing","contract":"totals","label":"C1"}}},
{"rule":"judged.case-review","subject":{"case":{"context":"pricing","harness":"rounding-tests","name":"half rounds up"}}},
{"rule":"cr.keep-moved","subject":{"change_request":"change rounding"}},
{"rule":"judged.project-review","subject":null}
]Omitting subject also names the whole Project. The legacy address forms with
an item or clause directly beside rule remain accepted. Each address may
contain only one selector key (subject, item, or clause), even when a value
is null. Unknown address fields and unknown fields inside a subject are rejected.
Names must resolve when the change request opens. After that, the reference uses stable ids and survives later changes to the subject. The rule need not be registered or currently report a finding; its id must be well formed. Opening a request does not itself run a checker or judge a rule.
For Rust callers, FindingInput uses subject: Option<SubjectInput> in place of
its former item / clause fields. Wrap those references in
SubjectInput::Item / SubjectInput::Clause; app::SubjectInput remains the
public re-export. See app::subject and domain::check::FindingState in the
RustDoc for subject resolution and how a finding's state follows its requests.
A refused operation is a violation of a rule, and it is printed as structure, not as a sentence to be parsed:
{"ok":false,"line":9,"diagnostics":[{
"code":"cr.parent-open","level":"error",
"message":"the change request `Parent` has 2 part(s) that are not resolved: `Part one`, `Part two`",
"subjects":[{"kind":"change_request","name":"Parent"},
{"kind":"change_request","name":"Part one"},{"kind":"change_request","name":"Part two"}],
"values":{"count":2},
"help":"A part is a change request whose `parent` is this one. Resolve each part first ...",
"suggestions":[{"op":{"op":"cr_resolve","title":"Part one","resolution":"done","reason":"<why>","by":{"person":"<who>"}},
"applicability":"placeholder"}],
"did_you_mean":[]}]}codeis the id of the rule and is stable: a published id keeps its meaning and is never reused, and the keys of a rule'svaluesare added to, never removed. Read thecodeand thevalues; themessageis for a person.subjectsare what it is about, named asstatusshows them.field(when the refusal is about the input) says where:for,puts[0].context,cases[0].items[0].interface.helpsays why the rule holds and how to mend a violation.suggestionsare the operations that mend it:opis an operation to send (with"<..>"where a value is yours to fill in), or a sentence when no single operation does it. Theirapplicabilitysays how far to trust one, as rustc's does:machine(it mends the violation and changes nothing else: apply it as it stands),review(likely what is meant, but it changes the model: look first) andplacehold 781F er(it has a hole: fill it in).did_you_meanlists the names that exist and are close to one that does not (input.unknown-name): a title or a label one letter off, or the same in another case.- A line that cannot be read as an operation is a diagnostic as well, under an
input.*rule:input.unknown-op,input.unknown-field,input.missing-field,input.wrong-type,input.unknown-name,input.invalid, and forviewinput.view-sections-empty. A name that does not exist isinput.unknown-name(values.kindandvalues.name, the closest names indid_you_mean, a suggestion where the name can be made by naming it): a title, a label, acruxofcr_crux_decideand asupersedesofcr_crux_raise(values.kindcrux,fieldsupersedes, no suggestion), a title inbeyond.requests. Aninterface_putthat leavesitemsout has its own:input.items-required,items.no-provider,items.protocol,items.failed, and forrenamesinput.rename-unknown,input.rename-conflict,input.replaces-kept,input.renames-with-items. Aclauses_put, or acontract_putwithreplaces, hasinput.unknown-namefor areplacesorremovelabel the Contract does not have (values.fieldsays which),input.replaces-keptandinput.rename-conflict(values.side:label,replacesorremove). The subjects of the last two are the Context and the Contract; the subject ofinput.unknown-nameis the Contract, as for an unknown cited label. - A change refused by checks keeps its
findingsbeside the diagnostics, and a finding in any result has the same fields (codeis its rule,helpits rule's). A refusal that has no rule of its own yet isdomain.unmapped(orapp.unmapped), which says what was refused; it is a bug to report.
A rule's level is what is shown, not what refuses. A change is refused for a
violation of an invariant (always error; no policy lowers it), for an error of a
mandatory check, and for a finding of a gate (see "External checks"); every other
check says what it finds at its level and the change is kept.
{"op":"rules"} lists every rule (the invariants an operation is refused for, the
input rules, and the checks with the level the policy gives them), and
{"op":"explain","rule":"cr.parent-open"} explains one: its help, the keys of the
values a diagnostic of it carries, and an operation that violates it. An agent
that is refused should read suggestions and help before it tries again, and run
explain for a rule it does not know.
In Rust, ChangeRequestError, ProjectError, ContextError and PinError give the
domain's Violation (violation, violations; see domain::violation and
domain::rule), and App::diagnose turns an AppError into Vec<Diagnostic>,
which any front end (the CLI, a server) lists as it is.
When an agent reads check_rules, each rule includes a rule_digest. The agent
must return that exact value in its finding_report, including a report with no
findings. A missing, null or malformed digest is refused as an input error, and
one that differs from the current definition as a rule version mismatch; either
way the report replaces nothing and records no run. The result carries the saved
rule_digest and the freshness state.
The digest covers only the fields that decide the answer: id, subject,
question, true and false (see JudgedRule::digest in RustDoc). The
summary, fix, when hints, moments and default level describe, guide, schedule or
weight the rule without changing what is a finding, so editing them, TOML
comments or formatting, or a Policy level override leaves saved judgments
current. Changing a deciding field makes the saved report and every finding
stale. An App keeps its Registry for its lifetime; the CLI reloads
configuration on each invocation.
Database rows written before digests existed hold a null rule_digest: they
stay readable and show as stale (rule_digest: null), since the definition they
were judged on cannot be established after the fact. A matching digest still
cannot make a report on an older model current.
Rust callers supply ReportInput.rule_digest (a String) from
RuleToJudge.rule_digest.
Licensed under either of Apache License, Version 2.0 or MIT license at your option.