@jterrazz/typescript 9.2.1 → 10.0.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.
Files changed (71) hide show
  1. package/README.md +20 -16
  2. package/bin/commands/check.sh +348 -117
  3. package/bin/typescript.sh +55 -0
  4. package/lib/check-architecture.js +89 -0
  5. package/lib/check-baseline.js +144 -0
  6. package/lib/check-docs.js +4 -3
  7. package/lib/check-drift.js +209 -0
  8. package/lib/check-gitignore.js +4 -4
  9. package/lib/check-markdown.js +279 -0
  10. package/lib/check-names.js +125 -0
  11. package/lib/check-publish.js +150 -0
  12. package/lib/check-secrets.js +115 -0
  13. package/lib/check-suppressions.js +355 -0
  14. package/lib/doctor.js +185 -0
  15. package/lib/merge-knip-config.js +57 -25
  16. package/lib/tracked-files.js +165 -0
  17. package/lib/workspace-members.js +5 -6
  18. package/package.json +19 -8
  19. package/presets/oxfmt/index.js +49 -5
  20. package/presets/oxlint/profiles/astro.js +10 -0
  21. package/presets/oxlint/profiles/bun.js +7 -0
  22. package/presets/oxlint/profiles/expo.js +7 -0
  23. package/presets/oxlint/profiles/library.js +16 -0
  24. package/presets/oxlint/profiles/next.js +7 -0
  25. package/presets/oxlint/profiles/node.js +7 -0
  26. package/presets/prettier/astro.json +6 -0
  27. package/presets/tsconfig/expo.json +16 -6
  28. package/presets/tsconfig/library.json +18 -0
  29. package/presets/tsconfig/next.json +12 -2
  30. package/presets/tsconfig/node.json +18 -4
  31. package/rules/README.md +23 -0
  32. package/rules/_contract.js +191 -0
  33. package/rules/_contract.test.ts +81 -0
  34. package/rules/a11y.js +51 -0
  35. package/rules/architecture/hexagonal.js +56 -0
  36. package/rules/architecture/layers.js +75 -0
  37. package/rules/astro.js +49 -0
  38. package/rules/catalog.js +134 -0
  39. package/rules/catalog.test.ts +84 -0
  40. package/rules/compile.js +125 -0
  41. package/rules/core/eslint.js +234 -0
  42. package/rules/core/import.js +107 -0
  43. package/rules/core/jsdoc.js +52 -0
  44. package/rules/core/node.js +36 -0
  45. package/rules/core/oxc.js +54 -0
  46. package/rules/core/promise.js +39 -0
  47. package/rules/core/typescript.js +204 -0
  48. package/rules/core/unicorn.js +200 -0
  49. package/rules/next.js +53 -0
  50. package/rules/profiles.js +89 -0
  51. package/rules/react-native.js +48 -0
  52. package/rules/react.js +148 -0
  53. package/rules/sorted.js +41 -0
  54. package/rules/vitest.js +153 -0
  55. package/src/docs.d.ts +4 -4
  56. package/src/docs.js +75 -47
  57. package/src/docs.test.ts +136 -29
  58. package/src/index.d.ts +13 -9
  59. package/src/index.js +15 -8
  60. package/src/oxfmt.d.ts +15 -2
  61. package/src/oxfmt.test.ts +10 -0
  62. package/src/oxlint.d.ts +57 -10
  63. package/src/oxlint.js +35 -50
  64. package/src/oxlint.test.ts +82 -28
  65. package/presets/oxlint/architectures/hexagonal-rules.js +0 -39
  66. package/presets/oxlint/architectures/hexagonal.js +0 -13
  67. package/presets/oxlint/base.js +0 -145
  68. package/presets/oxlint/expo.js +0 -36
  69. package/presets/oxlint/next.js +0 -43
  70. package/presets/oxlint/node.js +0 -14
  71. package/presets/oxlint/plugins/codestyle.js +0 -231
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @jterrazz/typescript
2
2
 
3
- The complete TypeScript toolchain — build, run, check, and document with zero configuration. Powered by tsdown, Oxlint, Oxfmt, TypeScript 7, and Knip.
3
+ The complete TypeScript toolchain — build, run, check, and document with zero configuration. Powered by tsdown, Oxlint, Oxfmt, TypeScript 7, and Knip. Six profiles, one rulebook: every rule of every plugin it loads is decided by name.
4
4
 
5
5
  ## Installation
6
6
 
@@ -17,8 +17,10 @@ npx typescript start # Run the built application
17
17
  npx typescript dev # Build, run, and rebuild on changes
18
18
  npx typescript docs # Compile the committed docs/reference tree from source
19
19
  npx typescript docs-layout . # Check a repository's docs/ against the manual spine
20
- npx typescript check # Type-check, lint, format-check, and unused-code in parallel
21
- npx typescript fix # Auto-fix lint and formatting issues
20
+ npx typescript check # Every quality pass, in parallel, ending on a drift report
21
+ npx typescript fix # Auto-fix lint, formatting, the .gitignore and suppression spellings
22
+ npx typescript doctor # Installed tool versions against the ranges this release declares
23
+ npx typescript baseline # Record today's oxlint diagnostics, so the count may only fall
22
24
  npx typescript clean # Remove .artifacts/ (dist/ stays — it is the product)
