@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/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,16 @@ version number always means the same runtime behaviour. This package is versione
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [0.
|
|
10
|
+
## [0.2.0] - 2026-07-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Best match** selection (a new `sequence` order, `specificity`): among the eligible children,
|
|
14
|
+
play the one whose condition most specifically fits the current state; equally-specific ties break
|
|
15
|
+
by the seeded shuffle, and a condition-less child is the filler that wins only when nothing more
|
|
16
|
+
specific applies. Composes with the exhaust axis (re-pickable, or graceful degradation to the
|
|
17
|
+
filler). Locked by the conformance corpus, so all four runtimes agree.
|
|
18
|
+
|
|
19
|
+
## [0.1.0] - 2026-07-04
|
|
11
20
|
|
|
12
21
|
### Added
|
|
13
22
|
- The Patter runtime in JS/TS: `Engine` + `Flow` over a compiled `.patterc` bundle - scenes,
|
package/dist/index.cjs
CHANGED
|
@@ -933,7 +933,7 @@ var Flow = class {
|
|
|
933
933
|
case "sequence": {
|
|
934
934
|
const order = group.options?.order ?? "sequential";
|
|
935
935
|
const exhaust = group.options?.exhaust ?? "once";
|
|
936
|
-
return order === "shuffle" ? this.pickShuffle(eligible, exhaust, st) : this.pickSequential(eligible, exhaust, st);
|
|
936
|
+
return order === "shuffle" ? this.pickShuffle(eligible, exhaust, st) : order === "specificity" ? this.pickSpecificity(eligible, exhaust, st) : this.pickSequential(eligible, exhaust, st);
|
|
937
937
|
}
|
|
938
938
|
case "run":
|
|
939
939
|
case "choice":
|
|
@@ -980,6 +980,78 @@ var Flow = class {
|
|
|
980
980
|
st.last = id;
|
|
981
981
|
return eligible.find((c) => c.id === id);
|
|
982
982
|
}
|
|
983
|
+
/**
|
|
984
|
+
* `sequence` with `order: "specificity"` - **Best match**: score every eligible child by how
|
|
985
|
+
* specifically its condition fits the CURRENT state (`matchedSpec`), keep the top-scoring tier,
|
|
986
|
+
* and break ties with the seeded shuffle (no immediate repeat). A child with no condition scores
|
|
987
|
+
* 0, so it is the filler that wins only when nothing more specific is eligible.
|
|
988
|
+
*
|
|
989
|
+
* `exhaust` composes as it does for the other orders: `repeat` re-scores the full eligible set
|
|
990
|
+
* every draw (re-pickable - the character keeps preferring the on-topic line); `once` uses each
|
|
991
|
+
* pick up (a bag of remaining ids), so as specific lines are consumed the group slides down to
|
|
992
|
+
* less-specific ones and finally the filler, then yields null; `stick` degrades like `once` but
|
|
993
|
+
* holds the final pick forever instead of drying up.
|
|
994
|
+
*/
|
|
995
|
+
pickSpecificity(eligible, exhaust, st) {
|
|
996
|
+
let pool = eligible;
|
|
997
|
+
if (exhaust !== "repeat") {
|
|
998
|
+
if (st.bag === void 0) st.bag = eligible.map((c) => c.id);
|
|
999
|
+
const remaining = new Set(st.bag);
|
|
1000
|
+
pool = eligible.filter((c) => remaining.has(c.id));
|
|
1001
|
+
if (pool.length === 0) {
|
|
1002
|
+
return exhaust === "stick" && st.last !== void 0 ? eligible.find((c) => c.id === st.last) ?? null : null;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
let best = -1;
|
|
1006
|
+
const scored = pool.map((c) => {
|
|
1007
|
+
const s = this.specScore(c);
|
|
1008
|
+
if (s > best) best = s;
|
|
1009
|
+
return { c, s };
|
|
1010
|
+
});
|
|
1011
|
+
const tier = scored.filter((x) => x.s === best).map((x) => x.c);
|
|
1012
|
+
let pick;
|
|
1013
|
+
if (tier.length === 1) {
|
|
1014
|
+
pick = tier[0];
|
|
1015
|
+
} else {
|
|
1016
|
+
const p = st.last !== void 0 ? tier.findIndex((c) => c.id === st.last) : -1;
|
|
1017
|
+
let i = Math.floor(this.rng() * (p >= 0 ? tier.length - 1 : tier.length));
|
|
1018
|
+
if (p >= 0 && i >= p) i++;
|
|
1019
|
+
pick = tier[i];
|
|
1020
|
+
}
|
|
1021
|
+
if (exhaust !== "repeat") st.bag = st.bag.filter((id) => id !== pick.id);
|
|
1022
|
+
st.last = pick.id;
|
|
1023
|
+
return pick;
|
|
1024
|
+
}
|
|
1025
|
+
/** A child's Best-match score against the current state: 0 when it has no condition (the filler
|
|
1026
|
+
* tier), else the specificity of its (already-passing) condition. */
|
|
1027
|
+
specScore(node) {
|
|
1028
|
+
return node.condition ? this.matchedSpec(this.conditionAst(node.condition), true) : 0;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* The **matched-specificity** metric (parity contract): how many atomic constraints are actively
|
|
1032
|
+
* holding this condition TRUE against the live state. Evaluation-aware, not a static clause count -
|
|
1033
|
+
* it walks the tree with a De-Morgan polarity flag so `or` and `not` score the branch that is
|
|
1034
|
+
* actually carrying the truth. `want` = "does this subtree need to be true for the whole condition
|
|
1035
|
+
* to hold?" (true at the root). Only `and`/`or`/`not`/`check_flags` are structural; every other
|
|
1036
|
+
* node (comparisons, scoped vars, literals, other calls) is an atom, evaluated whole.
|
|
1037
|
+
*/
|
|
1038
|
+
matchedSpec(node, want) {
|
|
1039
|
+
if (node.kind === "binary" && (node.op === "and" || node.op === "or")) {
|
|
1040
|
+
const behaveAsAnd = node.op === "and" === want;
|
|
1041
|
+
const l = this.matchedSpec(node.left, want);
|
|
1042
|
+
const r = this.matchedSpec(node.right, want);
|
|
1043
|
+
return behaveAsAnd ? l > 0 && r > 0 ? l + r : 0 : Math.max(l, r);
|
|
1044
|
+
}
|
|
1045
|
+
if (node.kind === "unary" && node.op === "not") {
|
|
1046
|
+
return this.matchedSpec(node.operand, !want);
|
|
1047
|
+
}
|
|
1048
|
+
if (node.kind === "call" && node.name === "check_flags") {
|
|
1049
|
+
const operands = Math.max(1, node.args.length - 1);
|
|
1050
|
+
const hit = truthy((0, import_expr.evaluate)(node, this.evalCtx, import_dialect.patterDialect));
|
|
1051
|
+
return want ? hit ? operands : 0 : hit ? 0 : 1;
|
|
1052
|
+
}
|
|
1053
|
+
return truthy((0, import_expr.evaluate)(node, this.evalCtx, import_dialect.patterDialect)) === want ? 1 : 0;
|
|
1054
|
+
}
|
|
983
1055
|
/** A selector's cursor state - shared across flows (`group.shared`) or this flow's own. */
|
|
984
1056
|
selectorState(group) {
|
|
985
1057
|
const map = group.shared ? this.host.sharedSelectors : this.selectors;
|
|
@@ -1001,12 +1073,17 @@ var Flow = class {
|
|
|
1001
1073
|
return truthy(this.evalExpr(node.condition));
|
|
1002
1074
|
}
|
|
1003
1075
|
evalExpr(expr) {
|
|
1076
|
+
return (0, import_expr.evaluate)(this.conditionAst(expr), this.evalCtx, import_dialect.patterDialect);
|
|
1077
|
+
}
|
|
1078
|
+
/** The deserialised (in-memory) AST for an expression, cached per Expression. Shared by the
|
|
1079
|
+
* evaluator and the Best-match specificity walker so both work off one parse. */
|
|
1080
|
+
conditionAst(expr) {
|
|
1004
1081
|
let ast = astCache.get(expr);
|
|
1005
1082
|
if (!ast) {
|
|
1006
1083
|
ast = (0, import_expr.deserialiseAst)(expr.ast);
|
|
1007
1084
|
astCache.set(expr, ast);
|
|
1008
1085
|
}
|
|
1009
|
-
return
|
|
1086
|
+
return ast;
|
|
1010
1087
|
}
|
|
1011
1088
|
/** Record an entry of a node (entered-only; spec §7): bumps the flow + world counts. */
|
|
1012
1089
|
enter(id) {
|