@iyulab/m3l 0.1.4 → 0.5.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/dist/reader.d.ts DELETED
@@ -1,21 +0,0 @@
1
- export interface M3LFile {
2
- path: string;
3
- content: string;
4
- }
5
- /**
6
- * Read M3L files from a path (file or directory).
7
- * If path is a directory, scans for **\/*.m3l.md and **\/*.m3l files.
8
- * If an m3l.config.yaml exists in the directory, uses its sources patterns.
9
- */
10
- export declare function readM3LFiles(inputPath: string): Promise<M3LFile[]>;
11
- /**
12
- * Wrap a string content as an M3LFile.
13
- */
14
- export declare function readM3LString(content: string, filename?: string): M3LFile;
15
- /**
16
- * Read project config from m3l.config.yaml if it exists.
17
- */
18
- export declare function readProjectConfig(dirPath: string): Promise<{
19
- name?: string;
20
- version?: string;
21
- } | null>;
package/dist/reader.js DELETED
@@ -1,84 +0,0 @@
1
- import { readFileSync, statSync, existsSync } from 'fs';
2
- import { join, resolve as resolvePath } from 'path';
3
- import fg from 'fast-glob';
4
- /**
5
- * Read M3L files from a path (file or directory).
6
- * If path is a directory, scans for **\/*.m3l.md and **\/*.m3l files.
7
- * If an m3l.config.yaml exists in the directory, uses its sources patterns.
8
- */
9
- export async function readM3LFiles(inputPath) {
10
- const resolved = resolvePath(inputPath);
11
- const stat = statSync(resolved);
12
- if (stat.isFile()) {
13
- return [readSingleFile(resolved)];
14
- }
15
- if (stat.isDirectory()) {
16
- // Check for m3l.config.yaml
17
- const configPath = join(resolved, 'm3l.config.yaml');
18
- if (existsSync(configPath)) {
19
- return readFromConfig(configPath, resolved);
20
- }
21
- // Default: scan for all .m3l.md files
22
- return scanDirectory(resolved);
23
- }
24
- throw new Error(`Path is neither a file nor a directory: ${resolved}`);
25
- }
26
- /**
27
- * Wrap a string content as an M3LFile.
28
- */
29
- export function readM3LString(content, filename = 'inline.m3l.md') {
30
- return { path: filename, content };
31
- }
32
- function readSingleFile(filePath) {
33
- const content = readFileSync(filePath, 'utf-8');
34
- return { path: filePath, content };
35
- }
36
- async function scanDirectory(dirPath) {
37
- const pattern = '**/*.{m3l.md,m3l}';
38
- const files = await fg(pattern, {
39
- cwd: dirPath,
40
- absolute: true,
41
- onlyFiles: true,
42
- });
43
- return files.sort().map(f => readSingleFile(f));
44
- }
45
- async function readFromConfig(configPath, baseDir) {
46
- const yamlContent = readFileSync(configPath, 'utf-8');
47
- // Dynamic import yaml to parse config
48
- const { parse: parseYaml } = await import('yaml');
49
- const config = parseYaml(yamlContent);
50
- if (!config.sources || config.sources.length === 0) {
51
- return scanDirectory(baseDir);
52
- }
53
- const allFiles = [];
54
- const seen = new Set();
55
- for (const pattern of config.sources) {
56
- const files = await fg(pattern, {
57
- cwd: baseDir,
58
- absolute: true,
59
- onlyFiles: true,
60
- });
61
- for (const f of files.sort()) {
62
- if (!seen.has(f)) {
63
- seen.add(f);
64
- allFiles.push(readSingleFile(f));
65
- }
66
- }
67
- }
68
- return allFiles;
69
- }
70
- /**
71
- * Read project config from m3l.config.yaml if it exists.
72
- */
73
- export async function readProjectConfig(dirPath) {
74
- const configPath = join(resolvePath(dirPath), 'm3l.config.yaml');
75
- if (!existsSync(configPath))
76
- return null;
77
- const yamlContent = readFileSync(configPath, 'utf-8');
78
- const { parse: parseYaml } = await import('yaml');
79
- const config = parseYaml(yamlContent);
80
- return {
81
- name: config?.name,
82
- version: config?.version,
83
- };
84
- }
@@ -1,10 +0,0 @@
1
- import type { ParsedFile, M3LAST, ProjectInfo } from './types.js';
2
- /** AST schema version — bump major on breaking structure changes */
3
- export declare const AST_VERSION = "1.0";
4
- /** Parser package version — kept in sync with package.json */
5
- export declare const PARSER_VERSION = "0.1.4";
6
- /**
7
- * Resolve and merge multiple parsed file ASTs into a single M3LAST.
8
- * Handles: inheritance resolution, duplicate detection, reference validation.
9
- */
10
- export declare function resolve(files: ParsedFile[], project?: ProjectInfo): M3LAST;
package/dist/resolver.js DELETED
@@ -1,192 +0,0 @@
1
- /** AST schema version — bump major on breaking structure changes */
2
- export const AST_VERSION = '1.0';
3
- /** Parser package version — kept in sync with package.json */
4
- export const PARSER_VERSION = '0.1.4';
5
- /**
6
- * Resolve and merge multiple parsed file ASTs into a single M3LAST.
7
- * Handles: inheritance resolution, duplicate detection, reference validation.
8
- */
9
- export function resolve(files, project) {
10
- const errors = [];
11
- const warnings = [];
12
- // Collect all elements from all files
13
- const allModels = [];
14
- const allEnums = [];
15
- const allInterfaces = [];
16
- const allViews = [];
17
- const allAttrRegistry = [];
18
- const sources = [];
19
- for (const file of files) {
20
- sources.push(file.source);
21
- allModels.push(...file.models);
22
- allEnums.push(...file.enums);
23
- allInterfaces.push(...file.interfaces);
24
- allViews.push(...file.views);
25
- if (file.attributeRegistry)
26
- allAttrRegistry.push(...file.attributeRegistry);
27
- }
28
- // Build name maps
29
- const modelMap = new Map();
30
- const enumMap = new Map();
31
- const interfaceMap = new Map();
32
- const allNamedMap = new Map();
33
- // Check for duplicate model names
34
- for (const model of allModels) {
35
- checkDuplicate(model.name, 'model', model, modelMap, allNamedMap, errors);
36
- modelMap.set(model.name, model);
37
- allNamedMap.set(model.name, { type: 'model', loc: { file: model.source, line: model.line } });
38
- }
39
- for (const en of allEnums) {
40
- checkDuplicate(en.name, 'enum', en, enumMap, allNamedMap, errors);
41
- enumMap.set(en.name, en);
42
- allNamedMap.set(en.name, { type: 'enum', loc: { file: en.source, line: en.line } });
43
- }
44
- for (const iface of allInterfaces) {
45
- checkDuplicate(iface.name, 'interface', iface, interfaceMap, allNamedMap, errors);
46
- interfaceMap.set(iface.name, iface);
47
- allNamedMap.set(iface.name, { type: 'interface', loc: { file: iface.source, line: iface.line } });
48
- }
49
- for (const view of allViews) {
50
- allNamedMap.set(view.name, { type: 'view', loc: { file: view.source, line: view.line } });
51
- }
52
- // Resolve inheritance
53
- for (const model of allModels) {
54
- resolveInheritance(model, modelMap, interfaceMap, allNamedMap, errors);
55
- }
56
- // Check duplicate field names within each model
57
- for (const model of [...allModels, ...allViews]) {
58
- checkDuplicateFields(model, errors);
59
- }
60
- // Tag isRegistered on attributes matching the registry
61
- if (allAttrRegistry.length > 0) {
62
- const registeredNames = new Set(allAttrRegistry.map(r => r.name));
63
- const tagAttrs = (attrs) => {
64
- for (const a of attrs) {
65
- if (registeredNames.has(a.name)) {
66
- a.isRegistered = true;
67
- }
68
- }
69
- };
70
- const tagModel = (m) => {
71
- tagAttrs(m.attributes);
72
- for (const f of m.fields) {
73
- tagAttrs(f.attributes);
74
- }
75
- };
76
- for (const m of allModels)
77
- tagModel(m);
78
- for (const v of allViews)
79
- tagModel(v);
80
- for (const i of allInterfaces)
81
- tagModel(i);
82
- }
83
- // Detect namespace from first file if available
84
- const projectInfo = project || {};
85
- if (!projectInfo.name) {
86
- const ns = files.find(f => f.namespace)?.namespace;
87
- if (ns)
88
- projectInfo.name = ns;
89
- }
90
- return {
91
- parserVersion: PARSER_VERSION,
92
- astVersion: AST_VERSION,
93
- project: projectInfo,
94
- sources,
95
- models: allModels,
96
- enums: allEnums,
97
- interfaces: allInterfaces,
98
- views: allViews,
99
- attributeRegistry: allAttrRegistry,
100
- errors,
101
- warnings,
102
- };
103
- }
104
- function checkDuplicate(name, kind, item, map, allMap, errors) {
105
- const existing = allMap.get(name);
106
- if (existing) {
107
- errors.push({
108
- code: 'M3L-E005',
109
- severity: 'error',
110
- file: item.source,
111
- line: item.line,
112
- col: 1,
113
- message: `Duplicate ${kind} name "${name}" (first defined in ${existing.loc.file}:${existing.loc.line})`,
114
- });
115
- }
116
- }
117
- function resolveInheritance(model, modelMap, interfaceMap, allNamedMap, errors) {
118
- if (model.inherits.length === 0)
119
- return;
120
- const inheritedFields = [];
121
- const resolved = new Set();
122
- const visiting = new Set();
123
- function collectFields(name, fromModel) {
124
- if (resolved.has(name) || visiting.has(name))
125
- return;
126
- visiting.add(name);
127
- const parent = modelMap.get(name) || interfaceMap.get(name);
128
- if (!parent) {
129
- if (!allNamedMap.has(name)) {
130
- errors.push({
131
- code: 'M3L-E007',
132
- severity: 'error',
133
- file: fromModel.source,
134
- line: fromModel.line,
135
- col: 1,
136
- message: `Unresolved inheritance reference "${name}" in model "${fromModel.name}"`,
137
- });
138
- }
139
- visiting.delete(name);
140
- return;
141
- }
142
- // Recursively resolve parent's parents first
143
- for (const grandparent of parent.inherits) {
144
- collectFields(grandparent, fromModel);
145
- }
146
- // Add parent's own fields
147
- for (const field of parent.fields) {
148
- if (!inheritedFields.some(f => f.name === field.name)) {
149
- inheritedFields.push({ ...field });
150
- }
151
- }
152
- visiting.delete(name);
153
- resolved.add(name);
154
- }
155
- for (const parentName of model.inherits) {
156
- collectFields(parentName, model);
157
- }
158
- // Handle @override: child fields with @override replace inherited fields
159
- const overrideNames = new Set();
160
- for (const ownField of model.fields) {
161
- const hasOverride = ownField.attributes.some(a => a.name === 'override');
162
- if (hasOverride) {
163
- overrideNames.add(ownField.name);
164
- // Preserve @override in attributes so AST consumers can detect it
165
- }
166
- }
167
- // Remove inherited fields that are overridden
168
- const filteredInherited = inheritedFields.filter(f => !overrideNames.has(f.name));
169
- // Prepend inherited fields before model's own fields
170
- if (filteredInherited.length > 0) {
171
- model.fields = [...filteredInherited, ...model.fields];
172
- }
173
- }
174
- function checkDuplicateFields(model, errors) {
175
- const seen = new Map();
176
- for (const field of model.fields) {
177
- const existing = seen.get(field.name);
178
- if (existing) {
179
- errors.push({
180
- code: 'M3L-E005',
181
- severity: 'error',
182
- file: field.loc.file,
183
- line: field.loc.line,
184
- col: 1,
185
- message: `Duplicate field name "${field.name}" in ${model.type} "${model.name}" (first at line ${existing.loc.line})`,
186
- });
187
- }
188
- else {
189
- seen.set(field.name, field);
190
- }
191
- }
192
- }
package/dist/types.d.ts DELETED
@@ -1,180 +0,0 @@
1
- /** Source location for error reporting */
2
- export interface SourceLocation {
3
- file: string;
4
- line: number;
5
- col: number;
6
- }
7
- export type TokenType = 'namespace' | 'model' | 'enum' | 'interface' | 'view' | 'attribute_def' | 'section' | 'field' | 'nested_item' | 'blockquote' | 'horizontal_rule' | 'blank' | 'text';
8
- export interface Token {
9
- type: TokenType;
10
- raw: string;
11
- line: number;
12
- indent: number;
13
- data?: Record<string, unknown>;
14
- }
15
- export type FieldKind = 'stored' | 'computed' | 'lookup' | 'rollup';
16
- export interface FieldAttribute {
17
- name: string;
18
- args?: (string | number | boolean)[];
19
- cascade?: string;
20
- /** Whether this is a standard M3L attribute (from the official catalog) */
21
- isStandard?: boolean;
22
- /** Whether this attribute is registered in an Attribute Registry (::attribute definition) */
23
- isRegistered?: boolean;
24
- }
25
- /** Structured representation of a backtick-wrapped framework attribute like `[MaxLength(100)]` */
26
- export interface CustomAttribute {
27
- /** Content inside brackets, e.g., "MaxLength(100)" for `[MaxLength(100)]` */
28
- content: string;
29
- /** Original text including brackets, e.g., "[MaxLength(100)]" */
30
- raw: string;
31
- /** Parsed structure — name and arguments extracted from the content */
32
- parsed?: {
33
- name: string;
34
- arguments: (string | number | boolean)[];
35
- };
36
- }
37
- export interface EnumValue {
38
- name: string;
39
- description?: string;
40
- type?: string;
41
- value?: unknown;
42
- }
43
- export interface FieldNode {
44
- name: string;
45
- label?: string;
46
- type?: string;
47
- params?: (string | number)[];
48
- generic_params?: string[];
49
- nullable: boolean;
50
- array: boolean;
51
- arrayItemNullable: boolean;
52
- kind: FieldKind;
53
- default_value?: string;
54
- description?: string;
55
- attributes: FieldAttribute[];
56
- framework_attrs?: CustomAttribute[];
57
- lookup?: {
58
- path: string;
59
- };
60
- rollup?: {
61
- target: string;
62
- fk: string;
63
- aggregate: string;
64
- field?: string;
65
- where?: string;
66
- };
67
- computed?: {
68
- expression: string;
69
- platform?: string;
70
- };
71
- enum_values?: EnumValue[];
72
- fields?: FieldNode[];
73
- loc: SourceLocation;
74
- }
75
- export interface ModelNode {
76
- name: string;
77
- label?: string;
78
- type: 'model' | 'enum' | 'interface' | 'view';
79
- source: string;
80
- line: number;
81
- inherits: string[];
82
- description?: string;
83
- attributes: FieldAttribute[];
84
- fields: FieldNode[];
85
- sections: {
86
- indexes: unknown[];
87
- relations: unknown[];
88
- behaviors: unknown[];
89
- metadata: Record<string, unknown>;
90
- [key: string]: unknown;
91
- };
92
- materialized?: boolean;
93
- source_def?: ViewSourceDef;
94
- refresh?: {
95
- strategy: string;
96
- interval?: string;
97
- };
98
- loc: SourceLocation;
99
- }
100
- export interface ViewSourceDef {
101
- from: string;
102
- joins?: {
103
- model: string;
104
- on: string;
105
- }[];
106
- where?: string;
107
- order_by?: string;
108
- group_by?: string[];
109
- }
110
- export interface EnumNode {
111
- name: string;
112
- label?: string;
113
- type: 'enum';
114
- source: string;
115
- line: number;
116
- inherits: string[];
117
- description?: string;
118
- values: EnumValue[];
119
- loc: SourceLocation;
120
- }
121
- export interface ProjectInfo {
122
- name?: string;
123
- version?: string;
124
- }
125
- export interface Diagnostic {
126
- code: string;
127
- severity: 'error' | 'warning';
128
- file: string;
129
- line: number;
130
- col: number;
131
- message: string;
132
- }
133
- export interface ParsedFile {
134
- source: string;
135
- namespace?: string;
136
- models: ModelNode[];
137
- enums: EnumNode[];
138
- interfaces: ModelNode[];
139
- views: ModelNode[];
140
- attributeRegistry: AttributeRegistryEntry[];
141
- }
142
- export interface AttributeRegistryEntry {
143
- /** Attribute name (without @) */
144
- name: string;
145
- /** Description */
146
- description?: string;
147
- /** Valid targets: 'field', 'model' */
148
- target: ('field' | 'model')[];
149
- /** Value type: 'boolean', 'integer', 'string', etc. */
150
- type: string;
151
- /** Valid range for numeric types */
152
- range?: [number, number];
153
- /** Whether the attribute is required */
154
- required: boolean;
155
- /** Default value */
156
- defaultValue?: string | number | boolean;
157
- }
158
- export interface M3LAST {
159
- /** Parser package version (semver) */
160
- parserVersion: string;
161
- /** AST schema version — incremented on breaking AST structure changes */
162
- astVersion: string;
163
- project: ProjectInfo;
164
- sources: string[];
165
- models: ModelNode[];
166
- enums: EnumNode[];
167
- interfaces: ModelNode[];
168
- views: ModelNode[];
169
- /** Attribute registry entries parsed from ::attribute definitions */
170
- attributeRegistry: AttributeRegistryEntry[];
171
- errors: Diagnostic[];
172
- warnings: Diagnostic[];
173
- }
174
- export interface ValidateOptions {
175
- strict?: boolean;
176
- }
177
- export interface ValidateResult {
178
- errors: Diagnostic[];
179
- warnings: Diagnostic[];
180
- }
package/dist/types.js DELETED
@@ -1 +0,0 @@
1
- export {};
@@ -1,5 +0,0 @@
1
- import type { M3LAST, ValidateOptions, ValidateResult } from './types.js';
2
- /**
3
- * Validate a resolved M3L AST for semantic errors and style warnings.
4
- */
5
- export declare function validate(ast: M3LAST, options?: ValidateOptions): ValidateResult;
package/dist/validator.js DELETED
@@ -1,196 +0,0 @@
1
- /**
2
- * Validate a resolved M3L AST for semantic errors and style warnings.
3
- */
4
- export function validate(ast, options = {}) {
5
- const errors = [...ast.errors];
6
- const warnings = [...ast.warnings];
7
- const allModels = [...ast.models, ...ast.views];
8
- const modelMap = new Map();
9
- for (const m of allModels) {
10
- modelMap.set(m.name, m);
11
- }
12
- // M3L-E001: @rollup FK missing @reference
13
- for (const model of allModels) {
14
- for (const field of model.fields) {
15
- if (field.kind === 'rollup' && field.rollup) {
16
- validateRollupReference(field, model, modelMap, errors);
17
- }
18
- }
19
- }
20
- // M3L-E002: @lookup path FK missing @reference
21
- for (const model of allModels) {
22
- for (const field of model.fields) {
23
- if (field.kind === 'lookup' && field.lookup) {
24
- validateLookupReference(field, model, modelMap, errors);
25
- }
26
- }
27
- }
28
- // M3L-E004: View @from references model not found
29
- for (const view of ast.views) {
30
- if (view.source_def?.from) {
31
- if (!modelMap.has(view.source_def.from)) {
32
- errors.push({
33
- code: 'M3L-E004',
34
- severity: 'error',
35
- file: view.source,
36
- line: view.line,
37
- col: 1,
38
- message: `View "${view.name}" references model "${view.source_def.from}" which is not defined`,
39
- });
40
- }
41
- }
42
- }
43
- // M3L-E006: Duplicate field names (already checked in resolver, but re-check for safety)
44
- for (const model of allModels) {
45
- const seen = new Set();
46
- for (const field of model.fields) {
47
- if (seen.has(field.name)) {
48
- errors.push({
49
- code: 'M3L-E006',
50
- severity: 'error',
51
- file: field.loc.file,
52
- line: field.loc.line,
53
- col: 1,
54
- message: `Duplicate field name "${field.name}" in ${model.type} "${model.name}"`,
55
- });
56
- }
57
- seen.add(field.name);
58
- }
59
- }
60
- // Strict mode warnings
61
- if (options.strict) {
62
- for (const model of allModels) {
63
- for (const field of model.fields) {
64
- // M3L-W001: Field line length > 80 chars
65
- // We check the source loc raw length — approximate using field attributes count
66
- checkFieldLineLength(field, model, warnings);
67
- // M3L-W003: Framework attrs without backtick (already processed in lexer, skip)
68
- // M3L-W004: Lookup chain > 3 hops
69
- if (field.kind === 'lookup' && field.lookup) {
70
- const hops = field.lookup.path.split('.').length;
71
- if (hops > 3) {
72
- warnings.push({
73
- code: 'M3L-W004',
74
- severity: 'warning',
75
- file: field.loc.file,
76
- line: field.loc.line,
77
- col: 1,
78
- message: `Lookup chain "${field.lookup.path}" exceeds 3 hops (${hops} hops)`,
79
- });
80
- }
81
- }
82
- }
83
- // M3L-W002: Object nesting > 3 levels
84
- checkNestingDepth(model.fields, 1, model, warnings);
85
- }
86
- // M3L-W006: Inline enum missing values: key
87
- for (const model of allModels) {
88
- for (const field of model.fields) {
89
- if (field.type === 'enum' && field.enum_values && field.enum_values.length > 0) {
90
- // The lexer/parser would have marked whether values: key was used
91
- // For now, we check based on presence — if enum_values exist without
92
- // the values: wrapper, the parser still collects them.
93
- // This warning is informational for style.
94
- // We'll rely on a flag set during parsing in the future.
95
- }
96
- }
97
- }
98
- }
99
- return { errors, warnings };
100
- }
101
- function validateRollupReference(field, model, modelMap, errors) {
102
- const rollup = field.rollup;
103
- const targetModel = modelMap.get(rollup.target);
104
- if (!targetModel) {
105
- // Target model doesn't exist — this is M3L-E007 (already caught in resolver)
106
- return;
107
- }
108
- // Check that the FK field in the target model has @reference or @fk
109
- const fkField = targetModel.fields.find(f => f.name === rollup.fk);
110
- if (!fkField) {
111
- // FK field doesn't exist in target
112
- return;
113
- }
114
- const hasReference = fkField.attributes.some(a => a.name === 'reference' || a.name === 'fk');
115
- if (!hasReference) {
116
- errors.push({
117
- code: 'M3L-E001',
118
- severity: 'error',
119
- file: field.loc.file,
120
- line: field.loc.line,
121
- col: 1,
122
- message: `@rollup on "${field.name}" targets "${rollup.target}.${rollup.fk}" which has no @reference or @fk attribute`,
123
- });
124
- }
125
- }
126
- function validateLookupReference(field, model, modelMap, errors) {
127
- const lookupPath = field.lookup.path;
128
- const segments = lookupPath.split('.');
129
- if (segments.length < 2)
130
- return;
131
- const fkFieldName = segments[0];
132
- // Find the FK field in the current model
133
- const fkField = model.fields.find(f => f.name === fkFieldName);
134
- if (!fkField)
135
- return; // Missing field — different issue
136
- const hasReference = fkField.attributes.some(a => a.name === 'reference' || a.name === 'fk');
137
- if (!hasReference) {
138
- errors.push({
139
- code: 'M3L-E002',
140
- severity: 'error',
141
- file: field.loc.file,
142
- line: field.loc.line,
143
- col: 1,
144
- message: `@lookup on "${field.name}" references FK "${fkFieldName}" which has no @reference or @fk attribute`,
145
- });
146
- }
147
- }
148
- function checkFieldLineLength(field, model, warnings) {
149
- // Reconstruct approximate line length
150
- let len = 2 + field.name.length; // "- name"
151
- if (field.label)
152
- len += field.label.length + 2; // "(label)"
153
- if (field.type) {
154
- len += 2 + field.type.length; // ": type"
155
- if (field.params)
156
- len += field.params.join(',').length + 2;
157
- }
158
- if (field.nullable)
159
- len += 1;
160
- if (field.default_value)
161
- len += 3 + field.default_value.length;
162
- for (const attr of field.attributes) {
163
- len += 2 + attr.name.length; // " @name"
164
- if (attr.args)
165
- len += attr.args.join(',').length + 2;
166
- }
167
- if (field.description)
168
- len += 3 + field.description.length;
169
- if (len > 80) {
170
- warnings.push({
171
- code: 'M3L-W001',
172
- severity: 'warning',
173
- file: field.loc.file,
174
- line: field.loc.line,
175
- col: 1,
176
- message: `Field "${field.name}" line length (~${len} chars) exceeds 80 character guideline`,
177
- });
178
- }
179
- }
180
- function checkNestingDepth(fields, depth, model, warnings) {
181
- for (const field of fields) {
182
- if (field.fields && field.fields.length > 0) {
183
- if (depth >= 3) {
184
- warnings.push({
185
- code: 'M3L-W002',
186
- severity: 'warning',
187
- file: field.loc.file,
188
- line: field.loc.line,
189
- col: 1,
190
- message: `Object nesting depth exceeds 3 levels at field "${field.name}" in "${model.name}"`,
191
- });
192
- }
193
- checkNestingDepth(field.fields, depth + 1, model, warnings);
194
- }
195
- }
196
- }