@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/CHANGELOG.md +10 -1
- package/dist/index.cjs +79 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +79 -2
- package/dist/index.js.map +1 -1
- package/dist/patterplay.min.js +2 -2
- package/dist/patterplay.min.js.map +1 -1
- package/package.json +1 -1
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
|
|
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) {
|