@dailephd/my-dev-kit-lab 0.5.0 → 0.5.1

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ All notable changes to my-dev-kit-lab are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.1] - 2026-09-22
8
+
9
+ Expanded warm-index benchmark suite.
10
+
11
+ - Added the dedicated warm-index benchmark corpus `benchmarks/contracts/warm-index-benchmark-cases.json` with 12 tasks: six for `task-workflow-medium-ts` and six for `task-analytics-large-mixed`. Each project has one localized task, four cross-module tasks, and one broad-change negative control.
12
+ - Added optional task-locality metadata (`localized`, `cross-module`, `broad-change`) to evaluation cases. Existing case files without it stay valid, invalid values are rejected, and the dedicated corpus requires it.
13
+ - Every corpus task has an answer key with expected facts, plus expected files and symbols that must exactly match the answer key and must exist in the benchmark project.
14
+ - `npm run verify:benchmarks` now validates the dedicated corpus, requires at least five tasks and every locality category for each warm-index benchmark project, and checks project-profile task statistics against the corpus. The medium and large/mixed profiles now record six tasks each (complexity scores 31 and 47 under the unchanged formula).
15
+ - Regression tests prove the existing one-index-per-project warm-index runtime at six tasks per project: two index builds for the 12 tasks, task ordinals 1–6, amortized and cumulative metrics, per-project reset, strict-prefix availability, and isolation of single-task and single-project failures.
16
+ - The existing warm-index report (schema `my-dev-kit-lab-warm-index-report-v1`) and the existing four plots cover the expanded suite: 2 projects, 12 tasks, and ordinals 1–6 per project.
17
+ - The packed-package acceptance gate now checks that the corpus is in the tarball, readable from a clean install, and selectable through the installed CLI's existing `--cases` option.
18
+ - No CLI command or flag, warm-index metric formula, report schema, or plot was added. The default cases file (`examples/token-savings-cases.json`) and the default my-dev-kit command are unchanged. Real-agent warm-index campaigns remain planned for v0.5.2.
19
+
7
20
  ## [0.5.0] - 2026-09-21
8
21
 
9
22
  Warm-index reuse experiment support.
package/README.md CHANGED
@@ -6,16 +6,16 @@ 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.5.0 (warm-index reuse experiment support). The previous release, v0.4.9, added semantic native-select option selection for declarative browser tutorials through the value-only `select-option` action backed by Playwright `Locator.selectOption({ value })`. 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).
9
+ The latest release is v0.5.1 (expanded warm-index benchmark suite). The previous release, v0.5.0, added warm-index reuse experiment support. 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).
10
10
 
11
- The current release includes v0.5.0 warm-index reuse. The package and installed CLI are version 0.5.0; v0.4.9 remains the previous release. See [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) for the validated release state.
11
+ The current release includes v0.5.1 expanded warm-index benchmarks. The package and installed CLI are version 0.5.1; v0.5.0 remains the previous release. v0.5.2 real-agent warm-index campaigns are planned next. See [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) for the validated release state.
12
12
 
13
13
  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.
14
14
 
15
15
  ## Current capabilities
16
16
 
17
17
  - **Run context-strategy experiments:** compare `raw-full-file` with `my-dev-kit-guided` using deterministic fixtures or locally configured Codex and Claude CLIs.
