yarramate 0.16.0 → 0.18.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 +2 -1
- package/dist/adapters/graphify-cli.js +0 -0
- package/dist/adapters/likec4-cli.js +0 -0
- package/dist/adapters/mcp-cli.js +0 -0
- package/dist/adapters/visual/client.d.ts +74 -0
- package/dist/adapters/visual/client.js +448 -0
- package/dist/adapters/visual/likec4-compiler.d.ts +55 -0
- package/dist/adapters/visual/likec4-compiler.js +460 -0
- package/dist/adapters/visual/protocol-contract.d.ts +246 -0
- package/dist/adapters/visual/protocol-contract.js +19 -0
- package/dist/adapters/visual/protocol.d.ts +13 -0
- package/dist/adapters/visual/protocol.js +258 -0
- package/dist/adapters/visual/session-server.d.ts +130 -0
- package/dist/adapters/visual/session-server.js +1406 -0
- package/dist/adapters/visual/session-store.d.ts +126 -0
- package/dist/adapters/visual/session-store.js +755 -0
- package/dist/adapters/visual/wire.d.ts +93 -0
- package/dist/adapters/visual/wire.js +1 -0
- package/dist/adapters/visual-cli.d.ts +42 -0
- package/dist/adapters/visual-cli.js +274 -0
- package/dist/attestation-staleness.js +8 -1
- package/dist/cli.js +0 -0
- package/dist/compiler.d.ts +12 -0
- package/dist/compiler.js +46 -2
- package/dist/reconciliation.d.ts +17 -1
- package/dist/reconciliation.js +48 -1
- package/dist/rtm.d.ts +1 -0
- package/dist/rtm.js +9 -7
- package/dist/visual-app/assets/index-CGvC8e3R.css +1 -0
- package/dist/visual-app/assets/index-CXuW1tOj.js +209 -0
- package/dist/visual-app/index.html +18 -0
- package/docs/CONSUMING-YARRAMATE.md +49 -0
- package/package.json +25 -5
- package/schema/yarramate-ask-result.schema.json +4 -0
- package/schema/yarramate-document.schema.json +5 -0
- package/schema/yarramate-operations.schema.json +7 -1
- package/schema/yarramate-reconciliation-report.schema.json +36 -1
- package/schema/yarramate-visual-diagnostic-result.schema.json +67 -0
- package/schema/yarramate-visual-event.schema.json +255 -0
- package/schema/yarramate-visual-handoff.schema.json +120 -0
- package/schema/yarramate-visual-model.schema.json +111 -0
- package/schema/yarramate-visual-response.schema.json +230 -0
- package/schema/yarramate-visual-session-descriptor.schema.json +45 -0
- package/schema/yarramate-visual-session-request.schema.json +110 -0
- package/schema/yarramate-visual-session-started.schema.json +121 -0
- package/schema/yarramate-visual-status.schema.json +123 -0
- package/skills/yarramate-architecture/SKILL.md +28 -1
- package/skills/yarramate-architecture/references/native-authoring.md +15 -3
- package/skills/yarramate-architecture/references/visual-conversations.md +263 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { VISUAL_PROTOCOL_VERSION, VisualAuthority, VisualCapabilities, VisualChoicePresentPayload, VisualDiagnostic, VisualFreezeReason, VisualResponse, VisualTerminationReason } from './protocol-contract.js';
|
|
2
|
+
/**
|
|
3
|
+
* Transport shapes the session server and the browser application both speak.
|
|
4
|
+
*
|
|
5
|
+
* These are not protocol documents: they carry validated documents plus the
|
|
6
|
+
* transport's own acknowledgements, so they are discriminated by `kind` rather
|
|
7
|
+
* than by a versioned `format`. They live apart from the server because the
|
|
8
|
+
* browser bundle must import the contract without importing the Node runtime
|
|
9
|
+
* that serves it — this module holds types only, and every dependency it has on
|
|
10
|
+
* the protocol is type-only too.
|
|
11
|
+
*/
|
|
12
|
+
/** One promoted candidate as the browser receives it. */
|
|
13
|
+
export interface VisualRenderedModel {
|
|
14
|
+
readonly candidate: string;
|
|
15
|
+
readonly authority: VisualAuthority;
|
|
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;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One line of the conversation, as plain text.
|
|
27
|
+
*
|
|
28
|
+
* The server keeps this beside the journal so a browser that reloads or
|
|
29
|
+
* reconnects sees the conversation it left. It carries no model source, no
|
|
30
|
+
* credential, and none of the journal's own bookkeeping — only what was said
|
|
31
|
+
* and who said it.
|
|
32
|
+
*/
|
|
33
|
+
export interface VisualTranscriptRecord {
|
|
34
|
+
readonly id: string;
|
|
35
|
+
readonly speaker: 'reviewer' | 'agent';
|
|
36
|
+
readonly text: string;
|
|
37
|
+
}
|
|
38
|
+
/** Everything the browser needs to render, and nothing that authenticates it. */
|
|
39
|
+
export interface VisualSessionSnapshot {
|
|
40
|
+
readonly protocolVersion: typeof VISUAL_PROTOCOL_VERSION;
|
|
41
|
+
readonly sessionId: string;
|
|
42
|
+
readonly authority: VisualAuthority;
|
|
43
|
+
readonly title: string;
|
|
44
|
+
readonly description: string;
|
|
45
|
+
readonly chatEnabled: boolean;
|
|
46
|
+
readonly capabilities: VisualCapabilities;
|
|
47
|
+
readonly webSocketUrl: string;
|
|
48
|
+
readonly model: VisualRenderedModel;
|
|
49
|
+
readonly transcript: readonly VisualTranscriptRecord[];
|
|
50
|
+
/**
|
|
51
|
+
* Whether the agent still owes an answer to something the reviewer sent. A
|
|
52
|
+
* browser that reconnects mid-turn reads this rather than inferring the turn
|
|
53
|
+
* from the transcript, so a reply it never saw does not leave it waiting.
|
|
54
|
+
*/
|
|
55
|
+
readonly agentTurnOpen: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* The structured choice the agent is still waiting on, or `null`. The
|
|
58
|
+
* question lives in the agent's response and never in the transcript, so a
|
|
59
|
+
* browser that reloads or reconnects reads it here; without it the reviewer
|
|
60
|
+
* comes back to a session waiting on a selection they can no longer make.
|
|
61
|
+
*/
|
|
62
|
+
readonly pendingChoice: VisualChoicePresentPayload | null;
|
|
63
|
+
/**
|
|
64
|
+
* Nonce this session's policy admits for the inline styles the diagram
|
|
65
|
+
* renderer injects. It authorises styling and nothing else, and it is not a
|
|
66
|
+
* credential: the session cookie is what authenticates this snapshot.
|
|
67
|
+
*/
|
|
68
|
+
readonly styleNonce: string;
|
|
69
|
+
readonly lastSequence: number;
|
|
70
|
+
readonly frozen: boolean;
|
|
71
|
+
}
|
|
72
|
+
/** Server-to-browser transport frame. */
|
|
73
|
+
export type VisualServerFrame = {
|
|
74
|
+
readonly kind: 'ready';
|
|
75
|
+
readonly snapshot: VisualSessionSnapshot;
|
|
76
|
+
} | {
|
|
77
|
+
readonly kind: 'accepted';
|
|
78
|
+
readonly sequence: number;
|
|
79
|
+
readonly eventId: string;
|
|
80
|
+
} | {
|
|
81
|
+
readonly kind: 'rejected';
|
|
82
|
+
readonly diagnostics: readonly VisualDiagnostic[];
|
|
83
|
+
readonly frozen?: VisualFreezeReason;
|
|
84
|
+
} | {
|
|
85
|
+
readonly kind: 'response';
|
|
86
|
+
readonly response: VisualResponse;
|
|
87
|
+
} | {
|
|
88
|
+
readonly kind: 'model';
|
|
89
|
+
readonly model: VisualRenderedModel;
|
|
90
|
+
} | {
|
|
91
|
+
readonly kind: 'closing';
|
|
92
|
+
readonly reason: VisualTerminationReason;
|
|
93
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { type CliResult } from '../cli-support.js';
|
|
3
|
+
export declare const visualUsage: string;
|
|
4
|
+
/**
|
|
5
|
+
* Where a repository keeps its live sessions, one directory per session. It sits
|
|
6
|
+
* under the ignored build directory because a session is runtime state: never
|
|
7
|
+
* canonical, never committed, and pruned by the next start.
|
|
8
|
+
*/
|
|
9
|
+
export declare const VISUAL_SESSION_DIRECTORY: string;
|
|
10
|
+
/**
|
|
11
|
+
* Every command an agent can run to completion. `start` is not one of them: it
|
|
12
|
+
* blocks for the life of the session and is dispatched by `runVisualCli`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function runVisualClientCli(args: readonly string[], cwd?: string): Promise<CliResult>;
|
|
15
|
+
/** Where a foreground session publishes, and where it observes its signals. */
|
|
16
|
+
export interface VisualSignalSource {
|
|
17
|
+
on(signal: 'SIGINT' | 'SIGTERM', listener: () => void): unknown;
|
|
18
|
+
off(signal: 'SIGINT' | 'SIGTERM', listener: () => void): unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface VisualStartIo {
|
|
21
|
+
readonly stdout: (chunk: string) => void;
|
|
22
|
+
readonly stderr: (chunk: string) => void;
|
|
23
|
+
/** Defaults to the process itself. */
|
|
24
|
+
readonly signals?: VisualSignalSource;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Serves one visual session in the foreground.
|
|
28
|
+
*
|
|
29
|
+
* The request is validated before any filesystem or network effect, so a bad
|
|
30
|
+
* document costs nothing. Exactly one public `visual-session-started/v1` line is
|
|
31
|
+
* published before the command blocks — it names the private descriptor but
|
|
32
|
+
* never carries the agent capability, which lives only in that mode 0600 file.
|
|
33
|
+
* The command then holds the session open until the runtime closes it, whether
|
|
34
|
+
* that is a signal, an agent stop, or a runtime failure; the runtime recovers
|
|
35
|
+
* the handoff before it removes the session in every one of those cases.
|
|
36
|
+
*/
|
|
37
|
+
export declare const runVisualStart: (requestPath: string, cwd?: string, io?: VisualStartIo) => Promise<CliResult>;
|
|
38
|
+
/**
|
|
39
|
+
* The whole binary: `start` blocks and streams its one document through `io`,
|
|
40
|
+
* every other command runs to completion and returns its document.
|
|
41
|
+
*/
|
|
42
|
+
export declare const runVisualCli: (args: readonly string[], cwd?: string, io?: VisualStartIo) => Promise<CliResult>;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { isMainModule, versionResult } from '../cli-support.js';
|
|
4
|
+
import { fetchVisualStatus, readVisualJsonDocument, readVisualSessionDescriptor, recoverVisualSessionClient, sendVisualResponse, stopVisualSessionClient, visualClientDiagnostic, visualFailureDiagnostics, visualSessionAlreadyStopped, waitForVisualEvent, } from './visual/client.js';
|
|
5
|
+
import { parseVisualResponse, parseVisualSessionRequest, } from './visual/protocol.js';
|
|
6
|
+
import { startVisualServer } from './visual/session-server.js';
|
|
7
|
+
import { pruneStaleVisualSessions } from './visual/session-store.js';
|
|
8
|
+
export const visualUsage = 'Usage:\n' +
|
|
9
|
+
' yarramate-visual start <request.json>\n' +
|
|
10
|
+
' yarramate-visual wait <descriptor.json> [--after <sequence>]\n' +
|
|
11
|
+
' yarramate-visual respond <descriptor.json> <response.json>\n' +
|
|
12
|
+
' yarramate-visual status <descriptor.json>\n' +
|
|
13
|
+
' yarramate-visual recover <descriptor.json> [--transcript]\n' +
|
|
14
|
+
' yarramate-visual stop <descriptor.json> [--transcript]\n' +
|
|
15
|
+
' yarramate-visual --version\n';
|
|
16
|
+
/**
|
|
17
|
+
* Where a repository keeps its live sessions, one directory per session. It sits
|
|
18
|
+
* under the ignored build directory because a session is runtime state: never
|
|
19
|
+
* canonical, never committed, and pruned by the next start.
|
|
20
|
+
*/
|
|
21
|
+
export const VISUAL_SESSION_DIRECTORY = join('.yarramate-out', 'visual');
|
|
22
|
+
/** Terminations that mean the session did not end the way it was asked to. */
|
|
23
|
+
const FAILED_TERMINATION = {
|
|
24
|
+
'child-failed': true,
|
|
25
|
+
'browser-timeout': true,
|
|
26
|
+
'server-failed': true,
|
|
27
|
+
'compiler-failed': true,
|
|
28
|
+
};
|
|
29
|
+
const usageResult = {
|
|
30
|
+
exitCode: 2,
|
|
31
|
+
stdout: '',
|
|
32
|
+
stderr: visualUsage,
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* One document per line: every command an agent drives writes at most one
|
|
36
|
+
* versioned document, so a caller reads exactly one line per invocation.
|
|
37
|
+
*/
|
|
38
|
+
const documentLine = (value) => `${JSON.stringify(value)}\n`;
|
|
39
|
+
/**
|
|
40
|
+
* Stdout carries the document that was asked for and nothing else, so a refusal
|
|
41
|
+
* goes to stderr where it cannot be mistaken for an answer.
|
|
42
|
+
*/
|
|
43
|
+
const refusalResult = (diagnostics) => ({
|
|
44
|
+
exitCode: 1,
|
|
45
|
+
stdout: '',
|
|
46
|
+
stderr: documentLine({
|
|
47
|
+
format: 'yarramate/visual-diagnostic-result/v1',
|
|
48
|
+
diagnostics,
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
/** Sequences are counted, so only a plain non-negative integer is one. */
|
|
52
|
+
const sequenceFlag = (rest) => {
|
|
53
|
+
if (rest.length === 0)
|
|
54
|
+
return 0;
|
|
55
|
+
const [flag, value] = rest;
|
|
56
|
+
if (flag !== '--after' || value === undefined || rest.length !== 2) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
if (!/^(?:0|[1-9][0-9]*)$/.test(value))
|
|
60
|
+
return undefined;
|
|
61
|
+
const sequence = Number(value);
|
|
62
|
+
return Number.isSafeInteger(sequence) ? sequence : undefined;
|
|
63
|
+
};
|
|
64
|
+
const transcriptFlag = (rest) => rest.length === 0
|
|
65
|
+
? false
|
|
66
|
+
: rest.length === 1 && rest[0] === '--transcript'
|
|
67
|
+
? true
|
|
68
|
+
: undefined;
|
|
69
|
+
const waitCliCommand = async (descriptorPath, rest, cwd) => {
|
|
70
|
+
const after = sequenceFlag(rest);
|
|
71
|
+
if (after === undefined)
|
|
72
|
+
return usageResult;
|
|
73
|
+
const descriptor = await readVisualSessionDescriptor(descriptorPath, cwd);
|
|
74
|
+
if (!descriptor.ok)
|
|
75
|
+
return refusalResult(descriptor.diagnostics);
|
|
76
|
+
const delivery = await waitForVisualEvent(descriptor.value, after);
|
|
77
|
+
if (!delivery.ok)
|
|
78
|
+
return refusalResult(delivery.diagnostics);
|
|
79
|
+
return {
|
|
80
|
+
exitCode: 0,
|
|
81
|
+
// An idle window is not a failure and has no document: the caller polls
|
|
82
|
+
// again from the sequence it already knows.
|
|
83
|
+
stdout: delivery.value.waiting ? '' : documentLine(delivery.value.event),
|
|
84
|
+
stderr: '',
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
const respondCliCommand = async (descriptorPath, rest, cwd) => {
|
|
88
|
+
const [responsePath, ...trailing] = rest;
|
|
89
|
+
if (responsePath === undefined || trailing.length > 0)
|
|
90
|
+
return usageResult;
|
|
91
|
+
const descriptor = await readVisualSessionDescriptor(descriptorPath, cwd);
|
|
92
|
+
if (!descriptor.ok)
|
|
93
|
+
return refusalResult(descriptor.diagnostics);
|
|
94
|
+
const document = await readVisualJsonDocument(responsePath, cwd);
|
|
95
|
+
if (!document.ok)
|
|
96
|
+
return refusalResult(document.diagnostics);
|
|
97
|
+
// Validated here as well as by the runtime, so an invalid response never
|
|
98
|
+
// reaches the session at all.
|
|
99
|
+
const response = parseVisualResponse(document.value);
|
|
100
|
+
if (!response.ok)
|
|
101
|
+
return refusalResult(response.diagnostics);
|
|
102
|
+
const accepted = await sendVisualResponse(descriptor.value, response.value);
|
|
103
|
+
if (!accepted.ok)
|
|
104
|
+
return refusalResult(accepted.diagnostics);
|
|
105
|
+
return { exitCode: 0, stdout: documentLine(accepted.value), stderr: '' };
|
|
106
|
+
};
|
|
107
|
+
const statusCliCommand = async (descriptorPath, rest, cwd) => {
|
|
108
|
+
if (rest.length > 0)
|
|
109
|
+
return usageResult;
|
|
110
|
+
const descriptor = await readVisualSessionDescriptor(descriptorPath, cwd);
|
|
111
|
+
if (!descriptor.ok)
|
|
112
|
+
return refusalResult(descriptor.diagnostics);
|
|
113
|
+
const status = await fetchVisualStatus(descriptor.value);
|
|
114
|
+
if (!status.ok)
|
|
115
|
+
return refusalResult(status.diagnostics);
|
|
116
|
+
return { exitCode: 0, stdout: documentLine(status.value), stderr: '' };
|
|
117
|
+
};
|
|
118
|
+
const recoverCliCommand = async (descriptorPath, rest, cwd) => {
|
|
119
|
+
const includeTranscript = transcriptFlag(rest);
|
|
120
|
+
if (includeTranscript === undefined)
|
|
121
|
+
return usageResult;
|
|
122
|
+
const descriptor = await readVisualSessionDescriptor(descriptorPath, cwd);
|
|
123
|
+
if (!descriptor.ok)
|
|
124
|
+
return refusalResult(descriptor.diagnostics);
|
|
125
|
+
const handoff = await recoverVisualSessionClient(descriptor.value, includeTranscript);
|
|
126
|
+
if (!handoff.ok)
|
|
127
|
+
return refusalResult(handoff.diagnostics);
|
|
128
|
+
return { exitCode: 0, stdout: documentLine(handoff.value), stderr: '' };
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Converges the session on stopped, and says so however many times it is asked.
|
|
132
|
+
* The design's failure table requires a repeated stop to report the
|
|
133
|
+
* already-stopped state, and the first stop takes the descriptor with the
|
|
134
|
+
* session root, so the second invocation has nothing left to read. That one
|
|
135
|
+
* shape — descriptor and session root both gone — reports the same benign
|
|
136
|
+
* already-stopped result the client returns for a retained descriptor: exit 0,
|
|
137
|
+
* no handoff document, because there is no session left to hand off.
|
|
138
|
+
*
|
|
139
|
+
* Every other unreadable descriptor still refuses. Nothing weakens: no
|
|
140
|
+
* credential is retained, no directory is removed on the strength of a
|
|
141
|
+
* descriptor that did not parse, and a corrupt, redirected, or foreign
|
|
142
|
+
* descriptor fails exactly as before.
|
|
143
|
+
*/
|
|
144
|
+
const stopCliCommand = async (descriptorPath, rest, cwd) => {
|
|
145
|
+
const includeTranscript = transcriptFlag(rest);
|
|
146
|
+
if (includeTranscript === undefined)
|
|
147
|
+
return usageResult;
|
|
148
|
+
const descriptor = await readVisualSessionDescriptor(descriptorPath, cwd);
|
|
149
|
+
if (!descriptor.ok) {
|
|
150
|
+
return (await visualSessionAlreadyStopped(descriptorPath, cwd))
|
|
151
|
+
? { exitCode: 0, stdout: '', stderr: '' }
|
|
152
|
+
: refusalResult(descriptor.diagnostics);
|
|
153
|
+
}
|
|
154
|
+
const stopped = await stopVisualSessionClient(descriptor.value, includeTranscript);
|
|
155
|
+
if (!stopped.ok)
|
|
156
|
+
return refusalResult(stopped.diagnostics);
|
|
157
|
+
return {
|
|
158
|
+
exitCode: 0,
|
|
159
|
+
// A session that was already gone has no handoff left to publish.
|
|
160
|
+
stdout: stopped.value === undefined ? '' : documentLine(stopped.value),
|
|
161
|
+
stderr: '',
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Every command an agent can run to completion. `start` is not one of them: it
|
|
166
|
+
* blocks for the life of the session and is dispatched by `runVisualCli`.
|
|
167
|
+
*/
|
|
168
|
+
export async function runVisualClientCli(args, cwd = process.cwd()) {
|
|
169
|
+
const [command, descriptor, ...rest] = args;
|
|
170
|
+
if (command === '--version')
|
|
171
|
+
return versionResult('yarramate-visual');
|
|
172
|
+
if (descriptor === undefined)
|
|
173
|
+
return usageResult;
|
|
174
|
+
switch (command) {
|
|
175
|
+
case 'wait':
|
|
176
|
+
return waitCliCommand(descriptor, rest, cwd);
|
|
177
|
+
case 'respond':
|
|
178
|
+
return respondCliCommand(descriptor, rest, cwd);
|
|
179
|
+
case 'status':
|
|
180
|
+
return statusCliCommand(descriptor, rest, cwd);
|
|
181
|
+
case 'recover':
|
|
182
|
+
return recoverCliCommand(descriptor, rest, cwd);
|
|
183
|
+
case 'stop':
|
|
184
|
+
return stopCliCommand(descriptor, rest, cwd);
|
|
185
|
+
default:
|
|
186
|
+
return usageResult;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const processIo = {
|
|
190
|
+
stdout: (chunk) => void process.stdout.write(chunk),
|
|
191
|
+
stderr: (chunk) => void process.stderr.write(chunk),
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* Serves one visual session in the foreground.
|
|
195
|
+
*
|
|
196
|
+
* The request is validated before any filesystem or network effect, so a bad
|
|
197
|
+
* document costs nothing. Exactly one public `visual-session-started/v1` line is
|
|
198
|
+
* published before the command blocks — it names the private descriptor but
|
|
199
|
+
* never carries the agent capability, which lives only in that mode 0600 file.
|
|
200
|
+
* The command then holds the session open until the runtime closes it, whether
|
|
201
|
+
* that is a signal, an agent stop, or a runtime failure; the runtime recovers
|
|
202
|
+
* the handoff before it removes the session in every one of those cases.
|
|
203
|
+
*/
|
|
204
|
+
export const runVisualStart = async (requestPath, cwd = process.cwd(), io = processIo) => {
|
|
205
|
+
const document = await readVisualJsonDocument(requestPath, cwd);
|
|
206
|
+
if (!document.ok)
|
|
207
|
+
return refusalResult(document.diagnostics);
|
|
208
|
+
const request = parseVisualSessionRequest(document.value);
|
|
209
|
+
if (!request.ok)
|
|
210
|
+
return refusalResult(request.diagnostics);
|
|
211
|
+
const baseDir = join(cwd, VISUAL_SESSION_DIRECTORY);
|
|
212
|
+
const signals = io.signals ?? process;
|
|
213
|
+
let handle;
|
|
214
|
+
try {
|
|
215
|
+
// A previous runtime's orphans are collected on the critical path of a new
|
|
216
|
+
// session, bounded per pass so a full directory cannot stall this one.
|
|
217
|
+
await pruneStaleVisualSessions(baseDir, new Date());
|
|
218
|
+
handle = await startVisualServer({ request: request.value, baseDir });
|
|
219
|
+
}
|
|
220
|
+
catch (cause) {
|
|
221
|
+
// A failed start has already recovered and removed whatever it created.
|
|
222
|
+
return refusalResult(visualFailureDiagnostics(cause));
|
|
223
|
+
}
|
|
224
|
+
io.stdout(documentLine(handle.started));
|
|
225
|
+
// One handler for both signals: `stop` converges on the first reason it is
|
|
226
|
+
// given, so a repeated or second signal is absorbed rather than racing.
|
|
227
|
+
//
|
|
228
|
+
// A signal is not a caller, and nothing here awaits the teardown it starts:
|
|
229
|
+
// a stop that fails is observed rather than left to reach the runtime as an
|
|
230
|
+
// unhandled rejection and take the whole session down with it. What it could
|
|
231
|
+
// not tear down is still up, this command is still blocked on `closed`, and
|
|
232
|
+
// the next signal runs the teardown again.
|
|
233
|
+
const interrupt = () => {
|
|
234
|
+
void handle.stop('main-cancelled').catch(() => undefined);
|
|
235
|
+
};
|
|
236
|
+
signals.on('SIGINT', interrupt);
|
|
237
|
+
signals.on('SIGTERM', interrupt);
|
|
238
|
+
try {
|
|
239
|
+
const closed = await handle.closed;
|
|
240
|
+
// The recovered handoff is what the journal says happened; the reason the
|
|
241
|
+
// stop asked for is only what someone wanted. A session the reviewer ended
|
|
242
|
+
// is a success even when a cancelling agent is what closed the server, and
|
|
243
|
+
// a child that died before its handoff is a failure even when it did not.
|
|
244
|
+
const outcome = closed.handoff?.terminationReason ?? closed.reason;
|
|
245
|
+
return FAILED_TERMINATION[outcome] === true
|
|
246
|
+
? refusalResult([
|
|
247
|
+
visualClientDiagnostic('YMVS409', `Visual session ${handle.started.sessionId} ended with termination reason "${outcome}"`),
|
|
248
|
+
])
|
|
249
|
+
: { exitCode: 0, stdout: '', stderr: '' };
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
signals.off('SIGINT', interrupt);
|
|
253
|
+
signals.off('SIGTERM', interrupt);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
/**
|
|
257
|
+
* The whole binary: `start` blocks and streams its one document through `io`,
|
|
258
|
+
* every other command runs to completion and returns its document.
|
|
259
|
+
*/
|
|
260
|
+
export const runVisualCli = async (args, cwd = process.cwd(), io = processIo) => {
|
|
261
|
+
if (args[0] !== 'start')
|
|
262
|
+
return runVisualClientCli(args, cwd);
|
|
263
|
+
const requestPath = args[1];
|
|
264
|
+
if (requestPath === undefined || args.length !== 2)
|
|
265
|
+
return usageResult;
|
|
266
|
+
return runVisualStart(requestPath, cwd, io);
|
|
267
|
+
};
|
|
268
|
+
if (isMainModule(import.meta.url, process.argv[1])) {
|
|
269
|
+
void runVisualCli(process.argv.slice(2)).then((result) => {
|
|
270
|
+
process.stdout.write(result.stdout);
|
|
271
|
+
process.stderr.write(result.stderr);
|
|
272
|
+
process.exitCode = result.exitCode;
|
|
273
|
+
});
|
|
274
|
+
}
|
|
@@ -49,7 +49,14 @@ const attestedConcepts = (source, documentId) => {
|
|
|
49
49
|
}
|
|
50
50
|
const { topic, by, on } = record;
|
|
51
51
|
if (topic !== undefined && by !== undefined && on !== undefined) {
|
|
52
|
-
|
|
52
|
+
// The authority is a reference; report it in the same qualified form
|
|
53
|
+
// the compiler resolves, so one sign-off reads identically in a
|
|
54
|
+
// staleness finding, a reconcile finding, and the RTM.
|
|
55
|
+
attestations.push({
|
|
56
|
+
topic,
|
|
57
|
+
by: by.includes('#') ? by : `${documentId}#${by}`,
|
|
58
|
+
on,
|
|
59
|
+
});
|
|
53
60
|
}
|
|
54
61
|
}
|
|
55
62
|
if (attestations.length === 0)
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/compiler.d.ts
CHANGED
|
@@ -63,5 +63,17 @@ export type ContextualCompilationResult = {
|
|
|
63
63
|
readonly ok: false;
|
|
64
64
|
readonly diagnostics: readonly Diagnostic[];
|
|
65
65
|
};
|
|
66
|
+
export declare const ATTESTATION_PREDICATE_PREFIX = "yarramate/attestation/";
|
|
67
|
+
export declare const attestationClaimValue: (attestation: {
|
|
68
|
+
readonly by: string;
|
|
69
|
+
readonly on: string;
|
|
70
|
+
readonly recordedBy?: string;
|
|
71
|
+
}) => string;
|
|
72
|
+
export interface AttestationClaimParts {
|
|
73
|
+
readonly by: string;
|
|
74
|
+
readonly on: string;
|
|
75
|
+
readonly recordedBy?: string;
|
|
76
|
+
}
|
|
77
|
+
export declare const parseAttestationClaimValue: (value: string) => AttestationClaimParts | undefined;
|
|
66
78
|
export declare function compileWorkspace(sources: readonly WorkspaceSource[]): CompilationResult;
|
|
67
79
|
export declare const compileWorkspaceWithProfileContext: (sources: readonly WorkspaceSource[]) => ContextualCompilationResult;
|
package/dist/compiler.js
CHANGED
|
@@ -37,6 +37,25 @@ const presenceClaimId = (subject, state) => `${subject}~present-in-${Buffer.from
|
|
|
37
37
|
const aliasClaimId = (subject, alias) => `${subject}~alias-${Buffer.from(alias, 'utf8').toString('hex')}`;
|
|
38
38
|
const distinctFromClaimId = (subject, other) => `${subject}~distinct-from-${Buffer.from(other, 'utf8').toString('hex')}`;
|
|
39
39
|
const supersedesClaimId = (subject, predecessor) => `${subject}~supersedes-${Buffer.from(predecessor, 'utf8').toString('hex')}`;
|
|
40
|
+
export const ATTESTATION_PREDICATE_PREFIX = 'yarramate/attestation/';
|
|
41
|
+
// An attestation claim packs the authority, the date it was given, and
|
|
42
|
+
// the recorder when a machine held the pen. A reference carries no
|
|
43
|
+
// spaces and the date is fixed width, so the three parse back out of one
|
|
44
|
+
// value unambiguously wherever a reader needs them.
|
|
45
|
+
export const attestationClaimValue = (attestation) => attestation.recordedBy === undefined
|
|
46
|
+
? `${attestation.by} ${attestation.on}`
|
|
47
|
+
: `${attestation.by} ${attestation.on} ${attestation.recordedBy}`;
|
|
48
|
+
export const parseAttestationClaimValue = (value) => {
|
|
49
|
+
const match = /^(\S+) ([0-9]{4}-[0-9]{2}-[0-9]{2})(?: (.+))?$/.exec(value);
|
|
50
|
+
if (match === null)
|
|
51
|
+
return undefined;
|
|
52
|
+
const recordedBy = match[3];
|
|
53
|
+
return {
|
|
54
|
+
by: match[1],
|
|
55
|
+
on: match[2],
|
|
56
|
+
...(recordedBy === undefined ? {} : { recordedBy }),
|
|
57
|
+
};
|
|
58
|
+
};
|
|
40
59
|
const describeAspect = (aspect) => aspect.replace('-', ' ');
|
|
41
60
|
// Candidate order is the policy-matrix declaration order: the resolved kind
|
|
42
61
|
// map inserts core policies first, then extension kinds as declared.
|
|
@@ -1128,11 +1147,36 @@ function compileWorkspaceResolved(sources) {
|
|
|
1128
1147
|
// evaluates: the claim's existence is what triggers can see, and
|
|
1129
1148
|
// revocation is deletion, reviewed at the Git boundary.
|
|
1130
1149
|
for (const [attestationIndex, attestation] of (concept.attestations ?? []).entries()) {
|
|
1150
|
+
// The authority is held to the same rule as ownership: a judgment
|
|
1151
|
+
// is worthless if nobody in the model made it, and a name only the
|
|
1152
|
+
// signer knows cannot be checked by the reviewer reading the diff.
|
|
1153
|
+
const authority = qualifyReference(value.id, attestation.by);
|
|
1154
|
+
if (!conceptByQualifiedId.has(authority)) {
|
|
1155
|
+
const pointer = `/concepts/${index}/attestations/${attestationIndex}/by`;
|
|
1156
|
+
const source = location(['concepts', index, 'attestations', attestationIndex, 'by'], pointer);
|
|
1157
|
+
diagnostics.push({
|
|
1158
|
+
severity: 'error',
|
|
1159
|
+
code: 'YM304',
|
|
1160
|
+
message: `Unresolved attestation authority reference "${attestation.by}"`,
|
|
1161
|
+
path: input.path,
|
|
1162
|
+
pointer,
|
|
1163
|
+
line: source.line,
|
|
1164
|
+
column: source.column,
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1131
1167
|
claims.push({
|
|
1132
1168
|
id: `${subject}~attestation-${attestation.topic}`,
|
|
1133
1169
|
subject,
|
|
1134
|
-
predicate:
|
|
1135
|
-
object: {
|
|
1170
|
+
predicate: `${ATTESTATION_PREDICATE_PREFIX}${attestation.topic}`,
|
|
1171
|
+
object: {
|
|
1172
|
+
value: attestationClaimValue({
|
|
1173
|
+
by: authority,
|
|
1174
|
+
on: attestation.on,
|
|
1175
|
+
...(attestation.recordedBy === undefined
|
|
1176
|
+
? {}
|
|
1177
|
+
: { recordedBy: attestation.recordedBy }),
|
|
1178
|
+
}),
|
|
1179
|
+
},
|
|
1136
1180
|
origin: 'declared',
|
|
1137
1181
|
source: location(['concepts', index, 'attestations', attestationIndex, 'topic'], `/concepts/${index}/attestations/${attestationIndex}/topic`),
|
|
1138
1182
|
});
|
package/dist/reconciliation.d.ts
CHANGED
|
@@ -55,7 +55,22 @@ export interface StaleAttestationFinding {
|
|
|
55
55
|
readonly changedAt?: string;
|
|
56
56
|
readonly evidence: EvidenceLocator;
|
|
57
57
|
}
|
|
58
|
-
export
|
|
58
|
+
export interface UnconfirmedAttestationFinding {
|
|
59
|
+
readonly target: {
|
|
60
|
+
readonly type: 'subject';
|
|
61
|
+
readonly id: string;
|
|
62
|
+
};
|
|
63
|
+
readonly result: 'unconfirmed-attestation';
|
|
64
|
+
readonly attestation: {
|
|
65
|
+
readonly topic: string;
|
|
66
|
+
readonly by: string;
|
|
67
|
+
readonly recordedBy: string;
|
|
68
|
+
readonly on: string;
|
|
69
|
+
};
|
|
70
|
+
readonly provider: 'model';
|
|
71
|
+
readonly declared: DeclaredSource;
|
|
72
|
+
}
|
|
73
|
+
export type ReconciliationFinding = EvidenceFinding | StaleAttestationFinding | UnconfirmedAttestationFinding;
|
|
59
74
|
export interface AttestationStaleness {
|
|
60
75
|
readonly findings: readonly StaleAttestationFinding[];
|
|
61
76
|
readonly notes: readonly string[];
|
|
@@ -73,6 +88,7 @@ export interface ReconciliationReport {
|
|
|
73
88
|
readonly notObserved: number;
|
|
74
89
|
readonly subjectsWithoutEvidence: number;
|
|
75
90
|
readonly staleAttestations?: number;
|
|
91
|
+
readonly unconfirmedAttestations?: number;
|
|
76
92
|
readonly expectationsCompared: number;
|
|
77
93
|
readonly expectationsWithoutObservation: number;
|
|
78
94
|
};
|
package/dist/reconciliation.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ATTESTATION_PREDICATE_PREFIX, parseAttestationClaimValue, } from './compiler.js';
|
|
1
2
|
const assertedRelationshipsByClaim = (graph) => {
|
|
2
3
|
const asserted = new Map();
|
|
3
4
|
if (graph === undefined)
|
|
@@ -147,10 +148,48 @@ const unobservedCurrentConcepts = (graph, reports) => {
|
|
|
147
148
|
.map(({ subject }) => subject)
|
|
148
149
|
.sort((left, right) => left.localeCompare(right));
|
|
149
150
|
};
|
|
151
|
+
// A judgment a machine transcribed is not the act the authority
|
|
152
|
+
// performed. The recorder is in the model, so the difference is
|
|
153
|
+
// derivable here: an authority who wrote the record in their own hand
|
|
154
|
+
// names nobody else, and anything else is a claim awaiting confirmation.
|
|
155
|
+
const unconfirmedAttestations = (graph) => {
|
|
156
|
+
if (graph === undefined)
|
|
157
|
+
return [];
|
|
158
|
+
return graph.claims.flatMap((claim) => {
|
|
159
|
+
if (!claim.predicate.startsWith(ATTESTATION_PREDICATE_PREFIX))
|
|
160
|
+
return [];
|
|
161
|
+
if (!('value' in claim.object))
|
|
162
|
+
return [];
|
|
163
|
+
const parts = parseAttestationClaimValue(claim.object.value);
|
|
164
|
+
const recordedBy = parts?.recordedBy;
|
|
165
|
+
if (parts === undefined || recordedBy === undefined)
|
|
166
|
+
return [];
|
|
167
|
+
// The authority is qualified; a recorder naming the same subject,
|
|
168
|
+
// long form or short, is that authority signing for themselves.
|
|
169
|
+
const local = parts.by.slice(parts.by.indexOf('#') + 1);
|
|
170
|
+
if (recordedBy === parts.by || recordedBy === local)
|
|
171
|
+
return [];
|
|
172
|
+
return [
|
|
173
|
+
{
|
|
174
|
+
target: { type: 'subject', id: claim.subject },
|
|
175
|
+
result: 'unconfirmed-attestation',
|
|
176
|
+
attestation: {
|
|
177
|
+
topic: claim.predicate.slice(ATTESTATION_PREDICATE_PREFIX.length),
|
|
178
|
+
by: parts.by,
|
|
179
|
+
recordedBy,
|
|
180
|
+
on: parts.on,
|
|
181
|
+
},
|
|
182
|
+
provider: 'model',
|
|
183
|
+
declared: claim.source,
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
});
|
|
187
|
+
};
|
|
150
188
|
export function reconcileEvidenceReports(workspace, reports, graph, staleness) {
|
|
151
189
|
const assertedByClaim = assertedRelationshipsByClaim(graph);
|
|
152
190
|
const unobservedSubjects = unobservedCurrentConcepts(graph, reports);
|
|
153
191
|
const expectations = compareExpectations(graph, reports);
|
|
192
|
+
const unconfirmed = unconfirmedAttestations(graph);
|
|
154
193
|
const summary = {
|
|
155
194
|
evidenceDocuments: reports.length,
|
|
156
195
|
observations: 0,
|
|
@@ -167,10 +206,18 @@ export function reconcileEvidenceReports(workspace, reports, graph, staleness) {
|
|
|
167
206
|
...(staleness === undefined
|
|
168
207
|
? {}
|
|
169
208
|
: { staleAttestations: staleness.findings.length }),
|
|
209
|
+
// Recorder disagreement is derived from the model alone, so the
|
|
210
|
+
// counter appears whenever there was a graph to read.
|
|
211
|
+
...(graph === undefined
|
|
212
|
+
? {}
|
|
213
|
+
: { unconfirmedAttestations: unconfirmed.length }),
|
|
170
214
|
expectationsCompared: expectations.compared,
|
|
171
215
|
expectationsWithoutObservation: expectations.unobserved.length,
|
|
172
216
|
};
|
|
173
|
-
const findings = [
|
|
217
|
+
const findings = [
|
|
218
|
+
...(staleness?.findings ?? []),
|
|
219
|
+
...unconfirmed,
|
|
220
|
+
];
|
|
174
221
|
for (const report of reports) {
|
|
175
222
|
summary.observations += report.observations.length;
|
|
176
223
|
for (const observation of report.observations) {
|