@rsconcept/rstool 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +5 -5
  2. package/dist/{agent-workflow-DY-hfAap.d.ts → agent-workflow-BqEC_Foh.d.ts} +5 -6
  3. package/dist/{analysis-ktWFu4BY.d.ts → analysis-C7GrmB17.d.ts} +3 -4
  4. package/dist/{common-D6TFl6l_.d.ts → common-oafVB-wX.d.ts} +1 -2
  5. package/dist/{constituenta-HTnFtmxt.d.ts → constituenta-CwJogNKY.d.ts} +4 -5
  6. package/dist/{diagnostic-CODFUU-Q.d.ts → diagnostic-CtxXifJY.d.ts} +4 -3
  7. package/dist/{diagnostic-assembly-B-6vIA5I.js → diagnostic-assembly-B-r0NyHL.js} +3 -2
  8. package/dist/diagnostic-assembly-B-r0NyHL.js.map +1 -0
  9. package/dist/{evaluation-6A7rK4t7.d.ts → evaluation-BHv2xNwf.d.ts} +3 -4
  10. package/dist/index.d.ts +11 -10
  11. package/dist/index.js +1 -1
  12. package/dist/mappers/model-adapter.d.ts +9 -4
  13. package/dist/mappers/model-adapter.js +29 -4
  14. package/dist/mappers/model-adapter.js.map +1 -1
  15. package/dist/mappers/schema-adapter.d.ts +5 -5
  16. package/dist/mappers/schema-adapter.js +1 -1
  17. package/dist/mappers/types.d.ts +5 -4
  18. package/dist/mappers/types.js +1 -1
  19. package/dist/mappers/types.js.map +1 -1
  20. package/dist/{model-value-BUhr2IjA.d.ts → model-value-C0QA-ji2.d.ts} +2 -3
  21. package/dist/models/agent-workflow.d.ts +1 -1
  22. package/dist/models/analysis.d.ts +1 -1
  23. package/dist/models/common.d.ts +1 -1
  24. package/dist/models/constituenta.d.ts +1 -1
  25. package/dist/models/diagnostic.d.ts +1 -1
  26. package/dist/models/diagnostic.js +1 -1
  27. package/dist/models/evaluation.d.ts +1 -1
  28. package/dist/models/index.d.ts +10 -10
  29. package/dist/models/index.js +1 -1
  30. package/dist/models/model-value.d.ts +1 -1
  31. package/dist/models/rstool-agent.d.ts +1 -1
  32. package/dist/models/rstool-agent.js +1 -1
  33. package/dist/models/session.d.ts +1 -1
  34. package/dist/models/tool-contract.d.ts +1 -1
  35. package/dist/models/tool-contract.js +1 -1
  36. package/dist/models/tool-contract.js.map +1 -1
  37. package/dist/{models-BKmWx92e.js → models-D06g8LId.js} +8 -7
  38. package/dist/{models-BKmWx92e.js.map → models-D06g8LId.js.map} +1 -1
  39. package/dist/{rsform-builder-DKwZfJvN.js → rsform-builder-BlXEe5Ow.js} +2 -2
  40. package/dist/{rsform-builder-DKwZfJvN.js.map → rsform-builder-BlXEe5Ow.js.map} +1 -1
  41. package/dist/{rstool-agent-D-w4DlEo.d.ts → rstool-agent-DLq0mtym.d.ts} +9 -10
  42. package/dist/session/session-store.d.ts +3 -2
  43. package/dist/session/session-store.js +1 -1
  44. package/dist/{session-Uc_vHrq9.d.ts → session-OaLqLq0H.d.ts} +3 -4
  45. package/dist/{tool-contract-i0v2gbWs.d.ts → tool-contract-ZVyh7hhp.d.ts} +10 -11
  46. package/dist/wrapper/client.d.ts +12 -5
  47. package/dist/wrapper/client.js +79 -12
  48. package/dist/wrapper/client.js.map +1 -1
  49. package/dist/wrapper/stdio-wrapper.d.ts +1 -1
  50. package/dist/wrapper/stdio-wrapper.js +18 -10
  51. package/dist/wrapper/stdio-wrapper.js.map +1 -1
  52. package/docs/CONCEPTUAL-SCHEMA.md +1 -1
  53. package/docs/CONSTRUCTIONS.md +372 -0
  54. package/docs/DIAGNOSTICS.md +70 -28
  55. package/docs/DOMAIN.md +7 -2
  56. package/docs/MODEL-TESTING.md +48 -0
  57. package/docs/README.md +1 -0
  58. package/docs/SYNTAX.md +1 -1
  59. package/docs/TYPIFICATION.md +11 -2
  60. package/examples/diagnostics-utils.ts +3 -4
  61. package/package.json +20 -19
  62. package/skills/README.md +1 -1
  63. package/skills/rstool-helper/EXAMPLES.md +24 -0
  64. package/skills/rstool-helper/GUIDE.md +49 -22
  65. package/skills/rstool-helper/REFERENCE.md +24 -5
  66. package/skills/rstool-helper/SKILL.md +7 -4
  67. package/src/mappers/diagnostic-assembly.ts +5 -2
  68. package/src/mappers/model-adapter.ts +34 -1
  69. package/src/mappers/types.ts +1 -0
  70. package/src/models/rstool-agent.test.ts +95 -0
  71. package/src/models/tool-contract.ts +1 -1
  72. package/src/wrapper/client.test.ts +43 -4
  73. package/src/wrapper/client.ts +95 -16
  74. package/src/wrapper/stdio-handler.test.ts +43 -0
  75. package/src/wrapper/stdio-handler.ts +19 -5
  76. package/dist/diagnostic-assembly-B-6vIA5I.js.map +0 -1
