@plures/praxis 1.3.0 → 1.4.4
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-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 +149 -0
- package/dist/browser/factory/index.js +15 -0
- package/dist/browser/index.d.ts +274 -3
- package/dist/browser/index.js +407 -54
- package/dist/browser/integrations/svelte.d.ts +3 -2
- package/dist/browser/integrations/svelte.js +3 -2
- package/dist/browser/project/index.d.ts +176 -0
- package/dist/browser/project/index.js +19 -0
- package/dist/browser/reactive-engine.svelte-DgVTqHLc.d.ts +223 -0
- package/dist/browser/{reactive-engine.svelte-DjynI82A.d.ts → rules-i1LHpnGd.d.ts} +13 -221
- package/dist/node/chunk-2IUFZBH3.js +87 -0
- package/dist/node/{chunk-WZ6B3LZ6.js → chunk-7CSWBDFL.js} +3 -56
- 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-PGVSB6NR.js +59 -0
- package/dist/node/{chunk-5JQJZADT.js → chunk-ZO2LU4G4.js} +4 -4
- package/dist/node/cli/index.cjs +1126 -211
- package/dist/node/cli/index.js +21 -2
- package/dist/node/{engine-FEN5IYZ5.js → engine-VFHCIEM4.js} +2 -1
- package/dist/node/index.cjs +5623 -2765
- package/dist/node/index.d.cts +1181 -1
- package/dist/node/index.d.ts +1181 -1
- package/dist/node/index.js +1646 -79
- package/dist/node/integrations/svelte.js +4 -3
- package/dist/node/{reverse-W7THPV45.js → reverse-YD3CWIGM.js} +3 -2
- package/dist/node/rules-4DAJ4Z4N.js +7 -0
- package/dist/node/server-FKLVY57V.js +363 -0
- package/dist/node/{validate-EN3M4FUR.js → validate-5PSWJTIC.js} +5 -3
- package/package.json +50 -3
- package/src/__tests__/chronos-project.test.ts +799 -0
- package/src/__tests__/decision-ledger.test.ts +857 -402
- package/src/__tests__/expectations.test.ts +364 -0
- package/src/__tests__/factory.test.ts +426 -0
- package/src/__tests__/mcp-server.test.ts +310 -0
- package/src/__tests__/project.test.ts +396 -0
- 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/cli/index.ts +28 -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/expectations/expectations.ts +471 -0
- package/src/expectations/index.ts +29 -0
- package/src/expectations/types.ts +95 -0
- package/src/factory/factory.ts +634 -0
- package/src/factory/index.ts +27 -0
- package/src/factory/types.ts +64 -0
- package/src/index.browser.ts +83 -0
- package/src/index.ts +134 -0
- package/src/mcp/index.ts +33 -0
- package/src/mcp/server.ts +485 -0
- package/src/mcp/types.ts +161 -0
- package/src/project/index.ts +31 -0
- package/src/project/project.ts +423 -0
- package/src/project/types.ts +87 -0
- package/dist/node/chunk-PTH6MD6P.js +0 -487
- /package/dist/node/{chunk-R2PSBPKQ.js → chunk-TEMFJOIH.js} +0 -0
|
@@ -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
|
+
}
|