@jterrazz/test 12.2.0 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/match.js CHANGED
@@ -1,194 +1,74 @@
1
- //#region src/core/literate/literate-file.ts
2
- /** The closed set of header keys. Anything else is an error naming the line. */
3
- const HEADER_KEYS = [
4
- "test",
5
- "given",
6
- "then",
7
- "fixture",
8
- "env",
9
- "serve"
10
- ];
11
- /** The separator opening a block's stderr section. */
12
- const STDERR_MARKER = "--- stderr";
13
- const HEADER_LINE = /^(?<key>[a-z]+):(?<value>.*)$/;
14
- const EXIT_LINE = /^exit:\s*(?<code>-?\d+)\s*$/;
15
- const KEY_SET = new Set(HEADER_KEYS);
16
- /** A refusal from the grammar, carrying where it happened and which rule it is. */
17
- var LiterateSyntaxError = class extends Error {
18
- defect;
19
- line;
20
- constructor(fileName, line, message, defect) {
21
- super(`${fileName}:${line}: ${message}`);
22
- this.defect = defect;
23
- this.line = line;
24
- this.name = "LiterateSyntaxError";
25
- }
26
- };
27
- function fail(fileName, line, message, defect = "shape") {
28
- throw new LiterateSyntaxError(fileName, line, message, defect);
29
- }
30
- /** Is this line the start of a block? `$ cmd`, or a bare `$` (no arguments). */
31
- function isBlockStart(line) {
32
- return line === "$" || line.startsWith("$ ");
33
- }
34
- function parseEnvTokens(value, fileName, line) {
35
- const tokens = [];
36
- for (const token of value.split(/\s+/).filter((part) => part.length > 0)) {
37
- const separator = token.indexOf("=");
38
- if (separator === -1) {
39
- tokens.push({
40
- kind: "set",
41
- name: token
42
- });
43
- continue;
44
- }
45
- const key = token.slice(0, separator);
46
- if (key.length === 0) fail(fileName, line, `env: "${token}" has no variable name before the "="`);
47
- tokens.push({
48
- key,
49
- kind: "pair",
50
- value: token.slice(separator + 1)
51
- });
52
- }
53
- if (tokens.length === 0) fail(fileName, line, "env: needs at least one KEY=value pair or a registered env-set name");
54
- return tokens;
55
- }
56
- function parseServeEntry(value, fileName, line) {
57
- const parts = value.split(/\s+/).filter((part) => part.length > 0);
58
- const name = parts.shift();
59
- if (name === void 0) fail(fileName, line, "serve: needs the name of a server registered in specification.cli()");
60
- if (name.includes("=")) fail(fileName, line, `serve: "${name}" is a KEY=value pair — the server NAME comes first`);
61
- const env = {};
62
- for (const part of parts) {
63
- const separator = part.indexOf("=");
64
- if (separator <= 0) fail(fileName, line, `serve: "${part}" is not a KEY=value pair`);
65
- env[part.slice(0, separator)] = part.slice(separator + 1);
66
- }
67
- return {
68
- env,
69
- line,
70
- name
71
- };
72
- }
1
+ import { basename, dirname, resolve } from "node:path";
2
+ //#region src/core/specification/shared/fixtures.ts
73
3
  /**
74
- * Parse the header: every line up to the first blank one. `#` lines are
75
- * comments; `test:`, `given:` and `then:` are mandatory and single; `fixture:`,
76
- * `env:` and `serve:` repeat and keep their declaration order.
4
+ * Fixture path resolution + copy semantics for the `cli` facet's `.fixture()`.
5
+ *
6
+ * A fixture path is one of two shapes:
7
+ *
8
+ * - `$FIXTURES/<rest>` — the shared pool at `<specs-root>/fixtures/<rest>`,
9
+ * where `<specs-root>` is the nearest ancestor directory named `specs`.
10
+ * - `<path>` (no marker) — feature-local, at `<test-dir>/fixtures/<path>`.
11
+ *
12
+ * Any other `$`-prefixed marker is a usage error (listed against the known
13
+ * markers). Copy semantics mirror rsync's trailing-slash rule (see
14
+ * {@link copyPlan}).
77
15
  */
