rig-c 0.0.0-stage → 2.20.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 (213) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +13 -0
  3. package/LICENSE +30 -0
  4. package/NOTICE.md +145 -0
  5. package/README.md +817 -3
  6. package/bin/rigc.cjs +83 -0
  7. package/cli.ts +61 -0
  8. package/cli_core.ts +46 -0
  9. package/docs/AUTHORING.md +9923 -0
  10. package/docs/FACE.md +1948 -0
  11. package/docs/INGEST.md +1488 -0
  12. package/docs/MOTION.md +1241 -0
  13. package/docs/PROMPTING.md +109 -0
  14. package/docs/RIGGING.md +1441 -0
  15. package/docs/SPEC_COVERAGE.md +357 -0
  16. package/package.json +108 -4
  17. package/skills/rigc/SKILL.md +133 -0
  18. package/skills/rigc-face/SKILL.md +60 -0
  19. package/skills/rigc-ingest/SKILL.md +78 -0
  20. package/skills/rigc-motion/SKILL.md +51 -0
  21. package/skills/rigc-rigging/SKILL.md +49 -0
  22. package/src/areaband.ts +159 -0
  23. package/src/assertions/bodies/a01.ts +23 -0
  24. package/src/assertions/bodies/a02.ts +21 -0
  25. package/src/assertions/bodies/a03.ts +27 -0
  26. package/src/assertions/bodies/a04.ts +40 -0
  27. package/src/assertions/bodies/a05.ts +56 -0
  28. package/src/assertions/bodies/a06.ts +245 -0
  29. package/src/assertions/bodies/a07.ts +68 -0
  30. package/src/assertions/bodies/a08.ts +76 -0
  31. package/src/assertions/bodies/a09.ts +82 -0
  32. package/src/assertions/bodies/a10.ts +116 -0
  33. package/src/assertions/bodies/a11.ts +15 -0
  34. package/src/assertions/bodies/a12.ts +30 -0
  35. package/src/assertions/bodies/a13.ts +51 -0
  36. package/src/assertions/bodies/a14.ts +35 -0
  37. package/src/assertions/bodies/a15.ts +97 -0
  38. package/src/assertions/bodies/a16.ts +24 -0
  39. package/src/assertions/bodies/a17.ts +26 -0
  40. package/src/assertions/bodies/a18.ts +62 -0
  41. package/src/assertions/bodies/a19.ts +404 -0
  42. package/src/assertions/bodies/a20.ts +122 -0
  43. package/src/assertions/bodies/a21.ts +190 -0
  44. package/src/assertions/bodies/a22.ts +39 -0
  45. package/src/assertions/bodies/a23.ts +305 -0
  46. package/src/assertions/bodies/a24.ts +68 -0
  47. package/src/assertions/bodies/a25.ts +39 -0
  48. package/src/assertions/bodies/a26.ts +61 -0
  49. package/src/assertions/bodies/a27.ts +33 -0
  50. package/src/assertions/bodies/a28.ts +70 -0
  51. package/src/assertions/bodies/a29.ts +34 -0
  52. package/src/assertions/bodies/a30.ts +50 -0
  53. package/src/assertions/bodies/a31.ts +61 -0
  54. package/src/assertions/bodies/a32.ts +44 -0
  55. package/src/assertions/bodies/a33.ts +110 -0
  56. package/src/assertions/bodies/a34.ts +133 -0
  57. package/src/assertions/bodies/a35.ts +160 -0
  58. package/src/assertions/bodies/a36.ts +81 -0
  59. package/src/assertions/bodies/a37.ts +77 -0
  60. package/src/assertions/bodies/a38.ts +73 -0
  61. package/src/assertions/bodies/a39.ts +303 -0
  62. package/src/assertions/bodies/a40.ts +128 -0
  63. package/src/assertions/bodies/a42.ts +97 -0
  64. package/src/assertions/bodies/a43.ts +181 -0
  65. package/src/assertions/bodies/a44.ts +23 -0
  66. package/src/assertions/bodies/a45.ts +172 -0
  67. package/src/assertions/bodies/a46.ts +224 -0
  68. package/src/assertions/bodies/a47.ts +126 -0
  69. package/src/assertions/bodies/a48.ts +83 -0
  70. package/src/assertions/bodies/a49.ts +81 -0
  71. package/src/assertions/bodies/a50.ts +97 -0
  72. package/src/assertions/constraint_words.ts +169 -0
  73. package/src/assertions/emitted/index.ts +148 -0
  74. package/src/assertions/facts/animated_bones.ts +30 -0
  75. package/src/assertions/facts/animation_durations.ts +37 -0
  76. package/src/assertions/facts/atlas_pages.ts +19 -0
  77. package/src/assertions/facts/atlas_regions.ts +52 -0
  78. package/src/assertions/facts/bone_timelines.ts +37 -0
  79. package/src/assertions/facts/constraint_targets.ts +56 -0
  80. package/src/assertions/facts/constraints.ts +155 -0
  81. package/src/assertions/facts/deform_survey.ts +27 -0
  82. package/src/assertions/facts/event_keys.ts +55 -0
  83. package/src/assertions/facts/linked_meshes.ts +38 -0
  84. package/src/assertions/facts/mesh_attachments.ts +100 -0
  85. package/src/assertions/facts/region_joins.ts +34 -0
  86. package/src/assertions/facts/sequences.ts +85 -0
  87. package/src/assertions/facts/skeleton_roster.ts +45 -0
  88. package/src/assertions/facts/skin_entries.ts +37 -0
  89. package/src/assertions/facts/skin_members.ts +53 -0
  90. package/src/assertions/facts/slider_composition.ts +78 -0
  91. package/src/assertions/facts/slot_colour.ts +43 -0
  92. package/src/assertions/facts/stage.ts +27 -0
  93. package/src/assertions/facts/stage_box.ts +65 -0
  94. package/src/assertions/facts/stepped_poses.ts +74 -0
  95. package/src/assertions/facts/two_colour.ts +52 -0
  96. package/src/assertions/facts/vertex_polygons.ts +53 -0
  97. package/src/assertions/footprints.ts +367 -0
  98. package/src/assertions/harness.ts +109 -0
  99. package/src/assertions/inward_advance.ts +58 -0
  100. package/src/assertions/kinds.ts +105 -0
  101. package/src/assertions/mesh_kinds.ts +56 -0
  102. package/src/assertions/model/animated_bones.ts +38 -0
  103. package/src/assertions/model/animation_durations.ts +57 -0
  104. package/src/assertions/model/atlas_pages.ts +15 -0
  105. package/src/assertions/model/atlas_regions.ts +76 -0
  106. package/src/assertions/model/bone_timelines.ts +58 -0
  107. package/src/assertions/model/constraint_targets.ts +82 -0
  108. package/src/assertions/model/constraints.ts +233 -0
  109. package/src/assertions/model/declared.ts +125 -0
  110. package/src/assertions/model/deform_survey.ts +24 -0
  111. package/src/assertions/model/event_keys.ts +45 -0
  112. package/src/assertions/model/given.ts +45 -0
  113. package/src/assertions/model/index.ts +398 -0
  114. package/src/assertions/model/linked_meshes.ts +24 -0
  115. package/src/assertions/model/mesh_attachments.ts +119 -0
  116. package/src/assertions/model/parse.ts +146 -0
  117. package/src/assertions/model/region_joins.ts +67 -0
  118. package/src/assertions/model/runtime_timelines.ts +78 -0
  119. package/src/assertions/model/sequences.ts +157 -0
  120. package/src/assertions/model/skeleton_roster.ts +23 -0
  121. package/src/assertions/model/skin_entries.ts +69 -0
  122. package/src/assertions/model/skin_members.ts +64 -0
  123. package/src/assertions/model/slider_composition.ts +193 -0
  124. package/src/assertions/model/slot_colour.ts +81 -0
  125. package/src/assertions/model/stage.ts +28 -0
  126. package/src/assertions/model/stage_box.ts +51 -0
  127. package/src/assertions/model/stepped_poses.ts +105 -0
  128. package/src/assertions/model/two_colour.ts +61 -0
  129. package/src/assertions/model/vertex_polygons.ts +72 -0
  130. package/src/assertions/reasons.ts +129 -0
  131. package/src/assertions/region_lookups.ts +61 -0
  132. package/src/assertions/report.ts +189 -0
  133. package/src/assertions/values.ts +39 -0
  134. package/src/atlas.ts +2870 -0
  135. package/src/ballot.ts +866 -0
  136. package/src/bonedist.ts +643 -0
  137. package/src/chainfit.ts +2752 -0
  138. package/src/chains.ts +170 -0
  139. package/src/check.ts +4303 -0
  140. package/src/checkpics.ts +295 -0
  141. package/src/cli/core_commands.ts +1627 -0
  142. package/src/cli/repack.ts +414 -0
  143. package/src/cli/shared.ts +2776 -0
  144. package/src/cli/spine_commands.ts +820 -0
  145. package/src/compile.ts +9414 -0
  146. package/src/core/additive.ts +458 -0
  147. package/src/core/animation.ts +1050 -0
  148. package/src/core/clipping.ts +696 -0
  149. package/src/core/constraints.ts +1876 -0
  150. package/src/core/constraints_path.ts +964 -0
  151. package/src/core/constraints_physics.ts +881 -0
  152. package/src/core/constraints_slider.ts +635 -0
  153. package/src/core/deform.ts +613 -0
  154. package/src/core/draw_order.ts +125 -0
  155. package/src/core/events.ts +135 -0
  156. package/src/core/hooks.ts +249 -0
  157. package/src/core/index.ts +1400 -0
  158. package/src/core/raw.ts +739 -0
  159. package/src/core/skins.ts +129 -0
  160. package/src/core/uvs.ts +469 -0
  161. package/src/core/vertices.ts +490 -0
  162. package/src/core/walk.ts +197 -0
  163. package/src/core/world.ts +289 -0
  164. package/src/correspondence.ts +15 -0
  165. package/src/deformbuild.ts +60 -0
  166. package/src/deformgen.ts +630 -0
  167. package/src/deformmeasure.ts +732 -0
  168. package/src/deformreport.ts +373 -0
  169. package/src/deformstructure.ts +386 -0
  170. package/src/deformsurvey.ts +2162 -0
  171. package/src/depth.ts +784 -0
  172. package/src/diff.ts +2252 -0
  173. package/src/emit.ts +134 -0
  174. package/src/emit_spine.ts +854 -0
  175. package/src/errors.ts +53 -0
  176. package/src/framing.ts +819 -0
  177. package/src/generation.ts +139 -0
  178. package/src/ingest.ts +2293 -0
  179. package/src/json-position.ts +253 -0
  180. package/src/keyorder.ts +587 -0
  181. package/src/keys.ts +486 -0
  182. package/src/ladder.ts +121 -0
  183. package/src/mesh.ts +2382 -0
  184. package/src/meshcompare.ts +1188 -0
  185. package/src/meshquality.ts +2042 -0
  186. package/src/meshrasters.ts +944 -0
  187. package/src/meshreduce.ts +1425 -0
  188. package/src/model.ts +1245 -0
  189. package/src/motion.ts +809 -0
  190. package/src/nonfinite.ts +54 -0
  191. package/src/package_meta.ts +48 -0
  192. package/src/png.ts +297 -0
  193. package/src/pose.ts +2324 -0
  194. package/src/preview.ts +434 -0
  195. package/src/region_joins.ts +54 -0
  196. package/src/render.ts +1013 -0
  197. package/src/render_core.ts +871 -0
  198. package/src/render_shared.ts +2958 -0
  199. package/src/repack.ts +495 -0
  200. package/src/rig.ts +2941 -0
  201. package/src/slots.ts +892 -0
  202. package/src/spine_side.ts +138 -0
  203. package/src/timelines.ts +837 -0
  204. package/src/trackgen.ts +364 -0
  205. package/src/transform.ts +310 -0
  206. package/src/types.ts +1797 -0
  207. package/src/validate.ts +3875 -0
  208. package/tools/contact.ts +126 -0
  209. package/tools/editor_roundtrip.ts +1641 -0
  210. package/tools/font5x7.ts +101 -0
  211. package/tools/measure_contact_depth.ts +105 -0
  212. package/tools/plate.ts +508 -0
  213. package/tools/png_probe.mjs +72 -0
