@ryanfw/prompt-orchestration-pipeline 1.3.2 → 1.3.4

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 (206) hide show
  1. package/docs/http-api.md +80 -4
  2. package/package.json +1 -4
  3. package/src/api/__tests__/index.test.ts +443 -32
  4. package/src/api/index.ts +108 -127
  5. package/src/cli/__tests__/analyze-task.test.ts +40 -7
  6. package/src/cli/__tests__/index.test.ts +337 -17
  7. package/src/cli/__tests__/types.test.ts +0 -18
  8. package/src/cli/__tests__/update-pipeline-json.test.ts +19 -9
  9. package/src/cli/analyze-task.ts +3 -14
  10. package/src/cli/index.ts +73 -61
  11. package/src/cli/types.ts +0 -13
  12. package/src/cli/update-pipeline-json.ts +3 -14
  13. package/src/config/__tests__/paths.test.ts +46 -1
  14. package/src/config/__tests__/pipeline-registry.test.ts +767 -0
  15. package/src/config/__tests__/sse-events.test.ts +470 -0
  16. package/src/config/__tests__/statuses.test.ts +41 -0
  17. package/src/config/paths.ts +11 -2
  18. package/src/config/pipeline-registry.ts +258 -0
  19. package/src/config/sse-events.ts +122 -0
  20. package/src/config/statuses.ts +20 -1
  21. package/src/core/__tests__/agent-step.test.ts +115 -0
  22. package/src/core/__tests__/config.test.ts +545 -105
  23. package/src/core/__tests__/core-boot-resolution.test.ts +181 -0
  24. package/src/core/__tests__/job-concurrency.test.ts +260 -0
  25. package/src/core/__tests__/job-submission.test.ts +396 -0
  26. package/src/core/__tests__/job-view.test.ts +1341 -0
  27. package/src/core/__tests__/json-file.test.ts +111 -0
  28. package/src/core/__tests__/lifecycle-policy.test.ts +81 -7
  29. package/src/core/__tests__/logger.test.ts +22 -0
  30. package/src/core/__tests__/orchestrator-no-deprecated-exports.test.ts +41 -0
  31. package/src/core/__tests__/orchestrator.test.ts +615 -2
  32. package/src/core/__tests__/pipeline-runner.test.ts +946 -9
  33. package/src/core/__tests__/redact.test.ts +153 -0
  34. package/src/core/__tests__/runner-liveness.test.ts +910 -0
  35. package/src/core/__tests__/seed-naming.test.ts +57 -0
  36. package/src/core/__tests__/single-derivation.test.ts +159 -0
  37. package/src/core/__tests__/task-runner.test.ts +594 -3
  38. package/src/core/__tests__/task-telemetry.test.ts +241 -0
  39. package/src/core/__tests__/workspace-root-resolution-guard.test.ts +62 -0
  40. package/src/core/agent-step.ts +4 -1
  41. package/src/core/agent-types.ts +5 -4
  42. package/src/core/config.ts +134 -222
  43. package/src/core/control.ts +3 -0
  44. package/src/core/job-concurrency.ts +56 -20
  45. package/src/core/job-submission.ts +133 -0
  46. package/src/core/job-view.ts +473 -0
  47. package/src/core/json-file.ts +45 -0
  48. package/src/core/lifecycle-policy.ts +23 -8
  49. package/src/core/logger.ts +0 -29
  50. package/src/core/orchestrator.ts +200 -74
  51. package/src/core/pipeline-runner.ts +85 -53
  52. package/src/core/redact.ts +40 -0
  53. package/src/core/runner-liveness.ts +280 -0
  54. package/src/core/seed-naming.ts +9 -0
  55. package/src/core/status-writer.ts +27 -33
  56. package/src/core/task-runner.ts +356 -319
  57. package/src/core/task-telemetry.ts +107 -0
  58. package/src/harness/__tests__/subprocess.test.ts +112 -1
  59. package/src/harness/subprocess.ts +55 -16
  60. package/src/llm/__tests__/index.test.ts +684 -33
  61. package/src/llm/index.ts +310 -67
  62. package/src/providers/__tests__/alibaba.test.ts +67 -6
  63. package/src/providers/__tests__/anthropic.test.ts +35 -14
  64. package/src/providers/__tests__/base.test.ts +62 -0
  65. package/src/providers/__tests__/claude-code.test.ts +99 -14
  66. package/src/providers/__tests__/deepseek.test.ts +16 -6
  67. package/src/providers/__tests__/gemini.test.ts +47 -25
  68. package/src/providers/__tests__/moonshot.test.ts +27 -0
  69. package/src/providers/__tests__/openai.test.ts +262 -74
  70. package/src/providers/__tests__/opencode.test.ts +77 -0
  71. package/src/providers/__tests__/request-timeout-contract.test.ts +226 -0
  72. package/src/providers/__tests__/stream-accumulator.test.ts +52 -0
  73. package/src/providers/__tests__/types.test.ts +85 -0
  74. package/src/providers/__tests__/zero-fill-guard.test.ts +62 -0
  75. package/src/providers/__tests__/zhipu.test.ts +27 -14
  76. package/src/providers/alibaba.ts +20 -39
  77. package/src/providers/anthropic.ts +23 -11
  78. package/src/providers/base.ts +19 -0
  79. package/src/providers/claude-code.ts +27 -18
  80. package/src/providers/deepseek.ts +9 -28
  81. package/src/providers/gemini.ts +20 -58
  82. package/src/providers/moonshot.ts +15 -11
  83. package/src/providers/openai.ts +79 -61
  84. package/src/providers/opencode.ts +16 -33
  85. package/src/providers/stream-accumulator.ts +27 -21
  86. package/src/providers/types.ts +29 -4
  87. package/src/providers/zhipu.ts +15 -44
  88. package/src/task-analysis/__tests__/analyzer.test.ts +0 -0
  89. package/src/task-analysis/__tests__/enrichers-schema-deducer.test.ts +34 -2
  90. package/src/task-analysis/__tests__/no-ast-imports.test.ts +52 -0
  91. package/src/task-analysis/__tests__/repository.test.ts +65 -0
  92. package/src/task-analysis/__tests__/types.test.ts +63 -130
  93. package/src/task-analysis/analyzer.ts +91 -0
  94. package/src/task-analysis/enrichers/schema-deducer.ts +13 -2
  95. package/src/task-analysis/index.ts +2 -36
  96. package/src/task-analysis/repository.ts +45 -0
  97. package/src/task-analysis/types.ts +42 -58
  98. package/src/ui/client/__tests__/api.test.ts +143 -1
  99. package/src/ui/client/__tests__/bootstrap.test.ts +178 -62
  100. package/src/ui/client/__tests__/job-adapter.test.ts +203 -2
  101. package/src/ui/client/__tests__/load-state.test.ts +70 -0
  102. package/src/ui/client/__tests__/types.test.ts +66 -3
  103. package/src/ui/client/__tests__/useJobDetailWithUpdates.test.ts +390 -77
  104. package/src/ui/client/__tests__/useJobList.test.ts +198 -23
  105. package/src/ui/client/__tests__/useJobListWithUpdates.test.ts +218 -7
  106. package/src/ui/client/adapters/__tests__/job-adapter.test.ts +186 -0
  107. package/src/ui/client/adapters/job-adapter.ts +41 -16
  108. package/src/ui/client/api.ts +38 -15
  109. package/src/ui/client/bootstrap.ts +19 -14
  110. package/src/ui/client/hooks/useJobDetailWithUpdates.ts +73 -97
  111. package/src/ui/client/hooks/useJobList.ts +26 -31
  112. package/src/ui/client/hooks/useJobListWithUpdates.ts +42 -76
  113. package/src/ui/client/load-state.ts +20 -0
  114. package/src/ui/client/reducers/__tests__/job-events.test.ts +568 -0
  115. package/src/ui/client/reducers/job-events.ts +137 -0
  116. package/src/ui/client/types.ts +16 -20
  117. package/src/ui/components/DAGGrid.tsx +6 -1
  118. package/src/ui/components/JobDetail.tsx +12 -4
  119. package/src/ui/components/JobTable.tsx +41 -13
  120. package/src/ui/components/StageTimeline.tsx +8 -20
  121. package/src/ui/components/TaskAnalysisDisplay.tsx +48 -6
  122. package/src/ui/components/__tests__/DAGGrid.test.tsx +36 -0
  123. package/src/ui/components/__tests__/JobDetail.test.tsx +112 -0
  124. package/src/ui/components/__tests__/JobTable.test.tsx +137 -1
  125. package/src/ui/components/__tests__/StageTimeline.test.tsx +5 -6
  126. package/src/ui/components/__tests__/TaskAnalysisDisplay.test.tsx +64 -0
  127. package/src/ui/components/types.ts +35 -15
  128. package/src/ui/dist/assets/{index--RH3sAt3.js → index-DN3-zvtP.js} +4324 -263
  129. package/src/ui/dist/assets/index-DN3-zvtP.js.map +1 -0
  130. package/src/ui/dist/assets/style-CtZBnjlR.css +2 -0
  131. package/src/ui/dist/index.html +2 -2
  132. package/src/ui/pages/PipelineDetail.tsx +60 -4
  133. package/src/ui/pages/PromptPipelineDashboard.tsx +59 -7
  134. package/src/ui/pages/__tests__/PromptPipelineDashboard.test.tsx +114 -32
  135. package/src/ui/pages/__tests__/pages.test.tsx +236 -42
  136. package/src/ui/server/__tests__/concurrency-endpoint.test.ts +54 -0
  137. package/src/ui/server/__tests__/config-bridge-node.test.ts +34 -1
  138. package/src/ui/server/__tests__/config-bridge.test.ts +9 -1
  139. package/src/ui/server/__tests__/embedded-assets.test.ts +66 -0
  140. package/src/ui/server/__tests__/file-endpoints.test.ts +183 -5
  141. package/src/ui/server/__tests__/gate-endpoints.test.ts +55 -0
  142. package/src/ui/server/__tests__/index.test.ts +63 -0
  143. package/src/ui/server/__tests__/job-control-endpoints.test.ts +512 -2
  144. package/src/ui/server/__tests__/job-endpoints.test.ts +158 -2
  145. package/src/ui/server/__tests__/read-static-path-guard.test.ts +94 -0
  146. package/src/ui/server/__tests__/router-root-isolation.test.ts +204 -0
  147. package/src/ui/server/__tests__/router-threading.test.ts +148 -0
  148. package/src/ui/server/__tests__/router.test.ts +104 -0
  149. package/src/ui/server/__tests__/sse-broadcast.test.ts +44 -0
  150. package/src/ui/server/__tests__/sse-enhancer.test.ts +70 -0
  151. package/src/ui/server/config-bridge-node.ts +8 -26
  152. package/src/ui/server/config-bridge.ts +3 -4
  153. package/src/ui/server/embedded-assets-imports.d.ts +44 -0
  154. package/src/ui/server/embedded-assets.ts +13 -2
  155. package/src/ui/server/endpoints/__tests__/meta-endpoint.test.ts +1 -0
  156. package/src/ui/server/endpoints/__tests__/pipeline-analysis-endpoint.test.ts +167 -0
  157. package/src/ui/server/endpoints/__tests__/schema-file-endpoint.test.ts +104 -0
  158. package/src/ui/server/endpoints/__tests__/task-analysis-endpoint.test.ts +103 -0
  159. package/src/ui/server/endpoints/__tests__/upload-endpoints.test.ts +162 -96
  160. package/src/ui/server/endpoints/concurrency-endpoint.ts +2 -1
  161. package/src/ui/server/endpoints/create-pipeline-endpoint.ts +14 -29
  162. package/src/ui/server/endpoints/file-endpoints.ts +15 -10
  163. package/src/ui/server/endpoints/gate-endpoints.ts +2 -6
  164. package/src/ui/server/endpoints/job-control-endpoints.ts +129 -141
  165. package/src/ui/server/endpoints/job-endpoints.ts +7 -0
  166. package/src/ui/server/endpoints/meta-endpoint.ts +1 -1
  167. package/src/ui/server/endpoints/pipeline-analysis-endpoint.ts +99 -11
  168. package/src/ui/server/endpoints/schema-file-endpoint.ts +11 -3
  169. package/src/ui/server/endpoints/task-analysis-endpoint.ts +21 -5
  170. package/src/ui/server/endpoints/task-save-endpoint.ts +1 -2
  171. package/src/ui/server/endpoints/upload-endpoints.ts +5 -40
  172. package/src/ui/server/index.ts +19 -14
  173. package/src/ui/server/job-reader.ts +14 -2
  174. package/src/ui/server/router.ts +33 -10
  175. package/src/ui/server/sse-broadcast.ts +14 -3
  176. package/src/ui/server/sse-enhancer.ts +12 -2
  177. package/src/ui/state/__tests__/schema-loader.test.ts +11 -1
  178. package/src/ui/state/__tests__/snapshot.test.ts +120 -14
  179. package/src/ui/state/__tests__/types.test.ts +104 -5
  180. package/src/ui/state/snapshot.ts +2 -2
  181. package/src/ui/state/transformers/__tests__/list-transformer.test.ts +85 -2
  182. package/src/ui/state/transformers/__tests__/status-transformer.test.ts +250 -22
  183. package/src/ui/state/transformers/list-transformer.ts +13 -3
  184. package/src/ui/state/transformers/status-transformer.ts +36 -170
  185. package/src/ui/state/types.ts +15 -48
  186. package/src/utils/__tests__/path-containment.test.ts +160 -0
  187. package/src/{ui/server/utils → utils}/path-containment.ts +21 -0
  188. package/src/task-analysis/__tests__/enrichers-analysis-writer.test.ts +0 -86
  189. package/src/task-analysis/__tests__/enrichers-artifact-resolver.test.ts +0 -124
  190. package/src/task-analysis/__tests__/extractors-artifacts.test.ts +0 -133
  191. package/src/task-analysis/__tests__/extractors-llm-calls.test.ts +0 -46
  192. package/src/task-analysis/__tests__/extractors-stages.test.ts +0 -52
  193. package/src/task-analysis/__tests__/index.test.ts +0 -143
  194. package/src/task-analysis/__tests__/parser.test.ts +0 -41
  195. package/src/task-analysis/__tests__/utils-ast.test.ts +0 -82
  196. package/src/task-analysis/enrichers/analysis-writer.ts +0 -75
  197. package/src/task-analysis/enrichers/artifact-resolver.ts +0 -89
  198. package/src/task-analysis/extractors/artifacts.ts +0 -143
  199. package/src/task-analysis/extractors/llm-calls.ts +0 -117
  200. package/src/task-analysis/extractors/stages.ts +0 -45
  201. package/src/task-analysis/parser.ts +0 -20
  202. package/src/task-analysis/utils/ast.ts +0 -45
  203. package/src/ui/dist/assets/index--RH3sAt3.js.map +0 -1
  204. package/src/ui/dist/assets/style-CSSKuMOe.css +0 -2
  205. package/src/ui/embedded-assets.js +0 -12
  206. package/src/ui/server/__tests__/path-containment.test.ts +0 -54
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ parseSeedFilename,
4
+ seedFilename,
5
+ SEED_FILENAME_PATTERN,
6
+ } from "../seed-naming.ts";
7
+
8
+ describe("seedFilename", () => {
9
+ it("appends the seed suffix to the jobId", () => {
10
+ expect(seedFilename("abc")).toBe("abc-seed.json");
11
+ });
12
+
13
+ it("preserves dashes and underscores in the jobId", () => {
14
+ expect(seedFilename("job-0123_abc")).toBe("job-0123_abc-seed.json");
15
+ });
16
+
17
+ it("round-trips with parseSeedFilename", () => {
18
+ const jobId = "job-0123_abc";
19
+ expect(parseSeedFilename(seedFilename(jobId))).toBe(jobId);
20
+ });
21
+ });
22
+
23
+ describe("parseSeedFilename", () => {
24
+ it("captures the jobId from a valid seed filename", () => {
25
+ expect(parseSeedFilename("abc-seed.json")).toBe("abc");
26
+ });
27
+
28
+ it("captures jobIds that include dashes and underscores", () => {
29
+ expect(parseSeedFilename("job-0123_abc-seed.json")).toBe("job-0123_abc");
30
+ });
31
+
32
+ it("returns null for a filename without the seed suffix", () => {
33
+ expect(parseSeedFilename("foo.json")).toBeNull();
34
+ });
35
+
36
+ it("returns null when the first character is not alphanumeric", () => {
37
+ expect(parseSeedFilename("-seed.json")).toBeNull();
38
+ });
39
+
40
+ it("returns null when the suffix is not .json", () => {
41
+ expect(parseSeedFilename("bar-seed.txt")).toBeNull();
42
+ });
43
+
44
+ it("returns null when the name contains a path separator", () => {
45
+ expect(parseSeedFilename("a/b-seed.json")).toBeNull();
46
+ });
47
+ });
48
+
49
+ describe("SEED_FILENAME_PATTERN", () => {
50
+ it("requires the leading character to be alphanumeric", () => {
51
+ expect(SEED_FILENAME_PATTERN.exec("_abc-seed.json")).toBeNull();
52
+ });
53
+
54
+ it("matches the spec regex literal", () => {
55
+ expect(SEED_FILENAME_PATTERN.source).toBe("^([A-Za-z0-9][A-Za-z0-9-_]*)-seed\\.json$");
56
+ });
57
+ });
@@ -0,0 +1,159 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { describe, expect, it } from "bun:test";
5
+
6
+ const ROOT = path.resolve(import.meta.dir, "../../..");
7
+ const SRC_DIR = path.join(ROOT, "src");
8
+ const DOCS_PATH = path.join(ROOT, "docs/http-api.md");
9
+ const BREAKING_CHANGE_DATE = "2026-06-19";
10
+
11
+ const ALLOWLIST = new Set<string>([
12
+ "src/core/job-view.ts",
13
+ "src/config/statuses.ts",
14
+ "src/ui/server/endpoints/job-control-endpoints.ts",
15
+ "src/ui/client/adapters/job-adapter.ts",
16
+ ]);
17
+
18
+ const DISPLAY_EXEMPT = new Set<string>([
19
+ "src/utils/ui.tsx",
20
+ "src/ui/components/JobCard.tsx",
21
+ ]);
22
+
23
+ const TASK_IDX_PATTERN = /`task-\$\{[^}]+\}`/;
24
+ const QUOTED_DONE = /(["'])done\1/;
25
+ const QUOTED_COMPLETE = /(["'])complete\1/;
26
+ const QUOTED_FAILED = /(["'])failed\1/;
27
+ const QUOTED_ERROR = /(["'])error\1/;
28
+
29
+ const LOG_CONTEXT = /\b(console|logger)\.[a-z]+\s*\(|\b[a-zA-Z_]\w*Error\s*\(/;
30
+
31
+ interface Hit {
32
+ file: string;
33
+ line: number;
34
+ content: string;
35
+ }
36
+
37
+ async function walkSourceFiles(dir: string): Promise<string[]> {
38
+ const entries = await readdir(dir, { withFileTypes: true });
39
+ const files: string[] = [];
40
+ for (const entry of entries) {
41
+ const fullPath = path.join(dir, entry.name);
42
+ if (entry.isDirectory()) {
43
+ if (entry.name === "__tests__" || entry.name === "node_modules" || entry.name === "dist") continue;
44
+ files.push(...(await walkSourceFiles(fullPath)));
45
+ } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
46
+ files.push(fullPath);
47
+ }
48
+ }
49
+ return files;
50
+ }
51
+
52
+ function toRelativePath(absolutePath: string): string {
53
+ return path.relative(ROOT, absolutePath).split(path.sep).join("/");
54
+ }
55
+
56
+ function isJsxAttributeValue(line: string, literal: string): boolean {
57
+ const jsxAttrPattern = new RegExp(`=\\s*["']${literal}["']`);
58
+ if (!jsxAttrPattern.test(line)) return false;
59
+ return /[<>]/.test(line) || /<\/?[A-Za-z]/.test(line);
60
+ }
61
+
62
+ async function findTaskIndexSynthesis(): Promise<Hit[]> {
63
+ const files = await walkSourceFiles(SRC_DIR);
64
+ const hits: Hit[] = [];
65
+ for (const file of files) {
66
+ const relPath = toRelativePath(file);
67
+ if (ALLOWLIST.has(relPath) || DISPLAY_EXEMPT.has(relPath)) continue;
68
+ const source = await readFile(file, "utf-8");
69
+ const lines = source.split("\n");
70
+ lines.forEach((line, index) => {
71
+ if (TASK_IDX_PATTERN.test(line)) {
72
+ hits.push({ file: relPath, line: index + 1, content: line.trim() });
73
+ }
74
+ });
75
+ }
76
+ return hits;
77
+ }
78
+
79
+ async function findStatusMapping(literalA: RegExp, literalB: RegExp, literalAWord: string, literalBWord: string): Promise<Hit[]> {
80
+ const files = await walkSourceFiles(SRC_DIR);
81
+ const hits: Hit[] = [];
82
+ for (const file of files) {
83
+ const relPath = toRelativePath(file);
84
+ if (ALLOWLIST.has(relPath) || DISPLAY_EXEMPT.has(relPath)) continue;
85
+ const source = await readFile(file, "utf-8");
86
+ const lines = source.split("\n");
87
+ lines.forEach((line, index) => {
88
+ if (LOG_CONTEXT.test(line)) return;
89
+ if (!literalA.test(line) || !literalB.test(line)) return;
90
+ if (isJsxAttributeValue(line, literalAWord) || isJsxAttributeValue(line, literalBWord)) return;
91
+ hits.push({ file: relPath, line: index + 1, content: line.trim() });
92
+ });
93
+ }
94
+ return hits;
95
+ }
96
+
97
+ describe("single derivation point guard (AC-11)", () => {
98
+ it("no production source synthesizes `task-${index}` outside the allowlist", async () => {
99
+ const hits = await findTaskIndexSynthesis();
100
+ expect(hits, formatHits("task-${index} synthesis", hits)).toEqual([]);
101
+ });
102
+
103
+ it("no production source maps 'done' ↔ 'complete' job-status spellings outside the allowlist", async () => {
104
+ const hits = await findStatusMapping(QUOTED_DONE, QUOTED_COMPLETE, "done", "complete");
105
+ expect(hits, formatHits("done ↔ complete status mapping", hits)).toEqual([]);
106
+ });
107
+
108
+ it("no production source maps 'failed' ↔ 'error' job-status spellings outside the allowlist", async () => {
109
+ const hits = await findStatusMapping(QUOTED_FAILED, QUOTED_ERROR, "failed", "error");
110
+ expect(hits, formatHits("failed ↔ error status mapping", hits)).toEqual([]);
111
+ });
112
+ });
113
+
114
+ describe("display-exempt files are display-only", () => {
115
+ it("exempt files do not import normalizeJobView or core/job-view", async () => {
116
+ for (const relPath of DISPLAY_EXEMPT) {
117
+ const absPath = path.join(ROOT, relPath);
118
+ const source = await readFile(absPath, "utf-8");
119
+ expect(source, `${relPath} should not import normalizeJobView`).not.toContain("normalizeJobView");
120
+ expect(source, `${relPath} should not import core/job-view`).not.toContain("core/job-view");
121
+ }
122
+ });
123
+ });
124
+
125
+ describe("docs/http-api.md breaking-change note (C-11, AC-11 G-mode)", () => {
126
+ it("contains the dated breaking-change entry for 'error' → 'failed'", async () => {
127
+ const docs = await readFile(DOCS_PATH, "utf-8");
128
+ expect(docs).toContain(BREAKING_CHANGE_DATE);
129
+ expect(docs.toLowerCase()).toContain("breaking change");
130
+ const errorToFailedRow = /(["'])error\1[^\n]*?["']failed["']|(["'])error\2[^a-zA-Z]+[→=>]+[^a-zA-Z]+(.*?)(["'])failed\4/;
131
+ expect(docs, "expected a row or sentence pairing \"error\" with \"failed\"").toMatch(errorToFailedRow);
132
+ });
133
+
134
+ it("contains the dated breaking-change entry for 'done' → 'complete'", async () => {
135
+ const docs = await readFile(DOCS_PATH, "utf-8");
136
+ expect(docs).toContain(BREAKING_CHANGE_DATE);
137
+ const doneToCompleteRow = /(["'])done\1[^\n]*?["']complete["']|(["'])done\2[^a-zA-Z]+[→=>]+[^a-zA-Z]+(.*?)(["'])complete\4/;
138
+ expect(docs, "expected a row or sentence pairing \"done\" with \"complete\"").toMatch(doneToCompleteRow);
139
+ });
140
+
141
+ it("documents name-keyed task ids with a 'task-${index}' fallback", async () => {
142
+ const docs = await readFile(DOCS_PATH, "utf-8");
143
+ expect(docs).toContain("task-${index}");
144
+ expect(docs.toLowerCase()).toMatch(/name[- ]keyed/);
145
+ });
146
+
147
+ it("enumerates the canonical JobStatus enum", async () => {
148
+ const docs = await readFile(DOCS_PATH, "utf-8");
149
+ for (const value of ["pending", "running", "failed", "complete", "waiting"]) {
150
+ expect(docs, `canonical JobStatus value "${value}" should be documented`).toContain(`\`${value}\``);
151
+ }
152
+ });
153
+ });
154
+
155
+ function formatHits(label: string, hits: Hit[]): string {
156
+ if (hits.length === 0) return `no ${label} found outside the allowlist`;
157
+ return `${label} found in ${hits.length} file(s) outside the allowlist:\n` +
158
+ hits.map((hit) => ` - ${hit.file}:${hit.line}: ${hit.content}`).join("\n");
159
+ }