@vaudience/aigentflow-flow-validator 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/PARITY.md CHANGED
@@ -4,7 +4,47 @@ This document maps every rule in this JavaScript validator back to the AIgentFlo
4
4
  Go reference implementation, records the intentional divergences, and defines the
5
5
  discipline for keeping the two in sync.
6
6
 
7
- **Tracks AIgentFlow flow schema: `v2.821.0`** (`SPEC_VERSION` in [`src/spec/aigentflow-spec.json`](./src/spec/aigentflow-spec.json)), which adds the warning `compliance_residency_unenforced` (package 0.20.0, below). Earlier: `orchestrator.region` / `orchestrator.enforce_region` (package 0.19.0). Earlier: the AigentChat executor was removed (package 0.18.0). Earlier additions: the `examples:` block (package 0.17.0), `server_owned_query_key` (package 0.15.1) from the AIgentFlow release after v2.790.0, `credential_endpoint_unpaired` (package 0.15.2) from AIgentFlow v2.793.0 and the release after it, and `display_name_too_long` (package 0.15.3) from AIgentFlow v2.802.0.
7
+ **Tracks AIgentFlow flow schema: `v2.827.0`** (`SPEC_VERSION` in [`src/spec/aigentflow-spec.json`](./src/spec/aigentflow-spec.json)), which adds the `here` executor scheme, the `strict_templates` flow key and the rule `template_default_piped` (package 0.21.0, below). Earlier: the warning `compliance_residency_unenforced` (package 0.20.0). `orchestrator.region` / `orchestrator.enforce_region` (package 0.19.0). Earlier: the AigentChat executor was removed (package 0.18.0). Earlier additions: the `examples:` block (package 0.17.0), `server_owned_query_key` (package 0.15.1) from the AIgentFlow release after v2.790.0, `credential_endpoint_unpaired` (package 0.15.2) from AIgentFlow v2.793.0 and the release after it, and `display_name_too_long` (package 0.15.3) from AIgentFlow v2.802.0.
8
+
9
+ > package 0.21.0 — **`here://`, `strict_templates` and `template_default_piped`**, in step
10
+ > with the reference (AIgentFlow v2.827.0; `specVersion` 2.827.0). Three changes.
11
+ >
12
+ > 1. **The `here` executor scheme** (`here://maps/{geocode|reverse_geocode|route|places}`) is
13
+ > added to `executorSchemes`, between `getmd` and `homeassistant`; a `here://` step no longer
14
+ > draws `unknown_executor_scheme`. Fixture `valid-executor-here.yaml` forbids that code.
15
+ > 2. **`strict_templates`**, a new optional top-level boolean, is a known key (`knownKeys.Flow`).
16
+ > A value that is not a boolean is `invalid_type` (field `strict_templates`). A YAML null and
17
+ > the YAML 1.1 words the reference's decoder still reads into a `bool` (`yes`/`no`/`on`/`off`,
18
+ > `y`/`n`, in the usual cases) are accepted; this port cannot tell a quoted scalar from a plain
19
+ > one, so a quoted `"yes"` passes here (looser). Its run-time effect is engine behaviour and out
20
+ > of scope. Fixtures: `valid-strict-templates.yaml`, `invalid-strict-templates-type.yaml`.
21
+ > 3. **`template_default_piped`.** The template function `default` takes the value FIRST
22
+ > (`default .x "fallback"`). A pipeline that passes a value into it as a **non-first command**
23
+ > (`{{ .x | default "w" }}`, mid-chain, parenthesised, inside `if`/`else if`/`range`/`with`/
24
+ > `template`/`block`/`define`, or after a `$v :=` declaration) hands the piped value as the
25
+ > LAST argument, so the fallback is always the result and nothing fails at run time. An
26
+ > **error** at the top level and a **warning** inside a loop body (positional, exactly like
27
+ > `template_syntax_error`; the code survives the demotion), reported at the field of the
28
+ > template with its pipeline in the message. The rule runs only on a template that raised no
29
+ > syntax or function finding, because the reference walks a parse tree that exists only for a
30
+ > template that parses. Message and suggestion are the reference's text (spec section
31
+ > `templateDefaultPiped`). Never flagged: a field (`.query.default`), a variable (`$default`),
32
+ > a string or raw-string literal containing the word, a comment, `default` as the FIRST
33
+ > command (`default .x "w"`, `default (.x | upper) "w"`). The reference's run door tolerates
34
+ > this code (it only refuses a run for other errors); this port has no run door. Fixtures:
35
+ > `invalid-template-default-piped.yaml`, `warn-template-default-piped-loop.yaml`,
36
+ > `valid-template-default-clean.yaml` (forbids the code in both severities: its whole risk is
37
+ > a false positive). The all-correct `valid-minimal.yaml` was rewritten from
38
+ > `{{ .query.name | default "world" }}` to `{{ default .query.name "world" }}`.
39
+ >
40
+ > **Known limits.** There is no Go parse tree here: `src/template/gotmpl-default.ts` is a small
41
+ > tokenizer over each `{{ ... }}` action (string, raw-string and character literals and
42
+ > comments respected; pipelines split on top-level `|`; parentheses recursed). Consequences:
43
+ > (a) the printed pipeline is rebuilt from tokens (whitespace normalised) and matches Go's
44
+ > printing for ordinary pipelines, but may differ in spelling for exotic ones (numeric
45
+ > literals are echoed as written); (b) a `default` reached as a method or chain (`(.x).default`) is not a function call and is not flagged; (c) only the
46
+ > template fields the existing syntax check already walks are covered; (d) the same pipeline
47
+ > text appearing twice in one template is reported once, as in the reference.
8
48
 
