Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ AI coding agents produce edits with limited context knowledge (myopia) — their
- `readonly` — files and directories the agent must not touch
- `sealed` — files where no new exports are allowed (body still editable)
- `visible` — the set of exports allowed to be added or modified in that module
- `signature-lock` — names whose captured signature must remain unchanged (body still editable)

The extension intercepts agent `write`/`edit` operations and enforces these contracts. Violations are blocked with a clear reason.

Expand All @@ -28,12 +29,14 @@ The attempt to add 2 public helper functions is blocked, forcing the agent to re
3. **Gating** — On every write/edit, checks:
- **Readonly gate** — is the target file locked?
**Sealed gate** — would the change add new exports to a file in the `sealed` list?
- **Signature gate** — would the change alter a locked signature declared in the module's `signature-lock` list?
- **Export gate** — would the change introduce an export not in the `visible` list?
- **Module interface import gate** — external files can only import from the module not internal files, i.e. re-exports from `index.ts` or `mod.rs`. A child module may import from a parent module's internal files (not recommended but allowed). (Only Typescript/JavaScript and Rust are supported)
- **Import gate** (not implemented yet) — would the change introduce an import violating visibility scope?

- System prompt: [system-prompt.md](src/context/system-prompt.template.md)
- Currently [supported languages](src/gates/checkers/index.ts): **TypeScript/JavaScript**, **Rust**, **Java**, **Go**, **Kotlin**, **Scala**
- Currently [supported languages](src/gates/export-checkers/index.ts): **TypeScript/JavaScript**, **Rust**, **Java**, **Go**, **Kotlin**, **Scala**
- Signature checkers ([src/gates/signature-checkers/index.ts](src/gates/signature-checkers/index.ts)) ship a TypeScript/JavaScript checker; other languages are stubs in v1.

## Installation
```bash
Expand Down Expand Up @@ -67,6 +70,27 @@ Sealed files cannot change their surface size: no new exports or public entries

A skill [module-seal-all](skills/module-seal-all) has been included to auto-seal modules.

### Type Signature Lock (signature-gate)

```yaml
signature-lock:
- my-type.ts$MyFunction
- sub/foo.ts$Bar
```

Lock the captured signature of named types in a target file. The `$` separates the file path (resolved relative to the module directory) from the symbol name. The body of the declaration remains editable; only the captured head (parameter list, return type, generics, class `extends`/`implements`, interface body, or type alias RHS) must remain unchanged.

For a file `src/my-type.ts` containing `export function MyFunction(a: number): boolean { ... }`:
- Adding a parameter breaks the lock.
- Changing the return type breaks the lock.
- Changing only the body (`return true;` → `return false;`) is allowed.

Limitations (v1):
- Signatures are extracted via regex (not AST); multi-line parameter lists with comments inside parens may produce false positives.
- Interfaces are locked in their entirety (the body is part of the signature).
- Class inner methods are not individually lockable; only the class header is captured.
- Per-method locking inside classes/interfaces requires an AST-based checker (out of v1 scope).

### Visibility whitelist (under redesign)

```yaml
Expand Down
44 changes: 35 additions & 9 deletions doc/Development.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,26 @@ src/
gates/
readonly-gate.ts — Blocks writes to readonly files in a module
sealed-gate.ts — Blocks new exports on sealed files in a module
signature-gate.ts — Blocks changes to locked signatures
export-gate.ts — Blocks changes that break a module's public API
checkers/
index.ts — Auto-registers all checkers
registry.ts — Checker registry: dispatch by language
typescript.ts — TS-specific public-API checker
export-checkers/
index.ts — Auto-registers all export checkers
registry.ts — ExportChecker registry: dispatch by language
typescript.ts — TS/JS-specific public-API checker
rust.ts — Rust-specific public-API checker
java.ts — Java-specific public-API checker
go.ts — Go-specific public-API checker
kotlin.ts — Kotlin-specific public-API checker
scala.ts — Scala-specific public-API checker
signature-checkers/
index.ts — Auto-registers all signature checkers
registry.ts — SignatureChecker registry: dispatch by language
typescript.ts — TS/JS-specific signature extractor
rust.ts — Rust-specific signature stub
java.ts — Java-specific signature stub
go.ts — Go-specific signature stub
kotlin.ts — Kotlin-specific signature stub
scala.ts — Scala-specific signature stub
graph/
module-index-builder.ts — Scans for module.md files, builds the index
validation.ts — Validates that visible entries actually exist
Expand All @@ -42,8 +56,10 @@ test/
context/system-prompt.test.ts
gates/export-gate.test.ts
gates/readonly-gate.test.ts
gates/checkers/typescript.test.ts
gates/checkers/rust.test.ts
gates/export-checkers/typescript.test.ts
gates/export-checkers/rust.test.ts
gates/signature-gate.test.ts
gates/signature-checkers/typescript.test.ts
graph/module-index-builder.test.ts
graph/validation.test.ts
fixture/
Expand All @@ -56,6 +72,16 @@ doc/

