deepline 0.3.90 → 0.3.91

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 (87) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +101 -6
  2. package/dist/bundling-sources/sdk/src/monitor-fleet-contract.ts +2 -556
  3. package/dist/bundling-sources/sdk/src/monitor-fleets.ts +2 -2
  4. package/dist/bundling-sources/sdk/src/release.ts +7 -12
  5. package/dist/bundling-sources/sdk/src/types.ts +6 -1
  6. package/dist/bundling-sources/shared_libs/integration-contracts/monitor-fleet-contract.ts +549 -0
  7. package/dist/bundling-sources/shared_libs/monitors/fleets/errors.ts +96 -0
  8. package/dist/bundling-sources/shared_libs/monitors/fleets/limits.ts +8 -0
  9. package/dist/bundling-sources/shared_libs/monitors/fleets/plan.ts +320 -0
  10. package/dist/bundling-sources/shared_libs/monitors/fleets/verdict.ts +106 -0
  11. package/dist/bundling-sources/shared_libs/monitors/http-error-response.ts +5 -1
  12. package/dist/bundling-sources/shared_libs/monitors/monitor-fleet-limits.ts +2 -12
  13. package/dist/bundling-sources/shared_libs/monitors/package.json +107 -5
  14. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +12 -7
  15. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +36 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +1 -1
  17. package/dist/bundling-sources/shared_libs/play-runtime/db-session-lease-store.ts +578 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-execution-relay.ts +521 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-receipt-gateway.ts +1378 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/runtime-topology.ts +1024 -0
  21. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-gateway-route-probe.ts +193 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machine-lifecycle.ts +104 -0
  23. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +1844 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +21 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/deployment/process-tree.ts +40 -0
  26. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-drain.ts +262 -0
  27. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-surface.ts +98 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-topology.ts +303 -0
  29. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-tuple-publication-guardian.ts +70 -0
  30. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-tuple-recovery.ts +519 -0
  31. package/dist/bundling-sources/shared_libs/play-runtime/deployment/vercel-fly-rollback-alignment.ts +280 -0
  32. package/dist/bundling-sources/shared_libs/play-runtime/deployment/vercel-production-gate.ts +159 -0
  33. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/memory-store.ts +114 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +44 -5
  35. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/child-play-resolver.ts +26 -0
  36. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +3662 -0
  37. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/progress-sidecar.ts +47 -0
  38. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +84 -0
  39. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-gateway-transport.ts +91 -0
  40. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-heartbeat.ts +424 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +321 -0
  42. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-sheet-persistence-retry.ts +110 -0
  43. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-terminal-push.ts +625 -0
  44. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +46 -3
  45. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +1 -1
  46. package/dist/bundling-sources/shared_libs/play-runtime/webhook-receipt-retry.ts +50 -0
  47. package/dist/bundling-sources/shared_libs/plays/bundling/repository-adapter.ts +24 -0
  48. package/dist/bundling-sources/shared_libs/plays/child-run-projection.ts +174 -0
  49. package/dist/bundling-sources/shared_libs/plays/deploy/api-client.ts +267 -0
  50. package/dist/bundling-sources/shared_libs/plays/deploy/control-plane-types.ts +73 -0
  51. package/dist/bundling-sources/shared_libs/plays/deploy/manifest.ts +162 -0
  52. package/dist/bundling-sources/shared_libs/plays/deploy/project-serializer.ts +87 -0
  53. package/dist/bundling-sources/shared_libs/plays/deploy/source-file-path.ts +26 -0
  54. package/dist/bundling-sources/shared_libs/plays/deploy/target-paths.ts +36 -0
  55. package/dist/bundling-sources/shared_libs/plays/launch/input-contract.ts +400 -0
  56. package/dist/bundling-sources/shared_libs/plays/launch/prepare.ts +96 -0
  57. package/dist/bundling-sources/shared_libs/plays/local-file-discovery.ts +385 -0
  58. package/dist/bundling-sources/shared_libs/plays/play-grep-search.ts +79 -6
  59. package/dist/bundling-sources/shared_libs/plays/play-reference.ts +117 -0
  60. package/dist/bundling-sources/shared_libs/plays/run-input-summary.ts +260 -0
  61. package/dist/bundling-sources/shared_libs/plays/run-summary-types.ts +49 -0
  62. package/dist/bundling-sources/shared_libs/plays/secrets/errors.ts +19 -0
  63. package/dist/bundling-sources/shared_libs/plays/sheet-filter.ts +347 -0
  64. package/dist/bundling-sources/shared_libs/plays/standard-webhooks.ts +117 -0
  65. package/dist/bundling-sources/shared_libs/plays/status-result.ts +388 -0
  66. package/dist/bundling-sources/shared_libs/plays/transform-factor-structure.ts +377 -0
  67. package/dist/bundling-sources/shared_libs/plays/webhook-payload.ts +113 -0
  68. package/dist/cli/index.js +369 -162
  69. package/dist/cli/index.mjs +369 -162
  70. package/dist/{compiler-manifest-BaJfE-5H.d.mts → compiler-manifest-x14KL83E.d.mts} +1 -1
  71. package/dist/{compiler-manifest-BaJfE-5H.d.ts → compiler-manifest-x14KL83E.d.ts} +1 -1
  72. package/dist/index.d.mts +45 -8
  73. package/dist/index.d.ts +45 -8
  74. package/dist/index.js +61 -14
  75. package/dist/index.mjs +61 -14
  76. package/dist/install-integrity.json +54 -2
  77. package/dist/monitor-fleet-contract.d.mts +3 -10
  78. package/dist/monitor-fleet-contract.d.ts +3 -10
  79. package/dist/monitor-fleet-contract.js +2 -0
  80. package/dist/monitor-fleet-contract.mjs +1 -1
  81. package/dist/plays/bundle-play-file.d.mts +2 -2
  82. package/dist/plays/bundle-play-file.d.ts +2 -2
  83. package/dist/release.d.mts +3 -3
  84. package/dist/release.d.ts +3 -3
  85. package/dist/release.js +7 -12
  86. package/dist/release.mjs +7 -12
  87. package/package.json +2 -2
