@osovv/grace-cli 3.2.0 → 3.4.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
@@ -10,13 +10,13 @@ This repository packages GRACE as reusable skills for coding agents. The current
10
10
  - knowledge-graph synchronization
11
11
  - controller-managed sequential or multi-agent implementation
12
12
 
13
- Current packaged version: `3.2.0`
13
+ Current packaged version: `3.4.0`
14
14
 
15
15
  ## What Changed In This Version
16
16
 
17
- - Documented the published Bun-powered `grace` CLI more explicitly across skills and repo context.
18
- - Added `grace lint` guidance as a fast integrity preflight alongside reviewer, refresh, and status workflows.
19
- - Updated the public install example to use `bun add -g @osovv/grace-cli`.
17
+ - Added a rich Python adapter without `pyright` for `grace lint`.
18
+ - Kept TypeScript/JavaScript on the TypeScript compiler API for exact export analysis.
19
+ - Made adapter failures non-fatal so linting can continue with structural checks and warnings.
20
20
 
21
21
  ## Repository Layout
22
22
 
@@ -195,6 +195,17 @@ The lint command is role-aware and adapter-aware:
195
195
  - export-map parity uses language adapters where available
196
196
  - file behavior is driven by semantic roles instead of filename masks
197
197
 
198
+ Current rich adapters:
199
+
200
+ - TypeScript/JavaScript via the TypeScript compiler API
201
+ - Python via the Python standard-library AST, without `pyright`
202
+
203
+ Current adapter behavior:
204
+
205
+ - TypeScript/JavaScript export analysis is treated as exact
206
+ - Python export analysis is exact when `__all__` is explicit, otherwise heuristic
207
+ - other languages still benefit from structural GRACE checks even when rich export analysis is not available yet
208
+
198
209
  Optional `MODULE_CONTRACT` fields for linting:
199
210
 
200
211
  ```text
@@ -215,16 +226,11 @@ Optional repository config file:
215
226
 
216
227
  ```json
217
228
  {
218
- "profile": "auto",
219
229
  "ignoredDirs": ["tmp"]
220
230
  }