78
- function parseHeader(lines, fileName) {
79
- const fixtures = [];
80
- const env = [];
81
- const serve = [];
82
- const singles = /* @__PURE__ */ new Map();
83
- let index = 0;
84
- for (; index < lines.length; index++) {
85
- const raw = lines[index];
86
- if (raw.trim() === "") break;
87
- const number = index + 1;
88
- if (raw.trimStart().startsWith("#")) continue;
89
- if (isBlockStart(raw)) fail(fileName, number, "a \"$\" block starts before the header is closed — separate the header from the blocks with a blank line");
90
- const found = HEADER_LINE.exec(raw);
91
- if (!found?.groups) fail(fileName, number, `"${raw}" is not a header line — expected "<key>: <value>" (keys: ${HEADER_KEYS.join(", ")})`);
92
- const key = found.groups.key;
93
- const value = found.groups.value.trim();
94
- if (!KEY_SET.has(key)) fail(fileName, number, `unknown header key "${key}:" — known keys: ${HEADER_KEYS.join(", ")}`);
95
- if (key === "fixture" || key === "env" || key === "serve") {
96
- if (value.length === 0) fail(fileName, number, `${key}: is empty`);
97
- if (key === "fixture") fixtures.push(value);
98
- else if (key === "env") env.push(...parseEnvTokens(value, fileName, number));
99
- else serve.push(parseServeEntry(value, fileName, number));
100
- continue;
101
- }
102
- if (singles.has(key)) fail(fileName, number, `${key}: is declared twice — it takes exactly one line`);
103
- if (value.length === 0) fail(fileName, number, `${key}: is empty`);
104
- singles.set(key, value);
16
+ /**
17
+ * Markers understood in a `.fixture()` path. Extend here + in
18
+ * {@link resolveFixtureSource}. Exported so the lint layer (rule
19
+ * `jterrazz/b2-known-fixture-marker`) validates literals against the same list.
20
+ */
21
+ const KNOWN_FIXTURE_MARKERS = ["$FIXTURES"];
22
+ /**
23
+ * Walk up from `startDir` to the nearest ancestor directory named `specs`.
24
+ * Throws with guidance if none is found up to the filesystem root — the
25
+ * `$FIXTURES` marker is meaningless without a specs root.
26
+ */
27
+ function discoverSpecsRoot(startDir) {
28
+ let dir = startDir;
29
+ for (;;) {
30
+ if (basename(dir) === "specs") return dir;
31
+ const parent = dirname(dir);
32
+ if (parent === dir) break;
33
+ dir = parent;
105
34
  }
106
- for (const key of [
107
- "test",
108
- "given",
109
- "then"
110
- ]) if (!singles.has(key)) fail(fileName, 1, `missing "${key}:" in the header — test, given and then are mandatory`, "narrative");
111
- const header = {
112
- env,
113
- fixtures,
114
- given: singles.get("given"),
115
- serve,
116
- test: singles.get("test"),
117
- then: singles.get("then")
118
- };
119
- return {
120
- end: index,
121
- header
122
- };
35
+ throw new Error(`.fixture(): the $FIXTURES marker resolves to <specs-root>/fixtures, but no directory named "specs" was found walking up from ${startDir}. Move the specification under a specs/ directory, or use a feature-local fixtures/ path (no $FIXTURES marker).`);
123
36
  }
124
37
  /**
125
- * Split a block's stream section at `--- stderr`, dropping the ONE blank line
126
- * that separates the block from the next `$` (that line belongs to the
127
- * separator, not to the stream).
38
+ * Resolve the absolute source path of a fixture, applying marker resolution.
39
+ * The trailing slash (spread semantics) is preserved by callers via
40
+ * {@link copyPlan} — it is irrelevant to source resolution.
128
41
  */
129
- function parseStreams(section, trailingSeparator) {
130
- const lines = [...section];
131
- if (trailingSeparator && lines.at(-1) === "") lines.pop();
132
- const marker = lines.indexOf(STDERR_MARKER);
133
- if (marker === -1) return {
134
- stderr: null,
135
- stdout: lines.join("\n")
136
- };
137
- return {
138
- stderr: lines.slice(marker + 1).join("\n"),
139
- stdout: lines.slice(0, marker).join("\n")
140
- };
141
- }
142
- /** Parse the blocks: everything after the header's blank line. */
143
- function parseBlocks(lines, start, fileName) {
144
- const starts = [];
145
- for (let index = start; index < lines.length; index++) if (isBlockStart(lines[index])) starts.push(index);
146
- if (starts.length === 0) fail(fileName, start + 1, "no \"$ <command>\" block — a literate spec runs at least one command");
147
- for (let index = start; index < starts[0]; index++) if (lines[index].trim() !== "") fail(fileName, index + 1, `"${lines[index]}" sits outside a block — the first line after the header must be "$ <command>"`);
148
- return starts.map((blockStart, position) => {
149
- const next = starts[position + 1] ?? lines.length;
150
- const argv = lines[blockStart].slice(1).trim();
151
- const exitLine = lines[blockStart + 1];
152
- const exit = exitLine === void 0 ? null : EXIT_LINE.exec(exitLine);
153
- if (!exit?.groups) fail(fileName, blockStart + 2, `the line after "$ ${argv}" must be "exit: <integer>", got ${exitLine === void 0 ? "(end of file)" : `"${exitLine}"`}`);
154
- const streams = parseStreams(lines.slice(blockStart + 2, next), position + 1 < starts.length);
155
- return {
156
- argv,
157
- exitCode: Number(exit.groups.code),
158
- line: blockStart + 1,
159
- stderr: streams.stderr,
160
- stdout: streams.stdout
161
- };
162
- });
42
+ function resolveFixtureSource(path, testDir) {
43
+ const clean = path.replace(/\/+$/, "");
44
+ if (clean === "$FIXTURES" || path.startsWith("$FIXTURES/")) {
45
+ const rest = clean.slice(9).replace(/^\/+/, "");
46
+ return resolve(discoverSpecsRoot(testDir), "fixtures", rest);
47
+ }
48
+ if (path.startsWith("$")) {
49
+ const marker = clean.split("/")[0];
50
+ throw new Error(`.fixture("${path}"): unknown marker "${marker}". Known markers: ${KNOWN_FIXTURE_MARKERS.join(", ")}. A path without a marker is feature-local (resolved under <test-dir>/fixtures/).`);
51
+ }
52
+ return resolve(testDir, "fixtures", clean);
163
53
  }
164
54
  /**
165
- * Parse a `<case>.cli` file. `fileName` is used verbatim in error messages, so
166
- * pass the path the reader would open (`specs/cli/no-estate.cli`).
55
+ * Compute the source + destination for copying a fixture into the working
56
+ * directory. Copy semantics follow rsync's trailing-slash rule:
57
+ *
58
+ * - `dir/` (trailing slash) — the CONTENTS are spread into the cwd.
59
+ * - `dir` (no slash, a directory) — the directory is copied as `<cwd>/<basename>`.
60
+ * - `file` — copied as `<cwd>/<basename>`.
61
+ *
62
+ * Chained `.fixture()` calls layer in order: a later fixture overwrites files
63
+ * written by an earlier one.
167
64
  */
168
- function parseLiterateFile(content, fileName) {
169
- const lines = content.replace(/\r\n/g, "\n").replace(/\n$/, "").split("\n");
170
- const { end, header } = parseHeader(lines, fileName);
65
+ function copyPlan(path, testDir, workDir) {
66
+ const src = resolveFixtureSource(path, testDir);
171
67
  return {
172
- blocks: parseBlocks(lines, end + 1, fileName),
173
- header,
174
- headerText: `${lines.slice(0, end).join("\n")}\n`
68
+ dest: /\/+$/.test(path) ? workDir : resolve(workDir, basename(src)),
69
+ src
175
70
  };
176
71
  }
177
- /** Render one block back to its source form (the update-mode inverse of the parser). */
178
- function serializeLiterateBlock(block) {
179
- const lines = [`$ ${block.argv}`.trimEnd(), `exit: ${block.exitCode}`];
180
- if (block.stdout.length > 0) lines.push(block.stdout);
181
- if (block.stderr !== null && block.stderr.length > 0) lines.push(STDERR_MARKER, block.stderr);
182
- return lines.join("\n");
183
- }
184
- /**
185
- * Rebuild a `.cli` file from its VERBATIM header text and a fresh set of
186
- * blocks. Update mode rewrites only what follows each `$` — the header comes
187
- * back byte-identical, comments and all.
188
- */
189
- function serializeLiterateFile(headerText, blocks) {
190
- return `${headerText}\n${blocks.map((block) => serializeLiterateBlock(block)).join("\n\n")}\n`;
191
- }
192
72
  //#endregion
193
73
  //#region src/core/matching/match.ts
194
74
  /** Every kind usable as a `{{token}}` in fixture files (all but ref/regex/includes). */
@@ -353,4 +233,4 @@ var CaptureScope = class {
353
233
  }
354
234
  };
355
235
  //#endregion
356
- export { LiterateSyntaxError as a, match as i, Matcher as n, parseLiterateFile as o, TOKEN_KINDS as r, serializeLiterateFile as s, CaptureScope as t };
236
+ export { copyPlan as a, match as i, Matcher as n, resolveFixtureSource as o, TOKEN_KINDS as r, CaptureScope as t };
package/dist/oxlint.cjs CHANGED
@@ -306,7 +306,7 @@ const RULE_DOCS = {
306
306
  },
307
307
  "b4-given-then": {
308
308
  channel: "statique",
309
- convention: "Chaque test contient `// Given -` puis `// Then -` (les deux, dans cet ordre) ; Given déclaré après Then est une erreur. Dans un spec littéraire `<cas>.cli`, les deux marqueurs sont les lignes `given:` et `then:` de l’en-tête (passe checker `b4-cli-header`).",
309
+ convention: "Chaque test contient `// Given -` puis `// Then -` (les deux, dans cet ordre) ; Given déclaré après Then est une erreur. Un document `<cas>.spec.yaml` n’a pas de commentaires : sa narration est sa `description:`.",
310
310
  family: "B",
311
311
  id: "B4",
312
312
  rationale: "La narration Given/Then rend l’intention du test lisible sans lire les assertions."
@@ -567,7 +567,7 @@ const RULE_DOCS = {
567
567
  const CHECKER_PASSES = [
568
568
  {
569
569
  channel: "checker",
570
- convention: "Tout `{{token}}` dans une fixture `expected/` — ou dans les blocs d’un spec littéraire `<cas>.cli` — appartient au vocabulaire figé ; un token inconnu est une erreur.",
570
+ convention: "Tout `{{token}}` dans une fixture `expected/` — ou dans un flux attendu d’un document `<cas>.spec.yaml` — appartient au vocabulaire figé ; un token inconnu est une erreur.",
571
571
  family: "D",
572
572
  id: "D4",
573
573
  name: "d4-unknown-token",
@@ -591,19 +591,91 @@ const CHECKER_PASSES = [
591
591
  },
592
592
  {
593
593
  channel: "checker",
594
- convention: "Un spec littéraire `<cas>.cli` respecte sa grammaire : clés d’en-tête fermées (`test`, `given`, `then`, `fixture`, `env`, `serve`), première ligne de corps en `$ <commande>`, `exit: <entier>` juste après.",
594
+ convention: "Un document `<cas>.spec.yaml` respecte sa grammaire : clés fermées au niveau du document (`kind`, `description`, `fixture`, `env`, `serve`, `runs`) comme dans un run (`command`, `stdin`, `timeout`, `waitFor`, `exit`, `stdout`, `stderr`, `files`), `description:` et `runs:` obligatoires, `command:` et `exit:` obligatoires dans chaque run, `exit:` entier littéral, `waitFor:` seulement sur le dernier run et jamais avec `stdin:`, chemin `files:` relatif et sous le workdir.",
595
595
  family: "D",
596
596
  id: "D4b",
597
- name: "d4b-cli-shape",
598
- rationale: "La grammaire est lue par le parseur du runner lui-même : le fichier que le lint accepte est exactement celui que le runner exécute."
597
+ name: "d4b-spec-shape",
598
+ rationale: "La grammaire est lue par le parseur du runner lui-même : le document que le lint accepte est exactement celui que le runner exécute."
599
599
  },
600
600
  {
601
601
  channel: "checker",
602
- convention: "Un spec littéraire `<cas>.cli` porte les trois lignes narratives `test:`, `given:` et `then:` ; il en manque une est une erreur.",
603
- family: "B",
604
- id: "B4",
605
- name: "b4-cli-header",
606
- rationale: "Le format littéraire est un test : la narration Given/Then y vit dans l’en-tête, pas dans des commentaires."
602
+ convention: "Les clés d’un `<cas>.spec.yaml` suivent l’ordre canonique — `kind, description, fixture, env, serve, runs` au niveau du document, `command, stdin, timeout, waitFor, exit, stdout, stderr, files` dans un run. Corrigeable (`--fix`).",
603
+ family: "D",
604
+ id: "D4b",
605
+ name: "d4b-spec-key-order",
606
+ rationale: "Le décor avant la session, l’entrée avant la sortie : un ordre unique rend les documents comparables et supprime les diffs qui ne changent rien."
607
+ },
608
+ {
609
+ channel: "checker",
610
+ convention: "`stdout`, `stderr`, `stdin` et `files.*.equals` d’un `<cas>.spec.yaml` s’écrivent en scalaire bloc (`|` garde le saut de ligne final, `|-` le supprime), jamais en chaîne entre guillemets avec des `\\n`. Corrigeable (`--fix`).",
611
+ family: "D",
612
+ id: "D4b",
613
+ name: "d4b-spec-block-scalar",
614
+ rationale: "Une sortie attendue doit ressembler à une sortie — une golden sur une ligne est illisible et aucun diff ne peut la montrer."
615
+ },
616
+ {
617
+ channel: "checker",
618
+ convention: "Un document se nomme `<cas>.spec.yaml` : `<cas>` en kebab-case, sans les mots `test`/`spec`/`cli` que le suffixe porte déjà, et jamais le nom nu de son dossier.",
619
+ family: "C",
620
+ id: "C12",
621
+ name: "c12-spec-file-name",
622
+ rationale: "Le nom du fichier est la première phrase que le lecteur lit ; `rm/rm.spec.yaml` ne dit rien deux fois."
623
+ },
624
+ {
625
+ channel: "checker",
626
+ convention: "La `description:` d’un document est un titre : une seule ligne, initiale minuscule (identifiant tout en majuscules exempté), sans point final, moins de 100 caractères.",
627
+ family: "J",
628
+ id: "J5",
629
+ name: "j5-spec-description",
630
+ rationale: "La description EST le titre vitest — les règles du titre s’y appliquent, pas celles d’un paragraphe."
631
+ },
632
+ {
633
+ channel: "checker",
634
+ convention: "Deux `<cas>.spec.yaml` d’un même dossier ne partagent pas leur `description:`.",
635
+ family: "J",
636
+ id: "J4",
637
+ name: "j4-spec-description-unique",
638
+ rationale: "Le rapporteur nomme le titre : deux titres identiques laissent deux fichiers à ouvrir."
639
+ },
640
+ {
641
+ channel: "checker",
642
+ convention: "Un flux attendu ne contient pas de valeur volatile littérale : origine loopback avec port (`127.0.0.1:8080`, `localhost:3000`), chemin temporaire absolu (`/tmp/`, `/private/tmp/`, `/var/folders/`) ou chemin home (`/Users/…`, `/home/…`). Le message nomme le token à écrire.",
643
+ family: "D",
644
+ id: "D5",
645
+ name: "d5-spec-volatile-literal",
646
+ rationale: "C’est exactement la forme qu’un `TEST_UPDATE` aurait tokenisée : en trouver une signifie qu’un token a été écrasé à la main."
647
+ },
648
+ {
649
+ channel: "checker",
650
+ convention: "Un horodatage ISO-8601 ou un uuid littéral dans un flux attendu → warning, sauf si la même valeur apparaît dans un `fixture:` ou un `stdin:` du même document (elle est alors fixée, pas volatile).",
651
+ family: "D",
652
+ id: "D5",
653
+ name: "d5w-spec-pinned-value",
654
+ rationale: "Le défaut n’est pas le littéral : c’est le littéral que rien n’a fixé."
655
+ },
656
+ {
657
+ channel: "checker",
658
+ convention: "Un flux attendu dont tout le contenu est `{{any}}` → warning : le run s’exécute et ne prouve rien.",
659
+ family: "J",
660
+ id: "J3",
661
+ name: "j3w-spec-empty-assertion",
662
+ rationale: "Le miroir de J3 pour les documents — une assertion qui accepte tout est une assertion absente."
663
+ },
664
+ {
665
+ channel: "checker",
666
+ convention: "Un run dont `exit:` est non nul sans `stdout:` ni `stderr:` → warning : le document ne dit pas pourquoi la commande a refusé.",
667
+ family: "D",
668
+ id: "D11",
669
+ name: "d11w-spec-silent-refusal",
670
+ rationale: "Un refus muet est soit un défaut du produit, soit une golden qui a oublié que les mots partaient sur stderr."
671
+ },
672
+ {
673
+ channel: "checker",
674
+ convention: "Les mots nus d’`env:` et les noms de `serve:` d’un document sont des clés des objets `env`/`serve` d’un `specification.cli(…)` du plus proche dossier portant des `*.specification.ts` (lecture statique des littéraux ; ignoré sinon).",
675
+ family: "C",
676
+ id: "C8",
677
+ name: "c8-spec-registered-name",
678
+ rationale: "Une faute de frappe (`serve: dashbord`) échoue aujourd’hui à l’exécution, un fichier de test plus tard ; ici c’est une ligne de lint."
607
679
  },
608
680
  {
609
681
  channel: "checker",
@@ -615,7 +687,7 @@ const CHECKER_PASSES = [
615
687
  },
616
688
  {
617
689
  channel: "checker",
618
- convention: "Aucune fixture morte : tout fichier sous `seeds/`/`requests/`/`fixtures/` et toute entrée de premier niveau de `expected/` doit être référencée ; un dossier de feature sans `*.test.ts` ni `*.cli` est orphelin (warning si argument non littéral). Un `<cas>.cli` référence les fixtures nommées par ses lignes `fixture:`.",
690
+ convention: "Aucune fixture morte : tout fichier sous `seeds/`/`requests/`/`fixtures/` et toute entrée de premier niveau de `expected/` doit être référencée ; un dossier de feature sans `*.test.ts` ni `*.spec.yaml` est orphelin (warning si argument non littéral). Un `<cas>.spec.yaml` référence les fixtures nommées par ses entrées `fixture:`.",
619
691
  family: "C",
620
692
  id: "C9",
621
693
  name: "c9-dead-fixtures",
package/dist/oxlint.js CHANGED
@@ -302,7 +302,7 @@ const RULE_DOCS = {
302
302
  },
303
303
  "b4-given-then": {
304
304
  channel: "statique",
305
- convention: "Chaque test contient `// Given -` puis `// Then -` (les deux, dans cet ordre) ; Given déclaré après Then est une erreur. Dans un spec littéraire `<cas>.cli`, les deux marqueurs sont les lignes `given:` et `then:` de l’en-tête (passe checker `b4-cli-header`).",
305
+ convention: "Chaque test contient `// Given -` puis `// Then -` (les deux, dans cet ordre) ; Given déclaré après Then est une erreur. Un document `<cas>.spec.yaml` n’a pas de commentaires : sa narration est sa `description:`.",
306
306
  family: "B",
307
307
  id: "B4",
308
308
  rationale: "La narration Given/Then rend l’intention du test lisible sans lire les assertions."
@@ -563,7 +563,7 @@ const RULE_DOCS = {
563
563
  const CHECKER_PASSES = [
564
564
  {
565
565
  channel: "checker",
566
- convention: "Tout `{{token}}` dans une fixture `expected/` — ou dans les blocs d’un spec littéraire `<cas>.cli` — appartient au vocabulaire figé ; un token inconnu est une erreur.",
566
+ convention: "Tout `{{token}}` dans une fixture `expected/` — ou dans un flux attendu d’un document `<cas>.spec.yaml` — appartient au vocabulaire figé ; un token inconnu est une erreur.",
567
567
  family: "D",
568
568
  id: "D4",
569
569
  name: "d4-unknown-token",
@@ -587,19 +587,91 @@ const CHECKER_PASSES = [
587
587
  },
588
588
  {
589
589
  channel: "checker",
590
- convention: "Un spec littéraire `<cas>.cli` respecte sa grammaire : clés d’en-tête fermées (`test`, `given`, `then`, `fixture`, `env`, `serve`), première ligne de corps en `$ <commande>`, `exit: <entier>` juste après.",
590
+ convention: "Un document `<cas>.spec.yaml` respecte sa grammaire : clés fermées au niveau du document (`kind`, `description`, `fixture`, `env`, `serve`, `runs`) comme dans un run (`command`, `stdin`, `timeout`, `waitFor`, `exit`, `stdout`, `stderr`, `files`), `description:` et `runs:` obligatoires, `command:` et `exit:` obligatoires dans chaque run, `exit:` entier littéral, `waitFor:` seulement sur le dernier run et jamais avec `stdin:`, chemin `files:` relatif et sous le workdir.",
591
591
  family: "D",
592
592
  id: "D4b",
593
- name: "d4b-cli-shape",
594
- rationale: "La grammaire est lue par le parseur du runner lui-même : le fichier que le lint accepte est exactement celui que le runner exécute."
593
+ name: "d4b-spec-shape",
594
+ rationale: "La grammaire est lue par le parseur du runner lui-même : le document que le lint accepte est exactement celui que le runner exécute."
595
595
  },
596
596
  {
597
597
  channel: "checker",
598
- convention: "Un spec littéraire `<cas>.cli` porte les trois lignes narratives `test:`, `given:` et `then:` ; il en manque une est une erreur.",
599
- family: "B",
600
- id: "B4",
601
- name: "b4-cli-header",
602
- rationale: "Le format littéraire est un test : la narration Given/Then y vit dans l’en-tête, pas dans des commentaires."
598
+ convention: "Les clés d’un `<cas>.spec.yaml` suivent l’ordre canonique — `kind, description, fixture, env, serve, runs` au niveau du document, `command, stdin, timeout, waitFor, exit, stdout, stderr, files` dans un run. Corrigeable (`--fix`).",
599
+ family: "D",
600
+ id: "D4b",
601
+ name: "d4b-spec-key-order",
602
+ rationale: "Le décor avant la session, l’entrée avant la sortie : un ordre unique rend les documents comparables et supprime les diffs qui ne changent rien."
603
+ },
604
+ {
605
+ channel: "checker",
606
+ convention: "`stdout`, `stderr`, `stdin` et `files.*.equals` d’un `<cas>.spec.yaml` s’écrivent en scalaire bloc (`|` garde le saut de ligne final, `|-` le supprime), jamais en chaîne entre guillemets avec des `\\n`. Corrigeable (`--fix`).",
607
+ family: "D",
608
+ id: "D4b",
609
+ name: "d4b-spec-block-scalar",
610
+ rationale: "Une sortie attendue doit ressembler à une sortie — une golden sur une ligne est illisible et aucun diff ne peut la montrer."
611
+ },
612
+ {
613
+ channel: "checker",
614
+ convention: "Un document se nomme `<cas>.spec.yaml` : `<cas>` en kebab-case, sans les mots `test`/`spec`/`cli` que le suffixe porte déjà, et jamais le nom nu de son dossier.",
615
+ family: "C",
616
+ id: "C12",
617
+ name: "c12-spec-file-name",
618
+ rationale: "Le nom du fichier est la première phrase que le lecteur lit ; `rm/rm.spec.yaml` ne dit rien deux fois."
619
+ },
620
+ {
621
+ channel: "checker",
622
+ convention: "La `description:` d’un document est un titre : une seule ligne, initiale minuscule (identifiant tout en majuscules exempté), sans point final, moins de 100 caractères.",
623
+ family: "J",
624
+ id: "J5",
625
+ name: "j5-spec-description",
626
+ rationale: "La description EST le titre vitest — les règles du titre s’y appliquent, pas celles d’un paragraphe."
627
+ },
628
+ {
629
+ channel: "checker",
630
+ convention: "Deux `<cas>.spec.yaml` d’un même dossier ne partagent pas leur `description:`.",
631
+ family: "J",
632
+ id: "J4",
633
+ name: "j4-spec-description-unique",
634
+ rationale: "Le rapporteur nomme le titre : deux titres identiques laissent deux fichiers à ouvrir."
635
+ },
636
+ {
637
+ channel: "checker",
638
+ convention: "Un flux attendu ne contient pas de valeur volatile littérale : origine loopback avec port (`127.0.0.1:8080`, `localhost:3000`), chemin temporaire absolu (`/tmp/`, `/private/tmp/`, `/var/folders/`) ou chemin home (`/Users/…`, `/home/…`). Le message nomme le token à écrire.",
639
+ family: "D",
640
+ id: "D5",
641
+ name: "d5-spec-volatile-literal",
642
+ rationale: "C’est exactement la forme qu’un `TEST_UPDATE` aurait tokenisée : en trouver une signifie qu’un token a été écrasé à la main."
643
+ },
644
+ {
645
+ channel: "checker",
646
+ convention: "Un horodatage ISO-8601 ou un uuid littéral dans un flux attendu → warning, sauf si la même valeur apparaît dans un `fixture:` ou un `stdin:` du même document (elle est alors fixée, pas volatile).",
647
+ family: "D",
648
+ id: "D5",
649
+ name: "d5w-spec-pinned-value",
650
+ rationale: "Le défaut n’est pas le littéral : c’est le littéral que rien n’a fixé."
651
+ },
652
+ {
653
+ channel: "checker",
654
+ convention: "Un flux attendu dont tout le contenu est `{{any}}` → warning : le run s’exécute et ne prouve rien.",
655
+ family: "J",
656
+ id: "J3",
657
+ name: "j3w-spec-empty-assertion",
658
+ rationale: "Le miroir de J3 pour les documents — une assertion qui accepte tout est une assertion absente."
659
+ },
660
+ {
661
+ channel: "checker",
662
+ convention: "Un run dont `exit:` est non nul sans `stdout:` ni `stderr:` → warning : le document ne dit pas pourquoi la commande a refusé.",
663
+ family: "D",
664
+ id: "D11",
665
+ name: "d11w-spec-silent-refusal",
666
+ rationale: "Un refus muet est soit un défaut du produit, soit une golden qui a oublié que les mots partaient sur stderr."
667
+ },
668
+ {
669
+ channel: "checker",
670
+ convention: "Les mots nus d’`env:` et les noms de `serve:` d’un document sont des clés des objets `env`/`serve` d’un `specification.cli(…)` du plus proche dossier portant des `*.specification.ts` (lecture statique des littéraux ; ignoré sinon).",
671
+ family: "C",
672
+ id: "C8",
673
+ name: "c8-spec-registered-name",
674
+ rationale: "Une faute de frappe (`serve: dashbord`) échoue aujourd’hui à l’exécution, un fichier de test plus tard ; ici c’est une ligne de lint."
603
675
  },
604
676
  {
605
677
  channel: "checker",
@@ -611,7 +683,7 @@ const CHECKER_PASSES = [
611
683
  },
612
684
  {
613
685
  channel: "checker",
614
- convention: "Aucune fixture morte : tout fichier sous `seeds/`/`requests/`/`fixtures/` et toute entrée de premier niveau de `expected/` doit être référencée ; un dossier de feature sans `*.test.ts` ni `*.cli` est orphelin (warning si argument non littéral). Un `<cas>.cli` référence les fixtures nommées par ses lignes `fixture:`.",
686
+ convention: "Aucune fixture morte : tout fichier sous `seeds/`/`requests/`/`fixtures/` et toute entrée de premier niveau de `expected/` doit être référencée ; un dossier de feature sans `*.test.ts` ni `*.spec.yaml` est orphelin (warning si argument non littéral). Un `<cas>.spec.yaml` référence les fixtures nommées par ses entrées `fixture:`.",
615
687
  family: "C",
616
688
  id: "C9",
617
689
  name: "c9-dead-fixtures",
package/dist/queue.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as CaptureScope } from "./match.js";
2
- import { F as textEquals, O as hasPlaceholders, j as placeholderPatternSource } from "./literate.js";
2
+ import { A as placeholderPatternSource, D as hasPlaceholders, P as textEquals } from "./literate.js";
3
3
  //#region src/core/contracts/contract.ts
4
4
  /**
5
5
  * Declare one contract. Identity function — its value is the enforced shape