@donartcha/openlag 0.1.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.
Files changed (99) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +82 -0
  3. package/bin/openlag.js +2 -0
  4. package/dist/assets/arc-4YUHkXo3.js +1 -0
  5. package/dist/assets/architectureDiagram-3BPJPVTR-WeGmL7HM.js +36 -0
  6. package/dist/assets/blockDiagram-GPEHLZMM-CtV7ubAx.js +132 -0
  7. package/dist/assets/c4Diagram-AAUBKEIU-DqYDW5c3.js +10 -0
  8. package/dist/assets/channel-Tsel3-MK.js +1 -0
  9. package/dist/assets/chunk-2J33WTMH-BE8P9tjh.js +1 -0
  10. package/dist/assets/chunk-4BX2VUAB-Bi7oLGF5.js +1 -0
  11. package/dist/assets/chunk-55IACEB6-D9Xhxp_r.js +1 -0
  12. package/dist/assets/chunk-727SXJPM-Dz8jKE60.js +206 -0
  13. package/dist/assets/chunk-AQP2D5EJ-BzmM0IeH.js +231 -0
  14. package/dist/assets/chunk-FMBD7UC4-Cvl5dpcx.js +15 -0
  15. package/dist/assets/chunk-ND2GUHAM-Dz2efqnq.js +1 -0
  16. package/dist/assets/chunk-QZHKN3VN-CwblgSnQ.js +1 -0
  17. package/dist/assets/classDiagram-4FO5ZUOK-Bgm-_cW8.js +1 -0
  18. package/dist/assets/classDiagram-v2-Q7XG4LA2-Bgm-_cW8.js +1 -0
  19. package/dist/assets/cose-bilkent-S5V4N54A-h_A3nZUx.js +1 -0
  20. package/dist/assets/cytoscape.esm-D_LviqZs.js +331 -0
  21. package/dist/assets/dagre-BM42HDAG-CN_B2Doz.js +4 -0
  22. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  23. package/dist/assets/diagram-2AECGRRQ-C9TAFwjG.js +43 -0
  24. package/dist/assets/diagram-5GNKFQAL-BThljQLo.js +10 -0
  25. package/dist/assets/diagram-KO2AKTUF-bRPq25Se.js +3 -0
  26. package/dist/assets/diagram-LMA3HP47-BubLCIus.js +24 -0
  27. package/dist/assets/diagram-OG6HWLK6-CJpfhIsS.js +24 -0
  28. package/dist/assets/erDiagram-TEJ5UH35-6Xkza9wL.js +85 -0
  29. package/dist/assets/flowDiagram-I6XJVG4X-Bq_to3hX.js +162 -0
  30. package/dist/assets/ganttDiagram-6RSMTGT7-C3CmvYl7.js +292 -0
  31. package/dist/assets/gitGraphDiagram-PVQCEYII-C93LTfrl.js +106 -0
  32. package/dist/assets/graph-CAnANduQ.js +1 -0
  33. package/dist/assets/index-0RMQQ34p.css +1 -0
  34. package/dist/assets/index-ByxguSZe.js +729 -0
  35. package/dist/assets/infoDiagram-5YYISTIA-CMfuwygl.js +2 -0
  36. package/dist/assets/init-Gi6I4Gst.js +1 -0
  37. package/dist/assets/ishikawaDiagram-YF4QCWOH-CbJ5ojDF.js +70 -0
  38. package/dist/assets/journeyDiagram-JHISSGLW-C_Xz8YyT.js +139 -0
  39. package/dist/assets/kanban-definition-UN3LZRKU-GVv_iRMq.js +89 -0
  40. package/dist/assets/katex-DkKDou_j.js +257 -0
  41. package/dist/assets/layout-DGIYPm2g.js +1 -0
  42. package/dist/assets/linear-BNEtUH2J.js +1 -0
  43. package/dist/assets/mindmap-definition-RKZ34NQL-DIsL0XSF.js +96 -0
  44. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  45. package/dist/assets/pieDiagram-4H26LBE5-CSCTSOjk.js +30 -0
  46. package/dist/assets/quadrantDiagram-W4KKPZXB-CQQ9OaFY.js +7 -0
  47. package/dist/assets/requirementDiagram-4Y6WPE33-Cjn3la_S.js +84 -0
  48. package/dist/assets/sankeyDiagram-5OEKKPKP-DoVspvVc.js +40 -0
  49. package/dist/assets/sequenceDiagram-3UESZ5HK-UsoGmL4w.js +162 -0
  50. package/dist/assets/stateDiagram-AJRCARHV-DLmf7Dc8.js +1 -0
  51. package/dist/assets/stateDiagram-v2-BHNVJYJU-jkiDZ_3u.js +1 -0
  52. package/dist/assets/timeline-definition-PNZ67QCA-HfyRxZ8p.js +120 -0
  53. package/dist/assets/vennDiagram-CIIHVFJN-B6pM3L33.js +34 -0
  54. package/dist/assets/wardley-L42UT6IY-B-LdKtrI.js +173 -0
  55. package/dist/assets/wardleyDiagram-YWT4CUSO-BD45zhOu.js +78 -0
  56. package/dist/assets/xychartDiagram-2RQKCTM6-zsDMbUiS.js +7 -0
  57. package/dist/cli/openlag.js +1793 -0
  58. package/dist/index.html +14 -0
  59. package/index.html +13 -0
  60. package/package.json +84 -0
  61. package/scripts/cli/build.ts +34 -0
  62. package/scripts/cli/dev.ts +35 -0
  63. package/scripts/cli/generate.ts +92 -0
  64. package/scripts/cli/init.ts +427 -0
  65. package/scripts/cli/lint.ts +29 -0
  66. package/scripts/cli/openlag.ts +110 -0
  67. package/scripts/cli/vite-bin.ts +8 -0
  68. package/scripts/core/parser/diagnostic.ts +34 -0
  69. package/scripts/core/parser/normalizer.ts +27 -0
  70. package/scripts/core/parser/scanner.ts +30 -0
  71. package/scripts/core/parser/schemas.ts +23 -0
  72. package/scripts/core/parser/types.ts +30 -0
  73. package/scripts/core/parser.ts +127 -0
  74. package/scripts/generate-relations.ts +53 -0
  75. package/scripts/lint/lint-engine.ts +85 -0
  76. package/scripts/lint/lint-profiles.ts +49 -0
  77. package/scripts/lint/lint-rules.ts +174 -0
  78. package/scripts/lint/lint-types.ts +43 -0
  79. package/src/App.tsx +164 -0
  80. package/src/components/DocumentationView.tsx +905 -0
  81. package/src/components/GraphView.tsx +529 -0
  82. package/src/components/GuideView.tsx +535 -0
  83. package/src/components/ImpactView.tsx +365 -0
  84. package/src/components/MarkdownRenderer.tsx +120 -0
  85. package/src/components/OrphansView.tsx +360 -0
  86. package/src/components/SettingsView.tsx +146 -0
  87. package/src/core/generated/relation-definitions.ts +622 -0
  88. package/src/core/graph/GraphQueryLayer.ts +194 -0
  89. package/src/core/registry/ArtifactRegistry.ts +19 -0
  90. package/src/core/registry/RelationRegistry.ts +27 -0
  91. package/src/core/semantic/artifact-layers.ts +43 -0
  92. package/src/core/semantic/ownership-rules.ts +13 -0
  93. package/src/core/semantic/types.ts +11 -0
  94. package/src/index.css +121 -0
  95. package/src/lib/reportUtils.ts +59 -0
  96. package/src/main.tsx +10 -0
  97. package/src/store.ts +146 -0
  98. package/src/types.ts +77 -0
  99. package/vite.config.ts +31 -0
