@awak-app/simy-cli 0.5.71 → 0.5.73
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/README.md +101 -3
- package/package.json +2 -2
- package/src/agent.js +205 -12
- package/src/backend-executable.js +45 -2
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/README.md +22 -0
- package/src/codex-plugins/simy/hooks/continuation-policy.mjs +27 -0
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +180 -31
- package/src/codex-plugins/simy/hooks/outcome-progress.mjs +129 -0
- package/src/coding-run-recovery.js +88 -0
- package/src/coding-run-store.js +2 -2
- package/src/continuation-policy.js +2 -0
- package/src/delivery-hooks/command.js +11 -5
- package/src/delivery-hooks/desktop.js +12 -3
- package/src/delivery-hooks/supervisor.js +17 -12
- package/src/desktop-host.js +19 -8
- package/src/desktop-sqm-settings.js +24 -7
- package/src/device-signing-identity.js +65 -0
- package/src/diagnostic-log.js +68 -0
- package/src/doctor.js +55 -0
- package/src/index.js +18 -1
- package/src/local-task.js +68 -3
- package/src/orchestrator/budget.js +55 -4
- package/src/orchestrator/charter-validation.js +33 -0
- package/src/orchestrator/completion-contract.js +1 -1
- package/src/orchestrator/contract.js +2 -0
- package/src/orchestrator/delivery/references/state-and-receipts.md +21 -3
- package/src/orchestrator/evidence.js +20 -19
- package/src/orchestrator/independent-audit.js +9 -0
- package/src/orchestrator/instruction.js +6 -1
- package/src/orchestrator/loop.js +66 -41
- package/src/orchestrator/outcome-monitor.js +95 -44
- package/src/orchestrator/outcome-progress.js +2 -0
- package/src/orchestrator/recovery.js +3 -0
- package/src/purpose-local-proof.js +380 -0
- package/src/runner.js +227 -19
- package/src/runtime-diagnostics.js +77 -0
- package/src/session-supervision/codex-app-server.js +3 -2
- package/src/session-supervision/index.js +16 -4
- package/src/sqm/bundle-store.js +14 -7
- package/src/sqm/claude-plugin.js +2 -0
- package/src/sqm/command.js +42 -4
- package/src/sqm/diagnostics.js +23 -0
- package/src/sqm/evidence-client.js +24 -7
- package/src/sqm/history-store.js +21 -5
- package/src/sqm/index.js +116 -17
- package/src/sqm/proof.js +4 -23
- package/src/sqm/report.js +9 -2
- package/src/sqm/request-recovery.js +82 -0
- package/src/sqm/review-worker.js +97 -15
- package/src/sqm/session.js +33 -3
- package/src/startup-preflight.js +44 -0
package/src/sqm/claude-plugin.js
CHANGED
|
@@ -21,6 +21,8 @@ export async function claudePluginFiles() {
|
|
|
21
21
|
files.set(relative, content.replaceAll("current Codex thread", "current Claude Code conversation"));
|
|
22
22
|
}
|
|
23
23
|
files.set("hooks/delivery-loop.mjs", await readFile(path.join(SOURCE, "hooks/delivery-loop.mjs"), "utf8"));
|
|
24
|
+
files.set("hooks/outcome-progress.mjs", await readFile(path.join(SOURCE, "hooks/outcome-progress.mjs"), "utf8"));
|
|
25
|
+
files.set("hooks/continuation-policy.mjs", await readFile(path.join(SOURCE, "hooks/continuation-policy.mjs"), "utf8"));
|
|
24
26
|
files.set("hooks/action-association.mjs", await readFile(path.join(SOURCE, "hooks/action-association.mjs"), "utf8"));
|
|
25
27
|
files.set("hooks/workspace-lease.mjs", await readFile(path.join(SOURCE, "hooks/workspace-lease.mjs"), "utf8"));
|
|
26
28
|
files.set("hooks/connection-reminder.mjs", await readFile(path.join(SOURCE, "hooks/connection-reminder.mjs"), "utf8"));
|
package/src/sqm/command.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { safeSqmDiagnostic } from "./diagnostics.js";
|
|
1
2
|
import { createSqmCheckRecorder } from "./history-store.js";
|
|
2
3
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
@@ -10,8 +11,9 @@ import { uploadRunEvidence } from "./evidence-client.js";
|
|
|
10
11
|
import { evaluateKnowledgeBundle } from "./checkers.js";
|
|
11
12
|
import { runSqmCheck, runSqmMinCheck } from "./index.js";
|
|
12
13
|
import { lintKnowledgeModule, summarizeReplay } from "./reliability.js";
|
|
13
|
-
import { ensureSqmSession } from "./session.js";
|
|
14
|
+
import { ensureSqmSession, sqmSessionScope } from "./session.js";
|
|
14
15
|
import { formatSqmCoverage } from "./report.js";
|
|
16
|
+
import { sqmDiagnostic } from "./request-recovery.js";
|
|
15
17
|
|
|
16
18
|
export async function runSqmCommand(argv, { cliVersion }) {
|
|
17
19
|
if (argv.includes("--help") || argv.includes("-h") || !argv.length) {
|
|
@@ -23,14 +25,17 @@ export async function runSqmCommand(argv, { cliVersion }) {
|
|
|
23
25
|
if (argv[0] === "replay") return await runReplayCommand(argv);
|
|
24
26
|
if (!["min-check", "check"].includes(argv[0])) throw new Error("Usage: simy sqm <min-check|check|lint|replay> [options]");
|
|
25
27
|
const webOrigin = resolveWebOrigin(option(argv, "--host"));
|
|
28
|
+
const authorizationLogger = argv.includes("--json")
|
|
29
|
+
? { log: message => console.warn(message), error: message => console.error(message) }
|
|
30
|
+
: console;
|
|
26
31
|
let session = await readSession(webOrigin);
|
|
27
32
|
let validSession = isSessionValid(session, Date.now(), webOrigin) ? session : null;
|
|
28
33
|
if (!validSession && !explicitSqmEnvironment(argv)) {
|
|
29
|
-
session = await ensureSqmSession(webOrigin, { noOpen: argv.includes("--no-open") });
|
|
34
|
+
session = await ensureSqmSession(webOrigin, { noOpen: argv.includes("--no-open"), logger: authorizationLogger });
|
|
30
35
|
validSession = session;
|
|
31
36
|
}
|
|
32
37
|
const organizationId = process.env.SIMY_SQM_ORGANIZATION_ID || validSession?.organization_id || validSession?.org_id;
|
|
33
|
-
|
|
38
|
+
let token = process.env.SIMY_SQM_TOKEN || process.env.SIMY_ACCESS_TOKEN || validSession?.token;
|
|
34
39
|
const accountId = process.env.SIMY_SQM_ACCOUNT_ID || validSession?.account_id || validSession?.auth_user_id || validSession?.device_id;
|
|
35
40
|
const deviceId = process.env.SIMY_SQM_DEVICE_ID || validSession?.device_id;
|
|
36
41
|
if (!organizationId) throw new Error(`SQM needs an organization-bound SIMY session. Run \`simy --host ${webOrigin}\` once, then retry.`);
|
|
@@ -45,10 +50,33 @@ export async function runSqmCommand(argv, { cliVersion }) {
|
|
|
45
50
|
endpoint,
|
|
46
51
|
accountId,
|
|
47
52
|
organizationId,
|
|
53
|
+
deviceId,
|
|
48
54
|
};
|
|
49
55
|
|
|
50
56
|
const save = createSqmCheckRecorder({ scope: { webOrigin, accountId, organizationId },
|
|
51
57
|
phase: argv[0] === "min-check" ? "min" : "full", cwd: common.cwd, baseBranch: common.baseBranch });
|
|
58
|
+
const onRecoveryEvent = async diagnostic => createSqmCheckRecorder({ scope: { webOrigin, accountId, organizationId },
|
|
59
|
+
phase: argv[0] === "min-check" ? "min" : "full", cwd: common.cwd, baseBranch: common.baseBranch })({ status: "failed", diagnostic });
|
|
60
|
+
common.bundleDependencies = { onRecoveryEvent };
|
|
61
|
+
common.onRecoveryEvent = onRecoveryEvent;
|
|
62
|
+
let authenticationAttempted = false;
|
|
63
|
+
if (validSession && !process.env.SIMY_SQM_TOKEN && !process.env.SIMY_ACCESS_TOKEN) {
|
|
64
|
+
const expectedScope = sqmSessionScope(validSession, webOrigin);
|
|
65
|
+
common.onAuthenticationRequired = async ({ rejectedToken }) => {
|
|
66
|
+
if (authenticationAttempted) {
|
|
67
|
+
const error = new Error("SQM authorization was already refreshed once; resolve the sign-in error before another check.");
|
|
68
|
+
error.code = "authorization_refresh_failed"; throw error;
|
|
69
|
+
}
|
|
70
|
+
authenticationAttempted = true;
|
|
71
|
+
validSession = await ensureSqmSession(webOrigin, { noOpen: argv.includes("--no-open"), rejectedToken, expectedScope, logger: authorizationLogger });
|
|
72
|
+
token = validSession.token;
|
|
73
|
+
common.token = token;
|
|
74
|
+
return { token, accountId: validSession.account_id || validSession.auth_user_id || validSession.device_id,
|
|
75
|
+
organizationId: validSession.organization_id || validSession.org_id, deviceId: validSession.device_id,
|
|
76
|
+
endpoint: process.env.SIMY_SQM_BUNDLE_URL || new URL("sqm/knowledge-bundle", validSession.api_base_url).href };
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
let stage = "min_check";
|
|
52
80
|
try {
|
|
53
81
|
if (argv[0] === "min-check") {
|
|
54
82
|
const result = await runSqmMinCheck(common);
|
|
@@ -60,6 +88,7 @@ export async function runSqmCommand(argv, { cliVersion }) {
|
|
|
60
88
|
return result.status === "unavailable" ? 2 : result.status === "preliminary_failed" ? 1 : 0;
|
|
61
89
|
}
|
|
62
90
|
|
|
91
|
+
stage = "min_result_read";
|
|
63
92
|
const minPath = option(argv, "--min-result") || defaultMinResultPath();
|
|
64
93
|
let minCheck;
|
|
65
94
|
try {
|
|
@@ -71,11 +100,19 @@ export async function runSqmCommand(argv, { cliVersion }) {
|
|
|
71
100
|
await atomicJson(minPath, minCheck);
|
|
72
101
|
}
|
|
73
102
|
if (minCheck.status === "unavailable") { await save(minCheck); return printUnavailable(minCheck, argv); }
|
|
103
|
+
stage = "full_check";
|
|
74
104
|
const result = await runSqmCheck({ ...common, minCheck, deviceId, signingKeyPath: option(argv, "--signing-key") || undefined });
|
|
75
105
|
if (result.status === "unavailable") { await save(result); return printUnavailable(result, argv); }
|
|
106
|
+
if (result.recovery?.code === "knowledge_bundle_changed") {
|
|
107
|
+
// Keep the earlier min as history; never rewrite it into an apparent
|
|
108
|
+
// success for the new bundle.
|
|
109
|
+
await atomicJson(`${minPath}.previous.${minCheck.result_digest.slice(-16)}.json`, minCheck);
|
|
110
|
+
await atomicJson(minPath, result.min_check);
|
|
111
|
+
}
|
|
76
112
|
const proofPath = path.resolve(option(argv, "--proof") || "proof.json");
|
|
77
113
|
const evidencePath = path.resolve(option(argv, "--signed-evidence") || `${proofPath}.signed.json`);
|
|
78
114
|
const diagnosticsPath = path.resolve(option(argv, "--diagnostics") || `${proofPath}.diagnostics.json`);
|
|
115
|
+
stage = "artifact_write";
|
|
79
116
|
await Promise.all([
|
|
80
117
|
atomicJson(proofPath, result.proof),
|
|
81
118
|
atomicJson(evidencePath, result.signed_evidence),
|
|
@@ -87,6 +124,7 @@ export async function runSqmCommand(argv, { cliVersion }) {
|
|
|
87
124
|
let upload = { uploaded: false, reason: "disabled" };
|
|
88
125
|
if (!argv.includes("--no-upload")) {
|
|
89
126
|
const evidenceEndpoint = process.env.SIMY_SQM_EVIDENCE_URL || (validSession?.api_base_url ? new URL("sqm/run-evidence", validSession.api_base_url).href : undefined);
|
|
127
|
+
stage = "evidence_upload";
|
|
90
128
|
upload = await uploadRunEvidence({ endpoint: evidenceEndpoint, token, proof: result.proof, signedEvidence: result.signed_evidence });
|
|
91
129
|
}
|
|
92
130
|
result.upload = upload;
|
|
@@ -101,7 +139,7 @@ export async function runSqmCommand(argv, { cliVersion }) {
|
|
|
101
139
|
else printFull(result);
|
|
102
140
|
return result.proof.outcome.status === "failed" ? 1 : 0;
|
|
103
141
|
} catch (error) {
|
|
104
|
-
await save({ status: "failed" });
|
|
142
|
+
await save({ status: "failed", diagnostic: { ...safeSqmDiagnostic(error, stage), ...sqmDiagnostic(error, stage) } });
|
|
105
143
|
throw error;
|
|
106
144
|
}
|
|
107
145
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Persist machine diagnostics only. Provider/server response text can contain
|
|
2
|
+
// repository content or credentials and must never enter durable check history.
|
|
3
|
+
export function safeSqmDiagnostic(error, stage = "check", code = "sqm_check_failed") {
|
|
4
|
+
const value = error?.diagnostic || (error?.error && typeof error.error === "object" ? error.error : error) || {};
|
|
5
|
+
const machine = (candidate, fallback) => typeof candidate === "string" && /^[a-zA-Z][a-zA-Z0-9_]{0,79}$/.test(candidate) ? candidate : fallback;
|
|
6
|
+
const status = [value.status, value.http_status, value.errorStatus, value.httpStatus, value.error_status].find(candidate => Number.isInteger(candidate) && candidate >= 400 && candidate <= 599);
|
|
7
|
+
const requestId = value.requestId ?? value.request_id;
|
|
8
|
+
return {
|
|
9
|
+
stage: machine(value.stage ?? value.errorStage ?? value.error_stage, stage),
|
|
10
|
+
code: machine(value.code ?? value.errorCode ?? value.error_code, code),
|
|
11
|
+
...(Number.isInteger(status) && status >= 400 && status <= 599 ? { status } : {}),
|
|
12
|
+
...(typeof requestId === "string" && /^(?:[a-f0-9]{16,64}|[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$/i.test(requestId) ? { requestId } : {}),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatSqmDiagnostic(value) {
|
|
17
|
+
const hint = value.status === 401 ? "Reconnect your SIMY session."
|
|
18
|
+
: value.status === 403 ? "Check access to SQM in the selected organization."
|
|
19
|
+
: value.code === "repository_scope_unresolved" ? "Connect this repository to a system in SIMY."
|
|
20
|
+
: value.code === "knowledge_bundle_changed" || value.code === "repository_identity_changed" ? "Run min-check again for the current repository and knowledge version."
|
|
21
|
+
: "Inspect the command diagnostics for this stage before retrying.";
|
|
22
|
+
return `${value.stage} · ${value.code}${value.status ? ` · HTTP ${value.status}` : ""}${value.requestId ? ` · Request ${value.requestId}` : ""}\n${hint}`;
|
|
23
|
+
}
|
|
@@ -1,12 +1,29 @@
|
|
|
1
|
+
import { safeSqmDiagnostic } from "./diagnostics.js";
|
|
1
2
|
export async function uploadRunEvidence({ endpoint, token, proof, signedEvidence, fetchImpl = globalThis.fetch }) {
|
|
2
3
|
if (!endpoint || !token) return { uploaded: false, reason: "authenticated_cloud_session_unavailable" };
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
let response;
|
|
5
|
+
try {
|
|
6
|
+
response = await fetchImpl(endpoint, {
|
|
7
|
+
method: "POST",
|
|
8
|
+
headers: { accept: "application/json", authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
9
|
+
body: JSON.stringify({ proof, signed_evidence: signedEvidence }),
|
|
10
|
+
signal: AbortSignal.timeout(10_000),
|
|
11
|
+
});
|
|
12
|
+
} catch (cause) {
|
|
13
|
+
const error = cause instanceof Error ? cause : new Error("SQM run evidence upload failed.");
|
|
14
|
+
error.stage = "evidence_upload";
|
|
15
|
+
error.retryable = false;
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
9
18
|
const payload = await response.json().catch(() => null);
|
|
10
|
-
if (!response.ok)
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
const error = new Error(`SQM run evidence upload failed with HTTP ${response.status}${payload?.error ? `: ${typeof payload.error === "string" ? payload.error : payload.error.message || "unknown error"}` : ""}.`);
|
|
21
|
+
error.status = response.status;
|
|
22
|
+
error.stage = "evidence_upload";
|
|
23
|
+
error.retryable = false;
|
|
24
|
+
Object.assign(error, safeSqmDiagnostic({ status: response.status, code: payload?.error?.code,
|
|
25
|
+
requestId: response.headers?.get?.("x-request-id") }, "evidence_upload", "evidence_upload_failed"));
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
11
28
|
return { uploaded: true, ...payload };
|
|
12
29
|
}
|
package/src/sqm/history-store.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { diagnosticLogger } from "../diagnostic-log.js";
|
|
2
|
+
import { safeSqmDiagnostic, formatSqmDiagnostic } from "./diagnostics.js";
|
|
1
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
4
|
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
5
|
import { basename, join } from "node:path";
|
|
4
|
-
import { formatSqmCoverage } from "./report.js";
|
|
6
|
+
import { formatSqmCoverage, sqmCoverageWarnings } from "./report.js";
|
|
5
7
|
import { defaultSessionRoot } from "../session-store.js";
|
|
6
8
|
|
|
7
9
|
const LIMIT = 100;
|
|
@@ -44,6 +46,8 @@ export function createSqmHistoryStore({ root = defaultSessionRoot(), now = () =>
|
|
|
44
46
|
try {
|
|
45
47
|
await writeFile(temp, JSON.stringify(item), { mode: 0o600 });
|
|
46
48
|
await rename(temp, target);
|
|
49
|
+
if (["failed", "unavailable"].includes(item.state)) await diagnosticLogger({ root, scope }).record({
|
|
50
|
+
kind: item.kind, stage: entry.errorStage || item.phase || "unknown", code: item.errorCode, provider: item.provider, http_status: entry.httpStatus, at: item.finishedAt || item.startedAt });
|
|
47
51
|
} finally { await rm(temp, { force: true }); cache.delete(dir); }
|
|
48
52
|
const items = await list(scope);
|
|
49
53
|
for (const kind of ["analysis", "check"]) {
|
|
@@ -69,9 +73,11 @@ function metadata(entry) {
|
|
|
69
73
|
if (!entry || !["analysis", "check"].includes(entry.kind) || typeof entry.key !== "string" || !entry.key.startsWith(entry.kind === "analysis" ? "sqm:" : "sqm-check:")) return null;
|
|
70
74
|
if (!["running", "completed", "failed", "unavailable", "interrupted", "verification_pending"].includes(entry.state) || !Number.isFinite(Date.parse(entry.startedAt))) return null;
|
|
71
75
|
const item = {};
|
|
72
|
-
for (const name of ["key", "id", "kind", "label", "state", "provider", "phase", "repository", "startedAt", "finishedAt", "instruction", "result", "error", "errorCode", "webUrl"]) {
|
|
76
|
+
for (const name of ["key", "id", "kind", "label", "state", "provider", "phase", "repository", "startedAt", "finishedAt", "instruction", "result", "error", "errorCode", "errorStage", "requestId", "webUrl"]) {
|
|
73
77
|
if (typeof entry[name] === "string") item[name] = entry[name].slice(0, ["instruction", "result", "error"].includes(name) ? 4096 : 512);
|
|
74
78
|
}
|
|
79
|
+
if (Number.isInteger(entry.errorStatus) && entry.errorStatus >= 400 && entry.errorStatus <= 599) item.errorStatus = entry.errorStatus;
|
|
80
|
+
if (Number.isInteger(entry.httpStatus) && entry.httpStatus >= 400 && entry.httpStatus <= 599) item.httpStatus = entry.httpStatus;
|
|
75
81
|
return item;
|
|
76
82
|
}
|
|
77
83
|
|
|
@@ -94,15 +100,25 @@ export function createSqmCheckRecorder({ root, scope, phase, cwd, baseBranch, on
|
|
|
94
100
|
const repository = identity?.repository || result.repository;
|
|
95
101
|
const outcome = proof?.outcome.status || result.min_check?.status || result.status;
|
|
96
102
|
const state = outcome === "unavailable" ? "unavailable" : ["failed", "preliminary_failed"].includes(outcome) ? "failed" : ["requires_human", "requires_post_deploy"].includes(outcome) ? "verification_pending" : "completed";
|
|
103
|
+
const failure = result.upload?.diagnostic ? safeSqmDiagnostic(result.upload.diagnostic, "evidence_upload", "evidence_upload_failed") : ["failed", "unavailable"].includes(state) && (result.diagnostic || result.error || result.errorCode || !identity)
|
|
104
|
+
? safeSqmDiagnostic(result, phase, state === "unavailable" ? "sqm_check_unavailable" : "sqm_check_failed") : null;
|
|
105
|
+
const minCheck = result.min_check || result;
|
|
106
|
+
const minWarnings = !proof && Array.isArray(minCheck.matched_modules)
|
|
107
|
+
? sqmCoverageWarnings({ moduleCount: minCheck.matched_modules.length,
|
|
108
|
+
scenarioCount: Array.isArray(minCheck.full_check_plan) ? minCheck.full_check_plan.length :
|
|
109
|
+
minCheck.matched_modules.reduce((total, module) => total + (module.scenario_ids?.length || 0), 0),
|
|
110
|
+
noSystemKnowledge: minCheck.matched_modules.length === 0 && minCheck.coverage_reasons?.includes("no_system_knowledge") === true,
|
|
111
|
+
outcomeStatus: minCheck.status }) : [];
|
|
97
112
|
const summary = proof ? [`Full check: ${proof.outcome.status}`, formatSqmCoverage(proof), `Evidence upload: ${result.upload?.uploaded ? "uploaded" : "kept locally"}`].join("\n")
|
|
98
|
-
: state === "unavailable" ? "SQM could not obtain a verified result.
|
|
99
|
-
: state === "failed" && !identity ? "The command did not finish.
|
|
113
|
+
: state === "unavailable" ? "SQM could not obtain a verified result. See the recorded diagnostics below."
|
|
114
|
+
: state === "failed" && !identity ? "The command did not finish. See the recorded diagnostics below."
|
|
100
115
|
: `Min-check: ${result.min_check?.status || result.status} · ${(result.preliminary_findings || result.min_check?.preliminary_findings)?.length || 0} preliminary finding(s) · ${(result.matched_modules || result.min_check?.matched_modules)?.length || 0} matched module(s).\nThis is a preflight; run the full check for signed verification.`;
|
|
101
116
|
try {
|
|
102
117
|
await history.record(scope, { key, id: key, kind: "check", phase, state, startedAt, finishedAt: new Date().toISOString(), repository,
|
|
103
118
|
label: `${phase === "min" ? "Min-check" : "Check"} · ${repository || basename(cwd)}`,
|
|
104
119
|
instruction: [repository || basename(cwd), `Base: ${identity?.base_ref || baseBranch}`, identity?.head_commit ? `Head: ${identity.head_commit}` : ""].filter(Boolean).join("\n"),
|
|
105
|
-
result: summary
|
|
120
|
+
result: [summary, ...minWarnings].join("\n"),
|
|
121
|
+
...(failure ? { error: formatSqmDiagnostic(failure), errorCode: failure.code, errorStage: failure.stage, errorStatus: failure.status, requestId: failure.requestId } : {}) });
|
|
106
122
|
} catch { onError(); }
|
|
107
123
|
};
|
|
108
124
|
}
|
package/src/sqm/index.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { sqmCoverageWarnings } from "./report.js";
|
|
2
|
+
import { safeSqmDiagnostic } from "./diagnostics.js";
|
|
1
3
|
import { createSqmCheckRecorder } from "./history-store.js";
|
|
2
4
|
import { evaluateKnowledgeBundle } from "./checkers.js";
|
|
3
5
|
import { loadKnowledgeBundle } from "./bundle-store.js";
|
|
4
6
|
import { uploadRunEvidence } from "./evidence-client.js";
|
|
5
7
|
import { buildProof, finalizeMinCheck, repositoryIdentity, sameRepositoryIdentity, signProof, verifyMinCheck, verifySignedEvidence } from "./proof.js";
|
|
6
8
|
import { inspectRepository } from "./repository.js";
|
|
9
|
+
import { sqmDiagnostic } from "./request-recovery.js";
|
|
10
|
+
import { canonicalize } from "./canonical.js";
|
|
7
11
|
|
|
8
12
|
export function createSqmChecker({
|
|
9
13
|
cwd = process.cwd(),
|
|
@@ -19,6 +23,7 @@ export function createSqmChecker({
|
|
|
19
23
|
webOrigin,
|
|
20
24
|
historyRoot,
|
|
21
25
|
evidenceEndpoint,
|
|
26
|
+
signingKeyPath,
|
|
22
27
|
uploadRunEvidenceImpl = uploadRunEvidence,
|
|
23
28
|
inspectRepositoryImpl = inspectRepository,
|
|
24
29
|
loadKnowledgeBundleImpl = loadKnowledgeBundle,
|
|
@@ -29,34 +34,59 @@ export function createSqmChecker({
|
|
|
29
34
|
let pinnedLoad = null;
|
|
30
35
|
let pinnedRepository = null;
|
|
31
36
|
let latestMinCheck = null;
|
|
37
|
+
let pinnedSystemScope;
|
|
38
|
+
const onRecoveryEvent = async (diagnostic) => {
|
|
39
|
+
await createSqmCheckRecorder({ root: historyRoot,
|
|
40
|
+
scope: historyRoot ? { webOrigin, accountId, organizationId } : null,
|
|
41
|
+
phase: diagnostic.stage === "evidence_upload" ? "full" : "min", cwd, baseBranch })({ status: "failed", diagnostic });
|
|
42
|
+
await dependencies.onRecoveryEvent?.(diagnostic);
|
|
43
|
+
};
|
|
32
44
|
|
|
33
45
|
const checkCurrentRepository = async function(_attempt = null, { phase = "min" } = {}) {
|
|
34
46
|
let repository;
|
|
35
47
|
try {
|
|
36
48
|
repository = await inspectRepositoryImpl(cwd, { baseBranch, runCommand: dependencies.runCommand });
|
|
37
49
|
} catch (error) {
|
|
38
|
-
return unavailable(`SQM repository inspection failed: ${message(error)}`);
|
|
50
|
+
return { ...unavailable(`SQM repository inspection failed: ${message(error)}`), diagnostic: { ...safeSqmDiagnostic(error, "repository", "repository_inspection_failed"), ...sqmDiagnostic(error, "repository") } };
|
|
39
51
|
}
|
|
40
52
|
if (pinnedRepository && pinnedRepository !== repository.repository) return unavailable(`SQM repository changed during a pinned run (${pinnedRepository} to ${repository.repository}).`, repository.repository);
|
|
41
53
|
if (!pinnedLoad) {
|
|
42
54
|
pinnedRepository = repository.repository;
|
|
43
|
-
pinnedLoad = loadKnowledgeBundleImpl({ repository: repository.repository, cliVersion, offline, refresh, token, endpoint, accountId, organizationId,
|
|
55
|
+
pinnedLoad = loadKnowledgeBundleImpl({ ...dependencies, repository: repository.repository, cliVersion, offline, refresh, token, endpoint, accountId, organizationId, onRecoveryEvent });
|
|
44
56
|
}
|
|
45
57
|
const loaded = await pinnedLoad;
|
|
46
|
-
if (!loaded.bundle)
|
|
58
|
+
if (!loaded.bundle) {
|
|
59
|
+
// A failed read must not poison the run permanently. A later explicit
|
|
60
|
+
// same-scope check or refreshed sign-in may try loading again.
|
|
61
|
+
pinnedLoad = null;
|
|
62
|
+
return { ...unavailable(loaded.warnings[0], repository.repository), warnings: loaded.warnings, diagnostic: loaded.diagnostic };
|
|
63
|
+
}
|
|
64
|
+
if (pinnedSystemScope === undefined) pinnedSystemScope = loaded.bundle.system_scope || null;
|
|
65
|
+
else if (!sameSystemScope(pinnedSystemScope, loaded.bundle.system_scope || null)) {
|
|
66
|
+
return { ...unavailable("SQM system scope changed during recovery; authorize the current scope before a new check.", repository.repository),
|
|
67
|
+
diagnostic: { stage: "knowledge_loading", code: "authorization_scope_changed", retryable: false } };
|
|
68
|
+
}
|
|
47
69
|
try {
|
|
48
70
|
const evaluate = executeKnowledgeBundleImpl
|
|
49
71
|
? async (bundle, currentRepository) => ({ findings: await executeKnowledgeBundleImpl(bundle, currentRepository), modules: [] })
|
|
50
72
|
: evaluateKnowledgeBundleImpl;
|
|
51
73
|
if (phase === "full") {
|
|
52
|
-
if (!latestMinCheck || !sameRepositoryIdentity(latestMinCheck.repository_identity, repositoryIdentity(repository))
|
|
74
|
+
if (!latestMinCheck || !sameRepositoryIdentity(latestMinCheck.repository_identity, repositoryIdentity(repository)) ||
|
|
75
|
+
latestMinCheck.knowledge_bundle.digest !== loaded.bundle.integrity.digest) {
|
|
53
76
|
latestMinCheck = await buildMinCheck({ repository, bundle: loaded.bundle, evaluation: await evaluate(loaded.bundle, repository), organizationId: organizationId || loaded.bundle.organization_id, cliVersion });
|
|
54
77
|
}
|
|
55
|
-
const
|
|
78
|
+
const fullEvaluation = await evaluate(loaded.bundle, repository);
|
|
79
|
+
const current = await inspectRepositoryImpl(cwd, { baseBranch, runCommand: dependencies.runCommand });
|
|
80
|
+
if (!sameRepositoryIdentity(repositoryIdentity(repository), repositoryIdentity(current))) {
|
|
81
|
+
throw candidateError("Repository identity changed during SQM full verification; no current proof can be signed.");
|
|
82
|
+
}
|
|
83
|
+
const full = await buildFullCheck({ repository, bundle: loaded.bundle, minCheck: latestMinCheck, evaluation: fullEvaluation, organizationId: organizationId || loaded.bundle.organization_id, cliVersion, deviceId, accountId, signingKeyPath });
|
|
56
84
|
try {
|
|
57
85
|
full.upload = await uploadRunEvidenceImpl({ endpoint: evidenceEndpoint, token, proof: full.proof, signedEvidence: full.signed_evidence });
|
|
58
86
|
} catch (error) {
|
|
59
87
|
full.upload = { uploaded: false, reason: `upload_failed: ${message(error)}` };
|
|
88
|
+
full.upload.diagnostic = { ...safeSqmDiagnostic(error, "evidence_upload", "evidence_upload_failed"), ...sqmDiagnostic(error, "evidence_upload") };
|
|
89
|
+
await onRecoveryEvent(full.upload.diagnostic);
|
|
60
90
|
full.warnings.push(`SQM signed evidence remains local because Cloud upload failed: ${message(error)}`);
|
|
61
91
|
}
|
|
62
92
|
return full;
|
|
@@ -64,38 +94,91 @@ export function createSqmChecker({
|
|
|
64
94
|
latestMinCheck = await buildMinCheck({ repository, bundle: loaded.bundle, evaluation: await evaluate(loaded.bundle, repository), organizationId: organizationId || loaded.bundle.organization_id, cliVersion });
|
|
65
95
|
return toLegacyCheck(latestMinCheck, loaded);
|
|
66
96
|
} catch (error) {
|
|
67
|
-
return { ...unavailable(`SQM ${phase} execution failed: ${message(error)}`, repository.repository), bundle: bundleIdentity(loaded.bundle), warnings: [...loaded.warnings, `SQM ${phase} execution failed: ${message(error)}`] };
|
|
97
|
+
return { ...unavailable(`SQM ${phase} execution failed: ${message(error)}`, repository.repository), bundle: bundleIdentity(loaded.bundle), warnings: [...loaded.warnings, `SQM ${phase} execution failed: ${message(error)}`], diagnostic: { ...safeSqmDiagnostic(error, `${phase}_check`, "sqm_execution_failed"), ...sqmDiagnostic(error, `${phase}_check`) } };
|
|
68
98
|
}
|
|
69
99
|
};
|
|
70
|
-
|
|
100
|
+
const checker = async (attempt, options = {}) => {
|
|
71
101
|
const save = createSqmCheckRecorder({ root: historyRoot, scope: historyRoot ? { webOrigin, accountId, organizationId } : null, phase: options.phase || "min", cwd, baseBranch });
|
|
72
102
|
try {
|
|
73
103
|
const result = await checkCurrentRepository(attempt, options);
|
|
74
104
|
await save(result); return result;
|
|
75
|
-
} catch (error) {
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const stage = options.phase === "full" ? "full_check" : "min_check";
|
|
107
|
+
const diagnostic = { ...safeSqmDiagnostic(error, stage), ...sqmDiagnostic(error, stage) };
|
|
108
|
+
if (diagnostic.retryable) pinnedLoad = null;
|
|
109
|
+
await save({ status: "failed", diagnostic }); throw error;
|
|
110
|
+
}
|
|
76
111
|
};
|
|
112
|
+
checker.updateSession = (next) => {
|
|
113
|
+
for (const [field, current] of Object.entries({ accountId, organizationId, deviceId, endpoint, webOrigin })) {
|
|
114
|
+
if (current && next?.[field] !== current) {
|
|
115
|
+
const error = new Error(`SQM session changed its ${field}; the original scope cannot be resumed with it.`);
|
|
116
|
+
error.code = "authorization_scope_changed"; throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!next?.token) throw new Error("An authenticated same-scope SQM session is required.");
|
|
120
|
+
if (next.token !== token) { token = next.token; pinnedLoad = null; }
|
|
121
|
+
};
|
|
122
|
+
return checker;
|
|
77
123
|
}
|
|
78
124
|
|
|
79
125
|
export async function runSqmMinCheck(options) {
|
|
80
126
|
const context = await loadContext(options);
|
|
81
|
-
if (!context.loaded.bundle) return { ...unavailable(context.loaded.warnings[0], context.repository.repository), warnings: context.loaded.warnings };
|
|
127
|
+
if (!context.loaded.bundle) return { ...unavailable(context.loaded.warnings[0], context.repository.repository), warnings: context.loaded.warnings, diagnostic: context.loaded.diagnostic };
|
|
82
128
|
const evaluation = await (options.evaluateKnowledgeBundleImpl || evaluateKnowledgeBundle)(context.loaded.bundle, context.repository);
|
|
83
129
|
return buildMinCheck({ repository: context.repository, bundle: context.loaded.bundle, evaluation, organizationId: options.organizationId, cliVersion: options.cliVersion });
|
|
84
130
|
}
|
|
85
131
|
|
|
86
132
|
export async function runSqmCheck(options) {
|
|
87
133
|
const context = await loadContext(options);
|
|
88
|
-
if (!context.loaded.bundle) return { ...unavailable(context.loaded.warnings[0], context.repository.repository), warnings: context.loaded.warnings };
|
|
134
|
+
if (!context.loaded.bundle) return { ...unavailable(context.loaded.warnings[0], context.repository.repository), warnings: context.loaded.warnings, diagnostic: context.loaded.diagnostic };
|
|
89
135
|
verifyMinCheck(options.minCheck);
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
136
|
+
if (options.minCheck.organization_id !== options.organizationId || context.loaded.bundle.organization_id !== options.organizationId) {
|
|
137
|
+
throw scopeError("SQM organization changed after min-check; authorize the original organization before resuming.");
|
|
138
|
+
}
|
|
139
|
+
if (!sameRepositoryIdentity(options.minCheck.repository_identity, repositoryIdentity(context.repository))) throw Object.assign(candidateError("Repository identity changed after min-check; run `simy sqm min-check` again."), { stage: "identity_validation" });
|
|
140
|
+
if (!sameSystemScope(options.minCheck.knowledge_bundle.system_scope || null, context.loaded.bundle.system_scope || null)) {
|
|
141
|
+
throw scopeError("SQM system scope changed after min-check; authorize the current scope before a new check.");
|
|
142
|
+
}
|
|
143
|
+
const evaluate = options.evaluateKnowledgeBundleImpl || evaluateKnowledgeBundle;
|
|
144
|
+
let minCheck = options.minCheck;
|
|
145
|
+
if (minCheck.knowledge_bundle.digest !== context.loaded.bundle.integrity.digest) {
|
|
146
|
+
await options.onRecoveryEvent?.({ stage: "full_check", code: "knowledge_bundle_changed", retryable: true });
|
|
147
|
+
minCheck = await buildMinCheck({ repository: context.repository, bundle: context.loaded.bundle,
|
|
148
|
+
evaluation: await evaluate(context.loaded.bundle, context.repository), organizationId: options.organizationId, cliVersion: options.cliVersion });
|
|
149
|
+
const current = await (options.inspectRepositoryImpl || inspectRepository)(options.cwd || process.cwd(), { baseBranch: options.baseBranch || "dev", runCommand: options.runCommand });
|
|
150
|
+
if (!sameRepositoryIdentity(minCheck.repository_identity, repositoryIdentity(current))) throw new Error("Repository identity changed during min-check recovery; run `simy sqm min-check` again.");
|
|
151
|
+
}
|
|
152
|
+
const evaluation = await evaluate(context.loaded.bundle, context.repository);
|
|
153
|
+
const verifiedRepository = await (options.inspectRepositoryImpl || inspectRepository)(options.cwd || process.cwd(), { baseBranch: options.baseBranch || "dev", runCommand: options.runCommand });
|
|
154
|
+
if (!sameRepositoryIdentity(repositoryIdentity(context.repository), repositoryIdentity(verifiedRepository))) {
|
|
155
|
+
throw candidateError("Repository identity changed during SQM full verification; no current proof can be signed.");
|
|
156
|
+
}
|
|
157
|
+
const result = await buildFullCheck({ repository: context.repository, bundle: context.loaded.bundle, minCheck, evaluation, organizationId: options.organizationId, cliVersion: options.cliVersion, deviceId: options.deviceId, accountId: options.accountId, signingKeyPath: options.signingKeyPath });
|
|
158
|
+
if (minCheck !== options.minCheck) result.recovery = { code: "knowledge_bundle_changed", previous_min_digest: options.minCheck.result_digest, current_min_digest: minCheck.result_digest };
|
|
159
|
+
return result;
|
|
94
160
|
}
|
|
95
161
|
|
|
96
162
|
async function loadContext(options) {
|
|
97
163
|
const repository = await (options.inspectRepositoryImpl || inspectRepository)(options.cwd || process.cwd(), { baseBranch: options.baseBranch || "dev", runCommand: options.runCommand });
|
|
98
|
-
const
|
|
164
|
+
const rejectedToken = options.token;
|
|
165
|
+
const originalIdentity = repositoryIdentity(repository);
|
|
166
|
+
const scope = Object.fromEntries(["accountId", "organizationId", "deviceId", "endpoint"].map(field => [field, options[field]]));
|
|
167
|
+
const load = (token) => (options.loadKnowledgeBundleImpl || loadKnowledgeBundle)({ ...(options.bundleDependencies || {}),
|
|
168
|
+
repository: repository.repository, cliVersion: options.cliVersion, offline: options.offline, refresh: options.refresh,
|
|
169
|
+
token, endpoint: scope.endpoint, accountId: scope.accountId, organizationId: scope.organizationId });
|
|
170
|
+
let loaded = await load(rejectedToken);
|
|
171
|
+
if (!loaded.bundle && loaded.diagnostic?.http_status === 401 && options.onAuthenticationRequired && !options.offline) {
|
|
172
|
+
await options.onRecoveryEvent?.(loaded.diagnostic);
|
|
173
|
+
const next = await options.onAuthenticationRequired({ rejectedToken, ...scope });
|
|
174
|
+
for (const field of ["accountId", "organizationId", "deviceId", "endpoint"]) {
|
|
175
|
+
if (scope[field] && next?.[field] !== scope[field]) throw scopeError(`SQM authorization changed its ${field}; do not resume another scope.`);
|
|
176
|
+
}
|
|
177
|
+
if (!next?.token || next.token === rejectedToken) throw scopeError("SQM authorization did not replace the rejected token.");
|
|
178
|
+
const current = await (options.inspectRepositoryImpl || inspectRepository)(options.cwd || process.cwd(), { baseBranch: options.baseBranch || "dev", runCommand: options.runCommand });
|
|
179
|
+
if (!sameRepositoryIdentity(originalIdentity, repositoryIdentity(current))) throw candidateError("Repository identity changed while waiting for SQM authorization; begin a new check against the current candidate.");
|
|
180
|
+
loaded = await load(next.token);
|
|
181
|
+
}
|
|
99
182
|
return { repository, loaded };
|
|
100
183
|
}
|
|
101
184
|
|
|
@@ -113,6 +196,13 @@ async function buildMinCheck({ repository, bundle, evaluation, organizationId, c
|
|
|
113
196
|
preliminary_findings: evaluation.findings,
|
|
114
197
|
matched_modules: evaluation.modules.map((module) => ({ module_id: module.module_id, module_version: module.module_version, state_model_ids: module.state_models.map((model) => model.id), transition_ids: module.state_models.flatMap((model) => model.transitions.map((transition) => transition.id)), stressor_ids: module.stressors.map((stressor) => stressor.id), scenario_ids: module.scenarios.map((scenario) => scenario.id) })),
|
|
115
198
|
full_check_plan: plan,
|
|
199
|
+
coverage_reasons: [
|
|
200
|
+
...(evaluation.modules.length === 0 ? ["no_applicable_rules"] : []),
|
|
201
|
+
...(bundle.modules?.length === 0 ? ["no_system_knowledge"] : []),
|
|
202
|
+
],
|
|
203
|
+
warnings: sqmCoverageWarnings({ moduleCount: evaluation.modules.length, scenarioCount: plan.length,
|
|
204
|
+
noSystemKnowledge: bundle.modules?.length === 0,
|
|
205
|
+
outcomeStatus: evaluation.findings.some(finding => finding.enforcement === "blocking") ? "preliminary_failed" : "passed" }),
|
|
116
206
|
});
|
|
117
207
|
}
|
|
118
208
|
|
|
@@ -133,12 +223,12 @@ async function buildFullCheck({ repository, bundle, minCheck, evaluation, organi
|
|
|
133
223
|
proof,
|
|
134
224
|
signed_evidence: signedEvidence,
|
|
135
225
|
signature_verification: verification,
|
|
136
|
-
warnings:
|
|
226
|
+
warnings: sqmCoverageWarnings({ moduleCount: evaluation.modules.length, scenarioCount: evaluation.modules.reduce((count, module) => count + module.scenarios.length, 0), noSystemKnowledge: bundle.modules?.length === 0, outcomeStatus: proof.outcome.status }),
|
|
137
227
|
};
|
|
138
228
|
}
|
|
139
229
|
|
|
140
230
|
function toLegacyCheck(minCheck, loaded) {
|
|
141
|
-
return { status: "checked", mode: "min", repository: minCheck.repository_identity.repository, base_ref: minCheck.repository_identity.base_ref, source: loaded.source, warnings: loaded.warnings, bundle: minCheck.knowledge_bundle, findings: minCheck.preliminary_findings, min_check: minCheck };
|
|
231
|
+
return { status: "checked", mode: "min", repository: minCheck.repository_identity.repository, base_ref: minCheck.repository_identity.base_ref, source: loaded.source, warnings: [...loaded.warnings, ...(minCheck.warnings || [])], bundle: minCheck.knowledge_bundle, findings: minCheck.preliminary_findings, min_check: minCheck };
|
|
142
232
|
}
|
|
143
233
|
|
|
144
234
|
function bundleIdentity(bundle) {
|
|
@@ -150,3 +240,12 @@ function unavailable(warning, repository = null) {
|
|
|
150
240
|
}
|
|
151
241
|
|
|
152
242
|
function message(error) { return error instanceof Error ? error.message : String(error); }
|
|
243
|
+
|
|
244
|
+
function sameSystemScope(left, right) {
|
|
245
|
+
const normalized = (scope) => scope ? { organization_id: scope.organization_id,
|
|
246
|
+
repository: scope.repository?.toLowerCase(), graph_revision: scope.graph_revision,
|
|
247
|
+
repositories: [...(scope.repositories || [])].map((repository) => repository.toLowerCase()).sort() } : null;
|
|
248
|
+
return canonicalize(normalized(left)) === canonicalize(normalized(right));
|
|
249
|
+
}
|
|
250
|
+
function scopeError(message) { const error = new Error(message); error.code = "authorization_scope_changed"; return error; }
|
|
251
|
+
function candidateError(message) { const error = new Error(message); error.code = "repository_identity_changed"; return error; }
|
package/src/sqm/proof.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { createHash,
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { createHash, createPublicKey, randomUUID, sign, verify } from "node:crypto";
|
|
3
2
|
import { homedir } from "node:os";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
|
|
6
5
|
import { canonicalize } from "./canonical.js";
|
|
6
|
+
import { loadDeviceSigningIdentity } from "../device-signing-identity.js";
|
|
7
7
|
|
|
8
8
|
export function repositoryIdentity(repository) {
|
|
9
9
|
return {
|
|
@@ -73,6 +73,7 @@ export function buildProof({ organizationId, repository, bundle, minCheck, evalu
|
|
|
73
73
|
...(hasFailure ? ["one_or_more_blocking_checks_failed"] : []),
|
|
74
74
|
...(advisoryFindingCount > 0 ? ["advisory_findings_observed"] : []),
|
|
75
75
|
...(evaluation.modules.length === 0 ? ["no_applicable_rules"] : []),
|
|
76
|
+
...(bundle.modules?.length === 0 ? ["no_system_knowledge"] : []),
|
|
76
77
|
...(requiresHuman ? ["human_evidence_required"] : []),
|
|
77
78
|
...(requiresPostDeploy ? ["post_deploy_evidence_required"] : []),
|
|
78
79
|
],
|
|
@@ -97,7 +98,7 @@ export async function signProof(proof, {
|
|
|
97
98
|
issuedAt = new Date().toISOString(),
|
|
98
99
|
} = {}) {
|
|
99
100
|
if (!deviceId) throw new Error("A trusted SIMY CLI device identity is required to sign SQM proof.");
|
|
100
|
-
const identity = await
|
|
101
|
+
const identity = await loadDeviceSigningIdentity(keyPath, { create: true });
|
|
101
102
|
const proofDigest = verifyProof(proof);
|
|
102
103
|
const signature = sign(null, Buffer.from(proofDigest.slice("sha256:".length), "hex"), identity.privateKey).toString("base64");
|
|
103
104
|
return {
|
|
@@ -132,26 +133,6 @@ export function verifySignedEvidence(proof, evidence) {
|
|
|
132
133
|
return { proof_digest: proofDigest, key_id: keyId, valid: true };
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
async function loadOrCreateSigningIdentity(location) {
|
|
136
|
-
try {
|
|
137
|
-
const parsed = JSON.parse(await readFile(location, "utf8"));
|
|
138
|
-
const privateKey = createPrivateKey({ key: Buffer.from(parsed.private_key, "base64"), format: "der", type: "pkcs8" });
|
|
139
|
-
const publicKeyDer = Buffer.from(parsed.public_key, "base64");
|
|
140
|
-
const keyId = `sha256:${createHash("sha256").update(publicKeyDer).digest("hex")}`;
|
|
141
|
-
if (parsed.key_id !== keyId) throw new Error("stored key ID mismatch");
|
|
142
|
-
return { privateKey, publicKeyDer, keyId };
|
|
143
|
-
} catch (error) {
|
|
144
|
-
if (error?.code !== "ENOENT") throw new Error(`SQM signing identity is invalid: ${error.message}`);
|
|
145
|
-
}
|
|
146
|
-
const pair = generateKeyPairSync("ed25519");
|
|
147
|
-
const privateKeyDer = pair.privateKey.export({ format: "der", type: "pkcs8" });
|
|
148
|
-
const publicKeyDer = pair.publicKey.export({ format: "der", type: "spki" });
|
|
149
|
-
const keyId = `sha256:${createHash("sha256").update(publicKeyDer).digest("hex")}`;
|
|
150
|
-
await mkdir(path.dirname(location), { recursive: true, mode: 0o700 });
|
|
151
|
-
await writeFile(location, `${JSON.stringify({ algorithm: "ed25519", key_id: keyId, private_key: privateKeyDer.toString("base64"), public_key: publicKeyDer.toString("base64"), created_at: new Date().toISOString() }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
152
|
-
return { privateKey: pair.privateKey, publicKeyDer, keyId };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
136
|
function taggedDigest(value) {
|
|
156
137
|
return `sha256:${createHash("sha256").update(canonicalize(value), "utf8").digest("hex")}`;
|
|
157
138
|
}
|
package/src/sqm/report.js
CHANGED
|
@@ -15,9 +15,16 @@ export function formatSqmCoverage(proof) {
|
|
|
15
15
|
];
|
|
16
16
|
if (scenarios.length) lines.push(`Scenario evaluations: ${[...statusCounts].map(([status, count]) => `${status}=${count}`).join(" · ")}`);
|
|
17
17
|
if (proof.outcome.reasons.length) lines.push(`Outcome reasons: ${proof.outcome.reasons.join(", ")}`);
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
lines.push(...sqmCoverageWarnings({ moduleCount: modules.length, scenarioCount: scenarios.length,
|
|
19
|
+
noSystemKnowledge: proof.outcome.reasons.includes("no_system_knowledge"), outcomeStatus: proof.outcome.status }));
|
|
20
20
|
if (modules.length && !blocking) lines.push("Advisory-only scope: findings do not block this outcome.");
|
|
21
21
|
lines.push("Scenario evaluations are deterministic checks, not browser/provider/deployed journey evidence. Record incident coverage and delivery acceptance separately.");
|
|
22
22
|
return lines.join("\n");
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
export function sqmCoverageWarnings({ moduleCount, scenarioCount, noSystemKnowledge, outcomeStatus = "passed" }) {
|
|
26
|
+
if (noSystemKnowledge) return [`Warning: the selected system/repository's downloaded SQM knowledge bundle has no published knowledge. ${outcomeStatus === "passed" ? "This is a pass with 0 evaluated cases" : "This result has 0 evaluated cases"}; it does not establish feature or historical incident coverage.`];
|
|
27
|
+
if (!moduleCount) return ["Warning: no applicable rules matched this change (0 evaluated cases). This result does not establish feature or historical incident coverage; it does not mean the system has no past knowledge."];
|
|
28
|
+
if (!scenarioCount) return ["Warning: No scenario evaluations: rule results alone do not establish scenario coverage."];
|
|
29
|
+
return [];
|
|
30
|
+
}
|