feat(launchpad): verdict contract in code (#118 STEP 2) - #261
Conversation
Expresses ADJUDICATION.md's six added finding fields and nine adjudication-block keys as verdicts.py's Verdict/Adjudication dataclasses and a validate(input_document, output_document) that reports every violation rather than stopping at the first. SEVERITY_ORDER is imported from review.py, never redeclared, and validate re-runs #117's own findings.validate against the output so a document that breaks that contract on the way out is caught too. Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…es (#118 STEP 2) Independent review found five crash sites where a malformed finding or adjudication-block value of the wrong type (a list or dict where a string was expected -- verdict, severity, reported_severity, duplicate_of, downgrades[].finding_id, duplicate_groups[].duplicates[]) raised TypeError: unhashable type instead of returning a violation string, breaking validate()'s own "never raises" contract. Adds the same isinstance-before-membership-test guard findings.py already uses for this exact class of input. All 17 existing tests still pass unchanged. Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…cks (#118 STEP 2) review-code found that 3 of the adjudication block's 9 keys (schema_version, verdict_counts, notes) are never checked by validate(), though its docstring claimed "every violation of ADJUDICATION.md". Per Serina's call, this is intentionally deferred to STEP 10's separate check_adjudication.py control suite (the plan's own STEP 1 text assigns "one control per key" there, and STEP 2's done-when never names these three) -- so the fix is to state the boundary explicitly rather than extend validate()'s scope. Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…EP 3) Reads one #117 merged document on stdin, adjudicates every finding with an injected judge callable (--judge stub default, --replay <dir> for STEP 9's future recordings), and prints one document on stdout. Input is validated with #117's own findings.validate before a single finding is adjudicated, pr/merge_base_sha/head_sha/containment pass through byte-identical, and anchor "pr" (file and line null) is adjudicated without raising. Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
review-code found that run_adjudication.py only caught JSONDecodeError, so syntactically valid JSON whose top level isn't an object ([], "x", 42) parsed successfully and crashed downstream with an unhandled AttributeError/TypeError inside findings.validate, which assumes a dict. Reachable directly through this CLI's untrusted stdin -- the only caller that hands arbitrary input to findings.validate. Fixed by checking isinstance(input_document, dict) right after json.loads succeeds, refusing cleanly (matching the sibling refusal paths) before that assumption is ever exercised. Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Review pipeline — PR #261Stages run: Not applicable, declared rather than faked: I ran the two mechanical clauses of STEP 2's done-when myself, in a clean worktree at The Everything below came from probing the validator with mutated copies of the suite's own Findings1. High — whitespace-only
|
Correction to finding 2 above — reachability, not the finding itselfReviewing #267 (STEP 7) put the producer side of What stands: What I got wrong: my failure scenario said the runner might emit such a group "after a dedupe pass that grouped nothing". It cannot. if len(candidate_ids) < 2:
continueI probed that too — a dedupe judge asked to group a finding with itself, and one returning the same pair twice, both yield a correct single group or none, never an empty one. So the corrected reading: this is a validator-only gap, reachable from a hand-written or forged document, or from STEP 10's planned "feed every field malformed" controls — not from the current producer. That makes it materially less urgent than I framed it. I would still fix it, because catching what a producer might do wrong is the validator's whole job and STEP 10 is going to feed it exactly this, but it is not a live path. Findings 1 and 3 are unaffected. Finding 1 in particular I re-verified end-to-end through Revised severity for finding 2 only: Medium → Low. This block supersedes the one in my earlier comment. Ranking is unchanged apart from finding 2 dropping below finding 3. ADJUDICATION COMPLETE REVIEW COMPLETE |
ciaran-slow
left a comment
There was a problem hiding this comment.
Comment review recording the pipeline result. Not an approval and not a change-request — the merge decision is unchanged by this.
Reviewed via the full pipeline — detail in my comment on this PR. I ran STEP 2's two mechanical clauses myself: python3 -c "import verdicts" resolves, verdicts.SEVERITY_ORDER is review.SEVERITY_ORDER is True, and Ran 17 tests ... OK.
The validator is good work — it returns every violation rather than raising on the first, takes both documents for the reason the docstring reconstructs correctly, and checks all four of the plan's both-directions rules in both directions.
One High finding — must be fixed before this merges.
verdicts.py:268 — whitespace-only verdict_evidence satisfies "non-empty". if not finding.get("verdict_evidence") is a falsiness test and not " " is False. Probed: setting a finding's evidence to " " yields 0 violations. The same shape sits at run_adjudication.py:516 (#263), where the docstring promises to fail closed on "empty verdict_evidence" and does not — so a judge returning {"verdict": "CONFIRMED", "verdict_evidence": "\n"} publishes a CONFIRMED verdict with blank evidence, and a CONFIRMED Blocker is what blocks a merge. Fix at both ends: test the stripped value.
Also for the author, non-blocking:
- Medium,
:322/:324—isinstance(True, int)isTrue, sofindings_in: truewith everyfindings_count: truevalidates with 0 violations.total_refutationnext to it uses a strictisinstance(..., bool), which shows strictness was intended. Useisinstance(x, int) and not isinstance(x, bool). - Low,
:425— aduplicate_groupsentry with an emptyduplicateslist never has itssurvivorvalidated at all. Downgraded from Medium after I confirmed on #267 that_build_duplicate_groupscannot emit that shape — so it is validator-only, reachable from a forged document or STEP 10's planned malformed-field controls. Correction posted on this PR.
I checked four other candidates and refuted all four — missing nonce (caught upstream by findings.validate), containment findings in total_refutation, keys[-1] on an empty mapping, and self-duplication. Recorded in my comment rather than dropped.
ciaran-slow
left a comment
There was a problem hiding this comment.
Change-request review. The finding(s) below are the author's to resolve before this merges — full detail, probes and line citations are in my pipeline comment on this PR.
Reviewed via the full pipeline — detail in my comment on this PR. I ran STEP 2's two mechanical clauses myself: python3 -c "import verdicts" resolves, verdicts.SEVERITY_ORDER is review.SEVERITY_ORDER is True, and Ran 17 tests ... OK.
The validator is good work — it returns every violation rather than raising on the first, takes both documents for the reason the docstring reconstructs correctly, and checks all four of the plan's both-directions rules in both directions.
One High finding — must be fixed before this merges.
verdicts.py:268 — whitespace-only verdict_evidence satisfies "non-empty". if not finding.get("verdict_evidence") is a falsiness test and not " " is False. Probed: setting a finding's evidence to " " yields 0 violations. The same shape sits at run_adjudication.py:516 (#263), where the docstring promises to fail closed on "empty verdict_evidence" and does not — so a judge returning {"verdict": "CONFIRMED", "verdict_evidence": "\n"} publishes a CONFIRMED verdict with blank evidence, and a CONFIRMED Blocker is what blocks a merge. Fix at both ends: test the stripped value.
Also for the author, non-blocking:
- Medium,
:322/:324—isinstance(True, int)isTrue, sofindings_in: truewith everyfindings_count: truevalidates with 0 violations.total_refutationnext to it uses a strictisinstance(..., bool), which shows strictness was intended. Useisinstance(x, int) and not isinstance(x, bool). - Low,
:425— aduplicate_groupsentry with an emptyduplicateslist never has itssurvivorvalidated at all. Downgraded from Medium after I confirmed on #267 that_build_duplicate_groupscannot emit that shape — so it is validator-only, reachable from a forged document or STEP 10's planned malformed-field controls. Correction posted on this PR.
I checked four other candidates and refuted all four — missing nonce (caught upstream by findings.validate), containment findings in total_refutation, keys[-1] on an empty mapping, and self-duplication. Recorded in my comment rather than dropped.
Independent adjudication of the change-request findingsAdjudicated in a fresh context, separate from the pipeline that produced the findings. That matters here because the review states its own limitation plainly — one context ran the reviewers, the adjudicator and the final pass, so its findings were self-adjudicated. This pass supplies the independent adjudication that was missing. It does not supply an independent second reviewer, so anything both passes missed is still missed. Head is unchanged since the review ( The three mechanical claims verified: Findings1. The hole is wider than reported. There is no type check at all, so every truthy value passes: The empty-string control firing is what proves the check exists and that everything else slips past it specifically. This also removes the one available defence — that Not documented: High rather than Blocker: nothing on this branch calls 2. Reproduced: One narrowing the review did not mention: Medium because the producer cannot emit it ( 3. Three shapes validate clean, including a missing The reviewer's own Medium -> Low downgrade was re-verified rather than accepted: on the chain tip, Where the fix belongs, and one row un-mergedThe review merged
The primary fix belongs here, on the lowest branch in the chain — Two notes for the reviewer harness
Verdict
Confirmed 3, refuted 0, resolved 1 (the The 🤖 Adjudicated by Claude Code ( |
…EP 2)
`validate` tested several required fields with Python truthiness, which is
the wrong check twice over: `not " "` is False, so whitespace satisfied
"non-empty", and there was no type check at all, so any truthy value passed.
Probed on the well-formed baseline, `verdict_evidence` set to each of
' ', '\n', '\t', '\xa0', 42, True, 0.5, ['x'] and {'a': 1} all yielded
zero violations; only '' was caught.
ADJUDICATION.md gives the reason the check exists: "An UNPROVEN with no
reason is indistinguishable from a stage that skipped the finding."
Whitespace is no reason, and `verdict_evidence: 42` is not actionable under
any reading -- so a CONFIRMED Blocker could carry a blank justification and
still validate clean. `verdict` one field above already does
`not isinstance(verdict, str)`, which is this module showing the strictness
it intended.
Four sites, two new helpers:
- `verdict_evidence` -- reported on #261 as High.
- `severity_reason` -- the same idiom in the same function, NOT reported by
any reviewer. Found while fixing its neighbour. A re-rating could be
justified by whitespace, which is the same defect wearing a different
field name.
- `findings_in` / `findings_out` / `reports[].findings_count` -- `bool`
subclasses `int`, so `isinstance(True, int)` is True. Worst at count 1,
where `True == 1 == len(findings)` keeps the equality comparison silent.
A non-int `findings_count` is now named rather than silently skipped,
because skipping it made the sum wrong and blamed `findings_in`.
- `duplicate_groups[].survivor` -- validated only indirectly, via a member
pointing back at it, so a group with an empty `duplicates` list never had
its survivor checked. Validator-only today (`_build_duplicate_groups`
gates on `len(candidate_ids) < 2`), but STEP 10's malformed-field controls
look for exactly this.
12 tests added, each failing before the change: 29 in test_verdicts (was
17), 150 across launchpad/review-agent, check_step2 and check_contract green.
Adjudicated on #261 by an independent pass over ciaran-slow's review; the
whitespace idiom's sibling site in `run_adjudication.py` belongs to #263 and
is not touched here.
Refs #118
Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…erdicts' into feat/review-agent-adjudication-run Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…EP 2) `is_nonempty_str` and `is_int` were private. STEP 3's `run_adjudication.py` enforces the same "non-empty" rule at its own fail-closed guard, and a second private copy of one contract rule is how the two drifted apart in the first place -- the validator accepted whitespace because the producer did, and neither had a shared definition to disagree with. Public so #263 can import the rule rather than re-implement it. No behaviour change: 150 tests across launchpad/review-agent still green. Refs #118 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…erdicts' into feat/review-agent-adjudication-run Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…es deferral (#118 STEP 3) Two findings adjudicated on #263. **`_run_judge_safely` did not fail closed on blank evidence.** The guard was `verdict not in verdicts.VERDICTS or not evidence` -- a truthiness test, so `not " "` is False and whitespace passed as usable output. Reproduced through the shipped `--replay` flag, no code injection: a recording carrying `verdict_evidence: " \n "` published a CONFIRMED verdict at exit 0, and `verdicts.validate` returned zero violations because the contract check used the same idiom. A CONFIRMED Blocker is what blocks a merge, so this could publish a merge-blocking verdict with no stated reason. The rule is now `verdicts.is_nonempty_str`, imported rather than re-implemented. That matters more than the strip() itself: this producer guard and the contract check drifted apart precisely because each had its own copy, and each admitted whitespace because the other did. The docstring said "empty verdict_evidence", which was literally accurate -- `" "` is not empty -- so it has been corrected to say what the guard enforces. The promise it actually broke was the clause before it, "fail closed on anything unusable", and ADJUDICATION.md's own words it quotes. **`adjudication.notes` was hardcoded empty with no deferral stated.** Every other hardcoded-empty field is named in the module docstring's STEP 6/7 deferral list; `notes` was the one that was not, while ADJUDICATION.md declares it and `verdicts.py` carries it -- so a reader had every reason to assume the channel worked. Now documented, at the docstring and at the assignment. Deliberately NOT resolved here: `adjudicator.md` (#265) normatively tells a judge to "record it in `adjudication.notes`", against a protocol that drops the key. Plumbing `notes` means designing how notes are collected and attributed, which is a STEP 6/7 decision, and amending #265 is #265's call. The tension is now stated in the code so it cannot merge past unnoticed rather than silently picked. 6 tests added, each failing before the change. One of them originally passed for the WRONG reason and was fixed: the replay recording format is a mapping `finding_id -> {...}`, not a flat record, so the first version missed the lookup entirely and asserted UNPROVEN against "no recorded judge output". It now carries a control proving the lookup HITS and returns CONFIRMED on good evidence, so the UNPROVEN in the blank cases is the guard firing. 25 tests in test_run_adjudication (was 19), 176 across launchpad/review-agent. Refs #118 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…STEP 4) Adjudicated on #264 as the root cause inherited unchanged by #266 and #267. A `stages` value that was present but not a list was treated as absent at both sites that read it: `_check_not_already_adjudicated` returned early, and the manifest builder substituted `[]`. Two failures from one shape defect, reproduced through the real CLI: stages = [{"name":"adjudication",...}] -> exit 1, correctly refused stages = {"0":{"name":"adjudication",...}} -> exit 0, guard bypassed stages = {"p":{"name":"preflight","status":"blocked",...}} -> exit 0, entry GONE, "complete" stages = 42 / "adjudication" / True -> exit 0, dropped The second is the expensive one. A `blocked` pre-flight -- #116's fork-PR-secrets-withheld case -- disappeared and the document published as a clean, complete review, because #119 only banners a non-`complete` status. That is #118's fifth criterion failing through a shape defect no verdict-side check looks at: `grep -n stages findings.py verdicts.py` returns nothing, so neither contract validator inspects this key at all. "#117 never emits that shape" is not available as a defence here. The `stages` manifest is explicitly an output #117 does NOT produce, so there is no upstream guarantee for this stage to inherit. Fix: one `_input_stages` reader, raising `StagesShapeError`, used by BOTH sites. One function rather than two corrected inline checks on purpose -- the two readers each had their own `isinstance(..., list)` test and each treated malformed as absent, which is how one defect became two independent failures. The same lesson as `verdicts.is_nonempty_str` on #261/#263: a second copy of a rule is a second chance to disagree with it. The comment claiming `_check_not_already_adjudicated` "already guarantees" no input entry is named `adjudication` is now actually true, and says why. Absent stays legal, and an explicit null reads as absent -- #117 emits no `stages` key at all, so a fix that refused absence would break every real document. Both are tested as controls, as is a well-formed `blocked` pre-flight surviving in order. Also closes the Low at the same site: a `stages` entry that is not an object, or whose `name` is not a string, is refused in the same change. 11 tests added, each failing before the change, including the refusal driven through the real process. 57 tests in test_run_adjudication (was 46 after merging #263), 207 across launchpad/review-agent. #266 and #267 carry these two guard sites byte-identically, only line-shifted. Fixing here rather than there so one change propagates up the chain instead of becoming three copies that can diverge. Refs #118 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Brings in #264's `StagesShapeError` / `_input_stages` fix and #261/#263's `verdicts.is_nonempty_str` guard. This PR's inherited Blocker is now closed by propagation rather than by a second copy of the guard, which is the point -- two divergent copies of one check is worse than the bug. Verified on this branch through the real CLI after the merge: an `adjudication` entry inside an object container exits 1 with no document (was exit 0), a `blocked` preflight in an object container exits 1 rather than being silently discarded (was published as `complete`), and the control -- a well-formed list carrying a `blocked` preflight -- still exits 0 with both entries in order. Conflicts were docstring and error-class additions only; both sides kept. 219 tests across launchpad/review-agent. Refs #118 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Brings #264's `StagesShapeError` / `_input_stages` and #261/#263's `verdicts.is_nonempty_str` up to the chain tip. Both of this PR's findings were homed on earlier branches -- the `stages` Blocker on #264 and the `notes` drift on #263 -- so this branch is cleared by propagation rather than by any change of its own, which is what the adjudication asked for. Clean merge, no conflicts. 230 tests across launchpad/review-agent. `notes` remains empty and is now documented as deferred at STEP 6/7, with the unresolved tension against `adjudicator.md` (#265) stated in the code. #265 should not merge ahead of that decision. Refs #118 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
benmitchell11
left a comment
There was a problem hiding this comment.
Independent pass (not the pipeline that left the earlier reviews on this PR).
Checked out feat/review-agent-adjudication-verdicts locally and ran python3 -m unittest discover -s launchpad/review-agent -p 'test_*.py' myself: Ran 150 tests ... OK, in a clean worktree. That's more than the Ran 17 tests the PR body's own Verification section quotes, because this branch's tip already carries fix commits (e2e953faa, f79aa3d64, 77065b95b) added after that section was written -- worth updating the PR body so Verification reflects the current tip, but not a blocker.
Re-verified the three findings from the standing review myself rather than trusting either account:
- Whitespace-only
verdict_evidence--verdicts.py:37-48now definesis_nonempty_str(isinstance(value, str) and bool(value.strip())) and it's used at the evidence check (:295) and theseverity_reasoncheck (:313). I built a minimal output document withverdict_evidence: " "and calledvalidate()directly: it now correctly reports"verdict_evidence must be a non-empty string, got ' '". Fixed. isinstance(True, int)admitting booleans as counts --is_int(:51-61) now explicitly excludesbool, used at bothfindings_count(:356) andfindings_in/findings_out(:366). Fixed.- Empty-
duplicatesgroup skipping survivor validation --:472-481now checkssurvivorunconditionally, before the loop overduplicates, with a comment explaining exactly why it moved. Fixed.
I didn't find anything new. The module is well-scoped (three adjudication keys deliberately left to STEP 10, documented rather than silently gapped) and validate()'s never-raise, collect-everything design holds up under the probes above.
Not verified: no live model or subprocess involved here (pure dataclasses/validation), so nothing beyond the test run and my own probes applies.
…ion-run feat(launchpad): run_adjudication.py -- the adjudication CLI (#118 STEP 3)
Requested changes are done — but I am not the right party to certify itDisclosure first: I wrote some of the commits on this branch, including the fix for the blocker @ciaran-slow raised. So this is a status report, not an independent review, and I am not clearing it. @ciaran-slow's change-request (2026-08-21T03:56Z) is stale — the head has moved to
Also fixed, and worth flagging because nobody reported it: Both predicates were made public deliberately so #263's producer guard imports the rule rather than re-implementing it — two private copies of one contract rule is how these drifted apart in the first place. What this needs: a re-review from @ciaran-slow, whose findings these were, rather than a clearance from me. His change-request is stale and will not clear itself, and I should not be the one dismissing findings against a fix I wrote. I have re-requested his review. Non-blocking follow-ups from the same pass are filed and need nothing here. |
tucktuck101
left a comment
There was a problem hiding this comment.
Reviewed current head and the existing review history. The previously confirmed High/Medium findings have been fixed and independently re-verified; current CI is green, and the accumulated STEP 2 + STEP 3 scope is intentional. Approving this revision
Summary
Implements STEP 2 of
launchpad/plans/2026-08-13-issue-118-adjudication.md, following STEP 1 (ADJUDICATION.md, merged in #256). Addslaunchpad/review-agent/verdicts.pyandtest_verdicts.py: theVerdict/Adjudicationdataclasses matchingADJUDICATION.md's six added finding fields and nineadjudicationblock keys, plus a purevalidate(input_document, output_document) -> list[str]that returns every violation rather than stopping at the first.Related issue
Refs #118
Issue type
Task
Agent provenance
Provenance caveat, stated rather than papered over: this body was filled in by a different session from the one that wrote the code, after CI's
pr_body_check.pyfailed the PR for missing provenance, a missing### Not verifiedsection, and no fenced output block. The authoring session's own model id is not recorded anywhere I can read, so I have not guessed at it — the original body said only "Drafted by Claude Code". Everything below the provenance table is either quoted from the original body or re-verified by me directly; nothing here is inferred about what the authoring session did.Objective
Turn
ADJUDICATION.md's prose verdict contract into executable dataclasses plus a validator, so later steps can check an adjudication document instead of trusting it.Impacted components
launchpad/review-agent/verdicts.py
launchpad/review-agent/test_verdicts.py
Approach and rejected alternatives
SEVERITY_ORDERis imported fromreview.pyrather than redeclared, so the ordering cannot drift between the two modules.validate()is pure — no subprocess, no network, no model call — and returns the full list of violations rather than raising on the first, because STEP 10's planned controls feed it deliberately malformed documents and need to see every violation each one produces. It re-runs #117's ownfindings.validateover the output rather than reimplementing those checks.One Medium finding was resolved by scoping rather than extending:
validate()does not check 3 of the 9adjudicationkeys (schema_version,verdict_counts,notes). Per STEP 1's own text, per-key controls for theadjudicationblock are STEP 10's job (check_adjudication.py), and STEP 2's done-when never names those three. The fix (77065b95b) was to state that boundary explicitly invalidate()'s docstring rather than pull STEP 10's scope forward — the rejected alternative being to widenvalidate()now and leave STEP 10 with nothing to do and no record of why.A
serina:review-codepass before this PR found one Blocker:validate()raisedTypeErroron a non-string field value (e.g.severity: ["Blocker"]) instead of returning a violation, which would have broken exactly those STEP 10 malformed-input controls. Fixed inf79aa3d64and independently re-confirmed against a fresh repro.Verification
Command run — both suites, at this PR's head (
77065b95b, confirmed to matchgh pr view 261 --json headRefOid):Raw output:
test_verdicts.py's 17 cases cover every item in STEP 2's own done-when list: four independent violations reported at once, drop-vs-invent detected via id sets rather than counts, both severity fields independently, both downgrade directions, bothtotal_refutationdirections, both dedupe directions, thefindings.validatere-run, no-mutation of the input, andSEVERITY_ORDERidentity withreview.py.test_findings.py(36 tests, from #117) confirms this change leaves that contract untouched.Not verified
I did not re-derive the review-code Blocker (
f79aa3d64) or the Medium scoping finding myself — those are quoted from the authoring session's own account, and I have not independently reproduced the originalTypeError. I did not checkvalidate()against a real adjudicator's output, only against the fixtures intest_verdicts.py, so nothing here proves it accepts a document a live adjudication run would actually produce. The three uncheckedadjudicationkeys (schema_version,verdict_counts,notes) are deliberately unvalidated at this step and remain unverified until STEP 10 buildscheck_adjudication.py. No mutation testing was run againstvalidate(), so I cannot say the 17 cases would catch a weakening of the checks they cover.launchpad/review-agent's tests are run in CI bylaunchpad-review-agent-controls.ymlonly on paths underlaunchpad/review-agent/**— I confirmed that path filter matches this diff, but did not watch the job itself go green for this specific head.Security implications
None that change exposure.
validate()is pure and offline by construction — no subprocess, no network, no model call — and it reads documents rather than executing anything in them. It is a containment mechanism in the weak sense that it is the thing that will later refuse a malformed or adversarial adjudication document, so the Blocker fixed inf79aa3d64(raisingTypeErrorinstead of returning a violation on a non-string field) mattered: a validator that crashes on hostile input fails open in any caller that catches exceptions broadly.Escalations
The provenance caveat above is the one item raised rather than decided: I filled this body in for a PR I did not author, and I have deliberately not invented the authoring session's model id or re-stated its findings as my own observations. If @serina-mcfall wants the provenance table to name the actual authoring model, that has to come from her or that session — I cannot read it.