@@ -0,0 +1,385 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile, stat } from 'node:fs/promises';
3
+ import {
4
+ extname,
5
+ resolve,
6
+ dirname,
7
+ isAbsolute,
8
+ join,
9
+ relative,
10
+ } from 'node:path';
11
+ import * as ts from 'typescript';
12
+
13
+ export interface PlayLocalFileReference {
14
+ sourceFragment: string;
15
+ logicalPath: string;
16
+ absolutePath: string;
17
+ bytes: number;
18
+ contentHash: string;
19
+ contentType: string;
20
+ }
21
+
22
+ export interface PlayLocalFileDiscoveryError {
23
+ sourceFragment: string;
24
+ message: string;
25
+ }
26
+
27
+ export interface PlayLocalFileDiscoveryResult {
28
+ files: PlayLocalFileReference[];
29
+ unresolved: PlayLocalFileDiscoveryError[];
30
+ }
31
+
32
+ type ConstMap = Map<string, string>;
33
+ const SOURCE_EXTENSIONS = [
34
+ '.ts',
35
+ '.tsx',
36
+ '.mts',
37
+ '.cts',
38
+ '.js',
39
+ '.jsx',
40
+ '.mjs',
41
+ '.cjs',
42
+ '.json',
43
+ ];
44
+ const TEXT_IMPORT_EXTENSIONS = new Set(['.md', '.txt']);
45
+
46
+ function isTextImportFile(filePath: string): boolean {
47
+ return TEXT_IMPORT_EXTENSIONS.has(extname(filePath));
48
+ }
49
+
50
+ function sha256(buffer: Buffer): string {
51
+ return createHash('sha256').update(buffer).digest('hex');
52
+ }
53
+
54
+ function contentTypeForFile(filePath: string): string {
55
+ const extension = extname(filePath).toLowerCase();
56
+ if (extension === '.csv') return 'text/csv';
57
+ if (extension === '.json') return 'application/json';
58
+ if (extension === '.txt') return 'text/plain';
59
+ return 'application/octet-stream';
60
+ }
61
+
62
+ function isCtxCsvCall(node: ts.CallExpression): boolean {
63
+ if (!ts.isPropertyAccessExpression(node.expression)) {
64
+ return false;
65
+ }
66
+ const target = node.expression.expression;
67
+ return (
68
+ ts.isIdentifier(target) &&
69
+ (target.text === 'ctx' || target.text.endsWith('Ctx')) &&
70
+ node.expression.name.text === 'csv'
71
+ );
72
+ }
73
+
74
+ function extractSourceFragment(source: string, node: ts.Node): string {
75
+ return source.slice(node.getStart(), node.getEnd()).trim();
76
+ }
77
+
78
+ function referencesInputIdentifier(node: ts.Node): boolean {
79
+ if (ts.isIdentifier(node) && node.text === 'input') {
80
+ return true;
81
+ }
82
+
83
+ return node.getChildren().some((child) => referencesInputIdentifier(child));
84
+ }
85
+
86
+ function isRuntimeInputExpression(node: ts.Expression): boolean {
87
+ if (ts.isPropertyAccessExpression(node)) {
88
+ return ts.isIdentifier(node.expression) && node.expression.text === 'input';
89
+ }
90
+ if (ts.isElementAccessExpression(node)) {
91
+ return ts.isIdentifier(node.expression) && node.expression.text === 'input';
92
+ }
93
+ if (ts.isIdentifier(node)) {
94
+ return node.text === 'input';
95
+ }
96
+
97
+ if (ts.isParenthesizedExpression(node)) {
98
+ return isRuntimeInputExpression(node.expression);
99
+ }
100
+
101
+ if (
102
+ ts.isBinaryExpression(node) &&
103
+ (node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken ||
104
+ node.operatorToken.kind === ts.SyntaxKind.BarBarToken)
105
+ ) {
106
+ return (
107
+ isRuntimeInputExpression(node.left) ||
108
+ isRuntimeInputExpression(node.right)
109
+ );
110
+ }
111
+
112
+ if (ts.isConditionalExpression(node)) {
113
+ return (
114
+ isRuntimeInputExpression(node.condition) ||
115
+ isRuntimeInputExpression(node.whenTrue) ||
116
+ isRuntimeInputExpression(node.whenFalse)
117
+ );
118
+ }
119
+
120
+ return referencesInputIdentifier(node);
121
+ }
122
+
123
+ function resolveStringExpression(
124
+ node: ts.Expression,
125
+ constants: ConstMap,
126
+ ): string | null {
127
+ if (
128
+ ts.isStringLiteralLike(node) ||
129
+ ts.isNoSubstitutionTemplateLiteral(node)
130
+ ) {
131
+ return node.text;
132
+ }
133
+
134
+ if (ts.isParenthesizedExpression(node)) {
135
+ return resolveStringExpression(node.expression, constants);
136
+ }
137
+
138
+ if (ts.isIdentifier(node)) {
139
+ return constants.get(node.text) ?? null;
140
+ }
141
+
142
+ if (ts.isTemplateExpression(node)) {
143
+ let value = node.head.text;
144
+ for (const span of node.templateSpans) {
145
+ const resolved = resolveStringExpression(span.expression, constants);
146
+ if (resolved == null) {
147
+ return null;
148
+ }
149
+ value += resolved + span.literal.text;
150
+ }
151
+ return value;
152
+ }
153
+
154
+ if (
155
+ ts.isBinaryExpression(node) &&
156
+ node.operatorToken.kind === ts.SyntaxKind.PlusToken
157
+ ) {
158
+ const left = resolveStringExpression(node.left, constants);
159
+ const right = resolveStringExpression(node.right, constants);
160
+ if (left == null || right == null) {
161
+ return null;
162
+ }
163
+ return left + right;
164
+ }
165
+
166
+ return null;
167
+ }
168
+
169
+ function collectTopLevelStringConstants(sourceFile: ts.SourceFile): ConstMap {
170
+ const constants: ConstMap = new Map();
171
+
172
+ for (const statement of sourceFile.statements) {
173
+ if (!ts.isVariableStatement(statement)) {
174
+ continue;
175
+ }
176
+
177
+ if (!(statement.declarationList.flags & ts.NodeFlags.Const)) {
178
+ continue;
179
+ }
180
+
181
+ for (const declaration of statement.declarationList.declarations) {
182
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer) {
183
+ continue;
184
+ }
185
+
186
+ const resolved = resolveStringExpression(
187
+ declaration.initializer,
188
+ constants,
189
+ );
190
+ if (resolved != null) {
191
+ constants.set(declaration.name.text, resolved);
192
+ }
193
+ }
194
+ }
195
+
196
+ return constants;
197
+ }
198
+
199
+ async function fileExists(filePath: string): Promise<boolean> {
200
+ try {
201
+ await stat(filePath);
202
+ return true;
203
+ } catch {
204
+ return false;
205
+ }
206
+ }
207
+
208
+ function isPathInsideDirectory(filePath: string, directory: string): boolean {
209
+ const relativePath = relative(directory, filePath);
210
+ return (
211
+ relativePath === '' ||
212
+ (!relativePath.startsWith('..') && !isAbsolute(relativePath))
213
+ );
214
+ }
215
+
216
+ async function resolveLocalImport(
217
+ fromFile: string,
218
+ specifier: string,
219
+ ): Promise<string> {
220
+ const base = isAbsolute(specifier)
221
+ ? resolve(specifier)
222
+ : resolve(dirname(fromFile), specifier);
223
+ const candidates: string[] = [base];
224
+ const explicitExtension = extname(base).toLowerCase();
225
+
226
+ if (!explicitExtension) {
227
+ candidates.push(
228
+ ...SOURCE_EXTENSIONS.map((extension) => `${base}${extension}`),
229
+ );
230
+ candidates.push(
231
+ ...SOURCE_EXTENSIONS.map((extension) => join(base, `index${extension}`)),
232
+ );
233
+ } else if (['.js', '.jsx', '.mjs', '.cjs'].includes(explicitExtension)) {
234
+ const stem = base.slice(0, -explicitExtension.length);
235
+ candidates.push(
236
+ ...SOURCE_EXTENSIONS.map((extension) => `${stem}${extension}`),
237
+ );
238
+ }
239
+
240
+ for (const candidate of candidates) {
241
+ if (await fileExists(candidate)) {
242
+ return candidate;
243
+ }
244
+ }
245
+
246
+ throw new Error(
247
+ `Could not resolve local import "${specifier}" from ${fromFile}`,
248
+ );
249
+ }
250
+
251
+ export async function discoverPackagedLocalFiles(
252
+ entryFile: string,
253
+ ): Promise<PlayLocalFileDiscoveryResult> {
254
+ const absoluteEntryFile = resolve(entryFile);
255
+ const packagingRoot = dirname(absoluteEntryFile);
256
+ const files = new Map<string, PlayLocalFileReference>();
257
+ const unresolved: PlayLocalFileDiscoveryError[] = [];
258
+ const visitedFiles = new Set<string>();
259
+
260
+ const visitSourceFile = async (filePath: string): Promise<void> => {
261
+ const absolutePath = resolve(filePath);
262
+ if (visitedFiles.has(absolutePath)) {
263
+ return;
264
+ }
265
+ visitedFiles.add(absolutePath);
266
+
267
+ const sourceCode = await readFile(absolutePath, 'utf-8');
268
+ if (isTextImportFile(absolutePath)) {
269
+ return;
270
+ }
271
+ const sourceFile = ts.createSourceFile(
272
+ absolutePath,
273
+ sourceCode,
274
+ ts.ScriptTarget.Latest,
275
+ true,
276
+ ts.ScriptKind.TS,
277
+ );
278
+ const constants = collectTopLevelStringConstants(sourceFile);
279
+ const childVisits: Promise<void>[] = [];
280
+
281
+ const visitNode = async (node: ts.Node): Promise<void> => {
282
+ if (ts.isCallExpression(node) && isCtxCsvCall(node)) {
283
+ const argument = node.arguments[0];
284
+ if (!argument) {
285
+ unresolved.push({
286
+ sourceFragment: 'ctx.csv()',
287
+ message:
288
+ 'ctx.csv() requires a file path string or input reference.',
289
+ });
290
+ } else if (!isRuntimeInputExpression(argument)) {
291
+ const resolvedPath = resolveStringExpression(argument, constants);
292
+ if (resolvedPath == null) {
293
+ unresolved.push({
294
+ sourceFragment: extractSourceFragment(sourceCode, argument),
295
+ message:
296
+ 'Could not resolve this ctx.csv(...) path at submit time. Use a string literal, a top-level const string, or pass a runtime input like input.file.',
297
+ });
298
+ } else {
299
+ const absoluteCsvPath = resolve(
300
+ dirname(absolutePath),
301
+ resolvedPath,
302
+ );
303
+ if (
304
+ isAbsolute(resolvedPath) ||
305
+ !isPathInsideDirectory(absoluteCsvPath, packagingRoot)
306
+ ) {
307
+ unresolved.push({
308
+ sourceFragment: extractSourceFragment(sourceCode, argument),
309
+ message:
310
+ 'ctx.csv(...) packaged file paths must be relative paths inside the play directory. Pass external files at runtime with input.file instead.',
311
+ });
312
+ return;
313
+ }
314
+ const buffer = await readFile(
315
+ /* turbopackIgnore: true */ absoluteCsvPath,
316
+ );
317
+ const stats = await stat(
318
+ /* turbopackIgnore: true */ absoluteCsvPath,
319
+ );
320
+ files.set(absoluteCsvPath, {
321
+ sourceFragment: extractSourceFragment(sourceCode, argument),
322
+ logicalPath: resolvedPath,
323
+ absolutePath: absoluteCsvPath,
324
+ bytes: stats.size,
325
+ contentHash: sha256(buffer),
326
+ contentType: contentTypeForFile(absoluteCsvPath),
327
+ });
328
+ }
329
+ }
330
+ }
331
+
332
+ if (
333
+ ts.isImportDeclaration(node) &&
334
+ !node.importClause?.isTypeOnly &&
335
+ ts.isStringLiteral(node.moduleSpecifier) &&
336
+ node.moduleSpecifier.text.startsWith('.')
337
+ ) {
338
+ childVisits.push(
339
+ resolveLocalImport(absolutePath, node.moduleSpecifier.text).then(
340
+ (resolvedImport) => visitSourceFile(resolvedImport),
341
+ ),
342
+ );
343
+ }
344
+
345
+ if (
346
+ ts.isCallExpression(node) &&
347
+ ts.isIdentifier(node.expression) &&
348
+ node.expression.text === 'require' &&
349
+ node.arguments.length === 1 &&
350
+ ts.isStringLiteral(node.arguments[0]) &&
351
+ node.arguments[0].text.startsWith('.')
352
+ ) {
353
+ childVisits.push(
354
+ resolveLocalImport(absolutePath, node.arguments[0].text).then(
355
+ (resolvedImport) => visitSourceFile(resolvedImport),
356
+ ),
357
+ );
358
+ }
359
+
360
+ await Promise.all(
361
+ node.getChildren(sourceFile).map((child) => visitNode(child)),
362
+ );
363
+ };
364
+
365
+ await visitNode(sourceFile);
366
+ await Promise.all(childVisits);
367
+ };
368
+
369
+ await visitSourceFile(absoluteEntryFile);
370
+
371
+ return {
372
+ files: [...files.values()],
373
+ unresolved,
374
+ };
375
+ }
376
+
377
+ export interface PlayStagedFileRef {
378
+ storageKind: 'r2';
379
+ storageKey: string;
380
+ logicalPath: string;
381
+ fileName: string;
382
+ contentHash: string;
383
+ contentType: string;
384
+ bytes: number;
385
+ }
@@ -1,21 +1,23 @@
1
1
  // Convex limits its final prefix-search term to 32 UTF-8 bytes, so the public
