legion-audit 0.1.0 → 0.5.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 (92) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/README.md +63 -2
  3. package/dist/advice/index.d.ts +12 -0
  4. package/dist/advice/index.d.ts.map +1 -0
  5. package/dist/advice/index.js +151 -0
  6. package/dist/advice/index.js.map +1 -0
  7. package/dist/advice/lessons.d.ts +6 -0
  8. package/dist/advice/lessons.d.ts.map +1 -0
  9. package/dist/advice/lessons.js +94 -0
  10. package/dist/advice/lessons.js.map +1 -0
  11. package/dist/advice/toolchain.d.ts +5 -0
  12. package/dist/advice/toolchain.d.ts.map +1 -0
  13. package/dist/advice/toolchain.js +235 -0
  14. package/dist/advice/toolchain.js.map +1 -0
  15. package/dist/args.d.ts +5 -2
  16. package/dist/args.d.ts.map +1 -1
  17. package/dist/args.js +19 -0
  18. package/dist/args.js.map +1 -1
  19. package/dist/audit.d.ts +2 -1
  20. package/dist/audit.d.ts.map +1 -1
  21. package/dist/audit.js +50 -2
  22. package/dist/audit.js.map +1 -1
  23. package/dist/checks/coverage-report.d.ts +5 -0
  24. package/dist/checks/coverage-report.d.ts.map +1 -0
  25. package/dist/checks/coverage-report.js +75 -0
  26. package/dist/checks/coverage-report.js.map +1 -0
  27. package/dist/checks/dependencies.d.ts +4 -0
  28. package/dist/checks/dependencies.d.ts.map +1 -0
  29. package/dist/checks/dependencies.js +61 -0
  30. package/dist/checks/dependencies.js.map +1 -0
  31. package/dist/checks/expo.d.ts +20 -0
  32. package/dist/checks/expo.d.ts.map +1 -0
  33. package/dist/checks/expo.js +138 -0
  34. package/dist/checks/expo.js.map +1 -0
  35. package/dist/checks/forge.d.ts +7 -0
  36. package/dist/checks/forge.d.ts.map +1 -0
  37. package/dist/checks/forge.js +62 -0
  38. package/dist/checks/forge.js.map +1 -0
  39. package/dist/checks/git.d.ts +2 -2
  40. package/dist/checks/git.d.ts.map +1 -1
  41. package/dist/checks/git.js +32 -17
  42. package/dist/checks/git.js.map +1 -1
  43. package/dist/checks/rule-coverage.d.ts +7 -0
  44. package/dist/checks/rule-coverage.d.ts.map +1 -0
  45. package/dist/checks/rule-coverage.js +101 -0
  46. package/dist/checks/rule-coverage.js.map +1 -0
  47. package/dist/checks/suppressions.d.ts +8 -0
  48. package/dist/checks/suppressions.d.ts.map +1 -0
  49. package/dist/checks/suppressions.js +56 -0
  50. package/dist/checks/suppressions.js.map +1 -0
  51. package/dist/checks/test-coverage.d.ts +3 -2
  52. package/dist/checks/test-coverage.d.ts.map +1 -1
  53. package/dist/checks/test-coverage.js +62 -4
  54. package/dist/checks/test-coverage.js.map +1 -1
  55. package/dist/checks/toolchain.d.ts +5 -0
  56. package/dist/checks/toolchain.d.ts.map +1 -0
  57. package/dist/checks/toolchain.js +142 -0
  58. package/dist/checks/toolchain.js.map +1 -0
  59. package/dist/cli.js +59 -13
  60. package/dist/cli.js.map +1 -1
  61. package/dist/exec.d.ts.map +1 -1
  62. package/dist/exec.js +6 -3
  63. package/dist/exec.js.map +1 -1
  64. package/dist/index.d.ts +15 -1
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +12 -1
  67. package/dist/index.js.map +1 -1
  68. package/dist/report/annotations.d.ts +4 -0
  69. package/dist/report/annotations.d.ts.map +1 -0
  70. package/dist/report/annotations.js +27 -0
  71. package/dist/report/annotations.js.map +1 -0
  72. package/dist/report/baseline.d.ts.map +1 -1
  73. package/dist/report/baseline.js +8 -4
  74. package/dist/report/baseline.js.map +1 -1
  75. package/dist/report/console.d.ts +4 -0
  76. package/dist/report/console.d.ts.map +1 -0
  77. package/dist/report/console.js +84 -0
  78. package/dist/report/console.js.map +1 -0
  79. package/dist/report/markdown.d.ts.map +1 -1
  80. package/dist/report/markdown.js +39 -1
  81. package/dist/report/markdown.js.map +1 -1
  82. package/dist/report/score.d.ts +4 -5
  83. package/dist/report/score.d.ts.map +1 -1
  84. package/dist/report/score.js +8 -7
  85. package/dist/report/score.js.map +1 -1
  86. package/dist/types.d.ts +90 -0
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/workspace.d.ts +5 -0
  89. package/dist/workspace.d.ts.map +1 -0
  90. package/dist/workspace.js +52 -0
  91. package/dist/workspace.js.map +1 -0
  92. package/package.json +2 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,162 @@
