@likec4/leanix-bridge 1.53.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 +21 -0
- package/README.md +145 -0
- package/dist/index.d.mts +559 -0
- package/dist/index.mjs +1110 -0
- package/package.json +62 -0
- package/src/adr-generation.ts +105 -0
- package/src/contracts.ts +96 -0
- package/src/drawio-leanix-roundtrip.ts +51 -0
- package/src/drift-report.ts +61 -0
- package/src/fixture-model.ts +52 -0
- package/src/governance-checks.ts +103 -0
- package/src/impact-report.ts +38 -0
- package/src/index.ts +109 -0
- package/src/leanix-api-client.ts +138 -0
- package/src/leanix-graphql-operations.ts +160 -0
- package/src/leanix-inventory-snapshot.ts +263 -0
- package/src/mapping.ts +88 -0
- package/src/model-input.ts +36 -0
- package/src/reconcile.ts +227 -0
- package/src/report.ts +73 -0
- package/src/sync-to-leanix.ts +352 -0
- package/src/to-bridge-manifest.ts +85 -0
- package/src/to-leanix-inventory-dry-run.ts +105 -0
- package/src/validate.ts +106 -0
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@likec4/leanix-bridge",
|
|
3
|
+
"version": "1.53.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"bugs": "https://github.com/likec4/likec4/issues",
|
|
6
|
+
"homepage": "https://likec4.dev",
|
|
7
|
+
"author": "Denis Davydkov <denis@davydkov.com>",
|
|
8
|
+
"description": "Bridge from LikeC4 semantic model to LeanIX-shaped inventory artifacts (supports optional live sync or dry-run modes)",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"src",
|
|
12
|
+
"**/package.json",
|
|
13
|
+
"!**/__*/**",
|
|
14
|
+
"!**/*.spec.*",
|
|
15
|
+
"!**/*.snap",
|
|
16
|
+
"!**/*.map"
|
|
17
|
+
],
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/likec4/likec4.git",
|
|
21
|
+
"directory": "packages/leanix-bridge"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"sources": "./src/index.ts",
|
|
28
|
+
"default": {
|
|
29
|
+
"types": "./dist/index.d.mts",
|
|
30
|
+
"import": "./dist/index.mjs",
|
|
31
|
+
"default": "./dist/index.mjs"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"registry": "https://registry.npmjs.org",
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"remeda": "^2.33.6",
|
|
41
|
+
"@likec4/core": "1.53.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "~22.19.15",
|
|
45
|
+
"typescript": "5.9.3",
|
|
46
|
+
"obuild": "0.4.31",
|
|
47
|
+
"vitest": "4.1.0",
|
|
48
|
+
"@likec4/devops": "1.42.0",
|
|
49
|
+
"@likec4/tsconfig": "1.53.0"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"typecheck": "tsc -b --verbose",
|
|
53
|
+
"build": "obuild",
|
|
54
|
+
"lint": "oxlint --type-aware",
|
|
55
|
+
"clean": "likec4ops clean",
|
|
56
|
+
"pack": "pnpm pack",
|
|
57
|
+
"test": "vitest run --no-isolate",
|
|
58
|
+
"test:watch": "vitest"
|
|
59
|
+
},
|
|
60
|
+
"types": "dist/index.d.mts",
|
|
61
|
+
"module": "dist/index.mjs"
|
|
62
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3: Generate an Architecture Decision Record (ADR) from reconciliation and/or impact.
|
|
3
|
+
* Output is markdown; no DSL generation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { DriftReport } from './drift-report'
|
|
7
|
+
import type { ImpactReport } from './impact-report'
|
|
8
|
+
import type { ReconciliationResult } from './reconcile'
|
|
9
|
+
|
|
10
|
+
/** Returns date part YYYY-MM-DD from ISO timestamp (G25, G5). Handles short strings defensively. */
|
|
11
|
+
function formatIsoDateString(iso: string): string {
|
|
12
|
+
if (typeof iso !== 'string' || iso.length < 10) return iso
|
|
13
|
+
return iso.slice(0, 10)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Shared ADR header lines (title, status, date). Reduces duplication between ADR generators. */
|
|
17
|
+
function buildAdrHeader(title: string, status: string, dateIso: string): string[] {
|
|
18
|
+
return [
|
|
19
|
+
`# ${title}`,
|
|
20
|
+
'',
|
|
21
|
+
`- Status: ${status}`,
|
|
22
|
+
`- Date: ${formatIsoDateString(dateIso)}`,
|
|
23
|
+
'',
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Options for ADR generation (title, status, optional impact report). */
|
|
28
|
+
export interface AdrGenerationOptions {
|
|
29
|
+
title?: string
|
|
30
|
+
/** ADR status line. Default: "Proposed". */
|
|
31
|
+
status?: string
|
|
32
|
+
/** Optional impact report to include. */
|
|
33
|
+
impact?: ImpactReport
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generates a short ADR-style markdown document from a reconciliation result (and optional impact).
|
|
38
|
+
* Use for governance or audit trail; does not modify any data.
|
|
39
|
+
*/
|
|
40
|
+
export function generateAdrFromReconciliation(
|
|
41
|
+
reconciliation: ReconciliationResult,
|
|
42
|
+
options: AdrGenerationOptions = {},
|
|
43
|
+
): string {
|
|
44
|
+
const title = options.title ?? 'LeanIX inventory reconciliation'
|
|
45
|
+
const status = options.status ?? 'Proposed'
|
|
46
|
+
const impact = options.impact
|
|
47
|
+
const lines: string[] = [
|
|
48
|
+
...buildAdrHeader(title, status, reconciliation.generatedAt),
|
|
49
|
+
'## Context',
|
|
50
|
+
'',
|
|
51
|
+
`Reconciliation of LikeC4 manifest (project: ${reconciliation.manifestProjectId}) with LeanIX inventory snapshot (${
|
|
52
|
+
formatIsoDateString(reconciliation.snapshotGeneratedAt)
|
|
53
|
+
}).`,
|
|
54
|
+
'',
|
|
55
|
+
'## Result',
|
|
56
|
+
'',
|
|
57
|
+
`- Matched: ${reconciliation.summary.matched}`,
|
|
58
|
+
`- Unmatched in LikeC4: ${reconciliation.summary.unmatchedInLikec4}`,
|
|
59
|
+
`- Unmatched in LeanIX: ${reconciliation.summary.unmatchedInLeanix}`,
|
|
60
|
+
`- Ambiguous: ${reconciliation.summary.ambiguous}`,
|
|
61
|
+
'',
|
|
62
|
+
]
|
|
63
|
+
if (impact) {
|
|
64
|
+
lines.push('## Impact (if sync applied)', '', impact.impactSummary, '', '')
|
|
65
|
+
}
|
|
66
|
+
lines.push(
|
|
67
|
+
'## Decision',
|
|
68
|
+
'',
|
|
69
|
+
'LikeC4 remains the single source of truth; LeanIX is an adapter. No DSL is auto-generated from LeanIX.',
|
|
70
|
+
'',
|
|
71
|
+
)
|
|
72
|
+
return lines.join('\n')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Options for ADR generation from a drift report (subset of AdrGenerationOptions). */
|
|
76
|
+
export type DriftAdrGenerationOptions = Pick<AdrGenerationOptions, 'title' | 'status'>
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Generates a short ADR-style markdown from a drift report.
|
|
80
|
+
*/
|
|
81
|
+
export function generateAdrFromDriftReport(
|
|
82
|
+
drift: DriftReport,
|
|
83
|
+
options: DriftAdrGenerationOptions = {},
|
|
84
|
+
): string {
|
|
85
|
+
const title = options.title ?? 'LeanIX drift report'
|
|
86
|
+
const status = options.status ?? 'Proposed'
|
|
87
|
+
return [
|
|
88
|
+
...buildAdrHeader(title, status, drift.generatedAt),
|
|
89
|
+
'## Drift status',
|
|
90
|
+
'',
|
|
91
|
+
drift.description,
|
|
92
|
+
'',
|
|
93
|
+
'## Summary',
|
|
94
|
+
'',
|
|
95
|
+
`- Matched: ${drift.summary.matched}`,
|
|
96
|
+
`- Unmatched in LikeC4: ${drift.summary.unmatchedInLikec4}`,
|
|
97
|
+
`- Unmatched in LeanIX: ${drift.summary.unmatchedInLeanix}`,
|
|
98
|
+
`- Ambiguous: ${drift.summary.ambiguous}`,
|
|
99
|
+
'',
|
|
100
|
+
'## Decision',
|
|
101
|
+
'',
|
|
102
|
+
'Review drift findings and determine whether synchronization or manual remediation is required.',
|
|
103
|
+
'',
|
|
104
|
+
].join('\n')
|
|
105
|
+
}
|
package/src/contracts.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical bridge contracts for LikeC4 ↔ LeanIX interoperability.
|
|
3
|
+
* LikeC4 remains the semantic source of truth; external IDs are provider-scoped.
|
|
4
|
+
* Uses readFileSync (not require) so when bundled into likec4 CLI the bundler
|
|
5
|
+
* does not emit require('../package.json') which fails in the tarball layout.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from 'node:fs'
|
|
9
|
+
import { dirname, join } from 'node:path'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
|
|
12
|
+
const _dir = dirname(fileURLToPath(import.meta.url))
|
|
13
|
+
function readVersion(): string {
|
|
14
|
+
try {
|
|
15
|
+
const pkg = JSON.parse(readFileSync(join(_dir, '..', 'package.json'), 'utf8')) as { version?: string }
|
|
16
|
+
return pkg.version ?? '0.1.0'
|
|
17
|
+
} catch {
|
|
18
|
+
const pkg = JSON.parse(readFileSync(join(_dir, '..', '..', 'package.json'), 'utf8')) as { version?: string }
|
|
19
|
+
return pkg.version ?? '0.1.0'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Single source of truth: must match package.json version. */
|
|
23
|
+
export const BRIDGE_VERSION: string = readVersion()
|
|
24
|
+
|
|
25
|
+
/** Semantic anchor: LikeC4 FQN (e.g. cloud.backend.api) */
|
|
26
|
+
export type CanonicalId = string
|
|
27
|
+
|
|
28
|
+
/** Provider-scoped external identifier (e.g. LeanIX factSheetId) */
|
|
29
|
+
export type ExternalId = string
|
|
30
|
+
|
|
31
|
+
/** LikeC4 view id (e.g. index, saas, landscape.overview) */
|
|
32
|
+
export type ViewId = string
|
|
33
|
+
|
|
34
|
+
/** LikeC4 relation id; for composite key use sourceFqn|targetFqn|relationId */
|
|
35
|
+
export type RelationId = string
|
|
36
|
+
|
|
37
|
+
/** Version of the manifest schema */
|
|
38
|
+
export type ManifestVersion = string
|
|
39
|
+
|
|
40
|
+
/** ISO timestamp when the artifact was generated */
|
|
41
|
+
export type GeneratedAt = string
|
|
42
|
+
|
|
43
|
+
/** Version of the bridge that produced the artifact */
|
|
44
|
+
export type BridgeVersion = string
|
|
45
|
+
|
|
46
|
+
/** Name or id of the mapping profile used */
|
|
47
|
+
export type MappingProfile = string
|
|
48
|
+
|
|
49
|
+
/** Provider name (e.g. leanix, drawio) */
|
|
50
|
+
export type Provider = string
|
|
51
|
+
|
|
52
|
+
/** External IDs for a single provider (e.g. factSheetId, externalId). */
|
|
53
|
+
export interface ProviderExternalIds {
|
|
54
|
+
factSheetId?: string
|
|
55
|
+
externalId?: string
|
|
56
|
+
[key: string]: string | undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Entity entry in the manifest: canonical id + optional external IDs per provider. */
|
|
60
|
+
export interface ManifestEntity {
|
|
61
|
+
canonicalId: CanonicalId
|
|
62
|
+
external?: Partial<Record<Provider, ProviderExternalIds>>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** View entry in the manifest (viewId + optional provider external ids). */
|
|
66
|
+
export interface ManifestView {
|
|
67
|
+
viewId: ViewId
|
|
68
|
+
external?: Partial<Record<Provider, Record<string, string>>>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Relation entry: composite key and optional external relation id per provider. */
|
|
72
|
+
export interface ManifestRelation {
|
|
73
|
+
relationId: RelationId
|
|
74
|
+
sourceFqn: CanonicalId
|
|
75
|
+
targetFqn: CanonicalId
|
|
76
|
+
compositeKey: string
|
|
77
|
+
external?: Partial<Record<Provider, { relationId?: string; [key: string]: string | undefined }>>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Identity manifest: canonical IDs and provider-scoped external IDs */
|
|
81
|
+
export interface BridgeManifest {
|
|
82
|
+
manifestVersion: ManifestVersion
|
|
83
|
+
generatedAt: GeneratedAt
|
|
84
|
+
bridgeVersion: BridgeVersion
|
|
85
|
+
mappingProfile: MappingProfile
|
|
86
|
+
projectId: string
|
|
87
|
+
entities: Record<CanonicalId, ManifestEntity>
|
|
88
|
+
views: Record<ViewId, ManifestView>
|
|
89
|
+
relations: ManifestRelation[]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Manifest schema version (semantic; must match parser). */
|
|
93
|
+
export const BRIDGE_MANIFEST_VERSION = '1.0'
|
|
94
|
+
|
|
95
|
+
/** Provider identifier for LeanIX (single source of truth for external.leanix). */
|
|
96
|
+
export const LEANIX_PROVIDER = 'leanix' as const
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Draw.io ↔ LeanIX round-trip: mapping between bridge manifest (with LeanIX external IDs)
|
|
3
|
+
* and diagram identity (likec4Id, likec4RelationId).
|
|
4
|
+
* Use after syncToLeanix to get a mapping for re-export or for annotating diagrams.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type BridgeManifest, type CanonicalId, LEANIX_PROVIDER } from './contracts'
|
|
8
|
+
|
|
9
|
+
/** Mapping from LikeC4 canonical id to LeanIX identity (fact sheet id or externalId for Draw.io cells or export). */
|
|
10
|
+
export interface DrawioLeanixMapping {
|
|
11
|
+
likec4IdToLeanixId: Record<CanonicalId, string>
|
|
12
|
+
/** Composite key (sourceFqn|targetFqn|relationId) -> LeanIX relation id. */
|
|
13
|
+
relationKeyToLeanixRelationId: Record<string, string>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Collects likec4Id → LeanIX identity (factSheetId or externalId) from manifest entities that have LeanIX external.
|
|
18
|
+
* Single responsibility: one level of abstraction for entity extraction.
|
|
19
|
+
*/
|
|
20
|
+
function collectLikec4IdToLeanixId(manifest: BridgeManifest): Record<CanonicalId, string> {
|
|
21
|
+
const out: Record<CanonicalId, string> = {}
|
|
22
|
+
for (const [canonicalId, entity] of Object.entries(manifest.entities)) {
|
|
23
|
+
const leanixId = entity.external?.[LEANIX_PROVIDER]?.factSheetId ?? entity.external?.[LEANIX_PROVIDER]?.externalId
|
|
24
|
+
if (leanixId) out[canonicalId] = leanixId
|
|
25
|
+
}
|
|
26
|
+
return out
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Collects compositeKey → LeanIX relationId from manifest relations that have LeanIX external.
|
|
31
|
+
* Single responsibility: one level of abstraction for relation extraction.
|
|
32
|
+
*/
|
|
33
|
+
function collectRelationKeyToLeanixRelationId(manifest: BridgeManifest): Record<string, string> {
|
|
34
|
+
const out: Record<string, string> = {}
|
|
35
|
+
for (const rel of manifest.relations) {
|
|
36
|
+
const leanixRelId = rel.external?.[LEANIX_PROVIDER]?.relationId
|
|
37
|
+
if (leanixRelId) out[rel.compositeKey] = leanixRelId
|
|
38
|
+
}
|
|
39
|
+
return out
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Builds a mapping from manifest (after sync) for use in Draw.io bridge-managed export
|
|
44
|
+
* or when re-importing from LeanIX. Elements can store leanixFactSheetId in style for round-trip.
|
|
45
|
+
*/
|
|
46
|
+
export function manifestToDrawioLeanixMapping(manifest: BridgeManifest): DrawioLeanixMapping {
|
|
47
|
+
return {
|
|
48
|
+
likec4IdToLeanixId: collectLikec4IdToLeanixId(manifest),
|
|
49
|
+
relationKeyToLeanixRelationId: collectRelationKeyToLeanixRelationId(manifest),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3: Drift detection from reconciliation result.
|
|
3
|
+
* Produces a structured drift report (in sync, LikeC4 ahead, LeanIX ahead, or diverged).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ReconciliationResult } from './reconcile'
|
|
7
|
+
|
|
8
|
+
/** Drift status: in_sync, likec4_ahead, leanix_ahead, or diverged. */
|
|
9
|
+
export type DriftStatus = 'in_sync' | 'likec4_ahead' | 'leanix_ahead' | 'diverged'
|
|
10
|
+
|
|
11
|
+
/** Drift report (status, summary, description). */
|
|
12
|
+
export interface DriftReport {
|
|
13
|
+
generatedAt: string
|
|
14
|
+
manifestProjectId: string
|
|
15
|
+
snapshotGeneratedAt: string
|
|
16
|
+
status: DriftStatus
|
|
17
|
+
summary: ReconciliationResult['summary']
|
|
18
|
+
/** Short human-readable drift description. */
|
|
19
|
+
description: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Builds a drift report from a reconciliation result.
|
|
24
|
+
* In_sync: all matched, no unmatched, no ambiguous. Likec4_ahead: only unmatched in LikeC4. Leanix_ahead: only unmatched in LeanIX. Diverged: mixed or ambiguous.
|
|
25
|
+
*/
|
|
26
|
+
export function buildDriftReport(reconciliation: ReconciliationResult): DriftReport {
|
|
27
|
+
const { summary } = reconciliation
|
|
28
|
+
const hasUnmatchedLikec4 = summary.unmatchedInLikec4 > 0
|
|
29
|
+
const hasUnmatchedLeanix = summary.unmatchedInLeanix > 0
|
|
30
|
+
const hasAmbiguous = summary.ambiguous > 0
|
|
31
|
+
|
|
32
|
+
let status: DriftStatus
|
|
33
|
+
let description: string
|
|
34
|
+
switch (true) {
|
|
35
|
+
case hasAmbiguous || (hasUnmatchedLikec4 && hasUnmatchedLeanix):
|
|
36
|
+
status = 'diverged'
|
|
37
|
+
description =
|
|
38
|
+
`${summary.ambiguous} ambiguous; ${summary.unmatchedInLikec4} only in LikeC4, ${summary.unmatchedInLeanix} only in LeanIX`
|
|
39
|
+
break
|
|
40
|
+
case hasUnmatchedLikec4 && !hasUnmatchedLeanix:
|
|
41
|
+
status = 'likec4_ahead'
|
|
42
|
+
description = `${summary.unmatchedInLikec4} element(s) in LikeC4 not yet in LeanIX`
|
|
43
|
+
break
|
|
44
|
+
case hasUnmatchedLeanix && !hasUnmatchedLikec4:
|
|
45
|
+
status = 'leanix_ahead'
|
|
46
|
+
description = `${summary.unmatchedInLeanix} fact sheet(s) in LeanIX not in LikeC4`
|
|
47
|
+
break
|
|
48
|
+
default:
|
|
49
|
+
status = 'in_sync'
|
|
50
|
+
description = `${summary.matched} matched; no drift`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
generatedAt: reconciliation.generatedAt,
|
|
55
|
+
manifestProjectId: reconciliation.manifestProjectId,
|
|
56
|
+
snapshotGeneratedAt: reconciliation.snapshotGeneratedAt,
|
|
57
|
+
status,
|
|
58
|
+
summary: reconciliation.summary,
|
|
59
|
+
description,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { BridgeModelInput } from './model-input'
|
|
2
|
+
|
|
3
|
+
/** Minimal in-memory model for tests */
|
|
4
|
+
export function createFixtureModel(overrides: Partial<{
|
|
5
|
+
projectId: string
|
|
6
|
+
elements: Array<
|
|
7
|
+
{
|
|
8
|
+
id: string
|
|
9
|
+
kind: string
|
|
10
|
+
title: string
|
|
11
|
+
tags?: string[]
|
|
12
|
+
technology?: string | null
|
|
13
|
+
metadata?: Record<string, unknown>
|
|
14
|
+
}
|
|
15
|
+
>
|
|
16
|
+
relations: Array<{ id: string; source: string; target: string; kind?: string | null; title?: string | null }>
|
|
17
|
+
views: Array<{ id: string }>
|
|
18
|
+
}> = {}): BridgeModelInput {
|
|
19
|
+
const projectId = overrides.projectId ?? 'test-project'
|
|
20
|
+
const elements = overrides.elements ?? [
|
|
21
|
+
{ id: 'cloud', kind: 'system', title: 'Cloud', tags: [], metadata: {} },
|
|
22
|
+
{ id: 'cloud.backend', kind: 'container', title: 'Backend', tags: [], metadata: {} },
|
|
23
|
+
{ id: 'cloud.backend.api', kind: 'component', title: 'API', tags: ['core'], technology: 'Node', metadata: {} },
|
|
24
|
+
]
|
|
25
|
+
const relations = overrides.relations ?? [
|
|
26
|
+
{ id: 'r1', source: 'cloud', target: 'cloud.backend', kind: 'contains', title: null },
|
|
27
|
+
{ id: 'r2', source: 'cloud.backend', target: 'cloud.backend.api', kind: 'contains', title: null },
|
|
28
|
+
]
|
|
29
|
+
const views = overrides.views ?? [{ id: 'index' }, { id: 'landscape' }]
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
projectId,
|
|
33
|
+
elements: () =>
|
|
34
|
+
elements.map(e => ({
|
|
35
|
+
id: e.id,
|
|
36
|
+
kind: e.kind,
|
|
37
|
+
title: e.title,
|
|
38
|
+
tags: e.tags ?? [],
|
|
39
|
+
technology: e.technology ?? null,
|
|
40
|
+
getMetadata: () => e.metadata ?? {},
|
|
41
|
+
})),
|
|
42
|
+
relationships: () =>
|
|
43
|
+
relations.map(r => ({
|
|
44
|
+
id: r.id,
|
|
45
|
+
source: { id: r.source },
|
|
46
|
+
target: { id: r.target },
|
|
47
|
+
kind: r.kind ?? null,
|
|
48
|
+
title: r.title ?? null,
|
|
49
|
+
})),
|
|
50
|
+
views: () => views.map(v => ({ id: v.id })),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3: Governance checks on reconciliation result.
|
|
3
|
+
* Configurable rules (no ambiguous, all LikeC4 matched, no orphans in LeanIX); returns pass/fail per check.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ReconciliationResult } from './reconcile'
|
|
7
|
+
|
|
8
|
+
/** Single governance check result (id, name, passed, optional message). */
|
|
9
|
+
export interface GovernanceCheckResult {
|
|
10
|
+
id: string
|
|
11
|
+
name: string
|
|
12
|
+
passed: boolean
|
|
13
|
+
message?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface GovernanceReport {
|
|
17
|
+
passed: boolean
|
|
18
|
+
generatedAt: string
|
|
19
|
+
checks: GovernanceCheckResult[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Options for runGovernanceChecks (noAmbiguous, allLikec4Matched, noOrphanInLeanix). */
|
|
23
|
+
export interface GovernanceCheckOptions {
|
|
24
|
+
/** Fail if any ambiguous matches. Default: true. */
|
|
25
|
+
noAmbiguous?: boolean
|
|
26
|
+
/** Fail if any LikeC4 entity has no match in LeanIX. Default: false. */
|
|
27
|
+
allLikec4Matched?: boolean
|
|
28
|
+
/** Fail if any LeanIX fact sheet has no match in LikeC4. Default: false. */
|
|
29
|
+
noOrphanInLeanix?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DEFAULT_OPTIONS: Required<GovernanceCheckOptions> = {
|
|
33
|
+
noAmbiguous: true,
|
|
34
|
+
allLikec4Matched: false,
|
|
35
|
+
noOrphanInLeanix: false,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Builds a single check result (G5: reduce duplication). */
|
|
39
|
+
function buildCheck(
|
|
40
|
+
id: string,
|
|
41
|
+
name: string,
|
|
42
|
+
passed: boolean,
|
|
43
|
+
failedMessage?: string,
|
|
44
|
+
): GovernanceCheckResult {
|
|
45
|
+
const result: GovernanceCheckResult = { id, name, passed }
|
|
46
|
+
if (!passed && failedMessage !== undefined) {
|
|
47
|
+
result.message = failedMessage
|
|
48
|
+
}
|
|
49
|
+
return result
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Runs governance checks on a reconciliation result.
|
|
54
|
+
* Returns a report with one entry per check; overall passed iff all checks pass.
|
|
55
|
+
*/
|
|
56
|
+
export function runGovernanceChecks(
|
|
57
|
+
reconciliation: ReconciliationResult,
|
|
58
|
+
options: GovernanceCheckOptions = {},
|
|
59
|
+
): GovernanceReport {
|
|
60
|
+
const opts = { ...DEFAULT_OPTIONS, ...options }
|
|
61
|
+
const checks: GovernanceCheckResult[] = []
|
|
62
|
+
const { summary } = reconciliation
|
|
63
|
+
|
|
64
|
+
if (opts.noAmbiguous) {
|
|
65
|
+
checks.push(
|
|
66
|
+
buildCheck(
|
|
67
|
+
'noAmbiguous',
|
|
68
|
+
'No ambiguous matches',
|
|
69
|
+
summary.ambiguous === 0,
|
|
70
|
+
`${summary.ambiguous} ambiguous match(es)`,
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (opts.allLikec4Matched) {
|
|
76
|
+
checks.push(
|
|
77
|
+
buildCheck(
|
|
78
|
+
'allLikec4Matched',
|
|
79
|
+
'All LikeC4 elements matched in LeanIX',
|
|
80
|
+
summary.unmatchedInLikec4 === 0,
|
|
81
|
+
`${summary.unmatchedInLikec4} unmatched in LikeC4`,
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (opts.noOrphanInLeanix) {
|
|
87
|
+
checks.push(
|
|
88
|
+
buildCheck(
|
|
89
|
+
'noOrphanInLeanix',
|
|
90
|
+
'No LeanIX fact sheets without LikeC4 match',
|
|
91
|
+
summary.unmatchedInLeanix === 0,
|
|
92
|
+
`${summary.unmatchedInLeanix} unmatched in LeanIX`,
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const passed = checks.every(c => c.passed)
|
|
98
|
+
return {
|
|
99
|
+
passed,
|
|
100
|
+
generatedAt: new Date().toISOString(),
|
|
101
|
+
checks,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3: Impact analysis from a sync plan.
|
|
3
|
+
* Produces a structured report of what would change if the plan were applied (no writes).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SyncPlan, SyncPlanSummary } from './sync-to-leanix'
|
|
7
|
+
|
|
8
|
+
/** Impact report from a sync plan (summary counts and human-readable impact summary). */
|
|
9
|
+
export interface ImpactReport {
|
|
10
|
+
generatedAt: string
|
|
11
|
+
projectId: string
|
|
12
|
+
mappingProfile: string
|
|
13
|
+
summary: SyncPlanSummary
|
|
14
|
+
/** Human-readable one-line impact summary. */
|
|
15
|
+
impactSummary: string
|
|
16
|
+
hasErrors: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Builds an impact report from a sync plan (read-only).
|
|
21
|
+
* Use before applying sync to understand what would be created/updated.
|
|
22
|
+
*/
|
|
23
|
+
export function impactReportFromSyncPlan(plan: SyncPlan): ImpactReport {
|
|
24
|
+
const { summary, errors } = plan
|
|
25
|
+
const parts: string[] = []
|
|
26
|
+
if (summary.factSheetsToCreate > 0) parts.push(`${summary.factSheetsToCreate} fact sheet(s) to create`)
|
|
27
|
+
if (summary.factSheetsToUpdate > 0) parts.push(`${summary.factSheetsToUpdate} fact sheet(s) to update`)
|
|
28
|
+
if (summary.relationsToCreate > 0) parts.push(`${summary.relationsToCreate} relation(s) to create`)
|
|
29
|
+
const impactSummary = parts.length > 0 ? parts.join('; ') : 'No changes'
|
|
30
|
+
return {
|
|
31
|
+
generatedAt: plan.generatedAt,
|
|
32
|
+
projectId: plan.projectId,
|
|
33
|
+
mappingProfile: plan.mappingProfile,
|
|
34
|
+
summary,
|
|
35
|
+
impactSummary: errors.length > 0 ? `${impactSummary} (${errors.length} plan error(s))` : impactSummary,
|
|
36
|
+
hasErrors: errors.length > 0,
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @likec4/leanix-bridge
|
|
3
|
+
*
|
|
4
|
+
* Bridge from LikeC4 semantic model to LeanIX-shaped inventory artifacts.
|
|
5
|
+
* Supports dry-run artifacts, optional LeanIX API sync (LeanixApiClient, syncToLeanix),
|
|
6
|
+
* and Draw.io round-trip helpers (manifestToDrawioLeanixMapping). LikeC4 remains canonical.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
BRIDGE_MANIFEST_VERSION,
|
|
11
|
+
BRIDGE_VERSION,
|
|
12
|
+
LEANIX_PROVIDER,
|
|
13
|
+
} from './contracts'
|
|
14
|
+
export type {
|
|
15
|
+
BridgeManifest,
|
|
16
|
+
BridgeVersion,
|
|
17
|
+
CanonicalId,
|
|
18
|
+
ExternalId,
|
|
19
|
+
GeneratedAt,
|
|
20
|
+
ManifestEntity,
|
|
21
|
+
ManifestRelation,
|
|
22
|
+
ManifestVersion,
|
|
23
|
+
ManifestView,
|
|
24
|
+
MappingProfile,
|
|
25
|
+
Provider,
|
|
26
|
+
ProviderExternalIds,
|
|
27
|
+
RelationId as BridgeRelationId,
|
|
28
|
+
ViewId as BridgeViewId,
|
|
29
|
+
} from './contracts'
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
DEFAULT_LEANIX_MAPPING,
|
|
33
|
+
FALLBACK_FACT_SHEET_TYPE,
|
|
34
|
+
FALLBACK_RELATION_TYPE,
|
|
35
|
+
getFactSheetType,
|
|
36
|
+
getRelationType,
|
|
37
|
+
mergeWithDefault,
|
|
38
|
+
} from './mapping'
|
|
39
|
+
export type { LeanixMappingConfig } from './mapping'
|
|
40
|
+
|
|
41
|
+
export type { BridgeElementLike, BridgeModelInput, BridgeRelationLike, BridgeViewLike } from './model-input'
|
|
42
|
+
|
|
43
|
+
export { toBridgeManifest } from './to-bridge-manifest'
|
|
44
|
+
export type { ToBridgeManifestOptions } from './to-bridge-manifest'
|
|
45
|
+
|
|
46
|
+
export { toLeanixInventoryDryRun } from './to-leanix-inventory-dry-run'
|
|
47
|
+
export type {
|
|
48
|
+
LeanixFactSheetDryRun,
|
|
49
|
+
LeanixInventoryDryRun,
|
|
50
|
+
LeanixRelationDryRun,
|
|
51
|
+
ToLeanixInventoryDryRunOptions,
|
|
52
|
+
} from './to-leanix-inventory-dry-run'
|
|
53
|
+
|
|
54
|
+
export { buildBridgeReport } from './report'
|
|
55
|
+
export type { BridgeReport } from './report'
|
|
56
|
+
|
|
57
|
+
export { LeanixApiClient } from './leanix-api-client'
|
|
58
|
+
export type { GraphQLResponse, LeanixApiClientConfig } from './leanix-api-client'
|
|
59
|
+
export { LeanixApiError } from './leanix-api-client'
|
|
60
|
+
|
|
61
|
+
export { planSyncToLeanix, syncToLeanix } from './sync-to-leanix'
|
|
62
|
+
export type {
|
|
63
|
+
PlanSyncToLeanixOptions,
|
|
64
|
+
SyncPlan,
|
|
65
|
+
SyncPlanFactSheetEntry,
|
|
66
|
+
SyncPlanRelationEntry,
|
|
67
|
+
SyncPlanSummary,
|
|
68
|
+
SyncToLeanixOptions,
|
|
69
|
+
SyncToLeanixResult,
|
|
70
|
+
} from './sync-to-leanix'
|
|
71
|
+
|
|
72
|
+
export { manifestToDrawioLeanixMapping } from './drawio-leanix-roundtrip'
|
|
73
|
+
export type { DrawioLeanixMapping } from './drawio-leanix-roundtrip'
|
|
74
|
+
|
|
75
|
+
export { fetchLeanixInventorySnapshot } from './leanix-inventory-snapshot'
|
|
76
|
+
export type {
|
|
77
|
+
FetchLeanixInventorySnapshotOptions,
|
|
78
|
+
LeanixFactSheetSnapshotItem,
|
|
79
|
+
LeanixInventorySnapshot,
|
|
80
|
+
LeanixRelationSnapshotItem,
|
|
81
|
+
} from './leanix-inventory-snapshot'
|
|
82
|
+
|
|
83
|
+
export { reconcileInventoryWithManifest } from './reconcile'
|
|
84
|
+
export type {
|
|
85
|
+
AmbiguousMatch,
|
|
86
|
+
MatchedPair,
|
|
87
|
+
ReconcileOptions,
|
|
88
|
+
ReconciliationResult,
|
|
89
|
+
UnmatchedInLeanix,
|
|
90
|
+
UnmatchedInLikec4,
|
|
91
|
+
} from './reconcile'
|
|
92
|
+
|
|
93
|
+
export { impactReportFromSyncPlan } from './impact-report'
|
|
94
|
+
export type { ImpactReport } from './impact-report'
|
|
95
|
+
|
|
96
|
+
export { buildDriftReport } from './drift-report'
|
|
97
|
+
export type { DriftReport, DriftStatus } from './drift-report'
|
|
98
|
+
|
|
99
|
+
export { generateAdrFromDriftReport, generateAdrFromReconciliation } from './adr-generation'
|
|
100
|
+
export type { AdrGenerationOptions, DriftAdrGenerationOptions } from './adr-generation'
|
|
101
|
+
|
|
102
|
+
export { runGovernanceChecks } from './governance-checks'
|
|
103
|
+
export type {
|
|
104
|
+
GovernanceCheckOptions,
|
|
105
|
+
GovernanceCheckResult,
|
|
106
|
+
GovernanceReport,
|
|
107
|
+
} from './governance-checks'
|
|
108
|
+
|
|
109
|
+
export { isBridgeManifest, isLeanixInventorySnapshot } from './validate'
|