@cratis/screenplay-language 1.6.4 → 1.7.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.
- package/completion-items.ts +45 -3
- package/completion-planner.ts +12 -1
- package/completions.ts +7 -0
- package/dist/completion-items.d.ts +4 -0
- package/dist/completion-items.d.ts.map +1 -1
- package/dist/completion-items.js +41 -3
- package/dist/completion-items.js.map +1 -1
- package/dist/completion-planner.d.ts.map +1 -1
- package/dist/completion-planner.js +12 -1
- package/dist/completion-planner.js.map +1 -1
- package/dist/completions.d.ts.map +1 -1
- package/dist/completions.js +1 -0
- package/dist/completions.js.map +1 -1
- package/dist/hover-content.d.ts.map +1 -1
- package/dist/hover-content.js +18 -1
- package/dist/hover-content.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/keyword-docs.d.ts.map +1 -1
- package/dist/keyword-docs.js +21 -6
- package/dist/keyword-docs.js.map +1 -1
- package/dist/language.d.ts +2 -0
- package/dist/language.d.ts.map +1 -1
- package/dist/language.js +18 -2
- package/dist/language.js.map +1 -1
- package/dist/symbols.d.ts +15 -1
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +71 -9
- package/dist/symbols.js.map +1 -1
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +1 -0
- package/dist/tokens.js.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +50 -3
- package/dist/validation.js.map +1 -1
- package/hover-content.ts +23 -1
- package/index.ts +5 -1
- package/keyword-docs.ts +21 -6
- package/language.ts +20 -2
- package/package.json +1 -1
- package/symbols.ts +94 -10
- package/tokens.ts +1 -0
- package/validation.ts +90 -3
package/keyword-docs.ts
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
export const keywordDocs: Record<string, string> = {
|
|
5
5
|
domain: 'The domain the file belongs to — optional, at most one per file, before everything else.',
|
|
6
6
|
import: 'Imports a type from another module; it becomes available by its short name.',
|
|
7
|
-
concept: 'A formalized value type wrapping a primitive. Attributes such as `@pii` travel with every usage.',
|
|
7
|
+
concept: 'A formalized value type wrapping a primitive. Attributes such as `@pii` travel with every usage, optionally with a `<attribute> reason "..."` line saying why.',
|
|
8
|
+
type: 'A composite value type — a named shape built from several properties, referenced by events, commands and other types the same way a concept is.',
|
|
9
|
+
reason: 'Records why a concept attribute applies — the purpose, the lawful basis, whose subject the value lives under. Written as `pii reason "..."` in the concept body.',
|
|
10
|
+
identifier: 'Marks the command property a runtime resolves the event source id from. At most one per command; without it a new Uuid is generated.',
|
|
8
11
|
policy: 'A named authorization rule referenced by commands and queries. Multiple policies AND together.',
|
|
9
12
|
persona: 'A named role interacting with the application, with an optional description and its associated policies.',
|
|
10
13
|
authentication: 'Declares the identity providers of the application — at most one block per file.',
|
|
@@ -17,10 +20,12 @@ export const keywordDocs: Record<string, string> = {
|
|
|
17
20
|
slice: 'The atomic unit of behavior, aligned with Event Modeling. Has a type and a name.',
|
|
18
21
|
event: 'An event type — an immutable, past-tense fact. Properties use concepts or primitives.',
|
|
19
22
|
command: 'An imperative intent. Declares authorization, validation, and what events it produces — declaratively, or via a full handler.',
|
|
20
|
-
query: 'A read-side entry point mapping to a return type.',
|
|
23
|
+
query: 'A read-side entry point mapping to a return type, with an optional description, parameters and performer.',
|
|
24
|
+
performer: 'The code that performs a query — a `file <Path>` reference or an inline `csharp`/`sql` block. The query\'s counterpart to a command handler.',
|
|
25
|
+
from: 'Fills a query parameter from the query context instead of the caller, as in `filter tenantId TenantId from $context.tenant`.',
|
|
21
26
|
projection: 'Declares how events project into a read model, using the embedded Projection Declaration Language (PDL).',
|
|
22
27
|
capture: 'Converts external data into events, using the embedded Change Data Capture Language (CDL).',
|
|
23
|
-
reactor: 'Reacts to events and produces side effects. Lives inside Automation slices.',
|
|
28
|
+
reactor: 'Reacts to events and produces side effects. Lives inside Automation slices. A trigger with no body states intent on its own — `file`/code is optional realization metadata.',
|
|
24
29
|
screen: 'A UI declaration. Supports intent, structure, and layout-with-code levels, or a full file reference.',
|
|
25
30
|
constraint: 'A server-side rule enforced in the Chronicle kernel before events are committed.',
|
|
26
31
|
authorize: 'References the policies that must all pass for the construct to execute.',
|
|
@@ -70,6 +75,7 @@ export const keywordDocs: Record<string, string> = {
|
|
|
70
75
|
and: 'Both conditions must hold.',
|
|
71
76
|
or: 'Either condition may hold.',
|
|
72
77
|
csharp: 'An inline C# block between triple backticks.',
|
|
78
|
+
sql: 'An inline SQL block between triple backticks — a query performer written against a relational store.',
|
|
73
79
|
typescript: 'An inline TypeScript block between triple backticks.',
|
|
74
80
|
react: 'An inline React/TSX block; receives the surrounding data contract as `Props`.',
|
|
75
81
|
html: 'An inline static HTML block.',
|
|
@@ -88,12 +94,21 @@ export const keywordDocs: Record<string, string> = {
|
|
|
88
94
|
};
|
|
89
95
|
|
|
90
96
|
export const attributeDocs: Record<string, string> = {
|
|
91
|
-
pii: '`@pii` — the value is personally identifiable information; Chronicle manages and erases it automatically wherever the concept is used.',
|
|
92
|
-
sensitive: '`@sensitive` — the value is sensitive; Chronicle applies its sensitivity rules automatically wherever the concept is used.',
|
|
97
|
+
pii: '`@pii` — the value is personally identifiable information; Chronicle manages and erases it automatically wherever the concept is used. Add `pii reason "..."` in the concept body to record why.',
|
|
98
|
+
sensitive: '`@sensitive` — the value is sensitive; Chronicle applies its sensitivity rules automatically wherever the concept is used. Add `sensitive reason "..."` in the concept body to record why.',
|
|
93
99
|
};
|
|
94
100
|
|
|
95
101
|
export const contextVariableDocs: Record<string, string> = {
|
|
96
|
-
'$context.occurred': '
|
|
102
|
+
'$context.occurred': 'When the command or query was received.',
|
|
103
|
+
'$context.tenant': 'The tenant the command or query is executing for.',
|
|
104
|
+
'$context.command': 'The command being handled, shaped by its `command` declaration.',
|
|
105
|
+
'$context.arguments': 'The arguments of the query being performed.',
|
|
106
|
+
'$context.causedBy': 'The identity that caused the command or query — `subject`, `name`, `userName`.',
|
|
107
|
+
'$context.causedBy.subject': 'The subject of the identity that caused the command or query.',
|
|
108
|
+
'$context.causedBy.name': 'The display name of the identity that caused the command or query.',
|
|
109
|
+
'$context.causedBy.userName': 'The user name of the identity that caused the command or query.',
|
|
110
|
+
'$context.causation': 'What caused the command or query — `type`, `occurred`, `properties`.',
|
|
111
|
+
'$context.causation.type': 'What caused the command or query — a command, a reactor, a schedule.',
|
|
97
112
|
'$context.identity.id': 'The subject of the caller from the auth token.',
|
|
98
113
|
'$env': 'An environment variable, e.g. `$env.SERVICE_NAME`.',
|
|
99
114
|
'$secrets': 'An encrypted secret resolved at runtime, e.g. `$secrets.azureAdClientId` — stays symbolic in the compiled tree.',
|
package/language.ts
CHANGED
|
@@ -18,6 +18,7 @@ export const constructKeywords = [
|
|
|
18
18
|
'domain',
|
|
19
19
|
'import',
|
|
20
20
|
'concept',
|
|
21
|
+
'type',
|
|
21
22
|
'policy',
|
|
22
23
|
'persona',
|
|
23
24
|
'authentication',
|
|
@@ -47,6 +48,10 @@ export const clauseKeywords = [
|
|
|
47
48
|
'validate',
|
|
48
49
|
'produces',
|
|
49
50
|
'handler',
|
|
51
|
+
'performer',
|
|
52
|
+
'identifier',
|
|
53
|
+
'reason',
|
|
54
|
+
'from',
|
|
50
55
|
'when',
|
|
51
56
|
'given',
|
|
52
57
|
'then',
|
|
@@ -92,7 +97,20 @@ export const clauseKeywords = [
|
|
|
92
97
|
'provider',
|
|
93
98
|
];
|
|
94
99
|
|
|
95
|
-
export const codeBlockTags = ['csharp', 'typescript', 'react', 'html'];
|
|
100
|
+
export const codeBlockTags = ['csharp', 'typescript', 'react', 'html', 'sql'];
|
|
101
|
+
|
|
102
|
+
// The roots a `$context.` path can start with — the members of CommandContext and QueryContext.
|
|
103
|
+
export const contextRoots = [
|
|
104
|
+
'command',
|
|
105
|
+
'arguments',
|
|
106
|
+
'tenant',
|
|
107
|
+
'causedBy',
|
|
108
|
+
'causation',
|
|
109
|
+
'occurred',
|
|
110
|
+
'identity',
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
export const causedByProperties = ['subject', 'name', 'userName'];
|
|
96
114
|
|
|
97
115
|
export const sliceTypes = ['StateChange', 'StateView', 'Automation', 'Translate'];
|
|
98
116
|
|
|
@@ -123,7 +141,7 @@ export const languageConfiguration: languages.LanguageConfiguration = {
|
|
|
123
141
|
},
|
|
124
142
|
indentationRules: {
|
|
125
143
|
increaseIndentPattern:
|
|
126
|
-
/^\s*(module|feature|slice|policy|persona|authentication|provider|event|command|screen|projection|capture|reactor|constraint|layout|template|validate|produces|handler|section|concurrency|seed|for)\b.*$/,
|
|
144
|
+
/^\s*(module|feature|slice|policy|persona|authentication|provider|event|command|query|type|screen|projection|capture|reactor|constraint|layout|template|validate|produces|handler|performer|section|concurrency|seed|for)\b.*$/,
|
|
127
145
|
// Dedents are always explicit in an offside language — never auto-dedent.
|
|
128
146
|
decreaseIndentPattern: /(?!)/,
|
|
129
147
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cratis/screenplay-language",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Monaco language service for the Cratis Screenplay DSL — syntax highlighting, IntelliSense, hover and diagnostics for .play files",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/symbols.ts
CHANGED
|
@@ -6,16 +6,25 @@ import { fenceMap, indentOf } from './document-context';
|
|
|
6
6
|
export interface PropertySymbol {
|
|
7
7
|
name: string;
|
|
8
8
|
type: string;
|
|
9
|
+
isIdentifier: boolean;
|
|
10
|
+
line: number;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export interface ConceptSymbol {
|
|
12
14
|
name: string;
|
|
13
15
|
primitive: string;
|
|
14
16
|
attributes: string[];
|
|
17
|
+
attributeReasons: Record<string, string>;
|
|
15
18
|
enumValues: string[];
|
|
16
19
|
line: number;
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
export interface TypeSymbol {
|
|
23
|
+
name: string;
|
|
24
|
+
properties: PropertySymbol[];
|
|
25
|
+
line: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
export interface PolicySymbol {
|
|
20
29
|
name: string;
|
|
21
30
|
requires: string[];
|
|
@@ -28,6 +37,10 @@ export interface EventSymbol {
|
|
|
28
37
|
line: number;
|
|
29
38
|
}
|
|
30
39
|
|
|
40
|
+
export interface CommandSymbol extends NamedSymbol {
|
|
41
|
+
properties: PropertySymbol[];
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
export interface NamedSymbol {
|
|
32
45
|
name: string;
|
|
33
46
|
line: number;
|
|
@@ -35,6 +48,7 @@ export interface NamedSymbol {
|
|
|
35
48
|
|
|
36
49
|
export interface QuerySymbol extends NamedSymbol {
|
|
37
50
|
returnType: string;
|
|
51
|
+
parameters: PropertySymbol[];
|
|
38
52
|
}
|
|
39
53
|
|
|
40
54
|
export interface ImportSymbol {
|
|
@@ -46,15 +60,31 @@ export interface ImportSymbol {
|
|
|
46
60
|
export interface DocumentSymbols {
|
|
47
61
|
imports: ImportSymbol[];
|
|
48
62
|
concepts: ConceptSymbol[];
|
|
63
|
+
types: TypeSymbol[];
|
|
49
64
|
policies: PolicySymbol[];
|
|
50
65
|
events: EventSymbol[];
|
|
51
|
-
commands:
|
|
66
|
+
commands: CommandSymbol[];
|
|
52
67
|
queries: QuerySymbol[];
|
|
53
68
|
screens: NamedSymbol[];
|
|
54
69
|
}
|
|
55
70
|
|
|
56
71
|
const conceptPattern = /^concept\s+(\w+)\s*:\s*(\w+)((?:\s+@\w+)*)\s*$/;
|
|
57
|
-
const propertyPattern = /^\s*([a-z_]\w*)\s+([\w.]+(?:\[\])?\??)\s*$/;
|
|
72
|
+
const propertyPattern = /^\s*(@?[a-z_]\w*)\s+([\w.]+(?:\[\])?\??)(\s+identifier)?\s*$/;
|
|
73
|
+
const attributeReasonPattern = /^([a-z_]\w*)\s+reason\s+"((?:[^"\\]|\\.)*)"\s*$/;
|
|
74
|
+
const queryParameterPattern =
|
|
75
|
+
/^\s*(?:by|filter)\s+([a-z_]\w*)\s+([\w.]+(?:\[\])?\??)(?:\s+from\s+.+)?\s*$/;
|
|
76
|
+
|
|
77
|
+
function propertiesIn(lines: string[], body: number[]): PropertySymbol[] {
|
|
78
|
+
return body
|
|
79
|
+
.map((index) => ({ index, match: lines[index].match(propertyPattern) }))
|
|
80
|
+
.filter((entry): entry is { index: number; match: RegExpMatchArray } => entry.match !== null)
|
|
81
|
+
.map(({ index, match }) => ({
|
|
82
|
+
name: match[1].replace(/^@/, ''),
|
|
83
|
+
type: match[2],
|
|
84
|
+
isIdentifier: match[3] !== undefined,
|
|
85
|
+
line: index,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
58
88
|
|
|
59
89
|
function collectBody(lines: string[], fences: boolean[], start: number, indent: number): number[] {
|
|
60
90
|
const body: number[] = [];
|
|
@@ -75,6 +105,7 @@ export function scanDocument(lines: string[]): DocumentSymbols {
|
|
|
75
105
|
const symbols: DocumentSymbols = {
|
|
76
106
|
imports: [],
|
|
77
107
|
concepts: [],
|
|
108
|
+
types: [],
|
|
78
109
|
policies: [],
|
|
79
110
|
events: [],
|
|
80
111
|
commands: [],
|
|
@@ -99,20 +130,39 @@ export function scanDocument(lines: string[]): DocumentSymbols {
|
|
|
99
130
|
|
|
100
131
|
const conceptMatch = trimmed.match(conceptPattern);
|
|
101
132
|
if (conceptMatch) {
|
|
133
|
+
const body = collectBody(lines, fences, index, indent).map((i) => lines[i].trim());
|
|
134
|
+
const attributeReasons: Record<string, string> = {};
|
|
135
|
+
for (const line of body) {
|
|
136
|
+
const reason = line.match(attributeReasonPattern);
|
|
137
|
+
if (reason && attributeReasons[reason[1]] === undefined) {
|
|
138
|
+
attributeReasons[reason[1]] = reason[2];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
102
141
|
const enumValues =
|
|
103
142
|
conceptMatch[2] === 'Enum'
|
|
104
|
-
?
|
|
143
|
+
? body.filter((line) => !attributeReasonPattern.test(line))
|
|
105
144
|
: [];
|
|
106
145
|
symbols.concepts.push({
|
|
107
146
|
name: conceptMatch[1],
|
|
108
147
|
primitive: conceptMatch[2],
|
|
109
148
|
attributes: conceptMatch[3].trim().split(/\s+/).filter(Boolean),
|
|
149
|
+
attributeReasons,
|
|
110
150
|
enumValues,
|
|
111
151
|
line: index,
|
|
112
152
|
});
|
|
113
153
|
continue;
|
|
114
154
|
}
|
|
115
155
|
|
|
156
|
+
const typeMatch = trimmed.match(/^type\s+(\w+)\s*$/);
|
|
157
|
+
if (typeMatch) {
|
|
158
|
+
symbols.types.push({
|
|
159
|
+
name: typeMatch[1],
|
|
160
|
+
properties: propertiesIn(lines, collectBody(lines, fences, index, indent)),
|
|
161
|
+
line: index,
|
|
162
|
+
});
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
116
166
|
const policyMatch = trimmed.match(/^policy\s+(\w+)\s*$/);
|
|
117
167
|
if (policyMatch) {
|
|
118
168
|
const requires = collectBody(lines, fences, index, indent)
|
|
@@ -124,23 +174,46 @@ export function scanDocument(lines: string[]): DocumentSymbols {
|
|
|
124
174
|
|
|
125
175
|
const eventMatch = trimmed.match(/^event\s+(\w+)\s*$/);
|
|
126
176
|
if (eventMatch) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
177
|
+
symbols.events.push({
|
|
178
|
+
name: eventMatch[1],
|
|
179
|
+
properties: propertiesIn(lines, collectBody(lines, fences, index, indent)),
|
|
180
|
+
line: index,
|
|
181
|
+
});
|
|
132
182
|
continue;
|
|
133
183
|
}
|
|
134
184
|
|
|
135
185
|
const commandMatch = trimmed.match(/^command\s+(\w+)\s*$/);
|
|
136
186
|
if (commandMatch) {
|
|
137
|
-
symbols.commands.push({
|
|
187
|
+
symbols.commands.push({
|
|
188
|
+
name: commandMatch[1],
|
|
189
|
+
properties: propertiesIn(lines, collectBody(lines, fences, index, indent)),
|
|
190
|
+
line: index,
|
|
191
|
+
});
|
|
138
192
|
continue;
|
|
139
193
|
}
|
|
140
194
|
|
|
141
195
|
const queryMatch = trimmed.match(/^query\s+(\w+)\s*=>\s*([\w[\]]+)\s*$/);
|
|
142
196
|
if (queryMatch) {
|
|
143
|
-
|
|
197
|
+
// The return type names a read model, which no construct declares — only the
|
|
198
|
+
// 'by' and 'filter' parameters resolve against the document's own types.
|
|
199
|
+
const parameters = collectBody(lines, fences, index, indent)
|
|
200
|
+
.map((i) => ({ index: i, match: lines[i].match(queryParameterPattern) }))
|
|
201
|
+
.filter(
|
|
202
|
+
(entry): entry is { index: number; match: RegExpMatchArray } =>
|
|
203
|
+
entry.match !== null,
|
|
204
|
+
)
|
|
205
|
+
.map(({ index: line, match }) => ({
|
|
206
|
+
name: match[1],
|
|
207
|
+
type: match[2],
|
|
208
|
+
isIdentifier: false,
|
|
209
|
+
line,
|
|
210
|
+
}));
|
|
211
|
+
symbols.queries.push({
|
|
212
|
+
name: queryMatch[1],
|
|
213
|
+
returnType: queryMatch[2],
|
|
214
|
+
parameters,
|
|
215
|
+
line: index,
|
|
216
|
+
});
|
|
144
217
|
continue;
|
|
145
218
|
}
|
|
146
219
|
|
|
@@ -159,3 +232,14 @@ export function knownEventNames(symbols: DocumentSymbols): string[] {
|
|
|
159
232
|
...symbols.imports.map((imported) => imported.shortName),
|
|
160
233
|
];
|
|
161
234
|
}
|
|
235
|
+
|
|
236
|
+
// Everything a property type reference can resolve to — the primitives, the declared
|
|
237
|
+
// concepts and types, and the short names of the imports.
|
|
238
|
+
export function knownTypeNames(symbols: DocumentSymbols, primitives: readonly string[]): string[] {
|
|
239
|
+
return [
|
|
240
|
+
...primitives,
|
|
241
|
+
...symbols.concepts.map((concept) => concept.name),
|
|
242
|
+
...symbols.types.map((type) => type.name),
|
|
243
|
+
...symbols.imports.map((imported) => imported.shortName),
|
|
244
|
+
];
|
|
245
|
+
}
|
package/tokens.ts
CHANGED
package/validation.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
// Copyright (c) Cratis. All rights reserved.
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
|
-
import { primitiveTypes, sliceTypes } from './language';
|
|
4
|
+
import { causedByProperties, contextRoots, primitiveTypes, sliceTypes } from './language';
|
|
5
5
|
import { fenceMap, indentOf } from './document-context';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
DocumentSymbols,
|
|
8
|
+
PropertySymbol,
|
|
9
|
+
knownEventNames,
|
|
10
|
+
knownTypeNames,
|
|
11
|
+
scanDocument,
|
|
12
|
+
} from './symbols';
|
|
7
13
|
|
|
8
14
|
export type ValidationSeverity = 'error' | 'warning';
|
|
9
15
|
|
|
@@ -36,6 +42,69 @@ function tokenIssue(
|
|
|
36
42
|
return issue(severity, line, column, token.length, message);
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
// Reports every property whose type reference names nothing the document declares, and
|
|
46
|
+
// every command that marks more than one property as its identifier.
|
|
47
|
+
function validateDeclarations(lines: string[], symbols: DocumentSymbols): ValidationIssue[] {
|
|
48
|
+
const issues: ValidationIssue[] = [];
|
|
49
|
+
const types = new Set(knownTypeNames(symbols, primitiveTypes));
|
|
50
|
+
|
|
51
|
+
const checkProperties = (properties: PropertySymbol[], owner: string) => {
|
|
52
|
+
for (const property of properties.filter(
|
|
53
|
+
(candidate) => !types.has(candidate.type.replace(/[[\]?]/g, '')),
|
|
54
|
+
)) {
|
|
55
|
+
const bare = property.type.replace(/[[\]?]/g, '');
|
|
56
|
+
issues.push(
|
|
57
|
+
tokenIssue(
|
|
58
|
+
'warning',
|
|
59
|
+
property.line,
|
|
60
|
+
lines[property.line],
|
|
61
|
+
property.type,
|
|
62
|
+
`Unknown type '${bare}' on '${property.name}' of ${owner} — declare it with 'concept ${bare} : <Primitive>' or 'type ${bare}'.`,
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
for (const type of symbols.types) checkProperties(type.properties, `type '${type.name}'`);
|
|
69
|
+
for (const event of symbols.events) checkProperties(event.properties, `event '${event.name}'`);
|
|
70
|
+
for (const query of symbols.queries) checkProperties(query.parameters, `query '${query.name}'`);
|
|
71
|
+
for (const command of symbols.commands) {
|
|
72
|
+
checkProperties(command.properties, `command '${command.name}'`);
|
|
73
|
+
const identifiers = command.properties.filter((property) => property.isIdentifier);
|
|
74
|
+
for (const extra of identifiers.slice(1)) {
|
|
75
|
+
issues.push(
|
|
76
|
+
tokenIssue(
|
|
77
|
+
'error',
|
|
78
|
+
extra.line,
|
|
79
|
+
lines[extra.line],
|
|
80
|
+
'identifier',
|
|
81
|
+
`Command '${command.name}' already marks '${identifiers[0].name}' as identifier — only one property can be the identifier.`,
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const declared = new Map<string, number>();
|
|
88
|
+
for (const declaration of [...symbols.concepts, ...symbols.types]) {
|
|
89
|
+
const first = declared.get(declaration.name);
|
|
90
|
+
if (first === undefined) {
|
|
91
|
+
declared.set(declaration.name, declaration.line);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
issues.push(
|
|
95
|
+
tokenIssue(
|
|
96
|
+
'error',
|
|
97
|
+
declaration.line,
|
|
98
|
+
lines[declaration.line],
|
|
99
|
+
declaration.name,
|
|
100
|
+
`Duplicate declaration of '${declaration.name}' — concept and type names must be unique.`,
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return issues;
|
|
106
|
+
}
|
|
107
|
+
|
|
39
108
|
// Validates a Screenplay document without any editor dependency — both the Monaco
|
|
40
109
|
// service and the VSCode extension adapt these issues to their marker/diagnostic APIs.
|
|
41
110
|
export function validateLines(lines: string[]): ValidationIssue[] {
|
|
@@ -43,7 +112,7 @@ export function validateLines(lines: string[]): ValidationIssue[] {
|
|
|
43
112
|
const symbols = scanDocument(lines);
|
|
44
113
|
const events = new Set(knownEventNames(symbols));
|
|
45
114
|
const policies = new Set(symbols.policies.map((policy) => policy.name));
|
|
46
|
-
const issues: ValidationIssue[] =
|
|
115
|
+
const issues: ValidationIssue[] = validateDeclarations(lines, symbols);
|
|
47
116
|
|
|
48
117
|
const checkEvent = (line: number, text: string, name: string) => {
|
|
49
118
|
if (!events.has(name)) {
|
|
@@ -131,6 +200,24 @@ export function validateLines(lines: string[]): ValidationIssue[] {
|
|
|
131
200
|
|
|
132
201
|
const uniqueProperty = trimmed.match(/^unique\s+[a-z_]\w*\s+on\s+([A-Z]\w*)/);
|
|
133
202
|
if (uniqueProperty) checkEvent(index, line, uniqueProperty[1]);
|
|
203
|
+
|
|
204
|
+
// Every $context. path must name something CommandContext or QueryContext carries.
|
|
205
|
+
for (const path of line.match(/\$context\.[\w.]+/g) ?? []) {
|
|
206
|
+
const segments = path.substring('$context.'.length).split('.');
|
|
207
|
+
if (!contextRoots.includes(segments[0])) {
|
|
208
|
+
issues.push(
|
|
209
|
+
tokenIssue('warning', index, line, path, `Unknown $context path '${segments.join('.')}' — expected one of ${contextRoots.join(', ')}.`),
|
|
210
|
+
);
|
|
211
|
+
} else if (
|
|
212
|
+
segments[0] === 'causedBy' &&
|
|
213
|
+
segments.length > 1 &&
|
|
214
|
+
!causedByProperties.includes(segments[1])
|
|
215
|
+
) {
|
|
216
|
+
issues.push(
|
|
217
|
+
tokenIssue('warning', index, line, path, `Unknown $context.causedBy property '${segments[1]}' — expected ${causedByProperties.join(', ')}.`),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
134
221
|
}
|
|
135
222
|
|
|
136
223
|
const fenceLines = lines
|