Five jobs, end to end

Written 3 September 2026 by walking each job at the command line. Every command below was run; every figure is read off its output. Where a workflow stops, it says so and names what would move it.

The command line has been built one subcommand at a time and nobody had written down the journeys it is supposed to serve. This document is those journeys. It is not a feature tour: the useful half of it is where each one breaks, because that is the part a buyer discovers on their own otherwise.

Read GETTING-STARTED.md first if you have never run the tool. This assumes it is installed and that construct is on your path — everything below also works as python -m forensic from a source tree, and the help text names whichever one you typed.

One fact before any of them. On the measured corpus — 66 files when this was measured, 62 since 2026-09-04, because a file holding several projects is now refused rather than reviewed as its first one (engine/quality/VERDICT-YIELD.md §13.1) — a single schedule file with no contract terms supplied answers a median of 45 clauses, 13 of them a FAIL or an OBSERVATION, and leaves 75 listed as "Not evaluated", each naming the one input that would settle it. That is the shape of workflow 1 and it should be stated rather than discovered. (Counting REFERRED as adverse too, the middle figure is 22 rather than 13; the sentence used to say "adverse" and mean the narrower thing without saying so.) 116 of the 351 registered rules — 33% of the catalogue — cannot answer from a schedule file at all. They want a number from the contract or a declaration from the analyst. A buyer who supplies nothing but an XER is buying the other 220. That pair was ~~122 of 331~~ until 6 September 2026, when the numerator was re-measured rather than re-denominated; quality/VERDICT-YIELD.md §3.3 has the run. The companion figure — 83 still silent once every contract term we ship an example of is supplied — was measured over 331 rules and has not been re-measured, so it is not restated here.

And what those terms buy is answers, which is not the same as insight. Every input a reviewer can supply adds 68 rules that return a verdict and 8 that return a different verdict on different schedules. The agency specifications are where the difference lands — their terms are approval registers checked against your own activities, and 642 of the 972 new adverse findings the state-DOT terms produce vary from file to file. The doctrinal protocols are not: the UK SCL terms take 19 answering rules to 46 and leave the discriminating count at 12, because those clauses interrogate the analysis rather than the file (engine/quality/VERDICT-YIELD.md §8.4, §9.1).


Contents

  1. A DOT reviewer, baseline submission
  2. The same reviewer, update 7 of 30
  3. A contractor's scheduler, before sending
  4. A forensic analyst, baseline against as-built
  5. An expert being deposed, reproducing a record
  6. What is common to all five
  7. Where the whole surface stops

1. A DOT reviewer receives a contractor's baseline

The question: is this baseline acceptable? The tool does not answer it, and says so at the head of every report. It states what the file contains and what the cited clauses say. The determination is the reviewing authority's.

The commands, in order

construct check BASELINE.xer --baseline 0 --jurisdiction us_federal
construct validate BASELINE.xer --baseline 0
construct record BASELINE.xer --supplied-by "Contractor" --received-at 2024-02-01

Worked on claim-01-baseline.xer from demo/claim-outputs/ — 18 activities, 20 relationships, data date 2026-03-02. Runtime about three seconds.

Why that file and not a real one. The corpus schedules this project measures against were harvested from repositories that mostly state no licence, which is the absence of a grant rather than a permission (engine/oracle/corpus/SOURCES.md), so no report derived from one is published here and no corpus file is named. demo/claim-outputs/ is written by demo/claim.py from engine/tests/xerfuzz.py:claim_scenario, which builds every table from literals in this repository: it derives from no real project, no client file and no third-party dataset. It is a small file, and the numbers below are correspondingly small — that is the honest cost of a worked example anyone can regenerate.

What check returns and what it supports

21 passed, 6 failed — 78% of the 27 clauses that were scored, and those 27
are 22% of the 120 clauses that apply. Of the rest, 4 were stated as
observations rather than scored, 10 need a person and 79 could not run.

