@plures/praxis 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/node/chunk-2IUFZBH3.js +87 -0
  2. package/dist/node/{chunk-WZ6B3LZ6.js → chunk-7CSWBDFL.js} +3 -56
  3. package/dist/node/{chunk-5JQJZADT.js → chunk-7M3HV4XR.js} +3 -3
  4. package/dist/node/{chunk-PTH6MD6P.js → chunk-FWOXU4MM.js} +1 -1
  5. package/dist/node/chunk-PGVSB6NR.js +59 -0
  6. package/dist/node/cli/index.cjs +1078 -211
  7. package/dist/node/cli/index.js +21 -2
  8. package/dist/node/index.cjs +1111 -0
  9. package/dist/node/index.d.cts +499 -1
  10. package/dist/node/index.d.ts +499 -1
  11. package/dist/node/index.js +1092 -78
  12. package/dist/node/integrations/svelte.js +2 -2
  13. package/dist/node/{reverse-W7THPV45.js → reverse-YD3CWIGM.js} +3 -2
  14. package/dist/node/rules-4DAJ4Z4N.js +7 -0
  15. package/dist/node/server-SYZPDULV.js +361 -0
  16. package/dist/node/{validate-EN3M4FUR.js → validate-TQGVIG7G.js} +4 -3
  17. package/package.json +28 -2
  18. package/src/__tests__/expectations.test.ts +364 -0
  19. package/src/__tests__/factory.test.ts +426 -0
  20. package/src/__tests__/mcp-server.test.ts +310 -0
  21. package/src/__tests__/project.test.ts +396 -0
  22. package/src/cli/index.ts +28 -0
  23. package/src/expectations/expectations.ts +471 -0
  24. package/src/expectations/index.ts +29 -0
  25. package/src/expectations/types.ts +95 -0
  26. package/src/factory/factory.ts +634 -0
  27. package/src/factory/index.ts +27 -0
  28. package/src/factory/types.ts +64 -0
  29. package/src/index.ts +57 -0
  30. package/src/mcp/index.ts +33 -0
  31. package/src/mcp/server.ts +485 -0
  32. package/src/mcp/types.ts +161 -0
  33. package/src/project/index.ts +31 -0
  34. package/src/project/project.ts +423 -0
  35. package/src/project/types.ts +87 -0
  36. /package/dist/node/{chunk-R2PSBPKQ.js → chunk-TEMFJOIH.js} +0 -0
@@ -1,3 +1,7 @@
1
+ import {
2
+ auditCompleteness,
3
+ formatReport
4
+ } from "./chunk-2IUFZBH3.js";
1
5
  import {
2
6
  AcknowledgeContractGap,
3
7
  BehaviorLedger,
@@ -21,12 +25,13 @@ import {
21
25
  formatValidationReportJSON,
22
26
  formatValidationReportSARIF,
23
27
  validateContracts
24
- } from "./chunk-PTH6MD6P.js";
28
+ } from "./chunk-FWOXU4MM.js";
29
+ import "./chunk-7CSWBDFL.js";
25
30
  import {
26
31
  defineContract,
27
32
  getContract,
28
33
  isContract
29
- } from "./chunk-WZ6B3LZ6.js";
34
+ } from "./chunk-PGVSB6NR.js";
30
35
  import {
31
36
  InMemoryPraxisDB,
32
37
  PluresDBPraxisAdapter,
@@ -50,10 +55,14 @@ import {
50
55
  import {
51
56
  ReactiveLogicEngine,
52
57
  createReactiveEngine
53
- } from "./chunk-5JQJZADT.js";
58
+ } from "./chunk-7M3HV4XR.js";
54
59
  import {
55
- PraxisRegistry
56
- } from "./chunk-R2PSBPKQ.js";
60
+ LogicEngine,
61
+ PRAXIS_PROTOCOL_VERSION,
62
+ RuleResult,
63
+ createPraxisEngine,
64
+ fact
65
+ } from "./chunk-KMJWAFZV.js";
57
66
  import {
58
67
  TerminalAdapter,
59
68
  createMockExecutor,
@@ -76,12 +85,8 @@ import {
76
85
  validateSchema
77
86
  } from "./chunk-UATVJBNV.js";
78
87
  import {
79
- LogicEngine,
80
- PRAXIS_PROTOCOL_VERSION,
81
- RuleResult,
82
- createPraxisEngine,
83
- fact
84
- } from "./chunk-KMJWAFZV.js";
88
+ PraxisRegistry
89
+ } from "./chunk-TEMFJOIH.js";
85
90
  import "./chunk-QGM4M3NI.js";
86
91
 
87
92
  // src/core/reactive-engine.ts
@@ -2114,87 +2119,1079 @@ function resizeEvent(width, height) {
2114
2119
  return { tag: "ui.resize", payload: { width, height } };
2115
2120
  }
2116
2121
 
