@rig-c/parts 1.15.0 → 1.16.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/docs/STABILITY.md CHANGED
@@ -35,8 +35,9 @@ Exit codes are part of the interface: **0** the command did what it says, **1**
35
35
  refused its input (every reason a `FAIL` line), **2** a usage error or a command
36
36
  this version does not implement. No command is unimplemented at 1.0.0 (`LATER`
37
37
  in `cli.ts` is empty). A refusal line has the shape
38
- ` FAIL <RULE>: <object> — <value found>; <value required>`: the rule name is a
39
- stable identifier, and the detail after it is prose that may be reworded. A command
38
+ ` FAIL <RULE>: <object> — <value found>; <value required> (see rig-parts guide <RULE>)`:
39
+ the rule name is a stable identifier, the pointer at the end is fixed text, and the
40
+ detail between them is prose that may be reworded. A command
40
41
  also answers to the alias `spine-parts` as long as the install keeps it
41
42
  ([MIGRATION.md](MIGRATION.md)). `-h`, `help` and `-v` are the short forms of
42
43
  `--help` and `--version`. The flags' meanings are `rig-parts --help`'s; their
@@ -60,6 +61,7 @@ defaults are the help's too, and none is guessed from the art.
60
61
  | `build` | `--config`, `--source`, `--full`, `--head`, `--out`; optional `--seam`, `--project`, `--page-edges`, `--pack-shape`, `--loop`, `--lead-in`, `--requirements`, `--idle-keys`, `--seam-pairs`, `--keep-stills` |
61
62
  | `compose` | `--scene`, `--out`; optional `--requirements`, `--accept` |
62
63
  | `mesh-draft` | `--config`, `--parts`, `--out`; optional `--requirements`, `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon), `--more-triangles` |
64
+ | `guide` | one positional argument: a rule name, `§<n>`, `§<n>.<m>` or `§<n>.<m>.<k>` |
63
65
  | (none) | `--version`, `--help` |
64
66
  <!-- /stable:commands -->
65
67
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/parts",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "AI-authored 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through rig-c before anything is written. A CLI for agents that cannot see the image.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,6 +13,13 @@ rig-c, which compiles, gates, packs and renders. You cannot see the painting,
13
13
  the layers or the rig. The printed lines, `docs/AUTHORING.md` and the files each
14
14
  stage writes are the whole interface.
15
15
 
16
+ **A FAIL line ends with the command that prints its row of the guide:**
17
+ `FAIL RULE: object — detail (see rig-parts guide RULE)`. Run that command before
18
+ anything else. It prints the one row of `docs/AUTHORING.md` that maps the rule to
19
+ what has to change, read from the installed file. `rig-parts guide §<n>` prints a
20
+ section, `§<n>.<m>` the m-th `###` under it and `§<n>.<m>.<k>` the k-th `####`
21
+ under that.
22
+
16
23
  ## Non-negotiables
17
24
 
18
25
  - **rig-c's gate is never bypassed.** rig-parts writes no Spine data
@@ -26,7 +33,7 @@ stage writes are the whole interface.
26
33
  because the tool refused.
27
34
  - **The refusal is the instruction.** `FAIL RULE: object — detail` names the object,
28
35
  the value found and the value required; AUTHORING §6 maps every rule to the field
29
- or input that has to change.
36
+ or input that has to change, and `rig-parts guide RULE` prints that row.
30
37
  - **Green is not "looks right".** `check` measures the gates, the seam, the loop and