Exit 1, because all six failures are blocking. The six are the review: two missing mandatory activities (§3.3.8.1, §3.3.8.2), three open-ended logic defects under §3.3.11 naming MOBILISE, OPEN and STRIPE, a baseline carrying an actual date on MOBILISE (AACE 29R-03 §2.1.B(3)), and no contemporaneous update supplied — which two clauses report separately, ASCE 67-17 §4.2 and AACE 29R-03 §2.3.B, because they lose different things by it. Each names the activity by the Activity ID P6 displays, so the reviewer can find it in the software without a lookup.

That is a rejection letter's worth of material, and it is the whole of what one file supports.

Three things --baseline 0 and --jurisdiction buy, measured

With neither flag the jurisdiction defaults to neutral: 107 clauses apply, 9 are scored, 86 could not run. With --jurisdiction us_federal --baseline 0: 120 apply, 27 are scored, 79 could not run. Naming the two facts the tool cannot infer is worth eighteen scored clauses and thirteen more applicable ones on this file.

Naming the baseline is not a formality: --baseline is never guessed, because the earliest file supplied is very often a superseded plan, and promoting it silently would measure every variance from the wrong origin. Two AACE 29R-03 clauses and the whole of the AACE 53R-06 update-review checklist sit behind it.

Where it breaks

79 clauses could not run, and that is the largest section of the report. Every one names its missing input. They are not defects — VERDICT-YIELD.md §3.1 established that zero of 21,846 rule-file pairs abstained for a reason internal to the tool, and it is still zero of the 20,522 the smaller population produces (VERDICT-YIELD.md §13) — but a reviewer opening the report sees a long list of things not checked and has to work out that it is a shopping list.

The seventy-nine divide into two kinds:

Start from examples/terms.us-federal.json, examples/terms.state-dot.json or examples/terms.uk-scl.json, which carry every key their pack reads with the clause that wants it beside each one; examples/terms.min.json is the short version. A key nothing reads is complained about on stderr, with the near match, and a key stated twice in the same object is refused outright rather than resolved to one of its two values. * The other nine want a second schedule, which a baseline submission does not have by definition. They are correctly silent and workflow 2 is where they answer.

--respond is what turns that list into a next action. It regroups the same findings by what the recipient has to do with them — nothing is recomputed and no verdict changes:

construct check BASELINE.xer --baseline 0 --jurisdiction us_federal --respond

On this file that produces Change in P6 before resubmitting (6), Inputs you can supply (79), Questions for the narrative, not the file (10) and Nothing to do (25). It is written for the contractor and is the section a reviewer can forward without editing.

What validate adds, and why it comes second

Source validation under AACE RP 29R-03 §1.5.B(1) is the precondition for float and criticality meaning anything. On the same file:

2 passed, 2 failed of 14 source validation checks.

Not scored, deliberately: one failure can make every number computed from the schedule unusable, so a percentage would average away the finding that matters. The failures are SVP-2.1.B.3-PROGRESS — one activity in a baseline carrying actual dates — and SVP-2.1.B.5, MOBILISE with no predecessor. Two protocols are referred to a person (is the level of detail adequate; did the calendars represent the expected working days) and eight could not run, six of them for want of a terms key and two for want of a second dated update.

Exit 0 here even though a protocol failed, because none of the failures is blocking. --strict also fails on a protocol that could not run.

What record adds

A file digest over the bytes as received, a model digest over the parse that excludes everything computed, and a reproducibility manifest. --supplied-by and --received-at are free text and a date you supply: the received date is never read from the system clock, because a wall-clock reading taken at analysis time would be a fabricated fact. Exit 1 if the set of documents has an observable hole in it.

The workflow's own limits


2. The same reviewer receives update 7 of 30

The question: is this a valid update? This is the workflow the tool is best at and the one its documentation under-sells.

The commands

construct check UPDATE-06.xer UPDATE-07.xer --baseline 0 --jurisdiction us_federal --review
construct validate UPDATE-06.xer UPDATE-07.xer --baseline 0
construct explain UPDATE-07.xer --activity <the activity that moved>

Files are given oldest first, and the order is an input. check reads them as successive versions of one project. If they are unrelated submissions, that is index, not check, and the tool will not guess which: activity-code overlap runs high between unrelated jobs and low between genuine updates, so a guess would sometimes report several jobs as one job's history, silently and plausibly.

What the second file is worth, measured

