Reading a report
The report is ordered the way it is on purpose, and the ordering cannot be changed. This document explains what each part is claiming, and — for two sections in particular — what it obliges you to do next.
1. Why the assumptions come before the findings
Every Construct report emits, in this fixed order:
- Method
- Governing documents (and where they disagree)
- Choices that could have gone the other way
- Assumptions
- Limitations
- Principles relied on
- Findings
There is no parameter to reorder it. A report that opens with its conclusion and buries its assumptions in an appendix has the emphasis exactly backwards for its purpose: the assumptions are what the other side will attack, and burying them reads as concealment even where nothing was concealed.
The commercial argument is the same as the evidentiary one. AACE RP 29R-03 §1.1 states that the practice exists to eliminate "black box" analyses, and §1.2(d) states as a premise that every method is subject to manipulation through analyst judgment. A report presenting any method as mechanical is misrepresenting the standard it cites.
The reporting layer computes nothing. Every value it prints was decided elsewhere and passed in. That is deliberate: a reporting layer able to compute would be able to disagree with the analysis it reports, and the first time those two diverge the report is worthless.
2. Three things that look similar and are not
Disclosure — a choice that could have gone the other way
A disclosure is not any parameter. A report listing four hundred settings is a black box with an appendix: the reader cannot tell which of them mattered, so they check none. A disclosure meets all three of:
- It could have gone the other way. Retained logic or progress override is a choice. The number of days in a week is not.
- The other way gives a different answer. If both settings produce the same dates on this schedule, the choice did not matter here, and saying so is more useful than listing it.
- A published document leaves it open, or two documents disagree. Then the citation goes in the report, because the reader's objection is not "you chose wrongly" but "you chose without saying" — and a citation answers it.
Each disclosure carries what was chosen, the alternative, the reason, the source, and — where it was computed — what the alternative would have given. Naming the alternative's result is what turns a disclosure from an admission into evidence of diligence.
Disclosures are split into material and immaterial. Immaterial ones are listed in one line each, "recorded so the reader need not verify that".
Read the materiality claim carefully in command-line reports. The
python -m forensiccommand marks every disclosure material without establishing it, because computing materiality would mean rescheduling every file under every combination of settings — which is the analysis rather than a footnote to it. The rendered heading therefore says "N of N choices below would change the result on this schedule" when what the tool actually established is "N choices were made and none was tested for materiality". Treat the list as complete rather than as filtered. See LIMITATIONS.md §9.
Assumption — a gap in the evidence, filled in order to proceed
An assumption is not a choice between two defensible readings. It is something taken as true that was not established. Conflating the two understates the gaps: a reader skimming a list of "methodological choices" will not register that one of them means "we did not have the baseline".
Each assumption carries: what was assumed, why the analysis could not establish it, what would resolve it, and what it affects. An assumption with no route to resolution is not an assumption — it is a limitation, and belongs in the other list.
Limitation — something the analysis cannot do at all
Stated before anyone asks. Each carries what it is, the consequence, and — where anything would — what would remove it.
One limitation applies to every report this engine produces today: "This engine's results are not validated against Primavera P6." Stating it costs a paragraph. Having it discovered in cross-examination costs the analysis.
3. DETERMINISTIC, HYBRID and JUDGMENT
Every conformance finding carries a determinism class. It comes from the AACE
implementation survey's own classification of the procedures in RP 29R-03, and
it is the most useful thing in that document for anyone building software. It is
a required field on every rule, and ~~the report prints it~~ --json carries
it on every finding; the Markdown and HTML reports do not print it at all —
corrected 6 September 2026, by grepping both outputs of one run for all three
class names and finding none of them outside ordinary prose.
What those two forms print per finding instead is the other partition,
dependence: from your file or from our arithmetic, which says whether the
verdict rests on this engine's CPM calculation. The two are different questions
and neither substitutes for the other. Until the class is printed, the way to
read this section against a Markdown report is by grouping: REFERRED is a
JUDGMENT rule and nothing else is, and an abstention naming a terms[...] key is
a HYBRID rule saying which term it wanted. DETERMINISTIC and HYBRID are not
distinguishable in the Markdown at all where both decided, and that is a gap in
the report rather than in the model.
DETERMINISTIC — the tool decided
The rule computes its own answer from the schedule. "Every activity except the first and last has a predecessor and a successor" is a fact about a graph.
What you do differently: check the evidence list, not the verdict. The verdict is arithmetic. If you disagree with it, you disagree with the parse or with the clause reading, not with the computation.
HYBRID — the tool decided, given an input you supplied
The rule computes an answer given something the schedule does not contain. "No activity exceeds the maximum duration" needs the specification's maximum.
What you do differently: check that the term supplied was the right one.
A HYBRID PASS is only as good as the number you fed it. If
contract_completion_date was wrong by a week, every rule that reads it is
wrong by a week, and the finding will look identical either way.
When the term was not supplied the rule does not pass and does not fail. It
reports NOT_EVALUATED naming the key. No industry default is ever
substituted — a default silently applied is a specification the reviewer never
agreed to, and it is worse here than elsewhere because the report looks
identical either way.
There is one pattern that recovers many HYBRID rules without any term at all.
Many clauses read "X is prohibited unless the Contracting Officer approved
it." Without the approval log such a rule looks undecidable — but when the
schedule contains no X at all, there is nothing to approve, and the rule is
decided. So these rules PASS on a clean schedule and report NOT_EVALUATED,
with the offending items already named as evidence, on a dirty one. You are then
reading "these three constrained activities need an approval record", which is
the actual work item, rather than "could not evaluate".
JUDGMENT — a person decides, and it is you
The rule cannot be decided by computation at all. "The logic is reasonable" is not a computation.
A JUDGMENT rule cannot return PASS. That is refused structurally, not by
convention. A Finding that claims a JUDGMENT PASS raises where it is built —
but Finding.determinism is written by the rule author, so the constructor
alone would catch only an author who labelled the finding honestly. run()
therefore rebuilds every finding with the determinism the rule was registered
under, so the refusal is against the registration. A rule registered JUDGMENT
that labels its finding DETERMINISTIC loses its verdict to NOT_EVALUATED
naming the reason; it cannot print a green tick. A green tick against "the logic is
reasonable" is a lie told in the one document whose reader is entitled to assume
otherwise, and a rule pack that reclassified a judgment rule to raise its
automation percentage would make the tool untrustworthy in exactly the setting
where trust is the entire product.
A JUDGMENT rule returns REFERRED, and a REFERRED finding must state a
question — that too is enforced at construction.
What you do differently: answer the question. That is the whole point. The tool's job on a judgment clause is to turn "consider whether the lags are reasonable" into "consider these four lags, which total 46 days across the driving path" — which is most of the reviewer's work, and the part software can actually do. A referred finding that names no activities has done nothing.
4. The five verdicts
| Verdict | Meaning | Counts toward score? |
|---|---|---|
PASS |
The rule ran and the schedule satisfied the clause | Yes |
FAIL |
The rule ran and the schedule breached the clause | Yes |
OBSERVATION |
The rule ran and found something worth looking at, which the specification does not make a failure | No |
NOT_EVALUATED |
The rule could not run. missing says what would let it |
No |
REFERRED |
JUDGMENT rules only: the evidence is assembled, the question stated, and a person must answer it | No |
OBSERVATION exists because a report that cannot separate "breaches the spec"
from "is worth a look" trains its reader to ignore both.
Severity is orthogonal to verdict. A rule can fail on a trivial point and
pass on a load-bearing one, so ranking a report by verdict alone puts a missing
activity code above a schedule with no critical path. The four levels are
CRITICAL, MAJOR, MINOR, INFO, and blocking means a FAIL at CRITICAL
or MAJOR — the failures a reviewer would reject the submission over.
A rule that crashes is reported as a NOT_EVALUATED finding naming the
exception, at MAJOR severity, and does not abort the run. A reviewer with 40 of
41 rules and one named crash is better served than one with a traceback, and the
crash stays visible in the report rather than being swallowed. A rule that yields
nothing is likewise reported as NOT_EVALUATED: a silent rule is
indistinguishable from a rule that crashed.
5. Score and coverage are two different statements
score = passed / (passed + failed)
coverage = (passed + failed) / total findings
Not-evaluated, referred and observation findings are excluded from the score's numerator and denominator, and coverage tells you how much the score is worth.
Abstaining cannot raise the score. An abstention is removed from the fraction entirely — it is in neither the numerator nor the denominator — so a schedule does not become more conformant because a check could not be performed on it. What an abstention does instead is lower coverage, which is why the two numbers are always printed together and neither is fit to quote alone.
An earlier version of this page said "skipped checks leave the score's denominator", which contradicted the correct sentence three lines above it and described arithmetic nothing performs. Leaving them in the denominator would mean scoring them as failures, which would be a different lie.
Compare two reports on the same 41-clause pack:
| 38/41, coverage 100% | 38/41, coverage 60% | |
|---|---|---|
| Clauses decided | 41 | 25 |
| Clauses that could not be decided | 0 | 16 |
| What the 38 means | 38 of the 41 clauses that govern this submission were satisfied | 38 of the 25 clauses the tool could reach were satisfied — and 16 clauses were never assessed at all |
| What you can say to a Contracting Officer | "This submission was checked against the whole section and fails three clauses." | "This submission was checked against three-fifths of the section. Three of the clauses reached fail. Nothing is known about the rest." |
The second report is a narrower claim, not a weaker version of the first. Its score is a statement about a subset you did not choose — the subset happens to be "whichever clauses need no contract terms", which is systematically the easier clauses. Reading it as 93% conformant is the single most common way to misuse this tool.
When nothing at all could be decided, score is None, not 0.0 and not
1.0. Both of those are claims. "We could not assess this schedule" is the
truth, and it needs its own value. The report prints "No clause could be
decided on the material supplied. The score is not zero; there is no score."
Confidence
Each findings section carries a confidence: computed,
computed_unvalidated, assumed, or judgment. The report prints the
weakest one across all its sections, because a conclusion is only as strong
as the weakest step supporting it — a summary must not present a chain of
reasoning as computed when one link in it was judgment.
~~The command-line conformance section labels itself
computed_unvalidatedwhenever coverage is below 100%. Read that as "coverage is incomplete", not as "the inputs were unvalidated".~~Corrected 6 September 2026, and the correction had been sitting in LIMITATIONS.md §9 item 4 — which this note already cited — since the code was fixed. The label is set unconditionally, and it means exactly what the enum says: computed from inputs that could not be validated, which is true of every run of
checkregardless of coverage, becausecheckdoes not perform the Source Validation Protocols. Measured: a run restricted to one deterministic clause with--rule ASCE-67-17-5.1reports coverage1.0and is still labelledcomputed_unvalidated. Tying it to coverage is the defect that was removed — it labelled a fully-covered run as validated when nothing had been validated. Runvalidatefor the thing this label says was not done.
6. What NOT_EVALUATED obliges you to do
It obliges you to do the check yourself, or to supply the input. It does not mean the clause passed, and it does not mean the clause is unimportant. It means the tool refused to guess.
Each NOT_EVALUATED finding names exactly what would let it run — every one of
the 79 abstentions in the §7 sample run carries a non-empty missing. What it
names falls into three families. ~~There are three shapes~~ — corrected
6 September 2026: the sentence read as three fixed strings, and one no-terms run
across the thirteen packs emits forty distinct wordings outside family 1.
Match on what the line asks for, not on its exact words.
- A contract term. Usually
needs terms['some_key'] -- what it is. Supply it in the--termsJSON file and rerun. The ASCE pack writes the bare key instead —needs asce.delay_events, with noterms[...]wrapper and no explanation — so searching a report forterms[misses fourteen of them. CONFORMANCE.md §5 explains what each key means for three of the thirteen packs; the run itself is the only complete list, andunansweredgroups it by the input rather than by the clause. - More schedule material. The commonest is
needs a prior schedule submission to compare against— pass more files, oldest first. Also in this family:needs a snapshot marked as the baseline (Snapshot.is_baseline), which wants--baseline Nand not another file;needs two calculated schedules,needs at least three schedule versions, andneeds in-progress activities, which want a schedule that has actually been statused. The sentence after the dash says which. - A defect in the tool.
needs a finding from the rule; a silent rule cannot be scoredorneeds a fix to the rule implementation— that is not in the schedule. Report it. These are the only two fixed strings in the list, because they are written once inrules.runrather than by each rule.
A report where a third of the clauses are NOT_EVALUATED is the correct
output for a submission whose contract terms were never provided. Printing a
confident score instead would be the single most damaging thing this tool could
do. But it is a work item, not a result: for each one, either fill the term or
perform the check by hand and record that you did.
7. A worked example, annotated
This is a real run against a three-activity file, under --jurisdiction
us_federal with no --terms and no --baseline. It is deliberately a
bad case, because a bad case is what you need to be able to read.
python -m forensic sample.xer --jurisdiction us_federal
Method
## Method
No method has been declared for this analysis. Under AACE RP 29R-03 the
method is itself an argument to be made and defended, so this report
cannot be presented as a delay analysis until one is stated.
Annotation. This is a conformance review, not a delay analysis, so no MIP was declared. The report says so rather than leaving the section blank. If you see this heading on something presented as a delay analysis, that analysis has not named its method and cannot be defended under 29R-03.
Governing documents
1. UFGS 01 32 01.00 10
2. ANSI/ASCE/CI 67-17
3. AACE RP 29R-03
### Where the governing documents disagree
**which standard governs the analysis on US federal work**
- Followed, per ANSI/ASCE/CI 67-17: UFGS 01 32 01.00 10 §3.8.4 makes
ANSI/ASCE/CI 67-17 the governing standard for schedule delay analysis on work
adopting the section
- AACE RP 29R-03 instead holds: AACE 29R-03 is the more widely cited taxonomy and
is frequently applied by default, including on federal work where it does not
govern
Annotation. No
assumedwarning here, because--jurisdiction us_federalwas stated. Had it been omitted, this section would open with "No governing document was established for this project."The conflict block is the reason this section exists. If the expert on the other side has run an AACE 29R-03 analysis on federal work, this is the paragraph you cite.
Choices that could have gone the other way
3 of 3 choices below would change the result on this schedule.
### Out-of-sequence progress
- **Chosen:** retained_logic
- **Alternative:** the other two progress modes
- **Reason:** read from the schedule as computed, not from the covering letter
- **Source:** UFGS 01 32 01.00 10 §3.3.17
### Lag calendar
- **Chosen:** predecessor
...
### Data date convention
- **Chosen:** through
- **Source:** AACE RP 10S-90
Annotation. Three settings, each of which moves dates. The progress mode was read from the file's own recorded scheduling options, not chosen by the tool — which is the only correct behaviour for a conformance check, because the question is what the submitter scheduled with. See CONCEPTS.md §4 and §3 for what each one does.
The "3 of 3 would change the result" heading overstates what was established — see §2 above.
Assumptions
### No schedule was identified as the contract baseline
- **Why assumed:** --baseline was not given, and the earliest file is often a
superseded plan
- **Resolved by:** the executed baseline acceptance, and --baseline naming that file
- **Affects:** ASCE-67-17-10.1, ASCE-67-17-10.2, … ASCE-67-17-4.5
### No contract terms were supplied
- **Why assumed:** --terms was not given
- **Resolved by:** a JSON file of the thresholds the specification states
- **Affects:** 47 rules that could not be decided
Annotation. Both are work items. The tool did not promote the earliest file to baseline — which is a contractual fact, not a modelling one, and the earliest file is very often a superseded plan. The
Affectsline names the rules that went undecided as a result, so you can see the cost of not supplying it.
Limitations
- **This engine's results are not validated against Primavera P6** — The
scheduling logic is internally consistent, deterministic and property-checked,
but no held-out corpus pairs real schedules with P6's own computed answers, so
agreement with P6 has not been measured. Figures here should be reconciled
against the source software before they are relied on
- Removed by: a validation corpus of real XER files carrying P6's stored
computed dates and floats
Annotation, and the excerpt above is stale. A limitation like this appears on every report and must not be removed from a deliverable — but its text has moved, and in the direction that matters. The version above says "agreement with P6 has not been measured". It has been: the report now names the corpus figure, interpolated from
disclosure.P6_AGREEMENTrather than typed, and states which four movements produced it. A reader who trusted this page's excerpt would quote a report we no longer print. See LIMITATIONS.md.
Findings
Stale as of 2026-09-02, and not repaired here. The excerpt below predates three changes to the report: the score and its coverage are now one sentence (they were separable, and a score that quotes without its coverage is
DEPOSITION.mdattack 8); the completeness assurance says "read" rather than "loaded and cross-checked", because there is nothing to cross-check one file against; and the verdict-class labels are####headings carrying their own counts rather than bold paragraphs. The annotations under each block are readings of particular figures, so re-deriving the digits without rewriting the readings would produce the failureCLAUDE.mdrecords under "Derive the number, but never the sentence about it".demo/outputs/is generated from a real run on every commit and is the live example until somebody reads this page through.
The weakest step in the reasoning below is *computed_unvalidated*. No conclusion
drawn from it is stronger than that.
### Schedules considered
- **XV01** — data date 2026-01-05, 3 activities, 2 relationships
Every file supplied was loaded and cross-checked, including any the chosen
method does not consume (AACE RP 29R-03 §1.5.B principle 7).
### Specification conformance
**22 passed, 4 failed** of 26 clauses that could be decided — 85%.
Coverage: 31% of 83 applicable clauses were decided. 47 could not run and
6 need a person.
Annotation — the two most important numbers in the report.
85% is a statement about 26 clauses. It is not a statement about the submission. Fifty-three of the eighty-three applicable clauses were never reached.
Eighty-three was UFGS (48 rules) plus ASCE 67-17 (35 guidelines), because
us_federalput both authorities in the precedence when this transcript was captured. It is 120 today — AACE 29R-03's 37 rules were registered, tested and wired to nothing until the authority map was made total, anduk_sclwent from 0 rules to 72 in the same fix. The reading above is unchanged and the arithmetic under it is not: change the jurisdiction and the denominator changes — see CONFORMANCE.md §2 for the current four.The correct summary of this report is: "Of the clauses that can be checked without contract terms or a comparison schedule, four fail — two of them blocking. Nothing has been established about the other 53."
**Blocking failures**
- `UFGS-034` §3.3.8.1 — the schedule contains 0 activities titled
'NTP Acknowledged'; §3.3.8.1 requires exactly one
- `UFGS-035` §3.3.8.2 — the schedule contains 0 activities titled 'End Project';
§3.3.8.2 requires exactly one
- `UFGS-041` §3.3.11 — 2 open-ended logic defects under §3.3.11
- 9200 '01-fs-chain A' opens the schedule; §3.3.11 requires that to be
'NTP Acknowledged'
- 9202 '01-fs-chain C' closes the schedule; §3.3.11 requires that to be
'End Project'
- `UFGS-098` §3.12.f — the file does not define critical activities as the
longest path; §3.12.f requires Longest Path rather than total float
- sched_open_critical_flag = N
- sched_float_type = FT_FF
Annotation. Every failure names the offending items. A finding that says "17 activities have no predecessor" without naming them is a number you have to reproduce by hand, which is the work the tool was supposed to remove. Evidence lists are truncated at 50 items with an exact "…and N more" tail — the truncation is lossy, the count is not.
UFGS-098is worth reading closely: the tool readsched_float_typeout of the file's own scheduling options. This is a finding about how the submitter configured P6, not about how the tool computed anything.
**Referred — the tool assembled the evidence and will not conclude**
- `UFGS-016` §3.3.1 — Is this level of detail sufficient for the work, per
§3.3.1? The clause gives an open list of characteristics and no threshold, so
the officer's judgment cannot be pre-empted -- only supplied with the figures.
- activities: 3 (3 work, 0 milestones)
- relationships: 2
- median original duration: 3 days
- longest: 9200 '01-fs-chain A' 5d
Annotation. This is what a JUDGMENT rule looks like when it is working. It has not passed, it has not failed, and it will never do either. It has handed you the figures the clause is about and asked the question in the clause's own terms. Your answer goes in your review; the tool records that it did not answer it.
**Not evaluated — one input away from deciding**
- `UFGS-011` §3.2 — needs terms['pay_period_thru_date'] -- the 'Pay Period Thru'
date on the payment request
- `UFGS-046` §3.3.14 — needs a prior schedule submission to compare against
- `UFGS-060` §3.4.1 — needs terms['ntp_ack_date'];
terms['preliminary_submission_date']
- `ASCE-67-17-8.1` §8.1; cf. AACE RP 29R-03 §4.1 — needs asce.delay_events
…
Annotation. This is your to-do list, and it is the most useful part of a first-pass report. Three of the four shapes appear here: a missing contract term, a missing comparison schedule, and a missing analysis input.
UFGS-011— the data-date-to-pay-period check — is one JSON line away from deciding, and it is the clause that ties schedule conformance to the payment the specification conditions on it.
8. What a passing report does not mean
A passing conformance report means the clauses this tool could reach, on the material you supplied, were satisfied. It does not mean the schedule complies with the specification. CONFORMANCE.md §4 lists, by clause number, what the packs do not check and why.
9. One snapshot and two: the same project, measured
§6 tells you to pass more files. This is what the second file is worth, measured on one project rather than asserted.
harbour-point-24.12 is a public P6 export held in the oracle corpus as a
baseline and an as-built of the same project (58 and 59 activities;
engine/oracle/corpus/MANIFEST.json carries the commit and digest). Both runs
below are on the same engine, the same day, with no --terms and no
--jurisdiction.
validate |
check |
|
|---|---|---|
| baseline alone | 0 passed, 0 failed of 3 checks | 10 passed, 1 failed of 11 decidable clauses — 10% of 107 |
baseline + as-built, --baseline 0 |
3 passed, 1 failed of 16 checks | 14 passed, 2 failed of 16 decidable clauses — 15% of 109 |
The second snapshot does not improve the score; it changes how many checks exist at all. Source validation goes from three checks of which none could be evaluated, to sixteen of which four decided. That is structural: every protocol under AACE RP 29R-03 §2.3.B compares one dated snapshot against another, so with one file there is nothing for them to read, and they correctly report NOT_EVALUATED rather than passing vacantly.
What the pair produced that the single file could not:
SVP-2.3.B-PATH— "HP-DCP03-AB reroutes the longest path from HP-DCP03-BL: 32 activities dropped, 1 picked up, first difference at position 0."SVP-2.2.D.1-BUILT-ONLY— activity6058(Substantial Completion & Taking Over) present in the as-built and not the baseline, with the closest name on the other side offered as6057, referred for a person to classify as added detail, changed scope, or a baseline omission.
--baseline 0 is not optional to this. Without it the earliest file is not
assumed to be the baseline — §2.1.B's protocols are the ones that validate the
baseline, so assuming which file it is would decide the question they exist to
ask.
For a first run, hand it two dated snapshots and name the baseline. A single file yields an honest report with the source-validation section empty, and the emptiness is the correct answer to what was supplied, not a defect to work around.
10. Reading the HTML report with a screen reader
The HTML report was built with accessibility in mind and, until now, none of it
was tested. engine/tests/test_html.py covered tag balance, one h1 with no
skipped levels, the provenance dl, an ol for the governing documents, and a
nested list inside its parent li. engine/tests/test_report_accessibility.py
adds the rest, each check written as a predicate that a deliberately broken
control document must trip, so a check that cannot fire is visible as one.
What is now tested, and the mutation each was watched to fail against:
| Property | Mutation that fails it |
|---|---|
One <main>, all content inside it |
remove the wrapper, or emit the h1 before it |
The ol items are the precedence the engine computed, in order, for every jurisdiction |
reverse governance.precedence |
| Nothing tabular arrives as a layout table or as a line of pipes | put a Markdown table in a section body |
| No verdict carried by a glyph, and the stylesheet declares no colour | render a pass as a tick; add color: red |
Source order is reading order — no float, position, order, direction |
add float: right |
The reader's zoom and font size win — no px/pt font size, no locked viewport |
font-size: 12px; user-scalable=no |
| Every finding names its clause and says what was found, in its own text | render a finding as its id and verdict alone |
| The report contains no links (a tripwire: the first one added forces a review of its link text) | add an <a> |
What could not be honestly tested. Whether the headings describe the
structure is a reading, not a property — a test for it would assert that
headings exist, which they do, and pass for the wrong reason. Whether a <th>
carries the right scope cannot be tested on a document that contains no
tables; the check is written so that the day one appears it must have headers,
but today it is the control document, not the report, that exercises it.
Four defects the checks above found and do not yet fail on, because fixing them changes the renderer:
**Blocking failures**,**Referred ...**and**Not evaluated ...**are emitted as<p><strong>...</strong></p>— bold paragraphs, noth4. They are the only signposts inside the conformance section, so a listener navigating by heading arrives at "Specification conformance" and then has no way to reach the failures except by reading everything between. The same applies to the conflict subject line under "Where the governing documents disagree".- Emphasis inside a line is not converted:
*from your file*and*from our arithmetic*reach the document as literal asterisks.html.py's_EMmatches only a line that is entirely emphasised. - The confidence marker renders as
<p><em>computed</em></p>— one italicised word with no label. Read aloud after a heading it is the word "computed" and nothing else; it needs to say what is computed, e.g. "Confidence: computed". <dl aria-label="Report provenance">is the only labelled region in the document. That is right, and the point is the contrast: every other block is reachable only through the heading above it.
None of these makes the report unusable. A screen-reader user can open it, hear
the title, walk the h2s, hear the provenance as label-and-value pairs, hear
the governing documents numbered in precedence order, and read any finding as a
self-contained sentence naming its rule id, its clause and what was found. The
defects are navigation cost inside the findings section, not lost information.
Source: docs/READING-A-REPORT.md. Source commit date: 2026-09-06.