@vaudience/aigentflow-flow-validator 0.15.3
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 +21 -0
- package/PARITY.md +1247 -0
- package/README.md +214 -0
- package/dist/cli.js +5306 -0
- package/dist/index.cjs +5160 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +291 -0
- package/dist/index.d.ts +291 -0
- package/dist/index.js +5154 -0
- package/dist/index.js.map +1 -0
- package/package.json +76 -0
package/PARITY.md
ADDED
|
@@ -0,0 +1,1247 @@
|
|
|
1
|
+
# Parity with the AIgentFlow reference validator
|
|
2
|
+
|
|
3
|
+
This document maps every rule in this JavaScript validator back to the AIgentFlow
|
|
4
|
+
Go reference implementation, records the intentional divergences, and defines the
|
|
5
|
+
discipline for keeping the two in sync.
|
|
6
|
+
|
|
7
|
+
**Tracks AIgentFlow flow schema: `v2.802.0`** (`SPEC_VERSION` in [`src/spec/aigentflow-spec.json`](./src/spec/aigentflow-spec.json)), plus `server_owned_query_key` (package 0.15.1, below) from the AIgentFlow release after v2.790.0, and `credential_endpoint_unpaired` (package 0.15.2, below) from AIgentFlow v2.793.0 and the release after it, and `display_name_too_long` (package 0.15.3, below) from AIgentFlow v2.802.0.
|
|
8
|
+
|
|
9
|
+
> package 0.15.3 — **the optional top-level `display_name`** (AIgentFlow #187). The spec
|
|
10
|
+
> gains `display_name: scalar` on `Flow` and `specVersion` becomes `2.802.0`.
|
|
11
|
+
>
|
|
12
|
+
> | Rule (reference) | Code | Severity | Module |
|
|
13
|
+
> | --------------------------------------------------- | ----------------------- | -------- | ------------------------------ |
|
|
14
|
+
> | `validateBasicStructure`, display_name length bound | `display_name_too_long` | error | `validators/basicStructure.ts` |
|
|
15
|
+
>
|
|
16
|
+
> - A free-form label of at most 80 characters, counted in **Unicode code points** (Go's
|
|
17
|
+
> `utf8.RuneCountInString`), not UTF-16 units: 80 emoji pass here although they are 160
|
|
18
|
+
> UTF-16 units. Field `display_name`; absent or empty is fine.
|
|
19
|
+
> - A non-string scalar (`display_name: 123`) is read by its source text, as every other Go
|
|
20
|
+
> `string` field is; a mapping or list is not judged by this rule, as for `description`.
|
|
21
|
+
|
|
22
|
+
> package 0.15.2 — **an endpoint a server-supplied credential will never be sent to**, in
|
|
23
|
+
> step with the Go port (go-aigentflow-validator v0.6.2). The spec (new section
|
|
24
|
+
> `credentialEndpointPairing`; `specVersion` unchanged) and the ten new conformance
|
|
25
|
+
> fixtures are the Go port's byte for byte.
|
|
26
|
+
>
|
|
27
|
+
> | Rule (reference) | Code | Severity | Module |
|
|
28
|
+
> | ------------------------------------------------------------------------------------------------------------ | ------------------------------ | -------- | ------------------------------------------------------------------------------------------- |
|
|
29
|
+
> | `validateCredentialEndpointPairing` (DC-FORGE-231), `validateFamilyCredentialEndpointPairing` (DC-FORGE-233) | `credential_endpoint_unpaired` | warning | `validators/credentialEndpoint.ts`, `validators/goUrl.ts`, spec `credentialEndpointPairing` |
|
|
30
|
+
>
|
|
31
|
+
> - **Why.** AIgentFlow sends a credential the SERVER supplied (an org's or the
|
|
32
|
+
> platform's stored key, or a value of one of the deployment's environment variables)
|
|
33
|
+
> only to the endpoint that came with it: the default, the stored credential's own
|
|
34
|
+
> base URL, or a deployment-configured one. An endpoint the flow names is honoured only
|
|
35
|
+
> with a credential the flow supplies. The run-time refusal is the control; this
|
|
36
|
+
> warning names, at save, the shapes statically certain to meet it.
|
|
37
|
+
> - **ai://.** A step's (or loop sub-step's) `<provider>_base_url` with no key of the
|
|
38
|
+
> flow's own (the query's `api_key` / `<provider>_api_key`, or a literal
|
|
39
|
+
> `executor_config.<provider>.api_key`), and a literal
|
|
40
|
+
> `executor_config.<provider>.base_url` for an AI provider with no literal key beside
|
|
41
|
+
> it, unless at least one step uses the provider and every such step brings its own
|
|
42
|
+
> key. `ollama` and `vllm` are exempt.
|
|
43
|
+
> - **The executor families.** The reference's per-protocol table is spec data
|
|
44
|
+
> (`credentialEndpointPairing.families`), each row's server-variable set evaluated. A
|
|
45
|
+
> step warns when its secret (query first, then the family's `executor_config` block)
|
|
46
|
+
> is exactly `${NAME}` for one of the row's server variables on a row whose plugin
|
|
47
|
+
> expands references, and its endpoint (same order) is a literal: not templated, not a
|
|
48
|
+
> reference, and not of the same origin as the row's default. Implicit-credential
|
|
49
|
+
> families are not judged; `nexus://` and `aigentchat://` get the ai:// shape.
|
|
50
|
+
> - **The default-origin comparison needs Go's URL semantics**, so `validators/goUrl.ts`
|
|
51
|
+
> ports `net/url.Parse` (what it refuses, and the scheme and host it extracts),
|
|
52
|
+
> `strings.TrimSpace` (Go trims U+0085 and keeps U+FEFF) and `strings.ToLower` (simple
|
|
53
|
+
> case mapping: U+0130 is `i`). Its outputs are pinned against the Go port's.
|
|
54
|
+
> - **Measured** with the built CLI against the reference's own verdict on 465 files (the
|
|
55
|
+
> 403-file differential corpus of the Go port, plus 62 boundary probes): identical
|
|
56
|
+
> `(code, field)` pairs on the whole corpus (15 warnings, none on a bundled flow), and on
|
|
57
|
+
> every probe but four — two shared with the Go port (divergence 18) and two JS-only
|
|
58
|
+
> (below). Go `make parity-check` reports no drift.
|
|
59
|
+
|
|
60
|
+
> package 0.15.1 — **a step query may not declare a server-owned parameter**, in step
|
|
61
|
+
> with the Go port (go-aigentflow-validator v0.6.1). The spec (new section
|
|
62
|
+
> `serverOwnedQueryKeys`; `specVersion` unchanged) and the seven new conformance
|
|
63
|
+
> fixtures are the Go port's byte for byte.
|
|
64
|
+
>
|
|
65
|
+
> | Rule (reference) | Code | Severity | Module |
|
|
66
|
+
> | ----------------------------------------------------------------------- | ------------------------ | -------- | ----------------------------------------------------------------- |
|
|
67
|
+
> | `validateNoServerOwnedStepQueryKeys` / `IsServerOwnedParamKey` (CFX-05) | `server_owned_query_key` | error | `validators/serverOwnedQueryKeys.ts`, spec `serverOwnedQueryKeys` |
|
|
68
|
+
>
|
|
69
|
+
> - **Why.** The `aiv://` executor sends the running org's aigentverse credential, or
|
|
70
|
+
> a signed token naming the running person, to the base URL in its parameters, and
|
|
71
|
+
> the reference used to lay a step's `query:` over the credential resolver's output.
|
|
72
|
+
> A shared flow declaring `aiv_base_url: https://attacker.example` therefore chose
|
|
73
|
+
> where that credential went. The reference now discards such a value at run time
|
|
74
|
+
> and refuses it by name at save.
|
|
75
|
+
> - **Exactly as the reference.** The key set is data (`serverOwnedQueryKeys.keys`:
|
|
76
|
+
> `aiv_api_key`, `aiv_base_url`, `aiv_delegation`), so a new server-owned key is a
|
|
77
|
+
> spec change. Matching is exact and case-sensitive. The scanned surfaces are exactly
|
|
78
|
+
> the reference's two: a top-level step's `query:` (field `steps.<step>.query.<key>`)
|
|
79
|
+
> and a loop sub-step's `query:` (field `steps.<step>.loop.steps.<sub-step id>.query.<key>`,
|
|
80
|
+
> `stepId` the loop step). A parallel branch or a `for_each` body is a top-level step.
|
|
81
|
+
> Any value is refused, `null` and templates included; a key nested inside a query
|
|
82
|
+
> value, the name as a value, and a flow input parameter of that name are not.
|
|
83
|
+
> - **Measured** (in the Go port, with the reference at the rule's commit, over the 216
|
|
84
|
+
> bundled flows and every conformance fixture): identical `(code, field)` pairs, and
|
|
85
|
+
> no bundled flow trips the rule. Go `make parity-check` reports no drift. Three
|
|
86
|
+
> mutants here (the loop surface unscanned, the step surface unscanned, a folded
|
|
87
|
+
> case): all killed.
|
|
88
|
+
|
|
89
|
+
> v2.788.0, package 0.15.0 — **every save-door rule AIgentFlow added after v2.753.0,
|
|
90
|
+
> in step with the Go port (go-aigentflow-validator v0.6.0).** Since AIgentFlow
|
|
91
|
+
> v2.788.0 its save door and `/flows/validate` are one verdict, so "refuses what
|
|
92
|
+
> AIgentFlow's save refuses" is one target. The spec is the Go port's
|
|
93
|
+
> `spec/aigentflow-spec.json` byte for byte (`specVersion` 2.788.0; new sections
|
|
94
|
+
> `orchestratorToolAllow`, `exons`, `executorConfigEnvScopes`), and so are the 19
|
|
95
|
+
> new conformance fixtures and the one changed one.
|
|
96
|
+
>
|
|
97
|
+
> | Rule (reference) | Code | Severity | Module |
|
|
98
|
+
> | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ---------------------- | --------------------------------------------------- |
|
|
99
|
+
> | `end` is an ordinary step id to the reachability and cycle walks too (v2.760.0) | `unreachable_step` / `potential_infinite_loop` stop treating `end` as terminal | warning | spec `reachabilityTerminalMarkers` |
|
|
100
|
+
> | `ValidateExecutorConfigEnvScopes` (v2.597.0) | `executor_config_env_scope` | error | `validators/executorConfigEnv.ts` |
|
|
101
|
+
> | orchestrator `.exons` parse and spec presence (`validateOrchestrator`) | `orchestrator_exons_parse_failed` | error | `validators/exons.ts` |
|
|
102
|
+
> | orchestrator `.exons` `execution.provider` | `orchestrator_exons_no_provider` | error | `validators/exons.ts` |
|
|
103
|
+
> | orchestrator `requirements.resources` (v2.767.0) | `exons_resources_unhonoured` | error | `validators/exons.ts` |
|
|
104
|
+
> | a tool `orchestrator.tools` / `tools.allow` withholds (v2.760.0) | `orchestrator_tool_withheld` | warning | `validators/exons.ts`, spec `orchestratorToolAllow` |
|
|
105
|
+
> | inline `.exons` intake, on a step and on the orchestrator (v2.777.0) | `exons_attributes` | error, **engine only** | not ported (divergence #16) |
|
|
106
|
+
>
|
|
107
|
+
> - **`executor_config_env_scope`.** `api_key`, `base_url` and `organization` are Go
|
|
108
|
+
> `string` fields and are read by source text; an `extra` value is checked only when
|
|
109
|
+
> it is a YAML string. A reference is exactly `${NAME}` with a non-empty name, so
|
|
110
|
+
> `${A}${B}` is the single name `A}${B`. The scope table is the reference's own
|
|
111
|
+
> function evaluated for every key that has one (38 keys); any other key refuses every
|
|
112
|
+
> reference, and is looked up as an own property, so `constructor:` is an ordinary key.
|
|
113
|
+
> - **The frontmatter reader** follows the engine's extraction: an optional BOM, then
|
|
114
|
+
> spaces or tabs, then `---` and LF or CRLF; the frontmatter closes at the next line
|
|
115
|
+
> starting `---` followed by a line break or the end of the input, and is trimmed with
|
|
116
|
+
> Go's `TrimSpace` (which trims U+0085 and not U+FEFF). The retired `{~exons.config~}`
|
|
117
|
+
> block and an unclosed frontmatter are parse failures; no frontmatter, or an empty
|
|
118
|
+
> one, is `orchestrator_exons_parse_failed` too, because the reference refuses an
|
|
119
|
+
> orchestrator document without a spec. A frontmatter containing `{~` is not read at
|
|
120
|
+
> all (the engine executes it before decoding). The three fields are decoded as
|
|
121
|
+
> yaml.v3 decodes them non-strictly: an undeclared key is ignored, a declared one of
|
|
122
|
+
> the wrong shape fails the decode (and the reader then judges nothing), a number is
|
|
123
|
+
> its text, and a null list element is dropped.
|
|
124
|
+
> - **`orchestrator_tool_withheld`** is a warning, never an error: an unattended
|
|
125
|
+
> orchestrator is a legitimate design. `enable_signals` defaults to on (a YAML boolean
|
|
126
|
+
> only sets it), and a null `campaign:` is no campaign.
|
|
127
|
+
> - **Codes.** The reference refuses all of these at its strict parse with one generic
|
|
128
|
+
> code; each code here is the reference's own name for the rule where it has one, as
|
|
129
|
+
> in the Go port. The reference stops at the first refusal; this validator reports
|
|
130
|
+
> each. The verdict is the same.
|
|
131
|
+
>
|
|
132
|
+
> **Measured.** The Go port's `make parity-check` reports no drift: the spec and all 94
|
|
133
|
+
> fixtures are byte-identical, and the verdict and error codes agree on every fixture.
|
|
134
|
+
> Over the 235 flow files in AIgentFlow's repository the verdict, error codes and
|
|
135
|
+
> warning codes agree with the Go port's built-in reader on 235 of 235. A 39-shape
|
|
136
|
+
> orchestrator frontmatter matrix (the Go port's 15-shape engine-agreement matrix plus
|
|
137
|
+
> 24 decode shapes, each verdict measured on the Go port) is pinned in
|
|
138
|
+
> `test/v0150-save-door.test.ts`; 32 of the 39 get the engine's own verdict, and the
|
|
139
|
+
> other 7 are divergence #16, all looser. 16 mutants of the new rules (each guard
|
|
140
|
+
> removed or inverted, the scope table emptied, the signal exemption dropped, the
|
|
141
|
+
> templated-frontmatter skip removed, the `end` marker restored): all killed.
|
|
142
|
+
|
|
143
|
+
> v2.753.0, package 0.14.1 — **a number where the reference expects text is that
|
|
144
|
+
> text, in every Go `string` field.** No grammar change, so `specVersion` stays
|
|
145
|
+
> `2.753.0`. The reference decodes a step reference, an enum, a name and a
|
|
146
|
+
> duration into a Go `string`, and yaml.v3 fills a `string` from any scalar by
|
|
147
|
+
> its **source text**: `next: { default: 2 }` names the step `"2"`, `action: 1`
|
|
148
|
+
> is the invalid action `"1"`, `name: 123` is the name `"123"`. This validator
|
|
149
|
+
> read most of those fields with `isString`, so a number was either
|
|
150
|
+
> **invisible** (an existence or enum check skipped: looser) or **absent** (a
|
|
151
|
+
> present value reported missing: stricter). Ported from the Go port's v0.5.1,
|
|
152
|
+
> whose verdicts were measured on the reference's strict save parser.
|
|
153
|
+
>
|
|
154
|
+
> | Shape | Reference | 0.14.0 | 0.14.1 |
|
|
155
|
+
> | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | --------------------------------------------------------------------- | ------------------------------------------------ |
|
|
156
|
+
> | `next.default: 2`, a condition's `goto: 7`, `rendezvous: 9`, `error_strategy.goto_step: 9`, no such step | refused, step not found | valid (default, goto); `missing_required_field` / `goto_step_missing` | `step_not_found` |
|
|
157
|
+
> | the same with the step present, and `parallel.steps: [1]` | saves, reachable | `unreachable_step`; `step_not_found` / `goto_step_missing` | valid, reachable |
|
|
158
|
+
> | `default: 1e3` with a step `1000` | refused (`1e3` is not `1000`) | valid | `step_not_found` |
|
|
159
|
+
> | step keys `1e3:`, `0x1F:`, `True:` named by the same spelling | saves | `unreachable_step` (keys read `1000`, `31`, `true`) | valid |
|
|
160
|
+
> | `action: 1`, `on_fail: 1`, `for_each.resolution: 1`, `orchestrator.mode: 1` | refused, invalid value | valid | the rule's own code |
|
|
161
|
+
> | `name: 123`, `aigentflow_version: 2.0`, `start: 1`, `rubric: 5`, `for_each.items: 5`, `inject_as: 5`, a `query` param/property `type: 1` | saves | `missing_required_field` / the rule's "missing" code | valid (`unknown_data_type` warning for the type) |
|
|
162
|
+
> | loop sub-steps `id: 3` / `id: 4`, `next: { default: 4 }` | saves | `loop_step_id_required`, `invalid_type` | valid |
|
|
163
|
+
> | `executor: 42` | refused, unusable executor URL | `invalid_type` | `invalid_executor_url` |
|
|
164
|
+
> | `expression_functions: [{ function: 5 }]` | refused, not in the catalog | `invalid_expression_function` | `expression_function_unknown` |
|
|
165
|
+
>
|
|
166
|
+
> - **One reader.** Every Go `string` field is read through `Issues.stringAt` /
|
|
167
|
+
> `stringOf` / `nonEmptyStringOf` (`validators/util.ts`), which return a string
|
|
168
|
+
> as itself and a number or boolean by its source spelling. Only a mapping, a
|
|
169
|
+
> list or null is "not a string". The `Issues` collector now carries the
|
|
170
|
+
> pass's `scalarSources`, so the per-validator `sources` parameters are gone.
|
|
171
|
+
> Sites are left alone where the value is compared only against a literal no
|
|
172
|
+
> scalar spelling can equal (`orchestrator`, an `fn_` name, `{{`, an empty
|
|
173
|
+
> `output:` entry), and a step `query:` is a Go `any` in the reference, not a
|
|
174
|
+
> string field.
|
|
175
|
+
> - **The text comes from the parsed node, not the JS value.** The `yaml`
|
|
176
|
+
> package resolves `1.0` to 1, `0x1F` to 31, `True` to true and `-0` to -0
|
|
177
|
+
> before `toJS`. `parseFlow` reads `Scalar.source`, the scalar's own text,
|
|
178
|
+
> from the document node for each value (as `scalarSources` did for
|
|
179
|
+
> durations) and now for each **key** too: `retagScalarKeys` replaces a number
|
|
180
|
+
> or boolean key's value with its source text before conversion, so `1e3:` is
|
|
181
|
+
> the step `"1e3"`. Duplicate keys are compared by text, as yaml.v3 compares
|
|
182
|
+
> them: `1:` and `1.0:` are two steps, `"1":` and `1:` are one.
|
|
183
|
+
> `test/scalar-text.test.ts` pins the same 15 spellings the Go port pins
|
|
184
|
+
> against yaml.v3 (`2`, `-0`, `+1`, `017`, `0o17`, `0x1F`, `1_000`, `1e3`,
|
|
185
|
+
> `.5`, `0.10`, `.inf`, `true`, `True`, `FALSE`, `2024-01-01`) as a step key
|
|
186
|
+
> and as a value.
|
|
187
|
+
> - **Timestamps need nothing here.** The `yaml` package's default YAML 1.2
|
|
188
|
+
> core schema has no timestamp type, so `2024-01-01` is already the string
|
|
189
|
+
> `"2024-01-01"`, which is what the reference's `string` field receives.
|
|
190
|
+
> `1_000` and `017` differ between the two parsers as numbers, but both
|
|
191
|
+
> parsers keep the text, which is all a `string` field reads.
|
|
192
|
+
> - **Measured.** The Go port's ten `*-numeric-*` conformance fixtures are
|
|
193
|
+
> copied byte-for-byte, and `valid-open-key-sets.yaml` now forbids
|
|
194
|
+
> `unreachable_step`. On 0.14.0, 10 of those 11 cases fail; on 0.14.1 all 75
|
|
195
|
+
> fixtures pass, and the Go port's `make parity-check` (spec byte-identical,
|
|
196
|
+
> fixture set identical, 75 verdicts equal) is green. Over the reference's
|
|
197
|
+
> 216 bundled flows, not one finding changed, with or without
|
|
198
|
+
> `strictRegistries` (`unreachable_step` 25, `potential_infinite_loop` 1).
|
|
199
|
+
|
|
200
|
+
> v2.753.0 — **the three gaps that were looser than the reference in both ports
|
|
201
|
+
> are closed (package 0.14.0).** The reference's parser, validator, flow, step,
|
|
202
|
+
> orchestrator, campaign, input-schema and template-registry sources have a
|
|
203
|
+
> **zero** diff from `v2.738.0` to `v2.753.0`, so `specVersion` moves to
|
|
204
|
+
> `2.753.0` with no other rule change. Every verdict below was measured on the
|
|
205
|
+
> reference's save door (`NewStrictFlowParser().ParseFromYAMLBytes`, then
|
|
206
|
+
> `ValidateFlowWithDetails`) over a 133-shape probe matrix. Both ports now match
|
|
207
|
+
> the reference on all 133, and on all 65 conformance fixtures.
|
|
208
|
+
>
|
|
209
|
+
> 1. **`end` is not a sentinel at the save door.** `validateNextLogic` accepts
|
|
210
|
+
> only `null` and `orchestrator` without a step of that name, so
|
|
211
|
+
> `next: { default: end }` and a condition's `goto: end` are refused
|
|
212
|
+
> ("step_id (end) … not found") unless a step is called `end`. `End`, a
|
|
213
|
+
> rendezvous `end`, a parallel member `end`, an `error_strategy.goto_step: end`
|
|
214
|
+
> and a `quality_gate.goto_step: end` were already refused on both sides.
|
|
215
|
+
> `next: end` (a scalar) fails to decode. **Divergence #2 is closed**, and it
|
|
216
|
+
> was hiding a real problem: 15 fixtures this suite called valid routed to
|
|
217
|
+
> `end`, and the reference refused all 15 (14 on `end` itself, one first on
|
|
218
|
+
> an unknown key). They now say `'null'`, as do the 8 invalid fixtures that
|
|
219
|
+
> used it (three loop-body fixtures were refused by the reference on `end`
|
|
220
|
+
> before it reached the rule they test).
|
|
221
|
+
>
|
|
222
|
+
> The reference's reachability and cycle walks still skip `end`, so a flow
|
|
223
|
+
> that routes to a real step named `end` saves, and that step is reported
|
|
224
|
+
> `unreachable_step`. That is reproduced: the spec now carries two sets,
|
|
225
|
+
> `nextMarkers` (`null`, `orchestrator`: the save door) and
|
|
226
|
+
> `reachabilityTerminalMarkers` (`null`, `end`, `orchestrator`: the walks).
|
|
227
|
+
>
|
|
228
|
+
> 2. **Unknown keys are refused at every level (`unknown_yaml_key`), and a value
|
|
229
|
+
> of the wrong kind is `invalid_type`.** See [Unknown keys and value
|
|
230
|
+
> kinds](#unknown-keys-and-value-kinds). The key sets are derived from the
|
|
231
|
+
> reference's own types, not hand-listed. On the 216 bundled flows the new
|
|
232
|
+
> rule fires **zero** times. `valid-branching.yaml` carried a nested
|
|
233
|
+
> `constraints:` block the reference refuses; its two keys now sit at the
|
|
234
|
+
> flow root, where the reference reads them.
|
|
235
|
+
> 3. **A number in a Go `string` duration field is judged by its source text.**
|
|
236
|
+
> yaml.v3 fills such a field with the scalar's text, so `delay: 0.0` arrives
|
|
237
|
+
> as `"0.0"` (no unit, refused). `parseFlow` now records the source spelling
|
|
238
|
+
> of every number and boolean scalar (`scalarSources`), and `validateFlow`
|
|
239
|
+
> uses it. Measured for a mock delay: `0`, `+0`, `-0`, `100ms` and `"0"` save;
|
|
240
|
+
> `0.0`, `00`, `0x0`, `.0`, `-0.0`, `0.`, `0o0`, `0_0`, `1.5`, `100`, `1e3`,
|
|
241
|
+
> `true` and `.inf` are refused. The same mechanism closes four more gaps the
|
|
242
|
+
> probe found in the same family, each of which skipped numbers entirely:
|
|
243
|
+
> `for_each.throttle.delay`, `throttle.batch_delay` and `error_strategy.max_delay`
|
|
244
|
+
> (`100`, `1.5`, `0.0` were accepted, and are refused by the reference) and an
|
|
245
|
+
> orchestrator timer `interval: 0` (refused here as "no interval"; the
|
|
246
|
+
> reference saves it). **Divergence #13 is narrowed** to `validateFlowObject`,
|
|
247
|
+
> which never sees source text.
|
|
248
|
+
>
|
|
249
|
+
> Two parse changes keep the unknown-key rule from refusing flows the reference
|
|
250
|
+
> saves: `merge: true` (yaml.v3 honours `<<: *anchor`, and without it `<<` was a
|
|
251
|
+
> literal key), and a step id written as a number (`1:`) was already fine here.
|
|
252
|
+
>
|
|
253
|
+
> Cross-check over the reference's `example_flows/` (216 files): with
|
|
254
|
+
> `strictRegistries` the verdict matches on all 216, and `unreachable_step`
|
|
255
|
+
> 25 = 25 and `potential_infinite_loop` 1 = 1. In default mode the same 10 files
|
|
256
|
+
> as before differ (unknown template functions warn here, divergence #4).
|
|
257
|
+
|
|
258
|
+
> v2.738.0 — **five older save-door refusals, and one false positive removed
|
|
259
|
+
> (package 0.13.0).** No grammar change, so `specVersion` stays `2.738.0`: every
|
|
260
|
+
> rule below predates it and was simply never ported. The Go port
|
|
261
|
+
> (`go-aigentflow-validator`) carried them first. Each was checked against the
|
|
262
|
+
> reference's **strict save parser** (`NewStrictFlowParser().ParseFromYAMLBytes`,
|
|
263
|
+
> then `ValidateFlowWithDetails`) with a refused shape and an accepted shape, and
|
|
264
|
+
> each invalid conformance fixture was re-run with only the offending value
|
|
265
|
+
> corrected, to confirm it then saves.
|
|
266
|
+
>
|
|
267
|
+
> 1. **`reserved_step_id_orchestrator`** (v2.484.0, DC-COND-1). A top-level step
|
|
268
|
+
> may not be called `orchestrator`: it is the engine's step id for
|
|
269
|
+
> orchestrator signals and a reserved `next:` marker. Case-sensitive
|
|
270
|
+
> (`Orchestrator` saves), top-level steps only. `basicStructure.ts`.
|
|
271
|
+
> 2. **`tool_discovery_invalid`** (DC-FORGE-48). On the flow root, the
|
|
272
|
+
> orchestrator, and a step `query`, the value must be `eager`, `lazy` or
|
|
273
|
+
> `off`. Empty, null and templated values are skipped. The first two are Go
|
|
274
|
+
> `string` fields, which yaml.v3 fills from any scalar, so `tool_discovery: 5`
|
|
275
|
+
> or `true` is refused there. In a step `query` only a YAML string is judged.
|
|
276
|
+
> `saveDoor.ts`.
|
|
277
|
+
> 3. **`mock_delay_invalid`** (DC-FORGE-51). A `mock_scenarios.<scenario>.<step>.delay`
|
|
278
|
+
> must pass `time.ParseDuration`. `100` is refused and `100ms` saves. Every
|
|
279
|
+
> step key is checked, including one the flow does not define, because the
|
|
280
|
+
> reference walks the scenario map. `saveDoor.ts`. See divergence #13 for
|
|
281
|
+
> the one spelling family that cannot be told apart here.
|
|
282
|
+
> 4. **`output_param_empty`**. An `output:` entry may not be `''`. A YAML null
|
|
283
|
+
> entry is **not** refused: yaml.v3 drops null list entries while decoding,
|
|
284
|
+
> so the reference never sees it. `saveDoor.ts`.
|
|
285
|
+
> 5. **`campaign_no_child_flows`, `campaign_child_flow_no_id`**
|
|
286
|
+
> (`CampaignConfig.Validate`). `campaign.child_flows` needs at least one entry,
|
|
287
|
+
> and each entry needs a non-empty `flow_id` or `flow_name`. Null entries are
|
|
288
|
+
> dropped before the check, as in (4): `[~]` is empty, and a null beside a real
|
|
289
|
+
> entry is ignored. `flow_id: 123` names a flow (a string field). A
|
|
290
|
+
> non-list `child_flows` or a non-mapping entry is `invalid_type`.
|
|
291
|
+
> `orchestratorCampaign.ts`.
|
|
292
|
+
>
|
|
293
|
+
> The other `CampaignConfig.Validate` checks (`max_concurrent`, `max_depth`,
|
|
294
|
+
> `max_total_children` must each be `>= 1`) are **not ported, because no
|
|
295
|
+
> document can reach them**. `ApplyDefaults` runs first and replaces every
|
|
296
|
+
> value `<= 0` with a positive default. Measured: `max_concurrent: 0`,
|
|
297
|
+
> `-3`, `max_depth: 0.5` and `-1e30` all save.
|
|
298
|
+
>
|
|
299
|
+
> **False positive removed: `campaign.max_credits_per_child: 1.5` saves.**
|
|
300
|
+
> The field is a Go `int64`, and yaml.v3 decodes a float into an int field by
|
|
301
|
+
> truncating toward zero. Measured on the strict save parser: `1.5` is stored as
|
|
302
|
+
> `1`, `0.9` and `-0.5` as `0` (which then passes the `>= 0` check), and `1e3`
|
|
303
|
+
> as `1000`. All of them save. `-1`, `-1.5` and `-.inf` are refused by the
|
|
304
|
+
> `>= 0` check. A string (`"5"`), a boolean, `.nan`, `.inf`, and any float at or
|
|
305
|
+
> above 2^63 (`9.3e18`, `1e30`) fail to decode. Until now this validator refused
|
|
306
|
+
> every non-integer as `invalid_type`, which made it stricter than the door it
|
|
307
|
+
> predicts.
|
|
308
|
+
>
|
|
309
|
+
> **`parseGoDuration` is now a line-for-line port of `time.ParseDuration`**,
|
|
310
|
+
> using integer arithmetic with Go's overflow rules. The old version summed in
|
|
311
|
+
> floating point and was approximate near the int64 limit
|
|
312
|
+
> (`9223372036854775807ns` saves, `9223372036854775808ns` does not). This also
|
|
313
|
+
> affects every other duration rule that uses it (throttle, timer interval,
|
|
314
|
+
> `human_question_timeout`, step `max_duration`, error-strategy delays). None of
|
|
315
|
+
> their verdicts change outside that boundary.
|
|
316
|
+
>
|
|
317
|
+
> Cross-check over the reference's `example_flows/` (216 files; the reference
|
|
318
|
+
> run through its strict save parser, then `ValidateFlowWithDetails`):
|
|
319
|
+
> `unreachable_step` 25 = 25 and `potential_infinite_loop` 1 = 1, with identical
|
|
320
|
+
> `(file, field)` pairs. With `strictRegistries` the verdict matches on all 216
|
|
321
|
+
> files (206 valid on both sides). In default mode the same 10 files as before
|
|
322
|
+
> differ, for the reason given in the 0.12.0 note. 115 of the 216 files use
|
|
323
|
+
> `mock_scenarios`, `tool_discovery` or `child_flows`, and none of the new rules
|
|
324
|
+
> fires on any of them. The reference's parser, validator, flow, step, campaign,
|
|
325
|
+
> orchestrator, input-schema and template-registry sources are unchanged from
|
|
326
|
+
> `v2.738.0` to `v2.745.0`.
|
|
327
|
+
|
|
328
|
+
> v2.738.0 — **merge train (package 0.12.0).** The ten open parity PRs (#6–#12,
|
|
329
|
+
> #14–#16) landed together, and `specVersion` moved from `2.642.0` straight to
|
|
330
|
+
> `2.738.0`. Several of those PRs recorded a `specVersion` that deliberately
|
|
331
|
+
> understated, because each was cut from a `main` that lacked the others. That
|
|
332
|
+
> reconciliation is now done, and those notes are removed. The per-version notes
|
|
333
|
+
> below keep their original "no grammar change" wording as history.
|
|
334
|
+
>
|
|
335
|
+
> `2.738.0` is claimed because every grammar change between `v2.642.0` and
|
|
336
|
+
> `v2.738.0` that affects a static verdict was audited against the reference and
|
|
337
|
+
> is now ported, apart from the documented divergences. Three gaps turned up in
|
|
338
|
+
> review and are closed in the same train:
|
|
339
|
+
>
|
|
340
|
+
> 1. **`loop_sub_step_id_reserved`** (v2.648.0, warning). A loop sub-step whose
|
|
341
|
+
> id is `iterations`, `break`, `vars` or `duration_ms` collides with the loop
|
|
342
|
+
> result's own summary field, so `{{ .data.<loop>.<id> }}` reads the summary.
|
|
343
|
+
> Field `steps.<id>.loop.steps`, attributed to the parent step, as upstream.
|
|
344
|
+
> Like the rest of the loop-body walk, this validator checks every step, where
|
|
345
|
+
> the reference only walks steps reachable from `start`. It is a warning either
|
|
346
|
+
> way, so no verdict changes.
|
|
347
|
+
> 2. **`campaign.budget_max_per_child`** (v2.728.0, error). Retired for
|
|
348
|
+
> `max_credits_per_child`. The strict decoder refuses it and
|
|
349
|
+
> `retiredGrammarKeys` attaches advice, so it is reported as
|
|
350
|
+
> `unknown_yaml_key`, like the other retired keys in `retiredKeys.ts`.
|
|
351
|
+
> 3. **`campaign.max_credits_per_child`** (v2.728.0, error). It is a Go `int64`,
|
|
352
|
+
> so a non-integer fails to decode (`invalid_type`), and `CampaignConfig.Validate`
|
|
353
|
+
> refuses a negative value (`campaign_invalid_max_credits_per_child`). Absent,
|
|
354
|
+
> `null` and `0` mean "no per-child cap".
|
|
355
|
+
>
|
|
356
|
+
> One false positive was also fixed: `orchestrator.human_question_timeout: ""`
|
|
357
|
+
> (or `null`) was refused here, but the reference treats both as absent.
|
|
358
|
+
>
|
|
359
|
+
> The corpus cross-check then found two older defects, both fixed here:
|
|
360
|
+
>
|
|
361
|
+
> - **Seven orchestrator tool names were missing** from `orchestrator.tools`:
|
|
362
|
+
> `aif_memory_recall`, `aif_memory_reflect` and the five `aif_e2b_*` tools.
|
|
363
|
+
> The reference's `IsValidOrchestratorToolName` accepts all of them, so under
|
|
364
|
+
> `strictRegistries` this validator refused eight bundled flows the reference
|
|
365
|
+
> saves.
|
|
366
|
+
> - **Divergence #4 was not true for template functions.** By default the
|
|
367
|
+
> function lookup was skipped, so an unknown function produced no finding at
|
|
368
|
+
> all. It is now a `template_function_unknown` warning by default and an error
|
|
369
|
+
> under `strictRegistries`, as #4 says.
|
|
370
|
+
>
|
|
371
|
+
> Cross-check over the reference's `example_flows/` (216 YAML files, the reference
|
|
372
|
+
> run through `ValidateFlowWithDetails`): `unreachable_step` 25 = 25 and
|
|
373
|
+
> `potential_infinite_loop` 1 = 1, with identical `(file, field)` pairs. With
|
|
374
|
+
> `strictRegistries`, the valid/invalid verdict matches on all 216 files (206
|
|
375
|
+
> valid on both sides). In default mode the 10 files the reference refuses for an
|
|
376
|
+
> undefined template function are valid here and carry the warning instead.
|
|
377
|
+
>
|
|
378
|
+
> Still not ported, and older than this window: the rest of
|
|
379
|
+
> `CampaignConfig.Validate`. Ported in package 0.13.0 (note above), apart from
|
|
380
|
+
> the `>= 1` checks, which no document can reach.
|
|
381
|
+
|
|
382
|
+
> v2.648.0 — **the loop body is walked.** ⛔ The reference's `validateStepsInOrder`
|
|
383
|
+
> iterates `flow.steps`, and a `loop:` step's sub-steps live in a SECOND step table
|
|
384
|
+
> — so until v2.648.0 **no template and no operation shape inside a loop sub-step
|
|
385
|
+
> had been examined by either implementation.** This validator inherited the same
|
|
386
|
+
> blind spot and recorded it as a scope note, which is what that note was for.
|
|
387
|
+
>
|
|
388
|
+
> ⚠️ **Read it as a capability statement about the defects it PERMITS.** Turning
|
|
389
|
+
> the walk on in the reference produced ten syntax findings across five of its own
|
|
390
|
+
> shipped example flows, behind which sat five engine defects — including three
|
|
391
|
+
> template functions (`atoi`, `mod`, `int`) that five bundled flows used and the
|
|
392
|
+
> registry did not have. Those three are now in `templateFunctions`.
|
|
393
|
+
>
|
|
394
|
+
> 1. **Every loop-body finding is a WARNING, never an error.** ⛔ The reference
|
|
395
|
+
> consults this validator at its RUN door over flows stored long before the
|
|
396
|
+
> walk existed. ⚠️ The "the run already died anyway" licence that would make an
|
|
397
|
+
> Error safe requires the failing path to be UNCONDITIONAL, and inside a loop
|
|
398
|
+
> body four of the six evaluation sites SWALLOW a template failure and carry on
|
|
399
|
+
> with the raw string. The CODE survives the demotion; only the severity
|
|
400
|
+
> changes — dropping the finding would be worse than raising it.
|
|
401
|
+
> 2. **The dispatch set is a PARTITION, never a union.** `loop.set` / `loop.break`
|
|
402
|
+
> are dispatched by `executeLoopPostProcessing` and are legal ONLY on a loop
|
|
403
|
+
> sub-step; at the top level they remain undispatchable, and a merged set would
|
|
404
|
+
> silently accept a flow that fails at run time. `dispatchableOperationTypes`
|
|
405
|
+
> takes the scope as a parameter for exactly that reason, and both halves have
|
|
406
|
+
> their own fixture.
|
|
407
|
+
> 3. **Findings are attributed to the PARENT step id** — the id every other
|
|
408
|
+
> surface knows this work by — with the sub-step named by its index in the
|
|
409
|
+
> field path (`steps.<id>.loop.steps[i].…`), matching the reference.
|
|
410
|
+
>
|
|
411
|
+
> v2.647.0 — **a processing operation's SHAPE is now checked: the operation type,
|
|
412
|
+
> and the config keys its handler reads.** Both verdicts are **warnings** in the
|
|
413
|
+
> reference and here, which is the point of them: a flow carrying either mistake
|
|
414
|
+
> saves and runs. An operation type the engine has no dispatch case for fails at
|
|
415
|
+
> run time with "unsupported operation type"; a config key the handler never reads
|
|
416
|
+
> is simply ignored, so `transformation:` written where the handler reads
|
|
417
|
+
> `transformer:` produces a step that succeeds and does nothing. Ported into
|
|
418
|
+
> [`src/validators/processingOperations.ts`](./src/validators/processingOperations.ts),
|
|
419
|
+
> which also now owns the ONE decomposition of a processing operation —
|
|
420
|
+
> `templates.ts` shares it, so the `if:` guard is recognised in a single place.
|
|
421
|
+
>
|
|
422
|
+
> 1. **`unknown_processing_operation`.** The dispatchable set in a top-level
|
|
423
|
+
> step's `pre_processing:` / `post_processing:` is `data.set`,
|
|
424
|
+
> `conversation.append`, `output.set`, `binary.store`, `binary.get`,
|
|
425
|
+
> `binary.update`, `binary.delete`, `binary.transform`, `parse`. `loop.set` and
|
|
426
|
+
> `loop.break` are handled by a loop sub-step's post-processing before it
|
|
427
|
+
> delegates, so they are dispatchable **only there** — and are correctly
|
|
428
|
+
> reported as undispatchable at the top level. The two scopes are kept apart
|
|
429
|
+
> rather than merged for the same reason the key sets are (below): a name in
|
|
430
|
+
> the wrong half is the failure mode, and no merged set can see it.
|
|
431
|
+
>
|
|
432
|
+
> The operation type is the YAML **map key** (`- data.set: { … }`), not a
|
|
433
|
+
> field, so the finding addresses the operation itself
|
|
434
|
+
> (`steps.<id>.post_processing[0]`). Naming an `operation_type` field would
|
|
435
|
+
> send an author looking for something the grammar does not have. An unknown
|
|
436
|
+
> type also suppresses the key verdict beneath it: one verdict per defect.
|
|
437
|
+
>
|
|
438
|
+
> 2. **`unknown_processing_config_key`**, for the operations with a **closed** key
|
|
439
|
+
> set — `parse`, `binary.store`, `binary.get`, `binary.update`,
|
|
440
|
+
> `binary.delete`, `binary.transform`, `loop.break`. The sets are **per
|
|
441
|
+
> operation and are never unioned**: `asset_id` is read by
|
|
442
|
+
> `binary.get`/`binary.update`/`binary.delete` and is **not** read by
|
|
443
|
+
> `binary.transform`, which reads `source_asset_id`. A union over the
|
|
444
|
+
> operations accepts that key in the wrong half, which is precisely the
|
|
445
|
+
> mistake the rule exists to catch, so the spec models each operation
|
|
446
|
+
> separately and a conformance fixture pins the wrong-half case.
|
|
447
|
+
>
|
|
448
|
+
> `data.set`, `output.set`, `conversation.append` and `loop.set` have **open**
|
|
449
|
+
> key sets — every key is a name the author chose (a data key, an output key, a
|
|
450
|
+
> conversation id, a loop variable) — so "unknown key" is not a notion that
|
|
451
|
+
> applies to them and they can never produce this warning. That half is what
|
|
452
|
+
> makes the rule safe; asking them for a known key set would accuse every
|
|
453
|
+
> correct flow. The `if:` guard is a **sibling** of the operation key rather
|
|
454
|
+
> than a config key, except on `loop.break`, where `if` IS its config key.
|
|
455
|
+
>
|
|
456
|
+
> Scope, stated so it is not mistaken for more: **top-level config keys only**.
|
|
457
|
+
> Sub-keys of `metadata:` (forwarded to the asset store, which has its own
|
|
458
|
+
> vocabulary) and of `parameters:` (the transformer's, which varies per
|
|
459
|
+
> transformer) are out of scope in both implementations.
|
|
460
|
+
>
|
|
461
|
+
> Verified by running the reference validator over the new fixtures: identical
|
|
462
|
+
> codes and identical field paths, and the all-correct fixture produces neither
|
|
463
|
+
> code on either side.
|
|
464
|
+
>
|
|
465
|
+
> New conformance fixtures: `valid-processing-operation-unknown-type-warns.yaml`,
|
|
466
|
+
> `valid-processing-config-key-wrong-half-warns.yaml`,
|
|
467
|
+
> `valid-processing-config-keys-accepted.yaml`. The third needed a new
|
|
468
|
+
> `forbidWarningCodes` assertion in the conformance harness: a rule whose whole
|
|
469
|
+
> risk is a false positive needs a fixture that goes **red** when it fires, and
|
|
470
|
+
> `valid: true` does not say that, because a warning never changes the verdict.
|
|
471
|
+
|
|
472
|
+
> v2.652.0 (DC-FORGE-82; no grammar change, so `specVersion` stays `2.642.0`) —
|
|
473
|
+
> **one new static rule ported: `loop_substep_error_goto_ignored`.** A loop BODY is
|
|
474
|
+
> a second step table, and the walk that raises `unreachable_error_goto` never
|
|
475
|
+
> entered it — **on either side**; this validator had the same blind spot as the
|
|
476
|
+
> reference, for the same reason, which is why the finding ports rather than
|
|
477
|
+
> diverges.
|
|
478
|
+
>
|
|
479
|
+
> ⛔ **It is a DIFFERENT finding from `unreachable_error_goto`, and collapsing the
|
|
480
|
+
> two would be wrong.** That one is about a `goto_step` beside the wrong action,
|
|
481
|
+
> and its remedy is to write `action: "goto"`. Inside a loop body there is no
|
|
482
|
+
> action that helps: `executeLoopStep`'s failure switch has exactly two arms —
|
|
483
|
+
> `continue` (skip to the next sub-step) and a default that ABORTS the whole loop
|
|
484
|
+
> step — so `goto` lands in the default and the loop fails. **The strategy the
|
|
485
|
+
> author chose in order to avoid failing is the one that fails.** The message names
|
|
486
|
+
> the real remedy: put the `goto_step` on the LOOP step, whose own `error_strategy`
|
|
487
|
+
> is what the abort routes through.
|
|
488
|
+
>
|
|
489
|
+
> ⚠️ **Only the new warning is emitted over loop sub-steps — deliberately.** The
|
|
490
|
+
> reference's parser does not run its `error_strategy` checks (action enum, goto
|
|
491
|
+
> target existence, duration and backoff validation) over a loop body at all, so
|
|
492
|
+
> routing sub-steps through `validateOne` would make this oracle refuse shapes its
|
|
493
|
+
> door accepts — wrong in the more damaging direction.
|
|
494
|
+
>
|
|
495
|
+
> The fixture pair follows the `forbidWarningCodes` discipline established below.
|
|
496
|
+
> The counter-fixture puts a `goto_step` on the LOOP step beside `action: goto` —
|
|
497
|
+
> the exact shape the warning's own advice produces — so a rule that fired on any
|
|
498
|
+
> `goto_step` near a loop would make its own remedy warn. Both directions are
|
|
499
|
+
> mutation-verified: deleting the emission fails the positive fixture, dropping the
|
|
500
|
+
> `goto_step` presence guard fails the counter-fixture.
|
|
501
|
+
>
|
|
502
|
+
> The rest of v2.652.0 is out of scope: a composite step's `*StepError`, the
|
|
503
|
+
> one-hop `.step.error` carry into a for_each/loop handler, the loop sub-step
|
|
504
|
+
> strategy resolver and the unchecked `map[string]any` assertion are all engine
|
|
505
|
+
> runtime behaviour (divergence #3), not static grammar.
|
|
506
|
+
|
|
507
|
+
> v2.651.0 (DC-FORGE-81; no grammar change, so `specVersion` stays `2.642.0`) —
|
|
508
|
+
> **one new static rule ported: `unreachable_error_goto`.** The reference's engine
|
|
509
|
+
> switches on `error_strategy.action` and reads `goto_step` in the `goto` branch
|
|
510
|
+
> **only**; `retry`, `fail` and an absent action all fall through to failing the
|
|
511
|
+
> mission. Its parser has always checked that a `goto_step` names an existing step
|
|
512
|
+
> — but only once the action already was `goto`, so the one combination that
|
|
513
|
+
> silently does nothing was the one combination nothing asked about. ⛔ **Both of
|
|
514
|
+
> the reference's bundled example flows declared `action: "retry"` beside a
|
|
515
|
+
> `goto_step:` naming an error handler**, so neither handler was reachable, which
|
|
516
|
+
> is also why several broken `.step.error` reads inside those handlers had never
|
|
517
|
+
> been noticed — the steps containing them never ran.
|
|
518
|
+
>
|
|
519
|
+
> It is a **warning** on both sides, for the same reason: the reference consults
|
|
520
|
+
> this validator at its RUN door over flows that are already stored, and the
|
|
521
|
+
> declaration is INERT rather than fatal.
|
|
522
|
+
>
|
|
523
|
+
> ⭐ **The fixture pair is the point, and `forbidWarningCodes` is new here because
|
|
524
|
+
> of it.** This rule's entire risk is a FALSE POSITIVE, and neither `valid` nor
|
|
525
|
+
> `expectWarningCodes` can express that: a warning never changes the verdict, so an
|
|
526
|
+
> all-correct fixture stays green no matter how indiscriminately the rule fires.
|
|
527
|
+
> `valid-reachable-error-goto.yaml` forbids the code; without that assertion the
|
|
528
|
+
> suite passes with the rule's guard deleted. Both directions are mutation-verified.
|
|
529
|
+
>
|
|
530
|
+
> The rest of v2.649.0–v2.651.0 is out of scope: v2.649.0 is SPA styling, and
|
|
531
|
+
> v2.651.0's other five findings are runtime template-context behaviour
|
|
532
|
+
> (divergence #3), engine routing, or documentation surface.
|
|
533
|
+
|
|
534
|
+
> v2.672.0 (DC-FORGE-102, aigentflow#116; no grammar change, so `specVersion`
|
|
535
|
+
> stays `2.642.0`) — **three new static rules ported: a loop sub-step's `next:`
|
|
536
|
+
> targets are now checked.** A `loop:` sub-step may carry its own `next:` block,
|
|
537
|
+
> and its targets — `next.default` and every `next.conditions[].goto` — resolve
|
|
538
|
+
> ONLY against the sub-step ids of the SAME loop.
|
|
539
|
+
>
|
|
540
|
+
> ⛔ **Nothing checked them, on either side.** `validateNextLogic`,
|
|
541
|
+
> `validateOrchestratorNext` and this validator's connectivity pass all walk
|
|
542
|
+
> `flow.steps` only, and **a loop body is a SECOND step table** — the distinction
|
|
543
|
+
> this family of defects keeps being about. So `goto: pol` for a sub-step called
|
|
544
|
+
> `poll` saved clean, stored, and surfaced at run time as a WARN followed by a
|
|
545
|
+
> silent sequential advance: the branch the author wrote simply never happened,
|
|
546
|
+
> in the one construct whose entire purpose is branching.
|
|
547
|
+
>
|
|
548
|
+
> Three refusals, each with its own code, each a shape the runtime cannot act on:
|
|
549
|
+
>
|
|
550
|
+
> 1. **`loop_substep_next_target_not_found`** — the target names no sub-step of
|
|
551
|
+
> that loop. ⭐ **The interesting miss is a target that names a real TOP-LEVEL
|
|
552
|
+
> step**, which looks correct to every reader and to every previous check, and
|
|
553
|
+
> which the loop driver cannot see.
|
|
554
|
+
> 2. **`loop_substep_next_sentinel`** — the target is `null` or `orchestrator`.
|
|
555
|
+
> The reference's `resolveLoopSubStepNext` has no sentinel awareness at all, so
|
|
556
|
+
> `null` cannot end a mission from a loop body and `orchestrator` cannot yield;
|
|
557
|
+
> both take the same miss path as a typo. (This is also why the reference's
|
|
558
|
+
> `flowHasOrchestratorYieldEdge` skipping loop bodies is correct rather than a
|
|
559
|
+
> bug: the edge is unreachable, so the fix is to refuse WRITING it.)
|
|
560
|
+
> 3. **`loop_substep_next_parallel`** — the sub-step declares `next.parallel`. The
|
|
561
|
+
> loop driver reads conditions and default only, so the block never fans out
|
|
562
|
+
> and its rendezvous never runs.
|
|
563
|
+
>
|
|
564
|
+
> **Both jump directions stay legal** — the runtime sub-step index covers the
|
|
565
|
+
> whole loop, not only the sub-steps declared earlier — which is why the check
|
|
566
|
+
> runs as a SECOND pass over the already-collected id set, and why the valid
|
|
567
|
+
> fixture carries a forward jump AND a backward one. **An empty target is the
|
|
568
|
+
> documented "advance sequentially" and is left alone.**
|
|
569
|
+
>
|
|
570
|
+
> ⚠️ **`end` is reported as a missing target, not as a sentinel.** The reference
|
|
571
|
+
> names only `null` and `orchestrator` and lets everything else fall through to
|
|
572
|
+
> the existence check, and that is the honest answer inside a loop body, where
|
|
573
|
+
> nothing reads `end` either. (Since 0.14.0 the same is true of top-level next
|
|
574
|
+
> targets; divergence #2 is closed.)
|
|
575
|
+
>
|
|
576
|
+
> **Severity: all three are `error` here.** Upstream they are hard refusals at the
|
|
577
|
+
> SAVE door and downgraded to a warning on the stored-flow LOAD door, so a flow
|
|
578
|
+
> written before the rule keeps running exactly as degraded as it already was.
|
|
579
|
+
> This validator has no notion of doors and answers **"would this save"** — the
|
|
580
|
+
> same choice already made for the executor-URL shape and expression-function
|
|
581
|
+
> catalog rules. **No new severity was invented, and a refusal here does not imply
|
|
582
|
+
> a broken running flow.**
|
|
583
|
+
>
|
|
584
|
+
> ⭐ **`forbidErrorCodes` is new in the conformance harness.** `valid: true`
|
|
585
|
+
> already fails on any spurious error, so this is not the verdict-shaped hole
|
|
586
|
+
> `forbidWarningCodes` fills for a warning rule — it NAMES the rule under test, so
|
|
587
|
+
> a regression reads as "the loop-target rule fired on a legal backward jump"
|
|
588
|
+
> instead of as an anonymous count mismatch. All five mutants (drop the existence
|
|
589
|
+
> check, make an empty target illegal, drop the sentinel set, drop the parallel
|
|
590
|
+
> check, judge targets single-pass so a forward jump is refused) are killed by
|
|
591
|
+
> both a unit test and a conformance fixture.
|
|
592
|
+
>
|
|
593
|
+
> New conformance fixtures: `valid-loop-substep-next.yaml`,
|
|
594
|
+
> `invalid-loop-substep-next-unknown-target.yaml`,
|
|
595
|
+
> `invalid-loop-substep-next-sentinel.yaml`,
|
|
596
|
+
> `invalid-loop-substep-next-parallel.yaml`.
|
|
597
|
+
|
|
598
|
+
> DC-FORGE-145 (the release after v2.715.0; no grammar change, so `specVersion`
|
|
599
|
+
> stays `2.642.0`) — **one new static rule ported: `response_expectation_unread`.**
|
|
600
|
+
> The reference's engine reads a step's `response_expectation` only when
|
|
601
|
+
> `response_evaluation` is set; with no evaluation mode it returns the raw response
|
|
602
|
+
> untouched, so the expectation's `required`, `type` and `fallback` are never
|
|
603
|
+
> consulted. The reference measured **21 such steps in 9 of its own bundled flows**,
|
|
604
|
+
> including a requirement meant to fail an answer that had not searched.
|
|
605
|
+
> `async://` is exempt because its respond route validates the posted output
|
|
606
|
+
> against the expectation on its own. Loop sub-steps cannot declare an
|
|
607
|
+
> expectation, so — unlike `unreachable_error_goto` — there is no loop-body walk.
|
|
608
|
+
>
|
|
609
|
+
> It is a **warning** on both sides: the reference consults it at its RUN door over
|
|
610
|
+
> flows that are already stored, and the declaration is inert rather than fatal.
|
|
611
|
+
> The message text is the reference's `WARN_MSG_RESPONSE_EXPECTATION_UNREAD`
|
|
612
|
+
> verbatim, with Go's `%q` rendered as `JSON.stringify` (identical for printable
|
|
613
|
+
> ASCII, which is all a step id holds).
|
|
614
|
+
>
|
|
615
|
+
> Parity was measured, not assumed: over the reference's bundled corpus **before**
|
|
616
|
+
> its fix, this validator and the reference emit the **identical** 21
|
|
617
|
+
> `(flow, step)` findings; after it, both emit zero. Three counter-fixtures
|
|
618
|
+
> (`raw-text`, `async://`, absent/empty expectation) carry `forbidWarningCodes`, and
|
|
619
|
+
> each of the rule's four guards is mutation-verified against them.
|
|
620
|
+
|
|
621
|
+
> DC-FORGE-147 (no grammar change, so `specVersion` stays `2.642.0`) — **one new
|
|
622
|
+
> static rule ported, a warning: `step_max_duration_ignored`.** The reference now
|
|
623
|
+
> bounds each executor invocation of a step by its `max_duration`. Two
|
|
624
|
+
> declarations fall outside that, each warned on at `steps.<id>.max_duration`:
|
|
625
|
+
>
|
|
626
|
+
> - a value that is not `none`/`never`/`infinite` and does not parse as a Go
|
|
627
|
+
> `time.ParseDuration` string (`2d`, `5 minutes`, a `{{ template }}`, a bare
|
|
628
|
+
> `90`) — the engine runs that step with **no** time limit rather than fail a
|
|
629
|
+
> stored flow at its run door;
|
|
630
|
+
> - a parseable value on a `loop:` step, which makes no executor call of its own.
|
|
631
|
+
>
|
|
632
|
+
> Only top-level steps are walked: loop sub-steps have no `max_duration` field in
|
|
633
|
+
> the reference. `0s` and negative values parse (the engine treats them as no
|
|
634
|
+
> bound) and do not warn. Both messages are the reference's
|
|
635
|
+
> `WARN_MSG_STEP_MAX_DURATION_UNPARSEABLE` / `_LOOP` verbatim, with `%q` rendered
|
|
636
|
+
> as `JSON.stringify`. The reference sets no `StepID` on this warning, so neither
|
|
637
|
+
> does this port.
|
|
638
|
+
>
|
|
639
|
+
> The shared `parseGoDuration` helper was brought into line with Go on two edges
|
|
640
|
+
> it got wrong: `1.s` (digits on one side of the point suffice) now parses, and a
|
|
641
|
+
> value overflowing int64 nanoseconds (`2562048h`) no longer does. The unit test
|
|
642
|
+
> pins 28 strings against `go run` output. This also tightens `invalid_duration`
|
|
643
|
+
> on throttle `delay`/`batch_delay` and error-strategy delays, toward Go.
|
|
644
|
+
>
|
|
645
|
+
> Divergences, none of which changes a verdict: the reference's field is a Go
|
|
646
|
+
> `string`, so YAML decodes any scalar into it; this port reads a string or a
|
|
647
|
+
> finite number (as its decimal text) and ignores booleans/mappings. A YAML
|
|
648
|
+
> number's original spelling (`0x10`) is lost at parse here, so its text may
|
|
649
|
+
> differ in the message. The overflow boundary is float-approximate to within a
|
|
650
|
+
> few ns of 2^63.
|
|
651
|
+
|
|
652
|
+
> AIF DC-FORGE-150 (v2.721.0) — **three grammar keys deleted because nothing read them: the top-level
|
|
653
|
+
> `budget:`, the top-level `max_retries:`, and a `max_retries:` directly on a
|
|
654
|
+
> top-level step.** Measured inert on the reference's real engine before the
|
|
655
|
+
> deletion: a sub-cent `budget` completed a step that cost 2.0, and step
|
|
656
|
+
> `max_retries: 0` still made two attempts. The reference's save door parses with
|
|
657
|
+
> `KnownFields(true)`, so a flow declaring any of them is now **refused at save**,
|
|
658
|
+
> with migration advice from its `retiredGrammarKeys` table (parser.go); stored
|
|
659
|
+
> flows still load leniently and run.
|
|
660
|
+
>
|
|
661
|
+
> Ported as an **error** with the code this validator already uses for the other
|
|
662
|
+
> key the reference refuses specifically, `unknown_yaml_key` (the
|
|
663
|
+
> `next.conditions[].goto_step` case below) — new module
|
|
664
|
+
> [`src/validators/retiredKeys.ts`](./src/validators/retiredKeys.ts). Fields:
|
|
665
|
+
> `budget`, `max_retries`, `steps.<id>.max_retries`. The check is on key
|
|
666
|
+
> **presence**, not value: the strict decoder refuses the key whatever it holds, so
|
|
667
|
+
> `budget: 0`, `max_retries: 0` and `budget: null` are all refused. The message
|
|
668
|
+
> names the version, says AIgentFlow refuses to save it, and the suggestion names
|
|
669
|
+
> the replacement — `billing: { max_credits: N }` with its reach (it caps the
|
|
670
|
+
> credit reservation and refuses further agentic turns: agentic `ai://`,
|
|
671
|
+
> `exons://`, the orchestrator; plain chat steps and `flow://` sub-flows are not
|
|
672
|
+
> checked against it), or `error_strategy: { action: "retry", max_retries: N }`
|
|
673
|
+
> (N counts attempts, including the first).
|
|
674
|
+
>
|
|
675
|
+
> **Loop sub-steps are included** (`steps.<id>.loop.steps[<i>].max_retries`). The
|
|
676
|
+
> reference's loop sub-step type never had a `max_retries` field, so it was always
|
|
677
|
+
> refused there as an unknown key, independently of this deletion — and because
|
|
678
|
+
> the advice table matches the key NAME in the decoder error, not the type, the
|
|
679
|
+
> reference now attaches this same advice to it. Reporting it is therefore parity
|
|
680
|
+
> with "would this save", not an extension; it is the one location where this
|
|
681
|
+
> validator reports a generic unknown key, and it does so only because the
|
|
682
|
+
> reference names it.
|
|
683
|
+
>
|
|
684
|
+
> The false positive to guard is the working key one level down:
|
|
685
|
+
> `error_strategy.max_retries` (flow and step), `quality_gate.max_retries`,
|
|
686
|
+
> `billing.max_credits`, and the surviving `currency` / `max_duration`. The
|
|
687
|
+
> counter-fixture `valid-limit-keys-one-level-down.yaml` carries all of them;
|
|
688
|
+
> because this is an ERROR rule, `valid: true` (zero errors) is the proof and no
|
|
689
|
+
> `forbid*Codes` harness extension is needed. Four mutants were run and each was
|
|
690
|
+
> killed: presence→truthiness (skips `0`/`null`; 5 tests fail), the step rule also
|
|
691
|
+
> reading `error_strategy.max_retries` (2 fail), the budget rule also firing on a
|
|
692
|
+
> `billing:` block (2 fail), and not walking loop sub-steps (1 fails).
|
|
693
|
+
>
|
|
694
|
+
> `budget` and the flow-level `max_retries` are also removed from the exported
|
|
695
|
+
> `Flow` type (they were never on `StepDefinition`). `ErrorStrategyDefinition.
|
|
696
|
+
max_retries` and `QualityGateDefinition.max_retries` are unchanged. Removing
|
|
697
|
+
> public type fields is why the package version is a minor bump.
|
|
698
|
+
>
|
|
699
|
+
> **Supersedes the DC-FORGE-146 warnings** (`flow_budget_unenforced`,
|
|
700
|
+
> `max_retries_unread`) proposed in an open PR for the same three keys: the keys no
|
|
701
|
+
> longer exist, so there is nothing left to warn about.
|
|
702
|
+
>
|
|
703
|
+
> Divergence, none of which changes a verdict on a flow the reference would save:
|
|
704
|
+
> the reference refuses a `budget` or `max_retries` key at ANY depth where the
|
|
705
|
+
> containing type has no such field (the generic unknown-key rule, ported in
|
|
706
|
+
> 0.14.0 — see [Unknown keys and value kinds](#unknown-keys-and-value-kinds)).
|
|
707
|
+
> The four locations above keep their specific advice.
|
|
708
|
+
|
|
709
|
+
> v2.646.0 (no grammar change, so `specVersion` stays `2.642.0`) — **the reference
|
|
710
|
+
> finally validates `pre_processing:` / `post_processing:` templates at all.** Its
|
|
711
|
+
> `validateProcessingOperation` took `operation any` and asserted `map[string]any`
|
|
712
|
+
> while both call sites passed `*ProcessingOperationDefinition`, so it returned
|
|
713
|
+
> silently for the life of the repository. ⭐ **This validator has checked those
|
|
714
|
+
> blocks for syntax the whole time, which means the port was STRICTER than the
|
|
715
|
+
> reference it ports** — the opposite of the direction a mirror is usually wrong in,
|
|
716
|
+
> and invisible to both sides.
|
|
717
|
+
>
|
|
718
|
+
> What changed here: nothing about _what_ is reported, only _where_. The reference
|
|
719
|
+
> unmarshals a processing operation's single key into `OperationType` and its body
|
|
720
|
+
> into an inline `Config`, so it addresses findings as
|
|
721
|
+
> `steps.<id>.post_processing[0].<configKey>` and `steps.<id>.post_processing[0].if`.
|
|
722
|
+
> This validator walked the raw record and inserted the operation name as a path
|
|
723
|
+
> segment (`…post_processing[0].data.set.<configKey>`). The walker now matches the
|
|
724
|
+
> reference, pinned by a test that fails on the old address.
|
|
725
|
+
>
|
|
726
|
+
> The reference also gained `template_missing_field` coverage inside those blocks,
|
|
727
|
+
> grounded on the namespaces the processing handler actually builds — which is
|
|
728
|
+
> **narrower** than a step's own context (no `.loop`, no `.binary`, four of five
|
|
729
|
+
> `.step` sub-namespaces absent, and `.step.response` only after the step, holding
|
|
730
|
+
> the _evaluated_ response). That whole class stays out of scope here under
|
|
731
|
+
> **divergence #3**, unchanged.
|
|
732
|
+
|
|
733
|
+
> v2.642.0 — **`expression_functions:` stopped being inert.** The block had always
|
|
734
|
+
> been validated for SHAPE (exactly one key, `package` XOR `function`, non-empty
|
|
735
|
+
> value) and honoured in no other way: nothing read it, so a flow could declare
|
|
736
|
+
> functions and get none. It is now a real opt-in over a **fixed catalog of 20
|
|
737
|
+
> functions compiled into the AIgentFlow binary**. Three rules ported, all into
|
|
738
|
+
> [`src/validators/expressionFunctions.ts`](./src/validators/expressionFunctions.ts);
|
|
739
|
+
> the pre-existing structural rules are unchanged and still fire first.
|
|
740
|
+
>
|
|
741
|
+
> 1. **`package:` is refused** (`expression_function_package_unsupported`).
|
|
742
|
+
> Nothing is loaded at run time, ever — there is no safe way to load a package
|
|
743
|
+
> at run time, and flow YAML is reachable by any authenticated caller, so the
|
|
744
|
+
> catalog _is_ the security envelope. The message points at `function:` and
|
|
745
|
+
> lists the catalog.
|
|
746
|
+
> 2. **A `function:` outside the catalog is refused** (`expression_function_unknown`).
|
|
747
|
+
> The catalog is an enumerable, compile-time-linked set, which is why this is
|
|
748
|
+
> an error and not the lag-prone allow-list warning of divergence #4 — a name
|
|
749
|
+
> this validator does not know is a name AIgentFlow does not have.
|
|
750
|
+
> 3. **A template action calling an `fn_` name must name a catalog entry
|
|
751
|
+
> (`expression_function_unknown_use`) and be declared by the flow
|
|
752
|
+
> (`expression_function_undeclared_use`).** This is what makes the block
|
|
753
|
+
> load-bearing rather than decorative. Only the text between `{{` and `}}` is
|
|
754
|
+
> scanned, so a description mentioning `fn_slugify` in prose is not a call, and
|
|
755
|
+
> the action pattern is newline-tolerant so a multi-line action in a block
|
|
756
|
+
> scalar is matched whole. Every catalog name carries the `fn_` prefix
|
|
757
|
+
> precisely so a catalog entry can never shadow a standard function such as
|
|
758
|
+
> `index` or `default`.
|
|
759
|
+
>
|
|
760
|
+
> The 20 catalog names are also added to `templateFunctions`: the reference
|
|
761
|
+
> registers the whole catalog on its standard template registry unconditionally,
|
|
762
|
+
> so `{{ fn_round … }}` is a _known_ function there, and omitting them would make
|
|
763
|
+
> `{ strictRegistries: true }` report a second, wrong verdict on a correct flow.
|
|
764
|
+
>
|
|
765
|
+
> **Severity choice — the save door vs the run door.** Upstream, rules 1 and 2 are
|
|
766
|
+
> hard refusals when a flow is SAVED but only warnings when a stored flow is
|
|
767
|
+
> LOADED, so flows written before the rules existed keep running. This validator
|
|
768
|
+
> has no notion of doors and no severity meaning "refused at save, tolerated at
|
|
769
|
+
> run"; it answers **"would this save"**, which is the same choice already made
|
|
770
|
+
> for the executor-URL shape rule (also retroactive-tolerant upstream, also an
|
|
771
|
+
> error here). So all three rules are `error`, and no new severity was invented.
|
|
772
|
+
> **Consequence to know:** a flow this validator refuses may still be running in
|
|
773
|
+
> production — the refusal means it can no longer be saved unchanged, not that it
|
|
774
|
+
> is broken. Rule 3 is save-door-only upstream for a different reason (cost: the
|
|
775
|
+
> scan re-serialises the flow, and the load path runs on every mission start),
|
|
776
|
+
> which lands in the same place.
|
|
777
|
+
>
|
|
778
|
+
> **One deliberate scope difference (divergence #10).** The Go rule re-serialises
|
|
779
|
+
> the `Flow` struct, so it sees only fields that struct declares; this validator
|
|
780
|
+
> walks the parsed document, so it also sees keys the struct does not carry. On a
|
|
781
|
+
> flow AIgentFlow would accept the two are identical, because AIgentFlow's create
|
|
782
|
+
> path parses with `KnownFields(true)` and refuses anything else outright.
|
|
783
|
+
>
|
|
784
|
+
> **Correction to rule 3, made in the reference and ported here: a FIELD is not a
|
|
785
|
+
> CALL.** The first cut matched `\bfn_[A-Za-z0-9_]+`, and `\b` matches happily
|
|
786
|
+
> between the `.` and the `f` — so `{{ .data.fn_total }}`,
|
|
787
|
+
> `{{ .step.response.fn_score }}`, `{{ index .data "fn_result" }}` and
|
|
788
|
+
> `{{ index .data "step.fn_result" }}` were all read as calls. A flow with a state
|
|
789
|
+
> field, a step, or a data key whose name begins with `fn_` was then **refused
|
|
790
|
+
> with a message about a function it never called** — a false refusal on a valid
|
|
791
|
+
> flow, which is worse than a missed detection. Two guards, both required, each
|
|
792
|
+
> covered by its own test: quoted spans (`"…"` and backticks) are **stripped from
|
|
793
|
+
> the action before the scan**, because a quoted string inside an action names a
|
|
794
|
+
> data key rather than an identifier; and the character before `fn_` must be
|
|
795
|
+
> neither a dot nor a word character. The Go side captures and discards that
|
|
796
|
+
> character (`(^|[^.\w])(fn_…)`) because RE2 has no lookbehind; this validator uses
|
|
797
|
+
> the negative lookbehind `(?<![.\w])`, which is the cleaner equivalent — no
|
|
798
|
+
> capture group, and it cannot consume the delimiter between two adjacent matches
|
|
799
|
+
> — and is available on the Node >= 20 this package requires. A real call still
|
|
800
|
+
> matches after `{{`, `(`, `|` or whitespace.
|
|
801
|
+
>
|
|
802
|
+
> New conformance fixtures: `invalid-expression-function-package.yaml`,
|
|
803
|
+
> `invalid-expression-function-unknown-name.yaml`,
|
|
804
|
+
> `invalid-expression-function-undeclared-use.yaml`,
|
|
805
|
+
> `valid-expression-functions.yaml`,
|
|
806
|
+
> `valid-expression-function-prose-mention.yaml`,
|
|
807
|
+
> `valid-expression-function-field-lookalikes.yaml`.
|
|
808
|
+
|
|
809
|
+
> v2.640.0 — `unique_items` removed from the spec surface and from
|
|
810
|
+
> `PropertyDefinition`. AIgentFlow deleted the grammar field: it was declared
|
|
811
|
+
> twice in the Go domain with **zero** readers anywhere, and the served OpenAPI
|
|
812
|
+
> spec advertised it under a camelCase name the YAML parser would have rejected.
|
|
813
|
+
> Nothing honoured it in either direction. `billing.budget_exceeded_policy`,
|
|
814
|
+
> deleted in the same upstream cycle, was never mirrored here.
|
|
815
|
+
|
|
816
|
+
> v2.608.0 (AIF DC-FORGE-38 — a parity sweep covering v2.485.0 → v2.607.0).
|
|
817
|
+
> The audit of that 122-release window found only four rule-changing commits, and
|
|
818
|
+
> the sweep closed every one that is in scope, plus a defect in this validator
|
|
819
|
+
> that predates the window and mattered more than any of them.
|
|
820
|
+
>
|
|
821
|
+
> 1. **`next.conditions[].goto` — this validator read the wrong key.** The YAML
|
|
822
|
+
> key is `goto`; `ConditionDefinition.GotoStep` carries `yaml:"goto"`
|
|
823
|
+
> (aigentflow.domain.step.go:134). It is the ONE place in the grammar spelled
|
|
824
|
+
> that way — the flow-level and step-level `error_strategy` and `quality_gate`
|
|
825
|
+
> all use `goto_step` — and this validator read `goto_step` everywhere,
|
|
826
|
+
> including here. Two silent consequences: a conditional branch naming a
|
|
827
|
+
> step that does not exist was **never reported** (`step_not_found` on a
|
|
828
|
+
> condition could not fire at all), and every conditionally-reached step was
|
|
829
|
+
> accused of being `unreachable_step`. Measured against AIgentFlow's own
|
|
830
|
+
> validator over its 203 bundled flows, this validator reported **100**
|
|
831
|
+
> unreachable steps where the reference reported 24, and **missed** the one
|
|
832
|
+
> `potential_infinite_loop` the reference finds. Both numbers now match
|
|
833
|
+
> exactly. The old conformance fixtures used `goto_step`, so the whole suite
|
|
834
|
+
> was green over the defect. A condition carrying `goto_step` is now an
|
|
835
|
+
> explicit error (`unknown_yaml_key`), because AIgentFlow's create/update/
|
|
836
|
+
> validate path parses with `KnownFields(true)` and refuses such a flow
|
|
837
|
+
> outright — verified directly against the Go parser.
|
|
838
|
+
> 2. **Executor URL shape (AIF DC-FORGE-30, v2.598.0; DC-FORGE-38, v2.608.0).**
|
|
839
|
+
> `FlowParser.ValidateFlow` now applies the ONE parser (`URL_PATTERN_REGEX`,
|
|
840
|
+
> `ParseExecutorURLString`) to every step executor, and from v2.608.0 to
|
|
841
|
+
> `loop.steps[i].executor` as well. `openai:///gpt-4`, `ai://openai` and
|
|
842
|
+
> `http://api.example.com/v1` are refused at SAVE; this validator accepted all
|
|
843
|
+
> three. The regex is vendored verbatim as `executorUrlPattern`. **Templated
|
|
844
|
+
> URLs are skipped** in both implementations — the engine renders
|
|
845
|
+
> `step.executor` as a Go template before dispatch, and omitting that exception
|
|
846
|
+
> rejected eight working bundled flows when the Go rule was first written.
|
|
847
|
+
> Divergence #1 is untouched: the new rule is about SHAPE, and an unknown
|
|
848
|
+
> scheme is still a warning.
|
|
849
|
+
> 3. **Reachability follows five edge kinds (AIF DC-FORGE-30 §8, v2.598.0 +
|
|
850
|
+
> v2.598.1).** `findReachableSteps` delegates to the simulator's
|
|
851
|
+
> `collectNextTargets` — `next.default`, `next.conditions[].goto`,
|
|
852
|
+
> `next.parallel.steps[]`, `next.parallel.rendezvous`, step-level
|
|
853
|
+
> `error_strategy.goto_step` — plus the FLOW-level `error_strategy.goto_step`
|
|
854
|
+
> seeded into the queue. **The cycle detector was deliberately NOT widened**
|
|
855
|
+
> (`checkForCycles` still walks two edges), so this validator now uses two
|
|
856
|
+
> separate target functions; widening `hasCycle` too would invent a divergence
|
|
857
|
+
> and emit a spurious `potential_infinite_loop` on any rendezvous or error
|
|
858
|
+
> redirect back to an earlier step.
|
|
859
|
+
> 4. **`executorSchemes` is AIgentFlow's registered set exactly** (43 protocols,
|
|
860
|
+
> enumerated from `NewExecutorSchemaRegistry`). `web://` was missing, so a
|
|
861
|
+
> correct step warned; nine schemes AIgentFlow does not register (`openai`,
|
|
862
|
+
> `anthropic`, `perplexity`, `vertexai`, `ollama`, `vllm`, `aigentchat`,
|
|
863
|
+
> `external`, `https` — legacy names banner-marked non-functional in AIF
|
|
864
|
+
> v2.596.0) were listed, so this validator stayed silent where AIgentFlow
|
|
865
|
+
> fails at dispatch.
|
|
866
|
+
>
|
|
867
|
+
> **Audited and deliberately NOT ported** (recorded so the next sweep does not
|
|
868
|
+
> re-derive them):
|
|
869
|
+
>
|
|
870
|
+
> - `ValidateExecutorConfigEnvScopes` (AIF v2.597.0) — **ported in 0.15.0**
|
|
871
|
+
> (`executor_config_env_scope`, see the 0.15.0 note at the top). The four scope tables
|
|
872
|
+
> are vendored as one evaluated table, `executorConfigEnvScopes.scopes` in the spec.
|
|
873
|
+
> - Unknown-key rejection (**ported in 0.14.0**, see
|
|
874
|
+
> [Unknown keys and value kinds](#unknown-keys-and-value-kinds)). AIgentFlow's create path has always parsed with
|
|
875
|
+
> `KnownFields(true)`; AIF v2.604.0 made `POST /flows/validate` and the Studio
|
|
876
|
+
> assistant strict too, so "would this save" now answers the same everywhere.
|
|
877
|
+
> This validator inspects no unknown keys, so a step-level `output:` block
|
|
878
|
+
> (the real grammar is `post_processing: - output.set:`) still validates clean.
|
|
879
|
+
> Porting it means a full field inventory of `Flow`/`StepDefinition` and
|
|
880
|
+
> interacts with divergence #8. **A decision to make, not a delta.** The one
|
|
881
|
+
> case that mattered in practice — `goto_step` inside a condition — is now
|
|
882
|
+
> reported specifically, as are the keys AIgentFlow v2.721.0 deleted from the
|
|
883
|
+
> grammar (`budget`, `max_retries` — see DC-FORGE-150 above).
|
|
884
|
+
> - Everything else in the window is runtime field resolution (divergence #3),
|
|
885
|
+
> credentials/compliance (#5, #6) or documentation surface: AIF 2.526.0's
|
|
886
|
+
> `DeploymentQueryOption` rename, 2.580.0/2.582.0/2.585.0's
|
|
887
|
+
> `template_missing_field` grounding, 2.591.0–2.594.0's step-query stripping,
|
|
888
|
+
> 2.603.0's billed-header redaction, 2.604.0's `BuildOutputContractWarnings`.
|
|
889
|
+
> `input_schema.go` and `template.registry.go` have a **zero** diff across the
|
|
890
|
+
> whole window, and no new YAML-tagged flow or step field was added.
|
|
891
|
+
|
|
892
|
+
> v2.485.0 (DC-COND-2 CONDUCTOR): added the `campaign.on_children_complete`
|
|
893
|
+
> field (names the step the engine deterministically routes into once every
|
|
894
|
+
> spawned child is terminal). One static check ported:
|
|
895
|
+
> `campaign_handoff_step_unknown` (the referenced step must exist in `steps`) —
|
|
896
|
+
> consistent with the existing `next`-reference validation. The field's _runtime_
|
|
897
|
+
> effect (the deterministic all-children-terminal hand-off, idle-tick gating,
|
|
898
|
+
> event-authoritative campaign state) is engine behaviour and out of scope for
|
|
899
|
+
> static validation. New conformance fixtures: `valid-campaign-handoff.yaml`,
|
|
900
|
+
> `invalid-campaign-handoff-unknown-step.yaml`.
|
|
901
|
+
>
|
|
902
|
+
> v2.484.0 (DC-COND-1 CONDUCTOR): added the orchestrator `mode:` field
|
|
903
|
+
> (`monitor` | `owner`, default `monitor`). Two static checks ported:
|
|
904
|
+
> `orchestrator_mode_invalid` (unknown mode value) and
|
|
905
|
+
> `orchestrator_owner_needs_yield` (`owner` mode requires at least one step with
|
|
906
|
+
> `next: orchestrator` — mirrored via `flowHasOrchestratorYieldEdge`). The
|
|
907
|
+
> mode's _runtime_ effect (terminal-vs-yield lifecycle, deterministic backstops)
|
|
908
|
+
> is engine behaviour and out of scope for static validation. New conformance
|
|
909
|
+
> fixtures: `valid-orchestrator-monitor.yaml`, `invalid-orchestrator-owner-no-yield.yaml`.
|
|
910
|
+
>
|
|
911
|
+
> v2.735.0 (DC-FORGE-162, aigentflow#149): added Sprig's float names `addf`, `subf`,
|
|
912
|
+
> `mulf`, `divf` and `float64` to `templateFunctions`. `ternary` also accepts Sprig's
|
|
913
|
+
> condition-last order at runtime; that changes no name, so the allow-list is unaffected.
|
|
914
|
+
> Allow-list only.
|
|
915
|
+
>
|
|
916
|
+
> v2.478.0: added the `htmlDocument` template function (sanitises LLM chat output
|
|
917
|
+
> destined for public hosting — slices `<!doctype>`…`</html>`, dropping markdown
|
|
918
|
+
> fences + conversational preamble/postamble). Allow-list only; runtime behaviour
|
|
919
|
+
> is out of scope for static validation.
|
|
920
|
+
|
|
921
|
+
The Go reference has two layers, both ported here:
|
|
922
|
+
|
|
923
|
+
- `Validator.ValidateFlowWithDetails` — `aigentflow/aigentflow.validation.go` (the structured-result validator; our primary model).
|
|
924
|
+
- `FlowParser.ValidateFlow` — `aigentflow/aigentflow.parser.go` (parse-time structural checks).
|
|
925
|
+
|
|
926
|
+
Comparison contract: **error `code` + `valid` verdict**, not message wording. The conformance suite (`test/conformance/`) asserts verdicts structurally.
|
|
927
|
+
|
|
928
|
+
---
|
|
929
|
+
|
|
930
|
+
## Rule map
|
|
931
|
+
|
|
932
|
+
| Area | Go source | JS module | Tested by |
|
|
933
|
+
| ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ---------------------------------------------------------- |
|
|
934
|
+
| Required fields, start-step existence, per-step executor, reserved `.` in IDs, reserved id `orchestrator` | `validateBasicStructure`, `ValidateFlow` head | `validators/basicStructure.ts` | `validate.test.ts`, `conformance.test.ts` |
|
|
935
|
+
| Executor URI shape (the ONE parser) + scheme | `ValidateFlow` executor-URL rule (parser.go), `ParseExecutorURLString` | `validators/executors.ts` | `validate.test.ts` |
|
|
936
|
+
| Query/property/array-item schema + array constraints | `validateQueryParameters`, `validateProperties`, `validateArrayItems`, `validateArrayConstraints` | `validators/querySchema.ts` | `validate.test.ts` |
|
|
937
|
+
| Response-expectation types + array items + `required` | `ValidateFlow` (response block), `validateSemantics` | `validators/responseExpectation.ts` | `validate.test.ts` |
|
|
938
|
+
| Response expectation nothing reads (`response_expectation_unread`, warning) | `validateResponseExpectationIsRead` (validation.go) | `validators/responseExpectation.ts` | `validate.test.ts`, `conformance.test.ts` |
|
|
939
|
+
| Error strategy (action, goto, max_delay, backoff, retry_on) | `validateErrorStrategy` | `validators/errorStrategy.ts` | `validate.test.ts` |
|
|
940
|
+
| Retired `budget:` / `max_retries:` (flow, step, loop sub-step) and `campaign.budget_max_per_child` → `unknown_yaml_key` | `retiredGrammarKeys` + `KnownFields(true)` (parser.go) | `validators/retiredKeys.ts` | `validate.test.ts`, `conformance.test.ts` |
|
|
941
|
+
| `next` references (save-door sentinels `null`/`orchestrator`), reachability and cycles (`end` is an ordinary step) | `validateNextLogic` (parser.go), `findReachableSteps`, `checkForCycles` | `validators/connectivity.ts` | `validate.test.ts`, `conformance.test.ts` |
|
|
942
|
+
| Unknown keys at every level (`unknown_yaml_key`) and value kinds (`invalid_type`) | `KnownFields(true)` over the reference's types (parser.go); `knownKeys` in the spec | `validators/unknownKeys.ts` | `validate.test.ts`, `conformance.test.ts` |
|
|
943
|
+
| `next.parallel` + orchestrator-next requirement | `validateNextLogic`, `validateOrchestratorNext` | `validators/nextLogic.ts` | `validate.test.ts` |
|
|
944
|
+
| Expression functions (XOR package/function) | `validateExpressionFunctions` | `validators/expressionFunctions.ts` | `validate.test.ts` |
|
|
945
|
+
| Expression-function catalog (`package:` refused, unknown `function:` refused) | `validateExpressionFunctionCatalog` (parser.go) | `validators/expressionFunctions.ts` | `validate.test.ts`, `conformance.test.ts` |
|
|
946
|
+
| Expression-function USE (`{{ fn_* }}` must be in the catalog AND declared) | `validateExpressionFunctionUsage` (parser.go) | `validators/expressionFunctions.ts` | `validate.test.ts`, `conformance.test.ts` |
|
|
947
|
+
| Loop / for_each / throttle | `validateLoop`, `validateForEach`, `validateThrottle` | `validators/loopForEachThrottle.ts` | `validate.test.ts` |
|
|
948
|
+
| Loop sub-step id that shadows a loop-result summary field (`loop_sub_step_id_reserved`, warning) | `validateLoopSubStepIDCollision` (validation.loopbody.go) | `validators/loopForEachThrottle.ts` | `validate.test.ts`, `conformance.test.ts` |
|
|
949
|
+
| Loop sub-step `next:` targets (same-loop only, sentinels, no parallel) | `validateLoopSubStepNext` (parser.go) | `validators/loopForEachThrottle.ts` | `validate.test.ts`, `conformance.test.ts` |
|
|
950
|
+
| Orchestrator structure + campaign requires orchestrator | `validateOrchestrator`, `validateAndNormalizeCampaign` | `validators/orchestratorCampaign.ts` | `validate.test.ts` |
|
|
951
|
+
| Campaign `max_credits_per_child` (decoded as int64, >= 0), `child_flows` | `CampaignConfig.Validate` (domain.campaign.go) | `validators/orchestratorCampaign.ts` | `validate.test.ts`, `conformance.test.ts` |
|
|
952
|
+
| `tool_discovery` vocabulary, mock scenario `delay`, empty `output:` entry | `validateToolDiscoveryVocabulary`, `validateMockScenarioDelays`, `validateOutputParameters` (parser.go) | `validators/saveDoor.ts` | `validate.test.ts`, `conformance.test.ts` |
|
|
953
|
+
| `executor_config` `${NAME}` references outside the key's scope (`executor_config_env_scope`) | `ValidateExecutorConfigEnvScopes` (parser.go) | `validators/executorConfigEnv.ts` | `v0150-save-door.test.ts`, `conformance.test.ts` |
|
|
954
|
+
| A step or loop sub-step `query:` declaring a server-owned key (`server_owned_query_key`) | `validateNoServerOwnedStepQueryKeys` (validation.go), `IsServerOwnedParamKey` | `validators/serverOwnedQueryKeys.ts` | `v0151-save-door.test.ts`, `conformance.test.ts` |
|
|
955
|
+
| An endpoint a server-supplied credential will never be sent to (`credential_endpoint_unpaired`, warning) | `validateCredentialEndpointPairing`, `validateFamilyCredentialEndpointPairing` (validation.go) | `validators/credentialEndpoint.ts`, `validators/goUrl.ts` | `v0152-credential-endpoint.test.ts`, `conformance.test.ts` |
|
|
956
|
+
| Orchestrator `.exons` frontmatter: spec, provider, `requirements.resources`, `tools.allow` withholds | `validateOrchestrator` (parser.go), `validateOrchestratorToolAllowWithholds` | `validators/exons.ts` | `v0150-save-door.test.ts`, `conformance.test.ts` |
|
|
957
|
+
| Credential bindings (`stored/...`, inject_as, exclusivity) | `validateStepCredentialBindings` | `validators/credentialBindings.ts` | `validate.test.ts` |
|
|
958
|
+
| `input_schema` definition + ordering lint | `ValidateInputSchemaDefinition`, `LintInputSchemaFieldOrdering` | `validators/inputSchema.ts` | `validate.test.ts` |
|
|
959
|
+
| Step `output_schema` definition (reuses the input-schema subset) | `ValidateInputSchemaDefinition` (on `step.OutputSchema`, parser.go) | `validators/outputSchema.ts` (+ shared `inputSchema.ts`) | `validate.test.ts` |
|
|
960
|
+
| `quality_gate:` block (rubric/threshold/on_fail/goto) | `FlowParser.validateQualityGate` (parser.go) | `validators/qualityGate.ts` | `validate.test.ts` |
|
|
961
|
+
| Processing-operation type + per-operation config keys | `validateProcessingOperationShape` | `validators/processingOperations.ts` | `validate.test.ts`, `conformance.test.ts` |
|
|
962
|
+
| Step `max_duration` the engine cannot apply (`step_max_duration_ignored`, warning) | `validateStepMaxDurationIsApplied` (validation.go), `parseStepMaxDuration` (flowengine.steptimeout.go) | `validators/stepMaxDuration.ts` | `validate.test.ts`, `conformance.test.ts` |
|
|
963
|
+
| Go-template syntax | `validateTemplateExpression` (Parse step) | `template/gotmpl-syntax.ts` + `validators/templates.ts` | `gotmpl-syntax.test.ts`, `validate.test.ts` |
|
|
964
|
+
|
|
965
|
+
---
|
|
966
|
+
|
|
967
|
+
## Intentional divergences
|
|
968
|
+
|
|
969
|
+
These are deliberate, documented differences from the Go static pass. They keep the
|
|
970
|
+
validator useful and low-false-positive while staying offline.
|
|
971
|
+
|
|
972
|
+
1. **Executor scheme is a warning, not an error.** The Go static validator does not
|
|
973
|
+
reject unknown schemes (the live executor registry decides at runtime, and schemes
|
|
974
|
+
are added frequently). We flag a **malformed** URI (`invalid_executor_url`) as an
|
|
975
|
+
error — that is always genuinely broken — but an unrecognised scheme is a
|
|
976
|
+
`unknown_executor_scheme` **warning**. This means a brand-new AIgentFlow scheme never
|
|
977
|
+
produces a false failure here.
|
|
978
|
+
|
|
979
|
+
2. **Closed in 0.14.0 and 0.15.0: `end` was always a terminal marker here.** The reference
|
|
980
|
+
was internally inconsistent: its structured validator exempted `end`, and its save door
|
|
981
|
+
(`validateNextLogic`) did not. This port used to follow the structured validator, so
|
|
982
|
+
it accepted `next: { default: end }`, which the reference refuses to save. 0.14.0
|
|
983
|
+
followed each: the existence check uses the save door's set (`null`, `orchestrator`),
|
|
984
|
+
and the reachability and cycle walks kept `end` as terminal, so a real step named `end`
|
|
985
|
+
that only `end` routed to was `unreachable_step` on both sides.
|
|
986
|
+
|
|
987
|
+
**Since 0.15.0 (reference v2.760.0) the second half is gone too:** no reference walk
|
|
988
|
+
treats `end` as terminal, so a real step named `end` is reachable and a cycle through
|
|
989
|
+
it is a cycle (`warn-next-end-cycle.yaml`). Spec `reachabilityTerminalMarkers` is now
|
|
990
|
+
`["null", "orchestrator"]`, the same values as `nextMarkers`; the two keys stay
|
|
991
|
+
separate so a future split is a one-value change.
|
|
992
|
+
|
|
993
|
+
3. **Runtime template field-resolution is not reproduced.** Go additionally _executes_
|
|
994
|
+
each template against a mock context to emit `template_missing_field` /
|
|
995
|
+
`condition_not_boolean` **warnings**. That requires simulating the runtime state graph;
|
|
996
|
+
we validate template **syntax** only. (Candidate for a future best-effort pass.)
|
|
997
|
+
|
|
998
|
+
4. **Unknown orchestrator tools / template functions are warnings by default.** The
|
|
999
|
+
vendored allow-lists (`orchestrator.tools`, `templateFunctions`) can lag the live
|
|
1000
|
+
AIgentFlow registries. Pass `{ strictRegistries: true }` to make them errors.
|
|
1001
|
+
|
|
1002
|
+
5. **Narrowed in 0.15.0: the orchestrator exons document is read, its body is not
|
|
1003
|
+
parsed.** The frontmatter is read the way the engine extracts and decodes it (see the
|
|
1004
|
+
0.15.0 note at the top), so the spec's presence, `execution.provider`,
|
|
1005
|
+
`requirements.resources` and `tools.allow` are judged as the reference judges them.
|
|
1006
|
+
Whether the document as a whole parses, and whether its tags render, needs the
|
|
1007
|
+
engine: divergence #16.
|
|
1008
|
+
|
|
1009
|
+
6. **Compliance / credentials / publish gates are out of scope** — they require a live
|
|
1010
|
+
server, the provider catalogue, and org context. See the README.
|
|
1011
|
+
|
|
1012
|
+
7. **Regex engine.** `input_schema` `pattern` compilation uses the JS regex engine, not
|
|
1013
|
+
Go RE2. A pattern valid in one engine but not the other is a (rare) known divergence.
|
|
1014
|
+
|
|
1015
|
+
8. **Shape errors.** Because YAML decodes into an untyped object (vs. Go's typed
|
|
1016
|
+
unmarshal), this validator emits `invalid_type` errors where Go would have failed at
|
|
1017
|
+
decode time. This is strictly additive.
|
|
1018
|
+
|
|
1019
|
+
9. **`unresolvable_data_path` is not reproduced (DC-CP-7).** The Go engine, when a step
|
|
1020
|
+
declares an `output_schema`, treats that step's output as a typed contract and errors
|
|
1021
|
+
(`unresolvable_data_path`) on any `.data.<step>.<field>` template reference elsewhere in
|
|
1022
|
+
the flow that names a field the producing step's `output_schema` does not declare
|
|
1023
|
+
(opt-in: steps _without_ an `output_schema` are left lenient). Emitting it here would
|
|
1024
|
+
require statically resolving `.data.<step>.<field>` references out of every template and
|
|
1025
|
+
matching them to the producing step — i.e. the very runtime template field-resolution
|
|
1026
|
+
this validator deliberately does not perform (see divergence #3). We therefore validate
|
|
1027
|
+
the `output_schema` **definition** (structure/field types, via the shared input-schema
|
|
1028
|
+
validator) but do **not** cross-check template references against it. Candidate for the
|
|
1029
|
+
same future best-effort resolution pass as divergence #3.
|
|
1030
|
+
|
|
1031
|
+
The `wait://` and `eval://` schemes (DC-CP-5 / DC-CP-8) are now in the known-scheme set,
|
|
1032
|
+
so they no longer warn (`unknown_executor_scheme`); malformed `wait://` / `eval://` URIs
|
|
1033
|
+
still error via the shared shape check. `quality_gate.on_fail=human` exists in the Go
|
|
1034
|
+
enum but is validation-REJECTED there (pending the human-task inbox), so this validator
|
|
1035
|
+
rejects it too (`quality_gate_on_fail_unsupported`) — this is parity, not a divergence.
|
|
1036
|
+
|
|
1037
|
+
10. **The `fn_*` usage scan walks the document, not the typed struct (v2.642.0).** The
|
|
1038
|
+
Go rule re-serialises the `Flow` struct before scanning for template actions, so it
|
|
1039
|
+
sees only fields the struct declares. This validator has no typed unmarshal, so it
|
|
1040
|
+
walks every string leaf of the parsed document — including keys the Go struct does
|
|
1041
|
+
not carry. The two agree on every flow AIgentFlow would accept, because its create
|
|
1042
|
+
path parses with `KnownFields(true)`; they differ only on a document that AIgentFlow
|
|
1043
|
+
rejects for an unrelated reason, and since 0.14.0 this validator refuses that document
|
|
1044
|
+
too (`unknown_yaml_key`). Same family as divergence #8, and strictly additive.
|
|
1045
|
+
|
|
1046
|
+
Severity: the catalog and usage rules are refused at AIgentFlow's SAVE door and only
|
|
1047
|
+
warned at its load/run door, so a flow stored before the rules existed keeps running.
|
|
1048
|
+
This validator answers "would this save" and therefore reports all of them as
|
|
1049
|
+
`error` — the choice already made for the executor-URL shape rule. That is parity
|
|
1050
|
+
with the save door, not a divergence, but it means a refusal here does **not** imply
|
|
1051
|
+
a broken running flow.
|
|
1052
|
+
|
|
1053
|
+
11. **A malformed processing-operation entry produces no shape verdict (v2.647.0).**
|
|
1054
|
+
The reference's custom unmarshaller refuses an entry that is not a map, that
|
|
1055
|
+
carries more than one non-`if` key, or whose config is not a mapping — those
|
|
1056
|
+
flows never reach the rules above, because they fail to parse at all. This
|
|
1057
|
+
validator has no typed unmarshal, so such an entry simply yields no
|
|
1058
|
+
operation to have an opinion about and both rules stay silent; the
|
|
1059
|
+
structural pass keeps whatever it already said. Same family as divergence
|
|
1060
|
+
#8, in the lenient direction.
|
|
1061
|
+
|
|
1062
|
+
Scope note, resolved in v2.648.0: both implementations now walk the loop body
|
|
1063
|
+
as well, with `loop.set` / `loop.break` dispatchable there and undispatchable
|
|
1064
|
+
at the top level.
|
|
1065
|
+
|
|
1066
|
+
12. **`orchestrator.human_question_timeout` — the refusal is ported, the WARNING is not.**
|
|
1067
|
+
AIF v2.695.0 (DC-FORGE-125, aigentflow#124) adds an opt-in per-question HITL deadline. The
|
|
1068
|
+
reference refuses an unparseable **or non-positive** duration, and this validator does the same
|
|
1069
|
+
— note the positivity half: `-5m` is a well-formed Go duration, so a bare `isValidGoDuration`
|
|
1070
|
+
check would accept what the door refuses, and an oracle **looser** than its door is wrong in the
|
|
1071
|
+
more damaging direction.
|
|
1072
|
+
|
|
1073
|
+
The reference ALSO logs a server-side warning (a log line, not a validation warning) when the declared timeout exceeds `ORCH_MAX_MISSION_DURATION` (the
|
|
1074
|
+
orchestrator's 60-minute runaway guard), because a deadline the mission clock outlives can never
|
|
1075
|
+
fire. That is **not** ported: the threshold is a deployment-side constant this package cannot
|
|
1076
|
+
observe, and hard-coding it here would put a second copy of an AIgentFlow number in another
|
|
1077
|
+
repository — the drift this file exists to prevent. A flow that declares an over-long timeout is
|
|
1078
|
+
therefore `valid` on both sides; only the reference's server log mentions it.
|
|
1079
|
+
|
|
1080
|
+
⚠️ Absent means **no deadline**, in both — and an empty string or YAML `null` counts as absent, because
|
|
1081
|
+
the reference's field is a Go string and its check runs only on a non-empty value. This validator must never infer a default: the
|
|
1082
|
+
reference treats a default here as a policy nobody chose, and inventing one would make the
|
|
1083
|
+
oracle refuse or accept on a premise the door does not hold.
|
|
1084
|
+
|
|
1085
|
+
13. **Narrowed in 0.14.0 and 0.14.1: a number in a Go `string` field is judged by
|
|
1086
|
+
its source text only when there is source text.** yaml.v3 fills the reference's
|
|
1087
|
+
`string` field with the scalar's **source text**. `validateFlow` now has it
|
|
1088
|
+
(`parseFlow` records the spelling of every number and boolean scalar, and gives
|
|
1089
|
+
every number or boolean mapping key its spelling), so `delay: 0.0`, `00` and
|
|
1090
|
+
`0x0` are refused as the reference refuses them, and `default: 1e3` names the
|
|
1091
|
+
step `"1e3"`. Two cases still see only the parsed value: `validateFlowObject`,
|
|
1092
|
+
whose caller parsed the document, and a value reached through a YAML alias.
|
|
1093
|
+
There a number is rendered the way JavaScript renders it (`0.0` is `"0"`, `1e3`
|
|
1094
|
+
is `"1000"`, `0x1F` is `"31"`), which is also how JavaScript renders a numeric
|
|
1095
|
+
object key, so an ordinary `2` still names the step `2`. For durations this is
|
|
1096
|
+
the lenient direction: every other number is refused either way, because no
|
|
1097
|
+
number's text carries a unit.
|
|
1098
|
+
|
|
1099
|
+
Since 0.14.1 the mechanism covers every Go `string` field the validators read:
|
|
1100
|
+
step references (`next.default`, condition `goto`, `parallel.steps` and
|
|
1101
|
+
`rendezvous`, `error_strategy` / `quality_gate` `goto_step`, `start`,
|
|
1102
|
+
`campaign.on_children_complete`, loop sub-step ids and `next` targets), enums
|
|
1103
|
+
(`action`, `retry_on`, `on_fail`, `resolution`, `mode`, trigger `type`, data
|
|
1104
|
+
types, `tools`), names and required text (`name`, `aigentflow_version`,
|
|
1105
|
+
`executor`, `rubric`, `items`, `while`, `exons`, credential `source` /
|
|
1106
|
+
`inject_as`, input-schema `name` / `type` / `pattern` / `visible_when.field`,
|
|
1107
|
+
`expression_functions` values, `response_evaluation`, `flow_id` /
|
|
1108
|
+
`flow_name`), and the durations (`delay`, `batch_delay`, `max_delay`,
|
|
1109
|
+
`retry_delay`, timer `interval`, `human_question_timeout`, `max_duration`,
|
|
1110
|
+
`tool_discovery`).
|
|
1111
|
+
|
|
1112
|
+
_Numbers 14 and 15 are the Go port's own (its #14 collects reference checks outside both
|
|
1113
|
+
ports' scope, such as plugin pre-validation; its #15 is #13 here). From 16 on, both ports
|
|
1114
|
+
number their shared divergences alike._
|
|
1115
|
+
|
|
1116
|
+
16. **`.exons` documents: the engine's judgement needs the engine (0.15.0; Go port #16).**
|
|
1117
|
+
The reference judges an inline `.exons` document with go-exons: whether it parses
|
|
1118
|
+
(grammar, frontmatter decode and the spec's own validation), and whether every tag
|
|
1119
|
+
can render. This validator has no `.exons` template engine and must not grow one,
|
|
1120
|
+
because a second implementation of that grammar is a second opinion about it. The
|
|
1121
|
+
frontmatter reader makes `orchestrator_exons_parse_failed` (no spec, an unclosed
|
|
1122
|
+
frontmatter, the retired config block), `orchestrator_exons_no_provider`,
|
|
1123
|
+
`exons_resources_unhonoured` and `orchestrator_tool_withheld` the reference's. What
|
|
1124
|
+
it cannot see:
|
|
1125
|
+
- `exons_attributes`: a tag the engine's resolver refuses at execute
|
|
1126
|
+
(`{~exons.message~}` without `role=`, `{~exons.include~}` without `template=`),
|
|
1127
|
+
on a step's inline `query.exons` and on the orchestrator. The step rule is not
|
|
1128
|
+
ported at all: with no engine it has nothing to judge.
|
|
1129
|
+
- a document that does not parse for any other reason, including a spec the engine's
|
|
1130
|
+
own validation refuses (a missing `description`), and a frontmatter whose declared
|
|
1131
|
+
fields have the wrong shape (the reader then judges nothing);
|
|
1132
|
+
- a frontmatter containing a tag, which the engine executes before decoding.
|
|
1133
|
+
|
|
1134
|
+
**Direction: looser, never stricter.** The reader refuses only where the engine
|
|
1135
|
+
refuses as well; the Go port pins that with a 15-shape matrix against go-exons, and
|
|
1136
|
+
this repository pins the same 15 plus 24 decode shapes against the Go port's verdicts.
|
|
1137
|
+
The engine-only fixtures (`invalid-exons-step-attributes.yaml`,
|
|
1138
|
+
`invalid-orchestrator-exons-attributes.yaml`,
|
|
1139
|
+
`invalid-orchestrator-exons-spec-invalid.yaml`) state the reference's verdict and are
|
|
1140
|
+
marked `exonsEngineOnly`: this suite asserts the looser one (valid, none of the
|
|
1141
|
+
engine's codes). The Go port offers its engine as an optional companion module; there
|
|
1142
|
+
is no JavaScript equivalent.
|
|
1143
|
+
|
|
1144
|
+
17. **`input_schema_file_after_parametric` is spelt `INPUT_SCHEMA_FILE_AFTER_PARAMETRIC`
|
|
1145
|
+
by the reference (Go port #17).** The reference's save door merges its `input_schema`
|
|
1146
|
+
ordering lint into the verdict with its own upper-case code, at field `doc`. Both
|
|
1147
|
+
ports have always reported it as `input_schema_file_after_parametric` at the offending
|
|
1148
|
+
field (`input_schema.fields[N]`). A warning only; renaming a published code would break
|
|
1149
|
+
every consumer that branches on it, so it stays.
|
|
1150
|
+
|
|
1151
|
+
18. **`credential_endpoint_unpaired`: what the static rule cannot see (Go port #19).** The
|
|
1152
|
+
reference's save-time rule is itself static and is ported exactly. What it predicts is
|
|
1153
|
+
a RUN-TIME refusal that reads what no document carries: the values of the server's
|
|
1154
|
+
environment variables, the credential resolver's output (which stored key, with which
|
|
1155
|
+
base URL), the service configuration an executor fills, and whether a client attaches
|
|
1156
|
+
the deployment's service credential. Neither side reads them statically.
|
|
1157
|
+
- **Default endpoints of rows the rule never reads are not carried** (`getmd://`, which
|
|
1158
|
+
is never judged, and `nexus://`, whose shape consults no default). Verdict-neutral.
|
|
1159
|
+
- **A `credentials:` mapping with a non-string key (looser, shared).** The reference
|
|
1160
|
+
decodes it into `map[any]any`, which its nexus own-key check does not accept, so the
|
|
1161
|
+
endpoint warns there; both ports stringify mapping keys and see a key of the step's own.
|
|
1162
|
+
- **JS only, looser: an endpoint shaped like a YAML timestamp is not read as a string.**
|
|
1163
|
+
yaml.v3 decodes a PLAIN `2024-01-01` into `time.Time`, which the reference's
|
|
1164
|
+
`.(string)` rejects; the `yaml` package gives a string and the source's quoting is not
|
|
1165
|
+
kept. Where reading a value as a string could ADD a warning (an endpoint), a
|
|
1166
|
+
timestamp-shaped string is skipped, so a QUOTED one warns in the reference and not here.
|
|
1167
|
+
- **JS only, looser: a `!!binary` value** is a string to yaml.v3 and bytes here.
|
|
1168
|
+
- **Not ported, verdict-neutral:** Go validates a bracketed IPv6 host with
|
|
1169
|
+
`netip.ParseAddr`; a bracketed host is never a default's host either way.
|
|
1170
|
+
|
|
1171
|
+
---
|
|
1172
|
+
|
|
1173
|
+
## Unknown keys and value kinds
|
|
1174
|
+
|
|
1175
|
+
The reference's save door decodes a flow with yaml.v3 `KnownFields(true)`. That refuses
|
|
1176
|
+
a key the containing Go struct does not declare, **at every depth**, and a value whose
|
|
1177
|
+
KIND does not decode into the field (a scalar where a struct, map or list is required,
|
|
1178
|
+
or a mapping or list where a scalar is). `validators/unknownKeys.ts` reproduces both.
|
|
1179
|
+
|
|
1180
|
+
**The key sets are derived, not hand-listed.** `knownKeys` in the spec is generated by
|
|
1181
|
+
reflection over the reference's `Flow` type, applying yaml.v3's own field rules:
|
|
1182
|
+
|
|
1183
|
+
- a field's key is its `yaml:` tag name, or the lowercased field name when untagged;
|
|
1184
|
+
`yaml:"-"` and unexported fields are skipped;
|
|
1185
|
+
- an `,inline` struct's keys merge into the parent (this is why `currency` and
|
|
1186
|
+
`max_duration` are flow-root keys); an `,inline` map would make the type open (none
|
|
1187
|
+
does today);
|
|
1188
|
+
- a pointer is followed; a slice becomes `list<…>`; a map becomes `map<…>` with
|
|
1189
|
+
author-chosen keys; an interface is `any`;
|
|
1190
|
+
- a type with its own `UnmarshalYAML` is `any`, because yaml.v3 does not propagate
|
|
1191
|
+
`KnownFields` into `Node.Decode`. The one such type is a pre/post-processing entry,
|
|
1192
|
+
whose shape stays out of scope (divergence #11).
|
|
1193
|
+
|
|
1194
|
+
**How it was checked.** For each of the 35 struct types, a document placing an unknown
|
|
1195
|
+
key at that type's position must be refused as an unknown key; for each of its keys, a
|
|
1196
|
+
value of each wrong kind must fail to decode and a null must decode; for each `any`
|
|
1197
|
+
position, a mapping, a list and a scalar must all decode. 771 checks, run against the
|
|
1198
|
+
reference's strict parser, all as expected, and a mutated inventory (a key removed, a
|
|
1199
|
+
kind changed) fails them. On the 216 bundled flows the rule fires zero times.
|
|
1200
|
+
|
|
1201
|
+
**What is judged.** Keys and kinds only. Whether a scalar fits its field (a number in an
|
|
1202
|
+
integer field, `yes` in a boolean) is left to the rules that already own those fields
|
|
1203
|
+
(divergence #8). A YAML null is the zero value of any type and is never reported. A key
|
|
1204
|
+
the reference deleted from the grammar is named as retired wherever it appears; at the
|
|
1205
|
+
locations `retiredKeys.ts` covers, that rule's specific advice is reported instead, and a
|
|
1206
|
+
location is never reported twice.
|
|
1207
|
+
|
|
1208
|
+
**Regenerating it.** On a grammar change, re-run the derivation above against the new
|
|
1209
|
+
reference types (a short Go program: reflect over `Flow`, apply the rules above, print
|
|
1210
|
+
each struct type's key → shape map, sorted) and replace `knownKeys` in the spec. Then
|
|
1211
|
+
re-run the check above and the bundled-corpus comparison. The derivation program is
|
|
1212
|
+
not shipped here, because it links the reference implementation.
|
|
1213
|
+
|
|
1214
|
+
---
|
|
1215
|
+
|
|
1216
|
+
## Migration discipline — keep this in sync with AIgentFlow
|
|
1217
|
+
|
|
1218
|
+
When the AIgentFlow flow grammar or validation rules change, this validator **must** be
|
|
1219
|
+
updated. The trigger conditions and the checklist:
|
|
1220
|
+
|
|
1221
|
+
**Triggers** (any flow-YAML-affecting change in the `aigentflow` repo):
|
|
1222
|
+
|
|
1223
|
+
- a new executor scheme;
|
|
1224
|
+
- a new top-level flow field or step field;
|
|
1225
|
+
- a new/changed enum (data types, error actions, next markers, resolutions, input-schema
|
|
1226
|
+
field types, orchestrator triggers/tools);
|
|
1227
|
+
- a new or changed validation rule in `aigentflow.validation.go`, `aigentflow.parser.go`,
|
|
1228
|
+
or `aigentflow.input_schema.go`;
|
|
1229
|
+
- a new template function in `aigentflow.template.registry.go`.
|
|
1230
|
+
|
|
1231
|
+
**Checklist:**
|
|
1232
|
+
|
|
1233
|
+
1. Update [`src/spec/aigentflow-spec.json`](./src/spec/aigentflow-spec.json) — the enum
|
|
1234
|
+
values and bump `specVersion` to the new AIgentFlow version. A new or renamed field
|
|
1235
|
+
in any flow type also means regenerating `knownKeys` (see
|
|
1236
|
+
[Unknown keys and value kinds](#unknown-keys-and-value-kinds)); otherwise the new key
|
|
1237
|
+
is refused as unknown.
|
|
1238
|
+
2. Port the rule into the matching `validators/*.ts` module (or add a new module).
|
|
1239
|
+
3. Add a row to the [rule map](#rule-map) above and, if it diverges, an entry under
|
|
1240
|
+
[Intentional divergences](#intentional-divergences).
|
|
1241
|
+
4. Add a conformance fixture under `test/conformance/fixtures/` plus an entry in
|
|
1242
|
+
`test/conformance/conformance.test.ts`, and a focused unit test.
|
|
1243
|
+
5. `npm run typecheck && npm run lint && npm test && npm run build` — all green.
|
|
1244
|
+
6. Bump the package version and update `SPEC_VERSION` in the README.
|
|
1245
|
+
|
|
1246
|
+
> The `aigentflow` repository's `CLAUDE.md` carries a reciprocal note pointing back here,
|
|
1247
|
+
> so a flow-YAML change in either repo surfaces the obligation to update the other.
|