@ornexus/neocortex 4.60.67 → 4.60.68
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/release-source.json +1 -1
- package/dist/sbom.cdx.json +1194 -1194
- package/install.sh +1 -1
- package/package.json +2 -2
- package/packages/client/dist/adapters/adapter-registry.js +108 -1
- package/packages/client/dist/adapters/antigravity-adapter.js +77 -2
- package/packages/client/dist/adapters/claude-code-adapter.js +79 -3
- package/packages/client/dist/adapters/codex-adapter.js +80 -2
- package/packages/client/dist/adapters/cursor-adapter.js +115 -4
- package/packages/client/dist/adapters/gemini-adapter.js +71 -2
- package/packages/client/dist/adapters/grok-adapter.js +42 -2
- package/packages/client/dist/adapters/index.js +22 -1
- package/packages/client/dist/adapters/platform-detector.js +212 -1
- package/packages/client/dist/adapters/target-adapter.js +12 -0
- package/packages/client/dist/adapters/vscode-adapter.js +72 -2
- package/packages/client/dist/agent/refresh-stubs.js +417 -2
- package/packages/client/dist/agent/update-agent-yaml.js +102 -1
- package/packages/client/dist/agent/update-description.js +251 -1
- package/packages/client/dist/cache/crypto-utils.js +78 -1
- package/packages/client/dist/cache/encrypted-cache.js +108 -1
- package/packages/client/dist/cache/in-memory-asset-cache.js +73 -1
- package/packages/client/dist/cache/index.js +13 -1
- package/packages/client/dist/cache/protected-pi-boundary.js +44 -1
- package/packages/client/dist/checkpoint/checkpoint-client-reader.js +127 -2
- package/packages/client/dist/checkpoint/index.js +11 -1
- package/packages/client/dist/checkpoint/shared-checkpoint-types.js +32 -1
- package/packages/client/dist/cli.js +290 -3
- package/packages/client/dist/commands/activate.js +394 -8
- package/packages/client/dist/commands/architecture-answers.js +274 -7
- package/packages/client/dist/commands/architecture-memory-terminal.js +253 -4
- package/packages/client/dist/commands/architecture-non-ui-continuation.js +100 -1
- package/packages/client/dist/commands/architecture-root-binding.js +51 -1
- package/packages/client/dist/commands/cache-status.js +112 -2
- package/packages/client/dist/commands/config.js +88 -1
- package/packages/client/dist/commands/doctor.js +635 -2
- package/packages/client/dist/commands/invoke.js +7140 -142
- package/packages/client/dist/commands/legacy-epic-terminal.js +71 -1
- package/packages/client/dist/commands/legacy-story-branch.js +96 -1
- package/packages/client/dist/commands/legacy-story-pr.js +276 -1
- package/packages/client/dist/commands/mutation-qa-checkpoint.js +285 -5
- package/packages/client/dist/commands/mutation-qa-gate.js +54 -1
- package/packages/client/dist/commands/mutation-qa-git.js +41 -1
- package/packages/client/dist/commands/mutation-qa-policy.js +102 -1
- package/packages/client/dist/commands/mutation-qa-run.js +219 -3
- package/packages/client/dist/commands/mutation-qa-verdict.js +149 -1
- package/packages/client/dist/commands/mutation-qa.js +220 -1
- package/packages/client/dist/commands/native-agent.js +136 -2
- package/packages/client/dist/commands/planning-diagnosis.js +43 -1
- package/packages/client/dist/commands/planning-generation.js +53 -1
- package/packages/client/dist/commands/planning-memory.js +40 -1
- package/packages/client/dist/commands/planning-research.js +42 -1
- package/packages/client/dist/commands/private-session.js +75 -1
- package/packages/client/dist/commands/property-qa.js +139 -1
- package/packages/client/dist/commands/qa-control.js +16 -1
- package/packages/client/dist/commands/qa-scan-browser-launch.js +55 -1
- package/packages/client/dist/commands/qa-scan-browser-route.js +28 -1
- package/packages/client/dist/commands/qa-scan-capture.js +663 -5
- package/packages/client/dist/commands/qa-scan-discovery.js +376 -3
- package/packages/client/dist/commands/qa-scan-review.js +826 -5
- package/packages/client/dist/commands/qa-synthetic-backend-capture.js +266 -1
- package/packages/client/dist/commands/qa-synthetic-backend-review.js +297 -1
- package/packages/client/dist/commands/qa-synthetic-backend.js +340 -2
- package/packages/client/dist/commands/qa-synthetic-broker-port.js +85 -1
- package/packages/client/dist/commands/qa-synthetic-browser.js +39 -1
- package/packages/client/dist/commands/refactor-characterization.js +671 -21
- package/packages/client/dist/commands/refactor-commit.js +399 -13
- package/packages/client/dist/commands/refactor-control.js +142 -3
- package/packages/client/dist/commands/refactor-cpp-assertions.js +93 -3
- package/packages/client/dist/commands/refactor-review.js +283 -5
- package/packages/client/dist/commands/refactor-root.js +588 -7
- package/packages/client/dist/commands/refresh-memory.js +156 -3
- package/packages/client/dist/commands/shared-architecture-non-ui.js +24 -1
- package/packages/client/dist/commands/story-checkpoint.js +171 -1
- package/packages/client/dist/config/profile-directory.js +13 -1
- package/packages/client/dist/config/resolver-selection.js +278 -1
- package/packages/client/dist/config/secure-config.js +281 -12
- package/packages/client/dist/config.js +223 -2
- package/packages/client/dist/constants.js +25 -1
- package/packages/client/dist/context/context-collector.js +222 -2
- package/packages/client/dist/context/context-sanitizer.js +145 -1
- package/packages/client/dist/continuity/autonomous-governance.js +285 -1
- package/packages/client/dist/continuity/continuity-client-state-store.js +1587 -1
- package/packages/client/dist/continuity/invoke-hooks.js +129 -1
- package/packages/client/dist/continuity/migrations/001-initial-schema.js +12 -2
- package/packages/client/dist/continuity/migrations/002-processing-output-summary-indexes.js +12 -2
- package/packages/client/dist/continuity/migrations/003-headless-execution-attempts.js +12 -2
- package/packages/client/dist/continuity/migrations/004-outbox-delivery-state.js +12 -2
- package/packages/client/dist/continuity/migrations/005-bounded-headless-queue.js +12 -2
- package/packages/client/dist/continuity/migrations/006-native-agent-dispatches.js +10 -2
- package/packages/client/dist/continuity/native-agent-store.js +69 -1
- package/packages/client/dist/continuity/native-agent-types.js +90 -1
- package/packages/client/dist/continuity/outbox.js +117 -1
- package/packages/client/dist/continuity/runner-bootstrap-policy.js +172 -1
- package/packages/client/dist/continuity/sqlite-store.js +2536 -81
- package/packages/client/dist/effects/invoke-effect-gateways.js +62 -1
- package/packages/client/dist/errors/error-messages.js +228 -2
- package/packages/client/dist/errors/frontmatter-diagnostics.js +125 -1
- package/packages/client/dist/evidence/collector.js +347 -1
- package/packages/client/dist/evidence/index.js +1 -1
- package/packages/client/dist/evidence/types.js +2 -0
- package/packages/client/dist/fs/atomic-state-replace.js +152 -1
- package/packages/client/dist/generated/command-registry.generated.js +4076 -1
- package/packages/client/dist/generated/public-projections.generated.js +149 -1
- package/packages/client/dist/git/direct-yolo-c01-retry.js +138 -4
- package/packages/client/dist/git/direct-yolo-checkpoint.js +1234 -4
- package/packages/client/dist/git/git-lifecycle-step-ids.js +14 -1
- package/packages/client/dist/git/git-lifecycle-store.js +1938 -9
- package/packages/client/dist/git/legacy-story-branch-checkpoint.js +259 -9
- package/packages/client/dist/git/legacy-story-branch-setup.js +348 -4
- package/packages/client/dist/git/legacy-story-pr.js +3538 -70
- package/packages/client/dist/git/secure-filesystem-boundary.js +182 -5
- package/packages/client/dist/graph-retrieval/pre-command-hook.js +284 -1
- package/packages/client/dist/graph-retrieval/shared-graph-retrieval-contract.js +237 -1
- package/packages/client/dist/i18n/first-run.js +60 -2
- package/packages/client/dist/index.js +47 -1
- package/packages/client/dist/invocation/invoke-input.js +24 -1
- package/packages/client/dist/license/index.js +5 -1
- package/packages/client/dist/license/license-client.js +304 -1
- package/packages/client/dist/machine/fingerprint.js +163 -2
- package/packages/client/dist/machine/index.js +5 -1
- package/packages/client/dist/memory/project-memory-path-scope.js +62 -1
- package/packages/client/dist/memory/project-memory-writer.js +1007 -40
- package/packages/client/dist/memory/shared-project-memory-types.js +253 -2
- package/packages/client/dist/migrations/project-identity-capability.js +50 -1
- package/packages/client/dist/migrations/project-identity.js +493 -7
- package/packages/client/dist/migrations/state-json.js +344 -4
- package/packages/client/dist/policy/architecture-policy.js +184 -2
- package/packages/client/dist/policy/index.js +7 -1
- package/packages/client/dist/policy/shared-policy-types.js +38 -0
- package/packages/client/dist/policy/subagent-approval-policy.js +55 -1
- package/packages/client/dist/private-execution/clean-baseline.js +279 -2
- package/packages/client/dist/private-execution/codex-executor.js +574 -4
- package/packages/client/dist/private-execution/codex-host-runtime.js +137 -1
- package/packages/client/dist/private-execution/codex-lifecycle-guard.js +97 -1
- package/packages/client/dist/private-execution/codex-scoped-permissions.js +128 -1
- package/packages/client/dist/private-execution/delivery.js +410 -13
- package/packages/client/dist/private-execution/diagnosis-root-review.js +294 -2
- package/packages/client/dist/private-execution/frozen-handoff.js +19 -6
- package/packages/client/dist/private-execution/implementation-validation.js +106 -2
- package/packages/client/dist/private-execution/isolated-admission.js +1432 -23
- package/packages/client/dist/private-execution/isolated-session.js +725 -5
- package/packages/client/dist/private-execution/isolated-workspace.js +122 -1
- package/packages/client/dist/private-execution/pact-consumer-runtime.js +409 -3
- package/packages/client/dist/private-execution/pact-test-source.js +258 -1
- package/packages/client/dist/private-execution/pact-toolchain.js +290 -3
- package/packages/client/dist/private-execution/pact-write-set.js +172 -1
- package/packages/client/dist/private-execution/planning-diagnosis-checkpoint.js +406 -5
- package/packages/client/dist/private-execution/planning-generation-checkpoint.js +462 -5
- package/packages/client/dist/private-execution/planning-memory-checkpoint.js +139 -3
- package/packages/client/dist/private-execution/planning-research-checkpoint.js +325 -4
- package/packages/client/dist/private-execution/private-report-store.js +42 -1
- package/packages/client/dist/private-execution/property-test-candidate.js +103 -1
- package/packages/client/dist/private-execution/property-test-choices.js +147 -3
- package/packages/client/dist/private-execution/property-test-plan.js +271 -7
- package/packages/client/dist/private-execution/property-test-review.js +258 -4
- package/packages/client/dist/private-execution/property-test-runtime.js +372 -9
- package/packages/client/dist/private-execution/property-test-source.js +139 -1
- package/packages/client/dist/private-execution/root-checkpoint.js +80 -1
- package/packages/client/dist/private-execution/root-memory-git-evidence.js +135 -3
- package/packages/client/dist/private-execution/root-memory-recovery.js +93 -1
- package/packages/client/dist/private-execution/root-memory-request.js +29 -1
- package/packages/client/dist/private-execution/root-memory-scope.js +178 -1
- package/packages/client/dist/private-execution/root-memory-writer.js +102 -1
- package/packages/client/dist/private-execution/session-lease.js +63 -1
- package/packages/client/dist/private-execution/shared-private-admission-types.js +50 -1
- package/packages/client/dist/private-execution/shared-project-validation.js +66 -1
- package/packages/client/dist/private-execution/shared-root-memory-types.js +61 -1
- package/packages/client/dist/private-execution/ux-source-validation.js +372 -1
- package/packages/client/dist/private-execution/ux-write-set.js +191 -2
- package/packages/client/dist/resilience/circuit-breaker.js +170 -1
- package/packages/client/dist/resilience/degradation-manager.js +164 -1
- package/packages/client/dist/resilience/freshness-indicator.js +100 -1
- package/packages/client/dist/resilience/index.js +8 -1
- package/packages/client/dist/resilience/recovery-detector.js +74 -1
- package/packages/client/dist/resolvers/asset-resolver.js +13 -0
- package/packages/client/dist/resolvers/local-resolver.js +255 -8
- package/packages/client/dist/resolvers/remote-resolver.js +233 -1
- package/packages/client/dist/runner/binary.js +404 -1
- package/packages/client/dist/runner/cli.js +1571 -17
- package/packages/client/dist/runner/headless/adapters.js +49 -1
- package/packages/client/dist/runner/headless/claude-code-adapter.js +79 -1
- package/packages/client/dist/runner/headless/continuous-story-workspace.js +1628 -11
- package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.js +69 -1
- package/packages/client/dist/runner/headless/epic-merge-candidate-qa.js +110 -2
- package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.js +1236 -8
- package/packages/client/dist/runner/headless/epic-terminal-git-runner.js +318 -2
- package/packages/client/dist/runner/headless/fake-adapter.js +62 -1
- package/packages/client/dist/runner/headless/managed-profile.js +132 -1
- package/packages/client/dist/runner/headless/opencode-adapter.js +145 -2
- package/packages/client/dist/runner/headless/prepared-story-bundle.js +43 -1
- package/packages/client/dist/runner/headless/resolver.js +231 -1
- package/packages/client/dist/runner/headless/run-yolo.js +885 -1
- package/packages/client/dist/runner/headless/run-yoloop.js +2574 -2
- package/packages/client/dist/runner/headless/types.js +574 -2
- package/packages/client/dist/runner/local-console.js +396 -13
- package/packages/client/dist/runner/portal-sync-worker.js +1487 -1
- package/packages/client/dist/runner/project-identity.js +106 -1
- package/packages/client/dist/runner/scheduler.js +995 -7
- package/packages/client/dist/runner-cli.js +18 -2
- package/packages/client/dist/state/invocation-shape.js +13 -1
- package/packages/client/dist/state/project-state-snapshot.js +1677 -1
- package/packages/client/dist/state/state-json-repair.js +785 -3
- package/packages/client/dist/state/structural-dag-projection.js +118 -1
- package/packages/client/dist/telemetry/index.js +5 -1
- package/packages/client/dist/telemetry/offline-queue.js +131 -1
- package/packages/client/dist/tier/index.js +5 -1
- package/packages/client/dist/tier/tier-aware-client.js +405 -1
- package/packages/client/dist/types/index.js +38 -1
- package/packages/client/dist/version.js +27 -1
- package/packages/client/dist/yolo/change-evidence-collector.js +126 -1
- package/packages/client/dist/yolo/invoke-envelope-capability.js +62 -1
- package/packages/client/dist/yolo/spec-envelope-collector.js +185 -5
- package/packages/client/dist/yoloop/discovery-hook.js +300 -2
- package/packages/client/dist/yoloop/index.js +8 -1
- package/packages/client/dist/yoloop/invoke-hooks.js +669 -12
- package/packages/client/dist/yoloop/shared-discover-epics.js +1007 -1
- package/packages/client/dist/yoloop/shared-yoloop-types.js +33 -1
- package/packages/client/dist/yoloop/target-normalization.js +180 -1
- package/packages/client/dist/yoloop/yoloop-client-state-store.js +1723 -1
- package/targets-stubs/antigravity/gemini.md +1 -1
- package/targets-stubs/antigravity/skill/SKILL.md +1 -1
- package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex-root.md +2 -2
- package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex.md +2 -2
- package/targets-stubs/codex/AGENTS.md +1 -1
- package/targets-stubs/cursor/agent.md +2 -2
- package/targets-stubs/gemini-cli/agent.md +2 -2
- package/targets-stubs/opencode/neocortex-root.md +1 -1
- package/targets-stubs/vscode/neocortex.agent.md +2 -2
|
@@ -1 +1,266 @@
|
|
|
1
|
-
|
|
1
|
+
/** Browser/backend observations for the separately granted synthetic path.
|
|
2
|
+
* Schema 3 deliberately cannot masquerade as the DOM-only schema 2 evidence. */
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { closeSync, constants, fsyncSync, openSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import axe from 'axe-core';
|
|
7
|
+
import { launchSyntheticBackendBrowser } from './qa-synthetic-browser.js';
|
|
8
|
+
import { scenarioRequirementDigest, validateRootQaScanScenario } from './qa-scan-capture.js';
|
|
9
|
+
import { planRootQaScan, rootQaScanEvidenceDirectory } from './qa-scan-review.js';
|
|
10
|
+
import { openSyntheticBackendSession } from './qa-synthetic-backend.js';
|
|
11
|
+
const sha = (s) => createHash('sha256').update(s).digest('hex');
|
|
12
|
+
export async function captureSyntheticBackendScenario(options, trustedPort, dockerContext) {
|
|
13
|
+
const scenario = validateRootQaScanScenario(options.scenario);
|
|
14
|
+
const scenarioSha256 = sha(JSON.stringify(scenario));
|
|
15
|
+
const plan = planRootQaScan(options.projectRoot, options.surface, options.targetUrl);
|
|
16
|
+
const requirementSourceSha256 = scenarioRequirementDigest(options.projectRoot, scenario);
|
|
17
|
+
if (trustedPort && (Object.keys(trustedPort).sort().join(',') !== 'contractSha256,observations,request,scopeSha256,verify'
|
|
18
|
+
|| !['observations', 'request', 'verify'].every(key => typeof trustedPort[key] === 'function')
|
|
19
|
+
|| !/^[a-f0-9]{64}$/.test(trustedPort.contractSha256)
|
|
20
|
+
|| trustedPort.scopeSha256 !== sha(JSON.stringify([plan.sourceHead, options.surface, options.targetUrl,
|
|
21
|
+
options.generation, scenarioSha256, options.grantId]))))
|
|
22
|
+
throw new Error('qa-synthetic-backend-broker-port-unverified');
|
|
23
|
+
const backend = trustedPort ?? await openSyntheticBackendSession({ ...options, scenarioSha256 });
|
|
24
|
+
if (dockerContext && (!trustedPort || Object.keys(dockerContext).sort().join(',') !== 'containerId,imageId,jobBindingSha256,kind,launch'
|
|
25
|
+
|| dockerContext.kind !== 'operator-docker-job' || !/^sha256:[a-f0-9]{64}$/.test(dockerContext.imageId)
|
|
26
|
+
|| !/^[a-f0-9]{64}$/.test(dockerContext.containerId) || !/^[a-f0-9]{64}$/.test(dockerContext.jobBindingSha256)
|
|
27
|
+
|| typeof dockerContext.launch !== 'function'))
|
|
28
|
+
throw new Error('qa-synthetic-backend-docker-context-invalid');
|
|
29
|
+
const browser = dockerContext ? await dockerContext.launch() : await launchSyntheticBackendBrowser();
|
|
30
|
+
let timer;
|
|
31
|
+
let expired = false;
|
|
32
|
+
let phase = 'context';
|
|
33
|
+
let routeFailure = '';
|
|
34
|
+
try {
|
|
35
|
+
const context = await browser.newContext({ serviceWorkers: 'block', acceptDownloads: false,
|
|
36
|
+
permissions: [], ignoreHTTPSErrors: false, viewport: { width: 1280, height: 900 } });
|
|
37
|
+
let denied = 0;
|
|
38
|
+
let disruptions = 0;
|
|
39
|
+
let started = false;
|
|
40
|
+
let actionRequests = 0;
|
|
41
|
+
let initialRequests = 0;
|
|
42
|
+
let consoleErrors = 0;
|
|
43
|
+
let pageErrors = 0;
|
|
44
|
+
let failedRequests = 0;
|
|
45
|
+
let requestsObserved = 0;
|
|
46
|
+
const pending = new Set();
|
|
47
|
+
const target = new URL(options.targetUrl);
|
|
48
|
+
await context.route('**/*', async (route) => {
|
|
49
|
+
try {
|
|
50
|
+
const request = route.request();
|
|
51
|
+
// Request cookies/auth are refused; headers are never forwarded.
|
|
52
|
+
if (started)
|
|
53
|
+
actionRequests++;
|
|
54
|
+
else
|
|
55
|
+
initialRequests++;
|
|
56
|
+
const transfer = backend.request(request.url(), request.method(), request.postData(), await request.allHeaders());
|
|
57
|
+
pending.add(transfer);
|
|
58
|
+
let response;
|
|
59
|
+
try {
|
|
60
|
+
response = await transfer;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
pending.delete(transfer);
|
|
64
|
+
}
|
|
65
|
+
await route.fulfill({ status: response.status, body: response.body,
|
|
66
|
+
headers: { 'content-type': response.contentType, 'cache-control': 'no-store' } });
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error instanceof Error && /^qa-[a-z-]+$/.test(error.message))
|
|
70
|
+
routeFailure = error.message;
|
|
71
|
+
denied++;
|
|
72
|
+
await route.abort('blockedbyclient').catch(() => undefined);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
await context.routeWebSocket(/.*/, async (socket) => { denied++; await socket.close({ code: 1008, reason: 'blocked' }); });
|
|
76
|
+
const page = await context.newPage();
|
|
77
|
+
context.on('page', opened => { if (opened !== page) {
|
|
78
|
+
disruptions++;
|
|
79
|
+
void opened.close();
|
|
80
|
+
} });
|
|
81
|
+
page.on('framenavigated', () => { if (started)
|
|
82
|
+
disruptions++; });
|
|
83
|
+
page.on('download', () => { disruptions++; });
|
|
84
|
+
page.on('pageerror', () => { disruptions++; pageErrors++; });
|
|
85
|
+
page.on('console', message => { if (message.type() === 'error') {
|
|
86
|
+
disruptions++;
|
|
87
|
+
consoleErrors++;
|
|
88
|
+
} });
|
|
89
|
+
page.on('request', () => { requestsObserved++; });
|
|
90
|
+
page.on('requestfailed', () => { failedRequests++; });
|
|
91
|
+
page.on('response', r => { if (r.status() >= 400) {
|
|
92
|
+
failedRequests++;
|
|
93
|
+
disruptions++;
|
|
94
|
+
} });
|
|
95
|
+
timer = setTimeout(() => { expired = true; void context.close().catch(() => undefined); }, 15000);
|
|
96
|
+
phase = 'navigation';
|
|
97
|
+
const response = await page.goto(target.href, { waitUntil: 'load', timeout: 5000 });
|
|
98
|
+
if (!response || response.status() >= 400 || page.url() !== target.href || denied || disruptions) {
|
|
99
|
+
throw new Error('qa-synthetic-backend-target-unavailable');
|
|
100
|
+
}
|
|
101
|
+
started = true;
|
|
102
|
+
const safe = () => {
|
|
103
|
+
if (denied)
|
|
104
|
+
throw new Error('qa-synthetic-backend-effects-denied-network');
|
|
105
|
+
if (disruptions)
|
|
106
|
+
throw new Error('qa-synthetic-backend-effects-denied-page');
|
|
107
|
+
if (expired)
|
|
108
|
+
throw new Error('qa-synthetic-backend-effects-denied-budget');
|
|
109
|
+
if (page.url() !== target.href)
|
|
110
|
+
throw new Error('qa-synthetic-backend-effects-denied-navigation');
|
|
111
|
+
};
|
|
112
|
+
const unique = async (selector) => {
|
|
113
|
+
const locator = page.locator(selector);
|
|
114
|
+
if (await locator.count() !== 1)
|
|
115
|
+
throw new Error('qa-synthetic-backend-target-ambiguous');
|
|
116
|
+
return locator;
|
|
117
|
+
};
|
|
118
|
+
phase = 'scenario';
|
|
119
|
+
for (const action of scenario.actions) {
|
|
120
|
+
const locator = await unique(action.selector);
|
|
121
|
+
if (!await locator.isVisible() || !await locator.isEnabled())
|
|
122
|
+
throw new Error('qa-synthetic-backend-action-unavailable');
|
|
123
|
+
if (action.kind === 'click')
|
|
124
|
+
await locator.click({ timeout: 2000 });
|
|
125
|
+
else if (action.kind === 'fill')
|
|
126
|
+
await locator.fill(action.value, { timeout: 2000 });
|
|
127
|
+
else if (action.kind === 'select')
|
|
128
|
+
await locator.selectOption(action.value, { timeout: 2000 });
|
|
129
|
+
else if (action.kind === 'press')
|
|
130
|
+
await locator.press(action.key, { timeout: 2000 });
|
|
131
|
+
safe();
|
|
132
|
+
}
|
|
133
|
+
const checks = [];
|
|
134
|
+
for (const check of scenario.checks) {
|
|
135
|
+
const locator = await unique(check.selector);
|
|
136
|
+
const deadline = Date.now() + 2000;
|
|
137
|
+
for (;;) {
|
|
138
|
+
safe();
|
|
139
|
+
const value = check.kind === 'text' ? (await locator.textContent({ timeout: 1000 }))?.trim()
|
|
140
|
+
: check.kind === 'value' ? await locator.inputValue({ timeout: 1000 })
|
|
141
|
+
: check.kind === 'visible' ? await locator.isVisible()
|
|
142
|
+
: check.kind === 'enabled' ? await locator.isEnabled() : await locator.isChecked({ timeout: 1000 });
|
|
143
|
+
if (value === check.expected)
|
|
144
|
+
break;
|
|
145
|
+
if (Date.now() >= deadline)
|
|
146
|
+
throw new Error('qa-synthetic-backend-check-failed');
|
|
147
|
+
await page.waitForTimeout(25);
|
|
148
|
+
}
|
|
149
|
+
checks.push({ id: `functional-${check.id}`, passed: true });
|
|
150
|
+
}
|
|
151
|
+
await page.waitForTimeout(250);
|
|
152
|
+
safe();
|
|
153
|
+
await Promise.allSettled([...pending]);
|
|
154
|
+
safe();
|
|
155
|
+
await backend.verify();
|
|
156
|
+
if (!actionRequests || !backend.observations().length)
|
|
157
|
+
throw new Error('qa-synthetic-backend-no-backend-observation');
|
|
158
|
+
if (planRootQaScan(options.projectRoot, options.surface, options.targetUrl).planSha256 !== plan.planSha256) {
|
|
159
|
+
throw new Error('qa-synthetic-backend-source-drift');
|
|
160
|
+
}
|
|
161
|
+
if (scenarioRequirementDigest(options.projectRoot, scenario) !== requirementSourceSha256)
|
|
162
|
+
throw new Error('qa-synthetic-backend-source-drift');
|
|
163
|
+
phase = 'artifacts';
|
|
164
|
+
const dir = rootQaScanEvidenceDirectory(options.projectRoot);
|
|
165
|
+
const observations = [];
|
|
166
|
+
const capturedAt = new Date().toISOString();
|
|
167
|
+
const save = (kind, method, bytes, suffix, rationale) => {
|
|
168
|
+
if (bytes.length < 8 || bytes.length > 8 * 1024 * 1024)
|
|
169
|
+
throw new Error('qa-synthetic-backend-artifact-limit');
|
|
170
|
+
const digest = createHash('sha256').update(bytes).digest('hex');
|
|
171
|
+
const artifactId = `backend-${kind}-${randomUUID().replaceAll('-', '').slice(0, 20)}.${suffix}`;
|
|
172
|
+
const fd = openSync(join(dir, artifactId), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
173
|
+
try {
|
|
174
|
+
writeFileSync(fd, bytes);
|
|
175
|
+
fsyncSync(fd);
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
closeSync(fd);
|
|
179
|
+
}
|
|
180
|
+
observations.push({ kind, artifactId, sha256: digest, captureMethod: method, capturedAt, rationale });
|
|
181
|
+
};
|
|
182
|
+
const json = (v) => Buffer.from(JSON.stringify(v));
|
|
183
|
+
await page.addScriptTag({ content: axe.source });
|
|
184
|
+
let violations = 0;
|
|
185
|
+
let incomplete = 0;
|
|
186
|
+
let passes = 0;
|
|
187
|
+
for (const [kind, width, height] of [['visual', 1440, 900], ['responsive', 375, 812],
|
|
188
|
+
['responsive-tablet', 768, 1024], ['responsive-desktop', 1024, 768]]) {
|
|
189
|
+
await page.setViewportSize({ width, height });
|
|
190
|
+
safe();
|
|
191
|
+
save(kind, 'browser-screenshot', await page.screenshot({ type: 'png', animations: 'disabled', timeout: 2000 }), 'png', `Isolated network-namespace browser screenshot at ${width}x${height} after the source-bound synthetic scenario.`);
|
|
192
|
+
const result = await page.evaluate('window.axe.run(document)');
|
|
193
|
+
if (!result || !Array.isArray(result.violations) || !Array.isArray(result.incomplete) || !Array.isArray(result.passes))
|
|
194
|
+
throw new Error('qa-synthetic-backend-accessibility-unavailable');
|
|
195
|
+
violations += result.violations.length;
|
|
196
|
+
incomplete += result.incomplete.length;
|
|
197
|
+
passes += result.passes.length;
|
|
198
|
+
safe();
|
|
199
|
+
}
|
|
200
|
+
save('accessibility', 'axe-scan', json({ schemaVersion: 1, engine: 'axe-core', violationCount: violations, incompleteCount: incomplete, passCount: passes }), 'json', 'Independent-review input: axe observations across all four declared viewport widths after the synthetic scenario.');
|
|
201
|
+
const duration = await page.evaluate(`performance.getEntriesByType('navigation')[0]?.duration`);
|
|
202
|
+
if (typeof duration !== 'number' || !Number.isFinite(duration) || duration < 0)
|
|
203
|
+
throw new Error('qa-synthetic-backend-performance-unavailable');
|
|
204
|
+
save('performance', 'performance-trace', json({ schemaVersion: 1, metric: 'load', observedMs: Math.ceil(duration), budgetMs: 10000 }), 'json', 'Physical navigation load observation in the isolated browser with a predeclared ten-second load budget.');
|
|
205
|
+
await Promise.allSettled([...pending]);
|
|
206
|
+
safe();
|
|
207
|
+
await context.close();
|
|
208
|
+
await Promise.allSettled([...pending]);
|
|
209
|
+
safe();
|
|
210
|
+
await backend.verify();
|
|
211
|
+
save('interaction', 'browser-interaction', json({ schemaVersion: 3, syntheticTarget: true, allowedActionRequests: actionRequests,
|
|
212
|
+
deniedRequests: denied, generation: options.generation, scenarioSha256, checks, backend: backend.observations() }), 'json', 'Source-bound synthetic backend interaction with exact declared transport and physical browser/backend observations.');
|
|
213
|
+
save('console-network', 'console-capture', json({ schemaVersion: 1, initialCapture: true, consoleErrors, pageErrors, failedRequests, requestsObserved }), 'json', 'Console and request listeners were installed before navigation in a fresh network-isolated browser context.');
|
|
214
|
+
if (planRootQaScan(options.projectRoot, options.surface, options.targetUrl).planSha256 !== plan.planSha256
|
|
215
|
+
|| scenarioRequirementDigest(options.projectRoot, scenario) !== requirementSourceSha256)
|
|
216
|
+
throw new Error('qa-synthetic-backend-source-drift');
|
|
217
|
+
const receipt = { schemaVersion: 3, kind: 'root-qa-synthetic-backend-observation', sourceHead: plan.sourceHead,
|
|
218
|
+
planSha256: plan.planSha256, surface: options.surface, targetSha256: plan.targetSha256,
|
|
219
|
+
generation: options.generation, scenarioSha256, backendContractSha256: backend.contractSha256,
|
|
220
|
+
requirementSourceSha256, requirementIds: scenario.requirementIds,
|
|
221
|
+
capturedAt, checks, scenario, grantId: options.grantId, evidenceCount: 8, observations,
|
|
222
|
+
initialRequests, allowedActionRequests: actionRequests, deniedRequests: denied, backend: backend.observations(),
|
|
223
|
+
sandboxNetwork: 'none', browserNetwork: dockerContext ? 'docker-none' : 'unshared', browserHostWorkspaceMounted: false, browserVersion: browser.version(),
|
|
224
|
+
...(dockerContext ? { producerJob: { imageId: dockerContext.imageId, containerId: dockerContext.containerId, jobBindingSha256: dockerContext.jobBindingSha256 } } : {}),
|
|
225
|
+
qaAccepted: false, checkpointAccepted: false, workflowContinuationAuthorized: false };
|
|
226
|
+
const bytes = JSON.stringify(receipt);
|
|
227
|
+
const digest = sha(bytes);
|
|
228
|
+
const fd = openSync(join(dir, `synthetic-backend-${digest}.json`), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
229
|
+
try {
|
|
230
|
+
writeFileSync(fd, bytes);
|
|
231
|
+
fsyncSync(fd);
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
closeSync(fd);
|
|
235
|
+
}
|
|
236
|
+
const parent = openSync(dir, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
237
|
+
try {
|
|
238
|
+
fsyncSync(parent);
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
closeSync(parent);
|
|
242
|
+
}
|
|
243
|
+
return { ...receipt, observationSha256: digest };
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
if (routeFailure)
|
|
247
|
+
throw new Error(routeFailure);
|
|
248
|
+
if (error instanceof Error && /^qa-(synthetic-backend|scan-review)-[a-z-]+$/.test(error.message))
|
|
249
|
+
throw error;
|
|
250
|
+
if (error instanceof Error) {
|
|
251
|
+
const network = error.message.match(/net::(ERR_[A-Z_]+)/)?.[1];
|
|
252
|
+
if (network)
|
|
253
|
+
throw new Error(`qa-synthetic-backend-${phase}-${network.toLowerCase().replaceAll('_', '-')}`);
|
|
254
|
+
if (/Target.*closed|browser has been closed/i.test(error.message))
|
|
255
|
+
throw new Error(`qa-synthetic-backend-${phase}-browser-closed`);
|
|
256
|
+
if (/Timeout/i.test(error.message))
|
|
257
|
+
throw new Error(`qa-synthetic-backend-${phase}-timeout`);
|
|
258
|
+
}
|
|
259
|
+
throw new Error(`qa-synthetic-backend-${phase}-unavailable`);
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
if (timer)
|
|
263
|
+
clearTimeout(timer);
|
|
264
|
+
await browser.close().catch(() => undefined);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -1 +1,297 @@
|
|
|
1
|
-
import{createHash as B}from"node:crypto";import{closeSync as v,constants as l,fstatSync as R,fsyncSync as I,openSync as w,readFileSync as C,writeFileSync as $}from"node:fs";import{join as q}from"node:path";import{planRootQaScan as j,rootQaScanEvidenceDirectory as J,validateRootQaScanArtifact as M}from"./qa-scan-review.js";import{validateRootQaScanScenario as z,scenarioRequirementDigest as L}from"./qa-scan-capture.js";import{validateSyntheticBackendContract as _}from"./qa-synthetic-backend.js";import{readRootQaScanDiscovery as E,readRootQaScanDiscoveryTargets as F}from"./qa-scan-discovery.js";const p=/^[a-f0-9]{64}$/,x=["visual","responsive","responsive-tablet","responsive-desktop","accessibility","interaction","console-network","performance"],T={visual:"browser-screenshot",responsive:"browser-screenshot","responsive-tablet":"browser-screenshot","responsive-desktop":"browser-screenshot",accessibility:"axe-scan",interaction:"browser-interaction","console-network":"console-capture",performance:"performance-trace"},m=t=>B("sha256").update(t).digest("hex");function s(){throw new Error("qa-synthetic-backend-review-unverified")}function h(t,i){return!!t&&typeof t=="object"&&!Array.isArray(t)&&Object.keys(t).sort().join(",")===[...i].sort().join(",")}function g(t,i,o){const n=w(q(J(t),i),l.O_RDONLY|l.O_NOFOLLOW);try{const a=R(n);(!a.isFile()||a.nlink!==1||a.uid!==process.getuid?.()||a.mode&63||a.size<1||a.size>o)&&s();const d=C(n),e=R(n);return(d.length!==a.size||e.size!==a.size||e.mtimeMs!==a.mtimeMs||e.ctimeMs!==a.ctimeMs)&&s(),d}finally{v(n)}}function y(t){const i=new TextDecoder("utf-8",{fatal:!0}).decode(t),o=JSON.parse(i);return(!o||typeof o!="object"||Array.isArray(o)||JSON.stringify(o)!==i)&&s(),o}function A(t,i,o){const n=J(t);let a;try{a=w(q(n,i),l.O_WRONLY|l.O_CREAT|l.O_EXCL|l.O_NOFOLLOW,384)}catch(e){(e.code!=="EEXIST"||g(t,i,131072).toString("utf8")!==o)&&s();return}try{$(a,o),I(a)}finally{v(a)}const d=w(n,l.O_RDONLY|l.O_DIRECTORY|l.O_NOFOLLOW);try{I(d)}finally{v(d)}}function W(t,i,o,n){try{p.test(n)||s();const a=j(t,i,o),d=g(t,`synthetic-backend-${n}.json`,131072);m(d)!==n&&s();const e=y(d);if((!h(e,["schemaVersion","kind","sourceHead","planSha256","surface","targetSha256","generation","scenarioSha256","backendContractSha256","requirementSourceSha256","requirementIds","capturedAt","checks","scenario","grantId","evidenceCount","observations","initialRequests","allowedActionRequests","deniedRequests","backend","sandboxNetwork","browserNetwork","browserHostWorkspaceMounted","browserVersion","qaAccepted","checkpointAccepted","workflowContinuationAuthorized",..."producerJob"in e?["producerJob"]:[]])||e.schemaVersion!==3||e.kind!=="root-qa-synthetic-backend-observation"||e.sourceHead!==a.sourceHead||e.planSha256!==a.planSha256||e.surface!==i||e.targetSha256!==a.targetSha256||typeof e.generation!="string"||!p.test(e.generation)||typeof e.scenarioSha256!="string"||!p.test(e.scenarioSha256)||typeof e.grantId!="string"||!/^[a-f0-9]{8}-[a-f0-9-]{27}$/.test(e.grantId)||typeof e.capturedAt!="string"||!Number.isFinite(Date.parse(e.capturedAt))||new Date(e.capturedAt).toISOString()!==e.capturedAt||Date.parse(e.capturedAt)>Date.now()||Date.now()-Date.parse(e.capturedAt)>36e5||e.evidenceCount!==8||!Array.isArray(e.observations)||e.observations.length!==8||e.sandboxNetwork!=="none"||!["unshared","docker-none"].includes(String(e.browserNetwork))||e.browserHostWorkspaceMounted!==!1||typeof e.browserVersion!="string"||!/^[0-9]+(?:\.[0-9]+){2,3}$/.test(e.browserVersion)||!Number.isSafeInteger(e.allowedActionRequests)||Number(e.allowedActionRequests)<1||Number(e.allowedActionRequests)>64||!Number.isSafeInteger(e.initialRequests)||Number(e.initialRequests)<1||Number(e.initialRequests)>64||e.deniedRequests!==0||e.qaAccepted!==!1||e.checkpointAccepted!==!1||e.workflowContinuationAuthorized!==!1)&&s(),e.browserNetwork==="docker-none"){const r=e.producerJob;(!h(r,["imageId","containerId","jobBindingSha256"])||typeof r.imageId!="string"||!/^sha256:[a-f0-9]{64}$/.test(r.imageId)||typeof r.containerId!="string"||!p.test(r.containerId)||typeof r.jobBindingSha256!="string"||!p.test(r.jobBindingSha256))&&s();const f=y(g(t,`producer-job-${r.jobBindingSha256}-${r.containerId}.json`,4096));(!h(f,["schemaVersion","kind","containerId","imageId","jobBindingSha256","sourceHead","requestSha256","phase","bindingSha256","network","producerDockerSocketMounted","createdVerified","completedVerified"])||f.schemaVersion!==1||f.kind!=="root-synthetic-producer-job-attestation"||f.containerId!==r.containerId||f.imageId!==r.imageId||f.jobBindingSha256!==r.jobBindingSha256||f.sourceHead!==a.sourceHead||typeof f.requestSha256!="string"||!p.test(f.requestSha256)||typeof f.bindingSha256!="string"||!p.test(f.bindingSha256)||f.phase!=="completed"||f.network!=="none"||f.producerDockerSocketMounted!==!1||f.createdVerified!==!0||f.completedVerified!==!0)&&s()}else"producerJob"in e&&s();const c=z(e.scenario);(m(JSON.stringify(c))!==e.scenarioSha256||L(t,c)!==e.requirementSourceSha256||JSON.stringify(c.requirementIds)!==JSON.stringify(e.requirementIds)||JSON.stringify(e.checks)!==JSON.stringify(c.checks.map(r=>({id:`functional-${r.id}`,passed:!0}))))&&s();const u=y(g(t,`backend-grant-${e.grantId}.json`,32768));(!h(u,["schemaVersion","contract","planSha256","sourceHead","surface","targetUrl","expiresAt"])||u.schemaVersion!==1||u.planSha256!==a.planSha256||u.sourceHead!==a.sourceHead||u.surface!==i||u.targetUrl!==o||typeof u.expiresAt!="string"||!Number.isFinite(Date.parse(u.expiresAt)))&&s();const b=_(u.contract);(b.generation!==e.generation||b.scenarioSha256!==e.scenarioSha256||m(JSON.stringify(b))!==e.backendContractSha256||Date.parse(e.capturedAt)>Date.parse(u.expiresAt)||Date.parse(e.capturedAt)<Date.parse(u.expiresAt)-b.durationMs||g(t,`backend-consumed-${e.grantId}`,16).toString("utf8")!=="consumed")&&s(),(!Array.isArray(e.backend)||e.backend.length<2||e.backend.length>b.maxRequests)&&s();for(const r of e.backend)(!h(r,["path","method","status","responseSha256"])||!b.routes.some(f=>f.path===r.path&&f.method===r.method)||!Number.isSafeInteger(r.status)||Number(r.status)<200||Number(r.status)>599||Number(r.status)>=300&&Number(r.status)<400||typeof r.responseSha256!="string"||!p.test(r.responseSha256))&&s();Number(e.allowedActionRequests)+Number(e.initialRequests)!==e.backend.length&&s();const k=new Map,O=new Set;for(const r of e.observations){(!h(r,["kind","artifactId","sha256","captureMethod","capturedAt","rationale"])||!x.includes(r.kind)||k.has(r.kind)||typeof r.artifactId!="string"||!/^[a-z][a-z0-9-]{0,63}\.(?:png|json)$/.test(r.artifactId)||O.has(r.artifactId)||r.captureMethod!==T[r.kind]||typeof r.sha256!="string"||!p.test(r.sha256)||r.capturedAt!==e.capturedAt||typeof r.rationale!="string"||r.rationale.length<30||r.rationale.length>500||/[\x00-\x1f\x7f]/.test(r.rationale))&&s();const f=g(t,r.artifactId,8*1024*1024);(m(f)!==r.sha256||r.captureMethod==="browser-screenshot"!==r.artifactId.endsWith(".png"))&&s(),O.add(r.artifactId),k.set(r.kind,f)}const S=y(k.get("interaction"));(!h(S,["schemaVersion","syntheticTarget","allowedActionRequests","deniedRequests","generation","scenarioSha256","checks","backend"])||S.schemaVersion!==3||S.syntheticTarget!==!0||S.allowedActionRequests!==e.allowedActionRequests||S.deniedRequests!==0||S.generation!==e.generation||S.scenarioSha256!==e.scenarioSha256||JSON.stringify(S.checks)!==JSON.stringify(e.checks)||JSON.stringify(S.backend)!==JSON.stringify(e.backend))&&s(),j(t,i,o).planSha256!==a.planSha256&&s();const V=x.filter(r=>r!=="interaction"&&!M(r,k.get(r)));return{receipt:e,observationSha256:n,rejectedKinds:V}}catch{return s()}}function N(t,i,o,n){(!h(n,["schemaVersion","kind","observationSha256","verdict","issues"])||n.schemaVersion!==3||n.kind!=="root-qa-synthetic-backend-report"||typeof n.observationSha256!="string"||!["approved","issues-found"].includes(String(n.verdict))||!Array.isArray(n.issues)||n.issues.length>128)&&s();const a=W(t,i,o,n.observationSha256);(n.verdict==="approved"&&(n.issues.length||a.rejectedKinds.length)||n.verdict==="issues-found"&&!n.issues.length)&&s();const d=new Set(a.receipt.observations.map(c=>c.artifactId));for(const c of n.issues)(!h(c,["severity","evidenceArtifactId","summary"])||!["critical","high","medium","low"].includes(String(c.severity))||!d.has(String(c.evidenceArtifactId))||typeof c.summary!="string"||c.summary.length<20||c.summary.length>500||/[\x00-\x1f\x7f]/.test(c.summary))&&s();const e=n;return{report:e,receipt:{schemaVersion:3,kind:"root-qa-synthetic-backend-review",sourceHead:a.receipt.sourceHead,planSha256:a.receipt.planSha256,observationSha256:a.observationSha256,reviewSha256:m(JSON.stringify(e)),verdict:e.verdict,evidenceCount:8,issueCount:e.issues.length,qaAccepted:e.verdict==="approved",checkpointAccepted:!1,workflowContinuationAuthorized:!1}}}function ee(t){try{t.authorizeRootReview!==!0&&s();const i=N(t.projectRoot,t.surface,t.targetUrl,t.report),o=JSON.stringify(i);A(t.projectRoot,`backend-review-${i.receipt.reviewSha256}.json`,o),JSON.stringify(N(t.projectRoot,t.surface,t.targetUrl,i.report))!==o&&s();const n=JSON.stringify({observationSha256:i.receipt.observationSha256,reviewSha256:i.receipt.reviewSha256});return A(t.projectRoot,`backend-reviewed-${i.receipt.observationSha256}.json`,n),i.receipt}catch{return s()}}function te(t,i,o,n){try{p.test(n)||s();const a=y(g(t,`backend-reviewed-${n}.json`,4096));(!h(a,["observationSha256","reviewSha256"])||a.observationSha256!==n||typeof a.reviewSha256!="string")&&s();const d=D(t,i,o,a.reviewSha256);return d.receipt.observationSha256!==n&&s(),d}catch{return s()}}function D(t,i,o,n){try{p.test(n)||s();const a=y(g(t,`backend-review-${n}.json`,131072));h(a,["report","receipt"])||s();const d=N(t,i,o,a.report);return(d.receipt.reviewSha256!==n||JSON.stringify(d.receipt)!==JSON.stringify(a.receipt))&&s(),d}catch{return s()}}function H(t,i,o){(!Array.isArray(o)||!o.length||o.length>32)&&s();const n=E(t,i),a=F(t,i),d=new Set,e=o.map(c=>{(!h(c,["surface","targetUrl","observationSha256","reviewSha256"])||typeof c.surface!="string"||d.has(c.surface)||typeof c.targetUrl!="string"||typeof c.observationSha256!="string"||typeof c.reviewSha256!="string")&&s(),d.add(c.surface);const u=D(t,c.surface,c.targetUrl,c.reviewSha256);return(u.receipt.sourceHead!==n.sourceHead||u.receipt.observationSha256!==c.observationSha256)&&s(),u.receipt});return JSON.stringify(o.map(c=>c.targetUrl).sort((c,u)=>Buffer.from(c).compare(Buffer.from(u))))!==JSON.stringify(a)&&s(),{schemaVersion:3,kind:"root-qa-synthetic-backend-discovered-phase",sourceHead:n.sourceHead,discoverySha256:i,aggregateSha256:m(JSON.stringify({discoverySha256:i,targets:o,receipts:e})),surfaceCount:o.length,qaAccepted:e.every(c=>c.qaAccepted),issueCount:e.reduce((c,u)=>c+u.issueCount,0),workflowContinuationAuthorized:!1,checkpointAccepted:!1}}function re(t,i,o){try{const n=H(t,i,o),a=JSON.stringify(n),d=`backend-phase-${n.aggregateSha256}.json`;return A(t,d,a),Q(t,i,o)}catch{return s()}}function Q(t,i,o){try{const n=H(t,i,o),a=y(g(t,`backend-phase-${n.aggregateSha256}.json`,4096));return JSON.stringify(a)!==JSON.stringify(n)&&s(),n}catch{return s()}}export{re as acceptSyntheticBackendDiscoveredPhase,ee as acceptSyntheticBackendRootReview,Q as readSyntheticBackendDiscoveredPhase,W as readSyntheticBackendObservation,te as readSyntheticBackendReviewedObservation,D as readSyntheticBackendRootReview};
|
|
1
|
+
/** Separate root review of confined backend evidence. No legacy schema coercion,
|
|
2
|
+
* workflow checkpoint, caller artifact path or sandbox effect is accepted here. */
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { closeSync, constants, fstatSync, fsyncSync, openSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { planRootQaScan, rootQaScanEvidenceDirectory, validateRootQaScanArtifact } from './qa-scan-review.js';
|
|
7
|
+
import { validateRootQaScanScenario, scenarioRequirementDigest } from './qa-scan-capture.js';
|
|
8
|
+
import { validateSyntheticBackendContract } from './qa-synthetic-backend.js';
|
|
9
|
+
import { readRootQaScanDiscovery, readRootQaScanDiscoveryTargets } from './qa-scan-discovery.js';
|
|
10
|
+
const HASH = /^[a-f0-9]{64}$/;
|
|
11
|
+
const KINDS = ['visual', 'responsive', 'responsive-tablet', 'responsive-desktop', 'accessibility', 'interaction', 'console-network', 'performance'];
|
|
12
|
+
const METHODS = { visual: 'browser-screenshot', responsive: 'browser-screenshot', 'responsive-tablet': 'browser-screenshot',
|
|
13
|
+
'responsive-desktop': 'browser-screenshot', accessibility: 'axe-scan', interaction: 'browser-interaction',
|
|
14
|
+
'console-network': 'console-capture', performance: 'performance-trace' };
|
|
15
|
+
const sha = (v) => createHash('sha256').update(v).digest('hex');
|
|
16
|
+
function fail() { throw new Error('qa-synthetic-backend-review-unverified'); }
|
|
17
|
+
function exact(v, keys) {
|
|
18
|
+
return !!v && typeof v === 'object' && !Array.isArray(v) && Object.keys(v).sort().join(',') === [...keys].sort().join(',');
|
|
19
|
+
}
|
|
20
|
+
function read(root, name, limit) {
|
|
21
|
+
const fd = openSync(join(rootQaScanEvidenceDirectory(root), name), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
22
|
+
try {
|
|
23
|
+
const before = fstatSync(fd);
|
|
24
|
+
if (!before.isFile() || before.nlink !== 1 || before.uid !== process.getuid?.() || before.mode & 0o077
|
|
25
|
+
|| before.size < 1 || before.size > limit)
|
|
26
|
+
fail();
|
|
27
|
+
const bytes = readFileSync(fd);
|
|
28
|
+
const after = fstatSync(fd);
|
|
29
|
+
if (bytes.length !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs)
|
|
30
|
+
fail();
|
|
31
|
+
return bytes;
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
closeSync(fd);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function parse(bytes) {
|
|
38
|
+
const raw = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
39
|
+
const value = JSON.parse(raw);
|
|
40
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || JSON.stringify(value) !== raw)
|
|
41
|
+
fail();
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
function save(root, name, bytes) {
|
|
45
|
+
const dir = rootQaScanEvidenceDirectory(root);
|
|
46
|
+
let fd;
|
|
47
|
+
try {
|
|
48
|
+
fd = openSync(join(dir, name), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code !== 'EEXIST' || read(root, name, 131072).toString('utf8') !== bytes)
|
|
52
|
+
fail();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
writeFileSync(fd, bytes);
|
|
57
|
+
fsyncSync(fd);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
closeSync(fd);
|
|
61
|
+
}
|
|
62
|
+
const parent = openSync(dir, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
63
|
+
try {
|
|
64
|
+
fsyncSync(parent);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
closeSync(parent);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Source and all artifacts are recomputed; an arbitrary JSON/200 response is no proof. */
|
|
71
|
+
export function readSyntheticBackendObservation(root, surface, targetUrl, digest) {
|
|
72
|
+
try {
|
|
73
|
+
if (!HASH.test(digest))
|
|
74
|
+
fail();
|
|
75
|
+
const plan = planRootQaScan(root, surface, targetUrl);
|
|
76
|
+
const bytes = read(root, `synthetic-backend-${digest}.json`, 131072);
|
|
77
|
+
if (sha(bytes) !== digest)
|
|
78
|
+
fail();
|
|
79
|
+
const v = parse(bytes);
|
|
80
|
+
if (!exact(v, ['schemaVersion', 'kind', 'sourceHead', 'planSha256', 'surface', 'targetSha256', 'generation',
|
|
81
|
+
'scenarioSha256', 'backendContractSha256', 'requirementSourceSha256', 'requirementIds', 'capturedAt', 'checks', 'scenario',
|
|
82
|
+
'grantId', 'evidenceCount', 'observations', 'initialRequests', 'allowedActionRequests', 'deniedRequests', 'backend', 'sandboxNetwork',
|
|
83
|
+
'browserNetwork', 'browserHostWorkspaceMounted', 'browserVersion', 'qaAccepted', 'checkpointAccepted', 'workflowContinuationAuthorized',
|
|
84
|
+
...('producerJob' in v ? ['producerJob'] : [])])
|
|
85
|
+
|| v.schemaVersion !== 3 || v.kind !== 'root-qa-synthetic-backend-observation'
|
|
86
|
+
|| v.sourceHead !== plan.sourceHead || v.planSha256 !== plan.planSha256 || v.surface !== surface || v.targetSha256 !== plan.targetSha256
|
|
87
|
+
|| typeof v.generation !== 'string' || !HASH.test(v.generation) || typeof v.scenarioSha256 !== 'string' || !HASH.test(v.scenarioSha256)
|
|
88
|
+
|| typeof v.grantId !== 'string' || !/^[a-f0-9]{8}-[a-f0-9-]{27}$/.test(v.grantId)
|
|
89
|
+
|| typeof v.capturedAt !== 'string' || !Number.isFinite(Date.parse(v.capturedAt))
|
|
90
|
+
|| new Date(v.capturedAt).toISOString() !== v.capturedAt || Date.parse(v.capturedAt) > Date.now()
|
|
91
|
+
|| Date.now() - Date.parse(v.capturedAt) > 3600000 || v.evidenceCount !== 8
|
|
92
|
+
|| !Array.isArray(v.observations) || v.observations.length !== 8 || v.sandboxNetwork !== 'none'
|
|
93
|
+
|| !['unshared', 'docker-none'].includes(String(v.browserNetwork)) || v.browserHostWorkspaceMounted !== false
|
|
94
|
+
|| typeof v.browserVersion !== 'string' || !/^[0-9]+(?:\.[0-9]+){2,3}$/.test(v.browserVersion)
|
|
95
|
+
|| !Number.isSafeInteger(v.allowedActionRequests) || Number(v.allowedActionRequests) < 1 || Number(v.allowedActionRequests) > 64
|
|
96
|
+
|| !Number.isSafeInteger(v.initialRequests) || Number(v.initialRequests) < 1 || Number(v.initialRequests) > 64
|
|
97
|
+
|| v.deniedRequests !== 0 || v.qaAccepted !== false || v.checkpointAccepted !== false || v.workflowContinuationAuthorized !== false)
|
|
98
|
+
fail();
|
|
99
|
+
if (v.browserNetwork === 'docker-none') {
|
|
100
|
+
const job = v.producerJob;
|
|
101
|
+
if (!exact(job, ['imageId', 'containerId', 'jobBindingSha256']) || typeof job.imageId !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(job.imageId)
|
|
102
|
+
|| typeof job.containerId !== 'string' || !HASH.test(job.containerId) || typeof job.jobBindingSha256 !== 'string' || !HASH.test(job.jobBindingSha256))
|
|
103
|
+
fail();
|
|
104
|
+
const attestation = parse(read(root, `producer-job-${job.jobBindingSha256}-${job.containerId}.json`, 4096));
|
|
105
|
+
if (!exact(attestation, ['schemaVersion', 'kind', 'containerId', 'imageId', 'jobBindingSha256', 'sourceHead', 'requestSha256',
|
|
106
|
+
'phase', 'bindingSha256', 'network', 'producerDockerSocketMounted', 'createdVerified', 'completedVerified'])
|
|
107
|
+
|| attestation.schemaVersion !== 1 || attestation.kind !== 'root-synthetic-producer-job-attestation'
|
|
108
|
+
|| attestation.containerId !== job.containerId || attestation.imageId !== job.imageId || attestation.jobBindingSha256 !== job.jobBindingSha256
|
|
109
|
+
|| attestation.sourceHead !== plan.sourceHead || typeof attestation.requestSha256 !== 'string' || !HASH.test(attestation.requestSha256)
|
|
110
|
+
|| typeof attestation.bindingSha256 !== 'string' || !HASH.test(attestation.bindingSha256) || attestation.phase !== 'completed'
|
|
111
|
+
|| attestation.network !== 'none' || attestation.producerDockerSocketMounted !== false
|
|
112
|
+
|| attestation.createdVerified !== true || attestation.completedVerified !== true)
|
|
113
|
+
fail();
|
|
114
|
+
}
|
|
115
|
+
else if ('producerJob' in v)
|
|
116
|
+
fail();
|
|
117
|
+
const scenario = validateRootQaScanScenario(v.scenario);
|
|
118
|
+
if (sha(JSON.stringify(scenario)) !== v.scenarioSha256
|
|
119
|
+
|| scenarioRequirementDigest(root, scenario) !== v.requirementSourceSha256
|
|
120
|
+
|| JSON.stringify(scenario.requirementIds) !== JSON.stringify(v.requirementIds)
|
|
121
|
+
|| JSON.stringify(v.checks) !== JSON.stringify(scenario.checks.map(c => ({ id: `functional-${c.id}`, passed: true }))))
|
|
122
|
+
fail();
|
|
123
|
+
const grant = parse(read(root, `backend-grant-${v.grantId}.json`, 32768));
|
|
124
|
+
if (!exact(grant, ['schemaVersion', 'contract', 'planSha256', 'sourceHead', 'surface', 'targetUrl', 'expiresAt'])
|
|
125
|
+
|| grant.schemaVersion !== 1 || grant.planSha256 !== plan.planSha256 || grant.sourceHead !== plan.sourceHead
|
|
126
|
+
|| grant.surface !== surface || grant.targetUrl !== targetUrl || typeof grant.expiresAt !== 'string'
|
|
127
|
+
|| !Number.isFinite(Date.parse(grant.expiresAt)))
|
|
128
|
+
fail();
|
|
129
|
+
const contract = validateSyntheticBackendContract(grant.contract);
|
|
130
|
+
if (contract.generation !== v.generation || contract.scenarioSha256 !== v.scenarioSha256
|
|
131
|
+
|| sha(JSON.stringify(contract)) !== v.backendContractSha256 || Date.parse(v.capturedAt) > Date.parse(grant.expiresAt)
|
|
132
|
+
|| Date.parse(v.capturedAt) < Date.parse(grant.expiresAt) - contract.durationMs
|
|
133
|
+
|| read(root, `backend-consumed-${v.grantId}`, 16).toString('utf8') !== 'consumed')
|
|
134
|
+
fail();
|
|
135
|
+
if (!Array.isArray(v.backend) || v.backend.length < 2 || v.backend.length > contract.maxRequests)
|
|
136
|
+
fail();
|
|
137
|
+
for (const row of v.backend) {
|
|
138
|
+
if (!exact(row, ['path', 'method', 'status', 'responseSha256']) || !contract.routes.some(r => r.path === row.path && r.method === row.method)
|
|
139
|
+
|| !Number.isSafeInteger(row.status) || Number(row.status) < 200 || Number(row.status) > 599
|
|
140
|
+
|| Number(row.status) >= 300 && Number(row.status) < 400 || typeof row.responseSha256 !== 'string' || !HASH.test(row.responseSha256))
|
|
141
|
+
fail();
|
|
142
|
+
}
|
|
143
|
+
if (Number(v.allowedActionRequests) + Number(v.initialRequests) !== v.backend.length)
|
|
144
|
+
fail();
|
|
145
|
+
const artifacts = new Map();
|
|
146
|
+
const artifactIds = new Set();
|
|
147
|
+
for (const row of v.observations) {
|
|
148
|
+
if (!exact(row, ['kind', 'artifactId', 'sha256', 'captureMethod', 'capturedAt', 'rationale'])
|
|
149
|
+
|| !KINDS.includes(row.kind) || artifacts.has(row.kind)
|
|
150
|
+
|| typeof row.artifactId !== 'string' || !/^[a-z][a-z0-9-]{0,63}\.(?:png|json)$/.test(row.artifactId)
|
|
151
|
+
|| artifactIds.has(row.artifactId) || row.captureMethod !== METHODS[row.kind]
|
|
152
|
+
|| typeof row.sha256 !== 'string' || !HASH.test(row.sha256) || row.capturedAt !== v.capturedAt
|
|
153
|
+
|| typeof row.rationale !== 'string' || row.rationale.length < 30 || row.rationale.length > 500
|
|
154
|
+
|| /[\x00-\x1f\x7f]/.test(row.rationale))
|
|
155
|
+
fail();
|
|
156
|
+
const data = read(root, row.artifactId, 8 * 1024 * 1024);
|
|
157
|
+
if (sha(data) !== row.sha256 || (row.captureMethod === 'browser-screenshot') !== row.artifactId.endsWith('.png'))
|
|
158
|
+
fail();
|
|
159
|
+
artifactIds.add(row.artifactId);
|
|
160
|
+
artifacts.set(row.kind, data);
|
|
161
|
+
}
|
|
162
|
+
const interaction = parse(artifacts.get('interaction'));
|
|
163
|
+
if (!exact(interaction, ['schemaVersion', 'syntheticTarget', 'allowedActionRequests', 'deniedRequests', 'generation', 'scenarioSha256', 'checks', 'backend'])
|
|
164
|
+
|| interaction.schemaVersion !== 3 || interaction.syntheticTarget !== true
|
|
165
|
+
|| interaction.allowedActionRequests !== v.allowedActionRequests || interaction.deniedRequests !== 0
|
|
166
|
+
|| interaction.generation !== v.generation || interaction.scenarioSha256 !== v.scenarioSha256
|
|
167
|
+
|| JSON.stringify(interaction.checks) !== JSON.stringify(v.checks) || JSON.stringify(interaction.backend) !== JSON.stringify(v.backend))
|
|
168
|
+
fail();
|
|
169
|
+
if (planRootQaScan(root, surface, targetUrl).planSha256 !== plan.planSha256)
|
|
170
|
+
fail();
|
|
171
|
+
const rejectedKinds = KINDS.filter(kind => kind !== 'interaction' && !validateRootQaScanArtifact(kind, artifacts.get(kind)));
|
|
172
|
+
return { receipt: v, observationSha256: digest, rejectedKinds };
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return fail();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function validateReport(root, surface, targetUrl, input) {
|
|
179
|
+
if (!exact(input, ['schemaVersion', 'kind', 'observationSha256', 'verdict', 'issues']) || input.schemaVersion !== 3
|
|
180
|
+
|| input.kind !== 'root-qa-synthetic-backend-report' || typeof input.observationSha256 !== 'string'
|
|
181
|
+
|| !['approved', 'issues-found'].includes(String(input.verdict)) || !Array.isArray(input.issues) || input.issues.length > 128)
|
|
182
|
+
fail();
|
|
183
|
+
const checked = readSyntheticBackendObservation(root, surface, targetUrl, input.observationSha256);
|
|
184
|
+
if (input.verdict === 'approved' && (input.issues.length || checked.rejectedKinds.length)
|
|
185
|
+
|| input.verdict === 'issues-found' && !input.issues.length)
|
|
186
|
+
fail();
|
|
187
|
+
const ids = new Set(checked.receipt.observations.map(row => row.artifactId));
|
|
188
|
+
for (const issue of input.issues)
|
|
189
|
+
if (!exact(issue, ['severity', 'evidenceArtifactId', 'summary'])
|
|
190
|
+
|| !['critical', 'high', 'medium', 'low'].includes(String(issue.severity)) || !ids.has(String(issue.evidenceArtifactId))
|
|
191
|
+
|| typeof issue.summary !== 'string' || issue.summary.length < 20 || issue.summary.length > 500 || /[\x00-\x1f\x7f]/.test(issue.summary))
|
|
192
|
+
fail();
|
|
193
|
+
const report = input;
|
|
194
|
+
return { report, receipt: { schemaVersion: 3, kind: 'root-qa-synthetic-backend-review',
|
|
195
|
+
sourceHead: checked.receipt.sourceHead, planSha256: checked.receipt.planSha256,
|
|
196
|
+
observationSha256: checked.observationSha256, reviewSha256: sha(JSON.stringify(report)), verdict: report.verdict,
|
|
197
|
+
evidenceCount: 8, issueCount: report.issues.length, qaAccepted: report.verdict === 'approved',
|
|
198
|
+
checkpointAccepted: false, workflowContinuationAuthorized: false } };
|
|
199
|
+
}
|
|
200
|
+
export function acceptSyntheticBackendRootReview(options) {
|
|
201
|
+
try {
|
|
202
|
+
if (options.authorizeRootReview !== true)
|
|
203
|
+
fail();
|
|
204
|
+
const checked = validateReport(options.projectRoot, options.surface, options.targetUrl, options.report);
|
|
205
|
+
const bytes = JSON.stringify(checked);
|
|
206
|
+
save(options.projectRoot, `backend-review-${checked.receipt.reviewSha256}.json`, bytes);
|
|
207
|
+
// Recompute after persistence; a raced source or artifact grants no accepted result.
|
|
208
|
+
if (JSON.stringify(validateReport(options.projectRoot, options.surface, options.targetUrl, checked.report)) !== bytes)
|
|
209
|
+
fail();
|
|
210
|
+
const pointerBytes = JSON.stringify({ observationSha256: checked.receipt.observationSha256, reviewSha256: checked.receipt.reviewSha256 });
|
|
211
|
+
save(options.projectRoot, `backend-reviewed-${checked.receipt.observationSha256}.json`, pointerBytes);
|
|
212
|
+
return checked.receipt;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return fail();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
export function readSyntheticBackendReviewedObservation(root, surface, targetUrl, observationSha256) {
|
|
219
|
+
try {
|
|
220
|
+
if (!HASH.test(observationSha256))
|
|
221
|
+
fail();
|
|
222
|
+
const pointer = parse(read(root, `backend-reviewed-${observationSha256}.json`, 4096));
|
|
223
|
+
if (!exact(pointer, ['observationSha256', 'reviewSha256']) || pointer.observationSha256 !== observationSha256 || typeof pointer.reviewSha256 !== 'string')
|
|
224
|
+
fail();
|
|
225
|
+
const checked = readSyntheticBackendRootReview(root, surface, targetUrl, pointer.reviewSha256);
|
|
226
|
+
if (checked.receipt.observationSha256 !== observationSha256)
|
|
227
|
+
fail();
|
|
228
|
+
return checked;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return fail();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
export function readSyntheticBackendRootReview(root, surface, targetUrl, digest) {
|
|
235
|
+
try {
|
|
236
|
+
if (!HASH.test(digest))
|
|
237
|
+
fail();
|
|
238
|
+
const stored = parse(read(root, `backend-review-${digest}.json`, 131072));
|
|
239
|
+
if (!exact(stored, ['report', 'receipt']))
|
|
240
|
+
fail();
|
|
241
|
+
const checked = validateReport(root, surface, targetUrl, stored.report);
|
|
242
|
+
if (checked.receipt.reviewSha256 !== digest || JSON.stringify(checked.receipt) !== JSON.stringify(stored.receipt))
|
|
243
|
+
fail();
|
|
244
|
+
return checked;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return fail();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function checkedPhase(root, discoverySha256, targets) {
|
|
251
|
+
if (!Array.isArray(targets) || !targets.length || targets.length > 32)
|
|
252
|
+
fail();
|
|
253
|
+
const discovery = readRootQaScanDiscovery(root, discoverySha256);
|
|
254
|
+
const urls = readRootQaScanDiscoveryTargets(root, discoverySha256);
|
|
255
|
+
const surfaces = new Set();
|
|
256
|
+
const receipts = targets.map(target => {
|
|
257
|
+
if (!exact(target, ['surface', 'targetUrl', 'observationSha256', 'reviewSha256'])
|
|
258
|
+
|| typeof target.surface !== 'string' || surfaces.has(target.surface) || typeof target.targetUrl !== 'string'
|
|
259
|
+
|| typeof target.observationSha256 !== 'string' || typeof target.reviewSha256 !== 'string')
|
|
260
|
+
fail();
|
|
261
|
+
surfaces.add(target.surface);
|
|
262
|
+
const checked = readSyntheticBackendRootReview(root, target.surface, target.targetUrl, target.reviewSha256);
|
|
263
|
+
if (checked.receipt.sourceHead !== discovery.sourceHead || checked.receipt.observationSha256 !== target.observationSha256)
|
|
264
|
+
fail();
|
|
265
|
+
return checked.receipt;
|
|
266
|
+
});
|
|
267
|
+
if (JSON.stringify(targets.map(t => t.targetUrl).sort((a, b) => Buffer.from(a).compare(Buffer.from(b)))) !== JSON.stringify(urls))
|
|
268
|
+
fail();
|
|
269
|
+
return { schemaVersion: 3, kind: 'root-qa-synthetic-backend-discovered-phase', sourceHead: discovery.sourceHead,
|
|
270
|
+
discoverySha256, aggregateSha256: sha(JSON.stringify({ discoverySha256, targets, receipts })), surfaceCount: targets.length,
|
|
271
|
+
qaAccepted: receipts.every(r => r.qaAccepted), issueCount: receipts.reduce((sum, r) => sum + r.issueCount, 0),
|
|
272
|
+
workflowContinuationAuthorized: false, checkpointAccepted: false };
|
|
273
|
+
}
|
|
274
|
+
export function acceptSyntheticBackendDiscoveredPhase(root, discoverySha256, targets) {
|
|
275
|
+
try {
|
|
276
|
+
const receipt = checkedPhase(root, discoverySha256, targets);
|
|
277
|
+
const bytes = JSON.stringify(receipt);
|
|
278
|
+
const name = `backend-phase-${receipt.aggregateSha256}.json`;
|
|
279
|
+
save(root, name, bytes);
|
|
280
|
+
return readSyntheticBackendDiscoveredPhase(root, discoverySha256, targets);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return fail();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
export function readSyntheticBackendDiscoveredPhase(root, discoverySha256, targets) {
|
|
287
|
+
try {
|
|
288
|
+
const expected = checkedPhase(root, discoverySha256, targets);
|
|
289
|
+
const stored = parse(read(root, `backend-phase-${expected.aggregateSha256}.json`, 4096));
|
|
290
|
+
if (JSON.stringify(stored) !== JSON.stringify(expected))
|
|
291
|
+
fail();
|
|
292
|
+
return expected;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return fail();
|
|
296
|
+
}
|
|
297
|
+
}
|