@awak-app/simy-cli 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/package.json +4 -2
- package/src/agent.js +18 -1
- package/src/cli-contract.js +1 -1
- package/src/execution-guardrail.js +2 -2
- package/src/index.js +14 -0
- package/src/local-task.js +51 -8
- package/src/orchestrator/audit.js +42 -1
- package/src/orchestrator/loop.js +29 -3
- package/src/runner.js +44 -0
- package/src/sqm/bundle-store.js +249 -0
- package/src/sqm/canonical.js +45 -0
- package/src/sqm/checkers.js +299 -0
- package/src/sqm/command.js +149 -0
- package/src/sqm/evidence-client.js +12 -0
- package/src/sqm/index.js +141 -0
- package/src/sqm/proof.js +154 -0
- package/src/sqm/repository.js +163 -0
- package/src/sqm/session.js +58 -0
- package/src/sqm/validation.js +305 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { startAgent } from "../agent.js";
|
|
2
|
+
import { openAuthorizationUrl } from "../browser.js";
|
|
3
|
+
import { isSessionValid, readSession } from "../session-store.js";
|
|
4
|
+
import { createAgentShutdown } from "../shutdown.js";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_AUTH_TIMEOUT_MS = 2 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
export async function ensureSqmSession(
|
|
9
|
+
webOrigin,
|
|
10
|
+
{
|
|
11
|
+
noOpen = false,
|
|
12
|
+
timeoutMs = DEFAULT_AUTH_TIMEOUT_MS,
|
|
13
|
+
logger = console,
|
|
14
|
+
now = Date.now,
|
|
15
|
+
sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
16
|
+
readSessionImpl = readSession,
|
|
17
|
+
isSessionValidImpl = isSessionValid,
|
|
18
|
+
startAgentImpl = startAgent,
|
|
19
|
+
openAuthorizationUrlImpl = openAuthorizationUrl,
|
|
20
|
+
createAgentShutdownImpl = createAgentShutdown,
|
|
21
|
+
} = {},
|
|
22
|
+
) {
|
|
23
|
+
let session = await readSessionImpl(webOrigin);
|
|
24
|
+
if (sqmSessionReady(session, webOrigin, now(), isSessionValidImpl)) return session;
|
|
25
|
+
|
|
26
|
+
const agent = await startAgentImpl({ requestedPort: 0, webOrigin, quiet: true });
|
|
27
|
+
const shutdown = createAgentShutdownImpl(agent);
|
|
28
|
+
try {
|
|
29
|
+
const loginUrl = agent.loginUrl;
|
|
30
|
+
if (!loginUrl) throw new Error("SIMY could not create an SQM authorization session.");
|
|
31
|
+
logger.log("SIMY sign-in is required before the SQM check can continue.");
|
|
32
|
+
if (noOpen) logger.log(`Open this URL to authorize SIMY CLI: ${loginUrl}`);
|
|
33
|
+
else await openAuthorizationUrlImpl(loginUrl, { logger });
|
|
34
|
+
logger.log("Waiting for browser authorization...");
|
|
35
|
+
|
|
36
|
+
const deadline = now() + timeoutMs;
|
|
37
|
+
while (now() < deadline) {
|
|
38
|
+
await sleep(Math.min(500, Math.max(1, deadline - now())));
|
|
39
|
+
session = await readSessionImpl(webOrigin);
|
|
40
|
+
if (sqmSessionReady(session, webOrigin, now(), isSessionValidImpl)) {
|
|
41
|
+
logger.log("SIMY CLI authorization completed. Continuing SQM check.");
|
|
42
|
+
return session;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw new Error("SIMY authorization timed out. Complete the browser sign-in and rerun the SQM command.");
|
|
46
|
+
} finally {
|
|
47
|
+
await shutdown();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sqmSessionReady(session, webOrigin, now, isSessionValidImpl) {
|
|
52
|
+
return Boolean(
|
|
53
|
+
isSessionValidImpl(session, now, webOrigin) &&
|
|
54
|
+
(session?.organization_id || session?.org_id) &&
|
|
55
|
+
session?.token &&
|
|
56
|
+
session?.api_base_url,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
const SEMVER = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
|
|
2
|
+
const DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
3
|
+
const MODULE_ID = /^KM-[A-Z0-9][A-Z0-9._-]{2,127}$/;
|
|
4
|
+
const INCIDENT_ID = /^INC-[A-Z0-9][A-Z0-9._-]{2,127}$/;
|
|
5
|
+
const RULE_ID = /^[a-z][a-z0-9._-]{2,127}$/;
|
|
6
|
+
const REPOSITORY = /^[^/\s]+\/[^/\s]+$/;
|
|
7
|
+
const CHECKERS = new Set(["diff_regex", "path_presence", "content_regex", "reference_definition"]);
|
|
8
|
+
const MATCHERS = new Set(["changed_path", "diff_regex", "content_regex", "reference"]);
|
|
9
|
+
const LIFECYCLES = new Set(["candidate", "shadow", "warn", "blocking", "retired"]);
|
|
10
|
+
const SEVERITIES = new Set(["info", "low", "medium", "high", "critical"]);
|
|
11
|
+
const BOUNDARIES = new Set(["api", "auth", "database", "deployment", "external_tool", "human", "llm", "pipeline", "runtime", "ui"]);
|
|
12
|
+
const ASSURANCE_ID = /^[A-Z][A-Z0-9._-]{2,127}$/;
|
|
13
|
+
const STRENGTH_KINDS = new Set(["prevention", "detection", "recovery"]);
|
|
14
|
+
const STRENGTH_STATUSES = new Set(["existing", "expected"]);
|
|
15
|
+
const SCENARIO_REQUIREMENTS = new Set(["local", "requires_human", "requires_post_deploy"]);
|
|
16
|
+
|
|
17
|
+
export function validateKnowledgeBundle(bundle, { cliVersion }) {
|
|
18
|
+
const errors = [];
|
|
19
|
+
object(bundle, "$", errors);
|
|
20
|
+
exactKeys(bundle, ["schema_version", "bundle_id", "version", "organization_id", "generated_at", "expires_at", "modules", "integrity"], "$", errors);
|
|
21
|
+
equal(bundle?.schema_version, "1.0.0", "$.schema_version", errors);
|
|
22
|
+
stringPattern(bundle?.bundle_id, /^[a-z][a-z0-9._-]{2,127}$/, "$.bundle_id", errors);
|
|
23
|
+
stringPattern(bundle?.version, /^[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]+$/, "$.version", errors);
|
|
24
|
+
nullableString(bundle?.organization_id, 200, "$.organization_id", errors);
|
|
25
|
+
dateTime(bundle?.generated_at, "$.generated_at", errors);
|
|
26
|
+
dateTime(bundle?.expires_at, "$.expires_at", errors);
|
|
27
|
+
if (!Array.isArray(bundle?.modules)) errors.push("$.modules must be an array");
|
|
28
|
+
for (const [index, module] of (bundle?.modules || []).entries()) {
|
|
29
|
+
validateModule(module, `$.modules[${index}]`, errors, cliVersion);
|
|
30
|
+
}
|
|
31
|
+
object(bundle?.integrity, "$.integrity", errors);
|
|
32
|
+
exactKeys(bundle?.integrity, ["algorithm", "digest", "key_id", "signature"], "$.integrity", errors);
|
|
33
|
+
equal(bundle?.integrity?.algorithm, "ed25519", "$.integrity.algorithm", errors);
|
|
34
|
+
stringPattern(bundle?.integrity?.digest, DIGEST, "$.integrity.digest", errors);
|
|
35
|
+
boundedString(bundle?.integrity?.key_id, 1, 200, "$.integrity.key_id", errors);
|
|
36
|
+
boundedString(bundle?.integrity?.signature, 1, 1000, "$.integrity.signature", errors);
|
|
37
|
+
if (errors.length) throw new Error(`Invalid SQM knowledge bundle:\n- ${errors.join("\n- ")}`);
|
|
38
|
+
return bundle;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function validateModule(module, at, errors, cliVersion) {
|
|
42
|
+
object(module, at, errors);
|
|
43
|
+
exactKeys(module, ["schema_version", "id", "version", "title", "summary", "lifecycle", "scope", "invariant", "detection", "assurance", "incident_refs", "provenance", "evaluation", "compatibility"], at, errors);
|
|
44
|
+
oneOf(module?.schema_version, new Set(["1.0.0", "1.1.0"]), `${at}.schema_version`, errors);
|
|
45
|
+
stringPattern(module?.id, MODULE_ID, `${at}.id`, errors);
|
|
46
|
+
stringPattern(module?.version, SEMVER, `${at}.version`, errors);
|
|
47
|
+
boundedString(module?.title, 1, 200, `${at}.title`, errors);
|
|
48
|
+
boundedString(module?.summary, 1, 2000, `${at}.summary`, errors);
|
|
49
|
+
|
|
50
|
+
object(module?.lifecycle, `${at}.lifecycle`, errors);
|
|
51
|
+
exactKeys(module?.lifecycle, ["status", "severity", "owner"], `${at}.lifecycle`, errors);
|
|
52
|
+
oneOf(module?.lifecycle?.status, LIFECYCLES, `${at}.lifecycle.status`, errors);
|
|
53
|
+
oneOf(module?.lifecycle?.severity, SEVERITIES, `${at}.lifecycle.severity`, errors);
|
|
54
|
+
nullableString(module?.lifecycle?.owner, 200, `${at}.lifecycle.owner`, errors);
|
|
55
|
+
|
|
56
|
+
object(module?.scope, `${at}.scope`, errors);
|
|
57
|
+
exactKeys(module?.scope, ["visibility", "organization_id", "repositories", "languages", "frameworks", "boundaries"], `${at}.scope`, errors);
|
|
58
|
+
oneOf(module?.scope?.visibility, new Set(["core", "framework", "organization"]), `${at}.scope.visibility`, errors);
|
|
59
|
+
nullableString(module?.scope?.organization_id, 200, `${at}.scope.organization_id`, errors);
|
|
60
|
+
stringArray(module?.scope?.repositories, `${at}.scope.repositories`, errors, REPOSITORY, 500, false);
|
|
61
|
+
stringArray(module?.scope?.languages, `${at}.scope.languages`, errors, null, 100, false);
|
|
62
|
+
stringArray(module?.scope?.frameworks, `${at}.scope.frameworks`, errors, null, 100, false);
|
|
63
|
+
enumArray(module?.scope?.boundaries, `${at}.scope.boundaries`, errors, BOUNDARIES, true);
|
|
64
|
+
|
|
65
|
+
object(module?.invariant, `${at}.invariant`, errors);
|
|
66
|
+
exactKeys(module?.invariant, ["statement", "failure_condition", "remediation"], `${at}.invariant`, errors);
|
|
67
|
+
boundedString(module?.invariant?.statement, 1, 1000, `${at}.invariant.statement`, errors);
|
|
68
|
+
boundedString(module?.invariant?.failure_condition, 1, 1000, `${at}.invariant.failure_condition`, errors);
|
|
69
|
+
nullableString(module?.invariant?.remediation, 2000, `${at}.invariant.remediation`, errors);
|
|
70
|
+
|
|
71
|
+
object(module?.detection, `${at}.detection`, errors);
|
|
72
|
+
exactKeys(module?.detection, ["matchers", "checks", "exceptions"], `${at}.detection`, errors);
|
|
73
|
+
nonEmptyArray(module?.detection?.matchers, `${at}.detection.matchers`, errors);
|
|
74
|
+
for (const [index, matcher] of (module?.detection?.matchers || []).entries()) validateMatcher(matcher, `${at}.detection.matchers[${index}]`, errors);
|
|
75
|
+
nonEmptyArray(module?.detection?.checks, `${at}.detection.checks`, errors);
|
|
76
|
+
for (const [index, check] of (module?.detection?.checks || []).entries()) validateCheck(check, `${at}.detection.checks[${index}]`, errors);
|
|
77
|
+
array(module?.detection?.exceptions, `${at}.detection.exceptions`, errors);
|
|
78
|
+
for (const [index, exception] of (module?.detection?.exceptions || []).entries()) validateException(exception, `${at}.detection.exceptions[${index}]`, errors);
|
|
79
|
+
|
|
80
|
+
if (module?.schema_version === "1.1.0") validateAssurance(module?.assurance, module, `${at}.assurance`, errors);
|
|
81
|
+
else if (module?.assurance !== undefined) validateAssurance(module.assurance, module, `${at}.assurance`, errors);
|
|
82
|
+
|
|
83
|
+
stringArray(module?.incident_refs, `${at}.incident_refs`, errors, INCIDENT_ID, 128, true);
|
|
84
|
+
object(module?.provenance, `${at}.provenance`, errors);
|
|
85
|
+
exactKeys(module?.provenance, ["created_at", "created_by", "generation_method", "model", "prompt_digest", "source_digests"], `${at}.provenance`, errors);
|
|
86
|
+
dateTime(module?.provenance?.created_at, `${at}.provenance.created_at`, errors);
|
|
87
|
+
boundedString(module?.provenance?.created_by, 1, 200, `${at}.provenance.created_by`, errors);
|
|
88
|
+
oneOf(module?.provenance?.generation_method, new Set(["human", "ai", "hybrid", "imported"]), `${at}.provenance.generation_method`, errors);
|
|
89
|
+
nullableString(module?.provenance?.model, 200, `${at}.provenance.model`, errors);
|
|
90
|
+
nullablePattern(module?.provenance?.prompt_digest, /^(sha256:[a-f0-9]{64})?$/, `${at}.provenance.prompt_digest`, errors);
|
|
91
|
+
stringArray(module?.provenance?.source_digests, `${at}.provenance.source_digests`, errors, DIGEST, 71, true);
|
|
92
|
+
|
|
93
|
+
object(module?.evaluation, `${at}.evaluation`, errors);
|
|
94
|
+
exactKeys(module?.evaluation, ["status", "true_positives", "false_positives", "false_negatives", "evaluated_at", "benchmark_digest"], `${at}.evaluation`, errors);
|
|
95
|
+
oneOf(module?.evaluation?.status, new Set(["not_evaluated", "synthetic_only", "historical_replay", "prospective"]), `${at}.evaluation.status`, errors);
|
|
96
|
+
nonNegativeInteger(module?.evaluation?.true_positives, `${at}.evaluation.true_positives`, errors);
|
|
97
|
+
nonNegativeInteger(module?.evaluation?.false_positives, `${at}.evaluation.false_positives`, errors);
|
|
98
|
+
nonNegativeInteger(module?.evaluation?.false_negatives, `${at}.evaluation.false_negatives`, errors);
|
|
99
|
+
nullableDateTime(module?.evaluation?.evaluated_at, `${at}.evaluation.evaluated_at`, errors);
|
|
100
|
+
nullablePattern(module?.evaluation?.benchmark_digest, /^(sha256:[a-f0-9]{64})?$/, `${at}.evaluation.benchmark_digest`, errors);
|
|
101
|
+
|
|
102
|
+
object(module?.compatibility, `${at}.compatibility`, errors);
|
|
103
|
+
exactKeys(module?.compatibility, ["min_cli_version", "checker_api_version"], `${at}.compatibility`, errors);
|
|
104
|
+
stringPattern(module?.compatibility?.min_cli_version, SEMVER, `${at}.compatibility.min_cli_version`, errors);
|
|
105
|
+
oneOf(module?.compatibility?.checker_api_version, new Set(["1.0.0", "1.1.0"]), `${at}.compatibility.checker_api_version`, errors);
|
|
106
|
+
if (SEMVER.test(cliVersion || "") && SEMVER.test(module?.compatibility?.min_cli_version || "") && compareSemver(cliVersion, module.compatibility.min_cli_version) < 0) {
|
|
107
|
+
errors.push(`${at} requires CLI ${module.compatibility.min_cli_version}, current ${cliVersion}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function validateAssurance(value, module, at, errors) {
|
|
112
|
+
object(value, at, errors);
|
|
113
|
+
exactKeys(value, ["state_models", "stressors", "strengths", "scenarios"], at, errors);
|
|
114
|
+
for (const key of ["state_models", "stressors", "strengths", "scenarios"]) nonEmptyArray(value?.[key], `${at}.${key}`, errors);
|
|
115
|
+
const checkIds = new Set((module?.detection?.checks || []).map((check) => check?.id));
|
|
116
|
+
const stressorIds = new Set();
|
|
117
|
+
const strengthIds = new Set();
|
|
118
|
+
const scenarioIds = new Set();
|
|
119
|
+
const modelIds = new Set();
|
|
120
|
+
|
|
121
|
+
for (const [index, stressor] of (value?.stressors || []).entries()) {
|
|
122
|
+
const path = `${at}.stressors[${index}]`;
|
|
123
|
+
object(stressor, path, errors);
|
|
124
|
+
exactKeys(stressor, ["id", "title", "description", "source", "activation_check_refs"], path, errors);
|
|
125
|
+
assuranceId(stressor?.id, `${path}.id`, stressorIds, errors);
|
|
126
|
+
boundedString(stressor?.title, 1, 200, `${path}.title`, errors);
|
|
127
|
+
boundedString(stressor?.description, 1, 1000, `${path}.description`, errors);
|
|
128
|
+
oneOf(stressor?.source, new Set(["historical_incident", "derived", "human"]), `${path}.source`, errors);
|
|
129
|
+
checkReferenceArray(stressor?.activation_check_refs, `${path}.activation_check_refs`, checkIds, errors, true);
|
|
130
|
+
}
|
|
131
|
+
for (const [index, strength] of (value?.strengths || []).entries()) {
|
|
132
|
+
const path = `${at}.strengths[${index}]`;
|
|
133
|
+
object(strength, path, errors);
|
|
134
|
+
exactKeys(strength, ["id", "title", "description", "kind", "status", "check_refs"], path, errors);
|
|
135
|
+
assuranceId(strength?.id, `${path}.id`, strengthIds, errors);
|
|
136
|
+
boundedString(strength?.title, 1, 200, `${path}.title`, errors);
|
|
137
|
+
boundedString(strength?.description, 1, 1000, `${path}.description`, errors);
|
|
138
|
+
oneOf(strength?.kind, STRENGTH_KINDS, `${path}.kind`, errors);
|
|
139
|
+
oneOf(strength?.status, STRENGTH_STATUSES, `${path}.status`, errors);
|
|
140
|
+
checkReferenceArray(strength?.check_refs, `${path}.check_refs`, checkIds, errors, true);
|
|
141
|
+
}
|
|
142
|
+
for (const [index, scenario] of (value?.scenarios || []).entries()) {
|
|
143
|
+
const path = `${at}.scenarios[${index}]`;
|
|
144
|
+
object(scenario, path, errors);
|
|
145
|
+
exactKeys(scenario, ["id", "title", "state_model_id", "transition_id", "stressor_refs", "expected_strength_refs", "check_refs", "expected_outcome", "execution_requirement"], path, errors);
|
|
146
|
+
assuranceId(scenario?.id, `${path}.id`, scenarioIds, errors);
|
|
147
|
+
boundedString(scenario?.title, 1, 200, `${path}.title`, errors);
|
|
148
|
+
stringPattern(scenario?.state_model_id, ASSURANCE_ID, `${path}.state_model_id`, errors);
|
|
149
|
+
stringPattern(scenario?.transition_id, ASSURANCE_ID, `${path}.transition_id`, errors);
|
|
150
|
+
referenceArray(scenario?.stressor_refs, `${path}.stressor_refs`, stressorIds, errors, true);
|
|
151
|
+
referenceArray(scenario?.expected_strength_refs, `${path}.expected_strength_refs`, strengthIds, errors, true);
|
|
152
|
+
checkReferenceArray(scenario?.check_refs, `${path}.check_refs`, checkIds, errors, true);
|
|
153
|
+
boundedString(scenario?.expected_outcome, 1, 1000, `${path}.expected_outcome`, errors);
|
|
154
|
+
oneOf(scenario?.execution_requirement, SCENARIO_REQUIREMENTS, `${path}.execution_requirement`, errors);
|
|
155
|
+
}
|
|
156
|
+
for (const [index, model] of (value?.state_models || []).entries()) {
|
|
157
|
+
const path = `${at}.state_models[${index}]`;
|
|
158
|
+
object(model, path, errors);
|
|
159
|
+
exactKeys(model, ["id", "version", "subject", "initial_state", "states", "transitions", "forbidden_states", "forbidden_transitions"], path, errors);
|
|
160
|
+
assuranceId(model?.id, `${path}.id`, modelIds, errors);
|
|
161
|
+
stringPattern(model?.version, SEMVER, `${path}.version`, errors);
|
|
162
|
+
boundedString(model?.subject, 1, 500, `${path}.subject`, errors);
|
|
163
|
+
stringPattern(model?.initial_state, ASSURANCE_ID, `${path}.initial_state`, errors);
|
|
164
|
+
nonEmptyArray(model?.states, `${path}.states`, errors);
|
|
165
|
+
nonEmptyArray(model?.transitions, `${path}.transitions`, errors);
|
|
166
|
+
array(model?.forbidden_states, `${path}.forbidden_states`, errors);
|
|
167
|
+
array(model?.forbidden_transitions, `${path}.forbidden_transitions`, errors);
|
|
168
|
+
const stateIds = new Set();
|
|
169
|
+
const invariantIds = new Set();
|
|
170
|
+
const transitionIds = new Set();
|
|
171
|
+
for (const [stateIndex, state] of (model?.states || []).entries()) {
|
|
172
|
+
const statePath = `${path}.states[${stateIndex}]`;
|
|
173
|
+
object(state, statePath, errors);
|
|
174
|
+
exactKeys(state, ["id", "label", "terminal", "invariants"], statePath, errors);
|
|
175
|
+
assuranceId(state?.id, `${statePath}.id`, stateIds, errors);
|
|
176
|
+
boundedString(state?.label, 1, 200, `${statePath}.label`, errors);
|
|
177
|
+
if (typeof state?.terminal !== "boolean") errors.push(`${statePath}.terminal must be a boolean`);
|
|
178
|
+
array(state?.invariants, `${statePath}.invariants`, errors);
|
|
179
|
+
for (const [invIndex, invariant] of (state?.invariants || []).entries()) {
|
|
180
|
+
const invPath = `${statePath}.invariants[${invIndex}]`;
|
|
181
|
+
object(invariant, invPath, errors);
|
|
182
|
+
exactKeys(invariant, ["id", "statement", "check_refs"], invPath, errors);
|
|
183
|
+
assuranceId(invariant?.id, `${invPath}.id`, invariantIds, errors);
|
|
184
|
+
boundedString(invariant?.statement, 1, 1000, `${invPath}.statement`, errors);
|
|
185
|
+
checkReferenceArray(invariant?.check_refs, `${invPath}.check_refs`, checkIds, errors, true);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (!stateIds.has(model?.initial_state)) errors.push(`${path}.initial_state must reference a declared state`);
|
|
189
|
+
for (const [transitionIndex, transition] of (model?.transitions || []).entries()) {
|
|
190
|
+
const transitionPath = `${path}.transitions[${transitionIndex}]`;
|
|
191
|
+
object(transition, transitionPath, errors);
|
|
192
|
+
exactKeys(transition, ["id", "from", "event", "to", "preconditions", "invariant_refs", "stressor_refs", "strength_refs", "scenario_refs"], transitionPath, errors);
|
|
193
|
+
assuranceId(transition?.id, `${transitionPath}.id`, transitionIds, errors);
|
|
194
|
+
if (!stateIds.has(transition?.from)) errors.push(`${transitionPath}.from must reference a declared state`);
|
|
195
|
+
if (!stateIds.has(transition?.to)) errors.push(`${transitionPath}.to must reference a declared state`);
|
|
196
|
+
boundedString(transition?.event, 1, 200, `${transitionPath}.event`, errors);
|
|
197
|
+
array(transition?.preconditions, `${transitionPath}.preconditions`, errors);
|
|
198
|
+
for (const [preIndex, precondition] of (transition?.preconditions || []).entries()) {
|
|
199
|
+
const prePath = `${transitionPath}.preconditions[${preIndex}]`;
|
|
200
|
+
object(precondition, prePath, errors);
|
|
201
|
+
exactKeys(precondition, ["id", "statement", "check_refs"], prePath, errors);
|
|
202
|
+
stringPattern(precondition?.id, ASSURANCE_ID, `${prePath}.id`, errors);
|
|
203
|
+
boundedString(precondition?.statement, 1, 1000, `${prePath}.statement`, errors);
|
|
204
|
+
checkReferenceArray(precondition?.check_refs, `${prePath}.check_refs`, checkIds, errors, true);
|
|
205
|
+
}
|
|
206
|
+
referenceArray(transition?.invariant_refs, `${transitionPath}.invariant_refs`, invariantIds, errors, false);
|
|
207
|
+
referenceArray(transition?.stressor_refs, `${transitionPath}.stressor_refs`, stressorIds, errors, true);
|
|
208
|
+
referenceArray(transition?.strength_refs, `${transitionPath}.strength_refs`, strengthIds, errors, true);
|
|
209
|
+
referenceArray(transition?.scenario_refs, `${transitionPath}.scenario_refs`, scenarioIds, errors, true);
|
|
210
|
+
}
|
|
211
|
+
for (const [forbiddenIndex, forbidden] of (model?.forbidden_states || []).entries()) {
|
|
212
|
+
const forbiddenPath = `${path}.forbidden_states[${forbiddenIndex}]`;
|
|
213
|
+
object(forbidden, forbiddenPath, errors);
|
|
214
|
+
exactKeys(forbidden, ["id", "statement", "check_refs"], forbiddenPath, errors);
|
|
215
|
+
stringPattern(forbidden?.id, ASSURANCE_ID, `${forbiddenPath}.id`, errors);
|
|
216
|
+
boundedString(forbidden?.statement, 1, 1000, `${forbiddenPath}.statement`, errors);
|
|
217
|
+
checkReferenceArray(forbidden?.check_refs, `${forbiddenPath}.check_refs`, checkIds, errors, true);
|
|
218
|
+
}
|
|
219
|
+
for (const [forbiddenIndex, forbidden] of (model?.forbidden_transitions || []).entries()) {
|
|
220
|
+
const forbiddenPath = `${path}.forbidden_transitions[${forbiddenIndex}]`;
|
|
221
|
+
object(forbidden, forbiddenPath, errors);
|
|
222
|
+
exactKeys(forbidden, ["from", "to", "reason"], forbiddenPath, errors);
|
|
223
|
+
if (!stateIds.has(forbidden?.from)) errors.push(`${forbiddenPath}.from must reference a declared state`);
|
|
224
|
+
if (!stateIds.has(forbidden?.to)) errors.push(`${forbiddenPath}.to must reference a declared state`);
|
|
225
|
+
boundedString(forbidden?.reason, 1, 1000, `${forbiddenPath}.reason`, errors);
|
|
226
|
+
}
|
|
227
|
+
for (const scenario of value?.scenarios || []) {
|
|
228
|
+
if (scenario?.state_model_id === model?.id && !transitionIds.has(scenario?.transition_id)) errors.push(`${at}.scenarios transition must exist in ${model.id}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
for (const scenario of value?.scenarios || []) if (!modelIds.has(scenario?.state_model_id)) errors.push(`${at}.scenarios state_model_id must reference a declared model`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function assuranceId(value, at, seen, errors) {
|
|
235
|
+
stringPattern(value, ASSURANCE_ID, at, errors);
|
|
236
|
+
if (seen.has(value)) errors.push(`${at} must be unique`);
|
|
237
|
+
seen.add(value);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function referenceArray(value, at, declared, errors, required) {
|
|
241
|
+
stringArray(value, at, errors, ASSURANCE_ID, 128, required);
|
|
242
|
+
for (const item of value || []) if (!declared.has(item)) errors.push(`${at} references unknown ${item}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function checkReferenceArray(value, at, declared, errors, required) {
|
|
246
|
+
stringArray(value, at, errors, RULE_ID, 128, required);
|
|
247
|
+
for (const item of value || []) if (!declared.has(item)) errors.push(`${at} references unknown ${item}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function validateMatcher(value, at, errors) {
|
|
251
|
+
object(value, at, errors);
|
|
252
|
+
exactKeys(value, ["type", "paths", "pattern", "side"], at, errors);
|
|
253
|
+
oneOf(value?.type, MATCHERS, `${at}.type`, errors);
|
|
254
|
+
stringArray(value?.paths, `${at}.paths`, errors, null, 500, true);
|
|
255
|
+
nullableString(value?.pattern, 2000, `${at}.pattern`, errors);
|
|
256
|
+
if (["diff_regex", "content_regex", "reference"].includes(value?.type) && !value?.pattern) errors.push(`${at}.pattern is required for ${value.type}`);
|
|
257
|
+
if (value?.side !== undefined) oneOf(value.side, new Set(["added", "removed", "either"]), `${at}.side`, errors);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function validateCheck(value, at, errors) {
|
|
261
|
+
object(value, at, errors);
|
|
262
|
+
exactKeys(value, ["id", "type", "message", "paths", "pattern", "expect", "definition_paths"], at, errors);
|
|
263
|
+
stringPattern(value?.id, RULE_ID, `${at}.id`, errors);
|
|
264
|
+
oneOf(value?.type, CHECKERS, `${at}.type`, errors);
|
|
265
|
+
boundedString(value?.message, 1, 1000, `${at}.message`, errors);
|
|
266
|
+
stringArray(value?.paths, `${at}.paths`, errors, null, 500, false);
|
|
267
|
+
nullableString(value?.pattern, 2000, `${at}.pattern`, errors);
|
|
268
|
+
if (["diff_regex", "content_regex", "reference_definition"].includes(value?.type) && !value?.pattern) errors.push(`${at}.pattern is required for ${value.type}`);
|
|
269
|
+
if (value?.expect !== undefined) oneOf(value.expect, new Set(["present", "absent", "defined"]), `${at}.expect`, errors);
|
|
270
|
+
stringArray(value?.definition_paths, `${at}.definition_paths`, errors, null, 500, false);
|
|
271
|
+
if (value?.type === "reference_definition" && (!value?.definition_paths?.length || value?.expect !== "defined")) errors.push(`${at} reference_definition requires definition_paths and expect=defined`);
|
|
272
|
+
if (value?.type === "path_presence" && !["present", "absent"].includes(value?.expect)) errors.push(`${at} path_presence requires expect=present|absent`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function validateException(value, at, errors) {
|
|
276
|
+
object(value, at, errors);
|
|
277
|
+
exactKeys(value, ["id", "reason", "paths", "expires_at"], at, errors);
|
|
278
|
+
stringPattern(value?.id, RULE_ID, `${at}.id`, errors);
|
|
279
|
+
boundedString(value?.reason, 1, 1000, `${at}.reason`, errors);
|
|
280
|
+
stringArray(value?.paths, `${at}.paths`, errors, null, 500, true);
|
|
281
|
+
nullableDateTime(value?.expires_at, `${at}.expires_at`, errors);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function compareSemver(left, right) {
|
|
285
|
+
const a = left.split(".").map(Number);
|
|
286
|
+
const b = right.split(".").map(Number);
|
|
287
|
+
for (let index = 0; index < 3; index += 1) if (a[index] !== b[index]) return a[index] - b[index];
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function exactKeys(value, allowed, at, errors) { if (!value || typeof value !== "object" || Array.isArray(value)) return; const set = new Set(allowed); for (const key of Object.keys(value)) if (!set.has(key)) errors.push(`${at}.${key} is not allowed`); }
|
|
292
|
+
function object(value, at, errors) { if (!value || typeof value !== "object" || Array.isArray(value)) errors.push(`${at} must be an object`); }
|
|
293
|
+
function array(value, at, errors) { if (value !== undefined && !Array.isArray(value)) errors.push(`${at} must be an array`); }
|
|
294
|
+
function nonEmptyArray(value, at, errors) { if (!Array.isArray(value) || value.length === 0) errors.push(`${at} must be a non-empty array`); }
|
|
295
|
+
function equal(value, expected, at, errors) { if (value !== expected) errors.push(`${at} must equal ${expected}`); }
|
|
296
|
+
function oneOf(value, allowed, at, errors) { if (!allowed.has(value)) errors.push(`${at} has an unsupported value`); }
|
|
297
|
+
function boundedString(value, min, max, at, errors) { if (typeof value !== "string" || value.length < min || value.length > max) errors.push(`${at} must be a string of length ${min}-${max}`); }
|
|
298
|
+
function nullableString(value, max, at, errors) { if (value !== undefined && value !== null && (typeof value !== "string" || value.length > max)) errors.push(`${at} must be null or a string up to ${max} characters`); }
|
|
299
|
+
function stringPattern(value, pattern, at, errors) { if (typeof value !== "string" || !pattern.test(value)) errors.push(`${at} has an invalid format`); }
|
|
300
|
+
function nullablePattern(value, pattern, at, errors) { if (value !== undefined && value !== null && (typeof value !== "string" || !pattern.test(value))) errors.push(`${at} has an invalid format`); }
|
|
301
|
+
function dateTime(value, at, errors) { if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) errors.push(`${at} must be a date-time`); }
|
|
302
|
+
function nullableDateTime(value, at, errors) { if (value !== undefined && value !== null) dateTime(value, at, errors); }
|
|
303
|
+
function nonNegativeInteger(value, at, errors) { if (!Number.isInteger(value) || value < 0) errors.push(`${at} must be a non-negative integer`); }
|
|
304
|
+
function stringArray(value, at, errors, pattern, max, required) { if (value === undefined && !required) return; if (!Array.isArray(value) || (required && value.length === 0)) { errors.push(`${at} must be ${required ? "a non-empty" : "an"} array`); return; } const seen = new Set(); for (const item of value) { if (typeof item !== "string" || item.length === 0 || item.length > max || (pattern && !pattern.test(item))) errors.push(`${at} contains an invalid string`); if (seen.has(item)) errors.push(`${at} must contain unique values`); seen.add(item); } }
|
|
305
|
+
function enumArray(value, at, errors, allowed, required) { if (!Array.isArray(value) || (required && value.length === 0)) { errors.push(`${at} must be a non-empty array`); return; } const seen = new Set(); for (const item of value) { if (!allowed.has(item)) errors.push(`${at} contains an unsupported value`); if (seen.has(item)) errors.push(`${at} must contain unique values`); seen.add(item); } }
|