1
1
  # legion-audit
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 45ef19c: Five changes aimed at the same thing: making the toolkit enforceable in a
8
+ repo that already has debt, rather than only in a new one.
9
+
10
+ **Autofixers.** None of the rules had one. `button-has-type` and
11
+ `no-target-blank-without-rel` now insert the missing attribute, and
12
+ `no-react-fc` performs the whole transform, moving the type argument onto the
13
+ parameter so props do not silently become untyped. Three rules are
14
+ deliberately left unfixable: `alt=""` is a semantic claim that an image is
15
+ decorative, converting a `function` declaration to a `const` arrow changes
16
+ hoisting, and a hex literal cannot be replaced without knowing the token
17
+ name.
18
+
19
+ **A suppression ratchet.** `legion-audit suppress` records today's findings
20
+ as accepted, so a repo can move to error level immediately and fail only on
21
+ new violations. This is what `--baseline` never did: that compares two
22
+ reports, this is a budget per rule and file, and a fourth violation in a file
23
+ that recorded three still fails.
24
+
25
+ **`legion-steer`.** A new package that writes a managed block into
26
+ `AGENTS.md`, `.junie/guidelines.md`, `.cursor/rules/legion.mdc` and
27
+ `.github/copilot-instructions.md`, generated from the resolved lint config.
28
+ A rule the config does not enforce cannot appear in the block, so steering
29
+ cannot drift from enforcement. `legion-steer check` fails CI when it has.
30
+ Only the region between its markers is ever touched.
31
+
32
+ `legion-steer hook` closes the loop. Lint reports a violation after the file
33
+ exists; the hook runs the same rules on the content Claude Code is about to
34
+ write and exits 2, which blocks the write. `PreToolUse` lints
35
+ `tool_input.content` in a scratch copy beside the intended path, so
36
+ path-based rules see the real directory, and the finding is reported against
37
+ the file the agent tried to write rather than the copy. `PostToolUse` lints
38
+ what just landed and reports without blocking. `init` registers both in
39
+ `.claude/settings.json`, merging with hooks that are already there.
40
+
41
+ **A dependency check.** Section 10 lists adding a state library under
42
+ automatic rejection and nothing was checking it. The audit now reads
43
+ `package.json` for Redux, MobX, runtime CSS-in-JS and Moment, and nudges with
44
+ what to use instead.
45
+
46
+ **GitHub annotations.** `legion-audit --annotations` emits workflow commands,
47
+ so findings land on the pull request diff rather than in a log.
48
+
49
+ ## 0.4.0
50
+
51
+ No changes in this release.
52
+
53
+ ## 0.3.0
54
+
55
+ ### Minor Changes
56
+
57
+ - 649a264: Ten new rules, a platform split, and the React Compiler flag actually working.
58
+
59
+ Two of the rules were already law in LEGION-STANDARDS with no mechanical
60
+ check, which is the drift §8.1 warns about: `no-hardcoded-hex` (§7, §10 lists
61
+ it under automatic rejection) and `no-rn-button` (§3 mandates
62
+ `TouchableOpacity`, not `Button`). The rest are silent-default bugs rather
63
+ than style: `button-has-type` (HTML defaults a bare button to `submit`, so
64
+ one inside a form reloads the page), `no-target-blank-without-rel`,
65
+ `anchor-is-not-a-button`, `img-has-alt`, `no-dangerous-html`,
66
+ `no-array-index-key`, `touchable-has-accessibility-label` and
67
+ `controlled-text-input`.
68
+
69
+ Rules now know their platform. `WEB_ONLY_RULES` gained the five DOM rules and
70
+ a mirrored `NATIVE_ONLY_RULES` holds the three React Native ones, so a config
71
+ only runs what can fire on its platform: the DOM rules are off under
72
+ `reactNative`, the native rules off everywhere else, and `no-hardcoded-hex`
73
+ and `no-array-index-key` run in both.
74
+
75
+ New rules land at level 1 in `recommended` and 3 in `strict`. The cohort is
76
+ exported as `INTRODUCED_AT_WARN`, so promoting them later is one edit and the
77
+ preset tests fail if the policy changes silently. Adding a rule should never
78
+ turn an existing repo's build red for code written before it existed.
79
+
80
+ `reactCompiler: true` now works when composed with a preset. It only enabled
81
+ `no-manual-memo` when the rule was `undefined`, but both presets set it to
82
+ `0` explicitly, so the flag was inert for every documented way of using it.
83
+ `legion-audit` also detects the compiler now — from
84
+ `babel-plugin-react-compiler`, `eslint-plugin-react-compiler`, or
85
+ `reactCompiler` in a Next config, never from the React version, since React
86
+ 19 does not imply the compiler — and nudges when it is on while the rule is
87
+ off.
88
+
89
+ `no-hardcoded-hex` is off in test files, since a rule's own fixtures have to
90
+ contain the thing it forbids.
91
+ - 6c16c00: **Breaking:** `runAudit` is now `async` and returns `Promise<AuditResult>`.
92
+ Reading the consumer's installed plugin needs a dynamic import, which cannot
93
+ be done synchronously. Callers must `await` it. The `legion-audit` CLI does
94
+ this internally, so only code importing `runAudit` directly is affected. The
95
+ version bump stays minor because pre-1.0 that is where a breaking change
96
+ lands; a `major` here would force 1.0.0, which is a decision this change
97
+ should not make on its own.
98
+
99
+ PR hygiene understands more than GitHub, and the audit reports rules your
100
+ config never picked up.
101
+
102
+ `prHygiene` matched `/\(#\d+\)/`, which is GitHub's squash-merge subject and
103
+ nothing else. A GitLab, Bitbucket or Azure DevOps repo scored 0% and got a
104
+ nudge telling a team that reviews everything that it reviews nothing. The
105
+ forge is now read from the origin remote, each one gets the pattern it
106
+ actually writes, and the fix command matches the host (`gh pr create`,
107
+ `glab mr create`, `az repos pr create`). GitLab's merge-request reference
108
+ lives in the commit body rather than the subject, so the log format is now
109
+ `%s%n%b`. An unrecognized host skips the check rather than guessing, because
110
+ guessing is what caused this.
111
+
112
+ New `ruleCoverage` check. A config that enumerates rules by hand stops
113
+ receiving them: the rules added since it was written sit at 0 and nothing
114
+ says so. The audit now reads the rule list from the plugin _the consumer has
115
+ installed_, compares it against what their oxlint config names or what
116
+ `eslint --print-config` resolves, and nudges with the missing rules by name.
117
+ Reading from the installed plugin rather than a list baked into the audit
118
+ keeps `legion-audit` dependency-free and means it reports against the version
119
+ in front of it.
120
+
121
+ ## 0.2.0
122
+
123
+ ### Minor Changes
124
+
125
+ - 4d0ccf6: React Native support and an advice layer.
126
+
127
+ `legion-rules` gains a `reactNative` preset and a `reactNative: true` flag on
128
+ `defineLegionConfig`. It turns off the two rules that can only describe a
129
+ server-rendered page, widens `no-admin-client-in-browser` to every file
130
+ because a device bundle has no unreachable half, and adds the `src/`-prefixed
131
+ `any` boundaries. `eslint-plugin-legion` exposes it as
132
+ `configs.reactNative`.
133
+
134
+ `legion-audit` gains three things. An `expo-doctor` gate on Expo projects
135
+ that keeps a real version incompatibility blocking while demoting
136
+ patch-level drift to a nudge. Workspace awareness, so a package inside a
137
+ monorepo finds the lockfile and the hoisted bins at the workspace root
138
+ instead of reporting them missing. And a nudge layer: every run now ends with
139
+ what the repo could do better, why, and the command, including migrating off
140
+ npm or Yarn Classic to Yarn Berry in whichever package manager is currently
141
+ in use. Nudges never affect the score or the exit code. `legion-audit advice`
142
+ prints only those.
143
+
144
+ Two scoring changes, both so the number measures what the current code
145
+ controls. PR hygiene no longer scores at all: it reads history on `main`,
146
+ cannot be fixed retroactively, and section 12 only requires a PR on a client
147
+ repo, so it is now a nudge. And the test penalty is the _share_ of components
148
+ and hooks left uncovered rather than the count, so decomposing one oversized
149
+ component into ten presentational children cannot make the score worse than
150
+ the component it replaced. `findRegressions` compares the same share for the
151
+ same reason.
152
+
153
+ Test coverage is now measured rather than guessed. The audit reads
154
+ `coverage/coverage-summary.json` when it exists, then Istanbul's
155
+ `coverage-final.json` (which is what Jest's default reporters write, so it is
156
+ the more common of the two), then falls back to what the test files actually
157
+ import and `describe`, and only then to matching base names. A single test
158
+ file covering many components now counts for all of them.
159
+
3
160
  ## 0.1.0
