@plures/praxis 1.2.41 → 1.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/dist/browser/{chunk-BBP2F7TT.js → chunk-MJK3IYTJ.js} +123 -5
- package/dist/browser/{chunk-FCEH7WMH.js → chunk-N63K4KWS.js} +1 -1
- package/dist/browser/{engine-65QDGCAN.js → engine-YIEGSX7U.js} +1 -1
- package/dist/browser/index.d.ts +2 -2
- package/dist/browser/index.js +10 -5
- package/dist/browser/integrations/svelte.d.ts +2 -2
- package/dist/browser/integrations/svelte.js +2 -2
- package/dist/browser/{reactive-engine.svelte-Cqd8Mod2.d.ts → reactive-engine.svelte-DjynI82A.d.ts} +83 -4
- package/dist/node/chunk-2IUFZBH3.js +87 -0
- package/dist/node/{chunk-WZ6B3LZ6.js → chunk-7CSWBDFL.js} +3 -56
- package/dist/node/{chunk-32YFEEML.js → chunk-7M3HV4XR.js} +4 -4
- package/dist/node/{chunk-PTH6MD6P.js → chunk-FWOXU4MM.js} +1 -1
- package/dist/node/{chunk-BBP2F7TT.js → chunk-KMJWAFZV.js} +128 -5
- package/dist/node/chunk-PGVSB6NR.js +59 -0
- package/dist/node/cli/index.cjs +1078 -211
- package/dist/node/cli/index.js +21 -2
- package/dist/node/cloud/index.d.cts +1 -1
- package/dist/node/cloud/index.d.ts +1 -1
- package/dist/node/{engine-7CXQV6RC.js → engine-FEN5IYZ5.js} +1 -1
- package/dist/node/index.cjs +1633 -59
- package/dist/node/index.d.cts +769 -5
- package/dist/node/index.d.ts +769 -5
- package/dist/node/index.js +1375 -45
- package/dist/node/integrations/svelte.cjs +123 -5
- package/dist/node/integrations/svelte.d.cts +3 -3
- package/dist/node/integrations/svelte.d.ts +3 -3
- package/dist/node/integrations/svelte.js +3 -3
- package/dist/node/{protocol-BocKczNv.d.ts → protocol-DcyGMmWY.d.cts} +7 -0
- package/dist/node/{protocol-BocKczNv.d.cts → protocol-DcyGMmWY.d.ts} +7 -0
- package/dist/node/{reactive-engine.svelte-CGe8SpVE.d.cts → reactive-engine.svelte-Cg0Yc2Hs.d.cts} +90 -6
- package/dist/node/{reactive-engine.svelte-D-xTDxT5.d.ts → reactive-engine.svelte-DekxqFu0.d.ts} +90 -6
- package/dist/node/{reverse-W7THPV45.js → reverse-YD3CWIGM.js} +3 -2
- package/dist/node/rules-4DAJ4Z4N.js +7 -0
- package/dist/node/server-SYZPDULV.js +361 -0
- package/dist/node/{validate-EN3M4FUR.js → validate-TQGVIG7G.js} +4 -3
- package/package.json +29 -3
- package/src/__tests__/engine-v2.test.ts +532 -0
- package/src/__tests__/expectations.test.ts +364 -0
- package/src/__tests__/factory.test.ts +426 -0
- package/src/__tests__/mcp-server.test.ts +310 -0
- package/src/__tests__/project.test.ts +396 -0
- package/src/cli/index.ts +28 -0
- package/src/core/completeness.ts +274 -0
- package/src/core/engine.ts +47 -5
- package/src/core/pluresdb/store.ts +9 -3
- package/src/core/protocol.ts +7 -0
- package/src/core/rule-result.ts +130 -0
- package/src/core/rules.ts +12 -5
- package/src/core/ui-rules.ts +340 -0
- package/src/expectations/expectations.ts +471 -0
- package/src/expectations/index.ts +29 -0
- package/src/expectations/types.ts +95 -0
- package/src/factory/factory.ts +634 -0
- package/src/factory/index.ts +27 -0
- package/src/factory/types.ts +64 -0
- package/src/index.ts +84 -0
- package/src/mcp/index.ts +33 -0
- package/src/mcp/server.ts +485 -0
- package/src/mcp/types.ts +161 -0
- package/src/project/index.ts +31 -0
- package/src/project/project.ts +423 -0
- package/src/project/types.ts +87 -0
- package/src/vite/completeness-plugin.ts +72 -0
- /package/dist/node/{chunk-R2PSBPKQ.js → chunk-TEMFJOIH.js} +0 -0
package/dist/node/index.cjs
CHANGED
|
@@ -39,6 +39,99 @@ var init_protocol = __esm({
|
|
|
39
39
|
}
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
// src/core/rule-result.ts
|
|
43
|
+
function fact(tag, payload) {
|
|
44
|
+
return { tag, payload };
|
|
45
|
+
}
|
|
46
|
+
var RuleResult;
|
|
47
|
+
var init_rule_result = __esm({
|
|
48
|
+
"src/core/rule-result.ts"() {
|
|
49
|
+
"use strict";
|
|
50
|
+
RuleResult = class _RuleResult {
|
|
51
|
+
/** The kind of result */
|
|
52
|
+
kind;
|
|
53
|
+
/** Facts produced (only for 'emit') */
|
|
54
|
+
facts;
|
|
55
|
+
/** Fact tags to retract (only for 'retract') */
|
|
56
|
+
retractTags;
|
|
57
|
+
/** Optional reason (for noop/skip/retract — useful for debugging) */
|
|
58
|
+
reason;
|
|
59
|
+
/** The rule ID that produced this result (set by engine) */
|
|
60
|
+
ruleId;
|
|
61
|
+
constructor(kind, facts, retractTags, reason) {
|
|
62
|
+
this.kind = kind;
|
|
63
|
+
this.facts = facts;
|
|
64
|
+
this.retractTags = retractTags;
|
|
65
|
+
this.reason = reason;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Rule produced facts.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* return RuleResult.emit([
|
|
72
|
+
* { tag: 'sprint.behind', payload: { deficit: 5 } }
|
|
73
|
+
* ]);
|
|
74
|
+
*/
|
|
75
|
+
static emit(facts) {
|
|
76
|
+
if (facts.length === 0) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"RuleResult.emit() requires at least one fact. Use RuleResult.noop() or RuleResult.skip() when a rule has nothing to say."
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return new _RuleResult("emit", facts, []);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Rule evaluated but had nothing to report.
|
|
85
|
+
* Unlike returning [], this is explicit and traceable.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* if (ctx.completedHours >= expectedHours) {
|
|
89
|
+
* return RuleResult.noop('Sprint is on pace');
|
|
90
|
+
* }
|
|
91
|
+
*/
|
|
92
|
+
static noop(reason) {
|
|
93
|
+
return new _RuleResult("noop", [], [], reason);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Rule decided to skip because preconditions were not met.
|
|
97
|
+
* Distinct from noop: skip means "I can't evaluate", noop means "I evaluated and found nothing".
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* if (!ctx.sprintName) {
|
|
101
|
+
* return RuleResult.skip('No active sprint');
|
|
102
|
+
* }
|
|
103
|
+
*/
|
|
104
|
+
static skip(reason) {
|
|
105
|
+
return new _RuleResult("skip", [], [], reason);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Rule retracts previously emitted facts by tag.
|
|
109
|
+
* Used when a condition that previously produced facts is no longer true.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* // Sprint was behind, but caught up
|
|
113
|
+
* if (ctx.completedHours >= expectedHours) {
|
|
114
|
+
* return RuleResult.retract(['sprint.behind'], 'Sprint caught up');
|
|
115
|
+
* }
|
|
116
|
+
*/
|
|
117
|
+
static retract(tags, reason) {
|
|
118
|
+
if (tags.length === 0) {
|
|
119
|
+
throw new Error("RuleResult.retract() requires at least one tag.");
|
|
120
|
+
}
|
|
121
|
+
return new _RuleResult("retract", [], tags, reason);
|
|
122
|
+
}
|
|
123
|
+
/** Whether this result produced facts */
|
|
124
|
+
get hasFacts() {
|
|
125
|
+
return this.facts.length > 0;
|
|
126
|
+
}
|
|
127
|
+
/** Whether this result retracts facts */
|
|
128
|
+
get hasRetractions() {
|
|
129
|
+
return this.retractTags.length > 0;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
42
135
|
// src/core/engine.ts
|
|
43
136
|
var engine_exports = {};
|
|
44
137
|
__export(engine_exports, {
|
|
@@ -68,6 +161,7 @@ var init_engine = __esm({
|
|
|
68
161
|
"src/core/engine.ts"() {
|
|
69
162
|
"use strict";
|
|
70
163
|
init_protocol();
|
|
164
|
+
init_rule_result();
|
|
71
165
|
LogicEngine = class {
|
|
72
166
|
state;
|
|
73
167
|
registry;
|
|
@@ -131,7 +225,13 @@ var init_engine = __esm({
|
|
|
131
225
|
stepWithConfig(events, config) {
|
|
132
226
|
const diagnostics = [];
|
|
133
227
|
let newState = { ...this.state };
|
|
228
|
+
const stateWithEvents = {
|
|
229
|
+
...newState,
|
|
230
|
+
events
|
|
231
|
+
// current batch — rules can read state.events
|
|
232
|
+
};
|
|
134
233
|
const newFacts = [];
|
|
234
|
+
const retractions = [];
|
|
135
235
|
const eventTags = new Set(events.map((e) => e.tag));
|
|
136
236
|
for (const ruleId of config.ruleIds) {
|
|
137
237
|
const rule = this.registry.getRule(ruleId);
|
|
@@ -150,8 +250,31 @@ var init_engine = __esm({
|
|
|
150
250
|
}
|
|
151
251
|
}
|
|
152
252
|
try {
|
|
153
|
-
const
|
|
154
|
-
|
|
253
|
+
const rawResult = rule.impl(stateWithEvents, events);
|
|
254
|
+
if (rawResult instanceof RuleResult) {
|
|
255
|
+
rawResult.ruleId = ruleId;
|
|
256
|
+
switch (rawResult.kind) {
|
|
257
|
+
case "emit":
|
|
258
|
+
newFacts.push(...rawResult.facts);
|
|
259
|
+
break;
|
|
260
|
+
case "retract":
|
|
261
|
+
retractions.push(...rawResult.retractTags);
|
|
262
|
+
break;
|
|
263
|
+
case "noop":
|
|
264
|
+
case "skip":
|
|
265
|
+
if (rawResult.reason) {
|
|
266
|
+
diagnostics.push({
|
|
267
|
+
kind: "rule-error",
|
|
268
|
+
// reused kind — could add 'rule-trace' in protocol v2
|
|
269
|
+
message: `[${rawResult.kind}] ${ruleId}: ${rawResult.reason}`,
|
|
270
|
+
data: { ruleId, resultKind: rawResult.kind, reason: rawResult.reason }
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
} else if (Array.isArray(rawResult)) {
|
|
276
|
+
newFacts.push(...rawResult);
|
|
277
|
+
}
|
|
155
278
|
} catch (error) {
|
|
156
279
|
diagnostics.push({
|
|
157
280
|
kind: "rule-error",
|
|
@@ -160,21 +283,26 @@ var init_engine = __esm({
|
|
|
160
283
|
});
|
|
161
284
|
}
|
|
162
285
|
}
|
|
286
|
+
let existingFacts = newState.facts;
|
|
287
|
+
if (retractions.length > 0) {
|
|
288
|
+
const retractSet = new Set(retractions);
|
|
289
|
+
existingFacts = existingFacts.filter((f) => !retractSet.has(f.tag));
|
|
290
|
+
}
|
|
163
291
|
let mergedFacts;
|
|
164
292
|
switch (this.factDedup) {
|
|
165
293
|
case "last-write-wins": {
|
|
166
294
|
const factMap = /* @__PURE__ */ new Map();
|
|
167
|
-
for (const f of
|
|
295
|
+
for (const f of existingFacts) factMap.set(f.tag, f);
|
|
168
296
|
for (const f of newFacts) factMap.set(f.tag, f);
|
|
169
297
|
mergedFacts = Array.from(factMap.values());
|
|
170
298
|
break;
|
|
171
299
|
}
|
|
172
300
|
case "append":
|
|
173
|
-
mergedFacts = [...
|
|
301
|
+
mergedFacts = [...existingFacts, ...newFacts];
|
|
174
302
|
break;
|
|
175
303
|
case "none":
|
|
176
304
|
default:
|
|
177
|
-
mergedFacts = [...
|
|
305
|
+
mergedFacts = [...existingFacts, ...newFacts];
|
|
178
306
|
break;
|
|
179
307
|
}
|
|
180
308
|
if (this.maxFacts > 0 && mergedFacts.length > this.maxFacts) {
|
|
@@ -481,6 +609,8 @@ __export(src_exports, {
|
|
|
481
609
|
ContractMissing: () => ContractMissing,
|
|
482
610
|
ContractUpdated: () => ContractUpdated,
|
|
483
611
|
ContractValidated: () => ContractValidated,
|
|
612
|
+
Expectation: () => Expectation,
|
|
613
|
+
ExpectationSet: () => ExpectationSet,
|
|
484
614
|
FrameworkAgnosticReactiveEngine: () => ReactiveLogicEngine2,
|
|
485
615
|
InMemoryPraxisDB: () => InMemoryPraxisDB,
|
|
486
616
|
LogicEngine: () => LogicEngine,
|
|
@@ -494,6 +624,7 @@ __export(src_exports, {
|
|
|
494
624
|
PraxisSchemaRegistry: () => PraxisSchemaRegistry,
|
|
495
625
|
ReactiveLogicEngine: () => ReactiveLogicEngine,
|
|
496
626
|
RegistryIntrospector: () => RegistryIntrospector,
|
|
627
|
+
RuleResult: () => RuleResult,
|
|
497
628
|
StateDocsGenerator: () => StateDocsGenerator,
|
|
498
629
|
TerminalAdapter: () => TerminalAdapter,
|
|
499
630
|
ValidateContracts: () => ValidateContracts,
|
|
@@ -501,9 +632,12 @@ __export(src_exports, {
|
|
|
501
632
|
attachTauriToEngine: () => attachTauriToEngine,
|
|
502
633
|
attachToEngine: () => attachToEngine,
|
|
503
634
|
attachUnumToEngine: () => attachUnumToEngine,
|
|
635
|
+
auditCompleteness: () => auditCompleteness,
|
|
636
|
+
branchRules: () => branchRules,
|
|
504
637
|
canvasToMermaid: () => canvasToMermaid,
|
|
505
638
|
canvasToSchema: () => canvasToSchema,
|
|
506
639
|
canvasToYaml: () => canvasToYaml,
|
|
640
|
+
commitFromState: () => commitFromState,
|
|
507
641
|
createBehaviorLedger: () => createBehaviorLedger,
|
|
508
642
|
createCanvasEditor: () => createCanvasEditor,
|
|
509
643
|
createChronicle: () => createChronicle,
|
|
@@ -526,21 +660,33 @@ __export(src_exports, {
|
|
|
526
660
|
createTauriPraxisAdapter: () => createTauriPraxisAdapter,
|
|
527
661
|
createTerminalAdapter: () => createTerminalAdapter,
|
|
528
662
|
createTimerActor: () => createTimerActor,
|
|
663
|
+
createUIModule: () => createUIModule,
|
|
529
664
|
createUnifiedApp: () => createUnifiedApp,
|
|
530
665
|
createUnumAdapter: () => createUnumAdapter,
|
|
666
|
+
dataRules: () => dataRules,
|
|
531
667
|
defineConstraint: () => defineConstraint,
|
|
532
668
|
defineContract: () => defineContract,
|
|
533
669
|
defineEvent: () => defineEvent,
|
|
534
670
|
defineFact: () => defineFact,
|
|
671
|
+
defineGate: () => defineGate,
|
|
535
672
|
defineModule: () => defineModule,
|
|
536
673
|
defineRule: () => defineRule,
|
|
674
|
+
dirtyGuardRule: () => dirtyGuardRule,
|
|
675
|
+
errorDisplayRule: () => errorDisplayRule,
|
|
676
|
+
expectBehavior: () => expectBehavior,
|
|
677
|
+
expectationGate: () => expectationGate,
|
|
678
|
+
fact: () => fact,
|
|
537
679
|
filterEvents: () => filterEvents,
|
|
538
680
|
filterFacts: () => filterFacts,
|
|
539
681
|
findEvent: () => findEvent,
|
|
540
682
|
findFact: () => findFact,
|
|
683
|
+
formRules: () => formRules,
|
|
684
|
+
formatGate: () => formatGate,
|
|
685
|
+
formatReport: () => formatReport,
|
|
541
686
|
formatValidationReport: () => formatValidationReport,
|
|
542
687
|
formatValidationReportJSON: () => formatValidationReportJSON,
|
|
543
688
|
formatValidationReportSARIF: () => formatValidationReportSARIF,
|
|
689
|
+
formatVerificationReport: () => formatVerificationReport,
|
|
544
690
|
generateDocs: () => generateDocs,
|
|
545
691
|
generateId: () => generateId,
|
|
546
692
|
generateTauriConfig: () => generateTauriConfig,
|
|
@@ -548,18 +694,34 @@ __export(src_exports, {
|
|
|
548
694
|
getEventPath: () => getEventPath,
|
|
549
695
|
getFactPath: () => getFactPath,
|
|
550
696
|
getSchemaPath: () => getSchemaPath,
|
|
697
|
+
initGateRule: () => initGateRule,
|
|
698
|
+
inputRules: () => inputRules,
|
|
551
699
|
isContract: () => isContract,
|
|
700
|
+
lintGate: () => lintGate,
|
|
552
701
|
loadSchema: () => loadSchema,
|
|
553
702
|
loadSchemaFromFile: () => loadSchemaFromFile,
|
|
554
703
|
loadSchemaFromJson: () => loadSchemaFromJson,
|
|
555
704
|
loadSchemaFromYaml: () => loadSchemaFromYaml,
|
|
705
|
+
loadingGateRule: () => loadingGateRule,
|
|
706
|
+
mustBeInitializedConstraint: () => mustBeInitializedConstraint,
|
|
707
|
+
navigationRequest: () => navigationRequest,
|
|
708
|
+
navigationRules: () => navigationRules,
|
|
709
|
+
noInteractionWhileLoadingConstraint: () => noInteractionWhileLoadingConstraint,
|
|
710
|
+
offlineIndicatorRule: () => offlineIndicatorRule,
|
|
556
711
|
registerSchema: () => registerSchema,
|
|
712
|
+
resizeEvent: () => resizeEvent,
|
|
557
713
|
runTerminalCommand: () => runTerminalCommand,
|
|
558
714
|
schemaToCanvas: () => schemaToCanvas,
|
|
715
|
+
semverContract: () => semverContract,
|
|
716
|
+
toastRules: () => toastRules,
|
|
717
|
+
uiModule: () => uiModule,
|
|
718
|
+
uiStateChanged: () => uiStateChanged,
|
|
559
719
|
validateContracts: () => validateContracts,
|
|
560
720
|
validateForGeneration: () => validateForGeneration,
|
|
561
721
|
validateSchema: () => validateSchema,
|
|
562
|
-
validateWithGuardian: () => validateWithGuardian
|
|
722
|
+
validateWithGuardian: () => validateWithGuardian,
|
|
723
|
+
verify: () => verify,
|
|
724
|
+
viewportRule: () => viewportRule
|
|
563
725
|
});
|
|
564
726
|
module.exports = __toCommonJS(src_exports);
|
|
565
727
|
init_protocol();
|
|
@@ -1336,8 +1498,8 @@ function defineFact(tag) {
|
|
|
1336
1498
|
create(payload) {
|
|
1337
1499
|
return { tag, payload };
|
|
1338
1500
|
},
|
|
1339
|
-
is(
|
|
1340
|
-
return
|
|
1501
|
+
is(fact2) {
|
|
1502
|
+
return fact2.tag === tag;
|
|
1341
1503
|
}
|
|
1342
1504
|
};
|
|
1343
1505
|
}
|
|
@@ -2090,16 +2252,16 @@ function validateSchema(schema) {
|
|
|
2090
2252
|
if (schema.logic) {
|
|
2091
2253
|
schema.logic.forEach((logic, logicIndex) => {
|
|
2092
2254
|
if (logic.facts) {
|
|
2093
|
-
logic.facts.forEach((
|
|
2094
|
-
if (!
|
|
2255
|
+
logic.facts.forEach((fact2, factIndex) => {
|
|
2256
|
+
if (!fact2.tag) {
|
|
2095
2257
|
errors.push({
|
|
2096
2258
|
path: `logic[${logicIndex}].facts[${factIndex}].tag`,
|
|
2097
2259
|
message: "Fact tag is required"
|
|
2098
2260
|
});
|
|
2099
|
-
} else if (!isValidIdentifier(
|
|
2261
|
+
} else if (!isValidIdentifier(fact2.tag)) {
|
|
2100
2262
|
errors.push({
|
|
2101
2263
|
path: `logic[${logicIndex}].facts[${factIndex}].tag`,
|
|
2102
|
-
message: `Fact tag "${
|
|
2264
|
+
message: `Fact tag "${fact2.tag}" is not a valid JavaScript identifier. Use only letters, numbers, underscores, and dollar signs, and do not start with a number.`
|
|
2103
2265
|
});
|
|
2104
2266
|
}
|
|
2105
2267
|
});
|
|
@@ -2484,37 +2646,37 @@ var PraxisDBStore = class {
|
|
|
2484
2646
|
* @param fact The fact to store
|
|
2485
2647
|
* @returns Promise that resolves when the fact is stored
|
|
2486
2648
|
*/
|
|
2487
|
-
async storeFact(
|
|
2488
|
-
const constraintResult = await this.checkConstraints([
|
|
2649
|
+
async storeFact(fact2) {
|
|
2650
|
+
const constraintResult = await this.checkConstraints([fact2]);
|
|
2489
2651
|
if (!constraintResult.valid) {
|
|
2490
2652
|
throw new Error(`Constraint violation: ${constraintResult.errors.join(", ")}`);
|
|
2491
2653
|
}
|
|
2492
2654
|
let before;
|
|
2493
2655
|
if (this.chronicle) {
|
|
2494
|
-
const payload =
|
|
2656
|
+
const payload = fact2.payload;
|
|
2495
2657
|
const id = payload?.id;
|
|
2496
2658
|
if (id) {
|
|
2497
|
-
before = await this.getFact(
|
|
2659
|
+
before = await this.getFact(fact2.tag, id);
|
|
2498
2660
|
}
|
|
2499
2661
|
}
|
|
2500
|
-
await this.persistFact(
|
|
2662
|
+
await this.persistFact(fact2);
|
|
2501
2663
|
if (this.chronicle) {
|
|
2502
|
-
const payload =
|
|
2664
|
+
const payload = fact2.payload;
|
|
2503
2665
|
const id = payload?.id ?? "";
|
|
2504
2666
|
const span = ChronicleContext.current;
|
|
2505
2667
|
try {
|
|
2506
2668
|
await this.chronicle.record({
|
|
2507
|
-
path: getFactPath(
|
|
2669
|
+
path: getFactPath(fact2.tag, id),
|
|
2508
2670
|
before,
|
|
2509
|
-
after:
|
|
2671
|
+
after: fact2,
|
|
2510
2672
|
cause: span?.spanId,
|
|
2511
2673
|
context: span?.contextId,
|
|
2512
|
-
metadata: { factTag:
|
|
2674
|
+
metadata: { factTag: fact2.tag, operation: "storeFact" }
|
|
2513
2675
|
});
|
|
2514
2676
|
} catch {
|
|
2515
2677
|
}
|
|
2516
2678
|
}
|
|
2517
|
-
await this.triggerRules([
|
|
2679
|
+
await this.triggerRules([fact2]);
|
|
2518
2680
|
}
|
|
2519
2681
|
/**
|
|
2520
2682
|
* Store multiple facts in PluresDB
|
|
@@ -2526,28 +2688,28 @@ var PraxisDBStore = class {
|
|
|
2526
2688
|
if (!constraintResult.valid) {
|
|
2527
2689
|
throw new Error(`Constraint violation: ${constraintResult.errors.join(", ")}`);
|
|
2528
2690
|
}
|
|
2529
|
-
for (const
|
|
2691
|
+
for (const fact2 of facts) {
|
|
2530
2692
|
let before;
|
|
2531
2693
|
if (this.chronicle) {
|
|
2532
|
-
const payload =
|
|
2694
|
+
const payload = fact2.payload;
|
|
2533
2695
|
const id = payload?.id;
|
|
2534
2696
|
if (id) {
|
|
2535
|
-
before = await this.getFact(
|
|
2697
|
+
before = await this.getFact(fact2.tag, id);
|
|
2536
2698
|
}
|
|
2537
2699
|
}
|
|
2538
|
-
await this.persistFact(
|
|
2700
|
+
await this.persistFact(fact2);
|
|
2539
2701
|
if (this.chronicle) {
|
|
2540
|
-
const payload =
|
|
2702
|
+
const payload = fact2.payload;
|
|
2541
2703
|
const id = payload?.id ?? "";
|
|
2542
2704
|
const span = ChronicleContext.current;
|
|
2543
2705
|
try {
|
|
2544
2706
|
await this.chronicle.record({
|
|
2545
|
-
path: getFactPath(
|
|
2707
|
+
path: getFactPath(fact2.tag, id),
|
|
2546
2708
|
before,
|
|
2547
|
-
after:
|
|
2709
|
+
after: fact2,
|
|
2548
2710
|
cause: span?.spanId,
|
|
2549
2711
|
context: span?.contextId,
|
|
2550
|
-
metadata: { factTag:
|
|
2712
|
+
metadata: { factTag: fact2.tag, operation: "storeFacts" }
|
|
2551
2713
|
});
|
|
2552
2714
|
} catch {
|
|
2553
2715
|
}
|
|
@@ -2559,11 +2721,11 @@ var PraxisDBStore = class {
|
|
|
2559
2721
|
* Internal method to persist a fact without constraint checking
|
|
2560
2722
|
* Used by both storeFact and derived fact storage
|
|
2561
2723
|
*/
|
|
2562
|
-
async persistFact(
|
|
2563
|
-
const payload =
|
|
2724
|
+
async persistFact(fact2) {
|
|
2725
|
+
const payload = fact2.payload;
|
|
2564
2726
|
const id = payload?.id ?? generateId();
|
|
2565
|
-
const path3 = getFactPath(
|
|
2566
|
-
await this.db.set(path3,
|
|
2727
|
+
const path3 = getFactPath(fact2.tag, id);
|
|
2728
|
+
await this.db.set(path3, fact2);
|
|
2567
2729
|
}
|
|
2568
2730
|
/**
|
|
2569
2731
|
* Get a fact by tag and id
|
|
@@ -2700,8 +2862,8 @@ var PraxisDBStore = class {
|
|
|
2700
2862
|
if (watchers) {
|
|
2701
2863
|
watchers.add(callback);
|
|
2702
2864
|
}
|
|
2703
|
-
const unsubscribe = this.db.watch(path3, (
|
|
2704
|
-
callback([
|
|
2865
|
+
const unsubscribe = this.db.watch(path3, (fact2) => {
|
|
2866
|
+
callback([fact2]);
|
|
2705
2867
|
});
|
|
2706
2868
|
this.subscriptions.push(unsubscribe);
|
|
2707
2869
|
return () => {
|
|
@@ -2759,13 +2921,18 @@ var PraxisDBStore = class {
|
|
|
2759
2921
|
const state2 = {
|
|
2760
2922
|
context: this.context,
|
|
2761
2923
|
facts: [],
|
|
2924
|
+
events,
|
|
2762
2925
|
meta: {}
|
|
2763
2926
|
};
|
|
2764
2927
|
const derivedFacts = [];
|
|
2765
2928
|
for (const rule of rules) {
|
|
2766
2929
|
try {
|
|
2767
|
-
const
|
|
2768
|
-
|
|
2930
|
+
const result = rule.impl(state2, events);
|
|
2931
|
+
if (Array.isArray(result)) {
|
|
2932
|
+
derivedFacts.push(...result);
|
|
2933
|
+
} else if (result && "kind" in result && result.kind === "emit") {
|
|
2934
|
+
derivedFacts.push(...result.facts);
|
|
2935
|
+
}
|
|
2769
2936
|
} catch (error) {
|
|
2770
2937
|
this.onRuleError(rule.id, error);
|
|
2771
2938
|
}
|
|
@@ -2773,19 +2940,19 @@ var PraxisDBStore = class {
|
|
|
2773
2940
|
if (derivedFacts.length > 0) {
|
|
2774
2941
|
const constraintResult = await this.checkConstraints(derivedFacts);
|
|
2775
2942
|
if (constraintResult.valid) {
|
|
2776
|
-
for (const
|
|
2777
|
-
await this.persistFact(
|
|
2943
|
+
for (const fact2 of derivedFacts) {
|
|
2944
|
+
await this.persistFact(fact2);
|
|
2778
2945
|
if (this.chronicle) {
|
|
2779
|
-
const payload =
|
|
2946
|
+
const payload = fact2.payload;
|
|
2780
2947
|
const id = payload?.id ?? "";
|
|
2781
2948
|
const span = ChronicleContext.current;
|
|
2782
2949
|
try {
|
|
2783
2950
|
await this.chronicle.record({
|
|
2784
|
-
path: getFactPath(
|
|
2785
|
-
after:
|
|
2951
|
+
path: getFactPath(fact2.tag, id),
|
|
2952
|
+
after: fact2,
|
|
2786
2953
|
cause: span?.spanId,
|
|
2787
2954
|
context: span?.contextId,
|
|
2788
|
-
metadata: { factTag:
|
|
2955
|
+
metadata: { factTag: fact2.tag, operation: "derivedFact" }
|
|
2789
2956
|
});
|
|
2790
2957
|
} catch {
|
|
2791
2958
|
}
|
|
@@ -3398,7 +3565,7 @@ async function createUnumAdapter(config) {
|
|
|
3398
3565
|
type: "event"
|
|
3399
3566
|
});
|
|
3400
3567
|
}
|
|
3401
|
-
async function broadcastFact(channelId,
|
|
3568
|
+
async function broadcastFact(channelId, fact2) {
|
|
3402
3569
|
const channel = channels.get(channelId);
|
|
3403
3570
|
if (!channel) {
|
|
3404
3571
|
throw new Error(`Not joined to channel ${channelId}`);
|
|
@@ -3406,7 +3573,7 @@ async function createUnumAdapter(config) {
|
|
|
3406
3573
|
await channel.publish({
|
|
3407
3574
|
id: generateId2(),
|
|
3408
3575
|
sender: currentIdentity || { id: "anonymous", createdAt: Date.now() },
|
|
3409
|
-
content:
|
|
3576
|
+
content: fact2,
|
|
3410
3577
|
type: "fact"
|
|
3411
3578
|
});
|
|
3412
3579
|
}
|
|
@@ -3549,17 +3716,17 @@ function schemaToCanvas(schema, _options = {}) {
|
|
|
3549
3716
|
yOffset += schema.events.length * ySpacing + 30;
|
|
3550
3717
|
}
|
|
3551
3718
|
if (schema.facts) {
|
|
3552
|
-
schema.facts.forEach((
|
|
3553
|
-
const pos =
|
|
3719
|
+
schema.facts.forEach((fact2, index) => {
|
|
3720
|
+
const pos = fact2.position && (fact2.position.x !== 0 || fact2.position.y !== 0) ? fact2.position : { x: 50 + xSpacing * 3, y: yOffset + index * ySpacing };
|
|
3554
3721
|
const node = {
|
|
3555
|
-
id:
|
|
3722
|
+
id: fact2.id || `fact-${nodeId++}`,
|
|
3556
3723
|
type: "fact",
|
|
3557
|
-
label:
|
|
3724
|
+
label: fact2.tag,
|
|
3558
3725
|
x: pos.x,
|
|
3559
3726
|
y: pos.y,
|
|
3560
3727
|
width: 150,
|
|
3561
3728
|
height: 50,
|
|
3562
|
-
data:
|
|
3729
|
+
data: fact2,
|
|
3563
3730
|
style: {
|
|
3564
3731
|
backgroundColor: "#fce4ec",
|
|
3565
3732
|
borderColor: "#c2185b"
|
|
@@ -4014,8 +4181,8 @@ var StateDocsGenerator = class {
|
|
|
4014
4181
|
if (logic.facts && logic.facts.length > 0) {
|
|
4015
4182
|
lines.push("**Facts:**");
|
|
4016
4183
|
lines.push("");
|
|
4017
|
-
for (const
|
|
4018
|
-
lines.push(`- \`${
|
|
4184
|
+
for (const fact2 of logic.facts) {
|
|
4185
|
+
lines.push(`- \`${fact2.tag}\`: ${fact2.description || ""}`);
|
|
4019
4186
|
}
|
|
4020
4187
|
lines.push("");
|
|
4021
4188
|
}
|
|
@@ -4147,9 +4314,9 @@ var StateDocsGenerator = class {
|
|
|
4147
4314
|
lines.push("");
|
|
4148
4315
|
lines.push("| Fact | Description | Payload |");
|
|
4149
4316
|
lines.push("|------|-------------|---------|");
|
|
4150
|
-
for (const
|
|
4151
|
-
const payload =
|
|
4152
|
-
lines.push(`| \`${
|
|
4317
|
+
for (const fact2 of logic.facts) {
|
|
4318
|
+
const payload = fact2.payload ? Object.entries(fact2.payload).map(([k, v]) => `${k}: ${v}`).join(", ") : "-";
|
|
4319
|
+
lines.push(`| \`${fact2.tag}\` | ${fact2.description || "-"} | ${payload} |`);
|
|
4153
4320
|
}
|
|
4154
4321
|
lines.push("");
|
|
4155
4322
|
}
|
|
@@ -4201,8 +4368,8 @@ var StateDocsGenerator = class {
|
|
|
4201
4368
|
for (const event of logic.events) {
|
|
4202
4369
|
lines.push(` Processing --> ${event.tag.replace(/[^a-zA-Z0-9]/g, "")}: ${event.tag}`);
|
|
4203
4370
|
}
|
|
4204
|
-
for (const
|
|
4205
|
-
lines.push(` ${
|
|
4371
|
+
for (const fact2 of logic.facts) {
|
|
4372
|
+
lines.push(` ${fact2.tag.replace(/[^a-zA-Z0-9]/g, "")} --> [*]`);
|
|
4206
4373
|
}
|
|
4207
4374
|
}
|
|
4208
4375
|
return {
|
|
@@ -4653,6 +4820,1379 @@ async function attachAllIntegrations(engine, registry, options = {}) {
|
|
|
4653
4820
|
}
|
|
4654
4821
|
};
|
|
4655
4822
|
}
|
|
4823
|
+
|
|
4824
|
+
// src/index.ts
|
|
4825
|
+
init_rule_result();
|
|
4826
|
+
|
|
4827
|
+
// src/core/ui-rules.ts
|
|
4828
|
+
init_rule_result();
|
|
4829
|
+
var loadingGateRule = {
|
|
4830
|
+
id: "ui/loading-gate",
|
|
4831
|
+
description: "Signals when the app is in a loading state",
|
|
4832
|
+
eventTypes: ["ui.state-change", "app.init"],
|
|
4833
|
+
impl: (state2) => {
|
|
4834
|
+
const ctx = state2.context;
|
|
4835
|
+
if (ctx.loading) {
|
|
4836
|
+
return RuleResult.emit([fact("ui.loading-gate", { active: true })]);
|
|
4837
|
+
}
|
|
4838
|
+
return RuleResult.retract(["ui.loading-gate"], "Not loading");
|
|
4839
|
+
},
|
|
4840
|
+
contract: {
|
|
4841
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
4842
|
+
behavior: "Emits ui.loading-gate when context.loading is true, retracts when false",
|
|
4843
|
+
examples: [
|
|
4844
|
+
{ given: "loading is true", when: "ui state changes", then: "ui.loading-gate emitted" },
|
|
4845
|
+
{ given: "loading is false", when: "ui state changes", then: "ui.loading-gate retracted" }
|
|
4846
|
+
],
|
|
4847
|
+
invariants: ["Loading gate must reflect context.loading exactly"]
|
|
4848
|
+
}
|
|
4849
|
+
};
|
|
4850
|
+
var errorDisplayRule = {
|
|
4851
|
+
id: "ui/error-display",
|
|
4852
|
+
description: "Signals when an error should be displayed to the user",
|
|
4853
|
+
eventTypes: ["ui.state-change", "app.error"],
|
|
4854
|
+
impl: (state2) => {
|
|
4855
|
+
const ctx = state2.context;
|
|
4856
|
+
if (ctx.error) {
|
|
4857
|
+
return RuleResult.emit([fact("ui.error-display", { message: ctx.error, severity: "error" })]);
|
|
4858
|
+
}
|
|
4859
|
+
return RuleResult.retract(["ui.error-display"], "Error cleared");
|
|
4860
|
+
},
|
|
4861
|
+
contract: {
|
|
4862
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
4863
|
+
behavior: "Emits ui.error-display when context.error is non-null, retracts when cleared",
|
|
4864
|
+
examples: [
|
|
4865
|
+
{ given: "error is set", when: "ui state changes", then: "ui.error-display emitted with message" },
|
|
4866
|
+
{ given: "error is null", when: "ui state changes", then: "ui.error-display retracted" }
|
|
4867
|
+
],
|
|
4868
|
+
invariants: ["Error display must clear when error is null"]
|
|
4869
|
+
}
|
|
4870
|
+
};
|
|
4871
|
+
var offlineIndicatorRule = {
|
|
4872
|
+
id: "ui/offline-indicator",
|
|
4873
|
+
description: "Signals when the app is offline",
|
|
4874
|
+
eventTypes: ["ui.state-change", "network.change"],
|
|
4875
|
+
impl: (state2) => {
|
|
4876
|
+
if (state2.context.offline) {
|
|
4877
|
+
return RuleResult.emit([fact("ui.offline", { message: "You are offline. Changes will sync when reconnected." })]);
|
|
4878
|
+
}
|
|
4879
|
+
return RuleResult.retract(["ui.offline"], "Back online");
|
|
4880
|
+
},
|
|
4881
|
+
contract: {
|
|
4882
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
4883
|
+
behavior: "Emits ui.offline when context.offline is true, retracts when back online",
|
|
4884
|
+
examples: [
|
|
4885
|
+
{ given: "offline is true", when: "network changes", then: "ui.offline emitted" },
|
|
4886
|
+
{ given: "offline is false", when: "network changes", then: "ui.offline retracted" }
|
|
4887
|
+
],
|
|
4888
|
+
invariants: ["Offline indicator must match actual connectivity"]
|
|
4889
|
+
}
|
|
4890
|
+
};
|
|
4891
|
+
var dirtyGuardRule = {
|
|
4892
|
+
id: "ui/dirty-guard",
|
|
4893
|
+
description: "Warns when there are unsaved changes",
|
|
4894
|
+
eventTypes: ["ui.state-change", "navigation.request"],
|
|
4895
|
+
impl: (state2) => {
|
|
4896
|
+
if (state2.context.dirty) {
|
|
4897
|
+
return RuleResult.emit([fact("ui.unsaved-warning", {
|
|
4898
|
+
message: "You have unsaved changes",
|
|
4899
|
+
blocking: true
|
|
4900
|
+
})]);
|
|
4901
|
+
}
|
|
4902
|
+
return RuleResult.retract(["ui.unsaved-warning"], "No unsaved changes");
|
|
4903
|
+
},
|
|
4904
|
+
contract: {
|
|
4905
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
4906
|
+
behavior: "Emits ui.unsaved-warning when context.dirty is true, retracts when saved",
|
|
4907
|
+
examples: [
|
|
4908
|
+
{ given: "dirty is true", when: "ui state changes", then: "ui.unsaved-warning emitted with blocking=true" },
|
|
4909
|
+
{ given: "dirty is false", when: "ui state changes", then: "ui.unsaved-warning retracted" }
|
|
4910
|
+
],
|
|
4911
|
+
invariants: ["Dirty guard must clear after save"]
|
|
4912
|
+
}
|
|
4913
|
+
};
|
|
4914
|
+
var initGateRule = {
|
|
4915
|
+
id: "ui/init-gate",
|
|
4916
|
+
description: "Signals whether the app has completed initialization",
|
|
4917
|
+
eventTypes: ["ui.state-change", "app.init"],
|
|
4918
|
+
impl: (state2) => {
|
|
4919
|
+
if (!state2.context.initialized) {
|
|
4920
|
+
return RuleResult.emit([fact("ui.init-pending", {
|
|
4921
|
+
message: "App is initializing..."
|
|
4922
|
+
})]);
|
|
4923
|
+
}
|
|
4924
|
+
return RuleResult.retract(["ui.init-pending"], "App initialized");
|
|
4925
|
+
},
|
|
4926
|
+
contract: {
|
|
4927
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
4928
|
+
behavior: "Emits ui.init-pending until context.initialized is true",
|
|
4929
|
+
examples: [
|
|
4930
|
+
{ given: "initialized is false", when: "app starts", then: "ui.init-pending emitted" },
|
|
4931
|
+
{ given: "initialized is true", when: "init completes", then: "ui.init-pending retracted" }
|
|
4932
|
+
],
|
|
4933
|
+
invariants: ["Init gate must clear exactly once, when initialization completes"]
|
|
4934
|
+
}
|
|
4935
|
+
};
|
|
4936
|
+
var viewportRule = {
|
|
4937
|
+
id: "ui/viewport-class",
|
|
4938
|
+
description: "Classifies viewport size for responsive layout decisions",
|
|
4939
|
+
eventTypes: ["ui.state-change", "ui.resize"],
|
|
4940
|
+
impl: (state2) => {
|
|
4941
|
+
const vp = state2.context.viewport;
|
|
4942
|
+
if (!vp) return RuleResult.skip("No viewport data");
|
|
4943
|
+
return RuleResult.emit([fact("ui.viewport-class", {
|
|
4944
|
+
viewport: vp,
|
|
4945
|
+
compact: vp === "mobile",
|
|
4946
|
+
showSidebar: vp !== "mobile"
|
|
4947
|
+
})]);
|
|
4948
|
+
},
|
|
4949
|
+
contract: {
|
|
4950
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
4951
|
+
behavior: "Classifies viewport into responsive layout hints",
|
|
4952
|
+
examples: [
|
|
4953
|
+
{ given: "viewport is mobile", when: "resize event", then: "compact=true, showSidebar=false" },
|
|
4954
|
+
{ given: "viewport is desktop", when: "resize event", then: "compact=false, showSidebar=true" }
|
|
4955
|
+
],
|
|
4956
|
+
invariants: ["Viewport class must update on every resize event"]
|
|
4957
|
+
}
|
|
4958
|
+
};
|
|
4959
|
+
var noInteractionWhileLoadingConstraint = {
|
|
4960
|
+
id: "ui/no-interaction-while-loading",
|
|
4961
|
+
description: "Prevents data mutations while a load operation is in progress",
|
|
4962
|
+
impl: (state2) => {
|
|
4963
|
+
if (state2.context.loading) {
|
|
4964
|
+
return "Cannot perform action while data is loading";
|
|
4965
|
+
}
|
|
4966
|
+
return true;
|
|
4967
|
+
},
|
|
4968
|
+
contract: {
|
|
4969
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
4970
|
+
behavior: "Fails when context.loading is true",
|
|
4971
|
+
examples: [
|
|
4972
|
+
{ given: "loading is true", when: "action attempted", then: "violation" },
|
|
4973
|
+
{ given: "loading is false", when: "action attempted", then: "pass" }
|
|
4974
|
+
],
|
|
4975
|
+
invariants: ["Must always fail during loading"]
|
|
4976
|
+
}
|
|
4977
|
+
};
|
|
4978
|
+
var mustBeInitializedConstraint = {
|
|
4979
|
+
id: "ui/must-be-initialized",
|
|
4980
|
+
description: "Requires app initialization before user interactions",
|
|
4981
|
+
impl: (state2) => {
|
|
4982
|
+
if (!state2.context.initialized) {
|
|
4983
|
+
return "App must be initialized before performing this action";
|
|
4984
|
+
}
|
|
4985
|
+
return true;
|
|
4986
|
+
},
|
|
4987
|
+
contract: {
|
|
4988
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
4989
|
+
behavior: "Fails when context.initialized is false",
|
|
4990
|
+
examples: [
|
|
4991
|
+
{ given: "initialized is false", when: "action attempted", then: "violation" },
|
|
4992
|
+
{ given: "initialized is true", when: "action attempted", then: "pass" }
|
|
4993
|
+
],
|
|
4994
|
+
invariants: ["Must always fail before init completes"]
|
|
4995
|
+
}
|
|
4996
|
+
};
|
|
4997
|
+
var uiModule = {
|
|
4998
|
+
rules: [
|
|
4999
|
+
loadingGateRule,
|
|
5000
|
+
errorDisplayRule,
|
|
5001
|
+
offlineIndicatorRule,
|
|
5002
|
+
dirtyGuardRule,
|
|
5003
|
+
initGateRule,
|
|
5004
|
+
viewportRule
|
|
5005
|
+
],
|
|
5006
|
+
constraints: [
|
|
5007
|
+
noInteractionWhileLoadingConstraint,
|
|
5008
|
+
mustBeInitializedConstraint
|
|
5009
|
+
],
|
|
5010
|
+
meta: {
|
|
5011
|
+
name: "praxis-ui",
|
|
5012
|
+
version: "1.0.0",
|
|
5013
|
+
description: "Predefined UI rules and constraints \u2014 separate from business logic"
|
|
5014
|
+
}
|
|
5015
|
+
};
|
|
5016
|
+
function createUIModule(options) {
|
|
5017
|
+
const allRules = uiModule.rules;
|
|
5018
|
+
const allConstraints = uiModule.constraints;
|
|
5019
|
+
const selectedRules = options.rules ? allRules.filter((r) => options.rules.includes(r.id)) : allRules;
|
|
5020
|
+
const selectedConstraints = options.constraints ? allConstraints.filter((c) => options.constraints.includes(c.id)) : allConstraints;
|
|
5021
|
+
return {
|
|
5022
|
+
rules: [...selectedRules, ...options.extraRules ?? []],
|
|
5023
|
+
constraints: [...selectedConstraints, ...options.extraConstraints ?? []],
|
|
5024
|
+
meta: { ...uiModule.meta, customized: true }
|
|
5025
|
+
};
|
|
5026
|
+
}
|
|
5027
|
+
function uiStateChanged(changes) {
|
|
5028
|
+
return { tag: "ui.state-change", payload: changes ?? {} };
|
|
5029
|
+
}
|
|
5030
|
+
function navigationRequest(to) {
|
|
5031
|
+
return { tag: "navigation.request", payload: { to } };
|
|
5032
|
+
}
|
|
5033
|
+
function resizeEvent(width, height) {
|
|
5034
|
+
return { tag: "ui.resize", payload: { width, height } };
|
|
5035
|
+
}
|
|
5036
|
+
|
|
5037
|
+
// src/core/completeness.ts
|
|
5038
|
+
function auditCompleteness(manifest, registryRuleIds, registryConstraintIds, rulesWithContracts, config) {
|
|
5039
|
+
const threshold = config?.threshold ?? 90;
|
|
5040
|
+
const domainBranches = manifest.branches.filter((b) => b.kind === "domain");
|
|
5041
|
+
const coveredDomain = domainBranches.filter((b) => b.coveredBy && registryRuleIds.includes(b.coveredBy));
|
|
5042
|
+
const uncoveredDomain = domainBranches.filter((b) => !b.coveredBy || !registryRuleIds.includes(b.coveredBy));
|
|
5043
|
+
const invariantBranches = manifest.branches.filter((b) => b.kind === "invariant");
|
|
5044
|
+
const coveredInvariants = invariantBranches.filter((b) => b.coveredBy && registryConstraintIds.includes(b.coveredBy));
|
|
5045
|
+
const uncoveredInvariants = invariantBranches.filter((b) => !b.coveredBy || !registryConstraintIds.includes(b.coveredBy));
|
|
5046
|
+
const needContracts = manifest.rulesNeedingContracts;
|
|
5047
|
+
const haveContracts = needContracts.filter((id) => rulesWithContracts.includes(id));
|
|
5048
|
+
const missingContracts = needContracts.filter((id) => !rulesWithContracts.includes(id));
|
|
5049
|
+
const neededFields = manifest.stateFields.filter((f) => f.usedByRule);
|
|
5050
|
+
const coveredFields = neededFields.filter((f) => f.inContext);
|
|
5051
|
+
const missingFields = neededFields.filter((f) => !f.inContext);
|
|
5052
|
+
const coveredTransitions = manifest.transitions.filter((t) => t.eventTag);
|
|
5053
|
+
const missingTransitions = manifest.transitions.filter((t) => !t.eventTag);
|
|
5054
|
+
const ruleScore = domainBranches.length > 0 ? coveredDomain.length / domainBranches.length * 40 : 40;
|
|
5055
|
+
const constraintScore = invariantBranches.length > 0 ? coveredInvariants.length / invariantBranches.length * 20 : 20;
|
|
5056
|
+
const contractScore = needContracts.length > 0 ? haveContracts.length / needContracts.length * 15 : 15;
|
|
5057
|
+
const contextScore = neededFields.length > 0 ? coveredFields.length / neededFields.length * 15 : 15;
|
|
5058
|
+
const eventScore = manifest.transitions.length > 0 ? coveredTransitions.length / manifest.transitions.length * 10 : 10;
|
|
5059
|
+
const score = Math.round(ruleScore + constraintScore + contractScore + contextScore + eventScore);
|
|
5060
|
+
const rating = score >= 90 ? "complete" : score >= 70 ? "good" : score >= 50 ? "partial" : "incomplete";
|
|
5061
|
+
const report = {
|
|
5062
|
+
score,
|
|
5063
|
+
rating,
|
|
5064
|
+
rules: { total: domainBranches.length, covered: coveredDomain.length, uncovered: uncoveredDomain },
|
|
5065
|
+
constraints: { total: invariantBranches.length, covered: coveredInvariants.length, uncovered: uncoveredInvariants },
|
|
5066
|
+
contracts: { total: needContracts.length, withContracts: haveContracts.length, missing: missingContracts },
|
|
5067
|
+
context: { total: neededFields.length, covered: coveredFields.length, missing: missingFields },
|
|
5068
|
+
events: { total: manifest.transitions.length, covered: coveredTransitions.length, missing: missingTransitions }
|
|
5069
|
+
};
|
|
5070
|
+
if (config?.strict && score < threshold) {
|
|
5071
|
+
throw new Error(`Praxis completeness ${score}/100 (${rating}) \u2014 below threshold ${threshold}. ${uncoveredDomain.length} uncovered rules, ${uncoveredInvariants.length} uncovered invariants, ${missingContracts.length} missing contracts.`);
|
|
5072
|
+
}
|
|
5073
|
+
return report;
|
|
5074
|
+
}
|
|
5075
|
+
function formatReport(report) {
|
|
5076
|
+
const lines = [];
|
|
5077
|
+
const icon = report.rating === "complete" ? "\u2705" : report.rating === "good" ? "\u{1F7E2}" : report.rating === "partial" ? "\u{1F7E1}" : "\u{1F534}";
|
|
5078
|
+
lines.push(`${icon} Praxis Completeness: ${report.score}/100 (${report.rating})`);
|
|
5079
|
+
lines.push("");
|
|
5080
|
+
lines.push(`Rules: ${report.rules.covered}/${report.rules.total} domain branches covered (${pct(report.rules.covered, report.rules.total)})`);
|
|
5081
|
+
lines.push(`Constraints: ${report.constraints.covered}/${report.constraints.total} invariants covered (${pct(report.constraints.covered, report.constraints.total)})`);
|
|
5082
|
+
lines.push(`Contracts: ${report.contracts.withContracts}/${report.contracts.total} rules have contracts (${pct(report.contracts.withContracts, report.contracts.total)})`);
|
|
5083
|
+
lines.push(`Context: ${report.context.covered}/${report.context.total} state fields in context (${pct(report.context.covered, report.context.total)})`);
|
|
5084
|
+
lines.push(`Events: ${report.events.covered}/${report.events.total} transitions have events (${pct(report.events.covered, report.events.total)})`);
|
|
5085
|
+
if (report.rules.uncovered.length > 0) {
|
|
5086
|
+
lines.push("");
|
|
5087
|
+
lines.push("Uncovered domain logic:");
|
|
5088
|
+
for (const b of report.rules.uncovered) {
|
|
5089
|
+
lines.push(` \u274C ${b.location}: ${b.condition}${b.note ? ` \u2014 ${b.note}` : ""}`);
|
|
5090
|
+
}
|
|
5091
|
+
}
|
|
5092
|
+
if (report.constraints.uncovered.length > 0) {
|
|
5093
|
+
lines.push("");
|
|
5094
|
+
lines.push("Uncovered invariants:");
|
|
5095
|
+
for (const b of report.constraints.uncovered) {
|
|
5096
|
+
lines.push(` \u274C ${b.location}: ${b.condition}${b.note ? ` \u2014 ${b.note}` : ""}`);
|
|
5097
|
+
}
|
|
5098
|
+
}
|
|
5099
|
+
if (report.contracts.missing.length > 0) {
|
|
5100
|
+
lines.push("");
|
|
5101
|
+
lines.push("Rules missing contracts:");
|
|
5102
|
+
for (const id of report.contracts.missing) {
|
|
5103
|
+
lines.push(` \u{1F4DD} ${id}`);
|
|
5104
|
+
}
|
|
5105
|
+
}
|
|
5106
|
+
if (report.events.missing.length > 0) {
|
|
5107
|
+
lines.push("");
|
|
5108
|
+
lines.push("State transitions without events:");
|
|
5109
|
+
for (const t of report.events.missing) {
|
|
5110
|
+
lines.push(` \u26A1 ${t.location}: ${t.description}`);
|
|
5111
|
+
}
|
|
5112
|
+
}
|
|
5113
|
+
return lines.join("\n");
|
|
5114
|
+
}
|
|
5115
|
+
function pct(a, b) {
|
|
5116
|
+
if (b === 0) return "100%";
|
|
5117
|
+
return Math.round(a / b * 100) + "%";
|
|
5118
|
+
}
|
|
5119
|
+
|
|
5120
|
+
// src/expectations/expectations.ts
|
|
5121
|
+
var Expectation = class {
|
|
5122
|
+
name;
|
|
5123
|
+
_conditions = [];
|
|
5124
|
+
constructor(name) {
|
|
5125
|
+
this.name = name;
|
|
5126
|
+
}
|
|
5127
|
+
/**
|
|
5128
|
+
* Declare that this behavior should ONLY occur when a condition is true.
|
|
5129
|
+
* If the condition is false, the behavior should NOT occur.
|
|
5130
|
+
*/
|
|
5131
|
+
onlyWhen(condition) {
|
|
5132
|
+
this._conditions.push({ description: condition, type: "onlyWhen" });
|
|
5133
|
+
return this;
|
|
5134
|
+
}
|
|
5135
|
+
/**
|
|
5136
|
+
* Declare that this behavior should NEVER occur under a given condition.
|
|
5137
|
+
*/
|
|
5138
|
+
never(condition) {
|
|
5139
|
+
this._conditions.push({ description: condition, type: "never" });
|
|
5140
|
+
return this;
|
|
5141
|
+
}
|
|
5142
|
+
/**
|
|
5143
|
+
* Declare that this behavior should ALWAYS have a certain property.
|
|
5144
|
+
*/
|
|
5145
|
+
always(condition) {
|
|
5146
|
+
this._conditions.push({ description: condition, type: "always" });
|
|
5147
|
+
return this;
|
|
5148
|
+
}
|
|
5149
|
+
/** Get all declared conditions. */
|
|
5150
|
+
get conditions() {
|
|
5151
|
+
return this._conditions;
|
|
5152
|
+
}
|
|
5153
|
+
};
|
|
5154
|
+
var ExpectationSet = class {
|
|
5155
|
+
name;
|
|
5156
|
+
description;
|
|
5157
|
+
_expectations = [];
|
|
5158
|
+
constructor(options) {
|
|
5159
|
+
this.name = options.name;
|
|
5160
|
+
this.description = options.description ?? "";
|
|
5161
|
+
}
|
|
5162
|
+
/** Add an expectation to the set. */
|
|
5163
|
+
add(expectation) {
|
|
5164
|
+
this._expectations.push(expectation);
|
|
5165
|
+
return this;
|
|
5166
|
+
}
|
|
5167
|
+
/** Get all expectations in this set. */
|
|
5168
|
+
get expectations() {
|
|
5169
|
+
return this._expectations;
|
|
5170
|
+
}
|
|
5171
|
+
/** Number of expectations. */
|
|
5172
|
+
get size() {
|
|
5173
|
+
return this._expectations.length;
|
|
5174
|
+
}
|
|
5175
|
+
};
|
|
5176
|
+
function expectBehavior(name) {
|
|
5177
|
+
return new Expectation(name);
|
|
5178
|
+
}
|
|
5179
|
+
function verify(registry, expectations) {
|
|
5180
|
+
const rules = registry.getAllRules();
|
|
5181
|
+
const constraints = registry.getAllConstraints();
|
|
5182
|
+
const ruleIds = registry.getRuleIds();
|
|
5183
|
+
const constraintIds = registry.getConstraintIds();
|
|
5184
|
+
const allDescriptors = [...rules, ...constraints];
|
|
5185
|
+
const expectationResults = [];
|
|
5186
|
+
for (const exp of expectations.expectations) {
|
|
5187
|
+
const result = verifyExpectation(exp, allDescriptors, ruleIds, constraintIds);
|
|
5188
|
+
expectationResults.push(result);
|
|
5189
|
+
}
|
|
5190
|
+
const satisfied = expectationResults.filter((r) => r.status === "satisfied").length;
|
|
5191
|
+
const violated = expectationResults.filter((r) => r.status === "violated").length;
|
|
5192
|
+
const partial = expectationResults.filter((r) => r.status === "partial").length;
|
|
5193
|
+
const allEdgeCases = expectationResults.flatMap((r) => r.edgeCases);
|
|
5194
|
+
const allMitigations = expectationResults.flatMap((r) => r.mitigations);
|
|
5195
|
+
return {
|
|
5196
|
+
setName: expectations.name,
|
|
5197
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5198
|
+
status: violated > 0 ? "violated" : partial > 0 ? "partial" : "satisfied",
|
|
5199
|
+
expectations: expectationResults,
|
|
5200
|
+
summary: {
|
|
5201
|
+
total: expectationResults.length,
|
|
5202
|
+
satisfied,
|
|
5203
|
+
violated,
|
|
5204
|
+
partial
|
|
5205
|
+
},
|
|
5206
|
+
allEdgeCases,
|
|
5207
|
+
allMitigations
|
|
5208
|
+
};
|
|
5209
|
+
}
|
|
5210
|
+
function verifyExpectation(expectation, descriptors, ruleIds, _constraintIds) {
|
|
5211
|
+
const conditionResults = [];
|
|
5212
|
+
const edgeCases = [];
|
|
5213
|
+
const mitigations = [];
|
|
5214
|
+
const related = findRelatedDescriptors(expectation.name, descriptors);
|
|
5215
|
+
for (const condition of expectation.conditions) {
|
|
5216
|
+
const result = verifyCondition(condition, expectation.name, related, ruleIds);
|
|
5217
|
+
conditionResults.push(result);
|
|
5218
|
+
if (result.status === "unverifiable") {
|
|
5219
|
+
edgeCases.push(`Cannot verify "${condition.description}" for "${expectation.name}" \u2014 no covering rule/contract found`);
|
|
5220
|
+
mitigations.push(`Add a rule or constraint that explicitly covers: ${condition.description}`);
|
|
5221
|
+
} else if (result.status === "violated") {
|
|
5222
|
+
edgeCases.push(`"${expectation.name}" may fire incorrectly: ${condition.description}`);
|
|
5223
|
+
mitigations.push(`Review rule logic for "${expectation.name}" regarding: ${condition.description}`);
|
|
5224
|
+
}
|
|
5225
|
+
}
|
|
5226
|
+
const satisfiedCount = conditionResults.filter((r) => r.status === "satisfied").length;
|
|
5227
|
+
const violatedCount = conditionResults.filter((r) => r.status === "violated").length;
|
|
5228
|
+
const total = conditionResults.length;
|
|
5229
|
+
let status;
|
|
5230
|
+
if (total === 0) {
|
|
5231
|
+
status = "satisfied";
|
|
5232
|
+
} else if (violatedCount > 0) {
|
|
5233
|
+
status = "violated";
|
|
5234
|
+
} else if (satisfiedCount === total) {
|
|
5235
|
+
status = "satisfied";
|
|
5236
|
+
} else {
|
|
5237
|
+
status = "partial";
|
|
5238
|
+
}
|
|
5239
|
+
return {
|
|
5240
|
+
name: expectation.name,
|
|
5241
|
+
status,
|
|
5242
|
+
conditions: conditionResults,
|
|
5243
|
+
edgeCases,
|
|
5244
|
+
mitigations
|
|
5245
|
+
};
|
|
5246
|
+
}
|
|
5247
|
+
function textOverlaps(a, b) {
|
|
5248
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
5249
|
+
"the",
|
|
5250
|
+
"a",
|
|
5251
|
+
"an",
|
|
5252
|
+
"is",
|
|
5253
|
+
"are",
|
|
5254
|
+
"was",
|
|
5255
|
+
"were",
|
|
5256
|
+
"be",
|
|
5257
|
+
"been",
|
|
5258
|
+
"being",
|
|
5259
|
+
"have",
|
|
5260
|
+
"has",
|
|
5261
|
+
"had",
|
|
5262
|
+
"do",
|
|
5263
|
+
"does",
|
|
5264
|
+
"did",
|
|
5265
|
+
"will",
|
|
5266
|
+
"would",
|
|
5267
|
+
"could",
|
|
5268
|
+
"should",
|
|
5269
|
+
"may",
|
|
5270
|
+
"might",
|
|
5271
|
+
"shall",
|
|
5272
|
+
"can",
|
|
5273
|
+
"to",
|
|
5274
|
+
"of",
|
|
5275
|
+
"in",
|
|
5276
|
+
"for",
|
|
5277
|
+
"on",
|
|
5278
|
+
"with",
|
|
5279
|
+
"at",
|
|
5280
|
+
"by",
|
|
5281
|
+
"from",
|
|
5282
|
+
"as",
|
|
5283
|
+
"into",
|
|
5284
|
+
"through",
|
|
5285
|
+
"during",
|
|
5286
|
+
"before",
|
|
5287
|
+
"after",
|
|
5288
|
+
"when",
|
|
5289
|
+
"that",
|
|
5290
|
+
"this",
|
|
5291
|
+
"it",
|
|
5292
|
+
"its",
|
|
5293
|
+
"and",
|
|
5294
|
+
"or",
|
|
5295
|
+
"but",
|
|
5296
|
+
"not",
|
|
5297
|
+
"no",
|
|
5298
|
+
"if",
|
|
5299
|
+
"then",
|
|
5300
|
+
"than",
|
|
5301
|
+
"so",
|
|
5302
|
+
"up",
|
|
5303
|
+
"out",
|
|
5304
|
+
"about",
|
|
5305
|
+
"just",
|
|
5306
|
+
"must"
|
|
5307
|
+
]);
|
|
5308
|
+
const wordsA = a.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
|
|
5309
|
+
const wordsB = b.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
|
|
5310
|
+
if (wordsA.length === 0 || wordsB.length === 0) return false;
|
|
5311
|
+
const matches = wordsA.filter(
|
|
5312
|
+
(wa) => wordsB.some((wb) => wa.startsWith(wb.slice(0, 4)) || wb.startsWith(wa.slice(0, 4)))
|
|
5313
|
+
);
|
|
5314
|
+
const minWords = Math.min(wordsA.length, wordsB.length);
|
|
5315
|
+
return matches.length >= Math.max(1, Math.ceil(minWords * 0.5));
|
|
5316
|
+
}
|
|
5317
|
+
function verifyCondition(condition, expectationName, relatedDescriptors, _ruleIds) {
|
|
5318
|
+
if (relatedDescriptors.length === 0) {
|
|
5319
|
+
return {
|
|
5320
|
+
condition,
|
|
5321
|
+
status: "unverifiable",
|
|
5322
|
+
explanation: `No rules or constraints found related to "${expectationName}"`,
|
|
5323
|
+
relatedRules: []
|
|
5324
|
+
};
|
|
5325
|
+
}
|
|
5326
|
+
const relatedIds = relatedDescriptors.map((d) => d.id);
|
|
5327
|
+
const condLower = condition.description.toLowerCase();
|
|
5328
|
+
const matches = (target) => target.toLowerCase().includes(condLower) || condLower.includes(target.toLowerCase()) || textOverlaps(condLower, target);
|
|
5329
|
+
switch (condition.type) {
|
|
5330
|
+
case "onlyWhen": {
|
|
5331
|
+
const coveringRule = relatedDescriptors.find(
|
|
5332
|
+
(d) => d.contract?.examples.some(
|
|
5333
|
+
(ex) => matches(ex.given) || matches(ex.when)
|
|
5334
|
+
) || d.contract?.invariants.some((inv) => matches(inv))
|
|
5335
|
+
);
|
|
5336
|
+
if (coveringRule) {
|
|
5337
|
+
return {
|
|
5338
|
+
condition,
|
|
5339
|
+
status: "satisfied",
|
|
5340
|
+
explanation: `Rule "${coveringRule.id}" contract covers precondition: ${condition.description}`,
|
|
5341
|
+
relatedRules: relatedIds
|
|
5342
|
+
};
|
|
5343
|
+
}
|
|
5344
|
+
const descMatch = relatedDescriptors.find((d) => matches(d.description));
|
|
5345
|
+
if (descMatch) {
|
|
5346
|
+
return {
|
|
5347
|
+
condition,
|
|
5348
|
+
status: "satisfied",
|
|
5349
|
+
explanation: `Rule "${descMatch.id}" description addresses: ${condition.description}`,
|
|
5350
|
+
relatedRules: relatedIds
|
|
5351
|
+
};
|
|
5352
|
+
}
|
|
5353
|
+
return {
|
|
5354
|
+
condition,
|
|
5355
|
+
status: "unverifiable",
|
|
5356
|
+
explanation: `No rule contract explicitly covers the precondition: ${condition.description}`,
|
|
5357
|
+
relatedRules: relatedIds
|
|
5358
|
+
};
|
|
5359
|
+
}
|
|
5360
|
+
case "never": {
|
|
5361
|
+
const preventingDescriptor = relatedDescriptors.find(
|
|
5362
|
+
(d) => d.contract?.invariants.some((inv) => matches(inv)) || d.contract?.examples.some(
|
|
5363
|
+
(ex) => (matches(ex.given) || matches(ex.when)) && (ex.then.toLowerCase().includes("retract") || ex.then.toLowerCase().includes("fail") || ex.then.toLowerCase().includes("violation") || ex.then.toLowerCase().includes("skip") || ex.then.toLowerCase().includes("block") || ex.then.toLowerCase().includes("no "))
|
|
5364
|
+
) || d.contract?.behavior.toLowerCase().includes("block") || d.contract?.behavior.toLowerCase().includes("prevent")
|
|
5365
|
+
);
|
|
5366
|
+
if (preventingDescriptor) {
|
|
5367
|
+
return {
|
|
5368
|
+
condition,
|
|
5369
|
+
status: "satisfied",
|
|
5370
|
+
explanation: `Constraint/rule "${preventingDescriptor.id}" prevents: ${condition.description}`,
|
|
5371
|
+
relatedRules: relatedIds
|
|
5372
|
+
};
|
|
5373
|
+
}
|
|
5374
|
+
return {
|
|
5375
|
+
condition,
|
|
5376
|
+
status: "unverifiable",
|
|
5377
|
+
explanation: `No rule or constraint explicitly prevents: ${condition.description}`,
|
|
5378
|
+
relatedRules: relatedIds
|
|
5379
|
+
};
|
|
5380
|
+
}
|
|
5381
|
+
case "always": {
|
|
5382
|
+
const guaranteeing = relatedDescriptors.find(
|
|
5383
|
+
(d) => d.contract?.invariants.some((inv) => matches(inv)) || d.contract?.behavior && matches(d.contract.behavior)
|
|
5384
|
+
);
|
|
5385
|
+
if (guaranteeing) {
|
|
5386
|
+
return {
|
|
5387
|
+
condition,
|
|
5388
|
+
status: "satisfied",
|
|
5389
|
+
explanation: `Rule "${guaranteeing.id}" guarantees: ${condition.description}`,
|
|
5390
|
+
relatedRules: relatedIds
|
|
5391
|
+
};
|
|
5392
|
+
}
|
|
5393
|
+
const exampleMatch = relatedDescriptors.find(
|
|
5394
|
+
(d) => d.contract?.examples.some((ex) => matches(ex.then))
|
|
5395
|
+
);
|
|
5396
|
+
if (exampleMatch) {
|
|
5397
|
+
return {
|
|
5398
|
+
condition,
|
|
5399
|
+
status: "satisfied",
|
|
5400
|
+
explanation: `Rule "${exampleMatch.id}" example demonstrates: ${condition.description}`,
|
|
5401
|
+
relatedRules: relatedIds
|
|
5402
|
+
};
|
|
5403
|
+
}
|
|
5404
|
+
return {
|
|
5405
|
+
condition,
|
|
5406
|
+
status: "unverifiable",
|
|
5407
|
+
explanation: `No rule contract guarantees: ${condition.description}`,
|
|
5408
|
+
relatedRules: relatedIds
|
|
5409
|
+
};
|
|
5410
|
+
}
|
|
5411
|
+
}
|
|
5412
|
+
}
|
|
5413
|
+
function findRelatedDescriptors(expectationName, descriptors) {
|
|
5414
|
+
const nameLower = expectationName.toLowerCase();
|
|
5415
|
+
const nameParts = nameLower.split(/[-_./\s]+/);
|
|
5416
|
+
return descriptors.filter((d) => {
|
|
5417
|
+
const idLower = d.id.toLowerCase();
|
|
5418
|
+
const descLower = d.description.toLowerCase();
|
|
5419
|
+
const behaviorLower = d.contract?.behavior?.toLowerCase() ?? "";
|
|
5420
|
+
if (idLower.includes(nameLower) || nameLower.includes(idLower)) return true;
|
|
5421
|
+
if (descLower.includes(nameLower)) return true;
|
|
5422
|
+
if (behaviorLower.includes(nameLower)) return true;
|
|
5423
|
+
const minParts = Math.min(2, nameParts.length);
|
|
5424
|
+
const matchingParts = nameParts.filter(
|
|
5425
|
+
(part) => part.length > 2 && (idLower.includes(part) || descLower.includes(part) || behaviorLower.includes(part))
|
|
5426
|
+
);
|
|
5427
|
+
if (matchingParts.length >= minParts) return true;
|
|
5428
|
+
if (d.eventTypes) {
|
|
5429
|
+
const eventStr = Array.isArray(d.eventTypes) ? d.eventTypes.join(" ") : d.eventTypes;
|
|
5430
|
+
if (eventStr.toLowerCase().includes(nameLower)) return true;
|
|
5431
|
+
}
|
|
5432
|
+
return false;
|
|
5433
|
+
});
|
|
5434
|
+
}
|
|
5435
|
+
function formatVerificationReport(report) {
|
|
5436
|
+
const lines = [];
|
|
5437
|
+
const icon = report.status === "satisfied" ? "\u2705" : report.status === "partial" ? "\u{1F7E1}" : "\u{1F534}";
|
|
5438
|
+
lines.push(`${icon} Expectations: ${report.setName} \u2014 ${report.status.toUpperCase()}`);
|
|
5439
|
+
lines.push(` ${report.summary.satisfied}/${report.summary.total} satisfied, ${report.summary.violated} violated, ${report.summary.partial} partial`);
|
|
5440
|
+
lines.push("");
|
|
5441
|
+
for (const exp of report.expectations) {
|
|
5442
|
+
const expIcon = exp.status === "satisfied" ? "\u2705" : exp.status === "partial" ? "\u{1F7E1}" : "\u{1F534}";
|
|
5443
|
+
lines.push(`${expIcon} ${exp.name}`);
|
|
5444
|
+
for (const cond of exp.conditions) {
|
|
5445
|
+
const condIcon = cond.status === "satisfied" ? " \u2713" : cond.status === "violated" ? " \u2717" : " ?";
|
|
5446
|
+
lines.push(`${condIcon} [${cond.condition.type}] ${cond.condition.description}`);
|
|
5447
|
+
lines.push(` ${cond.explanation}`);
|
|
5448
|
+
}
|
|
5449
|
+
if (exp.edgeCases.length > 0) {
|
|
5450
|
+
lines.push(" Edge cases:");
|
|
5451
|
+
for (const ec of exp.edgeCases) {
|
|
5452
|
+
lines.push(` \u26A0\uFE0F ${ec}`);
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5455
|
+
lines.push("");
|
|
5456
|
+
}
|
|
5457
|
+
if (report.allMitigations.length > 0) {
|
|
5458
|
+
lines.push("Suggested mitigations:");
|
|
5459
|
+
for (const m of report.allMitigations) {
|
|
5460
|
+
lines.push(` \u{1F4A1} ${m}`);
|
|
5461
|
+
}
|
|
5462
|
+
}
|
|
5463
|
+
return lines.join("\n");
|
|
5464
|
+
}
|
|
5465
|
+
|
|
5466
|
+
// src/factory/factory.ts
|
|
5467
|
+
init_rule_result();
|
|
5468
|
+
var SANITIZE_PATTERNS = {
|
|
5469
|
+
"sql-injection": /('|"|;|--|\/\*|\*\/|xp_|exec\s|union\s+select|drop\s+table|insert\s+into|delete\s+from)/i,
|
|
5470
|
+
"xss": /(<script|javascript:|on\w+\s*=|<iframe|<object|<embed|<img[^>]+onerror)/i,
|
|
5471
|
+
"path-traversal": /(\.\.[/\\]|~\/|\/etc\/|\/proc\/)/i,
|
|
5472
|
+
"command-injection": /([;&|`$]|\$\(|>\s*\/)/i
|
|
5473
|
+
};
|
|
5474
|
+
function inputRules(config = {}) {
|
|
5475
|
+
const {
|
|
5476
|
+
sanitize = [],
|
|
5477
|
+
maxLength = 0,
|
|
5478
|
+
required = false,
|
|
5479
|
+
fieldName = "input"
|
|
5480
|
+
} = config;
|
|
5481
|
+
const rules = [];
|
|
5482
|
+
const constraints = [];
|
|
5483
|
+
if (sanitize.length > 0) {
|
|
5484
|
+
rules.push({
|
|
5485
|
+
id: `factory/input.sanitize-${fieldName}`,
|
|
5486
|
+
description: `Validates ${fieldName} against ${sanitize.join(", ")} patterns`,
|
|
5487
|
+
eventTypes: [`${fieldName}.submit`, `${fieldName}.change`],
|
|
5488
|
+
contract: {
|
|
5489
|
+
ruleId: `factory/input.sanitize-${fieldName}`,
|
|
5490
|
+
behavior: `Checks ${fieldName} for dangerous patterns: ${sanitize.join(", ")}`,
|
|
5491
|
+
examples: [
|
|
5492
|
+
{ given: `${fieldName} contains safe text`, when: "input submitted", then: "input.valid emitted" },
|
|
5493
|
+
{ given: `${fieldName} contains <script> tag`, when: "input submitted", then: "input.violation emitted" }
|
|
5494
|
+
],
|
|
5495
|
+
invariants: [
|
|
5496
|
+
`Dangerous ${fieldName} patterns must never pass validation`,
|
|
5497
|
+
"All violations must include the violation type"
|
|
5498
|
+
]
|
|
5499
|
+
},
|
|
5500
|
+
impl: (state2, events) => {
|
|
5501
|
+
const inputEvent = events.find(
|
|
5502
|
+
(e) => e.tag === `${fieldName}.submit` || e.tag === `${fieldName}.change`
|
|
5503
|
+
);
|
|
5504
|
+
if (!inputEvent) return RuleResult.skip("No input event");
|
|
5505
|
+
const value = inputEvent.payload?.value ?? state2.context.input?.value ?? "";
|
|
5506
|
+
const violations = [];
|
|
5507
|
+
for (const type of sanitize) {
|
|
5508
|
+
const pattern = SANITIZE_PATTERNS[type];
|
|
5509
|
+
if (pattern && pattern.test(value)) {
|
|
5510
|
+
violations.push(type);
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
5513
|
+
if (violations.length > 0) {
|
|
5514
|
+
return RuleResult.emit([
|
|
5515
|
+
fact(`${fieldName}.violation`, {
|
|
5516
|
+
field: fieldName,
|
|
5517
|
+
violations,
|
|
5518
|
+
message: `Input failed sanitization: ${violations.join(", ")}`
|
|
5519
|
+
})
|
|
5520
|
+
]);
|
|
5521
|
+
}
|
|
5522
|
+
return RuleResult.emit([
|
|
5523
|
+
fact(`${fieldName}.valid`, { field: fieldName, sanitized: true })
|
|
5524
|
+
]);
|
|
5525
|
+
}
|
|
5526
|
+
});
|
|
5527
|
+
}
|
|
5528
|
+
if (maxLength > 0) {
|
|
5529
|
+
constraints.push({
|
|
5530
|
+
id: `factory/input.max-length-${fieldName}`,
|
|
5531
|
+
description: `${fieldName} must not exceed ${maxLength} characters`,
|
|
5532
|
+
contract: {
|
|
5533
|
+
ruleId: `factory/input.max-length-${fieldName}`,
|
|
5534
|
+
behavior: `Enforces max length of ${maxLength} for ${fieldName}`,
|
|
5535
|
+
examples: [
|
|
5536
|
+
{ given: `${fieldName} is 10 chars`, when: `maxLength is ${maxLength}`, then: maxLength >= 10 ? "passes" : "violation" }
|
|
5537
|
+
],
|
|
5538
|
+
invariants: [`${fieldName} length must never exceed ${maxLength}`]
|
|
5539
|
+
},
|
|
5540
|
+
impl: (state2) => {
|
|
5541
|
+
const value = state2.context.input?.value ?? "";
|
|
5542
|
+
if (value.length > maxLength) {
|
|
5543
|
+
return `${fieldName} exceeds maximum length of ${maxLength} (got ${value.length})`;
|
|
5544
|
+
}
|
|
5545
|
+
return true;
|
|
5546
|
+
}
|
|
5547
|
+
});
|
|
5548
|
+
}
|
|
5549
|
+
if (required) {
|
|
5550
|
+
constraints.push({
|
|
5551
|
+
id: `factory/input.required-${fieldName}`,
|
|
5552
|
+
description: `${fieldName} is required and must not be empty`,
|
|
5553
|
+
contract: {
|
|
5554
|
+
ruleId: `factory/input.required-${fieldName}`,
|
|
5555
|
+
behavior: `Enforces that ${fieldName} is non-empty`,
|
|
5556
|
+
examples: [
|
|
5557
|
+
{ given: `${fieldName} is "hello"`, when: "checked", then: "passes" },
|
|
5558
|
+
{ given: `${fieldName} is empty`, when: "checked", then: "violation" }
|
|
5559
|
+
],
|
|
5560
|
+
invariants: [`${fieldName} must never be empty when required`]
|
|
5561
|
+
},
|
|
5562
|
+
impl: (state2) => {
|
|
5563
|
+
const value = state2.context.input?.value ?? "";
|
|
5564
|
+
if (value.trim().length === 0) {
|
|
5565
|
+
return `${fieldName} is required but empty`;
|
|
5566
|
+
}
|
|
5567
|
+
return true;
|
|
5568
|
+
}
|
|
5569
|
+
});
|
|
5570
|
+
}
|
|
5571
|
+
return { rules, constraints };
|
|
5572
|
+
}
|
|
5573
|
+
function toastRules(config = {}) {
|
|
5574
|
+
const {
|
|
5575
|
+
requireDiff = false,
|
|
5576
|
+
autoDismissMs = 0,
|
|
5577
|
+
deduplicate = false
|
|
5578
|
+
} = config;
|
|
5579
|
+
const rules = [];
|
|
5580
|
+
const constraints = [];
|
|
5581
|
+
rules.push({
|
|
5582
|
+
id: "factory/toast.show",
|
|
5583
|
+
description: "Emits toast notification with content and config",
|
|
5584
|
+
eventTypes: ["toast.request"],
|
|
5585
|
+
contract: {
|
|
5586
|
+
ruleId: "factory/toast.show",
|
|
5587
|
+
behavior: "Shows toast when requested, respecting diff requirement and auto-dismiss",
|
|
5588
|
+
examples: [
|
|
5589
|
+
{ given: "toast requested with message", when: "toast.request fires", then: "toast.show emitted" },
|
|
5590
|
+
...requireDiff ? [{ given: "no diff present", when: "toast.request fires", then: "toast skipped" }] : []
|
|
5591
|
+
],
|
|
5592
|
+
invariants: [
|
|
5593
|
+
"Toast message must be non-empty",
|
|
5594
|
+
...requireDiff ? ["Toast must not appear when diff is empty"] : []
|
|
5595
|
+
]
|
|
5596
|
+
},
|
|
5597
|
+
impl: (state2, events) => {
|
|
5598
|
+
const toastEvent = events.find((e) => e.tag === "toast.request");
|
|
5599
|
+
if (!toastEvent) return RuleResult.skip("No toast request");
|
|
5600
|
+
const payload = toastEvent.payload;
|
|
5601
|
+
const message = payload.message ?? "";
|
|
5602
|
+
if (!message) return RuleResult.skip("Empty toast message");
|
|
5603
|
+
if (requireDiff) {
|
|
5604
|
+
const diff = state2.context.diff;
|
|
5605
|
+
if (!diff || Object.keys(diff).length === 0) {
|
|
5606
|
+
return RuleResult.skip("No diff \u2014 toast suppressed");
|
|
5607
|
+
}
|
|
5608
|
+
}
|
|
5609
|
+
return RuleResult.emit([
|
|
5610
|
+
fact("toast.show", {
|
|
5611
|
+
message,
|
|
5612
|
+
type: payload.type ?? "info",
|
|
5613
|
+
autoDismissMs: autoDismissMs > 0 ? autoDismissMs : void 0,
|
|
5614
|
+
timestamp: Date.now()
|
|
5615
|
+
})
|
|
5616
|
+
]);
|
|
5617
|
+
}
|
|
5618
|
+
});
|
|
5619
|
+
if (deduplicate) {
|
|
5620
|
+
constraints.push({
|
|
5621
|
+
id: "factory/toast.no-duplicates",
|
|
5622
|
+
description: "Prevents duplicate toast messages",
|
|
5623
|
+
contract: {
|
|
5624
|
+
ruleId: "factory/toast.no-duplicates",
|
|
5625
|
+
behavior: "Rejects toast if identical message is already showing",
|
|
5626
|
+
examples: [
|
|
5627
|
+
{ given: "same toast already visible", when: "duplicate toast requested", then: "violation" }
|
|
5628
|
+
],
|
|
5629
|
+
invariants: ["No two toasts may have the same message simultaneously"]
|
|
5630
|
+
},
|
|
5631
|
+
impl: (state2) => {
|
|
5632
|
+
const toasts = state2.context.toasts ?? [];
|
|
5633
|
+
const messages = toasts.map((t) => t.message);
|
|
5634
|
+
const uniqueMessages = new Set(messages);
|
|
5635
|
+
if (uniqueMessages.size < messages.length) {
|
|
5636
|
+
return "Duplicate toast detected";
|
|
5637
|
+
}
|
|
5638
|
+
return true;
|
|
5639
|
+
}
|
|
5640
|
+
});
|
|
5641
|
+
}
|
|
5642
|
+
return { rules, constraints };
|
|
5643
|
+
}
|
|
5644
|
+
function formRules(config = {}) {
|
|
5645
|
+
const {
|
|
5646
|
+
validateOnBlur = false,
|
|
5647
|
+
submitGate = false,
|
|
5648
|
+
formName = "form"
|
|
5649
|
+
} = config;
|
|
5650
|
+
const rules = [];
|
|
5651
|
+
const constraints = [];
|
|
5652
|
+
if (validateOnBlur) {
|
|
5653
|
+
rules.push({
|
|
5654
|
+
id: `factory/${formName}.validate-on-blur`,
|
|
5655
|
+
description: `Triggers field validation when a ${formName} field loses focus`,
|
|
5656
|
+
eventTypes: [`${formName}.blur`],
|
|
5657
|
+
contract: {
|
|
5658
|
+
ruleId: `factory/${formName}.validate-on-blur`,
|
|
5659
|
+
behavior: `Validates the blurred field and emits validation result`,
|
|
5660
|
+
examples: [
|
|
5661
|
+
{ given: `${formName} field has value`, when: "field loses focus", then: "validation result emitted" }
|
|
5662
|
+
],
|
|
5663
|
+
invariants: ["Validation must run for every blur event on a registered field"]
|
|
5664
|
+
},
|
|
5665
|
+
impl: (_state, events) => {
|
|
5666
|
+
const blurEvent = events.find((e) => e.tag === `${formName}.blur`);
|
|
5667
|
+
if (!blurEvent) return RuleResult.skip("No blur event");
|
|
5668
|
+
const payload = blurEvent.payload;
|
|
5669
|
+
const field = payload.field ?? "unknown";
|
|
5670
|
+
const value = payload.value;
|
|
5671
|
+
const valid = value !== null && value !== void 0 && value !== "";
|
|
5672
|
+
return RuleResult.emit([
|
|
5673
|
+
fact(`${formName}.field-validated`, {
|
|
5674
|
+
field,
|
|
5675
|
+
valid,
|
|
5676
|
+
error: valid ? null : `${field} is required`
|
|
5677
|
+
})
|
|
5678
|
+
]);
|
|
5679
|
+
}
|
|
5680
|
+
});
|
|
5681
|
+
}
|
|
5682
|
+
if (submitGate) {
|
|
5683
|
+
constraints.push({
|
|
5684
|
+
id: `factory/${formName}.submit-gate`,
|
|
5685
|
+
description: `Prevents ${formName} submission when validation has not passed`,
|
|
5686
|
+
contract: {
|
|
5687
|
+
ruleId: `factory/${formName}.submit-gate`,
|
|
5688
|
+
behavior: `Blocks form submission until all fields are valid`,
|
|
5689
|
+
examples: [
|
|
5690
|
+
{ given: `${formName} is invalid`, when: "submit attempted", then: "violation \u2014 submission blocked" },
|
|
5691
|
+
{ given: `${formName} is valid`, when: "submit attempted", then: "passes" }
|
|
5692
|
+
],
|
|
5693
|
+
invariants: ["Form must not submit while any field has errors"]
|
|
5694
|
+
},
|
|
5695
|
+
impl: (state2) => {
|
|
5696
|
+
const form = state2.context.form;
|
|
5697
|
+
if (!form) return true;
|
|
5698
|
+
if (form.submitting && !form.valid) {
|
|
5699
|
+
return `${formName} cannot submit: validation has not passed`;
|
|
5700
|
+
}
|
|
5701
|
+
return true;
|
|
5702
|
+
}
|
|
5703
|
+
});
|
|
5704
|
+
}
|
|
5705
|
+
rules.push({
|
|
5706
|
+
id: `factory/${formName}.dirty-tracking`,
|
|
5707
|
+
description: `Tracks whether ${formName} has unsaved changes`,
|
|
5708
|
+
eventTypes: [`${formName}.change`, `${formName}.reset`],
|
|
5709
|
+
contract: {
|
|
5710
|
+
ruleId: `factory/${formName}.dirty-tracking`,
|
|
5711
|
+
behavior: "Emits dirty state when form fields change, clears on reset",
|
|
5712
|
+
examples: [
|
|
5713
|
+
{ given: "field value changed", when: "form.change fires", then: "form.dirty emitted" },
|
|
5714
|
+
{ given: "form reset", when: "form.reset fires", then: "form.dirty retracted" }
|
|
5715
|
+
],
|
|
5716
|
+
invariants: ["Dirty state must reflect actual field changes"]
|
|
5717
|
+
},
|
|
5718
|
+
impl: (_state, events) => {
|
|
5719
|
+
const resetEvent = events.find((e) => e.tag === `${formName}.reset`);
|
|
5720
|
+
if (resetEvent) {
|
|
5721
|
+
return RuleResult.retract([`${formName}.dirty`], "Form reset");
|
|
5722
|
+
}
|
|
5723
|
+
const changeEvent = events.find((e) => e.tag === `${formName}.change`);
|
|
5724
|
+
if (changeEvent) {
|
|
5725
|
+
return RuleResult.emit([
|
|
5726
|
+
fact(`${formName}.dirty`, { dirty: true })
|
|
5727
|
+
]);
|
|
5728
|
+
}
|
|
5729
|
+
return RuleResult.skip("No form event");
|
|
5730
|
+
}
|
|
5731
|
+
});
|
|
5732
|
+
return { rules, constraints };
|
|
5733
|
+
}
|
|
5734
|
+
function navigationRules(config = {}) {
|
|
5735
|
+
const {
|
|
5736
|
+
dirtyGuard = false,
|
|
5737
|
+
authRequired = false
|
|
5738
|
+
} = config;
|
|
5739
|
+
const rules = [];
|
|
5740
|
+
const constraints = [];
|
|
5741
|
+
rules.push({
|
|
5742
|
+
id: "factory/navigation.handle",
|
|
5743
|
+
description: "Processes navigation requests and emits navigation facts",
|
|
5744
|
+
eventTypes: ["navigation.request"],
|
|
5745
|
+
contract: {
|
|
5746
|
+
ruleId: "factory/navigation.handle",
|
|
5747
|
+
behavior: "Emits navigation.allowed or navigation.blocked based on guards",
|
|
5748
|
+
examples: [
|
|
5749
|
+
{ given: "no guards active", when: "navigation requested", then: "navigation.allowed emitted" },
|
|
5750
|
+
...dirtyGuard ? [{ given: "form is dirty", when: "navigation requested", then: "navigation.blocked emitted" }] : [],
|
|
5751
|
+
...authRequired ? [{ given: "user not authenticated", when: "navigation requested", then: "navigation.blocked emitted" }] : []
|
|
5752
|
+
],
|
|
5753
|
+
invariants: [
|
|
5754
|
+
"Every navigation request must result in either allowed or blocked",
|
|
5755
|
+
...dirtyGuard ? ["Navigation must be blocked when dirty data exists"] : [],
|
|
5756
|
+
...authRequired ? ["Navigation must be blocked when not authenticated"] : []
|
|
5757
|
+
]
|
|
5758
|
+
},
|
|
5759
|
+
impl: (state2, events) => {
|
|
5760
|
+
const navEvent = events.find((e) => e.tag === "navigation.request");
|
|
5761
|
+
if (!navEvent) return RuleResult.skip("No navigation request");
|
|
5762
|
+
const target = navEvent.payload?.target ?? "/";
|
|
5763
|
+
const reasons = [];
|
|
5764
|
+
if (dirtyGuard && state2.context.dirty) {
|
|
5765
|
+
reasons.push("Unsaved changes will be lost");
|
|
5766
|
+
}
|
|
5767
|
+
if (authRequired && !state2.context.authenticated) {
|
|
5768
|
+
reasons.push("Authentication required");
|
|
5769
|
+
}
|
|
5770
|
+
if (reasons.length > 0) {
|
|
5771
|
+
return RuleResult.emit([
|
|
5772
|
+
fact("navigation.blocked", { target, reasons })
|
|
5773
|
+
]);
|
|
5774
|
+
}
|
|
5775
|
+
return RuleResult.emit([
|
|
5776
|
+
fact("navigation.allowed", { target })
|
|
5777
|
+
]);
|
|
5778
|
+
}
|
|
5779
|
+
});
|
|
5780
|
+
if (dirtyGuard) {
|
|
5781
|
+
constraints.push({
|
|
5782
|
+
id: "factory/navigation.dirty-guard",
|
|
5783
|
+
description: "Prevents silent navigation when unsaved changes exist",
|
|
5784
|
+
contract: {
|
|
5785
|
+
ruleId: "factory/navigation.dirty-guard",
|
|
5786
|
+
behavior: "Blocks navigation when dirty state is true",
|
|
5787
|
+
examples: [
|
|
5788
|
+
{ given: "dirty is true", when: "navigation attempted", then: "violation" },
|
|
5789
|
+
{ given: "dirty is false", when: "navigation attempted", then: "passes" }
|
|
5790
|
+
],
|
|
5791
|
+
invariants: ["Must never silently lose unsaved changes"]
|
|
5792
|
+
},
|
|
5793
|
+
impl: (state2) => {
|
|
5794
|
+
if (state2.context.dirty && state2.facts.some((f) => f.tag === "navigation.allowed")) {
|
|
5795
|
+
return "Navigation allowed while dirty \u2014 unsaved changes may be lost";
|
|
5796
|
+
}
|
|
5797
|
+
return true;
|
|
5798
|
+
}
|
|
5799
|
+
});
|
|
5800
|
+
}
|
|
5801
|
+
return { rules, constraints };
|
|
5802
|
+
}
|
|
5803
|
+
function dataRules(config = {}) {
|
|
5804
|
+
const {
|
|
5805
|
+
optimisticUpdate = false,
|
|
5806
|
+
rollbackOnError = false,
|
|
5807
|
+
cacheInvalidation = false,
|
|
5808
|
+
entityName = "data"
|
|
5809
|
+
} = config;
|
|
5810
|
+
const rules = [];
|
|
5811
|
+
const constraints = [];
|
|
5812
|
+
if (optimisticUpdate) {
|
|
5813
|
+
rules.push({
|
|
5814
|
+
id: `factory/${entityName}.optimistic-update`,
|
|
5815
|
+
description: `Applies optimistic update for ${entityName} while request is pending`,
|
|
5816
|
+
eventTypes: [`${entityName}.mutate`],
|
|
5817
|
+
contract: {
|
|
5818
|
+
ruleId: `factory/${entityName}.optimistic-update`,
|
|
5819
|
+
behavior: `Immediately emits updated ${entityName} state before server confirmation`,
|
|
5820
|
+
examples: [
|
|
5821
|
+
{ given: `${entityName} mutation requested`, when: "mutate event fires", then: "optimistic state emitted" }
|
|
5822
|
+
],
|
|
5823
|
+
invariants: [
|
|
5824
|
+
"Optimistic state must store original for rollback",
|
|
5825
|
+
"Optimistic update must be distinguishable from confirmed state"
|
|
5826
|
+
]
|
|
5827
|
+
},
|
|
5828
|
+
impl: (_state, events) => {
|
|
5829
|
+
const mutateEvent = events.find((e) => e.tag === `${entityName}.mutate`);
|
|
5830
|
+
if (!mutateEvent) return RuleResult.skip("No mutation event");
|
|
5831
|
+
const payload = mutateEvent.payload;
|
|
5832
|
+
return RuleResult.emit([
|
|
5833
|
+
fact(`${entityName}.optimistic`, {
|
|
5834
|
+
id: payload.id,
|
|
5835
|
+
data: payload.data,
|
|
5836
|
+
pending: true,
|
|
5837
|
+
timestamp: Date.now()
|
|
5838
|
+
})
|
|
5839
|
+
]);
|
|
5840
|
+
}
|
|
5841
|
+
});
|
|
5842
|
+
}
|
|
5843
|
+
if (rollbackOnError) {
|
|
5844
|
+
rules.push({
|
|
5845
|
+
id: `factory/${entityName}.rollback`,
|
|
5846
|
+
description: `Rolls back optimistic ${entityName} update on error`,
|
|
5847
|
+
eventTypes: [`${entityName}.error`],
|
|
5848
|
+
contract: {
|
|
5849
|
+
ruleId: `factory/${entityName}.rollback`,
|
|
5850
|
+
behavior: `Reverts to original ${entityName} state when mutation fails`,
|
|
5851
|
+
examples: [
|
|
5852
|
+
{ given: "optimistic update was applied", when: "server returns error", then: "rollback emitted, optimistic retracted" }
|
|
5853
|
+
],
|
|
5854
|
+
invariants: [
|
|
5855
|
+
"Rollback must restore original state exactly",
|
|
5856
|
+
"Optimistic facts must be retracted on rollback"
|
|
5857
|
+
]
|
|
5858
|
+
},
|
|
5859
|
+
impl: (_state, events) => {
|
|
5860
|
+
const errorEvent = events.find((e) => e.tag === `${entityName}.error`);
|
|
5861
|
+
if (!errorEvent) return RuleResult.skip("No error event");
|
|
5862
|
+
const payload = errorEvent.payload;
|
|
5863
|
+
const result = RuleResult.emit([
|
|
5864
|
+
fact(`${entityName}.rollback`, {
|
|
5865
|
+
id: payload.id,
|
|
5866
|
+
error: payload.error,
|
|
5867
|
+
timestamp: Date.now()
|
|
5868
|
+
})
|
|
5869
|
+
]);
|
|
5870
|
+
return result;
|
|
5871
|
+
}
|
|
5872
|
+
});
|
|
5873
|
+
}
|
|
5874
|
+
if (cacheInvalidation) {
|
|
5875
|
+
rules.push({
|
|
5876
|
+
id: `factory/${entityName}.cache-invalidate`,
|
|
5877
|
+
description: `Invalidates ${entityName} cache when data changes are confirmed`,
|
|
5878
|
+
eventTypes: [`${entityName}.confirmed`, `${entityName}.deleted`],
|
|
5879
|
+
contract: {
|
|
5880
|
+
ruleId: `factory/${entityName}.cache-invalidate`,
|
|
5881
|
+
behavior: `Emits cache invalidation signal when ${entityName} is confirmed or deleted`,
|
|
5882
|
+
examples: [
|
|
5883
|
+
{ given: `${entityName} mutation confirmed`, when: "confirmed event fires", then: "cache.invalidate emitted" }
|
|
5884
|
+
],
|
|
5885
|
+
invariants: ["Stale cache entries must be invalidated after confirmed mutations"]
|
|
5886
|
+
},
|
|
5887
|
+
impl: (_state, events) => {
|
|
5888
|
+
const confirmEvent = events.find(
|
|
5889
|
+
(e) => e.tag === `${entityName}.confirmed` || e.tag === `${entityName}.deleted`
|
|
5890
|
+
);
|
|
5891
|
+
if (!confirmEvent) return RuleResult.skip("No confirmation event");
|
|
5892
|
+
const payload = confirmEvent.payload;
|
|
5893
|
+
return RuleResult.emit([
|
|
5894
|
+
fact(`${entityName}.cache-invalidate`, {
|
|
5895
|
+
id: payload.id,
|
|
5896
|
+
timestamp: Date.now()
|
|
5897
|
+
})
|
|
5898
|
+
]);
|
|
5899
|
+
}
|
|
5900
|
+
});
|
|
5901
|
+
}
|
|
5902
|
+
constraints.push({
|
|
5903
|
+
id: `factory/${entityName}.integrity`,
|
|
5904
|
+
description: `Ensures ${entityName} state integrity \u2014 no orphaned optimistic updates`,
|
|
5905
|
+
contract: {
|
|
5906
|
+
ruleId: `factory/${entityName}.integrity`,
|
|
5907
|
+
behavior: "Detects orphaned optimistic updates without pending confirmation",
|
|
5908
|
+
examples: [
|
|
5909
|
+
{ given: "optimistic update exists without pending request", when: "checked", then: "violation" }
|
|
5910
|
+
],
|
|
5911
|
+
invariants: [`Every optimistic ${entityName} update must have a corresponding pending request`]
|
|
5912
|
+
},
|
|
5913
|
+
impl: (state2) => {
|
|
5914
|
+
const pending = state2.context.pending ?? {};
|
|
5915
|
+
const optimisticFacts = state2.facts.filter((f) => f.tag === `${entityName}.optimistic`);
|
|
5916
|
+
for (const optFact of optimisticFacts) {
|
|
5917
|
+
const id = optFact.payload?.id;
|
|
5918
|
+
if (id && !pending[id]) {
|
|
5919
|
+
return `Orphaned optimistic update for ${entityName} id=${id} \u2014 no pending request`;
|
|
5920
|
+
}
|
|
5921
|
+
}
|
|
5922
|
+
return true;
|
|
5923
|
+
}
|
|
5924
|
+
});
|
|
5925
|
+
return { rules, constraints };
|
|
5926
|
+
}
|
|
5927
|
+
|
|
5928
|
+
// src/project/project.ts
|
|
5929
|
+
init_rule_result();
|
|
5930
|
+
function defineGate(name, config) {
|
|
5931
|
+
const { expects, onSatisfied, onViolation } = config;
|
|
5932
|
+
const rule = {
|
|
5933
|
+
id: `gate/${name}`,
|
|
5934
|
+
description: `Feature gate: ${name} \u2014 requires: ${expects.join(", ")}`,
|
|
5935
|
+
eventTypes: ["gate.check", `gate.${name}.check`],
|
|
5936
|
+
contract: {
|
|
5937
|
+
ruleId: `gate/${name}`,
|
|
5938
|
+
behavior: `Opens gate "${name}" when all expectations are met: ${expects.join(", ")}`,
|
|
5939
|
+
examples: [
|
|
5940
|
+
{
|
|
5941
|
+
given: `all expectations satisfied: ${expects.join(", ")}`,
|
|
5942
|
+
when: "gate checked",
|
|
5943
|
+
then: `gate.${name}.open emitted${onSatisfied ? ` \u2192 ${onSatisfied}` : ""}`
|
|
5944
|
+
},
|
|
5945
|
+
{
|
|
5946
|
+
given: "one or more expectations unsatisfied",
|
|
5947
|
+
when: "gate checked",
|
|
5948
|
+
then: `gate.${name}.blocked emitted${onViolation ? ` \u2192 ${onViolation}` : ""}`
|
|
5949
|
+
}
|
|
5950
|
+
],
|
|
5951
|
+
invariants: [
|
|
5952
|
+
`Gate "${name}" must never open with unsatisfied expectations`,
|
|
5953
|
+
"Gate status must reflect current expectation state exactly"
|
|
5954
|
+
]
|
|
5955
|
+
},
|
|
5956
|
+
impl: (state2, events) => {
|
|
5957
|
+
const gateEvent = events.find(
|
|
5958
|
+
(e) => e.tag === "gate.check" || e.tag === `gate.${name}.check`
|
|
5959
|
+
);
|
|
5960
|
+
if (!gateEvent) return RuleResult.skip("No gate check event");
|
|
5961
|
+
const expectationState = state2.context.expectations ?? {};
|
|
5962
|
+
const satisfied = [];
|
|
5963
|
+
const unsatisfied = [];
|
|
5964
|
+
for (const exp of expects) {
|
|
5965
|
+
if (expectationState[exp]) {
|
|
5966
|
+
satisfied.push(exp);
|
|
5967
|
+
} else {
|
|
5968
|
+
unsatisfied.push(exp);
|
|
5969
|
+
}
|
|
5970
|
+
}
|
|
5971
|
+
const status = unsatisfied.length === 0 ? "open" : "blocked";
|
|
5972
|
+
const gateState = {
|
|
5973
|
+
name,
|
|
5974
|
+
status,
|
|
5975
|
+
satisfied,
|
|
5976
|
+
unsatisfied,
|
|
5977
|
+
lastChanged: Date.now()
|
|
5978
|
+
};
|
|
5979
|
+
const facts = [fact(`gate.${name}.status`, gateState)];
|
|
5980
|
+
if (status === "open" && onSatisfied) {
|
|
5981
|
+
facts.push(fact(`gate.${name}.action`, { action: onSatisfied }));
|
|
5982
|
+
} else if (status === "blocked" && onViolation) {
|
|
5983
|
+
facts.push(fact(`gate.${name}.action`, { action: onViolation }));
|
|
5984
|
+
}
|
|
5985
|
+
return RuleResult.emit(facts);
|
|
5986
|
+
}
|
|
5987
|
+
};
|
|
5988
|
+
const constraint = {
|
|
5989
|
+
id: `gate/${name}/integrity`,
|
|
5990
|
+
description: `Ensures gate "${name}" status matches expectation reality`,
|
|
5991
|
+
contract: {
|
|
5992
|
+
ruleId: `gate/${name}/integrity`,
|
|
5993
|
+
behavior: `Validates that gate "${name}" is not open when expectations are unmet`,
|
|
5994
|
+
examples: [
|
|
5995
|
+
{
|
|
5996
|
+
given: `gate ${name} is open but expectations unmet`,
|
|
5997
|
+
when: "constraint checked",
|
|
5998
|
+
then: "violation"
|
|
5999
|
+
}
|
|
6000
|
+
],
|
|
6001
|
+
invariants: [`Gate "${name}" must never report open when expectations are unsatisfied`]
|
|
6002
|
+
},
|
|
6003
|
+
impl: (state2) => {
|
|
6004
|
+
const gateState = state2.context.gates?.[name];
|
|
6005
|
+
if (!gateState) return true;
|
|
6006
|
+
if (gateState.status === "open" && gateState.unsatisfied.length > 0) {
|
|
6007
|
+
return `Gate "${name}" is open but has unsatisfied expectations: ${gateState.unsatisfied.join(", ")}`;
|
|
6008
|
+
}
|
|
6009
|
+
return true;
|
|
6010
|
+
}
|
|
6011
|
+
};
|
|
6012
|
+
return { rules: [rule], constraints: [constraint] };
|
|
6013
|
+
}
|
|
6014
|
+
function semverContract(config) {
|
|
6015
|
+
const { sources, invariants } = config;
|
|
6016
|
+
const rule = {
|
|
6017
|
+
id: "project/semver-check",
|
|
6018
|
+
description: `Checks version consistency across: ${sources.join(", ")}`,
|
|
6019
|
+
eventTypes: ["project.version-check"],
|
|
6020
|
+
contract: {
|
|
6021
|
+
ruleId: "project/semver-check",
|
|
6022
|
+
behavior: `Verifies version consistency across ${sources.length} sources`,
|
|
6023
|
+
examples: [
|
|
6024
|
+
{
|
|
6025
|
+
given: "all sources have version 1.2.3",
|
|
6026
|
+
when: "version check runs",
|
|
6027
|
+
then: "semver.consistent emitted"
|
|
6028
|
+
},
|
|
6029
|
+
{
|
|
6030
|
+
given: "package.json has 1.2.3 but README has 1.2.2",
|
|
6031
|
+
when: "version check runs",
|
|
6032
|
+
then: "semver.inconsistent emitted with diff"
|
|
6033
|
+
}
|
|
6034
|
+
],
|
|
6035
|
+
invariants: invariants.length > 0 ? invariants : ["All version sources must report the same semver string"]
|
|
6036
|
+
},
|
|
6037
|
+
impl: (_state, events) => {
|
|
6038
|
+
const checkEvent = events.find((e) => e.tag === "project.version-check");
|
|
6039
|
+
if (!checkEvent) return RuleResult.skip("No version check event");
|
|
6040
|
+
const versions = checkEvent.payload?.versions ?? {};
|
|
6041
|
+
const versionValues = Object.values(versions);
|
|
6042
|
+
const unique = new Set(versionValues);
|
|
6043
|
+
if (unique.size <= 1) {
|
|
6044
|
+
return RuleResult.emit([
|
|
6045
|
+
fact("semver.consistent", {
|
|
6046
|
+
version: versionValues[0] ?? "unknown",
|
|
6047
|
+
sources: Object.keys(versions)
|
|
6048
|
+
})
|
|
6049
|
+
]);
|
|
6050
|
+
}
|
|
6051
|
+
const report = {
|
|
6052
|
+
consistent: false,
|
|
6053
|
+
versions,
|
|
6054
|
+
violations: [`Version mismatch: ${JSON.stringify(versions)}`]
|
|
6055
|
+
};
|
|
6056
|
+
return RuleResult.emit([fact("semver.inconsistent", report)]);
|
|
6057
|
+
}
|
|
6058
|
+
};
|
|
6059
|
+
return { rules: [rule], constraints: [] };
|
|
6060
|
+
}
|
|
6061
|
+
function commitFromState(diff) {
|
|
6062
|
+
const parts = [];
|
|
6063
|
+
const bodyParts = [];
|
|
6064
|
+
const totalAdded = diff.rulesAdded.length + diff.contractsAdded.length + diff.expectationsAdded.length;
|
|
6065
|
+
const totalRemoved = diff.rulesRemoved.length + diff.contractsRemoved.length + diff.expectationsRemoved.length;
|
|
6066
|
+
const totalModified = diff.rulesModified.length;
|
|
6067
|
+
const hasGateChanges = diff.gateChanges.length > 0;
|
|
6068
|
+
if (totalAdded > 0 && totalRemoved === 0 && totalModified === 0) {
|
|
6069
|
+
if (diff.rulesAdded.length > 0) {
|
|
6070
|
+
const scope = inferScope(diff.rulesAdded);
|
|
6071
|
+
parts.push(`feat(${scope}): add ${formatIds(diff.rulesAdded)}`);
|
|
6072
|
+
} else if (diff.contractsAdded.length > 0) {
|
|
6073
|
+
parts.push(`feat(contracts): add contracts for ${formatIds(diff.contractsAdded)}`);
|
|
6074
|
+
} else {
|
|
6075
|
+
parts.push(`feat(expectations): add ${formatIds(diff.expectationsAdded)}`);
|
|
6076
|
+
}
|
|
6077
|
+
} else if (totalRemoved > 0 && totalAdded === 0) {
|
|
6078
|
+
if (diff.rulesRemoved.length > 0) {
|
|
6079
|
+
const scope = inferScope(diff.rulesRemoved);
|
|
6080
|
+
parts.push(`refactor(${scope}): remove ${formatIds(diff.rulesRemoved)}`);
|
|
6081
|
+
} else {
|
|
6082
|
+
parts.push(`refactor: remove ${totalRemoved} item(s)`);
|
|
6083
|
+
}
|
|
6084
|
+
} else if (totalModified > 0) {
|
|
6085
|
+
const scope = inferScope(diff.rulesModified);
|
|
6086
|
+
parts.push(`refactor(${scope}): update ${formatIds(diff.rulesModified)}`);
|
|
6087
|
+
} else if (hasGateChanges) {
|
|
6088
|
+
const gateNames = diff.gateChanges.map((g) => g.gate);
|
|
6089
|
+
parts.push(`chore(gates): ${formatIds(gateNames)} state changed`);
|
|
6090
|
+
} else {
|
|
6091
|
+
parts.push("chore: behavioral state update");
|
|
6092
|
+
}
|
|
6093
|
+
if (diff.rulesAdded.length > 0) bodyParts.push(`Rules added: ${diff.rulesAdded.join(", ")}`);
|
|
6094
|
+
if (diff.rulesRemoved.length > 0) bodyParts.push(`Rules removed: ${diff.rulesRemoved.join(", ")}`);
|
|
6095
|
+
if (diff.rulesModified.length > 0) bodyParts.push(`Rules modified: ${diff.rulesModified.join(", ")}`);
|
|
6096
|
+
if (diff.contractsAdded.length > 0) bodyParts.push(`Contracts added: ${diff.contractsAdded.join(", ")}`);
|
|
6097
|
+
if (diff.contractsRemoved.length > 0) bodyParts.push(`Contracts removed: ${diff.contractsRemoved.join(", ")}`);
|
|
6098
|
+
if (diff.expectationsAdded.length > 0) bodyParts.push(`Expectations added: ${diff.expectationsAdded.join(", ")}`);
|
|
6099
|
+
if (diff.expectationsRemoved.length > 0) bodyParts.push(`Expectations removed: ${diff.expectationsRemoved.join(", ")}`);
|
|
6100
|
+
for (const gc of diff.gateChanges) {
|
|
6101
|
+
bodyParts.push(`Gate "${gc.gate}": ${gc.from} \u2192 ${gc.to}`);
|
|
6102
|
+
}
|
|
6103
|
+
const subject = parts[0] || "chore: update";
|
|
6104
|
+
return bodyParts.length > 0 ? `${subject}
|
|
6105
|
+
|
|
6106
|
+
${bodyParts.join("\n")}` : subject;
|
|
6107
|
+
}
|
|
6108
|
+
function inferScope(ids) {
|
|
6109
|
+
if (ids.length === 0) return "rules";
|
|
6110
|
+
const prefixes = ids.map((id) => {
|
|
6111
|
+
const slash = id.indexOf("/");
|
|
6112
|
+
return slash > 0 ? id.slice(0, slash) : id;
|
|
6113
|
+
});
|
|
6114
|
+
const unique = new Set(prefixes);
|
|
6115
|
+
return unique.size === 1 ? prefixes[0] : "rules";
|
|
6116
|
+
}
|
|
6117
|
+
function formatIds(ids) {
|
|
6118
|
+
if (ids.length <= 3) return ids.join(", ");
|
|
6119
|
+
return `${ids.slice(0, 2).join(", ")} (+${ids.length - 2} more)`;
|
|
6120
|
+
}
|
|
6121
|
+
function branchRules(config) {
|
|
6122
|
+
const { naming, mergeConditions } = config;
|
|
6123
|
+
const namePattern = naming.replace("{name}", "(.+)").replace("{issue}", "(\\d+)");
|
|
6124
|
+
const nameRegex = new RegExp(`^${namePattern}$`);
|
|
6125
|
+
const rule = {
|
|
6126
|
+
id: "project/branch-check",
|
|
6127
|
+
description: `Validates branch naming (${naming}) and merge conditions`,
|
|
6128
|
+
eventTypes: ["project.branch-check"],
|
|
6129
|
+
contract: {
|
|
6130
|
+
ruleId: "project/branch-check",
|
|
6131
|
+
behavior: `Ensures branch follows "${naming}" pattern and merge conditions are met`,
|
|
6132
|
+
examples: [
|
|
6133
|
+
{
|
|
6134
|
+
given: `branch named "${naming.replace("{name}", "my-feature")}"`,
|
|
6135
|
+
when: "branch checked",
|
|
6136
|
+
then: "branch.valid emitted"
|
|
6137
|
+
},
|
|
6138
|
+
{
|
|
6139
|
+
given: 'branch named "random-name"',
|
|
6140
|
+
when: "branch checked",
|
|
6141
|
+
then: "branch.invalid emitted"
|
|
6142
|
+
}
|
|
6143
|
+
],
|
|
6144
|
+
invariants: [
|
|
6145
|
+
`Branch names must follow pattern: ${naming}`,
|
|
6146
|
+
`Merge requires: ${mergeConditions.join(", ")}`
|
|
6147
|
+
]
|
|
6148
|
+
},
|
|
6149
|
+
impl: (_state, events) => {
|
|
6150
|
+
const checkEvent = events.find((e) => e.tag === "project.branch-check");
|
|
6151
|
+
if (!checkEvent) return RuleResult.skip("No branch check event");
|
|
6152
|
+
const payload = checkEvent.payload;
|
|
6153
|
+
const branch = payload.branch ?? "";
|
|
6154
|
+
const conditions = payload.conditions ?? {};
|
|
6155
|
+
const validName = nameRegex.test(branch);
|
|
6156
|
+
const unmetConditions = mergeConditions.filter((c) => !conditions[c]);
|
|
6157
|
+
if (validName && unmetConditions.length === 0) {
|
|
6158
|
+
return RuleResult.emit([
|
|
6159
|
+
fact("branch.valid", { branch, mergeReady: true })
|
|
6160
|
+
]);
|
|
6161
|
+
}
|
|
6162
|
+
const reasons = [];
|
|
6163
|
+
if (!validName) reasons.push(`Branch name "${branch}" doesn't match pattern "${naming}"`);
|
|
6164
|
+
if (unmetConditions.length > 0) reasons.push(`Unmet merge conditions: ${unmetConditions.join(", ")}`);
|
|
6165
|
+
return RuleResult.emit([
|
|
6166
|
+
fact("branch.invalid", { branch, reasons, mergeReady: false })
|
|
6167
|
+
]);
|
|
6168
|
+
}
|
|
6169
|
+
};
|
|
6170
|
+
return { rules: [rule], constraints: [] };
|
|
6171
|
+
}
|
|
6172
|
+
function lintGate(config = {}) {
|
|
6173
|
+
const expects = ["lint-passes", ...config.additionalExpects ?? []];
|
|
6174
|
+
return defineGate("lint", {
|
|
6175
|
+
expects,
|
|
6176
|
+
onSatisfied: "lint-passed",
|
|
6177
|
+
onViolation: "lint-failed"
|
|
6178
|
+
});
|
|
6179
|
+
}
|
|
6180
|
+
function formatGate(config = {}) {
|
|
6181
|
+
const expects = ["format-passes", ...config.additionalExpects ?? []];
|
|
6182
|
+
return defineGate("format", {
|
|
6183
|
+
expects,
|
|
6184
|
+
onSatisfied: "format-passed",
|
|
6185
|
+
onViolation: "format-failed"
|
|
6186
|
+
});
|
|
6187
|
+
}
|
|
6188
|
+
function expectationGate(config = {}) {
|
|
6189
|
+
const expects = ["expectations-verified", ...config.additionalExpects ?? []];
|
|
6190
|
+
return defineGate("expectations", {
|
|
6191
|
+
expects,
|
|
6192
|
+
onSatisfied: "expectations-passed",
|
|
6193
|
+
onViolation: "expectations-failed"
|
|
6194
|
+
});
|
|
6195
|
+
}
|
|
4656
6196
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4657
6197
|
0 && (module.exports = {
|
|
4658
6198
|
AcknowledgeContractGap,
|
|
@@ -4665,6 +6205,8 @@ async function attachAllIntegrations(engine, registry, options = {}) {
|
|
|
4665
6205
|
ContractMissing,
|
|
4666
6206
|
ContractUpdated,
|
|
4667
6207
|
ContractValidated,
|
|
6208
|
+
Expectation,
|
|
6209
|
+
ExpectationSet,
|
|
4668
6210
|
FrameworkAgnosticReactiveEngine,
|
|
4669
6211
|
InMemoryPraxisDB,
|
|
4670
6212
|
LogicEngine,
|
|
@@ -4678,6 +6220,7 @@ async function attachAllIntegrations(engine, registry, options = {}) {
|
|
|
4678
6220
|
PraxisSchemaRegistry,
|
|
4679
6221
|
ReactiveLogicEngine,
|
|
4680
6222
|
RegistryIntrospector,
|
|
6223
|
+
RuleResult,
|
|
4681
6224
|
StateDocsGenerator,
|
|
4682
6225
|
TerminalAdapter,
|
|
4683
6226
|
ValidateContracts,
|
|
@@ -4685,9 +6228,12 @@ async function attachAllIntegrations(engine, registry, options = {}) {
|
|
|
4685
6228
|
attachTauriToEngine,
|
|
4686
6229
|
attachToEngine,
|
|
4687
6230
|
attachUnumToEngine,
|
|
6231
|
+
auditCompleteness,
|
|
6232
|
+
branchRules,
|
|
4688
6233
|
canvasToMermaid,
|
|
4689
6234
|
canvasToSchema,
|
|
4690
6235
|
canvasToYaml,
|
|
6236
|
+
commitFromState,
|
|
4691
6237
|
createBehaviorLedger,
|
|
4692
6238
|
createCanvasEditor,
|
|
4693
6239
|
createChronicle,
|
|
@@ -4710,21 +6256,33 @@ async function attachAllIntegrations(engine, registry, options = {}) {
|
|
|
4710
6256
|
createTauriPraxisAdapter,
|
|
4711
6257
|
createTerminalAdapter,
|
|
4712
6258
|
createTimerActor,
|
|
6259
|
+
createUIModule,
|
|
4713
6260
|
createUnifiedApp,
|
|
4714
6261
|
createUnumAdapter,
|
|
6262
|
+
dataRules,
|
|
4715
6263
|
defineConstraint,
|
|
4716
6264
|
defineContract,
|
|
4717
6265
|
defineEvent,
|
|
4718
6266
|
defineFact,
|
|
6267
|
+
defineGate,
|
|
4719
6268
|
defineModule,
|
|
4720
6269
|
defineRule,
|
|
6270
|
+
dirtyGuardRule,
|
|
6271
|
+
errorDisplayRule,
|
|
6272
|
+
expectBehavior,
|
|
6273
|
+
expectationGate,
|
|
6274
|
+
fact,
|
|
4721
6275
|
filterEvents,
|
|
4722
6276
|
filterFacts,
|
|
4723
6277
|
findEvent,
|
|
4724
6278
|
findFact,
|
|
6279
|
+
formRules,
|
|
6280
|
+
formatGate,
|
|
6281
|
+
formatReport,
|
|
4725
6282
|
formatValidationReport,
|
|
4726
6283
|
formatValidationReportJSON,
|
|
4727
6284
|
formatValidationReportSARIF,
|
|
6285
|
+
formatVerificationReport,
|
|
4728
6286
|
generateDocs,
|
|
4729
6287
|
generateId,
|
|
4730
6288
|
generateTauriConfig,
|
|
@@ -4732,16 +6290,32 @@ async function attachAllIntegrations(engine, registry, options = {}) {
|
|
|
4732
6290
|
getEventPath,
|
|
4733
6291
|
getFactPath,
|
|
4734
6292
|
getSchemaPath,
|
|
6293
|
+
initGateRule,
|
|
6294
|
+
inputRules,
|
|
4735
6295
|
isContract,
|
|
6296
|
+
lintGate,
|
|
4736
6297
|
loadSchema,
|
|
4737
6298
|
loadSchemaFromFile,
|
|
4738
6299
|
loadSchemaFromJson,
|
|
4739
6300
|
loadSchemaFromYaml,
|
|
6301
|
+
loadingGateRule,
|
|
6302
|
+
mustBeInitializedConstraint,
|
|
6303
|
+
navigationRequest,
|
|
6304
|
+
navigationRules,
|
|
6305
|
+
noInteractionWhileLoadingConstraint,
|
|
6306
|
+
offlineIndicatorRule,
|
|
4740
6307
|
registerSchema,
|
|
6308
|
+
resizeEvent,
|
|
4741
6309
|
runTerminalCommand,
|
|
4742
6310
|
schemaToCanvas,
|
|
6311
|
+
semverContract,
|
|
6312
|
+
toastRules,
|
|
6313
|
+
uiModule,
|
|
6314
|
+
uiStateChanged,
|
|
4743
6315
|
validateContracts,
|
|
4744
6316
|
validateForGeneration,
|
|
4745
6317
|
validateSchema,
|
|
4746
|
-
validateWithGuardian
|
|
6318
|
+
validateWithGuardian,
|
|
6319
|
+
verify,
|
|
6320
|
+
viewportRule
|
|
4747
6321
|
});
|