yarramate 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.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/adapter-mapping.d.ts +40 -0
- package/dist/adapter-mapping.js +200 -0
- package/dist/adapters/graphify-cli.d.ts +3 -0
- package/dist/adapters/graphify-cli.js +127 -0
- package/dist/adapters/graphify-entry.d.ts +1 -0
- package/dist/adapters/graphify-entry.js +1 -0
- package/dist/adapters/graphify.d.ts +23 -0
- package/dist/adapters/graphify.js +47 -0
- package/dist/adapters/likec4-cli.d.ts +3 -0
- package/dist/adapters/likec4-cli.js +662 -0
- package/dist/adapters/likec4-export.d.ts +25 -0
- package/dist/adapters/likec4-export.js +204 -0
- package/dist/adapters/likec4-kind-mapping.d.ts +22 -0
- package/dist/adapters/likec4-kind-mapping.js +60 -0
- package/dist/adapters/likec4-prepare.d.ts +28 -0
- package/dist/adapters/likec4-prepare.js +154 -0
- package/dist/adapters/likec4-project.d.ts +58 -0
- package/dist/adapters/likec4-project.js +176 -0
- package/dist/adapters/likec4.d.ts +4 -0
- package/dist/adapters/likec4.js +4 -0
- package/dist/architecture-state.d.ts +22 -0
- package/dist/architecture-state.js +63 -0
- package/dist/check-command.d.ts +2 -0
- package/dist/check-command.js +232 -0
- package/dist/cli-support.d.ts +24 -0
- package/dist/cli-support.js +81 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +575 -0
- package/dist/compiler.d.ts +66 -0
- package/dist/compiler.js +940 -0
- package/dist/core-contract.d.ts +43 -0
- package/dist/core-contract.js +162 -0
- package/dist/evidence.d.ts +58 -0
- package/dist/evidence.js +161 -0
- package/dist/graph.d.ts +2 -0
- package/dist/graph.js +37 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +9 -0
- package/dist/profile.d.ts +30 -0
- package/dist/profile.js +162 -0
- package/dist/projection.d.ts +42 -0
- package/dist/projection.js +183 -0
- package/dist/reconciliation.d.ts +26 -0
- package/dist/reconciliation.js +47 -0
- package/dist/source-document.d.ts +24 -0
- package/dist/source-document.js +80 -0
- package/dist/workspace.d.ts +29 -0
- package/dist/workspace.js +114 -0
- package/docs/CONSUMING-YARRAMATE.md +145 -0
- package/package.json +110 -0
- package/schema/yarramate-adapter-mapping.schema.json +59 -0
- package/schema/yarramate-check-result.schema.json +92 -0
- package/schema/yarramate-core-contract.schema.json +132 -0
- package/schema/yarramate-diagnostic-result.schema.json +64 -0
- package/schema/yarramate-document.schema.json +168 -0
- package/schema/yarramate-evidence-report.schema.json +73 -0
- package/schema/yarramate-evidence.schema.json +92 -0
- package/schema/yarramate-graph-v2.schema.json +170 -0
- package/schema/yarramate-likec4-check-result.schema.json +50 -0
- package/schema/yarramate-likec4-diagnostic-result.schema.json +69 -0
- package/schema/yarramate-likec4-generated-project-v2.schema.json +100 -0
- package/schema/yarramate-likec4-generated-project.schema.json +76 -0
- package/schema/yarramate-likec4-kind-mapping.schema.json +65 -0
- package/schema/yarramate-likec4-project.schema.json +160 -0
- package/schema/yarramate-profile.schema.json +113 -0
- package/schema/yarramate-projection-result.schema.json +177 -0
- package/schema/yarramate-projection.schema.json +126 -0
- package/schema/yarramate-reconciliation-report.schema.json +90 -0
- package/schema/yarramate-state-comparison.schema.json +61 -0
- package/schema/yarramate-workspace.schema.json +55 -0
- package/skills/yarramate-architecture/SKILL.md +135 -0
- package/skills/yarramate-architecture/agents/openai.yaml +4 -0
- package/skills/yarramate-architecture/references/journey-checklists.md +57 -0
- package/skills/yarramate-architecture/references/native-authoring.md +167 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 YarraMate contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# YarraMate
|
|
2
|
+
|
|
3
|
+
YarraMate is a tool-neutral semantic architecture engine and guided
|
|
4
|
+
methodology. It turns architectural intent into deterministic, testable
|
|
5
|
+
context shared by people and agents.
|
|
6
|
+
|
|
7
|
+
> YarraMate is pre-release software. Interfaces may evolve before the first
|
|
8
|
+
> stable release.
|
|
9
|
+
|
|
10
|
+
## Why YarraMate?
|
|
11
|
+
|
|
12
|
+
Architecture documents often drift away from implementation or become tied to
|
|
13
|
+
one notation and tool. YarraMate keeps concise, native YAML documents
|
|
14
|
+
canonical and compiles them into an explicit semantic graph.
|
|
15
|
+
|
|
16
|
+
The same model supports two workflows:
|
|
17
|
+
|
|
18
|
+
- discover an existing project's architecture from repository evidence;
|
|
19
|
+
- design a solution before implementation and later reconcile intent with
|
|
20
|
+
evidence.
|
|
21
|
+
|
|
22
|
+
Git provides authorship, review, history, and acceptance. YarraMate does not
|
|
23
|
+
introduce a parallel governance workflow.
|
|
24
|
+
|
|
25
|
+
## Product boundaries
|
|
26
|
+
|
|
27
|
+
YarraMate Core:
|
|
28
|
+
|
|
29
|
+
- owns native, versioned architecture documents;
|
|
30
|
+
- compiles a claim-centred, tool-neutral semantic graph;
|
|
31
|
+
- checks deterministic correctness rather than architectural taste;
|
|
32
|
+
- supports explicit workspaces, profiles, projections, evidence, and
|
|
33
|
+
architecture states;
|
|
34
|
+
- exposes a stable CLI for people, CI, skills, and agent harnesses.
|
|
35
|
+
|
|
36
|
+
Optional adapters provide:
|
|
37
|
+
|
|
38
|
+
- LikeC4 visualization from semantic projections;
|
|
39
|
+
- Graphify observations as evidence overlays;
|
|
40
|
+
- separately governed compatibility profiles for external languages.
|
|
41
|
+
|
|
42
|
+
Core does not depend on LikeC4, Graphify, or ArchiMate.
|
|
43
|
+
|
|
44
|
+
YarraMate is not affiliated with or certified by The Open Group. ArchiMate® is
|
|
45
|
+
a registered trademark of The Open Group. LikeC4 and Graphify are independent
|
|
46
|
+
projects; their mention does not imply affiliation or endorsement.
|
|
47
|
+
|
|
48
|
+
## Repository layout
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
src/ compiler, CLI, graph, and adapter sources
|
|
52
|
+
schema/ normative JSON Schemas
|
|
53
|
+
test/ tests and acceptance fixtures
|
|
54
|
+
skills/ portable architecture workflow for agent harnesses
|
|
55
|
+
docs/ contracts, guides, and decisions
|
|
56
|
+
.yarramate/ canonical dogfooded architecture
|
|
57
|
+
.yarramate-out/ reproducible generated output (ignored)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The [documentation index](docs/README.md) links the public guides and
|
|
61
|
+
maintainer material. Start semantic work with the
|
|
62
|
+
[product contract](docs/PRODUCT-CONTRACT.md) and
|
|
63
|
+
[glossary](docs/GLOSSARY.md).
|
|
64
|
+
|
|
65
|
+
## Development
|
|
66
|
+
|
|
67
|
+
Requirements:
|
|
68
|
+
|
|
69
|
+
- Node.js 22 or newer
|
|
70
|
+
- Corepack
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
corepack enable
|
|
74
|
+
pnpm install --frozen-lockfile
|
|
75
|
+
pnpm run verify
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The full CI command runs typechecking, tests, native self-validation, LikeC4
|
|
79
|
+
generation, and LikeC4 validation.
|
|
80
|
+
|
|
81
|
+
Useful focused commands:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
pnpm build
|
|
85
|
+
pnpm test
|
|
86
|
+
pnpm typecheck
|
|
87
|
+
pnpm self:check
|
|
88
|
+
pnpm self:evidence
|
|
89
|
+
pnpm self:reconcile
|
|
90
|
+
pnpm self:check:likec4
|
|
91
|
+
pnpm self:export:likec4
|
|
92
|
+
pnpm docs:dev
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## CLI
|
|
96
|
+
|
|
97
|
+
Build the repository, then invoke the same executable surface intended for
|
|
98
|
+
published use:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
pnpm build
|
|
102
|
+
|
|
103
|
+
node dist/cli.js init .
|
|
104
|
+
node dist/cli.js check .yarramate/workspace.yaml --json
|
|
105
|
+
node dist/cli.js compile .yarramate/workspace.yaml
|
|
106
|
+
node dist/cli.js context .yarramate/projections/context.yaml .yarramate/workspace.yaml
|
|
107
|
+
node dist/cli.js view .yarramate/projections/context.yaml .yarramate/workspace.yaml
|
|
108
|
+
node dist/cli.js evidence .yarramate/evidence/repository.yaml .yarramate/workspace.yaml
|
|
109
|
+
node dist/cli.js reconcile .yarramate/workspace.yaml
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`init` creates `.yarramate/architecture/main.yaml` and
|
|
113
|
+
`.yarramate/workspace.yaml`. Commands accept explicit source documents or one
|
|
114
|
+
explicit workspace manifest.
|
|
115
|
+
|
|
116
|
+
For a local consumer test, create a package artifact:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
pnpm pack --pack-destination /tmp/yarramate-package
|
|
120
|
+
npm install --global /tmp/yarramate-package/yarramate-0.1.0.tgz
|
|
121
|
+
yarramate --help
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
See [Consuming YarraMate](docs/CONSUMING-YARRAMATE.md) for the packaged CLI,
|
|
125
|
+
schemas, agent skill, and optional adapters.
|
|
126
|
+
|
|
127
|
+
## Library API
|
|
128
|
+
|
|
129
|
+
The typed library exposes the same deep compiler seam:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { compileWorkspace } from 'yarramate'
|
|
133
|
+
|
|
134
|
+
const result = compileWorkspace([
|
|
135
|
+
{ path: 'architecture.yaml', source: yamlSource },
|
|
136
|
+
])
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`compileWorkspaceWithProfileContext` additionally returns resolved profile
|
|
140
|
+
lineage for operations that explicitly require kind ancestry. Graph v2 remains
|
|
141
|
+
the stable, graph-only interchange result.
|
|
142
|
+
|
|
143
|
+
Normative schemas are available through package exports such as:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
yarramate/schema/document
|
|
147
|
+
yarramate/schema/profile
|
|
148
|
+
yarramate/schema/workspace
|
|
149
|
+
yarramate/schema/graph-v2
|
|
150
|
+
yarramate/schema/projection
|
|
151
|
+
yarramate/schema/evidence
|
|
152
|
+
yarramate/schema/core-contract
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Optional adapter entry points are exported from:
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
yarramate/adapter/likec4
|
|
159
|
+
yarramate/adapter/graphify
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Contributing and security
|
|
163
|
+
|
|
164
|
+
Found a bug, confusing behaviour, missing capability, documentation problem,
|
|
165
|
+
or possible improvement? Please
|
|
166
|
+
[open a GitHub Issue](https://github.com/yarrasys/yarramate/issues/new).
|
|
167
|
+
Issues are welcome from users, contributors, agents, and curious observers.
|
|
168
|
+
You do not need to provide a solution, formal proposal, or implementation.
|
|
169
|
+
|
|
170
|
+
Read [CONTRIBUTING.md](CONTRIBUTING.md) before proposing changes to native
|
|
171
|
+
semantics or stable interfaces. Report suspected vulnerabilities according to
|
|
172
|
+
[SECURITY.md](SECURITY.md).
|
|
173
|
+
|
|
174
|
+
YarraMate is available under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Diagnostic, SemanticGraph, WorkspaceSource } from './compiler.js';
|
|
2
|
+
import { type SourceLocation } from './source-document.js';
|
|
3
|
+
export interface AdapterSubjectMapping {
|
|
4
|
+
readonly native: string;
|
|
5
|
+
readonly external: string;
|
|
6
|
+
readonly type: 'concept' | 'relationship';
|
|
7
|
+
}
|
|
8
|
+
export interface AdapterMapping {
|
|
9
|
+
readonly format: 'yarramate/adapter-mapping/v1';
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly version: string;
|
|
12
|
+
readonly adapter: string;
|
|
13
|
+
readonly mappings: readonly AdapterSubjectMapping[];
|
|
14
|
+
}
|
|
15
|
+
export type AdapterMappingLoadResult = {
|
|
16
|
+
readonly ok: true;
|
|
17
|
+
readonly mapping: AdapterMapping;
|
|
18
|
+
} | {
|
|
19
|
+
readonly ok: false;
|
|
20
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
21
|
+
};
|
|
22
|
+
export type AdapterMappingValidationResult = {
|
|
23
|
+
readonly ok: true;
|
|
24
|
+
readonly mapping: AdapterMapping;
|
|
25
|
+
} | {
|
|
26
|
+
readonly ok: false;
|
|
27
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
28
|
+
};
|
|
29
|
+
export type AdapterMappingsValidationResult = {
|
|
30
|
+
readonly ok: true;
|
|
31
|
+
readonly mappings: readonly AdapterMapping[];
|
|
32
|
+
} | {
|
|
33
|
+
readonly ok: false;
|
|
34
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
35
|
+
};
|
|
36
|
+
export declare function loadAdapterMapping(source: WorkspaceSource): AdapterMappingLoadResult;
|
|
37
|
+
export declare function adapterMappingLocation(mapping: AdapterMapping, field: 'id' | 'adapter'): SourceLocation;
|
|
38
|
+
export declare function adapterMappingEntryLocation(mapping: AdapterMapping, entry: AdapterSubjectMapping, field: 'native' | 'external' | 'type'): SourceLocation;
|
|
39
|
+
export declare function validateAdapterMapping(graph: SemanticGraph, mapping: AdapterMapping): AdapterMappingValidationResult;
|
|
40
|
+
export declare function validateAdapterMappings(graph: SemanticGraph, mappings: readonly AdapterMapping[]): AdapterMappingsValidationResult;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import Ajv2020Module from 'ajv/dist/2020.js';
|
|
2
|
+
import { diagnosticOrder, loadSourceDocument, locateSourcePath, } from './source-document.js';
|
|
3
|
+
import adapterMappingSchema from '../schema/yarramate-adapter-mapping.schema.json' with {
|
|
4
|
+
type: 'json'
|
|
5
|
+
};
|
|
6
|
+
const Ajv2020 = Ajv2020Module.default;
|
|
7
|
+
const validateSchema = new Ajv2020({ allErrors: true }).compile(adapterMappingSchema);
|
|
8
|
+
const mappingLocations = new WeakMap();
|
|
9
|
+
export function loadAdapterMapping(source) {
|
|
10
|
+
const loaded = loadSourceDocument(source, validateSchema, 'Adapter mapping');
|
|
11
|
+
if (!loaded.ok)
|
|
12
|
+
return loaded;
|
|
13
|
+
const { value, yaml, lineCounter } = loaded.document;
|
|
14
|
+
const mapping = {
|
|
15
|
+
...value,
|
|
16
|
+
mappings: [...value.mappings].sort((left, right) => left.native.localeCompare(right.native) ||
|
|
17
|
+
left.external.localeCompare(right.external) ||
|
|
18
|
+
left.type.localeCompare(right.type)),
|
|
19
|
+
};
|
|
20
|
+
const locateNode = (yamlPath, pointer) => locateSourcePath(source.path, yaml, lineCounter, yamlPath, pointer);
|
|
21
|
+
mappingLocations.set(mapping, {
|
|
22
|
+
id: locateNode(['id'], '/id'),
|
|
23
|
+
adapter: locateNode(['adapter'], '/adapter'),
|
|
24
|
+
mappings: mapping.mappings.map((entry) => {
|
|
25
|
+
const authoredIndex = value.mappings.indexOf(entry);
|
|
26
|
+
const locate = (field) => locateNode(['mappings', authoredIndex, field], `/mappings/${authoredIndex}/${field}`);
|
|
27
|
+
return {
|
|
28
|
+
native: locate('native'),
|
|
29
|
+
external: locate('external'),
|
|
30
|
+
type: locate('type'),
|
|
31
|
+
};
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
return { ok: true, mapping };
|
|
35
|
+
}
|
|
36
|
+
export function adapterMappingLocation(mapping, field) {
|
|
37
|
+
return (mappingLocations.get(mapping)?.[field] ?? {
|
|
38
|
+
path: `${mapping.id}.mapping.yaml`,
|
|
39
|
+
pointer: `/${field}`,
|
|
40
|
+
line: 1,
|
|
41
|
+
column: 1,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export function adapterMappingEntryLocation(mapping, entry, field) {
|
|
45
|
+
const index = mapping.mappings.indexOf(entry);
|
|
46
|
+
return (mappingLocations.get(mapping)?.mappings[index]?.[field] ?? {
|
|
47
|
+
path: `${mapping.id}.mapping.yaml`,
|
|
48
|
+
pointer: `/mappings/${Math.max(index, 0)}/${field}`,
|
|
49
|
+
line: 1,
|
|
50
|
+
column: 1,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function validateAdapterMapping(graph, mapping) {
|
|
54
|
+
const subjectTypes = new Map(graph.subjects.map(({ id, type }) => [id, type]));
|
|
55
|
+
const locations = mappingLocations.get(mapping)?.mappings ?? [];
|
|
56
|
+
const diagnostics = [];
|
|
57
|
+
const seenNativeSubjects = new Set();
|
|
58
|
+
const seenExternalIdentities = new Set();
|
|
59
|
+
for (const [index, entry] of mapping.mappings.entries()) {
|
|
60
|
+
const fallback = (field) => ({
|
|
61
|
+
path: `${mapping.id}.mapping.yaml`,
|
|
62
|
+
pointer: `/mappings/${index}/${field}`,
|
|
63
|
+
line: 1,
|
|
64
|
+
column: 1,
|
|
65
|
+
});
|
|
66
|
+
const nativeLocation = locations[index]?.native ?? fallback('native');
|
|
67
|
+
const externalLocation = locations[index]?.external ?? fallback('external');
|
|
68
|
+
const typeLocation = locations[index]?.type ?? fallback('type');
|
|
69
|
+
const actualType = subjectTypes.get(entry.native);
|
|
70
|
+
if (actualType === undefined) {
|
|
71
|
+
diagnostics.push({
|
|
72
|
+
severity: 'error',
|
|
73
|
+
code: 'YM601',
|
|
74
|
+
message: `Native subject "${entry.native}" does not exist`,
|
|
75
|
+
path: nativeLocation.path,
|
|
76
|
+
pointer: nativeLocation.pointer,
|
|
77
|
+
line: nativeLocation.line,
|
|
78
|
+
column: nativeLocation.column,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else if (actualType !== entry.type) {
|
|
82
|
+
diagnostics.push({
|
|
83
|
+
severity: 'error',
|
|
84
|
+
code: 'YM602',
|
|
85
|
+
message: `Native subject "${entry.native}" is a ${actualType}, not a ${entry.type}`,
|
|
86
|
+
path: typeLocation.path,
|
|
87
|
+
pointer: typeLocation.pointer,
|
|
88
|
+
line: typeLocation.line,
|
|
89
|
+
column: typeLocation.column,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (seenNativeSubjects.has(entry.native)) {
|
|
93
|
+
diagnostics.push({
|
|
94
|
+
severity: 'error',
|
|
95
|
+
code: 'YM603',
|
|
96
|
+
message: `Native subject "${entry.native}" is mapped more than once`,
|
|
97
|
+
path: nativeLocation.path,
|
|
98
|
+
pointer: nativeLocation.pointer,
|
|
99
|
+
line: nativeLocation.line,
|
|
100
|
+
column: nativeLocation.column,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
seenNativeSubjects.add(entry.native);
|
|
104
|
+
if (seenExternalIdentities.has(entry.external)) {
|
|
105
|
+
diagnostics.push({
|
|
106
|
+
severity: 'error',
|
|
107
|
+
code: 'YM604',
|
|
108
|
+
message: `External identity "${entry.external}" is mapped more than once`,
|
|
109
|
+
path: externalLocation.path,
|
|
110
|
+
pointer: externalLocation.pointer,
|
|
111
|
+
line: externalLocation.line,
|
|
112
|
+
column: externalLocation.column,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
seenExternalIdentities.add(entry.external);
|
|
116
|
+
}
|
|
117
|
+
return diagnostics.length === 0
|
|
118
|
+
? { ok: true, mapping }
|
|
119
|
+
: { ok: false, diagnostics: diagnostics.sort(diagnosticOrder) };
|
|
120
|
+
}
|
|
121
|
+
export function validateAdapterMappings(graph, mappings) {
|
|
122
|
+
const ordered = [...mappings].sort((left, right) => {
|
|
123
|
+
const identityOrder = `${left.id}@${left.version}`.localeCompare(`${right.id}@${right.version}`);
|
|
124
|
+
if (identityOrder !== 0)
|
|
125
|
+
return identityOrder;
|
|
126
|
+
const leftPath = mappingLocations.get(left)?.id.path ?? '';
|
|
127
|
+
const rightPath = mappingLocations.get(right)?.id.path ?? '';
|
|
128
|
+
return leftPath.localeCompare(rightPath);
|
|
129
|
+
});
|
|
130
|
+
const diagnostics = [];
|
|
131
|
+
const seenIdentities = new Set();
|
|
132
|
+
const nativeOwnersByAdapter = new Map();
|
|
133
|
+
const externalOwnersByAdapter = new Map();
|
|
134
|
+
for (const mapping of ordered) {
|
|
135
|
+
const identity = `${mapping.id}@${mapping.version}`;
|
|
136
|
+
const validation = validateAdapterMapping(graph, mapping);
|
|
137
|
+
if (!validation.ok) {
|
|
138
|
+
diagnostics.push(...validation.diagnostics);
|
|
139
|
+
}
|
|
140
|
+
if (seenIdentities.has(identity)) {
|
|
141
|
+
const location = mappingLocations.get(mapping)?.id ?? {
|
|
142
|
+
path: `${mapping.id}.mapping.yaml`,
|
|
143
|
+
pointer: '/id',
|
|
144
|
+
line: 1,
|
|
145
|
+
column: 1,
|
|
146
|
+
};
|
|
147
|
+
diagnostics.push({
|
|
148
|
+
severity: 'error',
|
|
149
|
+
code: 'YM605',
|
|
150
|
+
message: `Adapter mapping "${identity}" is declared more than once`,
|
|
151
|
+
...location,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
seenIdentities.add(identity);
|
|
155
|
+
const nativeOwners = nativeOwnersByAdapter.get(mapping.adapter) ??
|
|
156
|
+
new Map();
|
|
157
|
+
const externalOwners = externalOwnersByAdapter.get(mapping.adapter) ??
|
|
158
|
+
new Map();
|
|
159
|
+
nativeOwnersByAdapter.set(mapping.adapter, nativeOwners);
|
|
160
|
+
externalOwnersByAdapter.set(mapping.adapter, externalOwners);
|
|
161
|
+
const locations = mappingLocations.get(mapping)?.mappings ?? [];
|
|
162
|
+
for (const [index, entry] of mapping.mappings.entries()) {
|
|
163
|
+
const nativeOwner = nativeOwners.get(entry.native);
|
|
164
|
+
if (nativeOwner !== undefined && nativeOwner !== mapping) {
|
|
165
|
+
const location = locations[index]?.native ?? {
|
|
166
|
+
path: `${mapping.id}.mapping.yaml`,
|
|
167
|
+
pointer: `/mappings/${index}/native`,
|
|
168
|
+
line: 1,
|
|
169
|
+
column: 1,
|
|
170
|
+
};
|
|
171
|
+
diagnostics.push({
|
|
172
|
+
severity: 'error',
|
|
173
|
+
code: 'YM603',
|
|
174
|
+
message: `Native subject "${entry.native}" is mapped more than once for adapter "${mapping.adapter}"`,
|
|
175
|
+
...location,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
nativeOwners.set(entry.native, nativeOwner ?? mapping);
|
|
179
|
+
const externalOwner = externalOwners.get(entry.external);
|
|
180
|
+
if (externalOwner !== undefined && externalOwner !== mapping) {
|
|
181
|
+
const location = locations[index]?.external ?? {
|
|
182
|
+
path: `${mapping.id}.mapping.yaml`,
|
|
183
|
+
pointer: `/mappings/${index}/external`,
|
|
184
|
+
line: 1,
|
|
185
|
+
column: 1,
|
|
186
|
+
};
|
|
187
|
+
diagnostics.push({
|
|
188
|
+
severity: 'error',
|
|
189
|
+
code: 'YM604',
|
|
190
|
+
message: `External identity "${entry.external}" is mapped more than once for adapter "${mapping.adapter}"`,
|
|
191
|
+
...location,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
externalOwners.set(entry.external, externalOwner ?? mapping);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return diagnostics.length === 0
|
|
198
|
+
? { ok: true, mappings: ordered }
|
|
199
|
+
: { ok: false, diagnostics: diagnostics.sort(diagnosticOrder) };
|
|
200
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { adapterMappingEntryLocation, adapterMappingLocation, loadAdapterMapping, validateAdapterMapping, } from '../adapter-mapping.js';
|
|
5
|
+
import { compileWorkspace } from '../compiler.js';
|
|
6
|
+
import { diagnosticJson, isMainModule, resolveCliWorkspaceSources, } from '../cli-support.js';
|
|
7
|
+
import { observeGraphify, } from './graphify.js';
|
|
8
|
+
const usage = 'Usage:\n' +
|
|
9
|
+
' yarramate-graphify observe <graph.json> <mapping.yaml> <workspace-or-source...> --id <evidence-id> --version <major.minor>\n';
|
|
10
|
+
const parseOptions = (options) => {
|
|
11
|
+
const idFlag = options.indexOf('--id');
|
|
12
|
+
const versionFlag = options.indexOf('--version');
|
|
13
|
+
if (idFlag < 3 ||
|
|
14
|
+
versionFlag < 3 ||
|
|
15
|
+
options[idFlag + 1] === undefined ||
|
|
16
|
+
options[versionFlag + 1] === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
const flagIndexes = [idFlag, versionFlag].sort((left, right) => left - right);
|
|
19
|
+
const firstFlag = flagIndexes[0];
|
|
20
|
+
if (firstFlag === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
const paths = options.slice(0, firstFlag);
|
|
23
|
+
const trailing = options.slice(firstFlag);
|
|
24
|
+
if (paths.length < 3 ||
|
|
25
|
+
trailing.length !== 4 ||
|
|
26
|
+
new Set([trailing[0], trailing[2]]).size !== 2)
|
|
27
|
+
return undefined;
|
|
28
|
+
return {
|
|
29
|
+
graphPath: paths[0],
|
|
30
|
+
mappingPath: paths[1],
|
|
31
|
+
sources: paths.slice(2),
|
|
32
|
+
id: options[idFlag + 1],
|
|
33
|
+
version: options[versionFlag + 1],
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export function runGraphifyCli(args, cwd = process.cwd()) {
|
|
37
|
+
const [command, ...options] = args;
|
|
38
|
+
const parsed = command === 'observe' ? parseOptions(options) : undefined;
|
|
39
|
+
if (parsed === undefined ||
|
|
40
|
+
!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(parsed.id) ||
|
|
41
|
+
!/^[0-9]+\.[0-9]+$/.test(parsed.version)) {
|
|
42
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const resolved = resolveCliWorkspaceSources(parsed.sources, cwd);
|
|
46
|
+
if (!resolved.ok) {
|
|
47
|
+
return {
|
|
48
|
+
exitCode: 1,
|
|
49
|
+
stdout: diagnosticJson(resolved.diagnostics),
|
|
50
|
+
stderr: '',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const compilation = compileWorkspace(resolved.paths.map((path) => ({
|
|
54
|
+
path,
|
|
55
|
+
source: readFileSync(resolve(cwd, path), 'utf8'),
|
|
56
|
+
})));
|
|
57
|
+
if (!compilation.ok) {
|
|
58
|
+
return {
|
|
59
|
+
exitCode: 1,
|
|
60
|
+
stdout: diagnosticJson(compilation.diagnostics),
|
|
61
|
+
stderr: '',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const loadedMapping = loadAdapterMapping({
|
|
65
|
+
path: parsed.mappingPath,
|
|
66
|
+
source: readFileSync(resolve(cwd, parsed.mappingPath), 'utf8'),
|
|
67
|
+
});
|
|
68
|
+
if (!loadedMapping.ok) {
|
|
69
|
+
return {
|
|
70
|
+
exitCode: 1,
|
|
71
|
+
stdout: diagnosticJson(loadedMapping.diagnostics),
|
|
72
|
+
stderr: '',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const validatedMapping = validateAdapterMapping(compilation.graph, loadedMapping.mapping);
|
|
76
|
+
if (!validatedMapping.ok) {
|
|
77
|
+
return {
|
|
78
|
+
exitCode: 1,
|
|
79
|
+
stdout: diagnosticJson(validatedMapping.diagnostics),
|
|
80
|
+
stderr: '',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const graph = JSON.parse(readFileSync(resolve(cwd, parsed.graphPath), 'utf8'));
|
|
84
|
+
if (!Array.isArray(graph.nodes) ||
|
|
85
|
+
graph.nodes.some((node) => typeof node !== 'object' ||
|
|
86
|
+
node === null ||
|
|
87
|
+
typeof node.id !== 'string' ||
|
|
88
|
+
node.id.length === 0)) {
|
|
89
|
+
return {
|
|
90
|
+
exitCode: 2,
|
|
91
|
+
stdout: '',
|
|
92
|
+
stderr: `${parsed.graphPath} does not contain Graphify nodes with string IDs\n`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const observation = observeGraphify(graph, validatedMapping.mapping, { id: parsed.id, version: parsed.version });
|
|
96
|
+
if (!observation.ok) {
|
|
97
|
+
const diagnostics = observation.issues.map((issue) => ({
|
|
98
|
+
severity: 'error',
|
|
99
|
+
code: issue.code,
|
|
100
|
+
message: issue.message,
|
|
101
|
+
...(issue.mapping === undefined
|
|
102
|
+
? adapterMappingLocation(loadedMapping.mapping, 'adapter')
|
|
103
|
+
: adapterMappingEntryLocation(loadedMapping.mapping, issue.mapping, 'type')),
|
|
104
|
+
}));
|
|
105
|
+
return {
|
|
106
|
+
exitCode: 1,
|
|
107
|
+
stdout: diagnosticJson(diagnostics),
|
|
108
|
+
stderr: '',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
exitCode: 0,
|
|
113
|
+
stdout: `${JSON.stringify(observation.evidence, null, 2)}\n`,
|
|
114
|
+
stderr: '',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
119
|
+
return { exitCode: 2, stdout: '', stderr: `${message}\n` };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (isMainModule(import.meta.url, process.argv[1])) {
|
|
123
|
+
const result = runGraphifyCli(process.argv.slice(2));
|
|
124
|
+
process.stdout.write(result.stdout);
|
|
125
|
+
process.stderr.write(result.stderr);
|
|
126
|
+
process.exitCode = result.exitCode;
|
|
127
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { observeGraphify, type GraphifyGraph, type GraphifyObservationIssue, type GraphifyObservationResult, } from './graphify.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { observeGraphify, } from './graphify.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AdapterMapping, AdapterSubjectMapping } from '../adapter-mapping.js';
|
|
2
|
+
import type { EvidenceDocument } from '../evidence.js';
|
|
3
|
+
export interface GraphifyGraph {
|
|
4
|
+
readonly nodes: readonly {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
}[];
|
|
7
|
+
}
|
|
8
|
+
export interface GraphifyObservationIssue {
|
|
9
|
+
readonly code: 'YMG101' | 'YMG102';
|
|
10
|
+
readonly message: string;
|
|
11
|
+
readonly mapping?: AdapterSubjectMapping;
|
|
12
|
+
}
|
|
13
|
+
export type GraphifyObservationResult = {
|
|
14
|
+
readonly ok: true;
|
|
15
|
+
readonly evidence: EvidenceDocument;
|
|
16
|
+
} | {
|
|
17
|
+
readonly ok: false;
|
|
18
|
+
readonly issues: readonly GraphifyObservationIssue[];
|
|
19
|
+
};
|
|
20
|
+
export declare function observeGraphify(graph: GraphifyGraph, mapping: AdapterMapping, identity: {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly version: string;
|
|
23
|
+
}): GraphifyObservationResult;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function observeGraphify(graph, mapping, identity) {
|
|
2
|
+
if (mapping.adapter !== 'graphify') {
|
|
3
|
+
return {
|
|
4
|
+
ok: false,
|
|
5
|
+
issues: [{
|
|
6
|
+
code: 'YMG101',
|
|
7
|
+
message: `Adapter mapping "${mapping.id}@${mapping.version}" belongs to "${mapping.adapter}", not "graphify"`,
|
|
8
|
+
}],
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const relationshipMappings = mapping.mappings.filter(({ type }) => type === 'relationship');
|
|
12
|
+
if (relationshipMappings.length > 0) {
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
issues: relationshipMappings.map((entry) => ({
|
|
16
|
+
code: 'YMG102',
|
|
17
|
+
message: `Graphify node evidence cannot evaluate relationship subject "${entry.native}"`,
|
|
18
|
+
mapping: entry,
|
|
19
|
+
})),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const nodeIds = new Set(graph.nodes.map(({ id }) => id));
|
|
23
|
+
const observations = mapping.mappings.map((entry) => nodeIds.has(entry.external)
|
|
24
|
+
? {
|
|
25
|
+
subject: entry.native,
|
|
26
|
+
result: 'confirmed',
|
|
27
|
+
evidence: { uri: `graphify:${entry.external}` },
|
|
28
|
+
}
|
|
29
|
+
: {
|
|
30
|
+
subject: entry.native,
|
|
31
|
+
result: 'not-observed',
|
|
32
|
+
evidence: {
|
|
33
|
+
uri: `graphify:${entry.external}`,
|
|
34
|
+
message: `Graphify node "${entry.external}" was not observed`,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
evidence: {
|
|
40
|
+
format: 'yarramate/evidence/v1',
|
|
41
|
+
id: identity.id,
|
|
42
|
+
version: identity.version,
|
|
43
|
+
provider: 'graphify',
|
|
44
|
+
observations,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|