@rsconcept/rstool 1.2.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) 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 +8 -8
  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-DTLctDmB.js} +72 -15
  41. package/dist/models-DTLctDmB.js.map +1 -0
  42. package/dist/{rstool-agent-D2cQze_b.d.ts → rstool-agent-BMVAyMAG.d.ts} +11 -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/BASE-SELECTION.md +92 -0
  49. package/docs/CONCEPTUAL-SCHEMA.md +20 -15
  50. package/docs/CONSTITUENTA.md +3 -3
  51. package/docs/DIAGNOSTICS.md +63 -20
  52. package/docs/DOMAIN.md +27 -21
  53. package/docs/README.md +1 -0
  54. package/docs/SYNTAX.md +3 -2
  55. package/docs/TYPIFICATION.md +3 -9
  56. package/examples/README.md +2 -0
  57. package/examples/agent-client.ts +15 -3
  58. package/examples/audit-diagnostics.ts +40 -0
  59. package/examples/chocolate-nim/build-rsform.ts +15 -0
  60. package/examples/chocolate-nim/build-rsmodel.ts +15 -22
  61. package/examples/chocolate-nim/model-demo.ts +11 -0
  62. package/examples/chocolate-nim/rsform-session.json +329 -310
  63. package/examples/chocolate-nim/rsmodel-session.json +311 -311
  64. package/examples/diagnostics-utils.ts +25 -0
  65. package/examples/expression-bank/bank-constituents.ts +10 -10
  66. package/examples/expression-bank/build-rsform.ts +8 -0
  67. package/examples/expression-bank/rsform-session.json +1501 -1501
  68. package/examples/kinship/build-rsform.ts +16 -1
  69. package/examples/kinship/build-rsmodel.ts +15 -56
  70. package/examples/kinship/model-demo.ts +35 -0
  71. package/examples/kinship/rsform-session.json +300 -215
  72. package/examples/kinship/rsmodel-session.json +216 -216
  73. package/examples/movd/build-rsform.ts +15 -0
  74. package/examples/movd/build-rsmodel.ts +15 -46
  75. package/examples/movd/model-demo.ts +31 -0
  76. package/examples/movd/rsform-session.json +359 -270
  77. package/examples/movd/rsmodel-session.json +263 -263
  78. package/examples/sample/build-rsform.ts +43 -3
  79. package/examples/sample/build-rsmodel.ts +26 -2
  80. package/examples/sample/rsform-session.json +41 -12
  81. package/examples/sample/rsmodel-session.json +37 -37
  82. package/examples/template-apply/build-rsform.ts +26 -9
  83. package/examples/template-apply/rsform-session.json +100 -45
  84. package/package.json +3 -2
  85. package/skills/rstool-helper/EXAMPLES.md +22 -1
  86. package/skills/rstool-helper/GUIDE.md +23 -28
  87. package/skills/rstool-helper/REFERENCE.md +28 -23
  88. package/src/index.ts +7 -4
  89. package/src/mappers/diagnostic-assembly.ts +96 -0
  90. package/src/mappers/diagnostics-collector.test.ts +63 -0
  91. package/src/mappers/diagnostics-collector.ts +73 -0
  92. package/src/mappers/model-adapter.ts +28 -99
  93. package/src/mappers/rsform-builder.ts +127 -0
  94. package/src/mappers/schema-adapter.ts +19 -15
  95. package/src/mappers/types.ts +12 -12
  96. package/src/models/agent-workflow.ts +2 -2
  97. package/src/models/analysis.ts +3 -2
  98. package/src/models/common.ts +0 -12
  99. package/src/models/constituenta.ts +4 -4
  100. package/src/models/diagnostic.ts +15 -10
  101. package/src/models/evaluation.ts +3 -8
  102. package/src/models/index.ts +8 -2
  103. package/src/models/rstool-agent.test.ts +66 -5
  104. package/src/models/rstool-agent.ts +22 -10
  105. package/src/models/tool-contract.ts +4 -4
  106. package/src/session/persistence.ts +2 -2
  107. package/src/session/session-store.ts +14 -7
  108. package/dist/common-DHJalS-Q.d.ts +0 -22
  109. package/dist/diagnostic-D9yl_mEL.d.ts +0 -19
  110. package/dist/mappers/model-adapter.js.map +0 -1
  111. package/dist/models-9G6ur_pg.js.map +0 -1
