@mrclrchtr/supi-tree-sitter 8.0.1 → 9.1.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 +12 -3
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +10 -1
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +9 -4
- package/node_modules/@mrclrchtr/supi-core/src/session-capabilities.ts +55 -1
- package/node_modules/@mrclrchtr/supi-core/src/session-capability-profiles.ts +240 -0
- package/node_modules/@mrclrchtr/supi-core/src/session.ts +22 -0
- package/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm +0 -0
- package/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm.map +7 -7
- package/node_modules/web-tree-sitter/package.json +1 -1
- package/node_modules/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/node_modules/web-tree-sitter/web-tree-sitter.wasm.map +7 -7
- package/package.json +6 -4
- package/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +1 -1
- package/resources/grammars/latex/LICENSE +21 -0
- package/resources/grammars/latex/tree-sitter-latex.wasm +0 -0
- package/resources/grammars/latex/tree-sitter-latex.wasm.json +17 -0
- package/resources/grammars/sql/tree-sitter-sql.wasm.json +1 -1
- package/src/language.ts +5 -1
- package/src/operation-support.ts +6 -0
- package/src/provider/tree-sitter-provider.ts +2 -0
- package/src/session/structural-worker-client.ts +45 -34
- package/src/session/structural-worker-process.ts +122 -0
- package/src/session/structural-worker-protocol.ts +1 -1
- package/src/tool/callees.ts +1 -0
- package/src/tool/imports.ts +55 -1
- package/src/tool/outline-latex.ts +288 -0
- package/src/tool/outline-polyglot.ts +8 -1
- package/src/tool/outline.ts +18 -4
- package/src/types.ts +11 -3
- package/src/worker/process-host.mjs +92 -0
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ This package provides the parser-backed structural substrate consumed by `@mrclr
|
|
|
27
27
|
|
|
28
28
|
- an owned parsing session API for direct library consumers
|
|
29
29
|
- a `StructuralProvider` adapter published through `./provider/tree-sitter-provider`
|
|
30
|
-
-
|
|
30
|
+
- one long-lived Structural Worker in a dedicated Node process; the Worker keeps Pi responsive during parser-backed work
|
|
31
31
|
- `CodeRequestControl` cancellation, absolute deadlines, shared atomic interruption, and hard-stop termination
|
|
32
32
|
- structural outline/import/export/node/callee/call-site operations through the asynchronous service surface
|
|
33
33
|
- operation-specific extension discovery through `getStructuralSearchSupportedExtensions()`
|
|
@@ -50,8 +50,13 @@ Coordinates in the library APIs use **1-based** line and character columns. Char
|
|
|
50
50
|
- HTML (`.html`, `.htm`, `.xhtml`)
|
|
51
51
|
- R (`.r`)
|
|
52
52
|
- SQL (`.sql`)
|
|
53
|
+
- LaTeX (`.tex`, `.sty`, `.cls`)
|
|
53
54
|
|
|
54
|
-
Outline collection supports every listed family. HTML outlines
|
|
55
|
+
Outline collection supports every listed family. HTML outlines list elements with a non-empty `id` attribute. SQL outlines list `CREATE` declarations and their first-level table and type members. LaTeX outlines list section headings and command, environment, label, acronym, glossary-entry, and color definitions.
|
|
56
|
+
|
|
57
|
+
Import collection supports JavaScript, TypeScript, TSX, and LaTeX. LaTeX imports include package, class, source, and bibliography directives. Export collection supports JavaScript, TypeScript, and TSX only. Call-site collection supports every listed family except HTML, SQL, and LaTeX. LaTeX does not support export collection, callee queries, or call-site collection. The LaTeX grammar does not parse `.bib` files.
|
|
58
|
+
|
|
59
|
+
The Go and Ruby grammars do not parse Go module manifests or ERB templates. `supi-lsp` still handles them semantically. For a bulk scan, use `getStructuralSearchSupportedExtensions(operation)`. Parser support does not mean that each operation is supported.
|
|
55
60
|
|
|
56
61
|
## Architecture
|
|
57
62
|
|
|
@@ -102,15 +107,19 @@ The benchmark reports cold and repeated outline results. It also reports cold an
|
|
|
102
107
|
|
|
103
108
|
The Structural Worker reads files asynchronously. It identifies fresh parsed files by canonical path, grammar, and SHA-256 content hash. Cached trees use true least-recently-used eviction with limits of 128 files and 32 MiB of retained UTF-8 source bytes. Compiled queries use the same policy with limits of 128 queries and 512 KiB of retained UTF-8 query text. Source and query byte counts are memory-related proxies because `web-tree-sitter` does not report WASM resource sizes. Cached canonical trees stay private in the Worker.
|
|
104
109
|
|
|
105
|
-
|
|
110
|
+
The Structural Worker runs in a dedicated Node child process. The process uses `--liftoff-only` so V8 avoids the optimizing WebAssembly compiler that uses unsafe amounts of memory for large grammars. This applies to every grammar in the Worker. Pi's main process does not receive this flag, and a V8 failure in the Worker process cannot crash Pi.
|
|
111
|
+
|
|
112
|
+
All structural service operations accept optional `CodeRequestControl`. The parent converts cancellation to one shared atomic flag and forwards absolute deadlines. When present, it forwards only the opaque Debug Operation ID through the Worker protocol so parse and query timing events keep direct request ownership. Direct library calls have no ID. The process host creates a shared atomic flag for its Worker thread because Node IPC cannot share the parent flag. Worker read phases observe a local abort signal. Parser and query progress callbacks observe the flag and deadline. If cooperative interruption does not settle in the fixed 250 ms grace period, the parent terminates the Worker process. Valid queued work keeps FIFO order on one fresh Worker with cold caches. There is no main-thread parser fallback.
|
|
106
113
|
|
|
107
114
|
## Source
|
|
108
115
|
|
|
109
116
|
- `src/api.ts` — public library entrypoint
|
|
117
|
+
- `src/worker/process-host.mjs` — process bridge and Worker-local cancellation flags
|
|
110
118
|
- `src/worker/bootstrap.mjs` — package-owned Worker bootstrap and direct `jiti` loader
|
|
111
119
|
- `src/worker/runtime.ts` — Worker-only parser and query runtime
|
|
112
120
|
- `src/worker/parsed-file-store.ts` — Worker-only parsed-file and compiled-query reuse
|
|
113
121
|
- `src/session/structural-worker-client.ts` — bounded FIFO mailbox, protocol, cancellation, and restart ownership
|
|
122
|
+
- `src/session/structural-worker-process.ts` — isolated Node process and IPC bridge
|
|
114
123
|
- `src/session/runtime-controller.ts` — generation-fenced shared Worker lifecycle
|
|
115
124
|
- `src/session/session.ts` — asynchronous Worker-proxy service and owned session API
|
|
116
125
|
- `src/operation-support.ts` — authoritative operation-specific extension support
|
|
@@ -94,6 +94,10 @@ export type ConfidenceMode = "semantic" | "structural" | "heuristic" | "unavaila
|
|
|
94
94
|
export interface OutlineData {
|
|
95
95
|
name: string;
|
|
96
96
|
kind: string;
|
|
97
|
+
/** Exact 1-based start of the declaration name token, when known. */
|
|
98
|
+
nameAnchor?: SymbolAnchor;
|
|
99
|
+
/** Exclusive 1-based end of a verified literal name span, when known. */
|
|
100
|
+
nameEndAnchor?: SymbolAnchor;
|
|
97
101
|
startLine: number;
|
|
98
102
|
startCharacter: number;
|
|
99
103
|
endLine: number;
|
|
@@ -216,7 +220,12 @@ export type RefactorResult =
|
|
|
216
220
|
authorizedMutationRoots: string[];
|
|
217
221
|
}
|
|
218
222
|
| { kind: "ambiguous"; candidates: DisambiguationCandidate[] }
|
|
219
|
-
| {
|
|
223
|
+
| {
|
|
224
|
+
kind: "unavailable";
|
|
225
|
+
reason: string;
|
|
226
|
+
/** Roots authorized by the routed LSP client, when a route returned no edit. */
|
|
227
|
+
authorizedMutationRoots?: string[];
|
|
228
|
+
};
|
|
220
229
|
|
|
221
230
|
// ── Structural data shapes (value types, range-flattened) ──────────────
|
|
222
231
|
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
export type DebugLevel = "debug" | "info" | "warning" | "error";
|
|
8
8
|
export type DebugAgentAccess = "off" | "sanitized" | "raw";
|
|
9
|
+
|
|
10
|
+
/** Custom session-entry type used to persist sanitized debug events. */
|
|
11
|
+
export const DEBUG_EVENT_ENTRY_TYPE = "supi-debug-event";
|
|
9
12
|
export interface DebugRegistryConfig {
|
|
10
13
|
/** Whether producers should retain debug events. */
|
|
11
14
|
enabled: boolean;
|
|
@@ -212,7 +215,7 @@ function toSanitizedView(event: DebugEvent): DebugEventView {
|
|
|
212
215
|
category: event.category,
|
|
213
216
|
message: event.message,
|
|
214
217
|
cwd: event.cwd,
|
|
215
|
-
data: event.data,
|
|
218
|
+
data: event.data === undefined ? undefined : redactDebugData(event.data),
|
|
216
219
|
};
|
|
217
220
|
}
|
|
218
221
|
|
|
@@ -241,15 +244,17 @@ export function recordDebugEvent(input: DebugEventInput): DebugEvent | null {
|
|
|
241
244
|
};
|
|
242
245
|
state.events.push(event);
|
|
243
246
|
trimToMaxEvents(state);
|
|
244
|
-
const view = toSanitizedView(event);
|
|
245
247
|
for (const listener of state.listeners) {
|
|
246
248
|
try {
|
|
247
|
-
listener(
|
|
249
|
+
listener(toSanitizedView(event));
|
|
248
250
|
} catch {
|
|
249
251
|
// Debug-event consumers must not alter producer behavior.
|
|
250
252
|
}
|
|
251
253
|
}
|
|
252
|
-
return {
|
|
254
|
+
return {
|
|
255
|
+
...event,
|
|
256
|
+
data: event.data === undefined ? undefined : redactDebugData(event.data),
|
|
257
|
+
};
|
|
253
258
|
}
|
|
254
259
|
|
|
255
260
|
/** Query retained debug events newest-first. Results are sanitized unless raw access is requested and allowed. */
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decodeSessionCapabilityProfileSnapshot,
|
|
3
|
+
type SessionCapabilityProfile,
|
|
4
|
+
type SessionCapabilityProfileSnapshot,
|
|
5
|
+
} from "./session-capability-profiles.ts";
|
|
6
|
+
|
|
1
7
|
/** Session-only tool and skill visibility state. */
|
|
2
8
|
export interface SessionCapabilityState {
|
|
3
9
|
version: 1;
|
|
@@ -7,6 +13,8 @@ export interface SessionCapabilityState {
|
|
|
7
13
|
/** Tools that the user explicitly enabled for this session. */
|
|
8
14
|
toolEnabledNames?: string[];
|
|
9
15
|
hiddenSkillNames: string[];
|
|
16
|
+
/** Original profile identity and exclusions. Later configuration changes do not affect this copy. */
|
|
17
|
+
profileSnapshot?: SessionCapabilityProfileSnapshot;
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
/** A model-visible skill that a provider can offer to the selector. */
|
|
@@ -94,8 +102,9 @@ export function decodeSessionCapabilities(value: unknown): SessionCapabilityStat
|
|
|
94
102
|
(name) => !initiallyInactive.has(name),
|
|
95
103
|
);
|
|
96
104
|
const eligible = new Set(eligibleToolNames);
|
|
97
|
-
const toolDenylist = stringList(record.toolDenylist)
|
|
105
|
+
const toolDenylist = stringList(record.toolDenylist);
|
|
98
106
|
const denied = new Set(toolDenylist);
|
|
107
|
+
const profileSnapshot = decodeSessionCapabilityProfileSnapshot(record.profileSnapshot);
|
|
99
108
|
|
|
100
109
|
return {
|
|
101
110
|
version: 1,
|
|
@@ -106,9 +115,54 @@ export function decodeSessionCapabilities(value: unknown): SessionCapabilityStat
|
|
|
106
115
|
(name) => eligible.has(name) && !denied.has(name),
|
|
107
116
|
),
|
|
108
117
|
hiddenSkillNames: stringList(record.hiddenSkillNames),
|
|
118
|
+
...(profileSnapshot ? { profileSnapshot } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Copy current exclusion sets for saving. Do not use inventory to remove names. */
|
|
123
|
+
export function captureSessionCapabilityExclusions(
|
|
124
|
+
state: SessionCapabilityState,
|
|
125
|
+
): SessionCapabilityProfile {
|
|
126
|
+
return {
|
|
127
|
+
excludedTools: [...new Set(state.toolDenylist)],
|
|
128
|
+
hiddenSkills: [...new Set(state.hiddenSkillNames)],
|
|
109
129
|
};
|
|
110
130
|
}
|
|
111
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Replace exclusions and capture a profile copy. Return undefined for an invalid profile.
|
|
134
|
+
* Do not mutate the input state or activate tools. The caller owns runtime restoration.
|
|
135
|
+
*/
|
|
136
|
+
export function applySessionCapabilityProfile(
|
|
137
|
+
state: SessionCapabilityState,
|
|
138
|
+
profile: unknown,
|
|
139
|
+
): SessionCapabilityState | undefined {
|
|
140
|
+
const profileSnapshot = decodeSessionCapabilityProfileSnapshot(profile);
|
|
141
|
+
if (!profileSnapshot) return undefined;
|
|
142
|
+
return decodeSessionCapabilities({
|
|
143
|
+
...state,
|
|
144
|
+
toolDenylist: profileSnapshot.excludedTools,
|
|
145
|
+
hiddenSkillNames: profileSnapshot.hiddenSkills,
|
|
146
|
+
profileSnapshot,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function equalNameSets(left: readonly string[], right: readonly string[]): boolean {
|
|
151
|
+
const leftSet = new Set(left);
|
|
152
|
+
const rightSet = new Set(right);
|
|
153
|
+
return leftSet.size === rightSet.size && [...leftSet].every((name) => rightSet.has(name));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Compare current exclusions with the applied copy. No profile means no Modified marker. */
|
|
157
|
+
export function isSessionCapabilityProfileModified(state: SessionCapabilityState): boolean {
|
|
158
|
+
const snapshot = state.profileSnapshot;
|
|
159
|
+
return (
|
|
160
|
+
snapshot !== undefined &&
|
|
161
|
+
(!equalNameSets(state.toolDenylist, snapshot.excludedTools) ||
|
|
162
|
+
!equalNameSets(state.hiddenSkillNames, snapshot.hiddenSkills))
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
112
166
|
/** Return a safe copy of the latest valid snapshot in all session entries. */
|
|
113
167
|
export function findSessionCapabilities(
|
|
114
168
|
entries: readonly unknown[],
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/** Exact names to exclude. Omitted configuration lists mean empty sets. */
|
|
2
|
+
export interface SessionCapabilityProfileDefinition {
|
|
3
|
+
excludedTools?: string[];
|
|
4
|
+
hiddenSkills?: string[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Validated exclusion sets. Names need not match the current inventory. */
|
|
8
|
+
export interface SessionCapabilityProfile {
|
|
9
|
+
excludedTools: string[];
|
|
10
|
+
hiddenSkills: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A profile is available only when validation has no errors. */
|
|
14
|
+
export interface SessionCapabilityProfileValidation {
|
|
15
|
+
profile?: SessionCapabilityProfile;
|
|
16
|
+
errors: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
20
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
21
|
+
const prototype = Object.getPrototypeOf(value);
|
|
22
|
+
return prototype === Object.prototype || prototype === null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readNames(value: unknown): string[] | undefined {
|
|
26
|
+
if (!Array.isArray(value)) return undefined;
|
|
27
|
+
const names: unknown[] = [...new Set(value)];
|
|
28
|
+
return names.every((name): name is string => typeof name === "string") ? names : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Validate and copy a complete definition. Do not check inventory or expand patterns. */
|
|
32
|
+
export function validateSessionCapabilityProfile(
|
|
33
|
+
value: unknown,
|
|
34
|
+
): SessionCapabilityProfileValidation {
|
|
35
|
+
if (!isRecord(value)) return { errors: ["Profile must be an object."] };
|
|
36
|
+
const excludedTools = Object.hasOwn(value, "excludedTools") ? readNames(value.excludedTools) : [];
|
|
37
|
+
const hiddenSkills = Object.hasOwn(value, "hiddenSkills") ? readNames(value.hiddenSkills) : [];
|
|
38
|
+
const errors = Object.keys(value)
|
|
39
|
+
.filter((key) => key !== "excludedTools" && key !== "hiddenSkills")
|
|
40
|
+
.map((key) => `Unknown profile field: ${key}.`);
|
|
41
|
+
if (!excludedTools) errors.push("excludedTools must be an array of strings.");
|
|
42
|
+
if (!hiddenSkills) errors.push("hiddenSkills must be an array of strings.");
|
|
43
|
+
if (errors.length || !excludedTools || !hiddenSkills) return { errors };
|
|
44
|
+
return { profile: { excludedTools, hiddenSkills }, errors };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Configuration scope that supplied a profile or default. */
|
|
48
|
+
export type SessionCapabilityProfileScope = "global" | "project";
|
|
49
|
+
|
|
50
|
+
/** Configuration values for the sessionCapabilities section. */
|
|
51
|
+
export interface SessionCapabilityProfilesSection {
|
|
52
|
+
profiles?: Record<string, SessionCapabilityProfileDefinition>;
|
|
53
|
+
defaultProfile?: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Session-owned identity and original exclusion sets, independent of configuration. */
|
|
57
|
+
export interface SessionCapabilityProfileSnapshot extends SessionCapabilityProfile {
|
|
58
|
+
id: string;
|
|
59
|
+
source: SessionCapabilityProfileScope;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A configuration error with a scope and field path for display. */
|
|
63
|
+
export interface SessionCapabilityProfileDiagnostic {
|
|
64
|
+
scope: SessionCapabilityProfileScope;
|
|
65
|
+
path: string;
|
|
66
|
+
message: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A scoped definition, including invalid definitions and hidden global definitions. */
|
|
70
|
+
export interface SessionCapabilityProfileEntry {
|
|
71
|
+
id: string;
|
|
72
|
+
source: SessionCapabilityProfileScope;
|
|
73
|
+
shadowed: boolean;
|
|
74
|
+
profile?: SessionCapabilityProfile;
|
|
75
|
+
errors: SessionCapabilityProfileDiagnostic[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Default selection source can differ from the effective profile source. */
|
|
79
|
+
export type SessionCapabilityProfileDefault =
|
|
80
|
+
| { status: "none"; source?: SessionCapabilityProfileScope }
|
|
81
|
+
| {
|
|
82
|
+
status: "resolved";
|
|
83
|
+
source: SessionCapabilityProfileScope;
|
|
84
|
+
id: string;
|
|
85
|
+
profile: SessionCapabilityProfileSnapshot;
|
|
86
|
+
}
|
|
87
|
+
| { status: "invalid"; source: SessionCapabilityProfileScope; id?: string };
|
|
88
|
+
|
|
89
|
+
/** Raw sections, not merged configuration. Callers must check trust before project reads. */
|
|
90
|
+
export interface ResolveSessionCapabilityProfilesOptions {
|
|
91
|
+
global?: unknown;
|
|
92
|
+
project?: unknown;
|
|
93
|
+
projectTrusted: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Data for profile rows, scoped actions, and default warnings. No I/O is performed. */
|
|
97
|
+
export interface SessionCapabilityProfilesResolution {
|
|
98
|
+
entries: SessionCapabilityProfileEntry[];
|
|
99
|
+
effectiveProfiles: SessionCapabilityProfileSnapshot[];
|
|
100
|
+
default: SessionCapabilityProfileDefault;
|
|
101
|
+
errors: SessionCapabilityProfileDiagnostic[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** IDs contain lowercase ASCII words separated by single hyphens. */
|
|
105
|
+
export function isSessionCapabilityProfileId(value: unknown): value is string {
|
|
106
|
+
return typeof value === "string" && /^[a-z]+(?:-[a-z]+)*$/.test(value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Validate and copy stored identity and original lists. Invalid metadata has no identity. */
|
|
110
|
+
export function decodeSessionCapabilityProfileSnapshot(
|
|
111
|
+
value: unknown,
|
|
112
|
+
): SessionCapabilityProfileSnapshot | undefined {
|
|
113
|
+
if (!isRecord(value) || !isSessionCapabilityProfileId(value.id)) return undefined;
|
|
114
|
+
if (value.source !== "global" && value.source !== "project") return undefined;
|
|
115
|
+
const { id, source, ...definition } = value;
|
|
116
|
+
if (!Object.hasOwn(definition, "excludedTools") || !Object.hasOwn(definition, "hiddenSkills"))
|
|
117
|
+
return undefined;
|
|
118
|
+
const result = validateSessionCapabilityProfile(definition);
|
|
119
|
+
return result.profile ? { id, source, ...result.profile } : undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readEntries(
|
|
123
|
+
value: unknown,
|
|
124
|
+
source: SessionCapabilityProfileScope,
|
|
125
|
+
): SessionCapabilityProfileEntry[] {
|
|
126
|
+
if (!isRecord(value) || !isRecord(value.profiles)) return [];
|
|
127
|
+
return Object.entries(value.profiles).map(([id, definition]) => {
|
|
128
|
+
const result = validateSessionCapabilityProfile(definition);
|
|
129
|
+
if (!isSessionCapabilityProfileId(id))
|
|
130
|
+
result.errors.push("Invalid profile ID: use lowercase words with single hyphens.");
|
|
131
|
+
return {
|
|
132
|
+
id,
|
|
133
|
+
source,
|
|
134
|
+
shadowed: false,
|
|
135
|
+
profile: result.errors.length ? undefined : result.profile,
|
|
136
|
+
errors: result.errors.map((message) => ({ scope: source, path: `profiles.${id}`, message })),
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
type DefaultReference =
|
|
142
|
+
| SessionCapabilityProfileDefault
|
|
143
|
+
| { status: "pending"; source: SessionCapabilityProfileScope; id: string };
|
|
144
|
+
|
|
145
|
+
function readDefault(
|
|
146
|
+
value: unknown,
|
|
147
|
+
source: SessionCapabilityProfileScope,
|
|
148
|
+
errors: SessionCapabilityProfileDiagnostic[],
|
|
149
|
+
): DefaultReference | undefined {
|
|
150
|
+
if (!isRecord(value) || !Object.hasOwn(value, "defaultProfile")) return undefined;
|
|
151
|
+
if (value.defaultProfile === null) return { status: "none", source };
|
|
152
|
+
if (isSessionCapabilityProfileId(value.defaultProfile)) {
|
|
153
|
+
return { status: "pending", source, id: value.defaultProfile };
|
|
154
|
+
}
|
|
155
|
+
errors.push({
|
|
156
|
+
scope: source,
|
|
157
|
+
path: "defaultProfile",
|
|
158
|
+
message: "defaultProfile must be null or a valid profile ID.",
|
|
159
|
+
});
|
|
160
|
+
return { status: "invalid", source };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function resolveDefault(
|
|
164
|
+
selected: DefaultReference | undefined,
|
|
165
|
+
profiles: SessionCapabilityProfileSnapshot[],
|
|
166
|
+
errors: SessionCapabilityProfileDiagnostic[],
|
|
167
|
+
): SessionCapabilityProfileDefault {
|
|
168
|
+
if (!selected) return { status: "none" };
|
|
169
|
+
if (selected.status !== "pending") return selected;
|
|
170
|
+
const { source, id } = selected;
|
|
171
|
+
const profile = profiles.find((entry) => entry.id === id);
|
|
172
|
+
if (profile) return { status: "resolved", source, id, profile };
|
|
173
|
+
errors.push({
|
|
174
|
+
scope: source,
|
|
175
|
+
path: "defaultProfile",
|
|
176
|
+
message: `Default profile ${id} is missing or invalid. No profile will be applied.`,
|
|
177
|
+
});
|
|
178
|
+
return { status: "invalid", source, id };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface ScopedProfiles {
|
|
182
|
+
entries: SessionCapabilityProfileEntry[];
|
|
183
|
+
errors: SessionCapabilityProfileDiagnostic[];
|
|
184
|
+
default?: DefaultReference;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Reject malformed sections, but retain known IDs to prevent global fallback. */
|
|
188
|
+
function readSection(value: unknown, scope: SessionCapabilityProfileScope): ScopedProfiles {
|
|
189
|
+
if (value === undefined) return { entries: [], errors: [] };
|
|
190
|
+
if (!isRecord(value)) {
|
|
191
|
+
return {
|
|
192
|
+
entries: [],
|
|
193
|
+
default: { status: "invalid", source: scope },
|
|
194
|
+
errors: [
|
|
195
|
+
{ scope, path: "sessionCapabilities", message: "sessionCapabilities must be an object." },
|
|
196
|
+
],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const sectionErrors = Object.keys(value)
|
|
200
|
+
.filter((key) => key !== "profiles" && key !== "defaultProfile")
|
|
201
|
+
.map((key) => ({ scope, path: key, message: `Unknown section field: ${key}.` }));
|
|
202
|
+
if (Object.hasOwn(value, "profiles") && !isRecord(value.profiles)) {
|
|
203
|
+
sectionErrors.push({ scope, path: "profiles", message: "profiles must be an object." });
|
|
204
|
+
}
|
|
205
|
+
const entries = readEntries(value, scope);
|
|
206
|
+
const errors = [...sectionErrors, ...entries.flatMap((entry) => entry.errors)];
|
|
207
|
+
const selection = readDefault(value, scope, errors);
|
|
208
|
+
if (sectionErrors.length) {
|
|
209
|
+
for (const entry of entries) {
|
|
210
|
+
entry.profile = undefined;
|
|
211
|
+
entry.errors.push(...sectionErrors);
|
|
212
|
+
}
|
|
213
|
+
return { entries, errors, default: { status: "invalid", source: scope } };
|
|
214
|
+
}
|
|
215
|
+
return { entries, errors, default: selection };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Resolve whole profiles by ID. Ignore project data unless projectTrusted is true.
|
|
220
|
+
* An absent section is undefined. Malformed sections cannot supply profiles or a default.
|
|
221
|
+
*/
|
|
222
|
+
export function resolveSessionCapabilityProfiles(
|
|
223
|
+
options: ResolveSessionCapabilityProfilesOptions,
|
|
224
|
+
): SessionCapabilityProfilesResolution {
|
|
225
|
+
const global = readSection(options.global, "global");
|
|
226
|
+
const project = readSection(options.projectTrusted ? options.project : undefined, "project");
|
|
227
|
+
const entries = [...global.entries, ...project.entries];
|
|
228
|
+
const errors = [...global.errors, ...project.errors];
|
|
229
|
+
const selected = new Map(entries.map((entry) => [entry.id, entry]));
|
|
230
|
+
for (const entry of entries) entry.shadowed = selected.get(entry.id) !== entry;
|
|
231
|
+
const effectiveProfiles = [...selected.values()].flatMap((entry) =>
|
|
232
|
+
entry.profile ? [{ id: entry.id, source: entry.source, ...entry.profile }] : [],
|
|
233
|
+
);
|
|
234
|
+
return {
|
|
235
|
+
entries,
|
|
236
|
+
effectiveProfiles,
|
|
237
|
+
default: resolveDefault(project.default ?? global.default, effectiveProfiles, errors),
|
|
238
|
+
errors,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -7,17 +7,39 @@ export type {
|
|
|
7
7
|
SessionCapabilityState,
|
|
8
8
|
} from "./session-capabilities.ts";
|
|
9
9
|
export {
|
|
10
|
+
applySessionCapabilityProfile,
|
|
11
|
+
captureSessionCapabilityExclusions,
|
|
10
12
|
clearPendingSessionCapabilityForks,
|
|
11
13
|
createEmptySessionCapabilityState,
|
|
12
14
|
decodeSessionCapabilities,
|
|
13
15
|
findSessionCapabilities,
|
|
14
16
|
getSessionCapabilitySkillProvider,
|
|
17
|
+
isSessionCapabilityProfileModified,
|
|
15
18
|
readPendingSessionCapabilityFork,
|
|
16
19
|
registerSessionCapabilitySkillProvider,
|
|
17
20
|
SESSION_CAPABILITIES_ENTRY,
|
|
18
21
|
sessionCapabilityState,
|
|
19
22
|
setPendingSessionCapabilityFork,
|
|
20
23
|
} from "./session-capabilities.ts";
|
|
24
|
+
export type {
|
|
25
|
+
ResolveSessionCapabilityProfilesOptions,
|
|
26
|
+
SessionCapabilityProfile,
|
|
27
|
+
SessionCapabilityProfileDefault,
|
|
28
|
+
SessionCapabilityProfileDefinition,
|
|
29
|
+
SessionCapabilityProfileDiagnostic,
|
|
30
|
+
SessionCapabilityProfileEntry,
|
|
31
|
+
SessionCapabilityProfileScope,
|
|
32
|
+
SessionCapabilityProfileSnapshot,
|
|
33
|
+
SessionCapabilityProfilesResolution,
|
|
34
|
+
SessionCapabilityProfilesSection,
|
|
35
|
+
SessionCapabilityProfileValidation,
|
|
36
|
+
} from "./session-capability-profiles.ts";
|
|
37
|
+
export {
|
|
38
|
+
decodeSessionCapabilityProfileSnapshot,
|
|
39
|
+
isSessionCapabilityProfileId,
|
|
40
|
+
resolveSessionCapabilityProfiles,
|
|
41
|
+
validateSessionCapabilityProfile,
|
|
42
|
+
} from "./session-capability-profiles.ts";
|
|
21
43
|
export type { SessionNameTrackerHost } from "./session-utils.ts";
|
|
22
44
|
export {
|
|
23
45
|
createSessionNameTracker,
|
|
Binary file
|