Conformance — the reviewer's guide to the rule packs
A schedule submitted under a specification is checked against that specification by a person, by hand, clause by clause, in an afternoon. The check is slow, inconsistent between reviewers, and undocumented in its reasoning. That is the job these rule packs automate.
A passing report does not mean a compliant schedule. §4 of this document is the list of what the packs do not check, by clause number, and it is the section to read before you rely on a green tick.
1. The packs
| Pack | Specification | Rules | Deterministic | Hybrid | Judgment |
|---|---|---|---|---|---|
ufgs |
UFGS 01 32 01.00 10 | 48 | 18 | 23 | 7 |
asce67 |
ANSI/ASCE/CI 67-17 | 35 | 3 | 25 | 7 |
aace29 |
AACE RP 29R-03 | 37 | 13 | 20 | 4 |
scl |
SCL Delay and Disruption Protocol, 2nd ed. | 35 | 6 | 27 | 2 |
aace_review |
AACE RP 78R-13 and 53R-06 | 61 | 42 | 16 | 3 |
vdot |
VDOT progress-schedule special provisions | 26 | 18 | 6 | 2 |
gao |
GAO Schedule Assessment Guide (GAO-16-89G) | 29 | 15 | 7 | 7 |
aace84 |
AACE RP 84R-13 | 8 | 3 | 4 | 1 |
caltrans |
Caltrans 2025 Std Specs §8-1.02C | 11 | 4 | 7 | 0 |
paseg |
NDIA IPMD PASEG (v4.0) | 5 | 3 | 2 | 0 |
nysdot |
NYSDOT Item 639.2X010011 Type 2, with the NYSDOT CPM Best Practices manual | 16 | 12 | 4 | 0 |
navfac |
UFGS 01 32 17.00 20 (NAVFAC) | 19 | 10 | 8 | 1 |
wisdot |
WisDOT SE Region CPM review guidance (Dec 2023) | 5 | 3 | 2 | 0 |
wvdoh |
WVDOH 2023 Std Specs §108.3, with the WVDOH CPM Schedule Review Manual | 16 | 12 | 4 | 0 |
351 rules across fourteen packs. Read off cli._AUTHORITY_PACKS, first on 2
September 2026 at 331, again on 5 September at 335, when navfac gained the
four clauses of UFGS 01 32 17.00 20 outside its §1.10 tree that
engine/quality/DUTIES-OUTSIDE-THE-METHOD.md audits, and on 6 September at 351,
when wvdoh landed West Virginia's §108.3 with the agency's own reviewer
checklist beside it. That is the count of the library; no single command runs all of them,
and the two populations have been confused on this page before. §2 gives the
count each --jurisdiction actually runs, which is the number a reader should
quote about a report.
Until 2 September 2026 the paragraph below said "three" and then named two, when
aace84 was already in the table and in no jurisdiction's precedence. The count
is now read off Authority: an authority in no governing() precedence is
reachable only by --review or --contract.
Every one of the fourteen is selectable by some command. Ten do not appear in
any jurisdiction's default count below. aace_review is elected and runs only
under --review. The other nine — vdot, gao, aace84, caltrans, paseg,
nysdot, navfac, wisdot and wvdoh — are contract-named authorities:
they bind when the contract names them, and they are named with --contract:
wvdoh is the second pair after nysdot: §108.3 of West Virginia's standard
specification book is a contract term, and the Construction CPM Schedule
Review Manual read beside it is training material for the agency's own
reviewers and is not. Ten of its sixteen rules cite the specification and may
FAIL; the six that cite a numbered checklist item return an OBSERVATION at
worst, and a test reads the module source to hold that ceiling rather than
trusting a fixture to trip it.
wisdot stretches that description and the stretch is recorded rather than
smoothed over. WisDOT SE Region's Sample Schedule Review Comments is not a
contract term — it states no requirement in its own voice, and
Authority.is_contract_term is False for it alone among the state DOT
authorities here. --contract wisdot_se_review_2023 therefore elects it the
way --review elects the AACE checklists, and every rule in that pack returns an
OBSERVATION rather than a FAIL. docs/WISDOT-PARTITION.md records what the
document is evidence of and what it is not.
construct check engine/oracle/cases/update-chain-02-update.xer --jurisdiction us_state_or_private \
--contract vdot_progress_schedule
which takes that job's count to 98 (26 + 37 + 35). A named document is put
ahead of the jurisdiction's own order, not appended to it and not listed as
elected — the special provision deletes and replaces §108.03 of Virginia's
specifications, so it is a term of the contract, and a contract term outranks a
standard for the same reason UFGS §3.8.4 makes ASCE govern over AACE on federal
work. --contract is repeatable and takes the documents in the contract's own
order, because that ordering is itself a term.
Until 2 September 2026 this paragraph was true of the library and false of every
command a user could type: vdot was in no jurisdiction's precedence and no
flag reached it, so the 98 was a real number produced by a call only a library
caller could make. engine/quality/UNREACHED.md §3.1 is the record;
engine/tests/test_unreached.py now asserts the 98 of the command above.
The gao pack takes the same route, and for a different reason. GAO-16-89G is
not a contract term by its own force and not an ANSI consensus standard: it is
one audit agency's published criteria, and it governs by adoption. So it is
in no jurisdiction's default precedence — putting it in us_federal would assert
that every federal construction contract incorporates it — and
--contract gao_16_89g is the assertion that this contract, solicitation or
agency policy is one that does.
construct check engine/oracle/cases/update-chain-02-update.xer --jurisdiction us_federal --contract gao_16_89g
which takes that job's count to 149 (29 + 48 + 35 + 37).
The GAO pack's honest partition is its main product, and it is smaller than 29
rules suggests. Three of the Guide's ten best practices carry no rules at
all. Best Practice 3 (assigning resources) and Best Practice 8 (conducting a
schedule risk analysis) are refused whole — a Snapshot carries no resource
assignments and no three-point duration fields, and every measure Table 11 lists
for those two is a count over a field this engine does not read. Best Practice 5
(traceability) carries one rule that refers rather than measures: its horizontal
half is the delay-injection probe already implemented once as DCMA check 12 in
cpmcore.health, and its vertical half needs schedules at more than one level.
engine/src/forensic/rules/gao.py's module docstring is the practice-by-practice
partition and the list of what is deliberately not implemented.
Nothing in the GAO pack carries a threshold the Guide does not state, and the
Guide says why, at the head of appendix VI: "No 'pass-or-fail' thresholds or
tripwires are associated with the measures." Seven of its rules therefore report
a measure and refer the question — the float distribution, the duration profile,
the critical share, path convergence, the detail-to-milestone ratio, baseline
variance, traceability — and none of them can return FAIL, which
engine/tests/test_rules_gao.py asserts across the whole pack.
navfac is the first --contract pack that is federal, and the reason it
takes that route is the reason it exists. UFGS 01 32 17.00 20 is a Division 01
specification section prepared by NAVFAC, exactly as UFGS 01 32 01.00 10 is one
prepared by USACE, and both are terms of the contracts that incorporate them. But
a federal construction contract carries one of the two, not both: putting
this section into us_federal's default precedence alongside the other would run
two specifications against one schedule that contradict each other in plain
text, and print both under "Governing documents" on every federal job.
construct check engine/oracle/cases/update-chain-02-update.xer --jurisdiction us_federal --contract ufgs_01_32_17
The contradiction is the pack's argument and not an inconvenience. On
out-of-sequence progress, NAVFAC §1.4.2.f says "Out of Sequence progress is not
allowed" and states no approval route; USACE §3.3.13 allows it "only on a
case-by-case basis subject to approval by the Contracting Officer". One check,
one schedule, two verdicts — NAVFAC-1.4.2.f is DETERMINISTIC and UFGS-044 is
HYBRID — and which is right is a fact about which section the contract carries.
On the share of a baseline that may sit on the longest path, NAVFAC caps it at a
bracketed "[30 percent][_____]" whose editing note instructs the specifier
to retain 20 and permits up to 80 on linear work; Caltrans §8-1.02C(6) permits 50
percent of a larger population; USACE states no such cap at all. Three
authorities, one metric, three answers. Both disagreements are registered in
forensic.jurisdiction as Conflict records rather than resolved, because
neither federal agency's section is senior to the other's.
That bracket is also why NAVFAC-1.10.1.3 abstains rather than measuring:
[30 percent][_____] is a fill-in-the-blank for the specifier, so the threshold
is a term the executed contract states and the rule returns NOT_EVALUATED
naming longest_path_share_limit until somebody supplies it. It is the clearest
case in the library of GAO's "No 'pass-or-fail' thresholds or tripwires are
associated with the measures" being true of a specification and not only of an
audit guide.
Cost loading is this section's title and no rule in the pack checks it.
§1.10.1.10 states arithmetic requirements — the sum of activity costs must equal
the contract value, a 40 percent demobilization floor, a bracketed 10 percent
commissioning floor, zero cost on Government-responsibility activities — and
cpmcore does read costs from TASKRSRC. They stop at the exchange model:
forensic.rules.Snapshot carries cpmcore.network.Task, which has no cost
field, and cli._load_snapshot does not carry source.assignments past the
boundary. A rule reading an absent field passes every schedule, so none was
written. corpus/standards/UFGS-01-32-17.00-20_PROVENANCE.json records it as the
open item with what becomes decidable when Snapshot carries assignments.
The caltrans pack is the opposite shape, and its numbers are all
Caltrans's. VDOT's special provision states triggers — things P6's own
Schedule Log reports. §8-1.02C states bands: 50–500 activities, 1–20 working
days per activity, and a cap of 50 percent of baseline activities critical or
near critical. Two of those fail from either end and the third from one, because
the specification states one end. The near-critical threshold the cap needs —
total float of not more than 10 working days — is Caltrans's own, and it is
in §1-1.07B, the Glossary, not in §8-1.02C; a reviewer looking only at §8
will not find it. §8-1.02C(1) makes the section apply where the Bid Item List
carries the level 2 CPM bid item, so it is a contract term and outranks a
standard it conflicts with. engine/src/forensic/rules/caltrans.py records the
one conflict that follows: AACE 92R-17's near-critical rules give roughly ten
working days on a monthly-updated job and two or three on a weekly one, so they
nearly agree on the first and do not on the second.
The pack cites clause (13) of the Category III special provision throughout,
and gives the Category II and Design-Build equivalents per rule — Category II
has no Cost Loading and no Resource Loading clause, so from (11) onwards its
numbering runs two lower, and the Design-Build exhibit uses roman numerals.
Citing "clause (13)" on a Category II job cites Progress As-built Information
(corpus/specifications/README.md §2).
Severity distribution in the UFGS pack: 1 critical, 26 major, 13 minor, 8 info.
The one CRITICAL rule is UFGS-043 (§3.3.12, actual dates against the QC
daily reports), because §3.3.12 makes that breach automatic disapproval of the
whole update — the specification says so; it is not the reviewer's discretion.
The packs check different subjects
The UFGS pack checks a schedule. The ASCE pack mostly checks an analyst.
ASCE 67-17 is a principles standard for how a schedule delay analysis is conducted, ordered, evidenced and bounded. It states no method taxonomy of its own and prescribes no CPM calculation beyond the ones any scheduler already performs. So the subject of most of its 35 guidelines is not the network — it is the conduct of the analysis over the network: whether the critical path was recomputed per delay, whether prior entitlement was resolved before the delay at issue, whether an after-the-fact edit touched the contractor's own preferential sequencing, whether a compensability conclusion cites a remedy-granting clause.
That is why only three ASCE rules are DETERMINISTIC:
- §4.2 — whether a contemporaneous update chain exists at all;
- §5.1 — whether criticality and longest-path membership diverge in the network as computed;
- §5.3 — whether constrained non-milestone activities are distorting float.
The rest need either something the contract said or something the analyst did,
and neither is in the schedule file. Marking them DETERMINISTIC to raise a
coverage percentage would be worse here than usual: ASCE 67-17 governs on US
federal work by UFGS §3.8.4, and a false green tick there is a false green tick
in front of a board.
2. Which pack runs depends on the jurisdiction
Only rules from an authority that governs appear in the report — not as a pass, not as a skip. A rule from a document that does not bind would imply the document was considered and satisfied.
--jurisdiction |
Precedence | Rules that run |
|---|---|---|
us_federal |
UFGS → ASCE 67-17 → AACE 29R-03 | 120 (48 UFGS + 35 ASCE + 37 AACE 29R-03) |
us_state_or_private |
AACE 29R-03 → ASCE 67-17 | 72 (37 AACE 29R-03 + 35 ASCE) |
neutral |
AACE 29R-03 → ASCE 67-17 → SCL | 107 (37 + 35 + 35) |
uk_scl |
SCL → AACE 29R-03 | 72 (35 SCL + 37 AACE 29R-03) |
Add 61 to any row where the analyst passes --review, which elects the
aace_review pack. Then add, per --contract name given: 26
vdot_progress_schedule, 29 gao_16_89g, 8 aace_84r_13, 11
caltrans_8_1_02c, 5 ndia_paseg. Any --contract name also moves its
document to the head of that row's precedence, ahead of what the jurisdiction
infers, because incorporation by name is itself a term.
ndia_paseg is the exception that proves what the flag means, and it is
recorded here rather than left to surprise a reader of §2's arithmetic: PASEG
states no requirement — shall appears 0 times in every edition this
project holds — so its five rules return only OBSERVATION or NOT_EVALUATED.
They add 5 to the rules that run and 0 to both sides of the score, which
lowers a run's reported coverage. Naming a guide with --contract does not make
it bind; it makes it visible. engine/src/forensic/rules/paseg.py states the
measurement and its editions.
These are the counts a command runs. The ~~295~~ 335 in §1 is the
library, and no
--jurisdiction produces it. The distinction has been got wrong twice on this
page, in both directions — and a third time, in a way neither of those was:
this sentence went on naming 295 while §1's own pack table was corrected to 311,
then 331, then 335. The owner of the count disagreed with itself for as long as
the count kept moving, which is exactly when a reader is most likely to come
here for it. Corrected 6 September 2026, re-derived from the registries.
AACE 29R-03 is also implemented as the taxonomy (forensic.mip), the Source
Validation Protocols (forensic.svp), the prospective TIA procedure
(forensic.tia) and the concurrency doctrines (forensic.concurrency). The
aace29 pack is the clause-by-clause conformance layer over the same document
and does not replace them.
This table previously read 83 / 35 / 35 / 0 and said no AACE 29R-03 or SCL pack
existed. Both packs existed, registered and tested; they were absent from
cli.py's _AUTHORITY_PACKS, so a uk_scl run resolved both of its governing
documents to nothing and printed an empty findings list. The map is now total
over Authority and refuses at import if it is not.
Rules are ordered by precedence, so the report reads top-down in the order the documents bind — the contract's own terms first, then the standard it names.
3. What a rule is allowed to do when it lacks an input
Three things, and never a fourth.
- If the schedule alone settles the clause, decide it.
- If the clause needs a number or a record the schedule does not contain,
report
NOT_EVALUATEDand name the term. No industry default is ever substituted. - If the clause is not a computation at all — "lags must be reasonable" — the
rule assembles the evidence, states the question, and returns
REFERRED.
See READING-A-REPORT.md §3 for what each of those obliges you to do.
Interpretations the UFGS pack takes
Where a clause admits more than one reading, the reading taken is named here rather than buried, so that a reviewer who disagrees can find it.
- Milestone titles ("NTP Acknowledged", "End Project", "Start Phase X") are matched case-insensitively on the stripped name. Rejecting "NTP acknowledged" on its capital A would be a finding about shift-key discipline, not about the schedule.
- §3.3.2's "20 workdays or 30 calendar days" is read as one limit selected by the activity's calendar: 30 where the calendar works all seven days, 20 otherwise. Reading it as "20 and 30" makes the second number unreachable; "whichever is larger" makes the first decorative.
- §3.3.10's "non-work periods identified including weekends and holidays" is
reported as an
OBSERVATIONrather than a failure when no calendar declares a holiday. A six-week job may genuinely span none, and failing it would train the reader to ignore the check. - §3.3.8's "artificial float constraints" cannot be decided by computation:
artificiality is intent. The rule reports constrained activities sitting at
exactly zero float as an
OBSERVATION— the population the prohibition describes — and does not claim they breach it. - §3.3.11's permitted relationship types are read literally: a predecessor tie must bound the activity's start (SS or FS), a successor tie must be driven by its finish (FF or FS). Start-to-finish satisfies neither, which is consistent with §3.3.16.b banning it outright.
- §3.3.4's "long lead" is taken as an activity spanning more than 90 calendar
days; whether such an activity is identified as long lead procurement needs
long_lead_activity_ids. - §3.4.3's "monthly" is one calendar month, clamped to the month's last day — not every 30 days. A 31 January data date followed by 28 February is monthly; counting days would make it a breach.
- §3.6.1 and §3.6.2 "workdays" are counted on the calendar most of the submission's activities use, named in the finding. The contract's definition of a Government workday is not in the schedule, and picking one silently would move a submission across a deadline.
- §3.12.j "verb-noun activity names" has no verb list, because a controlled list of construction verbs would be this tool's invention rather than the specification's, and every name it rejected would be a finding against a word someone here chose. What the rule reports instead is the clause's own test: names that do not distinguish themselves within the first 30 characters.
Interpretations the ASCE pack takes
- §4.7 vs §8.3. ASCE §4.7 states sole cause as the compensability gate and places the apportionment carve-out in Chapter 8. The pack implements §4.7 as a hard gate that a stated apportionment basis relieves, rather than as a rule §8.3 silently overrides.
- §7.1 vs §7.2. Both concern chronology. §7.1 is read as the ordering rule (were events evaluated first-in-time first) and §7.2 as the completeness rule (was every chronologically earlier event resolved before the one at issue). They fail on different facts.
- §10.1 vs §10.3. §10.1 is read as the burden of proof (what showed the contemporaneous schedule to be inaccurate) and §10.3 as the audit record (what changed, and why). A change can carry one without the other.
- §5.2 float ownership. ASCE states a default — float belongs to the project
— that applies absent contract language. The tool cannot know whether such
language exists, so the rule reports
NOT_EVALUATEDrather than asserting the default. The standard's default is a rule about the contract, not a fact about this project.
Citations use the guideline numbers the standard carries (chapter and guideline, e.g. §8.3). ASCE 67-17 does not number the commentary beneath its guidelines, so a rule resting on commentary cites the guideline it sits under and says so in the summary.
Where ASCE and AACE give different answers
Three ASCE guidelines produce answers AACE would not, and on federal work ASCE's answer binds:
- §8.3 — apportion concurrency where possible. AACE detects and quantifies concurrency and stays neutral on the outcome. ASCE states a preference, with §8.2's shared excusable-but-non-compensable result as the fallback taken only when the facts will not support apportionment. A federal analysis that stops at "concurrency exists" has stopped one step early.
- §10.5 — physical, contractual, preferential. AACE's Source Validation Protocols are a named-technique toolkit that goes further than ASCE clearly authorises; ASCE draws a bright line at the contractor's own discretionary sequencing. Technique by technique, the narrower boundary governs.
- §4.4 / §4.1 — measure against the updated schedules. ASCE anchors this to
Sollitt, Blinderman and Fortec; AACE treats single-base static methods as
one design axis among several. Under ASCE, a static single-base method (MIPs
3.1, 3.2, 3.6, 3.8) used while a genuine update chain sat unused is a
credibility problem — reported as an
OBSERVATION, because ASCE flags it and does not forbid it.
4. What the packs do NOT check
This is the section that stops a passing report from meaning more than it does.
UFGS 01 32 01.00 10 — clauses deliberately not implemented
The pack was written against the 103-item clause inventory in
corpus/analysis/RULES.md. Forty-eight are implemented; fifty-five are not.
~~Fifty-four of those carry a stated reason~~ All fifty-five carry a stated
reason — corrected 6 September 2026; see the note under Implemented
elsewhere below — reproduced here. A weaker version
of any of them, shipped quietly under the real clause number, would be the one
defect this package could not survive: a citation a reviewer relies on that does
not check what the clause says.
No record of the thing being checked exists in a submission at all — items
001, 002, 003, 005, 007, 009 (submittal transmittals, scheduler qualifications,
software capability, SEKO minutes); 004, 006 (the exporting P6 version — the
snapshot carries a file name, not a schema version string); 018, 021, 063, 068,
074, 080, 086, 087, 089, 092 (content-completeness and administrative judgments
with nothing in the schedule to assemble); 045 (narrative text is not part of a
submission — UFGS-067 assembles what the narrative would have to address, but
cannot read the narrative); 082 (the TIA base-schedule test needs "last
approved" as a property of a snapshot; the model carries is_baseline, which is
a different fact); 084 (fragnet identity is not carried, so an added sequence
cannot be traced to the fragnet that was approved); 078 (needs the draft and the
resubmission distinguished from each other, and nothing separates them).
The model carries no cost, resource or activity-code data — items 010, 012, 013, 014, 015, 054, 056, 057, 058, 061, 070, 075, 100 (cost loading, CLIN reconciliation, front-end loading, the cost-loading floors); 022 through 030 (the entire SDEF activity coding dictionary — an activity has no codes, and cardinality checks over an absent field would pass every schedule, which is the worst possible result); 093 (activity-code scope, same reason).
The P6 setting is not modelled, and guessing a field name is guessing — item 042 (the percent-complete / remaining-duration linkage is a client preference, not persisted per activity); 094, 095, 096 (calendar scope, duration type, percent-complete type); 103 (the daily ending hour — the calendar carries hours per day, not shift times, so "all calendars end at the same hour" has nothing to compare).
Implemented elsewhere, and duplicating it would double-count the same defect
— item 065 (same predicate as 043); 069, 072 (report artefacts and milestone
presence, covered by 034/035/037/038). Item 073 (§3.5.4.3, "the critical path
is the longest path") belongs in this group: a second enforcement point for
§3.12(f), which runs as UFGS-098, and running both would report one defect
twice under two clause numbers, which reads to a reviewer as two problems.
~~but it is the one unimplemented item the pack does not list a reason for~~
Corrected 6 September 2026. rules/ufgs.py's not-implemented header lists
073 with that reason, in those words. LIMITATIONS.md §9 item 5
recorded the gap as fixed — "the pack's header now says so" — and the
correction had not travelled to this page, so this section went on counting
fifty-four reasons and naming the fifty-fifth as missing.
No period stated in the specification — item 090 (the recovery-schedule submission clause states no deadline; a number here would be invented).
Partially implemented — item 097 (UFGS-097, §3.12.e): the calendar half
(8.0 hours per day) is checked; the P6 Time Period admin preferences
8/40/172/2000 are not in the model. The finding says so, rather than only this
document.
What that means in practice
If your review covers cost loading, SDEF activity coding, resource loading, scheduler qualifications, submittal transmittals, SEKO meeting minutes, narrative content, or P6 admin preferences, the tool has told you nothing about any of them. Those checks remain entirely manual.
ASCE 67-17
All 35 guidelines are implemented as rules. But only two of them can be decided
from a schedule file, so on a run with no --terms file, 30 of the 35 report
NOT_EVALUATED, three are REFERRED to a person, and two are decided.
~~32 of 35 report NOT_EVALUATED~~ — corrected 6 September 2026, measured at 30
on five different files, one of them the cross-validation fixture and four from
the corpus. The three that refer rather than abstain are 7.3, 9.6 and 11.4,
which want a judgment and not a term, so no --terms file moves them; the two
that decide are 5.1 (negative float on the longest path) and 5.3 (constraints
altering the critical path), which are facts about the network.
That is not a partial implementation; it is the standard's subject matter. ASCE 67-17 asks what the analyst did, and the analyst has to say.
Both packs
- The tool checks clauses, not compliance. A schedule can satisfy every runnable clause and still be unbuildable, wrong about the scope, or logically absurd in a way §3.3.1 covers and no computation reaches.
- The tool cannot see the contract. Everything contract-derived arrives via
--terms, and a wrong term produces a confident wrong finding that looks identical to a correct one. - The tool does not read narratives, transmittals, correspondence, submittal logs, daily reports or drawings. Where a rule needs one, it says so and abstains.
5. Supplying contract terms
The three tables in this section are not the whole terms surface, and were
read as though they were until 6 September 2026. cli.py::known_terms accepts
149 keys across the fourteen packs; the tables below name 44 of them —
UFGS 01 32 01.00 10, ANSI/ASCE/CI 67-17 and the Source Validation Protocols,
which is what a US federal conformance run needs plus the SVP. The other hundred
belong to packs reached with --contract or --review and to AACE RP 29R-03,
which runs by default under every jurisdiction: 43 aace. keys, 19 scl.,
2 wisdot. and 41 unnamespaced state-DOT, NAVFAC, Caltrans, GAO, WVDOH and
weather keys. None of them appears on this page.
So the list that cannot go stale is the run itself: every clause that wants a
term names its key in the report's Not evaluated section, and unanswered
groups the same abstentions by the input that would settle them. Read a no-terms
run first and write the file from what it asks for. The tables below tell you
what each key means, which the report does not, and that is what they are for.
--terms FILE.json takes a flat JSON object. Keys are read verbatim. A key no
rule reads ~~is ignored silently~~ is complained about on standard error,
by name, with the nearest real key offered — corrected 6 September 2026,
against a run this page's own sentence would have sent a reviewer past. Measured:
$ construct check ... --terms typo.json # {"contract_completion_dat": ...}
--terms typo.json: no rule reads 'contract_completion_dat' -- did you mean
'contract_completion_date'? It was ignored, and the clause that wants it will
report it as not supplied
The key is still ignored, and the clause that wanted it still reports
NOT_EVALUATED — so the second half of the old sentence holds and the warning is
what tells the two apart. It goes to stderr and the run continues, which means a
reviewer who redirects stdout to a file and reads only the file sees the
NOT_EVALUATED and not the reason for it. A key stated twice in the same
object is refused outright, exit 2, rather than resolved to one of its two
values. cli.py::term_complaints is the implementation.
Dates are written as ISO strings ("2025-06-01") and converted at the edge.
Only unambiguous ten-character ISO dates are converted; a string that merely
looks date-shaped is left as a string, so a term that is genuinely a string
cannot change type without anyone asking.
An empty list is not the same as an absent key. "approved_activity_changes":
[] means "the Contracting Officer approved nothing", which is an answer, and a
reviewer who states it gets a decided rule. Omitting the key means "nobody
said", and gets NOT_EVALUATED.
UFGS 01 32 01.00 10 terms
| Key | Type | Used by | What it is |
|---|---|---|---|
pay_period_thru_date |
date | UFGS-011 §3.2 |
The "Pay Period Thru" date on the payment request |
procurement_activity_ids |
list of activity ids | UFGS-017 §3.3.2 |
Procurement activities, which §3.3.2 exempts from the duration maximum |
long_lead_activity_ids |
list of activity ids | UFGS-019 §3.3.4 |
Which activities the submission identifies as long lead procurement |
mandatory_activity_names |
list of strings | UFGS-020 §3.3.5 |
The titles §3.3.5's fifteen required activity classes take in this project's naming convention, as agreed at the SEKO meeting |
approved_constraint_activity_ids |
list of activity ids | UFGS-033 §3.3.8 |
Activities whose constraint the Contracting Officer approved |
ntp_ack_date |
date | UFGS-034 §3.3.8.1, UFGS-060 §3.4.1, UFGS-062 §3.4.2 |
The date the notice to proceed was acknowledged |
contract_completion_date |
date | UFGS-035 §3.3.8.2, UFGS-059 §3.3.21, UFGS-085 §3.8.6 |
The date the contract requires completion |
interim_completion_dates |
{phase: date}, or records with phase_id/phase and date |
UFGS-036 §3.3.8.3 |
The contractually specified interim completion date for each phase |
qc_daily_reports |
list of records {task_id, date, event} |
UFGS-043 §3.3.12 |
The Contractor Quality Control daily reports. activity_id is accepted in place of task_id |
approved_out_of_sequence_activity_ids |
list of activity ids | UFGS-044 §3.3.13 |
Activities whose out-of-sequence progress was approved |
approved_activity_changes |
list of activity ids | UFGS-046, UFGS-047 §3.3.14 |
Activity additions, deletions and description changes the Contracting Officer approved |
approved_duration_changes |
list of activity ids | UFGS-048 §3.3.15 |
Activities whose original duration change was approved |
preliminary_submission_date |
date | UFGS-060 §3.4.1 |
When the Preliminary Project Schedule was submitted (15-day limit) |
initial_submission_date |
date | UFGS-062 §3.4.2 |
When the Initial Project Schedule was submitted (42-day limit) |
naming_convention_regex |
string (regular expression) | UFGS-066 §3.5.1 |
The file naming convention agreed at the SEKO meeting |
meeting_log |
list of records | UFGS-076, UFGS-077 §3.6.1, UFGS-079 §3.6.2 |
Periodic update meetings. UFGS-076 reads {date, proposed_data_date}; UFGS-077 reads {date, submission_date}; UFGS-079 reads {date, update_submission_date} |
delay_occurrence_date |
date | UFGS-081 §3.8 |
When the delay occurred (10-day justification limit) |
time_extension_request_date |
date | UFGS-081 §3.8 |
When the time extension request was submitted |
recovery_plan_snapshot |
string | UFGS-088 §3.9.1 |
The label of the snapshot submitted as the recovery plan |
ANSI/ASCE/CI 67-17 terms
All ASCE keys are namespaced asce..
| Key | Type | Guidelines |
|---|---|---|
asce.method |
MIP section number or common name | 4.1 |
asce.delay_measure_basis |
"completion_date_change" or "event_duration" |
4.4 |
asce.contract_completion_date |
date | 4.3, 6.2 |
asce.current_adjusted_completion_date |
date — the contract completion date adjusted for prior excusable delay | 4.3, 4.6, 7.2 |
asce.scheduled_completion_date |
date — the schedule's current forecast completion | 4.6, 6.2 |
asce.delay_events |
list of records; see below | 4.5, 4.7, 7.1, 7.2, 7.4, 8.1, 8.2, 8.3, 9.1, 9.2, 9.4, 9.5 |
asce.apportionment_basis |
mapping keyed by event id, or "all" |
4.7, 8.2, 8.3 |
asce.damaged_milestones |
activity ids carrying their own delay damages | 5.4 |
asce.milestone_analyses |
milestone ids for which a separate analysis was performed | 5.4 |
asce.float_ownership |
"project", "owner", "contractor", or the clause text |
5.2 |
asce.early_completion_plan |
mapping with planned_completion_date, owner_reviewed, reviewed_on |
6.1, 6.2 |
asce.time_extension_claimed |
boolean | 6.2 |
asce.remedy_granting_clause |
citation string | 9.3 |
asce.technical_expert_reliance |
records with discipline and question |
9.5 |
asce.legal_review |
mapping with reviewed_by and jurisdiction |
9.6 |
asce.rectifications |
list of records; see below. An empty list means none were made | 10.1, 10.2, 10.3, 10.5 |
asce.schedules_considered |
snapshot labels the analysis actually considered | 10.4 |
asce.abandonment_reasons |
mapping keyed by label — why each dropped snapshot was abandoned rather than corrected | 10.4 |
asce.acceleration |
list of records; see below | 11.1, 11.2, 11.3 |
Note that asce.method is resolved through the MIP taxonomy, so supplying
"TIA" will make the rule report the ambiguity rather than pick a method. Supply
"3.6" or "3.7".
asce.delay_events record keys
| Key | What it is |
|---|---|
id |
An identifier the reviewer can trace to the record |
start, end |
The dates the delay began and ended |
critical |
Whether the delay was critical when it occurred (8.1) |
excusable |
Whether it entitles the contractor to time (4.5) |
compensable |
Whether it entitles the contractor to money (4.5, 4.7) |
responsibility |
"owner", "contractor" or "third_party" (9.1) |
evidence_basis |
How responsibility was established: records, interview, deposition, expert (9.1, 9.4) |
records |
Contemporaneous record references supporting the chronology (7.4) |
notice_given |
Whether notice of delay was given for an excusable event (9.2) |
evaluated |
Whether this event was carried through the entitlement analysis (7.2) |
evaluation_order |
The position at which the analysis evaluated it (7.1) |
apportioned |
Whether concurrency involving this event was apportioned (8.2, 8.3) |
technical_question |
Any non-scheduling technical question the conclusion rests on (9.5) |
An absent key is treated as unstated, never as false. An event with no
critical key has not been found non-critical; it has not been assessed.
asce.rectifications record keys
id, snapshot, target (what was changed — activity, link, calendar or
date), reason, shown_inaccurate_by (the evidence rebutting the presumption
of correctness, per 10.1), error_class ("logic_error", "missing_scope",
"incorrect_duration" or other), constraint_class ("physical",
"contractual", "preferential" or "situational", per 10.5).
asce.acceleration record keys
id, kind ("voluntary", "directed" or "constructive"),
compensation_claimed, agreed_on, started_on, revised_completion_date,
cost_basis, and elements — a mapping of booleans covering the five
constructive-acceleration elements: excusable_delay,
time_extension_requested, owner_denied_or_failed_to_act,
owner_insisted_and_contractor_notified, additional_cost_incurred.
Source Validation Protocol terms
forensic.svp is called from the library rather than from the conformance
command, but it reads terms from the same object.
| Key | What it is |
|---|---|
notice_to_proceed |
date — NTP or the contractually recognised project start (SVP 2.1.B.3) |
contract_milestones |
{activity_id: date} — contract milestone dates (SVP 2.1.B.7) |
baseline_submittal_log |
Evidence that this baseline is the earliest conformed plan, or the documented deviation (SVP 2.1.B.1) |
rectification_log |
The record of changes made to the baseline during validation, with the basis for each (SVP 2.1.B.9) |
contract_scope |
Whether the activity set covers the whole of the contracted work (SVP 2.1.B.6) |
contract_terms |
Whether the schedule breaches any other contract term — phasing, access, sequencing restrictions (SVP 2.1.B.8) |
update_submittal_logused to appear in this table and has been removed from both the table andREQUIRED_TERMS. Nothing reads it: the check that would — distinguishing officially submitted updates from unsubmitted working copies, SVP 2.3.B — is not built. Listing an input nobody consumes is worse than omitting it, because a reviewer supplies the evidence, nothing happens, and they conclude the tool ignored it. See LIMITATIONS.md §9.
A minimal example
{
"ntp_ack_date": "2025-01-06",
"preliminary_submission_date": "2025-01-17",
"initial_submission_date": "2025-02-14",
"contract_completion_date": "2026-11-30",
"pay_period_thru_date": "2025-06-30",
"approved_constraint_activity_ids": [],
"approved_activity_changes": [],
"approved_duration_changes": [],
"asce.method": "3.3",
"asce.float_ownership": "project",
"asce.rectifications": []
}
~~Six of those values are dates~~ Five of those eleven values are dates from
the contract file — recounted 6 September 2026. Four are statements that
nothing was approved, nothing was changed and nothing was rectified — which are
answers, and which decide their rules. The last two, asce.method and
asce.float_ownership, are neither: they state what the analysis did and what
the contract says about float, and no schedule file carries either.
On the sample run in READING-A-REPORT.md §7 — the
three-activity file under --jurisdiction us_federal — that file moves ten
clauses out of NOT_EVALUATED, taking the abstentions from 79 to 69 and the
decided count from 28 to 37. Measured 6 September 2026, on the file that section
describes.
6. Conformance and the Source Validation Protocols are different jobs
Conformance asks does this submission satisfy the specification. SVP asks is this schedule good enough to analyse at all. They are separate, and running conformance rules over an unvalidated baseline verifies the internal consistency of something nobody has checked.
forensic.svp.validate() runs the SVP checks in dependency order — baseline
first, then the update chain, then as-planned/as-built correlation — because an
update chain validated against an unvalidated baseline has verified its own
internal consistency and nothing else.
Of SVP 2.1.B's eleven baseline checklist items, four are decidable from the
file: no progress data at or after the data date (SVP-2.1.B.3-PROGRESS), a
continuous longest-path chain (SVP-2.1.B.4), no open ends (SVP-2.1.B.5), and
the source file's scheduling settings carried through (SVP-2.1.B.11).
~~a data date at or before NTP~~ is not one of them, corrected 6 September
2026 — SVP-2.1.B.3-DATA-DATE abstains with needs
terms['notice_to_proceed'], because NTP is a contract date and no schedule file
carries it. The count of four was right and the list was wrong, which is the
worse of the two: a reviewer reading it would have expected the NTP test to run
on the file alone and would not have gone looking for the term.
The RP's open-end rule is
stricter than DCMA's — every activity except the start and finish milestones
needs a predecessor and a successor, with no tolerance percentage — so it is
computed directly rather than read off a DCMA health report. The remaining seven
need the contract, the submittal log or a person, and are emitted as
NOT_EVALUATED or REFERRED naming exactly what would settle them. Dropping
them would produce a checklist that looks complete and covers four items.
SVP is exposed as python -m forensic validate baseline.xer update-*.xer,
which takes --baseline N and --terms like check does. The library API is
the same procedure over the same checks, but ~~takes the same arguments~~
takes one already-built Submission — forensic.svp.validate(submission),
one parameter — and the subcommand's work is reading the files, choosing the
baseline and coercing the terms into one. Corrected 6 September 2026.
Source: docs/CONFORMANCE.md. Source commit date: 2026-09-06.