@geonosis/oxlint-plugin-biological-architecture 2.9.0 → 2.10.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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @geonosis/oxlint-plugin-biological-architecture
2
2
 
3
+ ## 2.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4c240df: `tissue-no-data-props` forbids state through props, not data (#9).
8
+
9
+ A stateless component handed `collections` by its parent and rendering a carousel cell had no
10
+ tier: the tissue rule read a data prop as ownership. Data is not state — the route fetched it, or
11
+ the tissue above did. A tissue may now take any declared data type. What it may not be handed is a
12
+ handler or setter (a function type, `Dispatch`, `SetStateAction`) or an undeclared one (`any`,
13
+ `unknown`, `object`, `{}`): `any` was "unknown, therefore allowed", and is now reported.
14
+
15
+ If you migrate against this, re-tier any `cells/` file that only re-exports a non-cell first. A
16
+ parent that stays in `cells/` and imports such a shim through a bare `cells` barrel is silent today
17
+ only because the barrel is untraced (#4); when #4's barrel fix lands it picks up
18
+ `cell-must-not-compose-cell`. A parent this release lets you move to `tissues/` is not affected by
19
+ that fix — it would only surface if barrels were followed through their re-exports.
20
+
3
21
  ## 2.9.0
4
22
 
5
23
  ### Minor Changes
@@ -0,0 +1,7 @@
1
+ // Breaches tissue-no-data-props: a handler is state coordinated through props. The parent that
2
+ // holds `onSelect` holds state, which makes it a cell — and a cell does not render a tissue.
3
+ import { GoodStateful } from '../cells/good-stateful'
4
+
5
+ export function BadHandlerProp({ onSelect }: { onSelect: (id: string) => void }) {
6
+ return <GoodStateful key={String(onSelect)} />
7
+ }
@@ -1,3 +1,5 @@
1
- export function BadPassthrough({ children }: { children: unknown }) {
2
- return <div>{children as never}</div>
1
+ import type { ReactNode } from 'react'
2
+
3
+ export function BadPassthrough({ children }: { children: ReactNode }) {
4
+ return <div>{children}</div>
3
5
  }
@@ -0,0 +1,7 @@
1
+ // Breaches tissue-no-data-props: `any` is not a declaration. The tissue hands the value to a
2
+ // typed child; say what it is.
3
+ import { GoodStateful } from '../cells/good-stateful'
4
+
5
+ export function BadUntypedProp({ order }: { order: any }) {
6
+ return <GoodStateful key={String(order)} />
7
+ }
@@ -0,0 +1,13 @@
1
+ // The shape issue #9 had no tier for: stateless, handed rows by its parent, arranging a cell.
2
+ // Data is not state — a tissue may carry what it hands down.
3
+ import { GoodStateful } from '../cells/good-stateful'
4
+
5
+ export function GoodDataProps({ rows, title }: { rows: { id: string }[]; title: string }) {
6
+ return (
7
+ <section aria-label={title}>
8
+ {rows.map((row) => (
9
+ <GoodStateful key={row.id} />
10
+ ))}
11
+ </section>
12
+ )
13
+ }
package/dist/index.js CHANGED
@@ -542,8 +542,8 @@ down to \`compounds/\`, a store adapter goes sideways to \`organelles/\`. A pare
542
542
  hosts a cell (a summary card that opens a modal-hosted form) is two cells side by side: the child
543
543
  hosts its own modal and reads whether it is open from the store, the parent writes it, and a
544
544
  \`tissues/\` file arranges both. Cells coordinate through a store, never through props from one to
545
- the other. A stateless adapter between cells is a tissue \u2014 it may take number and boolean layout
546
- configuration with a literal default (\`step = 10\`). Private siblings of the SAME
545
+ the other. A stateless adapter between cells is a tissue \u2014 it may take the data it hands down
546
+ (\`{ collections: Collection[] }\`); it may not take a handler. Private siblings of the SAME
547
547
  component (\`invoice-editor.parts.tsx\` from \`invoice-editor.tsx\`) are one cell and are fine. The
548
548
  barrels \`cells/index.ts\`, \`cells/dynamic.ts\` and \`cells/dynamic.tsx\` are exempt \u2014 re-exporting
549
549
  siblings is the shape \`cells-folder-index-is-barrel\` requires of them.`,
@@ -6286,35 +6286,37 @@ var ROUTE_FALLBACK_BASENAMES2 = /* @__PURE__ */ new Set([
6286
6286
  "loading.tsx",
6287
6287
  "not-found.tsx"
6288
6288
  ]);
6289
- var ALWAYS_ALLOWED_PROP_NAMES = /* @__PURE__ */ new Set(["children", "className", "params", "searchParams"]);
6290
- var REACT_NODE_TYPE_NAMES = /* @__PURE__ */ new Set([
6291
- "ComponentType",
6292
- "Element",
6293
- "JSX",
6294
- "PropsWithChildren",
6295
- "ReactElement",
6296
- "ReactNode"
6297
- ]);
6298
- var isKindVocabulary = (name) => /(Kind|EntityName)$/.test(name);
6299
- function isAllowedTypeRefName(name) {
6300
- if (REACT_NODE_TYPE_NAMES.has(name)) return true;
6301
- if (isKindVocabulary(name)) return true;
6302
- if (name.endsWith("Icon")) return true;
6303
- if (name.endsWith("IconComponent")) return true;
6304
- if (name.endsWith("IconType")) return true;
6289
+ var HANDLER_TYPE_NAMES = /* @__PURE__ */ new Set(["Dispatch", "Function", "SetStateAction"]);
6290
+ var UNDECLARED_KEYWORDS = /* @__PURE__ */ new Set(["TSAnyKeyword", "TSObjectKeyword", "TSUnknownKeyword"]);
6291
+ var isHandler = (t) => {
6292
+ if (!t?.type) return false;
6293
+ if (t.type === "TSFunctionType") return true;
6294
+ if (t.type === "TSTypeReference") return HANDLER_TYPE_NAMES.has(t.typeName?.name ?? "");
6295
+ if (t.type === "TSUnionType") return (t.types ?? []).some(isHandler);
6305
6296
  return false;
6306
- }
6307
- function describeTypeAnnotation(t) {
6308
- if (!t || !t.type) return "unknown";
6297
+ };
6298
+ var isUndeclared = (t) => {
6299
+ if (!t?.type) return false;
6300
+ if (UNDECLARED_KEYWORDS.has(t.type)) return true;
6301
+ if (t.type === "TSTypeLiteral") return (t.members ?? []).length === 0;
6302
+ if (t.type === "TSUnionType") return (t.types ?? []).some(isUndeclared);
6303
+ return false;
6304
+ };
6305
+ var KEYWORD_NAMES = {
6306
+ TSAnyKeyword: "any",
6307
+ TSNullKeyword: "null",
6308
+ TSObjectKeyword: "object",
6309
+ TSUndefinedKeyword: "undefined",
6310
+ TSUnknownKeyword: "unknown"
6311
+ };
6312
+ var describeTypeAnnotation = (t) => {
6313
+ if (!t?.type) return "unknown";
6314
+ if (t.type in KEYWORD_NAMES) return KEYWORD_NAMES[t.type] ?? t.type;
6309
6315
  switch (t.type) {
6310
- case "TSStringKeyword":
6311
- return "string";
6312
- case "TSNumberKeyword":
6313
- return "number";
6314
- case "TSBooleanKeyword":
6315
- return "boolean";
6316
- case "TSArrayType":
6317
- return `Array<${describeTypeAnnotation(t.elementType)}>`;
6316
+ case "TSFunctionType":
6317
+ return "a function";
6318
+ case "TSTypeLiteral":
6319
+ return "{}";
6318
6320
  case "TSTypeReference":
6319
6321
  return t.typeName?.name ?? "(unknown-ref)";
6320
6322
  case "TSUnionType":
@@ -6322,47 +6324,7 @@ function describeTypeAnnotation(t) {
6322
6324
  default:
6323
6325
  return t.type;
6324
6326
  }
6325
- }
6326
- function isLiteralStringUnion(t) {
6327
- if (t.type !== "TSUnionType") return false;
6328
- const types = t.types ?? [];
6329
- if (types.length === 0) return false;
6330
- return types.every((x) => x.type === "TSLiteralType" && typeof x.literal?.value === "string");
6331
- }
6332
- var narrowsAKindVocabulary = (t) => {
6333
- const name = t.typeName?.name ?? "";
6334
- if (name !== "Exclude" && name !== "Extract") return false;
6335
- const [subject] = (t.typeArguments ?? t.typeParameters)?.params ?? [];
6336
- return subject?.type === "TSTypeReference" && isKindVocabulary(subject.typeName?.name ?? "");
6337
- };
6338
- function isAllowedType(t) {
6339
- if (!t || !t.type) return true;
6340
- switch (t.type) {
6341
- case "TSTypeReference":
6342
- return isAllowedTypeRefName(t.typeName?.name ?? "") || narrowsAKindVocabulary(t);
6343
- case "TSUnionType":
6344
- return isLiteralStringUnion(t);
6345
- // TSTypeLiteral (inline object shape) — allowed because nested shapes
6346
- // Are typically ReactNode records; if it turns out to contain data
6347
- // Primitives, the outer check will miss them (v1 limitation).
6348
- case "TSTypeLiteral":
6349
- return true;
6350
- // Primitives and arrays fall through to forbidden.
6351
- case "TSStringKeyword":
6352
- case "TSNumberKeyword":
6353
- case "TSBooleanKeyword":
6354
- case "TSArrayType":
6355
- return false;
6356
- default:
6357
- return true;
6358
- }
6359
- }
6360
- var literalDefaultsOf = (param) => new Map(
6361
- (param.properties ?? []).flatMap(
6362
- (property) => property.type === "Property" && property.key?.type === "Identifier" && property.value?.type === "AssignmentPattern" && property.value.right?.type === "Literal" ? [[property.key.name ?? "", property.value.right.value]] : []
6363
- )
6364
- );
6365
- var isLayoutConfiguration = (t, defaults, prop) => t.type === "TSNumberKeyword" && typeof defaults.get(prop) === "number" || t.type === "TSBooleanKeyword" && typeof defaults.get(prop) === "boolean";
6327
+ };
6366
6328
  var tissueNoDataProps = {
6367
6329
  create(context) {
6368
6330
  const normalized = normalise(context.filename);
@@ -6377,20 +6339,17 @@ var tissueNoDataProps = {
6377
6339
  if (!firstParam) return;
6378
6340
  const typeLiteral = firstParam.typeAnnotation?.typeAnnotation;
6379
6341
  if (!typeLiteral || typeLiteral.type !== "TSTypeLiteral") return;
6380
- const members = typeLiteral.members ?? [];
6381
- const defaults = literalDefaultsOf(firstParam);
6382
- for (const member of members) {
6342
+ for (const member of typeLiteral.members ?? []) {
6383
6343
  if (member.key?.type !== "Identifier") continue;
6384
- const propName = member.key.name ?? "";
6385
- if (!propName) continue;
6386
- if (ALWAYS_ALLOWED_PROP_NAMES.has(propName)) continue;
6344
+ const prop = member.key.name ?? "";
6345
+ if (!prop) continue;
6387
6346
  const t = member.typeAnnotation?.typeAnnotation;
6388
6347
  if (!t) continue;
6389
- if (isAllowedType(t)) continue;
6390
- if (isLayoutConfiguration(t, defaults, propName)) continue;
6348
+ const messageId = isHandler(t) ? "handler" : isUndeclared(t) ? "undeclared" : void 0;
6349
+ if (messageId === void 0) continue;
6391
6350
  context.report({
6392
- data: { prop: propName, type: describeTypeAnnotation(t) },
6393
- messageId: "dataProp",
6351
+ data: { prop, type: describeTypeAnnotation(t) },
6352
+ messageId,
6394
6353
  node: member
6395
6354
  });
6396
6355
  }
@@ -6412,10 +6371,8 @@ var tissueNoDataProps = {
6412
6371
  }
6413
6372
  }
6414
6373
  return {
6415
- // Only inspect the PUBLIC INTERFACE of the tissue file — the default or
6416
- // Named export. Inner helper functions (not exported) are file-scoped
6417
- // Implementation details; their props don't define the tissue's
6418
- // Contract, so they're out of scope for this rule.
6374
+ // Only the PUBLIC INTERFACE of the tissue file — the default or named export. Inner helpers
6375
+ // are file-scoped implementation; their props do not define the tissue's contract.
6419
6376
  ExportDefaultDeclaration(node) {
6420
6377
  checkDeclaration(node.declaration);
6421
6378
  },
@@ -6424,17 +6381,21 @@ var tissueNoDataProps = {
6424
6381
  }
6425
6382
  };
6426
6383
  },
6427
- fixShape: `A tissue takes \`children\`, ReactNode slot props, layout variants, \`params\` and \`searchParams\` \u2014 and
6428
- nothing else. A prop typed \`string\`, \`number\`, an array or a domain type (\`id\`, \`initialData\`,
6429
- \`documents\`) means the file owns data, which makes it a CELL: move it to \`cells/\`. A tissue gets its
6430
- content as slots, never as data. Layout configuration is not data: a \`number\` or \`boolean\` prop the
6431
- tissue gives a literal default (\`{ step = 10, wrap = false }\`) is allowed.`,
6384
+ fixShape: `A tissue holds no state, and data is not state: a tissue may take the rows, the cart or the title
6385
+ its parent hands it and hand them down to the cells and compounds it arranges (\`{ collections:
6386
+ Collection[] }\` rendering a carousel cell is a tissue). What a tissue is never handed is STATE \u2014 a
6387
+ handler or a setter (\`onSelect: (id) => void\`, \`setOpen: Dispatch<\u2026>\`) is coordination through
6388
+ props, and the parent holding it is a cell, which does not render tissues: move the handler into the
6389
+ cell below, or the file to \`cells/\`. And never an undeclared type \u2014 \`any\`, \`unknown\`, \`object\`,
6390
+ \`{}\`, alone or in a union \u2014 declare what the child takes. A tissue with no hooks, no store and no
6391
+ organelle that composes a cell and takes data is a tissue; do not invent a store to make it a cell.`,
6432
6392
  meta: {
6433
6393
  docs: {
6434
- description: "Tissues must not own data. Props on a tissue must be limited to `children`, ReactNode slot props, `params`, `searchParams`, icon types, or layout-variant literal unions. Any primitive (`string`/`number`/`boolean`), array type, or arbitrary domain type reference means the file is actually a cell \u2014 move it to cells/. That is lint-theater.2 (Tissue self-sufficiency rule)."
6394
+ description: "A tissue may carry data, never state. Its props may not include a handler or setter (a function type, `Dispatch`, `SetStateAction`, `Function`, or a union holding one) \u2014 that is state coordinated through props, which makes the parent a cell \u2014 nor an undeclared type (`any`, `unknown`, `object`, `{}`, or a union holding one). Every declared data type is allowed: the route fetched it or the tissue above did, and the tissue hands it down (#9)."
6435
6395
  },
6436
6396
  messages: {
6437
- dataProp: "Tissue prop '{{prop}}' has type '{{type}}' which is not allowed on a tissue. Tissues arrange children without owning data. Move the file from features/*/tissues/ to features/*/cells/ \u2014 cells own data. Allowed tissue props: children, params, searchParams, ReactNode slots, literal-union variants, icon types, and number/boolean layout configuration with a literal default."
6397
+ handler: "Tissue prop '{{prop}}' is {{type}}. A handler or setter is state coordinated through props \u2014 the parent that holds it is a cell, and a cell does not render a tissue. Move the handler into the cell this tissue arranges, or move this file to cells/.",
6398
+ undeclared: "Tissue prop '{{prop}}' is typed '{{type}}', which declares nothing. A tissue hands data to typed children; declare the type the child takes. `any` is not a way past the tier wall."
6438
6399
  },
6439
6400
  schema: [],
6440
6401
  type: "problem"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/oxlint-plugin-biological-architecture",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "types": "./dist/index.d.ts",
5
5
  "description": "Biological tier architecture as lint — the union of the dielime and during.day rule sets, shipped as presets.",
6
6
  "keywords": [
@@ -45,7 +45,7 @@
45
45
  "oxlint": ">=1.77"
46
46
  },
47
47
  "devDependencies": {
48
- "@geonosis/lint-parity": "2.9.0"
48
+ "@geonosis/lint-parity": "2.10.0"
49
49
  },
50
50
  "engines": {
51
51
  "node": ">=22"
@@ -1,5 +0,0 @@
1
- import { GoodCompound } from '../compounds/good-compound'
2
-
3
- export function BadDataProps({ title }: { title: string }) {
4
- return <GoodCompound label={title} />
5
- }