@hatua/model 0.0.0 → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pedro Gomes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,47 @@
1
+ import { Step } from '@hatua/schema';
2
+ /**
3
+ * What the verb decides about a Step that does not exist yet.
4
+ *
5
+ * `regionsOf` reads the keys and never the verb, and that is load-bearing: a
6
+ * `handler:` on a `core.fork` is meaningless and has no runner, but refusing to
7
+ * draw it would make it undeletable rather than absent (`docs/handoff.md` §
8
+ * Flow map geometry). That rule answers "what does this Step nest". It cannot
9
+ * answer "what should a new one nest", because a Step written with neither key
10
+ * nests nothing at all — and the verb is the only thing that knows a `core.try`
11
+ * protects a body and falls back to a handler.
12
+ *
13
+ * The two questions are asked by different callers at different moments, which
14
+ * is why they are two functions and this one sits downstream of both the region
15
+ * vocabulary and the verbs.
16
+ */
17
+ /**
18
+ * The regions a Step of this verb is born carrying.
19
+ *
20
+ * Empty and present, never populated. An empty region is a Band on the map with
21
+ * a word over it and an insert point inside — `banded` in @hatua/layout draws
22
+ * one for exactly that reason — and that is the whole of what makes it somewhere
23
+ * a Step can be dropped. Without the key there is no region, so there is no
24
+ * frame, no `+`, and no way to put anything inside the container: a card that
25
+ * can never be filled in.
26
+ *
27
+ * `TRY_HAS_NO_BODY` and `LOOP_HAS_NO_BODY` still report against the result. That
28
+ * is the point — the Step is unfinished, and these are the frames it gets
29
+ * finished in.
30
+ *
31
+ * `core.fork` is the one verb whose regions are not empty lists, because a
32
+ * Branch carries a label and a condition of its own. It is still born with two
33
+ * of them: CONTEXT.md defines a Fork as holding two or more, one Branch is the
34
+ * same path with a condition on it, and a Fork with none is a card with nothing
35
+ * inside it and no way to put anything there.
36
+ *
37
+ * Two, and a **condition** fork rather than a parallel one — `when: ''` on the
38
+ * first Branch is a condition nobody has written yet, and its absence on the
39
+ * second is what makes that one the fallback. A parallel fork is the shape that
40
+ * cannot be reached by editing one field, so the reachable shape is the one to
41
+ * be born in.
42
+ *
43
+ * The labels are rendered copy and reach an end user's screen, so they are
44
+ * written in the words on the screen rather than in this repository's
45
+ * (`.agents/rules/rendered-copy-is-written-for-the-hosts-users.md`).
46
+ */
47
+ export declare function bornRegionsOf(use: string): Pick<Step, 'steps' | 'handler' | 'branches'>;
@@ -0,0 +1,119 @@
1
+ import { Slot } from '@hatua/expressions';
2
+ import { Block, Step, WorkflowDefinition } from '@hatua/schema';
3
+ import { Diagnostic } from './diagnostic';
4
+ import { StepRef } from './tree';
5
+ /**
6
+ * Blocks: what a call takes, what a return publishes, and which Blocks reach
7
+ * which.
8
+ *
9
+ * A Block is invoked as `use: block.<id>` rather than by a verb of its own —
10
+ * ADR-0014 gives the verb namespace three roots, and "declared in this document"
11
+ * is one of them, so calling costs a namespace rather than a fourth structural
12
+ * verb (ADR-0013).
13
+ */
14
+ /** The root that says a verb names a Block in this document. */
15
+ export declare const BLOCK_PREFIX = "block.";
16
+ /** The verb that publishes a Block's declared outputs and ends it. */
17
+ export declare const RETURN_VERB = "core.return";
18
+ /** The Block a verb names, or null when it names something else. */
19
+ export declare const blockIdOf: (use: string) => string | null;
20
+ /** One Block by id. */
21
+ export declare const blockOf: (doc: WorkflowDefinition, id: string) => Block | undefined;
22
+ /**
23
+ * The Slots a call's `with:` map resolves into: one per declared parameter,
24
+ * typed by the declaration.
25
+ *
26
+ * Deliberately NOT routed through a synthesized Component Manifest. A manifest
27
+ * field carries a rendering `kind` and no type, so
28
+ * `slotsFor` recovers the expected type from `FIELD_KIND_TYPES` — and that
29
+ * vocabulary cannot express "a Template that must produce a boolean" at all,
30
+ * because `bool` holds a literal rather than a Template. Synthesizing a manifest
31
+ * would therefore have thrown away exactly the half of the contract the call
32
+ * site exists to check. A declaration's `t` IS the expected type, which is what
33
+ * a Slot has always been (CONTEXT.md): a Template together with the type it must
34
+ * produce.
35
+ *
36
+ * The rendering kind is a screen's problem and is derived where the screen is.
37
+ */
38
+ export declare const callSlots: (step: Step, block: Block) => Slot[];
39
+ /**
40
+ * The Slots a `core.return`'s `with:` map resolves into: one per declared
41
+ * output of the Block it sits on.
42
+ *
43
+ * `core.return` is the mirror of `core.map`. A mapping's *outputs* come from its
44
+ * own field values because no manifest can declare them; a return's *inputs*
45
+ * come from the enclosing Block's `outputs:` for the same reason — they are
46
+ * whatever that Block promised, and no manifest knows which Block a step is on.
47
+ */
48
+ export declare const returnSlots: (step: Step, block: Block) => Slot[];
49
+ /**
50
+ * Which Blocks a Block reaches directly, in document order.
51
+ *
52
+ * Reads the whole Board rather than its top level: a call nested inside a Fork
53
+ * branch, a loop body or a `core.try`'s handler still reaches, which is the
54
+ * entire point of asking. A region missing from this walk is a region recursion
55
+ * can hide in: the call graph comes out short an edge, the cycle is not found,
56
+ * and the document publishes.
57
+ */
58
+ export declare function callsOf(steps: readonly Step[]): string[];
59
+ /**
60
+ * The Blocks that take part in a cycle, by id.
61
+ *
62
+ * ADR-0013 refuses recursion because "unbounded recursion is the jump problem
63
+ * wearing a contract's clothes" — so this is a design-time answer rather than a
64
+ * depth limit a runner discovers. Direct and indirect are one question: a Block
65
+ * that reaches itself through any chain is in a cycle, and a colour-marked
66
+ * depth-first walk answers both without a second traversal.
67
+ */
68
+ export declare function cyclicBlocks(doc: WorkflowDefinition): Set<string>;
69
+ /**
70
+ * Every Step that calls a Block, on every Board including the Block's own.
71
+ *
72
+ * The other direction from `callsOf`, and it walks the whole document rather
73
+ * than one Board: a call sits wherever somebody wrote it, and a count that only
74
+ * looked at the root would tell a user deleting a Block that nothing calls it
75
+ * while two other Blocks do.
76
+ */
77
+ export declare function callSitesOf(doc: WorkflowDefinition, id: string): StepRef[];
78
+ /**
79
+ * What a Block takes and publishes, in a line: `1 param · 2 outputs`.
80
+ *
81
+ * One definition because two surfaces say it — the canvas's root node for the
82
+ * Board a Block opens, and the card that Board is reached from. Two spellings
83
+ * of the same count read as two different facts about one Block.
84
+ *
85
+ * An absent Block reads as a contract of nothing rather than as an empty
86
+ * string: a Board resolved against a document that no longer declares it still
87
+ * draws a node, and a summary that vanishes reads as a Board with no contract
88
+ * instead of one that is not there.
89
+ */
90
+ export declare const contractSummary: (block: Block | undefined) => string;
91
+ /**
92
+ * Every Block that will not run: one with a diagnostic on its own Board, and
93
+ * one that calls another such Block, however far down.
94
+ *
95
+ * A call is a **doorway** (ADR-0013), and a doorway says nothing about what is
96
+ * behind it. So a Board holding nothing but a clean-looking call can be a Board
97
+ * that cannot run, and the only way to find out is to walk through — which
98
+ * nobody does, because there is nothing on screen suggesting they should. That
99
+ * is the same invisibility a Reference naming nothing had before
100
+ * `validateDefinition` grew its expression family, one level of nesting up.
101
+ *
102
+ * **Derived, and never a diagnostic.** Every problem here is already reported,
103
+ * on the Board that holds it and by the rule that found it. Raising a second one
104
+ * at each call site would report one problem twice — so a Publish gate counting
105
+ * what it found would count the same fault once per doorway, and a Block called
106
+ * from five places would look five times as broken as one called from one.
107
+ * This says which Blocks are affected; the surfaces decide what to draw.
108
+ *
109
+ * **Transitive**, because the alternative is not honest. A Block whose own
110
+ * Steps are all fine but which calls a broken one is a Block that does not run,
111
+ * and marking only the immediate caller leaves every Board above it clean —
112
+ * with nothing to suggest walking down to the level that says so.
113
+ *
114
+ * Terminates on a document whose Blocks call each other, because a caller
115
+ * already in the set is not queued again. Such a document also has
116
+ * `BLOCK_RECURSION` against every Block on the cycle, so they are seeded here
117
+ * in any case.
118
+ */
119
+ export declare function troubledBlocks(doc: WorkflowDefinition, diagnostics: readonly Diagnostic[]): ReadonlySet<string>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ import { Manifest, WorkflowDefinition } from '@hatua/schema';
2
+ import { Diagnostic } from './diagnostic';
3
+ /**
4
+ * Connection rules. Two families, and they fail at different moments on purpose.
5
+ *
6
+ * The only rules here that cannot be answered from the Workflow Definition. A
7
+ * Connection stores an opaque `ref` and nothing else (ADR-0007), so what type it
8
+ * is comes from the Host — which makes absence a third answer these rules have
9
+ * to hold, distinct from "matches" and "does not match". ADR-0022 is why that
10
+ * answer is silence rather than a diagnostic.
11
+ */
12
+ type ManifestIndex = ReadonlyMap<string, Manifest>;
13
+ /** Takes flat manifests. Catalogues are flattened at load time, not here. */
14
+ export declare const indexManifests: (manifests: readonly Manifest[]) => ManifestIndex;
15
+ /**
16
+ * What the Host says each established Connection is, keyed by the opaque `ref` a
17
+ * Workflow Definition stores.
18
+ *
19
+ * A map rather than a `(ref) => type | undefined` function so that a missing
20
+ * entry means one thing and one thing only: the Host listed its Connections and
21
+ * this handle was not among them. A function returning `undefined` cannot say
22
+ * whether it was asked before the Host answered.
23
+ *
24
+ * **An empty map is an answer.** It says the Host has established no Connections
25
+ * — a fresh environment, and a legitimate one. `undefined` in its place is the
26
+ * other case entirely: nobody can describe them, because no `ConnectionSource`
27
+ * is wired or the one that is would not answer.
28
+ *
29
+ * The two must not look alike. Collapsed, every Connection in the workflow is
30
+ * CONNECTION_UNRESOLVABLE on first paint — so a workflow with nothing wrong with
31
+ * it cannot be published, and every `conn` field carries a sentence saying its
32
+ * Connection is gone. It clears when the port answers, and for a Host that wires
33
+ * no port it never clears at all.
34
+ */
35
+ export type ConnectionTypes = ReadonlyMap<string, string>;
36
+ /**
37
+ * A connection with no `ref` was never established. That blocks publish but not
38
+ * editing — you can lay out a whole workflow before wiring up its connections,
39
+ * and forcing the connection first would make the builder unusable on a fresh
40
+ * environment.
41
+ *
42
+ * Answered from the document alone, so it is reported whether or not anything
43
+ * can describe a Connection.
44
+ */
45
+ export declare function unresolvedConnections(doc: WorkflowDefinition): Diagnostic[];
46
+ /**
47
+ * A `conn` field offers only connections whose Host-reported type matches its
48
+ * `conn_type` — so a "send email" step is never handed an LLM connection. Both
49
+ * codes that decide it block editing, because unlike a missing connection
50
+ * neither is a legitimate intermediate state: each can only arise from a
51
+ * hand-edit.
52
+ *
53
+ * **`types` is optional, and its absence is not an error.** Handed nothing, this
54
+ * reports only what the document answers on its own — a field naming a
55
+ * Connection the document does not declare — and says nothing about the two
56
+ * questions that need a type. Reporting those anyway would say "no longer
57
+ * resolves" about every Connection in the workflow before the Host has spoken,
58
+ * which refuses Publish to a workflow with nothing wrong with it. See ADR-0022.
59
+ */
60
+ export declare function mismatchedConnections(doc: WorkflowDefinition, manifests: ManifestIndex, types?: ConnectionTypes): Diagnostic[];
61
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ import { DefinitionCode } from './generated/diagnostics';
2
+ /**
3
+ * One thing wrong with a Workflow Definition, and the vocabulary every rule
4
+ * family raises it in.
5
+ *
6
+ * Its own module because it belongs to no one family. A rule about a Fork's
7
+ * branches, a Reference naming nothing and a Connection whose type does not fit
8
+ * are all this shape, and putting the shape inside whichever file happened to
9
+ * need it first makes every other family import from that one.
10
+ */
11
+ export interface Diagnostic {
12
+ code: string;
13
+ message: string;
14
+ /** Where it surfaces: an unconnected connection must not block editing. */
15
+ blocks: 'edit' | 'publish';
16
+ stepId?: string;
17
+ /**
18
+ * Set instead of `stepId` when the subject is a Trigger.
19
+ *
20
+ * Separate because a Trigger is not a Step and the two are rendered by
21
+ * different regions: the Flow tab looks a Step's id up in `byStep`, and a
22
+ * Trigger's id filed there is either drawn by nobody or — if a hand-edited
23
+ * Trigger id happens to match a Step's — painted on that Step's row.
24
+ */
25
+ triggerId?: string;
26
+ /**
27
+ * Which Board the subject sits on: a Block's id, or absent for the root.
28
+ *
29
+ * Set ALONGSIDE `stepId`, not instead of it: a step id alone does not name one
30
+ * Step, because ids are Board-local — two Blocks may each hold a `ret`.
31
+ * Set on its own when the subject is the Block itself: "a path through this
32
+ * block can finish without returning" belongs to no Step in it.
33
+ */
34
+ blockId?: string;
35
+ /**
36
+ * Which Connection the diagnostic is about.
37
+ *
38
+ * Set ALONGSIDE `stepId` when a field's Connection is the thing at fault, so
39
+ * the diagnostic still files under the Step whose field the user can act on.
40
+ * Set on its own when the subject is the Connection itself — "this was never
41
+ * connected" belongs to no Step, because a declared Connection nothing uses
42
+ * is still unfinished.
43
+ */
44
+ connectionId?: string;
45
+ fieldKey?: string;
46
+ }
47
+ /**
48
+ * Fill a declared message's `{name}` holes from the fields a diagnostic carries.
49
+ *
50
+ * Exported because the generated table carries templates, and a Host reading
51
+ * `DEFINITION_DIAGNOSTICS[code].message` itself would get the literal braces.
52
+ * The Go SDK's `FormatDefinitionMessage` is the same function.
53
+ *
54
+ * A hole with no field keeps its braces rather than becoming empty: a sentence
55
+ * missing a word reads as a bug in Hatua, and one still holding `{label}` reads
56
+ * as a diagnostic raised without the field it names — which is what it is.
57
+ */
58
+ export declare const formatDefinitionMessage: (code: DefinitionCode, fields?: Record<string, string>) => string;
59
+ /** One diagnostic, taking `blocks` from the declaration rather than restating it. */
60
+ export declare const raise: (code: DefinitionCode, subject: Partial<Diagnostic>, fields?: Record<string, string>) => Diagnostic;
@@ -0,0 +1,43 @@
1
+ import { WorkflowExecution } from '@hatua/schema';
2
+ /**
3
+ * Reading a **Workflow Execution** against the definition it references.
4
+ *
5
+ * Here rather than in a region for the reason `regionsOf` is here: the card on
6
+ * the map, the pane beside it and any viewer a **Host** writes all ask the same
7
+ * two questions — which records belong to a Step, and what one status stands for
8
+ * them — and three answers to that is a card reading `failed` beside a pane
9
+ * reading `succeeded`.
10
+ */
11
+ /** One Step's record, as the schema shapes it. */
12
+ export type StepRecord = WorkflowExecution['steps'][number];
13
+ export type RunStatus = StepRecord['status'];
14
+ /**
15
+ * Every record a Step has in one execution.
16
+ *
17
+ * More than one when it sits inside a loop: `core.for_each` runs its children
18
+ * once per item, so a flat `stepId -> status` list cannot say "succeeded 23
19
+ * times and failed once", and the schema gives each pass its own record with its
20
+ * own nested results.
21
+ *
22
+ * The walk descends into iterations and nowhere else, because that is the only
23
+ * nesting an execution has — a **Fork**'s branches are nesting in the
24
+ * definition rather than in the record, and the Steps that ran inside one are
25
+ * reported flat.
26
+ */
27
+ export declare function recordsFor(execution: WorkflowExecution, stepId: string): StepRecord[];
28
+ /**
29
+ * The one status a Step's card shows, or null when the Step has no record at
30
+ * all.
31
+ *
32
+ * Null is a real answer and not a missing one: a Step inside a **Branch** that
33
+ * was not taken never ran, and the version the run is drawn against still holds
34
+ * it. An unmarked card is what says so.
35
+ */
36
+ export declare function statusOf(records: readonly StepRecord[]): RunStatus | null;
37
+ /** The log entries belonging to one Step, in the order the Host reported them. */
38
+ export declare function logFor(execution: WorkflowExecution, stepId: string): {
39
+ at: string;
40
+ message: string;
41
+ step?: string | undefined;
42
+ channel?: string | undefined;
43
+ }[];
@@ -0,0 +1,10 @@
1
+ import { Manifest, WorkflowDefinition } from '@hatua/schema';
2
+ import { ConnectionTypes } from './connections';
3
+ /** Mirrors the worked example in the plan, so tests and docs cannot drift apart. */
4
+ export declare const DOC: WorkflowDefinition;
5
+ export declare const MANIFESTS: Manifest[];
6
+ /**
7
+ * What the Host reports each established Connection is, keyed by the opaque ref
8
+ * a Workflow Definition stores — `ConnectionSource.listConnections` as a map.
9
+ */
10
+ export declare const CONNECTION_TYPES: ConnectionTypes;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * What a diagnostic stops. `publish` never blocks editing (ADR-0009);
3
+ * `edit` is reserved for what ordinary building cannot produce.
4
+ */
5
+ export type Blocks = 'edit' | 'publish';
6
+ export type DefinitionCode = 'FIELD_REQUIRED' | 'COMPONENT_UNKNOWN' | 'FORK_HAS_NO_BRANCHES' | 'FORK_NEEDS_TWO_BRANCHES' | 'BRANCH_UNREACHABLE_AFTER' | 'LOOP_HAS_NO_BODY' | 'TRY_HAS_NO_BODY' | 'TRY_HAS_NO_HANDLER' | 'LOOP_LIST_NOT_A_LIST' | 'REPEAT_HAS_NO_CONDITION' | 'VAR_UNKNOWN' | 'BLOCK_UNKNOWN' | 'BLOCK_RECURSION' | 'RETURN_OUTSIDE_BLOCK' | 'BLOCK_PATH_WITHOUT_RETURN' | 'STEP_AFTER_RETURN' | 'BLOCK_ID_DUPLICATE' | 'DECLARATION_KEY_DUPLICATE' | 'STEP_ID_DUPLICATE' | 'CONNECTION_NOT_ESTABLISHED' | 'CONNECTION_UNKNOWN' | 'CONNECTION_UNRESOLVABLE' | 'CONNECTION_TYPE_MISMATCH';
7
+ export interface DefinitionDiagnosticSpec {
8
+ readonly code: DefinitionCode;
9
+ readonly blocks: Blocks;
10
+ /** Template. `{name}` holes are filled by `formatDefinitionMessage`. */
11
+ readonly message: string;
12
+ }
13
+ /**
14
+ * `blocks` is part of the shared contract. A code that stopped Publish here
15
+ * and merely informed in Go would let a workflow publish from one builder
16
+ * and not another.
17
+ */
18
+ export declare const DEFINITION_DIAGNOSTICS: Record<DefinitionCode, DefinitionDiagnosticSpec>;
@@ -0,0 +1,11 @@
1
+ export * from './authoring';
2
+ export * from './blocks';
3
+ export * from './connections';
4
+ export * from './diagnostic';
5
+ export * from './execution';
6
+ export * from './metadata';
7
+ export * from './scope';
8
+ export * from './segment';
9
+ export * from './slots';
10
+ export * from './tree';
11
+ export * from './validity';