## Adding a checker (new language)

1. Create `src/gates/checkers/<language>.ts` exporting a function matching the `Checker` signature
2. Add it to `src/gates/checkers/index.ts` with an import + auto-register call
3. Create `test/gates/checkers/<language>.test.ts` with unit tests
### Export checker

1. Create `src/gates/export-checkers/<language>.ts` exporting an `ExportChecker` with `extensions` and `getNewExports`.
2. Add it to `src/gates/export-checkers/index.ts` with an import for its side-effect (auto-registers).
3. Create `src/gates/export-checkers/<language>.test.ts` with unit tests.
4. Add the file to the `sealed` list in `src/gates/export-checkers/MODULE.md`.

### Signature checker

1. Create `src/gates/signature-checkers/<language>.ts` exporting a `SignatureChecker` with `extensions` and `getSignatures(src)` returning a `Map<name, signatureText>`.
2. Add it to `src/gates/signature-checkers/index.ts` with an import for its side-effect.
3. Create `src/gates/signature-checkers/<language>.test.ts` with unit tests.
4. Add the file to the `sealed` list in `src/gates/signature-checkers/MODULE.md`.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cuzfrog/pi-module-gates",
"version": "0.19.0",
"version": "0.20.0",
"description": "pi extension that controls the entropy of the codebase by enforcing code module boundaries.",
"keywords": [
"pi-package"
Expand Down
2 changes: 1 addition & 1 deletion src/claude/pre-tool-use.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as path from "node:path";
import { applyEdits, readFileSafe } from "../utils.ts";
import { runGates, type GateEdit } from "../gates/run-gates.ts";
import { loadIndexForHook, notifyNoContracts } from "./index-loader.ts";
import "../gates/checkers/index.ts";
import "../gates/export-checkers/index.ts";

type HookEvent = {
hook_event_name?: string;
Expand Down
1 change: 1 addition & 0 deletions src/context/system-prompt.template.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ A `{{descriptorFileName}}` with a `visible` list means only entries in the list
- `module interface`: the file representing the module surface, e.g. `index.ts` in Typescript, `mod.rs` in Rust;
- `readonly`: files are readonly;
- `sealed`: files cannot add new exports, but the body is still editable; the export surface is sealed;
- `signature-lock`: list of `file$Name` entries locking the captured signature of named types in the target file; bodies remain editable;
- `visible`: visible from outside the module; files not in the module directory are outside the module;
7 changes: 7 additions & 0 deletions src/context/system-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe("buildSystemPromptHint", () => {
readonly: ["secret.ts", "module.md"],
sealed: [],
prose: "Greeting module.",
signatureLock: [],
},
],
dirToModule: new Map(),
Expand Down Expand Up @@ -54,6 +55,7 @@ describe("buildSystemPromptHint", () => {
readonly: ["module.md"],
sealed: [],
prose: "Module A.",
signatureLock: [],
},
],
dirToModule: new Map(),
Expand All @@ -75,6 +77,7 @@ describe("buildSystemPromptHint", () => {
readonly: ["module.md"],
sealed: [],
prose: "Module A.",
signatureLock: [],
},
],
dirToModule: new Map(),
Expand All @@ -100,6 +103,7 @@ describe("buildSystemPromptHint", () => {
readonly: ["config.ts"],
sealed: [],
prose: "Module A.",
signatureLock: [],
},
],
dirToModule: new Map(),
Expand All @@ -125,6 +129,7 @@ describe("buildSystemPromptHint", () => {
readonly: ["module.md"],
sealed: [],
prose: "Module A.",
signatureLock: [],
},
],
dirToModule: new Map(),
Expand All @@ -144,6 +149,7 @@ describe("buildSystemPromptHint", () => {
readonly: ["module.md"],
sealed: [],
prose: "Module A.",
signatureLock: [],
},
],
dirToModule: new Map(),
Expand All @@ -168,6 +174,7 @@ describe("buildSystemPromptHint", () => {
readonly: ["module.md"],
sealed: [],
prose: "Module A.",
signatureLock: [],
},
],
dirToModule: new Map(),
Expand Down
2 changes: 2 additions & 0 deletions src/gates/MODULE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ sealed:
- run-gates.test.ts
- sealed-gate.ts
- sealed-gate.test.ts
- signature-gate.ts
- signature-gate.test.ts
---
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
20 changes: 19 additions & 1 deletion src/gates/export-gate.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect } from "vitest";
import { checkExports } from "./export-gate.ts";
import type { ModuleIndex, ModuleContract } from "../types.ts";
import "./checkers/typescript.ts";
import "./export-checkers/typescript.ts";

function makeIndex(contracts: ModuleContract[]): ModuleIndex {
return { contracts, dirToModule: new Map() };
Expand All @@ -18,6 +18,7 @@ describe("checkExports", () => {
readonly: ["module.md"],
sealed: [],
prose: "",
signatureLock: [],
},
]);

Expand All @@ -43,6 +44,7 @@ describe("checkExports", () => {
readonly: ["module.md"],
sealed: [],
prose: "",
signatureLock: [],
},
]);

