spine-rigc 1.9.4 → 2.0.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.
@@ -3,7 +3,9 @@
3
3
  * `ingest`, `diff`, `check`, `render`, `pose`, `chainfit` and `skills` —
4
4
  * moved here unchanged from `cli.ts` (issue #1052, step 4e of #380), with the
5
5
  * helpers only they call. Both entries register them (`CORE_COMMAND_RUNS`):
6
- * `cli.ts` beside the runtime's, `cli_core.ts` alone.
6
+ * `cli.ts` beside the runtime's, `cli_core.ts` alone. And the body
7
+ * `cli_core.ts` runs as `build` (`CORE_ENTRY_RUNS`, issue #1060): `runBuild`
8
+ * with a gate that links none of the runtime.
7
9
  *
8
10
  * Which commands these are is not a list kept here: it is `COMMANDS`'s
9
11
  * `runtime` field (`./shared.ts`), which `RC26` in `selftest.ts` holds to the
@@ -12,6 +14,9 @@
12
14
  * absent, which `RC24` runs.
13
15
  */
14
16
  import { parseAtlasText } from '../atlas.ts';
17
+ import { validateEmittedText } from '../assertions/emitted/index.ts';
18
+ import { validateModel } from '../assertions/model/index.ts';
19
+ import { reportLines } from '../assertions/report.ts';
15
20
  import { chainFitLines, type ChainFitOptions, estimateChainFit } from '../chainfit.ts';
16
21
  import { checkLines, CheckPlates } from '../check.ts';
17
22
  import { writeCheckPictures } from '../checkpics.ts';
@@ -29,7 +34,7 @@ import { estimatePose, poseLines, type PoseOptions } from '../pose.ts';
29
34
  import { BACKGROUND, contactSheet, type Frame, FRAMES_SIDECAR, FRAMES_SPEC, type FrameSet, type FramesSidecar, framingViewport, GEOMETRY_FILE, geometryFileOf, geometryText, loadCandidate, POSER_NAMES, type PoserName, PROTOCOL_FPS, refuseUnchosen, renderFrame, sampleAll, sampleAnimation, SETUP_POSE_DIR, SHEET_FILE, SHEET_TILE, sidecarViewport, type SkeletonFacts, type SlotSubset, SlotSubsetError, throughPoser } from '../render_shared.ts';
30
35
  import { type CompileResult } from '../types.ts';
31
36
  import { attachmentRegionJoins } from '../region_joins.ts';
32
- import { ATLAS_ABSENT, COMMANDS, type CommandRun, PACKAGE_ROOT, DEFAULT_CHAINFIT_OUT, DEFAULT_POSE_OUT, DEFAULT_SKILLS_DIR, ExplainError, meshBudget, meshDepthNote, meshFit, meshInfluenceNote, PAGE_GRID_UNLOCATED, readAnimationFlag, readJsonFile, readPoserFlag, readSkeletonText, readVersion, resolveCut, resolveDrawable, runCheck, SkillsInstallError, STAGELESS_FRAMING, UsageError, writeJson } from './shared.ts';
37
+ import { ATLAS_ABSENT, COMMANDS, type CommandRun, resolveBuild, type BuildGate, runBuild, PACKAGE_ROOT, DEFAULT_CHAINFIT_OUT, DEFAULT_POSE_OUT, DEFAULT_SKILLS_DIR, ExplainError, meshBudget, meshDepthNote, meshFit, meshInfluenceNote, PAGE_GRID_UNLOCATED, readAnimationFlag, readJsonFile, readPoserFlag, readSkeletonText, readVersion, resolveCut, resolveDrawable, runCheck, SkillsInstallError, STAGELESS_FRAMING, UsageError, writeJson } from './shared.ts';
33
38
  import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
34
39
  import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
35
40
 
@@ -321,11 +326,17 @@ function readAnimationPairs(values: string[], candidate: unknown, reference: unk
321
326
  export function cmdDiff(flags: Record<string, string>, lists: Record<string, string[]>, positional: string[]): void {
322
327
  const [candidate, reference] = positional;
323
328
  if (!candidate || !reference) throw new UsageError('diff takes two paths: <candidate.json> <reference.json>');
324
- const candidatePath = resolve(candidate);
325
- const referencePath = resolve(reference);
326
- for (const path of [candidatePath, referencePath]) {
327
- if (!existsSync(path)) throw new UsageError(`nothing at ${path}`);
328
- }
329
+ // A file is read as the JSON it is; a directory is a build, and its skeleton is what is compared — through the one
330
+ // statement of a build (issue #1046), which refuses a directory holding none. A directory used to reach the JSON
331
+ // reader and die on an EISDIR and a stack.
332
+ const skeletonAt = (path: string): string => {
333
+ const at = resolve(path);
334
+ if (existsSync(at) && statSync(at).isDirectory()) return resolveBuild(at, undefined).skeletonPath;
335
+ if (!existsSync(at)) throw new UsageError(`nothing at ${at}`);
336
+ return at;
337
+ };
338
+ const candidatePath = skeletonAt(candidate);
339
+ const referencePath = skeletonAt(reference);
329
340
  const candidateJson = readJsonFile(candidatePath);
330
341
  const referenceJson = readJsonFile(referencePath);
331
342
  const animationPairs = readAnimationPairs(lists.as ?? [], candidateJson, referenceJson);
@@ -696,6 +707,9 @@ export function cmdChainFit(flags: Record<string, string>): void {
696
707
  'skeleton is all it needs of the candidate. Drop --atlas',
697
708
  );
698
709
  }
710
+ // The candidate through the one statement of a build (issue #1046): a path, or a directory holding no skeleton.json,
711
+ // is refused here as by every command that takes a build, rather than by `src/chainfit.ts`'s own reader in its own words.
712
+ resolveBuild(flags.candidate, undefined);
699
713
  const options: ChainFitOptions = {
700
714
  candidatePath: flags.candidate,
701
715
  imagesDir: flags.images,
@@ -1515,6 +1529,67 @@ export function cmdSkills(flags: Record<string, string>, positional: string[]):
1515
1529
  );
1516
1530
  }
1517
1531
 
1532
+ // ---------------------------------------------------------------------------
1533
+ // build — the second entry's gate (issue #1060, step 4e of #380)
1534
+ // ---------------------------------------------------------------------------
1535
+
1536
+ /**
1537
+ * The gate `cli_core.ts build` runs (`BuildGate`), where `cli.ts build` runs
1538
+ * the round trip: the model side over the document (`validateModel`), and the
1539
+ * round trip's own rules restated over the text the emitter wrote
1540
+ * (`validateEmittedText`) — A18 among them, comparing a second, independent
1541
+ * compile's skeleton, atlas and document byte for byte — with A00, spine-core's
1542
+ * parse, a SKIP naming it. One report, printed by the printer `cli.ts build`
1543
+ * prints with, and a line saying which rules ran here and which did not.
1544
+ *
1545
+ * ⚠️ The document the model side reads is the one this gate judges: spelled
1546
+ * from the atlas text the gate is handed, so under `--copy-images` it names
1547
+ * the pages where the compile measured them — the files the round trip's own
1548
+ * A17 reads on `cli.ts` — and A18 compares the documents as written, the
1549
+ * copies' names in them. Under `--pack` the two are one text.
1550
+ */
1551
+ const MODEL_AND_TEXT_GATE: BuildGate = {
1552
+ heading: (profile) =>
1553
+ ` .. validate (the model side over the document + the round trip's rules restated over the emitted text, profile ${profile}; this entry links no spine-core, so the round trip does not run)`,
1554
+ run: ({ result, atlasText, atlasDir, modelText, reEmit, profile }) => {
1555
+ const model = validateModel({ modelText: modelDocument(result.model, result.skeletonText, atlasText), atlasDir, profile });
1556
+ const text = validateEmittedText({ skeletonText: result.skeletonText, atlasText, modelText, reEmit, profile });
1557
+ const report = {
1558
+ failures: [...model.failures, ...text.failures],
1559
+ passed: [...model.passed, ...text.passed],
1560
+ skipped: [...model.skipped, ...text.skipped],
1561
+ profileSkipped: [...model.profileSkipped, ...text.profileSkipped],
1562
+ stats: { ...model.stats, ...text.stats },
1563
+ profile,
1564
+ };
1565
+ for (const line of reportLines(report)) console.log(line);
1566
+ console.log(
1567
+ ` .. ${Object.entries(report.stats)
1568
+ .map(([k, v]) => `${k}=${v}`)
1569
+ .join(' ')}`,
1570
+ );
1571
+ const ran = (r: { passed: string[]; failures: Array<{ assertion: string }>; skipped: Array<{ assertion: string }>; profileSkipped: Array<{ assertion: string }> }): number =>
1572
+ new Set([...r.passed, ...r.failures.map((f) => f.assertion), ...r.skipped.map((x) => x.assertion), ...r.profileSkipped.map((x) => x.assertion)]).size;
1573
+ const notRun = text.skipped.filter((x) => x.assertion === 'A00_ROUNDTRIP_PARSE').map((x) => x.assertion);
1574
+ console.log(
1575
+ ` .. here: ${ran(model)} rule(s) on the model side over the document, ${ran(text) - notRun.length} of the round trip's own restated over the emitted text; ` +
1576
+ `not run: ${notRun.join(', ') || 'none'} — spine-core's parse, which only \`bun cli.ts build\` and \`rigc validate\` run`,
1577
+ );
1578
+ return report.failures.length;
1579
+ },
1580
+ look: (outDir) => `rigc: look at it: rigc render --candidate ${outDir}`,
1581
+ };
1582
+
1583
+ /**
1584
+ * The bodies the second entry runs under a name whose full-entry body is the
1585
+ * runtime's (`runtime.core` in `COMMANDS`, issue #1060) — registered by
1586
+ * `cli_core.ts` alone, beside `CORE_COMMAND_RUNS`. `build` is `runBuild` with
1587
+ * this entry's gate: it writes what `cli.ts build` writes.
1588
+ */
1589
+ export const CORE_ENTRY_RUNS: Readonly<Record<string, CommandRun>> = {
1590
+ build: ({ flags }) => runBuild(flags, MODEL_AND_TEXT_GATE),
1591
+ };
1592
+
1518
1593
  /**
1519
1594
  * The bodies of every command whose `runtime` is `false` (`COMMANDS`), by
1520
1595
  * name — what both entries register. Nothing this module imports links