yarramate 0.19.0 → 0.20.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/README.md +7 -0
- package/dist/adapters/likec4-export.d.ts +1 -1
- package/dist/adapters/likec4-export.js +32 -0
- package/dist/adapters/visual/session-store.d.ts +3 -1
- package/dist/adapters/visual/session-store.js +66 -5
- package/dist/compiler.d.ts +53 -0
- package/dist/compiler.js +173 -87
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/package.json +4 -4
- package/schema/yarramate-likec4-diagnostic-result.schema.json +1 -1
package/README.md
CHANGED
|
@@ -252,6 +252,13 @@ const result = compileWorkspace([
|
|
|
252
252
|
lineage for operations that explicitly require kind ancestry. Graph v2
|
|
253
253
|
remains the stable, graph-only interchange result.
|
|
254
254
|
|
|
255
|
+
`compileWorkspaceIncremental(sources, previous)` is the entry point for a
|
|
256
|
+
consumer that recompiles a whole workspace on every write. It returns the
|
|
257
|
+
same result plus an opaque `cache` to hand back on the next call, and
|
|
258
|
+
re-parses only the sources whose text changed; `incremental: false` reports
|
|
259
|
+
that it fell back to a full compile. Reuse is decided by source-text
|
|
260
|
+
equality, so a stale cache costs work but never changes output.
|
|
261
|
+
|
|
255
262
|
Normative schemas are available through package exports such as
|
|
256
263
|
`yarramate/schema/document`, `yarramate/schema/workspace`,
|
|
257
264
|
`yarramate/schema/graph-v2`, `yarramate/schema/projection`,
|
|
@@ -4,7 +4,7 @@ import type { ProjectionResult } from '../projection.js';
|
|
|
4
4
|
import type { LikeC4KindMapping } from './likec4-kind-mapping.js';
|
|
5
5
|
export interface LikeC4ExportDiagnostic {
|
|
6
6
|
readonly severity: 'error';
|
|
7
|
-
readonly code: 'YMLC101' | 'YMLC102' | 'YMLC103' | 'YMLC104' | 'YMLC105' | 'YMLC106' | 'YMLC107' | 'YMLC108' | 'YMLC109' | 'YMLC111';
|
|
7
|
+
readonly code: 'YMLC101' | 'YMLC102' | 'YMLC103' | 'YMLC104' | 'YMLC105' | 'YMLC106' | 'YMLC107' | 'YMLC108' | 'YMLC109' | 'YMLC111' | 'YMLC112';
|
|
8
8
|
readonly message: string;
|
|
9
9
|
readonly subject?: string;
|
|
10
10
|
readonly path: string;
|
|
@@ -155,6 +155,38 @@ export function exportLikeC4(projection, mapping, kindMapping, options = {}) {
|
|
|
155
155
|
: [` description ${quote(description)}`]), ...metadata, ' }');
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
+
// `likec4 validate` accepts an empty model as valid, so it can never notice
|
|
159
|
+
// that selected concepts stopped reaching the emitted text. The shortfall is
|
|
160
|
+
// only visible here, and it is measured on the assembled lines rather than on
|
|
161
|
+
// loop iterations so that a dropped definition is what fails, not a skipped
|
|
162
|
+
// turn of the loop.
|
|
163
|
+
const rendered = new Set(lines.flatMap((line) => {
|
|
164
|
+
if (!line.startsWith(' ') || line.startsWith(' '))
|
|
165
|
+
return [];
|
|
166
|
+
const [head, ...rest] = line.slice(2).split(' = ');
|
|
167
|
+
return rest.length > 0 ? [head] : [];
|
|
168
|
+
}));
|
|
169
|
+
const unrendered = concepts.filter(({ id }) => !rendered.has(externalByNative.get(id)));
|
|
170
|
+
const missing = unrendered[0];
|
|
171
|
+
if (missing !== undefined) {
|
|
172
|
+
const source = sourceForConcept(projection.claims, missing.id) ??
|
|
173
|
+
adapterMappingLocation(mapping, 'adapter');
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
diagnostics: [
|
|
177
|
+
{
|
|
178
|
+
severity: 'error',
|
|
179
|
+
code: 'YMLC112',
|
|
180
|
+
message: `Rendering coverage: ${concepts.length - unrendered.length} of ${concepts.length} projected concepts reached the LikeC4 model`,
|
|
181
|
+
subject: missing.id,
|
|
182
|
+
path: source.path,
|
|
183
|
+
pointer: source.pointer,
|
|
184
|
+
line: source.line,
|
|
185
|
+
column: source.column,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
158
190
|
const relationships = projection.subjects
|
|
159
191
|
.filter(({ type }) => type === 'relationship')
|
|
160
192
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
@@ -110,7 +110,9 @@ export declare const buildVisualModelGraph: (sources: readonly WorkspaceSource[]
|
|
|
110
110
|
/**
|
|
111
111
|
* Recovers the handoff and only then deletes the session, so cleanup can never
|
|
112
112
|
* be the step that loses confirmed state. Returns `undefined` when the session
|
|
113
|
-
* is already gone, which makes a repeated stop idempotent
|
|
113
|
+
* is already gone, which makes a repeated stop idempotent, and when a previous
|
|
114
|
+
* cleanup already took the journal: there is no handoff left to recover, and
|
|
115
|
+
* refusing would strand a marked directory that only this call will remove.
|
|
114
116
|
*/
|
|
115
117
|
export declare const removeVisualSession: (paths: VisualSessionPaths, includeTranscript?: boolean) => Promise<VisualHandoff | undefined>;
|
|
116
118
|
/**
|
|
@@ -582,10 +582,66 @@ export const buildVisualModelGraph = (sources) => {
|
|
|
582
582
|
graph: projectGraphForCanvas(compiled.graph, compiled.profileContext),
|
|
583
583
|
};
|
|
584
584
|
};
|
|
585
|
+
/**
|
|
586
|
+
* Whether a journal is still on disk. A cleanup that failed partway can leave
|
|
587
|
+
* a marked session without one, and that is a question about the filesystem
|
|
588
|
+
* rather than a corruption to report.
|
|
589
|
+
*/
|
|
590
|
+
const journalExists = async (paths) => {
|
|
591
|
+
try {
|
|
592
|
+
await lstat(paths.journal);
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
/**
|
|
600
|
+
* Deletes a session directory one entry at a time, journal and marker last.
|
|
601
|
+
*
|
|
602
|
+
* `rm(..., { recursive: true })` rejects the moment one entry fails while the
|
|
603
|
+
* sibling removals it already started are still in flight; those stragglers go
|
|
604
|
+
* on deleting after the caller has already seen the rejection. A cleanup that
|
|
605
|
+
* failed could therefore still take the journal with it, leaving the retry that
|
|
606
|
+
* follows nothing to recover from. Removing entries in a decided order, and
|
|
607
|
+
* awaiting each one, keeps the two files a retry depends on - the journal it
|
|
608
|
+
* recovers from, and the marker that authorises deleting this directory at all
|
|
609
|
+
* - on disk for as long as any other trace of the session is. The marker goes
|
|
610
|
+
* last of the two: a directory that outlives its marker is one no later pass
|
|
611
|
+
* would agree to remove.
|
|
612
|
+
*/
|
|
613
|
+
const removeSessionDirectory = async (paths) => {
|
|
614
|
+
let entries;
|
|
615
|
+
try {
|
|
616
|
+
entries = await readdir(paths.root, { withFileTypes: true });
|
|
617
|
+
}
|
|
618
|
+
catch (cause) {
|
|
619
|
+
// A session already gone is a completed cleanup, not a failed one.
|
|
620
|
+
if (typeof cause === 'object' &&
|
|
621
|
+
cause !== null &&
|
|
622
|
+
'code' in cause &&
|
|
623
|
+
cause.code === 'ENOENT') {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
throw cause;
|
|
627
|
+
}
|
|
628
|
+
const journal = basename(paths.journal);
|
|
629
|
+
const marker = basename(paths.marker);
|
|
630
|
+
for (const entry of entries) {
|
|
631
|
+
if (entry.name === journal || entry.name === marker)
|
|
632
|
+
continue;
|
|
633
|
+
await rm(join(paths.root, entry.name), { recursive: true, force: true });
|
|
634
|
+
}
|
|
635
|
+
await rm(paths.journal, { force: true });
|
|
636
|
+
await rm(paths.marker, { force: true });
|
|
637
|
+
await rm(paths.root, { recursive: true, force: true });
|
|
638
|
+
};
|
|
585
639
|
/**
|
|
586
640
|
* Recovers the handoff and only then deletes the session, so cleanup can never
|
|
587
641
|
* be the step that loses confirmed state. Returns `undefined` when the session
|
|
588
|
-
* is already gone, which makes a repeated stop idempotent
|
|
642
|
+
* is already gone, which makes a repeated stop idempotent, and when a previous
|
|
643
|
+
* cleanup already took the journal: there is no handoff left to recover, and
|
|
644
|
+
* refusing would strand a marked directory that only this call will remove.
|
|
589
645
|
*/
|
|
590
646
|
export const removeVisualSession = async (paths, includeTranscript = false) => {
|
|
591
647
|
let entry;
|
|
@@ -599,8 +655,12 @@ export const removeVisualSession = async (paths, includeTranscript = false) => {
|
|
|
599
655
|
if (!entry.isDirectory()) {
|
|
600
656
|
throw storeError('YMVS125', `Session root "${paths.root}" is not a directory`);
|
|
601
657
|
}
|
|
602
|
-
|
|
603
|
-
await
|
|
658
|
+
// The marker authorises the deletion whether or not a journal survived it.
|
|
659
|
+
await readSessionMarker(paths);
|
|
660
|
+
const handoff = (await journalExists(paths))
|
|
661
|
+
? await recoverVisualSession(paths, includeTranscript)
|
|
662
|
+
: undefined;
|
|
663
|
+
await removeSessionDirectory(paths);
|
|
604
664
|
await syncDirectory(dirname(paths.root));
|
|
605
665
|
forget(paths);
|
|
606
666
|
return handoff;
|
|
@@ -683,8 +743,9 @@ export const pruneStaleVisualSessions = async (baseDir, now, limit = VISUAL_SESS
|
|
|
683
743
|
stale.sort((left, right) => left.activeAt - right.activeAt || left.root.localeCompare(right.root));
|
|
684
744
|
const removed = [];
|
|
685
745
|
for (const candidate of stale.slice(0, Math.max(0, limit))) {
|
|
686
|
-
|
|
687
|
-
|
|
746
|
+
const paths = visualSessionPaths(candidate.root);
|
|
747
|
+
await removeSessionDirectory(paths);
|
|
748
|
+
forget(paths);
|
|
688
749
|
removed.push(candidate.root);
|
|
689
750
|
}
|
|
690
751
|
if (removed.length > 0) {
|
package/dist/compiler.d.ts
CHANGED
|
@@ -70,6 +70,44 @@ export type ContextualCompilationResult = {
|
|
|
70
70
|
readonly ok: false;
|
|
71
71
|
readonly diagnostics: readonly Diagnostic[];
|
|
72
72
|
};
|
|
73
|
+
/**
|
|
74
|
+
* One parsed workspace source, retained by a {@link CompilationCache}. Hold it
|
|
75
|
+
* and hand it back; never construct one. `value` is the composed YAML of
|
|
76
|
+
* `source` and nothing else, so an entry is a pure function of its text.
|
|
77
|
+
*/
|
|
78
|
+
export interface ParsedWorkspaceSource {
|
|
79
|
+
readonly source: string;
|
|
80
|
+
readonly kind: 'profile' | 'document';
|
|
81
|
+
readonly value: unknown;
|
|
82
|
+
readonly schemaDiagnostics: readonly Diagnostic[];
|
|
83
|
+
/**
|
|
84
|
+
* Line/column already resolved for this text, keyed by YAML path. An
|
|
85
|
+
* internal memo of the compiler, filled as positions are asked for; a
|
|
86
|
+
* consumer that mutates it corrupts the `source` of later claims.
|
|
87
|
+
*/
|
|
88
|
+
readonly positions: Map<string, ResolvedPosition>;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Opaque parse cache returned by {@link compileWorkspaceIncremental} and
|
|
92
|
+
* accepted by its next call. Reuse is decided by exact source-text equality,
|
|
93
|
+
* not by a caller-declared change set and not by a digest, so a stale cache
|
|
94
|
+
* cannot change the compiled output - it can only fail to save work.
|
|
95
|
+
*/
|
|
96
|
+
export interface CompilationCache {
|
|
97
|
+
readonly sources: ReadonlyMap<string, ParsedWorkspaceSource>;
|
|
98
|
+
}
|
|
99
|
+
export type IncrementalCompilationResult = ({
|
|
100
|
+
readonly ok: true;
|
|
101
|
+
readonly graph: SemanticGraph;
|
|
102
|
+
readonly profileContext: ResolvedProfileContext;
|
|
103
|
+
} | {
|
|
104
|
+
readonly ok: false;
|
|
105
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
106
|
+
}) & {
|
|
107
|
+
/** False when every source had to be parsed, e.g. the first call. */
|
|
108
|
+
readonly incremental: boolean;
|
|
109
|
+
readonly cache: CompilationCache;
|
|
110
|
+
};
|
|
73
111
|
export declare const ATTESTATION_PREDICATE_PREFIX = "yarramate/attestation/";
|
|
74
112
|
export declare const attestationClaimValue: (attestation: {
|
|
75
113
|
readonly by: string;
|
|
@@ -88,5 +126,20 @@ export interface ConstraintExpectsParts {
|
|
|
88
126
|
readonly value: string;
|
|
89
127
|
}
|
|
90
128
|
export declare const parseConstraintExpectsValue: (value: string) => ConstraintExpectsParts | undefined;
|
|
129
|
+
interface ResolvedPosition {
|
|
130
|
+
readonly line: number;
|
|
131
|
+
readonly col: number;
|
|
132
|
+
}
|
|
91
133
|
export declare function compileWorkspace(sources: readonly WorkspaceSource[]): CompilationResult;
|
|
92
134
|
export declare const compileWorkspaceWithProfileContext: (sources: readonly WorkspaceSource[]) => ContextualCompilationResult;
|
|
135
|
+
/**
|
|
136
|
+
* Compiles the whole workspace, reusing the YAML parse of every source whose
|
|
137
|
+
* text is unchanged since `previous`. The compiled output is byte-identical to
|
|
138
|
+
* {@link compileWorkspaceWithProfileContext} for the same sources: the cache
|
|
139
|
+
* holds parse results only, and every cross-document decision is re-derived.
|
|
140
|
+
*
|
|
141
|
+
* Hold the returned `cache` and pass it to the next call. It retains one
|
|
142
|
+
* composed value per current source and drops sources that left the workspace.
|
|
143
|
+
*/
|
|
144
|
+
export declare const compileWorkspaceIncremental: (sources: readonly WorkspaceSource[], previous?: CompilationCache) => IncrementalCompilationResult;
|
|
145
|
+
export {};
|
package/dist/compiler.js
CHANGED
|
@@ -99,12 +99,143 @@ const diagnosticFailure = (diagnostics) => ({
|
|
|
99
99
|
left.code.localeCompare(right.code) ||
|
|
100
100
|
left.message.localeCompare(right.message)),
|
|
101
101
|
});
|
|
102
|
-
|
|
102
|
+
const nodePosition = (yaml, lineCounter, yamlPath) => {
|
|
103
|
+
const node = yaml.getIn(yamlPath, true);
|
|
104
|
+
const offset = typeof node === 'object' &&
|
|
105
|
+
node !== null &&
|
|
106
|
+
'range' in node &&
|
|
107
|
+
Array.isArray(node.range)
|
|
108
|
+
? node.range[0]
|
|
109
|
+
: 0;
|
|
110
|
+
return lineCounter.linePos(offset);
|
|
111
|
+
};
|
|
112
|
+
// A position is read for every emitted claim's `source`, not only for faults,
|
|
113
|
+
// so a compile that re-derived them from text would parse every fresh source
|
|
114
|
+
// twice - measured at 282us/document against 193us/document for one parse.
|
|
115
|
+
// A source parsed in this call hands its composed document straight to the
|
|
116
|
+
// reader; a source served from cache carries the memo its earlier compile
|
|
117
|
+
// filled and parses only for a path never asked for before. The reader
|
|
118
|
+
// memoises either way, so the returned cache answers next time without the
|
|
119
|
+
// document - and without the memo a delta costs 95% of a full compile.
|
|
120
|
+
const positionReader = (source, positions, fresh) => {
|
|
121
|
+
let parsed = fresh;
|
|
122
|
+
return (yamlPath) => {
|
|
123
|
+
// Paths mix keys and indices; a JSON key cannot confuse `['a']` with
|
|
124
|
+
// `['a', 0]` the way a joined string could, and a wrong hit here would be
|
|
125
|
+
// a wrong line number in compiled output rather than lost work.
|
|
126
|
+
const key = JSON.stringify(yamlPath);
|
|
127
|
+
const memoised = positions.get(key);
|
|
128
|
+
if (memoised !== undefined) {
|
|
129
|
+
return memoised;
|
|
130
|
+
}
|
|
131
|
+
if (parsed === undefined) {
|
|
132
|
+
const lineCounter = new LineCounter();
|
|
133
|
+
parsed = { yaml: parseDocument(source, { lineCounter }), lineCounter };
|
|
134
|
+
}
|
|
135
|
+
const position = nodePosition(parsed.yaml, parsed.lineCounter, yamlPath);
|
|
136
|
+
positions.set(key, position);
|
|
137
|
+
return position;
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
// Every field of the entry is derived from `input.source` alone, which is what
|
|
141
|
+
// makes an entry reusable across compiles: profile membership, the composed
|
|
142
|
+
// value, and the faults the text carries on its own. Cross-document faults are
|
|
143
|
+
// never cached - they are re-derived on every compile.
|
|
144
|
+
const parseWorkspaceSource = (input) => {
|
|
145
|
+
const lineCounter = new LineCounter();
|
|
146
|
+
const yaml = parseDocument(input.source, { lineCounter });
|
|
147
|
+
const value = yaml.toJS();
|
|
148
|
+
const parseDiagnostics = yaml.errors.map((error) => {
|
|
149
|
+
const position = error.linePos?.[0] ?? { line: 1, col: 1 };
|
|
150
|
+
return {
|
|
151
|
+
severity: 'error',
|
|
152
|
+
code: 'YM101',
|
|
153
|
+
message: error.message.split(' at line ')[0] ?? error.message,
|
|
154
|
+
path: input.path,
|
|
155
|
+
pointer: '/',
|
|
156
|
+
line: position.line,
|
|
157
|
+
column: position.col,
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
// Classification reads the composed mapping through the YAML document, which
|
|
161
|
+
// types the lookup as `unknown` - the same key the old probe pass read.
|
|
162
|
+
const fresh = { yaml, lineCounter };
|
|
163
|
+
if (yaml.get('format') === 'yarramate/profile/v1') {
|
|
164
|
+
return {
|
|
165
|
+
entry: {
|
|
166
|
+
source: input.source,
|
|
167
|
+
kind: 'profile',
|
|
168
|
+
value,
|
|
169
|
+
schemaDiagnostics: parseDiagnostics,
|
|
170
|
+
positions: new Map(),
|
|
171
|
+
},
|
|
172
|
+
fresh,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const valid = parseDiagnostics.length === 0 && validateDocument(value);
|
|
176
|
+
const schemaDiagnostics = parseDiagnostics.length > 0
|
|
177
|
+
? parseDiagnostics
|
|
178
|
+
: valid
|
|
179
|
+
? []
|
|
180
|
+
: (validateDocument.errors ?? []).map((error) => {
|
|
181
|
+
const property = error.keyword === 'additionalProperties'
|
|
182
|
+
? String(error.params.additionalProperty)
|
|
183
|
+
: undefined;
|
|
184
|
+
const pointer = property
|
|
185
|
+
? `${error.instancePath}/${property}`
|
|
186
|
+
: error.instancePath || '/';
|
|
187
|
+
const yamlPath = pointer
|
|
188
|
+
.split('/')
|
|
189
|
+
.slice(1)
|
|
190
|
+
.map((segment) => /^\d+$/.test(segment) ? Number(segment) : segment);
|
|
191
|
+
const position = nodePosition(yaml, lineCounter, yamlPath);
|
|
192
|
+
return {
|
|
193
|
+
severity: 'error',
|
|
194
|
+
code: 'YM201',
|
|
195
|
+
message: property
|
|
196
|
+
? `Property "${property}" is not allowed`
|
|
197
|
+
: `Document schema violation: ${describeSchemaViolation(error)}`,
|
|
198
|
+
path: input.path,
|
|
199
|
+
pointer,
|
|
200
|
+
line: position.line,
|
|
201
|
+
column: position.col,
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
entry: {
|
|
206
|
+
source: input.source,
|
|
207
|
+
kind: 'document',
|
|
208
|
+
value,
|
|
209
|
+
schemaDiagnostics,
|
|
210
|
+
positions: new Map(),
|
|
211
|
+
},
|
|
212
|
+
fresh,
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
// Reuse is decided by exact source-text equality against the previous cache, so
|
|
216
|
+
// a wrong or stale cache can only cost work, never change output. Sources that
|
|
217
|
+
// left the workspace leave the returned cache with them.
|
|
218
|
+
const parseSources = (sources, previous) => {
|
|
219
|
+
const entries = new Map();
|
|
220
|
+
let reused = 0;
|
|
221
|
+
const parsed = sources.map((input) => {
|
|
222
|
+
const cached = previous?.sources.get(input.path);
|
|
223
|
+
if (cached !== undefined && cached.source === input.source) {
|
|
224
|
+
reused += 1;
|
|
225
|
+
entries.set(input.path, cached);
|
|
226
|
+
return { input, entry: cached };
|
|
227
|
+
}
|
|
228
|
+
const { entry, fresh } = parseWorkspaceSource(input);
|
|
229
|
+
entries.set(input.path, entry);
|
|
230
|
+
return { input, entry, fresh };
|
|
231
|
+
});
|
|
232
|
+
return { parsed, cache: { sources: entries }, reused };
|
|
233
|
+
};
|
|
234
|
+
function compileWorkspaceResolved(parsed) {
|
|
103
235
|
const profileInputs = [];
|
|
104
236
|
const documentInputs = [];
|
|
105
|
-
for (const source of
|
|
106
|
-
|
|
107
|
-
if (probe.get('format') === 'yarramate/profile/v1') {
|
|
237
|
+
for (const source of parsed) {
|
|
238
|
+
if (source.entry.kind === 'profile') {
|
|
108
239
|
profileInputs.push(source);
|
|
109
240
|
}
|
|
110
241
|
else {
|
|
@@ -146,33 +277,13 @@ function compileWorkspaceResolved(sources) {
|
|
|
146
277
|
});
|
|
147
278
|
const profileDiagnostics = [];
|
|
148
279
|
const pendingProfiles = [];
|
|
149
|
-
for (const input of profileInputs) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const value =
|
|
153
|
-
const positionFor = (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
node !== null &&
|
|
157
|
-
'range' in node &&
|
|
158
|
-
Array.isArray(node.range)
|
|
159
|
-
? node.range[0]
|
|
160
|
-
: 0;
|
|
161
|
-
return lineCounter.linePos(offset);
|
|
162
|
-
};
|
|
163
|
-
if (yaml.errors.length > 0) {
|
|
164
|
-
for (const error of yaml.errors) {
|
|
165
|
-
const position = error.linePos?.[0] ?? { line: 1, col: 1 };
|
|
166
|
-
profileDiagnostics.push({
|
|
167
|
-
severity: 'error',
|
|
168
|
-
code: 'YM101',
|
|
169
|
-
message: error.message.split(' at line ')[0] ?? error.message,
|
|
170
|
-
path: input.path,
|
|
171
|
-
pointer: '/',
|
|
172
|
-
line: position.line,
|
|
173
|
-
column: position.col,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
280
|
+
for (const { input, entry, fresh } of profileInputs) {
|
|
281
|
+
// `validateProfile` below is what checks this shape; the cast carries the
|
|
282
|
+
// same pre-validation assumption the loop has always made.
|
|
283
|
+
const value = entry.value;
|
|
284
|
+
const positionFor = positionReader(input.source, entry.positions, fresh);
|
|
285
|
+
if (entry.schemaDiagnostics.length > 0) {
|
|
286
|
+
profileDiagnostics.push(...entry.schemaDiagnostics);
|
|
176
287
|
continue;
|
|
177
288
|
}
|
|
178
289
|
if (!validateProfile(value)) {
|
|
@@ -383,19 +494,13 @@ function compileWorkspaceResolved(sources) {
|
|
|
383
494
|
if (profileDiagnostics.length > 0) {
|
|
384
495
|
return diagnosticFailure(profileDiagnostics);
|
|
385
496
|
}
|
|
386
|
-
const documents = documentInputs.map((input) => {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const value =
|
|
497
|
+
const documents = documentInputs.map(({ input, entry, fresh }) => {
|
|
498
|
+
// Schema-checked by `parseWorkspaceSource`; the faults it found are the
|
|
499
|
+
// `schemaDiagnostics` returned below, and they gate every later phase.
|
|
500
|
+
const value = entry.value;
|
|
501
|
+
const positionAt = positionReader(input.source, entry.positions, fresh);
|
|
390
502
|
const location = (yamlPath, pointer) => {
|
|
391
|
-
const
|
|
392
|
-
const offset = typeof node === 'object' &&
|
|
393
|
-
node !== null &&
|
|
394
|
-
'range' in node &&
|
|
395
|
-
Array.isArray(node.range)
|
|
396
|
-
? node.range[0]
|
|
397
|
-
: 0;
|
|
398
|
-
const position = lineCounter.linePos(offset);
|
|
503
|
+
const position = positionAt(yamlPath);
|
|
399
504
|
return {
|
|
400
505
|
document: value?.id ?? '<unknown>',
|
|
401
506
|
path: input.path,
|
|
@@ -404,48 +509,12 @@ function compileWorkspaceResolved(sources) {
|
|
|
404
509
|
column: position.col,
|
|
405
510
|
};
|
|
406
511
|
};
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
path: input.path,
|
|
414
|
-
pointer: '/',
|
|
415
|
-
line: position.line,
|
|
416
|
-
column: position.col,
|
|
417
|
-
};
|
|
418
|
-
});
|
|
419
|
-
const valid = parseDiagnostics.length === 0 && validateDocument(value);
|
|
420
|
-
const schemaDiagnostics = parseDiagnostics.length > 0
|
|
421
|
-
? parseDiagnostics
|
|
422
|
-
: valid
|
|
423
|
-
? []
|
|
424
|
-
: (validateDocument.errors ?? []).map((error) => {
|
|
425
|
-
const property = error.keyword === 'additionalProperties'
|
|
426
|
-
? String(error.params.additionalProperty)
|
|
427
|
-
: undefined;
|
|
428
|
-
const pointer = property
|
|
429
|
-
? `${error.instancePath}/${property}`
|
|
430
|
-
: error.instancePath || '/';
|
|
431
|
-
const yamlPath = pointer
|
|
432
|
-
.split('/')
|
|
433
|
-
.slice(1)
|
|
434
|
-
.map((segment) => /^\d+$/.test(segment) ? Number(segment) : segment);
|
|
435
|
-
const source = location(yamlPath, pointer);
|
|
436
|
-
return {
|
|
437
|
-
severity: 'error',
|
|
438
|
-
code: 'YM201',
|
|
439
|
-
message: property
|
|
440
|
-
? `Property "${property}" is not allowed`
|
|
441
|
-
: `Document schema violation: ${describeSchemaViolation(error)}`,
|
|
442
|
-
path: input.path,
|
|
443
|
-
pointer,
|
|
444
|
-
line: source.line,
|
|
445
|
-
column: source.column,
|
|
446
|
-
};
|
|
447
|
-
});
|
|
448
|
-
return { input, value, location, schemaDiagnostics };
|
|
512
|
+
return {
|
|
513
|
+
input,
|
|
514
|
+
value,
|
|
515
|
+
location,
|
|
516
|
+
schemaDiagnostics: entry.schemaDiagnostics,
|
|
517
|
+
};
|
|
449
518
|
});
|
|
450
519
|
const claims = [];
|
|
451
520
|
const subjects = [];
|
|
@@ -1395,7 +1464,24 @@ function compileWorkspaceResolved(sources) {
|
|
|
1395
1464
|
};
|
|
1396
1465
|
}
|
|
1397
1466
|
export function compileWorkspace(sources) {
|
|
1398
|
-
const result = compileWorkspaceResolved(sources);
|
|
1467
|
+
const result = compileWorkspaceResolved(parseSources(sources).parsed);
|
|
1399
1468
|
return result.ok ? { ok: true, graph: result.graph } : result;
|
|
1400
1469
|
}
|
|
1401
|
-
export const compileWorkspaceWithProfileContext = (sources) => compileWorkspaceResolved(sources);
|
|
1470
|
+
export const compileWorkspaceWithProfileContext = (sources) => compileWorkspaceResolved(parseSources(sources).parsed);
|
|
1471
|
+
/**
|
|
1472
|
+
* Compiles the whole workspace, reusing the YAML parse of every source whose
|
|
1473
|
+
* text is unchanged since `previous`. The compiled output is byte-identical to
|
|
1474
|
+
* {@link compileWorkspaceWithProfileContext} for the same sources: the cache
|
|
1475
|
+
* holds parse results only, and every cross-document decision is re-derived.
|
|
1476
|
+
*
|
|
1477
|
+
* Hold the returned `cache` and pass it to the next call. It retains one
|
|
1478
|
+
* composed value per current source and drops sources that left the workspace.
|
|
1479
|
+
*/
|
|
1480
|
+
export const compileWorkspaceIncremental = (sources, previous) => {
|
|
1481
|
+
const { parsed, cache, reused } = parseSources(sources, previous);
|
|
1482
|
+
return {
|
|
1483
|
+
...compileWorkspaceResolved(parsed),
|
|
1484
|
+
incremental: reused > 0,
|
|
1485
|
+
cache,
|
|
1486
|
+
};
|
|
1487
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { compileWorkspace, compileWorkspaceWithProfileContext, } from './compiler.js';
|
|
1
|
+
export { compileWorkspace, compileWorkspaceIncremental, compileWorkspaceWithProfileContext, } from './compiler.js';
|
|
2
2
|
export { serializeSemanticGraph } from './graph.js';
|
|
3
3
|
export { checkCoreContract, loadCoreContract, type CoreContract, type CoreContractCommand, type CoreContractFormat, type CoreContractLoadResult, type CoreContractSurface, } from './core-contract.js';
|
|
4
4
|
export { compareArchitectureStates, type StateComparison, type StateComparisonIssue, type StateComparisonResult, } from './architecture-state.js';
|
|
@@ -7,7 +7,7 @@ export { evaluateEvidence, evaluateEvidenceWorkspace, loadEvidence, type Evidenc
|
|
|
7
7
|
export { constraintExpectsPredicate, reconcileEvidenceReports, type AssertedRelationship, type AttestationStaleness, type DeclaredSource, type EvidenceFinding, type ExpectationComparison, type ReconciliationFinding, type ReconciliationReport, type StaleAttestationFinding, type UnobservedExpectation, } from './reconciliation.js';
|
|
8
8
|
export { deriveAttestationStaleness } from './attestation-staleness.js';
|
|
9
9
|
export { buildRtm, renderRtmMarkdown, type RequirementsTraceabilityMatrix, type RtmAttestation, type RtmContextEntry, type RtmDescopedEntry, type RtmEvidenceVerdict, type RtmLineageEntry, type RtmRealizer, type RtmRow, type RtmSource, } from './rtm.js';
|
|
10
|
-
export type { CompilationResult, ContextualCompilationResult, Diagnostic, GraphClaim, GraphSource, SemanticGraph, ResolvedProfileContext, WorkspaceSource, } from './compiler.js';
|
|
10
|
+
export type { CompilationCache, CompilationResult, ContextualCompilationResult, IncrementalCompilationResult, ParsedWorkspaceSource, Diagnostic, GraphClaim, GraphSource, SemanticGraph, ResolvedProfileContext, WorkspaceSource, } from './compiler.js';
|
|
11
11
|
export { canonicalProjection, evaluateProjection, loadProjection, renderProjectionMarkdown, } from './projection.js';
|
|
12
12
|
export { loadAdapterMapping, validateAdapterMapping, validateAdapterMappings, type AdapterMapping, type AdapterMappingLoadResult, type AdapterMappingValidationResult, type AdapterMappingsValidationResult, type AdapterSubjectMapping, } from './adapter-mapping.js';
|
|
13
13
|
export type { LifecycleStatus, ProjectionDefinition, ProjectionLoadResult, ProjectionResult, } from './projection.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { compileWorkspace, compileWorkspaceWithProfileContext, } from './compiler.js';
|
|
1
|
+
export { compileWorkspace, compileWorkspaceIncremental, compileWorkspaceWithProfileContext, } from './compiler.js';
|
|
2
2
|
export { serializeSemanticGraph } from './graph.js';
|
|
3
3
|
export { checkCoreContract, loadCoreContract, } from './core-contract.js';
|
|
4
4
|
export { compareArchitectureStates, } from './architecture-state.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yarramate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Tool-neutral semantic architecture engine and guided methodology",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -113,7 +113,8 @@
|
|
|
113
113
|
"self:check:likec4": "pnpm build && node dist/adapters/likec4-cli.js check .yarramate/likec4-project.yaml .yarramate/workspace.yaml",
|
|
114
114
|
"self:check:likec4:json": "pnpm build && node dist/adapters/likec4-cli.js check .yarramate/likec4-project.yaml --json .yarramate/workspace.yaml",
|
|
115
115
|
"self:export:likec4": "pnpm build && node dist/adapters/likec4-cli.js export-project .yarramate/likec4-project.yaml .yarramate-out/likec4 .yarramate/workspace.yaml",
|
|
116
|
-
"
|
|
116
|
+
"self:check:likec4:output": "pnpm build && node dist/adapters/likec4-cli.js export-project --check .yarramate/likec4-project.yaml .yarramate-out/likec4 .yarramate/workspace.yaml",
|
|
117
|
+
"validate": "pnpm self:export:likec4 && pnpm self:check:likec4:output && likec4 validate --no-layout .yarramate-out/likec4",
|
|
117
118
|
"verify": "pnpm typecheck && pnpm build && pnpm test && pnpm self:check && pnpm validate",
|
|
118
119
|
"test": "vitest run",
|
|
119
120
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.visual.json"
|
|
@@ -124,7 +125,6 @@
|
|
|
124
125
|
"yaml": "^2.8.1"
|
|
125
126
|
},
|
|
126
127
|
"devDependencies": {
|
|
127
|
-
"@likec4/core": "1.59.2",
|
|
128
128
|
"@types/node": "^26.1.2",
|
|
129
129
|
"@types/react": "^19.2.18",
|
|
130
130
|
"@types/react-dom": "^19.2.4",
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"cytoscape": "^3.34.1",
|
|
133
133
|
"cytoscape-elk": "^2.3.0",
|
|
134
134
|
"elkjs": "^0.12.0",
|
|
135
|
-
"likec4": "
|
|
135
|
+
"likec4": "1.59.2",
|
|
136
136
|
"react": "^19.2.8",
|
|
137
137
|
"react-dom": "^19.2.8",
|
|
138
138
|
"typescript": "^7.0.2",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"const": "error"
|
|
35
35
|
},
|
|
36
36
|
"code": {
|
|
37
|
-
"enum": ["YMLC101", "YMLC102", "YMLC103", "YMLC104", "YMLC105", "YMLC106", "YMLC107", "YMLC108", "YMLC109", "YMLC110"]
|
|
37
|
+
"enum": ["YMLC101", "YMLC102", "YMLC103", "YMLC104", "YMLC105", "YMLC106", "YMLC107", "YMLC108", "YMLC109", "YMLC110", "YMLC111", "YMLC112"]
|
|
38
38
|
},
|
|
39
39
|
"message": {
|
|
40
40
|
"type": "string",
|