@plures/praxis 1.1.3 → 1.2.10
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/FRAMEWORK.md +106 -15
- package/README.md +194 -119
- package/dist/browser/adapter-CIMBGDC7.js +14 -0
- package/dist/browser/chunk-K377RW4V.js +230 -0
- package/dist/browser/chunk-MBVHLOU2.js +152 -0
- package/dist/browser/{chunk-R45WXWKH.js → chunk-VOMLVI6V.js} +1 -149
- package/dist/browser/engine-YJZV4SLD.js +8 -0
- package/dist/browser/index.d.ts +161 -5
- package/dist/browser/index.js +156 -141
- package/dist/browser/integrations/svelte.d.ts +2 -2
- package/dist/browser/integrations/svelte.js +2 -1
- package/dist/browser/{reactive-engine.svelte-C9OpcTHf.d.ts → reactive-engine.svelte-9aS0kTa8.d.ts} +136 -1
- package/dist/node/adapter-75ISSMWD.js +15 -0
- package/dist/node/chunk-5RH7UAQC.js +486 -0
- package/dist/node/chunk-MBVHLOU2.js +152 -0
- package/dist/node/chunk-PRPQO6R5.js +85 -0
- package/dist/node/chunk-R2PSBPKQ.js +150 -0
- package/dist/node/chunk-S54337I5.js +446 -0
- package/dist/node/{chunk-R45WXWKH.js → chunk-VOMLVI6V.js} +1 -149
- package/dist/node/chunk-WZ6B3LZ6.js +638 -0
- package/dist/node/cli/index.cjs +2936 -897
- package/dist/node/cli/index.js +27 -0
- package/dist/node/components/index.d.cts +3 -2
- package/dist/node/components/index.d.ts +3 -2
- package/dist/node/docs-JFNYTOJA.js +102 -0
- package/dist/node/engine-2DQBKBJC.js +9 -0
- package/dist/node/index.cjs +1114 -354
- package/dist/node/index.d.cts +388 -5
- package/dist/node/index.d.ts +388 -5
- package/dist/node/index.js +201 -640
- package/dist/node/integrations/svelte.cjs +76 -0
- package/dist/node/integrations/svelte.d.cts +2 -2
- package/dist/node/integrations/svelte.d.ts +2 -2
- package/dist/node/integrations/svelte.js +3 -1
- package/dist/node/{reactive-engine.svelte-1M4m_C_v.d.cts → reactive-engine.svelte-BFIZfawz.d.cts} +199 -1
- package/dist/node/{reactive-engine.svelte-ChNFn4Hj.d.ts → reactive-engine.svelte-CRNqHlbv.d.ts} +199 -1
- package/dist/node/reverse-W7THPV45.js +193 -0
- package/dist/node/{terminal-adapter-CWka-yL8.d.ts → terminal-adapter-B-UK_Vdz.d.ts} +28 -3
- package/dist/node/{terminal-adapter-CDzxoLKR.d.cts → terminal-adapter-BQSIF5bf.d.cts} +28 -3
- package/dist/node/validate-CNHUULQE.js +180 -0
- package/docs/core/pluresdb-integration.md +15 -15
- package/docs/decision-ledger/BEHAVIOR_LEDGER.md +225 -0
- package/docs/decision-ledger/DecisionLedger.tla +180 -0
- package/docs/decision-ledger/IMPLEMENTATION_SUMMARY.md +217 -0
- package/docs/decision-ledger/LATEST.md +166 -0
- package/docs/guides/cicd-pipeline.md +142 -0
- package/package.json +2 -2
- package/src/__tests__/cli-validate.test.ts +197 -0
- package/src/__tests__/decision-ledger.test.ts +485 -0
- package/src/__tests__/reverse-generator.test.ts +189 -0
- package/src/__tests__/scanner.test.ts +215 -0
- package/src/cli/commands/docs.ts +147 -0
- package/src/cli/commands/reverse.ts +289 -0
- package/src/cli/commands/validate.ts +264 -0
- package/src/cli/index.ts +68 -0
- package/src/core/pluresdb/adapter.ts +46 -3
- package/src/core/reactive-engine.svelte.ts +6 -1
- package/src/core/reactive-engine.ts +1 -1
- package/src/core/rules.ts +133 -0
- package/src/decision-ledger/README.md +400 -0
- package/src/decision-ledger/REVERSE_ENGINEERING.md +484 -0
- package/src/decision-ledger/facts-events.ts +121 -0
- package/src/decision-ledger/index.ts +70 -0
- package/src/decision-ledger/ledger.ts +246 -0
- package/src/decision-ledger/logic-ledger.ts +158 -0
- package/src/decision-ledger/reverse-generator.ts +426 -0
- package/src/decision-ledger/scanner.ts +506 -0
- package/src/decision-ledger/types.ts +247 -0
- package/src/decision-ledger/validation.ts +336 -0
- package/src/dsl/index.ts +13 -2
- package/src/index.browser.ts +6 -0
- package/src/index.ts +40 -0
- package/src/integrations/pluresdb.ts +14 -2
- package/src/integrations/unified.ts +350 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Decision Ledger Integration - LATEST Behavior Snapshot
|
|
2
|
+
|
|
3
|
+
**Generated From**: BEHAVIOR_LEDGER.md Entry 1 (`decision-ledger-v1`)
|
|
4
|
+
**Generated At**: 2025-01-26
|
|
5
|
+
**Status**: Non-Authoritative (derived from ledger)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Current Behavior Summary
|
|
10
|
+
|
|
11
|
+
The Decision Ledger Integration extends Praxis with contract-based rule and constraint validation, enabling teams to document, test, and enforce behavioral contracts for their logic.
|
|
12
|
+
|
|
13
|
+
### Core Capabilities
|
|
14
|
+
|
|
15
|
+
1. **Contract Definition**
|
|
16
|
+
- Attach contracts to rules and constraints via the `meta.contract` field
|
|
17
|
+
- Define canonical behavior, examples, invariants, assumptions, and references
|
|
18
|
+
- Contracts are JSON-serializable and language-neutral
|
|
19
|
+
|
|
20
|
+
2. **Build-time Validation**
|
|
21
|
+
- `praxis validate` CLI command scans registered rules and constraints
|
|
22
|
+
- Reports contract coverage and identifies gaps
|
|
23
|
+
- Outputs structured diagnostics (JSON, console, SARIF)
|
|
24
|
+
|
|
25
|
+
3. **Runtime Validation**
|
|
26
|
+
- Optional strict mode for contract enforcement during rule registration
|
|
27
|
+
- Emits `ContractMissing` facts for rules without complete contracts
|
|
28
|
+
- Policy rules can enforce contract requirements
|
|
29
|
+
|
|
30
|
+
4. **Contract Gap Management**
|
|
31
|
+
- Acknowledge known gaps with `ACKNOWLEDGE_CONTRACT_GAP` event
|
|
32
|
+
- Track technical debt and expiration dates
|
|
33
|
+
- Audit trail of contract coverage over time
|
|
34
|
+
|
|
35
|
+
### Active Assumptions
|
|
36
|
+
|
|
37
|
+
All assumptions from Entry 1 are active:
|
|
38
|
+
- A1: Contracts stored in `meta` field
|
|
39
|
+
- A2: All contract data is JSON-serializable
|
|
40
|
+
- A3: CLI follows Commander.js pattern
|
|
41
|
+
- A4: Tests use Vitest framework
|
|
42
|
+
- A5: Contracts are optional by default
|
|
43
|
+
|
|
44
|
+
### Quick Start Example
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { defineRule, defineContract } from '@plures/praxis';
|
|
48
|
+
|
|
49
|
+
// Define a contract
|
|
50
|
+
const loginContract = defineContract({
|
|
51
|
+
ruleId: 'auth.login',
|
|
52
|
+
behavior: 'Process login events and create user session facts',
|
|
53
|
+
examples: [
|
|
54
|
+
{
|
|
55
|
+
given: 'User provides valid credentials',
|
|
56
|
+
when: 'LOGIN event is received',
|
|
57
|
+
then: 'UserSessionCreated fact is emitted'
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
invariants: ['Session must have unique ID']
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Attach contract to rule
|
|
64
|
+
const loginRule = defineRule({
|
|
65
|
+
id: 'auth.login',
|
|
66
|
+
description: 'Process login events',
|
|
67
|
+
impl: (state, events) => { /* ... */ },
|
|
68
|
+
meta: { contract: loginContract }
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Validate at build time
|
|
72
|
+
// $ praxis validate
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### API Reference
|
|
76
|
+
|
|
77
|
+
#### Contract Types
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
interface Contract {
|
|
81
|
+
ruleId: string;
|
|
82
|
+
behavior: string;
|
|
83
|
+
examples: Array<{
|
|
84
|
+
given: string;
|
|
85
|
+
when: string;
|
|
86
|
+
then: string;
|
|
87
|
+
}>;
|
|
88
|
+
invariants: string[];
|
|
89
|
+
assumptions?: Assumption[];
|
|
90
|
+
references?: Reference[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface Assumption {
|
|
94
|
+
id: string;
|
|
95
|
+
statement: string;
|
|
96
|
+
confidence: number; // 0.0 to 1.0
|
|
97
|
+
justification: string;
|
|
98
|
+
derivedFrom?: string;
|
|
99
|
+
impacts: Array<'spec' | 'tests' | 'code'>;
|
|
100
|
+
status: 'active' | 'revised' | 'invalidated';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface Reference {
|
|
104
|
+
type: string;
|
|
105
|
+
url?: string;
|
|
106
|
+
description?: string;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Validation API
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// CLI
|
|
114
|
+
praxis validate [options]
|
|
115
|
+
--output <format> Output format: json, console, sarif (default: console)
|
|
116
|
+
--strict Exit with error if contracts are missing
|
|
117
|
+
--config <file> Path to validation config file
|
|
118
|
+
|
|
119
|
+
// Programmatic
|
|
120
|
+
import { validateContracts } from '@plures/praxis/decision-ledger';
|
|
121
|
+
|
|
122
|
+
const report = validateContracts(registry, options);
|
|
123
|
+
// report.complete: Contract[]
|
|
124
|
+
// report.incomplete: ContractGap[]
|
|
125
|
+
// report.missing: string[] (rule IDs)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### Facts and Events
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// ContractMissing fact
|
|
132
|
+
const ContractMissing = defineFact<'ContractMissing', {
|
|
133
|
+
ruleId: string;
|
|
134
|
+
missing: Array<'behavior' | 'examples' | 'invariants' | 'tests' | 'spec'>;
|
|
135
|
+
severity: 'warning' | 'error';
|
|
136
|
+
}>('ContractMissing');
|
|
137
|
+
|
|
138
|
+
// ACKNOWLEDGE_CONTRACT_GAP event
|
|
139
|
+
const AcknowledgeContractGap = defineEvent<'ACKNOWLEDGE_CONTRACT_GAP', {
|
|
140
|
+
ruleId: string;
|
|
141
|
+
missing: string[];
|
|
142
|
+
justification: string;
|
|
143
|
+
expiresAt?: string;
|
|
144
|
+
}>('ACKNOWLEDGE_CONTRACT_GAP');
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Configuration
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// praxis.config.ts
|
|
151
|
+
export default {
|
|
152
|
+
decisionLedger: {
|
|
153
|
+
strict: false, // Require contracts for all rules
|
|
154
|
+
validateOnRegister: true, // Validate at runtime
|
|
155
|
+
outputFormat: 'console', // 'json' | 'console' | 'sarif'
|
|
156
|
+
requiredFields: [ // Required contract fields
|
|
157
|
+
'behavior',
|
|
158
|
+
'examples'
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
**Note**: This is a non-authoritative snapshot derived from the behavior ledger. The source of truth is BEHAVIOR_LEDGER.md.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# CI/CD Pipeline
|
|
2
|
+
|
|
3
|
+
This document describes the automated continuous integration and deployment pipeline for the Praxis framework.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The Praxis CI/CD pipeline is fully automated, ensuring that code merged into the `main` branch flows automatically through version bumping, tagging, release creation, and publishing to all package repositories in parallel.
|
|
8
|
+
|
|
9
|
+
## Pipeline Flow
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
PR Merged to main
|
|
13
|
+
↓
|
|
14
|
+
Auto Version Bump (based on semver labels)
|
|
15
|
+
↓
|
|
16
|
+
Create Git Tag (v*.*.*)
|
|
17
|
+
↓
|
|
18
|
+
Run Tests & Build
|
|
19
|
+
↓
|
|
20
|
+
Create GitHub Release
|
|
21
|
+
↓
|
|
22
|
+
Publish to Repositories (Parallel)
|
|
23
|
+
├─→ NPM
|
|
24
|
+
├─→ JSR (JavaScript Registry)
|
|
25
|
+
└─→ NuGet
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Workflow Details
|
|
29
|
+
|
|
30
|
+
### 1. Auto Version Bump (`auto-version-bump.yml`)
|
|
31
|
+
|
|
32
|
+
**Trigger**: When a PR is merged to `main`
|
|
33
|
+
|
|
34
|
+
**Process**:
|
|
35
|
+
- Determines version bump level based on PR labels:
|
|
36
|
+
- `semver:major` → Major version bump (e.g., 1.0.0 → 2.0.0)
|
|
37
|
+
- `semver:minor` → Minor version bump (e.g., 1.0.0 → 1.1.0)
|
|
38
|
+
- `semver:patch` → Patch version bump (e.g., 1.0.0 → 1.0.1) - **default**
|
|
39
|
+
- Updates `package.json` version
|
|
40
|
+
- Creates and pushes a git tag (e.g., `v1.2.3`)
|
|
41
|
+
|
|
42
|
+
**Requirements**: Add one of the semver labels to your PR to control the version bump.
|
|
43
|
+
|
|
44
|
+
### 2. Release Creation (`release.yml`)
|
|
45
|
+
|
|
46
|
+
**Trigger**: When a tag matching `v*.*.*` is pushed (automatically triggered by auto-version-bump)
|
|
47
|
+
|
|
48
|
+
**Process**:
|
|
49
|
+
- Checks out code at the tagged version
|
|
50
|
+
- Runs full test suite
|
|
51
|
+
- Builds the project
|
|
52
|
+
- Creates a GitHub Release with changelog
|
|
53
|
+
- Automatically triggers the publish workflow
|
|
54
|
+
|
|
55
|
+
### 3. Publishing (`publish.yml`)
|
|
56
|
+
|
|
57
|
+
**Trigger**: Called automatically by `release.yml` after release creation
|
|
58
|
+
|
|
59
|
+
**Process**: Three parallel publishing jobs run simultaneously:
|
|
60
|
+
|
|
61
|
+
#### NPM Publishing
|
|
62
|
+
- Builds the TypeScript/Node.js package
|
|
63
|
+
- Publishes to npm registry as `@plures/praxis`
|
|
64
|
+
- Requires `NPM_TOKEN` secret
|
|
65
|
+
|
|
66
|
+
#### JSR Publishing
|
|
67
|
+
- Publishes to JavaScript Registry (JSR) using Deno
|
|
68
|
+
- Publishes as `@plures/praxis`
|
|
69
|
+
- Uses OIDC authentication
|
|
70
|
+
- Note: Uses `--allow-dirty` flag to allow publishing from clean checkout state
|
|
71
|
+
|
|
72
|
+
#### NuGet Publishing
|
|
73
|
+
- Extracts version from the most recent git tag
|
|
74
|
+
- Updates version in `.csproj` file before building
|
|
75
|
+
- Builds the C# package
|
|
76
|
+
- Runs C# tests
|
|
77
|
+
- Publishes to NuGet.org as `Praxis`
|
|
78
|
+
- Requires `NUGET_API_KEY` secret
|
|
79
|
+
|
|
80
|
+
**Note**: When the publish workflow is called from the release workflow, it extracts version information from the git tags instead of relying on GitHub context, ensuring consistent versioning across all published packages.
|
|
81
|
+
|
|
82
|
+
## Additional Workflows
|
|
83
|
+
|
|
84
|
+
### CI (`ci.yml`)
|
|
85
|
+
|
|
86
|
+
**Trigger**: On push to `main` or pull request
|
|
87
|
+
|
|
88
|
+
**Purpose**: Validates code quality before merging
|
|
89
|
+
- Runs tests on multiple Node.js versions (18.x, 20.x)
|
|
90
|
+
- Runs C# tests on .NET 8
|
|
91
|
+
- Performs Deno compatibility checks
|
|
92
|
+
|
|
93
|
+
### C# Build Check (`publish-nuget.yml`)
|
|
94
|
+
|
|
95
|
+
**Trigger**: On push to `main` or pull request
|
|
96
|
+
|
|
97
|
+
**Purpose**: Validates C# code builds and tests pass
|
|
98
|
+
- Ensures C# codebase is always in a buildable state
|
|
99
|
+
- Runs independently of publishing workflow
|
|
100
|
+
|
|
101
|
+
## Secrets Required
|
|
102
|
+
|
|
103
|
+
The following secrets must be configured in the GitHub repository:
|
|
104
|
+
|
|
105
|
+
- `NPM_TOKEN`: NPM authentication token for publishing packages
|
|
106
|
+
- `NUGET_API_KEY`: NuGet API key for publishing packages
|
|
107
|
+
|
|
108
|
+
## Manual Triggers
|
|
109
|
+
|
|
110
|
+
The main pipeline workflows support `workflow_dispatch`, allowing manual triggering when needed:
|
|
111
|
+
|
|
112
|
+
- **Release Workflow**: Manually create a release and publish for the current state
|
|
113
|
+
- **Publish Workflow**: Re-publish to all repositories without creating a new release
|
|
114
|
+
- **C# Build Check**: Manually validate C# code builds and tests (separate from publishing)
|
|
115
|
+
|
|
116
|
+
## Best Practices
|
|
117
|
+
|
|
118
|
+
1. **Always label your PRs**: Use semver labels (`semver:major`, `semver:minor`, or `semver:patch`) to control version bumping
|
|
119
|
+
2. **Review before merging**: Once merged to `main`, the entire pipeline runs automatically
|
|
120
|
+
3. **Monitor releases**: Check the Actions tab to ensure all publishing jobs complete successfully
|
|
121
|
+
4. **Breaking changes**: Use `semver:major` for breaking changes
|
|
122
|
+
5. **New features**: Use `semver:minor` for new features
|
|
123
|
+
6. **Bug fixes**: Use `semver:patch` for bug fixes and patches
|
|
124
|
+
|
|
125
|
+
## Troubleshooting
|
|
126
|
+
|
|
127
|
+
### Publishing failures
|
|
128
|
+
|
|
129
|
+
If any publishing job fails:
|
|
130
|
+
1. Check the Actions tab for detailed error logs
|
|
131
|
+
2. Verify all required secrets are configured
|
|
132
|
+
3. The workflow can be manually re-triggered using `workflow_dispatch`
|
|
133
|
+
|
|
134
|
+
### Version conflicts
|
|
135
|
+
|
|
136
|
+
If version bumping fails:
|
|
137
|
+
1. Ensure no manual version changes conflict with automated bumping
|
|
138
|
+
2. Check that the main branch is up to date
|
|
139
|
+
|
|
140
|
+
### Parallel publishing
|
|
141
|
+
|
|
142
|
+
All three repositories (NPM, JSR, NuGet) publish in parallel. If one fails, the others will continue. Check individual job logs for failures.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plures/praxis",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.10",
|
|
4
4
|
"description": "The Full Plures Application Framework - declarative schemas, logic engine, component generation, and local-first data",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/node/index.js",
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"dependencies": {
|
|
118
118
|
"commander": "^14.0.2",
|
|
119
119
|
"js-yaml": "^4.1.1",
|
|
120
|
-
"pluresdb": "^1.
|
|
120
|
+
"@plures/pluresdb": "^1.6.10"
|
|
121
121
|
},
|
|
122
122
|
"peerDependencies": {
|
|
123
123
|
"svelte": "^5.46.0"
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Validate Command - Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the praxis validate command with decision ledger features.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
import { promises as fs } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { exec } from 'node:child_process';
|
|
13
|
+
import { promisify } from 'node:util';
|
|
14
|
+
|
|
15
|
+
const execAsync = promisify(exec);
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
// Go up from src/__tests__ to project root
|
|
19
|
+
const projectRoot = path.resolve(__dirname, '..', '..');
|
|
20
|
+
const cliPath = path.join(projectRoot, 'dist/node/cli/index.js');
|
|
21
|
+
const sampleRegistryPath = path.join(projectRoot, 'examples/sample-registry.js');
|
|
22
|
+
|
|
23
|
+
describe('CLI Validate Command', () => {
|
|
24
|
+
let tempDir: string;
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
tempDir = path.join(tmpdir(), `praxis-test-${Date.now()}`);
|
|
28
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
try {
|
|
33
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// Ignore cleanup errors
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should validate registry and output to console', async () => {
|
|
40
|
+
const { stdout, stderr } = await execAsync(
|
|
41
|
+
`node ${cliPath} validate --registry ${sampleRegistryPath}`
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(stdout).toContain('Contract Validation Report');
|
|
45
|
+
expect(stdout).toContain('Total: 5');
|
|
46
|
+
expect(stdout).toContain('auth.login');
|
|
47
|
+
expect(stderr).toContain('[Praxis][WARN]');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should output validation report as JSON', async () => {
|
|
51
|
+
const { stdout } = await execAsync(
|
|
52
|
+
`node ${cliPath} validate --registry ${sampleRegistryPath} --output json 2>/dev/null`
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const report = JSON.parse(stdout);
|
|
56
|
+
|
|
57
|
+
expect(report).toHaveProperty('complete');
|
|
58
|
+
expect(report).toHaveProperty('incomplete');
|
|
59
|
+
expect(report).toHaveProperty('missing');
|
|
60
|
+
expect(report).toHaveProperty('total');
|
|
61
|
+
expect(report.total).toBe(5);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should output validation report as SARIF', async () => {
|
|
65
|
+
const { stdout } = await execAsync(
|
|
66
|
+
`node ${cliPath} validate --registry ${sampleRegistryPath} --output sarif 2>/dev/null`
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const sarif = JSON.parse(stdout);
|
|
70
|
+
|
|
71
|
+
expect(sarif).toHaveProperty('version', '2.1.0');
|
|
72
|
+
expect(sarif).toHaveProperty('runs');
|
|
73
|
+
expect(sarif.runs).toHaveLength(1);
|
|
74
|
+
expect(sarif.runs[0].tool.driver.name).toBe('Praxis Decision Ledger');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should create logic ledger snapshots with --ledger option', async () => {
|
|
78
|
+
const ledgerDir = path.join(tempDir, 'ledger');
|
|
79
|
+
|
|
80
|
+
await execAsync(
|
|
81
|
+
`node ${cliPath} validate --registry ${sampleRegistryPath} --ledger ${ledgerDir} --author "test-user" 2>&1`
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Check that ledger directory was created
|
|
85
|
+
const ledgerPath = path.join(ledgerDir, 'logic-ledger');
|
|
86
|
+
const exists = await fs.access(ledgerPath).then(() => true).catch(() => false);
|
|
87
|
+
expect(exists).toBe(true);
|
|
88
|
+
|
|
89
|
+
// Check that index.json was created
|
|
90
|
+
const indexPath = path.join(ledgerPath, 'index.json');
|
|
91
|
+
const indexContent = await fs.readFile(indexPath, 'utf-8');
|
|
92
|
+
const index = JSON.parse(indexContent);
|
|
93
|
+
expect(index).toHaveProperty('byRuleId');
|
|
94
|
+
expect(index.byRuleId).toHaveProperty('auth.login');
|
|
95
|
+
|
|
96
|
+
// Check that a versioned snapshot was created
|
|
97
|
+
// The path in byRuleId is relative to ledgerDir, not ledgerPath
|
|
98
|
+
const ruleDirRelative = index.byRuleId['auth.login'];
|
|
99
|
+
const ruleDir = path.join(ledgerDir, ruleDirRelative);
|
|
100
|
+
const latestPath = path.join(ruleDir, 'LATEST.json');
|
|
101
|
+
const latestContent = await fs.readFile(latestPath, 'utf-8');
|
|
102
|
+
const latest = JSON.parse(latestContent);
|
|
103
|
+
expect(latest).toHaveProperty('ruleId', 'auth.login');
|
|
104
|
+
expect(latest).toHaveProperty('version', 1);
|
|
105
|
+
expect(latest).toHaveProperty('canonicalBehavior');
|
|
106
|
+
expect(latest).toHaveProperty('assumptions');
|
|
107
|
+
expect(latest).toHaveProperty('artifacts');
|
|
108
|
+
expect(latest).toHaveProperty('drift');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should emit contract gaps as facts with --emit-facts option', async () => {
|
|
112
|
+
const gapFile = path.join(tempDir, 'gaps.json');
|
|
113
|
+
|
|
114
|
+
await execAsync(
|
|
115
|
+
`node ${cliPath} validate --registry ${sampleRegistryPath} --emit-facts --gap-output ${gapFile}`
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Check that gap file was created
|
|
119
|
+
const gapContent = await fs.readFile(gapFile, 'utf-8');
|
|
120
|
+
const gaps = JSON.parse(gapContent);
|
|
121
|
+
|
|
122
|
+
expect(gaps).toHaveProperty('facts');
|
|
123
|
+
expect(gaps).toHaveProperty('events');
|
|
124
|
+
expect(Array.isArray(gaps.facts)).toBe(true);
|
|
125
|
+
expect(gaps.facts.length).toBeGreaterThan(0);
|
|
126
|
+
|
|
127
|
+
// Check that facts have correct structure
|
|
128
|
+
const firstFact = gaps.facts[0];
|
|
129
|
+
expect(firstFact).toHaveProperty('tag', 'ContractMissing');
|
|
130
|
+
expect(firstFact).toHaveProperty('payload');
|
|
131
|
+
expect(firstFact.payload).toHaveProperty('ruleId');
|
|
132
|
+
expect(firstFact.payload).toHaveProperty('missing');
|
|
133
|
+
expect(firstFact.payload).toHaveProperty('severity');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should exit with error code in strict mode if contracts missing', async () => {
|
|
137
|
+
try {
|
|
138
|
+
await execAsync(
|
|
139
|
+
`node ${cliPath} validate --registry ${sampleRegistryPath} --strict 2>&1`
|
|
140
|
+
);
|
|
141
|
+
// Should not reach here - strict mode should exit with error
|
|
142
|
+
throw new Error('Expected command to fail in strict mode');
|
|
143
|
+
} catch (error: any) {
|
|
144
|
+
// Expect non-zero exit code
|
|
145
|
+
expect(error.code).toBe(1);
|
|
146
|
+
// Check that stderr contains error message
|
|
147
|
+
const combinedOutput = error.stdout || error.stderr || '';
|
|
148
|
+
expect(combinedOutput).toContain('Validation failed');
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should handle missing registry gracefully', async () => {
|
|
153
|
+
const { stdout } = await execAsync(
|
|
154
|
+
`node ${cliPath} validate --registry ./non-existent-registry.js`
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(stdout).toContain('Contract Validation Report');
|
|
158
|
+
expect(stdout).toContain('Total: 0');
|
|
159
|
+
expect(stdout).toContain('All contracts validated successfully');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should track drift when updating contracts', async () => {
|
|
163
|
+
const ledgerDir = path.join(tempDir, 'ledger');
|
|
164
|
+
|
|
165
|
+
// First validation
|
|
166
|
+
await execAsync(
|
|
167
|
+
`node ${cliPath} validate --registry ${sampleRegistryPath} --ledger ${ledgerDir} --author "test-user" 2>&1`
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Second validation (simulates contract update)
|
|
171
|
+
await execAsync(
|
|
172
|
+
`node ${cliPath} validate --registry ${sampleRegistryPath} --ledger ${ledgerDir} --author "test-user" 2>&1`
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Check that version was incremented
|
|
176
|
+
const indexPath = path.join(ledgerDir, 'logic-ledger', 'index.json');
|
|
177
|
+
const indexContent = await fs.readFile(indexPath, 'utf-8');
|
|
178
|
+
const index = JSON.parse(indexContent);
|
|
179
|
+
|
|
180
|
+
const ruleDirRelative = index.byRuleId['auth.login'];
|
|
181
|
+
const ruleDir = path.join(ledgerDir, ruleDirRelative);
|
|
182
|
+
const latestPath = path.join(ruleDir, 'LATEST.json');
|
|
183
|
+
const latestContent = await fs.readFile(latestPath, 'utf-8');
|
|
184
|
+
const latest = JSON.parse(latestContent);
|
|
185
|
+
|
|
186
|
+
// Second run should have version 2
|
|
187
|
+
expect(latest.version).toBe(2);
|
|
188
|
+
|
|
189
|
+
// Check that v0001.json and v0002.json exist
|
|
190
|
+
const v1Path = path.join(ruleDir, 'v0001.json');
|
|
191
|
+
const v2Path = path.join(ruleDir, 'v0002.json');
|
|
192
|
+
const v1Exists = await fs.access(v1Path).then(() => true).catch(() => false);
|
|
193
|
+
const v2Exists = await fs.access(v2Path).then(() => true).catch(() => false);
|
|
194
|
+
expect(v1Exists).toBe(true);
|
|
195
|
+
expect(v2Exists).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
});
|