@plures/praxis 1.2.41 → 1.3.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-32YFEEML.js → chunk-5JQJZADT.js} +1 -1
- package/dist/node/{chunk-BBP2F7TT.js → chunk-KMJWAFZV.js} +128 -5
- 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 +522 -59
- package/dist/node/index.d.cts +271 -5
- package/dist/node/index.d.ts +271 -5
- package/dist/node/index.js +355 -39
- 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 +2 -2
- 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-D-xTDxT5.d.ts → reactive-engine.svelte-Cg0Yc2Hs.d.cts} +90 -6
- package/dist/node/{reactive-engine.svelte-CGe8SpVE.d.cts → reactive-engine.svelte-DekxqFu0.d.ts} +90 -6
- package/package.json +2 -2
- package/src/__tests__/engine-v2.test.ts +532 -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/index.ts +27 -0
- package/src/vite/completeness-plugin.ts +72 -0
package/dist/node/index.js
CHANGED
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
import {
|
|
51
51
|
ReactiveLogicEngine,
|
|
52
52
|
createReactiveEngine
|
|
53
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-5JQJZADT.js";
|
|
54
54
|
import {
|
|
55
55
|
PraxisRegistry
|
|
56
56
|
} from "./chunk-R2PSBPKQ.js";
|
|
@@ -78,8 +78,10 @@ import {
|
|
|
78
78
|
import {
|
|
79
79
|
LogicEngine,
|
|
80
80
|
PRAXIS_PROTOCOL_VERSION,
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
RuleResult,
|
|
82
|
+
createPraxisEngine,
|
|
83
|
+
fact
|
|
84
|
+
} from "./chunk-KMJWAFZV.js";
|
|
83
85
|
import "./chunk-QGM4M3NI.js";
|
|
84
86
|
|
|
85
87
|
// src/core/reactive-engine.ts
|
|
@@ -739,37 +741,37 @@ var PraxisDBStore = class {
|
|
|
739
741
|
* @param fact The fact to store
|
|
740
742
|
* @returns Promise that resolves when the fact is stored
|
|
741
743
|
*/
|
|
742
|
-
async storeFact(
|
|
743
|
-
const constraintResult = await this.checkConstraints([
|
|
744
|
+
async storeFact(fact2) {
|
|
745
|
+
const constraintResult = await this.checkConstraints([fact2]);
|
|
744
746
|
if (!constraintResult.valid) {
|
|
745
747
|
throw new Error(`Constraint violation: ${constraintResult.errors.join(", ")}`);
|
|
746
748
|
}
|
|
747
749
|
let before;
|
|
748
750
|
if (this.chronicle) {
|
|
749
|
-
const payload =
|
|
751
|
+
const payload = fact2.payload;
|
|
750
752
|
const id = payload?.id;
|
|
751
753
|
if (id) {
|
|
752
|
-
before = await this.getFact(
|
|
754
|
+
before = await this.getFact(fact2.tag, id);
|
|
753
755
|
}
|
|
754
756
|
}
|
|
755
|
-
await this.persistFact(
|
|
757
|
+
await this.persistFact(fact2);
|
|
756
758
|
if (this.chronicle) {
|
|
757
|
-
const payload =
|
|
759
|
+
const payload = fact2.payload;
|
|
758
760
|
const id = payload?.id ?? "";
|
|
759
761
|
const span = ChronicleContext.current;
|
|
760
762
|
try {
|
|
761
763
|
await this.chronicle.record({
|
|
762
|
-
path: getFactPath(
|
|
764
|
+
path: getFactPath(fact2.tag, id),
|
|
763
765
|
before,
|
|
764
|
-
after:
|
|
766
|
+
after: fact2,
|
|
765
767
|
cause: span?.spanId,
|
|
766
768
|
context: span?.contextId,
|
|
767
|
-
metadata: { factTag:
|
|
769
|
+
metadata: { factTag: fact2.tag, operation: "storeFact" }
|
|
768
770
|
});
|
|
769
771
|
} catch {
|
|
770
772
|
}
|
|
771
773
|
}
|
|
772
|
-
await this.triggerRules([
|
|
774
|
+
await this.triggerRules([fact2]);
|
|
773
775
|
}
|
|
774
776
|
/**
|
|
775
777
|
* Store multiple facts in PluresDB
|
|
@@ -781,28 +783,28 @@ var PraxisDBStore = class {
|
|
|
781
783
|
if (!constraintResult.valid) {
|
|
782
784
|
throw new Error(`Constraint violation: ${constraintResult.errors.join(", ")}`);
|
|
783
785
|
}
|
|
784
|
-
for (const
|
|
786
|
+
for (const fact2 of facts) {
|
|
785
787
|
let before;
|
|
786
788
|
if (this.chronicle) {
|
|
787
|
-
const payload =
|
|
789
|
+
const payload = fact2.payload;
|
|
788
790
|
const id = payload?.id;
|
|
789
791
|
if (id) {
|
|
790
|
-
before = await this.getFact(
|
|
792
|
+
before = await this.getFact(fact2.tag, id);
|
|
791
793
|
}
|
|
792
794
|
}
|
|
793
|
-
await this.persistFact(
|
|
795
|
+
await this.persistFact(fact2);
|
|
794
796
|
if (this.chronicle) {
|
|
795
|
-
const payload =
|
|
797
|
+
const payload = fact2.payload;
|
|
796
798
|
const id = payload?.id ?? "";
|
|
797
799
|
const span = ChronicleContext.current;
|
|
798
800
|
try {
|
|
799
801
|
await this.chronicle.record({
|
|
800
|
-
path: getFactPath(
|
|
802
|
+
path: getFactPath(fact2.tag, id),
|
|
801
803
|
before,
|
|
802
|
-
after:
|
|
804
|
+
after: fact2,
|
|
803
805
|
cause: span?.spanId,
|
|
804
806
|
context: span?.contextId,
|
|
805
|
-
metadata: { factTag:
|
|
807
|
+
metadata: { factTag: fact2.tag, operation: "storeFacts" }
|
|
806
808
|
});
|
|
807
809
|
} catch {
|
|
808
810
|
}
|
|
@@ -814,11 +816,11 @@ var PraxisDBStore = class {
|
|
|
814
816
|
* Internal method to persist a fact without constraint checking
|
|
815
817
|
* Used by both storeFact and derived fact storage
|
|
816
818
|
*/
|
|
817
|
-
async persistFact(
|
|
818
|
-
const payload =
|
|
819
|
+
async persistFact(fact2) {
|
|
820
|
+
const payload = fact2.payload;
|
|
819
821
|
const id = payload?.id ?? generateId();
|
|
820
|
-
const path = getFactPath(
|
|
821
|
-
await this.db.set(path,
|
|
822
|
+
const path = getFactPath(fact2.tag, id);
|
|
823
|
+
await this.db.set(path, fact2);
|
|
822
824
|
}
|
|
823
825
|
/**
|
|
824
826
|
* Get a fact by tag and id
|
|
@@ -955,8 +957,8 @@ var PraxisDBStore = class {
|
|
|
955
957
|
if (watchers) {
|
|
956
958
|
watchers.add(callback);
|
|
957
959
|
}
|
|
958
|
-
const unsubscribe = this.db.watch(path, (
|
|
959
|
-
callback([
|
|
960
|
+
const unsubscribe = this.db.watch(path, (fact2) => {
|
|
961
|
+
callback([fact2]);
|
|
960
962
|
});
|
|
961
963
|
this.subscriptions.push(unsubscribe);
|
|
962
964
|
return () => {
|
|
@@ -1014,13 +1016,18 @@ var PraxisDBStore = class {
|
|
|
1014
1016
|
const state = {
|
|
1015
1017
|
context: this.context,
|
|
1016
1018
|
facts: [],
|
|
1019
|
+
events,
|
|
1017
1020
|
meta: {}
|
|
1018
1021
|
};
|
|
1019
1022
|
const derivedFacts = [];
|
|
1020
1023
|
for (const rule of rules) {
|
|
1021
1024
|
try {
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1025
|
+
const result = rule.impl(state, events);
|
|
1026
|
+
if (Array.isArray(result)) {
|
|
1027
|
+
derivedFacts.push(...result);
|
|
1028
|
+
} else if (result && "kind" in result && result.kind === "emit") {
|
|
1029
|
+
derivedFacts.push(...result.facts);
|
|
1030
|
+
}
|
|
1024
1031
|
} catch (error) {
|
|
1025
1032
|
this.onRuleError(rule.id, error);
|
|
1026
1033
|
}
|
|
@@ -1028,19 +1035,19 @@ var PraxisDBStore = class {
|
|
|
1028
1035
|
if (derivedFacts.length > 0) {
|
|
1029
1036
|
const constraintResult = await this.checkConstraints(derivedFacts);
|
|
1030
1037
|
if (constraintResult.valid) {
|
|
1031
|
-
for (const
|
|
1032
|
-
await this.persistFact(
|
|
1038
|
+
for (const fact2 of derivedFacts) {
|
|
1039
|
+
await this.persistFact(fact2);
|
|
1033
1040
|
if (this.chronicle) {
|
|
1034
|
-
const payload =
|
|
1041
|
+
const payload = fact2.payload;
|
|
1035
1042
|
const id = payload?.id ?? "";
|
|
1036
1043
|
const span = ChronicleContext.current;
|
|
1037
1044
|
try {
|
|
1038
1045
|
await this.chronicle.record({
|
|
1039
|
-
path: getFactPath(
|
|
1040
|
-
after:
|
|
1046
|
+
path: getFactPath(fact2.tag, id),
|
|
1047
|
+
after: fact2,
|
|
1041
1048
|
cause: span?.spanId,
|
|
1042
1049
|
context: span?.contextId,
|
|
1043
|
-
metadata: { factTag:
|
|
1050
|
+
metadata: { factTag: fact2.tag, operation: "derivedFact" }
|
|
1044
1051
|
});
|
|
1045
1052
|
} catch {
|
|
1046
1053
|
}
|
|
@@ -1479,7 +1486,7 @@ async function createUnumAdapter(config) {
|
|
|
1479
1486
|
type: "event"
|
|
1480
1487
|
});
|
|
1481
1488
|
}
|
|
1482
|
-
async function broadcastFact(channelId,
|
|
1489
|
+
async function broadcastFact(channelId, fact2) {
|
|
1483
1490
|
const channel = channels.get(channelId);
|
|
1484
1491
|
if (!channel) {
|
|
1485
1492
|
throw new Error(`Not joined to channel ${channelId}`);
|
|
@@ -1487,7 +1494,7 @@ async function createUnumAdapter(config) {
|
|
|
1487
1494
|
await channel.publish({
|
|
1488
1495
|
id: generateId2(),
|
|
1489
1496
|
sender: currentIdentity || { id: "anonymous", createdAt: Date.now() },
|
|
1490
|
-
content:
|
|
1497
|
+
content: fact2,
|
|
1491
1498
|
type: "fact"
|
|
1492
1499
|
});
|
|
1493
1500
|
}
|
|
@@ -1768,7 +1775,7 @@ function generateTauriConfig(config) {
|
|
|
1768
1775
|
|
|
1769
1776
|
// src/integrations/unified.ts
|
|
1770
1777
|
async function createUnifiedApp(config) {
|
|
1771
|
-
const { createPraxisEngine: createPraxisEngine2 } = await import("./engine-
|
|
1778
|
+
const { createPraxisEngine: createPraxisEngine2 } = await import("./engine-FEN5IYZ5.js");
|
|
1772
1779
|
const { createInMemoryDB: createInMemoryDB2 } = await import("./adapter-75ISSMWD.js");
|
|
1773
1780
|
const db = config.db || createInMemoryDB2();
|
|
1774
1781
|
const pluresdb = createPluresDBAdapter({
|
|
@@ -1897,6 +1904,298 @@ async function attachAllIntegrations(engine, registry, options = {}) {
|
|
|
1897
1904
|
}
|
|
1898
1905
|
};
|
|
1899
1906
|
}
|
|
1907
|
+
|
|
1908
|
+
// src/core/ui-rules.ts
|
|
1909
|
+
var loadingGateRule = {
|
|
1910
|
+
id: "ui/loading-gate",
|
|
1911
|
+
description: "Signals when the app is in a loading state",
|
|
1912
|
+
eventTypes: ["ui.state-change", "app.init"],
|
|
1913
|
+
impl: (state) => {
|
|
1914
|
+
const ctx = state.context;
|
|
1915
|
+
if (ctx.loading) {
|
|
1916
|
+
return RuleResult.emit([fact("ui.loading-gate", { active: true })]);
|
|
1917
|
+
}
|
|
1918
|
+
return RuleResult.retract(["ui.loading-gate"], "Not loading");
|
|
1919
|
+
},
|
|
1920
|
+
contract: {
|
|
1921
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
1922
|
+
behavior: "Emits ui.loading-gate when context.loading is true, retracts when false",
|
|
1923
|
+
examples: [
|
|
1924
|
+
{ given: "loading is true", when: "ui state changes", then: "ui.loading-gate emitted" },
|
|
1925
|
+
{ given: "loading is false", when: "ui state changes", then: "ui.loading-gate retracted" }
|
|
1926
|
+
],
|
|
1927
|
+
invariants: ["Loading gate must reflect context.loading exactly"]
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
var errorDisplayRule = {
|
|
1931
|
+
id: "ui/error-display",
|
|
1932
|
+
description: "Signals when an error should be displayed to the user",
|
|
1933
|
+
eventTypes: ["ui.state-change", "app.error"],
|
|
1934
|
+
impl: (state) => {
|
|
1935
|
+
const ctx = state.context;
|
|
1936
|
+
if (ctx.error) {
|
|
1937
|
+
return RuleResult.emit([fact("ui.error-display", { message: ctx.error, severity: "error" })]);
|
|
1938
|
+
}
|
|
1939
|
+
return RuleResult.retract(["ui.error-display"], "Error cleared");
|
|
1940
|
+
},
|
|
1941
|
+
contract: {
|
|
1942
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
1943
|
+
behavior: "Emits ui.error-display when context.error is non-null, retracts when cleared",
|
|
1944
|
+
examples: [
|
|
1945
|
+
{ given: "error is set", when: "ui state changes", then: "ui.error-display emitted with message" },
|
|
1946
|
+
{ given: "error is null", when: "ui state changes", then: "ui.error-display retracted" }
|
|
1947
|
+
],
|
|
1948
|
+
invariants: ["Error display must clear when error is null"]
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
var offlineIndicatorRule = {
|
|
1952
|
+
id: "ui/offline-indicator",
|
|
1953
|
+
description: "Signals when the app is offline",
|
|
1954
|
+
eventTypes: ["ui.state-change", "network.change"],
|
|
1955
|
+
impl: (state) => {
|
|
1956
|
+
if (state.context.offline) {
|
|
1957
|
+
return RuleResult.emit([fact("ui.offline", { message: "You are offline. Changes will sync when reconnected." })]);
|
|
1958
|
+
}
|
|
1959
|
+
return RuleResult.retract(["ui.offline"], "Back online");
|
|
1960
|
+
},
|
|
1961
|
+
contract: {
|
|
1962
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
1963
|
+
behavior: "Emits ui.offline when context.offline is true, retracts when back online",
|
|
1964
|
+
examples: [
|
|
1965
|
+
{ given: "offline is true", when: "network changes", then: "ui.offline emitted" },
|
|
1966
|
+
{ given: "offline is false", when: "network changes", then: "ui.offline retracted" }
|
|
1967
|
+
],
|
|
1968
|
+
invariants: ["Offline indicator must match actual connectivity"]
|
|
1969
|
+
}
|
|
1970
|
+
};
|
|
1971
|
+
var dirtyGuardRule = {
|
|
1972
|
+
id: "ui/dirty-guard",
|
|
1973
|
+
description: "Warns when there are unsaved changes",
|
|
1974
|
+
eventTypes: ["ui.state-change", "navigation.request"],
|
|
1975
|
+
impl: (state) => {
|
|
1976
|
+
if (state.context.dirty) {
|
|
1977
|
+
return RuleResult.emit([fact("ui.unsaved-warning", {
|
|
1978
|
+
message: "You have unsaved changes",
|
|
1979
|
+
blocking: true
|
|
1980
|
+
})]);
|
|
1981
|
+
}
|
|
1982
|
+
return RuleResult.retract(["ui.unsaved-warning"], "No unsaved changes");
|
|
1983
|
+
},
|
|
1984
|
+
contract: {
|
|
1985
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
1986
|
+
behavior: "Emits ui.unsaved-warning when context.dirty is true, retracts when saved",
|
|
1987
|
+
examples: [
|
|
1988
|
+
{ given: "dirty is true", when: "ui state changes", then: "ui.unsaved-warning emitted with blocking=true" },
|
|
1989
|
+
{ given: "dirty is false", when: "ui state changes", then: "ui.unsaved-warning retracted" }
|
|
1990
|
+
],
|
|
1991
|
+
invariants: ["Dirty guard must clear after save"]
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
var initGateRule = {
|
|
1995
|
+
id: "ui/init-gate",
|
|
1996
|
+
description: "Signals whether the app has completed initialization",
|
|
1997
|
+
eventTypes: ["ui.state-change", "app.init"],
|
|
1998
|
+
impl: (state) => {
|
|
1999
|
+
if (!state.context.initialized) {
|
|
2000
|
+
return RuleResult.emit([fact("ui.init-pending", {
|
|
2001
|
+
message: "App is initializing..."
|
|
2002
|
+
})]);
|
|
2003
|
+
}
|
|
2004
|
+
return RuleResult.retract(["ui.init-pending"], "App initialized");
|
|
2005
|
+
},
|
|
2006
|
+
contract: {
|
|
2007
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
2008
|
+
behavior: "Emits ui.init-pending until context.initialized is true",
|
|
2009
|
+
examples: [
|
|
2010
|
+
{ given: "initialized is false", when: "app starts", then: "ui.init-pending emitted" },
|
|
2011
|
+
{ given: "initialized is true", when: "init completes", then: "ui.init-pending retracted" }
|
|
2012
|
+
],
|
|
2013
|
+
invariants: ["Init gate must clear exactly once, when initialization completes"]
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
var viewportRule = {
|
|
2017
|
+
id: "ui/viewport-class",
|
|
2018
|
+
description: "Classifies viewport size for responsive layout decisions",
|
|
2019
|
+
eventTypes: ["ui.state-change", "ui.resize"],
|
|
2020
|
+
impl: (state) => {
|
|
2021
|
+
const vp = state.context.viewport;
|
|
2022
|
+
if (!vp) return RuleResult.skip("No viewport data");
|
|
2023
|
+
return RuleResult.emit([fact("ui.viewport-class", {
|
|
2024
|
+
viewport: vp,
|
|
2025
|
+
compact: vp === "mobile",
|
|
2026
|
+
showSidebar: vp !== "mobile"
|
|
2027
|
+
})]);
|
|
2028
|
+
},
|
|
2029
|
+
contract: {
|
|
2030
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
2031
|
+
behavior: "Classifies viewport into responsive layout hints",
|
|
2032
|
+
examples: [
|
|
2033
|
+
{ given: "viewport is mobile", when: "resize event", then: "compact=true, showSidebar=false" },
|
|
2034
|
+
{ given: "viewport is desktop", when: "resize event", then: "compact=false, showSidebar=true" }
|
|
2035
|
+
],
|
|
2036
|
+
invariants: ["Viewport class must update on every resize event"]
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2039
|
+
var noInteractionWhileLoadingConstraint = {
|
|
2040
|
+
id: "ui/no-interaction-while-loading",
|
|
2041
|
+
description: "Prevents data mutations while a load operation is in progress",
|
|
2042
|
+
impl: (state) => {
|
|
2043
|
+
if (state.context.loading) {
|
|
2044
|
+
return "Cannot perform action while data is loading";
|
|
2045
|
+
}
|
|
2046
|
+
return true;
|
|
2047
|
+
},
|
|
2048
|
+
contract: {
|
|
2049
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
2050
|
+
behavior: "Fails when context.loading is true",
|
|
2051
|
+
examples: [
|
|
2052
|
+
{ given: "loading is true", when: "action attempted", then: "violation" },
|
|
2053
|
+
{ given: "loading is false", when: "action attempted", then: "pass" }
|
|
2054
|
+
],
|
|
2055
|
+
invariants: ["Must always fail during loading"]
|
|
2056
|
+
}
|
|
2057
|
+
};
|
|
2058
|
+
var mustBeInitializedConstraint = {
|
|
2059
|
+
id: "ui/must-be-initialized",
|
|
2060
|
+
description: "Requires app initialization before user interactions",
|
|
2061
|
+
impl: (state) => {
|
|
2062
|
+
if (!state.context.initialized) {
|
|
2063
|
+
return "App must be initialized before performing this action";
|
|
2064
|
+
}
|
|
2065
|
+
return true;
|
|
2066
|
+
},
|
|
2067
|
+
contract: {
|
|
2068
|
+
ruleId: "RULE_ID_PLACEHOLDER",
|
|
2069
|
+
behavior: "Fails when context.initialized is false",
|
|
2070
|
+
examples: [
|
|
2071
|
+
{ given: "initialized is false", when: "action attempted", then: "violation" },
|
|
2072
|
+
{ given: "initialized is true", when: "action attempted", then: "pass" }
|
|
2073
|
+
],
|
|
2074
|
+
invariants: ["Must always fail before init completes"]
|
|
2075
|
+
}
|
|
2076
|
+
};
|
|
2077
|
+
var uiModule = {
|
|
2078
|
+
rules: [
|
|
2079
|
+
loadingGateRule,
|
|
2080
|
+
errorDisplayRule,
|
|
2081
|
+
offlineIndicatorRule,
|
|
2082
|
+
dirtyGuardRule,
|
|
2083
|
+
initGateRule,
|
|
2084
|
+
viewportRule
|
|
2085
|
+
],
|
|
2086
|
+
constraints: [
|
|
2087
|
+
noInteractionWhileLoadingConstraint,
|
|
2088
|
+
mustBeInitializedConstraint
|
|
2089
|
+
],
|
|
2090
|
+
meta: {
|
|
2091
|
+
name: "praxis-ui",
|
|
2092
|
+
version: "1.0.0",
|
|
2093
|
+
description: "Predefined UI rules and constraints \u2014 separate from business logic"
|
|
2094
|
+
}
|
|
2095
|
+
};
|
|
2096
|
+
function createUIModule(options) {
|
|
2097
|
+
const allRules = uiModule.rules;
|
|
2098
|
+
const allConstraints = uiModule.constraints;
|
|
2099
|
+
const selectedRules = options.rules ? allRules.filter((r) => options.rules.includes(r.id)) : allRules;
|
|
2100
|
+
const selectedConstraints = options.constraints ? allConstraints.filter((c) => options.constraints.includes(c.id)) : allConstraints;
|
|
2101
|
+
return {
|
|
2102
|
+
rules: [...selectedRules, ...options.extraRules ?? []],
|
|
2103
|
+
constraints: [...selectedConstraints, ...options.extraConstraints ?? []],
|
|
2104
|
+
meta: { ...uiModule.meta, customized: true }
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
function uiStateChanged(changes) {
|
|
2108
|
+
return { tag: "ui.state-change", payload: changes ?? {} };
|
|
2109
|
+
}
|
|
2110
|
+
function navigationRequest(to) {
|
|
2111
|
+
return { tag: "navigation.request", payload: { to } };
|
|
2112
|
+
}
|
|
2113
|
+
function resizeEvent(width, height) {
|
|
2114
|
+
return { tag: "ui.resize", payload: { width, height } };
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// src/core/completeness.ts
|
|
2118
|
+
function auditCompleteness(manifest, registryRuleIds, registryConstraintIds, rulesWithContracts, config) {
|
|
2119
|
+
const threshold = config?.threshold ?? 90;
|
|
2120
|
+
const domainBranches = manifest.branches.filter((b) => b.kind === "domain");
|
|
2121
|
+
const coveredDomain = domainBranches.filter((b) => b.coveredBy && registryRuleIds.includes(b.coveredBy));
|
|
2122
|
+
const uncoveredDomain = domainBranches.filter((b) => !b.coveredBy || !registryRuleIds.includes(b.coveredBy));
|
|
2123
|
+
const invariantBranches = manifest.branches.filter((b) => b.kind === "invariant");
|
|
2124
|
+
const coveredInvariants = invariantBranches.filter((b) => b.coveredBy && registryConstraintIds.includes(b.coveredBy));
|
|
2125
|
+
const uncoveredInvariants = invariantBranches.filter((b) => !b.coveredBy || !registryConstraintIds.includes(b.coveredBy));
|
|
2126
|
+
const needContracts = manifest.rulesNeedingContracts;
|
|
2127
|
+
const haveContracts = needContracts.filter((id) => rulesWithContracts.includes(id));
|
|
2128
|
+
const missingContracts = needContracts.filter((id) => !rulesWithContracts.includes(id));
|
|
2129
|
+
const neededFields = manifest.stateFields.filter((f) => f.usedByRule);
|
|
2130
|
+
const coveredFields = neededFields.filter((f) => f.inContext);
|
|
2131
|
+
const missingFields = neededFields.filter((f) => !f.inContext);
|
|
2132
|
+
const coveredTransitions = manifest.transitions.filter((t) => t.eventTag);
|
|
2133
|
+
const missingTransitions = manifest.transitions.filter((t) => !t.eventTag);
|
|
2134
|
+
const ruleScore = domainBranches.length > 0 ? coveredDomain.length / domainBranches.length * 40 : 40;
|
|
2135
|
+
const constraintScore = invariantBranches.length > 0 ? coveredInvariants.length / invariantBranches.length * 20 : 20;
|
|
2136
|
+
const contractScore = needContracts.length > 0 ? haveContracts.length / needContracts.length * 15 : 15;
|
|
2137
|
+
const contextScore = neededFields.length > 0 ? coveredFields.length / neededFields.length * 15 : 15;
|
|
2138
|
+
const eventScore = manifest.transitions.length > 0 ? coveredTransitions.length / manifest.transitions.length * 10 : 10;
|
|
2139
|
+
const score = Math.round(ruleScore + constraintScore + contractScore + contextScore + eventScore);
|
|
2140
|
+
const rating = score >= 90 ? "complete" : score >= 70 ? "good" : score >= 50 ? "partial" : "incomplete";
|
|
2141
|
+
const report = {
|
|
2142
|
+
score,
|
|
2143
|
+
rating,
|
|
2144
|
+
rules: { total: domainBranches.length, covered: coveredDomain.length, uncovered: uncoveredDomain },
|
|
2145
|
+
constraints: { total: invariantBranches.length, covered: coveredInvariants.length, uncovered: uncoveredInvariants },
|
|
2146
|
+
contracts: { total: needContracts.length, withContracts: haveContracts.length, missing: missingContracts },
|
|
2147
|
+
context: { total: neededFields.length, covered: coveredFields.length, missing: missingFields },
|
|
2148
|
+
events: { total: manifest.transitions.length, covered: coveredTransitions.length, missing: missingTransitions }
|
|
2149
|
+
};
|
|
2150
|
+
if (config?.strict && score < threshold) {
|
|
2151
|
+
throw new Error(`Praxis completeness ${score}/100 (${rating}) \u2014 below threshold ${threshold}. ${uncoveredDomain.length} uncovered rules, ${uncoveredInvariants.length} uncovered invariants, ${missingContracts.length} missing contracts.`);
|
|
2152
|
+
}
|
|
2153
|
+
return report;
|
|
2154
|
+
}
|
|
2155
|
+
function formatReport(report) {
|
|
2156
|
+
const lines = [];
|
|
2157
|
+
const icon = report.rating === "complete" ? "\u2705" : report.rating === "good" ? "\u{1F7E2}" : report.rating === "partial" ? "\u{1F7E1}" : "\u{1F534}";
|
|
2158
|
+
lines.push(`${icon} Praxis Completeness: ${report.score}/100 (${report.rating})`);
|
|
2159
|
+
lines.push("");
|
|
2160
|
+
lines.push(`Rules: ${report.rules.covered}/${report.rules.total} domain branches covered (${pct(report.rules.covered, report.rules.total)})`);
|
|
2161
|
+
lines.push(`Constraints: ${report.constraints.covered}/${report.constraints.total} invariants covered (${pct(report.constraints.covered, report.constraints.total)})`);
|
|
2162
|
+
lines.push(`Contracts: ${report.contracts.withContracts}/${report.contracts.total} rules have contracts (${pct(report.contracts.withContracts, report.contracts.total)})`);
|
|
2163
|
+
lines.push(`Context: ${report.context.covered}/${report.context.total} state fields in context (${pct(report.context.covered, report.context.total)})`);
|
|
2164
|
+
lines.push(`Events: ${report.events.covered}/${report.events.total} transitions have events (${pct(report.events.covered, report.events.total)})`);
|
|
2165
|
+
if (report.rules.uncovered.length > 0) {
|
|
2166
|
+
lines.push("");
|
|
2167
|
+
lines.push("Uncovered domain logic:");
|
|
2168
|
+
for (const b of report.rules.uncovered) {
|
|
2169
|
+
lines.push(` \u274C ${b.location}: ${b.condition}${b.note ? ` \u2014 ${b.note}` : ""}`);
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
if (report.constraints.uncovered.length > 0) {
|
|
2173
|
+
lines.push("");
|
|
2174
|
+
lines.push("Uncovered invariants:");
|
|
2175
|
+
for (const b of report.constraints.uncovered) {
|
|
2176
|
+
lines.push(` \u274C ${b.location}: ${b.condition}${b.note ? ` \u2014 ${b.note}` : ""}`);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
if (report.contracts.missing.length > 0) {
|
|
2180
|
+
lines.push("");
|
|
2181
|
+
lines.push("Rules missing contracts:");
|
|
2182
|
+
for (const id of report.contracts.missing) {
|
|
2183
|
+
lines.push(` \u{1F4DD} ${id}`);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
if (report.events.missing.length > 0) {
|
|
2187
|
+
lines.push("");
|
|
2188
|
+
lines.push("State transitions without events:");
|
|
2189
|
+
for (const t of report.events.missing) {
|
|
2190
|
+
lines.push(` \u26A1 ${t.location}: ${t.description}`);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
return lines.join("\n");
|
|
2194
|
+
}
|
|
2195
|
+
function pct(a, b) {
|
|
2196
|
+
if (b === 0) return "100%";
|
|
2197
|
+
return Math.round(a / b * 100) + "%";
|
|
2198
|
+
}
|
|
1900
2199
|
export {
|
|
1901
2200
|
AcknowledgeContractGap,
|
|
1902
2201
|
ActorManager,
|
|
@@ -1921,6 +2220,7 @@ export {
|
|
|
1921
2220
|
PraxisSchemaRegistry,
|
|
1922
2221
|
ReactiveLogicEngine,
|
|
1923
2222
|
RegistryIntrospector,
|
|
2223
|
+
RuleResult,
|
|
1924
2224
|
StateDocsGenerator,
|
|
1925
2225
|
TerminalAdapter,
|
|
1926
2226
|
ValidateContracts,
|
|
@@ -1928,6 +2228,7 @@ export {
|
|
|
1928
2228
|
attachTauriToEngine,
|
|
1929
2229
|
attachToEngine,
|
|
1930
2230
|
attachUnumToEngine,
|
|
2231
|
+
auditCompleteness,
|
|
1931
2232
|
canvasToMermaid,
|
|
1932
2233
|
canvasToSchema,
|
|
1933
2234
|
canvasToYaml,
|
|
@@ -1953,6 +2254,7 @@ export {
|
|
|
1953
2254
|
createTauriPraxisAdapter,
|
|
1954
2255
|
createTerminalAdapter,
|
|
1955
2256
|
createTimerActor,
|
|
2257
|
+
createUIModule,
|
|
1956
2258
|
createUnifiedApp,
|
|
1957
2259
|
createUnumAdapter,
|
|
1958
2260
|
defineConstraint,
|
|
@@ -1961,10 +2263,14 @@ export {
|
|
|
1961
2263
|
defineFact,
|
|
1962
2264
|
defineModule,
|
|
1963
2265
|
defineRule,
|
|
2266
|
+
dirtyGuardRule,
|
|
2267
|
+
errorDisplayRule,
|
|
2268
|
+
fact,
|
|
1964
2269
|
filterEvents,
|
|
1965
2270
|
filterFacts,
|
|
1966
2271
|
findEvent,
|
|
1967
2272
|
findFact,
|
|
2273
|
+
formatReport,
|
|
1968
2274
|
formatValidationReport,
|
|
1969
2275
|
formatValidationReportJSON,
|
|
1970
2276
|
formatValidationReportSARIF,
|
|
@@ -1975,16 +2281,26 @@ export {
|
|
|
1975
2281
|
getEventPath,
|
|
1976
2282
|
getFactPath,
|
|
1977
2283
|
getSchemaPath,
|
|
2284
|
+
initGateRule,
|
|
1978
2285
|
isContract,
|
|
1979
2286
|
loadSchema,
|
|
1980
2287
|
loadSchemaFromFile,
|
|
1981
2288
|
loadSchemaFromJson,
|
|
1982
2289
|
loadSchemaFromYaml,
|
|
2290
|
+
loadingGateRule,
|
|
2291
|
+
mustBeInitializedConstraint,
|
|
2292
|
+
navigationRequest,
|
|
2293
|
+
noInteractionWhileLoadingConstraint,
|
|
2294
|
+
offlineIndicatorRule,
|
|
1983
2295
|
registerSchema,
|
|
2296
|
+
resizeEvent,
|
|
1984
2297
|
runTerminalCommand,
|
|
1985
2298
|
schemaToCanvas,
|
|
2299
|
+
uiModule,
|
|
2300
|
+
uiStateChanged,
|
|
1986
2301
|
validateContracts,
|
|
1987
2302
|
validateForGeneration,
|
|
1988
2303
|
validateSchema,
|
|
1989
|
-
validateWithGuardian
|
|
2304
|
+
validateWithGuardian,
|
|
2305
|
+
viewportRule
|
|
1990
2306
|
};
|