4
161
 
5
162
  ### Minor Changes
package/README.md CHANGED
@@ -25,11 +25,36 @@ yarn legion-audit --md AUDIT.md --json audit.json
25
25
  | lint bypasses | every `eslint-disable` / `oxlint-disable` directive and `NOSONAR`, counted per rule |
26
26
  | file-wide disables | a bare `/* eslint-disable */` or `/* oxlint-disable */`; always a hard fail |
27
27
  | PR hygiene | share of the last 80 commits whose subject carries a `(#123)` PR number |
28
- | tests by file | components (`components/**`, `app/**`) and `use*` hooks without a matching test file |
29
- | lockfile | exactly one lockfile at the root |
28
+ | expo-doctor | `expo-doctor` on an Expo project; patch-level drift is a nudge, not a failure |
29
+ | test coverage | the coverage report when there is one, otherwise what the test files actually import |
30
+ | lockfile | exactly one lockfile, found at the workspace root when this is a monorepo package |
31
+ | toolchain | package manager and Yarn flavor, framework, whether the plugin is actually wired in |
30
32
 
31
33
  A tool that is not installed reports `skipped`, never `pass`.
32
34
 
35
+ ### Test coverage, in order of preference
36
+
37
+ The question is which components and hooks are actually exercised, and there
38
+ are three ways to answer it. The audit uses the best one available and says
39
+ which in the report:
40
+
41
+ 1. **`coverage/coverage-summary.json`** if it exists. Real per-file line
42
+ coverage, which is a measurement rather than a guess. Run the suite once
43
+ with `--coverage --coverageReporters=json-summary` to get it.
44
+ 2. **What the test files import and `describe`.** One file covering thirteen
45
+ components counts for all thirteen. Group your tests however reads best;
46
+ the audit is not a reason to split them.
47
+ 3. **Matching base names**, only when there is nothing better to go on.
48
+
49
+ ### expo-doctor
50
+
51
+ On an Expo project the doctor runs as a gate, but not every doctor failure is
52
+ equal. A wrong major or minor against the installed SDK breaks builds and
53
+ fails the gate. A newer patch existing does not break anything, and Expo
54
+ ships patches faster than a project can consume them, so that is reported as
55
+ a nudge with the exact `expo install --fix` to run. A gate that goes red for
56
+ something unrelated to your code is a gate people learn to ignore.
57
+
33
58
  ## Output
