@dailephd/my-dev-kit-lab 0.5.2 → 0.6.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.
@@ -5,6 +5,8 @@ const UNIT_LABELS = {
5
5
  "estimated-tokens": "est. tokens",
6
6
  tokens: "tokens",
7
7
  score: "score",
8
+ count: "",
9
+ percent: "%",
8
10
  };
9
11
  export function renderWarmIndexReuseHtml(section) {
10
12
  if (section === null) {
@@ -25,6 +27,8 @@ export function renderWarmIndexReuseHtml(section) {
25
27
  ["Agent correctness available", `${section.summary.agentCorrectnessAvailableCount} of ${section.summary.agentSideCount}`],
26
28
  ["Agent token totals available", `${section.summary.agentTotalTokensAvailableCount} of ${section.summary.agentSideCount}`],
27
29
  ])}
30
+ ${renderFreshnessSummary(section.indexFreshnessSummary ?? legacyFreshnessSummary(section.summary.taskCount))}
31
+ ${renderAffectedNeighborhoodSummary(section.affectedNeighborhoodSummary ?? legacyAffectedNeighborhoodSummary(section.summary.taskCount))}
28
32
  ${section.agentCampaign ? renderCampaignSection(section.agentCampaign) : ""}
29
33
  <h3>Cold Start And Warm Reuse</h3>
30
34
  ${list(section.costModel)}
