@rigour-labs/core 1.0.0 → 1.2.1

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.
@@ -0,0 +1,150 @@
1
+ import ts from 'typescript';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { globby } from 'globby';
5
+ import { Gate, GateContext } from './base.js';
6
+ import { Failure, Gates } from '../types/index.js';
7
+ import micromatch from 'micromatch';
8
+
9
+ export class ASTGate extends Gate {
10
+ constructor(private config: Gates) {
11
+ super('ast-analysis', 'AST Structural Analysis');
12
+ }
13
+
14
+ async run(context: GateContext): Promise<Failure[]> {
15
+ const failures: Failure[] = [];
16
+ const files = await globby(['**/*.{ts,js,tsx,jsx}'], {
17
+ cwd: context.cwd,
18
+ ignore: ['node_modules/**', 'dist/**', 'build/**', '**/*.test.*', '**/*.spec.*'],
19
+ });
20
+
21
+ for (const file of files) {
22
+ const fullPath = path.join(context.cwd, file);
23
+ const content = await fs.readFile(fullPath, 'utf-8');
24
+ const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
25
+
26
+ this.analyzeSourceFile(sourceFile, file, failures);
27
+ }
28
+
29
+ return failures;
30
+ }
31
+
32
+ private analyzeSourceFile(sourceFile: ts.SourceFile, relativePath: string, failures: Failure[]) {
33
+ const astConfig = this.config.ast || {};
34
+ const maxComplexity = astConfig.complexity || 10;
35
+ const maxMethods = astConfig.max_methods || 10;
36
+ const maxParams = astConfig.max_params || 5;
37
+
38
+ const visit = (node: ts.Node) => {
39
+ // 1. Complexity & Params for functions
40
+ if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isArrowFunction(node)) {
41
+ const name = this.getNodeName(node);
42
+
43
+ // Parameter count
44
+ if (node.parameters.length > maxParams) {
45
+ failures.push(this.createFailure(
46
+ `Function '${name}' has ${node.parameters.length} parameters (max: ${maxParams})`,
47
+ [relativePath],
48
+ `Reduce number of parameters or use an options object.`
49
+ ));
50
+ // Update: Failures in Runner will be mapped to FixPacket
51
+ (failures[failures.length - 1] as any).metrics = { count: node.parameters.length, max: maxParams };
52
+ }
53
+
54
+ // Cyclomatic Complexity (Simplified: nodes that cause branching)
55
+ let complexity = 1;
56
+ const countComplexity = (n: ts.Node) => {
57
+ if (ts.isIfStatement(n) || ts.isCaseClause(n) || ts.isDefaultClause(n) ||
58
+ ts.isForStatement(n) || ts.isForInStatement(n) || ts.isForOfStatement(n) ||
59
+ ts.isWhileStatement(n) || ts.isDoStatement(n) || ts.isConditionalExpression(n)) {
60
+ complexity++;
61
+ }
62
+ if (ts.isBinaryExpression(n)) {
63
+ if (n.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
64
+ n.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
65
+ complexity++;
66
+ }
67
+ }
68
+ ts.forEachChild(n, countComplexity);
69
+ };
70
+ ts.forEachChild(node, countComplexity);
71
+
72
+ if (complexity > maxComplexity) {
73
+ failures.push(this.createFailure(
74
+ `Function '${name}' has cyclomatic complexity of ${complexity} (max: ${maxComplexity})`,
75
+ [relativePath],
76
+ `Refactor '${name}' into smaller, more focused functions.`
77
+ ));
78
+ (failures[failures.length - 1] as any).metrics = { complexity, max: maxComplexity };
79
+ }
80
+ }
81
+
82
+ // 2. Class metrics
83
+ if (ts.isClassDeclaration(node)) {
84
+ const name = node.name?.text || 'Anonymous Class';
85
+ const methods = node.members.filter(ts.isMethodDeclaration);
86
+
87
+ if (methods.length > maxMethods) {
88
+ failures.push(this.createFailure(
89
+ `Class '${name}' has ${methods.length} methods (max: ${maxMethods})`,
90
+ [relativePath],
91
+ `Class '${name}' is becoming a 'God Object'. Split it into smaller services.`
92
+ ));
93
+ (failures[failures.length - 1] as any).metrics = { methodCount: methods.length, max: maxMethods };
94
+ }
95
+ }
96
+
97
+ // 3. Import check for Layer Boundaries
98
+ if (ts.isImportDeclaration(node)) {
99
+ const importPath = (node.moduleSpecifier as ts.StringLiteral).text;
100
+ this.checkBoundary(importPath, relativePath, failures);
101
+ }
102
+
103
+ ts.forEachChild(node, visit);
104
+ };
105
+
106
+ ts.forEachChild(sourceFile, visit);
107
+ }
108
+
109
+ private checkBoundary(importPath: string, relativePath: string, failures: Failure[]) {
110
+ const boundaries = (this.config as any).architecture?.boundaries || [];
111
+ if (boundaries.length === 0) return;
112
+
113
+ for (const rule of boundaries) {
114
+ const isFromMatch = micromatch.isMatch(relativePath, rule.from);
115
+ if (isFromMatch) {
116
+ // Approximate resolution (simplified for now)
117
+ // Real implementation would need to handle alias and absolute path resolution
118
+ const resolved = importPath.startsWith('.')
119
+ ? path.join(path.dirname(relativePath), importPath)
120
+ : importPath;
121
+
122
+ const isToMatch = micromatch.isMatch(resolved, rule.to);
123
+
124
+ if (rule.mode === 'deny' && isToMatch) {
125
+ failures.push(this.createFailure(
126
+ `Architectural Violation: '${relativePath}' is forbidden from importing '${importPath}' (denied by boundary rule).`,
127
+ [relativePath],
128
+ `Remove this import to maintain architectural layering.`
129
+ ));
130
+ } else if (rule.mode === 'allow' && !isToMatch && importPath.startsWith('.')) {
131
+ // Complexity: Allow rules are trickier to implement strictly without full resolution
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ private getNodeName(node: ts.Node): string {
138
+ if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
139
+ return node.name?.getText() || 'anonymous';
140
+ }
141
+ if (ts.isArrowFunction(node)) {
142
+ const parent = node.parent;
143
+ if (ts.isVariableDeclaration(parent)) {
144
+ return parent.name.getText();
145
+ }
146
+ return 'anonymous arrow';
147
+ }
148
+ return 'unknown';
149
+ }
150
+ }
@@ -0,0 +1,44 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { Failure, Config } from '../types/index.js';
4
+ import { Gate, GateContext } from './base.js';
5
+
6
+ export class DependencyGate extends Gate {
7
+ constructor(private config: Config) {
8
+ super('dependency-guardian', 'Dependency Guardian');
9
+ }
10
+
11
+ async run(context: GateContext): Promise<Failure[]> {
12
+ const failures: Failure[] = [];
13
+ const forbidden = this.config.gates.dependencies?.forbid || [];
14
+
15
+ if (forbidden.length === 0) return [];
16
+
17
+ const { cwd } = context;
18
+
19
+ // 1. Scan Node.js (package.json)
20
+ const pkgPath = path.join(cwd, 'package.json');
21
+ if (await fs.pathExists(pkgPath)) {
22
+ try {
23
+ const pkg = await fs.readJson(pkgPath);
24
+ const allDeps = {
25
+ ...(pkg.dependencies || {}),
26
+ ...(pkg.devDependencies || {}),
27
+ ...(pkg.peerDependencies || {}),
28
+ };
29
+
30
+ for (const dep of forbidden) {
31
+ if (allDeps[dep]) {
32
+ failures.push(this.createFailure(
33
+ `The package '${dep}' is forbidden by project standards.`,
34
+ ['package.json'],
35
+ `Remove '${dep}' from package.json and use approved alternatives.`
36
+ ));
37
+ }
38
+ }
39
+ } catch (e) { }
40
+ }
41
+
42
+ return failures;
43
+ }
44
+ }
@@ -3,6 +3,9 @@ import { Failure, Config, Report, Status } from '../types/index.js';
3
3
  import { FileGate } from './file.js';
4
4
  import { ContentGate } from './content.js';
5
5
  import { StructureGate } from './structure.js';
6
+ import { ASTGate } from './ast.js';
7
+ import { SafetyGate } from './safety.js';
8
+ import { DependencyGate } from './dependency.js';
6
9
  import { execa } from 'execa';
7
10
  import { Logger } from '../utils/logger.js';
8
11
 
@@ -26,6 +29,9 @@ export class GateRunner {
26
29
  if (this.config.gates.required_files) {
27
30
  this.gates.push(new StructureGate({ requiredFiles: this.config.gates.required_files }));
28
31
  }
32
+ this.gates.push(new ASTGate(this.config.gates));
33
+ this.gates.push(new DependencyGate(this.config));
34
+ this.gates.push(new SafetyGate(this.config.gates));
29
35
  }
30
36
 
31
37
  /**
@@ -0,0 +1,49 @@
1
+ import { Gate, GateContext } from './base.js';
2
+ import { Failure, Gates } from '../types/index.js';
3
+ import { execa } from 'execa';
4
+
5
+ export class SafetyGate extends Gate {
6
+ constructor(private config: Gates) {
7
+ super('safety-rail', 'Safety & Protection Rails');
8
+ }
9
+
10
+ async run(context: GateContext): Promise<Failure[]> {
11
+ const failures: Failure[] = [];
12
+ const safety = this.config.safety || {};
13
+ const protectedPaths = safety.protected_paths || [];
14
+
15
+ if (protectedPaths.length === 0) return [];
16
+
17
+ try {
18
+ // Check for modified files in protected paths using git
19
+ // This is a "Safety Rail" - if an agent touched these, we fail.
20
+ const { stdout } = await execa('git', ['status', '--porcelain'], { cwd: context.cwd });
21
+ const modifiedFiles = stdout.split('\n')
22
+ .filter(line => line.trim().length > 0)
23
+ .map(line => line.slice(3));
24
+
25
+ for (const file of modifiedFiles) {
26
+ if (this.isProtected(file, protectedPaths)) {
27
+ failures.push(this.createFailure(
28
+ `Protected file '${file}' was modified.`,
29
+ [file],
30
+ `Agents are forbidden from modifying files in ${protectedPaths.join(', ')}.`
31
+ ));
32
+ }
33
+ }
34
+ } catch (error) {
35
+ // If not a git repo, skip safety for now
36
+ }
37
+
38
+ return failures;
39
+ }
40
+
41
+ private isProtected(file: string, patterns: string[]): boolean {
42
+ return patterns.some(p => {
43
+ const cleanP = p.replace('/**', '').replace('/*', '');
44
+ if (file === cleanP) return true;
45
+ if (cleanP.endsWith('/')) return file.startsWith(cleanP);
46
+ return file.startsWith(cleanP + '/');
47
+ });
48
+ }
49
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export * from './types/index.js';
2
2
  export * from './gates/runner.js';
3
3
  export * from './discovery.js';
4
+ export * from './services/fix-packet-service.js';
4
5
  export * from './templates/index.js';
6
+ export * from './types/fix-packet.js';
7
+ export { Gate, GateContext } from './gates/base.js';
5
8
  export * from './utils/logger.js';
@@ -0,0 +1,42 @@
1
+ import { Report, Failure, Config } from '../types/index.js';
2
+ import { FixPacketV2, FixPacketV2Schema } from '../types/fix-packet.js';
3
+
4
+ export class FixPacketService {
5
+ generate(report: Report, config: Config): FixPacketV2 {
6
+ const violations = report.failures.map(f => ({
7
+ id: f.id,
8
+ gate: f.id,
9
+ severity: this.inferSeverity(f),
10
+ title: f.title,
11
+ details: f.details,
12
+ files: f.files,
13
+ hint: f.hint,
14
+ instructions: f.hint ? [f.hint] : [], // Use hint as first instruction
15
+ metrics: (f as any).metrics,
16
+ }));
17
+
18
+ const packet: FixPacketV2 = {
19
+ version: 2,
20
+ goal: "Achieve PASS state by resolving all listed engineering violations.",
21
+ violations,
22
+ constraints: {
23
+ paradigm: config.paradigm,
24
+ protected_paths: config.gates.safety?.protected_paths,
25
+ do_not_touch: config.gates.safety?.protected_paths,
26
+ max_files_changed: config.gates.safety?.max_files_changed_per_cycle,
27
+ no_new_deps: true,
28
+ },
29
+ };
30
+
31
+ return FixPacketV2Schema.parse(packet);
32
+ }
33
+
34
+ private inferSeverity(f: Failure): "low" | "medium" | "high" | "critical" {
35
+ // High complexity or God objects are usually High severity
36
+ if (f.id === 'ast-analysis') return 'high';
37
+ // Unit test or Lint failures are Medium
38
+ if (f.id === 'test' || f.id === 'lint') return 'medium';
39
+ // Documentation or small file size issues are Low
40
+ return 'medium';
41
+ }
42
+ }
@@ -1,60 +1,132 @@
1
- import { Config } from '../types/index.js';
1
+ import { Config, Commands, Gates } from '../types/index.js';
2
2
 
3
3
  export interface Template {
4
4
  name: string;
5
5
  markers: string[];
6
- config: Partial<Config>;
6
+ config: {
7
+ preset?: string;
8
+ paradigm?: string;
9
+ commands?: Partial<Commands>;
10
+ gates?: Partial<Gates>;
11
+ planned?: string[];
12
+ };
7
13
  }
8
14
 
9
15
  export const TEMPLATES: Template[] = [
10
16
  {
11
- name: 'Node.js',
12
- markers: ['package.json'],
17
+ name: 'ui',
18
+ markers: [
19
+ 'package.json', // Check deps later
20
+ 'next.config.js',
21
+ 'vite.config.ts',
22
+ 'tailwind.config.js',
23
+ 'base.css',
24
+ 'index.html',
25
+ ],
13
26
  config: {
14
- commands: {
15
- lint: 'npm run lint',
16
- test: 'npm test',
27
+ preset: 'ui',
28
+ gates: {
29
+ max_file_lines: 300,
30
+ required_files: ['docs/SPEC.md', 'docs/ARCH.md', 'README.md'],
17
31
  },
32
+ planned: [
33
+ 'Layer Boundary: Components cannot import from DB',
34
+ 'Prop-Drilling Detection: Max depth 5',
35
+ ],
36
+ },
37
+ },
38
+ {
39
+ name: 'api',
40
+ markers: [
41
+ 'go.mod',
42
+ 'requirements.txt',
43
+ 'pyproject.toml',
44
+ 'package.json', // Check for backend frameworks later
45
+ 'app.py',
46
+ 'main.go',
47
+ 'index.js',
48
+ ],
49
+ config: {
50
+ preset: 'api',
18
51
  gates: {
19
- max_file_lines: 500,
20
- forbid_todos: true,
21
- forbid_fixme: true,
22
- forbid_paths: [],
52
+ max_file_lines: 400,
23
53
  required_files: ['docs/SPEC.md', 'docs/ARCH.md', 'README.md'],
24
54
  },
55
+ planned: [
56
+ 'Service Layer Enforcement: Controllers -> Services only',
57
+ 'Repo Pattern: Databases access isolated to repositories/',
58
+ ],
25
59
  },
26
60
  },
27
61
  {
28
- name: 'Python',
29
- markers: ['pyproject.toml', 'requirements.txt', 'setup.py'],
62
+ name: 'infra',
63
+ markers: [
64
+ 'Dockerfile',
65
+ 'docker-compose.yml',
66
+ 'main.tf',
67
+ 'k8s/',
68
+ 'helm/',
69
+ 'ansible/',
70
+ ],
30
71
  config: {
31
- commands: {
32
- lint: 'ruff check .',
33
- test: 'pytest',
72
+ preset: 'infra',
73
+ gates: {
74
+ max_file_lines: 300,
75
+ required_files: ['docs/RUNBOOK.md', 'docs/ARCH.md', 'README.md'],
34
76
  },
77
+ },
78
+ },
79
+ {
80
+ name: 'data',
81
+ markers: [
82
+ 'ipynb',
83
+ 'spark',
84
+ 'pandas',
85
+ 'data/',
86
+ 'dbt_project.yml',
87
+ ],
88
+ config: {
89
+ preset: 'data',
35
90
  gates: {
36
91
  max_file_lines: 500,
37
- forbid_todos: true,
38
- forbid_fixme: true,
39
- forbid_paths: [],
40
- required_files: ['docs/SPEC.md', 'docs/ARCH.md', 'README.md'],
92
+ required_files: ['docs/DATA_DICTIONARY.md', 'docs/PIPELINE.md', 'README.md'],
41
93
  },
94
+ planned: [
95
+ 'Stochastic Determinism: Seed setting enforcement',
96
+ 'Data Leaks: Detecting PII in notebook outputs',
97
+ ],
42
98
  },
43
99
  },
100
+ ];
101
+
102
+ export const PARADIGM_TEMPLATES: Template[] = [
44
103
  {
45
- name: 'Frontend (React/Vite/Next)',
46
- markers: ['next.config.js', 'vite.config.ts', 'tailwind.config.js'],
104
+ name: 'oop',
105
+ markers: [
106
+ 'class ',
107
+ 'interface ',
108
+ 'implements ',
109
+ 'extends ',
110
+ ],
47
111
  config: {
48
- commands: {
49
- lint: 'npm run lint',
50
- test: 'npm test',
112
+ paradigm: 'oop',
113
+ gates: {
114
+ // Future: class-specific gates
51
115
  },
116
+ },
117
+ },
118
+ {
119
+ name: 'functional',
120
+ markers: [
121
+ '=>',
122
+ 'export const',
123
+ 'map(',
124
+ 'filter(',
125
+ ],
126
+ config: {
127
+ paradigm: 'functional',
52
128
  gates: {
53
- max_file_lines: 300, // Frontend files often should be smaller
54
- forbid_todos: true,
55
- forbid_fixme: true,
56
- forbid_paths: [],
57
- required_files: ['docs/SPEC.md', 'docs/ARCH.md', 'README.md'],
129
+ // Future: function-specific gates
58
130
  },
59
131
  },
60
132
  },
@@ -69,8 +141,24 @@ export const UNIVERSAL_CONFIG: Config = {
69
141
  forbid_fixme: true,
70
142
  forbid_paths: [],
71
143
  required_files: ['docs/SPEC.md', 'docs/ARCH.md', 'docs/DECISIONS.md', 'docs/TASKS.md'],
144
+ ast: {
145
+ complexity: 10,
146
+ max_methods: 10,
147
+ max_params: 5,
148
+ },
149
+ dependencies: {
150
+ forbid: [],
151
+ },
152
+ architecture: {
153
+ boundaries: [],
154
+ },
155
+ safety: {
156
+ max_files_changed_per_cycle: 10,
157
+ protected_paths: ['.github/**', 'docs/**', 'rigour.yml'],
158
+ },
72
159
  },
73
160
  output: {
74
161
  report_path: 'rigour-report.json',
75
162
  },
163
+ planned: [],
76
164
  };
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Fix Packet v2 Schema
5
+ * Designed for high-fidelity communication with AI agents during the refinement loop.
6
+ */
7
+ export const FixPacketV2Schema = z.object({
8
+ version: z.literal(2),
9
+ goal: z.string().default('Achieve PASS state for all quality gates'),
10
+ violations: z.array(z.object({
11
+ id: z.string(),
12
+ gate: z.string(),
13
+ severity: z.enum(['low', 'medium', 'high', 'critical']).default('medium'),
14
+ category: z.string().optional(),
15
+ title: z.string(),
16
+ details: z.string(),
17
+ files: z.array(z.string()).optional(),
18
+ hint: z.string().optional(),
19
+ instructions: z.array(z.string()).optional(), // Step-by-step fix instructions
20
+ metrics: z.record(z.any()).optional(), // e.g., { complexity: 15, max: 10 }
21
+ })),
22
+ constraints: z.object({
23
+ protected_paths: z.array(z.string()).optional(),
24
+ do_not_touch: z.array(z.string()).optional(), // Alias for protected_paths
25
+ max_files_changed: z.number().optional(),
26
+ no_new_deps: z.boolean().optional().default(true),
27
+ allowed_dependencies: z.array(z.string()).optional(),
28
+ paradigm: z.string().optional(), // 'oop', 'functional'
29
+ }).optional().default({}),
30
+ });
31
+
32
+ export type FixPacketV2 = z.infer<typeof FixPacketV2Schema>;
@@ -11,6 +11,26 @@ export const GatesSchema = z.object({
11
11
  'docs/DECISIONS.md',
12
12
  'docs/TASKS.md',
13
13
  ]),
14
+ ast: z.object({
15
+ complexity: z.number().optional().default(10),
16
+ max_methods: z.number().optional().default(10),
17
+ max_params: z.number().optional().default(5),
18
+ }).optional().default({}),
19
+ dependencies: z.object({
20
+ forbid: z.array(z.string()).optional().default([]),
21
+ trusted_registry: z.string().optional(),
22
+ }).optional().default({}),
23
+ architecture: z.object({
24
+ boundaries: z.array(z.object({
25
+ from: z.string(),
26
+ to: z.string(),
27
+ mode: z.enum(['allow', 'deny']).default('deny'),
28
+ })).optional().default([]),
29
+ }).optional().default({}),
30
+ safety: z.object({
31
+ max_files_changed_per_cycle: z.number().optional().default(10),
32
+ protected_paths: z.array(z.string()).optional().default(['.github/**', 'docs/**', 'rigour.yml']),
33
+ }).optional().default({}),
14
34
  });
15
35
 
16
36
  export const CommandsSchema = z.object({
@@ -22,11 +42,14 @@ export const CommandsSchema = z.object({
22
42
 
23
43
  export const ConfigSchema = z.object({
24
44
  version: z.number().default(1),
45
+ preset: z.string().optional(),
46
+ paradigm: z.string().optional(),
25
47
  commands: CommandsSchema.optional().default({}),
26
48
  gates: GatesSchema.optional().default({}),
27
49
  output: z.object({
28
50
  report_path: z.string().default('rigour-report.json'),
29
51
  }).optional().default({}),
52
+ planned: z.array(z.string()).optional().default([]),
30
53
  });
31
54
 
32
55
  export type Gates = z.infer<typeof GatesSchema>;