@dailephd/my-dev-kit-lab 0.6.1 → 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 (34) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +3 -2
  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/affectedNeighborhoodMetrics.js +51 -0
  6. package/dist/src/experiments/defaultRegistry.js +2 -0
  7. package/dist/src/experiments/plugins/incrementalChangeStaleness/comparison.js +101 -0
  8. package/dist/src/experiments/plugins/incrementalChangeStaleness/config.js +66 -0
  9. package/dist/src/experiments/plugins/incrementalChangeStaleness/disposableTarget.js +107 -0
  10. package/dist/src/experiments/plugins/incrementalChangeStaleness/execution.js +404 -0
  11. package/dist/src/experiments/plugins/incrementalChangeStaleness/executionArtifact.js +160 -0
  12. package/dist/src/experiments/plugins/incrementalChangeStaleness/index.js +13 -0
  13. package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecycle.js +276 -0
  14. package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecyclePolicy.js +152 -0
  15. package/dist/src/experiments/plugins/incrementalChangeStaleness/mutationExecution.js +144 -0
  16. package/dist/src/experiments/plugins/incrementalChangeStaleness/plugin.js +274 -0
  17. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioCatalog.js +42 -0
  18. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioMutation.js +116 -0
  19. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioSelection.js +79 -0
  20. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioTypes.js +37 -0
  21. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioValidation.js +168 -0
  22. package/dist/src/experiments/plugins/incrementalChangeStaleness/sourceState.js +145 -0
  23. package/dist/src/experiments/plugins/incrementalChangeStaleness/treatmentSession.js +6 -0
  24. package/dist/src/experiments/plugins/index.js +1 -0
  25. package/dist/src/experiments/plugins/warmIndexReuse/metrics.js +17 -20
  26. package/dist/src/report/experiments/buildIncrementalChangeStalenessReport.js +112 -0
  27. package/dist/src/report/experiments/buildPluginExperimentReport.js +3 -0
  28. package/dist/src/report/experiments/incrementalChangeStalenessReportModel.js +4 -0
  29. package/dist/src/report/experiments/index.js +3 -0
  30. package/dist/src/report/experiments/renderIncrementalChangeStalenessHtml.js +239 -0
  31. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +3 -0
  32. package/dist/src/report/experiments/renderPluginExperimentReportText.js +138 -0
  33. package/docs/METRICS.md +35 -1
  34. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to my-dev-kit-lab are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.6.2] - 2026-09-27
8
+
9
+ ### Incremental-change and staleness plugin
10
+
11
+ - Added the `incremental-change-staleness` experiment plugin (`src/experiments/plugins/incrementalChangeStaleness/`), registered alongside `context-strategy-comparison` and `warm-index-reuse` with registry status `experimental` and exactly two treatments, `stale-index` and `full-refresh`.
12
+ - Added a separate versioned scenario catalog (`benchmarks/contracts/incremental-change-staleness-scenarios.json`, schema `1.0.0`) with six frozen controlled-change scenarios (`U1` unrelated-file, `L2` local-implementation, `E1` exported-symbol, `P1` public-API, `I1` import-graph, `T1` test-only), a bounded declarative mutation contract (relative path, pre/post SHA-256, ordered literal exact-preimage replacements, exactly-one-match validation), and an `inherit`/`scenario` answer-key policy reusing the existing benchmark answer-key vocabulary.
13
+ - Added disposable treatment targets: each scenario runs against two independent disposable copies of the same immutable benchmark project, proving pre-mutation and post-mutation controlled-source equivalence before and after the shared deterministic mutation.
14
+ - Added the stale/full-refresh index lifecycle: both treatments build a baseline index/snapshot/graph before mutation; `stale-index` keeps retrieving from that baseline index, while `full-refresh` builds a distinct complete post-mutation index and retrieves from it. Affected-neighborhood evidence (the released v0.6.1 six numeric metrics, `relationship`, and `reindexRecommendation`) is asserted symmetric across both treatments before retrieval; the six-metric conversion is now shared through `src/evaluation/affectedNeighborhoodMetrics.ts`.
15
+ - Added deterministic fake-agent retrieval/correctness evidence, required-file evidence (`present`/`missing`/`unknown`), a `correctnessRelation` and `requiredFileEvidenceRelation` (`stale-worse`/`same`/`stale-better`/`unknown`), and a conservative `staleRiskClassification` (`observed-stale-regression`/`no-observed-stale-regression`/`inconclusive`) with no numeric stale-risk score, no treatment winner, and no safety verdict.
16
+ - Added the persisted execution artifact `incremental-change-staleness-execution.json` (schema `my-dev-kit-lab-incremental-change-staleness-execution-v1`) and plugin `report.json`/`report.txt`/`report.html` presentation built from that persisted evidence only, with fixed limitations text.
17
+ - `experiment run --kit-command` now also accepts `incremental-change-staleness`, in addition to `warm-index-reuse`; it remains rejected for `context-strategy-comparison`.
18
+ - Validated the exact npm tarball end to end against the real published `@dailephd/my-dev-kit@1.12.4`: all six scenarios ran through the installed CLI, producing the execution artifact and all three reports while keeping the installed package, canonical benchmarks, and source checkout immutable.
19
+ - No partial refresh, no `graph-diff` dependency, no true-symbol-diff metric, and no new plot, screenshot, gallery item, or top-level CLI command were added.
20
+
7
21
  ## [0.6.1] - 2026-09-26
8
22
 
9
23
  ### Affected-neighborhood experiments
