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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +9 -8
  3. package/dist/src/cli/help.js +16 -9
  4. package/dist/src/commands/runExperimentDescribeCommand.js +5 -2
  5. package/dist/src/commands/runExperimentRunCommand.js +0 -4
  6. package/dist/src/evaluation/benchmarkMetadata.js +5 -1
  7. package/dist/src/evaluation/parseAgentAnswer.js +172 -24
  8. package/dist/src/evaluation/runMyDevKitRetrieval.js +120 -7
  9. package/dist/src/experiments/plugins/incrementalChangeStaleness/comparisonV2.js +49 -0
  10. package/dist/src/experiments/plugins/incrementalChangeStaleness/config.js +5 -4
  11. package/dist/src/experiments/plugins/incrementalChangeStaleness/disposableTarget.js +20 -2
  12. package/dist/src/experiments/plugins/incrementalChangeStaleness/execution.js +42 -16
  13. package/dist/src/experiments/plugins/incrementalChangeStaleness/executionArtifact.js +4 -4
  14. package/dist/src/experiments/plugins/incrementalChangeStaleness/executionArtifactV2.js +289 -0
  15. package/dist/src/experiments/plugins/incrementalChangeStaleness/executionV2.js +195 -0
  16. package/dist/src/experiments/plugins/incrementalChangeStaleness/indexClone.js +76 -0
  17. package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecycle.js +64 -32
  18. package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecyclePolicy.js +6 -4
  19. package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecyclePolicyV2.js +78 -0
  20. package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecycleV2.js +298 -0
  21. package/dist/src/experiments/plugins/incrementalChangeStaleness/plugin.js +213 -12
  22. package/dist/src/experiments/plugins/incrementalChangeStaleness/treatmentSessionV2.js +5 -0
  23. package/dist/src/experiments/plugins/warmIndexReuse/warmIndexSession.js +3 -1
  24. package/dist/src/mobile/android/advancedSecurity/candidateEvidence.js +9 -2
  25. package/dist/src/mobile/android/advancedSecurity/redaction.js +19 -10
  26. package/dist/src/mobile/android/advancedSecurity/secretCandidates/analyzeSecretCandidates.js +12 -2
  27. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/analyzeSigningConfiguration.js +1 -1
  28. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/extractSigningConfigurations.js +7 -7
  29. package/dist/src/report/experiments/buildIncrementalChangeStalenessPluginReport.js +33 -0
  30. package/dist/src/report/experiments/buildIncrementalChangeStalenessReport.js +1 -1
  31. package/dist/src/report/experiments/buildIncrementalChangeStalenessReportV2.js +80 -0
  32. package/dist/src/report/experiments/buildPluginExperimentReport.js +2 -2
  33. package/dist/src/report/experiments/incrementalChangeStalenessReportModelV2.js +3 -0
  34. package/dist/src/report/experiments/incrementalChangeStalenessReportRowsV2.js +146 -0
  35. package/dist/src/report/experiments/index.js +3 -0
  36. package/dist/src/report/experiments/renderIncrementalChangeStalenessHtml.js +4 -0
  37. package/dist/src/report/experiments/renderIncrementalChangeStalenessHtmlV2.js +111 -0
  38. package/dist/src/report/experiments/renderIncrementalChangeStalenessTextV2.js +96 -0
  39. package/dist/src/report/experiments/renderPluginExperimentReportText.js +6 -0
  40. package/dist/src/securityValidation/staticScans/codeql.js +8 -8
  41. package/docs/METRICS.md +32 -0
  42. package/examples/tutorial-browser/prepare.mjs +13 -13
  43. package/examples/tutorial-browser/server.mjs +13 -13
  44. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@ All notable changes to my-dev-kit-lab are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.6.3] - 2026-09-30
