@thinair/factory 0.1.160 → 0.1.166
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/cli/evaluate.ts +6 -1
- package/cli/load.ts +6 -3
- package/domain/load.ts +5 -0
- package/domain/step.ts +13 -0
- package/domain/system-elements/compose.ts +8 -2
- package/domain/system-elements/node.ts +2 -0
- package/domain/system-elements/pool.ts +30 -3
- package/domain/system-elements/registry.ts +7 -5
- package/package.json +1 -1
package/cli/evaluate.ts
CHANGED
|
@@ -26,8 +26,12 @@ export const EvaluateInput = Schema.Struct({
|
|
|
26
26
|
});
|
|
27
27
|
export type EvaluateInput = typeof EvaluateInput.Type;
|
|
28
28
|
|
|
29
|
+
const EvaluateNotice = Schema.Struct({ node: Schema.NullOr(NodeId), at: Schema.NullOr(SourceAt), message: Schema.String });
|
|
30
|
+
|
|
29
31
|
export const EvaluateOutput = Schema.Struct({
|
|
30
32
|
program: Schema.NullOr(Program),
|
|
33
|
+
/** Notes that hold nothing: a retired constructor the code still calls runs as its replacement. */
|
|
34
|
+
notices: Schema.Array(EvaluateNotice),
|
|
31
35
|
problems: Schema.Array(Schema.Struct({
|
|
32
36
|
/** `evaluate`: the code threw while it loaded. `value`: the encoder's checks. */
|
|
33
37
|
step: Schema.Literals(["evaluate", "value"]),
|
|
@@ -73,12 +77,13 @@ const evaluate = Effect.fn("factory.evaluate")(function* (inputPath: string) {
|
|
|
73
77
|
hash: (text) => createHash("sha256").update(text).digest("hex"),
|
|
74
78
|
files: input.files,
|
|
75
79
|
});
|
|
76
|
-
return { program: encoded.program, problems: encoded.problems.map((problem) => ({ step: "value" as const, ...problem })) };
|
|
80
|
+
return { program: encoded.program, notices: encoded.notices, problems: encoded.problems.map((problem) => ({ step: "value" as const, ...problem })) };
|
|
77
81
|
},
|
|
78
82
|
catch: (error) => error,
|
|
79
83
|
})),
|
|
80
84
|
Effect.catch((error) => Effect.succeed<EvaluateOutput>({
|
|
81
85
|
program: null,
|
|
86
|
+
notices: [],
|
|
82
87
|
problems: [{ step: "evaluate", node: null, at: throwAt(error, input.tree), message: `The factory's code threw while it loaded: ${error instanceof Error ? error.message : String(error)}` }],
|
|
83
88
|
})),
|
|
84
89
|
);
|
package/cli/load.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { createRequire } from "node:module";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { Context, Effect, Layer, Result, Schema } from "effect";
|
|
9
|
-
import { LOAD_VERSION, LoadJson, LoadPost, type Finding, type Load } from "../domain/load";
|
|
9
|
+
import { holds, LOAD_VERSION, LoadJson, LoadPost, type Finding, type Load } from "../domain/load";
|
|
10
10
|
import { appShapeProblems, AUTHORING_MODULES, folderKey, IGNORED_FOLDERS, isLoadedPath, memberFiles } from "../domain/system-elements/app-shape";
|
|
11
11
|
import { FrameworkVersion, holesOf, type Node, type Program } from "../domain/system-elements/program";
|
|
12
12
|
import { EvaluateOutputJson, type EvaluateInput } from "./evaluate";
|
|
@@ -270,7 +270,10 @@ const evaluate = Effect.fn("factory.load.evaluate")(function* (input: Omit<Evalu
|
|
|
270
270
|
);
|
|
271
271
|
return {
|
|
272
272
|
program: output.program,
|
|
273
|
-
findings:
|
|
273
|
+
findings: [
|
|
274
|
+
...output.problems.map((problem) => finding(problem.step, problem.message, { node: problem.node, file: problem.at?.file ?? null, line: problem.at?.line ?? null })),
|
|
275
|
+
...output.notices.map((notice) => ({ ...finding("value", notice.message, { node: notice.node, file: notice.at?.file ?? null, line: notice.at?.line ?? null }), notice: true as const })),
|
|
276
|
+
],
|
|
274
277
|
};
|
|
275
278
|
});
|
|
276
279
|
|
|
@@ -354,7 +357,7 @@ export const loadFactory = Effect.fn("factory.load")(function* (input: Readonly<
|
|
|
354
357
|
|
|
355
358
|
const load: Load = yield* timed(steps, "encode", Effect.sync((): Load => {
|
|
356
359
|
const all = findings.map(pinned(program));
|
|
357
|
-
return { version: LOAD_VERSION, framework, key: snapshot.key, state: all.
|
|
360
|
+
return { version: LOAD_VERSION, framework, key: snapshot.key, state: all.some(holds) ? "held" : "ready", program, holes: program === null ? [] : holesOf(program), findings: all, cached: false, steps };
|
|
358
361
|
}));
|
|
359
362
|
const encoded = yield* Schema.encodeEffect(LoadJson)(load).pipe(Effect.mapError((error) => new FactoryLoadFailed({ message: `The load does not encode: ${error.message}` })));
|
|
360
363
|
yield* Effect.tryPromise({ try: () => mkdir(path.dirname(cacheFile), { recursive: true }).then(() => writeFile(cacheFile, JSON.stringify(encoded))), catch: failed("Could not write the load cache.") });
|
package/domain/load.ts
CHANGED
|
@@ -22,7 +22,12 @@ export const Finding = Schema.Struct({
|
|
|
22
22
|
file: Schema.NullOr(Schema.String),
|
|
23
23
|
line: Schema.NullOr(Schema.Int),
|
|
24
24
|
message: Schema.String,
|
|
25
|
+
/** A note that holds nothing: the code still runs as written (a retired constructor runs as its replacement). */
|
|
26
|
+
notice: Schema.optionalKey(Schema.Literal(true)),
|
|
25
27
|
});
|
|
28
|
+
|
|
29
|
+
/** Whether a finding holds the load: every finding but a notice does. */
|
|
30
|
+
export const holds = (finding: Readonly<{ notice?: true | undefined }>) => finding.notice !== true;
|
|
26
31
|
export type Finding = typeof Finding.Type;
|
|
27
32
|
|
|
28
33
|
/** `ready` runs (live) or draws (preview). `held` keeps the last good revision: its findings say why. */
|
package/domain/step.ts
CHANGED
|
@@ -19,6 +19,19 @@ import { Context, Effect, Schema } from "effect";
|
|
|
19
19
|
export const Tree = Schema.Struct({ path: Schema.String, commit: Schema.String });
|
|
20
20
|
export type Tree = typeof Tree.Type;
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* How far a checkout still landing has got, as the plane answers a caller that takes it (`progress`): the plane goes on
|
|
24
|
+
* with it apart from the call, and the next call joins it. `received` of `size` bundle bytes are on the machine.
|
|
25
|
+
*/
|
|
26
|
+
export const CheckoutLanding = Schema.TaggedStruct("Landing", {
|
|
27
|
+
stage: Schema.Literals(["bundling", "sending", "checking-out"]),
|
|
28
|
+
received: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), size: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
29
|
+
});
|
|
30
|
+
export type CheckoutLanding = typeof CheckoutLanding.Type;
|
|
31
|
+
/** A checkout call's answer: its tree, or, for a caller that takes it, how far it has got. */
|
|
32
|
+
export const CheckoutAnswer = Schema.Union([Tree, CheckoutLanding]);
|
|
33
|
+
export type CheckoutAnswer = typeof CheckoutAnswer.Type;
|
|
34
|
+
|
|
22
35
|
/** What a command did: plain data, no methods. */
|
|
23
36
|
export const Ran = Schema.Struct({
|
|
24
37
|
exit: Schema.Int,
|
|
@@ -18,7 +18,7 @@ import type { AnyStage, StageSpec } from "./stage";
|
|
|
18
18
|
* Act, one-time and outward: wake, checkRun, mergePullRequest, post, call.
|
|
19
19
|
* They go through the act outbox, keyed, with a lease, settled by reading.
|
|
20
20
|
* Seen, read only: output, verdict, ticket, children, fact, judged, pinned,
|
|
21
|
-
* lifecycle. App-specific questions become facts or job outputs.
|
|
21
|
+
* lifecycle, placeable. App-specific questions become facts or job outputs.
|
|
22
22
|
*
|
|
23
23
|
* A rule routes one fact: it returns ticket(stage, key, spec) to file or
|
|
24
24
|
* update a ticket, and acts.
|
|
@@ -73,7 +73,7 @@ export interface Child<S extends AnyStage = AnyStage> {
|
|
|
73
73
|
export const child = <S extends AnyStage>(stage: S, key: string, spec: StageSpec<S>): Child<S> => ({ _tag: "Child", stage, key, spec });
|
|
74
74
|
|
|
75
75
|
export interface Done { readonly _tag: "Done"; readonly note: string }
|
|
76
|
-
/** This ticket's work is done. */
|
|
76
|
+
/** This ticket's work is done; its note is what the ticket says it did (its why). */
|
|
77
77
|
export const done = (note: string): Done => ({ _tag: "Done", note });
|
|
78
78
|
|
|
79
79
|
export interface Hold { readonly _tag: "Hold"; readonly reason: string }
|
|
@@ -176,6 +176,12 @@ export interface Seen<W extends Work = Work> {
|
|
|
176
176
|
readonly judged: <A>(question: Question<A>) => Judgment<A>;
|
|
177
177
|
readonly pinned: (name: string) => Schema.Json | undefined;
|
|
178
178
|
readonly lifecycle: Readonly<{ matches: (state: string) => boolean }>;
|
|
179
|
+
/**
|
|
180
|
+
* Whether a computer of the factory's fleet can run this job now, room aside: one that offers its os, arch and uses
|
|
181
|
+
* is up, and admits this factory's work of its kind there (a command on the computer itself, or a sealed VM).
|
|
182
|
+
* Undefined while the fleet cannot be read. A wanted job no computer can run waits; compose may leave it out instead.
|
|
183
|
+
*/
|
|
184
|
+
readonly placeable: <J extends W & AnyJob>(job: J) => boolean | undefined;
|
|
179
185
|
}
|
|
180
186
|
|
|
181
187
|
/** What a rule may read while it routes one fact. */
|
|
@@ -158,6 +158,8 @@ export interface Encoder {
|
|
|
158
158
|
readonly ref: <T extends NodeTag>(to: T, value: Slot<T>) => Ref<T>;
|
|
159
159
|
/** A problem with this node's settings, pinned to it. */
|
|
160
160
|
readonly problem: (message: string) => void;
|
|
161
|
+
/** A note on this node that holds nothing, pinned to it: its code still runs as written. */
|
|
162
|
+
readonly notice: (message: string) => void;
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
/** The rows a floor reads, by the node each belongs to. The records name their nodes by id. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
-
import { AUTHORED, authoredHead, head, type Authored, type LiveRows } from "./node";
|
|
2
|
+
import { AUTHORED, authoredHead, head, type Authored, type Encoder, type LiveRows } from "./node";
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
5
|
* A pool names a provider and an optional narrowing, nothing else.
|
|
@@ -22,6 +22,7 @@ export const RETIRED_PROVIDERS = ["macs", "thinair-cloud", "namespace"] as const
|
|
|
22
22
|
export const Provider = Schema.Literals([...PROVIDERS, ...RETIRED_PROVIDERS]);
|
|
23
23
|
export type Provider = typeof Provider.Type;
|
|
24
24
|
export type AuthoredProvider = (typeof PROVIDERS)[number];
|
|
25
|
+
export type RetiredProvider = (typeof RETIRED_PROVIDERS)[number];
|
|
25
26
|
|
|
26
27
|
/** Whether a pool names the owner's fleet: `fleet`, or a retired name that meant part of it. */
|
|
27
28
|
export const isFleet = (provider: Provider) => provider === "fleet" || provider === "macs" || provider === "thinair-cloud";
|
|
@@ -38,10 +39,12 @@ export type Narrowing = typeof Narrowing.Type;
|
|
|
38
39
|
export const Node = Schema.TaggedStruct("Pool", { ...head, provider: Provider, ...Narrowing.fields });
|
|
39
40
|
export type Node = typeof Node.Type;
|
|
40
41
|
|
|
41
|
-
export interface AuthoredPool<P extends AuthoredProvider = AuthoredProvider> extends Authored<"Pool",
|
|
42
|
+
export interface AuthoredPool<P extends AuthoredProvider = AuthoredProvider> extends Authored<"Pool", string> {
|
|
42
43
|
readonly provider: P;
|
|
43
44
|
readonly os: ReadonlyArray<Os>;
|
|
44
45
|
readonly arch: ReadonlyArray<Arch>;
|
|
46
|
+
/** The retired constructor that made it, when one did: it runs as the fleet, and the load says so. */
|
|
47
|
+
readonly retired?: RetiredProvider;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
type Narrow = Readonly<{ os?: ReadonlyArray<Os>; arch?: ReadonlyArray<Arch> }>;
|
|
@@ -53,6 +56,20 @@ const made = <const P extends AuthoredProvider>(provider: P, narrow: Narrow | un
|
|
|
53
56
|
arch: narrow?.arch ?? [],
|
|
54
57
|
});
|
|
55
58
|
|
|
59
|
+
/**
|
|
60
|
+
* A retired constructor, kept so code written before one fleet still loads and its passes still run: it makes the
|
|
61
|
+
* fleet with the same narrowing, under its own name (`pool:macs`), so its node and every Ref to it stay as they were.
|
|
62
|
+
*/
|
|
63
|
+
const retired = (name: RetiredProvider) => (narrow?: Narrow): AuthoredPool<"fleet"> => ({
|
|
64
|
+
[AUTHORED]: authoredHead("Pool", name, false),
|
|
65
|
+
provider: "fleet",
|
|
66
|
+
os: narrow?.os ?? [],
|
|
67
|
+
arch: narrow?.arch ?? [],
|
|
68
|
+
retired: name,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const RETIRED_CALL: Readonly<Record<RetiredProvider, string>> = { macs: "pool.macs()", "thinair-cloud": "pool.thinairCloud()", namespace: "pool.namespace()" };
|
|
72
|
+
|
|
56
73
|
/** The pools a factory can name. Each is named after its provider: `pool:fleet`. */
|
|
57
74
|
export const pool = {
|
|
58
75
|
/**
|
|
@@ -64,9 +81,19 @@ export const pool = {
|
|
|
64
81
|
fleet: (narrow?: Narrow) => made("fleet", narrow),
|
|
65
82
|
/** GitHub Actions runners, dispatched with the Agents App. */
|
|
66
83
|
githubActions: (narrow?: Narrow) => made("github-actions", narrow),
|
|
84
|
+
/** @deprecated The fleet: use `pool.fleet()`. It makes the fleet with this narrowing, and the load says so. */
|
|
85
|
+
macs: retired("macs"),
|
|
86
|
+
/** @deprecated The fleet, which Thinair Cloud joins once the owner names the factory's Cloud Organization: use `pool.fleet()`. */
|
|
87
|
+
thinairCloud: retired("thinair-cloud"),
|
|
88
|
+
/** @deprecated Never had a filling: use `pool.fleet()`. It makes the fleet with this narrowing, and the load says so. */
|
|
89
|
+
namespace: retired("namespace"),
|
|
67
90
|
};
|
|
68
91
|
|
|
69
|
-
export const make = (authored: AuthoredPool
|
|
92
|
+
export const make = (authored: AuthoredPool, encoder: Encoder) => {
|
|
93
|
+
if (authored.retired !== undefined)
|
|
94
|
+
encoder.notice(`${RETIRED_CALL[authored.retired]} is pool.fleet() now: it runs as the fleet with the same narrowing. Rename it to pool.fleet().`);
|
|
95
|
+
return { provider: authored.provider, os: [...authored.os], arch: [...authored.arch] };
|
|
96
|
+
};
|
|
70
97
|
|
|
71
98
|
/** The machines placement can use now, from their own facts. */
|
|
72
99
|
export const live = (node: Node, rows: LiveRows) => {
|
|
@@ -67,7 +67,7 @@ export type EncodeOptions = Readonly<{
|
|
|
67
67
|
files: Readonly<Record<string, FileFacts>>;
|
|
68
68
|
}>;
|
|
69
69
|
|
|
70
|
-
export type Encoded = Readonly<{ program: Program | null; problems: ReadonlyArray<EncodeProblem> }>;
|
|
70
|
+
export type Encoded = Readonly<{ program: Program | null; problems: ReadonlyArray<EncodeProblem>; notices: ReadonlyArray<EncodeProblem> }>;
|
|
71
71
|
|
|
72
72
|
/** JSON with keys sorted at every level: two values are the same when this is. */
|
|
73
73
|
const canonicalJson = (value: unknown): string => JSON.stringify(value, (_key, item: unknown) =>
|
|
@@ -80,6 +80,7 @@ type Draft = Readonly<{ node: Readonly<Record<string, unknown>> & { readonly at:
|
|
|
80
80
|
/** Encode a factory's root, as `factory.ts` exports it, into its Program. */
|
|
81
81
|
export const encodeFactory = (root: unknown, options: EncodeOptions): Encoded => {
|
|
82
82
|
const problems: EncodeProblem[] = [];
|
|
83
|
+
const notices: EncodeProblem[] = [];
|
|
83
84
|
const drafts = new Map<NodeId, Draft>();
|
|
84
85
|
const encodedValues = new Map<object, NodeId>();
|
|
85
86
|
const visiting = new Set<object>();
|
|
@@ -117,6 +118,7 @@ export const encodeFactory = (root: unknown, options: EncodeOptions): Encoded =>
|
|
|
117
118
|
const encoder: Encoder = {
|
|
118
119
|
ref: (tag, slot) => encode(tag, slot, { node: id, at }),
|
|
119
120
|
problem: (message) => problems.push({ node: id, at, message }),
|
|
121
|
+
notice: (message) => notices.push({ node: id, at, message }),
|
|
120
122
|
};
|
|
121
123
|
const settings = (definitions[made.tag] as Definition<Tag>).make(value as never, encoder);
|
|
122
124
|
visiting.delete(value);
|
|
@@ -132,10 +134,10 @@ export const encodeFactory = (root: unknown, options: EncodeOptions): Encoded =>
|
|
|
132
134
|
|
|
133
135
|
if (!isAuthored(root) || headOf(root).tag !== "Factory") {
|
|
134
136
|
problems.push({ node: null, at: { file: "factory.ts", line: 1 }, message: "factory.ts's default export is the factory: export default defineFactory({ ... })." });
|
|
135
|
-
return { program: null, problems };
|
|
137
|
+
return { program: null, problems, notices };
|
|
136
138
|
}
|
|
137
139
|
encode("Factory", root, null);
|
|
138
|
-
if (broken) return { program: null, problems };
|
|
140
|
+
if (broken) return { program: null, problems, notices };
|
|
139
141
|
|
|
140
142
|
// A node's digest covers its content and, when it holds code, the import closure of its file,
|
|
141
143
|
// stopping at files that make other nodes: those nodes have digests of their own.
|
|
@@ -165,9 +167,9 @@ export const encodeFactory = (root: unknown, options: EncodeOptions): Encoded =>
|
|
|
165
167
|
const decoded = Schema.decodeUnknownResult(Program)({ version: LANGUAGE_VERSION, framework: options.framework, root: ROOT_ID, nodes });
|
|
166
168
|
if (Result.isFailure(decoded)) {
|
|
167
169
|
problems.push({ node: null, at: null, message: `The program does not decode: ${decoded.failure.message}` });
|
|
168
|
-
return { program: null, problems };
|
|
170
|
+
return { program: null, problems, notices };
|
|
169
171
|
}
|
|
170
172
|
for (const problem of graphProblems(decoded.success))
|
|
171
173
|
problems.push({ node: problem.node, at: (decoded.success.nodes[problem.node] as Node | undefined)?.at ?? null, message: problem.message });
|
|
172
|
-
return { program: decoded.success, problems };
|
|
174
|
+
return { program: decoded.success, problems, notices };
|
|
173
175
|
};
|
package/package.json
CHANGED