9
49
  > package 0.20.0 — **`compliance_residency_unenforced`**, in step with the reference
10
50
  > (AIgentFlow v2.821.0; `specVersion` 2.821.0). A **warning**, never an error, and it
@@ -1021,40 +1061,41 @@ Comparison contract: **error `code` + `valid` verdict**, not message wording. Th
1021
1061
 
1022
1062
  ## Rule map
1023
1063
 
1024
- | Area | Go source | JS module | Tested by |
1025
- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------------- |
1026
- | 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` |
1027
- | Executor URI shape (the ONE parser) + scheme | `ValidateFlow` executor-URL rule (parser.go), `ParseExecutorURLString` | `validators/executors.ts` | `validate.test.ts` |
1028
- | Query/property/array-item schema + array constraints | `validateQueryParameters`, `validateProperties`, `validateArrayItems`, `validateArrayConstraints` | `validators/querySchema.ts` | `validate.test.ts` |
1029
- | Response-expectation types + array items + `required` | `ValidateFlow` (response block), `validateSemantics` | `validators/responseExpectation.ts` | `validate.test.ts` |
1030
- | Response expectation nothing reads (`response_expectation_unread`, warning) | `validateResponseExpectationIsRead` (validation.go) | `validators/responseExpectation.ts` | `validate.test.ts`, `conformance.test.ts` |
1031
- | Error strategy (action, goto, max_delay, backoff, retry_on) | `validateErrorStrategy` | `validators/errorStrategy.ts` | `validate.test.ts` |
1032
- | 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` |
1033
- | `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` |
1034
- | 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` |
1035
- | `next.parallel` + orchestrator-next requirement | `validateNextLogic`, `validateOrchestratorNext` | `validators/nextLogic.ts` | `validate.test.ts` |
1036
- | Expression functions (XOR package/function) | `validateExpressionFunctions` | `validators/expressionFunctions.ts` | `validate.test.ts` |
1037
- | Expression-function catalog (`package:` refused, unknown `function:` refused) | `validateExpressionFunctionCatalog` (parser.go) | `validators/expressionFunctions.ts` | `validate.test.ts`, `conformance.test.ts` |
1038
- | Expression-function USE (`{{ fn_* }}` must be in the catalog AND declared) | `validateExpressionFunctionUsage` (parser.go) | `validators/expressionFunctions.ts` | `validate.test.ts`, `conformance.test.ts` |
1039
- | Loop / for_each / throttle | `validateLoop`, `validateForEach`, `validateThrottle` | `validators/loopForEachThrottle.ts` | `validate.test.ts` |
1040
- | 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` |
1041
- | Loop sub-step `next:` targets (same-loop only, sentinels, no parallel) | `validateLoopSubStepNext` (parser.go) | `validators/loopForEachThrottle.ts` | `validate.test.ts`, `conformance.test.ts` |
1042
- | Orchestrator structure + campaign requires orchestrator | `validateOrchestrator`, `validateAndNormalizeCampaign` | `validators/orchestratorCampaign.ts` | `validate.test.ts` |
1043
- | 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` |
1044
- | `tool_discovery` vocabulary, mock scenario `delay`, empty `output:` entry | `validateToolDiscoveryVocabulary`, `validateMockScenarioDelays`, `validateOutputParameters` (parser.go) | `validators/saveDoor.ts` | `validate.test.ts`, `conformance.test.ts` |
1045
- | `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` |
1046
- | 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` |
1047
- | 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` |
1048
- | The `examples:` block: 58 codes, one walker (ids, inputs, references, expectations, checkpoints, variants) | the examples walker (`ValidateExamples`) | `validators/examples.ts`, `validators/goUrl.ts` | `examples.test.ts`, `conformance.test.ts` |
1049
- | 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` |
1050
- | Credential bindings (`stored/...`, inject_as, exclusivity) | `validateStepCredentialBindings` | `validators/credentialBindings.ts` | `validate.test.ts` |
1051
- | `input_schema` definition + ordering lint | `ValidateInputSchemaDefinition`, `LintInputSchemaFieldOrdering` | `validators/inputSchema.ts` | `validate.test.ts` |
1052
- | Step `output_schema` definition (reuses the input-schema subset) | `ValidateInputSchemaDefinition` (on `step.OutputSchema`, parser.go) | `validators/outputSchema.ts` (+ shared `inputSchema.ts`) | `validate.test.ts` |
1053
- | `quality_gate:` block (rubric/threshold/on_fail/goto) | `FlowParser.validateQualityGate` (parser.go) | `validators/qualityGate.ts` | `validate.test.ts` |
1054
- | Processing-operation type + per-operation config keys | `validateProcessingOperationShape` | `validators/processingOperations.ts` | `validate.test.ts`, `conformance.test.ts` |
1055
- | 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` |
1056
- | An EU-only residency beside an unpinned region-lane `ai://` call (`compliance_residency_unenforced`, warning) | `validateResidencyEnforcement` (validation.residency.go) | `validators/complianceResidency.ts` | `v0200-compliance-residency.test.ts`, `conformance.test.ts` |
1057
- | Go-template syntax | `validateTemplateExpression` (Parse step) | `template/gotmpl-syntax.ts` + `validators/templates.ts` | `gotmpl-syntax.test.ts`, `validate.test.ts` |
1064
+ | Area | Go source | JS module | Tested by |
1065
+ | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------- |
1066
+ | 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` |
1067
+ | Executor URI shape (the ONE parser) + scheme | `ValidateFlow` executor-URL rule (parser.go), `ParseExecutorURLString` | `validators/executors.ts` | `validate.test.ts` |
1068
+ | Query/property/array-item schema + array constraints | `validateQueryParameters`, `validateProperties`, `validateArrayItems`, `validateArrayConstraints` | `validators/querySchema.ts` | `validate.test.ts` |
1069
+ | Response-expectation types + array items + `required` | `ValidateFlow` (response block), `validateSemantics` | `validators/responseExpectation.ts` | `validate.test.ts` |
1070
+ | Response expectation nothing reads (`response_expectation_unread`, warning) | `validateResponseExpectationIsRead` (validation.go) | `validators/responseExpectation.ts` | `validate.test.ts`, `conformance.test.ts` |
1071
+ | Error strategy (action, goto, max_delay, backoff, retry_on) | `validateErrorStrategy` | `validators/errorStrategy.ts` | `validate.test.ts` |
1072
+ | 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` |
1073
+ | `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` |
1074
+ | 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` |
1075
+ | `next.parallel` + orchestrator-next requirement | `validateNextLogic`, `validateOrchestratorNext` | `validators/nextLogic.ts` | `validate.test.ts` |
1076
+ | Expression functions (XOR package/function) | `validateExpressionFunctions` | `validators/expressionFunctions.ts` | `validate.test.ts` |
1077
+ | Expression-function catalog (`package:` refused, unknown `function:` refused) | `validateExpressionFunctionCatalog` (parser.go) | `validators/expressionFunctions.ts` | `validate.test.ts`, `conformance.test.ts` |
1078
+ | Expression-function USE (`{{ fn_* }}` must be in the catalog AND declared) | `validateExpressionFunctionUsage` (parser.go) | `validators/expressionFunctions.ts` | `validate.test.ts`, `conformance.test.ts` |
1079
+ | Loop / for_each / throttle | `validateLoop`, `validateForEach`, `validateThrottle` | `validators/loopForEachThrottle.ts` | `validate.test.ts` |
1080
+ | 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` |
1081
+ | Loop sub-step `next:` targets (same-loop only, sentinels, no parallel) | `validateLoopSubStepNext` (parser.go) | `validators/loopForEachThrottle.ts` | `validate.test.ts`, `conformance.test.ts` |
1082
+ | Orchestrator structure + campaign requires orchestrator | `validateOrchestrator`, `validateAndNormalizeCampaign` | `validators/orchestratorCampaign.ts` | `validate.test.ts` |
1083
+ | 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` |
1084
+ | `tool_discovery` vocabulary, mock scenario `delay`, empty `output:` entry | `validateToolDiscoveryVocabulary`, `validateMockScenarioDelays`, `validateOutputParameters` (parser.go) | `validators/saveDoor.ts` | `validate.test.ts`, `conformance.test.ts` |
1085
+ | `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` |
1086
+ | 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` |
1087
+ | 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` |
1088
+ | The `examples:` block: 58 codes, one walker (ids, inputs, references, expectations, checkpoints, variants) | the examples walker (`ValidateExamples`) | `validators/examples.ts`, `validators/goUrl.ts` | `examples.test.ts`, `conformance.test.ts` |
1089
+ | 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` |
1090
+ | Credential bindings (`stored/...`, inject_as, exclusivity) | `validateStepCredentialBindings` | `validators/credentialBindings.ts` | `validate.test.ts` |
1091
+ | `input_schema` definition + ordering lint | `ValidateInputSchemaDefinition`, `LintInputSchemaFieldOrdering` | `validators/inputSchema.ts` | `validate.test.ts` |
1092
+ | Step `output_schema` definition (reuses the input-schema subset) | `ValidateInputSchemaDefinition` (on `step.OutputSchema`, parser.go) | `validators/outputSchema.ts` (+ shared `inputSchema.ts`) | `validate.test.ts` |
1093
+ | `quality_gate:` block (rubric/threshold/on_fail/goto) | `FlowParser.validateQualityGate` (parser.go) | `validators/qualityGate.ts` | `validate.test.ts` |
1094
+ | Processing-operation type + per-operation config keys | `validateProcessingOperationShape` | `validators/processingOperations.ts` | `validate.test.ts`, `conformance.test.ts` |
1095
+ | 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` |
1096
+ | An EU-only residency beside an unpinned region-lane `ai://` call (`compliance_residency_unenforced`, warning) | `validateResidencyEnforcement` (validation.residency.go) | `validators/complianceResidency.ts` | `v0200-compliance-residency.test.ts`, `conformance.test.ts` |
1097
+ | Go-template syntax | `validateTemplateExpression` (Parse step) | `template/gotmpl-syntax.ts` + `validators/templates.ts` | `gotmpl-syntax.test.ts`, `validate.test.ts` |
1098
+ | A value piped into `default` (`template_default_piped`; error, warning in a loop body) | `pipedDefaultPipelines` (validation.templatedefault.go) | `template/gotmpl-default.ts` + `validators/templates.ts` | `v0210-template-default-piped.test.ts`, `conformance.test.ts` |
1058
1099
 
