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,51 @@
1
+ /**
2
+ * A13, the body (issue #1025, step 4c of #380): the mesh slots and each mesh's
3
+ * triangles stay within the budget the rig declares.
4
+ *
5
+ * Moved out of `src/validate.ts` unchanged but for what it reads: the meshes
6
+ * are a fact (`../facts/mesh_attachments.ts`) rather than the lists `validate()`'s
7
+ * prelude filled from spine-core's loaded skins, and the rig info is the
8
+ * caller's. Why the two budgets come from the rig and never from here stays
9
+ * above the `check` call in `validate()`.
10
+ */
11
+ import type { Verdicts } from '../harness.ts';
12
+ import type { MeshFacts } from '../facts/mesh_attachments.ts';
13
+ import { SKIP_NO_MESH_ATTACHMENT } from '../reasons.ts';
14
+ import type { RigInfo } from '../../types.ts';
15
+
16
+ export function a13MeshBudget({ fail, skip }: Verdicts, { meshes }: Pick<MeshFacts, 'meshes'>, input: { rig?: RigInfo }): void {
17
+ const meshAttachments = meshes;
18
+ const meshSlots = new Set(meshes.map((mesh) => mesh.slot));
19
+ const slotBudget = input.rig?.meshSlotBudget ?? null;
20
+ const triangleBudget = input.rig?.meshTriangleBudget ?? null;
21
+ if (slotBudget === null && triangleBudget === null) {
22
+ return skip(
23
+ 'A13_MESH_BUDGET',
24
+ input.rig
25
+ ? `the rig "${input.rig.archetype}" declares no \`invariants.meshSlots\` or \`invariants.meshTriangles\` budget`
26
+ : 'no rig info (validating a bare directory), so no budget is declared',
27
+ );
28
+ }
29
+ // Two clauses and only one of them has a subject that can vanish (#580).
30
+ // A slot budget is a ceiling on a COUNT, and zero is a count — "this rig
31
+ // uses 0 of its 3 mesh slots" is a measurement — so that half holds on a
32
+ // skeleton with no mesh. A triangle budget is a ceiling on each mesh, so a
33
+ // rig that declares only that one and carries no mesh has measured
34
+ // nothing at all.
35
+ if (slotBudget === null && meshAttachments.length === 0) {
36
+ return skip(
37
+ 'A13_MESH_BUDGET',
38
+ `the rig "${input.rig?.archetype}" budgets mesh triangles and nothing else, and ${SKIP_NO_MESH_ATTACHMENT}`,
39
+ );
40
+ }
41
+ if (slotBudget !== null && meshSlots.size > slotBudget) {
42
+ fail('A13_MESH_BUDGET', `${meshSlots.size} mesh slots, the rig budgets ${slotBudget}`);
43
+ }
44
+ if (triangleBudget === null) return;
45
+ for (const mesh of meshAttachments) {
46
+ const tris = (mesh.triangles?.length ?? 0) / 3;
47
+ if (tris > triangleBudget) {
48
+ fail('A13_MESH_BUDGET', `mesh "${mesh.name}" has ${tris} triangles, the rig budgets ${triangleBudget}`);
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * A14, the body (issue #1025, step 4c of #380): no mesh spans the whole
3
+ * stage.
4
+ *
5
+ * Moved out of `src/validate.ts` unchanged but for what it reads: the meshes
6
+ * and the stage are facts (`../facts/mesh_attachments.ts`, `../facts/stage.ts`)
7
+ * rather than spine-core's loaded skins and skeleton header. The `check` call
8
+ * stays in `validate()`.
9
+ */
10
+ import type { Verdicts } from '../harness.ts';
11
+ import type { MeshFacts } from '../facts/mesh_attachments.ts';
12
+ import type { StageFacts } from '../facts/stage.ts';
13
+
14
+ export function a14NoFullFrameMesh({ fail, skip }: Verdicts, { meshes }: Pick<MeshFacts, 'meshes'>, stage: StageFacts): void {
15
+ const meshAttachments = meshes;
16
+ const stageW = stage.width || 0;
17
+ const stageH = stage.height || 0;
18
+ // ⚠️ A stage-less skeleton has nothing for a mesh to span, and this rule
19
+ // used to report that as a PASS — the `stageW && stageH` guard below reads
20
+ // as a measurement of a 0x0 stage that no mesh can reach. It was only ever
21
+ // reachable from a foreign file until a rig spec could *declare* no stage
22
+ // (issue #578), and a pass certifying an unmeasured rig is the exact
23
+ // failure mode `A21_MESH_RIM_PINNED`'s `|| 'ring'` default was (#44).
24
+ if (!stageW || !stageH) {
25
+ return skip(
26
+ 'A14_NO_FULL_FRAME_MESH',
27
+ 'the skeleton declares no stage size, so there is no full frame for a mesh to span',
28
+ );
29
+ }
30
+ for (const mesh of meshAttachments) {
31
+ if (mesh.width >= stageW && mesh.height >= stageH) {
32
+ fail('A14_NO_FULL_FRAME_MESH', `mesh "${mesh.name}" spans the whole ${stageW}x${stageH} stage`);
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * A15, the body (issue #1025, step 4c of #380): `idle` keys no bone that
3
+ * drives a mesh, unless the rig declares that its idle deforms them.
4
+ *
5
+ * Moved out of `src/validate.ts` unchanged but for what it reads: the meshes
6
+ * and the bones that drive them are a fact (`../facts/mesh_attachments.ts`, the bones derived from the weights by `weightBonesOf`) rather
7
+ * than spine-core's loaded skins and the weight run decoded here, the bones
8
+ * `idle` keys are a fact (`../facts/animated_bones.ts`) rather than the raw
9
+ * JSON, and the rig info is the caller's. Why the rule assumes meshes are
10
+ * mostly static, and why a painting rig may say otherwise, stays above the
11
+ * `check` call in `validate()`.
12
+ */
13
+ import type { Verdicts } from '../harness.ts';
14
+ import { weightBonesOf, type MeshEntry, type MeshFacts } from '../facts/mesh_attachments.ts';
15
+ import type { AnimatedBoneFacts } from '../facts/animated_bones.ts';
16
+ import type { RigInfo } from '../../types.ts';
17
+
18
+ export function a15IdleNoMeshBoneKeys({ fail, skip }: Verdicts, facts: MeshFacts, animated: AnimatedBoneFacts, input: { rig?: RigInfo }): void {
19
+ const A15 = 'A15_IDLE_NO_MESH_BONE_KEYS';
20
+ /**
21
+ * Every mesh attachment, loaded, with the bones that drive it — its slot's
22
+ * bone, and, once weights exist, every bone its weights name. A ring mesh
23
+ * is driven by its CONTROL bone, a different bone from the slot's, and
24
+ * checking only the slot bone would let `idle` key the one bone that
25
+ * actually dirties the canvas every frame.
26
+ */
27
+ const meshDrivers: Array<{ mesh: MeshEntry; bones: Set<string> }> = [];
28
+ for (const mesh of facts.meshes) {
29
+ const bones = new Set<string>([mesh.slotBone]);
30
+ for (const bone of weightBonesOf(facts, mesh)) bones.add(bone);
31
+ meshDrivers.push({ mesh, bones });
32
+ }
33
+ const meshBoneNames = new Set<string>();
34
+ for (const { bones } of meshDrivers) for (const name of bones) meshBoneNames.add(name);
35
+ const declared = input.rig?.idleDrivesMeshes ?? null;
36
+ /**
37
+ * A declaration that switches off nothing is refused rather than skipped,
38
+ * the standard `consumerDrivenMix` is held to: an opt-out that exempts
39
+ * nothing reads exactly like one that worked, and the next reader cannot
40
+ * tell the rig that needs it from the rig it was copied onto.
41
+ */
42
+ const stale = (why: string): void => {
43
+ fail(
44
+ A15,
45
+ `the rig "${input.rig?.archetype}" declares invariants.idleDrivesMeshes ("${declared}"), but ${why}, so the ` +
46
+ 'declaration switches off nothing — remove it',
47
+ );
48
+ };
49
+ // The same shape as A06's and A17's guards, found by auditing for it
50
+ // (#568): a rig with no `idle` at all has nothing here to be wrong, and a
51
+ // rule that reports "held" over a subject that does not exist is the
52
+ // vacuous pass this file's own doctrine refuses. The two states get their
53
+ // own sentences because they are different absences — no such animation,
54
+ // versus one that keys no bone.
55
+ const idleBones = animated.bonesKeyedBy('idle');
56
+ if (idleBones === undefined) {
57
+ if (declared !== null) return stale('the skeleton declares no "idle" animation');
58
+ return skip(A15, 'the skeleton declares no "idle" animation, so nothing here can key a mesh-driving bone');
59
+ }
60
+ if (idleBones === null) {
61
+ if (declared !== null) return stale('"idle" carries no bone timeline at all');
62
+ return skip(A15, '"idle" carries no bone timeline at all, so there is no key to hold against the mesh-driving bones');
63
+ }
64
+ const keyed = idleBones.filter((name) => meshBoneNames.has(name));
65
+ if (declared !== null) {
66
+ if (keyed.length === 0) return stale('"idle" keys no bone that drives a mesh');
67
+ const keyedSet = new Set(keyed);
68
+ const moved = meshDrivers.filter(({ bones }) => [...bones].some((name) => keyedSet.has(name)));
69
+ // `worldVerticesLength` is two numbers per vertex whatever the encoding,
70
+ // which is the count the runtime recomputes; reading `vertices.length`
71
+ // would count a weighted mesh's bone entries instead.
72
+ const vertices = moved.reduce((sum, { mesh }) => sum + mesh.worldVerticesLength / 2, 0);
73
+ const SHOWN = 8;
74
+ const names =
75
+ keyed.slice(0, SHOWN).map((name) => `"${name}"`).join(', ') + (keyed.length > SHOWN ? ` +${keyed.length - SHOWN}` : '');
76
+ return skip(
77
+ A15,
78
+ `declared by the rig ("${declared}"): idle keys ${keyed.length} bone(s) that drive ${moved.length} mesh ` +
79
+ `attachment(s) totalling ${vertices} vertices — ${names} — and each of those meshes is recomputed on every ` +
80
+ 'frame it is shown',
81
+ );
82
+ }
83
+ for (const [i, boneName] of keyed.entries()) {
84
+ // The case is named once, on the first line an agent reads, rather than
85
+ // after every bone: the first painting rig this met printed 42 of these,
86
+ // and 42 identical sentences read as 42 separate mistakes. It rides the
87
+ // first finding rather than being a finding of its own, so the count of
88
+ // FAIL lines is still the count of bones.
89
+ const hint =
90
+ i > 0
91
+ ? ''
92
+ : `. ${keyed.length} bone(s) keyed by idle drive meshes; if this idle is meant to deform them (a painting ` +
93
+ 'rig), declare invariants.idleDrivesMeshes: { "why": … } in the rig spec — or, where the motion belongs to ' +
94
+ 'a pivot above the mesh, key that pivot one link up (FACE.md §3)';
95
+ fail(A15, `idle keys bone "${boneName}", which drives a mesh — meshes never idle-skip${hint}`);
96
+ }
97
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A16, restated over the emitted text (issue #1060): the clause
3
+ * `validate()` runs in `src/validate.ts`, word for word, over the skeleton JSON — the
4
+ * text rigc wrote, read by rigc's own reader rather than by spine-core's. The
5
+ * round trip's copy is unchanged and runs in `cli.ts build`; this one runs in
6
+ * the gate of the entry that links none of the runtime
7
+ * (`../emitted/index.ts`), and the selftest's `RC28` holds the two to the
8
+ * same lines on every recipe and on this assertion's mutants.
9
+ */
10
+ import type { Verdicts } from '../harness.ts';
11
+ import { spineGeneration, type SpineGeneration } from '../../generation.ts';
12
+ import { isObj, type Json } from '../values.ts';
13
+
14
+ const SPINE_4_3: SpineGeneration = '4.3';
15
+
16
+ export function a16SkeletonVersion43({ fail }: Verdicts, raw: Json | null): void {
17
+ const declared = isObj(raw?.skeleton) ? (raw.skeleton as Json).spine : undefined;
18
+ if (typeof declared !== 'string' || spineGeneration(declared) !== SPINE_4_3) {
19
+ fail(
20
+ 'A16_SKELETON_VERSION_4_3',
21
+ `skeleton.spine is ${JSON.stringify(declared)}, expected 4.3, 4.3.<patch> or 4.3.<patch>-<suffix>`,
22
+ );
23
+ }
24
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * A17, the body (issue #1025, step 4c of #380): every page the atlas names is
3
+ * a file on disk, resolved against the directory the atlas sits in.
4
+ *
5
+ * Moved out of `src/validate.ts` unchanged but for what it reads: the pages are
6
+ * a fact (`../facts/atlas_pages.ts`), and the directory is the caller's. Why
7
+ * the guard is a SKIP and never a bare `return` (issues #568, #608) is argued
8
+ * above the `check` call in `validate()`, which stays there with A06 and A19.
9
+ */
10
+ import { existsSync } from 'node:fs';
11
+ import { resolve } from 'node:path';
12
+ import type { Verdicts } from '../harness.ts';
13
+ import type { AtlasPageFacts } from '../facts/atlas_pages.ts';
14
+ import { SKIP_NO_ATLAS, SKIP_NO_ATLAS_PAGE } from '../reasons.ts';
15
+
16
+ export function a17AtlasPageFilesExist({ fail, skip }: Verdicts, { atlas }: AtlasPageFacts, input: { atlasDir: string }): void {
17
+ if (!atlas) return skip('A17_ATLAS_PAGE_FILES_EXIST', SKIP_NO_ATLAS);
18
+ // The other absence, one ring in (#580): the atlas LOADED and declares no
19
+ // page, so there is no file to find missing. `!atlas` and "zero pages" print
20
+ // different reasons because they are different facts about the artifact.
21
+ if (atlas.pages.length === 0) return skip('A17_ATLAS_PAGE_FILES_EXIST', SKIP_NO_ATLAS_PAGE);
22
+ for (const page of atlas.pages) {
23
+ const abs = resolve(input.atlasDir, page.name);
24
+ if (!existsSync(abs)) fail('A17_ATLAS_PAGE_FILES_EXIST', `page "${page.name}" is not on disk at ${abs}`);
25
+ }
26
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * A18, the body (issue #1060, step 4e of #380): a second, independent compile
3
+ * writes the same model document, byte for byte.
4
+ *
5
+ * One body, two suppliers, as every moved assertion is — but split. The
6
+ * runtime's side (`validate()` in `src/validate.ts`) compares the second
7
+ * compile's Spine pair as well, `skeleton.json` and `skeleton.atlas`, and
8
+ * hands those findings over as `encoding`, in the order it always printed
9
+ * them; the model side (`../model/index.ts`) compares the document alone,
10
+ * which is all a build with no Spine pair writes (`cli_core.ts build`). So
11
+ * the model's clause is written once, here, and the two sides cannot spell it
12
+ * apart. When neither side was handed a second compile — a re-gate of files on
13
+ * disk — there is nothing determinate about a comparison that never ran, and
14
+ * it SKIPs with the reason the round trip always gave (the vacuous-pass trap
15
+ * `A09` is guarded against too).
16
+ *
17
+ * Moved out of `src/validate.ts` unchanged but for the seam; `firstDifferingLine`
18
+ * moved with it, the one reader of it.
19
+ */
20
+ import type { Verdicts } from '../harness.ts';
21
+
22
+ const A18 = 'A18_DETERMINISTIC_EMIT';
23
+
24
+ /** What A18 reads: whether a second compile was handed over, the encoding's findings, and the two documents. */
25
+ export interface DeterminismFacts {
26
+ /** Whether a second, independent compile was handed over — without one the assertion has nothing to compare. */
27
+ again: boolean;
28
+ /** The findings of the clauses only one side compares (the Spine pair, on the runtime's), each a detail, in the order they print. */
29
+ encoding: readonly string[];
30
+ /** The first compile's model document — `undefined` where the caller handed a second compile and no first document. */
31
+ first: string | undefined;
32
+ /** The second compile's model document; read only where `again`. */
33
+ second: string;
34
+ }
35
+
36
+ /**
37
+ * Where two texts first differ, as A18 names it: the 1-based line and each
38
+ * side's line, trimmed and cut to 120 characters.
39
+ */
40
+ export function firstDifferingLine(first: string, second: string): string {
41
+ const a = first.split('\n');
42
+ const b = second.split('\n');
43
+ let i = 0;
44
+ while (i < a.length && i < b.length && a[i] === b[i]) i += 1;
45
+ const cut = (line: string | undefined): string => (line === undefined ? '(end of text)' : JSON.stringify(line.trim().slice(0, 120)));
46
+ return `${i + 1}: ${cut(a[i])} first, ${cut(b[i])} second`;
47
+ }
48
+
49
+ export function a18DeterministicEmit({ fail, skip }: Verdicts, facts: DeterminismFacts): void {
50
+ if (!facts.again) return skip(A18, 'no second compile to compare against (re-gating artifacts on disk)');
51
+ for (const detail of facts.encoding) fail(A18, detail);
52
+ // The model document is compared like the Spine pair (issue #922): it is
53
+ // written beside them, and a map iterated in an order nothing fixed would
54
+ // reach it before it reached either of them — the document writes every
55
+ // `Map` of the model as an array in the map's order. On a build with no
56
+ // Spine pair it is the only artifact there is to compare.
57
+ if (facts.first === undefined) {
58
+ fail(A18, 'a second compile was handed over with no first model document to compare its skeleton.model.json against');
59
+ } else if (facts.second !== facts.first) {
60
+ fail(A18, `recompiling produced a different skeleton.model.json (first differing line ${firstDifferingLine(facts.first, facts.second)})`);
61
+ }
62
+ }