@geonosis/oxlint-plugin-biological-architecture 1.2.0 → 1.3.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.
@@ -150,7 +150,10 @@
150
150
  "roots": [
151
151
  "integrations/providers"
152
152
  ],
153
- "registry": "integrations/registry.ts",
153
+ "registry": [
154
+ "integrations/registry.ts",
155
+ "integrations/registry.fulfillment.ts"
156
+ ],
154
157
  "manifests": [
155
158
  "index.ts"
156
159
  ]
@@ -0,0 +1,8 @@
1
+ // A budget built by a call the rule cannot name: reported, never a silent pass.
2
+ declare const emitEventStep: (input: unknown) => unknown
3
+ declare const retry: { background: () => unknown }
4
+
5
+ export const emitted = emitEventStep({
6
+ eventName: 'sample.converted',
7
+ options: retry.background(),
8
+ })
@@ -0,0 +1 @@
1
+ export const WarpProvider = { id: 'cp_warp' }
@@ -0,0 +1,4 @@
1
+ // The other half of a SPLIT registry: the file beside it imports this one, and `warp` is
2
+ // registered only here. Reading the named half alone reported two registered carrier directories
3
+ // as dead weight on a real consumer, which is how a rule gets turned off.
4
+ export const carriers = ['providers/warp']
@@ -1,9 +1,12 @@
1
1
  import { AttioProvider } from './providers/attio/index'
2
+ import { carriers } from './registry.fulfillment'
2
3
 
3
- // Two of the three directories beside this file are named here. The third is `dead-cal`, which
4
- // ships a manifest, is imported by nothing, and reads as an integration to anyone counting
5
- // directories — Midday carries eight of these.
4
+ // This registry is SPLIT: two of the four directories beside it are named here, `warp` is named in
5
+ // the half this file imports, and `dead-cal` is named in neither. `dead-cal` ships a manifest, is
6
+ // imported by nothing, and reads as an integration to anyone counting directories — Midday carries
7
+ // eight of these.
6
8
  export const providers = {
9
+ carriers,
7
10
  cp_attio: AttioProvider,
8
11
  cp_hubspot: 'providers/hubspot',
9
12
  }
package/dist/index.d.ts CHANGED
@@ -21,7 +21,20 @@ type Probe = {
21
21
  path: string;
22
22
  source: string;
23
23
  };
24
- type ProbeFn = (options: readonly unknown[]) => Probe;
24
+ /**
25
+ * A probe that needs a TREE rather than a file.
26
+ *
27
+ * A rule that reads a sibling off disk — a registry, a schema's twin — cannot be exercised by one
28
+ * file however right its source is: `no-unregistered-plugin-dir` returns silently when the registry
29
+ * its options name is not there, so the single-file probe fired nothing and every consumer
30
+ * enforcing the rule over a real registry read as a permanent FAIL. The answer is not a widened
31
+ * probe, which would be the rule claiming evidence it does not have; it is a probe that may declare
32
+ * the sibling too, and a reader that mounts all of them.
33
+ */
34
+ type ProbeTree = {
35
+ files: readonly Probe[];
36
+ };
37
+ type ProbeFn = (options: readonly unknown[]) => Probe | ProbeTree;
25
38
 
