@patterkit/ops 0.10.4 → 0.11.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/dist/index.d.ts CHANGED
@@ -375,6 +375,10 @@ interface CoverageBeat {
375
375
  reachedRuns: number;
376
376
  /** reachedRuns / runs-executed * 100; 0 iff never reached. */
377
377
  reachPct: number;
378
+ /** Reached, but in fewer than {@link RARE_REACH_PCT}% of runs: content that CAN play but hangs on an
379
+ * unlikely route or a condition that is nearly always false. Worth a look, not necessarily a fault
380
+ * (one of several random picks is rare by design). Absent on a never-reached beat. */
381
+ rare?: true;
378
382
  /** Set on a never-reached beat that is gated on a host-scope ref (`@world.x`) nothing writes and no
379
383
  * driver provides, i.e. it may just need an input, not be truly dead. Lists the offending refs. */
380
384
  needsInput?: string[];
@@ -414,12 +418,16 @@ interface CoverageReport {
414
418
  block?: string;
415
419
  };
416
420
  beats: CoverageBeat[];
421
+ /** `rare` counts beats reached in fewer than `rareThresholdPct`% of runs (and not never). */
417
422
  totals: {
418
423
  beats: number;
419
424
  covered: number;
420
425
  neverHit: number;
426
+ rare: number;
421
427
  coveragePct: number;
422
428
  };
429
+ /** The reach % below which a reached beat counts as rare ({@link RARE_REACH_PCT}). */
430
+ rareThresholdPct: number;
423
431
  /** How each run ended, for the summary header. */
424
432
  termination: {
425
433
  ended: number;
@@ -437,6 +445,15 @@ interface CoverageReport {
437
445
  dryChoices: DryChoice[];
438
446
  cancelled: boolean;
439
447
  }
448
+ /** Below this share of runs, a beat that WAS reached is flagged rare. Coverage used to flag only the never
449
+ * reached, so a beat that played in 0.5% of runs looked as healthy as one that played in all of them. */
450
+ declare const RARE_REACH_PCT = 5;
451
+ /**
452
+ * The beats least reached first: never-reached at the top, then by reach %, then by times played, with
453
+ * script order kept among equals. The order both Patterpad's Coverage window and `patter coverage` lead
454
+ * with, because the beats a sweep reaches least are the ones worth a look.
455
+ */
456
+ declare function leastReachedFirst(beats: readonly CoverageBeat[]): CoverageBeat[];
440
457
  /**
441
458
  * Auto-propose coverage drivers by scanning the project's conditions for host-scope refs (`@world.x`)
442
459
  * and the literals they are compared against. Each proposed driver is `recurring`/`sometimes` and offers
@@ -466,9 +483,14 @@ interface CoverageAsyncHooks extends CoverageHooks {
466
483
  * it for a full sample.
467
484
  */
468
485
  declare function runCoverageAsync(loaded: LoadedProject, options?: CoverageOptions, hooks?: CoverageAsyncHooks): Promise<CoverageReport>;
469
- /** Render a coverage report as the CLI's readable text: a summary, then a per-scene beat table with
470
- * never-reached (0%) rows marked. */
471
- declare function renderCoverageText(report: CoverageReport, sceneName?: (id: string) => string): string[];
486
+ /** Which order the beat table is in: least reached first (the default: the rows worth a look lead), or
487
+ * the script's own order, scene by scene. */
488
+ type CoverageOrder = "least" | "script";
489
+ /** Render a coverage report as the CLI's readable text: a summary, then the beat table with never-reached
490
+ * and rarely reached rows marked, least reached first unless `order` says script order. */
491
+ declare function renderCoverageText(report: CoverageReport, sceneName?: (id: string) => string, opts?: {
492
+ order?: CoverageOrder;
493
+ }): string[];
472
494
 
473
495
  /** Resolve where a flow starts: an explicit override, else the project's authored `start`, else `{}`
474
496
  * (the runtime's first-scene default). Shared by `runPlay` and `runCoverage`. */
@@ -1114,4 +1136,4 @@ declare function classesForChannel(classes: DocumentationClass[], channel: strin
1114
1136
  */
1115
1137
  declare function resolveDocumentation(loaded: LoadedProject, channel: string): Map<string, DocLine[]>;
1116
1138
 
1117
- export { AUTHORING_HANDLED, type ApplyStats, type BundlePosture, CONFLICT_SIDECAR, type CharacterReport, type Conflict, type ConflictKind, type CoverageAsyncHooks, type CoverageBeat, type CoverageHooks, type CoverageOptions, type CoverageReport, type DocumentManifest, type FormatResult, GAME_SCOPES_OWNER, type GameScopes, type GameScopesIssue, type HygieneIssue, type InitOptions, type InitResult, type InitVcs, type LoadedProject, type LocCatalog, type LocEntry, type LocaleReport, type MergeFileType, type MergeResult, type MergedShard, PATTER_SCOPES_FILE, PATTER_SCOPES_OWNER, type PinPlan, type PinnedName, type PlannedWrite, type PlayEvent, type PlayOptions, type PlayOutcome, type PlayResult, type ProvenanceCheck, type ReplaceHit, type ReplaceOptions, type ReplacePlan, type ReportData, type ReportTotals, type ResolveEntry, SHARD_EXTENSIONS, type SceneReport, type ScriptDoc, type ScriptElement, type SearchFocus, type UnpackMergeResult, type UnpackResult, UnsafeEntryError, UnsupportedMergeError, type ValidateResult, type VoiceLine, type VoiceScript, type VoicedCounts, type WebExport, type WrittenCounts, applyLoc, applySceneOrder, applyWrites, bundleOutputPath, catalogToJson, catalogToPo, catalogToXlsx, classesForChannel, defaultGameScopesDir, detectMergeType, discoverGameScopes, drivableScopes, extractLoc, findProjectFile, gameScopeTokens, gameScopesCatalogue, gameScopesSnapshot, hostScopeTokens, jsonToCatalog, listProjectTags, loadProject, loadProjectLanding, patterScopesFile, patterScopesStale, patterScopesWrite, planPins, planReturnedWorld, planShareScopes, planWorldSave, poToCatalog, previewRegistry, proposeCoverageDrivers, reachabilityIssues, readGameScopes, renderCoverageText, renderPlay, renderReportText, resolveDocumentation, resolveStart, runCoverage, runCoverageAsync, runExport, runExportFull, runExportHtml, runExportWeb, runFormat, runInit, runMerge, runPack, runPlay, runPropertyUsage, runReplace, runReport, runReportXlsx, runResolve, runScriptDoc, runSearch, runStatusBrowse, runTagBrowse, runUnpack, runUnpackMerge, runValidate, runVoiceScript, scanAudioStatus, sceneIdForShard, scriptToDocx, scriptToPdf, sidecarIssues, vcsConfigWrites, voiceScriptToXlsx, worldSettingsScopes, xlsxToCatalog };
1139
+ export { AUTHORING_HANDLED, type ApplyStats, type BundlePosture, CONFLICT_SIDECAR, type CharacterReport, type Conflict, type ConflictKind, type CoverageAsyncHooks, type CoverageBeat, type CoverageHooks, type CoverageOptions, type CoverageOrder, type CoverageReport, type DocumentManifest, type FormatResult, GAME_SCOPES_OWNER, type GameScopes, type GameScopesIssue, type HygieneIssue, type InitOptions, type InitResult, type InitVcs, type LoadedProject, type LocCatalog, type LocEntry, type LocaleReport, type MergeFileType, type MergeResult, type MergedShard, PATTER_SCOPES_FILE, PATTER_SCOPES_OWNER, type PinPlan, type PinnedName, type PlannedWrite, type PlayEvent, type PlayOptions, type PlayOutcome, type PlayResult, type ProvenanceCheck, RARE_REACH_PCT, type ReplaceHit, type ReplaceOptions, type ReplacePlan, type ReportData, type ReportTotals, type ResolveEntry, SHARD_EXTENSIONS, type SceneReport, type ScriptDoc, type ScriptElement, type SearchFocus, type UnpackMergeResult, type UnpackResult, UnsafeEntryError, UnsupportedMergeError, type ValidateResult, type VoiceLine, type VoiceScript, type VoicedCounts, type WebExport, type WrittenCounts, applyLoc, applySceneOrder, applyWrites, bundleOutputPath, catalogToJson, catalogToPo, catalogToXlsx, classesForChannel, defaultGameScopesDir, detectMergeType, discoverGameScopes, drivableScopes, extractLoc, findProjectFile, gameScopeTokens, gameScopesCatalogue, gameScopesSnapshot, hostScopeTokens, jsonToCatalog, leastReachedFirst, listProjectTags, loadProject, loadProjectLanding, patterScopesFile, patterScopesStale, patterScopesWrite, planPins, planReturnedWorld, planShareScopes, planWorldSave, poToCatalog, previewRegistry, proposeCoverageDrivers, reachabilityIssues, readGameScopes, renderCoverageText, renderPlay, renderReportText, resolveDocumentation, resolveStart, runCoverage, runCoverageAsync, runExport, runExportFull, runExportHtml, runExportWeb, runFormat, runInit, runMerge, runPack, runPlay, runPropertyUsage, runReplace, runReport, runReportXlsx, runResolve, runScriptDoc, runSearch, runStatusBrowse, runTagBrowse, runUnpack, runUnpackMerge, runValidate, runVoiceScript, scanAudioStatus, sceneIdForShard, scriptToDocx, scriptToPdf, sidecarIssues, vcsConfigWrites, voiceScriptToXlsx, worldSettingsScopes, xlsxToCatalog };
package/dist/index.js CHANGED
@@ -1591,6 +1591,10 @@ function mulberry32(seed) {
1591
1591
  const prng = makePrng(seed);
1592
1592
  return () => prng.next();
1593
1593
  }
1594
+ var RARE_REACH_PCT = 5;
1595
+ function leastReachedFirst(beats) {
1596
+ return beats.map((b, i) => ({ b, i })).sort((x, y) => x.b.reachPct - y.b.reachPct || x.b.hits - y.b.hits || x.i - y.i).map(({ b }) => b);
1597
+ }
1594
1598
  function scanExpr(node, hostTokens, refs, proposals) {
1595
1599
  const refOf = (n) => n.kind === "scopedvar" && hostTokens.has(n.scope) ? `@${n.scope}.${n.name}` : void 0;
1596
1600
  const propose = (ref, v) => (proposals.get(ref) ?? proposals.set(ref, /* @__PURE__ */ new Set()).get(ref)).add(v);
@@ -1880,6 +1884,7 @@ function* sweep(loaded, options = {}, hooks = {}) {
1880
1884
  const blocked = blockedGates(id, analysis, drivenRefs, reachedRuns);
1881
1885
  if (blocked.length) blockedBy = blocked;
1882
1886
  }
1887
+ const reachPct = executed ? reached / executed * 100 : 0;
1883
1888
  return {
1884
1889
  id,
1885
1890
  scene: m.scene,
@@ -1888,13 +1893,15 @@ function* sweep(loaded, options = {}, hooks = {}) {
1888
1893
  preview: m.preview,
1889
1894
  hits: hitCount.get(id),
1890
1895
  reachedRuns: reached,
1891
- reachPct: executed ? reached / executed * 100 : 0,
1896
+ reachPct,
1897
+ ...reached > 0 && reachPct < RARE_REACH_PCT ? { rare: true } : {},
1892
1898
  ...needsInput ? { needsInput } : {},
1893
1899
  ...blockedBy ? { blockedBy } : {}
1894
1900
  };
1895
1901
  });
1896
1902
  const neverHit = beats.filter((b) => b.reachedRuns === 0).length;
1897
1903
  const covered = beats.length - neverHit;
1904
+ const rare = beats.filter((b) => b.rare).length;
1898
1905
  const dryChoices = [...dryRuns.entries()].map(([id, r]) => ({ id, scene: choiceScene.get(id) ?? "", runs: r })).sort((a, b) => b.runs - a.runs || a.id.localeCompare(b.id));
1899
1906
  return {
1900
1907
  runs: executed,
@@ -1902,7 +1909,8 @@ function* sweep(loaded, options = {}, hooks = {}) {
1902
1909
  seed,
1903
1910
  start,
1904
1911
  beats,
1905
- totals: { beats: beats.length, covered, neverHit, coveragePct: beats.length ? covered / beats.length * 100 : 100 },
1912
+ totals: { beats: beats.length, covered, neverHit, rare, coveragePct: beats.length ? covered / beats.length * 100 : 100 },
1913
+ rareThresholdPct: RARE_REACH_PCT,
1906
1914
  termination,
1907
1915
  drivers,
1908
1916
  unwrittenInputs: [...unwrittenInputs].sort(),
@@ -1926,14 +1934,19 @@ async function runCoverageAsync(loaded, options = {}, hooks = {}) {
1926
1934
  }
1927
1935
  return step.value;
1928
1936
  }
1929
- function renderCoverageText(report, sceneName = (id) => id) {
1937
+ function renderCoverageText(report, sceneName = (id) => id, opts = {}) {
1938
+ const order = opts.order ?? "least";
1930
1939
  const out = [];
1931
1940
  const t = report.totals;
1932
1941
  const pct = (n) => `${n.toFixed(0)}%`;
1933
- out.push(`coverage: ${t.covered}/${t.beats} beats reached (${pct(t.coveragePct)})${t.neverHit ? ` - ${t.neverHit} never reached` : ""}`);
1942
+ const rareLimit = report.rareThresholdPct ?? RARE_REACH_PCT;
1943
+ const rareCount = t.rare ?? report.beats.filter((b) => b.rare).length;
1944
+ out.push(
1945
+ `coverage: ${t.covered}/${t.beats} beats reached (${pct(t.coveragePct)})` + (t.neverHit ? ` - ${t.neverHit} never reached` : "") + (rareCount ? ` - ${rareCount} rarely reached (under ${rareLimit}% of runs)` : "")
1946
+ );
1934
1947
  out.push(`${report.runs} run(s) - ${report.maxSteps} max steps - seed ${report.seed}${report.cancelled ? " - CANCELLED" : ""}`);
1935
1948
  const term = report.termination;
1936
- out.push(`runs ended: ${term.ended} reached the end, ${term.stalled} stalled, ${term.capped} hit the step cap, ${term.evalError} errored`);
1949
+ out.push(`runs ended: ${term.ended} reached the end, ${term.stalled} stalled at a choice with nothing to pick, ${term.capped} hit the step limit, ${term.evalError} errored`);
1937
1950
  if (report.drivers.length) out.push(`input drivers: ${report.drivers.map((d) => d.ref).join(", ")}`);
1938
1951
  if (report.unwrittenInputs.length) {
1939
1952
  out.push(`? = gated on an input nothing writes/drives: ${report.unwrittenInputs.join(", ")} (add a coverage driver?)`);
@@ -1945,25 +1958,43 @@ function renderCoverageText(report, sceneName = (id) => id) {
1945
1958
  out.push(` \u203C ${String(d.runs).padStart(6)} run(s) ${sceneName(d.scene)} choice '${d.id}'`);
1946
1959
  }
1947
1960
  }
1961
+ if (!report.beats.length) return out;
1962
+ const clip = (s, n = 48) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
1963
+ const scenes = new Set(report.beats.map((b) => b.scene));
1964
+ const heading = " reached played beat";
1965
+ const row = (b, withScene) => {
1966
+ const mark = b.reachedRuns === 0 ? b.needsInput || b.blockedBy ? "? " : "\u203C " : b.rare ? "~ " : " ";
1967
+ const label = b.character ? `${b.character}: ${clip(b.preview)}` : clip(b.preview || `(${b.kind})`);
1968
+ const where = withScene ? `[${sceneName(b.scene)}] ` : "";
1969
+ out.push(` ${mark}${pct(b.reachPct).padStart(6)} ${String(b.hits).padStart(6)} ${where}${label}`);
1970
+ for (const bg of b.blockedBy ?? []) {
1971
+ const names = bg.writers.map((w) => {
1972
+ const target = report.beats.find((x) => x.id === w);
1973
+ return target ? clip(target.preview || target.id, 28) : w;
1974
+ });
1975
+ out.push(` gated on ${bg.ref}, written only by: ${names.join(", ")} (never played either)`);
1976
+ }
1977
+ };
1978
+ out.push("");
1979
+ out.push(`reached = share of runs that played the beat at least once; played = times it played in all runs`);
1980
+ out.push(`\u203C never reached ? never reached, may need an input ~ rarely reached (under ${rareLimit}% of runs)`);
1981
+ if (order === "least") {
1982
+ out.push("");
1983
+ out.push(`least reached first${scenes.size > 1 ? ", every scene" : ""}`);
1984
+ out.push(heading);
1985
+ for (const b of leastReachedFirst(report.beats)) row(b, scenes.size > 1);
1986
+ return out;
1987
+ }
1948
1988
  const byScene = /* @__PURE__ */ new Map();
1949
1989
  for (const b of report.beats) (byScene.get(b.scene) ?? byScene.set(b.scene, []).get(b.scene)).push(b);
1950
- const clip = (s, n = 48) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
1951
1990
  for (const [scene, beats] of byScene) {
1952
1991
  const dead = beats.filter((b) => b.reachedRuns === 0).length;
1992
+ const rare = beats.filter((b) => b.rare).length;
1993
+ const notes = [dead ? `${dead} never reached` : "", rare ? `${rare} rarely reached` : ""].filter(Boolean).join(", ");
1953
1994
  out.push("");
1954
- out.push(`${sceneName(scene)}${dead ? ` (${dead} never reached)` : ""}`);
1955
- for (const b of beats) {
1956
- const mark = b.reachedRuns === 0 ? b.needsInput || b.blockedBy ? "? " : "\u203C " : " ";
1957
- const label = b.character ? `${b.character}: ${clip(b.preview)}` : clip(b.preview || `(${b.kind})`);
1958
- out.push(` ${mark}${b.reachPct.toFixed(0).padStart(3)}% ${String(b.hits).padStart(6)} ${label}`);
1959
- for (const bg of b.blockedBy ?? []) {
1960
- const names = bg.writers.map((w) => {
1961
- const target = report.beats.find((x) => x.id === w);
1962
- return target ? clip(target.preview || target.id, 28) : w;
1963
- });
1964
- out.push(` gated on ${bg.ref}, written only by: ${names.join(", ")} (never played either)`);
1965
- }
1966
- }
1995
+ out.push(`${sceneName(scene)}${notes ? ` (${notes})` : ""}`);
1996
+ out.push(heading);
1997
+ for (const b of beats) row(b, false);
1967
1998
  }
1968
1999
  return out;
1969
2000
  }
@@ -4076,6 +4107,7 @@ export {
4076
4107
  GAME_SCOPES_OWNER,
4077
4108
  PATTER_SCOPES_FILE,
4078
4109
  PATTER_SCOPES_OWNER,
4110
+ RARE_REACH_PCT,
4079
4111
  SHARD_EXTENSIONS,
4080
4112
  UnsafeEntryError,
4081
4113
  UnsupportedMergeError,
@@ -4099,6 +4131,7 @@ export {
4099
4131
  hostScopeTokens,
4100
4132
  isUnsafeEntry2 as isUnsafeEntry,
4101
4133
  jsonToCatalog,
4134
+ leastReachedFirst,
4102
4135
  listProjectTags,
4103
4136
  loadProject,
4104
4137
  loadProjectLanding,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patterkit/ops",
3
- "version": "0.10.4",
3
+ "version": "0.11.0",
4
4
  "description": "The shared operations layer: every project operation (load, validate, export, format, play, ...) as a pure function, consumed by the patter CLI and Patterpad alike.",
5
5
  "type": "module",
6
6
  "license": "MIT",