@@ -0,0 +1,127 @@
1
+ import { Graph } from '@rsconcept/domain/graph/graph';
2
+ import { extractGlobals } from '@rsconcept/domain/rslang/api';
3
+ import { type ExpressionType, RSLangAnalyzer, type ValueClass } from '@rsconcept/domain/rslang';
4
+ import { assignSchemaDiagnostics } from '@rsconcept/domain/library';
5
+ import { isBasicConcept, isLogical, normalizeExpression } from '@rsconcept/domain/library/rsform-api';
6
+ import { type Constituenta, CstType, type RSForm } from '@rsconcept/domain/library/rsform';
7
+
8
+ import { type ConstituentaState, type SessionState } from '../models';
9
+
10
+ export interface EnrichedConstituenta {
11
+ item: ConstituentaState;
12
+ cst: Constituenta;
13
+ }
14
+
15
+ /** Build a domain {@link RSForm} view from session state (same shape as Portal loader output). */
16
+ export function buildRSFormFromSession(session: SessionState): RSForm {
17
+ const graph = new Graph<number>();
18
+ const cstByAlias = new Map<string, Constituenta>();
19
+ const cstByID = new Map<number, Constituenta>();
20
+ const analyzer = new RSLangAnalyzer();
21
+
22
+ const items = session.items.map(item => {
23
+ const cst = toFrontendConstituenta(item);
24
+ cstByAlias.set(cst.alias, cst);
25
+ cstByID.set(cst.id, cst);
26
+ graph.addNode(cst.id);
27
+ if (item.cstType === CstType.BASE) {
28
+ analyzer.addBase(cst.alias);
29
+ }
30
+ if (cst.effectiveType) {
31
+ analyzer.setGlobal(cst.alias, cst.effectiveType, cst.analysis.valueClass as ValueClass | null);
32
+ }
33
+ return cst;
34
+ });
35
+
36
+ for (const cst of items) {
37
+ for (const alias of extractGlobals(cst.definition_formal)) {
38
+ const source = cstByAlias.get(alias);
39
+ if (source) {
40
+ graph.addEdge(source.id, cst.id);
41
+ }
42
+ }
43
+ }
44
+
45
+ return {
46
+ id: 0,
47
+ items,
48
+ cstByAlias,
49
+ cstByID,
50
+ graph,
51
+ analyzer,
52
+ inheritance: [],
53
+ attribution: [],
54
+ attribution_graph: graph.clone(),
55
+ oss: [],
56
+ models: [],
57
+ editors: [],
58
+ versions: [],
59
+ is_produced: false,
60
+ is_attributive: false,
61
+ version: 'latest'
62
+ } as unknown as RSForm;
63
+ }
64
+
65
+ /** Enrich session constituents with Portal-style schema metadata used for diagnostics. */
66
+ export function enrichSessionConstituents(session: SessionState): EnrichedConstituenta[] {
67
+ const schema = buildRSFormFromSession(session);
68
+ const normalizedDefinitions = new Map<number, string>();
69
+ const resolveAlias = (id: number) => schema.cstByID.get(id)?.alias ?? String(id);
70
+
71
+ for (const item of session.items) {
72
+ const normalized = normalizeExpression(item.definitionFormal, item.cstType);
73
+ if (normalized) {
74
+ normalizedDefinitions.set(item.id, normalized);
75
+ }
76
+ }
77
+
78
+ assignSchemaDiagnostics(schema.items, resolveAlias, normalizedDefinitions);
79
+
80
+ return session.items.map(item => ({
81
+ item,
82
+ cst: schema.cstByID.get(item.id)!
83
+ }));
84
+ }
85
+
86
+ function toFrontendConstituenta(item: ConstituentaState): Constituenta {
87
+ const effectiveType = (item.analysis.type ?? null) as ExpressionType | null;
88
+ return {
89
+ id: item.id,
90
+ alias: item.alias,
91
+ cst_type: item.cstType,
92
+ definition_formal: item.definitionFormal,
93
+ definition_raw: item.definitionFormal,
94
+ definition_resolved: item.definitionFormal,
95
+ term_raw: item.term,
96
+ term_resolved: item.term,
97
+ term_forms: [],
98
+ convention: item.convention,
99
+ typification_manual: '',
100
+ value_is_property: false,
101
+ crucial: false,
102
+ attributes: [],
103
+ diagnostics: [],
104
+ analysis: {
105
+ success: item.analysis.success,
106
+ type: effectiveType,
107
+ valueClass: item.analysis.valueClass
108
+ },
109
+ effectiveType,
110
+ is_type_mismatch: false,
111
+ schema: 0,
112
+ cst_class: 'derived',
113
+ status: item.analysis.success ? 'verified' : 'incorrect',
114
+ is_template: false,
115
+ is_simple_expression: true,
116
+ parent_schema_index: 0,
117
+ parent_schema: null,
118
+ is_inherited: false,
119
+ has_inherited_children: false,
120
+ spawn: [],
121
+ spawn_alias: []
122
+ } as unknown as Constituenta;
123
+ }
124
+
125
+ export function needsBasicMetadata(item: ConstituentaState): boolean {
126
+ return isBasicConcept(item.cstType) && !isLogical(item.cstType);
127
+ }
@@ -1,4 +1,6 @@
1
+ import { expressionDiagnostic } from './diagnostic-assembly';
1
2
  import { RSLangAnalyzer, type AnalysisFull, type ValueClass } from '@rsconcept/domain/rslang';
