Civic Tech + AI Incubation Lab

City Code & Regulation Search Assistant

How responses are scored

Every AI response is scored on four dimensions, each on a 0–3 scale. Scores are derived from atomized claims, not from a single end-of-response scoring pass. A response can score independently on each dimension — high accuracy + low completeness is a real outcome, as is high accuracy + low citation authority. The four dimensions are intentionally not collapsed into a composite.

The four dimensions

#
Dimension
What it measures
Range
1
Accurate
Are the specific factual claims actually correct?
0–3
2
Complete
Did the response cover the things a resident needs to act?
0–3
3
Cited
Are sources from portland.gov / Portland Titles, or from Redfin / blogs / nothing?
0–3
4
Useful
Would an average Portland resident or business owner understand and act on this?
0–3

Adversarial protocol — how scores are produced

Generator ≠ Evaluator

The model that generates a response must not be the model that evaluates it. Self-evaluation is too generous: the same model that wrote a confident-sounding answer is unlikely to flag its own confident-sounding errors.

  1. Generate. A "user-where-they-are" model (e.g. consumer ChatGPT for Column 1) produces a response to the resident's question.
  2. Atomize / verify. A different model — the evaluator — receives the verbatim response and breaks it into atomic claims. For each atom it produces a correctness tag, a verified_truth grounded in its own lookup, plus citation, topic, specificity, actionability, and importance.
  3. Aggregate. The four 0–3 dimension scores are computed deterministically from the atoms (formulas below).

The evaluator model is configurable — default is Claude (best agentic lookup), but the cross-evaluation matrix (Claude judging GPT, GPT judging Claude) can be demonstrated on demand. The evaluator-model used is recorded in each atoms file so the detail page can show "verified by: <model>".

Per-dimension detail

Dimension 1
Accurate
0 – 3
Question: of the specific factual claims this response made, how many were actually correct?
Aggregation
let A = atoms attributed to this variant
let weight(atom) = atom.importance         // 1–5
let credit(atom) =
    1.0  if correctness ∈ {correct, correct-vague, correct-action,
                            correct-judg, correct-range, hedge-correct}
    0.6  if correctness ∈ {vague-correct, vague-action}
    0.0  if correctness ∈ {WRONG, PARTIAL-WRONG}
    skip if correctness == admitted-gap    // admitted gaps don't penalize accuracy
let raw = sum(weight·credit) / sum(weight)
score = floor(raw × 3 + 0.5)               // 0..3, with 0.83+ → 3
Score scale
0
Mostly wrong on the specifics that matter.
1
Mix of right and wrong; the resident can't trust unverified claims.
2
Right on most specifics but with at least one material error.
3
All material specifics check out under independent verification.
admitted-gap atoms (where the AI honestly said "I don't know") do not penalize accuracy. They penalize completeness instead.
Dimension 2
Complete
0 – 3
Question: did this response cover the topics a resident actually needs to act on?

Each scenario in data/scenarios.json carries a must_cover array — the topics a complete answer must hit (e.g. for tiny-house: ["zoning", "size", "setbacks", "permits", "costs", "sequence"]). Topics use the same vocabulary as atom topic fields.

Aggregation
let covered = distinct topics across this variant's atoms ∩ must_cover
score = round(3 × |covered| / |must_cover|)
Score scale
0
Missed almost everything material.
1
Hit a few topics, missed most.
2
Covered most topics; one or two material gaps.
3
Covered every topic on the must-cover list.
Address-specific cap: if any atom for this variant has correctness == admitted-gap on a topic in {address, zoning}, completeness is capped at 2. Admitting the AI couldn't reach property data is honesty about the data gap — but it still means the resident didn't get parcel-specific guidance.
Dimension 3
Cited
0 – 3
Question: are the cited sources actually authoritative for Portland code?

This is not the same as "is the citation specific?" A vague reference to portland.gov is more authoritative than a precise-looking citation to a Redfin listing or a third-party blog. The hackathon brief said authoritative, not specific; this dimension reflects that.

Source-authority buckets
Bucket
Examples
Credit
authoritative
PCC 33.205.040, Title 11, portland.gov/..., PortlandMaps, BDS Code Guide, Oregon Revised Statutes
1.0
plausible-third-party
Redfin, Zillow, Wikipedia, real-estate blog
0.3
non-authoritative
random forum, unattributed
0.0
none
null
0.0
Aggregation
let A = atoms attributed to this variant where atom carries any factual claim
let raw = sum(credit(atom.citation)) / |A|
score = floor(raw × 3 + 0.5)
Score scale
0
No authoritative sources at all.
1
Mostly uncited or third-party-cited.
2
Mix — some Portland sources, some non-authoritative.
3
Cited claims overwhelmingly resolve to portland.gov / Portland Titles / authoritative state sources.
Dimension 4
Useful
0 – 3
Question: would an average Portland resident or business owner — with no permitting expertise — understand this response and feel equipped to act on it?

This is the city's stated audience: "People who live and work in the city of Portland have questions about what they can do on their property." It is not a contractor, not a planner, not a code-fluent reader.

Aggregation
let A = atoms attributed to this variant
let raw = mean(actionability(atom)) for atom ∈ A
let jargon_penalty = 0.10 × (count of unexplained jargon tokens / 100 words)   // cap at 0.30
score = floor((raw − jargon_penalty) × 3 + 0.5)

Unexplained-jargon detection looks for un-glossed acronyms (ADU, FAR, SDC, DAR, BDS) and code-citation shorthand without a plain-language gloss. The penalty is bounded to keep the dimension stable.

Score scale
0
Inaccessible. Jargon-dense, no scaffolding.
1
Partially accessible. Mostly readable but unexplained terms.
2
Accessible. Key terms defined, concrete next steps stated.
3
Resident-ready. Plainly written, anticipates resident-level confusion, scaffolds next steps.
← Back to dashboard