@dailephd/my-dev-kit-lab 0.9.2 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +6 -1
  2. package/README.md +7 -5
  3. package/dist/src/agents/adapters/codexAdapter.js +4 -1
  4. package/dist/src/cli/help.js +38 -2
  5. package/dist/src/cli/runLabCli.js +9 -1
  6. package/dist/src/commands/buildGalleryCommand.js +16 -3
  7. package/dist/src/commands/runComparePromptsCommand.js +109 -0
  8. package/dist/src/commands/runExperimentRunCommand.js +47 -2
  9. package/dist/src/core/processTree.js +41 -17
  10. package/dist/src/core/runMeasuredCommand.js +14 -8
  11. package/dist/src/experiments/plugins/agentSuccessRate/campaignCheckpointStore.js +23 -1
  12. package/dist/src/experiments/plugins/agentSuccessRate/campaignExecution.js +7 -2
  13. package/dist/src/experiments/plugins/agentSuccessRate/campaignIdentity.js +29 -4
  14. package/dist/src/experiments/plugins/agentSuccessRate/campaignResumableExecution.js +1 -1
  15. package/dist/src/experiments/plugins/agentSuccessRate/config.js +47 -2
  16. package/dist/src/experiments/plugins/agentSuccessRate/plugin.js +4 -2
  17. package/dist/src/experiments/plugins/agentSuccessRate/promptContract.js +121 -0
  18. package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +32 -2
  19. package/dist/src/experiments/plugins/agentSuccessRate/realAgentPrompt.js +105 -11
  20. package/dist/src/experiments/plugins/agentSuccessRate/strictDiffResponse.js +312 -0
  21. package/dist/src/gallery/galleryPathSafety.js +111 -0
  22. package/dist/src/gallery/readGallerySources.js +542 -0
  23. package/dist/src/gallery/writeGalleryManifest.js +57 -3
  24. package/dist/src/report/experiments/buildPluginExperimentReport.js +4 -16
  25. package/dist/src/report/experiments/htmlSafety.js +32 -0
  26. package/dist/src/report/experiments/metricEvidence.js +389 -0
  27. package/dist/src/report/experiments/metricGlossary.js +227 -0
  28. package/dist/src/report/experiments/metricGlossaryCatalog.js +199 -0
  29. package/dist/src/report/experiments/promptTemplateComparisonEvidence.js +377 -0
  30. package/dist/src/report/experiments/promptTemplateComparisonModel.js +436 -0
  31. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +158 -64
  32. package/dist/src/report/experiments/renderPluginExperimentReportText.js +22 -677
  33. package/dist/src/report/experiments/renderPromptTemplateComparison.js +192 -0
  34. package/dist/src/report/experiments/renderSpecializedTextSections.js +633 -0
  35. package/dist/src/report/experiments/reportSectionRegistry.js +79 -0
  36. package/dist/src/report/experiments/specializedReportSections.js +142 -0
  37. package/dist/src/report/experiments/writePluginExperimentReports.js +4 -2
  38. package/dist/src/securityValidation/attackScenarios/scenarios/secretLeakageScenario.js +146 -133
  39. package/dist/src/securityValidation/attackScenarios/scenarios/subprocessInjectionScenario.js +54 -36
  40. package/dist/src/securityValidation/commandRunner.js +13 -6
  41. package/docs/METRICS.md +11 -1
  42. package/package.json +1 -1
