@geonosis/oxlint-plugin-biological-architecture 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # @geonosis/oxlint-plugin-biological-architecture
2
2
 
3
- 54 architecture rules as an oxlint JS plugin: which tier a file belongs to, what it may import,
3
+ 55 architecture rules as an oxlint JS plugin: which tier a file belongs to, what it may import,
4
4
  where state is allowed to live, and the backend invariants that go with them. Nothing is hardcoded
5
- to a repo — every list of brands, features, tables or paths is an option whose default does nothing
6
- until it is configured.
5
+ to a repo — every list of brands, features, tables or paths is an option. A rule enabled without the
6
+ options it needs refuses the run rather than passing silently.
7
7
 
8
8
  ## Install
9
9
 
@@ -63,6 +63,7 @@ everything below the organelle is inert. A cell never composes another cell.
63
63
  | `backend-d1` | 2 | Cloudflare D1 facts — false on an engine with transactions |
64
64
  | `backend-medusa` | 1 | the storefront route surface |
65
65
  | `backend-sagaflow-cf` | 1 | durable runs on Cloudflare Workflows — false of an inline saga |
66
+ | `tooling` | 1 | `layer-walls` — a repo's OWN layers, declared as data and walled |
66
67
  | `comment-ceiling` | 1 | the comment posture, opt-in (+ oxlint's `capitalized-comments`) |
67
68
 
68
69
  A rule that is false on another stack gets its own preset rather than riding along in a generic
@@ -72,78 +73,76 @@ one: a preset carrying a repo fact is a repo fact wearing a preset's name.
72
73
 
73
74
  A repo that already has its own copy of these rules does not delete it because the version numbers
74
75
  match. It deletes it when the **findings** match: same tree, same rule ids, two configs, and a diff.
75
- `geonosis-lint-parity` is that diff.
76
+ [`@geonosis/lint-parity`](https://www.npmjs.com/package/@geonosis/lint-parity) is that diff — a
77
+ separate package, because a parity tool welded to one rule plugin can only ever check that plugin.
78
+ What this package ships towards it is the **evidence**: `corpus/`, the fixture tree its own gates
79
+ run against, one deliberately broken file per rule, plus a `manifest.json` generated from the
80
+ plugin's own rules export naming exactly the rules that corpus speaks for.
76
81
 
77
82
  ```bash
78
- geonosis-lint-parity --a <config-a.json> --b <config-b.json> [--oxlint <path>] [--out <dir>] -- <paths…>
83
+ pnpm add -D @geonosis/lint-parity
79
84
  ```
80
85
 
81
86
  ### Step 0 — reach, before parity
82
87
 
83
88
  Parity between two configs that both do nothing is perfect parity. In one source repo 34 of 51
84
89
  configured rules fired nowhere in any corpus it had: 34 rules a consumer would inherit as claims
85
- nothing could check. So this package ships the **reach corpus** the fixture tree its own gates run
86
- against, one deliberately broken file per rule — and answers the question directly:
90
+ nothing could check. Point the tool at the corpus this package installed:
87
91
 
88
92
  ```bash
89
- geonosis-lint-parity --corpus --a oxlintrc.fork.json --b oxlintrc.published.json [--out <dir>]
93
+ npx geonosis-lint-parity \
94
+ --corpus node_modules/@geonosis/oxlint-plugin-biological-architecture/corpus \
95
+ --a oxlintrc.fork.json --b oxlintrc.published.json --out proofs/reach
90
96
  ```
91
97
 
92
- Both configs run over the shipped corpus (copied to a temp root first, so neither a `.gitignore` nor
93
- oxlint's `node_modules` skip can quietly turn "every rule fired nowhere" into the answer). It prints
94
- a fired-under-A / fired-under-B row per configured rule and **exits 1 when a rule fires under
95
- neither**. `--out` writes `CORPUS.md`.
98
+ It prints a fired-under-A / fired-under-B row per in-scope rule and **exits 1 when one fires under
99
+ neither**. A configured `eqeqeq`, `unicorn/*` or `typescript/*` is listed under **outside this
100
+ corpus** and gets no verdict: this tree is no evidence either way about somebody else's rule.
96
101
 
97
102
  Every rule this package exports has at least one corpus file that fires it, and a test fails if that
98
103
  stops being true — so a rule cannot be added without evidence it reaches anything.
99
104
 
100
105
  ### Then parity
101
106
 
102
- It runs oxlint twice with `--format=unix` over the same paths, strips the working-directory prefix,
103
- sorts, and prints the two counts, `only in A`, `only in B` and a per-rule tally. **It exits 1 when
104
- `only in A` is non-empty** — a finding the old config caught and the new one does not. New findings
105
- are what an upgrade is for; lost ones stop the adoption. `--out` writes `a.txt`, `b.txt`,
106
- `only-in-a.txt`, `only-in-b.txt` and `SUMMARY.md` for the proof file.
107
-
108
107
  **Fork vs published**, before deleting a vendored copy — two configs identical but for `jsPlugins`:
109
108
 
110
109
  ```bash
111
110
  # oxlintrc.fork.json → "jsPlugins": ["./packages/eslint-plugin-biological-architecture"]
112
111
  # oxlintrc.published.json → "jsPlugins": ["@geonosis/oxlint-plugin-biological-architecture"]
113
- geonosis-lint-parity \
112
+ npx geonosis-lint-parity \
114
113
  --a oxlintrc.fork.json --b oxlintrc.published.json \
115
114
  --out proofs/adoption-parity \
116
115
  -- apps packages
117
116
  ```
118
117
 
119
- **Version vs version**, as the kit's own release check the same config, pointing first at the
120
- installed version and then at a local build, by absolute path:
121
-
122
- ```bash
123
- geonosis-lint-parity \
124
- --a oxlintrc.0.1.0.json --b oxlintrc.0.1.1.json \
125
- --out proofs/parity-0.1.0-vs-0.1.1 \
126
- -- apps packages
127
- ```
118
+ **Write both configs inside the workspace being linted**oxlint resolves a `jsPlugins` specifier
119
+ relative to the **config file's** directory, not the working directory. It exits 1 when a finding
120
+ config A reported is missing under config B; a reworded message is reported as **changed** and never
121
+ reaches the exit code. The tool's README has the rest.
128
122
 
129
- `--oxlint` overrides the binary; by default it walks up from the working directory for
130
- `node_modules/.bin/oxlint`, so the comparison runs under the consumer's own linter rather than one
131
- `npx` might fetch. Every difference the run reports has to trace to a decision written down in
132
- `RULES.md`; one that does not is a defect, not a difference.
123
+ Every difference a run reports has to trace to a decision written down in `RULES.md`; one that does
124
+ not is a defect, not a difference.
133
125
 
134
126
  ### What the two configs must say for the findings to match
135
127
 
136
- A fork usually had its repo's vocabulary compiled in. Here it is configuration, and a config that
137
- omits it turns the rule off which reads as a lost finding, correctly. Carry these across:
128
+ A fork usually had its repo's vocabulary compiled in. Here it is configuration and **a rule
129
+ enabled without the options it needs refuses the run**, naming itself and the missing option, rather
130
+ than quietly finding nothing. That is deliberate: a rule enabled at `"error"` with nothing to
131
+ compare against is indistinguishable from a clean tree, and one consumer shipped exactly that for
132
+ months. Carry these across, or turn the rule off:
138
133
 
139
- | Rule | Option | Without it |
134
+ | Rule | Option | Enabled without it |
140
135
  | --- | --- | --- |
141
- | `no-brand-names` | `{ brands, allowedSubstrings }` | does nothing |
136
+ | `no-brand-names` | `{ brands, allowedSubstrings }` | refuses the run |
142
137
  | `organelle-dependency` | `{ sharedFeatures }` | only `shared` crosses a feature boundary |
143
- | `documents-share-one-table` | `{ perKindTables }` | does nothing |
144
- | `document-sagas-are-generic` | `{ perKindPrefixes }` | does nothing |
145
- | `step-opens-its-own-cell` | `{ engine, within, wrapper }` | does nothing |
146
- | `no-raw-html-atoms` | the positional element→atom map | does nothing |
138
+ | `documents-share-one-table` | `{ perKindTables }` | refuses the run |
139
+ | `document-sagas-are-generic` | `{ perKindPrefixes }` | refuses the run |
140
+ | `step-opens-its-own-cell` | `{ engine, within, wrapper }` | refuses the run |
141
+ | `no-raw-html-atoms` | the positional element→atom map | refuses the run |
142
+ | `layer-walls` | `{ layers: [{ name, paths, mayImport }], externals? }` | refuses the run |
143
+
144
+ An empty list counts as unconfigured. A rule set to `"off"`, or never named, is untouched — "does
145
+ nothing until configured" survives for a rule nobody enabled, which is oxlint's job, not ours.
147
146
 
148
147
  Two more facts a fork's config will trip on:
149
148
 
@@ -35,6 +35,39 @@
35
35
  }
36
36
  ],
37
37
  "biological-architecture/effect-hook-naming": "error",
38
+ "biological-architecture/layer-walls": [
39
+ "error",
40
+ {
41
+ "layers": [
42
+ {
43
+ "name": "core",
44
+ "paths": [
45
+ "(?:^|/)layers/core/"
46
+ ],
47
+ "mayImport": []
48
+ },
49
+ {
50
+ "name": "feature",
51
+ "paths": [
52
+ "(?:^|/)layers/feature/"
53
+ ],
54
+ "mayImport": [
55
+ "core"
56
+ ]
57
+ },
58
+ {
59
+ "name": "shell",
60
+ "paths": [
61
+ "(?:^|/)layers/shell/"
62
+ ],
63
+ "mayImport": [
64
+ "core",
65
+ "feature"
66
+ ]
67
+ }
68
+ ]
69
+ }
70
+ ],
38
71
  "biological-architecture/max-comment-density": "error",
39
72
  "biological-architecture/molecule-atoms-only": "error",
40
73
  "biological-architecture/molecule-must-compose": "error",
@@ -0,0 +1,3 @@
1
+ import { helper } from '../feature/sibling'
2
+
3
+ export const reachesUp = () => helper()
@@ -0,0 +1 @@
1
+ export const thing = () => 'core'
@@ -0,0 +1,3 @@
1
+ import { boot } from '../shell/boot'
2
+
3
+ export const crosses = () => boot()
@@ -0,0 +1,3 @@
1
+ import { thing } from '../core/thing'
2
+
3
+ export const downward = () => thing()
@@ -0,0 +1,3 @@
1
+ import { helper } from './sibling'
2
+
3
+ export const sameLayer = () => helper()
@@ -0,0 +1,3 @@
1
+ import type { Boot } from '../shell/boot'
2
+
3
+ export const describeBoot = (boot: Boot): string => boot.name
@@ -0,0 +1 @@
1
+ export const helper = () => 'feature'
@@ -0,0 +1,3 @@
1
+ export type Boot = { name: string }
2
+
3
+ export const boot = () => 'shell'
@@ -0,0 +1,60 @@
1
+ {
2
+ "plugin": "biological-architecture",
3
+ "rules": [
4
+ "biological-architecture/atom-no-deps",
5
+ "biological-architecture/cell-must-be-stateful",
6
+ "biological-architecture/cell-must-not-compose-cell",
7
+ "biological-architecture/cell-no-tissues",
8
+ "biological-architecture/cells-folder-index-is-barrel",
9
+ "biological-architecture/compound-must-be-stateless",
10
+ "biological-architecture/compound-no-stores",
11
+ "biological-architecture/constants-in-constants-file",
12
+ "biological-architecture/dialect-through-the-seam",
13
+ "biological-architecture/document-sagas-are-generic",
14
+ "biological-architecture/documents-share-one-table",
15
+ "biological-architecture/effect-hook-naming",
16
+ "biological-architecture/layer-walls",
17
+ "biological-architecture/max-comment-density",
18
+ "biological-architecture/molecule-atoms-only",
19
+ "biological-architecture/molecule-must-compose",
20
+ "biological-architecture/next-route-segment-is-thin-delegate",
21
+ "biological-architecture/no-brand-names",
22
+ "biological-architecture/no-card-shaped-div",
23
+ "biological-architecture/no-cross-feature-stores",
24
+ "biological-architecture/no-d1-transaction",
25
+ "biological-architecture/no-duplicate-jsx-patterns",
26
+ "biological-architecture/no-hook-in-component-disguise",
27
+ "biological-architecture/no-inert-hidden-jsx",
28
+ "biological-architecture/no-inline-data-in-jsx",
29
+ "biological-architecture/no-invalid-feature-folders",
30
+ "biological-architecture/no-logic-in-component-files",
31
+ "biological-architecture/no-orm-outside-db",
32
+ "biological-architecture/no-raw-html-atoms",
33
+ "biological-architecture/no-raw-sql-outside-allowed",
34
+ "biological-architecture/no-react-namespace",
35
+ "biological-architecture/no-renamed-html-props",
36
+ "biological-architecture/no-render-prop-reader",
37
+ "biological-architecture/no-trivial-wrapper-component",
38
+ "biological-architecture/no-ts-in-bio-folders",
39
+ "biological-architecture/no-type-definitions-in-components",
40
+ "biological-architecture/no-void-port",
41
+ "biological-architecture/organelle-dependency",
42
+ "biological-architecture/organelle-single-source",
43
+ "biological-architecture/queries-require-org-scope",
44
+ "biological-architecture/queue-loop-is-the-library",
45
+ "biological-architecture/ssot-no-inline-facts",
46
+ "biological-architecture/ssot-no-process-env",
47
+ "biological-architecture/step-opens-its-own-cell",
48
+ "biological-architecture/store-route-scopes-tenant-data",
49
+ "biological-architecture/tables-declare-their-plane",
50
+ "biological-architecture/tenant-tables-carry-org-id",
51
+ "biological-architecture/time-through-the-door",
52
+ "biological-architecture/tissue-must-compose",
53
+ "biological-architecture/tissue-no-data-props",
54
+ "biological-architecture/tissue-no-hooks",
55
+ "biological-architecture/tissue-no-organelles",
56
+ "biological-architecture/tissue-no-stores",
57
+ "biological-architecture/worker-handles-are-scoped",
58
+ "biological-architecture/zustand-v5-best-practices"
59
+ ]
60
+ }
@@ -59,6 +59,7 @@ var TENANT_SCOPING = [
59
59
  "tables-declare-their-plane",
60
60
  "tenant-tables-carry-org-id"
61
61
  ];
62
+ var TOOLING = ["layer-walls"];
62
63
  var RULE_PREFIX = "biological-architecture";
63
64
  var asRules = (names) => Object.fromEntries(names.map((name) => [`${RULE_PREFIX}/${name}`, "error"]));
64
65
  var COMMENT_CEILING = {
@@ -75,7 +76,8 @@ var PRESETS = {
75
76
  "comment-ceiling": COMMENT_CEILING,
76
77
  frontend: { rules: asRules(FRONTEND) },
77
78
  recommended: { rules: asRules(FRONTEND) },
78
- "tenant-scoping": { rules: asRules(TENANT_SCOPING) }
79
+ "tenant-scoping": { rules: asRules(TENANT_SCOPING) },
80
+ tooling: { rules: asRules(TOOLING) }
79
81
  };
80
82
  var rulesOfPreset = (preset) => Object.keys(PRESETS[preset].rules).filter((id) => id.startsWith(`${RULE_PREFIX}/`)).map((id) => id.slice(RULE_PREFIX.length + 1));
81
83
 
@@ -0,0 +1,39 @@
1
+ interface AtomMapping {
2
+ atomFile: string;
3
+ atomName: string;
4
+ nativeElement: string;
5
+ }
6
+ interface AtomRestriction {
7
+ atom: string;
8
+ element: string;
9
+ importPath: string;
10
+ }
11
+ interface RestrictedSyntaxEntry {
12
+ message: string;
13
+ selector: string;
14
+ }
15
+ /**
16
+ * Build no-restricted-syntax entries that forbid raw native HTML usage
17
+ * for every native element wrapped by an atom. Apply these entries to
18
+ * molecules, compounds, cells, and tissues (NOT atoms themselves).
19
+ */
20
+ declare function buildAtomRestrictions(atomsDir: string, importPathPrefix?: string): RestrictedSyntaxEntry[];
21
+ /**
22
+ * Build atom restrictions in the format expected by the no-raw-html-atoms rule.
23
+ * This replaces no-restricted-syntax for linters that don't support it (e.g. oxlint).
24
+ */
25
+ declare function buildAtomRuleRestrictions(atomsDir: string, importPathPrefix?: string): AtomRestriction[];
26
+ /**
27
+ * Scan the atoms directory and build a mapping from native HTML element
28
+ * to the atom that wraps it. This is used to generate no-restricted-syntax
29
+ * entries that forbid raw native HTML usage in higher-level components.
30
+ *
31
+ * When multiple atoms wrap the same element (e.g. `input.tsx` and
32
+ * `inline-input.tsx` both wrap `<input>`), the atom whose filename
33
+ * exactly matches the element name is preferred as the canonical one.
34
+ * Variants are kept as alternatives in the error message.
35
+ */
36
+ declare function scanAtomMappings(atomsDir: string): AtomMapping[];
37
+ declare function detectFirstNativeJsxElement(source: string): null | string;
38
+
39
+ export { buildAtomRestrictions, buildAtomRuleRestrictions, detectFirstNativeJsxElement, scanAtomMappings };
@@ -0,0 +1,54 @@
1
+ import { Preset } from './presets.js';
2
+ export { PRESETS, PresetName, rulesOfPreset } from './presets.js';
3
+
4
+ type Rule = {
5
+ create: (context: never) => Record<string, unknown>;
6
+ fixShape: string;
7
+ meta: {
8
+ docs: {
9
+ description: string;
10
+ };
11
+ messages: Record<string, string>;
12
+ schema: unknown[];
13
+ type: string;
14
+ };
15
+ };
16
+ declare const rules: Record<string, Rule>;
17
+ type RuleName = string;
18
+
19
+ declare const plugin: {
20
+ configs: {
21
+ 'backend-d1': {
22
+ rules: Record<string, "error">;
23
+ };
24
+ 'backend-medusa': {
25
+ rules: Record<string, "error">;
26
+ };
27
+ 'backend-sagaflow-cf': {
28
+ rules: Record<string, "error">;
29
+ };
30
+ 'backend-workflows': {
31
+ rules: Record<string, "error">;
32
+ };
33
+ 'comment-ceiling': Preset;
34
+ frontend: {
35
+ rules: Record<string, "error">;
36
+ };
37
+ recommended: {
38
+ rules: Record<string, "error">;
39
+ };
40
+ 'tenant-scoping': {
41
+ rules: Record<string, "error">;
42
+ };
43
+ tooling: {
44
+ rules: Record<string, "error">;
45
+ };
46
+ };
47
+ meta: {
48
+ name: string;
49
+ version: string;
50
+ };
51
+ rules: Record<string, Rule>;
52
+ };
53
+
54
+ export { Preset, type Rule, type RuleName, plugin as default, rules };