8
+
9
+ ### Four-treatment partial-refresh experiments
10
+
11
+ - Replaced CodeQL-flagged polynomial regular-expression parsing paths with linear scanning and strengthened credential evidence fingerprints with context-salted scrypt derivation.
12
+
13
+ - Extended the `incremental-change-staleness` plugin from two treatments to four, in fixed order: `stale-index` (`my-dev-kit-no-refresh`), `changed-files-refresh` (`my-dev-kit-changed-files-refresh`), `affected-neighborhood-refresh` (`my-dev-kit-affected-neighborhood-refresh`), and `full-refresh` (`my-dev-kit-full-refresh`). No new experiment plugin was added.
14
+ - Changed the default kit command of `incremental-change-staleness` from `npx @dailephd/my-dev-kit@1.12.4` to `npx @dailephd/my-dev-kit@1.12.5`, the first published version with `index --incremental --refresh-scope changed-files|affected-neighborhood`. Lab requests the scope internally; no Lab-level `--refresh-scope` flag exists.
15
+ - Added a four-treatment lifecycle: four isolated disposable targets, equivalent trusted baselines, containment-checked whole-directory index clones for the two partial treatments, a mutation barrier, and per-treatment freshness validation. `stale-index` performs no post-mutation index invocation.
16
+ - Added truthful refresh evidence: upstream `requestedScope`, `appliedScope`, `selectionStatus`, `fallbackReason`, fresh-extraction, reused-file, forced-neighbor, and seed/affected counts, and a Lab `refreshRealization` of `NO_REFRESH`, `APPLIED_PARTIAL`, `FALLBACK_FULL`, or `FULL_REFRESH`. A partial treatment that falls back is `not-comparable-as-partial-refresh`.
17
+ - Added exactly three reference comparisons against `full-refresh`: `stale-index` (unchanged v0.6.2 stale-risk vocabulary) and the two partial treatments (`observed-regression-relative-to-full`, `no-observed-regression-relative-to-full`, `inconclusive`, `not-comparable-as-partial-refresh`). There is no winner, ranking, safety score, or automatic refresh policy.
18
+ - New runs write execution schema `my-dev-kit-lab-incremental-change-staleness-execution-v2` and report schema `my-dev-kit-lab-incremental-change-staleness-report-v2` (JSON, text, HTML); historical V1 artifacts and reports remain readable. Added additive numeric `upstream-refresh-*` metrics for the partial treatments, kept separate from the Lab affected-neighborhood metrics.
19
+ - `experiment run --help` now states that `--target` applies only to plugins that support external targets and that self-only plugins reject an explicit external `--target`.
20
+ - Automatic warm-index campaign report screenshots are best-effort presentation evidence: failed capture stays explicitly failed with its warning and no fabricated PNG, but does not fail an otherwise successful campaign. Required campaign, report, plot, and gallery failures remain fatal.
21
+ - Updated the packed-package acceptance gate (`npm run verify:packed-package`) to pin the real registry package `@dailephd/my-dev-kit@1.12.5` and to verify all six scenarios with four treatments, applied partial refresh, operational discrimination, and installed-package, upstream-package, and canonical-benchmark immutability.
22
+ - Added repository-controlled GitHub CodeQL advanced-setup analysis for JavaScript/TypeScript and Actions; the release candidate passed exact-SHA analysis with no open applicable alerts.
23
+ - No new plugin, `graph-diff` dependency, true-symbol-diff metric, multi-hop traversal, automatic refresh policy, external-target support, or new plot, screenshot, or gallery item was added.
24
+
7
25
  ## [0.6.2] - 2026-09-27
8
26
 
9
27
  ### Incremental-change and staleness plugin
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.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.
9
+ The latest release is v0.6.3 (four-treatment partial-refresh experiments), and the published package and installed CLI are version 0.6.3. The previous release is v0.6.2 (incremental-change and staleness experiments). 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
 
@@ -14,10 +14,10 @@ v0.4.6 adds a supported `my-dev-kit-lab` installed CLI router (`--help`, `--vers
14
14
 
15
15
  - **Run context-strategy experiments:** compare `raw-full-file` with `my-dev-kit-guided` using deterministic fixtures or locally configured Codex and Claude CLIs.
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
- - **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).
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, and a three-item gallery (report, plots, bounded execution evidence). Its report screenshot is best-effort presentation evidence: unavailable browser runtime or failed capture is reported explicitly, no PNG is fabricated, and neither outcome fails an otherwise successful campaign. 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).
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).
19
+ - **Compare no-refresh, partial-refresh, and full-refresh retrieval after a controlled change (v0.6.3, current release):** 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 four disposable target copies — `stale-index` (no refresh), `changed-files-refresh` and `affected-neighborhood-refresh` (real partial refreshes performed by my-dev-kit 1.12.5 through `index --incremental --refresh-scope`, selected internally with no Lab-level flag), and `full-refresh` (a complete new post-change index, used as a comparison reference rather than a preferred treatment). Each partial treatment truthfully records whether upstream applied the requested partial scope or fell back to a full rebuild (`APPLIED_PARTIAL` or `FALLBACK_FULL`); a fallback is not counted as partial-refresh evidence. Three comparisons against `full-refresh` are persisted with retrieval, deterministic fake-agent correctness, and required-file evidence in `incremental-change-staleness-execution.json` (schema `my-dev-kit-lab-incremental-change-staleness-execution-v2`) and the existing `report.json`/`report.txt`/`report.html`; historical v0.6.2 (V1) artifacts stay readable. There is no winner, ranking, or safety claim, and the plugin is self-only (`--target` is not accepted). `--kit-command` also accepts `incremental-change-staleness` and now defaults to my-dev-kit 1.12.5. See [docs/WORKFLOWS.md](docs/WORKFLOWS.md) and [docs/METRICS.md](docs/METRICS.md).
20
+ - **Assess affected-neighborhood evidence (v0.6.1, released; retained in the current package):** 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).
21
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.
22
22
  - **Validate CLI/package security:** inspect dependencies, package contents, path and subprocess boundaries, malformed inputs, optional static scanners, and bounded fuzz targets.
23
23
  - **Validate Android projects:** run nineteen static checks by default, with Gradle operations, external tools, and network access available only through explicit opt-in flags.
@@ -182,9 +182,9 @@ npm run experiment:run -- \
182
182
  --out lab-output/warm-index-campaign
183
183
  ```
184
184
 
185
- This automatically writes `report.json`/`.txt`/`.html`, the four warm-index charts under `plots/`, a best-effort `report.png`, and a three-item `gallery/` (report, plots, bounded execution evidence) beneath `--out`. `--timeout-ms` overrides the preset's default per-agent timeout (240000 ms). `codex-timeout-isolation` narrows the corpus to three larger cases for exercising provider timeout/partial-outcome handling without running the full 12-case corpus.
185
+ This automatically writes `report.json`/`.txt`/`.html`, the four warm-index charts under `plots/`, an optional best-effort `report.png`, and a three-item `gallery/` (report, plots, bounded execution evidence) beneath `--out`. Screenshot capture failure remains `failed`, is reported in the command output and gallery warning, and does not fail an otherwise successful campaign or fabricate a PNG. Required experiment, report, plot, and gallery failures remain fatal. `--timeout-ms` overrides the preset's default per-agent timeout (240000 ms). `codex-timeout-isolation` narrows the corpus to three larger cases for exercising provider timeout/partial-outcome handling without running the full 12-case corpus.
186
186
 
187
- 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.
187
+ For experiment plugins that support explicit targets — `context-strategy-comparison` and ordinary (non-campaign) `warm-index-reuse` runs — omitting `--target` uses self mode against my-dev-kit-lab. When those plugins receive `--target <path>`, the lab remains the tool root and the target project is inspected separately. `incremental-change-staleness` does not accept `--target`; it always runs against the lab's bundled benchmark projects. Generated experiment outputs stay under lab-controlled output directories by default, not inside an inspected target project.
188
188
 
189
189
  ---
190
190
 
@@ -236,10 +236,11 @@ See [docs/METRICS.md](docs/METRICS.md) for full metric definitions.
236
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)
237
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
238
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
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
+ - The generic experiment-plugin framework has three registered plugins: `context-strategy-comparison`, the v0.5.0-introduced `warm-index-reuse`, and the `incremental-change-staleness` plugin (introduced in v0.6.2; extended to four treatments in current release v0.6.3). Later plugins such as context-window scaling remain future roadmap work
240
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
241
241
  - The current release does not guarantee token savings; it produces auditable evidence for specific cases, targets, agents, and strategies