26
39
  type Rule = {
27
40
  create: (context: never) => Record<string, unknown>;
package/dist/index.js CHANGED
@@ -593,10 +593,10 @@ var dialect_through_the_seam_default = dialectThroughTheSeam;
593
593
  // src/rules/lib/options.ts
594
594
  var NOTHING = "nothing";
595
595
  var isMissing = (value) => value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
596
- var requireOption = (rule, value, option) => {
596
+ var requireOption = (rule, value, option, accepts) => {
597
597
  if (isMissing(value)) {
598
598
  throw new Error(
599
- `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.`
599
+ `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.${accepts === void 0 ? "" : ` It accepts ${accepts}.`}`
600
600
  );
601
601
  }
602
602
  return value;
@@ -854,9 +854,19 @@ var emitDeclaresAttempts = {
854
854
  const site = named?.type === "Literal" && typeof named.value === "string" ? named.value : emitter;
855
855
  const options = valueOf(properties, OPTIONS);
856
856
  if (options !== void 0 && objectOf(options) === null) {
857
- if (budgetHelpers.size === 0) return;
857
+ if (budgetHelpers.size === 0) {
858
+ context.report({ data: { emitter, site }, messageId: "undeclaredHelpers", node });
859
+ return;
860
+ }
858
861
  const call = options;
859
- if (call.type !== "CallExpression" || call.callee?.type !== "Identifier") return;
862
+ if (call.type !== "CallExpression" || call.callee?.type !== "Identifier") {
863
+ context.report({
864
+ data: { emitter, helper: "a call this rule cannot name", site },
865
+ messageId: "opaqueBudget",
866
+ node
867
+ });
868
+ return;
869
+ }
860
870
  if (budgetHelpers.has(call.callee.name ?? "")) return;
861
871
  context.report({
862
872
  data: { emitter, helper: call.callee.name ?? "a call", site },
@@ -886,8 +896,9 @@ var emitDeclaresAttempts = {
886
896
  than written as a number at the call site. \`attempts\` defaults to 1 and the worker reads only
887
897
  \`attempts > 1\` as "a retry was configured", so an emit with \`{ priority }\` and no \`attempts\` is
888
898
  droppable while looking deliberate. Pass \`options: { attempts: RETRY_BACKGROUND.attempts, priority }\`.
889
- A budget the rule cannot evaluate \u2014 a constant, a computed value \u2014 passes: it says nothing rather
890
- than pushing the number back inline. Name the repo's own budget builders in \`budgetHelpers\`
899
+ A literal budget the rule cannot evaluate \u2014 a shared constant's property \u2014 passes; an options bag
900
+ built by a CALL never passes silently: with no \`budgetHelpers\` declared it is reported as such, and
901
+ with them declared any call that is not one of them is reported. Name the repo's own budget builders in \`budgetHelpers\`
891
902
  (dielime's is \`retryableEvent\`, which throws on \`attempts <= 1\`) and a call to one of them counts as
892
903
  a budget while a call to anything else is reported: without that, a repo whose every call site goes
893
904
  through a helper gets zero findings and no coverage. Configure \`emitters\` if the repo wraps the emit
@@ -898,6 +909,7 @@ under another name.`,
898
909
  },
899
910
  messages: {
900
911
  noBudget: '`{{emitter}}` emits "{{site}}" with no `options.attempts`. `attempts` defaults to 1 and the worker treats only `attempts > 1` as a configured retry, so this event is dropped on its first failure. Give it a budget from the shared retry constants.',
912
+ undeclaredHelpers: '`{{emitter}}` emits "{{site}}" with a budget built by a call, and this repo declares no `budgetHelpers` \u2014 nothing here can tell whether that call configures a retry. Name the repo\'s budget builders in `budgetHelpers`, or pass `attempts` where it can be read.',
901
913
  opaqueBudget: '`{{emitter}}` emits "{{site}}" with a budget built by `{{helper}}`, which this repo has not named as a budget helper. The bus reads only `attempts > 1` as a configured retry, and nothing here can tell whether that call produces one. Use one of the declared `budgetHelpers`, or pass `attempts` where it can be read.',
902
914
  tooFew: '`{{emitter}}` emits "{{site}}" with `attempts: {{attempts}}`, which is the default spelled out \u2014 one attempt is no retry. Take the budget from the shared retry constants instead.'
903
915
  },
@@ -3409,6 +3421,7 @@ var no_type_definitions_in_components_default = noTypeDefinitionsInComponents;
3409
3421
  // src/rules/no-unregistered-plugin-dir.ts
3410
3422
  import { readFileSync as readFileSync3 } from "fs";
3411
3423
  var RULE8 = "no-unregistered-plugin-dir";
3424
+ var ACCEPTS = "{ roots: string[], registry: string | string[], manifests: string[] }";
3412
3425
  var trimSlashes = (path2) => path2.replace(/^\/+/, "").replace(/\/+$/, "");
3413
3426
  var withoutComments = (source) => source.replaceAll(/\/\*[\s\S]*?\*\//g, " ").replaceAll(/\/\/[^\n]*/g, " ");
3414
3427
  var literalsOf = (source) => [...withoutComments(source).matchAll(/['"`]([^'"`\n]+)['"`]/g)].map((match) => match[1] ?? "");
@@ -3425,9 +3438,10 @@ var rootedAt = (filename, root) => {
3425
3438
  var noUnregisteredPluginDir = {
3426
3439
  create(context) {
3427
3440
  const options = context.options?.[0] ?? {};
3428
- const roots = requireOption(RULE8, options.roots, "roots").map(trimSlashes);
3429
- const registry = requireOption(RULE8, options.registry, "registry");
3430
- const manifests = new Set(requireOption(RULE8, options.manifests, "manifests"));
3441
+ const roots = requireOption(RULE8, options.roots, "roots", ACCEPTS).map(trimSlashes);
3442
+ const declared = requireOption(RULE8, options.registry, "registry", ACCEPTS);
3443
+ const registries = (typeof declared === "string" ? [declared] : declared).map(trimSlashes);
3444
+ const manifests = new Set(requireOption(RULE8, options.manifests, "manifests", ACCEPTS));
3431
3445
  const filename = normalise(context.filename);
3432
3446
  return {
3433
3447
  Program(node) {
@@ -3440,11 +3454,12 @@ var noUnregisteredPluginDir = {
3440
3454
  if (directory === void 0 || file === void 0 || !manifests.has(file)) continue;
3441
3455
  let source = "";
3442
3456
  try {
3443
- source = readFileSync3(`${rooted.repoRoot}/${trimSlashes(registry)}`, "utf8");
3457
+ source = registries.map((half) => readFileSync3(`${rooted.repoRoot}/${half}`, "utf8")).join("\n");
3444
3458
  } catch {
3445
3459
  return;
3446
3460
  }
3447
3461
  if (literalsOf(source).some((literal) => names(literal, directory))) return;
3462
+ const registry = registries.join(", ");
3448
3463
  context.report({ data: { directory, registry, root }, messageId: "unregistered", node });
3449
3464
  return;
3450
3465
  }
@@ -3454,13 +3469,14 @@ var noUnregisteredPluginDir = {
3454
3469
  fixShape: `Every directory under a plugin root is named by the registry. Add the directory to the registry file \u2014
3455
3470
  as an import path or as an id \u2014 or delete it. A directory that ships a manifest and assets and is
3456
3471
  reached by nothing reads as an integration to everyone who counts directories, and Midday carried
3457
- eight of them. Configure \`roots\`, \`registry\` and \`manifests\` \u2014 all three are required, and \`manifests\` has no
3458
- default on purpose: dielime's plugin directories hold \`service.ts\` and no index, so a defaulted list
3472
+ eight of them. Configure \`roots\`, \`registry\` and \`manifests\` \u2014 all three are required. \`registry\` takes a list when the
3473
+ registry is SPLIT across files (a config that imports a second config): name every half, because the
3474
+ rule reads source and does not follow imports. \`manifests\` has no default on purpose: dielime's plugin directories hold \`service.ts\` and no index, so a defaulted list
3459
3475
  would match nothing and report a clean tree it never looked at. A seam whose providers are flat files
3460
3476
  rather than directories is out of scope \u2014 nothing under a root matches, so nothing is reported.`,
3461
3477
  meta: {
3462
3478
  docs: {
3463
- description: "Every directory under a plugin root must be named by the registry file. Midday ships 35 app-store directories and 8 are reachable from nothing. The registry is read as source, so an id or an import path both count. `roots`, `registry` and `manifests` are all required; `manifests` has no default because a defaulted entry filename matched nothing in a real consumer and reported a clean tree it had never looked at."
3479
+ description: "Every directory under a plugin root must be named by the registry file. Midday ships 35 app-store directories and 8 are reachable from nothing. The registry is read as source, so an id or an import path both count, and `registry` takes a LIST when the registry is split across files \u2014 reading only the named half reported two registered carrier directories as dead weight on a real consumer. `roots`, `registry` and `manifests` are all required; `manifests` has no default because a defaulted entry filename matched nothing in a real consumer and reported a clean tree it had never looked at."
3464
3480
  },
3465
3481
  messages: {
3466
3482
  unregistered: 'The plugin directory "{{directory}}" under `{{root}}` is named nowhere in `{{registry}}`. Nothing reaches it, so it ships as an integration and behaves as dead weight \u2014 register it, or delete it.'
@@ -3470,7 +3486,12 @@ rather than directories is out of scope \u2014 nothing under a root matches, so
3470
3486
  additionalProperties: false,
3471
3487
  properties: {
3472
3488
  manifests: { items: { type: "string" }, type: "array" },
3473
- registry: { type: "string" },
3489
+ registry: {
3490
+ anyOf: [
3491
+ { type: "string" },
3492
+ { items: { type: "string" }, type: "array" }
3493
+ ]
3494
+ },
3474
3495
  roots: { items: { type: "string" }, type: "array" }
3475
3496
  },
3476
3497
  type: "object"
@@ -3480,21 +3501,28 @@ rather than directories is out of scope \u2014 nothing under a root matches, so
3480
3501
  },
3481
3502
  /**
3482
3503
  * A directory under a configured root, carrying a manifest, that the registry does not name — and
3483
- * the one probe in this plugin a single file cannot make fire on its own.
3504
+ * the registry itself, because the rule reads it off disk and returns silently when it is not
3505
+ * there. One file cannot be both the directory under `roots` and the registry at the consumer's
3506
+ * own path, so the probe declares two. It was a single file until 2026-08-30, and every consumer
3507
+ * enforcing this rule over a real registry read as a permanent FAIL for it: the probe fired
3508
+ * nothing, which was the truth about the probe and a lie about the repo.
3484
3509
  *
3485
- * The rule reads the registry off disk, and a registry it cannot read is a configuration answer
3486
- * rather than a verdict about the tree, so it returns silently. The probe sits under `roots` and
3487
- * the registry sits at the consumer's own path; one file cannot be both. It fires when the
3488
- * registry ALSO exists beside it and fires nothing when it does not, which is the truth and is
3489
- * left as the truth: a probe widened until it fired would be this rule claiming evidence it does
3490
- * not have, which is precisely the defect the rule is about.
3510
+ * The registry it writes carries no string literal at all, so nothing in it can name the
3511
+ * directory beside it the probe must be a directory the registry does NOT reach, or the rule is
3512
+ * right to stay quiet.
3491
3513
  */
3492
3514
  probe(options) {
3493
- const { manifests = [], roots = [] } = bagOf(options);
3515
+ const { manifests = [], registry = "", roots = [] } = bagOf(options);
3494
3516
  const root = trimSlashes(roots[0] ?? "");
3517
+ const half = typeof registry === "string" ? registry : registry[0] ?? "";
3495
3518
  return {
3496
- path: `${root}/geonosis-probe/${manifests[0] ?? ""}`,
3497
- source: "export const geonosisProbe = { id: 'geonosis-probe' }\n"
3519
+ files: [
3520
+ {
3521
+ path: `${root}/geonosis-probe/${manifests[0] ?? ""}`,
3522
+ source: "export const geonosisProbe = { id: 'geonosis-probe' }\n"
3523
+ },
3524
+ { path: trimSlashes(half), source: "export const registry = { providers: [] }\n" }
3525
+ ]
3498
3526
  };
3499
3527
  }
3500
3528
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/oxlint-plugin-biological-architecture",
3
- "version": "1.2.0",
3
+ "version": "1.3.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": [
@@ -43,7 +43,7 @@
43
43
  "oxlint": ">=1.77"
44
44
  },
45
45
  "devDependencies": {
46
- "@geonosis/lint-parity": "1.2.0"
46
+ "@geonosis/lint-parity": "1.3.0"
47
47
  },
48
48
  "engines": {
49
49
  "node": ">=22"