instar 1.3.15 → 1.3.17
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/dist/scheduler/MentorStageBForensics.d.ts.map +1 -1
- package/dist/scheduler/MentorStageBForensics.js +29 -2
- package/dist/scheduler/MentorStageBForensics.js.map +1 -1
- 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/1.3.17.md +34 -0
- package/upgrades/side-effects/feedback-factory-verify.md +31 -0
- package/upgrades/side-effects/mentor-forensic-dedup-stability.md +49 -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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MentorStageBForensics.d.ts","sourceRoot":"","sources":["../../src/scheduler/MentorStageBForensics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,uCAAuC,CAAC;
|
|
1
|
+
{"version":3,"file":"MentorStageBForensics.d.ts","sourceRoot":"","sources":["../../src/scheduler/MentorStageBForensics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,uCAAuC,CAAC;AA6B/C;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA0B9E;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,eAAe,EAAE,CAgCvF;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/C;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAS/F"}
|
|
@@ -14,10 +14,28 @@
|
|
|
14
14
|
import { ISSUE_BUCKETS, ISSUE_SEVERITIES, } from '../monitoring/FrameworkIssueLedger.js';
|
|
15
15
|
const MAX_FINDINGS_PER_RUN = 10; // bound a single tick's output
|
|
16
16
|
const MAX_TITLE = 200;
|
|
17
|
-
/** A normalized slug for a
|
|
17
|
+
/** A normalized slug for a dedupKey from a model-supplied stable id (keeps it as-is, kebabed). */
|
|
18
18
|
function slug(s) {
|
|
19
19
|
return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'untitled';
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Derive a conservative dedupKey from a title when the model omits one. Strips
|
|
23
|
+
* VOLATILE tokens that would split one issue across runs — standalone numbers,
|
|
24
|
+
* timeout/percent/version literals, and hex-ish ids — keeping only the stable
|
|
25
|
+
* symptom words. (§13.3: titles are operator-dependent; this is the last-resort
|
|
26
|
+
* fallback — the prompt asks the model for a stable id directly.)
|
|
27
|
+
*/
|
|
28
|
+
function deriveStableSlug(title) {
|
|
29
|
+
const stable = title
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/\bv?\d+(?:\.\d+)+\b/g, ' ') // version strings (v1.3.14, 1.2.3)
|
|
32
|
+
.replace(/\b\d+\s*(?:ms|s|m|h|%|k|kb|mb|tokens?|turns?)\b/g, ' ') // 8s, 42%, 15k, 1 turn
|
|
33
|
+
.replace(/\b[0-9a-f]{6,}\b/g, ' ') // hex ids / shas
|
|
34
|
+
.replace(/\b\d+\b/g, ' ') // any remaining standalone numbers
|
|
35
|
+
.replace(/\s+/g, ' ')
|
|
36
|
+
.trim();
|
|
37
|
+
return slug(stable || title);
|
|
38
|
+
}
|
|
21
39
|
/**
|
|
22
40
|
* Build the Stage-B forensic prompt. The model is asked to classify ONLY what
|
|
23
41
|
* the signals actually evidence into the three buckets, and to output a strict
|
|
@@ -36,6 +54,15 @@ export function buildForensicPrompt(framework, signals) {
|
|
|
36
54
|
``,
|
|
37
55
|
`Output ONLY a JSON array (no markdown, no prose). Each element:`,
|
|
38
56
|
` {"bucket": "...", "title": "<short>", "severity": "low|medium|high", "dedupKey": "<stable-id>"}`,
|
|
57
|
+
``,
|
|
58
|
+
`CRITICAL — the dedupKey must be STABLE across runs. It is a short lowercase-kebab identifier`,
|
|
59
|
+
`for the ROOT SYMPTOM, NOT a rephrasing of your title. Use the SAME id you would use if you saw`,
|
|
60
|
+
`this exact issue again next time. Base it on the symptom class + the component, e.g.`,
|
|
61
|
+
`"feedback-webhook-429", "inputguard-review-timeout", "codex-single-turn-exit". Do NOT put`,
|
|
62
|
+
`version numbers, timestamps, session ids, counts, or wording variations in the dedupKey —`,
|
|
63
|
+
`those drift and would split one issue into many. Two reports of the same root problem MUST`,
|
|
64
|
+
`produce the same dedupKey.`,
|
|
65
|
+
``,
|
|
39
66
|
`If the signals evidence no concrete issue, output []. Never invent issues.`,
|
|
40
67
|
``,
|
|
41
68
|
`--- Signals ---`,
|
|
@@ -84,7 +111,7 @@ export function parseForensicFindings(raw, framework) {
|
|
|
84
111
|
: 'medium';
|
|
85
112
|
const dedupKey = typeof o.dedupKey === 'string' && o.dedupKey.trim()
|
|
86
113
|
? `${framework}::${slug(o.dedupKey)}`
|
|
87
|
-
: `${framework}::${
|
|
114
|
+
: `${framework}::${deriveStableSlug(title)}`;
|
|
88
115
|
out.push({ bucket: bucket, title, severity, dedupKey, signature: title });
|
|
89
116
|
}
|
|
90
117
|
return out;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MentorStageBForensics.js","sourceRoot":"","sources":["../../src/scheduler/MentorStageBForensics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EACL,aAAa,EACb,gBAAgB,GAIjB,MAAM,uCAAuC,CAAC;AAE/C,MAAM,oBAAoB,GAAG,EAAE,CAAC,CAAC,+BAA+B;AAChE,MAAM,SAAS,GAAG,GAAG,CAAC;AAEtB,
|
|
1
|
+
{"version":3,"file":"MentorStageBForensics.js","sourceRoot":"","sources":["../../src/scheduler/MentorStageBForensics.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:57:14.226Z",
|
|
5
|
+
"instarVersion": "1.3.17",
|
|
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,34 @@
|
|
|
1
|
+
# Instar Upgrade Guide — NEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
**Framework-Onboarding Mentor System — forensic dedup stability.** A live validation run surfaced
|
|
8
|
+
that the same root issue could be logged 2–3 times across ticks under different IDs, because when the
|
|
9
|
+
model omitted a stable identifier the system derived one from the (drifting) wording — the exact
|
|
10
|
+
title-dependence the design doc warns against. Fixed two ways: the forensic prompt now demands a
|
|
11
|
+
stable, symptom-based identifier (no version numbers, counts, ids, or wording variants), and the
|
|
12
|
+
fallback derivation strips volatile tokens before building a key. Live re-run confirmed cross-tick
|
|
13
|
+
merging now works (16 → 12 issues over the same 3 iterations). Still dormant.
|
|
14
|
+
|
|
15
|
+
## What to Tell Your User
|
|
16
|
+
|
|
17
|
+
- The mentor's notebook now keeps one entry per real problem even when the wording wobbles between
|
|
18
|
+
inspections — no more slow drizzle of near-duplicate entries.
|
|
19
|
+
|
|
20
|
+
## Summary of New Capabilities
|
|
21
|
+
|
|
22
|
+
| Capability | How to Use |
|
|
23
|
+
|-----------|-----------|
|
|
24
|
+
| Stable forensic dedup | Automatic — the same root issue collapses to one ledger entry across ticks |
|
|
25
|
+
|
|
26
|
+
## Evidence
|
|
27
|
+
|
|
28
|
+
Found by running the real loop through multiple iterations against real data (the iterate-and-harden
|
|
29
|
+
loop working as designed). Fix proven by: 3 new unit tests asserting the model-supplied stable key is
|
|
30
|
+
preferred, that two phrasings of one issue differing only in volatile tokens (`8s` vs `8000ms`)
|
|
31
|
+
collapse to the **same** dedupKey, and that version/percent/hex tokens are stripped from the derived
|
|
32
|
+
fallback; plus a live re-run on Echo's real server.log + Codey's real rollouts showing cross-tick
|
|
33
|
+
merge dropped duplicates from 16 → 12 over the same 3 iterations. 13 unit tests total; affected
|
|
34
|
+
push-config suite green vs canonical main.
|
|
@@ -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.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Side-Effects Review — Forensic dedupKey stability (§19.4 follow-on)
|
|
2
|
+
|
|
3
|
+
**Spec:** `docs/specs/FRAMEWORK-ONBOARDING-MENTOR-SPEC.md` (converged 5 iters, approved by Justin)
|
|
4
|
+
**Change:** Hardens Stage-B forensic dedupKey stability so the same root issue produces the same
|
|
5
|
+
key across ticks (§13.3: "title is too operator-dependent"). Found by **live validation**: running
|
|
6
|
+
the real forensics through 3 iterations on real data logged the same issue 2–3× under different
|
|
7
|
+
keys, because the LLM-omitted-key fallback derived from the (drifting) title. Fix: (1) the prompt
|
|
8
|
+
now demands a stable, symptom-based lowercase-kebab `dedupKey` with explicit "no versions / numbers /
|
|
9
|
+
ids / wording variants" guidance; (2) the fallback derivation strips volatile tokens
|
|
10
|
+
(numbers, version/percent/unit literals, hex ids) before slugging.
|
|
11
|
+
**Files:** `src/scheduler/MentorStageBForensics.ts`, `tests/unit/MentorStageBForensics.test.ts`,
|
|
12
|
+
`upgrades/NEXT.md`.
|
|
13
|
+
|
|
14
|
+
## Principle check (Phase 1)
|
|
15
|
+
|
|
16
|
+
Decision point? No. This sharpens an identifier (dedup quality) on a signal-only path. No gating.
|
|
17
|
+
|
|
18
|
+
## The seven questions
|
|
19
|
+
|
|
20
|
+
1. **Over-block.** Over-aggressive token stripping could in theory merge two genuinely-distinct
|
|
21
|
+
issues whose titles differ only in numbers. Mitigated: the strip only applies to the *fallback*
|
|
22
|
+
(when the LLM omits a key); the LLM-supplied symptom key is preferred. And §13.3's design makes
|
|
23
|
+
false merges the conservative-acceptable direction only on `dedupKey` while `signature` (the full
|
|
24
|
+
title) preserves the distinction for probable-dup review.
|
|
25
|
+
2. **Under-block.** LLM forensics are inherently variable run-to-run, so cross-tick merging is
|
|
26
|
+
improved (live-measured 16→12 issues over the same 3 iterations) but not perfect; the residual
|
|
27
|
+
is handled by the `signature`-based probable-dup review (§13.3), unchanged.
|
|
28
|
+
3. **Level-of-abstraction fit.** Pure-module change (prompt + key derivation); no I/O. Correct layer.
|
|
29
|
+
4. **Signal vs authority.** Unchanged — signal-only.
|
|
30
|
+
5. **Interactions.** Only changes the `dedupKey` a finding carries; the ledger's existing
|
|
31
|
+
conservative auto-merge on `(framework, dedupKey)` does the rest (no ledger change).
|
|
32
|
+
6. **External surfaces.** None — internal forensic logic.
|
|
33
|
+
7. **Rollback cost.** Trivial — revert the prompt + derivation change.
|
|
34
|
+
|
|
35
|
+
## Phase 5 — second-pass
|
|
36
|
+
|
|
37
|
+
Not required — a dedup-quality refinement on a pure, tested module; no decision/spawn/session surface.
|
|
38
|
+
|
|
39
|
+
## Validation
|
|
40
|
+
|
|
41
|
+
Live re-run on Echo's real server.log + Codey's real rollouts: before, 3 iterations produced 16
|
|
42
|
+
issues (near-zero cross-tick merge); after, the same 3 iterations produced 12 (iterations 2–3 each
|
|
43
|
+
merged ~half their findings into existing issues) — the cross-tick merge now demonstrably works.
|
|
44
|
+
|
|
45
|
+
## Testing
|
|
46
|
+
|
|
47
|
+
13 unit tests (+3): prefers the model-supplied stable key; fallback strips volatile tokens so
|
|
48
|
+
phrasing/number variants of one issue collapse to the same key; version/percent/hex tokens stripped.
|
|
49
|
+
Affected push-config suite green vs canonical main.
|