geospec 0.1.0-beta.7 → 0.1.0-beta.8

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 (52) hide show
  1. package/agent/geospec-authoring/SKILL.md +11 -11
  2. package/agent/geospec-authoring/api-functions.md +1 -16
  3. package/agent/geospec-authoring/api-index.md +50 -46
  4. package/agent/geospec-authoring/api-types.md +188 -168
  5. package/agent/geospec-authoring/public-api-functions.md +29 -22
  6. package/agent/geospec-authoring/public-api-index.md +196 -183
  7. package/agent/geospec-authoring/public-api-types-2.md +188 -220
  8. package/agent/geospec-authoring/public-api-types-3.md +112 -99
  9. package/agent/geospec-authoring/public-api-types.md +236 -125
  10. package/agent/resources.js +12 -12
  11. package/agent/skills.json +2 -2
  12. package/dist/config/index.d.mts +1 -2
  13. package/dist/config/index.mjs +1 -1
  14. package/dist/config/types.d.mts +0 -3
  15. package/dist/config/validate.mjs +1 -1
  16. package/dist/mesh/index.d.mts +2 -2
  17. package/dist/model/index.d.mts +3 -3
  18. package/dist/model/index.mjs +1 -1
  19. package/dist/model/mesh-evidence-matchers.d.mts +12 -0
  20. package/dist/model/mesh-evidence-matchers.mjs +1 -0
  21. package/dist/model/native-model-loader.d.mts +8 -8
  22. package/dist/model/native-model-loader.mjs +1 -1
  23. package/dist/model/node/command-exporter.d.mts +31 -0
  24. package/dist/model/node/command-exporter.mjs +1 -0
  25. package/dist/model/node/index.d.mts +2 -0
  26. package/dist/model/node/index.mjs +1 -0
  27. package/dist/model/types.d.mts +111 -63
  28. package/dist/runner/collector.d.mts +0 -2
  29. package/dist/runner/node/index.d.mts +2 -0
  30. package/dist/runner/node/index.mjs +1 -0
  31. package/dist/runner/node/node-vm-filesystem.d.mts +22 -0
  32. package/dist/runner/node/node-vm-filesystem.mjs +1 -0
  33. package/dist/runner/pool/events.mjs +1 -0
  34. package/dist/runner/pool/pool-messages.d.mts +0 -10
  35. package/dist/runner/pool/pool.d.mts +36 -0
  36. package/dist/runner/pool/pool.mjs +1 -0
  37. package/dist/runner/pool/shard-planner.mjs +1 -0
  38. package/dist/runner/pool/shard-timings.d.mts +36 -0
  39. package/dist/runner/pool/transport.mjs +1 -0
  40. package/dist/runner/pool/worker-host.d.mts +36 -0
  41. package/dist/runner/pool/worker-host.mjs +1 -0
  42. package/dist/runner/run-geospec-module.mjs +1 -1
  43. package/dist/runner/types.d.mts +0 -4
  44. package/dist/runner/worker/index.d.mts +5 -2
  45. package/dist/runner/worker/index.mjs +1 -1
  46. package/dist/runner/worker/runner-types.d.mts +2 -14
  47. package/dist/runner/worker/serial-runner.mjs +1 -1
  48. package/package.json +14 -4
  49. package/dist/config/tau-project-artifact.d.mts +0 -54
  50. package/dist/config/tau-project-artifact.mjs +0 -3
  51. package/dist/model/export-intent.d.mts +0 -79
  52. package/dist/model/export-intent.mjs +0 -1
@@ -0,0 +1 @@
1
+ import{GeoSpecModelLoadError as e}from"../errors.mjs";import{z as t}from"zod";import{mkdtemp as n,readFile as r,rm as i}from"node:fs/promises";import{isAbsolute as a,join as o,relative as s,resolve as c,sep as l}from"node:path";import{spawn as u}from"node:child_process";import{tmpdir as d}from"node:os";import f from"node:process";const p=t.object({protocol:t.literal(1),error:t.object({code:t.string().min(1),message:t.string(),suggestion:t.string().optional()})}),m=t.object({protocol:t.literal(1),format:t.enum([`glb`,`step`]),sourceUnit:t.string().optional(),primary:t.string().min(1),resources:t.array(t.object({name:t.string().min(1),path:t.string().min(1)})).optional(),consumedSources:t.array(t.object({path:t.string().min(1),sha256:t.string().min(1)}))}),h=({id:t,file:n,reason:r,details:i={}})=>new e([{code:`GEOSPEC_EXPORTER_FAILED`,severity:`error`,message:`Exporter '${t}' failed to export '${n}': ${r}`,suggestion:`Run the exporter command by hand with the same arguments and fix what it reports.`,details:{exporter:t,file:n,...i}}]),g=(e,t)=>{let n=c(e,t),r=s(e,n);return r===``||r===`..`||r.startsWith(`..${l}`)||a(r)?void 0:n},_=async e=>{let t=await r(e);return new Uint8Array(t.buffer,t.byteOffset,t.byteLength)},v=(e,t,n)=>[`--file`,e.file,`--format`,e.format,`--project`,t,`--out`,n,...e.parameters===void 0?[]:[`--parameters-json`,JSON.stringify(e.parameters)],...e.mesh?.linearTolerance===void 0?[]:[`--mesh-linear-tolerance`,String(e.mesh.linearTolerance)],...e.mesh?.angularToleranceDegrees===void 0?[]:[`--mesh-angular-tolerance-degrees`,String(e.mesh.angularToleranceDegrees)],...e.region===void 0?[]:[`--region-min`,e.region.min.join(`,`),`--region-max`,e.region.max.join(`,`)]],y=e=>{let t=new TextDecoder;return e.map(e=>t.decode(e,{stream:!0})).join(``)+t.decode()},b=async(e,t,n)=>new Promise((r,i)=>{let a=u(e,t,{cwd:n.cwd,env:n.env,stdio:[`ignore`,`pipe`,`pipe`],...n.signal===void 0?{}:{signal:n.signal}}),o=[],s=[];a.stdout.on(`data`,e=>o.push(e)),a.stderr.on(`data`,e=>s.push(e)),a.once(`error`,i),a.once(`close`,e=>{r({exitCode:e??void 0,stdout:y(o),stderr:y(s)})})}),x=t=>{let[r,...a]=t.command;if(t.id===``||t.extensions.length===0||r===void 0||r===``)throw TypeError(`commandExporter needs an id, at least one extension and a command.`);if(t.capabilities.formats.length===0)throw TypeError(`commandExporter '${t.id}' must declare at least one format.`);if(t.capabilities.inlineSources===!0)throw TypeError(`commandExporter '${t.id}' cannot accept inline sources: protocol 1 passes files only.`);let{id:s}=t;return{id:s,extensions:[...t.extensions],capabilities:{...t.capabilities,formats:[...t.capabilities.formats]},async export(c){let l=c.projectPath??t.cwd??f.cwd(),u=await n(o(d(),`geospec-export-`));try{let n;try{n=await b(r,[...a,...v(c,l,u)],{cwd:t.cwd??l,env:{...f.env,...t.env},signal:c.signal})}catch(e){throw h({id:s,file:c.file,reason:`the command could not run (${e instanceof Error?e.message:String(e)}).`})}let i=n.stderr.slice(-2e3),o;try{o=JSON.parse(n.stdout)}catch{throw h({id:s,file:c.file,reason:`it printed no protocol 1 JSON document (exit ${String(n.exitCode)}).`,details:{exitCode:n.exitCode,stderr:i}})}let d=p.safeParse(o);if(d.success){let{code:t,message:n,suggestion:r}=d.data.error;throw new e([{code:t,severity:`error`,message:n,...r===void 0?{}:{suggestion:r},details:{exporter:s,file:c.file}}])}let y=m.safeParse(o);if(n.exitCode!==0||!y.success)throw h({id:s,file:c.file,reason:n.exitCode===0?`its JSON document is not a protocol 1 response.`:`it exited ${String(n.exitCode)} without a protocol 1 error.`,details:{exitCode:n.exitCode,stderr:i}});let x=y.data,S=g(u,x.primary),C=(x.resources??[]).flatMap(({name:e,path:t})=>{let n=g(u,t);return n===void 0?[]:[{name:e,path:n}]});if(S===void 0||C.length!==(x.resources??[]).length)throw h({id:s,file:c.file,reason:`it named an artifact outside its output directory.`});let[w,T]=await Promise.all([_(S),Promise.all(C.map(async({name:e,path:t})=>({name:e,bytes:await _(t)})))]);return{bytes:w,format:x.format,...x.sourceUnit===void 0?{}:{sourceUnit:x.sourceUnit},...T.length===0?{}:{resources:T},consumedSources:x.consumedSources}}finally{await i(u,{recursive:!0,force:!0})}}}};export{x as commandExporter};
@@ -0,0 +1,2 @@
1
+ import { CommandExporterOptions, commandExporter } from "./command-exporter.mjs";
2
+ export { type CommandExporterOptions, commandExporter };
@@ -0,0 +1 @@
1
+ import{commandExporter as e}from"./command-exporter.mjs";export{e as commandExporter};
@@ -1,9 +1,8 @@
1
1
  import { GeoSpecUnit } from "../geometry-unit.mjs";
2
2
  import { StepSource, StepStreamingMode } from "../step/types.mjs";
3
- import { MeshFileFormat } from "../mesh/types.mjs";
3
+ import { GeometryDiagnostic, MeshFileFormat } from "../mesh/types.mjs";
4
4
  import { GeoSpecSubject } from "./subject.mjs";
