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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.8",
3
+ "version": "1.3.9",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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:23:30.515Z",
5
- "instarVersion": "1.3.8",
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.