@dailephd/my-dev-kit-lab 0.7.1 → 0.8.0
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 +28 -0
- package/README.md +10 -5
- package/benchmarks/contracts/warm-index-benchmark-cases.json +330 -0
- package/dist/src/cli/help.js +27 -4
- package/dist/src/commands/runExperimentDescribeCommand.js +13 -2
- package/dist/src/commands/runExperimentRunCommand.js +210 -20
- package/dist/src/core/fileGlobs.js +44 -0
- package/dist/src/evaluation/benchmarkMetadata.js +13 -0
- package/dist/src/evaluation/localRepositorySubject/config.js +296 -0
- package/dist/src/evaluation/localRepositorySubject/evaluationCase.js +36 -0
- package/dist/src/evaluation/localRepositorySubject/expectedFiles.js +51 -0
- package/dist/src/evaluation/localRepositorySubject/gitRepository.js +128 -0
- package/dist/src/evaluation/localRepositorySubject/index.js +8 -0
- package/dist/src/evaluation/localRepositorySubject/inventory.js +127 -0
- package/dist/src/evaluation/localRepositorySubject/loadLocalRepositorySubject.js +48 -0
- package/dist/src/evaluation/localRepositorySubject/manifest.js +52 -0
- package/dist/src/evaluation/localRepositorySubject/types.js +55 -0
- package/dist/src/evaluation/projectFileTree.js +45 -1
- package/dist/src/evaluation/retrievalQuality/buildRetrievalEvidence.js +292 -0
- package/dist/src/evaluation/retrievalQuality/corpusCompleteness.js +122 -0
- package/dist/src/evaluation/retrievalQuality/factContextTargets.js +44 -0
- package/dist/src/evaluation/retrievalQuality/index.js +5 -0
- package/dist/src/evaluation/retrievalQuality/metrics.js +233 -0
- package/dist/src/evaluation/retrievalQuality/types.js +9 -0
- package/dist/src/evaluation/runMyDevKitRetrieval.js +6 -1
- package/dist/src/evaluation/runRawFullFileBaseline.js +18 -4
- package/dist/src/evaluation/targetImmutability/captureTargetSnapshot.js +29 -7
- package/dist/src/evaluation/targetImmutability/compareTargetSnapshots.js +4 -0
- package/dist/src/experiments/defaultRegistry.js +2 -0
- package/dist/src/experiments/plugins/contextWindowScaling/localSubjectErrors.js +35 -0
- package/dist/src/experiments/plugins/contextWindowScaling/localSubjectExclusions.js +75 -0
- package/dist/src/experiments/plugins/contextWindowScaling/localSubjectExecution.js +136 -0
- package/dist/src/experiments/plugins/contextWindowScaling/localSubjectPrivacy.js +128 -0
- package/dist/src/experiments/plugins/contextWindowScaling/localSubjectScratch.js +85 -0
- package/dist/src/experiments/plugins/contextWindowScaling/metadata.js +2 -2
- package/dist/src/experiments/plugins/contextWindowScaling/plugin.js +93 -0
- package/dist/src/experiments/plugins/contextWindowScaling/projectProfile.js +4 -9
- package/dist/src/experiments/plugins/index.js +1 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/config.js +39 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/execution.js +152 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/executionArtifact.js +15 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/index.js +9 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/localSubjectExecution.js +139 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/localSubjectPrivacy.js +119 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/metadata.js +21 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/metrics.js +158 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/plugin.js +253 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/types.js +8 -0
- package/dist/src/report/experiments/buildPluginExperimentReport.js +21 -2
- package/dist/src/report/experiments/buildRetrievalPrecisionRecallReport.js +83 -0
- package/dist/src/report/experiments/index.js +4 -0
- package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +7 -3
- package/dist/src/report/experiments/renderPluginExperimentReportText.js +9 -0
- package/dist/src/report/experiments/renderRetrievalPrecisionRecallHtml.js +96 -0
- package/dist/src/report/experiments/renderRetrievalPrecisionRecallText.js +121 -0
- package/dist/src/report/experiments/retrievalPrecisionRecallReportModel.js +3 -0
- package/dist/src/report/experiments/writePluginExperimentReports.js +8 -2
- package/docs/METRICS.md +34 -2
- package/package.json +1 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveWithinRoot } from "../../../core/pathSafety.js";
|
|
4
|
+
import { serializeLocalRepositorySubjectManifest } from "../../../evaluation/localRepositorySubject/index.js";
|
|
5
|
+
import { summarizeExperimentRun } from "../../results.js";
|
|
6
|
+
import { LocalSubjectExecutionError } from "../contextWindowScaling/localSubjectErrors.js";
|
|
7
|
+
import { projectExternalLocalTarget } from "../contextWindowScaling/localSubjectPrivacy.js";
|
|
8
|
+
import { isSamePhysicalDirectory } from "../contextWindowScaling/localSubjectScratch.js";
|
|
9
|
+
import { selectWarmIndexCases } from "../warmIndexReuse/selection.js";
|
|
10
|
+
import { defaultRetrievalPrecisionRecallConfig, retrievalPrecisionRecallConfigDefinition, validateRetrievalPrecisionRecallConfig } from "./config.js";
|
|
11
|
+
import { executeRetrievalPrecisionRecall } from "./execution.js";
|
|
12
|
+
import { executeLocalRepositorySubjectRetrievalPrecisionRecall } from "./localSubjectExecution.js";
|
|
13
|
+
import { assertExternalLocalProjectionIsPrivate, describeRetrievalLocalSubjectFailureForPersistence, projectRetrievalEvidenceForExternalLocalPersistence, UNEXPECTED_EXTERNAL_FAILURE_MESSAGE } from "./localSubjectPrivacy.js";
|
|
14
|
+
import { buildRetrievalPrecisionRecallExecutionArtifact, RETRIEVAL_PRECISION_RECALL_EXECUTION_ARTIFACT_FILE } from "./executionArtifact.js";
|
|
15
|
+
import { aggregateRetrievalPrecisionRecall, retrievalRatioOf, toRetrievalPrecisionRecallOutcomeMetrics, toRetrievalPrecisionRecallRunMetrics } from "./metrics.js";
|
|
16
|
+
import { RETRIEVAL_PRECISION_RECALL_PLUGIN_ID, RETRIEVAL_PRECISION_RECALL_VARIANTS, RETRIEVAL_PRECISION_RECALL_VARIANT_ID, retrievalPrecisionRecallMetadata } from "./metadata.js";
|
|
17
|
+
import { RETRIEVAL_PRECISION_RECALL_RATIO_KEYS } from "./types.js";
|
|
18
|
+
/** Privacy-safe Batch 1 manifest persisted beside the execution artifact for external-local runs (same file as context-window-scaling). */
|
|
19
|
+
export const RETRIEVAL_PRECISION_RECALL_LOCAL_SUBJECT_MANIFEST_FILE = "local-repository-subject-manifest.json";
|
|
20
|
+
/** An external target must always arrive with a loaded local repository subject; there is no bundled fallback against it. */
|
|
21
|
+
export const RETRIEVAL_PRECISION_RECALL_EXTERNAL_REQUIRES_SUBJECT_MESSAGE = "External-local retrieval-precision-recall targets require a loaded local repository subject (--local-subject-config).";
|
|
22
|
+
export const retrievalPrecisionRecallPlugin = {
|
|
23
|
+
metadata: retrievalPrecisionRecallMetadata,
|
|
24
|
+
defaultConfig: defaultRetrievalPrecisionRecallConfig,
|
|
25
|
+
configDefinition: retrievalPrecisionRecallConfigDefinition,
|
|
26
|
+
supportedVariants: RETRIEVAL_PRECISION_RECALL_VARIANTS.map((variant) => variant.id),
|
|
27
|
+
validateConfig: validateRetrievalPrecisionRecallConfig,
|
|
28
|
+
async run(context) {
|
|
29
|
+
const startedAt = context.startedAt.toISOString();
|
|
30
|
+
// The runner does not enforce supportedTargets, so the plugin fails closed on its own.
|
|
31
|
+
const localSubject = readLocalSubjectInput(context.inputs);
|
|
32
|
+
if (localSubject)
|
|
33
|
+
return runLocalSubject(context, localSubject, startedAt);
|
|
34
|
+
if (context.target.kind !== "self" || context.target.isSelf !== true) {
|
|
35
|
+
// Never fall back to the bundled corpus against an external repository.
|
|
36
|
+
throw new Error(RETRIEVAL_PRECISION_RECALL_EXTERNAL_REQUIRES_SUBJECT_MESSAGE);
|
|
37
|
+
}
|
|
38
|
+
const cases = selectWarmIndexCases(readCasesInput(context.inputs), context.config);
|
|
39
|
+
const outDir = context.outputRoot ?? path.resolve(context.toolRoot, context.config.outDir);
|
|
40
|
+
await mkdir(outDir, { recursive: true });
|
|
41
|
+
const caseEvidence = await executeRetrievalPrecisionRecall({
|
|
42
|
+
cases,
|
|
43
|
+
kitCommand: context.config.kitCommand,
|
|
44
|
+
outputRoot: outDir,
|
|
45
|
+
dependencies: readDependenciesInput(context.inputs)
|
|
46
|
+
});
|
|
47
|
+
// Scientific truth is calculated before persistence; the artifact and every report consume it unchanged.
|
|
48
|
+
const aggregate = aggregateRetrievalPrecisionRecall(caseEvidence);
|
|
49
|
+
const completedAt = new Date().toISOString();
|
|
50
|
+
const artifact = buildRetrievalPrecisionRecallExecutionArtifact({
|
|
51
|
+
runId: context.runId,
|
|
52
|
+
pluginId: RETRIEVAL_PRECISION_RECALL_PLUGIN_ID,
|
|
53
|
+
pluginSchemaVersion: retrievalPrecisionRecallMetadata.schemaVersion,
|
|
54
|
+
startedAt,
|
|
55
|
+
completedAt,
|
|
56
|
+
cases: caseEvidence,
|
|
57
|
+
aggregate
|
|
58
|
+
});
|
|
59
|
+
const artifactPath = resolveWithinRoot(outDir, RETRIEVAL_PRECISION_RECALL_EXECUTION_ARTIFACT_FILE);
|
|
60
|
+
await writeFile(artifactPath, `${JSON.stringify(artifact, null, 2)}\n`, "utf8");
|
|
61
|
+
return mapRetrievalPrecisionRecallToRun({
|
|
62
|
+
runId: context.runId,
|
|
63
|
+
startedAt,
|
|
64
|
+
completedAt,
|
|
65
|
+
target: context.target,
|
|
66
|
+
caseEvidence,
|
|
67
|
+
aggregate,
|
|
68
|
+
artifactPath
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
summarize(result) {
|
|
72
|
+
return result.summary ?? summarizeExperimentRun(result);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
function readLocalSubjectInput(inputs) {
|
|
76
|
+
const value = inputs?.localSubject;
|
|
77
|
+
return value && typeof value === "object" ? value : undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* External-local path. The safe seam owns execution, immutability and scratch cleanup and either returns evidence or
|
|
81
|
+
* throws; only after it succeeds is anything durable written: the case evidence is privacy-projected, the aggregate
|
|
82
|
+
* (calculated from the unprojected numbers) is preserved, and the artifact and manifest are written, in that order.
|
|
83
|
+
*/
|
|
84
|
+
async function runLocalSubject(context, subject, startedAt) {
|
|
85
|
+
if (context.target.isSelf || context.target.kind !== "external-local") {
|
|
86
|
+
throw new Error("Local-repository subject mode requires an external-local target.");
|
|
87
|
+
}
|
|
88
|
+
if (context.config.caseIds !== undefined || context.config.benchmarkProjects !== undefined) {
|
|
89
|
+
throw new Error("Case and benchmark-project filters are not supported for an external local repository subject; the subject config owns the case set.");
|
|
90
|
+
}
|
|
91
|
+
if (!(await isSamePhysicalDirectory(context.target.targetRoot, subject.repositoryRoot))) {
|
|
92
|
+
throw new Error("The selected --target is not the repository the local subject was loaded from.");
|
|
93
|
+
}
|
|
94
|
+
const outDir = context.outputRoot;
|
|
95
|
+
if (!outDir)
|
|
96
|
+
throw new Error("Local-repository subject mode requires an experiment output directory.");
|
|
97
|
+
let localResult;
|
|
98
|
+
try {
|
|
99
|
+
localResult = await executeLocalRepositorySubjectRetrievalPrecisionRecall({
|
|
100
|
+
subject,
|
|
101
|
+
kitCommand: context.config.kitCommand,
|
|
102
|
+
workRoot: outDir,
|
|
103
|
+
dependencies: readDependenciesInput(context.inputs)
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error instanceof LocalSubjectExecutionError)
|
|
108
|
+
throw new Error(describeRetrievalLocalSubjectFailureForPersistence(error));
|
|
109
|
+
throw new Error(UNEXPECTED_EXTERNAL_FAILURE_MESSAGE);
|
|
110
|
+
}
|
|
111
|
+
// Scientific numbers are calculated once from the unprojected evidence; projection only withholds identities.
|
|
112
|
+
const aggregate = aggregateRetrievalPrecisionRecall(localResult.caseEvidence);
|
|
113
|
+
const projected = projectRetrievalEvidenceForExternalLocalPersistence(localResult.caseEvidence);
|
|
114
|
+
assertExternalLocalProjectionIsPrivate(projected, {
|
|
115
|
+
filePaths: [...subject.eligibleFiles, ...subject.runtimeSafetyExclusions.gitIgnoredFiles, ...subject.runtimeSafetyExclusions.oversizedFiles],
|
|
116
|
+
caseTitles: subject.evaluationCases.map((evaluationCase) => evaluationCase.title),
|
|
117
|
+
warnings: localResult.caseEvidence.flatMap((entry) => entry.retrieval?.warnings ?? [])
|
|
118
|
+
});
|
|
119
|
+
await mkdir(outDir, { recursive: true });
|
|
120
|
+
const completedAt = new Date().toISOString();
|
|
121
|
+
const artifact = buildRetrievalPrecisionRecallExecutionArtifact({
|
|
122
|
+
runId: context.runId,
|
|
123
|
+
pluginId: RETRIEVAL_PRECISION_RECALL_PLUGIN_ID,
|
|
124
|
+
pluginSchemaVersion: retrievalPrecisionRecallMetadata.schemaVersion,
|
|
125
|
+
startedAt,
|
|
126
|
+
completedAt,
|
|
127
|
+
cases: projected,
|
|
128
|
+
aggregate
|
|
129
|
+
});
|
|
130
|
+
const artifactPath = resolveWithinRoot(outDir, RETRIEVAL_PRECISION_RECALL_EXECUTION_ARTIFACT_FILE);
|
|
131
|
+
await writeFile(artifactPath, `${JSON.stringify(artifact, null, 2)}
|
|
132
|
+
`, "utf8");
|
|
133
|
+
const manifestPath = resolveWithinRoot(outDir, RETRIEVAL_PRECISION_RECALL_LOCAL_SUBJECT_MANIFEST_FILE);
|
|
134
|
+
await writeFile(manifestPath, serializeLocalRepositorySubjectManifest(subject.manifest), "utf8");
|
|
135
|
+
const run = mapRetrievalPrecisionRecallToRun({
|
|
136
|
+
runId: context.runId,
|
|
137
|
+
startedAt,
|
|
138
|
+
completedAt,
|
|
139
|
+
target: projectExternalLocalTarget(subject.manifest),
|
|
140
|
+
caseEvidence: projected,
|
|
141
|
+
aggregate,
|
|
142
|
+
artifactPath
|
|
143
|
+
});
|
|
144
|
+
run.artifacts.push({
|
|
145
|
+
id: "local-repository-subject-manifest",
|
|
146
|
+
label: "Privacy-safe local repository subject manifest",
|
|
147
|
+
path: manifestPath,
|
|
148
|
+
kind: "artifact",
|
|
149
|
+
mimeType: "application/json",
|
|
150
|
+
description: "Privacy-safe logical subject, Git, safety and aggregate inventory metadata without source or private identity lists."
|
|
151
|
+
});
|
|
152
|
+
return run;
|
|
153
|
+
}
|
|
154
|
+
export function mapRetrievalPrecisionRecallToRun(args) {
|
|
155
|
+
const cases = args.caseEvidence.map((entry) => ({
|
|
156
|
+
id: entry.caseId,
|
|
157
|
+
name: entry.caseName,
|
|
158
|
+
outcomes: [buildOutcome(entry)],
|
|
159
|
+
metadata: { benchmarkProject: entry.benchmarkProject, taskLocality: entry.taskLocality }
|
|
160
|
+
}));
|
|
161
|
+
const failures = args.caseEvidence.flatMap((entry) => entry.errors.map((error) => ({
|
|
162
|
+
code: error.code,
|
|
163
|
+
message: error.message,
|
|
164
|
+
variantId: RETRIEVAL_PRECISION_RECALL_VARIANT_ID,
|
|
165
|
+
caseId: entry.caseId,
|
|
166
|
+
recoverable: true
|
|
167
|
+
})));
|
|
168
|
+
const run = {
|
|
169
|
+
runId: args.runId,
|
|
170
|
+
pluginId: RETRIEVAL_PRECISION_RECALL_PLUGIN_ID,
|
|
171
|
+
startedAt: args.startedAt,
|
|
172
|
+
completedAt: args.completedAt,
|
|
173
|
+
status: aggregateStatus(args.caseEvidence.map((entry) => entry.status)),
|
|
174
|
+
target: args.target,
|
|
175
|
+
variants: RETRIEVAL_PRECISION_RECALL_VARIANTS.map((variant) => ({ ...variant })),
|
|
176
|
+
cases,
|
|
177
|
+
metrics: toRetrievalPrecisionRecallRunMetrics(args.aggregate),
|
|
178
|
+
artifacts: [
|
|
179
|
+
{
|
|
180
|
+
id: "retrieval-precision-recall-execution",
|
|
181
|
+
label: "Retrieval precision/recall execution evidence",
|
|
182
|
+
path: args.artifactPath,
|
|
183
|
+
kind: "artifact",
|
|
184
|
+
mimeType: "application/json",
|
|
185
|
+
description: "Per-case retrieval-quality evidence and aggregate without context text or raw command output."
|
|
186
|
+
}
|
|
187
|
+
],
|
|
188
|
+
warnings: [],
|
|
189
|
+
failures,
|
|
190
|
+
metadata: { executionArtifactPath: args.artifactPath },
|
|
191
|
+
caseExecutionEvidence: args.caseEvidence.map((entry) => structuredClone(entry)),
|
|
192
|
+
aggregate: structuredClone(args.aggregate)
|
|
193
|
+
};
|
|
194
|
+
run.summary = summarizeExperimentRun(run);
|
|
195
|
+
return run;
|
|
196
|
+
}
|
|
197
|
+
function buildOutcome(entry) {
|
|
198
|
+
const warnings = (entry.retrieval?.warnings ?? []).map((message) => ({
|
|
199
|
+
code: "retrieval-warning",
|
|
200
|
+
message,
|
|
201
|
+
variantId: RETRIEVAL_PRECISION_RECALL_VARIANT_ID,
|
|
202
|
+
caseId: entry.caseId
|
|
203
|
+
}));
|
|
204
|
+
const failures = entry.errors.map((error) => ({
|
|
205
|
+
code: error.code,
|
|
206
|
+
message: error.message,
|
|
207
|
+
variantId: RETRIEVAL_PRECISION_RECALL_VARIANT_ID,
|
|
208
|
+
caseId: entry.caseId,
|
|
209
|
+
recoverable: true
|
|
210
|
+
}));
|
|
211
|
+
// Availability and reason stay visible even though an unavailable/not-applicable ratio projects as a null value.
|
|
212
|
+
const metricAvailability = {};
|
|
213
|
+
for (const key of RETRIEVAL_PRECISION_RECALL_RATIO_KEYS) {
|
|
214
|
+
const ratio = retrievalRatioOf(entry.quality, key);
|
|
215
|
+
metricAvailability[key] = { availability: ratio.availability, reason: ratio.reason };
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
id: `${entry.caseId}:${RETRIEVAL_PRECISION_RECALL_VARIANT_ID}`,
|
|
219
|
+
caseId: entry.caseId,
|
|
220
|
+
variantId: RETRIEVAL_PRECISION_RECALL_VARIANT_ID,
|
|
221
|
+
status: entry.status,
|
|
222
|
+
metrics: toRetrievalPrecisionRecallOutcomeMetrics(entry.quality, RETRIEVAL_PRECISION_RECALL_VARIANT_ID, entry.caseId),
|
|
223
|
+
artifacts: [],
|
|
224
|
+
warnings,
|
|
225
|
+
failures,
|
|
226
|
+
metadata: {
|
|
227
|
+
benchmarkProject: entry.benchmarkProject,
|
|
228
|
+
evidenceAvailability: entry.retrieval?.evidenceAvailability ?? null,
|
|
229
|
+
metricAvailability
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/** Same aggregation rule as the generic per-case summary: uniform statuses win, mixed is partial. */
|
|
234
|
+
function aggregateStatus(statuses) {
|
|
235
|
+
if (statuses.length === 0)
|
|
236
|
+
return "skipped";
|
|
237
|
+
for (const status of ["completed", "skipped", "failed"]) {
|
|
238
|
+
if (statuses.every((value) => value === status))
|
|
239
|
+
return status;
|
|
240
|
+
}
|
|
241
|
+
return "partial";
|
|
242
|
+
}
|
|
243
|
+
function readCasesInput(inputs) {
|
|
244
|
+
const value = inputs?.cases;
|
|
245
|
+
if (!Array.isArray(value)) {
|
|
246
|
+
throw new Error("Retrieval precision/recall requires cases input.");
|
|
247
|
+
}
|
|
248
|
+
return value;
|
|
249
|
+
}
|
|
250
|
+
function readDependenciesInput(inputs) {
|
|
251
|
+
const value = inputs?.retrievalDependencies;
|
|
252
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
253
|
+
}
|
|
@@ -3,9 +3,12 @@ import { buildContextStrategyComparisonV043Report } from "./buildContextStrategy
|
|
|
3
3
|
import { buildWarmIndexReuseReport } from "./buildWarmIndexReuseReport.js";
|
|
4
4
|
import { buildIncrementalChangeStalenessPluginReport } from "./buildIncrementalChangeStalenessPluginReport.js";
|
|
5
5
|
import { buildContextWindowScalingReport } from "./buildContextWindowScalingReport.js";
|
|
6
|
+
import { buildRetrievalPrecisionRecallReport } from "./buildRetrievalPrecisionRecallReport.js";
|
|
6
7
|
// Bulk context-window-scaling evidence is presented by the typed report section; the complete
|
|
7
8
|
// evidence stays in context-window-scaling-execution.json.
|
|
8
9
|
const CONTEXT_WINDOW_SCALING_BULK_KEYS = ["executionEvidence", "aggregate"];
|
|
10
|
+
// Same for retrieval-precision-recall: the typed section presents the evidence; the execution artifact keeps all of it.
|
|
11
|
+
const RETRIEVAL_PRECISION_RECALL_BULK_KEYS = ["caseExecutionEvidence", "aggregate"];
|
|
9
12
|
const V043_BULK_ARRAY_KEYS = [
|
|
10
13
|
"v043StageContextExecutions",
|
|
11
14
|
"v043StageContextEvaluations",
|
|
@@ -18,6 +21,7 @@ export function buildPluginExperimentReport(args) {
|
|
|
18
21
|
const warmIndexReuse = buildWarmIndexReuseReport(args.run);
|
|
19
22
|
const incrementalChangeStaleness = buildIncrementalChangeStalenessPluginReport(args.run);
|
|
20
23
|
const contextWindowScaling = buildContextWindowScalingReport(args.run);
|
|
24
|
+
const retrievalPrecisionRecall = buildRetrievalPrecisionRecallReport(args.run);
|
|
21
25
|
const rawRun = { ...args.run, artifacts: relativizeArtifacts(args.run.artifacts, outputRoot) };
|
|
22
26
|
for (const key of V043_BULK_ARRAY_KEYS) {
|
|
23
27
|
delete rawRun[key];
|
|
@@ -27,6 +31,11 @@ export function buildPluginExperimentReport(args) {
|
|
|
27
31
|
delete rawRun[key];
|
|
28
32
|
}
|
|
29
33
|
}
|
|
34
|
+
if (retrievalPrecisionRecall) {
|
|
35
|
+
for (const key of RETRIEVAL_PRECISION_RECALL_BULK_KEYS) {
|
|
36
|
+
delete rawRun[key];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
30
39
|
return {
|
|
31
40
|
metadata: {
|
|
32
41
|
generatedAt: args.generatedAt ?? new Date().toISOString(),
|
|
@@ -53,8 +62,9 @@ export function buildPluginExperimentReport(args) {
|
|
|
53
62
|
warmIndexReuse,
|
|
54
63
|
incrementalChangeStaleness,
|
|
55
64
|
contextWindowScaling,
|
|
65
|
+
retrievalPrecisionRecall,
|
|
56
66
|
contextStrategyComparisonV043,
|
|
57
|
-
interpretation: buildInterpretation(args.run, contextStrategyComparisonV043, warmIndexReuse, contextWindowScaling),
|
|
67
|
+
interpretation: buildInterpretation(args.run, contextStrategyComparisonV043, warmIndexReuse, contextWindowScaling, retrievalPrecisionRecall),
|
|
58
68
|
rawRun,
|
|
59
69
|
};
|
|
60
70
|
}
|
|
@@ -133,7 +143,16 @@ function affectedNeighborhoodSentence(report) {
|
|
|
133
143
|
`reindex recommendations were recommended=${summary.recommendedReindexCount}, not-indicated=${summary.notIndicatedReindexCount}, unknown=${summary.unknownReindexRecommendationCount}. ` +
|
|
134
144
|
"Affected-neighborhood evidence uses a one-hop baseline graph around confirmed changed indexed files/symbols. It is observational and does not alter retrieval or execution status.");
|
|
135
145
|
}
|
|
136
|
-
function buildInterpretation(run, contextStrategyComparisonV043, warmIndexReuse, contextWindowScaling) {
|
|
146
|
+
function buildInterpretation(run, contextStrategyComparisonV043, warmIndexReuse, contextWindowScaling, retrievalPrecisionRecall) {
|
|
147
|
+
if (retrievalPrecisionRecall) {
|
|
148
|
+
const summary = retrievalPrecisionRecall.runSummary;
|
|
149
|
+
return {
|
|
150
|
+
summary: `Retrieval quality was measured over ${summary.caseCount} case${summary.caseCount === 1 ? "" : "s"} (${summary.completedCaseCount} completed, ${summary.partialCaseCount} partial, ${summary.failedCaseCount} failed). ` +
|
|
151
|
+
"Available case-level file, symbol and fact metrics are summarized without ranking strategies. " +
|
|
152
|
+
"Unavailable and not-applicable evidence is excluded from macro means rather than treated as zero.",
|
|
153
|
+
recommendedNextStep: "Review per-case missed and irrelevant context together with availability before drawing conclusions."
|
|
154
|
+
};
|
|
155
|
+
}
|
|
137
156
|
if (contextWindowScaling) {
|
|
138
157
|
const summary = contextWindowScaling.runSummary;
|
|
139
158
|
return {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { RETRIEVAL_PRECISION_RECALL_PLUGIN_ID } from "../../experiments/plugins/retrievalPrecisionRecall/metadata.js";
|
|
2
|
+
import { retrievalRatioOf } from "../../experiments/plugins/retrievalPrecisionRecall/metrics.js";
|
|
3
|
+
import { RETRIEVAL_PRECISION_RECALL_RATIO_KEYS } from "../../experiments/plugins/retrievalPrecisionRecall/types.js";
|
|
4
|
+
import { MAX_RETRIEVAL_REPORT_IDENTITIES, RETRIEVAL_PRECISION_RECALL_REPORT_SCHEMA_VERSION } from "./retrievalPrecisionRecallReportModel.js";
|
|
5
|
+
/** Fixed wording; the report states observed evidence only and never ranks or scores anything. */
|
|
6
|
+
export const RETRIEVAL_PRECISION_RECALL_LIMITATIONS = [
|
|
7
|
+
"File and symbol precision and recall are set-based measures, not ranked metrics.",
|
|
8
|
+
"Symbol scoring uses exact unique symbol names, because the answer key lists names rather than node identities.",
|
|
9
|
+
"Fact coverage uses explicit fact-to-context mappings in the answer key; it does not judge generated answers or interpret fact text.",
|
|
10
|
+
"Irrelevant context ratio is file-identity based and is the complement of file precision when both are available.",
|
|
11
|
+
"Retrieved token count measures the selected retrieval context payload; it is not a relevant versus irrelevant token attribution.",
|
|
12
|
+
"The current lifecycle expands only the top search candidate (lookup, slice and source).",
|
|
13
|
+
"Unavailable and not-applicable values are not zeros and are excluded from macro means.",
|
|
14
|
+
"The experiment uses no real agents.",
|
|
15
|
+
"v0.8.0 does not compare retrieval strategies."
|
|
16
|
+
];
|
|
17
|
+
function boundedList(items) {
|
|
18
|
+
if (items === null)
|
|
19
|
+
return null;
|
|
20
|
+
const displayed = items.slice(0, MAX_RETRIEVAL_REPORT_IDENTITIES);
|
|
21
|
+
return {
|
|
22
|
+
totalCount: items.length,
|
|
23
|
+
displayed: [...displayed],
|
|
24
|
+
displayedCount: displayed.length,
|
|
25
|
+
omittedCount: items.length - displayed.length
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Presents the per-case metric results and aggregate already calculated on the run. It performs no precision, recall,
|
|
30
|
+
* coverage, ratio or mean calculation of its own; identity lists are only bounded for display.
|
|
31
|
+
*/
|
|
32
|
+
export function buildRetrievalPrecisionRecallReport(run) {
|
|
33
|
+
if (run.pluginId !== RETRIEVAL_PRECISION_RECALL_PLUGIN_ID)
|
|
34
|
+
return null;
|
|
35
|
+
const candidate = run;
|
|
36
|
+
const evidence = candidate.caseExecutionEvidence;
|
|
37
|
+
const aggregate = candidate.aggregate;
|
|
38
|
+
if (!evidence || !aggregate) {
|
|
39
|
+
// A run that failed before producing evidence (for example an unknown case id or a non-self target) still gets the
|
|
40
|
+
// generic report with its failure; only a non-failed run with missing evidence is a broken report source.
|
|
41
|
+
if (run.status === "failed")
|
|
42
|
+
return null;
|
|
43
|
+
throw new Error("Invalid retrieval-precision-recall report source: case execution evidence and aggregate are required.");
|
|
44
|
+
}
|
|
45
|
+
if (aggregate.runSummary.caseCount !== evidence.length) {
|
|
46
|
+
throw new Error("Invalid retrieval-precision-recall report source: aggregate and case execution evidence are inconsistent.");
|
|
47
|
+
}
|
|
48
|
+
const cases = evidence.map((entry) => {
|
|
49
|
+
const quality = entry.quality;
|
|
50
|
+
const metrics = Object.fromEntries(RETRIEVAL_PRECISION_RECALL_RATIO_KEYS.map((key) => {
|
|
51
|
+
const ratio = retrievalRatioOf(quality, key);
|
|
52
|
+
return [key, { availability: ratio.availability, numerator: ratio.numerator, denominator: ratio.denominator, value: ratio.value, reason: ratio.reason }];
|
|
53
|
+
}));
|
|
54
|
+
return {
|
|
55
|
+
caseId: entry.caseId,
|
|
56
|
+
caseName: entry.caseName,
|
|
57
|
+
benchmarkProject: entry.benchmarkProject,
|
|
58
|
+
taskLocality: entry.taskLocality,
|
|
59
|
+
status: entry.status,
|
|
60
|
+
evidenceAvailability: entry.retrieval?.evidenceAvailability ?? null,
|
|
61
|
+
metrics,
|
|
62
|
+
retrievedTokenCount: quality?.retrievedTokenCount ?? null,
|
|
63
|
+
tokenCountMethod: quality?.tokenCountMethod ?? null,
|
|
64
|
+
missedFiles: boundedList(quality?.file.missedFiles ?? null),
|
|
65
|
+
irrelevantRetrievedFiles: boundedList(quality?.file.irrelevantRetrievedFiles ?? null),
|
|
66
|
+
missedSymbols: boundedList(quality?.symbol.missedSymbols ?? null),
|
|
67
|
+
irrelevantRetrievedSymbols: boundedList(quality?.symbol.irrelevantRetrievedSymbols ?? null),
|
|
68
|
+
uncoveredFactIds: boundedList(quality?.fact.uncoveredFactIds ?? null),
|
|
69
|
+
errors: entry.errors.map((error) => `${error.code}: ${error.message}`),
|
|
70
|
+
identityRedaction: entry.identityRedaction ? { ...entry.identityRedaction } : null
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
schemaVersion: RETRIEVAL_PRECISION_RECALL_REPORT_SCHEMA_VERSION,
|
|
75
|
+
tokenCountMethod: evidence.find((entry) => entry.quality?.tokenCountMethod != null)?.quality?.tokenCountMethod ?? null,
|
|
76
|
+
runSummary: { ...aggregate.runSummary },
|
|
77
|
+
ratioSummaries: structuredClone(aggregate.ratios),
|
|
78
|
+
tokenSummary: { ...aggregate.tokens },
|
|
79
|
+
occurrenceSummary: { ...aggregate.occurrences },
|
|
80
|
+
cases,
|
|
81
|
+
limitations: [...RETRIEVAL_PRECISION_RECALL_LIMITATIONS]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -24,3 +24,7 @@ export * from "./contextWindowScalingReportModel.js";
|
|
|
24
24
|
export { buildContextWindowScalingReport, CONTEXT_WINDOW_SCALING_INTERPRETATION } from "./buildContextWindowScalingReport.js";
|
|
25
25
|
export { renderContextWindowScalingHtml } from "./renderContextWindowScalingHtml.js";
|
|
26
26
|
export { renderContextWindowScalingTextLines } from "./renderContextWindowScalingText.js";
|
|
27
|
+
export * from "./retrievalPrecisionRecallReportModel.js";
|
|
28
|
+
export { buildRetrievalPrecisionRecallReport, RETRIEVAL_PRECISION_RECALL_LIMITATIONS } from "./buildRetrievalPrecisionRecallReport.js";
|
|
29
|
+
export { renderRetrievalPrecisionRecallHtml } from "./renderRetrievalPrecisionRecallHtml.js";
|
|
30
|
+
export { renderRetrievalPrecisionRecallTextLines } from "./renderRetrievalPrecisionRecallText.js";
|
|
@@ -2,7 +2,9 @@ import { renderContextStrategyComparisonV043Html } from "./renderContextStrategy
|
|
|
2
2
|
import { renderWarmIndexReuseHtml } from "./renderWarmIndexReuseHtml.js";
|
|
3
3
|
import { renderIncrementalChangeStalenessHtml } from "./renderIncrementalChangeStalenessHtml.js";
|
|
4
4
|
import { renderContextWindowScalingHtml } from "./renderContextWindowScalingHtml.js";
|
|
5
|
+
import { renderRetrievalPrecisionRecallHtml } from "./renderRetrievalPrecisionRecallHtml.js";
|
|
5
6
|
export function renderPluginExperimentReportHtml(report) {
|
|
7
|
+
const redactedTarget = report.target.privacyProjection === "external-local-redacted";
|
|
6
8
|
return `<!DOCTYPE html>
|
|
7
9
|
<html lang="en">
|
|
8
10
|
<head>
|
|
@@ -49,10 +51,10 @@ export function renderPluginExperimentReportHtml(report) {
|
|
|
49
51
|
["Mode", report.target.mode],
|
|
50
52
|
["Tool root", report.target.toolRoot],
|
|
51
53
|
["Target root", report.target.targetRoot],
|
|
52
|
-
["Target package", report.target.packageName ? `${report.target.packageName}@${report.target.packageVersion ?? "unknown"}` : "unavailable"],
|
|
54
|
+
["Target package", redactedTarget ? "redacted" : report.target.packageName ? `${report.target.packageName}@${report.target.packageVersion ?? "unknown"}` : "unavailable"],
|
|
53
55
|
["Target branch/commit", report.target.hasGit ? `${report.target.branch ?? "unknown"} / ${report.target.commit ?? "unknown"}` : "unavailable"],
|
|
54
|
-
["Has package.json", String(report.target.hasPackageJson)],
|
|
55
|
-
["Has lockfile", String(report.target.hasLockfile)],
|
|
56
|
+
["Has package.json", redactedTarget ? "redacted" : String(report.target.hasPackageJson)],
|
|
57
|
+
["Has lockfile", redactedTarget ? "redacted" : String(report.target.hasLockfile)],
|
|
56
58
|
["Output root", report.metadata.outputRoot ?? "unavailable"],
|
|
57
59
|
])}
|
|
58
60
|
</section>
|
|
@@ -105,6 +107,8 @@ export function renderPluginExperimentReportHtml(report) {
|
|
|
105
107
|
|
|
106
108
|
${renderContextWindowScalingHtml(report.contextWindowScaling)}
|
|
107
109
|
|
|
110
|
+
${renderRetrievalPrecisionRecallHtml(report.retrievalPrecisionRecall ?? null)}
|
|
111
|
+
|
|
108
112
|
<section>
|
|
109
113
|
<h2>Warnings, Skips, And Failures</h2>
|
|
110
114
|
${report.findings.length ? table(["Severity", "Code", "Message", "Variant", "Case"], report.findings.map((finding) => [
|
|
@@ -3,6 +3,7 @@ import { isV2ReportSection } from "./buildIncrementalChangeStalenessReportV2.js"
|
|
|
3
3
|
import { renderIncrementalChangeStalenessSectionV2 } from "./renderIncrementalChangeStalenessTextV2.js";
|
|
4
4
|
import { REINDEX_RECOMMENDATION_EXPLANATIONS } from "./buildWarmIndexReuseReport.js";
|
|
5
5
|
import { renderContextWindowScalingTextLines } from "./renderContextWindowScalingText.js";
|
|
6
|
+
import { renderRetrievalPrecisionRecallTextLines } from "./renderRetrievalPrecisionRecallText.js";
|
|
6
7
|
function sanitizeScalar(value) {
|
|
7
8
|
const text = String(value);
|
|
8
9
|
let result = "";
|
|
@@ -671,6 +672,14 @@ export function renderPluginExperimentReportText(report) {
|
|
|
671
672
|
else {
|
|
672
673
|
lines.push(...renderContextWindowScalingTextLines(report.contextWindowScaling));
|
|
673
674
|
}
|
|
675
|
+
pushSection(lines, "Retrieval Precision/Recall");
|
|
676
|
+
const retrievalPrecisionRecall = report.retrievalPrecisionRecall ?? null;
|
|
677
|
+
if (retrievalPrecisionRecall === null) {
|
|
678
|
+
lines.push("Not applicable to this plugin.");
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
lines.push(...renderRetrievalPrecisionRecallTextLines(retrievalPrecisionRecall));
|
|
682
|
+
}
|
|
674
683
|
pushSection(lines, "Warnings, Skips, And Failures");
|
|
675
684
|
pushDashList(lines, report.findings.map((finding) => `${finding.severity} ${finding.code} ${finding.message} variant=${finding.variantId ?? ""} case=${finding.caseId ?? ""}`));
|
|
676
685
|
pushSection(lines, "Artifacts");
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { RETRIEVAL_PRECISION_RECALL_RATIO_KEYS } from "../../experiments/plugins/retrievalPrecisionRecall/types.js";
|
|
2
|
+
import { RETRIEVAL_IDENTITY_REDACTION_EXPLANATION, formatRetrievalCount, formatRetrievalList, formatRetrievalMean, formatRetrievalMetric, RETRIEVAL_RATIO_LABELS } from "./renderRetrievalPrecisionRecallText.js";
|
|
3
|
+
export function renderRetrievalPrecisionRecallHtml(section) {
|
|
4
|
+
if (section === null) {
|
|
5
|
+
return `<section>
|
|
6
|
+
<h2>Retrieval Precision/Recall</h2>
|
|
7
|
+
<p>Not applicable to this plugin.</p>
|
|
8
|
+
</section>`;
|
|
9
|
+
}
|
|
10
|
+
const tokens = section.tokenSummary;
|
|
11
|
+
const occurrences = section.occurrenceSummary;
|
|
12
|
+
return `<section>
|
|
13
|
+
<h2>Retrieval Precision/Recall</h2>
|
|
14
|
+
<p class="muted">Measures which expected files, symbols and facts the my-dev-kit retrieval lifecycle surfaced, and which irrelevant files it surfaced, without agents. It is descriptive and does not rank anything.</p>
|
|
15
|
+
${table(["Field", "Value"], [
|
|
16
|
+
["Schema", section.schemaVersion],
|
|
17
|
+
["Token count method", section.tokenCountMethod ?? "unavailable"],
|
|
18
|
+
...Object.entries(section.runSummary).map(([label, value]) => [label, String(value)])
|
|
19
|
+
])}
|
|
20
|
+
${section.cases.some((entry) => entry.identityRedaction !== null) ? `<p><strong>Identity redaction.</strong> ${escapeHtml(RETRIEVAL_IDENTITY_REDACTION_EXPLANATION)}</p>` : ""}
|
|
21
|
+
<h3>Macro Averages</h3>
|
|
22
|
+
<p class="muted">Mean over available per-case values only. Unavailable and not-applicable cases are counted separately and are never treated as zero.</p>
|
|
23
|
+
${table(["Metric", "Available", "Unavailable", "Not applicable", "Mean"], RETRIEVAL_PRECISION_RECALL_RATIO_KEYS.map((key) => {
|
|
24
|
+
const summary = section.ratioSummaries[key];
|
|
25
|
+
return [RETRIEVAL_RATIO_LABELS[key], String(summary.availableCount), String(summary.unavailableCount), String(summary.notApplicableCount), formatRetrievalMean(summary.meanValue)];
|
|
26
|
+
}))}
|
|
27
|
+
<h3>Retrieved Token Summary</h3>
|
|
28
|
+
<p class="muted">Measured retrieval payload size; not a relevant versus irrelevant token attribution.</p>
|
|
29
|
+
${table(["Available", "Unavailable", "Total", "Mean", "Min", "Max"], [[
|
|
30
|
+
String(tokens.availableCount),
|
|
31
|
+
String(tokens.unavailableCount),
|
|
32
|
+
formatRetrievalCount(tokens.totalTokens),
|
|
33
|
+
formatRetrievalMean(tokens.meanTokens),
|
|
34
|
+
formatRetrievalCount(tokens.minTokens),
|
|
35
|
+
formatRetrievalCount(tokens.maxTokens)
|
|
36
|
+
]])}
|
|
37
|
+
<h3>Missed And Irrelevant Occurrence Totals</h3>
|
|
38
|
+
<p class="muted">Summed per case; identical paths in different cases or projects are not deduplicated.</p>
|
|
39
|
+
${table(["Missed files", "Irrelevant files", "Cases with file evidence", "Missed symbols", "Irrelevant symbols", "Cases with symbol evidence", "Uncovered facts", "Cases with fact evidence"], [[
|
|
40
|
+
String(occurrences.totalMissedFileOccurrences),
|
|
41
|
+
String(occurrences.totalIrrelevantRetrievedFileOccurrences),
|
|
42
|
+
String(occurrences.fileOccurrenceEvidenceCaseCount),
|
|
43
|
+
String(occurrences.totalMissedSymbolOccurrences),
|
|
44
|
+
String(occurrences.totalIrrelevantRetrievedSymbolOccurrences),
|
|
45
|
+
String(occurrences.symbolOccurrenceEvidenceCaseCount),
|
|
46
|
+
String(occurrences.totalUncoveredFactOccurrences),
|
|
47
|
+
String(occurrences.factOccurrenceEvidenceCaseCount)
|
|
48
|
+
]])}
|
|
49
|
+
<h3>Per-Case Results</h3>
|
|
50
|
+
${table(["Case", "Project", "Locality", "Status", "Evidence", ...RETRIEVAL_PRECISION_RECALL_RATIO_KEYS.map((key) => RETRIEVAL_RATIO_LABELS[key]), "Retrieved tokens"], section.cases.map((entry) => [
|
|
51
|
+
entry.caseId,
|
|
52
|
+
entry.benchmarkProject,
|
|
53
|
+
entry.taskLocality ?? "unavailable",
|
|
54
|
+
entry.status,
|
|
55
|
+
entry.evidenceAvailability ?? "unavailable",
|
|
56
|
+
...RETRIEVAL_PRECISION_RECALL_RATIO_KEYS.map((key) => formatRetrievalMetric(entry.metrics[key])),
|
|
57
|
+
formatRetrievalCount(entry.retrievedTokenCount)
|
|
58
|
+
]))}
|
|
59
|
+
<h3>Per-Case Missed And Irrelevant Context</h3>
|
|
60
|
+
<p class="muted">Displayed lists are bounded; totals are exact.</p>
|
|
61
|
+
${section.cases
|
|
62
|
+
.map((entry) => {
|
|
63
|
+
const lists = [
|
|
64
|
+
["Missed files", entry.missedFiles],
|
|
65
|
+
["Irrelevant retrieved files", entry.irrelevantRetrievedFiles],
|
|
66
|
+
["Missed symbols", entry.missedSymbols],
|
|
67
|
+
["Irrelevant retrieved symbols", entry.irrelevantRetrievedSymbols],
|
|
68
|
+
["Uncovered fact IDs", entry.uncoveredFactIds]
|
|
69
|
+
];
|
|
70
|
+
return `<h4><code>${escapeHtml(entry.caseId)}</code> (${escapeHtml(entry.status)})</h4>
|
|
71
|
+
${entry.identityRedaction !== null ? `<p class="muted">Identity redaction: files, symbols, facts, warnings and case title withheld.</p>` : ""}
|
|
72
|
+
${lists
|
|
73
|
+
.map(([label, list]) => `<p>${escapeHtml(label)}: ${escapeHtml(formatRetrievalList(list))}</p>${list && list.displayed.length > 0 ? listHtml(list.displayed) : ""}`)
|
|
74
|
+
.join("\n ")}${entry.errors.length > 0 ? `\n ${listHtml(entry.errors)}` : ""}`;
|
|
75
|
+
})
|
|
76
|
+
.join("\n ")}
|
|
77
|
+
<h3>Interpretation Limits</h3>
|
|
78
|
+
${listHtml(section.limitations)}
|
|
79
|
+
</section>`;
|
|
80
|
+
}
|
|
81
|
+
function listHtml(items) {
|
|
82
|
+
return `<ul>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`;
|
|
83
|
+
}
|
|
84
|
+
function table(headers, rows) {
|
|
85
|
+
return `<table><thead><tr>${headers.map((header) => `<th>${escapeHtml(header)}</th>`).join("")}</tr></thead><tbody>${rows
|
|
86
|
+
.map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}</tr>`)
|
|
87
|
+
.join("")}</tbody></table>`;
|
|
88
|
+
}
|
|
89
|
+
function escapeHtml(value) {
|
|
90
|
+
return String(value ?? "")
|
|
91
|
+
.replaceAll("&", "&")
|
|
92
|
+
.replaceAll("<", "<")
|
|
93
|
+
.replaceAll(">", ">")
|
|
94
|
+
.replaceAll('"', """)
|
|
95
|
+
.replaceAll("'", "'");
|
|
96
|
+
}
|