@rsconcept/rstool 1.0.0 → 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.
- package/README.md +22 -4
- package/dist/{agent-workflow-D-PSIb-m.d.ts → agent-workflow-Gk0Vfnv1.d.ts} +3 -9
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -2
- package/dist/models/agent-workflow.d.ts +2 -2
- package/dist/models/index.d.ts +3 -3
- package/dist/models/index.js +1 -1
- package/dist/models/rstool-agent.d.ts +1 -1
- package/dist/models/rstool-agent.js +1 -1
- package/dist/models/tool-contract.d.ts +1 -1
- package/dist/{rstool-agent-kijHA9ML.js → models-Bw6Uum8i.js} +243 -34
- package/dist/models-Bw6Uum8i.js.map +1 -0
- package/dist/{rstool-agent-_8bplZnb.d.ts → rstool-agent-D2cQze_b.d.ts} +3 -3
- package/dist/session/session-store.js +1 -1
- package/dist/{tool-contract-5_Q44DGE.d.ts → tool-contract-0uRGhEfW.d.ts} +2 -2
- package/dist/wrapper/stdio-wrapper.js +16 -14
- package/dist/wrapper/stdio-wrapper.js.map +1 -1
- package/examples/build-chocolate-nim-rsform.ts +2 -56
- package/examples/kinship/build-rsmodel.ts +5 -3
- package/examples/sample/build-rsform.ts +7 -9
- package/examples/sample/rsform-session.json +31 -31
- package/package.json +3 -1
- package/src/mappers/portal-adapter.ts +14 -8
- package/src/models/agent-workflow.ts +1 -13
- package/src/models/import-detect.test.ts +66 -0
- package/src/models/import-detect.ts +6 -3
- package/src/models/portal-json.test.ts +38 -0
- package/src/models/portal-json.ts +11 -2
- package/src/models/rstool-agent.test.ts +402 -3
- package/src/models/rstool-agent.ts +60 -17
- package/src/session/batch-apply.test.ts +103 -8
- package/src/session/batch-apply.ts +35 -0
- package/src/session/persistence.test.ts +63 -0
- package/src/session/persistence.ts +14 -1
- package/src/session/session-store.ts +10 -3
- package/src/wrapper/client.test.ts +58 -0
- package/src/wrapper/stdio-handler.test.ts +101 -0
- package/src/wrapper/stdio-handler.ts +195 -0
- package/src/wrapper/stdio-wrapper.ts +2 -195
- package/dist/rstool-agent-kijHA9ML.js.map +0 -1
- package/dist/session-store-C3jyOSqI.js +0 -142
- package/dist/session-store-C3jyOSqI.js.map +0 -1
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { type CstType } from './common';
|
|
2
|
-
import {
|
|
3
|
-
type ApplyConstituentsMode,
|
|
4
|
-
type ApplyConstituentsResult,
|
|
5
|
-
type ConstituentaDraft,
|
|
6
|
-
type ConstituentaState
|
|
7
|
-
} from './constituenta';
|
|
2
|
+
import { type ApplyConstituentsMode, type ApplyConstituentsResult, type ConstituentaDraft } from './constituenta';
|
|
8
3
|
import { type DiagnosticRecord } from './diagnostic';
|
|
9
4
|
import { type SessionHandle, type SessionRevision, type SessionState } from './session';
|
|
10
5
|
|
|
@@ -69,10 +64,3 @@ export interface ApplySchemaPatchResult extends ApplyConstituentsResult {
|
|
|
69
64
|
summary: SessionSummary;
|
|
70
65
|
revision?: SessionRevision;
|
|
71
66
|
}
|
|
72
|
-
|
|
73
|
-
/** Internal: resolved patch with previous state for undo bookkeeping. */
|
|
74
|
-
export interface ResolvedAgentPatch {
|
|
75
|
-
patch: AgentConstituentaPatch;
|
|
76
|
-
draft: ConstituentaDraft;
|
|
77
|
-
previous?: ConstituentaState;
|
|
78
|
-
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { CstType } from './common';
|
|
4
|
+
import { detectImportKind, parseImportPayload } from './import-detect';
|
|
5
|
+
|
|
6
|
+
describe('parseImportPayload', () => {
|
|
7
|
+
it('parses JSON strings', () => {
|
|
8
|
+
expect(parseImportPayload('{"title":"x"}')).toEqual({ title: 'x' });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('returns objects unchanged', () => {
|
|
12
|
+
const payload = { title: 'x' };
|
|
13
|
+
expect(parseImportPayload(payload)).toBe(payload);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('detectImportKind', () => {
|
|
18
|
+
it('detects session exports', () => {
|
|
19
|
+
expect(detectImportKind({ contractVersion: '2.0.0', state: { items: [] } })).toBe('session');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('detects portal schema JSON', () => {
|
|
23
|
+
expect(
|
|
24
|
+
detectImportKind({
|
|
25
|
+
contract_version: '1.0.0',
|
|
26
|
+
items: [{ id: 1, alias: 'X1', cst_type: CstType.BASE }]
|
|
27
|
+
})
|
|
28
|
+
).toBe('portal-schema');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('detects portal details JSON', () => {
|
|
32
|
+
expect(
|
|
33
|
+
detectImportKind({
|
|
34
|
+
title: 'Schema',
|
|
35
|
+
items: [{ id: 1, alias: 'X1', cst_type: CstType.BASE }]
|
|
36
|
+
})
|
|
37
|
+
).toBe('portal-details');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('rejects model JSON as schema session', () => {
|
|
41
|
+
expect(() =>
|
|
42
|
+
detectImportKind({
|
|
43
|
+
contract_version: '1.0.0',
|
|
44
|
+
items: [{ id: 1, type: 'basic', value: { 0: 'a' } }]
|
|
45
|
+
})
|
|
46
|
+
).toThrow(/Portal model JSON cannot be imported/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('treats empty contract_version items as undetectable', () => {
|
|
50
|
+
expect(() =>
|
|
51
|
+
detectImportKind({
|
|
52
|
+
contract_version: '1.0.0',
|
|
53
|
+
items: []
|
|
54
|
+
})
|
|
55
|
+
).toThrow(/Cannot detect import kind/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('rejects non-objects', () => {
|
|
59
|
+
expect(() => detectImportKind(null)).toThrow(/Invalid import payload/);
|
|
60
|
+
expect(() => detectImportKind('string')).toThrow(/Invalid import payload/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('rejects undetectable payloads', () => {
|
|
64
|
+
expect(() => detectImportKind({ title: 'orphan' })).toThrow(/Cannot detect import kind/);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -22,10 +22,13 @@ export function detectImportKind(data: unknown): Exclude<ImportDataKind, 'auto'>
|
|
|
22
22
|
|
|
23
23
|
if ('contract_version' in data && Array.isArray(data.items)) {
|
|
24
24
|
const items = data.items as unknown[];
|
|
25
|
-
if (items.length > 0
|
|
26
|
-
|
|
25
|
+
if (items.length > 0) {
|
|
26
|
+
const first = items[0];
|
|
27
|
+
if (isRecord(first) && 'cst_type' in first) {
|
|
28
|
+
return 'portal-schema';
|
|
29
|
+
}
|
|
30
|
+
throw new Error('Portal model JSON cannot be imported as a schema session');
|
|
27
31
|
}
|
|
28
|
-
throw new Error('Portal model JSON cannot be imported as a schema session');
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
if (Array.isArray(data.items) && data.items.length > 0) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { CstType } from './common';
|
|
4
|
+
import { portalItemToDraft } from './portal-json';
|
|
5
|
+
|
|
6
|
+
describe('portalItemToDraft', () => {
|
|
7
|
+
it('maps valid portal items', () => {
|
|
8
|
+
expect(
|
|
9
|
+
portalItemToDraft({
|
|
10
|
+
id: 1,
|
|
11
|
+
alias: 'D1',
|
|
12
|
+
cst_type: CstType.TERM,
|
|
13
|
+
definition_formal: '1+2',
|
|
14
|
+
term_raw: 'term',
|
|
15
|
+
definition_raw: 'def',
|
|
16
|
+
convention: 'conv'
|
|
17
|
+
})
|
|
18
|
+
).toEqual({
|
|
19
|
+
id: 1,
|
|
20
|
+
alias: 'D1',
|
|
21
|
+
cstType: CstType.TERM,
|
|
22
|
+
definitionFormal: '1+2',
|
|
23
|
+
term: 'term',
|
|
24
|
+
definitionText: 'def',
|
|
25
|
+
convention: 'conv'
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('rejects unknown cst_type values', () => {
|
|
30
|
+
expect(() =>
|
|
31
|
+
portalItemToDraft({
|
|
32
|
+
id: 1,
|
|
33
|
+
alias: 'X1',
|
|
34
|
+
cst_type: 'not-a-cst-type'
|
|
35
|
+
})
|
|
36
|
+
).toThrow(/Invalid cst_type "not-a-cst-type" for constituent "X1"/);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import { type BasicBinding, type RSToolValue } from './common';
|
|
1
|
+
import { CstType, type BasicBinding, type RSToolValue } from './common';
|
|
2
2
|
import { type ConstituentaDraft } from './constituenta';
|
|
3
3
|
|
|
4
|
+
const CST_TYPE_VALUES = new Set<string>(Object.values(CstType));
|
|
5
|
+
|
|
6
|
+
function parsePortalCstType(value: string, alias: string): ConstituentaDraft['cstType'] {
|
|
7
|
+
if (!CST_TYPE_VALUES.has(value)) {
|
|
8
|
+
throw new Error(`Invalid cst_type "${value}" for constituent "${alias}"`);
|
|
9
|
+
}
|
|
10
|
+
return value as ConstituentaDraft['cstType'];
|
|
11
|
+
}
|
|
12
|
+
|
|
4
13
|
/** Portal JSON import/export format version (schema and model files). */
|
|
5
14
|
export const PORTAL_JSON_CONTRACT_VERSION = '1.0.0';
|
|
6
15
|
|
|
@@ -80,7 +89,7 @@ export function portalItemToDraft(item: {
|
|
|
80
89
|
return {
|
|
81
90
|
id: item.id,
|
|
82
91
|
alias: item.alias,
|
|
83
|
-
cstType: item.cst_type
|
|
92
|
+
cstType: parsePortalCstType(item.cst_type, item.alias),
|
|
84
93
|
definitionFormal: item.definition_formal ?? '',
|
|
85
94
|
term: item.term_raw ?? '',
|
|
86
95
|
definitionText: item.definition_raw ?? '',
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdtempSync, rmSync, unlinkSync } from 'node:fs';
|
|
2
3
|
import { tmpdir } from 'node:os';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
import { describe, expect, it } from 'vitest';
|
|
5
6
|
|
|
7
|
+
import { TUPLE_ID } from '@rsconcept/domain';
|
|
8
|
+
|
|
6
9
|
import { CstType, EvalStatus, RSErrorCode, RSToolAgent } from './index';
|
|
7
10
|
|
|
8
11
|
function buildSampleForm(tool: RSToolAgent, sessionId: string) {
|
|
@@ -284,6 +287,37 @@ describe('RSToolAgent modeling and evaluation', () => {
|
|
|
284
287
|
);
|
|
285
288
|
});
|
|
286
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
|
+
|
|
296
|
+
await expect(
|
|
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
|
+
]);
|
|
319
|
+
});
|
|
320
|
+
|
|
287
321
|
it('evaluates expression against session context', () => {
|
|
288
322
|
const tool = new RSToolAgent();
|
|
289
323
|
const session = tool.createSession();
|
|
@@ -416,8 +450,8 @@ describe('RSToolAgent agent ergonomics', () => {
|
|
|
416
450
|
expect(result.summary.title).toBe('Agent patch');
|
|
417
451
|
expect(result.summary.itemCount).toBe(3);
|
|
418
452
|
expect(result.revision?.message).toBe('initial schema');
|
|
419
|
-
expect(fullState(tool).items.map(item => item.alias)).toEqual(['
|
|
420
|
-
expect(fullState(tool).items.map(item => item.cstType)).toEqual([CstType.
|
|
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]);
|
|
421
455
|
});
|
|
422
456
|
|
|
423
457
|
it('exports Portal payloads as structured objects', () => {
|
|
@@ -549,3 +583,368 @@ describe('RSToolAgent agent ergonomics', () => {
|
|
|
549
583
|
}
|
|
550
584
|
});
|
|
551
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
|
+
});
|