@plures/praxis 1.4.0 → 2.0.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-N63K4KWS.js → chunk-4IRUGWR3.js} +1 -1
- package/dist/browser/chunk-6MVRT7CK.js +363 -0
- package/dist/browser/chunk-6SJ44Q64.js +473 -0
- package/dist/browser/chunk-BQOYZBWA.js +282 -0
- package/dist/browser/chunk-IG5BJ2MT.js +91 -0
- package/dist/browser/{chunk-MJK3IYTJ.js → chunk-JZDJU2DO.js} +4 -84
- package/dist/browser/chunk-ZEW4LJAJ.js +353 -0
- package/dist/browser/{engine-YIEGSX7U.js → engine-3B5WJPGT.js} +2 -1
- package/dist/browser/expectations/index.d.ts +180 -0
- package/dist/browser/expectations/index.js +14 -0
- package/dist/browser/factory/index.d.ts +150 -0
- package/dist/browser/factory/index.js +15 -0
- package/dist/browser/index.d.ts +277 -3
- package/dist/browser/index.js +425 -60
- package/dist/browser/integrations/svelte.d.ts +4 -2
- package/dist/browser/integrations/svelte.js +3 -2
- package/dist/browser/project/index.d.ts +177 -0
- package/dist/browser/project/index.js +19 -0
- package/dist/browser/reactive-engine.svelte-BwWadvAW.d.ts +224 -0
- package/dist/browser/rule-result-DcXWe9tn.d.ts +206 -0
- package/dist/browser/rules-BaWMqxuG.d.ts +277 -0
- package/dist/browser/unified/index.d.ts +239 -0
- package/dist/browser/unified/index.js +20 -0
- package/dist/node/chunk-6MVRT7CK.js +363 -0
- package/dist/node/chunk-AZLNISFI.js +1690 -0
- package/dist/node/chunk-IG5BJ2MT.js +91 -0
- package/dist/node/{chunk-KMJWAFZV.js → chunk-JZDJU2DO.js} +4 -89
- package/dist/node/{chunk-7M3HV4XR.js → chunk-WFRHXZBP.js} +3 -3
- package/dist/node/cli/index.cjs +48 -0
- package/dist/node/cli/index.js +2 -2
- package/dist/node/{engine-FEN5IYZ5.js → engine-VFHCIEM4.js} +2 -1
- package/dist/node/index.cjs +2114 -0
- package/dist/node/index.d.cts +964 -280
- package/dist/node/index.d.ts +964 -280
- package/dist/node/index.js +575 -10
- package/dist/node/integrations/svelte.d.cts +3 -2
- package/dist/node/integrations/svelte.d.ts +3 -2
- package/dist/node/integrations/svelte.js +3 -2
- package/dist/node/{reactive-engine.svelte-DekxqFu0.d.ts → reactive-engine.svelte-BBZLMzus.d.ts} +3 -79
- package/dist/node/{reactive-engine.svelte-Cg0Yc2Hs.d.cts → reactive-engine.svelte-Cbq_V20o.d.cts} +3 -79
- package/dist/node/rule-result-B9GMivAn.d.cts +80 -0
- package/dist/node/rule-result-Bo3sFMmN.d.ts +80 -0
- package/dist/node/{server-SYZPDULV.js → server-FKLVY57V.js} +4 -2
- package/dist/node/unified/index.cjs +484 -0
- package/dist/node/unified/index.d.cts +240 -0
- package/dist/node/unified/index.d.ts +240 -0
- package/dist/node/unified/index.js +21 -0
- package/dist/node/{validate-TQGVIG7G.js → validate-BY7JNY7H.js} +2 -1
- package/package.json +38 -11
- package/src/__tests__/chronos-project.test.ts +799 -0
- package/src/__tests__/decision-ledger.test.ts +857 -402
- package/src/chronos/diff.ts +336 -0
- package/src/chronos/hooks.ts +227 -0
- package/src/chronos/index.ts +83 -0
- package/src/chronos/project-chronicle.ts +198 -0
- package/src/chronos/timeline.ts +152 -0
- package/src/decision-ledger/analyzer-types.ts +280 -0
- package/src/decision-ledger/analyzer.ts +518 -0
- package/src/decision-ledger/contract-verification.ts +456 -0
- package/src/decision-ledger/derivation.ts +158 -0
- package/src/decision-ledger/index.ts +59 -0
- package/src/decision-ledger/report.ts +378 -0
- package/src/decision-ledger/suggestions.ts +287 -0
- package/src/index.browser.ts +103 -0
- package/src/index.ts +98 -0
- package/src/unified/__tests__/unified.test.ts +396 -0
- package/src/unified/core.ts +517 -0
- package/src/unified/index.ts +32 -0
- package/src/unified/rules.ts +66 -0
- package/src/unified/types.ts +148 -0
- package/dist/browser/reactive-engine.svelte-DjynI82A.d.ts +0 -688
- package/dist/node/chunk-FWOXU4MM.js +0 -487
package/dist/browser/index.js
CHANGED
|
@@ -2,12 +2,46 @@ import {
|
|
|
2
2
|
PraxisRegistry,
|
|
3
3
|
ReactiveLogicEngine,
|
|
4
4
|
createReactiveEngine
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-4IRUGWR3.js";
|
|
6
|
+
import {
|
|
7
|
+
Expectation,
|
|
8
|
+
ExpectationSet,
|
|
9
|
+
expectBehavior,
|
|
10
|
+
formatVerificationReport,
|
|
11
|
+
verify
|
|
12
|
+
} from "./chunk-ZEW4LJAJ.js";
|
|
13
|
+
import {
|
|
14
|
+
dataRules,
|
|
15
|
+
formRules,
|
|
16
|
+
inputRules,
|
|
17
|
+
navigationRules,
|
|
18
|
+
toastRules
|
|
19
|
+
} from "./chunk-6SJ44Q64.js";
|
|
20
|
+
import {
|
|
21
|
+
branchRules,
|
|
22
|
+
commitFromState,
|
|
23
|
+
defineGate,
|
|
24
|
+
expectationGate,
|
|
25
|
+
formatGate,
|
|
26
|
+
lintGate,
|
|
27
|
+
semverContract
|
|
28
|
+
} from "./chunk-BQOYZBWA.js";
|
|
29
|
+
import {
|
|
30
|
+
createApp,
|
|
31
|
+
defineConstraint,
|
|
32
|
+
defineModule,
|
|
33
|
+
definePath,
|
|
34
|
+
defineRule
|
|
35
|
+
} from "./chunk-6MVRT7CK.js";
|
|
6
36
|
import {
|
|
7
37
|
LogicEngine,
|
|
8
38
|
PRAXIS_PROTOCOL_VERSION,
|
|
9
39
|
createPraxisEngine
|
|
10
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-JZDJU2DO.js";
|
|
41
|
+
import {
|
|
42
|
+
RuleResult,
|
|
43
|
+
fact
|
|
44
|
+
} from "./chunk-IG5BJ2MT.js";
|
|
11
45
|
import {
|
|
12
46
|
InMemoryPraxisDB,
|
|
13
47
|
PluresDBPraxisAdapter,
|
|
@@ -563,8 +597,8 @@ function defineFact(tag) {
|
|
|
563
597
|
create(payload) {
|
|
564
598
|
return { tag, payload };
|
|
565
599
|
},
|
|
566
|
-
is(
|
|
567
|
-
return
|
|
600
|
+
is(fact2) {
|
|
601
|
+
return fact2.tag === tag;
|
|
568
602
|
}
|
|
569
603
|
};
|
|
570
604
|
}
|
|
@@ -579,7 +613,7 @@ function defineEvent(tag) {
|
|
|
579
613
|
}
|
|
580
614
|
};
|
|
581
615
|
}
|
|
582
|
-
function
|
|
616
|
+
function defineRule2(options) {
|
|
583
617
|
const contract = options.contract ?? options.meta?.contract;
|
|
584
618
|
const meta = contract ? { ...options.meta ?? {}, contract } : options.meta;
|
|
585
619
|
return {
|
|
@@ -591,7 +625,7 @@ function defineRule(options) {
|
|
|
591
625
|
meta
|
|
592
626
|
};
|
|
593
627
|
}
|
|
594
|
-
function
|
|
628
|
+
function defineConstraint2(options) {
|
|
595
629
|
const contract = options.contract ?? options.meta?.contract;
|
|
596
630
|
const meta = contract ? { ...options.meta ?? {}, contract } : options.meta;
|
|
597
631
|
return {
|
|
@@ -602,7 +636,7 @@ function defineConstraint(options) {
|
|
|
602
636
|
meta
|
|
603
637
|
};
|
|
604
638
|
}
|
|
605
|
-
function
|
|
639
|
+
function defineModule2(options) {
|
|
606
640
|
return {
|
|
607
641
|
rules: options.rules ?? [],
|
|
608
642
|
constraints: options.constraints ?? [],
|
|
@@ -666,16 +700,16 @@ function validateSchema(schema) {
|
|
|
666
700
|
if (schema.logic) {
|
|
667
701
|
schema.logic.forEach((logic, logicIndex) => {
|
|
668
702
|
if (logic.facts) {
|
|
669
|
-
logic.facts.forEach((
|
|
670
|
-
if (!
|
|
703
|
+
logic.facts.forEach((fact2, factIndex) => {
|
|
704
|
+
if (!fact2.tag) {
|
|
671
705
|
errors.push({
|
|
672
706
|
path: `logic[${logicIndex}].facts[${factIndex}].tag`,
|
|
673
707
|
message: "Fact tag is required"
|
|
674
708
|
});
|
|
675
|
-
} else if (!isValidIdentifier(
|
|
709
|
+
} else if (!isValidIdentifier(fact2.tag)) {
|
|
676
710
|
errors.push({
|
|
677
711
|
path: `logic[${logicIndex}].facts[${factIndex}].tag`,
|
|
678
|
-
message: `Fact tag "${
|
|
712
|
+
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.`
|
|
679
713
|
});
|
|
680
714
|
}
|
|
681
715
|
});
|
|
@@ -1002,37 +1036,37 @@ var PraxisDBStore = class {
|
|
|
1002
1036
|
* @param fact The fact to store
|
|
1003
1037
|
* @returns Promise that resolves when the fact is stored
|
|
1004
1038
|
*/
|
|
1005
|
-
async storeFact(
|
|
1006
|
-
const constraintResult = await this.checkConstraints([
|
|
1039
|
+
async storeFact(fact2) {
|
|
1040
|
+
const constraintResult = await this.checkConstraints([fact2]);
|
|
1007
1041
|
if (!constraintResult.valid) {
|
|
1008
1042
|
throw new Error(`Constraint violation: ${constraintResult.errors.join(", ")}`);
|
|
1009
1043
|
}
|
|
1010
1044
|
let before;
|
|
1011
1045
|
if (this.chronicle) {
|
|
1012
|
-
const payload =
|
|
1046
|
+
const payload = fact2.payload;
|
|
1013
1047
|
const id = payload?.id;
|
|
1014
1048
|
if (id) {
|
|
1015
|
-
before = await this.getFact(
|
|
1049
|
+
before = await this.getFact(fact2.tag, id);
|
|
1016
1050
|
}
|
|
1017
1051
|
}
|
|
1018
|
-
await this.persistFact(
|
|
1052
|
+
await this.persistFact(fact2);
|
|
1019
1053
|
if (this.chronicle) {
|
|
1020
|
-
const payload =
|
|
1054
|
+
const payload = fact2.payload;
|
|
1021
1055
|
const id = payload?.id ?? "";
|
|
1022
1056
|
const span = ChronicleContext.current;
|
|
1023
1057
|
try {
|
|
1024
1058
|
await this.chronicle.record({
|
|
1025
|
-
path: getFactPath(
|
|
1059
|
+
path: getFactPath(fact2.tag, id),
|
|
1026
1060
|
before,
|
|
1027
|
-
after:
|
|
1061
|
+
after: fact2,
|
|
1028
1062
|
cause: span?.spanId,
|
|
1029
1063
|
context: span?.contextId,
|
|
1030
|
-
metadata: { factTag:
|
|
1064
|
+
metadata: { factTag: fact2.tag, operation: "storeFact" }
|
|
1031
1065
|
});
|
|
1032
1066
|
} catch {
|
|
1033
1067
|
}
|
|
1034
1068
|
}
|
|
1035
|
-
await this.triggerRules([
|
|
1069
|
+
await this.triggerRules([fact2]);
|
|
1036
1070
|
}
|
|
1037
1071
|
/**
|
|
1038
1072
|
* Store multiple facts in PluresDB
|
|
@@ -1044,28 +1078,28 @@ var PraxisDBStore = class {
|
|
|
1044
1078
|
if (!constraintResult.valid) {
|
|
1045
1079
|
throw new Error(`Constraint violation: ${constraintResult.errors.join(", ")}`);
|
|
1046
1080
|
}
|
|
1047
|
-
for (const
|
|
1081
|
+
for (const fact2 of facts) {
|
|
1048
1082
|
let before;
|
|
1049
1083
|
if (this.chronicle) {
|
|
1050
|
-
const payload =
|
|
1084
|
+
const payload = fact2.payload;
|
|
1051
1085
|
const id = payload?.id;
|
|
1052
1086
|
if (id) {
|
|
1053
|
-
before = await this.getFact(
|
|
1087
|
+
before = await this.getFact(fact2.tag, id);
|
|
1054
1088
|
}
|
|
1055
1089
|
}
|
|
1056
|
-
await this.persistFact(
|
|
1090
|
+
await this.persistFact(fact2);
|
|
1057
1091
|
if (this.chronicle) {
|
|
1058
|
-
const payload =
|
|
1092
|
+
const payload = fact2.payload;
|
|
1059
1093
|
const id = payload?.id ?? "";
|
|
1060
1094
|
const span = ChronicleContext.current;
|
|
1061
1095
|
try {
|
|
1062
1096
|
await this.chronicle.record({
|
|
1063
|
-
path: getFactPath(
|
|
1097
|
+
path: getFactPath(fact2.tag, id),
|
|
1064
1098
|
before,
|
|
1065
|
-
after:
|
|
1099
|
+
after: fact2,
|
|
1066
1100
|
cause: span?.spanId,
|
|
1067
1101
|
context: span?.contextId,
|
|
1068
|
-
metadata: { factTag:
|
|
1102
|
+
metadata: { factTag: fact2.tag, operation: "storeFacts" }
|
|
1069
1103
|
});
|
|
1070
1104
|
} catch {
|
|
1071
1105
|
}
|
|
@@ -1077,11 +1111,11 @@ var PraxisDBStore = class {
|
|
|
1077
1111
|
* Internal method to persist a fact without constraint checking
|
|
1078
1112
|
* Used by both storeFact and derived fact storage
|
|
1079
1113
|
*/
|
|
1080
|
-
async persistFact(
|
|
1081
|
-
const payload =
|
|
1114
|
+
async persistFact(fact2) {
|
|
1115
|
+
const payload = fact2.payload;
|
|
1082
1116
|
const id = payload?.id ?? generateId();
|
|
1083
|
-
const path = getFactPath(
|
|
1084
|
-
await this.db.set(path,
|
|
1117
|
+
const path = getFactPath(fact2.tag, id);
|
|
1118
|
+
await this.db.set(path, fact2);
|
|
1085
1119
|
}
|
|
1086
1120
|
/**
|
|
1087
1121
|
* Get a fact by tag and id
|
|
@@ -1218,8 +1252,8 @@ var PraxisDBStore = class {
|
|
|
1218
1252
|
if (watchers) {
|
|
1219
1253
|
watchers.add(callback);
|
|
1220
1254
|
}
|
|
1221
|
-
const unsubscribe = this.db.watch(path, (
|
|
1222
|
-
callback([
|
|
1255
|
+
const unsubscribe = this.db.watch(path, (fact2) => {
|
|
1256
|
+
callback([fact2]);
|
|
1223
1257
|
});
|
|
1224
1258
|
this.subscriptions.push(unsubscribe);
|
|
1225
1259
|
return () => {
|
|
@@ -1296,19 +1330,19 @@ var PraxisDBStore = class {
|
|
|
1296
1330
|
if (derivedFacts.length > 0) {
|
|
1297
1331
|
const constraintResult = await this.checkConstraints(derivedFacts);
|
|
1298
1332
|
if (constraintResult.valid) {
|
|
1299
|
-
for (const
|
|
1300
|
-
await this.persistFact(
|
|
1333
|
+
for (const fact2 of derivedFacts) {
|
|
1334
|
+
await this.persistFact(fact2);
|
|
1301
1335
|
if (this.chronicle) {
|
|
1302
|
-
const payload =
|
|
1336
|
+
const payload = fact2.payload;
|
|
1303
1337
|
const id = payload?.id ?? "";
|
|
1304
1338
|
const span = ChronicleContext.current;
|
|
1305
1339
|
try {
|
|
1306
1340
|
await this.chronicle.record({
|
|
1307
|
-
path: getFactPath(
|
|
1308
|
-
after:
|
|
1341
|
+
path: getFactPath(fact2.tag, id),
|
|
1342
|
+
after: fact2,
|
|
1309
1343
|
cause: span?.spanId,
|
|
1310
1344
|
context: span?.contextId,
|
|
1311
|
-
metadata: { factTag:
|
|
1345
|
+
metadata: { factTag: fact2.tag, operation: "derivedFact" }
|
|
1312
1346
|
});
|
|
1313
1347
|
} catch {
|
|
1314
1348
|
}
|
|
@@ -1768,7 +1802,7 @@ async function createUnumAdapter(config) {
|
|
|
1768
1802
|
type: "event"
|
|
1769
1803
|
});
|
|
1770
1804
|
}
|
|
1771
|
-
async function broadcastFact(channelId,
|
|
1805
|
+
async function broadcastFact(channelId, fact2) {
|
|
1772
1806
|
const channel = channels.get(channelId);
|
|
1773
1807
|
if (!channel) {
|
|
1774
1808
|
throw new Error(`Not joined to channel ${channelId}`);
|
|
@@ -1776,7 +1810,7 @@ async function createUnumAdapter(config) {
|
|
|
1776
1810
|
await channel.publish({
|
|
1777
1811
|
id: generateId2(),
|
|
1778
1812
|
sender: currentIdentity || { id: "anonymous", createdAt: Date.now() },
|
|
1779
|
-
content:
|
|
1813
|
+
content: fact2,
|
|
1780
1814
|
type: "fact"
|
|
1781
1815
|
});
|
|
1782
1816
|
}
|
|
@@ -1919,17 +1953,17 @@ function schemaToCanvas(schema, _options = {}) {
|
|
|
1919
1953
|
yOffset += schema.events.length * ySpacing + 30;
|
|
1920
1954
|
}
|
|
1921
1955
|
if (schema.facts) {
|
|
1922
|
-
schema.facts.forEach((
|
|
1923
|
-
const pos =
|
|
1956
|
+
schema.facts.forEach((fact2, index) => {
|
|
1957
|
+
const pos = fact2.position && (fact2.position.x !== 0 || fact2.position.y !== 0) ? fact2.position : { x: 50 + xSpacing * 3, y: yOffset + index * ySpacing };
|
|
1924
1958
|
const node = {
|
|
1925
|
-
id:
|
|
1959
|
+
id: fact2.id || `fact-${nodeId++}`,
|
|
1926
1960
|
type: "fact",
|
|
1927
|
-
label:
|
|
1961
|
+
label: fact2.tag,
|
|
1928
1962
|
x: pos.x,
|
|
1929
1963
|
y: pos.y,
|
|
1930
1964
|
width: 150,
|
|
1931
1965
|
height: 50,
|
|
1932
|
-
data:
|
|
1966
|
+
data: fact2,
|
|
1933
1967
|
style: {
|
|
1934
1968
|
backgroundColor: "#fce4ec",
|
|
1935
1969
|
borderColor: "#c2185b"
|
|
@@ -2384,8 +2418,8 @@ var StateDocsGenerator = class {
|
|
|
2384
2418
|
if (logic.facts && logic.facts.length > 0) {
|
|
2385
2419
|
lines.push("**Facts:**");
|
|
2386
2420
|
lines.push("");
|
|
2387
|
-
for (const
|
|
2388
|
-
lines.push(`- \`${
|
|
2421
|
+
for (const fact2 of logic.facts) {
|
|
2422
|
+
lines.push(`- \`${fact2.tag}\`: ${fact2.description || ""}`);
|
|
2389
2423
|
}
|
|
2390
2424
|
lines.push("");
|
|
2391
2425
|
}
|
|
@@ -2517,9 +2551,9 @@ var StateDocsGenerator = class {
|
|
|
2517
2551
|
lines.push("");
|
|
2518
2552
|
lines.push("| Fact | Description | Payload |");
|
|
2519
2553
|
lines.push("|------|-------------|---------|");
|
|
2520
|
-
for (const
|
|
2521
|
-
const payload =
|
|
2522
|
-
lines.push(`| \`${
|
|
2554
|
+
for (const fact2 of logic.facts) {
|
|
2555
|
+
const payload = fact2.payload ? Object.entries(fact2.payload).map(([k, v]) => `${k}: ${v}`).join(", ") : "-";
|
|
2556
|
+
lines.push(`| \`${fact2.tag}\` | ${fact2.description || "-"} | ${payload} |`);
|
|
2523
2557
|
}
|
|
2524
2558
|
lines.push("");
|
|
2525
2559
|
}
|
|
@@ -2571,8 +2605,8 @@ var StateDocsGenerator = class {
|
|
|
2571
2605
|
for (const event of logic.events) {
|
|
2572
2606
|
lines.push(` Processing --> ${event.tag.replace(/[^a-zA-Z0-9]/g, "")}: ${event.tag}`);
|
|
2573
2607
|
}
|
|
2574
|
-
for (const
|
|
2575
|
-
lines.push(` ${
|
|
2608
|
+
for (const fact2 of logic.facts) {
|
|
2609
|
+
lines.push(` ${fact2.tag.replace(/[^a-zA-Z0-9]/g, "")} --> [*]`);
|
|
2576
2610
|
}
|
|
2577
2611
|
}
|
|
2578
2612
|
return {
|
|
@@ -2894,7 +2928,7 @@ function generateTauriConfig(config) {
|
|
|
2894
2928
|
|
|
2895
2929
|
// src/integrations/unified.ts
|
|
2896
2930
|
async function createUnifiedApp(config) {
|
|
2897
|
-
const { createPraxisEngine: createPraxisEngine2 } = await import("./engine-
|
|
2931
|
+
const { createPraxisEngine: createPraxisEngine2 } = await import("./engine-3B5WJPGT.js");
|
|
2898
2932
|
const { createInMemoryDB: createInMemoryDB2 } = await import("./adapter-CIMBGDC7.js");
|
|
2899
2933
|
const db = config.db || createInMemoryDB2();
|
|
2900
2934
|
const pluresdb = createPluresDBAdapter({
|
|
@@ -3023,8 +3057,302 @@ async function attachAllIntegrations(engine, registry, options = {}) {
|
|
|
3023
3057
|
}
|
|
3024
3058
|
};
|
|
3025
3059
|
}
|
|
3060
|
+
|
|
3061
|
+
// src/core/ui-rules.ts
|
|
3062
|
+
var loadingGateRule = {
|
|
3063
|
+
id: "ui/loading-gate",
|
|
3064
|
+
description: "Signals when the app is in a loading state",
|
|
3065
|
+
eventTypes: ["ui.state-change", "app.init"],
|
|
3066
|
+
impl: (state) => {
|
|
3067
|
+
const ctx = state.context;
|
|
3068
|
+
if (ctx.loading) {
|
|
3069
|
+
return RuleResult.emit([fact("ui.loading-gate", { active: true })]);
|
|
3070
|
+
}
|
|
3071
|
+
return RuleResult.retract(["ui.loading-gate"], "Not loading");
|
|
3072
|
+
},
|
|
3073
|
+
contract: {
|
|
3074
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
3075
|
+
behavior: "Emits ui.loading-gate when context.loading is true, retracts when false",
|
|
3076
|
+
examples: [
|
|
3077
|
+
{ given: "loading is true", when: "ui state changes", then: "ui.loading-gate emitted" },
|
|
3078
|
+
{ given: "loading is false", when: "ui state changes", then: "ui.loading-gate retracted" }
|
|
3079
|
+
],
|
|
3080
|
+
invariants: ["Loading gate must reflect context.loading exactly"]
|
|
3081
|
+
}
|
|
3082
|
+
};
|
|
3083
|
+
var errorDisplayRule = {
|
|
3084
|
+
id: "ui/error-display",
|
|
3085
|
+
description: "Signals when an error should be displayed to the user",
|
|
3086
|
+
eventTypes: ["ui.state-change", "app.error"],
|
|
3087
|
+
impl: (state) => {
|
|
3088
|
+
const ctx = state.context;
|
|
3089
|
+
if (ctx.error) {
|
|
3090
|
+
return RuleResult.emit([fact("ui.error-display", { message: ctx.error, severity: "error" })]);
|
|
3091
|
+
}
|
|
3092
|
+
return RuleResult.retract(["ui.error-display"], "Error cleared");
|
|
3093
|
+
},
|
|
3094
|
+
contract: {
|
|
3095
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
3096
|
+
behavior: "Emits ui.error-display when context.error is non-null, retracts when cleared",
|
|
3097
|
+
examples: [
|
|
3098
|
+
{ given: "error is set", when: "ui state changes", then: "ui.error-display emitted with message" },
|
|
3099
|
+
{ given: "error is null", when: "ui state changes", then: "ui.error-display retracted" }
|
|
3100
|
+
],
|
|
3101
|
+
invariants: ["Error display must clear when error is null"]
|
|
3102
|
+
}
|
|
3103
|
+
};
|
|
3104
|
+
var offlineIndicatorRule = {
|
|
3105
|
+
id: "ui/offline-indicator",
|
|
3106
|
+
description: "Signals when the app is offline",
|
|
3107
|
+
eventTypes: ["ui.state-change", "network.change"],
|
|
3108
|
+
impl: (state) => {
|
|
3109
|
+
if (state.context.offline) {
|
|
3110
|
+
return RuleResult.emit([fact("ui.offline", { message: "You are offline. Changes will sync when reconnected." })]);
|
|
3111
|
+
}
|
|
3112
|
+
return RuleResult.retract(["ui.offline"], "Back online");
|
|
3113
|
+
},
|
|
3114
|
+
contract: {
|
|
3115
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
3116
|
+
behavior: "Emits ui.offline when context.offline is true, retracts when back online",
|
|
3117
|
+
examples: [
|
|
3118
|
+
{ given: "offline is true", when: "network changes", then: "ui.offline emitted" },
|
|
3119
|
+
{ given: "offline is false", when: "network changes", then: "ui.offline retracted" }
|
|
3120
|
+
],
|
|
3121
|
+
invariants: ["Offline indicator must match actual connectivity"]
|
|
3122
|
+
}
|
|
3123
|
+
};
|
|
3124
|
+
var dirtyGuardRule = {
|
|
3125
|
+
id: "ui/dirty-guard",
|
|
3126
|
+
description: "Warns when there are unsaved changes",
|
|
3127
|
+
eventTypes: ["ui.state-change", "navigation.request"],
|
|
3128
|
+
impl: (state) => {
|
|
3129
|
+
if (state.context.dirty) {
|
|
3130
|
+
return RuleResult.emit([fact("ui.unsaved-warning", {
|
|
3131
|
+
message: "You have unsaved changes",
|
|
3132
|
+
blocking: true
|
|
3133
|
+
})]);
|
|
3134
|
+
}
|
|
3135
|
+
return RuleResult.retract(["ui.unsaved-warning"], "No unsaved changes");
|
|
3136
|
+
},
|
|
3137
|
+
contract: {
|
|
3138
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
3139
|
+
behavior: "Emits ui.unsaved-warning when context.dirty is true, retracts when saved",
|
|
3140
|
+
examples: [
|
|
3141
|
+
{ given: "dirty is true", when: "ui state changes", then: "ui.unsaved-warning emitted with blocking=true" },
|
|
3142
|
+
{ given: "dirty is false", when: "ui state changes", then: "ui.unsaved-warning retracted" }
|
|
3143
|
+
],
|
|
3144
|
+
invariants: ["Dirty guard must clear after save"]
|
|
3145
|
+
}
|
|
3146
|
+
};
|
|
3147
|
+
var initGateRule = {
|
|
3148
|
+
id: "ui/init-gate",
|
|
3149
|
+
description: "Signals whether the app has completed initialization",
|
|
3150
|
+
eventTypes: ["ui.state-change", "app.init"],
|
|
3151
|
+
impl: (state) => {
|
|
3152
|
+
if (!state.context.initialized) {
|
|
3153
|
+
return RuleResult.emit([fact("ui.init-pending", {
|
|
3154
|
+
message: "App is initializing..."
|
|
3155
|
+
})]);
|
|
3156
|
+
}
|
|
3157
|
+
return RuleResult.retract(["ui.init-pending"], "App initialized");
|
|
3158
|
+
},
|
|
3159
|
+
contract: {
|
|
3160
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
3161
|
+
behavior: "Emits ui.init-pending until context.initialized is true",
|
|
3162
|
+
examples: [
|
|
3163
|
+
{ given: "initialized is false", when: "app starts", then: "ui.init-pending emitted" },
|
|
3164
|
+
{ given: "initialized is true", when: "init completes", then: "ui.init-pending retracted" }
|
|
3165
|
+
],
|
|
3166
|
+
invariants: ["Init gate must clear exactly once, when initialization completes"]
|
|
3167
|
+
}
|
|
3168
|
+
};
|
|
3169
|
+
var viewportRule = {
|
|
3170
|
+
id: "ui/viewport-class",
|
|
3171
|
+
description: "Classifies viewport size for responsive layout decisions",
|
|
3172
|
+
eventTypes: ["ui.state-change", "ui.resize"],
|
|
3173
|
+
impl: (state) => {
|
|
3174
|
+
const vp = state.context.viewport;
|
|
3175
|
+
if (!vp) return RuleResult.skip("No viewport data");
|
|
3176
|
+
return RuleResult.emit([fact("ui.viewport-class", {
|
|
3177
|
+
viewport: vp,
|
|
3178
|
+
compact: vp === "mobile",
|
|
3179
|
+
showSidebar: vp !== "mobile"
|
|
3180
|
+
})]);
|
|
3181
|
+
},
|
|
3182
|
+
contract: {
|
|
3183
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
3184
|
+
behavior: "Classifies viewport into responsive layout hints",
|
|
3185
|
+
examples: [
|
|
3186
|
+
{ given: "viewport is mobile", when: "resize event", then: "compact=true, showSidebar=false" },
|
|
3187
|
+
{ given: "viewport is desktop", when: "resize event", then: "compact=false, showSidebar=true" }
|
|
3188
|
+
],
|
|
3189
|
+
invariants: ["Viewport class must update on every resize event"]
|
|
3190
|
+
}
|
|
3191
|
+
};
|
|
3192
|
+
var noInteractionWhileLoadingConstraint = {
|
|
3193
|
+
id: "ui/no-interaction-while-loading",
|
|
3194
|
+
description: "Prevents data mutations while a load operation is in progress",
|
|
3195
|
+
impl: (state) => {
|
|
3196
|
+
if (state.context.loading) {
|
|
3197
|
+
return "Cannot perform action while data is loading";
|
|
3198
|
+
}
|
|
3199
|
+
return true;
|
|
3200
|
+
},
|
|
3201
|
+
contract: {
|
|
3202
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
3203
|
+
behavior: "Fails when context.loading is true",
|
|
3204
|
+
examples: [
|
|
3205
|
+
{ given: "loading is true", when: "action attempted", then: "violation" },
|
|
3206
|
+
{ given: "loading is false", when: "action attempted", then: "pass" }
|
|
3207
|
+
],
|
|
3208
|
+
invariants: ["Must always fail during loading"]
|
|
3209
|
+
}
|
|
3210
|
+
};
|
|
3211
|
+
var mustBeInitializedConstraint = {
|
|
3212
|
+
id: "ui/must-be-initialized",
|
|
3213
|
+
description: "Requires app initialization before user interactions",
|
|
3214
|
+
impl: (state) => {
|
|
3215
|
+
if (!state.context.initialized) {
|
|
3216
|
+
return "App must be initialized before performing this action";
|
|
3217
|
+
}
|
|
3218
|
+
return true;
|
|
3219
|
+
},
|
|
3220
|
+
contract: {
|
|
3221
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
3222
|
+
behavior: "Fails when context.initialized is false",
|
|
3223
|
+
examples: [
|
|
3224
|
+
{ given: "initialized is false", when: "action attempted", then: "violation" },
|
|
3225
|
+
{ given: "initialized is true", when: "action attempted", then: "pass" }
|
|
3226
|
+
],
|
|
3227
|
+
invariants: ["Must always fail before init completes"]
|
|
3228
|
+
}
|
|
3229
|
+
};
|
|
3230
|
+
var uiModule = {
|
|
3231
|
+
rules: [
|
|
3232
|
+
loadingGateRule,
|
|
3233
|
+
errorDisplayRule,
|
|
3234
|
+
offlineIndicatorRule,
|
|
3235
|
+
dirtyGuardRule,
|
|
3236
|
+
initGateRule,
|
|
3237
|
+
viewportRule
|
|
3238
|
+
],
|
|
3239
|
+
constraints: [
|
|
3240
|
+
noInteractionWhileLoadingConstraint,
|
|
3241
|
+
mustBeInitializedConstraint
|
|
3242
|
+
],
|
|
3243
|
+
meta: {
|
|
3244
|
+
name: "praxis-ui",
|
|
3245
|
+
version: "1.0.0",
|
|
3246
|
+
description: "Predefined UI rules and constraints \u2014 separate from business logic"
|
|
3247
|
+
}
|
|
3248
|
+
};
|
|
3249
|
+
function createUIModule(options) {
|
|
3250
|
+
const allRules = uiModule.rules;
|
|
3251
|
+
const allConstraints = uiModule.constraints;
|
|
3252
|
+
const selectedRules = options.rules ? allRules.filter((r) => options.rules.includes(r.id)) : allRules;
|
|
3253
|
+
const selectedConstraints = options.constraints ? allConstraints.filter((c) => options.constraints.includes(c.id)) : allConstraints;
|
|
3254
|
+
return {
|
|
3255
|
+
rules: [...selectedRules, ...options.extraRules ?? []],
|
|
3256
|
+
constraints: [...selectedConstraints, ...options.extraConstraints ?? []],
|
|
3257
|
+
meta: { ...uiModule.meta, customized: true }
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3260
|
+
function uiStateChanged(changes) {
|
|
3261
|
+
return { tag: "ui.state-change", payload: changes ?? {} };
|
|
3262
|
+
}
|
|
3263
|
+
function navigationRequest(to) {
|
|
3264
|
+
return { tag: "navigation.request", payload: { to } };
|
|
3265
|
+
}
|
|
3266
|
+
function resizeEvent(width, height) {
|
|
3267
|
+
return { tag: "ui.resize", payload: { width, height } };
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
// src/core/completeness.ts
|
|
3271
|
+
function auditCompleteness(manifest, registryRuleIds, registryConstraintIds, rulesWithContracts, config) {
|
|
3272
|
+
const threshold = config?.threshold ?? 90;
|
|
3273
|
+
const domainBranches = manifest.branches.filter((b) => b.kind === "domain");
|
|
3274
|
+
const coveredDomain = domainBranches.filter((b) => b.coveredBy && registryRuleIds.includes(b.coveredBy));
|
|
3275
|
+
const uncoveredDomain = domainBranches.filter((b) => !b.coveredBy || !registryRuleIds.includes(b.coveredBy));
|
|
3276
|
+
const invariantBranches = manifest.branches.filter((b) => b.kind === "invariant");
|
|
3277
|
+
const coveredInvariants = invariantBranches.filter((b) => b.coveredBy && registryConstraintIds.includes(b.coveredBy));
|
|
3278
|
+
const uncoveredInvariants = invariantBranches.filter((b) => !b.coveredBy || !registryConstraintIds.includes(b.coveredBy));
|
|
3279
|
+
const needContracts = manifest.rulesNeedingContracts;
|
|
3280
|
+
const haveContracts = needContracts.filter((id) => rulesWithContracts.includes(id));
|
|
3281
|
+
const missingContracts = needContracts.filter((id) => !rulesWithContracts.includes(id));
|
|
3282
|
+
const neededFields = manifest.stateFields.filter((f) => f.usedByRule);
|
|
3283
|
+
const coveredFields = neededFields.filter((f) => f.inContext);
|
|
3284
|
+
const missingFields = neededFields.filter((f) => !f.inContext);
|
|
3285
|
+
const coveredTransitions = manifest.transitions.filter((t) => t.eventTag);
|
|
3286
|
+
const missingTransitions = manifest.transitions.filter((t) => !t.eventTag);
|
|
3287
|
+
const ruleScore = domainBranches.length > 0 ? coveredDomain.length / domainBranches.length * 40 : 40;
|
|
3288
|
+
const constraintScore = invariantBranches.length > 0 ? coveredInvariants.length / invariantBranches.length * 20 : 20;
|
|
3289
|
+
const contractScore = needContracts.length > 0 ? haveContracts.length / needContracts.length * 15 : 15;
|
|
3290
|
+
const contextScore = neededFields.length > 0 ? coveredFields.length / neededFields.length * 15 : 15;
|
|
3291
|
+
const eventScore = manifest.transitions.length > 0 ? coveredTransitions.length / manifest.transitions.length * 10 : 10;
|
|
3292
|
+
const score = Math.round(ruleScore + constraintScore + contractScore + contextScore + eventScore);
|
|
3293
|
+
const rating = score >= 90 ? "complete" : score >= 70 ? "good" : score >= 50 ? "partial" : "incomplete";
|
|
3294
|
+
const report = {
|
|
3295
|
+
score,
|
|
3296
|
+
rating,
|
|
3297
|
+
rules: { total: domainBranches.length, covered: coveredDomain.length, uncovered: uncoveredDomain },
|
|
3298
|
+
constraints: { total: invariantBranches.length, covered: coveredInvariants.length, uncovered: uncoveredInvariants },
|
|
3299
|
+
contracts: { total: needContracts.length, withContracts: haveContracts.length, missing: missingContracts },
|
|
3300
|
+
context: { total: neededFields.length, covered: coveredFields.length, missing: missingFields },
|
|
3301
|
+
events: { total: manifest.transitions.length, covered: coveredTransitions.length, missing: missingTransitions }
|
|
3302
|
+
};
|
|
3303
|
+
if (config?.strict && score < threshold) {
|
|
3304
|
+
throw new Error(`Praxis completeness ${score}/100 (${rating}) \u2014 below threshold ${threshold}. ${uncoveredDomain.length} uncovered rules, ${uncoveredInvariants.length} uncovered invariants, ${missingContracts.length} missing contracts.`);
|
|
3305
|
+
}
|
|
3306
|
+
return report;
|
|
3307
|
+
}
|
|
3308
|
+
function formatReport(report) {
|
|
3309
|
+
const lines = [];
|
|
3310
|
+
const icon = report.rating === "complete" ? "\u2705" : report.rating === "good" ? "\u{1F7E2}" : report.rating === "partial" ? "\u{1F7E1}" : "\u{1F534}";
|
|
3311
|
+
lines.push(`${icon} Praxis Completeness: ${report.score}/100 (${report.rating})`);
|
|
3312
|
+
lines.push("");
|
|
3313
|
+
lines.push(`Rules: ${report.rules.covered}/${report.rules.total} domain branches covered (${pct(report.rules.covered, report.rules.total)})`);
|
|
3314
|
+
lines.push(`Constraints: ${report.constraints.covered}/${report.constraints.total} invariants covered (${pct(report.constraints.covered, report.constraints.total)})`);
|
|
3315
|
+
lines.push(`Contracts: ${report.contracts.withContracts}/${report.contracts.total} rules have contracts (${pct(report.contracts.withContracts, report.contracts.total)})`);
|
|
3316
|
+
lines.push(`Context: ${report.context.covered}/${report.context.total} state fields in context (${pct(report.context.covered, report.context.total)})`);
|
|
3317
|
+
lines.push(`Events: ${report.events.covered}/${report.events.total} transitions have events (${pct(report.events.covered, report.events.total)})`);
|
|
3318
|
+
if (report.rules.uncovered.length > 0) {
|
|
3319
|
+
lines.push("");
|
|
3320
|
+
lines.push("Uncovered domain logic:");
|
|
3321
|
+
for (const b of report.rules.uncovered) {
|
|
3322
|
+
lines.push(` \u274C ${b.location}: ${b.condition}${b.note ? ` \u2014 ${b.note}` : ""}`);
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
if (report.constraints.uncovered.length > 0) {
|
|
3326
|
+
lines.push("");
|
|
3327
|
+
lines.push("Uncovered invariants:");
|
|
3328
|
+
for (const b of report.constraints.uncovered) {
|
|
3329
|
+
lines.push(` \u274C ${b.location}: ${b.condition}${b.note ? ` \u2014 ${b.note}` : ""}`);
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
if (report.contracts.missing.length > 0) {
|
|
3333
|
+
lines.push("");
|
|
3334
|
+
lines.push("Rules missing contracts:");
|
|
3335
|
+
for (const id of report.contracts.missing) {
|
|
3336
|
+
lines.push(` \u{1F4DD} ${id}`);
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
if (report.events.missing.length > 0) {
|
|
3340
|
+
lines.push("");
|
|
3341
|
+
lines.push("State transitions without events:");
|
|
3342
|
+
for (const t of report.events.missing) {
|
|
3343
|
+
lines.push(` \u26A1 ${t.location}: ${t.description}`);
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
return lines.join("\n");
|
|
3347
|
+
}
|
|
3348
|
+
function pct(a, b) {
|
|
3349
|
+
if (b === 0) return "100%";
|
|
3350
|
+
return Math.round(a / b * 100) + "%";
|
|
3351
|
+
}
|
|
3026
3352
|
export {
|
|
3027
3353
|
ActorManager,
|
|
3354
|
+
Expectation,
|
|
3355
|
+
ExpectationSet,
|
|
3028
3356
|
ReactiveLogicEngine2 as FrameworkAgnosticReactiveEngine,
|
|
3029
3357
|
InMemoryPraxisDB,
|
|
3030
3358
|
LogicEngine,
|
|
@@ -3037,14 +3365,19 @@ export {
|
|
|
3037
3365
|
PraxisSchemaRegistry,
|
|
3038
3366
|
ReactiveLogicEngine,
|
|
3039
3367
|
RegistryIntrospector,
|
|
3368
|
+
RuleResult,
|
|
3040
3369
|
StateDocsGenerator,
|
|
3041
3370
|
attachAllIntegrations,
|
|
3042
3371
|
attachTauriToEngine,
|
|
3043
3372
|
attachToEngine,
|
|
3044
3373
|
attachUnumToEngine,
|
|
3374
|
+
auditCompleteness,
|
|
3375
|
+
branchRules,
|
|
3045
3376
|
canvasToMermaid,
|
|
3046
3377
|
canvasToSchema,
|
|
3047
3378
|
canvasToYaml,
|
|
3379
|
+
commitFromState,
|
|
3380
|
+
createApp,
|
|
3048
3381
|
createCanvasEditor,
|
|
3049
3382
|
createReactiveEngine2 as createFrameworkAgnosticReactiveEngine,
|
|
3050
3383
|
createInMemoryDB,
|
|
@@ -3063,28 +3396,60 @@ export {
|
|
|
3063
3396
|
createStateDocsGenerator,
|
|
3064
3397
|
createTauriPraxisAdapter,
|
|
3065
3398
|
createTimerActor,
|
|
3399
|
+
createUIModule,
|
|
3066
3400
|
createUnifiedApp,
|
|
3067
3401
|
createUnumAdapter,
|
|
3068
|
-
|
|
3402
|
+
dataRules,
|
|
3403
|
+
defineConstraint2 as defineConstraint,
|
|
3069
3404
|
defineEvent,
|
|
3070
3405
|
defineFact,
|
|
3071
|
-
|
|
3072
|
-
|
|
3406
|
+
defineGate,
|
|
3407
|
+
defineModule2 as defineModule,
|
|
3408
|
+
definePath,
|
|
3409
|
+
defineRule2 as defineRule,
|
|
3410
|
+
defineConstraint as defineUnifiedConstraint,
|
|
3411
|
+
defineModule as defineUnifiedModule,
|
|
3412
|
+
defineRule as defineUnifiedRule,
|
|
3413
|
+
dirtyGuardRule,
|
|
3414
|
+
errorDisplayRule,
|
|
3415
|
+
expectBehavior,
|
|
3416
|
+
expectationGate,
|
|
3417
|
+
fact,
|
|
3073
3418
|
filterEvents,
|
|
3074
3419
|
filterFacts,
|
|
3075
3420
|
findEvent,
|
|
3076
3421
|
findFact,
|
|
3422
|
+
formRules,
|
|
3423
|
+
formatGate,
|
|
3424
|
+
formatReport,
|
|
3425
|
+
formatVerificationReport,
|
|
3077
3426
|
generateDocs,
|
|
3078
3427
|
generateId,
|
|
3079
3428
|
generateTauriConfig,
|
|
3080
3429
|
getEventPath,
|
|
3081
3430
|
getFactPath,
|
|
3082
3431
|
getSchemaPath,
|
|
3432
|
+
initGateRule,
|
|
3433
|
+
inputRules,
|
|
3434
|
+
lintGate,
|
|
3083
3435
|
loadSchemaFromJson,
|
|
3084
3436
|
loadSchemaFromYaml,
|
|
3437
|
+
loadingGateRule,
|
|
3438
|
+
mustBeInitializedConstraint,
|
|
3439
|
+
navigationRequest,
|
|
3440
|
+
navigationRules,
|
|
3441
|
+
noInteractionWhileLoadingConstraint,
|
|
3442
|
+
offlineIndicatorRule,
|
|
3085
3443
|
registerSchema,
|
|
3444
|
+
resizeEvent,
|
|
3086
3445
|
schemaToCanvas,
|
|
3446
|
+
semverContract,
|
|
3447
|
+
toastRules,
|
|
3448
|
+
uiModule,
|
|
3449
|
+
uiStateChanged,
|
|
3087
3450
|
validateForGeneration,
|
|
3088
3451
|
validateSchema,
|
|
3089
|
-
validateWithGuardian
|
|
3452
|
+
validateWithGuardian,
|
|
3453
|
+
verify,
|
|
3454
|
+
viewportRule
|
|
3090
3455
|
};
|