@conform-ed/qti-react 0.0.12 → 0.0.13

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 (65) hide show
  1. package/dist/index.js +4566 -212
  2. package/package.json +3 -1
  3. package/src/capability.ts +24 -0
  4. package/src/content-model.ts +104 -5
  5. package/src/graphic.ts +103 -0
  6. package/src/index.ts +139 -3
  7. package/src/interactions/associate.ts +22 -0
  8. package/src/interactions/drawing.ts +24 -0
  9. package/src/interactions/end-attempt.ts +19 -0
  10. package/src/interactions/extended-text.ts +21 -0
  11. package/src/interactions/gap-match.ts +22 -0
  12. package/src/interactions/graphic.ts +104 -0
  13. package/src/interactions/hottext.ts +21 -0
  14. package/src/interactions/index.ts +57 -2
  15. package/src/interactions/match.ts +27 -0
  16. package/src/interactions/media.ts +24 -0
  17. package/src/interactions/order.ts +21 -0
  18. package/src/interactions/slider.ts +24 -0
  19. package/src/interactions/upload.ts +19 -0
  20. package/src/normalized-item.ts +561 -0
  21. package/src/pci/index.ts +22 -0
  22. package/src/pci/interaction.ts +42 -0
  23. package/src/pci/markup.ts +102 -0
  24. package/src/pci/mount.ts +135 -0
  25. package/src/pci/registry.ts +240 -0
  26. package/src/pci/response.ts +138 -0
  27. package/src/pci/skin.ts +87 -0
  28. package/src/reference-skin/associate.ts +98 -0
  29. package/src/reference-skin/choice.ts +44 -0
  30. package/src/reference-skin/content.ts +30 -0
  31. package/src/reference-skin/drawing.ts +150 -0
  32. package/src/reference-skin/end-attempt.ts +27 -0
  33. package/src/reference-skin/extended-text.ts +35 -0
  34. package/src/reference-skin/gap-match.ts +69 -0
  35. package/src/reference-skin/graphic-associate.ts +123 -0
  36. package/src/reference-skin/graphic-base.ts +142 -0
  37. package/src/reference-skin/graphic-gap-match.ts +143 -0
  38. package/src/reference-skin/graphic-order.ts +76 -0
  39. package/src/reference-skin/hotspot.ts +43 -0
  40. package/src/reference-skin/hottext.ts +42 -0
  41. package/src/reference-skin/index.ts +75 -0
  42. package/src/reference-skin/inline-choice.ts +42 -0
  43. package/src/reference-skin/match.ts +80 -0
  44. package/src/reference-skin/media.ts +74 -0
  45. package/src/reference-skin/order.ts +79 -0
  46. package/src/reference-skin/position-object.ts +84 -0
  47. package/src/reference-skin/select-point.ts +87 -0
  48. package/src/reference-skin/slider.ts +41 -0
  49. package/src/reference-skin/text-entry.ts +31 -0
  50. package/src/reference-skin/upload.ts +46 -0
  51. package/src/response-processing.ts +178 -29
  52. package/src/rp/evaluate.ts +828 -0
  53. package/src/rp/index.ts +30 -0
  54. package/src/rp/interpreter.ts +251 -0
  55. package/src/rp/template-processing.ts +295 -0
  56. package/src/rp/templates.ts +190 -0
  57. package/src/rp/types.ts +161 -0
  58. package/src/rp/values.ts +198 -0
  59. package/src/runtime.ts +474 -28
  60. package/src/store.ts +155 -5
  61. package/src/test/controller.ts +806 -0
  62. package/src/test/index.ts +25 -0
  63. package/src/test/session-store.ts +244 -0
  64. package/src/test/types.ts +203 -0
  65. package/src/types.ts +27 -1