3
+ import { type RSErrorDescription } from '@rsconcept/domain/rslang/error';
2
4
  import { getAnalysisFor } from '@rsconcept/domain/library/rsform-api';
3
5
  import { CstType, type RSForm } from '@rsconcept/domain/library/rsform';
4
6
 
@@ -6,33 +8,35 @@ import {
6
8
  type AnalysisResult,
7
9
  type ConstituentaDraft,
8
10
  type ConstituentaState,
9
- type DiagnosticRecord,
11
+ type Diagnostic,
10
12
  type SessionState
11
13
  } from '../models';
12
- import { toPublicAnalysis, toPublicError } from './types';
14
+ import { toPublicAnalysis } from './types';
13
15
 
14
16
  export class SchemaAdapter {
15
17
  public analyzeAgainstSession(
16
18
  session: SessionState,
17
19
  draft: ConstituentaDraft
18
- ): { result: AnalysisResult; diagnostics: DiagnosticRecord[] } {
20
+ ): { result: AnalysisResult; diagnostics: Diagnostic[] } {
19
21
  const analyzer = this.buildAnalyzer(session);
20
22
  const schema = this.toPseudoRSFormState(session, analyzer);
21
23
  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
- });
24
+ const target = { constituentId: draft.id, alias: draft.alias };
25
+ const result = toPublicAnalysis(
26
+ {
27
+ success: analysis.success,
28
+ type: analysis.type as Record<string, unknown> | null,
29
+ valueClass: analysis.valueClass,
30
+ errors: analysis.errors
31
+ },
32
+ draft.definitionFormal,
33
+ target
34
+ );
28
35
  return {
29
36
  result,
30
- diagnostics: analysis.errors.map(error => ({
31
- sessionId: session.sessionId,
32
- constituentId: draft.id,
33
- expression: draft.definitionFormal,
34
- error: toPublicError(error)
35
- }))
37
+ diagnostics: analysis.errors.map(error =>
38
+ expressionDiagnostic(error as RSErrorDescription, draft.definitionFormal, target)
39
+ )
36
40
  };
37
41
  }
38
42
 
@@ -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(
@@ -56,7 +56,7 @@ describe('RSToolAgent', () => {
56
56
  session.sessionId
57
57
  );
58
58
  expect(result.success).toBe(false);
59
- expect(result.failed[0]?.diagnostics[0]?.error.code).toBe(RSErrorCode.definitionNotAllowed);
59
+ expect(result.failed[0]?.diagnostics[0]?.code).toBe(RSErrorCode.definitionNotAllowed);
60
60
  });
61
61
 
