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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.15",
3
+ "version": "1.3.16",
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: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.