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 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
- const handoff = await recoverVisualSession(paths, includeTranscript);
603
- await rm(paths.root, { recursive: true, force: true });
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
- await rm(candidate.root, { recursive: true, force: true });
687
- forget(visualSessionPaths(candidate.root));
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) {
@@ -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
- function compileWorkspaceResolved(sources) {
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 sources) {
106
- const probe = parseDocument(source.source);
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
- const lineCounter = new LineCounter();
151
- const yaml = parseDocument(input.source, { lineCounter });
152
- const value = yaml.toJS();
153
- const positionFor = (yamlPath) => {
154
- const node = yaml.getIn(yamlPath, true);
155
- const offset = typeof node === 'object' &&
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
- const lineCounter = new LineCounter();
388
- const yaml = parseDocument(input.source, { lineCounter });
389
- const value = yaml.toJS();
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 node = yaml.getIn(yamlPath, true);
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
- const parseDiagnostics = yaml.errors.map((error) => {
408
- const position = error.linePos?.[0] ?? { line: 1, col: 1 };
409
- return {
410
- severity: 'error',
411
- code: 'YM101',
412
- message: error.message.split(' at line ')[0] ?? error.message,
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.19.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
- "validate": "pnpm self:export:likec4 && likec4 validate --no-layout .yarramate-out/likec4",
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": "^1.59.2",
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",