@dailephd/my-dev-kit-lab 0.6.0 → 0.6.2

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 (44) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +7 -4
  3. package/benchmarks/contracts/incremental-change-staleness-scenarios.json +360 -0
  4. package/dist/src/commands/runExperimentRunCommand.js +4 -3
  5. package/dist/src/evaluation/affectedNeighborhood.js +712 -0
  6. package/dist/src/evaluation/affectedNeighborhoodMetrics.js +51 -0
  7. package/dist/src/evaluation/indexSnapshot.js +1 -1
  8. package/dist/src/experiments/defaultRegistry.js +2 -0
  9. package/dist/src/experiments/plugins/incrementalChangeStaleness/comparison.js +101 -0
  10. package/dist/src/experiments/plugins/incrementalChangeStaleness/config.js +66 -0
  11. package/dist/src/experiments/plugins/incrementalChangeStaleness/disposableTarget.js +107 -0
  12. package/dist/src/experiments/plugins/incrementalChangeStaleness/execution.js +404 -0
  13. package/dist/src/experiments/plugins/incrementalChangeStaleness/executionArtifact.js +160 -0
  14. package/dist/src/experiments/plugins/incrementalChangeStaleness/index.js +13 -0
  15. package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecycle.js +276 -0
  16. package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecyclePolicy.js +152 -0
  17. package/dist/src/experiments/plugins/incrementalChangeStaleness/mutationExecution.js +144 -0
  18. package/dist/src/experiments/plugins/incrementalChangeStaleness/plugin.js +274 -0
  19. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioCatalog.js +42 -0
  20. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioMutation.js +116 -0
  21. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioSelection.js +79 -0
  22. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioTypes.js +37 -0
  23. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioValidation.js +168 -0
  24. package/dist/src/experiments/plugins/incrementalChangeStaleness/sourceState.js +145 -0
  25. package/dist/src/experiments/plugins/incrementalChangeStaleness/treatmentSession.js +6 -0
  26. package/dist/src/experiments/plugins/index.js +1 -0
  27. package/dist/src/experiments/plugins/warmIndexReuse/execution.js +21 -0
  28. package/dist/src/experiments/plugins/warmIndexReuse/executionArtifact.js +1 -0
  29. package/dist/src/experiments/plugins/warmIndexReuse/metrics.js +68 -0
  30. package/dist/src/experiments/plugins/warmIndexReuse/warmIndexSession.js +9 -1
  31. package/dist/src/report/experiments/buildIncrementalChangeStalenessReport.js +112 -0
  32. package/dist/src/report/experiments/buildPluginExperimentReport.js +17 -2
  33. package/dist/src/report/experiments/buildWarmIndexReuseReport.js +80 -3
  34. package/dist/src/report/experiments/incrementalChangeStalenessReportModel.js +4 -0
  35. package/dist/src/report/experiments/index.js +3 -0
  36. package/dist/src/report/experiments/renderIncrementalChangeStalenessHtml.js +239 -0
  37. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +3 -0
  38. package/dist/src/report/experiments/renderPluginExperimentReportText.js +222 -0
  39. package/dist/src/report/experiments/renderWarmIndexReuseHtml.js +98 -0
  40. package/dist/src/report/experiments/warmIndexReuseReportModel.js +5 -0
  41. package/docs/METRICS.md +79 -3
  42. package/examples/tutorial-browser/prepare.mjs +13 -13
  43. package/examples/tutorial-browser/server.mjs +13 -13
  44. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { buildContextStrategyComparisonV043Report } from "./buildContextStrategyComparisonV043Report.js";
3
3
  import { buildWarmIndexReuseReport } from "./buildWarmIndexReuseReport.js";
