@dailephd/my-dev-kit-lab 0.6.1 → 0.6.3

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +9 -7
  3. package/benchmarks/contracts/incremental-change-staleness-scenarios.json +360 -0
  4. package/dist/src/cli/help.js +16 -9
  5. package/dist/src/commands/runExperimentDescribeCommand.js +5 -2
  6. package/dist/src/commands/runExperimentRunCommand.js +4 -7
  7. package/dist/src/evaluation/affectedNeighborhoodMetrics.js +51 -0
  8. package/dist/src/evaluation/benchmarkMetadata.js +5 -1
  9. package/dist/src/evaluation/parseAgentAnswer.js +172 -24
  10. package/dist/src/evaluation/runMyDevKitRetrieval.js +120 -7
  11. package/dist/src/experiments/defaultRegistry.js +2 -0
  12. package/dist/src/experiments/plugins/incrementalChangeStaleness/comparison.js +101 -0
  13. package/dist/src/experiments/plugins/incrementalChangeStaleness/comparisonV2.js +49 -0
  14. package/dist/src/experiments/plugins/incrementalChangeStaleness/config.js +67 -0
  15. package/dist/src/experiments/plugins/incrementalChangeStaleness/disposableTarget.js +125 -0
  16. package/dist/src/experiments/plugins/incrementalChangeStaleness/execution.js +430 -0
  17. package/dist/src/experiments/plugins/incrementalChangeStaleness/executionArtifact.js +160 -0
  18. package/dist/src/experiments/plugins/incrementalChangeStaleness/executionArtifactV2.js +289 -0
  19. package/dist/src/experiments/plugins/incrementalChangeStaleness/executionV2.js +195 -0
  20. package/dist/src/experiments/plugins/incrementalChangeStaleness/index.js +13 -0
  21. package/dist/src/experiments/plugins/incrementalChangeStaleness/indexClone.js +76 -0
  22. package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecycle.js +308 -0
  23. package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecyclePolicy.js +154 -0
  24. package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecyclePolicyV2.js +78 -0
  25. package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecycleV2.js +298 -0
  26. package/dist/src/experiments/plugins/incrementalChangeStaleness/mutationExecution.js +144 -0
  27. package/dist/src/experiments/plugins/incrementalChangeStaleness/plugin.js +475 -0
  28. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioCatalog.js +42 -0
  29. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioMutation.js +116 -0
  30. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioSelection.js +79 -0
  31. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioTypes.js +37 -0
  32. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioValidation.js +168 -0
  33. package/dist/src/experiments/plugins/incrementalChangeStaleness/sourceState.js +145 -0
  34. package/dist/src/experiments/plugins/incrementalChangeStaleness/treatmentSession.js +6 -0
  35. package/dist/src/experiments/plugins/incrementalChangeStaleness/treatmentSessionV2.js +5 -0
  36. package/dist/src/experiments/plugins/index.js +1 -0
  37. package/dist/src/experiments/plugins/warmIndexReuse/metrics.js +17 -20
  38. package/dist/src/experiments/plugins/warmIndexReuse/warmIndexSession.js +3 -1
  39. package/dist/src/mobile/android/advancedSecurity/candidateEvidence.js +9 -2
  40. package/dist/src/mobile/android/advancedSecurity/redaction.js +19 -10
  41. package/dist/src/mobile/android/advancedSecurity/secretCandidates/analyzeSecretCandidates.js +12 -2
  42. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/analyzeSigningConfiguration.js +1 -1
  43. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/extractSigningConfigurations.js +7 -7
  44. package/dist/src/report/experiments/buildIncrementalChangeStalenessPluginReport.js +33 -0
  45. package/dist/src/report/experiments/buildIncrementalChangeStalenessReport.js +112 -0
  46. package/dist/src/report/experiments/buildIncrementalChangeStalenessReportV2.js +80 -0
  47. package/dist/src/report/experiments/buildPluginExperimentReport.js +3 -0
  48. package/dist/src/report/experiments/incrementalChangeStalenessReportModel.js +4 -0
  49. package/dist/src/report/experiments/incrementalChangeStalenessReportModelV2.js +3 -0
  50. package/dist/src/report/experiments/incrementalChangeStalenessReportRowsV2.js +146 -0
  51. package/dist/src/report/experiments/index.js +6 -0
  52. package/dist/src/report/experiments/renderIncrementalChangeStalenessHtml.js +243 -0
  53. package/dist/src/report/experiments/renderIncrementalChangeStalenessHtmlV2.js +111 -0
  54. package/dist/src/report/experiments/renderIncrementalChangeStalenessTextV2.js +96 -0
  55. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +3 -0
  56. package/dist/src/report/experiments/renderPluginExperimentReportText.js +144 -0
  57. package/dist/src/securityValidation/staticScans/codeql.js +8 -8
  58. package/docs/METRICS.md +67 -1
  59. package/examples/tutorial-browser/prepare.mjs +13 -13
  60. package/examples/tutorial-browser/server.mjs +13 -13
  61. package/package.json +1 -1
