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); }