@patterkit/cli 0.2.7 → 0.3.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.
Files changed (2) hide show
  1. package/dist/cli.js +347 -16
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -194628,6 +194628,201 @@ function exportBundle(input) {
194628
194628
  };
194629
194629
  }
194630
194630
 
194631
+ // ../ops/src/reachability.ts
194632
+ var normaliseRef = (ref) => ref.includes(".") ? ref : `@patter.${ref.slice(1)}`;
194633
+ var SEP = "\0";
194634
+ var keyOf = (ref, sceneId) => ref.startsWith("@scene.") ? `${sceneId}${SEP}${ref}` : ref;
194635
+ var shown = (key2) => {
194636
+ const bare = key2.includes(SEP) ? key2.slice(key2.indexOf(SEP) + 1) : key2;
194637
+ const at = bare.indexOf(":");
194638
+ return at < 0 ? bare : `${bare.slice(0, at)} +${bare.slice(at + 1)}`;
194639
+ };
194640
+ var svRef = (ast) => Array.isArray(ast) && ast[0] === "sv" ? `@${String(ast[1])}.${String(ast[2])}` : void 0;
194641
+ function latchOf(ast, sceneId) {
194642
+ const direct = svRef(ast);
194643
+ if (direct !== void 0) return keyOf(direct, sceneId);
194644
+ if (!Array.isArray(ast)) return void 0;
194645
+ if (ast[0] === "bin" && ast[1] === "==") {
194646
+ const [, , l2, r] = ast;
194647
+ for (const [a, b] of [[l2, r], [r, l2]]) {
194648
+ const ref = svRef(a);
194649
+ if (ref !== void 0 && Array.isArray(b) && b[0] === "b" && b[1] === true) return keyOf(ref, sceneId);
194650
+ }
194651
+ return void 0;
194652
+ }
194653
+ if (ast[0] === "call" && ast[1] === "check_flags") {
194654
+ const ref = svRef(ast[2]);
194655
+ const args = ast.slice(3);
194656
+ if (ref === void 0 || args.length !== 1) return void 0;
194657
+ const arg = args[0];
194658
+ if (Array.isArray(arg) && arg[0] === "fd" && arg[1] === "+") return `${keyOf(ref, sceneId)}:${String(arg[2])}`;
194659
+ }
194660
+ return void 0;
194661
+ }
194662
+ function disjuncts(ast) {
194663
+ if (Array.isArray(ast) && ast[0] === "bin" && ast[1] === "or") {
194664
+ const [, , l2, r] = ast;
194665
+ return [...disjuncts(l2), ...disjuncts(r)];
194666
+ }
194667
+ return [ast];
194668
+ }
194669
+ function terms(ast, sceneId, negated = false, into = []) {
194670
+ const latch = latchOf(ast, sceneId);
194671
+ if (latch !== void 0) {
194672
+ into.push({ key: latch, negated });
194673
+ return into;
194674
+ }
194675
+ if (!Array.isArray(ast)) return into;
194676
+ if (ast[0] === "u" && ast[1] === "not") return terms(ast[2], sceneId, !negated, into);
194677
+ if (ast[0] === "bin" && ast[1] === "and" && !negated) {
194678
+ terms(ast[2], sceneId, false, into);
194679
+ terms(ast[3], sceneId, false, into);
194680
+ }
194681
+ return into;
194682
+ }
194683
+ var requirementsOf = (expr, sceneId) => expr === void 0 ? [] : terms(expr.ast, sceneId);
194684
+ var asFlags = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
194685
+ function reachabilityIssues(loaded, compiled) {
194686
+ const bundle = compiled ?? exportBundle({ project: loaded.project, scenes: loaded.scenes, locales: loaded.locales });
194687
+ const hostTokens = new Set((loaded.project.scopeRegistry?.scopes ?? []).map((s) => s.token));
194688
+ const preset = /* @__PURE__ */ new Set();
194689
+ const notALatch = (decl, key2) => {
194690
+ if (decl.temporary) {
194691
+ preset.add(key2);
194692
+ for (const v of decl.values ?? []) preset.add(`${key2}:${v}`);
194693
+ return;
194694
+ }
194695
+ if (decl.type === "boolean" && decl.default === true) preset.add(key2);
194696
+ if (decl.type === "flags") for (const v of asFlags(decl.default)) preset.add(`${key2}:${v}`);
194697
+ };
194698
+ for (const decl of loaded.project.properties ?? []) notALatch(decl, `@patter.${decl.name}`);
194699
+ for (const scene of loaded.scenes) {
194700
+ for (const decl of scene.sceneProps ?? []) notALatch(decl, `${scene.id}${SEP}@scene.${decl.name}`);
194701
+ }
194702
+ const latched = /* @__PURE__ */ new Set();
194703
+ const broken = /* @__PURE__ */ new Set();
194704
+ const writers = /* @__PURE__ */ new Map();
194705
+ const noteWrite = (key2, requires) => {
194706
+ latched.add(key2);
194707
+ (writers.get(key2) ?? writers.set(key2, []).get(key2)).push({ requires });
194708
+ };
194709
+ const breakKey = (key2) => {
194710
+ broken.add(key2);
194711
+ for (const k of latched) if (k.startsWith(`${key2}:`)) broken.add(k);
194712
+ };
194713
+ const scanEffects = (effects, sceneId, need) => {
194714
+ for (const e of effects ?? []) {
194715
+ const ref = normaliseRef(e.target);
194716
+ const key2 = keyOf(ref, sceneId);
194717
+ const ast = e.value.ast;
194718
+ if (Array.isArray(ast) && ast[0] === "b" && ast[1] === true) {
194719
+ noteWrite(key2, need);
194720
+ continue;
194721
+ }
194722
+ if (Array.isArray(ast) && ast[0] === "call" && ast[1] === "set_flags" && svRef(ast[2]) === ref) {
194723
+ let clean3 = true;
194724
+ for (const arg of ast.slice(3)) {
194725
+ if (Array.isArray(arg) && arg[0] === "fd" && arg[1] === "+") noteWrite(`${key2}:${String(arg[2])}`, need);
194726
+ else clean3 = false;
194727
+ }
194728
+ if (clean3) continue;
194729
+ }
194730
+ breakKey(key2);
194731
+ }
194732
+ };
194733
+ const walk2 = (nodes, sceneId, gate) => {
194734
+ for (const node of nodes) {
194735
+ const here = [...gate, ...requirementsOf(node.condition, sceneId)];
194736
+ if (node.type === "group") walk2(node.children, sceneId, here);
194737
+ else {
194738
+ scanEffects(node.onEnter, sceneId, here);
194739
+ scanEffects(node.onExit, sceneId, here);
194740
+ }
194741
+ }
194742
+ };
194743
+ for (const scene of Object.values(bundle.scenes)) {
194744
+ scanEffects(scene.onEntry, scene.id, []);
194745
+ for (const block of scene.blocks) walk2(block.children, scene.id, []);
194746
+ }
194747
+ for (const key2 of [...latched]) {
194748
+ const at = key2.indexOf(":");
194749
+ if (at > 0 && broken.has(key2.slice(0, at))) broken.add(key2);
194750
+ }
194751
+ const hostDriven = (key2) => hostTokens.has(key2.slice(1, key2.indexOf(".")));
194752
+ const monotonic = (key2) => latched.has(key2) && !broken.has(key2) && !preset.has(key2) && !hostDriven(key2);
194753
+ const cache2 = /* @__PURE__ */ new Map();
194754
+ const inFlight = /* @__PURE__ */ new Set();
194755
+ const mustHold = (key2) => {
194756
+ const done = cache2.get(key2);
194757
+ if (done !== void 0) return { need: done, cut: false };
194758
+ if (inFlight.has(key2)) return { need: /* @__PURE__ */ new Set(), cut: true };
194759
+ const routes = writers.get(key2);
194760
+ if (routes === void 0 || routes.length === 0) return { need: /* @__PURE__ */ new Set(), cut: false };
194761
+ inFlight.add(key2);
194762
+ let shared;
194763
+ let cut = false;
194764
+ for (const route of routes) {
194765
+ const need = /* @__PURE__ */ new Set();
194766
+ for (const t of route.requires) {
194767
+ if (t.negated || !monotonic(t.key)) continue;
194768
+ need.add(t.key);
194769
+ const deeper = mustHold(t.key);
194770
+ cut ||= deeper.cut;
194771
+ for (const k of deeper.need) need.add(k);
194772
+ }
194773
+ shared = shared === void 0 ? need : new Set([...shared].filter((k) => need.has(k)));
194774
+ }
194775
+ inFlight.delete(key2);
194776
+ const result = shared ?? /* @__PURE__ */ new Set();
194777
+ if (!cut) cache2.set(key2, result);
194778
+ return { need: result, cut };
194779
+ };
194780
+ const refute = (gate) => {
194781
+ for (const no of gate.filter((t) => t.negated)) {
194782
+ if (!monotonic(no.key)) continue;
194783
+ if (gate.some((t) => !t.negated && t.key === no.key)) {
194784
+ return `it asks for ${shown(no.key)} to be both set and not set`;
194785
+ }
194786
+ for (const yes of gate.filter((t) => !t.negated && t.key !== no.key)) {
194787
+ if (!monotonic(yes.key)) continue;
194788
+ const chain = mustHold(yes.key);
194789
+ if (!chain.cut && chain.need.has(no.key)) {
194790
+ return `${shown(yes.key)} can only become true after ${shown(no.key)}, which nothing sets back, so this condition can never hold`;
194791
+ }
194792
+ }
194793
+ }
194794
+ return void 0;
194795
+ };
194796
+ const issues = [];
194797
+ const report = (nodes, sceneId, gate) => {
194798
+ for (const node of nodes) {
194799
+ const own = node.condition;
194800
+ const branches = own ? disjuncts(own.ast) : [void 0];
194801
+ let reason;
194802
+ const refuted = branches.every((b) => {
194803
+ const r = refute([...gate, ...b ? terms(b, sceneId) : []]);
194804
+ reason ??= r;
194805
+ return r !== void 0;
194806
+ });
194807
+ if (refuted && reason !== void 0 && own) {
194808
+ issues.push({
194809
+ nodeId: node.id,
194810
+ field: "condition",
194811
+ src: own.src,
194812
+ severity: "warning",
194813
+ message: `this can never run: ${reason}`
194814
+ });
194815
+ continue;
194816
+ }
194817
+ if (node.type === "group") report(node.children, sceneId, [...gate, ...requirementsOf(own, sceneId)]);
194818
+ }
194819
+ };
194820
+ for (const scene of Object.values(bundle.scenes)) {
194821
+ for (const block of scene.blocks) report(block.children, scene.id, []);
194822
+ }
194823
+ return issues;
194824
+ }
194825
+
194631
194826
  // ../ops/src/validate.ts
