@rsconcept/rstool 1.3.0 → 1.4.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 (104) hide show
  1. package/README.md +4 -4
  2. package/dist/{agent-workflow-Gk0Vfnv1.d.ts → agent-workflow-D22FeODL.d.ts} +6 -6
  3. package/dist/{analysis-LLnPhmGa.d.ts → analysis-DA44pfgA.d.ts} +4 -3
  4. package/dist/common-D6TFl6l_.d.ts +11 -0
  5. package/dist/{constituenta-DnGR6bnM.d.ts → constituenta-QY5w1x91.d.ts} +7 -7
  6. package/dist/diagnostic-C6TRoBgt.d.ts +38 -0
  7. package/dist/diagnostic-assembly-DRunIcKQ.js +54 -0
  8. package/dist/diagnostic-assembly-DRunIcKQ.js.map +1 -0
  9. package/dist/{evaluation-Cns8BFm4.d.ts → evaluation-Cd4S7-3t.d.ts} +4 -3
  10. package/dist/index.d.ts +12 -12
  11. package/dist/index.js +4 -3
  12. package/dist/mappers/model-adapter.d.ts +3 -3
  13. package/dist/mappers/model-adapter.js +1 -199
  14. package/dist/mappers/schema-adapter.d.ts +5 -5
  15. package/dist/mappers/schema-adapter.js +13 -13
  16. package/dist/mappers/schema-adapter.js.map +1 -1
  17. package/dist/mappers/types.d.ts +6 -6
  18. package/dist/mappers/types.js +6 -10
  19. package/dist/mappers/types.js.map +1 -1
  20. package/dist/model-adapter-D-YmIvQW.js +225 -0
  21. package/dist/model-adapter-D-YmIvQW.js.map +1 -0
  22. package/dist/{model-value-BbonPzMz.d.ts → model-value-BUhr2IjA.d.ts} +2 -2
  23. package/dist/models/agent-workflow.d.ts +1 -1
  24. package/dist/models/analysis.d.ts +1 -1
  25. package/dist/models/common.d.ts +2 -2
  26. package/dist/models/common.js +1 -1
  27. package/dist/models/constituenta.d.ts +1 -1
  28. package/dist/models/diagnostic.d.ts +2 -2
  29. package/dist/models/diagnostic.js +3 -1
  30. package/dist/models/evaluation.d.ts +1 -1
  31. package/dist/models/index.d.ts +11 -11
  32. package/dist/models/index.js +3 -2
  33. package/dist/models/model-value.d.ts +1 -1
  34. package/dist/models/rstool-agent.d.ts +1 -1
  35. package/dist/models/rstool-agent.js +1 -1
  36. package/dist/models/session.d.ts +1 -1
  37. package/dist/models/tool-contract.d.ts +1 -1
  38. package/dist/models/tool-contract.js +1 -1
  39. package/dist/models/tool-contract.js.map +1 -1
  40. package/dist/{models-9G6ur_pg.js → models-B255GaEC.js} +76 -14
  41. package/dist/models-B255GaEC.js.map +1 -0
  42. package/dist/{rstool-agent-D2cQze_b.d.ts → rstool-agent-BGQAzFly.d.ts} +12 -10
  43. package/dist/session/session-store.d.ts +6 -6
  44. package/dist/session/session-store.js +1 -1
  45. package/dist/{session-ChexW8i7.d.ts → session-CI4rLYby.d.ts} +3 -3
  46. package/dist/{tool-contract-0uRGhEfW.d.ts → tool-contract-sQfr-DYD.d.ts} +12 -12
  47. package/dist/wrapper/stdio-wrapper.js +1 -1
  48. package/docs/DIAGNOSTICS.md +63 -20
  49. package/examples/README.md +2 -0
  50. package/examples/agent-client.ts +15 -3
  51. package/examples/audit-diagnostics.ts +40 -0
  52. package/examples/chocolate-nim/build-rsform.ts +15 -0
  53. package/examples/chocolate-nim/build-rsmodel.ts +15 -22
  54. package/examples/chocolate-nim/model-demo.ts +11 -0
  55. package/examples/chocolate-nim/rsform-session.json +329 -310
  56. package/examples/chocolate-nim/rsmodel-session.json +311 -311
  57. package/examples/diagnostics-utils.ts +27 -0
  58. package/examples/expression-bank/bank-constituents.ts +10 -10
  59. package/examples/expression-bank/build-rsform.ts +8 -0
  60. package/examples/expression-bank/rsform-session.json +1501 -1501
  61. package/examples/kinship/build-rsform.ts +16 -1
  62. package/examples/kinship/build-rsmodel.ts +15 -56
  63. package/examples/kinship/model-demo.ts +35 -0
  64. package/examples/kinship/rsform-session.json +300 -215
  65. package/examples/kinship/rsmodel-session.json +216 -216
  66. package/examples/movd/build-rsform.ts +15 -0
  67. package/examples/movd/build-rsmodel.ts +15 -46
  68. package/examples/movd/model-demo.ts +31 -0
  69. package/examples/movd/rsform-session.json +359 -270
  70. package/examples/movd/rsmodel-session.json +263 -263
  71. package/examples/sample/build-rsform.ts +43 -3
  72. package/examples/sample/build-rsmodel.ts +26 -2
  73. package/examples/sample/rsform-session.json +41 -12
  74. package/examples/sample/rsmodel-session.json +37 -37
  75. package/examples/template-apply/build-rsform.ts +31 -18
  76. package/examples/template-apply/rsform-session.json +100 -45
  77. package/package.json +3 -2
  78. package/skills/rstool-helper/EXAMPLES.md +3 -1
  79. package/skills/rstool-helper/GUIDE.md +7 -6
  80. package/skills/rstool-helper/REFERENCE.md +19 -19
  81. package/src/index.ts +7 -4
  82. package/src/mappers/diagnostic-assembly.ts +96 -0
  83. package/src/mappers/diagnostics-collector.test.ts +92 -0
  84. package/src/mappers/diagnostics-collector.ts +77 -0
  85. package/src/mappers/model-adapter.ts +29 -100
  86. package/src/mappers/rsform-builder.ts +127 -0
  87. package/src/mappers/schema-adapter.ts +15 -15
  88. package/src/mappers/types.ts +12 -12
  89. package/src/models/agent-workflow.ts +2 -2
  90. package/src/models/analysis.ts +3 -2
  91. package/src/models/common.ts +0 -12
  92. package/src/models/constituenta.ts +4 -4
  93. package/src/models/diagnostic.ts +15 -10
  94. package/src/models/evaluation.ts +3 -8
  95. package/src/models/index.ts +8 -2
  96. package/src/models/rstool-agent.test.ts +92 -5
  97. package/src/models/rstool-agent.ts +29 -11
  98. package/src/models/tool-contract.ts +4 -4
  99. package/src/session/persistence.ts +2 -2
  100. package/src/session/session-store.ts +14 -7
  101. package/dist/common-DHJalS-Q.d.ts +0 -22
  102. package/dist/diagnostic-D9yl_mEL.d.ts +0 -19
  103. package/dist/mappers/model-adapter.js.map +0 -1
  104. package/dist/models-9G6ur_pg.js.map +0 -1
