@bytesbrains/pi-contrib-gate 1.6.1 → 1.7.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.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Contrib Gate for Pi
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/pi-contrib-gate)](https://www.npmjs.com/package/pi-contrib-gate)
4
- [![license](https://img.shields.io/npm/l/pi-contrib-gate)](./LICENSE)
3
+ [![npm version](https://img.shields.io/npm/v/@bytesbrains/pi-contrib-gate)](https://www.npmjs.com/package/@bytesbrains/pi-contrib-gate)
4
+ [![license](https://img.shields.io/npm/l/@bytesbrains/pi-contrib-gate)](./LICENSE)
5
5
 
6
6
  > Contribution gateway for AI agents — enforce branch naming, conventional commits, pre-commit quality gates, and PR automation. **Agents don't call `git push` — they call `contrib_submit()`.**
7
7
 
@@ -10,7 +10,7 @@
10
10
  ## Install
11
11
 
12
12
  ```bash
13
- pi install npm:pi-contrib-gate
13
+ pi install npm:@bytesbrains/pi-contrib-gate
14
14
  ```
15
15
 
16
16
  ## Tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytesbrains/pi-contrib-gate",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "description": "Contribution gateway for AI agents \u2014 enforce branch naming, conventional commits, pre-commit quality gates, and PR automation.",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/types.ts CHANGED
@@ -1,64 +1,70 @@
1
1
  export interface BestPracticesConfig {
2
- shortFrequentCommits: boolean;
3
- maxLinesPerCommit: number;
4
- requireAtomic: boolean;
5
- maxUnrelatedDirs: number;
6
- guidanceText: string[];
2
+ shortFrequentCommits: boolean;
3
+ maxLinesPerCommit: number;
4
+ requireAtomic: boolean;
5
+ maxUnrelatedDirs: number;
6
+ guidanceText: string[];
7
7
  }
8
8
 
9
9
  export interface ContribConfig {
10
- branches: {
11
- featPattern: string;
12
- fixPattern: string;
13
- chorePattern: string;
14
- };
15
- commits: {
16
- convention: "conventional" | "simple";
17
- maxSubjectLength: number;
18
- scopes: string[];
19
- bestPractices: BestPracticesConfig;
20
- };
21
- quality: {
22
- lint: boolean;
23
- typeCheck: boolean;
24
- doctorAudit: boolean;
25
- maxFilesChanged: number;
26
- maxLinesAdded: number;
27
- };
28
- /** Validate that the linked Gitea issue actually exists before starting work (default: true) */
29
- requireIssueValidation: boolean;
10
+ branches: {
11
+ featPattern: string;
12
+ fixPattern: string;
13
+ chorePattern: string;
14
+ };
15
+ commits: {
16
+ convention: "conventional" | "simple";
17
+ maxSubjectLength: number;
18
+ scopes: string[];
19
+ bestPractices: BestPracticesConfig;
20
+ };
21
+ quality: {
22
+ lint: boolean;
23
+ typeCheck: boolean;
24
+ doctorAudit: boolean;
25
+ maxFilesChanged: number;
26
+ maxLinesAdded: number;
27
+ /** Block commits if LSP errors exist in staged files (default: true) */
28
+ lensErrors: boolean;
29
+ /** Max allowed LSP errors in staged files (default: 0) */
30
+ maxLensErrors: number;
31
+ };
32
+ /** Validate that the linked Gitea issue actually exists before starting work (default: true) */
33
+ requireIssueValidation: boolean;
30
34
  }
31
35
 
32
36
  export const BEST_PRACTICES_DEFAULTS: BestPracticesConfig = {
33
- shortFrequentCommits: true,
34
- maxLinesPerCommit: 150,
35
- requireAtomic: true,
36
- maxUnrelatedDirs: 3,
37
- guidanceText: [
38
- "Commit after every logical unit of work.",
39
- "Aim for < 150 lines per commit.",
40
- "Each commit should do one thing — keep changes atomic.",
41
- ],
37
+ shortFrequentCommits: true,
38
+ maxLinesPerCommit: 150,
39
+ requireAtomic: true,
40
+ maxUnrelatedDirs: 3,
41
+ guidanceText: [
42
+ "Commit after every logical unit of work.",
43
+ "Aim for < 150 lines per commit.",
44
+ "Each commit should do one thing — keep changes atomic.",
45
+ ],
42
46
  };
43
47
 
44
48
  export const DEFAULT_CONFIG: ContribConfig = {
45
- branches: {
46
- featPattern: "feat/",
47
- fixPattern: "fix/",
48
- chorePattern: "chore/",
49
- },
50
- commits: {
51
- convention: "conventional",
52
- maxSubjectLength: 72,
53
- scopes: [],
54
- bestPractices: { ...BEST_PRACTICES_DEFAULTS },
55
- },
56
- quality: {
57
- lint: true,
58
- typeCheck: true,
59
- doctorAudit: true,
60
- maxFilesChanged: 20,
61
- maxLinesAdded: 500,
62
- },
63
- requireIssueValidation: true,
49
+ branches: {
50
+ featPattern: "feat/",
51
+ fixPattern: "fix/",
52
+ chorePattern: "chore/",
53
+ },
54
+ commits: {
55
+ convention: "conventional",
56
+ maxSubjectLength: 72,
57
+ scopes: [],
58
+ bestPractices: { ...BEST_PRACTICES_DEFAULTS },
59
+ },
60
+ quality: {
61
+ lint: true,
62
+ typeCheck: true,
63
+ doctorAudit: true,
64
+ maxFilesChanged: 20,
65
+ maxLinesAdded: 500,
66
+ lensErrors: true,
67
+ maxLensErrors: 0,
68
+ },
69
+ requireIssueValidation: true,
64
70
  };
package/src/validate.ts CHANGED
@@ -2,52 +2,114 @@ import * as path from "node:path";
2
2
  import type { ContribConfig } from "./types";
3
3
  import { exec, scanForConflictMarkers } from "./helpers";
4
4
 
5
- export function validateConventionalCommit(message: string, config: ContribConfig): { ok: true } | { ok: false; error: string } {
6
- const firstLine = message.split("\n")[0].trim();
7
- const convPattern = /^(feat|fix|chore|docs|style|refactor|test|perf|ci|build|revert)(\([^)]+\))?:\s.+$/;
8
- if (!convPattern.test(firstLine)) {
9
- return { ok: false, error: `Commit message must follow conventional commits format: type(scope): subject\nExamples: feat(api): add endpoint, fix: resolve null pointer, chore: update deps` };
10
- }
11
- if (firstLine.length > config.commits.maxSubjectLength) {
12
- return { ok: false, error: `Subject line too long (${firstLine.length} > ${config.commits.maxSubjectLength} chars).` };
13
- }
14
- return { ok: true };
5
+ export function validateConventionalCommit(
6
+ message: string,
7
+ config: ContribConfig,
8
+ ): { ok: true } | { ok: false; error: string } {
9
+ const firstLine = message.split("\n")[0].trim();
10
+ const convPattern =
11
+ /^(feat|fix|chore|docs|style|refactor|test|perf|ci|build|revert)(\([^)]+\))?:\s.+$/;
12
+ if (!convPattern.test(firstLine)) {
13
+ return {
14
+ ok: false,
15
+ error: `Commit message must follow conventional commits format: type(scope): subject\nExamples: feat(api): add endpoint, fix: resolve null pointer, chore: update deps`,
16
+ };
17
+ }
18
+ if (firstLine.length > config.commits.maxSubjectLength) {
19
+ return {
20
+ ok: false,
21
+ error: `Subject line too long (${firstLine.length} > ${config.commits.maxSubjectLength} chars).`,
22
+ };
23
+ }
24
+ return { ok: true };
15
25
  }
16
26
 
17
- export function validateBranchName(branch: string): { ok: true } | { ok: false; error: string } {
18
- if (/^(feat|fix|chore)\//.test(branch)) return { ok: true };
19
- return { ok: false, error: `Branch name "${branch}" does not follow convention: feat/*, fix/*, or chore/*` };
27
+ export function validateBranchName(
28
+ branch: string,
29
+ ): { ok: true } | { ok: false; error: string } {
30
+ if (/^(feat|fix|chore)\//.test(branch)) return { ok: true };
31
+ return {
32
+ ok: false,
33
+ error: `Branch name "${branch}" does not follow convention: feat/*, fix/*, or chore/*`,
34
+ };
20
35
  }
21
36
 
22
- export function runQualityGate(cwd: string, config: ContribConfig): { ok: true } | { ok: false; errors: string[] } {
23
- const errors: string[] = [];
37
+ export function runQualityGate(
38
+ cwd: string,
39
+ config: ContribConfig,
40
+ ): { ok: true } | { ok: false; errors: string[] } {
41
+ const errors: string[] = [];
24
42
 
25
- // ── Conflict marker check (MUST come first — safety-critical) ──
26
- const conflictFiles = scanForConflictMarkers(cwd);
27
- if (conflictFiles.length > 0) {
28
- errors.push(
29
- `Unresolved merge conflict markers found in staged files: ${conflictFiles.join(", ")}.\n Remove all <<<<<<<, =======, >>>>>>> markers before committing.`,
30
- );
31
- }
43
+ // ── Conflict marker check (MUST come first — safety-critical) ──
44
+ const conflictFiles = scanForConflictMarkers(cwd);
45
+ if (conflictFiles.length > 0) {
46
+ errors.push(
47
+ `Unresolved merge conflict markers found in staged files: ${conflictFiles.join(", ")}.\n Remove all <<<<<<<, =======, >>>>>>> markers before committing.`,
48
+ );
49
+ }
32
50
 
33
- const diff = exec("git diff --cached --name-only", cwd);
34
- if (diff.ok) {
35
- const files = diff.stdout.split("\n").filter(Boolean);
36
- if (files.length > config.quality.maxFilesChanged) {
37
- errors.push(`Too many files changed (${files.length} > ${config.quality.maxFilesChanged}).`);
38
- }
39
- const loc = exec("git diff --cached --numstat | awk '{s+=$1} END {print s}'", cwd);
40
- if (loc.ok && parseInt(loc.stdout) > config.quality.maxLinesAdded) {
41
- errors.push(`Too many lines added (${loc.stdout} > ${config.quality.maxLinesAdded}).`);
42
- }
43
- }
44
- if (config.quality.typeCheck) {
45
- const tsc = exec("npx tsc --noEmit 2>&1 || true", path.join(cwd, "factory"));
46
- if (tsc.stdout.includes("error TS")) errors.push("TypeScript errors found. Run: npx tsc --noEmit");
47
- }
48
- if (config.quality.lint) {
49
- const lint = exec("npm run lint 2>&1 || true", path.join(cwd, "factory"));
50
- if (lint.stdout.includes("error") && !lint.stdout.includes("0 errors")) errors.push("Lint errors found.");
51
- }
52
- return errors.length === 0 ? { ok: true } : { ok: false, errors };
51
+ const diff = exec("git diff --cached --name-only", cwd);
52
+ if (diff.ok) {
53
+ const files = diff.stdout.split("\n").filter(Boolean);
54
+ if (files.length > config.quality.maxFilesChanged) {
55
+ errors.push(
56
+ `Too many files changed (${files.length} > ${config.quality.maxFilesChanged}).`,
57
+ );
58
+ }
59
+ const loc = exec(
60
+ "git diff --cached --numstat | awk '{s+=$1} END {print s}'",
61
+ cwd,
62
+ );
63
+ if (loc.ok && parseInt(loc.stdout) > config.quality.maxLinesAdded) {
64
+ errors.push(
65
+ `Too many lines added (${loc.stdout} > ${config.quality.maxLinesAdded}).`,
66
+ );
67
+ }
68
+ }
69
+ if (config.quality.typeCheck) {
70
+ const tsc = exec(
71
+ "npx tsc --noEmit 2>&1 || true",
72
+ path.join(cwd, "factory"),
73
+ );
74
+ if (tsc.stdout.includes("error TS"))
75
+ errors.push("TypeScript errors found. Run: npx tsc --noEmit");
76
+ }
77
+ if (config.quality.lensErrors) {
78
+ // Run LSP diagnostics on staged TypeScript/TSX files
79
+ const staged = exec("git diff --cached --name-only --diff-filter=ACM", cwd);
80
+ if (staged.ok) {
81
+ const tsFiles = staged.stdout
82
+ .split("\n")
83
+ .filter((f) => /\.(ts|tsx)$/.test(f));
84
+ if (tsFiles.length > 0) {
85
+ let totalErrors = 0;
86
+ const errorFiles: string[] = [];
87
+ for (const file of tsFiles) {
88
+ const tscCheck = exec(
89
+ `npx tsc --noEmit --pretty false 2>&1 || true`,
90
+ cwd,
91
+ );
92
+ if (tscCheck.ok && tscCheck.stdout.includes(file)) {
93
+ const matches = tscCheck.stdout.match(new RegExp(file, "g"));
94
+ const fileErrors = matches ? matches.length : 0;
95
+ if (fileErrors > 0) {
96
+ totalErrors += fileErrors;
97
+ errorFiles.push(`${file} (${fileErrors} errors)`);
98
+ }
99
+ }
100
+ }
101
+ if (totalErrors > config.quality.maxLensErrors) {
102
+ errors.push(
103
+ `LSP errors in staged files (${totalErrors} > ${config.quality.maxLensErrors} allowed).\n Affected: ${errorFiles.join(", ")}.\n Run: npx tsc --noEmit`,
104
+ );
105
+ }
106
+ }
107
+ }
108
+ }
109
+ if (config.quality.lint) {
110
+ const lint = exec("npm run lint 2>&1 || true", path.join(cwd, "factory"));
111
+ if (lint.stdout.includes("error") && !lint.stdout.includes("0 errors"))
112
+ errors.push("Lint errors found.");
113
+ }
114
+ return errors.length === 0 ? { ok: true } : { ok: false, errors };
53
115
  }