5
5
  import { GeoSpecNativeModelEngine, GeoSpecNativeSourceReader } from "./native-model-loader.mjs";
6
- import { RuntimeClient } from "@taucad/runtime";
7
6
  //#region src/model/types.d.ts
8
7
  /**
9
8
  * In-memory triangle mesh source.
@@ -29,49 +28,110 @@ type MeshSource = Uint8Array<ArrayBuffer> | ArrayBuffer | Blob | File | URL | st
29
28
  */
30
29
  type GeoSpecModelFormat = MeshFileFormat | 'step' | 'stp';
31
30
  /**
32
- * Runtime client surface consumed by `geospec/model`.
31
+ * Formats a {@link GeoSpecModelExporter} may produce from a source-kind file.
33
32
  *
34
- * GeoSpec accepts concrete Tau runtime clients from multiple call sites but
35
- * only needs connection lifecycle and request-scoped documents. Keep this shape
36
- * small so typed runtime clients do not have to widen their full generic
37
- * method surface to GeoSpec's testing DSL. A client with `shutdown` is awaited
38
- * through it when GeoSpec releases an owned runtime, so its host resources are
39
- * closed before the caller (for example a worker thread) exits. A client with
40
- * `snapshotSource` lets one run reuse an identical file load after confirming
41
- * that the source closure it exported is unchanged.
33
+ * @public
34
+ */
35
+ type GeoSpecExportFormat = Extract<GeoSpecModelFormat, 'glb' | 'step'>;
36
+ /**
37
+ * What a {@link GeoSpecModelExporter} declares it can do. The loader refuses a load the exporter
38
+ * cannot serve with `GEOSPEC_EXPORTER_UNSUPPORTED` before it calls `export`.
42
39
  *
43
40
  * @public
44
41
  */
