instar 1.3.15 → 1.3.16
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/verify.d.ts +40 -0
- package/dist/feedback-factory/processor/verify.d.ts.map +1 -0
- package/dist/feedback-factory/processor/verify.js +85 -0
- package/dist/feedback-factory/processor/verify.js.map +1 -0
- package/package.json +1 -1
- package/scripts/feedback-factory/_py_verify_ref.py +59 -0
- package/scripts/feedback-factory/verify-corpus.json +14 -0
- package/scripts/feedback-factory/verify-parity.mjs +69 -0
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.16.md +26 -0
- package/upgrades/side-effects/feedback-factory-verify.md +31 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify.ts — TS port of the feedback-factory fix-verification logic.
|
|
3
|
+
*
|
|
4
|
+
* Byte-exact port of `can_transition_to_verified` (:1084) from the reference
|
|
5
|
+
* `the-portal/.claude/scripts/feedback-processor.py`. Decides whether a fixed
|
|
6
|
+
* cluster has actually stayed fixed (so it may transition to `verified`), via two
|
|
7
|
+
* strategies: version-anchored (HIGH confidence — no recurrence on the fixed
|
|
8
|
+
* version) and silence-based (LOW confidence — quiet long enough relative to its
|
|
9
|
+
* historical report frequency).
|
|
10
|
+
*
|
|
11
|
+
* The reference is time- and DB-dependent (`datetime.now()` + a recent-reports
|
|
12
|
+
* query). This port keeps it PURE by INJECTING both: `now` and the version-
|
|
13
|
+
* anchored query result (`recentReportsSinceFix`) are passed in by the caller
|
|
14
|
+
* (the real store adapter does `datetime.now()` + the query). Parity is verified
|
|
15
|
+
* by monkeypatching the reference's `datetime.now` + `run_prisma_query` to the
|
|
16
|
+
* same fixed values.
|
|
17
|
+
*/
|
|
18
|
+
import type { Cluster } from './types.js';
|
|
19
|
+
export interface VerifyOptions {
|
|
20
|
+
/** Injected "now" (ISO string). The reference uses datetime.now(timezone.utc). */
|
|
21
|
+
now: string;
|
|
22
|
+
/**
|
|
23
|
+
* The version-anchored recent-reports query result (items with the cluster's
|
|
24
|
+
* fingerprint received since fixAppliedAt). Only consulted when the cluster has
|
|
25
|
+
* BOTH fixedInVersion and fingerprint. The real adapter runs the query; tests
|
|
26
|
+
* inject it. Treated as empty when omitted.
|
|
27
|
+
*/
|
|
28
|
+
recentReportsSinceFix?: unknown[];
|
|
29
|
+
}
|
|
30
|
+
export interface VerifyResult {
|
|
31
|
+
allowed: boolean;
|
|
32
|
+
evidence: string;
|
|
33
|
+
recommendation?: string;
|
|
34
|
+
confidence?: string;
|
|
35
|
+
verified_by?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Reproduce Python `f"{x:.0f}"` — round-half-to-even to an integer, as a string. */
|
|
38
|
+
export declare function pyFormat0f(x: number): string;
|
|
39
|
+
export declare function canTransitionToVerified(cluster: Cluster, opts: VerifyOptions): VerifyResult;
|
|
40
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/feedback-factory/processor/verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,WAAW,aAAa;IAC5B,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,OAAO,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,qFAAqF;AACrF,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ5C;AAOD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,GAAG,YAAY,CAwD3F"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify.ts — TS port of the feedback-factory fix-verification logic.
|
|
3
|
+
*
|
|
4
|
+
* Byte-exact port of `can_transition_to_verified` (:1084) from the reference
|
|
5
|
+
* `the-portal/.claude/scripts/feedback-processor.py`. Decides whether a fixed
|
|
6
|
+
* cluster has actually stayed fixed (so it may transition to `verified`), via two
|
|
7
|
+
* strategies: version-anchored (HIGH confidence — no recurrence on the fixed
|
|
8
|
+
* version) and silence-based (LOW confidence — quiet long enough relative to its
|
|
9
|
+
* historical report frequency).
|
|
10
|
+
*
|
|
11
|
+
* The reference is time- and DB-dependent (`datetime.now()` + a recent-reports
|
|
12
|
+
* query). This port keeps it PURE by INJECTING both: `now` and the version-
|
|
13
|
+
* anchored query result (`recentReportsSinceFix`) are passed in by the caller
|
|
14
|
+
* (the real store adapter does `datetime.now()` + the query). Parity is verified
|
|
15
|
+
* by monkeypatching the reference's `datetime.now` + `run_prisma_query` to the
|
|
16
|
+
* same fixed values.
|
|
17
|
+
*/
|
|
18
|
+
/** Reproduce Python `f"{x:.0f}"` — round-half-to-even to an integer, as a string. */
|
|
19
|
+
export function pyFormat0f(x) {
|
|
20
|
+
const floor = Math.floor(x);
|
|
21
|
+
const diff = x - floor;
|
|
22
|
+
let r;
|
|
23
|
+
if (diff > 0.5)
|
|
24
|
+
r = floor + 1;
|
|
25
|
+
else if (diff < 0.5)
|
|
26
|
+
r = floor;
|
|
27
|
+
else
|
|
28
|
+
r = floor % 2 === 0 ? floor : floor + 1;
|
|
29
|
+
return String(r);
|
|
30
|
+
}
|
|
31
|
+
/** Parse an ISO timestamp to epoch ms (mirrors datetime.fromisoformat(s.replace('Z','+00:00'))). */
|
|
32
|
+
function parseIso(s) {
|
|
33
|
+
return new Date(s).getTime();
|
|
34
|
+
}
|
|
35
|
+
export function canTransitionToVerified(cluster, opts) {
|
|
36
|
+
const fingerprint = cluster.fingerprint;
|
|
37
|
+
const fixedVersion = cluster.fixedInVersion;
|
|
38
|
+
// Fall back to dispatchedAt when fixAppliedAt is missing (reference comment:
|
|
39
|
+
// dispatch happens after the fix is applied, so it's a safe proxy timestamp).
|
|
40
|
+
const fixAppliedAt = cluster.fixAppliedAt || cluster.dispatchedAt;
|
|
41
|
+
if (!fixAppliedAt) {
|
|
42
|
+
return {
|
|
43
|
+
allowed: false,
|
|
44
|
+
evidence: 'No fixAppliedAt or dispatchedAt timestamp',
|
|
45
|
+
recommendation: 'set_fix_applied_at',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const nowMs = parseIso(opts.now);
|
|
49
|
+
const fixTimeMs = parseIso(fixAppliedAt);
|
|
50
|
+
const hoursSinceFix = (nowMs - fixTimeMs) / 1000 / 3600;
|
|
51
|
+
// Strategy 1: Version-anchored (HIGH confidence).
|
|
52
|
+
if (fixedVersion && fingerprint) {
|
|
53
|
+
const recentItems = opts.recentReportsSinceFix ?? [];
|
|
54
|
+
if (recentItems.length > 0) {
|
|
55
|
+
return { allowed: false, evidence: `Still seeing ${recentItems.length} reports since fix`, recommendation: 'revert_to_investigating' };
|
|
56
|
+
}
|
|
57
|
+
if (hoursSinceFix >= 24) {
|
|
58
|
+
return {
|
|
59
|
+
allowed: true,
|
|
60
|
+
evidence: `No recurrence on v${fixedVersion}+ in ${pyFormat0f(hoursSinceFix)}h`,
|
|
61
|
+
confidence: 'high',
|
|
62
|
+
verified_by: `auto:version_check:v${fixedVersion}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Strategy 2: Silence-based (LOW confidence).
|
|
67
|
+
const createdAt = cluster.createdAt;
|
|
68
|
+
const firstSeenMs = parseIso(createdAt);
|
|
69
|
+
const lastSeenStr = cluster.updatedAt ?? createdAt;
|
|
70
|
+
const lastSeenMs = parseIso(lastSeenStr);
|
|
71
|
+
const reportCount = cluster.reportCount ?? 1;
|
|
72
|
+
const spanHours = (lastSeenMs - firstSeenMs) / 1000 / 3600;
|
|
73
|
+
const freqHours = (spanHours < 1 || reportCount < 2) ? 24.0 : spanHours / Math.max(reportCount - 1, 1);
|
|
74
|
+
const silenceRequired = Math.max(freqHours * 3, 48);
|
|
75
|
+
if (hoursSinceFix < silenceRequired) {
|
|
76
|
+
return { allowed: false, evidence: `Only ${pyFormat0f(hoursSinceFix)}h since fix, need ${pyFormat0f(silenceRequired)}h`, recommendation: 'wait' };
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
allowed: true,
|
|
80
|
+
evidence: `No recurrence in ${pyFormat0f(hoursSinceFix)}h (required: ${pyFormat0f(silenceRequired)}h)`,
|
|
81
|
+
confidence: 'low',
|
|
82
|
+
verified_by: 'auto:silence_check',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../../src/feedback-factory/processor/verify.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Parity reference: run the REAL can_transition_to_verified deterministically.
|
|
3
|
+
|
|
4
|
+
The reference is time- and DB-dependent. We monkeypatch the processor's
|
|
5
|
+
`datetime` (so .now() returns the case's fixed `now`) and `run_prisma_query` (so
|
|
6
|
+
the version-anchored query returns the case's recentReportsSinceFix), then call
|
|
7
|
+
the actual, unmodified can_transition_to_verified. Emits JSON {case, result}.
|
|
8
|
+
|
|
9
|
+
Usage: python3 _py_verify_ref.py <processor_path> <corpus_path>
|
|
10
|
+
"""
|
|
11
|
+
import sys
|
|
12
|
+
import json
|
|
13
|
+
import importlib.util
|
|
14
|
+
import datetime as _dt
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_processor(path):
|
|
18
|
+
spec = importlib.util.spec_from_file_location("_feedback_processor_ref_vf", path)
|
|
19
|
+
mod = importlib.util.module_from_spec(spec)
|
|
20
|
+
spec.loader.exec_module(mod)
|
|
21
|
+
return mod
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def make_fake_datetime(fixed_now_iso):
|
|
25
|
+
real = _dt.datetime
|
|
26
|
+
|
|
27
|
+
class FakeDateTime:
|
|
28
|
+
@staticmethod
|
|
29
|
+
def now(tz=None):
|
|
30
|
+
return real.fromisoformat(fixed_now_iso.replace('Z', '+00:00'))
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def fromisoformat(s):
|
|
34
|
+
return real.fromisoformat(s)
|
|
35
|
+
return FakeDateTime
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main():
|
|
39
|
+
if len(sys.argv) != 3:
|
|
40
|
+
print("usage: _py_verify_ref.py <processor_path> <corpus_path>", file=sys.stderr)
|
|
41
|
+
sys.exit(2)
|
|
42
|
+
processor_path, corpus_path = sys.argv[1], sys.argv[2]
|
|
43
|
+
proc = load_processor(processor_path)
|
|
44
|
+
with open(corpus_path, encoding="utf-8") as f:
|
|
45
|
+
corpus = json.load(f)
|
|
46
|
+
|
|
47
|
+
out = []
|
|
48
|
+
for case in corpus["cases"]:
|
|
49
|
+
proc.datetime = make_fake_datetime(case["now"])
|
|
50
|
+
recent = case.get("recentReportsSinceFix", [])
|
|
51
|
+
proc.run_prisma_query = lambda *a, **k: json.dumps(recent)
|
|
52
|
+
result = proc.can_transition_to_verified(case["cluster"])
|
|
53
|
+
out.append({"clusterId": case["cluster"]["clusterId"], "result": result})
|
|
54
|
+
|
|
55
|
+
sys.stdout.write(json.dumps(out, ensure_ascii=False))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
main()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Parity corpus for can_transition_to_verified. Each case fixes `now` and the version-anchored query result (recentReportsSinceFix) so the time/DB-dependent reference is deterministic. The Python ref monkeypatches datetime.now + run_prisma_query to these. Timestamps are UTC (Z).",
|
|
3
|
+
"cases": [
|
|
4
|
+
{ "_name": "no fix timestamp", "now": "2026-05-27T00:00:00Z", "cluster": { "clusterId": "c1", "title": "t", "description": "d", "createdAt": "2026-05-20T00:00:00Z" } },
|
|
5
|
+
{ "_name": "silence: too soon → wait", "now": "2026-05-27T00:00:00Z", "cluster": { "clusterId": "c2", "title": "t", "description": "d", "createdAt": "2026-05-20T00:00:00Z", "updatedAt": "2026-05-26T00:00:00Z", "reportCount": 1, "fixAppliedAt": "2026-05-26T14:00:00Z" } },
|
|
6
|
+
{ "_name": "silence: quiet long enough → verified low", "now": "2026-05-27T00:00:00Z", "cluster": { "clusterId": "c3", "title": "t", "description": "d", "createdAt": "2026-05-20T00:00:00Z", "updatedAt": "2026-05-22T00:00:00Z", "reportCount": 1, "fixAppliedAt": "2026-05-22T20:00:00Z" } },
|
|
7
|
+
{ "_name": "version-anchored: recent reports → revert", "now": "2026-05-27T00:00:00Z", "recentReportsSinceFix": [ { "instarVersion": "1.2.4", "receivedAt": "2026-05-26T00:00:00Z" } ], "cluster": { "clusterId": "c4", "title": "t", "description": "d", "createdAt": "2026-05-20T00:00:00Z", "fixedInVersion": "1.2.3", "fingerprint": "abc123", "fixAppliedAt": "2026-05-25T18:00:00Z" } },
|
|
8
|
+
{ "_name": "version-anchored: no recent, >=24h → verified high", "now": "2026-05-27T00:00:00Z", "recentReportsSinceFix": [], "cluster": { "clusterId": "c5", "title": "t", "description": "d", "createdAt": "2026-05-20T00:00:00Z", "fixedInVersion": "1.2.3", "fingerprint": "abc123", "fixAppliedAt": "2026-05-25T18:00:00Z" } },
|
|
9
|
+
{ "_name": "version-anchored: no recent, <24h → falls through to silence (wait)", "now": "2026-05-27T00:00:00Z", "recentReportsSinceFix": [], "cluster": { "clusterId": "c6", "title": "t", "description": "d", "createdAt": "2026-05-20T00:00:00Z", "updatedAt": "2026-05-26T00:00:00Z", "reportCount": 1, "fixedInVersion": "1.2.3", "fingerprint": "abc123", "fixAppliedAt": "2026-05-26T14:00:00Z" } },
|
|
10
|
+
{ "_name": "dispatchedAt fallback when no fixAppliedAt", "now": "2026-05-27T00:00:00Z", "cluster": { "clusterId": "c7", "title": "t", "description": "d", "createdAt": "2026-05-20T00:00:00Z", "updatedAt": "2026-05-22T00:00:00Z", "reportCount": 1, "dispatchedAt": "2026-05-22T20:00:00Z" } },
|
|
11
|
+
{ "_name": "fractional hours exercises :.0f rounding", "now": "2026-05-27T00:00:00Z", "cluster": { "clusterId": "c8", "title": "t", "description": "d", "createdAt": "2026-05-20T00:00:00Z", "updatedAt": "2026-05-25T00:00:00Z", "reportCount": 1, "fixAppliedAt": "2026-05-24T23:30:00Z" } },
|
|
12
|
+
{ "_name": "multi-report frequency drives silence_required", "now": "2026-05-27T00:00:00Z", "cluster": { "clusterId": "c9", "title": "t", "description": "d", "createdAt": "2026-05-20T00:00:00Z", "updatedAt": "2026-05-26T00:00:00Z", "reportCount": 7, "fixAppliedAt": "2026-05-26T20:00:00Z" } }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* verify-parity.mjs — LOCAL parity gate for the fix-verification port.
|
|
4
|
+
*
|
|
5
|
+
* Runs the REAL can_transition_to_verified (datetime.now + DB query monkeypatched
|
|
6
|
+
* deterministically to each case) and the TS canTransitionToVerified over the same
|
|
7
|
+
* fixed `now` + recentReportsSinceFix, and asserts the full result object matches
|
|
8
|
+
* (allowed, evidence, recommendation, confidence, verified_by). LOCAL evidence
|
|
9
|
+
* gate, not CI.
|
|
10
|
+
*
|
|
11
|
+
* Exit 0 = parity; exit 1 = mismatch.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { spawnSync } from 'node:child_process';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
21
|
+
|
|
22
|
+
const PROCESSOR = process.env.PORTAL_PROCESSOR
|
|
23
|
+
|| '/Users/justin/Documents/Projects/the-portal/.claude/scripts/feedback-processor.py';
|
|
24
|
+
const CORPUS = path.join(__dirname, 'verify-corpus.json');
|
|
25
|
+
const PY_REF = path.join(__dirname, '_py_verify_ref.py');
|
|
26
|
+
|
|
27
|
+
if (!fs.existsSync(PROCESSOR)) {
|
|
28
|
+
console.error(`[verify-parity] reference processor not found: ${PROCESSOR}`);
|
|
29
|
+
process.exit(2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const py = spawnSync('python3', [PY_REF, PROCESSOR, CORPUS], { encoding: 'utf8' });
|
|
33
|
+
if (py.status !== 0) {
|
|
34
|
+
console.error('[verify-parity] python reference failed:\n', py.stderr || py.stdout);
|
|
35
|
+
process.exit(2);
|
|
36
|
+
}
|
|
37
|
+
const reference = JSON.parse(py.stdout);
|
|
38
|
+
|
|
39
|
+
const corpus = JSON.parse(fs.readFileSync(CORPUS, 'utf8'));
|
|
40
|
+
const distUrl = new URL('file://' + path.join(ROOT, 'dist', 'feedback-factory', 'processor', 'verify.js'));
|
|
41
|
+
const { canTransitionToVerified } = await import(distUrl.href);
|
|
42
|
+
|
|
43
|
+
const norm = (r) => JSON.stringify({
|
|
44
|
+
allowed: r.allowed, evidence: r.evidence,
|
|
45
|
+
recommendation: r.recommendation ?? null, confidence: r.confidence ?? null,
|
|
46
|
+
verified_by: r.verified_by ?? null,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
let mismatches = 0;
|
|
50
|
+
for (let i = 0; i < corpus.cases.length; i++) {
|
|
51
|
+
const c = corpus.cases[i];
|
|
52
|
+
const got = canTransitionToVerified(c.cluster, { now: c.now, recentReportsSinceFix: c.recentReportsSinceFix });
|
|
53
|
+
const want = reference[i].result;
|
|
54
|
+
if (norm(got) !== norm(want)) {
|
|
55
|
+
mismatches++;
|
|
56
|
+
console.error(`[MISMATCH] ${c._name || c.cluster.clusterId}`);
|
|
57
|
+
console.error(` python: ${norm(want)}`);
|
|
58
|
+
console.error(` ts : ${norm(got)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const total = corpus.cases.length;
|
|
63
|
+
if (mismatches === 0) {
|
|
64
|
+
console.error(`[verify-parity] ✓ ${total}/${total} verification results match the reference can_transition_to_verified.`);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
} else {
|
|
67
|
+
console.error(`[verify-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-27T06:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-05-27T06:41:15.968Z",
|
|
5
|
+
"instarVersion": "1.3.16",
|
|
6
6
|
"entryCount": 192,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
Fifth increment of the **Feedback Factory Migration** (Dawn → Echo; spec `docs/specs/feedback-factory-migration.md`, approved). Ports the **fix verifier** — `can_transition_to_verified` — from the reference Python (`the-portal/.claude/scripts/feedback-processor.py`) to TypeScript at `src/feedback-factory/processor/verify.ts`.
|
|
8
|
+
|
|
9
|
+
This is the logic that decides whether a bug marked "fixed" has actually stayed fixed before it's allowed to move to "verified": a high-confidence check (no recurrence on the fixed version) and a low-confidence fallback (quiet long enough relative to how often the bug used to be reported). The clock and the recent-reports lookup are passed in by the caller, so the decision itself is pure and deterministic. **Not wired into any route or job yet** — no behavioral change.
|
|
10
|
+
|
|
11
|
+
## What to Tell Your User
|
|
12
|
+
|
|
13
|
+
- The part of the feedback brain that guards against declaring a bug fixed too early is now ported — it won't mark something verified while reports are still coming in, or before enough quiet time has passed.
|
|
14
|
+
- Proven against Dawn's original by feeding both the same fixed clock and the same recent-report data: identical verdicts, 9 for 9 — including the exact wording of the "how many hours" explanations.
|
|
15
|
+
|
|
16
|
+
## Summary of New Capabilities
|
|
17
|
+
|
|
18
|
+
| Capability | How to Use |
|
|
19
|
+
|-----------|-----------|
|
|
20
|
+
| Fix verifier (TS port) | Internal module `src/feedback-factory/processor/verify.ts` — not yet wired |
|
|
21
|
+
| Verifier parity harness | `node scripts/feedback-factory/verify-parity.mjs` (local; set `PORTAL_PROCESSOR`) |
|
|
22
|
+
|
|
23
|
+
## Evidence
|
|
24
|
+
|
|
25
|
+
- **Parity vs the REAL `can_transition_to_verified`:** the harness monkeypatches the reference's clock (`datetime.now`) and database query to fixed per-case values and runs the actual function, then compares the full verdict (allowed, evidence text, recommendation, confidence, verified-by) to the TS port. Result: **9/9 identical** across no-fix-timestamp, silence too-soon vs quiet-enough, version-anchored (recent reports / clean / under-24h fallthrough), the dispatched-at fallback, and a fractional-hours case that exercises Python's round-half-to-even number formatting.
|
|
26
|
+
- **CI anchors:** unit tests assert each branch plus the half-to-even rounding, so a regression fails in CI even without the reference checkout.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Side-Effects Review — feedback-factory fix verifier (Phase 1, increment 5)
|
|
2
|
+
|
|
3
|
+
**Slug:** `feedback-factory-verify`
|
|
4
|
+
**Date:** `2026-05-27`
|
|
5
|
+
**Author:** Echo (autonomous)
|
|
6
|
+
**Spec:** `docs/specs/feedback-factory-migration.md` (converged v2, approved by Justin 2026-05-26)
|
|
7
|
+
**Scope:** `can_transition_to_verified` (:1084) — decide whether a fixed cluster has stayed fixed (version-anchored HIGH-confidence + silence-based LOW-confidence). The real `datetime.now()` + recent-reports query are injected by the caller; this is the pure decision.
|
|
8
|
+
|
|
9
|
+
## Summary of the change
|
|
10
|
+
|
|
11
|
+
Byte-exact port of `can_transition_to_verified` from `the-portal/.claude/scripts/feedback-processor.py` to `src/feedback-factory/processor/verify.ts` (`canTransitionToVerified`). Pure: `now` and the version-anchored query result (`recentReportsSinceFix`) are injected via `VerifyOptions` (the real store adapter does `datetime.now()` + the query). Adds `pyFormat0f` (Python `:.0f` round-half-to-even), the parity harness, and Tier-1 unit tests. **Not wired into any route/job yet** — no behavioral change.
|
|
12
|
+
|
|
13
|
+
## Equivalence verification
|
|
14
|
+
|
|
15
|
+
- **9/9 cases match the REAL `can_transition_to_verified`.** The parity harness monkeypatches the reference's `datetime` (so `.now()` returns each case's fixed `now`) and `run_prisma_query` (so the version-anchored query returns each case's `recentReportsSinceFix`), then runs the actual, unmodified function — full result object compared (allowed, evidence, recommendation, confidence, verified_by). Cases cover: no-fix-timestamp, silence too-soon/quiet-enough, version-anchored recent-reports/clean/under-24h-fallthrough, the `dispatchedAt` fallback, fractional-hours `:.0f` rounding, and multi-report frequency.
|
|
16
|
+
|
|
17
|
+
## Seven-dimension review
|
|
18
|
+
|
|
19
|
+
1. **Over/under-reach** — Pure deterministic function (given injected now + reports), no I/O, no global state, not imported by any runtime path.
|
|
20
|
+
2. **Level-of-abstraction fit** — Processor-logic layer. Time + DB are injected at the function boundary (dependency injection), keeping the decision pure + the real query/clock in the adapter — the cleanest seam.
|
|
21
|
+
3. **Signal vs Authority** — N/A; produces a recommendation object. The actual transition is still gated by `canTransition` (increment 3) + the curated lifecycle.
|
|
22
|
+
4. **Interactions** — None. New isolated module. Parity scripts LOCAL-only.
|
|
23
|
+
5. **Rollback cost** — Trivial: delete the module + tests + scripts.
|
|
24
|
+
6. **Migration parity** — N/A. New internal library code; no agent-installed file touched.
|
|
25
|
+
7. **Failure modes** — (a) Port diverges → parity harness (9/9, against the real function) + unit tests. (b) `:.0f` half-rounding divergence → `pyFormat0f` unit-tested on exact-half cases + parity (fractional-hours case). (c) Timezone/ISO parse divergence → both sides parse to epoch ms (UTC); corpus uses tz-aware Z timestamps; parity verifies. (d) Caller forgets to inject `recentReportsSinceFix` when version+fingerprint present → treated as empty (no recent reports), matching "no recurrence"; documented in the type.
|
|
26
|
+
|
|
27
|
+
## Tests
|
|
28
|
+
|
|
29
|
+
- Tier-1 unit (CI): `tests/unit/feedback-factory/verify.test.ts` — pyFormat0f (half-to-even), no-timestamp, version-anchored revert/high/fallthrough, silence verified, dispatchedAt fallback. 7 tests.
|
|
30
|
+
- Parity (local gate, evidence): `scripts/feedback-factory/verify-parity.mjs` → **9/9** vs the real `can_transition_to_verified`.
|
|
31
|
+
- No integration/E2E this increment: not wired to a route/job; those tiers attach when the transition driver + job land. Reasoned decision, documented.
|