Separate finding a candidate, labelling it, and showing it
See a cue-based classifier miss two of seven hand-picked pairs, and watch a record carry how a label arose, how far to trust it, and whether a person has ruled on it.
Similarity finds candidates. A separate step labels them. A third step decides whether the label is worth the reader’s attention. The chapter’s claim is that these are three different things, and that a label is a claim — so every record must say how the label arose, how far it can be trusted, and whether anyone has ruled on it.
Run it
git clone https://github.com/ernanhughes/project-language.git
cd project-language
git checkout 329660f679a5a87831f91fe1430c20ac9360dda8
npm ci
npm run example:ch09No model is called. The classifier is cue-based and model-free; the pairs it is probed on are hand-picked.
Expected outcome
Similarity cannot tell a contradiction from a paraphrase — they sit at nearly the same distance:
...
opposite similarity 0.75 "The treatment did not improve outcomes in the trial."
paraphrase similarity 0.67 "Outcomes were better in the trial because of the treatment."The classifier is wrong some of the time, and the run says how often:
...
ok negation flips meaning expected CONTRADICTS got CONTRADICTS
ok paraphrase expected SUPPORTS got SUPPORTS
MISS scope condition expected RELATED got CONTRADICTS
MISS contradiction, no negation word expected CONTRADICTS got RELATED
ok explicit update expected UPDATES got UPDATES
ok unrelated expected UNKNOWN got UNKNOWN
ok near-duplicate expected SUPPORTS got SUPPORTS
5 of 7. Seven hand-picked pairs say nothing about accuracy in general; they show the kinds of error.A wrong label is worse than none, so the record carries its provenance, and a contradiction without a declared shared scope is flagged rather than shown as one:
...
classifier said CONTRADICTS; route=classified; standing=proposal; caveats=["SCOPE_UNCONFIRMED"]
shown as: possible contradiction, unconfirmed: scope not declared as shared (proposed)A person’s ruling is not displaced by a later classification, and confirming a label is not confirming a scope:
...
history: classified → adjudicated → classified
in force: contradicts (confirmed by a person) [adjudicated]Whether the label is shown at all depends on what the reader is doing:
...
check_a_claim show contradicts (proposed)
understand show contradicts (proposed)
stay_current hold back contradicts (proposed)
build hold back contradicts (proposed)The record says how the label arose
A classifier’s output is only ever a proposal, and the standing names that:
/** How the label reached the record. */
export type Route = "asserted_by_source" | "classified" | "adjudicated";
/**
* How far to trust it. There is no "verified" here: this module cannot establish truth.
* proposal a classifier's guess; the weakest standing that is still a label
* asserted an author's own claim about the relation, with a citation
* adjudicated confirmed or corrected by a person
* unknown no label; an honest value, not a failure
*/
export type Standing = "proposal" | "asserted" | "adjudicated" | "unknown";
export type Caveat = "SCOPE_UNCONFIRMED";Only a person adjudicates, and a person’s adjudication is not displaced by a later classification of the same pair:
export function adjudicate(
record: RelationRecord,
by: { type: "human" | "model" | "classifier"; id: string },
input: { relation?: Relation; direction?: Direction; reason: string; /** The person asserts the two items share the scope in which the relation holds. Without it, a scope flag stays. */ confirms_scope?: boolean },
): RelationRecord {
if (by.type !== "human") throw new RelationError("NOT_A_PERSON", `only a person adjudicates; got ${by.type}`);
if (input.reason.trim() === "") throw new RelationError("NO_REASON", "adjudication needs a reason");
const { id: _id, ...rest } = record;
return seal({
...rest,
relation: input.relation ?? record.relation,
direction: input.direction ?? record.direction,
route: "adjudicated",
standing: (input.relation ?? record.relation) === "UNKNOWN" ? "unknown" : "adjudicated",
by: `person:${by.id}`,
// Confirming a label is not confirming a scope. The flag clears only if the person says the scope is shared, or if the
// relation they settle on is not one that depends on scope at all.
caveats: SCOPE_BOUND.has(input.relation ?? record.relation) && !input.confirms_scope ? record.caveats : [],
reason: input.reason,
});
}export function effectiveRecords(history: readonly RelationRecord[]): RelationRecord[] {
const byPair = new Map<string, RelationRecord>();
for (const r of history) {
const key = r.a.id + "|" + r.b.id;
const held = byPair.get(key);
if (held?.route === "adjudicated" && r.route !== "adjudicated") continue;
byPair.set(key, r);
}
return [...byPair.values()];
}Mechanism and limitations
Documented: the three-stage split, the standing vocabulary and the scope
caveat are stated in core/relations/src/index.ts and in the module’s header
comment. core/relate, the classifier underneath, describes itself as a
cue-based v0.1 that is uncalibrated.
Observed: the run above. The classifier got 5 of 7 hand-picked pairs, missing a scope condition (which the record’s scope flag catches, though the label stays wrong) and a contradiction with no negation word. A model asking for another label on an adjudicated pair was refused and not recorded. A flagged contradiction was shown as possible and unconfirmed.
Not shown. Seven hand-picked pairs say nothing about accuracy in general. The
classifier reads the scope-condition example as a contradiction; the scope guard is
string equality and needs scopes declared by someone. The core’s adjudicate
trusts the caller’s human; the host command is the boundary, not
authentication. An explicit-revision cue was added after seeing the miss it
fixed, so that probe is no longer independent of it — an 8-pair regression set
guards it, and “no longer accepts” reads as a contradiction. “Adds nothing” is
measured against the text being read, not the reader’s wider knowledge, over
invented snippets, and it is never applied to a contradiction, update or
implementation.
Understand this example
Copy this prompt into your AI tool. No code runs here.
Apply this example
Copy this prompt into your AI tool. No code runs here.