Example

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:ch09

No 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.

Canonical source and the relations module .

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.