@@ -0,0 +1,414 @@
1
+ /**
2
+ * `rigc repack <build> --out <dir>` — a packed build repacked from its own
3
+ * output, written only after the repack is shown to have lost nothing (issue
4
+ * #1169).
5
+ *
6
+ * A consumer that keeps only `skeleton.json`, `skeleton.atlas` and the page
7
+ * PNGs could not take up a packer improvement (`--page-edges free`,
8
+ * `--pack-shape polygon`) without regenerating its parts upstream. The route
9
+ * it measured by hand — cut each region off its page, `ingest --art loose`,
10
+ * `build --pack` over the cut parts — is this command, in process and through
11
+ * the tree's own functions: `liftAtlas` (`extractRegion` per region), `ingest`,
12
+ * and `runBuild` with the entry's own gate, the body every `build` runs.
13
+ *
14
+ * 🔒 **Nothing reaches `--out` until three things have been shown**, and the
15
+ * command prints each:
16
+ *
17
+ * (a) every region lifted off the NEW pages is pixel-identical, by name, to
18
+ * the same region lifted off the input's pages (`regionDifferences`);
19
+ * (b) the rebuilt `skeleton.json` is byte-identical to the input's. A repack
20
+ * is entitled to change nothing in the skeleton: attachments name their
21
+ * regions and the atlas alone carries where a region sits, so the pack
22
+ * owns the atlas and the pages and none of the skeleton. Measured on the
23
+ * tree's recipes, every repack of a rigc build wrote the same bytes;
24
+ * a difference is refused naming the paths (`skeletonDifferences`);
25
+ * (c) the gate is green — `runBuild` returns only then, having gated the
26
+ * compile and the packed pair exactly as `build --pack` does.
27
+ *
28
+ * The build runs in a work directory of its own under `tmpdir()`
29
+ * (`REPACK_WORK_PREFIX`), which also holds the lifted parts and the two specs;
30
+ * the output is the build alone, the same four kinds of file `build --pack`
31
+ * writes, so a repacked build can be repacked again. The directory is removed
32
+ * when the command ends — green, refused, a gate that exits, or a throw —
33
+ * from this function's `finally` and, for the `process.exit` inside
34
+ * `runBuild` that no `finally` sees, from an `exit` listener. A process killed
35
+ * by a signal runs neither and leaves its one directory.
36
+ *
37
+ * ⚠️ The lifted parts are not kept: the output is the input's shape, and the
38
+ * parts are recoverable from it at any time by the same lift — keeping a
39
+ * second copy of every texel beside the pages would be two records of one
40
+ * thing that can disagree.
41
+ */
42
+ import { parseAtlasText } from '../atlas.ts';
43
+ import { relativeImagesPath } from '../compile.ts';
44
+ import { ingest, INGEST_GUTTERS, IngestError, type IngestFinding, type IngestOptions, IngestSpecRefused, type IngestStage } from '../ingest.ts';
45
+ import { MODEL_DOCUMENT_FILE } from '../model.ts';
46
+ import { atlasRefusals, documentDisagreements, firstLineApart, type InputPage, liftAtlas, polygonOwnedTexels, regionDifferences, REPACK_WORK_PREFIX, RepackError, skeletonDifferences } from '../repack.ts';
47
+ import {
48
+ type BuildGate,
49
+ type BuildReport,
50
+ type CommandArgs,
51
+ documentStageBeside,
52
+ modelTextBeside,
53
+ parseJsonNamed,
54
+ readIntFlag,
55
+ readPackShape,
56
+ readPageEdges,
57
+ readProfile,
58
+ readReportFlag,
59
+ readSkeletonText,
60
+ readVersion,
61
+ resolveBuild,
62
+ runBuild,
63
+ UsageError,
64
+ writeBuildReport,
65
+ } from './shared.ts';
66
+ import { DEFAULT_PADDING, DEFAULT_PAGE_SIZE } from '../atlas.ts';
67
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
68
+ import { tmpdir } from 'node:os';
69
+ import { basename, dirname, join, resolve } from 'node:path';
70
+
71
+ /** The flags `repack` takes: the packing flags `build --pack` takes, the gate's profile, the atlas when it is not beside the skeleton, the stage `ingest` takes, and `--out`. */
72
+ /**
73
+ * The switch that writes a rebuild whose skeleton differs from the input's
74
+ * (issue #1169's follow-up). Default off: check (b) refuses a difference, and
75
+ * the refusal names this flag. A general acceptance rather than a switch for one
76
+ * field, because the command cannot know WHY a skeleton differs — only where —
77
+ * and every path is printed when it is accepted.
78
+ */
79
+ export const ACCEPT_FLAG = 'accept-skeleton-differences';
80
+
81
+ export const REPACK_FLAGS = ['out', 'atlas', 'page-size', 'padding', 'page-edges', 'pack-shape', 'profile', 'stage', 'stage-box', ACCEPT_FLAG, 'report'] as const;
82
+
83
+ /** `build`'s flags that `repack` does not take, and why — each refused by name rather than ignored. */
84
+ const BUILD_FLAGS_REFUSED: Readonly<Record<string, string>> = {
85
+ rig: 'repack reads the rig back out of the skeleton (ingest); it takes no rig spec',
86
+ motion: 'repack reads the motion back out of the skeleton (ingest); it takes no motion spec',
87
+ manifest: 'repack lifts the parts off the pages; it takes no cut manifest',
88
+ images: 'repack lifts the parts off the pages the atlas names; it takes no parts directory',
89
+ 'copy-images': 'repack always writes self-contained pages into --out, which is what packing is',
90
+ pack: 'repack always packs; the flag would say nothing',
91
+ 'atlas-in': 'repack MAKES a new pack out of the regions of the old one; --atlas-in would keep the old pack — pass the build itself',
92
+ cut: 'repack takes a build directory, not a cut',
93
+ cuts: 'repack takes a build directory, not a cuts table',
94
+ };
95
+
96
+ /** The three files besides the pages that every repacked build is, in the order `runBuild` writes them. */
97
+ const BUILD_TEXTS = ['skeleton.json', 'skeleton.atlas', MODEL_DOCUMENT_FILE] as const;
98
+
99
+ /**
100
+ * A hook between the lift and the build, given the directory the parts were
101
+ * lifted into — the selftest's plant (`RPK`), which alters a lifted part to
102
+ * show check (a) refusing before anything is written. Nothing else passes one.
103
+ */
104
+ export type RepackPlant = (liftDir: string) => void;
105
+
106
+ /** How every gate's closing line opens (`BuildGate.look`), which a staged build drops. */
107
+ const LOOK_LINE = 'rigc: look at it: ';
108
+
109
+ /** How many skeleton differences a refusal lists before it says how many more there are. */
110
+ const SKELETON_DIFFERENCES_SHOWN = 8;
111
+
112
+ /** `--stage x,y,w,h`, read as `ingest` reads it. */
113
+ function readStageFlag(raw: string | undefined): IngestStage | undefined {
114
+ if (raw === undefined) return undefined;
115
+ const parts = raw.split(',').map(Number);
116
+ if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) throw new UsageError(`--stage is ${JSON.stringify(raw)}; give four numbers, x,y,width,height`);
117
+ return { x: parts[0], y: parts[1], width: parts[2], height: parts[3] };
118
+ }
119
+
120
+ /** Every flag given that `repack` does not take, refused by name. */
121
+ function refuseForeignFlags(flags: Record<string, string>): void {
122
+ const taken = new Set<string>([...REPACK_FLAGS, 'help']);
123
+ for (const name of Object.keys(flags).sort()) {
124
+ if (taken.has(name)) continue;
125
+ const why = BUILD_FLAGS_REFUSED[name];
126
+ throw new UsageError(why === undefined ? `repack takes no --${name}; it takes --${REPACK_FLAGS.join(', --')}` : `repack takes no --${name}: ${why}`);
127
+ }
128
+ }
129
+
130
+ /** `--out`: required, and absent or an empty directory — refused by name otherwise, before anything is read. */
131
+ function readOut(flags: Record<string, string>): string {
132
+ if (flags.out === undefined) throw new UsageError('repack needs --out <dir> — the directory the repacked build is written into');
133
+ const out = resolve(flags.out);
134
+ if (existsSync(out)) {
135
+ if (!statSync(out).isDirectory()) throw new RepackError(`--out ${out} is a file; it takes a directory, absent or empty — nothing was written`, 2);
136
+ const held = readdirSync(out).sort();
137
+ if (held.length > 0) {
138
+ throw new RepackError(
139
+ `--out ${out} is not empty (${held.slice(0, 4).join(', ')}${held.length > 4 ? `, … ${held.length} entries` : ''}) — a page an earlier pack wrote and this one does not ` +
140
+ 'would stay beside the new atlas with nothing naming it, so repack writes only into an absent or empty directory; nothing was written',
141
+ 2,
142
+ );
143
+ }
144
+ }
145
+ return out;
146
+ }
147
+
148
+ /**
149
+ * The one place `repack` calls `ingest`'s options: the skeleton read back with
150
+ * `--art loose` (an `image` per attachment, named after the region it draws,
151
+ * which is the file the lift wrote), the parts directory spelled from the
152
+ * specs, and the stage by `ingest`'s own rules — a caller's `--stage`, else
153
+ * the model document beside the skeleton when it is this skeleton's, else the
154
+ * header. A new `ingest` option goes in here and nowhere else.
155
+ */
156
+ function ingestOptionsFor(
157
+ skeletonPath: string,
158
+ specsDir: string,
159
+ liftDir: string,
160
+ stage: IngestStage | undefined,
161
+ stageBox: string | undefined,
162
+ documentStage: IngestStage | null | undefined,
163
+ ): IngestOptions {
164
+ return {
165
+ ...(stageBox === undefined ? {} : { stageBox }),
166
+ ...(documentStage === undefined ? {} : { documentStage }),
167
+ name: basename(skeletonPath, '.json'),
168
+ art: 'loose',
169
+ images: relativeImagesPath(specsDir, liftDir),
170
+ stage,
171
+ source: basename(skeletonPath),
172
+ version: readVersion(),
173
+ };
174
+ }
175
+
176
+ /** The stage line: the box the rebuild declares and where it came from (issue #1169's step 6). */
177
+ function stageLine(
178
+ rig: { skeleton?: { x?: number | null; y?: number | null; width?: number | null; height?: number | null } },
179
+ flagged: boolean,
180
+ stageBox: string | undefined,
181
+ documentStage: IngestStage | null | undefined,
182
+ ): string {
183
+ const head = rig.skeleton;
184
+ const box =
185
+ head !== undefined && typeof head.width === 'number' && typeof head.height === 'number'
186
+ ? `${head.width} x ${head.height} at ${head.x ?? 0},${head.y ?? 0}`
187
+ : 'none';
188
+ const why = flagged
189
+ ? '— from --stage, which ingest takes only for a skeleton that states no box'
190
+ : stageBox !== undefined
191
+ ? `— read from the bounding box in slot "${stageBox}" (--stage-box), the stage the build carried in its own files${documentStage === undefined ? '' : `, and equal to the ${MODEL_DOCUMENT_FILE} beside it (ingest refuses the two disagreeing)`}`
192
+ : documentStage !== undefined
193
+ ? `— read from the ${MODEL_DOCUMENT_FILE} beside it (its spine.sha256 is this skeleton's)${documentStage === null ? ', which states the rig declared none' : ''}`
194
+ : box === 'none'
195
+ ? `— the skeleton's header states no box and no ${MODEL_DOCUMENT_FILE} of this skeleton is beside it`
196
+ : `— read from the skeleton's header box: no ${MODEL_DOCUMENT_FILE} of this skeleton is beside it, and which box it is depends on the rigc that wrote it — on a build written before 2.2.0 it is the stage itself, on one written since it is the setup-pose bounding box, not the stage the rig declared, and nothing in the skeleton says which; keep ${MODEL_DOCUMENT_FILE} beside the pair to carry the stage, or, for a build that carries it as a bounding box (skeleton.stageBox), pass --stage-box <slot> to read it exactly`;
197
+ return ` .. stage ${box} ${why}. skeleton.json is held byte-identical below whatever it is; ${MODEL_DOCUMENT_FILE}'s stage, and the stage rules on the rebuild, read it`;
198
+ }
199
+
200
+ /** The findings `ingest` reported, in its own format, blockers first. */
201
+ function findingLines(findings: readonly IngestFinding[]): string[] {
202
+ const width = Math.max(...Object.values(INGEST_GUTTERS).map((gutter) => gutter.length));
203
+ const lines: string[] = [];
204
+ for (const kind of ['blocker', 'judgement', 'lossy'] as const) {
205
+ for (const f of findings.filter((x) => x.kind === kind)) lines.push(` ${INGEST_GUTTERS[kind].padEnd(width)} ${f.code}: ${f.where} — ${f.detail}`);
206
+ }
207
+ return lines;
208
+ }
209
+
210
+ /**
211
+ * `runBuild` with its two closing lines restated for a build that is staged:
212
+ * `rigc: wrote <work>/…` becomes `staged <work>/…`, and the `look at it` line
213
+ * is dropped — the work directory is removed when the command ends, and
214
+ * `repack` names `--out` itself once it has written there. Every other line is
215
+ * printed as it comes, so a gate that exits mid-way loses none.
216
+ */
217
+ function stagedBuild(flags: Record<string, string>, gate: BuildGate, record: BuildReport | null): void {
218
+ const print = console.log;
219
+ console.log = (...args: unknown[]): void => {
220
+ const line = args.map((a) => (typeof a === 'string' ? a : String(a))).join(' ');
221
+ if (line.startsWith('rigc: wrote ')) print(` .. staged ${line.slice('rigc: wrote '.length)}`);
222
+ else if (!line.startsWith(LOOK_LINE)) print(...args);
223
+ };
224
+ try {
225
+ runBuild(flags, gate, record);
226
+ } finally {
227
+ console.log = print;
228
+ }
229
+ }
230
+
231
+ /** The pages and the atlas against the input's: a line saying which are byte-identical. */
232
+ function pagesLine(inputAtlas: string, inputPages: readonly InputPage[], outAtlas: string, buildDir: string, outPages: readonly string[]): string {
233
+ const inputBytes = new Map(inputPages.map((p) => [p.page.name, p.path]));
234
+ const same = outPages.filter((name) => {
235
+ const from = inputBytes.get(name);
236
+ return from !== undefined && readFileSync(from).equals(readFileSync(join(buildDir, name)));
237
+ });
238
+ if (inputAtlas === outAtlas && same.length === outPages.length && outPages.length === inputPages.length) {
239
+ return ` .. pages skeleton.atlas and all ${outPages.length} page(s) byte-identical to the input's — these settings pack these regions as the input was packed`;
240
+ }
241
+ return (
242
+ ` .. pages skeleton.atlas ${inputAtlas === outAtlas ? 'byte-identical to' : 'differs from'} the input's; ${same.length} of ${outPages.length} page(s) byte-identical to the input's page of the same name ` +
243
+ `(the input had ${inputPages.length}) — pages move under other pack settings; what (a) holds is every region`
244
+ );
245
+ }
246
+
247
+ /**
248
+ * repack — the command body both entries register, with the entry's own gate
249
+ * (`build`'s). `plant` is the selftest's (`RepackPlant`).
250
+ */
251
+ export function cmdRepack({ flags, positional }: CommandArgs, gate: BuildGate, plant?: RepackPlant): void {
252
+ refuseForeignFlags(flags);
253
+ if (positional.length !== 1) throw new UsageError(`repack takes one path — the build directory, or its skeleton.json — and was given ${positional.length}`);
254
+ // Every flag read before anything is: a typo is a usage error, never a default.
255
+ const packing: Record<string, string> = {
256
+ 'page-size': String(readIntFlag(flags, 'page-size', DEFAULT_PAGE_SIZE)),
257
+ padding: String(readIntFlag(flags, 'padding', DEFAULT_PADDING)),
258
+ 'page-edges': readPageEdges(flags),
259
+ 'pack-shape': readPackShape(flags),
260
+ profile: readProfile(flags),
261
+ };
262
+ const stage = readStageFlag(flags.stage);
263
+ const out = readOut(flags);
264
+ // `--report` (issue #1213): refused inside --out, written on a red gate by the build body and here once --out is written.
265
+ const record = readReportFlag(flags, 'repack', out, gate.supplier);
266
+ const input = resolveBuild(positional[0], flags.atlas);
267
+ if (input.atlas !== 'there') throw new UsageError(input.atlasRefusal);
268
+ const skeletonText = readSkeletonText(input.skeletonPath);
269
+ const atlasText = readFileSync(input.atlasPath, 'utf8');
270
+ const parsed = parseAtlasText(atlasText);
271
+
272
+ console.log(`rigc repack ${input.skeletonPath}`);
273
+ console.log(` .. atlas ${input.atlasPath}`);
274
+ console.log(` .. out ${out}`);
275
+ console.log(` .. pack page-size ${packing['page-size']}, padding ${packing.padding}, page-edges ${packing['page-edges']}, pack-shape ${packing['pack-shape']}, profile ${packing.profile}`);
276
+
277
+ // What the atlas cannot carry through a repack, all of it, before anything is made.
278
+ const { refusals, pages } = atlasRefusals(parsed, dirname(input.atlasPath));
279
+ const modelText = modelTextBeside(input.skeletonPath, skeletonText);
280
+ const disagreement = modelText === undefined ? null : documentDisagreements((parseJsonNamed(modelText, join(dirname(input.skeletonPath), MODEL_DOCUMENT_FILE)) as { pages?: unknown }).pages, parsed);
281
+ if (disagreement !== null) {
282
+ refusals.push(
283
+ ...disagreement.lines.map(
284
+ (line) => `the atlas and ${MODEL_DOCUMENT_FILE} beside it disagree — ${line}; the document is the record of where the build placed each region, so one of the two was edited after it was written`,
285
+ ),
286
+ );
287
+ }
288
+ if (refusals.length > 0) {
289
+ throw new RepackError(
290
+ `${input.atlasPath} cannot be lifted exactly — ${refusals.length} reason(s), and nothing was written:\n${refusals.map((r) => ` ${r}`).join('\n')}`,
291
+ );
292
+ }
293
+ console.log(
294
+ ` .. model ${
295
+ disagreement === null
296
+ ? `no ${MODEL_DOCUMENT_FILE} of this skeleton beside it (or one stating no pages); the atlas is read as it stands`
297
+ : `the atlas held to ${MODEL_DOCUMENT_FILE}'s pages beside it: ${disagreement.regions} region(s), every placement field equal`
298
+ }`,
299
+ );
300
+
301
+ const documentStage = documentStageBeside(input.skeletonPath, skeletonText);
302
+ const work = mkdtempSync(join(tmpdir(), REPACK_WORK_PREFIX));
303
+ console.error(`rigc repack: work directory ${work}`);
304
+ const removeWork = (): void => rmSync(work, { recursive: true, force: true });
305
+ // The `process.exit` inside `runBuild` (a red gate) and the refusal chain's are seen by no `finally`.
306
+ const onExit = (code: number): void => {
307
+ removeWork();
308
+ if (code !== 0) console.error(`rigc repack: nothing was written to ${out}; the work directory ${work} was removed`);
309
+ };
310
+ process.on('exit', onExit);
311
+ try {
312
+ const liftDir = join(work, 'lift');
313
+ const specsDir = join(work, 'specs');
314
+ const buildDir = join(work, 'build');
315
+ const lifted = liftAtlas(pages, liftDir);
316
+ console.log(
317
+ ` .. lift ${lifted.parts.size} region(s) off ${pages.length} page(s), each named once: ${lifted.turned} turned on its page, ${lifted.trimmed} trimmed of whitespace — ` +
318
+ 'each lifted as the drawing it is (extractRegion)',
319
+ );
320
+ if (plant !== undefined) plant(liftDir);
321
+
322
+ let result: { rig: unknown; motion: unknown; findings: IngestFinding[] };
323
+ try {
324
+ result = ingest(parseJsonNamed(skeletonText, input.skeletonPath), ingestOptionsFor(input.skeletonPath, specsDir, liftDir, stage, flags['stage-box'], documentStage));
325
+ } catch (err) {
326
+ if (err instanceof IngestError) throw new RepackError(`ingest refused the skeleton: ${err.message} — nothing was written`, 2);
327
+ if (!(err instanceof IngestSpecRefused)) throw err;
328
+ result = { rig: err.rig, motion: err.motion, findings: err.findings };
329
+ }
330
+ console.log(stageLine(result.rig as Parameters<typeof stageLine>[0], stage !== undefined, flags['stage-box'], documentStage));
331
+ const blockers = result.findings.filter((f) => f.kind === 'blocker');
332
+ console.log(` .. ingest ${result.findings.length} finding(s), ${blockers.length} blocker(s)`);
333
+ for (const line of findingLines(result.findings)) console.log(line);
334
+ if (blockers.length > 0) {
335
+ throw new RepackError(
336
+ `ingest found ${blockers.length} blocker(s) (${[...new Set(blockers.map((f) => f.code))].join(', ')}), so a rebuild would not be the skeleton that was read — nothing was written`,
337
+ );
338
+ }
339
+ mkdirSync(specsDir, { recursive: true });
340
+ const rigPath = join(specsDir, 'rig.json');
341
+ const motionPath = join(specsDir, 'motion.json');
342
+ writeFileSync(rigPath, `${JSON.stringify(result.rig, null, 2)}\n`);
343
+ writeFileSync(motionPath, `${JSON.stringify(result.motion, null, 2)}\n`);
344
+
345
+ // (c) `build --pack` through the entry's own gate. It returns only green; a red gate exits from inside it.
346
+ stagedBuild({ rig: rigPath, motion: motionPath, out: buildDir, pack: 'true', ...packing }, gate, record);
347
+
348
+ // (a) and (b), both before the first write.
349
+ const outAtlas = readFileSync(join(buildDir, 'skeleton.atlas'), 'utf8');
350
+ const repacked = atlasRefusals(parseAtlasText(outAtlas), buildDir);
351
+ if (repacked.refusals.length > 0) {
352
+ throw new Error(`internal: the repacked atlas is one this command would refuse to lift:\n${repacked.refusals.join('\n')}`);
353
+ }
354
+ const outSkeleton = readFileSync(join(buildDir, 'skeleton.json'), 'utf8');
355
+ const owned = packing['pack-shape'] === 'polygon' ? polygonOwnedTexels(outSkeleton, lifted.parts, Number(packing.padding)) : undefined;
356
+ const regions = regionDifferences(lifted.parts, liftAtlas(repacked.pages).parts, owned);
357
+ const lost: string[] = [];
358
+ if (regions.differences.length > 0) lost.push(`(a) ${regions.differences.length} region(s) are not pixel-identical to the input's:`, ...regions.differences.map((d) => ` ${d}`));
359
+ else {
360
+ console.log(
361
+ ` .. check (a) regions: ${regions.identical} of ${lifted.parts.size} pixel-identical, by name, to the same region lifted off the input's pages — ` +
362
+ (regions.byFootprint === 0
363
+ ? 'each over its whole rectangle'
364
+ : `${regions.identical - regions.byFootprint} over the whole rectangle, ${regions.byFootprint} that only meshes draw over the footprint a polygon page keeps as its own (the rest of its rectangle may hold a neighbour, by design)`),
365
+ );
366
+ }
367
+ const accepting = flags[ACCEPT_FLAG] !== undefined;
368
+ if (outSkeleton === skeletonText) {
369
+ console.log(
370
+ " .. check (b) skeleton.json: byte-identical to the input's — the pack owns none of it" +
371
+ (accepting ? `; --${ACCEPT_FLAG} accepted nothing, because there was nothing to accept` : ''),
372
+ );
373
+ } else {
374
+ // Accepted, every path is printed: an accepted difference nobody is shown is the silence the check exists to remove.
375
+ const diff = skeletonDifferences(parseJsonNamed(skeletonText, input.skeletonPath), parseJsonNamed(outSkeleton, join(buildDir, 'skeleton.json')), accepting ? Infinity : SKELETON_DIFFERENCES_SHOWN);
376
+ const head =
377
+ diff.total === 0
378
+ ? `skeleton.json: the rebuild states the same values in other bytes — ${firstLineApart(skeletonText, outSkeleton)}`
379
+ : `skeleton.json: ${diff.total} place(s) differ from the input`;
380
+ if (accepting) {
381
+ console.log(` .. check (b) ${head}, accepted by --${ACCEPT_FLAG} — the output's skeleton is this rigc's rebuild, not the input's bytes:`);
382
+ for (const line of diff.lines) console.log(` .. ${line}`);
383
+ } else {
384
+ lost.push(
385
+ `(b) ${head}, which the pack does not own${diff.total > diff.lines.length ? ` (the first ${diff.lines.length})` : ''}:`,
386
+ ...diff.lines.map((d) => ` ${d}`),
387
+ ' a repack changes where regions sit and nothing in the skeleton; this skeleton is not what this rigc writes from what it states. ' +
388
+ `--${ACCEPT_FLAG} writes the rebuild anyway — the skeleton this rigc writes from what the input states, every difference listed — ` +
389
+ 'with check (a) and the gate exactly as they are; a build written before 2.2.0, whose header was the stage, differs here in its header box and nowhere else',
390
+ );
391
+ }
392
+ }
393
+ if (lost.length > 0) throw new RepackError(`the repack lost something, so nothing was written to ${out}:\n${lost.map((l) => ` ${l}`).join('\n')}`);
394
+ console.log(' .. check (c) gate: green — the report above, on the compile and on the packed pair');
395
+
396
+ const outParsed = parseAtlasText(outAtlas);
397
+ const outPages = outParsed.pages.map((p) => p.name);
398
+ const staged = readdirSync(buildDir).sort();
399
+ const expected = [...BUILD_TEXTS, ...outPages].sort();
400
+ if (staged.join('\n') !== expected.join('\n')) throw new Error(`internal: the staged build holds [${staged.join(', ')}], not [${expected.join(', ')}]`);
401
+ console.log(pagesLine(atlasText, pages, outAtlas, buildDir, outPages));
402
+
403
+ mkdirSync(out, { recursive: true });
404
+ for (const name of [...outPages, ...BUILD_TEXTS]) {
405
+ copyFileSync(join(buildDir, name), join(out, name));
406
+ console.log(`rigc: wrote ${join(out, name)}`);
407
+ }
408
+ if (record !== null) writeBuildReport(record);
409
+ console.log(gate.look(out));
410
+ } finally {
411
+ removeWork();
412
+ process.off('exit', onExit);
413
+ }
414
+ }