@dailephd/my-dev-kit-lab 0.6.1 → 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 +32 -0
- package/README.md +9 -7
- package/benchmarks/contracts/incremental-change-staleness-scenarios.json +360 -0
- package/dist/src/cli/help.js +16 -9
- package/dist/src/commands/runExperimentDescribeCommand.js +5 -2
- package/dist/src/commands/runExperimentRunCommand.js +4 -7
- package/dist/src/evaluation/affectedNeighborhoodMetrics.js +51 -0
- 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/defaultRegistry.js +2 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/comparison.js +101 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/comparisonV2.js +49 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/config.js +67 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/disposableTarget.js +125 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/execution.js +430 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/executionArtifact.js +160 -0
- 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/index.js +13 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/indexClone.js +76 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecycle.js +308 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecyclePolicy.js +154 -0
- 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/mutationExecution.js +144 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/plugin.js +475 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioCatalog.js +42 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioMutation.js +116 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioSelection.js +79 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioTypes.js +37 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioValidation.js +168 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/sourceState.js +145 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/treatmentSession.js +6 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/treatmentSessionV2.js +5 -0
- package/dist/src/experiments/plugins/index.js +1 -0
- package/dist/src/experiments/plugins/warmIndexReuse/metrics.js +17 -20
- 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 +112 -0
- package/dist/src/report/experiments/buildIncrementalChangeStalenessReportV2.js +80 -0
- package/dist/src/report/experiments/buildPluginExperimentReport.js +3 -0
- package/dist/src/report/experiments/incrementalChangeStalenessReportModel.js +4 -0
- 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 +6 -0
- package/dist/src/report/experiments/renderIncrementalChangeStalenessHtml.js +243 -0
- package/dist/src/report/experiments/renderIncrementalChangeStalenessHtmlV2.js +111 -0
- package/dist/src/report/experiments/renderIncrementalChangeStalenessTextV2.js +96 -0
- package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +3 -0
- package/dist/src/report/experiments/renderPluginExperimentReportText.js +144 -0
- package/dist/src/securityValidation/staticScans/codeql.js +8 -8
- package/docs/METRICS.md +67 -1
- package/examples/tutorial-browser/prepare.mjs +13 -13
- package/examples/tutorial-browser/server.mjs +13 -13
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveWithinRoot } from "../../../core/pathSafety.js";
|
|
4
|
+
import { assessIndexFreshness } from "../../../evaluation/indexFreshness.js";
|
|
5
|
+
import { prepareWarmIndexSession } from "../warmIndexReuse/warmIndexSession.js";
|
|
6
|
+
import { createDisposableTreatmentTarget } from "./disposableTarget.js";
|
|
7
|
+
import { evaluateBaselineStaleFreshness, evaluateChangedPathSymmetry, evaluateIndexEvidence, evaluateRefreshedFreshness, evaluateToolIdentity, normalizeRelativePath, normalizeRootForComparison } from "./lifecyclePolicy.js";
|
|
8
|
+
import { executeIncrementalChangeStalenessMutation } from "./mutationExecution.js";
|
|
9
|
+
import { captureIncrementalChangeStalenessSourceState, compareIncrementalChangeStalenessSourceStates } from "./sourceState.js";
|
|
10
|
+
import { INCREMENTAL_CHANGE_STALENESS_LIFECYCLE_SESSION_SCHEMA_VERSION } from "./treatmentSession.js";
|
|
11
|
+
export const defaultIncrementalChangeStalenessLifecycleDeps = {
|
|
12
|
+
prepareIndexSession: prepareWarmIndexSession,
|
|
13
|
+
assessIndexFreshness,
|
|
14
|
+
executeMutation: executeIncrementalChangeStalenessMutation,
|
|
15
|
+
captureSourceState: captureIncrementalChangeStalenessSourceState
|
|
16
|
+
};
|
|
17
|
+
/** Deterministic run-owned layout: targets/, indexes/, and commands/ are separate sibling trees. */
|
|
18
|
+
export function incrementalChangeStalenessTargetsRoot(runOwnedRoot) {
|
|
19
|
+
return path.join(path.resolve(runOwnedRoot), "targets");
|
|
20
|
+
}
|
|
21
|
+
export function incrementalChangeStalenessIndexDir(runOwnedRoot, scenarioId, treatmentId, role) {
|
|
22
|
+
return resolveWithinRoot(path.resolve(runOwnedRoot), path.join("indexes", scenarioId, treatmentId, role));
|
|
23
|
+
}
|
|
24
|
+
export function incrementalChangeStalenessCommandsDir(runOwnedRoot, scenarioId, treatmentId, role) {
|
|
25
|
+
return resolveWithinRoot(path.resolve(runOwnedRoot), path.join("commands", scenarioId, treatmentId, role));
|
|
26
|
+
}
|
|
27
|
+
export class LifecycleFailure extends Error {
|
|
28
|
+
failure;
|
|
29
|
+
constructor(failure) {
|
|
30
|
+
super(failure.message);
|
|
31
|
+
this.failure = failure;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function fail(failure) {
|
|
35
|
+
if (failure)
|
|
36
|
+
throw new LifecycleFailure(failure);
|
|
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
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Owns the matched Batch 3 lifecycle for ONE frozen scenario:
|
|
92
|
+
*
|
|
93
|
+
* 1. two independent disposable copies (stale-index, full-refresh);
|
|
94
|
+
* 2. pre-mutation controlled source-state equivalence;
|
|
95
|
+
* 3. stale-index baseline: index -> snapshot -> graph, then identity proof;
|
|
96
|
+
* 4. full-refresh baseline: index -> snapshot -> graph, then identity proof;
|
|
97
|
+
* 5. BARRIER: only after both baselines are fully ready, the same frozen
|
|
98
|
+
* mutation is applied independently to both copies;
|
|
99
|
+
* 6. post-mutation controlled source-state equivalence;
|
|
100
|
+
* 7. each baseline snapshot assessed against its mutated target (complete
|
|
101
|
+
* `stale` on exactly the scenario-controlled paths, symmetric across
|
|
102
|
+
* treatments);
|
|
103
|
+
* 8. stale-index keeps its baseline index as active retrieval authority
|
|
104
|
+
* (no rebuild);
|
|
105
|
+
* 9. full-refresh builds a NEW complete index into a separate directory,
|
|
106
|
+
* captures its snapshot/graph, and proves it complete `fresh`.
|
|
107
|
+
*
|
|
108
|
+
* Exactly three full index builds and three freshness assessments happen per
|
|
109
|
+
* scenario. Performs no retrieval, affected-neighborhood traversal, metric
|
|
110
|
+
* computation, correctness evaluation, or comparison classification, and
|
|
111
|
+
* never acts on a reindex recommendation. Sequential by design. Does not
|
|
112
|
+
* delete anything: the caller owns and cleans the run-owned root after the
|
|
113
|
+
* session has been consumed.
|
|
114
|
+
*/
|
|
115
|
+
export async function prepareIncrementalChangeStalenessScenarioLifecycle(options) {
|
|
116
|
+
const deps = { ...defaultIncrementalChangeStalenessLifecycleDeps, ...options.deps };
|
|
117
|
+
const { repoRoot, scenario, baseCase, kitCommand } = options;
|
|
118
|
+
const runOwnedRoot = path.resolve(options.runOwnedRoot);
|
|
119
|
+
const events = [];
|
|
120
|
+
const indexBuildCounts = { "stale-index": 0, "full-refresh": 0 };
|
|
121
|
+
const sourceRoots = baseCase.sourceRoots;
|
|
122
|
+
const controlledChangedPaths = scenario.mutation.files.map((file) => normalizeRelativePath(file.path)).sort();
|
|
123
|
+
async function prepareIndex(target, role, indexDir) {
|
|
124
|
+
const treatmentId = target.treatmentId;
|
|
125
|
+
if (existsSync(indexDir)) {
|
|
126
|
+
fail({ code: "index-directory-collision", message: `Refusing to overwrite existing ${treatmentId} ${role} index directory: ${indexDir}.`, treatmentId });
|
|
127
|
+
}
|
|
128
|
+
indexBuildCounts[treatmentId] += 1;
|
|
129
|
+
const { evidence } = await prepareTreatmentIndexEvidence({
|
|
130
|
+
deps,
|
|
131
|
+
runOwnedRoot,
|
|
132
|
+
scenarioId: scenario.id,
|
|
133
|
+
kitCommand,
|
|
134
|
+
sourceRoots,
|
|
135
|
+
target,
|
|
136
|
+
role,
|
|
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`)
|
|
141
|
+
});
|
|
142
|
+
events.push(`${treatmentId}:${role}-identity-verified`);
|
|
143
|
+
return evidence;
|
|
144
|
+
}
|
|
145
|
+
async function captureState(target) {
|
|
146
|
+
return deps.captureSourceState(target.targetRoot, sourceRoots, target.treatmentId, baseCase.benchmarkProjectId);
|
|
147
|
+
}
|
|
148
|
+
async function mutate(target) {
|
|
149
|
+
const receipt = await deps.executeMutation(scenario, target, sourceRoots, repoRoot);
|
|
150
|
+
if (receipt.status !== "applied") {
|
|
151
|
+
fail({ code: "mutation-failed", message: `${target.treatmentId} mutation was rejected: ${receipt.errors.join(" ")}`, treatmentId: target.treatmentId });
|
|
152
|
+
}
|
|
153
|
+
events.push(`${target.treatmentId}:mutation-applied`);
|
|
154
|
+
return receipt;
|
|
155
|
+
}
|
|
156
|
+
async function assess(index, target) {
|
|
157
|
+
return deps.assessIndexFreshness({ snapshot: index.snapshot, targetRoot: target.targetRoot, sourceRoots });
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const indexDirs = {
|
|
161
|
+
staleBaseline: incrementalChangeStalenessIndexDir(runOwnedRoot, scenario.id, "stale-index", "baseline"),
|
|
162
|
+
fullBaseline: incrementalChangeStalenessIndexDir(runOwnedRoot, scenario.id, "full-refresh", "baseline"),
|
|
163
|
+
fullRefreshed: incrementalChangeStalenessIndexDir(runOwnedRoot, scenario.id, "full-refresh", "refreshed")
|
|
164
|
+
};
|
|
165
|
+
if (normalizeRootForComparison(indexDirs.staleBaseline) === normalizeRootForComparison(indexDirs.fullBaseline) ||
|
|
166
|
+
normalizeRootForComparison(indexDirs.fullBaseline) === normalizeRootForComparison(indexDirs.fullRefreshed) ||
|
|
167
|
+
normalizeRootForComparison(indexDirs.staleBaseline) === normalizeRootForComparison(indexDirs.fullRefreshed)) {
|
|
168
|
+
fail({ code: "index-directory-collision", message: "Treatment/phase index directories are not distinct.", treatmentId: null });
|
|
169
|
+
}
|
|
170
|
+
// 1. Two independent disposable copies through the Batch 2 owner.
|
|
171
|
+
const targets = {};
|
|
172
|
+
for (const treatmentId of ["stale-index", "full-refresh"]) {
|
|
173
|
+
try {
|
|
174
|
+
targets[treatmentId] = await createDisposableTreatmentTarget({
|
|
175
|
+
repoRoot,
|
|
176
|
+
runRoot: incrementalChangeStalenessTargetsRoot(runOwnedRoot),
|
|
177
|
+
scenarioId: scenario.id,
|
|
178
|
+
treatmentId,
|
|
179
|
+
benchmarkProjectId: baseCase.benchmarkProjectId,
|
|
180
|
+
canonicalProjectRootRelative: baseCase.canonicalProjectRootRelative
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
fail({ code: "treatment-target-creation-failed", message: `${treatmentId}: ${error.message}`, treatmentId });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const stale = targets["stale-index"];
|
|
188
|
+
const full = targets["full-refresh"];
|
|
189
|
+
events.push("treatment-targets-created");
|
|
190
|
+
// 2. Pre-mutation equivalence gates all indexing and mutation.
|
|
191
|
+
const stalePre = await captureState(stale);
|
|
192
|
+
const fullPre = await captureState(full);
|
|
193
|
+
events.push("pre-mutation-source-state-captured");
|
|
194
|
+
const preMutationEquivalence = compareIncrementalChangeStalenessSourceStates(stalePre, fullPre);
|
|
195
|
+
if (preMutationEquivalence.result !== "equivalent") {
|
|
196
|
+
fail({
|
|
197
|
+
code: "pre-mutation-not-equivalent",
|
|
198
|
+
message: `Treatment copies are not pre-mutation equivalent (${preMutationEquivalence.result}): ${preMutationEquivalence.reasons.join("; ")}`,
|
|
199
|
+
treatmentId: null
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
events.push("pre-mutation-equivalence-proven");
|
|
203
|
+
// 3-4. Baselines: each index -> snapshot -> graph -> identity, sequentially.
|
|
204
|
+
const staleBaseline = await prepareIndex(stale, "baseline", indexDirs.staleBaseline);
|
|
205
|
+
const fullBaseline = await prepareIndex(full, "baseline", indexDirs.fullBaseline);
|
|
206
|
+
const baselineTools = evaluateToolIdentity([staleBaseline, fullBaseline]);
|
|
207
|
+
fail(baselineTools.failure);
|
|
208
|
+
events.push("baseline-tool-identity-verified");
|
|
209
|
+
// 5. Barrier: both baselines are index + snapshot + graph READY. Only now may mutation begin.
|
|
210
|
+
events.push("baseline-barrier-ready");
|
|
211
|
+
const staleReceipt = await mutate(stale);
|
|
212
|
+
const fullReceipt = await mutate(full);
|
|
213
|
+
// 6. Post-mutation equivalence.
|
|
214
|
+
const stalePost = await captureState(stale);
|
|
215
|
+
const fullPost = await captureState(full);
|
|
216
|
+
events.push("post-mutation-source-state-captured");
|
|
217
|
+
const postMutationEquivalence = compareIncrementalChangeStalenessSourceStates(stalePost, fullPost);
|
|
218
|
+
if (postMutationEquivalence.result !== "equivalent") {
|
|
219
|
+
fail({
|
|
220
|
+
code: "post-mutation-not-equivalent",
|
|
221
|
+
message: `Treatment copies are not post-mutation equivalent (${postMutationEquivalence.result}): ${postMutationEquivalence.reasons.join("; ")}`,
|
|
222
|
+
treatmentId: null
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
events.push("post-mutation-equivalence-proven");
|
|
226
|
+
// 7. Baseline (change-authority) freshness against each mutated target.
|
|
227
|
+
const staleBaselineFreshness = await assess(staleBaseline, stale);
|
|
228
|
+
fail(evaluateBaselineStaleFreshness(staleBaselineFreshness, controlledChangedPaths, "stale-index"));
|
|
229
|
+
events.push("stale-index:baseline-freshness-stale");
|
|
230
|
+
const fullBaselineFreshness = await assess(fullBaseline, full);
|
|
231
|
+
fail(evaluateBaselineStaleFreshness(fullBaselineFreshness, controlledChangedPaths, "full-refresh"));
|
|
232
|
+
events.push("full-refresh:baseline-freshness-stale");
|
|
233
|
+
fail(evaluateChangedPathSymmetry(staleBaselineFreshness, fullBaselineFreshness));
|
|
234
|
+
events.push("baseline-changed-paths-symmetric");
|
|
235
|
+
// 8. stale-index: no rebuild; baseline stays the active retrieval index.
|
|
236
|
+
events.push("stale-index:baseline-retained-as-active");
|
|
237
|
+
// 9. full-refresh: new complete post-mutation index in a separate directory.
|
|
238
|
+
const refreshed = await prepareIndex(full, "refreshed", indexDirs.fullRefreshed);
|
|
239
|
+
const refreshedFreshness = await assess(refreshed, full);
|
|
240
|
+
fail(evaluateRefreshedFreshness(refreshedFreshness));
|
|
241
|
+
events.push("full-refresh:refreshed-freshness-fresh");
|
|
242
|
+
const allTools = evaluateToolIdentity([staleBaseline, fullBaseline, refreshed]);
|
|
243
|
+
fail(allTools.failure);
|
|
244
|
+
events.push("tool-identity-consistent");
|
|
245
|
+
events.push("full-refresh:refreshed-active");
|
|
246
|
+
events.push("lifecycle-ready");
|
|
247
|
+
const staleSession = Object.freeze({
|
|
248
|
+
treatmentId: "stale-index",
|
|
249
|
+
target: stale,
|
|
250
|
+
preMutationSourceState: stalePre,
|
|
251
|
+
postMutationSourceState: stalePost,
|
|
252
|
+
mutationReceipt: staleReceipt,
|
|
253
|
+
changeAuthority: Object.freeze({ baselineIndex: staleBaseline, postMutationBaselineFreshness: staleBaselineFreshness }),
|
|
254
|
+
activeRetrieval: Object.freeze({ role: "baseline", index: staleBaseline }),
|
|
255
|
+
postMutationIndexBuilt: false,
|
|
256
|
+
refreshedIndex: null,
|
|
257
|
+
refreshedFreshness: null,
|
|
258
|
+
indexBuildCount: indexBuildCounts["stale-index"],
|
|
259
|
+
freshnessAssessmentCount: 1,
|
|
260
|
+
lifecycleStatus: "ready",
|
|
261
|
+
warnings: Object.freeze([])
|
|
262
|
+
});
|
|
263
|
+
const fullSession = Object.freeze({
|
|
264
|
+
treatmentId: "full-refresh",
|
|
265
|
+
target: full,
|
|
266
|
+
preMutationSourceState: fullPre,
|
|
267
|
+
postMutationSourceState: fullPost,
|
|
268
|
+
mutationReceipt: fullReceipt,
|
|
269
|
+
changeAuthority: Object.freeze({ baselineIndex: fullBaseline, postMutationBaselineFreshness: fullBaselineFreshness }),
|
|
270
|
+
activeRetrieval: Object.freeze({ role: "refreshed", index: refreshed }),
|
|
271
|
+
postMutationIndexBuilt: true,
|
|
272
|
+
refreshedIndex: refreshed,
|
|
273
|
+
refreshedFreshness,
|
|
274
|
+
indexBuildCount: indexBuildCounts["full-refresh"],
|
|
275
|
+
freshnessAssessmentCount: 2,
|
|
276
|
+
lifecycleStatus: "ready",
|
|
277
|
+
warnings: Object.freeze([])
|
|
278
|
+
});
|
|
279
|
+
const session = Object.freeze({
|
|
280
|
+
schemaVersion: INCREMENTAL_CHANGE_STALENESS_LIFECYCLE_SESSION_SCHEMA_VERSION,
|
|
281
|
+
scenarioId: scenario.id,
|
|
282
|
+
scenario,
|
|
283
|
+
baseCase,
|
|
284
|
+
runOwnedRoot,
|
|
285
|
+
kitCommand,
|
|
286
|
+
toolIdentity: allTools.tool,
|
|
287
|
+
controlledChangedPaths: Object.freeze(controlledChangedPaths),
|
|
288
|
+
preMutationEquivalence,
|
|
289
|
+
postMutationEquivalence,
|
|
290
|
+
treatments: Object.freeze({ "stale-index": staleSession, "full-refresh": fullSession }),
|
|
291
|
+
lifecycleEvents: Object.freeze([...events]),
|
|
292
|
+
status: "ready"
|
|
293
|
+
});
|
|
294
|
+
return { status: "ready", session };
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
const failure = error instanceof LifecycleFailure
|
|
298
|
+
? error.failure
|
|
299
|
+
: { code: "lifecycle-error", message: `Lifecycle failed unexpectedly: ${error instanceof Error ? error.message : String(error)}`, treatmentId: null };
|
|
300
|
+
return {
|
|
301
|
+
status: "failed",
|
|
302
|
+
scenarioId: scenario.id,
|
|
303
|
+
failure,
|
|
304
|
+
lifecycleEvents: Object.freeze([...events]),
|
|
305
|
+
indexBuildCounts: Object.freeze({ ...indexBuildCounts })
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Pure v0.6.2 Batch 3 lifecycle-readiness policy. Every function here only
|
|
4
|
+
* inspects already-produced evidence and returns an explicit failure or
|
|
5
|
+
* null; none touches the filesystem, runs my-dev-kit, mutates, or retrieves.
|
|
6
|
+
* These rules are scoped to this experiment's matched production lifecycle
|
|
7
|
+
* and deliberately do not change the released v0.6.0/v0.6.1 semantics of the
|
|
8
|
+
* evidence they read.
|
|
9
|
+
*/
|
|
10
|
+
export function normalizeRootForComparison(root) {
|
|
11
|
+
const resolved = path.resolve(root);
|
|
12
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
13
|
+
}
|
|
14
|
+
export function normalizeRelativePath(relativePath) {
|
|
15
|
+
return relativePath.replace(/\\/g, "/");
|
|
16
|
+
}
|
|
17
|
+
function normalizeSourceRoot(root) {
|
|
18
|
+
return root.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
19
|
+
}
|
|
20
|
+
/** Exact, order-preserving source-root equality after separator/trailing-slash normalization. */
|
|
21
|
+
export function sameSourceRoots(left, right) {
|
|
22
|
+
return left.length === right.length && left.every((root, index) => normalizeSourceRoot(root) === normalizeSourceRoot(right[index]));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Graph evidence is usable for this lifecycle only when an indexed-file claim
|
|
26
|
+
* exists (not `unavailable`) AND the code graph was actually loaded, since a
|
|
27
|
+
* null code graph makes later node mapping impossible.
|
|
28
|
+
*/
|
|
29
|
+
export function isUsableGraphEvidence(graph) {
|
|
30
|
+
return graph.status !== "unavailable" && graph.codeGraph !== null;
|
|
31
|
+
}
|
|
32
|
+
function failure(code, message, treatmentId) {
|
|
33
|
+
return { code, message, treatmentId };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Proves one prepared index belongs to the intended treatment target, uses
|
|
37
|
+
* the canonical source roots, carries a complete IndexSnapshotV1 (whose
|
|
38
|
+
* manifest projectRoot/sourceRoots were already checked against the target by
|
|
39
|
+
* captureIndexSnapshot), and carries usable graph evidence loaded from the
|
|
40
|
+
* same index directory. Never relies on directory naming alone.
|
|
41
|
+
*/
|
|
42
|
+
export function evaluateIndexEvidence(evidence, expected) {
|
|
43
|
+
const refreshed = evidence.role === "refreshed";
|
|
44
|
+
const label = `${evidence.treatmentId} ${evidence.role} index`;
|
|
45
|
+
const targetMismatchCode = refreshed ? "active-index-target-mismatch" : "index-target-mismatch";
|
|
46
|
+
if (normalizeRootForComparison(evidence.targetRoot) !== normalizeRootForComparison(expected.targetRoot)) {
|
|
47
|
+
return failure(targetMismatchCode, `${label} target root ${evidence.targetRoot} does not match treatment target ${expected.targetRoot}.`, evidence.treatmentId);
|
|
48
|
+
}
|
|
49
|
+
if (normalizeRootForComparison(evidence.indexDir) !== normalizeRootForComparison(expected.indexDir)) {
|
|
50
|
+
return failure(targetMismatchCode, `${label} directory ${evidence.indexDir} does not match the owned index directory ${expected.indexDir}.`, evidence.treatmentId);
|
|
51
|
+
}
|
|
52
|
+
if (!sameSourceRoots(evidence.sourceRoots, expected.sourceRoots)) {
|
|
53
|
+
return failure("source-root-mismatch", `${label} source roots [${evidence.sourceRoots.join(", ")}] do not match canonical base-case source roots [${expected.sourceRoots.join(", ")}].`, evidence.treatmentId);
|
|
54
|
+
}
|
|
55
|
+
const snapshot = evidence.snapshot;
|
|
56
|
+
if (snapshot.status === "unavailable" && snapshot.unavailable?.code === "manifest-target-mismatch") {
|
|
57
|
+
return failure(targetMismatchCode, `${label} manifest does not belong to the treatment target: ${snapshot.unavailable.message}`, evidence.treatmentId);
|
|
58
|
+
}
|
|
59
|
+
if (snapshot.status !== "complete") {
|
|
60
|
+
const reason = snapshot.unavailable ? `${snapshot.unavailable.code}: ${snapshot.unavailable.message}` : `${snapshot.unresolvedFileCount} unresolved indexed file(s)`;
|
|
61
|
+
return failure(refreshed ? "refreshed-snapshot-unavailable" : "baseline-snapshot-unavailable", `${label} IndexSnapshotV1 is ${snapshot.status} (${reason}).`, evidence.treatmentId);
|
|
62
|
+
}
|
|
63
|
+
if (normalizeRootForComparison(evidence.graph.indexRoot) !== normalizeRootForComparison(expected.indexDir)) {
|
|
64
|
+
return failure(targetMismatchCode, `${label} graph evidence was loaded from ${evidence.graph.indexRoot}, not ${expected.indexDir}.`, evidence.treatmentId);
|
|
65
|
+
}
|
|
66
|
+
if (!isUsableGraphEvidence(evidence.graph)) {
|
|
67
|
+
const reason = evidence.graph.unavailable ? `${evidence.graph.unavailable.code}: ${evidence.graph.unavailable.message}` : "code graph was not loaded";
|
|
68
|
+
return failure(refreshed ? "refreshed-graph-unavailable" : "baseline-graph-unavailable", `${label} graph evidence is not usable (${evidence.graph.status}; ${reason}).`, evidence.treatmentId);
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Every index of one matched scenario must come from the same configured
|
|
74
|
+
* my-dev-kit command and the same probed tool version. An unavailable
|
|
75
|
+
* version is never guessed: identity that cannot be established blocks
|
|
76
|
+
* readiness explicitly.
|
|
77
|
+
*/
|
|
78
|
+
export function evaluateToolIdentity(indexes) {
|
|
79
|
+
for (const index of indexes) {
|
|
80
|
+
if (index.snapshot.tool.availability !== "available" || index.snapshot.tool.version === null) {
|
|
81
|
+
return {
|
|
82
|
+
failure: failure("tool-identity-unavailable", `${index.treatmentId} ${index.role} index my-dev-kit version could not be established: ${index.snapshot.tool.reason ?? "unavailable"}.`, index.treatmentId),
|
|
83
|
+
tool: null
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const versions = [...new Set(indexes.map((index) => index.snapshot.tool.version))];
|
|
88
|
+
if (versions.length > 1) {
|
|
89
|
+
return {
|
|
90
|
+
failure: failure("tool-identity-mismatch", `Matched scenario indexes were built with different my-dev-kit versions: ${indexes.map((index) => `${index.treatmentId}/${index.role}=${index.snapshot.tool.version}`).join(", ")}.`, null),
|
|
91
|
+
tool: null
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const commands = [...new Set(indexes.map((index) => index.snapshot.indexCommand.commandString))];
|
|
95
|
+
if (commands.length > 1) {
|
|
96
|
+
return {
|
|
97
|
+
failure: failure("tool-identity-mismatch", `Matched scenario indexes were built with different my-dev-kit commands: ${commands.join(" | ")}.`, null),
|
|
98
|
+
tool: null
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return { failure: null, tool: indexes.length > 0 ? { ...indexes[0].snapshot.tool } : null };
|
|
102
|
+
}
|
|
103
|
+
function changedPaths(assessment) {
|
|
104
|
+
return assessment.changes.map((change) => normalizeRelativePath(change.path)).sort();
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A frozen production scenario must be observed as COMPLETE `stale` against
|
|
108
|
+
* the pre-mutation baseline snapshot, and the confirmed changed set must be
|
|
109
|
+
* exactly the scenario-declared controlled file(s) (all modifications, none
|
|
110
|
+
* missing). `fresh`, `unknown`, and `partially-stale` never count as a proven
|
|
111
|
+
* controlled stale state.
|
|
112
|
+
*/
|
|
113
|
+
export function evaluateBaselineStaleFreshness(assessment, controlledChangedPaths, treatmentId) {
|
|
114
|
+
if (assessment.status !== "stale") {
|
|
115
|
+
return failure("baseline-freshness-not-stale", `${treatmentId} baseline freshness after mutation is ${assessment.status}, not complete stale.`, treatmentId);
|
|
116
|
+
}
|
|
117
|
+
if (assessment.baselineSnapshotStatus !== "complete" || assessment.unresolvedFileCount !== 0 || assessment.changesTruncated) {
|
|
118
|
+
return failure("baseline-freshness-not-stale", `${treatmentId} baseline freshness comparison is incomplete (snapshot ${assessment.baselineSnapshotStatus}, ${assessment.unresolvedFileCount} unresolved, truncated=${assessment.changesTruncated}).`, treatmentId);
|
|
119
|
+
}
|
|
120
|
+
const expected = [...controlledChangedPaths].map(normalizeRelativePath).sort();
|
|
121
|
+
const observed = changedPaths(assessment);
|
|
122
|
+
const sameSet = observed.length === expected.length && observed.every((value, index) => value === expected[index]);
|
|
123
|
+
if (!sameSet || assessment.missingFileCount !== 0) {
|
|
124
|
+
return failure("baseline-changed-paths-mismatch", `${treatmentId} baseline freshness changed paths [${observed.join(", ")}] (missing=${assessment.missingFileCount}) do not match scenario-controlled paths [${expected.join(", ")}].`, treatmentId);
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Both treatments applied the same mutation to equivalent copies, so their
|
|
130
|
+
* baseline freshness change evidence must agree path-for-path, including
|
|
131
|
+
* baseline and current content identity.
|
|
132
|
+
*/
|
|
133
|
+
export function evaluateChangedPathSymmetry(stale, fullRefresh, labels = { reference: "stale-index", other: "full-refresh" }) {
|
|
134
|
+
const key = (assessment) => assessment.changes
|
|
135
|
+
.map((change) => `${normalizeRelativePath(change.path)}\t${change.changeType}\t${change.baselineSha256}\t${change.currentSha256 ?? "null"}`)
|
|
136
|
+
.sort();
|
|
137
|
+
const left = key(stale);
|
|
138
|
+
const right = key(fullRefresh);
|
|
139
|
+
const same = left.length === right.length && left.every((value, index) => value === right[index]);
|
|
140
|
+
if (!same) {
|
|
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
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
/** The refreshed index must be proven complete `fresh` against the unchanged mutated target. */
|
|
146
|
+
export function evaluateRefreshedFreshness(assessment, treatmentId = "full-refresh") {
|
|
147
|
+
const complete = assessment.baselineSnapshotStatus === "complete" && assessment.unresolvedFileCount === 0;
|
|
148
|
+
if (assessment.status !== "fresh" || !complete || assessment.changedFileCount !== 0 || assessment.missingFileCount !== 0) {
|
|
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
|
+
}
|
|
151
|
+
return null;
|
|
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
|
+
}
|