@maias/core 0.2.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/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +46 -0
- package/dist/diagnostics.d.ts +42 -0
- package/dist/diagnostics.js +29 -0
- package/dist/edit.d.ts +84 -0
- package/dist/edit.js +321 -0
- package/dist/fmt.d.ts +27 -0
- package/dist/fmt.js +257 -0
- package/dist/graph.d.ts +29 -0
- package/dist/graph.js +93 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/lint.d.ts +8 -0
- package/dist/lint.js +176 -0
- package/dist/maias-schema.gen.d.ts +1 -0
- package/dist/maias-schema.gen.js +497 -0
- package/dist/model.d.ts +104 -0
- package/dist/model.js +58 -0
- package/dist/parse.d.ts +36 -0
- package/dist/parse.js +65 -0
- package/dist/schema-validate.d.ts +3 -0
- package/dist/schema-validate.js +97 -0
- package/dist/validate.d.ts +11 -0
- package/dist/validate.js +19 -0
- package/package.json +48 -0
package/dist/parse.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { LineCounter, parseDocument } from 'yaml';
|
|
2
|
+
import { CODES } from './diagnostics.js';
|
|
3
|
+
/**
|
|
4
|
+
* A parsed MAIAS document. Wraps the `yaml` Document (which preserves comments
|
|
5
|
+
* and source ranges for round-trip edits and line-referenced diagnostics)
|
|
6
|
+
* together with the plain-data view used by validation and graph utilities.
|
|
7
|
+
*
|
|
8
|
+
* JSON input is parsed through the same YAML parser (JSON is a YAML subset),
|
|
9
|
+
* so positions and edits work identically; `format` records the original
|
|
10
|
+
* serialisation so `toString()` can emit it back.
|
|
11
|
+
*/
|
|
12
|
+
export class ParsedDocument {
|
|
13
|
+
ydoc;
|
|
14
|
+
lineCounter;
|
|
15
|
+
format;
|
|
16
|
+
constructor(ydoc, lineCounter, format) {
|
|
17
|
+
this.ydoc = ydoc;
|
|
18
|
+
this.lineCounter = lineCounter;
|
|
19
|
+
this.format = format;
|
|
20
|
+
}
|
|
21
|
+
/** Plain-data view. Recomputed on each call — cheap, and edits stay visible. */
|
|
22
|
+
get data() {
|
|
23
|
+
return this.ydoc.toJS();
|
|
24
|
+
}
|
|
25
|
+
/** 1-based line/col for a JSON path, when resolvable. */
|
|
26
|
+
position(path) {
|
|
27
|
+
const node = this.ydoc.getIn(path, true);
|
|
28
|
+
const offset = node?.range?.[0];
|
|
29
|
+
if (offset === undefined) {
|
|
30
|
+
// Fall back to the parent so e.g. a missing required key still points somewhere useful.
|
|
31
|
+
if (path.length === 0)
|
|
32
|
+
return undefined;
|
|
33
|
+
return this.position(path.slice(0, -1));
|
|
34
|
+
}
|
|
35
|
+
const pos = this.lineCounter.linePos(offset);
|
|
36
|
+
return { line: pos.line, col: pos.col };
|
|
37
|
+
}
|
|
38
|
+
/** Serialise. YAML keeps comments and (for unedited nodes) original layout. */
|
|
39
|
+
toString() {
|
|
40
|
+
if (this.format === 'json') {
|
|
41
|
+
return JSON.stringify(this.ydoc.toJS(), null, 2) + '\n';
|
|
42
|
+
}
|
|
43
|
+
return this.ydoc.toString({ lineWidth: 0 });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Parse MAIAS source text (YAML or JSON). Syntax errors come back as diagnostics, never throws. */
|
|
47
|
+
export function parse(text, opts = {}) {
|
|
48
|
+
const format = opts.format ?? (looksLikeJson(text) ? 'json' : 'yaml');
|
|
49
|
+
const lineCounter = new LineCounter();
|
|
50
|
+
const ydoc = parseDocument(text, { lineCounter, keepSourceTokens: true });
|
|
51
|
+
const diagnostics = ydoc.errors.map((err) => ({
|
|
52
|
+
code: CODES.PARSE,
|
|
53
|
+
severity: 'error',
|
|
54
|
+
message: err.message.split('\n')[0],
|
|
55
|
+
path: [],
|
|
56
|
+
line: err.linePos?.[0]?.line,
|
|
57
|
+
col: err.linePos?.[0]?.col,
|
|
58
|
+
}));
|
|
59
|
+
if (diagnostics.length > 0)
|
|
60
|
+
return { diagnostics };
|
|
61
|
+
return { parsed: new ParsedDocument(ydoc, lineCounter, format), diagnostics };
|
|
62
|
+
}
|
|
63
|
+
function looksLikeJson(text) {
|
|
64
|
+
return text.trimStart().startsWith('{');
|
|
65
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Validator } from '@cfworker/json-schema';
|
|
2
|
+
import { MAIAS_SCHEMA as schema } from './maias-schema.gen.js';
|
|
3
|
+
import { CODES } from './diagnostics.js';
|
|
4
|
+
// @cfworker/json-schema is an interpreting validator (no eval/new Function),
|
|
5
|
+
// so this exact code path runs under Node and React Native/Hermes alike (D11).
|
|
6
|
+
const validator = new Validator(schema, '2020-12', false);
|
|
7
|
+
/** Keywords whose output units describe a concrete leaf problem (vs. structural bookkeeping). */
|
|
8
|
+
const LEAF_KEYWORDS = new Set([
|
|
9
|
+
'type',
|
|
10
|
+
'required',
|
|
11
|
+
'pattern',
|
|
12
|
+
'enum',
|
|
13
|
+
'const',
|
|
14
|
+
'minItems',
|
|
15
|
+
'uniqueItems',
|
|
16
|
+
'minLength',
|
|
17
|
+
'additionalProperties',
|
|
18
|
+
]);
|
|
19
|
+
export function schemaValidate(parsed) {
|
|
20
|
+
const result = validator.validate(parsed.data);
|
|
21
|
+
if (result.valid)
|
|
22
|
+
return [];
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
const diagnostics = [];
|
|
25
|
+
const units = result.errors.filter((u) => !isCascade(u, result.errors));
|
|
26
|
+
for (const unit of units) {
|
|
27
|
+
if (!LEAF_KEYWORDS.has(unit.keyword))
|
|
28
|
+
continue;
|
|
29
|
+
const path = pointerToPath(unit.instanceLocation);
|
|
30
|
+
const message = describe(unit.keyword, unit, path);
|
|
31
|
+
const key = `${unit.instanceLocation}|${message}`;
|
|
32
|
+
if (seen.has(key))
|
|
33
|
+
continue;
|
|
34
|
+
seen.add(key);
|
|
35
|
+
diagnostics.push({
|
|
36
|
+
code: CODES.SCHEMA,
|
|
37
|
+
severity: 'error',
|
|
38
|
+
message,
|
|
39
|
+
path,
|
|
40
|
+
...parsed.position(path),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// Everything was filtered as structural noise — surface the raw root cause rather than nothing.
|
|
44
|
+
if (diagnostics.length === 0 && result.errors.length > 0) {
|
|
45
|
+
const unit = result.errors[result.errors.length - 1];
|
|
46
|
+
const path = pointerToPath(unit.instanceLocation);
|
|
47
|
+
diagnostics.push({
|
|
48
|
+
code: CODES.SCHEMA,
|
|
49
|
+
severity: 'error',
|
|
50
|
+
message: unit.error,
|
|
51
|
+
path,
|
|
52
|
+
...parsed.position(path),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return diagnostics;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* `additionalProperties: false` failures cascade: when `screens.0.elements.1.target`
|
|
59
|
+
* fails its pattern, every ancestor also reports "Property X does not match
|
|
60
|
+
* additional properties schema". A unit is such a cascade when the property it
|
|
61
|
+
* names has its own deeper error units. Genuine unknown keys (typos) have no
|
|
62
|
+
* deeper units and are kept.
|
|
63
|
+
*/
|
|
64
|
+
function isCascade(unit, all) {
|
|
65
|
+
if (unit.keyword !== 'additionalProperties')
|
|
66
|
+
return false;
|
|
67
|
+
const property = /^Property "([^"]+)"/.exec(unit.error)?.[1];
|
|
68
|
+
if (!property)
|
|
69
|
+
return false;
|
|
70
|
+
const childPointer = `${unit.instanceLocation === '#' ? '#' : unit.instanceLocation}/${escapePointer(property)}`;
|
|
71
|
+
return all.some((u) => u !== unit && (u.instanceLocation === childPointer || u.instanceLocation.startsWith(childPointer + '/')));
|
|
72
|
+
}
|
|
73
|
+
function escapePointer(segment) {
|
|
74
|
+
return segment.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
75
|
+
}
|
|
76
|
+
function pointerToPath(pointer) {
|
|
77
|
+
if (pointer === '#' || pointer === '')
|
|
78
|
+
return [];
|
|
79
|
+
return pointer
|
|
80
|
+
.replace(/^#\//, '')
|
|
81
|
+
.split('/')
|
|
82
|
+
.map((seg) => {
|
|
83
|
+
const dec = seg.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
84
|
+
return /^\d+$/.test(dec) ? Number(dec) : dec;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function describe(keyword, unit, path) {
|
|
88
|
+
const where = path.length ? path.join('.') : 'document root';
|
|
89
|
+
switch (keyword) {
|
|
90
|
+
case 'additionalProperties':
|
|
91
|
+
return `${unit.error} at ${where} — unknown keys must be x_-prefixed (spec §10.3)`;
|
|
92
|
+
case 'pattern':
|
|
93
|
+
return `${unit.error} at ${where}`;
|
|
94
|
+
default:
|
|
95
|
+
return `${unit.error} at ${where}`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Diagnostic } from './diagnostics.js';
|
|
2
|
+
import { type ParsedDocument } from './parse.js';
|
|
3
|
+
export interface ValidationResult {
|
|
4
|
+
/** No parse errors, no schema errors, no lint errors. Warnings do not affect validity. */
|
|
5
|
+
valid: boolean;
|
|
6
|
+
diagnostics: Diagnostic[];
|
|
7
|
+
/** Present whenever the source parsed, even if invalid. */
|
|
8
|
+
parsed?: ParsedDocument;
|
|
9
|
+
}
|
|
10
|
+
/** Full validation: parse (if given text) → JSON Schema → semantic lint. */
|
|
11
|
+
export declare function validate(source: string | ParsedDocument): ValidationResult;
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { hasErrors, sortDiagnostics } from './diagnostics.js';
|
|
2
|
+
import { lint } from './lint.js';
|
|
3
|
+
import { parse } from './parse.js';
|
|
4
|
+
import { schemaValidate } from './schema-validate.js';
|
|
5
|
+
/** Full validation: parse (if given text) → JSON Schema → semantic lint. */
|
|
6
|
+
export function validate(source) {
|
|
7
|
+
let parsed;
|
|
8
|
+
if (typeof source === 'string') {
|
|
9
|
+
const result = parse(source);
|
|
10
|
+
if (!result.parsed)
|
|
11
|
+
return { valid: false, diagnostics: sortDiagnostics(result.diagnostics) };
|
|
12
|
+
parsed = result.parsed;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
parsed = source;
|
|
16
|
+
}
|
|
17
|
+
const diagnostics = sortDiagnostics([...schemaValidate(parsed), ...lint(parsed)]);
|
|
18
|
+
return { valid: !hasErrors(diagnostics), diagnostics, parsed };
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maias/core",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MAIAS core library: parse, typed model, canonical serialise, graph utilities, safe edits, validation. The single implementation all MAIAS tooling builds on.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/MAIAS-project/maias.git",
|
|
9
|
+
"directory": "packages/core"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"maias",
|
|
13
|
+
"information-architecture",
|
|
14
|
+
"yaml",
|
|
15
|
+
"schema",
|
|
16
|
+
"validation",
|
|
17
|
+
"canonical-form",
|
|
18
|
+
"agent"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"NOTICE"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"sync-schema": "node scripts/sync-schema.mjs",
|
|
38
|
+
"prebuild": "npm run sync-schema",
|
|
39
|
+
"build": "tsc -p tsconfig.json",
|
|
40
|
+
"pretypecheck": "npm run sync-schema",
|
|
41
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
42
|
+
"prepublishOnly": "npm run build"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@cfworker/json-schema": "^4.1.1",
|
|
46
|
+
"yaml": "^2.8.0"
|
|
47
|
+
}
|
|
48
|
+
}
|