@patterkit/cli 0.5.4 → 0.6.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/README.md +28 -0
- package/dist/cli.js +60 -23
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -128,6 +128,34 @@ patter play --scene scn_tavern --choices opt_work,opt_secret
|
|
|
128
128
|
patter play --seed 42
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
+
### `patter coverage [path]`
|
|
132
|
+
|
|
133
|
+
Play the story many times with random choices and count how often each beat comes
|
|
134
|
+
up, to find content no player can reach. The summary counts beats reached, never
|
|
135
|
+
reached, and rarely reached (in fewer than 5% of runs), and says how the runs
|
|
136
|
+
ended: reached the end, stalled at a choice with nothing to pick, or hit the step
|
|
137
|
+
limit. The table gives each beat's **reached** (the share of runs that played it)
|
|
138
|
+
and **played** (times it played in all runs), least reached first. Marks: `‼`
|
|
139
|
+
never reached, `?` never reached but may just need an input driver, `~` rarely
|
|
140
|
+
reached. Choices that ran dry (nothing to take and no fallback) are listed too.
|
|
141
|
+
|
|
142
|
+
| Option | Values | Meaning |
|
|
143
|
+
|--------|--------|---------|
|
|
144
|
+
| `--runs <n>` | integer | How many playthroughs (default 5000). |
|
|
145
|
+
| `--max-steps <n>` | integer | Steps before a run is stopped (default 200). |
|
|
146
|
+
| `--seed <n>` | integer | The same seed gives the same result. |
|
|
147
|
+
| `--scene <id>` / `--block <id>` | id | Start here instead of the project's start. |
|
|
148
|
+
| `--order <o>` | `least` / `script` | Least reached first (default), or the script's order, scene by scene. |
|
|
149
|
+
| `--fail-on-gap` | | Exit 1 if any beat is never reached (a CI gate). |
|
|
150
|
+
| `--propose` | | Print suggested `@world` input drivers instead of running. |
|
|
151
|
+
| `--json` | | The full report as JSON. |
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
patter coverage
|
|
155
|
+
patter coverage --runs 10000 --seed 7 --order script
|
|
156
|
+
patter coverage --fail-on-gap
|
|
157
|
+
```
|
|
158
|
+
|
|
131
159
|
### `patter resolve <query> [path]`
|
|
132
160
|
|
|
133
161
|
Look up an **id**, **handle**, or **name** and report what it is and where it
|
package/dist/cli.js
CHANGED
|
@@ -198055,6 +198055,10 @@ function mulberry32(seed) {
|
|
|
198055
198055
|
const prng = makePrng(seed);
|
|
198056
198056
|
return () => prng.next();
|
|
198057
198057
|
}
|
|
198058
|
+
var RARE_REACH_PCT = 5;
|
|
198059
|
+
function leastReachedFirst(beats) {
|
|
198060
|
+
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);
|
|
198061
|
+
}
|
|
198058
198062
|
function scanExpr(node, hostTokens, refs, proposals) {
|
|
198059
198063
|
const refOf = (n) => n.kind === "scopedvar" && hostTokens.has(n.scope) ? `@${n.scope}.${n.name}` : void 0;
|
|
198060
198064
|
const propose = (ref, v) => (proposals.get(ref) ?? proposals.set(ref, /* @__PURE__ */ new Set()).get(ref)).add(v);
|
|
@@ -198344,6 +198348,7 @@ function* sweep(loaded, options = {}, hooks = {}) {
|
|
|
198344
198348
|
const blocked = blockedGates(id, analysis, drivenRefs, reachedRuns);
|
|
198345
198349
|
if (blocked.length) blockedBy = blocked;
|
|
198346
198350
|
}
|
|
198351
|
+
const reachPct = executed ? reached / executed * 100 : 0;
|
|
198347
198352
|
return {
|
|
198348
198353
|
id,
|
|
198349
198354
|
scene: m.scene,
|
|
@@ -198352,13 +198357,15 @@ function* sweep(loaded, options = {}, hooks = {}) {
|
|
|
198352
198357
|
preview: m.preview,
|
|
198353
198358
|
hits: hitCount.get(id),
|
|
198354
198359
|
reachedRuns: reached,
|
|
198355
|
-
reachPct
|
|
198360
|
+
reachPct,
|
|
198361
|
+
...reached > 0 && reachPct < RARE_REACH_PCT ? { rare: true } : {},
|
|
198356
198362
|
...needsInput ? { needsInput } : {},
|
|
198357
198363
|
...blockedBy ? { blockedBy } : {}
|
|
198358
198364
|
};
|
|
198359
198365
|
});
|
|
198360
198366
|
const neverHit = beats.filter((b) => b.reachedRuns === 0).length;
|
|
198361
198367
|
const covered = beats.length - neverHit;
|
|
198368
|
+
const rare = beats.filter((b) => b.rare).length;
|
|
198362
198369
|
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));
|
|
198363
198370
|
return {
|
|
198364
198371
|
runs: executed,
|
|
@@ -198366,7 +198373,8 @@ function* sweep(loaded, options = {}, hooks = {}) {
|
|
|
198366
198373
|
seed,
|
|
198367
198374
|
start,
|
|
198368
198375
|
beats,
|
|
198369
|
-
totals: { beats: beats.length, covered, neverHit, coveragePct: beats.length ? covered / beats.length * 100 : 100 },
|
|
198376
|
+
totals: { beats: beats.length, covered, neverHit, rare, coveragePct: beats.length ? covered / beats.length * 100 : 100 },
|
|
198377
|
+
rareThresholdPct: RARE_REACH_PCT,
|
|
198370
198378
|
termination,
|
|
198371
198379
|
drivers,
|
|
198372
198380
|
unwrittenInputs: [...unwrittenInputs].sort(),
|
|
@@ -198380,14 +198388,19 @@ function runCoverage(loaded, options = {}, hooks = {}) {
|
|
|
198380
198388
|
while (!step.done) step = it.next();
|
|
198381
198389
|
return step.value;
|
|
198382
198390
|
}
|
|
198383
|
-
function renderCoverageText(report, sceneName = (id) => id) {
|
|
198391
|
+
function renderCoverageText(report, sceneName = (id) => id, opts = {}) {
|
|
198392
|
+
const order = opts.order ?? "least";
|
|
198384
198393
|
const out = [];
|
|
198385
198394
|
const t = report.totals;
|
|
198386
198395
|
const pct = (n) => `${n.toFixed(0)}%`;
|
|
198387
|
-
|
|
198396
|
+
const rareLimit = report.rareThresholdPct ?? RARE_REACH_PCT;
|
|
198397
|
+
const rareCount = t.rare ?? report.beats.filter((b) => b.rare).length;
|
|
198398
|
+
out.push(
|
|
198399
|
+
`coverage: ${t.covered}/${t.beats} beats reached (${pct(t.coveragePct)})` + (t.neverHit ? ` - ${t.neverHit} never reached` : "") + (rareCount ? ` - ${rareCount} rarely reached (under ${rareLimit}% of runs)` : "")
|
|
198400
|
+
);
|
|
198388
198401
|
out.push(`${report.runs} run(s) - ${report.maxSteps} max steps - seed ${report.seed}${report.cancelled ? " - CANCELLED" : ""}`);
|
|
198389
198402
|
const term = report.termination;
|
|
198390
|
-
out.push(`runs ended: ${term.ended} reached the end, ${term.stalled} stalled, ${term.capped} hit the step
|
|
198403
|
+
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`);
|
|
198391
198404
|
if (report.drivers.length) out.push(`input drivers: ${report.drivers.map((d) => d.ref).join(", ")}`);
|
|
198392
198405
|
if (report.unwrittenInputs.length) {
|
|
198393
198406
|
out.push(`? = gated on an input nothing writes/drives: ${report.unwrittenInputs.join(", ")} (add a coverage driver?)`);
|
|
@@ -198399,25 +198412,43 @@ function renderCoverageText(report, sceneName = (id) => id) {
|
|
|
198399
198412
|
out.push(` \u203C ${String(d.runs).padStart(6)} run(s) ${sceneName(d.scene)} choice '${d.id}'`);
|
|
198400
198413
|
}
|
|
198401
198414
|
}
|
|
198415
|
+
if (!report.beats.length) return out;
|
|
198416
|
+
const clip = (s, n = 48) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
198417
|
+
const scenes = new Set(report.beats.map((b) => b.scene));
|
|
198418
|
+
const heading = " reached played beat";
|
|
198419
|
+
const row = (b, withScene) => {
|
|
198420
|
+
const mark = b.reachedRuns === 0 ? b.needsInput || b.blockedBy ? "? " : "\u203C " : b.rare ? "~ " : " ";
|
|
198421
|
+
const label = b.character ? `${b.character}: ${clip(b.preview)}` : clip(b.preview || `(${b.kind})`);
|
|
198422
|
+
const where = withScene ? `[${sceneName(b.scene)}] ` : "";
|
|
198423
|
+
out.push(` ${mark}${pct(b.reachPct).padStart(6)} ${String(b.hits).padStart(6)} ${where}${label}`);
|
|
198424
|
+
for (const bg of b.blockedBy ?? []) {
|
|
198425
|
+
const names = bg.writers.map((w) => {
|
|
198426
|
+
const target = report.beats.find((x) => x.id === w);
|
|
198427
|
+
return target ? clip(target.preview || target.id, 28) : w;
|
|
198428
|
+
});
|
|
198429
|
+
out.push(` gated on ${bg.ref}, written only by: ${names.join(", ")} (never played either)`);
|
|
198430
|
+
}
|
|
198431
|
+
};
|
|
198432
|
+
out.push("");
|
|
198433
|
+
out.push(`reached = share of runs that played the beat at least once; played = times it played in all runs`);
|
|
198434
|
+
out.push(`\u203C never reached ? never reached, may need an input ~ rarely reached (under ${rareLimit}% of runs)`);
|
|
198435
|
+
if (order === "least") {
|
|
198436
|
+
out.push("");
|
|
198437
|
+
out.push(`least reached first${scenes.size > 1 ? ", every scene" : ""}`);
|
|
198438
|
+
out.push(heading);
|
|
198439
|
+
for (const b of leastReachedFirst(report.beats)) row(b, scenes.size > 1);
|
|
198440
|
+
return out;
|
|
198441
|
+
}
|
|
198402
198442
|
const byScene = /* @__PURE__ */ new Map();
|
|
198403
198443
|
for (const b of report.beats) (byScene.get(b.scene) ?? byScene.set(b.scene, []).get(b.scene)).push(b);
|
|
198404
|
-
const clip = (s, n = 48) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
198405
198444
|
for (const [scene, beats] of byScene) {
|
|
198406
198445
|
const dead = beats.filter((b) => b.reachedRuns === 0).length;
|
|
198446
|
+
const rare = beats.filter((b) => b.rare).length;
|
|
198447
|
+
const notes = [dead ? `${dead} never reached` : "", rare ? `${rare} rarely reached` : ""].filter(Boolean).join(", ");
|
|
198407
198448
|
out.push("");
|
|
198408
|
-
out.push(`${sceneName(scene)}${
|
|
198409
|
-
|
|
198410
|
-
|
|
198411
|
-
const label = b.character ? `${b.character}: ${clip(b.preview)}` : clip(b.preview || `(${b.kind})`);
|
|
198412
|
-
out.push(` ${mark}${b.reachPct.toFixed(0).padStart(3)}% ${String(b.hits).padStart(6)} ${label}`);
|
|
198413
|
-
for (const bg of b.blockedBy ?? []) {
|
|
198414
|
-
const names = bg.writers.map((w) => {
|
|
198415
|
-
const target = report.beats.find((x) => x.id === w);
|
|
198416
|
-
return target ? clip(target.preview || target.id, 28) : w;
|
|
198417
|
-
});
|
|
198418
|
-
out.push(` gated on ${bg.ref}, written only by: ${names.join(", ")} (never played either)`);
|
|
198419
|
-
}
|
|
198420
|
-
}
|
|
198449
|
+
out.push(`${sceneName(scene)}${notes ? ` (${notes})` : ""}`);
|
|
198450
|
+
out.push(heading);
|
|
198451
|
+
for (const b of beats) row(b, false);
|
|
198421
198452
|
}
|
|
198422
198453
|
return out;
|
|
198423
198454
|
}
|
|
@@ -241787,7 +241818,7 @@ function writeTextFiles(files, encoding = "utf8") {
|
|
|
241787
241818
|
// package.json
|
|
241788
241819
|
var package_default = {
|
|
241789
241820
|
name: "@patterkit/cli",
|
|
241790
|
-
version: "0.
|
|
241821
|
+
version: "0.6.0",
|
|
241791
241822
|
description: "The `patter` CLI: validate, format, export, and play a Patter project.",
|
|
241792
241823
|
type: "module",
|
|
241793
241824
|
license: "MIT",
|
|
@@ -241817,7 +241848,7 @@ var package_default = {
|
|
|
241817
241848
|
},
|
|
241818
241849
|
dependencies: {
|
|
241819
241850
|
"@patterkit/core": "0.3.0",
|
|
241820
|
-
"@patterkit/ops": "0.
|
|
241851
|
+
"@patterkit/ops": "0.11.0",
|
|
241821
241852
|
"@wildwinter/simple-vc-lib": "^0.4.1"
|
|
241822
241853
|
}
|
|
241823
241854
|
};
|
|
@@ -241857,6 +241888,7 @@ Usage:
|
|
|
241857
241888
|
[--scene id] [--block id] [--choices a,b,c] [--seed N]
|
|
241858
241889
|
patter coverage [path] Narrative coverage: random playthroughs, find never-reached content
|
|
241859
241890
|
[--runs N] [--max-steps M] [--seed S] [--scene id] [--block id]
|
|
241891
|
+
[--order least|script] (least reached first, the default, or the script's own order)
|
|
241860
241892
|
[--json] [--fail-on-gap] (--fail-on-gap exits 1 if any beat is never reached)
|
|
241861
241893
|
[--propose] (print auto-proposed @world input drivers instead of running)
|
|
241862
241894
|
patter resolve <query> [path] Find a line by id, Game ID, or name: shows where it lives + what it says
|
|
@@ -241892,7 +241924,7 @@ var FLAGS = {
|
|
|
241892
241924
|
export: { boolean: ["ids", "source-debug"], valued: ["o"] },
|
|
241893
241925
|
"export-html": { boolean: [], valued: ["o"] },
|
|
241894
241926
|
play: { boolean: [], valued: ["scene", "block", "choices", "seed"] },
|
|
241895
|
-
coverage: { boolean: ["json", "fail-on-gap", "propose"], valued: ["runs", "max-steps", "seed", "scene", "block"] },
|
|
241927
|
+
coverage: { boolean: ["json", "fail-on-gap", "propose"], valued: ["runs", "max-steps", "seed", "scene", "block", "order"] },
|
|
241896
241928
|
resolve: { boolean: [], valued: [] },
|
|
241897
241929
|
usage: { boolean: ["json"], valued: [] },
|
|
241898
241930
|
report: { boolean: ["json"], valued: ["xlsx"] },
|
|
@@ -242150,6 +242182,11 @@ ${count} issue(s)${advisory}`);
|
|
|
242150
242182
|
}
|
|
242151
242183
|
numbers[flag] = n;
|
|
242152
242184
|
}
|
|
242185
|
+
const order = flags.order ?? "least";
|
|
242186
|
+
if (order !== "least" && order !== "script") {
|
|
242187
|
+
console.error(`coverage: --order '${order}' is not least or script`);
|
|
242188
|
+
return 2;
|
|
242189
|
+
}
|
|
242153
242190
|
const loaded = loadProject(positionals[0] ?? ".");
|
|
242154
242191
|
if (flags.propose === true) {
|
|
242155
242192
|
const drivers = proposeCoverageDrivers(loaded);
|
|
@@ -242168,7 +242205,7 @@ ${count} issue(s)${advisory}`);
|
|
|
242168
242205
|
if (flags.json === true) console.log(JSON.stringify(report));
|
|
242169
242206
|
else {
|
|
242170
242207
|
const nameOf = (id) => loaded.scenes.find((s) => s.id === id)?.name ?? id;
|
|
242171
|
-
for (const line2 of renderCoverageText(report, nameOf)) console.log(line2);
|
|
242208
|
+
for (const line2 of renderCoverageText(report, nameOf, { order })) console.log(line2);
|
|
242172
242209
|
}
|
|
242173
242210
|
return flags["fail-on-gap"] === true && report.totals.neverHit > 0 ? 1 : 0;
|
|
242174
242211
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patterkit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "The `patter` CLI: validate, format, export, and play a Patter project.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@patterkit/core": "0.3.0",
|
|
33
|
-
"@patterkit/ops": "0.
|
|
33
|
+
"@patterkit/ops": "0.11.0",
|
|
34
34
|
"@wildwinter/simple-vc-lib": "^0.4.1"
|
|
35
35
|
}
|
|
36
36
|
}
|