@mrclrchtr/supi-tree-sitter 8.0.1 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +12 -3
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +10 -1
  4. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  5. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +9 -4
  6. package/node_modules/@mrclrchtr/supi-core/src/session-capabilities.ts +55 -1
  7. package/node_modules/@mrclrchtr/supi-core/src/session-capability-profiles.ts +240 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/session.ts +22 -0
  9. package/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm +0 -0
  10. package/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm.map +7 -7
  11. package/node_modules/web-tree-sitter/package.json +1 -1
  12. package/node_modules/web-tree-sitter/web-tree-sitter.wasm +0 -0
  13. package/node_modules/web-tree-sitter/web-tree-sitter.wasm.map +7 -7
  14. package/package.json +6 -4
  15. package/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +1 -1
  16. package/resources/grammars/latex/LICENSE +21 -0
  17. package/resources/grammars/latex/tree-sitter-latex.wasm +0 -0
  18. package/resources/grammars/latex/tree-sitter-latex.wasm.json +17 -0
  19. package/resources/grammars/sql/tree-sitter-sql.wasm.json +1 -1
  20. package/src/language.ts +5 -1
  21. package/src/operation-support.ts +6 -0
  22. package/src/provider/tree-sitter-provider.ts +2 -0
  23. package/src/session/structural-worker-client.ts +45 -34
  24. package/src/session/structural-worker-process.ts +122 -0
  25. package/src/session/structural-worker-protocol.ts +1 -1
  26. package/src/tool/callees.ts +1 -0
  27. package/src/tool/imports.ts +55 -1
  28. package/src/tool/outline-latex.ts +288 -0
  29. package/src/tool/outline-polyglot.ts +8 -1
  30. package/src/tool/outline.ts +18 -4
  31. package/src/types.ts +11 -3
  32. 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
- - a long-lived owned Structural Worker that keeps Pi responsive during parser-backed work
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 contain elements with non-empty `id` attributes; SQL outlines contain `CREATE` declarations and shallow table/type members. Import and export collection remains JavaScript/TypeScript-only, and call-site collection supports every listed family except HTML and SQL. Go module manifests and ERB templates are intentionally excluded because the Go and Ruby grammars do not parse those mixed or separate syntaxes; `supi-lsp` still handles them semantically. Consumers performing a bulk structural scan should use `getStructuralSearchSupportedExtensions(operation)` rather than treating parser support as operation support.
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
- 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. 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. Valid queued work keeps FIFO order on one fresh Worker with cold caches. There is no main-thread parser fallback.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-code-runtime",
3
- "version": "8.0.1",
3
+ "version": "9.0.0",
4
4
  "description": "Shared workspace context and capability contracts for code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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
- | { kind: "unavailable"; reason: string };
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
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "8.0.1",
3
+ "version": "9.0.0",
4
4
  "description": "Shared settings, configuration, reporting, and session infrastructure",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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(view);
249
+ listener(toSanitizedView(event));
248
250
  } catch {
249
251
  // Debug-event consumers must not alter producer behavior.
250
252
  }
251
253
  }
252
- return { ...event };
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).filter((name) => eligible.has(name));
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,