4
+ import { buildIncrementalChangeStalenessReport } from "./buildIncrementalChangeStalenessReport.js";
4
5
  const V043_BULK_ARRAY_KEYS = [
5
6
  "v043StageContextExecutions",
6
7
  "v043StageContextEvaluations",
@@ -11,6 +12,7 @@ export function buildPluginExperimentReport(args) {
11
12
  const allOutcomes = args.run.cases.flatMap((experimentCase) => experimentCase.outcomes);
12
13
  const contextStrategyComparisonV043 = buildContextStrategyComparisonV043Report(args.run);
13
14
  const warmIndexReuse = buildWarmIndexReuseReport(args.run);
15
+ const incrementalChangeStaleness = buildIncrementalChangeStalenessReport(args.run);
14
16
  const rawRun = { ...args.run, artifacts: relativizeArtifacts(args.run.artifacts, outputRoot) };
15
17
  for (const key of V043_BULK_ARRAY_KEYS) {
16
18
  delete rawRun[key];
@@ -39,6 +41,7 @@ export function buildPluginExperimentReport(args) {
39
41
  skippedOutcomes: allOutcomes.filter((outcome) => outcome.status === "skipped"),
40
42
  findings: buildFindings(args.run),
41
43
  warmIndexReuse,
44
+ incrementalChangeStaleness,
42
45
  contextStrategyComparisonV043,
43
46
  interpretation: buildInterpretation(args.run, contextStrategyComparisonV043, warmIndexReuse),
44
47
  rawRun,
@@ -107,6 +110,18 @@ function freshnessSentence(report) {
107
110
  `fresh=${summary.freshTaskCount}, stale=${summary.staleTaskCount}, partially-stale=${summary.partiallyStaleTaskCount}, unknown=${summary.unknownTaskCount}. ` +
108
111
  "Freshness is observational evidence over files represented by the index snapshot and does not change retrieval or provider status.");
109
112
  }
113
+ /** Neutral, count-only sentence derived from the report's persisted affected-neighborhood summary. */
114
+ function affectedNeighborhoodSentence(report) {
115
+ const summary = report.affectedNeighborhoodSummary;
116
+ if (!summary || summary.assessedTaskCount === 0) {
117
+ return "No per-task affected-neighborhood assessment was available in this report.";
118
+ }
119
+ return (`Affected-neighborhood evidence was available for ${summary.assessedTaskCount} task boundar${summary.assessedTaskCount === 1 ? "y" : "ies"}: ` +
120
+ `complete=${summary.completeAssessmentCount}, partial=${summary.partialAssessmentCount}, unavailable=${summary.unavailableAssessmentCount}; ` +
121
+ `relationships were related=${summary.relatedTaskCount}, unrelated=${summary.unrelatedTaskCount}, unknown=${summary.unknownRelationshipTaskCount}; ` +
122
+ `reindex recommendations were recommended=${summary.recommendedReindexCount}, not-indicated=${summary.notIndicatedReindexCount}, unknown=${summary.unknownReindexRecommendationCount}. ` +
123
+ "Affected-neighborhood evidence uses a one-hop baseline graph around confirmed changed indexed files/symbols. It is observational and does not alter retrieval or execution status.");
124
+ }
110
125
  function buildInterpretation(run, contextStrategyComparisonV043, warmIndexReuse) {
111
126
  if (run.pluginId === "warm-index-reuse" && warmIndexReuse) {
112
127
  const summary = warmIndexReuse.summary;
@@ -118,7 +133,7 @@ function buildInterpretation(run, contextStrategyComparisonV043, warmIndexReuse)
118
133
  `Deterministic fake-agent correctness/token evidence is available for ${summary.agentCorrectnessAvailableCount} of ${summary.agentSideCount} task sides (correctness) and ${summary.agentTotalTokensAvailableCount} (total tokens); ` +
119
134
  "fake-agent totals are simulated harness evidence, not real-model or provider measurements. " +
120
135
  "Estimated context-token values are context-size estimates, not provider token usage. " +
121
- freshnessSentence(warmIndexReuse),
136
+ freshnessSentence(warmIndexReuse) + " " + affectedNeighborhoodSentence(warmIndexReuse),
122
137
  recommendedNextStep: run.status === "completed"
123
138
  ? "Review per-task measurements and cumulative component costs for each project; the variants are reported side by side and are not ranked."
124
139
  : "Inspect unavailable metrics, warnings, and failures before drawing conclusions from this run.",
@@ -144,7 +159,7 @@ function buildInterpretation(run, contextStrategyComparisonV043, warmIndexReuse)
144
159
  `${nonCompletedCounts ? `; outcome counts: ${nonCompletedCounts}` : ""}). ` +
145
160
  `Token evidence status: ${campaign.tokenEvidenceStatus} (${summary.agentTotalTokensAvailableCount} of ${summary.agentSideCount} task sides have a total-token value). ` +
146
161
  "Context estimated tokens remain a separate character-based context-size estimate and are never substituted for provider telemetry. " +
147
- freshnessSentence(warmIndexReuse),
162
+ freshnessSentence(warmIndexReuse) + " " + affectedNeighborhoodSentence(warmIndexReuse),
148
163
  recommendedNextStep: isInfrastructureComplete && isAgentEvidenceComplete
149
164
  ? "Review per-task correctness, token provenance, and cumulative component measurements."
150
165
  : "Review infrastructure gaps (index build, raw/warm retrieval) separately from provider/agent limitations before drawing conclusions from this run.",
@@ -1,4 +1,4 @@
1
- import { MAX_REPORT_FRESHNESS_CHANGES, MAX_REPORT_FRESHNESS_UNRESOLVED, WARM_INDEX_REUSE_REPORT_SCHEMA_VERSION, } from "./warmIndexReuseReportModel.js";
1
+ import { MAX_REPORT_AFFECTED_EDGES, MAX_REPORT_AFFECTED_NODES, MAX_REPORT_AFFECTED_TASK_AMBIGUOUS, MAX_REPORT_AFFECTED_TASK_UNRESOLVED, MAX_REPORT_AFFECTED_WARNINGS, MAX_REPORT_FRESHNESS_CHANGES, MAX_REPORT_FRESHNESS_UNRESOLVED, WARM_INDEX_REUSE_REPORT_SCHEMA_VERSION, } from "./warmIndexReuseReportModel.js";
2
2
  const WARM_INDEX_REUSE_PLUGIN_ID = "warm-index-reuse";
3
3
  const COMMON_LIMITATIONS = [
4
4
  "Context estimated-token counts use the character-based estimate and are not provider billing telemetry.",
@@ -30,6 +30,17 @@ const FRESHNESS_LIMITATIONS = [
30
30
  "Freshness is observational evidence. It does not trigger reindexing, suppress warm retrieval, alter execution status, or alter provider/agent outcome status.",
31
31
  "Freshness statuses: fresh means all represented indexed files were completely compared and their content identities still match; stale means a complete comparison confirmed at least one represented indexed file is modified or missing; partially-stale means at least one represented indexed file is confirmed changed but comparison evidence is incomplete; unknown means no confirmed change established staleness but evidence is insufficient to prove freshness. A task without an assessment is reported as not assessed, which is not the same as unknown.",
32
32
  ];
33
+ const AFFECTED_NEIGHBORHOOD_LIMITATIONS = [
34
+ "Affected-neighborhood evidence uses a one-hop baseline graph around confirmed changed indexed files/symbols. It is observational and does not alter retrieval or execution status.",
35
+ "A changed baseline symbol means the symbol was present in a confirmed changed indexed file; it is not proof that the symbol's own source text changed.",
36
+ "Reindex recommendation categories describe this bounded analysis only. They do not claim that reindexing is mandatory, that it guarantees correctness, that skipping it is universally safe, or that the whole repository is fresh.",
37
+ ];
38
+ /** Fixed neutral wording per recommendation category. */
39
+ export const REINDEX_RECOMMENDATION_EXPLANATIONS = {
40
+ recommended: "Reindex recommended: the task has confirmed overlap with the affected one-hop baseline graph neighborhood.",
41
+ "not-indicated": "Reindex not indicated by this bounded analysis: complete evidence found no task-node overlap with the affected neighborhood.",
42
+ unknown: "Reindex recommendation unknown: evidence is incomplete or unavailable, so absence of observed overlap is not sufficient to conclude that the task is unaffected.",
43
+ };
33
44
  const COST_MODEL = [
34
45
  "Index construction is a one-time cost per benchmark project; task retrieval is the repeated warm cost.",
35
46
  "Cold start: for the first task in a project, the cumulative warm component duration is the one-time index build plus the first retrieval.",
@@ -98,6 +109,7 @@ export function buildWarmIndexReuseReport(run) {
98
109
  rawAgent: toReportAgent(rawEvidence),
99
110
  warmAgent: toReportAgent(warmEvidence),
100
111
  indexFreshness: toReportFreshness(execution.tasks[taskIndex].indexFreshness),
112
+ affectedNeighborhood: toReportAffectedNeighborhood(execution.tasks[taskIndex].affectedNeighborhood, task.warm),
101
113
  };
102
114
  }),
103
115
  };
@@ -130,6 +142,7 @@ export function buildWarmIndexReuseReport(run) {
130
142
  agent: agentIdentity,
131
143
  agentCampaign,
132
144
  indexFreshnessSummary: summarizeFreshness(projects.flatMap((project) => project.tasks)),
145
+ affectedNeighborhoodSummary: summarizeAffectedNeighborhood(projects.flatMap((project) => project.tasks)),
133
146
  };
134
147
  }
135
148
  function boundedList(source, limit, map) {
@@ -162,6 +175,70 @@ function toReportFreshness(freshness) {
162
175
  warnings: [...freshness.warnings],
163
176
  };
164
177
  }
178
+ /** Bounded list whose totals come from persisted counts, which can exceed the persisted item list. */
179
+ function boundedListWithTotal(source, totalCount, limit, map) {
180
+ const items = source.slice(0, limit).map(map);
181
+ const total = Math.max(totalCount, source.length);
182
+ return { totalCount: total, displayedCount: items.length, omittedCount: total - items.length, items };
183
+ }
184
+ /**
185
+ * Presents a persisted affected-neighborhood assessment only: it never traverses, remaps seeds,
186
+ * recomputes overlap, or reads the graph, index, or filesystem. Numeric fields are the warm-side
187
+ * metric objects already calculated by the metric owner. Absent evidence stays null.
188
+ */
189
+ function toReportAffectedNeighborhood(assessment, warm) {
190
+ if (!assessment)
191
+ return null;
192
+ const mapping = assessment.taskMapping;
193
+ return {
194
+ status: assessment.status,
195
+ freshnessStatus: assessment.freshnessStatus,
196
+ seedMappingStatus: assessment.seedMappingStatus,
197
+ graphEvidenceStatus: assessment.graphEvidenceStatus,
198
+ neighborhoodStatus: assessment.neighborhoodStatus,
199
+ metrics: {
200
+ changedFileCount: warm.changedFileCount,
201
+ changedSymbolCount: warm.changedSymbolCount,
202
+ affectedNodeCount: warm.affectedNodeCount,
203
+ affectedEdgeCount: warm.affectedEdgeCount,
204
+ taskOverlapCount: warm.taskOverlapCount,
205
+ taskOverlapPercent: warm.taskOverlapPercent,
206
+ },
207
+ taskMappingStatus: mapping.status,
208
+ resolvableTaskNodeCount: mapping.resolvableTaskNodeCount,
209
+ relationship: assessment.relationship,
210
+ reindexRecommendation: assessment.reindexRecommendation,
211
+ recommendationExplanation: REINDEX_RECOMMENDATION_EXPLANATIONS[assessment.reindexRecommendation],
212
+ affectedNodeIds: boundedList(assessment.affectedNodeIds, MAX_REPORT_AFFECTED_NODES, (id) => id),
213
+ participatingEdgeIds: boundedList(assessment.participatingEdgeIds, MAX_REPORT_AFFECTED_EDGES, (id) => id),
214
+ unresolvedTaskMappings: boundedListWithTotal(mapping.unresolved, mapping.unresolvedCount, MAX_REPORT_AFFECTED_TASK_UNRESOLVED, (entry) => ({ ...entry })),
215
+ ambiguousTaskSymbols: boundedListWithTotal(mapping.ambiguousSymbols, mapping.ambiguousCount, MAX_REPORT_AFFECTED_TASK_AMBIGUOUS, (entry) => ({
216
+ name: entry.name,
217
+ candidateNodeIds: [...entry.candidateNodeIds],
218
+ })),
219
+ warnings: boundedListWithTotal(assessment.warnings, assessment.warningCount, MAX_REPORT_AFFECTED_WARNINGS, (warning) => ({
220
+ code: warning.code,
221
+ message: warning.message,
222
+ })),
223
+ };
224
+ }
225
+ function summarizeAffectedNeighborhood(tasks) {
226
+ const assessed = tasks.flatMap((task) => (task.affectedNeighborhood ? [task.affectedNeighborhood] : []));
227
+ const count = (key, value) => assessed.filter((entry) => entry[key] === value).length;
228
+ return {
229
+ assessedTaskCount: assessed.length,
230
+ unassessedTaskCount: tasks.length - assessed.length,
231
+ completeAssessmentCount: count("status", "complete"),
232
+ partialAssessmentCount: count("status", "partial"),
233
+ unavailableAssessmentCount: count("status", "unavailable"),
234
+ relatedTaskCount: count("relationship", "related"),
235
+ unrelatedTaskCount: count("relationship", "unrelated"),
236
+ unknownRelationshipTaskCount: count("relationship", "unknown"),
237
+ recommendedReindexCount: count("reindexRecommendation", "recommended"),
238
+ notIndicatedReindexCount: count("reindexRecommendation", "not-indicated"),
239
+ unknownReindexRecommendationCount: count("reindexRecommendation", "unknown"),
240
+ };
241
+ }
165
242
  function summarizeFreshness(tasks) {
166
243
  const assessed = tasks.flatMap((task) => (task.indexFreshness ? [task.indexFreshness] : []));
167
244
  const count = (status) => assessed.filter((freshness) => freshness.status === status).length;
@@ -242,10 +319,10 @@ function buildCampaignSummary(args) {
242
319
  }
243
320
  function buildLimitations(agent) {
244
321
  if (agent.mode === "deterministic-fake") {
245
- return [...COMMON_LIMITATIONS, ...FAKE_AGENT_LIMITATIONS, ...FRESHNESS_LIMITATIONS];
322
+ return [...COMMON_LIMITATIONS, ...FAKE_AGENT_LIMITATIONS, ...FRESHNESS_LIMITATIONS, ...AFFECTED_NEIGHBORHOOD_LIMITATIONS];
246
323
  }
247
324
  const providerLimitations = agent.id === "codex" ? CODEX_CAMPAIGN_LIMITATIONS : CLAUDE_CAMPAIGN_LIMITATIONS;
248
- return [...COMMON_LIMITATIONS, ...providerLimitations, ...FRESHNESS_LIMITATIONS];
325
+ return [...COMMON_LIMITATIONS, ...providerLimitations, ...FRESHNESS_LIMITATIONS, ...AFFECTED_NEIGHBORHOOD_LIMITATIONS];
249
326
  }
250
327
  function toReportAgent(agent) {
251
328
  if (!agent)
@@ -0,0 +1,4 @@
1
+ // v0.6.2 Batch 5 -- plugin-specific presentation model types only. No projection logic, no HTML,
2
+ // no text rendering, and no recomputation live here; see buildIncrementalChangeStalenessReport.ts
3
+ // and renderIncrementalChangeStalenessHtml.ts for those responsibilities.
4
+ export const INCREMENTAL_CHANGE_STALENESS_REPORT_SCHEMA_VERSION = "my-dev-kit-lab-incremental-change-staleness-report-v1";
@@ -14,3 +14,6 @@ export { buildContextIntegrityReport } from "./buildContextIntegrityReport.js";
14
14
  export { renderContextIntegrityJsonReport } from "./renderContextIntegrityJsonReport.js";
15
15
  export { renderContextIntegrityText } from "./renderContextIntegrityText.js";
16
16
  export { renderContextIntegrityHtml } from "./renderContextIntegrityHtml.js";
17
+ export * from "./incrementalChangeStalenessReportModel.js";
18
+ export { buildIncrementalChangeStalenessReport, STALE_RISK_CLASSIFICATION_EXPLANATIONS } from "./buildIncrementalChangeStalenessReport.js";
19
+ export { renderIncrementalChangeStalenessHtml } from "./renderIncrementalChangeStalenessHtml.js";
@@ -0,0 +1,239 @@
1
+ // v0.6.2 Batch 5 -- plugin-specific HTML section rendering only. Renders exactly the persisted
2
+ // report-model fields; performs no classification, no metric arithmetic, and no percent
3
+ // calculation beyond formatting an already-persisted number.
4
+ import { STALE_RISK_CLASSIFICATION_EXPLANATIONS } from "./buildIncrementalChangeStalenessReport.js";
5
+ import { REINDEX_RECOMMENDATION_EXPLANATIONS } from "./buildWarmIndexReuseReport.js";
6
+ const MAX_DISPLAY_ITEMS = 20;
7
+ export function renderIncrementalChangeStalenessHtml(section) {
8
+ if (section === null) {
9
+ return `<section>
10
+ <h2>Incremental-Change And Staleness Evidence</h2>
11
+ <p>Not applicable to this plugin.</p>
12
+ </section>`;
13
+ }
14
+ return `<section>
15
+ <h2>Incremental-Change And Staleness Evidence</h2>
16
+ <p class="muted">Scoped experimental evidence: results are limited to the executed scenarios below, not a general safety verdict.</p>
17
+ ${table(["Field", "Value"], [
18
+ ["Scenarios", String(section.scenarioCount)],
19
+ ["Ready", String(section.readyScenarioCount)],
20
+ ["Failed", String(section.failedScenarioCount)],
21
+ ["Observed stale regression", String(section.observedStaleRegressionCount)],
22
+ ["No observed stale regression", String(section.noObservedStaleRegressionCount)],
23
+ ["Inconclusive", String(section.inconclusiveCount)]
24
+ ])}
25
+ <h3>Scenario Comparison</h3>
26
+ ${table([
27
+ "Scenario",
28
+ "Category",
29
+ "Status",
30
+ "Relationship",
31
+ "Reindex recommendation",
32
+ "Stale correctness",
33
+ "Full-refresh correctness",
34
+ "Correctness relation",
35
+ "Stale required files",
36
+ "Full-refresh required files",
37
+ "Required-file relation",
38
+ "Stale-risk classification"
39
+ ], section.scenarios.map(comparisonRow))}
40
+ ${section.scenarios.map(renderScenarioDetail).join("\n")}
41
+ <h3>Limitations</h3>
42
+ ${list(section.limitations)}
43
+ </section>`;
44
+ }
45
+ function symmetricAffectedNeighborhood(scenario) {
46
+ const stale = scenario.staleTreatment?.affectedNeighborhood ?? null;
47
+ const fullRefresh = scenario.fullRefreshTreatment?.affectedNeighborhood ?? null;
48
+ if (!stale || !fullRefresh)
49
+ return null;
50
+ if (stale.relationship !== fullRefresh.relationship || stale.reindexRecommendation !== fullRefresh.reindexRecommendation) {
51
+ return null;
52
+ }
53
+ return stale;
54
+ }
55
+ function comparisonRow(scenario) {
56
+ const stale = scenario.staleTreatment?.affectedNeighborhood ?? null;
57
+ const fullRefresh = scenario.fullRefreshTreatment?.affectedNeighborhood ?? null;
58
+ const symmetric = symmetricAffectedNeighborhood(scenario);
59
+ const inconsistent = stale !== null && fullRefresh !== null && symmetric === null;
60
+ return [
61
+ `${scenario.scenarioId}`,
62
+ scenario.category,
63
+ scenario.status,
64
+ inconsistent ? "inconsistent persisted evidence" : symmetric?.relationship ?? "not available",
65
+ inconsistent ? "inconsistent persisted evidence" : symmetric?.reindexRecommendation ?? "not available",
66
+ formatCorrectness(scenario.staleTreatment),
67
+ formatCorrectness(scenario.fullRefreshTreatment),
68
+ scenario.comparison.correctnessRelation,
69
+ scenario.staleTreatment?.requiredFileEvidence.status ?? "not available",
70
+ scenario.fullRefreshTreatment?.requiredFileEvidence.status ?? "not available",
71
+ scenario.comparison.requiredFileEvidenceRelation,
72
+ scenario.comparison.staleRiskClassification
73
+ ];
74
+ }
75
+ function formatCorrectness(treatment) {
76
+ if (!treatment || !treatment.fakeAgent)
77
+ return "unavailable";
78
+ const correctness = treatment.fakeAgent.correctness;
79
+ return correctness.available && correctness.score !== null ? String(correctness.score) : "unavailable";
80
+ }
81
+ function renderScenarioDetail(scenario) {
82
+ return `<h3>Scenario: ${escapeHtml(scenario.scenarioId)}</h3>
83
+ ${renderDefinition(scenario)}
84
+ ${renderLifecycle(scenario)}
85
+ ${renderAffectedNeighborhoodSection(scenario)}
86
+ ${renderTreatment("Stale-Index Treatment", scenario.staleTreatment)}
87
+ ${renderTreatment("Full-Refresh Treatment", scenario.fullRefreshTreatment)}
88
+ ${renderComparison(scenario)}`;
89
+ }
90
+ function renderDefinition(scenario) {
91
+ return `<h4>Scenario Definition</h4>
92
+ ${table(["Field", "Value"], [
93
+ ["Scenario ID", scenario.scenarioId],
94
+ ["Category", scenario.category],
95
+ ["Benchmark project", scenario.benchmarkProjectId],
96
+ ["Base case", scenario.baseCaseId],
97
+ ["Answer policy", scenario.answerPolicy],
98
+ ["Query", scenario.query ?? "unavailable"],
99
+ ["Status", scenario.status],
100
+ ["Failure reason", scenario.failureReason ?? "not applicable"]
101
+ ])}
102
+ <p>Expected files:</p>
103
+ ${boundedList(scenario.expectedFiles)}
104
+ <p>Expected symbols:</p>
105
+ ${boundedList(scenario.expectedSymbols)}`;
106
+ }
107
+ function renderLifecycle(scenario) {
108
+ const lifecycle = scenario.lifecycle;
109
+ if (!lifecycle) {
110
+ return `<h4>Lifecycle Integrity</h4><p>Not available for this scenario.</p>`;
111
+ }
112
+ return `<h4>Lifecycle Integrity</h4>
113
+ ${table(["Field", "Value"], [
114
+ ["Pre-mutation equivalence", lifecycle.preMutationEquivalence],
115
+ ["Post-mutation equivalence", lifecycle.postMutationEquivalence],
116
+ ["my-dev-kit version", lifecycle.myDevKitVersion ?? "unavailable"],
117
+ ["Stale baseline freshness", lifecycle.staleBaselineFreshnessStatus],
118
+ ["Full-refresh baseline freshness", lifecycle.fullRefreshBaselineFreshnessStatus],
119
+ ["Full-refresh refreshed freshness", lifecycle.fullRefreshRefreshedFreshnessStatus ?? "unavailable"]
120
+ ])}
121
+ <p>Controlled file paths:</p>
122
+ ${boundedList(lifecycle.controlledFilePaths)}
123
+ <p>Source roots:</p>
124
+ ${boundedList(lifecycle.sourceRoots)}`;
125
+ }
126
+ function renderAffectedNeighborhoodSection(scenario) {
127
+ const stale = scenario.staleTreatment?.affectedNeighborhood ?? null;
128
+ const fullRefresh = scenario.fullRefreshTreatment?.affectedNeighborhood ?? null;
129
+ if (!stale && !fullRefresh) {
130
+ return `<h4>Affected-Neighborhood Evidence (Changed Baseline Neighborhood Relative To The Task)</h4><p>Not available.</p>`;
131
+ }
132
+ const symmetric = symmetricAffectedNeighborhood(scenario);
133
+ if (!symmetric) {
134
+ return `<h4>Affected-Neighborhood Evidence (Changed Baseline Neighborhood Relative To The Task)</h4><p>inconsistent persisted evidence</p>`;
135
+ }
136
+ const explanation = REINDEX_RECOMMENDATION_EXPLANATIONS[symmetric.reindexRecommendation];
137
+ return `<h4>Affected-Neighborhood Evidence (Changed Baseline Neighborhood Relative To The Task)</h4>
138
+ ${table(["Field", "Value"], [
139
+ ["Relationship", symmetric.relationship],
140
+ ["Reindex recommendation", symmetric.reindexRecommendation],
141
+ ["Changed indexed files", formatAffectedNumber(symmetric.changedFileCount)],
142
+ ["Changed baseline symbols", formatAffectedNumber(symmetric.changedSymbolCount)],
143
+ ["Affected graph nodes", formatAffectedNumber(symmetric.affectedNodeCount)],
144
+ ["Affected graph edges", formatAffectedNumber(symmetric.affectedEdgeCount)],
145
+ ["Task-overlap nodes", formatAffectedNumber(symmetric.taskOverlapCount)],
146
+ ["Task-overlap percent", formatAffectedNumber(symmetric.taskOverlapPercent)]
147
+ ])}
148
+ <p>${escapeHtml(explanation)}</p>`;
149
+ }
150
+ function renderTreatment(title, treatment) {
151
+ if (!treatment) {
152
+ return `<h4>${escapeHtml(title)}</h4><p>Not available.</p>`;
153
+ }
154
+ const retrieval = treatment.retrieval;
155
+ const fakeAgent = treatment.fakeAgent;
156
+ const requiredFileEvidence = treatment.requiredFileEvidence;
157
+ return `<h4>${escapeHtml(title)}</h4>
158
+ ${table(["Field", "Value"], [
159
+ ["Treatment status", treatment.status],
160
+ ["Failure reason", treatment.failureReason ?? "not applicable"],
161
+ ["Active index phase", treatment.activeIndexPhase],
162
+ ["Baseline freshness status", treatment.baselineFreshness.status]
163
+ ])}
164
+ <p><strong>Retrieval</strong> (bounded evidence; full retrieved context body is never rendered here)</p>
165
+ ${table(["Field", "Value"], [
166
+ ["Status", retrieval.status],
167
+ ["Selected node ID", retrieval.selectedNodeId ?? "unavailable"],
168
+ ["Selected file", retrieval.selectedFile ?? "unavailable"],
169
+ ["Selected symbol", retrieval.selectedSymbol ?? "unavailable"],
170
+ ["Context characters", String(retrieval.totalChars)],
171
+ ["Estimated context tokens", String(retrieval.totalEstimatedTokens)],
172
+ ["Duration (ms)", String(retrieval.durationMs)]
173
+ ])}
174
+ <p>Files read:</p>
175
+ ${boundedList(retrieval.filesRead)}
176
+ <p>Retrieval warnings:</p>
177
+ ${boundedList(retrieval.warnings)}
178
+ <p><strong>Fake-Agent Evidence</strong> (deterministic fake-agent / simulated harness evidence, not a provider or real-model measurement)</p>
179
+ ${fakeAgent
180
+ ? table(["Field", "Value"], [
181
+ ["Status", fakeAgent.status],
182
+ ["Correctness score", fakeAgent.correctness.available && fakeAgent.correctness.score !== null ? String(fakeAgent.correctness.score) : "unavailable"],
183
+ ["Passed", fakeAgent.correctness.passed === null ? "unavailable" : String(fakeAgent.correctness.passed)],
184
+ ["Duration (ms)", fakeAgent.durationMs === null ? "unavailable" : String(fakeAgent.durationMs)]
185
+ ])
186
+ : "<p>Not run for this treatment.</p>"}
187
+ <p><strong>Required-File Evidence</strong> (bounded answer-key file-presence check, not retrieval precision/recall)</p>
188
+ ${table(["Field", "Value"], [
189
+ ["Status", requiredFileEvidence.status],
190
+ ["Reason", requiredFileEvidence.reason ?? "not applicable"]
191
+ ])}
192
+ <p>Required files:</p>
193
+ ${boundedList(requiredFileEvidence.requiredFiles)}
194
+ <p>Observed files:</p>
195
+ ${boundedList(requiredFileEvidence.observedFiles)}
196
+ <p>Missing files:</p>
197
+ ${boundedList(requiredFileEvidence.missingFiles)}`;
198
+ }
199
+ function renderComparison(scenario) {
200
+ const comparison = scenario.comparison;
201
+ const explanation = STALE_RISK_CLASSIFICATION_EXPLANATIONS[comparison.staleRiskClassification];
202
+ return `<h4>Matched Comparison</h4>
203
+ ${table(["Field", "Value"], [
204
+ ["Correctness relation", comparison.correctnessRelation],
205
+ ["Required-file evidence relation", comparison.requiredFileEvidenceRelation],
206
+ ["Stale-risk classification", comparison.staleRiskClassification]
207
+ ])}
208
+ <p>Reason codes:</p>
209
+ ${list(comparison.reasonCodes)}
210
+ <p>${escapeHtml(explanation)}</p>`;
211
+ }
212
+ function formatAffectedNumber(value) {
213
+ return value === null ? "unavailable" : String(value);
214
+ }
215
+ function boundedList(items) {
216
+ if (items.length === 0)
217
+ return "<p>none</p>";
218
+ const shown = items.slice(0, MAX_DISPLAY_ITEMS);
219
+ const omitted = items.length - shown.length;
220
+ return `${list(shown)}${omitted > 0 ? `<p class="muted">... ${omitted} more</p>` : ""}`;
221
+ }
222
+ function list(items) {
223
+ if (items.length === 0)
224
+ return "<ul><li>none</li></ul>";
225
+ return `<ul>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`;
226
+ }
227
+ function table(headers, rows) {
228
+ return `<table><thead><tr>${headers.map((header) => `<th>${escapeHtml(header)}</th>`).join("")}</tr></thead><tbody>${rows
229
+ .map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}</tr>`)
230
+ .join("")}</tbody></table>`;
231
+ }
232
+ function escapeHtml(value) {
233
+ return String(value ?? "")
234
+ .replaceAll("&", "&amp;")
235
+ .replaceAll("<", "&lt;")
236
+ .replaceAll(">", "&gt;")
237
+ .replaceAll('"', "&quot;")
238
+ .replaceAll("'", "&#39;");
239
+ }
@@ -1,5 +1,6 @@
1
1
  import { renderContextStrategyComparisonV043Html } from "./renderContextStrategyComparisonV043Html.js";
2
2
  import { renderWarmIndexReuseHtml } from "./renderWarmIndexReuseHtml.js";
3
+ import { renderIncrementalChangeStalenessHtml } from "./renderIncrementalChangeStalenessHtml.js";
3
4
  export function renderPluginExperimentReportHtml(report) {
4
5
  return `<!DOCTYPE html>
5
6
  <html lang="en">
@@ -99,6 +100,8 @@ export function renderPluginExperimentReportHtml(report) {
99
100
 
100
101
  ${renderWarmIndexReuseHtml(report.warmIndexReuse)}
101
102
 
103
+ ${renderIncrementalChangeStalenessHtml(report.incrementalChangeStaleness)}
104
+
102
105
  <section>
103
106
  <h2>Warnings, Skips, And Failures</h2>
104
107
  ${report.findings.length ? table(["Severity", "Code", "Message", "Variant", "Case"], report.findings.map((finding) => [