18
- - **Measure warm-index reuse (v0.5.0):** 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. 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. See [docs/WORKFLOWS.md](docs/WORKFLOWS.md#warm-index-reuse-experiment) and [docs/METRICS.md](docs/METRICS.md#warm-index-reuse-metrics).
18
+ - **Measure warm-index reuse (v0.5.1):** 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. See [docs/WORKFLOWS.md](docs/WORKFLOWS.md#warm-index-reuse-experiment) and [docs/METRICS.md](docs/METRICS.md#warm-index-reuse-metrics).
19
19
  - **Audit repository health:** run conservative code-rot detectors for TypeScript/JavaScript, Python, Java, and Kotlin, or adapt security findings into the common audit report.
20
20
  - **Validate CLI/package security:** inspect dependencies, package contents, path and subprocess boundaries, malformed inputs, optional static scanners, and bounded fuzz targets.
21
21
  - **Validate Android projects:** run nineteen static checks by default, with Gradle operations, external tools, and network access available only through explicit opt-in flags.
@@ -156,18 +156,18 @@ npm run experiment:run -- `
156
156
  --no-screenshot
157
157
  ```
158
158
 
159
- Run the warm-index reuse plugin deterministically with the fake my-dev-kit fixture (omit `--kit-command` to use the default `npx @dailephd/my-dev-kit@latest`):
159
+ Run the warm-index reuse plugin over the dedicated expanded benchmark corpus released in v0.5.1, deterministically with the fake my-dev-kit fixture (omit `--kit-command` to use the default `npx @dailephd/my-dev-kit@latest`):
160
160
 
161
161
  ```bash
162
162
  npm run experiment:run -- \
163
163
  --experiment warm-index-reuse \
164
- --cases tests/fixtures/warm-index-reuse/multi-task-cases.json \
164
+ --cases benchmarks/contracts/warm-index-benchmark-cases.json \
165
165
  --kit-command "node tests/fixtures/fake-my-dev-kit-cli.js" \
166
166
  --out lab-output/warm-index-reuse
167
167
  npm run generate-experiment-plots -- --experiment lab-output/warm-index-reuse --out lab-output/warm-index-plots
168
168
  ```
169
169
 
170
- The bundled `examples/token-savings-cases.json` currently has one task per benchmark project, so multi-task reuse needs a cases file with several tasks per project, such as the test fixture above; an expanded bundled warm-index benchmark suite is planned.
170
+ The default cases file, `examples/token-savings-cases.json`, is a small compatibility corpus with one task per benchmark project and remains the default when `--cases` is omitted. For multi-task reuse, select the dedicated corpus explicitly as above: it runs six tasks for each of the two benchmark projects against one index per project. Add `--benchmark-project task-workflow-medium-ts` or `--case <id>` to narrow the selection.
171
171
 
172
172
  When `--target` is omitted, the experiment runs in self mode against my-dev-kit-lab. When `--target <path>` is provided, the lab remains the tool root and the target project is inspected separately. Generated experiment outputs stay under lab-controlled output directories by default, not inside the target project.
173
173
 
@@ -220,8 +220,8 @@ See [docs/METRICS.md](docs/METRICS.md) for full metric definitions.
220
220
  - Claude does not expose token totals; token savings comparisons are unavailable for Claude runs
221
221
  - Codex may expose token totals but can produce timeouts or invalid-output runs
222
222
  - 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
223
- - The generic experiment-plugin framework has two plugins: `context-strategy-comparison` and the current-release v0.5.0 `warm-index-reuse`. Later plugins such as incremental-change and context-window scaling are future roadmap work
224
- - `warm-index-reuse` uses the deterministic fake agent only; selectable Codex/Claude warm-index campaigns remain planned for v0.5.2, the expanded benchmark suite remains planned for v0.5.1, and warm-index screenshots/gallery integration are not implemented. Its component-duration sums are not wall-clock latency, and it calculates no token-savings percentage, break-even task, winner, or ranking
223
+ - 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
224
+ - `warm-index-reuse` uses the deterministic fake agent only; selectable Codex/Claude warm-index campaigns remain planned for v0.5.2, and warm-index screenshots/gallery integration are not implemented. The v0.5.1 expanded corpus is deterministic fake-agent benchmark evidence, not real-agent evidence. Its component-duration sums are not wall-clock latency, and it calculates no token-savings percentage, break-even task, winner, or ranking
225
225
  - The current release does not guarantee token savings; it produces auditable evidence for specific cases, targets, agents, and strategies
226
226
  - Provider telemetry dashboards, semantic LLM judging, and cloud API billing integration are not yet implemented
227
227
  - The six new stage-context strategies have no CLI flags yet, are configured programmatically, and do not yet include plots, screenshots, or gallery integration
@@ -581,11 +581,11 @@
581
581
  "answer-key correctness scoring"
582
582
  ],
583
583
  "taskStats": {
584
- "taskCount": 1,
585
- "expectedRelevantFilesAverage": 3,
586
- "expectedRelevantSymbolsAverage": 4
584
+ "taskCount": 6,
585
+ "expectedRelevantFilesAverage": 3.83,
586
+ "expectedRelevantSymbolsAverage": 4.33
587
587
  },
588
- "complexityScore": 30,
588
+ "complexityScore": 31,
589
589
  "complexityMetrics": {
590
590
  "fileCount": 20,
591
591
  "sourceFileCount": 11,
@@ -597,9 +597,9 @@
597
597
  "dependencyFileCount": 1,
598
598
  "internalImportCount": 17,
599
599
  "exportedSymbolEstimate": 21,
600
- "taskCount": 1,
601
- "expectedRelevantFilesAverage": 3,
602
- "expectedRelevantSymbolsAverage": 4,
600
+ "taskCount": 6,
601
+ "expectedRelevantFilesAverage": 3.83,
602
+ "expectedRelevantSymbolsAverage": 4.33,
603
603
  "maxFileLines": 77,
604
604
  "averageFileLines": 26.31,
605
605
  "packageDependencyCount": 2,
@@ -839,11 +839,11 @@
839
839
  "answer-key correctness scoring"
840
840
  ],
841
841
  "taskStats": {
842
- "taskCount": 1,
843
- "expectedRelevantFilesAverage": 5,
844
- "expectedRelevantSymbolsAverage": 5
842
+ "taskCount": 6,
843
+ "expectedRelevantFilesAverage": 4.5,
844
+ "expectedRelevantSymbolsAverage": 4.67
845
845
  },
846
- "complexityScore": 48,
846
+ "complexityScore": 47,
847
847
  "complexityMetrics": {
848
848
  "fileCount": 35,
849
849
  "sourceFileCount": 22,
@@ -855,9 +855,9 @@
855
855
  "dependencyFileCount": 1,
856
856
  "internalImportCount": 28,
857
857
  "exportedSymbolEstimate": 24,
858
- "taskCount": 1,
859
- "expectedRelevantFilesAverage": 5,
860
- "expectedRelevantSymbolsAverage": 5,
858
+ "taskCount": 6,
859
+ "expectedRelevantFilesAverage": 4.5,
860
+ "expectedRelevantSymbolsAverage": 4.67,
861
861
  "maxFileLines": 49,
862
862
  "averageFileLines": 18.55,
863
863
  "packageDependencyCount": 2,
@@ -0,0 +1,764 @@
1
+ [
2
+ {
3
+ "id": "warm-medium-import-dedupe",
4
+ "title": "Warm-index task workflow import and dedupe retrieval",
5
+ "benchmarkProject": "task-workflow-medium-ts",
6
+ "projectProfileRef": "task-workflow-medium-ts",
7
+ "taskLocality": "cross-module",
8
+ "targetRoot": "benchmarks/projects/task-workflow-medium-ts",
9
+ "sourceRoots": ["src", "tests"],
10
+ "query": "import tasks duplicate normalized title project validation deterministic id",
11
+ "expectedFiles": [
12
+ "src/services/importTasks.ts",
13
+ "src/store/taskStore.ts",
14
+ "src/validation/taskValidation.ts"
15
+ ],
16
+ "expectedSymbols": ["importTasks", "validateImportInput", "findDuplicate", "createTask"],
17
+ "answerKey": {
18
+ "expectedFiles": [
19
+ "src/services/importTasks.ts",
20
+ "src/store/taskStore.ts",
21
+ "src/validation/taskValidation.ts"
22
+ ],
23
+ "expectedSymbols": ["importTasks", "validateImportInput", "findDuplicate", "createTask"],
24
+ "expectedFacts": [
25
+ {
26
+ "id": "warm-medium-import-validates-title-and-project",
27
+ "text": "importTasks validates normalized task titles and project ids before inserting records.",
28
+ "weight": 1,
29
+ "required": true
30
+ },
31
+ {
32
+ "id": "warm-medium-import-skips-duplicates-by-project-and-normalized-title",
33
+ "text": "importTasks skips duplicates when a task with the same normalized title already exists in the same project.",
34
+ "weight": 1,
35
+ "required": true
36
+ },
37
+ {
38
+ "id": "warm-medium-import-keeps-deterministic-task-sequences",
39
+ "text": "New imported tasks still receive deterministic ids such as task-1 and task-2 from the shared store sequence.",
40
+ "weight": 1,
41
+ "required": true
42
+ }
43
+ ],
44
+ "minimumCorrectFacts": 3,
45
+ "notes": "Cross-module retrieval spanning the import service, validation, and store logic."
46
+ },
47
+ "rawIncludeGlobs": ["src/**/*", "tests/**/*"],
48
+ "notes": "v0.5.1 warm-index seed case grounded in the existing task-workflow-import-dedup behavior."
49
+ },
50
+ {
51
+ "id": "warm-medium-create-project-task",
52
+ "title": "Warm-index create task with project metadata",
53
+ "benchmarkProject": "task-workflow-medium-ts",
54
+ "projectProfileRef": "task-workflow-medium-ts",
55
+ "taskLocality": "cross-module",
56
+ "targetRoot": "benchmarks/projects/task-workflow-medium-ts",
57
+ "sourceRoots": ["src", "tests"],
58
+ "query": "create task validate project title priority tags normalize tags deterministic id",
59
+ "expectedFiles": [
60
+ "src/services/createTask.ts",
61
+ "src/validation/taskValidation.ts",
62
+ "src/store/taskStore.ts",
63
+ "src/utils/deterministicId.ts"
64
+ ],
65
+ "expectedSymbols": [
66
+ "createTask",
67
+ "validateTaskTitle",
68
+ "validateProjectId",
69
+ "normalizeTags",
70
+ "TaskWorkflowStore",
71
+ "createDeterministicId"
72
+ ],
73
+ "answerKey": {
74
+ "expectedFiles": [
75
+ "src/services/createTask.ts",
76
+ "src/validation/taskValidation.ts",
77
+ "src/store/taskStore.ts",
78
+ "src/utils/deterministicId.ts"
79
+ ],
80
+ "expectedSymbols": [
81
+ "createTask",
82
+ "validateTaskTitle",
83
+ "validateProjectId",
84
+ "normalizeTags",
85
+ "TaskWorkflowStore",
86
+ "createDeterministicId"
87
+ ],
88
+ "expectedFacts": [
89
+ {
90
+ "id": "warm-medium-create-validates-title",
91
+ "text": "createTask validates the title with validateTaskTitle, which collapses whitespace and rejects an empty title with \"Task title must not be empty.\"",
92
+ "weight": 1,
93
+ "required": true
94
+ },
95
+ {
96
+ "id": "warm-medium-create-validates-project-before-storing",
97
+ "text": "createTask validates the projectId with validateProjectId and throws \"Unknown project id: <id>\" when the store has no such project, before the task is stored.",
98
+ "weight": 1,
99
+ "required": true
100
+ },
101
+ {
102
+ "id": "warm-medium-create-normalizes-tags",
103
+ "text": "Tags are normalized with normalizeTags (trimmed, lowercased, empty values dropped, de-duplicated, and sorted) before the task is stored.",
104
+ "weight": 1,
105
+ "required": true
106
+ },
107
+ {
108
+ "id": "warm-medium-create-uses-deterministic-sequence",
109
+ "text": "TaskWorkflowStore.createTask assigns the id with createDeterministicId(\"task\", sequence) from the store's shared incrementing sequence, producing ids such as task-1 and task-2.",
110
+ "weight": 1,
111
+ "required": true
112
+ },
113
+ {
114
+ "id": "warm-medium-create-starts-incomplete",
115
+ "text": "A newly created task is stored with completed set to false.",
116
+ "weight": 1,
117
+ "required": true
118
+ }
119
+ ],
120
+ "minimumCorrectFacts": 4,
121
+ "notes": "Cross-module: the create service composes validation, the store, and the deterministic id utility."
122
+ },
123
+ "rawIncludeGlobs": ["src/**/*", "tests/**/*"],
124
+ "notes": "v0.5.1 warm-index medium case for task creation."
125
+ },
126
+ {
127
+ "id": "warm-medium-complete-idempotent",
128
+ "title": "Warm-index idempotent task completion",
129
+ "benchmarkProject": "task-workflow-medium-ts",
130
+ "projectProfileRef": "task-workflow-medium-ts",
131
+ "taskLocality": "localized",
132
+ "targetRoot": "benchmarks/projects/task-workflow-medium-ts",
133
+ "sourceRoots": ["src", "tests"],
134
+ "query": "complete task by id idempotent completedAt unknown task",
135
+ "expectedFiles": ["src/services/completeTask.ts", "src/store/taskStore.ts"],
136
+ "expectedSymbols": ["completeTask", "TaskWorkflowStore"],
137
+ "answerKey": {
138
+ "expectedFiles": ["src/services/completeTask.ts", "src/store/taskStore.ts"],
139
+ "expectedSymbols": ["completeTask", "TaskWorkflowStore"],
140
+ "expectedFacts": [
141
+ {
142
+ "id": "warm-medium-complete-targets-task-by-id",
143
+ "text": "completeTask(store, taskId) changes one task by id through TaskWorkflowStore.updateTask.",
144
+ "weight": 1,
145
+ "required": true
146
+ },
147
+ {
148
+ "id": "warm-medium-complete-sets-completed-at",
149
+ "text": "The first completion sets completed to true and completedAt to \"2026-02-01T00:00:00.000Z\".",
150
+ "weight": 1,
151
+ "required": true
152
+ },
153
+ {
154
+ "id": "warm-medium-complete-is-idempotent",
155
+ "text": "When the task is already completed the updater returns it unchanged, so completing it again does not rewrite completedAt.",
156
+ "weight": 1,
157
+ "required": true
158
+ },
159
+ {
160
+ "id": "warm-medium-complete-unknown-task-throws",
161
+ "text": "An unknown task id throws \"Unknown task id: <id>\" from TaskWorkflowStore.updateTask.",
162
+ "weight": 1,
163
+ "required": true
164
+ }
165
+ ],
166
+ "minimumCorrectFacts": 3,
167
+ "notes": "Localized: the behavior lives in one service plus the store update primitive."
168
+ },
169
+ "rawIncludeGlobs": ["src/**/*", "tests/**/*"],
170
+ "notes": "v0.5.1 warm-index medium localized case."
171
+ },
172
+ {
173
+ "id": "warm-medium-composite-filter",
174
+ "title": "Warm-index composite task filtering",
175
+ "benchmarkProject": "task-workflow-medium-ts",
176
+ "projectProfileRef": "task-workflow-medium-ts",
177
+ "taskLocality": "cross-module",
178
+ "targetRoot": "benchmarks/projects/task-workflow-medium-ts",
179
+ "sourceRoots": ["src", "tests"],
180
+ "query": "filter tasks project completed priority tag text query ordering",
181
+ "expectedFiles": ["src/services/filterTasks.ts", "src/models/task.ts", "src/store/taskStore.ts"],
182
+ "expectedSymbols": ["filterTasks", "TaskFilter", "TaskWorkflowStore"],
183
+ "answerKey": {
184
+ "expectedFiles": ["src/services/filterTasks.ts", "src/models/task.ts", "src/store/taskStore.ts"],
185
+ "expectedSymbols": ["filterTasks", "TaskFilter", "TaskWorkflowStore"],
186
+ "expectedFacts": [
187
+ {
188
+ "id": "warm-medium-filter-combines-active-predicates",
189
+ "text": "filterTasks keeps a task only when it satisfies every supplied TaskFilter field; fields that are not supplied do not filter.",
190
+ "weight": 1,
191
+ "required": true
192
+ },
193
+ {
194
+ "id": "warm-medium-filter-project",
195
+ "text": "The projectId filter requires task.projectId to equal the requested project id.",
196
+ "weight": 1,
197
+ "required": true
198
+ },
199
+ {
200
+ "id": "warm-medium-filter-completed-priority-tag",
201
+ "text": "The completed filter compares the boolean when defined, priority must match exactly, and the tag is trimmed and lowercased before checking task.tags.",
202
+ "weight": 1,
203
+ "required": true
204
+ },
205
+ {
206
+ "id": "warm-medium-filter-text-query",
207
+ "text": "The query is trimmed and lowercased and matched as a substring of the lowercased title, notes, and tags joined with spaces.",
208
+ "weight": 1,
209
+ "required": true
210
+ },
211
+ {
212
+ "id": "warm-medium-filter-read-only-ordering",
213
+ "text": "filterTasks only reads store.listTasks(), which returns task copies sorted by task id, and returns the matches in that order without mutating the store.",
214
+ "weight": 1,
215
+ "required": true
216
+ }
217
+ ],
218
+ "minimumCorrectFacts": 4,
219
+ "notes": "Cross-module: filter semantics depend on the TaskFilter model and store listing order."
220
+ },
221
+ "rawIncludeGlobs": ["src/**/*", "tests/**/*"],
222
+ "notes": "v0.5.1 warm-index medium case for composite filtering."
223
+ },
224
+ {
225
+ "id": "warm-medium-project-summary",
226
+ "title": "Warm-index workflow project summary",
227
+ "benchmarkProject": "task-workflow-medium-ts",
228
+ "projectProfileRef": "task-workflow-medium-ts",
229
+ "taskLocality": "cross-module",
230
+ "targetRoot": "benchmarks/projects/task-workflow-medium-ts",
231
+ "sourceRoots": ["src", "tests"],
232
+ "query": "summarize tasks total open completed per project high priority open",
233
+ "expectedFiles": [
234
+ "src/services/summarizeTasks.ts",
235
+ "src/store/taskStore.ts",
236
+ "src/models/task.ts",
237
+ "src/models/project.ts"
238
+ ],
239
+ "expectedSymbols": ["summarizeTasks", "TaskSummary", "TaskWorkflowStore", "WorkflowProject"],
240
+ "answerKey": {
241
+ "expectedFiles": [
242
+ "src/services/summarizeTasks.ts",
243
+ "src/store/taskStore.ts",
244
+ "src/models/task.ts",
245
+ "src/models/project.ts"
246
+ ],
247
+ "expectedSymbols": ["summarizeTasks", "TaskSummary", "TaskWorkflowStore", "WorkflowProject"],
248
+ "expectedFacts": [
249
+ {
250
+ "id": "warm-medium-summary-total-open-completed",
251
+ "text": "summarizeTasks reports total, completed, and open counts across all tasks, where open equals total minus completed.",
252
+ "weight": 1,
253
+ "required": true
254
+ },
255
+ {
256
+ "id": "warm-medium-summary-by-project",
257
+ "text": "byProject holds total, completed, and open counts for every project returned by store.listProjects(), including zero counts for projects without tasks, plus any task projectId not registered as a project.",
258
+ "weight": 1,
259
+ "required": true
260
+ },
261
+ {
262
+ "id": "warm-medium-summary-high-priority-open",
263
+ "text": "highPriorityOpenTitles lists the titles of tasks that are not completed and have priority \"high\".",
264
+ "weight": 1,
265
+ "required": true
266
+ },
267
+ {
268
+ "id": "warm-medium-summary-read-only",
269
+ "text": "summarizeTasks reads copies from store.listTasks() and store.listProjects() and does not mutate stored tasks.",
270
+ "weight": 1,
271
+ "required": true
272
+ }
273
+ ],
274
+ "minimumCorrectFacts": 3,
275
+ "notes": "Cross-module: the summary combines the store with task and project models."
276
+ },
277
+ "rawIncludeGlobs": ["src/**/*", "tests/**/*"],
278
+ "notes": "v0.5.1 warm-index medium case for project summaries."
279
+ },
280
+ {
281
+ "id": "warm-medium-broad-workflow-map",
282
+ "title": "Warm-index broad task workflow architecture map",
283
+ "benchmarkProject": "task-workflow-medium-ts",
284
+ "projectProfileRef": "task-workflow-medium-ts",
285
+ "taskLocality": "broad-change",
286
+ "targetRoot": "benchmarks/projects/task-workflow-medium-ts",
287
+ "sourceRoots": ["src", "tests"],
288
+ "query": "explain task creation import duplicate filtering completion summary validation store workflow",
289
+ "expectedFiles": [
290
+ "src/services/createTask.ts",
291
+ "src/services/importTasks.ts",
292
+ "src/services/filterTasks.ts",
293
+ "src/services/completeTask.ts",
294
+ "src/services/summarizeTasks.ts",
295
+ "src/store/taskStore.ts",
296
+ "src/validation/taskValidation.ts"
297
+ ],
298
+ "expectedSymbols": [
299
+ "createTask",
300
+ "importTasks",
301
+ "filterTasks",
302
+ "completeTask",
303
+ "summarizeTasks",
304
+ "TaskWorkflowStore",
305
+ "validateImportInput"
306
+ ],
307
+ "answerKey": {
308
+ "expectedFiles": [
309
+ "src/services/createTask.ts",
310
+ "src/services/importTasks.ts",
311
+ "src/services/filterTasks.ts",
312
+ "src/services/completeTask.ts",
313
+ "src/services/summarizeTasks.ts",
314
+ "src/store/taskStore.ts",
315
+ "src/validation/taskValidation.ts"
316
+ ],
317
+ "expectedSymbols": [
318
+ "createTask",
319
+ "importTasks",
320
+ "filterTasks",
321
+ "completeTask",
322
+ "summarizeTasks",
323
+ "TaskWorkflowStore",
324
+ "validateImportInput"
325
+ ],
326
+ "expectedFacts": [
327
+ {
328
+ "id": "warm-medium-broad-shared-storage",
329
+ "text": "Both the createTask service and importTasks store new tasks through TaskWorkflowStore.createTask, sharing its deterministic task id sequence.",
330
+ "weight": 1,
331
+ "required": true
332
+ },
333
+ {
334
+ "id": "warm-medium-broad-import-adds-dedupe",
335
+ "text": "importTasks validates each item with validateImportInput and skips duplicates found by TaskWorkflowStore.findDuplicate (same project and normalized title), recording their ids in skippedDuplicates; plain task creation does not deduplicate.",
336
+ "weight": 1,
337
+ "required": true
338
+ },
339
+ {
340
+ "id": "warm-medium-broad-filter-read-only",
341
+ "text": "filterTasks reads store.listTasks() and returns matching tasks without changing workflow state.",
342
+ "weight": 1,
343
+ "required": true
344
+ },
345
+ {
346
+ "id": "warm-medium-broad-complete-targeted",
347
+ "text": "completeTask changes a single task by id through TaskWorkflowStore.updateTask.",
348
+ "weight": 1,
349
+ "required": true
350
+ },
351
+ {
352
+ "id": "warm-medium-broad-summary-aggregates",
353
+ "text": "summarizeTasks aggregates counts from the current tasks and projects held by the store.",
354
+ "weight": 1,
355
+ "required": true
356
+ }
357
+ ],
358
+ "forbiddenWrongClaims": ["importTasks calls the createTask service to insert imported tasks."],
359
+ "minimumCorrectFacts": 4,
360
+ "notes": "Deliberately wide-scope negative control spanning every workflow service; it is not expected to fit a small localized context."
361
+ },
362
+ "rawIncludeGlobs": ["src/**/*", "tests/**/*"],
363
+ "notes": "v0.5.1 warm-index medium broad-change negative control."
364
+ },
365
+ {
366
+ "id": "warm-large-health-label",
367
+ "title": "Warm-index task analytics health label retrieval",
368
+ "benchmarkProject": "task-analytics-large-mixed",
369
+ "projectProfileRef": "task-analytics-large-mixed",
370
+ "taskLocality": "cross-module",
371
+ "targetRoot": "benchmarks/projects/task-analytics-large-mixed",
372
+ "sourceRoots": ["ts/src", "ts/tests", "py/task_analytics", "py/tests"],
373
+ "query": "completion rate stale tasks quality label health report analytics snapshot",
374
+ "expectedFiles": [
375
+ "ts/src/services/buildAnalyticsSnapshot.ts",
376
+ "ts/src/reporting/formatTaskHealthReport.ts",
377
+ "py/task_analytics/metrics.py",
378
+ "py/task_analytics/quality.py",
379
+ "py/task_analytics/reporting.py"
380
+ ],
381
+ "expectedSymbols": [
382
+ "buildAnalyticsSnapshot",
383
+ "formatTaskHealthReport",
384
+ "calculate_project_metrics",
385
+ "determine_quality_label",
386
+ "build_health_report"
387
+ ],
388
+ "answerKey": {
389
+ "expectedFiles": [
390
+ "ts/src/services/buildAnalyticsSnapshot.ts",
391
+ "ts/src/reporting/formatTaskHealthReport.ts",
392
+ "py/task_analytics/metrics.py",
393
+ "py/task_analytics/quality.py",
394
+ "py/task_analytics/reporting.py"
395
+ ],
396
+ "expectedSymbols": [
397
+ "buildAnalyticsSnapshot",
398
+ "formatTaskHealthReport",
399
+ "calculate_project_metrics",
400
+ "determine_quality_label",
401
+ "build_health_report"
402
+ ],
403
+ "expectedFacts": [
404
+ {
405
+ "id": "warm-large-snapshot-computes-completion-rate",
406
+ "text": "The analytics snapshot computes completionRate as completedTasks divided by totalTasks times 100, rounded to two decimals.",
407
+ "weight": 1,
408
+ "required": true
409
+ },
410
+ {
411
+ "id": "warm-large-snapshot-counts-stale-open-tasks",
412
+ "text": "Stale tasks count only open tasks whose updated day is at least ten days behind the current day.",
413
+ "weight": 1,
414
+ "required": true
415
+ },
416
+ {
417
+ "id": "warm-large-python-quality-labels-healthy-watch-risk",
418
+ "text": "The Python quality layer maps metrics to healthy, watch, or risk labels based on completion rate and stale task thresholds.",
419
+ "weight": 1,
420
+ "required": true
421
+ },
422
+ {
423
+ "id": "warm-large-python-reporting-renders-quality-lines",
424
+ "text": "The Python report writer emits one line per project with the quality label plus completion, open, and stale counts.",
425
+ "weight": 1,
426
+ "required": true
427
+ }
428
+ ],
429
+ "minimumCorrectFacts": 3,
430
+ "notes": "Cross-module retrieval spanning TypeScript snapshot building and Python quality reporting."
431
+ },
432
+ "rawIncludeGlobs": ["ts/src/**/*", "ts/tests/**/*", "py/task_analytics/**/*", "py/tests/**/*"],
433
+ "notes": "v0.5.1 warm-index seed case grounded in the existing task-analytics-health-label behavior."
434
+ },
435
+ {
436
+ "id": "warm-large-ts-analytics-snapshot",
437
+ "title": "Warm-index TypeScript analytics snapshot",
438
+ "benchmarkProject": "task-analytics-large-mixed",
439
+ "projectProfileRef": "task-analytics-large-mixed",
440
+ "taskLocality": "cross-module",
441
+ "targetRoot": "benchmarks/projects/task-analytics-large-mixed",
442
+ "sourceRoots": ["ts/src", "ts/tests", "py/task_analytics", "py/tests"],
443
+ "query": "analytics snapshot project completion rate stale tasks average story points task store",
444
+ "expectedFiles": [
445
+ "ts/src/services/buildAnalyticsSnapshot.ts",
446
+ "ts/src/services/listTasksByProject.ts",
447
+ "ts/src/models/analyticsSnapshot.ts",
448
+ "ts/src/store/taskStore.ts"
449
+ ],
450
+ "expectedSymbols": ["buildAnalyticsSnapshot", "listTasksByProject", "ProjectSnapshot", "AnalyticsTaskStore"],
451
+ "answerKey": {
452
+ "expectedFiles": [
453
+ "ts/src/services/buildAnalyticsSnapshot.ts",
454
+ "ts/src/services/listTasksByProject.ts",
455
+ "ts/src/models/analyticsSnapshot.ts",
456
+ "ts/src/store/taskStore.ts"
457
+ ],
458
+ "expectedSymbols": ["buildAnalyticsSnapshot", "listTasksByProject", "ProjectSnapshot", "AnalyticsTaskStore"],
459
+ "expectedFacts": [
460
+ {
461
+ "id": "warm-large-snapshot-per-project-counts",
462
+ "text": "For each project from ProjectStore.list(), buildAnalyticsSnapshot loads the project's tasks with listTasksByProject (AnalyticsTaskStore.list() filtered by projectId) and records totalTasks, completedTasks, and openTasks as totalTasks minus completedTasks.",
463
+ "weight": 1,
464
+ "required": true
465
+ },
466
+ {
467
+ "id": "warm-large-snapshot-completion-rate-formula",
468
+ "text": "completionRate is completedTasks divided by totalTasks times 100, rounded to two decimals with Math.round(value * 100) / 100.",
469
+ "weight": 1,
470
+ "required": true
471
+ },
472
+ {
473
+ "id": "warm-large-snapshot-stale-condition",
474
+ "text": "staleTasks counts only incomplete tasks whose currentDay minus updatedDay is at least STALE_DAY_THRESHOLD (10).",
475
+ "weight": 1,
476
+ "required": true
477
+ },
478
+ {
479
+ "id": "warm-large-snapshot-empty-project",
480
+ "text": "A project with no tasks gets zero counts, completionRate 0, and averageStoryPoints 0; otherwise averageStoryPoints is the mean story points rounded to two decimals.",
481
+ "weight": 1,
482
+ "required": true
483
+ },
484
+ {
485
+ "id": "warm-large-snapshot-totals-aggregate",
486
+ "text": "The snapshot totals sum totalTasks, completedTasks, openTasks, and staleTasks across the per-project snapshots.",
487
+ "weight": 1,
488
+ "required": true
489
+ }
490
+ ],
491
+ "minimumCorrectFacts": 4,
492
+ "notes": "Cross-module: the snapshot service composes the task-listing service, the task store, and the snapshot model."
493
+ },
494
+ "rawIncludeGlobs": ["ts/src/**/*", "ts/tests/**/*", "py/task_analytics/**/*", "py/tests/**/*"],
495
+ "notes": "v0.5.1 warm-index large case for the TypeScript analytics snapshot."
496
+ },
497
+ {
498
+ "id": "warm-large-ts-leaderboard",
499
+ "title": "Warm-index deterministic project leaderboard",
500
+ "benchmarkProject": "task-analytics-large-mixed",
501
+ "projectProfileRef": "task-analytics-large-mixed",
502
+ "taskLocality": "localized",
503
+ "targetRoot": "benchmarks/projects/task-analytics-large-mixed",
504
+ "sourceRoots": ["ts/src", "ts/tests", "py/task_analytics", "py/tests"],
505
+ "query": "project leaderboard completion rate stale tasks tie break project id ranking",
506
+ "expectedFiles": ["ts/src/reporting/buildProjectLeaderboard.ts", "ts/src/models/analyticsSnapshot.ts"],
507
+ "expectedSymbols": ["buildProjectLeaderboard", "ProjectSnapshot"],
508
+ "answerKey": {
509
+ "expectedFiles": ["ts/src/reporting/buildProjectLeaderboard.ts", "ts/src/models/analyticsSnapshot.ts"],
510
+ "expectedSymbols": ["buildProjectLeaderboard", "ProjectSnapshot"],
511
+ "expectedFacts": [
512
+ {
513
+ "id": "warm-large-leaderboard-completion-rate-descending",
514
+ "text": "buildProjectLeaderboard sorts a copy of snapshot.projects by completionRate in descending order.",
515
+ "weight": 1,
516
+ "required": true
517
+ },
518
+ {
519
+ "id": "warm-large-leaderboard-stale-ascending",
520
+ "text": "Ties on completionRate are broken by staleTasks in ascending order.",
521
+ "weight": 1,
522
+ "required": true
523
+ },
524
+ {
525
+ "id": "warm-large-leaderboard-project-id-final",
526
+ "text": "Remaining ties are broken by projectId in ascending localeCompare order.",
527
+ "weight": 1,
528
+ "required": true
529
+ },
530
+ {
531
+ "id": "warm-large-leaderboard-one-based-ranks",
532
+ "text": "Each line is formatted as \"<rank>. <projectId> (<completionRate>% complete, <staleTasks> stale)\" with ranks starting at 1.",
533
+ "weight": 1,
534
+ "required": true
535
+ }
536
+ ],
537
+ "minimumCorrectFacts": 3,
538
+ "notes": "Localized: the ranking rule lives in one reporting function over the ProjectSnapshot model."
539
+ },
540
+ "rawIncludeGlobs": ["ts/src/**/*", "ts/tests/**/*", "py/task_analytics/**/*", "py/tests/**/*"],
541
+ "notes": "v0.5.1 warm-index large localized case."
542
+ },
543
+ {
544
+ "id": "warm-large-python-parser-metrics",
545
+ "title": "Warm-index Python parser and project metrics",
546
+ "benchmarkProject": "task-analytics-large-mixed",
547
+ "projectProfileRef": "task-analytics-large-mixed",
548
+ "taskLocality": "cross-module",
549
+ "targetRoot": "benchmarks/projects/task-analytics-large-mixed",
550
+ "sourceRoots": ["ts/src", "ts/tests", "py/task_analytics", "py/tests"],
551
+ "query": "parse task rows calculate project metrics completion open stale story points",
552
+ "expectedFiles": ["py/task_analytics/parser.py", "py/task_analytics/models.py", "py/task_analytics/metrics.py"],
553
+ "expectedSymbols": ["parse_task_rows", "TaskRecord", "calculate_project_metrics", "ProjectMetrics"],
554
+ "answerKey": {
555
+ "expectedFiles": ["py/task_analytics/parser.py", "py/task_analytics/models.py", "py/task_analytics/metrics.py"],
556
+ "expectedSymbols": ["parse_task_rows", "TaskRecord", "calculate_project_metrics", "ProjectMetrics"],
557
+ "expectedFacts": [
558
+ {
559
+ "id": "warm-large-python-parser-task-record",
560
+ "text": "parse_task_rows converts each row into a TaskRecord with task_id and project_id as str, completed as bool, and story_points and updated_day as int.",
561
+ "weight": 1,
562
+ "required": true
563
+ },
564
+ {
565
+ "id": "warm-large-python-metrics-grouped-by-project",
566
+ "text": "calculate_project_metrics groups tasks by project_id and returns one ProjectMetrics per project in sorted project_id order.",
567
+ "weight": 1,
568
+ "required": true
569
+ },
570
+ {
571
+ "id": "warm-large-python-metrics-counts",
572
+ "text": "open_tasks is total_tasks minus completed_tasks, and stale_tasks counts incomplete tasks whose current_day minus updated_day is at least STALE_DAY_THRESHOLD (10).",
573
+ "weight": 1,
574
+ "required": true
575
+ },
576
+ {
577
+ "id": "warm-large-python-metrics-completion-rate",
578
+ "text": "completion_rate is round((completed / total) * 100, 2), or 0.0 when a project has no tasks.",
579
+ "weight": 1,
580
+ "required": true
581
+ },
582
+ {
583
+ "id": "warm-large-python-metrics-average-story-points",
584
+ "text": "average_story_points is the mean story_points rounded with round(..., 2), or 0.0 when a project has no tasks.",
585
+ "weight": 1,
586
+ "required": true
587
+ }
588
+ ],
589
+ "minimumCorrectFacts": 4,
590
+ "notes": "Cross-module: parsing, the record models, and metric calculation live in separate Python modules."
591
+ },
592
+ "rawIncludeGlobs": ["ts/src/**/*", "ts/tests/**/*", "py/task_analytics/**/*", "py/tests/**/*"],
593
+ "notes": "v0.5.1 warm-index large case for Python parsing and metrics."
594
+ },
595
+ {
596
+ "id": "warm-large-python-pipeline",
597
+ "title": "Warm-index Python analytics report pipeline",
598
+ "benchmarkProject": "task-analytics-large-mixed",
599
+ "projectProfileRef": "task-analytics-large-mixed",
600
+ "taskLocality": "cross-module",
601
+ "targetRoot": "benchmarks/projects/task-analytics-large-mixed",
602
+ "sourceRoots": ["ts/src", "ts/tests", "py/task_analytics", "py/tests"],
603
+ "query": "pipeline parse rows project metrics quality label health report",
604
+ "expectedFiles": [
605
+ "py/task_analytics/pipeline.py",
606
+ "py/task_analytics/parser.py",
607
+ "py/task_analytics/metrics.py",
608
+ "py/task_analytics/quality.py",
609
+ "py/task_analytics/reporting.py"
610
+ ],
611
+ "expectedSymbols": [
612
+ "build_report_from_rows",
613
+ "parse_task_rows",
614
+ "calculate_project_metrics",
615
+ "determine_quality_label",
616
+ "build_health_report"
617
+ ],
618
+ "answerKey": {
619
+ "expectedFiles": [
620
+ "py/task_analytics/pipeline.py",
621
+ "py/task_analytics/parser.py",
622
+ "py/task_analytics/metrics.py",
623
+ "py/task_analytics/quality.py",
624
+ "py/task_analytics/reporting.py"
625
+ ],
626
+ "expectedSymbols": [
627
+ "build_report_from_rows",
628
+ "parse_task_rows",
629
+ "calculate_project_metrics",
630
+ "determine_quality_label",
631
+ "build_health_report"
632
+ ],
633
+ "expectedFacts": [
634
+ {
635
+ "id": "warm-large-pipeline-parses-first",
636
+ "text": "build_report_from_rows(rows, current_day) first parses the rows with parse_task_rows.",
637
+ "weight": 1,
638
+ "required": true
639
+ },
640
+ {
641
+ "id": "warm-large-pipeline-metrics-before-report",
642
+ "text": "The parsed tasks go to calculate_project_metrics(tasks, current_day), and the resulting metrics go to build_health_report.",
643
+ "weight": 1,
644
+ "required": true
645
+ },
646
+ {
647
+ "id": "warm-large-pipeline-quality-label-in-report",
648
+ "text": "build_health_report calls determine_quality_label for each metric, which returns empty, healthy, watch, or risk from total_tasks, completion_rate, and stale_tasks thresholds.",
649
+ "weight": 1,
650
+ "required": true
651
+ },
652
+ {
653
+ "id": "warm-large-pipeline-deterministic-output",
654
+ "text": "The report is one line per project, \"<project_id>: <label> | completion=<rate>% | open=<n> | stale=<n>\", joined by newlines in sorted project_id order, so fixed rows and current_day produce the same output.",
655
+ "weight": 1,
656
+ "required": true
657
+ },
658
+ {
659
+ "id": "warm-large-pipeline-composes-owners",
660
+ "text": "pipeline.py only composes parse_task_rows, calculate_project_metrics, and build_health_report; quality rules stay in quality.py and are reached through reporting.",
661
+ "weight": 1,
662
+ "required": true
663
+ }
664
+ ],
665
+ "minimumCorrectFacts": 4,
666
+ "notes": "Cross-module: the pipeline composes parser, metrics, reporting, and (through reporting) quality modules."
667
+ },
668
+ "rawIncludeGlobs": ["ts/src/**/*", "ts/tests/**/*", "py/task_analytics/**/*", "py/tests/**/*"],
669
+ "notes": "v0.5.1 warm-index large case for the Python report pipeline."
670
+ },
671
+ {
672
+ "id": "warm-large-broad-analytics-comparison",
673
+ "title": "Warm-index broad TypeScript and Python analytics comparison",
674
+ "benchmarkProject": "task-analytics-large-mixed",
675
+ "projectProfileRef": "task-analytics-large-mixed",
676
+ "taskLocality": "broad-change",
677
+ "targetRoot": "benchmarks/projects/task-analytics-large-mixed",
678
+ "sourceRoots": ["ts/src", "ts/tests", "py/task_analytics", "py/tests"],
679
+ "query": "compare TypeScript and Python project analytics snapshot metrics quality reporting pipeline leaderboard",
680
+ "expectedFiles": [
681
+ "ts/src/services/buildAnalyticsSnapshot.ts",
682
+ "ts/src/reporting/formatTaskHealthReport.ts",
683
+ "ts/src/reporting/buildProjectLeaderboard.ts",
684
+ "py/task_analytics/parser.py",
685
+ "py/task_analytics/metrics.py",
686
+ "py/task_analytics/quality.py",
687
+ "py/task_analytics/reporting.py",
688
+ "py/task_analytics/pipeline.py"
689
+ ],
690
+ "expectedSymbols": [
691
+ "buildAnalyticsSnapshot",
692
+ "formatTaskHealthReport",
693
+ "buildProjectLeaderboard",
694
+ "parse_task_rows",
695
+ "calculate_project_metrics",
696
+ "determine_quality_label",
697
+ "build_health_report",
698
+ "build_report_from_rows"
699
+ ],
700
+ "answerKey": {
701
+ "expectedFiles": [
702
+ "ts/src/services/buildAnalyticsSnapshot.ts",
703
+ "ts/src/reporting/formatTaskHealthReport.ts",
704
+ "ts/src/reporting/buildProjectLeaderboard.ts",
705
+ "py/task_analytics/parser.py",
706
+ "py/task_analytics/metrics.py",
707
+ "py/task_analytics/quality.py",
708
+ "py/task_analytics/reporting.py",
709
+ "py/task_analytics/pipeline.py"
710
+ ],
711
+ "expectedSymbols": [
712
+ "buildAnalyticsSnapshot",
713
+ "formatTaskHealthReport",
714
+ "buildProjectLeaderboard",
715
+ "parse_task_rows",
716
+ "calculate_project_metrics",
717
+ "determine_quality_label",
718
+ "build_health_report",
719
+ "build_report_from_rows"
720
+ ],
721
+ "expectedFacts": [
722
+ {
723
+ "id": "warm-large-broad-typescript-side",
724
+ "text": "The TypeScript side builds analytics snapshots with buildAnalyticsSnapshot and presents them through formatTaskHealthReport, which uses buildProjectLeaderboard for ranking.",
725
+ "weight": 1,
726
+ "required": true
727
+ },
728
+ {
729
+ "id": "warm-large-broad-python-side",
730
+ "text": "The Python side parses rows (parse_task_rows), derives project metrics (calculate_project_metrics), assigns quality labels (determine_quality_label), and builds reports (build_health_report), composed by build_report_from_rows.",
731
+ "weight": 1,
732
+ "required": true
733
+ },
734
+ {
735
+ "id": "warm-large-broad-comparable-evidence",
736
+ "text": "Both sides expose comparable per-project health evidence: total, completed, open, and stale task counts plus a completion rate rounded to two decimals, using a stale threshold of 10 days.",
737
+ "weight": 1,
738
+ "required": true
739
+ },
740
+ {
741
+ "id": "warm-large-broad-independent-implementations",
742
+ "text": "The TypeScript and Python code are independent parallel benchmark implementations, not a runtime TypeScript-to-Python call chain.",
743
+ "weight": 1,
744
+ "required": true
745
+ },
746
+ {
747
+ "id": "warm-large-broad-no-cross-invocation",
748
+ "text": "Neither implementation imports, spawns, or calls the other; TypeScript imports stay within ts/src and Python imports stay within the task_analytics package.",
749
+ "weight": 1,
750
+ "required": true
751
+ }
752
+ ],
753
+ "forbiddenWrongClaims": [
754
+ "The TypeScript implementation invokes the Python analytics pipeline.",
755
+ "Python is a runtime backend for the TypeScript analytics implementation.",
756
+ "The TypeScript or Python implementation directly imports or calls the other."
757
+ ],
758
+ "minimumCorrectFacts": 4,
759
+ "notes": "Deliberately wide-scope negative control spanning both language implementations; it is not expected to fit a small localized context."
760
+ },
761
+ "rawIncludeGlobs": ["ts/src/**/*", "ts/tests/**/*", "py/task_analytics/**/*", "py/tests/**/*"],
762
+ "notes": "v0.5.1 warm-index large broad-change negative control."
763
+ }
764
+ ]
@@ -1,7 +1,8 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { PROJECT_COMPLEXITY_FORMULA, calculateProjectComplexityScore } from "./projectComplexity.js";
4
+ import { PROJECT_COMPLEXITY_FORMULA, calculateProjectComplexityScore, roundToTwo } from "./projectComplexity.js";
5
+ import { TASK_LOCALITIES } from "./types.js";
5
6
  export const REQUIRED_BENCHMARK_PROJECT_IDS = [
6
7
  "todo-ts",
7
8
  "todo-python",
@@ -11,6 +12,8 @@ export const REQUIRED_BENCHMARK_PROJECT_IDS = [
11
12
  "task-analytics-large-mixed"
12
13
  ];
13
14
  export const VALID_COMPLEXITY_LEVELS = new Set(["small", "medium", "large", "mixed-language"]);
15
+ export const REQUIRED_WARM_INDEX_BENCHMARK_PROJECT_IDS = ["task-workflow-medium-ts", "task-analytics-large-mixed"];
16
+ export const MIN_WARM_INDEX_TASKS_PER_PROJECT = 5;
14
17
  export async function readBenchmarkProjectProfiles(profilesPath, repoRoot = process.cwd()) {
15
18
  let parsed;
16
19
  try {
@@ -95,6 +98,193 @@ export function validateAnswerKey(answerKey, label) {
95
98
  }
96
99
  return errors;
97
100
  }
101
+ export function validateTaskLocality(value, label) {
102
+ if (typeof value === "string" && TASK_LOCALITIES.includes(value)) {
103
+ return [];
104
+ }
105
+ return [`${label}: taskLocality must be one of ${TASK_LOCALITIES.join(", ")} (received ${JSON.stringify(value)}).`];
106
+ }
107
+ export function deriveWarmIndexTaskStats(cases, projectId) {
108
+ const projectCases = cases.filter((benchmarkCase) => benchmarkCase?.benchmarkProject === projectId);
109
+ if (projectCases.length === 0) {
110
+ return { taskCount: 0, expectedRelevantFilesAverage: 0, expectedRelevantSymbolsAverage: 0 };
111
+ }
112
+ const fileCounts = projectCases.map((benchmarkCase) => (Array.isArray(benchmarkCase.expectedFiles) ? benchmarkCase.expectedFiles.length : 0));
113
+ const symbolCounts = projectCases.map((benchmarkCase) => Array.isArray(benchmarkCase.expectedSymbols) ? benchmarkCase.expectedSymbols.length : 0);
114
+ return {
115
+ taskCount: projectCases.length,
116
+ expectedRelevantFilesAverage: roundToTwo(fileCounts.reduce((total, count) => total + count, 0) / projectCases.length),
117
+ expectedRelevantSymbolsAverage: roundToTwo(symbolCounts.reduce((total, count) => total + count, 0) / projectCases.length)
118
+ };
119
+ }
120
+ export function validateWarmIndexBenchmarkCases(cases, profiles, repoRoot = process.cwd()) {
121
+ const errors = [];
122
+ const profilesById = new Map(profiles.map((profile) => [profile.projectId, profile]));
123
+ const ids = new Set();
124
+ const sourceRootsByProject = new Map();
125
+ cases.forEach((benchmarkCase, index) => {
126
+ if (!benchmarkCase || typeof benchmarkCase !== "object") {
127
+ errors.push(`warm-index case at index ${index}: must be an object.`);
128
+ return;
129
+ }
130
+ const id = typeof benchmarkCase.id === "string" && benchmarkCase.id.length > 0 ? benchmarkCase.id : undefined;
131
+ const label = id === undefined ? `warm-index case at index ${index}` : `warm-index case ${id}`;
132
+ if (id === undefined) {
133
+ errors.push(`${label}: id must be a nonempty string.`);
134
+ }
135
+ else if (ids.has(id)) {
136
+ errors.push(`${label}: duplicate case id.`);
137
+ }
138
+ else {
139
+ ids.add(id);
140
+ }
141
+ const projectId = typeof benchmarkCase.benchmarkProject === "string" && benchmarkCase.benchmarkProject.length > 0
142
+ ? benchmarkCase.benchmarkProject
143
+ : undefined;
144
+ if (projectId === undefined) {
145
+ errors.push(`${label}: benchmarkProject must be a nonempty string.`);
146
+ }
147
+ if (typeof benchmarkCase.projectProfileRef !== "string" || benchmarkCase.projectProfileRef.length === 0) {
148
+ errors.push(`${label}: missing projectProfileRef.`);
149
+ }
150
+ else if (benchmarkCase.projectProfileRef !== projectId) {
151
+ errors.push(`${label}: projectProfileRef ${benchmarkCase.projectProfileRef} must equal benchmarkProject ${projectId ?? "<missing>"}.`);
152
+ }
153
+ const profile = projectId === undefined ? undefined : profilesById.get(projectId);
154
+ if (projectId !== undefined && profile === undefined) {
155
+ errors.push(`${label}: unknown benchmark project profile ${projectId}.`);
156
+ }
157
+ if (benchmarkCase.taskLocality === undefined) {
158
+ errors.push(`${label}: missing taskLocality; expected one of ${TASK_LOCALITIES.join(", ")}.`);
159
+ }
160
+ else {
161
+ errors.push(...validateTaskLocality(benchmarkCase.taskLocality, label));
162
+ }
163
+ const targetRoot = typeof benchmarkCase.targetRoot === "string" && benchmarkCase.targetRoot.length > 0 ? benchmarkCase.targetRoot : undefined;
164
+ if (targetRoot === undefined) {
165
+ errors.push(`${label}: targetRoot must be a nonempty string.`);
166
+ }
167
+ else if (profile !== undefined && normalizeRelativeRoot(targetRoot) !== normalizeRelativeRoot(profile.rootPath ?? "")) {
168
+ errors.push(`${label}: targetRoot ${targetRoot} does not match profile ${profile.projectId} rootPath ${profile.rootPath}.`);
169
+ }
170
+ if (!Array.isArray(benchmarkCase.sourceRoots) || benchmarkCase.sourceRoots.length === 0) {
171
+ errors.push(`${label}: sourceRoots must be a nonempty array.`);
172
+ }
173
+ else {
174
+ if (profile !== undefined) {
175
+ errors.push(...validateWarmIndexSourceRoots(benchmarkCase.sourceRoots, profile, label));
176
+ }
177
+ if (projectId !== undefined) {
178
+ const reference = sourceRootsByProject.get(projectId);
179
+ if (reference === undefined) {
180
+ sourceRootsByProject.set(projectId, { caseLabel: label, sourceRoots: benchmarkCase.sourceRoots });
181
+ }
182
+ else if (!sameOrderedValues(reference.sourceRoots, benchmarkCase.sourceRoots)) {
183
+ errors.push(`${label}: sourceRoots ${JSON.stringify(benchmarkCase.sourceRoots)} must match the ordered sourceRoots ${JSON.stringify(reference.sourceRoots)} of ${reference.caseLabel} for project ${projectId}.`);
184
+ }
185
+ }
186
+ }
187
+ const expectedFiles = Array.isArray(benchmarkCase.expectedFiles) ? benchmarkCase.expectedFiles : undefined;
188
+ const expectedSymbols = Array.isArray(benchmarkCase.expectedSymbols) ? benchmarkCase.expectedSymbols : undefined;
189
+ if (expectedFiles === undefined || expectedFiles.length === 0) {
190
+ errors.push(`${label}: expectedFiles must be a nonempty array.`);
191
+ }
192
+ if (expectedSymbols === undefined || expectedSymbols.length === 0) {
193
+ errors.push(`${label}: expectedSymbols must be a nonempty array.`);
194
+ }
195
+ if (benchmarkCase.answerKey === undefined) {
196
+ errors.push(`${label}: missing answerKey.`);
197
+ }
198
+ else {
199
+ errors.push(...validateAnswerKey(benchmarkCase.answerKey, label));
200
+ const answerKey = benchmarkCase.answerKey;
201
+ if (answerKey && typeof answerKey === "object") {
202
+ if (expectedFiles !== undefined && Array.isArray(answerKey.expectedFiles) && !sameOrderedValues(expectedFiles, answerKey.expectedFiles)) {
203
+ errors.push(`${label}: expectedFiles must exactly match answerKey.expectedFiles (same values in the same order).`);
204
+ }
205
+ if (expectedSymbols !== undefined &&
206
+ Array.isArray(answerKey.expectedSymbols) &&
207
+ !sameOrderedValues(expectedSymbols, answerKey.expectedSymbols)) {
208
+ errors.push(`${label}: expectedSymbols must exactly match answerKey.expectedSymbols (same values in the same order).`);
209
+ }
210
+ }
211
+ }
212
+ if (expectedFiles !== undefined && targetRoot !== undefined) {
213
+ for (const expectedFile of expectedFiles) {
214
+ if (typeof expectedFile !== "string" || expectedFile.length === 0 || path.isAbsolute(expectedFile) || expectedFile.includes("..")) {
215
+ errors.push(`${label}: expected file must be a safe relative path: ${String(expectedFile)}.`);
216
+ }
217
+ else if (!existsSync(path.resolve(repoRoot, targetRoot, expectedFile))) {
218
+ errors.push(`${label}: expected file does not exist in ${projectId ?? targetRoot}: ${expectedFile}.`);
219
+ }
220
+ }
221
+ }
222
+ });
223
+ const representedProjectIds = [
224
+ ...new Set(cases
225
+ .map((benchmarkCase) => benchmarkCase?.benchmarkProject)
226
+ .filter((projectId) => typeof projectId === "string" && projectId.length > 0))
227
+ ].sort();
228
+ for (const projectId of representedProjectIds) {
229
+ const profile = profilesById.get(projectId);
230
+ if (profile === undefined || !profile.complexityMetrics || typeof profile.complexityMetrics !== "object") {
231
+ continue;
232
+ }
233
+ const derived = deriveWarmIndexTaskStats(cases, projectId);
234
+ for (const field of ["taskCount", "expectedRelevantFilesAverage", "expectedRelevantSymbolsAverage"]) {
235
+ if (profile.complexityMetrics[field] !== derived[field]) {
236
+ errors.push(`profile ${projectId}: complexityMetrics.${field} ${profile.complexityMetrics[field]} does not match warm-index corpus value ${derived[field]}.`);
237
+ }
238
+ }
239
+ }
240
+ return errors;
241
+ }
242
+ export function validateWarmIndexBenchmarkSuiteCoverage(cases) {
243
+ const errors = [];
244
+ for (const projectId of REQUIRED_WARM_INDEX_BENCHMARK_PROJECT_IDS) {
245
+ const projectCases = cases.filter((benchmarkCase) => benchmarkCase?.benchmarkProject === projectId);
246
+ if (projectCases.length === 0) {
247
+ errors.push(`warm-index suite: missing required benchmark project ${projectId}.`);
248
+ continue;
249
+ }
250
+ if (projectCases.length < MIN_WARM_INDEX_TASKS_PER_PROJECT) {
251
+ errors.push(`warm-index suite: project ${projectId} has ${projectCases.length} cases; at least ${MIN_WARM_INDEX_TASKS_PER_PROJECT} are required.`);
252
+ }
253
+ for (const locality of TASK_LOCALITIES) {
254
+ if (!projectCases.some((benchmarkCase) => benchmarkCase.taskLocality === locality)) {
255
+ errors.push(`warm-index suite: project ${projectId} has no ${locality} case.`);
256
+ }
257
+ }
258
+ }
259
+ return errors;
260
+ }
261
+ function validateWarmIndexSourceRoots(sourceRoots, profile, label) {
262
+ const errors = [];
263
+ const allowedRoots = [...(profile.sourceRoots ?? []), ...(profile.testRoots ?? [])];
264
+ const seen = new Set();
265
+ for (const sourceRoot of sourceRoots) {
266
+ if (seen.has(sourceRoot)) {
267
+ errors.push(`${label}: duplicate source root ${JSON.stringify(sourceRoot)}.`);
268
+ continue;
269
+ }
270
+ seen.add(sourceRoot);
271
+ if (typeof sourceRoot !== "string" || !allowedRoots.includes(sourceRoot)) {
272
+ errors.push(`${label}: unknown source root ${JSON.stringify(sourceRoot)} for profile ${profile.projectId}; allowed roots: ${allowedRoots.join(", ")}.`);
273
+ }
274
+ }
275
+ for (const allowedRoot of allowedRoots) {
276
+ if (!seen.has(allowedRoot)) {
277
+ errors.push(`${label}: missing profile source/test root ${allowedRoot} for profile ${profile.projectId}.`);
278
+ }
279
+ }
280
+ return errors;
281
+ }
282
+ function normalizeRelativeRoot(value) {
283
+ return path.posix.normalize(value.replace(/\\/g, "/")).replace(/\/+$/, "");
284
+ }
285
+ function sameOrderedValues(left, right) {
286
+ return left.length === right.length && left.every((value, index) => value === right[index]);
287
+ }
98
288
  export function validateBenchmarkProjectProfiles(profiles, repoRoot = process.cwd()) {
99
289
  const errors = [];
100
290
  const ids = new Set(profiles.map((profile) => profile.projectId));
@@ -73,7 +73,7 @@ export function computeProjectComplexityMetrics(projectRoot, fileTree, taskStats
73
73
  function normalize(value, cap) {
74
74
  return Math.min(value / cap, 1);
75
75
  }
76
- function roundToTwo(value) {
76
+ export function roundToTwo(value) {
77
77
  return Math.round(value * 100) / 100;
78
78
  }
79
79
  function sum(values) {
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { resolveWithinRoot } from "../core/pathSafety.js";
4
- import { validateAnswerKey } from "./benchmarkMetadata.js";
4
+ import { validateAnswerKey, validateTaskLocality } from "./benchmarkMetadata.js";
5
5
  export async function readEvaluationCases(casesPath, repoRoot = process.cwd(), options = {}) {
6
6
  let parsed;
7
7
  try {
@@ -36,6 +36,12 @@ export async function readEvaluationCases(casesPath, repoRoot = process.cwd(), o
36
36
  throw new Error(`Duplicate evaluation case id: ${input.id}`);
37
37
  }
38
38
  ids.add(input.id);
39
+ if (candidate.taskLocality !== undefined) {
40
+ const localityErrors = validateTaskLocality(candidate.taskLocality, `evaluation case ${input.id}`);
41
+ if (localityErrors.length > 0) {
42
+ throw new Error(localityErrors.join("\n"));
43
+ }
44
+ }
39
45
  if (candidate.answerKey !== undefined) {
40
46
  const answerKeyErrors = validateAnswerKey(candidate.answerKey, `evaluation case ${input.id}`);
41
47
  if (answerKeyErrors.length > 0) {
@@ -1 +1 @@
1
- export {};
1
+ export const TASK_LOCALITIES = ["localized", "cross-module", "broad-change"];
package/docs/METRICS.md CHANGED
@@ -137,6 +137,15 @@ Each entry below uses the same fields: **Meaning**, **Appears in** (the artifact
137
137
  Interpretation: human-readable size category.
138
138
  Caveat: coarse label; use the score and metrics for detail.
139
139
 
140
+ In the released v0.5.1 benchmark suite, the task statistics (`taskCount`, `expectedRelevantFilesAverage`, `expectedRelevantSymbolsAverage`) of the two warm-index benchmark projects are derived from the dedicated corpus `benchmarks/contracts/warm-index-benchmark-cases.json`: the averages are the mean `expectedFiles` and `expectedSymbols` counts of that project's cases, rounded to two decimals. `npm run verify:benchmarks` fails if a profile disagrees with the corpus. The `benchmark-project-complexity-v1` formula, its weights, and its caps are unchanged; the current values are:
141
+
142
+ | Project | `taskCount` | `expectedRelevantFilesAverage` | `expectedRelevantSymbolsAverage` | `complexityScore` |
143
+ |---|---|---|---|---|
144
+ | `task-workflow-medium-ts` | 6 | 3.83 | 4.33 | 31 |
145
+ | `task-analytics-large-mixed` | 6 | 4.5 | 4.67 | 47 |
146
+
147
+ Task locality (`localized`, `cross-module`, `broad-change`) is benchmark metadata on each corpus case. It is not a metric, score, or ranking, and it does not enter the complexity formula.
148
+
140
149
  ## Prompt complexity metrics
141
150
 
142
151
  - `promptChars`
@@ -444,7 +453,9 @@ Implemented in `src/evaluation/stageContextMetrics` and composed once per run by
444
453
 
445
454
  ## Warm-index reuse metrics
446
455
 
447
- Implemented in the current release v0.5.0. The `warm-index-reuse` plugin calculates these metrics once, in `src/experiments/plugins/warmIndexReuse/metrics.ts`, from bounded execution summaries and bounded fake-agent evidence. The report section (`report.warmIndexReuse` in `report.json`, and the text and HTML "Warm Index Reuse Evidence" sections) and the four warm-index plots render these precomputed values; they never recalculate a formula.
456
+ Introduced in v0.5.0. The `warm-index-reuse` plugin calculates these metrics once, in `src/experiments/plugins/warmIndexReuse/metrics.ts`, from bounded execution summaries and bounded fake-agent evidence. The report section (`report.warmIndexReuse` in `report.json`, and the text and HTML "Warm Index Reuse Evidence" sections) and the four warm-index plots render these precomputed values; they never recalculate a formula.
457
+
458
+ The released v0.5.1 expanded benchmark suite adds no warm-index metric and changes no formula: over the dedicated 12-case corpus the same metrics run through task ordinal 6 in each project, and amortization and cumulative sums restart for each project. No token-savings percentage, break-even task, winner, speedup, ranking, or provider billing metric is calculated.
448
459
 
449
460
  **Availability contract.** Every warm-index metric carries `availability`, `value`, `unit`, `source`, `reason`, and `tokenCountMethod`:
450
461
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dailephd/my-dev-kit-lab",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "description": "Evidence, benchmark, and evaluation companion for my-dev-kit.",
6
6
  "bin": {