@geonosis/oxlint-plugin-biological-architecture 0.1.2 → 0.2.1
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 +39 -61
- package/corpus/.oxlintrc.json +33 -0
- package/corpus/layers/core/bad-reaches-up.ts +3 -0
- package/corpus/layers/core/thing.ts +1 -0
- package/corpus/layers/feature/bad-relative.ts +3 -0
- package/corpus/layers/feature/good-downward.ts +3 -0
- package/corpus/layers/feature/good-same-layer.ts +3 -0
- package/corpus/layers/feature/good-type-only.ts +3 -0
- package/corpus/layers/feature/sibling.ts +1 -0
- package/corpus/layers/shell/boot.ts +3 -0
- package/corpus/manifest.json +1 -0
- package/dist/{chunk-74VWOEPL.js → chunk-HXBZSOIU.js} +3 -1
- package/dist/discover-atoms.d.ts +39 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +159 -24
- package/dist/presets.d.ts +36 -0
- package/dist/presets.js +1 -1
- package/package.json +5 -8
- package/bin/geonosis-lint-parity.mjs +0 -4
- package/dist/chunk-2VSZAP5M.js +0 -240
- package/dist/chunk-BFFWNFJM.js +0 -6
- package/dist/chunk-BGX2XYEL.js +0 -153
- package/dist/chunk-FGUOGKA5.js +0 -228
- package/dist/chunk-FJ3J7B5J.js +0 -155
- package/dist/chunk-KZ5XEK6F.js +0 -83
- package/dist/chunk-NZVMV3Y5.js +0 -339
- package/dist/chunk-RNY5NDH5.js +0 -155
- package/dist/chunk-SUTNA6YD.js +0 -268
- package/dist/chunk-UM6UIUH3.js +0 -250
- package/dist/chunk-YIAUS4GG.js +0 -298
- package/dist/parity-cli.js +0 -84
- package/dist/parity.js +0 -32
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# @geonosis/oxlint-plugin-biological-architecture
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
6
|
-
|
|
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,99 +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
|
|
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
|
-
|
|
83
|
+
pnpm add -D @geonosis/lint-parity
|
|
79
84
|
```
|
|
80
85
|
|
|
81
|
-
**Write both configs inside the workspace being linted** — `<ws>/.oxlintrc.parity-a.json`, deleted
|
|
82
|
-
afterwards — or name the plugin by absolute path in `jsPlugins`. oxlint resolves a `jsPlugins`
|
|
83
|
-
specifier relative to the **config file's** directory, not the working directory, so a config
|
|
84
|
-
written to a temp dir cannot find a plugin installed in the workspace: it fails with
|
|
85
|
-
`Cannot find module '@…/plugin'`, and the tool correctly refuses with
|
|
86
|
-
`config A: oxlint exited 1 without reporting a single finding`.
|
|
87
|
-
|
|
88
86
|
### Step 0 — reach, before parity
|
|
89
87
|
|
|
90
88
|
Parity between two configs that both do nothing is perfect parity. In one source repo 34 of 51
|
|
91
89
|
configured rules fired nowhere in any corpus it had: 34 rules a consumer would inherit as claims
|
|
92
|
-
nothing could check.
|
|
93
|
-
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:
|
|
94
91
|
|
|
95
92
|
```bash
|
|
96
|
-
geonosis-lint-parity
|
|
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
|
|
97
96
|
```
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
`--out` writes `CORPUS.md`.
|
|
103
|
-
|
|
104
|
-
**A corpus covers the rules its own `manifest.json` names, and only those.** The one this package
|
|
105
|
-
ships names `biological-architecture/*` — one deliberately broken file per rule, the list generated
|
|
106
|
-
from the plugin's own rules export at build time. A configured `eqeqeq`, `unicorn/*` or
|
|
107
|
-
`typescript/*` is listed under **outside this corpus** and gets no verdict: this tree is no evidence
|
|
108
|
-
either way about somebody else's rule, and judging one would fail every real config for saying
|
|
109
|
-
nothing. `--corpus <dir>` points the same run at another plugin's corpus; the scope moves with it.
|
|
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.
|
|
110
101
|
|
|
111
102
|
Every rule this package exports has at least one corpus file that fires it, and a test fails if that
|
|
112
103
|
stops being true — so a rule cannot be added without evidence it reaches anything.
|
|
113
104
|
|
|
114
105
|
### Then parity
|
|
115
106
|
|
|
116
|
-
It runs oxlint twice with `--format=unix` over the same paths, strips the working-directory prefix,
|
|
117
|
-
sorts, and prints the two counts, `only in A`, `only in B` and a per-rule tally. **It exits 1 when
|
|
118
|
-
`only in A` is non-empty** — a finding the old config caught and the new one does not. New findings
|
|
119
|
-
are what an upgrade is for; lost ones stop the adoption. `--out` writes `a.txt`, `b.txt`,
|
|
120
|
-
`only-in-a.txt`, `only-in-b.txt`, `changed.txt` and `SUMMARY.md` for the proof file.
|
|
121
|
-
|
|
122
|
-
**A finding is identified by where it is and which rule found it — `path:line:col` plus
|
|
123
|
-
`plugin(rule)`.** The message is not part of it, and neither is the severity. A release that only
|
|
124
|
-
rewords a rule's message would otherwise show as one finding lost and one gained, and stop an
|
|
125
|
-
adoption over a sentence. Those pair up instead under **changed**, reported in full and never
|
|
126
|
-
reaching the exit code; a severity change lands there too, because that is a fact about the config
|
|
127
|
-
rather than a rule that stopped firing.
|
|
128
|
-
|
|
129
107
|
**Fork vs published**, before deleting a vendored copy — two configs identical but for `jsPlugins`:
|
|
130
108
|
|
|
131
109
|
```bash
|
|
132
110
|
# oxlintrc.fork.json → "jsPlugins": ["./packages/eslint-plugin-biological-architecture"]
|
|
133
111
|
# oxlintrc.published.json → "jsPlugins": ["@geonosis/oxlint-plugin-biological-architecture"]
|
|
134
|
-
geonosis-lint-parity \
|
|
112
|
+
npx geonosis-lint-parity \
|
|
135
113
|
--a oxlintrc.fork.json --b oxlintrc.published.json \
|
|
136
114
|
--out proofs/adoption-parity \
|
|
137
115
|
-- apps packages
|
|
138
116
|
```
|
|
139
117
|
|
|
140
|
-
**
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
geonosis-lint-parity \
|
|
145
|
-
--a oxlintrc.0.1.0.json --b oxlintrc.0.1.1.json \
|
|
146
|
-
--out proofs/parity-0.1.0-vs-0.1.1 \
|
|
147
|
-
-- apps packages
|
|
148
|
-
```
|
|
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.
|
|
149
122
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
`npx` might fetch. Every difference the run reports has to trace to a decision written down in
|
|
153
|
-
`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.
|
|
154
125
|
|
|
155
126
|
### What the two configs must say for the findings to match
|
|
156
127
|
|
|
157
|
-
A fork usually had its repo's vocabulary compiled in. Here it is configuration
|
|
158
|
-
|
|
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:
|
|
159
133
|
|
|
160
|
-
| Rule | Option |
|
|
134
|
+
| Rule | Option | Enabled without it |
|
|
161
135
|
| --- | --- | --- |
|
|
162
|
-
| `no-brand-names` | `{ brands, allowedSubstrings }` |
|
|
136
|
+
| `no-brand-names` | `{ brands, allowedSubstrings }` | refuses the run |
|
|
163
137
|
| `organelle-dependency` | `{ sharedFeatures }` | only `shared` crosses a feature boundary |
|
|
164
|
-
| `documents-share-one-table` | `{ perKindTables }` |
|
|
165
|
-
| `document-sagas-are-generic` | `{ perKindPrefixes }` |
|
|
166
|
-
| `step-opens-its-own-cell` | `{ engine, within, wrapper }` |
|
|
167
|
-
| `no-raw-html-atoms` | the positional element→atom map |
|
|
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.
|
|
168
146
|
|
|
169
147
|
Two more facts a fork's config will trip on:
|
|
170
148
|
|
package/corpus/.oxlintrc.json
CHANGED
|
@@ -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 @@
|
|
|
1
|
+
export const thing = () => 'core'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const helper = () => 'feature'
|
package/corpus/manifest.json
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"biological-architecture/document-sagas-are-generic",
|
|
14
14
|
"biological-architecture/documents-share-one-table",
|
|
15
15
|
"biological-architecture/effect-hook-naming",
|
|
16
|
+
"biological-architecture/layer-walls",
|
|
16
17
|
"biological-architecture/max-comment-density",
|
|
17
18
|
"biological-architecture/molecule-atoms-only",
|
|
18
19
|
"biological-architecture/molecule-must-compose",
|
|
@@ -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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
PRESETS
|
|
3
|
-
|
|
2
|
+
PRESETS,
|
|
3
|
+
rulesOfPreset
|
|
4
|
+
} from "./chunk-HXBZSOIU.js";
|
|
4
5
|
|
|
5
6
|
// src/index.ts
|
|
6
7
|
import { createRequire } from "module";
|
|
@@ -142,7 +143,7 @@ import * as path from "path";
|
|
|
142
143
|
|
|
143
144
|
// src/rules/lib/cells-barrel.ts
|
|
144
145
|
var CELLS_BARREL_FILE = /(?:^|\/)cells\/(?:index\.ts|dynamic\.tsx?)$/;
|
|
145
|
-
var isCellsBarrel = (
|
|
146
|
+
var isCellsBarrel = (path3) => CELLS_BARREL_FILE.test(path3);
|
|
146
147
|
|
|
147
148
|
// src/rules/cell-must-not-compose-cell.ts
|
|
148
149
|
function cellStem(filePath) {
|
|
@@ -566,12 +567,27 @@ var literalNamedBy = (node, callee) => {
|
|
|
566
567
|
return name?.type === "Literal" && typeof name.value === "string" ? name.value : null;
|
|
567
568
|
};
|
|
568
569
|
|
|
570
|
+
// src/rules/lib/options.ts
|
|
571
|
+
var NOTHING = "nothing";
|
|
572
|
+
var isMissing = (value) => value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
|
|
573
|
+
var requireOption = (rule, value, option) => {
|
|
574
|
+
if (isMissing(value)) {
|
|
575
|
+
throw new Error(
|
|
576
|
+
`biological-architecture/${rule} is enabled but its \`${option}\` option is ${NOTHING}, so it can never fire. Configure \`${option}\`, or turn the rule off \u2014 an enabled rule that cannot fire reads exactly like a rule that found nothing.`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
return value;
|
|
580
|
+
};
|
|
581
|
+
|
|
569
582
|
// src/rules/document-sagas-are-generic.ts
|
|
570
583
|
var escapeForRegex = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
571
584
|
var documentSagasAreGeneric = {
|
|
572
585
|
create(context) {
|
|
573
|
-
const prefixes =
|
|
574
|
-
|
|
586
|
+
const prefixes = requireOption(
|
|
587
|
+
"document-sagas-are-generic",
|
|
588
|
+
context.options?.[0]?.perKindPrefixes,
|
|
589
|
+
"perKindPrefixes"
|
|
590
|
+
);
|
|
575
591
|
if (!/\/packages\/workflows\/src\//.test(context.filename.replaceAll("\\", "/"))) return {};
|
|
576
592
|
const perKindPrefix = new RegExp(`^(?:${prefixes.map(escapeForRegex).join("|")})\\.`);
|
|
577
593
|
return {
|
|
@@ -584,7 +600,8 @@ var documentSagasAreGeneric = {
|
|
|
584
600
|
},
|
|
585
601
|
fixShape: `A saga named for one document kind (\`invoice.issue\`, \`quote.accept\`) is the hand-written fork the
|
|
586
602
|
generic base entity replaced. Mint it from the generic factory \u2014 \`documentSagas(entity, effects)\` \u2014
|
|
587
|
-
and pass the kind as data. Configure the kind vocabulary through the rule's \`perKindPrefixes\` option
|
|
603
|
+
and pass the kind as data. Configure the kind vocabulary through the rule's \`perKindPrefixes\` option; enabled without it the
|
|
604
|
+
rule refuses the run.`,
|
|
588
605
|
meta: {
|
|
589
606
|
docs: {
|
|
590
607
|
description: "Document sagas are minted, not written: `documentSagas(entity, effects)` gives every entity that extends Document its create/save/setStatus/remove/send/convert/duplicate/share from one body. A saga literally named for one entity is a fork."
|
|
@@ -610,8 +627,13 @@ var document_sagas_are_generic_default = documentSagasAreGeneric;
|
|
|
610
627
|
var TABLE_CALLEES = ["sqliteTable", "pgTable"];
|
|
611
628
|
var documentsShareOneTable = {
|
|
612
629
|
create(context) {
|
|
613
|
-
const perKindTables = new Set(
|
|
614
|
-
|
|
630
|
+
const perKindTables = new Set(
|
|
631
|
+
requireOption(
|
|
632
|
+
"documents-share-one-table",
|
|
633
|
+
context.options?.[0]?.perKindTables,
|
|
634
|
+
"perKindTables"
|
|
635
|
+
)
|
|
636
|
+
);
|
|
615
637
|
if (!/\/packages\/db\/src\/schema\//.test(context.filename.replaceAll("\\", "/"))) return {};
|
|
616
638
|
return {
|
|
617
639
|
CallExpression(node) {
|
|
@@ -627,8 +649,9 @@ var documentsShareOneTable = {
|
|
|
627
649
|
fixShape: `Every document kind is a row of the one \`documents\` table with a \`kind\` column, not a table of its
|
|
628
650
|
own. A \`pgTable('invoices', \u2026)\` beside a \`pgTable('quotes', \u2026)\` is the second storage the base entity
|
|
629
651
|
retired: write the row, not the table. Both constructors count \u2014 \`pgTable\` and \`sqliteTable\` \u2014 since
|
|
630
|
-
the dialect is a seam fact, not a reason for a kind to own storage. The forbidden table names are
|
|
631
|
-
the rule
|
|
652
|
+
the dialect is a seam fact, not a reason for a kind to own storage. The forbidden table names are the rule's
|
|
653
|
+
\`perKindTables\` option; enabled without it the rule refuses the run rather than passing every
|
|
654
|
+
schema silently.`,
|
|
632
655
|
meta: {
|
|
633
656
|
docs: {
|
|
634
657
|
description: "Documents share ONE table: `documents` holds the head every kind derives and the body its kind declares. A table named for one kind of document is a second storage."
|
|
@@ -720,6 +743,109 @@ file and the export together \u2014 half the pair still fires.`,
|
|
|
720
743
|
};
|
|
721
744
|
var effect_hook_naming_default = effectHookNaming;
|
|
722
745
|
|
|
746
|
+
// src/rules/layer-walls.ts
|
|
747
|
+
import * as path2 from "path";
|
|
748
|
+
var REQUIRE = "require";
|
|
749
|
+
var isRelative = (source) => source.startsWith("./") || source.startsWith("../");
|
|
750
|
+
var isExternal = (source) => !isRelative(source) && !source.startsWith("/");
|
|
751
|
+
var layerWalls = {
|
|
752
|
+
create(context) {
|
|
753
|
+
const { externals = "allow", layers = [] } = context.options?.[0] ?? {};
|
|
754
|
+
requireOption("layer-walls", layers, "layers");
|
|
755
|
+
const compiled = layers.map((layer) => ({
|
|
756
|
+
mayImport: new Set(layer.mayImport ?? []),
|
|
757
|
+
name: layer.name,
|
|
758
|
+
paths: layer.paths.map((source) => new RegExp(source))
|
|
759
|
+
}));
|
|
760
|
+
const layerOf = (pathish) => compiled.find((layer) => layer.paths.some((pattern) => pattern.test(pathish)));
|
|
761
|
+
const filename = context.filename.replaceAll(/\\/g, "/");
|
|
762
|
+
const here = layerOf(filename);
|
|
763
|
+
if (here === void 0) return {};
|
|
764
|
+
const dir = path2.posix.dirname(filename);
|
|
765
|
+
const file = filename.split("/").pop() ?? filename;
|
|
766
|
+
const allowed = here.mayImport.size === 0 ? "nothing" : [...here.mayImport].toSorted().join(", ");
|
|
767
|
+
const check = (source, node) => {
|
|
768
|
+
const target = layerOf(source) ?? (isRelative(source) ? layerOf(path2.posix.normalize(path2.posix.join(dir, source))) : void 0);
|
|
769
|
+
if (target === void 0) {
|
|
770
|
+
if (externals === "deny" && isExternal(source)) {
|
|
771
|
+
context.report({ data: { file, from: here.name, source }, messageId: "external", node });
|
|
772
|
+
}
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (target.name === here.name || here.mayImport.has(target.name)) return;
|
|
776
|
+
context.report({
|
|
777
|
+
data: { allowed, file, from: here.name, source, to: target.name },
|
|
778
|
+
messageId: "crossed",
|
|
779
|
+
node
|
|
780
|
+
});
|
|
781
|
+
};
|
|
782
|
+
const fromModule = (node) => {
|
|
783
|
+
if (!node.source) return;
|
|
784
|
+
if (node.importKind === "type" || node.exportKind === "type") return;
|
|
785
|
+
check(node.source.value, node);
|
|
786
|
+
};
|
|
787
|
+
return {
|
|
788
|
+
CallExpression(node) {
|
|
789
|
+
const source = literalNamedBy(node, REQUIRE);
|
|
790
|
+
if (source !== null) check(source, node);
|
|
791
|
+
},
|
|
792
|
+
ExportAllDeclaration: fromModule,
|
|
793
|
+
ExportNamedDeclaration: fromModule,
|
|
794
|
+
ImportDeclaration: fromModule,
|
|
795
|
+
ImportExpression(node) {
|
|
796
|
+
const source = node.source;
|
|
797
|
+
if (source?.type === "Literal" && typeof source.value === "string") {
|
|
798
|
+
check(source.value, node);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
},
|
|
803
|
+
fixShape: `Layers are declared in the rule's \`layers\` option \u2014 \`{ name, paths, mayImport }\` \u2014 and a file may
|
|
804
|
+
import its own layer plus the layers it names, nothing else. When this fires, MOVE something: the
|
|
805
|
+
code the wrong side wanted goes down into the layer both sides may import, or the file itself
|
|
806
|
+
belongs in the layer it keeps reaching for. Widening \`mayImport\` to make the diagnostic stop is
|
|
807
|
+
deleting the wall, and the wall is the only reason the layers are worth naming.
|
|
808
|
+
|
|
809
|
+
A relative source is resolved against the importing file before it is judged, so \`../../shell/boot\`
|
|
810
|
+
is the same breach as naming the layer outright. Type-only imports and re-exports are exempt; a
|
|
811
|
+
package or a \`node:\` builtin is not a wall unless \`externals\` is \`"deny"\`. A file in no declared
|
|
812
|
+
layer is unconstrained, so a repo can wall one corner of its tree and leave the rest alone. Enabled
|
|
813
|
+
with no \`layers\` at all the rule refuses the run: walls nobody declared are not walls held.`,
|
|
814
|
+
meta: {
|
|
815
|
+
docs: {
|
|
816
|
+
description: "Import direction between a repo's own declared layers. Each layer names the files it owns (regex sources) and the layers it may import; a file may always import its own layer. Relative sources are resolved against the importing file first. Enabled with no layers configured it refuses the run."
|
|
817
|
+
},
|
|
818
|
+
messages: {
|
|
819
|
+
crossed: '"{{file}}" is in layer "{{from}}", which may not import layer "{{to}}" \u2014 and "{{source}}" is in it. "{{from}}" may import: {{allowed}}. Move the shared code down into a layer both may import, or move this file into the layer it keeps reaching for; widening the wall deletes the only thing the layers were for.',
|
|
820
|
+
external: '"{{file}}" is in layer "{{from}}", which declares its externals denied, and "{{source}}" is outside every declared layer. Reach it through a layer that owns it, or allow externals for this configuration.'
|
|
821
|
+
},
|
|
822
|
+
schema: [
|
|
823
|
+
{
|
|
824
|
+
additionalProperties: false,
|
|
825
|
+
properties: {
|
|
826
|
+
externals: { enum: ["allow", "deny"], type: "string" },
|
|
827
|
+
layers: {
|
|
828
|
+
items: {
|
|
829
|
+
additionalProperties: false,
|
|
830
|
+
properties: {
|
|
831
|
+
mayImport: { items: { type: "string" }, type: "array" },
|
|
832
|
+
name: { type: "string" },
|
|
833
|
+
paths: { items: { type: "string" }, type: "array" }
|
|
834
|
+
},
|
|
835
|
+
required: ["name", "paths"],
|
|
836
|
+
type: "object"
|
|
837
|
+
},
|
|
838
|
+
type: "array"
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
type: "object"
|
|
842
|
+
}
|
|
843
|
+
],
|
|
844
|
+
type: "problem"
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
var layer_walls_default = layerWalls;
|
|
848
|
+
|
|
723
849
|
// src/rules/max-comment-density.ts
|
|
724
850
|
var exemptFiles2 = [
|
|
725
851
|
/\/__tests__\//,
|
|
@@ -1117,7 +1243,7 @@ var escapeForRegex2 = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
|
1117
1243
|
var noBrandNames = {
|
|
1118
1244
|
create(context) {
|
|
1119
1245
|
const { allowedSubstrings = [], brands = [] } = context.options?.[0] ?? {};
|
|
1120
|
-
|
|
1246
|
+
requireOption("no-brand-names", brands, "brands");
|
|
1121
1247
|
const filename = context.filename.replaceAll(/\\/g, "/");
|
|
1122
1248
|
if (EXEMPT_PATH.test(filename)) return {};
|
|
1123
1249
|
const brandPattern = new RegExp(`\\b(?:${brands.map(escapeForRegex2).join("|")})\\b`, "i");
|
|
@@ -1158,12 +1284,13 @@ var noBrandNames = {
|
|
|
1158
1284
|
};
|
|
1159
1285
|
},
|
|
1160
1286
|
fixShape: `The kit is brand-agnostic: no brand identity in its source \u2014 not in identifiers, strings, copy or
|
|
1161
|
-
comments. Brand names live in the demo/example layer. Configure the rule's \`brands\` list
|
|
1162
|
-
|
|
1163
|
-
|
|
1287
|
+
comments. Brand names live in the demo/example layer. Configure the rule's \`brands\` list and put real
|
|
1288
|
+
demo-asset hosts in \`allowedSubstrings\` so a CDN URL never trips it. Enabled with no \`brands\`, the
|
|
1289
|
+
rule refuses the run \u2014 configure it or turn it off, because a brand rule that knows no brands reads
|
|
1290
|
+
exactly like a clean tree.`,
|
|
1164
1291
|
meta: {
|
|
1165
1292
|
docs: {
|
|
1166
|
-
description: "No brand identity in a brand-agnostic kit. Brand names (in code, copy, or comments) belong in the examples/demo layer, not the kit source. Configure the brand list and any allowed demo-asset substrings through the rule options; with no brands
|
|
1293
|
+
description: "No brand identity in a brand-agnostic kit. Brand names (in code, copy, or comments) belong in the examples/demo layer, not the kit source. Configure the brand list and any allowed demo-asset substrings through the rule options; enabled with no brands it refuses the run."
|
|
1167
1294
|
},
|
|
1168
1295
|
messages: {
|
|
1169
1296
|
brandLeak: 'Brand name "{{brand}}" leaked into the kit at line {{line}}. The kit is brand-agnostic \u2014 move brand identity to the examples/demo layer.'
|
|
@@ -1988,8 +2115,11 @@ var no_orm_outside_db_default = noOrmOutsideDb;
|
|
|
1988
2115
|
// src/rules/no-raw-html-atoms.ts
|
|
1989
2116
|
var noRawHtmlAtoms = {
|
|
1990
2117
|
create(context) {
|
|
1991
|
-
const restrictions =
|
|
1992
|
-
|
|
2118
|
+
const restrictions = requireOption(
|
|
2119
|
+
"no-raw-html-atoms",
|
|
2120
|
+
context.options[0],
|
|
2121
|
+
"element\u2192atom map"
|
|
2122
|
+
);
|
|
1993
2123
|
const elementMap = /* @__PURE__ */ new Map();
|
|
1994
2124
|
for (const r of restrictions) {
|
|
1995
2125
|
elementMap.set(r.element, r);
|
|
@@ -2014,8 +2144,9 @@ var noRawHtmlAtoms = {
|
|
|
2014
2144
|
};
|
|
2015
2145
|
},
|
|
2016
2146
|
fixShape: `A native element that already has an atom wrapper (\`<button>\`, \`<input>\`, \`<td>\`, \`<header>\`, \u2026) is
|
|
2017
|
-
written as the atom in levels 2-6. Import the atom named in the rule's options and use it. Configure
|
|
2018
|
-
the
|
|
2147
|
+
written as the atom in levels 2-6. Import the atom named in the rule's options and use it. Configure the element\u2192atom map per repo;
|
|
2148
|
+
enabled without one the rule refuses the run, because a rule with no map to check reads exactly like
|
|
2149
|
+
a tree with no raw elements in it.`,
|
|
2019
2150
|
meta: {
|
|
2020
2151
|
docs: {
|
|
2021
2152
|
description: "Forbid raw native HTML elements when an atom wrapper exists. Use the atom the options name instead."
|
|
@@ -3001,12 +3132,14 @@ can be misspelled and a second default nobody agreed to.`,
|
|
|
3001
3132
|
var ssot_no_process_env_default = ssotNoProcessEnv;
|
|
3002
3133
|
|
|
3003
3134
|
// src/rules/step-opens-its-own-cell.ts
|
|
3135
|
+
var RULE = "step-opens-its-own-cell";
|
|
3004
3136
|
var STEP = "step";
|
|
3005
3137
|
var readable = (pattern) => pattern.replaceAll("\\", "").replace(/\$$/, "");
|
|
3006
3138
|
var stepOpensItsOwnCell = {
|
|
3007
3139
|
create(context) {
|
|
3008
|
-
const {
|
|
3009
|
-
|
|
3140
|
+
const { wrapper } = context.options?.[0] ?? {};
|
|
3141
|
+
const engine = requireOption(RULE, context.options?.[0]?.engine, "engine");
|
|
3142
|
+
const within = requireOption(RULE, context.options?.[0]?.within, "within");
|
|
3010
3143
|
const filename = context.filename.replaceAll("\\", "/");
|
|
3011
3144
|
if (!new RegExp(within).test(filename)) return {};
|
|
3012
3145
|
if (wrapper !== void 0 && new RegExp(wrapper).test(filename)) return {};
|
|
@@ -3036,10 +3169,10 @@ var stepOpensItsOwnCell = {
|
|
|
3036
3169
|
package. A durable run hibernates and retries between steps, so each step body must open the
|
|
3037
3170
|
connection it uses rather than share one opened once per run \u2014 the wrapper does that, the engine's
|
|
3038
3171
|
\`step\` does not. The wrapper file is the one place allowed to reach the engine. Configure
|
|
3039
|
-
\`engine\`, \`within\` and \`wrapper\`;
|
|
3172
|
+
\`engine\`, \`within\` and \`wrapper\`; enabled without \`engine\` or \`within\` the rule refuses the run.`,
|
|
3040
3173
|
meta: {
|
|
3041
3174
|
docs: {
|
|
3042
|
-
description: "A durable saga takes its `step` from the repo's own wrapper, which opens a connection inside each step body \u2014 a durable run hibernates and retries, so a handle shared across steps is dead by the time a later step uses it. The engine module, the guarded tree and the wrapper file are options;
|
|
3175
|
+
description: "A durable saga takes its `step` from the repo's own wrapper, which opens a connection inside each step body \u2014 a durable run hibernates and retries, so a handle shared across steps is dead by the time a later step uses it. The engine module, the guarded tree and the wrapper file are options; enabled without them it refuses the run."
|
|
3043
3176
|
},
|
|
3044
3177
|
messages: {
|
|
3045
3178
|
engineStep: '"{{file}}" imports `step` from `{{engine}}`. Import it from {{wrapper}} instead: a durable run spans hibernation and retries, so the connection a step uses has to be opened inside that step rather than shared across steps. The wrapper opens it; the engine\'s `step` does not.'
|
|
@@ -3869,6 +4002,7 @@ var rules = {
|
|
|
3869
4002
|
"document-sagas-are-generic": document_sagas_are_generic_default,
|
|
3870
4003
|
"documents-share-one-table": documents_share_one_table_default,
|
|
3871
4004
|
"effect-hook-naming": effect_hook_naming_default,
|
|
4005
|
+
"layer-walls": layer_walls_default,
|
|
3872
4006
|
"max-comment-density": max_comment_density_default,
|
|
3873
4007
|
"molecule-atoms-only": molecule_atoms_only_default,
|
|
3874
4008
|
"molecule-must-compose": molecule_must_compose_default,
|
|
@@ -3925,5 +4059,6 @@ var index_default = plugin;
|
|
|
3925
4059
|
export {
|
|
3926
4060
|
PRESETS,
|
|
3927
4061
|
index_default as default,
|
|
3928
|
-
rules
|
|
4062
|
+
rules,
|
|
4063
|
+
rulesOfPreset
|
|
3929
4064
|
};
|