Same invocation, --jurisdiction us_federal --baseline 0 --review, on harbour-point-24.12 baseline and as-built:

On the later file alone, 181 clauses apply, 68 are answered and 113 could not run. On both files given oldest first, 183 apply, 88 are answered and 95 could not run.

Twenty clauses move from abstention to a verdict on the second file. They are the ones whose subject is change: what was added or deleted, whether original durations moved, whether activity descriptions were rewritten, whether the controlling path changed at the update boundary, whether the update cadence meets the specification.

--review is doing a lot of that work and it is worth knowing why: it elects the AACE schedule-review recommended practices, of which 53R-06 is the update-review checklist — twelve rules that exist only for this workflow. Without --review the same pair gains nothing measurable, because the update-review clauses are never applicable. They are elected rather than added to the precedence: they apply to any schedule and govern none, and the report prints them under their own heading so nobody reads them as binding.

What the reviewer actually decides on

Then explain --activity ID narrates why one date is what it is, link by link — the relation type, the lag, the calendar the lag was measured on, and the two dates either side of it, so the reviewer can check each line against P6 without trusting this engine. Where two predecessors impose the same bound, both are named: a trace that picked one would invite the reply "but the other finished the same day".

Where it breaks


3. A contractor's scheduler checks their own submission

The question: what will the reviewer say, and what do I fix first?

This is the same engine pointed the other way, and until 3 September 2026 there was no command for it: the report was ordered by verdict and severity, which is the reviewer's question. --respond is the answer to the contractor's.

The commands

construct check MY-SUBMISSION.xer --baseline 0 --jurisdiction us_federal --respond
construct validate MY-SUBMISSION.xer --baseline 0

What comes back

The findings section unchanged — both parties read the same document, which is the only reason either can trust it — followed by Responding to this review, which is the identical set of findings grouped by what to do:

Above all of it, four sentences saying what the section is not. It does not produce a corrected schedule file, and no file this tool touched should be submitted: NAVFAC UFGS 01 32 17.00 20 makes importing schedule data through third-party software cause for rejection of the submittal. It does not state an entitlement position — whether a finding is correct, applicable, waived or worth disputing is a contractual judgment, and the tool locates findings and quotes clauses rather than arguing them.

The line is: the tool says what was found and where to find it; the person says what it means. engine/tests/test_respond.py::test_does_not_argue asserts the vocabulary of argument absent and there is no field through which a caller could supply it.

Where it breaks


4. A forensic analyst: baseline, as-built, and a delay analysis

The question: how many days, whose, and will it survive cross-examination?

The commands

construct validate BASELINE.xer ASBUILT.xer --baseline 0
construct analyse BASELINE.xer ASBUILT.xer --baseline 0 --method 3.1
construct analyse BASELINE.xer ASBUILT.xer --baseline 0 --method 3.3
construct analyse BASELINE.xer ASBUILT.xer --baseline 0 --method 3.8 --delays delays.json
construct concurrency BASELINE.xer --delays periods.json --interval 1
construct record BASELINE.xer ASBUILT.xer --baseline 0

validate first, and it is not a formality. AACE RP 29R-03 §1.5.B(1) requires the critical path and float to come from a validated network. An analysis on an unvalidated baseline is worthless however good the engine is, and the source validation state is carried into every analyse report as an assumption of that analysis.

The method is an argument, and the tool will not make it for you

--method takes a section number, or a common name only where that name means exactly one method. TIA does not:

'TIA' could mean MIP 3.6, 3.7. Was the delay modelled against one base
schedule (3.6) or against a separate contemporaneous update per event (3.7)?

That refusal is the point of the whole package. So are these:

What the methods return, on the worked pair

Where it breaks

tia is a different job, and is prospective

construct tia UPDATE.xer --fragnet fragnet.json --delay-start 2024-03-01 \
    --unit work_days --reviewed 0 --already-awarded

Eight steps under AACE RP 52R-06, each recording what it did, what it assumed, and whether the tool decided it or a person did. Three of its arguments are refusals in disguise and all three are right:

--reporting-period is the one that defaults, to RP 52R-06's own example figure of 30 days, and the report carries that as an assumption naming what it gates. On a fortnightly contract it decides whether the analysis can proceed at all.