2117
- // src/core/completeness.ts
2118
- function auditCompleteness(manifest, registryRuleIds, registryConstraintIds, rulesWithContracts, config) {
2119
- const threshold = config?.threshold ?? 90;
2120
- const domainBranches = manifest.branches.filter((b) => b.kind === "domain");
2121
- const coveredDomain = domainBranches.filter((b) => b.coveredBy && registryRuleIds.includes(b.coveredBy));
2122
- const uncoveredDomain = domainBranches.filter((b) => !b.coveredBy || !registryRuleIds.includes(b.coveredBy));
2123
- const invariantBranches = manifest.branches.filter((b) => b.kind === "invariant");
2124
- const coveredInvariants = invariantBranches.filter((b) => b.coveredBy && registryConstraintIds.includes(b.coveredBy));
2125
- const uncoveredInvariants = invariantBranches.filter((b) => !b.coveredBy || !registryConstraintIds.includes(b.coveredBy));
2126
- const needContracts = manifest.rulesNeedingContracts;
2127
- const haveContracts = needContracts.filter((id) => rulesWithContracts.includes(id));
2128
- const missingContracts = needContracts.filter((id) => !rulesWithContracts.includes(id));
2129
- const neededFields = manifest.stateFields.filter((f) => f.usedByRule);
2130
- const coveredFields = neededFields.filter((f) => f.inContext);
2131
- const missingFields = neededFields.filter((f) => !f.inContext);
2132
- const coveredTransitions = manifest.transitions.filter((t) => t.eventTag);
2133
- const missingTransitions = manifest.transitions.filter((t) => !t.eventTag);
2134
- const ruleScore = domainBranches.length > 0 ? coveredDomain.length / domainBranches.length * 40 : 40;
2135
- const constraintScore = invariantBranches.length > 0 ? coveredInvariants.length / invariantBranches.length * 20 : 20;
2136
- const contractScore = needContracts.length > 0 ? haveContracts.length / needContracts.length * 15 : 15;
2137
- const contextScore = neededFields.length > 0 ? coveredFields.length / neededFields.length * 15 : 15;
2138
- const eventScore = manifest.transitions.length > 0 ? coveredTransitions.length / manifest.transitions.length * 10 : 10;
2139
- const score = Math.round(ruleScore + constraintScore + contractScore + contextScore + eventScore);
2140
- const rating = score >= 90 ? "complete" : score >= 70 ? "good" : score >= 50 ? "partial" : "incomplete";
2141
- const report = {
2142
- score,
2143
- rating,
2144
- rules: { total: domainBranches.length, covered: coveredDomain.length, uncovered: uncoveredDomain },
2145
- constraints: { total: invariantBranches.length, covered: coveredInvariants.length, uncovered: uncoveredInvariants },
2146
- contracts: { total: needContracts.length, withContracts: haveContracts.length, missing: missingContracts },
2147
- context: { total: neededFields.length, covered: coveredFields.length, missing: missingFields },
2148
- events: { total: manifest.transitions.length, covered: coveredTransitions.length, missing: missingTransitions }
2122
+ // src/expectations/expectations.ts
2123
+ var Expectation = class {
2124
+ name;
2125
+ _conditions = [];
2126
+ constructor(name) {
2127
+ this.name = name;
2128
+ }
2129
+ /**
2130
+ * Declare that this behavior should ONLY occur when a condition is true.
2131
+ * If the condition is false, the behavior should NOT occur.
2132
+ */
2133
+ onlyWhen(condition) {
2134
+ this._conditions.push({ description: condition, type: "onlyWhen" });
2135
+ return this;
2136
+ }
2137
+ /**
2138
+ * Declare that this behavior should NEVER occur under a given condition.
2139
+ */
2140
+ never(condition) {
2141
+ this._conditions.push({ description: condition, type: "never" });
2142
+ return this;
2143
+ }
2144
+ /**
2145
+ * Declare that this behavior should ALWAYS have a certain property.
2146
+ */
2147
+ always(condition) {
2148
+ this._conditions.push({ description: condition, type: "always" });
2149
+ return this;
2150
+ }
2151
+ /** Get all declared conditions. */
2152
+ get conditions() {
2153
+ return this._conditions;
2154
+ }
2155
+ };
2156
+ var ExpectationSet = class {
2157
+ name;
2158
+ description;
2159
+ _expectations = [];
2160
+ constructor(options) {
2161
+ this.name = options.name;
2162
+ this.description = options.description ?? "";
2163
+ }
2164
+ /** Add an expectation to the set. */
2165
+ add(expectation) {
2166
+ this._expectations.push(expectation);
2167
+ return this;
2168
+ }
2169
+ /** Get all expectations in this set. */
2170
+ get expectations() {
2171
+ return this._expectations;
2172
+ }
2173
+ /** Number of expectations. */
2174
+ get size() {
2175
+ return this._expectations.length;
2176
+ }
2177
+ };
2178
+ function expectBehavior(name) {
2179
+ return new Expectation(name);
2180
+ }
2181
+ function verify(registry, expectations) {
2182
+ const rules = registry.getAllRules();
2183
+ const constraints = registry.getAllConstraints();
2184
+ const ruleIds = registry.getRuleIds();
2185
+ const constraintIds = registry.getConstraintIds();
2186
+ const allDescriptors = [...rules, ...constraints];
2187
+ const expectationResults = [];
2188
+ for (const exp of expectations.expectations) {
2189
+ const result = verifyExpectation(exp, allDescriptors, ruleIds, constraintIds);
2190
+ expectationResults.push(result);
2191
+ }
2192
+ const satisfied = expectationResults.filter((r) => r.status === "satisfied").length;
2193
+ const violated = expectationResults.filter((r) => r.status === "violated").length;
2194
+ const partial = expectationResults.filter((r) => r.status === "partial").length;
2195
+ const allEdgeCases = expectationResults.flatMap((r) => r.edgeCases);
2196
+ const allMitigations = expectationResults.flatMap((r) => r.mitigations);
2197
+ return {
2198
+ setName: expectations.name,
2199
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2200
+ status: violated > 0 ? "violated" : partial > 0 ? "partial" : "satisfied",
2201
+ expectations: expectationResults,
2202
+ summary: {
2203
+ total: expectationResults.length,
2204
+ satisfied,
2205
+ violated,
2206
+ partial
2207
+ },
2208
+ allEdgeCases,
2209
+ allMitigations
2149
2210
  };
2150
- if (config?.strict && score < threshold) {
2151
- throw new Error(`Praxis completeness ${score}/100 (${rating}) \u2014 below threshold ${threshold}. ${uncoveredDomain.length} uncovered rules, ${uncoveredInvariants.length} uncovered invariants, ${missingContracts.length} missing contracts.`);
2211
+ }
2212
+ function verifyExpectation(expectation, descriptors, ruleIds, _constraintIds) {
2213
+ const conditionResults = [];
2214
+ const edgeCases = [];
2215
+ const mitigations = [];
2216
+ const related = findRelatedDescriptors(expectation.name, descriptors);
2217
+ for (const condition of expectation.conditions) {
2218
+ const result = verifyCondition(condition, expectation.name, related, ruleIds);
2219
+ conditionResults.push(result);
2220
+ if (result.status === "unverifiable") {
2221
+ edgeCases.push(`Cannot verify "${condition.description}" for "${expectation.name}" \u2014 no covering rule/contract found`);
2222
+ mitigations.push(`Add a rule or constraint that explicitly covers: ${condition.description}`);
2223
+ } else if (result.status === "violated") {
2224
+ edgeCases.push(`"${expectation.name}" may fire incorrectly: ${condition.description}`);
2225
+ mitigations.push(`Review rule logic for "${expectation.name}" regarding: ${condition.description}`);
2226
+ }
2227
+ }
2228
+ const satisfiedCount = conditionResults.filter((r) => r.status === "satisfied").length;
2229
+ const violatedCount = conditionResults.filter((r) => r.status === "violated").length;
2230
+ const total = conditionResults.length;
2231
+ let status;
2232
+ if (total === 0) {
2233
+ status = "satisfied";
2234
+ } else if (violatedCount > 0) {
2235
+ status = "violated";
2236
+ } else if (satisfiedCount === total) {
2237
+ status = "satisfied";
2238
+ } else {
2239
+ status = "partial";
2152
2240
  }
2153
- return report;
2241
+ return {
2242
+ name: expectation.name,
2243
+ status,
2244
+ conditions: conditionResults,
2245
+ edgeCases,
2246
+ mitigations
2247
+ };
2154
2248
  }
2155
- function formatReport(report) {
2249
+ function textOverlaps(a, b) {
2250
+ const stopWords = /* @__PURE__ */ new Set([
2251
+ "the",
2252
+ "a",
2253
+ "an",
2254
+ "is",
2255
+ "are",
2256
+ "was",
2257
+ "were",
2258
+ "be",
2259
+ "been",
2260
+ "being",
2261
+ "have",
2262
+ "has",
2263
+ "had",
2264
+ "do",
2265
+ "does",
2266
+ "did",
2267
+ "will",
2268
+ "would",
2269
+ "could",
2270
+ "should",
2271
+ "may",
2272
+ "might",
2273
+ "shall",
2274
+ "can",
2275
+ "to",
2276
+ "of",
2277
+ "in",
2278
+ "for",
2279
+ "on",
2280
+ "with",
2281
+ "at",
2282
+ "by",
2283
+ "from",
2284
+ "as",
2285
+ "into",
2286
+ "through",
2287
+ "during",
2288
+ "before",
2289
+ "after",
2290
+ "when",
2291
+ "that",
2292
+ "this",
2293
+ "it",
2294
+ "its",
2295
+ "and",
2296
+ "or",
2297
+ "but",
2298
+ "not",
2299
+ "no",
2300
+ "if",
2301
+ "then",
2302
+ "than",
2303
+ "so",
2304
+ "up",
2305
+ "out",
2306
+ "about",
2307
+ "just",
2308
+ "must"
2309
+ ]);
2310
+ const wordsA = a.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
2311
+ const wordsB = b.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
2312
+ if (wordsA.length === 0 || wordsB.length === 0) return false;
2313
+ const matches = wordsA.filter(
2314
+ (wa) => wordsB.some((wb) => wa.startsWith(wb.slice(0, 4)) || wb.startsWith(wa.slice(0, 4)))
2315
+ );
2316
+ const minWords = Math.min(wordsA.length, wordsB.length);
2317
+ return matches.length >= Math.max(1, Math.ceil(minWords * 0.5));
2318
+ }
2319
+ function verifyCondition(condition, expectationName, relatedDescriptors, _ruleIds) {
2320
+ if (relatedDescriptors.length === 0) {
2321
+ return {
2322
+ condition,
2323
+ status: "unverifiable",
2324
+ explanation: `No rules or constraints found related to "${expectationName}"`,
2325
+ relatedRules: []
2326
+ };
2327
+ }
2328
+ const relatedIds = relatedDescriptors.map((d) => d.id);
2329
+ const condLower = condition.description.toLowerCase();
2330
+ const matches = (target) => target.toLowerCase().includes(condLower) || condLower.includes(target.toLowerCase()) || textOverlaps(condLower, target);
2331
+ switch (condition.type) {
2332
+ case "onlyWhen": {
2333
+ const coveringRule = relatedDescriptors.find(
2334
+ (d) => d.contract?.examples.some(
2335
+ (ex) => matches(ex.given) || matches(ex.when)
2336
+ ) || d.contract?.invariants.some((inv) => matches(inv))
2337
+ );
2338
+ if (coveringRule) {
2339
+ return {
2340
+ condition,
2341
+ status: "satisfied",
2342
+ explanation: `Rule "${coveringRule.id}" contract covers precondition: ${condition.description}`,
2343
+ relatedRules: relatedIds
2344
+ };
2345
+ }
2346
+ const descMatch = relatedDescriptors.find((d) => matches(d.description));
2347
+ if (descMatch) {
2348
+ return {
2349
+ condition,
2350
+ status: "satisfied",
2351
+ explanation: `Rule "${descMatch.id}" description addresses: ${condition.description}`,
2352
+ relatedRules: relatedIds
2353
+ };
2354
+ }
2355
+ return {
2356
+ condition,
2357
+ status: "unverifiable",
2358
+ explanation: `No rule contract explicitly covers the precondition: ${condition.description}`,
2359
+ relatedRules: relatedIds
2360
+ };
2361
+ }
2362
+ case "never": {
2363
+ const preventingDescriptor = relatedDescriptors.find(
2364
+ (d) => d.contract?.invariants.some((inv) => matches(inv)) || d.contract?.examples.some(
2365
+ (ex) => (matches(ex.given) || matches(ex.when)) && (ex.then.toLowerCase().includes("retract") || ex.then.toLowerCase().includes("fail") || ex.then.toLowerCase().includes("violation") || ex.then.toLowerCase().includes("skip") || ex.then.toLowerCase().includes("block") || ex.then.toLowerCase().includes("no "))
2366
+ ) || d.contract?.behavior.toLowerCase().includes("block") || d.contract?.behavior.toLowerCase().includes("prevent")
2367
+ );
2368
+ if (preventingDescriptor) {
2369
+ return {
2370
+ condition,
2371
+ status: "satisfied",
2372
+ explanation: `Constraint/rule "${preventingDescriptor.id}" prevents: ${condition.description}`,
2373
+ relatedRules: relatedIds
2374
+ };
2375
+ }
2376
+ return {
2377
+ condition,
2378
+ status: "unverifiable",
2379
+ explanation: `No rule or constraint explicitly prevents: ${condition.description}`,
2380
+ relatedRules: relatedIds
2381
+ };
2382
+ }
2383
+ case "always": {
2384
+ const guaranteeing = relatedDescriptors.find(
2385
+ (d) => d.contract?.invariants.some((inv) => matches(inv)) || d.contract?.behavior && matches(d.contract.behavior)
2386
+ );
2387
+ if (guaranteeing) {
2388
+ return {
2389
+ condition,
2390
+ status: "satisfied",
2391
+ explanation: `Rule "${guaranteeing.id}" guarantees: ${condition.description}`,
2392
+ relatedRules: relatedIds
2393
+ };
2394
+ }
2395
+ const exampleMatch = relatedDescriptors.find(
2396
+ (d) => d.contract?.examples.some((ex) => matches(ex.then))
2397
+ );
2398
+ if (exampleMatch) {
2399
+ return {
2400
+ condition,
2401
+ status: "satisfied",
2402
+ explanation: `Rule "${exampleMatch.id}" example demonstrates: ${condition.description}`,
2403
+ relatedRules: relatedIds
2404
+ };
2405
+ }
2406
+ return {
2407
+ condition,
2408
+ status: "unverifiable",
2409
+ explanation: `No rule contract guarantees: ${condition.description}`,
2410
+ relatedRules: relatedIds
2411
+ };
2412
+ }
2413
+ }
2414
+ }
2415
+ function findRelatedDescriptors(expectationName, descriptors) {
2416
+ const nameLower = expectationName.toLowerCase();
2417
+ const nameParts = nameLower.split(/[-_./\s]+/);
2418
+ return descriptors.filter((d) => {
2419
+ const idLower = d.id.toLowerCase();
2420
+ const descLower = d.description.toLowerCase();
2421
+ const behaviorLower = d.contract?.behavior?.toLowerCase() ?? "";
2422
+ if (idLower.includes(nameLower) || nameLower.includes(idLower)) return true;
2423
+ if (descLower.includes(nameLower)) return true;
2424
+ if (behaviorLower.includes(nameLower)) return true;
2425
+ const minParts = Math.min(2, nameParts.length);
2426
+ const matchingParts = nameParts.filter(
2427
+ (part) => part.length > 2 && (idLower.includes(part) || descLower.includes(part) || behaviorLower.includes(part))
2428
+ );
2429
+ if (matchingParts.length >= minParts) return true;
2430
+ if (d.eventTypes) {
2431
+ const eventStr = Array.isArray(d.eventTypes) ? d.eventTypes.join(" ") : d.eventTypes;
2432
+ if (eventStr.toLowerCase().includes(nameLower)) return true;
2433
+ }
2434
+ return false;
2435
+ });
2436
+ }
2437
+ function formatVerificationReport(report) {
2156
2438
  const lines = [];
2157
- const icon = report.rating === "complete" ? "\u2705" : report.rating === "good" ? "\u{1F7E2}" : report.rating === "partial" ? "\u{1F7E1}" : "\u{1F534}";
2158
- lines.push(`${icon} Praxis Completeness: ${report.score}/100 (${report.rating})`);
2439
+ const icon = report.status === "satisfied" ? "\u2705" : report.status === "partial" ? "\u{1F7E1}" : "\u{1F534}";
2440
+ lines.push(`${icon} Expectations: ${report.setName} \u2014 ${report.status.toUpperCase()}`);
2441
+ lines.push(` ${report.summary.satisfied}/${report.summary.total} satisfied, ${report.summary.violated} violated, ${report.summary.partial} partial`);
2159
2442
  lines.push("");
2160
- lines.push(`Rules: ${report.rules.covered}/${report.rules.total} domain branches covered (${pct(report.rules.covered, report.rules.total)})`);
2161
- lines.push(`Constraints: ${report.constraints.covered}/${report.constraints.total} invariants covered (${pct(report.constraints.covered, report.constraints.total)})`);
2162
- lines.push(`Contracts: ${report.contracts.withContracts}/${report.contracts.total} rules have contracts (${pct(report.contracts.withContracts, report.contracts.total)})`);
2163
- lines.push(`Context: ${report.context.covered}/${report.context.total} state fields in context (${pct(report.context.covered, report.context.total)})`);
2164
- lines.push(`Events: ${report.events.covered}/${report.events.total} transitions have events (${pct(report.events.covered, report.events.total)})`);
2165
- if (report.rules.uncovered.length > 0) {
2166
- lines.push("");
2167
- lines.push("Uncovered domain logic:");
2168
- for (const b of report.rules.uncovered) {
2169
- lines.push(` \u274C ${b.location}: ${b.condition}${b.note ? ` \u2014 ${b.note}` : ""}`);
2443
+ for (const exp of report.expectations) {
2444
+ const expIcon = exp.status === "satisfied" ? "\u2705" : exp.status === "partial" ? "\u{1F7E1}" : "\u{1F534}";
2445
+ lines.push(`${expIcon} ${exp.name}`);
2446
+ for (const cond of exp.conditions) {
2447
+ const condIcon = cond.status === "satisfied" ? " \u2713" : cond.status === "violated" ? " \u2717" : " ?";
2448
+ lines.push(`${condIcon} [${cond.condition.type}] ${cond.condition.description}`);
2449
+ lines.push(` ${cond.explanation}`);
2450
+ }
2451
+ if (exp.edgeCases.length > 0) {
2452
+ lines.push(" Edge cases:");
2453
+ for (const ec of exp.edgeCases) {
2454
+ lines.push(` \u26A0\uFE0F ${ec}`);
2455
+ }
2170
2456
  }
2171
- }
2172
- if (report.constraints.uncovered.length > 0) {
2173
2457
  lines.push("");
2174
- lines.push("Uncovered invariants:");
2175
- for (const b of report.constraints.uncovered) {
2176
- lines.push(` \u274C ${b.location}: ${b.condition}${b.note ? ` \u2014 ${b.note}` : ""}`);
2458
+ }
2459
+ if (report.allMitigations.length > 0) {
2460
+ lines.push("Suggested mitigations:");
2461
+ for (const m of report.allMitigations) {
2462
+ lines.push(` \u{1F4A1} ${m}`);
2177
2463
  }
2178
2464
  }
2179
- if (report.contracts.missing.length > 0) {
2180
- lines.push("");
2181
- lines.push("Rules missing contracts:");
2182
- for (const id of report.contracts.missing) {
2183
- lines.push(` \u{1F4DD} ${id}`);
2465
+ return lines.join("\n");
2466
+ }
2467
+
2468
+ // src/factory/factory.ts
2469
+ var SANITIZE_PATTERNS = {
2470
+ "sql-injection": /('|"|;|--|\/\*|\*\/|xp_|exec\s|union\s+select|drop\s+table|insert\s+into|delete\s+from)/i,
2471
+ "xss": /(<script|javascript:|on\w+\s*=|<iframe|<object|<embed|<img[^>]+onerror)/i,
2472
+ "path-traversal": /(\.\.[/\\]|~\/|\/etc\/|\/proc\/)/i,
2473
+ "command-injection": /([;&|`$]|\$\(|>\s*\/)/i
2474
+ };
2475
+ function inputRules(config = {}) {
2476
+ const {
2477
+ sanitize = [],
2478
+ maxLength = 0,
2479
+ required = false,
2480
+ fieldName = "input"
2481
+ } = config;
2482
+ const rules = [];
2483
+ const constraints = [];
2484
+ if (sanitize.length > 0) {
2485
+ rules.push({
2486
+ id: `factory/input.sanitize-${fieldName}`,
2487
+ description: `Validates ${fieldName} against ${sanitize.join(", ")} patterns`,
2488
+ eventTypes: [`${fieldName}.submit`, `${fieldName}.change`],
2489
+ contract: {
2490
+ ruleId: `factory/input.sanitize-${fieldName}`,
2491
+ behavior: `Checks ${fieldName} for dangerous patterns: ${sanitize.join(", ")}`,
2492
+ examples: [
2493
+ { given: `${fieldName} contains safe text`, when: "input submitted", then: "input.valid emitted" },
2494
+ { given: `${fieldName} contains <script> tag`, when: "input submitted", then: "input.violation emitted" }
2495
+ ],
2496
+ invariants: [
2497
+ `Dangerous ${fieldName} patterns must never pass validation`,
2498
+ "All violations must include the violation type"
2499
+ ]
2500
+ },
2501
+ impl: (state, events) => {
2502
+ const inputEvent = events.find(
2503
+ (e) => e.tag === `${fieldName}.submit` || e.tag === `${fieldName}.change`
2504
+ );
2505
+ if (!inputEvent) return RuleResult.skip("No input event");
2506
+ const value = inputEvent.payload?.value ?? state.context.input?.value ?? "";
2507
+ const violations = [];
2508
+ for (const type of sanitize) {
2509
+ const pattern = SANITIZE_PATTERNS[type];
2510
+ if (pattern && pattern.test(value)) {
2511
+ violations.push(type);
2512
+ }
2513
+ }
2514
+ if (violations.length > 0) {
2515
+ return RuleResult.emit([
2516
+ fact(`${fieldName}.violation`, {
2517
+ field: fieldName,
2518
+ violations,
2519
+ message: `Input failed sanitization: ${violations.join(", ")}`
2520
+ })
2521
+ ]);
2522
+ }
2523
+ return RuleResult.emit([
2524
+ fact(`${fieldName}.valid`, { field: fieldName, sanitized: true })
2525
+ ]);
2526
+ }
2527
+ });
2528
+ }
2529
+ if (maxLength > 0) {
2530
+ constraints.push({
2531
+ id: `factory/input.max-length-${fieldName}`,
2532
+ description: `${fieldName} must not exceed ${maxLength} characters`,
2533
+ contract: {
2534
+ ruleId: `factory/input.max-length-${fieldName}`,
2535
+ behavior: `Enforces max length of ${maxLength} for ${fieldName}`,
2536
+ examples: [
2537
+ { given: `${fieldName} is 10 chars`, when: `maxLength is ${maxLength}`, then: maxLength >= 10 ? "passes" : "violation" }
2538
+ ],
2539
+ invariants: [`${fieldName} length must never exceed ${maxLength}`]
2540
+ },
2541
+ impl: (state) => {
2542
+ const value = state.context.input?.value ?? "";
2543
+ if (value.length > maxLength) {
2544
+ return `${fieldName} exceeds maximum length of ${maxLength} (got ${value.length})`;
2545
+ }
2546
+ return true;
2547
+ }
2548
+ });
2549
+ }
2550
+ if (required) {
2551
+ constraints.push({
2552
+ id: `factory/input.required-${fieldName}`,
2553
+ description: `${fieldName} is required and must not be empty`,
2554
+ contract: {
2555
+ ruleId: `factory/input.required-${fieldName}`,
2556
+ behavior: `Enforces that ${fieldName} is non-empty`,
2557
+ examples: [
2558
+ { given: `${fieldName} is "hello"`, when: "checked", then: "passes" },
2559
+ { given: `${fieldName} is empty`, when: "checked", then: "violation" }
2560
+ ],
2561
+ invariants: [`${fieldName} must never be empty when required`]
2562
+ },
2563
+ impl: (state) => {
2564
+ const value = state.context.input?.value ?? "";
2565
+ if (value.trim().length === 0) {
2566
+ return `${fieldName} is required but empty`;
2567
+ }
2568
+ return true;
2569
+ }
2570
+ });
2571
+ }
2572
+ return { rules, constraints };
2573
+ }
2574
+ function toastRules(config = {}) {
2575
+ const {
2576
+ requireDiff = false,
2577
+ autoDismissMs = 0,
2578
+ deduplicate = false
2579
+ } = config;
2580
+ const rules = [];
2581
+ const constraints = [];
2582
+ rules.push({
2583
+ id: "factory/toast.show",
2584
+ description: "Emits toast notification with content and config",
2585
+ eventTypes: ["toast.request"],
2586
+ contract: {
2587
+ ruleId: "factory/toast.show",
2588
+ behavior: "Shows toast when requested, respecting diff requirement and auto-dismiss",
2589
+ examples: [
2590
+ { given: "toast requested with message", when: "toast.request fires", then: "toast.show emitted" },
2591
+ ...requireDiff ? [{ given: "no diff present", when: "toast.request fires", then: "toast skipped" }] : []
2592
+ ],
2593
+ invariants: [
2594
+ "Toast message must be non-empty",
2595
+ ...requireDiff ? ["Toast must not appear when diff is empty"] : []
2596
+ ]
2597
+ },
2598
+ impl: (state, events) => {
2599
+ const toastEvent = events.find((e) => e.tag === "toast.request");
2600
+ if (!toastEvent) return RuleResult.skip("No toast request");
2601
+ const payload = toastEvent.payload;
2602
+ const message = payload.message ?? "";
2603
+ if (!message) return RuleResult.skip("Empty toast message");
2604
+ if (requireDiff) {
2605
+ const diff = state.context.diff;
2606
+ if (!diff || Object.keys(diff).length === 0) {
2607
+ return RuleResult.skip("No diff \u2014 toast suppressed");
2608
+ }
2609
+ }
2610
+ return RuleResult.emit([
2611
+ fact("toast.show", {
2612
+ message,
2613
+ type: payload.type ?? "info",
2614
+ autoDismissMs: autoDismissMs > 0 ? autoDismissMs : void 0,
2615
+ timestamp: Date.now()
2616
+ })
2617
+ ]);
2184
2618
  }
2619
+ });
2620
+ if (deduplicate) {
2621
+ constraints.push({
2622
+ id: "factory/toast.no-duplicates",
2623
+ description: "Prevents duplicate toast messages",
2624
+ contract: {
2625
+ ruleId: "factory/toast.no-duplicates",
2626
+ behavior: "Rejects toast if identical message is already showing",
2627
+ examples: [
2628
+ { given: "same toast already visible", when: "duplicate toast requested", then: "violation" }
2629
+ ],
2630
+ invariants: ["No two toasts may have the same message simultaneously"]
2631
+ },
2632
+ impl: (state) => {
2633
+ const toasts = state.context.toasts ?? [];
2634
+ const messages = toasts.map((t) => t.message);
2635
+ const uniqueMessages = new Set(messages);
2636
+ if (uniqueMessages.size < messages.length) {
2637
+ return "Duplicate toast detected";
2638
+ }
2639
+ return true;
2640
+ }
2641
+ });
2185
2642
  }
2186
- if (report.events.missing.length > 0) {
2187
- lines.push("");
2188
- lines.push("State transitions without events:");
2189
- for (const t of report.events.missing) {
2190
- lines.push(` \u26A1 ${t.location}: ${t.description}`);
2643
+ return { rules, constraints };
2644
+ }
2645
+ function formRules(config = {}) {
2646
+ const {
2647
+ validateOnBlur = false,
2648
+ submitGate = false,
2649
+ formName = "form"
2650
+ } = config;
2651
+ const rules = [];
2652
+ const constraints = [];
2653
+ if (validateOnBlur) {
2654
+ rules.push({
2655
+ id: `factory/${formName}.validate-on-blur`,
2656
+ description: `Triggers field validation when a ${formName} field loses focus`,
2657
+ eventTypes: [`${formName}.blur`],
2658
+ contract: {
2659
+ ruleId: `factory/${formName}.validate-on-blur`,
2660
+ behavior: `Validates the blurred field and emits validation result`,
2661
+ examples: [
2662
+ { given: `${formName} field has value`, when: "field loses focus", then: "validation result emitted" }
2663
+ ],
2664
+ invariants: ["Validation must run for every blur event on a registered field"]
2665
+ },
2666
+ impl: (_state, events) => {
2667
+ const blurEvent = events.find((e) => e.tag === `${formName}.blur`);
2668
+ if (!blurEvent) return RuleResult.skip("No blur event");
2669
+ const payload = blurEvent.payload;
2670
+ const field = payload.field ?? "unknown";
2671
+ const value = payload.value;
2672
+ const valid = value !== null && value !== void 0 && value !== "";
2673
+ return RuleResult.emit([
2674
+ fact(`${formName}.field-validated`, {
2675
+ field,
2676
+ valid,
2677
+ error: valid ? null : `${field} is required`
2678
+ })
2679
+ ]);
2680
+ }
2681
+ });
2682
+ }
2683
+ if (submitGate) {
2684
+ constraints.push({
2685
+ id: `factory/${formName}.submit-gate`,
2686
+ description: `Prevents ${formName} submission when validation has not passed`,
2687
+ contract: {
2688
+ ruleId: `factory/${formName}.submit-gate`,
2689
+ behavior: `Blocks form submission until all fields are valid`,
2690
+ examples: [
2691
+ { given: `${formName} is invalid`, when: "submit attempted", then: "violation \u2014 submission blocked" },
2692
+ { given: `${formName} is valid`, when: "submit attempted", then: "passes" }
2693
+ ],
2694
+ invariants: ["Form must not submit while any field has errors"]
2695
+ },
2696
+ impl: (state) => {
2697
+ const form = state.context.form;
2698
+ if (!form) return true;
2699
+ if (form.submitting && !form.valid) {
2700
+ return `${formName} cannot submit: validation has not passed`;
2701
+ }
2702
+ return true;
2703
+ }
2704
+ });
2705
+ }
2706
+ rules.push({
2707
+ id: `factory/${formName}.dirty-tracking`,
2708
+ description: `Tracks whether ${formName} has unsaved changes`,
2709
+ eventTypes: [`${formName}.change`, `${formName}.reset`],
2710
+ contract: {
2711
+ ruleId: `factory/${formName}.dirty-tracking`,
2712
+ behavior: "Emits dirty state when form fields change, clears on reset",
2713
+ examples: [
2714
+ { given: "field value changed", when: "form.change fires", then: "form.dirty emitted" },
2715
+ { given: "form reset", when: "form.reset fires", then: "form.dirty retracted" }
2716
+ ],
2717
+ invariants: ["Dirty state must reflect actual field changes"]
2718
+ },
2719
+ impl: (_state, events) => {
2720
+ const resetEvent = events.find((e) => e.tag === `${formName}.reset`);
2721
+ if (resetEvent) {
2722
+ return RuleResult.retract([`${formName}.dirty`], "Form reset");
2723
+ }
2724
+ const changeEvent = events.find((e) => e.tag === `${formName}.change`);
2725
+ if (changeEvent) {
2726
+ return RuleResult.emit([
2727
+ fact(`${formName}.dirty`, { dirty: true })
2728
+ ]);
2729
+ }
2730
+ return RuleResult.skip("No form event");
2191
2731
  }
2732
+ });
2733
+ return { rules, constraints };
2734
+ }
2735
+ function navigationRules(config = {}) {
2736
+ const {
2737
+ dirtyGuard = false,
2738
+ authRequired = false
2739
+ } = config;
2740
+ const rules = [];
2741
+ const constraints = [];
2742
+ rules.push({
2743
+ id: "factory/navigation.handle",
2744
+ description: "Processes navigation requests and emits navigation facts",
2745
+ eventTypes: ["navigation.request"],
2746
+ contract: {
2747
+ ruleId: "factory/navigation.handle",
2748
+ behavior: "Emits navigation.allowed or navigation.blocked based on guards",
2749
+ examples: [
2750
+ { given: "no guards active", when: "navigation requested", then: "navigation.allowed emitted" },
2751
+ ...dirtyGuard ? [{ given: "form is dirty", when: "navigation requested", then: "navigation.blocked emitted" }] : [],
2752
+ ...authRequired ? [{ given: "user not authenticated", when: "navigation requested", then: "navigation.blocked emitted" }] : []
2753
+ ],
2754
+ invariants: [
2755
+ "Every navigation request must result in either allowed or blocked",
2756
+ ...dirtyGuard ? ["Navigation must be blocked when dirty data exists"] : [],
2757
+ ...authRequired ? ["Navigation must be blocked when not authenticated"] : []
2758
+ ]
2759
+ },
2760
+ impl: (state, events) => {
2761
+ const navEvent = events.find((e) => e.tag === "navigation.request");
2762
+ if (!navEvent) return RuleResult.skip("No navigation request");
2763
+ const target = navEvent.payload?.target ?? "/";
2764
+ const reasons = [];
2765
+ if (dirtyGuard && state.context.dirty) {
2766
+ reasons.push("Unsaved changes will be lost");
2767
+ }
2768
+ if (authRequired && !state.context.authenticated) {
2769
+ reasons.push("Authentication required");
2770
+ }
2771
+ if (reasons.length > 0) {
2772
+ return RuleResult.emit([
2773
+ fact("navigation.blocked", { target, reasons })
2774
+ ]);
2775
+ }
2776
+ return RuleResult.emit([
2777
+ fact("navigation.allowed", { target })
2778
+ ]);
2779
+ }
2780
+ });
2781
+ if (dirtyGuard) {
2782
+ constraints.push({
2783
+ id: "factory/navigation.dirty-guard",
2784
+ description: "Prevents silent navigation when unsaved changes exist",
2785
+ contract: {
2786
+ ruleId: "factory/navigation.dirty-guard",
2787
+ behavior: "Blocks navigation when dirty state is true",
2788
+ examples: [
2789
+ { given: "dirty is true", when: "navigation attempted", then: "violation" },
2790
+ { given: "dirty is false", when: "navigation attempted", then: "passes" }
2791
+ ],
2792
+ invariants: ["Must never silently lose unsaved changes"]
2793
+ },
2794
+ impl: (state) => {
2795
+ if (state.context.dirty && state.facts.some((f) => f.tag === "navigation.allowed")) {
2796
+ return "Navigation allowed while dirty \u2014 unsaved changes may be lost";
2797
+ }
2798
+ return true;
2799
+ }
2800
+ });
2192
2801
  }
2193
- return lines.join("\n");
2802
+ return { rules, constraints };
2803
+ }
2804
+ function dataRules(config = {}) {
2805
+ const {
2806
+ optimisticUpdate = false,
2807
+ rollbackOnError = false,
2808
+ cacheInvalidation = false,
2809
+ entityName = "data"
2810
+ } = config;
2811
+ const rules = [];
2812
+ const constraints = [];
2813
+ if (optimisticUpdate) {
2814
+ rules.push({
2815
+ id: `factory/${entityName}.optimistic-update`,
2816
+ description: `Applies optimistic update for ${entityName} while request is pending`,
2817
+ eventTypes: [`${entityName}.mutate`],
2818
+ contract: {
2819
+ ruleId: `factory/${entityName}.optimistic-update`,
2820
+ behavior: `Immediately emits updated ${entityName} state before server confirmation`,
2821
+ examples: [
2822
+ { given: `${entityName} mutation requested`, when: "mutate event fires", then: "optimistic state emitted" }
2823
+ ],
2824
+ invariants: [
2825
+ "Optimistic state must store original for rollback",
2826
+ "Optimistic update must be distinguishable from confirmed state"
2827
+ ]
2828
+ },
2829
+ impl: (_state, events) => {
2830
+ const mutateEvent = events.find((e) => e.tag === `${entityName}.mutate`);
2831
+ if (!mutateEvent) return RuleResult.skip("No mutation event");
2832
+ const payload = mutateEvent.payload;
2833
+ return RuleResult.emit([
2834
+ fact(`${entityName}.optimistic`, {
2835
+ id: payload.id,
2836
+ data: payload.data,
2837
+ pending: true,
2838
+ timestamp: Date.now()
2839
+ })
2840
+ ]);
2841
+ }
2842
+ });
2843
+ }
2844
+ if (rollbackOnError) {
2845
+ rules.push({
2846
+ id: `factory/${entityName}.rollback`,
2847
+ description: `Rolls back optimistic ${entityName} update on error`,
2848
+ eventTypes: [`${entityName}.error`],
2849
+ contract: {
2850
+ ruleId: `factory/${entityName}.rollback`,
2851
+ behavior: `Reverts to original ${entityName} state when mutation fails`,
2852
+ examples: [
2853
+ { given: "optimistic update was applied", when: "server returns error", then: "rollback emitted, optimistic retracted" }
2854
+ ],
2855
+ invariants: [
2856
+ "Rollback must restore original state exactly",
2857
+ "Optimistic facts must be retracted on rollback"
2858
+ ]
2859
+ },
2860
+ impl: (_state, events) => {
2861
+ const errorEvent = events.find((e) => e.tag === `${entityName}.error`);
2862
+ if (!errorEvent) return RuleResult.skip("No error event");
2863
+ const payload = errorEvent.payload;
2864
+ const result = RuleResult.emit([
2865
+ fact(`${entityName}.rollback`, {
2866
+ id: payload.id,
2867
+ error: payload.error,
2868
+ timestamp: Date.now()
2869
+ })
2870
+ ]);
2871
+ return result;
2872
+ }
2873
+ });
2874
+ }
2875
+ if (cacheInvalidation) {
2876
+ rules.push({
2877
+ id: `factory/${entityName}.cache-invalidate`,
2878
+ description: `Invalidates ${entityName} cache when data changes are confirmed`,
2879
+ eventTypes: [`${entityName}.confirmed`, `${entityName}.deleted`],
2880
+ contract: {
2881
+ ruleId: `factory/${entityName}.cache-invalidate`,
2882
+ behavior: `Emits cache invalidation signal when ${entityName} is confirmed or deleted`,
2883
+ examples: [
2884
+ { given: `${entityName} mutation confirmed`, when: "confirmed event fires", then: "cache.invalidate emitted" }
2885
+ ],
2886
+ invariants: ["Stale cache entries must be invalidated after confirmed mutations"]
2887
+ },
2888
+ impl: (_state, events) => {
2889
+ const confirmEvent = events.find(
2890
+ (e) => e.tag === `${entityName}.confirmed` || e.tag === `${entityName}.deleted`
2891
+ );
2892
+ if (!confirmEvent) return RuleResult.skip("No confirmation event");
2893
+ const payload = confirmEvent.payload;
2894
+ return RuleResult.emit([
2895
+ fact(`${entityName}.cache-invalidate`, {
2896
+ id: payload.id,
2897
+ timestamp: Date.now()
2898
+ })
2899
+ ]);
2900
+ }
2901
+ });
2902
+ }
2903
+ constraints.push({
2904
+ id: `factory/${entityName}.integrity`,
2905
+ description: `Ensures ${entityName} state integrity \u2014 no orphaned optimistic updates`,
2906
+ contract: {
2907
+ ruleId: `factory/${entityName}.integrity`,
2908
+ behavior: "Detects orphaned optimistic updates without pending confirmation",
2909
+ examples: [
2910
+ { given: "optimistic update exists without pending request", when: "checked", then: "violation" }
2911
+ ],
2912
+ invariants: [`Every optimistic ${entityName} update must have a corresponding pending request`]
2913
+ },
2914
+ impl: (state) => {
2915
+ const pending = state.context.pending ?? {};
2916
+ const optimisticFacts = state.facts.filter((f) => f.tag === `${entityName}.optimistic`);
2917
+ for (const optFact of optimisticFacts) {
2918
+ const id = optFact.payload?.id;
2919
+ if (id && !pending[id]) {
2920
+ return `Orphaned optimistic update for ${entityName} id=${id} \u2014 no pending request`;
2921
+ }
2922
+ }
2923
+ return true;
2924
+ }
2925
+ });
2926
+ return { rules, constraints };
2927
+ }
2928
+
2929
+ // src/project/project.ts
2930
+ function defineGate(name, config) {
2931
+ const { expects, onSatisfied, onViolation } = config;
2932
+ const rule = {
2933
+ id: `gate/${name}`,
2934
+ description: `Feature gate: ${name} \u2014 requires: ${expects.join(", ")}`,
2935
+ eventTypes: ["gate.check", `gate.${name}.check`],
2936
+ contract: {
2937
+ ruleId: `gate/${name}`,
2938
+ behavior: `Opens gate "${name}" when all expectations are met: ${expects.join(", ")}`,
2939
+ examples: [
2940
+ {
2941
+ given: `all expectations satisfied: ${expects.join(", ")}`,
2942
+ when: "gate checked",
2943
+ then: `gate.${name}.open emitted${onSatisfied ? ` \u2192 ${onSatisfied}` : ""}`
2944
+ },
2945
+ {
2946
+ given: "one or more expectations unsatisfied",
2947
+ when: "gate checked",
2948
+ then: `gate.${name}.blocked emitted${onViolation ? ` \u2192 ${onViolation}` : ""}`
2949
+ }
2950
+ ],
2951
+ invariants: [
2952
+ `Gate "${name}" must never open with unsatisfied expectations`,
2953
+ "Gate status must reflect current expectation state exactly"
2954
+ ]
2955
+ },
2956
+ impl: (state, events) => {
2957
+ const gateEvent = events.find(
2958
+ (e) => e.tag === "gate.check" || e.tag === `gate.${name}.check`
2959
+ );
2960
+ if (!gateEvent) return RuleResult.skip("No gate check event");
2961
+ const expectationState = state.context.expectations ?? {};
2962
+ const satisfied = [];
2963
+ const unsatisfied = [];
2964
+ for (const exp of expects) {
2965
+ if (expectationState[exp]) {
2966
+ satisfied.push(exp);
2967
+ } else {
2968
+ unsatisfied.push(exp);
2969
+ }
2970
+ }
2971
+ const status = unsatisfied.length === 0 ? "open" : "blocked";
2972
+ const gateState = {
2973
+ name,
2974
+ status,
2975
+ satisfied,
2976
+ unsatisfied,
2977
+ lastChanged: Date.now()
2978
+ };
2979
+ const facts = [fact(`gate.${name}.status`, gateState)];
2980
+ if (status === "open" && onSatisfied) {
2981
+ facts.push(fact(`gate.${name}.action`, { action: onSatisfied }));
2982
+ } else if (status === "blocked" && onViolation) {
2983
+ facts.push(fact(`gate.${name}.action`, { action: onViolation }));
2984
+ }
2985
+ return RuleResult.emit(facts);
2986
+ }
2987
+ };
2988
+ const constraint = {
2989
+ id: `gate/${name}/integrity`,
2990
+ description: `Ensures gate "${name}" status matches expectation reality`,
2991
+ contract: {
2992
+ ruleId: `gate/${name}/integrity`,
2993
+ behavior: `Validates that gate "${name}" is not open when expectations are unmet`,
2994
+ examples: [
2995
+ {
2996
+ given: `gate ${name} is open but expectations unmet`,
2997
+ when: "constraint checked",
2998
+ then: "violation"
2999
+ }
3000
+ ],
3001
+ invariants: [`Gate "${name}" must never report open when expectations are unsatisfied`]
3002
+ },
3003
+ impl: (state) => {
3004
+ const gateState = state.context.gates?.[name];
3005
+ if (!gateState) return true;
3006
+ if (gateState.status === "open" && gateState.unsatisfied.length > 0) {
3007
+ return `Gate "${name}" is open but has unsatisfied expectations: ${gateState.unsatisfied.join(", ")}`;
3008
+ }
3009
+ return true;
3010
+ }
3011
+ };
3012
+ return { rules: [rule], constraints: [constraint] };
2194
3013
  }
2195
- function pct(a, b) {
2196
- if (b === 0) return "100%";
2197
- return Math.round(a / b * 100) + "%";
3014
+ function semverContract(config) {
3015
+ const { sources, invariants } = config;
3016
+ const rule = {
3017
+ id: "project/semver-check",
3018
+ description: `Checks version consistency across: ${sources.join(", ")}`,
3019
+ eventTypes: ["project.version-check"],
3020
+ contract: {
3021
+ ruleId: "project/semver-check",
3022
+ behavior: `Verifies version consistency across ${sources.length} sources`,
3023
+ examples: [
3024
+ {
3025
+ given: "all sources have version 1.2.3",
3026
+ when: "version check runs",
3027
+ then: "semver.consistent emitted"
3028
+ },
3029
+ {
3030
+ given: "package.json has 1.2.3 but README has 1.2.2",
3031
+ when: "version check runs",
3032
+ then: "semver.inconsistent emitted with diff"
3033
+ }
3034
+ ],
3035
+ invariants: invariants.length > 0 ? invariants : ["All version sources must report the same semver string"]
3036
+ },
3037
+ impl: (_state, events) => {
3038
+ const checkEvent = events.find((e) => e.tag === "project.version-check");
3039
+ if (!checkEvent) return RuleResult.skip("No version check event");
3040
+ const versions = checkEvent.payload?.versions ?? {};
3041
+ const versionValues = Object.values(versions);
3042
+ const unique = new Set(versionValues);
3043
+ if (unique.size <= 1) {
3044
+ return RuleResult.emit([
3045
+ fact("semver.consistent", {
3046
+ version: versionValues[0] ?? "unknown",
3047
+ sources: Object.keys(versions)
3048
+ })
3049
+ ]);
3050
+ }
3051
+ const report = {
3052
+ consistent: false,
3053
+ versions,
3054
+ violations: [`Version mismatch: ${JSON.stringify(versions)}`]
3055
+ };
3056
+ return RuleResult.emit([fact("semver.inconsistent", report)]);
3057
+ }
3058
+ };
3059
+ return { rules: [rule], constraints: [] };
3060
+ }
3061
+ function commitFromState(diff) {
3062
+ const parts = [];
3063
+ const bodyParts = [];
3064
+ const totalAdded = diff.rulesAdded.length + diff.contractsAdded.length + diff.expectationsAdded.length;
3065
+ const totalRemoved = diff.rulesRemoved.length + diff.contractsRemoved.length + diff.expectationsRemoved.length;
3066
+ const totalModified = diff.rulesModified.length;
3067
+ const hasGateChanges = diff.gateChanges.length > 0;
3068
+ if (totalAdded > 0 && totalRemoved === 0 && totalModified === 0) {
3069
+ if (diff.rulesAdded.length > 0) {
3070
+ const scope = inferScope(diff.rulesAdded);
3071
+ parts.push(`feat(${scope}): add ${formatIds(diff.rulesAdded)}`);
3072
+ } else if (diff.contractsAdded.length > 0) {
3073
+ parts.push(`feat(contracts): add contracts for ${formatIds(diff.contractsAdded)}`);
3074
+ } else {
3075
+ parts.push(`feat(expectations): add ${formatIds(diff.expectationsAdded)}`);
3076
+ }
3077
+ } else if (totalRemoved > 0 && totalAdded === 0) {
3078
+ if (diff.rulesRemoved.length > 0) {
3079
+ const scope = inferScope(diff.rulesRemoved);
3080
+ parts.push(`refactor(${scope}): remove ${formatIds(diff.rulesRemoved)}`);
3081
+ } else {
3082
+ parts.push(`refactor: remove ${totalRemoved} item(s)`);
3083
+ }
3084
+ } else if (totalModified > 0) {
3085
+ const scope = inferScope(diff.rulesModified);
3086
+ parts.push(`refactor(${scope}): update ${formatIds(diff.rulesModified)}`);
3087
+ } else if (hasGateChanges) {
3088
+ const gateNames = diff.gateChanges.map((g) => g.gate);
3089
+ parts.push(`chore(gates): ${formatIds(gateNames)} state changed`);
3090
+ } else {
3091
+ parts.push("chore: behavioral state update");
3092
+ }
3093
+ if (diff.rulesAdded.length > 0) bodyParts.push(`Rules added: ${diff.rulesAdded.join(", ")}`);
3094
+ if (diff.rulesRemoved.length > 0) bodyParts.push(`Rules removed: ${diff.rulesRemoved.join(", ")}`);
3095
+ if (diff.rulesModified.length > 0) bodyParts.push(`Rules modified: ${diff.rulesModified.join(", ")}`);
3096
+ if (diff.contractsAdded.length > 0) bodyParts.push(`Contracts added: ${diff.contractsAdded.join(", ")}`);
3097
+ if (diff.contractsRemoved.length > 0) bodyParts.push(`Contracts removed: ${diff.contractsRemoved.join(", ")}`);
3098
+ if (diff.expectationsAdded.length > 0) bodyParts.push(`Expectations added: ${diff.expectationsAdded.join(", ")}`);
3099
+ if (diff.expectationsRemoved.length > 0) bodyParts.push(`Expectations removed: ${diff.expectationsRemoved.join(", ")}`);
3100
+ for (const gc of diff.gateChanges) {
3101
+ bodyParts.push(`Gate "${gc.gate}": ${gc.from} \u2192 ${gc.to}`);
3102
+ }
3103
+ const subject = parts[0] || "chore: update";
3104
+ return bodyParts.length > 0 ? `${subject}
3105
+
3106
+ ${bodyParts.join("\n")}` : subject;
3107
+ }
3108
+ function inferScope(ids) {
3109
+ if (ids.length === 0) return "rules";
3110
+ const prefixes = ids.map((id) => {
3111
+ const slash = id.indexOf("/");
3112
+ return slash > 0 ? id.slice(0, slash) : id;
3113
+ });
3114
+ const unique = new Set(prefixes);
3115
+ return unique.size === 1 ? prefixes[0] : "rules";
3116
+ }
3117
+ function formatIds(ids) {
3118
+ if (ids.length <= 3) return ids.join(", ");
3119
+ return `${ids.slice(0, 2).join(", ")} (+${ids.length - 2} more)`;
3120
+ }
3121
+ function branchRules(config) {
3122
+ const { naming, mergeConditions } = config;
3123
+ const namePattern = naming.replace("{name}", "(.+)").replace("{issue}", "(\\d+)");
3124
+ const nameRegex = new RegExp(`^${namePattern}$`);
3125
+ const rule = {
3126
+ id: "project/branch-check",
3127
+ description: `Validates branch naming (${naming}) and merge conditions`,
3128
+ eventTypes: ["project.branch-check"],
3129
+ contract: {
3130
+ ruleId: "project/branch-check",
3131
+ behavior: `Ensures branch follows "${naming}" pattern and merge conditions are met`,
3132
+ examples: [
3133
+ {
3134
+ given: `branch named "${naming.replace("{name}", "my-feature")}"`,
3135
+ when: "branch checked",
3136
+ then: "branch.valid emitted"
3137
+ },
3138
+ {
3139
+ given: 'branch named "random-name"',
3140
+ when: "branch checked",
3141
+ then: "branch.invalid emitted"
3142
+ }
3143
+ ],
3144
+ invariants: [
3145
+ `Branch names must follow pattern: ${naming}`,
3146
+ `Merge requires: ${mergeConditions.join(", ")}`
3147
+ ]
3148
+ },
3149
+ impl: (_state, events) => {
3150
+ const checkEvent = events.find((e) => e.tag === "project.branch-check");
3151
+ if (!checkEvent) return RuleResult.skip("No branch check event");
3152
+ const payload = checkEvent.payload;
3153
+ const branch = payload.branch ?? "";
3154
+ const conditions = payload.conditions ?? {};
3155
+ const validName = nameRegex.test(branch);
3156
+ const unmetConditions = mergeConditions.filter((c) => !conditions[c]);
3157
+ if (validName && unmetConditions.length === 0) {
3158
+ return RuleResult.emit([
3159
+ fact("branch.valid", { branch, mergeReady: true })
3160
+ ]);
3161
+ }
3162
+ const reasons = [];
3163
+ if (!validName) reasons.push(`Branch name "${branch}" doesn't match pattern "${naming}"`);
3164
+ if (unmetConditions.length > 0) reasons.push(`Unmet merge conditions: ${unmetConditions.join(", ")}`);
3165
+ return RuleResult.emit([
3166
+ fact("branch.invalid", { branch, reasons, mergeReady: false })
3167
+ ]);
3168
+ }
3169
+ };
3170
+ return { rules: [rule], constraints: [] };
3171
+ }
3172
+ function lintGate(config = {}) {
3173
+ const expects = ["lint-passes", ...config.additionalExpects ?? []];
3174
+ return defineGate("lint", {
3175
+ expects,
3176
+ onSatisfied: "lint-passed",
3177
+ onViolation: "lint-failed"
3178
+ });
3179
+ }
3180
+ function formatGate(config = {}) {
3181
+ const expects = ["format-passes", ...config.additionalExpects ?? []];
3182
+ return defineGate("format", {
3183
+ expects,
3184
+ onSatisfied: "format-passed",
3185
+ onViolation: "format-failed"
3186
+ });
3187
+ }
3188
+ function expectationGate(config = {}) {
3189
+ const expects = ["expectations-verified", ...config.additionalExpects ?? []];
3190
+ return defineGate("expectations", {
3191
+ expects,
3192
+ onSatisfied: "expectations-passed",
3193
+ onViolation: "expectations-failed"
3194
+ });
2198
3195
  }
2199
3196
  export {
2200
3197
  AcknowledgeContractGap,
@@ -2207,6 +3204,8 @@ export {
2207
3204
  ContractMissing,
2208
3205
  ContractUpdated,
2209
3206
  ContractValidated,
3207
+ Expectation,
3208
+ ExpectationSet,
2210
3209
  ReactiveLogicEngine2 as FrameworkAgnosticReactiveEngine,
2211
3210
  InMemoryPraxisDB,
2212
3211
  LogicEngine,
@@ -2229,9 +3228,11 @@ export {
2229
3228
  attachToEngine,
2230
3229
  attachUnumToEngine,
2231
3230
  auditCompleteness,
3231
+ branchRules,
2232
3232
  canvasToMermaid,
2233
3233
  canvasToSchema,
2234
3234
  canvasToYaml,
3235
+ commitFromState,
2235
3236
  createBehaviorLedger,
2236
3237
  createCanvasEditor,
2237
3238
  createChronicle,
@@ -2257,23 +3258,30 @@ export {
2257
3258
  createUIModule,
2258
3259
  createUnifiedApp,
2259
3260
  createUnumAdapter,
3261
+ dataRules,
2260
3262
  defineConstraint,
2261
3263
  defineContract,
2262
3264
  defineEvent,
2263
3265
  defineFact,
3266
+ defineGate,
2264
3267
  defineModule,
2265
3268
  defineRule,
2266
3269
  dirtyGuardRule,
2267
3270
  errorDisplayRule,
3271
+ expectBehavior,
3272
+ expectationGate,
2268
3273
  fact,
2269
3274
  filterEvents,
2270
3275
  filterFacts,
2271
3276
  findEvent,
2272
3277
  findFact,
3278
+ formRules,
3279
+ formatGate,
2273
3280
  formatReport,
2274
3281
  formatValidationReport,
2275
3282
  formatValidationReportJSON,
2276
3283
  formatValidationReportSARIF,
3284
+ formatVerificationReport,
2277
3285
  generateDocs,
2278
3286
  generateId,
2279
3287
  generateTauriConfig,
@@ -2282,7 +3290,9 @@ export {
2282
3290
  getFactPath,
2283
3291
  getSchemaPath,
2284
3292
  initGateRule,
3293
+ inputRules,
2285
3294
  isContract,
3295
+ lintGate,
2286
3296
  loadSchema,
2287
3297
  loadSchemaFromFile,
2288
3298
  loadSchemaFromJson,
@@ -2290,17 +3300,21 @@ export {
2290
3300
  loadingGateRule,
2291
3301
  mustBeInitializedConstraint,
2292
3302
  navigationRequest,
3303
+ navigationRules,
2293
3304
  noInteractionWhileLoadingConstraint,
2294
3305
  offlineIndicatorRule,
2295
3306
  registerSchema,
2296
3307
  resizeEvent,
2297
3308
  runTerminalCommand,
2298
3309
  schemaToCanvas,
3310
+ semverContract,
3311
+ toastRules,
2299
3312
  uiModule,
2300
3313
  uiStateChanged,
2301
3314
  validateContracts,
2302
3315
  validateForGeneration,
2303
3316
  validateSchema,
2304
3317
  validateWithGuardian,
3318
+ verify,
2305
3319
  viewportRule
2306
3320
  };