@rsconcept/rstool 0.10.3 → 1.0.1

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 (116) hide show
  1. package/README.md +61 -33
  2. package/dist/agent-workflow-Gk0Vfnv1.d.ts +64 -0
  3. package/dist/analysis-LLnPhmGa.d.ts +23 -0
  4. package/dist/{common-DxLg3eXX.d.ts → common-DHJalS-Q.d.ts} +6 -1
  5. package/dist/constituenta-DnGR6bnM.d.ts +54 -0
  6. package/dist/diagnostic-D9yl_mEL.d.ts +19 -0
  7. package/dist/evaluation-Cns8BFm4.d.ts +31 -0
  8. package/dist/index.d.ts +11 -11
  9. package/dist/index.js +1 -2
  10. package/dist/mappers/model-adapter.d.ts +3 -3
  11. package/dist/mappers/schema-adapter.d.ts +4 -4
  12. package/dist/mappers/types.d.ts +6 -2
  13. package/dist/mappers/types.js +2 -0
  14. package/dist/mappers/types.js.map +1 -1
  15. package/dist/{model-value-SFAVj0dw.d.ts → model-value-BbonPzMz.d.ts} +14 -3
  16. package/dist/models/agent-workflow.d.ts +2 -0
  17. package/dist/models/agent-workflow.js +1 -0
  18. package/dist/models/analysis.d.ts +1 -1
  19. package/dist/models/common.d.ts +1 -1
  20. package/dist/models/constituenta.d.ts +2 -2
  21. package/dist/models/diagnostic.d.ts +1 -1
  22. package/dist/models/evaluation.d.ts +2 -2
  23. package/dist/models/index.d.ts +11 -11
  24. package/dist/models/index.js +2 -2
  25. package/dist/models/model-value.d.ts +2 -2
  26. package/dist/models/rstool-agent.d.ts +1 -1
  27. package/dist/models/rstool-agent.js +1 -1
  28. package/dist/models/session.d.ts +1 -1
  29. package/dist/models/tool-contract.d.ts +2 -2
  30. package/dist/models/tool-contract.js +2 -1
  31. package/dist/models/tool-contract.js.map +1 -1
  32. package/dist/models-Bw6Uum8i.js +685 -0
  33. package/dist/models-Bw6Uum8i.js.map +1 -0
  34. package/dist/rstool-agent-D2cQze_b.d.ts +71 -0
  35. package/dist/session/session-store.d.ts +18 -5
  36. package/dist/session/session-store.js +1 -64
  37. package/dist/{session-BPgsE80c.d.ts → session-ChexW8i7.d.ts} +11 -8
  38. package/dist/tool-contract-0uRGhEfW.d.ts +164 -0
  39. package/dist/wrapper/client.d.ts +23 -0
  40. package/dist/wrapper/client.js +17 -0
  41. package/dist/wrapper/client.js.map +1 -1
  42. package/dist/wrapper/stdio-wrapper.js +75 -63
  43. package/dist/wrapper/stdio-wrapper.js.map +1 -1
  44. package/docs/CONSTITUENTA.md +2 -2
  45. package/docs/DIAGNOSTICS.md +6 -5
  46. package/docs/MODEL-TESTING.md +3 -3
  47. package/docs/PORTAL-API.md +24 -18
  48. package/examples/README.md +1 -1
  49. package/examples/agent-client.ts +11 -41
  50. package/examples/build-chocolate-nim-rsform.ts +21 -70
  51. package/examples/chocolate-nim/build-rsform.ts +23 -18
  52. package/examples/chocolate-nim/build-rsmodel.ts +10 -12
  53. package/examples/chocolate-nim/rsform-session.json +290 -290
  54. package/examples/chocolate-nim/rsmodel-session.json +291 -291
  55. package/examples/expression-bank/bank-constituents.ts +304 -53
  56. package/examples/expression-bank/build-rsform.ts +19 -16
  57. package/examples/expression-bank/rsform-session.json +1551 -1551
  58. package/examples/kinship/build-rsform.ts +23 -18
  59. package/examples/kinship/build-rsmodel.ts +16 -16
  60. package/examples/kinship/rsform-session.json +219 -219
  61. package/examples/kinship/rsmodel-session.json +221 -221
  62. package/examples/kinship/session.ts +19 -21
  63. package/examples/movd/build-rsform.ts +23 -18
  64. package/examples/movd/build-rsmodel.ts +18 -20
  65. package/examples/movd/rsform-session.json +262 -262
  66. package/examples/movd/rsmodel-session.json +264 -264
  67. package/examples/sample/build-rsform.ts +18 -51
  68. package/examples/sample/build-rsmodel.ts +25 -44
  69. package/examples/sample/rsform-session.json +10 -7
  70. package/examples/sample/rsmodel-session.json +36 -33
  71. package/examples/template-apply/build-rsform.ts +27 -24
  72. package/examples/template-apply/rsform-session.json +48 -48
  73. package/package.json +4 -2
  74. package/skills/rstool-helper/EXAMPLES.md +44 -116
  75. package/skills/rstool-helper/GUIDE.md +40 -25
  76. package/skills/rstool-helper/REFERENCE.md +40 -177
  77. package/src/index.ts +24 -17
  78. package/src/mappers/portal-adapter.ts +49 -0
  79. package/src/mappers/types.ts +4 -0
  80. package/src/models/agent-workflow.ts +66 -0
  81. package/src/models/analysis.ts +7 -0
  82. package/src/models/common.ts +7 -0
  83. package/src/models/constituenta.ts +24 -6
  84. package/src/models/diagnostic.ts +4 -0
  85. package/src/models/evaluation.ts +11 -0
  86. package/src/models/import-detect.test.ts +66 -0
  87. package/src/models/import-detect.ts +42 -0
  88. package/src/models/import-export.ts +24 -0
  89. package/src/models/index.ts +22 -14
  90. package/src/models/model-value.ts +12 -0
  91. package/src/models/portal-json.test.ts +38 -0
  92. package/src/models/portal-json.ts +54 -1
  93. package/src/models/rstool-agent.test.ts +698 -146
  94. package/src/models/rstool-agent.ts +392 -92
  95. package/src/models/session.ts +8 -5
  96. package/src/models/tool-contract.ts +81 -42
  97. package/src/session/batch-apply.test.ts +123 -0
  98. package/src/session/batch-apply.ts +82 -0
  99. package/src/session/persistence.test.ts +63 -0
  100. package/src/session/persistence.ts +69 -0
  101. package/src/session/session-store.ts +76 -6
  102. package/src/wrapper/client.test.ts +58 -0
  103. package/src/wrapper/client.ts +23 -0
  104. package/src/wrapper/stdio-handler.test.ts +101 -0
  105. package/src/wrapper/stdio-handler.ts +195 -0
  106. package/src/wrapper/stdio-wrapper.ts +4 -187
  107. package/dist/analysis-JiwOYDKx.d.ts +0 -16
  108. package/dist/constituenta-Dnd6iToB.d.ts +0 -36
  109. package/dist/diagnostic-BMYvciz8.d.ts +0 -15
  110. package/dist/evaluation-CCVYH0wA.d.ts +0 -21
  111. package/dist/index-uhkmwruf.d.ts +0 -46
  112. package/dist/rstool-agent-BZi5jO1y.js +0 -158
  113. package/dist/rstool-agent-BZi5jO1y.js.map +0 -1
  114. package/dist/rstool-agent-pRaPnZay.d.ts +0 -35
  115. package/dist/session/session-store.js.map +0 -1
  116. package/dist/tool-contract-n1ghUOrK.d.ts +0 -32