5. An expert being deposed reproduces a record

The question: is this the same answer, from the same files, months later?

The commands

python -m forensic.evidence record BASELINE.xer -o record.json
python -m forensic.evidence verify record.json BASELINE.xer

What comes back

WARNING REPRO.METHOD.UNCHECKED the record's method states command='evidence
record', files=['claim-01-baseline.xer'], and this verifier does not check any
of it
WARNING REPRO.CONFORMANCE.ABSENT this record binds the computed schedule and
states no conformance verdicts, so nothing here re-derives a PASS, a FAIL or an
abstention
REPRODUCED: every value this verifier checks was recomputed from these files.
2 note(s) above say what it did not check, or what differs without changing
the answer.

Followed, on both branches, by the sentence that matters in a deposition: the manifest digest is computed by whoever wrote the record and signed by nobody, so it detects a record altered after it was written, not a record its author wrote to say what they wanted; and nothing here establishes when it was written or that these are the files in dispute.

Verified against the wrong file, it names every difference — the label, the file digest, the model digest, the answer digest, and each summary field with both values — and exits 1. Exit 1 is not an accusation: the commonest cause of a divergence is a different engine version, which the manifest names, and the second is reproducing against a different file, which the input digests name. Both are stated before any date is compared.

Where it breaks

~~verify is not a construct subcommand. It is reached as python -m forensic.evidence verify. Until 3 September 2026 it appeared in demo/run.py and in no --help output anywhere, so a reviewer who had run construct --help had no way to learn that the integrity claim the whole product rests on is executable. The top-level help now names it — which tells you it exists and does not make it a subcommand. CAPABILITY-GAPS.md §5 item 6 calls registering it "trivial"; it has not been done, and it was not done tonight because evidence.py was under another agent's edit. This is the single largest discoverability defect on the surface.~~

Struck 6 September 2026: it was registered, and this page went on describing the gap as the product's worst. construct verify is a subcommand. Measured on this tree at engine 0.3.0, from the repository root:

construct record engine/oracle/cases/p6-23.12-xval.oracle.xer --project XV01 \
    --json --out record.json
construct verify record.json \
    engine/oracle/cases/p6-23.12-xval.oracle.xer --project XV01

Both exit 0, and the second prints REPRODUCED: every value this verifier checks was recomputed from these files, followed by the notes naming what it did not check.

It is listed in construct --help between record and the end of the command table, cli.py registers it beside the others, and verify --help states its own exit codes. python -m forensic.evidence verify also still works and writes a smaller record than construct record --json does; the two are not the same artefact, which is worth knowing before handing one to the other.

The discoverability point survives its own repair in one narrower form, and that is what is left of the paragraph above: verify takes no --terms, --jurisdiction or --baseline, deliberately, but --project must be given again, because it changes every digest. A reviewer who omits it on the second command verifies a different question from the recorded one and is told so.

Two smaller ones at the same site, both handed back rather than fixed for the same reason:

And record is a name doing double duty in a third sense: -o is accepted by forensic.evidence and by nothing else. Everywhere else the flag is --out.


6. What is common to all five

The flags mean one thing each, across every subcommand.

Exit codes, everywhere: 0 nothing blocking was found — which, given the coverage figures, is a weaker claim than "conforms"; 1 the tool ran and something is wrong or unestablished; 2 nothing was analysed, because the inputs could not be read or an input was refused by name. Each subcommand's --help states its own variant.

Everything that is not the report goes to stderr. Progress, refusals, and the "written to PATH" confirmation. A shell pipeline capturing the report is unaffected.

Large files say what they are doing. A file over 4MB prints one line naming it and its size before it is read — to a terminal only, never to a pipe, a redirect or a captured stderr, so it cannot reach an artefact. The largest corpus file measured takes about 30 seconds and printed nothing at all before this existed. There is no spinner and there will not be: a spinner carries its meaning entirely in an animated glyph, which is the one thing that does not survive being read aloud.