@@ -33,6 +37,167 @@ export function renderWarmIndexReuseHtml(section) {
33
37
  ${section.projects.length === 0 ? "<p>No warm-index project evidence was recorded.</p>" : section.projects.map((project, index) => renderProject(project, index, section.agent.id)).join("\n")}
34
38
  </section>`;
35
39
  }
40
+ // Reports serialized before v0.6.0 Batch 3 lack the summary; they are rendered as having no assessment.
41
+ function legacyFreshnessSummary(taskCount) {
42
+ return {
43
+ assessedTaskCount: 0,
44
+ unassessedTaskCount: taskCount,
45
+ freshTaskCount: 0,
46
+ staleTaskCount: 0,
47
+ partiallyStaleTaskCount: 0,
48
+ unknownTaskCount: 0,
49
+ };
50
+ }
51
+ // Reports serialized before v0.6.1 lack the summary; they are rendered as having no assessment.
52
+ function legacyAffectedNeighborhoodSummary(taskCount) {
53
+ return {
54
+ assessedTaskCount: 0,
55
+ unassessedTaskCount: taskCount,
56
+ completeAssessmentCount: 0,
57
+ partialAssessmentCount: 0,
58
+ unavailableAssessmentCount: 0,
59
+ relatedTaskCount: 0,
60
+ unrelatedTaskCount: 0,
61
+ unknownRelationshipTaskCount: 0,
62
+ recommendedReindexCount: 0,
63
+ notIndicatedReindexCount: 0,
64
+ unknownReindexRecommendationCount: 0,
65
+ };
66
+ }
67
+ function renderAffectedNeighborhoodSummary(summary) {
68
+ return `<h3>Affected Neighborhood</h3>
69
+ ${table(["Field", "Value"], [
70
+ ["Assessed tasks", String(summary.assessedTaskCount)],
71
+ ["Unassessed tasks", String(summary.unassessedTaskCount)],
72
+ ["Complete assessments", String(summary.completeAssessmentCount)],
73
+ ["Partial assessments", String(summary.partialAssessmentCount)],
74
+ ["Unavailable assessments", String(summary.unavailableAssessmentCount)],
75
+ ["Related tasks", String(summary.relatedTaskCount)],
76
+ ["Unrelated tasks", String(summary.unrelatedTaskCount)],
77
+ ["Unknown-relationship tasks", String(summary.unknownRelationshipTaskCount)],
78
+ ["Reindex recommended", String(summary.recommendedReindexCount)],
79
+ ["Reindex not indicated", String(summary.notIndicatedReindexCount)],
80
+ ["Reindex recommendation unknown", String(summary.unknownReindexRecommendationCount)],
81
+ ])}`;
82
+ }
83
+ function boundedNote(label, taskOrdinal, bounded) {
84
+ return bounded.omittedCount > 0 ? [`Task ${taskOrdinal}: ${bounded.omittedCount} of ${bounded.totalCount} ${label} omitted.`] : [];
85
+ }
86
+ /** Persisted affected-neighborhood evidence per task; numeric cells are the metric owner's objects. */
87
+ function renderProjectAffectedNeighborhood(tasks) {
88
+ const overview = table(["Task", "Assessment", "Relationship", "Reindex recommendation", "Freshness", "Seeds", "Neighborhood", "Task mapping", "Resolvable task nodes"], tasks.map((task) => {
89
+ const label = `${task.taskOrdinal}. ${task.caseId}`;
90
+ const evidence = task.affectedNeighborhood;
91
+ return evidence
92
+ ? [
93
+ label,
94
+ evidence.status,
95
+ evidence.relationship,
96
+ evidence.reindexRecommendation,
97
+ evidence.freshnessStatus,
98
+ evidence.seedMappingStatus,
99
+ evidence.neighborhoodStatus,
100
+ evidence.taskMappingStatus,
101
+ String(evidence.resolvableTaskNodeCount),
102
+ ]
103
+ : [label, "not assessed", "", "", "", "", "", "", ""];
104
+ }));
105
+ const assessed = tasks.filter((task) => task.affectedNeighborhood !== null);
106
+ const metricRows = assessed.map((task) => {
107
+ const metrics = task.affectedNeighborhood.metrics;
108
+ return [
109
+ `${task.taskOrdinal}. ${task.caseId}`,
110
+ withReason(metrics.changedFileCount),
111
+ withReason(metrics.changedSymbolCount),
112
+ withReason(metrics.affectedNodeCount),
113
+ withReason(metrics.affectedEdgeCount),
114
+ withReason(metrics.taskOverlapCount),
115
+ withReason(metrics.taskOverlapPercent),
116
+ ];
117
+ });
118
+ const explanations = assessed.map((task) => `Task ${task.taskOrdinal}: ${task.affectedNeighborhood.recommendationExplanation}`);
119
+ const detailRows = assessed.flatMap((task) => {
120
+ const evidence = task.affectedNeighborhood;
121
+ const label = `${task.taskOrdinal}. ${task.caseId}`;
122
+ return [
123
+ ...evidence.unresolvedTaskMappings.items.map((entry) => [label, `unresolved ${entry.subject}`, entry.name, entry.reason]),
124
+ ...evidence.ambiguousTaskSymbols.items.map((entry) => [label, "ambiguous expected-symbol", entry.name, entry.candidateNodeIds.join(", ")]),
125
+ ...evidence.warnings.items.map((warning) => [label, "warning", warning.code, warning.message]),
126
+ ];
127
+ });
128
+ const omitted = assessed.flatMap((task) => {
129
+ const evidence = task.affectedNeighborhood;
130
+ return [
131
+ ...boundedNote("affected node IDs", task.taskOrdinal, evidence.affectedNodeIds),
132
+ ...boundedNote("participating edge IDs", task.taskOrdinal, evidence.participatingEdgeIds),
133
+ ...boundedNote("unresolved task mappings", task.taskOrdinal, evidence.unresolvedTaskMappings),
134
+ ...boundedNote("ambiguous task symbols", task.taskOrdinal, evidence.ambiguousTaskSymbols),
135
+ ...boundedNote("warnings", task.taskOrdinal, evidence.warnings),
136
+ ];
137
+ });
138
+ return `<h4>Affected Neighborhood</h4>
139
+ ${overview}
140
+ ${metricRows.length === 0 ? "" : table(["Task", "Changed files", "Changed symbols", "Affected nodes", "Affected edges", "Task-overlap nodes", "Task-overlap percent"], metricRows)}
141
+ ${explanations.length === 0 ? "" : list(explanations)}
142
+ ${detailRows.length === 0 ? "" : table(["Task", "Evidence type", "Name", "Detail"], detailRows)}
143
+ ${omitted.length === 0 ? "" : list(omitted)}`;
144
+ }
145
+ function renderFreshnessSummary(summary) {
146
+ return `<h3>Index Freshness</h3>
147
+ ${table(["Field", "Value"], [
148
+ ["Assessed tasks", String(summary.assessedTaskCount)],
149
+ ["Unassessed tasks", String(summary.unassessedTaskCount)],
150
+ ["Fresh tasks", String(summary.freshTaskCount)],
151
+ ["Stale tasks", String(summary.staleTaskCount)],
152
+ ["Partially-stale tasks", String(summary.partiallyStaleTaskCount)],
153
+ ["Unknown tasks", String(summary.unknownTaskCount)],
154
+ ])}`;
155
+ }
156
+ /** Persisted freshness evidence per task; hashes are deliberately absent from this presentation. */
157
+ function renderProjectFreshness(tasks) {
158
+ const overview = table(["Task", "Freshness", "Baseline", "Indexed", "Comparable", "Unchanged", "Modified", "Missing", "Unresolved"], tasks.map((task) => {
159
+ const freshness = task.indexFreshness;
160
+ const label = `${task.taskOrdinal}. ${task.caseId}`;
161
+ return freshness
162
+ ? [
163
+ label,
164
+ freshness.status,
165
+ freshness.baselineSnapshotStatus,
166
+ String(freshness.indexedFileCount),
167
+ String(freshness.comparableFileCount),
168
+ String(freshness.unchangedFileCount),
169
+ String(freshness.changedFileCount),
170
+ String(freshness.missingFileCount),
171
+ String(freshness.unresolvedFileCount),
172
+ ]
173
+ : [label, "not assessed", "unavailable", "", "", "", "", "", ""];
174
+ }));
175
+ const detailRows = tasks.flatMap((task) => {
176
+ const freshness = task.indexFreshness;
177
+ if (!freshness)
178
+ return [];
179
+ const label = `${task.taskOrdinal}. ${task.caseId}`;
180
+ return [
181
+ ...freshness.changes.items.map((change) => [label, change.changeType, change.path, ""]),
182
+ ...freshness.unresolved.items.map((entry) => [label, "unresolved", entry.path ?? "run-level", `${entry.reasonCode}: ${entry.message}`]),
183
+ ];
184
+ });
185
+ const omitted = tasks.flatMap((task) => {
186
+ const freshness = task.indexFreshness;
187
+ if (!freshness)
188
+ return [];
189
+ return [
190
+ ...(freshness.changes.omittedCount > 0 ? [`Task ${task.taskOrdinal}: ${freshness.changes.omittedCount} of ${freshness.changes.totalCount} changes omitted.`] : []),
191
+ ...(freshness.unresolved.omittedCount > 0
192
+ ? [`Task ${task.taskOrdinal}: ${freshness.unresolved.omittedCount} of ${freshness.unresolved.totalCount} unresolved entries omitted.`]
193
+ : []),
194
+ ];
195
+ });
196
+ return `<h4>Index Freshness</h4>
197
+ ${overview}
198
+ ${detailRows.length === 0 ? "" : table(["Task", "Evidence type", "Path", "Detail"], detailRows)}
199
+ ${omitted.length === 0 ? "" : list(omitted)}`;
200
+ }
36
201
  function renderCampaignSection(campaign) {
37
202
  return `<h3>Real-Agent Campaign</h3>
38
203
  ${table(["Field", "Value"], [
@@ -115,6 +280,8 @@ function renderProject(project, index, agentId) {
115
280
  formatMetric(task.raw.cumulativeEstimatedContextTokens),
116
281
  formatMetric(task.warm.cumulativeEstimatedContextTokens),
117
282
  ]))}
283
+ ${renderProjectFreshness(project.tasks)}
284
+ ${renderProjectAffectedNeighborhood(project.tasks)}
118
285
  <p class="muted">Estimated tokens are character-based context-size estimates, not provider token usage.</p>
119
286
  <p class="muted">${agentColumnsNote(agentId)}</p>
120
287
  ${table([
@@ -1 +1,9 @@
1
1
  export const WARM_INDEX_REUSE_REPORT_SCHEMA_VERSION = "my-dev-kit-lab-warm-index-report-v1";
2
+ /** Report presentation limits; the full persisted lists stay in warm-index-execution.json. */
3
+ export const MAX_REPORT_FRESHNESS_CHANGES = 20;
4
+ export const MAX_REPORT_FRESHNESS_UNRESOLVED = 10;
5
+ export const MAX_REPORT_AFFECTED_NODES = 20;
6
+ export const MAX_REPORT_AFFECTED_EDGES = 20;
7
+ export const MAX_REPORT_AFFECTED_TASK_UNRESOLVED = 10;
8
+ export const MAX_REPORT_AFFECTED_TASK_AMBIGUOUS = 10;
9
+ export const MAX_REPORT_AFFECTED_WARNINGS = 10;
package/docs/METRICS.md CHANGED
@@ -463,7 +463,7 @@ The released v0.5.1 expanded benchmark suite adds no warm-index metric and chang
463
463
  - `unavailable`: `value` is `null` and `reason` explains the missing or invalid evidence. Missing evidence is never reported as zero, and a non-finite upstream number becomes `unavailable` with an invalid-input reason.
464
464
  - `not-applicable`: `value` is `null` and `reason` explains why the metric does not apply. The contract supports it; the current warm-index metrics do not emit it.
465
465
 
466
- Units are `ms`, `characters`, `estimated-tokens`, `tokens`, and `score`. Sources are `measured`, `derived`, `estimated-chars-div-4`, and `agent`. `tokenCountMethod` is set only for estimated-token metrics (currently `estimated_chars_div_4`).
466
+ Units are `ms`, `characters`, `estimated-tokens`, `tokens`, and `score`; the affected-neighborhood metrics below add `count` and `percent` in v0.6.1. Sources are `measured`, `derived`, `estimated-chars-div-4`, and `agent`. `tokenCountMethod` is set only for estimated-token metrics (currently `estimated_chars_div_4`).
467
467
 
468
468
  **Task ordinal.** Tasks keep their filtered source order inside each benchmark project and are numbered `1, 2, 3, …` per project. Ordinals, amortization, and every cumulative sum restart for each benchmark project.
469
469
 
@@ -513,7 +513,7 @@ The plugin evaluates each task side that has context evidence once with the dete
513
513
  - Cumulative fake-agent total tokens (`tokens`, `agent`)
514
514
  Formula: sum of fake-agent total tokens for tasks `1..N` on that side, under the strict-prefix rule.
515
515
 
516
- **Generic outcome metrics.** Available values also appear as `ExperimentMetric` entries on each outcome, with `variantId`, `caseId`, `unit`, and `description`. Both the `raw-full-file` and `warm-index-reuse` outcomes can carry `context-character-count`, `context-estimated-token-count`, `operation-duration-ms` (raw context construction or warm retrieval), `cumulative-component-duration-ms`, `cumulative-context-estimated-token-count`, `agent-correctness-score`, `agent-total-tokens`, and `cumulative-agent-total-tokens`. The warm outcome also carries `amortized-index-build-duration-ms`. Unavailable metrics are omitted rather than emitted as zero. Run-level metrics are limited to `warm-index-project-count`, `warm-index-task-count`, and `warm-index-session-prepared-project-count`.
516
+ **Generic outcome metrics.** Available values also appear as `ExperimentMetric` entries on each outcome, with `variantId`, `caseId`, `unit`, and `description`. Both the `raw-full-file` and `warm-index-reuse` outcomes can carry `context-character-count`, `context-estimated-token-count`, `operation-duration-ms` (raw context construction or warm retrieval), `cumulative-component-duration-ms`, `cumulative-context-estimated-token-count`, `agent-correctness-score`, `agent-total-tokens`, and `cumulative-agent-total-tokens`. The warm outcome also carries `amortized-index-build-duration-ms` and the six released v0.6.1 affected-neighborhood metrics listed below. Unavailable metrics are omitted rather than emitted as zero. Run-level metrics are limited to `warm-index-project-count`, `warm-index-task-count`, and `warm-index-session-prepared-project-count`.
517
517
 
518
518
  **Interpretation.** The report separates the one-time index cost from per-task retrieval cost and shows the fixed cost falling per task as more tasks reuse the index. It calculates no token-savings percentage, duration-reduction percentage, break-even task, composite score, winner, or ranking. Warm-index plots map the same precomputed values: amortized index build duration, raw versus retrieved estimated context tokens, fake-agent correctness (or campaign agent correctness for a `--campaign` run), and cumulative fake-agent total tokens. Unavailable values become skipped plot points with their reason.
519
519
 
@@ -535,3 +535,63 @@ Available in the installed v0.5.2 CLI. A `--campaign` run replaces the determini
535
535
  Caveat: Claude and Codex token exposure depends on that provider's actual CLI output for the run; token evidence is reported as `unavailable`/`partial` explicitly rather than assumed. No fixed claim that either provider always or never exposes a token total is made — `tokenEvidenceStatus` is the authoritative, per-run signal.
536
536
 
537
537
  No token-savings percentage, break-even task, winner, ranking, or provider billing metric is calculated for a campaign run, matching the fake-agent path above.
538
+
539
+ ### Index freshness evidence (v0.6.0)
540
+
541
+ Status: released in v0.6.0 and present in the current package. The freshness values below are report evidence, not metric formulas: no `ExperimentMetric` ID, plot, or gallery item is added for them.
542
+
543
+ Before each task's warm retrieval, the warm-index run compares the files the index snapshot proves were indexed against their current state and persists one assessment per task (`indexFreshness` in `warm-index-execution.json`). The report only presents that persisted assessment; it never re-reads the filesystem, re-hashes files, or invokes my-dev-kit.
544
+
545
+ - **Snapshot-scoped comparison.** Only files listed by the index snapshot are compared. Freshness does not establish that the whole repository is unchanged.
546
+ - **SHA-256 is the authority.** A file is modified when its current SHA-256 differs from the snapshot's, or missing when it no longer exists. A changed modified time with identical content is not a change; timestamps are diagnostic metadata only.
547
+ - **New and non-indexed files.** A file that was not represented in the snapshot is outside the comparison and is not, by itself, evidence that the index is stale. Lab does not guess which files my-dev-kit would index.
548
+ - **Four statuses.**
549
+ - `fresh`: the snapshot was complete, every represented file was compared, and every content identity still matches.
550
+ - `stale`: a complete comparison confirmed at least one represented file is modified or missing.
551
+ - `partially-stale`: at least one represented file is confirmed changed, but comparison evidence is incomplete (a partial snapshot or unresolved comparisons).
552
+ - `unknown`: no confirmed change established staleness, but evidence is insufficient to prove freshness (an unavailable or partial snapshot, or an unresolved comparison). It is never treated as `fresh`.
553
+ - **`unknown` versus not assessed.** `unknown` means an assessment ran and could not prove freshness. A task with no assessment (for example a report from before v0.6.0) is shown as `not assessed` and counted as unassessed, never as `unknown`.
554
+ - **Report-level counts.** `report.warmIndexReuse.indexFreshnessSummary` holds direct presentation counts of assessed, unassessed, `fresh`, `stale`, `partially-stale`, and `unknown` tasks (assessed equals the sum of the four statuses). They are counts, not `ExperimentMetric` formulas: no percentage, score, or ranking is calculated. Each task's `indexFreshness` lists at most 20 changed files and 10 unresolved comparisons, with the persisted totals and the omitted count; hashes stay in `warm-index-execution.json`.
555
+ - **Observational only.** Freshness does not trigger reindexing, suppress or alter warm retrieval, or change execution status, correctness, provider or agent outcome status, or token evidence. v0.6.0 makes no reindex recommendation.
556
+
557
+ ### Affected-neighborhood metrics (v0.6.1)
558
+
559
+ Status: released in v0.6.1. This evidence extends the warm-index evidence above without a new command, flag, plugin, plot, or gallery item.
560
+
561
+ For each task, after that task's freshness assessment and before its warm retrieval, the run maps the confirmed changed indexed files, and the baseline symbols they contain, onto the baseline my-dev-kit graph of the prepared warm index, takes the exactly one-hop neighborhood, maps the task's `expectedFiles` and `expectedSymbols` onto the same graph, and compares them. The assessment is persisted as the per-task `affectedNeighborhood` in `warm-index-execution.json`. The metric owner (`metrics.ts`) converts that persisted assessment into the six warm-side metrics below exactly once; the report renders the metric owner's objects and never recomputes them.
562
+
563
+ All six metrics are warm-side only (they are not on the raw-full-file side, because raw context construction does not own baseline index graph evidence), have source `derived`, and carry the standard availability contract: `available` with a finite value (zero is a valid available value) or `unavailable` with a reason. An absent or `null` assessment makes all six unavailable with the reason "No affected-neighborhood assessment was recorded for this task."; a `null` field in a performed assessment is unavailable with a reason naming the assessment, seed-mapping, graph, neighborhood, and task-mapping statuses. Missing evidence is never reported as zero.
564
+
565
+ - `changedFileCount` (`count`)
566
+ Meaning: the number of unique confirmed modified or missing files that the index snapshot represents.
567
+ Availability: available (including 0 for a fresh index) when freshness gave a basis for a changed-file list; unavailable when freshness is `unknown`.
568
+ Limitations: files the snapshot does not represent, including new files, are not counted.
569
+ - `changedSymbolCount` (`count`)
570
+ Meaning: the number of unique baseline symbol identities (file path plus symbol name) that belong to confirmed changed indexed files, read from the baseline symbol index.
571
+ Limitations: it does not prove that any individual counted symbol's own source text changed; it means the symbol was present in a changed indexed file. A symbol counts even when its graph node cannot be resolved, so mapping completeness is separate from this count. Duplicate same-file, same-name records count once. It is unavailable when the baseline symbol index is unavailable.
572
+ - `affectedNodeCount` (`count`)
573
+ Meaning: the number of unique graph node IDs among the resolved seed nodes plus their direct one-hop neighbors.
574
+ Formula: exactly one hop, bidirectional (either endpoint of an edge may be the seed), over every structurally valid retained edge kind, with no weighting by kind. Unresolved seed identities are not counted as affected nodes.
575
+ Availability: 0 for a complete empty seed set (for example a fresh index); unavailable when the seed mapping or graph evidence is unavailable.
576
+ - `affectedEdgeCount` (`count`)
577
+ Meaning: the number of unique baseline graph edges incident to at least one resolved seed node (the edges actually followed by the one-hop expansion).
578
+ Limitations: it is not the number of edges among all affected nodes; an edge between two non-seed neighbors is not counted.
579
+ - `taskOverlapCount` (`count`)
580
+ Meaning: the number of unique graph nodes that are both in the affected neighborhood and among the task's resolved task nodes.
581
+ Availability: evaluable only when both the neighborhood and the task mapping are usable.
582
+ - `taskOverlapPercent` (`percent`)
583
+ Formula: `taskOverlapCount / resolvableTaskNodeCount * 100`, unrounded.
584
+ Denominator: `resolvableTaskNodeCount` is the number of unique graph nodes resolved from the task's `expectedFiles` (exact `file:<path>` nodes) and its unambiguous `expectedSymbols`. An expected symbol resolves only within the task's own expected files, only when exactly one candidate exists (zero is unresolved, several is ambiguous), and never by a global lookup. Unresolved and ambiguous metadata are excluded from the denominator.
585
+ Availability: unavailable, never zero, when there are no resolvable task nodes or overlap cannot be evaluated. When task mapping is partial, the percentage covers only the resolvable subset.
586
+
587
+ **Generic outcome metrics.** Available values appear as `ExperimentMetric` entries on the warm outcome only: `affected-neighborhood-changed-file-count`, `affected-neighborhood-changed-symbol-count`, `affected-neighborhood-node-count`, `affected-neighborhood-edge-count`, `affected-neighborhood-task-overlap-count`, and `affected-neighborhood-task-overlap-percent`. Unavailable metrics stay explicit in the structured metrics and the report but are omitted from these entries rather than emitted as zero. There is no run-level aggregate, average, ranking, or score for this evidence.
588
+
589
+ **Categorical evidence (not metrics).** The assessment also records, and the report presents, two categories that are deliberately not numeric `ExperimentMetric` entries:
590
+
591
+ - `relationship`: `related` when the confirmed overlap is greater than zero (even when other evidence is partial); `unrelated` only when the seed mapping, neighborhood, and task mapping are all complete and the overlap is zero; `unknown` for zero observed overlap under incomplete or unavailable evidence (for example partially-stale freshness, an unresolved changed file or symbol, a partial graph, an unresolved or ambiguous expected file or symbol, or no resolvable task nodes).
592
+ - `reindexRecommendation`: `recommended` for `related`, `not-indicated` for `unrelated`, and `unknown` for `unknown`. There is no freshness-only shortcut: a fresh index with an empty affected neighborhood and incomplete task mapping is `unknown` / `unknown`. `not-indicated` means only that this bounded analysis found no evidence supporting reindexing for that task; it does not mean skipping a reindex is universally safe, that the whole repository is fresh, that no new non-indexed file exists, or that reindexing could never help.
593
+ - The assessment `status` (`complete`, `partial`, or `unavailable`) says how complete the evidence was. A `partial` assessment can still be `related` and `recommended`. A `null` per-task value means no assessment was performed, an absent value means a pre-v0.6.1 artifact, and an assessment whose status is `unavailable` means it was performed but the evidence could not establish a usable result; these are reported separately.
594
+
595
+ **Report presentation.** `report.warmIndexReuse.affectedNeighborhoodSummary` holds presentation counts (assessed and unassessed tasks; complete, partial, and unavailable assessments; related, unrelated, and unknown relationships; recommended, not-indicated, and unknown recommendations). They are counts, not metric formulas. Each task's `affectedNeighborhood` block shows the statuses, the six metric objects, the relationship, the recommendation with a fixed explanation, and bounded lists (at most 20 affected node IDs, 20 edge IDs, 10 unresolved and 10 ambiguous task mappings, and 10 warnings, each with its total and omitted count). The full identity arrays stay in `warm-index-execution.json`.
596
+
597
+ **Observational only.** The evidence never triggers reindexing, suppresses or alters warm retrieval, or changes execution, provider, agent, correctness, or token-evidence status. It uses only the baseline graph of the prepared index: there is no graph-diff, refreshed-index, or stale-versus-refreshed comparison.
@@ -1,14 +1,14 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
-
5
- const sourceRoot = path.dirname(fileURLToPath(import.meta.url));
6
- const targetRoot = process.argv[2];
7
- if (!targetRoot) {
8
- console.error("prepare requires a target root argument");
9
- process.exit(2);
10
- }
11
- fs.mkdirSync(path.join(targetRoot, "app"), { recursive: true });
12
- fs.mkdirSync(path.join(targetRoot, "out"), { recursive: true });
13
- for (const file of ["index.html", "server.mjs"]) fs.copyFileSync(path.join(sourceRoot, file), path.join(targetRoot, "app", file));
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const sourceRoot = path.dirname(fileURLToPath(import.meta.url));
6
+ const targetRoot = process.argv[2];
7
+ if (!targetRoot) {
8
+ console.error("prepare requires a target root argument");
9
+ process.exit(2);
10
+ }
11
+ fs.mkdirSync(path.join(targetRoot, "app"), { recursive: true });
12
+ fs.mkdirSync(path.join(targetRoot, "out"), { recursive: true });
13
+ for (const file of ["index.html", "server.mjs"]) fs.copyFileSync(path.join(sourceRoot, file), path.join(targetRoot, "app", file));
14
14
  fs.writeFileSync(path.join(targetRoot, "out", "report.json"), JSON.stringify({ prepared: true, counts: { widgets: 3 } }, null, 2), "utf8");
@@ -1,14 +1,14 @@
1
- import http from "node:http";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- const root = path.dirname(fileURLToPath(import.meta.url));
7
- const port = Number(process.env.TUTORIAL_FIXTURE_PORT || 0);
8
- const server = http.createServer((req, res) => {
9
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
10
- if (url.pathname === "/api/status") { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ ok: true, widgets: 3 })); return; }
11
- const filePath = url.pathname === "/" ? path.join(root, "index.html") : path.join(root, url.pathname);
12
- fs.readFile(filePath, (error, contents) => { if (error) { res.writeHead(404); res.end("not found"); return; } res.writeHead(200, { "content-type": filePath.endsWith(".html") ? "text/html" : "application/octet-stream" }); res.end(contents); });
13
- });
1
+ import http from "node:http";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const root = path.dirname(fileURLToPath(import.meta.url));
7
+ const port = Number(process.env.TUTORIAL_FIXTURE_PORT || 0);
8
+ const server = http.createServer((req, res) => {
9
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
10
+ if (url.pathname === "/api/status") { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ ok: true, widgets: 3 })); return; }
11
+ const filePath = url.pathname === "/" ? path.join(root, "index.html") : path.join(root, url.pathname);
12
+ fs.readFile(filePath, (error, contents) => { if (error) { res.writeHead(404); res.end("not found"); return; } res.writeHead(200, { "content-type": filePath.endsWith(".html") ? "text/html" : "application/octet-stream" }); res.end(contents); });
13
+ });
14
14
  server.listen(port, "127.0.0.1", () => console.log("tutorial fixture listening on " + server.address().port));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dailephd/my-dev-kit-lab",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "description": "Evidence, benchmark, and evaluation companion for my-dev-kit.",
6
6
  "bin": {