@ontrails/topography 0.2.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/CHANGELOG.md +866 -0
- package/README.md +228 -0
- package/package.json +40 -0
- package/src/activation-report.ts +385 -0
- package/src/backend-support.ts +11 -0
- package/src/derive.ts +690 -0
- package/src/diff.ts +1088 -0
- package/src/forces.ts +125 -0
- package/src/hash.ts +55 -0
- package/src/index.ts +273 -0
- package/src/internal/topo-snapshots.ts +682 -0
- package/src/internal/topo-store-read.ts +1102 -0
- package/src/internal/topo-store.ts +1651 -0
- package/src/io.ts +280 -0
- package/src/library-derivation.ts +131 -0
- package/src/overlays.ts +155 -0
- package/src/permit.ts +19 -0
- package/src/source-fingerprint.ts +103 -0
- package/src/surface-bindings.ts +101 -0
- package/src/topo-store.ts +716 -0
- package/src/types.ts +749 -0
- package/src/versioning.ts +277 -0
- package/src/wayfind/error-facts.ts +363 -0
- package/src/wayfind/filters.ts +430 -0
- package/src/wayfind/loader.ts +443 -0
- package/src/wayfind/navigation.ts +265 -0
- package/src/wayfind/provenance.ts +152 -0
- package/src/wayfind/queries.ts +1656 -0
- package/src/wayfind/relations.ts +344 -0
- package/src/workspace-configured-aliases.ts +221 -0
- package/src/workspace-lock-census.ts +306 -0
- package/src/workspace-view-types.ts +104 -0
- package/src/workspace-view.ts +486 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import {
|
|
2
|
+
includedByPathScope,
|
|
3
|
+
matchesAnyPathGlob,
|
|
4
|
+
NotFoundError,
|
|
5
|
+
} from '@ontrails/core';
|
|
6
|
+
import type { PathScope } from '@ontrails/core';
|
|
7
|
+
import { trailsLockFileName } from '@ontrails/config';
|
|
8
|
+
import type {
|
|
9
|
+
ReadTrailsProjectIdentityResult,
|
|
10
|
+
ResolvedTrailsWorkspaceApp,
|
|
11
|
+
} from '@ontrails/config';
|
|
12
|
+
import { collectSourceTree } from '@ontrails/source';
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
UnownedWorkspaceLockObservation,
|
|
16
|
+
WorkspaceViewCollectionSkip,
|
|
17
|
+
} from './workspace-view-types.js';
|
|
18
|
+
import { deriveConfiguredAppAliases } from './workspace-configured-aliases.js';
|
|
19
|
+
|
|
20
|
+
const DEFAULT_IGNORED_DIRECTORIES = new Set([
|
|
21
|
+
'.git',
|
|
22
|
+
'.turbo',
|
|
23
|
+
'coverage',
|
|
24
|
+
'dist',
|
|
25
|
+
'node_modules',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export interface WorkspaceLockCensus {
|
|
29
|
+
readonly collectionSkips: readonly WorkspaceViewCollectionSkip[];
|
|
30
|
+
readonly configuredAppRootDirectories: ReadonlyMap<string, string>;
|
|
31
|
+
readonly unownedLocks: readonly UnownedWorkspaceLockObservation[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const containsRelativePath = (boundary: string, target: string): boolean =>
|
|
35
|
+
target === boundary || target.startsWith(`${boundary}/`);
|
|
36
|
+
|
|
37
|
+
const compareCollectionSkips = (
|
|
38
|
+
left: WorkspaceViewCollectionSkip,
|
|
39
|
+
right: WorkspaceViewCollectionSkip
|
|
40
|
+
): number => {
|
|
41
|
+
if (left.path !== right.path) {
|
|
42
|
+
return left.path < right.path ? -1 : 1;
|
|
43
|
+
}
|
|
44
|
+
if (left.reason === right.reason) {
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
return left.reason < right.reason ? -1 : 1;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const excludedPathForLock = (
|
|
51
|
+
lockPath: string,
|
|
52
|
+
lockScope: PathScope | undefined
|
|
53
|
+
): string | undefined => {
|
|
54
|
+
const segments = lockPath.split('/');
|
|
55
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
56
|
+
const ancestor = segments.slice(0, index).join('/');
|
|
57
|
+
if (matchesAnyPathGlob(ancestor, lockScope?.exclude)) {
|
|
58
|
+
return ancestor;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return matchesAnyPathGlob(lockPath, lockScope?.exclude)
|
|
62
|
+
? lockPath
|
|
63
|
+
: undefined;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const scopeExcludedPathForExpectedLock = (
|
|
67
|
+
lockPath: string,
|
|
68
|
+
lockScope: PathScope | undefined
|
|
69
|
+
): string | undefined => {
|
|
70
|
+
const excludedPath = excludedPathForLock(lockPath, lockScope);
|
|
71
|
+
if (excludedPath !== undefined) {
|
|
72
|
+
return excludedPath;
|
|
73
|
+
}
|
|
74
|
+
return includedByPathScope(lockPath, lockScope) ? undefined : lockPath;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const workspaceCollectionSkipForApp = (
|
|
78
|
+
app: ResolvedTrailsWorkspaceApp,
|
|
79
|
+
collectionSkips: readonly WorkspaceViewCollectionSkip[]
|
|
80
|
+
): WorkspaceViewCollectionSkip | undefined => {
|
|
81
|
+
const lockPath =
|
|
82
|
+
app.root === '.' ? trailsLockFileName : `${app.root}/${trailsLockFileName}`;
|
|
83
|
+
return collectionSkips.find(
|
|
84
|
+
(skip) =>
|
|
85
|
+
skip.path === lockPath ||
|
|
86
|
+
(app.root !== '.' &&
|
|
87
|
+
(app.root === skip.path || app.root.startsWith(`${skip.path}/`)))
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Collect observation-only lock evidence without deriving workspace identity. */
|
|
92
|
+
export const collectWorkspaceLockCensus = (
|
|
93
|
+
identity: ReadTrailsProjectIdentityResult,
|
|
94
|
+
expectedLockPaths: ReadonlySet<string>,
|
|
95
|
+
lockScope: PathScope | undefined
|
|
96
|
+
): WorkspaceLockCensus => {
|
|
97
|
+
const aliases = deriveConfiguredAppAliases(identity);
|
|
98
|
+
const authoredLockPathByPhysicalPath = new Map(
|
|
99
|
+
aliases.map((alias) => [alias.physicalLockPath, alias.authoredLockPath])
|
|
100
|
+
);
|
|
101
|
+
const safeAliasEntryPaths = new Set(
|
|
102
|
+
aliases.flatMap((alias) => [
|
|
103
|
+
...alias.aliasEntryPaths,
|
|
104
|
+
...alias.physicalAliasEntryPaths,
|
|
105
|
+
])
|
|
106
|
+
);
|
|
107
|
+
const configuredAppRootDirectories = new Map(
|
|
108
|
+
aliases.map((alias) => [alias.authoredLockPath, alias.physicalRootDir])
|
|
109
|
+
);
|
|
110
|
+
// Config supports app roots below ignored ancestry, so descent toward a
|
|
111
|
+
// configured root stays observable here too. The exemption reaches only
|
|
112
|
+
// ancestors of a configured root, or the root itself. Ignored-named
|
|
113
|
+
// directories below a configured root stay pruned, so an app's own
|
|
114
|
+
// dependency tree never enters the census as unowned lock evidence. Config's
|
|
115
|
+
// own collection still carries the wider two-arm profile; narrowing it there
|
|
116
|
+
// is a separate follow-up. Workspace-root apps never need the exemption;
|
|
117
|
+
// their lock is already outside every ignored directory.
|
|
118
|
+
const configuredRootPaths = [
|
|
119
|
+
...identity.apps.map((app) => app.root),
|
|
120
|
+
...aliases.map((alias) => alias.physicalRoot),
|
|
121
|
+
].filter((root) => root !== '.');
|
|
122
|
+
const collection = collectSourceTree(identity.rootDir, {
|
|
123
|
+
classify: (entry) => {
|
|
124
|
+
if (entry.kind === 'directory') {
|
|
125
|
+
if (
|
|
126
|
+
DEFAULT_IGNORED_DIRECTORIES.has(entry.name) &&
|
|
127
|
+
!configuredRootPaths.some((root) =>
|
|
128
|
+
containsRelativePath(entry.path, root)
|
|
129
|
+
)
|
|
130
|
+
) {
|
|
131
|
+
return { action: 'skip', reason: 'ignored-directory' };
|
|
132
|
+
}
|
|
133
|
+
if (matchesAnyPathGlob(entry.path, lockScope?.exclude)) {
|
|
134
|
+
return { action: 'skip', reason: 'scope-excluded' };
|
|
135
|
+
}
|
|
136
|
+
return { action: 'recurse' };
|
|
137
|
+
}
|
|
138
|
+
if (matchesAnyPathGlob(entry.path, lockScope?.exclude)) {
|
|
139
|
+
return { action: 'skip', reason: 'scope-excluded' };
|
|
140
|
+
}
|
|
141
|
+
if (entry.kind !== 'file') {
|
|
142
|
+
return { action: 'skip', reason: 'unsupported-entry' };
|
|
143
|
+
}
|
|
144
|
+
if (entry.name !== trailsLockFileName) {
|
|
145
|
+
return { action: 'skip', reason: 'not-lock-artifact' };
|
|
146
|
+
}
|
|
147
|
+
const scopedPath =
|
|
148
|
+
authoredLockPathByPhysicalPath.get(entry.path) ?? entry.path;
|
|
149
|
+
return includedByPathScope(scopedPath, lockScope)
|
|
150
|
+
? { action: 'collect' }
|
|
151
|
+
: { action: 'skip', reason: 'scope-excluded' };
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
if (collection === null) {
|
|
155
|
+
throw new NotFoundError(
|
|
156
|
+
`Cannot observe workspace collection root ${identity.rootDir}.`,
|
|
157
|
+
{ context: { rootDir: identity.rootDir } }
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const observedSkips = [
|
|
162
|
+
...collection.skipped
|
|
163
|
+
.filter(
|
|
164
|
+
(skip) =>
|
|
165
|
+
skip.reason !== 'not-lock-artifact' &&
|
|
166
|
+
!(
|
|
167
|
+
skip.reason === 'unsupported-entry' &&
|
|
168
|
+
safeAliasEntryPaths.has(skip.path)
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
.flatMap((skip): WorkspaceViewCollectionSkip[] => {
|
|
172
|
+
const observed = {
|
|
173
|
+
path: skip.path,
|
|
174
|
+
provenance: 'source-collection' as const,
|
|
175
|
+
reason: skip.reason,
|
|
176
|
+
};
|
|
177
|
+
const translated = aliases.flatMap(
|
|
178
|
+
(alias): WorkspaceViewCollectionSkip[] =>
|
|
179
|
+
alias.physicalRoot === skip.path ||
|
|
180
|
+
alias.physicalRoot.startsWith(`${skip.path}/`) ||
|
|
181
|
+
alias.physicalLockPath === skip.path ||
|
|
182
|
+
[...alias.physicalAliasEntryPaths].some(
|
|
183
|
+
(path) => path === skip.path || path.startsWith(`${skip.path}/`)
|
|
184
|
+
)
|
|
185
|
+
? [
|
|
186
|
+
{
|
|
187
|
+
...observed,
|
|
188
|
+
path:
|
|
189
|
+
alias.physicalLockPath === skip.path
|
|
190
|
+
? alias.authoredLockPath
|
|
191
|
+
: alias.authoredRoot,
|
|
192
|
+
},
|
|
193
|
+
]
|
|
194
|
+
: []
|
|
195
|
+
);
|
|
196
|
+
return [observed, ...translated];
|
|
197
|
+
}),
|
|
198
|
+
...identity.apps.flatMap((app): WorkspaceViewCollectionSkip[] =>
|
|
199
|
+
aliases.some((alias) => alias.authoredRoot === app.root) ||
|
|
200
|
+
![...safeAliasEntryPaths].some(
|
|
201
|
+
(path) => app.root === path || app.root.startsWith(`${path}/`)
|
|
202
|
+
)
|
|
203
|
+
? []
|
|
204
|
+
: [
|
|
205
|
+
{
|
|
206
|
+
path: app.root,
|
|
207
|
+
provenance: 'source-collection',
|
|
208
|
+
reason: 'unsupported-entry',
|
|
209
|
+
},
|
|
210
|
+
]
|
|
211
|
+
),
|
|
212
|
+
];
|
|
213
|
+
const syntheticScopeSkipPaths = new Set<string>();
|
|
214
|
+
const syntheticScopeSkips = [...expectedLockPaths]
|
|
215
|
+
.toSorted()
|
|
216
|
+
.flatMap((lockPath): WorkspaceViewCollectionSkip[] => {
|
|
217
|
+
const scopeExcludedPath = scopeExcludedPathForExpectedLock(
|
|
218
|
+
lockPath,
|
|
219
|
+
lockScope
|
|
220
|
+
);
|
|
221
|
+
if (
|
|
222
|
+
scopeExcludedPath === undefined ||
|
|
223
|
+
syntheticScopeSkipPaths.has(scopeExcludedPath) ||
|
|
224
|
+
observedSkips.some(
|
|
225
|
+
(skip) =>
|
|
226
|
+
skip.path === lockPath || lockPath.startsWith(`${skip.path}/`)
|
|
227
|
+
)
|
|
228
|
+
) {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
syntheticScopeSkipPaths.add(scopeExcludedPath);
|
|
232
|
+
return [
|
|
233
|
+
{
|
|
234
|
+
path: scopeExcludedPath,
|
|
235
|
+
provenance: 'source-collection',
|
|
236
|
+
reason: 'scope-excluded',
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
});
|
|
240
|
+
const syntheticAliasScopeSkips = aliases.flatMap(
|
|
241
|
+
(alias): WorkspaceViewCollectionSkip[] => {
|
|
242
|
+
const physicalExclusion = [
|
|
243
|
+
...[...alias.physicalAliasEntryPaths].flatMap((path) => [
|
|
244
|
+
{ authoredPath: alias.authoredRoot, path },
|
|
245
|
+
{
|
|
246
|
+
authoredPath: alias.authoredLockPath,
|
|
247
|
+
path: `${path}/${trailsLockFileName}`,
|
|
248
|
+
},
|
|
249
|
+
]),
|
|
250
|
+
{
|
|
251
|
+
authoredPath: alias.authoredLockPath,
|
|
252
|
+
path: alias.physicalLockPath,
|
|
253
|
+
},
|
|
254
|
+
]
|
|
255
|
+
.map((candidate) => ({
|
|
256
|
+
...candidate,
|
|
257
|
+
excludedPath: excludedPathForLock(candidate.path, lockScope),
|
|
258
|
+
}))
|
|
259
|
+
.find((candidate) => candidate.excludedPath !== undefined);
|
|
260
|
+
if (physicalExclusion?.excludedPath === undefined) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
return [
|
|
264
|
+
{
|
|
265
|
+
path: physicalExclusion.excludedPath,
|
|
266
|
+
provenance: 'source-collection',
|
|
267
|
+
reason: 'scope-excluded',
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
path: physicalExclusion.authoredPath,
|
|
271
|
+
provenance: 'source-collection',
|
|
272
|
+
reason: 'scope-excluded',
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
const collectionSkips = [
|
|
278
|
+
...new Map(
|
|
279
|
+
[
|
|
280
|
+
...observedSkips,
|
|
281
|
+
...syntheticScopeSkips,
|
|
282
|
+
...syntheticAliasScopeSkips,
|
|
283
|
+
].map((skip) => [`${skip.path}\0${skip.reason}`, skip])
|
|
284
|
+
).values(),
|
|
285
|
+
].toSorted(compareCollectionSkips);
|
|
286
|
+
const unownedLocks = collection.files
|
|
287
|
+
.filter((file) => {
|
|
288
|
+
const authoredPath =
|
|
289
|
+
authoredLockPathByPhysicalPath.get(file.path) ?? file.path;
|
|
290
|
+
return !expectedLockPaths.has(authoredPath);
|
|
291
|
+
})
|
|
292
|
+
.map((file): UnownedWorkspaceLockObservation => {
|
|
293
|
+
const rootAggregate = file.path === trailsLockFileName;
|
|
294
|
+
return {
|
|
295
|
+
coaching: rootAggregate
|
|
296
|
+
? 'Remove the workspace-root trails.lock; configured workspaces use app-root locks only.'
|
|
297
|
+
: `Declare the lock-owning app root for ${file.path} in workspace.apps, or remove the lock if it is not an app artifact.`,
|
|
298
|
+
kind: rootAggregate
|
|
299
|
+
? 'forbidden-workspace-aggregate'
|
|
300
|
+
: 'unconfigured-app-lock',
|
|
301
|
+
path: file.path,
|
|
302
|
+
provenance: 'source-collection',
|
|
303
|
+
};
|
|
304
|
+
});
|
|
305
|
+
return { collectionSkips, configuredAppRootDirectories, unownedLocks };
|
|
306
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { PathScope } from '@ontrails/core';
|
|
2
|
+
import type { ReadTrailsProjectIdentityResult } from '@ontrails/config';
|
|
3
|
+
|
|
4
|
+
import type { TopoGraph, TopoGraphEntry } from './types.js';
|
|
5
|
+
|
|
6
|
+
/** Version of the canonical workspace-view hash contract. */
|
|
7
|
+
export const WORKSPACE_VIEW_SCHEMA_VERSION = 1 as const;
|
|
8
|
+
|
|
9
|
+
/** One Config-owned app and its complete saved graph. */
|
|
10
|
+
export interface WorkspaceViewApp {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly root: string;
|
|
13
|
+
readonly topoGraph: TopoGraph;
|
|
14
|
+
readonly topoGraphHash: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** One graph identity owned by more than one configured app. */
|
|
18
|
+
export interface WorkspaceViewCollision {
|
|
19
|
+
readonly appIds: readonly string[];
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly kind: TopoGraphEntry['kind'];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Canonical graph content. Machine-local observation facts do not enter it. */
|
|
25
|
+
export interface WorkspaceViewContent {
|
|
26
|
+
readonly apps: readonly WorkspaceViewApp[];
|
|
27
|
+
readonly collisions: readonly WorkspaceViewCollision[];
|
|
28
|
+
readonly workspaceViewSchemaVersion: typeof WORKSPACE_VIEW_SCHEMA_VERSION;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type WorkspaceAppLockStatus =
|
|
32
|
+
| 'available'
|
|
33
|
+
| 'invalid'
|
|
34
|
+
| 'missing'
|
|
35
|
+
| 'unavailable';
|
|
36
|
+
export type WorkspaceAppLockBinding = 'matched' | 'mismatched' | 'unavailable';
|
|
37
|
+
export type WorkspaceAppLockFreshness =
|
|
38
|
+
| 'fresh'
|
|
39
|
+
| 'stale'
|
|
40
|
+
| 'unknown'
|
|
41
|
+
| 'unavailable';
|
|
42
|
+
|
|
43
|
+
/** Evidence for one configured app's expected app-local lock. */
|
|
44
|
+
export interface WorkspaceAppLockObservation {
|
|
45
|
+
readonly actualAppId?: string | undefined;
|
|
46
|
+
readonly binding: WorkspaceAppLockBinding;
|
|
47
|
+
readonly coaching?: string | undefined;
|
|
48
|
+
readonly detail?: string | undefined;
|
|
49
|
+
readonly freshness: WorkspaceAppLockFreshness;
|
|
50
|
+
readonly id: string;
|
|
51
|
+
readonly lockPath: string;
|
|
52
|
+
readonly provenance: 'configured-app-lock';
|
|
53
|
+
readonly root: string;
|
|
54
|
+
readonly selected: boolean;
|
|
55
|
+
readonly status: WorkspaceAppLockStatus;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A lock found inside the collection but not owned by `workspace.apps`. */
|
|
59
|
+
export interface UnownedWorkspaceLockObservation {
|
|
60
|
+
readonly coaching: string;
|
|
61
|
+
readonly kind: 'forbidden-workspace-aggregate' | 'unconfigured-app-lock';
|
|
62
|
+
readonly path: string;
|
|
63
|
+
readonly provenance: 'source-collection';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A collection edge or policy skip observed while performing the lock census. */
|
|
67
|
+
export interface WorkspaceViewCollectionSkip {
|
|
68
|
+
readonly path: string;
|
|
69
|
+
readonly provenance: 'source-collection';
|
|
70
|
+
readonly reason: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Non-canonical evidence about the current workspace observation. */
|
|
74
|
+
export interface WorkspaceViewEvidence {
|
|
75
|
+
readonly apps: readonly WorkspaceAppLockObservation[];
|
|
76
|
+
readonly collectionSkips: readonly WorkspaceViewCollectionSkip[];
|
|
77
|
+
readonly configuredAppIds: readonly string[];
|
|
78
|
+
readonly configuredCompleteness: 'complete' | 'partial';
|
|
79
|
+
readonly selectedAppIds: readonly string[];
|
|
80
|
+
readonly selectedCompleteness: 'complete' | 'partial';
|
|
81
|
+
readonly unownedLocks: readonly UnownedWorkspaceLockObservation[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** One canonical app-partitioned graph view plus current observation evidence. */
|
|
85
|
+
export interface WorkspaceView {
|
|
86
|
+
readonly content: WorkspaceViewContent;
|
|
87
|
+
readonly evidence: WorkspaceViewEvidence;
|
|
88
|
+
/** Null when a missing, invalid, or contradictory lock makes content partial. */
|
|
89
|
+
readonly workspaceViewHash: string | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface DeriveWorkspaceViewOptions {
|
|
93
|
+
/** Config-owned workspace identity returned by `readTrailsProjectIdentity()`. */
|
|
94
|
+
readonly identity: ReadTrailsProjectIdentityResult;
|
|
95
|
+
/**
|
|
96
|
+
* Optional current graph hashes used to prove freshness. An omitted app stays
|
|
97
|
+
* `unknown`; lock integrity is always verified independently.
|
|
98
|
+
*/
|
|
99
|
+
readonly currentAppGraphHashes?: Readonly<Record<string, string>> | undefined;
|
|
100
|
+
/** Scope for the observation-only lock census. */
|
|
101
|
+
readonly lockScope?: PathScope | undefined;
|
|
102
|
+
/** Configured app IDs selected by the caller. Defaults to every app. */
|
|
103
|
+
readonly selectedAppIds?: readonly string[] | undefined;
|
|
104
|
+
}
|