@dailephd/my-dev-kit-lab 0.6.2 → 0.6.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.
- package/CHANGELOG.md +18 -0
- package/README.md +9 -8
- package/dist/src/cli/help.js +16 -9
- package/dist/src/commands/runExperimentDescribeCommand.js +5 -2
- package/dist/src/commands/runExperimentRunCommand.js +0 -4
- package/dist/src/evaluation/benchmarkMetadata.js +5 -1
- package/dist/src/evaluation/parseAgentAnswer.js +172 -24
- package/dist/src/evaluation/runMyDevKitRetrieval.js +120 -7
- package/dist/src/experiments/plugins/incrementalChangeStaleness/comparisonV2.js +49 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/config.js +5 -4
- package/dist/src/experiments/plugins/incrementalChangeStaleness/disposableTarget.js +20 -2
- package/dist/src/experiments/plugins/incrementalChangeStaleness/execution.js +42 -16
- package/dist/src/experiments/plugins/incrementalChangeStaleness/executionArtifact.js +4 -4
- package/dist/src/experiments/plugins/incrementalChangeStaleness/executionArtifactV2.js +289 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/executionV2.js +195 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/indexClone.js +76 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecycle.js +64 -32
- package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecyclePolicy.js +6 -4
- package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecyclePolicyV2.js +78 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecycleV2.js +298 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/plugin.js +213 -12
- package/dist/src/experiments/plugins/incrementalChangeStaleness/treatmentSessionV2.js +5 -0
- package/dist/src/experiments/plugins/warmIndexReuse/warmIndexSession.js +3 -1
- package/dist/src/mobile/android/advancedSecurity/candidateEvidence.js +9 -2
- package/dist/src/mobile/android/advancedSecurity/redaction.js +19 -10
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/analyzeSecretCandidates.js +12 -2
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/analyzeSigningConfiguration.js +1 -1
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/extractSigningConfigurations.js +7 -7
- package/dist/src/report/experiments/buildIncrementalChangeStalenessPluginReport.js +33 -0
- package/dist/src/report/experiments/buildIncrementalChangeStalenessReport.js +1 -1
- package/dist/src/report/experiments/buildIncrementalChangeStalenessReportV2.js +80 -0
- package/dist/src/report/experiments/buildPluginExperimentReport.js +2 -2
- package/dist/src/report/experiments/incrementalChangeStalenessReportModelV2.js +3 -0
- package/dist/src/report/experiments/incrementalChangeStalenessReportRowsV2.js +146 -0
- package/dist/src/report/experiments/index.js +3 -0
- package/dist/src/report/experiments/renderIncrementalChangeStalenessHtml.js +4 -0
- package/dist/src/report/experiments/renderIncrementalChangeStalenessHtmlV2.js +111 -0
- package/dist/src/report/experiments/renderIncrementalChangeStalenessTextV2.js +96 -0
- package/dist/src/report/experiments/renderPluginExperimentReportText.js +6 -0
- package/dist/src/securityValidation/staticScans/codeql.js +8 -8
- package/docs/METRICS.md +32 -0
- package/examples/tutorial-browser/prepare.mjs +13 -13
- package/examples/tutorial-browser/server.mjs +13 -13
- package/package.json +1 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { resolveWithinRoot } from "../../../core/pathSafety.js";
|
|
3
|
+
import { classifyStaleRisk, compareCorrectness, compareRequiredFileEvidence } from "./comparison.js";
|
|
4
|
+
import { compareCandidateCorrectness, compareCandidateRequiredFileEvidence, classifyPartialRefreshAgainstFull } from "./comparisonV2.js";
|
|
5
|
+
import { INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_IDS, INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_INTENTS } from "./disposableTarget.js";
|
|
6
|
+
import { assessIncrementalChangeStalenessAffectedNeighborhood, checkIncrementalChangeStalenessAffectedNeighborhoodSymmetry, correctnessComparable, defaultIncrementalChangeStalenessExecutionDeps, executeTreatmentEvaluation, INCONCLUSIVE_NO_EXECUTION, resolveIncrementalChangeStalenessScenarioTask } from "./execution.js";
|
|
7
|
+
/**
|
|
8
|
+
* Four-way check with stale-index only as the deterministic reference. Reuses the v0.6.2 pairwise check
|
|
9
|
+
* (status, relationship, reindexRecommendation, and the six numeric metrics) and additionally requires
|
|
10
|
+
* equal seedNodeCount. Conflicting evidence is never averaged or merged.
|
|
11
|
+
*/
|
|
12
|
+
export function checkAffectedNeighborhoodFourWaySymmetry(assessments) {
|
|
13
|
+
const reference = assessments["stale-index"];
|
|
14
|
+
const reasons = [];
|
|
15
|
+
for (const treatmentId of INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_IDS) {
|
|
16
|
+
if (treatmentId === "stale-index")
|
|
17
|
+
continue;
|
|
18
|
+
const other = assessments[treatmentId];
|
|
19
|
+
const pairwise = checkIncrementalChangeStalenessAffectedNeighborhoodSymmetry(reference, other);
|
|
20
|
+
if (!pairwise.symmetric)
|
|
21
|
+
reasons.push(`${treatmentId}: ${pairwise.reason}`);
|
|
22
|
+
if (reference.seedNodeCount !== other.seedNodeCount) {
|
|
23
|
+
reasons.push(`${treatmentId}: seedNodeCount (${reference.seedNodeCount} vs ${other.seedNodeCount})`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return reasons.length === 0 ? { symmetric: true } : { symmetric: false, reason: `Affected-neighborhood assessments are asymmetric across treatments: ${reasons.join(" | ")}` };
|
|
27
|
+
}
|
|
28
|
+
const PARTIAL_TREATMENT_IDS = ["changed-files-refresh", "affected-neighborhood-refresh"];
|
|
29
|
+
function placeholderComparisons() {
|
|
30
|
+
return [
|
|
31
|
+
{ kind: "stale-risk", candidateTreatmentId: "stale-index", referenceTreatmentId: "full-refresh", comparison: { ...INCONCLUSIVE_NO_EXECUTION, reasonCodes: [...INCONCLUSIVE_NO_EXECUTION.reasonCodes] } },
|
|
32
|
+
...PARTIAL_TREATMENT_IDS.map((candidateTreatmentId) => ({
|
|
33
|
+
kind: "partial-refresh",
|
|
34
|
+
candidateTreatmentId,
|
|
35
|
+
referenceTreatmentId: "full-refresh",
|
|
36
|
+
refreshRealization: null,
|
|
37
|
+
correctnessRelation: "unknown",
|
|
38
|
+
requiredFileEvidenceRelation: "unknown",
|
|
39
|
+
classification: "inconclusive",
|
|
40
|
+
reasonCodes: ["correctness-unavailable", "required-file-evidence-unavailable"]
|
|
41
|
+
}))
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
function refreshExecutionOf(session, treatmentId) {
|
|
45
|
+
const treatment = session.treatments[treatmentId];
|
|
46
|
+
if (treatment.refreshRealization !== null) {
|
|
47
|
+
const incrementalRefresh = treatment.refreshedIndex.incrementalRefresh;
|
|
48
|
+
if (!incrementalRefresh) {
|
|
49
|
+
throw new Error(`${treatmentId} lifecycle reported ${treatment.refreshRealization} without upstream incrementalRefresh evidence.`);
|
|
50
|
+
}
|
|
51
|
+
return { kind: "incremental", realization: treatment.refreshRealization, incrementalRefresh: structuredClone(incrementalRefresh) };
|
|
52
|
+
}
|
|
53
|
+
return treatment.postMutationIndexBuilt
|
|
54
|
+
? { kind: "full", realization: "FULL_REFRESH", incrementalRefresh: null }
|
|
55
|
+
: { kind: "no-refresh", realization: "NO_REFRESH", incrementalRefresh: null };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Executes the four matched treatments in fixed order for one lifecycle-ready V2 session, then builds
|
|
59
|
+
* exactly three comparisons against full-refresh. A lifecycle that is not ready, or an affected-neighborhood
|
|
60
|
+
* asymmetry, is a failed scenario with no fabricated treatment evidence.
|
|
61
|
+
*/
|
|
62
|
+
export async function executeIncrementalChangeStalenessScenarioV2(options) {
|
|
63
|
+
const deps = { ...defaultIncrementalChangeStalenessExecutionDeps, ...options.deps };
|
|
64
|
+
const scenario = options.scenario;
|
|
65
|
+
if (options.lifecycle.status !== "ready") {
|
|
66
|
+
return {
|
|
67
|
+
scenarioId: scenario.id,
|
|
68
|
+
category: scenario.category,
|
|
69
|
+
benchmarkProjectId: options.baseCase.benchmarkProjectId,
|
|
70
|
+
baseCaseId: options.baseCase.caseId,
|
|
71
|
+
answerPolicy: scenario.answerPolicy,
|
|
72
|
+
query: null,
|
|
73
|
+
expectedFiles: [],
|
|
74
|
+
expectedSymbols: [],
|
|
75
|
+
status: "failed",
|
|
76
|
+
failureReason: `${options.lifecycle.failure.code}: ${options.lifecycle.failure.message}`,
|
|
77
|
+
session: null,
|
|
78
|
+
treatments: [],
|
|
79
|
+
referenceComparisons: placeholderComparisons()
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const session = options.lifecycle.session;
|
|
83
|
+
const { resolved, derivedTask } = await resolveIncrementalChangeStalenessScenarioTask({ repoRoot: options.repoRoot, scenario });
|
|
84
|
+
const scenarioBase = {
|
|
85
|
+
scenarioId: scenario.id,
|
|
86
|
+
category: scenario.category,
|
|
87
|
+
benchmarkProjectId: scenario.benchmarkProjectId,
|
|
88
|
+
baseCaseId: scenario.baseCaseId,
|
|
89
|
+
answerPolicy: scenario.answerPolicy,
|
|
90
|
+
query: resolved.query,
|
|
91
|
+
expectedFiles: resolved.expectedFiles,
|
|
92
|
+
expectedSymbols: resolved.expectedSymbols,
|
|
93
|
+
session
|
|
94
|
+
};
|
|
95
|
+
// Affected-neighborhood evidence is derived from each treatment's ORIGINAL BASELINE change authority.
|
|
96
|
+
const affected = {};
|
|
97
|
+
for (const treatmentId of INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_IDS) {
|
|
98
|
+
affected[treatmentId] = assessIncrementalChangeStalenessAffectedNeighborhood(session.treatments[treatmentId], derivedTask);
|
|
99
|
+
}
|
|
100
|
+
const symmetry = checkAffectedNeighborhoodFourWaySymmetry(affected);
|
|
101
|
+
if (!symmetry.symmetric) {
|
|
102
|
+
return {
|
|
103
|
+
...scenarioBase,
|
|
104
|
+
status: "failed",
|
|
105
|
+
failureReason: `AFFECTED_NEIGHBORHOOD_ASYMMETRY: ${symmetry.reason}`,
|
|
106
|
+
treatments: [],
|
|
107
|
+
referenceComparisons: placeholderComparisons()
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const retrievalCommandsDir = (treatmentId) => resolveWithinRoot(options.runOwnedRoot, path.join("commands", scenario.id, treatmentId, "retrieval"));
|
|
111
|
+
const agentOutDir = (treatmentId) => resolveWithinRoot(options.runOwnedRoot, path.join("agents", scenario.id, treatmentId));
|
|
112
|
+
// Fixed deterministic order, sequential; each treatment retrieves only from its own active index.
|
|
113
|
+
const treatments = [];
|
|
114
|
+
for (const treatmentId of INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_IDS) {
|
|
115
|
+
const lifecycleTreatment = session.treatments[treatmentId];
|
|
116
|
+
const evaluation = await executeTreatmentEvaluation({
|
|
117
|
+
treatmentId,
|
|
118
|
+
baselineFreshness: lifecycleTreatment.changeAuthority.postMutationBaselineFreshness,
|
|
119
|
+
activeIndexPhase: lifecycleTreatment.activeRetrieval.role,
|
|
120
|
+
activeIndexDir: lifecycleTreatment.activeRetrieval.index.indexDir,
|
|
121
|
+
derivedTask,
|
|
122
|
+
affectedNeighborhood: affected[treatmentId],
|
|
123
|
+
requiredFiles: resolved.expectedFiles,
|
|
124
|
+
kitCommand: session.kitCommand,
|
|
125
|
+
retrievalCommandsDir: retrievalCommandsDir(treatmentId),
|
|
126
|
+
agentOutDir: agentOutDir(treatmentId),
|
|
127
|
+
projectProfiles: options.projectProfiles,
|
|
128
|
+
cwd: options.cwd,
|
|
129
|
+
env: options.env,
|
|
130
|
+
deps
|
|
131
|
+
});
|
|
132
|
+
treatments.push({
|
|
133
|
+
...evaluation,
|
|
134
|
+
treatmentIntent: INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_INTENTS[treatmentId],
|
|
135
|
+
refreshedFreshness: lifecycleTreatment.refreshedFreshness,
|
|
136
|
+
refreshExecution: refreshExecutionOf(session, treatmentId)
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
...scenarioBase,
|
|
141
|
+
status: "ready",
|
|
142
|
+
failureReason: null,
|
|
143
|
+
treatments,
|
|
144
|
+
referenceComparisons: buildReferenceComparisons(treatments)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** Exactly three comparisons against full-refresh: stale-index, changed-files-refresh, affected-neighborhood-refresh. */
|
|
148
|
+
export function buildReferenceComparisons(treatments) {
|
|
149
|
+
const byId = new Map(treatments.map((treatment) => [treatment.treatmentId, treatment]));
|
|
150
|
+
const reference = byId.get("full-refresh");
|
|
151
|
+
const stale = byId.get("stale-index");
|
|
152
|
+
if (!reference || !stale)
|
|
153
|
+
throw new Error("Reference comparisons require the stale-index and full-refresh treatment executions.");
|
|
154
|
+
// Historical v0.6.2 semantics, unchanged.
|
|
155
|
+
const staleCorrectness = compareCorrectness(correctnessComparable(stale.fakeAgent), correctnessComparable(reference.fakeAgent));
|
|
156
|
+
const staleRequired = compareRequiredFileEvidence(stale.requiredFileEvidence, reference.requiredFileEvidence);
|
|
157
|
+
const staleRisk = classifyStaleRisk(staleCorrectness, staleRequired);
|
|
158
|
+
const comparisons = [
|
|
159
|
+
{
|
|
160
|
+
kind: "stale-risk",
|
|
161
|
+
candidateTreatmentId: "stale-index",
|
|
162
|
+
referenceTreatmentId: "full-refresh",
|
|
163
|
+
comparison: {
|
|
164
|
+
correctnessRelation: staleCorrectness,
|
|
165
|
+
requiredFileEvidenceRelation: staleRequired,
|
|
166
|
+
staleRiskClassification: staleRisk.staleRiskClassification,
|
|
167
|
+
reasonCodes: staleRisk.reasonCodes
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
];
|
|
171
|
+
for (const candidateTreatmentId of PARTIAL_TREATMENT_IDS) {
|
|
172
|
+
const candidate = byId.get(candidateTreatmentId);
|
|
173
|
+
if (!candidate || candidate.refreshExecution.kind !== "incremental") {
|
|
174
|
+
throw new Error(`${candidateTreatmentId} has no incremental refresh execution to compare.`);
|
|
175
|
+
}
|
|
176
|
+
const correctnessRelation = compareCandidateCorrectness(correctnessComparable(candidate.fakeAgent), correctnessComparable(reference.fakeAgent));
|
|
177
|
+
const requiredFileEvidenceRelation = compareCandidateRequiredFileEvidence(candidate.requiredFileEvidence, reference.requiredFileEvidence);
|
|
178
|
+
const classified = classifyPartialRefreshAgainstFull({
|
|
179
|
+
realization: candidate.refreshExecution.realization,
|
|
180
|
+
correctnessRelation,
|
|
181
|
+
requiredFileEvidenceRelation
|
|
182
|
+
});
|
|
183
|
+
comparisons.push({
|
|
184
|
+
kind: "partial-refresh",
|
|
185
|
+
candidateTreatmentId,
|
|
186
|
+
referenceTreatmentId: "full-refresh",
|
|
187
|
+
refreshRealization: candidate.refreshExecution.realization,
|
|
188
|
+
correctnessRelation,
|
|
189
|
+
requiredFileEvidenceRelation,
|
|
190
|
+
classification: classified.classification,
|
|
191
|
+
reasonCodes: classified.reasonCodes
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return comparisons;
|
|
195
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { cp, lstat, mkdir, readdir, readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { resolveWithinRoot } from "../../../core/pathSafety.js";
|
|
6
|
+
/**
|
|
7
|
+
* Narrow v0.6.3 lifecycle-preparation helper: copies one already-produced my-dev-kit index
|
|
8
|
+
* directory byte-for-byte to a new sibling directory so an upstream incremental refresh can run
|
|
9
|
+
* in place against the copy while the original baseline stays immutable change authority.
|
|
10
|
+
*
|
|
11
|
+
* Both paths must resolve inside `runOwnedRoot` (textually and, for the destination parent, by
|
|
12
|
+
* real path), the destination must not exist, symlinks in the source are refused, and the copy
|
|
13
|
+
* is verified file-for-file by SHA-256. Index contents are never edited. Any failure throws.
|
|
14
|
+
*/
|
|
15
|
+
export async function cloneIncrementalChangeStalenessIndexDirectory(runOwnedRoot, sourceDir, destinationDir) {
|
|
16
|
+
const root = path.resolve(runOwnedRoot);
|
|
17
|
+
const source = resolveWithinRoot(root, path.resolve(sourceDir));
|
|
18
|
+
const destination = resolveWithinRoot(root, path.resolve(destinationDir));
|
|
19
|
+
if (!existsSync(source)) {
|
|
20
|
+
throw new Error(`Index clone source does not exist: ${source}.`);
|
|
21
|
+
}
|
|
22
|
+
if (existsSync(destination)) {
|
|
23
|
+
throw new Error(`Index clone destination already exists; refusing to overwrite: ${destination}.`);
|
|
24
|
+
}
|
|
25
|
+
const relativeToSource = path.relative(source, destination);
|
|
26
|
+
if (relativeToSource === "" || (!relativeToSource.startsWith("..") && !path.isAbsolute(relativeToSource))) {
|
|
27
|
+
throw new Error(`Index clone destination ${destination} must not be inside the source ${source}.`);
|
|
28
|
+
}
|
|
29
|
+
const sourceFiles = await listRegularFiles(source);
|
|
30
|
+
// The destination parent must be real-path contained by the real run-owned root (no symlink escape).
|
|
31
|
+
const parent = path.dirname(destination);
|
|
32
|
+
await mkdir(parent, { recursive: true });
|
|
33
|
+
const [realParent, realRoot] = await Promise.all([realpath(parent), realpath(root)]);
|
|
34
|
+
const parentRelative = path.relative(realRoot, realParent);
|
|
35
|
+
if (parentRelative === ".." || parentRelative.startsWith(`..${path.sep}`) || path.isAbsolute(parentRelative)) {
|
|
36
|
+
throw new Error(`Index clone destination parent escapes the run-owned root via a symbolic link: ${parent}.`);
|
|
37
|
+
}
|
|
38
|
+
await cp(source, destination, { recursive: true, errorOnExist: true, force: false, dereference: false });
|
|
39
|
+
const copiedFiles = await listRegularFiles(destination);
|
|
40
|
+
if (copiedFiles.length !== sourceFiles.length || copiedFiles.some((file, index) => file !== sourceFiles[index])) {
|
|
41
|
+
throw new Error(`Index clone is incomplete: ${destination} does not contain exactly the files of ${source}.`);
|
|
42
|
+
}
|
|
43
|
+
for (const file of sourceFiles) {
|
|
44
|
+
const [left, right] = await Promise.all([sha256(path.join(source, file)), sha256(path.join(destination, file))]);
|
|
45
|
+
if (left !== right) {
|
|
46
|
+
throw new Error(`Index clone content differs for ${file}.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function sha256(filePath) {
|
|
51
|
+
return createHash("sha256").update(await readFile(filePath)).digest("hex");
|
|
52
|
+
}
|
|
53
|
+
/** Sorted POSIX-relative regular files; refuses symlinks and other non-regular entries. */
|
|
54
|
+
async function listRegularFiles(root) {
|
|
55
|
+
const files = [];
|
|
56
|
+
async function walk(dir) {
|
|
57
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
58
|
+
const full = path.join(dir, entry.name);
|
|
59
|
+
const stat = await lstat(full);
|
|
60
|
+
if (stat.isSymbolicLink()) {
|
|
61
|
+
throw new Error(`Index directory contains a symbolic link, which is not copied: ${full}.`);
|
|
62
|
+
}
|
|
63
|
+
if (stat.isDirectory()) {
|
|
64
|
+
await walk(full);
|
|
65
|
+
}
|
|
66
|
+
else if (stat.isFile()) {
|
|
67
|
+
files.push(path.relative(root, full).replace(/\\/g, "/"));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
throw new Error(`Index directory contains a non-regular entry: ${full}.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
await walk(root);
|
|
75
|
+
return files.sort();
|
|
76
|
+
}
|
|
@@ -24,17 +24,69 @@ export function incrementalChangeStalenessIndexDir(runOwnedRoot, scenarioId, tre
|
|
|
24
24
|
export function incrementalChangeStalenessCommandsDir(runOwnedRoot, scenarioId, treatmentId, role) {
|
|
25
25
|
return resolveWithinRoot(path.resolve(runOwnedRoot), path.join("commands", scenarioId, treatmentId, role));
|
|
26
26
|
}
|
|
27
|
-
class LifecycleFailure extends Error {
|
|
27
|
+
export class LifecycleFailure extends Error {
|
|
28
28
|
failure;
|
|
29
29
|
constructor(failure) {
|
|
30
30
|
super(failure.message);
|
|
31
31
|
this.failure = failure;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
function fail(failure) {
|
|
34
|
+
export function fail(failure) {
|
|
35
35
|
if (failure)
|
|
36
36
|
throw new LifecycleFailure(failure);
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Shared by the v0.6.2 and v0.6.3 lifecycles: runs the index through the established warm-index
|
|
40
|
+
* session owner (index -> version probe -> snapshot -> graph), then proves target/index identity.
|
|
41
|
+
* `mode` defaults to an ordinary full build. The index directory must be absent unless
|
|
42
|
+
* `existingIndexDir` is set (an in-place refresh of an already-cloned baseline).
|
|
43
|
+
*/
|
|
44
|
+
export async function prepareTreatmentIndexEvidence(params) {
|
|
45
|
+
const { deps, target, role, indexDir, sourceRoots } = params;
|
|
46
|
+
const treatmentId = target.treatmentId;
|
|
47
|
+
if (params.existingIndexDir ? !existsSync(indexDir) : existsSync(indexDir)) {
|
|
48
|
+
fail({
|
|
49
|
+
code: "index-directory-collision",
|
|
50
|
+
message: params.existingIndexDir
|
|
51
|
+
? `Expected an existing cloned ${treatmentId} ${role} index directory: ${indexDir}.`
|
|
52
|
+
: `Refusing to overwrite existing ${treatmentId} ${role} index directory: ${indexDir}.`,
|
|
53
|
+
treatmentId
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const commandsDir = incrementalChangeStalenessCommandsDir(params.runOwnedRoot, params.scenarioId, treatmentId, role);
|
|
57
|
+
const prepared = await deps.prepareIndexSession({
|
|
58
|
+
target: { absoluteTargetRoot: target.targetRoot, sourceRoots: [...sourceRoots] },
|
|
59
|
+
kitCommand: params.kitCommand,
|
|
60
|
+
indexDir,
|
|
61
|
+
commandsDir,
|
|
62
|
+
requireKit: false,
|
|
63
|
+
...(params.mode ? { mode: params.mode } : {})
|
|
64
|
+
});
|
|
65
|
+
if (!prepared.ok) {
|
|
66
|
+
fail({
|
|
67
|
+
code: role === "baseline" ? "baseline-index-build-failed" : "refresh-index-build-failed",
|
|
68
|
+
message: `${treatmentId} ${role} my-dev-kit index build failed: ${prepared.warnings.join(" ") || `exit code ${prepared.build.command.exitCode}`}`,
|
|
69
|
+
treatmentId
|
|
70
|
+
});
|
|
71
|
+
throw new Error("unreachable");
|
|
72
|
+
}
|
|
73
|
+
params.onIndexPrepared?.();
|
|
74
|
+
const evidence = Object.freeze({
|
|
75
|
+
role,
|
|
76
|
+
treatmentId,
|
|
77
|
+
builtRelativeToMutation: role === "baseline" ? "pre-mutation" : "post-mutation",
|
|
78
|
+
indexDir: prepared.session.indexDir,
|
|
79
|
+
commandsDir,
|
|
80
|
+
targetRoot: prepared.session.targetRoot,
|
|
81
|
+
sourceRoots: prepared.session.sourceRoots,
|
|
82
|
+
buildDurationMs: prepared.session.buildDurationMs,
|
|
83
|
+
buildCommand: prepared.session.buildCommand,
|
|
84
|
+
snapshot: prepared.session.indexSnapshot,
|
|
85
|
+
graph: prepared.session.affectedNeighborhoodGraph
|
|
86
|
+
});
|
|
87
|
+
fail(evaluateIndexEvidence(evidence, { targetRoot: target.targetRoot, sourceRoots, indexDir }));
|
|
88
|
+
return { evidence, build: prepared.build };
|
|
89
|
+
}
|
|
38
90
|
/**
|
|
39
91
|
* Owns the matched Batch 3 lifecycle for ONE frozen scenario:
|
|
40
92
|
*
|
|
@@ -73,40 +125,20 @@ export async function prepareIncrementalChangeStalenessScenarioLifecycle(options
|
|
|
73
125
|
if (existsSync(indexDir)) {
|
|
74
126
|
fail({ code: "index-directory-collision", message: `Refusing to overwrite existing ${treatmentId} ${role} index directory: ${indexDir}.`, treatmentId });
|
|
75
127
|
}
|
|
76
|
-
const commandsDir = incrementalChangeStalenessCommandsDir(runOwnedRoot, scenario.id, treatmentId, role);
|
|
77
128
|
indexBuildCounts[treatmentId] += 1;
|
|
78
|
-
const
|
|
79
|
-
|
|
129
|
+
const { evidence } = await prepareTreatmentIndexEvidence({
|
|
130
|
+
deps,
|
|
131
|
+
runOwnedRoot,
|
|
132
|
+
scenarioId: scenario.id,
|
|
80
133
|
kitCommand,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
requireKit: false
|
|
84
|
-
});
|
|
85
|
-
if (!prepared.ok) {
|
|
86
|
-
fail({
|
|
87
|
-
code: role === "baseline" ? "baseline-index-build-failed" : "refresh-index-build-failed",
|
|
88
|
-
message: `${treatmentId} ${role} my-dev-kit index build failed: ${prepared.warnings.join(" ") || `exit code ${prepared.build.command.exitCode}`}`,
|
|
89
|
-
treatmentId
|
|
90
|
-
});
|
|
91
|
-
throw new Error("unreachable");
|
|
92
|
-
}
|
|
93
|
-
// prepareIndexSession resolves only after the index command, snapshot capture, and graph
|
|
94
|
-
// loading have all completed, in that order.
|
|
95
|
-
events.push(`${treatmentId}:${role}-index-built`, `${treatmentId}:${role}-snapshot-captured`, `${treatmentId}:${role}-graph-loaded`);
|
|
96
|
-
const evidence = Object.freeze({
|
|
134
|
+
sourceRoots,
|
|
135
|
+
target,
|
|
97
136
|
role,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
targetRoot: prepared.session.targetRoot,
|
|
103
|
-
sourceRoots: prepared.session.sourceRoots,
|
|
104
|
-
buildDurationMs: prepared.session.buildDurationMs,
|
|
105
|
-
buildCommand: prepared.session.buildCommand,
|
|
106
|
-
snapshot: prepared.session.indexSnapshot,
|
|
107
|
-
graph: prepared.session.affectedNeighborhoodGraph
|
|
137
|
+
indexDir,
|
|
138
|
+
// prepareIndexSession resolves only after the index command, snapshot capture, and graph
|
|
139
|
+
// loading have all completed, in that order.
|
|
140
|
+
onIndexPrepared: () => events.push(`${treatmentId}:${role}-index-built`, `${treatmentId}:${role}-snapshot-captured`, `${treatmentId}:${role}-graph-loaded`)
|
|
108
141
|
});
|
|
109
|
-
fail(evaluateIndexEvidence(evidence, { targetRoot: target.targetRoot, sourceRoots, indexDir }));
|
|
110
142
|
events.push(`${treatmentId}:${role}-identity-verified`);
|
|
111
143
|
return evidence;
|
|
112
144
|
}
|
|
@@ -130,7 +130,7 @@ export function evaluateBaselineStaleFreshness(assessment, controlledChangedPath
|
|
|
130
130
|
* baseline freshness change evidence must agree path-for-path, including
|
|
131
131
|
* baseline and current content identity.
|
|
132
132
|
*/
|
|
133
|
-
export function evaluateChangedPathSymmetry(stale, fullRefresh) {
|
|
133
|
+
export function evaluateChangedPathSymmetry(stale, fullRefresh, labels = { reference: "stale-index", other: "full-refresh" }) {
|
|
134
134
|
const key = (assessment) => assessment.changes
|
|
135
135
|
.map((change) => `${normalizeRelativePath(change.path)}\t${change.changeType}\t${change.baselineSha256}\t${change.currentSha256 ?? "null"}`)
|
|
136
136
|
.sort();
|
|
@@ -138,15 +138,17 @@ export function evaluateChangedPathSymmetry(stale, fullRefresh) {
|
|
|
138
138
|
const right = key(fullRefresh);
|
|
139
139
|
const same = left.length === right.length && left.every((value, index) => value === right[index]);
|
|
140
140
|
if (!same) {
|
|
141
|
-
return failure("treatment-changed-paths-conflict",
|
|
141
|
+
return failure("treatment-changed-paths-conflict", `${labels.reference} and ${labels.other} baseline freshness disagree about the controlled change: ${labels.reference}=[${left.join("; ")}] ${labels.other}=[${right.join("; ")}].`, null);
|
|
142
142
|
}
|
|
143
143
|
return null;
|
|
144
144
|
}
|
|
145
145
|
/** The refreshed index must be proven complete `fresh` against the unchanged mutated target. */
|
|
146
|
-
export function evaluateRefreshedFreshness(assessment) {
|
|
146
|
+
export function evaluateRefreshedFreshness(assessment, treatmentId = "full-refresh") {
|
|
147
147
|
const complete = assessment.baselineSnapshotStatus === "complete" && assessment.unresolvedFileCount === 0;
|
|
148
148
|
if (assessment.status !== "fresh" || !complete || assessment.changedFileCount !== 0 || assessment.missingFileCount !== 0) {
|
|
149
|
-
return failure("refreshed-freshness-not-fresh",
|
|
149
|
+
return failure("refreshed-freshness-not-fresh", `${treatmentId} refreshed index freshness is ${assessment.status} (snapshot ${assessment.baselineSnapshotStatus}, ${assessment.changedFileCount} changed, ${assessment.missingFileCount} missing, ${assessment.unresolvedFileCount} unresolved); complete fresh is required.`, treatmentId);
|
|
150
150
|
}
|
|
151
151
|
return null;
|
|
152
152
|
}
|
|
153
|
+
/** Shared failure constructor for the v0.6.3 lifecycle policy module. */
|
|
154
|
+
export { failure as lifecycleFailure };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { lifecycleFailure } from "./lifecyclePolicy.js";
|
|
2
|
+
/** Pure v0.6.3 four-treatment lifecycle policy; inspects already-produced evidence only. */
|
|
3
|
+
/**
|
|
4
|
+
* v0.6.3: every treatment baseline is bootstrapped with `--incremental --refresh-scope changed-files`
|
|
5
|
+
* against a brand-new index directory. Upstream must truthfully report the expected bootstrap full
|
|
6
|
+
* fallback (`cache-missing`); this is bootstrap evidence, never an experimental partial-refresh fallback.
|
|
7
|
+
*/
|
|
8
|
+
export function evaluateBaselineBootstrapEvidence(build, treatmentId) {
|
|
9
|
+
const evidence = build.incrementalRefresh;
|
|
10
|
+
const label = `${treatmentId} baseline bootstrap`;
|
|
11
|
+
if (build.mode.kind !== "incremental" || build.mode.refreshScope !== "changed-files") {
|
|
12
|
+
return lifecycleFailure("baseline-bootstrap-contract-mismatch", `${label} was not requested as incremental changed-files.`, treatmentId);
|
|
13
|
+
}
|
|
14
|
+
if (!evidence) {
|
|
15
|
+
return lifecycleFailure("baseline-bootstrap-contract-mismatch", `${label} returned no incrementalRefresh evidence.`, treatmentId);
|
|
16
|
+
}
|
|
17
|
+
if (evidence.requestedScope !== "changed-files" ||
|
|
18
|
+
evidence.appliedScope !== "full" ||
|
|
19
|
+
evidence.selectionStatus !== "fallback-full" ||
|
|
20
|
+
evidence.fallbackReason !== "cache-missing") {
|
|
21
|
+
return lifecycleFailure("baseline-bootstrap-contract-mismatch", `${label} expected requested=changed-files applied=full status=fallback-full reason=cache-missing but observed requested=${evidence.requestedScope} applied=${evidence.appliedScope} status=${evidence.selectionStatus} reason=${evidence.fallbackReason}.`, treatmentId);
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* v0.6.3: classifies a post-mutation partial-refresh build. A truthful `fallback-full` is a valid
|
|
27
|
+
* realization (FALLBACK_FULL), never collapsed to APPLIED_PARTIAL. `not-needed` after an already
|
|
28
|
+
* proven controlled mutation contradicts that mutation and fails the lifecycle. Missing or invalid
|
|
29
|
+
* evidence fails too.
|
|
30
|
+
*/
|
|
31
|
+
export function evaluatePartialRefreshRealization(build, requestedScope, treatmentId) {
|
|
32
|
+
const evidence = build.incrementalRefresh;
|
|
33
|
+
if (build.mode.kind !== "incremental" || build.mode.refreshScope !== requestedScope || !evidence || evidence.requestedScope !== requestedScope) {
|
|
34
|
+
return {
|
|
35
|
+
failure: lifecycleFailure("refresh-evidence-invalid", `${treatmentId} refresh did not carry valid ${requestedScope} incrementalRefresh evidence.`, treatmentId),
|
|
36
|
+
realization: null
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (evidence.selectionStatus === "not-needed" || evidence.appliedScope === "none") {
|
|
40
|
+
return {
|
|
41
|
+
failure: lifecycleFailure("refresh-not-needed-after-mutation", `${treatmentId} refresh reported not-needed (applied=${evidence.appliedScope}) although the controlled mutation was already proven.`, treatmentId),
|
|
42
|
+
realization: null
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (evidence.selectionStatus === "applied" && evidence.appliedScope === requestedScope) {
|
|
46
|
+
return { failure: null, realization: "APPLIED_PARTIAL" };
|
|
47
|
+
}
|
|
48
|
+
if (evidence.selectionStatus === "fallback-full" && evidence.appliedScope === "full") {
|
|
49
|
+
return { failure: null, realization: "FALLBACK_FULL" };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
failure: lifecycleFailure("refresh-evidence-invalid", `${treatmentId} refresh reported an unclassifiable outcome (status=${evidence.selectionStatus}, applied=${evidence.appliedScope}).`, treatmentId),
|
|
53
|
+
realization: null
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** v0.6.3 per-scenario counts: 7 index invocations and 7 freshness assessments. */
|
|
57
|
+
export const INCREMENTAL_CHANGE_STALENESS_V2_EXPECTED_INDEX_INVOCATIONS = {
|
|
58
|
+
"stale-index": 1,
|
|
59
|
+
"changed-files-refresh": 2,
|
|
60
|
+
"affected-neighborhood-refresh": 2,
|
|
61
|
+
"full-refresh": 2
|
|
62
|
+
};
|
|
63
|
+
export const INCREMENTAL_CHANGE_STALENESS_V2_EXPECTED_FRESHNESS_ASSESSMENTS = {
|
|
64
|
+
"stale-index": 1,
|
|
65
|
+
"changed-files-refresh": 2,
|
|
66
|
+
"affected-neighborhood-refresh": 2,
|
|
67
|
+
"full-refresh": 2
|
|
68
|
+
};
|
|
69
|
+
export function evaluateV2InvocationCounts(indexInvocationCounts, freshnessAssessmentCounts) {
|
|
70
|
+
for (const treatmentId of Object.keys(INCREMENTAL_CHANGE_STALENESS_V2_EXPECTED_INDEX_INVOCATIONS)) {
|
|
71
|
+
const expectedIndex = INCREMENTAL_CHANGE_STALENESS_V2_EXPECTED_INDEX_INVOCATIONS[treatmentId];
|
|
72
|
+
const expectedFreshness = INCREMENTAL_CHANGE_STALENESS_V2_EXPECTED_FRESHNESS_ASSESSMENTS[treatmentId];
|
|
73
|
+
if (indexInvocationCounts[treatmentId] !== expectedIndex || freshnessAssessmentCounts[treatmentId] !== expectedFreshness) {
|
|
74
|
+
return lifecycleFailure("invocation-count-mismatch", `${treatmentId} performed ${indexInvocationCounts[treatmentId]} index invocation(s) and ${freshnessAssessmentCounts[treatmentId]} freshness assessment(s); expected ${expectedIndex} and ${expectedFreshness}.`, treatmentId);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|