@@ -0,0 +1,29 @@
1
+ import path from 'path';
2
+ import { runLint, printHumanReport, loadConfig } from '../lint/lint-engine.js';
3
+ import chalk from 'chalk';
4
+
5
+ export function lintDocs(profile: string, jsonOutput = false, strictWarnings = false) {
6
+ const docsDir = path.join(process.cwd(), 'docs');
7
+ const configPath = path.join(process.cwd(), 'openlag.config.yml');
8
+
9
+ const config = loadConfig(configPath);
10
+
11
+ const report = runLint(docsDir, configPath, profile);
12
+
13
+ if (jsonOutput) {
14
+ console.log(JSON.stringify(report, null, 2));
15
+ } else {
16
+ printHumanReport(report);
17
+ }
18
+
19
+ const failOnWarnings = strictWarnings || config.failOnWarnings;
20
+
21
+ let hasErrors = report.summary.errors > 0;
22
+ if (failOnWarnings && report.summary.warnings > 0) {
23
+ hasErrors = true;
24
+ }
25
+
26
+ if (hasErrors) {
27
+ process.exit(1);
28
+ }
29
+ }
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from 'child_process';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ import chalk from 'chalk';
7
+ import { Command } from 'commander';
8
+
9
+ import { buildPortal } from './build.js';
10
+ import { runDevServer } from './dev.js';
11
+ import { generateData, watchData } from './generate.js';
12
+ import { initProject } from './init.js';
13
+ import { lintDocs } from './lint.js';
14
+ import { resolveViteBin } from './vite-bin.js';
15
+
16
+ const program = new Command();
17
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
18
+
19
+ function runVitePreview() {
20
+ const viteBin = resolveViteBin(import.meta.url);
21
+ execFileSync(process.execPath, [viteBin, 'preview', '--outDir', path.join(process.cwd(), 'dist')], {
22
+ cwd: packageRoot,
23
+ env: {
24
+ ...process.env,
25
+ OPENLAG_PROJECT_ROOT: process.cwd(),
26
+ },
27
+ stdio: 'inherit',
28
+ });
29
+ }
30
+
31
+ program.name('openlag').description('Architecture as Code traceability graph generator').version('0.1.0');
32
+
33
+ program
34
+ .command('init')
35
+ .description('Initialize a new OpenLAG project')
36
+ .option('-n, --name <name>', 'Project name')
37
+ .option('-d, --desc <description>', 'Project description')
38
+ .option('--all', 'Include optional synthetic relations')
39
+ .action((options) => {
40
+ initProject(options.name, options.desc, options.all);
41
+ });
42
+
43
+ program
44
+ .command('generate')
45
+ .description('Generate static graph data from markdown docs')
46
+ .option('-w, --watch', 'Watch mode')
47
+ .action((options) => {
48
+ const docsDir = path.join(process.cwd(), 'docs');
49
+ const outputDir = path.join(process.cwd(), 'public');
50
+
51
+ if (options.watch) {
52
+ watchData(docsDir, outputDir);
53
+ } else {
54
+ generateData(docsDir, outputDir);
55
+ }
56
+ });
57
+
58
+ program
59
+ .command('dev')
60
+ .description('Start development server with live data refresh')
61
+ .action(() => {
62
+ runDevServer();
63
+ });
64
+
65
+ program
66
+ .command('build')
67
+ .description('Build the OpenLAG portal for production')
68
+ .action(() => {
69
+ buildPortal();
70
+ });
71
+
72
+ program
73
+ .command('lint')
74
+ .description('Validate architecture documentation')
75
+ .option('-p, --profile <profile>', 'Lint profile (develop, feature, release)', 'develop')
76
+ .option('--json', 'Output report in JSON format')
77
+ .option('--strict', 'Fail on warnings')
78
+ .action((options) => {
79
+ lintDocs(options.profile, options.json, options.strict);
80
+ });
81
+
82
+ program
83
+ .command('preview')
84
+ .description('Preview the production build')
85
+ .action(() => {
86
+ runVitePreview();
87
+ });
88
+
89
+ program
90
+ .command('check')
91
+ .description('Generate graph data and validate architecture documentation')
92
+ .option('-p, --profile <profile>', 'Lint profile (develop, feature, release)', 'develop')
93
+ .option('--strict', 'Fail on warnings')
94
+ .action((options) => {
95
+ console.log(chalk.blue('Running OpenLAG checks...'));
96
+
97
+ const docsDir = path.join(process.cwd(), 'docs');
98
+ const outputDir = path.join(process.cwd(), 'public');
99
+
100
+ try {
101
+ generateData(docsDir, outputDir);
102
+ lintDocs(options.profile, false, options.strict);
103
+ console.log(chalk.green('\nOpenLAG checks passed.'));
104
+ } catch {
105
+ console.error(chalk.red('\nOpenLAG checks failed.'));
106
+ process.exit(1);
107
+ }
108
+ });
109
+
110
+ program.parse();
@@ -0,0 +1,8 @@
1
+ import path from 'path';
2
+ import { createRequire } from 'module';
3
+
4
+ export function resolveViteBin(importMetaUrl: string) {
5
+ const require = createRequire(importMetaUrl);
6
+ const viteEntry = require.resolve('vite');
7
+ return path.resolve(path.dirname(viteEntry), '../../bin/vite.js');
8
+ }
@@ -0,0 +1,34 @@
1
+ import { ParseError } from "./types.js";
2
+
3
+ export enum Severity {
4
+ CRITICAL = 'CRITICAL',
5
+ INVALID = 'INVALID',
6
+ DEGRADED = 'DEGRADED',
7
+ WARNING = 'WARNING'
8
+ }
9
+
10
+ export interface Diagnostic {
11
+ file: string;
12
+ message: string;
13
+ severity: Severity;
14
+ }
15
+
16
+ export class DiagnosticEngine {
17
+ private diagnostics: Diagnostic[] = [];
18
+
19
+ add(file: string, message: string, severity: Severity) {
20
+ this.diagnostics.push({ file, message, severity });
21
+ }
22
+
23
+ getDiagnostics(): Diagnostic[] {
24
+ return this.diagnostics;
25
+ }
26
+
27
+ getErrors(): ParseError[] {
28
+ return this.diagnostics.map(d => ({ file: d.file, message: `[${d.severity}] ${d.message}` }));
29
+ }
30
+
31
+ hasCritical(): boolean {
32
+ return this.diagnostics.some(d => d.severity === Severity.CRITICAL);
33
+ }
34
+ }
@@ -0,0 +1,27 @@
1
+ import { ArtifactType } from "../../../src/types.js";
2
+ import { inferLayer } from "../../../src/core/semantic/artifact-layers.js";
3
+
4
+ export function normalizeArtifact(parsed: any, fullPath: string, body: string): any {
5
+ const p = {...parsed};
6
+ if (!p.schemaVersion) {
7
+ p.schemaVersion = '1.0.0'; // Assign legacy version
8
+ }
9
+
10
+ const typeValue = (p.type || p.Type) as ArtifactType;
11
+ return {
12
+ id: String(p.id || p.ID || ''),
13
+ type: typeValue,
14
+ subType: p.subType || p.subtype || p.SubType,
15
+ title: String(p.title || p.Title || (p.id || p.ID || '')),
16
+ version: String(p.version || p.Version || 'v-1'),
17
+ description: body,
18
+ systemVersionId: p.systemVersionId || p.systemversionid,
19
+ component: p.component,
20
+ releaseDate: p.releaseDate || p.timestamp,
21
+ status: p.status,
22
+ layer: inferLayer(typeValue),
23
+ ownership: p.ownership || p.owner ? { owner: p.owner, ...p.ownership } : undefined,
24
+ file: fullPath,
25
+ schemaVersion: p.schemaVersion
26
+ };
27
+ }
@@ -0,0 +1,30 @@
1
+
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { RawDocument } from "./types.js";
5
+
6
+ export function scanDocs(docsDir: string): RawDocument[] {
7
+ const documents: RawDocument[] = [];
8
+
9
+ const traverse = (dir: string) => {
10
+ if (!fs.existsSync(dir)) return;
11
+ const items = fs.readdirSync(dir);
12
+ for (const item of items) {
13
+ const fullPath = path.join(dir, item);
14
+ const stat = fs.statSync(fullPath);
15
+
16
+ if (stat.isDirectory()) {
17
+ traverse(fullPath);
18
+ } else if (item.endsWith('.md')) {
19
+ const content = fs.readFileSync(fullPath, 'utf-8');
20
+ documents.push({
21
+ file: fullPath,
22
+ content
23
+ });
24
+ }
25
+ }
26
+ };
27
+
28
+ traverse(docsDir);
29
+ return documents;
30
+ }
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+
3
+ export const ArtifactSchema = z.object({
4
+ id: z.string(),
5
+ type: z.string(),
6
+ subType: z.string().optional(),
7
+ title: z.string(),
8
+ version: z.string(),
9
+ description: z.string(),
10
+ systemVersionId: z.string().optional(),
11
+ status: z.enum(['draft', 'in_progress', 'ready', 'closed', 'deprecated']).optional(),
12
+ layer: z.string().optional(),
13
+ ownership: z.object({
14
+ owner: z.string().optional(),
15
+ team: z.string().optional(),
16
+ domain: z.string().optional(),
17
+ maintainers: z.array(z.string()).optional(),
18
+ reviewers: z.array(z.string()).optional(),
19
+ steward: z.string().optional(),
20
+ }).optional(),
21
+ file: z.string(),
22
+ schemaVersion: z.string().optional(),
23
+ });
@@ -0,0 +1,30 @@
1
+ import { Artifact, ArtifactType, Relation, RelationType, Change, ChangeType, SystemVersion, Version } from "../../../src/types.js";
2
+
3
+ export interface ParsedArtifact extends Artifact {
4
+ file: string;
5
+ status?: 'draft' | 'in_progress' | 'ready' | 'closed' | 'deprecated';
6
+ }
7
+
8
+ export interface ParsedRelation extends Relation {
9
+ file: string;
10
+ }
11
+
12
+ export interface ParseError {
13
+ file: string;
14
+ message: string;
15
+ }
16
+
17
+ export interface OpenLagData {
18
+ versions: Version[];
19
+ systemVersions: SystemVersion[];
20
+ changes: Change[];
21
+ artifacts: ParsedArtifact[];
22
+ relations: ParsedRelation[];
23
+ errors: ParseError[];
24
+ }
25
+
26
+ export interface RawDocument {
27
+ file: string;
28
+ content: string;
29
+ frontmatter?: Record<string, any>;
30
+ }
@@ -0,0 +1,127 @@
1
+ import { scanDocs } from "./parser/scanner.js";
2
+ import { ParsedArtifact, ParsedRelation, ParseError, OpenLagData, RawDocument } from "./parser/types.js";
3
+ export type { ParsedArtifact, ParsedRelation, ParseError, OpenLagData, RawDocument };
4
+ import { ArtifactType } from "../../src/types.js";
5
+ import { ArtifactSchema } from "./parser/schemas.js";
6
+ import { normalizeArtifact } from "./parser/normalizer.js";
7
+ import { DiagnosticEngine, Severity } from "./parser/diagnostic.js";
8
+ import { RelationRegistry } from "../../src/core/registry/RelationRegistry.js";
9
+ import yaml from "js-yaml";
10
+
11
+ export function parseOpenLagDocs(docsDir: string): OpenLagData {
12
+ const diag = new DiagnosticEngine();
13
+ const state: OpenLagData = {
14
+ versions: [],
15
+ systemVersions: [],
16
+ changes: [],
17
+ artifacts: [],
18
+ relations: [],
19
+ errors: []
20
+ };
21
+
22
+ const documents = scanDocs(docsDir);
23
+
24
+ for (const doc of documents) {
25
+ const { content, file: fullPath } = doc;
26
+ const sections = content.split(/---+\r?\n/);
27
+
28
+ let i = 0;
29
+ if (content.trim().startsWith('---')) i = 1;
30
+
31
+ for (; i < sections.length; i += 1) {
32
+ const section = sections[i].trim();
33
+ if (section.length < 5) continue;
34
+
35
+ let parsed: any;
36
+ try {
37
+ const yamlCandidate = sections[i].split('\n')
38
+ .map(l => l.replace(/^##\s?/, ''))
39
+ .join('\n')
40
+ .replace(/---$/, '')
41
+ .trim();
42
+
43
+ parsed = yaml.load(yamlCandidate) as any;
44
+ } catch (e) {
45
+ throw new Error(`CRITICAL: Invalid YAML block in ${fullPath}: ${(e as Error).message}`, { cause: e });
46
+ }
47
+
48
+ if (parsed && typeof parsed === 'object' && (parsed.id || parsed.ID) && (parsed.type || parsed.Type)) {
49
+ const body = (i + 1 < sections.length) ? sections[i+1].trim() : '';
50
+ if (i+1 < sections.length) i++; // Consume body section if we took it
51
+
52
+ const normalized = normalizeArtifact(parsed, fullPath, body);
53
+
54
+ try {
55
+ ArtifactSchema.parse(normalized);
56
+
57
+ const artifact: ParsedArtifact = normalized;
58
+ state.artifacts.push(artifact);
59
+
60
+ const typeValue = artifact.type as ArtifactType;
61
+
62
+ if (typeValue === 'VERSION') {
63
+ state.versions.push({
64
+ id: artifact.id,
65
+ name: String(parsed.name || ''),
66
+ timestamp: String(parsed.timestamp || ''),
67
+ parentVersion: parsed.parentVersion || null
68
+ });
69
+ } else if (typeValue === 'SYSTEM_VERSION') {
70
+ state.systemVersions.push({
71
+ id: artifact.id,
72
+ component: String(parsed.component || ''),
73
+ version: String(parsed.version || ''),
74
+ releaseDate: String(parsed.releaseDate || '')
75
+ });
76
+ } else if (typeValue === 'CHANGE') {
77
+ state.changes.push({
78
+ id: artifact.id,
79
+ type: parsed.changeType || 'FEATURE',
80
+ title: artifact.title,
81
+ description: artifact.description,
82
+ affects: Array.isArray(parsed.affects) ? parsed.affects : [],
83
+ versionFrom: String(parsed.versionFrom || ''),
84
+ versionTo: String(parsed.versionTo || '')
85
+ });
86
+ }
87
+
88
+ const rels = parsed.relations || parsed.Relations;
89
+ if (rels) {
90
+ const relArray = Array.isArray(rels) ? rels : [rels];
91
+ relArray.forEach((rel: any, idx: number) => {
92
+ const to = typeof rel === 'string' ? rel : (rel.to || rel.id || rel.ID);
93
+ if (to) {
94
+ const relType = rel.type;
95
+ if (!relType) {
96
+ diag.add(fullPath, `Relation targeting ${to} missing 'type' in artifact ${artifact.id}`, Severity.WARNING);
97
+ return; // Skip inference, strictly require type
98
+ }
99
+ const contract = RelationRegistry.getContract(relType);
100
+ state.relations.push({
101
+ id: `rel-${artifact.id}-${idx}`,
102
+ from: artifact.id,
103
+ to: String(to),
104
+ type: relType,
105
+ category: contract?.category,
106
+ strength: contract?.strength,
107
+ file: fullPath
108
+ });
109
+ }
110
+ });
111
+ }
112
+ } catch (e) {
113
+ diag.add(fullPath, `Schema validation error: ${(e as Error).message}`, Severity.INVALID);
114
+ }
115
+
116
+ } else if (parsed && typeof parsed === 'object') {
117
+ if (!(parsed.id || parsed.ID)) {
118
+ diag.add(fullPath, `Missing ID in artifact`, Severity.INVALID);
119
+ } else if (!(parsed.type || parsed.Type)) {
120
+ diag.add(fullPath, `Missing Type in artifact`, Severity.INVALID);
121
+ }
122
+ }
123
+ }
124
+ }
125
+ state.errors = diag.getErrors();
126
+ return state;
127
+ }
@@ -0,0 +1,53 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import yaml from 'js-yaml';
4
+
5
+ const relationsDir = path.join(process.cwd(), 'docs/relations');
6
+ const outputFile = path.join(process.cwd(), 'src/core/generated/relation-definitions.ts');
7
+
8
+ const files = fs.readdirSync(relationsDir);
9
+ const relations: any[] = [];
10
+
11
+ files.forEach(file => {
12
+ if (file.endsWith('.yaml')) {
13
+ const content = fs.readFileSync(path.join(relationsDir, file), 'utf-8');
14
+ const parsed = yaml.load(content) as any;
15
+ relations.push({
16
+ type: parsed.relation,
17
+ description: parsed.description,
18
+ category: parsed.category,
19
+ allowedFrom: parsed.allowedFrom || [],
20
+ allowedTo: parsed.allowedTo || [],
21
+ multiplicity: parsed.multiplicity || { from: 'many', to: 'many' },
22
+ validation: parsed.validation || { severity: 'error' },
23
+ strength: parsed.validation?.severity === 'error' ? 'STRONG' : (parsed.validation?.severity === 'warn' ? 'MEDIUM' : 'WEAK')
24
+ });
25
+ }
26
+ });
27
+
28
+ const fileContent = `
29
+ // GENERATED FILE - DO NOT EDIT MANUALLY
30
+ import { RelationCategory, RelationStrength } from '../../types.js';
31
+ import { ArtifactType } from '../registry/ArtifactRegistry.js';
32
+
33
+ export interface RelationContract {
34
+ type: string;
35
+ description?: string;
36
+ category: RelationCategory;
37
+ strength: RelationStrength;
38
+ allowedFrom: ArtifactType[];
39
+ allowedTo: ArtifactType[];
40
+ multiplicity: { from: string; to: string };
41
+ validation: { severity: string };
42
+ }
43
+
44
+ export const GENERATED_RELATIONS: RelationContract[] = ${JSON.stringify(relations, null, 2)};
45
+ `;
46
+
47
+ if (!fs.existsSync(path.dirname(outputFile))) {
48
+ fs.mkdirSync(path.dirname(outputFile), { recursive: true });
49
+ }
50
+
51
+ fs.writeFileSync(outputFile, fileContent);
52
+ console.log('Relation definitions generated.');
53
+
@@ -0,0 +1,85 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import yaml from "js-yaml";
4
+ import chalk from 'chalk';
5
+ import { parseOpenLagDocs } from "../core/parser.js";
6
+ import { runLintRules } from "./lint-rules.js";
7
+ import { defaultProfiles } from "./lint-profiles.js";
8
+ import { LintConfig, LintReport, LintIssue, LintSummary } from "./lint-types.js";
9
+
10
+ export function loadConfig(configPath: string): LintConfig {
11
+ const defaultConfig: LintConfig = {
12
+ defaultProfile: 'develop',
13
+ failOnWarnings: false,
14
+ profiles: {}
15
+ };
16
+
17
+ if (fs.existsSync(configPath)) {
18
+ try {
19
+ const fileContent = fs.readFileSync(configPath, 'utf8');
20
+ const parsed = yaml.load(fileContent) as any;
21
+ if (parsed && parsed.lint) {
22
+ return {
23
+ ...defaultConfig,
24
+ ...parsed.lint
25
+ };
26
+ }
27
+ } catch (e) {
28
+ console.warn("⚠️ Could not parse openlag.config.yml, using defaults.");
29
+ }
30
+ }
31
+
32
+ return defaultConfig;
33
+ }
34
+
35
+ export function runLint(docsDir: string, configPath: string, profileName?: string): LintReport {
36
+ const config = loadConfig(configPath);
37
+ const selectedProfileName = profileName || config.defaultProfile;
38
+
39
+ const baseProfile = defaultProfiles[selectedProfileName] || defaultProfiles['develop'];
40
+ const customProfileOverrides = config.profiles[selectedProfileName] || {};
41
+
42
+ const activeProfile = { ...baseProfile, ...customProfileOverrides };
43
+
44
+ const parsedData = parseOpenLagDocs(docsDir);
45
+ const issues = runLintRules(parsedData, activeProfile);
46
+
47
+ const summary: LintSummary = {
48
+ errors: 0,
49
+ warnings: 0,
50
+ info: 0
51
+ };
52
+
53
+ for (const issue of issues) {
54
+ if (issue.severity === 'error') summary.errors++;
55
+ if (issue.severity === 'warning') summary.warnings++;
56
+ if (issue.severity === 'info') summary.info++;
57
+ }
58
+
59
+ return {
60
+ profile: selectedProfileName,
61
+ summary,
62
+ issues
63
+ };
64
+ }
65
+
66
+ export function printHumanReport(report: LintReport) {
67
+ console.log(`\n` + chalk.bold(`OpenLAG Lint Report`));
68
+ console.log(chalk.dim(`Profile: ${report.profile}\n`));
69
+
70
+ if (report.issues.length === 0) {
71
+ console.log(chalk.green(`✅ No issues found!\n`));
72
+ } else {
73
+ for (const issue of report.issues) {
74
+ const sevColor = issue.severity === 'error' ? chalk.red : (issue.severity === 'warning' ? chalk.yellow : chalk.cyan);
75
+ const severityLabel = issue.severity.toUpperCase().padEnd(7);
76
+ const ruleLabel = issue.rule.padEnd(30);
77
+ console.log(`${sevColor(severityLabel)} ${chalk.dim(ruleLabel)} ${issue.message}`);
78
+ }
79
+ }
80
+
81
+ console.log(`\n` + chalk.bold(`Summary:`));
82
+ console.log(chalk.red(`Errors: ${report.summary.errors}`));
83
+ console.log(chalk.yellow(`Warnings: ${report.summary.warnings}`));
84
+ console.log(chalk.cyan(`Info: ${report.summary.info}\n`));
85
+ }
@@ -0,0 +1,49 @@
1
+ import { LintProfile } from './lint-types.js';
2
+
3
+ export const defaultProfiles: Record<string, LintProfile> = {
4
+ feature: {
5
+ duplicateId: 'error',
6
+ invalidYaml: 'error',
7
+ brokenRelation: 'error',
8
+ missingRequiredFields: 'warning',
9
+ requirementWithoutImplementation: 'info',
10
+ requirementWithoutTest: 'info',
11
+ codeWithoutRequirement: 'info',
12
+ closedArtifactWithPendingRelations: 'warning',
13
+ orphanArtifact: 'info',
14
+ invalidRelationType: 'error',
15
+ invalidArtifactType: 'error',
16
+ invalidLayerRelation: 'info',
17
+ missingOwnership: 'info',
18
+ },
19
+ develop: {
20
+ duplicateId: 'error',
21
+ invalidYaml: 'error',
22
+ brokenRelation: 'error',
23
+ missingRequiredFields: 'error',
24
+ requirementWithoutImplementation: 'warning',
25
+ requirementWithoutTest: 'warning',
26
+ codeWithoutRequirement: 'warning',
27
+ closedArtifactWithPendingRelations: 'warning',
28
+ orphanArtifact: 'warning',
29
+ invalidRelationType: 'error',
30
+ invalidArtifactType: 'error',
31
+ invalidLayerRelation: 'warning',
32
+ missingOwnership: 'warning',
33
+ },
34
+ release: {
35
+ duplicateId: 'error',
36
+ invalidYaml: 'error',
37
+ brokenRelation: 'error',
38
+ missingRequiredFields: 'error',
39
+ requirementWithoutImplementation: 'error',
40
+ requirementWithoutTest: 'error',
41
+ codeWithoutRequirement: 'error',
42
+ closedArtifactWithPendingRelations: 'error',
43
+ orphanArtifact: 'error',
44
+ invalidRelationType: 'error',
45
+ invalidArtifactType: 'error',
46
+ invalidLayerRelation: 'error',
47
+ missingOwnership: 'error',
48
+ }
49
+ };