mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-07-17 12:56:42 +02:00
chore(code-health): prepare v1.6.5
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
type PackageManifest = {
|
||||
version?: unknown;
|
||||
};
|
||||
|
||||
type PackageLock = {
|
||||
version?: unknown;
|
||||
packages?: Record<string, { version?: unknown } | undefined>;
|
||||
};
|
||||
|
||||
export type PackageVersionMismatch = {
|
||||
field: "package-lock.json version" | "package-lock.json packages[\"\"].version";
|
||||
expected: string;
|
||||
actual: unknown;
|
||||
};
|
||||
|
||||
export function packageVersionMismatches(
|
||||
packageJson: PackageManifest,
|
||||
packageLock: PackageLock,
|
||||
): PackageVersionMismatch[] {
|
||||
if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
|
||||
throw new Error("package.json version must be a non-empty string");
|
||||
}
|
||||
|
||||
const expected = packageJson.version;
|
||||
const rootLockVersion = packageLock.version;
|
||||
const rootPackageVersion = packageLock.packages?.[""]?.version;
|
||||
|
||||
const candidates = [
|
||||
{ field: "package-lock.json version" as const, actual: rootLockVersion },
|
||||
{ field: "package-lock.json packages[\"\"].version" as const, actual: rootPackageVersion },
|
||||
];
|
||||
|
||||
return candidates
|
||||
.filter(candidate => candidate.actual !== expected)
|
||||
.map(candidate => ({ ...candidate, expected }));
|
||||
}
|
||||
|
||||
export function formatPackageVersionMismatch(mismatch: PackageVersionMismatch): string {
|
||||
return `${mismatch.field} (${String(mismatch.actual)}) does not match package.json version (${mismatch.expected})`;
|
||||
}
|
||||
|
||||
export function packageLockReadErrorMessage(error: unknown): string {
|
||||
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
||||
if (code === "ENOENT") return "package-lock.json not found; run npm install first";
|
||||
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return `Unable to read package-lock.json; run npm install first. ${message}`;
|
||||
}
|
||||
|
||||
async function readJsonFile<T>(path: string): Promise<T> {
|
||||
return JSON.parse(await readFile(path, "utf8")) as T;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const packageJson = await readJsonFile<PackageManifest>(join(repoRoot, "package.json"));
|
||||
let packageLock: PackageLock;
|
||||
try {
|
||||
packageLock = await readJsonFile<PackageLock>(join(repoRoot, "package-lock.json"));
|
||||
} catch (error) {
|
||||
console.error(packageLockReadErrorMessage(error));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const mismatches = packageVersionMismatches(packageJson, packageLock);
|
||||
|
||||
if (mismatches.length > 0) {
|
||||
console.error("Package integrity check failed:");
|
||||
for (const mismatch of mismatches) {
|
||||
console.error(`- ${formatPackageVersionMismatch(mismatch)}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`PACKAGE_INTEGRITY_PASS version=${packageJson.version}`);
|
||||
}
|
||||
|
||||
function isMainModule(): boolean {
|
||||
const invokedPath = process.argv[1];
|
||||
return invokedPath ? import.meta.url === pathToFileURL(resolve(invokedPath)).href : false;
|
||||
}
|
||||
|
||||
if (isMainModule()) {
|
||||
await main();
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
RETENTION_TYPE_MAX,
|
||||
} from "../../../src/retention.ts";
|
||||
import { TYPES } from "../constants.ts";
|
||||
import { daysSinceIso, formatStrength } from "../retention-model.ts";
|
||||
import { formatStrength } from "../retention-model.ts";
|
||||
import { cleanText, truncate } from "../text.ts";
|
||||
import type { MemoryInspectionReadModel, RetentionDiagItem } from "../types.ts";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { EvidenceEventV1 } from "../../src/evidence-log.ts";
|
||||
import type { LongTermType } from "../../src/types.ts";
|
||||
import { countBy, objectFromCounts, uniqueStrings } from "./text.ts";
|
||||
import { countBy, objectFromCounts } from "./text.ts";
|
||||
import { groupEvidenceByMemoryId } from "./evidence-model.ts";
|
||||
import { loadRejectionRecords } from "./rejections-model.ts";
|
||||
import { snapshotForOptions } from "./workspace-snapshot.ts";
|
||||
|
||||
@@ -5,75 +5,42 @@ import { RETENTION_TYPE_MAX } from "../../src/retention.ts";
|
||||
import type { LongTermMemoryEntry, LongTermType } from "../../src/types.ts";
|
||||
import { TYPES } from "./constants.ts";
|
||||
import { disappearanceRows } from "./inspection-model.ts";
|
||||
import {
|
||||
VERSION_ANALYSIS_SAMPLE_THRESHOLD,
|
||||
VERSION_GROUPS,
|
||||
buildVersionBuckets,
|
||||
buildVersionCoverage,
|
||||
computeVersionedInference,
|
||||
hasKnownProducerVersion,
|
||||
hasProducerFields,
|
||||
producerVersionGroupFor,
|
||||
} from "./quality-versioning.ts";
|
||||
import type {
|
||||
AnswerabilityLevel,
|
||||
ProducerBearingRecord,
|
||||
ProducerVersionGroup,
|
||||
VersionCoverage,
|
||||
VersionedMechanismDiagnosticQuestion,
|
||||
VersionedMechanismFacts,
|
||||
} from "./quality-versioning.ts";
|
||||
import { hasWorkspaceScope, rejectionQualitySummary, uniqueByCanonicalText } from "./rejections-model.ts";
|
||||
import { canonicalMemoryText, cleanText, countBy, objectFromCounts, truncate, uniqueStrings, workspaceRootHash } from "./text.ts";
|
||||
import type { MemoryInspectionReadModel, NormalizedRejection } from "./types.ts";
|
||||
|
||||
export type AnswerabilityLevel = "supported" | "partial" | "inventory_only" | "not_instrumented";
|
||||
export type {
|
||||
AnswerabilityLevel,
|
||||
ProducerBearingRecord,
|
||||
ProducerVersionGroup,
|
||||
VersionAvailability,
|
||||
VersionBucketFacts,
|
||||
VersionCoverage,
|
||||
VersionedMechanismDiagnosticQuestion,
|
||||
VersionedMechanismFacts,
|
||||
VersionedMechanismInference,
|
||||
VersionSampleAssessment,
|
||||
} from "./quality-versioning.ts";
|
||||
|
||||
export type ProducerVersionGroup = "current" | "previous" | "unknown_unversioned";
|
||||
|
||||
export type VersionSampleAssessment =
|
||||
| "observed"
|
||||
| "not_observed_but_sample_small"
|
||||
| "not_observed_with_sufficient_sample"
|
||||
| "no_current_version_opportunities";
|
||||
|
||||
export type VersionAvailability = {
|
||||
noProducerFields: number;
|
||||
unknownProducerVersion: number;
|
||||
emptyProducerVersion: number;
|
||||
knownProducerVersion: number;
|
||||
};
|
||||
|
||||
export type VersionCoverage = {
|
||||
totalEvents: number;
|
||||
currentVersionEvents: number;
|
||||
previousVersionEvents: number;
|
||||
unknownVersionEvents: number;
|
||||
coveragePercent: number;
|
||||
isTransitional: boolean;
|
||||
};
|
||||
|
||||
export type VersionedMechanismInference = {
|
||||
status:
|
||||
| "current_recurrence_detected"
|
||||
| "pattern_persists_across_versions"
|
||||
| "no_current_evidence_observed"
|
||||
| "no_current_evidence_sample_small"
|
||||
| "no_current_version_opportunities"
|
||||
| "no_previous_pattern_observed";
|
||||
message: string;
|
||||
caveat: "Version grouping is based only on producerVersion strings in evidence";
|
||||
};
|
||||
|
||||
export type VersionBucketFacts<TFacts> = {
|
||||
group: ProducerVersionGroup;
|
||||
label: string;
|
||||
opportunityCount: number;
|
||||
observedPatternCount: number;
|
||||
producerVersions: Record<string, number>;
|
||||
versionAvailability: VersionAvailability;
|
||||
answerabilityLevel: AnswerabilityLevel;
|
||||
sampleAssessment: VersionSampleAssessment;
|
||||
facts: TFacts;
|
||||
};
|
||||
|
||||
export type VersionedMechanismDiagnosticQuestion = {
|
||||
mechanism: "reinforcement_rule";
|
||||
group: ProducerVersionGroup;
|
||||
question: string;
|
||||
evidence: string[];
|
||||
};
|
||||
|
||||
export type VersionedMechanismFacts<TFacts> = {
|
||||
currentPackageVersion: string;
|
||||
opportunityName: string;
|
||||
sampleThreshold: number;
|
||||
buckets: Record<ProducerVersionGroup, VersionBucketFacts<TFacts>>;
|
||||
inference: VersionedMechanismInference;
|
||||
diagnosticQuestions?: VersionedMechanismDiagnosticQuestion[];
|
||||
};
|
||||
export { hasKnownProducerVersion, producerVersionGroupFor } from "./quality-versioning.ts";
|
||||
|
||||
export type RejectionVersionFacts = {
|
||||
totalRecords: number;
|
||||
@@ -314,9 +281,6 @@ export type HeuristicFlag = {
|
||||
const ACTIVE_MEMORY_FULL_TEXT_THRESHOLD = 40;
|
||||
const REPRESENTATIVE_CANDIDATE_LIMIT = 10;
|
||||
const RECENT_EVICTION_DAYS = 7;
|
||||
const VERSION_ANALYSIS_SAMPLE_THRESHOLD = 5;
|
||||
const VERSION_GROUPS: ProducerVersionGroup[] = ["current", "previous", "unknown_unversioned"];
|
||||
const VERSION_GROUPING_CAVEAT = "Version grouping is based only on producerVersion strings in evidence" as const;
|
||||
|
||||
const KNOWN_MIGRATION_IDS = [
|
||||
"2026-04-26-p0-cleanup",
|
||||
@@ -617,7 +581,7 @@ function buildVersionedSystemMechanismFacts(
|
||||
currentPackageVersion: string,
|
||||
generatedAt: string,
|
||||
): VersionedSystemMechanismFacts {
|
||||
const versionCoverage = buildVersionCoverage(events, rejections, currentPackageVersion);
|
||||
const versionCoverage = buildVersionCoverage([...events, ...rejections], currentPackageVersion);
|
||||
return {
|
||||
currentPackageVersion,
|
||||
versionCoverage,
|
||||
@@ -761,30 +725,6 @@ function buildVersionedEvictionFacts(
|
||||
};
|
||||
}
|
||||
|
||||
function buildVersionBuckets<TRecord extends ProducerBearingRecord, TFacts>(
|
||||
records: TRecord[],
|
||||
currentPackageVersion: string,
|
||||
summarize: (records: TRecord[]) => { facts: TFacts; opportunityCount: number; observedPatternCount: number },
|
||||
): Record<ProducerVersionGroup, VersionBucketFacts<TFacts>> {
|
||||
const grouped = Object.fromEntries(VERSION_GROUPS.map(group => [group, []])) as Record<ProducerVersionGroup, TRecord[]>;
|
||||
for (const record of records) grouped[producerVersionGroupFor(record, currentPackageVersion)].push(record);
|
||||
return Object.fromEntries(VERSION_GROUPS.map(group => {
|
||||
const bucketRecords = grouped[group];
|
||||
const summary = summarize(bucketRecords);
|
||||
return [group, {
|
||||
group,
|
||||
label: versionGroupLabel(group, currentPackageVersion),
|
||||
opportunityCount: summary.opportunityCount,
|
||||
observedPatternCount: summary.observedPatternCount,
|
||||
producerVersions: producerVersionCounts(bucketRecords),
|
||||
versionAvailability: buildVersionAvailability(bucketRecords),
|
||||
answerabilityLevel: group === "current" && summary.opportunityCount > 0 ? "partial" : "inventory_only",
|
||||
sampleAssessment: sampleAssessmentFor(group, summary.opportunityCount, summary.observedPatternCount, currentPackageVersion),
|
||||
facts: summary.facts,
|
||||
} satisfies VersionBucketFacts<TFacts>];
|
||||
})) as Record<ProducerVersionGroup, VersionBucketFacts<TFacts>>;
|
||||
}
|
||||
|
||||
function buildEvictionVersionFacts(capacityEvents: EvidenceEventV1[], generatedAt: string): EvictionVersionFacts {
|
||||
const recentCapacityEvents = capacityEvents.filter(event => isWithinDaysOf(event.createdAt, generatedAt, RECENT_EVICTION_DAYS));
|
||||
const capacityEventsWithSnapshot = capacityEvents.filter(hasCapacitySnapshot);
|
||||
@@ -808,145 +748,6 @@ function isReviewableRejectionCandidate(record: NormalizedRejection): boolean {
|
||||
return label === "architecture_like_rejected_candidate" || label === "ambiguous_rejected_candidate";
|
||||
}
|
||||
|
||||
function producerVersionCounts(records: ProducerBearingRecord[]): Record<string, number> {
|
||||
const counts: Record<string, number> = {};
|
||||
for (const record of records) {
|
||||
if (!hasKnownProducerVersion(record)) continue;
|
||||
const version = String(record.producerVersion).trim();
|
||||
counts[version] = (counts[version] ?? 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function versionGroupLabel(group: ProducerVersionGroup, currentPackageVersion: string): string {
|
||||
if (group === "current") return `current version ${currentPackageVersion}`;
|
||||
if (group === "previous") return "previous versions";
|
||||
return "unknown/unversioned";
|
||||
}
|
||||
|
||||
function sampleAssessmentFor(
|
||||
group: ProducerVersionGroup,
|
||||
opportunityCount: number,
|
||||
observedPatternCount: number,
|
||||
currentPackageVersion: string,
|
||||
): VersionSampleAssessment {
|
||||
if (observedPatternCount > 0) return "observed";
|
||||
if (group === "current" && (!isAssessableCurrentPackageVersion(currentPackageVersion) || opportunityCount === 0)) return "no_current_version_opportunities";
|
||||
if (opportunityCount < VERSION_ANALYSIS_SAMPLE_THRESHOLD) return "not_observed_but_sample_small";
|
||||
return "not_observed_with_sufficient_sample";
|
||||
}
|
||||
|
||||
function isAssessableCurrentPackageVersion(currentPackageVersion: string): boolean {
|
||||
const trimmed = currentPackageVersion.trim();
|
||||
return trimmed.length > 0 && trimmed !== "unknown";
|
||||
}
|
||||
|
||||
function computeVersionedInference<TFacts>(
|
||||
mechanism: Omit<VersionedMechanismFacts<TFacts>, "inference">,
|
||||
text: { observedPattern: string; patternName: string },
|
||||
): VersionedMechanismInference {
|
||||
const current = mechanism.buckets.current;
|
||||
const previous = mechanism.buckets.previous;
|
||||
const currentFact = `Current version: ${current.observedPatternCount} ${text.observedPattern} in ${current.opportunityCount} ${mechanism.opportunityName}.`;
|
||||
const previousFact = `Previous versions: ${previous.observedPatternCount} ${text.observedPattern} in ${previous.opportunityCount} ${mechanism.opportunityName}.`;
|
||||
const unknownUnversioned = mechanism.buckets.unknown_unversioned;
|
||||
if (!isAssessableCurrentPackageVersion(mechanism.currentPackageVersion) || current.opportunityCount === 0) {
|
||||
return inference("no_current_version_opportunities", "Current package version is unknown or has no events; cannot assess recurrence.");
|
||||
}
|
||||
if (current.observedPatternCount > 0 && previous.observedPatternCount === 0 && unknownUnversioned.observedPatternCount === 0) {
|
||||
return inference("no_previous_pattern_observed", `${currentFact} No previous pattern observed — this is a new pattern, not a recurrence.`);
|
||||
}
|
||||
if (current.observedPatternCount > 0) {
|
||||
if (previous.observedPatternCount > 0) {
|
||||
return inference("pattern_persists_across_versions", `${currentFact} ${previousFact} Current recurrence detected — ${text.patternName} observed in current version. Pattern persists across versions.`);
|
||||
}
|
||||
// Current has signal, previous has none, but unknown/unversioned has signal
|
||||
return inference("current_recurrence_detected", `${currentFact} No known previous-version pattern observed, but unknown/unversioned evidence shows ${unknownUnversioned.observedPatternCount} ${text.observedPattern}. Pattern may persist — version grouping cannot confirm or deny.`);
|
||||
}
|
||||
if (current.opportunityCount < mechanism.sampleThreshold) {
|
||||
return inference("no_current_evidence_sample_small", `${currentFact} ${previousFact} No current evidence observed, but current-version opportunity count is ${current.opportunityCount} (<${mechanism.sampleThreshold}); do not infer absence.`);
|
||||
}
|
||||
return inference("no_current_evidence_observed", `${currentFact} ${previousFact} No recurrence observed with sufficient current-version sample.`);
|
||||
}
|
||||
|
||||
function inference(status: VersionedMechanismInference["status"], message: string): VersionedMechanismInference {
|
||||
return { status, message, caveat: VERSION_GROUPING_CAVEAT };
|
||||
}
|
||||
|
||||
function hasProducerFields(record: Pick<EvidenceEventV1, "producerName" | "producerVersion" | "instrumentationVersion"> | Pick<NormalizedRejection, "producerName" | "producerVersion" | "instrumentationVersion">): boolean {
|
||||
return typeof record.producerName === "string"
|
||||
&& record.producerName.length > 0
|
||||
&& typeof record.producerVersion === "string"
|
||||
&& record.producerVersion.length > 0
|
||||
&& typeof record.instrumentationVersion === "number";
|
||||
}
|
||||
|
||||
type ProducerBearingRecord = Pick<EvidenceEventV1 | NormalizedRejection, "producerName" | "producerVersion" | "instrumentationVersion">;
|
||||
|
||||
export function hasKnownProducerVersion(record: ProducerBearingRecord): boolean {
|
||||
if (typeof record.producerVersion !== "string") return false;
|
||||
const producerVersion = record.producerVersion.trim();
|
||||
return producerVersion.length > 0 && producerVersion !== "unknown";
|
||||
}
|
||||
|
||||
export function producerVersionGroupFor(record: ProducerBearingRecord, currentPackageVersion: string): ProducerVersionGroup {
|
||||
if (!hasKnownProducerVersion(record)) return "unknown_unversioned";
|
||||
const producerVersion = String(record.producerVersion).trim();
|
||||
const currentVersion = currentPackageVersion.trim();
|
||||
if (currentVersion.length > 0 && currentVersion !== "unknown" && producerVersion === currentVersion) return "current";
|
||||
return "previous";
|
||||
}
|
||||
|
||||
function buildVersionAvailability(records: ProducerBearingRecord[]): VersionAvailability {
|
||||
const availability: VersionAvailability = {
|
||||
noProducerFields: 0,
|
||||
unknownProducerVersion: 0,
|
||||
emptyProducerVersion: 0,
|
||||
knownProducerVersion: 0,
|
||||
};
|
||||
for (const record of records) {
|
||||
const hasAnyProducerField = typeof record.producerName === "string"
|
||||
|| typeof record.producerVersion === "string"
|
||||
|| typeof record.instrumentationVersion === "number";
|
||||
if (!hasAnyProducerField) {
|
||||
availability.noProducerFields += 1;
|
||||
continue;
|
||||
}
|
||||
if (typeof record.producerVersion !== "string" || record.producerVersion.trim().length === 0) {
|
||||
availability.emptyProducerVersion += 1;
|
||||
continue;
|
||||
}
|
||||
if (record.producerVersion.trim() === "unknown") {
|
||||
availability.unknownProducerVersion += 1;
|
||||
continue;
|
||||
}
|
||||
availability.knownProducerVersion += 1;
|
||||
}
|
||||
return availability;
|
||||
}
|
||||
|
||||
function buildVersionCoverage(events: EvidenceEventV1[], rejections: NormalizedRejection[], currentPackageVersion: string): VersionCoverage {
|
||||
const coverage: VersionCoverage = {
|
||||
totalEvents: events.length + rejections.length,
|
||||
currentVersionEvents: 0,
|
||||
previousVersionEvents: 0,
|
||||
unknownVersionEvents: 0,
|
||||
coveragePercent: 0,
|
||||
isTransitional: true,
|
||||
};
|
||||
for (const record of [...events, ...rejections]) {
|
||||
const group = producerVersionGroupFor(record, currentPackageVersion);
|
||||
if (group === "current") coverage.currentVersionEvents += 1;
|
||||
if (group === "previous") coverage.previousVersionEvents += 1;
|
||||
if (group === "unknown_unversioned") coverage.unknownVersionEvents += 1;
|
||||
}
|
||||
coverage.coveragePercent = coverage.totalEvents === 0
|
||||
? 0
|
||||
: Math.round(((coverage.currentVersionEvents + coverage.previousVersionEvents) / coverage.totalEvents) * 1000) / 10;
|
||||
coverage.isTransitional = coverage.coveragePercent < 50;
|
||||
return coverage;
|
||||
}
|
||||
|
||||
function typeCountsFor(entries: LongTermMemoryEntry[]): Record<string, number> {
|
||||
return Object.fromEntries(TYPES.map(type => [type, entries.filter(entry => entry.type === type).length]));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
export type AnswerabilityLevel = "supported" | "partial" | "inventory_only" | "not_instrumented";
|
||||
|
||||
export type ProducerVersionGroup = "current" | "previous" | "unknown_unversioned";
|
||||
|
||||
export type ProducerBearingRecord = {
|
||||
producerName?: string;
|
||||
producerVersion?: string;
|
||||
instrumentationVersion?: number;
|
||||
};
|
||||
|
||||
export type VersionSampleAssessment =
|
||||
| "observed"
|
||||
| "not_observed_but_sample_small"
|
||||
| "not_observed_with_sufficient_sample"
|
||||
| "no_current_version_opportunities";
|
||||
|
||||
export type VersionAvailability = {
|
||||
noProducerFields: number;
|
||||
unknownProducerVersion: number;
|
||||
emptyProducerVersion: number;
|
||||
knownProducerVersion: number;
|
||||
};
|
||||
|
||||
export type VersionCoverage = {
|
||||
totalEvents: number;
|
||||
currentVersionEvents: number;
|
||||
previousVersionEvents: number;
|
||||
unknownVersionEvents: number;
|
||||
coveragePercent: number;
|
||||
isTransitional: boolean;
|
||||
};
|
||||
|
||||
export const VERSION_ANALYSIS_SAMPLE_THRESHOLD = 5;
|
||||
export const VERSION_GROUPS: ProducerVersionGroup[] = ["current", "previous", "unknown_unversioned"];
|
||||
export const VERSION_GROUPING_CAVEAT = "Version grouping is based only on producerVersion strings in evidence" as const;
|
||||
|
||||
export type VersionedMechanismInference = {
|
||||
status:
|
||||
| "current_recurrence_detected"
|
||||
| "pattern_persists_across_versions"
|
||||
| "no_current_evidence_observed"
|
||||
| "no_current_evidence_sample_small"
|
||||
| "no_current_version_opportunities"
|
||||
| "no_previous_pattern_observed";
|
||||
message: string;
|
||||
caveat: typeof VERSION_GROUPING_CAVEAT;
|
||||
};
|
||||
|
||||
export type VersionBucketFacts<TFacts> = {
|
||||
group: ProducerVersionGroup;
|
||||
label: string;
|
||||
opportunityCount: number;
|
||||
observedPatternCount: number;
|
||||
producerVersions: Record<string, number>;
|
||||
versionAvailability: VersionAvailability;
|
||||
answerabilityLevel: AnswerabilityLevel;
|
||||
sampleAssessment: VersionSampleAssessment;
|
||||
facts: TFacts;
|
||||
};
|
||||
|
||||
export type VersionedMechanismDiagnosticQuestion = {
|
||||
mechanism: "reinforcement_rule";
|
||||
group: ProducerVersionGroup;
|
||||
question: string;
|
||||
evidence: string[];
|
||||
};
|
||||
|
||||
export type VersionedMechanismFacts<TFacts> = {
|
||||
currentPackageVersion: string;
|
||||
opportunityName: string;
|
||||
sampleThreshold: number;
|
||||
buckets: Record<ProducerVersionGroup, VersionBucketFacts<TFacts>>;
|
||||
inference: VersionedMechanismInference;
|
||||
diagnosticQuestions?: VersionedMechanismDiagnosticQuestion[];
|
||||
};
|
||||
|
||||
export function buildVersionBuckets<TRecord extends ProducerBearingRecord, TFacts>(
|
||||
records: TRecord[],
|
||||
currentPackageVersion: string,
|
||||
summarize: (records: TRecord[]) => { facts: TFacts; opportunityCount: number; observedPatternCount: number },
|
||||
): Record<ProducerVersionGroup, VersionBucketFacts<TFacts>> {
|
||||
const grouped = Object.fromEntries(VERSION_GROUPS.map(group => [group, []])) as Record<ProducerVersionGroup, TRecord[]>;
|
||||
for (const record of records) grouped[producerVersionGroupFor(record, currentPackageVersion)].push(record);
|
||||
return Object.fromEntries(VERSION_GROUPS.map(group => {
|
||||
const bucketRecords = grouped[group];
|
||||
const summary = summarize(bucketRecords);
|
||||
return [group, {
|
||||
group,
|
||||
label: versionGroupLabel(group, currentPackageVersion),
|
||||
opportunityCount: summary.opportunityCount,
|
||||
observedPatternCount: summary.observedPatternCount,
|
||||
producerVersions: producerVersionCounts(bucketRecords),
|
||||
versionAvailability: buildVersionAvailability(bucketRecords),
|
||||
answerabilityLevel: group === "current" && summary.opportunityCount > 0 ? "partial" : "inventory_only",
|
||||
sampleAssessment: sampleAssessmentFor(group, summary.opportunityCount, summary.observedPatternCount, currentPackageVersion),
|
||||
facts: summary.facts,
|
||||
} satisfies VersionBucketFacts<TFacts>];
|
||||
})) as Record<ProducerVersionGroup, VersionBucketFacts<TFacts>>;
|
||||
}
|
||||
|
||||
export function computeVersionedInference<TFacts>(
|
||||
mechanism: Omit<VersionedMechanismFacts<TFacts>, "inference">,
|
||||
text: { observedPattern: string; patternName: string },
|
||||
): VersionedMechanismInference {
|
||||
const current = mechanism.buckets.current;
|
||||
const previous = mechanism.buckets.previous;
|
||||
const currentFact = `Current version: ${current.observedPatternCount} ${text.observedPattern} in ${current.opportunityCount} ${mechanism.opportunityName}.`;
|
||||
const previousFact = `Previous versions: ${previous.observedPatternCount} ${text.observedPattern} in ${previous.opportunityCount} ${mechanism.opportunityName}.`;
|
||||
const unknownUnversioned = mechanism.buckets.unknown_unversioned;
|
||||
if (!isAssessableCurrentPackageVersion(mechanism.currentPackageVersion) || current.opportunityCount === 0) {
|
||||
return inference("no_current_version_opportunities", "Current package version is unknown or has no events; cannot assess recurrence.");
|
||||
}
|
||||
if (current.observedPatternCount > 0 && previous.observedPatternCount === 0 && unknownUnversioned.observedPatternCount === 0) {
|
||||
return inference("no_previous_pattern_observed", `${currentFact} No previous pattern observed — this is a new pattern, not a recurrence.`);
|
||||
}
|
||||
if (current.observedPatternCount > 0) {
|
||||
if (previous.observedPatternCount > 0) {
|
||||
return inference("pattern_persists_across_versions", `${currentFact} ${previousFact} Current recurrence detected — ${text.patternName} observed in current version. Pattern persists across versions.`);
|
||||
}
|
||||
// Current has signal, previous has none, but unknown/unversioned has signal
|
||||
return inference("current_recurrence_detected", `${currentFact} No known previous-version pattern observed, but unknown/unversioned evidence shows ${unknownUnversioned.observedPatternCount} ${text.observedPattern}. Pattern may persist — version grouping cannot confirm or deny.`);
|
||||
}
|
||||
if (current.opportunityCount < mechanism.sampleThreshold) {
|
||||
return inference("no_current_evidence_sample_small", `${currentFact} ${previousFact} No current evidence observed, but current-version opportunity count is ${current.opportunityCount} (<${mechanism.sampleThreshold}); do not infer absence.`);
|
||||
}
|
||||
return inference("no_current_evidence_observed", `${currentFact} ${previousFact} No recurrence observed with sufficient current-version sample.`);
|
||||
}
|
||||
|
||||
export function hasProducerFields(record: ProducerBearingRecord): boolean {
|
||||
return typeof record.producerName === "string"
|
||||
&& record.producerName.length > 0
|
||||
&& typeof record.producerVersion === "string"
|
||||
&& record.producerVersion.length > 0
|
||||
&& typeof record.instrumentationVersion === "number";
|
||||
}
|
||||
|
||||
export function hasKnownProducerVersion(record: ProducerBearingRecord): boolean {
|
||||
if (typeof record.producerVersion !== "string") return false;
|
||||
const producerVersion = record.producerVersion.trim();
|
||||
return producerVersion.length > 0 && producerVersion !== "unknown";
|
||||
}
|
||||
|
||||
export function producerVersionGroupFor(record: ProducerBearingRecord, currentPackageVersion: string): ProducerVersionGroup {
|
||||
if (!hasKnownProducerVersion(record)) return "unknown_unversioned";
|
||||
const producerVersion = String(record.producerVersion).trim();
|
||||
const currentVersion = currentPackageVersion.trim();
|
||||
if (currentVersion.length > 0 && currentVersion !== "unknown" && producerVersion === currentVersion) return "current";
|
||||
return "previous";
|
||||
}
|
||||
|
||||
export function buildVersionCoverage(records: ProducerBearingRecord[], currentPackageVersion: string): VersionCoverage {
|
||||
const coverage: VersionCoverage = {
|
||||
totalEvents: records.length,
|
||||
currentVersionEvents: 0,
|
||||
previousVersionEvents: 0,
|
||||
unknownVersionEvents: 0,
|
||||
coveragePercent: 0,
|
||||
isTransitional: true,
|
||||
};
|
||||
for (const record of records) {
|
||||
const group = producerVersionGroupFor(record, currentPackageVersion);
|
||||
if (group === "current") coverage.currentVersionEvents += 1;
|
||||
if (group === "previous") coverage.previousVersionEvents += 1;
|
||||
if (group === "unknown_unversioned") coverage.unknownVersionEvents += 1;
|
||||
}
|
||||
coverage.coveragePercent = coverage.totalEvents === 0
|
||||
? 0
|
||||
: Math.round(((coverage.currentVersionEvents + coverage.previousVersionEvents) / coverage.totalEvents) * 1000) / 10;
|
||||
coverage.isTransitional = coverage.coveragePercent < 50;
|
||||
return coverage;
|
||||
}
|
||||
|
||||
function producerVersionCounts(records: ProducerBearingRecord[]): Record<string, number> {
|
||||
const counts: Record<string, number> = {};
|
||||
for (const record of records) {
|
||||
if (!hasKnownProducerVersion(record)) continue;
|
||||
const version = String(record.producerVersion).trim();
|
||||
counts[version] = (counts[version] ?? 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function versionGroupLabel(group: ProducerVersionGroup, currentPackageVersion: string): string {
|
||||
if (group === "current") return `current version ${currentPackageVersion}`;
|
||||
if (group === "previous") return "previous versions";
|
||||
return "unknown/unversioned";
|
||||
}
|
||||
|
||||
function sampleAssessmentFor(
|
||||
group: ProducerVersionGroup,
|
||||
opportunityCount: number,
|
||||
observedPatternCount: number,
|
||||
currentPackageVersion: string,
|
||||
): VersionSampleAssessment {
|
||||
if (observedPatternCount > 0) return "observed";
|
||||
if (group === "current" && (!isAssessableCurrentPackageVersion(currentPackageVersion) || opportunityCount === 0)) return "no_current_version_opportunities";
|
||||
if (opportunityCount < VERSION_ANALYSIS_SAMPLE_THRESHOLD) return "not_observed_but_sample_small";
|
||||
return "not_observed_with_sufficient_sample";
|
||||
}
|
||||
|
||||
function isAssessableCurrentPackageVersion(currentPackageVersion: string): boolean {
|
||||
const trimmed = currentPackageVersion.trim();
|
||||
return trimmed.length > 0 && trimmed !== "unknown";
|
||||
}
|
||||
|
||||
function inference(status: VersionedMechanismInference["status"], message: string): VersionedMechanismInference {
|
||||
return { status, message, caveat: VERSION_GROUPING_CAVEAT };
|
||||
}
|
||||
|
||||
function buildVersionAvailability(records: ProducerBearingRecord[]): VersionAvailability {
|
||||
const availability: VersionAvailability = {
|
||||
noProducerFields: 0,
|
||||
unknownProducerVersion: 0,
|
||||
emptyProducerVersion: 0,
|
||||
knownProducerVersion: 0,
|
||||
};
|
||||
for (const record of records) {
|
||||
const hasAnyProducerField = typeof record.producerName === "string"
|
||||
|| typeof record.producerVersion === "string"
|
||||
|| typeof record.instrumentationVersion === "number";
|
||||
if (!hasAnyProducerField) {
|
||||
availability.noProducerFields += 1;
|
||||
continue;
|
||||
}
|
||||
if (typeof record.producerVersion !== "string" || record.producerVersion.trim().length === 0) {
|
||||
availability.emptyProducerVersion += 1;
|
||||
continue;
|
||||
}
|
||||
if (record.producerVersion.trim() === "unknown") {
|
||||
availability.unknownProducerVersion += 1;
|
||||
continue;
|
||||
}
|
||||
availability.knownProducerVersion += 1;
|
||||
}
|
||||
return availability;
|
||||
}
|
||||
Reference in New Issue
Block a user