34
59
 
35
60
  `AUDIT.md` opens with the gate table, then per-rule counts, findings with
@@ -40,6 +65,18 @@ The score starts at 100 and subtracts documented penalties (see the bottom of
40
65
  any report). It is meant for comparing runs of the same repo over time, not
41
66
  for comparing repos to each other.
42
67
 
68
+ Two things deliberately do not score, because a score should measure what the
69
+ current state of the code controls:
70
+
71
+ - **PR hygiene is a nudge, not a penalty.** It reads commit history on `main`,
72
+ which cannot be fixed retroactively and corrects itself as PR-tagged commits
73
+ land. Section 12 requires a PR on a client repo; on your own repo it is your
74
+ call, and the audit is not the place to argue about it.
75
+ - **The test penalty is a share, not a count.** Decomposing one oversized
76
+ component into ten presentational children is exactly what section 1 asks
77
+ for, and it triples the denominator. Scoring the count would mean the score
78
+ got worse for doing the right thing.
79
+
43
80
  ## In CI
44
81
 
45
82
  ```sh
@@ -52,6 +89,30 @@ file-wide disables, more files without tests, or a lower score. `--fail-on
52
89
  error` fails on any failed gate or `legion/*` error regardless of baseline;
53
90
  `--fail-on never` always exits 0.
54
91
 
92
+ ## Nudges
93
+
94
+ Every run ends with the nudges: what this repo could do better, why it
95
+ matters, and the command to do it. They are advisory. They never change the
96
+ score and never change the exit code, because advice that can fail your build
97
+ is not advice.
98
+
99
+ ```
100
+ legion-audit: score 71/100, all gates green
101
+
102
+ TOOLCHAIN
103
+ This repo runs on npm. Legion repos run on Yarn Berry.
104
+ why Berry writes a packageManager field into package.json, so
105
+ Corepack pins the exact Yarn every machine and every CI runner
106
+ uses.
107
+ fix corepack enable
108
+ fix yarn set version berry
109
+ fix rm -rf node_modules package-lock.json
110
+ fix yarn install
111
+ ```
112
+
113
+ `legion-audit advice` prints only that, skipping the slow gates. The nudges
114
+ also land in `AUDIT.md` under `## Nudges` and in the JSON as `advice`.
115
+
55
116
  ## `legion-audit directives`
56
117
 
57
118
  The fast subcommand: only the file-wide disable scan, exit 1 if any. A
@@ -0,0 +1,12 @@
1
+ import type { Advice, AuditResult, DirectivesResult, LintResult, DependencyResult, PrHygiene, RuleCoverage, TestCoverage } from '../types.js';
2
+ export declare const topRuleAdvice: (lint: LintResult) => Advice[];
3
+ export declare const testAdvice: (coverage: TestCoverage) => Advice | null;
4
+ export declare const bypassAdvice: (directives: DirectivesResult) => Advice | null;
5
+ export declare const ruleCoverageAdvice: (coverage: RuleCoverage) => Advice | null;
6
+ export declare const dependencyAdvice: (dependencies: DependencyResult) => Advice | null;
7
+ export declare const prAdvice: (hygiene: PrHygiene) => Advice | null;
8
+ export declare const buildAdvice: (result: Omit<AuditResult, "schema" | "generatedAt" | "score" | "advice">) => Advice[];
9
+ export { RULE_LESSONS } from './lessons.js';
10
+ export { yarnMigrationSteps, toolchainAdvice, TARGET_YARN, } from './toolchain.js';
11
+ export type { Lesson } from './lessons.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/advice/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,MAAM,EACN,WAAW,EACX,gBAAgB,EAChB,UAAU,EACV,gBAAgB,EAChB,SAAS,EACT,YAAY,EACZ,YAAY,EACb,MAAM,aAAa,CAAC;AAiBrB,eAAO,MAAM,aAAa,GAAI,MAAM,UAAU,KAAG,MAAM,EAoBtD,CAAC;AAWF,eAAO,MAAM,UAAU,GAAI,UAAU,YAAY,KAAG,MAAM,GAAG,IAuB5D,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,YAAY,gBAAgB,KAAG,MAAM,GAAG,IAcpE,CAAC;AAIF,eAAO,MAAM,kBAAkB,GAAI,UAAU,YAAY,KAAG,MAAM,GAAG,IAiBpE,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAC3B,cAAc,gBAAgB,KAC7B,MAAM,GAAG,IAeX,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,SAAS,SAAS,KAAG,MAAM,GAAG,IAetD,CAAC;AAEF,eAAO,MAAM,WAAW,GACtB,QAAQ,IAAI,CAAC,WAAW,EAAE,QAAQ,GAAG,aAAa,GAAG,OAAO,GAAG,QAAQ,CAAC,KACvE,MAAM,EAqBR,CAAC;AAEF,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,WAAW,GACZ,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,151 @@
1
+ import { RULE_LESSONS } from './lessons.js';
2
+ import { toolchainAdvice } from './toolchain.js';
3
+ import { forgeFix, forgeLabel } from '../checks/forge.js';
4
+ const MAX_DETAIL = 6;
5
+ const MEMO_RULES = new Set(['legion/no-manual-memo']);
6
+ const manualMemoCount = (lint) => lint.rules
7
+ .filter((rule) => MEMO_RULES.has(rule.rule))
8
+ .reduce((total, rule) => total + rule.errors + rule.warnings, 0);
9
+ const TOP_RULES = 3;
10
+ const withOverflow = (lines, total) => {
11
+ if (total <= lines.length)
12
+ return lines;
13
+ return [...lines, `...and ${total - lines.length} more`];
14
+ };
15
+ export const topRuleAdvice = (lint) => {
16
+ const ranked = lint.rules
17
+ .filter((rule) => RULE_LESSONS[rule.rule] !== undefined)
18
+ .filter((rule) => rule.errors + rule.warnings > 0)
19
+ .slice(0, TOP_RULES);
20
+ return ranked.map((rule) => {
21
+ const lesson = RULE_LESSONS[rule.rule];
22
+ const hits = lint.legion
23
+ .concat(lint.componentCap)
24
+ .filter((finding) => finding.rule === rule.rule);
25
+ const files = [...new Set(hits.map((finding) => finding.file))];
26
+ return {
27
+ id: `rule:${rule.rule}`,
28
+ area: 'rules',
29
+ title: `${rule.rule} fires ${rule.errors + rule.warnings} time(s).`,
30
+ why: lesson.why,
31
+ fix: lesson.fix,
32
+ detail: withOverflow(files.slice(0, MAX_DETAIL), files.length),
33
+ };
34
+ });
35
+ };
36
+ const COVERAGE_WORDING = {
37
+ 'coverage-report': 'Measured from your coverage report, so this is what the tests actually execute.',
38
+ references: 'Inferred from what your test files import and describe, so a batch file covering many components counts for all of them.',
39
+ filenames: 'Inferred from filenames only. Run the tests with coverage once and this becomes a measurement instead of a guess.',
40
+ };
41
+ export const testAdvice = (coverage) => {
42
+ if (coverage.missing.length === 0)
43
+ return null;
44
+ const components = coverage.missing.filter((entry) => entry.kind === 'component').length;
45
+ const hooks = coverage.missing.length - components;
46
+ const files = coverage.missing.map((entry) => entry.file);
47
+ const detail = [
48
+ COVERAGE_WORDING[coverage.source],
49
+ ...withOverflow(files.slice(0, MAX_DETAIL), files.length),
50
+ ];
51
+ return {
52
+ id: 'missing-tests',
53
+ area: 'tests',
54
+ title: `${components} component(s) and ${hooks} hook(s) are not covered by a test.`,
55
+ why: 'Section 8: a feature without tests is incomplete. The hooks are the cheap ones to start with, because a view-model hook is a plain function that takes props and returns an object.',
56
+ fix: [
57
+ 'hooks: call it with props and assert the returned object',
58
+ 'components: Testing Library with the hook mocked',
59
+ 'one file per component is not required; group them however reads best',
60
+ ],
61
+ detail,
62
+ };
63
+ };
64
+ export const bypassAdvice = (directives) => {
65
+ if (directives.fileWide.length === 0)
66
+ return null;
67
+ const lesson = RULE_LESSONS['legion/scoped-disables'];
68
+ const files = directives.fileWide.map((hit) => `${hit.file}:${hit.line}`);
69
+ return {
70
+ id: 'file-wide-disables',
71
+ area: 'bypasses',
72
+ title: `${directives.fileWide.length} file-wide lint disable(s).`,
73
+ why: lesson.why,
74
+ fix: lesson.fix,
75
+ detail: withOverflow(files.slice(0, MAX_DETAIL), files.length),
76
+ };
77
+ };
78
+ const PR_HYGIENE_FLOOR = 0.2;
79
+ export const ruleCoverageAdvice = (coverage) => {
80
+ if (coverage.status !== 'checked')
81
+ return null;
82
+ if (coverage.unconfigured.length === 0)
83
+ return null;
84
+ return {
85
+ id: 'unconfigured-rules',
86
+ area: 'toolchain',
87
+ title: `${coverage.configured.length} of ${coverage.available.length} Legion rules are configured.`,
88
+ why: 'A config that names every rule by hand stops receiving them. The rules added since this config was written sit at 0, nothing reports that, and the repo quietly drifts from the standard it thinks it is holding. A preset picks up new rules on upgrade; an explicit list never does.',
89
+ fix: [
90
+ 'oxlint: "jsPlugins": ["legion-toolkit/eslint-plugin"] plus the recommended preset',
91
+ 'eslint: spread plugin.configs.recommended, then override only what differs',
92
+ ],
93
+ detail: withOverflow(coverage.unconfigured.slice(0, MAX_DETAIL), coverage.unconfigured.length),
94
+ };
95
+ };
96
+ export const dependencyAdvice = (dependencies) => {
97
+ if (dependencies.forbidden.length === 0)
98
+ return null;
99
+ const names = dependencies.forbidden.map((entry) => entry.name).join(', ');
100
+ return {
101
+ id: 'forbidden-dependencies',
102
+ area: 'toolchain',
103
+ title: `${dependencies.forbidden.length} dependency choice(s) section 9 rules out: ${names}.`,
104
+ why: 'Section 10 lists adding a state library to a simple app under automatic rejection, and section 7 replaces runtime CSS-in-JS with tokens. Nothing was checking either, so the rule lived only in the document.',
105
+ fix: dependencies.forbidden.map((entry) => `${entry.name}: use ${entry.instead}`),
106
+ detail: dependencies.forbidden.map((entry) => `${entry.name}@${entry.version} — ${entry.reason}`),
107
+ };
108
+ };
109
+ export const prAdvice = (hygiene) => {
110
+ if (hygiene.status === 'skipped')
111
+ return null;
112
+ if (hygiene.ratio >= PR_HYGIENE_FLOOR)
113
+ return null;
114
+ const direct = hygiene.inspected - hygiene.withPr;
115
+ return {
116
+ id: 'pr-hygiene',
117
+ area: 'process',
118
+ title: `${direct} of the last ${hygiene.inspected} commits landed straight on the branch (${forgeLabel(hygiene.forge)}).`,
119
+ why: 'Not a problem by itself, and it does not affect the score. Section 12 only requires a PR on a client repo, where nothing reaches main without review. On your own repo it is your call, so the thing to check is that the commit messages carry what a PR description would have said, because they are then the only record.',
120
+ fix: [
121
+ `client repo: ${forgeFix(hygiene.forge).join(', ')}, and never merge it yourself`,
122
+ 'own repo: nothing to change, as long as the commit message says why',
123
+ ],
124
+ detail: [],
125
+ };
126
+ };
127
+ export const buildAdvice = (result) => {
128
+ const entries = [
129
+ ...toolchainAdvice(result.toolchain, result.lockfile, result.expo, manualMemoCount(result.lint)),
130
+ ...topRuleAdvice(result.lint),
131
+ ];
132
+ const tests = testAdvice(result.testCoverage);
133
+ if (tests)
134
+ entries.push(tests);
135
+ const bypasses = bypassAdvice(result.directives);
136
+ if (bypasses)
137
+ entries.push(bypasses);
138
+ const coverage = ruleCoverageAdvice(result.ruleCoverage);
139
+ if (coverage)
140
+ entries.push(coverage);
141
+ const deps = dependencyAdvice(result.dependencies);
142
+ if (deps)
143
+ entries.push(deps);
144
+ const process = prAdvice(result.prHygiene);
145
+ if (process)
146
+ entries.push(process);
147
+ return entries;
148
+ };
149
+ export { RULE_LESSONS } from './lessons.js';
150
+ export { yarnMigrationSteps, toolchainAdvice, TARGET_YARN, } from './toolchain.js';
151
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/advice/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export interface Lesson {
2
+ why: string;
3
+ fix: string[];
4
+ }
5
+ export declare const RULE_LESSONS: Record<string, Lesson>;
6
+ //# sourceMappingURL=lessons.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lessons.d.ts","sourceRoot":"","sources":["../../src/advice/lessons.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,EAAE,CAAC;CACf;AAED,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA4F/C,CAAC"}
@@ -0,0 +1,94 @@
1
+ export const RULE_LESSONS = {
2
+ 'legion/no-function-keyword': {
3
+ why: 'A default export written as `export default function App` loses its name in React DevTools and in stack traces, so a crash report points at Anonymous. Arrow consts also hoist the way you expect instead of silently working before their definition.',
4
+ fix: ['const App = () => { ... };', 'export default App;'],
5
+ },
6
+ 'legion/presentational-components': {
7
+ why: 'A component that holds state, effects or a handler body cannot be rendered in a test without also standing up everything it fetches. Moving the logic into use<Component> gives you a plain function to unit test and a component that is only markup.',
8
+ fix: [
9
+ 'create lib/hooks/use<Component>.ts',
10
+ 'move the state, effects and handlers into it',
11
+ 'return a flat object of values and handleX functions',
12
+ 'the component becomes: call the hook, early return, render children',
13
+ ],
14
+ },
15
+ 'legion/no-literal-conditions-in-jsx': {
16
+ why: 'A comparison in the markup is a business rule hidden where nobody greps for it. When the rule changes you have to find every JSX file that encoded it, and you will miss one.',
17
+ fix: [
18
+ 'derive the flag in the hook: const showBanner = status === "expired";',
19
+ 'render with the flag: {showBanner && <Banner />}',
20
+ ],
21
+ },
22
+ 'legion/no-narrative-comments': {
23
+ why: 'Comments drift. The code changes, the comment does not, and now the file actively lies to the next reader. Names and small functions cannot drift because they are the thing being executed.',
24
+ fix: [
25
+ 'delete the comment',
26
+ 'if it explained what, rename until the name says it',
27
+ 'if it explained why, that belongs in the PR description',
28
+ ],
29
+ },
30
+ 'legion/no-react-fc': {
31
+ why: 'React.FC adds an implicit children prop to components that do not take children, and it gets in the way of generics. A plain typed props parameter says exactly what the component accepts.',
32
+ fix: ['const Card = ({ title }: CardProps) => { ... }'],
33
+ },
34
+ 'legion/no-enum': {
35
+ why: 'A TypeScript enum emits a runtime object, so it survives into the bundle and is not erasable. A string-literal union is compile-time only and narrows better.',
36
+ fix: ["type Status = 'idle' | 'loading' | 'done';"],
37
+ },
38
+ 'legion/guarded-context-hook': {
39
+ why: 'A raw useContext returns undefined outside its provider, so the failure surfaces later as a property access on undefined, somewhere unrelated. A guarded hook fails at the call site with the provider name in the message.',
40
+ fix: [
41
+ 'export const useAppContext = () => {',
42
+ ' const ctx = useContext(AppContext);',
43
+ ' if (!ctx) throw new Error("useAppContext must be used within AppProvider");',
44
+ ' return ctx;',
45
+ '};',
46
+ ],
47
+ },
48
+ 'legion/no-client-globals-in-state-init': {
49
+ why: 'The server has no localStorage, so it renders one value and the browser hydrates with another. React throws away the server markup and you get a flash of the wrong content.',
50
+ fix: [
51
+ 'initialize from a prop or a URL value',
52
+ 'read the client-only preference in a useEffect and reconcile',
53
+ ],
54
+ },
55
+ 'legion/no-use-client-in-page': {
56
+ why: 'A "use client" page ships the whole route to the browser and gives up server-side data fetching for it. The point of the app router is that the page fetches and only the interactive leaf is client.',
57
+ fix: [
58
+ 'keep page.tsx a server component that fetches',
59
+ 'move the interactivity into <Route>Client.tsx with the directive',
60
+ 'pass the fetched data down as props',
61
+ ],
62
+ },
63
+ 'legion/no-admin-client-in-browser': {
64
+ why: 'The service-role key bypasses row level security. Anything that reaches the browser or a device bundle can be read out of it, and then the key is not a secret any more.',
65
+ fix: [
66
+ 'use the browser or server client here',
67
+ 'keep admin queries behind a route handler or a server component',
68
+ ],
69
+ },
70
+ 'legion/scoped-disables': {
71
+ why: 'A file-wide disable turns the rule off for every line in the file, including the code nobody has written yet. A scoped disable with a reason turns it off for exactly the line that needed it and tells the next reader why.',
72
+ fix: ['eslint-disable-next-line <rule> -- <the reason>'],
73
+ },
74
+ 'legion/no-manual-memo': {
75
+ why: 'Under the React Compiler the memoization is automatic, so a hand-written useMemo is a dependency array to keep correct for no gain. A wrong dep array is a real bug; no dep array cannot be.',
76
+ fix: ['drop the useMemo and write the value directly'],
77
+ },
78
+ 'no-ternary': {
79
+ why: 'Nested ternaries are where render logic goes to hide. A named flag reads as the rule it encodes, and a two-way branch is usually a child component or a Record lookup.',
80
+ fix: [
81
+ 'default: value ?? fallback',
82
+ 'render branch: {isReady && <Ready />} or an early return',
83
+ 'two-way: a child component, or a Record keyed by the value',
84
+ ],
85
+ },
86
+ 'max-lines': {
87
+ why: 'The cap is a symptom check, not the rule. A component over 180 lines has almost always absorbed logic that belongs in a hook, and the length is just how you noticed.',
88
+ fix: [
89
+ 'move state, effects and handlers into use<Component>',
90
+ 'extract each section of the markup into its own child component',
91
+ ],
92
+ },
93
+ };
94
+ //# sourceMappingURL=lessons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lessons.js","sourceRoot":"","sources":["../../src/advice/lessons.ts"],"names":[],"mappings":"AAKA,MAAM,CAAC,MAAM,YAAY,GAA2B;IAClD,4BAA4B,EAAE;QAC5B,GAAG,EAAE,wPAAwP;QAC7P,GAAG,EAAE,CAAC,4BAA4B,EAAE,qBAAqB,CAAC;KAC3D;IACD,kCAAkC,EAAE;QAClC,GAAG,EAAE,wPAAwP;QAC7P,GAAG,EAAE;YACH,oCAAoC;YACpC,8CAA8C;YAC9C,sDAAsD;YACtD,qEAAqE;SACtE;KACF;IACD,qCAAqC,EAAE;QACrC,GAAG,EAAE,+KAA+K;QACpL,GAAG,EAAE;YACH,uEAAuE;YACvE,kDAAkD;SACnD;KACF;IACD,8BAA8B,EAAE;QAC9B,GAAG,EAAE,8LAA8L;QACnM,GAAG,EAAE;YACH,oBAAoB;YACpB,qDAAqD;YACrD,yDAAyD;SAC1D;KACF;IACD,oBAAoB,EAAE;QACpB,GAAG,EAAE,6LAA6L;QAClM,GAAG,EAAE,CAAC,gDAAgD,CAAC;KACxD;IACD,gBAAgB,EAAE;QAChB,GAAG,EAAE,+JAA+J;QACpK,GAAG,EAAE,CAAC,4CAA4C,CAAC;KACpD;IACD,6BAA6B,EAAE;QAC7B,GAAG,EAAE,6NAA6N;QAClO,GAAG,EAAE;YACH,sCAAsC;YACtC,uCAAuC;YACvC,+EAA+E;YAC/E,eAAe;YACf,IAAI;SACL;KACF;IACD,wCAAwC,EAAE;QACxC,GAAG,EAAE,8KAA8K;QACnL,GAAG,EAAE;YACH,uCAAuC;YACvC,8DAA8D;SAC/D;KACF;IACD,8BAA8B,EAAE;QAC9B,GAAG,EAAE,uMAAuM;QAC5M,GAAG,EAAE;YACH,+CAA+C;YAC/C,kEAAkE;YAClE,qCAAqC;SACtC;KACF;IACD,mCAAmC,EAAE;QACnC,GAAG,EAAE,0KAA0K;QAC/K,GAAG,EAAE;YACH,uCAAuC;YACvC,iEAAiE;SAClE;KACF;IACD,wBAAwB,EAAE;QACxB,GAAG,EAAE,8NAA8N;QACnO,GAAG,EAAE,CAAC,iDAAiD,CAAC;KACzD;IACD,uBAAuB,EAAE;QACvB,GAAG,EAAE,8LAA8L;QACnM,GAAG,EAAE,CAAC,+CAA+C,CAAC;KACvD;IACD,YAAY,EAAE;QACZ,GAAG,EAAE,wKAAwK;QAC7K,GAAG,EAAE;YACH,4BAA4B;YAC5B,0DAA0D;YAC1D,4DAA4D;SAC7D;KACF;IACD,WAAW,EAAE;QACX,GAAG,EAAE,uKAAuK;QAC5K,GAAG,EAAE;YACH,sDAAsD;YACtD,iEAAiE;SAClE;KACF;CACF,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { Advice, ExpoDoctorResult, LockfileResult, Toolchain } from '../types.js';
2
+ export declare const TARGET_YARN = "yarn@4.18.0";
3
+ export declare const yarnMigrationSteps: (found: string[]) => string[];
4
+ export declare const toolchainAdvice: (toolchain: Toolchain, lockfile: LockfileResult, expo: ExpoDoctorResult, memoFindings?: number) => Advice[];
5
+ //# sourceMappingURL=toolchain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolchain.d.ts","sourceRoot":"","sources":["../../src/advice/toolchain.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EACN,gBAAgB,EAChB,cAAc,EACd,SAAS,EACV,MAAM,aAAa,CAAC;AAGrB,eAAO,MAAM,WAAW,gBAAgB,CAAC;AAEzC,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,EAAE,KAAG,MAAM,EAQ1D,CAAC;AAuNF,eAAO,MAAM,eAAe,GAC1B,WAAW,SAAS,EACpB,UAAU,cAAc,EACxB,MAAM,gBAAgB,EACtB,qBAAgB,KACf,MAAM,EAgBR,CAAC"}