62
62
  it('rejects formal definition for basic sets', () => {
@@ -69,7 +69,7 @@ describe('RSToolAgent', () => {
69
69
  session.sessionId
70
70
  );
71
71
  expect(result.success).toBe(false);
72
- expect(result.failed[0]?.diagnostics[0]?.error.code).toBe(RSErrorCode.definitionNotAllowed);
72
+ expect(result.failed[0]?.diagnostics[0]?.code).toBe(RSErrorCode.definitionNotAllowed);
73
73
  });
74
74
 
75
75
  it('returns known analysis for empty base definition', () => {
@@ -474,10 +474,68 @@ describe('RSToolAgent agent ergonomics', () => {
474
474
  { mode: 'best_effort', items: [{ alias: 'X1', cstType: CstType.BASE, definitionFormal: 'Z' }] },
475
475
  session.sessionId
476
476
  );
477
- expect(tool.listDiagnostics(undefined, session.sessionId)).toHaveLength(1);
477
+ const expressionDiags = tool.listDiagnostics({ kind: 'expression' }, session.sessionId);
478
+ expect(expressionDiags).toHaveLength(1);
479
+ expect(expressionDiags[0]?.code).toBe(RSErrorCode.definitionNotAllowed);
478
480
 
479
481
  tool.applySchemaPatch({ items: [{ alias: 'X1', cstType: CstType.BASE, definitionFormal: '' }] }, session.sessionId);
480
- expect(tool.listDiagnostics(undefined, session.sessionId)).toHaveLength(0);
482
+ expect(tool.listDiagnostics({ kind: 'expression' }, session.sessionId)).toHaveLength(0);
483
+ expect(tool.listDiagnostics({ kind: 'schema' }, session.sessionId).length).toBeGreaterThan(0);
484
+ expect(tool.listDiagnostics({ kind: 'model' }, session.sessionId).length).toBeGreaterThan(0);
485
+ });
486
+
487
+ it('reports schema homonyms and missing convention', () => {
488
+ const tool = new RSToolAgent();
489
+ const session = tool.createSession();
490
+ tool.applySchemaPatch(
491
+ {
492
+ items: [
493
+ { alias: 'X1', term: 'person', convention: 'people' },
494
+ { alias: 'X2', term: 'Person', convention: 'other people' }
495
+ ]
496
+ },
497
+ session.sessionId
498
+ );
499
+
500
+ const homonyms = tool.listDiagnostics({ kind: 'schema' }, session.sessionId);
501
+ expect(homonyms.some(record => record.code === RSDiagnosticCode.schemaHomonym)).toBe(true);
502
+ expect(homonyms.filter(record => record.code === RSDiagnosticCode.schemaHomonym)).toHaveLength(2);
503
+ });
504
+
505
+ it('reports formal duplicate definitions', () => {
506
+ const tool = new RSToolAgent();
507
+ const session = tool.createSession();
508
+ tool.applySchemaPatch(
509
+ {
510
+ items: [
511
+ { alias: 'X1', convention: 'base' },
512
+ { alias: 'S1', definitionFormal: 'ℬ(X1×X1)', convention: 'pairs' },
513
+ { alias: 'D1', definitionFormal: 'Pr1(S1)' },
514
+ { alias: 'D2', definitionFormal: 'Pr1(S1)' }
515
+ ]
516
+ },
517
+ session.sessionId
518
+ );
519
+
520
+ const duplicates = tool
521
+ .listDiagnostics({ kind: 'schema' }, session.sessionId)
522
+ .filter(record => record.code === RSDiagnosticCode.schemaFormalDuplicate);
523
+ expect(duplicates).toHaveLength(2);
524
+ expect(duplicates[0]?.params?.[0]).toContain('D2');
525
+ });
526
+
527
+ it('reports model diagnostics for empty basic sets', () => {
528
+ const tool = new RSToolAgent();
529
+ const session = tool.createSession();
530
+ tool.applySchemaPatch(
531
+ {
532
+ items: [{ alias: 'X1', convention: 'base', term: 'element' }]
533
+ },
534
+ session.sessionId
535
+ );
536
+
537
+ const modelIssues = tool.listDiagnostics({ kind: 'model' }, session.sessionId);
538
+ expect(modelIssues.some(record => record.code === RSDiagnosticCode.modelEmpty)).toBe(true);
481
539
  });
482
540
 
483
541
  it('does not record analyzeExpression diagnostics by default', () => {
@@ -919,6 +977,9 @@ describe('RSToolAgent modeling semantics', () => {
919
977
  const a1 = recalculated.items.find(item => item.alias === 'A1');
920
978
  expect(a1?.status).toBe(EvalStatus.AXIOM_FALSE);
921
979
  expect(a1?.value).toBe(0);
980
+
981
+ const modelIssues = tool.listDiagnostics({ kind: 'model' }, session.sessionId);
982
+ expect(modelIssues.some(record => record.code === RSDiagnosticCode.modelAxiomFalse)).toBe(true);
922
983
  });
923
984
 
924
985
  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 { collectSessionDiagnostics } 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';
@@ -191,11 +192,11 @@ export class RSToolAgent implements RSToolAgentContract {
191
192
  definitionFormal: input.expression
192
193
  });
193
194
  if (input.recordDiagnostics) {
194
- this.sessions.replaceDiagnosticsForConstituent(
195
- id,
196
- undefined,
197
- diagnostics.map(item => ({ ...item, constituentId: undefined }))
198
- );
195
+ const refreshed = collectSessionDiagnostics(envelope.state);
196
+ this.sessions.setDiagnostics(id, [
197
+ ...refreshed,
198
+ ...diagnostics.map(item => ({ ...item, constituentId: undefined }))
199
+ ]);
199
200
  }
200
201
  return result;
201
202
  }
@@ -263,9 +264,11 @@ export class RSToolAgent implements RSToolAgentContract {
263
264
  const model = await this.evaluation.setConstituentaValues(state, { items: input.set });
264
265
  state = { ...state, model };
265
266
  this.sessions.replaceState(id, state);
267
+ this.refreshDiagnostics(id);
266
268
  return model;
267
269
  }
268
270
 
271
+ this.refreshDiagnostics(id);
269
272
  return structuredClone(this.sessions.get(id).state.model);
270
273
  } catch (error) {
271
274
  this.sessions.restore(id, snapshot);
@@ -296,8 +299,11 @@ export class RSToolAgent implements RSToolAgentContract {
296
299
 
297
300
  /** @inheritdoc */
298
301
  public recalculateModel(sessionId?: string): RecalculateModelResult {
299
- const envelope = this.sessions.get(this.resolveSessionId(sessionId));
300
- return this.evaluation.recalculateModel(envelope.state);
302
+ const id = this.resolveSessionId(sessionId);
303
+ const envelope = this.sessions.get(id);
304
+ const result = this.evaluation.recalculateModel(envelope.state);
305
+ this.refreshDiagnostics(id);
306
+ return result;
301
307
  }
302
308
 
303
309
  private addOrUpdateConstituenta(
@@ -308,7 +314,6 @@ export class RSToolAgent implements RSToolAgentContract {
308
314
  const envelope = this.sessions.get(id);
309
315
  const { result, diagnostics } = this.adapter.analyzeAgainstSession(envelope.state, input.draft);
310
316
  const state = this.adapter.mergeStateWithDraft(envelope.state, input.draft, result);
311
- this.sessions.replaceDiagnosticsForConstituent(id, input.draft.id, diagnostics);
312
317
  return { state, diagnostics };
313
318
  }
314
319
 
@@ -342,6 +347,7 @@ export class RSToolAgent implements RSToolAgentContract {
342
347
  const envelope = this.sessions.get(id);
343
348
  reorderSessionItemsByDrafts(envelope.state.items, input.drafts, preBatchItemIds);
344
349
  this.sessions.replaceState(id, envelope.state);
350
+ this.refreshDiagnostics(id);
345
351
 
346
352
  return {
347
353
  success: failed.length === 0,
@@ -418,12 +424,13 @@ export class RSToolAgent implements RSToolAgentContract {
418
424
  }
419
425
  const data = parsed as {
420
426
  state: SessionState;
421
- diagnostics?: DiagnosticRecord[];
427
+ diagnostics?: Diagnostic[];
422
428
  };
423
429
  const handle = this.sessions.create(normalizeImportedState(data.state), this.contractVersion);
424
430
  if (data.diagnostics?.length) {
425
431
  this.sessions.setDiagnostics(handle.sessionId, data.diagnostics);
426
432
  }
433
+ this.refreshDiagnostics(handle.sessionId);
427
434
  return this.trackSession(handle);
428
435
  }
429
436
 
@@ -509,4 +516,9 @@ export class RSToolAgent implements RSToolAgentContract {
509
516
  this.sessions.saveCurrentSessionId(handle.sessionId);
510
517
  return handle;
511
518
  }
519
+
520
+ private refreshDiagnostics(sessionId: string): void {
521
+ const envelope = this.sessions.get(sessionId);
522
+ this.sessions.setDiagnostics(sessionId, collectSessionDiagnostics(envelope.state));
523
+ }
512
524
  }
@@ -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';