@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 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
- - `frozen` — files where no new exports are allowed
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
- **Fronzen gate** — is there any surface change to the target file?
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
- ### Frozen constraints
60
+ ### Sealed constraints
61
61
 
62
62
  ```yaml
63
- frozen: [mod.rs]
63
+ sealed: [mod.rs]
64
64
  ```
65
- Frozen files cannot change their surface size: no new exports or public entries are allowed.
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-freeze-all](src/skills/module-freeze-all) has been included to auto-freeze modules.
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.17.1",
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.8.1"
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-freeze-all
3
- description: Auto-freeze all files in module descriptors so their module surface area cannot be increased.
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 `frozen` entries with code files in each module directory.
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-freeze-all/scripts/freeze-all.mjs [options]
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 `frozen` only)
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 `frozen` frontmatter field
31
- 4. Preserves existing `frozen` entries, other fields in the frontmatter, and body prose
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>;
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Auto-freeze all files in module descriptors.
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 `frozen` field with every direct code file in the module
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 = freezeModule(modDir, modFile, descriptorName, allModuleDirs);
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}: freeze [${r.files.join(", ") || "(none)"}]`);
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 freezeModule(modDir, descriptorFileName, descriptorName, allModuleDirs) {
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 existingFrozen = normalizeFrozen(parsed.frontmatter.frozen);
182
+ const existingSealed = normalizeSealed(parsed.frontmatter.sealed);
183
183
  const directFiles = listDirectFiles(modDir, descriptorName, allModuleDirs);
184
- const mergedFrozen = mergePreservingOrder(existingFrozen, directFiles);
184
+ const mergedSealed = mergePreservingOrder(existingSealed, directFiles);
185
185
 
186
- if (arraysEqual(existingFrozen, mergedFrozen)) return undefined;
186
+ if (arraysEqual(existingSealed, mergedSealed)) return undefined;
187
187
 
188
- const newContent = serializeModule(parsed, mergedFrozen);
188
+ const newContent = serializeModule(parsed, mergedSealed);
189
189
 
190
- return { path: modPath, content: newContent, files: mergedFrozen };
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, frozen) {
287
- const fm = { ...parsed.frontmatter, frozen };
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 normalizeFrozen(value) {
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
@@ -1,6 +1,5 @@
1
1
  ---
2
- frozen:
2
+ sealed:
3
3
  - config.ts
4
4
  - index.ts
5
- ---
6
-
5
+ ---
@@ -1,6 +1,5 @@
1
1
  ---
2
- frozen:
2
+ sealed:
3
3
  - system-prompt.ts
4
4
  - index.ts
5
- ---
6
-
5
+ ---
@@ -1,5 +1,5 @@
1
1
  ## Module gates (boundary enforcement)
2
- This project uses `{{descriptorFileName}}`(case-insensitive) files to declare visibility, readonly and frozen rules that you should follow.
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
- - `frozen`: files cannot add new exports, but still editable;
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;
@@ -1,8 +1,7 @@
1
1
  ---
2
- frozen:
2
+ sealed:
3
3
  - export-gate.ts
4
- - frozen-gate.ts
4
+ - sealed-gate.ts
5
5
  - readonly-gate.ts
6
6
  - index.ts
7
- ---
8
-
7
+ ---
@@ -1,8 +1,7 @@
1
1
  ---
2
- frozen:
2
+ sealed:
3
3
  - index.ts
4
4
  - registry.ts
5
5
  - rust.ts
6
6
  - typescript.ts
7
- ---
8
-
7
+ ---
@@ -13,7 +13,33 @@ const goChecker: ExportChecker = {
13
13
  registerChecker(goChecker);
14
14
 
15
15
  function extractExports(src: string): Signature[] {
16
- return [...src.matchAll(
17
- /^(?:func\s+(?!\()|type\s+|var\s+|const\s+)([\p{Lu}]\w*)/gmu,
18
- )].map((m) => ({ name: m[1] }));
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[1] }));
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 re = /^(?:(public|internal|protected|private)\s+)?(?:(?:data|sealed|enum|abstract|open)\s+)?(?:class|interface|object|fun|val|var|typealias)\s+(\w+)/gm;
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
- for (const m of src.matchAll(re)) {
19
- if (m[1] === "private") continue;
20
- results.push({
21
- modifier: m[1] || undefined,
22
- name: m[2],
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(extractPubItems(before).map((s) => s.name));
9
- return extractPubItems(after).filter((sig) => !beforeNames.has(sig.name));
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(?:\([^)]*\))?)\s+(?:fn|struct|enum|trait|type|const|mod)\s+(\w+)/gm,
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 re = /^(?:(private(?:\[[^\]]*\])?|protected(?:\[[^\]]*\])?)\s+)?(?:class|object|trait|def|val|var|type|given|extension)\s+(\w+)/gm;
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
- for (const m of src.matchAll(re)) {
19
- if (m[1] === "private" || m[1] === "protected") continue;
20
- results.push({
21
- modifier: m[1] || undefined,
22
- name: m[2],
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
- ...src.matchAll(
18
- /^export\s+(?:default\s+)?(?:\w+\s+)*(?:function(?:\s*\*)?|class|const|let|var|type|interface|enum)\s+(\w+)/gm,
19
- ),
20
- ].map((m) => ({ name: m[1] }));
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
- /^export\s*(?:type\s+)?\{\s*([^}]+)\s*\}\s*from/gm,
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
- const trimmed = entry.trim();
33
+ let trimmed = entry.trim();
28
34
  if (!trimmed) continue;
29
- const asMatch = trimmed.match(/^(\w+)\s+as\s+(\w+)$/);
30
- if (asMatch) {
31
- results.push({ name: asMatch[2] });
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(/^export\s*\*\s*as\s+(\w+)\s+from/gm)) {
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(/^export\s*\*\s+from/gm)) {
43
- results.push({ name: "*" });
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
- /^export\s+default\s+(?!function|class|const|let|var|type|interface|enum|abstract|async|declare)([a-zA-Z_]\w*)/gm,
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
  }
@@ -1,4 +1,4 @@
1
1
  export { checkReadonly } from "./readonly-gate.ts";
2
- export { checkFrozen } from "./frozen-gate.ts";
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";
@@ -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
- checkFrozen,
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 frozenResult = checkFrozen(filePath, before, after, index, cwd, config.moduleDescriptorFileName);
61
- if (frozenResult.blocked) {
62
- return { block: true, reason: formatDenial(filePath, frozenResult.reason, absPath, index, cwd, config.moduleDescriptorFileName) };
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 FrozenCheckResult =
6
+ export type SealedCheckResult =
7
7
  | { blocked: true; reason: string }
8
8
  | { blocked: false };
9
9
 
10
- export function checkFrozen(
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
- ): FrozenCheckResult {
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.frozen) {
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: `Frozen rule: file is frozen in ${relModuleMd}. Cannot add new exports: ${names}`,
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
+ }
@@ -1,9 +1,7 @@
1
1
  ---
2
- frozen:
2
+ sealed:
3
3
  - frontmatter-parser.ts
4
4
  - module-index-builder.ts
5
5
  - validation.ts
6
6
  - index.ts
7
- ---
8
-
9
-
7
+ ---
@@ -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
- frozen?: string[];
8
+ sealed?: string[];
9
9
  };
10
10
 
11
11
  export function parseVisibleEntry(raw: VisibleEntryRaw): Signature {
@@ -70,7 +70,7 @@ function buildContracts(
70
70
  ? frontmatter.visible.map(parseVisibleEntry)
71
71
  : null,
72
72
  readonly: readonlyEntries,
73
- frozen: frontmatter.frozen ?? [],
73
+ sealed: frontmatter.sealed ?? [],
74
74
  prose: body.trim(),
75
75
  });
76
76
  }
package/src/types.ts CHANGED
@@ -9,7 +9,7 @@ export type ModuleContract = {
9
9
  modulePath: string;
10
10
  visible: Signature[] | null;
11
11
  readonly: string[];
12
- frozen: string[];
12
+ sealed: string[];
13
13
  prose: string;
14
14
  };
15
15
 
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[]; frozen: 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, frozen }) => ({ modulePath, readonly, frozen }));
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>;