@gentbajko/slopify 0.1.0 → 0.3.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.
Files changed (94) hide show
  1. package/README.md +2 -2
  2. package/dist/adapter-registry.js +11 -12
  3. package/dist/adapters/image/bytes.js +1 -1
  4. package/dist/adapters/image/fal.js +55 -44
  5. package/dist/adapters/image/openai.js +19 -24
  6. package/dist/adapters/image/replicate.js +33 -43
  7. package/dist/adapters/llm/claude-code.js +16 -19
  8. package/dist/adapters/llm/codex.js +14 -15
  9. package/dist/adapters/llm/openrouter.js +13 -16
  10. package/dist/adapters/llm/run-cli.js +9 -11
  11. package/dist/adapters/llm/sse-lines.js +1 -2
  12. package/dist/adapters/retry-after.js +6 -6
  13. package/dist/adapters/tts/cartesia.js +5 -5
  14. package/dist/adapters/tts/elevenlabs.js +14 -19
  15. package/dist/adapters/tts/openai.js +9 -10
  16. package/dist/edge/cli.js +0 -0
  17. package/dist/edge/http/actions.js +8 -9
  18. package/dist/edge/http/app.js +3 -3
  19. package/dist/edge/http/entries.js +1 -1
  20. package/dist/edge/http/projects.js +7 -7
  21. package/dist/edge/http/prompts.js +6 -6
  22. package/dist/edge/http/providers.js +4 -4
  23. package/dist/edge/http/settings.js +2 -2
  24. package/dist/edge/http/staging.js +1 -1
  25. package/dist/edge/http/telemetry.js +6 -7
  26. package/dist/edge/http/usage.js +5 -5
  27. package/dist/kernel/config/index.js +1 -1
  28. package/dist/kernel/db/tx.js +9 -12
  29. package/dist/kernel/log.js +5 -5
  30. package/dist/kernel/pipeline.js +7 -9
  31. package/dist/kernel/ports/model.js +3 -4
  32. package/dist/kernel/runner/attempt-repo.js +5 -6
  33. package/dist/kernel/runner/attempt.js +16 -23
  34. package/dist/kernel/runner/graph.js +18 -29
  35. package/dist/kernel/runner/index.js +27 -33
  36. package/dist/kernel/runner/piece-repo.js +9 -11
  37. package/dist/kernel/runner/providers.js +5 -7
  38. package/dist/kernel/version.js +3 -3
  39. package/dist/main.js +10 -11
  40. package/dist/slices/admission/model.js +2 -2
  41. package/dist/slices/admission/repo.js +6 -7
  42. package/dist/slices/admission/rules.js +12 -15
  43. package/dist/slices/admission/start.js +7 -8
  44. package/dist/slices/admission/substitute.js +11 -11
  45. package/dist/slices/article/continuation.js +9 -9
  46. package/dist/slices/article/plain.js +5 -5
  47. package/dist/slices/article/run.js +25 -25
  48. package/dist/slices/article/split.js +1 -1
  49. package/dist/slices/article/store.js +5 -5
  50. package/dist/slices/cancel/index.js +8 -8
  51. package/dist/slices/images/run.js +32 -34
  52. package/dist/slices/library/lint.js +3 -3
  53. package/dist/slices/library/model.js +7 -7
  54. package/dist/slices/library/repo.js +3 -3
  55. package/dist/slices/library/save.js +7 -7
  56. package/dist/slices/library/slots.js +5 -5
  57. package/dist/slices/narration/chunk.js +12 -12
  58. package/dist/slices/narration/concat.js +12 -13
  59. package/dist/slices/narration/run.js +46 -49
  60. package/dist/slices/reruns/cascade.js +10 -11
  61. package/dist/slices/reruns/index.js +36 -37
  62. package/dist/slices/research/planner.js +4 -5
  63. package/dist/slices/research/run.js +22 -23
  64. package/dist/slices/research/synthesis.js +5 -5
  65. package/dist/slices/settings/cli-status.js +9 -10
  66. package/dist/slices/settings/keys.js +8 -9
  67. package/dist/slices/settings/model.js +5 -5
  68. package/dist/slices/settings/readiness.js +3 -4
  69. package/dist/slices/settings/repo.js +1 -1
  70. package/dist/slices/settings/voices.js +3 -4
  71. package/dist/slices/storage/asset-name.js +2 -2
  72. package/dist/slices/storage/delete-project.js +3 -5
  73. package/dist/slices/storage/downloads.js +4 -4
  74. package/dist/slices/storage/layout.js +5 -6
  75. package/dist/slices/storage/model.js +1 -1
  76. package/dist/slices/storage/reconcile.js +6 -7
  77. package/dist/slices/storage/repo.js +1 -1
  78. package/dist/slices/storage/staging.js +12 -15
  79. package/dist/slices/telemetry/collector-client.js +2 -2
  80. package/dist/slices/telemetry/flush.js +8 -8
  81. package/dist/slices/telemetry/machine.js +6 -7
  82. package/dist/slices/telemetry/model.js +11 -13
  83. package/dist/slices/telemetry/record.js +7 -7
  84. package/dist/slices/telemetry/repo.js +4 -4
  85. package/dist/slices/telemetry/usage.js +5 -5
  86. package/dist/slices/thumbnail/by-llm.js +2 -2
  87. package/dist/slices/thumbnail/run.js +30 -32
  88. package/dist/slices/video/ffmpeg.js +6 -6
  89. package/dist/slices/video/plan.js +9 -9
  90. package/dist/slices/video/run.js +19 -20
  91. package/dist/web/assets/index-DTqh2zoJ.js +81 -0
  92. package/dist/web/index.html +1 -1
  93. package/package.json +1 -1
  94. package/dist/web/assets/index-D_sWbKQi.js +0 -81