@@ -1,17 +1,24 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdtempSync, rmSync, unlinkSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
1
5
  import { describe, expect, it } from 'vitest';
2
6
 
7
+ import { TUPLE_ID } from '@rsconcept/domain';
8
+
3
9
  import { CstType, EvalStatus, RSErrorCode, RSToolAgent } from './index';
4
10
 
5
11
  function buildSampleForm(tool: RSToolAgent, sessionId: string) {
6
- tool.addOrUpdateConstituenta(sessionId, {
7
- draft: { id: 1, alias: 'X1', cstType: CstType.BASE, definitionFormal: '' }
8
- });
9
- tool.addOrUpdateConstituenta(sessionId, {
10
- draft: { id: 2, alias: 'D1', cstType: CstType.TERM, definitionFormal: '1+2' }
11
- });
12
- tool.addOrUpdateConstituenta(sessionId, {
13
- draft: { id: 3, alias: 'A1', cstType: CstType.AXIOM, definitionFormal: '1=1' }
14
- });
12
+ tool.applySchemaPatch(
13
+ {
14
+ items: [{ alias: 'X1' }, { alias: 'D1', definitionFormal: '1+2' }, { alias: 'A1', definitionFormal: '1=1' }]
15
+ },
16
+ sessionId
17
+ );
18
+ }
19
+
20
+ function fullState(tool: RSToolAgent, sessionId?: string) {
21
+ return tool.getSessionState('full', sessionId) as import('./session').SessionState;
15
22
  }
16
23
 
