---
title: "Nothing Fired: Reading structural_trust_score as a Debit Ledger — NanoParse Blog"
url: https://nanoparse.app/blog/2026-10-25-nothing-fired
description: "A parse returns twenty signal groups. One of them, structural_trust_score, is the group most often read as a verdict and the easiest to misread: it holds a thre…"
---

A parse returns twenty signal groups. One of them, `structural_trust_score`, is the group most often read as a verdict and the easiest to misread: it holds a three-value level and a list of reasons.

Across nineteen submissions on 2026-10-25 — ten of which produced a document — the level came back `medium` nine times, `low` once, and `high` never. Nothing in those ten reads was broken. That distribution is what an additive debit ledger looks like when it meets ordinary web pages, and the deployed source states the mechanism in nine rules over about sixty lines.

## The ledger starts at zero

The score is assembled after the rest of the signals are computed, and it begins at zero with no credit anywhere:

```

let score = 0; // lower = more trust-reducing factors → lower trust level
...
if (score <= 0) return { level: "high", trust_factors };
if (score <= 2) return { level: "medium", trust_factors };
return { level: "low", trust_factors };
```

Every rule below that comment adds points. None subtracts. There is no path through the function where a good property lowers the score, which means `high` is not a positive finding — it is the absence of every finding. Two debits worth two points each cross the `low` threshold on their own; a single one-point debit lands `medium` and stays there.

The output carries both halves:

```

"structural_trust_score": {
"level": "medium",
"trust_factors": ["Moderate-authority source", "No publish date found — verify independently"]
}
```

The level is the summary. The array is the evidence, and it is the only part a client can act on.

## What it takes to read "high"

Zero points. In practice that means a page has to clear all of these at once: source authority at or above 0.4, an accepted publish date, at least 200 characters of Markdown, no match for the speculative-language pattern (`rumor`, `alleged`, `unconfirmed`, `speculation`, `leaked`), hedge density below `medium`, a source class that is not forum/social/commercial, and outbound authority at or above 0.3.

The two highest-authority documents in this run were `arxiv.org/abs/1706.03762` and `plato.stanford.edu/entries/artificial-intelligence/`, both at authority 0.85, both logging `High-authority source`. Both came back `medium`. One debit did it: neither page carries a publish date the extractor accepts, and a missing date is worth one point.

`High-authority source` reads like a compliment and is worth zero. It is a label on the evidence list, not a credit against the score, so it cannot offset a missing date. That is the field's shape in one line: `high` means no debit rule fired, not that the page was verified. A page nobody evaluated can read `high`; the Stanford Encyclopedia of Philosophy does not.

## The weights, and what fired

Every rule, its weight, and how many times it fired across the ten completed reads:

```

factor weight fired
------------------------------------------------------------------ ------ -----
Low-authority source (authority < 0.4) 2 0
No publish date found — verify independently 1 7
Content is N years old — may be outdated (>= 2 years) 1 1
Unparseable date 1 0
Very short content — may be incomplete (Markdown < 200 chars) 1 0
Contains speculative or unverified language 1 1
High hedge-language density — claims are heavily qualified 2 0
Moderate hedge-language density — some claims are qualified 1 1
User-generated content — verify independently (forum, social) 2 0
Commercial source — possible commercial bias 1 4
Weak outbound authority — few or low-quality external citations 1 1
High-authority source n/a 2
Moderate-authority source n/a 8
Strong outbound authority — cites high-quality sources n/a 0
```

The three `n/a` rows are labels. They appear in `trust_factors` and carry no weight, which is why the length of the array is not the score: the `low` page below lists four factors and scores three points, and eight of the ten reads list a zero-weight label first.

The distribution is lopsided in a specific way. Eight of ten reads took a freshness debit, seven of them for a missing publish date. Four took the commercial-bias debit. Exactly two took a text-level debit — one speculative-language match and one moderate hedge density. The ledger on this sample is mostly a metadata check, and the metadata it checks most often is a date.

## Ten reads, one ledger

