@cyanheads/mcp-ts-core 0.13.5 → 0.13.6
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/AGENTS.md +4 -4
- package/CLAUDE.md +4 -4
- package/README.md +1 -1
- package/biome.json +1 -1
- package/changelog/0.13.x/0.13.6.md +49 -0
- package/dist/linter/rules/enrichment-rules.d.ts +5 -4
- package/dist/linter/rules/enrichment-rules.d.ts.map +1 -1
- package/dist/linter/rules/enrichment-rules.js +99 -22
- package/dist/linter/rules/enrichment-rules.js.map +1 -1
- package/dist/linter/rules/error-contract-rules.d.ts +46 -10
- package/dist/linter/rules/error-contract-rules.d.ts.map +1 -1
- package/dist/linter/rules/error-contract-rules.js +180 -27
- package/dist/linter/rules/error-contract-rules.js.map +1 -1
- package/dist/linter/rules/format-parity-rules.js +1 -1
- package/dist/linter/rules/format-parity-rules.js.map +1 -1
- package/dist/linter/rules/index.d.ts +1 -1
- package/dist/linter/rules/index.d.ts.map +1 -1
- package/dist/linter/rules/index.js +1 -1
- package/dist/linter/rules/index.js.map +1 -1
- package/dist/linter/rules/resource-rules.d.ts.map +1 -1
- package/dist/linter/rules/resource-rules.js +2 -1
- package/dist/linter/rules/resource-rules.js.map +1 -1
- package/dist/linter/rules/tool-rules.d.ts.map +1 -1
- package/dist/linter/rules/tool-rules.js +2 -1
- package/dist/linter/rules/tool-rules.js.map +1 -1
- package/dist/types-global/errors.d.ts +18 -0
- package/dist/types-global/errors.d.ts.map +1 -1
- package/framework-skills/add-service/SKILL.md +5 -2
- package/framework-skills/add-tool/SKILL.md +4 -2
- package/framework-skills/api-errors/SKILL.md +19 -3
- package/framework-skills/api-linter/SKILL.md +88 -15
- package/package.json +3 -3
- package/scripts/lint-mcp.ts +43 -4
- package/templates/AGENTS.md +1 -1
- package/templates/CLAUDE.md +1 -1
- package/templates/devcheck.config.json +1 -0
- package/templates/package.json +1 -1
- package/templates/src/mcp-server/tools/definitions/echo.tool.ts +7 -1
package/AGENTS.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Package:** `@cyanheads/mcp-ts-core`
|
|
4
|
-
**Version:** 0.13.
|
|
4
|
+
**Version:** 0.13.6
|
|
5
5
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
6
6
|
**MCP SDK:** `@modelcontextprotocol/server` ^2.0.0 (protocol revisions 2026-07-28 and 2025-*)
|
|
7
7
|
**Zod:** ^4.6.5
|
|
@@ -247,7 +247,7 @@ export const myTool = tool('my_tool', {
|
|
|
247
247
|
**`format`**: Maps output to MCP `content[]`. Different clients forward different surfaces to the agent — some (Claude Code) read `structuredContent` from `output`, others (Claude Desktop) read `content[]` from `format()`. `format()` is the markdown twin of `structuredContent`, not a reduced summary.
|
|
248
248
|
|
|
249
249
|
- **Parity is lint-enforced.** Every terminal field in `output` must appear in `format()`'s rendered text (via sentinel injection), or the `format-parity` rule fails `bun run lint:mcp` / `devcheck`.
|
|
250
|
-
- **Primary fix:** render the missing field in `format()`.
|
|
250
|
+
- **Primary fix:** render the missing field in `format()`. For list/detail variants, declare one flat `z.object` with a `kind` discriminator and presence-based optional arms, rendered by independent `if` blocks — `tool()` rejects a `z.discriminatedUnion` output root.
|
|
251
251
|
- **Escape hatch:** if the schema was over-typed for a genuinely dynamic upstream API, relax it (`z.object({}).passthrough()`) — passthrough still flows data to `structuredContent`.
|
|
252
252
|
- **Fallback:** omit `format` for JSON stringify. Additional formatters in `/utils`: `markdown()` (builder), `diffFormatter` (async), `tableFormatter`, `treeFormatter`.
|
|
253
253
|
|
|
@@ -378,7 +378,7 @@ See `api-context` skill for full details.
|
|
|
378
378
|
|
|
379
379
|
## Error Handling
|
|
380
380
|
|
|
381
|
-
**Recommended path: declare a typed error contract.** Add `errors: [{ reason, code, when, recovery, retryable?, severity? }]` to `tool()` / `resource()`. Handler gets `ctx.fail(reason, msg?, data?)` typed against the reason union — typos fail at compile time. Runtime auto-populates `data.reason` for observability; linter enforces conformance against the handler body. `recovery` is required (≥5 words, lint-validated) — the single source of truth for the wire hint. Spread `ctx.recoveryFor('reason')` into `data` to opt the contract recovery onto the wire (framework mirrors `data.recovery.hint` into `content[]` text); override with explicit `{ recovery: { hint: '...' } }` when runtime context matters. Optional `severity` (`debug`/`info`/`notice`/`warning`) logs a modeled outcome below `error` — tools only, logging only: the wire envelope, the span status, and the `mcp.tool.*` metrics are untouched.
|
|
381
|
+
**Recommended path: declare a typed error contract.** Add `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` to `tool()` / `resource()`. Handler gets `ctx.fail(reason, msg?, data?)` typed against the reason union — typos fail at compile time. Runtime auto-populates `data.reason` for observability; linter enforces conformance against the handler body. `recovery` is required (≥5 words, lint-validated) — the single source of truth for the wire hint. Spread `ctx.recoveryFor('reason')` into `data` to opt the contract recovery onto the wire (framework mirrors `data.recovery.hint` into `content[]` text); override with explicit `{ recovery: { hint: '...' } }` when runtime context matters. Optional `severity` (`debug`/`info`/`notice`/`warning`) logs a modeled outcome below `error` — tools only, logging only: the wire envelope, the span status, and the `mcp.tool.*` metrics are untouched. Optional `thrownBy: 'service'` marks an entry the service layer produces, so `error-contract-unthrown` skips it while still checking the handler's own reasons — lint-only metadata, nothing at runtime reads it.
|
|
382
382
|
|
|
383
383
|
```ts
|
|
384
384
|
errors: [
|
|
@@ -420,7 +420,7 @@ For HTTP responses from upstream APIs, use `httpErrorFromResponse(response, { se
|
|
|
420
420
|
|
|
421
421
|
**Error-path parity.** Tool errors: `content[]` carries `Error: <message>`, then `Recovery: <hint>` when the hint says something the message does not already contain, then a closing `(reason … · not retryable)` for whichever of `data.reason` / `data.retryable` is present; the numeric code and `data.issues` stay JSON-only. `structuredContent.error` carries `{ code, message, data? }`. No `_meta.error`. Resources re-throw via JSON-RPC error envelope. An argument rejection is one of them: `-32602` with `data.issues`, plus `data.reason: 'invalid_arguments'` and a hint synthesized from the issues and the root schema — never a tool-declared `reason`, since the handler never ran. `client_capability_missing` is the second framework-owned reason: a `ctx.requestInput` return a 2025-era connection cannot serve is refused before any wire traffic as `-32600` carrying that reason and a hint naming the capability.
|
|
422
422
|
|
|
423
|
-
**Lint rules** (all warnings, surfaced in `devcheck`): `prefer-mcp-error-in-handler`, `prefer-error-factory`, `preserve-cause-on-rethrow`, `no-stringify-upstream-error`, `error-contract-conformance`, `error-contract-prefer-fail`, `error-contract-unthrown` (a declared reason no literal `ctx.fail`/`ctx.recoveryFor` in the handler names). See `api-linter` skill.
|
|
423
|
+
**Lint rules** (all warnings, surfaced in `devcheck`): `prefer-mcp-error-in-handler`, `prefer-error-factory`, `preserve-cause-on-rethrow`, `no-stringify-upstream-error`, `error-contract-conformance`, `error-contract-prefer-fail`, `error-contract-unthrown` (a declared reason no literal `ctx.fail`/`ctx.recoveryFor` in the handler names, unless marked `thrownBy: 'service'`), `error-contract-recovery-unforwarded` (a `ctx.fail` site carrying neither `ctx.recoveryFor('<reason>')` nor its own `recovery` key, so the declared hint reaches neither client surface). See `api-linter` skill.
|
|
424
424
|
|
|
425
425
|
See `api-errors` skill for the full pattern-matching table, error code reference, and detailed examples.
|
|
426
426
|
|
package/CLAUDE.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Package:** `@cyanheads/mcp-ts-core`
|
|
4
|
-
**Version:** 0.13.
|
|
4
|
+
**Version:** 0.13.6
|
|
5
5
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
6
6
|
**MCP SDK:** `@modelcontextprotocol/server` ^2.0.0 (protocol revisions 2026-07-28 and 2025-*)
|
|
7
7
|
**Zod:** ^4.6.5
|
|
@@ -247,7 +247,7 @@ export const myTool = tool('my_tool', {
|
|
|
247
247
|
**`format`**: Maps output to MCP `content[]`. Different clients forward different surfaces to the agent — some (Claude Code) read `structuredContent` from `output`, others (Claude Desktop) read `content[]` from `format()`. `format()` is the markdown twin of `structuredContent`, not a reduced summary.
|
|
248
248
|
|
|
249
249
|
- **Parity is lint-enforced.** Every terminal field in `output` must appear in `format()`'s rendered text (via sentinel injection), or the `format-parity` rule fails `bun run lint:mcp` / `devcheck`.
|
|
250
|
-
- **Primary fix:** render the missing field in `format()`.
|
|
250
|
+
- **Primary fix:** render the missing field in `format()`. For list/detail variants, declare one flat `z.object` with a `kind` discriminator and presence-based optional arms, rendered by independent `if` blocks — `tool()` rejects a `z.discriminatedUnion` output root.
|
|
251
251
|
- **Escape hatch:** if the schema was over-typed for a genuinely dynamic upstream API, relax it (`z.object({}).passthrough()`) — passthrough still flows data to `structuredContent`.
|
|
252
252
|
- **Fallback:** omit `format` for JSON stringify. Additional formatters in `/utils`: `markdown()` (builder), `diffFormatter` (async), `tableFormatter`, `treeFormatter`.
|
|
253
253
|
|
|
@@ -378,7 +378,7 @@ See `api-context` skill for full details.
|
|
|
378
378
|
|
|
379
379
|
## Error Handling
|
|
380
380
|
|
|
381
|
-
**Recommended path: declare a typed error contract.** Add `errors: [{ reason, code, when, recovery, retryable?, severity? }]` to `tool()` / `resource()`. Handler gets `ctx.fail(reason, msg?, data?)` typed against the reason union — typos fail at compile time. Runtime auto-populates `data.reason` for observability; linter enforces conformance against the handler body. `recovery` is required (≥5 words, lint-validated) — the single source of truth for the wire hint. Spread `ctx.recoveryFor('reason')` into `data` to opt the contract recovery onto the wire (framework mirrors `data.recovery.hint` into `content[]` text); override with explicit `{ recovery: { hint: '...' } }` when runtime context matters. Optional `severity` (`debug`/`info`/`notice`/`warning`) logs a modeled outcome below `error` — tools only, logging only: the wire envelope, the span status, and the `mcp.tool.*` metrics are untouched.
|
|
381
|
+
**Recommended path: declare a typed error contract.** Add `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` to `tool()` / `resource()`. Handler gets `ctx.fail(reason, msg?, data?)` typed against the reason union — typos fail at compile time. Runtime auto-populates `data.reason` for observability; linter enforces conformance against the handler body. `recovery` is required (≥5 words, lint-validated) — the single source of truth for the wire hint. Spread `ctx.recoveryFor('reason')` into `data` to opt the contract recovery onto the wire (framework mirrors `data.recovery.hint` into `content[]` text); override with explicit `{ recovery: { hint: '...' } }` when runtime context matters. Optional `severity` (`debug`/`info`/`notice`/`warning`) logs a modeled outcome below `error` — tools only, logging only: the wire envelope, the span status, and the `mcp.tool.*` metrics are untouched. Optional `thrownBy: 'service'` marks an entry the service layer produces, so `error-contract-unthrown` skips it while still checking the handler's own reasons — lint-only metadata, nothing at runtime reads it.
|
|
382
382
|
|
|
383
383
|
```ts
|
|
384
384
|
errors: [
|
|
@@ -420,7 +420,7 @@ For HTTP responses from upstream APIs, use `httpErrorFromResponse(response, { se
|
|
|
420
420
|
|
|
421
421
|
**Error-path parity.** Tool errors: `content[]` carries `Error: <message>`, then `Recovery: <hint>` when the hint says something the message does not already contain, then a closing `(reason … · not retryable)` for whichever of `data.reason` / `data.retryable` is present; the numeric code and `data.issues` stay JSON-only. `structuredContent.error` carries `{ code, message, data? }`. No `_meta.error`. Resources re-throw via JSON-RPC error envelope. An argument rejection is one of them: `-32602` with `data.issues`, plus `data.reason: 'invalid_arguments'` and a hint synthesized from the issues and the root schema — never a tool-declared `reason`, since the handler never ran. `client_capability_missing` is the second framework-owned reason: a `ctx.requestInput` return a 2025-era connection cannot serve is refused before any wire traffic as `-32600` carrying that reason and a hint naming the capability.
|
|
422
422
|
|
|
423
|
-
**Lint rules** (all warnings, surfaced in `devcheck`): `prefer-mcp-error-in-handler`, `prefer-error-factory`, `preserve-cause-on-rethrow`, `no-stringify-upstream-error`, `error-contract-conformance`, `error-contract-prefer-fail`, `error-contract-unthrown` (a declared reason no literal `ctx.fail`/`ctx.recoveryFor` in the handler names). See `api-linter` skill.
|
|
423
|
+
**Lint rules** (all warnings, surfaced in `devcheck`): `prefer-mcp-error-in-handler`, `prefer-error-factory`, `preserve-cause-on-rethrow`, `no-stringify-upstream-error`, `error-contract-conformance`, `error-contract-prefer-fail`, `error-contract-unthrown` (a declared reason no literal `ctx.fail`/`ctx.recoveryFor` in the handler names, unless marked `thrownBy: 'service'`), `error-contract-recovery-unforwarded` (a `ctx.fail` site carrying neither `ctx.recoveryFor('<reason>')` nor its own `recovery` key, so the declared hint reaches neither client surface). See `api-linter` skill.
|
|
424
424
|
|
|
425
425
|
See `api-errors` skill for the full pattern-matching table, error code reference, and detailed examples.
|
|
426
426
|
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<div align="center">
|
|
8
8
|
|
|
9
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://modelcontextprotocol.io/specification/2026-07-28)
|
|
10
10
|
|
|
11
11
|
[](https://modelcontextprotocol.io/) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
12
12
|
|
package/biome.json
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "A ctx.fail site that never forwards its declared recovery now warns, an errors[] entry can mark itself thrownBy: 'service' so the unthrown rule skips it, capped-list-no-truncation stops firing on value bounds like max_depth_km, and devcheck.config.json can carry the truncation allowlist."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: false
|
|
5
|
+
agent-notes: |
|
|
6
|
+
Four adoption steps for a consumer upgrading from 0.13.5.
|
|
7
|
+
|
|
8
|
+
1. `error-contract-recovery-unforwarded` is new and fires on the next
|
|
9
|
+
`devcheck` / `lint:mcp`: a literal `ctx.fail('<reason>', …)` site carrying
|
|
10
|
+
neither `ctx.recoveryFor('<reason>')` nor its own `recovery` key. Clear it
|
|
11
|
+
by spreading `...ctx.recoveryFor('<reason>')` into the data argument, by
|
|
12
|
+
passing the resolver as that argument, or with an explicit
|
|
13
|
+
`recovery: { hint }` when the hint needs runtime context.
|
|
14
|
+
2. An `errors[]` reason the service layer produces takes `thrownBy: 'service'`,
|
|
15
|
+
which is what `error-contract-unthrown` reads to skip it. Prefer it over
|
|
16
|
+
moving the handler's literal `ctx.fail` out of scan range — that silences
|
|
17
|
+
the rule for the whole definition, handler-local reasons included.
|
|
18
|
+
3. Declare `lint.truncationAllowlist` in `devcheck.config.json` in place of
|
|
19
|
+
`MCP_LINT_TRUNCATION_ALLOWLIST` on each script entry, and drop the env var
|
|
20
|
+
from the `devcheck` and `lint:mcp` scripts once the key is in place. A
|
|
21
|
+
scaffolded config predating this release has no `lint` key; add it.
|
|
22
|
+
4. Drop any `truncationAllowlist` entry added only to silence
|
|
23
|
+
`capped-list-no-truncation` on a value-bound `max_*` input — those no
|
|
24
|
+
longer warn.
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# 0.13.6 — 2026-09-19
|
|
28
|
+
|
|
29
|
+
## Added
|
|
30
|
+
|
|
31
|
+
- **Lint rule `error-contract-recovery-unforwarded`** (warning, [#255](https://github.com/cyanheads/mcp-ts-core/issues/255)) — one diagnostic per literal `ctx.fail('<reason>', …)` site carrying neither `ctx.recoveryFor('<reason>')` nor its own `recovery` key, so the declared hint reaches neither client surface. A site whose only resolver names a different reason warns too, naming both.
|
|
32
|
+
- **`thrownBy: 'service'` on an `errors[]` entry** ([#462](https://github.com/cyanheads/mcp-ts-core/issues/462)) — lint-only metadata marking a reason produced below the handler, so `error-contract-unthrown` skips it while the handler's own reasons keep being checked. The entry stays typed, advertised, and thrown exactly as an unmarked one.
|
|
33
|
+
- **`lint.truncationAllowlist` in `devcheck.config.json`** ([#388](https://github.com/cyanheads/mcp-ts-core/issues/388)) — read by `scripts/lint-mcp.ts` and forwarded as `LintInput.truncationAllowlist`, so one project-owned declaration covers every entrypoint that shells out to the linter. Takes an array of tool names or `false`; precedence is explicit `LintInput`, then the config file, then `MCP_LINT_TRUNCATION_ALLOWLIST`.
|
|
34
|
+
|
|
35
|
+
## Changed
|
|
36
|
+
|
|
37
|
+
- **The scaffold's `echo` tool forwards its declared recovery** ([#255](https://github.com/cyanheads/mcp-ts-core/issues/255)) — `templates/src/mcp-server/tools/definitions/echo.tool.ts` spreads `ctx.recoveryFor('empty_message')`, so a new server starts from the shape the rule asks for.
|
|
38
|
+
- Skill versions: `add-service` 1.10 → 1.11, `add-tool` 2.28 → 2.29, `api-errors` 1.14 → 1.15, `api-linter` 1.16 → 1.17.
|
|
39
|
+
|
|
40
|
+
## Fixed
|
|
41
|
+
|
|
42
|
+
- **`capped-list-no-truncation`'s `max_` arm requires the counted noun to name a returned list** ([#345](https://github.com/cyanheads/mcp-ts-core/issues/345)) — a depth-0 array in `output` (plural-insensitive, a trailing `_count` stripped first) or a generic result container (`results`, `records`, `items`, `rows`, `hits`, `entries`, `matches`, `count`, `page`, `docs`). Value bounds (`max_depth_km`, `maxLat`, `max_date`) and work budgets (`max_court_lookups`, `maxCharacters`) stop warning; `limit`, `<noun>_limit`, and the page-size idioms are unchanged.
|
|
43
|
+
- **`error-contract-unthrown` skips a definition whose `ctx.recoveryFor(` takes a non-literal first argument** ([#462](https://github.com/cyanheads/mcp-ts-core/issues/462)), as it already did for `ctx.fail(` — unknown reasons are in play either way. Its message now names all three fixes.
|
|
44
|
+
- **The `format-parity` diagnostic no longer points authors at a `z.discriminatedUnion` output** ([#268](https://github.com/cyanheads/mcp-ts-core/issues/268)), which `tool()` rejects before any lint rule runs. The "Primary fix" line names a flat `z.object` with a `kind` discriminator and presence-based optional arms, rendered by independent `if` blocks.
|
|
45
|
+
- **The `sanitizeHtml` event-handler property matches only inside a tag** ([#463](https://github.com/cyanheads/mcp-ts-core/issues/463)) — the whole-string `/\bon\w+\s*=/` also matched inert text such as `on0=`, failing roughly once in 300 runs. `on0=` and `<p>onclick=1</p>` are pinned as `examples`; the sanitizer itself is unchanged.
|
|
46
|
+
|
|
47
|
+
## Dependencies
|
|
48
|
+
|
|
49
|
+
- `@biomejs/biome` 2.5.13 → 2.5.14, `fast-check` ^4.10.0 → ^4.10.1.
|
|
@@ -45,10 +45,11 @@ export interface TruncationOptions {
|
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
47
|
* Warns when a tool takes a cap-like input field (`limit`, `per_page`,
|
|
48
|
-
* `maxRecords`, … — see `isCapFieldName`)
|
|
49
|
-
*
|
|
50
|
-
* `truncated` key in its declared `enrichment` shape, nor
|
|
51
|
-
* nor `truncated` or `totalCount` as top-level
|
|
48
|
+
* `maxRecords`, … — see `isCapFieldName`) that plausibly caps the returned list
|
|
49
|
+
* (see `capsTheList`) and returns an array output, but declares no truncation
|
|
50
|
+
* disclosure — neither a `truncated` key in its declared `enrichment` shape, nor
|
|
51
|
+
* `totalCount` in enrichment, nor `truncated` or `totalCount` as top-level
|
|
52
|
+
* `output` keys.
|
|
52
53
|
*
|
|
53
54
|
* The established `enrich.total()` convention (`totalCount`) and bespoke output fields
|
|
54
55
|
* count as honest disclosure — the rule fires only on a genuinely silent cap.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enrichment-rules.d.ts","sourceRoot":"","sources":["../../../src/linter/rules/enrichment-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"enrichment-rules.d.ts","sourceRoot":"","sources":["../../../src/linter/rules/enrichment-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAqJtE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE;IAAE,UAAU,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,EAC/F,cAAc,EAAE,kBAAkB,EAClC,cAAc,EAAE,MAAM,GACrB,cAAc,EAAE,CA0IlB;AAED,sDAAsD;AACtD,MAAM,WAAW,iBAAiB;IAChC,2EAA2E;IAC3E,mBAAmB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC;CACrD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EACC;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,GAC3E,IAAI,GACJ,SAAS,EACb,iBAAiB,CAAC,EAAE,iBAAiB,GACpC,cAAc,EAAE,CAgDlB"}
|
|
@@ -19,13 +19,60 @@ import { getCoreDefType, objectShape, objectShapeKeys, unwrapWrappers } from './
|
|
|
19
19
|
/** Cap-like names that carry no `max`/`limit` morpheme and must be listed outright. */
|
|
20
20
|
const CAP_FIELD_EXACT = new Set(['limit', 'per_page', 'page_size']);
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* Nouns that name a generic result container whatever the array is called, so a
|
|
23
|
+
* `max_<noun>` built on one of them caps the list even when nothing in `output`
|
|
24
|
+
* carries the same name — `maxRecords` against an `articles` array. Stored
|
|
25
|
+
* singular; the counted noun is singularized before the lookup, so the plural
|
|
26
|
+
* spellings (`results`, `records`, `items`, `rows`, `hits`, `entries`,
|
|
27
|
+
* `matches`, `docs`) resolve here too.
|
|
28
|
+
*/
|
|
29
|
+
const RESULT_CONTAINER_NOUNS = new Set([
|
|
30
|
+
'count',
|
|
31
|
+
'doc',
|
|
32
|
+
'entry',
|
|
33
|
+
'hit',
|
|
34
|
+
'item',
|
|
35
|
+
'match',
|
|
36
|
+
'page',
|
|
37
|
+
'record',
|
|
38
|
+
'result',
|
|
39
|
+
'row',
|
|
40
|
+
]);
|
|
41
|
+
/** camelCase → snake_case, lowercased, so `maxRecords` and `max_records` are one case. */
|
|
42
|
+
function snakeCase(key) {
|
|
43
|
+
return key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Singularizes on a bounded suffix set — `articles` → `article`, `studies` →
|
|
47
|
+
* `study`, `matches` → `match`. Not a general English pluralizer: it only has to
|
|
48
|
+
* make an input's counted noun and an output array's name meet in the middle.
|
|
49
|
+
*/
|
|
50
|
+
function singularize(noun) {
|
|
51
|
+
if (noun.endsWith('ies'))
|
|
52
|
+
return `${noun.slice(0, -3)}y`;
|
|
53
|
+
if (/(?:s|x|ch|sh)es$/.test(noun))
|
|
54
|
+
return noun.slice(0, -2);
|
|
55
|
+
if (noun.endsWith('s') && !noun.endsWith('ss'))
|
|
56
|
+
return noun.slice(0, -1);
|
|
57
|
+
return noun;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The noun a `max_*` field counts: the name minus the `max_` prefix, minus a
|
|
61
|
+
* trailing `_count` (`max_result_count` → `result`), reduced to its last
|
|
62
|
+
* underscore segment and singularized.
|
|
63
|
+
*/
|
|
64
|
+
function countedNoun(key) {
|
|
65
|
+
const stem = snakeCase(key)
|
|
66
|
+
.replace(/^max_/, '')
|
|
67
|
+
.replace(/_count$/, '');
|
|
68
|
+
return singularize(stem.split('_').pop() ?? stem);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* True for a depth-0 input field name that is cap-*shaped*.
|
|
23
72
|
*
|
|
24
73
|
* Matched by shape, not by an enumeration: an allowlist turns every new cap noun
|
|
25
74
|
* (`maxRecords`, `maxRows`, `resultLimit`) into a silent gap where the rule never
|
|
26
|
-
* runs at all, which is the same defect class the rule exists to catch.
|
|
27
|
-
* naming conventions normalize to the same snake form first, so `maxRecords` and
|
|
28
|
-
* `max_records` are one case.
|
|
75
|
+
* runs at all, which is the same defect class the rule exists to catch.
|
|
29
76
|
*
|
|
30
77
|
* - `limit`, and any `<noun>_limit` / `<noun>Limit`
|
|
31
78
|
* - any `max_<noun>` / `max<Noun>`
|
|
@@ -34,17 +81,36 @@ const CAP_FIELD_EXACT = new Set(['limit', 'per_page', 'page_size']);
|
|
|
34
81
|
* Deliberately NOT matched: bare `count`, `size`, `n`, `rows`, `records`, and
|
|
35
82
|
* words that merely start with the letters (`maximum`).
|
|
36
83
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* says anything, and `truncationAllowlist` exempts a tool that trips it anyway.
|
|
84
|
+
* Shape alone cannot separate a cap on *how many* from an upper bound on a
|
|
85
|
+
* *value* or a budget on work done, so the `max_` arm is narrowed further by
|
|
86
|
+
* `capsTheList`.
|
|
41
87
|
*/
|
|
42
88
|
function isCapFieldName(key) {
|
|
43
|
-
const normalized = key
|
|
89
|
+
const normalized = snakeCase(key);
|
|
44
90
|
return (CAP_FIELD_EXACT.has(normalized) ||
|
|
45
91
|
normalized.startsWith('max_') ||
|
|
46
92
|
normalized.endsWith('_limit'));
|
|
47
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* True when a cap-shaped input field plausibly caps the returned list.
|
|
96
|
+
*
|
|
97
|
+
* `limit` / `<noun>_limit` / the page-size idioms say what they bound in the
|
|
98
|
+
* name itself and always qualify. `max_<noun>` does not: the same spelling
|
|
99
|
+
* carries value bounds (`max_depth_km`, `maxLat`, `max_date`) and budgets on
|
|
100
|
+
* secondary work (`max_court_lookups`, `maxCharacters`), none of which slice the
|
|
101
|
+
* array. So the counted noun has to name something the tool returns — a depth-0
|
|
102
|
+
* array in `output`, or a generic result container.
|
|
103
|
+
*
|
|
104
|
+
* Accepted false negative: a domain cap naming neither, `max_studies` against a
|
|
105
|
+
* `documents` array, goes silent. Nothing in the declaration separates it from a
|
|
106
|
+
* value bound, and `truncationAllowlist` only suppresses — it cannot re-enable.
|
|
107
|
+
*/
|
|
108
|
+
function capsTheList(key, arrayNouns) {
|
|
109
|
+
if (!snakeCase(key).startsWith('max_'))
|
|
110
|
+
return true;
|
|
111
|
+
const noun = countedNoun(key);
|
|
112
|
+
return RESULT_CONTAINER_NOUNS.has(noun) || arrayNouns.has(noun);
|
|
113
|
+
}
|
|
48
114
|
/**
|
|
49
115
|
* Output field names that almost always indicate agent-facing context rather
|
|
50
116
|
* than domain payload. Kept deliberately tiny to avoid false positives —
|
|
@@ -228,10 +294,11 @@ export function lintEnrichmentContract(def, definitionType, definitionName) {
|
|
|
228
294
|
}
|
|
229
295
|
/**
|
|
230
296
|
* Warns when a tool takes a cap-like input field (`limit`, `per_page`,
|
|
231
|
-
* `maxRecords`, … — see `isCapFieldName`)
|
|
232
|
-
*
|
|
233
|
-
* `truncated` key in its declared `enrichment` shape, nor
|
|
234
|
-
* nor `truncated` or `totalCount` as top-level
|
|
297
|
+
* `maxRecords`, … — see `isCapFieldName`) that plausibly caps the returned list
|
|
298
|
+
* (see `capsTheList`) and returns an array output, but declares no truncation
|
|
299
|
+
* disclosure — neither a `truncated` key in its declared `enrichment` shape, nor
|
|
300
|
+
* `totalCount` in enrichment, nor `truncated` or `totalCount` as top-level
|
|
301
|
+
* `output` keys.
|
|
235
302
|
*
|
|
236
303
|
* The established `enrich.total()` convention (`totalCount`) and bespoke output fields
|
|
237
304
|
* count as honest disclosure — the rule fires only on a genuinely silent cap.
|
|
@@ -251,12 +318,16 @@ export function lintCappedListTruncation(def, truncationOptions) {
|
|
|
251
318
|
// per variant, so a cap in any branch counts — the rule asks whether the tool
|
|
252
319
|
// can be capped at all, not whether every branch caps.
|
|
253
320
|
const inputKeys = inputVariants(def.input).flatMap((variant) => objectShapeKeys(variant));
|
|
254
|
-
const
|
|
255
|
-
if (
|
|
321
|
+
const capShapedKeys = inputKeys.filter(isCapFieldName);
|
|
322
|
+
if (capShapedKeys.length === 0)
|
|
256
323
|
return [];
|
|
257
|
-
// Check output for at least one array-typed field
|
|
258
|
-
|
|
259
|
-
|
|
324
|
+
// Check output for at least one array-typed field. The array names are also
|
|
325
|
+
// what a `max_<noun>` cap has to correlate with to count as a list cap.
|
|
326
|
+
const arrayNouns = topLevelArrayNouns(def.output);
|
|
327
|
+
if (arrayNouns.size === 0)
|
|
328
|
+
return [];
|
|
329
|
+
const capKeys = capShapedKeys.filter((key) => capsTheList(key, arrayNouns));
|
|
330
|
+
if (capKeys.length === 0)
|
|
260
331
|
return [];
|
|
261
332
|
// Check for truncation disclosure:
|
|
262
333
|
// 1. Declared enrichment has `truncated` or `totalCount`
|
|
@@ -279,12 +350,18 @@ export function lintCappedListTruncation(def, truncationOptions) {
|
|
|
279
350
|
},
|
|
280
351
|
];
|
|
281
352
|
}
|
|
282
|
-
/**
|
|
283
|
-
|
|
353
|
+
/**
|
|
354
|
+
* The singularized names of every depth-0 field whose core Zod type is `array`.
|
|
355
|
+
* Empty means the tool returns no list at all, which is the rule's array
|
|
356
|
+
* precondition; non-empty is also what a `max_<noun>` cap correlates against.
|
|
357
|
+
*/
|
|
358
|
+
function topLevelArrayNouns(schema) {
|
|
284
359
|
const shape = objectShape(schema);
|
|
285
360
|
if (!shape)
|
|
286
|
-
return
|
|
287
|
-
return Object.
|
|
361
|
+
return new Set();
|
|
362
|
+
return new Set(Object.entries(shape)
|
|
363
|
+
.filter(([, field]) => getCoreDefType(unwrapWrappers(field)) === 'array')
|
|
364
|
+
.map(([key]) => singularize(snakeCase(key).split('_').pop() ?? key)));
|
|
288
365
|
}
|
|
289
366
|
/**
|
|
290
367
|
* True when truncation is already disclosed — enrichment block has `truncated` or
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enrichment-rules.js","sourceRoot":"","sources":["../../../src/linter/rules/enrichment-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AAExE,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEjG,uFAAuF;AACvF,MAAM,eAAe,GAAwB,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC,CAAC;AAEzF
|
|
1
|
+
{"version":3,"file":"enrichment-rules.js","sourceRoot":"","sources":["../../../src/linter/rules/enrichment-rules.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Lint rules for the declarative `errors[]` contract on tool and
|
|
3
3
|
* resource definitions. Validates structure (codes, reasons, severity),
|
|
4
|
-
* uniqueness, and — when a contract is present — cross-checks the handler body
|
|
5
|
-
*
|
|
6
|
-
* never
|
|
4
|
+
* uniqueness, and — when a contract is present — cross-checks the handler body:
|
|
5
|
+
* codes thrown but not declared, reasons declared but never thrown, and throw
|
|
6
|
+
* sites that never put the declared `recovery` on the wire.
|
|
7
7
|
* @module src/linter/rules/error-contract-rules
|
|
8
8
|
*/
|
|
9
9
|
import type { LintDefinitionType, LintDiagnostic } from '../types.js';
|
|
@@ -94,18 +94,54 @@ export declare function scanReasonCalls(source: string, callee: string): ReasonC
|
|
|
94
94
|
*
|
|
95
95
|
* **Trigger.** Only when the handler holds at least one literal `ctx.fail(`. A
|
|
96
96
|
* handler with none produces its reasons somewhere the scan cannot reach, and
|
|
97
|
-
* firing there would warn on every service-layer definition. A `ctx.fail(`
|
|
98
|
-
* first argument is not a string literal makes the
|
|
99
|
-
* whole definition is skipped rather than guessed
|
|
97
|
+
* firing there would warn on every service-layer definition. A `ctx.fail(` or
|
|
98
|
+
* `ctx.recoveryFor(` whose first argument is not a string literal makes the
|
|
99
|
+
* named set unknowable, so the whole definition is skipped rather than guessed
|
|
100
|
+
* at.
|
|
100
101
|
*
|
|
101
102
|
* **Warning, never error.** A reason produced outside the handler closure is
|
|
102
|
-
* invisible to any `toString()` scan, so the rule can never prove absence.
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
103
|
+
* invisible to any `toString()` scan, so the rule can never prove absence. An
|
|
104
|
+
* entry the service layer produces says so with `thrownBy: 'service'` and is
|
|
105
|
+
* skipped while the handler's own reasons keep being checked. Still silent
|
|
106
|
+
* without a marker: a `createFail(errors)` resolver built outside the handler,
|
|
107
|
+
* and an aliased `const fail = ctx.fail`.
|
|
106
108
|
*/
|
|
107
109
|
export declare function lintErrorContractUnthrown(def: {
|
|
108
110
|
handler?: unknown;
|
|
109
111
|
errors?: unknown;
|
|
110
112
|
}, definitionType: LintDefinitionType, definitionName: string): LintDiagnostic[];
|
|
113
|
+
/**
|
|
114
|
+
* Flags a literal `ctx.fail('<reason>', …)` site that does not put the
|
|
115
|
+
* contract's `recovery` on the wire.
|
|
116
|
+
*
|
|
117
|
+
* An `errors[]` entry must declare `recovery`, but reaching the client with it
|
|
118
|
+
* is opt-in: the throw site forwards `ctx.recoveryFor('<reason>')`, or passes
|
|
119
|
+
* its own `recovery` key. A site that does neither ships `reason` and
|
|
120
|
+
* `retryable` with no hint — and because the framework mirrors
|
|
121
|
+
* `data.recovery.hint` into the error `content[]`, both client surfaces lose it
|
|
122
|
+
* together. The declared guidance is right there in the contract and reaches
|
|
123
|
+
* nobody; an error-path test asserting `code` and `reason` passes either way.
|
|
124
|
+
*
|
|
125
|
+
* **Per site, not per reason.** A handler wiring one of six throws is covered
|
|
126
|
+
* at one of them, so each site is judged on its own argument list. Two sites
|
|
127
|
+
* naming one reason, one forwarding and one bare, produce exactly one
|
|
128
|
+
* diagnostic.
|
|
129
|
+
*
|
|
130
|
+
* **Accepted forms.** `{ ...ctx.recoveryFor('<reason>') }` spread into the data
|
|
131
|
+
* object, `ctx.recoveryFor('<reason>')` passed as the data argument, and an
|
|
132
|
+
* explicit `recovery` key carrying a runtime-interpolated hint.
|
|
133
|
+
*
|
|
134
|
+
* **Bails.** A non-literal first argument on either callee skips the whole
|
|
135
|
+
* definition — the reasons in play are unknown. A resolver sitting outside
|
|
136
|
+
* every fail span (a hoisted `const hint = ctx.recoveryFor('x')`) skips that
|
|
137
|
+
* reason, since the binding is assembled where the scan cannot follow it. A
|
|
138
|
+
* data argument the scan cannot read skips that one site.
|
|
139
|
+
*
|
|
140
|
+
* **Warning, never error.** A failure thrown below the handler is invisible to
|
|
141
|
+
* a `handler.toString()` scan, so the rule speaks only for the sites it sees.
|
|
142
|
+
*/
|
|
143
|
+
export declare function lintErrorContractRecoveryUnforwarded(def: {
|
|
144
|
+
handler?: unknown;
|
|
145
|
+
errors?: unknown;
|
|
146
|
+
}, definitionType: LintDefinitionType, definitionName: string): LintDiagnostic[];
|
|
111
147
|
//# sourceMappingURL=error-contract-rules.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-contract-rules.d.ts","sourceRoot":"","sources":["../../../src/linter/rules/error-contract-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAuBtE;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,OAAO,EACf,cAAc,EAAE,kBAAkB,EAClC,cAAc,EAAE,MAAM,GACrB,cAAc,EAAE,CAkMlB;AAyDD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAC5C,cAAc,EAAE,kBAAkB,EAClC,cAAc,EAAE,MAAM,GACrB,cAAc,EAAE,CAiGlB;AAMD,8EAA8E;AAC9E,MAAM,WAAW,cAAc;IAC7B,sDAAsD;IACtD,GAAG,EAAE,MAAM,CAAC;IACZ,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;CACf;AAED,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,aAAa,EAAE,OAAO,CAAC;IACvB,sDAAsD;IACtD,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,CA0B9E;
|
|
1
|
+
{"version":3,"file":"error-contract-rules.d.ts","sourceRoot":"","sources":["../../../src/linter/rules/error-contract-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAuBtE;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,OAAO,EACf,cAAc,EAAE,kBAAkB,EAClC,cAAc,EAAE,MAAM,GACrB,cAAc,EAAE,CAkMlB;AAyDD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAC5C,cAAc,EAAE,kBAAkB,EAClC,cAAc,EAAE,MAAM,GACrB,cAAc,EAAE,CAiGlB;AAMD,8EAA8E;AAC9E,MAAM,WAAW,cAAc;IAC7B,sDAAsD;IACtD,GAAG,EAAE,MAAM,CAAC;IACZ,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;CACf;AAED,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,aAAa,EAAE,OAAO,CAAC;IACvB,sDAAsD;IACtD,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,CA0B9E;AA6DD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAC5C,cAAc,EAAE,kBAAkB,EAClC,cAAc,EAAE,MAAM,GACrB,cAAc,EAAE,CA4BlB;AA4DD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,oCAAoC,CAClD,GAAG,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAC5C,cAAc,EAAE,kBAAkB,EAClC,cAAc,EAAE,MAAM,GACrB,cAAc,EAAE,CA4ClB"}
|