@sdxc/spec 0.0.0-pre.1

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 (77) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +924 -0
  3. package/dist/ast.d.ts +193 -0
  4. package/dist/ast.js +9 -0
  5. package/dist/builtins.d.ts +29 -0
  6. package/dist/builtins.js +66 -0
  7. package/dist/cli.d.ts +21 -0
  8. package/dist/cli.js +297 -0
  9. package/dist/diagnostics.d.ts +47 -0
  10. package/dist/diagnostics.js +8 -0
  11. package/dist/errors.d.ts +131 -0
  12. package/dist/errors.js +159 -0
  13. package/dist/executor.d.ts +66 -0
  14. package/dist/executor.js +320 -0
  15. package/dist/expectation.d.ts +61 -0
  16. package/dist/expectation.js +222 -0
  17. package/dist/index.d.ts +51 -0
  18. package/dist/index.js +36 -0
  19. package/dist/lexer.d.ts +22 -0
  20. package/dist/lexer.js +284 -0
  21. package/dist/loader.d.ts +21 -0
  22. package/dist/loader.js +81 -0
  23. package/dist/parser.d.ts +24 -0
  24. package/dist/parser.js +502 -0
  25. package/dist/permissions.d.ts +139 -0
  26. package/dist/permissions.js +325 -0
  27. package/dist/plugin.d.ts +90 -0
  28. package/dist/plugin.js +9 -0
  29. package/dist/plugins/browser.d.ts +24 -0
  30. package/dist/plugins/browser.js +896 -0
  31. package/dist/plugins/cli.d.ts +17 -0
  32. package/dist/plugins/cli.js +134 -0
  33. package/dist/plugins/db-e2e-probe.d.ts +14 -0
  34. package/dist/plugins/db-e2e-probe.js +112 -0
  35. package/dist/plugins/db.d.ts +19 -0
  36. package/dist/plugins/db.js +199 -0
  37. package/dist/plugins/demo.d.ts +17 -0
  38. package/dist/plugins/demo.js +70 -0
  39. package/dist/plugins/env.d.ts +18 -0
  40. package/dist/plugins/env.js +87 -0
  41. package/dist/plugins/fs.d.ts +16 -0
  42. package/dist/plugins/fs.js +415 -0
  43. package/dist/plugins/http.d.ts +19 -0
  44. package/dist/plugins/http.js +505 -0
  45. package/dist/plugins/jwt.d.ts +17 -0
  46. package/dist/plugins/jwt.js +342 -0
  47. package/dist/plugins/sample.d.ts +27 -0
  48. package/dist/plugins/sample.js +400 -0
  49. package/dist/plugins/url.d.ts +18 -0
  50. package/dist/plugins/url.js +126 -0
  51. package/dist/project-config.d.ts +163 -0
  52. package/dist/project-config.js +497 -0
  53. package/dist/registry.d.ts +56 -0
  54. package/dist/registry.js +110 -0
  55. package/dist/reporter.d.ts +30 -0
  56. package/dist/reporter.js +237 -0
  57. package/dist/run.d.ts +74 -0
  58. package/dist/run.js +179 -0
  59. package/dist/runner.d.ts +52 -0
  60. package/dist/runner.js +38 -0
  61. package/dist/source.d.ts +37 -0
  62. package/dist/source.js +31 -0
  63. package/dist/sources.d.ts +45 -0
  64. package/dist/sources.js +54 -0
  65. package/dist/tokens.d.ts +34 -0
  66. package/dist/tokens.js +25 -0
  67. package/dist/transport-stdio.d.ts +34 -0
  68. package/dist/transport-stdio.js +400 -0
  69. package/dist/values.d.ts +48 -0
  70. package/dist/values.js +52 -0
  71. package/dist/workers.d.ts +40 -0
  72. package/dist/workers.js +26 -0
  73. package/dist/workspace-none.d.ts +23 -0
  74. package/dist/workspace-none.js +33 -0
  75. package/dist/workspace.d.ts +47 -0
  76. package/dist/workspace.js +116 -0
  77. package/package.json +28 -0
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The host convenience over `runTests`: load a `spec/` directory from disk, give
3
+ * every test a fresh temp-directory workspace, and register the built-in
4
+ * capabilities. This is what the CLI runs, and the answers it supplies — a
5
+ * filesystem to read the suite from, a filesystem to run tests in, and all eight
6
+ * built-ins — are exactly the ones a runtime without a process cannot give. An
7
+ * embedder there calls `runTests` with its own answers instead.
8
+ *
9
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
10
+ * @copyright Sergio Xalambrí 2026
11
+ */
12
+ import type { Result } from "@sdxc/result";
13
+ import type { Seed } from "@sdxc/sample";
14
+ import type { BuiltinNamespace } from "./builtins.js";
15
+ import type { SuiteResult } from "./diagnostics.js";
16
+ import type { SpecError } from "./errors.js";
17
+ import type { Grants } from "./permissions.js";
18
+ import type { Plugin } from "./plugin.js";
19
+ /** What a `spec run` needs: the suite directory and the caller's grants. */
20
+ export interface RunOptions {
21
+ /** Directory scanned recursively for `.spec` files. */
22
+ root: string;
23
+ /** The caller's permission grants, parsed from `--allow-*` flags. */
24
+ grants: Grants;
25
+ /** Extra plugins beyond the built-ins this run registers. */
26
+ plugins?: Plugin[];
27
+ /**
28
+ * Which built-in namespaces to register; omit for all of them, as the CLI
29
+ * does. Excluding a namespace here makes it unresolvable, so a spec
30
+ * naming it fails as an unknown name (see `createBuiltinPlugins`).
31
+ */
32
+ builtins?: readonly BuiltinNamespace[];
33
+ /**
34
+ * How many tests may execute at once. `1` (the default) runs the suite
35
+ * strictly sequentially in source order; see `RunTestsOptions.concurrency`.
36
+ */
37
+ concurrency?: number;
38
+ /**
39
+ * The run's seed, which every test's generated data descends from; omit for
40
+ * the fixed default that makes two runs produce identical data.
41
+ */
42
+ seed?: Seed;
43
+ }
44
+ /**
45
+ * Load and execute a suite from disk. Load failures (unreadable directory,
46
+ * parse errors, duplicate definitions) fail the whole run before any test
47
+ * starts, while test failures surface as outcomes inside the returned result.
48
+ *
49
+ * @param options - Suite directory, grants, and optional plugin and concurrency choices.
50
+ * @returns Per-test outcomes, or the error that prevented the run entirely.
51
+ */
52
+ export declare function runSuite(options: RunOptions): Promise<Result<SuiteResult, SpecError>>;
package/dist/runner.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The host convenience over `runTests`: load a `spec/` directory from disk, give
3
+ * every test a fresh temp-directory workspace, and register the built-in
4
+ * capabilities. This is what the CLI runs, and the answers it supplies — a
5
+ * filesystem to read the suite from, a filesystem to run tests in, and all eight
6
+ * built-ins — are exactly the ones a runtime without a process cannot give. An
7
+ * embedder there calls `runTests` with its own answers instead.
8
+ *
9
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
10
+ * @copyright Sergio Xalambrí 2026
11
+ */
12
+ import { isFailure } from "@sdxc/result";
13
+ import { createBuiltinPlugins } from "./builtins.js";
14
+ import { loadSuite } from "./loader.js";
15
+ import { runTests } from "./run.js";
16
+ import { createWorkspace } from "./workspace.js";
17
+ /**
18
+ * Load and execute a suite from disk. Load failures (unreadable directory,
19
+ * parse errors, duplicate definitions) fail the whole run before any test
20
+ * starts, while test failures surface as outcomes inside the returned result.
21
+ *
22
+ * @param options - Suite directory, grants, and optional plugin and concurrency choices.
23
+ * @returns Per-test outcomes, or the error that prevented the run entirely.
24
+ */
25
+ export async function runSuite(options) {
26
+ let loaded = await loadSuite(options.root);
27
+ if (isFailure(loaded))
28
+ return loaded;
29
+ return runTests({
30
+ suite: loaded.data,
31
+ plugins: [...createBuiltinPlugins(options.builtins), ...(options.plugins ?? [])],
32
+ grants: options.grants,
33
+ createWorkspace,
34
+ concurrency: options.concurrency,
35
+ seed: options.seed,
36
+ root: options.root,
37
+ });
38
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Source-text bookkeeping for `.spec` files: file wrappers, spans, and
3
+ * positions. The lexer, parser, and diagnostics all share these shapes so
4
+ * every failure can point at the exact statement that produced it.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /** A caret location inside a source file, 1-indexed the way editors display. */
10
+ export interface Position {
11
+ /** 1-indexed line number. */
12
+ line: number;
13
+ /** 1-indexed column, counted in UTF-16 code units. */
14
+ column: number;
15
+ }
16
+ /** A half-open range of source text (`start` inclusive, `end` exclusive). */
17
+ export interface Span {
18
+ /** Offset of the first character, 0-indexed into the file text. */
19
+ start: number;
20
+ /** Offset one past the last character. */
21
+ end: number;
22
+ }
23
+ /** A `.spec` file's full text plus the path diagnostics report it under. */
24
+ export interface SourceFile {
25
+ /** Path as handed to the loader; relative paths stay relative. */
26
+ path: string;
27
+ text: string;
28
+ }
29
+ /**
30
+ * Translate a text offset into a line/column position, for rendering
31
+ * diagnostics. Offsets past the end of the file clamp to the last position.
32
+ *
33
+ * @param source - The file the offset points into.
34
+ * @param offset - 0-indexed character offset, e.g. a `Span`'s `start`.
35
+ * @returns The 1-indexed line and column of that offset.
36
+ */
37
+ export declare function positionAt(source: SourceFile, offset: number): Position;
package/dist/source.js ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Source-text bookkeeping for `.spec` files: file wrappers, spans, and
3
+ * positions. The lexer, parser, and diagnostics all share these shapes so
4
+ * every failure can point at the exact statement that produced it.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /**
10
+ * Translate a text offset into a line/column position, for rendering
11
+ * diagnostics. Offsets past the end of the file clamp to the last position.
12
+ *
13
+ * @param source - The file the offset points into.
14
+ * @param offset - 0-indexed character offset, e.g. a `Span`'s `start`.
15
+ * @returns The 1-indexed line and column of that offset.
16
+ */
17
+ export function positionAt(source, offset) {
18
+ let line = 1;
19
+ let column = 1;
20
+ let end = Math.min(offset, source.text.length);
21
+ for (let index = 0; index < end; index++) {
22
+ if (source.text[index] === "\n") {
23
+ line += 1;
24
+ column = 1;
25
+ }
26
+ else {
27
+ column += 1;
28
+ }
29
+ }
30
+ return { line, column };
31
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Suite loading from sources already in memory: parse each one, then register
3
+ * suite-global definitions in a second pass — so name resolution never depends
4
+ * on source order and duplicates surface before any test runs.
5
+ *
6
+ * This is the whole of loading that does not involve a filesystem, which is why
7
+ * it lives apart from `loader.ts`: a host that already holds its spec text
8
+ * (from a database row, an HTTP body, a bundled string) needs this and not the
9
+ * directory walk, and a runtime with no filesystem can reach nothing else.
10
+ *
11
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
12
+ * @copyright Sergio Xalambrí 2026
13
+ */
14
+ import type { Result } from "@sdxc/result";
15
+ import type { CommandNode, FixtureNode, SpecFileNode } from "./ast.js";
16
+ import type { SpecError } from "./errors.js";
17
+ /** One `.spec` file's text, plus the path errors from it are attributed to. */
18
+ export interface SpecSource {
19
+ /**
20
+ * How this source is named in diagnostics. Never opened, so it need not exist
21
+ * on any disk — but it is what a failure reports and what `positionAt` is
22
+ * given, so prefer something a reader can locate.
23
+ */
24
+ path: string;
25
+ /** The source text, verbatim. */
26
+ text: string;
27
+ }
28
+ /** A fully loaded suite: every parsed file plus its global definition maps. */
29
+ export interface LoadedSuite {
30
+ /** Every parsed `.spec` file, in the order its sources were given. */
31
+ files: SpecFileNode[];
32
+ /** Suite-global commands by name, registered across every file. */
33
+ commands: Map<string, CommandNode>;
34
+ /** Suite-global fixtures by name, registered across every file. */
35
+ fixtures: Map<string, FixtureNode>;
36
+ }
37
+ /**
38
+ * Load a suite from sources: parse each one, then register every definition
39
+ * suite-globally, failing on the first parse error or on any duplicate name
40
+ * across files. Sources are parsed and reported in the caller's order.
41
+ *
42
+ * @param sources - Every `.spec` source in the suite, in the intended order.
43
+ * @returns The loaded suite, or the load/parse error that prevented it.
44
+ */
45
+ export declare function loadSources(sources: readonly SpecSource[]): Result<LoadedSuite, SpecError>;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Suite loading from sources already in memory: parse each one, then register
3
+ * suite-global definitions in a second pass — so name resolution never depends
4
+ * on source order and duplicates surface before any test runs.
5
+ *
6
+ * This is the whole of loading that does not involve a filesystem, which is why
7
+ * it lives apart from `loader.ts`: a host that already holds its spec text
8
+ * (from a database row, an HTTP body, a bundled string) needs this and not the
9
+ * directory walk, and a runtime with no filesystem can reach nothing else.
10
+ *
11
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
12
+ * @copyright Sergio Xalambrí 2026
13
+ */
14
+ import { failure, isFailure, success } from "@sdxc/result";
15
+ import { LoadError, ParseError } from "./errors.js";
16
+ import { parse } from "./parser.js";
17
+ /**
18
+ * Load a suite from sources: parse each one, then register every definition
19
+ * suite-globally, failing on the first parse error or on any duplicate name
20
+ * across files. Sources are parsed and reported in the caller's order.
21
+ *
22
+ * @param sources - Every `.spec` source in the suite, in the intended order.
23
+ * @returns The loaded suite, or the load/parse error that prevented it.
24
+ */
25
+ export function loadSources(sources) {
26
+ if (sources.length === 0) {
27
+ return failure(new LoadError("load-error", "No .spec sources were given to load."));
28
+ }
29
+ let files = [];
30
+ for (let source of sources) {
31
+ let parsed = parse(source);
32
+ if (isFailure(parsed)) {
33
+ return failure(new ParseError(`${source.path}: ${parsed.error.message}`, parsed.error.file ?? source.path, parsed.error.span));
34
+ }
35
+ files.push(parsed.data);
36
+ }
37
+ let commands = new Map();
38
+ let fixtures = new Map();
39
+ let origins = new Map();
40
+ for (let file of files) {
41
+ for (let definition of file.definitions) {
42
+ let previous = origins.get(definition.name);
43
+ if (previous) {
44
+ return failure(new LoadError("duplicate-definition", `Duplicate definition "${definition.name}": ${previous.kind} in ${previous.file} and ${definition.kind} in ${file.path}.`));
45
+ }
46
+ origins.set(definition.name, { kind: definition.kind, file: file.path });
47
+ if (definition.kind === "command")
48
+ commands.set(definition.name, definition);
49
+ else
50
+ fixtures.set(definition.name, definition);
51
+ }
52
+ }
53
+ return success({ files, commands, fixtures });
54
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Token vocabulary shared by the lexer and parser. The set is intentionally
3
+ * tiny: the language has no operators, so tokens are literals, identifiers,
4
+ * keywords, a handful of punctuation marks, and significant newlines.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import type { Span } from "./source.js";
10
+ /** Reserved words; never valid as identifiers, tool names, or definitions. */
11
+ export declare const KEYWORDS: readonly ["use", "test", "given", "when", "then", "command", "fixture", "let", "return", "expect", "eventually", "within", "true", "false"];
12
+ /** One of the reserved words in {@link KEYWORDS}. */
13
+ export type Keyword = (typeof KEYWORDS)[number];
14
+ /**
15
+ * Every kind of token the lexer emits. `identifier` covers dotted paths too:
16
+ * identifiers joined by `.` with no surrounding whitespace lex as a single
17
+ * token whose text contains the dots (`http.post`, `user.email`).
18
+ */
19
+ export type TokenKind = "string" | "multiline-string" | "number" | "duration" | "identifier" | "keyword" | "lbrace" | "rbrace" | "lparen" | "rparen" | "comma" | "colon" | "equals" | "newline" | "eof";
20
+ /** A lexed token: kind, raw text, location, and decoded value when relevant. */
21
+ export interface Token {
22
+ kind: TokenKind;
23
+ /** The raw source text the token covers. */
24
+ text: string;
25
+ /** Where in the file the token sits. */
26
+ span: Span;
27
+ /**
28
+ * Decoded payload: the unescaped/dedented content for strings, the numeric
29
+ * value for numbers, milliseconds for durations. Absent otherwise.
30
+ */
31
+ value?: string | number;
32
+ /** The specific reserved word, present when `kind` is `"keyword"`. */
33
+ keyword?: Keyword;
34
+ }
package/dist/tokens.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Token vocabulary shared by the lexer and parser. The set is intentionally
3
+ * tiny: the language has no operators, so tokens are literals, identifiers,
4
+ * keywords, a handful of punctuation marks, and significant newlines.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /** Reserved words; never valid as identifiers, tool names, or definitions. */
10
+ export const KEYWORDS = [
11
+ "use",
12
+ "test",
13
+ "given",
14
+ "when",
15
+ "then",
16
+ "command",
17
+ "fixture",
18
+ "let",
19
+ "return",
20
+ "expect",
21
+ "eventually",
22
+ "within",
23
+ "true",
24
+ "false",
25
+ ];
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The NDJSON-over-stdio plugin transport: how an external executable becomes
3
+ * a `Plugin`, one JSON document per line over the child's stdio, with
4
+ * strictly increasing request ids and in-order replies. The child inherits
5
+ * no environment beyond PATH.
6
+ *
7
+ * `workspaceRoot` crosses the wire so a plugin can resolve its own paths, but
8
+ * scoped permission enforcement over the wire is still an open design
9
+ * question — the host's coarse `requires` gate runs before every call.
10
+ *
11
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
12
+ * @copyright Sergio Xalambrí 2026
13
+ */
14
+ import type { Result } from "@sdxc/result";
15
+ import type { Plugin } from "./plugin.js";
16
+ import { SpecError } from "./errors.js";
17
+ /**
18
+ * Spawn an external plugin process and connect it as a `Plugin`: send the
19
+ * describe handshake (5s timeout), cache the returned descriptors, and hand
20
+ * back a `call()` that round-trips over the child's stdio.
21
+ *
22
+ * @param command - The argv to spawn, e.g. `["bun", "plugins/demo.ts"]`.
23
+ * @param namespace - The namespace the connected plugin's tools live under.
24
+ * @returns The connected plugin, or the failure that prevented the handshake.
25
+ */
26
+ export declare function connectStdioPlugin(command: string[], namespace: string): Promise<Result<Plugin, SpecError>>;
27
+ /**
28
+ * The plugin-side serve loop: read requests from stdin, dispatch each to the
29
+ * given plugin, and write matching replies to stdout, in order. A line that
30
+ * fails to parse carries no id, so parsing simply continues to the next one.
31
+ *
32
+ * @param plugin - The local plugin implementation to expose over the wire.
33
+ */
34
+ export declare function servePlugin(plugin: Plugin): Promise<undefined>;