@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/node/index.cjs
CHANGED
|
@@ -622,12 +622,15 @@ __export(src_exports, {
|
|
|
622
622
|
PraxisDBStore: () => PraxisDBStore,
|
|
623
623
|
PraxisRegistry: () => PraxisRegistry,
|
|
624
624
|
PraxisSchemaRegistry: () => PraxisSchemaRegistry,
|
|
625
|
+
ProjectChronicle: () => ProjectChronicle,
|
|
625
626
|
ReactiveLogicEngine: () => ReactiveLogicEngine,
|
|
626
627
|
RegistryIntrospector: () => RegistryIntrospector,
|
|
627
628
|
RuleResult: () => RuleResult,
|
|
628
629
|
StateDocsGenerator: () => StateDocsGenerator,
|
|
629
630
|
TerminalAdapter: () => TerminalAdapter,
|
|
631
|
+
Timeline: () => Timeline,
|
|
630
632
|
ValidateContracts: () => ValidateContracts,
|
|
633
|
+
analyzeDependencyGraph: () => analyzeDependencyGraph,
|
|
631
634
|
attachAllIntegrations: () => attachAllIntegrations,
|
|
632
635
|
attachTauriToEngine: () => attachTauriToEngine,
|
|
633
636
|
attachToEngine: () => attachToEngine,
|
|
@@ -638,6 +641,7 @@ __export(src_exports, {
|
|
|
638
641
|
canvasToSchema: () => canvasToSchema,
|
|
639
642
|
canvasToYaml: () => canvasToYaml,
|
|
640
643
|
commitFromState: () => commitFromState,
|
|
644
|
+
createApp: () => createApp,
|
|
641
645
|
createBehaviorLedger: () => createBehaviorLedger,
|
|
642
646
|
createCanvasEditor: () => createCanvasEditor,
|
|
643
647
|
createChronicle: () => createChronicle,
|
|
@@ -653,16 +657,19 @@ __export(src_exports, {
|
|
|
653
657
|
createPraxisDBStore: () => createPraxisDBStore,
|
|
654
658
|
createPraxisEngine: () => createPraxisEngine,
|
|
655
659
|
createPraxisLocalFirst: () => createPraxisLocalFirst,
|
|
660
|
+
createProjectChronicle: () => createProjectChronicle,
|
|
656
661
|
createReactiveEngine: () => createReactiveEngine,
|
|
657
662
|
createSchemaRegistry: () => createSchemaRegistry,
|
|
658
663
|
createSchemaTemplate: () => createSchemaTemplate,
|
|
659
664
|
createStateDocsGenerator: () => createStateDocsGenerator,
|
|
660
665
|
createTauriPraxisAdapter: () => createTauriPraxisAdapter,
|
|
661
666
|
createTerminalAdapter: () => createTerminalAdapter,
|
|
667
|
+
createTimeline: () => createTimeline,
|
|
662
668
|
createTimerActor: () => createTimerActor,
|
|
663
669
|
createUIModule: () => createUIModule,
|
|
664
670
|
createUnifiedApp: () => createUnifiedApp,
|
|
665
671
|
createUnumAdapter: () => createUnumAdapter,
|
|
672
|
+
crossReferenceContracts: () => crossReferenceContracts,
|
|
666
673
|
dataRules: () => dataRules,
|
|
667
674
|
defineConstraint: () => defineConstraint,
|
|
668
675
|
defineContract: () => defineContract,
|
|
@@ -670,18 +677,38 @@ __export(src_exports, {
|
|
|
670
677
|
defineFact: () => defineFact,
|
|
671
678
|
defineGate: () => defineGate,
|
|
672
679
|
defineModule: () => defineModule,
|
|
680
|
+
definePath: () => definePath,
|
|
673
681
|
defineRule: () => defineRule,
|
|
682
|
+
defineUnifiedConstraint: () => defineConstraint2,
|
|
683
|
+
defineUnifiedModule: () => defineModule2,
|
|
684
|
+
defineUnifiedRule: () => defineRule2,
|
|
685
|
+
diffContracts: () => diffContracts,
|
|
686
|
+
diffExpectations: () => diffExpectations,
|
|
687
|
+
diffLedgers: () => diffLedgers,
|
|
688
|
+
diffRegistries: () => diffRegistries,
|
|
674
689
|
dirtyGuardRule: () => dirtyGuardRule,
|
|
690
|
+
enableProjectChronicle: () => enableProjectChronicle,
|
|
675
691
|
errorDisplayRule: () => errorDisplayRule,
|
|
676
692
|
expectBehavior: () => expectBehavior,
|
|
677
693
|
expectationGate: () => expectationGate,
|
|
678
694
|
fact: () => fact,
|
|
679
695
|
filterEvents: () => filterEvents,
|
|
680
696
|
filterFacts: () => filterFacts,
|
|
697
|
+
findContractGaps: () => findContractGaps,
|
|
698
|
+
findContradictions: () => findContradictions,
|
|
699
|
+
findDeadRules: () => findDeadRules,
|
|
681
700
|
findEvent: () => findEvent,
|
|
682
701
|
findFact: () => findFact,
|
|
702
|
+
findGaps: () => findGaps,
|
|
703
|
+
findShadowedRules: () => findShadowedRules,
|
|
704
|
+
findUnreachableStates: () => findUnreachableStates,
|
|
683
705
|
formRules: () => formRules,
|
|
706
|
+
formatBehavioralCommit: () => formatCommitMessage,
|
|
707
|
+
formatBuildOutput: () => formatBuildOutput,
|
|
708
|
+
formatDelta: () => formatDelta,
|
|
684
709
|
formatGate: () => formatGate,
|
|
710
|
+
formatLedger: () => formatLedger,
|
|
711
|
+
formatReleaseNotes: () => formatReleaseNotes,
|
|
685
712
|
formatReport: () => formatReport,
|
|
686
713
|
formatValidationReport: () => formatValidationReport,
|
|
687
714
|
formatValidationReportJSON: () => formatValidationReportJSON,
|
|
@@ -689,6 +716,7 @@ __export(src_exports, {
|
|
|
689
716
|
formatVerificationReport: () => formatVerificationReport,
|
|
690
717
|
generateDocs: () => generateDocs,
|
|
691
718
|
generateId: () => generateId,
|
|
719
|
+
generateLedger: () => generateLedger,
|
|
692
720
|
generateTauriConfig: () => generateTauriConfig,
|
|
693
721
|
getContract: () => getContract,
|
|
694
722
|
getEventPath: () => getEventPath,
|
|
@@ -708,12 +736,17 @@ __export(src_exports, {
|
|
|
708
736
|
navigationRules: () => navigationRules,
|
|
709
737
|
noInteractionWhileLoadingConstraint: () => noInteractionWhileLoadingConstraint,
|
|
710
738
|
offlineIndicatorRule: () => offlineIndicatorRule,
|
|
739
|
+
recordAudit: () => recordAudit,
|
|
711
740
|
registerSchema: () => registerSchema,
|
|
712
741
|
resizeEvent: () => resizeEvent,
|
|
713
742
|
runTerminalCommand: () => runTerminalCommand,
|
|
714
743
|
schemaToCanvas: () => schemaToCanvas,
|
|
715
744
|
semverContract: () => semverContract,
|
|
745
|
+
suggest: () => suggest,
|
|
746
|
+
suggestAll: () => suggestAll,
|
|
716
747
|
toastRules: () => toastRules,
|
|
748
|
+
traceDerivation: () => traceDerivation,
|
|
749
|
+
traceImpact: () => traceImpact,
|
|
717
750
|
uiModule: () => uiModule,
|
|
718
751
|
uiStateChanged: () => uiStateChanged,
|
|
719
752
|
validateContracts: () => validateContracts,
|
|
@@ -721,6 +754,8 @@ __export(src_exports, {
|
|
|
721
754
|
validateSchema: () => validateSchema,
|
|
722
755
|
validateWithGuardian: () => validateWithGuardian,
|
|
723
756
|
verify: () => verify,
|
|
757
|
+
verifyContractExamples: () => verifyContractExamples,
|
|
758
|
+
verifyInvariants: () => verifyInvariants,
|
|
724
759
|
viewportRule: () => viewportRule
|
|
725
760
|
});
|
|
726
761
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -2012,6 +2047,1190 @@ var import_node_path = __toESM(require("path"), 1);
|
|
|
2012
2047
|
var import_node_fs2 = require("fs");
|
|
2013
2048
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
2014
2049
|
|
|
2050
|
+
// src/decision-ledger/analyzer.ts
|
|
2051
|
+
init_rule_result();
|
|
2052
|
+
function analyzeDependencyGraph(registry) {
|
|
2053
|
+
const facts = /* @__PURE__ */ new Map();
|
|
2054
|
+
const edges = [];
|
|
2055
|
+
const producers = /* @__PURE__ */ new Map();
|
|
2056
|
+
const consumers = /* @__PURE__ */ new Map();
|
|
2057
|
+
const rules = registry.getAllRules();
|
|
2058
|
+
for (const rule of rules) {
|
|
2059
|
+
const ruleId = rule.id;
|
|
2060
|
+
const produced = [];
|
|
2061
|
+
const consumed = [];
|
|
2062
|
+
const probeFacts = probeRuleExecution(rule);
|
|
2063
|
+
for (const tag of probeFacts.produced) {
|
|
2064
|
+
produced.push(tag);
|
|
2065
|
+
}
|
|
2066
|
+
for (const tag of probeFacts.consumed) {
|
|
2067
|
+
consumed.push(tag);
|
|
2068
|
+
}
|
|
2069
|
+
if (rule.contract) {
|
|
2070
|
+
for (const example of rule.contract.examples) {
|
|
2071
|
+
const thenTags = extractFactTagsFromText(example.then);
|
|
2072
|
+
for (const tag of thenTags) {
|
|
2073
|
+
if (!produced.includes(tag)) produced.push(tag);
|
|
2074
|
+
}
|
|
2075
|
+
const givenTags = extractFactTagsFromText(example.given);
|
|
2076
|
+
for (const tag of givenTags) {
|
|
2077
|
+
if (!consumed.includes(tag)) consumed.push(tag);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
producers.set(ruleId, produced);
|
|
2082
|
+
consumers.set(ruleId, consumed);
|
|
2083
|
+
for (const tag of produced) {
|
|
2084
|
+
const node = getOrCreateFactNode(facts, tag);
|
|
2085
|
+
if (!node.producedBy.includes(ruleId)) {
|
|
2086
|
+
node.producedBy.push(ruleId);
|
|
2087
|
+
}
|
|
2088
|
+
edges.push({ from: ruleId, to: tag, type: "produces" });
|
|
2089
|
+
}
|
|
2090
|
+
for (const tag of consumed) {
|
|
2091
|
+
const node = getOrCreateFactNode(facts, tag);
|
|
2092
|
+
if (!node.consumedBy.includes(ruleId)) {
|
|
2093
|
+
node.consumedBy.push(ruleId);
|
|
2094
|
+
}
|
|
2095
|
+
edges.push({ from: tag, to: ruleId, type: "consumes" });
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
return { facts, edges, producers, consumers };
|
|
2099
|
+
}
|
|
2100
|
+
function findDeadRules(registry, knownEventTypes) {
|
|
2101
|
+
const dead = [];
|
|
2102
|
+
const known = new Set(knownEventTypes);
|
|
2103
|
+
const rules = registry.getAllRules();
|
|
2104
|
+
for (const rule of rules) {
|
|
2105
|
+
if (!rule.eventTypes) continue;
|
|
2106
|
+
const required = Array.isArray(rule.eventTypes) ? rule.eventTypes : [rule.eventTypes];
|
|
2107
|
+
const hasMatch = required.some((t) => known.has(t));
|
|
2108
|
+
if (!hasMatch) {
|
|
2109
|
+
dead.push({
|
|
2110
|
+
ruleId: rule.id,
|
|
2111
|
+
description: rule.description,
|
|
2112
|
+
requiredEventTypes: required,
|
|
2113
|
+
reason: `Rule requires event types [${required.join(", ")}] but none are in the known event types [${knownEventTypes.join(", ")}]`
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
return dead;
|
|
2118
|
+
}
|
|
2119
|
+
function findUnreachableStates(registry) {
|
|
2120
|
+
const graph = analyzeDependencyGraph(registry);
|
|
2121
|
+
const unreachable = [];
|
|
2122
|
+
for (const [tag, node] of graph.facts) {
|
|
2123
|
+
if (node.producedBy.length === 0 && node.consumedBy.length > 0) {
|
|
2124
|
+
unreachable.push({
|
|
2125
|
+
factTags: [tag],
|
|
2126
|
+
reason: `Fact "${tag}" is consumed by rules [${node.consumedBy.join(", ")}] but never produced by any rule`
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
const allProducedTags = Array.from(graph.facts.keys()).filter(
|
|
2131
|
+
(tag) => graph.facts.get(tag).producedBy.length > 0
|
|
2132
|
+
);
|
|
2133
|
+
for (let i = 0; i < allProducedTags.length; i++) {
|
|
2134
|
+
for (let j = i + 1; j < allProducedTags.length; j++) {
|
|
2135
|
+
const tagA = allProducedTags[i];
|
|
2136
|
+
const tagB = allProducedTags[j];
|
|
2137
|
+
const producersA = graph.facts.get(tagA).producedBy;
|
|
2138
|
+
const producersB = graph.facts.get(tagB).producedBy;
|
|
2139
|
+
const sharedProducer = producersA.find((p) => producersB.includes(p));
|
|
2140
|
+
if (sharedProducer) continue;
|
|
2141
|
+
const rules = registry.getAllRules();
|
|
2142
|
+
const getEventTypes = (ruleId) => {
|
|
2143
|
+
const rule = rules.find((r) => r.id === ruleId);
|
|
2144
|
+
if (!rule?.eventTypes) return [];
|
|
2145
|
+
return Array.isArray(rule.eventTypes) ? rule.eventTypes : [rule.eventTypes];
|
|
2146
|
+
};
|
|
2147
|
+
const eventTypesA = new Set(producersA.flatMap(getEventTypes));
|
|
2148
|
+
const eventTypesB = new Set(producersB.flatMap(getEventTypes));
|
|
2149
|
+
if (eventTypesA.size > 0 && eventTypesB.size > 0 && ![...eventTypesA].some((t) => eventTypesB.has(t))) {
|
|
2150
|
+
const consumersA = graph.facts.get(tagA).consumedBy;
|
|
2151
|
+
const consumersB = graph.facts.get(tagB).consumedBy;
|
|
2152
|
+
const sharedConsumer = consumersA.find((c) => consumersB.includes(c));
|
|
2153
|
+
if (sharedConsumer) {
|
|
2154
|
+
unreachable.push({
|
|
2155
|
+
factTags: [tagA, tagB],
|
|
2156
|
+
reason: `Facts "${tagA}" and "${tagB}" are both consumed by rule "${sharedConsumer}" but are produced by rules with non-overlapping event types`
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
return unreachable;
|
|
2163
|
+
}
|
|
2164
|
+
function findShadowedRules(registry) {
|
|
2165
|
+
const shadowed = [];
|
|
2166
|
+
const graph = analyzeDependencyGraph(registry);
|
|
2167
|
+
const rules = registry.getAllRules();
|
|
2168
|
+
for (let i = 0; i < rules.length; i++) {
|
|
2169
|
+
for (let j = 0; j < rules.length; j++) {
|
|
2170
|
+
if (i === j) continue;
|
|
2171
|
+
const ruleA = rules[i];
|
|
2172
|
+
const ruleB = rules[j];
|
|
2173
|
+
const typesA = normalizeEventTypes(ruleA.eventTypes);
|
|
2174
|
+
const typesB = normalizeEventTypes(ruleB.eventTypes);
|
|
2175
|
+
if (typesA.length === 0 || typesB.length === 0) continue;
|
|
2176
|
+
const shared = typesA.filter((t) => typesB.includes(t));
|
|
2177
|
+
if (shared.length === 0) continue;
|
|
2178
|
+
const producedA = graph.producers.get(ruleA.id) ?? [];
|
|
2179
|
+
const producedB = graph.producers.get(ruleB.id) ?? [];
|
|
2180
|
+
if (producedA.length === 0) continue;
|
|
2181
|
+
const isSuperset = producedA.every((tag) => producedB.includes(tag));
|
|
2182
|
+
const isProperSuperset = isSuperset && producedB.length > producedA.length;
|
|
2183
|
+
if (isProperSuperset) {
|
|
2184
|
+
shadowed.push({
|
|
2185
|
+
ruleId: ruleA.id,
|
|
2186
|
+
shadowedBy: ruleB.id,
|
|
2187
|
+
sharedEventTypes: shared,
|
|
2188
|
+
reason: `Rule "${ruleB.id}" produces a superset of facts [${producedB.join(", ")}] compared to "${ruleA.id}" [${producedA.join(", ")}] for the same event types [${shared.join(", ")}]`
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
return shadowed;
|
|
2194
|
+
}
|
|
2195
|
+
function findContradictions(registry) {
|
|
2196
|
+
const contradictions = [];
|
|
2197
|
+
const graph = analyzeDependencyGraph(registry);
|
|
2198
|
+
const rules = registry.getAllRules();
|
|
2199
|
+
for (const [tag, node] of graph.facts) {
|
|
2200
|
+
if (node.producedBy.length < 2) continue;
|
|
2201
|
+
for (let i = 0; i < node.producedBy.length; i++) {
|
|
2202
|
+
for (let j = i + 1; j < node.producedBy.length; j++) {
|
|
2203
|
+
const ruleIdA = node.producedBy[i];
|
|
2204
|
+
const ruleIdB = node.producedBy[j];
|
|
2205
|
+
const ruleA = rules.find((r) => r.id === ruleIdA);
|
|
2206
|
+
const ruleB = rules.find((r) => r.id === ruleIdB);
|
|
2207
|
+
if (!ruleA || !ruleB) continue;
|
|
2208
|
+
const typesA = normalizeEventTypes(ruleA.eventTypes);
|
|
2209
|
+
const typesB = normalizeEventTypes(ruleB.eventTypes);
|
|
2210
|
+
const bothCatchAll = typesA.length === 0 && typesB.length === 0;
|
|
2211
|
+
const sharedTypes = typesA.filter((t) => typesB.includes(t));
|
|
2212
|
+
const hasOverlap = sharedTypes.length > 0;
|
|
2213
|
+
if (bothCatchAll || hasOverlap) {
|
|
2214
|
+
const conflictDetail = checkContractConflict(ruleA, ruleB, tag);
|
|
2215
|
+
if (conflictDetail || bothCatchAll || hasOverlap) {
|
|
2216
|
+
contradictions.push({
|
|
2217
|
+
ruleA: ruleIdA,
|
|
2218
|
+
ruleB: ruleIdB,
|
|
2219
|
+
conflictingTag: tag,
|
|
2220
|
+
reason: conflictDetail ?? `Rules "${ruleIdA}" and "${ruleIdB}" both produce fact "${tag}" and respond to ${bothCatchAll ? "all events" : `event types [${sharedTypes.join(", ")}]`}`
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
return contradictions;
|
|
2228
|
+
}
|
|
2229
|
+
function findGaps(registry, expectations) {
|
|
2230
|
+
const gaps = [];
|
|
2231
|
+
const rules = registry.getAllRules();
|
|
2232
|
+
const constraints = registry.getAllConstraints();
|
|
2233
|
+
for (const exp of expectations.expectations) {
|
|
2234
|
+
const nameLower = exp.name.toLowerCase();
|
|
2235
|
+
const nameParts = nameLower.split(/[-_./\s]+/);
|
|
2236
|
+
const related = rules.filter((r) => {
|
|
2237
|
+
const idLower = r.id.toLowerCase();
|
|
2238
|
+
const descLower = r.description.toLowerCase();
|
|
2239
|
+
const behaviorLower = r.contract?.behavior?.toLowerCase() ?? "";
|
|
2240
|
+
if (idLower.includes(nameLower) || nameLower.includes(idLower)) return true;
|
|
2241
|
+
if (descLower.includes(nameLower) || behaviorLower.includes(nameLower)) return true;
|
|
2242
|
+
const minParts = Math.min(2, nameParts.length);
|
|
2243
|
+
const matches = nameParts.filter(
|
|
2244
|
+
(part) => part.length > 2 && (idLower.includes(part) || descLower.includes(part))
|
|
2245
|
+
);
|
|
2246
|
+
return matches.length >= minParts;
|
|
2247
|
+
});
|
|
2248
|
+
const relatedConstraints = constraints.filter((c) => {
|
|
2249
|
+
const idLower = c.id.toLowerCase();
|
|
2250
|
+
const descLower = c.description.toLowerCase();
|
|
2251
|
+
return idLower.includes(nameLower) || nameLower.includes(idLower) || descLower.includes(nameLower);
|
|
2252
|
+
});
|
|
2253
|
+
if (related.length === 0 && relatedConstraints.length === 0) {
|
|
2254
|
+
gaps.push({
|
|
2255
|
+
expectationName: exp.name,
|
|
2256
|
+
description: `No rules or constraints found for expectation "${exp.name}"`,
|
|
2257
|
+
partialCoverage: [],
|
|
2258
|
+
type: "no-rule"
|
|
2259
|
+
});
|
|
2260
|
+
continue;
|
|
2261
|
+
}
|
|
2262
|
+
const uncoveredConditions = exp.conditions.filter((cond) => {
|
|
2263
|
+
const condLower = cond.description.toLowerCase();
|
|
2264
|
+
return !related.some(
|
|
2265
|
+
(r) => r.contract?.examples.some(
|
|
2266
|
+
(ex) => ex.given.toLowerCase().includes(condLower) || ex.when.toLowerCase().includes(condLower) || ex.then.toLowerCase().includes(condLower) || condLower.includes(ex.given.toLowerCase()) || condLower.includes(ex.when.toLowerCase())
|
|
2267
|
+
) || r.contract?.invariants.some(
|
|
2268
|
+
(inv) => inv.toLowerCase().includes(condLower) || condLower.includes(inv.toLowerCase())
|
|
2269
|
+
) || r.contract?.behavior.toLowerCase().includes(condLower) || r.description.toLowerCase().includes(condLower)
|
|
2270
|
+
);
|
|
2271
|
+
});
|
|
2272
|
+
if (uncoveredConditions.length > 0 && uncoveredConditions.length < exp.conditions.length) {
|
|
2273
|
+
gaps.push({
|
|
2274
|
+
expectationName: exp.name,
|
|
2275
|
+
description: `Expectation "${exp.name}" is partially covered. Uncovered conditions: ${uncoveredConditions.map((c) => c.description).join("; ")}`,
|
|
2276
|
+
partialCoverage: related.map((r) => r.id),
|
|
2277
|
+
type: "partial-coverage"
|
|
2278
|
+
});
|
|
2279
|
+
} else if (uncoveredConditions.length === exp.conditions.length && exp.conditions.length > 0) {
|
|
2280
|
+
const rulesWithoutContracts = related.filter((r) => !r.contract);
|
|
2281
|
+
if (rulesWithoutContracts.length > 0) {
|
|
2282
|
+
gaps.push({
|
|
2283
|
+
expectationName: exp.name,
|
|
2284
|
+
description: `Rules related to "${exp.name}" lack contracts: [${rulesWithoutContracts.map((r) => r.id).join(", ")}]`,
|
|
2285
|
+
partialCoverage: related.map((r) => r.id),
|
|
2286
|
+
type: "no-contract"
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return gaps;
|
|
2292
|
+
}
|
|
2293
|
+
function getOrCreateFactNode(facts, tag) {
|
|
2294
|
+
let node = facts.get(tag);
|
|
2295
|
+
if (!node) {
|
|
2296
|
+
node = { tag, producedBy: [], consumedBy: [] };
|
|
2297
|
+
facts.set(tag, node);
|
|
2298
|
+
}
|
|
2299
|
+
return node;
|
|
2300
|
+
}
|
|
2301
|
+
function normalizeEventTypes(eventTypes) {
|
|
2302
|
+
if (!eventTypes) return [];
|
|
2303
|
+
return Array.isArray(eventTypes) ? eventTypes : [eventTypes];
|
|
2304
|
+
}
|
|
2305
|
+
function probeRuleExecution(rule) {
|
|
2306
|
+
const produced = [];
|
|
2307
|
+
const consumed = [];
|
|
2308
|
+
const eventTypes = normalizeEventTypes(rule.eventTypes);
|
|
2309
|
+
const syntheticEvents = eventTypes.length > 0 ? eventTypes.map((tag) => ({ tag, payload: {} })) : [{ tag: "__probe__", payload: {} }];
|
|
2310
|
+
const syntheticState = {
|
|
2311
|
+
context: {},
|
|
2312
|
+
facts: [],
|
|
2313
|
+
events: syntheticEvents,
|
|
2314
|
+
meta: {},
|
|
2315
|
+
protocolVersion: "1.0.0"
|
|
2316
|
+
};
|
|
2317
|
+
try {
|
|
2318
|
+
const result = rule.impl(syntheticState, syntheticEvents);
|
|
2319
|
+
if (result instanceof RuleResult) {
|
|
2320
|
+
if (result.kind === "emit") {
|
|
2321
|
+
for (const fact2 of result.facts) {
|
|
2322
|
+
if (!produced.includes(fact2.tag)) {
|
|
2323
|
+
produced.push(fact2.tag);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
} else if (result.kind === "retract") {
|
|
2327
|
+
for (const tag of result.retractTags) {
|
|
2328
|
+
if (!consumed.includes(tag)) {
|
|
2329
|
+
consumed.push(tag);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
} else if (Array.isArray(result)) {
|
|
2334
|
+
for (const fact2 of result) {
|
|
2335
|
+
if (fact2.tag && !produced.includes(fact2.tag)) {
|
|
2336
|
+
produced.push(fact2.tag);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
} catch {
|
|
2341
|
+
}
|
|
2342
|
+
return { produced, consumed };
|
|
2343
|
+
}
|
|
2344
|
+
function extractFactTagsFromText(text) {
|
|
2345
|
+
const tags = [];
|
|
2346
|
+
const patterns = [
|
|
2347
|
+
/(?:emit|produce|retract|read|consume|fact)\s+['"]?([a-zA-Z][a-zA-Z0-9._-]+)['"]?/gi,
|
|
2348
|
+
/['"]([a-zA-Z][a-zA-Z0-9._-]*\.[a-zA-Z][a-zA-Z0-9._-]*)['"]?/g
|
|
2349
|
+
];
|
|
2350
|
+
for (const pattern of patterns) {
|
|
2351
|
+
let match;
|
|
2352
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
2353
|
+
const tag = match[1];
|
|
2354
|
+
if (tag && !tags.includes(tag)) {
|
|
2355
|
+
tags.push(tag);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
return tags;
|
|
2360
|
+
}
|
|
2361
|
+
function checkContractConflict(ruleA, ruleB, _factTag) {
|
|
2362
|
+
if (!ruleA.contract || !ruleB.contract) return null;
|
|
2363
|
+
for (const exA of ruleA.contract.examples) {
|
|
2364
|
+
for (const exB of ruleB.contract.examples) {
|
|
2365
|
+
const sameGiven = exA.given.toLowerCase() === exB.given.toLowerCase();
|
|
2366
|
+
const sameWhen = exA.when.toLowerCase() === exB.when.toLowerCase();
|
|
2367
|
+
const differentThen = exA.then.toLowerCase() !== exB.then.toLowerCase();
|
|
2368
|
+
if ((sameGiven || sameWhen) && differentThen) {
|
|
2369
|
+
return `Contract conflict: "${ruleA.id}" expects "${exA.then}" but "${ruleB.id}" expects "${exB.then}" under similar conditions`;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
return null;
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
// src/decision-ledger/derivation.ts
|
|
2377
|
+
function traceDerivation(factTag, _engine, registry) {
|
|
2378
|
+
const graph = analyzeDependencyGraph(registry);
|
|
2379
|
+
const steps = [];
|
|
2380
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2381
|
+
function walkBackward(tag, depth) {
|
|
2382
|
+
if (visited.has(tag) || depth > 20) return;
|
|
2383
|
+
visited.add(tag);
|
|
2384
|
+
const factNode = graph.facts.get(tag);
|
|
2385
|
+
if (!factNode) {
|
|
2386
|
+
steps.unshift({
|
|
2387
|
+
type: "fact-produced",
|
|
2388
|
+
id: tag,
|
|
2389
|
+
description: `Fact "${tag}" (origin unknown \u2014 not in dependency graph)`
|
|
2390
|
+
});
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
steps.unshift({
|
|
2394
|
+
type: "fact-produced",
|
|
2395
|
+
id: tag,
|
|
2396
|
+
description: `Fact "${tag}" produced`
|
|
2397
|
+
});
|
|
2398
|
+
for (const ruleId of factNode.producedBy) {
|
|
2399
|
+
if (visited.has(ruleId)) continue;
|
|
2400
|
+
visited.add(ruleId);
|
|
2401
|
+
const rule = registry.getRule(ruleId);
|
|
2402
|
+
steps.unshift({
|
|
2403
|
+
type: "rule-fired",
|
|
2404
|
+
id: ruleId,
|
|
2405
|
+
description: `Rule "${ruleId}" fired${rule ? `: ${rule.description}` : ""}`
|
|
2406
|
+
});
|
|
2407
|
+
const consumed = graph.consumers.get(ruleId) ?? [];
|
|
2408
|
+
for (const consumedTag of consumed) {
|
|
2409
|
+
steps.unshift({
|
|
2410
|
+
type: "fact-read",
|
|
2411
|
+
id: consumedTag,
|
|
2412
|
+
description: `Rule "${ruleId}" reads fact "${consumedTag}"`
|
|
2413
|
+
});
|
|
2414
|
+
walkBackward(consumedTag, depth + 1);
|
|
2415
|
+
}
|
|
2416
|
+
if (rule?.eventTypes) {
|
|
2417
|
+
const types = Array.isArray(rule.eventTypes) ? rule.eventTypes : [rule.eventTypes];
|
|
2418
|
+
for (const eventType of types) {
|
|
2419
|
+
steps.unshift({
|
|
2420
|
+
type: "event",
|
|
2421
|
+
id: eventType,
|
|
2422
|
+
description: `Event "${eventType}" triggers rule "${ruleId}"`
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
walkBackward(factTag, 0);
|
|
2429
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2430
|
+
const dedupedSteps = steps.filter((step) => {
|
|
2431
|
+
const key = `${step.type}:${step.id}`;
|
|
2432
|
+
if (seen.has(key)) return false;
|
|
2433
|
+
seen.add(key);
|
|
2434
|
+
return true;
|
|
2435
|
+
});
|
|
2436
|
+
return {
|
|
2437
|
+
targetFact: factTag,
|
|
2438
|
+
steps: dedupedSteps,
|
|
2439
|
+
depth: dedupedSteps.filter((s) => s.type === "rule-fired").length
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
function traceImpact(factTag, registry) {
|
|
2443
|
+
const graph = analyzeDependencyGraph(registry);
|
|
2444
|
+
const affectedRules = [];
|
|
2445
|
+
const affectedFacts = [];
|
|
2446
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2447
|
+
function walkForward(tag, depth2) {
|
|
2448
|
+
if (visited.has(tag) || depth2 > 20) return depth2;
|
|
2449
|
+
visited.add(tag);
|
|
2450
|
+
const factNode = graph.facts.get(tag);
|
|
2451
|
+
if (!factNode) return depth2;
|
|
2452
|
+
let maxDepth = depth2;
|
|
2453
|
+
for (const ruleId of factNode.consumedBy) {
|
|
2454
|
+
if (!affectedRules.includes(ruleId)) {
|
|
2455
|
+
affectedRules.push(ruleId);
|
|
2456
|
+
}
|
|
2457
|
+
const produced = graph.producers.get(ruleId) ?? [];
|
|
2458
|
+
for (const producedTag of produced) {
|
|
2459
|
+
if (producedTag !== factTag && !affectedFacts.includes(producedTag)) {
|
|
2460
|
+
affectedFacts.push(producedTag);
|
|
2461
|
+
const childDepth = walkForward(producedTag, depth2 + 1);
|
|
2462
|
+
if (childDepth > maxDepth) maxDepth = childDepth;
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
return maxDepth;
|
|
2467
|
+
}
|
|
2468
|
+
const depth = walkForward(factTag, 0);
|
|
2469
|
+
return {
|
|
2470
|
+
factTag,
|
|
2471
|
+
affectedRules,
|
|
2472
|
+
affectedFacts,
|
|
2473
|
+
depth
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// src/decision-ledger/contract-verification.ts
|
|
2478
|
+
init_rule_result();
|
|
2479
|
+
function verifyContractExamples(rule, contract) {
|
|
2480
|
+
const examples = [];
|
|
2481
|
+
for (let i = 0; i < contract.examples.length; i++) {
|
|
2482
|
+
const example = contract.examples[i];
|
|
2483
|
+
try {
|
|
2484
|
+
const state2 = buildStateFromDescription(example.given);
|
|
2485
|
+
const events = buildEventsFromDescription(example.when, rule.eventTypes);
|
|
2486
|
+
const stateWithEvents = { ...state2, events };
|
|
2487
|
+
const result = rule.impl(stateWithEvents, events);
|
|
2488
|
+
const verification = verifyOutput(result, example.then, rule.id);
|
|
2489
|
+
examples.push({
|
|
2490
|
+
index: i,
|
|
2491
|
+
given: example.given,
|
|
2492
|
+
when: example.when,
|
|
2493
|
+
expectedThen: example.then,
|
|
2494
|
+
passed: verification.passed,
|
|
2495
|
+
actualOutput: verification.actualOutput,
|
|
2496
|
+
error: verification.error
|
|
2497
|
+
});
|
|
2498
|
+
} catch (error) {
|
|
2499
|
+
examples.push({
|
|
2500
|
+
index: i,
|
|
2501
|
+
given: example.given,
|
|
2502
|
+
when: example.when,
|
|
2503
|
+
expectedThen: example.then,
|
|
2504
|
+
passed: false,
|
|
2505
|
+
error: `Execution error: ${error instanceof Error ? error.message : String(error)}`
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
const passCount = examples.filter((e) => e.passed).length;
|
|
2510
|
+
const failCount = examples.filter((e) => !e.passed).length;
|
|
2511
|
+
return {
|
|
2512
|
+
ruleId: rule.id,
|
|
2513
|
+
examples,
|
|
2514
|
+
allPassed: failCount === 0,
|
|
2515
|
+
passCount,
|
|
2516
|
+
failCount
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
function verifyInvariants(registry) {
|
|
2520
|
+
const checks = [];
|
|
2521
|
+
const rules = registry.getAllRules();
|
|
2522
|
+
for (const rule of rules) {
|
|
2523
|
+
if (!rule.contract) continue;
|
|
2524
|
+
for (const invariant of rule.contract.invariants) {
|
|
2525
|
+
const consistent = rule.contract.examples.every((example) => {
|
|
2526
|
+
return isConsistentWithInvariant(example, invariant);
|
|
2527
|
+
});
|
|
2528
|
+
checks.push({
|
|
2529
|
+
invariant,
|
|
2530
|
+
ruleId: rule.id,
|
|
2531
|
+
holds: consistent,
|
|
2532
|
+
explanation: consistent ? `Invariant "${invariant}" is consistent with all ${rule.contract.examples.length} examples` : `Invariant "${invariant}" may be violated by one or more examples`
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
return checks;
|
|
2537
|
+
}
|
|
2538
|
+
function findContractGaps(registry) {
|
|
2539
|
+
const gaps = [];
|
|
2540
|
+
const rules = registry.getAllRules();
|
|
2541
|
+
for (const rule of rules) {
|
|
2542
|
+
if (!rule.contract) continue;
|
|
2543
|
+
const examples = rule.contract.examples;
|
|
2544
|
+
const hasErrorExamples = examples.some(
|
|
2545
|
+
(ex) => ex.then.toLowerCase().includes("error") || ex.then.toLowerCase().includes("fail") || ex.then.toLowerCase().includes("skip") || ex.then.toLowerCase().includes("noop") || ex.then.toLowerCase().includes("retract") || ex.then.toLowerCase().includes("violation")
|
|
2546
|
+
);
|
|
2547
|
+
if (!hasErrorExamples && examples.length > 0) {
|
|
2548
|
+
gaps.push({
|
|
2549
|
+
ruleId: rule.id,
|
|
2550
|
+
description: `No error/failure path examples. All ${examples.length} examples show happy paths`,
|
|
2551
|
+
type: "missing-error-path"
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
if (rule.eventTypes) {
|
|
2555
|
+
const eventTypes = Array.isArray(rule.eventTypes) ? rule.eventTypes : [rule.eventTypes];
|
|
2556
|
+
if (eventTypes.length > 1) {
|
|
2557
|
+
const coveredTypes = /* @__PURE__ */ new Set();
|
|
2558
|
+
for (const ex of examples) {
|
|
2559
|
+
for (const et of eventTypes) {
|
|
2560
|
+
if (ex.when.toLowerCase().includes(et.toLowerCase()) || ex.when.toLowerCase().includes(et.replace(".", " ").toLowerCase())) {
|
|
2561
|
+
coveredTypes.add(et);
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
const uncovered = eventTypes.filter((et) => !coveredTypes.has(et));
|
|
2566
|
+
if (uncovered.length > 0) {
|
|
2567
|
+
gaps.push({
|
|
2568
|
+
ruleId: rule.id,
|
|
2569
|
+
description: `Event types [${uncovered.join(", ")}] not covered by any example`,
|
|
2570
|
+
type: "missing-edge-case"
|
|
2571
|
+
});
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
if (examples.length === 1) {
|
|
2576
|
+
gaps.push({
|
|
2577
|
+
ruleId: rule.id,
|
|
2578
|
+
description: `Only 1 contract example \u2014 likely missing boundary conditions and edge cases`,
|
|
2579
|
+
type: "missing-boundary"
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
return gaps;
|
|
2584
|
+
}
|
|
2585
|
+
function crossReferenceContracts(registry) {
|
|
2586
|
+
const graph = analyzeDependencyGraph(registry);
|
|
2587
|
+
const references = [];
|
|
2588
|
+
const rules = registry.getAllRules();
|
|
2589
|
+
for (const rule of rules) {
|
|
2590
|
+
if (!rule.contract) continue;
|
|
2591
|
+
for (const example of rule.contract.examples) {
|
|
2592
|
+
const referencedTags = extractReferencedFactTags(example.given + " " + example.when);
|
|
2593
|
+
for (const tag of referencedTags) {
|
|
2594
|
+
const factNode = graph.facts.get(tag);
|
|
2595
|
+
const producerRuleId = factNode?.producedBy[0] ?? null;
|
|
2596
|
+
const valid = factNode ? factNode.producedBy.length > 0 : false;
|
|
2597
|
+
if (producerRuleId === rule.id) continue;
|
|
2598
|
+
references.push({
|
|
2599
|
+
sourceRuleId: rule.id,
|
|
2600
|
+
referencedFactTag: tag,
|
|
2601
|
+
producerRuleId,
|
|
2602
|
+
valid
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
return references;
|
|
2608
|
+
}
|
|
2609
|
+
function buildStateFromDescription(given) {
|
|
2610
|
+
const context = {};
|
|
2611
|
+
const facts = [];
|
|
2612
|
+
const factPattern = /fact\s+['"]([^'"]+)['"]/gi;
|
|
2613
|
+
let match;
|
|
2614
|
+
while ((match = factPattern.exec(given)) !== null) {
|
|
2615
|
+
facts.push({ tag: match[1], payload: {} });
|
|
2616
|
+
}
|
|
2617
|
+
const dottedPattern = /['"]([a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9.]*)['"]/g;
|
|
2618
|
+
while ((match = dottedPattern.exec(given)) !== null) {
|
|
2619
|
+
const tag = match[1];
|
|
2620
|
+
if (!facts.some((f) => f.tag === tag)) {
|
|
2621
|
+
facts.push({ tag, payload: {} });
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
return {
|
|
2625
|
+
context,
|
|
2626
|
+
facts,
|
|
2627
|
+
meta: {},
|
|
2628
|
+
protocolVersion: "1.0.0"
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
function buildEventsFromDescription(when, eventTypes) {
|
|
2632
|
+
const events = [];
|
|
2633
|
+
if (eventTypes) {
|
|
2634
|
+
const types = Array.isArray(eventTypes) ? eventTypes : [eventTypes];
|
|
2635
|
+
for (const type of types) {
|
|
2636
|
+
if (when.toLowerCase().includes(type.toLowerCase()) || when.toLowerCase().includes(type.replace(".", " ").toLowerCase())) {
|
|
2637
|
+
events.push({ tag: type, payload: {} });
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
if (events.length === 0) {
|
|
2642
|
+
const eventPattern = /['"]([A-Z][A-Z0-9._-]+)['"]/g;
|
|
2643
|
+
let match;
|
|
2644
|
+
while ((match = eventPattern.exec(when)) !== null) {
|
|
2645
|
+
events.push({ tag: match[1], payload: {} });
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
if (events.length === 0) {
|
|
2649
|
+
const types = eventTypes ? Array.isArray(eventTypes) ? eventTypes : [eventTypes] : ["__test__"];
|
|
2650
|
+
events.push({ tag: types[0], payload: {} });
|
|
2651
|
+
}
|
|
2652
|
+
return events;
|
|
2653
|
+
}
|
|
2654
|
+
function verifyOutput(result, expectedThen, ruleId) {
|
|
2655
|
+
const thenLower = expectedThen.toLowerCase();
|
|
2656
|
+
if (result instanceof RuleResult) {
|
|
2657
|
+
switch (result.kind) {
|
|
2658
|
+
case "emit": {
|
|
2659
|
+
const factTags = result.facts.map((f) => f.tag);
|
|
2660
|
+
const actualOutput = `Emitted: [${factTags.join(", ")}]`;
|
|
2661
|
+
const emitExpected = thenLower.includes("emit") || thenLower.includes("produce") || thenLower.includes("fact");
|
|
2662
|
+
if (emitExpected) {
|
|
2663
|
+
const expectedTags = extractReferencedFactTags(expectedThen);
|
|
2664
|
+
if (expectedTags.length > 0) {
|
|
2665
|
+
const allFound = expectedTags.every(
|
|
2666
|
+
(tag) => factTags.some((ft) => ft.toLowerCase() === tag.toLowerCase())
|
|
2667
|
+
);
|
|
2668
|
+
return { passed: allFound, actualOutput };
|
|
2669
|
+
}
|
|
2670
|
+
return { passed: true, actualOutput };
|
|
2671
|
+
}
|
|
2672
|
+
return { passed: true, actualOutput };
|
|
2673
|
+
}
|
|
2674
|
+
case "noop": {
|
|
2675
|
+
const expectNoop = thenLower.includes("noop") || thenLower.includes("nothing") || thenLower.includes("no fact") || thenLower.includes("skip");
|
|
2676
|
+
return {
|
|
2677
|
+
passed: expectNoop,
|
|
2678
|
+
actualOutput: `Noop: ${result.reason ?? "no reason"}`
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
case "skip": {
|
|
2682
|
+
const expectSkip = thenLower.includes("skip") || thenLower.includes("noop") || thenLower.includes("not fire") || thenLower.includes("nothing");
|
|
2683
|
+
return {
|
|
2684
|
+
passed: expectSkip,
|
|
2685
|
+
actualOutput: `Skip: ${result.reason ?? "no reason"}`
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
case "retract": {
|
|
2689
|
+
const expectRetract = thenLower.includes("retract") || thenLower.includes("remove") || thenLower.includes("clear");
|
|
2690
|
+
return {
|
|
2691
|
+
passed: expectRetract,
|
|
2692
|
+
actualOutput: `Retract: [${result.retractTags.join(", ")}]`
|
|
2693
|
+
};
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
} else if (Array.isArray(result)) {
|
|
2697
|
+
const factTags = result.map((f) => f.tag);
|
|
2698
|
+
const actualOutput = `Facts: [${factTags.join(", ")}]`;
|
|
2699
|
+
return { passed: factTags.length > 0 || thenLower.includes("nothing"), actualOutput };
|
|
2700
|
+
}
|
|
2701
|
+
return { passed: false, error: `Unexpected result type from rule "${ruleId}"` };
|
|
2702
|
+
}
|
|
2703
|
+
function isConsistentWithInvariant(example, invariant) {
|
|
2704
|
+
const invLower = invariant.toLowerCase();
|
|
2705
|
+
const thenLower = example.then.toLowerCase();
|
|
2706
|
+
if (invLower.includes("must not") || invLower.includes("never")) {
|
|
2707
|
+
const keyword = invLower.replace(/must not|never|should not/g, "").trim().split(/\s+/)[0];
|
|
2708
|
+
if (keyword && thenLower.includes(keyword)) {
|
|
2709
|
+
return false;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
if (invLower.includes("must ") && !invLower.includes("must not")) {
|
|
2713
|
+
const mustPart = invLower.split("must ")[1]?.split(/[.,;]/)[0]?.trim();
|
|
2714
|
+
if (mustPart) {
|
|
2715
|
+
const negations = ["no ", "not ", "never ", "without "];
|
|
2716
|
+
for (const neg of negations) {
|
|
2717
|
+
if (thenLower.includes(neg + mustPart.split(/\s+/)[0])) {
|
|
2718
|
+
return false;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
return true;
|
|
2724
|
+
}
|
|
2725
|
+
function extractReferencedFactTags(text) {
|
|
2726
|
+
const tags = [];
|
|
2727
|
+
const patterns = [
|
|
2728
|
+
/['"]([a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9.]*)['"]/g,
|
|
2729
|
+
/fact\s+['"]?([a-zA-Z][a-zA-Z0-9._-]+)['"]?/gi,
|
|
2730
|
+
/(?:emit|produce|retract)\s+['"]?([a-zA-Z][a-zA-Z0-9._-]+)['"]?/gi
|
|
2731
|
+
];
|
|
2732
|
+
for (const pattern of patterns) {
|
|
2733
|
+
let match;
|
|
2734
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
2735
|
+
const tag = match[1];
|
|
2736
|
+
if (tag && !tags.includes(tag)) {
|
|
2737
|
+
tags.push(tag);
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
return tags;
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
// src/decision-ledger/suggestions.ts
|
|
2745
|
+
function suggest(finding, type) {
|
|
2746
|
+
switch (type) {
|
|
2747
|
+
case "dead-rule":
|
|
2748
|
+
return suggestForDeadRule(finding);
|
|
2749
|
+
case "gap":
|
|
2750
|
+
return suggestForGap(finding);
|
|
2751
|
+
case "contradiction":
|
|
2752
|
+
return suggestForContradiction(finding);
|
|
2753
|
+
case "unreachable-state":
|
|
2754
|
+
return suggestForUnreachableState(finding);
|
|
2755
|
+
case "shadowed-rule":
|
|
2756
|
+
return suggestForShadowedRule(finding);
|
|
2757
|
+
case "contract-gap":
|
|
2758
|
+
return suggestForContractGap(finding);
|
|
2759
|
+
default:
|
|
2760
|
+
return {
|
|
2761
|
+
findingType: type,
|
|
2762
|
+
entityId: "unknown",
|
|
2763
|
+
message: "Unknown finding type",
|
|
2764
|
+
action: "modify",
|
|
2765
|
+
priority: 1
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
function suggestAll(findings) {
|
|
2770
|
+
const suggestions = [];
|
|
2771
|
+
if (findings.deadRules) {
|
|
2772
|
+
for (const f of findings.deadRules) {
|
|
2773
|
+
suggestions.push(suggestForDeadRule(f));
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
if (findings.gaps) {
|
|
2777
|
+
for (const f of findings.gaps) {
|
|
2778
|
+
suggestions.push(suggestForGap(f));
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
if (findings.contradictions) {
|
|
2782
|
+
for (const f of findings.contradictions) {
|
|
2783
|
+
suggestions.push(suggestForContradiction(f));
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
if (findings.unreachableStates) {
|
|
2787
|
+
for (const f of findings.unreachableStates) {
|
|
2788
|
+
suggestions.push(suggestForUnreachableState(f));
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
if (findings.shadowedRules) {
|
|
2792
|
+
for (const f of findings.shadowedRules) {
|
|
2793
|
+
suggestions.push(suggestForShadowedRule(f));
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
if (findings.contractGaps) {
|
|
2797
|
+
for (const f of findings.contractGaps) {
|
|
2798
|
+
suggestions.push(suggestForContractGap(f));
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
suggestions.sort((a, b) => b.priority - a.priority);
|
|
2802
|
+
return suggestions;
|
|
2803
|
+
}
|
|
2804
|
+
function suggestForDeadRule(finding) {
|
|
2805
|
+
const eventList = finding.requiredEventTypes.join(", ");
|
|
2806
|
+
return {
|
|
2807
|
+
findingType: "dead-rule",
|
|
2808
|
+
entityId: finding.ruleId,
|
|
2809
|
+
message: `Remove rule "${finding.ruleId}" or add event type "${finding.requiredEventTypes[0]}" to your event sources. Rule requires [${eventList}] but none are emitted by the application.`,
|
|
2810
|
+
action: finding.requiredEventTypes.length === 1 ? "add-event-type" : "remove",
|
|
2811
|
+
priority: 5,
|
|
2812
|
+
skeleton: `// Option 1: Remove the dead rule
|
|
2813
|
+
registry.removeRule('${finding.ruleId}');
|
|
2814
|
+
|
|
2815
|
+
// Option 2: Add the missing event type
|
|
2816
|
+
engine.step([{ tag: '${finding.requiredEventTypes[0]}', payload: {} }]);`
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
function suggestForGap(finding) {
|
|
2820
|
+
const ruleId = finding.expectationName.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
2821
|
+
switch (finding.type) {
|
|
2822
|
+
case "no-rule":
|
|
2823
|
+
return {
|
|
2824
|
+
findingType: "gap",
|
|
2825
|
+
entityId: finding.expectationName,
|
|
2826
|
+
message: `Add a rule covering: "${finding.expectationName}". No rules or constraints address this expected behavior.`,
|
|
2827
|
+
action: "add-rule",
|
|
2828
|
+
priority: 8,
|
|
2829
|
+
skeleton: `defineRule({
|
|
2830
|
+
id: '${ruleId}',
|
|
2831
|
+
description: '${finding.description}',
|
|
2832
|
+
eventTypes: ['TODO_EVENT_TYPE'],
|
|
2833
|
+
impl: (state, events) => {
|
|
2834
|
+
// TODO: Implement logic for "${finding.expectationName}"
|
|
2835
|
+
return RuleResult.noop('Not implemented');
|
|
2836
|
+
},
|
|
2837
|
+
contract: defineContract({
|
|
2838
|
+
ruleId: '${ruleId}',
|
|
2839
|
+
behavior: '${finding.expectationName}',
|
|
2840
|
+
examples: [
|
|
2841
|
+
{ given: 'TODO', when: 'TODO', then: 'TODO' },
|
|
2842
|
+
],
|
|
2843
|
+
invariants: [],
|
|
2844
|
+
}),
|
|
2845
|
+
});`
|
|
2846
|
+
};
|
|
2847
|
+
case "partial-coverage":
|
|
2848
|
+
return {
|
|
2849
|
+
findingType: "gap",
|
|
2850
|
+
entityId: finding.expectationName,
|
|
2851
|
+
message: `Expectation "${finding.expectationName}" is partially covered by rules [${finding.partialCoverage.join(", ")}]. Add contract examples or additional rules for uncovered conditions.`,
|
|
2852
|
+
action: "modify",
|
|
2853
|
+
priority: 6
|
|
2854
|
+
};
|
|
2855
|
+
case "no-contract":
|
|
2856
|
+
return {
|
|
2857
|
+
findingType: "gap",
|
|
2858
|
+
entityId: finding.expectationName,
|
|
2859
|
+
message: `Rules related to "${finding.expectationName}" ([${finding.partialCoverage.join(", ")}]) lack contracts. Add contracts with examples covering the expected behavior.`,
|
|
2860
|
+
action: "add-contract",
|
|
2861
|
+
priority: 7
|
|
2862
|
+
};
|
|
2863
|
+
default:
|
|
2864
|
+
return {
|
|
2865
|
+
findingType: "gap",
|
|
2866
|
+
entityId: finding.expectationName,
|
|
2867
|
+
message: finding.description,
|
|
2868
|
+
action: "add-rule",
|
|
2869
|
+
priority: 5
|
|
2870
|
+
};
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
function suggestForContradiction(finding) {
|
|
2874
|
+
return {
|
|
2875
|
+
findingType: "contradiction",
|
|
2876
|
+
entityId: `${finding.ruleA}\u2194${finding.ruleB}`,
|
|
2877
|
+
message: `Rules "${finding.ruleA}" and "${finding.ruleB}" both produce fact "${finding.conflictingTag}" with potentially different payloads. Add priority ordering, merge the rules, or add distinguishing conditions.`,
|
|
2878
|
+
action: "add-priority",
|
|
2879
|
+
priority: 9,
|
|
2880
|
+
skeleton: `// Option 1: Add priority via meta
|
|
2881
|
+
defineRule({
|
|
2882
|
+
id: '${finding.ruleA}',
|
|
2883
|
+
meta: { priority: 10 }, // Higher priority wins
|
|
2884
|
+
// ...
|
|
2885
|
+
});
|
|
2886
|
+
|
|
2887
|
+
// Option 2: Add distinguishing conditions
|
|
2888
|
+
defineRule({
|
|
2889
|
+
id: '${finding.ruleA}',
|
|
2890
|
+
impl: (state, events) => {
|
|
2891
|
+
// Add condition to distinguish from "${finding.ruleB}"
|
|
2892
|
+
if (/* specific condition */) {
|
|
2893
|
+
return RuleResult.emit([{ tag: '${finding.conflictingTag}', payload: { /* ... */ } }]);
|
|
2894
|
+
}
|
|
2895
|
+
return RuleResult.skip('Deferred to ${finding.ruleB}');
|
|
2896
|
+
},
|
|
2897
|
+
});`
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
function suggestForUnreachableState(finding) {
|
|
2901
|
+
const tags = finding.factTags.join(", ");
|
|
2902
|
+
return {
|
|
2903
|
+
findingType: "unreachable-state",
|
|
2904
|
+
entityId: finding.factTags.join("+"),
|
|
2905
|
+
message: `No rule produces facts [${tags}] together. If this state combination is valid, add a composite rule that produces all required facts.`,
|
|
2906
|
+
action: "add-rule",
|
|
2907
|
+
priority: 4,
|
|
2908
|
+
skeleton: `defineRule({
|
|
2909
|
+
id: 'composite-${finding.factTags[0]?.replace(".", "-") ?? "unknown"}',
|
|
2910
|
+
description: 'Produces facts [${tags}] together',
|
|
2911
|
+
impl: (state, events) => {
|
|
2912
|
+
return RuleResult.emit([
|
|
2913
|
+
${finding.factTags.map((t) => ` { tag: '${t}', payload: {} },`).join("\n")}
|
|
2914
|
+
]);
|
|
2915
|
+
},
|
|
2916
|
+
});`
|
|
2917
|
+
};
|
|
2918
|
+
}
|
|
2919
|
+
function suggestForShadowedRule(finding) {
|
|
2920
|
+
return {
|
|
2921
|
+
findingType: "shadowed-rule",
|
|
2922
|
+
entityId: finding.ruleId,
|
|
2923
|
+
message: `Rule "${finding.ruleId}" is always superseded by "${finding.shadowedBy}" for event types [${finding.sharedEventTypes.join(", ")}]. Consider merging the rules or adding a distinguishing condition to "${finding.ruleId}".`,
|
|
2924
|
+
action: "merge",
|
|
2925
|
+
priority: 3,
|
|
2926
|
+
skeleton: `// Option 1: Remove the shadowed rule
|
|
2927
|
+
registry.removeRule('${finding.ruleId}');
|
|
2928
|
+
|
|
2929
|
+
// Option 2: Add unique behavior to the shadowed rule
|
|
2930
|
+
defineRule({
|
|
2931
|
+
id: '${finding.ruleId}',
|
|
2932
|
+
impl: (state, events) => {
|
|
2933
|
+
// Add condition that "${finding.shadowedBy}" doesn't cover
|
|
2934
|
+
if (/* unique condition */) {
|
|
2935
|
+
return RuleResult.emit([/* unique facts */]);
|
|
2936
|
+
}
|
|
2937
|
+
return RuleResult.skip('Handled by ${finding.shadowedBy}');
|
|
2938
|
+
},
|
|
2939
|
+
});`
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2942
|
+
function suggestForContractGap(finding) {
|
|
2943
|
+
let message;
|
|
2944
|
+
let action = "add-contract";
|
|
2945
|
+
let priority;
|
|
2946
|
+
switch (finding.type) {
|
|
2947
|
+
case "missing-error-path":
|
|
2948
|
+
message = `Rule "${finding.ruleId}" has no error/failure examples in its contract. Add examples showing what happens when preconditions fail, inputs are invalid, or the rule needs to skip.`;
|
|
2949
|
+
priority = 6;
|
|
2950
|
+
break;
|
|
2951
|
+
case "missing-edge-case":
|
|
2952
|
+
message = `Rule "${finding.ruleId}": ${finding.description}. Add contract examples covering all declared event types.`;
|
|
2953
|
+
priority = 5;
|
|
2954
|
+
break;
|
|
2955
|
+
case "missing-boundary":
|
|
2956
|
+
message = `Rule "${finding.ruleId}" has only 1 contract example. Add boundary condition examples (empty input, maximum values, concurrent events).`;
|
|
2957
|
+
priority = 4;
|
|
2958
|
+
break;
|
|
2959
|
+
case "cross-reference-broken":
|
|
2960
|
+
message = `Rule "${finding.ruleId}": ${finding.description}. Verify the referenced fact producer exists.`;
|
|
2961
|
+
action = "modify";
|
|
2962
|
+
priority = 7;
|
|
2963
|
+
break;
|
|
2964
|
+
default:
|
|
2965
|
+
message = finding.description;
|
|
2966
|
+
priority = 3;
|
|
2967
|
+
}
|
|
2968
|
+
return {
|
|
2969
|
+
findingType: "contract-gap",
|
|
2970
|
+
entityId: finding.ruleId,
|
|
2971
|
+
message,
|
|
2972
|
+
action,
|
|
2973
|
+
priority
|
|
2974
|
+
};
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
// src/decision-ledger/report.ts
|
|
2978
|
+
function generateLedger(registry, engine, expectations) {
|
|
2979
|
+
const allEventTypes = /* @__PURE__ */ new Set();
|
|
2980
|
+
for (const rule of registry.getAllRules()) {
|
|
2981
|
+
if (rule.eventTypes) {
|
|
2982
|
+
const types = Array.isArray(rule.eventTypes) ? rule.eventTypes : [rule.eventTypes];
|
|
2983
|
+
for (const t of types) allEventTypes.add(t);
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
const deadRules = findDeadRules(registry, [...allEventTypes]);
|
|
2987
|
+
const unreachableStates = findUnreachableStates(registry);
|
|
2988
|
+
const shadowedRules = findShadowedRules(registry);
|
|
2989
|
+
const contradictions = findContradictions(registry);
|
|
2990
|
+
const contractGaps = findContractGaps(registry);
|
|
2991
|
+
const gaps = expectations ? findGaps(registry, expectations) : [];
|
|
2992
|
+
const currentFacts = engine.getFacts();
|
|
2993
|
+
const factDerivationChains = currentFacts.map(
|
|
2994
|
+
(fact2) => traceDerivation(fact2.tag, engine, registry)
|
|
2995
|
+
);
|
|
2996
|
+
const suggestions = suggestAll({
|
|
2997
|
+
deadRules,
|
|
2998
|
+
gaps,
|
|
2999
|
+
contradictions,
|
|
3000
|
+
unreachableStates,
|
|
3001
|
+
shadowedRules,
|
|
3002
|
+
contractGaps
|
|
3003
|
+
});
|
|
3004
|
+
const totalRules = registry.getRuleIds().length;
|
|
3005
|
+
const totalConstraints = registry.getConstraintIds().length;
|
|
3006
|
+
const totalIssues = deadRules.length + unreachableStates.length + shadowedRules.length + contradictions.length + gaps.length;
|
|
3007
|
+
const maxIssues = Math.max(totalRules + totalConstraints, 1);
|
|
3008
|
+
const healthScore = Math.max(0, Math.round(100 - totalIssues / maxIssues * 100));
|
|
3009
|
+
return {
|
|
3010
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3011
|
+
factDerivationChains,
|
|
3012
|
+
deadRules,
|
|
3013
|
+
unreachableStates,
|
|
3014
|
+
shadowedRules,
|
|
3015
|
+
contradictions,
|
|
3016
|
+
gaps,
|
|
3017
|
+
suggestions,
|
|
3018
|
+
summary: {
|
|
3019
|
+
totalRules,
|
|
3020
|
+
totalConstraints,
|
|
3021
|
+
deadRuleCount: deadRules.length,
|
|
3022
|
+
unreachableStateCount: unreachableStates.length,
|
|
3023
|
+
shadowedRuleCount: shadowedRules.length,
|
|
3024
|
+
contradictionCount: contradictions.length,
|
|
3025
|
+
gapCount: gaps.length,
|
|
3026
|
+
suggestionCount: suggestions.length,
|
|
3027
|
+
healthScore
|
|
3028
|
+
}
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
function formatLedger(report) {
|
|
3032
|
+
const lines = [];
|
|
3033
|
+
const icon = report.summary.healthScore >= 90 ? "\u2705" : report.summary.healthScore >= 70 ? "\u{1F7E1}" : "\u{1F534}";
|
|
3034
|
+
lines.push(`# ${icon} Decision Ledger Analysis`);
|
|
3035
|
+
lines.push("");
|
|
3036
|
+
lines.push(`**Health Score:** ${report.summary.healthScore}/100`);
|
|
3037
|
+
lines.push(`**Timestamp:** ${report.timestamp}`);
|
|
3038
|
+
lines.push("");
|
|
3039
|
+
lines.push("## Summary");
|
|
3040
|
+
lines.push("");
|
|
3041
|
+
lines.push(`| Metric | Count |`);
|
|
3042
|
+
lines.push(`|--------|-------|`);
|
|
3043
|
+
lines.push(`| Rules | ${report.summary.totalRules} |`);
|
|
3044
|
+
lines.push(`| Constraints | ${report.summary.totalConstraints} |`);
|
|
3045
|
+
lines.push(`| Dead Rules | ${report.summary.deadRuleCount} |`);
|
|
3046
|
+
lines.push(`| Unreachable States | ${report.summary.unreachableStateCount} |`);
|
|
3047
|
+
lines.push(`| Shadowed Rules | ${report.summary.shadowedRuleCount} |`);
|
|
3048
|
+
lines.push(`| Contradictions | ${report.summary.contradictionCount} |`);
|
|
3049
|
+
lines.push(`| Gaps | ${report.summary.gapCount} |`);
|
|
3050
|
+
lines.push(`| Suggestions | ${report.summary.suggestionCount} |`);
|
|
3051
|
+
lines.push("");
|
|
3052
|
+
if (report.deadRules.length > 0) {
|
|
3053
|
+
lines.push("## \u{1F480} Dead Rules");
|
|
3054
|
+
lines.push("");
|
|
3055
|
+
for (const dr of report.deadRules) {
|
|
3056
|
+
lines.push(`- **${dr.ruleId}**: ${dr.reason}`);
|
|
3057
|
+
}
|
|
3058
|
+
lines.push("");
|
|
3059
|
+
}
|
|
3060
|
+
if (report.unreachableStates.length > 0) {
|
|
3061
|
+
lines.push("## \u{1F6AB} Unreachable States");
|
|
3062
|
+
lines.push("");
|
|
3063
|
+
for (const us of report.unreachableStates) {
|
|
3064
|
+
lines.push(`- **[${us.factTags.join(", ")}]**: ${us.reason}`);
|
|
3065
|
+
}
|
|
3066
|
+
lines.push("");
|
|
3067
|
+
}
|
|
3068
|
+
if (report.shadowedRules.length > 0) {
|
|
3069
|
+
lines.push("## \u{1F47B} Shadowed Rules");
|
|
3070
|
+
lines.push("");
|
|
3071
|
+
for (const sr of report.shadowedRules) {
|
|
3072
|
+
lines.push(`- **${sr.ruleId}** shadowed by **${sr.shadowedBy}**: ${sr.reason}`);
|
|
3073
|
+
}
|
|
3074
|
+
lines.push("");
|
|
3075
|
+
}
|
|
3076
|
+
if (report.contradictions.length > 0) {
|
|
3077
|
+
lines.push("## \u26A1 Contradictions");
|
|
3078
|
+
lines.push("");
|
|
3079
|
+
for (const c of report.contradictions) {
|
|
3080
|
+
lines.push(`- **${c.ruleA}** \u2194 **${c.ruleB}** on \`${c.conflictingTag}\`: ${c.reason}`);
|
|
3081
|
+
}
|
|
3082
|
+
lines.push("");
|
|
3083
|
+
}
|
|
3084
|
+
if (report.gaps.length > 0) {
|
|
3085
|
+
lines.push("## \u{1F573}\uFE0F Gaps");
|
|
3086
|
+
lines.push("");
|
|
3087
|
+
for (const g of report.gaps) {
|
|
3088
|
+
lines.push(`- **${g.expectationName}** (${g.type}): ${g.description}`);
|
|
3089
|
+
}
|
|
3090
|
+
lines.push("");
|
|
3091
|
+
}
|
|
3092
|
+
if (report.factDerivationChains.length > 0) {
|
|
3093
|
+
lines.push("## \u{1F517} Fact Derivation Chains");
|
|
3094
|
+
lines.push("");
|
|
3095
|
+
for (const chain of report.factDerivationChains) {
|
|
3096
|
+
if (chain.steps.length === 0) continue;
|
|
3097
|
+
lines.push(`### \`${chain.targetFact}\` (depth: ${chain.depth})`);
|
|
3098
|
+
for (const step of chain.steps) {
|
|
3099
|
+
const icon2 = step.type === "event" ? "\u26A1" : step.type === "rule-fired" ? "\u2699\uFE0F" : step.type === "fact-produced" ? "\u{1F4E6}" : "\u{1F4D6}";
|
|
3100
|
+
lines.push(` ${icon2} ${step.description}`);
|
|
3101
|
+
}
|
|
3102
|
+
lines.push("");
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
if (report.suggestions.length > 0) {
|
|
3106
|
+
lines.push("## \u{1F4A1} Suggestions");
|
|
3107
|
+
lines.push("");
|
|
3108
|
+
for (const s of report.suggestions) {
|
|
3109
|
+
const priorityIcon = s.priority >= 8 ? "\u{1F534}" : s.priority >= 5 ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
3110
|
+
lines.push(`${priorityIcon} **[${s.findingType}]** ${s.message}`);
|
|
3111
|
+
if (s.skeleton) {
|
|
3112
|
+
lines.push("```typescript");
|
|
3113
|
+
lines.push(s.skeleton);
|
|
3114
|
+
lines.push("```");
|
|
3115
|
+
}
|
|
3116
|
+
lines.push("");
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
return lines.join("\n");
|
|
3120
|
+
}
|
|
3121
|
+
function formatBuildOutput(report) {
|
|
3122
|
+
const lines = [];
|
|
3123
|
+
lines.push(`::group::Decision Ledger Analysis (Score: ${report.summary.healthScore}/100)`);
|
|
3124
|
+
for (const c of report.contradictions) {
|
|
3125
|
+
lines.push(`::error title=Contradiction::Rules "${c.ruleA}" and "${c.ruleB}" both produce "${c.conflictingTag}"`);
|
|
3126
|
+
}
|
|
3127
|
+
for (const g of report.gaps) {
|
|
3128
|
+
if (g.type === "no-rule") {
|
|
3129
|
+
lines.push(`::error title=Missing Rule::No rule covers expectation "${g.expectationName}"`);
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
for (const dr of report.deadRules) {
|
|
3133
|
+
lines.push(`::warning title=Dead Rule::Rule "${dr.ruleId}" can never fire (requires [${dr.requiredEventTypes.join(", ")}])`);
|
|
3134
|
+
}
|
|
3135
|
+
for (const sr of report.shadowedRules) {
|
|
3136
|
+
lines.push(`::warning title=Shadowed Rule::Rule "${sr.ruleId}" is always superseded by "${sr.shadowedBy}"`);
|
|
3137
|
+
}
|
|
3138
|
+
for (const us of report.unreachableStates) {
|
|
3139
|
+
lines.push(`::warning title=Unreachable State::Facts [${us.factTags.join(", ")}] cannot be produced together`);
|
|
3140
|
+
}
|
|
3141
|
+
for (const g of report.gaps) {
|
|
3142
|
+
if (g.type === "partial-coverage") {
|
|
3143
|
+
lines.push(`::warning title=Partial Coverage::Expectation "${g.expectationName}" is only partially covered`);
|
|
3144
|
+
} else if (g.type === "no-contract") {
|
|
3145
|
+
lines.push(`::warning title=Missing Contract::Rules for "${g.expectationName}" lack contracts`);
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
lines.push("");
|
|
3149
|
+
lines.push(`Rules: ${report.summary.totalRules} | Constraints: ${report.summary.totalConstraints} | Health: ${report.summary.healthScore}/100`);
|
|
3150
|
+
lines.push(`Dead: ${report.summary.deadRuleCount} | Unreachable: ${report.summary.unreachableStateCount} | Shadowed: ${report.summary.shadowedRuleCount} | Contradictions: ${report.summary.contradictionCount} | Gaps: ${report.summary.gapCount}`);
|
|
3151
|
+
lines.push("::endgroup::");
|
|
3152
|
+
return lines.join("\n");
|
|
3153
|
+
}
|
|
3154
|
+
function diffLedgers(before, after) {
|
|
3155
|
+
const changes = [];
|
|
3156
|
+
diffItems(
|
|
3157
|
+
before.deadRules,
|
|
3158
|
+
after.deadRules,
|
|
3159
|
+
(dr) => dr.ruleId,
|
|
3160
|
+
(dr) => `Dead rule: ${dr.ruleId} \u2014 ${dr.reason}`,
|
|
3161
|
+
"dead-rule",
|
|
3162
|
+
changes
|
|
3163
|
+
);
|
|
3164
|
+
diffItems(
|
|
3165
|
+
before.unreachableStates,
|
|
3166
|
+
after.unreachableStates,
|
|
3167
|
+
(us) => us.factTags.join("+"),
|
|
3168
|
+
(us) => `Unreachable state: [${us.factTags.join(", ")}]`,
|
|
3169
|
+
"unreachable-state",
|
|
3170
|
+
changes
|
|
3171
|
+
);
|
|
3172
|
+
diffItems(
|
|
3173
|
+
before.shadowedRules,
|
|
3174
|
+
after.shadowedRules,
|
|
3175
|
+
(sr) => sr.ruleId,
|
|
3176
|
+
(sr) => `Shadowed rule: ${sr.ruleId} by ${sr.shadowedBy}`,
|
|
3177
|
+
"shadowed-rule",
|
|
3178
|
+
changes
|
|
3179
|
+
);
|
|
3180
|
+
diffItems(
|
|
3181
|
+
before.contradictions,
|
|
3182
|
+
after.contradictions,
|
|
3183
|
+
(c) => `${c.ruleA}\u2194${c.ruleB}`,
|
|
3184
|
+
(c) => `Contradiction: ${c.ruleA} \u2194 ${c.ruleB} on ${c.conflictingTag}`,
|
|
3185
|
+
"contradiction",
|
|
3186
|
+
changes
|
|
3187
|
+
);
|
|
3188
|
+
diffItems(
|
|
3189
|
+
before.gaps,
|
|
3190
|
+
after.gaps,
|
|
3191
|
+
(g) => g.expectationName,
|
|
3192
|
+
(g) => `Gap: ${g.expectationName} (${g.type})`,
|
|
3193
|
+
"gap",
|
|
3194
|
+
changes
|
|
3195
|
+
);
|
|
3196
|
+
const scoreDelta = after.summary.healthScore - before.summary.healthScore;
|
|
3197
|
+
const scoreDirection = scoreDelta > 0 ? "improved" : scoreDelta < 0 ? "degraded" : "unchanged";
|
|
3198
|
+
return {
|
|
3199
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3200
|
+
beforeTimestamp: before.timestamp,
|
|
3201
|
+
afterTimestamp: after.timestamp,
|
|
3202
|
+
changes,
|
|
3203
|
+
scoreDelta,
|
|
3204
|
+
summary: `Score ${scoreDirection} by ${Math.abs(scoreDelta)} points (${before.summary.healthScore} \u2192 ${after.summary.healthScore}). ${changes.length} changes: ${changes.filter((c) => c.type === "added").length} added, ${changes.filter((c) => c.type === "removed").length} removed, ${changes.filter((c) => c.type === "changed").length} changed.`
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
3207
|
+
function diffItems(before, after, getKey, describe, category, changes) {
|
|
3208
|
+
const beforeKeys = new Set(before.map(getKey));
|
|
3209
|
+
const afterKeys = new Set(after.map(getKey));
|
|
3210
|
+
for (const item of after) {
|
|
3211
|
+
const key = getKey(item);
|
|
3212
|
+
if (!beforeKeys.has(key)) {
|
|
3213
|
+
changes.push({
|
|
3214
|
+
type: "added",
|
|
3215
|
+
category,
|
|
3216
|
+
description: describe(item),
|
|
3217
|
+
entityId: key
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
for (const item of before) {
|
|
3222
|
+
const key = getKey(item);
|
|
3223
|
+
if (!afterKeys.has(key)) {
|
|
3224
|
+
changes.push({
|
|
3225
|
+
type: "removed",
|
|
3226
|
+
category,
|
|
3227
|
+
description: describe(item),
|
|
3228
|
+
entityId: key
|
|
3229
|
+
});
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
|
|
2015
3234
|
// src/runtime/terminal-adapter.ts
|
|
2016
3235
|
async function defaultExecutor(command, options) {
|
|
2017
3236
|
try {
|
|
@@ -6193,6 +7412,866 @@ function expectationGate(config = {}) {
|
|
|
6193
7412
|
onViolation: "expectations-failed"
|
|
6194
7413
|
});
|
|
6195
7414
|
}
|
|
7415
|
+
|
|
7416
|
+
// src/chronos/project-chronicle.ts
|
|
7417
|
+
var ProjectChronicle = class {
|
|
7418
|
+
events = [];
|
|
7419
|
+
maxEvents;
|
|
7420
|
+
now;
|
|
7421
|
+
constructor(options = {}) {
|
|
7422
|
+
this.maxEvents = options.maxEvents ?? 1e4;
|
|
7423
|
+
this.now = options.now ?? (() => Date.now());
|
|
7424
|
+
}
|
|
7425
|
+
// ── Recording ───────────────────────────────────────────────────────────
|
|
7426
|
+
/**
|
|
7427
|
+
* Record a project event. Returns the recorded event (with timestamp filled in).
|
|
7428
|
+
*/
|
|
7429
|
+
record(event) {
|
|
7430
|
+
const full = {
|
|
7431
|
+
kind: event.kind,
|
|
7432
|
+
action: event.action,
|
|
7433
|
+
subject: event.subject,
|
|
7434
|
+
timestamp: event.timestamp ?? this.now(),
|
|
7435
|
+
metadata: event.metadata,
|
|
7436
|
+
diff: event.diff
|
|
7437
|
+
};
|
|
7438
|
+
this.events.push(full);
|
|
7439
|
+
if (this.maxEvents > 0 && this.events.length > this.maxEvents) {
|
|
7440
|
+
this.events = this.events.slice(this.events.length - this.maxEvents);
|
|
7441
|
+
}
|
|
7442
|
+
return full;
|
|
7443
|
+
}
|
|
7444
|
+
// ── Convenience recorders ─────────────────────────────────────────────
|
|
7445
|
+
recordRuleRegistered(ruleId, meta = {}) {
|
|
7446
|
+
return this.record({ kind: "rule", action: "registered", subject: ruleId, metadata: meta });
|
|
7447
|
+
}
|
|
7448
|
+
recordRuleModified(ruleId, diff, meta = {}) {
|
|
7449
|
+
return this.record({ kind: "rule", action: "modified", subject: ruleId, metadata: meta, diff });
|
|
7450
|
+
}
|
|
7451
|
+
recordRuleRemoved(ruleId, meta = {}) {
|
|
7452
|
+
return this.record({ kind: "rule", action: "removed", subject: ruleId, metadata: meta });
|
|
7453
|
+
}
|
|
7454
|
+
recordContractAdded(contractId, meta = {}) {
|
|
7455
|
+
return this.record({ kind: "contract", action: "added", subject: contractId, metadata: meta });
|
|
7456
|
+
}
|
|
7457
|
+
recordContractModified(contractId, diff, meta = {}) {
|
|
7458
|
+
return this.record({ kind: "contract", action: "modified", subject: contractId, metadata: meta, diff });
|
|
7459
|
+
}
|
|
7460
|
+
recordExpectationSatisfied(name, meta = {}) {
|
|
7461
|
+
return this.record({ kind: "expectation", action: "satisfied", subject: name, metadata: meta });
|
|
7462
|
+
}
|
|
7463
|
+
recordExpectationViolated(name, meta = {}) {
|
|
7464
|
+
return this.record({ kind: "expectation", action: "violated", subject: name, metadata: meta });
|
|
7465
|
+
}
|
|
7466
|
+
recordGateTransition(gateName, from, to, meta = {}) {
|
|
7467
|
+
return this.record({
|
|
7468
|
+
kind: "gate",
|
|
7469
|
+
action: to,
|
|
7470
|
+
subject: gateName,
|
|
7471
|
+
metadata: { ...meta, from, to },
|
|
7472
|
+
diff: { before: from, after: to }
|
|
7473
|
+
});
|
|
7474
|
+
}
|
|
7475
|
+
recordBuildAudit(score, delta, meta = {}) {
|
|
7476
|
+
return this.record({
|
|
7477
|
+
kind: "build",
|
|
7478
|
+
action: "audit-complete",
|
|
7479
|
+
subject: "completeness",
|
|
7480
|
+
metadata: { ...meta, score, delta }
|
|
7481
|
+
});
|
|
7482
|
+
}
|
|
7483
|
+
recordFactIntroduced(factTag, meta = {}) {
|
|
7484
|
+
return this.record({ kind: "fact", action: "introduced", subject: factTag, metadata: meta });
|
|
7485
|
+
}
|
|
7486
|
+
recordFactDeprecated(factTag, meta = {}) {
|
|
7487
|
+
return this.record({ kind: "fact", action: "deprecated", subject: factTag, metadata: meta });
|
|
7488
|
+
}
|
|
7489
|
+
// ── Access ──────────────────────────────────────────────────────────────
|
|
7490
|
+
/** Return a shallow copy of all events. */
|
|
7491
|
+
getEvents() {
|
|
7492
|
+
return [...this.events];
|
|
7493
|
+
}
|
|
7494
|
+
/** Total number of recorded events. */
|
|
7495
|
+
get size() {
|
|
7496
|
+
return this.events.length;
|
|
7497
|
+
}
|
|
7498
|
+
/** Clear all events (primarily for testing). */
|
|
7499
|
+
clear() {
|
|
7500
|
+
this.events = [];
|
|
7501
|
+
}
|
|
7502
|
+
};
|
|
7503
|
+
function createProjectChronicle(options) {
|
|
7504
|
+
return new ProjectChronicle(options);
|
|
7505
|
+
}
|
|
7506
|
+
|
|
7507
|
+
// src/chronos/timeline.ts
|
|
7508
|
+
var Timeline = class {
|
|
7509
|
+
constructor(chronicle) {
|
|
7510
|
+
this.chronicle = chronicle;
|
|
7511
|
+
}
|
|
7512
|
+
// ── Queries ─────────────────────────────────────────────────────────────
|
|
7513
|
+
/**
|
|
7514
|
+
* Query events with optional filtering.
|
|
7515
|
+
* Returns matching events sorted chronologically (oldest first).
|
|
7516
|
+
*/
|
|
7517
|
+
getTimeline(filter) {
|
|
7518
|
+
let events = this.chronicle.getEvents();
|
|
7519
|
+
if (!filter) return events;
|
|
7520
|
+
events = applyFilter(events, filter);
|
|
7521
|
+
return events;
|
|
7522
|
+
}
|
|
7523
|
+
/**
|
|
7524
|
+
* Get all events since a timestamp (inclusive).
|
|
7525
|
+
*/
|
|
7526
|
+
getEventsSince(timestamp) {
|
|
7527
|
+
return this.getTimeline({ since: timestamp });
|
|
7528
|
+
}
|
|
7529
|
+
/**
|
|
7530
|
+
* Compute a behavioral delta between two timestamps.
|
|
7531
|
+
*/
|
|
7532
|
+
getDelta(from, to) {
|
|
7533
|
+
const events = this.getTimeline({ since: from, until: to });
|
|
7534
|
+
return buildDelta(from, to, events);
|
|
7535
|
+
}
|
|
7536
|
+
/**
|
|
7537
|
+
* Get full history for a specific subject (rule id, gate name, etc.).
|
|
7538
|
+
* Sorted chronologically.
|
|
7539
|
+
*/
|
|
7540
|
+
getHistory(subjectId) {
|
|
7541
|
+
return this.getTimeline({ subject: subjectId });
|
|
7542
|
+
}
|
|
7543
|
+
};
|
|
7544
|
+
function applyFilter(events, filter) {
|
|
7545
|
+
return events.filter((e) => {
|
|
7546
|
+
if (filter.kind) {
|
|
7547
|
+
const kinds = Array.isArray(filter.kind) ? filter.kind : [filter.kind];
|
|
7548
|
+
if (!kinds.includes(e.kind)) return false;
|
|
7549
|
+
}
|
|
7550
|
+
if (filter.action) {
|
|
7551
|
+
const actions = Array.isArray(filter.action) ? filter.action : [filter.action];
|
|
7552
|
+
if (!actions.includes(e.action)) return false;
|
|
7553
|
+
}
|
|
7554
|
+
if (filter.subject) {
|
|
7555
|
+
const subjects = Array.isArray(filter.subject) ? filter.subject : [filter.subject];
|
|
7556
|
+
if (!subjects.includes(e.subject)) return false;
|
|
7557
|
+
}
|
|
7558
|
+
if (filter.since != null && e.timestamp < filter.since) return false;
|
|
7559
|
+
if (filter.until != null && e.timestamp > filter.until) return false;
|
|
7560
|
+
return true;
|
|
7561
|
+
});
|
|
7562
|
+
}
|
|
7563
|
+
var ADD_ACTIONS = /* @__PURE__ */ new Set(["registered", "added", "introduced", "opened"]);
|
|
7564
|
+
var REMOVE_ACTIONS = /* @__PURE__ */ new Set(["removed", "deprecated", "closed"]);
|
|
7565
|
+
var MODIFY_ACTIONS = /* @__PURE__ */ new Set(["modified", "updated"]);
|
|
7566
|
+
function buildDelta(from, to, events) {
|
|
7567
|
+
const summary = {};
|
|
7568
|
+
const added = /* @__PURE__ */ new Set();
|
|
7569
|
+
const removed = /* @__PURE__ */ new Set();
|
|
7570
|
+
const modified = /* @__PURE__ */ new Set();
|
|
7571
|
+
for (const e of events) {
|
|
7572
|
+
summary[e.kind] = (summary[e.kind] ?? 0) + 1;
|
|
7573
|
+
if (ADD_ACTIONS.has(e.action)) {
|
|
7574
|
+
added.add(e.subject);
|
|
7575
|
+
removed.delete(e.subject);
|
|
7576
|
+
} else if (REMOVE_ACTIONS.has(e.action)) {
|
|
7577
|
+
removed.add(e.subject);
|
|
7578
|
+
added.delete(e.subject);
|
|
7579
|
+
} else if (MODIFY_ACTIONS.has(e.action)) {
|
|
7580
|
+
modified.add(e.subject);
|
|
7581
|
+
}
|
|
7582
|
+
}
|
|
7583
|
+
return {
|
|
7584
|
+
from,
|
|
7585
|
+
to,
|
|
7586
|
+
events,
|
|
7587
|
+
summary,
|
|
7588
|
+
added: [...added],
|
|
7589
|
+
removed: [...removed],
|
|
7590
|
+
modified: [...modified]
|
|
7591
|
+
};
|
|
7592
|
+
}
|
|
7593
|
+
function createTimeline(chronicle) {
|
|
7594
|
+
return new Timeline(chronicle);
|
|
7595
|
+
}
|
|
7596
|
+
|
|
7597
|
+
// src/chronos/hooks.ts
|
|
7598
|
+
function enableProjectChronicle(registry, engine, options = {}) {
|
|
7599
|
+
const chronicle = options.chronicle ?? createProjectChronicle();
|
|
7600
|
+
const recordSteps = options.recordSteps ?? true;
|
|
7601
|
+
const recordConstraints = options.recordConstraints ?? true;
|
|
7602
|
+
const origRegisterRule = registry.registerRule.bind(registry);
|
|
7603
|
+
const origRegisterModule = registry.registerModule.bind(registry);
|
|
7604
|
+
const origStep = engine.step.bind(engine);
|
|
7605
|
+
const origCheckConstraints = engine.checkConstraints.bind(engine);
|
|
7606
|
+
registry.registerRule = function hookedRegisterRule(descriptor) {
|
|
7607
|
+
origRegisterRule(descriptor);
|
|
7608
|
+
chronicle.recordRuleRegistered(descriptor.id, {
|
|
7609
|
+
description: descriptor.description,
|
|
7610
|
+
hasContract: !!descriptor.contract,
|
|
7611
|
+
eventTypes: descriptor.eventTypes
|
|
7612
|
+
});
|
|
7613
|
+
if (descriptor.contract) {
|
|
7614
|
+
chronicle.recordContractAdded(descriptor.id, {
|
|
7615
|
+
behavior: descriptor.contract.behavior,
|
|
7616
|
+
examplesCount: descriptor.contract.examples?.length ?? 0,
|
|
7617
|
+
invariantsCount: descriptor.contract.invariants?.length ?? 0
|
|
7618
|
+
});
|
|
7619
|
+
}
|
|
7620
|
+
};
|
|
7621
|
+
registry.registerModule = function hookedRegisterModule(module2) {
|
|
7622
|
+
origRegisterModule(module2);
|
|
7623
|
+
for (const rule of module2.rules) {
|
|
7624
|
+
chronicle.recordRuleRegistered(rule.id, {
|
|
7625
|
+
description: rule.description,
|
|
7626
|
+
hasContract: !!rule.contract,
|
|
7627
|
+
eventTypes: rule.eventTypes,
|
|
7628
|
+
registeredVia: "module"
|
|
7629
|
+
});
|
|
7630
|
+
if (rule.contract) {
|
|
7631
|
+
chronicle.recordContractAdded(rule.id, {
|
|
7632
|
+
behavior: rule.contract.behavior,
|
|
7633
|
+
examplesCount: rule.contract.examples?.length ?? 0,
|
|
7634
|
+
invariantsCount: rule.contract.invariants?.length ?? 0
|
|
7635
|
+
});
|
|
7636
|
+
}
|
|
7637
|
+
}
|
|
7638
|
+
};
|
|
7639
|
+
if (recordSteps) {
|
|
7640
|
+
engine.step = function hookedStep(events) {
|
|
7641
|
+
const result = origStep(events);
|
|
7642
|
+
const factTags = /* @__PURE__ */ new Set();
|
|
7643
|
+
for (const fact2 of result.state.facts) {
|
|
7644
|
+
factTags.add(fact2.tag);
|
|
7645
|
+
}
|
|
7646
|
+
chronicle.record({
|
|
7647
|
+
kind: "build",
|
|
7648
|
+
action: "step-complete",
|
|
7649
|
+
subject: "engine",
|
|
7650
|
+
metadata: {
|
|
7651
|
+
eventsProcessed: events.length,
|
|
7652
|
+
eventTags: events.map((e) => e.tag),
|
|
7653
|
+
factsAfter: result.state.facts.length,
|
|
7654
|
+
diagnosticsCount: result.diagnostics.length
|
|
7655
|
+
}
|
|
7656
|
+
});
|
|
7657
|
+
for (const diag of result.diagnostics) {
|
|
7658
|
+
if (diag.kind === "constraint-violation") {
|
|
7659
|
+
chronicle.record({
|
|
7660
|
+
kind: "expectation",
|
|
7661
|
+
action: "violated",
|
|
7662
|
+
subject: extractSubjectFromDiag(diag),
|
|
7663
|
+
metadata: { message: diag.message, data: diag.data }
|
|
7664
|
+
});
|
|
7665
|
+
}
|
|
7666
|
+
}
|
|
7667
|
+
return result;
|
|
7668
|
+
};
|
|
7669
|
+
}
|
|
7670
|
+
if (recordConstraints) {
|
|
7671
|
+
engine.checkConstraints = function hookedCheckConstraints() {
|
|
7672
|
+
const diagnostics = origCheckConstraints();
|
|
7673
|
+
chronicle.record({
|
|
7674
|
+
kind: "build",
|
|
7675
|
+
action: "constraints-checked",
|
|
7676
|
+
subject: "engine",
|
|
7677
|
+
metadata: {
|
|
7678
|
+
violations: diagnostics.length
|
|
7679
|
+
}
|
|
7680
|
+
});
|
|
7681
|
+
for (const diag of diagnostics) {
|
|
7682
|
+
chronicle.record({
|
|
7683
|
+
kind: "expectation",
|
|
7684
|
+
action: "violated",
|
|
7685
|
+
subject: extractSubjectFromDiag(diag),
|
|
7686
|
+
metadata: { message: diag.message }
|
|
7687
|
+
});
|
|
7688
|
+
}
|
|
7689
|
+
return diagnostics;
|
|
7690
|
+
};
|
|
7691
|
+
}
|
|
7692
|
+
function disconnect() {
|
|
7693
|
+
registry.registerRule = origRegisterRule;
|
|
7694
|
+
registry.registerModule = origRegisterModule;
|
|
7695
|
+
engine.step = origStep;
|
|
7696
|
+
engine.checkConstraints = origCheckConstraints;
|
|
7697
|
+
}
|
|
7698
|
+
return { chronicle, disconnect };
|
|
7699
|
+
}
|
|
7700
|
+
function recordAudit(chronicle, report, previousScore) {
|
|
7701
|
+
const delta = previousScore != null ? report.score - previousScore : 0;
|
|
7702
|
+
chronicle.recordBuildAudit(report.score, delta, {
|
|
7703
|
+
rating: report.rating,
|
|
7704
|
+
rulesCovered: report.rules.covered,
|
|
7705
|
+
rulesTotal: report.rules.total,
|
|
7706
|
+
constraintsCovered: report.constraints.covered,
|
|
7707
|
+
constraintsTotal: report.constraints.total,
|
|
7708
|
+
contractsCovered: report.contracts.withContracts,
|
|
7709
|
+
contractsTotal: report.contracts.total
|
|
7710
|
+
});
|
|
7711
|
+
}
|
|
7712
|
+
function extractSubjectFromDiag(diag) {
|
|
7713
|
+
const data = diag.data;
|
|
7714
|
+
if (data?.constraintId && typeof data.constraintId === "string") return data.constraintId;
|
|
7715
|
+
if (data?.ruleId && typeof data.ruleId === "string") return data.ruleId;
|
|
7716
|
+
return "unknown";
|
|
7717
|
+
}
|
|
7718
|
+
|
|
7719
|
+
// src/chronos/diff.ts
|
|
7720
|
+
function diffRegistries(before, after) {
|
|
7721
|
+
const beforeRules = toIdMap(before.rules);
|
|
7722
|
+
const afterRules = toIdMap(after.rules);
|
|
7723
|
+
const beforeConstraints = toIdMap(before.constraints);
|
|
7724
|
+
const afterConstraints = toIdMap(after.constraints);
|
|
7725
|
+
return {
|
|
7726
|
+
rulesAdded: setDiff(afterRules, beforeRules),
|
|
7727
|
+
rulesRemoved: setDiff(beforeRules, afterRules),
|
|
7728
|
+
rulesModified: findModified(beforeRules, afterRules),
|
|
7729
|
+
constraintsAdded: setDiff(afterConstraints, beforeConstraints),
|
|
7730
|
+
constraintsRemoved: setDiff(beforeConstraints, afterConstraints),
|
|
7731
|
+
constraintsModified: findModified(beforeConstraints, afterConstraints)
|
|
7732
|
+
};
|
|
7733
|
+
}
|
|
7734
|
+
function diffContracts(before, after) {
|
|
7735
|
+
const beforeMap = toRecord(before.coverage);
|
|
7736
|
+
const afterMap = toRecord(after.coverage);
|
|
7737
|
+
const contractsAdded = [];
|
|
7738
|
+
const contractsRemoved = [];
|
|
7739
|
+
for (const [id, has] of Object.entries(afterMap)) {
|
|
7740
|
+
if (has && !beforeMap[id]) {
|
|
7741
|
+
contractsAdded.push(id);
|
|
7742
|
+
}
|
|
7743
|
+
}
|
|
7744
|
+
for (const [id, had] of Object.entries(beforeMap)) {
|
|
7745
|
+
if (had && !afterMap[id]) {
|
|
7746
|
+
contractsRemoved.push(id);
|
|
7747
|
+
}
|
|
7748
|
+
}
|
|
7749
|
+
const countTrue = (r) => Object.values(r).filter(Boolean).length;
|
|
7750
|
+
const totalBefore = Object.keys(beforeMap).length;
|
|
7751
|
+
const totalAfter = Object.keys(afterMap).length;
|
|
7752
|
+
return {
|
|
7753
|
+
contractsAdded,
|
|
7754
|
+
contractsRemoved,
|
|
7755
|
+
coverageBefore: totalBefore > 0 ? countTrue(beforeMap) / totalBefore : 0,
|
|
7756
|
+
coverageAfter: totalAfter > 0 ? countTrue(afterMap) / totalAfter : 0
|
|
7757
|
+
};
|
|
7758
|
+
}
|
|
7759
|
+
function diffExpectations(before, after) {
|
|
7760
|
+
const beforeMap = toRecord(before.expectations);
|
|
7761
|
+
const afterMap = toRecord(after.expectations);
|
|
7762
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)]);
|
|
7763
|
+
const newlySatisfied = [];
|
|
7764
|
+
const newlyViolated = [];
|
|
7765
|
+
const unchanged = [];
|
|
7766
|
+
for (const key of allKeys) {
|
|
7767
|
+
const was = beforeMap[key] ?? false;
|
|
7768
|
+
const is = afterMap[key] ?? false;
|
|
7769
|
+
if (!was && is) {
|
|
7770
|
+
newlySatisfied.push(key);
|
|
7771
|
+
} else if (was && !is) {
|
|
7772
|
+
newlyViolated.push(key);
|
|
7773
|
+
} else {
|
|
7774
|
+
unchanged.push(key);
|
|
7775
|
+
}
|
|
7776
|
+
}
|
|
7777
|
+
return { newlySatisfied, newlyViolated, unchanged };
|
|
7778
|
+
}
|
|
7779
|
+
function formatDelta(diff) {
|
|
7780
|
+
const lines = [];
|
|
7781
|
+
if (diff.rulesAdded.length > 0)
|
|
7782
|
+
lines.push(`+ Rules added: ${diff.rulesAdded.join(", ")}`);
|
|
7783
|
+
if (diff.rulesRemoved.length > 0)
|
|
7784
|
+
lines.push(`- Rules removed: ${diff.rulesRemoved.join(", ")}`);
|
|
7785
|
+
if (diff.rulesModified.length > 0)
|
|
7786
|
+
lines.push(`~ Rules modified: ${diff.rulesModified.join(", ")}`);
|
|
7787
|
+
if (diff.constraintsAdded.length > 0)
|
|
7788
|
+
lines.push(`+ Constraints added: ${diff.constraintsAdded.join(", ")}`);
|
|
7789
|
+
if (diff.constraintsRemoved.length > 0)
|
|
7790
|
+
lines.push(`- Constraints removed: ${diff.constraintsRemoved.join(", ")}`);
|
|
7791
|
+
if (diff.constraintsModified.length > 0)
|
|
7792
|
+
lines.push(`~ Constraints modified: ${diff.constraintsModified.join(", ")}`);
|
|
7793
|
+
return lines.length > 0 ? lines.join("\n") : "No behavioral changes.";
|
|
7794
|
+
}
|
|
7795
|
+
function formatCommitMessage(diff) {
|
|
7796
|
+
const praxisDiff = {
|
|
7797
|
+
rulesAdded: diff.rulesAdded,
|
|
7798
|
+
rulesRemoved: diff.rulesRemoved,
|
|
7799
|
+
rulesModified: diff.rulesModified,
|
|
7800
|
+
contractsAdded: [],
|
|
7801
|
+
contractsRemoved: [],
|
|
7802
|
+
expectationsAdded: diff.constraintsAdded,
|
|
7803
|
+
expectationsRemoved: diff.constraintsRemoved,
|
|
7804
|
+
gateChanges: []
|
|
7805
|
+
};
|
|
7806
|
+
return commitFromDiff(praxisDiff);
|
|
7807
|
+
}
|
|
7808
|
+
function formatReleaseNotes(diffs) {
|
|
7809
|
+
const sections = [];
|
|
7810
|
+
const allAdded = [];
|
|
7811
|
+
const allRemoved = [];
|
|
7812
|
+
const allModified = [];
|
|
7813
|
+
for (const diff of diffs) {
|
|
7814
|
+
allAdded.push(...diff.rulesAdded, ...diff.constraintsAdded);
|
|
7815
|
+
allRemoved.push(...diff.rulesRemoved, ...diff.constraintsRemoved);
|
|
7816
|
+
allModified.push(...diff.rulesModified, ...diff.constraintsModified);
|
|
7817
|
+
}
|
|
7818
|
+
const added = [...new Set(allAdded)];
|
|
7819
|
+
const removed = [...new Set(allRemoved)];
|
|
7820
|
+
const modified = [...new Set(allModified)];
|
|
7821
|
+
if (added.length > 0) {
|
|
7822
|
+
sections.push(`### Added
|
|
7823
|
+
${added.map((id) => `- ${id}`).join("\n")}`);
|
|
7824
|
+
}
|
|
7825
|
+
if (modified.length > 0) {
|
|
7826
|
+
sections.push(`### Changed
|
|
7827
|
+
${modified.map((id) => `- ${id}`).join("\n")}`);
|
|
7828
|
+
}
|
|
7829
|
+
if (removed.length > 0) {
|
|
7830
|
+
sections.push(`### Removed
|
|
7831
|
+
${removed.map((id) => `- ${id}`).join("\n")}`);
|
|
7832
|
+
}
|
|
7833
|
+
if (sections.length === 0) return "No behavioral changes in this release.";
|
|
7834
|
+
return `## Release Notes
|
|
7835
|
+
|
|
7836
|
+
${sections.join("\n\n")}`;
|
|
7837
|
+
}
|
|
7838
|
+
function toIdMap(input) {
|
|
7839
|
+
if (input instanceof Map) return input;
|
|
7840
|
+
const map = /* @__PURE__ */ new Map();
|
|
7841
|
+
for (const item of input) map.set(item.id, item);
|
|
7842
|
+
return map;
|
|
7843
|
+
}
|
|
7844
|
+
function setDiff(a, b) {
|
|
7845
|
+
const result = [];
|
|
7846
|
+
for (const key of a.keys()) {
|
|
7847
|
+
if (!b.has(key)) result.push(key);
|
|
7848
|
+
}
|
|
7849
|
+
return result;
|
|
7850
|
+
}
|
|
7851
|
+
function findModified(before, after) {
|
|
7852
|
+
const result = [];
|
|
7853
|
+
for (const [key, beforeVal] of before) {
|
|
7854
|
+
const afterVal = after.get(key);
|
|
7855
|
+
if (afterVal && beforeVal.description !== afterVal.description) {
|
|
7856
|
+
result.push(key);
|
|
7857
|
+
}
|
|
7858
|
+
}
|
|
7859
|
+
return result;
|
|
7860
|
+
}
|
|
7861
|
+
function toRecord(input) {
|
|
7862
|
+
if (input instanceof Map) {
|
|
7863
|
+
const result = {};
|
|
7864
|
+
for (const [k, v] of input) result[k] = v;
|
|
7865
|
+
return result;
|
|
7866
|
+
}
|
|
7867
|
+
return input;
|
|
7868
|
+
}
|
|
7869
|
+
function commitFromDiff(diff) {
|
|
7870
|
+
const parts = [];
|
|
7871
|
+
const bodyParts = [];
|
|
7872
|
+
const totalAdded = diff.rulesAdded.length + diff.contractsAdded.length + diff.expectationsAdded.length;
|
|
7873
|
+
const totalRemoved = diff.rulesRemoved.length + diff.contractsRemoved.length + diff.expectationsRemoved.length;
|
|
7874
|
+
const totalModified = diff.rulesModified.length;
|
|
7875
|
+
if (totalAdded > 0 && totalRemoved === 0 && totalModified === 0) {
|
|
7876
|
+
if (diff.rulesAdded.length > 0) {
|
|
7877
|
+
const scope = inferScope2(diff.rulesAdded);
|
|
7878
|
+
parts.push(`feat(${scope}): add ${fmtIds(diff.rulesAdded)}`);
|
|
7879
|
+
} else if (diff.contractsAdded.length > 0) {
|
|
7880
|
+
parts.push(`feat(contracts): add contracts for ${fmtIds(diff.contractsAdded)}`);
|
|
7881
|
+
} else {
|
|
7882
|
+
parts.push(`feat(expectations): add ${fmtIds(diff.expectationsAdded)}`);
|
|
7883
|
+
}
|
|
7884
|
+
} else if (totalRemoved > 0 && totalAdded === 0) {
|
|
7885
|
+
if (diff.rulesRemoved.length > 0) {
|
|
7886
|
+
const scope = inferScope2(diff.rulesRemoved);
|
|
7887
|
+
parts.push(`refactor(${scope}): remove ${fmtIds(diff.rulesRemoved)}`);
|
|
7888
|
+
} else {
|
|
7889
|
+
parts.push(`refactor: remove ${totalRemoved} item(s)`);
|
|
7890
|
+
}
|
|
7891
|
+
} else if (totalModified > 0) {
|
|
7892
|
+
const scope = inferScope2(diff.rulesModified);
|
|
7893
|
+
parts.push(`refactor(${scope}): update ${fmtIds(diff.rulesModified)}`);
|
|
7894
|
+
} else {
|
|
7895
|
+
parts.push("chore: behavioral state update");
|
|
7896
|
+
}
|
|
7897
|
+
if (diff.rulesAdded.length > 0) bodyParts.push(`Rules added: ${diff.rulesAdded.join(", ")}`);
|
|
7898
|
+
if (diff.rulesRemoved.length > 0) bodyParts.push(`Rules removed: ${diff.rulesRemoved.join(", ")}`);
|
|
7899
|
+
if (diff.rulesModified.length > 0) bodyParts.push(`Rules modified: ${diff.rulesModified.join(", ")}`);
|
|
7900
|
+
const subject = parts[0] || "chore: update";
|
|
7901
|
+
return bodyParts.length > 0 ? `${subject}
|
|
7902
|
+
|
|
7903
|
+
${bodyParts.join("\n")}` : subject;
|
|
7904
|
+
}
|
|
7905
|
+
function inferScope2(ids) {
|
|
7906
|
+
if (ids.length === 0) return "rules";
|
|
7907
|
+
const prefixes = ids.map((id) => {
|
|
7908
|
+
const slash = id.indexOf("/");
|
|
7909
|
+
return slash > 0 ? id.slice(0, slash) : id;
|
|
7910
|
+
});
|
|
7911
|
+
const unique = new Set(prefixes);
|
|
7912
|
+
return unique.size === 1 ? prefixes[0] : "rules";
|
|
7913
|
+
}
|
|
7914
|
+
function fmtIds(ids) {
|
|
7915
|
+
if (ids.length <= 3) return ids.join(", ");
|
|
7916
|
+
return `${ids.slice(0, 2).join(", ")} (+${ids.length - 2} more)`;
|
|
7917
|
+
}
|
|
7918
|
+
|
|
7919
|
+
// src/unified/core.ts
|
|
7920
|
+
var _idCounter = 0;
|
|
7921
|
+
function nextId() {
|
|
7922
|
+
return `px:${Date.now()}-${++_idCounter}`;
|
|
7923
|
+
}
|
|
7924
|
+
function createApp(config) {
|
|
7925
|
+
const paths = /* @__PURE__ */ new Map();
|
|
7926
|
+
for (const schema of config.schema) {
|
|
7927
|
+
paths.set(schema.path, {
|
|
7928
|
+
schema,
|
|
7929
|
+
value: schema.initial,
|
|
7930
|
+
subscribers: /* @__PURE__ */ new Set(),
|
|
7931
|
+
lastUpdated: 0,
|
|
7932
|
+
updateCount: 0
|
|
7933
|
+
});
|
|
7934
|
+
}
|
|
7935
|
+
let facts = [];
|
|
7936
|
+
const factMap = /* @__PURE__ */ new Map();
|
|
7937
|
+
const timeline = [];
|
|
7938
|
+
const maxTimeline = 1e4;
|
|
7939
|
+
function recordTimeline(path3, kind, data) {
|
|
7940
|
+
const entry = {
|
|
7941
|
+
id: nextId(),
|
|
7942
|
+
timestamp: Date.now(),
|
|
7943
|
+
path: path3,
|
|
7944
|
+
kind,
|
|
7945
|
+
data
|
|
7946
|
+
};
|
|
7947
|
+
timeline.push(entry);
|
|
7948
|
+
if (timeline.length > maxTimeline) {
|
|
7949
|
+
timeline.splice(0, timeline.length - maxTimeline);
|
|
7950
|
+
}
|
|
7951
|
+
}
|
|
7952
|
+
const ruleStates = (config.rules ?? []).map((rule) => ({
|
|
7953
|
+
rule,
|
|
7954
|
+
lastResult: null
|
|
7955
|
+
}));
|
|
7956
|
+
const constraints = config.constraints ?? [];
|
|
7957
|
+
const livenessConfig = config.liveness;
|
|
7958
|
+
const initTime = Date.now();
|
|
7959
|
+
let livenessTimer = null;
|
|
7960
|
+
if (livenessConfig) {
|
|
7961
|
+
const timeout = livenessConfig.timeoutMs ?? 5e3;
|
|
7962
|
+
livenessTimer = setTimeout(() => {
|
|
7963
|
+
for (const expectedPath of livenessConfig.expect) {
|
|
7964
|
+
const state2 = paths.get(expectedPath);
|
|
7965
|
+
if (!state2 || state2.updateCount === 0) {
|
|
7966
|
+
const elapsed = Date.now() - initTime;
|
|
7967
|
+
recordTimeline(expectedPath, "liveness", {
|
|
7968
|
+
stale: true,
|
|
7969
|
+
elapsed,
|
|
7970
|
+
message: `Path "${expectedPath}" never updated after ${elapsed}ms`
|
|
7971
|
+
});
|
|
7972
|
+
livenessConfig.onStale?.(expectedPath, elapsed);
|
|
7973
|
+
}
|
|
7974
|
+
}
|
|
7975
|
+
}, timeout);
|
|
7976
|
+
}
|
|
7977
|
+
function getPathValues(watchPaths) {
|
|
7978
|
+
const values = {};
|
|
7979
|
+
for (const p of watchPaths) {
|
|
7980
|
+
const state2 = paths.get(p);
|
|
7981
|
+
values[p] = state2 ? state2.value : void 0;
|
|
7982
|
+
}
|
|
7983
|
+
return values;
|
|
7984
|
+
}
|
|
7985
|
+
function notify(path3, value) {
|
|
7986
|
+
const state2 = paths.get(path3);
|
|
7987
|
+
if (!state2) return;
|
|
7988
|
+
for (const cb of state2.subscribers) {
|
|
7989
|
+
try {
|
|
7990
|
+
cb(value);
|
|
7991
|
+
} catch (err) {
|
|
7992
|
+
console.error(`[praxis] Subscriber error for "${path3}":`, err);
|
|
7993
|
+
}
|
|
7994
|
+
}
|
|
7995
|
+
}
|
|
7996
|
+
function checkConstraints(path3, value) {
|
|
7997
|
+
const violations = [];
|
|
7998
|
+
for (const c of constraints) {
|
|
7999
|
+
if (!c.watch.includes(path3)) continue;
|
|
8000
|
+
const values = getPathValues(c.watch);
|
|
8001
|
+
values[path3] = value;
|
|
8002
|
+
try {
|
|
8003
|
+
const result = c.validate(values);
|
|
8004
|
+
if (result !== true) {
|
|
8005
|
+
violations.push({
|
|
8006
|
+
kind: "constraint-violation",
|
|
8007
|
+
message: result,
|
|
8008
|
+
data: { constraintId: c.id, path: path3, description: c.description }
|
|
8009
|
+
});
|
|
8010
|
+
recordTimeline(path3, "constraint-check", {
|
|
8011
|
+
constraintId: c.id,
|
|
8012
|
+
violated: true,
|
|
8013
|
+
message: result
|
|
8014
|
+
});
|
|
8015
|
+
}
|
|
8016
|
+
} catch (err) {
|
|
8017
|
+
violations.push({
|
|
8018
|
+
kind: "constraint-violation",
|
|
8019
|
+
message: `Constraint "${c.id}" threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
8020
|
+
data: { constraintId: c.id, error: err }
|
|
8021
|
+
});
|
|
8022
|
+
}
|
|
8023
|
+
}
|
|
8024
|
+
return violations;
|
|
8025
|
+
}
|
|
8026
|
+
function evaluateRules() {
|
|
8027
|
+
const newFacts = [];
|
|
8028
|
+
const retractions = [];
|
|
8029
|
+
for (const rs of ruleStates) {
|
|
8030
|
+
const values = getPathValues(rs.rule.watch);
|
|
8031
|
+
try {
|
|
8032
|
+
const result = rs.rule.evaluate(values, [...facts]);
|
|
8033
|
+
rs.lastResult = result;
|
|
8034
|
+
recordTimeline(rs.rule.watch[0] ?? "*", "rule-eval", {
|
|
8035
|
+
ruleId: rs.rule.id,
|
|
8036
|
+
kind: result.kind,
|
|
8037
|
+
reason: result.reason
|
|
8038
|
+
});
|
|
8039
|
+
switch (result.kind) {
|
|
8040
|
+
case "emit":
|
|
8041
|
+
newFacts.push(...result.facts);
|
|
8042
|
+
break;
|
|
8043
|
+
case "retract":
|
|
8044
|
+
retractions.push(...result.retractTags);
|
|
8045
|
+
break;
|
|
8046
|
+
case "noop":
|
|
8047
|
+
case "skip":
|
|
8048
|
+
break;
|
|
8049
|
+
}
|
|
8050
|
+
} catch (err) {
|
|
8051
|
+
console.error(`[praxis] Rule "${rs.rule.id}" error:`, err);
|
|
8052
|
+
}
|
|
8053
|
+
}
|
|
8054
|
+
if (retractions.length > 0) {
|
|
8055
|
+
const retractSet = new Set(retractions);
|
|
8056
|
+
for (const tag of retractSet) {
|
|
8057
|
+
factMap.delete(tag);
|
|
8058
|
+
}
|
|
8059
|
+
}
|
|
8060
|
+
for (const f of newFacts) {
|
|
8061
|
+
factMap.set(f.tag, f);
|
|
8062
|
+
}
|
|
8063
|
+
facts = Array.from(factMap.values());
|
|
8064
|
+
}
|
|
8065
|
+
function query(path3, opts) {
|
|
8066
|
+
let state2 = paths.get(path3);
|
|
8067
|
+
if (!state2) {
|
|
8068
|
+
state2 = {
|
|
8069
|
+
schema: { path: path3, initial: void 0 },
|
|
8070
|
+
value: void 0,
|
|
8071
|
+
subscribers: /* @__PURE__ */ new Set(),
|
|
8072
|
+
lastUpdated: 0,
|
|
8073
|
+
updateCount: 0
|
|
8074
|
+
};
|
|
8075
|
+
paths.set(path3, state2);
|
|
8076
|
+
}
|
|
8077
|
+
const ref = {
|
|
8078
|
+
get current() {
|
|
8079
|
+
const s = paths.get(path3);
|
|
8080
|
+
return applyQueryOpts(s?.value ?? state2.schema.initial, opts);
|
|
8081
|
+
},
|
|
8082
|
+
subscribe(cb) {
|
|
8083
|
+
const wrappedCb = (rawValue) => {
|
|
8084
|
+
const processed = applyQueryOpts(rawValue, opts);
|
|
8085
|
+
cb(processed);
|
|
8086
|
+
};
|
|
8087
|
+
const s = paths.get(path3);
|
|
8088
|
+
s.subscribers.add(wrappedCb);
|
|
8089
|
+
try {
|
|
8090
|
+
cb(ref.current);
|
|
8091
|
+
} catch (err) {
|
|
8092
|
+
console.error(`[praxis] query("${path3}") subscriber init error:`, err);
|
|
8093
|
+
}
|
|
8094
|
+
return () => {
|
|
8095
|
+
s.subscribers.delete(wrappedCb);
|
|
8096
|
+
};
|
|
8097
|
+
}
|
|
8098
|
+
};
|
|
8099
|
+
return ref;
|
|
8100
|
+
}
|
|
8101
|
+
function applyQueryOpts(value, opts) {
|
|
8102
|
+
if (!opts || !Array.isArray(value)) return value;
|
|
8103
|
+
let result = [...value];
|
|
8104
|
+
if (opts.where) result = result.filter(opts.where);
|
|
8105
|
+
if (opts.sort) result.sort(opts.sort);
|
|
8106
|
+
if (opts.select) result = result.map(opts.select);
|
|
8107
|
+
if (opts.limit) result = result.slice(0, opts.limit);
|
|
8108
|
+
return result;
|
|
8109
|
+
}
|
|
8110
|
+
function mutateInternal(path3, value) {
|
|
8111
|
+
const violations = checkConstraints(path3, value);
|
|
8112
|
+
if (violations.length > 0) {
|
|
8113
|
+
return { violations, emittedFacts: [] };
|
|
8114
|
+
}
|
|
8115
|
+
const state2 = paths.get(path3);
|
|
8116
|
+
if (!state2) {
|
|
8117
|
+
paths.set(path3, {
|
|
8118
|
+
schema: { path: path3, initial: value },
|
|
8119
|
+
value,
|
|
8120
|
+
subscribers: /* @__PURE__ */ new Set(),
|
|
8121
|
+
lastUpdated: Date.now(),
|
|
8122
|
+
updateCount: 1
|
|
8123
|
+
});
|
|
8124
|
+
} else {
|
|
8125
|
+
const before = state2.value;
|
|
8126
|
+
state2.value = value;
|
|
8127
|
+
state2.lastUpdated = Date.now();
|
|
8128
|
+
state2.updateCount++;
|
|
8129
|
+
recordTimeline(path3, "mutation", {
|
|
8130
|
+
before: summarize(before),
|
|
8131
|
+
after: summarize(value)
|
|
8132
|
+
});
|
|
8133
|
+
}
|
|
8134
|
+
notify(path3, value);
|
|
8135
|
+
const factsBefore = facts.length;
|
|
8136
|
+
evaluateRules();
|
|
8137
|
+
const emittedFacts = facts.slice(factsBefore);
|
|
8138
|
+
return { violations: [], emittedFacts };
|
|
8139
|
+
}
|
|
8140
|
+
function mutate(path3, value) {
|
|
8141
|
+
const { violations, emittedFacts } = mutateInternal(path3, value);
|
|
8142
|
+
return {
|
|
8143
|
+
accepted: violations.length === 0,
|
|
8144
|
+
violations,
|
|
8145
|
+
facts: emittedFacts
|
|
8146
|
+
};
|
|
8147
|
+
}
|
|
8148
|
+
function batchMutate(fn) {
|
|
8149
|
+
const allViolations = [];
|
|
8150
|
+
const pendingWrites = [];
|
|
8151
|
+
fn((path3, value) => {
|
|
8152
|
+
const violations = checkConstraints(path3, value);
|
|
8153
|
+
if (violations.length > 0) {
|
|
8154
|
+
allViolations.push(...violations);
|
|
8155
|
+
} else {
|
|
8156
|
+
pendingWrites.push({ path: path3, value });
|
|
8157
|
+
}
|
|
8158
|
+
});
|
|
8159
|
+
if (allViolations.length > 0) {
|
|
8160
|
+
return { accepted: false, violations: allViolations, facts: [] };
|
|
8161
|
+
}
|
|
8162
|
+
for (const { path: path3, value } of pendingWrites) {
|
|
8163
|
+
const state2 = paths.get(path3);
|
|
8164
|
+
if (state2) {
|
|
8165
|
+
const before = state2.value;
|
|
8166
|
+
state2.value = value;
|
|
8167
|
+
state2.lastUpdated = Date.now();
|
|
8168
|
+
state2.updateCount++;
|
|
8169
|
+
recordTimeline(path3, "mutation", {
|
|
8170
|
+
before: summarize(before),
|
|
8171
|
+
after: summarize(value)
|
|
8172
|
+
});
|
|
8173
|
+
} else {
|
|
8174
|
+
paths.set(path3, {
|
|
8175
|
+
schema: { path: path3, initial: value },
|
|
8176
|
+
value,
|
|
8177
|
+
subscribers: /* @__PURE__ */ new Set(),
|
|
8178
|
+
lastUpdated: Date.now(),
|
|
8179
|
+
updateCount: 1
|
|
8180
|
+
});
|
|
8181
|
+
}
|
|
8182
|
+
}
|
|
8183
|
+
for (const { path: path3, value } of pendingWrites) {
|
|
8184
|
+
notify(path3, value);
|
|
8185
|
+
}
|
|
8186
|
+
const factsBefore = facts.length;
|
|
8187
|
+
evaluateRules();
|
|
8188
|
+
return {
|
|
8189
|
+
accepted: true,
|
|
8190
|
+
violations: [],
|
|
8191
|
+
facts: facts.slice(factsBefore)
|
|
8192
|
+
};
|
|
8193
|
+
}
|
|
8194
|
+
function getLiveness() {
|
|
8195
|
+
const result = {};
|
|
8196
|
+
const now = Date.now();
|
|
8197
|
+
const watchPaths = livenessConfig?.expect ?? [];
|
|
8198
|
+
for (const p of watchPaths) {
|
|
8199
|
+
const state2 = paths.get(p);
|
|
8200
|
+
const lastUpdated = state2?.lastUpdated ?? 0;
|
|
8201
|
+
const elapsed = lastUpdated > 0 ? now - lastUpdated : now - initTime;
|
|
8202
|
+
const timeout = livenessConfig?.timeoutMs ?? 5e3;
|
|
8203
|
+
result[p] = {
|
|
8204
|
+
stale: state2?.updateCount === 0 || elapsed > timeout,
|
|
8205
|
+
lastUpdated,
|
|
8206
|
+
elapsed
|
|
8207
|
+
};
|
|
8208
|
+
}
|
|
8209
|
+
return result;
|
|
8210
|
+
}
|
|
8211
|
+
function destroy() {
|
|
8212
|
+
if (livenessTimer) clearTimeout(livenessTimer);
|
|
8213
|
+
for (const state2 of paths.values()) {
|
|
8214
|
+
state2.subscribers.clear();
|
|
8215
|
+
}
|
|
8216
|
+
paths.clear();
|
|
8217
|
+
facts = [];
|
|
8218
|
+
factMap.clear();
|
|
8219
|
+
timeline.length = 0;
|
|
8220
|
+
}
|
|
8221
|
+
return {
|
|
8222
|
+
query,
|
|
8223
|
+
mutate,
|
|
8224
|
+
batch: batchMutate,
|
|
8225
|
+
facts: () => [...facts],
|
|
8226
|
+
violations: () => {
|
|
8227
|
+
const allViolations = [];
|
|
8228
|
+
for (const c of constraints) {
|
|
8229
|
+
const values = getPathValues(c.watch);
|
|
8230
|
+
try {
|
|
8231
|
+
const result = c.validate(values);
|
|
8232
|
+
if (result !== true) {
|
|
8233
|
+
allViolations.push({
|
|
8234
|
+
kind: "constraint-violation",
|
|
8235
|
+
message: result,
|
|
8236
|
+
data: { constraintId: c.id }
|
|
8237
|
+
});
|
|
8238
|
+
}
|
|
8239
|
+
} catch {
|
|
8240
|
+
}
|
|
8241
|
+
}
|
|
8242
|
+
return allViolations;
|
|
8243
|
+
},
|
|
8244
|
+
timeline: () => [...timeline],
|
|
8245
|
+
evaluate: evaluateRules,
|
|
8246
|
+
destroy,
|
|
8247
|
+
liveness: getLiveness
|
|
8248
|
+
};
|
|
8249
|
+
}
|
|
8250
|
+
function summarize(value) {
|
|
8251
|
+
if (value === null || value === void 0) return value;
|
|
8252
|
+
if (typeof value !== "object") return value;
|
|
8253
|
+
if (Array.isArray(value)) return `[Array(${value.length})]`;
|
|
8254
|
+
const keys = Object.keys(value);
|
|
8255
|
+
if (keys.length > 10) return `{Object(${keys.length} keys)}`;
|
|
8256
|
+
return value;
|
|
8257
|
+
}
|
|
8258
|
+
|
|
8259
|
+
// src/unified/types.ts
|
|
8260
|
+
function definePath(path3, initial, opts) {
|
|
8261
|
+
return { path: path3, initial, ...opts };
|
|
8262
|
+
}
|
|
8263
|
+
|
|
8264
|
+
// src/unified/rules.ts
|
|
8265
|
+
init_rule_result();
|
|
8266
|
+
function defineRule2(rule) {
|
|
8267
|
+
return rule;
|
|
8268
|
+
}
|
|
8269
|
+
function defineConstraint2(constraint) {
|
|
8270
|
+
return constraint;
|
|
8271
|
+
}
|
|
8272
|
+
function defineModule2(name, rules) {
|
|
8273
|
+
return { name, rules };
|
|
8274
|
+
}
|
|
6196
8275
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6197
8276
|
0 && (module.exports = {
|
|
6198
8277
|
AcknowledgeContractGap,
|
|
@@ -6218,12 +8297,15 @@ function expectationGate(config = {}) {
|
|
|
6218
8297
|
PraxisDBStore,
|
|
6219
8298
|
PraxisRegistry,
|
|
6220
8299
|
PraxisSchemaRegistry,
|
|
8300
|
+
ProjectChronicle,
|
|
6221
8301
|
ReactiveLogicEngine,
|
|
6222
8302
|
RegistryIntrospector,
|
|
6223
8303
|
RuleResult,
|
|
6224
8304
|
StateDocsGenerator,
|
|
6225
8305
|
TerminalAdapter,
|
|
8306
|
+
Timeline,
|
|
6226
8307
|
ValidateContracts,
|
|
8308
|
+
analyzeDependencyGraph,
|
|
6227
8309
|
attachAllIntegrations,
|
|
6228
8310
|
attachTauriToEngine,
|
|
6229
8311
|
attachToEngine,
|
|
@@ -6234,6 +8316,7 @@ function expectationGate(config = {}) {
|
|
|
6234
8316
|
canvasToSchema,
|
|
6235
8317
|
canvasToYaml,
|
|
6236
8318
|
commitFromState,
|
|
8319
|
+
createApp,
|
|
6237
8320
|
createBehaviorLedger,
|
|
6238
8321
|
createCanvasEditor,
|
|
6239
8322
|
createChronicle,
|
|
@@ -6249,16 +8332,19 @@ function expectationGate(config = {}) {
|
|
|
6249
8332
|
createPraxisDBStore,
|
|
6250
8333
|
createPraxisEngine,
|
|
6251
8334
|
createPraxisLocalFirst,
|
|
8335
|
+
createProjectChronicle,
|
|
6252
8336
|
createReactiveEngine,
|
|
6253
8337
|
createSchemaRegistry,
|
|
6254
8338
|
createSchemaTemplate,
|
|
6255
8339
|
createStateDocsGenerator,
|
|
6256
8340
|
createTauriPraxisAdapter,
|
|
6257
8341
|
createTerminalAdapter,
|
|
8342
|
+
createTimeline,
|
|
6258
8343
|
createTimerActor,
|
|
6259
8344
|
createUIModule,
|
|
6260
8345
|
createUnifiedApp,
|
|
6261
8346
|
createUnumAdapter,
|
|
8347
|
+
crossReferenceContracts,
|
|
6262
8348
|
dataRules,
|
|
6263
8349
|
defineConstraint,
|
|
6264
8350
|
defineContract,
|
|
@@ -6266,18 +8352,38 @@ function expectationGate(config = {}) {
|
|
|
6266
8352
|
defineFact,
|
|
6267
8353
|
defineGate,
|
|
6268
8354
|
defineModule,
|
|
8355
|
+
definePath,
|
|
6269
8356
|
defineRule,
|
|
8357
|
+
defineUnifiedConstraint,
|
|
8358
|
+
defineUnifiedModule,
|
|
8359
|
+
defineUnifiedRule,
|
|
8360
|
+
diffContracts,
|
|
8361
|
+
diffExpectations,
|
|
8362
|
+
diffLedgers,
|
|
8363
|
+
diffRegistries,
|
|
6270
8364
|
dirtyGuardRule,
|
|
8365
|
+
enableProjectChronicle,
|
|
6271
8366
|
errorDisplayRule,
|
|
6272
8367
|
expectBehavior,
|
|
6273
8368
|
expectationGate,
|
|
6274
8369
|
fact,
|
|
6275
8370
|
filterEvents,
|
|
6276
8371
|
filterFacts,
|
|
8372
|
+
findContractGaps,
|
|
8373
|
+
findContradictions,
|
|
8374
|
+
findDeadRules,
|
|
6277
8375
|
findEvent,
|
|
6278
8376
|
findFact,
|
|
8377
|
+
findGaps,
|
|
8378
|
+
findShadowedRules,
|
|
8379
|
+
findUnreachableStates,
|
|
6279
8380
|
formRules,
|
|
8381
|
+
formatBehavioralCommit,
|
|
8382
|
+
formatBuildOutput,
|
|
8383
|
+
formatDelta,
|
|
6280
8384
|
formatGate,
|
|
8385
|
+
formatLedger,
|
|
8386
|
+
formatReleaseNotes,
|
|
6281
8387
|
formatReport,
|
|
6282
8388
|
formatValidationReport,
|
|
6283
8389
|
formatValidationReportJSON,
|
|
@@ -6285,6 +8391,7 @@ function expectationGate(config = {}) {
|
|
|
6285
8391
|
formatVerificationReport,
|
|
6286
8392
|
generateDocs,
|
|
6287
8393
|
generateId,
|
|
8394
|
+
generateLedger,
|
|
6288
8395
|
generateTauriConfig,
|
|
6289
8396
|
getContract,
|
|
6290
8397
|
getEventPath,
|
|
@@ -6304,12 +8411,17 @@ function expectationGate(config = {}) {
|
|
|
6304
8411
|
navigationRules,
|
|
6305
8412
|
noInteractionWhileLoadingConstraint,
|
|
6306
8413
|
offlineIndicatorRule,
|
|
8414
|
+
recordAudit,
|
|
6307
8415
|
registerSchema,
|
|
6308
8416
|
resizeEvent,
|
|
6309
8417
|
runTerminalCommand,
|
|
6310
8418
|
schemaToCanvas,
|
|
6311
8419
|
semverContract,
|
|
8420
|
+
suggest,
|
|
8421
|
+
suggestAll,
|
|
6312
8422
|
toastRules,
|
|
8423
|
+
traceDerivation,
|
|
8424
|
+
traceImpact,
|
|
6313
8425
|
uiModule,
|
|
6314
8426
|
uiStateChanged,
|
|
6315
8427
|
validateContracts,
|
|
@@ -6317,5 +8429,7 @@ function expectationGate(config = {}) {
|
|
|
6317
8429
|
validateSchema,
|
|
6318
8430
|
validateWithGuardian,
|
|
6319
8431
|
verify,
|
|
8432
|
+
verifyContractExamples,
|
|
8433
|
+
verifyInvariants,
|
|
6320
8434
|
viewportRule
|
|
6321
8435
|
});
|