23
25
  ```
24
26
 
@@ -26,16 +28,18 @@ npx typescript clean # Remove .artifacts/ (dist/ stays — it is the p
26
28
 
27
29
  Fully compiled — no JavaScript in the hot path:
28
30
 
29
- | Step | Tool | Language |
30
- | ------------ | ---------------------------------------------------------------- | -------- |
31
- | Transpile | [Oxc](https://oxc.rs) (via tsdown) | Rust |
32
- | Bundle | [Rolldown](https://rolldown.rs) | Rust |
33
- | Declarations | [tsdown](https://tsdown.dev) built-in | Rust |
34
- | Type check | [tsc (TypeScript 7)](https://github.com/microsoft/typescript-go) | Go |
35
- | Lint | [Oxlint](https://oxc.rs/docs/guide/usage/linter) | Rust |
36
- | Format | [Oxfmt](https://oxc.rs/docs/guide/usage/formatter) | Rust |
37
- | Unused code | [Knip](https://knip.dev) | Node |
38
- | API docs | [Typedoc](https://typedoc.org) | Node |
31
+ | Step | Tool | Language |
32
+ | ------------ | --------------------------------------------------------------------------- | -------- |
33
+ | Transpile | [Oxc](https://oxc.rs) (via tsdown) | Rust |
34
+ | Bundle | [Rolldown](https://rolldown.rs) | Rust |
35
+ | Declarations | [tsdown](https://tsdown.dev) built-in | Rust |
36
+ | Type check | [tsc (TypeScript 7)](https://github.com/microsoft/typescript-go) | Go |
37
+ | Lint | [Oxlint](https://oxc.rs/docs/guide/usage/linter) | Rust |
38
+ | Format | [Oxfmt](https://oxc.rs/docs/guide/usage/formatter) | Rust |
39
+ | Unused code | [Knip](https://knip.dev) | Node |
40
+ | Packaging | [publint](https://publint.dev) + [attw](https://arethetypeswrong.github.io) | Node |
41
+ | Layer map | [dependency-cruiser](https://github.com/sverweij/dependency-cruiser) | Node |
42
+ | API docs | [Typedoc](https://typedoc.org) | Node |
39
43
 
40
44
  ## Documentation
41
45
 
@@ -46,12 +50,12 @@ The full corpus lives in [`docs/`](docs/):
46
50
  - [Testing](docs/03-testing.md) — how this toolchain proves itself.
47
51
  - [Operating](docs/04-operating.md) — what publishes it, and which number moves.
48
52
  - [Building](docs/05-building.md) — `build`, `bundle`, `start`, `dev`.
49
- - [Quality checks](docs/06-quality-checks.md) — `check` / `fix` and their passes.
50
- - [Lint presets](docs/07-lint-presets.md) — oxlint presets, `compose`, architecture, knip.
53
+ - [Quality checks](docs/06-quality-checks.md) — `check` / `fix` and their fifteen passes.
54
+ - [Lint presets](docs/07-lint-presets.md) — the rulebook, the six profiles, `compose`, architecture, knip.
51
55
  - [Docs pipeline](docs/08-docs-pipeline.md) — the `typescript docs` compiler.
52
56
  - [Repo structure](docs/09-repo-structure.md) — pointer to the shared doctrine; what's TypeScript-specific here.
53
57
 
54
- For agents: read the chapters and the generated [`docs/reference/`](docs/reference/) tree straight from the repo, plus the [`skills/jterrazz-typescript`](skills/jterrazz-typescript/SKILL.md) Claude Code skill (the toolchain). The repo-structure doctrine itself is a Claude Code skill too — `jterrazz-repo-structure`, shipped from [`jterrazz-studio`](https://github.com/jterrazz/jterrazz-studio).
58
+ For agents: read the chapters and the generated [`docs/reference/`](docs/reference/) tree straight from the repo, plus the [`skills/jterrazz-typescript`](skills/jterrazz-typescript/SKILL.md) Claude Code skill (the toolchain) and its generated [rule reference](skills/jterrazz-typescript/references/rules.md). The repo-structure doctrine itself is a Claude Code skill too — `jterrazz-repo-structure`, shipped from [`jterrazz-studio`](https://github.com/jterrazz/jterrazz-studio).
55
59
 
56
60
  ## License
57
61
 
@@ -62,10 +62,62 @@ find_tsc() {
62
62
  fi
63
63
  }
64
64
 
65
+ # oxlint's type-aware rules run in `tsgolint`, a separate binary it looks up on
66
+ # PATH — and a consumer's PATH has no reason to carry this package's bin dir. It
67
+ # is a dependency here, so the lookup is made to succeed by putting the
68
+ # directory that holds it in front, for this process and its children only.
69
+ add_tsgolint_to_path() {
70
+ local tsgolint
71
+ tsgolint=$(find_binary tsgolint)
72
+ case "$tsgolint" in
73
+ */*)
74
+ PATH="$(cd -P "$(dirname "$tsgolint")" && pwd):$PATH"
75
+ export PATH
76
+ ;;
77
+ esac
78
+ }
79
+
80
+ add_tsgolint_to_path
81
+
82
+ # The config a consumer declares its rules in — the one thing that makes the
83
+ # drift report answerable. Without one there is no profile to have drifted from.
84
+ OXLINT_CONFIG=""
85
+ for candidate in oxlint.config.ts oxlint.config.mjs oxlint.config.js oxlint.config.cjs .oxlintrc.json; do
86
+ [ -f "$candidate" ] && { OXLINT_CONFIG="$candidate"; break; }
87
+ done
88
+
89
+ # The ratchet's file, at the project root. Its presence is what turns the oxlint
90
+ # pass from "no diagnostic at all" into "no diagnostic above what was recorded".
91
+ BASELINE_FILE="oxlint.baseline.json"
92
+
65
93
  TSC=$(find_tsc)
66
94
  OXLINT=$(find_binary oxlint)
67
95
  OXFMT=$(find_binary oxfmt)
68
96
  KNIP=$(find_binary knip)
97
+ DEPCRUISE=$(find_binary depcruise)
98
+ PUBLINT=$(find_binary publint)
99
+ ATTW=$(find_binary attw)
100
+ PRETTIER=$(find_binary prettier)
101
+
102
+ # `astro` is the CONSUMER's dependency, never this package's: the pass runs its
103
+ # checker, it does not ship one. So the lookup starts at the project.
104
+ find_project_binary() {
105
+ local name="$1"
106
+ if [ -x "node_modules/.bin/$name" ]; then
107
+ echo "$PWD/node_modules/.bin/$name"
108
+ else
109
+ find_binary "$name"
110
+ fi
111
+ }
112
+
113
+ # The one file shape oxfmt does not parse. The values are the oxfmt values —
114
+ # 100 / 4 / single / all — so a consumer never declares a formatter of its own.
115
+ #
116
+ # The plugin is passed as a RESOLVED PATH, not as a name in the config: prettier
117
+ # resolves a plugin name from the working directory, which is the consumer's,
118
+ # and the consumer is precisely the project that no longer declares it.
119
+ PRETTIER_ASTRO_CONFIG="$PACKAGE_ROOT/presets/prettier/astro.json"
120
+ PRETTIER_ASTRO_PLUGIN=$(cd "$PACKAGE_ROOT" && node -e 'process.stdout.write(require.resolve("prettier-plugin-astro"))' 2>/dev/null)
69
121
  CHECKER=$(find_binary jterrazz-test-check)
70
122
 
71
123
  # ── The unit is the workspace package, not the repository ────────────────────
@@ -106,15 +158,22 @@ project_uses_jterrazz_test() {
106
158
  node -e 'const {readFileSync}=require("node:fs");const p=JSON.parse(readFileSync(process.argv[1],"utf8"));const d={...p.dependencies,...p.devDependencies,...p.peerDependencies};process.exit(d["@jterrazz/test"]?0:1)' "$dir/package.json" 2>/dev/null
107
159
  }
108
160
 
109
- # In a workspace the dependency may sit on a member alone the warning below
110
- # is about the ROOT oxlint config, but the reason to print it is anywhere.
111
- workspace_uses_jterrazz_test() {
112
- project_uses_jterrazz_test "." && return 0
113
- local member
114
- for member in "${WORKSPACE_MEMBERS[@]}"; do
115
- project_uses_jterrazz_test "$member" && return 0
116
- done
117
- return 1
161
+ # An Astro project, read off its manifest. `.astro` is the one file shape oxfmt
162
+ # does not parse and `astro check` is the only checker that reads a template's
163
+ # frontmatter, so the pass exists exactly where the dependency does.
164
+ project_uses_astro() {
165
+ local dir="${1:-.}"
166
+ [ -f "$dir/package.json" ] || return 1
167
+ node -e 'const {readFileSync}=require("node:fs");const p=JSON.parse(readFileSync(process.argv[1],"utf8"));const d={...p.dependencies,...p.devDependencies,...p.peerDependencies};process.exit(d["astro"]?0:1)' "$dir/package.json" 2>/dev/null
168
+ }
169
+
170
+ # A package the registry would accept: it names an entry (`exports`, `main`) or
171
+ # a publish target, and it never says it is private. A private package, and a
172
+ # workspace root that only holds members, have no tarball to be judged on.
173
+ project_is_publishable() {
174
+ local dir="${1:-.}"
175
+ [ -f "$dir/package.json" ] || return 1
176
+ node -e 'const {readFileSync}=require("node:fs");const p=JSON.parse(readFileSync(process.argv[1],"utf8"));process.exit(p.private!==true&&(p.exports||p.main||p.publishConfig)?0:1)' "$dir/package.json" 2>/dev/null
118
177
  }
119
178
 
120
179
  # A path git has been told to forget is not this workspace's source. Clones,
@@ -174,31 +233,21 @@ discover_docs_roots() {
174
233
  } | LC_ALL=C sort -u
175
234
  }
176
235
 
177
- # The @jterrazz/test oxlint plugin is ESM-only. A CommonJS oxlint config silently drops
178
- # it (oxlint prints a load warning and still exits 0) none of the jterrazz/* rules run.
179
- # Warn loudly when that pitfall is detectable.
180
- warn_cjs_oxlint_config() {
181
- local cfg=""
182
- for c in oxlint.config.ts oxlint.config.mjs oxlint.config.cjs oxlint.config.js; do
183
- [ -f "$c" ] && { cfg="$c"; break; }
184
- done
185
- [ -z "$cfg" ] && return 0
186
-
187
- local is_cjs=false
188
- case "$cfg" in
189
- *.cjs) is_cjs=true ;;
236
+ # The name of the oxlint config, when that config is CommonJS and nothing
237
+ # otherwise. Everything this package ships is ESM, and so is every oxlint JS
238
+ # plugin the estate writes; a CommonJS config cannot load either, and oxlint
239
+ # drops what it cannot load and still exits 0. The rules a config names are the
240
+ # whole claim of a lint run, so the shape of the config is the oxlint pass's
241
+ # business ([Quality checks](../../docs/06-quality-checks.md)).
242
+ commonjs_oxlint_config() {
243
+ case "$OXLINT_CONFIG" in
244
+ *.cjs) printf '%s' "$OXLINT_CONFIG" ;;
190
245
  *.js)
191
246
  if ! node -e 'process.exit(require("./package.json").type==="module"?0:1)' 2>/dev/null; then
192
- is_cjs=true
247
+ printf '%s' "$OXLINT_CONFIG"
193
248
  fi
194
249
  ;;
195
250
  esac
196
-
197
- if [ "$is_cjs" = true ]; then
198
- printf "${RED} WARNING ${NC} @jterrazz/test is installed but %s is CommonJS.\n" "$cfg"
199
- printf " The @jterrazz/test oxlint plugin is ESM-only and will be SILENTLY DROPPED —\n"
200
- printf " none of the jterrazz/* rules will run. Switch to an ESM config (oxlint.config.ts or .mjs).\n\n"
201
- fi
202
251
  }
203
252
 
204
253
  # Parse command and args
@@ -226,6 +275,75 @@ while [[ $# -gt 0 ]]; do
226
275
  esac
227
276
  done
228
277
 
278
+ # ── How a pass speaks ────────────────────────────────────────────────────────
279
+ # Every pass that RAN prints the same three things, in the same order: a `RUN`
280
+ # header carrying its label, whatever it has to say, and one verdict line. A
281
+ # pass that did not apply — no Astro in the project, no declared layer map —
282
+ # prints nothing at all, because it answered no question.
283
+ #
284
+ # What sits between the header and the verdict is the one variable: a failing
285
+ # pass prints its whole captured log, and a passing one stays silent unless it
286
+ # WROTE something. `fix` changed a file the operator owns and silence would hide
287
+ # it, so a writer asks for its log with the fourth argument; a reader's success
288
+ # chatter — oxlint's summary, which its reporter prints on a runner and in a
289
+ # terminal and drops under an AI agent — never reaches the stream, so a green
290
+ # run is byte-identical everywhere.
291
+ report_pass() {
292
+ local label="$1" status="$2" log="$3" writer="${4:-}"
293
+
294
+ printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} %s\n\n" "$label"
295
+ if [ "$status" -ne 0 ] || [ -n "$writer" ]; then
296
+ [ -s "$log" ] && cat "$log"
297
+ fi
298
+ if [ "$status" -eq 0 ]; then
299
+ printf "${GREEN}✓ Passed${NC}\n"
300
+ else
301
+ printf "${RED}✗ Failed with exit code %d${NC}\n" "$status"
302
+ fi
303
+ }
304
+
305
+ # Two ways a lint run says nothing about the rules it was supposed to enforce.
306
+ #
307
+ # A config oxlint cannot parse — a `jsPlugins` naming a module that is not there
308
+ # is the common one — makes it print `Failed to parse oxlint configuration file`
309
+ # and lint nothing; 1.83.0 exits 1 for it, and this names the refusal in the
310
+ # toolchain's own vocabulary rather than leaving a reader with the tool's text.
311
+ # A CommonJS config is the silent one: oxlint drops it whole, prints NOTHING and
312
+ # exits 0, so the run is green having enforced no rule the config named.
313
+ #
314
+ # Both are the oxlint pass refusing, so both are written into its own log and
315
+ # both fail it. After this, `lint_status` is the verdict on what the linter
316
+ # actually ran, not on what it managed to exit with.
317
+ judge_lint_config() {
318
+ local cjs
319
+ cjs=$(commonjs_oxlint_config)
320
+ local refusals=""
321
+
322
+ if grep -q 'Failed to parse oxlint configuration file' "$tmp_dir/lint.log" 2>/dev/null; then
323
+ refusals+="oxlint-config-unparsed ${OXLINT_CONFIG} oxlint refused this config and linted nothing — its own report is above"$'\n'
324
+ fi
325
+ if [ -n "$cjs" ]; then
326
+ refusals+="oxlint-config-commonjs ${cjs} a CommonJS config cannot load an ESM preset or plugin, and oxlint drops what it cannot load — write oxlint.config.ts or .mjs"$'\n'
327
+ fi
328
+
329
+ [ -z "$refusals" ] && return 0
330
+
331
+ printf '%s' "$refusals" >> "$tmp_dir/lint.log"
332
+ lint_status=1
333
+ }
334
+
335
+ # One pass, N runs: the logs of the runs that FAILED, joined into the single log
336
+ # the pass reports under. A green member stays silent — it is the same pass.
337
+ join_logs() {
338
+ local into="$1"
339
+ shift
340
+ : > "$into"
341
+ local log
342
+ for log in "$@"; do
343
+ [ -s "$log" ] && cat "$log" >> "$into"
344
+ done
345
+ }
346
+
229
347
  # Create a temporary directory for log files
230
348
  tmp_dir=$(mktemp -d)
231
349
  cleanup() { rm -rf "$tmp_dir"; }
@@ -243,21 +361,31 @@ run_checks() {
243
361
 
244
362
  printf "${CYAN_BG}${BRIGHT_WHITE} START ${NC} ${LABEL}\n"
245
363
 
246
- if workspace_uses_jterrazz_test; then
247
- warn_cjs_oxlint_config
248
- fi
249
-
250
364
  # Run all tools in parallel
251
365
  "$TSC" --noEmit > "$tmp_dir/type.log" 2>&1 &
252
366
  local type_pid=$!
253
367
 
368
+ # --type-aware is explicit and unconditional: the rules it unlocks are the
369
+ # ones no syntactic linter can express, and a flag that is only sometimes
370
+ # passed is a rule set that is only sometimes enforced. `oxlint-tsgolint` is
371
+ # a dependency of this package, so it is there for every consumer.
254
372
  if [ "$FIX_MODE" = true ]; then
255
- "$OXLINT" --fix "${LINT_ARGS[@]}" > "$tmp_dir/lint.log" 2>&1 &
373
+ "$OXLINT" --type-aware --fix "${LINT_ARGS[@]}" > "$tmp_dir/lint.log" 2>&1 &
256
374
  else
257
- "$OXLINT" "${LINT_ARGS[@]}" > "$tmp_dir/lint.log" 2>&1 &
375
+ "$OXLINT" --type-aware "${LINT_ARGS[@]}" > "$tmp_dir/lint.log" 2>&1 &
258
376
  fi
259
377
  local lint_pid=$!
260
378
 
379
+ # The same run, machine-readable, so the ratchet can be judged rule by rule.
380
+ # A second invocation rather than a reformat of the first: the human log is
381
+ # what a failing pass prints, and neither form can be derived from the other.
382
+ local lint_json_pid=""
383
+ if [ "$FIX_MODE" = false ] && [ -f "$BASELINE_FILE" ]; then
384
+ "$OXLINT" --type-aware --format json "${LINT_ARGS[@]}" \
385
+ > "$tmp_dir/lint.json" 2>/dev/null &
386
+ lint_json_pid=$!
387
+ fi
388
+
261
389
  if [ "$FIX_MODE" = true ]; then
262
390
  "$OXFMT" > "$tmp_dir/format.log" 2>&1 &
263
391
  else
@@ -329,6 +457,113 @@ run_checks() {
329
457
  docs_layout_pid=$!
330
458
  fi
331
459
 
460
+ # Suppressions (directives): every place the project told a checker to look
461
+ # away is spelled in this toolchain's vocabulary, carries its reason, and
462
+ # names a rule that is still live. It runs in BOTH modes — `--fix` settles
463
+ # the two spellings a machine can settle, and never invents a reason.
464
+ local suppressions_pid=""
465
+ local suppressions_status=0
466
+ local suppressions_fix=()
467
+ [ "$FIX_MODE" = true ] && suppressions_fix=(--fix)
468
+ node "$PACKAGE_ROOT/lib/check-suppressions.js" . --oxlint "$OXLINT" \
469
+ "${suppressions_fix[@]}" "${LINT_ARGS[@]}" > "$tmp_dir/suppressions.log" 2>&1 &
470
+ suppressions_pid=$!
471
+
472
+ # Markdown (prose): every tracked page's coordinates resolve, and its blocks
473
+ # breathe. Check-only — there is no rewrite that splits a paragraph into the
474
+ # two ideas it was carrying. It reads the same `--ignore-pattern` globs the
475
+ # linter received, so one flag answers for the whole run.
476
+ local markdown_pid=""
477
+ local markdown_status=0
478
+ if [ "$FIX_MODE" = false ]; then
479
+ node "$PACKAGE_ROOT/lib/check-markdown.js" . "${LINT_ARGS[@]}" \
480
+ > "$tmp_dir/markdown.log" 2>&1 &
481
+ markdown_pid=$!
482
+ fi
483
+
484
+ # Names: what the project calls its own parts, under the roots where a
485
+ # project keeps what it wrote. Check-only — renaming a file is a move, and
486
+ # choosing the name it moves to is the work the rule is asking for.
487
+ local names_pid=""
488
+ local names_status=0
489
+ if [ "$FIX_MODE" = false ]; then
490
+ node "$PACKAGE_ROOT/lib/check-names.js" . "${LINT_ARGS[@]}" > "$tmp_dir/names.log" 2>&1 &
491
+ names_pid=$!
492
+ fi
493
+
494
+ # Secrets: no file the project would commit carries a live-looking
495
+ # credential. The gate always applies, so WHICH engine answers — gitleaks
496
+ # where the machine has it, the built-in patterns where it does not — is
497
+ # decided inside the script, not here.
498
+ local secrets_pid=""
499
+ local secrets_status=0
500
+ if [ "$FIX_MODE" = false ]; then
501
+ node "$PACKAGE_ROOT/lib/check-secrets.js" . "${LINT_ARGS[@]}" \
502
+ > "$tmp_dir/secrets.log" 2>&1 &
503
+ secrets_pid=$!
504
+ fi
505
+
506
+ # Astro: the consumer's own checker, plus the formatter for the one file
507
+ # shape oxfmt does not parse. Both halves run in fix mode too — prettier
508
+ # writes there, and `astro check` is read-only wherever it runs.
509
+ local astro_pid=""
510
+ local astro_status=0
511
+ if project_uses_astro "."; then
512
+ ASTRO=$(find_project_binary astro)
513
+ # A subshell, so both halves run and both are reported — a template that
514
+ # does not type-check is not a reason to stay quiet about its shape.
515
+ # `local` has no meaning past the `&`, hence the plain names.
516
+ (
517
+ "$ASTRO" check
518
+ astro_check=$?
519
+ if [ "$FIX_MODE" = true ]; then
520
+ "$PRETTIER" --write --config "$PRETTIER_ASTRO_CONFIG" \
521
+ --plugin "$PRETTIER_ASTRO_PLUGIN" \
522
+ --no-error-on-unmatched-pattern "**/*.astro"
523
+ else
524
+ "$PRETTIER" --check --config "$PRETTIER_ASTRO_CONFIG" \
525
+ --plugin "$PRETTIER_ASTRO_PLUGIN" \
526
+ --no-error-on-unmatched-pattern "**/*.astro"
527
+ fi
528
+ astro_format=$?
529
+ [ $astro_check -eq 0 ] && [ $astro_format -eq 0 ]
530
+ ) > "$tmp_dir/astro.log" 2>&1 &
531
+ astro_pid=$!
532
+ fi
533
+
534
+ # Architecture (layer map): the graph a project declared, resolved. Bash
535
+ # asks the one question that decides whether the gate applies at all — is
536
+ # there a map — and the script decides what it says. Opt-in by the file's
537
+ # existence: a project with no declared architecture is not in breach of one.
538
+ local architecture_pid=""
539
+ local architecture_status=0
540
+ if [ "$FIX_MODE" = false ] &&
541
+ { [ -f ".dependency-cruiser.cjs" ] || [ -f ".dependency-cruiser.js" ] ||
542
+ [ -f ".dependency-cruiser.mjs" ]; }; then
543
+ node "$PACKAGE_ROOT/lib/check-architecture.js" . --depcruise "$DEPCRUISE" \
544
+ > "$tmp_dir/architecture.log" 2>&1 &
545
+ architecture_pid=$!
546
+ fi
547
+
548
+ # Publish (packaging): what a published package promises, held to what the
549
+ # tarball will contain. Once per package the registry would accept — the
550
+ # unit is the workspace package, and a private one has no tarball.
551
+ local publish_pids=()
552
+ local publish_logs=()
553
+ local publish_status=0
554
+ if [ "$FIX_MODE" = false ]; then
555
+ local publish_index=0
556
+ for publish_root in "." "${WORKSPACE_MEMBERS[@]}"; do
557
+ project_is_publishable "$publish_root" || continue
558
+ node "$PACKAGE_ROOT/lib/check-publish.js" "$publish_root" \
559
+ --publint "$PUBLINT" --attw "$ATTW" \
560
+ > "$tmp_dir/publish-$publish_index.log" 2>&1 &
561
+ publish_pids+=($!)
562
+ publish_logs+=("$tmp_dir/publish-$publish_index.log")
563
+ publish_index=$((publish_index + 1))
564
+ done
565
+ fi
566
+
332
567
  # Conventions checker: only in check mode, once per specs root the workspace
333
568
  # owns, gated by the package that OWNS that root — a member may depend on
334
569
  # @jterrazz/test while the root does not, and the reverse.
@@ -371,10 +606,27 @@ run_checks() {
371
606
  # Wait and collect statuses
372
607
  wait $type_pid; local type_status=$?
373
608
  wait $lint_pid; local lint_status=$?
609
+
610
+ # The ratchet, where the project keeps one: the pass is judged by what the
611
+ # baseline tolerates, not by oxlint's exit code. Bash decides whether the
612
+ # file is there; the script decides what it says.
613
+ if [ -n "$lint_json_pid" ]; then
614
+ wait $lint_json_pid
615
+ node "$PACKAGE_ROOT/lib/check-baseline.js" "$tmp_dir/lint.json" . \
616
+ >> "$tmp_dir/lint.log" 2>&1
617
+ lint_status=$?
618
+ fi
619
+
374
620
  wait $format_pid; local format_status=$?
375
621
  [ -n "$knip_pid" ] && { wait $knip_pid; knip_status=$?; }
376
622
  [ -n "$gitignore_pid" ] && { wait $gitignore_pid; gitignore_status=$?; }
377
623
  [ -n "$docs_layout_pid" ] && { wait $docs_layout_pid; docs_layout_status=$?; }
624
+ wait $suppressions_pid; suppressions_status=$?
625
+ [ -n "$markdown_pid" ] && { wait $markdown_pid; markdown_status=$?; }
626
+ [ -n "$architecture_pid" ] && { wait $architecture_pid; architecture_status=$?; }
627
+ [ -n "$astro_pid" ] && { wait $astro_pid; astro_status=$?; }
628
+ [ -n "$names_pid" ] && { wait $names_pid; names_status=$?; }
629
+ [ -n "$secrets_pid" ] && { wait $secrets_pid; secrets_status=$?; }
378
630
 
379
631
  # One pass, N runs: the pass fails if any run failed, and only the logs of
380
632
  # the runs that FAILED are printed — a green member stays silent.
@@ -388,6 +640,16 @@ run_checks() {
388
640
  index=$((index + 1))
389
641
  done
390
642
 
643
+ local publish_failed_logs=()
644
+ index=0
645
+ for pid in "${publish_pids[@]}"; do
646
+ if ! wait "$pid"; then
647
+ publish_status=1
648
+ publish_failed_logs+=("${publish_logs[$index]}")
649
+ fi
650
+ index=$((index + 1))
651
+ done
652
+
391
653
  local docs_failed_logs=()
392
654
  index=0
393
655
  for pid in "${docs_pids[@]}"; do
@@ -398,92 +660,61 @@ run_checks() {
398
660
  index=$((index + 1))
399
661
  done
400
662
 
401
- # Print results quiet on success, verbose on failure: a tool's captured log
402
- # is shown only when it failed, so green output stays byte-identical across
403
- # platforms (some tool builds print success chatter on Linux but not macOS).
404
- printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} TypeScript Check\n\n"
405
- if [ $type_status -ne 0 ]; then
406
- [ -s "$tmp_dir/type.log" ] && cat "$tmp_dir/type.log"
407
- printf "${RED}✗ Failed with exit code %d${NC}\n" $type_status
408
- else
409
- printf "${GREEN}✓ Passed${NC}\n"
410
- fi
663
+ # ── The report ───────────────────────────────────────────────────────────
664
+ # One order, and it is the chapter's: the three tools, the artefact gate,
665
+ # knip, the conventions checker, the two Docs passes, then the gates a
666
+ # project opts into and the four that read its tree on every run. A pass
667
+ # that did not apply is absent; every pass that ran prints the same block.
668
+ join_logs "$tmp_dir/checker.log" "${checker_failed_logs[@]}"
669
+ join_logs "$tmp_dir/docs.log" "${docs_failed_logs[@]}"
670
+ join_logs "$tmp_dir/publish.log" "${publish_failed_logs[@]}"
411
671
 
412
672
  local lint_label="Oxlint Check"
413
- [ "$FIX_MODE" = true ] && lint_label="Oxlint Fix"
414
- printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} ${lint_label}\n\n"
415
- if [ $lint_status -ne 0 ]; then
416
- [ -s "$tmp_dir/lint.log" ] && cat "$tmp_dir/lint.log"
417
- printf "${RED}✗ Failed with exit code %d${NC}\n" $lint_status
418
- else
419
- printf "${GREEN}✓ Passed${NC}\n"
420
- fi
421
-
422
673
  local format_label="Oxfmt Check"
423
- [ "$FIX_MODE" = true ] && format_label="Oxfmt Format"
424
- printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} ${format_label}\n\n"
425
- if [ $format_status -ne 0 ]; then
426
- [ -s "$tmp_dir/format.log" ] && cat "$tmp_dir/format.log"
427
- printf "${RED}✗ Failed with exit code %d${NC}\n" $format_status
428
- else
429
- printf "${GREEN}✓ Passed${NC}\n"
430
- fi
431
-
432
- # The one pass that speaks on success: a rewrite changed a file the operator
433
- # owns, and silence would hide it. In check mode a green gate writes nothing,
434
- # so the green output stays byte-identical with the others.
435
- if [ -n "$gitignore_pid" ]; then
436
- printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} Gitignore (artefacts)\n\n"
437
- [ -s "$tmp_dir/gitignore.log" ] && cat "$tmp_dir/gitignore.log"
438
- if [ $gitignore_status -ne 0 ]; then
439
- printf "${RED}✗ Failed with exit code %d${NC}\n" $gitignore_status
440
- else
441
- printf "${GREEN}✓ Passed${NC}\n"
442
- fi
674
+ local write=""
675
+ if [ "$FIX_MODE" = true ]; then
676
+ lint_label="Oxlint Fix"
677
+ format_label="Oxfmt Format"
678
+ write="writer"
443
679
  fi
444
680
 
445
- if [ "$FIX_MODE" = false ]; then
446
- printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} Knip (unused code)\n\n"
447
- if [ $knip_status -ne 0 ]; then
448
- [ -s "$tmp_dir/knip.log" ] && cat "$tmp_dir/knip.log"
449
- printf "${RED}✗ Failed with exit code %d${NC}\n" $knip_status
450
- else
451
- printf "${GREEN}✓ Passed${NC}\n"
452
- fi
453
-
454
- if [ ${#checker_pids[@]} -gt 0 ]; then
455
- printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} Test Conventions (@jterrazz/test)\n\n"
456
- if [ $checker_status -ne 0 ]; then
457
- for log in "${checker_failed_logs[@]}"; do
458
- [ -s "$log" ] && cat "$log"
459
- done
460
- printf "${RED}✗ Failed with exit code %d${NC}\n" $checker_status
461
- else
462
- printf "${GREEN}✓ Passed${NC}\n"
463
- fi
464
- fi
465
-
466
- if [ -n "$docs_layout_pid" ]; then
467
- printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} Docs (layout)\n\n"
468
- if [ $docs_layout_status -ne 0 ]; then
469
- [ -s "$tmp_dir/docs-layout.log" ] && cat "$tmp_dir/docs-layout.log"
470
- printf "${RED}✗ Failed with exit code %d${NC}\n" $docs_layout_status
471
- else
472
- printf "${GREEN}✓ Passed${NC}\n"
473
- fi
474
- fi
475
-
476
- if [ ${#docs_pids[@]} -gt 0 ]; then
477
- printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} Docs (sync)\n\n"
478
- if [ $docs_status -ne 0 ]; then
479
- for log in "${docs_failed_logs[@]}"; do
480
- [ -s "$log" ] && cat "$log"
481
- done
482
- printf "${RED}✗ Failed with exit code %d${NC}\n" $docs_status
483
- else
484
- printf "${GREEN}✓ Passed${NC}\n"
485
- fi
486
- fi
681
+ judge_lint_config
682
+ report_pass "TypeScript Check" $type_status "$tmp_dir/type.log"
683
+ report_pass "$lint_label" $lint_status "$tmp_dir/lint.log"
684
+ report_pass "$format_label" $format_status "$tmp_dir/format.log"
685
+ [ -n "$gitignore_pid" ] &&
686
+ report_pass "Gitignore (artefacts)" $gitignore_status "$tmp_dir/gitignore.log" writer
687
+ [ "$FIX_MODE" = false ] &&
688
+ report_pass "Knip (unused code)" $knip_status "$tmp_dir/knip.log"
689
+ [ ${#checker_pids[@]} -gt 0 ] &&
690
+ report_pass "Test Conventions (@jterrazz/test)" $checker_status "$tmp_dir/checker.log"
691
+ [ -n "$docs_layout_pid" ] &&
692
+ report_pass "Docs (layout)" $docs_layout_status "$tmp_dir/docs-layout.log"
693
+ [ ${#docs_pids[@]} -gt 0 ] &&
694
+ report_pass "Docs (sync)" $docs_status "$tmp_dir/docs.log"
695
+ [ ${#publish_pids[@]} -gt 0 ] &&
696
+ report_pass "Publish (packaging)" $publish_status "$tmp_dir/publish.log"
697
+ [ -n "$architecture_pid" ] &&
698
+ report_pass "Architecture (layer map)" $architecture_status "$tmp_dir/architecture.log"
699
+ [ -n "$astro_pid" ] &&
700
+ report_pass "Astro (check + format)" $astro_status "$tmp_dir/astro.log" "$write"
701
+ report_pass "Suppressions (directives)" $suppressions_status "$tmp_dir/suppressions.log" "$write"
702
+ [ -n "$markdown_pid" ] &&
703
+ report_pass "Markdown (prose)" $markdown_status "$tmp_dir/markdown.log"
704
+ [ -n "$names_pid" ] && report_pass "Names (tree)" $names_status "$tmp_dir/names.log"
705
+ [ -n "$secrets_pid" ] && report_pass "Secrets (credentials)" $secrets_status "$tmp_dir/secrets.log"
706
+
707
+ # Drift: the report, not a gate — how far this project stands from the
708
+ # profile it says it extends, in four numbers. It runs last and it speaks on
709
+ # every check, because the alternative is what the estate had: every
710
+ # repository quietly a little further from the shared rulebook, and nobody
711
+ # able to say by how much without opening every config. Only a rule turned
712
+ # off with no reason beside it actually fails the run.
713
+ local drift_status=0
714
+ if [ "$FIX_MODE" = false ] && [ -n "$OXLINT_CONFIG" ]; then
715
+ printf "\n${CYAN_BG}${BRIGHT_WHITE} DRIFT ${NC} Deviations from the profile\n\n"
716
+ node "$PACKAGE_ROOT/lib/check-drift.js" . --oxlint "$OXLINT" "${LINT_ARGS[@]}"
717
+ drift_status=$?
487
718
  fi
488
719
 
489
720
  # Summary
@@ -493,7 +724,7 @@ run_checks() {
493
724
  printf "\n${CYAN_BG}${BRIGHT_WHITE} END ${NC} Finalizing quality checks\n\n"
494
725
  fi
495
726
 
496
- if [ $type_status -eq 0 ] && [ $lint_status -eq 0 ] && [ $format_status -eq 0 ] && [ $knip_status -eq 0 ] && [ $gitignore_status -eq 0 ] && [ $checker_status -eq 0 ] && [ $docs_layout_status -eq 0 ] && [ $docs_status -eq 0 ]; then
727
+ if [ $type_status -eq 0 ] && [ $lint_status -eq 0 ] && [ $format_status -eq 0 ] && [ $knip_status -eq 0 ] && [ $gitignore_status -eq 0 ] && [ $checker_status -eq 0 ] && [ $docs_layout_status -eq 0 ] && [ $docs_status -eq 0 ] && [ $markdown_status -eq 0 ] && [ $names_status -eq 0 ] && [ $secrets_status -eq 0 ] && [ $suppressions_status -eq 0 ] && [ $publish_status -eq 0 ] && [ $architecture_status -eq 0 ] && [ $astro_status -eq 0 ] && [ $drift_status -eq 0 ]; then
497
728
  printf "${GREEN}✓ All checks passed${NC}\n"
498
729
  exit 0
499
730
  else