@dzhechkov/harness-core 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dz-manifest.json +121 -41
- package/README.md +1 -1
- package/dist/cli-flag-notice.d.ts +50 -0
- package/dist/cli-flag-notice.d.ts.map +1 -0
- package/dist/cli-flag-notice.js +106 -0
- package/dist/cli-flag-notice.js.map +1 -0
- package/dist/event-chain.d.ts +50 -0
- package/dist/event-chain.d.ts.map +1 -1
- package/dist/event-chain.js +31 -0
- package/dist/event-chain.js.map +1 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/name-check.d.ts +98 -0
- package/dist/name-check.d.ts.map +1 -0
- package/dist/name-check.js +333 -0
- package/dist/name-check.js.map +1 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +33 -5
- package/dist/operations.js.map +1 -1
- package/dist/provenance.d.ts +100 -92
- package/dist/provenance.d.ts.map +1 -1
- package/dist/provenance.js +122 -122
- package/dist/provenance.js.map +1 -1
- package/dist/recall-domain-boost.d.ts +10 -3
- package/dist/recall-domain-boost.d.ts.map +1 -1
- package/dist/recall-domain-boost.js +7 -0
- package/dist/recall-domain-boost.js.map +1 -1
- package/dist/recall-hook-policy.d.ts +15 -0
- package/dist/recall-hook-policy.d.ts.map +1 -1
- package/dist/recall-hook-policy.js +59 -0
- package/dist/recall-hook-policy.js.map +1 -1
- package/dist/recap.d.ts +146 -0
- package/dist/recap.d.ts.map +1 -0
- package/dist/recap.js +346 -0
- package/dist/recap.js.map +1 -0
- package/dist/retro.d.ts +131 -0
- package/dist/retro.d.ts.map +1 -0
- package/dist/retro.js +207 -0
- package/dist/retro.js.map +1 -0
- package/dist/score.d.ts +21 -0
- package/dist/score.d.ts.map +1 -1
- package/dist/score.js +44 -3
- package/dist/score.js.map +1 -1
- package/dist/vector-tier.d.ts +54 -0
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +69 -8
- package/dist/vector-tier.js.map +1 -1
- package/package.json +8 -8
- package/sbom.json +240 -40
- package/src/cli-flag-notice.ts +114 -0
- package/src/event-chain.ts +64 -0
- package/src/index.ts +13 -0
- package/src/name-check.ts +331 -0
- package/src/operations.ts +34 -6
- package/src/provenance.ts +217 -0
- package/src/recall-domain-boost.ts +10 -3
- package/src/recall-hook-policy.ts +60 -0
- package/src/recap.ts +462 -0
- package/src/score.ts +53 -3
- package/src/vector-tier.ts +109 -10
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The provenance gate — nothing leaves this machine citing a source that may not.
|
|
3
|
+
*
|
|
4
|
+
* It checks PROVENANCE, not words. A denylist of forbidden phrases is an enumeration, and it loses
|
|
5
|
+
* to the first confidential note it has never heard of; the `.gitignore` in this repository carries
|
|
6
|
+
* a comment warning about exactly that mistake. An allowlist of SOURCES wins by construction: an
|
|
7
|
+
* unknown source is refused because it is unknown.
|
|
8
|
+
*
|
|
9
|
+
* Two things this module deliberately does NOT do, both named in ADR-001 rather than implied:
|
|
10
|
+
* - it does not read CONTENT, so confidential text pasted by hand into an allowed file inherits
|
|
11
|
+
* that file's permission;
|
|
12
|
+
* - it cannot see a paraphrase with no citation. It proves what was CITED, never what was known.
|
|
13
|
+
* The last line against both is a person reading the draft, and that is the design, not a gap.
|
|
14
|
+
*
|
|
15
|
+
* PURE: no filesystem, no git, no clock. The oracle runs in the CLI and arrives as a fact — a
|
|
16
|
+
* verdict that needs a real repository to reproduce is a verdict no test can pin.
|
|
17
|
+
*
|
|
18
|
+
* See features/provenance-gate/03_adr/ for the decisions and the measurements behind them.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Why one claim was refused. A CLOSED set, so a reason is machine-readable and never prose. */
|
|
22
|
+
export type SourceVerdict =
|
|
23
|
+
| 'allowed'
|
|
24
|
+
/** The claim names no source at all. A claim with no source is the thing we must not publish. */
|
|
25
|
+
| 'no-source'
|
|
26
|
+
/** The manifest did not declare what kind of source this is — never inferred from its shape. */
|
|
27
|
+
| 'unknown-kind'
|
|
28
|
+
/** A store record that the TRACKED public list does not name. Default-deny. */
|
|
29
|
+
| 'not-marked-public'
|
|
30
|
+
/** The path does not resolve. You cannot cite what does not exist. */
|
|
31
|
+
| 'unresolvable'
|
|
32
|
+
/** Resolved outside the repository — a symlink out of the tree lands here too. */
|
|
33
|
+
| 'outside-repo'
|
|
34
|
+
/** Git says this path is ignored: the owner's own boundary refuses it. */
|
|
35
|
+
| 'ignored-path'
|
|
36
|
+
/** Not tracked by git — nobody has reviewed it, so "not ignored" proves nothing about it. */
|
|
37
|
+
| 'untracked'
|
|
38
|
+
/** Tracked, but carrying uncommitted changes: its CURRENT contents went through no review. */
|
|
39
|
+
| 'uncommitted';
|
|
40
|
+
|
|
41
|
+
export type SourceProvenanceOutcome = 'allowed' | 'blocked' | 'not-established';
|
|
42
|
+
|
|
43
|
+
/** One claim in a draft, and where it came from. */
|
|
44
|
+
export interface SourceClaim {
|
|
45
|
+
readonly id: string;
|
|
46
|
+
/** `path` — a file in the repository. `record` — an addressed row in a `.dz` store. */
|
|
47
|
+
readonly kind?: string;
|
|
48
|
+
readonly source?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SourceManifest {
|
|
52
|
+
readonly version: number;
|
|
53
|
+
readonly draft: string;
|
|
54
|
+
readonly claims: readonly SourceClaim[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SourceProvenanceFacts {
|
|
58
|
+
/**
|
|
59
|
+
* Paths git reported as ignored. `null` means THE ORACLE DID NOT RUN — a different fact from
|
|
60
|
+
* "nothing is ignored", and the one a naive gate reads as "everything is allowed". Measured in
|
|
61
|
+
* this repo on a sibling command: `dz sync` prints `0/0` and exits 0 when it compared nothing.
|
|
62
|
+
*/
|
|
63
|
+
readonly ignoredPaths: ReadonlySet<string> | null;
|
|
64
|
+
/**
|
|
65
|
+
* Record addresses declared public in a GIT-TRACKED file. Not a `visibility` field inside the
|
|
66
|
+
* record: `.dz/` is gitignored, so a marker there appears in no diff, and the process that writes
|
|
67
|
+
* the draft can write it too. MEASURED 2026-08-22: `grep -r '"visibility"' .dz/` returns 0 — the
|
|
68
|
+
* field does not exist, and introducing it would have made "the owner opted in" mean "the
|
|
69
|
+
* generator marked itself" (ADR-001).
|
|
70
|
+
*/
|
|
71
|
+
readonly publicRecords: ReadonlySet<string>;
|
|
72
|
+
/** Each `path` source resolved through the filesystem; `null` = missing or outside the repo. */
|
|
73
|
+
readonly resolved: ReadonlyMap<string, string | null>;
|
|
74
|
+
/**
|
|
75
|
+
* Resolved paths git tracks. "Not ignored" is NOT "reviewed": a file the drafting process wrote a
|
|
76
|
+
* second ago is neither, and the first version cleared it while calling it "a tracked path"
|
|
77
|
+
* (cross-family review round 3, codex `gpt-5.6-sol`, 2026-08-22).
|
|
78
|
+
*/
|
|
79
|
+
readonly trackedPaths: ReadonlySet<string>;
|
|
80
|
+
/** Tracked paths with uncommitted changes — committed is what "reviewed" means here. */
|
|
81
|
+
readonly dirtyPaths: ReadonlySet<string>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface SourceClaimResolution {
|
|
85
|
+
readonly id: string;
|
|
86
|
+
readonly source: string | null;
|
|
87
|
+
readonly verdict: SourceVerdict;
|
|
88
|
+
readonly detail: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface SourceProvenanceDecision {
|
|
92
|
+
readonly outcome: SourceProvenanceOutcome;
|
|
93
|
+
/** 0 allowed · 1 blocked · 3 not established. A zero is only ever a proven pass. */
|
|
94
|
+
readonly exit: 0 | 1 | 3;
|
|
95
|
+
readonly claims: readonly SourceClaimResolution[];
|
|
96
|
+
readonly reason: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Parse a manifest. Unparseable is `null` — which the caller must NOT read as "empty". */
|
|
100
|
+
export function parseSourceManifest(text: string): SourceManifest | null {
|
|
101
|
+
let raw: unknown;
|
|
102
|
+
try {
|
|
103
|
+
raw = JSON.parse(text);
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
if (raw === null || typeof raw !== 'object') return null;
|
|
108
|
+
const o = raw as Record<string, unknown>;
|
|
109
|
+
if (!Array.isArray(o['claims'])) return null;
|
|
110
|
+
const claims: SourceClaim[] = [];
|
|
111
|
+
for (const c of o['claims'] as unknown[]) {
|
|
112
|
+
if (c === null || typeof c !== 'object') return null;
|
|
113
|
+
const cc = c as Record<string, unknown>;
|
|
114
|
+
claims.push({
|
|
115
|
+
id: typeof cc['id'] === 'string' ? cc['id'] : '',
|
|
116
|
+
...(typeof cc['kind'] === 'string' ? { kind: cc['kind'] } : {}),
|
|
117
|
+
...(typeof cc['source'] === 'string' ? { source: cc['source'] } : {}),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
version: typeof o['version'] === 'number' ? o['version'] : 1,
|
|
122
|
+
draft: typeof o['draft'] === 'string' ? o['draft'] : '',
|
|
123
|
+
claims,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Classify ONE claim's source.
|
|
129
|
+
*
|
|
130
|
+
* Order matters and is load-bearing. The KIND is read from the manifest and never guessed from the
|
|
131
|
+
* string: `.dz/` holds 119 git-TRACKED files (measured), so `.dz/guard-audit.jsonl` is not ignored
|
|
132
|
+
* and a shape-based guess would let it through as an ordinary repo path, skipping the public-list
|
|
133
|
+
* requirement entirely.
|
|
134
|
+
*/
|
|
135
|
+
export function classifySource(claim: SourceClaim, facts: SourceProvenanceFacts): SourceClaimResolution {
|
|
136
|
+
const id = claim.id === '' ? '(unnamed claim)' : claim.id;
|
|
137
|
+
const source = typeof claim.source === 'string' && claim.source.trim() !== '' ? claim.source.trim() : null;
|
|
138
|
+
const at = (verdict: SourceVerdict, detail: string): SourceClaimResolution => ({ id, source, verdict, detail });
|
|
139
|
+
|
|
140
|
+
if (source === null) return at('no-source', 'the claim names no source — a claim with no source is exactly what must not go out');
|
|
141
|
+
|
|
142
|
+
if (claim.kind === 'record') {
|
|
143
|
+
return facts.publicRecords.has(source)
|
|
144
|
+
? at('allowed', 'named in the tracked public-records list')
|
|
145
|
+
: at('not-marked-public', 'this record is not in the tracked public list — marking one public must be a reviewable commit, not a field inside an ignored store');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (claim.kind === 'path') {
|
|
149
|
+
const real = facts.resolved.get(source);
|
|
150
|
+
if (real === undefined || real === null) {
|
|
151
|
+
return at('unresolvable', 'the path does not resolve inside the repository — a source that cannot be resolved cannot be shown to be safe');
|
|
152
|
+
}
|
|
153
|
+
if (facts.ignoredPaths === null) {
|
|
154
|
+
// Defensive: `decideSourceProvenance` refuses before reaching here. Kept so a direct caller of
|
|
155
|
+
// classifySource cannot obtain an `allowed` from an oracle that never ran.
|
|
156
|
+
return at('unresolvable', 'the ignore oracle did not run, so no path can be cleared');
|
|
157
|
+
}
|
|
158
|
+
if (facts.ignoredPaths.has(real)) return at('ignored-path', 'git says this path is ignored — the owner\'s own boundary refuses it');
|
|
159
|
+
// Three separate questions, and the first version asked only one. A brand-new file the drafting
|
|
160
|
+
// process wrote is not ignored either, and clearing it would let the generator author its own
|
|
161
|
+
// evidence.
|
|
162
|
+
if (!facts.trackedPaths.has(real)) return at('untracked', 'git does not track this path, so nobody has reviewed it — not being ignored says nothing about a file that has never been committed');
|
|
163
|
+
if (facts.dirtyPaths.has(real)) return at('uncommitted', 'this path has uncommitted changes, so its current contents went through no review');
|
|
164
|
+
return at('allowed', 'committed, reviewed, and not refused by the owner\'s boundary');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return at('unknown-kind', `the manifest declares kind ${JSON.stringify(claim.kind ?? null)} — a kind this gate cannot check is refused, never inferred from the path's shape`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The whole-manifest verdict.
|
|
172
|
+
*
|
|
173
|
+
* Three outcomes, and neither non-pass returns zero. The dangerous one is the third: an empty
|
|
174
|
+
* manifest does not mean "nothing confidential is cited", it means "we checked nothing".
|
|
175
|
+
*/
|
|
176
|
+
export function decideSourceProvenance(manifest: SourceManifest | null, facts: SourceProvenanceFacts): SourceProvenanceDecision {
|
|
177
|
+
if (manifest === null) {
|
|
178
|
+
return { outcome: 'not-established', exit: 3, claims: [], reason: 'the manifest could not be read — nothing was checked, which is not the same as nothing being wrong' };
|
|
179
|
+
}
|
|
180
|
+
if (facts.ignoredPaths === null) {
|
|
181
|
+
// BEFORE classification, and for the WHOLE batch. Measured 2026-08-22: one out-of-repo path in
|
|
182
|
+
// a `git check-ignore --stdin` batch prints the matches found so far, then dies with exit 128 —
|
|
183
|
+
// so crediting what was printed would clear every path queued behind it (ADR-003).
|
|
184
|
+
return { outcome: 'not-established', exit: 3, claims: [], reason: 'the ignore oracle did not run, so not one path was checked — this is not a pass' };
|
|
185
|
+
}
|
|
186
|
+
if (manifest.claims.length === 0) {
|
|
187
|
+
return { outcome: 'not-established', exit: 3, claims: [], reason: 'the manifest lists no claims — a draft with nothing to check has not been shown to be safe, only left unchecked' };
|
|
188
|
+
}
|
|
189
|
+
const claims = manifest.claims.map((c) => classifySource(c, facts));
|
|
190
|
+
const blocked = claims.filter((c) => c.verdict !== 'allowed');
|
|
191
|
+
if (blocked.length > 0) {
|
|
192
|
+
return {
|
|
193
|
+
outcome: 'blocked',
|
|
194
|
+
exit: 1,
|
|
195
|
+
claims,
|
|
196
|
+
reason: `${blocked.length} of ${claims.length} claim(s) cite a source that may not leave this machine`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return { outcome: 'allowed', exit: 0, claims, reason: `all ${claims.length} claim(s) cite a source cleared to go out` };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** What the operator sees. Every blocked claim names its id, its source and its reason. */
|
|
203
|
+
export function renderSourceProvenance(decision: SourceProvenanceDecision): string[] {
|
|
204
|
+
const out: string[] = [];
|
|
205
|
+
const label = decision.outcome === 'allowed' ? 'ALLOWED' : decision.outcome === 'blocked' ? 'BLOCKED' : 'NOT ESTABLISHED';
|
|
206
|
+
for (const c of decision.claims) {
|
|
207
|
+
if (c.verdict === 'allowed') continue;
|
|
208
|
+
out.push(` [${c.verdict}] ${c.id} — ${c.source ?? '(no source)'}: ${c.detail}`);
|
|
209
|
+
}
|
|
210
|
+
out.push(`dz provenance-check: ${label} — ${decision.reason}`);
|
|
211
|
+
if (decision.outcome === 'allowed') {
|
|
212
|
+
// Said on the PASSING path too, because that is the path where it gets forgotten: a green tick
|
|
213
|
+
// here proves where the citations came from, not that the prose is safe.
|
|
214
|
+
out.push(' note: this proves what was CITED. It cannot see a paraphrase with no citation, nor confidential text pasted by hand into an allowed file. Read the draft.');
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
@@ -64,8 +64,8 @@ export function domainMatch(hitDomain: string | null | undefined, wanted: string
|
|
|
64
64
|
return 'none';
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
export interface DomainBoostResult {
|
|
68
|
-
readonly hits: readonly
|
|
67
|
+
export interface DomainBoostResult<T extends RecallHit = RecallHit> {
|
|
68
|
+
readonly hits: readonly T[];
|
|
69
69
|
/** How many hits MATCHED the domain (exactly / relatedly). */
|
|
70
70
|
readonly exact: number;
|
|
71
71
|
readonly related: number;
|
|
@@ -88,7 +88,14 @@ export interface DomainBoostResult {
|
|
|
88
88
|
* is deterministic — the same input always yields the same output, which is what
|
|
89
89
|
* makes it testable at all.
|
|
90
90
|
*/
|
|
91
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Generic over the hit type so EXTRA FIELDS SURVIVE the boost. Narrowing to `RecallHit` silently
|
|
93
|
+
* dropped `similarity` from a `HybridHit`, and the closeness a reader is meant to act on vanished
|
|
94
|
+
* the moment `--domain` was passed. Note the deliberate asymmetry with `relevance`, which the CLI
|
|
95
|
+
* nulls under a boost because the boost invalidates the RRF ordering: closeness is
|
|
96
|
+
* ORDER-INDEPENDENT, so it stays populated.
|
|
97
|
+
*/
|
|
98
|
+
export function applyDomainBoost<T extends RecallHit>(hits: readonly T[], wanted: string | null | undefined): DomainBoostResult<T> {
|
|
92
99
|
const target = normalizeDomain(wanted);
|
|
93
100
|
if (target === '' || hits.length === 0) {
|
|
94
101
|
return { hits, exact: 0, related: 0, moved: 0, noMatches: true };
|
|
@@ -180,3 +180,63 @@ export function renderHookContext(selection: HookSelection): string {
|
|
|
180
180
|
const lines = selection.hits.map((h) => ` - [${h.score.toFixed(2)}${h.domain ? ` / ${h.domain}` : ''}] ${h.pattern}`);
|
|
181
181
|
return `Learned lessons that match this prompt (dz recall, relevance ≥ ${selection.floor.toFixed(2)}):\n${lines.join('\n')}`;
|
|
182
182
|
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The closeness token for one recall row: `sim=0.67▲` above the floor, `sim=0.29▽` below it,
|
|
186
|
+
* `sim=—` when closeness was not measured for this row.
|
|
187
|
+
*
|
|
188
|
+
* The marker is the floor comparison DONE FOR THE READER, against the same per-language floors the
|
|
189
|
+
* hook already trusts — calibrated 2026-07-09 on a 32-probe labeled set, not chosen by feel. A bare
|
|
190
|
+
* cosine means nothing without that table, and the point of this token is a number the reader can
|
|
191
|
+
* act on: `▲` is on-topic, `▽` is "ranked because something had to rank first", `—` is "unmeasured".
|
|
192
|
+
*
|
|
193
|
+
* A lexical-only hit and an engine whose score is not a cosine both get the dash. Substituting a
|
|
194
|
+
* differently-scaled number there is the precise lie this exists to remove.
|
|
195
|
+
*/
|
|
196
|
+
export function closenessLine(similarity: number | undefined, query: unknown, floors?: Partial<RecallFloors>): string {
|
|
197
|
+
if (typeof similarity !== 'number' || !isFinite(similarity)) return 'sim=—';
|
|
198
|
+
const floor = relevanceFloorFor(query, floors);
|
|
199
|
+
return `sim=${showCloseness(similarity, floor)}${similarity >= floor ? '▲' : '▽'}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The cosine, printed with enough digits that it cannot LOOK equal to the floor when it is not.
|
|
204
|
+
*
|
|
205
|
+
* Two rounds of cross-family review (codex `gpt-5.6-sol`, 2026-08-24) landed on opposite sides of one
|
|
206
|
+
* trade-off, and both were right. Comparing before rounding shows `sim=0.50▽` for 0.499 against a
|
|
207
|
+
* 0.50 floor — a figure equal to the floor, marked below it. Comparing after rounding shows
|
|
208
|
+
* `sim=0.50▲` for the same input — a marker that says on-topic for a value the calibrated floor
|
|
209
|
+
* excludes. Choosing a side cannot fix this, because the contradiction lives in the DISPLAY, not in
|
|
210
|
+
* the comparison: two decimals cannot always distinguish a value from its floor.
|
|
211
|
+
*
|
|
212
|
+
* So the comparison stays on the true value — the floors are calibrated on true cosines — and the
|
|
213
|
+
* display widens until the number visibly differs from the floor. 0.499 against 0.50 prints as
|
|
214
|
+
* `sim=0.499▽`: below the floor, and visibly so.
|
|
215
|
+
*/
|
|
216
|
+
function showCloseness(similarity: number, floor: number): string {
|
|
217
|
+
// Widen until the number and the FLOOR differ AS DISPLAYED at the same precision. Comparing the
|
|
218
|
+
// shown value against the raw floor was not enough: 0.5009 against a floor of 0.501 stopped at two
|
|
219
|
+
// digits and printed `0.50`, which reads as the floor once the floor is itself rounded for a human
|
|
220
|
+
// (cross-family review round 3, 2026-08-24).
|
|
221
|
+
for (let digits = 2; digits <= 6; digits++) {
|
|
222
|
+
const shown = similarity.toFixed(digits);
|
|
223
|
+
if (similarity === floor || shown !== floor.toFixed(digits)) return shown;
|
|
224
|
+
}
|
|
225
|
+
// Even at full precision the two render alike — 0.4999999 and 0.5 both print `0.500000`, and the
|
|
226
|
+
// contradiction the widening exists to remove survives the cap (cross-family review round 4,
|
|
227
|
+
// 2026-08-24). At that distance the digits are not the answer: state the RELATION instead. The
|
|
228
|
+
// reader learns "just below the floor", which is exactly what is true and what the marker says.
|
|
229
|
+
// The floor is stated AS IT IS, never rounded: with a floor of 0.4999999 a value of 0.49999995 is
|
|
230
|
+
// above the floor and below 0.50, so `>0.50` was literally false (cross-family review round 5,
|
|
231
|
+
// 2026-08-24). `String(floor)` prints exactly what the comparison used.
|
|
232
|
+
return `${similarity < floor ? '<' : '>'}${String(floor)}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Did anything clear the floor? Used to say so ONCE, in words, instead of per row. */
|
|
236
|
+
export function anyAboveFloor(similarities: readonly (number | undefined)[], query: unknown, floors?: Partial<RecallFloors>): boolean {
|
|
237
|
+
const floor = relevanceFloorFor(query, floors);
|
|
238
|
+
// The TRUE value, matching what each row's marker uses — the footer must never contradict the
|
|
239
|
+
// markers above it, and "nothing clears the floor" printed under a visible ▲ would be worse than
|
|
240
|
+
// either alone.
|
|
241
|
+
return similarities.some((s) => typeof s === 'number' && isFinite(s) && s >= floor);
|
|
242
|
+
}
|