Each row is a real payload from 2026-10-25, and the score column was reconstructed from the factor strings alone using the weights above — it reproduced the level on all ten:

```

level score source authority url
------ ----- ---------- --------- ------------------------------------------------------
medium 1 academic 0.85 arxiv.org/abs/1706.03762
medium 1 academic 0.85 plato.stanford.edu/entries/artificial-intelligence/
medium 1 technical 0.70 stackoverflow.com/questions/11227809
medium 1 reference 0.50 en.wikipedia.org/wiki/Web_scraping
low 3 reference 0.50 en.wikipedia.org/wiki/Allegation
medium 1 commercial 0.40 developer.mozilla.org/en-US/docs/Web/API/Fetch_API
medium 1 commercial 0.40 example.com
medium 2 commercial 0.40 news.ycombinator.com
medium 2 commercial 0.40 stripe.com/pricing
medium 2 news 0.70 bbc.com/news
```

The one `low` is a page about the concept of an allegation, and its four factors split evenly between provenance and text: no publish date, moderate authority, a speculative-language match, moderate hedge density. Three points. The classification is defensible for that page.

Note what reaches two points without a single complaint about the writing. `bbc.com/news` is a news index: no publish date on the page, few outbound links the scorer rates, and two debits from structure alone. `stripe.com/pricing` and `news.ycombinator.com` get there through the same missing date plus the commercial default. A client that treats `medium` as a warning would flag all three, and would also flag every academic abstract in the run.

The Stack Overflow row is the case the field is actually good at: `Content is 14 years old — may be outdated`. That is the kind of thing a caller cannot see from Markdown length or word count, and it is a real reason to prefer a newer source for a question about current APIs.

## Nine submissions that never got a score

The level is downstream of a successful read, so it says nothing about the reads that failed. Nine of the nineteen submissions never produced a document:

```

outcome count example
--------------------- ----- --------------------------------------------
blocked 5 irs.gov, nih.gov, cdc.gov, nist.gov, apnews.com
content_insufficient 2 weather.gov, pubmed.ncbi.nlm.nih.gov
not_found 1 a 2015 article URL that now redirects away
domain blocked at submit 1 reddit.com/r/webdev ("seed: JS shell / login wall")
```