@@ -5,7 +5,7 @@ import { cliEvent, cliShaped, endedWithout, promptOf } from "./run-cli.js";
5
5
  import { lines } from "./sse-lines.js";
6
6
  // The local-agent adapter for the Codex CLI. Same shape as Claude Code's and a different
7
7
  // vocabulary: Codex writes a JSONL thread of `thread.started`, `item.*` and `turn.*`
8
- // events. No key here either - the CLI's own login authenticates it (`logic/02` §Q135).
8
+ // events. No key here either - the CLI's own login authenticates it.
9
9
  export const codexBinary = "codex";
10
10
  // ceiling: a fixed list. `codex` has no offline command that prints the models an account
11
11
  // may use - `codex doctor` reports the install, not the catalogue, and the model refresh
@@ -16,13 +16,13 @@ export const codexModels = [
16
16
  { id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max" },
17
17
  { id: "gpt-5.6-sol", name: "GPT-5.6 Sol" },
18
18
  ];
19
- // `codex exec --help` (0.149.1) for the flags. `-c web_search=<mode>` is a TOML override,
20
- // and the binary's own error names the modes: "unknown variant `bogus`, expected one of
21
- // `disabled`, `cached`, `indexed`, `live`". `live` is the grounded mode `logic/06` asks
22
- // for and `disabled` is what every other stage runs under, so no stage grounds itself by
23
- // accident. `--ephemeral` keeps no session file, `--skip-git-repo-check` lets it run in
24
- // the data directory, and the quotes in the value are part of the argv element because
25
- // the override is parsed as TOML, where a bare `live` is not a string.
19
+ // `codex exec --help` (0.149.1) for the flags. `-c web_search=<mode>` is a TOML override, and
20
+ // the binary's own error names the modes: "unknown variant `bogus`, expected one of `disabled`,
21
+ // `cached`, `indexed`, `live`". `live` is the grounded mode research asks for and `disabled` is
22
+ // what every other stage runs under, so none grounds itself by accident. `--ephemeral` keeps no
23
+ // session file, `--skip-git-repo-check` lets it run in the data directory, and the quotes in
24
+ // the value are part of the argv element because the override is parsed as TOML, where a bare
25
+ // `live` is not a string.
26
26
  export function codexArgs(req) {
27
27
  return [
28
28
  "exec",
@@ -72,10 +72,9 @@ export function codexLlm(deps) {
72
72
  }
73
73
  if (event.type === "turn.completed") {
74
74
  const { usage } = cliShaped(binary, turnCompleted, event.value);
75
- // ceiling: Codex reports no stop reason, so the continuation loop of
76
- // `logic/07` §Q59 cannot tell a finished answer from one cut at the output
77
- // limit for this provider. A `--output-schema` run would, at the cost of
78
- // constraining every stage's answer.
75
+ // ceiling: Codex reports no stop reason, so the continuation loop cannot tell a
76
+ // finished answer from one cut at the output limit. A `--output-schema` run
77
+ // would, at the cost of constraining every stage's answer.
79
78
  yield { type: "done", usage: usageOf(usage), finishReason: null };
80
79
  return;
81
80
  }
@@ -101,8 +100,8 @@ export function codexLlm(deps) {
101
100
  run.kill();
102
101
  }
103
102
  // A cancelled run ends its stream the same way an exhausted one does: the child was
104
- // killed, so stdout simply stopped. `logic/13` §Q112 says an aborted call counts
105
- // nothing, so it must not be reported as the provider failing.
103
+ // killed, so stdout simply stopped. An aborted call counts as nothing, so it must not
104
+ // be reported as the provider failing.
106
105
  req.signal.throwIfAborted();
107
106
  // The stream ended with neither a completed turn nor a failure.
108
107
  throw providerError({
@@ -113,7 +112,7 @@ export function codexLlm(deps) {
113
112
  return {
114
113
  id: "codex",
115
114
  // ceiling: one `item.completed` per whole message rather than per token, same as the
116
- // other CLI. Enough for the idle timeout of `logic/01` §Q62 to see life on the stream.
115
+ // other CLI. Enough for the idle timeout to see life on the stream.
117
116
  capabilities: { streams: true, reportsUsage: true, webSearch: true },
118
117
  models: () => Promise.resolve(codexModels),
119
118
  complete,
@@ -3,9 +3,8 @@ import { redact } from "../../kernel/log.js";
3
3
  import { providerError } from "../../kernel/ports/model.js";
4
4
  import { retryAfter } from "../retry-after.js";
5
5
  import { sseData } from "./sse-lines.js";
6
- // The HTTP gateway adapter (01-architecture Module boundaries). `fetch` plus the line
7
- // reader beside this file, no SDK: 05-dependencies records global `fetch` at rung 3 and
8
- // the reader at rung 6, so nothing here is worth a dependency.
6
+ // The HTTP gateway adapter: the platform's own `fetch` plus the line reader beside this
7
+ // file, no SDK. Neither is worth a dependency.
9
8
  export const openRouterBase = "https://openrouter.ai/api/v1";
10
9
  // OpenRouter ranks apps by these two headers; they carry no user data.
11
10
  const appHeaders = {
@@ -13,7 +12,7 @@ const appHeaders = {
13
12
  "X-Title": "Slopify",
14
13
  };
15
14
  // A wire payload is narrowed, never cast: everything unlisted is dropped at the seam so
16
- // no vendor shape can leak past this file (01-architecture §Q10, §Q33).
15
+ // no vendor shape can leak past this file.
17
16
  const modelList = z.object({
18
17
  data: z.array(z.object({ id: z.string(), name: z.string().optional() })),
19
18
  });
@@ -46,9 +45,9 @@ export function openRouterLlm(deps) {
46
45
  })),
47
46
  stream: true,
48
47
  // The usage-accounting flag: without it the final chunk carries no token counts
49
- // and the Usage page would have nothing to count (`logic/16`).
48
+ // and the Usage page would have nothing to count.
50
49
  usage: { include: true },
51
- // `logic/06` §Q47: web grounding is asked for explicitly. OpenRouter runs the
50
+ // Web grounding is asked for explicitly. OpenRouter runs the
52
51
  // search itself, so the plugin works whatever model was picked.
53
52
  ...(req.webSearch === true ? { plugins: [{ id: "web" }] } : {}),
54
53
  }),
@@ -86,8 +85,8 @@ export function openRouterLlm(deps) {
86
85
  }
87
86
  if (choice?.finish_reason !== undefined && choice.finish_reason !== null) {
88
87
  // It arrives on the last choice chunk, one frame before the usage frame, so it is
89
- // held rather than read off whichever chunk happens to be last (`logic/07` §Q59
90
- // reads it to tell a finished article from a truncated one).
88
+ // held rather than read off whichever chunk happens to be last - the continuation
89
+ // loop reads it to tell a finished article from a truncated one.
91
90
  finishReason = choice.finish_reason;
92
91
  }
93
92
  if (chunk.usage !== undefined && chunk.usage !== null) {
@@ -131,7 +130,7 @@ export function openRouterLlm(deps) {
131
130
  };
132
131
  }
133
132
  function headers(key) {
134
- // `logic/02` §Q13: an attempt that finds no key fails rather than calling anonymously
133
+ // An attempt that finds no key fails rather than calling anonymously
135
134
  // and being told off by the provider in words the user cannot act on. `missing_key`
136
135
  // rather than `auth` because the same rule makes it terminal: there is nothing to
137
136
  // retry until the user saves a key, and the wrapper is where that is decided.
@@ -140,8 +139,7 @@ function headers(key) {
140
139
  }
141
140
  return { Authorization: `Bearer ${key}`, ...appHeaders };
142
141
  }
143
- // Only the adapter can read a vendor's status code, so only the adapter names the kind;
144
- // the attempt wrapper maps it and nothing downstream classifies again (03-conventions).
142
+ // Only the adapter sees the vendor's status code, so only the adapter names the kind.
145
143
  function kindOf(status) {
146
144
  if (status === 401 || status === 403) {
147
145
  return "auth";
@@ -149,16 +147,15 @@ function kindOf(status) {
149
147
  if (status === 429) {
150
148
  return "rate_limit";
151
149
  }
152
- // ceiling: everything else is `other` and is retried. 400 and 402 will fail the same
153
- // way four times over; a terminal kind for "this request will never work" would have to
154
- // be added to the port's error contract first, which is not this adapter's to widen.
150
+ // ceiling: everything else is `other` and is retried, so a 400 or a 402 fails the same
151
+ // way four times over. A terminal "this will never work" kind has to reach the port's
152
+ // error contract first, which is not this adapter's to widen.
155
153
  return "other";
156
154
  }
157
155
  async function failure(response) {
158
156
  const text = await response.text().catch(() => "");
159
157
  const parsed = errorBody.safeParse(safeJson(text));
160
- // The provider's own words, verbatim, through the same redactor the wrapper uses: an
161
- // error body is free to quote the key back and this is the first place it is held.
158
+ // The provider's own words, through the redactor - an error body may quote the key back.
162
159
  const message = redact(parsed.success ? parsed.data.error.message : text.trim() || response.statusText);
163
160
  const retryAfterMs = retryAfter(response.headers.get("retry-after"));
164
161
  return providerError({
@@ -8,12 +8,11 @@ import { providerError } from "../../kernel/ports/model.js";
8
8
  // buries its reason in the first line of a long report.
9
9
  export const stderrMax = 8192;
10
10
  export function nodeRunCli(binary, args, signal) {
11
- // An argument array, never a shell string: a prompt carrying backticks, `$(...)`,
12
- // quotes or newlines is one argv element and nothing in it can become a command.
13
- // stdin is /dev/null because `codex exec` appends piped stdin to the prompt, and a pipe
14
- // nobody closes would leave it waiting for an EOF that never comes.
15
- // `signal` is how the child dies: Node sends it SIGTERM when the stage is cancelled, so
16
- // no agent session outlives the run that started it (`logic/13`).
11
+ // An argument array, never a shell string: a prompt carrying backticks, `$(...)`, quotes or
12
+ // newlines is one argv element and nothing in it can become a command. stdin is /dev/null
13
+ // because `codex exec` appends piped stdin to the prompt, and a pipe nobody closes would
14
+ // leave it waiting for an EOF that never comes. `signal` is how the child dies: Node sends it
15
+ // SIGTERM when the stage is cancelled, so no agent session outlives the run that started it.
17
16
  const child = spawn(binary, [...args], {
18
17
  signal,
19
18
  stdio: ["ignore", "pipe", "pipe"],
@@ -60,16 +59,15 @@ export function nodeRunCli(binary, args, signal) {
60
59
  };
61
60
  }
62
61
  // ceiling: both CLIs take one prompt string, so a system or assistant turn is flattened
63
- // into it under a header rather than being sent as its own message. Claude Code's
64
- // `--append-system-prompt` is the upgrade if a stage ever needs a real system turn; today
65
- // `logic/06` and `logic/07` each compose exactly one user message.
62
+ // into it under a header. Claude Code's `--append-system-prompt` is the upgrade if a stage
63
+ // ever needs a real system turn; today research and the article each compose one message.
66
64
  export function promptOf(messages) {
67
65
  return messages
68
66
  .map((message) => message.role === "user" ? message.content : `[${message.role}]\n${message.content}`)
69
67
  .join("\n\n");
70
68
  }
71
- // A bare non-zero exit tells the user nothing, so whatever the CLI put on stderr is what
72
- // the stage shows (`logic/01` unhappy paths: the provider's error text verbatim).
69
+ // A bare non-zero exit tells the user nothing, so the stage shows whatever the CLI put on
70
+ // stderr - the provider's error text, verbatim.
73
71
  export function endedWithout(binary, ended, stderr) {
74
72
  const said = redact(stderr.trim());
75
73
  const how = ended.error === null
@@ -1,7 +1,6 @@
1
1
  // Bytes off a socket or off a child's stdout, turned into the units above them. One file
2
2
  // for both because OpenRouter's SSE and the two CLIs' JSONL differ only in what a line
3
- // means, and 05-dependencies records the decision: global `fetch` at rung 3, this reader
4
- // at rung 6, no SDK for any of the three providers.
3
+ // means, and none of the three providers is worth an SDK.
5
4
  // A chunk boundary lands wherever the network or the pipe put it: between two lines, in
6
5
  // the middle of one, or inside a multi-byte character. TextDecoder's streaming mode holds
7
6
  // back the tail of a split UTF-8 sequence instead of emitting a replacement character,
@@ -1,10 +1,10 @@
1
- // `logic/01` §Q4: a 429 naming a Retry-After replaces the fixed backoff for that wait.
2
- // RFC 9110 allows seconds or an HTTP date; `Date.parse` is the platform's own reader for
3
- // the second form, so no date parsing is written here.
1
+ // A 429 naming a Retry-After replaces the fixed backoff for that wait. RFC 9110 allows seconds
2
+ // or an HTTP date; `Date.parse` is the platform's own reader for the second form, so no date
3
+ // parsing is written here.
4
4
  //
5
- // It sits beside the adapters rather than inside one because the rule is the retry
6
- // policy's, not any provider's: four adapters read the same header off the same kind of
7
- // response. Nothing else is shared between them.
5
+ // It sits beside the adapters rather than inside one because the rule is the retry policy's,
6
+ // not any provider's: four adapters read the same header off the same kind of response. Nothing
7
+ // else is shared between them.
8
8
  export function retryAfter(header) {
9
9
  if (header === null) {
10
10
  return undefined;
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { redact } from "../../kernel/log.js";
3
3
  import { providerError } from "../../kernel/ports/model.js";
4
4
  import { retryAfter } from "../retry-after.js";
5
- // The HTTP gateway adapter for Cartesia (01-architecture Module boundaries). `fetch` and
5
+ // The HTTP gateway adapter for Cartesia. `fetch` and
6
6
  // nothing else; `/tts/bytes` streams the audio it renders, so the response body is
7
7
  // already the stream the port asks for.
8
8
  export const cartesiaBase = "https://api.cartesia.ai";
@@ -12,7 +12,7 @@ export const cartesiaBase = "https://api.cartesia.ai";
12
12
  export const cartesiaVersion = "2026-03-01";
13
13
  export const cartesiaModel = "sonic-3.5";
14
14
  // mp3 is the port's container; `bit_rate` is required for it and `sample_rate` fixes the
15
- // rate the concatenation of `logic/08` step 4 then keeps.
15
+ // rate the concatenation then keeps.
16
16
  const outputFormat = { container: "mp3", bit_rate: 128_000, sample_rate: 44_100 };
17
17
  // The structured error of API version 2026-03-01 and newer.
18
18
  const errorBody = z.object({
@@ -33,7 +33,7 @@ export function cartesiaTts(deps) {
33
33
  "Cartesia-Version": cartesiaVersion,
34
34
  "Content-Type": "application/json",
35
35
  },
36
- // `logic/08` §Q69: no pre-check on length; Cartesia's own limit surfaces as its
36
+ // No pre-check on length; Cartesia's own limit surfaces as its
37
37
  // error. `language` is left out so the model reads it off the transcript.
38
38
  body: JSON.stringify({
39
39
  model_id: cartesiaModel,
@@ -54,7 +54,7 @@ export function cartesiaTts(deps) {
54
54
  }
55
55
  function keyOf(deps) {
56
56
  const key = deps.key();
57
- // `logic/02` §Q13: an absent key is terminal, so it never becomes a request.
57
+ // An absent key is terminal, so it never becomes a request.
58
58
  if (key === undefined || key === "") {
59
59
  throw providerError({ kind: "missing_key", message: "no Cartesia key is stored" });
60
60
  }
@@ -78,7 +78,7 @@ async function failure(response, voiceId) {
78
78
  const retryAfterMs = retryAfter(response.headers.get("retry-after"));
79
79
  return providerError({
80
80
  kind: kindOf(response.status),
81
- // `logic/02` §Q14: `voice_not_found` has to say which voice was asked for; Cartesia's
81
+ // `voice_not_found` has to say which voice was asked for; Cartesia's
82
82
  // own message does not repeat the id.
83
83
  message: `Cartesia answered ${response.status} for voice ${voiceId}: ${message}`,
84
84
  ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
@@ -2,17 +2,16 @@ import { z } from "zod";
2
2
  import { redact } from "../../kernel/log.js";
3
3
  import { providerError } from "../../kernel/ports/model.js";
4
4
  import { retryAfter } from "../retry-after.js";
5
- // The HTTP gateway adapter for ElevenLabs (01-architecture Module boundaries). `fetch`
6
- // and nothing else: 05-dependencies records global `fetch` at rung 3, and the response
7
- // body already is the `ReadableStream<Uint8Array>` the port asks for, so an SDK would buy
8
- // nothing but a dependency.
5
+ // The HTTP gateway adapter for ElevenLabs: the platform's own `fetch` and nothing else. The
6
+ // response body already is the `ReadableStream<Uint8Array>` the port asks for, so an SDK
7
+ // would buy nothing but a dependency.
9
8
  export const elevenLabsBase = "https://api.elevenlabs.io/v1";
10
9
  // The /stream endpoint rather than the plain one: `capabilities.streams` is true, and the
11
10
  // attempt wrapper measures its 120 s as an idle timeout between chunks only when bytes
12
- // keep arriving (`logic/01` §Q62).
11
+ // keep arriving.
13
12
  export const elevenLabsModel = "eleven_multilingual_v2";
14
- // mp3 at the port's container, 44.1 kHz, 128 kbps: `kernel/ports/tts.ts` fixes mp3 and
15
- // `logic/08` step 4 keeps the provider's own sample rate through the concatenation.
13
+ // mp3 at the port's container, 44.1 kHz, 128 kbps: `kernel/ports/tts.ts` fixes mp3 and the
14
+ // concatenation keeps the provider's own sample rate.
16
15
  export const elevenLabsFormat = "mp3_44100_128";
17
16
  // A wire payload is narrowed, never cast. `detail` is an object on a handled failure and
18
17
  // a string on the framework's own; anything else falls back to the raw text.
@@ -32,7 +31,7 @@ export function elevenLabsTts(deps) {
32
31
  method: "POST",
33
32
  signal: req.signal,
34
33
  headers: { "xi-api-key": keyOf(deps), "Content-Type": "application/json" },
35
- // `logic/08` §Q69: no pre-check on length. A text past the model's limit comes
34
+ // No pre-check on length. A text past the model's limit comes
36
35
  // back as the provider's own 400 and that is what the stage shows.
37
36
  body: JSON.stringify({ text: req.text, model_id: elevenLabsModel }),
38
37
  });
@@ -48,15 +47,13 @@ export function elevenLabsTts(deps) {
48
47
  }
49
48
  function keyOf(deps) {
50
49
  const key = deps.key();
51
- // `logic/02` §Q13: an attempt that finds no key fails rather than calling anonymously.
52
- // `missing_key` rather than `auth` because the same rule makes it terminal.
50
+ // `missing_key`, not `auth`, because that rule makes it terminal.
53
51
  if (key === undefined || key === "") {
54
52
  throw providerError({ kind: "missing_key", message: "no ElevenLabs key is stored" });
55
53
  }
56
54
  return key;
57
55
  }
58
- // Only the adapter can read a vendor's status code, so only the adapter names the kind;
59
- // the attempt wrapper maps it and nothing downstream classifies again (03-conventions).
56
+ // Only the adapter sees the vendor's status code, so only the adapter names the kind.
60
57
  function kindOf(status) {
61
58
  if (status === 401 || status === 403) {
62
59
  return "auth";
@@ -64,21 +61,19 @@ function kindOf(status) {
64
61
  if (status === 429) {
65
62
  return "rate_limit";
66
63
  }
67
- // ceiling: everything else is `other` and is retried, so a text past the character
68
- // limit fails the same way four times over. A terminal kind for "this request will
69
- // never work" would have to be added to the port's error contract first, which is not
70
- // this adapter's to widen.
64
+ // ceiling: everything else is `other` and is retried, so a text past the character limit
65
+ // fails the same way four times over. A terminal "this will never work" kind has to reach the
66
+ // port's error contract first, which is not this adapter's to widen.
71
67
  return "other";
72
68
  }
73
69
  async function failure(response, voiceId) {
74
70
  const text = await response.text().catch(() => "");
75
- // The provider's own words, verbatim, through the same redactor the wrapper uses: an
76
- // error body is free to quote the key back and this is the first place it is held.
71
+ // The provider's own words, through the redactor - an error body may quote the key back.
77
72
  const message = redact(detailOf(text) || response.statusText);
78
73
  const retryAfterMs = retryAfter(response.headers.get("retry-after"));
79
74
  return providerError({
80
75
  kind: kindOf(response.status),
81
- // `logic/02` §Q14: a rejected voice ID has to be named, and it is the one part of the
76
+ // A rejected voice ID has to be named, and it is the one part of the
82
77
  // request the user chose. It is not secret, unlike everything else on the wire.
83
78
  message: `ElevenLabs answered ${response.status} for voice ${voiceId}: ${message}`,
84
79
  ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
@@ -2,12 +2,11 @@ import { z } from "zod";
2
2
  import { redact } from "../../kernel/log.js";
3
3
  import { providerError } from "../../kernel/ports/model.js";
4
4
  import { retryAfter } from "../retry-after.js";
5
- // The HTTP gateway adapter for OpenAI's speech endpoint (01-architecture Module
6
- // boundaries). `fetch` and nothing else; the response body is already the stream the port
7
- // asks for.
5
+ // The HTTP gateway adapter for OpenAI's speech endpoint. `fetch` and nothing else; the response
6
+ // body is already the stream the port asks for.
8
7
  export const openAiAudioBase = "https://api.openai.com/v1";
9
- // 05-dependencies lists gpt-4o-mini-tts, tts-1 and tts-1-hd; the first is the current one
10
- // and the cheapest of the three per character.
8
+ // gpt-4o-mini-tts, tts-1 and tts-1-hd; the first is the current one and the cheapest of the
9
+ // three per character.
11
10
  export const openAiTtsModel = "gpt-4o-mini-tts";
12
11
  const errorBody = z.object({
13
12
  error: z.object({ message: z.string(), code: z.string().nullish() }),
@@ -28,9 +27,9 @@ export function openAiTts(deps) {
28
27
  Authorization: `Bearer ${keyOf(deps)}`,
29
28
  "Content-Type": "application/json",
30
29
  },
31
- // `logic/08` §Q69: the 4096-character cap is not checked here. A longer text
32
- // comes back as OpenAI's own 400 and that is what the stage shows, so a user who
33
- // chose Whole text learns the limit from the provider that set it.
30
+ // The 4096-character cap is not checked here. A longer text comes back as OpenAI's own
31
+ // 400 and that is what the stage shows, so a user who chose Whole text learns the limit
32
+ // from the provider that set it.
34
33
  body: JSON.stringify({
35
34
  model: openAiTtsModel,
36
35
  input: req.text,
@@ -53,7 +52,7 @@ function voiceOf(voiceId) {
53
52
  }
54
53
  function keyOf(deps) {
55
54
  const key = deps.key();
56
- // `logic/02` §Q13: an absent key is terminal, so it never becomes a request.
55
+ // An absent key is terminal, so it never becomes a request.
57
56
  if (key === undefined || key === "") {
58
57
  throw providerError({ kind: "missing_key", message: "no OpenAI key is stored" });
59
58
  }
@@ -79,7 +78,7 @@ async function failure(response, voiceId) {
79
78
  const retryAfterMs = retryAfter(response.headers.get("retry-after"));
80
79
  return providerError({
81
80
  kind: kindOf(response.status),
82
- // `logic/02` §Q14: the voice ID is named, so a rejected voice reads differently from
81
+ // The voice ID is named, so a rejected voice reads differently from
83
82
  // a rejected key.
84
83
  message: `OpenAI answered ${response.status} for voice ${voiceId}: ${message}`,
85
84
  ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
package/dist/edge/cli.js CHANGED
File without changes
@@ -8,10 +8,9 @@ import { cancelProject } from "../../slices/cancel/index.js";
8
8
  import { deleteImage, editArticle, regenerateImage, rerunStage, retryStage, } from "../../slices/reruns/index.js";
9
9
  import { outputsOf } from "../../slices/storage/repo.js";
10
10
  import { onInvalid, problem, titleOf } from "./problem.js";
11
- // The actions `mockup/08-project.md` puts on the project header and on each stage: Cancel,
12
- // Retry, Re-run, Save & re-run, Regenerate one image, Delete one image. Every one of them
13
- // changes rows and files through a slice and then ticks the runner; the route itself
14
- // decides nothing (`logic/12`, `logic/13`).
11
+ // The actions on the project header and on each stage: Cancel, Retry, Re-run, Save &
12
+ // re-run, Regenerate one image, Delete one image. Every one changes rows and files through
13
+ // a slice and then ticks the runner; the route itself decides nothing.
15
14
  const idParam = z.object({
16
15
  id: z
17
16
  .string()
@@ -44,12 +43,12 @@ const details = {
44
43
  "no-project": "No project has that id.",
45
44
  "unknown-image": "This project has no image with that id.",
46
45
  "empty-article": "An article cannot be saved empty.",
47
- // logic/12 preconditions: "no stage of the project is `running` (§Q106)".
46
+ // The precondition on every re-run: no stage of the project is `running`.
48
47
  running: "This project is still running. Cancel it or wait for it to finish.",
49
48
  "not-rerunnable": "Only a stage that has finished, failed, or been canceled can be re-run.",
50
49
  "not-retryable": "Only a failed or canceled stage can be retried.",
51
50
  "no-article": "This project has no article to edit yet.",
52
- // logic/09 §Q75 and logic/12's invariant: "at least one image always remains".
51
+ // At least one image always remains.
53
52
  "last-image": "At least one image must remain, so the last one cannot be deleted.",
54
53
  };
55
54
  // The return type is inferred so Hono keeps the route types the SPA's client is
@@ -79,8 +78,8 @@ export function actionRoutes(deps) {
79
78
  outputs: outputsOf(deps.db, projectId),
80
79
  };
81
80
  };
82
- // Every re-run action ends the same way: the rows are written, then the runner is
83
- // asked to look at the project. `logic/12` step 9 does the rest by itself.
81
+ // Every re-run action ends the same way: the rows are written, then the runner is asked
82
+ // to look at the project. The cascade does the rest by itself.
84
83
  const started = (c, projectId, result) => {
85
84
  if (!result.ok) {
86
85
  return problem(c, {
@@ -99,7 +98,7 @@ export function actionRoutes(deps) {
99
98
  if (!result.ok) {
100
99
  return problem(c, { status: 404, title: titleOf(404), detail: details["no-project"] });
101
100
  }
102
- // No tick: §Q111 leaves a canceled project sitting until the user retries a stage.
101
+ // No tick: a canceled project sits until the user retries a stage.
103
102
  return c.json({ ...view(id), canceled: result.canceled });
104
103
  })
105
104
  .post("/:id/stages/:kind/retry", zValidator("param", stageParam, onInvalid), (c) => {
@@ -24,8 +24,8 @@ function apiRoutes(deps, startedAt) {
24
24
  }))
25
25
  .route("/staging", stagingRoutes(deps))
26
26
  .route("/projects", projectRoutes(deps))
27
- // The actions of `logic/12` and `logic/13` sit on the same prefix as the project
28
- // itself; they are their own router because they are their own concern.
27
+ // The re-run and cancel actions sit on the same prefix as the project itself; they
28
+ // are their own router because they are their own concern.
29
29
  .route("/projects", actionRoutes(deps))
30
30
  .route("/prompts", promptRoutes(deps))
31
31
  .route("/entries", entryRoutes(deps))
@@ -46,7 +46,7 @@ export function createApp(deps) {
46
46
  .route("/api", apiRoutes(deps, startedAt))
47
47
  .get("/api/events/global", (c) => streamSSE(c, (stream) => deps.hub.subscribeGlobal(stream, c.req.raw.signal)))
48
48
  .get("/api/events/projects/:id", (c) => streamSSE(c, (stream) => deps.hub.subscribe(c.req.param("id"), stream, c.req.raw.signal)))
49
- // Files are served by URL, not through the API (02-models Boundaries).
49
+ // Files are served by URL, not through the API.
50
50
  .route("/", fileRoutes(deps))
51
51
  // The API answers for its whole prefix, so an unknown endpoint is a problem+json 404
52
52
  // rather than the SPA's index.html with a 200.
@@ -5,7 +5,7 @@ import { entryCategories, entryModes } from "../../slices/library/model.js";
5
5
  import { listEntries } from "../../slices/library/repo.js";
6
6
  import { createEntry, removeEntry, updateEntry } from "../../slices/library/save.js";
7
7
  import { onInvalid } from "./problem.js";
8
- // `logic/15` §Q121: one rule set for prompts and entries, so one refusal mapping too.
8
+ // One rule set for prompts and entries, so one refusal mapping too.
9
9
  import { refused } from "./prompts.js";
10
10
  const idParam = z.object({
11
11
  id: z
@@ -44,7 +44,7 @@ export function projectRoutes(deps) {
44
44
  });
45
45
  return (new Hono()
46
46
  .post("/", zValidator("json", runDraftSchema, onInvalid), (c) => {
47
- // logic/04 §Q34: the bodies are read here, at the click, so an edit made since the
47
+ // The bodies are read here, at the click, so an edit made since the
48
48
  // prompt was selected is the one that runs.
49
49
  const picked = pickTemplates(deps.db, c.req.valid("json"));
50
50
  const admitted = admit({
@@ -63,13 +63,13 @@ export function projectRoutes(deps) {
63
63
  });
64
64
  }
65
65
  // Rendered from the values admit() has trimmed, so the stored text carries no
66
- // padding the user did not intend (logic/03 step 4 and step 5).
66
+ // padding the user did not intend.
67
67
  const { project } = startRun(storage, admitted.draft, renderPicked(picked, admitted.draft.values));
68
- // logic/16 step 2: one event per project created. record() swallows its own
68
+ // One event per project created. record() swallows its own
69
69
  // failures, so a broken telemetry write cannot cost the user the run.
70
70
  record(telemetry, "project.created", {});
71
71
  deps.flushSoon();
72
- // logic/04 step 6: the run starts only once the project is committed.
72
+ // The run starts only once the project is committed.
73
73
  deps.runner.tick(project.id);
74
74
  // Read back after the tick, not from the rows startRun built: the runner has
75
75
  // already claimed every eligible stage, and a body that paired status "running"
@@ -101,8 +101,8 @@ export function projectRoutes(deps) {
101
101
  outputs: outputsOf(deps.db, project.id),
102
102
  });
103
103
  })
104
- // `logic/14` step 4. Irreversible, and only from the app: 07 Projects puts the
105
- // confirmation dialog in front of it.
104
+ // Irreversible, and only from the app: the Projects screen puts a confirmation
105
+ // dialog in front of it.
106
106
  .delete("/:id", zValidator("param", idParam, onInvalid), (c) => {
107
107
  const result = deleteProject(storageForDelete, c.req.valid("param").id);
108
108
  if (result.ok) {
@@ -122,7 +122,7 @@ const deleteStatus = {
122
122
  };
123
123
  const deleteDetails = {
124
124
  "no-project": "No project has that id.",
125
- // §Q117: the run has to be stopped before its files can go.
125
+ // The run has to be stopped before its files can go.
126
126
  running: "This project is still running. Cancel the run first.",
127
127
  files: "Some of this project's files could not be removed.",
128
128
  };
@@ -13,7 +13,7 @@ const idParam = z.object({
13
13
  .regex(/^[0-9A-Za-z_-]+$/),
14
14
  });
15
15
  // Shape only. The rules - trimming, emptiness, length, the slot lint - are the slice's,
16
- // so one set of messages reaches the editor (03-conventions).
16
+ // so one set of messages reaches the editor.
17
17
  const promptBody = z.object({
18
18
  kind: z.enum(promptKinds),
19
19
  name: z.string(),
@@ -25,7 +25,7 @@ export function promptRoutes(deps) {
25
25
  const library = { db: deps.db, ids: deps.ids, clock: deps.clock };
26
26
  return (new Hono()
27
27
  // Every kind in one list: 04 Prompts filters by tab, and Duplicate needs the body it
28
- // is copying (`logic/15` step 3).
28
+ // is copying.
29
29
  .get("/", (c) => c.json({ prompts: listPrompts(deps.db) }))
30
30
  .post("/", zValidator("json", promptBody, onInvalid), (c) => {
31
31
  const result = createPrompt(library, c.req.valid("json"));
@@ -35,15 +35,15 @@ export function promptRoutes(deps) {
35
35
  const result = updatePrompt(library, c.req.valid("param").id, c.req.valid("json"));
36
36
  return result.ok ? c.json(result.value) : refused(c, result, "prompt");
37
37
  })
38
- // `logic/15` §Q123: a project holds its own rendered text, so nothing cascades and a
38
+ // A project holds its own rendered text, so nothing cascades and a
39
39
  // template used by past projects is deleted like any other.
40
40
  .delete("/:id", zValidator("param", idParam, onInvalid), (c) => {
41
41
  const result = removePrompt(library, c.req.valid("param").id);
42
42
  return result.ok ? c.body(null, 204) : refused(c, result, "prompt");
43
43
  }));
44
44
  }
45
- // One refusal mapping for both libraries: `logic/15` §Q121 puts one rule set over
46
- // prompts and entries, so entryRoutes shares this rather than mirroring it.
45
+ // One refusal mapping for both libraries: prompts and entries share one rule set, so
46
+ // entryRoutes reuses this rather than mirroring it.
47
47
  export function refused(c, failure, noun) {
48
48
  switch (failure.reason) {
49
49
  case "invalid":
@@ -53,7 +53,7 @@ export function refused(c, failure, noun) {
53
53
  detail: `This ${noun} cannot be saved; the listed fields need attention.`,
54
54
  extensions: { fields: failure.fields },
55
55
  });
56
- // §Q122: the name is refused against a row that exists, and the form marks the field.
56
+ // The name is refused against a row that exists, and the form marks the field.
57
57
  case "duplicate-name":
58
58
  return problem(c, {
59
59
  status: 409,
@@ -6,9 +6,9 @@ import { providerById, providerIds } from "../../slices/settings/model.js";
6
6
  import { providerStatuses } from "../../slices/settings/readiness.js";
7
7
  import { onInvalid, problem, titleOf } from "./problem.js";
8
8
  const providerParam = z.object({ id: z.enum(providerIds) });
9
- // ceiling: `logic/02` §Q11 forbids any format check, so the only thing said about the
10
- // value is that it is a string of a length a key could plausibly have. Raise the bound
11
- // if a provider ever issues something longer.
9
+ // ceiling: no format check is allowed on a key, so the only thing said about the value is
10
+ // that it is a string of a length a key could plausibly have. Raise the bound if a provider
11
+ // ever issues something longer.
12
12
  const keyBody = z.object({ key: z.string().min(1).max(4096) });
13
13
  // The return type is inferred so Hono keeps the route types the SPA's client is
14
14
  // generated from; see stagingRoutes.
@@ -17,7 +17,7 @@ export function providerRoutes(deps) {
17
17
  const readiness = { db: deps.db, probe: deps.probe };
18
18
  return (new Hono()
19
19
  // What Settings draws its rails from and Play its dropdowns: every provider, with
20
- // the one fact that decides whether it is selectable (`logic/02` step 5, §Q135).
20
+ // the one fact that decides whether it is selectable.
21
21
  .get("/", async (c) => c.json({ providers: await providerStatuses(readiness) }))
22
22
  .put("/:id/key", zValidator("param", providerParam, onInvalid), zValidator("json", keyBody, onInvalid), (c) => {
23
23
  const { id } = c.req.valid("param");
@@ -7,7 +7,7 @@ import { addVoice, removeVoice, voiceIdMax, voiceNameMax, voices, } from "../../
7
7
  import { onInvalid, problem, titleOf } from "./problem.js";
8
8
  const idParam = z.object({ id: z.string().min(1).max(64) });
9
9
  // Shape only. The rules - trimming, emptiness, length, the provider speaking at all -
10
- // are the slice's, so one set of messages reaches the form (03-conventions).
10
+ // are the slice's, so one set of messages reaches the form.
11
11
  const voiceBody = z.object({
12
12
  provider: z.enum(providerIds),
13
13
  name: z.string(),
@@ -40,7 +40,7 @@ export function settingsRoutes(deps) {
40
40
  .post("/voices", zValidator("json", voiceBody, onInvalid), (c) => {
41
41
  const result = addVoice(voiceDeps, c.req.valid("json"));
42
42
  if (!result.ok) {
43
- // `logic/02` §Q18: a voice ID already listed for its provider is a conflict with
43
+ // A voice ID already listed for its provider is a conflict with
44
44
  // a row that exists, which the form shows under the Voice ID input.
45
45
  return result.reason === "duplicate-voice-id"
46
46
  ? problem(c, {
@@ -10,7 +10,7 @@ import { onInvalid, problem, titleOf } from "./problem.js";
10
10
  const kindParam = z.object({ kind: z.enum(uploadableStageKinds) });
11
11
  const idParam = z.object({ id: z.string().min(1).max(64) });
12
12
  // The return type is inferred on purpose: annotating it would erase the route types the
13
- // SPA's client is generated from (02-models §Q27).
13
+ // SPA's client is generated from.
14
14
  export function stagingRoutes(deps) {
15
15
  const storage = {
16
16
  db: deps.db,