@north-light/crouter 0.3.219 → 0.3.221
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/dist/api/client.d.ts +9 -0
- package/dist/api/client.js +10 -0
- package/dist/api/dto/chat-inventory.d.ts +43 -0
- package/dist/api/dto/chat-inventory.js +11 -0
- package/dist/api/dto/profiles.d.ts +19 -5
- package/dist/api/dto/profiles.js +2 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/build-root.d.ts +2 -6
- package/dist/build-root.js +51 -4
- package/dist/builtin-memory/internal/agent-shaping.md +3 -1
- package/dist/builtin-memory/internal/memory-loading.md +4 -0
- package/dist/builtin-memory/plan/roadmap.md +7 -1
- package/dist/builtin-memory/spec/guide.md +7 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +7 -2
- package/dist/clients/attach/__tests__/ref-autocomplete.test.js +1 -1
- package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -1
- package/dist/clients/attach/overlays/file-review.js +2 -2
- package/dist/clients/attach/session/keys.d.ts +1 -1
- package/dist/clients/attach/session/profile-files.js +1 -1
- package/dist/clients/attach/viewer.js +690 -690
- package/dist/clients/inbox/review/launch.d.ts +8 -4
- package/dist/clients/inbox/review/launch.js +55 -5
- package/dist/clients/inbox/review/review-client.d.ts +1 -0
- package/dist/clients/inbox/review/review-client.js +4 -0
- package/dist/clients/inbox/review-adapter.d.ts +1 -8
- package/dist/clients/inbox/review-adapter.js +4 -52
- package/dist/commands/memory/lint.js +2 -1
- package/dist/commands/memory/read.js +1 -0
- package/dist/commands/memory.js +1 -1
- package/dist/commands/pkg/market-manage.js +165 -75
- package/dist/commands/pkg/plugin-inspect.js +19 -2
- package/dist/commands/pkg/plugin-manage.d.ts +8 -3
- package/dist/commands/pkg/plugin-manage.js +72 -24
- package/dist/commands/profile/default.js +6 -10
- package/dist/commands/profile/list.js +5 -3
- package/dist/commands/profile/new.js +21 -8
- package/dist/commands/profile/project.js +25 -19
- package/dist/commands/profile/show.js +3 -3
- package/dist/commands/surface-inbox.js +1 -0
- package/dist/commands/sys/__tests__/migrate.test.js +16 -5
- package/dist/commands/sys/doctor.js +35 -5
- package/dist/commands/sys/migrate.js +38 -19
- package/dist/commands/sys/panels/broker-limits-panel.js +3 -3
- package/dist/commands/sys/setup-core.js +1 -1
- package/dist/commands/sys/sync-project-guidance.js +1 -1
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +1 -1
- package/dist/core/__tests__/fixtures/c5-command-boundary-ext.js +24 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -18
- package/dist/core/__tests__/fixtures/fake-engine.js +8 -1
- package/dist/core/__tests__/helpers/broker-clients.d.ts +1 -0
- package/dist/core/__tests__/helpers/broker-clients.js +1 -0
- package/dist/core/__tests__/inline-memory-refs.test.js +36 -2
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +217 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +37 -1
- package/dist/core/__tests__/serial/broker-sdk-wiring.test.js +102 -2
- package/dist/core/bootstrap.js +6 -0
- package/dist/core/canvas/browse/app.js +5 -2
- package/dist/core/canvas/browse/model.d.ts +25 -15
- package/dist/core/canvas/browse/model.js +86 -65
- package/dist/core/canvas/render-source.d.ts +6 -0
- package/dist/core/canvas/render-source.js +7 -1
- package/dist/core/canvas/render.js +10 -2
- package/dist/core/command-hooks/artifact.d.ts +10 -0
- package/dist/core/command-hooks/artifact.js +129 -0
- package/dist/core/command-hooks/catalog.d.ts +14 -0
- package/dist/core/command-hooks/catalog.js +38 -0
- package/dist/core/command-hooks/compose.d.ts +15 -0
- package/dist/core/command-hooks/compose.js +99 -0
- package/dist/core/command-hooks/discovery.d.ts +87 -0
- package/dist/core/command-hooks/discovery.js +174 -0
- package/dist/core/command-hooks/help.d.ts +5 -0
- package/dist/core/command-hooks/help.js +18 -0
- package/dist/core/command-hooks/index.d.ts +6 -0
- package/dist/core/command-hooks/index.js +6 -0
- package/dist/core/command-hooks/report.d.ts +23 -0
- package/dist/core/command-hooks/report.js +19 -0
- package/dist/core/command-hooks/schema.d.ts +27 -0
- package/dist/core/command-hooks/schema.js +68 -0
- package/dist/core/command-hooks/transport/exec-invoke.d.ts +22 -0
- package/dist/core/command-hooks/transport/exec-invoke.js +274 -0
- package/dist/core/command-plugins/presence.d.ts +2 -0
- package/dist/core/command-plugins/presence.js +17 -0
- package/dist/core/command-plugins/transport/exec-invoke.d.ts +5 -0
- package/dist/core/command-plugins/transport/exec-invoke.js +58 -5
- package/dist/core/command.d.ts +8 -1
- package/dist/core/command.js +12 -10
- package/dist/core/help.d.ts +7 -1
- package/dist/core/io.d.ts +9 -1
- package/dist/core/io.js +44 -2
- package/dist/core/memory/inline-ref-inventory.d.ts +2 -1
- package/dist/core/memory/inline-ref-inventory.js +15 -8
- package/dist/core/memory-resolver.d.ts +13 -1
- package/dist/core/memory-resolver.js +25 -19
- package/dist/core/profiles/manifest.d.ts +13 -2
- package/dist/core/profiles/manifest.js +84 -18
- package/dist/core/profiles/select.js +9 -9
- package/dist/core/render.js +11 -0
- package/dist/core/runtime/advertised-command-invocation.d.ts +20 -0
- package/dist/core/runtime/advertised-command-invocation.js +233 -0
- package/dist/core/runtime/bearings.js +1 -1
- package/dist/core/runtime/broker/client-registry.d.ts +6 -3
- package/dist/core/runtime/broker/client-registry.js +6 -4
- package/dist/core/runtime/broker/event-projection.js +7 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
- package/dist/core/runtime/broker/frame-dispatch.js +16 -10
- package/dist/core/runtime/broker/read-ops.d.ts +4 -0
- package/dist/core/runtime/broker/read-ops.js +6 -2
- package/dist/core/runtime/broker-extension-render.js +1 -1
- package/dist/core/runtime/broker-inventory.d.ts +5 -0
- package/dist/core/runtime/broker-inventory.js +191 -0
- package/dist/core/runtime/broker-protocol.d.ts +13 -2
- package/dist/core/runtime/broker.js +10 -1
- package/dist/core/runtime/command-surface.d.ts +33 -0
- package/dist/core/runtime/command-surface.js +81 -0
- package/dist/core/runtime/node-read.js +5 -0
- package/dist/core/scope.d.ts +26 -1
- package/dist/core/scope.js +52 -12
- package/dist/core/substrate/on-read.d.ts +7 -1
- package/dist/core/substrate/on-read.js +13 -4
- package/dist/core/substrate/render.js +14 -5
- package/dist/core/substrate/schema.d.ts +11 -1
- package/dist/core/substrate/schema.js +11 -2
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +4 -4
- package/dist/daemon/api/handlers/chat-inventory.d.ts +2 -0
- package/dist/daemon/api/handlers/chat-inventory.js +25 -0
- package/dist/daemon/api/handlers/profiles.js +7 -1
- package/dist/daemon/api/map.d.ts +2 -1
- package/dist/daemon/api/map.js +3 -2
- package/dist/daemon/api/server.js +2 -0
- package/dist/hook-authoring.d.ts +75 -0
- package/dist/hook-authoring.js +358 -0
- package/dist/hook-process.d.ts +7 -0
- package/dist/hook-process.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/migrations/002-profile-project-memory.d.ts +2 -0
- package/dist/migrations/002-profile-project-memory.js +71 -0
- package/dist/migrations/profile-manifests.d.ts +30 -0
- package/dist/migrations/profile-manifests.js +70 -0
- package/dist/migrations/registry.js +10 -5
- package/dist/migrations/types.d.ts +28 -1
- package/dist/migrations/types.js +15 -9
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +21 -4
- package/dist/pi-extensions/canvas-structured-output.js +85 -2
- package/dist/types.d.ts +15 -6
- package/dist/types.js +5 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/clients/attach/__tests__/file-review-focus.test.js +0 -49
- /package/dist/{clients/attach/__tests__/file-review-focus.test.d.ts → core/__tests__/profile-project-memory-delivery.test.d.ts} +0 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { posixProcessGroupSpawnOptions, terminateProcessGroup } from '../../../hook-process.js';
|
|
3
|
+
import { ExitCode } from '../../../types.js';
|
|
4
|
+
import { isRecord } from '../../../shared/predicates.js';
|
|
5
|
+
import { validateCoreDeclaredResult } from '../../command-plugins/transport/exec-invoke.js';
|
|
6
|
+
import { CrtrError } from '../../errors.js';
|
|
7
|
+
import { diag } from '../../io.js';
|
|
8
|
+
const MAX_STDOUT = 10 * 1024 * 1024;
|
|
9
|
+
export const EXEC_HOOK_TIMEOUT_MS = 15_000;
|
|
10
|
+
const RESERVED_CODES = new Set([
|
|
11
|
+
'internal',
|
|
12
|
+
'unknown_path',
|
|
13
|
+
'command_collision',
|
|
14
|
+
'command_hook_collision',
|
|
15
|
+
'plugin_protocol_error',
|
|
16
|
+
'hook_protocol_error',
|
|
17
|
+
]);
|
|
18
|
+
const SNAKE = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/;
|
|
19
|
+
const OPERATION_ID = /^[0-9a-f]{32}$/;
|
|
20
|
+
const NEXT = 'Run `crtr pkg plugin show` or `crtr sys doctor` to inspect the plugin.';
|
|
21
|
+
/** Invoke one hook executable; after failures retain the completed primary result. */
|
|
22
|
+
export async function invokeExecHook(spec, invocation) {
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
const request = buildRequest(spec, invocation, cwd);
|
|
25
|
+
const res = await runHookProcess(spec, invocation, JSON.stringify(request), cwd);
|
|
26
|
+
if (res.signal !== null) {
|
|
27
|
+
throw protocolError(spec, invocation, `executable was killed by signal ${res.signal}`, 'a clean exit with one JSON envelope on stdout');
|
|
28
|
+
}
|
|
29
|
+
const envelope = parseEnvelope(spec, invocation, res.stdout);
|
|
30
|
+
if (envelope.ok && res.status !== 0) {
|
|
31
|
+
diag(`crtr: ${label(spec)} returned ok:true but exited ${res.status} (honoring the envelope)`);
|
|
32
|
+
}
|
|
33
|
+
else if (!envelope.ok && res.status === 0) {
|
|
34
|
+
diag(`crtr: ${label(spec)} returned ok:false but exited 0 (honoring the envelope)`);
|
|
35
|
+
}
|
|
36
|
+
if (!envelope.ok)
|
|
37
|
+
throwHookFailure(spec, invocation, envelope);
|
|
38
|
+
if (spec.phase !== 'replace')
|
|
39
|
+
return;
|
|
40
|
+
const result = envelope.result;
|
|
41
|
+
if (!isRecord(result)) {
|
|
42
|
+
throw protocolError(spec, invocation, 'replacement success result was not an object', 'result: an object matching the target output declaration');
|
|
43
|
+
}
|
|
44
|
+
const output = spec.output;
|
|
45
|
+
if (output === undefined) {
|
|
46
|
+
throw protocolError(spec, invocation, 'replacement target output declaration was missing', 'the target leaf output declaration');
|
|
47
|
+
}
|
|
48
|
+
const issue = validateCoreDeclaredResult(output, result);
|
|
49
|
+
if (issue !== null) {
|
|
50
|
+
const field = output.find((candidate) => candidate.name === issue.field);
|
|
51
|
+
if (issue.receivedType !== undefined) {
|
|
52
|
+
throw protocolError(spec, invocation, `replacement output field "${issue.field}" has the wrong type`, `${issue.field}: ${field?.type ?? 'unknown'}`);
|
|
53
|
+
}
|
|
54
|
+
throw protocolError(spec, invocation, `replacement result is missing required output field "${issue.field}"`, `a "${issue.field}" field of type ${field?.type ?? 'unknown'}`);
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
async function runHookProcess(spec, invocation, input, cwd) {
|
|
59
|
+
let child;
|
|
60
|
+
try {
|
|
61
|
+
child = spawn(spec.executable, ['--crtr-hook-protocol', '1'], {
|
|
62
|
+
cwd,
|
|
63
|
+
env: process.env,
|
|
64
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
65
|
+
...posixProcessGroupSpawnOptions(),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw protocolError(spec, invocation, `executable could not be run (${describeError(error)})`, 'a runnable hook executable');
|
|
70
|
+
}
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
let stdoutBytes = 0;
|
|
73
|
+
const stdout = [];
|
|
74
|
+
let failure;
|
|
75
|
+
let closed = false;
|
|
76
|
+
let cleaned = false;
|
|
77
|
+
let status = null;
|
|
78
|
+
let signal = null;
|
|
79
|
+
let timeout;
|
|
80
|
+
const finish = () => {
|
|
81
|
+
if (!closed || (failure !== undefined && !cleaned))
|
|
82
|
+
return;
|
|
83
|
+
if (timeout !== undefined)
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
if (failure?.kind === 'timeout') {
|
|
86
|
+
reject(protocolError(spec, invocation, `executable timed out after ${EXEC_HOOK_TIMEOUT_MS}ms`, 'one JSON envelope before the execution timeout'));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (failure?.kind === 'output_limit') {
|
|
90
|
+
reject(protocolError(spec, invocation, 'stdout exceeded the 10 MiB cap', 'one JSON envelope under 10 MiB on stdout'));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (failure?.kind === 'spawn') {
|
|
94
|
+
reject(protocolError(spec, invocation, `executable could not be run (${describeError(failure.error)})`, 'a runnable hook executable'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (failure?.kind === 'cleanup') {
|
|
98
|
+
reject(protocolError(spec, invocation, `executable cleanup failed (${describeError(failure.error)})`, 'a hook executable whose process group can be terminated'));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
resolve({ stdout: Buffer.concat(stdout).toString('utf8'), status, signal });
|
|
102
|
+
};
|
|
103
|
+
const terminate = (reason) => {
|
|
104
|
+
if (failure !== undefined)
|
|
105
|
+
return;
|
|
106
|
+
failure = reason;
|
|
107
|
+
if (timeout !== undefined)
|
|
108
|
+
clearTimeout(timeout);
|
|
109
|
+
void terminateProcessGroup(child).then(() => {
|
|
110
|
+
cleaned = true;
|
|
111
|
+
finish();
|
|
112
|
+
}, (error) => {
|
|
113
|
+
failure = { kind: 'cleanup', error };
|
|
114
|
+
cleaned = true;
|
|
115
|
+
finish();
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
child.stdout.on('data', (chunk) => {
|
|
119
|
+
stdoutBytes += chunk.byteLength;
|
|
120
|
+
if (stdoutBytes > MAX_STDOUT) {
|
|
121
|
+
terminate({ kind: 'output_limit' });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
stdout.push(chunk);
|
|
125
|
+
});
|
|
126
|
+
child.once('error', (error) => {
|
|
127
|
+
if (child.pid === undefined) {
|
|
128
|
+
failure = { kind: 'spawn', error };
|
|
129
|
+
closed = true;
|
|
130
|
+
cleaned = true;
|
|
131
|
+
finish();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
terminate({ kind: 'spawn', error });
|
|
135
|
+
});
|
|
136
|
+
child.once('close', (nextStatus, nextSignal) => {
|
|
137
|
+
status = nextStatus;
|
|
138
|
+
signal = nextSignal;
|
|
139
|
+
closed = true;
|
|
140
|
+
finish();
|
|
141
|
+
});
|
|
142
|
+
timeout = setTimeout(() => terminate({ kind: 'timeout' }), EXEC_HOOK_TIMEOUT_MS);
|
|
143
|
+
child.stdin.end(input);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function buildRequest(spec, invocation, cwd) {
|
|
147
|
+
if (!OPERATION_ID.test(invocation.operationId)) {
|
|
148
|
+
throw protocolError(spec, invocation, 'operationId was not a 32-character lowercase hexadecimal string', 'a valid crouter operation identity');
|
|
149
|
+
}
|
|
150
|
+
if (!isJson(invocation.input)) {
|
|
151
|
+
throw protocolError(spec, invocation, 'input was not JSON-compatible', 'a JSON object');
|
|
152
|
+
}
|
|
153
|
+
if (!invocation.providedParams.every((param) => typeof param === 'string')) {
|
|
154
|
+
throw protocolError(spec, invocation, 'providedParams contained a non-string value', 'an array of parameter names');
|
|
155
|
+
}
|
|
156
|
+
const base = {
|
|
157
|
+
protocolVersion: 1,
|
|
158
|
+
op: spec.op,
|
|
159
|
+
command: [...spec.commandPath],
|
|
160
|
+
operationId: invocation.operationId,
|
|
161
|
+
input: invocation.input,
|
|
162
|
+
context: { cwd, providedParams: [...invocation.providedParams] },
|
|
163
|
+
};
|
|
164
|
+
if (spec.phase === 'after') {
|
|
165
|
+
if (invocation.result === undefined || !isJson(invocation.result)) {
|
|
166
|
+
throw protocolError(spec, invocation, 'after hook result was missing or not JSON-compatible', 'the completed primary result');
|
|
167
|
+
}
|
|
168
|
+
return { ...base, phase: 'after', result: invocation.result };
|
|
169
|
+
}
|
|
170
|
+
if (invocation.result !== undefined) {
|
|
171
|
+
throw protocolError(spec, invocation, `${spec.phase} hook invocation included a result`, 'result only for after hooks');
|
|
172
|
+
}
|
|
173
|
+
return { ...base, phase: spec.phase };
|
|
174
|
+
}
|
|
175
|
+
function parseEnvelope(spec, invocation, stdout) {
|
|
176
|
+
let value;
|
|
177
|
+
try {
|
|
178
|
+
value = JSON.parse(stdout.trim());
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
throw protocolError(spec, invocation, 'stdout was not a single JSON envelope', 'exactly one JSON envelope on stdout');
|
|
182
|
+
}
|
|
183
|
+
if (!isRecord(value)) {
|
|
184
|
+
throw protocolError(spec, invocation, 'stdout was not a JSON object', 'a { protocolVersion: 1, ok: boolean, ... } envelope');
|
|
185
|
+
}
|
|
186
|
+
if (value['protocolVersion'] !== 1) {
|
|
187
|
+
throw protocolError(spec, invocation, `unsupported protocolVersion ${String(value['protocolVersion'])}`, 'protocolVersion exactly 1');
|
|
188
|
+
}
|
|
189
|
+
if (value['ok'] === true) {
|
|
190
|
+
const expectedKeys = spec.phase === 'replace' ? ['protocolVersion', 'ok', 'result'] : ['protocolVersion', 'ok'];
|
|
191
|
+
if (!hasExactKeys(value, expectedKeys)) {
|
|
192
|
+
throw protocolError(spec, invocation, 'success envelope had missing or extra fields', `{ ${expectedKeys.join(', ')} } only`);
|
|
193
|
+
}
|
|
194
|
+
if (spec.phase === 'replace') {
|
|
195
|
+
if (!isJson(value['result'])) {
|
|
196
|
+
throw protocolError(spec, invocation, 'replacement result was not JSON-compatible', 'a JSON result');
|
|
197
|
+
}
|
|
198
|
+
return { protocolVersion: 1, ok: true, result: value['result'] };
|
|
199
|
+
}
|
|
200
|
+
return { protocolVersion: 1, ok: true };
|
|
201
|
+
}
|
|
202
|
+
if (value['ok'] !== false || !hasExactKeys(value, ['protocolVersion', 'ok', 'error'])) {
|
|
203
|
+
throw protocolError(spec, invocation, 'failure envelope had missing or extra fields', '{ protocolVersion, ok, error } only');
|
|
204
|
+
}
|
|
205
|
+
const error = value['error'];
|
|
206
|
+
if (!isRecord(error) || !hasExactKeys(error, ['code', 'message'], ['field', 'next'])) {
|
|
207
|
+
throw protocolError(spec, invocation, 'error envelope was missing fields or contained extra fields', 'error: { code, message, field?, next? }');
|
|
208
|
+
}
|
|
209
|
+
if (typeof error['code'] !== 'string' || error['code'] === '' || typeof error['message'] !== 'string' || error['message'] === '') {
|
|
210
|
+
throw protocolError(spec, invocation, 'error envelope had an empty or non-string code/message', 'non-empty error code and message strings');
|
|
211
|
+
}
|
|
212
|
+
if ((error['field'] !== undefined && typeof error['field'] !== 'string') || (error['next'] !== undefined && typeof error['next'] !== 'string')) {
|
|
213
|
+
throw protocolError(spec, invocation, 'error envelope had a non-string field/next', 'string field and next values when present');
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
protocolVersion: 1,
|
|
217
|
+
ok: false,
|
|
218
|
+
error: {
|
|
219
|
+
code: error['code'],
|
|
220
|
+
message: error['message'],
|
|
221
|
+
...(error['field'] === undefined ? {} : { field: error['field'] }),
|
|
222
|
+
...(error['next'] === undefined ? {} : { next: error['next'] }),
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function throwHookFailure(spec, invocation, failure) {
|
|
227
|
+
const { code, message, field, next } = failure.error;
|
|
228
|
+
if (!SNAKE.test(code) || RESERVED_CODES.has(code)) {
|
|
229
|
+
throw protocolError(spec, invocation, `error code "${code}" was invalid or reserved`, 'a non-reserved lowercase snake_case code');
|
|
230
|
+
}
|
|
231
|
+
throw new CrtrError(code, `${label(spec)}: ${message}`, ExitCode.GENERAL, failureDetails(spec, invocation, field, next));
|
|
232
|
+
}
|
|
233
|
+
function protocolError(spec, invocation, received, expected) {
|
|
234
|
+
return new CrtrError('hook_protocol_error', `${label(spec)}: ${received}. Expected ${expected}.`, ExitCode.GENERAL, failureDetails(spec, invocation, undefined, NEXT, received));
|
|
235
|
+
}
|
|
236
|
+
function failureDetails(spec, invocation, field, next, received) {
|
|
237
|
+
const identity = {
|
|
238
|
+
plugin: spec.plugin,
|
|
239
|
+
op: spec.op,
|
|
240
|
+
phase: spec.phase,
|
|
241
|
+
command: [...spec.commandPath],
|
|
242
|
+
...(received === undefined ? {} : { received }),
|
|
243
|
+
...(field === undefined ? {} : { field }),
|
|
244
|
+
};
|
|
245
|
+
if (spec.phase !== 'after')
|
|
246
|
+
return { ...identity, next: next ?? NEXT };
|
|
247
|
+
const guidance = 'Do not retry the original command; its primary action completed. Inspect and reconcile the failed after hook.';
|
|
248
|
+
return {
|
|
249
|
+
...identity,
|
|
250
|
+
primaryCompleted: true,
|
|
251
|
+
result: invocation.result,
|
|
252
|
+
next: next === undefined || next === NEXT ? guidance : `${next} ${guidance}`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function label(spec) {
|
|
256
|
+
return `plugin "${spec.plugin}" ${spec.phase} hook "${spec.op}" for \`${spec.commandPath.join(' ')}\``;
|
|
257
|
+
}
|
|
258
|
+
function hasExactKeys(value, required, optional = []) {
|
|
259
|
+
const keys = Object.keys(value);
|
|
260
|
+
return required.every((key) => Object.hasOwn(value, key))
|
|
261
|
+
&& keys.every((key) => required.includes(key) || optional.includes(key));
|
|
262
|
+
}
|
|
263
|
+
function isJson(value) {
|
|
264
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string')
|
|
265
|
+
return true;
|
|
266
|
+
if (typeof value === 'number')
|
|
267
|
+
return Number.isFinite(value);
|
|
268
|
+
if (Array.isArray(value))
|
|
269
|
+
return value.every(isJson);
|
|
270
|
+
return isRecord(value) && Object.values(value).every(isJson);
|
|
271
|
+
}
|
|
272
|
+
function describeError(error) {
|
|
273
|
+
return error instanceof Error ? error.message : String(error);
|
|
274
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { listInstalledPlugins, listInstalledPluginsInRoot } from '../resolver.js';
|
|
2
|
+
import { projectScopeRoots } from '../scope.js';
|
|
3
|
+
/** Whether an enabled command plugin survives nearest-scope name shadowing. */
|
|
4
|
+
export function hasEffectiveCommandPlugins(startDir = process.cwd(), profileId) {
|
|
5
|
+
const seen = new Set();
|
|
6
|
+
const consider = (plugin) => {
|
|
7
|
+
if (seen.has(plugin.name))
|
|
8
|
+
return false;
|
|
9
|
+
seen.add(plugin.name);
|
|
10
|
+
return plugin.enabled && (plugin.manifest.commands !== undefined || plugin.manifest.transport !== undefined);
|
|
11
|
+
};
|
|
12
|
+
for (const root of projectScopeRoots(startDir, profileId)) {
|
|
13
|
+
if (listInstalledPluginsInRoot('project', root).some(consider))
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return listInstalledPlugins('user').some(consider);
|
|
17
|
+
}
|
|
@@ -16,6 +16,11 @@ export declare function validateDeclaredResult(output: Field[], result: Record<s
|
|
|
16
16
|
field: string;
|
|
17
17
|
receivedType?: string;
|
|
18
18
|
} | null;
|
|
19
|
+
/** Core leaf declarations may explicitly require a nullable field. */
|
|
20
|
+
export declare function validateCoreDeclaredResult(output: Field[], result: Record<string, unknown>): {
|
|
21
|
+
field: string;
|
|
22
|
+
receivedType?: string;
|
|
23
|
+
} | null;
|
|
19
24
|
/** Execute one external leaf and return its result object (crtr renders it via
|
|
20
25
|
* the generic renderer; `--json` mirrors it). Throws a CrtrError for any
|
|
21
26
|
* protocol violation or the plugin's own recoverable error. */
|
|
@@ -21,13 +21,23 @@ const NEXT = 'Run `crtr pkg plugin show` or `crtr sys doctor` to inspect the plu
|
|
|
21
21
|
* Returns null if valid; otherwise returns a { field, receivedType } error.
|
|
22
22
|
* Transport-neutral: used by exec and HTTP transport adapters. */
|
|
23
23
|
export function validateDeclaredResult(output, result) {
|
|
24
|
+
return validateFields(output, result, false);
|
|
25
|
+
}
|
|
26
|
+
/** Core leaf declarations may explicitly require a nullable field. */
|
|
27
|
+
export function validateCoreDeclaredResult(output, result) {
|
|
28
|
+
return validateFields(output, result, true);
|
|
29
|
+
}
|
|
30
|
+
function validateFields(output, result, honorDeclaredNull) {
|
|
24
31
|
for (const f of output) {
|
|
25
|
-
const
|
|
26
|
-
|
|
32
|
+
const value = result[f.name];
|
|
33
|
+
const present = Object.prototype.hasOwnProperty.call(result, f.name)
|
|
34
|
+
&& value !== undefined
|
|
35
|
+
&& (honorDeclaredNull || value !== null);
|
|
36
|
+
if (f.required && !present)
|
|
27
37
|
return { field: f.name };
|
|
28
|
-
|
|
29
|
-
if (present && !
|
|
30
|
-
return { field: f.name, receivedType: typeof
|
|
38
|
+
const matches = honorDeclaredNull ? coreTypeMatches(f.type, value) : typeMatches(f.type, value);
|
|
39
|
+
if (present && !matches) {
|
|
40
|
+
return { field: f.name, receivedType: value === null ? 'null' : typeof value };
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
43
|
return null;
|
|
@@ -150,6 +160,49 @@ function typeMatches(type, v) {
|
|
|
150
160
|
return isRecord(v);
|
|
151
161
|
return true;
|
|
152
162
|
}
|
|
163
|
+
function coreTypeMatches(type, v) {
|
|
164
|
+
const alternatives = type.split('|').map((part) => part.trim()).filter(Boolean);
|
|
165
|
+
if (alternatives.length === 1 && !knownType(alternatives[0]))
|
|
166
|
+
return true;
|
|
167
|
+
const allKnown = alternatives.every((part) => knownType(part) || /^[a-z][a-z0-9_-]*$/.test(part));
|
|
168
|
+
if (!allKnown)
|
|
169
|
+
return true;
|
|
170
|
+
return alternatives.some((part) => atomMatches(part, v));
|
|
171
|
+
}
|
|
172
|
+
function knownType(type) {
|
|
173
|
+
const t = type.toLowerCase();
|
|
174
|
+
return t === 'null'
|
|
175
|
+
|| t === 'string'
|
|
176
|
+
|| t === 'path'
|
|
177
|
+
|| t === 'markdown'
|
|
178
|
+
|| t === 'int'
|
|
179
|
+
|| t === 'integer'
|
|
180
|
+
|| t === 'number'
|
|
181
|
+
|| t === 'float'
|
|
182
|
+
|| t === 'bool'
|
|
183
|
+
|| t === 'boolean'
|
|
184
|
+
|| t === 'array'
|
|
185
|
+
|| t.endsWith('[]')
|
|
186
|
+
|| t === 'object';
|
|
187
|
+
}
|
|
188
|
+
function atomMatches(type, v) {
|
|
189
|
+
const t = type.toLowerCase();
|
|
190
|
+
if (t === 'null')
|
|
191
|
+
return v === null;
|
|
192
|
+
if (t === 'string' || t === 'path' || t === 'markdown')
|
|
193
|
+
return typeof v === 'string';
|
|
194
|
+
if (t === 'int' || t === 'integer')
|
|
195
|
+
return typeof v === 'number' && Number.isInteger(v);
|
|
196
|
+
if (t === 'number' || t === 'float')
|
|
197
|
+
return typeof v === 'number';
|
|
198
|
+
if (t === 'bool' || t === 'boolean')
|
|
199
|
+
return typeof v === 'boolean';
|
|
200
|
+
if (t === 'array' || t.endsWith('[]'))
|
|
201
|
+
return Array.isArray(v);
|
|
202
|
+
if (t === 'object')
|
|
203
|
+
return isRecord(v);
|
|
204
|
+
return typeof v === 'string' && v === type;
|
|
205
|
+
}
|
|
153
206
|
function protocolError(spec, received, expected) {
|
|
154
207
|
return new CrtrError('plugin_protocol_error', `${label(spec)}: ${received}. Expected ${expected}.`, ExitCode.GENERAL, { received, next: NEXT });
|
|
155
208
|
}
|
package/dist/core/command.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RootHelp, RootEntry, BranchHelp, LeafHelp, InputParam, SubTier } from './help.js';
|
|
1
|
+
import type { RootHelp, RootEntry, BranchHelp, LeafHelp, EffectiveLeafHelp, InputParam, SubTier } from './help.js';
|
|
2
2
|
import { CrtrError } from './errors.js';
|
|
3
3
|
/** Runtime context passed to a leaf's run function. Includes metadata about
|
|
4
4
|
* which parameters were explicitly provided vs. defaulted. Used by configured
|
|
@@ -47,6 +47,11 @@ export interface LeafDef {
|
|
|
47
47
|
* 'hidden' keeps an internal leaf out of every listing. */
|
|
48
48
|
tier?: SubTier;
|
|
49
49
|
help: LeafHelp;
|
|
50
|
+
/** Invocation-local immutable help view. Present only on hook-eligible core
|
|
51
|
+
* leaves; static discovery may change Effects but never executes hooks. */
|
|
52
|
+
effectiveHelp?: () => Promise<EffectiveLeafHelp>;
|
|
53
|
+
/** Opt this conditionally-void leaf out of core command hooks. */
|
|
54
|
+
hookEligible?: false;
|
|
50
55
|
/** Opt into editor slash-command exposure (see SlashSpec). */
|
|
51
56
|
slash?: SlashSpec;
|
|
52
57
|
run: (input: Record<string, unknown>, context?: LeafRunContext) => Promise<Record<string, unknown> | void>;
|
|
@@ -100,6 +105,8 @@ export declare function defineLeaf(opts: {
|
|
|
100
105
|
tier?: SubTier;
|
|
101
106
|
help: LeafHelp;
|
|
102
107
|
slash?: SlashSpec;
|
|
108
|
+
/** Opt this conditionally-void leaf out of core command hooks. */
|
|
109
|
+
hookEligible?: false;
|
|
103
110
|
run: (input: Record<string, unknown>, context?: LeafRunContext) => Promise<Record<string, unknown> | void>;
|
|
104
111
|
render?: (result: Record<string, unknown>) => string;
|
|
105
112
|
}): LeafDef;
|
package/dist/core/command.js
CHANGED
|
@@ -34,6 +34,7 @@ export function defineLeaf(opts) {
|
|
|
34
34
|
tier: opts.tier,
|
|
35
35
|
help: freezeCommandMetadata(opts.help),
|
|
36
36
|
slash: opts.slash === undefined ? undefined : freezeCommandMetadata(opts.slash),
|
|
37
|
+
hookEligible: opts.hookEligible,
|
|
37
38
|
run: opts.run,
|
|
38
39
|
render: opts.render,
|
|
39
40
|
});
|
|
@@ -187,19 +188,20 @@ function renderNode(node) {
|
|
|
187
188
|
return renderBranch(node.help);
|
|
188
189
|
return renderLeafArgv(node.help);
|
|
189
190
|
}
|
|
190
|
-
/** Render
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* lookup receives the install gate's exact strict-validation inputs (reserved
|
|
196
|
-
* core names + the full core command path set, both from build-root), so a
|
|
197
|
-
* manifest rejected at ingress contributes nothing here either — loading
|
|
198
|
-
* every subtree for that path set is a help-path-only cost. */
|
|
191
|
+
/** Render effective Effects for a hook-eligible core leaf, then append any
|
|
192
|
+
* commands.json helpAddenda beneath the contract. Both lookups read stored
|
|
193
|
+
* manifest bytes only. Addenda validation receives the install gate's exact
|
|
194
|
+
* reserved core names and full core path set, so a manifest rejected at
|
|
195
|
+
* ingress contributes nothing here either. */
|
|
199
196
|
async function renderNodeWithAddenda(node, path) {
|
|
200
|
-
const body =
|
|
197
|
+
const body = node.kind === 'leaf' && node.effectiveHelp !== undefined
|
|
198
|
+
? renderLeafArgv(await node.effectiveHelp())
|
|
199
|
+
: renderNode(node);
|
|
201
200
|
if (path.length === 0)
|
|
202
201
|
return body;
|
|
202
|
+
const { hasEffectiveCommandPlugins } = await import('./command-plugins/presence.js');
|
|
203
|
+
if (!hasEffectiveCommandPlugins())
|
|
204
|
+
return body;
|
|
203
205
|
const [{ collectHelpAddenda }, { SUBTREE_NAMES, coreCommandPaths }] = await Promise.all([
|
|
204
206
|
import('./command-plugins/help-addenda.js'),
|
|
205
207
|
import('../build-root.js'),
|
package/dist/core/help.d.ts
CHANGED
|
@@ -216,6 +216,12 @@ export interface LeafHelp {
|
|
|
216
216
|
* default preview behaviour. */
|
|
217
217
|
preview?: PreviewMeta;
|
|
218
218
|
}
|
|
219
|
+
/** Invocation-local help view for one hook-eligible core leaf. It shares every
|
|
220
|
+
* non-effects field with the frozen core help and owns a separately frozen
|
|
221
|
+
* effects list that describes the effective pipeline. */
|
|
222
|
+
export type EffectiveLeafHelp = Readonly<Omit<LeafHelp, 'effects'> & {
|
|
223
|
+
effects: readonly string[];
|
|
224
|
+
}>;
|
|
219
225
|
/** Build a self-named runtime-state element: `<tag attr="v">body</tag>`. The
|
|
220
226
|
* subtree that owns the state authors it through this, so the tag name and any
|
|
221
227
|
* scalar metadata (e.g. a count) travel with the data and render identically
|
|
@@ -225,4 +231,4 @@ export interface LeafHelp {
|
|
|
225
231
|
export declare function stateBlock(tag: string, attrs: Record<string, string | number>, body: string): string;
|
|
226
232
|
export declare function renderRoot(h: RootHelp): string;
|
|
227
233
|
export declare function renderBranch(h: BranchHelp): string;
|
|
228
|
-
export declare function renderLeafArgv(h: LeafHelp): string;
|
|
234
|
+
export declare function renderLeafArgv(h: LeafHelp | EffectiveLeafHelp): string;
|
package/dist/core/io.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export declare function setJsonOutput(v: boolean): void;
|
|
|
30
30
|
export declare function isJsonOutput(): boolean;
|
|
31
31
|
/** Structured error payload. `error` is a stable code the agent branches on;
|
|
32
32
|
* `next` is the recovery road sign. */
|
|
33
|
-
|
|
33
|
+
interface ErrorPayloadBase {
|
|
34
34
|
error: string;
|
|
35
35
|
message: string;
|
|
36
36
|
received?: unknown;
|
|
@@ -41,6 +41,13 @@ export interface ErrorPayload {
|
|
|
41
41
|
http_status?: number;
|
|
42
42
|
next: string;
|
|
43
43
|
}
|
|
44
|
+
export type ErrorPayload = ErrorPayloadBase & ({
|
|
45
|
+
primaryCompleted: true;
|
|
46
|
+
result: Record<string, unknown>;
|
|
47
|
+
} | {
|
|
48
|
+
primaryCompleted?: never;
|
|
49
|
+
result?: never;
|
|
50
|
+
});
|
|
44
51
|
/** A command-level failure: surfaces as the JSON response on stdout. */
|
|
45
52
|
export declare class InputError extends CrtrError {
|
|
46
53
|
payload: ErrorPayload;
|
|
@@ -94,3 +101,4 @@ export declare function apiErrorToCliError(err: ApiError, next?: string): CrtrEr
|
|
|
94
101
|
* Runtime/internal failures go to stderr as `{error:"internal"}` — raw traces
|
|
95
102
|
* never reach the agent. Exits non-zero either way. */
|
|
96
103
|
export declare function handle(e: unknown): void;
|
|
104
|
+
export {};
|
package/dist/core/io.js
CHANGED
|
@@ -193,16 +193,58 @@ function payloadOf(e) {
|
|
|
193
193
|
if (e instanceof InputError)
|
|
194
194
|
return e.payload;
|
|
195
195
|
const d = (e.details !== undefined ? e.details : {});
|
|
196
|
+
const result = d.primaryCompleted === true
|
|
197
|
+
&& d.result !== null
|
|
198
|
+
&& typeof d.result === 'object'
|
|
199
|
+
&& !Array.isArray(d.result)
|
|
200
|
+
? jsonSafeResult(d.result)
|
|
201
|
+
: undefined;
|
|
196
202
|
const next = d.next !== undefined
|
|
197
203
|
? d.next
|
|
198
204
|
: 'Inspect the error and adjust the call. See -h for the schema.';
|
|
199
|
-
|
|
205
|
+
const base = {
|
|
200
206
|
error: e.code,
|
|
201
207
|
message: e.message,
|
|
202
208
|
received: d.received,
|
|
203
209
|
field: d.field,
|
|
204
210
|
...(typeof d.http_status === 'number' ? { http_status: d.http_status } : {}),
|
|
205
|
-
|
|
211
|
+
};
|
|
212
|
+
return result === undefined
|
|
213
|
+
? { ...base, next }
|
|
214
|
+
: { ...base, primaryCompleted: true, result, next };
|
|
215
|
+
}
|
|
216
|
+
/** Detach completed results from getters/prototypes and keep every error sink
|
|
217
|
+
* JSON-safe. Tagged values preserve a truthful representation when a trusted
|
|
218
|
+
* primary violates its declared JSON result contract. */
|
|
219
|
+
function jsonSafeResult(result) {
|
|
220
|
+
const seen = new WeakSet();
|
|
221
|
+
try {
|
|
222
|
+
const encoded = JSON.stringify(result, (_key, value) => {
|
|
223
|
+
if (typeof value === 'bigint')
|
|
224
|
+
return { $crtrType: 'bigint', value: value.toString() };
|
|
225
|
+
if (typeof value === 'number' && !Number.isFinite(value))
|
|
226
|
+
return { $crtrType: 'number', value: String(value) };
|
|
227
|
+
if (typeof value === 'undefined' || typeof value === 'function' || typeof value === 'symbol') {
|
|
228
|
+
return { $crtrType: typeof value };
|
|
229
|
+
}
|
|
230
|
+
if (value !== null && typeof value === 'object') {
|
|
231
|
+
if (seen.has(value))
|
|
232
|
+
return { $crtrType: 'circular' };
|
|
233
|
+
seen.add(value);
|
|
234
|
+
}
|
|
235
|
+
return value;
|
|
236
|
+
});
|
|
237
|
+
if (encoded !== undefined) {
|
|
238
|
+
const decoded = JSON.parse(encoded);
|
|
239
|
+
if (decoded !== null && typeof decoded === 'object' && !Array.isArray(decoded)) {
|
|
240
|
+
return decoded;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch { /* a throwing getter/toJSON must not suppress the partial failure */ }
|
|
245
|
+
return {
|
|
246
|
+
$crtrType: 'unavailable',
|
|
247
|
+
message: 'The completed primary result could not be represented as JSON.',
|
|
206
248
|
};
|
|
207
249
|
}
|
|
208
250
|
// ---------------------------------------------------------------------------
|
|
@@ -6,7 +6,8 @@ import type { RefMeta } from '../runtime/broker-protocol.js';
|
|
|
6
6
|
* (`refs`, stably sorted by winning scope then name) plus the resolution set
|
|
7
7
|
* (`names` — doc names plus every proper directory prefix, because a bare-dir
|
|
8
8
|
* ref is a legal listing link) that both the broker's per-submission resolver
|
|
9
|
-
* and `buildGuidance` test tokens against.
|
|
9
|
+
* and `buildGuidance` test tokens against. `refs` additionally labels Gateway
|
|
10
|
+
* eligibility; the terminal viewer still receives every referenceable row.
|
|
10
11
|
*/
|
|
11
12
|
export declare function buildRefInventory(): {
|
|
12
13
|
refs: RefMeta[];
|
|
@@ -30,8 +30,9 @@
|
|
|
30
30
|
// `/reload` — this module itself holds no cache beyond the session parse
|
|
31
31
|
// cache it reads through.
|
|
32
32
|
import { listAllMemoryDocs } from '../memory-resolver.js';
|
|
33
|
+
import { memoryExtensionEffectiveCatalog, projectEffectiveMemoryExtensions } from './extensions.js';
|
|
33
34
|
import { parseSubstrateDoc } from '../substrate/schema.js';
|
|
34
|
-
import { cachedSubstrateDocsInclusive } from '../substrate/session-cache.js';
|
|
35
|
+
import { cachedAllMemoryDocsInclusive, cachedSubstrateDocsInclusive } from '../substrate/session-cache.js';
|
|
35
36
|
// Display-sort weight matching resolution precedence (node > project >
|
|
36
37
|
// profile > user > builtin) — display-only; resolution itself is
|
|
37
38
|
// name-only (`names.has(candidate)`), never scope-aware.
|
|
@@ -49,21 +50,27 @@ const SCOPE_SORT_RANK = {
|
|
|
49
50
|
* (`refs`, stably sorted by winning scope then name) plus the resolution set
|
|
50
51
|
* (`names` — doc names plus every proper directory prefix, because a bare-dir
|
|
51
52
|
* ref is a legal listing link) that both the broker's per-submission resolver
|
|
52
|
-
* and `buildGuidance` test tokens against.
|
|
53
|
+
* and `buildGuidance` test tokens against. `refs` additionally labels Gateway
|
|
54
|
+
* eligibility; the terminal viewer still receives every referenceable row.
|
|
53
55
|
*/
|
|
54
56
|
export function buildRefInventory() {
|
|
55
|
-
const
|
|
57
|
+
const memoryDocs = cachedAllMemoryDocsInclusive(listAllMemoryDocs);
|
|
58
|
+
const substrateDocs = cachedSubstrateDocsInclusive(listAllMemoryDocs, parseSubstrateDoc);
|
|
59
|
+
const substrateDocsByPath = new Map(substrateDocs.map((doc) => [doc.path, doc]));
|
|
56
60
|
// First-wins by canonical name: docs arrive already in precedence order
|
|
57
61
|
// (listAllMemoryDocs' nearest-first source ordering), so the first doc seen
|
|
58
62
|
// for a name is the winner.
|
|
59
63
|
const winners = new Map();
|
|
60
|
-
for (const
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
for (const memoryDoc of memoryDocs) {
|
|
65
|
+
const doc = substrateDocsByPath.get(memoryDoc.path);
|
|
66
|
+
if (doc !== undefined && !winners.has(doc.name))
|
|
67
|
+
winners.set(doc.name, { doc, memoryDoc });
|
|
63
68
|
}
|
|
64
69
|
const refs = [];
|
|
65
|
-
for (const [name, doc] of winners) {
|
|
66
|
-
|
|
70
|
+
for (const [name, { doc, memoryDoc }] of winners) {
|
|
71
|
+
const gatewayVisible = doc.scope !== 'builtin'
|
|
72
|
+
&& projectEffectiveMemoryExtensions(memoryDoc.frontmatter?.['extensions'], memoryExtensionEffectiveCatalog(memoryDoc))['northlight']?.['visibility'] !== false;
|
|
73
|
+
refs.push({ name, kind: doc.kind, scope: doc.scope, shortForm: doc.shortForm, gatewayVisible });
|
|
67
74
|
}
|
|
68
75
|
refs.sort((a, b) => {
|
|
69
76
|
const scopeDelta = SCOPE_SORT_RANK[a.scope] - SCOPE_SORT_RANK[b.scope];
|