242
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
243
+ - v0.6.3 (current release) partial-refresh evidence is scoped to the executed deterministic scenarios and the configured my-dev-kit 1.12.5-or-later tool. A partial treatment can fall back to a full rebuild and is then reported as not comparable as partial refresh, so an applied partial refresh in one run does not guarantee the next; `no-observed-regression-relative-to-full` is not a safety claim, and full refresh is a reference rather than a winner. Correctness remains deterministic fake-agent evidence, and `incremental-change-staleness` accepts no external `--target`.
243
244
  - Provider telemetry dashboards, semantic LLM judging, and cloud API billing integration are not yet implemented
244
245
  - The six new stage-context strategies have no CLI flags yet, are configured programmatically, and do not yet include plots, screenshots, or gallery integration
245
246
  - The published upstream artifacts the stage-context strategies read do not expose considered-but-unselected reads or unnecessary-read evidence; those metrics report `unavailable` rather than zero
@@ -258,7 +259,7 @@ See [docs/METRICS.md](docs/METRICS.md) for full metric definitions.
258
259
 
259
260
  Android validation uses `--profile android`. Its default path is static and non-destructive: it starts zero Gradle processes, zero external tools, and zero network operations. Only confirmed `SecurityFinding` records can become audit issues; Android `CandidateEvidence` remains review-only evidence.
260
261
 
261
- Optional scanners are reported as `skipped` when unavailable, never as passed. The framework does not provide runtime isolation proof, device or APK/AAB analysis, signing verification, Play Console validation, automatic fixes, or manual pentesting. Manual pentest remains deferred until post-v1/version TBD.
262
+ Optional local scanners are reported as `skipped` when unavailable, never as passed. This local availability rule does not make CodeQL coverage optional for release readiness: the exact candidate must pass the repository-controlled GitHub CodeQL workflow and have its Code Scanning alerts reviewed. The framework does not provide runtime isolation proof, device or APK/AAB analysis, signing verification, Play Console validation, automatic fixes, or manual pentesting. Manual pentest remains deferred until post-v1/version TBD.
262
263
 
263
264
  See [COMMANDS.md](docs/COMMANDS.md) for exact syntax and [Security Validation Framework](docs/security-validation-framework.md) for checks, evidence semantics, verdicts, and limitations.
264
265
 
