@plures/praxis 1.2.41 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/browser/{chunk-BBP2F7TT.js → chunk-MJK3IYTJ.js} +123 -5
  2. package/dist/browser/{chunk-FCEH7WMH.js → chunk-N63K4KWS.js} +1 -1
  3. package/dist/browser/{engine-65QDGCAN.js → engine-YIEGSX7U.js} +1 -1
  4. package/dist/browser/index.d.ts +2 -2
  5. package/dist/browser/index.js +10 -5
  6. package/dist/browser/integrations/svelte.d.ts +2 -2
  7. package/dist/browser/integrations/svelte.js +2 -2
  8. package/dist/browser/{reactive-engine.svelte-Cqd8Mod2.d.ts → reactive-engine.svelte-DjynI82A.d.ts} +83 -4
  9. package/dist/node/chunk-2IUFZBH3.js +87 -0
  10. package/dist/node/{chunk-WZ6B3LZ6.js → chunk-7CSWBDFL.js} +3 -56
  11. package/dist/node/{chunk-32YFEEML.js → chunk-7M3HV4XR.js} +4 -4
  12. package/dist/node/{chunk-PTH6MD6P.js → chunk-FWOXU4MM.js} +1 -1
  13. package/dist/node/{chunk-BBP2F7TT.js → chunk-KMJWAFZV.js} +128 -5
  14. package/dist/node/chunk-PGVSB6NR.js +59 -0
  15. package/dist/node/cli/index.cjs +1078 -211
  16. package/dist/node/cli/index.js +21 -2
  17. package/dist/node/cloud/index.d.cts +1 -1
  18. package/dist/node/cloud/index.d.ts +1 -1
  19. package/dist/node/{engine-7CXQV6RC.js → engine-FEN5IYZ5.js} +1 -1
  20. package/dist/node/index.cjs +1633 -59
  21. package/dist/node/index.d.cts +769 -5
  22. package/dist/node/index.d.ts +769 -5
  23. package/dist/node/index.js +1375 -45
  24. package/dist/node/integrations/svelte.cjs +123 -5
  25. package/dist/node/integrations/svelte.d.cts +3 -3
  26. package/dist/node/integrations/svelte.d.ts +3 -3
  27. package/dist/node/integrations/svelte.js +3 -3
  28. package/dist/node/{protocol-BocKczNv.d.ts → protocol-DcyGMmWY.d.cts} +7 -0
  29. package/dist/node/{protocol-BocKczNv.d.cts → protocol-DcyGMmWY.d.ts} +7 -0
  30. package/dist/node/{reactive-engine.svelte-CGe8SpVE.d.cts → reactive-engine.svelte-Cg0Yc2Hs.d.cts} +90 -6
  31. package/dist/node/{reactive-engine.svelte-D-xTDxT5.d.ts → reactive-engine.svelte-DekxqFu0.d.ts} +90 -6
  32. package/dist/node/{reverse-W7THPV45.js → reverse-YD3CWIGM.js} +3 -2
  33. package/dist/node/rules-4DAJ4Z4N.js +7 -0
  34. package/dist/node/server-SYZPDULV.js +361 -0
  35. package/dist/node/{validate-EN3M4FUR.js → validate-TQGVIG7G.js} +4 -3
  36. package/package.json +29 -3
  37. package/src/__tests__/engine-v2.test.ts +532 -0
  38. package/src/__tests__/expectations.test.ts +364 -0
  39. package/src/__tests__/factory.test.ts +426 -0
  40. package/src/__tests__/mcp-server.test.ts +310 -0
  41. package/src/__tests__/project.test.ts +396 -0
  42. package/src/cli/index.ts +28 -0
  43. package/src/core/completeness.ts +274 -0
  44. package/src/core/engine.ts +47 -5
  45. package/src/core/pluresdb/store.ts +9 -3
  46. package/src/core/protocol.ts +7 -0
  47. package/src/core/rule-result.ts +130 -0
  48. package/src/core/rules.ts +12 -5
  49. package/src/core/ui-rules.ts +340 -0
  50. package/src/expectations/expectations.ts +471 -0
  51. package/src/expectations/index.ts +29 -0
  52. package/src/expectations/types.ts +95 -0
  53. package/src/factory/factory.ts +634 -0
  54. package/src/factory/index.ts +27 -0
  55. package/src/factory/types.ts +64 -0
  56. package/src/index.ts +84 -0
  57. package/src/mcp/index.ts +33 -0
  58. package/src/mcp/server.ts +485 -0
  59. package/src/mcp/types.ts +161 -0
  60. package/src/project/index.ts +31 -0
  61. package/src/project/project.ts +423 -0
  62. package/src/project/types.ts +87 -0
  63. package/src/vite/completeness-plugin.ts +72 -0
  64. /package/dist/node/{chunk-R2PSBPKQ.js → chunk-TEMFJOIH.js} +0 -0
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Praxis MCP Server — Types
3
+ *
4
+ * Types for the MCP (Model Context Protocol) server that exposes
5
+ * Praxis engine operations as tools for AI assistants.
6
+ */
7
+
8
+ import type { PraxisEvent, PraxisFact, PraxisDiagnostics } from '../core/protocol.js';
9
+ import type { CompletenessReport, LogicBranch, StateField, StateTransition } from '../core/completeness.js';
10
+ import type { Contract } from '../decision-ledger/types.js';
11
+
12
+ // ─── Tool Input Types ───────────────────────────────────────────────────────
13
+
14
+ export interface InspectInput {
15
+ /** Filter by rule/constraint ID pattern (glob-like) */
16
+ filter?: string;
17
+ /** Include contract details */
18
+ includeContracts?: boolean;
19
+ }
20
+
21
+ export interface EvaluateInput {
22
+ /** Rule ID to evaluate */
23
+ ruleId: string;
24
+ /** Events to process */
25
+ events: PraxisEvent[];
26
+ }
27
+
28
+ export interface AuditInput {
29
+ /** Completeness manifest */
30
+ manifest: {
31
+ branches: LogicBranch[];
32
+ stateFields: StateField[];
33
+ transitions: StateTransition[];
34
+ rulesNeedingContracts: string[];
35
+ };
36
+ /** Minimum passing score (default: 90) */
37
+ threshold?: number;
38
+ }
39
+
40
+ export interface CheckExpectationsInput {
41
+ /** Expectation set name to verify */
42
+ setName?: string;
43
+ }
44
+
45
+ export interface SuggestInput {
46
+ /** Description of the gap or failing expectation */
47
+ gap: string;
48
+ /** Current context for suggestions */
49
+ context?: Record<string, unknown>;
50
+ }
51
+
52
+ export interface StepInput {
53
+ /** Events to step the engine with */
54
+ events: PraxisEvent[];
55
+ }
56
+
57
+ export interface ContractsInput {
58
+ /** Filter by rule ID pattern */
59
+ filter?: string;
60
+ }
61
+
62
+ export interface GatesInput {
63
+ /** Filter by gate name */
64
+ filter?: string;
65
+ }
66
+
67
+ // ─── Tool Output Types ──────────────────────────────────────────────────────
68
+
69
+ export interface RuleInfo {
70
+ id: string;
71
+ description: string;
72
+ eventTypes?: string | string[];
73
+ hasContract: boolean;
74
+ contract?: Contract;
75
+ meta?: Record<string, unknown>;
76
+ }
77
+
78
+ export interface ConstraintInfo {
79
+ id: string;
80
+ description: string;
81
+ hasContract: boolean;
82
+ contract?: Contract;
83
+ meta?: Record<string, unknown>;
84
+ }
85
+
86
+ export interface InspectOutput {
87
+ rules: RuleInfo[];
88
+ constraints: ConstraintInfo[];
89
+ summary: {
90
+ totalRules: number;
91
+ totalConstraints: number;
92
+ rulesWithContracts: number;
93
+ constraintsWithContracts: number;
94
+ };
95
+ }
96
+
97
+ export interface EvaluateOutput {
98
+ ruleId: string;
99
+ resultKind: 'emit' | 'noop' | 'skip' | 'retract';
100
+ facts: PraxisFact[];
101
+ retractedTags: string[];
102
+ reason?: string;
103
+ diagnostics: PraxisDiagnostics[];
104
+ }
105
+
106
+ export interface AuditOutput {
107
+ report: CompletenessReport;
108
+ formatted: string;
109
+ }
110
+
111
+ export interface SuggestOutput {
112
+ suggestions: Array<{
113
+ type: 'rule' | 'constraint' | 'contract' | 'event';
114
+ id: string;
115
+ description: string;
116
+ rationale: string;
117
+ }>;
118
+ }
119
+
120
+ export interface StepOutput {
121
+ facts: PraxisFact[];
122
+ diagnostics: PraxisDiagnostics[];
123
+ factCount: number;
124
+ }
125
+
126
+ export interface ContractInfo {
127
+ ruleId: string;
128
+ hasContract: boolean;
129
+ contract?: Contract;
130
+ type: 'rule' | 'constraint';
131
+ }
132
+
133
+ export interface ContractsOutput {
134
+ contracts: ContractInfo[];
135
+ coverage: {
136
+ total: number;
137
+ withContracts: number;
138
+ percentage: number;
139
+ };
140
+ }
141
+
142
+ export interface FactsOutput {
143
+ facts: PraxisFact[];
144
+ count: number;
145
+ }
146
+
147
+ /**
148
+ * Options for creating a Praxis MCP server.
149
+ */
150
+ export interface PraxisMcpServerOptions<TContext = unknown> {
151
+ /** Name for the MCP server */
152
+ name?: string;
153
+ /** Version string */
154
+ version?: string;
155
+ /** Initial context for the engine */
156
+ initialContext: TContext;
157
+ /** Pre-configured registry (rules + constraints already registered) */
158
+ registry: import('../core/rules.js').PraxisRegistry<TContext>;
159
+ /** Initial facts */
160
+ initialFacts?: PraxisFact[];
161
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Praxis Project Logic
3
+ *
4
+ * Public API for developer workflow rules.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { defineGate, commitFromState, branchRules } from '@plures/praxis/project';
9
+ * ```
10
+ */
11
+
12
+ export {
13
+ defineGate,
14
+ semverContract,
15
+ commitFromState,
16
+ branchRules,
17
+ lintGate,
18
+ formatGate,
19
+ expectationGate,
20
+ } from './project.js';
21
+
22
+ export type {
23
+ GateConfig,
24
+ GateState,
25
+ GateStatus,
26
+ SemverContractConfig,
27
+ SemverReport,
28
+ PraxisDiff,
29
+ BranchRulesConfig,
30
+ PredefinedGateConfig,
31
+ } from './types.js';
@@ -0,0 +1,423 @@
1
+ /**
2
+ * Praxis Project Logic
3
+ *
4
+ * Developer workflow rules: gates, semver contracts, commit message
5
+ * generation from behavioral deltas, and branch management.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { defineGate, semverContract, commitFromState } from '@plures/praxis/project';
10
+ *
11
+ * const testGate = defineGate('test', {
12
+ * expects: ['all-tests-pass', 'no-type-errors'],
13
+ * onSatisfied: 'merge-allowed',
14
+ * onViolation: 'merge-blocked',
15
+ * });
16
+ *
17
+ * const diff = { rulesAdded: ['auth/login'], ... };
18
+ * const message = commitFromState(diff);
19
+ * // → "feat(rules): add auth/login rule"
20
+ * ```
21
+ */
22
+
23
+ import type { PraxisModule, RuleDescriptor, ConstraintDescriptor } from '../core/rules.js';
24
+ import { RuleResult, fact } from '../core/rule-result.js';
25
+ import type {
26
+ GateConfig,
27
+ GateState,
28
+ GateStatus,
29
+ SemverContractConfig,
30
+ SemverReport,
31
+ PraxisDiff,
32
+ BranchRulesConfig,
33
+ PredefinedGateConfig,
34
+ } from './types.js';
35
+
36
+ // ─── Gate System ────────────────────────────────────────────────────────────
37
+
38
+ /**
39
+ * Context type for gates. Apps extend their context with gate state.
40
+ */
41
+ interface GateContext {
42
+ gates?: Record<string, GateState>;
43
+ expectations?: Record<string, boolean>;
44
+ }
45
+
46
+ /**
47
+ * Define a feature gate — a condition that must be satisfied before
48
+ * proceeding with a workflow step (deploy, merge, release, etc.).
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * const testGate = defineGate('test', {
53
+ * expects: ['all-tests-pass', 'no-type-errors'],
54
+ * onSatisfied: 'deploy-allowed',
55
+ * onViolation: 'deploy-blocked',
56
+ * });
57
+ * registry.registerModule(testGate);
58
+ * ```
59
+ */
60
+ export function defineGate(name: string, config: GateConfig): PraxisModule<GateContext> {
61
+ const { expects, onSatisfied, onViolation } = config;
62
+
63
+ const rule: RuleDescriptor<GateContext> = {
64
+ id: `gate/${name}`,
65
+ description: `Feature gate: ${name} — requires: ${expects.join(', ')}`,
66
+ eventTypes: ['gate.check', `gate.${name}.check`],
67
+ contract: {
68
+ ruleId: `gate/${name}`,
69
+ behavior: `Opens gate "${name}" when all expectations are met: ${expects.join(', ')}`,
70
+ examples: [
71
+ {
72
+ given: `all expectations satisfied: ${expects.join(', ')}`,
73
+ when: 'gate checked',
74
+ then: `gate.${name}.open emitted${onSatisfied ? ` → ${onSatisfied}` : ''}`,
75
+ },
76
+ {
77
+ given: 'one or more expectations unsatisfied',
78
+ when: 'gate checked',
79
+ then: `gate.${name}.blocked emitted${onViolation ? ` → ${onViolation}` : ''}`,
80
+ },
81
+ ],
82
+ invariants: [
83
+ `Gate "${name}" must never open with unsatisfied expectations`,
84
+ 'Gate status must reflect current expectation state exactly',
85
+ ],
86
+ },
87
+ impl: (state, events) => {
88
+ const gateEvent = events.find(e =>
89
+ e.tag === 'gate.check' || e.tag === `gate.${name}.check`,
90
+ );
91
+ if (!gateEvent) return RuleResult.skip('No gate check event');
92
+
93
+ const expectationState = state.context.expectations ?? {};
94
+ const satisfied: string[] = [];
95
+ const unsatisfied: string[] = [];
96
+
97
+ for (const exp of expects) {
98
+ if (expectationState[exp]) {
99
+ satisfied.push(exp);
100
+ } else {
101
+ unsatisfied.push(exp);
102
+ }
103
+ }
104
+
105
+ const status: GateStatus = unsatisfied.length === 0 ? 'open' : 'blocked';
106
+ const gateState: GateState = {
107
+ name,
108
+ status,
109
+ satisfied,
110
+ unsatisfied,
111
+ lastChanged: Date.now(),
112
+ };
113
+
114
+ const facts = [fact(`gate.${name}.status`, gateState)];
115
+
116
+ if (status === 'open' && onSatisfied) {
117
+ facts.push(fact(`gate.${name}.action`, { action: onSatisfied }));
118
+ } else if (status === 'blocked' && onViolation) {
119
+ facts.push(fact(`gate.${name}.action`, { action: onViolation }));
120
+ }
121
+
122
+ return RuleResult.emit(facts);
123
+ },
124
+ };
125
+
126
+ const constraint: ConstraintDescriptor<GateContext> = {
127
+ id: `gate/${name}/integrity`,
128
+ description: `Ensures gate "${name}" status matches expectation reality`,
129
+ contract: {
130
+ ruleId: `gate/${name}/integrity`,
131
+ behavior: `Validates that gate "${name}" is not open when expectations are unmet`,
132
+ examples: [
133
+ {
134
+ given: `gate ${name} is open but expectations unmet`,
135
+ when: 'constraint checked',
136
+ then: 'violation',
137
+ },
138
+ ],
139
+ invariants: [`Gate "${name}" must never report open when expectations are unsatisfied`],
140
+ },
141
+ impl: (state) => {
142
+ const gateState = state.context.gates?.[name];
143
+ if (!gateState) return true; // gate not yet evaluated
144
+ if (gateState.status === 'open' && gateState.unsatisfied.length > 0) {
145
+ return `Gate "${name}" is open but has unsatisfied expectations: ${gateState.unsatisfied.join(', ')}`;
146
+ }
147
+ return true;
148
+ },
149
+ };
150
+
151
+ return { rules: [rule], constraints: [constraint] };
152
+ }
153
+
154
+ // ─── Semver Contract ────────────────────────────────────────────────────────
155
+
156
+ /**
157
+ * Create a semver contract module that checks version consistency
158
+ * across multiple sources (package.json, Cargo.toml, etc.).
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * const version = semverContract({
163
+ * sources: ['package.json', 'src/version.ts', 'README.md'],
164
+ * invariants: ['All sources must have the same version'],
165
+ * });
166
+ * ```
167
+ */
168
+ export function semverContract(config: SemverContractConfig): PraxisModule {
169
+ const { sources, invariants } = config;
170
+
171
+ const rule: RuleDescriptor = {
172
+ id: 'project/semver-check',
173
+ description: `Checks version consistency across: ${sources.join(', ')}`,
174
+ eventTypes: ['project.version-check'],
175
+ contract: {
176
+ ruleId: 'project/semver-check',
177
+ behavior: `Verifies version consistency across ${sources.length} sources`,
178
+ examples: [
179
+ {
180
+ given: 'all sources have version 1.2.3',
181
+ when: 'version check runs',
182
+ then: 'semver.consistent emitted',
183
+ },
184
+ {
185
+ given: 'package.json has 1.2.3 but README has 1.2.2',
186
+ when: 'version check runs',
187
+ then: 'semver.inconsistent emitted with diff',
188
+ },
189
+ ],
190
+ invariants: invariants.length > 0
191
+ ? invariants
192
+ : ['All version sources must report the same semver string'],
193
+ },
194
+ impl: (_state, events) => {
195
+ const checkEvent = events.find(e => e.tag === 'project.version-check');
196
+ if (!checkEvent) return RuleResult.skip('No version check event');
197
+
198
+ const versions = (checkEvent.payload as { versions?: Record<string, string> })?.versions ?? {};
199
+ const versionValues = Object.values(versions);
200
+ const unique = new Set(versionValues);
201
+
202
+ if (unique.size <= 1) {
203
+ return RuleResult.emit([
204
+ fact('semver.consistent', {
205
+ version: versionValues[0] ?? 'unknown',
206
+ sources: Object.keys(versions),
207
+ }),
208
+ ]);
209
+ }
210
+
211
+ const report: SemverReport = {
212
+ consistent: false,
213
+ versions,
214
+ violations: [`Version mismatch: ${JSON.stringify(versions)}`],
215
+ };
216
+
217
+ return RuleResult.emit([fact('semver.inconsistent', report)]);
218
+ },
219
+ };
220
+
221
+ return { rules: [rule], constraints: [] };
222
+ }
223
+
224
+ // ─── Commit Message Generation ──────────────────────────────────────────────
225
+
226
+ /**
227
+ * Generate a conventional commit message from a behavioral delta.
228
+ *
229
+ * Unlike file-based commit messages, this describes WHAT behavioral
230
+ * changes occurred — rule additions, contract changes, expectation shifts.
231
+ *
232
+ * @example
233
+ * ```ts
234
+ * const msg = commitFromState({
235
+ * rulesAdded: ['auth/login', 'auth/logout'],
236
+ * contractsAdded: ['auth/login'],
237
+ * ...empty
238
+ * });
239
+ * // → "feat(rules): add auth/login, auth/logout\n\nContracts added: auth/login"
240
+ * ```
241
+ */
242
+ export function commitFromState(diff: PraxisDiff): string {
243
+ const parts: string[] = [];
244
+ const bodyParts: string[] = [];
245
+
246
+ // Determine type from the dominant change
247
+ const totalAdded = diff.rulesAdded.length + diff.contractsAdded.length + diff.expectationsAdded.length;
248
+ const totalRemoved = diff.rulesRemoved.length + diff.contractsRemoved.length + diff.expectationsRemoved.length;
249
+ const totalModified = diff.rulesModified.length;
250
+ const hasGateChanges = diff.gateChanges.length > 0;
251
+
252
+ // Build subject line
253
+ if (totalAdded > 0 && totalRemoved === 0 && totalModified === 0) {
254
+ // Pure addition
255
+ if (diff.rulesAdded.length > 0) {
256
+ const scope = inferScope(diff.rulesAdded);
257
+ parts.push(`feat(${scope}): add ${formatIds(diff.rulesAdded)}`);
258
+ } else if (diff.contractsAdded.length > 0) {
259
+ parts.push(`feat(contracts): add contracts for ${formatIds(diff.contractsAdded)}`);
260
+ } else {
261
+ parts.push(`feat(expectations): add ${formatIds(diff.expectationsAdded)}`);
262
+ }
263
+ } else if (totalRemoved > 0 && totalAdded === 0) {
264
+ // Pure removal
265
+ if (diff.rulesRemoved.length > 0) {
266
+ const scope = inferScope(diff.rulesRemoved);
267
+ parts.push(`refactor(${scope}): remove ${formatIds(diff.rulesRemoved)}`);
268
+ } else {
269
+ parts.push(`refactor: remove ${totalRemoved} item(s)`);
270
+ }
271
+ } else if (totalModified > 0) {
272
+ // Modification
273
+ const scope = inferScope(diff.rulesModified);
274
+ parts.push(`refactor(${scope}): update ${formatIds(diff.rulesModified)}`);
275
+ } else if (hasGateChanges) {
276
+ const gateNames = diff.gateChanges.map(g => g.gate);
277
+ parts.push(`chore(gates): ${formatIds(gateNames)} state changed`);
278
+ } else {
279
+ parts.push('chore: behavioral state update');
280
+ }
281
+
282
+ // Build body
283
+ if (diff.rulesAdded.length > 0) bodyParts.push(`Rules added: ${diff.rulesAdded.join(', ')}`);
284
+ if (diff.rulesRemoved.length > 0) bodyParts.push(`Rules removed: ${diff.rulesRemoved.join(', ')}`);
285
+ if (diff.rulesModified.length > 0) bodyParts.push(`Rules modified: ${diff.rulesModified.join(', ')}`);
286
+ if (diff.contractsAdded.length > 0) bodyParts.push(`Contracts added: ${diff.contractsAdded.join(', ')}`);
287
+ if (diff.contractsRemoved.length > 0) bodyParts.push(`Contracts removed: ${diff.contractsRemoved.join(', ')}`);
288
+ if (diff.expectationsAdded.length > 0) bodyParts.push(`Expectations added: ${diff.expectationsAdded.join(', ')}`);
289
+ if (diff.expectationsRemoved.length > 0) bodyParts.push(`Expectations removed: ${diff.expectationsRemoved.join(', ')}`);
290
+ for (const gc of diff.gateChanges) {
291
+ bodyParts.push(`Gate "${gc.gate}": ${gc.from} → ${gc.to}`);
292
+ }
293
+
294
+ const subject = parts[0] || 'chore: update';
295
+ return bodyParts.length > 0 ? `${subject}\n\n${bodyParts.join('\n')}` : subject;
296
+ }
297
+
298
+ function inferScope(ids: string[]): string {
299
+ // Extract common prefix as scope
300
+ if (ids.length === 0) return 'rules';
301
+ const prefixes = ids.map(id => {
302
+ const slash = id.indexOf('/');
303
+ return slash > 0 ? id.slice(0, slash) : id;
304
+ });
305
+ const unique = new Set(prefixes);
306
+ return unique.size === 1 ? prefixes[0] : 'rules';
307
+ }
308
+
309
+ function formatIds(ids: string[]): string {
310
+ if (ids.length <= 3) return ids.join(', ');
311
+ return `${ids.slice(0, 2).join(', ')} (+${ids.length - 2} more)`;
312
+ }
313
+
314
+ // ─── Branch Rules ───────────────────────────────────────────────────────────
315
+
316
+ /**
317
+ * Create branch management rules.
318
+ *
319
+ * @example
320
+ * ```ts
321
+ * const branches = branchRules({
322
+ * naming: 'feat/{name}',
323
+ * mergeConditions: ['tests-pass', 'review-approved'],
324
+ * });
325
+ * ```
326
+ */
327
+ export function branchRules(config: BranchRulesConfig): PraxisModule {
328
+ const { naming, mergeConditions } = config;
329
+
330
+ const namePattern = naming.replace('{name}', '(.+)').replace('{issue}', '(\\d+)');
331
+ const nameRegex = new RegExp(`^${namePattern}$`);
332
+
333
+ const rule: RuleDescriptor = {
334
+ id: 'project/branch-check',
335
+ description: `Validates branch naming (${naming}) and merge conditions`,
336
+ eventTypes: ['project.branch-check'],
337
+ contract: {
338
+ ruleId: 'project/branch-check',
339
+ behavior: `Ensures branch follows "${naming}" pattern and merge conditions are met`,
340
+ examples: [
341
+ {
342
+ given: `branch named "${naming.replace('{name}', 'my-feature')}"`,
343
+ when: 'branch checked',
344
+ then: 'branch.valid emitted',
345
+ },
346
+ {
347
+ given: 'branch named "random-name"',
348
+ when: 'branch checked',
349
+ then: 'branch.invalid emitted',
350
+ },
351
+ ],
352
+ invariants: [
353
+ `Branch names must follow pattern: ${naming}`,
354
+ `Merge requires: ${mergeConditions.join(', ')}`,
355
+ ],
356
+ },
357
+ impl: (_state, events) => {
358
+ const checkEvent = events.find(e => e.tag === 'project.branch-check');
359
+ if (!checkEvent) return RuleResult.skip('No branch check event');
360
+
361
+ const payload = checkEvent.payload as { branch?: string; conditions?: Record<string, boolean> };
362
+ const branch = payload.branch ?? '';
363
+ const conditions = payload.conditions ?? {};
364
+
365
+ const validName = nameRegex.test(branch);
366
+ const unmetConditions = mergeConditions.filter(c => !conditions[c]);
367
+
368
+ if (validName && unmetConditions.length === 0) {
369
+ return RuleResult.emit([
370
+ fact('branch.valid', { branch, mergeReady: true }),
371
+ ]);
372
+ }
373
+
374
+ const reasons: string[] = [];
375
+ if (!validName) reasons.push(`Branch name "${branch}" doesn't match pattern "${naming}"`);
376
+ if (unmetConditions.length > 0) reasons.push(`Unmet merge conditions: ${unmetConditions.join(', ')}`);
377
+
378
+ return RuleResult.emit([
379
+ fact('branch.invalid', { branch, reasons, mergeReady: false }),
380
+ ]);
381
+ },
382
+ };
383
+
384
+ return { rules: [rule], constraints: [] };
385
+ }
386
+
387
+ // ─── Predefined Gates ───────────────────────────────────────────────────────
388
+
389
+ /**
390
+ * Create a lint gate — blocks workflow until linting passes.
391
+ */
392
+ export function lintGate(config: PredefinedGateConfig = {}): PraxisModule<GateContext> {
393
+ const expects = ['lint-passes', ...(config.additionalExpects ?? [])];
394
+ return defineGate('lint', {
395
+ expects,
396
+ onSatisfied: 'lint-passed',
397
+ onViolation: 'lint-failed',
398
+ });
399
+ }
400
+
401
+ /**
402
+ * Create a format gate — blocks workflow until formatting is correct.
403
+ */
404
+ export function formatGate(config: PredefinedGateConfig = {}): PraxisModule<GateContext> {
405
+ const expects = ['format-passes', ...(config.additionalExpects ?? [])];
406
+ return defineGate('format', {
407
+ expects,
408
+ onSatisfied: 'format-passed',
409
+ onViolation: 'format-failed',
410
+ });
411
+ }
412
+
413
+ /**
414
+ * Create an expectation gate — blocks workflow until expectations are verified.
415
+ */
416
+ export function expectationGate(config: PredefinedGateConfig = {}): PraxisModule<GateContext> {
417
+ const expects = ['expectations-verified', ...(config.additionalExpects ?? [])];
418
+ return defineGate('expectations', {
419
+ expects,
420
+ onSatisfied: 'expectations-passed',
421
+ onViolation: 'expectations-failed',
422
+ });
423
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Praxis Project Logic — Types
3
+ *
4
+ * Types for developer workflow rules: gates, semver contracts,
5
+ * commit generation, and branch management.
6
+ */
7
+
8
+ // ─── Gates ──────────────────────────────────────────────────────────────────
9
+
10
+ export type GateStatus = 'open' | 'closed' | 'blocked';
11
+
12
+ export interface GateConfig {
13
+ /** Expectations that must be satisfied for the gate to open */
14
+ expects: string[];
15
+ /** Action when gate is satisfied */
16
+ onSatisfied?: string;
17
+ /** Action when gate is violated */
18
+ onViolation?: string;
19
+ }
20
+
21
+ export interface GateState {
22
+ name: string;
23
+ status: GateStatus;
24
+ /** Which expectations are satisfied */
25
+ satisfied: string[];
26
+ /** Which expectations are not satisfied */
27
+ unsatisfied: string[];
28
+ /** Timestamp of last status change */
29
+ lastChanged: number;
30
+ }
31
+
32
+ // ─── Semver Contract ────────────────────────────────────────────────────────
33
+
34
+ export interface SemverContractConfig {
35
+ /** Files/sources that contain version strings */
36
+ sources: string[];
37
+ /** Invariants about version consistency */
38
+ invariants: string[];
39
+ }
40
+
41
+ export interface SemverReport {
42
+ /** Whether all sources have consistent versions */
43
+ consistent: boolean;
44
+ /** Version found in each source */
45
+ versions: Record<string, string>;
46
+ /** Invariant violations */
47
+ violations: string[];
48
+ }
49
+
50
+ // ─── Commit Generation ──────────────────────────────────────────────────────
51
+
52
+ export interface PraxisDiff {
53
+ /** Rules added since last commit */
54
+ rulesAdded: string[];
55
+ /** Rules removed */
56
+ rulesRemoved: string[];
57
+ /** Rules modified */
58
+ rulesModified: string[];
59
+ /** Contracts added */
60
+ contractsAdded: string[];
61
+ /** Contracts removed */
62
+ contractsRemoved: string[];
63
+ /** Expectations added */
64
+ expectationsAdded: string[];
65
+ /** Expectations removed */
66
+ expectationsRemoved: string[];
67
+ /** Gate state changes */
68
+ gateChanges: Array<{ gate: string; from: GateStatus; to: GateStatus }>;
69
+ }
70
+
71
+ // ─── Branch Rules ───────────────────────────────────────────────────────────
72
+
73
+ export interface BranchRulesConfig {
74
+ /** Naming convention pattern (e.g., 'feat/{name}', 'fix/{issue}') */
75
+ naming: string;
76
+ /** Conditions required for merge */
77
+ mergeConditions: string[];
78
+ }
79
+
80
+ // ─── Predefined Gate Configs ────────────────────────────────────────────────
81
+
82
+ export interface PredefinedGateConfig {
83
+ /** Whether to enable this gate */
84
+ enabled?: boolean;
85
+ /** Custom expectations to add */
86
+ additionalExpects?: string[];
87
+ }