@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/README.md +4 -0
- package/cli.ts +43 -6
- package/docs/AUTHORING.md +251 -185
- package/docs/STABILITY.md +4 -2
- package/package.json +1 -1
- package/skills/rig-parts/SKILL.md +8 -1
- package/src/build.ts +3 -3
- package/src/errors.ts +16 -0
- package/src/guide.ts +255 -0
- package/src/scene.ts +2 -2
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
|
|
39
|
-
stable identifier,
|
|
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.
|
|
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,
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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 };
|