instar 1.3.6 → 1.3.7
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/fingerprint.d.ts +45 -0
- package/dist/feedback-factory/processor/fingerprint.d.ts.map +1 -0
- package/dist/feedback-factory/processor/fingerprint.js +97 -0
- package/dist/feedback-factory/processor/fingerprint.js.map +1 -0
- package/package.json +1 -1
- package/scripts/feedback-factory/_py_fingerprint_ref.py +43 -0
- package/scripts/feedback-factory/fingerprint-corpus.json +38 -0
- package/scripts/feedback-factory/fingerprint-parity.mjs +69 -0
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.7.md +29 -0
- package/upgrades/side-effects/feedback-factory-fingerprint.md +36 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fingerprint.ts — TypeScript port of the feedback-factory dedup fingerprinter.
|
|
3
|
+
*
|
|
4
|
+
* Scar (b) of docs/specs/feedback-factory-migration.md. Byte-exact port of
|
|
5
|
+
* `compute_fingerprint` + its `extract_component` dependency from the reference
|
|
6
|
+
* `the-portal/.claude/scripts/feedback-processor.py` (verified against Dawn's
|
|
7
|
+
* line refs 2026-05-26: compute_fingerprint :227, extract_component :191,
|
|
8
|
+
* SEVERITY sets :182/:186). This decides whether two reports are the SAME bug —
|
|
9
|
+
* the single most correctness-critical port in the migration.
|
|
10
|
+
*
|
|
11
|
+
* Equivalence to the Python is proven EMPIRICALLY by the parity harness
|
|
12
|
+
* (scripts/feedback-factory/fingerprint-parity.mjs), not by reasoning alone:
|
|
13
|
+
* Python and JS differ on regex character classes (`\d`/`\w`/`\b` are Unicode
|
|
14
|
+
* in Python without re.ASCII, ASCII in JS), `.lower()` vs `.toLowerCase()`, and
|
|
15
|
+
* whitespace stripping. The port below mirrors Python's Unicode semantics where
|
|
16
|
+
* JS can (`u` flag + Unicode property escapes); the harness's adversarial corpus
|
|
17
|
+
* (non-ASCII digits, em-dash, NBSP, NFC/NFD, Turkish-İ/ß) is the gate that
|
|
18
|
+
* catches any residual divergence. Do NOT "simplify" a regex without re-running
|
|
19
|
+
* the harness — that is exactly how the silent history-fork ships.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Port of Python `extract_component(title)` (:191). Strips a leading [TAG] and a
|
|
23
|
+
* whitelisted severity prefix, then returns the first dotted identifier
|
|
24
|
+
* (e.g. "gitsync.pull") or the first word, lowercased; "" if none.
|
|
25
|
+
*
|
|
26
|
+
* Python `\w` is Unicode (no re.ASCII); JS `\w` is ASCII. We use `\p{L}\p{N}_`
|
|
27
|
+
* under the `u` flag to mirror Python's Unicode word-char semantics. `re.match`
|
|
28
|
+
* is start-anchored — the `^` anchors below make `.match()` equivalent.
|
|
29
|
+
*/
|
|
30
|
+
export declare function extractComponent(title: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Port of Python `compute_fingerprint(type, title, component="")` (:227).
|
|
33
|
+
* Normalizes the title (lowercase; collapse versions / hashes / bare ints /
|
|
34
|
+
* whitespace) and returns the first 32 hex chars of SHA-256 over
|
|
35
|
+
* `type|component|normalized_title`. `v1.1.0` and `v1.1.1` collapse to the same
|
|
36
|
+
* fingerprint → same bug.
|
|
37
|
+
*
|
|
38
|
+
* Python `re.sub` is global → JS needs the `g` flag. Python `\d`/`\w`/`\b` are
|
|
39
|
+
* Unicode (no re.ASCII): `\d`→`\p{Nd}`, `\w`→`[\p{L}\p{N}_]` under `u`. JS has no
|
|
40
|
+
* native Unicode `\b`; the hash/int strips below keep ASCII-hex/`\p{Nd}` content
|
|
41
|
+
* with `\b` — the parity harness verifies the boundary behavior against the
|
|
42
|
+
* Python on the adversarial corpus.
|
|
43
|
+
*/
|
|
44
|
+
export declare function computeFingerprint(type: string, title: string, component?: string): string;
|
|
45
|
+
//# sourceMappingURL=fingerprint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../../../src/feedback-factory/processor/fingerprint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAaH;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAwBtD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,SAAK,GAAG,MAAM,CAuBtF"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fingerprint.ts — TypeScript port of the feedback-factory dedup fingerprinter.
|
|
3
|
+
*
|
|
4
|
+
* Scar (b) of docs/specs/feedback-factory-migration.md. Byte-exact port of
|
|
5
|
+
* `compute_fingerprint` + its `extract_component` dependency from the reference
|
|
6
|
+
* `the-portal/.claude/scripts/feedback-processor.py` (verified against Dawn's
|
|
7
|
+
* line refs 2026-05-26: compute_fingerprint :227, extract_component :191,
|
|
8
|
+
* SEVERITY sets :182/:186). This decides whether two reports are the SAME bug —
|
|
9
|
+
* the single most correctness-critical port in the migration.
|
|
10
|
+
*
|
|
11
|
+
* Equivalence to the Python is proven EMPIRICALLY by the parity harness
|
|
12
|
+
* (scripts/feedback-factory/fingerprint-parity.mjs), not by reasoning alone:
|
|
13
|
+
* Python and JS differ on regex character classes (`\d`/`\w`/`\b` are Unicode
|
|
14
|
+
* in Python without re.ASCII, ASCII in JS), `.lower()` vs `.toLowerCase()`, and
|
|
15
|
+
* whitespace stripping. The port below mirrors Python's Unicode semantics where
|
|
16
|
+
* JS can (`u` flag + Unicode property escapes); the harness's adversarial corpus
|
|
17
|
+
* (non-ASCII digits, em-dash, NBSP, NFC/NFD, Turkish-İ/ß) is the gate that
|
|
18
|
+
* catches any residual divergence. Do NOT "simplify" a regex without re-running
|
|
19
|
+
* the harness — that is exactly how the silent history-fork ships.
|
|
20
|
+
*/
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
// Python: SEVERITY_PREFIXES (:182) / SEVERITY_PHRASES (:186) — byte-for-byte.
|
|
23
|
+
const SEVERITY_PREFIXES = new Set([
|
|
24
|
+
'DEGRADATION', 'ALERT', 'VERIFIED', 'CRITICAL', 'RECURRING',
|
|
25
|
+
'PATTERN', 'WARNING', 'BUG', 'FEATURE', 'WONTFIX',
|
|
26
|
+
]);
|
|
27
|
+
const SEVERITY_PHRASES = new Set([
|
|
28
|
+
'VERIFIED FIX', 'WONTFIX', 'BUG REPORT',
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* Port of Python `extract_component(title)` (:191). Strips a leading [TAG] and a
|
|
32
|
+
* whitelisted severity prefix, then returns the first dotted identifier
|
|
33
|
+
* (e.g. "gitsync.pull") or the first word, lowercased; "" if none.
|
|
34
|
+
*
|
|
35
|
+
* Python `\w` is Unicode (no re.ASCII); JS `\w` is ASCII. We use `\p{L}\p{N}_`
|
|
36
|
+
* under the `u` flag to mirror Python's Unicode word-char semantics. `re.match`
|
|
37
|
+
* is start-anchored — the `^` anchors below make `.match()` equivalent.
|
|
38
|
+
*/
|
|
39
|
+
export function extractComponent(title) {
|
|
40
|
+
if (!title)
|
|
41
|
+
return '';
|
|
42
|
+
let t = title;
|
|
43
|
+
// Python :208 — strip [TAG] prefix: re.sub(r'^\[([A-Z_]+)\]\s*', '', t)
|
|
44
|
+
t = t.replace(/^\[([A-Z_]+)\]\s*/u, '');
|
|
45
|
+
// Python :211 — strip a whitelisted severity prefix only (never arbitrary
|
|
46
|
+
// uppercase-colon): re.match(r'^([A-Z][A-Z\s]*?):\s*', t)
|
|
47
|
+
const sev = t.match(/^([A-Z][A-Z\s]*?):\s*/u);
|
|
48
|
+
if (sev) {
|
|
49
|
+
const candidate = sev[1].trim();
|
|
50
|
+
if (SEVERITY_PHRASES.has(candidate) || SEVERITY_PREFIXES.has(candidate)) {
|
|
51
|
+
t = t.slice(sev[0].length);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Python :218 — dotted identifier first: ^([A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+)
|
|
55
|
+
const dotted = t.match(/^([A-Za-z][\p{L}\p{N}_]*(?:\.[A-Za-z][\p{L}\p{N}_]*)+)/u);
|
|
56
|
+
if (dotted)
|
|
57
|
+
return dotted[1].toLowerCase();
|
|
58
|
+
// Python :223 — fallback to first word: ^([A-Za-z][\w]+)
|
|
59
|
+
const word = t.match(/^([A-Za-z][\p{L}\p{N}_]+)/u);
|
|
60
|
+
return word ? word[1].toLowerCase() : '';
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Port of Python `compute_fingerprint(type, title, component="")` (:227).
|
|
64
|
+
* Normalizes the title (lowercase; collapse versions / hashes / bare ints /
|
|
65
|
+
* whitespace) and returns the first 32 hex chars of SHA-256 over
|
|
66
|
+
* `type|component|normalized_title`. `v1.1.0` and `v1.1.1` collapse to the same
|
|
67
|
+
* fingerprint → same bug.
|
|
68
|
+
*
|
|
69
|
+
* Python `re.sub` is global → JS needs the `g` flag. Python `\d`/`\w`/`\b` are
|
|
70
|
+
* Unicode (no re.ASCII): `\d`→`\p{Nd}`, `\w`→`[\p{L}\p{N}_]` under `u`. JS has no
|
|
71
|
+
* native Unicode `\b`; the hash/int strips below keep ASCII-hex/`\p{Nd}` content
|
|
72
|
+
* with `\b` — the parity harness verifies the boundary behavior against the
|
|
73
|
+
* Python on the adversarial corpus.
|
|
74
|
+
*/
|
|
75
|
+
export function computeFingerprint(type, title, component = '') {
|
|
76
|
+
if (!component) {
|
|
77
|
+
component = extractComponent(title);
|
|
78
|
+
}
|
|
79
|
+
let normalized = title.toLowerCase();
|
|
80
|
+
// :237 — re.sub(r'v?\d+\.\d+\.\d+(-[\w.]+)?', 'vN', s). Python `\d`/`\w` are
|
|
81
|
+
// Unicode → `\p{Nd}` / `[\p{L}\p{N}_.]`. No `\b` here, so no boundary concern.
|
|
82
|
+
normalized = normalized.replace(/v?\p{Nd}+\.\p{Nd}+\.\p{Nd}+(-[\p{L}\p{N}_.]+)?/gu, 'vN');
|
|
83
|
+
// :238 — re.sub(r'\b[0-9a-f]{8,}\b', 'HASH', s). Python `\b` is a UNICODE word
|
|
84
|
+
// boundary; JS `\b` is ASCII-only. Emulate with lookarounds over the Unicode
|
|
85
|
+
// word-char set `[\p{L}\p{N}_]` (≈ Python `\w`) so adjacency to a non-ASCII
|
|
86
|
+
// letter/digit behaves like the reference.
|
|
87
|
+
normalized = normalized.replace(/(?<![\p{L}\p{N}_])[0-9a-f]{8,}(?![\p{L}\p{N}_])/gu, 'HASH');
|
|
88
|
+
// :239 — re.sub(r'\b\d+\b', 'N', s). Same Unicode-boundary emulation; `\d` →
|
|
89
|
+
// `\p{Nd}` (Python Unicode digits, e.g. arabic-indic ٥, which ASCII `\b` misses).
|
|
90
|
+
normalized = normalized.replace(/(?<![\p{L}\p{N}_])\p{Nd}+(?![\p{L}\p{N}_])/gu, 'N');
|
|
91
|
+
// :240 — re.sub(r'\s+', ' ', s).strip()
|
|
92
|
+
normalized = normalized.replace(/\s+/gu, ' ').trim();
|
|
93
|
+
// :242-243 — sha256 over UTF-8 bytes of "type|component|normalized", 32 hex chars
|
|
94
|
+
const raw = `${type.toLowerCase()}|${component.toLowerCase()}|${normalized}`;
|
|
95
|
+
return createHash('sha256').update(raw, 'utf8').digest('hex').slice(0, 32);
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=fingerprint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../../../src/feedback-factory/processor/fingerprint.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Parity reference: compute fingerprints using the REAL feedback-processor.py.
|
|
3
|
+
|
|
4
|
+
Imports the reference processor (the-portal/.claude/scripts/feedback-processor.py)
|
|
5
|
+
WITHOUT running its CLI main (it has an `if __name__ == '__main__'` guard; loading
|
|
6
|
+
under a different module name keeps that dormant), then calls its actual
|
|
7
|
+
compute_fingerprint over the shared corpus. Emits JSON to stdout so the Node
|
|
8
|
+
harness can diff the TS port against this ground truth.
|
|
9
|
+
|
|
10
|
+
Usage: python3 _py_fingerprint_ref.py <processor_path> <corpus_path>
|
|
11
|
+
"""
|
|
12
|
+
import sys
|
|
13
|
+
import json
|
|
14
|
+
import importlib.util
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_processor(path):
|
|
18
|
+
spec = importlib.util.spec_from_file_location("_feedback_processor_ref", path)
|
|
19
|
+
mod = importlib.util.module_from_spec(spec)
|
|
20
|
+
spec.loader.exec_module(mod) # safe: top-level is imports + constants + defs; DB work is inside cmd_* only
|
|
21
|
+
return mod
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main():
|
|
25
|
+
if len(sys.argv) != 3:
|
|
26
|
+
print("usage: _py_fingerprint_ref.py <processor_path> <corpus_path>", file=sys.stderr)
|
|
27
|
+
sys.exit(2)
|
|
28
|
+
processor_path, corpus_path = sys.argv[1], sys.argv[2]
|
|
29
|
+
proc = load_processor(processor_path)
|
|
30
|
+
with open(corpus_path, encoding="utf-8") as f:
|
|
31
|
+
corpus = json.load(f)
|
|
32
|
+
out = []
|
|
33
|
+
for case in corpus["cases"]:
|
|
34
|
+
t = case["type"]
|
|
35
|
+
title = case["title"]
|
|
36
|
+
component = case.get("component", "")
|
|
37
|
+
fp = proc.compute_fingerprint(t, title, component)
|
|
38
|
+
out.append({"type": t, "title": title, "component": component, "fp": fp})
|
|
39
|
+
sys.stdout.write(json.dumps(out, ensure_ascii=False))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
main()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Parity corpus for the feedback fingerprint port. Real cases + the adversarial Unicode/encoding/whitespace cases the convergence flagged. Each entry: {type, title, component?}. The parity harness asserts the TS port produces byte-identical fingerprints to the reference Python (the-portal/.claude/scripts/feedback-processor.py) for every entry. Tricky code points are written as \\u escapes so both json.load (Python) and JSON.parse (Node) decode identical bytes.",
|
|
3
|
+
"cases": [
|
|
4
|
+
{ "type": "bug", "title": "GitSync.pull fails intermittently" },
|
|
5
|
+
{ "type": "bug", "title": "StateManager.listSessions returns stale data" },
|
|
6
|
+
{ "type": "bug", "title": "auth token refresh broken in v1.1.0" },
|
|
7
|
+
{ "type": "bug", "title": "auth token refresh broken in v1.1.1" },
|
|
8
|
+
{ "type": "bug", "title": "auth token refresh broken in v2.13.4-beta.2" },
|
|
9
|
+
{ "type": "feature", "title": "[DEGRADATION] StateManager.listSessions timeout" },
|
|
10
|
+
{ "type": "bug", "title": "[ALERT] GitSync.pull deadlock" },
|
|
11
|
+
{ "type": "bug", "title": "CRITICAL: auth refresh broken" },
|
|
12
|
+
{ "type": "bug", "title": "VERIFIED FIX: GitSync.pull race" },
|
|
13
|
+
{ "type": "bug", "title": "BUG REPORT: session reaper kills active session" },
|
|
14
|
+
{ "type": "bug", "title": "WONTFIX: legacy thing" },
|
|
15
|
+
{ "type": "bug", "title": "NOTASEVERITY: should not be stripped" },
|
|
16
|
+
{ "type": "bug", "title": "commit abc123def4567890 broke the build" },
|
|
17
|
+
{ "type": "bug", "title": "deadbeefcafe1234 hash in title" },
|
|
18
|
+
{ "type": "bug", "title": "retry 5 times then 12 failures at 300 seconds" },
|
|
19
|
+
{ "type": "bug", "title": "lowercase already and simple" },
|
|
20
|
+
{ "type": "bug", "title": "Title With Irregular Spacing" },
|
|
21
|
+
{ "type": "bug", "title": " leading and trailing whitespace " },
|
|
22
|
+
{ "type": "bug", "title": "" },
|
|
23
|
+
{ "type": "bug", "title": "[TAG] only a tag then word" },
|
|
24
|
+
{ "type": "bug", "title": "Component.sub.deep.path.identifier works" },
|
|
25
|
+
{ "type": "BUG", "title": "Type Case BUG vs bug", "component": "Explicit.Component" },
|
|
26
|
+
{ "type": "bug", "title": "version ٣.١.٠ with arabic-indic digits" },
|
|
27
|
+
{ "type": "bug", "title": "bare arabic-indic ٥ digit here" },
|
|
28
|
+
{ "type": "bug", "title": "em dash — separates words" },
|
|
29
|
+
{ "type": "bug", "title": "non breaking space title" },
|
|
30
|
+
{ "type": "bug", "title": "café composed NFC" },
|
|
31
|
+
{ "type": "bug", "title": "café decomposed NFD" },
|
|
32
|
+
{ "type": "bug", "title": "İstanbul Turkish dotted I" },
|
|
33
|
+
{ "type": "bug", "title": "ınternal Turkish dotless i" },
|
|
34
|
+
{ "type": "bug", "title": "Straße German eszett" },
|
|
35
|
+
{ "type": "bug", "title": "tab\tand\nnewline whitespace" },
|
|
36
|
+
{ "type": "bug", "title": "MixedCASE GitSync.Pull AND v3.4.5 plus abcdef1234567 hash" }
|
|
37
|
+
]
|
|
38
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* fingerprint-parity.mjs — LOCAL parity gate for the feedback fingerprint port.
|
|
4
|
+
*
|
|
5
|
+
* Runs the REAL reference Python (the-portal/.claude/scripts/feedback-processor.py
|
|
6
|
+
* compute_fingerprint) and the TS port over the shared corpus and asserts every
|
|
7
|
+
* fingerprint is byte-identical. This is the empirical equivalence gate the spec
|
|
8
|
+
* requires — it is intentionally NOT a CI test (CI doesn't have the reference
|
|
9
|
+
* checkout); it is run locally and its output recorded as evidence, and re-run
|
|
10
|
+
* whenever the port's regexes/normalization change.
|
|
11
|
+
*
|
|
12
|
+
* Reference path is configurable: PORTAL_PROCESSOR env var, else the default
|
|
13
|
+
* the-portal checkout. The TS port is imported from the built dist (run
|
|
14
|
+
* `npm run build` first).
|
|
15
|
+
*
|
|
16
|
+
* Exit 0 = 100% parity; exit 1 = at least one mismatch (printed).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
26
|
+
|
|
27
|
+
const PROCESSOR = process.env.PORTAL_PROCESSOR
|
|
28
|
+
|| '/Users/justin/Documents/Projects/the-portal/.claude/scripts/feedback-processor.py';
|
|
29
|
+
const CORPUS = path.join(__dirname, 'fingerprint-corpus.json');
|
|
30
|
+
const PY_REF = path.join(__dirname, '_py_fingerprint_ref.py');
|
|
31
|
+
|
|
32
|
+
if (!fs.existsSync(PROCESSOR)) {
|
|
33
|
+
console.error(`[parity] reference processor not found: ${PROCESSOR}`);
|
|
34
|
+
console.error('[parity] set PORTAL_PROCESSOR to the reference feedback-processor.py');
|
|
35
|
+
process.exit(2);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Reference fingerprints from the real Python.
|
|
39
|
+
const py = spawnSync('python3', [PY_REF, PROCESSOR, CORPUS], { encoding: 'utf8' });
|
|
40
|
+
if (py.status !== 0) {
|
|
41
|
+
console.error('[parity] python reference failed:\n', py.stderr || py.stdout);
|
|
42
|
+
process.exit(2);
|
|
43
|
+
}
|
|
44
|
+
const reference = JSON.parse(py.stdout);
|
|
45
|
+
|
|
46
|
+
// TS port from the built dist.
|
|
47
|
+
const distUrl = new URL('file://' + path.join(ROOT, 'dist', 'feedback-factory', 'processor', 'fingerprint.js'));
|
|
48
|
+
const { computeFingerprint } = await import(distUrl.href);
|
|
49
|
+
|
|
50
|
+
let mismatches = 0;
|
|
51
|
+
for (const ref of reference) {
|
|
52
|
+
const got = computeFingerprint(ref.type, ref.title, ref.component);
|
|
53
|
+
const ok = got === ref.fp;
|
|
54
|
+
if (!ok) {
|
|
55
|
+
mismatches++;
|
|
56
|
+
console.error(`[MISMATCH] type=${JSON.stringify(ref.type)} title=${JSON.stringify(ref.title)} component=${JSON.stringify(ref.component)}`);
|
|
57
|
+
console.error(` python=${ref.fp}`);
|
|
58
|
+
console.error(` ts =${got}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const total = reference.length;
|
|
63
|
+
if (mismatches === 0) {
|
|
64
|
+
console.error(`[parity] ✓ ${total}/${total} fingerprints byte-identical to the reference Python.`);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
} else {
|
|
67
|
+
console.error(`[parity] ✗ ${mismatches}/${total} mismatches — the TS port diverges from the reference.`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-05-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-05-27T04:08:37.598Z",
|
|
5
|
+
"instarVersion": "1.3.7",
|
|
6
6
|
"entryCount": 192,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
First increment of the **Feedback Factory Migration** (Dawn → Echo; spec `docs/specs/feedback-factory-migration.md`, approved). Ports the dedup **fingerprinter** — scar (b), the single most correctness-critical piece of the "sorting brain" that decides whether two reports describe the same bug — from the reference Python (`the-portal/.claude/scripts/feedback-processor.py`) to TypeScript at `src/feedback-factory/processor/fingerprint.ts`.
|
|
8
|
+
|
|
9
|
+
This is an internal building block: it is **not wired into any route, job, or runtime path yet**, so there is no behavioral change for any agent or user. It establishes the new `src/feedback-factory/` module and — importantly — the **parity-harness methodology** (`scripts/feedback-factory/fingerprint-parity.mjs`) that runs the real reference Python and asserts the TS port is byte-identical. Every subsequent scar port reuses this harness.
|
|
10
|
+
|
|
11
|
+
## What to Tell Your User
|
|
12
|
+
|
|
13
|
+
- This is groundwork, not a feature you'll notice yet — the first verified piece of moving the feedback system to where it's built.
|
|
14
|
+
- The interesting part: I proved my rewrite produces exactly the same results as the original, character-for-character, including the tricky non-English-text cases — so nothing in the bug history can quietly drift as the move proceeds.
|
|
15
|
+
|
|
16
|
+
## Summary of New Capabilities
|
|
17
|
+
|
|
18
|
+
| Capability | How to Use |
|
|
19
|
+
|-----------|-----------|
|
|
20
|
+
| Feedback fingerprinter (TS port) | Internal module `src/feedback-factory/processor/fingerprint.ts` — not yet wired; foundation for the processor job |
|
|
21
|
+
| Parity harness | `node scripts/feedback-factory/fingerprint-parity.mjs` (local; set `PORTAL_PROCESSOR` to the reference) |
|
|
22
|
+
|
|
23
|
+
## Evidence
|
|
24
|
+
|
|
25
|
+
The byte-exact port is the whole risk, so it was verified empirically, not by reasoning:
|
|
26
|
+
|
|
27
|
+
- **Parity vs the real reference Python:** ran `compute_fingerprint` from `the-portal/.claude/scripts/feedback-processor.py` and the TS port over a 33-entry corpus (real titles + adversarial Unicode/encoding/whitespace cases). Result: **33/33 byte-identical**.
|
|
28
|
+
- **The harness caught a real divergence before it could ship:** the first cut diverged on a bare arabic-indic digit, because Python's `\b\d+\b` is a *Unicode* word boundary while JavaScript's `\b` is ASCII-only — so the digit wasn't collapsed. Fixed by emulating the Unicode boundary with lookarounds; re-ran → 33/33. This is exactly the silent-history-fork hazard the spec's convergence flagged, caught at the foundation.
|
|
29
|
+
- **CI regression anchors:** three golden fingerprint values captured from the reference are asserted in the unit test, so a future regex/encoding regression fails in CI even where the reference checkout isn't present.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Side-Effects Review — feedback-factory fingerprint port (Phase 1, increment 1)
|
|
2
|
+
|
|
3
|
+
**Slug:** `feedback-factory-fingerprint`
|
|
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:** Scar (b) only — the dedup fingerprinter. The first, foundational increment of the Phase-1 processor port. Other scars ((a) evidence gate, (c) false-merge/reopen/cycling, (d) lifecycle partitioning) and the processor-job wiring are subsequent increments.
|
|
8
|
+
|
|
9
|
+
## Summary of the change
|
|
10
|
+
|
|
11
|
+
Ports `compute_fingerprint` + its `extract_component` dependency (and the `SEVERITY_PREFIXES`/`SEVERITY_PHRASES` sets) from the reference `the-portal/.claude/scripts/feedback-processor.py` to TypeScript at `src/feedback-factory/processor/fingerprint.ts`. This is the single most correctness-critical port in the migration — it decides whether two reports are the SAME bug. Adds:
|
|
12
|
+
- `src/feedback-factory/processor/fingerprint.ts` — the port (pure functions, no I/O).
|
|
13
|
+
- `tests/unit/feedback-factory/fingerprint.test.ts` — Tier-1 behavioral + both-sides-of-boundary tests + golden-value CI anchors.
|
|
14
|
+
- `scripts/feedback-factory/fingerprint-parity.mjs` + `_py_fingerprint_ref.py` + `fingerprint-corpus.json` — the LOCAL parity gate that runs the REAL reference Python and asserts byte-identical output.
|
|
15
|
+
|
|
16
|
+
The module is **not wired into anything yet** — it is a standalone, importable building block. No route, no job, no behavior change for any agent or user.
|
|
17
|
+
|
|
18
|
+
## The byte-exact-port hazard and how it's handled
|
|
19
|
+
|
|
20
|
+
Per the spec, Python↔JS diverge on regex character classes, `\b`, `.lower()`, and encoding. The parity harness proved this empirically: a first cut diverged on a bare arabic-indic digit because Python's `\b\d+\b` is a Unicode word boundary while JS `\b` is ASCII-only. Fixed by emulating the Unicode boundary with lookarounds (`(?<![\p{L}\p{N}_])\p{Nd}+(?![\p{L}\p{N}_])`). After the fix: **33/33 corpus entries byte-identical to the reference Python**, including non-ASCII digits, em-dash, NBSP, Turkish İ/ı, German ß, hex hashes, and multi-version titles. Dawn's line-by-line review of the port remains a Phase-1 gate before any production cutover.
|
|
21
|
+
|
|
22
|
+
## Seven-dimension review
|
|
23
|
+
|
|
24
|
+
1. **Over/under-reach** — Pure functions, deterministic, no I/O, no global state. Not imported by any runtime path, so it cannot affect existing behavior. Risk is purely *equivalence to the reference*, addressed below.
|
|
25
|
+
2. **Level-of-abstraction fit** — Lives at the processor-logic layer (`src/feedback-factory/processor/`), the correct home for the ported scar logic per the spec's open/operated architecture.
|
|
26
|
+
3. **Signal vs Authority** — N/A; pure computation, no decision authority.
|
|
27
|
+
4. **Interactions** — None. New isolated module; nothing imports it yet. The parity scripts shell out to an external reference path (configurable via `PORTAL_PROCESSOR`) and are LOCAL-only (not CI), so they add no CI dependency on the reference checkout.
|
|
28
|
+
5. **Rollback cost** — Trivial: delete the module + tests + scripts. No data, no migration, no wiring to unwind.
|
|
29
|
+
6. **Migration parity** — N/A. New internal library code shipped in the package; touches no agent-installed file (`.claude/settings.json` / config / CLAUDE.md template / hooks / skills).
|
|
30
|
+
7. **Failure modes** — (a) Port diverges from the reference Python → caught by the parity harness (33/33 now) AND golden-value CI anchors in the unit test (so a future regex/encoding regression fails in CI even without the reference checkout). (b) Reference path absent → parity harness exits 2 with a clear message (it's a local gate, not CI). (c) An adversarial input not in the corpus diverges in production → mitigated by seeding the corpus with the known divergence classes; the corpus is extended as new classes are discovered, and Dawn's review is the human backstop.
|
|
31
|
+
|
|
32
|
+
## Tests
|
|
33
|
+
|
|
34
|
+
- Tier-1 unit (CI): `tests/unit/feedback-factory/fingerprint.test.ts` — `extractComponent` (dotted id, first-word fallback, [TAG] strip, whitelisted-vs-not severity prefix, empty); `computeFingerprint` (32-hex shape, version/int/hash collapse, internal-whitespace collapse, leading-whitespace component divergence, case-insensitivity, type+component incorporation, 3 golden anchors). 14 tests.
|
|
35
|
+
- Parity (local gate, evidence): `scripts/feedback-factory/fingerprint-parity.mjs` → **33/33 byte-identical** to the reference Python.
|
|
36
|
+
- No integration/E2E tier this increment: the module is not yet wired to a route or job; those tiers attach when the processor job + receiver land (subsequent increments). Documented here so the omission is a reasoned decision, not an oversight.
|