package/README.md CHANGED
@@ -6,7 +6,7 @@ my-dev-kit provides local repository indexing and graph-guided retrieval. my-dev
6
6
 
7
7
  Whole-ecosystem workflows are centralized in [my-dev-kit/docs/ECOSYSTEM_DEVELOPMENT_WORKFLOWS.md](https://github.com/dailephd/my-dev-kit/blob/main/docs/ECOSYSTEM_DEVELOPMENT_WORKFLOWS.md). Its [command-surface compatibility map](https://github.com/dailephd/my-dev-kit/blob/main/docs/ECOSYSTEM_DEVELOPMENT_WORKFLOWS.md#915-command-surface-compatibility-map) documents which Lab outputs can safely seed my-dev-kit investigation, Observer reference work, or Orchestrator evidence, and which look-alike artifacts are not direct inputs.
8
8
 
9
- The latest release is v0.6.1 (affected-neighborhood experiments), and the package and installed CLI are version 0.6.1. The previous release is v0.6.0 (index freshness and changed-file detection). Version v0.6.2 (incremental-change and staleness plugin) is the next planned version. The v0.4.5 context-integrity evaluation remains intentionally frozen against the published `@dailephd/my-dev-kit@1.10.4` and `@dailephd/my-dev-kit-orchestrator@1.2.3` contracts; those versions are historical validation baselines, not a statement that they are the ecosystem's current releases. See [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) for current state.
9
+ The latest release is v0.6.2 (incremental-change and staleness experiments), and the published package and installed CLI are version 0.6.2. The previous release is v0.6.1 (affected-neighborhood experiments). Partial-refresh work is planned for v0.6.3. The v0.4.5 context-integrity evaluation remains intentionally frozen against the published `@dailephd/my-dev-kit@1.10.4` and `@dailephd/my-dev-kit-orchestrator@1.2.3` contracts; those versions are historical validation baselines, not a statement that they are the ecosystem's current releases. See [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) for current state.
10
10
 
11
11
  v0.4.6 adds a supported `my-dev-kit-lab` installed CLI router (`--help`, `--version`, `security validate`, `audit`, the `experiment` family, `report render`, `plots generate`, `gallery build`, `demo final`, and the historical direct final-demo invocation form), a writable lab workspace model kept separate from the installed package and the inspected target, and a permanent packed-tarball installation/execution acceptance gate (`npm run verify:packed-package`). See [docs/ROADMAP.md](docs/ROADMAP.md) and [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) for status detail and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the runtime path model. The "Installed CLI" section below documents the shipped command surface; the source-checkout `npm run` workflow in Quickstart remains available for contributors.
12
12
 
@@ -16,6 +16,7 @@ v0.4.6 adds a supported `my-dev-kit-lab` installed CLI router (`--help`, `--vers
16
16
  - **Measure warm-index reuse (v0.5.0–v0.5.1, released):** the `warm-index-reuse` experiment plugin builds one my-dev-kit index per benchmark project, reuses it across that project's tasks, and compares every task with a matched `raw-full-file` baseline. Reports separate one-time index-build cost from per-task retrieval cost, show amortized index cost and cumulative measurements, and include deterministic fake-agent correctness and token evidence; `plots generate` renders four warm-index SVG charts. The v0.5.1 release adds a dedicated 12-task benchmark corpus (`benchmarks/contracts/warm-index-benchmark-cases.json`: six medium and six large/mixed tasks tagged `localized`, `cross-module`, or `broad-change`) for comparing warm-index behavior as task count grows. Estimated context tokens (a character-based context-size estimate) and fake-agent total tokens (simulated harness telemetry) are reported separately, and neither is provider billing telemetry.
17
17
  - **Run selectable real-agent warm-index campaigns (v0.5.2, released):** `--campaign codex-full`, `claude-full`, or `codex-timeout-isolation` (with `--include-real-agents`) runs exactly one provider — Codex or Claude — against the exact raw and warm contexts the experiment already measured, over the frozen production corpus each preset owns. Provider outcomes (`completed`, `failed`, `timeout`, `invalid-output`, `agent-unavailable`, `agent-limit-reached`, `skipped`) are classified and reported separately from warm-index infrastructure status, so a campaign can complete its indexing/retrieval work even when provider evidence is partial or unavailable. A campaign run automatically produces the report, the same four warm-index SVG charts, a best-effort report screenshot (skipped without a browser runtime, never fatal), and a three-item gallery (report, plots, bounded execution evidence). Legacy runs without `--campaign` keep the existing deterministic fake-agent, report-only behavior unchanged. See [docs/WORKFLOWS.md](docs/WORKFLOWS.md#warm-index-reuse-experiment) and [docs/METRICS.md](docs/METRICS.md#warm-index-reuse-metrics).
18
18
  - **Record index freshness evidence (v0.6.0):** a warm-index run captures an index snapshot after its one index build — the exact indexed file set from the my-dev-kit index contract, each with SHA-256 content identity, size, and modified-time metadata, plus the my-dev-kit version and index-command evidence — and assesses freshness immediately before each task's warm retrieval as `fresh`, `stale`, `partially-stale`, or `unknown`. The evidence appears in `warm-index-execution.json` and in the existing `report.json`/`report.txt`/`report.html` warm-index reports. It is observational only: it covers only files the snapshot lists (not the whole repository), never changes retrieval, execution or provider status, and adds no CLI flag, metric, plot, gallery item, or reindex recommendation in v0.6.0. See [docs/METRICS.md](docs/METRICS.md#index-freshness-evidence-v060) and [docs/WORKFLOWS.md](docs/WORKFLOWS.md#warm-index-reuse-experiment).
19
+ - **Compare stale-index vs full-refresh retrieval after a controlled change (`v0.6.2`):** the `incremental-change-staleness` experiment plugin runs six frozen controlled-change scenario families (unrelated-file, local-implementation, exported-symbol, public-API, import-graph, and test-only changes) against two disposable target copies — one kept on its pre-change baseline index (`stale-index`) and one fully reindexed after the same deterministic change (`full-refresh`) — and persists retrieval, deterministic fake-agent correctness, required-file, and a conservative stale-risk classification (`observed-stale-regression`, `no-observed-stale-regression`, or `inconclusive`) in `incremental-change-staleness-execution.json` and the existing `report.json`/`report.txt`/`report.html`. `--kit-command` also accepts `incremental-change-staleness`. See [docs/WORKFLOWS.md](docs/WORKFLOWS.md) and [docs/METRICS.md](docs/METRICS.md).
19
20
  - **Assess affected-neighborhood evidence (v0.6.1, current):** using the baseline my-dev-kit graph of the same prepared warm index (loaded once per session), each task's confirmed modified or missing indexed files, and the baseline symbols those files contain, are mapped to graph nodes. The affected neighborhood is those seed nodes plus their direct one-hop neighbors (edges of any kind, either direction). The task's `expectedFiles` and file-scoped `expectedSymbols` are mapped to graph nodes and compared with that neighborhood, which yields six numeric warm-side metrics (`changedFileCount`, `changedSymbolCount`, `affectedNodeCount`, `affectedEdgeCount`, `taskOverlapCount`, `taskOverlapPercent`) plus categorical evidence: a `related`, `unrelated`, or `unknown` relationship and a `recommended`, `not-indicated`, or `unknown` `reindexRecommendation`. Incomplete evidence is reported as `unknown`, never as unrelated. The assessment is recorded additively in `warm-index-execution.json` and shown in the existing `report.json`/`report.txt`/`report.html` warm-index reports. It is observational only: retrieval, execution status, and provider status are unchanged, nothing is reindexed automatically, and no CLI flag, experiment plugin, plot, or gallery item was added. See [docs/METRICS.md](docs/METRICS.md#affected-neighborhood-metrics-v061) and [docs/WORKFLOWS.md](docs/WORKFLOWS.md#warm-index-reuse-experiment).
20
21
  - **Audit repository health:** run conservative code-rot detectors for TypeScript/JavaScript, Python, Java, and Kotlin, or adapt security findings into the common audit report.
21
22
  - **Validate CLI/package security:** inspect dependencies, package contents, path and subprocess boundaries, malformed inputs, optional static scanners, and bounded fuzz targets.
@@ -235,7 +236,7 @@ See [docs/METRICS.md](docs/METRICS.md) for full metric definitions.
235
236
  - Claude and Codex token totals are used only when the CLI/run exposes them; missing provider token evidence stays explicitly unavailable (never zero, and never substituted with an estimated context-token count)
236
237
  - Codex and Claude campaign runs can produce timeouts, invalid-output, agent-unavailable, or agent-limit-reached outcomes; these are structured provider states, not warm-index infrastructure failures
237
238
  - Small projects may make raw-full-file cheaper than my-dev-kit-guided; larger localized tasks are where my-dev-kit is expected to become more useful
238
- - The generic experiment-plugin framework has two plugins: `context-strategy-comparison` and the v0.5.0-introduced `warm-index-reuse`. Later plugins such as incremental-change and context-window scaling are future roadmap work
239
+ - The generic experiment-plugin framework has three registered plugins: `context-strategy-comparison`, the v0.5.0-introduced `warm-index-reuse`, and the `v0.6.2` `incremental-change-staleness` plugin (current). Later plugins such as context-window scaling remain future roadmap work
239
240
  - `warm-index-reuse` without `--campaign` still uses the deterministic fake agent only. With `--campaign`, exactly one real provider (Codex or Claude) runs per campaign preset — there is no multi-agent matrix, retry, provider switching, or scheduler. The v0.5.1 expanded corpus is deterministic fake-agent benchmark evidence when run without `--campaign`. No warm-index mode calculates a token-savings percentage, break-even task, winner, or ranking, and component-duration sums are not wall-clock latency
240
241
  - The current release does not guarantee token savings; it produces auditable evidence for specific cases, targets, agents, and strategies
241
242
  - Released v0.6.1 affected-neighborhood evidence is bounded to the baseline graph of the prepared index and to the snapshot-represented files: a changed symbol means the symbol was present in a confirmed changed indexed file, not that its own text was proven to change; `not-indicated` means only that this bounded analysis found no supporting evidence for reindexing, not that skipping a reindex is safe; unresolved or ambiguous expected symbols keep a task at `unknown` rather than unrelated
@@ -0,0 +1,360 @@
1
+ {
2
+ "schemaVersion": "1.0.0",
3
+ "scenarios": [
4
+ {
5
+ "id": "U1",
6
+ "category": "unrelated-file-change",
7
+ "benchmarkProjectId": "task-analytics-large-mixed",
8
+ "baseCaseId": "warm-large-ts-leaderboard",
9
+ "answerPolicy": "inherit",
10
+ "expectedRelationship": "unrelated",
11
+ "mutation": {
12
+ "files": [
13
+ {
14
+ "path": "py/task_analytics/quality.py",
15
+ "expectedPreSha256": "5faab1bf018de6e647a11a2edffd641a51c0c05b399cb6f58a2c22b0ff035554",
16
+ "operations": [
17
+ {
18
+ "expectedPreimage": "metric.completion_rate >= 80 and metric.stale_tasks == 0",
19
+ "replacement": "metric.completion_rate >= 81 and metric.stale_tasks == 0"
20
+ }
21
+ ],
22
+ "expectedPostSha256": "9f59d256710a9345eb6e0f768e45c4676041b4cac215cd84cd1075cfd730ab18"
23
+ }
24
+ ]
25
+ },
26
+ "notes": "Negative control across the intentionally separate Python and TypeScript implementations. The follow-up TypeScript leaderboard query/answer is unaffected by this Python-only threshold change, so this record states the scenario planning expectation as unrelated. Runtime classification in later batches must still produce unknown instead of unrelated when evidence is incomplete, per v0.6.1 semantics; Batch 1 performs no runtime classification."
27
+ },
28
+ {
29
+ "id": "L2",
30
+ "category": "local-implementation-change",
31
+ "benchmarkProjectId": "task-workflow-medium-ts",
32
+ "baseCaseId": "warm-medium-complete-idempotent",
33
+ "answerPolicy": "scenario",
34
+ "scenarioQuery": "complete task by id after the completedAt timestamp change idempotent unknown task",
35
+ "scenarioAnswerKey": {
36
+ "expectedFiles": [
37
+ "src/services/completeTask.ts",
38
+ "src/store/taskStore.ts"
39
+ ],
40
+ "expectedSymbols": [
41
+ "completeTask",
42
+ "TaskWorkflowStore"
43
+ ],
44
+ "expectedFacts": [
45
+ {
46
+ "id": "warm062-l2-sets-new-completed-at",
47
+ "text": "The first completion sets completed to true and completedAt to \"2026-03-01T00:00:00.000Z\".",
48
+ "weight": 1,
49
+ "required": true
50
+ },
51
+ {
52
+ "id": "warm062-l2-still-idempotent",
53
+ "text": "When the task is already completed the updater returns it unchanged, so completing it again does not rewrite completedAt.",
54
+ "weight": 1,
55
+ "required": true
56
+ },
57
+ {
58
+ "id": "warm062-l2-unknown-task-throws",
59
+ "text": "An unknown task id still throws \"Unknown task id: <id>\" from TaskWorkflowStore.updateTask.",
60
+ "weight": 1,
61
+ "required": true
62
+ },
63
+ {
64
+ "id": "warm062-l2-targets-by-id",
65
+ "text": "completeTask(store, taskId) still changes exactly one task by id through TaskWorkflowStore.updateTask.",
66
+ "weight": 1,
67
+ "required": true
68
+ }
69
+ ],
70
+ "minimumCorrectFacts": 4,
71
+ "notes": "v0.6.2 L2 scenario: local implementation change to the completedAt literal. Exported name, callable signature, idempotence, and unknown-task behavior are preserved."
72
+ },
73
+ "mutation": {
74
+ "files": [
75
+ {
76
+ "path": "src/services/completeTask.ts",
77
+ "expectedPreSha256": "27f6833abb1c5f8fa7226ec8a4857ba267dd48272d7cf094c7a3462e425ca64c",
78
+ "operations": [
79
+ {
80
+ "expectedPreimage": "\"2026-02-01T00:00:00.000Z\"",
81
+ "replacement": "\"2026-03-01T00:00:00.000Z\""
82
+ }
83
+ ],
84
+ "expectedPostSha256": "0d148ddab0dbd43b9bc8eb40a09de9dfd4567b9097fb0f028b74ebf921798cc8"
85
+ }
86
+ ]
87
+ },
88
+ "notes": "Local implementation change: only the fixed completedAt literal changes."
89
+ },
90
+ {
91
+ "id": "E1",
92
+ "category": "exported-symbol-change",
93
+ "benchmarkProjectId": "task-analytics-large-mixed",
94
+ "baseCaseId": "warm-large-python-pipeline",
95
+ "answerPolicy": "scenario",
96
+ "scenarioQuery": "pipeline parse rows project metrics quality label health report after the healthy completion-rate boundary changed",
97
+ "scenarioAnswerKey": {
98
+ "expectedFiles": [
99
+ "py/task_analytics/pipeline.py",
100
+ "py/task_analytics/parser.py",
101
+ "py/task_analytics/metrics.py",
102
+ "py/task_analytics/quality.py",
103
+ "py/task_analytics/reporting.py"
104
+ ],
105
+ "expectedSymbols": [
106
+ "build_report_from_rows",
107
+ "parse_task_rows",
108
+ "calculate_project_metrics",
109
+ "determine_quality_label",
110
+ "build_health_report"
111
+ ],
112
+ "expectedFacts": [
113
+ {
114
+ "id": "warm062-e1-new-healthy-boundary",
115
+ "text": "determine_quality_label now returns \"healthy\" only when completion_rate is at least 85 (previously 80) and stale_tasks is 0.",
116
+ "weight": 1,
117
+ "required": true
118
+ },
119
+ {
120
+ "id": "warm062-e1-signature-unchanged",
121
+ "text": "determine_quality_label keeps its exported name and single-argument signature; only its healthy-boundary behavior changed.",
122
+ "weight": 1,
123
+ "required": true
124
+ },
125
+ {
126
+ "id": "warm062-e1-role-in-pipeline",
127
+ "text": "build_health_report still calls determine_quality_label for each metric produced by calculate_project_metrics inside build_report_from_rows.",
128
+ "weight": 1,
129
+ "required": true
130
+ },
131
+ {
132
+ "id": "warm062-e1-deterministic-output",
133
+ "text": "The report remains one line per project in sorted project_id order, so fixed rows and current_day still produce the same output under the new boundary.",
134
+ "weight": 1,
135
+ "required": true
136
+ }
137
+ ],
138
+ "minimumCorrectFacts": 3,
139
+ "notes": "v0.6.2 E1 scenario: exported-symbol change. Export presence, symbol name, and callable signature remain stable; externally observable behavior changes."
140
+ },
141
+ "mutation": {
142
+ "files": [
143
+ {
144
+ "path": "py/task_analytics/quality.py",
145
+ "expectedPreSha256": "5faab1bf018de6e647a11a2edffd641a51c0c05b399cb6f58a2c22b0ff035554",
146
+ "operations": [
147
+ {
148
+ "expectedPreimage": "metric.completion_rate >= 80 and metric.stale_tasks == 0",
149
+ "replacement": "metric.completion_rate >= 85 and metric.stale_tasks == 0"
150
+ }
151
+ ],
152
+ "expectedPostSha256": "9cd4df5fd2fd29b99a1e82b744a7bd7afddb123a2fe1d861c1edcce21a32998c"
153
+ }
154
+ ]
155
+ },
156
+ "notes": "Same canonical quality.py file as U1, but with an independent scenario-specific mutation to threshold 85 instead of 81."
157
+ },
158
+ {
159
+ "id": "P1",
160
+ "category": "public-api-change",
161
+ "benchmarkProjectId": "task-analytics-large-mixed",
162
+ "baseCaseId": "warm-large-python-pipeline",
163
+ "answerPolicy": "scenario",
164
+ "scenarioQuery": "calculate_project_metrics signature stale_day_threshold parameter default behavior for callers that omit it",
165
+ "scenarioAnswerKey": {
166
+ "expectedFiles": [
167
+ "py/task_analytics/metrics.py",
168
+ "py/task_analytics/pipeline.py"
169
+ ],
170
+ "expectedSymbols": [
171
+ "calculate_project_metrics",
172
+ "STALE_DAY_THRESHOLD"
173
+ ],
174
+ "expectedFacts": [
175
+ {
176
+ "id": "warm062-p1-new-optional-param",
177
+ "text": "calculate_project_metrics(tasks, current_day, stale_day_threshold=STALE_DAY_THRESHOLD) now accepts an optional stale_day_threshold parameter.",
178
+ "weight": 1,
179
+ "required": true
180
+ },
181
+ {
182
+ "id": "warm062-p1-default-preserved",
183
+ "text": "The parameter defaults to the existing STALE_DAY_THRESHOLD constant (10), so callers that omit it keep identical prior behavior.",
184
+ "weight": 1,
185
+ "required": true
186
+ },
187
+ {
188
+ "id": "warm062-p1-predicate-uses-param",
189
+ "text": "The stale-task predicate now compares current_day minus task.updated_day against stale_day_threshold instead of the module constant directly.",
190
+ "weight": 1,
191
+ "required": true
192
+ },
193
+ {
194
+ "id": "warm062-p1-pipeline-caller-unaffected",
195
+ "text": "pipeline.py build_report_from_rows calls calculate_project_metrics(tasks, current_day) without the new argument, so its behavior is unchanged.",
196
+ "weight": 1,
197
+ "required": true
198
+ }
199
+ ],
200
+ "minimumCorrectFacts": 3,
201
+ "notes": "v0.6.2 P1 scenario: public API change. Existing callers that omit the new argument preserve prior default behavior; the callable contract itself changes."
202
+ },
203
+ "mutation": {
204
+ "files": [
205
+ {
206
+ "path": "py/task_analytics/metrics.py",
207
+ "expectedPreSha256": "da2bfce46ec51016f87b3456a201e69a9d71721002e1781b44fdc59a96650e91",
208
+ "operations": [
209
+ {
210
+ "expectedPreimage": "def calculate_project_metrics(tasks, current_day):",
211
+ "replacement": "def calculate_project_metrics(tasks, current_day, stale_day_threshold=STALE_DAY_THRESHOLD):"
212
+ },
213
+ {
214
+ "expectedPreimage": "current_day - task.updated_day >= STALE_DAY_THRESHOLD",
215
+ "replacement": "current_day - task.updated_day >= stale_day_threshold"
216
+ }
217
+ ],
218
+ "expectedPostSha256": "c91af3c5549606c522a6cef8264d4da1a6febed0a58757ad5a96cafcb3ed58c9"
219
+ }
220
+ ]
221
+ },
222
+ "notes": "Two ordered exact replacements: the signature gains the optional parameter first, then the stale predicate is switched to use it."
223
+ },
224
+ {
225
+ "id": "I1",
226
+ "category": "import-graph-change",
227
+ "benchmarkProjectId": "task-analytics-large-mixed",
228
+ "baseCaseId": "warm-large-ts-analytics-snapshot",
229
+ "answerPolicy": "scenario",
230
+ "scenarioQuery": "analytics snapshot project completion rate stale tasks average story points task store after buildAnalyticsSnapshot task-listing dependency changed",
231
+ "scenarioAnswerKey": {
232
+ "expectedFiles": [
233
+ "ts/src/services/buildAnalyticsSnapshot.ts",
234
+ "ts/src/models/analyticsSnapshot.ts",
235
+ "ts/src/store/taskStore.ts"
236
+ ],
237
+ "expectedSymbols": [
238
+ "buildAnalyticsSnapshot",
239
+ "ProjectSnapshot",
240
+ "AnalyticsTaskStore"
241
+ ],
242
+ "expectedFacts": [
243
+ {
244
+ "id": "warm062-i1-no-longer-imports-helper",
245
+ "text": "buildAnalyticsSnapshot no longer imports or calls listTasksByProject.",
246
+ "weight": 1,
247
+ "required": true
248
+ },
249
+ {
250
+ "id": "warm062-i1-inline-filtering",
251
+ "text": "buildAnalyticsSnapshot now obtains a target projects tasks directly with taskStore.list().filter((task) => task.projectId === project.id).",
252
+ "weight": 1,
253
+ "required": true
254
+ },
255
+ {
256
+ "id": "warm062-i1-output-unchanged",
257
+ "text": "The inline filtering is behaviorally identical to the removed helper, so totalTasks, completedTasks, openTasks, staleTasks, and averageStoryPoints per project are unchanged.",
258
+ "weight": 1,
259
+ "required": true
260
+ },
261
+ {
262
+ "id": "warm062-i1-totals-aggregate-unchanged",
263
+ "text": "The snapshot totals still sum totalTasks, completedTasks, openTasks, and staleTasks across the per-project snapshots.",
264
+ "weight": 1,
265
+ "required": true
266
+ }
267
+ ],
268
+ "minimumCorrectFacts": 3,
269
+ "notes": "v0.6.2 I1 scenario: import-graph change. The intended snapshot output remains equivalent while the dependency edge on listTasksByProject disappears."
270
+ },
271
+ "mutation": {
272
+ "files": [
273
+ {
274
+ "path": "ts/src/services/buildAnalyticsSnapshot.ts",
275
+ "expectedPreSha256": "928af15041715ffa19ae3674e8d4f82e1c5f694cfa2bad1fea03db31bce6cedd",
276
+ "operations": [
277
+ {
278
+ "expectedPreimage": "import { listTasksByProject } from \"./listTasksByProject.js\";\r\n",
279
+ "replacement": ""
280
+ },
281
+ {
282
+ "expectedPreimage": "const tasks = listTasksByProject(taskStore, project.id);",
283
+ "replacement": "const tasks = taskStore.list().filter((task) => task.projectId === project.id);"
284
+ }
285
+ ],
286
+ "expectedPostSha256": "1ab0616f11107a2ffabfc1d7ee2b0614d9c9c58cd62fb461313a87c9754570b1"
287
+ }
288
+ ]
289
+ },
290
+ "notes": "Two ordered exact replacements: remove the now-unused import, then inline the equivalent filtering that listTasksByProject itself performed."
291
+ },
292
+ {
293
+ "id": "T1",
294
+ "category": "test-only-change",
295
+ "benchmarkProjectId": "task-analytics-large-mixed",
296
+ "baseCaseId": "warm-large-python-pipeline",
297
+ "answerPolicy": "scenario",
298
+ "scenarioQuery": "In py/tests/test_quality.py, what updated_day value does task-1 use in test_determine_quality_label, and what quality label does the test assert after calculating project metrics?",
299
+ "scenarioAnswerKey": {
300
+ "expectedFiles": [
301
+ "py/tests/test_quality.py"
302
+ ],
303
+ "expectedSymbols": [
304
+ "determine_quality_label",
305
+ "test_determine_quality_label"
306
+ ],
307
+ "expectedFacts": [
308
+ {
309
+ "id": "warm062-t1-task1-updated-day-nine",
310
+ "text": "task-1 row in test_determine_quality_label now uses updated_day 9 (previously 8).",
311
+ "weight": 1,
312
+ "required": true
313
+ },
314
+ {
315
+ "id": "warm062-t1-both-rows-completed",
316
+ "text": "Both fixture rows remain completed, so total_tasks stays 2 and completed_tasks stays 2.",
317
+ "weight": 1,
318
+ "required": true
319
+ },
320
+ {
321
+ "id": "warm062-t1-completion-rate-unchanged",
322
+ "text": "The computed project completion_rate therefore remains 100.0.",
323
+ "weight": 1,
324
+ "required": true
325
+ },
326
+ {
327
+ "id": "warm062-t1-no-stale-task-introduced",
328
+ "text": "current_day (15) minus the new updated_day (9) is 6, still below STALE_DAY_THRESHOLD (10), so stale_tasks remains 0.",
329
+ "weight": 1,
330
+ "required": true
331
+ },
332
+ {
333
+ "id": "warm062-t1-still-healthy",
334
+ "text": "The test still asserts determine_quality_label(metrics[0]) == \"healthy\"; production source and behavior are unchanged.",
335
+ "weight": 1,
336
+ "required": true
337
+ }
338
+ ],
339
+ "minimumCorrectFacts": 4,
340
+ "notes": "v0.6.2 T1 scenario: test-only change. Only the task-1 updated_day test input changes; production source in quality.py, metrics.py, and parser.py is unchanged, and the healthy-label assertion still passes."
341
+ },
342
+ "mutation": {
343
+ "files": [
344
+ {
345
+ "path": "py/tests/test_quality.py",
346
+ "expectedPreSha256": "b5610a2720012532baf6d18e114f375424008e0daadbc27291bfca2ce6316875",
347
+ "operations": [
348
+ {
349
+ "expectedPreimage": "{\"task_id\": \"task-1\", \"project_id\": \"alpha\", \"completed\": True, \"story_points\": 5, \"updated_day\": 8}",
350
+ "replacement": "{\"task_id\": \"task-1\", \"project_id\": \"alpha\", \"completed\": True, \"story_points\": 5, \"updated_day\": 9}"
351
+ }
352
+ ],
353
+ "expectedPostSha256": "eda7f39eed5b08d615b02a01b3b05655ed2aad06b65c39ad0ea09e9b93992ea5"
354
+ }
355
+ ]
356
+ },
357
+ "notes": "Test-only change: exactly one bounded test input literal changes. Production source and production behavior are unchanged; the query/answer targets the changed test evidence itself."
358
+ }
359
+ ]
360
+ }
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import { parseAgentCommandTemplate } from "../agents/index.js";
3
3
  import { parseAgentId } from "../agents/agentRegistry.js";
4
4
  import { readBenchmarkProjectProfiles, readEvaluationCases } from "../evaluation/index.js";
5
- import { contextStrategyComparisonPlugin, createDefaultExperimentPluginRegistry, getWarmIndexCampaignPreset, parseWarmIndexCampaignPresetId, resolveExperimentTarget, runExperiment, warmIndexReusePlugin } from "../experiments/index.js";
5
+ import { contextStrategyComparisonPlugin, createDefaultExperimentPluginRegistry, getWarmIndexCampaignPreset, incrementalChangeStalenessPlugin, parseWarmIndexCampaignPresetId, resolveExperimentTarget, runExperiment, warmIndexReusePlugin } from "../experiments/index.js";
6
6
  import { buildDefaultExperimentOutputRoot } from "../experiments/outputPaths.js";
7
7
  import { parsePromptComplexityLevel, parsePromptStrategy } from "../prompts/index.js";
8
8
  import { writePluginExperimentReports } from "../report/index.js";
@@ -220,8 +220,9 @@ export function parseRunExperimentArgs(argv) {
220
220
  if (!experimentId) {
221
221
  throw new Error("Usage: --experiment <id> [--target <path>] [--out <directory>]");
222
222
  }
223
- if (kitCommand !== undefined && experimentId !== warmIndexReusePlugin.metadata.id) {
224
- throw new Error(`--kit-command is only supported for --experiment ${warmIndexReusePlugin.metadata.id}.`);
223
+ const KIT_COMMAND_PLUGIN_IDS = [warmIndexReusePlugin.metadata.id, incrementalChangeStalenessPlugin.metadata.id];
224
+ if (kitCommand !== undefined && !KIT_COMMAND_PLUGIN_IDS.includes(experimentId)) {
225
+ throw new Error(`--kit-command is only supported for --experiment ${KIT_COMMAND_PLUGIN_IDS.join(" or ")}.`);
225
226
  }
226
227
  if (campaignPreset !== undefined) {
227
228
  if (experimentId !== warmIndexReusePlugin.metadata.id) {
@@ -0,0 +1,51 @@
1
+ const FIELD_LABELS = {
2
+ changedFileCount: "the changed indexed file count",
3
+ changedSymbolCount: "the changed baseline symbol count",
4
+ affectedNodeCount: "the affected graph node count",
5
+ affectedEdgeCount: "the affected graph edge count",
6
+ taskOverlapCount: "the task-overlap node count",
7
+ taskOverlapPercent: "the task-overlap percent"
8
+ };
9
+ /**
10
+ * Converts a persisted (or in-memory) affected-neighborhood assessment into the six frozen v0.6.1
11
+ * metrics, once. A finite value (including zero) is available; a missing assessment or a null
12
+ * field is unavailable with a bounded reason, never zero. Pure; this is the ONLY owner of this
13
+ * conversion, reused unchanged by every caller.
14
+ */
15
+ export function toAffectedNeighborhoodMetricValues(assessment, noAssessmentReason) {
16
+ const one = (field) => {
17
+ const unit = field === "taskOverlapPercent" ? "percent" : "count";
18
+ if (!assessment) {
19
+ return { availability: "unavailable", value: null, unit, reason: noAssessmentReason };
20
+ }
21
+ const value = assessment[field];
22
+ if (typeof value === "number") {
23
+ return { availability: "available", value, unit, reason: null };
24
+ }
25
+ const context = `assessment ${assessment.status}; seed mapping ${assessment.seedMappingStatus}; ` +
26
+ `graph ${assessment.graphEvidenceStatus}; neighborhood ${assessment.neighborhoodStatus}; task mapping ${assessment.taskMapping.status}` +
27
+ (field === "taskOverlapPercent" ? `; resolvable task nodes ${assessment.taskMapping.resolvableTaskNodeCount}` : "");
28
+ return {
29
+ availability: "unavailable",
30
+ value: null,
31
+ unit,
32
+ reason: `Affected-neighborhood evidence did not establish ${FIELD_LABELS[field]} (${context}).`
33
+ };
34
+ };
35
+ return {
36
+ changedFileCount: one("changedFileCount"),
37
+ changedSymbolCount: one("changedSymbolCount"),
38
+ affectedNodeCount: one("affectedNodeCount"),
39
+ affectedEdgeCount: one("affectedEdgeCount"),
40
+ taskOverlapCount: one("taskOverlapCount"),
41
+ taskOverlapPercent: one("taskOverlapPercent")
42
+ };
43
+ }
44
+ export const AFFECTED_NEIGHBORHOOD_METRIC_FIELDS = [
45
+ "changedFileCount",
46
+ "changedSymbolCount",
47
+ "affectedNodeCount",
48
+ "affectedEdgeCount",
49
+ "taskOverlapCount",
50
+ "taskOverlapPercent"
51
+ ];
@@ -1,9 +1,11 @@
1
1
  import { ExperimentPluginRegistry } from "./registry.js";
2
2
  import { contextStrategyComparisonPlugin } from "./plugins/contextStrategyComparison/index.js";
3
+ import { incrementalChangeStalenessPlugin } from "./plugins/incrementalChangeStaleness/index.js";
3
4
  import { warmIndexReusePlugin } from "./plugins/warmIndexReuse/index.js";
4
5
  export function createDefaultExperimentPluginRegistry() {
5
6
  const registry = new ExperimentPluginRegistry();
6
7
  registry.register(contextStrategyComparisonPlugin);
7
8
  registry.register(warmIndexReusePlugin);
9
+ registry.register(incrementalChangeStalenessPlugin);
8
10
  return registry;
9
11
  }
@@ -0,0 +1,101 @@
1
+ // v0.6.2 Batch 4 -- pure, deterministic comparison/classification owner.
2
+ //
3
+ // This module owns exactly:
4
+ // - CorrectnessRelationV1;
5
+ // - RequiredFileEvidenceStatusV1 / RequiredFileEvidenceV1;
6
+ // - RequiredFileEvidenceRelationV1;
7
+ // - StaleRiskClassificationV1;
8
+ // - the pure functions that compute them.
9
+ //
10
+ // Every function here is a pure, side-effect-free transform over already
11
+ // collected evidence. No retrieval, no filesystem access, no my-dev-kit
12
+ // invocation, and no numeric risk score: see the frozen batch prompt
13
+ // sections 22-35 for the exact rules this file implements.
14
+ /** Section 23: exact correctness comparison rules. */
15
+ export function compareCorrectness(stale, fullRefresh) {
16
+ if (!stale.available || !fullRefresh.available) {
17
+ return "unknown";
18
+ }
19
+ if (stale.score < fullRefresh.score)
20
+ return "stale-worse";
21
+ if (stale.score > fullRefresh.score)
22
+ return "stale-better";
23
+ return "same";
24
+ }
25
+ /** Repository-standard forward-slash path normalization, matching existing lifecycle policy. */
26
+ export function normalizeRequiredFilePath(value) {
27
+ return value.replace(/\\/g, "/").replace(/^(\.\/)+/, "");
28
+ }
29
+ function sortedUnique(values) {
30
+ return [...new Set(values.map(normalizeRequiredFilePath))].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
31
+ }
32
+ /**
33
+ * Section 27: builds one treatment's required-file evidence.
34
+ *
35
+ * - `observedFilesComplete: false` means the treatment's actual-files-read set could not be
36
+ * established completely (retrieval failed/partial or the contract could not prove completeness);
37
+ * the result is always `unknown` in that case, regardless of `observedFiles` contents.
38
+ * - Otherwise: every normalized required file present in `observedFiles` is `present`; any
39
+ * absence is `missing`.
40
+ */
41
+ export function buildRequiredFileEvidence(args) {
42
+ const requiredFiles = sortedUnique(args.requiredFiles);
43
+ const observedFiles = sortedUnique(args.observedFiles);
44
+ if (!args.observedFilesComplete) {
45
+ return {
46
+ status: "unknown",
47
+ requiredFiles,
48
+ observedFiles,
49
+ missingFiles: [],
50
+ reason: args.unknownReason ?? "Retrieval evidence did not establish a complete actual-files-read set."
51
+ };
52
+ }
53
+ const observedSet = new Set(observedFiles);
54
+ const missingFiles = requiredFiles.filter((file) => !observedSet.has(file));
55
+ return {
56
+ status: missingFiles.length === 0 ? "present" : "missing",
57
+ requiredFiles,
58
+ observedFiles,
59
+ missingFiles,
60
+ reason: null
61
+ };
62
+ }
63
+ /** Section 30: exact required-file comparison rules. */
64
+ export function compareRequiredFileEvidence(stale, fullRefresh) {
65
+ if (stale.status === "unknown" || fullRefresh.status === "unknown") {
66
+ return "unknown";
67
+ }
68
+ if (stale.status === "missing" && fullRefresh.status === "present")
69
+ return "stale-worse";
70
+ if (stale.status === "present" && fullRefresh.status === "missing")
71
+ return "stale-better";
72
+ return "same";
73
+ }
74
+ /**
75
+ * Sections 31-35: the frozen precedence algorithm. Correctness and required-file evidence are the
76
+ * only two inputs; affected-neighborhood evidence and reindexRecommendation are never inputs here.
77
+ */
78
+ export function classifyStaleRisk(correctnessRelation, requiredFileEvidenceRelation) {
79
+ const reasonCodes = [];
80
+ // RULE 1 -- observed regression wins even if the other dimension is unknown.
81
+ if (correctnessRelation === "stale-worse" || requiredFileEvidenceRelation === "stale-worse") {
82
+ if (correctnessRelation === "stale-worse")
83
+ reasonCodes.push("stale-correctness-lower");
84
+ if (requiredFileEvidenceRelation === "stale-worse")
85
+ reasonCodes.push("stale-missing-required-file");
86
+ return { staleRiskClassification: "observed-stale-regression", reasonCodes: dedupe(reasonCodes) };
87
+ }
88
+ // RULE 2 -- inconclusive.
89
+ if (correctnessRelation === "unknown" || requiredFileEvidenceRelation === "unknown") {
90
+ if (correctnessRelation === "unknown")
91
+ reasonCodes.push("correctness-unavailable");
92
+ if (requiredFileEvidenceRelation === "unknown")
93
+ reasonCodes.push("required-file-evidence-unavailable");
94
+ return { staleRiskClassification: "inconclusive", reasonCodes: dedupe(reasonCodes) };
95
+ }
96
+ // RULE 3 -- no observed regression.
97
+ return { staleRiskClassification: "no-observed-stale-regression", reasonCodes: ["no-stale-specific-difference-observed"] };
98
+ }
99
+ function dedupe(values) {
100
+ return [...new Set(values)];
101
+ }