instar 1.3.8 → 1.3.9
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/dist/feedback-factory/processor/similarity.d.ts +24 -0
- package/dist/feedback-factory/processor/similarity.d.ts.map +1 -0
- package/dist/feedback-factory/processor/similarity.js +44 -0
- package/dist/feedback-factory/processor/similarity.js.map +1 -0
- package/package.json +1 -1
- package/scripts/feedback-factory/_py_similarity_ref.py +39 -0
- package/scripts/feedback-factory/similarity-corpus.json +25 -0
- package/scripts/feedback-factory/similarity-parity.mjs +72 -0
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.9.md +27 -0
- package/upgrades/side-effects/feedback-factory-similarity.md +32 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* similarity.ts — TS port of the feedback-factory title-similarity primitives.
|
|
3
|
+
*
|
|
4
|
+
* Part of scar (c) of docs/specs/feedback-factory-migration.md. Byte-exact port
|
|
5
|
+
* of `_tokenize` (:1389) and `_jaccard_similarity` (:1394) from the reference
|
|
6
|
+
* `the-portal/.claude/scripts/feedback-processor.py`. These are the FUZZY match
|
|
7
|
+
* layer that sits on top of the exact-match fingerprint (fingerprint.ts): two
|
|
8
|
+
* reports whose titles overlap enough (Jaccard ≥ threshold) are candidates for
|
|
9
|
+
* the same cluster. The threshold-application logic (0.35 vs the 0.55 fixed-
|
|
10
|
+
* cluster false-merge guard) lives in the clustering driver and is a later
|
|
11
|
+
* increment; this module is the pure, parity-testable similarity primitive.
|
|
12
|
+
*
|
|
13
|
+
* Divergence surface (per spec / convergence): the TOKENIZER. Python
|
|
14
|
+
* `re.findall(r'[a-z0-9]+', text.lower())` is ASCII-only — non-ASCII letters and
|
|
15
|
+
* Unicode digits are token separators, NOT token content. JS `/[a-z0-9]+/g`
|
|
16
|
+
* (no `u`, no `i`) matches the identical ASCII class, so the only residual risk
|
|
17
|
+
* is `.lower()` vs `.toLowerCase()` on characters that then survive the ASCII
|
|
18
|
+
* filter. The parity harness verifies equivalence over the adversarial corpus.
|
|
19
|
+
*/
|
|
20
|
+
/** Port of Python `_tokenize` (:1391): ASCII word tokens of the lowercased text, deduped. */
|
|
21
|
+
export declare function tokenize(text: string): Set<string>;
|
|
22
|
+
/** Port of Python `_jaccard_similarity` (:1394): |A∩B| / |A∪B|, 0 when either side is empty. */
|
|
23
|
+
export declare function jaccardSimilarity(text1: string, text2: string): number;
|
|
24
|
+
//# sourceMappingURL=similarity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"similarity.d.ts","sourceRoot":"","sources":["../../../src/feedback-factory/processor/similarity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,6FAA6F;AAC7F,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAMlD;AAED,gGAAgG;AAChG,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAYtE"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* similarity.ts — TS port of the feedback-factory title-similarity primitives.
|
|
3
|
+
*
|
|
4
|
+
* Part of scar (c) of docs/specs/feedback-factory-migration.md. Byte-exact port
|
|
5
|
+
* of `_tokenize` (:1389) and `_jaccard_similarity` (:1394) from the reference
|
|
6
|
+
* `the-portal/.claude/scripts/feedback-processor.py`. These are the FUZZY match
|
|
7
|
+
* layer that sits on top of the exact-match fingerprint (fingerprint.ts): two
|
|
8
|
+
* reports whose titles overlap enough (Jaccard ≥ threshold) are candidates for
|
|
9
|
+
* the same cluster. The threshold-application logic (0.35 vs the 0.55 fixed-
|
|
10
|
+
* cluster false-merge guard) lives in the clustering driver and is a later
|
|
11
|
+
* increment; this module is the pure, parity-testable similarity primitive.
|
|
12
|
+
*
|
|
13
|
+
* Divergence surface (per spec / convergence): the TOKENIZER. Python
|
|
14
|
+
* `re.findall(r'[a-z0-9]+', text.lower())` is ASCII-only — non-ASCII letters and
|
|
15
|
+
* Unicode digits are token separators, NOT token content. JS `/[a-z0-9]+/g`
|
|
16
|
+
* (no `u`, no `i`) matches the identical ASCII class, so the only residual risk
|
|
17
|
+
* is `.lower()` vs `.toLowerCase()` on characters that then survive the ASCII
|
|
18
|
+
* filter. The parity harness verifies equivalence over the adversarial corpus.
|
|
19
|
+
*/
|
|
20
|
+
/** Port of Python `_tokenize` (:1391): ASCII word tokens of the lowercased text, deduped. */
|
|
21
|
+
export function tokenize(text) {
|
|
22
|
+
// Python: set(re.findall(r'[a-z0-9]+', text.lower())). re.findall returns
|
|
23
|
+
// non-overlapping matches; String.match(//g) is the JS equivalent (null when
|
|
24
|
+
// none → treat as empty). The class is ASCII (no `u` flag), matching Python.
|
|
25
|
+
const matches = text.toLowerCase().match(/[a-z0-9]+/g);
|
|
26
|
+
return new Set(matches ?? []);
|
|
27
|
+
}
|
|
28
|
+
/** Port of Python `_jaccard_similarity` (:1394): |A∩B| / |A∪B|, 0 when either side is empty. */
|
|
29
|
+
export function jaccardSimilarity(text1, text2) {
|
|
30
|
+
const t1 = tokenize(text1);
|
|
31
|
+
const t2 = tokenize(text2);
|
|
32
|
+
if (t1.size === 0 || t2.size === 0)
|
|
33
|
+
return 0.0;
|
|
34
|
+
let intersection = 0;
|
|
35
|
+
for (const tok of t1) {
|
|
36
|
+
if (t2.has(tok))
|
|
37
|
+
intersection++;
|
|
38
|
+
}
|
|
39
|
+
// |A∪B| = |A| + |B| − |A∩B|. Integer division as float — IEEE-754 doubles in
|
|
40
|
+
// both Python and JS, so boundary comparisons (≥0.35, ≥0.55) agree.
|
|
41
|
+
const union = t1.size + t2.size - intersection;
|
|
42
|
+
return intersection / union;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=similarity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"similarity.js","sourceRoot":"","sources":["../../../src/feedback-factory/processor/similarity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,6FAA6F;AAC7F,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,0EAA0E;IAC1E,6EAA6E;IAC7E,6EAA6E;IAC7E,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IACvD,OAAO,IAAI,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;AAChC,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,iBAAiB,CAAC,KAAa,EAAE,KAAa;IAC5D,MAAM,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC3B,MAAM,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC3B,IAAI,EAAE,CAAC,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IAC/C,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,KAAK,MAAM,GAAG,IAAI,EAAE,EAAE,CAAC;QACrB,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,YAAY,EAAE,CAAC;IAClC,CAAC;IACD,6EAA6E;IAC7E,oEAAoE;IACpE,MAAM,KAAK,GAAG,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC,IAAI,GAAG,YAAY,CAAC;IAC/C,OAAO,YAAY,GAAG,KAAK,CAAC;AAC9B,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Parity reference: Jaccard title similarity from the REAL feedback-processor.py.
|
|
3
|
+
|
|
4
|
+
Imports the reference processor without running its CLI main (same approach as
|
|
5
|
+
_py_fingerprint_ref.py) and calls its actual `_jaccard_similarity` over a corpus
|
|
6
|
+
of title PAIRS. Emits JSON {a, b, sim} so the Node harness can diff the TS port.
|
|
7
|
+
|
|
8
|
+
Usage: python3 _py_similarity_ref.py <processor_path> <pairs_corpus_path>
|
|
9
|
+
"""
|
|
10
|
+
import sys
|
|
11
|
+
import json
|
|
12
|
+
import importlib.util
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_processor(path):
|
|
16
|
+
spec = importlib.util.spec_from_file_location("_feedback_processor_ref_sim", path)
|
|
17
|
+
mod = importlib.util.module_from_spec(spec)
|
|
18
|
+
spec.loader.exec_module(mod)
|
|
19
|
+
return mod
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main():
|
|
23
|
+
if len(sys.argv) != 3:
|
|
24
|
+
print("usage: _py_similarity_ref.py <processor_path> <pairs_corpus_path>", file=sys.stderr)
|
|
25
|
+
sys.exit(2)
|
|
26
|
+
processor_path, corpus_path = sys.argv[1], sys.argv[2]
|
|
27
|
+
proc = load_processor(processor_path)
|
|
28
|
+
with open(corpus_path, encoding="utf-8") as f:
|
|
29
|
+
corpus = json.load(f)
|
|
30
|
+
out = []
|
|
31
|
+
for pair in corpus["pairs"]:
|
|
32
|
+
a, b = pair["a"], pair["b"]
|
|
33
|
+
sim = proc._jaccard_similarity(a, b)
|
|
34
|
+
out.append({"a": a, "b": b, "sim": repr(sim)}) # repr() = full-precision float string
|
|
35
|
+
sys.stdout.write(json.dumps(out, ensure_ascii=False))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
main()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Parity corpus of title PAIRS for the Jaccard similarity port. Covers identical / disjoint / partial-overlap / boundary (≈0.35, ≈0.55) / empty / punctuation-only / non-ASCII-dropped cases. Both Python json.load and Node JSON.parse decode identical bytes, so the input is shared.",
|
|
3
|
+
"pairs": [
|
|
4
|
+
{ "a": "GitSync.pull fails intermittently", "b": "GitSync.pull fails intermittently" },
|
|
5
|
+
{ "a": "auth token refresh broken", "b": "completely unrelated database migration" },
|
|
6
|
+
{ "a": "session reaper kills active session", "b": "session reaper kills idle session" },
|
|
7
|
+
{ "a": "the quick brown fox", "b": "the quick red fox" },
|
|
8
|
+
{ "a": "alpha beta gamma delta", "b": "alpha beta epsilon zeta" },
|
|
9
|
+
{ "a": "one two three four five", "b": "one two three six seven" },
|
|
10
|
+
{ "a": "a b c d e f g", "b": "a b c d h i j" },
|
|
11
|
+
{ "a": "tunnel retry exhaustion notify", "b": "tunnel retry backoff" },
|
|
12
|
+
{ "a": "", "b": "non empty title" },
|
|
13
|
+
{ "a": "non empty title", "b": "" },
|
|
14
|
+
{ "a": "", "b": "" },
|
|
15
|
+
{ "a": "!!! ??? ...", "b": "more punctuation: ;;; ///" },
|
|
16
|
+
{ "a": "café résumé naïve", "b": "cafe resume naive" },
|
|
17
|
+
{ "a": "İstanbul ören", "b": "istanbul oren" },
|
|
18
|
+
{ "a": "Straße über", "b": "strasse uber" },
|
|
19
|
+
{ "a": "arabic ٣٤٥ digits", "b": "arabic digits" },
|
|
20
|
+
{ "a": "MixedCASE Tokens HERE", "b": "mixedcase tokens here" },
|
|
21
|
+
{ "a": "duplicate duplicate duplicate word", "b": "duplicate word" },
|
|
22
|
+
{ "a": "v1.2.3 release notes", "b": "v9.9.9 release notes" },
|
|
23
|
+
{ "a": "GitSync.pull deadlock under load", "b": "GitSync push deadlock under heavy load now" }
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* similarity-parity.mjs — LOCAL parity gate for the Jaccard title-similarity port.
|
|
4
|
+
*
|
|
5
|
+
* Runs the REAL reference Python (`_jaccard_similarity` from
|
|
6
|
+
* the-portal/.claude/scripts/feedback-processor.py) and the TS port over the
|
|
7
|
+
* shared pairs corpus and asserts the similarity values match. Like the
|
|
8
|
+
* fingerprint harness this is a LOCAL evidence gate, not a CI test (CI lacks the
|
|
9
|
+
* reference checkout). Values should be bit-identical (same IEEE-754 division);
|
|
10
|
+
* we allow a 1e-12 tolerance only to absorb float-string serialization, and we
|
|
11
|
+
* ALSO assert the threshold decisions (≥0.35, ≥0.55) agree exactly — those are
|
|
12
|
+
* what actually drive clustering.
|
|
13
|
+
*
|
|
14
|
+
* Reference path: PORTAL_PROCESSOR env, else the default the-portal checkout.
|
|
15
|
+
* TS port is imported from the built dist (`npm run build` first).
|
|
16
|
+
*
|
|
17
|
+
* Exit 0 = parity; exit 1 = mismatch.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
27
|
+
|
|
28
|
+
const PROCESSOR = process.env.PORTAL_PROCESSOR
|
|
29
|
+
|| '/Users/justin/Documents/Projects/the-portal/.claude/scripts/feedback-processor.py';
|
|
30
|
+
const CORPUS = path.join(__dirname, 'similarity-corpus.json');
|
|
31
|
+
const PY_REF = path.join(__dirname, '_py_similarity_ref.py');
|
|
32
|
+
const EPS = 1e-12;
|
|
33
|
+
|
|
34
|
+
if (!fs.existsSync(PROCESSOR)) {
|
|
35
|
+
console.error(`[sim-parity] reference processor not found: ${PROCESSOR}`);
|
|
36
|
+
console.error('[sim-parity] set PORTAL_PROCESSOR to the reference feedback-processor.py');
|
|
37
|
+
process.exit(2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const py = spawnSync('python3', [PY_REF, PROCESSOR, CORPUS], { encoding: 'utf8' });
|
|
41
|
+
if (py.status !== 0) {
|
|
42
|
+
console.error('[sim-parity] python reference failed:\n', py.stderr || py.stdout);
|
|
43
|
+
process.exit(2);
|
|
44
|
+
}
|
|
45
|
+
const reference = JSON.parse(py.stdout);
|
|
46
|
+
|
|
47
|
+
const distUrl = new URL('file://' + path.join(ROOT, 'dist', 'feedback-factory', 'processor', 'similarity.js'));
|
|
48
|
+
const { jaccardSimilarity } = await import(distUrl.href);
|
|
49
|
+
|
|
50
|
+
const decide = (v) => (v >= 0.55 ? 'fixed-merge' : v >= 0.35 ? 'merge' : 'no-merge');
|
|
51
|
+
|
|
52
|
+
let mismatches = 0;
|
|
53
|
+
for (const ref of reference) {
|
|
54
|
+
const pySim = Number(ref.sim);
|
|
55
|
+
const tsSim = jaccardSimilarity(ref.a, ref.b);
|
|
56
|
+
const valueOk = Math.abs(tsSim - pySim) < EPS;
|
|
57
|
+
const decisionOk = decide(tsSim) === decide(pySim);
|
|
58
|
+
if (!valueOk || !decisionOk) {
|
|
59
|
+
mismatches++;
|
|
60
|
+
console.error(`[MISMATCH] a=${JSON.stringify(ref.a)} b=${JSON.stringify(ref.b)}`);
|
|
61
|
+
console.error(` python=${pySim} (${decide(pySim)}) ts=${tsSim} (${decide(tsSim)})`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const total = reference.length;
|
|
66
|
+
if (mismatches === 0) {
|
|
67
|
+
console.error(`[sim-parity] ✓ ${total}/${total} similarity values + threshold decisions match the reference Python.`);
|
|
68
|
+
process.exit(0);
|
|
69
|
+
} else {
|
|
70
|
+
console.error(`[sim-parity] ✗ ${mismatches}/${total} mismatches — the TS port diverges from the reference.`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-05-27T04:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-05-27T04:57:52.877Z",
|
|
5
|
+
"instarVersion": "1.3.9",
|
|
6
6
|
"entryCount": 192,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
Second increment of the **Feedback Factory Migration** (Dawn → Echo; spec `docs/specs/feedback-factory-migration.md`, approved). Ports the **fuzzy title-similarity primitives** — `_tokenize` + `_jaccard_similarity` — from the reference Python (`the-portal/.claude/scripts/feedback-processor.py`) to TypeScript at `src/feedback-factory/processor/similarity.ts`.
|
|
8
|
+
|
|
9
|
+
These sit on top of the exact-match fingerprinter shipped in the previous increment: the fingerprint groups identically-normalized reports, and Jaccard similarity is the fuzzy layer that decides whether two *differently-worded* titles overlap enough to be the same bug (the input to the clustering merge thresholds). Still an internal building block — **not wired into any route or job yet**, so no behavioral change.
|
|
10
|
+
|
|
11
|
+
## What to Tell Your User
|
|
12
|
+
|
|
13
|
+
- More groundwork for moving the feedback system in-house — the piece that recognizes two differently-worded reports as the same underlying issue.
|
|
14
|
+
- Same discipline as before: I proved my rewrite produces the same similarity scores AND the same group/don't-group decisions as Dawn's original, across 20 title pairs including the tricky non-English-text ones.
|
|
15
|
+
|
|
16
|
+
## Summary of New Capabilities
|
|
17
|
+
|
|
18
|
+
| Capability | How to Use |
|
|
19
|
+
|-----------|-----------|
|
|
20
|
+
| Title-similarity primitives (TS port) | Internal module `src/feedback-factory/processor/similarity.ts` — not yet wired |
|
|
21
|
+
| Similarity parity harness | `node scripts/feedback-factory/similarity-parity.mjs` (local; set `PORTAL_PROCESSOR`) |
|
|
22
|
+
|
|
23
|
+
## Evidence
|
|
24
|
+
|
|
25
|
+
- **Parity vs the real reference Python:** ran `_jaccard_similarity` from the reference processor and the TS port over 20 title pairs (identical / disjoint / partial-overlap / near the 0.35 and 0.55 merge thresholds / empty / punctuation-only / non-ASCII). Result: **20/20 match** — both the raw similarity value and the merge-threshold decision (the math is bit-identical IEEE-754 division; the harness asserts the *decision* too, since that is what actually drives clustering).
|
|
26
|
+
- **ASCII-tokenizer fidelity:** the reference tokenizer is ASCII-only (`[a-z0-9]+` after lowercasing), so the port uses the identical ASCII class — non-English letters and non-ASCII digits are separators in both, verified by the corpus.
|
|
27
|
+
- **CI anchors:** unit tests assert exact ratios (0.6, 0.4) and the threshold ordering, so a tokenizer/division regression fails in CI even without the reference checkout.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Side-Effects Review — feedback-factory similarity primitives (Phase 1, increment 2)
|
|
2
|
+
|
|
3
|
+
**Slug:** `feedback-factory-similarity`
|
|
4
|
+
**Date:** `2026-05-26`
|
|
5
|
+
**Author:** Echo
|
|
6
|
+
**Spec:** `docs/specs/feedback-factory-migration.md` (converged v2, approved by Justin 2026-05-26)
|
|
7
|
+
**Scope:** The fuzzy title-similarity primitives — `_tokenize` (:1389) and `_jaccard_similarity` (:1394) of the reference processor — which sit on top of the exact-match fingerprint (increment 1) and underpin scar (c)'s false-merge guard. The threshold-application/clustering driver and the other scars are subsequent increments.
|
|
8
|
+
|
|
9
|
+
## Summary of the change
|
|
10
|
+
|
|
11
|
+
Ports `_tokenize` + `_jaccard_similarity` from `the-portal/.claude/scripts/feedback-processor.py` to `src/feedback-factory/processor/similarity.ts`. Pure functions, no I/O. **Not wired into any route/job yet** — no behavioral change. Adds the similarity parity harness (`scripts/feedback-factory/similarity-parity.mjs` + `_py_similarity_ref.py` + `similarity-corpus.json`) and Tier-1 unit tests.
|
|
12
|
+
|
|
13
|
+
## Equivalence verification
|
|
14
|
+
|
|
15
|
+
- **20/20 title pairs match the reference Python** — both the raw similarity value (1e-12 tolerance for float-string serialization only; the math is bit-identical IEEE-754 division) AND the threshold decisions (`≥0.55` fixed-merge, `≥0.35` merge, else no-merge), which are what actually drive clustering.
|
|
16
|
+
- The tokenizer is ASCII-only in the reference (`re.findall(r'[a-z0-9]+', text.lower())`), so the JS port uses the identical ASCII class `/[a-z0-9]+/g` (no `u`, no `i`). Non-ASCII letters and Unicode digits are separators in both — verified by corpus entries (café→{caf}, arabic digits dropped, Straße→{stra,e}).
|
|
17
|
+
|
|
18
|
+
## Seven-dimension review
|
|
19
|
+
|
|
20
|
+
1. **Over/under-reach** — Pure deterministic functions, no I/O, no global state, not imported by any runtime path. Cannot affect existing behavior. Risk is equivalence-to-reference, addressed above.
|
|
21
|
+
2. **Level-of-abstraction fit** — Processor-logic layer (`src/feedback-factory/processor/`), alongside the fingerprint port. Correct home.
|
|
22
|
+
3. **Signal vs Authority** — N/A; pure computation. The threshold constants that turn a similarity score into a merge decision live in the (later) clustering driver, where the curated-lifecycle authority also lives.
|
|
23
|
+
4. **Interactions** — None. New isolated module; nothing imports it yet. Parity scripts are LOCAL-only (external reference path via `PORTAL_PROCESSOR`), no CI dependency on the reference checkout.
|
|
24
|
+
5. **Rollback cost** — Trivial: delete the module + tests + scripts. No data, no migration, no wiring.
|
|
25
|
+
6. **Migration parity** — N/A. New internal library code; touches no agent-installed file.
|
|
26
|
+
7. **Failure modes** — (a) Port diverges from reference → caught by the parity harness (20/20) + golden-shaped unit assertions in CI. (b) Float boundary divergence at exactly 0.35/0.55 → mitigated by identical IEEE-754 division + the parity harness asserting threshold DECISIONS, not just values. (c) Reference path absent → harness exits 2 with a clear message (local gate, not CI).
|
|
27
|
+
|
|
28
|
+
## Tests
|
|
29
|
+
|
|
30
|
+
- Tier-1 unit (CI): `tests/unit/feedback-factory/similarity.test.ts` — `tokenize` (lowercase/split/dedup, ASCII-only behavior, empty/punctuation), `jaccardSimilarity` (identical=1.0, disjoint=0.0, empty-guard=0.0, exact ∩/∪ ratios, threshold ordering). 8 tests.
|
|
31
|
+
- Parity (local gate, evidence): `scripts/feedback-factory/similarity-parity.mjs` → **20/20** values + threshold decisions identical to the reference Python.
|
|
32
|
+
- No integration/E2E this increment: not yet wired to a route/job; those tiers attach when the clustering driver + receiver land. Reasoned decision, documented.
|