45
- type GeoSpecRuntimeClient = Pick<RuntimeClient, 'connect' | 'terminate'> & Partial<Pick<RuntimeClient, 'shutdown' | 'snapshotSource' | 'describe'>> & {
46
- open: (input: Pick<Parameters<RuntimeClient['open']>[0], 'source' | 'parameters' | 'stage' | 'watch' | 'signal'>) => Pick<ReturnType<RuntimeClient['open']>, 'export' | 'close'>;
47
- on?(event: 'telemetry', handler: (batch: {
48
- readonly entries: ReadonlyArray<{
49
- name: string;
50
- duration: number;
51
- startTime: number;
52
- workerTimeOrigin: number;
53
- }>;
54
- }) => void): () => void;
42
+ type GeoSpecExporterCapabilities = {
43
+ /** The formats `export` produces; at least one. A GLB-only exporter refuses STEP loads up front. */
44
+ readonly formats: readonly GeoSpecExportFormat[];
45
+ /** Accepts `parameters`. Without it, a load that passes parameters refuses. */
46
+ readonly parameters?: boolean;
47
+ /** Crops to `region` (an axis-aligned millimetre box) before export. Without it, a region load refuses. */
48
+ readonly region?: boolean;
49
+ /** Accepts `inline` sources, which back `loadModel({ code })`. Without it, an inline load refuses. */
50
+ readonly inlineSources?: boolean;
55
51
  };
56
52
  /**
57
- * Lazy runtime factory consumed by `geospec/model`.
53
+ * One export request. Every field but `inline` and `signal` maps onto subprocess protocol 1, so the
54
+ * same request crosses a process boundary unchanged.
58
55
  *
59
56
  * @public
60
57
  */
61
- type GeoSpecRuntimeClientFactory = () => Promise<GeoSpecRuntimeClient>;
58
+ type GeoSpecModelExportRequest = {
59
+ /** Entry file, as a path from the project root. */
60
+ readonly file: string;
61
+ /** Absolute project root the exporter resolves `file` and its imports against, when the host has one. */
62
+ readonly projectPath?: string;
63
+ readonly format: GeoSpecExportFormat;
64
+ /** JSON-serialisable parameter values; present only when the exporter declares `parameters`. */
65
+ readonly parameters?: Readonly<Record<string, unknown>>;
66
+ /** Tessellation tolerances for a mesh export. */
67
+ readonly mesh?: {
68
+ readonly linearTolerance?: number;
69
+ readonly angularToleranceDegrees?: number;
70
+ };
71
+ /** Millimetre crop box; present only when the exporter declares `region`. */
72
+ readonly region?: GeoSpecLoadRegion;
73
+ /** Source text keyed by project-relative path; present only when the exporter declares `inlineSources`. */
74
+ readonly inline?: Readonly<Record<string, string>>;
75
+ readonly signal?: AbortSignal;
76
+ };
62
77
  /**
63
- * Explicit source adapter for formats whose runtime setup is not part of the
64
- * generic Tau runtime preset.
78
+ * The bytes an exporter returns, in the canonical Z-up millimetre frame.
65
79
  *
66
80
  * @public
67
81
  */
68
- type GeoSpecRuntimeSourceAdapter = {
69
- id: string;
70
- extensions: readonly string[];
71
- createRuntime(options: {
72
- projectPath?: string;
73
- file?: string;
74
- }): Promise<GeoSpecRuntimeClient>;
82
+ type GeoSpecModelArtifact = {
83
+ readonly bytes: Uint8Array<ArrayBuffer>;
84
+ readonly format: GeoSpecExportFormat;
85
+ /** Unit of raw GLB coordinates; defaults to `'mm'`. STEP carries its own unit. */
86
+ readonly sourceUnit?: GeoSpecUnit;
87
+ /** External resources the primary bytes reference, by name. */
88
+ readonly resources?: ReadonlyArray<{
89
+ readonly name: string;
90
+ readonly bytes: Uint8Array<ArrayBuffer>;
91
+ }>;
92
+ /**
93
+ * Every source file the export read, as a path from the project root with its SHA-256 (64 hex digits,
94
+ * optionally prefixed `sha256:`), or `'missing'` for a file the export looked for and did not find. The
95
+ * load records it as the subject's source revision.
96
+ */
97
+ readonly consumedSources?: GeoSpecConsumedSources;
98
+ /** Notices from a successful export, admitted with the subject so its evidence keeps them. */
99
+ readonly diagnostics?: readonly GeometryDiagnostic[];
100
+ /** The export options the exporter applied, recorded in the load evidence beside the exporter id. */
101
+ readonly exportOptions?: Readonly<Record<string, unknown>>;
102
+ };
103
+ /**
104
+ * The source files one export read, each with its SHA-256 or `'missing'`.
105
+ *
106
+ * @public
107
+ */
108
+ type GeoSpecConsumedSources = ReadonlyArray<{
109
+ readonly path: string;
110
+ readonly sha256: string;
111
+ }>;
112
+ /**
113
+ * Turns a source-kind model file into GLB or STEP bytes for GeoSpec. Hosts pass exporters to the
114
+ * model loader; the first whose `extensions` claim a file exports it. `extensions` have no leading dot,
115
+ * and `'*'` claims every file. `export` may be called concurrently; the exporter owns its caching.
116
+ *
117
+ * @public
118
+ */
119
+ type GeoSpecModelExporter = {
120
+ readonly id: string;
121
+ readonly extensions: readonly string[];
122
+ readonly capabilities: GeoSpecExporterCapabilities;
123
+ export(request: GeoSpecModelExportRequest): Promise<GeoSpecModelArtifact>;
124
+ /**
125
+ * The sources an export of `file` would read now, without exporting. With it, the loader reuses an
126
+ * earlier identical load in the same scope when every digest still matches; without it, every file
127
+ * load exports afresh.
128
+ */
129
+ revision?(request: Pick<GeoSpecModelExportRequest, 'file' | 'projectPath'>): Promise<GeoSpecConsumedSources | undefined>;
130
+ /**
131
+ * Releases what the exporter created for one scope. The loader calls it from `releaseAll` for every
132
+ * exporter it exported through in that scope; the exporter stays usable for the next one.
133
+ */
134
+ close?(): Promise<void>;
75
135
  };
76
136
  /**
77
137
  * Axis-aligned crop box in canonical Z-up millimetres; every `min` axis is below its `max` axis.
@@ -139,13 +199,9 @@ type LoadModelCodeOptions<Code extends Record<string, string> = Record<string, s
139
199
  rootDir?: never;
140
200
  /** Geometry format to export. Defaults to `glb`. */
141
201
  format?: GeoSpecModelFormat;
142
- /** Explicit parameters passed to the runtime. */
202
+ /** Explicit parameters passed to the exporter. */
143
203
  parameters?: Record<string, unknown>;
144
- /** Runtime client or lazy runtime factory. */
145
- runtime?: GeoSpecRuntimeClient | GeoSpecRuntimeClientFactory;
146
- /** Source-specific runtime adapters, e.g. host-provided GPL-isolated kernels. */
147
- sourceAdapters?: readonly GeoSpecRuntimeSourceAdapter[];
148
- /** Project root used by runtime integrations. */
204
+ /** Absolute project root passed to the exporter. */
149
205
  projectPath?: string;
150
206
  /** STEP reader strategy used for STEP exports. */
151
207
  stepStreaming?: StepStreamingMode;
@@ -162,12 +218,11 @@ type LoadModelCodeOptions<Code extends Record<string, string> = Record<string, s
162
218
  */
163
219
  workUnitLimit?: number;
164
220
  /**
165
- * Axis-aligned millimetre box the kernel crops the model to before export, so a large mesh model
166
- * can be checked region by region. Only kernels whose direct GLB export declares a `region` option
167
- * honor it; any other kernel, a STEP load or a crop the kernel does not confirm refuses with a typed
168
- * diagnostic instead of loading the whole model. The cropped subject is a different subject: its
169
- * admission carries the kernel's crop notice and its load evidence records `region`, so a claim on
170
- * it never stands for the whole model.
221
+ * Axis-aligned millimetre box the exporter crops the model to before export, so a large mesh model
222
+ * can be checked region by region. Only an exporter that declares `region` honours it; any other
223
+ * exporter, or a crop the exporter cannot make, refuses with a typed diagnostic instead of loading
224
+ * the whole model. The cropped subject is a different subject: its load evidence records `region`,
225
+ * so a claim on it never stands for the whole model.
171
226
  */
172
227
  region?: GeoSpecLoadRegion;
173
228
  };
@@ -188,16 +243,12 @@ type LoadModelFileOptions = {
188
243
  * evidence.
189
244
  */
190
245
  rootDir?: string;
191
- /** Project root used by runtime integrations. */
246
+ /** Absolute project root passed to the exporter. */
192
247
  projectPath?: string;
193
248
  /** Geometry format to export. Defaults to `glb`. */
194
249
  format?: GeoSpecModelFormat;
195
- /** Explicit parameters passed to the runtime. */
250
+ /** Explicit parameters passed to the exporter. */
196
251
  parameters?: Record<string, unknown>;
197
- /** Runtime client or lazy runtime factory. */
198
- runtime?: GeoSpecRuntimeClient | GeoSpecRuntimeClientFactory;
199
- /** Source-specific runtime adapters, e.g. host-provided GPL-isolated kernels. */
200
- sourceAdapters?: readonly GeoSpecRuntimeSourceAdapter[];
201
252
  /** STEP reader strategy used for STEP exports. */
202
253
  stepStreaming?: StepStreamingMode;
203
254
  /** Whether STEP loading should also produce mesh evidence. Defaults to true. */
@@ -213,12 +264,11 @@ type LoadModelFileOptions = {
213
264
  */
214
265
  workUnitLimit?: number;
215
266
  /**
216
- * Axis-aligned millimetre box the kernel crops the model to before export, so a large mesh model
217
- * can be checked region by region. Only kernels whose direct GLB export declares a `region` option
218
- * honor it; any other kernel, a STEP load or a crop the kernel does not confirm refuses with a typed
219
- * diagnostic instead of loading the whole model. The cropped subject is a different subject: its
220
- * admission carries the kernel's crop notice and its load evidence records `region`, so a claim on
221
- * it never stands for the whole model.
267
+ * Axis-aligned millimetre box the exporter crops the model to before export, so a large mesh model
268
+ * can be checked region by region. Only an exporter that declares `region` honours it; any other
269
+ * exporter, or a crop the exporter cannot make, refuses with a typed diagnostic instead of loading
270
+ * the whole model. The cropped subject is a different subject: its load evidence records `region`,
271
+ * so a claim on it never stands for the whole model.
222
272
  */
223
273
  region?: GeoSpecLoadRegion;
224
274
  };
@@ -235,7 +285,7 @@ type LoadModelOptions<Code extends Record<string, string> = Record<string, strin
235
285
  * @public
236
286
  */
237
287
  type GeoSpecModelLoader = <Code extends Record<string, string> = Record<string, string>>(options: LoadModelOptions<Code>) => Promise<GeoSpecSubject>;
238
- /** A configured loader whose shared runtime can be released with its owner. @public */
288
+ /** A configured loader whose admissions can be released with its owner. @public */
239
289
  type ManagedGeoSpecModelLoader = GeoSpecModelLoader & {
240
290
  dispose(): Promise<void>;
241
291
  };
@@ -251,11 +301,9 @@ type CreateModelLoaderOptions = {
251
301
  readSource?: GeoSpecNativeSourceReader;
252
302
  /** Geometry format to export when an individual call does not specify one. */
253
303
  format?: GeoSpecModelFormat;
254
- /** Runtime client or lazy runtime factory. */
255
- runtime?: GeoSpecRuntimeClient | GeoSpecRuntimeClientFactory;
256
- /** Source-specific runtime adapters, e.g. host-provided GPL-isolated kernels. */
257
- sourceAdapters?: readonly GeoSpecRuntimeSourceAdapter[];
258
- /** Project root used by runtime integrations. */
304
+ /** Model exporters for source-kind files; the first whose `extensions` claim the file exports it. */
305
+ exporters?: readonly GeoSpecModelExporter[];
306
+ /** Absolute project root passed to the exporter. */
259
307
  projectPath?: string;
260
308
  /** STEP reader strategy used for STEP sources or exports. */
261
309
  stepStreaming?: StepStreamingMode;
@@ -267,4 +315,4 @@ type CreateModelLoaderOptions = {
267
315
  meshAngularToleranceDegrees?: number;
268
316
  };
269
317
  //#endregion
270
- export { CreateModelLoaderOptions, GeoSpecLoadRegion, GeoSpecModelFormat, GeoSpecModelLoader, GeoSpecRuntimeClient, GeoSpecRuntimeClientFactory, GeoSpecRuntimeSourceAdapter, LoadModelCodeOptions, LoadModelFileOptions, LoadModelOptions, LoadModelSourceOptions, ManagedGeoSpecModelLoader, MeshBufferSource, MeshSource };
318
+ export { CreateModelLoaderOptions, GeoSpecConsumedSources, GeoSpecExportFormat, GeoSpecExporterCapabilities, GeoSpecLoadRegion, GeoSpecModelArtifact, GeoSpecModelExportRequest, GeoSpecModelExporter, GeoSpecModelFormat, GeoSpecModelLoader, LoadModelCodeOptions, LoadModelFileOptions, LoadModelOptions, LoadModelSourceOptions, ManagedGeoSpecModelLoader, MeshBufferSource, MeshSource };
@@ -22,8 +22,6 @@ type GeoSpecCollector = {
22
22
  };
23
23
  /** Per-module collector configuration; native subject/engine lifetime stays with the host. @public */
24
24
  type GeoSpecCollectorOptions = {
25
- matcherWallBackstop?: number;
26
- forensic?: boolean;
27
25
  nativeAssertions: GeoSpecAssertionClientOptions;
28
26
  /** Observes each selected test as it starts and settles, for host progress reporting. */
29
27
  onTestEvent?: (event: GeoSpecTestEvent) => void;
@@ -0,0 +1,2 @@
1
+ import { createNodeVmFileSystem, resolveUnderRoot } from "./node-vm-filesystem.mjs";
2
+ export { createNodeVmFileSystem, resolveUnderRoot };
@@ -0,0 +1 @@
1
+ import{createNodeVmFileSystem as e,resolveUnderRoot as t}from"./node-vm-filesystem.mjs";export{e as createNodeVmFileSystem,t as resolveUnderRoot};
@@ -0,0 +1,22 @@
1
+ import { VmFileSystem } from "@taucad/esbuild/vm";
2
+ //#region src/runner/node/node-vm-filesystem.d.ts
3
+ /**
4
+ * Resolve one VM path to a real path under the root.
5
+ *
6
+ * @param root - The project root.
7
+ * @param path - A canonical path rooted by the supplied project capability.
8
+ * @returns The absolute host path.
9
+ * @throws Error when the path escapes the root.
10
+ * @public
11
+ */
12
+ declare const resolveUnderRoot: (root: string, path: string) => string;
13
+ /**
14
+ * Create a Node `VmFileSystem` rooted at `root`.
15
+ *
16
+ * @param root - Absolute project root path.
17
+ * @returns A VM filesystem confined to `root`.
18
+ * @public
19
+ */
20
+ declare const createNodeVmFileSystem: (root: string) => VmFileSystem;
21
+ //#endregion
22
+ export { createNodeVmFileSystem, resolveUnderRoot };
@@ -0,0 +1 @@
1
+ import{lstat as e,mkdir as t,open as n,readFile as r,realpath as i,rename as a,unlink as o}from"node:fs/promises";import{basename as s,dirname as c,isAbsolute as l,join as u,relative as d,resolve as f,sep as p}from"node:path";import{assertRootedPath as m}from"@taucad/runtime/kernel";import{randomUUID as h}from"node:crypto";const g=(e,t)=>{let n=f(e),r=f(n,m(t)),i=d(n,r);if(i.startsWith(`..${p}`)||i===`..`)throw Error(`GeoSpec refused a path outside the project root: '${t}'.`);return r},_=(e,t)=>{let n=d(e,t);return n===``||!l(n)&&n!==`..`&&!n.startsWith(`..${p}`)},v=async t=>{let n=t;for(;;)try{return await e(n),n}catch(e){let{code:t}=e;if(t!==`ENOENT`&&t!==`ENOTDIR`)throw e;let r=c(n);if(r===n)throw e;n=r}},y=l=>{let d=f(l),p=i(d),m=async(e,t)=>{let[n,r]=await Promise.all([p,v(e)]);if(!_(n,await i(r)))throw Object.assign(Error(`GeoSpec refused a symbolic link outside the project root: '${t}'.`),{code:`EACCES`});return e},y=async e=>m(g(d,e),e),b=async e=>{let t=await r(await y(e));return new Uint8Array(t.buffer,t.byteOffset,t.byteLength)};return{exists:async e=>{try{let t=await n(await y(e),`r`);try{return(await t.stat()).isFile()||await t.readFile(),!0}finally{await t.close()}}catch(e){let{code:t}=e;if(t===`ENOENT`||t===`ENOTDIR`)return!1;throw e}},readFile:(async(e,t)=>t===`utf8`?r(await y(e),`utf8`):b(e)),writeFile:async(r,l)=>{let f=g(d,r),v=c(f);await m(v,r),await t(v,{recursive:!0});let[y,b]=await Promise.all([p,i(v)]);if(!_(y,b))throw Object.assign(Error(`GeoSpec refused a symbolic link outside the project root: '${r}'.`),{code:`EACCES`});let x=u(b,s(f));if((await e(x).catch(e=>{if(e.code!==`ENOENT`)throw e}))?.isSymbolicLink())throw Object.assign(Error(`GeoSpec refused to replace a symbolic link: '${r}'.`),{code:`ELOOP`});let S=u(b,`.${s(f)}.${process.pid}.${h()}.tmp`);try{let t=await n(S,`wx`);try{await t.writeFile(l,`utf8`),await t.sync()}finally{await t.close()}let o=await i(v);if(o!==b||!_(y,o))throw Object.assign(Error(`GeoSpec refused a changed parent directory: '${r}'.`),{code:`ELOOP`});if((await e(x).catch(e=>{if(e.code!==`ENOENT`)throw e}))?.isSymbolicLink())throw Object.assign(Error(`GeoSpec refused to replace a symbolic link: '${r}'.`),{code:`ELOOP`});await a(S,x)}finally{await o(S).catch(()=>void 0)}},ensureDir:async e=>{let n=await y(e);await t(n,{recursive:!0});let[r,a]=await Promise.all([p,i(n)]);if(!_(r,a))throw Object.assign(Error(`GeoSpec refused a symbolic link outside the project root: '${e}'.`),{code:`EACCES`})}}};export{y as createNodeVmFileSystem,g as resolveUnderRoot};
@@ -0,0 +1 @@
1
+ const e=()=>{let e=new Set;return{on(t,n){let r=e=>{e.type===t&&n(e)};return e.add(r),()=>e.delete(r)},emit(t){for(let n of e)n(t)},clear(){e.clear()}}};export{e as createRunnerEventChannel};
@@ -21,16 +21,12 @@ type GeoSpecPoolHostMessage = {
21
21
  shard: GeoSpecPoolShard;
22
22
  testNamePattern?: string;
23
23
  testTimeout?: number;
24
- matcherWallBackstop?: number;
25
- forensic?: boolean;
26
24
  } | {
27
25
  /** List-only collection pass: register tests, run no bodies (R3 splitting). */
28
26
  type: 'list-tests';
29
27
  shardId: number;
30
28
  file: string;
31
29
  testTimeout?: number;
32
- matcherWallBackstop?: number;
33
- forensic?: boolean;
34
30
  } | {
35
31
  type: 'shutdown';
36
32
  };
@@ -47,12 +43,6 @@ type GeoSpecPoolWorkerMessage = {
47
43
  type: 'file-start';
48
44
  shardId: number;
49
45
  file: string;
50
- } | {
51
- type: 'forensic';
52
- shardId: number;
53
- name: string;
54
- value: number;
55
- unit: 'milliseconds' | 'count';
56
46
  } | {
57
47
  type: 'tests-listed';
58
48
  shardId: number;
@@ -0,0 +1,36 @@
1
+ import "../types.mjs";
2
+ import { GeoSpecPoolWorkerHandle } from "./pool-messages.mjs";
3
+ import { GeoSpecRunner } from "../worker/runner-types.mjs";
4
+ import { ShardTimings } from "./shard-timings.mjs";
5
+ //#region src/runner/pool/pool.d.ts
6
+ /**
7
+ * Everything the pool needs that differs between Node and the browser.
8
+ *
9
+ * @public
10
+ */
11
+ type GeoSpecPoolOptions = {
12
+ /** Spawn one worker running `startGeoSpecPoolWorkerHost`. */
13
+ createWorker: () => GeoSpecPoolWorkerHandle | Promise<GeoSpecPoolWorkerHandle>;
14
+ /** Optional one-time worker initialization performed after its ready signal. */
15
+ initializeWorker?: (worker: GeoSpecPoolWorkerHandle) => Promise<void> | void;
16
+ /** Wait for each native worker's cleanup acknowledgement before terminating it. */
17
+ gracefulShutdown?: boolean;
18
+ /** Worker count. Omit to let the caller's host auto-size before calling. */
19
+ workers: number;
20
+ /** Optional scheduling telemetry. Absent = declared order, no memory class. */
21
+ timings?: ShardTimings;
22
+ /** Per-shard non-verdict watchdog, milliseconds (R11). */
23
+ shardTimeout?: number;
24
+ /** Split files whose recorded duration exceeds this. Milliseconds. */
25
+ splitThreshold?: number;
26
+ };
27
+ /**
28
+ * Create the host-agnostic pool runner.
29
+ *
30
+ * @param options - Worker factory, worker count, telemetry and event hook.
31
+ * @returns The runner lifecycle surface.
32
+ * @public
33
+ */
34
+ declare const createGeoSpecPoolRunner: (options: GeoSpecPoolOptions) => GeoSpecRunner;
35
+ //#endregion
36
+ export { GeoSpecPoolOptions, createGeoSpecPoolRunner };
@@ -0,0 +1 @@
1
+ import{geoSpecTimeoutCode as e}from"../timeout-codes.mjs";import{createNoMatchingGeoSpecTestsIssue as t}from"../worker/no-matching-tests-issue.mjs";import{accumulateFileResult as n}from"../worker/serial-runner.mjs";import{createRunnerEventChannel as r}from"./events.mjs";import{filesToSplit as i,planShards as a,selectShard as o}from"./shard-planner.mjs";import{assertRootedPath as s}from"@taucad/runtime/kernel";const c=(t,n)=>({code:e.shard,message:`GeoSpec shard '${t}' exceeded the ${n} ms non-verdict watchdog and its worker was terminated: infrastructure failure, not a geometry verdict.`,severity:`error`,type:`runtime`}),l=(e,t=`GEOSPEC_SHARD_FAILED`)=>({success:!1,issues:[{code:t,message:e,severity:`error`,type:`runtime`}]}),u=e=>({requestedFiles:[...e],completedFiles:[],notRunFiles:[...e],discoveryComplete:!1,discovered:0,selected:0,completed:0,passed:0,failed:0,unsupported:0,inconclusive:0,skipped:0,notRun:0,cancelled:!1,bailed:!1}),d=e=>{let t=`complete`,n=new Map;for(let r of e){r.lineage?.status===`mixed`?t=`mixed`:r.lineage?.status!==`complete`&&t!==`mixed`&&(t=`unavailable`);let e=r.lineage?.loads.flatMap(({evidence:e})=>{let t=e?.artifacts[0];return e?.exportOptions===void 0?[...(e?.artifacts??[]).flatMap(e=>e.sourcePath===void 0?[]:[{[e.sourcePath]:`sha256:${e.sha256}`}]),...e?.sourcePath===void 0||t===void 0?[]:[{[e.sourcePath]:`sha256:${t.sha256}`}]]:[]})??[];for(let i of[...r.lineage?.modules.map(e=>e.files)??[],...e,...r.lineage?.loads.map(e=>e.evidence?.sourceRevision?.files??{})??[]])for(let[e,r]of Object.entries(i))n.has(e)&&n.get(e)!==r&&(t=`mixed`),n.set(e,r)}return t},f=e=>{let t,n,r,i=new Promise(e=>{r=e}),a,o,s=new Promise((e,t)=>{a=e,o=t});s.catch(()=>void 0);let c=!1,l=!1,u,d,f=new Promise((e,t)=>{u=e,d=t});f.catch(()=>void 0);let p=e=>{let n=t;t=void 0,n?.(e)};return e.onMessage(e=>{switch(e.type){case`ready`:r?.();break;case`initialized`:c?(c=!1,a?.()):l?u?.():a?.();break;case`initialization-error`:n=e.message,r?.(),o?.(Error(e.message)),l&&d?.(Error(e.message));break;case`shard-complete`:p({type:`complete`,message:e});break;case`tests-listed`:p({type:`listed`,names:e.names});break;case`shard-error`:case`list-error`:p({type:`error`,message:e.message})}}),e.onExit(e=>{n=e.message??`the pool worker exited unexpectedly`,r?.(),o?.(Error(n)),d?.(Error(n)),p({type:`error`,message:n})}),{ready:i,initialized:s,shutdown:f,beginInitialization:()=>{c=!0},beginShutdown:()=>{l=!0},exited:()=>n,settle:async e=>n===void 0?new Promise(e=>{t=e}):{type:`error`,message:n}}},p=p=>{let h={closed:!1},g=()=>h.closed,_=()=>h.aborted,v=[],y=new WeakMap,b=r(),x=!1,S,C,w=async e=>{if(S!==void 0)return S;let t=(async()=>{for(;v.length<e&&!g();){let e=await p.createWorker();if(g()){await e.terminate();return}y.set(e,f(e)),v.push({handle:e,busy:!1,dead:!1,initialized:!1})}})();S=t;try{await t}finally{S===t&&(S=void 0)}},T=async(e,t,n)=>{let r=y.get(e.handle).settle(n.id);if(t(),p.shardTimeout===void 0)return r;let i=p.shardTimeout,a,o=new Promise(e=>{a=setTimeout(()=>{e({type:`timeout`,message:c(n.file,i).message})},i)});try{let t=await Promise.race([r,o]);return t.type===`timeout`&&(e.dead=!0,await e.handle.terminate()),t}finally{clearTimeout(a)}},E=()=>C===void 0?(h.closed=!0,h.aborted=`closed`,C=(async()=>{try{await S}catch{}let e=v.filter(e=>!e.dead&&y.get(e.handle)?.exited()===void 0);try{for(let t of e)y.get(t.handle).beginShutdown(),t.handle.postMessage({type:`shutdown`});p.gracefulShutdown&&await Promise.all(e.map(async e=>{let t=y.get(e.handle),n;try{await Promise.race([t.shutdown,new Promise((e,t)=>{n=setTimeout(()=>{t(Error(`Native pool shutdown timed out.`))},1e4)})])}finally{clearTimeout(n)}}))}finally{await Promise.all(v.map(async e=>e.handle.terminate())),v.length=0,b.emit({type:`close`}),b.clear()}})(),C):C;return{async run(r){let c=u(r.files);if(new Set(r.files).size!==r.files.length)return{success:!1,passed:0,failed:1,selectedTests:0,files:[],accounting:c,lineageStatus:`unavailable`,issues:[{code:`GEOSPEC_DUPLICATE_FILES`,message:`GeoSpec requested files must be unique.`,severity:`error`,type:`runtime`}]};if(h.closed)return{success:!1,passed:0,failed:1,selectedTests:0,files:[],accounting:c,lineageStatus:`unavailable`,issues:[{code:`GEOSPEC_RUNNER_CLOSED`,message:`GeoSpec runner is closed.`,severity:`error`,type:`runtime`}]};if(x)return{success:!1,passed:0,failed:1,selectedTests:0,files:[],accounting:c,lineageStatus:`unavailable`,issues:[{code:`GEOSPEC_RUNNER_BUSY`,message:`GeoSpec runner already has an active run.`,severity:`error`,type:`runtime`}]};x=!0;try{delete h.aborted;let c=r.files.map(s),f=performance.now();b.emit({type:`run-start`,files:c});try{if(await w(Math.max(1,p.workers)),g())throw Error(`GeoSpec pool closed during worker creation.`);if(await Promise.all(v.map(async e=>y.get(e.handle).ready)),g())throw Error(`GeoSpec pool closed during worker initialization.`);for(let e of v){let t=y.get(e.handle).exited();if(t!==void 0)throw Error(t)}if(p.initializeWorker&&await Promise.all(v.map(async e=>{if(e.initialized)return;let t=y.get(e.handle);t.beginInitialization(),await p.initializeWorker(e.handle),await t.initialized,e.initialized=!0})),g())throw Error(`GeoSpec pool closed during worker initialization.`)}catch(e){throw await E(),e}let x={passed:0,failed:0,selectedTests:0},S=[],C=[],D=new Map,O=c.length<v.length?[...new Set([...c,...i(c,p.timings,p.splitThreshold)])]:i(c,p.timings,p.splitThreshold);for(let[e,t]of O.entries()){let n=v[e%v.length],i={id:-1-e,file:t},a=await T(n,()=>{n.handle.postMessage({type:`list-tests`,shardId:i.id,file:t,...r.testTimeout===void 0?{}:{testTimeout:r.testTimeout}})},i);a.type===`listed`&&a.names.length>1&&new Set(a.names).size===a.names.length&&D.set(t,a.names)}let k=a({files:c,splitTests:D,...p.timings?{timings:p.timings}:{}}),A=new Map,j=new Map(c.map(e=>[e,k.filter(t=>t.file===e).length])),M=new Map,N=!1,P=0,F=async t=>{for(;!t.dead;){if(_()!==void 0)return;let n=o({pending:k,workerLoadKey:t.loadKey,heavyRunning:P});if(n===void 0)return;let[i]=k.splice(n,1);i.memoryClass===`heavy`&&(P+=1),t.busy=!0,b.emit({type:`file-start`,file:i.file});let a=performance.now(),s=await T(t,()=>{t.handle.postMessage({type:`run-shard`,shard:{id:i.id,file:i.file,...i.testNamePattern===void 0?{}:{testNamePattern:i.testNamePattern}},...r.testNamePattern===void 0?{}:{testNamePattern:String(r.testNamePattern)},...r.testTimeout===void 0?{}:{testTimeout:r.testTimeout}})},i),c=performance.now()-a;t.busy=!1,i.memoryClass===`heavy`&&--P;let u=s.type===`complete`?s.message.result:s.type===`timeout`?l(s.message,e.shard):l(s.type===`listed`?`the pool worker answered a shard with a test list`:s.message);s.type===`complete`&&(t.loadKey=s.message.primaryLoadKey??t.loadKey,p.timings?.record(i.timingKey,{durationMs:s.message.durationMs,peakRssBytes:s.message.workerMemoryBytes??0})),s.type===`timeout`&&C.push({code:e.shard,message:s.message,severity:`error`,type:`runtime`});let d=A.get(i.file);A.set(i.file,{result:d===void 0?u:m(d.result,u),durationMs:(d?.durationMs??0)+c}),M.set(i.file,(M.get(i.file)??0)+1),b.emit({type:`file-complete`,file:i.file,result:u,durationMs:c,...s.type===`complete`&&s.message.primaryLoadKey!==void 0?{primaryLoadKey:s.message.primaryLoadKey}:{},...s.type===`complete`&&s.message.workerMemoryBytes!==void 0?{workerMemoryBytes:s.message.workerMemoryBytes}:{}});let f=!u.success||!u.passed;if(r.bail===!0&&f){N=!0,k.length=0;return}}};await Promise.all(v.map(async e=>F(e))),p.timings?.save();let I=_();I!==void 0&&(C.push({code:`GEOSPEC_RUNNER_ABORTED`,message:I.length>0?`GeoSpec run aborted: ${I}`:`GeoSpec run aborted.`,severity:`error`,type:`runtime`}),x.failed+=1,b.emit({type:`abort`,reason:I}));for(let e of c){let t=A.get(e);t!==void 0&&(S.push({file:e,result:t.result,durationMs:t.durationMs}),n(x,t.result))}x.selectedTests===0&&x.failed===0&&(C.push(t()),x.failed+=1);let L=u(r.files);L.completedFiles=c.filter(e=>M.get(e)===j.get(e)),L.notRunFiles=c.filter(e=>!L.completedFiles.includes(e)),L.cancelled=I!==void 0,L.bailed=N,L.discoveryComplete=L.notRunFiles.length===0&&S.every(({result:e})=>e.success&&e.accounting!==void 0);for(let{result:e}of S)if(e.accounting!==void 0)for(let t of[`discovered`,`selected`,`completed`,`passed`,`failed`,`unsupported`,`inconclusive`,`skipped`,`notRun`])L[t]+=e.accounting[t];let R=d(S.map(({result:e})=>e)),z={success:L.discoveryComplete&&S.every(({result:e})=>e.success&&e.passed)&&R===`complete`&&x.failed===0&&C.length===0,passed:x.passed,failed:x.failed,selectedTests:x.selectedTests,files:S,accounting:L,lineageStatus:R,...C.length>0?{issues:C}:{},durationMs:performance.now()-f};return b.emit({type:`run-complete`,result:z}),z}finally{x=!1}},on:b.on,abort(e){h.aborted=e??`requested`},close:E}},m=(e,t)=>{let n=[...e.tests??[],...t.tests??[]].sort((e,t)=>(e.ordinal??1/0)-(t.ordinal??1/0)),r={status:d([e,t]),modules:[...e.lineage?.modules??[],...t.lineage?.modules??[]],loads:[...e.lineage?.loads??[],...t.lineage?.loads??[]]},i,a=n.map(e=>e.ordinal),o=e.accounting?.discovered;if(o!==void 0&&o===t.accounting?.discovered&&a.every(e=>e!==void 0&&Number.isSafeInteger(e)&&e>=0&&e<o)&&new Set(a).size===n.length){let e=e=>n.filter(t=>t.status===e).length;i={discovered:o,selected:n.length,completed:n.filter(e=>e.status!==`not-run`&&e.status!==`skipped`).length,passed:e(`passed`),failed:e(`failed`),unsupported:e(`unsupported`),inconclusive:e(`inconclusive`),skipped:e(`skipped`),notRun:Math.max(0,o-n.length)+e(`not-run`)}}let s={tests:n,lineage:r,...i===void 0?{}:{accounting:i}};return!e.success||!t.success?{success:!1,issues:[...e.success?[]:e.issues,...t.success?[]:t.issues],...e.bundle===void 0&&t.bundle===void 0?{}:{bundle:e.bundle??t.bundle},...s}:{success:!0,passed:e.passed&&t.passed&&i!==void 0&&r.status===`complete`,...s,bundle:e.bundle}};export{p as createGeoSpecPoolRunner,m as mergeShardResults};
@@ -0,0 +1 @@
1
+ const e=(e,t)=>t===void 0?e:`${e}::${t}`,t=6e4,n=3758096384,r=e=>`^${e.replaceAll(/[$()*+.?[\\\]^{|}]/gu,String.raw`\$&`)}$`,i=e=>e!==void 0&&e.peakRssBytes>=3758096384?`heavy`:`normal`,a=(e,n,r=t)=>n?e.filter(e=>(n.read(e)?.durationMs??0)>r):[],o=t=>{let{files:n,splitTests:a,timings:o,affinity:s}=t,c=[];for(let[t,l]of n.entries()){let n=a?.get(l),u=s?.get(l);if(n&&n.length>0){for(let a of n){let n=r(a),s=e(l,n),d=o?.read(s);c.push({file:l,testNamePattern:n,timingKey:s,estimated:d?.durationMs??0,memoryClass:i(d),...u===void 0?{}:{affinityKey:u},order:t})}continue}let d=o?.read(l);c.push({file:l,timingKey:l,estimated:d?.durationMs??0,memoryClass:i(d),...u===void 0?{}:{affinityKey:u},order:t})}return c.sort((e,t)=>t.estimated-e.estimated||e.order-t.order),c.map(({order:e,...t},n)=>({...t,id:n}))},s=e=>{let{pending:t,workerLoadKey:n,heavyRunning:r}=e,i=e=>e.memoryClass===`normal`||r===0;if(n!==void 0){let e=t.findIndex(e=>e.affinityKey===n&&i(e));if(e!==-1)return e}let a=t.findIndex(e=>i(e));return a===-1?void 0:a};export{n as defaultHeavyRssBytes,t as defaultSplitThreshold,r as exactTestPattern,a as filesToSplit,o as planShards,s as selectShard,e as shardTimingKey};
@@ -0,0 +1,36 @@
1
+ //#region src/runner/pool/shard-timings.d.ts
2
+ /**
3
+ * Shard timings vocabulary: the pool's longest-first scheduling hint, kept
4
+ * free of Node imports so the browser-safe pool can type it. The persisted
5
+ * table lives in `#runner/node/pool-timings.js`.
6
+ *
7
+ * @module
8
+ */
9
+ /**
10
+ * One shard's recorded cost.
11
+ *
12
+ * @public
13
+ */
14
+ type ShardTiming = {
15
+ /** Wall duration of the last recorded run, in milliseconds. */
16
+ durationMs: number;
17
+ /** Highest RSS observed for this shard, in bytes. Monotonic. */
18
+ peakRssBytes: number;
19
+ };
20
+ /**
21
+ * A mutable timings table.
22
+ *
23
+ * @public
24
+ */
25
+ type ShardTimings = {
26
+ /** Merge one observation. Peak RSS only ever grows. */
27
+ record(shard: string, timing: ShardTiming): void;
28
+ /** The hint for one shard, or `undefined` when unknown. */
29
+ read(shard: string): ShardTiming | undefined;
30
+ /** Every recorded shard, sorted by descending duration (longest-first). */
31
+ longestFirst(): string[];
32
+ /** Persist the table. Failure is silent: telemetry never fails a run. */
33
+ save(): void;
34
+ };
35
+ //#endregion
36
+ export { ShardTiming, ShardTimings };
@@ -0,0 +1 @@
1
+ const e=e=>{let t=e;if(t?.kind!==`geometry-subject`)return e;let n=t.provenance?.contentHash,r=t.provenance?.source?.format;return{kind:`geometry-subject-ref`,...typeof n==`string`?{contentHash:n}:{},...typeof r==`string`?{format:r}:{}}},t=t=>({...t,subject:e(t.subject)}),n=e=>({...e,assertions:e.assertions.map(e=>t(e))}),r=e=>{let t=e=>({...e,code:``,...e.sourceMap===void 0?{}:{sourceMap:``}});return e.success?{...e,tests:e.tests.map(e=>n(e)),bundle:t(e.bundle)}:{...e,...e.tests===void 0?{}:{tests:e.tests.map(e=>n(e))},...e.bundle===void 0?{}:{bundle:t(e.bundle)}}};export{r as sanitizePoolResult};
@@ -0,0 +1,36 @@
1
+ import { ManagedGeoSpecNativeModelLoader } from "../../model/native-model-loader.mjs";
2
+ import { GeoSpecPoolHostMessage, GeoSpecPoolWorkerMessage } from "./pool-messages.mjs";
3
+ import { GeoSpecRunnerOptions } from "../worker/runner-types.mjs";
4
+ //#region src/runner/pool/worker-host.d.ts
5
+ /**
6
+ * Options accepted by {@link startGeoSpecPoolWorkerHost}.
7
+ *
8
+ * @public
9
+ */
10
+ type GeoSpecPoolWorkerHostOptions = {
11
+ /** Filesystem containing the project and test modules. */
12
+ filesystem: GeoSpecRunnerOptions['filesystem'];
13
+ /** Native assertion client shared with this worker's model admissions. */
14
+ nativeAssertions: GeoSpecRunnerOptions['nativeAssertions'];
15
+ /** Managed native admissions, released after every shard and collection pass and before shutdown. */
16
+ nativeModelLoader: ManagedGeoSpecNativeModelLoader;
17
+ /** Additional in-memory modules made available to the VM. */
18
+ builtinModules?: GeoSpecRunnerOptions['builtinModules'];
19
+ /** Post a message to the pool host. */
20
+ postMessage: (message: GeoSpecPoolWorkerMessage) => void;
21
+ /** Subscribe to pool-host messages. */
22
+ onHostMessage: (listener: (message: GeoSpecPoolHostMessage) => void) => void;
23
+ /** Sample this worker's resident memory in bytes (R15 telemetry); optional. */
24
+ measureMemoryBytes?: () => number | undefined;
25
+ /** Release platform resources on shutdown, after the native subjects are released. */
26
+ onShutdown?: () => Promise<void> | void;
27
+ };
28
+ /**
29
+ * Serve shards until the host says shutdown.
30
+ *
31
+ * @param options - Worker filesystem, native engine, and message plumbing.
32
+ * @public
33
+ */
34
+ declare const startGeoSpecPoolWorkerHost: (options: GeoSpecPoolWorkerHostOptions) => void;
35
+ //#endregion
36
+ export { GeoSpecPoolWorkerHostOptions, startGeoSpecPoolWorkerHost };
@@ -0,0 +1 @@
1
+ import{executeGeoSpecFile as e}from"../worker/serial-runner.mjs";import{sanitizePoolResult as t}from"./transport.mjs";const n=e=>e instanceof Error?e.message:String(e),r=e=>e.success?e.tests.map(e=>[...e.suite,e.name].join(` > `)):[],i=i=>{let a={filesystem:i.filesystem,nativeAssertions:i.nativeAssertions,nativeModelLoader:i.nativeModelLoader,...i.builtinModules?{builtinModules:i.builtinModules}:{}},o=new Map,s=async()=>{await i.nativeModelLoader.releaseAll()},c=async(e,t)=>{let n=e;try{await s()}catch(r){e.type!==`shard-error`&&e.type!==`list-error`&&(n=t(r))}try{i.postMessage(n)}catch(e){i.postMessage(t(e))}},l=Promise.resolve(),u=e=>{let t=l;l=(async()=>{try{await t}catch{}try{await e()}catch(e){try{i.postMessage({type:`initialization-error`,message:n(e)})}catch{}}})()},d=async l=>{if(l.type===`initialize`){i.postMessage({type:`initialized`});return}if(l.type===`shutdown`){o.clear();let e=!1,t;try{await s()}catch(n){e=!0,t=n}try{await i.onShutdown?.()}catch(n){e||(e=!0,t=n)}if(e){i.postMessage({type:`initialization-error`,message:n(t)});return}i.postMessage({type:`initialized`});return}if(l.type===`list-tests`){let t=e=>({type:`list-error`,shardId:l.shardId,file:l.file,message:n(e)}),i;try{let t=await e({runner:a,file:l.file,collectOnly:!0,bundleCache:o,...l.testTimeout===void 0?{}:{testTimeout:l.testTimeout}});i={type:`tests-listed`,shardId:l.shardId,file:l.file,names:r(t)}}catch(e){i=t(e)}await c(i,t);return}let{shard:u}=l;i.postMessage({type:`file-start`,shardId:u.id,file:u.file});let d=performance.now(),f=e=>({type:`shard-error`,shardId:u.id,file:u.file,message:n(e)}),p;try{let n=await e({runner:a,file:u.file,bundleCache:o,...u.testNamePattern===void 0?l.testNamePattern===void 0?{}:{testNamePattern:l.testNamePattern}:{testNamePattern:u.testNamePattern},...l.testTimeout===void 0?{}:{testTimeout:l.testTimeout}}),r=i.measureMemoryBytes?.();p={type:`shard-complete`,shardId:u.id,file:u.file,result:t(n),durationMs:performance.now()-d,...r===void 0?{}:{workerMemoryBytes:r}}}catch(e){p=f(e)}await c(p,f)};i.onHostMessage(e=>{u(async()=>d(e))}),i.postMessage({type:`ready`})};export{i as startGeoSpecPoolWorkerHost};
@@ -44,4 +44,4 @@ export const createModelLoader = (defaults = {}) => {
44
44
  }
45
45
  return factory(defaults);
46
46
  };
47
- `;async function w(m){let h=a(m.testNamePattern);if(!h.success)return{success:!1,issues:[h.issue]};let g=v(m.filesystem),_=await l({filesystem:g.filesystem}),x=s({...m.matcherWallBackstop===void 0?{}:{matcherWallBackstop:m.matcherWallBackstop},...m.forensic===void 0?{}:{forensic:m.forensic},...m.onTestEvent===void 0?{}:{onTestEvent:m.onTestEvent},nativeAssertions:m.nativeAssertions}),w=await y(m),T=f(),E=b(),D=new Set,O=[],k=async()=>{do{let e=[...D];await Promise.allSettled(e);for(let t of e)D.delete(t)}while(D.size>0)},A=m.nativeModelLoader,j=r(m.nativeAssertions),M=!0,N=A===void 0?void 0:n=>{let r=O.length,a=`${T}:load:${r+1}`;O.push({loadId:a,status:`unavailable`});let o=(async()=>{try{let i=await A(n),{load:o,...s}=i,c=t(i),l=o===void 0?void 0:{...structuredClone(o),loadId:a};return O[r]={loadId:a,status:l?.status??`unavailable`,subject:s,...l===void 0?{}:{evidence:l}},e({engine:m.nativeAssertions.engine,client:j,identity:s,...l===void 0?{}:{load:l},isLive:()=>M,...c===void 0?{}:{ensureResident:c}})}catch(e){throw O[r]={loadId:a,status:`failed`,error:e instanceof Error?e.message:String(e),...e instanceof i?{diagnostics:e.diagnostics}:{}},e}})();return D.add(o.catch(()=>void 0)),D.add(o),o};E.set(T,{collector:x,geoSpecModelLoadError:i,...N?{nativeModelLoader:N}:{},...N?{createModelLoader:t=>{if(t.engine!==void 0||t.readSource!==void 0)throw TypeError(`Authored VM model defaults cannot replace the admitting host.`);let r=0,i=new Set,a=new Set,o,s=async i=>{let o=r,s=await N({...t,...i}),c=n(s);return c.lease!==void 0&&a.add(c.lease),e({...c,isLive:()=>c.isLive()&&r===o})},c=e=>{let t=s(e);return i.add(t),t},l=async()=>{do{let e=[...i];await Promise.allSettled(e);for(let t of e)i.delete(t)}while(i.size>0);r+=1;let e=[];for(let t of a)try{t.dispose(),a.delete(t)}catch(t){e.push(t)}if(e.length>0)throw AggregateError(e,`GeoSpec model scope disposal failed.`)};return Object.assign(c,{dispose(){return o??=l().finally(()=>{o=void 0}),o}})}}:{}}),_.registerModule(`geospec`,{version:`0.0.0-poc`,code:S(T)}),_.registerModule(`geospec/model`,{version:`0.0.0-poc`,code:C(T)});for(let[e,t]of Object.entries(m.builtinModules??{}))_.registerModule(e,t);try{let e=w?.bundle??await _.bundle(m.entryPath);if(!e.success)return{success:!1,issues:e.issues,bundle:e};let t=g.reads();m.bundleCache!==void 0&&w===void 0&&t!==void 0&&m.bundleCache.set(m.entryPath,{builtinIdentity:p(m),runToken:T,bundle:e,bundlerReads:t});let n=await _.execute(w===void 0?e.code:e.code.replaceAll(w.runToken,T)),r=async()=>{let t={},n=g.reads()!==void 0;for(let e of w?.bundlerReads??g.observed()){if(e.question===`exists`||typeof e.answer==`boolean`)continue;let r=await c(typeof e.answer==`string`?new TextEncoder().encode(e.answer):e.answer);n&&=t[e.path]===void 0||t[e.path]===`sha256:${r}`,t[e.path]=`sha256:${r}`}let r=new Map(Object.entries(t));for(let e of O){let{evidence:t}=e;for(let e of t?.exportOptions===void 0?t?.artifacts??[]:[])if(e.sourcePath!==void 0){let t=`sha256:${e.sha256}`;n&&=!r.has(e.sourcePath)||r.get(e.sourcePath)===t,r.set(e.sourcePath,t)}if(t?.sourcePath!==void 0&&t.exportOptions===void 0){let e=t.artifacts[0];if(e!==void 0){let i=`sha256:${e.sha256}`;n&&=!r.has(t.sourcePath)||r.get(t.sourcePath)===i,r.set(t.sourcePath,i)}}for(let[t,i]of Object.entries(e.evidence?.sourceRevision?.files??{}))n&&=!r.has(t)||r.get(t)===i,r.set(t,i)}return{status:n?O.some(e=>e.status!==`complete`)?`unavailable`:`complete`:`mixed`,modules:[{entryPath:m.entryPath,bundleSha256:await c(new TextEncoder().encode(w===void 0?e.code:e.code.replaceAll(w.runToken,T))),files:t,consistent:n}],loads:O}},i=e=>({discovered:x.tests.length,selected:e,completed:x.tests.filter(e=>e.status!==`not-run`&&e.status!==`skipped`).length,passed:x.tests.filter(e=>e.status===`passed`).length,failed:x.tests.filter(e=>e.status===`failed`).length,unsupported:x.tests.filter(e=>e.status===`unsupported`).length,inconclusive:x.tests.filter(e=>e.status===`inconclusive`).length,skipped:x.tests.filter(e=>e.status===`skipped`).length,notRun:x.tests.filter(e=>e.status===`not-run`).length});if(!n.success)return await k(),{success:!1,issues:n.issues,bundle:e,tests:x.tests,accounting:i(0),lineage:await r()};if(m.collectOnly===!0)return await x.waitForCompletion(m.testTimeout,/(?!)/u),await k(),{success:!0,passed:!1,tests:x.tests,bundle:e,accounting:i(0),lineage:await r()};await x.waitForCompletion(m.testTimeout,h.pattern),await k();let a=o(x.tests,h.pattern),s=await r();return{success:!0,passed:s.status===`complete`&&a.every(e=>e.status===`passed`||e.status===`skipped`),tests:a,bundle:e,accounting:i(a.length),lineage:s}}finally{await k(),M=!1,E.delete(T),E.size===0&&Reflect.deleteProperty(d,u),_.dispose()}}export{w as runGeoSpecModule};
47
+ `;async function w(m){let h=a(m.testNamePattern);if(!h.success)return{success:!1,issues:[h.issue]};let g=v(m.filesystem),_=await l({filesystem:g.filesystem}),x=s({...m.onTestEvent===void 0?{}:{onTestEvent:m.onTestEvent},nativeAssertions:m.nativeAssertions}),w=await y(m),T=f(),E=b(),D=new Set,O=[],k=async()=>{do{let e=[...D];await Promise.allSettled(e);for(let t of e)D.delete(t)}while(D.size>0)},A=m.nativeModelLoader,j=r(m.nativeAssertions),M=!0,N=A===void 0?void 0:n=>{let r=O.length,a=`${T}:load:${r+1}`;O.push({loadId:a,status:`unavailable`});let o=(async()=>{try{let i=await A(n),{load:o,...s}=i,c=t(i),l=o===void 0?void 0:{...structuredClone(o),loadId:a};return O[r]={loadId:a,status:l?.status??`unavailable`,subject:s,...l===void 0?{}:{evidence:l}},e({engine:m.nativeAssertions.engine,client:j,identity:s,...l===void 0?{}:{load:l},isLive:()=>M,...c===void 0?{}:{ensureResident:c}})}catch(e){throw O[r]={loadId:a,status:`failed`,error:e instanceof Error?e.message:String(e),...e instanceof i?{diagnostics:e.diagnostics}:{}},e}})();return D.add(o.catch(()=>void 0)),D.add(o),o};E.set(T,{collector:x,geoSpecModelLoadError:i,...N?{nativeModelLoader:N}:{},...N?{createModelLoader:t=>{if(t.engine!==void 0||t.readSource!==void 0)throw TypeError(`Authored VM model defaults cannot replace the admitting host.`);let r=0,i=new Set,a=new Set,o,s=async i=>{let o=r,s=await N({...t,...i}),c=n(s);return c.lease!==void 0&&a.add(c.lease),e({...c,isLive:()=>c.isLive()&&r===o})},c=e=>{let t=s(e);return i.add(t),t},l=async()=>{do{let e=[...i];await Promise.allSettled(e);for(let t of e)i.delete(t)}while(i.size>0);r+=1;let e=[];for(let t of a)try{t.dispose(),a.delete(t)}catch(t){e.push(t)}if(e.length>0)throw AggregateError(e,`GeoSpec model scope disposal failed.`)};return Object.assign(c,{dispose(){return o??=l().finally(()=>{o=void 0}),o}})}}:{}}),_.registerModule(`geospec`,{version:`0.0.0-poc`,code:S(T)}),_.registerModule(`geospec/model`,{version:`0.0.0-poc`,code:C(T)});for(let[e,t]of Object.entries(m.builtinModules??{}))_.registerModule(e,t);try{let e=w?.bundle??await _.bundle(m.entryPath);if(!e.success)return{success:!1,issues:e.issues,bundle:e};let t=g.reads();m.bundleCache!==void 0&&w===void 0&&t!==void 0&&m.bundleCache.set(m.entryPath,{builtinIdentity:p(m),runToken:T,bundle:e,bundlerReads:t});let n=await _.execute(w===void 0?e.code:e.code.replaceAll(w.runToken,T)),r=async()=>{let t={},n=g.reads()!==void 0;for(let e of w?.bundlerReads??g.observed()){if(e.question===`exists`||typeof e.answer==`boolean`)continue;let r=await c(typeof e.answer==`string`?new TextEncoder().encode(e.answer):e.answer);n&&=t[e.path]===void 0||t[e.path]===`sha256:${r}`,t[e.path]=`sha256:${r}`}let r=new Map(Object.entries(t));for(let e of O){let{evidence:t}=e;for(let e of t?.exportOptions===void 0?t?.artifacts??[]:[])if(e.sourcePath!==void 0){let t=`sha256:${e.sha256}`;n&&=!r.has(e.sourcePath)||r.get(e.sourcePath)===t,r.set(e.sourcePath,t)}if(t?.sourcePath!==void 0&&t.exportOptions===void 0){let e=t.artifacts[0];if(e!==void 0){let i=`sha256:${e.sha256}`;n&&=!r.has(t.sourcePath)||r.get(t.sourcePath)===i,r.set(t.sourcePath,i)}}for(let[t,i]of Object.entries(e.evidence?.sourceRevision?.files??{}))n&&=!r.has(t)||r.get(t)===i,r.set(t,i)}return{status:n?O.some(e=>e.status!==`complete`)?`unavailable`:`complete`:`mixed`,modules:[{entryPath:m.entryPath,bundleSha256:await c(new TextEncoder().encode(w===void 0?e.code:e.code.replaceAll(w.runToken,T))),files:t,consistent:n}],loads:O}},i=e=>({discovered:x.tests.length,selected:e,completed:x.tests.filter(e=>e.status!==`not-run`&&e.status!==`skipped`).length,passed:x.tests.filter(e=>e.status===`passed`).length,failed:x.tests.filter(e=>e.status===`failed`).length,unsupported:x.tests.filter(e=>e.status===`unsupported`).length,inconclusive:x.tests.filter(e=>e.status===`inconclusive`).length,skipped:x.tests.filter(e=>e.status===`skipped`).length,notRun:x.tests.filter(e=>e.status===`not-run`).length});if(!n.success)return await k(),{success:!1,issues:n.issues,bundle:e,tests:x.tests,accounting:i(0),lineage:await r()};if(m.collectOnly===!0)return await x.waitForCompletion(m.testTimeout,/(?!)/u),await k(),{success:!0,passed:!1,tests:x.tests,bundle:e,accounting:i(0),lineage:await r()};await x.waitForCompletion(m.testTimeout,h.pattern),await k();let a=o(x.tests,h.pattern),s=await r();return{success:!0,passed:s.status===`complete`&&a.every(e=>e.status===`passed`||e.status===`skipped`),tests:a,bundle:e,accounting:i(a.length),lineage:s}}finally{await k(),M=!1,E.delete(T),E.size===0&&Reflect.deleteProperty(d,u),_.dispose()}}export{w as runGeoSpecModule};
@@ -710,10 +710,6 @@ type RunGeoSpecModuleOptions = {
710
710
  testNamePattern?: string | RegExp;
711
711
  /** Timeout for async test callbacks, in milliseconds. */
712
712
  testTimeout?: number;
713
- /** Milliseconds. Non-verdict matcher infrastructure backstop. */
714
- matcherWallBackstop?: number;
715
- /** Emit structured forensic events for this run. */
716
- forensic?: boolean;
717
713
  /** Observes each selected test as it starts and settles, for host progress reporting. */
718
714
  onTestEvent?: (event: GeoSpecTestEvent) => void;
719
715
  /**
@@ -2,5 +2,8 @@ import { DiscoverGeoSpecFilesOptions, GeoSpecDiscoveryFileSystem, GeoSpecDiscove
2
2
  import { GeoSpecTimeoutCode, geoSpecTimeoutCode, isGeoSpecTimeoutCode } from "../timeout-codes.mjs";
3
3
  import { createNoMatchingGeoSpecTestsIssue } from "./no-matching-tests-issue.mjs";
4
4
  import { GeoSpecPoolHostMessage, GeoSpecPoolShard, GeoSpecPoolWorkerHandle, GeoSpecPoolWorkerMessage } from "../pool/pool-messages.mjs";
5
- import { GeoSpecForensicEvent, GeoSpecRunner, GeoSpecRunnerEvent, GeoSpecRunnerFileResult, GeoSpecRunnerOptions, GeoSpecRunnerResult, GeoSpecRunnerRunOptions } from "./runner-types.mjs";
6
- export { type DiscoverGeoSpecFilesOptions, type GeoSpecDiscoveryFileSystem, type GeoSpecDiscoveryResult, type GeoSpecForensicEvent, type GeoSpecPoolHostMessage, type GeoSpecPoolShard, type GeoSpecPoolWorkerHandle, type GeoSpecPoolWorkerMessage, type GeoSpecRunner, type GeoSpecRunnerEvent, type GeoSpecRunnerFileResult, type GeoSpecRunnerOptions, type GeoSpecRunnerResult, type GeoSpecRunnerRunOptions, type GeoSpecTimeoutCode, createNoMatchingGeoSpecTestsIssue, discoverGeoSpecFiles, geoSpecTimeoutCode, isGeoSpecTimeoutCode };
5
+ import { GeoSpecRunner, GeoSpecRunnerEvent, GeoSpecRunnerFileResult, GeoSpecRunnerOptions, GeoSpecRunnerResult, GeoSpecRunnerRunOptions } from "./runner-types.mjs";
6
+ import { ShardTiming, ShardTimings } from "../pool/shard-timings.mjs";
7
+ import { GeoSpecPoolOptions, createGeoSpecPoolRunner } from "../pool/pool.mjs";
8
+ import { GeoSpecPoolWorkerHostOptions, startGeoSpecPoolWorkerHost } from "../pool/worker-host.mjs";
9
+ export { type DiscoverGeoSpecFilesOptions, type GeoSpecDiscoveryFileSystem, type GeoSpecDiscoveryResult, type GeoSpecPoolHostMessage, type GeoSpecPoolOptions, type GeoSpecPoolShard, type GeoSpecPoolWorkerHandle, type GeoSpecPoolWorkerHostOptions, type GeoSpecPoolWorkerMessage, type GeoSpecRunner, type GeoSpecRunnerEvent, type GeoSpecRunnerFileResult, type GeoSpecRunnerOptions, type GeoSpecRunnerResult, type GeoSpecRunnerRunOptions, type GeoSpecTimeoutCode, type ShardTiming, type ShardTimings, createGeoSpecPoolRunner, createNoMatchingGeoSpecTestsIssue, discoverGeoSpecFiles, geoSpecTimeoutCode, isGeoSpecTimeoutCode, startGeoSpecPoolWorkerHost };
@@ -1 +1 @@
1
- import{geoSpecTimeoutCode as e,isGeoSpecTimeoutCode as t}from"../timeout-codes.mjs";import{discoverGeoSpecFiles as n}from"../discovery.mjs";import{createNoMatchingGeoSpecTestsIssue as r}from"./no-matching-tests-issue.mjs";export{r as createNoMatchingGeoSpecTestsIssue,n as discoverGeoSpecFiles,e as geoSpecTimeoutCode,t as isGeoSpecTimeoutCode};
1
+ import{geoSpecTimeoutCode as e,isGeoSpecTimeoutCode as t}from"../timeout-codes.mjs";import{discoverGeoSpecFiles as n}from"../discovery.mjs";import{createNoMatchingGeoSpecTestsIssue as r}from"./no-matching-tests-issue.mjs";import{createGeoSpecPoolRunner as i}from"../pool/pool.mjs";import{startGeoSpecPoolWorkerHost as a}from"../pool/worker-host.mjs";export{i as createGeoSpecPoolRunner,r as createNoMatchingGeoSpecTestsIssue,n as discoverGeoSpecFiles,e as geoSpecTimeoutCode,t as isGeoSpecTimeoutCode,a as startGeoSpecPoolWorkerHost};
@@ -89,20 +89,12 @@ type GeoSpecRunnerEvent = {
89
89
  } | {
90
90
  type: 'run-complete';
91
91
  result: GeoSpecRunnerResult;
92
- } | GeoSpecForensicEvent | {
92
+ } | {
93
93
  type: 'abort';
94
94
  reason?: string;
95
95
  } | {
96
96
  type: 'close';
97
97
  };
98
- /** One structured forensic measurement emitted by a runner. @public */
99
- type GeoSpecForensicEvent = {
100
- type: 'forensic';
101
- name: string;
102
- value: number;
103
- unit: 'milliseconds' | 'count';
104
- shardId?: number;
105
- };
106
98
  /**
107
99
  * Options accepted by a GeoSpec worker-style runner run.
108
100
  *
@@ -115,16 +107,12 @@ type GeoSpecRunnerRunOptions = {
115
107
  testNamePattern?: string | RegExp;
116
108
  /** Timeout for each async test callback, in milliseconds. */
117
109
  testTimeout?: number;
118
- /** Non-verdict matcher wall backstop. Milliseconds. */
119
- matcherWallBackstop?: number;
120
110
  /**
121
111
  * Engine work-unit budget for this run's claims, replacing the runner's
122
112
  * `nativeAssertions.workUnitLimit`. A `loadModel` or `expectGeo` budget still
123
113
  * wins for its own claims.
124
114
  */
125
115
  workUnitLimit?: number;
126
- /** Emit structured forensic measurements for this run. */
127
- forensic?: boolean;
128
116
  /**
129
117
  * Stop after the first failing file (R1). Interactive fail-fast only —
130
118
  * never the default for reward runs, which want the complete red set.
@@ -164,4 +152,4 @@ type GeoSpecRunner = {
164
152
  close(): Promise<void>;
165
153
  };
166
154
  //#endregion
167
- export { GeoSpecForensicEvent, GeoSpecRunner, GeoSpecRunnerAccounting, GeoSpecRunnerEvent, GeoSpecRunnerFileResult, GeoSpecRunnerOptions, GeoSpecRunnerResult, GeoSpecRunnerRunOptions, GeoSpecRunnerTestRef };
155
+ export { GeoSpecRunner, GeoSpecRunnerAccounting, GeoSpecRunnerEvent, GeoSpecRunnerFileResult, GeoSpecRunnerOptions, GeoSpecRunnerResult, GeoSpecRunnerRunOptions, GeoSpecRunnerTestRef };