From 38f355c5b64bfb04bc84afdf10d2be5082ff03c2 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:32:49 -0700 Subject: [PATCH] fix(engine): stop scoring issues with unknown timestamps as maximally fresh issueAgeDays returned 0 (the freshest possible age) whenever an open issue's updatedAt and createdAt were both missing or unparseable, so a garbage/missing-timestamp issue outranked genuinely fresh ones instead of falling to the freshness floor. Floor unknown-age issues to a large sentinel so they clamp to the same 0.05 floor a genuinely stale issue gets, matching this PR's own commit history intent ("malformed updatedAt cannot score a stale issue as fresh") and every existing opportunity-freshness/opportunity-metadata-signals/opportunity-branch- internals test assertion. Also fixes two unrelated build breaks on main discovered while verifying this fix's gate: - packages/gittensory-engine/src/index.ts: a merge collision dropped the `export {` opener before the contributor-fit re-export block, breaking `tsc` for the whole barrel file. - packages/gittensory-engine/src/governor-ledger.ts: importing node:util's isDeepStrictEqual doesn't type-check under this package's ambient-types-off tsconfig, and depends on a Node builtin in a package shared with the Worker backend. Replaced with a small self-contained structural-equality check scoped to its one call site (JSON round-trip fidelity), keeping the package portable. --- .../gittensory-engine/src/governor-ledger.ts | 22 ++++++++++++++++--- packages/gittensory-engine/src/index.ts | 1 + .../src/opportunity-freshness.ts | 10 +++++++-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/gittensory-engine/src/governor-ledger.ts b/packages/gittensory-engine/src/governor-ledger.ts index aa7f449090..f8323320f9 100644 --- a/packages/gittensory-engine/src/governor-ledger.ts +++ b/packages/gittensory-engine/src/governor-ledger.ts @@ -1,5 +1,3 @@ -import { isDeepStrictEqual } from "node:util"; - /** Immutable governor decision vocabulary — unknown values fail closed before insert. */ export const GOVERNOR_LEDGER_EVENT_TYPES = Object.freeze([ "allowed", @@ -31,6 +29,24 @@ export type NormalizedGovernorLedgerEvent = { const governorEventTypeSet = new Set(GOVERNOR_LEDGER_EVENT_TYPES); /* v8 ignore start -- Normalization helpers are covered through normalizeGovernorLedgerEvent export tests. */ +// Self-contained structural-equality check (no node:util) so this package stays runtime-portable across the +// Worker backend and the Node-only miner CLI. Scoped to serializePayload's own round-trip-fidelity use: both +// sides here are always plain objects/arrays/primitives (one is a fresh JSON.parse result), so this never needs +// to handle Maps, Dates, RegExps, or prototypes the way a general-purpose deep-equal would. +function deepStrictEqual(a: unknown, b: unknown): boolean { + if (Object.is(a, b)) return true; + if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false; + if (Array.isArray(a) !== Array.isArray(b)) return false; + const aRecord = a as Record; + const bRecord = b as Record; + const aKeys = Object.keys(aRecord); + const bKeys = Object.keys(bRecord); + if (aKeys.length !== bKeys.length) return false; + return aKeys.every( + (key) => Object.prototype.hasOwnProperty.call(bRecord, key) && deepStrictEqual(aRecord[key], bRecord[key]), + ); +} + function normalizeRequiredString(value: unknown, code: string): string { if (typeof value !== "string") throw new Error(code); const trimmed = value.trim(); @@ -57,7 +73,7 @@ function serializePayload(payload: unknown): string { } catch { throw new Error("invalid_payload"); } - if (!isDeepStrictEqual(JSON.parse(json), payload)) { + if (!deepStrictEqual(JSON.parse(json), payload)) { throw new Error("invalid_payload"); } return json; diff --git a/packages/gittensory-engine/src/index.ts b/packages/gittensory-engine/src/index.ts index 4adbe14d99..43b5f0cf32 100644 --- a/packages/gittensory-engine/src/index.ts +++ b/packages/gittensory-engine/src/index.ts @@ -49,6 +49,7 @@ export { computeLaneFit, type GoalModelInput, } from "./goal-model.js"; +export { classifyContributorFit, type ContributorFit, type ContributorFitCheck, diff --git a/packages/gittensory-engine/src/opportunity-freshness.ts b/packages/gittensory-engine/src/opportunity-freshness.ts index 943ee78f49..d23ef0ff02 100644 --- a/packages/gittensory-engine/src/opportunity-freshness.ts +++ b/packages/gittensory-engine/src/opportunity-freshness.ts @@ -26,10 +26,16 @@ function pickTimestamp(issue: FreshnessIssue): string | null { return null; } +// No usable timestamp survived pickTimestamp's updatedAt->createdAt fallback -- an unknown age must never +// register as "just updated" (age 0, the freshest possible score). Floor it to a large sentinel so +// computeOpportunityFreshness's exponential decay clamps straight to the 0.05 floor, matching how a genuinely +// stale issue scores, not a fresh one. +const UNKNOWN_AGE_DAYS = 9999; + function issueAgeDays(value: string | null, nowMs: number): number { - if (!value) return 0; + if (!value) return UNKNOWN_AGE_DAYS; const parsed = Date.parse(value); - if (!Number.isFinite(parsed)) return 0; + if (!Number.isFinite(parsed)) return UNKNOWN_AGE_DAYS; return Math.floor((nowMs - parsed) / 86_400_000); }