1059
1100
  ---
1060
1101
 
package/README.md CHANGED
@@ -116,7 +116,7 @@ Low-level: parse YAML into `{ flow?, parseErrors, parseWarnings }` without valid
116
116
 
117
117
  ### `SPEC_VERSION: string`
118
118
 
119
- The AIgentFlow flow-schema version this build tracks (e.g. `"2.821.0"`).
119
+ The AIgentFlow flow-schema version this build tracks (e.g. `"2.827.0"`).
120
120
 
121
121
  ### Options
122
122
 
@@ -183,7 +183,7 @@ interface ValidationIssue {
183
183
  - **`tool_discovery`** — `eager`/`lazy`/`off` on the flow, the orchestrator, and a step `query` (empty and templated values skipped).
184
184
  - **Mock scenarios** — each `delay` must be a Go duration (`100ms`, not `100`), judged by its written spelling: `0` saves, `0.0` does not.
185
185
  - **`output:`** — no empty-string entries.
186
- - **Templates** — Go `text/template` **syntax** across query, processing, and conditions.
186
+ - **Templates** — Go `text/template` **syntax** across query, processing, and conditions; and a value piped into `default` (`{{ .x | default "w" }}`, error; warning in a loop body — `default` takes the value first: `{{ default .x "w" }}`).
187
187
 
188
188
  ## What it does **not** check
189
189
 
package/dist/cli.js CHANGED
@@ -307,6 +307,17 @@ function isValidGoDuration(input) {
307
307
  var RESERVED_STEP_ID_CHAR = ".";
308
308
  var RESERVED_STEP_ID_ORCHESTRATOR = "orchestrator";
309
309
  var DISPLAY_NAME_MAX_LEN = 80;
310
+ var YAML11_BOOL_WORDS = new Set(
311
+ ["y", "yes", "on", "n", "no", "off"].flatMap((w) => [
312
+ w,
313
+ w.toUpperCase(),
314
+ w[0].toUpperCase() + w.slice(1)
315
+ ])
316
+ );
317
+ function isBoolLike(v) {
318
+ if (v === null || typeof v === "boolean") return true;
319
+ return typeof v === "string" && YAML11_BOOL_WORDS.has(v);
320
+ }
310
321
  function validateBasicStructure(flow, issues) {
311
322
  if (!issues.nonEmptyStringOf(flow, "aigentflow_version", "")) {
312
323
  issues.error({
@@ -336,6 +347,14 @@ function validateBasicStructure(flow, issues) {
336
347
  });
337
348
  }
338
349
  }
350
+ if (flow.strict_templates !== void 0 && !isBoolLike(flow.strict_templates)) {
351
+ issues.error({
352
+ field: "strict_templates",
353
+ message: "strict_templates must be a boolean (true or false)",
354
+ code: "invalid_type",
355
+ suggestion: "Write 'strict_templates: true' or remove the key"
356
+ });
357
+ }
339
358
  const start = issues.stringOf(flow, "start", "");
340
359
  if (start === null || start === "") {
341
360
  issues.error({
@@ -436,7 +455,7 @@ function validateBasicStructure(flow, issues) {
436
455
  // src/spec/aigentflow-spec.json
437
456
  var aigentflow_spec_default = {
438
457
  $comment: "Vendored enum surface mirrored from the AIgentFlow Go reference implementation. Regenerate / review on every flow-YAML schema change. See PARITY.md. Values here are public protocol/type names only.",
439
- specVersion: "2.821.0",
458
+ specVersion: "2.827.0",
440
459
  inputSchemaVersion: 1,
441
460
  executorSchemes: [
442
461
  "a2a",
@@ -460,6 +479,7 @@ var aigentflow_spec_default = {
460
479
  "geocoding",
461
480
  "geovisualize",
462
481
  "getmd",
482
+ "here",
463
483
  "homeassistant",
464
484
  "hosting",
465
485
  "http",
@@ -1275,6 +1295,13 @@ var aigentflow_spec_default = {
1275
1295
  }
1276
1296
  }
1277
1297
  },
1298
+ templateDefaultPiped: {
1299
+ $comment: "template_default_piped (ERROR at the top level, WARNING inside a loop body, like template_syntax_error). The template function `default` takes the value FIRST; a pipeline that passes a value into it as a non-first command hands the piped value as the LAST argument, so the fallback is always the result. Flagged: any pipeline (in any action, including if / else if / range / with / template / block conditions, parenthesised sub-pipelines and variable declarations) in which a command whose first word is the bare identifier `default` is not the FIRST command. Never flagged: a field, a variable, a string literal, a comment, `default` as the first command. The message fills %s with the pipeline.",
1300
+ code: "template_default_piped",
1301
+ function: "default",
1302
+ messageFormat: "`default` takes the value FIRST, and a pipe passes the piped value LAST: `%s` is `default <fallback> <value>`, so the fallback is always the result",
1303
+ suggestion: 'write the value first and the fallback second, as a call: `{{ default .x "fallback" }}`'
1304
+ },
1278
1305
  complianceResidency: {
1279
1306
  $comment: "compliance_residency_unenforced (WARNING, never an error; needs no catalogue). When compliance.residency is non-empty and every code (trimmed, upper-cased) is in euResidencyCodes, each top-level step and loop sub-step whose executor starts with aiProtocol+protocolSeparator, then the orchestrator (provider = its exons execution.provider), warns when its provider (literal, lower-cased; skipped when empty or containing templateActionOpen) is in regionLaneProviders and the call is not pinned. Pinned = query region (orchestrator.region) a non-empty string after trimming; OR enforce_region true (query: bool true or the strings in enforceRegionTrueStrings; orchestrator: bool true); OR the author named the endpoint: query <provider>+baseUrlParamSuffix a non-empty string after trimming, or executor_config.<provider>.base_url non-empty after trimming. The provider segment of ai://<provider>/<operation> exists only when a slash follows it. regionLaneProviders is the set of providers whose adapter routes on `region`; a provider outside it has no region lane and is never warned. Message, suggestion and site formats fill %s and %q (a Go-quoted string) in order; the message's last placeholder is the residency list as written, printed [A B]. Reference: validateResidencyEnforcement (validation.residency.go).",
1280
1307
  code: "compliance_residency_unenforced",
@@ -1638,6 +1665,7 @@ var aigentflow_spec_default = {
1638
1665
  rate_limit: "FlowRateLimitConfig",
1639
1666
  start: "scalar",
1640
1667
  steps: "map<StepDefinition>",
1668
+ strict_templates: "scalar",
1641
1669
  tags: "list<scalar>",
1642
1670
  template: "FlowTemplateMetadata",
1643
1671
  tool_discovery: "scalar",
@@ -2010,6 +2038,12 @@ var EXAMPLES = {
2010
2038
  })),
2011
2039
  codes: aigentflow_spec_default.examples.codes
2012
2040
  };
2041
+ var TEMPLATE_DEFAULT_PIPED = {
2042
+ code: aigentflow_spec_default.templateDefaultPiped.code,
2043
+ function: aigentflow_spec_default.templateDefaultPiped.function,
2044
+ messageFormat: aigentflow_spec_default.templateDefaultPiped.messageFormat,
2045
+ suggestion: aigentflow_spec_default.templateDefaultPiped.suggestion
2046
+ };
2013
2047
 
2014
2048
  // src/validators/executors.ts
2015
2049
  var SCHEME_SEPARATOR = "://";
@@ -3973,6 +4007,117 @@ function splitPipeline(body) {
3973
4007
  return stages;
3974
4008
  }
3975
4009
 
4010
+ // src/template/gotmpl-default.ts
4011
+ var FUNCTION_NAME = "default";
4012
+ var PIPE = "|";
4013
+ var OPEN_PAREN = "(";
4014
+ var CLOSE_PAREN = ")";
4015
+ var MAX_NESTING_DEPTH = 100;
4016
+ var PIPELINE_KEYWORDS = /^(?:else\s+if|else\s+with|if|range|with|template|block)(?![\w$.])/;
4017
+ var NO_PIPELINE = /^(?:define|else|end|break|continue)(?![\w$.])/;
4018
+ var DECLARATION = /^\$\w*(?:\s*,\s*\$\w*)*\s*:?=/;
4019
+ function pipedDefaultPipelines(src) {
4020
+ const { actions, fatal } = lexActions(src);
4021
+ if (fatal) return [];
4022
+ const found = /* @__PURE__ */ new Set();
4023
+ try {
4024
+ for (const action of actions) {
4025
+ if (action.isComment || action.body === "") continue;
4026
+ scanAction(action.body, found);
4027
+ }
4028
+ } catch (err) {
4029
+ if (!(err instanceof RangeError)) throw err;
4030
+ return [];
4031
+ }
4032
+ return [...found].sort();
4033
+ }
4034
+ function scanAction(body, found) {
4035
+ let rest = body;
4036
+ const kw = PIPELINE_KEYWORDS.exec(rest);
4037
+ if (!kw && NO_PIPELINE.test(rest)) return;
4038
+ if (kw) {
4039
+ rest = rest.slice(kw[0].length).trimStart();
4040
+ const keyword = kw[0].replace(/\s+/g, " ");
4041
+ if (keyword === "template" || keyword === "block") {
4042
+ const q2 = rest[0];
4043
+ if (q2 === '"' || q2 === "`") {
4044
+ const end = scanString(rest, 0, q2);
4045
+ if (end === -1) return;
4046
+ rest = rest.slice(end + 1).trimStart();
4047
+ }
4048
+ }
4049
+ }
4050
+ if (rest === "") return;
4051
+ parsePipeline(rest, 0, false, found, 0);
4052
+ }
4053
+ function parsePipeline(src, start, nested, found, depth) {
4054
+ if (depth > MAX_NESTING_DEPTH) return { text: src.slice(start), end: src.length };
4055
+ let i = start;
4056
+ const n = src.length;
4057
+ let decl = "";
4058
+ const decMatch = DECLARATION.exec(src.slice(i));
4059
+ if (decMatch) {
4060
+ decl = decMatch[0].replace(/\s+/g, " ") + " ";
4061
+ i += decMatch[0].length;
4062
+ }
4063
+ const commands = [];
4064
+ let current = [];
4065
+ let firstWords = [];
4066
+ let pipedDefault = false;
4067
+ const pushCommand = () => {
4068
+ if (current.length > 0) {
4069
+ commands.push(current);
4070
+ firstWords.push(current[0]);
4071
+ }
4072
+ current = [];
4073
+ };
4074
+ while (i < n) {
4075
+ const c = src[i];
4076
+ if (c === " " || c === " " || c === "\n" || c === "\r") {
4077
+ i += 1;
4078
+ continue;
4079
+ }
4080
+ if (c === '"' || c === "`" || c === "'") {
4081
+ const end = scanString(src, i, c);
4082
+ const stop = end === -1 ? n - 1 : end;
4083
+ current.push(src.slice(i, stop + 1));
4084
+ i = stop + 1;
4085
+ continue;
4086
+ }
4087
+ if (c === OPEN_PAREN) {
4088
+ const inner = parsePipeline(src, i + 1, true, found, depth + 1);
4089
+ current.push(`(${inner.text})`);
4090
+ i = inner.end;
4091
+ continue;
4092
+ }
4093
+ if (c === CLOSE_PAREN) {
4094
+ if (nested) {
4095
+ i += 1;
4096
+ break;
4097
+ }
4098
+ i += 1;
4099
+ continue;
4100
+ }
4101
+ if (c === PIPE) {
4102
+ pushCommand();
4103
+ i += 1;
4104
+ continue;
4105
+ }
4106
+ let j = i;
4107
+ while (j < n && !/[\s|()"`']/.test(src[j])) j += 1;
4108
+ if (j === i) j = i + 1;
4109
+ current.push(src.slice(i, j));
4110
+ i = j;
4111
+ }
4112
+ pushCommand();
4113
+ firstWords.forEach((word, idx) => {
4114
+ if (idx > 0 && word === FUNCTION_NAME) pipedDefault = true;
4115
+ });
4116
+ const text = decl + commands.map((cmd) => cmd.join(" ")).join(` ${PIPE} `);
4117
+ if (pipedDefault) found.add(text);
4118
+ return { text, end: i };
4119
+ }
4120
+
3976
4121
  // src/validators/processingOperations.ts
3977
4122
  var PROCESSING_OP_GUARD_KEY = PROCESSING_OPERATIONS.guardKey;
3978
4123
  var PROCESSING_PHASES = ["pre_processing", "post_processing"];
@@ -4119,6 +4264,17 @@ function checkTemplateString(value, field, stepID, issues, stats, opts, demote =
4119
4264
  });
4120
4265
  }
4121
4266
  }
4267
+ if (errors.length > 0) return;
4268
+ for (const pipe of pipedDefaultPipelines(value)) {
4269
+ report({
4270
+ field,
4271
+ message: TEMPLATE_DEFAULT_PIPED.messageFormat.replace("%s", () => pipe),
4272
+ code: TEMPLATE_DEFAULT_PIPED.code,
4273
+ stepId: stepID,
4274
+ context: `Template: ${value}`,
4275
+ suggestion: TEMPLATE_DEFAULT_PIPED.suggestion
4276
+ });
4277
+ }
4122
4278
  }
4123
4279
  function walkStrings2(basePath, value, stepID, issues, stats, opts, check, demote = false) {
4124
4280
  if (isString(value)) {
package/dist/index.cjs CHANGED
@@ -304,6 +304,17 @@ function isValidGoDuration(input) {
304
304
  var RESERVED_STEP_ID_CHAR = ".";
305
305
  var RESERVED_STEP_ID_ORCHESTRATOR = "orchestrator";
306
306
  var DISPLAY_NAME_MAX_LEN = 80;
307
+ var YAML11_BOOL_WORDS = new Set(
308
+ ["y", "yes", "on", "n", "no", "off"].flatMap((w) => [
309
+ w,
310
+ w.toUpperCase(),
311
+ w[0].toUpperCase() + w.slice(1)
312
+ ])
313
+ );
314
+ function isBoolLike(v) {
315
+ if (v === null || typeof v === "boolean") return true;
316
+ return typeof v === "string" && YAML11_BOOL_WORDS.has(v);
317
+ }
307
318
  function validateBasicStructure(flow, issues) {
308
319
  if (!issues.nonEmptyStringOf(flow, "aigentflow_version", "")) {
309
320
  issues.error({
@@ -333,6 +344,14 @@ function validateBasicStructure(flow, issues) {
333
344
  });
334
345
  }
335
346
  }
347
+ if (flow.strict_templates !== void 0 && !isBoolLike(flow.strict_templates)) {
348
+ issues.error({
349
+ field: "strict_templates",
350
+ message: "strict_templates must be a boolean (true or false)",
351
+ code: "invalid_type",
352
+ suggestion: "Write 'strict_templates: true' or remove the key"
353
+ });
354
+ }
336
355
  const start = issues.stringOf(flow, "start", "");
337
356
  if (start === null || start === "") {
338
357
  issues.error({
@@ -432,7 +451,7 @@ function validateBasicStructure(flow, issues) {
432
451
 
433
452
  // src/spec/aigentflow-spec.json
434
453
  var aigentflow_spec_default = {
435
- specVersion: "2.821.0",
454
+ specVersion: "2.827.0",
436
455
  inputSchemaVersion: 1,
437
456
  executorSchemes: [
438
457
  "a2a",
@@ -456,6 +475,7 @@ var aigentflow_spec_default = {
456
475
  "geocoding",
457
476
  "geovisualize",
458
477
  "getmd",
478
+ "here",
459
479
  "homeassistant",
460
480
  "hosting",
461
481
  "http",
@@ -1257,6 +1277,11 @@ var aigentflow_spec_default = {
1257
1277
  }
1258
1278
  }
1259
1279
  },
1280
+ templateDefaultPiped: {
1281
+ code: "template_default_piped",
1282
+ messageFormat: "`default` takes the value FIRST, and a pipe passes the piped value LAST: `%s` is `default <fallback> <value>`, so the fallback is always the result",
1283
+ suggestion: 'write the value first and the fallback second, as a call: `{{ default .x "fallback" }}`'
1284
+ },
1260
1285
  complianceResidency: {
1261
1286
  code: "compliance_residency_unenforced",
1262
1287
  euResidencyCodes: ["EU", "DE", "FR", "IE", "NL", "IT", "ES"],
@@ -1555,6 +1580,7 @@ var aigentflow_spec_default = {
1555
1580
  rate_limit: "FlowRateLimitConfig",
1556
1581
  start: "scalar",
1557
1582
  steps: "map<StepDefinition>",
1583
+ strict_templates: "scalar",
1558
1584
  tags: "list<scalar>",
1559
1585
  template: "FlowTemplateMetadata",
1560
1586
  tool_discovery: "scalar",
@@ -1916,6 +1942,11 @@ var EXAMPLES = {
1916
1942
  name: p.name,
1917
1943
  re: new RegExp(p.expr, p.flags)
1918
1944
  }))};
1945
+ var TEMPLATE_DEFAULT_PIPED = {
1946
+ code: aigentflow_spec_default.templateDefaultPiped.code,
1947
+ messageFormat: aigentflow_spec_default.templateDefaultPiped.messageFormat,
1948
+ suggestion: aigentflow_spec_default.templateDefaultPiped.suggestion
1949
+ };
1919
1950
 
1920
1951
  // src/validators/executors.ts
1921
1952
  var SCHEME_SEPARATOR = "://";
@@ -3879,6 +3910,117 @@ function splitPipeline(body) {
3879
3910
  return stages;
3880
3911
  }
3881
3912
 
3913
+ // src/template/gotmpl-default.ts
3914
+ var FUNCTION_NAME = "default";
3915
+ var PIPE = "|";
3916
+ var OPEN_PAREN = "(";
3917
+ var CLOSE_PAREN = ")";
3918
+ var MAX_NESTING_DEPTH = 100;
3919
+ var PIPELINE_KEYWORDS = /^(?:else\s+if|else\s+with|if|range|with|template|block)(?![\w$.])/;
3920
+ var NO_PIPELINE = /^(?:define|else|end|break|continue)(?![\w$.])/;
3921
+ var DECLARATION = /^\$\w*(?:\s*,\s*\$\w*)*\s*:?=/;
3922
+ function pipedDefaultPipelines(src) {
3923
+ const { actions, fatal } = lexActions(src);
3924
+ if (fatal) return [];
3925
+ const found = /* @__PURE__ */ new Set();
3926
+ try {
3927
+ for (const action of actions) {
3928
+ if (action.isComment || action.body === "") continue;
3929
+ scanAction(action.body, found);
3930
+ }
3931
+ } catch (err) {
3932
+ if (!(err instanceof RangeError)) throw err;
3933
+ return [];
3934
+ }
3935
+ return [...found].sort();
3936
+ }
3937
+ function scanAction(body, found) {
3938
+ let rest = body;
3939
+ const kw = PIPELINE_KEYWORDS.exec(rest);
3940
+ if (!kw && NO_PIPELINE.test(rest)) return;
3941
+ if (kw) {
3942
+ rest = rest.slice(kw[0].length).trimStart();
3943
+ const keyword = kw[0].replace(/\s+/g, " ");
3944
+ if (keyword === "template" || keyword === "block") {
3945
+ const q2 = rest[0];
3946
+ if (q2 === '"' || q2 === "`") {
3947
+ const end = scanString(rest, 0, q2);
3948
+ if (end === -1) return;
3949
+ rest = rest.slice(end + 1).trimStart();
3950
+ }
3951
+ }
3952
+ }
3953
+ if (rest === "") return;
3954
+ parsePipeline(rest, 0, false, found, 0);
3955
+ }
3956
+ function parsePipeline(src, start, nested, found, depth) {
3957
+ if (depth > MAX_NESTING_DEPTH) return { text: src.slice(start), end: src.length };
3958
+ let i = start;
3959
+ const n = src.length;
3960
+ let decl = "";
3961
+ const decMatch = DECLARATION.exec(src.slice(i));
3962
+ if (decMatch) {
3963
+ decl = decMatch[0].replace(/\s+/g, " ") + " ";
3964
+ i += decMatch[0].length;
3965
+ }
3966
+ const commands = [];
3967
+ let current = [];
3968
+ let firstWords = [];
3969
+ let pipedDefault = false;
3970
+ const pushCommand = () => {
3971
+ if (current.length > 0) {
3972
+ commands.push(current);
3973
+ firstWords.push(current[0]);
3974
+ }
3975
+ current = [];
3976
+ };
3977
+ while (i < n) {
3978
+ const c = src[i];
3979
+ if (c === " " || c === " " || c === "\n" || c === "\r") {
3980
+ i += 1;
3981
+ continue;
3982
+ }
3983
+ if (c === '"' || c === "`" || c === "'") {
3984
+ const end = scanString(src, i, c);
3985
+ const stop = end === -1 ? n - 1 : end;
3986
+ current.push(src.slice(i, stop + 1));
3987
+ i = stop + 1;
3988
+ continue;
3989
+ }
3990
+ if (c === OPEN_PAREN) {
3991
+ const inner = parsePipeline(src, i + 1, true, found, depth + 1);
3992
+ current.push(`(${inner.text})`);
3993
+ i = inner.end;
3994
+ continue;
3995
+ }
3996
+ if (c === CLOSE_PAREN) {
3997
+ if (nested) {
3998
+ i += 1;
3999
+ break;
4000
+ }
4001
+ i += 1;
4002
+ continue;
4003
+ }
4004
+ if (c === PIPE) {
4005
+ pushCommand();
4006
+ i += 1;
4007
+ continue;
4008
+ }
4009
+ let j = i;
4010
+ while (j < n && !/[\s|()"`']/.test(src[j])) j += 1;
4011
+ if (j === i) j = i + 1;
4012
+ current.push(src.slice(i, j));
4013
+ i = j;
4014
+ }
4015
+ pushCommand();
4016
+ firstWords.forEach((word, idx) => {
4017
+ if (idx > 0 && word === FUNCTION_NAME) pipedDefault = true;
4018
+ });
4019
+ const text = decl + commands.map((cmd) => cmd.join(" ")).join(` ${PIPE} `);
4020
+ if (pipedDefault) found.add(text);
4021
+ return { text, end: i };
4022
+ }
4023
+
3882
4024
  // src/validators/processingOperations.ts
3883
4025
  var PROCESSING_OP_GUARD_KEY = PROCESSING_OPERATIONS.guardKey;
3884
4026
  var PROCESSING_PHASES = ["pre_processing", "post_processing"];
@@ -4025,6 +4167,17 @@ function checkTemplateString(value, field, stepID, issues, stats, opts, demote =
4025
4167
  });
4026
4168
  }
4027
4169
  }
4170
+ if (errors.length > 0) return;
4171
+ for (const pipe of pipedDefaultPipelines(value)) {
4172
+ report({
4173
+ field,
4174
+ message: TEMPLATE_DEFAULT_PIPED.messageFormat.replace("%s", () => pipe),
4175
+ code: TEMPLATE_DEFAULT_PIPED.code,
4176
+ stepId: stepID,
4177
+ context: `Template: ${value}`,
4178
+ suggestion: TEMPLATE_DEFAULT_PIPED.suggestion
4179
+ });
4180
+ }
4028
4181
  }
4029
4182
  function walkStrings2(basePath, value, stepID, issues, stats, opts, check, demote = false) {
4030
4183
  if (isString(value)) {