@neuroverseos/governance 0.3.1 → 0.3.4
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/.well-known/ai-plugin.json +34 -9
- package/AGENTS.md +72 -24
- package/README.md +343 -248
- package/dist/adapters/autoresearch.cjs +1345 -0
- package/dist/adapters/autoresearch.d.cts +111 -0
- package/dist/adapters/autoresearch.d.ts +111 -0
- package/dist/adapters/autoresearch.js +12 -0
- package/dist/adapters/deep-agents.cjs +1528 -0
- package/dist/adapters/deep-agents.d.cts +181 -0
- package/dist/adapters/deep-agents.d.ts +181 -0
- package/dist/adapters/deep-agents.js +17 -0
- package/dist/adapters/express.cjs +1253 -0
- package/dist/adapters/express.d.cts +66 -0
- package/dist/adapters/express.d.ts +66 -0
- package/dist/adapters/express.js +12 -0
- package/dist/adapters/index.cjs +2112 -0
- package/dist/adapters/index.d.cts +8 -0
- package/dist/adapters/index.d.ts +8 -0
- package/dist/adapters/index.js +68 -0
- package/dist/adapters/langchain.cjs +1315 -0
- package/dist/adapters/langchain.d.cts +89 -0
- package/dist/adapters/langchain.d.ts +89 -0
- package/dist/adapters/langchain.js +17 -0
- package/dist/adapters/openai.cjs +1345 -0
- package/dist/adapters/openai.d.cts +99 -0
- package/dist/adapters/openai.d.ts +99 -0
- package/dist/adapters/openai.js +17 -0
- package/dist/adapters/openclaw.cjs +1337 -0
- package/dist/adapters/openclaw.d.cts +99 -0
- package/dist/adapters/openclaw.d.ts +99 -0
- package/dist/adapters/openclaw.js +17 -0
- package/dist/add-ROOZLU62.js +314 -0
- package/dist/behavioral-MJO34S6Q.js +118 -0
- package/dist/bootstrap-CQRZVOXK.js +116 -0
- package/dist/bootstrap-emitter-Q7UIJZ2O.js +7 -0
- package/dist/bootstrap-parser-EEF36XDU.js +7 -0
- package/dist/browser.global.js +941 -0
- package/dist/build-ZHPMX5AZ.js +342 -0
- package/dist/chunk-3WQLXYTP.js +91 -0
- package/dist/chunk-4FLICVVA.js +119 -0
- package/dist/chunk-4NGDRRQH.js +10 -0
- package/dist/chunk-5TPFNWRU.js +215 -0
- package/dist/chunk-5U2MQO5P.js +57 -0
- package/dist/chunk-6CZSKEY5.js +164 -0
- package/dist/chunk-7P3S7MAY.js +1090 -0
- package/dist/chunk-A5W4GNQO.js +130 -0
- package/dist/chunk-A7GKPPU7.js +226 -0
- package/dist/chunk-AKW5YVCE.js +96 -0
- package/dist/chunk-B6OXJLJ5.js +622 -0
- package/dist/chunk-BNKJPUPQ.js +113 -0
- package/dist/chunk-BQZMOEML.js +43 -0
- package/dist/chunk-CNSO6XW5.js +207 -0
- package/dist/chunk-CTZHONLA.js +135 -0
- package/dist/chunk-D2UCV5AK.js +326 -0
- package/dist/chunk-EMQDLDAF.js +458 -0
- package/dist/chunk-F66BVUYB.js +340 -0
- package/dist/chunk-FMSTRBBS.js +17 -0
- package/dist/chunk-G7DJ6VOD.js +101 -0
- package/dist/chunk-I3RRAYK2.js +11 -0
- package/dist/chunk-INWQHLPS.js +47 -0
- package/dist/chunk-IS4WUH6Y.js +363 -0
- package/dist/chunk-O5ABKEA7.js +304 -0
- package/dist/chunk-OT6PXH54.js +61 -0
- package/dist/chunk-PVTQQS3Y.js +186 -0
- package/dist/chunk-QLPTHTVB.js +253 -0
- package/dist/chunk-QWGCMQQD.js +16 -0
- package/dist/chunk-QXBFT7NI.js +201 -0
- package/dist/chunk-TG6SEF24.js +246 -0
- package/dist/chunk-U6U7EJZL.js +177 -0
- package/dist/chunk-VXHSMA3I.js +166 -0
- package/dist/chunk-W7LLXRGY.js +830 -0
- package/dist/chunk-YEKMVDWK.js +624 -0
- package/dist/chunk-ZJTDUCC2.js +194 -0
- package/dist/chunk-ZWI3NIXK.js +314 -0
- package/dist/cli/neuroverse.cjs +14379 -0
- package/dist/cli/neuroverse.d.cts +1 -0
- package/dist/cli/neuroverse.d.ts +1 -0
- package/dist/cli/neuroverse.js +227 -0
- package/dist/cli/plan.cjs +2439 -0
- package/dist/cli/plan.d.cts +20 -0
- package/dist/cli/plan.d.ts +20 -0
- package/dist/cli/plan.js +353 -0
- package/dist/cli/run.cjs +2001 -0
- package/dist/cli/run.d.cts +20 -0
- package/dist/cli/run.d.ts +20 -0
- package/dist/cli/run.js +143 -0
- package/dist/configure-ai-5MP5DWTT.js +134 -0
- package/dist/decision-flow-M63D47LO.js +61 -0
- package/dist/demo-G43RLCPK.js +469 -0
- package/dist/derive-LMDUTXDD.js +154 -0
- package/dist/doctor-6BC6X2VO.js +173 -0
- package/dist/equity-penalties-SG5IZQ7I.js +244 -0
- package/dist/explain-RHBU2GBR.js +51 -0
- package/dist/guard-AEEJNWLD.js +126 -0
- package/dist/guard-contract-B7lplwm9.d.cts +837 -0
- package/dist/guard-contract-B7lplwm9.d.ts +837 -0
- package/dist/guard-engine-PNR6MHCM.js +10 -0
- package/dist/impact-3XVDSCBU.js +59 -0
- package/dist/improve-TQP4ECSY.js +66 -0
- package/dist/index.cjs +7738 -0
- package/dist/index.d.cts +2350 -0
- package/dist/index.d.ts +2350 -0
- package/dist/index.js +479 -0
- package/dist/infer-world-IFXCACJ5.js +543 -0
- package/dist/init-FYPV4SST.js +144 -0
- package/dist/init-world-TI7ARHBT.js +223 -0
- package/dist/mcp-server-5Y3ZM7TV.js +13 -0
- package/dist/model-adapter-VXEKB4LS.js +11 -0
- package/dist/playground-VZBNPPBO.js +560 -0
- package/dist/redteam-MZPZD3EF.js +357 -0
- package/dist/session-JYOARW54.js +15 -0
- package/dist/shared-7RLUHNMU.js +16 -0
- package/dist/shared-C_zpdvBm.d.cts +60 -0
- package/dist/shared-Cf7yxx4-.d.ts +60 -0
- package/dist/simulate-LJXYBC6M.js +83 -0
- package/dist/test-BOOR4A5F.js +217 -0
- package/dist/trace-PKV4KX56.js +166 -0
- package/dist/validate-RALX7CZS.js +81 -0
- package/dist/validate-engine-7ZXFVGF2.js +7 -0
- package/dist/viz/assets/index-B8SaeJZZ.js +23 -0
- package/dist/viz/index.html +23 -0
- package/dist/world-BIP4GZBZ.js +376 -0
- package/dist/world-loader-Y6HMQH2D.js +13 -0
- package/dist/worlds/autoresearch.nv-world.md +230 -0
- package/dist/worlds/coding-agent.nv-world.md +211 -0
- package/dist/worlds/derivation-world.nv-world.md +278 -0
- package/dist/worlds/research-agent.nv-world.md +169 -0
- package/dist/worlds/social-media.nv-world.md +198 -0
- package/dist/worlds/trading-agent.nv-world.md +218 -0
- package/examples/social-media-sim/bridge.py +209 -0
- package/examples/social-media-sim/simulation.py +927 -0
- package/package.json +16 -3
- package/simulate.html +4 -336
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
// src/engine/validate-engine.ts
|
|
2
|
+
function validateWorld(world, mode = "standard") {
|
|
3
|
+
const startTime = performance.now();
|
|
4
|
+
const findings = [];
|
|
5
|
+
checkCompleteness(world, findings);
|
|
6
|
+
checkReferentialIntegrity(world, findings);
|
|
7
|
+
checkGuardCoverage(world, findings);
|
|
8
|
+
checkSemanticCoverage(world, findings);
|
|
9
|
+
checkContradictions(world, findings);
|
|
10
|
+
checkGuardShadows(world, findings);
|
|
11
|
+
checkFailClosedSurfaces(world, findings);
|
|
12
|
+
checkReachability(world, findings);
|
|
13
|
+
checkStateCoverage(world, findings);
|
|
14
|
+
checkOrphans(world, findings);
|
|
15
|
+
checkSchemaViolations(world, findings);
|
|
16
|
+
const governanceCategories = /* @__PURE__ */ new Set([
|
|
17
|
+
"guard-coverage",
|
|
18
|
+
"contradiction",
|
|
19
|
+
"semantic-tension",
|
|
20
|
+
"orphan"
|
|
21
|
+
]);
|
|
22
|
+
if (mode === "dev") {
|
|
23
|
+
for (const f of findings) {
|
|
24
|
+
if (governanceCategories.has(f.category) && f.severity === "warning") {
|
|
25
|
+
f.severity = "info";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} else if (mode === "strict") {
|
|
29
|
+
for (const f of findings) {
|
|
30
|
+
if (governanceCategories.has(f.category) && f.severity === "info") {
|
|
31
|
+
f.severity = "warning";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
36
|
+
findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
37
|
+
const errors = findings.filter((f) => f.severity === "error").length;
|
|
38
|
+
const warnings = findings.filter((f) => f.severity === "warning").length;
|
|
39
|
+
const info = findings.filter((f) => f.severity === "info").length;
|
|
40
|
+
const completenessScore = computeCompletenessScore(world);
|
|
41
|
+
const invariantCoverage = computeInvariantCoverage(world);
|
|
42
|
+
const governanceHealth = computeGovernanceHealth(world, findings);
|
|
43
|
+
const summary = {
|
|
44
|
+
errors,
|
|
45
|
+
warnings,
|
|
46
|
+
info,
|
|
47
|
+
completenessScore,
|
|
48
|
+
invariantCoverage,
|
|
49
|
+
canRun: errors === 0,
|
|
50
|
+
isHealthy: errors === 0 && warnings === 0,
|
|
51
|
+
governanceHealth
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
worldId: world.world.world_id,
|
|
55
|
+
worldName: world.world.name,
|
|
56
|
+
worldVersion: world.world.version,
|
|
57
|
+
validatedAt: Date.now(),
|
|
58
|
+
durationMs: performance.now() - startTime,
|
|
59
|
+
validationMode: mode,
|
|
60
|
+
summary,
|
|
61
|
+
findings
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function checkCompleteness(world, findings) {
|
|
65
|
+
if (!world.world?.world_id) {
|
|
66
|
+
findings.push(finding("missing-world-id", "World identity is missing world_id", "error", "completeness", ["world.json"]));
|
|
67
|
+
}
|
|
68
|
+
if (!world.world?.name) {
|
|
69
|
+
findings.push(finding("missing-world-name", "World identity is missing name", "error", "completeness", ["world.json"]));
|
|
70
|
+
}
|
|
71
|
+
if (!world.world?.thesis) {
|
|
72
|
+
findings.push(finding("missing-thesis", "World has no thesis \u2014 there is nothing to simulate", "error", "completeness", ["world.json"]));
|
|
73
|
+
}
|
|
74
|
+
if (!world.invariants || world.invariants.length === 0) {
|
|
75
|
+
findings.push(finding("missing-invariants", "No invariants declared \u2014 world has no structural constraints", "error", "completeness", ["invariants.json"]));
|
|
76
|
+
}
|
|
77
|
+
if (!world.stateSchema?.variables || Object.keys(world.stateSchema.variables).length === 0) {
|
|
78
|
+
findings.push(finding("missing-state-schema", "No state variables declared \u2014 nothing to simulate", "error", "completeness", ["state-schema.json"]));
|
|
79
|
+
}
|
|
80
|
+
if (!world.rules || world.rules.length === 0) {
|
|
81
|
+
findings.push(finding("missing-rules", "No rules declared \u2014 world has no causal mechanics", "error", "completeness", ["rules/"]));
|
|
82
|
+
}
|
|
83
|
+
if (!world.gates?.viability_classification || world.gates.viability_classification.length === 0) {
|
|
84
|
+
findings.push(finding("missing-gates", "No viability gates declared \u2014 cannot classify outcomes", "error", "completeness", ["gates.json"]));
|
|
85
|
+
}
|
|
86
|
+
if (!world.outcomes?.computed_outcomes || world.outcomes.computed_outcomes.length === 0) {
|
|
87
|
+
findings.push(finding("missing-outcomes", "No computed outcomes declared \u2014 nothing to display", "warning", "completeness", ["outcomes.json"]));
|
|
88
|
+
}
|
|
89
|
+
if (!world.assumptions?.profiles || Object.keys(world.assumptions.profiles).length === 0) {
|
|
90
|
+
findings.push(finding("missing-assumptions", "No assumption profiles declared \u2014 cannot compare scenarios", "warning", "completeness", ["assumptions.json"]));
|
|
91
|
+
}
|
|
92
|
+
if (!world.metadata) {
|
|
93
|
+
findings.push(finding("missing-metadata", "No metadata block \u2014 world has no version history", "warning", "completeness", ["metadata.json"]));
|
|
94
|
+
}
|
|
95
|
+
if (!world.guards) {
|
|
96
|
+
findings.push(finding("no-guards", "No guards declared \u2014 Action Space enforcement unavailable", "info", "completeness", ["guards.json"]));
|
|
97
|
+
}
|
|
98
|
+
if (!world.roles) {
|
|
99
|
+
findings.push(finding("no-roles", "No roles declared \u2014 multi-agent governance unavailable", "info", "completeness", ["roles.json"]));
|
|
100
|
+
}
|
|
101
|
+
if (!world.kernel) {
|
|
102
|
+
findings.push(finding("no-kernel", "No kernel config \u2014 Thinking Space governance unavailable", "info", "completeness", ["kernel.json"]));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function checkReferentialIntegrity(world, findings) {
|
|
106
|
+
if (!world.rules || !world.stateSchema?.variables) return;
|
|
107
|
+
const declaredVars = new Set(Object.keys(world.stateSchema.variables));
|
|
108
|
+
const declaredOutcomes = new Set(
|
|
109
|
+
(world.outcomes?.computed_outcomes ?? []).map((o) => o.id)
|
|
110
|
+
);
|
|
111
|
+
const declaredAssumptionParams = new Set(
|
|
112
|
+
Object.keys(world.assumptions?.parameter_definitions ?? {})
|
|
113
|
+
);
|
|
114
|
+
const allDeclared = /* @__PURE__ */ new Set([...declaredVars, ...declaredOutcomes]);
|
|
115
|
+
for (const rule of world.rules) {
|
|
116
|
+
for (const trigger of rule.triggers) {
|
|
117
|
+
if (trigger.source === "state" && !allDeclared.has(trigger.field)) {
|
|
118
|
+
findings.push(finding(
|
|
119
|
+
`undeclared-trigger-${rule.id}-${trigger.field}`,
|
|
120
|
+
`Rule "${rule.id}" trigger references undeclared state variable "${trigger.field}"`,
|
|
121
|
+
"error",
|
|
122
|
+
"referential-integrity",
|
|
123
|
+
["rules/", "state-schema.json"],
|
|
124
|
+
rule.id,
|
|
125
|
+
`Add "${trigger.field}" to state-schema.json variables`
|
|
126
|
+
));
|
|
127
|
+
}
|
|
128
|
+
if (trigger.source === "assumption" && !declaredAssumptionParams.has(trigger.field)) {
|
|
129
|
+
findings.push(finding(
|
|
130
|
+
`undeclared-assumption-trigger-${rule.id}-${trigger.field}`,
|
|
131
|
+
`Rule "${rule.id}" trigger references undeclared assumption parameter "${trigger.field}"`,
|
|
132
|
+
"error",
|
|
133
|
+
"referential-integrity",
|
|
134
|
+
["rules/", "assumptions.json"],
|
|
135
|
+
rule.id,
|
|
136
|
+
`Add "${trigger.field}" to assumptions.json parameter_definitions`
|
|
137
|
+
));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const effect of rule.effects ?? []) {
|
|
141
|
+
if (!allDeclared.has(effect.target)) {
|
|
142
|
+
findings.push(finding(
|
|
143
|
+
`undeclared-effect-${rule.id}-${effect.target}`,
|
|
144
|
+
`Rule "${rule.id}" effect targets undeclared variable "${effect.target}"`,
|
|
145
|
+
"error",
|
|
146
|
+
"referential-integrity",
|
|
147
|
+
["rules/", "state-schema.json", "outcomes.json"],
|
|
148
|
+
rule.id,
|
|
149
|
+
`Add "${effect.target}" to state-schema.json or outcomes.json`
|
|
150
|
+
));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (rule.exclusive_with) {
|
|
154
|
+
const refExists = world.rules.some((r) => r.id === rule.exclusive_with);
|
|
155
|
+
if (!refExists) {
|
|
156
|
+
findings.push(finding(
|
|
157
|
+
`broken-exclusive-${rule.id}`,
|
|
158
|
+
`Rule "${rule.id}" has exclusive_with="${rule.exclusive_with}" but that rule does not exist`,
|
|
159
|
+
"error",
|
|
160
|
+
"referential-integrity",
|
|
161
|
+
["rules/"],
|
|
162
|
+
rule.id
|
|
163
|
+
));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (rule.collapse_check && !allDeclared.has(rule.collapse_check.field)) {
|
|
167
|
+
findings.push(finding(
|
|
168
|
+
`undeclared-collapse-${rule.id}`,
|
|
169
|
+
`Rule "${rule.id}" collapse_check references undeclared field "${rule.collapse_check.field}"`,
|
|
170
|
+
"error",
|
|
171
|
+
"referential-integrity",
|
|
172
|
+
["rules/", "state-schema.json"],
|
|
173
|
+
rule.id
|
|
174
|
+
));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const gate of world.gates?.viability_classification ?? []) {
|
|
178
|
+
if (!allDeclared.has(gate.field)) {
|
|
179
|
+
findings.push(finding(
|
|
180
|
+
`undeclared-gate-field-${gate.status}`,
|
|
181
|
+
`Gate "${gate.status}" references undeclared field "${gate.field}"`,
|
|
182
|
+
"error",
|
|
183
|
+
"referential-integrity",
|
|
184
|
+
["gates.json", "state-schema.json", "outcomes.json"]
|
|
185
|
+
));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function checkGuardCoverage(world, findings) {
|
|
190
|
+
if (!world.invariants || world.invariants.length === 0) return;
|
|
191
|
+
const guards = world.guards?.guards ?? [];
|
|
192
|
+
for (const invariant of world.invariants) {
|
|
193
|
+
if (invariant.enforcement === "prompt") continue;
|
|
194
|
+
const coveringGuard = guards.find(
|
|
195
|
+
(g) => g.invariant_ref === invariant.id && g.immutable
|
|
196
|
+
);
|
|
197
|
+
if (!coveringGuard) {
|
|
198
|
+
findings.push(finding(
|
|
199
|
+
`uncovered-invariant-${invariant.id}`,
|
|
200
|
+
`Invariant "${invariant.id}" has no backing structural guard \u2014 it is declared but not enforced at runtime`,
|
|
201
|
+
"warning",
|
|
202
|
+
"guard-coverage",
|
|
203
|
+
["invariants.json", "guards.json"],
|
|
204
|
+
invariant.id,
|
|
205
|
+
`Add a structural guard with invariant_ref="${invariant.id}" to guards.json`
|
|
206
|
+
));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (const guard of guards) {
|
|
210
|
+
if (guard.invariant_ref) {
|
|
211
|
+
const invariantExists = world.invariants.some((i) => i.id === guard.invariant_ref);
|
|
212
|
+
if (!invariantExists) {
|
|
213
|
+
findings.push(finding(
|
|
214
|
+
`broken-guard-invariant-ref-${guard.id}`,
|
|
215
|
+
`Guard "${guard.id}" references invariant "${guard.invariant_ref}" which does not exist`,
|
|
216
|
+
"error",
|
|
217
|
+
"referential-integrity",
|
|
218
|
+
["guards.json", "invariants.json"],
|
|
219
|
+
guard.id
|
|
220
|
+
));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (world.guards) {
|
|
225
|
+
const vocabKeys = new Set(Object.keys(world.guards.intent_vocabulary));
|
|
226
|
+
for (const guard of guards) {
|
|
227
|
+
for (const patternKey of guard.intent_patterns) {
|
|
228
|
+
if (!vocabKeys.has(patternKey)) {
|
|
229
|
+
findings.push(finding(
|
|
230
|
+
`broken-guard-pattern-${guard.id}-${patternKey}`,
|
|
231
|
+
`Guard "${guard.id}" references intent pattern "${patternKey}" which is not in intent_vocabulary`,
|
|
232
|
+
"error",
|
|
233
|
+
"referential-integrity",
|
|
234
|
+
["guards.json"],
|
|
235
|
+
guard.id
|
|
236
|
+
));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function checkSemanticCoverage(world, findings) {
|
|
243
|
+
if (!world.invariants || world.invariants.length === 0) return;
|
|
244
|
+
const hasGuards = (world.guards?.guards?.length ?? 0) > 0;
|
|
245
|
+
const hasKernel = (world.kernel?.input_boundaries?.forbidden_patterns?.length ?? 0) > 0 || (world.kernel?.output_boundaries?.forbidden_patterns?.length ?? 0) > 0;
|
|
246
|
+
if (!hasGuards && !hasKernel) return;
|
|
247
|
+
const guards = world.guards?.guards ?? [];
|
|
248
|
+
const vocabEntries = world.guards?.intent_vocabulary ?? {};
|
|
249
|
+
const kernelInput = world.kernel?.input_boundaries?.forbidden_patterns ?? [];
|
|
250
|
+
const kernelOutput = world.kernel?.output_boundaries?.forbidden_patterns ?? [];
|
|
251
|
+
const allKernelRules = [...kernelInput, ...kernelOutput];
|
|
252
|
+
const guardSearchTexts = guards.map((g) => {
|
|
253
|
+
const parts = [];
|
|
254
|
+
for (const patternKey of g.intent_patterns) {
|
|
255
|
+
parts.push(patternKey.toLowerCase());
|
|
256
|
+
const vocab = vocabEntries[patternKey];
|
|
257
|
+
if (vocab) {
|
|
258
|
+
parts.push(vocab.label.toLowerCase());
|
|
259
|
+
parts.push(vocab.pattern.toLowerCase());
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
parts.push(g.description.toLowerCase());
|
|
263
|
+
return { guard: g, text: parts.join(" ") };
|
|
264
|
+
});
|
|
265
|
+
const kernelSearchTexts = allKernelRules.map((k) => ({
|
|
266
|
+
rule: k,
|
|
267
|
+
text: `${k.id} ${k.reason} ${k.pattern ?? ""}`.toLowerCase()
|
|
268
|
+
}));
|
|
269
|
+
for (const invariant of world.invariants) {
|
|
270
|
+
if (invariant.enforcement === "prompt") continue;
|
|
271
|
+
const tokens = extractActionTokens(invariant.id, invariant.label);
|
|
272
|
+
if (tokens.length === 0) continue;
|
|
273
|
+
const coveringGuards = guardSearchTexts.filter((gs) => {
|
|
274
|
+
const enabled = gs.guard.immutable || gs.guard.default_enabled !== false;
|
|
275
|
+
if (!enabled) return false;
|
|
276
|
+
return tokens.some((token) => gs.text.includes(token));
|
|
277
|
+
});
|
|
278
|
+
const coveringKernel = kernelSearchTexts.filter(
|
|
279
|
+
(ks) => tokens.some((token) => ks.text.includes(token))
|
|
280
|
+
);
|
|
281
|
+
const hasStructuralGuard = guards.some(
|
|
282
|
+
(g) => g.invariant_ref === invariant.id && g.immutable
|
|
283
|
+
);
|
|
284
|
+
if (coveringGuards.length === 0 && coveringKernel.length === 0) {
|
|
285
|
+
if (hasStructuralGuard) {
|
|
286
|
+
findings.push(finding(
|
|
287
|
+
`weak-coverage-${invariant.id}`,
|
|
288
|
+
`Invariant "${invariant.id}" has a structural guard but no guard's intent patterns match its action class [${tokens.join(", ")}] \u2014 the guard may not intercept violations`,
|
|
289
|
+
"warning",
|
|
290
|
+
"guard-coverage",
|
|
291
|
+
["invariants.json", "guards.json"],
|
|
292
|
+
invariant.id,
|
|
293
|
+
`Ensure the backing guard's intent_patterns include patterns that can detect "${invariant.label}"`
|
|
294
|
+
));
|
|
295
|
+
} else {
|
|
296
|
+
findings.push(finding(
|
|
297
|
+
`unenforced-invariant-${invariant.id}`,
|
|
298
|
+
`Invariant "${invariant.id}" has no guard or kernel rule capable of enforcing it \u2014 no interceptor matches action class [${tokens.join(", ")}]`,
|
|
299
|
+
"warning",
|
|
300
|
+
"guard-coverage",
|
|
301
|
+
["invariants.json", "guards.json"],
|
|
302
|
+
invariant.id,
|
|
303
|
+
`Add a guard with intent_patterns that can intercept "${invariant.label}", or add a kernel forbidden_pattern`
|
|
304
|
+
));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function extractActionTokens(id, label) {
|
|
310
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
311
|
+
"a",
|
|
312
|
+
"an",
|
|
313
|
+
"the",
|
|
314
|
+
"is",
|
|
315
|
+
"are",
|
|
316
|
+
"was",
|
|
317
|
+
"were",
|
|
318
|
+
"be",
|
|
319
|
+
"been",
|
|
320
|
+
"being",
|
|
321
|
+
"have",
|
|
322
|
+
"has",
|
|
323
|
+
"had",
|
|
324
|
+
"do",
|
|
325
|
+
"does",
|
|
326
|
+
"did",
|
|
327
|
+
"will",
|
|
328
|
+
"would",
|
|
329
|
+
"could",
|
|
330
|
+
"should",
|
|
331
|
+
"may",
|
|
332
|
+
"might",
|
|
333
|
+
"must",
|
|
334
|
+
"shall",
|
|
335
|
+
"can",
|
|
336
|
+
"need",
|
|
337
|
+
"dare",
|
|
338
|
+
"to",
|
|
339
|
+
"of",
|
|
340
|
+
"in",
|
|
341
|
+
"for",
|
|
342
|
+
"on",
|
|
343
|
+
"with",
|
|
344
|
+
"at",
|
|
345
|
+
"by",
|
|
346
|
+
"from",
|
|
347
|
+
"as",
|
|
348
|
+
"into",
|
|
349
|
+
"through",
|
|
350
|
+
"during",
|
|
351
|
+
"before",
|
|
352
|
+
"after",
|
|
353
|
+
"above",
|
|
354
|
+
"below",
|
|
355
|
+
"between",
|
|
356
|
+
"out",
|
|
357
|
+
"off",
|
|
358
|
+
"over",
|
|
359
|
+
"under",
|
|
360
|
+
"again",
|
|
361
|
+
"further",
|
|
362
|
+
"then",
|
|
363
|
+
"once",
|
|
364
|
+
"that",
|
|
365
|
+
"than",
|
|
366
|
+
"too",
|
|
367
|
+
"very",
|
|
368
|
+
"just",
|
|
369
|
+
"only",
|
|
370
|
+
"not",
|
|
371
|
+
"no",
|
|
372
|
+
"all",
|
|
373
|
+
"any",
|
|
374
|
+
"both",
|
|
375
|
+
"each",
|
|
376
|
+
"every",
|
|
377
|
+
"few",
|
|
378
|
+
"more",
|
|
379
|
+
"most",
|
|
380
|
+
"other",
|
|
381
|
+
"some",
|
|
382
|
+
"such",
|
|
383
|
+
"and",
|
|
384
|
+
"but",
|
|
385
|
+
"or",
|
|
386
|
+
"nor",
|
|
387
|
+
"so",
|
|
388
|
+
"yet",
|
|
389
|
+
"if",
|
|
390
|
+
"it",
|
|
391
|
+
"its",
|
|
392
|
+
"they",
|
|
393
|
+
"them",
|
|
394
|
+
"their",
|
|
395
|
+
"this",
|
|
396
|
+
"these",
|
|
397
|
+
"those",
|
|
398
|
+
"which",
|
|
399
|
+
"who",
|
|
400
|
+
"whom",
|
|
401
|
+
"what",
|
|
402
|
+
"where",
|
|
403
|
+
"when",
|
|
404
|
+
"how",
|
|
405
|
+
"why"
|
|
406
|
+
]);
|
|
407
|
+
const idTokens = id.toLowerCase().split(/[_\-]+/);
|
|
408
|
+
const labelTokens = label.toLowerCase().split(/[\s\-—:,;.!?()[\]{}]+/);
|
|
409
|
+
const allTokens = [...idTokens, ...labelTokens];
|
|
410
|
+
const unique = /* @__PURE__ */ new Set();
|
|
411
|
+
for (const token of allTokens) {
|
|
412
|
+
const clean = token.replace(/[^a-z0-9]/g, "");
|
|
413
|
+
if (clean.length >= 3 && !stopWords.has(clean)) {
|
|
414
|
+
unique.add(clean);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return [...unique];
|
|
418
|
+
}
|
|
419
|
+
function checkContradictions(world, findings) {
|
|
420
|
+
if (!world.rules || world.rules.length < 2) return;
|
|
421
|
+
checkCircularExclusiveWith(world.rules, findings);
|
|
422
|
+
const tensions = detectSemanticTensions(world.rules);
|
|
423
|
+
for (let i = 0; i < tensions.length; i++) {
|
|
424
|
+
const tension = tensions[i];
|
|
425
|
+
findings.push(finding(
|
|
426
|
+
`semantic-tension-${i}`,
|
|
427
|
+
tension.message,
|
|
428
|
+
tension.severity,
|
|
429
|
+
"semantic-tension",
|
|
430
|
+
tension.affectedBlocks
|
|
431
|
+
));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function checkCircularExclusiveWith(rules, findings) {
|
|
435
|
+
const exclusiveMap = /* @__PURE__ */ new Map();
|
|
436
|
+
for (const rule of rules) {
|
|
437
|
+
if (rule.exclusive_with) {
|
|
438
|
+
exclusiveMap.set(rule.id, rule.exclusive_with);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const reportedPairs = /* @__PURE__ */ new Set();
|
|
442
|
+
for (const [ruleA, ruleB] of exclusiveMap) {
|
|
443
|
+
if (exclusiveMap.get(ruleB) === ruleA) {
|
|
444
|
+
const pairKey = [ruleA, ruleB].sort().join("::");
|
|
445
|
+
if (!reportedPairs.has(pairKey)) {
|
|
446
|
+
reportedPairs.add(pairKey);
|
|
447
|
+
findings.push(finding(
|
|
448
|
+
`mutual-exclusion-${pairKey.replace("::", "-")}`,
|
|
449
|
+
`Rules "${ruleA}" and "${ruleB}" are mutually exclusive \u2014 only one can fire per evaluation`,
|
|
450
|
+
"info",
|
|
451
|
+
"semantic-tension",
|
|
452
|
+
["rules/"],
|
|
453
|
+
`${ruleA}, ${ruleB}`
|
|
454
|
+
));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const reportedChains = /* @__PURE__ */ new Set();
|
|
459
|
+
for (const startRule of exclusiveMap.keys()) {
|
|
460
|
+
let current = startRule;
|
|
461
|
+
const visited = /* @__PURE__ */ new Set();
|
|
462
|
+
const chain = [];
|
|
463
|
+
while (current && !visited.has(current)) {
|
|
464
|
+
visited.add(current);
|
|
465
|
+
chain.push(current);
|
|
466
|
+
current = exclusiveMap.get(current);
|
|
467
|
+
}
|
|
468
|
+
if (current === startRule && visited.size > 2) {
|
|
469
|
+
const chainKey = [...chain].sort().join("::");
|
|
470
|
+
if (!reportedChains.has(chainKey)) {
|
|
471
|
+
reportedChains.add(chainKey);
|
|
472
|
+
findings.push(finding(
|
|
473
|
+
`circular-exclusive-chain-${startRule}`,
|
|
474
|
+
`Circular exclusive_with chain: ${chain.join(" \u2192 ")} \u2192 ${startRule} (${chain.length} rules)`,
|
|
475
|
+
"warning",
|
|
476
|
+
"contradiction",
|
|
477
|
+
["rules/"],
|
|
478
|
+
chain.join(", ")
|
|
479
|
+
));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function detectSemanticTensions(rules) {
|
|
485
|
+
const results = [];
|
|
486
|
+
if (rules.length < 2) return results;
|
|
487
|
+
const exclusivePairs = /* @__PURE__ */ new Set();
|
|
488
|
+
for (const rule of rules) {
|
|
489
|
+
if (rule.exclusive_with) {
|
|
490
|
+
exclusivePairs.add(`${rule.id}:${rule.exclusive_with}`);
|
|
491
|
+
exclusivePairs.add(`${rule.exclusive_with}:${rule.id}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const isExclusive = (a, b) => exclusivePairs.has(`${a}:${b}`);
|
|
495
|
+
const targetEffects = /* @__PURE__ */ new Map();
|
|
496
|
+
for (const rule of rules) {
|
|
497
|
+
const allEffects = [
|
|
498
|
+
...rule.effects || [],
|
|
499
|
+
...(rule.effects_conditional || []).flatMap((c) => c.effects)
|
|
500
|
+
];
|
|
501
|
+
for (const effect of allEffects) {
|
|
502
|
+
const direction = classifyEffectDirection(effect);
|
|
503
|
+
if (direction === "neutral") continue;
|
|
504
|
+
const entries = targetEffects.get(effect.target) ?? [];
|
|
505
|
+
entries.push({ ruleId: rule.id, ruleLabel: rule.label, direction, effect });
|
|
506
|
+
targetEffects.set(effect.target, entries);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
for (const [target, effects] of targetEffects) {
|
|
510
|
+
const increasing = effects.filter((e) => e.direction === "increase");
|
|
511
|
+
const decreasing = effects.filter((e) => e.direction === "decrease");
|
|
512
|
+
if (increasing.length === 0 || decreasing.length === 0) continue;
|
|
513
|
+
const incRuleIds = /* @__PURE__ */ new Set();
|
|
514
|
+
const decRuleIds = /* @__PURE__ */ new Set();
|
|
515
|
+
for (const inc of increasing) {
|
|
516
|
+
for (const dec of decreasing) {
|
|
517
|
+
if (inc.ruleId === dec.ruleId) continue;
|
|
518
|
+
if (isExclusive(inc.ruleId, dec.ruleId)) continue;
|
|
519
|
+
incRuleIds.add(inc.ruleId);
|
|
520
|
+
decRuleIds.add(dec.ruleId);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (incRuleIds.size === 0 || decRuleIds.size === 0) continue;
|
|
524
|
+
const incLabels = [...incRuleIds].map((id) => {
|
|
525
|
+
const e = increasing.find((e2) => e2.ruleId === id);
|
|
526
|
+
return `"${e.ruleLabel}" (${describeEffect(e.effect)})`;
|
|
527
|
+
});
|
|
528
|
+
const decLabels = [...decRuleIds].map((id) => {
|
|
529
|
+
const e = decreasing.find((e2) => e2.ruleId === id);
|
|
530
|
+
return `"${e.ruleLabel}" (${describeEffect(e.effect)})`;
|
|
531
|
+
});
|
|
532
|
+
results.push({
|
|
533
|
+
type: "semantic_tension",
|
|
534
|
+
severity: "warning",
|
|
535
|
+
message: `Opposing effects on "${target}": ${incLabels.join(", ")} increase it while ${decLabels.join(", ")} decrease it`,
|
|
536
|
+
affectedBlocks: ["rules"]
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
const setEffects = /* @__PURE__ */ new Map();
|
|
540
|
+
for (const rule of rules) {
|
|
541
|
+
const allEffects = [
|
|
542
|
+
...rule.effects || [],
|
|
543
|
+
...(rule.effects_conditional || []).flatMap((c) => c.effects)
|
|
544
|
+
];
|
|
545
|
+
for (const effect of allEffects) {
|
|
546
|
+
if (effect.operation !== "set" && effect.operation !== "set_boolean" && effect.operation !== "set_dynamic") continue;
|
|
547
|
+
const entries = setEffects.get(effect.target) ?? [];
|
|
548
|
+
entries.push({ ruleId: rule.id, ruleLabel: rule.label, value: effect.value });
|
|
549
|
+
setEffects.set(effect.target, entries);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
for (const [target, effects] of setEffects) {
|
|
553
|
+
if (effects.length < 2) continue;
|
|
554
|
+
const byValue = /* @__PURE__ */ new Map();
|
|
555
|
+
for (const e of effects) {
|
|
556
|
+
const key = String(e.value);
|
|
557
|
+
const existing = byValue.get(key) ?? [];
|
|
558
|
+
existing.push(e);
|
|
559
|
+
byValue.set(key, existing);
|
|
560
|
+
}
|
|
561
|
+
if (byValue.size < 2) continue;
|
|
562
|
+
const valueGroups = [...byValue.entries()];
|
|
563
|
+
const involvedRules = [];
|
|
564
|
+
for (let i = 0; i < valueGroups.length; i++) {
|
|
565
|
+
for (let j = i + 1; j < valueGroups.length; j++) {
|
|
566
|
+
for (const a of valueGroups[i][1]) {
|
|
567
|
+
for (const b of valueGroups[j][1]) {
|
|
568
|
+
if (a.ruleId === b.ruleId) continue;
|
|
569
|
+
if (isExclusive(a.ruleId, b.ruleId)) continue;
|
|
570
|
+
if (!involvedRules.includes(a.ruleId)) involvedRules.push(a.ruleId);
|
|
571
|
+
if (!involvedRules.includes(b.ruleId)) involvedRules.push(b.ruleId);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (involvedRules.length === 0) continue;
|
|
577
|
+
const descriptions = involvedRules.map((id) => {
|
|
578
|
+
const e = effects.find((e2) => e2.ruleId === id);
|
|
579
|
+
return `"${e.ruleLabel}" sets to ${e.value}`;
|
|
580
|
+
});
|
|
581
|
+
results.push({
|
|
582
|
+
type: "semantic_tension",
|
|
583
|
+
severity: "warning",
|
|
584
|
+
message: `Conflicting set operations on "${target}": ${descriptions.join(", ")}`,
|
|
585
|
+
affectedBlocks: ["rules"]
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
return results;
|
|
589
|
+
}
|
|
590
|
+
function classifyEffectDirection(effect) {
|
|
591
|
+
if (effect.operation === "multiply" || effect.operation === "multiply_dynamic") {
|
|
592
|
+
const val = typeof effect.value === "number" ? effect.value : 1;
|
|
593
|
+
return val > 1 ? "increase" : val < 1 ? "decrease" : "neutral";
|
|
594
|
+
}
|
|
595
|
+
if (effect.operation === "add" || effect.operation === "add_dynamic") {
|
|
596
|
+
const val = typeof effect.value === "number" ? effect.value : 0;
|
|
597
|
+
return val > 0 ? "increase" : val < 0 ? "decrease" : "neutral";
|
|
598
|
+
}
|
|
599
|
+
if (effect.operation === "subtract" || effect.operation === "subtract_dynamic") {
|
|
600
|
+
const val = typeof effect.value === "number" ? effect.value : 0;
|
|
601
|
+
return val > 0 ? "decrease" : val < 0 ? "increase" : "neutral";
|
|
602
|
+
}
|
|
603
|
+
return "neutral";
|
|
604
|
+
}
|
|
605
|
+
function describeEffect(effect) {
|
|
606
|
+
switch (effect.operation) {
|
|
607
|
+
case "multiply":
|
|
608
|
+
case "multiply_dynamic":
|
|
609
|
+
return `multiplies by ${effect.value}`;
|
|
610
|
+
case "add":
|
|
611
|
+
case "add_dynamic":
|
|
612
|
+
return `adds ${effect.value}`;
|
|
613
|
+
case "subtract":
|
|
614
|
+
case "subtract_dynamic":
|
|
615
|
+
return `subtracts ${effect.value}`;
|
|
616
|
+
case "set":
|
|
617
|
+
case "set_dynamic":
|
|
618
|
+
case "set_boolean":
|
|
619
|
+
return `sets to ${effect.value}`;
|
|
620
|
+
default:
|
|
621
|
+
return `${effect.operation} ${effect.value}`;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function checkGuardShadows(world, findings) {
|
|
625
|
+
if (!world.guards?.guards || world.guards.guards.length < 2) return;
|
|
626
|
+
const guards = world.guards.guards;
|
|
627
|
+
for (let i = 0; i < guards.length; i++) {
|
|
628
|
+
const guardA = guards[i];
|
|
629
|
+
const enabledA = guardA.immutable || guardA.default_enabled !== false;
|
|
630
|
+
if (!enabledA) continue;
|
|
631
|
+
if (guardA.enforcement !== "block" && guardA.enforcement !== "pause") continue;
|
|
632
|
+
for (let j = i + 1; j < guards.length; j++) {
|
|
633
|
+
const guardB = guards[j];
|
|
634
|
+
const enabledB = guardB.immutable || guardB.default_enabled !== false;
|
|
635
|
+
if (!enabledB) continue;
|
|
636
|
+
const overlap = guardA.intent_patterns.filter(
|
|
637
|
+
(p) => guardB.intent_patterns.includes(p)
|
|
638
|
+
);
|
|
639
|
+
if (overlap.length === 0) continue;
|
|
640
|
+
if (guardA.appliesTo?.length && guardB.appliesTo?.length) {
|
|
641
|
+
const toolsA = new Set(guardA.appliesTo.map((t) => t.toLowerCase()));
|
|
642
|
+
const toolsB = new Set(guardB.appliesTo.map((t) => t.toLowerCase()));
|
|
643
|
+
const toolOverlap = [...toolsA].some((t) => toolsB.has(t));
|
|
644
|
+
if (!toolOverlap) continue;
|
|
645
|
+
}
|
|
646
|
+
if (guardA.required_roles?.length && guardB.required_roles?.length) {
|
|
647
|
+
const rolesA = new Set(guardA.required_roles);
|
|
648
|
+
const rolesB = new Set(guardB.required_roles);
|
|
649
|
+
const roleOverlap = [...rolesA].some((r) => rolesB.has(r));
|
|
650
|
+
if (!roleOverlap) continue;
|
|
651
|
+
}
|
|
652
|
+
const patternsStr = overlap.join(", ");
|
|
653
|
+
if (guardB.enforcement === guardA.enforcement) {
|
|
654
|
+
findings.push(finding(
|
|
655
|
+
`guard-shadow-${guardA.id}-${guardB.id}`,
|
|
656
|
+
`Guard "${guardB.label}" (${guardB.id}) is shadowed by "${guardA.label}" (${guardA.id}) \u2014 both ${guardA.enforcement.toUpperCase()} on patterns [${patternsStr}] but "${guardA.label}" appears first and will always win`,
|
|
657
|
+
"warning",
|
|
658
|
+
"contradiction",
|
|
659
|
+
["guards/"],
|
|
660
|
+
`${guardA.id}, ${guardB.id}`,
|
|
661
|
+
`Remove "${guardB.label}", merge its patterns into "${guardA.label}", or reorder guards`
|
|
662
|
+
));
|
|
663
|
+
} else {
|
|
664
|
+
findings.push(finding(
|
|
665
|
+
`guard-conflict-${guardA.id}-${guardB.id}`,
|
|
666
|
+
`Guards "${guardA.label}" (${guardA.enforcement.toUpperCase()}) and "${guardB.label}" (${guardB.enforcement.toUpperCase()}) share patterns [${patternsStr}] \u2014 "${guardA.label}" always wins because it appears first`,
|
|
667
|
+
"warning",
|
|
668
|
+
"contradiction",
|
|
669
|
+
["guards/"],
|
|
670
|
+
`${guardA.id}, ${guardB.id}`,
|
|
671
|
+
`If "${guardB.label}" should take precedence, move it before "${guardA.label}" in guards.json`
|
|
672
|
+
));
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function checkFailClosedSurfaces(world, findings) {
|
|
678
|
+
const declaredSurfaces = world.guards?.tool_surfaces;
|
|
679
|
+
if (!declaredSurfaces || declaredSurfaces.length === 0) return;
|
|
680
|
+
const guards = world.guards?.guards ?? [];
|
|
681
|
+
const guardedSurfaces = /* @__PURE__ */ new Set();
|
|
682
|
+
let hasCatchAllGuard = false;
|
|
683
|
+
for (const guard of guards) {
|
|
684
|
+
const enabled = guard.immutable || guard.default_enabled !== false;
|
|
685
|
+
if (!enabled) continue;
|
|
686
|
+
if (!guard.appliesTo || guard.appliesTo.length === 0) {
|
|
687
|
+
hasCatchAllGuard = true;
|
|
688
|
+
} else {
|
|
689
|
+
for (const tool of guard.appliesTo) {
|
|
690
|
+
guardedSurfaces.add(tool.toLowerCase());
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (hasCatchAllGuard) return;
|
|
695
|
+
for (const surface of declaredSurfaces) {
|
|
696
|
+
if (!guardedSurfaces.has(surface.toLowerCase())) {
|
|
697
|
+
findings.push(finding(
|
|
698
|
+
`fail-open-surface-${surface.toLowerCase()}`,
|
|
699
|
+
`Action surface "${surface}" has no governing guard \u2014 actions on this surface bypass governance entirely`,
|
|
700
|
+
"warning",
|
|
701
|
+
"guard-coverage",
|
|
702
|
+
["guards.json"],
|
|
703
|
+
void 0,
|
|
704
|
+
`Add a guard with appliesTo including "${surface}", or add a catch-all guard (no appliesTo) to cover all surfaces`
|
|
705
|
+
));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function checkReachability(world, findings) {
|
|
710
|
+
if (!world.stateSchema?.variables) return;
|
|
711
|
+
const vars = world.stateSchema.variables;
|
|
712
|
+
for (const rule of world.rules ?? []) {
|
|
713
|
+
for (const trigger of rule.triggers) {
|
|
714
|
+
if (trigger.source !== "state") continue;
|
|
715
|
+
const unreachable = isTriggerUnreachable(trigger, vars);
|
|
716
|
+
if (unreachable) {
|
|
717
|
+
findings.push(finding(
|
|
718
|
+
`unreachable-rule-${rule.id}-${trigger.field}`,
|
|
719
|
+
`Rule "${rule.id}" has unreachable trigger: ${trigger.field} ${trigger.operator} ${JSON.stringify(trigger.value)} \u2014 ${unreachable}`,
|
|
720
|
+
"warning",
|
|
721
|
+
"contradiction",
|
|
722
|
+
["rules/", "state-schema.json"],
|
|
723
|
+
rule.id,
|
|
724
|
+
`Remove this rule or adjust the trigger condition to match the schema constraints for "${trigger.field}"`
|
|
725
|
+
));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (rule.collapse_check) {
|
|
729
|
+
const cc = rule.collapse_check;
|
|
730
|
+
const unreachable = isTriggerUnreachable(
|
|
731
|
+
{ field: cc.field, operator: cc.operator, value: cc.value },
|
|
732
|
+
vars
|
|
733
|
+
);
|
|
734
|
+
if (unreachable) {
|
|
735
|
+
findings.push(finding(
|
|
736
|
+
`unreachable-collapse-${rule.id}`,
|
|
737
|
+
`Rule "${rule.id}" has unreachable collapse_check: ${cc.field} ${cc.operator} ${cc.value} \u2014 ${unreachable}`,
|
|
738
|
+
"warning",
|
|
739
|
+
"contradiction",
|
|
740
|
+
["rules/", "state-schema.json"],
|
|
741
|
+
rule.id
|
|
742
|
+
));
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
for (const gate of world.gates?.viability_classification ?? []) {
|
|
747
|
+
const unreachable = isTriggerUnreachable(
|
|
748
|
+
{ field: gate.field, operator: gate.operator, value: gate.value },
|
|
749
|
+
vars
|
|
750
|
+
);
|
|
751
|
+
if (unreachable) {
|
|
752
|
+
findings.push(finding(
|
|
753
|
+
`unreachable-gate-${gate.status}`,
|
|
754
|
+
`Viability gate "${gate.status}" has unreachable condition: ${gate.field} ${gate.operator} ${gate.value} \u2014 ${unreachable}`,
|
|
755
|
+
"warning",
|
|
756
|
+
"contradiction",
|
|
757
|
+
["gates.json", "state-schema.json"],
|
|
758
|
+
`gate-${gate.status}`
|
|
759
|
+
));
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function isTriggerUnreachable(trigger, vars) {
|
|
764
|
+
const variable = vars[trigger.field];
|
|
765
|
+
if (!variable) return null;
|
|
766
|
+
const { operator, value } = trigger;
|
|
767
|
+
if (variable.type === "number") {
|
|
768
|
+
const numVal = typeof value === "number" ? value : Number(value);
|
|
769
|
+
if (isNaN(numVal)) return null;
|
|
770
|
+
const min = variable.min;
|
|
771
|
+
const max = variable.max;
|
|
772
|
+
if (operator === ">" || operator === ">=") {
|
|
773
|
+
if (max !== void 0 && numVal >= max && operator === ">") {
|
|
774
|
+
return `schema declares max=${max}, so ${trigger.field} can never exceed ${max}`;
|
|
775
|
+
}
|
|
776
|
+
if (max !== void 0 && numVal > max && operator === ">=") {
|
|
777
|
+
return `schema declares max=${max}, so ${trigger.field} can never reach ${numVal}`;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (operator === "<" || operator === "<=") {
|
|
781
|
+
if (min !== void 0 && numVal <= min && operator === "<") {
|
|
782
|
+
return `schema declares min=${min}, so ${trigger.field} can never go below ${min}`;
|
|
783
|
+
}
|
|
784
|
+
if (min !== void 0 && numVal < min && operator === "<=") {
|
|
785
|
+
return `schema declares min=${min}, so ${trigger.field} can never reach ${numVal}`;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (operator === "==") {
|
|
789
|
+
if (min !== void 0 && numVal < min) {
|
|
790
|
+
return `schema declares min=${min}, so ${trigger.field} can never equal ${numVal}`;
|
|
791
|
+
}
|
|
792
|
+
if (max !== void 0 && numVal > max) {
|
|
793
|
+
return `schema declares max=${max}, so ${trigger.field} can never equal ${numVal}`;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (variable.type === "enum" && variable.options) {
|
|
798
|
+
if (operator === "==" && typeof value === "string") {
|
|
799
|
+
if (!variable.options.includes(value)) {
|
|
800
|
+
return `"${value}" is not in enum options [${variable.options.join(", ")}]`;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (operator === "!=" && typeof value === "string") {
|
|
804
|
+
if (variable.options.length === 1 && variable.options[0] === value) {
|
|
805
|
+
return `enum has only option "${value}", so != "${value}" can never be true`;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (operator === "in" && Array.isArray(value)) {
|
|
809
|
+
const validValues = value.filter((v) => variable.options.includes(v));
|
|
810
|
+
if (validValues.length === 0) {
|
|
811
|
+
return `none of [${value.join(", ")}] are in enum options [${variable.options.join(", ")}]`;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (variable.type === "boolean") {
|
|
816
|
+
if (operator === "==" && typeof value !== "boolean" && value !== "true" && value !== "false") {
|
|
817
|
+
return `boolean variable compared to non-boolean value "${value}"`;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
function checkStateCoverage(world, findings) {
|
|
823
|
+
if (!world.stateSchema?.variables) return;
|
|
824
|
+
const vars = world.stateSchema.variables;
|
|
825
|
+
for (const [varId, variable] of Object.entries(vars)) {
|
|
826
|
+
if (variable.type !== "enum" || !variable.options || variable.options.length <= 1) continue;
|
|
827
|
+
const allOptions = new Set(variable.options);
|
|
828
|
+
const coveredOptions = /* @__PURE__ */ new Set();
|
|
829
|
+
for (const rule of world.rules ?? []) {
|
|
830
|
+
for (const trigger of rule.triggers) {
|
|
831
|
+
if (trigger.field !== varId || trigger.source !== "state") continue;
|
|
832
|
+
if (trigger.operator === "==" && typeof trigger.value === "string") {
|
|
833
|
+
coveredOptions.add(trigger.value);
|
|
834
|
+
}
|
|
835
|
+
if (trigger.operator === "in" && Array.isArray(trigger.value)) {
|
|
836
|
+
for (const v of trigger.value) coveredOptions.add(v);
|
|
837
|
+
}
|
|
838
|
+
if (trigger.operator === "!=") {
|
|
839
|
+
for (const opt of allOptions) {
|
|
840
|
+
if (opt !== trigger.value) coveredOptions.add(opt);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
for (const gate of world.gates?.viability_classification ?? []) {
|
|
846
|
+
if (gate.field !== varId) continue;
|
|
847
|
+
if (gate.operator === "==" && typeof gate.value === "string") {
|
|
848
|
+
coveredOptions.add(gate.value);
|
|
849
|
+
}
|
|
850
|
+
if (gate.operator === "in" && Array.isArray(gate.value)) {
|
|
851
|
+
for (const v of gate.value) coveredOptions.add(v);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (coveredOptions.size === 0) continue;
|
|
855
|
+
const uncovered = [...allOptions].filter((opt) => !coveredOptions.has(opt));
|
|
856
|
+
if (uncovered.length > 0 && uncovered.length < allOptions.size) {
|
|
857
|
+
findings.push(finding(
|
|
858
|
+
`incomplete-state-coverage-${varId}`,
|
|
859
|
+
`Enum variable "${varId}" has ${uncovered.length} uncovered state${uncovered.length > 1 ? "s" : ""}: [${uncovered.join(", ")}] \u2014 rules/gates handle [${[...coveredOptions].join(", ")}] but not all ${allOptions.size} declared options`,
|
|
860
|
+
"warning",
|
|
861
|
+
"guard-coverage",
|
|
862
|
+
["state-schema.json", "rules/", "gates.json"],
|
|
863
|
+
varId,
|
|
864
|
+
`Add rules or gates that handle ${uncovered.map((u) => `"${u}"`).join(", ")} for variable "${varId}"`
|
|
865
|
+
));
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function checkOrphans(world, findings) {
|
|
870
|
+
if (!world.stateSchema?.variables || !world.rules) return;
|
|
871
|
+
const referencedVars = /* @__PURE__ */ new Set();
|
|
872
|
+
for (const rule of world.rules) {
|
|
873
|
+
for (const trigger of rule.triggers) {
|
|
874
|
+
referencedVars.add(trigger.field);
|
|
875
|
+
}
|
|
876
|
+
for (const effect of rule.effects ?? []) {
|
|
877
|
+
referencedVars.add(effect.target);
|
|
878
|
+
}
|
|
879
|
+
if (rule.collapse_check) {
|
|
880
|
+
referencedVars.add(rule.collapse_check.field);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
for (const gate of world.gates?.viability_classification ?? []) {
|
|
884
|
+
referencedVars.add(gate.field);
|
|
885
|
+
}
|
|
886
|
+
for (const varId of Object.keys(world.stateSchema.variables)) {
|
|
887
|
+
if (!referencedVars.has(varId)) {
|
|
888
|
+
findings.push(finding(
|
|
889
|
+
`orphan-variable-${varId}`,
|
|
890
|
+
`State variable "${varId}" is declared but never referenced by any rule or gate`,
|
|
891
|
+
"warning",
|
|
892
|
+
"orphan",
|
|
893
|
+
["state-schema.json"],
|
|
894
|
+
varId,
|
|
895
|
+
"Remove this variable or add rules that reference it"
|
|
896
|
+
));
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const effectTargets = /* @__PURE__ */ new Set();
|
|
900
|
+
for (const rule of world.rules) {
|
|
901
|
+
for (const effect of rule.effects ?? []) {
|
|
902
|
+
effectTargets.add(effect.target);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
for (const outcome of world.outcomes?.computed_outcomes ?? []) {
|
|
906
|
+
if (outcome.assignment === "external") continue;
|
|
907
|
+
if (!effectTargets.has(outcome.id) && !outcome.derived_from) {
|
|
908
|
+
findings.push(finding(
|
|
909
|
+
`orphan-outcome-${outcome.id}`,
|
|
910
|
+
`Outcome "${outcome.id}" is declared but no rule produces it`,
|
|
911
|
+
"warning",
|
|
912
|
+
"orphan",
|
|
913
|
+
["outcomes.json", "rules/"],
|
|
914
|
+
outcome.id
|
|
915
|
+
));
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
function checkSchemaViolations(world, findings) {
|
|
920
|
+
if (!world.stateSchema?.variables) return;
|
|
921
|
+
for (const [varId, variable] of Object.entries(world.stateSchema.variables)) {
|
|
922
|
+
if (variable.type === "number") {
|
|
923
|
+
const def = variable.default;
|
|
924
|
+
if (variable.min !== void 0 && def < variable.min) {
|
|
925
|
+
findings.push(finding(
|
|
926
|
+
`default-below-min-${varId}`,
|
|
927
|
+
`Variable "${varId}" default (${def}) is below declared min (${variable.min})`,
|
|
928
|
+
"error",
|
|
929
|
+
"schema-violation",
|
|
930
|
+
["state-schema.json"],
|
|
931
|
+
varId
|
|
932
|
+
));
|
|
933
|
+
}
|
|
934
|
+
if (variable.max !== void 0 && def > variable.max) {
|
|
935
|
+
findings.push(finding(
|
|
936
|
+
`default-above-max-${varId}`,
|
|
937
|
+
`Variable "${varId}" default (${def}) is above declared max (${variable.max})`,
|
|
938
|
+
"error",
|
|
939
|
+
"schema-violation",
|
|
940
|
+
["state-schema.json"],
|
|
941
|
+
varId
|
|
942
|
+
));
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (variable.type === "enum") {
|
|
946
|
+
if (!variable.options || variable.options.length === 0) {
|
|
947
|
+
findings.push(finding(
|
|
948
|
+
`enum-no-options-${varId}`,
|
|
949
|
+
`Enum variable "${varId}" has no options declared`,
|
|
950
|
+
"error",
|
|
951
|
+
"schema-violation",
|
|
952
|
+
["state-schema.json"],
|
|
953
|
+
varId
|
|
954
|
+
));
|
|
955
|
+
} else if (!variable.options.includes(variable.default)) {
|
|
956
|
+
findings.push(finding(
|
|
957
|
+
`enum-default-invalid-${varId}`,
|
|
958
|
+
`Enum variable "${varId}" default "${variable.default}" is not in declared options`,
|
|
959
|
+
"error",
|
|
960
|
+
"schema-violation",
|
|
961
|
+
["state-schema.json"],
|
|
962
|
+
varId
|
|
963
|
+
));
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
for (const [presetName, preset] of Object.entries(world.stateSchema.presets ?? {})) {
|
|
968
|
+
for (const [varId, value] of Object.entries(preset.values)) {
|
|
969
|
+
const variable = world.stateSchema.variables[varId];
|
|
970
|
+
if (!variable) {
|
|
971
|
+
findings.push(finding(
|
|
972
|
+
`preset-undeclared-var-${presetName}-${varId}`,
|
|
973
|
+
`Preset "${presetName}" sets undeclared variable "${varId}"`,
|
|
974
|
+
"error",
|
|
975
|
+
"referential-integrity",
|
|
976
|
+
["state-schema.json"],
|
|
977
|
+
presetName
|
|
978
|
+
));
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
if (variable.type === "number" && typeof value === "number") {
|
|
982
|
+
if (variable.min !== void 0 && value < variable.min) {
|
|
983
|
+
findings.push(finding(
|
|
984
|
+
`preset-below-min-${presetName}-${varId}`,
|
|
985
|
+
`Preset "${presetName}" sets "${varId}" to ${value}, below min ${variable.min}`,
|
|
986
|
+
"warning",
|
|
987
|
+
"schema-violation",
|
|
988
|
+
["state-schema.json"],
|
|
989
|
+
presetName
|
|
990
|
+
));
|
|
991
|
+
}
|
|
992
|
+
if (variable.max !== void 0 && value > variable.max) {
|
|
993
|
+
findings.push(finding(
|
|
994
|
+
`preset-above-max-${presetName}-${varId}`,
|
|
995
|
+
`Preset "${presetName}" sets "${varId}" to ${value}, above max ${variable.max}`,
|
|
996
|
+
"warning",
|
|
997
|
+
"schema-violation",
|
|
998
|
+
["state-schema.json"],
|
|
999
|
+
presetName
|
|
1000
|
+
));
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function computeCompletenessScore(world) {
|
|
1007
|
+
let score = 0;
|
|
1008
|
+
const total = 9;
|
|
1009
|
+
if (world.world?.world_id && world.world?.name && world.world?.thesis) score++;
|
|
1010
|
+
if (world.invariants && world.invariants.length > 0) score++;
|
|
1011
|
+
if (world.assumptions?.profiles && Object.keys(world.assumptions.profiles).length > 0) score++;
|
|
1012
|
+
if (world.stateSchema?.variables && Object.keys(world.stateSchema.variables).length > 0) score++;
|
|
1013
|
+
if (world.rules && world.rules.length > 0) score++;
|
|
1014
|
+
if (world.gates?.viability_classification && world.gates.viability_classification.length > 0) score++;
|
|
1015
|
+
if (world.outcomes?.computed_outcomes && world.outcomes.computed_outcomes.length > 0) score++;
|
|
1016
|
+
if (world.guards?.guards && world.guards.guards.length > 0) score++;
|
|
1017
|
+
if (world.metadata) score++;
|
|
1018
|
+
return Math.round(score / total * 100);
|
|
1019
|
+
}
|
|
1020
|
+
function computeInvariantCoverage(world) {
|
|
1021
|
+
if (!world.invariants || world.invariants.length === 0) return 0;
|
|
1022
|
+
const guards = world.guards?.guards ?? [];
|
|
1023
|
+
let covered = 0;
|
|
1024
|
+
for (const invariant of world.invariants) {
|
|
1025
|
+
const hasGuard = guards.some((g) => g.invariant_ref === invariant.id && g.immutable);
|
|
1026
|
+
if (hasGuard) covered++;
|
|
1027
|
+
}
|
|
1028
|
+
return Math.round(covered / world.invariants.length * 100);
|
|
1029
|
+
}
|
|
1030
|
+
function computeGovernanceHealth(world, findings) {
|
|
1031
|
+
const guards = world.guards?.guards ?? [];
|
|
1032
|
+
if (guards.length === 0 && !world.kernel) return void 0;
|
|
1033
|
+
const declaredSurfaces = world.guards?.tool_surfaces ?? [];
|
|
1034
|
+
const guardedSurfaces = /* @__PURE__ */ new Set();
|
|
1035
|
+
let hasCatchAll = false;
|
|
1036
|
+
for (const guard of guards) {
|
|
1037
|
+
const enabled = guard.immutable || guard.default_enabled !== false;
|
|
1038
|
+
if (!enabled) continue;
|
|
1039
|
+
if (!guard.appliesTo || guard.appliesTo.length === 0) {
|
|
1040
|
+
hasCatchAll = true;
|
|
1041
|
+
} else {
|
|
1042
|
+
for (const t of guard.appliesTo) guardedSurfaces.add(t.toLowerCase());
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
const allSurfaces = /* @__PURE__ */ new Set();
|
|
1046
|
+
for (const s of declaredSurfaces) allSurfaces.add(s.toLowerCase());
|
|
1047
|
+
for (const s of guardedSurfaces) allSurfaces.add(s);
|
|
1048
|
+
const surfaces = [...allSurfaces].map((name) => ({
|
|
1049
|
+
name,
|
|
1050
|
+
governed: hasCatchAll || guardedSurfaces.has(name)
|
|
1051
|
+
}));
|
|
1052
|
+
const surfacesCovered = hasCatchAll ? allSurfaces.size : guardedSurfaces.size;
|
|
1053
|
+
const structuralInvariants = (world.invariants ?? []).filter((i) => i.enforcement === "structural");
|
|
1054
|
+
let invariantsEnforced = 0;
|
|
1055
|
+
for (const inv of structuralInvariants) {
|
|
1056
|
+
const hasGuard = guards.some((g) => g.invariant_ref === inv.id && g.immutable);
|
|
1057
|
+
if (hasGuard) invariantsEnforced++;
|
|
1058
|
+
}
|
|
1059
|
+
const shadowedGuards = findings.filter((f) => f.id.startsWith("guard-shadow-")).length;
|
|
1060
|
+
const unenforcedInvariants = findings.filter((f) => f.id.startsWith("unenforced-invariant-")).length;
|
|
1061
|
+
const unreachableRules = findings.filter((f) => f.id.startsWith("unreachable-")).length;
|
|
1062
|
+
const incompleteStateCoverage = findings.filter((f) => f.id.startsWith("incomplete-state-coverage-")).length;
|
|
1063
|
+
const failOpenCount = findings.filter((f) => f.id.startsWith("fail-open-surface-")).length;
|
|
1064
|
+
let riskLevel = "low";
|
|
1065
|
+
const totalIssues = unenforcedInvariants + failOpenCount + incompleteStateCoverage;
|
|
1066
|
+
if (totalIssues > 0 || unreachableRules > 0) riskLevel = "moderate";
|
|
1067
|
+
if (totalIssues > 2 || unenforcedInvariants > 0 && failOpenCount > 0 || incompleteStateCoverage > 2) riskLevel = "high";
|
|
1068
|
+
return {
|
|
1069
|
+
surfacesCovered,
|
|
1070
|
+
surfacesTotal: allSurfaces.size,
|
|
1071
|
+
surfaces,
|
|
1072
|
+
invariantsEnforced,
|
|
1073
|
+
invariantsTotal: structuralInvariants.length,
|
|
1074
|
+
shadowedGuards,
|
|
1075
|
+
unenforcedInvariants,
|
|
1076
|
+
unreachableRules,
|
|
1077
|
+
incompleteStateCoverage,
|
|
1078
|
+
riskLevel
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
function finding(id, message, severity, category, affectedBlocks, source, suggestion) {
|
|
1082
|
+
const f = { id, message, severity, category, affectedBlocks };
|
|
1083
|
+
if (source) f.source = source;
|
|
1084
|
+
if (suggestion) f.suggestion = suggestion;
|
|
1085
|
+
return f;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
export {
|
|
1089
|
+
validateWorld
|
|
1090
|
+
};
|