Expand All @@ -61,6 +63,7 @@ describe("checkExports", () => {
readonly: ["module.md"],
sealed: [],
prose: "",
signatureLock: [],
},
]);

Expand All @@ -84,13 +87,15 @@ describe("checkExports", () => {
readonly: ["module.md"],
sealed: [],
prose: "",
signatureLock: [],
},
{
modulePath: "/project/src",
visible: [{ name: "sharedFn" }, { name: "srcOnly" }],
readonly: ["module.md"],
sealed: [],
prose: "",
signatureLock: [],
},
]);

Expand All @@ -112,6 +117,7 @@ describe("checkExports", () => {
readonly: ["module.md"],
sealed: [],
prose: "",
signatureLock: [],
},
]);

Expand All @@ -130,6 +136,7 @@ describe("checkExports", () => {
readonly: ["module.md"],
sealed: [],
prose: "",
signatureLock: [],
},
]);

Expand All @@ -151,6 +158,7 @@ describe("checkExports", () => {
readonly: ["module.md"],
sealed: [],
prose: "",
signatureLock: [],
},
]);

Expand All @@ -168,6 +176,7 @@ describe("checkExports", () => {
readonly: ["module.md"],
sealed: [],
prose: "",
signatureLock: [],
},
]);

Expand Down Expand Up @@ -203,13 +212,15 @@ describe("checkExports", () => {
readonly: ["module.md"],
sealed: [],
prose: "",
signatureLock: [],
},
{
modulePath: "/project/src",
visible: [{ name: "sharedFn" }, { name: "srcOnly" }],
readonly: ["module.md"],
sealed: [],
prose: "",
signatureLock: [],
},
]);

Expand All @@ -228,13 +239,15 @@ describe("checkExports", () => {
readonly: ["module.md"],
sealed: [],
prose: "",
signatureLock: [],
},
{
modulePath: "/project/src",
visible: [{ name: "childFn" }],
readonly: ["module.md"],
sealed: [],
prose: "",
signatureLock: [],
},
]);

Expand All @@ -253,20 +266,23 @@ describe("checkExports", () => {
readonly: ["module.md"],
sealed: [],
prose: "",
signatureLock: [],
},
{
modulePath: "/project/src",
visible: [{ name: "sharedFn" }],
readonly: ["module.md"],
sealed: [],
prose: "",
signatureLock: [],
},
{
modulePath: "/project/src/payments",
visible: [{ name: "leafOnly" }],
readonly: ["module.md"],
sealed: [],
prose: "",
signatureLock: [],
},
]);

Expand All @@ -292,13 +308,15 @@ describe("checkExports", () => {
readonly: ["module.md"],
sealed: [],
prose: "",
signatureLock: [],
},
{
modulePath: "/project/src",
visible: [{ name: "sharedFn" }, { name: "rootOnly" }],
readonly: ["module.md"],
sealed: [],
prose: "",
signatureLock: [],
},
]);

Expand Down
2 changes: 1 addition & 1 deletion src/gates/export-gate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as path from "node:path";
import { getChecker } from "./checkers/registry.ts";
import { getChecker } from "./export-checkers/registry.ts";
import type { ModuleIndex, Signature } from "../types.ts";

export type ExportViolation = { name: string; imposedBy: string };
Expand Down
2 changes: 1 addition & 1 deletion src/gates/module-interface-import-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
import { join } from "node:path";
import { checkModuleInterfaceImports } from "./module-interface-import-gate.ts";
import type { ModuleIndex } from "../types.ts";
import "./checkers/index.ts";
import "./export-checkers/index.ts";

function makeIndex(dirToModule: Map<string, string>): ModuleIndex {
return { contracts: [], dirToModule };
Expand Down
Loading
Loading