17
24
  describe('RSToolAgent', () => {
@@ -26,10 +33,7 @@ describe('RSToolAgent', () => {
26
33
  it('analyzes a valid expression', () => {
27
34
  const tool = new RSToolAgent();
28
35
  const session = tool.createSession();
29
- const analysis = tool.analyzeExpression(session.sessionId, {
30
- expression: '1+2',
31
- cstType: CstType.TERM
32
- });
36
+ const analysis = tool.analyzeExpression({ expression: '1+2', cstType: CstType.TERM }, session.sessionId);
33
37
  expect(analysis.success).toBe(true);
34
38
  expect(analysis.diagnostics.length).toBe(0);
35
39
  });
@@ -37,10 +41,7 @@ describe('RSToolAgent', () => {
37
41
  it('returns syntax diagnostics for invalid expression', () => {
38
42
  const tool = new RSToolAgent();
39
43
  const session = tool.createSession();
40
- const analysis = tool.analyzeExpression(session.sessionId, {
41
- expression: '(',
42
- cstType: CstType.TERM
43
- });
44
+ const analysis = tool.analyzeExpression({ expression: '(', cstType: CstType.TERM }, session.sessionId);
44
45
  expect(analysis.success).toBe(false);
45
46
  expect(analysis.diagnostics.length).toBeGreaterThan(0);
46
47
  });
@@ -48,68 +49,64 @@ describe('RSToolAgent', () => {
48
49
  it('rejects formal definition for constants', () => {
49
50
  const tool = new RSToolAgent();
50
51
  const session = tool.createSession();
51
- const result = tool.addOrUpdateConstituenta(session.sessionId, {
52
- draft: {
53
- id: 11,
54
- alias: 'C1',
55
- cstType: CstType.CONSTANT,
56
- definitionFormal: 'X1'
57
- }
58
- });
59
- expect(result.state.analysis.success).toBe(false);
60
- expect(result.diagnostics[0]?.error.code).toBe(RSErrorCode.definitionNotAllowed);
52
+ const result = tool.applySchemaPatch(
53
+ {
54
+ items: [{ alias: 'C1', cstType: CstType.CONSTANT, definitionFormal: 'X1' }]
55
+ },
56
+ session.sessionId
57
+ );
58
+ expect(result.success).toBe(false);
59
+ expect(result.failed[0]?.diagnostics[0]?.error.code).toBe(RSErrorCode.definitionNotAllowed);
61
60
  });
62
61
 
63
62
  it('rejects formal definition for basic sets', () => {
64
63
  const tool = new RSToolAgent();
65
64
  const session = tool.createSession();
66
- const result = tool.addOrUpdateConstituenta(session.sessionId, {
67
- draft: {
68
- id: 12,
69
- alias: 'X1',
70
- cstType: CstType.BASE,
71
- definitionFormal: 'Z'
72
- }
73
- });
74
- expect(result.state.analysis.success).toBe(false);
75
- expect(result.diagnostics[0]?.error.code).toBe(RSErrorCode.definitionNotAllowed);
65
+ const result = tool.applySchemaPatch(
66
+ {
67
+ items: [{ alias: 'X1', cstType: CstType.BASE, definitionFormal: 'Z' }]
68
+ },
69
+ session.sessionId
70
+ );
71
+ expect(result.success).toBe(false);
72
+ expect(result.failed[0]?.diagnostics[0]?.error.code).toBe(RSErrorCode.definitionNotAllowed);
76
73
  });
77
74
 
78
75
  it('returns known analysis for empty base definition', () => {
79
76
  const tool = new RSToolAgent();
80
77
  const session = tool.createSession();
81
- const result = tool.addOrUpdateConstituenta(session.sessionId, {
82
- draft: {
83
- id: 13,
84
- alias: 'X1',
85
- cstType: CstType.BASE,
86
- definitionFormal: ''
87
- }
88
- });
89
- expect(result.state.analysis.success).toBe(true);
90
- expect(result.state.analysis.type).not.toBeNull();
91
- expect(result.state.analysis.valueClass).toBe('value');
78
+ const result = tool.applySchemaPatch(
79
+ {
80
+ items: [{ alias: 'X1', cstType: CstType.BASE, definitionFormal: '' }]
81
+ },
82
+ session.sessionId
83
+ );
84
+ expect(result.success).toBe(true);
85
+ const state = fullState(tool, session.sessionId);
86
+ expect(state.items[0]?.analysis.type).not.toBeNull();
87
+ expect(state.items[0]?.analysis.valueClass).toBe('value');
92
88
  });
93
89
 
94
90
  it('persists term, definitionText, and convention in session state', () => {
95
91
  const tool = new RSToolAgent();
96
92
  const session = tool.createSession();
97
- const result = tool.addOrUpdateConstituenta(session.sessionId, {
98
- draft: {
99
- id: 15,
100
- alias: 'D2',
101
- cstType: CstType.TERM,
102
- definitionFormal: '1',
103
- term: 'natural number',
104
- definitionText: 'A positive integer',
105
- convention: 'Standard arithmetic'
106
- }
107
- });
108
- expect(result.state.term).toBe('natural number');
109
- expect(result.state.definitionText).toBe('A positive integer');
110
- expect(result.state.convention).toBe('Standard arithmetic');
111
-
112
- const form = tool.getFormState(session.sessionId);
93
+ tool.applySchemaPatch(
94
+ {
95
+ items: [
96
+ {
97
+ alias: 'D2',
98
+ cstType: CstType.TERM,
99
+ definitionFormal: '1',
100
+ term: 'natural number',
101
+ definitionText: 'A positive integer',
102
+ convention: 'Standard arithmetic'
103
+ }
104
+ ]
105
+ },
106
+ session.sessionId
107
+ );
108
+
109
+ const form = fullState(tool, session.sessionId);
113
110
  expect(form.items[0]).toMatchObject({
114
111
  term: 'natural number',
115
112
  definitionText: 'A positive integer',
@@ -117,8 +114,8 @@ describe('RSToolAgent', () => {
117
114
  });
118
115
 
119
116
  const exported = tool.exportSession(session.sessionId);
120
- const imported = tool.importSession(exported);
121
- const restored = tool.getFormState(imported.sessionId);
117
+ const imported = tool.importData(exported, 'session');
118
+ const restored = fullState(tool, imported.sessionId);
122
119
  expect(restored.items[0]).toMatchObject({
123
120
  term: 'natural number',
124
121
  definitionText: 'A positive integer',
@@ -134,13 +131,13 @@ describe('RSToolAgent', () => {
134
131
  comment: 'Example schema'
135
132
  });
136
133
 
137
- expect(tool.getFormState(session.sessionId)).toMatchObject({
134
+ expect(fullState(tool, session.sessionId)).toMatchObject({
138
135
  alias: 'KIN',
139
136
  title: 'Kinship',
140
137
  comment: 'Example schema'
141
138
  });
142
139
 
143
- const exported = JSON.parse(tool.exportPortalSchema(session.sessionId)) as {
140
+ const exported = JSON.parse(tool.exportPortal({ kind: 'schema' }, session.sessionId) as string) as {
144
141
  title: string;
145
142
  alias: string;
146
143
  description: string;
@@ -155,19 +152,23 @@ describe('RSToolAgent', () => {
155
152
  it('exports schema data for Portal JSON import', () => {
156
153
  const tool = new RSToolAgent();
157
154
  const session = tool.createSession();
158
- tool.addOrUpdateConstituenta(session.sessionId, {
159
- draft: {
160
- id: 15,
161
- alias: 'D2',
162
- cstType: CstType.TERM,
163
- definitionFormal: '1',
164
- term: 'natural number',
165
- definitionText: 'A positive integer',
166
- convention: 'Standard arithmetic'
167
- }
168
- });
169
-
170
- const exported = JSON.parse(tool.exportPortalSchema(session.sessionId)) as {
155
+ tool.applySchemaPatch(
156
+ {
157
+ items: [
158
+ {
159
+ alias: 'D2',
160
+ cstType: CstType.TERM,
161
+ definitionFormal: '1',
162
+ term: 'natural number',
163
+ definitionText: 'A positive integer',
164
+ convention: 'Standard arithmetic'
165
+ }
166
+ ]
167
+ },
168
+ session.sessionId
169
+ );
170
+
171
+ const exported = JSON.parse(tool.exportPortal({ kind: 'schema' }, session.sessionId) as string) as {
171
172
  contract_version: string;
172
173
  title: string;
173
174
  alias: string;
@@ -182,7 +183,6 @@ describe('RSToolAgent', () => {
182
183
  expect(exported.description).toBe('');
183
184
 
184
185
  expect(exported.items[0]).toMatchObject({
185
- id: 15,
186
186
  alias: 'D2',
187
187
  cst_type: CstType.TERM,
188
188
  definition_formal: '1',
@@ -198,12 +198,9 @@ describe('RSToolAgent', () => {
198
198
  const tool = new RSToolAgent();
199
199
  const session = tool.createSession();
200
200
  buildSampleForm(tool, session.sessionId);
201
- await tool.setConstituentaValue(session.sessionId, {
202
- target: 1,
203
- value: { 1: 'Alice' }
204
- });
201
+ await tool.setModelValues({ set: [{ target: 1, value: { 1: 'Alice' } }] }, session.sessionId);
205
202
 
206
- const exported = JSON.parse(tool.exportPortalModel(session.sessionId)) as {
203
+ const exported = JSON.parse(tool.exportPortal({ kind: 'model' }, session.sessionId) as string) as {
207
204
  contract_version: string;
208
205
  title: string;
209
206
  alias: string;
@@ -227,33 +224,31 @@ describe('RSToolAgent', () => {
227
224
  it('defaults missing text fields to empty strings', () => {
228
225
  const tool = new RSToolAgent();
229
226
  const session = tool.createSession();
230
- const result = tool.addOrUpdateConstituenta(session.sessionId, {
231
- draft: {
232
- id: 16,
233
- alias: 'D3',
234
- cstType: CstType.TERM,
235
- definitionFormal: '2'
236
- }
237
- });
238
- expect(result.state.term).toBe('');
239
- expect(result.state.definitionText).toBe('');
240
- expect(result.state.convention).toBe('');
227
+ tool.applySchemaPatch(
228
+ {
229
+ items: [{ alias: 'D3', cstType: CstType.TERM, definitionFormal: '2' }]
230
+ },
231
+ session.sessionId
232
+ );
233
+ const state = fullState(tool, session.sessionId);
234
+ expect(state.items[0]?.term).toBe('');
235
+ expect(state.items[0]?.definitionText).toBe('');
236
+ expect(state.items[0]?.convention).toBe('');
241
237
  });
242
238
 
243
239
  it('returns known analysis for empty constant definition', () => {
244
240
  const tool = new RSToolAgent();
245
241
  const session = tool.createSession();
246
- const result = tool.addOrUpdateConstituenta(session.sessionId, {
247
- draft: {
248
- id: 14,
249
- alias: 'C1',
250
- cstType: CstType.CONSTANT,
251
- definitionFormal: ''
252
- }
253
- });
254
- expect(result.state.analysis.success).toBe(true);
255
- expect(result.state.analysis.type).not.toBeNull();
256
- expect(result.state.analysis.valueClass).toBe('value');
242
+ const result = tool.applySchemaPatch(
243
+ {
244
+ items: [{ alias: 'C1', cstType: CstType.CONSTANT, definitionFormal: '' }]
245
+ },
246
+ session.sessionId
247
+ );
248
+ expect(result.success).toBe(true);
249
+ const state = fullState(tool, session.sessionId);
250
+ expect(state.items[0]?.analysis.type).not.toBeNull();
251
+ expect(state.items[0]?.analysis.valueClass).toBe('value');
257
252
  });
258
253
  });
259
254
 
@@ -270,10 +265,10 @@ describe('RSToolAgent modeling and evaluation', () => {
270
265
  const session = tool.createSession();
271
266
  buildSampleForm(tool, session.sessionId);
272
267
 
273
- const model = await tool.setConstituentaValue(session.sessionId, {
274
- target: 1,
275
- value: { 0: 'zero', 1: 'one' }
276
- });
268
+ const model = await tool.setModelValues(
269
+ { set: [{ target: 1, value: { 0: 'zero', 1: 'one' } }] },
270
+ session.sessionId
271
+ );
277
272
  expect(model.items).toHaveLength(1);
278
273
  expect(model.items[0]).toMatchObject({
279
274
  id: 1,
@@ -287,12 +282,40 @@ describe('RSToolAgent modeling and evaluation', () => {
287
282
  const session = tool.createSession();
288
283
  buildSampleForm(tool, session.sessionId);
289
284
 
285
+ await expect(tool.setModelValues({ set: [{ target: 2, value: 3 }] }, session.sessionId)).rejects.toThrow(
286
+ /inferrable/
287
+ );
288
+ });
289
+
290
+ it('restores session state when setModelValues fails mid-batch', async () => {
291
+ const tool = new RSToolAgent();
292
+ const session = tool.createSession();
293
+ buildSampleForm(tool, session.sessionId);
294
+ await tool.setModelValues({ set: [{ target: 1, value: { 0: 'a' } }] }, session.sessionId);
295
+
290
296
  await expect(
291
- tool.setConstituentaValue(session.sessionId, {
292
- target: 2,
293
- value: 3
294
- })
295
- ).rejects.toThrow(/inferrable/);
297
+ tool.setModelValues(
298
+ {
299
+ set: [
300
+ { target: 1, value: { 0: 'b' } },
301
+ { target: 999, value: { 0: 'x' } }
302
+ ]
303
+ },
304
+ session.sessionId
305
+ )
306
+ ).rejects.toThrow(/Unknown constituent/);
307
+
308
+ expect(tool.getModelState(session.sessionId).items).toEqual([
309
+ expect.objectContaining({ id: 1, value: { 0: 'a' } })
310
+ ]);
311
+
312
+ await expect(
313
+ tool.setModelValues({ clear: [1], set: [{ target: 999, value: { 0: 'x' } }] }, session.sessionId)
314
+ ).rejects.toThrow(/Unknown constituent/);
315
+
316
+ expect(tool.getModelState(session.sessionId).items).toEqual([
317
+ expect.objectContaining({ id: 1, value: { 0: 'a' } })
318
+ ]);
296
319
  });
297
320
 
298
321
  it('evaluates expression against session context', () => {
@@ -300,10 +323,7 @@ describe('RSToolAgent modeling and evaluation', () => {
300
323
  const session = tool.createSession();
301
324
  buildSampleForm(tool, session.sessionId);
302
325
 
303
- const result = tool.evaluateExpression(session.sessionId, {
304
- expression: '1+2',
305
- cstType: CstType.TERM
306
- });
326
+ const result = tool.evaluate({ expression: '1+2', cstType: CstType.TERM }, session.sessionId);
307
327
  expect(result.success).toBe(true);
308
328
  expect(result.value).toBe(3);
309
329
  expect(result.status).toBe(EvalStatus.HAS_DATA);
@@ -315,7 +335,7 @@ describe('RSToolAgent modeling and evaluation', () => {
315
335
  const session = tool.createSession();
316
336
  buildSampleForm(tool, session.sessionId);
317
337
 
318
- const result = tool.evaluateConstituenta(session.sessionId, { constituentId: 2 });
338
+ const result = tool.evaluate({ constituentId: 2 }, session.sessionId);
319
339
  expect(result.success).toBe(true);
320
340
  expect(result.value).toBe(3);
321
341
  expect(result.status).toBe(EvalStatus.HAS_DATA);
@@ -326,7 +346,7 @@ describe('RSToolAgent modeling and evaluation', () => {
326
346
  const session = tool.createSession();
327
347
  buildSampleForm(tool, session.sessionId);
328
348
 
329
- const result = tool.evaluateConstituenta(session.sessionId, { constituentId: 3 });
349
+ const result = tool.evaluate({ constituentId: 3 }, session.sessionId);
330
350
  expect(result.success).toBe(true);
331
351
  expect(result.value).toBe(1);
332
352
  });
@@ -349,31 +369,31 @@ describe('RSToolAgent modeling and evaluation', () => {
349
369
  const tool = new RSToolAgent();
350
370
  const session = tool.createSession();
351
371
  buildSampleForm(tool, session.sessionId);
352
- await tool.setConstituentaValue(session.sessionId, {
353
- target: 1,
354
- value: { 0: 'a' }
355
- });
372
+ await tool.setModelValues({ set: [{ target: 1, value: { 0: 'a' } }] }, session.sessionId);
356
373
 
357
- const model = await tool.clearConstituentaValues(session.sessionId, { items: [1] });
374
+ const model = await tool.setModelValues({ clear: [1] }, session.sessionId);
358
375
  expect(model.items).toHaveLength(0);
359
376
  });
360
377
 
361
378
  it('batch sets model values', async () => {
362
379
  const tool = new RSToolAgent();
363
380
  const session = tool.createSession();
364
- tool.addOrUpdateConstituenta(session.sessionId, {
365
- draft: { id: 1, alias: 'X1', cstType: CstType.BASE, definitionFormal: '' }
366
- });
367
- tool.addOrUpdateConstituenta(session.sessionId, {
368
- draft: { id: 2, alias: 'C1', cstType: CstType.CONSTANT, definitionFormal: '' }
369
- });
381
+ tool.applySchemaPatch(
382
+ {
383
+ items: [{ alias: 'X1' }, { alias: 'C1', cstType: CstType.CONSTANT, definitionFormal: '' }]
384
+ },
385
+ session.sessionId
386
+ );
370
387
 
371
- const model = await tool.setConstituentaValues(session.sessionId, {
372
- items: [
373
- { target: 1, value: { 0: 'a', 1: 'b' } },
374
- { target: 2, value: { 0: 'c' } }
375
- ]
376
- });
388
+ const model = await tool.setModelValues(
389
+ {
390
+ set: [
391
+ { target: 1, value: { 0: 'a', 1: 'b' } },
392
+ { target: 2, value: { 0: 'c' } }
393
+ ]
394
+ },
395
+ session.sessionId
396
+ );
377
397
  expect(model.items).toHaveLength(2);
378
398
  });
379
399
 
@@ -381,18 +401,550 @@ describe('RSToolAgent modeling and evaluation', () => {
381
401
  const tool = new RSToolAgent();
382
402
  const session = tool.createSession();
383
403
  buildSampleForm(tool, session.sessionId);
384
- await tool.setConstituentaValue(session.sessionId, {
385
- target: 1,
386
- value: { 0: 'zero' }
387
- });
404
+ await tool.setModelValues({ set: [{ target: 1, value: { 0: 'zero' } }] }, session.sessionId);
388
405
 
389
406
  const exported = tool.exportSession(session.sessionId);
390
407
  expect(exported).toContain('"model"');
391
408
 
392
409
  const newTool = new RSToolAgent();
393
- const imported = newTool.importSession(exported);
410
+ const imported = newTool.importData(exported, 'session');
394
411
  const model = newTool.getModelState(imported.sessionId);
395
412
  expect(model.items).toHaveLength(1);
396
413
  expect(model.items[0]?.value).toEqual({ 0: 'zero' });
397
414
  });
398
415
  });
416
+
417
+ describe('RSToolAgent agent ergonomics', () => {
418
+ it('tracks current session and allows omitting sessionId', () => {
419
+ const tool = new RSToolAgent();
420
+ const session = tool.createSession({ title: 'Active' });
421
+ expect(tool.getCurrentSession()?.sessionId).toBe(session.sessionId);
422
+ expect(fullState(tool).title).toBe('Active');
423
+ });
424
+
425
+ it('auto-creates a session when sessionId is omitted', () => {
426
+ const tool = new RSToolAgent();
427
+ const result = tool.applySchemaPatch({
428
+ items: [{ alias: 'X1' }]
429
+ });
430
+
431
+ expect(result.success).toBe(true);
432
+ expect(tool.getCurrentSession()).not.toBeNull();
433
+ expect(fullState(tool).items).toHaveLength(1);
434
+ });
435
+
436
+ it('applies agent schema patches with inferred ids and cstType', () => {
437
+ const tool = new RSToolAgent();
438
+
439
+ const result = tool.applySchemaPatch({
440
+ initial: { title: 'Agent patch' },
441
+ commitMessage: 'initial schema',
442
+ items: [
443
+ { alias: 'D1', definitionFormal: 'Pr1(S1)' },
444
+ { alias: 'X1' },
445
+ { alias: 'S1', definitionFormal: 'ℬ(X1×X1)' }
446
+ ]
447
+ });
448
+
449
+ expect(result.success).toBe(true);
450
+ expect(result.summary.title).toBe('Agent patch');
451
+ expect(result.summary.itemCount).toBe(3);
452
+ expect(result.revision?.message).toBe('initial schema');
453
+ expect(fullState(tool).items.map(item => item.alias)).toEqual(['D1', 'X1', 'S1']);
454
+ expect(fullState(tool).items.map(item => item.cstType)).toEqual([CstType.TERM, CstType.BASE, CstType.STRUCTURED]);
455
+ });
456
+
457
+ it('exports Portal payloads as structured objects', () => {
458
+ const tool = new RSToolAgent();
459
+ tool.applySchemaPatch({
460
+ items: [{ alias: 'D1', definitionFormal: '1+2' }]
461
+ });
462
+
463
+ const schema = tool.exportPortal({ kind: 'schema', format: 'object' });
464
+ expect(schema).toMatchObject({ items: [{ alias: 'D1', cst_type: CstType.TERM }] });
465
+ expect(JSON.parse(tool.exportPortal({ kind: 'schema' }) as string).items[0]).toMatchObject(
466
+ (schema as { items: unknown[] }).items[0] as object
467
+ );
468
+ });
469
+
470
+ it('replaces active diagnostics per constituent on upsert', () => {
471
+ const tool = new RSToolAgent();
472
+ const session = tool.createSession();
473
+ tool.applySchemaPatch(
474
+ { mode: 'best_effort', items: [{ alias: 'X1', cstType: CstType.BASE, definitionFormal: 'Z' }] },
475
+ session.sessionId
476
+ );
477
+ expect(tool.listDiagnostics(undefined, session.sessionId)).toHaveLength(1);
478
+
479
+ tool.applySchemaPatch({ items: [{ alias: 'X1', cstType: CstType.BASE, definitionFormal: '' }] }, session.sessionId);
480
+ expect(tool.listDiagnostics(undefined, session.sessionId)).toHaveLength(0);
481
+ });
482
+
483
+ it('does not record analyzeExpression diagnostics by default', () => {
484
+ const tool = new RSToolAgent();
485
+ const session = tool.createSession();
486
+ tool.analyzeExpression({ expression: '(', cstType: CstType.TERM }, session.sessionId);
487
+ expect(tool.listDiagnostics(undefined, session.sessionId)).toHaveLength(0);
488
+ });
489
+
490
+ it('records analyzeExpression diagnostics when requested', () => {
491
+ const tool = new RSToolAgent();
492
+ const session = tool.createSession();
493
+ tool.analyzeExpression({ expression: '(', cstType: CstType.TERM, recordDiagnostics: true }, session.sessionId);
494
+ expect(tool.listDiagnostics(undefined, session.sessionId).length).toBeGreaterThan(0);
495
+ });
496
+
497
+ it('applySchemaPatch rolls back in atomic mode', () => {
498
+ const tool = new RSToolAgent();
499
+ const session = tool.createSession();
500
+ tool.applySchemaPatch({ items: [{ alias: 'X1' }] }, session.sessionId);
501
+
502
+ const result = tool.applySchemaPatch(
503
+ {
504
+ mode: 'atomic',
505
+ items: [
506
+ { alias: 'D1', definitionFormal: '1+2' },
507
+ { alias: 'D2', definitionFormal: 'Pr1(MISSING)' }
508
+ ]
509
+ },
510
+ session.sessionId
511
+ );
512
+
513
+ expect(result.success).toBe(false);
514
+ expect(result.applied).toHaveLength(0);
515
+ expect(fullState(tool, session.sessionId).items).toHaveLength(1);
516
+ });
517
+
518
+ it('applySchemaPatch applies valid drafts in best_effort mode', () => {
519
+ const tool = new RSToolAgent();
520
+ const session = tool.createSession();
521
+ tool.applySchemaPatch(
522
+ {
523
+ items: [{ alias: 'X1' }, { alias: 'S1', definitionFormal: 'ℬ(X1×X1)' }]
524
+ },
525
+ session.sessionId
526
+ );
527
+
528
+ const result = tool.applySchemaPatch(
529
+ {
530
+ mode: 'best_effort',
531
+ items: [
532
+ { alias: 'D1', definitionFormal: 'Pr1(S1)' },
533
+ { alias: 'D2', definitionFormal: 'Pr1(MISSING)' }
534
+ ]
535
+ },
536
+ session.sessionId
537
+ );
538
+
539
+ expect(result.applied).toHaveLength(1);
540
+ expect(result.failed).toHaveLength(1);
541
+ expect(fullState(tool, session.sessionId).items.map(item => item.alias)).toContain('D1');
542
+ });
543
+
544
+ it('imports portal details JSON', () => {
545
+ const tool = new RSToolAgent();
546
+ const payload = {
547
+ title: 'Kinship',
548
+ alias: 'KIN',
549
+ description: 'Example',
550
+ items: [
551
+ { id: 1, alias: 'X1', cst_type: CstType.BASE, definition_formal: '' },
552
+ { id: 2, alias: 'D1', cst_type: CstType.TERM, definition_formal: '1+2' }
553
+ ]
554
+ };
555
+ const session = tool.importData(payload, 'portal-details');
556
+ const state = fullState(tool, session.sessionId);
557
+ expect(state.title).toBe('Kinship');
558
+ expect(state.items).toHaveLength(2);
559
+ expect(state.items.map(item => item.alias).sort()).toEqual(['D1', 'X1']);
560
+ });
561
+
562
+ it('auto-detects portal details import kind', () => {
563
+ const tool = new RSToolAgent();
564
+ const session = tool.importData({
565
+ title: 'Auto',
566
+ items: [{ id: 1, alias: 'X1', cst_type: CstType.BASE, definition_formal: '' }]
567
+ });
568
+ expect(fullState(tool, session.sessionId).title).toBe('Auto');
569
+ });
570
+
571
+ it('persists sessions across agent restarts', () => {
572
+ const dir = mkdtempSync(join(tmpdir(), 'rstool-test-sessions-'));
573
+ try {
574
+ const tool = new RSToolAgent({ persistenceDir: dir });
575
+ const session = tool.createSession({ title: 'Persisted' });
576
+ tool.applySchemaPatch({ items: [{ alias: 'X1' }] }, session.sessionId);
577
+
578
+ const restored = new RSToolAgent({ persistenceDir: dir });
579
+ expect(restored.getCurrentSession()?.sessionId).toBe(session.sessionId);
580
+ expect(fullState(restored, session.sessionId).items).toHaveLength(1);
581
+ } finally {
582
+ rmSync(dir, { recursive: true, force: true });
583
+ }
584
+ });
585
+ });
586
+
587
+ describe('RSToolAgent session management', () => {
588
+ it('ensureSession reuses the current session', () => {
589
+ const tool = new RSToolAgent();
590
+ const session = tool.createSession({ title: 'First' });
591
+ const ensured = tool.ensureSession({ title: 'Ignored' });
592
+ expect(ensured.sessionId).toBe(session.sessionId);
593
+ expect(fullState(tool).title).toBe('First');
594
+ });
595
+
596
+ it('ensureSession creates a session when none is active', () => {
597
+ const tool = new RSToolAgent();
598
+ expect(tool.getCurrentSession()).toBeNull();
599
+ const ensured = tool.ensureSession({ title: 'Created' });
600
+ expect(ensured.sessionId).toBeTruthy();
601
+ expect(fullState(tool).title).toBe('Created');
602
+ });
603
+
604
+ it('switches current session with setCurrentSession', () => {
605
+ const tool = new RSToolAgent();
606
+ const first = tool.createSession({ title: 'First' });
607
+ const second = tool.createSession({ title: 'Second' });
608
+ tool.setCurrentSession(first.sessionId);
609
+ expect(fullState(tool).title).toBe('First');
610
+ tool.setCurrentSession(second.sessionId);
611
+ expect(fullState(tool).title).toBe('Second');
612
+ });
613
+
614
+ it('throws when setCurrentSession receives an unknown id', () => {
615
+ const tool = new RSToolAgent();
616
+ const unknownId = randomUUID();
617
+ expect(() => tool.setCurrentSession(unknownId)).toThrow(/Unknown session/);
618
+ });
619
+
620
+ it('throws when an explicit unknown sessionId is passed to API methods', () => {
621
+ const tool = new RSToolAgent();
622
+ const unknownId = randomUUID();
623
+ expect(() => tool.getSessionState('full', unknownId)).toThrow(/Unknown session/);
624
+ });
625
+
626
+ it('applySchemaPatch with explicit sessionId does not switch current session', () => {
627
+ const tool = new RSToolAgent();
628
+ const first = tool.createSession({ title: 'First' });
629
+ const second = tool.createSession({ title: 'Second' });
630
+ tool.setCurrentSession(first.sessionId);
631
+
632
+ tool.applySchemaPatch({ items: [{ alias: 'X1' }] }, second.sessionId);
633
+
634
+ expect(tool.getCurrentSession()?.sessionId).toBe(first.sessionId);
635
+ expect(tool.getSessionState('summary', second.sessionId)).toMatchObject({ itemCount: 1 });
636
+ expect(tool.getSessionState('summary', first.sessionId)).toMatchObject({ itemCount: 0 });
637
+ });
638
+
639
+ it('returns summary session state by default', () => {
640
+ const tool = new RSToolAgent();
641
+ const session = tool.createSession({ title: 'Summary', alias: 'SUM' });
642
+ tool.applySchemaPatch({ items: [{ alias: 'X1' }] }, session.sessionId);
643
+
644
+ const summary = tool.getSessionState('summary', session.sessionId);
645
+ expect(summary).toMatchObject({
646
+ sessionId: session.sessionId,
647
+ title: 'Summary',
648
+ alias: 'SUM',
649
+ itemCount: 1,
650
+ modelItemCount: 0
651
+ });
652
+ expect('items' in summary && summary.items[0]).toMatchObject({
653
+ alias: 'X1',
654
+ analysisSuccess: true
655
+ });
656
+ });
657
+
658
+ it('records revisions via commitStep', () => {
659
+ const tool = new RSToolAgent();
660
+ const session = tool.createSession();
661
+ tool.applySchemaPatch({ items: [{ alias: 'X1' }] }, session.sessionId);
662
+ const revision = tool.commitStep('checkpoint', session.sessionId);
663
+ expect(revision.message).toBe('checkpoint');
664
+ expect(fullState(tool, session.sessionId).revisions).toHaveLength(1);
665
+ });
666
+
667
+ it('filters diagnostics by constituent id', () => {
668
+ const tool = new RSToolAgent();
669
+ const session = tool.createSession();
670
+ tool.applySchemaPatch(
671
+ {
672
+ mode: 'best_effort',
673
+ items: [
674
+ { alias: 'X1', cstType: CstType.BASE, definitionFormal: 'Z' },
675
+ { alias: 'X2', cstType: CstType.BASE, definitionFormal: 'Z' }
676
+ ]
677
+ },
678
+ session.sessionId
679
+ );
680
+ const all = tool.listDiagnostics(undefined, session.sessionId);
681
+ const forX1 = tool.listDiagnostics({ constituentId: 1 }, session.sessionId);
682
+ expect(all.length).toBeGreaterThanOrEqual(2);
683
+ expect(forX1.every(record => record.constituentId === 1)).toBe(true);
684
+ });
685
+
686
+ it('filters diagnostics by constituent id 0', () => {
687
+ const tool = new RSToolAgent();
688
+ const session = tool.createSession();
689
+ tool.applySchemaPatch(
690
+ {
691
+ mode: 'best_effort',
692
+ items: [
693
+ { id: 0, alias: 'X0', cstType: CstType.BASE, definitionFormal: 'Z' },
694
+ { alias: 'X1', cstType: CstType.BASE, definitionFormal: 'Z' }
695
+ ]
696
+ },
697
+ session.sessionId
698
+ );
699
+ const all = tool.listDiagnostics(undefined, session.sessionId);
700
+ const forX0 = tool.listDiagnostics({ constituentId: 0 }, session.sessionId);
701
+ expect(all.length).toBeGreaterThanOrEqual(2);
702
+ expect(forX0.length).toBeGreaterThan(0);
703
+ expect(forX0.length).toBeLessThan(all.length);
704
+ expect(forX0.every(record => record.constituentId === 0)).toBe(true);
705
+ });
706
+
707
+ it('applySchemaPatch avoids id collisions when explicit ids precede auto-assigned ones', () => {
708
+ const tool = new RSToolAgent();
709
+ const session = tool.createSession();
710
+ tool.applySchemaPatch(
711
+ {
712
+ items: [
713
+ { id: 1, alias: 'X1', cstType: CstType.BASE, definitionFormal: '' },
714
+ { alias: 'X2', cstType: CstType.BASE, definitionFormal: '' }
715
+ ]
716
+ },
717
+ session.sessionId
718
+ );
719
+
720
+ const ids = fullState(tool, session.sessionId).items.map(item => item.id);
721
+ expect(ids).toEqual([1, 2]);
722
+ });
723
+
724
+ it('loads a persisted session from disk via setCurrentSession', () => {
725
+ const dir = mkdtempSync(join(tmpdir(), 'rstool-test-sessions-'));
726
+ try {
727
+ const writer = new RSToolAgent({ persistenceDir: dir });
728
+ const session = writer.createSession({ title: 'On disk' });
729
+ writer.applySchemaPatch({ items: [{ alias: 'X1' }] }, session.sessionId);
730
+
731
+ const reader = new RSToolAgent({ persistenceDir: dir });
732
+ reader.setCurrentSession(session.sessionId);
733
+ expect(fullState(reader, session.sessionId).title).toBe('On disk');
734
+ } finally {
735
+ rmSync(dir, { recursive: true, force: true });
736
+ }
737
+ });
738
+
739
+ it('clears current session when persisted file is missing', () => {
740
+ const dir = mkdtempSync(join(tmpdir(), 'rstool-test-sessions-'));
741
+ try {
742
+ const writer = new RSToolAgent({ persistenceDir: dir });
743
+ const session = writer.createSession({ title: 'Gone' });
744
+
745
+ unlinkSync(join(dir, `${session.sessionId}.json`));
746
+
747
+ const reader = new RSToolAgent({ persistenceDir: dir });
748
+ expect(reader.getCurrentSession()).toBeNull();
749
+ } finally {
750
+ rmSync(dir, { recursive: true, force: true });
751
+ }
752
+ });
753
+ });
754
+
755
+ describe('RSToolAgent import and export', () => {
756
+ it('imports portal schema JSON', () => {
757
+ const tool = new RSToolAgent();
758
+ const payload = {
759
+ contract_version: '1.0.0',
760
+ title: 'Imported schema',
761
+ alias: 'IMP',
762
+ description: 'From portal',
763
+ items: [
764
+ { id: 1, alias: 'X1', cst_type: CstType.BASE, definition_formal: '' },
765
+ { id: 2, alias: 'D1', cst_type: CstType.TERM, definition_formal: '1+2' }
766
+ ],
767
+ attribution: []
768
+ };
769
+ const session = tool.importData(payload, 'portal-schema');
770
+ const state = fullState(tool, session.sessionId);
771
+ expect(state).toMatchObject({ title: 'Imported schema', alias: 'IMP', comment: 'From portal' });
772
+ expect(state.items.map(item => item.alias)).toEqual(['X1', 'D1']);
773
+ });
774
+
775
+ it('keeps declaration order in portal schema export', () => {
776
+ const tool = new RSToolAgent();
777
+ const session = tool.createSession({ title: 'Order', alias: 'ORD' });
778
+ tool.applySchemaPatch(
779
+ {
780
+ items: [
781
+ { alias: 'D1', cstType: CstType.TERM, definitionFormal: 'Pr1(S1)' },
782
+ { alias: 'S1', cstType: CstType.STRUCTURED, definitionFormal: 'ℬ(X1×X1)' },
783
+ { alias: 'C1', cstType: CstType.CONSTANT, definitionFormal: '' },
784
+ { alias: 'X1', cstType: CstType.BASE, definitionFormal: '' }
785
+ ]
786
+ },
787
+ session.sessionId
788
+ );
789
+
790
+ const exported = tool.exportPortal({ kind: 'schema', format: 'object' }, session.sessionId) as {
791
+ items: Array<{ alias: string }>;
792
+ };
793
+ expect(exported.items.map(item => item.alias)).toEqual(['D1', 'S1', 'C1', 'X1']);
794
+ });
795
+
796
+ it('round-trips portal schema export and import', () => {
797
+ const tool = new RSToolAgent();
798
+ const session = tool.createSession({ title: 'Round trip', alias: 'RT' });
799
+ tool.applySchemaPatch(
800
+ {
801
+ items: [{ alias: 'D1', cstType: CstType.TERM, definitionFormal: '1+2', term: 'sum' }]
802
+ },
803
+ session.sessionId
804
+ );
805
+
806
+ const exported = tool.exportPortal({ kind: 'schema', format: 'object' });
807
+ const imported = tool.importData(exported, 'portal-schema');
808
+ const state = fullState(tool, imported.sessionId);
809
+ expect(state.title).toBe('Round trip');
810
+ expect(state.items[0]).toMatchObject({
811
+ alias: 'D1',
812
+ definitionFormal: '1+2',
813
+ term: 'sum'
814
+ });
815
+ });
816
+
817
+ it('rejects invalid session export payloads', () => {
818
+ const tool = new RSToolAgent();
819
+ expect(() => tool.importData({ contractVersion: '2.0.0' }, 'session')).toThrow(/Invalid session export/);
820
+ });
821
+
822
+ it('rejects undetectable import payloads', () => {
823
+ const tool = new RSToolAgent();
824
+ expect(() => tool.importData({ title: 'orphan' })).toThrow(/Cannot detect import kind/);
825
+ });
826
+
827
+ it('infers cstType from N and T alias prefixes', () => {
828
+ const tool = new RSToolAgent();
829
+ const statementResult = tool.applySchemaPatch({ items: [{ alias: 'T1', definitionFormal: '1=1' }] });
830
+ expect(statementResult.success).toBe(true);
831
+ expect(fullState(tool).items[0]?.cstType).toBe(CstType.STATEMENT);
832
+
833
+ const nominalResult = tool.applySchemaPatch({ items: [{ alias: 'N1' }] });
834
+ expect(nominalResult.failed[0]?.draft.cstType).toBe(CstType.NOMINAL);
835
+ });
836
+
837
+ it('rejects aliases with unknown cstType prefix', () => {
838
+ const tool = new RSToolAgent();
839
+ expect(() =>
840
+ tool.applySchemaPatch({
841
+ items: [{ alias: 'Q1', definitionFormal: '1' }]
842
+ })
843
+ ).toThrow(/Cannot infer cstType/);
844
+ });
845
+ });
846
+
847
+ describe('RSToolAgent modeling semantics', () => {
848
+ function buildKinshipScratch(tool: RSToolAgent, sessionId: string) {
849
+ tool.applySchemaPatch(
850
+ {
851
+ items: [
852
+ { alias: 'X1' },
853
+ { alias: 'S1', definitionFormal: 'ℬ(X1×X1)' },
854
+ { alias: 'D1', definitionFormal: 'Pr1(S1)' },
855
+ { alias: 'A1', cstType: CstType.AXIOM, definitionFormal: 'card(X1)≤1' }
856
+ ]
857
+ },
858
+ sessionId
859
+ );
860
+ }
861
+
862
+ it('returns EMPTY when a basic set has no binding', () => {
863
+ const tool = new RSToolAgent();
864
+ const session = tool.createSession();
865
+ tool.applySchemaPatch({ items: [{ alias: 'X1' }] }, session.sessionId);
866
+
867
+ const result = tool.evaluate({ constituentId: 1 }, session.sessionId);
868
+ expect(result.status).toBe(EvalStatus.EMPTY);
869
+ expect(result.value).toBeNull();
870
+ });
871
+
872
+ it('sets structured set values and evaluates projections', async () => {
873
+ const tool = new RSToolAgent();
874
+ const session = tool.createSession();
875
+ buildKinshipScratch(tool, session.sessionId);
876
+
877
+ await tool.setModelValues(
878
+ {
879
+ set: [
880
+ { target: 1, value: { 0: 'ann', 1: 'bob' } },
881
+ {
882
+ target: 2,
883
+ value: [
884
+ [TUPLE_ID, 0, 1],
885
+ [TUPLE_ID, 1, 0]
886
+ ]
887
+ }
888
+ ]
889
+ },
890
+ session.sessionId
891
+ );
892
+
893
+ const result = tool.evaluate({ constituentId: 3 }, session.sessionId);
894
+ expect(result.success).toBe(true);
895
+ expect(result.status).toBe(EvalStatus.HAS_DATA);
896
+ expect(Array.isArray(result.value)).toBe(true);
897
+ expect((result.value as unknown[]).length).toBeGreaterThan(0);
898
+ });
899
+
900
+ it('reports AXIOM_FALSE when model data violates the axiom', async () => {
901
+ const tool = new RSToolAgent();
902
+ const session = tool.createSession();
903
+ buildKinshipScratch(tool, session.sessionId);
904
+
905
+ await tool.setModelValues({ set: [{ target: 1, value: { 0: 'ann', 1: 'bob' } }] }, session.sessionId);
906
+
907
+ const result = tool.evaluate({ constituentId: 4 }, session.sessionId);
908
+ expect(result.status).toBe(EvalStatus.AXIOM_FALSE);
909
+ expect(result.value).toBe(0);
910
+ });
911
+
912
+ it('reports AXIOM_FALSE via recalculateModel', async () => {
913
+ const tool = new RSToolAgent();
914
+ const session = tool.createSession();
915
+ buildKinshipScratch(tool, session.sessionId);
916
+ await tool.setModelValues({ set: [{ target: 1, value: { 0: 'ann', 1: 'bob' } }] }, session.sessionId);
917
+
918
+ const recalculated = tool.recalculateModel(session.sessionId);
919
+ const a1 = recalculated.items.find(item => item.alias === 'A1');
920
+ expect(a1?.status).toBe(EvalStatus.AXIOM_FALSE);
921
+ expect(a1?.value).toBe(0);
922
+ });
923
+
924
+ it('throws when evaluate input is incomplete', () => {
925
+ const tool = new RSToolAgent();
926
+ const session = tool.createSession();
927
+ expect(() => tool.evaluate({}, session.sessionId)).toThrow(/requires constituentId or expression/);
928
+ });
929
+
930
+ it('throws for unknown constituents on evaluate and setModelValues', async () => {
931
+ const tool = new RSToolAgent();
932
+ const session = tool.createSession();
933
+ buildSampleForm(tool, session.sessionId);
934
+
935
+ expect(() => tool.evaluate({ constituentId: 999 }, session.sessionId)).toThrow(/Unknown constituent/);
936
+ await expect(tool.setModelValues({ set: [{ target: 999, value: { 0: 'a' } }] }, session.sessionId)).rejects.toThrow(
937
+ /Unknown constituent/
938
+ );
939
+ });
940
+
941
+ it('rejects invalid basic binding data', async () => {
942
+ const tool = new RSToolAgent();
943
+ const session = tool.createSession();
944
+ tool.applySchemaPatch({ items: [{ alias: 'X1' }] }, session.sessionId);
945
+
946
+ await expect(
947
+ tool.setModelValues({ set: [{ target: 1, value: [1, 2, 3] as never }] }, session.sessionId)
948
+ ).rejects.toThrow(/Invalid basic binding/);
949
+ });
950
+ });