@apexops/testkit 0.10.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -0
- package/dist/determinism.d.ts +14 -0
- package/dist/determinism.d.ts.map +1 -0
- package/dist/determinism.js +38 -0
- package/dist/determinism.js.map +1 -0
- package/dist/faults.d.ts +8 -0
- package/dist/faults.d.ts.map +1 -0
- package/dist/faults.js +26 -0
- package/dist/faults.js.map +1 -0
- package/dist/fixtures.d.ts +18 -0
- package/dist/fixtures.d.ts.map +1 -0
- package/dist/fixtures.js +207 -0
- package/dist/fixtures.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/malicious.d.ts +9 -0
- package/dist/malicious.d.ts.map +1 -0
- package/dist/malicious.js +26 -0
- package/dist/malicious.js.map +1 -0
- package/dist/parity.d.ts +3 -0
- package/dist/parity.d.ts.map +1 -0
- package/dist/parity.js +10 -0
- package/dist/parity.js.map +1 -0
- package/dist/qualification.d.ts +53 -0
- package/dist/qualification.d.ts.map +1 -0
- package/dist/qualification.js +760 -0
- package/dist/qualification.js.map +1 -0
- package/dist/quality.d.ts +46 -0
- package/dist/quality.d.ts.map +1 -0
- package/dist/quality.js +118 -0
- package/dist/quality.js.map +1 -0
- package/dist/scenario.d.ts +26 -0
- package/dist/scenario.d.ts.map +1 -0
- package/dist/scenario.js +35 -0
- package/dist/scenario.js.map +1 -0
- package/dist/scorecard-samples.d.ts +5 -0
- package/dist/scorecard-samples.d.ts.map +1 -0
- package/dist/scorecard-samples.js +151 -0
- package/dist/scorecard-samples.js.map +1 -0
- package/dist/test/qualification.test.d.ts +2 -0
- package/dist/test/qualification.test.d.ts.map +1 -0
- package/dist/test/qualification.test.js +169 -0
- package/dist/test/qualification.test.js.map +1 -0
- package/dist/test/scorecard-samples.test.d.ts +2 -0
- package/dist/test/scorecard-samples.test.d.ts.map +1 -0
- package/dist/test/scorecard-samples.test.js +62 -0
- package/dist/test/scorecard-samples.test.js.map +1 -0
- package/dist/test/testkit.test.d.ts +2 -0
- package/dist/test/testkit.test.d.ts.map +1 -0
- package/dist/test/testkit.test.js +177 -0
- package/dist/test/testkit.test.js.map +1 -0
- package/dist/workspace.d.ts +3 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +11 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
import { CapabilityPackLoader } from "@apexops/capabilities";
|
|
2
|
+
import { ApexError, ApexService } from "@apexops/cli";
|
|
3
|
+
import { CONTRACT_VERSION } from "@apexops/contracts";
|
|
4
|
+
import { ContentCache, EventJournal, ObjectStore, benchmarkKernel, contentCacheKey, sha256Json } from "@apexops/kernel";
|
|
5
|
+
import { evaluateQualityScorecard, renderResourceInventory } from "@apexops/renderers";
|
|
6
|
+
import { cp, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
7
|
+
import { join, relative } from "node:path";
|
|
8
|
+
import { FakeClock, SequenceIds } from "./determinism.js";
|
|
9
|
+
const HASH = "a".repeat(64);
|
|
10
|
+
export async function runQualification(options) {
|
|
11
|
+
const clock = options.clock ?? new FakeClock();
|
|
12
|
+
const ids = options.ids ?? new SequenceIds("qualification");
|
|
13
|
+
const tracks = [];
|
|
14
|
+
const started = clock.now().getTime();
|
|
15
|
+
for (const track of ["bicep", "terraform"]) {
|
|
16
|
+
const root = join(options.workspaceRoot, track);
|
|
17
|
+
const source = join(options.workspaceRoot, `.customizations-${track}`);
|
|
18
|
+
await cp(options.customizationsSource, source, { recursive: true });
|
|
19
|
+
const report = await runTrack(root, source, track, clock, ids, options.injectFailure);
|
|
20
|
+
tracks.push(report);
|
|
21
|
+
}
|
|
22
|
+
const parityChecks = [];
|
|
23
|
+
await checked(parityChecks, clock, "logical-parity", async () => {
|
|
24
|
+
const [bicep, terraform] = tracks;
|
|
25
|
+
if (bicep?.hashes.logicalInventory !== terraform?.hashes.logicalInventory)
|
|
26
|
+
throw new Error("Track inventories differ logically");
|
|
27
|
+
});
|
|
28
|
+
const hashes = {
|
|
29
|
+
report: sha256Json(tracks.map(({ track, eventCount, hashes: trackHashes }) => ({ track, eventCount, hashes: trackHashes }))),
|
|
30
|
+
logicalInventory: tracks[0]?.hashes.logicalInventory ?? "",
|
|
31
|
+
};
|
|
32
|
+
const checks = [...tracks.flatMap(({ checks: trackChecks }) => trackChecks), ...parityChecks];
|
|
33
|
+
return {
|
|
34
|
+
schemaVersion: "1.0.0",
|
|
35
|
+
status: qualificationStatus(checks),
|
|
36
|
+
durationMs: clock.now().getTime() - started,
|
|
37
|
+
tracks,
|
|
38
|
+
checks: parityChecks,
|
|
39
|
+
eventCount: tracks.reduce((total, track) => total + track.eventCount, 0),
|
|
40
|
+
hashes,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function qualificationStatus(checks) {
|
|
44
|
+
return checks.every(({ status }) => status === "pass") ? "pass" : "fail";
|
|
45
|
+
}
|
|
46
|
+
export function qualificationJson(report) {
|
|
47
|
+
return `${JSON.stringify(report, null, 2)}\n`;
|
|
48
|
+
}
|
|
49
|
+
export function runQualificationBenchmark(eventCount, budgets) {
|
|
50
|
+
const metrics = benchmarkKernel(eventCount);
|
|
51
|
+
const checks = [
|
|
52
|
+
budgetCheck("append-budget", metrics.append.p95Ms, budgets.appendP95Ms),
|
|
53
|
+
budgetCheck("replay-budget", metrics.replay.p95Ms, budgets.replayP95Ms),
|
|
54
|
+
budgetCheck("status-budget", metrics.status.p95Ms, budgets.statusP95Ms),
|
|
55
|
+
];
|
|
56
|
+
return { schemaVersion: "1.0.0", status: qualificationStatus(checks), eventCount, budgets, metrics, checks };
|
|
57
|
+
}
|
|
58
|
+
async function runTrack(root, source, track, clock, ids, injectFailure) {
|
|
59
|
+
await mkdir(root, { recursive: true });
|
|
60
|
+
const serviceOptions = { clock: clock.now, idSource: ids.next };
|
|
61
|
+
const context = {
|
|
62
|
+
root,
|
|
63
|
+
source,
|
|
64
|
+
track,
|
|
65
|
+
clock,
|
|
66
|
+
ids,
|
|
67
|
+
options: serviceOptions,
|
|
68
|
+
runId: "",
|
|
69
|
+
service: new ApexService(root, serviceOptions),
|
|
70
|
+
checks: [],
|
|
71
|
+
taskContextBytes: [],
|
|
72
|
+
};
|
|
73
|
+
await checked(context.checks, clock, "initialize-customizations", async () => {
|
|
74
|
+
context.runId = (await context.service.init({
|
|
75
|
+
projectId: `qualification-${track}`,
|
|
76
|
+
iacTool: track,
|
|
77
|
+
customizationsSource: source,
|
|
78
|
+
})).runId;
|
|
79
|
+
}, injectFailure);
|
|
80
|
+
if (context.checks.at(-1)?.status === "fail") {
|
|
81
|
+
return failedTrackReport(track, context.checks);
|
|
82
|
+
}
|
|
83
|
+
await checked(context.checks, clock, "creative-workflow-gates-1-3", async () => {
|
|
84
|
+
await completeCreativeWorkflow(context);
|
|
85
|
+
}, injectFailure);
|
|
86
|
+
restart(context);
|
|
87
|
+
let inventory;
|
|
88
|
+
await checked(context.checks, clock, "preview-gate-4-deploy-inventory", async () => {
|
|
89
|
+
const preview = await context.service.preview({ operation: "apply", provider: "fake" });
|
|
90
|
+
await context.service.decideGateNumber(4, "approved", "qualification");
|
|
91
|
+
inventory = (await context.service.deploy(preview.previewHash)).inventory;
|
|
92
|
+
await complete(context, "diagnosis", [{ kind: "diagnosis", value: diagnosis(context) }]);
|
|
93
|
+
await complete(context, "quality", [{ kind: "quality-report", value: await quality(context) }]);
|
|
94
|
+
await context.service.render("preview");
|
|
95
|
+
await context.service.render("approval");
|
|
96
|
+
await context.service.render("inventory");
|
|
97
|
+
}, injectFailure);
|
|
98
|
+
restart(context);
|
|
99
|
+
await checked(context.checks, clock, "promotion-inheritance-fresh-gate-4", async () => {
|
|
100
|
+
const sameScope = await context.service.promote("stage", "local");
|
|
101
|
+
const sameScopeStates = sameScope.gates.map(({ state }) => state);
|
|
102
|
+
if (sameScopeStates.join(",") !== "inherited,inherited,inherited,closed")
|
|
103
|
+
throw new Error(`Unexpected same-scope promoted gates: ${sameScopeStates.join(",")}`);
|
|
104
|
+
await context.service.use(sameScope.projectId, context.runId);
|
|
105
|
+
const changedScope = await context.service.promote("prod", "local/prod");
|
|
106
|
+
const changedScopeStates = changedScope.gates.map(({ state }) => state);
|
|
107
|
+
if (changedScopeStates.join(",") !== "inherited,closed,closed,closed")
|
|
108
|
+
throw new Error(`Unexpected changed-scope promoted gates: ${changedScopeStates.join(",")}`);
|
|
109
|
+
await context.service.use(changedScope.projectId, context.runId);
|
|
110
|
+
}, injectFailure);
|
|
111
|
+
await checked(context.checks, clock, "destroy-preview-approval-destroy", async () => {
|
|
112
|
+
const destroy = await minimalService(join(root, ".scenarios", "destroy"), track, clock, ids);
|
|
113
|
+
const preview = await destroy.preview({ operation: "destroy", provider: "fake" });
|
|
114
|
+
await destroy.decideGateNumber(4, "approved", "qualification");
|
|
115
|
+
const destroyed = await destroy.deploy(preview.previewHash);
|
|
116
|
+
if (destroyed.inventory.resources.length !== 0)
|
|
117
|
+
throw new Error("Destroy inventory is not empty");
|
|
118
|
+
}, injectFailure);
|
|
119
|
+
await checked(context.checks, clock, "customization-conflict-preserves-edit", async () => verifyCustomizationConflict(context), injectFailure);
|
|
120
|
+
await checked(context.checks, clock, "fault-stale-task", async () => faultStaleTask(root, track, clock, ids), injectFailure);
|
|
121
|
+
await checked(context.checks, clock, "fault-corrupted-journal", async () => faultCorruptJournal(root, clock, ids), injectFailure);
|
|
122
|
+
await checked(context.checks, clock, "fault-stale-writer-epoch", async () => faultStaleEpoch(root, clock, ids), injectFailure);
|
|
123
|
+
await checked(context.checks, clock, "fault-changed-preview", async () => faultChangedPreview(root, track, clock, ids), injectFailure);
|
|
124
|
+
await checked(context.checks, clock, "fault-cache-invalidation", async () => faultCacheInvalidation(root), injectFailure);
|
|
125
|
+
await checked(context.checks, clock, "fault-missing-capability-pack", faultMissingCapabilityPack, injectFailure);
|
|
126
|
+
await checked(context.checks, clock, "fault-crash-after-side-effect-reconciliation", async () => {
|
|
127
|
+
await context.service.use(projectId(track), context.runId);
|
|
128
|
+
try {
|
|
129
|
+
await context.service.reconcile();
|
|
130
|
+
throw new Error("simulated crash after reconciliation side effect");
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (!(error instanceof Error) || !error.message.includes("simulated crash"))
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
restart(context);
|
|
137
|
+
const reconciled = await context.service.reconcile();
|
|
138
|
+
if (reconciled === undefined)
|
|
139
|
+
throw new Error("Reconciliation returned no inventory");
|
|
140
|
+
}, injectFailure);
|
|
141
|
+
await checked(context.checks, clock, "repository-byte-hygiene", async () => verifyRepositoryBytes(root), injectFailure);
|
|
142
|
+
const status = await context.service.status();
|
|
143
|
+
const journal = new EventJournal(join(root, ".apex", "projects", `qualification-${track}`, "runs", context.runId, "journal"));
|
|
144
|
+
const replay = await journal.replay();
|
|
145
|
+
const objectStore = new ObjectStore(root);
|
|
146
|
+
await checked(context.checks, clock, "event-replay-object-hashes", async () => {
|
|
147
|
+
if (replay.length === 0 || status.head !== replay.at(-1)?.hash)
|
|
148
|
+
throw new Error("Replay head does not match service status");
|
|
149
|
+
for (const event of replay) {
|
|
150
|
+
const payload = event.payload;
|
|
151
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
152
|
+
if ((key.endsWith("ObjectHash") || key === "inventoryHash" || key === "approvalHash") &&
|
|
153
|
+
typeof value === "string")
|
|
154
|
+
await objectStore.getJson(value);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}, injectFailure);
|
|
158
|
+
const logicalInventory = logicalInventoryHash(inventory);
|
|
159
|
+
const renderedInventory = inventory === undefined ? "" : sha256Json(renderResourceInventory(inventory));
|
|
160
|
+
return {
|
|
161
|
+
track,
|
|
162
|
+
status: qualificationStatus(context.checks),
|
|
163
|
+
checks: context.checks,
|
|
164
|
+
eventCount: replay.length,
|
|
165
|
+
gateRevisionLoops: replay.filter(({ type, payload }) => {
|
|
166
|
+
if (type === "gate.revision-requested")
|
|
167
|
+
return true;
|
|
168
|
+
if (type !== "gate.decided" || payload === null || typeof payload !== "object")
|
|
169
|
+
return false;
|
|
170
|
+
return payload.decision === "rejected";
|
|
171
|
+
}).length,
|
|
172
|
+
...(context.taskContextBytes.length === 0 ? {} : { taskContextBytes: Math.max(...context.taskContextBytes) }),
|
|
173
|
+
hashes: {
|
|
174
|
+
eventHead: replay.at(-1)?.hash ?? "",
|
|
175
|
+
logicalInventory,
|
|
176
|
+
renderedInventory,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function failedTrackReport(track, checks) {
|
|
181
|
+
return {
|
|
182
|
+
track,
|
|
183
|
+
status: "fail",
|
|
184
|
+
checks,
|
|
185
|
+
eventCount: 0,
|
|
186
|
+
hashes: { eventHead: "", logicalInventory: "", renderedInventory: "" },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function restart(context) {
|
|
190
|
+
context.clock.advance(10);
|
|
191
|
+
context.service = new ApexService(context.root, context.options);
|
|
192
|
+
}
|
|
193
|
+
async function completeRequirementsIntake(service, workload) {
|
|
194
|
+
let next = await service.nextTask();
|
|
195
|
+
while (next.status === "needs_input") {
|
|
196
|
+
await recordQualificationInput(service, next.request, workload);
|
|
197
|
+
next = await service.nextTask();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function recordQualificationInput(service, request, workload = "qualification-workload") {
|
|
201
|
+
await service.recordInput({
|
|
202
|
+
schemaVersion: "1.0.0",
|
|
203
|
+
requestId: request.requestId,
|
|
204
|
+
expectedHead: request.expectedHead,
|
|
205
|
+
ownerEpoch: request.ownerEpoch,
|
|
206
|
+
answers: request.questions.map(({ id, multiSelect, options, valueType }) => ({
|
|
207
|
+
questionId: id,
|
|
208
|
+
value: id === "workload"
|
|
209
|
+
? workload
|
|
210
|
+
: valueType === "budget"
|
|
211
|
+
? { kind: "budget", amount: 250, currency: "USD", cadence: "monthly" }
|
|
212
|
+
: valueType === "recovery"
|
|
213
|
+
? { kind: "recovery", rtoMinutes: 60, rpoMinutes: 15 }
|
|
214
|
+
: valueType === "data-classification"
|
|
215
|
+
? { kind: "data-classification", classification: "internal" }
|
|
216
|
+
: valueType === "compliance"
|
|
217
|
+
? { kind: "compliance", scopes: ["gdpr"] }
|
|
218
|
+
: options === undefined
|
|
219
|
+
? `qualification-${id}`
|
|
220
|
+
: multiSelect === true
|
|
221
|
+
? [options[0]]
|
|
222
|
+
: options[0],
|
|
223
|
+
})),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
async function completeCreativeWorkflow(context) {
|
|
227
|
+
const { service, runId, track } = context;
|
|
228
|
+
await completeRequirementsIntake(service, `${track} qualification workload`);
|
|
229
|
+
const requirementValue = requirements(track);
|
|
230
|
+
const requirementHashes = await complete(context, "requirements", [
|
|
231
|
+
{ kind: "requirements", value: requirementValue },
|
|
232
|
+
]);
|
|
233
|
+
await complete(context, "requirements-review", [
|
|
234
|
+
{ kind: "review-findings", value: review(context, "requirements", requirementHashes.requirements) },
|
|
235
|
+
]);
|
|
236
|
+
await service.decideGateNumber(1, "approved", "qualification");
|
|
237
|
+
const availabilityRefs = {};
|
|
238
|
+
for (const source of ["pricing", "quota", "regionalAvailability"]) {
|
|
239
|
+
const accepted = (await service.acceptEvidence({
|
|
240
|
+
kind: `${source}-evidence`,
|
|
241
|
+
contentType: "application/json",
|
|
242
|
+
value: { source, mode: "simulated", status: "current" },
|
|
243
|
+
required: true,
|
|
244
|
+
}));
|
|
245
|
+
if (accepted.hash === undefined)
|
|
246
|
+
throw new Error(`${source} evidence was not accepted`);
|
|
247
|
+
availabilityRefs[source] = accepted.hash;
|
|
248
|
+
}
|
|
249
|
+
await service.acceptEvidence({
|
|
250
|
+
kind: "architecture-availability-v1",
|
|
251
|
+
contentType: "application/json",
|
|
252
|
+
value: availabilityEvidence(context, availabilityRefs),
|
|
253
|
+
required: true,
|
|
254
|
+
});
|
|
255
|
+
restart(context);
|
|
256
|
+
const architectureValue = architecture(context);
|
|
257
|
+
const costValue = costEstimate(context);
|
|
258
|
+
const architectureHashes = await complete(context, "architecture", [
|
|
259
|
+
{ kind: "architecture", value: architectureValue },
|
|
260
|
+
{ kind: "cost-estimate", value: costValue },
|
|
261
|
+
{
|
|
262
|
+
kind: "workload-decision-manifest",
|
|
263
|
+
value: workloadDecisionManifest(context, requirementHashes.requirements, sha256Json(architectureValue), sha256Json(costValue)),
|
|
264
|
+
},
|
|
265
|
+
]);
|
|
266
|
+
await complete(context, "architecture-review", [
|
|
267
|
+
{ kind: "review-findings", value: review(context, "architecture", architectureHashes.architecture) },
|
|
268
|
+
]);
|
|
269
|
+
const governanceHashes = await complete(context, "governance-discovery", [
|
|
270
|
+
{ kind: "governance-constraints", value: governance(context) },
|
|
271
|
+
]);
|
|
272
|
+
const policyHashes = await complete(context, "governance-reconciliation", [
|
|
273
|
+
{ kind: "policy-property-map", value: policyMap(context, governanceHashes["governance-constraints"]) },
|
|
274
|
+
]);
|
|
275
|
+
await complete(context, "governance-review", [
|
|
276
|
+
{ kind: "review-findings", value: review(context, "policy-property-map", policyHashes["policy-property-map"]) },
|
|
277
|
+
]);
|
|
278
|
+
await context.service.decideGateNumber(2, "approved", "qualification");
|
|
279
|
+
restart(context);
|
|
280
|
+
const plan = planBundle(context, {
|
|
281
|
+
requirements: requirementHashes.requirements,
|
|
282
|
+
architecture: architectureHashes.architecture,
|
|
283
|
+
"governance-constraints": governanceHashes["governance-constraints"],
|
|
284
|
+
"policy-property-map": policyHashes["policy-property-map"],
|
|
285
|
+
});
|
|
286
|
+
const planHashes = await complete(context, "plan", plan);
|
|
287
|
+
await complete(context, "plan-review", [
|
|
288
|
+
{ kind: "review-findings", value: review(context, "plan", planHashes["implementation-intent"]) },
|
|
289
|
+
]);
|
|
290
|
+
await context.service.decideGateNumber(3, "approved", "qualification");
|
|
291
|
+
restart(context);
|
|
292
|
+
await complete(context, `codegen-${track}`, codegenBundle(context, plan));
|
|
293
|
+
await complete(context, `validation-${track}`, [{ kind: "validation-evidence", value: validation(context) }]);
|
|
294
|
+
if (runId.length === 0)
|
|
295
|
+
throw new Error("Run was not initialized");
|
|
296
|
+
}
|
|
297
|
+
async function complete(context, expected, outputs) {
|
|
298
|
+
let issued = await context.service.nextTask();
|
|
299
|
+
while (issued.status === "needs_input") {
|
|
300
|
+
await recordQualificationInput(context.service, issued.request);
|
|
301
|
+
issued = await context.service.nextTask();
|
|
302
|
+
}
|
|
303
|
+
if (issued.task.taskType !== expected)
|
|
304
|
+
throw new Error(`Expected ${expected}, received ${issued.task.taskType}`);
|
|
305
|
+
const projection = await context.service.taskContext(issued.task.taskId);
|
|
306
|
+
context.taskContextBytes.push(Buffer.byteLength(JSON.stringify(projection), "utf8"));
|
|
307
|
+
return (await context.service.completeTaskOutputs(issued.task.taskId, outputs)).outputHashes;
|
|
308
|
+
}
|
|
309
|
+
function projectId(track) {
|
|
310
|
+
return `qualification-${track}`;
|
|
311
|
+
}
|
|
312
|
+
function requirements(track) {
|
|
313
|
+
return requirementsFor(projectId(track));
|
|
314
|
+
}
|
|
315
|
+
function requirementsFor(id) {
|
|
316
|
+
return {
|
|
317
|
+
schemaVersion: CONTRACT_VERSION,
|
|
318
|
+
projectId: id,
|
|
319
|
+
workload: "qualification service",
|
|
320
|
+
environment: "dev",
|
|
321
|
+
requirements: [
|
|
322
|
+
{
|
|
323
|
+
id: "REQ-1",
|
|
324
|
+
statement: "Deploy deterministically",
|
|
325
|
+
priority: "must",
|
|
326
|
+
status: "confirmed",
|
|
327
|
+
source: "qualification",
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
assumptions: [],
|
|
331
|
+
unknowns: [],
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function intentFor(id, runId, sourceHashes) {
|
|
335
|
+
return {
|
|
336
|
+
schemaVersion: CONTRACT_VERSION,
|
|
337
|
+
projectId: id,
|
|
338
|
+
runId,
|
|
339
|
+
sourceHashes,
|
|
340
|
+
resources: [{ id: "api", type: "fake/service", purpose: "Serve requests", dependsOn: [], controls: [] }],
|
|
341
|
+
outputs: ["endpoint"],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function workloadDecisionManifest(context, requirementsHash, architectureHash, costEstimateHash) {
|
|
345
|
+
return {
|
|
346
|
+
schemaVersion: CONTRACT_VERSION,
|
|
347
|
+
projectId: projectId(context.track),
|
|
348
|
+
runId: context.runId,
|
|
349
|
+
environment: "dev",
|
|
350
|
+
sourceRequirementsHash: requirementsHash,
|
|
351
|
+
architectureHash,
|
|
352
|
+
costEstimateHash,
|
|
353
|
+
environments: ["dev", "prod"],
|
|
354
|
+
requirementTraceability: [{ requirementId: "REQ-1", skuDecisionIds: ["api-sku"], sloDecisionIds: ["api-slo"] }],
|
|
355
|
+
skuDecisions: [
|
|
356
|
+
{
|
|
357
|
+
id: "api-sku",
|
|
358
|
+
logicalId: "api",
|
|
359
|
+
service: "fake/service",
|
|
360
|
+
sku: "test",
|
|
361
|
+
quantity: 1,
|
|
362
|
+
rationale: "qualification",
|
|
363
|
+
requirementIds: ["REQ-1"],
|
|
364
|
+
environmentOverrides: [],
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
sloDecisions: [
|
|
368
|
+
{
|
|
369
|
+
id: "api-slo",
|
|
370
|
+
logicalId: "api",
|
|
371
|
+
availabilityPercent: 99.9,
|
|
372
|
+
rtoMinutes: 60,
|
|
373
|
+
rpoMinutes: 15,
|
|
374
|
+
supportWindow: "business-hours",
|
|
375
|
+
complianceScopes: ["gdpr"],
|
|
376
|
+
requirementIds: ["REQ-1"],
|
|
377
|
+
environmentOverrides: [],
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
revisions: [
|
|
381
|
+
{ number: 1, createdAt: "2026-01-01T00:00:00.000Z", sourceHash: requirementsHash, reason: "qualification" },
|
|
382
|
+
],
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function architecture(context) {
|
|
386
|
+
return {
|
|
387
|
+
schemaVersion: CONTRACT_VERSION,
|
|
388
|
+
projectId: projectId(context.track),
|
|
389
|
+
runId: context.runId,
|
|
390
|
+
title: "Qualification",
|
|
391
|
+
summary: "Equivalent fake architecture",
|
|
392
|
+
sourceHashes: { requirements: HASH },
|
|
393
|
+
components: [{ id: "api", service: "fake/service", purpose: "Serve", requirementIds: ["REQ-1"], dependsOn: [] }],
|
|
394
|
+
decisions: [],
|
|
395
|
+
risks: [],
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function costEstimate(context) {
|
|
399
|
+
return {
|
|
400
|
+
schemaVersion: CONTRACT_VERSION,
|
|
401
|
+
projectId: projectId(context.track),
|
|
402
|
+
runId: context.runId,
|
|
403
|
+
currency: "USD",
|
|
404
|
+
pricingDate: "2026-01-01",
|
|
405
|
+
lineItems: [
|
|
406
|
+
{
|
|
407
|
+
id: "api",
|
|
408
|
+
service: "fake/service",
|
|
409
|
+
sku: "test",
|
|
410
|
+
quantity: 1,
|
|
411
|
+
unitPrice: 1,
|
|
412
|
+
unitsPerMonth: 1,
|
|
413
|
+
monthlyCost: 1,
|
|
414
|
+
source: { provider: "qualification", uri: "https://example.test", retrievedAt: "2026-01-01T00:00:00.000Z" },
|
|
415
|
+
uncertainty: { lowerMonthlyCost: 1, upperMonthlyCost: 1, confidence: "high", basis: "fixed" },
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
totalMonthlyCost: 1,
|
|
419
|
+
assumptions: [],
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function review(context, subjectKind, subjectHash) {
|
|
423
|
+
return {
|
|
424
|
+
schemaVersion: CONTRACT_VERSION,
|
|
425
|
+
projectId: projectId(context.track),
|
|
426
|
+
runId: context.runId,
|
|
427
|
+
subjectKind,
|
|
428
|
+
subjectHash,
|
|
429
|
+
reviewedAt: context.clock.now().toISOString(),
|
|
430
|
+
findings: [],
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function governance(context) {
|
|
434
|
+
return {
|
|
435
|
+
schemaVersion: CONTRACT_VERSION,
|
|
436
|
+
projectId: projectId(context.track),
|
|
437
|
+
runId: context.runId,
|
|
438
|
+
targetScope: "local",
|
|
439
|
+
discoveredAt: context.clock.now().toISOString(),
|
|
440
|
+
expiresAt: "2027-01-01T00:00:00.000Z",
|
|
441
|
+
summary: { assignmentCount: 0, denyCount: 0, modifyCount: 0, auditCount: 0, exemptionCount: 0 },
|
|
442
|
+
constraintsRef: { mediaType: "application/json", uri: "memory://constraints", digest: HASH, bytes: 0 },
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
function availabilityEvidence(context, evidenceRefs) {
|
|
446
|
+
return {
|
|
447
|
+
schemaVersion: CONTRACT_VERSION,
|
|
448
|
+
projectId: projectId(context.track),
|
|
449
|
+
runId: context.runId,
|
|
450
|
+
targetScope: "local",
|
|
451
|
+
mode: "simulated",
|
|
452
|
+
collectedAt: context.clock.now().toISOString(),
|
|
453
|
+
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
454
|
+
checks: {
|
|
455
|
+
pricing: { status: "current", evidenceRef: evidenceRefs.pricing },
|
|
456
|
+
quota: { status: "current", evidenceRef: evidenceRefs.quota },
|
|
457
|
+
regionalAvailability: {
|
|
458
|
+
status: "current",
|
|
459
|
+
evidenceRef: evidenceRefs.regionalAvailability,
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function policyMap(context, governanceHash) {
|
|
465
|
+
return {
|
|
466
|
+
schemaVersion: CONTRACT_VERSION,
|
|
467
|
+
projectId: projectId(context.track),
|
|
468
|
+
runId: context.runId,
|
|
469
|
+
governanceHash,
|
|
470
|
+
mappings: [],
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function planBundle(context, sourceHashes) {
|
|
474
|
+
const intent = intentFor(projectId(context.track), context.runId, sourceHashes);
|
|
475
|
+
return [
|
|
476
|
+
{ kind: "implementation-intent", value: intent },
|
|
477
|
+
{
|
|
478
|
+
kind: "iac-binding",
|
|
479
|
+
value: {
|
|
480
|
+
schemaVersion: CONTRACT_VERSION,
|
|
481
|
+
projectId: projectId(context.track),
|
|
482
|
+
runId: context.runId,
|
|
483
|
+
track: context.track,
|
|
484
|
+
intentHash: sha256Json(intent),
|
|
485
|
+
resourceBindings: { api: { implementation: "fake/service", version: "1", parameters: {} } },
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
kind: "environment-inputs",
|
|
490
|
+
value: {
|
|
491
|
+
schemaVersion: CONTRACT_VERSION,
|
|
492
|
+
projectId: projectId(context.track),
|
|
493
|
+
runId: context.runId,
|
|
494
|
+
environment: "dev",
|
|
495
|
+
inputs: {},
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
];
|
|
499
|
+
}
|
|
500
|
+
function codegenBundle(context, plan) {
|
|
501
|
+
const manifest = {
|
|
502
|
+
schemaVersion: CONTRACT_VERSION,
|
|
503
|
+
projectId: projectId(context.track),
|
|
504
|
+
runId: context.runId,
|
|
505
|
+
track: context.track,
|
|
506
|
+
resources: [
|
|
507
|
+
{
|
|
508
|
+
logicalId: "api",
|
|
509
|
+
type: "fake/service",
|
|
510
|
+
implementationAddress: "api",
|
|
511
|
+
implementationKind: "resource",
|
|
512
|
+
ownership: "managed",
|
|
513
|
+
dependsOn: [],
|
|
514
|
+
generatedDependencies: [],
|
|
515
|
+
sourcePath: "main",
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
};
|
|
519
|
+
return [
|
|
520
|
+
{ kind: "logical-resource-manifest", value: manifest },
|
|
521
|
+
{
|
|
522
|
+
kind: "iac-handoff",
|
|
523
|
+
value: {
|
|
524
|
+
schemaVersion: CONTRACT_VERSION,
|
|
525
|
+
projectId: projectId(context.track),
|
|
526
|
+
runId: context.runId,
|
|
527
|
+
track: context.track,
|
|
528
|
+
rootPath: ".apex/work/code",
|
|
529
|
+
treeHash: HASH,
|
|
530
|
+
intentHash: sha256Json(plan[0].value),
|
|
531
|
+
bindingHash: sha256Json(plan[1].value),
|
|
532
|
+
environmentInputsHash: sha256Json(plan[2].value),
|
|
533
|
+
logicalResourceManifestHash: sha256Json(manifest),
|
|
534
|
+
requiredToolVersions: { [context.track]: "test" },
|
|
535
|
+
generatedAt: context.clock.now().toISOString(),
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
];
|
|
539
|
+
}
|
|
540
|
+
function validation(context) {
|
|
541
|
+
const validatorIds = context.track === "bicep"
|
|
542
|
+
? ["bicep:format", "bicep:build", "bicep:lint"]
|
|
543
|
+
: ["terraform:format", "terraform:init-backend-false", "terraform:validate"];
|
|
544
|
+
validatorIds.push("business:security-baseline", "business:policy-property-map", "business:logical-resource-parity");
|
|
545
|
+
return {
|
|
546
|
+
schemaVersion: CONTRACT_VERSION,
|
|
547
|
+
projectId: projectId(context.track),
|
|
548
|
+
runId: context.runId,
|
|
549
|
+
createdAt: context.clock.now().toISOString(),
|
|
550
|
+
entries: validatorIds.map((kind) => ({ kind, hash: HASH, bytes: 1, required: true, retention: "immutable" })),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function diagnosis(context) {
|
|
554
|
+
return {
|
|
555
|
+
schemaVersion: CONTRACT_VERSION,
|
|
556
|
+
projectId: projectId(context.track),
|
|
557
|
+
runId: context.runId,
|
|
558
|
+
diagnosedAt: context.clock.now().toISOString(),
|
|
559
|
+
status: "healthy",
|
|
560
|
+
observations: ["deployed"],
|
|
561
|
+
causes: [],
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
async function quality(context) {
|
|
565
|
+
const scorecard = JSON.parse(await readFile(join(context.root, ".apex", "runtime", "quality-scorecard.v1.json"), "utf8"));
|
|
566
|
+
const measurements = [...scorecard.rules]
|
|
567
|
+
.map(({ metric, scenario }) => ({ metric, scenario, samples: 0, evidenceRefs: [] }))
|
|
568
|
+
.sort((left, right) => `${left.metric}\u0000${left.scenario}`.localeCompare(`${right.metric}\u0000${right.scenario}`));
|
|
569
|
+
const measurementSet = { schemaVersion: CONTRACT_VERSION, measurements };
|
|
570
|
+
await mkdir(join(context.root, ".apex", "quality"), { recursive: true });
|
|
571
|
+
await writeFile(join(context.root, ".apex", "quality", "measurements.json"), `${JSON.stringify(measurementSet)}\n`, "utf8");
|
|
572
|
+
const measurementsHash = sha256Json(measurementSet);
|
|
573
|
+
const evaluations = evaluateQualityScorecard(scorecard, measurements);
|
|
574
|
+
return {
|
|
575
|
+
schemaVersion: CONTRACT_VERSION,
|
|
576
|
+
projectId: projectId(context.track),
|
|
577
|
+
runId: context.runId,
|
|
578
|
+
evaluatedAt: context.clock.now().toISOString(),
|
|
579
|
+
scorecardHash: sha256Json(scorecard),
|
|
580
|
+
measurementsHash,
|
|
581
|
+
status: evaluations.some(({ decision }) => decision === "fail") ||
|
|
582
|
+
!evaluations.some(({ decision }) => decision === "pass")
|
|
583
|
+
? "fail"
|
|
584
|
+
: "pass",
|
|
585
|
+
checks: evaluations.map(({ metric, scenario, decision, value, samples, reason }) => ({
|
|
586
|
+
id: metric,
|
|
587
|
+
scenario,
|
|
588
|
+
status: decision,
|
|
589
|
+
...(value === undefined ? {} : { value }),
|
|
590
|
+
samples,
|
|
591
|
+
evidenceRefs: decision === "omitted" ? [] : [measurementsHash],
|
|
592
|
+
detail: reason,
|
|
593
|
+
})),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
async function verifyCustomizationConflict(context) {
|
|
597
|
+
const sourceFile = join(context.source, "managed.txt");
|
|
598
|
+
const localFile = join(context.root, "managed.txt");
|
|
599
|
+
await writeFile(localFile, "deliberate-user-edit\n", "utf8");
|
|
600
|
+
await writeFile(sourceFile, "upstream-update\n", "utf8");
|
|
601
|
+
try {
|
|
602
|
+
await context.service.update(context.source);
|
|
603
|
+
throw new Error("Customization conflict was not detected");
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
if (!(error instanceof ApexError) || error.code !== "APEX_CONFLICT")
|
|
607
|
+
throw error;
|
|
608
|
+
}
|
|
609
|
+
if ((await readFile(localFile, "utf8")) !== "deliberate-user-edit\n")
|
|
610
|
+
throw new Error("Deliberate edit was overwritten");
|
|
611
|
+
}
|
|
612
|
+
async function faultStaleTask(root, track, clock, ids) {
|
|
613
|
+
const service = new ApexService(join(root, ".scenarios", "stale-task"), { clock: clock.now, idSource: ids.next });
|
|
614
|
+
await service.init({ projectId: `stale-${track}`, iacTool: track });
|
|
615
|
+
await completeRequirementsIntake(service, `${track} stale task workload`);
|
|
616
|
+
const issued = await service.nextTask();
|
|
617
|
+
if (issued.status !== "task")
|
|
618
|
+
throw new Error("Expected fault task");
|
|
619
|
+
await service.cancelTask(issued.task.taskId);
|
|
620
|
+
await expectFailure(() => service.completeTaskOutputs(issued.task.taskId, [
|
|
621
|
+
{ kind: "requirements", value: requirementsFor(`stale-${track}`) },
|
|
622
|
+
]), /stale/i);
|
|
623
|
+
}
|
|
624
|
+
async function faultCorruptJournal(root, clock, ids) {
|
|
625
|
+
const directory = join(root, ".faults", ids.next(), "journal");
|
|
626
|
+
const journal = new EventJournal(directory);
|
|
627
|
+
await journal.append({
|
|
628
|
+
eventId: ids.next(),
|
|
629
|
+
projectId: "fault",
|
|
630
|
+
runId: "fault",
|
|
631
|
+
type: "fault",
|
|
632
|
+
timestamp: clock.now().toISOString(),
|
|
633
|
+
ownerEpoch: 1,
|
|
634
|
+
expectedHead: null,
|
|
635
|
+
payload: { value: 1 },
|
|
636
|
+
});
|
|
637
|
+
const file = join(directory, (await readdir(directory))[0]);
|
|
638
|
+
await writeFile(file, (await readFile(file, "utf8")).replace('"value":1', '"value":2'), "utf8");
|
|
639
|
+
await expectFailure(() => journal.replay(), /corrupt/i);
|
|
640
|
+
}
|
|
641
|
+
async function faultStaleEpoch(root, clock, ids) {
|
|
642
|
+
const journal = new EventJournal(join(root, ".faults", ids.next(), "journal"));
|
|
643
|
+
const first = await journal.append({
|
|
644
|
+
eventId: ids.next(),
|
|
645
|
+
projectId: "fault",
|
|
646
|
+
runId: "fault",
|
|
647
|
+
type: "first",
|
|
648
|
+
timestamp: clock.now().toISOString(),
|
|
649
|
+
ownerEpoch: 2,
|
|
650
|
+
expectedHead: null,
|
|
651
|
+
payload: {},
|
|
652
|
+
});
|
|
653
|
+
await expectFailure(() => journal.append({
|
|
654
|
+
eventId: ids.next(),
|
|
655
|
+
projectId: "fault",
|
|
656
|
+
runId: "fault",
|
|
657
|
+
type: "stale",
|
|
658
|
+
timestamp: clock.now().toISOString(),
|
|
659
|
+
ownerEpoch: 1,
|
|
660
|
+
expectedHead: first.hash,
|
|
661
|
+
payload: {},
|
|
662
|
+
}), /epoch/i);
|
|
663
|
+
}
|
|
664
|
+
async function faultChangedPreview(root, track, clock, ids) {
|
|
665
|
+
const service = await minimalService(join(root, ".scenarios", "changed-preview"), track, clock, ids);
|
|
666
|
+
const preview = await service.preview({ operation: "apply", provider: "fake" });
|
|
667
|
+
await service.decideGateNumber(4, "approved", "qualification");
|
|
668
|
+
await expectFailure(() => service.deploy(sha256Json({ changed: preview.previewHash })), /current/i);
|
|
669
|
+
}
|
|
670
|
+
async function minimalService(root, track, clock, ids) {
|
|
671
|
+
const service = new ApexService(root, { clock: clock.now, idSource: ids.next });
|
|
672
|
+
const context = {
|
|
673
|
+
root,
|
|
674
|
+
source: "",
|
|
675
|
+
track,
|
|
676
|
+
clock,
|
|
677
|
+
ids,
|
|
678
|
+
options: { clock: clock.now, idSource: ids.next },
|
|
679
|
+
runId: "",
|
|
680
|
+
service,
|
|
681
|
+
checks: [],
|
|
682
|
+
taskContextBytes: [],
|
|
683
|
+
};
|
|
684
|
+
context.runId = (await service.init({ projectId: projectId(track), iacTool: track })).runId;
|
|
685
|
+
await completeCreativeWorkflow(context);
|
|
686
|
+
return service;
|
|
687
|
+
}
|
|
688
|
+
async function faultCacheInvalidation(root) {
|
|
689
|
+
const cache = new ContentCache(root);
|
|
690
|
+
const input = { dependencies: { source: HASH }, config: { mode: "qualification" }, toolchain: { version: "1" } };
|
|
691
|
+
contentCacheKey(input);
|
|
692
|
+
await cache.set(input, { valid: true });
|
|
693
|
+
if ((await cache.invalidate("source")) !== 1 || (await cache.get(input)) !== null)
|
|
694
|
+
throw new Error("Cache dependency was not invalidated");
|
|
695
|
+
}
|
|
696
|
+
async function faultMissingCapabilityPack() {
|
|
697
|
+
const loader = new CapabilityPackLoader({ resolvePackageJson: async () => undefined });
|
|
698
|
+
const status = await loader.check({ packageName: "@apexops/missing-qualification-pack", version: "1.0.0" });
|
|
699
|
+
if (status.available || status.compatible || !status.actionableMessage.includes("missing"))
|
|
700
|
+
throw new Error("Missing pack was not actionable");
|
|
701
|
+
}
|
|
702
|
+
async function verifyRepositoryBytes(root) {
|
|
703
|
+
const forbidden = [/ghp_[A-Za-z0-9]+/, /BEGIN PRIVATE KEY/, /terraform\.tfstate/, /\.tfplan/];
|
|
704
|
+
for (const file of await files(root)) {
|
|
705
|
+
const path = relative(root, file);
|
|
706
|
+
if (path === ".apex" || path.startsWith(`.apex/`) || path.startsWith(".faults/"))
|
|
707
|
+
continue;
|
|
708
|
+
const bytes = await readFile(file);
|
|
709
|
+
const text = bytes.toString("utf8");
|
|
710
|
+
if (forbidden.some((pattern) => pattern.test(text)) || /(^|\/)terraform\.tfstate($|\.)|\.tfplan$/i.test(path))
|
|
711
|
+
throw new Error(`Forbidden repository bytes in ${path}`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
async function files(directory) {
|
|
715
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
716
|
+
const nested = await Promise.all(entries.map(async (entry) => entry.isDirectory() ? files(join(directory, entry.name)) : [join(directory, entry.name)]));
|
|
717
|
+
return nested.flat();
|
|
718
|
+
}
|
|
719
|
+
function logicalInventoryHash(inventory) {
|
|
720
|
+
if (inventory === undefined)
|
|
721
|
+
return "";
|
|
722
|
+
return sha256Json(inventory.resources
|
|
723
|
+
.map(({ logicalId, type, location }) => ({ logicalId, type, location }))
|
|
724
|
+
.sort((left, right) => left.logicalId.localeCompare(right.logicalId)));
|
|
725
|
+
}
|
|
726
|
+
async function checked(checks, clock, id, callback, injectFailure) {
|
|
727
|
+
const started = clock.now().getTime();
|
|
728
|
+
clock.advance(1);
|
|
729
|
+
try {
|
|
730
|
+
if (injectFailure === id)
|
|
731
|
+
throw new Error(`Injected qualification failure: ${id}`);
|
|
732
|
+
await callback();
|
|
733
|
+
checks.push({ id, status: "pass", durationMs: clock.now().getTime() - started });
|
|
734
|
+
}
|
|
735
|
+
catch (error) {
|
|
736
|
+
checks.push({
|
|
737
|
+
id,
|
|
738
|
+
status: "fail",
|
|
739
|
+
durationMs: clock.now().getTime() - started,
|
|
740
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
async function expectFailure(callback, pattern) {
|
|
745
|
+
try {
|
|
746
|
+
await callback();
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
if (error instanceof Error && pattern.test(error.message))
|
|
750
|
+
return;
|
|
751
|
+
throw error;
|
|
752
|
+
}
|
|
753
|
+
throw new Error(`Expected failure matching ${pattern}`);
|
|
754
|
+
}
|
|
755
|
+
function budgetCheck(id, actual, maximum) {
|
|
756
|
+
return actual <= maximum
|
|
757
|
+
? { id, status: "pass", durationMs: 0 }
|
|
758
|
+
: { id, status: "fail", durationMs: 0, detail: `${actual}ms exceeds ${maximum}ms` };
|
|
759
|
+
}
|
|
760
|
+
//# sourceMappingURL=qualification.js.map
|