@@ -0,0 +1,79 @@
1
+ // Shared, typed registry of specialized (experiment-family) report sections. Core report sections
2
+ // (metadata, variants, cases, metrics, warnings, interpretation, artifacts) stay in the generic renderers.
3
+ // The registry only selects, orders and dispatches specialized sections for the builder, text and HTML renderers.
4
+ // It performs no calculation, no I/O, no dynamic import, and registers only in-process descriptors.
5
+ const ID_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
6
+ function validate(descriptor) {
7
+ const label = typeof descriptor?.id === "string" ? descriptor.id : "<unknown>";
8
+ if (typeof descriptor?.id !== "string" || !ID_PATTERN.test(descriptor.id)) {
9
+ throw new Error(`Invalid report section registration ${label}: id must be a stable lowercase namespaced identifier.`);
10
+ }
11
+ if (!Number.isInteger(descriptor.order)) {
12
+ throw new Error(`Invalid report section registration ${label}: order must be an integer.`);
13
+ }
14
+ if (typeof descriptor.projectionKey !== "string" || descriptor.projectionKey.length === 0) {
15
+ throw new Error(`Invalid report section registration ${label}: projectionKey is required.`);
16
+ }
17
+ const pluginIds = descriptor.appliesTo?.pluginIds;
18
+ if (!Array.isArray(pluginIds) || pluginIds.length === 0 || pluginIds.some((id) => typeof id !== "string" || !id)) {
19
+ throw new Error(`Invalid report section registration ${label}: appliesTo.pluginIds must be a non-empty list of plugin ids.`);
20
+ }
21
+ for (const member of ["build", "renderText", "renderHtml"]) {
22
+ if (typeof descriptor[member] !== "function") {
23
+ throw new Error(`Invalid report section registration ${label}: ${member} must be a function.`);
24
+ }
25
+ }
26
+ if (descriptor.text?.whenAbsent !== "placeholder" && descriptor.text?.whenAbsent !== "omit") {
27
+ throw new Error(`Invalid report section registration ${label}: text.whenAbsent must be placeholder or omit.`);
28
+ }
29
+ if (descriptor.text.whenAbsent === "placeholder" && !descriptor.text.title) {
30
+ throw new Error(`Invalid report section registration ${label}: a placeholder text section requires a title.`);
31
+ }
32
+ }
33
+ /** Assembles an immutable registry. Duplicate ids, duplicate projection keys and invalid descriptors fail. */
34
+ export function createReportSectionRegistry(input) {
35
+ const ids = new Set();
36
+ const keys = new Set();
37
+ const frozen = [];
38
+ for (const descriptor of input) {
39
+ validate(descriptor);
40
+ if (ids.has(descriptor.id)) {
41
+ throw new Error(`Duplicate report section registration: ${descriptor.id}.`);
42
+ }
43
+ if (keys.has(descriptor.projectionKey)) {
44
+ throw new Error(`Duplicate report section projection key ${descriptor.projectionKey} (section ${descriptor.id}).`);
45
+ }
46
+ ids.add(descriptor.id);
47
+ keys.add(descriptor.projectionKey);
48
+ frozen.push(Object.freeze({
49
+ ...descriptor,
50
+ appliesTo: Object.freeze({ pluginIds: Object.freeze([...descriptor.appliesTo.pluginIds]) }),
51
+ text: Object.freeze({ ...descriptor.text }),
52
+ }));
53
+ }
54
+ frozen.sort((a, b) => a.order - b.order || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
55
+ const descriptors = Object.freeze(frozen);
56
+ const byId = new Map(descriptors.map((descriptor) => [descriptor.id, descriptor]));
57
+ const availabilityOf = (descriptor, report) => {
58
+ const projection = report[descriptor.projectionKey] ?? null;
59
+ if (projection !== null)
60
+ return "available";
61
+ return descriptor.appliesTo.pluginIds.includes(report.plugin.id) ? "unavailable" : "not-applicable";
62
+ };
63
+ return Object.freeze({
64
+ descriptors,
65
+ get: (id) => byId.get(id),
66
+ resolve: (report) => descriptors.map((descriptor) => ({
67
+ id: descriptor.id,
68
+ order: descriptor.order,
69
+ availability: availabilityOf(descriptor, report),
70
+ descriptor,
71
+ })),
72
+ resolveById: (id, report) => {
73
+ const descriptor = byId.get(id);
74
+ if (!descriptor)
75
+ return { id, order: Number.MAX_SAFE_INTEGER, availability: "unsupported", descriptor: null };
76
+ return { id, order: descriptor.order, availability: availabilityOf(descriptor, report), descriptor };
77
+ },
78
+ });
79
+ }
@@ -0,0 +1,142 @@
1
+ // The registered specialized report families. Each descriptor adapts an existing family builder and
2
+ // renderer; no builder or renderer is cloned here. Order values reproduce the established section order
3
+ // (plugin families are mutually exclusive, so at most one family projection is present for a given run).
4
+ import { AGENT_SUCCESS_RATE_PLUGIN_ID } from "../../experiments/plugins/agentSuccessRate/metadata.js";
5
+ import { CONTEXT_PACK_GENERATION_PLUGIN_ID } from "../../experiments/plugins/contextPackGeneration/metadata.js";
6
+ import { CONTEXT_WINDOW_SCALING_PLUGIN_ID } from "../../experiments/plugins/contextWindowScaling/metadata.js";
7
+ import { INCREMENTAL_CHANGE_STALENESS_PLUGIN_ID } from "../../experiments/plugins/incrementalChangeStaleness/plugin.js";
8
+ import { RETRIEVAL_PRECISION_RECALL_PLUGIN_ID } from "../../experiments/plugins/retrievalPrecisionRecall/metadata.js";
9
+ import { RETRIEVAL_QUERY_STRATEGY_COMPARISON_PLUGIN_ID } from "../../experiments/plugins/retrievalQueryStrategyComparison/metadata.js";
10
+ import { buildAgentSuccessRateReport } from "./buildAgentSuccessRateReport.js";
11
+ import { buildContextPackGenerationReport } from "./buildContextPackGenerationReport.js";
12
+ import { buildContextStrategyComparisonV043Report } from "./buildContextStrategyComparisonV043Report.js";
13
+ import { buildContextWindowScalingReport } from "./buildContextWindowScalingReport.js";
14
+ import { buildIncrementalChangeStalenessPluginReport } from "./buildIncrementalChangeStalenessPluginReport.js";
15
+ import { buildRetrievalPrecisionRecallReport } from "./buildRetrievalPrecisionRecallReport.js";
16
+ import { buildRetrievalQueryStrategyComparisonReport } from "./buildRetrievalQueryStrategyComparisonReport.js";
17
+ import { buildWarmIndexReuseReport } from "./buildWarmIndexReuseReport.js";
18
+ import { renderAgentSuccessRateHtml } from "./renderAgentSuccessRateHtml.js";
19
+ import { renderAgentSuccessRateTextLines } from "./renderAgentSuccessRateText.js";
20
+ import { renderContextPackGenerationHtml } from "./renderContextPackGenerationHtml.js";
21
+ import { renderContextPackGenerationTextLines } from "./renderContextPackGenerationText.js";
22
+ import { renderContextStrategyComparisonV043Html } from "./renderContextStrategyComparisonV043Html.js";
23
+ import { renderContextWindowScalingHtml } from "./renderContextWindowScalingHtml.js";
24
+ import { renderContextWindowScalingTextLines } from "./renderContextWindowScalingText.js";
25
+ import { renderIncrementalChangeStalenessHtml } from "./renderIncrementalChangeStalenessHtml.js";
26
+ import { renderRetrievalPrecisionRecallHtml } from "./renderRetrievalPrecisionRecallHtml.js";
27
+ import { renderRetrievalPrecisionRecallTextLines } from "./renderRetrievalPrecisionRecallText.js";
28
+ import { renderRetrievalQueryStrategyComparisonHtml } from "./renderRetrievalQueryStrategyComparisonHtml.js";
29
+ import { renderRetrievalQueryStrategyComparisonTextLines } from "./renderRetrievalQueryStrategyComparisonText.js";
30
+ import { renderWarmIndexReuseHtml } from "./renderWarmIndexReuseHtml.js";
31
+ import { renderIncrementalChangeStalenessSection, renderV043Section, renderWarmIndexReuseSection, } from "./renderSpecializedTextSections.js";
32
+ import { createReportSectionRegistry, } from "./reportSectionRegistry.js";
33
+ const WARM_INDEX_REUSE_PLUGIN_ID = "warm-index-reuse";
34
+ const CONTEXT_STRATEGY_COMPARISON_PLUGIN_ID = "context-strategy-comparison";
35
+ export const SPECIALIZED_REPORT_SECTION_DESCRIPTORS = [
36
+ {
37
+ id: "context-strategy.v043",
38
+ order: 10,
39
+ projectionKey: "contextStrategyComparisonV043",
40
+ appliesTo: { pluginIds: [CONTEXT_STRATEGY_COMPARISON_PLUGIN_ID] },
41
+ text: { title: "V0.4.3 Stage-Context Evidence", whenAbsent: "placeholder" },
42
+ build: ({ run }) => buildContextStrategyComparisonV043Report(run),
43
+ renderText: (lines, projection) => renderV043Section(lines, projection),
44
+ renderHtml: (projection) => renderContextStrategyComparisonV043Html(projection),
45
+ },
46
+ {
47
+ id: "warm-index.reuse",
48
+ order: 20,
49
+ projectionKey: "warmIndexReuse",
50
+ appliesTo: { pluginIds: [WARM_INDEX_REUSE_PLUGIN_ID] },
51
+ text: { title: "Warm Index Reuse Evidence", whenAbsent: "placeholder" },
52
+ build: ({ run }) => buildWarmIndexReuseReport(run),
53
+ renderText: (lines, projection) => renderWarmIndexReuseSection(lines, projection),
54
+ renderHtml: (projection) => renderWarmIndexReuseHtml(projection),
55
+ },
56
+ {
57
+ id: "incremental-change.staleness",
58
+ order: 30,
59
+ projectionKey: "incrementalChangeStaleness",
60
+ appliesTo: { pluginIds: [INCREMENTAL_CHANGE_STALENESS_PLUGIN_ID] },
61
+ text: { title: "Incremental-Change And Staleness Evidence", whenAbsent: "placeholder" },
62
+ // The V1/V2 schema-version dispatch stays owned by the existing plugin report builder.
63
+ build: ({ run }) => buildIncrementalChangeStalenessPluginReport(run),
64
+ renderText: (lines, projection) => renderIncrementalChangeStalenessSection(lines, projection),
65
+ renderHtml: (projection) => renderIncrementalChangeStalenessHtml(projection),
66
+ },
67
+ {
68
+ id: "context-window.scaling",
69
+ order: 40,
70
+ projectionKey: "contextWindowScaling",
71
+ appliesTo: { pluginIds: [CONTEXT_WINDOW_SCALING_PLUGIN_ID] },
72
+ text: { title: "Context Window Scaling", whenAbsent: "placeholder" },
73
+ build: ({ run }) => buildContextWindowScalingReport(run),
74
+ renderText: (lines, projection) => {
75
+ lines.push(...renderContextWindowScalingTextLines(projection));
76
+ },
77
+ renderHtml: (projection) => renderContextWindowScalingHtml(projection),
78
+ },
79
+ {
80
+ id: "retrieval.precision-recall",
81
+ order: 50,
82
+ projectionKey: "retrievalPrecisionRecall",
83
+ appliesTo: { pluginIds: [RETRIEVAL_PRECISION_RECALL_PLUGIN_ID] },
84
+ text: { title: "Retrieval Precision/Recall", whenAbsent: "placeholder" },
85
+ build: ({ run }) => buildRetrievalPrecisionRecallReport(run),
86
+ renderText: (lines, projection) => {
87
+ lines.push(...renderRetrievalPrecisionRecallTextLines(projection));
88
+ },
89
+ renderHtml: (projection) => renderRetrievalPrecisionRecallHtml(projection ?? null),
90
+ },
91
+ {
92
+ id: "retrieval.query-strategy-comparison",
93
+ order: 60,
94
+ projectionKey: "retrievalQueryStrategyComparison",
95
+ appliesTo: { pluginIds: [RETRIEVAL_QUERY_STRATEGY_COMPARISON_PLUGIN_ID] },
96
+ text: { title: "Retrieval Query Strategy Comparison", whenAbsent: "placeholder" },
97
+ build: ({ run }) => buildRetrievalQueryStrategyComparisonReport(run),
98
+ renderText: (lines, projection) => {
99
+ lines.push(...renderRetrievalQueryStrategyComparisonTextLines(projection));
100
+ },
101
+ renderHtml: (projection) => renderRetrievalQueryStrategyComparisonHtml(projection ?? null),
102
+ },
103
+ {
104
+ id: "retrieval.context-pack-generation",
105
+ order: 70,
106
+ projectionKey: "contextPackGeneration",
107
+ appliesTo: { pluginIds: [CONTEXT_PACK_GENERATION_PLUGIN_ID] },
108
+ text: { title: "Context Pack Generation", whenAbsent: "omit" },
109
+ build: ({ run, contextPacks }) => buildContextPackGenerationReport(run, contextPacks),
110
+ renderText: (lines, projection) => {
111
+ lines.push(...renderContextPackGenerationTextLines(projection));
112
+ },
113
+ renderHtml: (projection) => renderContextPackGenerationHtml(projection ?? null),
114
+ },
115
+ {
116
+ id: "agent-success.rate",
117
+ order: 80,
118
+ projectionKey: "agentSuccessRate",
119
+ appliesTo: { pluginIds: [AGENT_SUCCESS_RATE_PLUGIN_ID] },
120
+ // The family renderer writes its own headings.
121
+ text: { title: null, whenAbsent: "omit" },
122
+ build: ({ run }) => buildAgentSuccessRateReport(run),
123
+ renderText: (lines, projection) => {
124
+ lines.push(...renderAgentSuccessRateTextLines(projection));
125
+ },
126
+ renderHtml: (projection) => renderAgentSuccessRateHtml(projection ?? null),
127
+ },
128
+ ];
129
+ let defaultRegistry;
130
+ /** The shared registry used by report generation. Assembled once; immutable afterwards. */
131
+ export function getSpecializedReportSectionRegistry() {
132
+ defaultRegistry ??= createReportSectionRegistry(SPECIALIZED_REPORT_SECTION_DESCRIPTORS);
133
+ return defaultRegistry;
134
+ }
135
+ /** Builds every registered specialized projection once, in registry order, using the family builders. */
136
+ export function buildSpecializedReportProjections(context) {
137
+ const projections = {};
138
+ for (const descriptor of getSpecializedReportSectionRegistry().descriptors) {
139
+ projections[descriptor.projectionKey] = descriptor.build(context);
140
+ }
141
+ return projections;
142
+ }
@@ -2,6 +2,7 @@ import { mkdir, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { resolveWithinRoot } from "../../core/pathSafety.js";
4
4
  import { buildPluginExperimentReport } from "./buildPluginExperimentReport.js";
5
+ import { collectMetricReportEvidence } from "./metricEvidence.js";
5
6
  import { loadContextPackArtifacts } from "./loadContextPackArtifacts.js";
6
7
  import { renderPluginExperimentReportHtml } from "./renderPluginExperimentReportHtml.js";
7
8
  import { renderPluginExperimentReportText } from "./renderPluginExperimentReportText.js";
@@ -33,8 +34,9 @@ export async function writePluginExperimentReports(args) {
33
34
  : outputPaths;
34
35
  const writeOptions = args.exclusive ? { encoding: "utf8", flag: "wx" } : "utf8";
35
36
  await writeFile(outputPaths.jsonPath, `${JSON.stringify({ report, outputPaths: serializedPaths }, null, 2)}\n`, writeOptions);
36
- await writeFile(outputPaths.htmlPath, renderPluginExperimentReportHtml(report), writeOptions);
37
- await writeFile(outputPaths.textPath, renderPluginExperimentReportText(report), writeOptions);
37
+ const evidence = collectMetricReportEvidence(args.run);
38
+ await writeFile(outputPaths.htmlPath, renderPluginExperimentReportHtml(report, evidence), writeOptions);
39
+ await writeFile(outputPaths.textPath, renderPluginExperimentReportText(report, evidence), writeOptions);
38
40
  return { report, outputPaths };
39
41
  }
40
42
  function readString(value) {
@@ -26,88 +26,6 @@ function genericSecretFileMode(relativePath) {
26
26
  return "allow-unquoted";
27
27
  return "quoted-only";
28
28
  }
29
- // Deterministic line-by-line comment filter. Preserves the original string
30
- // length and every newline so line numbers computed from the filtered
31
- // content remain identical to the original file; comment content (line or
32
- // block) is replaced with spaces so it can never contribute to a match.
33
- // Quote-aware: a comment marker inside a single- or double-quoted literal
34
- // does not start a comment, and a backslash-escaped quote does not end one.
35
- function stripCodeCommentsPreservingLength(content) {
36
- const result = new Array(content.length);
37
- let index = 0;
38
- let inLineComment = false;
39
- let inBlockComment = false;
40
- let inSingleQuote = false;
41
- let inDoubleQuote = false;
42
- while (index < content.length) {
43
- const ch = content[index];
44
- const next = index + 1 < content.length ? content[index + 1] : "";
45
- if (inLineComment) {
46
- if (ch === "\n") {
47
- inLineComment = false;
48
- result[index] = "\n";
49
- }
50
- else {
51
- result[index] = " ";
52
- }
53
- index++;
54
- continue;
55
- }
56
- if (inBlockComment) {
57
- if (ch === "*" && next === "/") {
58
- result[index] = " ";
59
- result[index + 1] = " ";
60
- inBlockComment = false;
61
- index += 2;
62
- continue;
63
- }
64
- result[index] = ch === "\n" ? "\n" : " ";
65
- index++;
66
- continue;
67
- }
68
- if (inSingleQuote || inDoubleQuote) {
69
- result[index] = ch;
70
- if (ch === "\\" && next !== "") {
71
- result[index + 1] = next;
72
- index += 2;
73
- continue;
74
- }
75
- if ((inSingleQuote && ch === "'") || (inDoubleQuote && ch === '"')) {
76
- inSingleQuote = false;
77
- inDoubleQuote = false;
78
- }
79
- index++;
80
- continue;
81
- }
82
- if (ch === "/" && next === "/") {
83
- inLineComment = true;
84
- result[index] = " ";
85
- index++;
86
- continue;
87
- }
88
- if (ch === "/" && next === "*") {
89
- inBlockComment = true;
90
- result[index] = " ";
91
- index++;
92
- continue;
93
- }
94
- if (ch === "'") {
95
- inSingleQuote = true;
96
- result[index] = ch;
97
- index++;
98
- continue;
99
- }
100
- if (ch === '"') {
101
- inDoubleQuote = true;
102
- result[index] = ch;
103
- index++;
104
- continue;
105
- }
106
- result[index] = ch;
107
- index++;
108
- }
109
- return result.join("");
110
- }
111
29
  const GENERIC_SENSITIVE_SINGLE_WORDS = new Set(["password", "passwd", "secret", "token"]);
112
30
  function splitGenericIdentifierWords(identifier) {
113
31
  const unquoted = identifier.replace(/^['"]|['"]$/g, "");
@@ -159,65 +77,160 @@ function isDynamicUnquotedValue(rawValue) {
159
77
  const QUOTED_ASSIGNMENT_PATTERN = /(?<![A-Za-z0-9_$])(["']?)([A-Za-z_$][A-Za-z0-9_$]*)\1\s*[:=]\s*(["'])((?:\\.|(?!\3)[^\\\r\n])*)\3/g;
160
78
  const UNQUOTED_CONFIG_PATTERN = /^([A-Za-z_][A-Za-z0-9_.-]*)\s*[:=]\s*(\S.*?)\s*$/;
161
79
  const MIN_GENERIC_SECRET_LENGTH = 8;
162
- function scanGenericSecretAssignments(relativePath, content) {
80
+ // A whole-value `${...}` reference is a template/interpolation reference, not
81
+ // a literal credential, even when it appears inside a plain quoted string.
82
+ const WHOLE_VALUE_INTERPOLATION = /^\$\{[^}]*\}$/;
83
+ const IDENTIFIER_PART = /[A-Za-z0-9_$]/;
84
+ function isCandidateSecretValue(identifier, rawValue) {
85
+ if (!isGenericSecretIdentifier(identifier))
86
+ return false;
87
+ if (rawValue.length < MIN_GENERIC_SECRET_LENGTH)
88
+ return false;
89
+ if (WHOLE_VALUE_INTERPOLATION.test(rawValue))
90
+ return false;
91
+ if (isPlaceholder(rawValue))
92
+ return false;
93
+ return true;
94
+ }
95
+ // Deterministic lexical-state scan for code files. Assignments are only
96
+ // recognized in code state, so assignment-shaped text inside an enclosing
97
+ // string literal, template-literal text, or a comment is never reported,
98
+ // while `${...}` expressions inside templates are scanned as code again.
99
+ // Offsets index the original content, so line numbers are exact. Single- and
100
+ // double-quoted strings end at an unescaped newline, as in JavaScript, so an
101
+ // unbalanced quote cannot swallow the rest of the file.
102
+ function scanQuotedCodeAssignments(relativePath, content) {
163
103
  const findings = [];
164
- const mode = genericSecretFileMode(relativePath);
165
- const scannedContent = mode === "quoted-only" ? stripCodeCommentsPreservingLength(content) : content;
166
- const lines = scannedContent.split("\n");
167
- for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
168
- const line = lines[lineIndex];
169
- if (mode === "allow-unquoted") {
170
- const trimmed = line.trim();
171
- if (trimmed.length === 0 || trimmed.startsWith("#") || trimmed.startsWith(";"))
172
- continue;
173
- const unquotedMatch = UNQUOTED_CONFIG_PATTERN.exec(trimmed);
174
- if (!unquotedMatch)
175
- continue;
176
- const identifier = unquotedMatch[1];
177
- if (!isGenericSecretIdentifier(identifier))
178
- continue;
179
- const rawCandidate = unquotedMatch[2];
180
- if (isDynamicUnquotedValue(rawCandidate))
181
- continue;
182
- const rawValue = stripSurroundingQuotes(rawCandidate);
183
- if (rawValue.length < MIN_GENERIC_SECRET_LENGTH)
184
- continue;
185
- if (isPlaceholder(rawValue))
186
- continue;
187
- findings.push({
188
- kind: "generic-secret-assignment",
189
- file: relativePath,
190
- line: lineIndex + 1,
191
- matchedText: `${identifier} ${line.includes(":") ? ":" : "="} [REDACTED]`,
192
- matchLength: rawValue.length,
193
- severity: SEVERITY_FOR_KIND["generic-secret-assignment"],
194
- });
195
- continue;
104
+ const pattern = new RegExp(QUOTED_ASSIGNMENT_PATTERN.source, "y");
105
+ const length = content.length;
106
+ const skipString = (start) => {
107
+ const quote = content[start];
108
+ let i = start + 1;
109
+ while (i < length) {
110
+ const ch = content[i];
111
+ if (ch === "\\")
112
+ i += 2;
113
+ else if (ch === quote)
114
+ return i + 1;
115
+ else if (ch === "\n")
116
+ return i;
117
+ else
118
+ i++;
196
119
  }
197
- QUOTED_ASSIGNMENT_PATTERN.lastIndex = 0;
198
- let match;
199
- while ((match = QUOTED_ASSIGNMENT_PATTERN.exec(line)) !== null) {
200
- const identifier = match[2];
201
- const rawValue = match[4];
202
- if (match[0].length === 0) {
203
- QUOTED_ASSIGNMENT_PATTERN.lastIndex += 1;
120
+ return length;
121
+ };
122
+ const lexTemplate = (start) => {
123
+ let i = start;
124
+ while (i < length) {
125
+ const ch = content[i];
126
+ if (ch === "\\")
127
+ i += 2;
128
+ else if (ch === "`")
129
+ return i + 1;
130
+ else if (ch === "$" && content[i + 1] === "{")
131
+ i = lexCode(i + 2, true);
132
+ else
133
+ i++;
134
+ }
135
+ return length;
136
+ };
137
+ const lexCode = (start, stopAtCloseBrace) => {
138
+ let i = start;
139
+ let depth = 0;
140
+ while (i < length) {
141
+ const ch = content[i];
142
+ const next = i + 1 < length ? content[i + 1] : "";
143
+ if (ch === "/" && next === "/") {
144
+ const end = content.indexOf("\n", i);
145
+ i = end === -1 ? length : end;
204
146
  continue;
205
147
  }
206
- if (!isGenericSecretIdentifier(identifier))
148
+ if (ch === "/" && next === "*") {
149
+ const end = content.indexOf("*/", i + 2);
150
+ i = end === -1 ? length : end + 2;
207
151
  continue;
208
- if (rawValue.length < MIN_GENERIC_SECRET_LENGTH)
152
+ }
153
+ if (ch === "`") {
154
+ i = lexTemplate(i + 1);
209
155
  continue;
210
- if (isPlaceholder(rawValue))
156
+ }
157
+ if (stopAtCloseBrace) {
158
+ if (ch === "{")
159
+ depth++;
160
+ else if (ch === "}") {
161
+ if (depth === 0)
162
+ return i + 1;
163
+ depth--;
164
+ }
165
+ }
166
+ const isQuote = ch === '"' || ch === "'";
167
+ if (isQuote || /[A-Za-z_$]/.test(ch)) {
168
+ pattern.lastIndex = i;
169
+ const match = pattern.exec(content);
170
+ if (match && match[0].length > 0) {
171
+ if (isCandidateSecretValue(match[2], match[4])) {
172
+ findings.push({
173
+ kind: "generic-secret-assignment",
174
+ file: relativePath,
175
+ line: lineNumberAt(content, match.index),
176
+ matchedText: `${match[2]} [REDACTED]`,
177
+ matchLength: match[4].length,
178
+ severity: SEVERITY_FOR_KIND["generic-secret-assignment"],
179
+ });
180
+ }
181
+ i = match.index + match[0].length;
182
+ continue;
183
+ }
184
+ if (isQuote) {
185
+ i = skipString(i);
186
+ continue;
187
+ }
188
+ let j = i + 1;
189
+ while (j < length && IDENTIFIER_PART.test(content[j]))
190
+ j++;
191
+ i = j;
211
192
  continue;
212
- findings.push({
213
- kind: "generic-secret-assignment",
214
- file: relativePath,
215
- line: lineIndex + 1,
216
- matchedText: `${identifier} [REDACTED]`,
217
- matchLength: rawValue.length,
218
- severity: SEVERITY_FOR_KIND["generic-secret-assignment"],
219
- });
193
+ }
194
+ i++;
220
195
  }
196
+ return length;
197
+ };
198
+ lexCode(0, false);
199
+ return findings;
200
+ }
201
+ function scanGenericSecretAssignments(relativePath, content) {
202
+ const mode = genericSecretFileMode(relativePath);
203
+ if (mode === "quoted-only")
204
+ return scanQuotedCodeAssignments(relativePath, content);
205
+ const findings = [];
206
+ const lines = content.split("\n");
207
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
208
+ const line = lines[lineIndex];
209
+ const trimmed = line.trim();
210
+ if (trimmed.length === 0 || trimmed.startsWith("#") || trimmed.startsWith(";"))
211
+ continue;
212
+ const unquotedMatch = UNQUOTED_CONFIG_PATTERN.exec(trimmed);
213
+ if (!unquotedMatch)
214
+ continue;
215
+ const identifier = unquotedMatch[1];
216
+ if (!isGenericSecretIdentifier(identifier))
217
+ continue;
218
+ const rawCandidate = unquotedMatch[2];
219
+ if (isDynamicUnquotedValue(rawCandidate))
220
+ continue;
221
+ const rawValue = stripSurroundingQuotes(rawCandidate);
222
+ if (rawValue.length < MIN_GENERIC_SECRET_LENGTH)
223
+ continue;
224
+ if (isPlaceholder(rawValue))
225
+ continue;
226
+ findings.push({
227
+ kind: "generic-secret-assignment",
228
+ file: relativePath,
229
+ line: lineIndex + 1,
230
+ matchedText: `${identifier} ${line.includes(":") ? ":" : "="} [REDACTED]`,
231
+ matchLength: rawValue.length,
232
+ severity: SEVERITY_FOR_KIND["generic-secret-assignment"],
233
+ });
221
234
  }
222
235
  return findings;
223
236
  }