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.
@@ -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;AAU/C;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAiB9E;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"}
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 conservative dedupKey when the model omits one. */
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}::${slug(title)}`;
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,8EAA8E;AAC9E,SAAS,IAAI,CAAC,CAAS;IACrB,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,UAAU,CAAC;AACxG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,SAAiB,EAAE,OAAe;IACpE,OAAO;QACL,4FAA4F;QAC5F,cAAc,SAAS,mEAAmE;QAC1F,gGAAgG;QAChG,wCAAwC;QACxC,6FAA6F;QAC7F,6FAA6F;QAC7F,qEAAqE;QACrE,EAAE;QACF,iEAAiE;QACjE,mGAAmG;QACnG,4EAA4E;QAC5E,EAAE;QACF,iBAAiB;QACjB,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,mBAAmB;KAC7C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAW,EAAE,SAAiB;IAClE,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,CAAC;IACnC,gFAAgF;IAChF,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC/B,MAAM,GAAG,GAAG,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,IAAI,GAAG,IAAI,KAAK;QAAE,OAAO,EAAE,CAAC;IAC1D,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,CAAC;IACtC,MAAM,GAAG,GAAsB,EAAE,CAAC;IAClC,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,IAAI,GAAG,CAAC,MAAM,IAAI,oBAAoB;YAAE,MAAM;QAC9C,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,SAAS;QAChD,MAAM,CAAC,GAAG,IAA+B,CAAC;QAC1C,MAAM,MAAM,GAAG,CAAC,CAAC,MAAgB,CAAC;QAClC,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,MAAqB,CAAC;YAAE,SAAS,CAAC,sBAAsB;QACpF,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACpF,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,MAAM,QAAQ,GAAkB,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAyB,CAAC;YACpF,CAAC,CAAE,CAAC,CAAC,QAA0B;YAC/B,CAAC,CAAC,QAAQ,CAAC;QACb,MAAM,QAAQ,GACZ,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE;YACjD,CAAC,CAAC,GAAG,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE;YACrC,CAAC,CAAC,GAAG,SAAS,KAAK,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAqB,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3F,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAUD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAA4B;IACjE,IAAI,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,CAAC;IACvD,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,mBAAmB,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;IAClF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC,CAAC,0DAA0D;IACvE,CAAC;IACD,OAAO,qBAAqB,CAAC,GAAG,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;AACrD,CAAC"}
1
+ {"version":3,"file":"MentorStageBForensics.js","sourceRoot":"","sources":["../../src/scheduler/MentorStageBForensics.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.15",
3
+ "version": "1.3.17",
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,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:27:48.836Z",
5
- "instarVersion": "1.3.15",
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.