@thinair/factory 0.0.0-stage → 0.1.124
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/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +104 -2
- package/cli/evaluate.ts +101 -0
- package/cli/factory-code-lint.ts +146 -0
- package/cli/load.ts +463 -0
- package/cli/template.ts +73 -0
- package/domain/load.ts +67 -0
- package/domain/step.ts +70 -0
- package/domain/system-elements/agent.ts +41 -0
- package/domain/system-elements/app-shape.ts +88 -0
- package/domain/system-elements/compose.ts +211 -0
- package/domain/system-elements/factory.ts +61 -0
- package/domain/system-elements/goal.ts +37 -0
- package/domain/system-elements/hole.ts +25 -0
- package/domain/system-elements/job.ts +78 -0
- package/domain/system-elements/node.ts +189 -0
- package/domain/system-elements/pool.ts +63 -0
- package/domain/system-elements/program.ts +123 -0
- package/domain/system-elements/registry.ts +173 -0
- package/domain/system-elements/repository.ts +79 -0
- package/domain/system-elements/rule.ts +55 -0
- package/domain/system-elements/service.ts +82 -0
- package/domain/system-elements/source.ts +40 -0
- package/domain/system-elements/stage.ts +70 -0
- package/package.json +40 -4
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import type { RuleOut, RuleSeen } from "./compose";
|
|
3
|
+
import { AUTHORED, authoredHead, FactKindName, head, Ref, type Authored, type Encoder, type FactKind, type LiveRows, type Slot } from "./node";
|
|
4
|
+
import type { AnyStage } from "./stage";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* A rule routes one kind of fact. Each fact of that kind that the factory
|
|
8
|
+
* has not routed yet goes through its route, which returns tickets to file
|
|
9
|
+
* or update (by key, in the stages it lists) and acts. A rule never sees a
|
|
10
|
+
* fact the factory wrote itself.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const Node = Schema.TaggedStruct("Rule", {
|
|
14
|
+
...head,
|
|
15
|
+
on: Schema.Struct({ from: Schema.Union([Ref("Repository"), Ref("Source")]), kind: FactKindName }),
|
|
16
|
+
/** The only stages it may file tickets in. */
|
|
17
|
+
files: Schema.Array(Ref("Stage")),
|
|
18
|
+
});
|
|
19
|
+
export type Node = typeof Node.Type;
|
|
20
|
+
|
|
21
|
+
export interface AuthoredRule<N extends string = string, A = any, S extends AnyStage = AnyStage> extends Authored<"Rule", N> {
|
|
22
|
+
readonly on: FactKind<A>;
|
|
23
|
+
readonly files: ReadonlyArray<S>;
|
|
24
|
+
readonly route: (fact: A, seen: RuleSeen) => ReadonlyArray<RuleOut<S>>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `rule("pull-request", { on: web.facts.pullRequest, files: [change], route: (pr, seen) => [...] })`. */
|
|
28
|
+
export const rule = <const N extends string, A, const S extends ReadonlyArray<AnyStage>>(name: N, settings: Readonly<{
|
|
29
|
+
on: FactKind<A>;
|
|
30
|
+
files: S;
|
|
31
|
+
route: (fact: A, seen: RuleSeen) => ReadonlyArray<RuleOut<S[number]>>;
|
|
32
|
+
}>): AuthoredRule<N, A, S[number]> => ({
|
|
33
|
+
[AUTHORED]: authoredHead("Rule", name, true),
|
|
34
|
+
on: settings.on,
|
|
35
|
+
files: settings.files,
|
|
36
|
+
route: settings.route,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const make = (authored: AuthoredRule, encoder: Encoder) => ({
|
|
40
|
+
on: {
|
|
41
|
+
from: authored.on.from[AUTHORED].tag === "Repository"
|
|
42
|
+
? encoder.ref("Repository", authored.on.from as Slot<"Repository">)
|
|
43
|
+
: encoder.ref("Source", authored.on.from as Slot<"Source">),
|
|
44
|
+
kind: authored.on.kind,
|
|
45
|
+
},
|
|
46
|
+
files: authored.files.map((stage: AnyStage) => encoder.ref("Stage", stage)),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** How many facts of its kind arrived. */
|
|
50
|
+
export const live = (node: Node, rows: LiveRows) => ({
|
|
51
|
+
facts: rows.facts.filter((fact) => fact.from === node.on.from.id && fact.kind === node.on.kind).length,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const owner = "vm" as const;
|
|
55
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import type { AuthoredAgent } from "./agent";
|
|
3
|
+
import type { Carries, Question } from "./compose";
|
|
4
|
+
import { Needs } from "./job";
|
|
5
|
+
import { AUTHORED, authoredHead, head, Ref, type Authored, type Encoder, type LiveRows } from "./node";
|
|
6
|
+
import type { AuthoredRepository } from "./repository";
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* A service: something that stays up while a stage wants it, rather than
|
|
10
|
+
* running once. A repository copy is the ticket's own `.repository` process
|
|
11
|
+
* of a repository; an agent session is one of these agents working the
|
|
12
|
+
* ticket, on the harness's own path. A Run is one `together` group of them.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const Service = Schema.Union([
|
|
16
|
+
Schema.TaggedStruct("RepositoryCopy", { repository: Ref("Repository") }),
|
|
17
|
+
Schema.TaggedStruct("AgentSession", { agents: Schema.Array(Ref("Agent")) }),
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
/** How a Disarm stops it: a copy rests (its disk is kept); a session cancels its turn, then rests. */
|
|
21
|
+
export const ServiceStop = Schema.Literals(["rest", "cancel-turn"]);
|
|
22
|
+
|
|
23
|
+
export const Node = Schema.TaggedStruct("Service", { ...head, service: Service, needs: Needs, stop: ServiceStop });
|
|
24
|
+
export type Node = typeof Node.Type;
|
|
25
|
+
|
|
26
|
+
/** A repository copy, as the next member of its Run receives it. */
|
|
27
|
+
export type RepositoryCopy = Readonly<{ process: string; path: string }>;
|
|
28
|
+
|
|
29
|
+
export interface RepositoryService<N extends string = string> extends Authored<"Service", N>, Carries<Readonly<{ base: string }>, RepositoryCopy> {
|
|
30
|
+
readonly service: "repository";
|
|
31
|
+
readonly repository: AuthoredRepository;
|
|
32
|
+
readonly needs: typeof Needs.Type;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type AgentSessionInput = Readonly<{ agent: AuthoredAgent; brief: string; repository: RepositoryCopy }>;
|
|
36
|
+
export type AgentSession = Readonly<{ session: string }>;
|
|
37
|
+
|
|
38
|
+
export interface AgentService<N extends string = string> extends Authored<"Service", N>, Carries<AgentSessionInput, AgentSession> {
|
|
39
|
+
readonly service: "agent";
|
|
40
|
+
readonly agents: ReadonlyArray<AuthoredAgent>;
|
|
41
|
+
readonly needs: typeof Needs.Type;
|
|
42
|
+
/** Which of its agents works a ticket: the judge picks from them; the safe default is the first listed. */
|
|
43
|
+
readonly choose: Question<AuthoredAgent>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type AnyService = RepositoryService | AgentService;
|
|
47
|
+
|
|
48
|
+
type Options<N extends string> = Readonly<{ name?: N; needs?: typeof Needs.Type }>;
|
|
49
|
+
|
|
50
|
+
export const service = {
|
|
51
|
+
/** The ticket's own copy of a repository. Named `repository-<repository>` unless named. */
|
|
52
|
+
repository: <const N extends string = string>(repository: AuthoredRepository, options?: Options<N>): RepositoryService<N> => ({
|
|
53
|
+
[AUTHORED]: authoredHead("Service", (options?.name ?? `repository-${repository[AUTHORED].name}`) as N, false),
|
|
54
|
+
service: "repository",
|
|
55
|
+
repository,
|
|
56
|
+
needs: options?.needs ?? {},
|
|
57
|
+
}),
|
|
58
|
+
/** One of these agents working the ticket. Named `agent-<agents>` unless named. */
|
|
59
|
+
agent: <const N extends string = string>(agents: ReadonlyArray<AuthoredAgent>, options?: Options<N>): AgentService<N> => {
|
|
60
|
+
const made: Omit<AgentService<N>, "choose"> = {
|
|
61
|
+
[AUTHORED]: authoredHead("Service", (options?.name ?? `agent-${agents.map((agent) => agent[AUTHORED].name).join("-")}`) as N, false),
|
|
62
|
+
service: "agent",
|
|
63
|
+
agents,
|
|
64
|
+
needs: options?.needs ?? {},
|
|
65
|
+
};
|
|
66
|
+
const session = made as AgentService<N>;
|
|
67
|
+
return Object.assign(session, { choose: { _tag: "Question", name: "choose", service: session, legal: agents } as const });
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const make = (authored: AnyService, encoder: Encoder) =>
|
|
72
|
+
authored.service === "repository"
|
|
73
|
+
? { service: { _tag: "RepositoryCopy" as const, repository: encoder.ref("Repository", authored.repository) }, needs: authored.needs, stop: "rest" as const }
|
|
74
|
+
: { service: { _tag: "AgentSession" as const, agents: authored.agents.map((agent) => encoder.ref("Agent", agent)) }, needs: authored.needs, stop: "cancel-turn" as const };
|
|
75
|
+
|
|
76
|
+
/** Its sessions or copies by state. */
|
|
77
|
+
export const live = (node: Node, rows: LiveRows) => {
|
|
78
|
+
const jobs = rows.jobs.filter((row) => row.node === node.id);
|
|
79
|
+
return { up: jobs.filter((row) => row.state === "running").length, starting: jobs.filter((row) => row.state === "waiting" || row.state === "bound").length };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const owner = "vm" as const;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { AUTHORED, authoredHead, FactKindName, head, type Authored, type Encoder, type FactKind, type LiveRows } from "./node";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* A source of facts that are not about a repository: anything a person, an
|
|
6
|
+
* agent or another program posts to the factory with `factory.fact.post`.
|
|
7
|
+
* `source("linear", { issue: Schema.Struct({ ... }) })` declares its kinds;
|
|
8
|
+
* `linear.facts.issue` is typed, and rules listen on it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const Node = Schema.TaggedStruct("Source", { ...head, kinds: Schema.Array(FactKindName) });
|
|
12
|
+
export type Node = typeof Node.Type;
|
|
13
|
+
|
|
14
|
+
type Facts = Readonly<Record<string, Schema.Codec<any, any, never, never>>>;
|
|
15
|
+
|
|
16
|
+
export interface AuthoredSource<N extends string = string, F extends Facts = Facts> extends Authored<"Source", N> {
|
|
17
|
+
readonly facts: { readonly [K in keyof F & string]: FactKind<F[K]["Type"]> };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const source = <const N extends string, const F extends Facts>(name: N, facts: F): AuthoredSource<N, F> => {
|
|
21
|
+
const made = { [AUTHORED]: authoredHead("Source", name, false) };
|
|
22
|
+
const from: Authored<"Source"> = made;
|
|
23
|
+
return Object.assign(made, {
|
|
24
|
+
facts: Object.fromEntries(Object.entries(facts).map(([kind, schema]) => [kind, { from, kind, schema }])) as AuthoredSource<N, F>["facts"],
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const make = (authored: AuthoredSource, encoder: Encoder) => {
|
|
29
|
+
const kinds = Object.keys(authored.facts).sort();
|
|
30
|
+
if (kinds.length === 0) encoder.problem("A source posts at least one kind of fact.");
|
|
31
|
+
return { kinds };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Its newest fact, and how many it has posted. */
|
|
35
|
+
export const live = (node: Node, rows: LiveRows) => {
|
|
36
|
+
const facts = rows.facts.filter((fact) => fact.from === node.id);
|
|
37
|
+
return { last: facts.at(-1)?.at ?? null, facts: facts.length };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const owner = "vm" as const;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import type { Out, Question, Seen, TicketView, Work } from "./compose";
|
|
3
|
+
import { AUTHORED, authoredHead, head, Name, Ref, type Authored, type Encoder, type LiveRows, type Slot } from "./node";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* A stage: a kind of ticket and how it is worked. Its spec is the ticket's
|
|
7
|
+
* desired state. Its compose reads the ticket and what the factory has seen,
|
|
8
|
+
* and returns what should exist now: wanted jobs and services, child
|
|
9
|
+
* tickets, pins, judgments and acts. Compose may want only the jobs the
|
|
10
|
+
* stage lists, and types enforce it. A stage's ticket state comes from its
|
|
11
|
+
* verdict, or from its lifecycle when it has one.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const Node = Schema.TaggedStruct("Stage", {
|
|
15
|
+
...head,
|
|
16
|
+
/** The only jobs and services compose may want. */
|
|
17
|
+
jobs: Schema.Array(Schema.Union([Ref("Job"), Ref("Service")])),
|
|
18
|
+
/** The stages whose tickets compose may file as its children. */
|
|
19
|
+
children: Schema.Array(Ref("Stage")),
|
|
20
|
+
/** Questions compose may ask the judge, each a choice one of its services offers. */
|
|
21
|
+
questions: Schema.Record(Name, Ref("Service")),
|
|
22
|
+
});
|
|
23
|
+
export type Node = typeof Node.Type;
|
|
24
|
+
|
|
25
|
+
export interface AuthoredStage<N extends string = string, Spec = any, W extends Work = Work, C extends AnyStage = any> extends Authored<"Stage", N> {
|
|
26
|
+
readonly spec: Schema.Codec<Spec, any, never, never>;
|
|
27
|
+
readonly jobs: ReadonlyArray<W>;
|
|
28
|
+
readonly children: ReadonlyArray<C>;
|
|
29
|
+
readonly questions: Readonly<Record<string, Question<unknown>>>;
|
|
30
|
+
readonly compose: (ticket: TicketView<Spec>, seen: Seen<W>) => ReadonlyArray<Out<W, C>>;
|
|
31
|
+
}
|
|
32
|
+
export type AnyStage = AuthoredStage<string, any, any, any>;
|
|
33
|
+
export type StageSpec<S> = S extends AuthoredStage<string, infer Spec, any, any> ? Spec : never;
|
|
34
|
+
|
|
35
|
+
/** `stage("change", { spec, jobs, compose })`. */
|
|
36
|
+
export const stage = <const N extends string, Spec, const W extends ReadonlyArray<Work> = readonly [], const C extends ReadonlyArray<AnyStage> = readonly []>(name: N, settings: Readonly<{
|
|
37
|
+
spec: Schema.Codec<Spec, any, never, never>;
|
|
38
|
+
jobs?: W;
|
|
39
|
+
children?: C;
|
|
40
|
+
questions?: Readonly<Record<string, Question<any>>>;
|
|
41
|
+
compose: (ticket: TicketView<Spec>, seen: Seen<W[number]>) => ReadonlyArray<Out<W[number], C[number]>>;
|
|
42
|
+
}>): AuthoredStage<N, Spec, W[number], C[number]> => ({
|
|
43
|
+
[AUTHORED]: authoredHead("Stage", name, true),
|
|
44
|
+
spec: settings.spec,
|
|
45
|
+
jobs: settings.jobs ?? [],
|
|
46
|
+
children: settings.children ?? [],
|
|
47
|
+
questions: settings.questions ?? {},
|
|
48
|
+
compose: settings.compose,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const make = (authored: AnyStage, encoder: Encoder) => ({
|
|
52
|
+
jobs: authored.jobs.map((work: Work) => work[AUTHORED].tag === "Job"
|
|
53
|
+
? encoder.ref("Job", work as Slot<"Job">)
|
|
54
|
+
: encoder.ref("Service", work as Slot<"Service">)),
|
|
55
|
+
children: authored.children.map((child: AnyStage) => encoder.ref("Stage", child)),
|
|
56
|
+
questions: Object.fromEntries(Object.entries(authored.questions).map(([name, question]) => {
|
|
57
|
+
if (!authored.jobs.includes(question.service)) encoder.problem(`Question ${name} asks ${question.service[AUTHORED].name}, which this stage does not list in its jobs.`);
|
|
58
|
+
return [name, encoder.ref("Service", question.service)];
|
|
59
|
+
})),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** Its tickets by state. */
|
|
63
|
+
export const live = (node: Node, rows: LiveRows) => {
|
|
64
|
+
const byState: Record<string, number> = {};
|
|
65
|
+
for (const ticket of rows.tickets) if (ticket.stage === node.id) byState[ticket.state] = (byState[ticket.state] ?? 0) + 1;
|
|
66
|
+
return { tickets: byState };
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const owner = "vm" as const;
|
|
70
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thinair/factory",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.124",
|
|
4
|
+
"description": "The Thinair factory SDK: the language a factory's code is written in, and the loader that checks it.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"thinair-factory": "cli/load.ts"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
"./domain/system-elements/factory": "./domain/system-elements/factory.ts",
|
|
12
|
+
"./domain/system-elements/repository": "./domain/system-elements/repository.ts",
|
|
13
|
+
"./domain/system-elements/source": "./domain/system-elements/source.ts",
|
|
14
|
+
"./domain/system-elements/rule": "./domain/system-elements/rule.ts",
|
|
15
|
+
"./domain/system-elements/stage": "./domain/system-elements/stage.ts",
|
|
16
|
+
"./domain/system-elements/job": "./domain/system-elements/job.ts",
|
|
17
|
+
"./domain/system-elements/service": "./domain/system-elements/service.ts",
|
|
18
|
+
"./domain/system-elements/pool": "./domain/system-elements/pool.ts",
|
|
19
|
+
"./domain/system-elements/agent": "./domain/system-elements/agent.ts",
|
|
20
|
+
"./domain/system-elements/goal": "./domain/system-elements/goal.ts",
|
|
21
|
+
"./domain/system-elements/hole": "./domain/system-elements/hole.ts",
|
|
22
|
+
"./domain/system-elements/compose": "./domain/system-elements/compose.ts",
|
|
23
|
+
"./domain/step": "./domain/step.ts",
|
|
24
|
+
"./domain/system-elements/program": "./domain/system-elements/program.ts",
|
|
25
|
+
"./domain/load": "./domain/load.ts",
|
|
26
|
+
"./cli/load": "./cli/load.ts"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"effect": "4.0.0-rc.117",
|
|
30
|
+
"oxlint": "1.88.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"bun": ">=1.4.3"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/ThinAirThings/thinair.monorepo.git"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|