@cuzfrog/pi-module-gates 0.17.1 → 0.18.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 +6 -6
- package/package.json +2 -2
- package/skills/{module-freeze-all → module-seal-all}/SKILL.md +7 -7
- package/skills/module-seal-all/scripts/seal-all.d.mts +1 -0
- package/skills/{module-freeze-all/scripts/freeze-all.mjs → module-seal-all/scripts/seal-all.mjs} +14 -14
- package/src/MODULE.md +2 -3
- package/src/context/MODULE.md +2 -3
- package/src/context/system-prompt.template.md +2 -2
- package/src/gates/MODULE.md +3 -4
- package/src/gates/checkers/MODULE.md +2 -3
- package/src/gates/checkers/go.ts +29 -3
- package/src/gates/checkers/java.ts +2 -2
- package/src/gates/checkers/kotlin.ts +19 -7
- package/src/gates/checkers/rust.ts +72 -3
- package/src/gates/checkers/scala.ts +19 -7
- package/src/gates/checkers/typescript.ts +32 -16
- package/src/gates/index.ts +2 -2
- package/src/gates/run-gates.ts +4 -4
- package/src/gates/{frozen-gate.ts → sealed-gate.ts} +6 -6
- package/src/graph/MODULE.md +2 -4
- package/src/graph/frontmatter-parser.ts +1 -1
- package/src/graph/module-index-builder.ts +1 -1
- package/src/types.ts +1 -1
- package/src/utils.ts +2 -2
- package/skills/module-freeze-all/scripts/freeze-all.d.mts +0 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ AI coding agents produce edits with limited context knowledge (myopia) — their
|
|
|
12
12
|
**Module contracts as guardrails.** Each directory can contain a descriptor file that declares:
|
|
13
13
|
|
|
14
14
|
- `readonly` — files and directories the agent must not touch
|
|
15
|
-
- `
|
|
15
|
+
- `sealed` — files where no new exports are allowed (body still editable)
|
|
16
16
|
- `visible` — the set of exports allowed to be added or modified in that module
|
|
17
17
|
|
|
18
18
|
The extension intercepts agent `write`/`edit` operations and enforces these contracts. Violations are blocked with a clear reason.
|
|
@@ -26,7 +26,7 @@ The attempt to add 2 public helper functions is blocked, forcing the agent to re
|
|
|
26
26
|
2. **System prompt** — Injects a hint so the agent knows to respect descriptor file conventions.
|
|
27
27
|
3. **Gating** — On every write/edit, checks:
|
|
28
28
|
- **Readonly gate** — is the target file locked?
|
|
29
|
-
**
|
|
29
|
+
**Sealed gate** — would the change add new exports to a file in the `sealed` list?
|
|
30
30
|
- **Export gate** — would the change introduce an export not in the `visible` list?
|
|
31
31
|
- **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)
|
|
32
32
|
- **Import gate** (not implemented yet) — would the change introduce an import violating visibility scope?
|
|
@@ -57,14 +57,14 @@ readonly: [mod.rs]
|
|
|
57
57
|
Any prose for the agent to better understand the module.
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
###
|
|
60
|
+
### Sealed constraints
|
|
61
61
|
|
|
62
62
|
```yaml
|
|
63
|
-
|
|
63
|
+
sealed: [mod.rs]
|
|
64
64
|
```
|
|
65
|
-
|
|
65
|
+
Sealed files cannot change their surface size: no new exports or public entries are allowed. The file body is still editable.
|
|
66
66
|
|
|
67
|
-
A skill [module-
|
|
67
|
+
A skill [module-seal-all](skills/module-seal-all) has been included to auto-seal modules.
|
|
68
68
|
|
|
69
69
|
### Visibility whitelist (under redesign)
|
|
70
70
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cuzfrog/pi-module-gates",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "pi extension that controls the entropy of the codebase by enforcing code module boundaries.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
]
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"yaml": "2.
|
|
25
|
+
"yaml": "2.9.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"@earendil-works/pi-coding-agent": "*"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: module-
|
|
3
|
-
description: Auto-
|
|
2
|
+
name: module-seal-all
|
|
3
|
+
description: Auto-seal all files in module descriptors so their module surface area cannot be increased.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
argument-hint: <user-instructions>
|
|
6
6
|
---
|
|
@@ -9,26 +9,26 @@ argument-hint: <user-instructions>
|
|
|
9
9
|
- Find out module descriptor filename in the context.
|
|
10
10
|
- Find out source root in the context or from configurations.
|
|
11
11
|
- Derive scripts args from user instructions.
|
|
12
|
-
- Call the script to scans the source tree for module descriptors and auto-populates their `
|
|
12
|
+
- Call the script to scans the source tree for module descriptors and auto-populates their `sealed` entries with code files in each module directory.
|
|
13
13
|
|
|
14
14
|
## Script Usage
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
node skills/module-
|
|
17
|
+
node skills/module-seal-all/scripts/seal-all.mjs [options]
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Options:
|
|
21
21
|
- `--root <dir>` - Source root directory (default: `src`)
|
|
22
22
|
- `--dry-run` - Show what would change without writing files
|
|
23
|
-
- `--create` - Create module descriptor files for directories without one (adds `
|
|
23
|
+
- `--create` - Create module descriptor files for directories without one (adds `sealed` only)
|
|
24
24
|
- `--descriptor-name <name>` - Module descriptor filename (default: `module.md`)
|
|
25
25
|
|
|
26
26
|
### Behavior
|
|
27
27
|
|
|
28
28
|
1. Finds all module descriptor files under the source root.
|
|
29
29
|
2. For each module directory, lists all direct files (not subdirectories, not module descriptor file itself)
|
|
30
|
-
3. Adds those files to the `
|
|
31
|
-
4. Preserves existing `
|
|
30
|
+
3. Adds those files to the `sealed` frontmatter field
|
|
31
|
+
4. Preserves existing `sealed` entries, other fields in the frontmatter, and body prose
|
|
32
32
|
|
|
33
33
|
## Extra user instructions:
|
|
34
34
|
"$ARGUMENTS"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SUPPORTED_EXTENSIONS: Set<string>;
|
package/skills/{module-freeze-all/scripts/freeze-all.mjs → module-seal-all/scripts/seal-all.mjs}
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Auto-
|
|
3
|
+
* Auto-seal all files in module descriptors.
|
|
4
4
|
*
|
|
5
5
|
* Scans module descriptor files under a source root and populates each
|
|
6
|
-
* descriptor's `
|
|
6
|
+
* descriptor's `sealed` field with every direct code file in the module
|
|
7
7
|
* directory. Files in nested sub-modules (directories with their own
|
|
8
8
|
* descriptor) are excluded from the parent.
|
|
9
9
|
*/
|
|
@@ -47,7 +47,7 @@ function main() {
|
|
|
47
47
|
const results = [];
|
|
48
48
|
|
|
49
49
|
for (const [modDir, modFile] of allModuleDirs) {
|
|
50
|
-
const result =
|
|
50
|
+
const result = sealModule(modDir, modFile, descriptorName, allModuleDirs);
|
|
51
51
|
if (result) results.push(result);
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -69,7 +69,7 @@ function main() {
|
|
|
69
69
|
console.log("Dry run — would modify:");
|
|
70
70
|
for (const r of results) {
|
|
71
71
|
const rel = relative(cwd, r.path);
|
|
72
|
-
console.log(` ${rel}:
|
|
72
|
+
console.log(` ${rel}: seal [${r.files.join(", ") || "(none)"}]`);
|
|
73
73
|
}
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
@@ -169,7 +169,7 @@ function walkDir(dir, visitor) {
|
|
|
169
169
|
// Module processing
|
|
170
170
|
// ---------------------------------------------------------------------------
|
|
171
171
|
|
|
172
|
-
function
|
|
172
|
+
function sealModule(modDir, descriptorFileName, descriptorName, allModuleDirs) {
|
|
173
173
|
const modPath = join(modDir, descriptorFileName);
|
|
174
174
|
let raw;
|
|
175
175
|
try {
|
|
@@ -179,15 +179,15 @@ function freezeModule(modDir, descriptorFileName, descriptorName, allModuleDirs)
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
const parsed = parseFrontmatter(raw);
|
|
182
|
-
const
|
|
182
|
+
const existingSealed = normalizeSealed(parsed.frontmatter.sealed);
|
|
183
183
|
const directFiles = listDirectFiles(modDir, descriptorName, allModuleDirs);
|
|
184
|
-
const
|
|
184
|
+
const mergedSealed = mergePreservingOrder(existingSealed, directFiles);
|
|
185
185
|
|
|
186
|
-
if (arraysEqual(
|
|
186
|
+
if (arraysEqual(existingSealed, mergedSealed)) return undefined;
|
|
187
187
|
|
|
188
|
-
const newContent = serializeModule(parsed,
|
|
188
|
+
const newContent = serializeModule(parsed, mergedSealed);
|
|
189
189
|
|
|
190
|
-
return { path: modPath, content: newContent, files:
|
|
190
|
+
return { path: modPath, content: newContent, files: mergedSealed };
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
function createModuleDescriptor(dir, descriptorName) {
|
|
@@ -283,8 +283,8 @@ function parseInlineList(raw) {
|
|
|
283
283
|
return inner.split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, ""));
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
-
function serializeModule(parsed,
|
|
287
|
-
const fm = { ...parsed.frontmatter,
|
|
286
|
+
function serializeModule(parsed, sealed) {
|
|
287
|
+
const fm = { ...parsed.frontmatter, sealed };
|
|
288
288
|
|
|
289
289
|
let yaml = parsed.prelude;
|
|
290
290
|
for (const [key, value] of Object.entries(fm)) {
|
|
@@ -344,7 +344,7 @@ function listDirectFiles(modDir, descriptorName, allModuleDirs) {
|
|
|
344
344
|
// Helpers
|
|
345
345
|
// ---------------------------------------------------------------------------
|
|
346
346
|
|
|
347
|
-
function
|
|
347
|
+
function normalizeSealed(value) {
|
|
348
348
|
if (Array.isArray(value)) return value.map(String);
|
|
349
349
|
return [];
|
|
350
350
|
}
|
|
@@ -366,4 +366,4 @@ function arraysEqual(a, b) {
|
|
|
366
366
|
|
|
367
367
|
if (import.meta.url === pathToFileURL(resolve(process.argv[1] ?? "")).href) {
|
|
368
368
|
main();
|
|
369
|
-
}
|
|
369
|
+
}
|
package/src/MODULE.md
CHANGED
package/src/context/MODULE.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
## Module gates (boundary enforcement)
|
|
2
|
-
This project uses `{{descriptorFileName}}`(case-insensitive) files to declare visibility, readonly and
|
|
2
|
+
This project uses `{{descriptorFileName}}`(case-insensitive) files to declare visibility, readonly and sealed rules that you should follow.
|
|
3
3
|
If you cannot comply, reconsider your design or raise to the user with tradeoffs if necessary.
|
|
4
4
|
Each `{{descriptorFileName}}` gates its branching point in the tree.
|
|
5
5
|
A `{{descriptorFileName}}` with a `visible` list means only entries in the list are allowed to be visible outside the module.
|
|
@@ -13,5 +13,5 @@ A `{{descriptorFileName}}` with a `visible` list means only entries in the list
|
|
|
13
13
|
- `external files`: files not in the module directory and subdirectories;
|
|
14
14
|
- `module interface`: the file representing the module surface, e.g. `index.ts` in Typescript, `mod.rs` in Rust;
|
|
15
15
|
- `readonly`: files are readonly;
|
|
16
|
-
- `
|
|
16
|
+
- `sealed`: files cannot add new exports, but the body is still editable; the export surface is sealed;
|
|
17
17
|
- `visible`: visible from outside the module; files not in the module directory are outside the module;
|
package/src/gates/MODULE.md
CHANGED
package/src/gates/checkers/go.ts
CHANGED
|
@@ -13,7 +13,33 @@ const goChecker: ExportChecker = {
|
|
|
13
13
|
registerChecker(goChecker);
|
|
14
14
|
|
|
15
15
|
function extractExports(src: string): Signature[] {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const out: Signature[] = [];
|
|
17
|
+
|
|
18
|
+
for (const m of src.matchAll(
|
|
19
|
+
/^(?:func\s+(?:\([^)]*\)\s+)?|type\s+|var\s+|const\s+)([\p{Lu}]\w*)/gmu,
|
|
20
|
+
)) {
|
|
21
|
+
out.push({ name: m[1] });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const block of src.matchAll(/(?:^|\n)(?:var|const)\s*\(([\s\S]*?)\)/g)) {
|
|
25
|
+
const inner = block[1];
|
|
26
|
+
for (const line of inner.split(/\n/)) {
|
|
27
|
+
const trimmed = line.trim();
|
|
28
|
+
if (!trimmed) continue;
|
|
29
|
+
const m = /^([\p{Lu}]\w*)(?:\s*(?::\s*[\w.\[\]any, |]+)?=\s*[\s\S]*|\s*$)/u.exec(trimmed);
|
|
30
|
+
if (m) out.push({ name: m[1] });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const iface of src.matchAll(/(?:^|\n)type\s+[\p{Lu}]\w*\s+interface\s*\{([\s\S]*?)\}/gu)) {
|
|
35
|
+
const inner = iface[1];
|
|
36
|
+
for (const line of inner.split(/\n/)) {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
if (!trimmed) continue;
|
|
39
|
+
const m = /^([\p{Lu}]\w*)\s*\(/u.exec(trimmed);
|
|
40
|
+
if (m) out.push({ name: m[1] });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return out;
|
|
19
45
|
}
|
|
@@ -14,6 +14,6 @@ registerChecker(javaChecker);
|
|
|
14
14
|
|
|
15
15
|
function extractExports(src: string): Signature[] {
|
|
16
16
|
return [...src.matchAll(
|
|
17
|
-
/^public\s+(?:class|interface|enum|@interface|record)\s+(\w+)/gm,
|
|
18
|
-
)].map((m) => ({ modifier: "public", name: m[
|
|
17
|
+
/^(?:@\w+(?:\([^)]*\))?\s+)*public\s+(?:(?:abstract|final|sealed|non-sealed|static)\s+)*(class|interface|enum|@interface|record)\s+(\w+)/gm,
|
|
18
|
+
)].map((m) => ({ modifier: "public", name: m[2] }));
|
|
19
19
|
}
|
|
@@ -13,14 +13,26 @@ const kotlinChecker: ExportChecker = {
|
|
|
13
13
|
registerChecker(kotlinChecker);
|
|
14
14
|
|
|
15
15
|
function extractExports(src: string): Signature[] {
|
|
16
|
-
const
|
|
16
|
+
const declRe = /^(?:@\w+(?:\([^)]*\))?\s+)*(?:(public|internal|private)\s+)?(?:(?:data|sealed|enum|abstract|open|final|inline|value|annotation|expect|actual|external)\s+)*(?:companion\s+)?(?:class|interface|object|fun|val|var|typealias)\s+(\w+)/m;
|
|
17
17
|
const results: Signature[] = [];
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
let depth = 0;
|
|
19
|
+
for (const rawLine of src.split("\n")) {
|
|
20
|
+
const line = rawLine.replace(/\/\/.*$/, "").replace(/\/\*.*?\*\//g, "");
|
|
21
|
+
if (depth === 0) {
|
|
22
|
+
const m = declRe.exec(line);
|
|
23
|
+
if (m) {
|
|
24
|
+
const visibility = m[1];
|
|
25
|
+
const name = m[2];
|
|
26
|
+
if (visibility === "private") {
|
|
27
|
+
// skip
|
|
28
|
+
} else {
|
|
29
|
+
results.push({ modifier: visibility, name });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
depth += (line.match(/\{/g) || []).length;
|
|
34
|
+
depth -= (line.match(/\}/g) || []).length;
|
|
35
|
+
if (depth < 0) depth = 0;
|
|
24
36
|
}
|
|
25
37
|
return results;
|
|
26
38
|
}
|
|
@@ -5,8 +5,12 @@ import type { Signature } from "../../types.ts";
|
|
|
5
5
|
const rustChecker: ExportChecker = {
|
|
6
6
|
extensions: [".rs"],
|
|
7
7
|
getNewExports(before: string, after: string): Signature[] {
|
|
8
|
-
const beforeNames = new Set(
|
|
9
|
-
|
|
8
|
+
const beforeNames = new Set(
|
|
9
|
+
[...extractPubItems(before), ...extractPubUses(before)].map((s) => s.name),
|
|
10
|
+
);
|
|
11
|
+
return [...extractPubItems(after), ...extractPubUses(after)].filter(
|
|
12
|
+
(sig) => !beforeNames.has(sig.name),
|
|
13
|
+
);
|
|
10
14
|
},
|
|
11
15
|
};
|
|
12
16
|
|
|
@@ -14,6 +18,71 @@ registerChecker(rustChecker);
|
|
|
14
18
|
|
|
15
19
|
function extractPubItems(src: string): Signature[] {
|
|
16
20
|
return [...src.matchAll(
|
|
17
|
-
/^(pub(?:\([^)]*\))?)
|
|
21
|
+
/^(pub(?:\([^)]*\))?)(?:\s+(?:unsafe|async|const|extern(?:\s+"[^"]+")?))*\s+(?:fn|struct|enum|trait|type|const|mod|static)\s+(\w+)/gm,
|
|
18
22
|
)].map((m) => ({ modifier: m[1], name: m[2] }));
|
|
19
23
|
}
|
|
24
|
+
|
|
25
|
+
function extractPubUses(src: string): Signature[] {
|
|
26
|
+
const out: Signature[] = [];
|
|
27
|
+
const re = /^(pub(?:\([^)]*\))?)\s+use\s+([\s\S]*?);/gm;
|
|
28
|
+
for (const m of src.matchAll(re)) {
|
|
29
|
+
const modifier = m[1];
|
|
30
|
+
const body = m[2];
|
|
31
|
+
for (const name of extractUseNames(body)) {
|
|
32
|
+
out.push({ modifier, name });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function extractUseNames(body: string): string[] {
|
|
39
|
+
const trimmed = body.trim();
|
|
40
|
+
const groupMatch = /^(.+?)::\s*\{([\s\S]*)\}\s*$/.exec(trimmed);
|
|
41
|
+
if (groupMatch) {
|
|
42
|
+
const prefix = groupMatch[1].split("::").pop() ?? "";
|
|
43
|
+
return extractGroupItems(groupMatch[2], prefix);
|
|
44
|
+
}
|
|
45
|
+
const last = trimmed.split("::").pop() ?? "";
|
|
46
|
+
if (last === "self") {
|
|
47
|
+
const segments = trimmed.split("::");
|
|
48
|
+
segments.pop();
|
|
49
|
+
const fallback = segments.pop() ?? "";
|
|
50
|
+
return fallback ? [fallback] : [];
|
|
51
|
+
}
|
|
52
|
+
const parsed = parseRenamed(last);
|
|
53
|
+
return parsed && parsed !== "*" ? [parsed] : [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function extractGroupItems(inner: string, prefix: string): string[] {
|
|
57
|
+
const out: string[] = [];
|
|
58
|
+
let depth = 0;
|
|
59
|
+
let buf = "";
|
|
60
|
+
for (const ch of inner) {
|
|
61
|
+
if (ch === "{") depth++;
|
|
62
|
+
else if (ch === "}") depth--;
|
|
63
|
+
if (ch === "," && depth === 0) {
|
|
64
|
+
pushItem(buf, out, prefix);
|
|
65
|
+
buf = "";
|
|
66
|
+
} else {
|
|
67
|
+
buf += ch;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
pushItem(buf, out, prefix);
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function pushItem(raw: string, out: string[], prefix: string): void {
|
|
75
|
+
const item = raw.trim();
|
|
76
|
+
if (!item || item === "*") return;
|
|
77
|
+
if (item === "self") {
|
|
78
|
+
if (prefix) out.push(prefix);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
out.push(parseRenamed(item));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseRenamed(item: string): string {
|
|
85
|
+
const asIdx = item.lastIndexOf(" as ");
|
|
86
|
+
const tail = asIdx >= 0 ? item.slice(asIdx + 4) : item;
|
|
87
|
+
return tail.split("::").pop() ?? "";
|
|
88
|
+
}
|
|
@@ -13,14 +13,26 @@ const scalaChecker: ExportChecker = {
|
|
|
13
13
|
registerChecker(scalaChecker);
|
|
14
14
|
|
|
15
15
|
function extractExports(src: string): Signature[] {
|
|
16
|
-
const
|
|
16
|
+
const declRe = /^(?:@\w+(?:\([^)]*\))?\s+)*(?:(private(?:\[[^\]]*\])?|protected(?:\[[^\]]*\])?|public)\s+)?(?:(?:sealed|final|abstract|lazy|opaque|implicit)\s+)*(?:case\s+)?(?:class|object|trait|def|val|var|type|enum|given|extension)\s+(\w+)/m;
|
|
17
17
|
const results: Signature[] = [];
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
let depth = 0;
|
|
19
|
+
for (const rawLine of src.split("\n")) {
|
|
20
|
+
const line = rawLine.replace(/\/\/.*$/, "");
|
|
21
|
+
if (depth === 0) {
|
|
22
|
+
const m = declRe.exec(line);
|
|
23
|
+
if (m) {
|
|
24
|
+
const visibility = m[1];
|
|
25
|
+
const name = m[2];
|
|
26
|
+
if (visibility === "private" || visibility === "protected") {
|
|
27
|
+
// skip bare private/protected
|
|
28
|
+
} else {
|
|
29
|
+
results.push({ modifier: visibility, name });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
depth += (line.match(/\{/g) || []).length;
|
|
34
|
+
depth -= (line.match(/\}/g) || []).length;
|
|
35
|
+
if (depth < 0) depth = 0;
|
|
24
36
|
}
|
|
25
37
|
return results;
|
|
26
38
|
}
|
|
@@ -12,39 +12,55 @@ const tsChecker: ExportChecker = {
|
|
|
12
12
|
|
|
13
13
|
registerChecker(tsChecker);
|
|
14
14
|
|
|
15
|
+
const ANNOTATION_PREFIX = String.raw`(?:@[^\n]*\n)*[ \t]*`;
|
|
16
|
+
const DECL_KEYWORD = String.raw`(?:function(?:\s*\*)?|class|const|let|var|type|interface|enum)`;
|
|
17
|
+
const DECL_KEYWORD_NEG = String.raw`(?:function\s*\*?|class|const|let|var|type|interface|enum|abstract|async|declare)`;
|
|
18
|
+
|
|
15
19
|
function extractExports(src: string): Signature[] {
|
|
16
|
-
const results: Signature[] = [
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
),
|
|
20
|
-
|
|
20
|
+
const results: Signature[] = [];
|
|
21
|
+
|
|
22
|
+
for (const m of src.matchAll(
|
|
23
|
+
new RegExp(`^${ANNOTATION_PREFIX}export\\s+(?:default\\s+)?(?:\\w+\\s+)*${DECL_KEYWORD}\\s+(\\w+)`, "gm"),
|
|
24
|
+
)) {
|
|
25
|
+
results.push({ name: m[1] });
|
|
26
|
+
}
|
|
21
27
|
|
|
22
28
|
for (const m of src.matchAll(
|
|
23
|
-
|
|
29
|
+
new RegExp(`^${ANNOTATION_PREFIX}export\\s*(?:type\\s+)?\\{\\s*([^}]+?)\\s*\\}(?:\\s+from\\b)?`, "gm"),
|
|
24
30
|
)) {
|
|
25
31
|
const inner = m[1];
|
|
26
32
|
for (const entry of inner.split(",")) {
|
|
27
|
-
|
|
33
|
+
let trimmed = entry.trim();
|
|
28
34
|
if (!trimmed) continue;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} else {
|
|
33
|
-
results.push({ name: trimmed });
|
|
35
|
+
if (trimmed.startsWith("type ")) {
|
|
36
|
+
trimmed = trimmed.slice(5).trim();
|
|
37
|
+
if (!trimmed) continue;
|
|
34
38
|
}
|
|
39
|
+
const asMatch = trimmed.match(/^(\w+)\s+as\s+(\w+)$/);
|
|
40
|
+
results.push({ name: asMatch ? asMatch[2] : trimmed });
|
|
35
41
|
}
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
for (const m of src.matchAll(
|
|
44
|
+
for (const m of src.matchAll(
|
|
45
|
+
new RegExp(`^${ANNOTATION_PREFIX}export\\s+(?:type\\s+)?\\*\\s+from`, "gm"),
|
|
46
|
+
)) {
|
|
47
|
+
results.push({ name: "*" });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const m of src.matchAll(
|
|
51
|
+
new RegExp(`^${ANNOTATION_PREFIX}export\\s*\\*\\s+as\\s+(\\w+)\\s+from`, "gm"),
|
|
52
|
+
)) {
|
|
39
53
|
results.push({ name: m[1] });
|
|
40
54
|
}
|
|
41
55
|
|
|
42
|
-
for (const m of src.matchAll(
|
|
43
|
-
|
|
56
|
+
for (const m of src.matchAll(
|
|
57
|
+
new RegExp(`^${ANNOTATION_PREFIX}export\\s*=\\s*(\\w+)`, "gm"),
|
|
58
|
+
)) {
|
|
59
|
+
results.push({ name: m[1] });
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
for (const m of src.matchAll(
|
|
47
|
-
|
|
63
|
+
new RegExp(`^${ANNOTATION_PREFIX}export\\s+default\\s+(?!${DECL_KEYWORD_NEG})([a-zA-Z_]\\w*)`, "gm"),
|
|
48
64
|
)) {
|
|
49
65
|
results.push({ name: m[1] });
|
|
50
66
|
}
|
package/src/gates/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { checkReadonly } from "./readonly-gate.ts";
|
|
2
|
-
export {
|
|
2
|
+
export { checkSealed } from "./sealed-gate.ts";
|
|
3
3
|
export { checkExports } from "./export-gate.ts";
|
|
4
|
-
export { checkModuleInterfaceImports } from "./module-interface-import-gate.ts";
|
|
4
|
+
export { checkModuleInterfaceImports } from "./module-interface-import-gate.ts";
|
package/src/gates/run-gates.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { ModuleGateConfig } from "../config.ts";
|
|
|
5
5
|
import { readFileSafe, applyEdits, isWithinSourceRoot, findOwningModule } from "../utils.ts";
|
|
6
6
|
import {
|
|
7
7
|
checkReadonly,
|
|
8
|
-
|
|
8
|
+
checkSealed,
|
|
9
9
|
checkExports,
|
|
10
10
|
checkModuleInterfaceImports,
|
|
11
11
|
} from "./index.ts";
|
|
@@ -57,9 +57,9 @@ export function runGates(
|
|
|
57
57
|
return { block: true, reason: formatDenial(filePath, readonlyResult.reason, absPath, index, cwd, config.moduleDescriptorFileName) };
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
62
|
-
return { block: true, reason: formatDenial(filePath,
|
|
60
|
+
const sealedResult = checkSealed(filePath, before, after, index, cwd, config.moduleDescriptorFileName);
|
|
61
|
+
if (sealedResult.blocked) {
|
|
62
|
+
return { block: true, reason: formatDenial(filePath, sealedResult.reason, absPath, index, cwd, config.moduleDescriptorFileName) };
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
const exportResult = checkExports(filePath, before, after, index, cwd, config.moduleDescriptorFileName);
|
|
@@ -3,18 +3,18 @@ import type { ModuleIndex } from "../types.ts";
|
|
|
3
3
|
import { getAncestorContracts, matchesPattern } from "../utils.ts";
|
|
4
4
|
import { getChecker } from "./checkers/registry.ts";
|
|
5
5
|
|
|
6
|
-
export type
|
|
6
|
+
export type SealedCheckResult =
|
|
7
7
|
| { blocked: true; reason: string }
|
|
8
8
|
| { blocked: false };
|
|
9
9
|
|
|
10
|
-
export function
|
|
10
|
+
export function checkSealed(
|
|
11
11
|
filePath: string,
|
|
12
12
|
beforeContent: string,
|
|
13
13
|
afterContent: string,
|
|
14
14
|
index: ModuleIndex,
|
|
15
15
|
cwd: string,
|
|
16
16
|
descriptorFileName: string,
|
|
17
|
-
):
|
|
17
|
+
): SealedCheckResult {
|
|
18
18
|
const absFile = path.resolve(cwd, filePath);
|
|
19
19
|
|
|
20
20
|
const checker = getChecker(absFile);
|
|
@@ -23,7 +23,7 @@ export function checkFrozen(
|
|
|
23
23
|
const ancestors = getAncestorContracts(absFile, index);
|
|
24
24
|
|
|
25
25
|
for (const contract of ancestors) {
|
|
26
|
-
for (const pattern of contract.
|
|
26
|
+
for (const pattern of contract.sealed) {
|
|
27
27
|
if (matchesPattern(absFile, pattern, contract.modulePath)) {
|
|
28
28
|
const newExports = checker.getNewExports(beforeContent, afterContent);
|
|
29
29
|
if (newExports.length === 0) return { blocked: false };
|
|
@@ -32,11 +32,11 @@ export function checkFrozen(
|
|
|
32
32
|
const names = newExports.map((s) => s.name).join(", ");
|
|
33
33
|
return {
|
|
34
34
|
blocked: true,
|
|
35
|
-
reason: `
|
|
35
|
+
reason: `Sealed rule: file is sealed in ${relModuleMd}. Cannot add new exports: ${names}`,
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
return { blocked: false };
|
|
42
|
-
}
|
|
42
|
+
}
|
package/src/graph/MODULE.md
CHANGED
|
@@ -5,7 +5,7 @@ export type VisibleEntryRaw = string | { path: string; modifier?: string };
|
|
|
5
5
|
export type ModuleFrontmatter = {
|
|
6
6
|
visible?: VisibleEntryRaw[];
|
|
7
7
|
readonly?: string[];
|
|
8
|
-
|
|
8
|
+
sealed?: string[];
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export function parseVisibleEntry(raw: VisibleEntryRaw): Signature {
|
package/src/types.ts
CHANGED
package/src/utils.ts
CHANGED
|
@@ -42,10 +42,10 @@ export function isWithinSourceRoot(absPath: string, resolvedRoot: string): boole
|
|
|
42
42
|
export function getAncestorContracts(
|
|
43
43
|
absFile: string,
|
|
44
44
|
index: ModuleIndex,
|
|
45
|
-
): { modulePath: string; readonly: string[];
|
|
45
|
+
): { modulePath: string; readonly: string[]; sealed: string[] }[] {
|
|
46
46
|
return index.contracts
|
|
47
47
|
.filter((c) => absFile.startsWith(c.modulePath + path.sep) || absFile === c.modulePath)
|
|
48
|
-
.map(({ modulePath, readonly,
|
|
48
|
+
.map(({ modulePath, readonly, sealed }) => ({ modulePath, readonly, sealed }));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export function matchesPattern(
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const SUPPORTED_EXTENSIONS: Set<string>;
|