Every report opens with the same eighty-odd lines, and you should know that before you scroll. The reservation of determination, the governing documents, where those documents disagree, the choices that could have gone the other way, the assumptions and the standing limitations all precede the answer — because in a document going to a forum they belong there. Measured on the worked file, the findings begin at line 97 of 268 for check, 90 of 124 for validate, 87 of 137 for record, 84 of 108 for index, and the driving-path trace begins at line 105 of 235 for explain. Piped to a pager, /^## Findings gets you there; the HTML report has a contents navigation and the Markdown does not. This is the most common complaint a first-time user will have and it is a deliberate trade, not an oversight.

The reports are byte-identical across runs, processes, locales, timezones and paths. engine/tests/test_determinism.py pins it. The one field that is not is Date prepared, which is a self-asserted local time and says so on the page.


7. Where the whole surface stops

Ordered by how often it will be hit.

  1. There is no command that helps you write a terms.json. 116 of 351 rules are inert without one; 61 of those want a single value a contract states in one line. Four annotated example files landed on 3 September and are worth 25 more scored clauses on the worked file, but no command lists the keys a given jurisdiction would read. ~~and none checks a supplied file against them.~~ Corrected 2026-09-04: --terms does check the file it is given — cli.term_complaints names an unrecognised key and a key in the wrong format instead of dropping it in silence. Listing the keys a jurisdiction would read is the half that is still missing. Measured since: with all three shipped terms files supplied at once the inert count falls from 122 rules to 83, and 41 of those 83 want an asce.* or aace.review.* namespace no example file demonstrates (engine/quality/VERDICT-YIELD.md §10). This is still the largest single lever on the product.
  2. verify is undiscoverable. §5.
  3. A schedule with circular logic gets no conformance report. It is refused by name with the loop in Activity IDs and an explicit statement that the silence is not a pass — but UFGS-071, the catalogue's only clause about circular logic, is never reached, and its 66 corpus PASSes are 66 files selected for being unable to falsify it. engine/quality/NEVER-ADVERSE.md §4 owns that finding. The honest options were to abstain the rule, or to let the rules run on an unscheduled network. ~~neither was done on 3 September 2026.~~ Superseded 2026-09-04: the first was done. UFGS-071 now abstains on any snapshot carrying a CPM result — such a snapshot was necessarily loop-free before the clause was asked — so 66 unfalsifiable PASSes were withdrawn in each arm and no FAIL count moved. Handed an unscheduled snapshot the rule still decides in both directions. A schedule with circular logic still gets no conformance report, which is the part of this item that stands.
  4. The multi-project XER needs --project and nothing tells you which. Naming an unknown project lists every one the file holds, which is the right refusal — but the first run is a refusal.
  5. --delays and --fragnet are hand-written JSON with no worked example against a real file.
  6. Thirty updates has not been timed, and the monthly resubmission loop re-reads the whole chain.
  7. No command answers "what changed since last month" directly. The material is in check on two files and in explain; the framing is not.

The commands, as one list

construct --version             the engine version every report names
construct check FILES...        conformance against the specification that governs
construct unanswered FILES...   the abstentions grouped by the input that would close them
construct health FILE           the DCMA 14-point screening metrics — never exits 1
construct serve                 a browser page on this computer, for a reviewer with no terminal
construct index FILES...        many unrelated submissions, one row each
construct validate FILES...     AACE 29R-03 source validation
construct explain FILE          why one activity starts when it does
construct clause CLAUSE         what one clause asks and what would settle it
construct analyse FILES...      one named MIP
construct concurrency FILE      concurrency under every doctrine
construct tia FILE              prospective TIA under AACE RP 52R-06
construct record FILES...       provenance, digests, and what is absent
construct verify REC FILES      re-run a record's files and check the answer holds

python -m forensic.evidence record FILES...   write a smaller reproduction record
python -m forensic.evidence verify REC FILES  check one against the files

Corrected 6 September 2026: this list held nine of the thirteen and omitted verify while listing its forensic.evidence form, which is the same omission §5 above is struck for. construct with no arguments prints all thirteen with a worked example, and that output — not this list — is the one to trust if the two ever disagree again.

Source: docs/WORKFLOWS.md. Source commit date: 2026-09-06.

See it in practice

Follow the evidence, from the schedule to the finding.

Explore the worked example