194632
194827
  function runValidate(loaded) {
194633
194828
  const { project, scenes, locales } = loaded;
@@ -194639,10 +194834,12 @@ function runValidate(loaded) {
194639
194834
  const staleBundles = checkBundles(loaded);
194640
194835
  const unresolvedMerges = sidecarIssues(walkFiles(loaded.root, CONFLICT_SIDECAR));
194641
194836
  const orphans = orphanShards(loaded);
194837
+ const reachability = structural.length === 0 && conditions.length === 0 ? reachabilityIssues(loaded) : [];
194642
194838
  return {
194643
194839
  structural,
194644
194840
  conditions,
194645
194841
  interpolation,
194842
+ reachability,
194646
194843
  hygiene,
194647
194844
  staleBundles,
194648
194845
  unresolvedMerges,
@@ -196959,40 +197156,113 @@ function targetHostRef(target, hostTokens) {
196959
197156
  const m = /^@([A-Za-z_][\w]*)\.(.+)$/.exec(target);
196960
197157
  return m && hostTokens.has(m[1]) ? `@${m[1]}.${m[2]}` : void 0;
196961
197158
  }
197159
+ function flagKeys(node, hostTokens, fn) {
197160
+ if (node.kind !== "call" || node.name !== fn) return [];
197161
+ const subject = node.args[0];
197162
+ if (!subject || subject.kind !== "scopedvar" || !hostTokens.has(subject.scope)) return [];
197163
+ const ref = `@${subject.scope}.${subject.name}`;
197164
+ return node.args.slice(1).filter((a) => a.kind === "flagdelta").map((a) => `${ref}:${a.name}`);
197165
+ }
197166
+ function fineRefsIn(node, hostTokens, out) {
197167
+ for (const k of flagKeys(node, hostTokens, "check_flags")) out.add(k);
197168
+ switch (node.kind) {
197169
+ case "unary":
197170
+ fineRefsIn(node.operand, hostTokens, out);
197171
+ break;
197172
+ case "binary":
197173
+ fineRefsIn(node.left, hostTokens, out);
197174
+ fineRefsIn(node.right, hostTokens, out);
197175
+ break;
197176
+ case "call":
197177
+ for (const a of node.args) fineRefsIn(a, hostTokens, out);
197178
+ break;
197179
+ default:
197180
+ break;
197181
+ }
197182
+ }
196962
197183
  function analyzeHostScopes(bundle, hostTokens) {
196963
197184
  const written = /* @__PURE__ */ new Set();
196964
197185
  const gatesByBeat = /* @__PURE__ */ new Map();
197186
+ const fineGatesByBeat = /* @__PURE__ */ new Map();
196965
197187
  const proposals = /* @__PURE__ */ new Map();
196966
- if (hostTokens.size === 0) return { written, gatesByBeat, proposals };
197188
+ const writerSites = /* @__PURE__ */ new Map();
197189
+ const opaqueWrites = /* @__PURE__ */ new Set();
197190
+ const empty = { written, gatesByBeat, proposals, fineGatesByBeat, writerSites, opaqueWrites };
197191
+ if (hostTokens.size === 0) return empty;
196967
197192
  const refsIn = (expr) => {
196968
197193
  const refs = /* @__PURE__ */ new Set();
196969
197194
  if (expr) scanExpr(deserialiseAst(expr.ast), hostTokens, refs, proposals);
196970
197195
  return refs;
196971
197196
  };
196972
- const scanEffects = (effects) => {
197197
+ const fineIn = (expr) => {
197198
+ const refs = /* @__PURE__ */ new Set();
197199
+ if (expr) fineRefsIn(deserialiseAst(expr.ast), hostTokens, refs);
197200
+ return refs;
197201
+ };
197202
+ const addSite = (key2, witnesses) => {
197203
+ (writerSites.get(key2) ?? writerSites.set(key2, []).get(key2)).push(witnesses);
197204
+ };
197205
+ const scanEffects = (effects, witnesses) => {
196973
197206
  for (const e of effects ?? []) {
196974
- const t = targetHostRef(e.target, hostTokens);
196975
- if (t) written.add(t);
197207
+ const target = targetHostRef(e.target, hostTokens);
197208
+ if (target) {
197209
+ written.add(target);
197210
+ addSite(target, witnesses);
197211
+ const flags = e.value ? flagKeys(deserialiseAst(e.value.ast), hostTokens, "set_flags") : [];
197212
+ if (flags.length) for (const k of flags) addSite(k, witnesses);
197213
+ else opaqueWrites.add(target);
197214
+ }
196976
197215
  refsIn(e.value);
196977
197216
  }
196978
197217
  };
196979
- const walk2 = (nodes, gate) => {
197218
+ const walk2 = (nodes, gate, fine) => {
196980
197219
  for (const node of nodes) {
196981
197220
  const here = /* @__PURE__ */ new Set([...gate, ...refsIn(node.condition)]);
197221
+ const hereFine = /* @__PURE__ */ new Set([...fine, ...fineIn(node.condition)]);
196982
197222
  if (node.type === "group") {
196983
- walk2(node.children, here);
197223
+ walk2(node.children, here, hereFine);
196984
197224
  } else {
196985
- scanEffects(node.onEnter);
196986
- scanEffects(node.onExit);
196987
- for (const beat of node.beats ?? []) gatesByBeat.set(beat.id, here);
197225
+ const witnesses = (node.beats ?? []).map((b) => b.id);
197226
+ scanEffects(node.onEnter, witnesses);
197227
+ scanEffects(node.onExit, witnesses);
197228
+ for (const beat of node.beats ?? []) {
197229
+ gatesByBeat.set(beat.id, here);
197230
+ fineGatesByBeat.set(beat.id, hereFine);
197231
+ }
196988
197232
  }
196989
197233
  }
196990
197234
  };
196991
197235
  for (const scene of Object.values(bundle.scenes)) {
196992
- scanEffects(scene.onEntry);
196993
- for (const block of scene.blocks) walk2(block.children, /* @__PURE__ */ new Set());
197236
+ const sceneBeats = [];
197237
+ const collect = (nodes) => {
197238
+ for (const n of nodes) {
197239
+ if (n.type === "group") collect(n.children);
197240
+ else for (const b of n.beats ?? []) sceneBeats.push(b.id);
197241
+ }
197242
+ };
197243
+ for (const block of scene.blocks) collect(block.children);
197244
+ scanEffects(scene.onEntry, sceneBeats);
197245
+ for (const block of scene.blocks) walk2(block.children, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
197246
+ }
197247
+ return empty;
197248
+ }
197249
+ function blockedGates(beatId, analysis, drivenRefs, reachedRuns) {
197250
+ const out = [];
197251
+ const gates = /* @__PURE__ */ new Set([...analysis.gatesByBeat.get(beatId) ?? [], ...analysis.fineGatesByBeat.get(beatId) ?? []]);
197252
+ for (const ref of [...gates].sort()) {
197253
+ const coarse = ref.includes(":") ? ref.slice(0, ref.indexOf(":")) : ref;
197254
+ if (drivenRefs.has(coarse)) continue;
197255
+ if (!analysis.written.has(coarse)) continue;
197256
+ if (ref !== coarse && analysis.opaqueWrites.has(coarse)) continue;
197257
+ if (ref === coarse && [...gates].some((g) => g !== ref && g.startsWith(`${ref}:`))) continue;
197258
+ const sites = analysis.writerSites.get(ref) ?? [];
197259
+ if (!sites.length) continue;
197260
+ if (sites.some((s) => !s.length || s.some((w) => !reachedRuns.has(w)))) continue;
197261
+ if (!sites.every((s) => s.every((w) => reachedRuns.get(w) === 0))) continue;
197262
+ const writers = [...new Set(sites.flat())].sort();
197263
+ out.push({ ref, writers });
196994
197264
  }
196995
- return { written, gatesByBeat, proposals };
197265
+ return out;
196996
197266
  }
196997
197267
  function proposeCoverageDrivers(loaded) {
196998
197268
  const hostTokens = new Set((loaded.project.scopeRegistry?.scopes ?? []).map((s) => s.token));
@@ -197125,12 +197395,15 @@ function* sweep(loaded, options = {}, hooks = {}) {
197125
197395
  const m = meta.get(id);
197126
197396
  const reached = reachedRuns.get(id);
197127
197397
  let needsInput;
197398
+ let blockedBy;
197128
197399
  if (reached === 0) {
197129
197400
  const gates = [...analysis.gatesByBeat.get(id) ?? []].filter((r) => !analysis.written.has(r) && !drivenRefs.has(r));
197130
197401
  if (gates.length) {
197131
197402
  needsInput = gates;
197132
197403
  for (const g of gates) unwrittenInputs.add(g);
197133
197404
  }
197405
+ const blocked = blockedGates(id, analysis, drivenRefs, reachedRuns);
197406
+ if (blocked.length) blockedBy = blocked;
197134
197407
  }
197135
197408
  return {
197136
197409
  id,
@@ -197141,7 +197414,8 @@ function* sweep(loaded, options = {}, hooks = {}) {
197141
197414
  hits: hitCount.get(id),
197142
197415
  reachedRuns: reached,
197143
197416
  reachPct: executed ? reached / executed * 100 : 0,
197144
- ...needsInput ? { needsInput } : {}
197417
+ ...needsInput ? { needsInput } : {},
197418
+ ...blockedBy ? { blockedBy } : {}
197145
197419
  };
197146
197420
  });
197147
197421
  const neverHit = beats.filter((b) => b.reachedRuns === 0).length;
@@ -197194,9 +197468,16 @@ function renderCoverageText(report, sceneName = (id) => id) {
197194
197468
  out.push("");
197195
197469
  out.push(`${sceneName(scene)}${dead ? ` (${dead} never reached)` : ""}`);
197196
197470
  for (const b of beats) {
197197
- const mark = b.reachedRuns === 0 ? b.needsInput ? "? " : "\u203C " : " ";
197471
+ const mark = b.reachedRuns === 0 ? b.needsInput || b.blockedBy ? "? " : "\u203C " : " ";
197198
197472
  const label = b.character ? `${b.character}: ${clip(b.preview)}` : clip(b.preview || `(${b.kind})`);
197199
197473
  out.push(` ${mark}${b.reachPct.toFixed(0).padStart(3)}% ${String(b.hits).padStart(6)} ${label}`);
197474
+ for (const bg of b.blockedBy ?? []) {
197475
+ const names = bg.writers.map((w) => {
197476
+ const target = report.beats.find((x) => x.id === w);
197477
+ return target ? clip(target.preview || target.id, 28) : w;
197478
+ });
197479
+ out.push(` gated on ${bg.ref}, written only by: ${names.join(", ")} (never played either)`);
197480
+ }
197200
197481
  }
197201
197482
  }
197202
197483
  return out;
@@ -240535,6 +240816,44 @@ function writeTextFiles(files, encoding = "utf8") {
240535
240816
  return { success: results.every((r) => r.success), results };
240536
240817
  }
240537
240818
 
240819
+ // package.json
240820
+ var package_default = {
240821
+ name: "@patterkit/cli",
240822
+ version: "0.3.0",
240823
+ description: "The `patter` CLI: validate, format, export, and play a Patter project.",
240824
+ type: "module",
240825
+ license: "MIT",
240826
+ author: "Ian Thomas",
240827
+ homepage: "https://patterkit.dev",
240828
+ repository: {
240829
+ type: "git",
240830
+ url: "git+https://github.com/patterkit/patter.git",
240831
+ directory: "packages/cli"
240832
+ },
240833
+ bugs: "https://github.com/patterkit/patter/issues",
240834
+ publishConfig: {
240835
+ access: "public"
240836
+ },
240837
+ bin: {
240838
+ patter: "./dist/cli.js"
240839
+ },
240840
+ files: [
240841
+ "dist",
240842
+ "README.md"
240843
+ ],
240844
+ scripts: {
240845
+ build: "tsup",
240846
+ "build:standalone": "node scripts/build-standalone.mjs",
240847
+ "build:standalone:mac": "node scripts/build-standalone.mjs --targets=darwin-arm64,darwin-x64",
240848
+ "build:standalone:others": "node scripts/build-standalone.mjs --targets=linux-x64,linux-arm64,windows-x64"
240849
+ },
240850
+ dependencies: {
240851
+ "@patterkit/core": "0.2.1",
240852
+ "@patterkit/ops": "0.5.0",
240853
+ "@wildwinter/simple-vc-lib": "^0.4.1"
240854
+ }
240855
+ };
240856
+
240538
240857
  // src/main.ts
240539
240858
  var isPatterSource = (path) => SHARD_EXTENSIONS.some((ext) => path.endsWith(ext));
240540
240859
  function writeMergeResult(result, out, announce) {
@@ -240554,6 +240873,8 @@ function writeMergeResult(result, out, announce) {
240554
240873
  var USAGE = `patter - Patter CLI
240555
240874
 
240556
240875
  Usage:
240876
+ patter --version Print the version (also -v / version)
240877
+ patter --help Print this usage (also -h / help)
240557
240878
  patter init [dir] Scaffold a new project (the <dir>.patter folder, starter scene, VCS config)
240558
240879
  [--name X] [--vcs git|perforce|plastic|svn] [--bundle commit|ignore]
240559
240880
  patter validate [path] Validate a project (structural + expressions + encoding + bundle)
@@ -240671,6 +240992,14 @@ async function main(argv) {
240671
240992
  console.log(USAGE);
240672
240993
  return 0;
240673
240994
  }
240995
+ if (cmd === "--version" || cmd === "-v" || cmd === "version") {
240996
+ console.log(package_default.version);
240997
+ return 0;
240998
+ }
240999
+ if (cmd === "--help" || cmd === "-h" || cmd === "help") {
241000
+ console.log(USAGE);
241001
+ return 0;
241002
+ }
240674
241003
  const canonical = cmd === "fmt" ? "format" : cmd === "stats" ? "report" : cmd;
240675
241004
  if (!(canonical in FLAGS)) {
240676
241005
  console.log(USAGE);
@@ -240757,7 +241086,7 @@ async function run(cmd, positionals, flags) {
240757
241086
  }
240758
241087
  case "validate": {
240759
241088
  const loaded = loadProject(positionals[0] ?? ".");
240760
- const { structural, conditions, interpolation, hygiene, staleBundles, unresolvedMerges, orphans, ok } = runValidate(loaded);
241089
+ const { structural, conditions, interpolation, hygiene, staleBundles, unresolvedMerges, orphans, reachability, ok } = runValidate(loaded);
240761
241090
  for (const i of structural) console.error(` [${i.code}] ${i.message}`);
240762
241091
  for (const i of conditions) console.error(` [${i.field}] ${i.nodeId}: ${i.message} (${i.src})`);
240763
241092
  for (const i of interpolation) console.error(` [${i.field}] ${i.nodeId}: ${i.message} (${i.src})`);
@@ -240765,8 +241094,10 @@ async function run(cmd, positionals, flags) {
240765
241094
  for (const i of staleBundles) console.error(` [stale-bundle] ${i.file}: ${i.message}`);
240766
241095
  for (const i of unresolvedMerges) console.error(` [unresolved-merge] ${i.file}: ${i.message}`);
240767
241096
  for (const i of orphans) console.error(` [not-in-project] ${i.file}: ${i.message}`);
241097
+ for (const i of reachability) console.error(` [unreachable] ${i.nodeId}: ${i.message} (${i.src})`);
240768
241098
  const count = structural.length + conditions.length + interpolation.length + hygiene.length + staleBundles.length + unresolvedMerges.length + orphans.length;
240769
- if (ok) console.log(`ok - ${loaded.scenes.length} scene(s), no issues`);
241099
+ const advisory = reachability.length ? `, ${reachability.length} warning(s) above` : "";
241100
+ if (ok) console.log(`ok - ${loaded.scenes.length} scene(s), no issues${advisory}`);
240770
241101
  else console.error(`
240771
241102
  ${count} issue(s)`);
240772
241103
  return ok ? 0 : 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patterkit/cli",
3
- "version": "0.2.7",
3
+ "version": "0.3.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.2.1",
33
- "@patterkit/ops": "0.4.0",
33
+ "@patterkit/ops": "0.5.0",
34
34
  "@wildwinter/simple-vc-lib": "^0.4.1"
35
35
  }
36
36
  }