@@ -6,33 +6,33 @@ import {
6
6
  type AnalysisResult,
7
7
  type ConstituentaDraft,
8
8
  type ConstituentaState,
9
- type DiagnosticRecord,
9
+ type Diagnostic,
10
10
  type SessionState
11
11
  } from '../models';
12
- import { toPublicAnalysis, toPublicError } from './types';
12
+ import { toPublicAnalysis } from './types';
13
13
 
14
14
  export class SchemaAdapter {
15
15
  public analyzeAgainstSession(
16
16
  session: SessionState,
17
17
  draft: ConstituentaDraft
18
- ): { result: AnalysisResult; diagnostics: DiagnosticRecord[] } {
18
+ ): { result: AnalysisResult; diagnostics: Diagnostic[] } {
19
19
  const analyzer = this.buildAnalyzer(session);
20
20
  const schema = this.toPseudoRSFormState(session, analyzer);
21
21
  const analysis = getAnalysisFor(draft.definitionFormal, draft.cstType, schema as unknown as RSForm, draft.alias);
22
- const result = toPublicAnalysis({
23
- success: analysis.success,
24
- type: analysis.type as Record<string, unknown> | null,
25
- valueClass: analysis.valueClass,
26
- errors: analysis.errors
27
- });
22
+ const target = { constituentId: draft.id, alias: draft.alias };
23
+ const result = toPublicAnalysis(
24
+ {
25
+ success: analysis.success,
26
+ type: analysis.type as Record<string, unknown> | null,
27
+ valueClass: analysis.valueClass,
28
+ errors: analysis.errors
29
+ },
30
+ draft.definitionFormal,
31
+ target
32
+ );
28
33
  return {
29
34
  result,
30
- diagnostics: analysis.errors.map(error => ({
31
- sessionId: session.sessionId,
32
- constituentId: draft.id,
33
- expression: draft.definitionFormal,
34
- error: toPublicError(error)
35
- }))
35
+ diagnostics: result.diagnostics
36
36
  };
37
37
  }
38
38
 
@@ -1,8 +1,9 @@
1
- import { type ValueClass } from '@rsconcept/domain/rslang';
1
+ import { DiagnosticKind, type DiagnosticTarget, expressionDiagnostic } from './diagnostic-assembly';
2
+ import { type RSErrorDescription, type ValueClass } from '@rsconcept/domain/rslang';
2
3
 
3
- import { type AnalysisResult, type RSToolErrorDescription } from '../models';
4
+ import { type AnalysisResult, type Diagnostic } from '../models';
4
5
 
5
- /** Minimal domain error shape before mapping to {@link RSToolErrorDescription}. */
6
+ /** Minimal domain error shape before mapping to {@link Diagnostic}. */
6
7
  export interface DomainErrorLike {
7
8
  code: number;
8
9
  from: number;
@@ -19,21 +20,20 @@ export interface DomainAnalysisLike {
19
20
  }
20
21
 
21
22
  /** Map a domain parser/semantic error to the public diagnostic shape. */
22
- export function toPublicError(error: DomainErrorLike): RSToolErrorDescription {
23
- return {
24
- code: error.code,
25
- from: error.from,
26
- to: error.to,
27
- params: error.params
28
- };
23
+ export function toDiagnostic(error: DomainErrorLike, expression: string, target?: DiagnosticTarget): Diagnostic {
24
+ return expressionDiagnostic(error as RSErrorDescription, expression, target);
29
25
  }
30
26
 
31
27
  /** Map a domain analysis result to the public {@link AnalysisResult} shape. */
32
- export function toPublicAnalysis(analysis: DomainAnalysisLike): AnalysisResult {
28
+ export function toPublicAnalysis(
29
+ analysis: DomainAnalysisLike,
30
+ expression: string,
31
+ target?: DiagnosticTarget
32
+ ): AnalysisResult {
33
33
  return {
34
34
  success: analysis.success,
35
35
  type: analysis.type,
36
36
  valueClass: analysis.valueClass,
37
- diagnostics: analysis.errors.map(toPublicError)
37
+ diagnostics: analysis.errors.map(error => expressionDiagnostic(error as RSErrorDescription, expression, target))
38
38
  };
39
39
  }
@@ -1,6 +1,6 @@
1
1
  import { type CstType } from './common';
2
2
  import { type ApplyConstituentsMode, type ApplyConstituentsResult, type ConstituentaDraft } from './constituenta';
3
- import { type DiagnosticRecord } from './diagnostic';
3
+ import { type Diagnostic } from './diagnostic';
4
4
  import { type SessionHandle, type SessionRevision, type SessionState } from './session';
5
5
 
6
6
  /** Level of detail returned by {@link RSToolAgent.getSessionState}. */
@@ -54,7 +54,7 @@ export interface SessionSummary {
54
54
  modelItemCount: number;
55
55
  diagnosticsCount: number;
56
56
  items: SessionSummaryItem[];
57
- diagnostics: DiagnosticRecord[];
57
+ diagnostics: Diagnostic[];
58
58
  lastRevision?: SessionRevision;
59
59
  }
60
60
 
@@ -1,4 +1,5 @@
1
- import { type CstType, type RSToolErrorDescription, type ValueClass } from './common';
1
+ import { type Diagnostic } from './diagnostic';
2
+ import { type CstType, type ValueClass } from './common';
2
3
 
3
4
  /** Input for {@link RSToolAgent.analyzeExpression}. */
4
5
  export interface AnalyzeExpressionInput {
@@ -16,5 +17,5 @@ export interface AnalysisResult {
16
17
  /** Inferred RS type AST as a plain object, or `null` on failure. */
17
18
  type: Record<string, unknown> | null;
18
19
  valueClass: ValueClass | null;
19
- diagnostics: RSToolErrorDescription[];
20
+ diagnostics: Diagnostic[];
20
21
  }
@@ -10,15 +10,3 @@ export type { BasicBinding };
10
10
 
11
11
  /** Runtime evaluation value: number, nested array (set/tuple), or boolean 0/1. */
12
12
  export type RSToolValue = number | RSToolValue[];
13
-
14
- /** Language or validation error with source span and optional format parameters. */
15
- export interface RSToolErrorDescription {
16
- /** Numeric error code (see {@link RSErrorCode}). */
17
- code: number;
18
- /** Start offset in the analyzed expression. */
19
- from: number;
20
- /** End offset in the analyzed expression. */
21
- to: number;
22
- /** Interpolation values for localized error messages. */
23
- params?: readonly string[];
24
- }
@@ -1,6 +1,6 @@
1
1
  import { type AnalysisResult } from './analysis';
2
2
  import { type CstType } from './common';
3
- import { type DiagnosticRecord } from './diagnostic';
3
+ import { type Diagnostic } from './diagnostic';
4
4
 
5
5
  /** Constituent payload before analysis and merge into session state. */
6
6
  export interface ConstituentaDraft {
@@ -33,7 +33,7 @@ export interface AddOrUpdateConstituentaInput {
33
33
 
34
34
  export interface AddOrUpdateConstituentaResult {
35
35
  state: ConstituentaState;
36
- diagnostics: DiagnosticRecord[];
36
+ diagnostics: Diagnostic[];
37
37
  }
38
38
 
39
39
  /** How a multi-draft apply handles partial failures. */
@@ -48,6 +48,6 @@ export interface ApplyConstituentsInput {
48
48
  export interface ApplyConstituentsResult {
49
49
  success: boolean;
50
50
  applied: ConstituentaState[];
51
- failed: Array<{ draft: ConstituentaDraft; diagnostics: DiagnosticRecord[] }>;
52
- diagnostics: DiagnosticRecord[];
51
+ failed: Array<{ draft: ConstituentaDraft; diagnostics: Diagnostic[] }>;
52
+ diagnostics: Diagnostic[];
53
53
  }
@@ -1,16 +1,21 @@
1
- import { type RSToolErrorDescription } from './common';
1
+ import { type CstDiagnostic, DiagnosticKind, RSDiagnosticCode } from '@rsconcept/domain/library';
2
2
 
3
- /** Persisted diagnostic for a failed or warned expression in a session. */
4
- export interface DiagnosticRecord {
5
- sessionId: string;
6
- /** Constituent id when the diagnostic is tied to a stored item; omitted for scratch analysis. */
7
- constituentId?: number;
8
- /** Expression text that was analyzed. */
9
- expression: string;
10
- error: RSToolErrorDescription;
11
- }
3
+ export {
4
+ type Diagnostic,
5
+ type DiagnosticSeverity,
6
+ type DiagnosticTarget,
7
+ expandCstDiagnostic,
8
+ expressionDiagnostic,
9
+ getDiagnosticName,
10
+ getDiagnosticSeverity,
11
+ modelStatusDiagnostic
12
+ } from '../mappers/diagnostic-assembly';
13
+
14
+ export { type CstDiagnostic, DiagnosticKind, RSDiagnosticCode };
12
15
 
13
16
  /** Filters for {@link RSToolAgent.listDiagnostics}. */
14
17
  export interface ListDiagnosticsFilters {
15
18
  constituentId?: number;
19
+ kind?: DiagnosticKind;
20
+ severity?: import('../mappers/diagnostic-assembly').DiagnosticSeverity;
16
21
  }
@@ -1,10 +1,5 @@
1
- import {
2
- type BasicBinding,
3
- type CstType,
4
- type EvalStatus,
5
- type RSToolErrorDescription,
6
- type RSToolValue
7
- } from './common';
1
+ import { type BasicBinding, type CstType, type EvalStatus, type RSToolValue } from './common';
2
+ import { type Diagnostic } from './diagnostic';
8
3
 
9
4
  export interface EvaluateExpressionInput {
10
5
  expression: string;
@@ -32,5 +27,5 @@ export interface EvaluationResult {
32
27
  status: EvalStatus;
33
28
  iterations: number;
34
29
  cacheHits: number;
35
- diagnostics: RSToolErrorDescription[];
30
+ diagnostics: Diagnostic[];
36
31
  }
@@ -14,11 +14,17 @@ export {
14
14
  RSErrorCode,
15
15
  ValueClass,
16
16
  type BasicBinding,
17
- type RSToolErrorDescription,
18
17
  type RSToolValue
19
18
  } from './common';
20
19
  export { type ApplyConstituentsMode, type ConstituentaDraft, type ConstituentaState } from './constituenta';
21
- export { type DiagnosticRecord, type ListDiagnosticsFilters } from './diagnostic';
20
+ export {
21
+ type CstDiagnostic,
22
+ type Diagnostic,
23
+ DiagnosticKind,
24
+ type DiagnosticSeverity,
25
+ type ListDiagnosticsFilters,
26
+ RSDiagnosticCode
27
+ } from './diagnostic';
22
28
  export { type EvaluateInput, type EvaluationResult } from './evaluation';
23
29
  export {
24
30
  type ExportFormat,
@@ -6,7 +6,7 @@ import { describe, expect, it } from 'vitest';
6
6
 
7
7
  import { TUPLE_ID } from '@rsconcept/domain';
8
8
 
9
- import { CstType, EvalStatus, RSErrorCode, RSToolAgent } from './index';
9
+ import { CstType, EvalStatus, RSErrorCode, RSDiagnosticCode, RSToolAgent } from './index';
10
10
 
11
11
  function buildSampleForm(tool: RSToolAgent, sessionId: string) {
12
12
  tool.applySchemaPatch(
@@ -44,6 +44,7 @@ describe('RSToolAgent', () => {
44
44
  const analysis = tool.analyzeExpression({ expression: '(', cstType: CstType.TERM }, session.sessionId);
45
45
  expect(analysis.success).toBe(false);
46
46
  expect(analysis.diagnostics.length).toBeGreaterThan(0);
47
+ expect(analysis.diagnostics.every(record => record.constituentId === undefined)).toBe(true);
47
48
  });
48
49
 
49
50
  it('rejects formal definition for constants', () => {
@@ -56,7 +57,7 @@ describe('RSToolAgent', () => {
56
57
  session.sessionId
57
58
  );
58
59
  expect(result.success).toBe(false);
59
- expect(result.failed[0]?.diagnostics[0]?.error.code).toBe(RSErrorCode.definitionNotAllowed);
60
+ expect(result.failed[0]?.diagnostics[0]?.code).toBe(RSErrorCode.definitionNotAllowed);
60
61
  });
61
62
 
62
63
  it('rejects formal definition for basic sets', () => {
@@ -69,7 +70,7 @@ describe('RSToolAgent', () => {
69
70
  session.sessionId
70
71
  );
71
72
  expect(result.success).toBe(false);
72
- expect(result.failed[0]?.diagnostics[0]?.error.code).toBe(RSErrorCode.definitionNotAllowed);
73
+ expect(result.failed[0]?.diagnostics[0]?.code).toBe(RSErrorCode.definitionNotAllowed);
73
74
  });
74
75
 
75
76
  it('returns known analysis for empty base definition', () => {
@@ -412,6 +413,27 @@ describe('RSToolAgent modeling and evaluation', () => {
412
413
  expect(model.items).toHaveLength(1);
413
414
  expect(model.items[0]?.value).toEqual({ 0: 'zero' });
414
415
  });
416
+
417
+ it('preserves exported diagnostics on session import', () => {
418
+ const tool = new RSToolAgent();
419
+ const session = tool.createSession();
420
+ tool.applySchemaPatch(
421
+ {
422
+ items: [
423
+ { alias: 'X1', term: 'person' },
424
+ { alias: 'X2', term: 'Person' }
425
+ ]
426
+ },
427
+ session.sessionId
428
+ );
429
+ const beforeImport = tool.listDiagnostics(undefined, session.sessionId);
430
+ expect(beforeImport.some(record => record.code === RSDiagnosticCode.schemaHomonym)).toBe(true);
431
+
432
+ const exported = tool.exportSession(session.sessionId);
433
+ const imported = tool.importData(exported, 'session');
434
+ const afterImport = tool.listDiagnostics(undefined, imported.sessionId);
435
+ expect(afterImport).toEqual(beforeImport);
436
+ });
415
437
  });
416
438
 
417
439
  describe('RSToolAgent agent ergonomics', () => {
@@ -474,10 +496,72 @@ describe('RSToolAgent agent ergonomics', () => {
474
496
  { mode: 'best_effort', items: [{ alias: 'X1', cstType: CstType.BASE, definitionFormal: 'Z' }] },
475
497
  session.sessionId
476
498
  );
477
- expect(tool.listDiagnostics(undefined, session.sessionId)).toHaveLength(1);
499
+ const expressionDiags = tool.listDiagnostics({ kind: 'expression' }, session.sessionId);
500
+ expect(expressionDiags).toHaveLength(1);
501
+ expect(expressionDiags[0]?.code).toBe(RSErrorCode.definitionNotAllowed);
478
502
 
479
503
  tool.applySchemaPatch({ items: [{ alias: 'X1', cstType: CstType.BASE, definitionFormal: '' }] }, session.sessionId);
480
- expect(tool.listDiagnostics(undefined, session.sessionId)).toHaveLength(0);
504
+ expect(tool.listDiagnostics({ kind: 'expression' }, session.sessionId)).toHaveLength(0);
505
+ expect(tool.listDiagnostics({ kind: 'schema' }, session.sessionId).length).toBeGreaterThan(0);
506
+ expect(tool.listDiagnostics({ kind: 'model' }, session.sessionId)).toHaveLength(0);
507
+ });
508
+
509
+ it('reports schema homonyms and missing convention', () => {
510
+ const tool = new RSToolAgent();
511
+ const session = tool.createSession();
512
+ tool.applySchemaPatch(
513
+ {
514
+ items: [
515
+ { alias: 'X1', term: 'person', convention: 'people' },
516
+ { alias: 'X2', term: 'Person', convention: 'other people' }
517
+ ]
518
+ },
519
+ session.sessionId
520
+ );
521
+
522
+ const homonyms = tool.listDiagnostics({ kind: 'schema' }, session.sessionId);
523
+ expect(homonyms.some(record => record.code === RSDiagnosticCode.schemaHomonym)).toBe(true);
524
+ expect(homonyms.filter(record => record.code === RSDiagnosticCode.schemaHomonym)).toHaveLength(2);
525
+ });
526
+
527
+ it('reports formal duplicate definitions', () => {
528
+ const tool = new RSToolAgent();
529
+ const session = tool.createSession();
530
+ tool.applySchemaPatch(
531
+ {
532
+ items: [
533
+ { alias: 'X1', convention: 'base' },
534
+ { alias: 'S1', definitionFormal: 'ℬ(X1×X1)', convention: 'pairs' },
535
+ { alias: 'D1', definitionFormal: 'Pr1(S1)' },
536
+ { alias: 'D2', definitionFormal: 'Pr1(S1)' }
537
+ ]
538
+ },
539
+ session.sessionId
540
+ );
541
+
542
+ const duplicates = tool
543
+ .listDiagnostics({ kind: 'schema' }, session.sessionId)
544
+ .filter(record => record.code === RSDiagnosticCode.schemaFormalDuplicate);
545
+ expect(duplicates).toHaveLength(2);
546
+ expect(duplicates[0]?.params?.[0]).toContain('D2');
547
+ });
548
+
549
+ it('reports model diagnostics for empty basic sets after model refresh', () => {
550
+ const tool = new RSToolAgent();
551
+ const session = tool.createSession();
552
+ tool.applySchemaPatch(
553
+ {
554
+ items: [{ alias: 'X1', convention: 'base', term: 'element' }]
555
+ },
556
+ session.sessionId
557
+ );
558
+
559
+ expect(tool.listDiagnostics({ kind: 'model' }, session.sessionId)).toHaveLength(0);
560
+
561
+ tool.recalculateModel(session.sessionId);
562
+
563
+ const modelIssues = tool.listDiagnostics({ kind: 'model' }, session.sessionId);
564
+ expect(modelIssues.some(record => record.code === RSDiagnosticCode.modelEmpty)).toBe(true);
481
565
  });
482
566
 
483
567
  it('does not record analyzeExpression diagnostics by default', () => {
@@ -919,6 +1003,9 @@ describe('RSToolAgent modeling semantics', () => {
919
1003
  const a1 = recalculated.items.find(item => item.alias === 'A1');
920
1004
  expect(a1?.status).toBe(EvalStatus.AXIOM_FALSE);
921
1005
  expect(a1?.value).toBe(0);
1006
+
1007
+ const modelIssues = tool.listDiagnostics({ kind: 'model' }, session.sessionId);
1008
+ expect(modelIssues.some(record => record.code === RSDiagnosticCode.modelAxiomFalse)).toBe(true);
922
1009
  });
923
1010
 
924
1011
  it('throws when evaluate input is incomplete', () => {
@@ -4,6 +4,7 @@ import {
4
4
  portalSchemaToDrafts,
5
5
  portalSchemaToSessionSeed
6
6
  } from '../mappers/portal-adapter';
7
+ import { collectModelDiagnostics, collectSchemaDiagnostics } from '../mappers/diagnostics-collector';
7
8
  import { ModelAdapter } from '../mappers/model-adapter';
8
9
  import { SchemaAdapter } from '../mappers/schema-adapter';
9
10
  import { orderDrafts, reorderSessionItemsByDrafts } from '../session/batch-apply';
@@ -26,7 +27,7 @@ import {
26
27
  type ConstituentaDraft,
27
28
  type ConstituentaState
28
29
  } from './constituenta';
29
- import { type DiagnosticRecord, type ListDiagnosticsFilters } from './diagnostic';
30
+ import { type Diagnostic, type ListDiagnosticsFilters } from './diagnostic';
30
31
  import { type EvaluateInput, type EvaluationResult } from './evaluation';
31
32
  import { detectImportKind, parseImportPayload } from './import-detect';
32
33
  import { type ExportPortalInput, type ExportPortalResult, type ImportDataKind } from './import-export';
@@ -190,14 +191,11 @@ export class RSToolAgent implements RSToolAgentContract {
190
191
  cstType: input.cstType,
191
192
  definitionFormal: input.expression
192
193
  });
194
+ const sanitizedDiagnostics = diagnostics.map(item => ({ ...item, constituentId: undefined }));
193
195
  if (input.recordDiagnostics) {
194
- this.sessions.replaceDiagnosticsForConstituent(
195
- id,
196
- undefined,
197
- diagnostics.map(item => ({ ...item, constituentId: undefined }))
198
- );
196
+ this.sessions.setDiagnostics(id, [...collectSchemaDiagnostics(envelope.state), ...sanitizedDiagnostics]);
199
197
  }
200
- return result;
198
+ return { ...result, diagnostics: sanitizedDiagnostics };
201
199
  }
202
200
 
203
201
  /** @inheritdoc */
@@ -263,9 +261,11 @@ export class RSToolAgent implements RSToolAgentContract {
263
261
  const model = await this.evaluation.setConstituentaValues(state, { items: input.set });
264
262
  state = { ...state, model };
265
263
  this.sessions.replaceState(id, state);
264
+ this.refreshModelDiagnostics(id);
266
265
  return model;
267
266
  }
268
267
 
268
+ this.refreshModelDiagnostics(id);
269
269
  return structuredClone(this.sessions.get(id).state.model);
270
270
  } catch (error) {
271
271
  this.sessions.restore(id, snapshot);
@@ -296,8 +296,11 @@ export class RSToolAgent implements RSToolAgentContract {
296
296
 
297
297
  /** @inheritdoc */
298
298
  public recalculateModel(sessionId?: string): RecalculateModelResult {
299
- const envelope = this.sessions.get(this.resolveSessionId(sessionId));
300
- return this.evaluation.recalculateModel(envelope.state);
299
+ const id = this.resolveSessionId(sessionId);
300
+ const envelope = this.sessions.get(id);
301
+ const result = this.evaluation.recalculateModel(envelope.state);
302
+ this.refreshModelDiagnostics(id);
303
+ return result;
301
304
  }
302
305
 
303
306
  private addOrUpdateConstituenta(
@@ -308,7 +311,6 @@ export class RSToolAgent implements RSToolAgentContract {
308
311
  const envelope = this.sessions.get(id);
309
312
  const { result, diagnostics } = this.adapter.analyzeAgainstSession(envelope.state, input.draft);
310
313
  const state = this.adapter.mergeStateWithDraft(envelope.state, input.draft, result);
311
- this.sessions.replaceDiagnosticsForConstituent(id, input.draft.id, diagnostics);
312
314
  return { state, diagnostics };
313
315
  }
314
316
 
@@ -342,6 +344,7 @@ export class RSToolAgent implements RSToolAgentContract {
342
344
  const envelope = this.sessions.get(id);
343
345
  reorderSessionItemsByDrafts(envelope.state.items, input.drafts, preBatchItemIds);
344
346
  this.sessions.replaceState(id, envelope.state);
347
+ this.refreshDiagnostics(id);
345
348
 
346
349
  return {
347
350
  success: failed.length === 0,
@@ -418,11 +421,13 @@ export class RSToolAgent implements RSToolAgentContract {
418
421
  }
419
422
  const data = parsed as {
420
423
  state: SessionState;
421
- diagnostics?: DiagnosticRecord[];
424
+ diagnostics?: Diagnostic[];
422
425
  };
423
426
  const handle = this.sessions.create(normalizeImportedState(data.state), this.contractVersion);
424
427
  if (data.diagnostics?.length) {
425
428
  this.sessions.setDiagnostics(handle.sessionId, data.diagnostics);
429
+ } else {
430
+ this.refreshDiagnostics(handle.sessionId);
426
431
  }
427
432
  return this.trackSession(handle);
428
433
  }
@@ -509,4 +514,17 @@ export class RSToolAgent implements RSToolAgentContract {
509
514
  this.sessions.saveCurrentSessionId(handle.sessionId);
510
515
  return handle;
511
516
  }
517
+
518
+ private refreshDiagnostics(sessionId: string): void {
519
+ const envelope = this.sessions.get(sessionId);
520
+ this.sessions.setDiagnostics(sessionId, collectSchemaDiagnostics(envelope.state));
521
+ }
522
+
523
+ private refreshModelDiagnostics(sessionId: string): void {
524
+ const envelope = this.sessions.get(sessionId);
525
+ this.sessions.setDiagnostics(sessionId, [
526
+ ...collectSchemaDiagnostics(envelope.state),
527
+ ...collectModelDiagnostics(envelope.state)
528
+ ]);
529
+ }
512
530
  }
@@ -5,14 +5,14 @@ import {
5
5
  type SessionStateResult
6
6
  } from './agent-workflow';
7
7
  import { type AnalysisResult, type AnalyzeExpressionInput } from './analysis';
8
- import { type DiagnosticRecord, type ListDiagnosticsFilters } from './diagnostic';
8
+ import { type Diagnostic, type ListDiagnosticsFilters } from './diagnostic';
9
9
  import { type EvaluateInput, type EvaluationResult } from './evaluation';
10
10
  import { type ExportPortalInput, type ExportPortalResult, type ImportDataKind } from './import-export';
11
11
  import { type RecalculateModelResult, type SessionModelState, type SetModelValuesInput } from './model-value';
12
12
  import { type SessionHandle, type SessionRevision, type SessionState } from './session';
13
13
 
14
14
  /** Agent-visible contract version; bump on breaking API changes. */
15
- export const CONTRACT_VERSION = '2.0.0';
15
+ export const CONTRACT_VERSION = '3.0.0';
16
16
 
17
17
  /** Options for constructing an {@link RSToolAgent}. */
18
18
  export interface RSToolAgentOptions {
@@ -48,8 +48,8 @@ export interface RSToolAgentContract {
48
48
  /** Return session summary (default) or full cloned state when `detail` is `'full'`. */
49
49
  getSessionState(detail?: SessionStateDetail, sessionId?: string): SessionStateResult;
50
50
 
51
- /** List diagnostics for the session, optionally filtered by constituent. */
52
- listDiagnostics(filters?: ListDiagnosticsFilters, sessionId?: string): DiagnosticRecord[];
51
+ /** List diagnostics for the session, optionally filtered by constituent or kind. */
52
+ listDiagnostics(filters?: ListDiagnosticsFilters, sessionId?: string): Diagnostic[];
53
53
 
54
54
  /** Parse and type-check an RSLang expression against the current schema context. */
55
55
  analyzeExpression(input: AnalyzeExpressionInput, sessionId?: string): AnalysisResult;
@@ -1,11 +1,11 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
- import { type DiagnosticRecord, type SessionState } from '../models';
4
+ import { type Diagnostic, type SessionState } from '../models';
5
5
 
6
6
  export interface PersistedSessionEnvelope {
7
7
  state: SessionState;
8
- diagnostics: DiagnosticRecord[];
8
+ diagnostics: Diagnostic[];
9
9
  }
10
10
 
11
11
  const CURRENT_SESSION_FILE = '_current.json';
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
 
3
3
  import {
4
4
  CONTRACT_VERSION,
5
- type DiagnosticRecord,
5
+ type Diagnostic,
6
6
  type ListDiagnosticsFilters,
7
7
  type SessionHandle,
8
8
  type SessionRevision,
@@ -16,7 +16,7 @@ export interface SessionStoreOptions {
16
16
 
17
17
  interface SessionEnvelope {
18
18
  state: SessionState;
19
- diagnostics: DiagnosticRecord[];
19
+ diagnostics: Diagnostic[];
20
20
  }
21
21
 
22
22
  export class SessionStore {
@@ -97,7 +97,7 @@ export class SessionStore {
97
97
  public replaceDiagnosticsForConstituent(
98
98
  sessionId: string,
99
99
  constituentId: number | undefined,
100
- records: DiagnosticRecord[]
100
+ records: Diagnostic[]
101
101
  ): void {
102
102
  const found = this.get(sessionId);
103
103
  found.diagnostics = found.diagnostics.filter(record => record.constituentId !== constituentId);
@@ -106,19 +106,26 @@ export class SessionStore {
106
106
  this.persist(sessionId, found);
107
107
  }
108
108
 
109
- public setDiagnostics(sessionId: string, records: DiagnosticRecord[]): void {
109
+ public setDiagnostics(sessionId: string, records: Diagnostic[]): void {
110
110
  const found = this.get(sessionId);
111
111
  found.diagnostics = [...records];
112
112
  found.state.updatedAt = new Date().toISOString();
113
113
  this.persist(sessionId, found);
114
114
  }
115
115
 
116
- public listDiagnostics(sessionId: string, filters?: ListDiagnosticsFilters): DiagnosticRecord[] {
116
+ public listDiagnostics(sessionId: string, filters?: ListDiagnosticsFilters): Diagnostic[] {
117
117
  const found = this.get(sessionId);
118
+ let records = found.diagnostics;
119
+ if (filters?.kind !== undefined) {
120
+ records = records.filter(record => record.kind === filters.kind);
121
+ }
122
+ if (filters?.severity !== undefined) {
123
+ records = records.filter(record => record.severity === filters.severity);
124
+ }
118
125
  if (filters?.constituentId === undefined) {
119
- return [...found.diagnostics];
126
+ return [...records];
120
127
  }
121
- return found.diagnostics.filter(record => record.constituentId === filters.constituentId);
128
+ return records.filter(record => record.constituentId === filters.constituentId);
122
129
  }
123
130
 
124
131
  public snapshot(sessionId: string): SessionEnvelope {
@@ -1,22 +0,0 @@
1
- import { CstType as CstType$1 } from "@rsconcept/domain/library/rsform";
2
- import { BasicBinding, EvalStatus as EvalStatus$1 } from "@rsconcept/domain/library/rsmodel";
3
- import { ValueClass as ValueClass$1 } from "@rsconcept/domain/rslang";
4
- import { RSErrorCode } from "@rsconcept/domain/rslang/error";
5
-
6
- //#region src/models/common.d.ts
7
- /** Runtime evaluation value: number, nested array (set/tuple), or boolean 0/1. */
8
- type RSToolValue = number | RSToolValue[];
9
- /** Language or validation error with source span and optional format parameters. */
10
- interface RSToolErrorDescription {
11
- /** Numeric error code (see {@link RSErrorCode}). */
12
- code: number;
13
- /** Start offset in the analyzed expression. */
14
- from: number;
15
- /** End offset in the analyzed expression. */
16
- to: number;
17
- /** Interpolation values for localized error messages. */
18
- params?: readonly string[];
19
- }
20
- //#endregion
21
- export { RSToolErrorDescription as a, RSErrorCode as i, CstType$1 as n, RSToolValue as o, EvalStatus$1 as r, ValueClass$1 as s, BasicBinding as t };
22
- //# sourceMappingURL=common-DHJalS-Q.d.ts.map
@@ -1,19 +0,0 @@
1
- import { a as RSToolErrorDescription } from "./common-DHJalS-Q.js";
2
-
3
- //#region src/models/diagnostic.d.ts
4
- /** Persisted diagnostic for a failed or warned expression in a session. */
5
- interface DiagnosticRecord {
6
- sessionId: string;
7
- /** Constituent id when the diagnostic is tied to a stored item; omitted for scratch analysis. */
8
- constituentId?: number;
9
- /** Expression text that was analyzed. */
10
- expression: string;
11
- error: RSToolErrorDescription;
12
- }
13
- /** Filters for {@link RSToolAgent.listDiagnostics}. */
14
- interface ListDiagnosticsFilters {
15
- constituentId?: number;
16
- }
17
- //#endregion
18
- export { ListDiagnosticsFilters as n, DiagnosticRecord as t };
19
- //# sourceMappingURL=diagnostic-D9yl_mEL.d.ts.map