@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 +26 -4
- package/dist/index.js +52 -19
- package/package.json +1 -1
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
|
-
/**
|
|
470
|
-
*
|
|
471
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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)}${
|
|
1955
|
-
|
|
1956
|
-
|
|
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.
|
|
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",
|