@dailephd/my-dev-kit-lab 0.9.0 → 0.9.2
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 +21 -0
- package/README.md +10 -3
- package/dist/src/agents/adapters/claudeAdapter.js +6 -3
- package/dist/src/agents/adapters/codexAdapter.js +51 -10
- package/dist/src/agents/index.js +1 -0
- package/dist/src/agents/normalizeAgentTelemetry.js +50 -0
- package/dist/src/agents/parseAgentTokenUsage.js +71 -49
- package/dist/src/agents/providerBuildObservation.js +97 -0
- package/dist/src/audits/core/projectInventory.js +1 -0
- package/dist/src/cli/help.js +8 -2
- package/dist/src/commands/runExperimentDescribeCommand.js +31 -0
- package/dist/src/commands/runExperimentRunCommand.js +193 -5
- package/dist/src/core/runMeasuredCommand.js +4 -0
- package/dist/src/evaluation/benchmarkSandbox/gitExecutor.js +3 -1
- package/dist/src/experiments/plugins/agentSuccessRate/campaignCheckpoint.js +216 -0
- package/dist/src/experiments/plugins/agentSuccessRate/campaignCheckpointStore.js +588 -0
- package/dist/src/experiments/plugins/agentSuccessRate/campaignEvidenceQuality.js +77 -0
- package/dist/src/experiments/plugins/agentSuccessRate/campaignExecution.js +314 -0
- package/dist/src/experiments/plugins/agentSuccessRate/campaignIdentity.js +111 -0
- package/dist/src/experiments/plugins/agentSuccessRate/campaignProgress.js +175 -0
- package/dist/src/experiments/plugins/agentSuccessRate/campaignQueue.js +11 -0
- package/dist/src/experiments/plugins/agentSuccessRate/campaignRecovery.js +256 -0
- package/dist/src/experiments/plugins/agentSuccessRate/campaignResumableExecution.js +207 -0
- package/dist/src/experiments/plugins/agentSuccessRate/index.js +6 -0
- package/dist/src/experiments/plugins/agentSuccessRate/plugin.js +32 -22
- package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +29 -7
- package/dist/src/experiments/plugins/agentSuccessRate/timeoutPresets.js +9 -0
- package/dist/src/report/experiments/agentSuccessCampaignProgressReportModel.js +1 -0
- package/dist/src/report/experiments/buildAgentSuccessCampaignProgressReport.js +6 -0
- package/dist/src/report/experiments/buildAgentSuccessRateReport.js +11 -0
- package/dist/src/report/experiments/buildPluginExperimentReport.js +1 -1
- package/dist/src/report/experiments/renderAgentSuccessCampaignProgressHtml.js +6 -0
- package/dist/src/report/experiments/renderAgentSuccessCampaignProgressText.js +25 -0
- package/dist/src/report/experiments/renderAgentSuccessRateHtml.js +3 -1
- package/dist/src/report/experiments/renderAgentSuccessRateText.js +7 -0
- package/dist/src/report/experiments/writeAgentSuccessCampaignProgressReport.js +139 -0
- package/dist/src/report/experiments/writePluginExperimentReports.js +4 -3
- package/dist/src/securityValidation/attackScenarios/payloadCorpus.js +0 -0
- package/dist/src/securityValidation/packageChecks/parseNpmPackDryRun.js +34 -4
- package/dist/src/securityValidation/packageChecks/requiredPackageContents.js +106 -2
- package/dist/src/securityValidation/packageChecks/runPackageChecks.js +31 -16
- package/dist/src/securityValidation/staticScans/semgrep.js +68 -64
- package/dist/src/securityValidation/types.js +0 -4
- package/dist/src/securityValidation/validate/runSecurityValidation.js +1 -1
- package/docs/METRICS.md +9 -0
- package/examples/tutorial-browser/prepare.mjs +13 -13
- package/examples/tutorial-browser/server.mjs +13 -13
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdtemp, readdir, rm } from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { normalizeAgentDurationV091, normalizeAgentTokenUsageV091 } from "../../../agents/index.js";
|
|
4
5
|
import { resolveWithinRoot } from "../../../core/pathSafety.js";
|
|
5
6
|
import { isSameOrInside, resolvePhysicalPath } from "../../../evaluation/benchmarkSandbox/pathPolicy.js";
|
|
6
7
|
import { classifyAgentRunOutcome } from "../../../evaluation/classifyAgentRunOutcome.js";
|
|
@@ -52,18 +53,33 @@ function toProviderStatus(status) {
|
|
|
52
53
|
case "agent-unavailable":
|
|
53
54
|
case "agent-limit-reached":
|
|
54
55
|
case "invalid-output":
|
|
56
|
+
case "not-invoked":
|
|
55
57
|
return status;
|
|
56
58
|
default:
|
|
57
59
|
return "failed";
|
|
58
60
|
}
|
|
59
61
|
}
|
|
62
|
+
/** Runtime-only projection. The evaluation pipeline copies named legacy fields, never this envelope. */
|
|
63
|
+
export function projectAgentSuccessProviderTelemetryV091(result, providerStatus, durationMs) {
|
|
64
|
+
const tokenUsage = normalizeAgentTokenUsageV091(result?.tokenUsage ?? { source: "unavailable" }, result?.tokenUsageReliability ?? "unavailable");
|
|
65
|
+
const conflict = result !== null && result.tokenUsage.source !== result.tokenUsageSource;
|
|
66
|
+
if (conflict) {
|
|
67
|
+
// Retain both identities and reliability; disagreement cannot certify either channel.
|
|
68
|
+
tokenUsage.evidenceLevel = "unavailable";
|
|
69
|
+
tokenUsage.classificationReason = "unattributed-source";
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
normalizedTelemetry: { providerStatus, tokenUsage, duration: normalizeAgentDurationV091(durationMs) },
|
|
73
|
+
tokenUsageSourceConflict: conflict ? { usageSource: result.tokenUsage.source, resultSource: result.tokenUsageSource } : null
|
|
74
|
+
};
|
|
75
|
+
}
|
|
60
76
|
function tokenEvidence(result) {
|
|
61
|
-
const
|
|
77
|
+
const normalized = normalizeAgentTokenUsageV091(result.tokenUsage, result.tokenUsageReliability);
|
|
62
78
|
return {
|
|
63
79
|
// Only provider-, agent-, CLI- or telemetry-reported totals count; a text-length estimate is not provider usage.
|
|
64
|
-
totalTokens: result.tokenUsageSource === "unavailable" ||
|
|
65
|
-
inputTokens:
|
|
66
|
-
outputTokens:
|
|
80
|
+
totalTokens: [result.tokenUsageSource, result.tokenUsage.source].some((source) => source === "unavailable" || source === "estimated-from-text") ? null : normalized.totalTokens,
|
|
81
|
+
inputTokens: normalized.inputTokens,
|
|
82
|
+
outputTokens: normalized.outputTokens,
|
|
67
83
|
source: result.tokenUsageSource,
|
|
68
84
|
reliability: result.tokenUsageReliability
|
|
69
85
|
};
|
|
@@ -95,6 +111,7 @@ export function createRealAgentProposalSource(args) {
|
|
|
95
111
|
const { settings } = args;
|
|
96
112
|
const attemptNumber = args.attemptNumber ?? 1;
|
|
97
113
|
return async ({ treatmentId, invoke, notInvokedReason }) => {
|
|
114
|
+
let runtimeTelemetry = projectAgentSuccessProviderTelemetryV091(null, "not-invoked", null);
|
|
98
115
|
const context = args.contexts[treatmentId];
|
|
99
116
|
const task = args.task;
|
|
100
117
|
const contextArtifactPath = context.text !== null ? agentSuccessContextArtifactPath(task.benchmarkProject, task.caseId, treatmentId) : null;
|
|
@@ -128,6 +145,7 @@ export function createRealAgentProposalSource(args) {
|
|
|
128
145
|
realAgent.providerStatusReason = boundMessage(reason, settings.privateRoots);
|
|
129
146
|
return {
|
|
130
147
|
kind: "no-proposal",
|
|
148
|
+
...runtimeTelemetry,
|
|
131
149
|
realAgent,
|
|
132
150
|
agentDurationMs: measured.agentDurationMs ?? null,
|
|
133
151
|
agentTokenUsage: measured.agentTokenUsage ?? null,
|
|
@@ -138,7 +156,7 @@ export function createRealAgentProposalSource(args) {
|
|
|
138
156
|
if (!invoke) {
|
|
139
157
|
realAgent.context.contextArtifactPath = null;
|
|
140
158
|
realAgent.providerStatusReason = boundMessage(notInvokedReason ?? "not-invoked", settings.privateRoots);
|
|
141
|
-
return { kind: "no-proposal", realAgent, agentDurationMs: null, agentTokenUsage: null, errors: [], contextFile: null };
|
|
159
|
+
return { kind: "no-proposal", ...runtimeTelemetry, realAgent, agentDurationMs: null, agentTokenUsage: null, errors: [], contextFile: null };
|
|
142
160
|
}
|
|
143
161
|
if (context.availability === "unavailable" || context.text === null) {
|
|
144
162
|
return noProposal(`context-unavailable: ${context.reason ?? "unknown"}`, "CONTEXT_UNAVAILABLE", `The ${treatmentId} context could not be built (${context.reason ?? "unknown"}); the provider was not invoked.`);
|
|
@@ -186,6 +204,7 @@ export function createRealAgentProposalSource(args) {
|
|
|
186
204
|
promptVariant: buildTransportVariant(task, treatmentId),
|
|
187
205
|
promptText,
|
|
188
206
|
promptTransport: "stdin",
|
|
207
|
+
trustedExecutable: settings.trustedExecutable,
|
|
189
208
|
timeoutMs: settings.timeoutMs,
|
|
190
209
|
requireAvailable: false,
|
|
191
210
|
env: settings.env,
|
|
@@ -218,7 +237,9 @@ export function createRealAgentProposalSource(args) {
|
|
|
218
237
|
realAgent.providerStatus = "failed";
|
|
219
238
|
if (notNeutral || !realAgent.providerInvoked)
|
|
220
239
|
realAgent.agentArtifactDirectory = null;
|
|
221
|
-
|
|
240
|
+
const agentDurationMs = notNeutral ? null : Date.now() - wallStarted;
|
|
241
|
+
runtimeTelemetry = projectAgentSuccessProviderTelemetryV091(null, realAgent.providerStatus, agentDurationMs);
|
|
242
|
+
return noProposal(notNeutral ? "provider-cwd-not-neutral" : "provider-invocation-error", notNeutral ? "AGENT_CWD_NOT_NEUTRAL" : "PROVIDER_FAILED", notNeutral ? "The provider working directory overlapped a protected root; the provider was not invoked." : `The provider invocation raised an error: ${invocationError ?? "unknown"}`, { agentDurationMs });
|
|
222
243
|
}
|
|
223
244
|
// Provider health is classified with the answer text withheld: patch content must never be read as a limit/timeout.
|
|
224
245
|
const classification = classifyAgentRunOutcome({ agentRunResult: { ...result, finalAnswerText: "" } });
|
|
@@ -234,6 +255,7 @@ export function createRealAgentProposalSource(args) {
|
|
|
234
255
|
};
|
|
235
256
|
const agentDurationMs = Number.isFinite(result.durationMs) && result.durationMs >= 0 ? result.durationMs : Date.now() - wallStarted;
|
|
236
257
|
const agentTokenUsage = tokenEvidence(result);
|
|
258
|
+
runtimeTelemetry = projectAgentSuccessProviderTelemetryV091(result, providerStatus, agentDurationMs);
|
|
237
259
|
if (providerStatus !== "completed") {
|
|
238
260
|
return noProposal(classification.statusReason, PROVIDER_STATUS_CODES[providerStatus] ?? "PROVIDER_FAILED", `Provider ${settings.providerId} ended with status ${providerStatus}: ${classification.statusReason}`, {
|
|
239
261
|
agentDurationMs,
|
|
@@ -245,6 +267,6 @@ export function createRealAgentProposalSource(args) {
|
|
|
245
267
|
}
|
|
246
268
|
realAgent.finalAnswerAvailable = true;
|
|
247
269
|
realAgent.providerStatusReason = null;
|
|
248
|
-
return { kind: "proposal", text: result.finalAnswerText, realAgent, agentDurationMs, agentTokenUsage, errors: [], contextFile };
|
|
270
|
+
return { kind: "proposal", ...runtimeTelemetry, text: result.finalAnswerText, realAgent, agentDurationMs, agentTokenUsage, errors: [], contextFile };
|
|
249
271
|
};
|
|
250
272
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const AGENT_TIMEOUT_PRESETS = Object.freeze({
|
|
2
|
+
codex: Object.freeze({ standard: 240000, extended: 900000, maximum: 1800000 }),
|
|
3
|
+
claude: Object.freeze({ standard: 240000, extended: 900000, maximum: 1800000 })
|
|
4
|
+
});
|
|
5
|
+
export function resolveAgentTimeoutPreset(provider, preset) {
|
|
6
|
+
if (!Object.hasOwn(AGENT_TIMEOUT_PRESETS, provider) || !Object.hasOwn(AGENT_TIMEOUT_PRESETS[provider], preset))
|
|
7
|
+
throw new Error("--timeout-preset must be standard, extended or maximum for codex or claude.");
|
|
8
|
+
return AGENT_TIMEOUT_PRESETS[provider][preset];
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CAMPAIGN_PROGRESS_SCHEMA, validateCampaignProgress } from "../../experiments/plugins/agentSuccessRate/campaignProgress.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { assertValidatedCampaignProgress } from "../../experiments/plugins/agentSuccessRate/campaignProgress.js";
|
|
2
|
+
/** All representations copy the same validated revision; no renderer evaluates tasks. */
|
|
3
|
+
export function buildAgentSuccessCampaignProgressReport(snapshot, boundary) {
|
|
4
|
+
assertValidatedCampaignProgress(snapshot, boundary);
|
|
5
|
+
return structuredClone(snapshot.model);
|
|
6
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateCampaignProgress } from "../../experiments/plugins/agentSuccessRate/campaignProgress.js";
|
|
1
2
|
import { attemptEvidenceOf } from "../../experiments/plugins/agentSuccessRate/attemptEvidence.js";
|
|
2
3
|
import { AGENT_SUCCESS_RATE_PLUGIN_ID, AGENT_SUCCESS_RATE_REAL_AGENT_EXECUTION_MODE, AGENT_SUCCESS_RATE_TREATMENT_IDS } from "../../experiments/plugins/agentSuccessRate/metadata.js";
|
|
3
4
|
import { AGENT_SUCCESS_RATE_REPORT_SCHEMA_VERSION } from "./agentSuccessRateReportModel.js";
|
|
@@ -211,7 +212,17 @@ export function buildAgentSuccessRateReport(run) {
|
|
|
211
212
|
})()
|
|
212
213
|
: null;
|
|
213
214
|
const overall = total === 0 || evaluable === 0 ? "unavailable" : unavailable === 0 ? "complete" : "partial";
|
|
215
|
+
const progress = run.agentSuccessCampaignProgress;
|
|
216
|
+
let campaignEvidence;
|
|
217
|
+
if (progress !== undefined) {
|
|
218
|
+
validateCampaignProgress(progress);
|
|
219
|
+
if (progress.state !== "complete" || !progress.finalScientificArtifactsAvailable)
|
|
220
|
+
throw new Error("Campaign final report requires complete validated evidence.");
|
|
221
|
+
const { schemaVersion, campaignIdentity, checkpointGeneration, checkpointRevision, state, selectedCaseCount, totalTreatmentUnits, unitCounts, caseCounts, comparisons, finalScientificArtifactsAvailable } = progress;
|
|
222
|
+
campaignEvidence = structuredClone({ schemaVersion, campaignIdentity, checkpointGeneration, checkpointRevision, state, selectedCaseCount, totalTreatmentUnits, unitCounts, caseCounts, comparisons, finalScientificArtifactsAvailable });
|
|
223
|
+
}
|
|
214
224
|
return {
|
|
225
|
+
...(campaignEvidence ? { campaignEvidence } : {}),
|
|
215
226
|
schemaVersion: AGENT_SUCCESS_RATE_REPORT_SCHEMA_VERSION,
|
|
216
227
|
identity: {
|
|
217
228
|
runId: run.runId,
|
|
@@ -17,7 +17,7 @@ const RETRIEVAL_QUERY_STRATEGY_COMPARISON_BULK_KEYS = ["caseExecutionEvidence",
|
|
|
17
17
|
// Same for context-pack-generation: the typed section presents execution evidence and analysis.
|
|
18
18
|
const CONTEXT_PACK_GENERATION_BULK_KEYS = ["caseExecutionEvidence", "analysis"];
|
|
19
19
|
// Same for agent-success-rate: the typed section presents execution evidence and analysis.
|
|
20
|
-
const AGENT_SUCCESS_RATE_BULK_KEYS = ["caseExecutionEvidence", "analysis"];
|
|
20
|
+
const AGENT_SUCCESS_RATE_BULK_KEYS = ["caseExecutionEvidence", "analysis", "agentSuccessCampaignProgress"];
|
|
21
21
|
const V043_BULK_ARRAY_KEYS = [
|
|
22
22
|
"v043StageContextExecutions",
|
|
23
23
|
"v043StageContextEvaluations",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { renderAgentSuccessCampaignProgressText } from "./renderAgentSuccessCampaignProgressText.js";
|
|
2
|
+
/** A static accessible projection of the same text; every dynamic character is escaped. */
|
|
3
|
+
export function renderAgentSuccessCampaignProgressHtml(model) {
|
|
4
|
+
const text = renderAgentSuccessCampaignProgressText(model).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
5
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Campaign progress</title></head><body><main><h1>Campaign progress and evidence</h1><pre style="white-space:pre-wrap">${text}</pre></main></body></html>\n`;
|
|
6
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function renderAgentSuccessCampaignProgressText(m) {
|
|
2
|
+
return [
|
|
3
|
+
"Agent Success Rate Campaign Progress", `Schema: ${m.schemaVersion}`, `Campaign: ${m.campaignIdentity}`, `Execution mode: ${m.executionMode}`,
|
|
4
|
+
`Checkpoint revision: ${m.checkpointGeneration} (${m.checkpointRevision})`, `State: ${m.state}`,
|
|
5
|
+
`Completed evaluations: ${m.unitCounts.completed}/${m.totalTreatmentUnits} treatment units`,
|
|
6
|
+
`Completed cases: ${m.caseCounts.completed}/${m.selectedCaseCount}`,
|
|
7
|
+
...Object.entries(m.unitCounts).map(([s, n]) => `Treatment state ${s}: ${n}`),
|
|
8
|
+
...Object.entries(m.caseCounts).map(([s, n]) => `Case state ${s}: ${n}`),
|
|
9
|
+
`Provider limit: ${m.knownProviderLimit}; uncertainty: ${m.knownUncertainty}`,
|
|
10
|
+
`Diagnostics: ${m.diagnostics.join(", ") || "none"}`,
|
|
11
|
+
`Scientific results: ${m.scientificCompleteness}; validated final artifacts: ${m.finalScientificArtifactsAvailable}`,
|
|
12
|
+
`Completed cases: ${m.completedCaseIds.join(", ") || "none"}`, `Unfinished cases: ${m.unfinishedCaseIds.join(", ") || "none"}`,
|
|
13
|
+
...m.cases.flatMap(c => c.treatments.flatMap(t => [
|
|
14
|
+
`${c.caseId} / ${t.treatmentId}: ${t.state}; evaluated task success=${t.evaluatedTaskSuccess ?? "unavailable"}; epochs=${t.epochCount}; historical limits=${t.historicalProviderLimitCount}; diagnostics=${t.diagnostics.join(", ") || "none"}`,
|
|
15
|
+
...t.attempts.map(a => ` Attempt ${a.attemptNumber}: provider invoked=${a.providerInvoked}; Lab duration=${a.duration.durationMs ?? "unavailable"} ms (${a.duration.source}); tokens=${a.tokens.totalTokens ?? "unavailable"}; input=${a.tokens.inputTokens ?? "unavailable"}; output=${a.tokens.outputTokens ?? "unavailable"}; source=${a.tokens.source}; reliability=${a.tokens.reliability}; category=${a.tokens.evidenceLevel}; numeric availability=${a.tokens.numericAvailability}; interpretation=${a.tokenInterpretation}`)
|
|
16
|
+
])),
|
|
17
|
+
"Comparisons (context-pack minus raw-full-file; descriptive matched subset means):",
|
|
18
|
+
...m.comparisons.flatMap(q => [
|
|
19
|
+
`${q.quantity}: ${q.quality}; basis=${q.basis}; measurement=${q.measurementBasis}; matched=${q.matchedCaseIds.length}/${q.selectedCaseCount}; excluded=${q.excludedCaseIds.length}`,
|
|
20
|
+
` Matched: ${q.matchedCaseIds.join(", ") || "none"}; excluded: ${q.excludedCaseIds.join(", ") || "none"}`,
|
|
21
|
+
` Raw mean=${q.rawFullFileMean ?? "unavailable"}; context mean=${q.contextPackMean ?? "unavailable"}; delta=${q.meanDifference ?? "unavailable"}; unit=${q.unit}`,
|
|
22
|
+
...q.reasons.map((r, i) => ` ${r}: ${q.reasonExplanations[i]}`)
|
|
23
|
+
]), "Scientific limitations:", ...m.limitations.map(l => `- ${l}`), ""
|
|
24
|
+
].join("\n");
|
|
25
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AGENT_SUCCESS_BASIS_LABEL, formatAgentSuccessMetric, formatAgentSuccessPairs, formatAgentSuccessTotal } from "./renderAgentSuccessRateText.js";
|
|
1
|
+
import { AGENT_SUCCESS_BASIS_LABEL, formatAgentSuccessMetric, formatAgentSuccessPairs, formatAgentSuccessTotal, renderCampaignEvidenceSummary } from "./renderAgentSuccessRateText.js";
|
|
2
2
|
function escapeHtml(value) {
|
|
3
3
|
return String(value ?? "")
|
|
4
4
|
.replaceAll("&", "&")
|
|
@@ -22,6 +22,8 @@ export function renderAgentSuccessRateHtml(report) {
|
|
|
22
22
|
const realMode = report.identity.executionMode === "real-agent";
|
|
23
23
|
const m = formatAgentSuccessMetric;
|
|
24
24
|
const parts = [];
|
|
25
|
+
if (report.campaignEvidence)
|
|
26
|
+
parts.push(`<section><h2>Campaign evidence</h2><pre>${escapeHtml(renderCampaignEvidenceSummary(report))}</pre></section>`);
|
|
25
27
|
parts.push(`<section id="agent-success-rate">
|
|
26
28
|
<h2>Agent Success Rate</h2>
|
|
27
29
|
<h3>Experiment identity</h3>
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export function renderCampaignEvidenceSummary(report) {
|
|
2
|
+
const m = report.campaignEvidence;
|
|
3
|
+
return m ? [`Campaign evidence: ${m.state}; checkpoint revision ${m.checkpointGeneration} (${m.checkpointRevision}); completed evaluations ${m.unitCounts.completed}/${m.totalTreatmentUnits}; completed cases ${m.caseCounts.completed}/${m.selectedCaseCount}`,
|
|
4
|
+
...m.comparisons.map(q => `${q.quantity}: ${q.quality}; ${q.basis}; ${q.measurementBasis}; matched ${q.matchedCaseIds.length}/${q.selectedCaseCount}; excluded ${q.excludedCaseIds.length}; raw=${q.rawFullFileMean ?? "unavailable"}; context=${q.contextPackMean ?? "unavailable"}; delta=${q.meanDifference ?? "unavailable"}; unit=${q.unit}; reasons=${q.reasons.join(", ") || "none"}; ${q.reasonExplanations.join(" ")}`)].join("\n") : "";
|
|
5
|
+
}
|
|
1
6
|
/** One display form for every metric: an absent value is labelled, never rendered as zero. */
|
|
2
7
|
export function formatAgentSuccessMetric(metric) {
|
|
3
8
|
if (!metric)
|
|
@@ -135,5 +140,7 @@ export function renderAgentSuccessRateTextLines(report) {
|
|
|
135
140
|
lines.push(`error ${error.caseId} / ${error.treatmentId} attempt ${error.attemptNumber} ${sanitize(error.code)}: ${sanitize(error.message)}`);
|
|
136
141
|
for (const artifact of report.artifacts)
|
|
137
142
|
lines.push(`artifact ${sanitize(artifact.id)}: ${artifact.path ?? "[reference withheld]"}`);
|
|
143
|
+
if (report.campaignEvidence)
|
|
144
|
+
lines.push(renderCampaignEvidenceSummary(report));
|
|
138
145
|
return lines;
|
|
139
146
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { lstat, mkdir, open, readdir, realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { campaignAssert, campaignHash, canonicalCampaignJson } from "../../experiments/plugins/agentSuccessRate/campaignIdentity.js";
|
|
4
|
+
import { validateCampaignOutputPath } from "../../experiments/plugins/agentSuccessRate/campaignCheckpointStore.js";
|
|
5
|
+
import { readCampaignProgressScientificFamily } from "../../experiments/plugins/agentSuccessRate/campaignProgress.js";
|
|
6
|
+
import { resolvePhysicalPath } from "../../evaluation/benchmarkSandbox/pathPolicy.js";
|
|
7
|
+
import { agentSuccessRatePlugin, mapAgentSuccessRateToRun } from "../../experiments/plugins/agentSuccessRate/plugin.js";
|
|
8
|
+
import { writePluginExperimentReports } from "./writePluginExperimentReports.js";
|
|
9
|
+
import { buildPluginExperimentReport } from "./buildPluginExperimentReport.js";
|
|
10
|
+
import { renderPluginExperimentReportText } from "./renderPluginExperimentReportText.js";
|
|
11
|
+
import { renderPluginExperimentReportHtml } from "./renderPluginExperimentReportHtml.js";
|
|
12
|
+
import { buildAgentSuccessCampaignProgressReport } from "./buildAgentSuccessCampaignProgressReport.js";
|
|
13
|
+
import { renderAgentSuccessCampaignProgressText } from "./renderAgentSuccessCampaignProgressText.js";
|
|
14
|
+
import { renderAgentSuccessCampaignProgressHtml } from "./renderAgentSuccessCampaignProgressHtml.js";
|
|
15
|
+
async function plain(p, kind) {
|
|
16
|
+
const s = await lstat(p);
|
|
17
|
+
campaignAssert(!s.isSymbolicLink() && (kind === "file" ? s.isFile() : s.isDirectory()) && path.resolve(await realpath(p)) === path.resolve(p), "PROGRESS_UNSAFE_PATH");
|
|
18
|
+
}
|
|
19
|
+
async function writeExclusive(p, content) {
|
|
20
|
+
const h = await open(p, "wx");
|
|
21
|
+
try {
|
|
22
|
+
await h.writeFile(content, "utf8");
|
|
23
|
+
await h.sync();
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
await h.close();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function readExpected(p, expected, code) {
|
|
30
|
+
await plain(p, "file");
|
|
31
|
+
const length = Buffer.byteLength(expected);
|
|
32
|
+
campaignAssert(length <= 64 * 1024 * 1024, "PROGRESS_DATA_BOUND");
|
|
33
|
+
const h = await open(p, "r");
|
|
34
|
+
try {
|
|
35
|
+
const before = await h.stat();
|
|
36
|
+
campaignAssert(before.isFile() && before.size === length, code);
|
|
37
|
+
const buffer = Buffer.alloc(length + 1);
|
|
38
|
+
let size = 0;
|
|
39
|
+
for (;;) {
|
|
40
|
+
const n = await h.read(buffer, size, buffer.length - size, size);
|
|
41
|
+
size += n.bytesRead;
|
|
42
|
+
if (!n.bytesRead || size === buffer.length)
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
campaignAssert(size === length && (await h.stat()).size === length && buffer.subarray(0, size).toString("utf8") === expected, code);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
await h.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function owned(parent, name, owner) {
|
|
52
|
+
const p = path.join(parent, name);
|
|
53
|
+
let created = false;
|
|
54
|
+
try {
|
|
55
|
+
await mkdir(p);
|
|
56
|
+
created = true;
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
if (e.code !== "EEXIST")
|
|
60
|
+
throw e;
|
|
61
|
+
}
|
|
62
|
+
await plain(p, "directory");
|
|
63
|
+
if (created)
|
|
64
|
+
await writeExclusive(path.join(p, "owner.json"), owner);
|
|
65
|
+
await readExpected(path.join(p, "owner.json"), owner, "PROGRESS_OWNER_MISMATCH");
|
|
66
|
+
return p;
|
|
67
|
+
}
|
|
68
|
+
/** Revision-specific files are exclusive. The manifest is published last and verified before returning success. */
|
|
69
|
+
export async function writeAgentSuccessCampaignProgressReport(args) {
|
|
70
|
+
const model = buildAgentSuccessCampaignProgressReport(args.snapshot, args.boundary);
|
|
71
|
+
await validateCampaignOutputPath(args.boundary);
|
|
72
|
+
// The checkpoint store has already validated this output root. Use the same physical identity for
|
|
73
|
+
// publication: a legitimate OS alias (for example macOS /var -> /private/var) is not a redirect
|
|
74
|
+
// created inside the campaign tree, and comparing it to the lexical spelling rejects valid roots.
|
|
75
|
+
const root = await resolvePhysicalPath(args.boundary.outputRoot);
|
|
76
|
+
await plain(root, "directory");
|
|
77
|
+
const family = await owned(root, "campaign-progress-v1", "agent-success-rate-campaign-progress-v1");
|
|
78
|
+
const identity = await owned(family, model.campaignIdentity, `agent-success-rate-campaign-progress-v1:${model.campaignIdentity}`);
|
|
79
|
+
// Ownership blockers can change without a checkpoint transition. Keep both projections immutable at that revision.
|
|
80
|
+
const modelDigest = campaignHash(canonicalCampaignJson(model));
|
|
81
|
+
const revision = `revision-${String(model.checkpointGeneration).padStart(10, "0")}-${model.checkpointRevision}-${modelDigest.slice(0, 16)}`;
|
|
82
|
+
const outDir = path.join(identity, revision);
|
|
83
|
+
const scientific = model.state === "complete" ? await readCampaignProgressScientificFamily(args.snapshot, args.boundary) : null;
|
|
84
|
+
const run = scientific ? mapAgentSuccessRateToRun({ runId: scientific.execution.runId, startedAt: scientific.execution.startedAt, completedAt: scientific.execution.completedAt,
|
|
85
|
+
target: { kind: "self", targetRoot: "[redacted]", toolRoot: "[redacted]", packageName: "@dailephd/my-dev-kit-lab", packageVersion: "0.9.0", hasPackageJson: true, hasLockfile: true, branch: null, commit: null, hasGit: false, isSelf: true },
|
|
86
|
+
caseEvidence: scientific.execution.cases.map(c => ({ ...c, caseName: c.caseId })), analysis: scientific.analysis.analysis, patchArtifactPaths: [],
|
|
87
|
+
providerId: scientific.execution.realAgent?.providerId, ...(scientific.execution.realAgent ? { realSettings: { timeoutMs: scientific.execution.realAgent.timeoutMs, repairAttempts: scientific.execution.realAgent.repairAttempts ?? 0 } } : {}) }) : null;
|
|
88
|
+
if (run)
|
|
89
|
+
Object.assign(run, { agentSuccessCampaignProgress: model });
|
|
90
|
+
const files = [
|
|
91
|
+
{ name: "agent-success-rate-campaign-progress.json", content: `${JSON.stringify(model, null, 2)}\n`, phase: "json-written" },
|
|
92
|
+
{ name: "agent-success-rate-campaign-progress.txt", content: renderAgentSuccessCampaignProgressText(model), phase: "text-written" },
|
|
93
|
+
{ name: "agent-success-rate-campaign-progress.html", content: renderAgentSuccessCampaignProgressHtml(model), phase: "html-written" }
|
|
94
|
+
];
|
|
95
|
+
if (run) {
|
|
96
|
+
const built = buildPluginExperimentReport({ run, plugin: agentSuccessRatePlugin.metadata, outputRoot: outDir, generatedAt: run.completedAt });
|
|
97
|
+
const report = { ...built, metadata: { ...built.metadata, outputRoot: "[redacted]" } };
|
|
98
|
+
files.push({ name: "report.json", content: `${JSON.stringify({ report, outputPaths: { outDir: "[redacted]", jsonPath: "report.json", htmlPath: "report.html", textPath: "report.txt" } }, null, 2)}\n`, phase: "json-written" }, { name: "report.txt", content: renderPluginExperimentReportText(report), phase: "text-written" }, { name: "report.html", content: renderPluginExperimentReportHtml(report), phase: "html-written" });
|
|
99
|
+
}
|
|
100
|
+
const marker = canonicalCampaignJson({ schemaVersion: "agent-success-rate-campaign-progress-publication-v1", campaignIdentity: model.campaignIdentity, checkpointGeneration: model.checkpointGeneration, checkpointRevision: model.checkpointRevision, modelDigest, files: files.map(f => ({ name: f.name, sha256: campaignHash(f.content), bytes: Buffer.byteLength(f.content) })) });
|
|
101
|
+
let created = false;
|
|
102
|
+
try {
|
|
103
|
+
await mkdir(outDir);
|
|
104
|
+
created = true;
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
if (e.code !== "EEXIST")
|
|
108
|
+
throw e;
|
|
109
|
+
}
|
|
110
|
+
await plain(outDir, "directory");
|
|
111
|
+
if (created) {
|
|
112
|
+
for (const file of files.slice(0, 3)) {
|
|
113
|
+
await writeExclusive(path.join(outDir, file.name), file.content);
|
|
114
|
+
await args.failureHook?.(file.phase);
|
|
115
|
+
}
|
|
116
|
+
// Exclusive directory reservation owns these ordinary report names; the existing writer remains their owner.
|
|
117
|
+
if (run)
|
|
118
|
+
await writePluginExperimentReports({ run, plugin: agentSuccessRatePlugin.metadata, outputRoot: outDir, generatedAt: run.completedAt, redactOutputRoot: true, exclusive: true });
|
|
119
|
+
for (const f of files)
|
|
120
|
+
await readExpected(path.join(outDir, f.name), f.content, "PROGRESS_SNAPSHOT_INTEGRITY");
|
|
121
|
+
// The legacy writer owns report rendering. Flush its campaign-only output before committing the group marker.
|
|
122
|
+
for (const f of files.slice(3)) {
|
|
123
|
+
const h = await open(path.join(outDir, f.name), "r+");
|
|
124
|
+
try {
|
|
125
|
+
await h.sync();
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
await h.close();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
await args.failureHook?.("before-manifest");
|
|
132
|
+
await writeExclusive(path.join(outDir, "manifest.json"), marker);
|
|
133
|
+
}
|
|
134
|
+
campaignAssert((await readdir(outDir)).sort().join(",") === [...files.map(f => f.name), "manifest.json"].sort().join(","), "PROGRESS_INCOMPLETE_OR_UNKNOWN_SNAPSHOT");
|
|
135
|
+
await readExpected(path.join(outDir, "manifest.json"), marker, "PROGRESS_SNAPSHOT_COLLISION");
|
|
136
|
+
for (const f of files)
|
|
137
|
+
await readExpected(path.join(outDir, f.name), f.content, "PROGRESS_SNAPSHOT_INTEGRITY");
|
|
138
|
+
return { model, outputPaths: { outDir, jsonPath: path.join(outDir, files[0].name), textPath: path.join(outDir, files[1].name), htmlPath: path.join(outDir, files[2].name), manifestPath: path.join(outDir, "manifest.json") } };
|
|
139
|
+
}
|
|
@@ -31,9 +31,10 @@ export async function writePluginExperimentReports(args) {
|
|
|
31
31
|
const serializedPaths = args.redactOutputRoot
|
|
32
32
|
? { outDir: "[redacted]", jsonPath: "report.json", htmlPath: "report.html", textPath: "report.txt" }
|
|
33
33
|
: outputPaths;
|
|
34
|
-
|
|
35
|
-
await writeFile(outputPaths.
|
|
36
|
-
await writeFile(outputPaths.
|
|
34
|
+
const writeOptions = args.exclusive ? { encoding: "utf8", flag: "wx" } : "utf8";
|
|
35
|
+
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
38
|
return { report, outputPaths };
|
|
38
39
|
}
|
|
39
40
|
function readString(value) {
|
|
Binary file
|
|
@@ -13,6 +13,29 @@ export function parseNpmPackDryRun(stdout) {
|
|
|
13
13
|
if (!stdout.trim()) {
|
|
14
14
|
return { files: [], parseError: "npm pack --dry-run produced no output" };
|
|
15
15
|
}
|
|
16
|
+
if (/^[\[{]/.test(stdout.trim())) {
|
|
17
|
+
try {
|
|
18
|
+
const inventory = JSON.parse(stdout);
|
|
19
|
+
if (!Array.isArray(inventory) || inventory.length !== 1 ||
|
|
20
|
+
!inventory[0] || !Array.isArray(inventory[0].files) || !inventory[0].files.length) {
|
|
21
|
+
return { files: [], parseError: "npm pack JSON must contain exactly one nonempty package inventory" };
|
|
22
|
+
}
|
|
23
|
+
const files = [];
|
|
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
|
+
}
|
|
38
|
+
}
|
|
16
39
|
const files = [];
|
|
17
40
|
let inContents = false;
|
|
18
41
|
let totalSize;
|
|
@@ -35,10 +58,8 @@ export function parseNpmPackDryRun(stdout) {
|
|
|
35
58
|
if (match?.[1]) {
|
|
36
59
|
files.push(match[1].trim());
|
|
37
60
|
}
|
|
38
|
-
else
|
|
39
|
-
|
|
40
|
-
files.push(line);
|
|
41
|
-
}
|
|
61
|
+
else
|
|
62
|
+
return { files: [], parseError: "Malformed npm tarball contents line" };
|
|
42
63
|
}
|
|
43
64
|
const totalMatch = line.match(/total files\s*:\s*(\d+)/i) ?? line.match(/unpacked size\s*:\s*([\d.]+\s*[kKmMgGbB]+)/i);
|
|
44
65
|
if (totalMatch?.[1]) {
|
|
@@ -52,5 +73,14 @@ export function parseNpmPackDryRun(stdout) {
|
|
|
52
73
|
parseError: "npm pack --dry-run: no files detected in tarball contents section",
|
|
53
74
|
};
|
|
54
75
|
}
|
|
76
|
+
if (files.some((file) => !safeInventoryPath(file))) {
|
|
77
|
+
return { files: [], parseError: "npm pack contains an unsafe file path" };
|
|
78
|
+
}
|
|
55
79
|
return { files, totalSize };
|
|
56
80
|
}
|
|
81
|
+
function safeInventoryPath(file) {
|
|
82
|
+
const normalized = file.replace(/\\/g, "/").replace(/^(\.\/)+/, "");
|
|
83
|
+
return Boolean(normalized) && !normalized.startsWith("/") &&
|
|
84
|
+
!/[\x00-\x1f\x7f:]/.test(normalized) &&
|
|
85
|
+
normalized.split("/").every((part) => Boolean(part) && part !== "." && part !== "..");
|
|
86
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
export const REQUIRED_PACKAGE_CONTENTS = [
|
|
2
4
|
"CHANGELOG.md",
|
|
3
5
|
"LICENSE",
|
|
@@ -20,15 +22,117 @@ export const REQUIRED_PACKAGE_CONTENTS = [
|
|
|
20
22
|
"examples/token-savings-cases.json",
|
|
21
23
|
];
|
|
22
24
|
function normalizeTarballPath(file) {
|
|
23
|
-
const normalized = file.replace(/\\/g, "/");
|
|
25
|
+
const normalized = file.replace(/\\/g, "/").replace(/^(\.\/)+/, "");
|
|
24
26
|
return normalized.startsWith("package/")
|
|
25
27
|
? normalized.slice("package/".length)
|
|
26
28
|
: normalized;
|
|
27
29
|
}
|
|
30
|
+
// Policy belongs beside the existing required-file detector. Identity is
|
|
31
|
+
// supplied by the existing target resolver, never inferred from package name.
|
|
32
|
+
export async function requiredContentsForTarget(target) {
|
|
33
|
+
const required = new Set(target.isSelf ? REQUIRED_PACKAGE_CONTENTS : ["package.json"]);
|
|
34
|
+
const findings = [];
|
|
35
|
+
function invalid(field, reason) {
|
|
36
|
+
findings.push({
|
|
37
|
+
id: `npm-pack-metadata-${findings.length}`,
|
|
38
|
+
title: `Package metadata cannot be validated: ${field}`,
|
|
39
|
+
severity: "blocker",
|
|
40
|
+
category: "package-content",
|
|
41
|
+
description: reason,
|
|
42
|
+
evidence: `package.json field: ${field}; ${reason}`,
|
|
43
|
+
affectedFiles: ["package.json"],
|
|
44
|
+
recommendation: "Provide readable package metadata with supported, safe package-relative entrypoint paths and rerun validation.",
|
|
45
|
+
releaseImpact: "Blocker: required package evidence is incomplete",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function entry(value, field, exportsPath = false) {
|
|
49
|
+
if (typeof value !== "string") {
|
|
50
|
+
invalid(field, "Malformed entrypoint: expected a nonempty string.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const normalized = value.replace(/\\/g, "/");
|
|
54
|
+
const relative = normalized.replace(/^(\.\/)+/, "");
|
|
55
|
+
if (!relative || normalized !== normalized.trim() ||
|
|
56
|
+
/[\x00-\x1f\x7f:*?#%]/.test(normalized) || normalized.startsWith("/") ||
|
|
57
|
+
relative.split("/").some((part) => !part || part === ".." || part === "." || part === "node_modules") ||
|
|
58
|
+
(exportsPath && !normalized.startsWith("./"))) {
|
|
59
|
+
invalid(field, "Unsafe or unsupported entrypoint path (absolute, escaping, pattern, URL, or ambiguous path).");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
required.add(relative);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const metadata = JSON.parse(await fs.readFile(path.join(target.targetRoot, "package.json"), "utf8"));
|
|
66
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
67
|
+
invalid("package.json", "Malformed package metadata: expected an object.");
|
|
68
|
+
}
|
|
69
|
+
else if (!target.isSelf) {
|
|
70
|
+
const pkg = metadata;
|
|
71
|
+
if ("bin" in pkg) {
|
|
72
|
+
if (typeof pkg.bin === "string")
|
|
73
|
+
entry(pkg.bin, "bin");
|
|
74
|
+
else if (pkg.bin && typeof pkg.bin === "object" && !Array.isArray(pkg.bin)) {
|
|
75
|
+
for (const key of Object.keys(pkg.bin).sort()) {
|
|
76
|
+
if (!key || key === "." || key === ".." || /[\\/:\x00-\x1f\x7f]/.test(key)) {
|
|
77
|
+
invalid("bin", "Unsafe or unsupported executable name in bin map.");
|
|
78
|
+
}
|
|
79
|
+
else
|
|
80
|
+
entry(pkg.bin[key], `bin.${key}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else
|
|
84
|
+
invalid("bin", "Malformed bin: expected a string or object map.");
|
|
85
|
+
}
|
|
86
|
+
if ("main" in pkg) {
|
|
87
|
+
if (typeof pkg.main === "string" && pkg.main && !path.posix.extname(pkg.main.replace(/\\/g, "/"))) {
|
|
88
|
+
invalid("main", "Unsupported or ambiguous main: extensionless or directory resolution is not validated; declare an exact runtime file.");
|
|
89
|
+
}
|
|
90
|
+
else
|
|
91
|
+
entry(pkg.main, "main");
|
|
92
|
+
}
|
|
93
|
+
if ("exports" in pkg && pkg.exports !== null) {
|
|
94
|
+
if (typeof pkg.exports === "string")
|
|
95
|
+
entry(pkg.exports, "exports", true);
|
|
96
|
+
else if (pkg.exports && typeof pkg.exports === "object" && !Array.isArray(pkg.exports)) {
|
|
97
|
+
const exports = pkg.exports;
|
|
98
|
+
const keys = Object.keys(exports).sort();
|
|
99
|
+
if (!keys.length || keys.some((key) => key !== "." && (!key.startsWith("./") || /[\\:*?#%\x00-\x1f\x7f]/.test(key) ||
|
|
100
|
+
key.slice(2).split("/").some((part) => !part || part === "." || part === "..")))) {
|
|
101
|
+
invalid("exports", "Unsupported or ambiguous exports: conditional or pattern mappings require additional evidence.");
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
for (const key of keys) {
|
|
105
|
+
if (exports[key] === null)
|
|
106
|
+
continue; // Explicitly excluded subpath.
|
|
107
|
+
if (typeof exports[key] !== "string") {
|
|
108
|
+
invalid(`exports.${key}`, "Unsupported exports shape: conditional objects and fallback arrays are not validated.");
|
|
109
|
+
}
|
|
110
|
+
else
|
|
111
|
+
entry(exports[key], `exports.${key}`, true);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else
|
|
115
|
+
invalid("exports", "Unsupported exports shape: expected an exact string or subpath map.");
|
|
116
|
+
}
|
|
117
|
+
// npm automatically ships existing root README and license files. Do not
|
|
118
|
+
// invent a documentation requirement for packages without those files.
|
|
119
|
+
for (const file of (await fs.readdir(target.targetRoot, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
120
|
+
if (file.isFile() && /^(readme|licen[cs]e)(\..*)?$/i.test(file.name))
|
|
121
|
+
required.add(file.name);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
invalid("package.json", "Package metadata is missing, unreadable, or unparseable.");
|
|
127
|
+
}
|
|
128
|
+
return { requiredFiles: [...required].sort(), findings };
|
|
129
|
+
}
|
|
28
130
|
export function detectMissingRequiredContents(options) {
|
|
29
131
|
const present = new Set(options.files.map(normalizeTarballPath));
|
|
30
132
|
const required = [...(options.requiredFiles ?? REQUIRED_PACKAGE_CONTENTS)]
|
|
31
|
-
.
|
|
133
|
+
// Requirements are package-relative declarations, not archive paths. A
|
|
134
|
+
// literal package/ directory in an entrypoint must not be stripped.
|
|
135
|
+
.map((file) => file.replace(/\\/g, "/").replace(/^(\.\/)+/, ""))
|
|
32
136
|
.sort((left, right) => left.localeCompare(right));
|
|
33
137
|
const missing = required.filter((file) => !present.has(file));
|
|
34
138
|
const findings = missing.map((file, index) => ({
|