221
231
  ```
222
232
 
223
- Profiles:
224
-
225
- - `auto` => require `docs/verification-plan.xml` only when the repo already looks verification-aware
226
- - `current` => require current GRACE artifacts
227
- - `legacy` => allow older GRACE repos without a verification plan
233
+ `grace lint` is current-only. Older GRACE repositories should fail until they are updated to the current artifact set, especially `docs/verification-plan.xml`.
228
234
 
229
235
  The validator checks marketplace/plugin metadata sync, version consistency, required fields, `.claude-plugin` structure, and hardcoded absolute paths. In branch or PR context it scopes validation to changed plugins via `git diff origin/main...HEAD`; otherwise it validates all plugins.
230
236
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osovv/grace-cli",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "description": "GRACE CLI for linting semantic markup, contracts, and GRACE XML artifacts with a Bun-powered grace binary.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/osovv/grace-marketplace#readme",
@@ -16,6 +16,8 @@
16
16
  "cli",
17
17
  "lint",
18
18
  "bun",
19
+ "python",
20
+ "typescript",
19
21
  "semantic-markup",
20
22
  "knowledge-graph"
21
23
  ],
package/src/grace-lint.ts CHANGED
@@ -6,7 +6,6 @@ import { formatTextReport, isValidTextFormat, lintGraceProject } from "./lint/co
6
6
  import type { LintOptions, LintResult } from "./lint/types";
7
7
 
8
8
  export type {
9
- EffectiveProfile,
10
9
  GraceLintConfig,
11
10
  LanguageAdapter,
12
11
  LanguageAnalysis,
@@ -16,7 +15,6 @@ export type {
16
15
  LintSeverity,
17
16
  MapMode,
18
17
  ModuleRole,
19
- RepoProfile,
20
18
  } from "./lint/types";
21
19
 
22
20
  export { formatTextReport, lintGraceProject } from "./lint/core";
@@ -48,11 +46,6 @@ export const lintCommand = defineCommand({
48
46
  description: "Output format: text or json",
49
47
  default: "text",
50
48
  },
51
- profile: {
52
- type: "string",
53
- description: "Lint profile: auto, current, or legacy",
54
- default: "auto",
55
- },
56
49
  allowMissingDocs: {
57
50
  type: "boolean",
58
51
  description: "Allow repositories that do not yet have full GRACE docs",
@@ -67,7 +60,6 @@ export const lintCommand = defineCommand({
67
60
 
68
61
  const result = lintGraceProject(String(context.args.path ?? "."), {
69
62
  allowMissingDocs: Boolean(context.args.allowMissingDocs),
70
- profile: String(context.args.profile ?? "auto") as LintOptions["profile"],
71
63
  });
72
64
 
73
65
  writeResult(format, result);
package/src/grace.ts CHANGED
@@ -7,7 +7,7 @@ import { lintCommand } from "./grace-lint";
7
7
  const main = defineCommand({
8
8
  meta: {
9
9
  name: "grace",
10
- version: "3.2.0",
10
+ version: "3.4.0",
11
11
  description: "GRACE CLI for linting semantic markup and GRACE project artifacts.",
12
12
  },
13
13
  subCommands: {
@@ -1,9 +1,10 @@
1
1
  import path from "node:path";
2
2
 
3
3
  import type { LanguageAdapter } from "../types";
4
+ import { createPythonAdapter } from "./python";
4
5
  import { createTypeScriptAdapter } from "./typescript";
5
6
 
6
- const adapters: LanguageAdapter[] = [createTypeScriptAdapter()];
7
+ const adapters: LanguageAdapter[] = [createTypeScriptAdapter(), createPythonAdapter()];
7
8
 
8
9
  export function getLanguageAdapter(filePath: string) {
9
10
  const normalizedPath = path.normalize(filePath);
@@ -0,0 +1,264 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import path from "node:path";
3
+
4
+ import type { LanguageAdapter, LanguageAnalysis } from "../types";
5
+
6
+ const PY_EXTENSIONS = new Set([".py", ".pyi"]);
7
+ const PYTHON_BINARIES = ["python3", "python"];
8
+
9
+ const PYTHON_ANALYZER_SCRIPT = String.raw`
10
+ import ast
11
+ import json
12
+ import os
13
+ import sys
14
+
15
+ source = sys.stdin.read()
16
+ file_path = sys.argv[1]
17
+ base_name = os.path.basename(file_path)
18
+
19
+
20
+ def is_public(name):
21
+ return isinstance(name, str) and len(name) > 0 and not name.startswith("_")
22
+
23
+
24
+ def extract_target_names(target):
25
+ if isinstance(target, ast.Name):
26
+ return [target.id]
27
+ if isinstance(target, (ast.Tuple, ast.List)):
28
+ names = []
29
+ for item in target.elts:
30
+ names.extend(extract_target_names(item))
31
+ return names
32
+ return []
33
+
34
+
35
+ def extract_string_sequence(node):
36
+ if isinstance(node, (ast.List, ast.Tuple, ast.Set)):
37
+ values = []
38
+ for element in node.elts:
39
+ if not isinstance(element, ast.Constant) or not isinstance(element.value, str):
40
+ return None
41
+ values.append(element.value)
42
+ return values
43
+ return None
44
+
45
+
46
+ def is_main_guard(test):
47
+ if not isinstance(test, ast.Compare):
48
+ return False
49
+ if len(test.ops) != 1 or len(test.comparators) != 1:
50
+ return False
51
+ left = test.left
52
+ comparator = test.comparators[0]
53
+ return (
54
+ isinstance(left, ast.Name)
55
+ and left.id == "__name__"
56
+ and isinstance(test.ops[0], ast.Eq)
57
+ and isinstance(comparator, ast.Constant)
58
+ and comparator.value == "__main__"
59
+ )
60
+
61
+
62
+ def imported_name(alias):
63
+ return alias.asname or alias.name.split(".", 1)[0]
64
+
65
+
66
+ local_public = set()
67
+ imported_public = set()
68
+ explicit_all = None
69
+ has_wildcard_reexport = False
70
+ direct_reexport_count = 0
71
+ local_implementation_count = 0
72
+ uses_test_framework = False
73
+ has_main_entrypoint = False
74
+
75
+ try:
76
+ tree = ast.parse(source, filename=file_path)
77
+ except SyntaxError as exc:
78
+ message = f"{exc.msg} at line {exc.lineno}:{exc.offset}" if exc.lineno else exc.msg
79
+ sys.stderr.write(message)
80
+ sys.exit(2)
81
+
82
+ for node in tree.body:
83
+ if isinstance(node, ast.Import):
84
+ for alias in node.names:
85
+ imported = imported_name(alias)
86
+ if is_public(imported):
87
+ imported_public.add(imported)
88
+ if alias.name == "pytest" or alias.name.startswith("unittest"):
89
+ uses_test_framework = True
90
+ continue
91
+
92
+ if isinstance(node, ast.ImportFrom):
93
+ module = node.module or ""
94
+ if module == "pytest" or module.startswith("unittest"):
95
+ uses_test_framework = True
96
+ if any(alias.name == "*" for alias in node.names):
97
+ has_wildcard_reexport = True
98
+ direct_reexport_count += 1
99
+ continue
100
+ public_imports = 0
101
+ for alias in node.names:
102
+ imported = imported_name(alias)
103
+ if is_public(imported):
104
+ imported_public.add(imported)
105
+ public_imports += 1
106
+ if public_imports:
107
+ direct_reexport_count += 1
108
+ continue
109
+
110
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
111
+ local_implementation_count += 1
112
+ if node.name.startswith("test_"):
113
+ uses_test_framework = True
114
+ if is_public(node.name):
115
+ local_public.add(node.name)
116
+ continue
117
+
118
+ if isinstance(node, ast.ClassDef):
119
+ local_implementation_count += 1
120
+ if node.name.startswith("Test"):
121
+ uses_test_framework = True
122
+ if is_public(node.name):
123
+ local_public.add(node.name)
124
+ continue
125
+
126
+ if isinstance(node, ast.Assign):
127
+ for target in node.targets:
128
+ for name in extract_target_names(target):
129
+ if name == "__all__":
130
+ sequence = extract_string_sequence(node.value)
131
+ if sequence is not None:
132
+ explicit_all = sequence
133
+ continue
134
+ if is_public(name):
135
+ local_public.add(name)
136
+ local_implementation_count += 1
137
+ continue
138
+
139
+ if isinstance(node, ast.AnnAssign):
140
+ for name in extract_target_names(node.target):
141
+ if name == "__all__":
142
+ sequence = extract_string_sequence(node.value)
143
+ if sequence is not None:
144
+ explicit_all = sequence
145
+ continue
146
+ if is_public(name):
147
+ local_public.add(name)
148
+ local_implementation_count += 1
149
+ continue
150
+
151
+ if isinstance(node, ast.If) and is_main_guard(node.test):
152
+ has_main_entrypoint = True
153
+
154
+ if explicit_all is not None:
155
+ export_names = sorted({name for name in explicit_all if isinstance(name, str)})
156
+ export_confidence = "exact"
157
+ else:
158
+ export_names = set(local_public)
159
+ if base_name == "__init__.py":
160
+ export_names.update(imported_public)
161
+ export_names = sorted(export_names)
162
+ export_confidence = "heuristic"
163
+
164
+ local_export_count = sum(1 for name in export_names if name in local_public)
165
+
166
+ print(json.dumps({
167
+ "exports": export_names,
168
+ "valueExports": export_names,
169
+ "typeExports": [],
170
+ "exportConfidence": export_confidence,
171
+ "hasDefaultExport": False,
172
+ "hasWildcardReExport": has_wildcard_reexport,
173
+ "hasMainEntrypoint": has_main_entrypoint,
174
+ "directReExportCount": direct_reexport_count,
175
+ "localExportCount": local_export_count,
176
+ "localImplementationCount": local_implementation_count,
177
+ "usesTestFramework": uses_test_framework,
178
+ }))
179
+ `;
180
+
181
+ function createEmptyAnalysis(): LanguageAnalysis {
182
+ return {
183
+ adapterId: "python",
184
+ exports: new Set<string>(),
185
+ valueExports: new Set<string>(),
186
+ typeExports: new Set<string>(),
187
+ exportConfidence: "heuristic",
188
+ hasDefaultExport: false,
189
+ hasWildcardReExport: false,
190
+ hasMainEntrypoint: false,
191
+ directReExportCount: 0,
192
+ localExportCount: 0,
193
+ localImplementationCount: 0,
194
+ usesTestFramework: false,
195
+ };
196
+ }
197
+
198
+ function normalizeResult(output: string) {
199
+ const parsed = JSON.parse(output) as {
200
+ exports: string[];
201
+ valueExports: string[];
202
+ typeExports: string[];
203
+ exportConfidence: "exact" | "heuristic";
204
+ hasDefaultExport: boolean;
205
+ hasWildcardReExport: boolean;
206
+ hasMainEntrypoint: boolean;
207
+ directReExportCount: number;
208
+ localExportCount: number;
209
+ localImplementationCount: number;
210
+ usesTestFramework: boolean;
211
+ };
212
+
213
+ const analysis = createEmptyAnalysis();
214
+ analysis.exports = new Set(parsed.exports ?? []);
215
+ analysis.valueExports = new Set(parsed.valueExports ?? []);
216
+ analysis.typeExports = new Set(parsed.typeExports ?? []);
217
+ analysis.exportConfidence = parsed.exportConfidence ?? "heuristic";
218
+ analysis.hasDefaultExport = Boolean(parsed.hasDefaultExport);
219
+ analysis.hasWildcardReExport = Boolean(parsed.hasWildcardReExport);
220
+ analysis.hasMainEntrypoint = Boolean(parsed.hasMainEntrypoint);
221
+ analysis.directReExportCount = Number(parsed.directReExportCount ?? 0);
222
+ analysis.localExportCount = Number(parsed.localExportCount ?? 0);
223
+ analysis.localImplementationCount = Number(parsed.localImplementationCount ?? 0);
224
+ analysis.usesTestFramework = Boolean(parsed.usesTestFramework);
225
+ return analysis;
226
+ }
227
+
228
+ function runPythonAnalyzer(filePath: string, text: string) {
229
+ for (const binary of PYTHON_BINARIES) {
230
+ const run = spawnSync(binary, ["-c", PYTHON_ANALYZER_SCRIPT, filePath], {
231
+ input: text,
232
+ encoding: "utf8",
233
+ maxBuffer: 1024 * 1024,
234
+ });
235
+
236
+ if (run.error) {
237
+ const code = (run.error as NodeJS.ErrnoException).code;
238
+ if (code === "ENOENT") {
239
+ continue;
240
+ }
241
+ throw run.error;
242
+ }
243
+
244
+ if (run.status === 0) {
245
+ return normalizeResult(run.stdout);
246
+ }
247
+
248
+ throw new Error(run.stderr.trim() || run.stdout.trim() || `Python analyzer failed via ${binary}.`);
249
+ }
250
+
251
+ throw new Error("Python adapter requires `python3` or `python` on PATH when linting Python files.");
252
+ }
253
+
254
+ export function createPythonAdapter(): LanguageAdapter {
255
+ return {
256
+ id: "python",
257
+ supports(filePath) {
258
+ return PY_EXTENSIONS.has(path.extname(filePath));
259
+ },
260
+ analyze(filePath, text) {
261
+ return runPythonAnalyzer(filePath, text);
262
+ },
263
+ };
264
+ }
@@ -73,8 +73,10 @@ export function createTypeScriptAdapter(): LanguageAdapter {
73
73
  exports: new Set<string>(),
74
74
  valueExports: new Set<string>(),
75
75
  typeExports: new Set<string>(),
76
+ exportConfidence: "exact",
76
77
  hasDefaultExport: false,
77
78
  hasWildcardReExport: false,
79
+ hasMainEntrypoint: false,
78
80
  directReExportCount: 0,
79
81
  localExportCount: 0,
80
82
  localImplementationCount: 0,
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import type { GraceLintConfig, LintIssue } from "./types";
5
5
 
6
6
  const CONFIG_FILE_NAME = ".grace-lint.json";
7
- const VALID_PROFILES = new Set(["auto", "current", "legacy"]);
7
+ const SUPPORTED_KEYS = new Set(["ignoredDirs"]);
8
8
 
9
9
  export function loadGraceLintConfig(projectRoot: string) {
10
10
  const configPath = path.join(projectRoot, CONFIG_FILE_NAME);
@@ -16,12 +16,26 @@ export function loadGraceLintConfig(projectRoot: string) {
16
16
  const parsed = JSON.parse(readFileSync(configPath, "utf8")) as GraceLintConfig;
17
17
  const issues: LintIssue[] = [];
18
18
 
19
- if (parsed.profile && !VALID_PROFILES.has(parsed.profile)) {
19
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
20
20
  issues.push({
21
21
  severity: "error",
22
- code: "config.invalid-profile",
22
+ code: "config.invalid-shape",
23
23
  file: CONFIG_FILE_NAME,
24
- message: `Unsupported profile \`${parsed.profile}\` in ${CONFIG_FILE_NAME}. Use \`auto\`, \`current\`, or \`legacy\`.`,
24
+ message: `${CONFIG_FILE_NAME} must contain a JSON object.`,
25
+ });
26
+ return { config: parsed, issues };
27
+ }
28
+
29
+ for (const key of Object.keys(parsed)) {
30
+ if (SUPPORTED_KEYS.has(key)) {
31
+ continue;
32
+ }
33
+
34
+ issues.push({
35
+ severity: "error",
36
+ code: "config.unknown-key",
37
+ file: CONFIG_FILE_NAME,
38
+ message: `Unsupported key \`${key}\` in ${CONFIG_FILE_NAME}. Supported keys: ignoredDirs.`,
25
39
  });
26
40
  }