2
- // query must stay below that boundary. The corpus was already backfilled in
3
- // Convex's lowercase representation, so do not rewrite terms here: changing
4
- // its token boundaries would make historical rows require another data
5
- // migration. Reject an unsupported query at the API boundary rather than
6
- // silently returning an incomplete result or adding a second reader.
2
+ // query must stay below that boundary. Keep this normalization aligned with
3
+ // the indexed corpus and reject unsupported queries at the API boundary.
7
4
  export const CONVEX_PLAY_GREP_SEARCH_TERM_MAX_BYTES = 31;
8
5
  export const CONVEX_PLAY_GREP_SEARCH_MAX_TERMS = 16;
9
6
 
10
7
  const CONVEX_PLAY_GREP_SEARCH_TERM_PATTERN = /[\p{L}\p{N}]+/gu;
11
8
 
9
+ export type PlayGrepMatch = {
10
+ field: 'name' | 'description' | 'provider' | 'source';
11
+ terms: string[];
12
+ };
13
+
12
14
  /** Return searchable raw terms for the public response filter. */
13
15
  export function extractPlayGrepSearchTerms(value: string): string[] {
14
16
  return value.match(CONVEX_PLAY_GREP_SEARCH_TERM_PATTERN) ?? [];
15
17
  }
16
18
 