@@ -143,7 +143,10 @@ export function renderExperimentRunHelp() {
143
143
  " --experiment <id> Registered experiment plugin id (see \"my-dev-kit-lab experiment list\")",
144
144
  "",
145
145
  "Common options (all plugins):",
146
- " --target <path> Local target project path (default: self)",
146
+ " --target <path> Local target project path (default: self); only for",
147
+ " plugins that support external targets. Self-only",
148
+ " plugins (for example incremental-change-staleness)",
149
+ " reject an explicit external --target.",
147
150
  " --out <dir> Output directory for experiment artifacts",
148
151
  " (default: beneath the workspace when installed)",
149
152
  " --cases <path> Path to the evaluation cases file",
@@ -153,10 +156,12 @@ export function renderExperimentRunHelp() {
153
156
  " --case <ids> Comma-separated case ids to run",
154
157
  " --benchmark-project <ids> Comma-separated benchmark project ids to run",
155
158
  "",
159
+ "my-dev-kit command override (warm-index-reuse and incremental-change-staleness):",
160
+ " --kit-command <command> my-dev-kit command used by the selected experiment plugin",
161
+ " (defaults: warm-index-reuse uses npx @dailephd/my-dev-kit@latest;",
162
+ " incremental-change-staleness uses npx @dailephd/my-dev-kit@1.12.5)",
163
+ "",
156
164
  "warm-index-reuse only:",
157
- " --kit-command <command> my-dev-kit command used to build one index per benchmark",
158
- " project and retrieve per task",
159
- " (default: npx @dailephd/my-dev-kit@latest)",
160
165
  " Legacy/default mode uses the deterministic fake agent only;",
161
166
  " agent options below are not accepted by this plugin.",
162
167
  " --campaign <preset> Runs the selected real-agent campaign preset: codex-full,",
@@ -173,11 +178,13 @@ export function renderExperimentRunHelp() {
173
178
  " replaced by a context-size estimate. A campaign run",
174
179
  " automatically produces the report JSON/text/HTML, the",
175
180
  " four warm-index SVG plots, a best-effort report PNG",
176
- " screenshot, and a gallery manifest/index. A missing",
177
- " Playwright/browser runtime makes screenshot capture skip",
178
- " without invalidating the campaign; an unexpected",
179
- " screenshot failure makes the command unsuccessful after",
180
- " preserving the report, plots, and gallery. Legacy",
181
+ " screenshot, and a gallery manifest/index. The automatic",
182
+ " campaign report screenshot is best-effort presentation",
183
+ " evidence: skipped or failed capture is reported explicitly",
184
+ " (failed remains failed), no PNG is fabricated, and",
185
+ " screenshot status alone does not fail an otherwise",
186
+ " successful campaign command. Core experiment, report,",
187
+ " plot, and gallery failures remain fatal. Legacy",
181
188
  " non-campaign warm-index runs retain their existing",
182
189
  " report-only behavior.",
183
190
  "",
@@ -147,9 +147,12 @@ function describeExamples(plugin) {
147
147
  const screenshotOption = hasConfigField(plugin, "agents") ? " --no-screenshot" : "";
148
148
  const examples = [
149
149
  `my-dev-kit-lab experiment describe --experiment ${plugin.metadata.id}`,
150
- `my-dev-kit-lab experiment run --experiment ${plugin.metadata.id}${agentOptions}`,
151
- `my-dev-kit-lab experiment run --experiment ${plugin.metadata.id} --target "Z:\\Users\\newuser\\Projects\\my-dev-kit-v1"${agentOptions}${screenshotOption}`
150
+ `my-dev-kit-lab experiment run --experiment ${plugin.metadata.id}${agentOptions}`
152
151
  ];
152
+ // Only advertise an explicit --target example for plugins that support external targets.
153
+ if (plugin.metadata.supportedTargets.includes("external-local")) {
154
+ examples.push(`my-dev-kit-lab experiment run --experiment ${plugin.metadata.id} --target "Z:\\Users\\newuser\\Projects\\my-dev-kit-v1"${agentOptions}${screenshotOption}`);
155
+ }
153
156
  // Only advertise campaign examples for plugins that declare the campaignPreset config field.
154
157
  if (hasConfigField(plugin, "campaignPreset")) {
155
158
  examples.push(`my-dev-kit-lab experiment run --experiment ${plugin.metadata.id} --campaign codex-full --include-real-agents --out <dir>`, `my-dev-kit-lab experiment run --experiment ${plugin.metadata.id} --campaign claude-full --include-real-agents --case warm-medium-complete-idempotent --out <dir>`);
@@ -76,7 +76,6 @@ export async function runExperimentRunCommandFromArgs(argv, options = {}) {
76
76
  // Automatic presentation (report already produced above) applies only to warm-index-reuse
77
77
  // campaign runs; legacy fake-agent warm-index runs and context-strategy-comparison stay
78
78
  // report-only, unchanged.
79
- let screenshotFailed = false;
80
79
  if (args.experimentId === warmIndexReusePlugin.metadata.id && args.config.campaignPreset) {
81
80
  const executionArtifactPath = readMetadataString(result.metadata?.executionArtifactPath);
82
81
  const campaignAgentId = readMetadataString(result.metadata?.campaignAgentId);
@@ -106,7 +105,6 @@ export async function runExperimentRunCommandFromArgs(argv, options = {}) {
106
105
  else {
107
106
  outputLines.push("Screenshot: failed");
108
107
  outputLines.push(presentation.screenshot.error ?? "Screenshot capture failed.");
109
- screenshotFailed = true;
110
108
  }
111
109
  outputLines.push(`Gallery manifest: ${presentation.gallery.manifestPath}`);
112
110
  outputLines.push(`Gallery index: ${presentation.gallery.indexPath}`);
@@ -114,8 +112,6 @@ export async function runExperimentRunCommandFromArgs(argv, options = {}) {
114
112
  console.log(outputLines.join("\n"));
115
113
  if (result.status === "failed")
116
114
  return 1;
117
- if (screenshotFailed)
118
- return 1;
119
115
  return 0;
120
116
  }
121
117
  catch (error) {
@@ -280,7 +280,11 @@ function validateWarmIndexSourceRoots(sourceRoots, profile, label) {
280
280
  return errors;
281
281
  }
282
282
  function normalizeRelativeRoot(value) {
283
- return path.posix.normalize(value.replace(/\\/g, "/")).replace(/\/+$/, "");
283
+ const normalized = path.posix.normalize(value.replaceAll("\\", "/"));
284
+ let end = normalized.length;
285
+ while (end > 0 && normalized[end - 1] === "/")
286
+ end -= 1;
287
+ return normalized.slice(0, end);
284
288
  }
285
289
  function sameOrderedValues(left, right) {
286
290
  return left.length === right.length && left.every((value, index) => value === right[index]);
@@ -77,11 +77,24 @@ function collectJsonCandidates(text) {
77
77
  if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
78
78
  candidates.push(trimmed);
79
79
  }
80
- for (const match of text.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)) {
81
- const body = match[1]?.trim();
80
+ let cursor = 0;
81
+ while (cursor < text.length) {
82
+ const opening = text.indexOf("```", cursor);
83
+ if (opening < 0)
84
+ break;
85
+ let bodyStart = opening + 3;
86
+ if (text.slice(bodyStart, bodyStart + 4).toLowerCase() === "json")
87
+ bodyStart += 4;
88
+ while (bodyStart < text.length && isWhitespace(text[bodyStart]))
89
+ bodyStart += 1;
90
+ const closing = text.indexOf("```", bodyStart);
91
+ if (closing < 0)
92
+ break;
93
+ const body = text.slice(bodyStart, closing).trim();
82
94
  if (body?.startsWith("{")) {
83
95
  candidates.push(body);
84
96
  }
97
+ cursor = closing + 3;
85
98
  }
86
99
  return candidates;
87
100
  }
@@ -89,39 +102,103 @@ function collectFieldValues(text) {
89
102
  const fields = new Map();
90
103
  let currentKey;
91
104
  for (const line of text.split(/\r?\n/)) {
92
- const fieldMatch = line.match(/^\s*(?:[-*]\s*)?(?:[*_`]{0,2})([A-Za-z][A-Za-z0-9 _-]{1,40})(?:[*_`]{0,2})\s*:\s*(.*)$/);
93
- if (fieldMatch) {
94
- currentKey = normalizeKey(fieldMatch[1]);
105
+ const field = parseStructuredFieldLine(line);
106
+ if (field) {
107
+ currentKey = normalizeKey(field.label);
95
108
  const current = fields.get(currentKey) ?? [];
96
- current.push(stripMarkupOnlyValue(fieldMatch[2] ?? ""));
109
+ current.push(stripMarkupOnlyValue(field.value));
97
110
  fields.set(currentKey, current);
98
111
  continue;
99
112
  }
100
- if (currentKey && (/^\s+[-*]?\s*\S/.test(line) || /^\s*[-*]\s+\S/.test(line))) {
101
- fields.get(currentKey)?.push(line.trim().replace(/^[-*]\s*/, ""));
113
+ if (currentKey) {
114
+ const continuation = parseFieldContinuation(line);
115
+ if (continuation !== undefined)
116
+ fields.get(currentKey)?.push(continuation);
102
117
  }
103
118
  }
104
119
  return fields;
105
120
  }
121
+ function isWhitespace(character) {
122
+ return character.trim().length === 0;
123
+ }
124
+ function parseStructuredFieldLine(line) {
125
+ let start = 0;
126
+ while (start < line.length && isWhitespace(line[start]))
127
+ start += 1;
128
+ if (line[start] === "-" || line[start] === "*") {
129
+ start += 1;
130
+ while (start < line.length && isWhitespace(line[start]))
131
+ start += 1;
132
+ }
133
+ let leadingMarkup = 0;
134
+ while (start < line.length && leadingMarkup < 2 && "*_`".includes(line[start])) {
135
+ start += 1;
136
+ leadingMarkup += 1;
137
+ }
138
+ const colon = line.indexOf(":", start);
139
+ if (colon < 0)
140
+ return undefined;
141
+ let labelEnd = colon;
142
+ let trailingMarkup = 0;
143
+ while (labelEnd > start && trailingMarkup < 2 && "*_`".includes(line[labelEnd - 1])) {
144
+ labelEnd -= 1;
145
+ trailingMarkup += 1;
146
+ }
147
+ const label = line.slice(start, labelEnd);
148
+ if (label.length < 2 || label.length > 41 || !isAsciiLetter(label[0]))
149
+ return undefined;
150
+ for (let index = 1; index < label.length; index += 1) {
151
+ const character = label[index];
152
+ if (!isAsciiLetter(character) && !(character >= "0" && character <= "9") && character !== " " && character !== "_" && character !== "-")
153
+ return undefined;
154
+ }
155
+ let valueStart = colon + 1;
156
+ while (valueStart < line.length && isWhitespace(line[valueStart]))
157
+ valueStart += 1;
158
+ return { label, value: line.slice(valueStart) };
159
+ }
160
+ function isAsciiLetter(character) {
161
+ return (character >= "a" && character <= "z") || (character >= "A" && character <= "Z");
162
+ }
163
+ function parseFieldContinuation(line) {
164
+ let index = 0;
165
+ while (index < line.length && isWhitespace(line[index]))
166
+ index += 1;
167
+ const indented = index > 0;
168
+ if (index === line.length)
169
+ return undefined;
170
+ if (line[index] === "-" || line[index] === "*") {
171
+ index += 1;
172
+ const whitespaceStart = index;
173
+ while (index < line.length && isWhitespace(line[index]))
174
+ index += 1;
175
+ if ((!indented && index === whitespaceStart) || index === line.length)
176
+ return undefined;
177
+ return line.slice(index).trim();
178
+ }
179
+ if (!indented)
180
+ return undefined;
181
+ return line.slice(index).trim();
182
+ }
106
183
  function parseMarkdownSection(text, headings) {
107
184
  const lines = text.split(/\r?\n/);
108
185
  const values = [];
109
186
  let inSection = false;
110
187
  for (const line of lines) {
111
- const heading = line.match(/^\s*#{1,6}\s*(.+?)\s*$/);
112
- if (heading) {
113
- inSection = headings.some((candidate) => normalizeKey(candidate) === normalizeKey(heading[1]));
188
+ const heading = parseMarkdownHeading(line);
189
+ if (heading !== undefined) {
190
+ inSection = headings.some((candidate) => normalizeKey(candidate) === normalizeKey(heading));
114
191
  continue;
115
192
  }
116
193
  if (!inSection) {
117
194
  continue;
118
195
  }
119
- if (/^\s*#{1,6}\s+/.test(line)) {
196
+ if (isMarkdownHeading(line)) {
120
197
  break;
121
198
  }
122
- const bullet = line.match(/^\s*[-*]\s+(.+)$/);
123
- if (bullet) {
124
- values.push(bullet[1].trim());
199
+ const bullet = parseMarkdownBullet(line);
200
+ if (bullet !== undefined) {
201
+ values.push(bullet);
125
202
  }
126
203
  else if (line.includes(",")) {
127
204
  values.push(...splitListValues([line]));
@@ -129,6 +206,41 @@ function parseMarkdownSection(text, headings) {
129
206
  }
130
207
  return unique(values);
131
208
  }
209
+ function parseMarkdownHeading(line) {
210
+ let index = 0;
211
+ while (index < line.length && isWhitespace(line[index]))
212
+ index += 1;
213
+ const hashStart = index;
214
+ while (index < line.length && line[index] === "#" && index - hashStart < 6)
215
+ index += 1;
216
+ if (index === hashStart || (index < line.length && line[index] === "#"))
217
+ return undefined;
218
+ const heading = line.slice(index).trim();
219
+ return heading.length > 0 ? heading : undefined;
220
+ }
221
+ function isMarkdownHeading(line) {
222
+ let index = 0;
223
+ while (index < line.length && isWhitespace(line[index]))
224
+ index += 1;
225
+ const start = index;
226
+ while (index < line.length && line[index] === "#" && index - start < 6)
227
+ index += 1;
228
+ return index > start && index < line.length && isWhitespace(line[index]);
229
+ }
230
+ function parseMarkdownBullet(line) {
231
+ let index = 0;
232
+ while (index < line.length && isWhitespace(line[index]))
233
+ index += 1;
234
+ if (line[index] !== "-" && line[index] !== "*")
235
+ return undefined;
236
+ index += 1;
237
+ if (index >= line.length || !isWhitespace(line[index]))
238
+ return undefined;
239
+ while (index < line.length && isWhitespace(line[index]))
240
+ index += 1;
241
+ const content = line.slice(index).trim();
242
+ return content.length > 0 ? content : undefined;
243
+ }
132
244
  function enrichFacts(parsed, text, answerKey) {
133
245
  parsed.expectedFactsFound = normalizeFactMatches(parsed.expectedFactsFound, text, answerKey);
134
246
  if (parsed.answerText.length === 0) {
@@ -168,15 +280,51 @@ function splitListValues(values) {
168
280
  .filter(Boolean));
169
281
  }
170
282
  function cleanListItem(value) {
171
- const codeSpan = value.match(/`([^`]+)`/);
172
- const cleaned = (codeSpan?.[1] ?? value)
173
- .trim()
174
- .replace(/^[-*]\s*/, "")
175
- .replace(/^["'`]+|["'`.]+$/g, "")
176
- .replace(/\s+[--]\s+.*$/g, "")
177
- .replace(/\s+[-–—]\s+.*$/g, "")
178
- .trim();
179
- return cleaned;
283
+ let source = value;
284
+ let searchFrom = 0;
285
+ while (searchFrom < value.length) {
286
+ const open = value.indexOf("`", searchFrom);
287
+ if (open < 0)
288
+ break;
289
+ const close = value.indexOf("`", open + 1);
290
+ if (close < 0)
291
+ break;
292
+ if (close > open + 1) {
293
+ source = value.slice(open + 1, close);
294
+ break;
295
+ }
296
+ searchFrom = close + 1;
297
+ }
298
+ let start = 0;
299
+ let end = source.length;
300
+ while (start < end && isWhitespace(source[start]))
301
+ start += 1;
302
+ while (end > start && isWhitespace(source[end - 1]))
303
+ end -= 1;
304
+ if (source[start] === "-" || source[start] === "*") {
305
+ start += 1;
306
+ while (start < end && isWhitespace(source[start]))
307
+ start += 1;
308
+ }
309
+ while (start < end && (source[start] === '"' || source[start] === "'" || source[start] === "`"))
310
+ start += 1;
311
+ while (end > start && (source[end - 1] === '"' || source[end - 1] === "'" || source[end - 1] === "`" || source[end - 1] === "."))
312
+ end -= 1;
313
+ let whitespaceStart = -1;
314
+ for (let index = start; index < end; index += 1) {
315
+ const character = source[index];
316
+ if (isWhitespace(character)) {
317
+ if (whitespaceStart < 0)
318
+ whitespaceStart = index;
319
+ continue;
320
+ }
321
+ if ((character === "-" || character === "–" || character === "—") && whitespaceStart >= start && index + 1 < end && isWhitespace(source[index + 1])) {
322
+ end = whitespaceStart;
323
+ break;
324
+ }
325
+ whitespaceStart = -1;
326
+ }
327
+ return source.slice(start, end).trim();
180
328
  }
181
329
  function stripMarkupOnlyValue(value) {
182
330
  return /^[*_`\s]+$/.test(value) ? "" : value;
@@ -55,13 +55,118 @@ function skippedRetrieval(caseId, warnings, commands, durationMs) {
55
55
  durationMs
56
56
  };
57
57
  }
58
+ const APPLIED_SCOPES = ["none", "changed-files", "affected-neighborhood", "full"];
59
+ const SELECTION_STATUSES = ["not-needed", "applied", "fallback-full"];
60
+ const NEIGHBORHOOD_COUNT_KEYS = ["seedFileCount", "seedSymbolCount", "affectedNodeCount", "affectedEdgeCount"];
61
+ function isNonNegativeInteger(value) {
62
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
63
+ }
64
+ /**
65
+ * Validates the per-invocation `incrementalRefresh` object from `my-dev-kit index --incremental --json`
66
+ * stdout against the scope Lab requested. Returns validated evidence or an explicit error; never coerces.
67
+ * The fallback reason is intentionally an open string: upstream reason codes may evolve additively.
68
+ */
69
+ export function parseIncrementalRefreshEvidence(stdout, requestedScope) {
70
+ const payload = parseJsonIfPossible(stdout);
71
+ if (payload === undefined)
72
+ return { ok: false, error: "index stdout is not valid JSON" };
73
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
74
+ return { ok: false, error: "index JSON result is not an object" };
75
+ }
76
+ const raw = payload.incrementalRefresh;
77
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
78
+ return { ok: false, error: "index JSON result has no incrementalRefresh object" };
79
+ }
80
+ const r = raw;
81
+ if (r.requestedScope !== requestedScope) {
82
+ return {
83
+ ok: false,
84
+ error: `incrementalRefresh.requestedScope ${JSON.stringify(r.requestedScope)} does not match requested scope ${requestedScope}`
85
+ };
86
+ }
87
+ if (!APPLIED_SCOPES.includes(r.appliedScope)) {
88
+ return { ok: false, error: `incrementalRefresh.appliedScope ${JSON.stringify(r.appliedScope)} is not a known value` };
89
+ }
90
+ if (!SELECTION_STATUSES.includes(r.selectionStatus)) {
91
+ return { ok: false, error: `incrementalRefresh.selectionStatus ${JSON.stringify(r.selectionStatus)} is not a known value` };
92
+ }
93
+ if (r.fallbackReason !== null && typeof r.fallbackReason !== "string") {
94
+ return { ok: false, error: "incrementalRefresh.fallbackReason must be a string or null" };
95
+ }
96
+ for (const key of NEIGHBORHOOD_COUNT_KEYS) {
97
+ if (r[key] !== null && !isNonNegativeInteger(r[key])) {
98
+ return { ok: false, error: `incrementalRefresh.${key} must be a non-negative integer or null` };
99
+ }
100
+ }
101
+ for (const key of ["forcedNeighborReanalysisFileCount", "freshExtractionFileCount", "reusedFileCount"]) {
102
+ if (!isNonNegativeInteger(r[key])) {
103
+ return { ok: false, error: `incrementalRefresh.${key} must be a non-negative integer` };
104
+ }
105
+ }
106
+ if (!Array.isArray(r.forcedNeighborSample) || r.forcedNeighborSample.some((item) => typeof item !== "string")) {
107
+ return { ok: false, error: "incrementalRefresh.forcedNeighborSample must be an array of strings" };
108
+ }
109
+ const appliedScope = r.appliedScope;
110
+ const selectionStatus = r.selectionStatus;
111
+ const fallbackReason = r.fallbackReason;
112
+ if (selectionStatus === "applied") {
113
+ if (appliedScope !== requestedScope) {
114
+ return { ok: false, error: "incrementalRefresh applied status requires appliedScope equal to requestedScope" };
115
+ }
116
+ if (fallbackReason !== null)
117
+ return { ok: false, error: "incrementalRefresh applied status requires a null fallbackReason" };
118
+ }
119
+ else if (selectionStatus === "fallback-full") {
120
+ if (appliedScope !== "full")
121
+ return { ok: false, error: "incrementalRefresh fallback-full status requires appliedScope full" };
122
+ if (fallbackReason === null || fallbackReason.length === 0) {
123
+ return { ok: false, error: "incrementalRefresh fallback-full status requires a nonempty fallbackReason" };
124
+ }
125
+ }
126
+ else {
127
+ if (appliedScope !== "none")
128
+ return { ok: false, error: "incrementalRefresh not-needed status requires appliedScope none" };
129
+ if (fallbackReason !== null)
130
+ return { ok: false, error: "incrementalRefresh not-needed status requires a null fallbackReason" };
131
+ }
132
+ if (requestedScope === "affected-neighborhood" && selectionStatus === "applied") {
133
+ for (const key of NEIGHBORHOOD_COUNT_KEYS) {
134
+ if (r[key] === null) {
135
+ return { ok: false, error: `incrementalRefresh.${key} must be numeric for an applied affected-neighborhood refresh` };
136
+ }
137
+ }
138
+ }
139
+ return {
140
+ ok: true,
141
+ evidence: {
142
+ requestedScope,
143
+ appliedScope,
144
+ selectionStatus,
145
+ fallbackReason,
146
+ seedFileCount: r.seedFileCount,
147
+ seedSymbolCount: r.seedSymbolCount,
148
+ affectedNodeCount: r.affectedNodeCount,
149
+ affectedEdgeCount: r.affectedEdgeCount,
150
+ forcedNeighborReanalysisFileCount: r.forcedNeighborReanalysisFileCount,
151
+ forcedNeighborSample: [...r.forcedNeighborSample],
152
+ freshExtractionFileCount: r.freshExtractionFileCount,
153
+ reusedFileCount: r.reusedFileCount
154
+ }
155
+ };
156
+ }
58
157
  /**
59
158
  * Runs exactly one my-dev-kit `index` command for the target and writes the index only to
60
159
  * `indexDir`. Performs no search/lookup/slice/source work. With `requireKit` a failed index
61
160
  * throws; otherwise the failure is returned as `ok: false` with a warning.
161
+ *
162
+ * `mode` defaults to a full index. An incremental mode adds `--incremental --refresh-scope <scope>`
163
+ * and requires valid per-invocation `incrementalRefresh` JSON evidence on stdout; a successful
164
+ * process without that evidence is not a usable incremental build. Upstream full-fallback and
165
+ * no-change outcomes are valid evidence and are returned as-is.
62
166
  */
63
167
  export async function buildMyDevKitIndex(options) {
64
168
  const warnings = [];
169
+ const mode = options.mode ?? { kind: "full" };
65
170
  const command = await runMeasuredCommand({
66
171
  commandId: "index",
67
172
  commandString: options.kitCommand,
@@ -74,22 +179,30 @@ export async function buildMyDevKitIndex(options) {
74
179
  ...options.target.sourceRoots.flatMap((sourceRoot) => ["--src", sourceRoot]),
75
180
  "--out",
76
181
  options.indexDir,
182
+ ...(mode.kind === "incremental" ? ["--incremental", "--refresh-scope", mode.refreshScope] : []),
77
183
  "--json"
78
184
  ]
79
185
  });
186
+ const result = { indexDir: options.indexDir, durationMs: command.durationMs, command, mode };
80
187
  if (!command.ok) {
81
188
  if (options.requireKit) {
82
189
  throw new Error(command.error || `my-dev-kit index failed with exit code ${command.exitCode}`);
83
190
  }
84
191
  warnings.push("my-dev-kit index command was unavailable or failed.");
192
+ return { ...result, ok: false, warnings, incrementalRefresh: null };
85
193
  }
86
- return {
87
- ok: command.ok,
88
- indexDir: options.indexDir,
89
- durationMs: command.durationMs,
90
- warnings,
91
- command
92
- };
194
+ if (mode.kind === "full") {
195
+ return { ...result, ok: true, warnings, incrementalRefresh: null };
196
+ }
197
+ const parsed = parseIncrementalRefreshEvidence(command.stdout, mode.refreshScope);
198
+ if (!parsed.ok) {
199
+ const message = `my-dev-kit incremental index (${mode.refreshScope}) returned no valid incrementalRefresh evidence: ${parsed.error}`;
200
+ if (options.requireKit)
201
+ throw new Error(message);
202
+ warnings.push(message);
203
+ return { ...result, ok: false, warnings, incrementalRefresh: null };
204
+ }
205
+ return { ...result, ok: true, warnings, incrementalRefresh: parsed.evidence };
93
206
  }
94
207
  /**
95
208
  * Runs `<kit-command> --version` once through the shared measured-command runner and returns