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.
- package/README.md +9 -1
- package/catalogues/core-enrichment.yaml +52 -1
- package/dist/adapters/graphify-cli.js +0 -0
- package/dist/adapters/likec4-cli.js +0 -0
- package/dist/adapters/likec4-export.d.ts +1 -1
- package/dist/adapters/likec4-export.js +32 -0
- package/dist/adapters/mcp-cli.js +0 -0
- package/dist/adapters/visual/client.js +0 -1
- package/dist/adapters/visual/protocol-contract.d.ts +118 -35
- package/dist/adapters/visual/protocol-contract.js +1 -11
- package/dist/adapters/visual/protocol.d.ts +8 -1
- package/dist/adapters/visual/protocol.js +86 -46
- package/dist/adapters/visual/request.d.ts +37 -0
- package/dist/adapters/visual/request.js +131 -0
- package/dist/adapters/visual/session-server.d.ts +12 -14
- package/dist/adapters/visual/session-server.js +641 -283
- package/dist/adapters/visual/session-store.d.ts +31 -29
- package/dist/adapters/visual/session-store.js +94 -94
- package/dist/adapters/visual/wire.d.ts +36 -10
- package/dist/adapters/visual-cli.d.ts +3 -0
- package/dist/adapters/visual-cli.js +66 -1
- package/dist/apply-command.d.ts +10 -0
- package/dist/apply-command.js +399 -247
- package/dist/cli.js +0 -0
- package/dist/compiler.d.ts +66 -0
- package/dist/compiler.js +209 -89
- package/dist/graph-projection.d.ts +62 -0
- package/dist/graph-projection.js +222 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/interrogate-command.d.ts +2 -0
- package/dist/interrogate-command.js +22 -0
- package/dist/kind-label.d.ts +8 -0
- package/dist/kind-label.js +8 -0
- package/dist/operations.d.ts +139 -0
- package/dist/operations.js +11 -0
- package/dist/projection.d.ts +2 -0
- package/dist/projection.js +8 -2
- package/dist/reconciliation.js +8 -7
- package/dist/source-document.d.ts +10 -0
- package/dist/source-document.js +21 -1
- package/dist/visual-app/assets/index-6oSR9RGM.js +354 -0
- package/dist/visual-app/assets/index-dwA9AOr5.css +1 -0
- package/dist/visual-app/index.html +2 -2
- package/docs/CONSUMING-YARRAMATE.md +25 -19
- package/package.json +9 -4
- package/schema/yarramate-adapter-mapping.schema.json +1 -1
- package/schema/yarramate-apply-result.schema.json +8 -2
- package/schema/yarramate-document.schema.json +1 -1
- package/schema/yarramate-likec4-diagnostic-result.schema.json +1 -1
- package/schema/yarramate-operations.schema.json +203 -1
- package/schema/yarramate-projection.schema.json +12 -3
- package/schema/yarramate-question-catalogue.schema.json +15 -2
- package/schema/yarramate-visual-event.schema.json +143 -0
- package/schema/yarramate-visual-graph.schema.json +272 -0
- package/schema/yarramate-visual-layout.schema.json +41 -0
- package/schema/yarramate-visual-model.schema.json +6 -44
- package/schema/yarramate-visual-response.schema.json +170 -19
- package/schema/yarramate-visual-session-descriptor.schema.json +1 -1
- package/schema/yarramate-visual-session-request.schema.json +0 -70
- package/schema/yarramate-visual-session-started.schema.json +2 -9
- package/schema/yarramate-visual-status.schema.json +3 -2
- package/skills/yarramate-architecture/SKILL.md +20 -13
- package/skills/yarramate-architecture/references/journey-checklists.md +4 -2
- package/skills/yarramate-architecture/references/native-authoring.md +30 -0
- package/skills/yarramate-architecture/references/visual-conversations.md +149 -150
- package/dist/adapters/visual/likec4-compiler.d.ts +0 -55
- package/dist/adapters/visual/likec4-compiler.js +0 -460
- package/dist/visual-app/assets/index-CGvC8e3R.css +0 -1
- 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
|
|
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
|
|
109
|
-
|
|
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
|
|
4
|
-
import { VISUAL_LIMITS, parseVisualEvent, parseVisualHandoff,
|
|
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
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
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
|
-
|
|
592
|
+
await lstat(paths.journal);
|
|
593
|
+
return true;
|
|
573
594
|
}
|
|
574
595
|
catch {
|
|
575
|
-
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
661
|
-
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);
|
|
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
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
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
|
-
|
|
748
|
-
|
|
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 {
|
|
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
|
-
/**
|
|
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
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
package/dist/apply-command.d.ts
CHANGED
|
@@ -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;
|