@@ -0,0 +1,160 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { resolveWithinRoot } from "../../../core/pathSafety.js";
3
+ // ---------------------------------------------------------------------------
4
+ // v0.6.2 Batch 4 -- final persisted execution/comparison artifact. Owns only
5
+ // the artifact types, the schema constant, the bounded projection from
6
+ // runtime evidence, summary counts, and writing the artifact file. No
7
+ // comparison formula and no artifact serialization logic for another plugin
8
+ // lives here.
9
+ // ---------------------------------------------------------------------------
10
+ export const INCREMENTAL_CHANGE_STALENESS_EXECUTION_ARTIFACT_FILE = "incremental-change-staleness-execution.json";
11
+ export const INCREMENTAL_CHANGE_STALENESS_EXECUTION_SCHEMA_VERSION = "my-dev-kit-lab-incremental-change-staleness-execution-v1";
12
+ export function sortedUnique(values) {
13
+ return [...new Set(values)].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
14
+ }
15
+ function summarizeCommand(command) {
16
+ return {
17
+ commandId: command.commandId,
18
+ ok: command.ok,
19
+ exitCode: command.exitCode,
20
+ durationMs: command.durationMs,
21
+ stdoutPath: command.stdoutPath,
22
+ stderrPath: command.stderrPath,
23
+ telemetryPath: command.telemetryPath,
24
+ error: command.error ?? null
25
+ };
26
+ }
27
+ export function summarizeRetrieval(treatment) {
28
+ const retrieval = treatment.retrieval;
29
+ if (!retrieval) {
30
+ return {
31
+ status: "not-run",
32
+ warnings: [],
33
+ totalChars: 0,
34
+ totalEstimatedTokens: 0,
35
+ tokenCountMethod: "unavailable",
36
+ filesRead: [],
37
+ selectedNodeId: null,
38
+ selectedFile: null,
39
+ selectedSymbol: null,
40
+ durationMs: 0,
41
+ commands: []
42
+ };
43
+ }
44
+ return {
45
+ // Bounded status only; never embeds retrieval.contextText (the full context body).
46
+ status: treatment.retrievalStatus === "not-run" ? "not-run" : treatment.retrievalStatus,
47
+ warnings: [...retrieval.warnings],
48
+ totalChars: retrieval.totalChars,
49
+ totalEstimatedTokens: retrieval.totalEstimatedTokens,
50
+ tokenCountMethod: retrieval.tokenCountMethod,
51
+ filesRead: sortedUnique(retrieval.filesRead),
52
+ selectedNodeId: retrieval.selectedNodeId ?? null,
53
+ selectedFile: retrieval.selectedFile ?? null,
54
+ selectedSymbol: retrieval.selectedSymbol ?? null,
55
+ durationMs: retrieval.durationMs,
56
+ commands: retrieval.commands.map(summarizeCommand)
57
+ };
58
+ }
59
+ export function summarizeFakeAgent(treatment) {
60
+ if (!treatment.fakeAgent)
61
+ return null;
62
+ const agent = treatment.fakeAgent;
63
+ return {
64
+ status: agent.status,
65
+ correctness: { ...agent.correctness, failureReasons: [...agent.correctness.failureReasons] },
66
+ correctnessDetail: agent.correctnessDetail ? { ...agent.correctnessDetail, failureReasons: [...agent.correctnessDetail.failureReasons] } : null,
67
+ durationMs: agent.durationMs,
68
+ warnings: [...agent.warnings],
69
+ errors: [...agent.errors]
70
+ };
71
+ }
72
+ export function summarizeRequiredFileEvidence(evidence) {
73
+ return {
74
+ status: evidence.status,
75
+ requiredFiles: sortedUnique(evidence.requiredFiles),
76
+ observedFiles: sortedUnique(evidence.observedFiles),
77
+ missingFiles: sortedUnique(evidence.missingFiles),
78
+ reason: evidence.reason
79
+ };
80
+ }
81
+ function summarizeTreatment(treatment) {
82
+ if (!treatment)
83
+ return null;
84
+ return {
85
+ treatmentId: treatment.treatmentId,
86
+ status: treatment.status,
87
+ failureReason: treatment.failureReason,
88
+ activeIndexPhase: treatment.activeIndexPhase,
89
+ baselineFreshness: structuredClone(treatment.baselineFreshness),
90
+ affectedNeighborhood: structuredClone(treatment.affectedNeighborhood),
91
+ retrieval: summarizeRetrieval(treatment),
92
+ fakeAgent: summarizeFakeAgent(treatment),
93
+ requiredFileEvidence: summarizeRequiredFileEvidence(treatment.requiredFileEvidence)
94
+ };
95
+ }
96
+ function summarizeLifecycle(execution) {
97
+ const session = execution.session;
98
+ if (!session)
99
+ return null;
100
+ return {
101
+ preMutationEquivalence: session.preMutationEquivalence.result,
102
+ postMutationEquivalence: session.postMutationEquivalence.result,
103
+ controlledFilePaths: [...session.controlledChangedPaths],
104
+ staleBaselineFreshnessStatus: session.treatments["stale-index"].changeAuthority.postMutationBaselineFreshness.status,
105
+ fullRefreshBaselineFreshnessStatus: session.treatments["full-refresh"].changeAuthority.postMutationBaselineFreshness.status,
106
+ fullRefreshRefreshedFreshnessStatus: session.treatments["full-refresh"].refreshedFreshness?.status ?? null,
107
+ myDevKitVersion: session.toolIdentity.version ?? null,
108
+ sourceRoots: [...session.baseCase.sourceRoots]
109
+ };
110
+ }
111
+ function summarizeScenario(execution) {
112
+ return {
113
+ scenarioId: execution.scenarioId,
114
+ category: execution.category,
115
+ benchmarkProjectId: execution.benchmarkProjectId,
116
+ baseCaseId: execution.baseCaseId,
117
+ answerPolicy: execution.answerPolicy,
118
+ query: execution.query,
119
+ expectedFiles: sortedUnique(execution.expectedFiles),
120
+ expectedSymbols: sortedUnique(execution.expectedSymbols),
121
+ status: execution.status,
122
+ failureReason: execution.failureReason,
123
+ lifecycle: summarizeLifecycle(execution),
124
+ // Treatment order in the persisted record is always stale-index then full-refresh.
125
+ stale: summarizeTreatment(execution.stale),
126
+ fullRefresh: summarizeTreatment(execution.fullRefresh),
127
+ comparison: { ...execution.comparison, reasonCodes: [...execution.comparison.reasonCodes] }
128
+ };
129
+ }
130
+ function summarize(scenarios) {
131
+ return {
132
+ scenarioCount: scenarios.length,
133
+ readyScenarioCount: scenarios.filter((scenario) => scenario.status === "ready").length,
134
+ failedScenarioCount: scenarios.filter((scenario) => scenario.status === "failed").length,
135
+ observedStaleRegressionCount: scenarios.filter((scenario) => scenario.comparison.staleRiskClassification === "observed-stale-regression").length,
136
+ noObservedStaleRegressionCount: scenarios.filter((scenario) => scenario.comparison.staleRiskClassification === "no-observed-stale-regression").length,
137
+ inconclusiveCount: scenarios.filter((scenario) => scenario.comparison.staleRiskClassification === "inconclusive").length
138
+ };
139
+ }
140
+ /**
141
+ * Builds the final artifact from already-executed scenario evidence, in the exact order the
142
+ * caller supplies (canonical selected-scenario order). Treatment order within each scenario is
143
+ * always stale-index then full-refresh (`INCREMENTAL_CHANGE_STALENESS_TREATMENT_IDS`).
144
+ */
145
+ export function buildIncrementalChangeStalenessExecutionArtifact(args) {
146
+ const scenarios = args.executions.map(summarizeScenario);
147
+ return {
148
+ schemaVersion: INCREMENTAL_CHANGE_STALENESS_EXECUTION_SCHEMA_VERSION,
149
+ runId: args.runId,
150
+ pluginId: args.pluginId,
151
+ scenarios,
152
+ summary: summarize(scenarios)
153
+ };
154
+ }
155
+ export async function writeIncrementalChangeStalenessExecutionArtifact(outDir, artifact) {
156
+ await mkdir(outDir, { recursive: true });
157
+ const artifactPath = resolveWithinRoot(outDir, INCREMENTAL_CHANGE_STALENESS_EXECUTION_ARTIFACT_FILE);
158
+ await writeFile(artifactPath, `${JSON.stringify(artifact, null, 2)}\n`, "utf8");
159
+ return artifactPath;
160
+ }
@@ -0,0 +1,289 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { resolveWithinRoot } from "../../../core/pathSafety.js";
3
+ import { classifyStaleRisk } from "./comparison.js";
4
+ import { classifyPartialRefreshAgainstFull } from "./comparisonV2.js";
5
+ import { INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_IDS, INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_INTENTS } from "./disposableTarget.js";
6
+ import { INCREMENTAL_CHANGE_STALENESS_EXECUTION_ARTIFACT_FILE, INCREMENTAL_CHANGE_STALENESS_EXECUTION_SCHEMA_VERSION, sortedUnique, summarizeFakeAgent, summarizeRequiredFileEvidence, summarizeRetrieval } from "./executionArtifact.js";
7
+ // ---------------------------------------------------------------------------
8
+ // v0.6.3 Batch 3 -- persisted four-treatment execution artifact. A distinct schema from V1 written to the
9
+ // same canonical filename; the schemaVersion is the discriminator. Bounded structured evidence only:
10
+ // never raw retrieved context bodies, graph artifacts, or file contents. Descriptive counts, no ranking.
11
+ // ---------------------------------------------------------------------------
12
+ export const INCREMENTAL_CHANGE_STALENESS_EXECUTION_SCHEMA_VERSION_V2 = "my-dev-kit-lab-incremental-change-staleness-execution-v2";
13
+ const PARTIAL_IDS = ["changed-files-refresh", "affected-neighborhood-refresh"];
14
+ const REQUESTED_SCOPE = {
15
+ "changed-files-refresh": "changed-files",
16
+ "affected-neighborhood-refresh": "affected-neighborhood"
17
+ };
18
+ const COMPARISON_CANDIDATE_ORDER = ["stale-index", "changed-files-refresh", "affected-neighborhood-refresh"];
19
+ function record(build) {
20
+ return Object.fromEntries(INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_IDS.map((id) => [id, build(id)]));
21
+ }
22
+ function summarizeTreatmentV2(treatment) {
23
+ return {
24
+ treatmentId: treatment.treatmentId,
25
+ treatmentIntent: treatment.treatmentIntent,
26
+ status: treatment.status,
27
+ failureReason: treatment.failureReason,
28
+ activeIndexPhase: treatment.activeIndexPhase,
29
+ baselineFreshness: structuredClone(treatment.baselineFreshness),
30
+ refreshedFreshness: treatment.refreshedFreshness ? structuredClone(treatment.refreshedFreshness) : null,
31
+ refreshExecution: structuredClone(treatment.refreshExecution),
32
+ affectedNeighborhood: structuredClone(treatment.affectedNeighborhood),
33
+ // Bounded status/telemetry only; never embeds retrieval.contextText.
34
+ retrieval: summarizeRetrieval(treatment),
35
+ fakeAgent: summarizeFakeAgent(treatment),
36
+ requiredFileEvidence: summarizeRequiredFileEvidence(treatment.requiredFileEvidence)
37
+ };
38
+ }
39
+ function summarizeLifecycleV2(execution) {
40
+ const session = execution.session;
41
+ if (!session)
42
+ return null;
43
+ const refreshedStatus = {};
44
+ for (const id of INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_IDS) {
45
+ const freshness = session.treatments[id].refreshedFreshness;
46
+ if (freshness)
47
+ refreshedStatus[id] = freshness.status;
48
+ }
49
+ return {
50
+ controlledFilePaths: [...session.controlledChangedPaths],
51
+ treatmentIntents: record((id) => INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_INTENTS[id]),
52
+ preMutationEquivalence: {
53
+ "changed-files-refresh": session.preMutationEquivalence["changed-files-refresh"].result,
54
+ "affected-neighborhood-refresh": session.preMutationEquivalence["affected-neighborhood-refresh"].result,
55
+ "full-refresh": session.preMutationEquivalence["full-refresh"].result
56
+ },
57
+ postMutationEquivalence: {
58
+ "changed-files-refresh": session.postMutationEquivalence["changed-files-refresh"].result,
59
+ "affected-neighborhood-refresh": session.postMutationEquivalence["affected-neighborhood-refresh"].result,
60
+ "full-refresh": session.postMutationEquivalence["full-refresh"].result
61
+ },
62
+ baselineFreshnessStatus: record((id) => session.treatments[id].changeAuthority.postMutationBaselineFreshness.status),
63
+ refreshedFreshnessStatus: refreshedStatus,
64
+ partialRealization: {
65
+ "changed-files-refresh": session.treatments["changed-files-refresh"].refreshRealization,
66
+ "affected-neighborhood-refresh": session.treatments["affected-neighborhood-refresh"].refreshRealization
67
+ },
68
+ indexInvocationCounts: record((id) => session.treatments[id].indexInvocationCount),
69
+ freshnessAssessmentCounts: record((id) => session.treatments[id].freshnessAssessmentCount),
70
+ totalIndexInvocationCount: session.totalIndexInvocationCount,
71
+ totalFreshnessAssessmentCount: session.totalFreshnessAssessmentCount,
72
+ myDevKitVersion: session.toolIdentity.version ?? null,
73
+ sourceRoots: [...session.baseCase.sourceRoots]
74
+ };
75
+ }
76
+ function summarizeScenarioV2(execution) {
77
+ return {
78
+ scenarioId: execution.scenarioId,
79
+ category: execution.category,
80
+ benchmarkProjectId: execution.benchmarkProjectId,
81
+ baseCaseId: execution.baseCaseId,
82
+ answerPolicy: execution.answerPolicy,
83
+ query: execution.query,
84
+ expectedFiles: sortedUnique(execution.expectedFiles),
85
+ expectedSymbols: sortedUnique(execution.expectedSymbols),
86
+ status: execution.status,
87
+ failureReason: execution.failureReason,
88
+ lifecycle: summarizeLifecycleV2(execution),
89
+ treatments: execution.treatments.map(summarizeTreatmentV2),
90
+ referenceComparisons: structuredClone(execution.referenceComparisons)
91
+ };
92
+ }
93
+ function partialComparison(scenario, candidate) {
94
+ const found = scenario.referenceComparisons.find((comparison) => comparison.candidateTreatmentId === candidate);
95
+ return found && found.kind === "partial-refresh" ? found : null;
96
+ }
97
+ function countPartial(scenarios, candidate, classification) {
98
+ return scenarios.filter((scenario) => partialComparison(scenario, candidate)?.classification === classification).length;
99
+ }
100
+ function countRealization(scenarios, candidate, realization) {
101
+ return scenarios.filter((scenario) => scenario.treatments.find((treatment) => treatment.treatmentId === candidate)?.refreshExecution.realization === realization).length;
102
+ }
103
+ function summarizeV2(scenarios) {
104
+ const staleClass = (scenario) => {
105
+ const stale = scenario.referenceComparisons.find((comparison) => comparison.candidateTreatmentId === "stale-index");
106
+ return stale && stale.kind === "stale-risk" ? stale.comparison.staleRiskClassification : null;
107
+ };
108
+ return {
109
+ scenarioCount: scenarios.length,
110
+ readyScenarioCount: scenarios.filter((scenario) => scenario.status === "ready").length,
111
+ failedScenarioCount: scenarios.filter((scenario) => scenario.status === "failed").length,
112
+ staleObservedRegressionCount: scenarios.filter((scenario) => staleClass(scenario) === "observed-stale-regression").length,
113
+ staleNoObservedRegressionCount: scenarios.filter((scenario) => staleClass(scenario) === "no-observed-stale-regression").length,
114
+ staleInconclusiveCount: scenarios.filter((scenario) => staleClass(scenario) === "inconclusive").length,
115
+ changedFilesAppliedPartialCount: countRealization(scenarios, "changed-files-refresh", "APPLIED_PARTIAL"),
116
+ changedFilesFallbackFullCount: countRealization(scenarios, "changed-files-refresh", "FALLBACK_FULL"),
117
+ affectedNeighborhoodAppliedPartialCount: countRealization(scenarios, "affected-neighborhood-refresh", "APPLIED_PARTIAL"),
118
+ affectedNeighborhoodFallbackFullCount: countRealization(scenarios, "affected-neighborhood-refresh", "FALLBACK_FULL"),
119
+ changedFilesObservedRegressionRelativeToFullCount: countPartial(scenarios, "changed-files-refresh", "observed-regression-relative-to-full"),
120
+ changedFilesNoObservedRegressionRelativeToFullCount: countPartial(scenarios, "changed-files-refresh", "no-observed-regression-relative-to-full"),
121
+ changedFilesInconclusiveCount: countPartial(scenarios, "changed-files-refresh", "inconclusive"),
122
+ changedFilesNotComparableCount: countPartial(scenarios, "changed-files-refresh", "not-comparable-as-partial-refresh"),
123
+ affectedNeighborhoodObservedRegressionRelativeToFullCount: countPartial(scenarios, "affected-neighborhood-refresh", "observed-regression-relative-to-full"),
124
+ affectedNeighborhoodNoObservedRegressionRelativeToFullCount: countPartial(scenarios, "affected-neighborhood-refresh", "no-observed-regression-relative-to-full"),
125
+ affectedNeighborhoodInconclusiveCount: countPartial(scenarios, "affected-neighborhood-refresh", "inconclusive"),
126
+ affectedNeighborhoodNotComparableCount: countPartial(scenarios, "affected-neighborhood-refresh", "not-comparable-as-partial-refresh")
127
+ };
128
+ }
129
+ function contradiction(scenarioId, message) {
130
+ throw new Error(`Contradictory v2 execution evidence for scenario ${scenarioId}: ${message}`);
131
+ }
132
+ function sameList(left, right) {
133
+ return left.length === right.length && left.every((value, index) => value === right[index]);
134
+ }
135
+ /**
136
+ * Rejects internally contradictory evidence; called before anything is built or written. Unavailable/null
137
+ * evidence is preserved, never coerced.
138
+ */
139
+ export function validateIncrementalChangeStalenessScenarioRecordV2(scenario) {
140
+ const id = scenario.scenarioId;
141
+ // Comparisons: exactly three, fixed order, explicit identities, reference is full-refresh, never full vs full.
142
+ if (!sameList(scenario.referenceComparisons.map((comparison) => comparison.candidateTreatmentId), COMPARISON_CANDIDATE_ORDER)) {
143
+ contradiction(id, "reference comparisons must be exactly stale-index, changed-files-refresh, affected-neighborhood-refresh in order.");
144
+ }
145
+ for (const comparison of scenario.referenceComparisons) {
146
+ if (comparison.referenceTreatmentId !== "full-refresh")
147
+ contradiction(id, `${comparison.candidateTreatmentId} comparison references ${comparison.referenceTreatmentId}, not full-refresh.`);
148
+ if ((comparison.candidateTreatmentId === "stale-index") !== (comparison.kind === "stale-risk")) {
149
+ contradiction(id, `${comparison.candidateTreatmentId} comparison uses the wrong comparison kind (${comparison.kind}).`);
150
+ }
151
+ }
152
+ // Stale comparison must use the unchanged stale-risk semantics.
153
+ const stale = scenario.referenceComparisons[0];
154
+ if (stale.kind === "stale-risk") {
155
+ const expected = classifyStaleRisk(stale.comparison.correctnessRelation, stale.comparison.requiredFileEvidenceRelation);
156
+ if (expected.staleRiskClassification !== stale.comparison.staleRiskClassification || !sameList(expected.reasonCodes, stale.comparison.reasonCodes)) {
157
+ contradiction(id, "stale-index comparison does not follow the stale-risk classification rules.");
158
+ }
159
+ }
160
+ // Treatment collection.
161
+ if (scenario.treatments.length === 0) {
162
+ if (scenario.status === "ready")
163
+ contradiction(id, "a ready scenario must carry four treatment records.");
164
+ for (const comparison of scenario.referenceComparisons) {
165
+ if (comparison.kind === "partial-refresh" && comparison.refreshRealization !== null) {
166
+ contradiction(id, `${comparison.candidateTreatmentId} carries a refresh realization although no treatment executed.`);
167
+ }
168
+ }
169
+ return;
170
+ }
171
+ if (scenario.status !== "ready")
172
+ contradiction(id, "a failed scenario must not carry treatment records.");
173
+ if (!sameList(scenario.treatments.map((treatment) => treatment.treatmentId), INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_IDS)) {
174
+ contradiction(id, "treatments must be exactly stale-index, changed-files-refresh, affected-neighborhood-refresh, full-refresh, each once, in order.");
175
+ }
176
+ for (const treatment of scenario.treatments) {
177
+ if (treatment.treatmentIntent !== INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_INTENTS[treatment.treatmentId]) {
178
+ contradiction(id, `${treatment.treatmentId} carries treatment intent ${treatment.treatmentIntent}.`);
179
+ }
180
+ const refresh = treatment.refreshExecution;
181
+ if (treatment.treatmentId === "stale-index") {
182
+ if (refresh.kind !== "no-refresh" || refresh.realization !== "NO_REFRESH" || refresh.incrementalRefresh !== null)
183
+ contradiction(id, "stale-index must be no-refresh without incrementalRefresh evidence.");
184
+ if (treatment.activeIndexPhase !== "baseline")
185
+ contradiction(id, "stale-index active index must be the baseline.");
186
+ if (treatment.refreshedFreshness !== null)
187
+ contradiction(id, "stale-index has no refreshed freshness.");
188
+ }
189
+ else {
190
+ if (treatment.activeIndexPhase !== "refreshed")
191
+ contradiction(id, `${treatment.treatmentId} active index must be the refreshed index.`);
192
+ if (treatment.refreshedFreshness === null)
193
+ contradiction(id, `${treatment.treatmentId} is missing refreshed freshness.`);
194
+ }
195
+ if (treatment.treatmentId === "full-refresh") {
196
+ if (refresh.kind !== "full" || refresh.realization !== "FULL_REFRESH" || refresh.incrementalRefresh !== null)
197
+ contradiction(id, "full-refresh must be a full refresh without incrementalRefresh evidence.");
198
+ }
199
+ if (treatment.treatmentId === "changed-files-refresh" || treatment.treatmentId === "affected-neighborhood-refresh") {
200
+ if (refresh.kind !== "incremental")
201
+ contradiction(id, `${treatment.treatmentId} must be an incremental refresh.`);
202
+ if (refresh.kind === "incremental") {
203
+ const upstream = refresh.incrementalRefresh;
204
+ if (upstream.requestedScope !== REQUESTED_SCOPE[treatment.treatmentId])
205
+ contradiction(id, `${treatment.treatmentId} requested scope ${upstream.requestedScope} does not match the treatment.`);
206
+ if (refresh.realization === "APPLIED_PARTIAL" && (upstream.selectionStatus !== "applied" || upstream.appliedScope !== upstream.requestedScope)) {
207
+ contradiction(id, `${treatment.treatmentId} is APPLIED_PARTIAL but upstream reported status=${upstream.selectionStatus} applied=${upstream.appliedScope}.`);
208
+ }
209
+ if (refresh.realization === "FALLBACK_FULL" && (upstream.selectionStatus !== "fallback-full" || upstream.appliedScope !== "full")) {
210
+ contradiction(id, `${treatment.treatmentId} is FALLBACK_FULL but upstream reported status=${upstream.selectionStatus} applied=${upstream.appliedScope}.`);
211
+ }
212
+ }
213
+ }
214
+ }
215
+ // Partial comparisons must agree with their treatment's realization and the frozen precedence.
216
+ for (const candidate of PARTIAL_IDS) {
217
+ const comparison = partialComparison(scenario, candidate);
218
+ const treatment = scenario.treatments.find((entry) => entry.treatmentId === candidate);
219
+ if (!comparison || !treatment || treatment.refreshExecution.kind !== "incremental")
220
+ contradiction(id, `${candidate} comparison or treatment is missing.`);
221
+ else {
222
+ if (comparison.refreshRealization !== treatment.refreshExecution.realization) {
223
+ contradiction(id, `${candidate} comparison realization ${comparison.refreshRealization} differs from the treatment realization ${treatment.refreshExecution.realization}.`);
224
+ }
225
+ const expected = classifyPartialRefreshAgainstFull({
226
+ realization: treatment.refreshExecution.realization,
227
+ correctnessRelation: comparison.correctnessRelation,
228
+ requiredFileEvidenceRelation: comparison.requiredFileEvidenceRelation
229
+ });
230
+ if (expected.classification !== comparison.classification || !sameList(expected.reasonCodes, comparison.reasonCodes)) {
231
+ contradiction(id, `${candidate} comparison does not follow the partial-refresh reference rules.`);
232
+ }
233
+ }
234
+ }
235
+ if (scenario.lifecycle) {
236
+ for (const count of [...Object.values(scenario.lifecycle.indexInvocationCounts), ...Object.values(scenario.lifecycle.freshnessAssessmentCounts)]) {
237
+ if (!Number.isInteger(count) || count < 0)
238
+ contradiction(id, "lifecycle counts must be non-negative integers.");
239
+ }
240
+ }
241
+ else {
242
+ contradiction(id, "a ready scenario must carry a lifecycle summary.");
243
+ }
244
+ }
245
+ export function validateIncrementalChangeStalenessExecutionArtifactV2(artifact) {
246
+ if (artifact.schemaVersion !== INCREMENTAL_CHANGE_STALENESS_EXECUTION_SCHEMA_VERSION_V2) {
247
+ throw new Error(`Unexpected execution artifact schema ${String(artifact.schemaVersion)}.`);
248
+ }
249
+ for (const scenario of artifact.scenarios)
250
+ validateIncrementalChangeStalenessScenarioRecordV2(scenario);
251
+ }
252
+ /**
253
+ * Builds and validates the v2 artifact from already-executed scenario evidence, in the caller's canonical
254
+ * scenario order. Throws on internal contradiction instead of serializing it.
255
+ */
256
+ export function buildIncrementalChangeStalenessExecutionArtifactV2(args) {
257
+ const scenarios = args.executions.map(summarizeScenarioV2);
258
+ const artifact = {
259
+ schemaVersion: INCREMENTAL_CHANGE_STALENESS_EXECUTION_SCHEMA_VERSION_V2,
260
+ runId: args.runId,
261
+ pluginId: args.pluginId,
262
+ scenarios,
263
+ summary: summarizeV2(scenarios)
264
+ };
265
+ validateIncrementalChangeStalenessExecutionArtifactV2(artifact);
266
+ return artifact;
267
+ }
268
+ /** Writes to the canonical filename shared with v1; validates first so contradictory evidence is never serialized. */
269
+ export async function writeIncrementalChangeStalenessExecutionArtifactV2(outDir, artifact) {
270
+ validateIncrementalChangeStalenessExecutionArtifactV2(artifact);
271
+ await mkdir(outDir, { recursive: true });
272
+ const artifactPath = resolveWithinRoot(outDir, INCREMENTAL_CHANGE_STALENESS_EXECUTION_ARTIFACT_FILE);
273
+ await writeFile(artifactPath, `${JSON.stringify(artifact, null, 2)}\n`, "utf8");
274
+ return artifactPath;
275
+ }
276
+ /** Discriminates V1 from V2 by schemaVersion only; historical V1 evidence is never converted to V2. */
277
+ export function parseIncrementalChangeStalenessExecutionArtifact(json) {
278
+ const schemaVersion = json && typeof json === "object" ? json.schemaVersion : undefined;
279
+ if (schemaVersion === INCREMENTAL_CHANGE_STALENESS_EXECUTION_SCHEMA_VERSION) {
280
+ return { version: "v1", artifact: json };
281
+ }
282
+ if (schemaVersion === INCREMENTAL_CHANGE_STALENESS_EXECUTION_SCHEMA_VERSION_V2) {
283
+ return { version: "v2", artifact: json };
284
+ }
285
+ throw new Error(`Unrecognized incremental-change-staleness execution artifact schema: ${String(schemaVersion)}.`);
286
+ }
287
+ export async function readIncrementalChangeStalenessExecutionArtifact(artifactPath) {
288
+ return parseIncrementalChangeStalenessExecutionArtifact(JSON.parse(await readFile(artifactPath, "utf8")));
289
+ }
@@ -0,0 +1,195 @@
1
+ import path from "node:path";
2
+ import { resolveWithinRoot } from "../../../core/pathSafety.js";
3
+ import { classifyStaleRisk, compareCorrectness, compareRequiredFileEvidence } from "./comparison.js";
4
+ import { compareCandidateCorrectness, compareCandidateRequiredFileEvidence, classifyPartialRefreshAgainstFull } from "./comparisonV2.js";
5
+ import { INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_IDS, INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_INTENTS } from "./disposableTarget.js";
6
+ import { assessIncrementalChangeStalenessAffectedNeighborhood, checkIncrementalChangeStalenessAffectedNeighborhoodSymmetry, correctnessComparable, defaultIncrementalChangeStalenessExecutionDeps, executeTreatmentEvaluation, INCONCLUSIVE_NO_EXECUTION, resolveIncrementalChangeStalenessScenarioTask } from "./execution.js";
7
+ /**
8
+ * Four-way check with stale-index only as the deterministic reference. Reuses the v0.6.2 pairwise check
9
+ * (status, relationship, reindexRecommendation, and the six numeric metrics) and additionally requires
10
+ * equal seedNodeCount. Conflicting evidence is never averaged or merged.
11
+ */
12
+ export function checkAffectedNeighborhoodFourWaySymmetry(assessments) {
13
+ const reference = assessments["stale-index"];
14
+ const reasons = [];
15
+ for (const treatmentId of INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_IDS) {
16
+ if (treatmentId === "stale-index")
17
+ continue;
18
+ const other = assessments[treatmentId];
19
+ const pairwise = checkIncrementalChangeStalenessAffectedNeighborhoodSymmetry(reference, other);
20
+ if (!pairwise.symmetric)
21
+ reasons.push(`${treatmentId}: ${pairwise.reason}`);
22
+ if (reference.seedNodeCount !== other.seedNodeCount) {
23
+ reasons.push(`${treatmentId}: seedNodeCount (${reference.seedNodeCount} vs ${other.seedNodeCount})`);
24
+ }
25
+ }
26
+ return reasons.length === 0 ? { symmetric: true } : { symmetric: false, reason: `Affected-neighborhood assessments are asymmetric across treatments: ${reasons.join(" | ")}` };
27
+ }
28
+ const PARTIAL_TREATMENT_IDS = ["changed-files-refresh", "affected-neighborhood-refresh"];
29
+ function placeholderComparisons() {
30
+ return [
31
+ { kind: "stale-risk", candidateTreatmentId: "stale-index", referenceTreatmentId: "full-refresh", comparison: { ...INCONCLUSIVE_NO_EXECUTION, reasonCodes: [...INCONCLUSIVE_NO_EXECUTION.reasonCodes] } },
32
+ ...PARTIAL_TREATMENT_IDS.map((candidateTreatmentId) => ({
33
+ kind: "partial-refresh",
34
+ candidateTreatmentId,
35
+ referenceTreatmentId: "full-refresh",
36
+ refreshRealization: null,
37
+ correctnessRelation: "unknown",
38
+ requiredFileEvidenceRelation: "unknown",
39
+ classification: "inconclusive",
40
+ reasonCodes: ["correctness-unavailable", "required-file-evidence-unavailable"]
41
+ }))
42
+ ];
43
+ }
44
+ function refreshExecutionOf(session, treatmentId) {
45
+ const treatment = session.treatments[treatmentId];
46
+ if (treatment.refreshRealization !== null) {
47
+ const incrementalRefresh = treatment.refreshedIndex.incrementalRefresh;
48
+ if (!incrementalRefresh) {
49
+ throw new Error(`${treatmentId} lifecycle reported ${treatment.refreshRealization} without upstream incrementalRefresh evidence.`);
50
+ }
51
+ return { kind: "incremental", realization: treatment.refreshRealization, incrementalRefresh: structuredClone(incrementalRefresh) };
52
+ }
53
+ return treatment.postMutationIndexBuilt
54
+ ? { kind: "full", realization: "FULL_REFRESH", incrementalRefresh: null }
55
+ : { kind: "no-refresh", realization: "NO_REFRESH", incrementalRefresh: null };
56
+ }
57
+ /**
58
+ * Executes the four matched treatments in fixed order for one lifecycle-ready V2 session, then builds
59
+ * exactly three comparisons against full-refresh. A lifecycle that is not ready, or an affected-neighborhood
60
+ * asymmetry, is a failed scenario with no fabricated treatment evidence.
61
+ */
62
+ export async function executeIncrementalChangeStalenessScenarioV2(options) {
63
+ const deps = { ...defaultIncrementalChangeStalenessExecutionDeps, ...options.deps };
64
+ const scenario = options.scenario;
65
+ if (options.lifecycle.status !== "ready") {
66
+ return {
67
+ scenarioId: scenario.id,
68
+ category: scenario.category,
69
+ benchmarkProjectId: options.baseCase.benchmarkProjectId,
70
+ baseCaseId: options.baseCase.caseId,
71
+ answerPolicy: scenario.answerPolicy,
72
+ query: null,
73
+ expectedFiles: [],
74
+ expectedSymbols: [],
75
+ status: "failed",
76
+ failureReason: `${options.lifecycle.failure.code}: ${options.lifecycle.failure.message}`,
77
+ session: null,
78
+ treatments: [],
79
+ referenceComparisons: placeholderComparisons()
80
+ };
81
+ }
82
+ const session = options.lifecycle.session;
83
+ const { resolved, derivedTask } = await resolveIncrementalChangeStalenessScenarioTask({ repoRoot: options.repoRoot, scenario });
84
+ const scenarioBase = {
85
+ scenarioId: scenario.id,
86
+ category: scenario.category,
87
+ benchmarkProjectId: scenario.benchmarkProjectId,
88
+ baseCaseId: scenario.baseCaseId,
89
+ answerPolicy: scenario.answerPolicy,
90
+ query: resolved.query,
91
+ expectedFiles: resolved.expectedFiles,
92
+ expectedSymbols: resolved.expectedSymbols,
93
+ session
94
+ };
95
+ // Affected-neighborhood evidence is derived from each treatment's ORIGINAL BASELINE change authority.
96
+ const affected = {};
97
+ for (const treatmentId of INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_IDS) {
98
+ affected[treatmentId] = assessIncrementalChangeStalenessAffectedNeighborhood(session.treatments[treatmentId], derivedTask);
99
+ }
100
+ const symmetry = checkAffectedNeighborhoodFourWaySymmetry(affected);
101
+ if (!symmetry.symmetric) {
102
+ return {
103
+ ...scenarioBase,
104
+ status: "failed",
105
+ failureReason: `AFFECTED_NEIGHBORHOOD_ASYMMETRY: ${symmetry.reason}`,
106
+ treatments: [],
107
+ referenceComparisons: placeholderComparisons()
108
+ };
109
+ }
110
+ const retrievalCommandsDir = (treatmentId) => resolveWithinRoot(options.runOwnedRoot, path.join("commands", scenario.id, treatmentId, "retrieval"));
111
+ const agentOutDir = (treatmentId) => resolveWithinRoot(options.runOwnedRoot, path.join("agents", scenario.id, treatmentId));
112
+ // Fixed deterministic order, sequential; each treatment retrieves only from its own active index.
113
+ const treatments = [];
114
+ for (const treatmentId of INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_IDS) {
115
+ const lifecycleTreatment = session.treatments[treatmentId];
116
+ const evaluation = await executeTreatmentEvaluation({
117
+ treatmentId,
118
+ baselineFreshness: lifecycleTreatment.changeAuthority.postMutationBaselineFreshness,
119
+ activeIndexPhase: lifecycleTreatment.activeRetrieval.role,
120
+ activeIndexDir: lifecycleTreatment.activeRetrieval.index.indexDir,
121
+ derivedTask,
122
+ affectedNeighborhood: affected[treatmentId],
123
+ requiredFiles: resolved.expectedFiles,
124
+ kitCommand: session.kitCommand,
125
+ retrievalCommandsDir: retrievalCommandsDir(treatmentId),
126
+ agentOutDir: agentOutDir(treatmentId),
127
+ projectProfiles: options.projectProfiles,
128
+ cwd: options.cwd,
129
+ env: options.env,
130
+ deps
131
+ });
132
+ treatments.push({
133
+ ...evaluation,
134
+ treatmentIntent: INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_INTENTS[treatmentId],
135
+ refreshedFreshness: lifecycleTreatment.refreshedFreshness,
136
+ refreshExecution: refreshExecutionOf(session, treatmentId)
137
+ });
138
+ }
139
+ return {
140
+ ...scenarioBase,
141
+ status: "ready",
142
+ failureReason: null,
143
+ treatments,
144
+ referenceComparisons: buildReferenceComparisons(treatments)
145
+ };
146
+ }
147
+ /** Exactly three comparisons against full-refresh: stale-index, changed-files-refresh, affected-neighborhood-refresh. */
148
+ export function buildReferenceComparisons(treatments) {
149
+ const byId = new Map(treatments.map((treatment) => [treatment.treatmentId, treatment]));
150
+ const reference = byId.get("full-refresh");
151
+ const stale = byId.get("stale-index");
152
+ if (!reference || !stale)
153
+ throw new Error("Reference comparisons require the stale-index and full-refresh treatment executions.");
154
+ // Historical v0.6.2 semantics, unchanged.
155
+ const staleCorrectness = compareCorrectness(correctnessComparable(stale.fakeAgent), correctnessComparable(reference.fakeAgent));
156
+ const staleRequired = compareRequiredFileEvidence(stale.requiredFileEvidence, reference.requiredFileEvidence);
157
+ const staleRisk = classifyStaleRisk(staleCorrectness, staleRequired);
158
+ const comparisons = [
159
+ {
160
+ kind: "stale-risk",
161
+ candidateTreatmentId: "stale-index",
162
+ referenceTreatmentId: "full-refresh",
163
+ comparison: {
164
+ correctnessRelation: staleCorrectness,
165
+ requiredFileEvidenceRelation: staleRequired,
166
+ staleRiskClassification: staleRisk.staleRiskClassification,
167
+ reasonCodes: staleRisk.reasonCodes
168
+ }
169
+ }
170
+ ];
171
+ for (const candidateTreatmentId of PARTIAL_TREATMENT_IDS) {
172
+ const candidate = byId.get(candidateTreatmentId);
173
+ if (!candidate || candidate.refreshExecution.kind !== "incremental") {
174
+ throw new Error(`${candidateTreatmentId} has no incremental refresh execution to compare.`);
175
+ }
176
+ const correctnessRelation = compareCandidateCorrectness(correctnessComparable(candidate.fakeAgent), correctnessComparable(reference.fakeAgent));
177
+ const requiredFileEvidenceRelation = compareCandidateRequiredFileEvidence(candidate.requiredFileEvidence, reference.requiredFileEvidence);
178
+ const classified = classifyPartialRefreshAgainstFull({
179
+ realization: candidate.refreshExecution.realization,
180
+ correctnessRelation,
181
+ requiredFileEvidenceRelation
182
+ });
183
+ comparisons.push({
184
+ kind: "partial-refresh",
185
+ candidateTreatmentId,
186
+ referenceTreatmentId: "full-refresh",
187
+ refreshRealization: candidate.refreshExecution.realization,
188
+ correctnessRelation,
189
+ requiredFileEvidenceRelation,
190
+ classification: classified.classification,
191
+ reasonCodes: classified.reasonCodes
192
+ });
193
+ }
194
+ return comparisons;
195
+ }
@@ -0,0 +1,13 @@
1
+ export * from "./scenarioTypes.js";
2
+ export * from "./scenarioMutation.js";
3
+ export * from "./scenarioValidation.js";
4
+ export * from "./scenarioCatalog.js";
5
+ export * from "./disposableTarget.js";
6
+ export * from "./sourceState.js";
7
+ export * from "./mutationExecution.js";
8
+ export * from "./treatmentSession.js";
9
+ export * from "./lifecyclePolicy.js";
10
+ export * from "./scenarioSelection.js";
11
+ export * from "./lifecycle.js";
12
+ export * from "./config.js";
13
+ export * from "./plugin.js";