@thinair/factory 0.1.158 → 0.1.164

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 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: output.problems.map((problem) => finding(problem.step, problem.message, { node: problem.node, file: problem.at?.file ?? null, line: problem.at?.line ?? null })),
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.length === 0 ? "ready" : "held", program, holes: program === null ? [] : holesOf(program), findings: all, cached: false, steps };
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. */
@@ -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", P> {
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) => ({ provider: authored.provider, os: [...authored.os], arch: [...authored.arch] });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinair/factory",
3
- "version": "0.1.158",
3
+ "version": "0.1.164",
4
4
  "description": "The Thinair factory SDK: the language a factory's code is written in, and the loader that checks it.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",