@patterkit/runtime 0.1.0 → 0.2.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.cts CHANGED
@@ -550,11 +550,39 @@ declare class Flow {
550
550
  * permanent terminal; `once` stops after one pass; `repeat` reshuffles.
551
551
  */
552
552
  private pickShuffle;
553
+ /**
554
+ * `sequence` with `order: "specificity"` - **Best match**: score every eligible child by how
555
+ * specifically its condition fits the CURRENT state (`matchedSpec`), keep the top-scoring tier,
556
+ * and break ties with the seeded shuffle (no immediate repeat). A child with no condition scores
557
+ * 0, so it is the filler that wins only when nothing more specific is eligible.
558
+ *
559
+ * `exhaust` composes as it does for the other orders: `repeat` re-scores the full eligible set
560
+ * every draw (re-pickable - the character keeps preferring the on-topic line); `once` uses each
561
+ * pick up (a bag of remaining ids), so as specific lines are consumed the group slides down to
562
+ * less-specific ones and finally the filler, then yields null; `stick` degrades like `once` but
563
+ * holds the final pick forever instead of drying up.
564
+ */
565
+ private pickSpecificity;
566
+ /** A child's Best-match score against the current state: 0 when it has no condition (the filler
567
+ * tier), else the specificity of its (already-passing) condition. */
568
+ private specScore;
569
+ /**
570
+ * The **matched-specificity** metric (parity contract): how many atomic constraints are actively
571
+ * holding this condition TRUE against the live state. Evaluation-aware, not a static clause count -
572
+ * it walks the tree with a De-Morgan polarity flag so `or` and `not` score the branch that is
573
+ * actually carrying the truth. `want` = "does this subtree need to be true for the whole condition
574
+ * to hold?" (true at the root). Only `and`/`or`/`not`/`check_flags` are structural; every other
575
+ * node (comparisons, scoped vars, literals, other calls) is an atom, evaluated whole.
576
+ */
577
+ private matchedSpec;
553
578
  /** A selector's cursor state - shared across flows (`group.shared`) or this flow's own. */
554
579
  private selectorState;
555
580
  private runEffects;
556
581
  private eligible;
557
582
  private evalExpr;
583
+ /** The deserialised (in-memory) AST for an expression, cached per Expression. Shared by the
584
+ * evaluator and the Best-match specificity walker so both work off one parse. */
585
+ private conditionAst;
558
586
  /** Record an entry of a node (entered-only; spec §7): bumps the flow + world counts. */
559
587
  private enter;
560
588
  /** Next float in [0, 1): the shared custom PRNG, or this flow's serialisable mulberry32. */
package/dist/index.d.ts CHANGED
@@ -550,11 +550,39 @@ declare class Flow {
550
550
  * permanent terminal; `once` stops after one pass; `repeat` reshuffles.
551
551
  */
552
552
  private pickShuffle;
553
+ /**
554
+ * `sequence` with `order: "specificity"` - **Best match**: score every eligible child by how
555
+ * specifically its condition fits the CURRENT state (`matchedSpec`), keep the top-scoring tier,
556
+ * and break ties with the seeded shuffle (no immediate repeat). A child with no condition scores
557
+ * 0, so it is the filler that wins only when nothing more specific is eligible.
558
+ *
559
+ * `exhaust` composes as it does for the other orders: `repeat` re-scores the full eligible set
560
+ * every draw (re-pickable - the character keeps preferring the on-topic line); `once` uses each
561
+ * pick up (a bag of remaining ids), so as specific lines are consumed the group slides down to
562
+ * less-specific ones and finally the filler, then yields null; `stick` degrades like `once` but
563
+ * holds the final pick forever instead of drying up.
564
+ */
565
+ private pickSpecificity;
566
+ /** A child's Best-match score against the current state: 0 when it has no condition (the filler
567
+ * tier), else the specificity of its (already-passing) condition. */
568
+ private specScore;
569
+ /**
570
+ * The **matched-specificity** metric (parity contract): how many atomic constraints are actively
571
+ * holding this condition TRUE against the live state. Evaluation-aware, not a static clause count -
572
+ * it walks the tree with a De-Morgan polarity flag so `or` and `not` score the branch that is
573
+ * actually carrying the truth. `want` = "does this subtree need to be true for the whole condition
574
+ * to hold?" (true at the root). Only `and`/`or`/`not`/`check_flags` are structural; every other
575
+ * node (comparisons, scoped vars, literals, other calls) is an atom, evaluated whole.
576
+ */
577
+ private matchedSpec;
553
578
  /** A selector's cursor state - shared across flows (`group.shared`) or this flow's own. */
554
579
  private selectorState;
555
580
  private runEffects;
556
581
  private eligible;
557
582
  private evalExpr;
583
+ /** The deserialised (in-memory) AST for an expression, cached per Expression. Shared by the
584
+ * evaluator and the Best-match specificity walker so both work off one parse. */
585
+ private conditionAst;
558
586
  /** Record an entry of a node (entered-only; spec §7): bumps the flow + world counts. */
559
587
  private enter;
560
588
  /** Next float in [0, 1): the shared custom PRNG, or this flow's serialisable mulberry32. */
package/dist/index.js CHANGED
@@ -902,7 +902,7 @@ var Flow = class {
902
902
  case "sequence": {
903
903
  const order = group.options?.order ?? "sequential";
904
904
  const exhaust = group.options?.exhaust ?? "once";
905
- return order === "shuffle" ? this.pickShuffle(eligible, exhaust, st) : this.pickSequential(eligible, exhaust, st);
905
+ return order === "shuffle" ? this.pickShuffle(eligible, exhaust, st) : order === "specificity" ? this.pickSpecificity(eligible, exhaust, st) : this.pickSequential(eligible, exhaust, st);
906
906
  }
907
907
  case "run":
908
908
  case "choice":
@@ -949,6 +949,78 @@ var Flow = class {
949
949
  st.last = id;
950
950
  return eligible.find((c) => c.id === id);
951
951
  }
952
+ /**
953
+ * `sequence` with `order: "specificity"` - **Best match**: score every eligible child by how
954
+ * specifically its condition fits the CURRENT state (`matchedSpec`), keep the top-scoring tier,
955
+ * and break ties with the seeded shuffle (no immediate repeat). A child with no condition scores
956
+ * 0, so it is the filler that wins only when nothing more specific is eligible.
957
+ *
958
+ * `exhaust` composes as it does for the other orders: `repeat` re-scores the full eligible set
959
+ * every draw (re-pickable - the character keeps preferring the on-topic line); `once` uses each
960
+ * pick up (a bag of remaining ids), so as specific lines are consumed the group slides down to
961
+ * less-specific ones and finally the filler, then yields null; `stick` degrades like `once` but
962
+ * holds the final pick forever instead of drying up.
963
+ */
964
+ pickSpecificity(eligible, exhaust, st) {
965
+ let pool = eligible;
966
+ if (exhaust !== "repeat") {
967
+ if (st.bag === void 0) st.bag = eligible.map((c) => c.id);
968
+ const remaining = new Set(st.bag);
969
+ pool = eligible.filter((c) => remaining.has(c.id));
970
+ if (pool.length === 0) {
971
+ return exhaust === "stick" && st.last !== void 0 ? eligible.find((c) => c.id === st.last) ?? null : null;
972
+ }
973
+ }
974
+ let best = -1;
975
+ const scored = pool.map((c) => {
976
+ const s = this.specScore(c);
977
+ if (s > best) best = s;
978
+ return { c, s };
979
+ });
980
+ const tier = scored.filter((x) => x.s === best).map((x) => x.c);
981
+ let pick;
982
+ if (tier.length === 1) {
983
+ pick = tier[0];
984
+ } else {
985
+ const p = st.last !== void 0 ? tier.findIndex((c) => c.id === st.last) : -1;
986
+ let i = Math.floor(this.rng() * (p >= 0 ? tier.length - 1 : tier.length));
987
+ if (p >= 0 && i >= p) i++;
988
+ pick = tier[i];
989
+ }
990
+ if (exhaust !== "repeat") st.bag = st.bag.filter((id) => id !== pick.id);
991
+ st.last = pick.id;
992
+ return pick;
993
+ }
994
+ /** A child's Best-match score against the current state: 0 when it has no condition (the filler
995
+ * tier), else the specificity of its (already-passing) condition. */
996
+ specScore(node) {
997
+ return node.condition ? this.matchedSpec(this.conditionAst(node.condition), true) : 0;
998
+ }
999
+ /**
1000
+ * The **matched-specificity** metric (parity contract): how many atomic constraints are actively
1001
+ * holding this condition TRUE against the live state. Evaluation-aware, not a static clause count -
1002
+ * it walks the tree with a De-Morgan polarity flag so `or` and `not` score the branch that is
1003
+ * actually carrying the truth. `want` = "does this subtree need to be true for the whole condition
1004
+ * to hold?" (true at the root). Only `and`/`or`/`not`/`check_flags` are structural; every other
1005
+ * node (comparisons, scoped vars, literals, other calls) is an atom, evaluated whole.
1006
+ */
1007
+ matchedSpec(node, want) {
1008
+ if (node.kind === "binary" && (node.op === "and" || node.op === "or")) {
1009
+ const behaveAsAnd = node.op === "and" === want;
1010
+ const l = this.matchedSpec(node.left, want);
1011
+ const r = this.matchedSpec(node.right, want);
1012
+ return behaveAsAnd ? l > 0 && r > 0 ? l + r : 0 : Math.max(l, r);
1013
+ }
1014
+ if (node.kind === "unary" && node.op === "not") {
1015
+ return this.matchedSpec(node.operand, !want);
1016
+ }
1017
+ if (node.kind === "call" && node.name === "check_flags") {
1018
+ const operands = Math.max(1, node.args.length - 1);
1019
+ const hit = truthy(evaluate(node, this.evalCtx, patterDialect));
1020
+ return want ? hit ? operands : 0 : hit ? 0 : 1;
1021
+ }
1022
+ return truthy(evaluate(node, this.evalCtx, patterDialect)) === want ? 1 : 0;
1023
+ }
952
1024
  /** A selector's cursor state - shared across flows (`group.shared`) or this flow's own. */
953
1025
  selectorState(group) {
954
1026
  const map = group.shared ? this.host.sharedSelectors : this.selectors;
@@ -970,12 +1042,17 @@ var Flow = class {
970
1042
  return truthy(this.evalExpr(node.condition));
971
1043
  }
972
1044
  evalExpr(expr) {
1045
+ return evaluate(this.conditionAst(expr), this.evalCtx, patterDialect);
1046
+ }
1047
+ /** The deserialised (in-memory) AST for an expression, cached per Expression. Shared by the
1048
+ * evaluator and the Best-match specificity walker so both work off one parse. */
1049
+ conditionAst(expr) {
973
1050
  let ast = astCache.get(expr);
974
1051
  if (!ast) {
975
1052
  ast = deserialiseAst(expr.ast);
976
1053
  astCache.set(expr, ast);
977
1054
  }
978
- return evaluate(ast, this.evalCtx, patterDialect);
1055
+ return ast;
979
1056
  }
980
1057
  /** Record an entry of a node (entered-only; spec §7): bumps the flow + world counts. */
981
1058
  enter(id) {