@@ -1029,6 +1029,101 @@ describe('RSToolAgent modeling semantics', () => {
1029
1029
  expect(modelIssues.some(record => record.code === RSDiagnosticCode.modelAxiomFalse)).toBe(true);
1030
1030
  });
1031
1031
 
1032
+ function buildGraphScratch(tool: RSToolAgent, sessionId: string) {
1033
+ tool.applySchemaPatch(
1034
+ {
1035
+ items: [
1036
+ { alias: 'X1' },
1037
+ { alias: 'S1', definitionFormal: 'ℬ(X1×X1)' },
1038
+ { alias: 'D1', definitionFormal: 'Pr2,1(S1)' },
1039
+ { alias: 'D2', definitionFormal: 'S1\\D1' },
1040
+ { alias: 'F1', definitionFormal: '[α∈X1] Pr2(Fi1[{α}](D2))' }
1041
+ ]
1042
+ },
1043
+ sessionId
1044
+ );
1045
+ }
1046
+
1047
+ it('normalizes unsorted structure values before evaluation', async () => {
1048
+ const tool = new RSToolAgent();
1049
+ const session = tool.createSession();
1050
+ buildGraphScratch(tool, session.sessionId);
1051
+
1052
+ await tool.setModelValues(
1053
+ {
1054
+ set: [
1055
+ { target: 1, value: { 0: 'a', 1: 'b', 2: 'c' } },
1056
+ {
1057
+ target: 2,
1058
+ value: [
1059
+ [TUPLE_ID, 1, 2],
1060
+ [TUPLE_ID, 0, 2],
1061
+ [TUPLE_ID, 0, 1],
1062
+ [TUPLE_ID, 1, 0],
1063
+ [TUPLE_ID, 0, 1]
1064
+ ]
1065
+ }
1066
+ ]
1067
+ },
1068
+ session.sessionId
1069
+ );
1070
+
1071
+ const stored = tool.getModelState(session.sessionId).items.find(item => item.id === 2);
1072
+ expect(stored?.value).toEqual([
1073
+ [TUPLE_ID, 0, 1],
1074
+ [TUPLE_ID, 0, 2],
1075
+ [TUPLE_ID, 1, 0],
1076
+ [TUPLE_ID, 1, 2]
1077
+ ]);
1078
+
1079
+ // S1 \ inverse(S1): (0,1) and (1,0) cancel out
1080
+ const result = tool.evaluate({ constituentId: 4 }, session.sessionId);
1081
+ expect(result.status).toBe(EvalStatus.HAS_DATA);
1082
+ expect(result.value).toEqual([
1083
+ [TUPLE_ID, 0, 2],
1084
+ [TUPLE_ID, 1, 2]
1085
+ ]);
1086
+ });
1087
+
1088
+ it('evaluates scratch expressions that reference derived constituents', async () => {
1089
+ const tool = new RSToolAgent();
1090
+ const session = tool.createSession();
1091
+ buildGraphScratch(tool, session.sessionId);
1092
+ await tool.setModelValues(
1093
+ {
1094
+ set: [
1095
+ { target: 1, value: { 0: 'a', 1: 'b', 2: 'c' } },
1096
+ {
1097
+ target: 2,
1098
+ value: [
1099
+ [TUPLE_ID, 0, 1],
1100
+ [TUPLE_ID, 0, 2],
1101
+ [TUPLE_ID, 1, 0],
1102
+ [TUPLE_ID, 1, 2]
1103
+ ]
1104
+ }
1105
+ ]
1106
+ },
1107
+ session.sessionId
1108
+ );
1109
+
1110
+ const direct = tool.evaluate({ expression: 'card(D2)', cstType: CstType.TERM }, session.sessionId);
1111
+ expect(direct.success).toBe(true);
1112
+ expect(direct.value).toBe(2);
1113
+
1114
+ // F1 body references D2, which must be computed before the call
1115
+ const viaFunction = tool.evaluate(
1116
+ { expression: 'card(red(I{F1[ξ] | ξ:∈Pr1(D2)}))', cstType: CstType.TERM },
1117
+ session.sessionId
1118
+ );
1119
+ expect(viaFunction.success).toBe(true);
1120
+ expect(viaFunction.value).toBe(1);
1121
+
1122
+ const logic = tool.evaluate({ expression: 'D2⊆S1', cstType: CstType.AXIOM }, session.sessionId);
1123
+ expect(logic.success).toBe(true);
1124
+ expect(logic.value).toBe(1);
1125
+ });
1126
+
1032
1127
  it('throws when evaluate input is incomplete', () => {
1033
1128
  const tool = new RSToolAgent();
1034
1129
  const session = tool.createSession();
@@ -15,7 +15,7 @@ import { type RecalculateModelResult, type SessionModelState, type SetModelValue
15
15
  import { type SessionHandle, type SessionRevision, type SessionState } from './session';
16
16
 
17
17
  /** Agent-visible contract version; bump on breaking API changes. */
18
- export const CONTRACT_VERSION = '3.2.0';
18
+ export const CONTRACT_VERSION = '3.3.0';
19
19
 
20
20
  /** Options for constructing an {@link RSToolAgent}. */
21
21
  export interface RSToolAgentOptions {
@@ -5,14 +5,53 @@ import { describe, expect, it } from 'vitest';
5
5
  import { CstType, EvalStatus, RSToolWrapperClient } from '../index';
6
6
 
7
7
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
8
+ const tsxCli = fileURLToPath(import.meta.resolve('tsx/cli'));
9
+ const wrapperEntry = resolve(packageRoot, 'src/wrapper/stdio-wrapper.ts');
8
10
 
9
11
  describe('RSToolWrapperClient integration', () => {
12
+ it('rejects readiness and calls when the wrapper command cannot start', async () => {
13
+ const client = new RSToolWrapperClient({
14
+ command: resolve(packageRoot, 'definitely-missing-rstool-wrapper-binary'),
15
+ args: [],
16
+ cwd: packageRoot
17
+ });
18
+ try {
19
+ await expect(client.waitUntilReady()).rejects.toThrow(/ENOENT/);
20
+ await expect(client.call('methods')).rejects.toThrow(/ENOENT/);
21
+ } finally {
22
+ await client.close();
23
+ }
24
+ }, 30_000);
25
+
26
+ it('rejects outstanding calls when the wrapper exits before responding', async () => {
27
+ const client = new RSToolWrapperClient({
28
+ command: process.execPath,
29
+ args: ['-e', 'process.stdin.resume(); setTimeout(() => process.exit(3), 200)'],
30
+ cwd: packageRoot
31
+ });
32
+ try {
33
+ await expect(client.call('methods')).rejects.toThrow(/exited before responding \(code 3/);
34
+ } finally {
35
+ await client.close();
36
+ }
37
+ }, 30_000);
38
+
39
+ it('starts the default npm wrapper without a shell', async () => {
40
+ const client = new RSToolWrapperClient({ cwd: packageRoot });
41
+ try {
42
+ await client.waitUntilReady();
43
+ const methods = await client.call<string[]>('methods');
44
+ expect(methods).toContain('applySchemaPatch');
45
+ } finally {
46
+ await client.close();
47
+ }
48
+ }, 30_000);
49
+
10
50
  it('runs createSession, patch, model, and evaluate over stdio', async () => {
11
51
  const client = new RSToolWrapperClient({
12
- command: 'npx',
13
- args: ['tsx', resolve(packageRoot, 'src/wrapper/stdio-wrapper.ts')],
14
- cwd: packageRoot,
15
- shell: true
52
+ command: process.execPath,
53
+ args: [tsxCli, wrapperEntry],
54
+ cwd: packageRoot
16
55
  });
17
56
 
18
57
  try {
@@ -1,4 +1,6 @@
1
1
  import { spawn, type ChildProcessByStdio } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
2
4
  import readline from 'node:readline';
3
5
  import { type Readable, type Writable } from 'node:stream';
4
6
 
@@ -16,14 +18,38 @@ export interface WrapperResponse<T = unknown> {
16
18
 
17
19
  /** Options for spawning the `rstool-wrapper` child process. */
18
20
  export interface RSToolWrapperClientOptions {
19
- /** Executable to spawn. Default: `npm`. */
21
+ /** Executable to spawn. Default: `npm`. Passed as argv, not a shell string. */
20
22
  command?: string;
21
23
  /** Arguments passed to `command`. Default: `['run', 'wrapper']`. */
22
24
  args?: string[];
23
25
  /** Working directory for the child process. Default: `process.cwd()`. */
24
26
  cwd?: string;
25
- /** Whether to run the command in a shell. Default: `true`. */
26
- shell?: boolean;
27
+ }
28
+
29
+ /**
30
+ * Rewrite Windows npm/npx shims to `node <npm-cli.js>` so spawn() never needs a shell.
31
+ * Node 20+ refuses to exec `.cmd` files without `shell: true` (CVE-2024-27980).
32
+ */
33
+ function resolveSpawn(command: string, args: string[]): { command: string; args: string[] } {
34
+ if (process.platform !== 'win32') {
35
+ return { command, args };
36
+ }
37
+ const base = (command.replaceAll('\\', '/').split('/').pop() ?? command).toLowerCase();
38
+ if (base === 'npm' || base === 'npm.cmd') {
39
+ return { command: process.execPath, args: [bundledNpmBin('npm-cli.js'), ...args] };
40
+ }
41
+ if (base === 'npx' || base === 'npx.cmd') {
42
+ return { command: process.execPath, args: [bundledNpmBin('npx-cli.js'), ...args] };
43
+ }
44
+ return { command, args };
45
+ }
46
+
47
+ function bundledNpmBin(bin: 'npm-cli.js' | 'npx-cli.js'): string {
48
+ const cli = join(dirname(process.execPath), 'node_modules', 'npm', 'bin', bin);
49
+ if (!existsSync(cli)) {
50
+ throw new Error(`Cannot resolve ${bin} next to Node at ${process.execPath}`);
51
+ }
52
+ return cli;
27
53
  }
28
54
 
29
55
  /**
@@ -34,16 +60,20 @@ export interface RSToolWrapperClientOptions {
34
60
  export class RSToolWrapperClient {
35
61
  private process: ChildProcessByStdio<Writable, Readable, null>;
36
62
  private input: readline.Interface;
37
- private pending = new Map<string, (value: WrapperResponse) => void>();
63
+ private pending = new Map<string, PendingCall>();
38
64
  private requestCounter = 1;
65
+ /** Set once the child cannot start or has exited; every later request fails with this error. */
66
+ private failure: Error | null = null;
67
+ private closing = false;
39
68
 
40
69
  /**
41
70
  * @param options - Spawn configuration; defaults run `npm run wrapper` in the current directory.
42
71
  */
43
72
  public constructor(options: RSToolWrapperClientOptions = {}) {
44
- this.process = spawn(options.command ?? 'npm', options.args ?? ['run', 'wrapper'], {
73
+ const resolved = resolveSpawn(options.command ?? 'npm', options.args ?? ['run', 'wrapper']);
74
+ this.process = spawn(resolved.command, resolved.args, {
45
75
  cwd: options.cwd ?? process.cwd(),
46
- shell: options.shell ?? true,
76
+ windowsHide: true,
47
77
  stdio: ['pipe', 'pipe', 'inherit']
48
78
  });
49
79
  this.input = readline.createInterface({
@@ -51,9 +81,25 @@ export class RSToolWrapperClient {
51
81
  crlfDelay: Infinity
52
82
  });
53
83
  this.input.on('line', line => this.handleLine(line));
84
+
85
+ // Without these listeners a failed spawn (ENOENT, EACCES) or a broken stdin pipe
86
+ // surfaces as an uncaught exception instead of a rejected promise.
87
+ this.process.on('error', error => this.fail(error));
88
+ this.process.stdin.on('error', error => this.fail(error));
89
+ this.process.on('exit', (code, signal) => {
90
+ if (this.closing) {
91
+ this.fail(new Error('Wrapper closed'));
92
+ return;
93
+ }
94
+ this.fail(new Error(`Wrapper exited before responding (code ${code ?? 'null'}, signal ${signal ?? 'null'})`));
95
+ });
54
96
  }
55
97
 
56
- /** Block until the wrapper emits its initial `{ ready: true }` event. */
98
+ /**
99
+ * Block until the wrapper emits its initial `{ ready: true }` event.
100
+ *
101
+ * @throws When the wrapper process cannot be started or exits before becoming ready.
102
+ */
57
103
  public async waitUntilReady(): Promise<void> {
58
104
  for (;;) {
59
105
  const line = await this.readOneEvent();
@@ -74,13 +120,16 @@ export class RSToolWrapperClient {
74
120
  *
75
121
  * @param method - Stdio method name (matches {@link RSToolAgentContract} operations).
76
122
  * @param params - Method parameters object.
77
- * @throws When the wrapper responds with `ok: false`.
123
+ * @throws When the wrapper responds with `ok: false`, cannot be started, or exits before responding.
78
124
  */
79
125
  public async call<T>(method: string, params: unknown = {}): Promise<T> {
126
+ if (this.failure) {
127
+ throw this.failure;
128
+ }
80
129
  const id = String(this.requestCounter++);
81
130
  const payload = JSON.stringify({ id, method, params });
82
- const responsePromise = new Promise<WrapperResponse>(resolve => {
83
- this.pending.set(id, resolve);
131
+ const responsePromise = new Promise<WrapperResponse>((resolve, reject) => {
132
+ this.pending.set(id, { resolve, reject });
84
133
  });
85
134
  this.process.stdin.write(`${payload}\n`);
86
135
  const response = await responsePromise;
@@ -92,14 +141,20 @@ export class RSToolWrapperClient {
92
141
 
93
142
  /** Close stdin and terminate the wrapper process. */
94
143
  public async close(): Promise<void> {
144
+ this.closing = true;
95
145
  this.input.close();
96
- this.process.stdin.end();
97
- if (!this.process.killed) {
146
+ if (!this.process.stdin.destroyed) {
147
+ this.process.stdin.end();
148
+ }
149
+ if (this.process.exitCode === null && !this.process.killed) {
98
150
  this.process.kill();
99
151
  }
100
152
  }
101
153
 
102
154
  private async readOneEvent(): Promise<string> {
155
+ if (this.failure) {
156
+ throw this.failure;
157
+ }
103
158
  return new Promise((resolve, reject) => {
104
159
  const onLine = (line: string) => {
105
160
  cleanup();
@@ -107,17 +162,36 @@ export class RSToolWrapperClient {
107
162
  };
108
163
  const onExit = () => {
109
164
  cleanup();
110
- reject(new Error('Wrapper exited before ready'));
165
+ reject(this.failure ?? new Error('Wrapper exited before ready'));
166
+ };
167
+ const onError = (error: Error) => {
168
+ cleanup();
169
+ reject(error);
111
170
  };
112
171
  const cleanup = () => {
113
172
  this.input.off('line', onLine);
114
173
  this.process.off('exit', onExit);
174
+ this.process.off('error', onError);
115
175
  };
116
176
  this.input.on('line', onLine);
117
177
  this.process.on('exit', onExit);
178
+ this.process.on('error', onError);
118
179
  });
119
180
  }
120
181
 
182
+ /** Records the terminal failure and rejects every outstanding request. */
183
+ private fail(error: Error): void {
184
+ if (this.failure) {
185
+ return;
186
+ }
187
+ this.failure = error;
188
+ const outstanding = [...this.pending.values()];
189
+ this.pending.clear();
190
+ for (const entry of outstanding) {
191
+ entry.reject(error);
192
+ }
193
+ }
194
+
121
195
  private handleLine(line: string): void {
122
196
  let parsed: WrapperResponse;
123
197
  try {
@@ -129,11 +203,16 @@ export class RSToolWrapperClient {
129
203
  return;
130
204
  }
131
205
  const id = String(parsed.id);
132
- const resolver = this.pending.get(id);
133
- if (!resolver) {
206
+ const entry = this.pending.get(id);
207
+ if (!entry) {
134
208
  return;
135
209
  }
136
210
  this.pending.delete(id);
137
- resolver(parsed);
211
+ entry.resolve(parsed);
138
212
  }
139
213
  }
214
+
215
+ interface PendingCall {
216
+ resolve: (value: WrapperResponse) => void;
217
+ reject: (reason: Error) => void;
218
+ }
@@ -98,4 +98,47 @@ describe('handleStdioRequest', () => {
98
98
  expect((all.result as unknown[]).length).toBeGreaterThan(0);
99
99
  expect(filtered.result).toEqual(all.result);
100
100
  });
101
+
102
+ it('filters diagnostics by flat kind and severity params', async () => {
103
+ const sessionTool = new RSToolAgent();
104
+ const created = await handleStdioRequest(sessionTool, { id: 30, method: 'createSession' });
105
+ const sessionId = (created.result as { sessionId: string }).sessionId;
106
+ // X1 without convention → schema diagnostic; D1 with broken formula → expression diagnostic
107
+ await handleStdioRequest(sessionTool, {
108
+ id: 31,
109
+ method: 'applySchemaPatch',
110
+ params: {
111
+ sessionId,
112
+ mode: 'best_effort',
113
+ items: [{ alias: 'X1' }, { alias: 'D1', definitionFormal: 'Pr1(' }]
114
+ }
115
+ });
116
+
117
+ const all = await handleStdioRequest(sessionTool, { id: 32, method: 'listDiagnostics', params: { sessionId } });
118
+ const kinds = new Set((all.result as { kind: string }[]).map(item => item.kind));
119
+ expect(kinds.has('schema')).toBe(true);
120
+ expect(kinds.has('expression')).toBe(true);
121
+
122
+ const schemaOnly = await handleStdioRequest(sessionTool, {
123
+ id: 33,
124
+ method: 'listDiagnostics',
125
+ params: { sessionId, kind: 'schema' }
126
+ });
127
+ expect((schemaOnly.result as { kind: string }[]).every(item => item.kind === 'schema')).toBe(true);
128
+ expect((schemaOnly.result as unknown[]).length).toBeLessThan((all.result as unknown[]).length);
129
+
130
+ const warningsOnly = await handleStdioRequest(sessionTool, {
131
+ id: 34,
132
+ method: 'listDiagnostics',
133
+ params: { sessionId, severity: 'warning' }
134
+ });
135
+ expect((warningsOnly.result as { severity: string }[]).every(item => item.severity === 'warning')).toBe(true);
136
+
137
+ const legacy = await handleStdioRequest(sessionTool, {
138
+ id: 35,
139
+ method: 'listDiagnostics',
140
+ params: { sessionId, filters: { kind: 'schema' } }
141
+ });
142
+ expect(legacy.result).toEqual(schemaOnly.result);
143
+ });
101
144
  });
@@ -1,3 +1,4 @@
1
+ import type { ListDiagnosticsFilters } from '../models/diagnostic';
1
2
  import type { RSToolAgent } from '../models/rstool-agent';
2
3
 
3
4
  export interface StdioRequest {
@@ -63,6 +64,22 @@ function omitSessionId(input: Record<string, unknown>): Record<string, unknown>
63
64
  return rest;
64
65
  }
65
66
 
67
+ /** Flat `constituentId` / `kind` / `severity` params; legacy nested `filters` object is still accepted. */
68
+ function diagnosticsFilters(input: Record<string, unknown>): ListDiagnosticsFilters | undefined {
69
+ const source = { ...asObject(input.filters), ...omitSessionId(input) };
70
+ const filters: ListDiagnosticsFilters = {};
71
+ if (typeof source.constituentId === 'number') {
72
+ filters.constituentId = source.constituentId;
73
+ }
74
+ if (source.kind === 'expression' || source.kind === 'schema' || source.kind === 'model') {
75
+ filters.kind = source.kind;
76
+ }
77
+ if (source.severity === 'error' || source.severity === 'warning') {
78
+ filters.severity = source.severity;
79
+ }
80
+ return Object.keys(filters).length > 0 ? filters : undefined;
81
+ }
82
+
66
83
  export async function handleStdioRequest(tool: RSToolAgent, request: StdioRequest): Promise<StdioResponse> {
67
84
  try {
68
85
  const params = asObject(request.params);
@@ -110,15 +127,12 @@ export async function handleStdioRequest(tool: RSToolAgent, request: StdioReques
110
127
  optionalSessionId(params)
111
128
  )
112
129
  };
113
- case 'listDiagnostics': {
114
- const constituentId = params.constituentId;
115
- const filters = typeof constituentId === 'number' ? { constituentId } : (params.filters as never);
130
+ case 'listDiagnostics':
116
131
  return {
117
132
  id: request.id,
118
133
  ok: true,
119
- result: tool.listDiagnostics(filters, optionalSessionId(params))
134
+ result: tool.listDiagnostics(diagnosticsFilters(params), optionalSessionId(params))
120
135
  };
121
- }
122
136
  case 'analyzeExpression':
123
137
  return {
124
138
  id: request.id,
@@ -1 +0,0 @@
1
- {"version":3,"file":"diagnostic-assembly-B-6vIA5I.js","names":[],"sources":["../src/mappers/diagnostic-assembly.ts"],"sourcesContent":["import {\n type CstDiagnostic,\n DiagnosticKind,\n modelStatusCstDiagnostic,\n RSDiagnosticCode\n} from '@rsconcept/domain/library';\nimport { type DiagnosticSourceFields } from '@rsconcept/domain/library/rsform-api';\nimport { EvalStatus } from '@rsconcept/domain/library/rsmodel';\nimport { isCritical, RSErrorCode, type RSErrorDescription } from '@rsconcept/domain/rslang/error';\n\nexport { DiagnosticKind, RSDiagnosticCode };\n\n/** Severity of a {@link Diagnostic}. */\nexport type DiagnosticSeverity = 'error' | 'warning';\n\n/** Agent-facing diagnostic with redundant fields for autonomous repair. */\nexport interface Diagnostic extends CstDiagnostic {\n name: string;\n severity: DiagnosticSeverity;\n constituentId?: number;\n alias?: string;\n expression: string;\n from: number;\n to: number;\n}\n\n/** Context for attributing a {@link Diagnostic} to a constituent. */\nexport interface DiagnosticTarget {\n constituentId?: number;\n alias?: string;\n}\n\nconst DIAGNOSTIC_NAMES: ReadonlyMap<number, string> = new Map([\n ...Object.entries(RSErrorCode).map(([name, code]) => [code, name] as const),\n ...Object.entries(RSDiagnosticCode).map(([name, code]) => [code, name] as const)\n]);\n\nexport function getDiagnosticName(code: number): string {\n return DIAGNOSTIC_NAMES.get(code) ?? `0x${code.toString(16).toUpperCase()}`;\n}\n\nexport function getDiagnosticSeverity(code: number): DiagnosticSeverity {\n return isCritical(code as RSErrorCode) ? 'error' : 'warning';\n}\n\nexport function expandCstDiagnostic(cst: DiagnosticSourceFields, diagnostic: CstDiagnostic): Diagnostic {\n const expression = expressionForCstDiagnostic(cst, diagnostic);\n return {\n ...diagnostic,\n name: getDiagnosticName(diagnostic.code),\n severity: getDiagnosticSeverity(diagnostic.code),\n constituentId: cst.id,\n alias: cst.alias,\n expression,\n from: 0,\n to: diagnostic.kind === DiagnosticKind.MODEL ? 0 : Math.max(expression.length, 1)\n };\n}\n\nexport function expressionDiagnostic(\n error: RSErrorDescription,\n expression: string,\n target?: DiagnosticTarget\n): Diagnostic {\n return {\n kind: DiagnosticKind.EXPRESSION,\n code: error.code,\n name: getDiagnosticName(error.code),\n severity: getDiagnosticSeverity(error.code),\n constituentId: target?.constituentId,\n alias: target?.alias,\n expression,\n from: error.from,\n to: error.to,\n ...(error.params ? { params: error.params } : {})\n };\n}\n\nexport function modelStatusDiagnostic(status: EvalStatus, cst: DiagnosticSourceFields): Diagnostic | null {\n const base = modelStatusCstDiagnostic(status, cst.cst_type);\n return base ? expandCstDiagnostic(cst, base) : null;\n}\n\nfunction expressionForCstDiagnostic(cst: DiagnosticSourceFields, diagnostic: CstDiagnostic): string {\n switch (diagnostic.code) {\n case RSDiagnosticCode.schemaHomonym:\n case RSDiagnosticCode.schemaMissingTerm:\n return cst.term_resolved;\n case RSDiagnosticCode.schemaMissingConvention:\n return cst.convention;\n case RSDiagnosticCode.schemaTypeMismatch:\n return cst.typification_manual;\n case RSDiagnosticCode.schemaDependencyCycle:\n case RSDiagnosticCode.schemaFormalDuplicate:\n return cst.definition_formal;\n default:\n return cst.definition_formal;\n }\n}\n"],"mappings":";;;AAgCA,MAAM,mBAAgD,IAAI,IAAI,CAC5D,GAAG,OAAO,QAAQ,WAAW,CAAC,CAAC,KAAK,CAAC,MAAM,UAAU,CAAC,MAAM,IAAI,CAAU,GAC1E,GAAG,OAAO,QAAQ,gBAAgB,CAAC,CAAC,KAAK,CAAC,MAAM,UAAU,CAAC,MAAM,IAAI,CAAU,CACjF,CAAC;AAED,SAAgB,kBAAkB,MAAsB;CACtD,OAAO,iBAAiB,IAAI,IAAI,KAAK,KAAK,KAAK,SAAS,EAAE,CAAC,CAAC,YAAY;AAC1E;AAEA,SAAgB,sBAAsB,MAAkC;CACtE,OAAO,WAAW,IAAmB,IAAI,UAAU;AACrD;AAEA,SAAgB,oBAAoB,KAA6B,YAAuC;CACtG,MAAM,aAAa,2BAA2B,KAAK,UAAU;CAC7D,OAAO;EACL,GAAG;EACH,MAAM,kBAAkB,WAAW,IAAI;EACvC,UAAU,sBAAsB,WAAW,IAAI;EAC/C,eAAe,IAAI;EACnB,OAAO,IAAI;EACX;EACA,MAAM;EACN,IAAI,WAAW,SAAS,eAAe,QAAQ,IAAI,KAAK,IAAI,WAAW,QAAQ,CAAC;CAClF;AACF;AAEA,SAAgB,qBACd,OACA,YACA,QACY;CACZ,OAAO;EACL,MAAM,eAAe;EACrB,MAAM,MAAM;EACZ,MAAM,kBAAkB,MAAM,IAAI;EAClC,UAAU,sBAAsB,MAAM,IAAI;EAC1C,eAAe,QAAQ;EACvB,OAAO,QAAQ;EACf;EACA,MAAM,MAAM;EACZ,IAAI,MAAM;EACV,GAAI,MAAM,SAAS,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;CACjD;AACF;AAEA,SAAgB,sBAAsB,QAAoB,KAAgD;CACxG,MAAM,OAAO,yBAAyB,QAAQ,IAAI,QAAQ;CAC1D,OAAO,OAAO,oBAAoB,KAAK,IAAI,IAAI;AACjD;AAEA,SAAS,2BAA2B,KAA6B,YAAmC;CAClG,QAAQ,WAAW,MAAnB;EACE,KAAK,iBAAiB;EACtB,KAAK,iBAAiB,mBACpB,OAAO,IAAI;EACb,KAAK,iBAAiB,yBACpB,OAAO,IAAI;EACb,KAAK,iBAAiB,oBACpB,OAAO,IAAI;EACb,KAAK,iBAAiB;EACtB,KAAK,iBAAiB,uBACpB,OAAO,IAAI;EACb,SACE,OAAO,IAAI;CACf;AACF"}