@osovv/grace-cli 3.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GRACE Framework Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # GRACE Framework - AI Agent Skills
2
+
3
+ **GRACE** (Graph-RAG Anchored Code Engineering) is a methodology for AI-driven code generation with semantic markup, knowledge graphs, contracts, and log-driven verification. Originally created by **Vladimir Ivanov** ([@turboplanner](https://t.me/turboplanner)).
4
+
5
+ This repository packages GRACE as reusable skills for coding agents. The current workflow is opinionated around:
6
+
7
+ - contract-first planning
8
+ - verification-first execution
9
+ - semantic markup for navigation and patching
10
+ - knowledge-graph synchronization
11
+ - controller-managed sequential or multi-agent implementation
12
+
13
+ Current packaged version: `3.1.0`
14
+
15
+ ## What Changed In This Version
16
+
17
+ - Added `grace-refactor` for safe rename/move/split/merge/extract workflows with graph and verification synchronization.
18
+ - Added `docs/operational-packets.xml` as the canonical reference for execution packets, graph deltas, verification deltas, and failure packets.
19
+ - Added a Bun-based `grace-lint` CLI built on `citty` for GRACE semantic markup and XML integrity checks.
20
+
21
+ ## Repository Layout
22
+
23
+ - `skills/grace/` - Agent Skills format
24
+ - `.claude-plugin/` - Claude Code marketplace packaging
25
+ - `openpackage.yml` - OpenPackage metadata
26
+
27
+ ## Installation
28
+
29
+ ### Via OpenPackage (recommended)
30
+
31
+ Install the [OpenPackage CLI](https://github.com/enulus/OpenPackage) first (`npm install -g opkg`), then:
32
+
33
+ ```bash
34
+ # Install GRACE to your workspace
35
+ opkg install gh@osovv/grace-marketplace
36
+
37
+ # Or install globally
38
+ opkg install gh@osovv/grace-marketplace -g
39
+
40
+ # Install only specific resource types
41
+ opkg install gh@osovv/grace-marketplace -s
42
+ opkg install gh@osovv/grace-marketplace -a
43
+
44
+ # Install to a specific platform
45
+ opkg install gh@osovv/grace-marketplace --platforms claude-code
46
+ opkg install gh@osovv/grace-marketplace --platforms cursor
47
+ opkg install gh@osovv/grace-marketplace --platforms opencode
48
+ ```
49
+
50
+ ### Via Claude Code Plugin Marketplace
51
+
52
+ ```bash
53
+ /plugin marketplace add osovv/grace-marketplace
54
+ /plugin install grace@grace-marketplace
55
+ ```
56
+
57
+ ### Via npx skills (Vercel Skills CLI)
58
+
59
+ ```bash
60
+ npx skills add osovv/grace-marketplace
61
+ npx skills add osovv/grace-marketplace -g
62
+ npx skills add osovv/grace-marketplace -a claude-code
63
+ ```
64
+
65
+ > Browse more skills at [skills.sh](https://skills.sh)
66
+
67
+ ### Via Bun (CLI)
68
+
69
+ Requires `bun` on `PATH`. The published CLI keeps the Bun shebang and installs the `grace` binary.
70
+
71
+ ```bash
72
+ bun add -g @osovv/grace-cli
73
+ grace lint --path /path/to/grace-project
74
+ ```
75
+
76
+ ### Via Codex CLI
77
+
78
+ Inside Codex, use the built-in skill installer:
79
+
80
+ ```text
81
+ $skill-installer install https://github.com/osovv/grace-marketplace/tree/main/skills/grace/grace-init
82
+ $skill-installer install https://github.com/osovv/grace-marketplace/tree/main/skills/grace/grace-plan
83
+ $skill-installer install https://github.com/osovv/grace-marketplace/tree/main/skills/grace/grace-execute
84
+ $skill-installer install https://github.com/osovv/grace-marketplace/tree/main/skills/grace/grace-multiagent-execute
85
+ $skill-installer install https://github.com/osovv/grace-marketplace/tree/main/skills/grace/grace-refactor
86
+ $skill-installer install https://github.com/osovv/grace-marketplace/tree/main/skills/grace/grace-setup-subagents
87
+ $skill-installer install https://github.com/osovv/grace-marketplace/tree/main/skills/grace/grace-fix
88
+ $skill-installer install https://github.com/osovv/grace-marketplace/tree/main/skills/grace/grace-refresh
89
+ $skill-installer install https://github.com/osovv/grace-marketplace/tree/main/skills/grace/grace-status
90
+ $skill-installer install https://github.com/osovv/grace-marketplace/tree/main/skills/grace/grace-ask
91
+ $skill-installer install https://github.com/osovv/grace-marketplace/tree/main/skills/grace/grace-explainer
92
+ $skill-installer install https://github.com/osovv/grace-marketplace/tree/main/skills/grace/grace-verification
93
+ $skill-installer install https://github.com/osovv/grace-marketplace/tree/main/skills/grace/grace-reviewer
94
+ ```
95
+
96
+ After installation, restart Codex to activate the skills.
97
+
98
+ ### Via Kilo Code
99
+
100
+ Copy skills to your Kilo Code skills directory:
101
+
102
+ ```bash
103
+ git clone https://github.com/osovv/grace-marketplace
104
+ cp -r grace-marketplace/skills/grace/grace-* ~/.kilocode/skills/
105
+ ```
106
+
107
+ ### Any Agent Skills-compatible agent
108
+
109
+ ```bash
110
+ git clone https://github.com/osovv/grace-marketplace
111
+ cp -r grace-marketplace/skills/grace/grace-* /path/to/your/agent/skills/
112
+ ```
113
+
114
+ ## Quick Start
115
+
116
+ ```bash
117
+ # 1. Bootstrap GRACE docs and templates
118
+ /grace-init
119
+
120
+ # 2. Fill requirements.xml and technology.xml
121
+
122
+ # 3. Plan modules, flows, graph, and verification refs
123
+ /grace-plan
124
+
125
+ # 4. Deepen testing, traces, and log-driven evidence
126
+ /grace-verification
127
+
128
+ # 5a. Execute the plan sequentially
129
+ /grace-execute
130
+
131
+ # 5b. Execute in parallel-safe waves
132
+ /grace-multiagent-execute
133
+ ```
134
+
135
+ `/grace-multiagent-execute` supports `safe`, `balanced`, and `fast` controller profiles. Use `balanced` by default, `safe` for risky or weakly verified modules, and `fast` only when module-local and wave-level verification are already strong.
136
+
137
+ ## Core Artifacts
138
+
139
+ - `docs/requirements.xml` - product intent and use cases
140
+ - `docs/technology.xml` - runtime, tooling, testing, observability, constraints
141
+ - `docs/development-plan.xml` - modules, contracts, flows, phases, execution ownership
142
+ - `docs/verification-plan.xml` - tests, traces, required log markers, and gates
143
+ - `docs/knowledge-graph.xml` - project navigation graph
144
+ - `docs/operational-packets.xml` - canonical execution packet, delta, and failure handoff templates
145
+
146
+ ## Skills
147
+
148
+ | Skill | Description |
149
+ |---|---|
150
+ | `grace-init` | Bootstrap GRACE docs, AGENTS, and XML templates |
151
+ | `grace-plan` | Architect modules, flows, knowledge graph, and verification refs |
152
+ | `grace-verification` | Design and maintain tests, traces, and log-driven evidence |
153
+ | `grace-execute` | Execute the full plan sequentially with scoped review and commits |
154
+ | `grace-multiagent-execute` | Execute independent modules in controller-managed parallel waves |
155
+ | `grace-refactor` | Refactor modules safely while keeping contracts, graph, and verification synchronized |
156
+ | `grace-setup-subagents` | Scaffold shell-specific GRACE worker and reviewer presets |
157
+ | `grace-fix` | Debug via semantic navigation, tests, and log markers |
158
+ | `grace-refresh` | Sync shared artifacts with the real codebase |
159
+ | `grace-status` | Project health report across docs, graph, and verification |
160
+ | `grace-ask` | Answer questions with full project context |
161
+ | `grace-explainer` | Complete GRACE methodology reference |
162
+ | `grace-reviewer` | Validate semantic markup, contracts, graph, and verification integrity |
163
+
164
+ ## Compatibility
165
+
166
+ | Agent | Installation | Format |
167
+ |---|---|---|
168
+ | **Any (via OpenPackage)** | `opkg install` | OpenPackage (`openpackage.yml`) |
169
+ | **Claude Code** | `/plugin install` or `npx skills add` | Native plugin (`.claude-plugin/`) |
170
+ | **Codex CLI** | `$skill-installer` | Agent Skills (`skills/`) |
171
+ | **Kilo Code** | Copy to `~/.kilocode/skills/` | Agent Skills (`skills/`) |
172
+ | **Cursor, Windsurf, etc.** | `opkg install --platforms <name>` | OpenPackage (`openpackage.yml`) |
173
+ | **Other agents** | Copy to agent's skills directory | Agent Skills (`skills/`) |
174
+
175
+ All skills follow the [Agent Skills](https://agentskills.io) open standard and the [OpenPackage](https://github.com/enulus/OpenPackage) specification.
176
+
177
+ ## Development
178
+
179
+ Run the marketplace validator from the repository root:
180
+
181
+ ```bash
182
+ bun run ./scripts/validate-marketplace.ts
183
+ ```
184
+
185
+ Run the GRACE lint CLI against a GRACE project:
186
+
187
+ ```bash
188
+ bun install
189
+ bun run grace lint --path /path/to/grace-project
190
+ ```
191
+
192
+ 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.
193
+
194
+ ## Origin
195
+
196
+ GRACE was designed and battle-tested by Vladimir Ivanov ([@turboplanner](https://t.me/turboplanner)). See the [TurboProject](https://t.me/turboproject) Telegram channel for more on the methodology. This repository extracts GRACE into a standalone, project-agnostic format.
197
+
198
+ ## License
199
+
200
+ MIT
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@osovv/grace-cli",
3
+ "version": "3.1.0",
4
+ "description": "GRACE CLI for linting semantic markup, contracts, and GRACE XML artifacts with a Bun-powered grace binary.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/osovv/grace-marketplace#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/osovv/grace-marketplace.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/osovv/grace-marketplace/issues"
13
+ },
14
+ "keywords": [
15
+ "grace",
16
+ "cli",
17
+ "lint",
18
+ "bun",
19
+ "semantic-markup",
20
+ "knowledge-graph"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "files": [
26
+ "src/grace.ts",
27
+ "src/grace-lint.ts",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "type": "module",
32
+ "packageManager": "bun@1.3.8",
33
+ "engines": {
34
+ "bun": ">=1.3.8"
35
+ },
36
+ "scripts": {
37
+ "grace": "bun run ./src/grace.ts",
38
+ "grace:lint": "bun run ./src/grace.ts lint",
39
+ "lint": "bun run ./src/grace.ts lint",
40
+ "test": "bun test",
41
+ "validate:marketplace": "bun run ./scripts/validate-marketplace.ts",
42
+ "prepublishOnly": "bun test && bun run ./src/grace.ts lint --path . --allow-missing-docs && bun run ./scripts/validate-marketplace.ts"
43
+ },
44
+ "bin": {
45
+ "grace": "./src/grace.ts"
46
+ },
47
+ "dependencies": {
48
+ "citty": "^0.2.2"
49
+ }
50
+ }
@@ -0,0 +1,782 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { defineCommand, type CommandDef, runMain } from "citty";
6
+
7
+ export type LintSeverity = "error" | "warning";
8
+
9
+ export type LintIssue = {
10
+ severity: LintSeverity;
11
+ code: string;
12
+ file: string;
13
+ line?: number;
14
+ message: string;
15
+ };
16
+
17
+ export type LintResult = {
18
+ root: string;
19
+ filesChecked: number;
20
+ governedFiles: number;
21
+ xmlFilesChecked: number;
22
+ issues: LintIssue[];
23
+ };
24
+
25
+ export type LintOptions = {
26
+ allowMissingDocs?: boolean;
27
+ };
28
+
29
+ const REQUIRED_DOCS = [
30
+ "docs/knowledge-graph.xml",
31
+ "docs/development-plan.xml",
32
+ "docs/verification-plan.xml",
33
+ ] as const;
34
+
35
+ const OPTIONAL_PACKET_DOC = "docs/operational-packets.xml";
36
+
37
+ const CODE_EXTENSIONS = new Set([
38
+ ".js",
39
+ ".jsx",
40
+ ".ts",
41
+ ".tsx",
42
+ ".mjs",
43
+ ".cjs",
44
+ ".mts",
45
+ ".cts",
46
+ ".py",
47
+ ".go",
48
+ ".java",
49
+ ".kt",
50
+ ".rs",
51
+ ".rb",
52
+ ".php",
53
+ ".swift",
54
+ ".scala",
55
+ ".sql",
56
+ ".sh",
57
+ ".bash",
58
+ ".zsh",
59
+ ]);
60
+
61
+ const IGNORED_DIRS = new Set([
62
+ ".git",
63
+ "node_modules",
64
+ "dist",
65
+ "build",
66
+ "coverage",
67
+ ".next",
68
+ ".turbo",
69
+ ".cache",
70
+ ]);
71
+
72
+ const TS_LIKE_EXTENSIONS = new Set([
73
+ ".js",
74
+ ".jsx",
75
+ ".ts",
76
+ ".tsx",
77
+ ".mjs",
78
+ ".cjs",
79
+ ".mts",
80
+ ".cts",
81
+ ]);
82
+
83
+ const UNIQUE_TAG_ANTI_PATTERNS = [
84
+ {
85
+ code: "xml.generic-module-tag",
86
+ regex: /<\/?Module(?=[\s>])/g,
87
+ message: 'Use unique module tags like `<M-AUTH>` instead of generic `<Module ID="...">`.',
88
+ },
89
+ {
90
+ code: "xml.generic-phase-tag",
91
+ regex: /<\/?Phase(?=[\s>])/g,
92
+ message: 'Use unique phase tags like `<Phase-1>` instead of generic `<Phase number="...">`.',
93
+ },
94
+ {
95
+ code: "xml.generic-flow-tag",
96
+ regex: /<\/?Flow(?=[\s>])/g,
97
+ message: 'Use unique flow tags like `<DF-LOGIN>` instead of generic `<Flow ID="...">`.',
98
+ },
99
+ {
100
+ code: "xml.generic-use-case-tag",
101
+ regex: /<\/?UseCase(?=[\s>])/g,
102
+ message: 'Use unique use-case tags like `<UC-001>` instead of generic `<UseCase ID="...">`.',
103
+ },
104
+ {
105
+ code: "xml.generic-step-tag",
106
+ regex: /<\/?step(?=[\s>])/g,
107
+ message: 'Use unique step tags like `<step-1>` instead of generic `<step order="...">`.',
108
+ },
109
+ {
110
+ code: "xml.generic-export-tag",
111
+ regex: /<\/?export(?=[\s>])/g,
112
+ message: 'Use unique export tags like `<export-run>` instead of generic `<export name="...">`.',
113
+ },
114
+ {
115
+ code: "xml.generic-function-tag",
116
+ regex: /<\/?function(?=[\s>])/g,
117
+ message: 'Use unique function tags like `<fn-run>` instead of generic `<function name="...">`.',
118
+ },
119
+ {
120
+ code: "xml.generic-type-tag",
121
+ regex: /<\/?type(?=[\s>])/g,
122
+ message: 'Use unique type tags like `<type-Result>` instead of generic `<type name="...">`.',
123
+ },
124
+ ];
125
+
126
+ const TEXT_FORMAT_OPTIONS = new Set(["text", "json"]);
127
+
128
+ function normalizeRelative(root: string, filePath: string) {
129
+ return path.relative(root, filePath) || ".";
130
+ }
131
+
132
+ function lineNumberAt(text: string, index: number) {
133
+ return text.slice(0, index).split("\n").length;
134
+ }
135
+
136
+ function addIssue(result: LintResult, issue: LintIssue) {
137
+ result.issues.push(issue);
138
+ }
139
+
140
+ function readTextIfExists(filePath: string) {
141
+ return existsSync(filePath) ? readFileSync(filePath, "utf8") : null;
142
+ }
143
+
144
+ function collectCodeFiles(root: string, currentDir = root): string[] {
145
+ const files: string[] = [];
146
+ const entries = readdirSync(currentDir, { withFileTypes: true });
147
+
148
+ for (const entry of entries) {
149
+ if (entry.isDirectory()) {
150
+ if (IGNORED_DIRS.has(entry.name)) {
151
+ continue;
152
+ }
153
+
154
+ files.push(...collectCodeFiles(root, path.join(currentDir, entry.name)));
155
+ continue;
156
+ }
157
+
158
+ if (!entry.isFile()) {
159
+ continue;
160
+ }
161
+
162
+ const filePath = path.join(currentDir, entry.name);
163
+ if (CODE_EXTENSIONS.has(path.extname(filePath))) {
164
+ files.push(filePath);
165
+ }
166
+ }
167
+
168
+ return files;
169
+ }
170
+
171
+ function stripQuotedStrings(text: string) {
172
+ let result = "";
173
+ let quote: '"' | "'" | "`" | null = null;
174
+ let escaped = false;
175
+
176
+ for (const char of text) {
177
+ if (!quote) {
178
+ if (char === '"' || char === "'" || char === "`") {
179
+ quote = char;
180
+ result += " ";
181
+ continue;
182
+ }
183
+
184
+ result += char;
185
+ continue;
186
+ }
187
+
188
+ if (escaped) {
189
+ escaped = false;
190
+ result += char === "\n" ? "\n" : " ";
191
+ continue;
192
+ }
193
+
194
+ if (char === "\\") {
195
+ escaped = true;
196
+ result += " ";
197
+ continue;
198
+ }
199
+
200
+ if (char === quote) {
201
+ quote = null;
202
+ result += " ";
203
+ continue;
204
+ }
205
+
206
+ result += char === "\n" ? "\n" : " ";
207
+ }
208
+
209
+ return result;
210
+ }
211
+
212
+ function hasGraceMarkers(text: string) {
213
+ const searchable = stripQuotedStrings(text);
214
+ return searchable.split("\n").some((line) => /^(\s*)(\/\/|#|--|\*)\s*(START_MODULE_CONTRACT|START_MODULE_MAP|START_CONTRACT:|START_BLOCK_|START_CHANGE_SUMMARY)/.test(line));
215
+ }
216
+
217
+ function ensureSectionPair(
218
+ result: LintResult,
219
+ root: string,
220
+ relativePath: string,
221
+ text: string,
222
+ startMarker: string,
223
+ endMarker: string,
224
+ code: string,
225
+ message: string,
226
+ ) {
227
+ const startIndex = text.indexOf(startMarker);
228
+ const endIndex = text.indexOf(endMarker);
229
+
230
+ if (startIndex === -1 || endIndex === -1) {
231
+ addIssue(result, {
232
+ severity: "error",
233
+ code,
234
+ file: relativePath,
235
+ line: startIndex === -1 ? undefined : lineNumberAt(text, startIndex),
236
+ message,
237
+ });
238
+ return null;
239
+ }
240
+
241
+ if (startIndex > endIndex) {
242
+ addIssue(result, {
243
+ severity: "error",
244
+ code,
245
+ file: relativePath,
246
+ line: lineNumberAt(text, endIndex),
247
+ message: `${message} Found the end marker before the start marker.`,
248
+ });
249
+ return null;
250
+ }
251
+
252
+ const sectionStart = startIndex + startMarker.length;
253
+ return text.slice(sectionStart, endIndex);
254
+ }
255
+
256
+ function lintScopedMarkers(
257
+ result: LintResult,
258
+ relativePath: string,
259
+ text: string,
260
+ startRegex: RegExp,
261
+ endRegex: RegExp,
262
+ kind: "block" | "contract",
263
+ ) {
264
+ const lines = text.split("\n");
265
+ const stack: Array<{ name: string; line: number }> = [];
266
+ const seen = new Set<string>();
267
+
268
+ for (let index = 0; index < lines.length; index += 1) {
269
+ const line = lines[index];
270
+ const startMatch = line.match(startRegex);
271
+ const endMatch = line.match(endRegex);
272
+
273
+ if (startMatch?.[1]) {
274
+ const name = startMatch[1];
275
+ if (kind === "block") {
276
+ if (seen.has(name)) {
277
+ addIssue(result, {
278
+ severity: "error",
279
+ code: "markup.duplicate-block-name",
280
+ file: relativePath,
281
+ line: index + 1,
282
+ message: `Semantic block name \`${name}\` is duplicated in this file.`,
283
+ });
284
+ }
285
+
286
+ seen.add(name);
287
+ }
288
+
289
+ stack.push({ name, line: index + 1 });
290
+ }
291
+
292
+ if (endMatch?.[1]) {
293
+ const name = endMatch[1];
294
+ const active = stack[stack.length - 1];
295
+
296
+ if (!active) {
297
+ addIssue(result, {
298
+ severity: "error",
299
+ code: kind === "block" ? "markup.unmatched-block-end" : "markup.unmatched-contract-end",
300
+ file: relativePath,
301
+ line: index + 1,
302
+ message: `Found an unmatched END marker for \`${name}\`.`,
303
+ });
304
+ continue;
305
+ }
306
+
307
+ if (active.name !== name) {
308
+ addIssue(result, {
309
+ severity: "error",
310
+ code: kind === "block" ? "markup.mismatched-block-end" : "markup.mismatched-contract-end",
311
+ file: relativePath,
312
+ line: index + 1,
313
+ message: `Expected END marker for \`${active.name}\`, found \`${name}\` instead.`,
314
+ });
315
+ continue;
316
+ }
317
+
318
+ stack.pop();
319
+ }
320
+ }
321
+
322
+ for (const active of stack) {
323
+ addIssue(result, {
324
+ severity: "error",
325
+ code: kind === "block" ? "markup.missing-block-end" : "markup.missing-contract-end",
326
+ file: relativePath,
327
+ line: active.line,
328
+ message: `Missing END marker for \`${active.name}\`.`,
329
+ });
330
+ }
331
+ }
332
+
333
+ function parseModuleMapEntries(section: string) {
334
+ const entries = new Set<string>();
335
+ const lines = section.split("\n");
336
+
337
+ for (const line of lines) {
338
+ const cleaned = line.replace(/^\s*(\/\/|#|--|\*)?\s*/, "").trim();
339
+ if (!cleaned) {
340
+ continue;
341
+ }
342
+
343
+ const match = cleaned.match(/^([A-Za-z_$][\w$]*)\s+-\s+/);
344
+ if (match?.[1]) {
345
+ entries.add(match[1]);
346
+ }
347
+ }
348
+
349
+ return entries;
350
+ }
351
+
352
+ function extractTypeScriptExports(text: string) {
353
+ const exports = new Set<string>();
354
+ const directPatterns = [
355
+ /^\s*export\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/gm,
356
+ /^\s*export\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)/gm,
357
+ /^\s*export\s+class\s+([A-Za-z_$][\w$]*)/gm,
358
+ /^\s*export\s+(?:interface|type|enum)\s+([A-Za-z_$][\w$]*)/gm,
359
+ ];
360
+
361
+ for (const pattern of directPatterns) {
362
+ for (const match of text.matchAll(pattern)) {
363
+ if (match[1]) {
364
+ exports.add(match[1]);
365
+ }
366
+ }
367
+ }
368
+
369
+ for (const match of text.matchAll(/^\s*export\s*\{([^}]+)\}/gm)) {
370
+ const names = match[1]
371
+ .split(",")
372
+ .map((part) => part.trim())
373
+ .filter(Boolean);
374
+
375
+ for (const name of names) {
376
+ const aliasMatch = name.match(/^(?:type\s+)?([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/);
377
+ if (!aliasMatch) {
378
+ continue;
379
+ }
380
+
381
+ exports.add(aliasMatch[2] ?? aliasMatch[1]);
382
+ }
383
+ }
384
+
385
+ return exports;
386
+ }
387
+
388
+ function lintGovernedFile(result: LintResult, root: string, filePath: string, text: string) {
389
+ const relativePath = normalizeRelative(root, filePath);
390
+ result.governedFiles += 1;
391
+
392
+ const moduleContract = ensureSectionPair(
393
+ result,
394
+ root,
395
+ relativePath,
396
+ text,
397
+ "START_MODULE_CONTRACT",
398
+ "END_MODULE_CONTRACT",
399
+ "markup.missing-module-contract",
400
+ "Governed files must include a paired MODULE_CONTRACT section.",
401
+ );
402
+ const moduleMap = ensureSectionPair(
403
+ result,
404
+ root,
405
+ relativePath,
406
+ text,
407
+ "START_MODULE_MAP",
408
+ "END_MODULE_MAP",
409
+ "markup.missing-module-map",
410
+ "Governed files must include a paired MODULE_MAP section.",
411
+ );
412
+ const changeSummary = ensureSectionPair(
413
+ result,
414
+ root,
415
+ relativePath,
416
+ text,
417
+ "START_CHANGE_SUMMARY",
418
+ "END_CHANGE_SUMMARY",
419
+ "markup.missing-change-summary",
420
+ "Governed files must include a paired CHANGE_SUMMARY section.",
421
+ );
422
+
423
+ lintScopedMarkers(
424
+ result,
425
+ relativePath,
426
+ text,
427
+ /START_CONTRACT:\s*([A-Za-z0-9_$.\-]+)/,
428
+ /END_CONTRACT:\s*([A-Za-z0-9_$.\-]+)/,
429
+ "contract",
430
+ );
431
+ lintScopedMarkers(
432
+ result,
433
+ relativePath,
434
+ text,
435
+ /START_BLOCK_([A-Za-z0-9_]+)/,
436
+ /END_BLOCK_([A-Za-z0-9_]+)/,
437
+ "block",
438
+ );
439
+
440
+ if (moduleContract && !/PURPOSE:|SCOPE:|DEPENDS:|LINKS:/s.test(moduleContract)) {
441
+ addIssue(result, {
442
+ severity: "error",
443
+ code: "markup.incomplete-module-contract",
444
+ file: relativePath,
445
+ message: "MODULE_CONTRACT should include PURPOSE, SCOPE, DEPENDS, and LINKS fields.",
446
+ });
447
+ }
448
+
449
+ const moduleMapEntries = moduleMap ? parseModuleMapEntries(moduleMap) : new Set<string>();
450
+ if (moduleMap && moduleMapEntries.size === 0) {
451
+ addIssue(result, {
452
+ severity: "error",
453
+ code: "markup.empty-module-map",
454
+ file: relativePath,
455
+ message: "MODULE_MAP must list at least one exported symbol and description.",
456
+ });
457
+ }
458
+
459
+ if (changeSummary && !/LAST_CHANGE:/s.test(changeSummary)) {
460
+ addIssue(result, {
461
+ severity: "error",
462
+ code: "markup.empty-change-summary",
463
+ file: relativePath,
464
+ message: "CHANGE_SUMMARY must contain at least one LAST_CHANGE entry.",
465
+ });
466
+ }
467
+
468
+ if (TS_LIKE_EXTENSIONS.has(path.extname(filePath))) {
469
+ const actualExports = extractTypeScriptExports(text);
470
+ for (const exportName of actualExports) {
471
+ if (!moduleMapEntries.has(exportName)) {
472
+ addIssue(result, {
473
+ severity: "error",
474
+ code: "markup.module-map-missing-export",
475
+ file: relativePath,
476
+ message: `MODULE_MAP is missing the exported symbol \`${exportName}\`.`,
477
+ });
478
+ }
479
+ }
480
+
481
+ for (const mapEntry of moduleMapEntries) {
482
+ if (!actualExports.has(mapEntry)) {
483
+ addIssue(result, {
484
+ severity: "warning",
485
+ code: "markup.module-map-extra-export",
486
+ file: relativePath,
487
+ message: `MODULE_MAP lists \`${mapEntry}\`, but no matching TypeScript export was found.`,
488
+ });
489
+ }
490
+ }
491
+ }
492
+ }
493
+
494
+ function lintUniqueTags(result: LintResult, relativePath: string, text: string) {
495
+ for (const antiPattern of UNIQUE_TAG_ANTI_PATTERNS) {
496
+ for (const match of text.matchAll(antiPattern.regex)) {
497
+ addIssue(result, {
498
+ severity: "error",
499
+ code: antiPattern.code,
500
+ file: relativePath,
501
+ line: match.index === undefined ? undefined : lineNumberAt(text, match.index),
502
+ message: antiPattern.message,
503
+ });
504
+ }
505
+ }
506
+ }
507
+
508
+ function extractModuleIds(text: string) {
509
+ return new Set(
510
+ Array.from(text.matchAll(/<(M-[A-Za-z0-9-]+)(?=[\s>])/g), (match) => match[1]),
511
+ );
512
+ }
513
+
514
+ function extractVerificationIds(text: string) {
515
+ return new Set(
516
+ Array.from(text.matchAll(/<(V-M-[A-Za-z0-9-]+)(?=[\s>])/g), (match) => match[1]),
517
+ );
518
+ }
519
+
520
+ function extractVerificationRefs(text: string) {
521
+ return Array.from(text.matchAll(/<verification-ref>\s*([^<\s]+)\s*<\/verification-ref>/g)).map((match) => ({
522
+ value: match[1],
523
+ line: match.index === undefined ? undefined : lineNumberAt(text, match.index),
524
+ }));
525
+ }
526
+
527
+ function extractStepRefs(text: string) {
528
+ return Array.from(
529
+ text.matchAll(/<(step-[A-Za-z0-9-]+)([^>]*)>/g),
530
+ (match) => {
531
+ const attrs = match[2] ?? "";
532
+ const moduleMatch = attrs.match(/module="([^"]+)"/);
533
+ const verificationMatch = attrs.match(/verification="([^"]+)"/);
534
+ return {
535
+ stepTag: match[1],
536
+ moduleId: moduleMatch?.[1] ?? null,
537
+ verificationId: verificationMatch?.[1] ?? null,
538
+ line: match.index === undefined ? undefined : lineNumberAt(text, match.index),
539
+ };
540
+ },
541
+ );
542
+ }
543
+
544
+ function lintRequiredPacketSections(result: LintResult, relativePath: string, text: string) {
545
+ const requiredTags = [
546
+ "ExecutionPacketTemplate",
547
+ "GraphDeltaTemplate",
548
+ "VerificationDeltaTemplate",
549
+ "FailurePacketTemplate",
550
+ ];
551
+
552
+ for (const tagName of requiredTags) {
553
+ const pattern = new RegExp(`<${tagName}(?=[\\s>])`);
554
+ if (!pattern.test(text)) {
555
+ addIssue(result, {
556
+ severity: "error",
557
+ code: "packets.missing-template-section",
558
+ file: relativePath,
559
+ message: `Operational packet reference is missing <${tagName}>.`,
560
+ });
561
+ }
562
+ }
563
+ }
564
+
565
+ export function lintGraceProject(projectRoot: string, options: LintOptions = {}): LintResult {
566
+ const root = path.resolve(projectRoot);
567
+ const result: LintResult = {
568
+ root,
569
+ filesChecked: 0,
570
+ governedFiles: 0,
571
+ xmlFilesChecked: 0,
572
+ issues: [],
573
+ };
574
+
575
+ const docs = Object.fromEntries(
576
+ REQUIRED_DOCS.map((relativePath) => [relativePath, readTextIfExists(path.join(root, relativePath))]),
577
+ ) as Record<(typeof REQUIRED_DOCS)[number], string | null>;
578
+ const operationalPackets = readTextIfExists(path.join(root, OPTIONAL_PACKET_DOC));
579
+
580
+ if (!options.allowMissingDocs) {
581
+ for (const relativePath of REQUIRED_DOCS) {
582
+ if (!docs[relativePath]) {
583
+ addIssue(result, {
584
+ severity: "error",
585
+ code: "docs.missing-required-artifact",
586
+ file: relativePath,
587
+ message: `Missing required GRACE artifact \`${relativePath}\`.`,
588
+ });
589
+ }
590
+ }
591
+ }
592
+
593
+ for (const [relativePath, contents] of Object.entries(docs)) {
594
+ if (!contents) {
595
+ continue;
596
+ }
597
+
598
+ result.xmlFilesChecked += 1;
599
+ lintUniqueTags(result, relativePath, contents);
600
+ }
601
+
602
+ if (operationalPackets) {
603
+ result.xmlFilesChecked += 1;
604
+ lintRequiredPacketSections(result, OPTIONAL_PACKET_DOC, operationalPackets);
605
+ }
606
+
607
+ const knowledgeGraph = docs["docs/knowledge-graph.xml"];
608
+ const developmentPlan = docs["docs/development-plan.xml"];
609
+ const verificationPlan = docs["docs/verification-plan.xml"];
610
+
611
+ const graphModuleIds = knowledgeGraph ? extractModuleIds(knowledgeGraph) : new Set<string>();
612
+ const planModuleIds = developmentPlan ? extractModuleIds(developmentPlan) : new Set<string>();
613
+ const verificationIds = verificationPlan ? extractVerificationIds(verificationPlan) : new Set<string>();
614
+
615
+ if (knowledgeGraph && verificationPlan) {
616
+ for (const ref of extractVerificationRefs(knowledgeGraph)) {
617
+ if (!verificationIds.has(ref.value)) {
618
+ addIssue(result, {
619
+ severity: "error",
620
+ code: "graph.missing-verification-entry",
621
+ file: "docs/knowledge-graph.xml",
622
+ line: ref.line,
623
+ message: `Knowledge graph references \`${ref.value}\`, but no matching verification entry exists.`,
624
+ });
625
+ }
626
+ }
627
+ }
628
+
629
+ if (developmentPlan && verificationPlan) {
630
+ for (const ref of extractVerificationRefs(developmentPlan)) {
631
+ if (!verificationIds.has(ref.value)) {
632
+ addIssue(result, {
633
+ severity: "error",
634
+ code: "plan.missing-verification-entry",
635
+ file: "docs/development-plan.xml",
636
+ line: ref.line,
637
+ message: `Development plan references \`${ref.value}\`, but no matching verification entry exists.`,
638
+ });
639
+ }
640
+ }
641
+
642
+ for (const step of extractStepRefs(developmentPlan)) {
643
+ if (step.moduleId && !planModuleIds.has(step.moduleId)) {
644
+ addIssue(result, {
645
+ severity: "error",
646
+ code: "plan.step-missing-module",
647
+ file: "docs/development-plan.xml",
648
+ line: step.line,
649
+ message: `${step.stepTag} references module \`${step.moduleId}\`, but no matching module tag exists in the plan.`,
650
+ });
651
+ }
652
+
653
+ if (step.verificationId && !verificationIds.has(step.verificationId)) {
654
+ addIssue(result, {
655
+ severity: "error",
656
+ code: "plan.step-missing-verification",
657
+ file: "docs/development-plan.xml",
658
+ line: step.line,
659
+ message: `${step.stepTag} references verification entry \`${step.verificationId}\`, but no matching tag exists in verification-plan.xml.`,
660
+ });
661
+ }
662
+ }
663
+ }
664
+
665
+ if (knowledgeGraph && developmentPlan) {
666
+ for (const moduleId of graphModuleIds) {
667
+ if (!planModuleIds.has(moduleId)) {
668
+ addIssue(result, {
669
+ severity: "error",
670
+ code: "graph.module-missing-from-plan",
671
+ file: "docs/knowledge-graph.xml",
672
+ message: `Module \`${moduleId}\` exists in the knowledge graph but not in the development plan.`,
673
+ });
674
+ }
675
+ }
676
+
677
+ for (const moduleId of planModuleIds) {
678
+ if (!graphModuleIds.has(moduleId)) {
679
+ addIssue(result, {
680
+ severity: "error",
681
+ code: "plan.module-missing-from-graph",
682
+ file: "docs/development-plan.xml",
683
+ message: `Module \`${moduleId}\` exists in the development plan but not in the knowledge graph.`,
684
+ });
685
+ }
686
+ }
687
+ }
688
+
689
+ for (const filePath of collectCodeFiles(root)) {
690
+ result.filesChecked += 1;
691
+ const text = readFileSync(filePath, "utf8");
692
+ if (!hasGraceMarkers(text)) {
693
+ continue;
694
+ }
695
+
696
+ lintGovernedFile(result, root, filePath, text);
697
+ }
698
+
699
+ return result;
700
+ }
701
+
702
+ export function formatTextReport(result: LintResult) {
703
+ const errors = result.issues.filter((issue) => issue.severity === "error");
704
+ const warnings = result.issues.filter((issue) => issue.severity === "warning");
705
+ const lines = [
706
+ "GRACE Lint Report",
707
+ "=================",
708
+ `Root: ${result.root}`,
709
+ `Code files checked: ${result.filesChecked}`,
710
+ `Governed files checked: ${result.governedFiles}`,
711
+ `XML files checked: ${result.xmlFilesChecked}`,
712
+ `Issues: ${result.issues.length} (errors: ${errors.length}, warnings: ${warnings.length})`,
713
+ ];
714
+
715
+ if (errors.length > 0) {
716
+ lines.push("", "Errors:");
717
+ for (const issue of errors) {
718
+ lines.push(`- [${issue.code}] ${issue.file}${issue.line ? `:${issue.line}` : ""} ${issue.message}`);
719
+ }
720
+ }
721
+
722
+ if (warnings.length > 0) {
723
+ lines.push("", "Warnings:");
724
+ for (const issue of warnings) {
725
+ lines.push(`- [${issue.code}] ${issue.file}${issue.line ? `:${issue.line}` : ""} ${issue.message}`);
726
+ }
727
+ }
728
+
729
+ if (result.issues.length === 0) {
730
+ lines.push("", "No GRACE integrity issues found.");
731
+ }
732
+
733
+ return lines.join("\n");
734
+ }
735
+
736
+ export const lintCommand = defineCommand({
737
+ meta: {
738
+ name: "lint",
739
+ description: "Lint GRACE artifacts, XML tag conventions, and semantic markup.",
740
+ },
741
+ args: {
742
+ path: {
743
+ type: "string",
744
+ alias: "p",
745
+ description: "Project root to lint",
746
+ default: ".",
747
+ },
748
+ format: {
749
+ type: "string",
750
+ alias: "f",
751
+ description: "Output format: text or json",
752
+ default: "text",
753
+ },
754
+ allowMissingDocs: {
755
+ type: "boolean",
756
+ description: "Allow repositories that do not yet have full GRACE docs",
757
+ default: false,
758
+ },
759
+ },
760
+ async run(context) {
761
+ const format = String(context.args.format ?? "text");
762
+ if (!TEXT_FORMAT_OPTIONS.has(format)) {
763
+ throw new Error(`Unsupported format \`${format}\`. Use \`text\` or \`json\`.`);
764
+ }
765
+
766
+ const result = lintGraceProject(String(context.args.path ?? "."), {
767
+ allowMissingDocs: Boolean(context.args.allowMissingDocs),
768
+ });
769
+
770
+ if (format === "json") {
771
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
772
+ } else {
773
+ process.stdout.write(`${formatTextReport(result)}\n`);
774
+ }
775
+
776
+ process.exitCode = result.issues.some((issue) => issue.severity === "error") ? 1 : 0;
777
+ },
778
+ });
779
+
780
+ if (import.meta.main) {
781
+ await runMain(lintCommand as CommandDef);
782
+ }
package/src/grace.ts ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { defineCommand, type CommandDef, runMain } from "citty";
4
+
5
+ import { lintCommand } from "./grace-lint";
6
+
7
+ const main = defineCommand({
8
+ meta: {
9
+ name: "grace",
10
+ version: "3.1.0",
11
+ description: "GRACE CLI for linting semantic markup and GRACE project artifacts.",
12
+ },
13
+ subCommands: {
14
+ lint: lintCommand,
15
+ },
16
+ });
17
+
18
+ if (import.meta.main) {
19
+ await runMain(main as CommandDef);
20
+ }