31
38
  six judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`,
32
39
  `TIP_OVER_ROOT`, `STILL_REGIONS_DARK` — its face half in the head's own frame —
package/src/build.ts CHANGED
@@ -72,7 +72,7 @@ import {
72
72
  } from './autoreplay.ts';
73
73
  import { besideIdleLine, FOLD_LINE, type FoldsLine, buildGateLines, causeLines, IDLE_FPS, IDLE_MAX_PX, maxRgbDiff, REQUIREMENTS_DIR, type CheckReport, type PackLine, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, JUDGEMENT_LINES, type JudgementLine, packedBuildArgs, packedBuildLabel, type PackMode, type PackShape, type PageEdges, readFrameSet, rigcFailed, rigcIdentity, rigcRanClause, REPORTED_LINES, type ReportedLine, type RigcRunner, runCheck, SEAM_DIR, SEAM_MEAN_BAR, seamPairText, seamRigPxText, SOURCE_LINE, SEAM_PX_BAR, SEAM_PX_LEVEL, SEAM_PX_LEVEL_HIGH, SPINEBOY_YARDSTICK, STILL_DIR } from './check.ts';
74
74
  import { type Cut, loadConfig, loadConfigAndAnimations, loadEarlyConfig } from './config.ts';
75
- import { PartsError, type Problem, problemLine, refuseIfAny } from './errors.ts';
75
+ import { failLine, PartsError, type Problem, refuseIfAny } from './errors.ts';
76
76
  import type { MeshQualityReport, MotionSchedule } from 'rig-c/mesh';
77
77
  import { encodeGif } from './gif.ts';
78
78
  import { CONTROL_SUFFIX, withAnimations } from './motion.ts';
@@ -1095,7 +1095,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
1095
1095
  }
1096
1096
  log(` gate: spine-html ${fig.gate_spine_html_green ? 'green' : 'RED'} (${fig.rigc_entry.entry}${fig.rigc_entry.spine_core === null ? ', rigc\'s own validator' : `, the spine-core ${fig.rigc_entry.spine_core} round trip`})`);
1097
1097
  log(` wrote ${r.written.map((w) => join(input.out, w)).join(', ')}, ${join(input.out, 'build')}/${r.idle === null ? '' : `, ${join(input.out, 'idle_frames')}/`}${r.requirements === null ? '' : `, ${join(input.out, REQUIREMENTS_DIR)}/`}${fig.seam_pairs === undefined ? '' : `, ${join(input.out, SEAM_DIR)}/`}${existsSync(join(input.out, STILL_DIR)) ? `, ${join(input.out, STILL_DIR)}/` : ''}`);
1098
- for (const p of r.problems) log(` FAIL ${problemLine(p)}`);
1098
+ for (const p of r.problems) log(failLine(p));
1099
1099
  const reqNotPass = r.requirements === null ? 0 : r.requirements.summary.declared - r.requirements.summary.pass;
1100
1100
  const notMet = r.requirements === null ? `${r.problems.length} bar(s) not met` : `${r.problems.length - reqNotPass} bar(s) not met, ${reqNotPass} declared requirement(s) not PASS`;
1101
1101
  log(`${fig.PASS ? 'check: PASS' : `check: FAIL — ${notMet}`}; ${barsSummary(r)}`);
@@ -1466,7 +1466,7 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
1466
1466
  const refused = (stage: BuildStage, err: unknown, check: CheckReport | null = null): BuildResult => {
1467
1467
  if (!(err instanceof PartsError)) throw err;
1468
1468
  const say = prefixed(stage);
1469
- for (const p of err.problems) say(` FAIL ${problemLine(p)}`);
1469
+ for (const p of err.problems) say(failLine(p));
1470
1470
  say(`refused: ${err.problems.length} problem(s)`);
1471
1471
  log(`build: stopped at ${stage}; no later stage ran`);
1472
1472
  return { stoppedAt: stage, check, artifact: [] };
package/src/errors.ts CHANGED
@@ -23,6 +23,22 @@ export function problemLine(p: Problem): string {
23
23
  return `${p.code}: ${p.object} — ${p.detail}`;
24
24
  }
25
25
 
26
+ /**
27
+ * The end of every FAIL line: the command that prints the guide's row for its
28
+ * rule (issue #343). This is the one place that writes it, so every line has the
29
+ * same form, and rig-c's lines have the same form with its own command. A line
30
+ * that quotes a rigc failure keeps rigc's pointer inside the quote, and this one
31
+ * goes after it.
32
+ */
33
+ export function guidePointer(code: string): string {
34
+ return ` (see rig-parts guide ${code})`;
35
+ }
36
+
37
+ /** A refusal as it is printed: ` FAIL RULE: object — detail (see rig-parts guide RULE)`. */
38
+ export function failLine(p: Problem): string {
39
+ return ` FAIL ${problemLine(p)}${guidePointer(p.code)}`;
40
+ }
41
+
26
42
  export class PartsError extends Error {
27
43
  readonly problems: readonly Problem[];
28
44
 
package/src/guide.ts ADDED
@@ -0,0 +1,255 @@
1
+ /**
2
+ * `rig-parts guide`: a slice of `docs/AUTHORING.md`, read at the moment of the
3
+ * call (issue #343).
4
+ *
5
+ * The guide is one file, and it is the only copy. An agent building a rig
6
+ * reads a failure line, `FAIL RULE: object — detail (see rig-parts guide
7
+ * RULE)`, and needs the row that maps RULE to what has to change. It should
8
+ * not need the other 330 KB around that row. Every function here takes the
9
+ * guide's text and returns lines cut from it. Nothing is generated or kept
10
+ * apart from the file, so there is nothing to drift.
11
+ *
12
+ * Four readings, each defined on the text alone:
13
+ *
14
+ * - **A ledger table** is a Markdown table, outside a code fence, whose header's
15
+ * first cell is exactly `rule` (a refusal or a warning) or `line` (a line
16
+ * `check` prints). Other tables are not read.
17
+ * - **A row's names** are the backticked tokens in its first cell that are
18
+ * rule names ({@link RULE_NAME}). A wildcard such as `PARTS_*` names
19
+ * nothing. Each name sits on exactly one row in the whole guide; the
20
+ * selftest's `TY16` holds that against every name the source can print.
21
+ * - **A row's anchors** are the `<a id="NAME"></a>` tags at the start of its
22
+ * first cell, one per name. github.com keeps them as `user-content-<name>`,
23
+ * and its script lowercases a link's fragment before it looks one up, so
24
+ * `AUTHORING.md#NAME` lands on the row.
25
+ * - **`§n`** is the `## n.` heading and everything up to the next `##`.
26
+ * **`§n.m`** is the m-th `###` under it, and **`§n.m.k`** the k-th `####`
27
+ * under that, each counted at read time, up to the next heading of its level
28
+ * or above. The guide's headings carry no number. `guide §n` and `guide
29
+ * §n.m` print the numbers they counted beside each child's size.
30
+ *
31
+ * A refusal and the bar it enforces are two names, each with its own row:
32
+ * `CHECK_<LINE>` in a `rule` table, `<LINE>` in a `line` table. `guide` prints
33
+ * the asked name's row first, then `paired with <OTHER>:` and the other one's.
34
+ *
35
+ * The printed form is the one HQ settled for both packages on issue #343:
36
+ * `docs/AUTHORING.md §<addr>, line <N> — under "<heading>"` over each row;
37
+ * `docs/AUTHORING.md §<addr> "<title>", lines <a>–<b>, <X.X> KB` over a
38
+ * section, its children listed beneath in rig-c's columns; an unknown name or
39
+ * section one coded FAIL line on stderr, with rig-c's three-name ranking.
40
+ */
41
+ import type { Problem } from './errors.ts';
42
+
43
+ /** The guide's path inside the package, beside `cli.ts`. */
44
+ export const GUIDE_DOC = 'docs/AUTHORING.md';
45
+
46
+ /** A rule name: upper-case words joined by underscores (`USAGE` and `CHECK_LOOP_CLOSES` alike). */
47
+ export const RULE_NAME = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/;
48
+
49
+ /** How many of the nearest names an unknown name's refusal lists (the form shared with rig-c). */
50
+ export const NEAREST_LISTED = 3;
51
+
52
+ /** The two header words that make a table a ledger table, and what its rows are. */
53
+ export const LEDGER_HEADS = ['rule', 'line'] as const;
54
+ export type LedgerKind = (typeof LEDGER_HEADS)[number];
55
+
56
+ /** A `##` section, a `###` or a `####`: its number, its title, its lines (1-based, end exclusive) and its size. */
57
+ export interface GuideSection {
58
+ /** `"6"` for `## 6.`, `"6.3"` for the third `###` under it, `"3.6.1"` for the first `####` under §3.6. */
59
+ number: string;
60
+ /** The heading's text as written (a `##` keeps its `n. `). */
61
+ title: string;
62
+ start: number;
63
+ end: number;
64
+ /** UTF-8 bytes from the heading's line to the next heading of its level or above, each line with its newline. */
65
+ bytes: number;
66
+ }
67
+
68
+ /** One ledger row. */
69
+ export interface LedgerRow {
70
+ kind: LedgerKind;
71
+ /** The row's line, 1-based. */
72
+ line: number;
73
+ /** The table's header line and its separator line, as written. */
74
+ header: [string, string];
75
+ /** The row as written. */
76
+ text: string;
77
+ /** The rule names in the first cell, in the order written. */
78
+ names: string[];
79
+ /** The ids of the `<a id="…"></a>` tags at the start of the first cell, in order. */
80
+ anchors: string[];
81
+ /** The deepest numbered section the row sits in (`§n`, `§n.m` or `§n.m.k`). */
82
+ section: GuideSection | null;
83
+ /** The nearest heading above the row, its text as written. */
84
+ heading: string;
85
+ }
86
+
87
+ function utf8Bytes(s: string): number {
88
+ return new TextEncoder().encode(s).length;
89
+ }
90
+
91
+ /** Each line, with whether it sits inside a code fence (a fence's own lines count as inside). */
92
+ function fenced(lines: readonly string[]): boolean[] {
93
+ let inside = false;
94
+ return lines.map((l) => {
95
+ if (l.startsWith('```')) {
96
+ inside = !inside;
97
+ return true;
98
+ }
99
+ return inside;
100
+ });
101
+ }
102
+
103
+ /** Every `## n.` section, every `###` under one, and every `####` under those, numbered as {@link GuideSection} says. */
104
+ export function guideSections(text: string): GuideSection[] {
105
+ const lines = text.split('\n');
106
+ const inFence = fenced(lines);
107
+ const heads: Array<{ level: number; title: string; line: number }> = [];
108
+ lines.forEach((l, i) => {
109
+ const h = inFence[i] ? null : /^(#{1,6}) (.*)$/.exec(l);
110
+ if (h !== null) heads.push({ level: h[1].length, title: h[2], line: i + 1 });
111
+ });
112
+ const out: GuideSection[] = [];
113
+ const counts = [0, 0, 0];
114
+ let chapter: string | null = null;
115
+ heads.forEach((h, k) => {
116
+ if (h.level <= 2) {
117
+ chapter = h.level === 2 ? (/^(\d+)\. /.exec(h.title)?.[1] ?? null) : null;
118
+ counts.fill(0);
119
+ } else if (h.level === 3) {
120
+ counts[1]++;
121
+ counts[2] = 0;
122
+ } else if (h.level === 4) counts[2]++;
123
+ if (chapter === null || h.level > 4 || (h.level === 4 && counts[1] === 0)) return;
124
+ const number = h.level === 2 ? chapter : h.level === 3 ? `${chapter}.${counts[1]}` : `${chapter}.${counts[1]}.${counts[2]}`;
125
+ const next = heads.slice(k + 1).find((x) => x.level <= h.level);
126
+ const end = next === undefined ? lines.length + 1 : next.line;
127
+ const title = h.title;
128
+ const body = lines.slice(h.line - 1, end - 1).join('\n');
129
+ out.push({ number, title, start: h.line, end, bytes: utf8Bytes(body) + (next === undefined ? 0 : 1) });
130
+ });
131
+ return out;
132
+ }
133
+
134
+ /** The ledger rows, in the order written. */
135
+ export function ledgerRows(text: string): LedgerRow[] {
136
+ const lines = text.split('\n');
137
+ const inFence = fenced(lines);
138
+ const sections = guideSections(text);
139
+ const rows: LedgerRow[] = [];
140
+ let heading = '';
141
+ let header: [string, string] | null = null;
142
+ let kind: LedgerKind | null = null;
143
+ let inTable = false;
144
+ lines.forEach((l, i) => {
145
+ if (!inFence[i] && /^#{1,6} /.test(l)) heading = l.replace(/^#{1,6} /, '');
146
+ if (inFence[i] || !l.startsWith('|')) {
147
+ inTable = false;
148
+ return;
149
+ }
150
+ const first = (l.split('|')[1] ?? '').trim();
151
+ if (!inTable) {
152
+ inTable = true;
153
+ kind = (LEDGER_HEADS as readonly string[]).includes(first) ? (first as LedgerKind) : null;
154
+ header = [l, lines[i + 1] ?? ''];
155
+ return;
156
+ }
157
+ if (kind === null || header === null || /^:?-+:?$/.test(first)) return;
158
+ const names = [...first.matchAll(/`([^`]+)`/g)].map((x) => x[1]).filter((t) => RULE_NAME.test(t));
159
+ if (names.length === 0) return;
160
+ const lead = /^((?:<a id="[^"]*"><\/a>)*)/.exec(first)?.[1] ?? '';
161
+ const anchors = [...lead.matchAll(/<a id="([^"]*)"><\/a>/g)].map((x) => x[1]);
162
+ const line = i + 1;
163
+ const holding = sections.filter((s) => s.start <= line && line < s.end).sort((a, b) => b.number.split('.').length - a.number.split('.').length);
164
+ rows.push({ kind, line, header, text: l, names, anchors, section: holding[0] ?? null, heading });
165
+ });
166
+ return rows;
167
+ }
168
+
169
+ function editDistance(a: string, b: string): number {
170
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
171
+ for (let i = 1; i <= a.length; i++) {
172
+ const cur = [i];
173
+ for (let j = 1; j <= b.length; j++) cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
174
+ prev = cur;
175
+ }
176
+ return prev[b.length];
177
+ }
178
+
179
+ /**
180
+ * The {@link NEAREST_LISTED} names of `known` nearest `asked`, case aside — rig-c's ranking, the one the shared form
181
+ * fixes: a name `asked` is the start of (or, from three characters, a part of) first; then by edit distance less the
182
+ * prefix the two share; then by name. A list, never a substitute.
183
+ */
184
+ export function nearestNames(known: readonly string[], asked: string): string[] {
185
+ const key = asked.toUpperCase();
186
+ const shared = (name: string): number => {
187
+ let k = 0;
188
+ while (k < key.length && k < name.length && key[k] === name[k]) k++;
189
+ return k;
190
+ };
191
+ const score = (name: string): number => {
192
+ const up = name.toUpperCase();
193
+ return up.startsWith(key) || (key.length >= 3 && up.includes(key)) ? -Infinity : editDistance(key, up) - shared(up);
194
+ };
195
+ return [...new Set(known)]
196
+ .map((name) => ({ name, d: score(name) }))
197
+ .sort((a, b) => a.d - b.d || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
198
+ .slice(0, NEAREST_LISTED)
199
+ .map((x) => x.name);
200
+ }
201
+
202
+ /** A size as the shared form prints it: KB to one decimal. */
203
+ export function kb(bytes: number): string {
204
+ return `${(bytes / 1024).toFixed(1)} KB`;
205
+ }
206
+
207
+ /** One row's block: where it is, then the row as written. */
208
+ function rowBlock(row: LedgerRow): string[] {
209
+ return [`${GUIDE_DOC}${row.section === null ? '' : ` §${row.section.number}`}, line ${row.line} — under "${row.heading}"`, row.text];
210
+ }
211
+
212
+ /** The other half of a refusal–bar pair: `CHECK_<LINE>` and `<LINE>`, each when the guide has its row. */
213
+ function pairOf(name: string, rows: readonly LedgerRow[]): { row: LedgerRow; name: string } | null {
214
+ const other = name.startsWith('CHECK_') ? name.slice('CHECK_'.length) : `CHECK_${name}`;
215
+ const want: LedgerKind = name.startsWith('CHECK_') ? 'line' : 'rule';
216
+ const row = rows.find((r) => r.kind === want && r.names.includes(other));
217
+ return row === undefined ? null : { row, name: other };
218
+ }
219
+
220
+ /** What `guide` answers: the lines to print, or the refusal. */
221
+ export type GuideAnswer = { lines: string[] } | { problem: Problem };
222
+
223
+ /** `guide <RULE>`: the row naming it, then `paired with <OTHER>:` and its pair's; or `GUIDE_RULE_KNOWN` with the nearest names. */
224
+ export function guideRule(text: string, name: string): GuideAnswer {
225
+ const rows = ledgerRows(text);
226
+ const own = rows.find((r) => r.names.includes(name));
227
+ if (own === undefined) {
228
+ return { problem: { code: 'GUIDE_RULE_KNOWN', object: `"${name}"`, detail: `no row in ${GUIDE_DOC} names it; the nearest: ${nearestNames(rows.flatMap((r) => r.names), name).join(', ')}` } };
229
+ }
230
+ const pair = pairOf(name, rows);
231
+ return { lines: [...rowBlock(own), ...(pair === null ? [] : [`paired with ${pair.name}:`, ...rowBlock(pair.row)])] };
232
+ }
233
+
234
+ /** `guide §n`, `§n.m` or `§n.m.k`: a line naming the slice, its children's listing when it has children, then the slice. */
235
+ export function guideSection(text: string, ask: string): GuideAnswer {
236
+ const sections = guideSections(text);
237
+ const depth = (s: GuideSection): number => s.number.split('.').length;
238
+ const childrenOf = (n: string | null): GuideSection[] => sections.filter((s) => (n === null ? depth(s) === 1 : s.number.startsWith(`${n}.`) && depth(s) === n.split('.').length + 1));
239
+ const m = /^§(\d+(?:\.\d+){0,2})$/.exec(ask);
240
+ const hit = m === null ? undefined : sections.find((s) => s.number === m[1]);
241
+ if (hit === undefined) {
242
+ // The deepest address the asked one shares, and what it holds: never a section put in its place.
243
+ const asked = m === null ? [] : m[1].split('.');
244
+ let parent: GuideSection | undefined;
245
+ for (let d = asked.length - 1; d >= 1 && parent === undefined; d--) parent = sections.find((s) => s.number === asked.slice(0, d).join('.'));
246
+ const kids = childrenOf(parent?.number ?? null);
247
+ const list = kids.length === 0 ? 'none' : kids.map((s) => `§${s.number}`).join(', ');
248
+ const deepest = parent === undefined ? `the guide itself, ${GUIDE_DOC}` : `§${parent.number} "${parent.title}"`;
249
+ return { problem: { code: 'GUIDE_SECTION_KNOWN', object: `"${ask}"`, detail: `no such section; the deepest that exists is ${deepest}, whose sections are: ${list}` } };
250
+ }
251
+ const lines = text.split('\n');
252
+ const head = [`${GUIDE_DOC} §${hit.number} "${hit.title}", lines ${hit.start}–${hit.end - 1}, ${kb(hit.bytes)}`];
253
+ for (const s of childrenOf(hit.number)) head.push(`§${s.number.padEnd(6)} ${kb(s.bytes).padStart(9)} ${s.title}`);
254
+ return { lines: [...head, '', lines.slice(hit.start - 1, hit.end - 1).join('\n').replace(/\n+$/, '')] };
255
+ }
package/src/scene.ts CHANGED
@@ -95,7 +95,7 @@ import { checkStage, gateThroughRigc, type Log } from './build.ts';
95
95
  import { ACCEPTABLE_BARS, barsNotMet, type CheckReport, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, type PackMode, requirementsNotPass, type RigcRunner } from './check.ts';
96
96
  import { CONSTRAINT_BONE_FIELDS, ROOT_BONE } from './config.ts';
97
97
  import { cropToSpineY } from './coords.ts';
98
- import { PartsError, type Problem, problemLine, refuseIfAny } from './errors.ts';
98
+ import { failLine, PartsError, type Problem, refuseIfAny } from './errors.ts';
99
99
  import type { MotionSpec } from './motion.ts';
100
100
  import { readPng } from './raster/png.ts';
101
101
  import { readRequirements, type RequirementsFile } from './requirements.ts';
@@ -1353,7 +1353,7 @@ export function composeStage(input: ComposeInput, run: ComposeRunners, log: Log)
1353
1353
  const refused = (stage: ComposeStage, err: unknown, composed: ComposedScene | null = null): ComposeResult => {
1354
1354
  if (!(err instanceof PartsError)) throw err;
1355
1355
  const p = prefix(stage);
1356
- for (const x of err.problems) p(` FAIL ${problemLine(x)}`);
1356
+ for (const x of err.problems) p(failLine(x));
1357
1357
  p(`refused: ${err.problems.length} problem(s)`);
1358
1358
  log(`compose: stopped at ${stage}; nothing ${stage === 'compose' ? 'was written' : 'after the check was written'}`);
1359
1359
  return { stoppedAt: stage, composed, check: null };