yarramate 0.18.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.
Files changed (70) hide show
  1. package/README.md +9 -1
  2. package/catalogues/core-enrichment.yaml +52 -1
  3. package/dist/adapters/graphify-cli.js +0 -0
  4. package/dist/adapters/likec4-cli.js +0 -0
  5. package/dist/adapters/likec4-export.d.ts +1 -1
  6. package/dist/adapters/likec4-export.js +32 -0
  7. package/dist/adapters/mcp-cli.js +0 -0
  8. package/dist/adapters/visual/client.js +0 -1
  9. package/dist/adapters/visual/protocol-contract.d.ts +118 -35
  10. package/dist/adapters/visual/protocol-contract.js +1 -11
  11. package/dist/adapters/visual/protocol.d.ts +8 -1
  12. package/dist/adapters/visual/protocol.js +86 -46
  13. package/dist/adapters/visual/request.d.ts +37 -0
  14. package/dist/adapters/visual/request.js +131 -0
  15. package/dist/adapters/visual/session-server.d.ts +12 -14
  16. package/dist/adapters/visual/session-server.js +641 -283
  17. package/dist/adapters/visual/session-store.d.ts +31 -29
  18. package/dist/adapters/visual/session-store.js +94 -94
  19. package/dist/adapters/visual/wire.d.ts +36 -10
  20. package/dist/adapters/visual-cli.d.ts +3 -0
  21. package/dist/adapters/visual-cli.js +66 -1
  22. package/dist/apply-command.d.ts +10 -0
  23. package/dist/apply-command.js +399 -247
  24. package/dist/cli.js +0 -0
  25. package/dist/compiler.d.ts +66 -0
  26. package/dist/compiler.js +209 -89
  27. package/dist/graph-projection.d.ts +62 -0
  28. package/dist/graph-projection.js +222 -0
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.js +1 -1
  31. package/dist/interrogate-command.d.ts +2 -0
  32. package/dist/interrogate-command.js +22 -0
  33. package/dist/kind-label.d.ts +8 -0
  34. package/dist/kind-label.js +8 -0
  35. package/dist/operations.d.ts +139 -0
  36. package/dist/operations.js +11 -0
  37. package/dist/projection.d.ts +2 -0
  38. package/dist/projection.js +8 -2
  39. package/dist/reconciliation.js +8 -7
  40. package/dist/source-document.d.ts +10 -0
  41. package/dist/source-document.js +21 -1
  42. package/dist/visual-app/assets/index-6oSR9RGM.js +354 -0
  43. package/dist/visual-app/assets/index-dwA9AOr5.css +1 -0
  44. package/dist/visual-app/index.html +2 -2
  45. package/docs/CONSUMING-YARRAMATE.md +25 -19
  46. package/package.json +9 -4
  47. package/schema/yarramate-adapter-mapping.schema.json +1 -1
  48. package/schema/yarramate-apply-result.schema.json +8 -2
  49. package/schema/yarramate-document.schema.json +1 -1
  50. package/schema/yarramate-likec4-diagnostic-result.schema.json +1 -1
  51. package/schema/yarramate-operations.schema.json +203 -1
  52. package/schema/yarramate-projection.schema.json +12 -3
  53. package/schema/yarramate-question-catalogue.schema.json +15 -2
  54. package/schema/yarramate-visual-event.schema.json +143 -0
  55. package/schema/yarramate-visual-graph.schema.json +272 -0
  56. package/schema/yarramate-visual-layout.schema.json +41 -0
  57. package/schema/yarramate-visual-model.schema.json +6 -44
  58. package/schema/yarramate-visual-response.schema.json +170 -19
  59. package/schema/yarramate-visual-session-descriptor.schema.json +1 -1
  60. package/schema/yarramate-visual-session-request.schema.json +0 -70
  61. package/schema/yarramate-visual-session-started.schema.json +2 -9
  62. package/schema/yarramate-visual-status.schema.json +3 -2
  63. package/skills/yarramate-architecture/SKILL.md +20 -13
  64. package/skills/yarramate-architecture/references/journey-checklists.md +4 -2
  65. package/skills/yarramate-architecture/references/native-authoring.md +30 -0
  66. package/skills/yarramate-architecture/references/visual-conversations.md +149 -150
  67. package/dist/adapters/visual/likec4-compiler.d.ts +0 -55
  68. package/dist/adapters/visual/likec4-compiler.js +0 -460
  69. package/dist/visual-app/assets/index-CGvC8e3R.css +0 -1
  70. package/dist/visual-app/assets/index-CXuW1tOj.js +0 -209