17
19
  /**
18
- * Match the immutable representation of the completed grep-corpus backfill.
20
+ * Match the indexed grep corpus representation.
19
21
  */
20
22
  export function normalizePlayGrepSearchQuery(value: string): string {
21
23
  return value.toLowerCase();
@@ -37,3 +39,74 @@ export function hasOverlongPlayGrepSearchTerm(value: string): boolean {
37
39
  encoder.encode(term).byteLength > CONVEX_PLAY_GREP_SEARCH_TERM_MAX_BYTES,
38
40
  );
39
41
  }
42
+
43
+ function searchableTerms(value: string | null | undefined): Set<string> {
44
+ return new Set(
45
+ extractPlayGrepSearchTerms(normalizePlayGrepSearchQuery(value ?? '')),
46
+ );
47
+ }
48
+
49
+ function matchingTerms(
50
+ queryTerms: readonly string[],
51
+ value: string | null | undefined,
52
+ ): string[] {
53
+ const valueTerms = searchableTerms(value);
54
+ const finalTermIndex = queryTerms.length - 1;
55
+ const hasTermPrefixMatch = (queryTerm: string) => {
56
+ for (const valueTerm of valueTerms) {
57
+ if (valueTerm.startsWith(queryTerm)) return true;
58
+ }
59
+ return false;
60
+ };
61
+ return [
62
+ ...new Set(
63
+ queryTerms.filter((queryTerm, index) =>
64
+ index === finalTermIndex
65
+ ? hasTermPrefixMatch(queryTerm)
66
+ : valueTerms.has(queryTerm),
67
+ ),
68
+ ),
69
+ ];
70
+ }
71
+
72
+ /**
73
+ * Explain an indexed result without returning authored source. The direct
74
+ * Convex index remains the sole matcher; this only reports which indexed
75
+ * field accounted for each accepted term.
76
+ */
77
+ export function buildPlayGrepMatches(input: {
78
+ query: string;
79
+ name: string;
80
+ description?: string | null;
81
+ providers?: readonly string[] | null;
82
+ }): PlayGrepMatch[] {
83
+ const queryTerms = extractPlayGrepSearchTerms(
84
+ normalizePlayGrepSearchQuery(input.query),
85
+ );
86
+ if (queryTerms.length === 0) return [];
87
+
88
+ const fields: Array<{
89
+ field: Exclude<PlayGrepMatch['field'], 'source'>;
90
+ value: string;
91
+ }> = [
92
+ { field: 'name', value: input.name },
93
+ { field: 'description', value: input.description ?? '' },
94
+ { field: 'provider', value: (input.providers ?? []).join(' ') },
95
+ ];
96
+ const matches: PlayGrepMatch[] = [];
97
+ const accountedFor = new Set<string>();
98
+ for (const { field, value } of fields) {
99
+ const terms = matchingTerms(queryTerms, value);
100
+ if (terms.length === 0) continue;
101
+ terms.forEach((term) => accountedFor.add(term));
102
+ matches.push({ field, terms });
103
+ }
104
+
105
+ const sourceTerms = [
106
+ ...new Set(queryTerms.filter((term) => !accountedFor.has(term))),
107
+ ];
108
+ if (sourceTerms.length > 0) {
109
+ matches.push({ field: 'source', terms: sourceTerms });
110
+ }
111
+ return matches;
112
+ }
@@ -0,0 +1,117 @@
1
+ export const PREBUILT_PLAY_NAMESPACE = 'prebuilt';
2
+ export const SELF_PLAY_NAMESPACE = 'self';
3
+
4
+ export type PlayReference =
5
+ | {
6
+ kind: 'bare';
7
+ reference: string;
8
+ name: string;
9
+ }
10
+ | {
11
+ kind: 'prebuilt';
12
+ reference: string;
13
+ name: string;
14
+ }
15
+ | {
16
+ kind: 'self';
17
+ reference: string;
18
+ name: string;
19
+ }
20
+ | {
21
+ kind: 'org';
22
+ reference: string;
23
+ ownerSlug: string;
24
+ name: string;
25
+ }
26
+ | {
27
+ kind: 'invalid';
28
+ reference: string;
29
+ };
30
+
31
+ export function buildPlayReference(
32
+ ownerSlug: string,
33
+ playName: string,
34
+ ): string {
35
+ return `${ownerSlug.trim()}/${playName.trim()}`;
36
+ }
37
+
38
+ export function buildPrebuiltPlayReference(playName: string): string {
39
+ return buildPlayReference(PREBUILT_PLAY_NAMESPACE, playName);
40
+ }
41
+
42
+ export function buildSelfPlayReference(playName: string): string {
43
+ return buildPlayReference(SELF_PLAY_NAMESPACE, playName);
44
+ }
45
+
46
+ export function parsePlayReference(reference: string): PlayReference {
47
+ const trimmed = reference.trim();
48
+ if (!trimmed) {
49
+ return { kind: 'invalid', reference: trimmed };
50
+ }
51
+
52
+ const segments = trimmed.split('/');
53
+ if (segments.length === 1) {
54
+ return { kind: 'bare', reference: trimmed, name: trimmed };
55
+ }
56
+ if (segments.length !== 2) {
57
+ return { kind: 'invalid', reference: trimmed };
58
+ }
59
+
60
+ const [rawOwnerSlug = '', rawName = ''] = segments;
61
+ const ownerSlug = rawOwnerSlug.trim();
62
+ const name = rawName.trim();
63
+ if (!ownerSlug || !name) {
64
+ return { kind: 'invalid', reference: trimmed };
65
+ }
66
+
67
+ const normalizedOwnerSlug = ownerSlug.toLowerCase();
68
+ if (normalizedOwnerSlug === PREBUILT_PLAY_NAMESPACE) {
69
+ return {
70
+ kind: 'prebuilt',
71
+ reference: buildPrebuiltPlayReference(name),
72
+ name,
73
+ };
74
+ }
75
+ if (normalizedOwnerSlug === SELF_PLAY_NAMESPACE) {
76
+ return {
77
+ kind: 'self',
78
+ reference: buildSelfPlayReference(name),
79
+ name,
80
+ };
81
+ }
82
+
83
+ return {
84
+ kind: 'org',
85
+ reference: buildPlayReference(ownerSlug, name),
86
+ ownerSlug,
87
+ name,
88
+ };
89
+ }
90
+
91
+ export function isPrebuiltPlayReference(reference: string): boolean {
92
+ return parsePlayReference(reference).kind === 'prebuilt';
93
+ }
94
+
95
+ /**
96
+ * Resolve every spelling that can identify a play owned by the current org to
97
+ * its stored bare name. This is the single ownership boundary shared by
98
+ * CLI/API lookup, preflight static-contract lookup, and runtime ctx.runPlay.
99
+ */
100
+ export function resolveOwnedPlayReferenceName(
101
+ reference: string,
102
+ ownerSlug?: string | null,
103
+ ): string | null {
104
+ const parsed = parsePlayReference(reference);
105
+ if (parsed.kind === 'bare' || parsed.kind === 'self') {
106
+ return parsed.name;
107
+ }
108
+ if (parsed.kind !== 'org') {
109
+ return null;
110
+ }
111
+
112
+ const normalizedOwnerSlug = ownerSlug?.trim().toLowerCase() ?? '';
113
+ return normalizedOwnerSlug &&
114
+ parsed.ownerSlug.trim().toLowerCase() === normalizedOwnerSlug
115
+ ? parsed.name
116
+ : null;
117
+ }