@graphorin/eslint-plugin 0.6.0 → 0.7.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,31 @@
1
1
  # @graphorin/eslint-plugin
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#154](https://github.com/o-stepper/graphorin/pull/154) [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04) Thanks [@o-stepper](https://github.com/o-stepper)! - W-072: every export map's `import` condition becomes `default`, and the Node floor rises to `>=22.12.0`.
8
+
9
+ CJS consumers previously hit a bewildering `ERR_PACKAGE_PATH_NOT_EXPORTED` instead of a clear ESM-only signal. With the `default` condition, plain `require('@graphorin/core')` works via Node's stable `require(esm)` - which shipped in 22.12, hence the engines bump across every workspace manifest (packages, examples, benchmarks, docs; enforced by the widened mvp-readiness sweep). No dual-instance hazard: there is no CJS build, `require()` returns the same ESM module instance. ESM consumers are unaffected (`default` serves both paths; `types` stays first). The pack gate now runs attw under the full `node16` profile (was `esm-only`) and adds a runtime `require(esm)` smoke against the packed tarballs. Installs on Node 22.0-22.11 with `engine-strict` will refuse - upgrade Node (see the migration guide).
10
+
11
+ - [#162](https://github.com/o-stepper/graphorin/pull/162) [`73b19ca`](https://github.com/o-stepper/graphorin/commit/73b19caeda388bda628a48138cb7d70b1db839a3) Thanks [@o-stepper](https://github.com/o-stepper)! - `no-implicit-network-call` now activates in two stages: the linted file path must match `packages/*/src` AND the nearest `package.json` name must start with a prefix from the new `packagePrefixes` option (default `['@graphorin/']`). Downstream pnpm monorepos with the standard layout stop getting errors on their own `fetch()` calls just for adopting `flat/recommended`; a consumer that wants the rule to police their own scope passes `['error', { packagePrefixes: ['@myorg/'] }]`. When no package.json resolves above the file (virtual paths, programmatic Linter runs) the rule fails OPEN to the old path-only activation, so resolution hiccups can only over-flag, never silently disable the guard. The rule is now also dogfooded against the framework's own source in tests (sharing the check-no-network ALLOW_LIST as the single source of exemptions), and a lockstep contract test pins both matchers' verdicts over a shared corpus - closing the known axios asymmetry (the CI script now flags `axios(...)`/`axios.get(...)` call sites, not just imports).
12
+
13
+ - [#158](https://github.com/o-stepper/graphorin/pull/158) [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab) Thanks [@o-stepper](https://github.com/o-stepper)! - Tool discovery and grading are comment-aware (W-044). Discovery and every grading path run over a comment-blanked view of the source (newlines preserved, offsets stable; string/template and - conservatively - regex literals untouched): a commented-out `tool({...})` no longer appears in `graphorin tools lint` reports or the three ESLint tool rules, a commented-out property inside a live literal is never extracted, a commented email inside a live `examples:` block no longer penalizes the axis, and a `tool(` inside a string never matches. `DiscoveredTool` gains `gradingSource` (the blanked slice all graders consume) while `source` keeps the original text for reports. The description axis gets a deterministic anti-degenerate guard: 80+ chars of repeated filler (under 4 unique words, or one word over half the text) caps at 16 instead of scoring 40 - RB-49 calibration fixtures are unchanged; degenerate descriptions may now fall below `--threshold` gates. The false-positive contract (any callee lexically ending in `tool(`, renamed/wrapped calls invisible) is now documented in the module header and README.
14
+
15
+ ### Patch Changes
16
+
17
+ - [#162](https://github.com/o-stepper/graphorin/pull/162) [`73b19ca`](https://github.com/o-stepper/graphorin/commit/73b19caeda388bda628a48138cb7d70b1db839a3) Thanks [@o-stepper](https://github.com/o-stepper)! - `no-secret-unwrap` gains an opt-in `allowReceiverPattern` option for the documented collision with Zod's `.unwrap()` (ZodOptional/ZodNullable/ZodDefault) and Rust-style result libraries: when the source text of the receiver expression matches the pattern, both `unwrap` and `reveal` reports are skipped, so `['error', { allowReceiverPattern: 'Schema$' }]` lets schema-introspection code lint clean while `secret.unwrap()` keeps erroring. Default behaviour is byte-identical (no pattern, sharp deprecation cliff), and there is deliberately no built-in "looks like Zod" heuristic - an explicit narrow pattern or a file-glob override beats a nondeterministic guess. README documents both recipes.
18
+
19
+ - [#164](https://github.com/o-stepper/graphorin/pull/164) [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00) Thanks [@o-stepper](https://github.com/o-stepper)! - Tarballs now ship `src/` so the published `dist/**/*.d.ts.map` files actually work (W-136): the maps referenced `../src/*.ts` that the `files` whitelist excluded, so go-to-definition fell back into `.d.ts` and the shipped maps were dead weight. The pack gate gains a `map-integrity` leg: every source referenced by a shipped map must resolve inside the tarball (or be embedded via `sourcesContent`), with an anti-vacuous guard - a package whose tsdown config emits declaration maps must contain a non-zero number of `.d.ts.map` files, so a cache-restored dist that silently dropped maps fails the gate instead of passing vacuously. `mvp-readiness` now requires `src` in every publishable `files` array.
20
+
21
+ - [#164](https://github.com/o-stepper/graphorin/pull/164) [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00) Thanks [@o-stepper](https://github.com/o-stepper)! - Every published package now declares its tree-shaking contract via `sideEffects` (W-137): 18 packages audited to a pure module scope get `false`, the CLI declares its bin entry (`["./dist/bin/*"]`), and `@graphorin/security` gets an explicit `true` - its secrets subsystem registers built-in resolvers and the SecretValue caller-context provider at import time, so marking it pure would let bundlers drop those registrations. `mvp-readiness` now fails any publishable manifest without a declared `sideEffects`, closing the drift for future packages.
22
+
23
+ ## 0.6.1
24
+
25
+ ### Patch Changes
26
+
27
+ - [#142](https://github.com/o-stepper/graphorin/pull/142) [`436d6ca`](https://github.com/o-stepper/graphorin/commit/436d6ca5ebbd16df094e915682d3915c279a8430) Thanks [@o-stepper](https://github.com/o-stepper)! - Version constants and version-bearing strings now derive from each package's manifest at build time (`VERSION = pkg.version`; writer ids, client/server info, OTLP framework attributes, build-info metrics interpolate it). No behavioral change at the current version: the rendered strings are byte-identical. A release bump no longer edits source; the new `check-version-consistency` gate fails any reintroduced hardcoded framework version.
28
+
3
29
  ## 0.6.0
4
30
 
5
31
  ### Patch Changes
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > ESLint rules for projects that build on **Graphorin**.
4
4
 
5
- - **Status:** v0.6.0 - final Phase 16 ruleset.
5
+ - **Status:** v0.7.0 - final Phase 16 ruleset.
6
6
  - **License:** [MIT](./LICENSE) - © 2026 Oleksiy Stepurenko.
7
7
  - **Engines:** Node.js 22+ (ESM only).
8
8
  - **Peer dependency:** `eslint >= 9`.
@@ -58,10 +58,10 @@ The legacy `.eslintrc` form is still exported as `configs.recommended`
58
58
  | `tool-description-required` | Active. Flags `tool({...})` registrations whose `description` is missing, shorter than 20 characters, or a placeholder value (`'TODO'`, `'FIXME'`, `'tbd'`, `'description'`, `'placeholder'`). |
59
59
  | `tool-examples-recommended` | Active. Flags missing or empty `examples` arrays and rejects more than the documented upper bound (5). |
60
60
  | `tool-parameter-naming` | Active. Flags ambiguous single-word parameter names (`user`, `id`, `name`, `value`, `data`, `input`, `output`, `result`, `to`, `from`, `key`, `field`) and numeric-suffix names (`arg1`, `param2`) on `inputSchema: z.object({ ... })`. Per-tool opt-out via `tags: ['experimental']` or `tags: ['legacy']`. |
61
- | `no-secret-unwrap` | Active. Flags `.unwrap()` and `.reveal()` calls on `SecretValue`-shaped expressions. `.unwrap()` is reported as `'error'` regardless of comments (the method is `@deprecated`); `.reveal()` honours the `// graphorin-allow-secret-unwrap: <reason>` opt-out. |
61
+ | `no-secret-unwrap` | Active. Flags `.unwrap()` and `.reveal()` calls on `SecretValue`-shaped expressions. `.unwrap()` is reported as `'error'` regardless of comments (the method is `@deprecated`); `.reveal()` honours the `// graphorin-allow-secret-unwrap: <reason>` opt-out. Known collision: Zod's `ZodOptional`/`ZodNullable`/`ZodDefault` `.unwrap()` (and Rust-style result libraries) false-positive - either turn the rule off for introspection files (`{ files: ['**/schema-introspection/**'], rules: { '@graphorin/no-secret-unwrap': 'off' } }`) or set the carve-out option `['error', { allowReceiverPattern: 'Schema$' }]`, which skips receivers whose source text matches. Pick a NARROW suffix pattern (a broad regex would silence real `SecretValue.unwrap` calls); prefer the file-glob override when the collision is confined to a directory. |
62
62
  | `no-secret-in-deps` | Active. Flags `withChildToolSecretsContext({ secretsAllowed: [...] })` grants whose allowlist is non-empty and lacks an `// rb-24-justification: <reason>` comment (DEC-137). |
63
63
  | `provider-middleware-order` | Active. Lint-time enforcement of the canonical `withTracing → withRetry → withRateLimit → withCostLimit → withCostTracking → withFallback → withRedaction` ordering. |
64
- | `no-implicit-network-call` | Active. Flags network primitives in `@graphorin/*` framework code without the explicit `// graphorin-allow-network: <reason>` opt-out: `fetch(...)` / `axios.*` / `undici.*` / `got.*` / `http(s).request` / raw `net`·`tls`·`dgram` sockets / `new WebSocket` / `new EventSource` / `new XMLHttpRequest`, plus static, dynamic, and `require()` imports of HTTP clients (`node-fetch`, `undici`, `got`, `axios`, `ky`, `ws`). Kept in lockstep with `scripts/check-no-network.mjs`. |
64
+ | `no-implicit-network-call` | Active. Flags network primitives in framework code without the explicit `// graphorin-allow-network: <reason>` opt-out: `fetch(...)` / `axios.*` / `undici.*` / `got.*` / `http(s).request` / raw `net`·`tls`·`dgram` sockets / `new WebSocket` / `new EventSource` / `new XMLHttpRequest`, plus static, dynamic, and `require()` imports of HTTP clients (`node-fetch`, `undici`, `got`, `axios`, `ky`, `ws`). Activation is two-stage (W-039): the file path must match `packages/*/src` AND the nearest `package.json` name must start with a prefix from `packagePrefixes` (default `['@graphorin/']`), so a downstream monorepo with the standard layout is not flagged; when no package.json resolves (virtual paths), the rule FAILS OPEN to path-only activation. Polices your own scope via `'@graphorin/no-implicit-network-call': ['error', { packagePrefixes: ['@myorg/'] }]`. Kept in lockstep with `scripts/check-no-network.mjs` (contract-tested). |
65
65
  | `no-third-party-workflow-aliases` | Active. Flags identifiers that mirror third-party-library workflow primitives in the `@graphorin/workflow` package's source so the framework keeps its own naming. |
66
66
  | `no-bare-tool-exec` | Active. Flags `tool({ execute })` functions that do not reference `signal` so long-running tools always propagate the cancellation contract. |
67
67
 
@@ -88,10 +88,18 @@ for (const tool of tools) {
88
88
  }
89
89
  ```
90
90
 
91
+ ### Discovery contract (W-044)
92
+
93
+ The discovery is **text-based by design** (one implementation serves both the CLI and the ESLint rules). Its contract:
94
+
95
+ - **Comment-aware.** Discovery and grading run over a comment-blanked view of the source: a commented-out `tool({...})` is never discovered, a commented-out property inside a live literal is never extracted, and a commented email inside a live `examples:` block never penalizes the axis. `DiscoveredTool.source` keeps the original text for reports; grading consumes `DiscoveredTool.gradingSource`. String, template, and (conservatively) regex literals are left untouched, and a `tool(` inside a string never matches.
96
+ - **False positives.** Any callee whose last lexical token is `tool(` matches - including method calls like `.tool(` and same-named local helpers. Renamed or wrapped invocations (`const t = tool; t({...})`) are NOT seen. This is the accepted cost of the text-based surface.
97
+ - **Anti-degenerate description guard.** A description of 80+ chars scores the top tier only if it looks like prose: under 4 unique words, or one word carrying more than half the text, caps the axis at 16.
98
+
91
99
  ## Versioning
92
100
 
93
101
  `@graphorin/eslint-plugin` follows the same lockstep release as the rest of the `@graphorin/*` packages while the framework is on the `0.x` line. Once Graphorin reaches `1.0`, the plugin will move to its own release cadence.
94
102
 
95
103
  ---
96
104
 
97
- **Graphorin** · v0.6.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
105
+ **Graphorin** · v0.7.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
package/dist/index.d.ts CHANGED
@@ -19,6 +19,24 @@ import * as eslint0 from "eslint";
19
19
  * dynamic import will not be picked up by this lint surface; that
20
20
  * is the documented contract for the v0.1 lint surface.
21
21
  *
22
+ * **Comment-awareness (W-044).** Discovery AND grading run over a
23
+ * comment-blanked view of the source: line-comment and block-comment
24
+ * content is replaced with spaces (newlines preserved, so line numbers
25
+ * and offsets never shift) while string/template literals - and,
26
+ * conservatively, regex literals - are left untouched. A
27
+ * commented-out `tool({...})` is therefore never discovered, and a
28
+ * commented-out property (or a commented email inside a live
29
+ * `examples:` block) never feeds the grader. The ORIGINAL slice stays
30
+ * available as {@link DiscoveredTool.source} for reports; grading
31
+ * paths consume {@link DiscoveredTool.gradingSource}.
32
+ *
33
+ * **False-positive contract.** The scanner matches ANY callee whose
34
+ * last lexical token is `tool(` - including method calls like
35
+ * `.tool(` and locally-defined helpers that happen to share the name.
36
+ * Renamed or wrapped invocations (`const t = tool; t({...})`) are NOT
37
+ * seen. This is the accepted cost of the text-based surface that lets
38
+ * the CLI and the ESLint rules share one implementation (RB-49).
39
+ *
22
40
  * **Per-tool grader rubric (RB-49 calibration):**
23
41
  *
24
42
  * - **description axis (0..40 points):**
@@ -64,10 +82,18 @@ interface DiscoveredTool {
64
82
  /** Tags declared on the call (best-effort). */
65
83
  readonly tags: ReadonlyArray<string>;
66
84
  /**
67
- * Raw object-literal source. Useful for tests + as a context blob
68
- * when the CLI needs to surface the original source in a report.
85
+ * Raw object-literal source (ORIGINAL text, comments included).
86
+ * Useful for tests + as a context blob when the CLI needs to
87
+ * surface the original source in a report.
69
88
  */
70
89
  readonly source: string;
90
+ /**
91
+ * W-044: the same slice with comments blanked - what discovery
92
+ * parsed and what every grading path (examples PII scan,
93
+ * description/parameter scoring) consumes. Same length and line
94
+ * structure as `source`.
95
+ */
96
+ readonly gradingSource: string;
71
97
  }
72
98
  /**
73
99
  * @stable
@@ -186,10 +212,10 @@ declare function gradeTool(tool: DiscoveredTool, findings: ReadonlyArray<LintFin
186
212
  *
187
213
  * @packageDocumentation
188
214
  */
189
- declare const VERSION = "0.6.0";
215
+ declare const VERSION: string;
190
216
  declare const meta: {
191
217
  readonly name: "@graphorin/eslint-plugin";
192
- readonly version: "0.6.0";
218
+ readonly version: string;
193
219
  };
194
220
  declare const rules: {
195
221
  readonly 'no-bare-tool-exec': eslint0.Rule.RuleModule;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/tool-discovery.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;AA6CA;AA2BA;AAaA;AAuBA;AAqBA;AAwBA;AAWA;AAkBA;AAgCA;AAkIA;;;;;;;;;ACvTA;AAEA;AAKA;;;;;;;;;;;AAUW;AAaD;;;;;;;AAiCV;;AAhBe,UDnCE,cAAA,CCmCF;EACc;EAAiB,SAAA,IAAA,EAAA,MAAA;;;;;;;;;;;;2BDtBnB;;iBAEV;;;;;;;;;;KAWL,eAAA;;;;UAaK,WAAA;;iBAKA;;;;;;;;;;;;;;;;;UAkBA,eAAA;;;;;;;;;;qBAUI,cAAc;;;;;;;;;;cAWtB,2BAA2B;;;;;;;;;;cAwB3B,+BAA+B;;;;;;;cAW/B,0BAA0B;;;;;;;;iBAkBvB,yBAAA,gCAAyD;;;;;;;;iBAgCzD,YAAA,OACR;;;;IAML;;;;;;;;;iBA2Ha,SAAA,OACR,0BACI,cAAc,eACvB;;;;;;AA9SH;AA2BA;AAaA;AAuBA;AAqBA;AAwBA;AAWA;AAkBA;AAgCA;AAkIA;;;;;;;;;ACvTA;AAEa,cAFA,OAAA,GAKH,OAAA;AAEG,cALA,IAeH,EAAA;;;;cAVG;;;;;;;EAaP,SAAA,2BAUI,yBAAA;EAQJ,SAsBL,2BAAA,yBAAA;EArBuB,SAAA,uBAAA,yBAAA;CACC;;cApBnB,iBA2BS,EAAA;EACc,SAAA,8BAAA,EAAA,MAAA;EAAiB,SAAA,qCAAA,EAAA,OAAA;EAejC,SAAA,8BAAwB,EAAA,OAAA;EAnBR,SAAA,6BAAA,EAAA,OAAA;EAGd,SAAA,4CAAA,EAAA,OAAA;EACc,SAAA,sCAAA,EAAA,OAAA;EAAiB,SAAA,sCAAA,EAAA,OAAA;;;;;;;;;;cAVxC;wBACkB;yBACC;;;;6BAII;;;eAGd;6BACc;;;;cAehB;;;2BAnBgB;;;aAGd;2BACc"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/tool-discovery.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;AA+DA;AAmCA;AAaA;AAuBA;AAqBA;AAwBA;AAWA;AAkBA;AAoKA;AAoIA;;;;;;;;;ACtdA;AAEA;AAKA;;;;;;;;;;;AAUW;AAaD;;;;;;;AAiCV;;;;;;;;;;;;;;;;;;;;UDlCiB,cAAA;;;;;;;;;;;;;;2BAcU;;iBAEV;;;;;;;;;;;;;;;;;;KAmBL,eAAA;;;;UAaK,WAAA;;iBAKA;;;;;;;;;;;;;;;;;UAkBA,eAAA;;;;;;;;;;qBAUI,cAAc;;;;;;;;;;cAWtB,2BAA2B;;;;;;;;;;cAwB3B,+BAA+B;;;;;;;cAW/B,0BAA0B;;;;;;;;iBAkBvB,yBAAA,gCAAyD;;;;;;;;iBAoKzD,YAAA,OACR;;;;IAML;;;;;;;;;iBA6Ha,SAAA,OACR,0BACI,cAAc,eACvB;;;;;;AA5bH;AAmCA;AAaA;AAuBA;AAqBA;AAwBA;AAWA;AAkBA;AAoKA;AAoIA;;;;;;;;;ACtdA;AAEa,cAFA,OAKH,EAAA,MAAA;AAEG,cALA,IAeH,EAAA;;;;cAVG;;;;;;;EAaP,SAAA,2BAUI,yBAAA;EAQJ,SAsBL,2BAAA,yBAAA;EArBuB,SAAA,uBAAA,yBAAA;CACC;;cApBnB,iBA2BS,EAAA;EACc,SAAA,8BAAA,EAAA,MAAA;EAAiB,SAAA,qCAAA,EAAA,OAAA;EAejC,SAAA,8BAAwB,EAAA,OAAA;EAnBR,SAAA,6BAAA,EAAA,OAAA;EAGd,SAAA,4CAAA,EAAA,OAAA;EACc,SAAA,sCAAA,EAAA,OAAA;EAAiB,SAAA,sCAAA,EAAA,OAAA;;;;;;;;;;cAVxC;wBACkB;yBACC;;;;6BAII;;;eAGd;6BACc;;;;cAehB;;;2BAnBgB;;;aAGd;2BACc"}
package/dist/index.js CHANGED
@@ -1,3 +1,10 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+
4
+ //#region package.json
5
+ var version = "0.7.0";
6
+
7
+ //#endregion
1
8
  //#region src/rules/_comment-utils.ts
2
9
  /**
3
10
  * Returns `true` when the source contains a comment whose `value`
@@ -93,8 +100,72 @@ var no_bare_tool_exec_default = rule$8;
93
100
 
94
101
  //#endregion
95
102
  //#region src/rules/no-implicit-network-call.ts
103
+ /**
104
+ * Rule: `@graphorin/no-implicit-network-call` (DEC-154 / ADR-041).
105
+ * Flags any direct network primitive in code under a `@graphorin/*`
106
+ * package's `src/` directory unless the call site carries an opt-out
107
+ * comment whose text contains `'graphorin-allow-network'`:
108
+ *
109
+ * - calls: `fetch(...)`, `http(s).request/get(...)`, `axios(...)` /
110
+ * `axios.<verb>(...)`, `undici.<verb>(...)` / `got.<verb>(...)`,
111
+ * `net.createConnection/connect(...)`, `tls.connect(...)`,
112
+ * `dgram.createSocket(...)`
113
+ * - constructors: `new XMLHttpRequest()`, `new WebSocket(...)`,
114
+ * `new EventSource(...)`
115
+ * - imports of HTTP clients: `node-fetch`, `undici`, `got`, `axios`,
116
+ * `ky`, `ws` (static, dynamic `import(...)`, and `require(...)`)
117
+ *
118
+ * Companion to the `pnpm run check-no-network` static analysis script;
119
+ * the two matchers are kept in lockstep (this rule mirrors the EB-10
120
+ * hardening that taught the script about undici/got, raw sockets,
121
+ * WebSocket/EventSource, and HTTP-client import specifiers). The lint
122
+ * surface catches the pattern at author time so reviewers do not need
123
+ * to wait for CI to flag a missed network gate.
124
+ *
125
+ * The rule is intentionally limited to the framework's own code paths
126
+ * - consumer applications can call `fetch` freely. Activation is
127
+ * two-stage (W-039): the linted file path must match
128
+ * `/packages/<pkg>/src/` AND the nearest package.json's `name` must
129
+ * start with one of `options[0].packagePrefixes` (default
130
+ * `['@graphorin/']`), so a downstream pnpm monorepo with the standard
131
+ * `packages/*\/src` layout no longer gets errors on its own `fetch()`
132
+ * calls just for adopting `flat/recommended`.
133
+ *
134
+ * FAIL-OPEN fallback: when no package.json resolves above the file
135
+ * (virtual paths in editor buffers or programmatic `Linter` runs), the
136
+ * rule activates on the path match alone - exactly the pre-W-039
137
+ * behaviour, so a resolution hiccup can only over-flag framework-shaped
138
+ * paths, never silently disable the guard.
139
+ *
140
+ * @packageDocumentation
141
+ */
96
142
  const ALLOW_TAG$2 = /graphorin-allow-network/;
97
143
  const FRAMEWORK_PATH_RE = /\bpackages\/[a-z0-9_-]+\/src\b/;
144
+ /**
145
+ * Directory -> nearest package.json `name` (or null when none
146
+ * resolves). Module-level so one walk serves every file of a package
147
+ * within a lint run.
148
+ */
149
+ const packageNameCache = /* @__PURE__ */ new Map();
150
+ /** Walk up from `fileDir` to the nearest package.json name (cached). */
151
+ function nearestPackageName(fileDir) {
152
+ const cached = packageNameCache.get(fileDir);
153
+ if (cached !== void 0) return cached;
154
+ let name = null;
155
+ let dir = fileDir;
156
+ for (;;) try {
157
+ const raw = readFileSync(join(dir, "package.json"), "utf8");
158
+ const parsed = JSON.parse(raw);
159
+ name = typeof parsed.name === "string" ? parsed.name : null;
160
+ break;
161
+ } catch {
162
+ const parent = dirname(dir);
163
+ if (parent === dir) break;
164
+ dir = parent;
165
+ }
166
+ packageNameCache.set(fileDir, name);
167
+ return name;
168
+ }
98
169
  const NETWORK_CALLEES = new Set(["fetch", "XMLHttpRequest"]);
99
170
  const HTTP_VERBS = new Set([
100
171
  "get",
@@ -133,14 +204,25 @@ const rule$7 = {
133
204
  description: "Disallow direct network primitives (`fetch`, `axios`/`undici`/`got`, `http.request`, raw `net`/`tls`/`dgram` sockets, `WebSocket`/`EventSource`/`XMLHttpRequest`, HTTP-client imports) in `@graphorin/*` framework code without an explicit opt-out comment (DEC-154).",
134
205
  recommended: true
135
206
  },
136
- schema: [],
207
+ schema: [{
208
+ type: "object",
209
+ properties: { packagePrefixes: {
210
+ type: "array",
211
+ items: { type: "string" }
212
+ } },
213
+ additionalProperties: false
214
+ }],
137
215
  messages: {
138
216
  forbidden: "direct network call '{{callee}}' in framework code; user actions must initiate network I/O. Add `// graphorin-allow-network: <reason>` to opt out.",
139
217
  forbiddenImport: "HTTP-client import '{{specifier}}' in framework code; user actions must initiate network I/O. Add `// graphorin-allow-network: <reason>` to opt out."
140
218
  }
141
219
  },
142
220
  create(context) {
143
- if (!FRAMEWORK_PATH_RE.test(context.filename.replace(/\\/g, "/"))) return {};
221
+ const filename = context.filename.replace(/\\/g, "/");
222
+ if (!FRAMEWORK_PATH_RE.test(filename)) return {};
223
+ const prefixes = (context.options?.[0] ?? {}).packagePrefixes ?? ["@graphorin/"];
224
+ const packageName = nearestPackageName(dirname(context.filename));
225
+ if (packageName !== null && !prefixes.some((p) => packageName.startsWith(p))) return {};
144
226
  return {
145
227
  CallExpression(node) {
146
228
  const specifier = requiredClientSpecifier(node);
@@ -305,13 +387,19 @@ const rule$5 = {
305
387
  description: "Disallow `.unwrap()` / `.reveal()` calls on `SecretValue` instances. Prefer `.use(fn)` (scoped) or attach a `// graphorin-allow-secret-unwrap: <reason>` opt-out comment for `.reveal()`.",
306
388
  recommended: true
307
389
  },
308
- schema: [],
390
+ schema: [{
391
+ type: "object",
392
+ properties: { allowReceiverPattern: { type: "string" } },
393
+ additionalProperties: false
394
+ }],
309
395
  messages: {
310
396
  avoidReveal: "`.reveal()` returns the unwrapped secret as a V8 string. Prefer `.use(fn)` so the value is scoped to a single callback. Add `// graphorin-allow-secret-unwrap: <reason>` to opt out.",
311
397
  avoidUnwrap: "`.unwrap()` is deprecated - call `.reveal()` (audited) or `.use(fn)` (scoped). The opt-out comment is intentionally NOT honoured for `.unwrap()` so the deprecation stays sharp."
312
398
  }
313
399
  },
314
400
  create(context) {
401
+ const options = context.options?.[0] ?? {};
402
+ const allowReceiver = options.allowReceiverPattern !== void 0 ? new RegExp(options.allowReceiverPattern) : null;
315
403
  return { CallExpression(node) {
316
404
  const callee = node.callee;
317
405
  if (callee.type !== "MemberExpression") return;
@@ -319,6 +407,8 @@ const rule$5 = {
319
407
  if (me.computed) return;
320
408
  if (me.property.type !== "Identifier") return;
321
409
  const propName = me.property.name;
410
+ if (propName !== "unwrap" && propName !== "reveal") return;
411
+ if (allowReceiver?.test(context.sourceCode.getText(me.object)) === true) return;
322
412
  if (propName === "unwrap") {
323
413
  context.report({
324
414
  node,
@@ -544,21 +634,138 @@ const MAX_EXAMPLES$1 = 5;
544
634
  */
545
635
  function discoverToolCallsInSource(file, source) {
546
636
  const out = [];
637
+ const blanked = blankComments(source);
638
+ const searchable = blankStringContents(blanked);
547
639
  const regex = /\btool\s*\(\s*\{/g;
548
- let match = regex.exec(source);
640
+ let match = regex.exec(searchable);
549
641
  while (match !== null) {
550
642
  const objectStart = match.index + match[0].length - 1;
551
- const objectEnd = matchBrace(source, objectStart);
643
+ const objectEnd = matchBrace(blanked, objectStart);
552
644
  if (objectEnd > 0) {
553
645
  const literal = source.slice(objectStart, objectEnd + 1);
554
- const tool = parseToolLiteral(file, countLines(source, match.index), literal);
646
+ const gradingLiteral = blanked.slice(objectStart, objectEnd + 1);
647
+ const tool = parseToolLiteral(file, countLines(source, match.index), literal, gradingLiteral);
555
648
  if (tool !== null) out.push(tool);
556
649
  }
557
650
  regex.lastIndex = objectEnd + 1;
558
- match = regex.exec(source);
651
+ match = regex.exec(searchable);
559
652
  }
560
653
  return out;
561
654
  }
655
+ /**
656
+ * W-044: blank the CONTENTS of string/template literals (quotes kept,
657
+ * newlines preserved) so the discovery regex cannot match `tool(`
658
+ * inside prose. Used only for SEARCHING - parsing reads the
659
+ * strings-intact view.
660
+ */
661
+ function blankStringContents(source) {
662
+ const out = source.split("");
663
+ let inString = null;
664
+ let i = 0;
665
+ while (i < source.length) {
666
+ const ch = source[i];
667
+ if (inString !== null) {
668
+ if (ch === "\\") {
669
+ out[i] = " ";
670
+ if (i + 1 < source.length && source[i + 1] !== "\n") out[i + 1] = " ";
671
+ i += 2;
672
+ continue;
673
+ }
674
+ if (ch === inString) {
675
+ inString = null;
676
+ i += 1;
677
+ continue;
678
+ }
679
+ if (ch !== "\n") out[i] = " ";
680
+ i += 1;
681
+ continue;
682
+ }
683
+ if (ch === "\"" || ch === "'" || ch === "`") inString = ch;
684
+ i += 1;
685
+ }
686
+ return out.join("");
687
+ }
688
+ /**
689
+ * W-044: replace line-comment and block-comment CONTENT with spaces
690
+ * while preserving every newline (offsets and 1-indexed lines never
691
+ * shift).
692
+ * String and template literals pass through untouched; regex literals
693
+ * are tracked with the classic prev-significant-token heuristic so a
694
+ * `/` inside one is never mistaken for a comment opener - when in
695
+ * doubt the scanner does NOT blank.
696
+ *
697
+ * @stable
698
+ */
699
+ function blankComments(source) {
700
+ const out = source.split("");
701
+ let inString = null;
702
+ let prevSignificant = "";
703
+ let i = 0;
704
+ const isRegexStartContext = () => prevSignificant === "" || "([{=,:;!&|?+-*%<>~^".includes(prevSignificant);
705
+ while (i < source.length) {
706
+ const ch = source[i];
707
+ if (inString !== null) {
708
+ if (ch === "\\") {
709
+ i += 2;
710
+ continue;
711
+ }
712
+ if (ch === inString) inString = null;
713
+ i += 1;
714
+ continue;
715
+ }
716
+ if (ch === "\"" || ch === "'" || ch === "`") {
717
+ inString = ch;
718
+ prevSignificant = ch;
719
+ i += 1;
720
+ continue;
721
+ }
722
+ if (ch === "/" && source[i + 1] === "/") {
723
+ while (i < source.length && source[i] !== "\n") {
724
+ out[i] = " ";
725
+ i += 1;
726
+ }
727
+ continue;
728
+ }
729
+ if (ch === "/" && source[i + 1] === "*") {
730
+ out[i] = " ";
731
+ out[i + 1] = " ";
732
+ i += 2;
733
+ while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) {
734
+ if (source[i] !== "\n") out[i] = " ";
735
+ i += 1;
736
+ }
737
+ if (i < source.length) {
738
+ out[i] = " ";
739
+ out[i + 1] = " ";
740
+ i += 2;
741
+ }
742
+ continue;
743
+ }
744
+ if (ch === "/" && isRegexStartContext()) {
745
+ let j = i + 1;
746
+ let inClass = false;
747
+ while (j < source.length && source[j] !== "\n") {
748
+ const cj = source[j];
749
+ if (cj === "\\") {
750
+ j += 2;
751
+ continue;
752
+ }
753
+ if (cj === "[") inClass = true;
754
+ else if (cj === "]") inClass = false;
755
+ else if (cj === "/" && !inClass) break;
756
+ j += 1;
757
+ }
758
+ if (j < source.length && source[j] === "/") {
759
+ prevSignificant = "/";
760
+ i = j + 1;
761
+ continue;
762
+ }
763
+ }
764
+ if (!/\s/.test(ch)) prevSignificant = ch;
765
+ i += 1;
766
+ }
767
+ return out.join("");
768
+ }
562
769
  /** Email PII pattern used by the examples-pii sub-check. */
563
770
  const EMAIL_PATTERN = /[\w.+%-]+@[\w.-]+\.[A-Za-z]{2,}/;
564
771
  /** Numeric-suffix pattern used by the parameter-naming sub-check. */
@@ -583,7 +790,7 @@ function runToolRules(tool, severityOverrides) {
583
790
  if (examplesSeverity !== "off") {
584
791
  if (!tool.hasExamples || tool.examplesCount === 0) findings.push(finding("graphorin/tool-examples-recommended", "examples-missing", examplesSeverity, tool, `tool '${tool.name}' has no examples; add 1-5 worked examples per Anthropic 2026 guidance.`));
585
792
  else if (tool.examplesCount > MAX_EXAMPLES$1) findings.push(finding("graphorin/tool-examples-recommended", "examples-too-many", "error", tool, `tool '${tool.name}' declares ${tool.examplesCount} examples; the upper bound is ${MAX_EXAMPLES$1}.`));
586
- const emailMatch = EMAIL_PATTERN.exec(extractExamplesBlock(tool.source));
793
+ const emailMatch = EMAIL_PATTERN.exec(extractExamplesBlock(tool.gradingSource));
587
794
  if (emailMatch !== null) {
588
795
  const matched = emailMatch[0];
589
796
  findings.push(finding("graphorin/tool-examples-recommended", "examples-pii-detected", "error", tool, `tool '${tool.name}' example contains a real-looking email '${matched}'; replace with synthetic data (e.g. 'user@example.com').`, {
@@ -631,11 +838,20 @@ function gradeTool(tool, findings) {
631
838
  function scoreDescription(tool, findings) {
632
839
  if (findings.length > 0) return 0;
633
840
  const desc = tool.description?.trim() ?? "";
634
- if (desc.length >= 80) return 40;
635
- if (desc.length >= 50) return 32;
636
- if (desc.length >= 30) return 24;
637
- if (desc.length >= MIN_DESCRIPTION_LENGTH$1) return 16;
638
- return 0;
841
+ let lengthScore = 0;
842
+ if (desc.length >= 80) lengthScore = 40;
843
+ else if (desc.length >= 50) lengthScore = 32;
844
+ else if (desc.length >= 30) lengthScore = 24;
845
+ else if (desc.length >= MIN_DESCRIPTION_LENGTH$1) lengthScore = 16;
846
+ if (lengthScore > 16 && desc.length > 0) {
847
+ const words = desc.toLowerCase().split(/\s+/).filter((w) => w.length > 0);
848
+ const counts = /* @__PURE__ */ new Map();
849
+ for (const w of words) counts.set(w, (counts.get(w) ?? 0) + 1);
850
+ const unique = counts.size;
851
+ const topShare = words.length === 0 ? 0 : Math.max(...counts.values()) / words.length;
852
+ if (unique < 4 || topShare > .5) return 16;
853
+ }
854
+ return lengthScore;
639
855
  }
640
856
  function scoreExamples(tool, findings) {
641
857
  if (tool.examplesCount === 0) return 0;
@@ -777,14 +993,14 @@ function extractParameterNames(literal) {
777
993
  }
778
994
  return names;
779
995
  }
780
- function parseToolLiteral(file, line, literal) {
781
- const name = readStringProp(literal, "name") ?? "<anonymous>";
782
- const description = readStringProp(literal, "description");
783
- const examples = countArrayProp(literal, "examples");
784
- const tagsArr = countArrayProp(literal, "tags");
996
+ function parseToolLiteral(file, line, literal, gradingLiteral) {
997
+ const name = readStringProp(gradingLiteral, "name") ?? "<anonymous>";
998
+ const description = readStringProp(gradingLiteral, "description");
999
+ const examples = countArrayProp(gradingLiteral, "examples");
1000
+ const tagsArr = countArrayProp(gradingLiteral, "tags");
785
1001
  const tags = [];
786
1002
  if (tagsArr.present && tagsArr.count > 0) {
787
- const m = /tags\s*:\s*\[([^\]]*)\]/.exec(literal);
1003
+ const m = /tags\s*:\s*\[([^\]]*)\]/.exec(gradingLiteral);
788
1004
  if (m !== null) {
789
1005
  const inner = m[1] ?? "";
790
1006
  const tagRegex = /['"`]([^'"`]+)['"`]/g;
@@ -795,7 +1011,7 @@ function parseToolLiteral(file, line, literal) {
795
1011
  }
796
1012
  }
797
1013
  }
798
- const parameterNames = extractParameterNames(literal);
1014
+ const parameterNames = extractParameterNames(gradingLiteral);
799
1015
  return Object.freeze({
800
1016
  file,
801
1017
  line,
@@ -805,7 +1021,8 @@ function parseToolLiteral(file, line, literal) {
805
1021
  hasExamples: examples.present,
806
1022
  parameterNames: Object.freeze(parameterNames),
807
1023
  tags: Object.freeze(tags),
808
- source: literal
1024
+ source: literal,
1025
+ gradingSource: gradingLiteral
809
1026
  });
810
1027
  }
811
1028
  function countLines(source, index) {
@@ -1027,7 +1244,7 @@ var tool_parameter_naming_default = rule;
1027
1244
  *
1028
1245
  * @packageDocumentation
1029
1246
  */
1030
- const VERSION = "0.6.0";
1247
+ const VERSION = version;
1031
1248
  const meta = {
1032
1249
  name: "@graphorin/eslint-plugin",
1033
1250
  version: VERSION