@dailephd/my-dev-kit-lab 0.9.3 → 0.9.4
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
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to my-dev-kit-lab are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.9.4] - 2026-10-23
|
|
6
|
+
|
|
7
|
+
### Kit schema-3 and npm 12 compatibility
|
|
8
|
+
|
|
9
|
+
- Accepted my-dev-kit symbol-index schema `3` in addition to schema `2` for the `incremental-change-staleness` affected-neighborhood evidence. Schema-3 symbols use their persisted qualified `nodeId`; identities are not reconstructed from paths and simple names. Same-name symbols in one file stay distinct, ambiguous names are reported as ambiguous, and missing or mismatched identities stay unresolved. Other schema values are rejected, and schema-2 results are unchanged.
|
|
10
|
+
- Package content checks and `npm run verify:packed-package` accept the npm 11 and npm 12 `npm pack --json` formats through one validator that requires exactly one package, a nonempty file list, and safe relative paths.
|
|
11
|
+
- Corrected the packed-package verifier so a symlink or junction invocation runs, early failures print `PACKED_PACKAGE_VERDICT: FAIL`, and verified failures exit nonzero.
|
|
12
|
+
- Corrected the npm 12 output-prefix assumption in the target `test:security` test without weakening its assertions.
|
|
13
|
+
- Patched vulnerable development-only dependencies by pinning Vite to 7.3.7 and Vitest to 4.1.12 (with `@vitest/mocker` 4.1.12), which clears the dependency-audit advisories GHSA-587h-7wmj-h5pm, GHSA-qxg6-89mg-rqv8, and GHSA-xp26-3454-mjgp. The published runtime dependencies and package contents are unchanged.
|
|
14
|
+
- Security-verdict policy, report and execution schemas, commands, and package contents are unchanged.
|
|
15
|
+
|
|
5
16
|
## [0.9.3] - 2026-10-16
|
|
6
17
|
|
|
7
18
|
- Improved security-validator runtime evidence and secret-scan accuracy while retaining bounded proof and candidate-versus-confirmed finding distinctions.
|
package/README.md
CHANGED
|
@@ -6,21 +6,23 @@ 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
|
-
|
|
9
|
+
The v0.9.3 release added hardened prompt contracts and output validation, extensible reports with metric evidence, broader gallery inputs, and prompt-template comparison. 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.9.0 adds the `agent-success-rate` plugin, the eighth registered experiment plugin, which evaluates implementation patches under the `raw-full-file` and `context-pack` treatments.
|
|
12
12
|
|
|
13
|
-
v0.9.
|
|
13
|
+
v0.9.3 is the previous package release.
|
|
14
14
|
|
|
15
15
|
The v0.9.2 release corrected external npm package entrypoint validation, preserved strict Lab self-package validation, eliminated nine inherited false positives, corrected Semgrep failed/incomplete-scan evidence, and included campaign filesystem identity, path-safety, and cross-platform corrections. Security verdict strictness is unchanged. The v0.9.2 release-document state is final; actual npm publication is established separately through registry evidence.
|
|
16
16
|
|
|
17
17
|
The v0.9.3 release adds configurable Codex/Claude prompt contracts, strict structured-diff output handling, generic report sections, HTML metric evidence and caveats, source-based gallery items including tutorial manifests, and prompt-template comparison. See [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) for release and validation details.
|
|
18
18
|
|
|
19
|
+
The v0.9.4 release supports my-dev-kit symbol-index schema 3 alongside schema 2, preserves qualified symbol identities, and handles ambiguous names conservatively. Package inventory validation supports npm 11 and npm 12 formats, and the packed-package verifier reports early failures reliably. Development-only Vite and Vitest dependencies are patched; security-verdict policy is unchanged. See [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md), [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md), [docs/COMMANDS.md](docs/COMMANDS.md), and [docs/WORKFLOWS.md](docs/WORKFLOWS.md).
|
|
20
|
+
|
|
19
21
|
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.
|
|
20
22
|
|
|
21
23
|
## Current capabilities
|
|
22
24
|
|
|
23
|
-
Package metadata is `0.9.
|
|
25
|
+
Package metadata is `0.9.4`. The v0.9.4 release supports my-dev-kit symbol-index schema 3 alongside schema 2, preserves qualified identities, handles ambiguity conservatively, supports npm 11 and npm 12 package inventories, corrects packed-package verifier behavior, and patches development-only security dependencies. Security-verdict policy is unchanged. The v0.9.3 release added configurable Codex/Claude prompt contracts, strict structured-diff output handling, generic report sections, HTML metric evidence and caveats, source-based gallery items including tutorial manifests, and prompt-template comparison. Registered metrics have evidence-based glossary definitions and links, and prompt comparison validates persisted identity and checkpoint evidence. Real-provider improvement remains NOT_VERIFIED because no paid smoke was authorized; its measurement is a separate opt-in empirical objective, not a v0.9.3 release gate. Bounded tool-use remains prompt guidance, not provider enforcement. See [Current State](docs/CURRENT_STATE.md) and [the roadmap](docs/ROADMAP.md).
|
|
24
26
|
|
|
25
27
|
- **Run context-strategy experiments:** compare `raw-full-file` with `my-dev-kit-guided` using deterministic fixtures or locally configured Codex and Claude CLIs.
|
|
26
28
|
- **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.
|
|
@@ -4,7 +4,13 @@ import { compareCodeUnits, interpretIndexManifest, readJson } from "./indexSnaps
|
|
|
4
4
|
export const AFFECTED_NEIGHBORHOOD_GRAPH_EVIDENCE_SCHEMA_VERSION = "my-dev-kit-lab-affected-neighborhood-graph-evidence-v1";
|
|
5
5
|
export const AFFECTED_NEIGHBORHOOD_SEED_MAPPING_SCHEMA_VERSION = "my-dev-kit-lab-affected-neighborhood-seed-mapping-v1";
|
|
6
6
|
const MANIFEST_FILE = "manifest.json";
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Schema 2 (my-dev-kit 1.13.x) identifies a symbol by file path + name. Schema 3 (my-dev-kit 1.14.x)
|
|
9
|
+
* additionally persists each symbol's actual graph `nodeId`, which may be a distinct `symbolq:` identity
|
|
10
|
+
* for same-named members. Both are understood; anything else is rejected.
|
|
11
|
+
*/
|
|
12
|
+
const SUPPORTED_SYMBOL_INDEX_SCHEMA_VERSIONS = ["2", "3"];
|
|
13
|
+
const SCHEMA_WITH_PERSISTED_SYMBOL_NODE_IDS = "3";
|
|
8
14
|
const SUPPORTED_CODE_GRAPH_ARTIFACT_KIND = "code-graph";
|
|
9
15
|
const SUPPORTED_CODE_GRAPH_VERSION_PATTERN = /^1\./;
|
|
10
16
|
// Same bounds as the v0.6.0 freshness/snapshot evidence lists.
|
|
@@ -57,18 +63,23 @@ function sortWarnings(warnings) {
|
|
|
57
63
|
return [...warnings].sort((left, right) => compareCodeUnits(left.code, right.code) || compareCodeUnits(left.subject ?? "", right.subject ?? "") || compareCodeUnits(left.message, right.message));
|
|
58
64
|
}
|
|
59
65
|
/**
|
|
60
|
-
* Policy: reads indexed-file membership and baseline
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
66
|
+
* Policy: reads indexed-file membership and baseline symbol identities from a symbol-index artifact.
|
|
67
|
+
* Schema versions 2 and 3 are understood; anything else is unsupported. Under schema 2 a symbol's
|
|
68
|
+
* identity is its upstream file+name node ID. Under schema 3 it is the `nodeId` the index persisted,
|
|
69
|
+
* which is never reconstructed from the simple name: same-named members keep distinct identities,
|
|
70
|
+
* and a record without a usable `nodeId` stays visible as an unmappable identity with a warning
|
|
71
|
+
* instead of receiving a guessed one. A file entry whose symbol list is malformed keeps its
|
|
72
|
+
* membership but yields no symbol record; a malformed symbol entry is dropped with a warning. Pure.
|
|
64
73
|
*/
|
|
65
74
|
export function interpretSymbolIndexForNeighborhood(symbolIndex) {
|
|
66
75
|
if (!isRecord(symbolIndex)) {
|
|
67
76
|
return { ok: false, message: "Symbol index is not a JSON object." };
|
|
68
77
|
}
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
const schemaVersion = symbolIndex.schemaVersion;
|
|
79
|
+
if (typeof schemaVersion !== "string" || !SUPPORTED_SYMBOL_INDEX_SCHEMA_VERSIONS.includes(schemaVersion)) {
|
|
80
|
+
return { ok: false, message: `Symbol index schemaVersion is not a supported version (${SUPPORTED_SYMBOL_INDEX_SCHEMA_VERSIONS.join(", ")}).` };
|
|
71
81
|
}
|
|
82
|
+
const persistsNodeIds = schemaVersion === SCHEMA_WITH_PERSISTED_SYMBOL_NODE_IDS;
|
|
72
83
|
if (!Array.isArray(symbolIndex.files)) {
|
|
73
84
|
return { ok: false, message: "Symbol index does not list indexed files." };
|
|
74
85
|
}
|
|
@@ -92,29 +103,57 @@ export function interpretSymbolIndexForNeighborhood(symbolIndex) {
|
|
|
92
103
|
});
|
|
93
104
|
continue;
|
|
94
105
|
}
|
|
95
|
-
const
|
|
106
|
+
const identities = new Map();
|
|
96
107
|
const duplicates = new Set();
|
|
108
|
+
const missingNodeIdNames = new Set();
|
|
97
109
|
for (const symbol of entry.symbols) {
|
|
98
110
|
if (!isRecord(symbol) || !nonEmptyString(symbol.name)) {
|
|
99
111
|
warnings.push({ code: "invalid-symbol-entry", subject: boundedText(filePath), message: "Symbol index entry lacks a symbol name and was ignored." });
|
|
100
112
|
continue;
|
|
101
113
|
}
|
|
102
|
-
|
|
103
|
-
|
|
114
|
+
const name = symbol.name;
|
|
115
|
+
let identity;
|
|
116
|
+
let identityKey;
|
|
117
|
+
if (!persistsNodeIds) {
|
|
118
|
+
identity = { name, nodeId: symbolNodeId(filePath, name), persisted: false };
|
|
119
|
+
identityKey = identity.nodeId;
|
|
120
|
+
}
|
|
121
|
+
else if (nonEmptyString(symbol.nodeId)) {
|
|
122
|
+
identity = { name, nodeId: symbol.nodeId, persisted: true };
|
|
123
|
+
identityKey = `id:${symbol.nodeId}`;
|
|
104
124
|
}
|
|
105
|
-
|
|
125
|
+
else {
|
|
126
|
+
// No persisted identity: one unmappable identity per name, never a reconstructed one.
|
|
127
|
+
identity = { name, nodeId: null, persisted: true };
|
|
128
|
+
identityKey = `missing:${name}`;
|
|
129
|
+
missingNodeIdNames.add(name);
|
|
130
|
+
}
|
|
131
|
+
if (identities.has(identityKey)) {
|
|
132
|
+
duplicates.add(name);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
identities.set(identityKey, identity);
|
|
106
136
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
137
|
+
for (const name of [...missingNodeIdNames].sort(compareCodeUnits)) {
|
|
138
|
+
warnings.push({
|
|
139
|
+
code: "symbol-node-id-missing",
|
|
140
|
+
subject: boundedText(`${filePath}#${name}`),
|
|
141
|
+
message: "Schema-3 symbol record has no usable persisted nodeId; its identity is not guessed and it cannot be mapped to a graph node.",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const sortedRecords = [...identities.values()].sort((left, right) => compareCodeUnits(left.nodeId ?? "", right.nodeId ?? "") || compareCodeUnits(left.name, right.name));
|
|
145
|
+
const names = [...new Set(sortedRecords.map((record) => record.name))].sort(compareCodeUnits);
|
|
146
|
+
const sameName = names.filter((name) => sortedRecords.filter((record) => record.name === name).length > 1);
|
|
147
|
+
// Schema-2 evidence keeps its original shape: each name is its only identity, so no per-record data is added.
|
|
148
|
+
const fileRecord = { symbolNames: names, duplicateSymbolNames: [...duplicates].sort(compareCodeUnits) };
|
|
149
|
+
records.set(filePath, persistsNodeIds ? { ...fileRecord, records: sortedRecords, sameNameSymbolNames: sameName } : fileRecord);
|
|
111
150
|
}
|
|
112
151
|
const indexedFilePaths = [...paths].sort(compareCodeUnits);
|
|
113
152
|
const symbolFiles = new Map();
|
|
114
153
|
for (const key of [...records.keys()].sort(compareCodeUnits)) {
|
|
115
154
|
symbolFiles.set(key, records.get(key));
|
|
116
155
|
}
|
|
117
|
-
return { ok: true, value: { indexedFilePaths, symbolFiles, warnings } };
|
|
156
|
+
return { ok: true, value: { schemaVersion, indexedFilePaths, symbolFiles, warnings } };
|
|
118
157
|
}
|
|
119
158
|
/**
|
|
120
159
|
* Policy: validates the structure of a code-graph artifact and preserves its nodes and edges by
|
|
@@ -234,7 +273,7 @@ export async function loadAffectedNeighborhoodGraphEvidence(options) {
|
|
|
234
273
|
}
|
|
235
274
|
const symbolIndexSummary = {
|
|
236
275
|
path: normalizePath(path.relative(path.resolve(indexDir), symbolIndexFile)),
|
|
237
|
-
schemaVersion:
|
|
276
|
+
schemaVersion: symbols.value.schemaVersion,
|
|
238
277
|
};
|
|
239
278
|
const warnings = [...symbols.value.warnings];
|
|
240
279
|
let codeGraphSummary = null;
|
|
@@ -310,10 +349,35 @@ function lookupNode(evidence, expectedNodeId, kind) {
|
|
|
310
349
|
export function lookupFileNode(evidence, filePath) {
|
|
311
350
|
return lookupNode(evidence, fileNodeId(filePath), "file");
|
|
312
351
|
}
|
|
313
|
-
/**
|
|
352
|
+
/**
|
|
353
|
+
* Same-file symbol lookup by the upstream file+name identity. Never searches for a same-named symbol
|
|
354
|
+
* in another file. This is the schema-2 identity only; it does not find a qualified (`symbolq:`) member.
|
|
355
|
+
*/
|
|
314
356
|
export function lookupSymbolNode(evidence, filePath, symbolName) {
|
|
315
357
|
return lookupNode(evidence, symbolNodeId(filePath, symbolName), "symbol");
|
|
316
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Exact lookup of one baseline symbol identity. The node must exist under the identity the index
|
|
361
|
+
* actually recorded, be a `symbol` node, and (when the graph names a path) belong to the same
|
|
362
|
+
* file; otherwise it is unresolved. A record without a persisted identity is never mapped, and no
|
|
363
|
+
* identity is derived from its name.
|
|
364
|
+
*/
|
|
365
|
+
export function lookupSymbolRecordNode(evidence, filePath, record) {
|
|
366
|
+
if (record.nodeId === null)
|
|
367
|
+
return { ok: false, expectedNodeId: "", reason: "symbol-node-id-missing" };
|
|
368
|
+
const lookup = lookupNode(evidence, record.nodeId, "symbol");
|
|
369
|
+
if (!lookup.ok)
|
|
370
|
+
return lookup;
|
|
371
|
+
const node = evidence.nodes.get(lookup.nodeId);
|
|
372
|
+
if (node && node.path !== null && normalizePath(node.path) !== filePath) {
|
|
373
|
+
return { ok: false, expectedNodeId: record.nodeId, reason: "graph-node-path-mismatch" };
|
|
374
|
+
}
|
|
375
|
+
return lookup;
|
|
376
|
+
}
|
|
377
|
+
/** The baseline identities of one indexed file; evidence without per-record data keeps the schema-2 file+name identity. */
|
|
378
|
+
function symbolRecordsOf(filePath, file) {
|
|
379
|
+
return file.records ?? file.symbolNames.map((name) => ({ name, nodeId: symbolNodeId(filePath, name), persisted: false }));
|
|
380
|
+
}
|
|
317
381
|
/**
|
|
318
382
|
* Maps confirmed changed indexed files, and the baseline symbols belonging to them, onto baseline
|
|
319
383
|
* graph nodes. Pure and deterministic: consumes only the v0.6.0 snapshot and freshness assessment
|
|
@@ -404,18 +468,21 @@ export function mapAffectedNeighborhoodSeeds(input) {
|
|
|
404
468
|
continue;
|
|
405
469
|
}
|
|
406
470
|
if (record.duplicateSymbolNames.length > 0) {
|
|
407
|
-
warn("duplicate-symbol-records", `Baseline symbol index
|
|
471
|
+
warn("duplicate-symbol-records", `Baseline symbol index repeats ${record.duplicateSymbolNames.length} identical symbol record(s); each counts as one identity.`, filePath);
|
|
408
472
|
}
|
|
409
|
-
|
|
473
|
+
// Each distinct baseline identity is a seed in its own right: same-named members that the index
|
|
474
|
+
// persisted with different node IDs are never merged, and none is reconstructed from the name.
|
|
475
|
+
for (const identity of symbolRecordsOf(filePath, record)) {
|
|
476
|
+
const name = identity.name;
|
|
410
477
|
symbolIdentityCount += 1;
|
|
411
|
-
const symbolExpectedId =
|
|
478
|
+
const symbolExpectedId = identity.nodeId ?? "";
|
|
412
479
|
let symbolResolvedId = null;
|
|
413
480
|
let symbolReason = null;
|
|
414
481
|
if (!codeGraphLoaded) {
|
|
415
482
|
symbolReason = "graph-unavailable";
|
|
416
483
|
}
|
|
417
484
|
else {
|
|
418
|
-
const lookup =
|
|
485
|
+
const lookup = lookupSymbolRecordNode(graph, filePath, identity);
|
|
419
486
|
if (lookup.ok)
|
|
420
487
|
symbolResolvedId = lookup.nodeId;
|
|
421
488
|
else
|
|
@@ -426,7 +493,12 @@ export function mapAffectedNeighborhoodSeeds(input) {
|
|
|
426
493
|
seeds.add(symbolResolvedId);
|
|
427
494
|
}
|
|
428
495
|
else {
|
|
429
|
-
unresolved.push({
|
|
496
|
+
unresolved.push({
|
|
497
|
+
subject: "changed-symbol",
|
|
498
|
+
path: boundedText(filePath),
|
|
499
|
+
name: boundedText(name),
|
|
500
|
+
reason: symbolReason,
|
|
501
|
+
});
|
|
430
502
|
}
|
|
431
503
|
changedSymbols.push({
|
|
432
504
|
path: filePath,
|
|
@@ -490,11 +562,41 @@ function uniqueSorted(values) {
|
|
|
490
562
|
function normalizeExpectedPath(value) {
|
|
491
563
|
return normalizePath(value).replace(/^(\.\/)+/, "");
|
|
492
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* Candidate graph nodes for one expected symbol in one expected file. A request matches a baseline
|
|
567
|
+
* identity by its simple name or by that identity's exact persisted node ID; every matching identity
|
|
568
|
+
* is a distinct candidate (same-named members are never collapsed). A file the symbol index has no
|
|
569
|
+
* record for keeps the schema-2 file+name lookup. Nothing is derived from the name beyond that.
|
|
570
|
+
*/
|
|
571
|
+
function resolveExpectedSymbolInFile(graph, filePath, request) {
|
|
572
|
+
const file = graph.symbolFiles.get(filePath);
|
|
573
|
+
if (!file) {
|
|
574
|
+
const lookup = lookupSymbolNode(graph, filePath, request);
|
|
575
|
+
return { nodeIds: lookup.ok ? [lookup.nodeId] : [], identityMissing: false, pathMismatch: !lookup.ok && lookup.reason === "graph-node-path-mismatch" };
|
|
576
|
+
}
|
|
577
|
+
const nodeIds = [];
|
|
578
|
+
let identityMissing = false;
|
|
579
|
+
let pathMismatch = false;
|
|
580
|
+
for (const identity of symbolRecordsOf(filePath, file)) {
|
|
581
|
+
if (identity.name !== request && identity.nodeId !== request)
|
|
582
|
+
continue;
|
|
583
|
+
const lookup = lookupSymbolRecordNode(graph, filePath, identity);
|
|
584
|
+
if (lookup.ok)
|
|
585
|
+
nodeIds.push(lookup.nodeId);
|
|
586
|
+
else if (lookup.reason === "symbol-node-id-missing")
|
|
587
|
+
identityMissing = true;
|
|
588
|
+
else if (lookup.reason === "graph-node-path-mismatch")
|
|
589
|
+
pathMismatch = true;
|
|
590
|
+
}
|
|
591
|
+
return { nodeIds, identityMissing, pathMismatch };
|
|
592
|
+
}
|
|
493
593
|
/**
|
|
494
594
|
* Policy: maps a task's expected files and symbols to baseline graph nodes. Files resolve only to
|
|
495
595
|
* their exact `file:` node. A symbol resolves only within the task's own expected files and only
|
|
496
|
-
* when exactly one
|
|
497
|
-
*
|
|
596
|
+
* when exactly one matching identity exists there; zero matches is unresolved and several (in one
|
|
597
|
+
* file or across files, including same-named members with distinct persisted node IDs) is
|
|
598
|
+
* ambiguous (no winner is picked, no node is added). A request that is itself a persisted node ID
|
|
599
|
+
* resolves to exactly that identity. Never searches the rest of the graph. Pure.
|
|
498
600
|
*/
|
|
499
601
|
export function mapAffectedNeighborhoodTask(input) {
|
|
500
602
|
const { graph } = input;
|
|
@@ -561,15 +663,21 @@ export function mapAffectedNeighborhoodTask(input) {
|
|
|
561
663
|
const resolvedSymbols = [];
|
|
562
664
|
for (const name of expectedSymbols) {
|
|
563
665
|
// Scope is the task's expected files, whether or not their own file node resolved.
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
666
|
+
const found = expectedFiles.map((filePath) => resolveExpectedSymbolInFile(graph, filePath, name));
|
|
667
|
+
const candidates = uniqueSorted(found.flatMap((entry) => entry.nodeIds));
|
|
668
|
+
// A same-name identity that persisted no node ID cannot be excluded, so a lone mappable candidate
|
|
669
|
+
// is not trusted as "the" owner when such a sibling exists.
|
|
670
|
+
const identityMissing = found.some((entry) => entry.identityMissing);
|
|
671
|
+
if (candidates.length === 1 && !identityMissing) {
|
|
569
672
|
resolvedSymbols.push({ name, nodeId: candidates[0] });
|
|
570
673
|
}
|
|
571
|
-
else if (candidates.length
|
|
572
|
-
|
|
674
|
+
else if (candidates.length <= 1) {
|
|
675
|
+
const reason = identityMissing
|
|
676
|
+
? "symbol-node-id-missing"
|
|
677
|
+
: found.some((entry) => entry.pathMismatch)
|
|
678
|
+
? "graph-node-path-mismatch"
|
|
679
|
+
: "graph-node-missing";
|
|
680
|
+
unresolved.push({ subject: "expected-symbol", name: boundedText(name), reason });
|
|
573
681
|
}
|
|
574
682
|
else {
|
|
575
683
|
ambiguous.push({ name: boundedText(name), candidateNodeIds: candidates });
|
|
@@ -14,27 +14,13 @@ export function parseNpmPackDryRun(stdout) {
|
|
|
14
14
|
return { files: [], parseError: "npm pack --dry-run produced no output" };
|
|
15
15
|
}
|
|
16
16
|
if (/^[\[{]/.test(stdout.trim())) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
for (const file of inventory[0].files) {
|
|
25
|
-
if (!file || typeof file.path !== "string" || !safeInventoryPath(file.path)) {
|
|
26
|
-
return { files: [], parseError: "npm pack JSON contains a malformed or unsafe file path" };
|
|
27
|
-
}
|
|
28
|
-
// JSON paths are package-relative, unlike text tarball paths. Add the
|
|
29
|
-
// archive root exactly once so a real nested package/ directory cannot
|
|
30
|
-
// masquerade as a root entrypoint in the existing detectors.
|
|
31
|
-
files.push(`package/${file.path.replace(/\\/g, "/").replace(/^(\.\/)+/, "")}`);
|
|
32
|
-
}
|
|
33
|
-
return { files };
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
return { files: [], parseError: "npm pack JSON is unparseable" };
|
|
37
|
-
}
|
|
17
|
+
const parsed = parseNpmPackJsonEntry(stdout);
|
|
18
|
+
if (!parsed.entry)
|
|
19
|
+
return { files: [], parseError: parsed.parseError };
|
|
20
|
+
// JSON paths are package-relative, unlike text tarball paths. Add the
|
|
21
|
+
// archive root exactly once so a real nested package/ directory cannot
|
|
22
|
+
// masquerade as a root entrypoint in the existing detectors.
|
|
23
|
+
return { files: parsed.entry.files.map((file) => `package/${file}`) };
|
|
38
24
|
}
|
|
39
25
|
const files = [];
|
|
40
26
|
let inContents = false;
|
|
@@ -78,6 +64,45 @@ export function parseNpmPackDryRun(stdout) {
|
|
|
78
64
|
}
|
|
79
65
|
return { files, totalSize };
|
|
80
66
|
}
|
|
67
|
+
// Validated single-package entry from `npm pack --json` (or --dry-run --json) output, accepting both
|
|
68
|
+
// the npm 11 array shape and the npm 12 identity-keyed object shape. Exactly one package with a
|
|
69
|
+
// nonempty, safe file list is required; anything else is an error, never an empty success.
|
|
70
|
+
export function parseNpmPackJsonEntry(stdout) {
|
|
71
|
+
try {
|
|
72
|
+
const packages = packageInventories(JSON.parse(stdout));
|
|
73
|
+
const inventory = packages?.length === 1 ? packages[0] : null;
|
|
74
|
+
if (!isPlainObject(inventory) || !Array.isArray(inventory.files) || !inventory.files.length) {
|
|
75
|
+
return { parseError: "npm pack JSON must contain exactly one nonempty package inventory" };
|
|
76
|
+
}
|
|
77
|
+
const files = [];
|
|
78
|
+
for (const file of inventory.files) {
|
|
79
|
+
if (!file || typeof file.path !== "string" || !safeInventoryPath(file.path)) {
|
|
80
|
+
return { parseError: "npm pack JSON contains a malformed or unsafe file path" };
|
|
81
|
+
}
|
|
82
|
+
files.push(file.path.replace(/\\/g, "/").replace(/^(\.\/)+/, ""));
|
|
83
|
+
}
|
|
84
|
+
const text = (value) => (typeof value === "string" ? value : undefined);
|
|
85
|
+
return { entry: { name: text(inventory.name), version: text(inventory.version), filename: text(inventory.filename), files } };
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return { parseError: "npm pack JSON is unparseable" };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function isPlainObject(value) {
|
|
92
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
93
|
+
}
|
|
94
|
+
// The package inventories in `npm pack --json` output. npm 11 and earlier print an array of package
|
|
95
|
+
// objects; npm 12 prints one object keyed by package identity whose values are the package objects.
|
|
96
|
+
// Anything else (including a bare package object, or a keyed object holding non-objects) is not a
|
|
97
|
+
// recognized shape and yields null, so it can never be read as an empty or successful package.
|
|
98
|
+
function packageInventories(output) {
|
|
99
|
+
if (Array.isArray(output))
|
|
100
|
+
return output;
|
|
101
|
+
if (!isPlainObject(output))
|
|
102
|
+
return null;
|
|
103
|
+
const packages = Object.values(output);
|
|
104
|
+
return packages.every(isPlainObject) ? packages : null;
|
|
105
|
+
}
|
|
81
106
|
function safeInventoryPath(file) {
|
|
82
107
|
const normalized = file.replace(/\\/g, "/").replace(/^(\.\/)+/, "");
|
|
83
108
|
return Boolean(normalized) && !normalized.startsWith("/") &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dailephd/my-dev-kit-lab",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Evidence, benchmark, and evaluation companion for my-dev-kit.",
|
|
6
6
|
"bin": {
|
|
@@ -93,10 +93,10 @@
|
|
|
93
93
|
"@types/node": "^24.0.0",
|
|
94
94
|
"tsx": "^4.20.3",
|
|
95
95
|
"typescript": "^5.8.3",
|
|
96
|
-
"vitest": "
|
|
96
|
+
"vitest": "4.1.12"
|
|
97
97
|
},
|
|
98
98
|
"overrides": {
|
|
99
|
-
"vite": "7.3.
|
|
99
|
+
"vite": "7.3.7"
|
|
100
100
|
},
|
|
101
101
|
"engines": {
|
|
102
102
|
"node": ">=24"
|