Skip to content

Commit eaa990c

Browse files
committed
feat(cli): add orgscript check combined quality command
1 parent 4268988 commit eaa990c

5 files changed

Lines changed: 207 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ The format is based on Keep a Changelog, and this project follows Semantic Versi
88

99
- Added `orgscript format <file> --check` for canonical formatting checks without rewriting files.
1010
- Added `npm run format:check:all` and integrated formatting checks into CI.
11+
- Added `orgscript check <file>` as the combined quality command for validation, linting, and formatting checks.
1112

1213
## [0.3.0] - 2026-03-29
1314

README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ See the full example in [`examples/craft-business-lead-to-order.orgs`](examples/
145145
- AST-backed formatting: `orgscript format <file>`
146146
- Canonical format checks: `orgscript format <file> --check`
147147
- AST-backed linting: `orgscript lint <file>`
148+
- Combined quality checks: `orgscript check <file>`
148149
- Canonical JSON export: `orgscript export json <file>`
149150
- Machine-readable diagnostics: `orgscript validate <file> --json`, `orgscript lint <file> --json`
150151
- Golden snapshot tests for AST, canonical model, and formatter output
@@ -167,6 +168,7 @@ Exit codes are CI-friendly:
167168

168169
- `validate` returns `0` for valid files and `1` for invalid files.
169170
- `lint` returns `0` when findings contain only `warning` and `info`, and `1` when findings contain at least one `error`.
171+
- `check` returns `0` only when validation passes, lint has no `error`, and formatting is canonical. Warnings and info findings alone do not fail `check`.
170172

171173
## Guides
172174

@@ -176,11 +178,11 @@ Exit codes are CI-friendly:
176178

177179
## Near-term plan
178180

179-
1. Add `format --check` for CI and pre-commit workflows.
180-
2. Show real JSON diagnostics examples in the README and diagnostics spec.
181-
3. Add `orgscript check` as a combined quality command.
182-
4. Improve diagnostics consistency across CLI commands.
183-
5. Add an initial VS Code syntax highlighting scaffold.
181+
1. Show real JSON diagnostics examples in the README and diagnostics spec.
182+
2. Improve diagnostics consistency across CLI commands.
183+
3. Add `orgscript check --json` for machine-readable combined quality output.
184+
4. Add an initial VS Code syntax highlighting scaffold.
185+
5. Add a first editor integration path that contributors can install locally.
184186

185187
See [`docs/roadmaps/v0.4.0.md`](docs/roadmaps/v0.4.0.md) for the current milestone plan.
186188

@@ -196,8 +198,11 @@ orgscript format file.orgs --check
196198
orgscript lint file.orgs
197199
orgscript lint file.orgs --json
198200
orgscript export json file.orgs
201+
orgscript check file.orgs
199202
```
200203

204+
`orgscript check` runs `validate`, `lint`, and `format --check` in that order and fails on validation errors, lint errors, or formatting drift. Warnings and info findings alone do not fail the command.
205+
201206
See [`docs/cli-v0.1-plan.md`](docs/cli-v0.1-plan.md) for the implementation plan.
202207

203208
## Testing

docs/cli-v0.1-plan.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Implemented:
1717
- `export json`
1818
- `validate --json`
1919
- `lint --json`
20+
- `check`
2021

2122
Planned next:
2223

@@ -76,6 +77,26 @@ Output model:
7677
- one stable line per finding using severity, code, line, and message
7778
- optional machine-readable diagnostics via `--json`
7879

80+
### `orgscript check <file>`
81+
82+
Runs the quality pipeline in one command:
83+
84+
- `validate`
85+
- `lint`
86+
- `format --check`
87+
88+
Output:
89+
90+
- compact pass/fail summary for each stage
91+
- clear indication of which stage failed
92+
- stable text output suitable for CI logs
93+
94+
Exit behavior:
95+
96+
- `0` when validation passes, lint has no `error`, and formatting is canonical
97+
- `1` when validation fails, lint finds at least one `error`, or formatting drift is detected
98+
- warnings and info findings alone do not fail the command
99+
79100
Initial lint rules:
80101

81102
- duplicate state names
@@ -126,6 +147,7 @@ TypeScript is the fastest start:
126147
- One can run `orgscript validate --json` and `orgscript lint --json` for downstream tooling.
127148
- One can run `orgscript format` on example files without changing canonical files.
128149
- One can run `orgscript format --check` in CI or pre-commit workflows.
150+
- One can run `orgscript check` to combine validation, linting, and format checks.
129151
- One can run `orgscript lint` on valid but suspicious models and get stable findings.
130152
- Lint exit codes are safe for CI use with advisory warnings.
131153
- Invalid files produce useful errors with line references.

src/cli.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ function printUsage() {
1515
1616
Usage:
1717
orgscript validate <file> [--json]
18+
orgscript check <file>
1819
orgscript format <file> [--check]
1920
orgscript lint <file> [--json]
2021
orgscript export json <file>
@@ -51,6 +52,15 @@ function run(args) {
5152
process.exit(0);
5253
}
5354

55+
if (command === "check") {
56+
const absolutePath = resolveFile("check", maybeSubcommand);
57+
const result = runCheck(absolutePath);
58+
const lines = renderCheckReport(absolutePath, result);
59+
const stream = result.ok ? console.log : console.error;
60+
stream(lines.join("\n"));
61+
process.exit(result.ok ? 0 : 1);
62+
}
63+
5464
if (command === "export" && maybeSubcommand === "json") {
5565
const absolutePath = resolveFile("export", maybeFile);
5666
const result = buildModel(absolutePath);
@@ -204,4 +214,93 @@ function printIssues(header, issues) {
204214
}
205215
}
206216

217+
function runCheck(filePath) {
218+
const validation = validateFile(filePath);
219+
220+
if (!validation.ok) {
221+
return {
222+
ok: false,
223+
validate: {
224+
ok: false,
225+
issues: validation.issues,
226+
},
227+
lint: {
228+
ok: false,
229+
skipped: true,
230+
findings: [],
231+
summary: { error: 0, warning: 0, info: 0 },
232+
},
233+
format: {
234+
ok: false,
235+
skipped: true,
236+
requiresChanges: false,
237+
},
238+
};
239+
}
240+
241+
const findings = lintDocument(validation.ast);
242+
const lintSummary = summarizeFindings(findings);
243+
const current = fs.readFileSync(filePath, "utf8");
244+
const formatted = formatDocument(validation.ast);
245+
const requiresChanges = current !== formatted;
246+
const lintOk = lintSummary.error === 0;
247+
const formatOk = !requiresChanges;
248+
249+
return {
250+
ok: lintOk && formatOk,
251+
validate: {
252+
ok: true,
253+
issues: [],
254+
},
255+
lint: {
256+
ok: lintOk,
257+
skipped: false,
258+
findings,
259+
summary: lintSummary,
260+
},
261+
format: {
262+
ok: formatOk,
263+
skipped: false,
264+
requiresChanges,
265+
},
266+
};
267+
}
268+
269+
function renderCheckReport(filePath, result) {
270+
const lines = [`CHECK ${toDisplayPath(filePath)}`];
271+
272+
if (result.validate.ok) {
273+
lines.push(" validate: ok");
274+
} else {
275+
lines.push(" validate: failed");
276+
for (const issue of result.validate.issues) {
277+
lines.push(` line ${issue.line}: ${issue.message}`);
278+
}
279+
}
280+
281+
if (result.lint.skipped) {
282+
lines.push(" lint: skipped");
283+
} else {
284+
const { error, warning, info } = result.lint.summary;
285+
const status = result.lint.ok ? "ok" : "failed";
286+
lines.push(` lint: ${status} (${error} error(s), ${warning} warning(s), ${info} info)`);
287+
}
288+
289+
if (result.format.skipped) {
290+
lines.push(" format: skipped");
291+
} else if (result.format.ok) {
292+
lines.push(" format: ok");
293+
} else {
294+
lines.push(" format: failed (canonical formatting changes required)");
295+
}
296+
297+
lines.push(`Result: ${result.ok ? "PASS" : "FAIL"}`);
298+
return lines;
299+
}
300+
301+
function toDisplayPath(filePath) {
302+
const relative = path.relative(process.cwd(), filePath).replace(/\\/g, "/");
303+
return relative || path.basename(filePath);
304+
}
305+
207306
module.exports = { run };

tests/run.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ function run() {
2525
testFormatterStability();
2626
testCliDiagnosticsAndExitCodes();
2727
testFormatCheckMode();
28+
testCheckCommand();
2829
console.log("All tests passed.");
2930
}
3031

@@ -318,6 +319,80 @@ function testFormatCheckMode() {
318319
);
319320
}
320321

322+
function testCheckCommand() {
323+
const cliPath = path.join(repoRoot, "bin", "orgscript.js");
324+
const canonicalSourcePath = path.join(examplesDir, "craft-business-lead-to-order.orgs");
325+
const canonicalSource = fs.readFileSync(canonicalSourcePath, "utf8");
326+
327+
const checkOk = runCli([cliPath, "check", "./examples/craft-business-lead-to-order.orgs"]);
328+
assert.strictEqual(checkOk.status, 0, "Expected check to pass for canonical example");
329+
assert.ok(
330+
checkOk.stdout.includes("CHECK examples/craft-business-lead-to-order.orgs"),
331+
"Expected check header in check output"
332+
);
333+
assert.ok(checkOk.stdout.includes("validate: ok"), "Expected validate pass in check output");
334+
assert.ok(checkOk.stdout.includes("lint: ok (0 error(s), 0 warning(s), 0 info)"), "Expected lint pass in check output");
335+
assert.ok(checkOk.stdout.includes("format: ok"), "Expected format pass in check output");
336+
assert.ok(checkOk.stdout.includes("Result: PASS"), "Expected passing summary in check output");
337+
338+
const checkWarning = runCli([cliPath, "check", "./tests/lint/process-missing-trigger.orgs"]);
339+
assert.strictEqual(checkWarning.status, 0, "Expected warning-only check to stay non-failing");
340+
assert.ok(
341+
checkWarning.stdout.includes("lint: ok (0 error(s), 1 warning(s), 0 info)"),
342+
"Expected warning summary in check output"
343+
);
344+
assert.ok(
345+
checkWarning.stdout.includes("Result: PASS"),
346+
"Expected overall pass for warning-only check"
347+
);
348+
349+
const checkLintError = runCli([cliPath, "check", "./tests/lint/process-multiple-triggers.orgs"]);
350+
assert.strictEqual(checkLintError.status, 1, "Expected check to fail on lint errors");
351+
assert.ok(
352+
checkLintError.stderr.includes("lint: failed"),
353+
"Expected lint failure in check output"
354+
);
355+
assert.ok(
356+
checkLintError.stderr.includes("Result: FAIL"),
357+
"Expected failed summary for lint-error check"
358+
);
359+
360+
const nonCanonicalPath = path.join(repoRoot, "tests", ".tmp-check-noncanonical.orgs");
361+
const nonCanonicalSource = canonicalSource.replace("\n\n when lead.created", "\n when lead.created");
362+
fs.writeFileSync(nonCanonicalPath, nonCanonicalSource, "utf8");
363+
364+
try {
365+
const checkFormatFailure = runCli([cliPath, "check", "./tests/.tmp-check-noncanonical.orgs"]);
366+
assert.strictEqual(checkFormatFailure.status, 1, "Expected check to fail on format drift");
367+
assert.ok(
368+
checkFormatFailure.stderr.includes("format: failed"),
369+
"Expected format failure in check output"
370+
);
371+
assert.strictEqual(
372+
fs.readFileSync(nonCanonicalPath, "utf8"),
373+
nonCanonicalSource,
374+
"Expected check to leave non-canonical file unchanged"
375+
);
376+
} finally {
377+
if (fs.existsSync(nonCanonicalPath)) {
378+
fs.unlinkSync(nonCanonicalPath);
379+
}
380+
}
381+
382+
const checkInvalid = runCli([cliPath, "check", "./tests/invalid/unknown-top-level.orgs"]);
383+
assert.strictEqual(checkInvalid.status, 1, "Expected check to fail for invalid file");
384+
assert.ok(
385+
checkInvalid.stderr.includes("validate: failed"),
386+
"Expected validate failure in check output"
387+
);
388+
assert.ok(checkInvalid.stderr.includes("lint: skipped"), "Expected skipped lint in check output");
389+
assert.ok(
390+
checkInvalid.stderr.includes("format: skipped"),
391+
"Expected skipped format in check output"
392+
);
393+
assert.ok(checkInvalid.stderr.includes("Result: FAIL"), "Expected failed summary for invalid file");
394+
}
395+
321396
function runCli(args) {
322397
const result = spawnSync(process.execPath, args, {
323398
cwd: repoRoot,

0 commit comments

Comments
 (0)