27
41
 
package/src/lint/core.ts CHANGED
@@ -4,7 +4,6 @@ import path from "node:path";
4
4
  import { loadGraceLintConfig } from "./config";
5
5
  import { getLanguageAdapter } from "./adapters/base";
6
6
  import type {
7
- EffectiveProfile,
8
7
  GraceLintConfig,
9
8
  LanguageAnalysis,
10
9
  LintIssue,
@@ -15,13 +14,9 @@ import type {
15
14
  ModuleContractInfo,
16
15
  ModuleMapItem,
17
16
  ModuleRole,
18
- RepoProfile,
19
17
  } from "./types";
20
18
 
21
- const REQUIRED_DOCS_BY_PROFILE: Record<EffectiveProfile, string[]> = {
22
- legacy: ["docs/knowledge-graph.xml", "docs/development-plan.xml"],
23
- current: ["docs/knowledge-graph.xml", "docs/development-plan.xml", "docs/verification-plan.xml"],
24
- };
19
+ const REQUIRED_DOCS = ["docs/knowledge-graph.xml", "docs/development-plan.xml", "docs/verification-plan.xml"] as const;
25
20
 
26
21
  const OPTIONAL_PACKET_DOC = "docs/operational-packets.xml";
27
22
  const LINT_CONFIG_FILE = ".grace-lint.json";
@@ -46,6 +41,7 @@ const CODE_EXTENSIONS = new Set([
46
41
  ".mts",
47
42
  ".cts",
48
43
  ".py",
44
+ ".pyi",
49
45
  ".go",
50
46
  ".java",
51
47
  ".kt",
@@ -437,6 +433,10 @@ function inferRole(contract: ModuleContractInfo | null, analysis: LanguageAnalys
437
433
  return "CONFIG";
438
434
  }
439
435
 
436
+ if (analysis?.hasMainEntrypoint) {
437
+ return "SCRIPT";
438
+ }
439
+
440
440
  if (analysis && mentionsScript && analysis.exports.size === 0) {
441
441
  return "SCRIPT";
442
442
  }
@@ -557,46 +557,6 @@ function lintRequiredPacketSections(result: LintResult, relativePath: string, te
557
557
  }
558
558
  }
559
559
 
560
- function resolveProfile(
561
- requestedProfile: RepoProfile | undefined,
562
- configProfile: RepoProfile | undefined,
563
- docs: Record<string, string | null>,
564
- ) {
565
- const desiredProfile = requestedProfile ?? configProfile ?? "auto";
566
- if (desiredProfile !== "auto") {
567
- return desiredProfile;
568
- }
569
-
570
- const verificationPlan = docs["docs/verification-plan.xml"];
571
- if (verificationPlan) {
572
- return "current" as const;
573
- }
574
-
575
- const joinedDocs = `${docs["docs/knowledge-graph.xml"] ?? ""}\n${docs["docs/development-plan.xml"] ?? ""}`;
576
- return /<verification-ref>|<V-M-/.test(joinedDocs) ? "current" : "legacy";
577
- }
578
-
579
- function validateProfileSelection(profile: string | undefined) {
580
- if (!profile) {
581
- return null;
582
- }
583
-
584
- if (profile === "auto" || profile === "current" || profile === "legacy") {
585
- return null;
586
- }
587
-
588
- return {
589
- severity: "error",
590
- code: "config.invalid-profile-selection",
591
- file: "CLI",
592
- message: `Unsupported profile \`${profile}\`. Use \`auto\`, \`current\`, or \`legacy\`.`,
593
- } satisfies LintIssue;
594
- }
595
-
596
- function isInvalidProfileIssue(issue: LintIssue) {
597
- return issue.code === "config.invalid-profile" || issue.code === "config.invalid-profile-selection";
598
- }
599
-
600
560
  function lintExportMapParity(
601
561
  result: LintResult,
602
562
  relativePath: string,
@@ -609,6 +569,17 @@ function lintExportMapParity(
609
569
  return;
610
570
  }
611
571
 
572
+ const exportSeverity = analysis.exportConfidence === "exact" ? "error" : "warning";
573
+
574
+ if (analysis.exportConfidence === "heuristic") {
575
+ addIssue(result, {
576
+ severity: "warning",
577
+ code: "analysis.heuristic-export-surface",
578
+ file: relativePath,
579
+ message: `The ${analysis.adapterId} adapter inferred exports heuristically for this file. Exact MODULE_MAP parity may require explicit file ROLE/MAP_MODE or stronger language-specific export declarations.`,
580
+ });
581
+ }
582
+
612
583
  if (analysis.hasWildcardReExport) {
613
584
  addIssue(result, {
614
585
  severity: "warning",
@@ -622,7 +593,7 @@ function lintExportMapParity(
622
593
  const mappedSymbols = new Set(items.flatMap((item) => (item.symbolName ? [item.symbolName] : [])));
623
594
  if (mappedSymbols.size === 0 && analysis.exports.size > 0) {
624
595
  addIssue(result, {
625
- severity: "error",
596
+ severity: exportSeverity,
626
597
  code: "markup.module-map-missing-symbol-entries",
627
598
  file: relativePath,
628
599
  message: "MODULE_MAP should list concrete symbol names when MAP_MODE resolves to EXPORTS.",
@@ -633,7 +604,7 @@ function lintExportMapParity(
633
604
  for (const exportName of analysis.exports) {
634
605
  if (!mappedSymbols.has(exportName)) {
635
606
  addIssue(result, {
636
- severity: "error",
607
+ severity: exportSeverity,
637
608
  code: "markup.module-map-missing-export",
638
609
  file: relativePath,
639
610
  message: `MODULE_MAP is missing the exported symbol \`${exportName}\`.`,
@@ -702,7 +673,19 @@ function lintGovernedFile(result: LintResult, root: string, filePath: string, te
702
673
  const contract = moduleContractSection ? parseModuleContract(moduleContractSection) : null;
703
674
  const mapItems = moduleMapSection ? parseModuleMapItems(moduleMapSection) : [];
704
675
  const adapter = getLanguageAdapter(filePath);
705
- const analysis = adapter ? adapter.analyze(filePath, text) : null;
676
+ let analysis: LanguageAnalysis | null = null;
677
+ if (adapter) {
678
+ try {
679
+ analysis = adapter.analyze(filePath, text);
680
+ } catch (error) {
681
+ addIssue(result, {
682
+ severity: "warning",
683
+ code: "analysis.adapter-failed",
684
+ file: relativePath,
685
+ message: `${adapter.id} adapter failed: ${error instanceof Error ? error.message : String(error)}`,
686
+ });
687
+ }
688
+ }
706
689
  const role = inferRole(contract, analysis);
707
690
  const mapMode = inferMapMode(contract, role, mapItems, analysis);
708
691
 
@@ -771,11 +754,6 @@ function lintGovernedFile(result: LintResult, root: string, filePath: string, te
771
754
  export function lintGraceProject(projectRoot: string, options: LintOptions = {}): LintResult {
772
755
  const root = path.resolve(projectRoot);
773
756
  const { config, issues: configIssues } = loadGraceLintConfig(root);
774
- const requestedProfileIssue = validateProfileSelection(options.profile as string | undefined);
775
- const effectiveRequestedProfile = requestedProfileIssue ? undefined : options.profile;
776
- const effectiveConfigProfile = configIssues.some((issue) => issue.code === "config.invalid-profile")
777
- ? undefined
778
- : config?.profile;
779
757
 
780
758
  const docs = {
781
759
  "docs/knowledge-graph.xml": readTextIfExists(path.join(root, "docs/knowledge-graph.xml")),
@@ -783,29 +761,26 @@ export function lintGraceProject(projectRoot: string, options: LintOptions = {})
783
761
  "docs/verification-plan.xml": readTextIfExists(path.join(root, "docs/verification-plan.xml")),
784
762
  } satisfies Record<string, string | null>;
785
763
 
786
- const profile = resolveProfile(effectiveRequestedProfile, effectiveConfigProfile, docs);
787
764
  const result: LintResult = {
788
765
  root,
789
- profile,
790
766
  filesChecked: 0,
791
767
  governedFiles: 0,
792
768
  xmlFilesChecked: 0,
793
- issues: [...configIssues, ...(requestedProfileIssue ? [requestedProfileIssue] : [])],
769
+ issues: [...configIssues],
794
770
  };
795
771
 
796
- if (configIssues.some((issue) => issue.severity === "error" && issue.file === LINT_CONFIG_FILE && !isInvalidProfileIssue(issue))) {
772
+ if (configIssues.some((issue) => issue.severity === "error" && issue.file === LINT_CONFIG_FILE)) {
797
773
  return result;
798
774
  }
799
775
 
800
- const requiredDocs = REQUIRED_DOCS_BY_PROFILE[profile];
801
776
  if (!options.allowMissingDocs) {
802
- for (const relativePath of requiredDocs) {
777
+ for (const relativePath of REQUIRED_DOCS) {
803
778
  if (!docs[relativePath]) {
804
779
  addIssue(result, {
805
780
  severity: "error",
806
781
  code: "docs.missing-required-artifact",
807
782
  file: relativePath,
808
- message: `Missing required GRACE artifact \`${relativePath}\` for the ${profile} profile.`,
783
+ message: `Missing required current GRACE artifact \`${relativePath}\`.`,
809
784
  });
810
785
  }
811
786
  }
@@ -928,7 +903,6 @@ export function formatTextReport(result: LintResult) {
928
903
  "GRACE Lint Report",
929
904
  "=================",
930
905
  `Root: ${result.root}`,
931
- `Profile: ${result.profile}`,
932
906
  `Code files checked: ${result.filesChecked}`,
933
907
  `Governed files checked: ${result.governedFiles}`,
934
908
  `XML files checked: ${result.xmlFilesChecked}`,
package/src/lint/types.ts CHANGED
@@ -1,8 +1,5 @@
1
1
  export type LintSeverity = "error" | "warning";
2
2
 
3
- export type RepoProfile = "auto" | "current" | "legacy";
4
- export type EffectiveProfile = Exclude<RepoProfile, "auto">;
5
-
6
3
  export type ModuleRole = "RUNTIME" | "TEST" | "BARREL" | "CONFIG" | "TYPES" | "SCRIPT";
7
4
  export type MapMode = "EXPORTS" | "LOCALS" | "SUMMARY" | "NONE";
8
5
 
@@ -16,7 +13,6 @@ export type LintIssue = {
16
13
 
17
14
  export type LintResult = {
18
15
  root: string;
19
- profile: EffectiveProfile;
20
16
  filesChecked: number;
21
17
  governedFiles: number;
22
18
  xmlFilesChecked: number;
@@ -25,11 +21,9 @@ export type LintResult = {
25
21
 
26
22
  export type LintOptions = {
27
23
  allowMissingDocs?: boolean;
28
- profile?: RepoProfile;
29
24
  };
30
25
 
31
26
  export type GraceLintConfig = {
32
- profile?: RepoProfile;
33
27
  ignoredDirs?: string[];
34
28
  };
35
29
 
@@ -60,8 +54,10 @@ export type LanguageAnalysis = {
60
54
  exports: Set<string>;
61
55
  valueExports: Set<string>;
62
56
  typeExports: Set<string>;
57
+ exportConfidence: "exact" | "heuristic";
63
58
  hasDefaultExport: boolean;
64
59
  hasWildcardReExport: boolean;
60
+ hasMainEntrypoint: boolean;
65
61
  directReExportCount: number;
66
62
  localExportCount: number;
67
63
  localImplementationCount: number;