@iyulab/m3l 0.1.3 → 0.1.4

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/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # @iyulab/m3l
2
2
 
3
- M3L (Meta Model Markup Language) parser and CLI — parse `.m3l.md` files into a structured JSON AST.
3
+ [![npm](https://img.shields.io/npm/v/@iyulab/m3l)](https://www.npmjs.com/package/@iyulab/m3l)
4
+ [![TypeScript CI](https://github.com/iyulab/m3l/actions/workflows/parser-publish.yml/badge.svg)](https://github.com/iyulab/m3l/actions/workflows/parser-publish.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](../../LICENSE)
6
+
7
+ M3L (Meta Model Markup Language) parser and CLI — parse `.m3l.md` / `.m3l` files into a structured JSON AST.
4
8
 
5
9
  M3L is a Markdown-based data modeling language. You write data models in readable Markdown, and this parser converts them into a machine-processable AST with full validation.
6
10
 
@@ -107,6 +111,7 @@ npx m3l validate ./models --strict --format json
107
111
  | `## Name ::enum` | Enum definition |
108
112
  | `## Name ::interface` | Interface definition |
109
113
  | `## Name ::view` | Derived view |
114
+ | `## @name ::attribute` | Attribute registry entry |
110
115
  | `- field: type` | Field definition |
111
116
  | `- field: type?` | Nullable field |
112
117
  | `- field: type[]` | Array field |
@@ -133,6 +138,7 @@ interface M3LAST {
133
138
  enums: EnumNode[];
134
139
  interfaces: ModelNode[];
135
140
  views: ModelNode[];
141
+ attributeRegistry: AttributeRegistryEntry[];
136
142
  errors: Diagnostic[];
137
143
  warnings: Diagnostic[];
138
144
  }
@@ -146,10 +152,10 @@ interface FieldNode {
146
152
  label?: string;
147
153
  type?: string;
148
154
  params?: (string | number)[];
149
- generic_params?: string[]; // map<K,V> ["K", "V"]
155
+ generic_params?: string[]; // map<K,V> -> ["K", "V"]
150
156
  nullable: boolean;
151
157
  array: boolean;
152
- arrayItemNullable: boolean; // string?[] true
158
+ arrayItemNullable: boolean; // string?[] -> true
153
159
  kind: 'stored' | 'computed' | 'lookup' | 'rollup';
154
160
  default_value?: string;
155
161
  description?: string;
@@ -163,17 +169,61 @@ interface FieldNode {
163
169
  loc: SourceLocation;
164
170
  }
165
171
 
166
- interface ModelNode {
172
+ interface FieldAttribute {
173
+ name: string;
174
+ args?: (string | number | boolean)[];
175
+ cascade?: string;
176
+ isStandard?: boolean; // true for M3L standard attributes
177
+ isRegistered?: boolean; // true for attributes in the registry
178
+ }
179
+
180
+ interface CustomAttribute {
181
+ content: string; // e.g. "MaxLength(100)"
182
+ raw: string; // e.g. "[MaxLength(100)]"
183
+ parsed?: { // structured parse result
184
+ name: string;
185
+ arguments: (string | number | boolean)[];
186
+ };
187
+ }
188
+
189
+ interface AttributeRegistryEntry {
167
190
  name: string;
168
- type: 'model' | 'enum' | 'interface' | 'view';
169
- inherits: string[];
170
- attributes: FieldAttribute[]; // model-level attributes (@public, etc.)
171
- fields: FieldNode[];
172
- sections: { indexes; relations; behaviors; metadata; [key: string]: unknown };
173
- // ...
191
+ description?: string;
192
+ target: ('field' | 'model')[];
193
+ type: string;
194
+ range?: [number, number];
195
+ required: boolean;
196
+ defaultValue?: string | number | boolean;
174
197
  }
175
198
  ```
176
199
 
200
+ ## Attribute Registry
201
+
202
+ Define custom attributes with validation metadata using `::attribute`:
203
+
204
+ ```markdown
205
+ ## @pii ::attribute
206
+ > Personal identifiable information marker
207
+ - target: [field]
208
+ - type: boolean
209
+ - default: false
210
+
211
+ ## @audit_level ::attribute
212
+ > Audit compliance level
213
+ - target: [field, model]
214
+ - type: integer
215
+ - range: [1, 5]
216
+ - default: 1
217
+ ```
218
+
219
+ Attributes are classified into 3 tiers:
220
+
221
+ | Tier | `isStandard` | `isRegistered` | Example |
222
+ |------|-------------|----------------|---------|
223
+ | Standard | `true` | — | `@primary`, `@unique`, `@reference` |
224
+ | Registered | — | `true` | `@pii`, `@audit_level` (defined via `::attribute`) |
225
+ | Unregistered | — | — | `@some_unknown_attr` |
226
+
177
227
  ## Validation
178
228
 
179
229
  The validator checks for semantic errors and style warnings:
@@ -184,7 +234,9 @@ The validator checks for semantic errors and style warnings:
184
234
  | M3L-E001 | Rollup FK field missing `@reference` |
185
235
  | M3L-E002 | Lookup FK field missing `@reference` |
186
236
  | M3L-E004 | View references non-existent model |
237
+ | M3L-E005 | Duplicate model/enum name |
187
238
  | M3L-E006 | Duplicate field name within model |
239
+ | M3L-E007 | Unresolved parent in inheritance |
188
240
 
189
241
  **Warnings (--strict):**
190
242
  | Code | Description |
@@ -221,6 +273,10 @@ Parse an M3L content string into an AST.
221
273
 
222
274
  Parse and validate M3L files. Options: `{ strict?: boolean }`.
223
275
 
276
+ ### `STANDARD_ATTRIBUTES: Set<string>`
277
+
278
+ The set of 27 M3L standard attribute names (e.g. `primary`, `unique`, `reference`, `computed`, ...).
279
+
224
280
  ### Lower-level API
225
281
 
226
282
  ```typescript
package/dist/lexer.js CHANGED
@@ -3,7 +3,7 @@ const RE_H1 = /^# (.+)$/;
3
3
  const RE_H2 = /^## (.+)$/;
4
4
  const RE_H3 = /^### (.+)$/;
5
5
  const RE_HR = /^-{3,}$/;
6
- const RE_BLOCKQUOTE = /^> (.+)$/;
6
+ const RE_BLOCKQUOTE = /^\s*> (.+)$/;
7
7
  const RE_LIST_ITEM = /^(\s*)- (.+)$/;
8
8
  const RE_BLANK = /^\s*$/;
9
9
  // H2 sub-patterns
@@ -143,6 +143,14 @@ function tokenizeH2(content, raw, line) {
143
143
  const rest = typeMatch[3]?.trim() || '';
144
144
  const { name, label } = parseNameLabel(namepart);
145
145
  const data = { name, label };
146
+ // Parse inheritance: ::enum : Base1, Base2
147
+ const inheritMatch = rest.match(/^:\s*(.+?)(?:\s+@|\s*"|\s*$)/);
148
+ if (inheritMatch) {
149
+ data.inherits = inheritMatch[1].split(',').map(s => s.trim()).filter(Boolean);
150
+ }
151
+ else {
152
+ data.inherits = [];
153
+ }
146
154
  if (typeIndicator === 'view') {
147
155
  data.materialized = rest.includes('@materialized');
148
156
  }
package/dist/parser.js CHANGED
@@ -139,6 +139,7 @@ function handleEnumStart(token, state) {
139
139
  type: 'enum',
140
140
  source: state.file,
141
141
  line: token.line,
142
+ inherits: data.inherits || [],
142
143
  description: data.description,
143
144
  values: [],
144
145
  loc: { file: state.file, line: token.line, col: 1 },
@@ -265,11 +266,12 @@ function handleDirective(data, model, token, state) {
265
266
  if (!attrs || attrs.length === 0)
266
267
  return;
267
268
  const attr = attrs[0];
268
- if (attr.name === 'index') {
269
+ if (attr.name === 'index' || attr.name === 'unique') {
269
270
  model.sections.indexes.push({
270
271
  type: 'directive',
271
272
  raw: data.raw_content,
272
273
  args: attr.args,
274
+ unique: attr.name === 'unique',
273
275
  loc: { file: state.file, line: token.line, col: 1 },
274
276
  });
275
277
  }
@@ -358,6 +360,7 @@ function handleSectionItem(data, model, token, state) {
358
360
  raw: token.raw.trim().replace(/^- /, ''),
359
361
  loc,
360
362
  });
363
+ state.lastField = { name: token.raw.trim().replace(/^- /, '') };
361
364
  return;
362
365
  }
363
366
  // Metadata section
@@ -420,6 +423,16 @@ function handleNestedItem(token, state) {
420
423
  }
421
424
  return;
422
425
  }
426
+ // Nested items under relation in Relations section
427
+ if (state.currentSection === 'Relations' && state.lastField) {
428
+ const lastRelation = model.sections.relations[model.sections.relations.length - 1];
429
+ if (lastRelation && typeof lastRelation === 'object') {
430
+ if (key) {
431
+ lastRelation[key] = parseNestedValue(value || '');
432
+ }
433
+ }
434
+ return;
435
+ }
423
436
  // Nested items under a field
424
437
  if (state.lastField) {
425
438
  const field = state.lastField;
@@ -500,6 +513,16 @@ function handleBlockquote(token, state) {
500
513
  if (!state.currentElement)
501
514
  return;
502
515
  const text = token.data.text;
516
+ // Field-level blockquote: apply to lastField if available (not for enums)
517
+ if (state.lastField && !isEnumNode(state.currentElement)) {
518
+ if (state.lastField.description) {
519
+ state.lastField.description += '\n' + text;
520
+ }
521
+ else {
522
+ state.lastField.description = text;
523
+ }
524
+ return;
525
+ }
503
526
  if (state.currentElement.description) {
504
527
  state.currentElement.description += '\n' + text;
505
528
  }
@@ -667,6 +690,10 @@ function buildFieldNode(data, token, state) {
667
690
  field.computed.platform = parts.platform;
668
691
  }
669
692
  }
693
+ // Inline comment as field description
694
+ if (!field.description && data.comment) {
695
+ field.description = data.comment;
696
+ }
670
697
  return field;
671
698
  }
672
699
  /**
@@ -2,7 +2,7 @@ import type { ParsedFile, M3LAST, ProjectInfo } from './types.js';
2
2
  /** AST schema version — bump major on breaking structure changes */
3
3
  export declare const AST_VERSION = "1.0";
4
4
  /** Parser package version — kept in sync with package.json */
5
- export declare const PARSER_VERSION = "0.1.3";
5
+ export declare const PARSER_VERSION = "0.1.4";
6
6
  /**
7
7
  * Resolve and merge multiple parsed file ASTs into a single M3LAST.
8
8
  * Handles: inheritance resolution, duplicate detection, reference validation.
package/dist/resolver.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /** AST schema version — bump major on breaking structure changes */
2
2
  export const AST_VERSION = '1.0';
3
3
  /** Parser package version — kept in sync with package.json */
4
- export const PARSER_VERSION = '0.1.3';
4
+ export const PARSER_VERSION = '0.1.4';
5
5
  /**
6
6
  * Resolve and merge multiple parsed file ASTs into a single M3LAST.
7
7
  * Handles: inheritance resolution, duplicate detection, reference validation.
package/dist/types.d.ts CHANGED
@@ -113,6 +113,7 @@ export interface EnumNode {
113
113
  type: 'enum';
114
114
  source: string;
115
115
  line: number;
116
+ inherits: string[];
116
117
  description?: string;
117
118
  values: EnumValue[];
118
119
  loc: SourceLocation;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/m3l",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "M3L parser and CLI tool — parse .m3l.md files into JSON AST",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",