@nhtio/adk 1.20260607.0 → 1.20260607.2
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 +29 -0
- package/common-BUIjZ6LV.mjs +119 -0
- package/common-BUIjZ6LV.mjs.map +1 -0
- package/common-DeNipTQI.js +148 -0
- package/common-DeNipTQI.js.map +1 -0
- package/eslint/index.d.ts +106 -0
- package/eslint/rules/artifact_tool_forbids_artifact_constructor.cjs +57 -0
- package/eslint/rules/artifact_tool_forbids_artifact_constructor.cjs.map +1 -0
- package/eslint/rules/artifact_tool_forbids_artifact_constructor.d.ts +21 -0
- package/eslint/rules/artifact_tool_forbids_artifact_constructor.mjs +52 -0
- package/eslint/rules/artifact_tool_forbids_artifact_constructor.mjs.map +1 -0
- package/eslint/rules/common.d.ts +41 -0
- package/eslint/rules/index.d.ts +12 -0
- package/eslint/rules/no_model_in_tool_handler.cjs +64 -0
- package/eslint/rules/no_model_in_tool_handler.cjs.map +1 -0
- package/eslint/rules/no_model_in_tool_handler.d.ts +26 -0
- package/eslint/rules/no_model_in_tool_handler.mjs +59 -0
- package/eslint/rules/no_model_in_tool_handler.mjs.map +1 -0
- package/eslint/rules/require_validator_any_required.cjs +100 -0
- package/eslint/rules/require_validator_any_required.cjs.map +1 -0
- package/eslint/rules/require_validator_any_required.d.ts +31 -0
- package/eslint/rules/require_validator_any_required.mjs +95 -0
- package/eslint/rules/require_validator_any_required.mjs.map +1 -0
- package/eslint/rules/thought_payload_requires_replay_tag.cjs +71 -0
- package/eslint/rules/thought_payload_requires_replay_tag.cjs.map +1 -0
- package/eslint/rules/thought_payload_requires_replay_tag.d.ts +26 -0
- package/eslint/rules/thought_payload_requires_replay_tag.mjs +66 -0
- package/eslint/rules/thought_payload_requires_replay_tag.mjs.map +1 -0
- package/eslint/rules/token_encoding_requires_context_window.cjs +70 -0
- package/eslint/rules/token_encoding_requires_context_window.cjs.map +1 -0
- package/eslint/rules/token_encoding_requires_context_window.d.ts +24 -0
- package/eslint/rules/token_encoding_requires_context_window.mjs +65 -0
- package/eslint/rules/token_encoding_requires_context_window.mjs.map +1 -0
- package/eslint/rules.cjs +12 -0
- package/eslint/rules.mjs +6 -0
- package/eslint.cjs +82 -0
- package/eslint.cjs.map +1 -0
- package/eslint.mjs +74 -0
- package/eslint.mjs.map +1 -0
- package/index.cjs +1 -1
- package/index.mjs +1 -1
- package/mcp/adk-docs-corpus.json +1 -1
- package/package.json +230 -187
- package/skills/adk-assembly/SKILL.md +2 -2
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @nhtio/adk/eslint/rules
|
|
3
|
+
*
|
|
4
|
+
* Barrel of the individual `@nhtio/adk` ESLint rule objects. Most consumers want the assembled
|
|
5
|
+
* plugin from `@nhtio/adk/eslint` instead; import individual rules from here only when composing a
|
|
6
|
+
* bespoke plugin or testing a single rule.
|
|
7
|
+
*/
|
|
8
|
+
export { default as requireValidatorAnyRequired } from "./require_validator_any_required";
|
|
9
|
+
export { default as noModelInToolHandler } from "./no_model_in_tool_handler";
|
|
10
|
+
export { default as thoughtPayloadRequiresReplayTag } from "./thought_payload_requires_replay_tag";
|
|
11
|
+
export { default as tokenEncodingRequiresContextWindow } from "./token_encoding_requires_context_window";
|
|
12
|
+
export { default as artifactToolForbidsArtifactConstructor } from "./artifact_tool_forbids_artifact_constructor";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
require("../../chunk-Ble4zEEl.js");
|
|
6
|
+
const require_common = require("../../common-DeNipTQI.js");
|
|
7
|
+
//#region src/eslint/rules/no_model_in_tool_handler.ts
|
|
8
|
+
/**
|
|
9
|
+
* @module @nhtio/adk/eslint/rules/no_model_in_tool_handler
|
|
10
|
+
*
|
|
11
|
+
* Flags an LLM/model call inside a `Tool` / `ArtifactTool` `handler`.
|
|
12
|
+
*
|
|
13
|
+
* Why: the executor owns the dispatch loop. A tool handler runs as one step the model proposed; it
|
|
14
|
+
* must do work and return a result, not start its own reasoning loop. Calling a model from inside a
|
|
15
|
+
* handler hides a nested, unmanaged dispatch from the harness — no token accounting, no event
|
|
16
|
+
* surfacing, no abort propagation. The one legitimate exception is a tool that is explicitly a
|
|
17
|
+
* sub-agent running its own scoped dispatch; this rule allows a handler that constructs a
|
|
18
|
+
* `new TurnRunner(...)` or calls the lower-level `DispatchRunner.dispatch(...)`.
|
|
19
|
+
*
|
|
20
|
+
* Detects provider SDK constructors (`new OpenAI()`, `new Anthropic()`) and chat/completion calls
|
|
21
|
+
* (a `.create()` whose receiver chain passes through `messages`/`completions`/`responses`, or a
|
|
22
|
+
* `generateContent()` call) within the `handler` function passed to `new Tool({ handler })` /
|
|
23
|
+
* `new ArtifactTool({ handler })`, ignoring inner closures (so a sub-agent's own callbacks don't
|
|
24
|
+
* false-positive).
|
|
25
|
+
*
|
|
26
|
+
* Opt-out:
|
|
27
|
+
* // eslint-disable-next-line adk/no-model-in-tool-handler -- <reason>
|
|
28
|
+
*/
|
|
29
|
+
var literalKey = (prop) => {
|
|
30
|
+
if (prop.computed) return void 0;
|
|
31
|
+
if (prop.key.type === "Identifier") return prop.key.name;
|
|
32
|
+
if (prop.key.type === "Literal" && typeof prop.key.value === "string") return prop.key.value;
|
|
33
|
+
};
|
|
34
|
+
var noModelInToolHandlerRule = require_common.createRule({
|
|
35
|
+
name: "no-model-in-tool-handler",
|
|
36
|
+
meta: {
|
|
37
|
+
type: "problem",
|
|
38
|
+
docs: { description: "A Tool/ArtifactTool handler must not invoke a model — the executor owns the dispatch loop. The only exception is a sub-agent tool that runs its own scoped dispatch via `new TurnRunner(...)` or `DispatchRunner.dispatch(...)`." },
|
|
39
|
+
schema: [],
|
|
40
|
+
messages: { noModelInHandler: "Do not call a model inside a tool handler — the executor owns the dispatch loop, and a model call here hides an unmanaged nested dispatch (no token accounting, events, or abort). If this tool is a sub-agent, run a scoped `new TurnRunner(...)` or `DispatchRunner.dispatch(...)` instead. Opt out with an eslint-disable-next-line adk/no-model-in-tool-handler comment + reason." }
|
|
41
|
+
},
|
|
42
|
+
defaultOptions: [],
|
|
43
|
+
create(context) {
|
|
44
|
+
return { NewExpression(node) {
|
|
45
|
+
if (node.callee.type !== "Identifier" || node.callee.name !== "Tool" && node.callee.name !== "ArtifactTool") return;
|
|
46
|
+
const arg = node.arguments[0];
|
|
47
|
+
if (!arg || arg.type !== "ObjectExpression") return;
|
|
48
|
+
const handlerProp = arg.properties.find((p) => p.type === "Property" && literalKey(p) === "handler");
|
|
49
|
+
if (!handlerProp || !require_common.isFunctionNode(handlerProp.value)) return;
|
|
50
|
+
const handler = handlerProp.value;
|
|
51
|
+
if (require_common.runsSubAgent(handler.body)) return;
|
|
52
|
+
require_common.walkBodySkippingNestedFunctions(handler.body, (n) => {
|
|
53
|
+
if (require_common.isLlmCall(n)) context.report({
|
|
54
|
+
node: n,
|
|
55
|
+
messageId: "noModelInHandler"
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
} };
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
//#endregion
|
|
62
|
+
exports.default = noModelInToolHandlerRule;
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=no_model_in_tool_handler.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no_model_in_tool_handler.cjs","names":[],"sources":["../../../src/eslint/rules/no_model_in_tool_handler.ts"],"sourcesContent":["/**\n * @module @nhtio/adk/eslint/rules/no_model_in_tool_handler\n *\n * Flags an LLM/model call inside a `Tool` / `ArtifactTool` `handler`.\n *\n * Why: the executor owns the dispatch loop. A tool handler runs as one step the model proposed; it\n * must do work and return a result, not start its own reasoning loop. Calling a model from inside a\n * handler hides a nested, unmanaged dispatch from the harness — no token accounting, no event\n * surfacing, no abort propagation. The one legitimate exception is a tool that is explicitly a\n * sub-agent running its own scoped dispatch; this rule allows a handler that constructs a\n * `new TurnRunner(...)` or calls the lower-level `DispatchRunner.dispatch(...)`.\n *\n * Detects provider SDK constructors (`new OpenAI()`, `new Anthropic()`) and chat/completion calls\n * (a `.create()` whose receiver chain passes through `messages`/`completions`/`responses`, or a\n * `generateContent()` call) within the `handler` function passed to `new Tool({ handler })` /\n * `new ArtifactTool({ handler })`, ignoring inner closures (so a sub-agent's own callbacks don't\n * false-positive).\n *\n * Opt-out:\n * // eslint-disable-next-line adk/no-model-in-tool-handler -- <reason>\n */\n\nimport {\n createRule,\n isFunctionNode,\n isLlmCall,\n runsSubAgent,\n walkBodySkippingNestedFunctions,\n} from './common'\n\nimport type { TSESTree } from '@typescript-eslint/utils'\n\nconst literalKey = (prop: TSESTree.Property): string | undefined => {\n if (prop.computed) return undefined\n if (prop.key.type === 'Identifier') return prop.key.name\n if (prop.key.type === 'Literal' && typeof prop.key.value === 'string') return prop.key.value\n return undefined\n}\n\nconst noModelInToolHandlerRule = createRule({\n name: 'no-model-in-tool-handler',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'A Tool/ArtifactTool handler must not invoke a model — the executor owns the dispatch loop. The only exception is a sub-agent tool that runs its own scoped dispatch via `new TurnRunner(...)` or `DispatchRunner.dispatch(...)`.',\n },\n schema: [],\n messages: {\n noModelInHandler:\n 'Do not call a model inside a tool handler — the executor owns the dispatch loop, and a model call here hides an unmanaged nested dispatch (no token accounting, events, or abort). If this tool is a sub-agent, run a scoped `new TurnRunner(...)` or `DispatchRunner.dispatch(...)` instead. Opt out with an eslint-disable-next-line adk/no-model-in-tool-handler comment + reason.',\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n NewExpression(node: TSESTree.NewExpression) {\n if (\n node.callee.type !== 'Identifier' ||\n (node.callee.name !== 'Tool' && node.callee.name !== 'ArtifactTool')\n ) {\n return\n }\n const arg = node.arguments[0]\n if (!arg || arg.type !== 'ObjectExpression') return\n\n const handlerProp = arg.properties.find(\n (p): p is TSESTree.Property => p.type === 'Property' && literalKey(p) === 'handler'\n )\n if (!handlerProp || !isFunctionNode(handlerProp.value)) return\n const handler = handlerProp.value\n\n // A sub-agent tool legitimately runs a scoped dispatch (new TurnRunner / DispatchRunner.dispatch)\n // — that's the documented exception.\n if (runsSubAgent(handler.body)) return\n\n walkBodySkippingNestedFunctions(handler.body, (n) => {\n if (isLlmCall(n)) {\n context.report({ node: n, messageId: 'noModelInHandler' })\n }\n })\n },\n }\n },\n})\n\nexport default noModelInToolHandlerRule\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAM,cAAc,SAAgD;CAClE,IAAI,KAAK,UAAU,OAAO,KAAA;CAC1B,IAAI,KAAK,IAAI,SAAS,cAAc,OAAO,KAAK,IAAI;CACpD,IAAI,KAAK,IAAI,SAAS,aAAa,OAAO,KAAK,IAAI,UAAU,UAAU,OAAO,KAAK,IAAI;AAEzF;AAEA,IAAM,2BAA2B,eAAA,WAAW;CAC1C,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,mOACJ;EACA,QAAQ,CAAC;EACT,UAAU,EACR,kBACE,wXACJ;CACF;CACA,gBAAgB,CAAC;CACjB,OAAO,SAAS;EACd,OAAO,EACL,cAAc,MAA8B;GAC1C,IACE,KAAK,OAAO,SAAS,gBACpB,KAAK,OAAO,SAAS,UAAU,KAAK,OAAO,SAAS,gBAErD;GAEF,MAAM,MAAM,KAAK,UAAU;GAC3B,IAAI,CAAC,OAAO,IAAI,SAAS,oBAAoB;GAE7C,MAAM,cAAc,IAAI,WAAW,MAChC,MAA8B,EAAE,SAAS,cAAc,WAAW,CAAC,MAAM,SAC5E;GACA,IAAI,CAAC,eAAe,CAAC,eAAA,eAAe,YAAY,KAAK,GAAG;GACxD,MAAM,UAAU,YAAY;GAI5B,IAAI,eAAA,aAAa,QAAQ,IAAI,GAAG;GAEhC,eAAA,gCAAgC,QAAQ,OAAO,MAAM;IACnD,IAAI,eAAA,UAAU,CAAC,GACb,QAAQ,OAAO;KAAE,MAAM;KAAG,WAAW;IAAmB,CAAC;GAE7D,CAAC;EACH,EACF;CACF;AACF,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @nhtio/adk/eslint/rules/no_model_in_tool_handler
|
|
3
|
+
*
|
|
4
|
+
* Flags an LLM/model call inside a `Tool` / `ArtifactTool` `handler`.
|
|
5
|
+
*
|
|
6
|
+
* Why: the executor owns the dispatch loop. A tool handler runs as one step the model proposed; it
|
|
7
|
+
* must do work and return a result, not start its own reasoning loop. Calling a model from inside a
|
|
8
|
+
* handler hides a nested, unmanaged dispatch from the harness — no token accounting, no event
|
|
9
|
+
* surfacing, no abort propagation. The one legitimate exception is a tool that is explicitly a
|
|
10
|
+
* sub-agent running its own scoped dispatch; this rule allows a handler that constructs a
|
|
11
|
+
* `new TurnRunner(...)` or calls the lower-level `DispatchRunner.dispatch(...)`.
|
|
12
|
+
*
|
|
13
|
+
* Detects provider SDK constructors (`new OpenAI()`, `new Anthropic()`) and chat/completion calls
|
|
14
|
+
* (a `.create()` whose receiver chain passes through `messages`/`completions`/`responses`, or a
|
|
15
|
+
* `generateContent()` call) within the `handler` function passed to `new Tool({ handler })` /
|
|
16
|
+
* `new ArtifactTool({ handler })`, ignoring inner closures (so a sub-agent's own callbacks don't
|
|
17
|
+
* false-positive).
|
|
18
|
+
*
|
|
19
|
+
* Opt-out:
|
|
20
|
+
* // eslint-disable-next-line adk/no-model-in-tool-handler -- <reason>
|
|
21
|
+
*/
|
|
22
|
+
declare const noModelInToolHandlerRule: import("@typescript-eslint/utils/ts-eslint").RuleModule<"noModelInHandler", [
|
|
23
|
+
], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
24
|
+
name: string;
|
|
25
|
+
};
|
|
26
|
+
export default noModelInToolHandlerRule;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { a as walkBodySkippingNestedFunctions, i as runsSubAgent, n as isFunctionNode, r as isLlmCall, t as createRule } from "../../common-BUIjZ6LV.mjs";
|
|
2
|
+
//#region src/eslint/rules/no_model_in_tool_handler.ts
|
|
3
|
+
/**
|
|
4
|
+
* @module @nhtio/adk/eslint/rules/no_model_in_tool_handler
|
|
5
|
+
*
|
|
6
|
+
* Flags an LLM/model call inside a `Tool` / `ArtifactTool` `handler`.
|
|
7
|
+
*
|
|
8
|
+
* Why: the executor owns the dispatch loop. A tool handler runs as one step the model proposed; it
|
|
9
|
+
* must do work and return a result, not start its own reasoning loop. Calling a model from inside a
|
|
10
|
+
* handler hides a nested, unmanaged dispatch from the harness — no token accounting, no event
|
|
11
|
+
* surfacing, no abort propagation. The one legitimate exception is a tool that is explicitly a
|
|
12
|
+
* sub-agent running its own scoped dispatch; this rule allows a handler that constructs a
|
|
13
|
+
* `new TurnRunner(...)` or calls the lower-level `DispatchRunner.dispatch(...)`.
|
|
14
|
+
*
|
|
15
|
+
* Detects provider SDK constructors (`new OpenAI()`, `new Anthropic()`) and chat/completion calls
|
|
16
|
+
* (a `.create()` whose receiver chain passes through `messages`/`completions`/`responses`, or a
|
|
17
|
+
* `generateContent()` call) within the `handler` function passed to `new Tool({ handler })` /
|
|
18
|
+
* `new ArtifactTool({ handler })`, ignoring inner closures (so a sub-agent's own callbacks don't
|
|
19
|
+
* false-positive).
|
|
20
|
+
*
|
|
21
|
+
* Opt-out:
|
|
22
|
+
* // eslint-disable-next-line adk/no-model-in-tool-handler -- <reason>
|
|
23
|
+
*/
|
|
24
|
+
var literalKey = (prop) => {
|
|
25
|
+
if (prop.computed) return void 0;
|
|
26
|
+
if (prop.key.type === "Identifier") return prop.key.name;
|
|
27
|
+
if (prop.key.type === "Literal" && typeof prop.key.value === "string") return prop.key.value;
|
|
28
|
+
};
|
|
29
|
+
var noModelInToolHandlerRule = createRule({
|
|
30
|
+
name: "no-model-in-tool-handler",
|
|
31
|
+
meta: {
|
|
32
|
+
type: "problem",
|
|
33
|
+
docs: { description: "A Tool/ArtifactTool handler must not invoke a model — the executor owns the dispatch loop. The only exception is a sub-agent tool that runs its own scoped dispatch via `new TurnRunner(...)` or `DispatchRunner.dispatch(...)`." },
|
|
34
|
+
schema: [],
|
|
35
|
+
messages: { noModelInHandler: "Do not call a model inside a tool handler — the executor owns the dispatch loop, and a model call here hides an unmanaged nested dispatch (no token accounting, events, or abort). If this tool is a sub-agent, run a scoped `new TurnRunner(...)` or `DispatchRunner.dispatch(...)` instead. Opt out with an eslint-disable-next-line adk/no-model-in-tool-handler comment + reason." }
|
|
36
|
+
},
|
|
37
|
+
defaultOptions: [],
|
|
38
|
+
create(context) {
|
|
39
|
+
return { NewExpression(node) {
|
|
40
|
+
if (node.callee.type !== "Identifier" || node.callee.name !== "Tool" && node.callee.name !== "ArtifactTool") return;
|
|
41
|
+
const arg = node.arguments[0];
|
|
42
|
+
if (!arg || arg.type !== "ObjectExpression") return;
|
|
43
|
+
const handlerProp = arg.properties.find((p) => p.type === "Property" && literalKey(p) === "handler");
|
|
44
|
+
if (!handlerProp || !isFunctionNode(handlerProp.value)) return;
|
|
45
|
+
const handler = handlerProp.value;
|
|
46
|
+
if (runsSubAgent(handler.body)) return;
|
|
47
|
+
walkBodySkippingNestedFunctions(handler.body, (n) => {
|
|
48
|
+
if (isLlmCall(n)) context.report({
|
|
49
|
+
node: n,
|
|
50
|
+
messageId: "noModelInHandler"
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
} };
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
//#endregion
|
|
57
|
+
export { noModelInToolHandlerRule as default };
|
|
58
|
+
|
|
59
|
+
//# sourceMappingURL=no_model_in_tool_handler.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no_model_in_tool_handler.mjs","names":[],"sources":["../../../src/eslint/rules/no_model_in_tool_handler.ts"],"sourcesContent":["/**\n * @module @nhtio/adk/eslint/rules/no_model_in_tool_handler\n *\n * Flags an LLM/model call inside a `Tool` / `ArtifactTool` `handler`.\n *\n * Why: the executor owns the dispatch loop. A tool handler runs as one step the model proposed; it\n * must do work and return a result, not start its own reasoning loop. Calling a model from inside a\n * handler hides a nested, unmanaged dispatch from the harness — no token accounting, no event\n * surfacing, no abort propagation. The one legitimate exception is a tool that is explicitly a\n * sub-agent running its own scoped dispatch; this rule allows a handler that constructs a\n * `new TurnRunner(...)` or calls the lower-level `DispatchRunner.dispatch(...)`.\n *\n * Detects provider SDK constructors (`new OpenAI()`, `new Anthropic()`) and chat/completion calls\n * (a `.create()` whose receiver chain passes through `messages`/`completions`/`responses`, or a\n * `generateContent()` call) within the `handler` function passed to `new Tool({ handler })` /\n * `new ArtifactTool({ handler })`, ignoring inner closures (so a sub-agent's own callbacks don't\n * false-positive).\n *\n * Opt-out:\n * // eslint-disable-next-line adk/no-model-in-tool-handler -- <reason>\n */\n\nimport {\n createRule,\n isFunctionNode,\n isLlmCall,\n runsSubAgent,\n walkBodySkippingNestedFunctions,\n} from './common'\n\nimport type { TSESTree } from '@typescript-eslint/utils'\n\nconst literalKey = (prop: TSESTree.Property): string | undefined => {\n if (prop.computed) return undefined\n if (prop.key.type === 'Identifier') return prop.key.name\n if (prop.key.type === 'Literal' && typeof prop.key.value === 'string') return prop.key.value\n return undefined\n}\n\nconst noModelInToolHandlerRule = createRule({\n name: 'no-model-in-tool-handler',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'A Tool/ArtifactTool handler must not invoke a model — the executor owns the dispatch loop. The only exception is a sub-agent tool that runs its own scoped dispatch via `new TurnRunner(...)` or `DispatchRunner.dispatch(...)`.',\n },\n schema: [],\n messages: {\n noModelInHandler:\n 'Do not call a model inside a tool handler — the executor owns the dispatch loop, and a model call here hides an unmanaged nested dispatch (no token accounting, events, or abort). If this tool is a sub-agent, run a scoped `new TurnRunner(...)` or `DispatchRunner.dispatch(...)` instead. Opt out with an eslint-disable-next-line adk/no-model-in-tool-handler comment + reason.',\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n NewExpression(node: TSESTree.NewExpression) {\n if (\n node.callee.type !== 'Identifier' ||\n (node.callee.name !== 'Tool' && node.callee.name !== 'ArtifactTool')\n ) {\n return\n }\n const arg = node.arguments[0]\n if (!arg || arg.type !== 'ObjectExpression') return\n\n const handlerProp = arg.properties.find(\n (p): p is TSESTree.Property => p.type === 'Property' && literalKey(p) === 'handler'\n )\n if (!handlerProp || !isFunctionNode(handlerProp.value)) return\n const handler = handlerProp.value\n\n // A sub-agent tool legitimately runs a scoped dispatch (new TurnRunner / DispatchRunner.dispatch)\n // — that's the documented exception.\n if (runsSubAgent(handler.body)) return\n\n walkBodySkippingNestedFunctions(handler.body, (n) => {\n if (isLlmCall(n)) {\n context.report({ node: n, messageId: 'noModelInHandler' })\n }\n })\n },\n }\n },\n})\n\nexport default noModelInToolHandlerRule\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAM,cAAc,SAAgD;CAClE,IAAI,KAAK,UAAU,OAAO,KAAA;CAC1B,IAAI,KAAK,IAAI,SAAS,cAAc,OAAO,KAAK,IAAI;CACpD,IAAI,KAAK,IAAI,SAAS,aAAa,OAAO,KAAK,IAAI,UAAU,UAAU,OAAO,KAAK,IAAI;AAEzF;AAEA,IAAM,2BAA2B,WAAW;CAC1C,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,mOACJ;EACA,QAAQ,CAAC;EACT,UAAU,EACR,kBACE,wXACJ;CACF;CACA,gBAAgB,CAAC;CACjB,OAAO,SAAS;EACd,OAAO,EACL,cAAc,MAA8B;GAC1C,IACE,KAAK,OAAO,SAAS,gBACpB,KAAK,OAAO,SAAS,UAAU,KAAK,OAAO,SAAS,gBAErD;GAEF,MAAM,MAAM,KAAK,UAAU;GAC3B,IAAI,CAAC,OAAO,IAAI,SAAS,oBAAoB;GAE7C,MAAM,cAAc,IAAI,WAAW,MAChC,MAA8B,EAAE,SAAS,cAAc,WAAW,CAAC,MAAM,SAC5E;GACA,IAAI,CAAC,eAAe,CAAC,eAAe,YAAY,KAAK,GAAG;GACxD,MAAM,UAAU,YAAY;GAI5B,IAAI,aAAa,QAAQ,IAAI,GAAG;GAEhC,gCAAgC,QAAQ,OAAO,MAAM;IACnD,IAAI,UAAU,CAAC,GACb,QAAQ,OAAO;KAAE,MAAM;KAAG,WAAW;IAAmB,CAAC;GAE7D,CAAC;EACH,EACF;CACF;AACF,CAAC"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
require("../../chunk-Ble4zEEl.js");
|
|
6
|
+
const require_common = require("../../common-DeNipTQI.js");
|
|
7
|
+
//#region src/eslint/rules/require_validator_any_required.ts
|
|
8
|
+
/**
|
|
9
|
+
* @module @nhtio/adk/eslint/rules/require_validator_any_required
|
|
10
|
+
*
|
|
11
|
+
* Flags every `validator.any()` schema chain that does not declare intent with `.required()`
|
|
12
|
+
* or `.optional()`.
|
|
13
|
+
*
|
|
14
|
+
* Why: in `@nhtio/validation`, `.any()` ADMITS `null`/`undefined` unless you make it `.required()`.
|
|
15
|
+
* That default is silent and easy to miss — a schema you believe rejects missing values quietly
|
|
16
|
+
* accepts them, and any `.custom()` refinement is skipped for an absent value (a `.custom()` guard
|
|
17
|
+
* is the usual way this surfaces, e.g. `implementsX(undefined) === true`). The fix is to make the
|
|
18
|
+
* disposition an EXPLICIT declaration with no ambiguity — every `.any()` must say which it is:
|
|
19
|
+
* - `.required()` → reject null/undefined
|
|
20
|
+
* - `.optional()` → deliberately allow null/undefined
|
|
21
|
+
* - `.default(x)` → allow absence, substituting a fallback value
|
|
22
|
+
* - `.forbidden()` → the value must be absent
|
|
23
|
+
* This applies whether the `.any()` is top-level or nested inside `items(...)` / `alternatives(...)`:
|
|
24
|
+
* the enclosing schema being `.required()` does NOT govern an inner `.any()`'s null/undefined handling.
|
|
25
|
+
*
|
|
26
|
+
* The sharpest illustration is `.valid(null)`: an author writing `validator.any().valid(null)`
|
|
27
|
+
* means "must be exactly null" — but because `.any()` admits `undefined`, that schema actually
|
|
28
|
+
* accepts BOTH `null` and `undefined` (and `undefined !== null`). The fix is
|
|
29
|
+
* `validator.any().required().valid(null)`.
|
|
30
|
+
*
|
|
31
|
+
* Opt-out (e.g. a bare `.any()` used purely as a type argument like `items(validator.any())`):
|
|
32
|
+
* // eslint-disable-next-line adk/require-validator-any-required -- <reason>
|
|
33
|
+
*/
|
|
34
|
+
var baseIdentifierName = (node) => {
|
|
35
|
+
let cur = node;
|
|
36
|
+
while (cur) {
|
|
37
|
+
if (cur.type === "Identifier") return cur.name;
|
|
38
|
+
if (cur.type === "MemberExpression") {
|
|
39
|
+
cur = cur.object;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (cur.type === "CallExpression") {
|
|
43
|
+
cur = cur.callee;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var isAnyCall = (node) => node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && node.callee.property.name === "any" && baseIdentifierName(node.callee.object) === "validator";
|
|
50
|
+
var collectChainCalls = (anyCall) => {
|
|
51
|
+
const calls = [];
|
|
52
|
+
let cur = anyCall;
|
|
53
|
+
while (cur && cur.type === "CallExpression") {
|
|
54
|
+
calls.push(cur);
|
|
55
|
+
cur = cur.callee.type === "MemberExpression" ? cur.callee.object : null;
|
|
56
|
+
}
|
|
57
|
+
cur = anyCall;
|
|
58
|
+
while (cur) {
|
|
59
|
+
const p = cur.parent;
|
|
60
|
+
if (!p) break;
|
|
61
|
+
if (p.type === "MemberExpression" && p.object === cur) {
|
|
62
|
+
cur = p;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (p.type === "CallExpression" && p.callee === cur) {
|
|
66
|
+
calls.push(p);
|
|
67
|
+
cur = p;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
return calls;
|
|
73
|
+
};
|
|
74
|
+
var chainCalls = (calls, name) => calls.some((c) => c.callee.type === "MemberExpression" && c.callee.property.type === "Identifier" && c.callee.property.name === name);
|
|
75
|
+
var requireValidatorAnyRequiredRule = require_common.createRule({
|
|
76
|
+
name: "require-validator-any-required",
|
|
77
|
+
meta: {
|
|
78
|
+
type: "problem",
|
|
79
|
+
docs: { description: "`validator.any()` admits null/undefined unless `.required()`; make the disposition explicit by ending every `.any()` in `.required()`, `.optional()`, or `.default(…)`. Opt out with a tactical disable comment + reason." },
|
|
80
|
+
schema: [],
|
|
81
|
+
messages: { declareIntent: "`validator.any()` admits null/undefined unless made `.required()`. Make the disposition explicit: end this `.any()` in `.required()` (reject null/undefined), `.optional()` (deliberately allow it), or `.default(…)` (allow it with a fallback) — applies even when nested in items()/alternatives(). Or add an eslint-disable-next-line adk/require-validator-any-required comment with a reason." }
|
|
82
|
+
},
|
|
83
|
+
defaultOptions: [],
|
|
84
|
+
create(context) {
|
|
85
|
+
return { CallExpression(node) {
|
|
86
|
+
if (!isAnyCall(node)) return;
|
|
87
|
+
const calls = collectChainCalls(node);
|
|
88
|
+
if (chainCalls(calls, "required") || chainCalls(calls, "optional") || chainCalls(calls, "default") || chainCalls(calls, "forbidden")) return;
|
|
89
|
+
const reportNode = node.callee.type === "MemberExpression" ? node.callee.property : node;
|
|
90
|
+
context.report({
|
|
91
|
+
node: reportNode,
|
|
92
|
+
messageId: "declareIntent"
|
|
93
|
+
});
|
|
94
|
+
} };
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
//#endregion
|
|
98
|
+
exports.default = requireValidatorAnyRequiredRule;
|
|
99
|
+
|
|
100
|
+
//# sourceMappingURL=require_validator_any_required.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require_validator_any_required.cjs","names":[],"sources":["../../../src/eslint/rules/require_validator_any_required.ts"],"sourcesContent":["/**\n * @module @nhtio/adk/eslint/rules/require_validator_any_required\n *\n * Flags every `validator.any()` schema chain that does not declare intent with `.required()`\n * or `.optional()`.\n *\n * Why: in `@nhtio/validation`, `.any()` ADMITS `null`/`undefined` unless you make it `.required()`.\n * That default is silent and easy to miss — a schema you believe rejects missing values quietly\n * accepts them, and any `.custom()` refinement is skipped for an absent value (a `.custom()` guard\n * is the usual way this surfaces, e.g. `implementsX(undefined) === true`). The fix is to make the\n * disposition an EXPLICIT declaration with no ambiguity — every `.any()` must say which it is:\n * - `.required()` → reject null/undefined\n * - `.optional()` → deliberately allow null/undefined\n * - `.default(x)` → allow absence, substituting a fallback value\n * - `.forbidden()` → the value must be absent\n * This applies whether the `.any()` is top-level or nested inside `items(...)` / `alternatives(...)`:\n * the enclosing schema being `.required()` does NOT govern an inner `.any()`'s null/undefined handling.\n *\n * The sharpest illustration is `.valid(null)`: an author writing `validator.any().valid(null)`\n * means \"must be exactly null\" — but because `.any()` admits `undefined`, that schema actually\n * accepts BOTH `null` and `undefined` (and `undefined !== null`). The fix is\n * `validator.any().required().valid(null)`.\n *\n * Opt-out (e.g. a bare `.any()` used purely as a type argument like `items(validator.any())`):\n * // eslint-disable-next-line adk/require-validator-any-required -- <reason>\n */\n\nimport { createRule } from './common'\n\nimport type { TSESTree } from '@typescript-eslint/utils'\n\n// The base identifier a member/call chain roots at, e.g. `validator` in `validator.any()` or\n// `validator.alternatives(...).any()`. Returns undefined if the chain doesn't root at a plain name.\nconst baseIdentifierName = (node: TSESTree.Node | undefined): string | undefined => {\n let cur: TSESTree.Node | undefined = node\n while (cur) {\n if (cur.type === 'Identifier') return cur.name\n if (cur.type === 'MemberExpression') {\n cur = cur.object\n continue\n }\n if (cur.type === 'CallExpression') {\n cur = cur.callee\n continue\n }\n return undefined\n }\n return undefined\n}\n\n// `validator.any()` (or a `validator.…().any()` chain) — NOT `expect.any()`, `_.any()`, etc.\nconst isAnyCall = (node: TSESTree.CallExpression): boolean =>\n node.callee.type === 'MemberExpression' &&\n node.callee.property.type === 'Identifier' &&\n node.callee.property.name === 'any' &&\n baseIdentifierName(node.callee.object) === 'validator'\n\n// Collect every CallExpression in the method chain `anyCall` belongs to — inward through the\n// callee object (`a.b().c()` -> `a.b()`) and outward through `.parent` links\n// (`x.any()` -> `x.any().required` -> `x.any().required()`).\nconst collectChainCalls = (anyCall: TSESTree.CallExpression): TSESTree.CallExpression[] => {\n const calls: TSESTree.CallExpression[] = []\n\n let cur: TSESTree.Node | null = anyCall\n while (cur && cur.type === 'CallExpression') {\n calls.push(cur)\n cur = cur.callee.type === 'MemberExpression' ? cur.callee.object : null\n }\n\n cur = anyCall\n while (cur) {\n const p: TSESTree.Node | undefined = cur.parent\n if (!p) break\n if (p.type === 'MemberExpression' && p.object === cur) {\n cur = p\n continue\n }\n if (p.type === 'CallExpression' && p.callee === cur) {\n calls.push(p)\n cur = p\n continue\n }\n break\n }\n\n return calls\n}\n\nconst chainCalls = (calls: TSESTree.CallExpression[], name: string): boolean =>\n calls.some(\n (c) =>\n c.callee.type === 'MemberExpression' &&\n c.callee.property.type === 'Identifier' &&\n c.callee.property.name === name\n )\n\nconst requireValidatorAnyRequiredRule = createRule({\n name: 'require-validator-any-required',\n meta: {\n type: 'problem',\n docs: {\n description:\n '`validator.any()` admits null/undefined unless `.required()`; make the disposition explicit by ending every `.any()` in `.required()`, `.optional()`, or `.default(…)`. Opt out with a tactical disable comment + reason.',\n },\n schema: [],\n messages: {\n declareIntent:\n '`validator.any()` admits null/undefined unless made `.required()`. Make the disposition explicit: end this `.any()` in `.required()` (reject null/undefined), `.optional()` (deliberately allow it), or `.default(…)` (allow it with a fallback) — applies even when nested in items()/alternatives(). Or add an eslint-disable-next-line adk/require-validator-any-required comment with a reason.',\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n CallExpression(node: TSESTree.CallExpression) {\n if (!isAnyCall(node)) return\n const calls = collectChainCalls(node)\n // An unambiguous disposition declaration clears the rule:\n // .required() — reject null/undefined\n // .optional() — deliberately allow null/undefined\n // .default(x) — allow absence, substituting a fallback\n // .forbidden() — the value must be absent\n if (\n chainCalls(calls, 'required') ||\n chainCalls(calls, 'optional') ||\n chainCalls(calls, 'default') ||\n chainCalls(calls, 'forbidden')\n ) {\n return\n }\n const reportNode = node.callee.type === 'MemberExpression' ? node.callee.property : node\n context.report({ node: reportNode, messageId: 'declareIntent' })\n },\n }\n },\n})\n\nexport default requireValidatorAnyRequiredRule\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,IAAM,sBAAsB,SAAwD;CAClF,IAAI,MAAiC;CACrC,OAAO,KAAK;EACV,IAAI,IAAI,SAAS,cAAc,OAAO,IAAI;EAC1C,IAAI,IAAI,SAAS,oBAAoB;GACnC,MAAM,IAAI;GACV;EACF;EACA,IAAI,IAAI,SAAS,kBAAkB;GACjC,MAAM,IAAI;GACV;EACF;EACA;CACF;AAEF;AAGA,IAAM,aAAa,SACjB,KAAK,OAAO,SAAS,sBACrB,KAAK,OAAO,SAAS,SAAS,gBAC9B,KAAK,OAAO,SAAS,SAAS,SAC9B,mBAAmB,KAAK,OAAO,MAAM,MAAM;AAK7C,IAAM,qBAAqB,YAAgE;CACzF,MAAM,QAAmC,CAAC;CAE1C,IAAI,MAA4B;CAChC,OAAO,OAAO,IAAI,SAAS,kBAAkB;EAC3C,MAAM,KAAK,GAAG;EACd,MAAM,IAAI,OAAO,SAAS,qBAAqB,IAAI,OAAO,SAAS;CACrE;CAEA,MAAM;CACN,OAAO,KAAK;EACV,MAAM,IAA+B,IAAI;EACzC,IAAI,CAAC,GAAG;EACR,IAAI,EAAE,SAAS,sBAAsB,EAAE,WAAW,KAAK;GACrD,MAAM;GACN;EACF;EACA,IAAI,EAAE,SAAS,oBAAoB,EAAE,WAAW,KAAK;GACnD,MAAM,KAAK,CAAC;GACZ,MAAM;GACN;EACF;EACA;CACF;CAEA,OAAO;AACT;AAEA,IAAM,cAAc,OAAkC,SACpD,MAAM,MACH,MACC,EAAE,OAAO,SAAS,sBAClB,EAAE,OAAO,SAAS,SAAS,gBAC3B,EAAE,OAAO,SAAS,SAAS,IAC/B;AAEF,IAAM,kCAAkC,eAAA,WAAW;CACjD,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,4NACJ;EACA,QAAQ,CAAC;EACT,UAAU,EACR,eACE,sYACJ;CACF;CACA,gBAAgB,CAAC;CACjB,OAAO,SAAS;EACd,OAAO,EACL,eAAe,MAA+B;GAC5C,IAAI,CAAC,UAAU,IAAI,GAAG;GACtB,MAAM,QAAQ,kBAAkB,IAAI;GAMpC,IACE,WAAW,OAAO,UAAU,KAC5B,WAAW,OAAO,UAAU,KAC5B,WAAW,OAAO,SAAS,KAC3B,WAAW,OAAO,WAAW,GAE7B;GAEF,MAAM,aAAa,KAAK,OAAO,SAAS,qBAAqB,KAAK,OAAO,WAAW;GACpF,QAAQ,OAAO;IAAE,MAAM;IAAY,WAAW;GAAgB,CAAC;EACjE,EACF;CACF;AACF,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @nhtio/adk/eslint/rules/require_validator_any_required
|
|
3
|
+
*
|
|
4
|
+
* Flags every `validator.any()` schema chain that does not declare intent with `.required()`
|
|
5
|
+
* or `.optional()`.
|
|
6
|
+
*
|
|
7
|
+
* Why: in `@nhtio/validation`, `.any()` ADMITS `null`/`undefined` unless you make it `.required()`.
|
|
8
|
+
* That default is silent and easy to miss — a schema you believe rejects missing values quietly
|
|
9
|
+
* accepts them, and any `.custom()` refinement is skipped for an absent value (a `.custom()` guard
|
|
10
|
+
* is the usual way this surfaces, e.g. `implementsX(undefined) === true`). The fix is to make the
|
|
11
|
+
* disposition an EXPLICIT declaration with no ambiguity — every `.any()` must say which it is:
|
|
12
|
+
* - `.required()` → reject null/undefined
|
|
13
|
+
* - `.optional()` → deliberately allow null/undefined
|
|
14
|
+
* - `.default(x)` → allow absence, substituting a fallback value
|
|
15
|
+
* - `.forbidden()` → the value must be absent
|
|
16
|
+
* This applies whether the `.any()` is top-level or nested inside `items(...)` / `alternatives(...)`:
|
|
17
|
+
* the enclosing schema being `.required()` does NOT govern an inner `.any()`'s null/undefined handling.
|
|
18
|
+
*
|
|
19
|
+
* The sharpest illustration is `.valid(null)`: an author writing `validator.any().valid(null)`
|
|
20
|
+
* means "must be exactly null" — but because `.any()` admits `undefined`, that schema actually
|
|
21
|
+
* accepts BOTH `null` and `undefined` (and `undefined !== null`). The fix is
|
|
22
|
+
* `validator.any().required().valid(null)`.
|
|
23
|
+
*
|
|
24
|
+
* Opt-out (e.g. a bare `.any()` used purely as a type argument like `items(validator.any())`):
|
|
25
|
+
* // eslint-disable-next-line adk/require-validator-any-required -- <reason>
|
|
26
|
+
*/
|
|
27
|
+
declare const requireValidatorAnyRequiredRule: import("@typescript-eslint/utils/ts-eslint").RuleModule<"declareIntent", [
|
|
28
|
+
], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
29
|
+
name: string;
|
|
30
|
+
};
|
|
31
|
+
export default requireValidatorAnyRequiredRule;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { t as createRule } from "../../common-BUIjZ6LV.mjs";
|
|
2
|
+
//#region src/eslint/rules/require_validator_any_required.ts
|
|
3
|
+
/**
|
|
4
|
+
* @module @nhtio/adk/eslint/rules/require_validator_any_required
|
|
5
|
+
*
|
|
6
|
+
* Flags every `validator.any()` schema chain that does not declare intent with `.required()`
|
|
7
|
+
* or `.optional()`.
|
|
8
|
+
*
|
|
9
|
+
* Why: in `@nhtio/validation`, `.any()` ADMITS `null`/`undefined` unless you make it `.required()`.
|
|
10
|
+
* That default is silent and easy to miss — a schema you believe rejects missing values quietly
|
|
11
|
+
* accepts them, and any `.custom()` refinement is skipped for an absent value (a `.custom()` guard
|
|
12
|
+
* is the usual way this surfaces, e.g. `implementsX(undefined) === true`). The fix is to make the
|
|
13
|
+
* disposition an EXPLICIT declaration with no ambiguity — every `.any()` must say which it is:
|
|
14
|
+
* - `.required()` → reject null/undefined
|
|
15
|
+
* - `.optional()` → deliberately allow null/undefined
|
|
16
|
+
* - `.default(x)` → allow absence, substituting a fallback value
|
|
17
|
+
* - `.forbidden()` → the value must be absent
|
|
18
|
+
* This applies whether the `.any()` is top-level or nested inside `items(...)` / `alternatives(...)`:
|
|
19
|
+
* the enclosing schema being `.required()` does NOT govern an inner `.any()`'s null/undefined handling.
|
|
20
|
+
*
|
|
21
|
+
* The sharpest illustration is `.valid(null)`: an author writing `validator.any().valid(null)`
|
|
22
|
+
* means "must be exactly null" — but because `.any()` admits `undefined`, that schema actually
|
|
23
|
+
* accepts BOTH `null` and `undefined` (and `undefined !== null`). The fix is
|
|
24
|
+
* `validator.any().required().valid(null)`.
|
|
25
|
+
*
|
|
26
|
+
* Opt-out (e.g. a bare `.any()` used purely as a type argument like `items(validator.any())`):
|
|
27
|
+
* // eslint-disable-next-line adk/require-validator-any-required -- <reason>
|
|
28
|
+
*/
|
|
29
|
+
var baseIdentifierName = (node) => {
|
|
30
|
+
let cur = node;
|
|
31
|
+
while (cur) {
|
|
32
|
+
if (cur.type === "Identifier") return cur.name;
|
|
33
|
+
if (cur.type === "MemberExpression") {
|
|
34
|
+
cur = cur.object;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (cur.type === "CallExpression") {
|
|
38
|
+
cur = cur.callee;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var isAnyCall = (node) => node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && node.callee.property.name === "any" && baseIdentifierName(node.callee.object) === "validator";
|
|
45
|
+
var collectChainCalls = (anyCall) => {
|
|
46
|
+
const calls = [];
|
|
47
|
+
let cur = anyCall;
|
|
48
|
+
while (cur && cur.type === "CallExpression") {
|
|
49
|
+
calls.push(cur);
|
|
50
|
+
cur = cur.callee.type === "MemberExpression" ? cur.callee.object : null;
|
|
51
|
+
}
|
|
52
|
+
cur = anyCall;
|
|
53
|
+
while (cur) {
|
|
54
|
+
const p = cur.parent;
|
|
55
|
+
if (!p) break;
|
|
56
|
+
if (p.type === "MemberExpression" && p.object === cur) {
|
|
57
|
+
cur = p;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (p.type === "CallExpression" && p.callee === cur) {
|
|
61
|
+
calls.push(p);
|
|
62
|
+
cur = p;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
return calls;
|
|
68
|
+
};
|
|
69
|
+
var chainCalls = (calls, name) => calls.some((c) => c.callee.type === "MemberExpression" && c.callee.property.type === "Identifier" && c.callee.property.name === name);
|
|
70
|
+
var requireValidatorAnyRequiredRule = createRule({
|
|
71
|
+
name: "require-validator-any-required",
|
|
72
|
+
meta: {
|
|
73
|
+
type: "problem",
|
|
74
|
+
docs: { description: "`validator.any()` admits null/undefined unless `.required()`; make the disposition explicit by ending every `.any()` in `.required()`, `.optional()`, or `.default(…)`. Opt out with a tactical disable comment + reason." },
|
|
75
|
+
schema: [],
|
|
76
|
+
messages: { declareIntent: "`validator.any()` admits null/undefined unless made `.required()`. Make the disposition explicit: end this `.any()` in `.required()` (reject null/undefined), `.optional()` (deliberately allow it), or `.default(…)` (allow it with a fallback) — applies even when nested in items()/alternatives(). Or add an eslint-disable-next-line adk/require-validator-any-required comment with a reason." }
|
|
77
|
+
},
|
|
78
|
+
defaultOptions: [],
|
|
79
|
+
create(context) {
|
|
80
|
+
return { CallExpression(node) {
|
|
81
|
+
if (!isAnyCall(node)) return;
|
|
82
|
+
const calls = collectChainCalls(node);
|
|
83
|
+
if (chainCalls(calls, "required") || chainCalls(calls, "optional") || chainCalls(calls, "default") || chainCalls(calls, "forbidden")) return;
|
|
84
|
+
const reportNode = node.callee.type === "MemberExpression" ? node.callee.property : node;
|
|
85
|
+
context.report({
|
|
86
|
+
node: reportNode,
|
|
87
|
+
messageId: "declareIntent"
|
|
88
|
+
});
|
|
89
|
+
} };
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
//#endregion
|
|
93
|
+
export { requireValidatorAnyRequiredRule as default };
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=require_validator_any_required.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require_validator_any_required.mjs","names":[],"sources":["../../../src/eslint/rules/require_validator_any_required.ts"],"sourcesContent":["/**\n * @module @nhtio/adk/eslint/rules/require_validator_any_required\n *\n * Flags every `validator.any()` schema chain that does not declare intent with `.required()`\n * or `.optional()`.\n *\n * Why: in `@nhtio/validation`, `.any()` ADMITS `null`/`undefined` unless you make it `.required()`.\n * That default is silent and easy to miss — a schema you believe rejects missing values quietly\n * accepts them, and any `.custom()` refinement is skipped for an absent value (a `.custom()` guard\n * is the usual way this surfaces, e.g. `implementsX(undefined) === true`). The fix is to make the\n * disposition an EXPLICIT declaration with no ambiguity — every `.any()` must say which it is:\n * - `.required()` → reject null/undefined\n * - `.optional()` → deliberately allow null/undefined\n * - `.default(x)` → allow absence, substituting a fallback value\n * - `.forbidden()` → the value must be absent\n * This applies whether the `.any()` is top-level or nested inside `items(...)` / `alternatives(...)`:\n * the enclosing schema being `.required()` does NOT govern an inner `.any()`'s null/undefined handling.\n *\n * The sharpest illustration is `.valid(null)`: an author writing `validator.any().valid(null)`\n * means \"must be exactly null\" — but because `.any()` admits `undefined`, that schema actually\n * accepts BOTH `null` and `undefined` (and `undefined !== null`). The fix is\n * `validator.any().required().valid(null)`.\n *\n * Opt-out (e.g. a bare `.any()` used purely as a type argument like `items(validator.any())`):\n * // eslint-disable-next-line adk/require-validator-any-required -- <reason>\n */\n\nimport { createRule } from './common'\n\nimport type { TSESTree } from '@typescript-eslint/utils'\n\n// The base identifier a member/call chain roots at, e.g. `validator` in `validator.any()` or\n// `validator.alternatives(...).any()`. Returns undefined if the chain doesn't root at a plain name.\nconst baseIdentifierName = (node: TSESTree.Node | undefined): string | undefined => {\n let cur: TSESTree.Node | undefined = node\n while (cur) {\n if (cur.type === 'Identifier') return cur.name\n if (cur.type === 'MemberExpression') {\n cur = cur.object\n continue\n }\n if (cur.type === 'CallExpression') {\n cur = cur.callee\n continue\n }\n return undefined\n }\n return undefined\n}\n\n// `validator.any()` (or a `validator.…().any()` chain) — NOT `expect.any()`, `_.any()`, etc.\nconst isAnyCall = (node: TSESTree.CallExpression): boolean =>\n node.callee.type === 'MemberExpression' &&\n node.callee.property.type === 'Identifier' &&\n node.callee.property.name === 'any' &&\n baseIdentifierName(node.callee.object) === 'validator'\n\n// Collect every CallExpression in the method chain `anyCall` belongs to — inward through the\n// callee object (`a.b().c()` -> `a.b()`) and outward through `.parent` links\n// (`x.any()` -> `x.any().required` -> `x.any().required()`).\nconst collectChainCalls = (anyCall: TSESTree.CallExpression): TSESTree.CallExpression[] => {\n const calls: TSESTree.CallExpression[] = []\n\n let cur: TSESTree.Node | null = anyCall\n while (cur && cur.type === 'CallExpression') {\n calls.push(cur)\n cur = cur.callee.type === 'MemberExpression' ? cur.callee.object : null\n }\n\n cur = anyCall\n while (cur) {\n const p: TSESTree.Node | undefined = cur.parent\n if (!p) break\n if (p.type === 'MemberExpression' && p.object === cur) {\n cur = p\n continue\n }\n if (p.type === 'CallExpression' && p.callee === cur) {\n calls.push(p)\n cur = p\n continue\n }\n break\n }\n\n return calls\n}\n\nconst chainCalls = (calls: TSESTree.CallExpression[], name: string): boolean =>\n calls.some(\n (c) =>\n c.callee.type === 'MemberExpression' &&\n c.callee.property.type === 'Identifier' &&\n c.callee.property.name === name\n )\n\nconst requireValidatorAnyRequiredRule = createRule({\n name: 'require-validator-any-required',\n meta: {\n type: 'problem',\n docs: {\n description:\n '`validator.any()` admits null/undefined unless `.required()`; make the disposition explicit by ending every `.any()` in `.required()`, `.optional()`, or `.default(…)`. Opt out with a tactical disable comment + reason.',\n },\n schema: [],\n messages: {\n declareIntent:\n '`validator.any()` admits null/undefined unless made `.required()`. Make the disposition explicit: end this `.any()` in `.required()` (reject null/undefined), `.optional()` (deliberately allow it), or `.default(…)` (allow it with a fallback) — applies even when nested in items()/alternatives(). Or add an eslint-disable-next-line adk/require-validator-any-required comment with a reason.',\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n CallExpression(node: TSESTree.CallExpression) {\n if (!isAnyCall(node)) return\n const calls = collectChainCalls(node)\n // An unambiguous disposition declaration clears the rule:\n // .required() — reject null/undefined\n // .optional() — deliberately allow null/undefined\n // .default(x) — allow absence, substituting a fallback\n // .forbidden() — the value must be absent\n if (\n chainCalls(calls, 'required') ||\n chainCalls(calls, 'optional') ||\n chainCalls(calls, 'default') ||\n chainCalls(calls, 'forbidden')\n ) {\n return\n }\n const reportNode = node.callee.type === 'MemberExpression' ? node.callee.property : node\n context.report({ node: reportNode, messageId: 'declareIntent' })\n },\n }\n },\n})\n\nexport default requireValidatorAnyRequiredRule\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,IAAM,sBAAsB,SAAwD;CAClF,IAAI,MAAiC;CACrC,OAAO,KAAK;EACV,IAAI,IAAI,SAAS,cAAc,OAAO,IAAI;EAC1C,IAAI,IAAI,SAAS,oBAAoB;GACnC,MAAM,IAAI;GACV;EACF;EACA,IAAI,IAAI,SAAS,kBAAkB;GACjC,MAAM,IAAI;GACV;EACF;EACA;CACF;AAEF;AAGA,IAAM,aAAa,SACjB,KAAK,OAAO,SAAS,sBACrB,KAAK,OAAO,SAAS,SAAS,gBAC9B,KAAK,OAAO,SAAS,SAAS,SAC9B,mBAAmB,KAAK,OAAO,MAAM,MAAM;AAK7C,IAAM,qBAAqB,YAAgE;CACzF,MAAM,QAAmC,CAAC;CAE1C,IAAI,MAA4B;CAChC,OAAO,OAAO,IAAI,SAAS,kBAAkB;EAC3C,MAAM,KAAK,GAAG;EACd,MAAM,IAAI,OAAO,SAAS,qBAAqB,IAAI,OAAO,SAAS;CACrE;CAEA,MAAM;CACN,OAAO,KAAK;EACV,MAAM,IAA+B,IAAI;EACzC,IAAI,CAAC,GAAG;EACR,IAAI,EAAE,SAAS,sBAAsB,EAAE,WAAW,KAAK;GACrD,MAAM;GACN;EACF;EACA,IAAI,EAAE,SAAS,oBAAoB,EAAE,WAAW,KAAK;GACnD,MAAM,KAAK,CAAC;GACZ,MAAM;GACN;EACF;EACA;CACF;CAEA,OAAO;AACT;AAEA,IAAM,cAAc,OAAkC,SACpD,MAAM,MACH,MACC,EAAE,OAAO,SAAS,sBAClB,EAAE,OAAO,SAAS,SAAS,gBAC3B,EAAE,OAAO,SAAS,SAAS,IAC/B;AAEF,IAAM,kCAAkC,WAAW;CACjD,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,4NACJ;EACA,QAAQ,CAAC;EACT,UAAU,EACR,eACE,sYACJ;CACF;CACA,gBAAgB,CAAC;CACjB,OAAO,SAAS;EACd,OAAO,EACL,eAAe,MAA+B;GAC5C,IAAI,CAAC,UAAU,IAAI,GAAG;GACtB,MAAM,QAAQ,kBAAkB,IAAI;GAMpC,IACE,WAAW,OAAO,UAAU,KAC5B,WAAW,OAAO,UAAU,KAC5B,WAAW,OAAO,SAAS,KAC3B,WAAW,OAAO,WAAW,GAE7B;GAEF,MAAM,aAAa,KAAK,OAAO,SAAS,qBAAqB,KAAK,OAAO,WAAW;GACpF,QAAQ,OAAO;IAAE,MAAM;IAAY,WAAW;GAAgB,CAAC;EACjE,EACF;CACF;AACF,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
require("../../chunk-Ble4zEEl.js");
|
|
6
|
+
const require_common = require("../../common-DeNipTQI.js");
|
|
7
|
+
//#region src/eslint/rules/thought_payload_requires_replay_tag.ts
|
|
8
|
+
/**
|
|
9
|
+
* @module @nhtio/adk/eslint/rules/thought_payload_requires_replay_tag
|
|
10
|
+
*
|
|
11
|
+
* Flags `new Thought({ … })` whose options literal sets a `payload` but omits a
|
|
12
|
+
* `replayCompatibility` tag.
|
|
13
|
+
*
|
|
14
|
+
* Why: a `Thought` may carry a vendor-opaque `payload` that the harness cannot interpret, to be
|
|
15
|
+
* replayed back to a matching model wire. The ADK can only route that payload if the thought also
|
|
16
|
+
* declares which adapter wire-shape it is replayable into, via `replayCompatibility`. A `payload`
|
|
17
|
+
* with no `replayCompatibility` is a footgun: the harness has no way to know which adapter can
|
|
18
|
+
* consume it, so it is dropped silently. The `Thought` constructor enforces this cross-field
|
|
19
|
+
* invariant at runtime (`payload` present ⇒ `replayCompatibility` required); this rule surfaces it
|
|
20
|
+
* statically at the construction site so it fails lint, not just at runtime.
|
|
21
|
+
*
|
|
22
|
+
* Detects the common literal form: a `new Thought({ … })` options object that has a `payload`
|
|
23
|
+
* property but no `replayCompatibility` property. Spreads or computed keys are not analyzable here —
|
|
24
|
+
* those fall back to the runtime check.
|
|
25
|
+
*
|
|
26
|
+
* Opt-out:
|
|
27
|
+
* // eslint-disable-next-line adk/thought-payload-requires-replay-tag -- <reason>
|
|
28
|
+
*/
|
|
29
|
+
var literalKey = (prop) => {
|
|
30
|
+
if (prop.computed) return void 0;
|
|
31
|
+
if (prop.key.type === "Identifier") return prop.key.name;
|
|
32
|
+
if (prop.key.type === "Literal" && typeof prop.key.value === "string") return prop.key.value;
|
|
33
|
+
};
|
|
34
|
+
var isUndefinedValue = (node) => node.type === "Identifier" && node.name === "undefined";
|
|
35
|
+
var thoughtPayloadRequiresReplayTagRule = require_common.createRule({
|
|
36
|
+
name: "thought-payload-requires-replay-tag",
|
|
37
|
+
meta: {
|
|
38
|
+
type: "problem",
|
|
39
|
+
docs: { description: "A `Thought` carrying a `payload` must declare `replayCompatibility`; otherwise the harness cannot route the opaque payload to a matching adapter and drops it. Enforced at runtime by the Thought constructor; this surfaces it at the construction site." },
|
|
40
|
+
schema: [],
|
|
41
|
+
messages: { requireReplayTag: "`new Thought({ payload })` requires a `replayCompatibility` tag — without it the harness cannot route the opaque payload to an adapter and silently drops it. Add `replayCompatibility: \"<wire-shape-id>\"`, or remove the payload. Opt out with an eslint-disable-next-line adk/thought-payload-requires-replay-tag comment + reason." }
|
|
42
|
+
},
|
|
43
|
+
defaultOptions: [],
|
|
44
|
+
create(context) {
|
|
45
|
+
return { NewExpression(node) {
|
|
46
|
+
if (node.callee.type !== "Identifier" || node.callee.name !== "Thought") return;
|
|
47
|
+
const arg = node.arguments[0];
|
|
48
|
+
if (!arg || arg.type !== "ObjectExpression") return;
|
|
49
|
+
let payloadProp;
|
|
50
|
+
let hasReplayTag = false;
|
|
51
|
+
let hasSpread = false;
|
|
52
|
+
for (const p of arg.properties) {
|
|
53
|
+
if (p.type === "SpreadElement") {
|
|
54
|
+
hasSpread = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const key = literalKey(p);
|
|
58
|
+
if (key === "payload" && !isUndefinedValue(p.value)) payloadProp = p;
|
|
59
|
+
else if (key === "replayCompatibility" && !isUndefinedValue(p.value)) hasReplayTag = true;
|
|
60
|
+
}
|
|
61
|
+
if (payloadProp && !hasReplayTag && !hasSpread) context.report({
|
|
62
|
+
node: payloadProp,
|
|
63
|
+
messageId: "requireReplayTag"
|
|
64
|
+
});
|
|
65
|
+
} };
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
//#endregion
|
|
69
|
+
exports.default = thoughtPayloadRequiresReplayTagRule;
|
|
70
|
+
|
|
71
|
+
//# sourceMappingURL=thought_payload_requires_replay_tag.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thought_payload_requires_replay_tag.cjs","names":[],"sources":["../../../src/eslint/rules/thought_payload_requires_replay_tag.ts"],"sourcesContent":["/**\n * @module @nhtio/adk/eslint/rules/thought_payload_requires_replay_tag\n *\n * Flags `new Thought({ … })` whose options literal sets a `payload` but omits a\n * `replayCompatibility` tag.\n *\n * Why: a `Thought` may carry a vendor-opaque `payload` that the harness cannot interpret, to be\n * replayed back to a matching model wire. The ADK can only route that payload if the thought also\n * declares which adapter wire-shape it is replayable into, via `replayCompatibility`. A `payload`\n * with no `replayCompatibility` is a footgun: the harness has no way to know which adapter can\n * consume it, so it is dropped silently. The `Thought` constructor enforces this cross-field\n * invariant at runtime (`payload` present ⇒ `replayCompatibility` required); this rule surfaces it\n * statically at the construction site so it fails lint, not just at runtime.\n *\n * Detects the common literal form: a `new Thought({ … })` options object that has a `payload`\n * property but no `replayCompatibility` property. Spreads or computed keys are not analyzable here —\n * those fall back to the runtime check.\n *\n * Opt-out:\n * // eslint-disable-next-line adk/thought-payload-requires-replay-tag -- <reason>\n */\n\nimport { createRule } from './common'\n\nimport type { TSESTree } from '@typescript-eslint/utils'\n\nconst literalKey = (prop: TSESTree.Property): string | undefined => {\n if (prop.computed) return undefined\n if (prop.key.type === 'Identifier') return prop.key.name\n if (prop.key.type === 'Literal' && typeof prop.key.value === 'string') return prop.key.value\n return undefined\n}\n\n// True when the property value is statically `undefined` (the absent case) — `payload: undefined`\n// is equivalent to omitting it, so it should not trigger the rule.\nconst isUndefinedValue = (node: TSESTree.Node): boolean =>\n node.type === 'Identifier' && node.name === 'undefined'\n\nconst thoughtPayloadRequiresReplayTagRule = createRule({\n name: 'thought-payload-requires-replay-tag',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'A `Thought` carrying a `payload` must declare `replayCompatibility`; otherwise the harness cannot route the opaque payload to a matching adapter and drops it. Enforced at runtime by the Thought constructor; this surfaces it at the construction site.',\n },\n schema: [],\n messages: {\n requireReplayTag:\n '`new Thought({ payload })` requires a `replayCompatibility` tag — without it the harness cannot route the opaque payload to an adapter and silently drops it. Add `replayCompatibility: \"<wire-shape-id>\"`, or remove the payload. Opt out with an eslint-disable-next-line adk/thought-payload-requires-replay-tag comment + reason.',\n },\n },\n defaultOptions: [],\n create(context) {\n return {\n NewExpression(node: TSESTree.NewExpression) {\n if (node.callee.type !== 'Identifier' || node.callee.name !== 'Thought') return\n const arg = node.arguments[0]\n if (!arg || arg.type !== 'ObjectExpression') return\n\n let payloadProp: TSESTree.Property | undefined\n let hasReplayTag = false\n let hasSpread = false\n for (const p of arg.properties) {\n if (p.type === 'SpreadElement') {\n hasSpread = true\n continue\n }\n const key = literalKey(p)\n if (key === 'payload' && !isUndefinedValue(p.value)) payloadProp = p\n else if (key === 'replayCompatibility' && !isUndefinedValue(p.value)) hasReplayTag = true\n }\n\n // A spread could supply replayCompatibility we can't see — stay conservative, don't flag.\n if (payloadProp && !hasReplayTag && !hasSpread) {\n context.report({ node: payloadProp, messageId: 'requireReplayTag' })\n }\n },\n }\n },\n})\n\nexport default thoughtPayloadRequiresReplayTagRule\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,IAAM,cAAc,SAAgD;CAClE,IAAI,KAAK,UAAU,OAAO,KAAA;CAC1B,IAAI,KAAK,IAAI,SAAS,cAAc,OAAO,KAAK,IAAI;CACpD,IAAI,KAAK,IAAI,SAAS,aAAa,OAAO,KAAK,IAAI,UAAU,UAAU,OAAO,KAAK,IAAI;AAEzF;AAIA,IAAM,oBAAoB,SACxB,KAAK,SAAS,gBAAgB,KAAK,SAAS;AAE9C,IAAM,sCAAsC,eAAA,WAAW;CACrD,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,4PACJ;EACA,QAAQ,CAAC;EACT,UAAU,EACR,kBACE,0UACJ;CACF;CACA,gBAAgB,CAAC;CACjB,OAAO,SAAS;EACd,OAAO,EACL,cAAc,MAA8B;GAC1C,IAAI,KAAK,OAAO,SAAS,gBAAgB,KAAK,OAAO,SAAS,WAAW;GACzE,MAAM,MAAM,KAAK,UAAU;GAC3B,IAAI,CAAC,OAAO,IAAI,SAAS,oBAAoB;GAE7C,IAAI;GACJ,IAAI,eAAe;GACnB,IAAI,YAAY;GAChB,KAAK,MAAM,KAAK,IAAI,YAAY;IAC9B,IAAI,EAAE,SAAS,iBAAiB;KAC9B,YAAY;KACZ;IACF;IACA,MAAM,MAAM,WAAW,CAAC;IACxB,IAAI,QAAQ,aAAa,CAAC,iBAAiB,EAAE,KAAK,GAAG,cAAc;SAC9D,IAAI,QAAQ,yBAAyB,CAAC,iBAAiB,EAAE,KAAK,GAAG,eAAe;GACvF;GAGA,IAAI,eAAe,CAAC,gBAAgB,CAAC,WACnC,QAAQ,OAAO;IAAE,MAAM;IAAa,WAAW;GAAmB,CAAC;EAEvE,EACF;CACF;AACF,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @nhtio/adk/eslint/rules/thought_payload_requires_replay_tag
|
|
3
|
+
*
|
|
4
|
+
* Flags `new Thought({ … })` whose options literal sets a `payload` but omits a
|
|
5
|
+
* `replayCompatibility` tag.
|
|
6
|
+
*
|
|
7
|
+
* Why: a `Thought` may carry a vendor-opaque `payload` that the harness cannot interpret, to be
|
|
8
|
+
* replayed back to a matching model wire. The ADK can only route that payload if the thought also
|
|
9
|
+
* declares which adapter wire-shape it is replayable into, via `replayCompatibility`. A `payload`
|
|
10
|
+
* with no `replayCompatibility` is a footgun: the harness has no way to know which adapter can
|
|
11
|
+
* consume it, so it is dropped silently. The `Thought` constructor enforces this cross-field
|
|
12
|
+
* invariant at runtime (`payload` present ⇒ `replayCompatibility` required); this rule surfaces it
|
|
13
|
+
* statically at the construction site so it fails lint, not just at runtime.
|
|
14
|
+
*
|
|
15
|
+
* Detects the common literal form: a `new Thought({ … })` options object that has a `payload`
|
|
16
|
+
* property but no `replayCompatibility` property. Spreads or computed keys are not analyzable here —
|
|
17
|
+
* those fall back to the runtime check.
|
|
18
|
+
*
|
|
19
|
+
* Opt-out:
|
|
20
|
+
* // eslint-disable-next-line adk/thought-payload-requires-replay-tag -- <reason>
|
|
21
|
+
*/
|
|
22
|
+
declare const thoughtPayloadRequiresReplayTagRule: import("@typescript-eslint/utils/ts-eslint").RuleModule<"requireReplayTag", [
|
|
23
|
+
], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
24
|
+
name: string;
|
|
25
|
+
};
|
|
26
|
+
export default thoughtPayloadRequiresReplayTagRule;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { t as createRule } from "../../common-BUIjZ6LV.mjs";
|
|
2
|
+
//#region src/eslint/rules/thought_payload_requires_replay_tag.ts
|
|
3
|
+
/**
|
|
4
|
+
* @module @nhtio/adk/eslint/rules/thought_payload_requires_replay_tag
|
|
5
|
+
*
|
|
6
|
+
* Flags `new Thought({ … })` whose options literal sets a `payload` but omits a
|
|
7
|
+
* `replayCompatibility` tag.
|
|
8
|
+
*
|
|
9
|
+
* Why: a `Thought` may carry a vendor-opaque `payload` that the harness cannot interpret, to be
|
|
10
|
+
* replayed back to a matching model wire. The ADK can only route that payload if the thought also
|
|
11
|
+
* declares which adapter wire-shape it is replayable into, via `replayCompatibility`. A `payload`
|
|
12
|
+
* with no `replayCompatibility` is a footgun: the harness has no way to know which adapter can
|
|
13
|
+
* consume it, so it is dropped silently. The `Thought` constructor enforces this cross-field
|
|
14
|
+
* invariant at runtime (`payload` present ⇒ `replayCompatibility` required); this rule surfaces it
|
|
15
|
+
* statically at the construction site so it fails lint, not just at runtime.
|
|
16
|
+
*
|
|
17
|
+
* Detects the common literal form: a `new Thought({ … })` options object that has a `payload`
|
|
18
|
+
* property but no `replayCompatibility` property. Spreads or computed keys are not analyzable here —
|
|
19
|
+
* those fall back to the runtime check.
|
|
20
|
+
*
|
|
21
|
+
* Opt-out:
|
|
22
|
+
* // eslint-disable-next-line adk/thought-payload-requires-replay-tag -- <reason>
|
|
23
|
+
*/
|
|
24
|
+
var literalKey = (prop) => {
|
|
25
|
+
if (prop.computed) return void 0;
|
|
26
|
+
if (prop.key.type === "Identifier") return prop.key.name;
|
|
27
|
+
if (prop.key.type === "Literal" && typeof prop.key.value === "string") return prop.key.value;
|
|
28
|
+
};
|
|
29
|
+
var isUndefinedValue = (node) => node.type === "Identifier" && node.name === "undefined";
|
|
30
|
+
var thoughtPayloadRequiresReplayTagRule = createRule({
|
|
31
|
+
name: "thought-payload-requires-replay-tag",
|
|
32
|
+
meta: {
|
|
33
|
+
type: "problem",
|
|
34
|
+
docs: { description: "A `Thought` carrying a `payload` must declare `replayCompatibility`; otherwise the harness cannot route the opaque payload to a matching adapter and drops it. Enforced at runtime by the Thought constructor; this surfaces it at the construction site." },
|
|
35
|
+
schema: [],
|
|
36
|
+
messages: { requireReplayTag: "`new Thought({ payload })` requires a `replayCompatibility` tag — without it the harness cannot route the opaque payload to an adapter and silently drops it. Add `replayCompatibility: \"<wire-shape-id>\"`, or remove the payload. Opt out with an eslint-disable-next-line adk/thought-payload-requires-replay-tag comment + reason." }
|
|
37
|
+
},
|
|
38
|
+
defaultOptions: [],
|
|
39
|
+
create(context) {
|
|
40
|
+
return { NewExpression(node) {
|
|
41
|
+
if (node.callee.type !== "Identifier" || node.callee.name !== "Thought") return;
|
|
42
|
+
const arg = node.arguments[0];
|
|
43
|
+
if (!arg || arg.type !== "ObjectExpression") return;
|
|
44
|
+
let payloadProp;
|
|
45
|
+
let hasReplayTag = false;
|
|
46
|
+
let hasSpread = false;
|
|
47
|
+
for (const p of arg.properties) {
|
|
48
|
+
if (p.type === "SpreadElement") {
|
|
49
|
+
hasSpread = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const key = literalKey(p);
|
|
53
|
+
if (key === "payload" && !isUndefinedValue(p.value)) payloadProp = p;
|
|
54
|
+
else if (key === "replayCompatibility" && !isUndefinedValue(p.value)) hasReplayTag = true;
|
|
55
|
+
}
|
|
56
|
+
if (payloadProp && !hasReplayTag && !hasSpread) context.report({
|
|
57
|
+
node: payloadProp,
|
|
58
|
+
messageId: "requireReplayTag"
|
|
59
|
+
});
|
|
60
|
+
} };
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
//#endregion
|
|
64
|
+
export { thoughtPayloadRequiresReplayTagRule as default };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=thought_payload_requires_replay_tag.mjs.map
|