A consent wall and a 14-year-old forum answer are opposite problems, and only one of them shows up in `structural_trust_score`. The refusal path is its own layer with its own codes — [Terminal or Transient](https://nanoparse.app/blog/2026-10-22-refusal-layer) walks the whole taxonomy. The point here is narrower: a client that reads `level` and nothing else is blind to half of what the endpoint answered that day. Half, measured, not estimated: nine refusals to ten documents.

## Reading the factors instead of the level

The level is coarse by construction — three buckets over a small integer — and the array is not. Reconstructing the score is a lookup, which makes a per-factor policy practical:

```

const DEBIT: Record<string, number> = {
"Low-authority source": 2,
"No publish date found — verify independently": 1,
"Unparseable date": 1,
"Very short content — may be incomplete": 1,
"Contains speculative or unverified language": 1,
"High hedge-language density — claims are heavily qualified": 2,
"Moderate hedge-language density — some claims are qualified": 1,
"User-generated content — verify independently": 2,
"Commercial source — possible commercial bias": 1,
"Weak outbound authority — few or low-quality external citations": 1,
};

function ledger(score: { level: string; trust_factors: string[] }) {
const debits = score.trust_factors.map((factor) => ({
factor,
points: DEBIT[factor] ?? 0, // zero-weight labels score nothing
}));
return {
level: score.level,
total: debits.reduce((n, d) => n + d.points, 0),
debits: debits.filter((d) => d.points > 0),
};
}
```

An age string arrives with the year embedded (`Content is 14 years old — may be outdated`), so match it by prefix rather than adding a key per number. Once the array is decomposed, the decision is about classes of problem, not a threshold:

```

const { total, debits } = ledger(litmus.structural_trust_score);
const names = debits.map((d) => d.factor);

const contested = names.some((f) => f.startsWith("Contains speculative"));
const unvetted = names.some((f) => f.startsWith("User-generated"));
const undated = names.some((f) => f.startsWith("No publish date"));
const dated = names.find((f) => f.startsWith("Content is"));

if (contested || unvetted) {
// claim-level risk: do not quote this page as fact
// escalate to a second source instead of retrying the same URL
} else if (dated) {
// not a trust problem: a freshness problem — check for a newer page
} else if (undated) {
// provenance problem: fine for a definition, weak for a "current" answer
}
```

Three branches, three different actions. `level` cannot tell them apart: `undated` is `medium` and `contested` alone is also `medium`, and the only difference visible in the payload is the string.

## Where the debits come from

Three properties of the scorer explain most of the table, and all three are visible in the deployed source and in the payloads above.

`commercial` is the default class, and it costs a point. The classifier runs its rules top to bottom and ends with `return "commercial"; // fallback`. A domain that matches nothing is treated as commercially motivated, so `developer.mozilla.org` (a documentation site), `example.com` (an IANA example page) and `news.ycombinator.com` (an index of links) all read `commercial` at authority 0.40. Four of the ten reads in this run carried the commercial-bias debit, and at least three of those pages sell nothing.

Wikipedia is de-rated on purpose. The domain table carries `"wikipedia.org": -0.1, // good overview, not authoritative` against a 0.6 reference base, which lands at 0.50 — inside the moderate band. No Wikipedia page can log `High-authority source`. Both Wikipedia reads came back moderate, one of them with a speculative-language debit on a page that discusses allegations.

The date extractor drives the ledger. Seven of ten reads were debited for a missing publish date. Documentation pages, index pages and abstract pages usually have no publish date to extract, and a page that is undated for a good reason is scored the same as a page that is undated because it is a shell.

None of that makes the field wrong. It makes it a specific instrument: on this sample it mostly measures whether a page looks like a dated article from a domain the classifier recognizes, with a secondary read on hedging and speculation in the text.

## What the field is not

- It is not a fact check. No rule reads whether a claim is true. A page can score zero debits while being completely wrong, and a correct page can score three.
- It is not a model call. Every rule is a regex, a lookup, or a threshold. The same page produces the same factors on every parse, which is why a score can be diffed between versions of a page.
- It is not an evaluation of the content's quality. `medium` is the modal state of the ordinary web in this sample, not a warning label.
- It carries no magnitude. One +2 and two +1s land in the same bucket, and a page one point below the `low` threshold looks identical to a page that just cleared `high`.

The last point is the practical one. The thresholds are policy — `0 / 1–2 / 3+` — not a measurement, and they were chosen to make `low` rare. On a sample where nine of ten pages read `medium`, the level distinguishes almost nothing; the factors distinguish everything.

## A client contract for the field

1. Do not branch on `level` alone. `medium` covers a documentation page, a news index and a speculative essay. Use the array.
2. Group the factors by class before deciding. Freshness (`No publish date`, `Content is N years old`), provenance (`Low-authority`, `Commercial source`, `User-generated`), and text (`speculative`, `hedge-language`) call for different actions: a freshness debit means look for a newer page, a text debit means do not quote it as fact, and a provenance debit means the claim needs a second source.
3. Read it next to `page_status` and `freshness`. The trust score does not see a wall, a redirect chain or a truncated body; those are other groups in the same response. [Was This the Page You Asked For?](https://nanoparse.app/blog/2026-10-19-page-you-asked-for) covers the six fields that answer the "is this the page I asked for" question.

A score that can only go down is still a useful instrument, as long as the caller knows which way it points. It never certifies a page. It names the debits, one string each, in the same response as the Markdown — twenty signal groups, no second call, no extra charge beyond the $0.005 per successful parse.

The payloads behind every number here are committed with the post, and the reconstruction script reproduces the level from the factor strings on all ten reads. The endpoint is `https://nanoparse.app/fetch`; the signal reference is at [nanoparse.app/litmus](https://nanoparse.app/litmus), the first call is at [nanoparse.app/quickstart](https://nanoparse.app/quickstart), and the hosted MCP endpoint is `https://nanoparse.app/mcp`.
