@llm4ts/core 2.13.0 → 2.14.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/blackboard/Fact.d.ts +48 -0
- package/dist/blackboard/Fact.d.ts.map +1 -0
- package/dist/blackboard/Fact.js +69 -0
- package/dist/blackboard/Fact.js.map +1 -0
- package/dist/blackboard/Rule.d.ts +92 -0
- package/dist/blackboard/Rule.d.ts.map +1 -0
- package/dist/blackboard/Rule.js +43 -0
- package/dist/blackboard/Rule.js.map +1 -0
- package/dist/blackboard/Ruleset.d.ts +107 -0
- package/dist/blackboard/Ruleset.d.ts.map +1 -0
- package/dist/blackboard/Ruleset.js +167 -0
- package/dist/blackboard/Ruleset.js.map +1 -0
- package/dist/blackboard/Run.d.ts +84 -0
- package/dist/blackboard/Run.d.ts.map +1 -0
- package/dist/blackboard/Run.js +195 -0
- package/dist/blackboard/Run.js.map +1 -0
- package/package.json +5 -1
- package/src/blackboard/Fact.ts +104 -0
- package/src/blackboard/Rule.ts +201 -0
- package/src/blackboard/Ruleset.ts +216 -0
- package/src/blackboard/Run.ts +251 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Schema from "effect/Schema"
|
|
3
|
+
import type { FactKey } from "./Fact.ts"
|
|
4
|
+
import type { Rule } from "./Rule.ts"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A ruleset (ADR 0020) is validated when it is built, not when it runs:
|
|
8
|
+
* every read is imported or produced, every export is produced, one
|
|
9
|
+
* producer per key, no constant or self-matching rule. Rules that cannot
|
|
10
|
+
* reach an export are pruned with a warning, so a company's ruleset never
|
|
11
|
+
* pays for a judgment nothing depends on. A valid ruleset is a value that
|
|
12
|
+
* runs many times.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const Problem = Schema.Union([
|
|
16
|
+
Schema.Struct({
|
|
17
|
+
kind: Schema.Literal("UnproducedMatch"),
|
|
18
|
+
rule: Schema.String,
|
|
19
|
+
key: Schema.String
|
|
20
|
+
}),
|
|
21
|
+
Schema.Struct({ kind: Schema.Literal("UnproducedExport"), key: Schema.String }),
|
|
22
|
+
Schema.Struct({
|
|
23
|
+
kind: Schema.Literal("ManyProducers"),
|
|
24
|
+
key: Schema.String,
|
|
25
|
+
rules: Schema.Array(Schema.String)
|
|
26
|
+
}),
|
|
27
|
+
Schema.Struct({ kind: Schema.Literal("EmptyCondition"), rule: Schema.String }),
|
|
28
|
+
Schema.Struct({ kind: Schema.Literal("SelfMatch"), rule: Schema.String, key: Schema.String }),
|
|
29
|
+
Schema.Struct({ kind: Schema.Literal("DuplicateRuleName"), rule: Schema.String }),
|
|
30
|
+
Schema.Struct({ kind: Schema.Literal("UnusedImport"), key: Schema.String }),
|
|
31
|
+
Schema.Struct({ kind: Schema.Literal("Unreachable"), rule: Schema.String })
|
|
32
|
+
])
|
|
33
|
+
export type Problem = typeof Problem.Type
|
|
34
|
+
|
|
35
|
+
const warningKinds: ReadonlySet<Problem["kind"]> = new Set(["UnusedImport", "Unreachable"])
|
|
36
|
+
|
|
37
|
+
export const isWarning = (problem: Problem): boolean => warningKinds.has(problem.kind)
|
|
38
|
+
|
|
39
|
+
export const renderProblem = (problem: Problem): string => {
|
|
40
|
+
switch (problem.kind) {
|
|
41
|
+
case "UnproducedMatch":
|
|
42
|
+
return `rule ${problem.rule} reads "${problem.key}", which nothing imports or produces`
|
|
43
|
+
case "UnproducedExport":
|
|
44
|
+
return `export "${problem.key}" is produced by no rule`
|
|
45
|
+
case "ManyProducers":
|
|
46
|
+
return `"${problem.key}" has several producers: ${problem.rules.join(", ")}`
|
|
47
|
+
case "EmptyCondition":
|
|
48
|
+
return `rule ${problem.rule} reads nothing`
|
|
49
|
+
case "SelfMatch":
|
|
50
|
+
return `rule ${problem.rule} reads "${problem.key}", which it produces`
|
|
51
|
+
case "DuplicateRuleName":
|
|
52
|
+
return `two rules are named ${problem.rule}`
|
|
53
|
+
case "UnusedImport":
|
|
54
|
+
return `import "${problem.key}" is read by no rule`
|
|
55
|
+
case "Unreachable":
|
|
56
|
+
return `rule ${problem.rule} reaches no export and is not run`
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class RulesetInvalid extends Schema.TaggedError<RulesetInvalid>()("RulesetInvalid", {
|
|
61
|
+
name: Schema.String,
|
|
62
|
+
problems: Schema.Array(Problem)
|
|
63
|
+
}) {
|
|
64
|
+
get message(): string {
|
|
65
|
+
return `ruleset "${this.name}" is invalid: ${this.problems.map(renderProblem).join("; ")}`
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface RulesetOptions<Rs extends ReadonlyArray<Rule<unknown, unknown>>> {
|
|
70
|
+
readonly name: string
|
|
71
|
+
readonly imports: ReadonlyArray<FactKey<unknown>>
|
|
72
|
+
readonly exports: ReadonlyArray<FactKey<unknown>>
|
|
73
|
+
readonly rules: Rs
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The union of the rules' error types, so a `judge` and a custom-error `rule` mix freely. */
|
|
77
|
+
export type ErrorOf<Rs extends ReadonlyArray<Rule<unknown, unknown>>> =
|
|
78
|
+
Rs[number] extends Rule<infer E, unknown> ? E : never
|
|
79
|
+
|
|
80
|
+
/** The union of the services the rules need. */
|
|
81
|
+
export type RequirementsOf<Rs extends ReadonlyArray<Rule<unknown, unknown>>> =
|
|
82
|
+
Rs[number] extends Rule<unknown, infer R> ? R : never
|
|
83
|
+
|
|
84
|
+
export interface Ruleset<E = never, R = never> {
|
|
85
|
+
readonly name: string
|
|
86
|
+
readonly imports: ReadonlyArray<string>
|
|
87
|
+
readonly exports: ReadonlyArray<string>
|
|
88
|
+
/** The rules that can reach an export, in the order given. */
|
|
89
|
+
readonly rules: ReadonlyArray<Rule<E, R>>
|
|
90
|
+
readonly warnings: ReadonlyArray<Problem>
|
|
91
|
+
/** One line per rule: `name: reads -> produces`. */
|
|
92
|
+
describe(): string
|
|
93
|
+
/** A Mermaid `flowchart LR` of keys and rules. */
|
|
94
|
+
mermaid(): string
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const producersByKey = <E, R>(
|
|
98
|
+
imports: ReadonlyArray<string>,
|
|
99
|
+
rules: ReadonlyArray<Rule<E, R>>
|
|
100
|
+
): ReadonlyMap<string, ReadonlyArray<string>> => {
|
|
101
|
+
const producers = new Map<string, ReadonlyArray<string>>()
|
|
102
|
+
for (const key of imports) producers.set(key, ["(import)"])
|
|
103
|
+
for (const rule of rules) {
|
|
104
|
+
for (const key of rule.produces) {
|
|
105
|
+
producers.set(key, [...(producers.get(key) ?? []), rule.name])
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return producers
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Rules whose products can reach an export, following reads backwards from the exports. */
|
|
112
|
+
const reachable = <E, R>(
|
|
113
|
+
exports: ReadonlyArray<string>,
|
|
114
|
+
rules: ReadonlyArray<Rule<E, R>>
|
|
115
|
+
): ReadonlySet<string> => {
|
|
116
|
+
const needed = new Set(exports)
|
|
117
|
+
const kept = new Set<string>()
|
|
118
|
+
let grew = true
|
|
119
|
+
while (grew) {
|
|
120
|
+
grew = false
|
|
121
|
+
for (const rule of rules) {
|
|
122
|
+
if (kept.has(rule.name) || !rule.produces.some((key) => needed.has(key))) continue
|
|
123
|
+
kept.add(rule.name)
|
|
124
|
+
for (const key of rule.reads) needed.add(key)
|
|
125
|
+
grew = true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return kept
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const escapeLabel = (label: string): string => label.replaceAll('"', "'")
|
|
132
|
+
|
|
133
|
+
const renderMermaid = <E, R>(name: string, rules: ReadonlyArray<Rule<E, R>>): string => {
|
|
134
|
+
const keys = [...new Set(rules.flatMap((rule) => [...rule.reads, ...rule.produces]))]
|
|
135
|
+
const keyId = new Map(keys.map((key, index) => [key, `k${index}`]))
|
|
136
|
+
const ruleId = new Map(rules.map((rule, index) => [rule.name, `r${index}`]))
|
|
137
|
+
const nodes = [
|
|
138
|
+
...keys.map((key) => ` ${keyId.get(key)}(["${escapeLabel(key)}"])`),
|
|
139
|
+
...rules.map((rule) => ` ${ruleId.get(rule.name)}["${escapeLabel(rule.name)}"]`)
|
|
140
|
+
]
|
|
141
|
+
const edges = rules.flatMap((rule) => [
|
|
142
|
+
...rule.reads.map((key) => ` ${keyId.get(key)} --> ${ruleId.get(rule.name)}`),
|
|
143
|
+
...rule.produces.map((key) => ` ${ruleId.get(rule.name)} --> ${keyId.get(key)}`)
|
|
144
|
+
])
|
|
145
|
+
return ["flowchart LR", ` %% ${escapeLabel(name)}`, ...nodes, ...edges].join("\n")
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Typed by overload: the ruleset's error and service types are the unions
|
|
150
|
+
* over its rules, so a `judge` and a custom-error `rule` mix in one array.
|
|
151
|
+
* The implementation only needs names, so it works over loose rules.
|
|
152
|
+
*/
|
|
153
|
+
export function makeRuleset<const Rs extends ReadonlyArray<Rule<unknown, unknown>>>(
|
|
154
|
+
options: RulesetOptions<Rs>
|
|
155
|
+
): Effect.Effect<Ruleset<ErrorOf<Rs>, RequirementsOf<Rs>>, RulesetInvalid>
|
|
156
|
+
export function makeRuleset(
|
|
157
|
+
options: RulesetOptions<ReadonlyArray<Rule<unknown, unknown>>>
|
|
158
|
+
): Effect.Effect<Ruleset<unknown, unknown>, RulesetInvalid> {
|
|
159
|
+
const given = options.rules
|
|
160
|
+
const imports = options.imports.map((key) => key.name)
|
|
161
|
+
const exports = options.exports.map((key) => key.name)
|
|
162
|
+
const problems: Array<Problem> = []
|
|
163
|
+
const seen = new Set<string>()
|
|
164
|
+
for (const rule of given) {
|
|
165
|
+
if (seen.has(rule.name)) problems.push({ kind: "DuplicateRuleName", rule: rule.name })
|
|
166
|
+
seen.add(rule.name)
|
|
167
|
+
if (rule.reads.length === 0) problems.push({ kind: "EmptyCondition", rule: rule.name })
|
|
168
|
+
for (const key of rule.reads) {
|
|
169
|
+
if (rule.produces.includes(key)) problems.push({ kind: "SelfMatch", rule: rule.name, key })
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const producers = producersByKey(imports, given)
|
|
173
|
+
for (const [key, names] of producers) {
|
|
174
|
+
if (names.length > 1) problems.push({ kind: "ManyProducers", key, rules: names })
|
|
175
|
+
}
|
|
176
|
+
for (const rule of given) {
|
|
177
|
+
for (const key of rule.reads) {
|
|
178
|
+
if (!producers.has(key)) problems.push({ kind: "UnproducedMatch", rule: rule.name, key })
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const key of exports) {
|
|
182
|
+
if (!producers.has(key)) problems.push({ kind: "UnproducedExport", key })
|
|
183
|
+
}
|
|
184
|
+
if (problems.length > 0) {
|
|
185
|
+
return Effect.fail(RulesetInvalid.make({ name: options.name, problems }))
|
|
186
|
+
}
|
|
187
|
+
const warnings: Array<Problem> = []
|
|
188
|
+
const kept = reachable(exports, given)
|
|
189
|
+
for (const rule of given) {
|
|
190
|
+
if (!kept.has(rule.name)) warnings.push({ kind: "Unreachable", rule: rule.name })
|
|
191
|
+
}
|
|
192
|
+
const rules = given.filter((rule) => kept.has(rule.name))
|
|
193
|
+
// An import only pruned rules read is unused too: nothing that runs needs it.
|
|
194
|
+
const read = new Set(rules.flatMap((rule) => rule.reads))
|
|
195
|
+
for (const key of imports) {
|
|
196
|
+
if (!read.has(key)) warnings.push({ kind: "UnusedImport", key })
|
|
197
|
+
}
|
|
198
|
+
return Effect.succeed({
|
|
199
|
+
name: options.name,
|
|
200
|
+
imports,
|
|
201
|
+
exports,
|
|
202
|
+
rules,
|
|
203
|
+
warnings,
|
|
204
|
+
describe: () =>
|
|
205
|
+
[
|
|
206
|
+
`ruleset ${options.name}`,
|
|
207
|
+
`imports: ${imports.join(", ")}`,
|
|
208
|
+
`exports: ${exports.join(", ")}`,
|
|
209
|
+
...rules.map(
|
|
210
|
+
(rule) => `${rule.name}: ${rule.reads.join(", ")} -> ${rule.produces.join(", ")}`
|
|
211
|
+
),
|
|
212
|
+
...warnings.map((warning) => `warning: ${renderProblem(warning)}`)
|
|
213
|
+
].join("\n"),
|
|
214
|
+
mermaid: () => renderMermaid(options.name, rules)
|
|
215
|
+
})
|
|
216
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import * as Clock from "effect/Clock"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import * as Option from "effect/Option"
|
|
4
|
+
import * as Result from "effect/Result"
|
|
5
|
+
import * as Schema from "effect/Schema"
|
|
6
|
+
import { Board, DuplicateFact, writeOnce, type Fact } from "./Fact.ts"
|
|
7
|
+
import type { Facts, Posted, Rule } from "./Rule.ts"
|
|
8
|
+
import type { Ruleset } from "./Ruleset.ts"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Forward chaining in rounds (ADR 0020). A round fires, concurrently, every
|
|
12
|
+
* rule whose condition is satisfied by the current snapshot; the facts they
|
|
13
|
+
* post are then applied one by one, write-once. The run ends when a round
|
|
14
|
+
* fires nothing, and it succeeds only if every export is on the board. A
|
|
15
|
+
* rule's failure is recorded and its defaults posted; a defect fails the run.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const JudgmentNote = Schema.Struct({ backend: Schema.String, identity: Schema.String })
|
|
19
|
+
|
|
20
|
+
export class Firing extends Schema.Class<Firing>("Firing")({
|
|
21
|
+
rule: Schema.String,
|
|
22
|
+
kind: Schema.Literals(["derive", "judge", "rule"]),
|
|
23
|
+
readKeys: Schema.Array(Schema.String),
|
|
24
|
+
postedKeys: Schema.Array(Schema.String),
|
|
25
|
+
/** Epoch milliseconds. */
|
|
26
|
+
startedAt: Schema.Number,
|
|
27
|
+
/** Milliseconds. */
|
|
28
|
+
duration: Schema.Number,
|
|
29
|
+
/** `failed`: the consequence failed and `postedKeys` are the rule's defaults. */
|
|
30
|
+
outcome: Schema.Literals(["posted", "failed"]),
|
|
31
|
+
judgment: Schema.optionalKey(JudgmentNote)
|
|
32
|
+
}) {}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A rule's typed failure. `error` is the value itself while the result is in
|
|
36
|
+
* memory; `tag` and `message` are what survive encoding, since `Defect`
|
|
37
|
+
* encodes any error as a plain one.
|
|
38
|
+
*/
|
|
39
|
+
export class RuleFailure extends Schema.Class<RuleFailure>("RuleFailure")({
|
|
40
|
+
rule: Schema.String,
|
|
41
|
+
tag: Schema.String,
|
|
42
|
+
message: Schema.String,
|
|
43
|
+
error: Schema.Defect()
|
|
44
|
+
}) {}
|
|
45
|
+
|
|
46
|
+
const isTagged = (error: unknown): error is { readonly _tag: string } =>
|
|
47
|
+
typeof error === "object" && error !== null && "_tag" in error && typeof error._tag === "string"
|
|
48
|
+
|
|
49
|
+
const ruleFailure = (rule: string, error: unknown): RuleFailure =>
|
|
50
|
+
RuleFailure.make({
|
|
51
|
+
rule,
|
|
52
|
+
tag: isTagged(error) ? error._tag : error instanceof Error ? error.name : typeof error,
|
|
53
|
+
message: error instanceof Error ? error.message : String(error),
|
|
54
|
+
error
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
export class RunResult extends Schema.Class<RunResult>("RunResult")({
|
|
58
|
+
board: Board,
|
|
59
|
+
trace: Schema.Array(Firing),
|
|
60
|
+
failures: Schema.Array(RuleFailure)
|
|
61
|
+
}) {}
|
|
62
|
+
|
|
63
|
+
export class UndeclaredFact extends Schema.TaggedError<UndeclaredFact>()("UndeclaredFact", {
|
|
64
|
+
rule: Schema.String,
|
|
65
|
+
key: Schema.String
|
|
66
|
+
}) {
|
|
67
|
+
get message(): string {
|
|
68
|
+
return `rule ${this.rule} posted "${this.key}", which it does not declare`
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class MissingImport extends Schema.TaggedError<MissingImport>()("MissingImport", {
|
|
73
|
+
keys: Schema.Array(Schema.String)
|
|
74
|
+
}) {
|
|
75
|
+
get message(): string {
|
|
76
|
+
return `initial facts miss the imports: ${this.keys.join(", ")}`
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class WaitingRule extends Schema.Class<WaitingRule>("WaitingRule")({
|
|
81
|
+
rule: Schema.String,
|
|
82
|
+
missingKeys: Schema.Array(Schema.String)
|
|
83
|
+
}) {}
|
|
84
|
+
|
|
85
|
+
/** Why an export is missing: who still waits, and who ran and did not post it. */
|
|
86
|
+
export class MissingExport extends Schema.Class<MissingExport>("MissingExport")({
|
|
87
|
+
key: Schema.String,
|
|
88
|
+
waitingRules: Schema.Array(WaitingRule),
|
|
89
|
+
/** Producers that fired, failed or were vetoed by `when` without posting the key. */
|
|
90
|
+
silentRules: Schema.Array(Schema.String)
|
|
91
|
+
}) {}
|
|
92
|
+
|
|
93
|
+
export class ExportsMissing extends Schema.TaggedError<ExportsMissing>()("ExportsMissing", {
|
|
94
|
+
missing: Schema.Array(MissingExport),
|
|
95
|
+
/** What did run, so a stall can be explained. */
|
|
96
|
+
trace: Schema.Array(Firing),
|
|
97
|
+
failures: Schema.Array(RuleFailure)
|
|
98
|
+
}) {
|
|
99
|
+
get message(): string {
|
|
100
|
+
const reasons = this.missing.map((entry) => {
|
|
101
|
+
const waiting = entry.waitingRules.map(
|
|
102
|
+
(rule) => `${rule.rule} waits for ${rule.missingKeys.join(", ")}`
|
|
103
|
+
)
|
|
104
|
+
const silent = entry.silentRules.map((rule) => `${rule} fired without posting it`)
|
|
105
|
+
return `"${entry.key}" (${[...waiting, ...silent].join("; ")})`
|
|
106
|
+
})
|
|
107
|
+
const failed =
|
|
108
|
+
this.failures.length === 0
|
|
109
|
+
? ""
|
|
110
|
+
: `; failed: ${this.failures.map((f) => `${f.rule} (${f.tag})`).join(", ")}`
|
|
111
|
+
return `the run ended without: ${reasons.join(", ")}${failed}`
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const RunError = Schema.Union([DuplicateFact, UndeclaredFact, MissingImport, ExportsMissing])
|
|
116
|
+
export type RunError = typeof RunError.Type
|
|
117
|
+
|
|
118
|
+
interface Ready<E, R> {
|
|
119
|
+
readonly rule: Rule<E, R>
|
|
120
|
+
readonly firing: Effect.Effect<Posted, E, R>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface Outcome<E, R> {
|
|
124
|
+
readonly rule: Rule<E, R>
|
|
125
|
+
readonly startedAt: number
|
|
126
|
+
readonly duration: number
|
|
127
|
+
readonly result: Result.Result<Posted, E>
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Apply posted facts write-once; the keys actually written, in first-seen order. */
|
|
131
|
+
const post = (
|
|
132
|
+
facts: Facts,
|
|
133
|
+
rule: string,
|
|
134
|
+
allowed: ReadonlyArray<string>,
|
|
135
|
+
posted: ReadonlyArray<Fact>
|
|
136
|
+
): Effect.Effect<[Facts, ReadonlyArray<string>], DuplicateFact | UndeclaredFact> =>
|
|
137
|
+
Effect.gen(function* () {
|
|
138
|
+
let current = facts
|
|
139
|
+
const keys: Array<string> = []
|
|
140
|
+
for (const fact of posted) {
|
|
141
|
+
if (!allowed.includes(fact.key)) {
|
|
142
|
+
return yield* Effect.fail(UndeclaredFact.make({ rule, key: fact.key }))
|
|
143
|
+
}
|
|
144
|
+
const written = writeOnce(current, fact.key, yield* fact.encoded)
|
|
145
|
+
if (Result.isFailure(written)) return yield* Effect.fail(written.failure)
|
|
146
|
+
current = written.success
|
|
147
|
+
if (!keys.includes(fact.key)) keys.push(fact.key)
|
|
148
|
+
}
|
|
149
|
+
return [current, keys]
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const fireAll = <E, R>(
|
|
153
|
+
ready: ReadonlyArray<Ready<E, R>>
|
|
154
|
+
): Effect.Effect<ReadonlyArray<Outcome<E, R>>, never, R> =>
|
|
155
|
+
Effect.forEach(
|
|
156
|
+
ready,
|
|
157
|
+
({ rule, firing }) =>
|
|
158
|
+
Effect.gen(function* () {
|
|
159
|
+
const startedAt = yield* Clock.currentTimeMillis
|
|
160
|
+
const result = yield* Effect.result(firing)
|
|
161
|
+
const finishedAt = yield* Clock.currentTimeMillis
|
|
162
|
+
return { rule, startedAt, duration: finishedAt - startedAt, result }
|
|
163
|
+
}),
|
|
164
|
+
{ concurrency: "unbounded" }
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
export const runRuleset = <E, R>(
|
|
168
|
+
ruleset: Ruleset<E, R>,
|
|
169
|
+
initial: ReadonlyArray<Fact>
|
|
170
|
+
): Effect.Effect<RunResult, RunError, R> =>
|
|
171
|
+
Effect.gen(function* () {
|
|
172
|
+
const initialKeys = initial.map((fact) => fact.key)
|
|
173
|
+
const missingImports = ruleset.imports.filter((key) => !initialKeys.includes(key))
|
|
174
|
+
if (missingImports.length > 0) {
|
|
175
|
+
return yield* Effect.fail(MissingImport.make({ keys: missingImports }))
|
|
176
|
+
}
|
|
177
|
+
let [facts] = yield* post(new Map(), "(initial)", ruleset.imports, initial)
|
|
178
|
+
const pending = new Map(ruleset.rules.map((rule) => [rule.name, rule]))
|
|
179
|
+
const trace: Array<Firing> = []
|
|
180
|
+
const failures: Array<RuleFailure> = []
|
|
181
|
+
for (;;) {
|
|
182
|
+
const ready: Array<Ready<E, R>> = []
|
|
183
|
+
for (const rule of pending.values()) {
|
|
184
|
+
if (!rule.reads.every((key) => facts.has(key))) continue
|
|
185
|
+
const prepared = yield* rule.prepare(facts)
|
|
186
|
+
// Every read is present and facts never change, so a None now is a No forever.
|
|
187
|
+
pending.delete(rule.name)
|
|
188
|
+
if (Option.isSome(prepared)) ready.push({ rule, firing: prepared.value })
|
|
189
|
+
}
|
|
190
|
+
if (ready.length === 0) break
|
|
191
|
+
const outcomes = yield* fireAll(ready)
|
|
192
|
+
for (const outcome of outcomes) {
|
|
193
|
+
const failed = Result.isFailure(outcome.result)
|
|
194
|
+
if (Result.isFailure(outcome.result)) {
|
|
195
|
+
failures.push(ruleFailure(outcome.rule.name, outcome.result.failure))
|
|
196
|
+
}
|
|
197
|
+
const posted: Posted = Result.isSuccess(outcome.result)
|
|
198
|
+
? outcome.result.success
|
|
199
|
+
: { facts: outcome.rule.defaults }
|
|
200
|
+
const [next, postedKeys] = yield* post(
|
|
201
|
+
facts,
|
|
202
|
+
outcome.rule.name,
|
|
203
|
+
outcome.rule.produces,
|
|
204
|
+
posted.facts
|
|
205
|
+
)
|
|
206
|
+
facts = next
|
|
207
|
+
trace.push(
|
|
208
|
+
Firing.make({
|
|
209
|
+
rule: outcome.rule.name,
|
|
210
|
+
kind: outcome.rule.kind,
|
|
211
|
+
readKeys: outcome.rule.reads,
|
|
212
|
+
postedKeys,
|
|
213
|
+
startedAt: outcome.startedAt,
|
|
214
|
+
duration: outcome.duration,
|
|
215
|
+
outcome: failed ? "failed" : "posted",
|
|
216
|
+
...(posted.judgment === undefined ? {} : { judgment: posted.judgment })
|
|
217
|
+
})
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const missing = ruleset.exports.filter((key) => !facts.has(key))
|
|
222
|
+
if (missing.length > 0) {
|
|
223
|
+
return yield* Effect.fail(
|
|
224
|
+
ExportsMissing.make({
|
|
225
|
+
missing: missing.map((key) =>
|
|
226
|
+
MissingExport.make({
|
|
227
|
+
key,
|
|
228
|
+
waitingRules: [...pending.values()]
|
|
229
|
+
.filter((rule) => rule.produces.includes(key))
|
|
230
|
+
.map((rule) =>
|
|
231
|
+
WaitingRule.make({
|
|
232
|
+
rule: rule.name,
|
|
233
|
+
missingKeys: rule.reads.filter((read) => !facts.has(read))
|
|
234
|
+
})
|
|
235
|
+
),
|
|
236
|
+
silentRules: ruleset.rules
|
|
237
|
+
.filter((rule) => rule.produces.includes(key) && !pending.has(rule.name))
|
|
238
|
+
.map((rule) => rule.name)
|
|
239
|
+
})
|
|
240
|
+
),
|
|
241
|
+
trace,
|
|
242
|
+
failures
|
|
243
|
+
})
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
return RunResult.make({
|
|
247
|
+
board: Board.make({ facts: Object.fromEntries(facts) }),
|
|
248
|
+
trace,
|
|
249
|
+
failures
|
|
250
|
+
})
|
|
251
|
+
})
|