@polyengine/ct-runner 0.1.0-pre.g633468a

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/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # @polyengine/ct-runner
2
+
3
+ The polyengine execution runner for component-test-results (L1) conformance suites.
4
+
5
+ Part of [polyengine](https://github.com/polymorph-components/polyengine), a
6
+ WebAssembly Component Model host for JavaScript engines. This is the npm
7
+ distribution; the same package is published to JSR as `jsr:@polyengine/ct-runner`, and the
8
+ two are built from the same sources at the same version.
9
+
10
+ ```sh
11
+ npm install @polyengine/ct-runner
12
+ ```
13
+
14
+ ```js
15
+ import * as api from "@polyengine/ct-runner";
16
+ ```
17
+
18
+ Entry points: `.`, `./context`, `./imports`, `./run`.
19
+
20
+ ESM only, Node >= 22.14. Documentation, examples and the embedder API contract
21
+ live in the [repository](https://github.com/polymorph-components/polyengine).
22
+
23
+ Apache-2.0.
package/esm/context.js ADDED
@@ -0,0 +1,48 @@
1
+ // The host-side `test-context` provider (L1 contract's runner-side growth
2
+ // surface): a host-implemented resource class per contracts/embedder-api.md
3
+ // §"Resources" (host provides a plain class; the runtime owns instance<->rep
4
+ // mapping). The `context` resource's `diagnostic` is an `async func` import
5
+ // (WIT: wit/tests.wit `test-context.diagnostic: async func(msg: string)`,
6
+ // vendored at examples/guests/test-suite/wit/tests.wit) — per
7
+ // contracts/embedder-api.md §"Functions and async", an async-func import may
8
+ // be a plain `async` JS function.
9
+ //
10
+ // ARCHITECTURE.md Rule 3 ("introspecting host runner"): this file *is* the L2
11
+ // context-provider component, reimplemented host-side rather than composed in
12
+ // as wasm — permitted as long as it stays behaviorally equivalent to the
13
+ // layered path. It must never grow beyond what `test-context` names.
14
+ /** The frozen L1 interface id `test-context` is provided under. */
15
+ export const TEST_CONTEXT_INTERFACE = "polymorph:test/test-context@0.1.0";
16
+ /**
17
+ * One case's diagnostic sink. Host-side: no reps, no side tables — see
18
+ * contracts/embedder-api.md's host-implemented-resource column.
19
+ *
20
+ * `diagnostic` "may block cooperatively" per the WIT doc comment; this
21
+ * in-process host implementation never blocks (no backpressure to model),
22
+ * so it resolves immediately — still a valid `async func` implementation
23
+ * (contracts/embedder-api.md: "sync implementations remain legal").
24
+ */
25
+ export class Context {
26
+ #onDiagnostic;
27
+ constructor(onDiagnostic) {
28
+ this.#onDiagnostic = onDiagnostic;
29
+ }
30
+ // deno-lint-ignore require-await
31
+ async diagnostic(msg) {
32
+ this.#onDiagnostic(msg);
33
+ }
34
+ }
35
+ /**
36
+ * Build the `test-context` import-record entry
37
+ * (`{ [TEST_CONTEXT_INTERFACE]: { Context } }`), per contracts/embedder-api.md
38
+ * §"Module wiring and instantiation" (resource classes sit at the resource's
39
+ * position in the record, PascalCase).
40
+ *
41
+ * The class is registered once per `instantiate` call regardless of how many
42
+ * cases run against that instance; the runner never asks the guest to
43
+ * construct a `context` (the WIT resource has no constructor — the host
44
+ * always initiates the borrow itself when calling `run`).
45
+ */
46
+ export function testContextImportRecord() {
47
+ return { [TEST_CONTEXT_INTERFACE]: { Context } };
48
+ }
@@ -0,0 +1,67 @@
1
+ // Translate-only import analysis: `requiredImports()` (contracts/embedder-api.md
2
+ // §"Module wiring and instantiation") plus the runner's own test-context
3
+ // auto-wiring policy, without ever instantiating the suite. Used both by
4
+ // `runSuite` (to fail fast, before an async instantiate) and standalone (the
5
+ // "translate-only acceptance" gate: enumerate what a not-yet-executable suite
6
+ // is missing, without trying to run it).
7
+ import { NameCollisionError, requiredImports, } from "@polyengine/runtime/embedder";
8
+ import { ImportResolver } from "@polyengine/runtime/embedder";
9
+ import { TEST_CONTEXT_INTERFACE } from "./context.js";
10
+ /** One or more of the suite's import leaves cannot be resolved (yet). */
11
+ export class MissingImportsError extends Error {
12
+ /** The unresolved leaves, in `requiredImports()` order. */
13
+ leaves;
14
+ /** The distinct top-level record keys (interface ids / bare names) missing. */
15
+ missing;
16
+ constructor(missing, leaves) {
17
+ super(`suite is missing ${missing.length} host import(s): ` +
18
+ missing.map((m) => `'${m}'`).join(", "));
19
+ this.name = "MissingImportsError";
20
+ this.missing = missing;
21
+ this.leaves = leaves;
22
+ }
23
+ }
24
+ /**
25
+ * Analyze a suite's import surface against a caller-provided imports record,
26
+ * WITHOUT instantiating anything (translate-only; `requiredImports` reads
27
+ * only the plan). Detects:
28
+ * - whether `test-context` is imported (only then does the runner provide
29
+ * it — pre-composed bundles with the provider already linked must work
30
+ * too, per the dispatch);
31
+ * - a caller/runner collision on `test-context` (the runner "merges,
32
+ * erroring on collisions" per the dispatch's import-wiring spec);
33
+ * - every other top-level import key the suite needs but `provided` lacks,
34
+ * via the same version-canonical resolution `instantiate` itself uses
35
+ * (`ImportResolver`, contracts/embedder-api.md §"Version canonicalization").
36
+ */
37
+ export function analyzeImports(plan, provided = {}) {
38
+ const leaves = requiredImports(plan);
39
+ const requiresTestContext = leaves.some((l) => l.interfaceId === TEST_CONTEXT_INTERFACE);
40
+ if (requiresTestContext && TEST_CONTEXT_INTERFACE in provided) {
41
+ throw new NameCollisionError(`'${TEST_CONTEXT_INTERFACE}' was provided explicitly, but this suite ` +
42
+ `imports test-context and the ct-runner always supplies it itself ` +
43
+ `(contracts/embedder-api.md's "merges, erroring on collisions" ` +
44
+ `policy). Remove it from your imports record — a pre-composed ` +
45
+ `bundle that already links a provider should not import ` +
46
+ `test-context in the first place.`);
47
+ }
48
+ const resolver = new ImportResolver(provided);
49
+ const groups = new Set(leaves.map((l) => l.interfaceId));
50
+ const missing = [];
51
+ for (const id of groups) {
52
+ if (requiresTestContext && id === TEST_CONTEXT_INTERFACE)
53
+ continue; // the runner supplies it
54
+ if (resolver.resolve(id) === undefined)
55
+ missing.push(id);
56
+ }
57
+ return { leaves, requiresTestContext, missing };
58
+ }
59
+ /** `analyzeImports`, throwing `MissingImportsError` if anything is missing. */
60
+ export function requireImportsResolved(plan, provided = {}) {
61
+ const analysis = analyzeImports(plan, provided);
62
+ if (analysis.missing.length > 0) {
63
+ const leaves = analysis.leaves.filter((l) => analysis.missing.includes(l.interfaceId));
64
+ throw new MissingImportsError(analysis.missing, leaves);
65
+ }
66
+ return analysis;
67
+ }
package/esm/mod.js ADDED
@@ -0,0 +1,12 @@
1
+ // ct-runner: polyengine's L3 execution runner for polymorph-test's L1
2
+ // suite contract (TRACK C2-D; docs/consumers.md, C2).
3
+ //
4
+ // "Execute an L1 suite component, emit canonical results JSONL (L4)" — an
5
+ // introspecting host runner (ARCHITECTURE.md Rule 3): it drives
6
+ // `polymorph:test/tests@0.1.0` directly against a host-side `test-context`
7
+ // provider, never composing wasm for L2. See src/run-suite.ts for the case
8
+ // loop and src/context.ts for the host resource.
9
+ export { runSuite, TESTS_INTERFACE, } from "./run-suite.js";
10
+ export { analyzeImports, MissingImportsError, requireImportsResolved, } from "./import-analysis.js";
11
+ export { Context, TEST_CONTEXT_INTERFACE, testContextImportRecord } from "./context.js";
12
+ export { applies, collectTagsSections, firstExcluding, loadTagsInventory, parseTagsRecords, TAGS_SECTION, tagsOf, } from "./tags.js";
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,299 @@
1
+ // The L3 case-loop driver: instantiate the suite behind the embedder
2
+ // conventions, enumerate cases, execute them, emit canonical L4 results
3
+ // JSONL. Mirrors the semantics of polymorph-test's own JS legs
4
+ // (js/viewer/harness.mjs `runCases`/`runSuiteJsonl`) rather than reinventing
5
+ // a policy — an "introspecting host runner" per ARCHITECTURE.md Rule 3 must
6
+ // stay behaviorally equivalent to the layered path.
7
+ //
8
+ // L4 schema authority: polymorph-test crates/component-test-results/src/lib.rs
9
+ // (`Envelope`, `RunInfo`, `CaseResult`, `Status`, `Provenance`,
10
+ // `wire_vocabulary_pinned` test) — the canonical wire types; cross-checked
11
+ // against expected/verify-pipeline-fixture.jsonl and
12
+ // expected/verify-compose-sample.jsonl (golden samples of the same format).
13
+ import { instantiate, Trap, ComponentException, } from "@polyengine/runtime/embedder";
14
+ import { Context, testContextImportRecord } from "./context.js";
15
+ import { requireImportsResolved } from "./import-analysis.js";
16
+ import { applies, firstExcluding, loadTagsInventory, tagsOf, } from "./tags.js";
17
+ /** The suite's `tests` interface id (wit/tests.wit `interface tests`, v0.1.0). */
18
+ export const TESTS_INTERFACE = "polymorph:test/tests@0.1.0";
19
+ /** `js/viewer/harness.mjs`'s `resolveTestsExport`, ported: the suite's
20
+ * `tests` interface from an instantiated component, whichever spelling the
21
+ * producer used (verbatim interface id is what this runtime always uses,
22
+ * but the fallback costs nothing and documents the contract). */
23
+ // deno-lint-ignore no-explicit-any
24
+ function resolveTestsExport(exports) {
25
+ const tests = exports[TESTS_INTERFACE] ?? exports["tests"];
26
+ if (tests === undefined) {
27
+ throw new Error(`suite instance exports no '${TESTS_INTERFACE}' interface: ` +
28
+ `${Object.keys(exports)}`);
29
+ }
30
+ return tests;
31
+ }
32
+ async function sha256Hex(bytes) {
33
+ const digest = await crypto.subtle.digest("SHA-256", bytes.slice().buffer);
34
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0"))
35
+ .join("");
36
+ }
37
+ function describeThrow(e) {
38
+ return e instanceof Error ? `${e.name}: ${e.message}` : String(e);
39
+ }
40
+ /**
41
+ * Run one suite end to end: instantiate, enumerate, execute every case,
42
+ * emit the complete results-JSONL stream (envelope, one line per case,
43
+ * terminator) through `opts.emit`. Throws `MissingImportsError` up front
44
+ * (contracts/embedder-api.md's `requiredImports`) if the caller's imports
45
+ * cannot satisfy the suite, and a plain `Error` if the census is empty (an
46
+ * empty selection is a run error, per component-test-results/src/lib.rs's
47
+ * `fold_jsonl` and harness.mjs's `runSuiteJsonl` — both refuse it).
48
+ */
49
+ export async function runSuite(artifacts, opts) {
50
+ const provided = opts.imports ?? {};
51
+ // Fail fast, translate-only, before any instantiate (gate 3's contract):
52
+ // detect whether test-context is imported at all — a pre-composed bundle
53
+ // with the provider already linked in must work with no test-context
54
+ // wiring — and merge, erroring on any caller/runner collision.
55
+ const analysis = requireImportsResolved(artifacts.plan, provided);
56
+ const mergedImports = analysis.requiresTestContext
57
+ ? { ...provided, ...testContextImportRecord() }
58
+ : provided;
59
+ const freshCases = opts.freshCases ?? true;
60
+ // Validate `shard` loudly (issue #110): integers, count >= 1, index in
61
+ // [0, count). Fail fast, before any instantiate, same posture as the
62
+ // imports/tags validation above.
63
+ if (opts.shard !== undefined) {
64
+ const { index, count } = opts.shard;
65
+ if (!Number.isInteger(count) || count < 1) {
66
+ throw new Error(`shard.count must be an integer >= 1, got ${count}`);
67
+ }
68
+ if (!Number.isInteger(index) || index < 0 || index >= count) {
69
+ throw new Error(`shard.index must be an integer in [0, ${count}), got ${index}`);
70
+ }
71
+ }
72
+ const newTests = async () => {
73
+ const inst = await instantiate(artifacts, mergedImports, {
74
+ jspi: opts.jspi,
75
+ });
76
+ return resolveTestsExport(inst.exports);
77
+ };
78
+ const censusTests = await newTests();
79
+ const census = await censusTests.all();
80
+ // Feature-tag scheduling (issue #25): gate on the suite's own
81
+ // `component-test:tags@0.1` inventory when it has one — the SDK embeds it
82
+ // in the guest core module and it survives wac composition, so this
83
+ // introspecting runner CAN see it (revising the earlier "cannot see the
84
+ // tags section" stance recorded below). Suites without an inventory run
85
+ // feature-blind exactly as before.
86
+ const inventory = loadTagsInventory(artifacts.componentBytes);
87
+ const missing = opts.missing ?? [];
88
+ if (inventory === null && opts.missing !== undefined) {
89
+ throw new Error("missing-features given, but the suite carries no " +
90
+ "component-test:tags@0.1 inventory (not built with their SDK, or " +
91
+ "sections stripped) — tag gating is impossible, refusing to " +
92
+ "silently run feature-blind");
93
+ }
94
+ const suiteName = opts.suiteName.replaceAll("-", "_");
95
+ const artifactSha256 = await sha256Hex(artifacts.componentBytes);
96
+ opts.emit(JSON.stringify({
97
+ "component-test-results": "0.1",
98
+ target: opts.target,
99
+ suite: { name: suiteName, "artifact-sha256": artifactSha256 },
100
+ // "tags" when this run schedules against the suite's tag inventory,
101
+ // "none" for inventory-less suites (component-test-results/src/lib.rs
102
+ // `RunInfo`: "none" is for producers that cannot see the tags section
103
+ // — with the inventory in hand, this runner no longer is one).
104
+ run: { segment: 0, scheduling: inventory !== null ? "tags" : "none" },
105
+ }));
106
+ if (census.length === 0) {
107
+ // Both authorities refuse this: component-test-results/src/lib.rs
108
+ // `fold_jsonl` ("empty selection is a run error") and harness.mjs
109
+ // `runSuiteJsonl` ("suite enumerated zero cases").
110
+ throw new Error("suite enumerated zero cases (empty selection is a run error)");
111
+ }
112
+ const counts = { passed: 0, failed: 0, skipped: 0, na: 0, total: 0 };
113
+ for (const [i, testCase] of census.entries()) {
114
+ // Stripe membership (issue #110) is decided on the census index `i`,
115
+ // BEFORE `only`/tag filtering — a case outside this shard's stripe is
116
+ // skipped with no emit and no count contribution, as if this shard's
117
+ // census never enumerated it at all.
118
+ if (opts.shard && i % opts.shard.count !== opts.shard.index)
119
+ continue;
120
+ const name = String(await testCase.name());
121
+ counts.total++;
122
+ // js/viewer/harness.mjs `runCases`: "if (only && !name.includes(only))
123
+ // continue" — a filtered-out case is skipped entirely, no emit.
124
+ if (opts.only && !name.includes(opts.only))
125
+ continue;
126
+ // harness.mjs `runCases` mark scheduling, in its exact order: `only`
127
+ // first (above), then drift, then applicability. The N/A row's shape is
128
+ // the embed runner's (expected/verify-pipeline-fixture.jsonl):
129
+ // status, first excluding mark as detail, diagnostics-complete true.
130
+ if (inventory !== null) {
131
+ const tags = tagsOf(inventory, name);
132
+ if (tags === undefined) {
133
+ throw new Error(`inventory drift: no tags record covers ${name}`);
134
+ }
135
+ if (!applies(tags, missing)) {
136
+ counts.na++;
137
+ opts.emit(JSON.stringify({
138
+ case: name,
139
+ status: "not-applicable",
140
+ detail: firstExcluding(tags, missing),
141
+ "diagnostics-complete": true,
142
+ }), i);
143
+ opts.log?.(`${name} … not-applicable`);
144
+ continue;
145
+ }
146
+ }
147
+ // js/viewer/harness.mjs `runCases`' `freshCases` branch: re-enumerate
148
+ // from a fresh instance and run the matching case; a vanished case is
149
+ // inventory drift, not a failing case, and throws.
150
+ let executed = testCase;
151
+ if (freshCases) {
152
+ const freshTests = await newTests();
153
+ const freshList = await freshTests.all();
154
+ const match = await findByName(freshList, name, i);
155
+ if (match === undefined) {
156
+ throw new Error(`case '${name}' vanished on re-enumeration`);
157
+ }
158
+ executed = match;
159
+ }
160
+ const diags = [];
161
+ // The host-side `test-context` sideband: `diagnostic` calls are consumed
162
+ // concurrently with `run` per wit/tests.wit's doc comment — here that is
163
+ // automatic (same event loop turn, synchronous push into `diags`).
164
+ const ctx = new Context((msg) => diags.push(msg));
165
+ const start = performance.now();
166
+ // deno-lint-ignore no-explicit-any
167
+ let event;
168
+ try {
169
+ const attempt = executed.run(ctx);
170
+ let timedOut = false;
171
+ if (opts.caseTimeoutMs) {
172
+ let timer;
173
+ timedOut = await Promise.race([
174
+ attempt.then(() => false),
175
+ new Promise((resolve) => {
176
+ timer = setTimeout(() => resolve(true), opts.caseTimeoutMs);
177
+ }),
178
+ ]).finally(() => clearTimeout(timer));
179
+ }
180
+ else {
181
+ await attempt;
182
+ }
183
+ const durationMs = Math.round(performance.now() - start);
184
+ if (timedOut) {
185
+ counts.failed++;
186
+ event = {
187
+ case: name,
188
+ status: "fail",
189
+ provenance: { "limit-exceeded": "case-timeout" },
190
+ detail: `case timeout exceeded (${(opts.caseTimeoutMs / 1000)}s)`,
191
+ "duration-ms": durationMs,
192
+ "diagnostics-complete": false,
193
+ };
194
+ }
195
+ else {
196
+ counts.passed++;
197
+ event = {
198
+ case: name,
199
+ status: "pass",
200
+ provenance: "returned",
201
+ "duration-ms": durationMs,
202
+ // The case returned normally, so its diagnostics sideband is
203
+ // complete (upstream emits this on every returned row; polyengine's
204
+ // trap/timeout rows already carry `false`).
205
+ "diagnostics-complete": true,
206
+ };
207
+ }
208
+ }
209
+ catch (e) {
210
+ const durationMs = Math.round(performance.now() - start);
211
+ if (e instanceof ComponentException) {
212
+ const payload = e.payload;
213
+ if (payload?.kind === "failed") {
214
+ counts.failed++;
215
+ event = {
216
+ case: name,
217
+ status: "fail",
218
+ provenance: "returned",
219
+ detail: payload.value,
220
+ "duration-ms": durationMs,
221
+ "diagnostics-complete": true,
222
+ };
223
+ }
224
+ else if (payload?.kind === "skipped") {
225
+ counts.skipped++;
226
+ event = {
227
+ case: name,
228
+ status: "skipped",
229
+ provenance: "returned",
230
+ detail: payload.value,
231
+ "duration-ms": durationMs,
232
+ "diagnostics-complete": true,
233
+ };
234
+ }
235
+ else {
236
+ // Contract violation: `outcome` has exactly two cases. Treat as
237
+ // this case's failure, same as a trap (the run() promise made a
238
+ // verdict-shaped claim the runner cannot parse).
239
+ counts.failed++;
240
+ event = {
241
+ case: name,
242
+ status: "fail",
243
+ provenance: "trap",
244
+ detail: `run() rejected with an unrecognized outcome payload: ` +
245
+ `${JSON.stringify(payload)}`,
246
+ "duration-ms": durationMs,
247
+ "diagnostics-complete": false,
248
+ };
249
+ }
250
+ }
251
+ else {
252
+ // Trap (real wasm trap, or any unbranded throw): "a runner treats a
253
+ // trap as this case's failure and the suite instance as poisoned"
254
+ // (wit/tests.wit `test-case.run` doc comment) — poisoning is moot
255
+ // under `freshCases` (the default), since the NEXT case gets a fresh
256
+ // instance regardless.
257
+ counts.failed++;
258
+ const isTrap = e instanceof Trap;
259
+ event = {
260
+ case: name,
261
+ status: "fail",
262
+ provenance: "trap",
263
+ detail: `trap: ${isTrap ? e.message : describeThrow(e)}`,
264
+ "duration-ms": durationMs,
265
+ "diagnostics-complete": false,
266
+ };
267
+ }
268
+ }
269
+ if (diags.length > 0)
270
+ event.diagnostics = diags;
271
+ opts.emit(JSON.stringify(event), i);
272
+ opts.log?.(`${name} … ${event.status}`);
273
+ }
274
+ opts.emit('{"segment-end":true}');
275
+ return counts;
276
+ }
277
+ // deno-lint-ignore no-explicit-any
278
+ async function findByName(list, name, hint) {
279
+ // Same-index fast path. Enumeration order is a hint, not a contract: real
280
+ // suites enumerate deterministically, so the re-enumerated case is
281
+ // virtually always at its census index — one name() round-trip instead of
282
+ // a front-to-back scan. The scan (harness.mjs's freshCases branch, which
283
+ // the fallback below mirrors verbatim) is quadratic in suite size, and
284
+ // each name() here is an interpreted CABI call (~8 us): a 19k-case suite
285
+ // pays ~182M of them, dominating the run's wall clock. An order-unstable
286
+ // suite just misses the hint and falls back; drift detection is unchanged
287
+ // (`undefined` still means the case vanished).
288
+ if (hint !== undefined && hint < list.length) {
289
+ const candidate = list[hint];
290
+ if (String(await candidate.name()) === name)
291
+ return candidate;
292
+ }
293
+ for (const c of list) {
294
+ if (String(await c.name()) === name)
295
+ return c;
296
+ }
297
+ return undefined;
298
+ }
299
+ export { analyzeImports, MissingImportsError, requireImportsResolved } from "./import-analysis.js";
package/esm/tags.js ADDED
@@ -0,0 +1,164 @@
1
+ // Feature-tag scheduling (issue #25): the L0 tags inventory and the
2
+ // applicability rule, ported from polymorph-test's authorities —
3
+ //
4
+ // - Section format: crates/component-test-formats/src/inventory.rs
5
+ // (`collect_tags_sections` / `parse_tags_records`): newline-delimited
6
+ // `name tag...` text records in `component-test:tags@0.1` custom
7
+ // sections, collected from the component AND nested modules/components
8
+ // (their reader uses wasmparser's `parse_all`, which descends; the
9
+ // SDK's `#[link_section]` puts the records in the guest CORE module,
10
+ // and those survive wac composition — verified empirically on the
11
+ // polymorph-tls composed suites). Records are newline-delimited within
12
+ // a section; a producer may omit the final newline, so a newline is
13
+ // repaired per section before concatenation, exactly as upstream does.
14
+ // - Record forms: `name tag...` (exact) and `prefix/* tag...` (generated
15
+ // rows: leaves are enumerated at run time below the prefix).
16
+ // - Applicability: crates/component-test-core/src/tags.rs — `feature`
17
+ // requires the target to HAVE the feature, `!feature` requires it to
18
+ // LACK it; a case applies iff every mark is satisfied against the
19
+ // runner's missing-features list. js/viewer/harness.mjs `applies()` is
20
+ // the JS-leg reference this mirrors.
21
+ // - Drift policy: harness.mjs `runCases` — an enumerated case that no
22
+ // record covers throws (the run is unsound, not failing).
23
+ /** The custom-section name (component-test-core `name::TAGS_SECTION`). */
24
+ export const TAGS_SECTION = "component-test:tags@0.1";
25
+ const MAGIC = [0x00, 0x61, 0x73, 0x6d]; // "\0asm"
26
+ function hasWasmMagic(bytes, at = 0) {
27
+ return bytes.length >= at + 8 && MAGIC.every((b, i) => bytes[at + i] === b);
28
+ }
29
+ /** u32 LEB128 at `pos`; returns [value, nextPos]. Traps on overlong/EOF. */
30
+ function lebU32(bytes, pos) {
31
+ let result = 0;
32
+ let shift = 0;
33
+ for (;;) {
34
+ if (pos >= bytes.length)
35
+ throw new Error("tags scan: truncated LEB128");
36
+ const b = bytes[pos++];
37
+ result |= (b & 0x7f) << shift;
38
+ if ((b & 0x80) === 0)
39
+ break;
40
+ shift += 7;
41
+ if (shift >= 35)
42
+ throw new Error("tags scan: LEB128 too long for u32");
43
+ }
44
+ return [result >>> 0, pos];
45
+ }
46
+ /**
47
+ * Collect the concatenated bytes of every `component-test:tags@0.1` custom
48
+ * section in `bytes` — the component's own sections plus those of nested
49
+ * core modules (section id 1) and nested components (section id 4), which
50
+ * both embed complete wasm binaries. Returns null when no section exists
51
+ * anywhere (a suite not built with their SDK).
52
+ */
53
+ export function collectTagsSections(bytes) {
54
+ if (!hasWasmMagic(bytes))
55
+ throw new Error("tags scan: not a wasm binary");
56
+ const chunks = [];
57
+ const decoder = new TextDecoder();
58
+ const NL = new Uint8Array([0x0a]);
59
+ const scan = (buf, core) => {
60
+ let pos = 8; // magic + version/layer
61
+ while (pos < buf.length) {
62
+ const id = buf[pos++];
63
+ const [size, afterSize] = lebU32(buf, pos);
64
+ pos = afterSize;
65
+ const end = pos + size;
66
+ if (end > buf.length)
67
+ throw new Error("tags scan: truncated section");
68
+ if (id === 0) {
69
+ const [nameLen, afterName] = lebU32(buf, pos);
70
+ const nameEnd = afterName + nameLen;
71
+ if (nameEnd > end)
72
+ throw new Error("tags scan: truncated custom name");
73
+ if (decoder.decode(buf.subarray(afterName, nameEnd)) === TAGS_SECTION) {
74
+ const data = buf.subarray(nameEnd, end);
75
+ chunks.push(data);
76
+ // Newline repair per section (inventory.rs: nothing guarantees a
77
+ // producer terminates its last record).
78
+ if (data.length === 0 || data[data.length - 1] !== 0x0a) {
79
+ chunks.push(NL);
80
+ }
81
+ }
82
+ }
83
+ else if (!core && (id === 1 || id === 4)) {
84
+ // 1 = core module, 4 = nested component: payload is a full binary.
85
+ const payload = buf.subarray(pos, end);
86
+ if (hasWasmMagic(payload))
87
+ scan(payload, id === 1);
88
+ }
89
+ pos = end;
90
+ }
91
+ };
92
+ scan(bytes, false);
93
+ if (chunks.length === 0)
94
+ return null;
95
+ const total = chunks.reduce((n, c) => n + c.length, 0);
96
+ const out = new Uint8Array(total);
97
+ let off = 0;
98
+ for (const c of chunks) {
99
+ out.set(c, off);
100
+ off += c.length;
101
+ }
102
+ return out;
103
+ }
104
+ /**
105
+ * Parse concatenated records (inventory.rs `parse_tags_records`): one
106
+ * record per line, `name tag...`, blank lines skipped, duplicate names and
107
+ * empty tags rejected. Grammar validation beyond that (WIT-label checks)
108
+ * is the producer's job — their SDK validates at macro-expansion time.
109
+ */
110
+ export function parseTagsRecords(bytes) {
111
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
112
+ const inv = { exact: new Map(), prefixes: [] };
113
+ const seen = new Set();
114
+ for (const line of text.split("\n")) {
115
+ if (line.trim() === "")
116
+ continue;
117
+ const parts = line.split(" ").filter((p) => p !== "");
118
+ const name = parts[0];
119
+ const tags = parts.slice(1);
120
+ for (const t of tags) {
121
+ if (t === "" || t === "!") {
122
+ throw new Error(`tags section: empty mark on \`${name}\``);
123
+ }
124
+ }
125
+ if (seen.has(name)) {
126
+ throw new Error(`tags section: duplicate record \`${name}\``);
127
+ }
128
+ seen.add(name);
129
+ const prefix = name.endsWith("/*") ? name.slice(0, -2) : null;
130
+ if (prefix !== null) {
131
+ inv.prefixes.push({ prefix, tags });
132
+ }
133
+ else {
134
+ inv.exact.set(name, tags);
135
+ }
136
+ }
137
+ return inv;
138
+ }
139
+ /** Convenience: scan + parse; null when the suite carries no inventory. */
140
+ export function loadTagsInventory(bytes) {
141
+ const sections = collectTagsSections(bytes);
142
+ return sections === null ? null : parseTagsRecords(sections);
143
+ }
144
+ /** The tags covering `name`: exact record, else a generated-row prefix
145
+ * record (leaves live below `prefix/`), else undefined (inventory drift). */
146
+ export function tagsOf(inv, name) {
147
+ const exact = inv.exact.get(name);
148
+ if (exact !== undefined)
149
+ return exact;
150
+ for (const { prefix, tags } of inv.prefixes) {
151
+ if (name.startsWith(prefix + "/"))
152
+ return tags;
153
+ }
154
+ return undefined;
155
+ }
156
+ /** harness.mjs `applies()`: `!f` needs f missing; `f` needs f present. */
157
+ export function applies(tags, missing) {
158
+ return tags.every((t) => t.startsWith("!") ? missing.includes(t.slice(1)) : !missing.includes(t));
159
+ }
160
+ /** The N/A row's `detail`: the first unsatisfied mark (harness.mjs's
161
+ * `excluding`), empty string if somehow none (mirrors `excluding ?? ""`). */
162
+ export function firstExcluding(tags, missing) {
163
+ return tags.find((t) => t.startsWith("!") ? !missing.includes(t.slice(1)) : missing.includes(t)) ?? "";
164
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@polyengine/ct-runner",
3
+ "version": "0.1.0-pre.g633468a",
4
+ "description": "The polyengine execution runner for component-test-results (L1) conformance suites.",
5
+ "homepage": "https://github.com/polymorph-components/polyengine#readme",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/polymorph-components/polyengine.git"
9
+ },
10
+ "license": "Apache-2.0",
11
+ "bugs": {
12
+ "url": "https://github.com/polymorph-components/polyengine/issues"
13
+ },
14
+ "module": "./esm/mod.js",
15
+ "types": "./types/mod.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "import": {
19
+ "types": "./types/mod.d.ts",
20
+ "default": "./esm/mod.js"
21
+ }
22
+ },
23
+ "./context": {
24
+ "import": {
25
+ "types": "./types/context.d.ts",
26
+ "default": "./esm/context.js"
27
+ }
28
+ },
29
+ "./imports": {
30
+ "import": {
31
+ "types": "./types/import-analysis.d.ts",
32
+ "default": "./esm/import-analysis.js"
33
+ }
34
+ },
35
+ "./run": {
36
+ "import": {
37
+ "types": "./types/run-suite.d.ts",
38
+ "default": "./esm/run-suite.js"
39
+ }
40
+ },
41
+ "./package.json": "./package.json"
42
+ },
43
+ "scripts": {},
44
+ "type": "module",
45
+ "engines": {
46
+ "node": ">=22.14.0"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "dependencies": {
52
+ "@polyengine/runtime": "0.1.0-pre.g633468a"
53
+ },
54
+ "_generatedBy": "dnt@0.43.2"
55
+ }
@@ -0,0 +1,28 @@
1
+ /** The frozen L1 interface id `test-context` is provided under. */
2
+ export declare const TEST_CONTEXT_INTERFACE = "polymorph:test/test-context@0.1.0";
3
+ /**
4
+ * One case's diagnostic sink. Host-side: no reps, no side tables — see
5
+ * contracts/embedder-api.md's host-implemented-resource column.
6
+ *
7
+ * `diagnostic` "may block cooperatively" per the WIT doc comment; this
8
+ * in-process host implementation never blocks (no backpressure to model),
9
+ * so it resolves immediately — still a valid `async func` implementation
10
+ * (contracts/embedder-api.md: "sync implementations remain legal").
11
+ */
12
+ export declare class Context {
13
+ #private;
14
+ constructor(onDiagnostic: (msg: string) => void);
15
+ diagnostic(msg: string): Promise<void>;
16
+ }
17
+ /**
18
+ * Build the `test-context` import-record entry
19
+ * (`{ [TEST_CONTEXT_INTERFACE]: { Context } }`), per contracts/embedder-api.md
20
+ * §"Module wiring and instantiation" (resource classes sit at the resource's
21
+ * position in the record, PascalCase).
22
+ *
23
+ * The class is registered once per `instantiate` call regardless of how many
24
+ * cases run against that instance; the runner never asks the guest to
25
+ * construct a `context` (the WIT resource has no constructor — the host
26
+ * always initiates the borrow itself when calling `run`).
27
+ */
28
+ export declare function testContextImportRecord(): Record<string, unknown>;
@@ -0,0 +1,35 @@
1
+ import { type ImportLeaf, type PlanLike } from "@polyengine/runtime/embedder";
2
+ /** One or more of the suite's import leaves cannot be resolved (yet). */
3
+ export declare class MissingImportsError extends Error {
4
+ /** The unresolved leaves, in `requiredImports()` order. */
5
+ readonly leaves: ImportLeaf[];
6
+ /** The distinct top-level record keys (interface ids / bare names) missing. */
7
+ readonly missing: string[];
8
+ constructor(missing: string[], leaves: ImportLeaf[]);
9
+ }
10
+ export interface ImportAnalysis {
11
+ /** Every linkable import leaf (`requiredImports()`, unfiltered). */
12
+ leaves: ImportLeaf[];
13
+ /** Whether this suite imports `test-context` at all (world-shaped suites do;
14
+ * a pre-composed bundle with the provider already linked in does not). */
15
+ requiresTestContext: boolean;
16
+ /** Top-level keys the suite needs that `provided` (plus test-context, if
17
+ * this runner will supply it) does not resolve. Empty when runnable. */
18
+ missing: string[];
19
+ }
20
+ /**
21
+ * Analyze a suite's import surface against a caller-provided imports record,
22
+ * WITHOUT instantiating anything (translate-only; `requiredImports` reads
23
+ * only the plan). Detects:
24
+ * - whether `test-context` is imported (only then does the runner provide
25
+ * it — pre-composed bundles with the provider already linked must work
26
+ * too, per the dispatch);
27
+ * - a caller/runner collision on `test-context` (the runner "merges,
28
+ * erroring on collisions" per the dispatch's import-wiring spec);
29
+ * - every other top-level import key the suite needs but `provided` lacks,
30
+ * via the same version-canonical resolution `instantiate` itself uses
31
+ * (`ImportResolver`, contracts/embedder-api.md §"Version canonicalization").
32
+ */
33
+ export declare function analyzeImports(plan: PlanLike, provided?: Record<string, unknown>): ImportAnalysis;
34
+ /** `analyzeImports`, throwing `MissingImportsError` if anything is missing. */
35
+ export declare function requireImportsResolved(plan: PlanLike, provided?: Record<string, unknown>): ImportAnalysis;
package/types/mod.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { type RunCounts, runSuite, type RunSuiteOptions, TESTS_INTERFACE, } from "./run-suite.js";
2
+ export { analyzeImports, type ImportAnalysis, MissingImportsError, requireImportsResolved, } from "./import-analysis.js";
3
+ export { Context, TEST_CONTEXT_INTERFACE, testContextImportRecord } from "./context.js";
4
+ export { applies, collectTagsSections, firstExcluding, loadTagsInventory, parseTagsRecords, TAGS_SECTION, type TagsInventory, tagsOf, } from "./tags.js";
@@ -0,0 +1,117 @@
1
+ import { type ComponentArtifacts } from "@polyengine/runtime/embedder";
2
+ /** The suite's `tests` interface id (wit/tests.wit `interface tests`, v0.1.0). */
3
+ export declare const TESTS_INTERFACE = "polymorph:test/tests@0.1.0";
4
+ export interface RunSuiteOptions {
5
+ /**
6
+ * Host imports for everything the suite needs OTHER than test-context
7
+ * (WASI, SUT interfaces, …) — the same shape `instantiate` itself takes
8
+ * (contracts/embedder-api.md §"Module wiring and instantiation"). The
9
+ * runner adds `test-context` itself and errors on a collision (dispatch's
10
+ * import-wiring spec); omit it here.
11
+ */
12
+ imports?: Record<string, unknown>;
13
+ /** Envelope `target` (opaque implementation x environment key). */
14
+ target: string;
15
+ /**
16
+ * Envelope `suite.name`. Per js/viewer/harness.mjs's `envelope()`: "the
17
+ * suite name is normalized to the lockfile identity — the wasm file stem,
18
+ * underscores"; callers may pass the kebab-case name as-is, this function
19
+ * does the same normalization (`replaceAll("-", "_")`).
20
+ */
21
+ suiteName: string;
22
+ /** Substring filter: non-matching cases are skipped entirely (no emit),
23
+ * per js/viewer/harness.mjs `runCases`'s `only` handling. */
24
+ only?: string;
25
+ /**
26
+ * Feature-tag scheduling (issue #25): the features this target LACKS —
27
+ * js/viewer/harness.mjs's `missing`. Tag-gating activates whenever the
28
+ * suite carries a `component-test:tags@0.1` inventory (src/tags.ts):
29
+ * non-applicable cases emit `not-applicable` rows instead of executing,
30
+ * and an enumerated case no record covers throws (inventory drift — the
31
+ * run is unsound, not failing). Passing `missing` for a suite WITHOUT an
32
+ * inventory is an error (gating requested but impossible — upstream's
33
+ * runner refuses the same way rather than silently degrading).
34
+ */
35
+ missing?: string[];
36
+ /**
37
+ * Per-case wall-clock budget in ms (the `--case-timeout` runner option
38
+ * documented in harness.mjs's `runSuiteJsonl` doc comment). On expiry the
39
+ * case fails with `{"limit-exceeded":"case-timeout"}` provenance and the
40
+ * loop moves on; JSPI attempts cannot be cancelled, so this is only safe
41
+ * paired with `freshCases` (the default) — see below.
42
+ */
43
+ caseTimeoutMs?: number;
44
+ /**
45
+ * Fresh suite instance per case (default true). harness.mjs's doc comment
46
+ * on `runSuiteJsonl`'s `freshCases` parameter: "a fresh instance per case
47
+ * ... contains trap poisoning" and is required to pair with
48
+ * `caseTimeoutMs` (an abandoned JSPI attempt keeps running until its
49
+ * instance is dropped). Setting this false reuses one instance for the
50
+ * whole run — legal, but a trapped case can poison every later one, exactly
51
+ * as harness.mjs warns.
52
+ */
53
+ freshCases?: boolean;
54
+ /** Opt in to JSPI-backed suspension; passed through to `instantiate`. */
55
+ jspi?: boolean;
56
+ /**
57
+ * Stripe this run to one shard of the suite (issue #110): case `i`
58
+ * (i = the census index — `census.entries()`'s index over the FULL
59
+ * enumerated case list, before `only`/tag filtering) belongs to shard
60
+ * `i % count`; this shard executes and emits only its own cases, mirroring
61
+ * `runCases`' established `i % count === index` striping semantics
62
+ * (striping, not contiguous ranges, balances load since expensive cases
63
+ * cluster by group). `only`/tag-gating are applied AFTER stripe
64
+ * membership is decided (a case not in this stripe is neither executed
65
+ * nor emitted, exactly as if it never existed for this shard) — this is
66
+ * the interpretation that keeps the invariant "the union of every shard's
67
+ * rows, in suite order, equals the unsharded run's rows" (pinned by
68
+ * shard_test.ts's partition-identity test).
69
+ *
70
+ * Sharded envelope/terminator contract: a sharded call still emits its
71
+ * own envelope line and its own `{"segment-end":true}` terminator —
72
+ * `runSuite` does not know about sibling shards and cannot merge. The
73
+ * documented consumer topology (issue #110's stated shape) is: a
74
+ * caller-side worker pool runs one `runSuite` call per shard, and the
75
+ * PARENT — not this function — discards all but one envelope, merges the
76
+ * per-case rows back into suite order using the `index` argument now
77
+ * passed to `emit`, and writes the single terminator. The returned
78
+ * `RunCounts` are likewise per-shard (they count only this stripe's
79
+ * cases); the parent sums them. `shard` absent
80
+ * (the default) is byte-identical to today: no `index` shard-partitioning
81
+ * occurs and single-argument `emit` callers are unaffected.
82
+ */
83
+ shard?: {
84
+ index: number;
85
+ count: number;
86
+ };
87
+ /** Receives each output line (envelope, one per case, terminator),
88
+ * WITHOUT a trailing newline — callers decide the line separator.
89
+ * `caseIndex` (issue #110) is the case's suite-order index (the same `i`
90
+ * used for stripe membership) for per-case rows; `undefined` for the
91
+ * envelope and terminator lines. A sharded consumer uses it to restore
92
+ * suite order when merging stripes back together. Optional second
93
+ * argument: existing single-argument `emit` callers are unaffected. */
94
+ emit: (line: string, caseIndex?: number) => void;
95
+ /** Optional progress log, one call per case (mirrors harness.mjs's
96
+ * `log?.(...)` callback). */
97
+ log?: (msg: string) => void;
98
+ }
99
+ export interface RunCounts {
100
+ passed: number;
101
+ failed: number;
102
+ skipped: number;
103
+ /** Cases scheduled out as `not-applicable` (tag gating; harness.mjs `na`). */
104
+ na: number;
105
+ total: number;
106
+ }
107
+ /**
108
+ * Run one suite end to end: instantiate, enumerate, execute every case,
109
+ * emit the complete results-JSONL stream (envelope, one line per case,
110
+ * terminator) through `opts.emit`. Throws `MissingImportsError` up front
111
+ * (contracts/embedder-api.md's `requiredImports`) if the caller's imports
112
+ * cannot satisfy the suite, and a plain `Error` if the census is empty (an
113
+ * empty selection is a run error, per component-test-results/src/lib.rs's
114
+ * `fold_jsonl` and harness.mjs's `runSuiteJsonl` — both refuse it).
115
+ */
116
+ export declare function runSuite(artifacts: ComponentArtifacts, opts: RunSuiteOptions): Promise<RunCounts>;
117
+ export { analyzeImports, MissingImportsError, requireImportsResolved } from "./import-analysis.js";
@@ -0,0 +1,35 @@
1
+ /** The custom-section name (component-test-core `name::TAGS_SECTION`). */
2
+ export declare const TAGS_SECTION = "component-test:tags@0.1";
3
+ /** Parsed static inventory: exact case records + generated-row prefixes. */
4
+ export interface TagsInventory {
5
+ exact: Map<string, string[]>;
6
+ prefixes: Array<{
7
+ prefix: string;
8
+ tags: string[];
9
+ }>;
10
+ }
11
+ /**
12
+ * Collect the concatenated bytes of every `component-test:tags@0.1` custom
13
+ * section in `bytes` — the component's own sections plus those of nested
14
+ * core modules (section id 1) and nested components (section id 4), which
15
+ * both embed complete wasm binaries. Returns null when no section exists
16
+ * anywhere (a suite not built with their SDK).
17
+ */
18
+ export declare function collectTagsSections(bytes: Uint8Array): Uint8Array | null;
19
+ /**
20
+ * Parse concatenated records (inventory.rs `parse_tags_records`): one
21
+ * record per line, `name tag...`, blank lines skipped, duplicate names and
22
+ * empty tags rejected. Grammar validation beyond that (WIT-label checks)
23
+ * is the producer's job — their SDK validates at macro-expansion time.
24
+ */
25
+ export declare function parseTagsRecords(bytes: Uint8Array): TagsInventory;
26
+ /** Convenience: scan + parse; null when the suite carries no inventory. */
27
+ export declare function loadTagsInventory(bytes: Uint8Array): TagsInventory | null;
28
+ /** The tags covering `name`: exact record, else a generated-row prefix
29
+ * record (leaves live below `prefix/`), else undefined (inventory drift). */
30
+ export declare function tagsOf(inv: TagsInventory, name: string): string[] | undefined;
31
+ /** harness.mjs `applies()`: `!f` needs f missing; `f` needs f present. */
32
+ export declare function applies(tags: string[], missing: string[]): boolean;
33
+ /** The N/A row's `detail`: the first unsatisfied mark (harness.mjs's
34
+ * `excluding`), empty string if somehow none (mirrors `excluding ?? ""`). */
35
+ export declare function firstExcluding(tags: string[], missing: string[]): string;