@@ -0,0 +1,30 @@
1
+ export { collectRpIssues, executeResponseProcessing } from "./interpreter";
2
+ export {
3
+ applyCorrectResponseOverrides,
4
+ collectTemplateIssues,
5
+ executeTemplateProcessing,
6
+ mulberry32,
7
+ type TemplateConditionBranch,
8
+ type TemplateProcessingContext,
9
+ type TemplateProcessingResult,
10
+ type TemplateProcessingView,
11
+ type TemplateRuleView,
12
+ } from "./template-processing";
13
+ export { resolveTemplate } from "./templates";
14
+ export type {
15
+ CustomOperatorImplementation,
16
+ MaybeRpValue,
17
+ OutcomeDeclarationView,
18
+ OutcomeValue,
19
+ ResponseNormalization,
20
+ ResponseProcessingContext,
21
+ ResponseProcessingResult,
22
+ ResponseProcessingView,
23
+ RpConditionBranch,
24
+ RpExpressionView,
25
+ RpRecordField,
26
+ RpRuleView,
27
+ RpScalar,
28
+ RpValue,
29
+ TemplateDeclarationView,
30
+ } from "./types";
@@ -0,0 +1,251 @@
1
+ /**
2
+ * The Response Processing Interpreter (ADR-0004): a pure, deterministic evaluator of
3
+ * the contracts-validated `responseProcessing` tree. Operator coverage grows milestone
4
+ * by milestone; anything outside it aborts execution to the declared outcome defaults
5
+ * and is reported as an `unsupported-rp` Capability issue — never partial scoring.
6
+ */
7
+
8
+ import type { CapabilityIssue } from "../capability";
9
+ import type { ResponseDeclarationView } from "../types";
10
+
11
+ import {
12
+ RpUnsupportedError,
13
+ collectExpressionIssues,
14
+ deterministicExpressionKinds,
15
+ evaluateExpression,
16
+ type EvalEnv,
17
+ } from "./evaluate";
18
+ import { resolveTemplate } from "./templates";
19
+ import type {
20
+ OutcomeDeclarationView,
21
+ ResponseProcessingContext,
22
+ ResponseProcessingResult,
23
+ ResponseProcessingView,
24
+ RpConditionBranch,
25
+ RpRuleView,
26
+ } from "./types";
27
+ import {
28
+ coerceScalar,
29
+ floatValue,
30
+ fromFlatValue,
31
+ fromResponse,
32
+ isNumericBaseType,
33
+ singleBoolean,
34
+ toOutcomeValue,
35
+ type MaybeRpValue,
36
+ } from "./values";
37
+
38
+ const supportedRuleKinds = new Set(["responseCondition", "setOutcomeValue", "exitResponse"]);
39
+
40
+ /**
41
+ * RP additionally supports the random operators: the attempt store always provides a
42
+ * seed-derived source, so they stay deterministic per clone (seed = replay key).
43
+ */
44
+ const rpExpressionKinds = new Set([...deterministicExpressionKinds, "random", "randomInteger", "randomFloat"]);
45
+
46
+ class ExitResponseSignal extends Error {}
47
+
48
+ function defaultOutcomes(declarations: readonly OutcomeDeclarationView[]): Map<string, MaybeRpValue> {
49
+ const outcomes = new Map<string, MaybeRpValue>();
50
+
51
+ for (const declaration of declarations) {
52
+ if (declaration.defaultValue) {
53
+ outcomes.set(declaration.identifier, {
54
+ cardinality: declaration.cardinality,
55
+ baseType: declaration.baseType,
56
+ values: declaration.defaultValue.values.map((entry) => coerceScalar(entry.value, declaration.baseType)),
57
+ });
58
+ continue;
59
+ }
60
+
61
+ // Spec default: numeric outcome variables initialize to 0, everything else to NULL.
62
+ outcomes.set(declaration.identifier, isNumericBaseType(declaration.baseType) ? floatValue(0) : null);
63
+ }
64
+
65
+ return outcomes;
66
+ }
67
+
68
+ export function executeResponseProcessing(
69
+ view: ResponseProcessingView | undefined,
70
+ context: ResponseProcessingContext,
71
+ ): ResponseProcessingResult {
72
+ const issues: CapabilityIssue[] = [];
73
+ const declarationsById = new Map<string, ResponseDeclarationView>(
74
+ context.responseDeclarations.map((declaration) => [declaration.identifier, declaration]),
75
+ );
76
+ const templateDeclarationsById = new Map(
77
+ (context.templateDeclarations ?? []).map((declaration) => [declaration.identifier, declaration]),
78
+ );
79
+
80
+ function initialOutcomes(): Map<string, MaybeRpValue> {
81
+ const outcomes = defaultOutcomes(context.outcomeDeclarations);
82
+
83
+ // Adaptive carry-over: prior outcome values (from earlier attempts in the same
84
+ // item session) replace the declared defaults.
85
+ for (const [identifier, prior] of Object.entries(context.priorOutcomes ?? {})) {
86
+ const declaration = context.outcomeDeclarations.find((entry) => entry.identifier === identifier);
87
+
88
+ outcomes.set(identifier, fromFlatValue(prior, declaration?.cardinality ?? "single", declaration?.baseType));
89
+ }
90
+
91
+ return outcomes;
92
+ }
93
+
94
+ let outcomes = initialOutcomes();
95
+
96
+ let rules: readonly RpRuleView[] = view?.rules ?? [];
97
+
98
+ if (view && !view.rules && view.template) {
99
+ const resolved = resolveTemplate(view.template);
100
+
101
+ if (resolved) {
102
+ rules = resolved;
103
+ } else {
104
+ issues.push({ type: "unsupported-rp", name: view.template, detail: "Unknown response-processing template URI." });
105
+ }
106
+ }
107
+
108
+ const env: EvalEnv = {
109
+ lookupVariable: (identifier) => {
110
+ const declaration = declarationsById.get(identifier);
111
+
112
+ if (declaration) {
113
+ return fromResponse(declaration, context.responses[identifier] ?? null);
114
+ }
115
+
116
+ const templateDeclaration = templateDeclarationsById.get(identifier);
117
+
118
+ if (templateDeclaration) {
119
+ return fromFlatValue(
120
+ context.templateValues?.[identifier] ?? null,
121
+ templateDeclaration.cardinality,
122
+ templateDeclaration.baseType,
123
+ );
124
+ }
125
+
126
+ return outcomes.get(identifier) ?? null;
127
+ },
128
+ responseDeclaration: (identifier) => declarationsById.get(identifier),
129
+ responseValue: (identifier) => context.responses[identifier] ?? null,
130
+ normalization: context.normalization,
131
+ random: context.random,
132
+ customOperators: context.customOperators,
133
+ };
134
+
135
+ function branchTaken(branch: RpConditionBranch): boolean {
136
+ if (singleBoolean(evaluateExpression(branch.expression, env)) !== true) {
137
+ return false;
138
+ }
139
+
140
+ executeRules(branch.rules);
141
+
142
+ return true;
143
+ }
144
+
145
+ function executeRules(rules_: readonly RpRuleView[]): void {
146
+ for (const rule of rules_) {
147
+ if (!supportedRuleKinds.has(rule.kind)) {
148
+ throw new RpUnsupportedError(rule.kind);
149
+ }
150
+
151
+ if (rule.kind === "exitResponse") {
152
+ throw new ExitResponseSignal();
153
+ }
154
+
155
+ if (rule.kind === "setOutcomeValue") {
156
+ if (rule.identifier !== undefined && rule.expression !== undefined) {
157
+ outcomes.set(rule.identifier, evaluateExpression(rule.expression, env));
158
+ }
159
+ continue;
160
+ }
161
+
162
+ // responseCondition
163
+ if (rule.responseIf && branchTaken(rule.responseIf)) {
164
+ continue;
165
+ }
166
+
167
+ const elseIfTaken = (rule.responseElseIfs ?? []).some((branch) => branchTaken(branch));
168
+
169
+ if (!elseIfTaken && rule.responseElse) {
170
+ executeRules(rule.responseElse.rules);
171
+ }
172
+ }
173
+ }
174
+
175
+ try {
176
+ executeRules(rules);
177
+ } catch (error) {
178
+ if (error instanceof RpUnsupportedError) {
179
+ issues.push({ type: "unsupported-rp", name: error.kindName });
180
+ outcomes = initialOutcomes(); // abort, never partial scoring
181
+ } else if (!(error instanceof ExitResponseSignal)) {
182
+ throw error;
183
+ }
184
+ }
185
+
186
+ return {
187
+ outcomes: Object.fromEntries([...outcomes].map(([identifier, value]) => [identifier, toOutcomeValue(value)])),
188
+ issues,
189
+ };
190
+ }
191
+
192
+ export interface RpIssueOptions {
193
+ /** `customOperator` classes the consumer has registered implementations for. */
194
+ readonly customOperatorClasses?: ReadonlySet<string>;
195
+ }
196
+
197
+ /** Static coverage walk for `canDeliver`: reports constructs the interpreter lacks without executing. */
198
+ export function collectRpIssues(
199
+ view: ResponseProcessingView | undefined,
200
+ options?: RpIssueOptions,
201
+ ): readonly CapabilityIssue[] {
202
+ if (!view) {
203
+ return [];
204
+ }
205
+
206
+ const issues: CapabilityIssue[] = [];
207
+ const seen = new Set<string>();
208
+
209
+ function report(name: string, detail?: string): void {
210
+ if (!seen.has(name)) {
211
+ seen.add(name);
212
+ issues.push({ type: "unsupported-rp", name, ...(detail === undefined ? {} : { detail }) });
213
+ }
214
+ }
215
+
216
+ function walkRules(rules: readonly RpRuleView[]): void {
217
+ for (const rule of rules) {
218
+ if (!supportedRuleKinds.has(rule.kind)) {
219
+ report(rule.kind);
220
+ continue;
221
+ }
222
+
223
+ if (rule.expression) {
224
+ collectExpressionIssues(rule.expression, rpExpressionKinds, report, options?.customOperatorClasses);
225
+ }
226
+
227
+ for (const branch of [rule.responseIf, ...(rule.responseElseIfs ?? [])]) {
228
+ if (branch) {
229
+ collectExpressionIssues(branch.expression, rpExpressionKinds, report, options?.customOperatorClasses);
230
+ walkRules(branch.rules);
231
+ }
232
+ }
233
+
234
+ if (rule.responseElse) {
235
+ walkRules(rule.responseElse.rules);
236
+ }
237
+ }
238
+ }
239
+
240
+ if (view.rules) {
241
+ walkRules(view.rules);
242
+ } else if (view.template) {
243
+ const resolved = resolveTemplate(view.template);
244
+
245
+ if (resolved === null) {
246
+ report(view.template, "Unknown response-processing template URI.");
247
+ }
248
+ }
249
+
250
+ return issues;
251
+ }
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Template processing (ADR-0004): the seeded, deterministic engine that produces an
3
+ * item clone. Given the same seed, the same template values and correctResponse
4
+ * overrides come out — replayability survives randomized items because the seed, not
5
+ * the outcome, is what gets stored. Supported rules: setTemplateValue,
6
+ * templateCondition, setCorrectResponse, exitTemplate.
7
+ */
8
+
9
+ import type { CapabilityIssue } from "../capability";
10
+ import type { CorrectResponseView, ResponseDeclarationView } from "../types";
11
+
12
+ import {
13
+ RpUnsupportedError,
14
+ collectExpressionIssues,
15
+ deterministicExpressionKinds,
16
+ evaluateExpression,
17
+ randomExpressionKinds,
18
+ type EvalEnv,
19
+ } from "./evaluate";
20
+ import type { CustomOperatorImplementation, OutcomeValue, RpExpressionView, TemplateDeclarationView } from "./types";
21
+ import { coerceScalar, singleBoolean, toOutcomeValue, type MaybeRpValue } from "./values";
22
+
23
+ export interface TemplateConditionBranch {
24
+ readonly expression: RpExpressionView;
25
+ readonly rules: readonly TemplateRuleView[];
26
+ }
27
+
28
+ export interface TemplateRuleView {
29
+ readonly kind: string;
30
+ readonly identifier?: string;
31
+ readonly expression?: RpExpressionView;
32
+ readonly templateIf?: TemplateConditionBranch;
33
+ readonly templateElseIfs?: readonly TemplateConditionBranch[];
34
+ readonly templateElse?: { readonly rules: readonly TemplateRuleView[] };
35
+ }
36
+
37
+ export interface TemplateProcessingView {
38
+ readonly rules: readonly TemplateRuleView[];
39
+ }
40
+
41
+ export interface TemplateProcessingContext {
42
+ readonly templateDeclarations: readonly TemplateDeclarationView[];
43
+ readonly responseDeclarations: readonly ResponseDeclarationView[];
44
+ readonly seed: number;
45
+ /** Registered vendor `customOperator` implementations by class (opt-in). */
46
+ readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation>>;
47
+ }
48
+
49
+ export interface TemplateProcessingResult {
50
+ readonly templateValues: Readonly<Record<string, OutcomeValue>>;
51
+ /** correctResponse values set by setCorrectResponse, keyed by response identifier. */
52
+ readonly correctResponseOverrides: Readonly<Record<string, CorrectResponseView>>;
53
+ readonly issues: readonly CapabilityIssue[];
54
+ }
55
+
56
+ const supportedTemplateRuleKinds = new Set([
57
+ "setTemplateValue",
58
+ "templateCondition",
59
+ "templateConstraint",
60
+ "setCorrectResponse",
61
+ "exitTemplate",
62
+ ]);
63
+
64
+ const templateExpressionKinds = new Set([...deterministicExpressionKinds, ...randomExpressionKinds]);
65
+
66
+ class ExitTemplateSignal extends Error {}
67
+
68
+ class TemplateConstraintSignal extends Error {}
69
+
70
+ /** Redraw budget before an unsatisfied templateConstraint falls back to defaults. */
71
+ const maxConstraintAttempts = 100;
72
+
73
+ /** mulberry32: a tiny, fast, seeded PRNG — deterministic across platforms. */
74
+ export function mulberry32(seed: number): () => number {
75
+ let state = seed >>> 0;
76
+
77
+ return () => {
78
+ state = (state + 0x6d2b79f5) >>> 0;
79
+ let t = state;
80
+
81
+ t = Math.imul(t ^ (t >>> 15), t | 1);
82
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
83
+
84
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
85
+ };
86
+ }
87
+
88
+ export function executeTemplateProcessing(
89
+ view: TemplateProcessingView | undefined,
90
+ context: TemplateProcessingContext,
91
+ ): TemplateProcessingResult {
92
+ const issues: CapabilityIssue[] = [];
93
+ const declarationsById = new Map(context.templateDeclarations.map((entry) => [entry.identifier, entry]));
94
+ const responseDeclarationsById = new Map(context.responseDeclarations.map((entry) => [entry.identifier, entry]));
95
+ const correctResponseOverrides: Record<string, CorrectResponseView> = {};
96
+
97
+ function initialValues(): Map<string, MaybeRpValue> {
98
+ const values = new Map<string, MaybeRpValue>();
99
+
100
+ for (const declaration of context.templateDeclarations) {
101
+ values.set(
102
+ declaration.identifier,
103
+ declaration.defaultValue
104
+ ? {
105
+ cardinality: declaration.cardinality,
106
+ baseType: declaration.baseType,
107
+ values: declaration.defaultValue.values.map((entry) => coerceScalar(entry.value, declaration.baseType)),
108
+ }
109
+ : null,
110
+ );
111
+ }
112
+
113
+ return values;
114
+ }
115
+
116
+ let templateValues = initialValues();
117
+
118
+ const env: EvalEnv = {
119
+ lookupVariable: (identifier) => templateValues.get(identifier) ?? null,
120
+ responseDeclaration: (identifier) => responseDeclarationsById.get(identifier),
121
+ responseValue: () => null, // no candidate responses exist at template-processing time
122
+ random: mulberry32(context.seed),
123
+ customOperators: context.customOperators,
124
+ };
125
+
126
+ function branchTaken(branch: TemplateConditionBranch): boolean {
127
+ if (singleBoolean(evaluateExpression(branch.expression, env)) !== true) {
128
+ return false;
129
+ }
130
+
131
+ executeRules(branch.rules);
132
+
133
+ return true;
134
+ }
135
+
136
+ function executeRules(rules: readonly TemplateRuleView[]): void {
137
+ for (const rule of rules) {
138
+ if (!supportedTemplateRuleKinds.has(rule.kind)) {
139
+ throw new RpUnsupportedError(rule.kind);
140
+ }
141
+
142
+ if (rule.kind === "exitTemplate") {
143
+ throw new ExitTemplateSignal();
144
+ }
145
+
146
+ if (rule.kind === "setTemplateValue") {
147
+ if (rule.identifier !== undefined && rule.expression !== undefined) {
148
+ const declaration = declarationsById.get(rule.identifier);
149
+ const value = evaluateExpression(rule.expression, env);
150
+
151
+ // Keep the declared typing on the stored value when one exists.
152
+ templateValues.set(
153
+ rule.identifier,
154
+ value === null || !declaration
155
+ ? value
156
+ : {
157
+ cardinality: declaration.cardinality,
158
+ baseType: declaration.baseType ?? value.baseType,
159
+ values: value.values,
160
+ },
161
+ );
162
+ }
163
+ continue;
164
+ }
165
+
166
+ if (rule.kind === "setCorrectResponse") {
167
+ if (rule.identifier !== undefined && rule.expression !== undefined) {
168
+ const value = evaluateExpression(rule.expression, env);
169
+
170
+ if (value !== null) {
171
+ correctResponseOverrides[rule.identifier] = {
172
+ values: value.values.map((member) => ({ value: String(member) })),
173
+ };
174
+ }
175
+ }
176
+ continue;
177
+ }
178
+
179
+ if (rule.kind === "templateConstraint") {
180
+ if (rule.expression !== undefined && singleBoolean(evaluateExpression(rule.expression, env)) !== true) {
181
+ throw new TemplateConstraintSignal();
182
+ }
183
+ continue;
184
+ }
185
+
186
+ // templateCondition
187
+ if (rule.templateIf && branchTaken(rule.templateIf)) {
188
+ continue;
189
+ }
190
+
191
+ const elseIfTaken = (rule.templateElseIfs ?? []).some((branch) => branchTaken(branch));
192
+
193
+ if (!elseIfTaken && rule.templateElse) {
194
+ executeRules(rule.templateElse.rules);
195
+ }
196
+ }
197
+ }
198
+
199
+ for (let attempt = 0; attempt < maxConstraintAttempts; attempt += 1) {
200
+ try {
201
+ executeRules(view?.rules ?? []);
202
+ break;
203
+ } catch (error) {
204
+ if (error instanceof TemplateConstraintSignal) {
205
+ // Unsatisfied constraint: discard the partial clone and redraw — the PRNG
206
+ // advances, so each attempt is a fresh deterministic draw. If the budget
207
+ // runs out, the declared defaults stand (the spec's fallback).
208
+ templateValues = initialValues();
209
+
210
+ for (const key of Object.keys(correctResponseOverrides)) {
211
+ delete correctResponseOverrides[key];
212
+ }
213
+
214
+ continue;
215
+ }
216
+
217
+ if (error instanceof RpUnsupportedError) {
218
+ issues.push({ type: "unsupported-rp", name: error.kindName });
219
+ templateValues = initialValues(); // abort, never a partial clone
220
+ } else if (!(error instanceof ExitTemplateSignal)) {
221
+ throw error;
222
+ }
223
+
224
+ break;
225
+ }
226
+ }
227
+
228
+ return {
229
+ templateValues: Object.fromEntries(
230
+ [...templateValues].map(([identifier, value]) => [identifier, toOutcomeValue(value)]),
231
+ ),
232
+ correctResponseOverrides,
233
+ issues,
234
+ };
235
+ }
236
+
237
+ /** The effective response declarations for a clone: setCorrectResponse overrides applied. */
238
+ export function applyCorrectResponseOverrides(
239
+ declarations: readonly ResponseDeclarationView[],
240
+ overrides: Readonly<Record<string, CorrectResponseView>>,
241
+ ): readonly ResponseDeclarationView[] {
242
+ return declarations.map((declaration) => {
243
+ const override = overrides[declaration.identifier];
244
+
245
+ return override ? { ...declaration, correctResponse: override } : declaration;
246
+ });
247
+ }
248
+
249
+ /** Static coverage walk for `canDeliver` over a templateProcessing tree. */
250
+ export function collectTemplateIssues(
251
+ view: TemplateProcessingView | undefined,
252
+ options?: { readonly customOperatorClasses?: ReadonlySet<string> },
253
+ ): readonly CapabilityIssue[] {
254
+ if (!view) {
255
+ return [];
256
+ }
257
+
258
+ const issues: CapabilityIssue[] = [];
259
+ const seen = new Set<string>();
260
+
261
+ function report(name: string): void {
262
+ if (!seen.has(name)) {
263
+ seen.add(name);
264
+ issues.push({ type: "unsupported-rp", name });
265
+ }
266
+ }
267
+
268
+ function walkRules(rules: readonly TemplateRuleView[]): void {
269
+ for (const rule of rules) {
270
+ if (!supportedTemplateRuleKinds.has(rule.kind)) {
271
+ report(rule.kind);
272
+ continue;
273
+ }
274
+
275
+ if (rule.expression) {
276
+ collectExpressionIssues(rule.expression, templateExpressionKinds, report, options?.customOperatorClasses);
277
+ }
278
+
279
+ for (const branch of [rule.templateIf, ...(rule.templateElseIfs ?? [])]) {
280
+ if (branch) {
281
+ collectExpressionIssues(branch.expression, templateExpressionKinds, report, options?.customOperatorClasses);
282
+ walkRules(branch.rules);
283
+ }
284
+ }
285
+
286
+ if (rule.templateElse) {
287
+ walkRules(rule.templateElse.rules);
288
+ }
289
+ }
290
+ }
291
+
292
+ walkRules(view.rules);
293
+
294
+ return issues;
295
+ }