@@ -1,6 +1,7 @@
1
- import { type VisualAuthority, type VisualDiagnostic, type VisualEvent, type VisualFreezeReason, type VisualHandoff, type VisualModel, type VisualResponse, type VisualSessionDescriptor, type VisualSessionRequest, type VisualTerminationReason } from './protocol.js';
1
+ import { type VisualAuthority, type VisualDiagnostic, type VisualEvent, type VisualFreezeReason, type VisualHandoff, type VisualResponse, type VisualSessionDescriptor, type VisualSessionRequest, type VisualTerminationReason } from './protocol.js';
2
+ import { type WorkspaceSource } from '../../compiler.js';
3
+ import { type CanvasGraph } from '../../graph-projection.js';
2
4
  export declare const VISUAL_SESSION_MARKER_FORMAT: 'yarramate/visual-session-marker/v1';
3
- export declare const VISUAL_ACTIVE_MODEL_FORMAT: 'yarramate/visual-active-model/v1';
4
5
  /**
5
6
  * Upper bound on how many orphaned sessions one `start` prunes. Cleanup runs on
6
7
  * the critical path of a new session, so a directory full of orphans must cost
@@ -19,8 +20,6 @@ export interface VisualSessionPaths {
19
20
  readonly marker: string;
20
21
  readonly descriptor: string;
21
22
  readonly journal: string;
22
- readonly candidates: string;
23
- readonly activeModel: string;
24
23
  }
25
24
  export interface SessionDependencies {
26
25
  readonly baseDir: string;
@@ -45,17 +44,6 @@ export interface VisualSessionMarker {
45
44
  readonly createdAt: string;
46
45
  readonly authority: VisualAuthority;
47
46
  }
48
- /**
49
- * Last-good rendering pointer. It names a staged candidate directory instead of
50
- * embedding the model so the swap is a single rename of a small file.
51
- */
52
- export interface VisualActiveModel {
53
- readonly format: typeof VISUAL_ACTIVE_MODEL_FORMAT;
54
- readonly candidate: string;
55
- readonly authority: VisualAuthority;
56
- readonly initialView: string;
57
- readonly promotedAt: string;
58
- }
59
47
  export interface VisualSessionCreated {
60
48
  readonly paths: VisualSessionPaths;
61
49
  readonly browserToken: string;
@@ -73,17 +61,6 @@ export interface VisualAppendRejected {
73
61
  readonly diagnostics: readonly VisualDiagnostic[];
74
62
  }
75
63
  export type VisualAppendResult = VisualAppendAccepted | VisualAppendRejected;
76
- export interface ModelPromotion {
77
- readonly model: VisualModel;
78
- readonly now: () => Date;
79
- readonly compile: (candidateDir: string) => Promise<readonly VisualDiagnostic[]>;
80
- }
81
- export interface ModelPromotionResult {
82
- readonly promoted: boolean;
83
- readonly candidate: string;
84
- readonly candidateDir: string;
85
- readonly diagnostics: readonly VisualDiagnostic[];
86
- }
87
64
  export declare const visualSessionPaths: (root: string) => VisualSessionPaths;
88
65
  export declare const createVisualSession: (request: VisualSessionRequest, deps: SessionDependencies) => Promise<VisualSessionCreated>;
89
66
  /**
@@ -105,12 +82,37 @@ export declare const appendVisualResponse: (paths: VisualSessionPaths, response:
105
82
  export declare const appendTerminalEvent: (paths: VisualSessionPaths, reason: VisualTerminationReason, deps: TerminalEventDependencies) => Promise<VisualEvent>;
106
83
  export declare const readActionableEventsAfter: (paths: VisualSessionPaths, sequence: number) => Promise<readonly VisualEvent[]>;
107
84
  export declare const recoverVisualSession: (paths: VisualSessionPaths, includeTranscript?: boolean) => Promise<VisualHandoff>;
108
- export declare const readActiveVisualModel: (paths: VisualSessionPaths) => Promise<VisualActiveModel | undefined>;
109
- export declare const promoteCompiledModel: (paths: VisualSessionPaths, promotion: ModelPromotion) => Promise<ModelPromotionResult>;
85
+ export type VisualModelGraphResult = {
86
+ readonly ok: true;
87
+ readonly graph: CanvasGraph;
88
+ } | {
89
+ readonly ok: false;
90
+ readonly diagnostics: readonly VisualDiagnostic[];
91
+ };
92
+ /**
93
+ * Builds the `graph` a session's `VisualModel` renders, from the workspace's
94
+ * own source documents. This is the whole construction step a visual session
95
+ * needs at start: no subprocess, no staged candidate directory, no on-disk
96
+ * pointer file — `compileWorkspaceWithProfileContext` and
97
+ * `projectGraphForCanvas` are both pure, synchronous functions over this
98
+ * repo's native compiler, so the caller assembles the result directly into
99
+ * the full `VisualModel` (format, authority, initialView, sourceDigests,
100
+ * graph) without any session-store I/O.
101
+ *
102
+ * A compile failure here is not a runtime defect — it is exactly the
103
+ * diagnostic-shaped rejection the browser already expects, so the workspace
104
+ * compiler's own diagnostics are returned unchanged rather than resynthesised.
105
+ * `Diagnostic` (compiler.ts) and `VisualDiagnostic` (protocol-contract.ts)
106
+ * already share one shape: severity, code, message, path, pointer, line,
107
+ * column.
108
+ */
109
+ export declare const buildVisualModelGraph: (sources: readonly WorkspaceSource[]) => VisualModelGraphResult;
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
  /**
@@ -1,16 +1,16 @@
1
1
  import { constants } from 'node:fs';
2
2
  import { lstat, mkdir, open, readFile, readdir, rename, rm, stat, truncate, writeFile, } from 'node:fs/promises';
3
- import { basename, dirname, join, resolve, sep } from 'node:path';
4
- import { VISUAL_LIMITS, parseVisualEvent, parseVisualHandoff, parseVisualModel, parseVisualResponse, parseVisualSessionDescriptor, parseVisualSessionRequest, } from './protocol.js';
3
+ import { basename, dirname, join, resolve } from 'node:path';
4
+ import { VISUAL_LIMITS, parseVisualEvent, parseVisualHandoff, parseVisualResponse, parseVisualSessionDescriptor, parseVisualSessionRequest, } from './protocol.js';
5
+ import { compileWorkspaceWithProfileContext, } from '../../compiler.js';
6
+ import { projectGraphForCanvas } from '../../graph-projection.js';
5
7
  export const VISUAL_SESSION_MARKER_FORMAT = 'yarramate/visual-session-marker/v1';
6
- export const VISUAL_ACTIVE_MODEL_FORMAT = 'yarramate/visual-active-model/v1';
7
8
  /**
8
9
  * Upper bound on how many orphaned sessions one `start` prunes. Cleanup runs on
9
10
  * the critical path of a new session, so a directory full of orphans must cost
10
11
  * a bounded amount of work; the remainder is collected by the next start.
11
12
  */
12
13
  export const VISUAL_SESSION_PRUNE_LIMIT = 64;
13
- const CANDIDATE_NAME = /^[0-9]{6}$/;
14
14
  const IDENTIFIER = /^[0-9a-f]{32}$/;
15
15
  const TIMESTAMP = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/;
16
16
  const NEWLINE = 0x0a;
@@ -58,7 +58,6 @@ const serialize = (key, action) => {
58
58
  const forget = (paths) => {
59
59
  states.delete(paths.journal);
60
60
  queues.delete(paths.journal);
61
- queues.delete(paths.candidates);
62
61
  };
63
62
  const drawHex = (randomBytes, size) => {
64
63
  const drawn = randomBytes(size);
@@ -113,8 +112,6 @@ export const visualSessionPaths = (root) => {
113
112
  marker: join(base, 'session.json'),
114
113
  descriptor: join(base, 'descriptor.json'),
115
114
  journal: join(base, 'journal.jsonl'),
116
- candidates: join(base, 'candidates'),
117
- activeModel: join(base, 'active-model.json'),
118
115
  };
119
116
  };
120
117
  const readSessionMarker = async (paths) => {
@@ -141,7 +138,7 @@ const readSessionMarker = async (paths) => {
141
138
  !IDENTIFIER.test(id) ||
142
139
  typeof createdAt !== 'string' ||
143
140
  !TIMESTAMP.test(createdAt) ||
144
- (authority !== 'canonical' && authority !== 'ad-hoc')) {
141
+ authority !== 'canonical') {
145
142
  throw storeError('YMVS124', `Session marker "${paths.marker}" is not ${VISUAL_SESSION_MARKER_FORMAT}`);
146
143
  }
147
144
  // A marker that names another directory cannot authorise destructive work on
@@ -277,7 +274,6 @@ export const createVisualSession = async (request, deps) => {
277
274
  createdAt: deps.now().toISOString(),
278
275
  authority: validated.value.authority,
279
276
  });
280
- await mkdir(paths.candidates, { recursive: false, mode: 0o700 });
281
277
  await writeFile(paths.journal, '', { mode: 0o600 });
282
278
  await syncDirectory(root);
283
279
  return {
@@ -559,91 +555,93 @@ export const recoverVisualSession = async (paths, includeTranscript = false) =>
559
555
  }
560
556
  return handoff;
561
557
  };
562
- export const readActiveVisualModel = async (paths) => {
563
- let raw = '';
564
- try {
565
- raw = await readFile(paths.activeModel, 'utf8');
566
- }
567
- catch {
568
- return undefined;
558
+ /**
559
+ * Builds the `graph` a session's `VisualModel` renders, from the workspace's
560
+ * own source documents. This is the whole construction step a visual session
561
+ * needs at start: no subprocess, no staged candidate directory, no on-disk
562
+ * pointer file — `compileWorkspaceWithProfileContext` and
563
+ * `projectGraphForCanvas` are both pure, synchronous functions over this
564
+ * repo's native compiler, so the caller assembles the result directly into
565
+ * the full `VisualModel` (format, authority, initialView, sourceDigests,
566
+ * graph) without any session-store I/O.
567
+ *
568
+ * A compile failure here is not a runtime defect — it is exactly the
569
+ * diagnostic-shaped rejection the browser already expects, so the workspace
570
+ * compiler's own diagnostics are returned unchanged rather than resynthesised.
571
+ * `Diagnostic` (compiler.ts) and `VisualDiagnostic` (protocol-contract.ts)
572
+ * already share one shape: severity, code, message, path, pointer, line,
573
+ * column.
574
+ */
575
+ export const buildVisualModelGraph = (sources) => {
576
+ const compiled = compileWorkspaceWithProfileContext(sources);
577
+ if (!compiled.ok) {
578
+ return { ok: false, diagnostics: compiled.diagnostics };
569
579
  }
570
- let parsed;
580
+ return {
581
+ ok: true,
582
+ graph: projectGraphForCanvas(compiled.graph, compiled.profileContext),
583
+ };
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) => {
571
591
  try {
572
- parsed = JSON.parse(raw);
592
+ await lstat(paths.journal);
593
+ return true;
573
594
  }
574
595
  catch {
575
- throw storeError('YMVS128', `Active model pointer "${paths.activeModel}" is not JSON`);
596
+ return false;
576
597
  }
577
- const fields = documentFields(parsed);
578
- const candidate = fields.candidate;
579
- const authority = fields.authority;
580
- const initialView = fields.initialView;
581
- const promotedAt = fields.promotedAt;
582
- if (fields.format !== VISUAL_ACTIVE_MODEL_FORMAT ||
583
- typeof candidate !== 'string' ||
584
- !CANDIDATE_NAME.test(candidate) ||
585
- (authority !== 'canonical' && authority !== 'ad-hoc') ||
586
- typeof initialView !== 'string' ||
587
- typeof promotedAt !== 'string' ||
588
- !TIMESTAMP.test(promotedAt)) {
589
- throw storeError('YMVS128', `Active model pointer "${paths.activeModel}" is not ${VISUAL_ACTIVE_MODEL_FORMAT}`);
590
- }
591
- return {
592
- format: VISUAL_ACTIVE_MODEL_FORMAT,
593
- candidate,
594
- authority,
595
- initialView,
596
- promotedAt,
597
- };
598
598
  };
599
- export const promoteCompiledModel = async (paths, promotion) => {
600
- // The runtime has already parsed the `model.replace` response that carried
601
- // this model, so a violation here is a runtime defect rather than untrusted
602
- // input, and must not be reported to the browser as a candidate diagnostic.
603
- const validated = parseVisualModel(promotion.model);
604
- if (!validated.ok) {
605
- const first = validated.diagnostics[0];
606
- throw storeError(first?.code ?? 'YMVS106', `Candidate model is invalid: ${first?.message ?? 'unknown violation'}`);
607
- }
608
- const model = validated.value;
609
- return serialize(paths.candidates, async () => {
610
- await readSessionMarker(paths);
611
- const existing = await readdir(paths.candidates);
612
- const highest = existing
613
- .filter((entry) => CANDIDATE_NAME.test(entry))
614
- .reduce((max, entry) => Math.max(max, Number(entry)), 0);
615
- const candidate = String(highest + 1).padStart(6, '0');
616
- const candidateDir = join(paths.candidates, candidate);
617
- await mkdir(candidateDir, { recursive: false, mode: 0o700 });
618
- for (const [file, contents] of Object.entries(model.files)) {
619
- const target = resolve(candidateDir, file);
620
- // Confinement is already enforced by the model schema and by the protocol
621
- // path check; re-checking the resolved path means neither of those alone
622
- // is load-bearing for a write outside the candidate directory.
623
- if (!target.startsWith(`${candidateDir}${sep}`)) {
624
- throw storeError('YMVS125', `Candidate file "${file}" resolves outside "${candidateDir}"`);
625
- }
626
- await mkdir(dirname(target), { recursive: true, mode: 0o700 });
627
- await writePrivateFile(target, contents);
628
- }
629
- const diagnostics = await promotion.compile(candidateDir);
630
- if (diagnostics.length > 0) {
631
- return { promoted: false, candidate, candidateDir, diagnostics };
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;
632
625
  }
633
- await writePrivateJson(paths.activeModel, {
634
- format: VISUAL_ACTIVE_MODEL_FORMAT,
635
- candidate,
636
- authority: model.authority,
637
- initialView: model.initialView,
638
- promotedAt: promotion.now().toISOString(),
639
- });
640
- return { promoted: true, candidate, candidateDir, diagnostics: [] };
641
- });
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 });
642
638
  };
643
639
  /**
644
640
  * Recovers the handoff and only then deletes the session, so cleanup can never
645
641
  * be the step that loses confirmed state. Returns `undefined` when the session
646
- * 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.
647
645
  */
648
646
  export const removeVisualSession = async (paths, includeTranscript = false) => {
649
647
  let entry;
@@ -657,8 +655,12 @@ export const removeVisualSession = async (paths, includeTranscript = false) => {
657
655
  if (!entry.isDirectory()) {
658
656
  throw storeError('YMVS125', `Session root "${paths.root}" is not a directory`);
659
657
  }
660
- const handoff = await recoverVisualSession(paths, includeTranscript);
661
- 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);
662
664
  await syncDirectory(dirname(paths.root));
663
665
  forget(paths);
664
666
  return handoff;
@@ -668,11 +670,10 @@ export const removeVisualSession = async (paths, includeTranscript = false) => {
668
670
  * none of them could be measured.
669
671
  *
670
672
  * POSIX updates a directory's modification time only when its own entries
671
- * change, so the session root records nothing about an appended journal, and
672
- * the candidates directory records nothing about a promoted pointer. Every
673
- * artefact that can carry activity is therefore measured directly; the
674
- * optional ones are simply absent until a session produces them. `lstat`
675
- * keeps a symlinked artefact from reporting some other file's activity.
673
+ * change, so the session root records nothing about an appended journal.
674
+ * Every artefact that can carry activity is therefore measured directly;
675
+ * `lstat` keeps a symlinked artefact from reporting some other file's
676
+ * activity.
676
677
  */
677
678
  const lastActivityMs = async (paths) => {
678
679
  let newest;
@@ -681,8 +682,6 @@ const lastActivityMs = async (paths) => {
681
682
  paths.marker,
682
683
  paths.descriptor,
683
684
  paths.journal,
684
- paths.candidates,
685
- paths.activeModel,
686
685
  ]) {
687
686
  let entry;
688
687
  try {
@@ -744,8 +743,9 @@ export const pruneStaleVisualSessions = async (baseDir, now, limit = VISUAL_SESS
744
743
  stale.sort((left, right) => left.activeAt - right.activeAt || left.root.localeCompare(right.root));
745
744
  const removed = [];
746
745
  for (const candidate of stale.slice(0, Math.max(0, limit))) {
747
- await rm(candidate.root, { recursive: true, force: true });
748
- forget(visualSessionPaths(candidate.root));
746
+ const paths = visualSessionPaths(candidate.root);
747
+ await removeSessionDirectory(paths);
748
+ forget(paths);
749
749
  removed.push(candidate.root);
750
750
  }
751
751
  if (removed.length > 0) {
@@ -1,4 +1,5 @@
1
- import type { VISUAL_PROTOCOL_VERSION, VisualAuthority, VisualCapabilities, VisualChoicePresentPayload, VisualDiagnostic, VisualFreezeReason, VisualResponse, VisualTerminationReason } from './protocol-contract.js';
1
+ import type { CanvasGraph } from '../../graph-projection.js';
2
+ import type { VISUAL_PROTOCOL_VERSION, VisualApplyResultPayload, VisualAuthority, VisualBrowserInput, VisualCapabilities, VisualChoicePresentPayload, VisualDiagnostic, VisualFilterResultPayload, VisualFreezeReason, VisualKindOption, VisualLayoutPositions, VisualLayoutSaveResultPayload, VisualResponse, VisualTerminationReason, VisualViewSaveResultPayload, VisualViewSummary } from './protocol-contract.js';
2
3
  /**
3
4
  * Transport shapes the session server and the browser application both speak.
4
5
  *
@@ -9,18 +10,21 @@ import type { VISUAL_PROTOCOL_VERSION, VisualAuthority, VisualCapabilities, Visu
9
10
  * that serves it — this module holds types only, and every dependency it has on
10
11
  * the protocol is type-only too.
11
12
  */
12
- /** One promoted candidate as the browser receives it. */
13
+ /** The resolved graph a session renders, as the browser receives it. */
13
14
  export interface VisualRenderedModel {
14
- readonly candidate: string;
15
15
  readonly authority: VisualAuthority;
16
16
  readonly initialView: string;
17
- readonly views: readonly string[];
18
- /**
19
- * The trusted compiler's own LikeC4 export for this candidate, the document
20
- * `createLikeC4Model` consumes. Its internals belong to LikeC4, so the
21
- * transport carries it verbatim instead of restating a shape it does not own.
22
- */
23
- readonly compiled: unknown;
17
+ readonly graph: CanvasGraph;
18
+ /** Manifest-relative document paths — the add-concept target dropdown. */
19
+ readonly documents: readonly string[];
20
+ readonly vocabulary: {
21
+ readonly conceptKinds: readonly VisualKindOption[];
22
+ readonly relationshipKinds: readonly VisualKindOption[];
23
+ };
24
+ /** Every saved layout sidecar, keyed by projection id (Plan-level decision 1). */
25
+ readonly layouts: {
26
+ readonly [projectionId: string]: VisualLayoutPositions;
27
+ };
24
28
  }
25
29
  /**
26
30
  * One line of the conversation, as plain text.
@@ -47,6 +51,8 @@ export interface VisualSessionSnapshot {
47
51
  readonly webSocketUrl: string;
48
52
  readonly model: VisualRenderedModel;
49
53
  readonly transcript: readonly VisualTranscriptRecord[];
54
+ /** The saved views the reviewer can switch this session's diagram to. */
55
+ readonly views: readonly VisualViewSummary[];
50
56
  /**
51
57
  * Whether the agent still owes an answer to something the reviewer sent. A
52
58
  * browser that reconnects mid-turn reads this rather than inferring the turn
@@ -79,6 +85,14 @@ export type VisualServerFrame = {
79
85
  readonly eventId: string;
80
86
  } | {
81
87
  readonly kind: 'rejected';
88
+ /**
89
+ * The input this refusal is about, as the frame that carried it named
90
+ * itself. A browser with a commit or a save in flight can only retire
91
+ * the right control if the refusal says which one died, and a refusal
92
+ * that never got as far as reading a type - a binary frame, a document
93
+ * that is not JSON - names nothing rather than guessing.
94
+ */
95
+ readonly refused?: VisualBrowserInput['type'];
82
96
  readonly diagnostics: readonly VisualDiagnostic[];
83
97
  readonly frozen?: VisualFreezeReason;
84
98
  } | {
@@ -87,6 +101,18 @@ export type VisualServerFrame = {
87
101
  } | {
88
102
  readonly kind: 'model';
89
103
  readonly model: VisualRenderedModel;
104
+ } | {
105
+ readonly kind: 'filter-result';
106
+ readonly result: VisualFilterResultPayload;
107
+ } | {
108
+ readonly kind: 'view-save-result';
109
+ readonly result: VisualViewSaveResultPayload;
110
+ } | {
111
+ readonly kind: 'apply-result';
112
+ readonly result: VisualApplyResultPayload;
113
+ } | {
114
+ readonly kind: 'layout-save-result';
115
+ readonly result: VisualLayoutSaveResultPayload;
90
116
  } | {
91
117
  readonly kind: 'closing';
92
118
  readonly reason: VisualTerminationReason;
@@ -10,6 +10,9 @@ export declare const VISUAL_SESSION_DIRECTORY: string;
10
10
  /**
11
11
  * Every command an agent can run to completion. `start` is not one of them: it
12
12
  * blocks for the life of the session and is dispatched by `runVisualCli`.
13
+ *
14
+ * `request` is the one verb that takes no descriptor - it runs before any
15
+ * session exists - so it is dispatched ahead of the descriptor requirement.
13
16
  */
14
17
  export declare function runVisualClientCli(args: readonly string[], cwd?: string): Promise<CliResult>;
15
18
  /** Where a foreground session publishes, and where it observes its signals. */
@@ -3,9 +3,12 @@ import { join } from 'node:path';
3
3
  import { isMainModule, versionResult } from '../cli-support.js';
4
4
  import { fetchVisualStatus, readVisualJsonDocument, readVisualSessionDescriptor, recoverVisualSessionClient, sendVisualResponse, stopVisualSessionClient, visualClientDiagnostic, visualFailureDiagnostics, visualSessionAlreadyStopped, waitForVisualEvent, } from './visual/client.js';
5
5
  import { parseVisualResponse, parseVisualSessionRequest, } from './visual/protocol.js';
6
+ import { buildVisualSessionRequest } from './visual/request.js';
6
7
  import { startVisualServer } from './visual/session-server.js';
7
8
  import { pruneStaleVisualSessions } from './visual/session-store.js';
8
9
  export const visualUsage = 'Usage:\n' +
10
+ ' yarramate-visual request [--view <id>] [--title <text>]\n' +
11
+ ' [--description <text>] [--chat]\n' +
9
12
  ' yarramate-visual start <request.json>\n' +
10
13
  ' yarramate-visual wait <descriptor.json> [--after <sequence>]\n' +
11
14
  ' yarramate-visual respond <descriptor.json> <response.json>\n' +
@@ -66,6 +69,63 @@ const transcriptFlag = (rest) => rest.length === 0
66
69
  : rest.length === 1 && rest[0] === '--transcript'
67
70
  ? true
68
71
  : undefined;
72
+ /**
73
+ * Strict because the document this builds is a session's whole input: a
74
+ * misspelled flag that was silently ignored would open a session on the wrong
75
+ * view, or with chat off when the caller asked for it. A repeated flag is a
76
+ * caller that means two things at once, and a value beginning with `--` is a
77
+ * missing value rather than a title, so both refuse with usage.
78
+ */
79
+ const requestFlags = (rest) => {
80
+ let view;
81
+ let title;
82
+ let description;
83
+ let chatEnabled = false;
84
+ for (let index = 0; index < rest.length; index += 1) {
85
+ const flag = rest[index];
86
+ if (flag === '--chat') {
87
+ if (chatEnabled)
88
+ return undefined;
89
+ chatEnabled = true;
90
+ continue;
91
+ }
92
+ const value = rest[index + 1];
93
+ if (value === undefined || value.startsWith('--'))
94
+ return undefined;
95
+ index += 1;
96
+ if (flag === '--view' && view === undefined)
97
+ view = value;
98
+ else if (flag === '--title' && title === undefined)
99
+ title = value;
100
+ else if (flag === '--description' && description === undefined) {
101
+ description = value;
102
+ }
103
+ else
104
+ return undefined;
105
+ }
106
+ return { view, title, description, chatEnabled };
107
+ };
108
+ /**
109
+ * Transcribes the workspace into the request document `start` consumes. It is
110
+ * the only visual command that reads the repository rather than a session:
111
+ * every other one addresses a descriptor, and this one runs before a session
112
+ * exists.
113
+ */
114
+ const requestCliCommand = (rest, cwd) => {
115
+ const flags = requestFlags(rest);
116
+ if (flags === undefined)
117
+ return usageResult;
118
+ const built = buildVisualSessionRequest({
119
+ cwd,
120
+ initialView: flags.view,
121
+ title: flags.title,
122
+ description: flags.description,
123
+ chatEnabled: flags.chatEnabled,
124
+ });
125
+ if (!built.ok)
126
+ return refusalResult(built.diagnostics);
127
+ return { exitCode: 0, stdout: documentLine(built.request), stderr: '' };
128
+ };
69
129
  const waitCliCommand = async (descriptorPath, rest, cwd) => {
70
130
  const after = sequenceFlag(rest);
71
131
  if (after === undefined)
@@ -164,11 +224,16 @@ const stopCliCommand = async (descriptorPath, rest, cwd) => {
164
224
  /**
165
225
  * Every command an agent can run to completion. `start` is not one of them: it
166
226
  * blocks for the life of the session and is dispatched by `runVisualCli`.
227
+ *
228
+ * `request` is the one verb that takes no descriptor - it runs before any
229
+ * session exists - so it is dispatched ahead of the descriptor requirement.
167
230
  */
168
231
  export async function runVisualClientCli(args, cwd = process.cwd()) {
169
232
  const [command, descriptor, ...rest] = args;
170
233
  if (command === '--version')
171
234
  return versionResult('yarramate-visual');
235
+ if (command === 'request')
236
+ return requestCliCommand(args.slice(1), cwd);
172
237
  if (descriptor === undefined)
173
238
  return usageResult;
174
239
  switch (command) {
@@ -215,7 +280,7 @@ export const runVisualStart = async (requestPath, cwd = process.cwd(), io = proc
215
280
  // A previous runtime's orphans are collected on the critical path of a new
216
281
  // session, bounded per pass so a full directory cannot stall this one.
217
282
  await pruneStaleVisualSessions(baseDir, new Date());
218
- handle = await startVisualServer({ request: request.value, baseDir });
283
+ handle = await startVisualServer({ request: request.value, baseDir, cwd });
219
284
  }
220
285
  catch (cause) {
221
286
  // A failed start has already recovered and removed whatever it created.
@@ -1,2 +1,12 @@
1
1
  import { type CliResult } from './cli-support.js';
2
+ import { type Diagnostic, type WorkspaceSource } from './compiler.js';
3
+ import type { YarramateApplyResult } from './operations.js';
4
+ export type ApplyOutcome = {
5
+ readonly ok: true;
6
+ readonly result: YarramateApplyResult;
7
+ } | {
8
+ readonly ok: false;
9
+ readonly diagnostics: readonly Diagnostic[];
10
+ };
11
+ export declare const applyOperations: (operations: WorkspaceSource, workspace: WorkspaceSource, cwd: string) => ApplyOutcome;
2
12
  export declare function runApplyCommand(options: readonly string[], cwd: string): CliResult;