@rigour-labs/core 1.0.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.
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReportSchema = exports.FailureSchema = exports.StatusSchema = exports.ConfigSchema = exports.CommandsSchema = exports.GatesSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ exports.GatesSchema = zod_1.z.object({
6
+ max_file_lines: zod_1.z.number().optional().default(500),
7
+ forbid_todos: zod_1.z.boolean().optional().default(true),
8
+ forbid_fixme: zod_1.z.boolean().optional().default(true),
9
+ forbid_paths: zod_1.z.array(zod_1.z.string()).optional().default([]),
10
+ required_files: zod_1.z.array(zod_1.z.string()).optional().default([
11
+ 'docs/SPEC.md',
12
+ 'docs/ARCH.md',
13
+ 'docs/DECISIONS.md',
14
+ 'docs/TASKS.md',
15
+ ]),
16
+ });
17
+ exports.CommandsSchema = zod_1.z.object({
18
+ format: zod_1.z.string().optional(),
19
+ lint: zod_1.z.string().optional(),
20
+ typecheck: zod_1.z.string().optional(),
21
+ test: zod_1.z.string().optional(),
22
+ });
23
+ exports.ConfigSchema = zod_1.z.object({
24
+ version: zod_1.z.number().default(1),
25
+ commands: exports.CommandsSchema.optional().default({}),
26
+ gates: exports.GatesSchema.optional().default({}),
27
+ output: zod_1.z.object({
28
+ report_path: zod_1.z.string().default('rigour-report.json'),
29
+ }).optional().default({}),
30
+ });
31
+ exports.StatusSchema = zod_1.z.enum(['PASS', 'FAIL', 'SKIP', 'ERROR']);
32
+ exports.FailureSchema = zod_1.z.object({
33
+ id: zod_1.z.string(),
34
+ title: zod_1.z.string(),
35
+ details: zod_1.z.string(),
36
+ files: zod_1.z.array(zod_1.z.string()).optional(),
37
+ hint: zod_1.z.string().optional(),
38
+ });
39
+ exports.ReportSchema = zod_1.z.object({
40
+ status: exports.StatusSchema,
41
+ summary: zod_1.z.record(exports.StatusSchema),
42
+ failures: zod_1.z.array(exports.FailureSchema),
43
+ stats: zod_1.z.object({
44
+ duration_ms: zod_1.z.number(),
45
+ }),
46
+ });
@@ -0,0 +1,14 @@
1
+ export declare enum LogLevel {
2
+ DEBUG = 0,
3
+ INFO = 1,
4
+ WARN = 2,
5
+ ERROR = 3
6
+ }
7
+ export declare class Logger {
8
+ private static level;
9
+ static setLevel(level: LogLevel): void;
10
+ static info(message: string): void;
11
+ static warn(message: string): void;
12
+ static error(message: string, error?: any): void;
13
+ static debug(message: string): void;
14
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Logger = exports.LogLevel = void 0;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ var LogLevel;
9
+ (function (LogLevel) {
10
+ LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
11
+ LogLevel[LogLevel["INFO"] = 1] = "INFO";
12
+ LogLevel[LogLevel["WARN"] = 2] = "WARN";
13
+ LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
14
+ })(LogLevel || (exports.LogLevel = LogLevel = {}));
15
+ class Logger {
16
+ static level = LogLevel.INFO;
17
+ static setLevel(level) {
18
+ this.level = level;
19
+ }
20
+ static info(message) {
21
+ if (this.level <= LogLevel.INFO) {
22
+ console.log(chalk_1.default.blue('info: ') + message);
23
+ }
24
+ }
25
+ static warn(message) {
26
+ if (this.level <= LogLevel.WARN) {
27
+ console.log(chalk_1.default.yellow('warn: ') + message);
28
+ }
29
+ }
30
+ static error(message, error) {
31
+ if (this.level <= LogLevel.ERROR) {
32
+ console.error(chalk_1.default.red('error: ') + message);
33
+ if (error) {
34
+ console.error(error);
35
+ }
36
+ }
37
+ }
38
+ static debug(message) {
39
+ if (this.level <= LogLevel.DEBUG) {
40
+ console.log(chalk_1.default.dim('debug: ') + message);
41
+ }
42
+ }
43
+ }
44
+ exports.Logger = Logger;
@@ -0,0 +1,11 @@
1
+ export interface ScannerOptions {
2
+ cwd: string;
3
+ patterns?: string[];
4
+ ignore?: string[];
5
+ }
6
+ export declare class FileScanner {
7
+ private static DEFAULT_PATTERNS;
8
+ private static DEFAULT_IGNORE;
9
+ static findFiles(options: ScannerOptions): Promise<string[]>;
10
+ static readFiles(cwd: string, files: string[]): Promise<Map<string, string>>;
11
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.FileScanner = void 0;
7
+ const globby_1 = require("globby");
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const path_1 = __importDefault(require("path"));
10
+ class FileScanner {
11
+ static DEFAULT_PATTERNS = ['**/*.{ts,js,py,css,html,md}'];
12
+ static DEFAULT_IGNORE = [
13
+ '**/node_modules/**',
14
+ '**/dist/**',
15
+ '**/package-lock.json',
16
+ '**/pnpm-lock.yaml',
17
+ '**/.git/**',
18
+ 'rigour-report.json'
19
+ ];
20
+ static async findFiles(options) {
21
+ return (0, globby_1.globby)(options.patterns || this.DEFAULT_PATTERNS, {
22
+ cwd: options.cwd,
23
+ ignore: options.ignore || this.DEFAULT_IGNORE,
24
+ });
25
+ }
26
+ static async readFiles(cwd, files) {
27
+ const contents = new Map();
28
+ for (const file of files) {
29
+ const filePath = path_1.default.join(cwd, file);
30
+ contents.set(file, await fs_extra_1.default.readFile(filePath, 'utf-8'));
31
+ }
32
+ return contents;
33
+ }
34
+ }
35
+ exports.FileScanner = FileScanner;
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@rigour-labs/core",
3
+ "version": "1.0.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/erashu212/rigour"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public",
12
+ "provenance": true
13
+ },
14
+ "dependencies": {
15
+ "chalk": "^5.6.2",
16
+ "execa": "^8.0.1",
17
+ "fs-extra": "^11.3.3",
18
+ "globby": "^14.1.0",
19
+ "yaml": "^2.3.4",
20
+ "zod": "^3.22.4"
21
+ },
22
+ "devDependencies": {
23
+ "@types/fs-extra": "^11.0.4",
24
+ "@types/node": "^25.0.3"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "test": "vitest run"
29
+ }
30
+ }
@@ -0,0 +1,30 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { Config } from './types/index.js';
4
+ import { TEMPLATES, UNIVERSAL_CONFIG } from './templates/index.js';
5
+
6
+ export class DiscoveryService {
7
+ async discover(cwd: string): Promise<Config> {
8
+ const config = { ...UNIVERSAL_CONFIG };
9
+
10
+ for (const template of TEMPLATES) {
11
+ const match = await this.hasAnyMarker(cwd, template.markers);
12
+ if (match) {
13
+ // Merge template config
14
+ config.commands = { ...config.commands, ...template.config.commands };
15
+ config.gates = { ...config.gates, ...template.config.gates };
16
+ }
17
+ }
18
+
19
+ return config;
20
+ }
21
+
22
+ private async hasAnyMarker(cwd: string, markers: string[]): Promise<boolean> {
23
+ for (const marker of markers) {
24
+ if (await fs.pathExists(path.join(cwd, marker))) {
25
+ return true;
26
+ }
27
+ }
28
+ return false;
29
+ }
30
+ }
@@ -0,0 +1,21 @@
1
+ import { Failure } from '../types/index.js';
2
+
3
+ export interface GateContext {
4
+ cwd: string;
5
+ }
6
+
7
+ export abstract class Gate {
8
+ constructor(public readonly id: string, public readonly title: string) { }
9
+
10
+ abstract run(context: GateContext): Promise<Failure[]>;
11
+
12
+ protected createFailure(details: string, files?: string[], hint?: string): Failure {
13
+ return {
14
+ id: this.id,
15
+ title: this.title,
16
+ details,
17
+ files,
18
+ hint,
19
+ };
20
+ }
21
+ }
@@ -0,0 +1,47 @@
1
+ import { Gate, GateContext } from './base.js';
2
+ import { Failure } from '../types/index.js';
3
+ import { FileScanner } from '../utils/scanner.js';
4
+
5
+ export interface ContentGateConfig {
6
+ forbidTodos: boolean;
7
+ forbidFixme: boolean;
8
+ }
9
+
10
+ export class ContentGate extends Gate {
11
+ constructor(private config: ContentGateConfig) {
12
+ super('content-check', 'Forbidden Content');
13
+ }
14
+
15
+ async run(context: GateContext): Promise<Failure[]> {
16
+ const patterns = [];
17
+ if (this.config.forbidTodos) patterns.push(/TODO/i);
18
+ if (this.config.forbidFixme) patterns.push(/FIXME/i);
19
+
20
+ if (patterns.length === 0) return [];
21
+
22
+ const files = await FileScanner.findFiles({ cwd: context.cwd });
23
+ const contents = await FileScanner.readFiles(context.cwd, files);
24
+
25
+ const violations: string[] = [];
26
+ for (const [file, content] of contents) {
27
+ for (const pattern of patterns) {
28
+ if (pattern.test(content)) {
29
+ violations.push(file);
30
+ break;
31
+ }
32
+ }
33
+ }
34
+
35
+ if (violations.length > 0) {
36
+ return [
37
+ this.createFailure(
38
+ 'Forbidden placeholders found in the following files:',
39
+ violations,
40
+ 'Remove all TODO and FIXME comments. Use the "Done is Done" mentality—address the root cause or create a tracked issue.'
41
+ ),
42
+ ];
43
+ }
44
+
45
+ return [];
46
+ }
47
+ }
@@ -0,0 +1,38 @@
1
+ import { Gate, GateContext } from './base.js';
2
+ import { Failure } from '../types/index.js';
3
+ import { FileScanner } from '../utils/scanner.js';
4
+
5
+ export interface FileGateConfig {
6
+ maxLines: number;
7
+ }
8
+
9
+ export class FileGate extends Gate {
10
+ constructor(private config: FileGateConfig) {
11
+ super('file-size', 'File Size Limit');
12
+ }
13
+
14
+ async run(context: GateContext): Promise<Failure[]> {
15
+ const files = await FileScanner.findFiles({ cwd: context.cwd });
16
+ const contents = await FileScanner.readFiles(context.cwd, files);
17
+
18
+ const violations: string[] = [];
19
+ for (const [file, content] of contents) {
20
+ const lines = content.split('\n').length;
21
+ if (lines > this.config.maxLines) {
22
+ violations.push(`${file} (${lines} lines)`);
23
+ }
24
+ }
25
+
26
+ if (violations.length > 0) {
27
+ return [
28
+ this.createFailure(
29
+ `The following files exceed the maximum limit of ${this.config.maxLines} lines:`,
30
+ violations,
31
+ 'Break these files into smaller, more modular components to improve maintainability (SOLID - Single Responsibility Principle).'
32
+ ),
33
+ ];
34
+ }
35
+
36
+ return [];
37
+ }
38
+ }
@@ -0,0 +1,101 @@
1
+ import { Gate } from './base.js';
2
+ import { Failure, Config, Report, Status } from '../types/index.js';
3
+ import { FileGate } from './file.js';
4
+ import { ContentGate } from './content.js';
5
+ import { StructureGate } from './structure.js';
6
+ import { execa } from 'execa';
7
+ import { Logger } from '../utils/logger.js';
8
+
9
+ export class GateRunner {
10
+ private gates: Gate[] = [];
11
+
12
+ constructor(private config: Config) {
13
+ this.initializeGates();
14
+ }
15
+
16
+ private initializeGates() {
17
+ if (this.config.gates.max_file_lines) {
18
+ this.gates.push(new FileGate({ maxLines: this.config.gates.max_file_lines }));
19
+ }
20
+ this.gates.push(
21
+ new ContentGate({
22
+ forbidTodos: !!this.config.gates.forbid_todos,
23
+ forbidFixme: !!this.config.gates.forbid_fixme,
24
+ })
25
+ );
26
+ if (this.config.gates.required_files) {
27
+ this.gates.push(new StructureGate({ requiredFiles: this.config.gates.required_files }));
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Allows adding custom gates dynamically (SOLID - Open/Closed Principle)
33
+ */
34
+ addGate(gate: Gate) {
35
+ this.gates.push(gate);
36
+ }
37
+
38
+ async run(cwd: string): Promise<Report> {
39
+ const start = Date.now();
40
+ const failures: Failure[] = [];
41
+ const summary: Record<string, Status> = {};
42
+
43
+ // 1. Run internal gates
44
+ for (const gate of this.gates) {
45
+ try {
46
+ const gateFailures = await gate.run({ cwd });
47
+ if (gateFailures.length > 0) {
48
+ failures.push(...gateFailures);
49
+ summary[gate.id] = 'FAIL';
50
+ } else {
51
+ summary[gate.id] = 'PASS';
52
+ }
53
+ } catch (error: any) {
54
+ Logger.error(`Gate ${gate.id} failed with error: ${error.message}`);
55
+ summary[gate.id] = 'ERROR';
56
+ failures.push({
57
+ id: gate.id,
58
+ title: `Gate Error: ${gate.title}`,
59
+ details: error.message,
60
+ hint: 'There was an internal error running this gate. Check the logs.',
61
+ });
62
+ }
63
+ }
64
+
65
+ // 2. Run command gates (lint, test, etc.)
66
+ const commands = this.config.commands;
67
+ if (commands) {
68
+ for (const [key, cmd] of Object.entries(commands)) {
69
+ if (!cmd) {
70
+ summary[key] = 'SKIP';
71
+ continue;
72
+ }
73
+
74
+ try {
75
+ Logger.info(`Running command gate: ${key} (${cmd})`);
76
+ await execa(cmd, { shell: true, cwd });
77
+ summary[key] = 'PASS';
78
+ } catch (error: any) {
79
+ summary[key] = 'FAIL';
80
+ failures.push({
81
+ id: key,
82
+ title: `${key.toUpperCase()} Check Failed`,
83
+ details: error.stderr || error.stdout || error.message,
84
+ hint: `Fix the issues reported by \`${cmd}\`. Use rigorous standards (SOLID, DRY) in your resolution.`,
85
+ });
86
+ }
87
+ }
88
+ }
89
+
90
+ const status: Status = failures.length > 0 ? 'FAIL' : 'PASS';
91
+
92
+ return {
93
+ status,
94
+ summary,
95
+ failures,
96
+ stats: {
97
+ duration_ms: Date.now() - start,
98
+ },
99
+ };
100
+ }
101
+ }
@@ -0,0 +1,36 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { Gate, GateContext } from './base.js';
4
+ import { Failure } from '../types/index.js';
5
+
6
+ export interface StructureGateConfig {
7
+ requiredFiles: string[];
8
+ }
9
+
10
+ export class StructureGate extends Gate {
11
+ constructor(private config: StructureGateConfig) {
12
+ super('structure-check', 'Project Structure');
13
+ }
14
+
15
+ async run(context: GateContext): Promise<Failure[]> {
16
+ const missing: string[] = [];
17
+ for (const file of this.config.requiredFiles) {
18
+ const filePath = path.join(context.cwd, file);
19
+ if (!(await fs.pathExists(filePath))) {
20
+ missing.push(file);
21
+ }
22
+ }
23
+
24
+ if (missing.length > 0) {
25
+ return [
26
+ this.createFailure(
27
+ 'The following required files are missing:',
28
+ missing,
29
+ 'Create these files to maintain project documentation and consistency.'
30
+ ),
31
+ ];
32
+ }
33
+
34
+ return [];
35
+ }
36
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './types/index.js';
2
+ export * from './gates/runner.js';
3
+ export * from './discovery.js';
4
+ export * from './templates/index.js';
5
+ export * from './utils/logger.js';
@@ -0,0 +1,18 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { GateRunner } from '../src/gates/runner.js';
3
+
4
+ describe('GateRunner Smoke Test', () => {
5
+ it('should initialize with empty config', async () => {
6
+ const config = {
7
+ version: 1,
8
+ commands: {},
9
+ gates: {
10
+ max_file_lines: 500,
11
+ forbid_todos: true,
12
+ forbid_fixme: true,
13
+ },
14
+ };
15
+ const runner = new GateRunner(config as any);
16
+ expect(runner).toBeDefined();
17
+ });
18
+ });
@@ -0,0 +1,76 @@
1
+ import { Config } from '../types/index.js';
2
+
3
+ export interface Template {
4
+ name: string;
5
+ markers: string[];
6
+ config: Partial<Config>;
7
+ }
8
+
9
+ export const TEMPLATES: Template[] = [
10
+ {
11
+ name: 'Node.js',
12
+ markers: ['package.json'],
13
+ config: {
14
+ commands: {
15
+ lint: 'npm run lint',
16
+ test: 'npm test',
17
+ },
18
+ gates: {
19
+ max_file_lines: 500,
20
+ forbid_todos: true,
21
+ forbid_fixme: true,
22
+ forbid_paths: [],
23
+ required_files: ['docs/SPEC.md', 'docs/ARCH.md', 'README.md'],
24
+ },
25
+ },
26
+ },
27
+ {
28
+ name: 'Python',
29
+ markers: ['pyproject.toml', 'requirements.txt', 'setup.py'],
30
+ config: {
31
+ commands: {
32
+ lint: 'ruff check .',
33
+ test: 'pytest',
34
+ },
35
+ gates: {
36
+ 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'],
41
+ },
42
+ },
43
+ },
44
+ {
45
+ name: 'Frontend (React/Vite/Next)',
46
+ markers: ['next.config.js', 'vite.config.ts', 'tailwind.config.js'],
47
+ config: {
48
+ commands: {
49
+ lint: 'npm run lint',
50
+ test: 'npm test',
51
+ },
52
+ 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'],
58
+ },
59
+ },
60
+ },
61
+ ];
62
+
63
+ export const UNIVERSAL_CONFIG: Config = {
64
+ version: 1,
65
+ commands: {},
66
+ gates: {
67
+ max_file_lines: 500,
68
+ forbid_todos: true,
69
+ forbid_fixme: true,
70
+ forbid_paths: [],
71
+ required_files: ['docs/SPEC.md', 'docs/ARCH.md', 'docs/DECISIONS.md', 'docs/TASKS.md'],
72
+ },
73
+ output: {
74
+ report_path: 'rigour-report.json',
75
+ },
76
+ };
@@ -0,0 +1,56 @@
1
+ import { z } from 'zod';
2
+
3
+ export const GatesSchema = z.object({
4
+ max_file_lines: z.number().optional().default(500),
5
+ forbid_todos: z.boolean().optional().default(true),
6
+ forbid_fixme: z.boolean().optional().default(true),
7
+ forbid_paths: z.array(z.string()).optional().default([]),
8
+ required_files: z.array(z.string()).optional().default([
9
+ 'docs/SPEC.md',
10
+ 'docs/ARCH.md',
11
+ 'docs/DECISIONS.md',
12
+ 'docs/TASKS.md',
13
+ ]),
14
+ });
15
+
16
+ export const CommandsSchema = z.object({
17
+ format: z.string().optional(),
18
+ lint: z.string().optional(),
19
+ typecheck: z.string().optional(),
20
+ test: z.string().optional(),
21
+ });
22
+
23
+ export const ConfigSchema = z.object({
24
+ version: z.number().default(1),
25
+ commands: CommandsSchema.optional().default({}),
26
+ gates: GatesSchema.optional().default({}),
27
+ output: z.object({
28
+ report_path: z.string().default('rigour-report.json'),
29
+ }).optional().default({}),
30
+ });
31
+
32
+ export type Gates = z.infer<typeof GatesSchema>;
33
+ export type Commands = z.infer<typeof CommandsSchema>;
34
+ export type Config = z.infer<typeof ConfigSchema>;
35
+
36
+ export const StatusSchema = z.enum(['PASS', 'FAIL', 'SKIP', 'ERROR']);
37
+ export type Status = z.infer<typeof StatusSchema>;
38
+
39
+ export const FailureSchema = z.object({
40
+ id: z.string(),
41
+ title: z.string(),
42
+ details: z.string(),
43
+ files: z.array(z.string()).optional(),
44
+ hint: z.string().optional(),
45
+ });
46
+ export type Failure = z.infer<typeof FailureSchema>;
47
+
48
+ export const ReportSchema = z.object({
49
+ status: StatusSchema,
50
+ summary: z.record(StatusSchema),
51
+ failures: z.array(FailureSchema),
52
+ stats: z.object({
53
+ duration_ms: z.number(),
54
+ }),
55
+ });
56
+ export type Report = z.infer<typeof ReportSchema>;
@@ -0,0 +1,43 @@
1
+ import chalk from 'chalk';
2
+
3
+ export enum LogLevel {
4
+ DEBUG = 0,
5
+ INFO = 1,
6
+ WARN = 2,
7
+ ERROR = 3,
8
+ }
9
+
10
+ export class Logger {
11
+ private static level: LogLevel = LogLevel.INFO;
12
+
13
+ static setLevel(level: LogLevel) {
14
+ this.level = level;
15
+ }
16
+
17
+ static info(message: string) {
18
+ if (this.level <= LogLevel.INFO) {
19
+ console.log(chalk.blue('info: ') + message);
20
+ }
21
+ }
22
+
23
+ static warn(message: string) {
24
+ if (this.level <= LogLevel.WARN) {
25
+ console.log(chalk.yellow('warn: ') + message);
26
+ }
27
+ }
28
+
29
+ static error(message: string, error?: any) {
30
+ if (this.level <= LogLevel.ERROR) {
31
+ console.error(chalk.red('error: ') + message);
32
+ if (error) {
33
+ console.error(error);
34
+ }
35
+ }
36
+ }
37
+
38
+ static debug(message: string) {
39
+ if (this.level <= LogLevel.DEBUG) {
40
+ console.log(chalk.dim('debug: ') + message);
41
+ }
42
+ }
43
+ }