agent-contracts 0.11.4 → 0.11.6

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/README.md CHANGED
@@ -221,12 +221,13 @@ A **Workflow** defines a phase-level execution sequence:
221
221
  * `entry_conditions`
222
222
  * `trigger`
223
223
  * `external_participants` — actors/participants outside the agent system (e.g., User, external advisory)
224
- * ordered steps (handoff, validation, decision)
224
+ * ordered steps (delegate, gate, validation, decision)
225
225
 
226
226
  Workflow steps support additional properties:
227
227
 
228
228
  * `group` — consecutive steps with the same group are rendered as `par` (parallel) blocks in sequence diagrams
229
- * `retry` (handoff steps only) — defines a conditional retry loop with `condition`, `fix_task`, and optional `revalidate_task`
229
+ * `retry` (delegate steps) — defines a conditional retry loop with `condition`, `fix_task`, and optional `revalidate_task`
230
+ * `routing_key` (decision steps) — the field that determines branch selection. The legacy field `on` is still accepted but deprecated due to YAML 1.1 reserved word collision
230
231
 
231
232
  ### Guardrail
232
233
 
@@ -305,6 +306,9 @@ Design regressions become testable.
305
306
  * **Software bindings** (DI) for tool-specific guardrail implementation (Cursor, Git, GitHub)
306
307
  * **Guardrail generation** from DSL + policy + bindings via `generate guardrails`
307
308
  * **Flexible file splitting** via `$ref` (replacement), `$refs` (import + deep-merge), and JSON Pointer `$ref` (in-document)
309
+ * **YAML safety linting** for reserved word collision detection across YAML 1.1/1.2
310
+ * **`x-extensions` declarations** for documenting project-specific custom extension fields
311
+ * **`resolve --expand-defaults`** to materialize all Zod schema defaults in output
308
312
  * **JSON Schema for editor support and external tooling**
309
313
  * **CI-friendly workflow checks**
310
314
 
@@ -342,6 +346,16 @@ guardrails: {}
342
346
  guardrail_policies: {}
343
347
  components:
344
348
  schemas: {}
349
+
350
+ # Optional: declare project-specific x-* extensions
351
+ x-extensions:
352
+ x-flags:
353
+ type: array
354
+ items: string
355
+ description: "CLI flags for tool commands"
356
+ x-check-script:
357
+ type: string
358
+ description: "Path to hook check script"
345
359
  ````
346
360
 
347
361
  This makes definitions easy to merge, extend, and reference by stable identifiers.
@@ -572,6 +586,23 @@ tasks:
572
586
  `x-` prefixed custom properties work at any nesting level — including inside
573
587
  `execution_steps`, `rules`, `workflow.steps`, and other nested objects.
574
588
 
589
+ ### `x-extensions` declarations
590
+
591
+ Projects can optionally declare their custom `x-*` extension fields in the DSL using `x-extensions`. This makes extensions discoverable and self-documenting:
592
+
593
+ ````yaml
594
+ x-extensions:
595
+ x-flags:
596
+ type: array
597
+ items: string
598
+ description: "CLI flags for tool commands"
599
+ x-check-script:
600
+ type: string
601
+ description: "Path to hook check script"
602
+ ````
603
+
604
+ Each key must start with `x-` (validated at schema level). The `type`, `items`, and `description` fields describe the expected shape. This declaration is informational today; future versions may validate `x-*` field values against their declared schemas.
605
+
575
606
  ---
576
607
 
577
608
  ## Example: Artifact definition
@@ -602,21 +633,20 @@ workflow:
602
633
  - User story or feature request received
603
634
  trigger: "User invokes /speckit.specify or asks to create a feature spec."
604
635
  steps:
605
- - type: handoff
606
- handoff_kind: delegation
636
+ - type: delegate
607
637
  task: specify-feature
608
638
  from_agent: main-architect
609
639
  - type: validation
610
- validation_id: spec-semantic-review
640
+ validation: spec-semantic-review
611
641
  - type: decision
612
- agent: main-architect
613
- options:
614
- - label: approve
615
- next_workflow: plan
616
- - label: revise
617
- retry_from_step: 0
642
+ routing_key: evidence-gate-verdict.verdict
643
+ branches:
644
+ PASS: [plan]
645
+ REVISE: [specify-feature]
618
646
  ````
619
647
 
648
+ Decision steps use `routing_key` to specify the field that determines branching. The legacy `on` field is still accepted but deprecated — see [YAML safety](#yaml-safety) below.
649
+
620
650
  ---
621
651
 
622
652
  ## Example: Handoff type definition
@@ -819,10 +849,19 @@ npx agent-contracts
819
849
  The `[path]` argument defaults to `agent-contracts.yaml` in the current directory.
820
850
  If `-c` / `--config` is specified, the DSL path from the config file is used.
821
851
 
852
+ #### `resolve` options
853
+
854
+ | Option | Description |
855
+ |--------|-------------|
856
+ | `--format <text\|json>` | Output format (default: `text`) |
857
+ | `--expand-defaults` | Expand all Zod default values in output. Fields like `required_validations: []`, `tags: []`, and `can_read_artifacts: []` are written explicitly instead of being silently applied by schema defaults. |
858
+ | `-c, --config <path>` | Path to `agent-contracts.config.yaml` |
859
+
822
860
  ### Common usage
823
861
 
824
862
  ````bash
825
863
  agent-contracts resolve
864
+ agent-contracts resolve --expand-defaults --format json
826
865
  agent-contracts validate
827
866
  agent-contracts lint --strict
828
867
  agent-contracts render -c agent-contracts.config.yaml
@@ -1153,6 +1192,34 @@ Checks:
1153
1192
 
1154
1193
  Custom properties with `x-` prefix are allowed on any object in the DSL — top-level entities (agents, tasks, artifacts, …), nested objects (rules, execution steps, workflow steps, …), and the root DSL itself.
1155
1194
 
1195
+ ### YAML safety
1196
+
1197
+ The DSL is expressed in YAML, which introduces risks from YAML 1.1's implicit type coercion. The `yaml-reserved-key-safety` lint rule warns when reserved words appear in positions that may be misinterpreted by non-1.2 parsers.
1198
+
1199
+ The most notable case is the `on` field in decision steps. In YAML 1.1, bare `on` as a mapping key is interpreted as boolean `true`. While `agent-contracts` uses a YAML 1.2 parser internally, DSL consumers (CI tools, editors, other parsers) may use YAML 1.1 parsers.
1200
+
1201
+ To address this:
1202
+
1203
+ * Decision steps now support `routing_key` as the preferred field name (replacing `on`)
1204
+ * The legacy `on` field is still accepted for backward compatibility but triggers a lint warning
1205
+ * Branch keys like `yes`, `no`, `true`, `false` also trigger warnings
1206
+
1207
+ ````yaml
1208
+ # Preferred — safe across all YAML versions
1209
+ - type: decision
1210
+ routing_key: evidence-gate-verdict.verdict
1211
+ branches:
1212
+ PASS: [release]
1213
+ FAIL: [fix-violations]
1214
+
1215
+ # Deprecated — works but triggers yaml-reserved-key-safety warning
1216
+ - type: decision
1217
+ on: evidence-gate-verdict.verdict
1218
+ branches:
1219
+ PASS: [release]
1220
+ FAIL: [fix-violations]
1221
+ ````
1222
+
1156
1223
  ### Reference integrity
1157
1224
 
1158
1225
  Checks:
@@ -1167,15 +1234,22 @@ Checks:
1167
1234
  Checks:
1168
1235
 
1169
1236
  * bidirectional consistency
1170
- * validation coverage
1237
+ * validation coverage — warns when artifacts lack validations or have empty `required_validations` (fails under `--strict`)
1238
+ * artifact-required-validation wiring — verifies every entry in `artifact.required_validations` exists, targets the correct artifact, and is referenced in a workflow step or task
1239
+ * task-output-validation completeness — checks that tasks producing artifacts (via `execution_steps.produces_artifact` or agent `can_write_artifacts`) cover those artifacts' `required_validations`
1171
1240
  * workflow graph completeness
1172
1241
  * merge integrity
1173
1242
  * read-only write violations
1174
1243
  * prerequisite readability
1175
1244
  * artifact ownership — `produces_artifact`/`reads_artifact` in execution steps vs. artifact producers/editors/consumers
1176
1245
  * tool commands — `commands[].reads`/`commands[].writes` reference valid artifacts and align with `output_artifacts`
1246
+ * YAML safety — warns when YAML 1.1 reserved words (`on`, `yes`, `no`, `true`, `false`, etc.) are used in positions where they may be misinterpreted by non-1.2 parsers
1177
1247
  * naming/style issues through Spectral rules
1178
1248
 
1249
+ #### `--strict` mode
1250
+
1251
+ When `--strict` is passed to `lint` or `check`, warnings are treated as failures (exit code 1). This is particularly relevant for artifact-centric validation rules — empty `required_validations`, orphaned validation wiring, and incomplete task coverage are all warnings that become blocking under `--strict`.
1252
+
1179
1253
  ---
1180
1254
 
1181
1255
  ## Best used with runtime frameworks
package/dist/cli.js CHANGED
@@ -50728,7 +50728,9 @@ var WorkflowValidationStepSchema = z11.object({
50728
50728
  var WorkflowDecisionStepSchema = z11.object({
50729
50729
  type: z11.literal("decision"),
50730
50730
  description: z11.string().optional(),
50731
- on: z11.string(),
50731
+ /** @deprecated Use `routing_key` instead. `on` is kept for backward compatibility. */
50732
+ on: z11.string().optional(),
50733
+ routing_key: z11.string().optional(),
50732
50734
  branches: z11.record(z11.string(), z11.array(z11.string())),
50733
50735
  group: z11.string().optional()
50734
50736
  }).passthrough();
@@ -50751,6 +50753,11 @@ var WorkflowSchema = z11.object({
50751
50753
  var ComponentsSchema = z12.object({
50752
50754
  schemas: z12.record(z12.string(), z12.record(z12.string(), z12.any())).default({})
50753
50755
  }).passthrough();
50756
+ var XExtensionDeclSchema = z12.object({
50757
+ type: z12.string(),
50758
+ items: z12.string().optional(),
50759
+ description: z12.string().optional()
50760
+ });
50754
50761
  var DslSchema = z12.object({
50755
50762
  version: z12.literal(1),
50756
50763
  extends: z12.string().optional(),
@@ -50765,7 +50772,8 @@ var DslSchema = z12.object({
50765
50772
  policies: z12.record(z12.string(), PolicySchema).default({}),
50766
50773
  guardrails: z12.record(z12.string(), GuardrailSchema).default({}),
50767
50774
  guardrail_policies: z12.record(z12.string(), GuardrailPolicySchema).default({}),
50768
- components: ComponentsSchema.default({ schemas: {} })
50775
+ components: ComponentsSchema.default({ schemas: {} }),
50776
+ "x-extensions": z12.record(z12.string(), XExtensionDeclSchema).optional()
50769
50777
  }).passthrough();
50770
50778
 
50771
50779
  // src/schema/json-schema-utils.ts
@@ -51620,6 +51628,13 @@ function substituteVars(data, vars) {
51620
51628
  return walk(data, vars);
51621
51629
  }
51622
51630
 
51631
+ // src/resolver/expand-defaults.ts
51632
+ function expandDefaults(data) {
51633
+ const result = DslSchema.safeParse(data);
51634
+ if (!result.success) return data;
51635
+ return JSON.parse(JSON.stringify(result.data));
51636
+ }
51637
+
51623
51638
  // src/config/binding-merger.ts
51624
51639
  function isRecord3(v2) {
51625
51640
  return typeof v2 === "object" && v2 !== null && !Array.isArray(v2);
@@ -51760,12 +51775,15 @@ ${issues}`,
51760
51775
  // src/cli/commands/resolve.ts
51761
51776
  import { stringify } from "yaml";
51762
51777
  var DIR_DEFAULT = "agent-contracts.yaml";
51763
- var resolveCommand = new Command("resolve").description("Resolve DSL (load + merge extends) and output YAML").argument("[dir]", "Path to agent-contracts.yaml", DIR_DEFAULT).option("-c, --config <path>", "Path to agent-contracts.config.yaml").option("--format <format>", "Output format (text|json)", "text").action(async (dir, opts) => {
51778
+ var resolveCommand = new Command("resolve").description("Resolve DSL (load + merge extends) and output YAML").argument("[dir]", "Path to agent-contracts.yaml", DIR_DEFAULT).option("-c, --config <path>", "Path to agent-contracts.config.yaml").option("--format <format>", "Output format (text|json)", "text").option("--expand-defaults", "Expand all Zod default values in output", false).action(async (dir, opts) => {
51764
51779
  try {
51765
51780
  const config = await loadConfig(opts.config);
51766
51781
  const dslPath = resolveDslPath(dir, DIR_DEFAULT, config);
51767
51782
  const result = await resolve4(dslPath);
51768
- const data = config?.vars ? substituteVars(result.data, config.vars) : result.data;
51783
+ let data = config?.vars ? substituteVars(result.data, config.vars) : result.data;
51784
+ if (opts.expandDefaults) {
51785
+ data = expandDefaults(data);
51786
+ }
51769
51787
  if (opts.format === "json") {
51770
51788
  process.stdout.write(JSON.stringify(data, null, 2) + "\n");
51771
51789
  } else {
@@ -51844,6 +51862,47 @@ function checkCustomPropsRecursive(data, schema, path) {
51844
51862
  }
51845
51863
  return [];
51846
51864
  }
51865
+ function checkXExtensionsKeys(data) {
51866
+ const extensions = data["x-extensions"];
51867
+ if (typeof extensions !== "object" || extensions === null) return [];
51868
+ const diagnostics = [];
51869
+ for (const key of Object.keys(extensions)) {
51870
+ if (!key.startsWith("x-")) {
51871
+ diagnostics.push({
51872
+ path: `x-extensions.${key}`,
51873
+ message: `Extension key "${key}" must start with "x-" prefix.`,
51874
+ code: "x-extension-key-prefix"
51875
+ });
51876
+ }
51877
+ }
51878
+ return diagnostics;
51879
+ }
51880
+ function checkDecisionStepRoutingKey(data) {
51881
+ const workflow = data["workflow"];
51882
+ if (typeof workflow !== "object" || workflow === null) return [];
51883
+ const diagnostics = [];
51884
+ for (const [wfKey, wf] of Object.entries(
51885
+ workflow
51886
+ )) {
51887
+ if (typeof wf !== "object" || wf === null) continue;
51888
+ const steps = wf["steps"];
51889
+ if (!Array.isArray(steps)) continue;
51890
+ for (let i3 = 0; i3 < steps.length; i3++) {
51891
+ const step = steps[i3];
51892
+ if (typeof step !== "object" || step === null) continue;
51893
+ const s2 = step;
51894
+ if (s2["type"] !== "decision") continue;
51895
+ if (s2["routing_key"] === void 0 && s2["on"] === void 0) {
51896
+ diagnostics.push({
51897
+ path: `workflow.${wfKey}.steps[${i3}]`,
51898
+ message: 'Decision step requires "routing_key" (or deprecated "on"). Prefer "routing_key".',
51899
+ code: "decision-missing-routing-key"
51900
+ });
51901
+ }
51902
+ }
51903
+ }
51904
+ return diagnostics;
51905
+ }
51847
51906
  function validateSchema(data) {
51848
51907
  const result = DslSchema.safeParse(data);
51849
51908
  if (!result.success) {
@@ -51858,6 +51917,14 @@ function validateSchema(data) {
51858
51917
  if (customPropDiagnostics.length > 0) {
51859
51918
  return { success: false, diagnostics: customPropDiagnostics };
51860
51919
  }
51920
+ const decisionDiagnostics = checkDecisionStepRoutingKey(data);
51921
+ if (decisionDiagnostics.length > 0) {
51922
+ return { success: false, diagnostics: decisionDiagnostics };
51923
+ }
51924
+ const xExtDiagnostics = checkXExtensionsKeys(data);
51925
+ if (xExtDiagnostics.length > 0) {
51926
+ return { success: false, diagnostics: xExtDiagnostics };
51927
+ }
51861
51928
  return { success: true, data: result.data, diagnostics: [] };
51862
51929
  }
51863
51930
 
@@ -52221,6 +52288,7 @@ var validationCoverageRule = {
52221
52288
  }
52222
52289
  validationsByArtifact.get(val.target_artifact).add(val.kind);
52223
52290
  }
52291
+ const strictArtifactTypes = ["code", "config", "schema"];
52224
52292
  for (const [artId, art] of Object.entries(dsl.artifacts)) {
52225
52293
  const kinds = validationsByArtifact.get(artId);
52226
52294
  if (!kinds || kinds.size === 0) {
@@ -52232,8 +52300,7 @@ var validationCoverageRule = {
52232
52300
  });
52233
52301
  continue;
52234
52302
  }
52235
- const mechanicalTypes = ["code", "config", "schema"];
52236
- if (mechanicalTypes.includes(art.type) && !kinds.has("mechanical") && !kinds.has("schema")) {
52303
+ if (strictArtifactTypes.includes(art.type) && !kinds.has("mechanical") && !kinds.has("schema")) {
52237
52304
  diagnostics.push({
52238
52305
  ruleId: "validation-coverage",
52239
52306
  severity: "warning",
@@ -52242,6 +52309,16 @@ var validationCoverageRule = {
52242
52309
  });
52243
52310
  }
52244
52311
  }
52312
+ for (const [artId, art] of Object.entries(dsl.artifacts)) {
52313
+ if (art.required_validations.length === 0) {
52314
+ diagnostics.push({
52315
+ ruleId: "validation-coverage",
52316
+ severity: "warning",
52317
+ path: `artifacts.${artId}`,
52318
+ message: `Artifact "${artId}" has empty required_validations`
52319
+ });
52320
+ }
52321
+ }
52245
52322
  const referencedValidations = /* @__PURE__ */ new Set();
52246
52323
  for (const wf of Object.values(dsl.workflow)) {
52247
52324
  for (const step of wf.steps) {
@@ -52583,6 +52660,146 @@ var guardrailPolicyCoverageRule = {
52583
52660
  }
52584
52661
  };
52585
52662
 
52663
+ // src/linter/rules/yaml-reserved-key-safety.ts
52664
+ var YAML_11_RESERVED_KEYS = /* @__PURE__ */ new Set([
52665
+ "on",
52666
+ "off",
52667
+ "yes",
52668
+ "no",
52669
+ "true",
52670
+ "false",
52671
+ "y",
52672
+ "n"
52673
+ ]);
52674
+ var yamlReservedKeySafetyRule = {
52675
+ id: "yaml-reserved-key-safety",
52676
+ description: "Warns when YAML 1.1 reserved words are used as field values in positions where they may be misinterpreted by non-1.2 parsers",
52677
+ run(dsl) {
52678
+ const diagnostics = [];
52679
+ for (const [wfKey, wf] of Object.entries(dsl.workflow)) {
52680
+ for (let i3 = 0; i3 < wf.steps.length; i3++) {
52681
+ const step = wf.steps[i3];
52682
+ if (step.type !== "decision") continue;
52683
+ if (step.on !== void 0 && step.routing_key === void 0) {
52684
+ diagnostics.push({
52685
+ ruleId: "yaml-reserved-key-safety",
52686
+ severity: "warning",
52687
+ path: `workflow.${wfKey}.steps[${i3}].on`,
52688
+ message: 'Field name "on" is a YAML 1.1 reserved word and may be interpreted as boolean true by some parsers. Use "routing_key" instead.'
52689
+ });
52690
+ }
52691
+ const branchKeys = Object.keys(step.branches);
52692
+ for (const key of branchKeys) {
52693
+ if (YAML_11_RESERVED_KEYS.has(key.toLowerCase())) {
52694
+ diagnostics.push({
52695
+ ruleId: "yaml-reserved-key-safety",
52696
+ severity: "warning",
52697
+ path: `workflow.${wfKey}.steps[${i3}].branches.${key}`,
52698
+ message: `Branch key "${key}" is a YAML 1.1 reserved word and may be interpreted as a boolean by some parsers. Consider quoting or renaming.`
52699
+ });
52700
+ }
52701
+ }
52702
+ }
52703
+ }
52704
+ return diagnostics;
52705
+ }
52706
+ };
52707
+
52708
+ // src/linter/rules/artifact-required-validation-wiring.ts
52709
+ var artifactRequiredValidationWiringRule = {
52710
+ id: "artifact-required-validation-wiring",
52711
+ description: "Every validation listed in artifact.required_validations must exist, target the artifact, and be referenced in a workflow step or task.",
52712
+ run(dsl) {
52713
+ const diagnostics = [];
52714
+ const referencedValidations = /* @__PURE__ */ new Set();
52715
+ for (const wf of Object.values(dsl.workflow)) {
52716
+ for (const step of wf.steps) {
52717
+ if (step.type === "validation") {
52718
+ referencedValidations.add(step.validation);
52719
+ }
52720
+ }
52721
+ }
52722
+ for (const task of Object.values(dsl.tasks)) {
52723
+ for (const valId of task.validations) {
52724
+ referencedValidations.add(valId);
52725
+ }
52726
+ }
52727
+ for (const [artId, art] of Object.entries(dsl.artifacts)) {
52728
+ for (const reqValId of art.required_validations) {
52729
+ const val = dsl.validations[reqValId];
52730
+ if (!val) {
52731
+ diagnostics.push({
52732
+ ruleId: "artifact-required-validation-wiring",
52733
+ severity: "error",
52734
+ path: `artifacts.${artId}`,
52735
+ message: `required_validation "${reqValId}" does not exist in validations`
52736
+ });
52737
+ continue;
52738
+ }
52739
+ if (val.target_artifact !== artId) {
52740
+ diagnostics.push({
52741
+ ruleId: "artifact-required-validation-wiring",
52742
+ severity: "error",
52743
+ path: `artifacts.${artId}`,
52744
+ message: `required_validation "${reqValId}" has target_artifact "${val.target_artifact}" instead of "${artId}"`
52745
+ });
52746
+ }
52747
+ if (!referencedValidations.has(reqValId)) {
52748
+ diagnostics.push({
52749
+ ruleId: "artifact-required-validation-wiring",
52750
+ severity: "warning",
52751
+ path: `artifacts.${artId}`,
52752
+ message: `required_validation "${reqValId}" is defined but not referenced in any workflow step or task`
52753
+ });
52754
+ }
52755
+ }
52756
+ }
52757
+ return diagnostics;
52758
+ }
52759
+ };
52760
+
52761
+ // src/linter/rules/task-output-validation-completeness.ts
52762
+ var taskOutputValidationCompletenessRule = {
52763
+ id: "task-output-validation-completeness",
52764
+ description: "Tasks producing artifacts should cover those artifacts' required_validations in their validations list.",
52765
+ run(dsl) {
52766
+ const diagnostics = [];
52767
+ for (const [taskId, task] of Object.entries(dsl.tasks)) {
52768
+ const producedArtifacts = /* @__PURE__ */ new Set();
52769
+ if (task.execution_steps) {
52770
+ for (const step of task.execution_steps) {
52771
+ if (step.produces_artifact) {
52772
+ producedArtifacts.add(step.produces_artifact);
52773
+ }
52774
+ }
52775
+ }
52776
+ const agent = dsl.agents[task.target_agent];
52777
+ if (agent) {
52778
+ for (const artId of agent.can_write_artifacts) {
52779
+ producedArtifacts.add(artId);
52780
+ }
52781
+ }
52782
+ const taskValidations = new Set(task.validations);
52783
+ for (const artId of producedArtifacts) {
52784
+ const art = dsl.artifacts[artId];
52785
+ if (!art || art.required_validations.length === 0) continue;
52786
+ const missing = art.required_validations.filter(
52787
+ (v2) => !taskValidations.has(v2)
52788
+ );
52789
+ if (missing.length > 0) {
52790
+ diagnostics.push({
52791
+ ruleId: "task-output-validation-completeness",
52792
+ severity: "warning",
52793
+ path: `tasks.${taskId}`,
52794
+ message: `Task produces artifact "${artId}" which requires validations [${missing.join(", ")}] but task.validations does not include them`
52795
+ });
52796
+ }
52797
+ }
52798
+ }
52799
+ return diagnostics;
52800
+ }
52801
+ };
52802
+
52586
52803
  // src/linter/linter.ts
52587
52804
  var builtinRules = [
52588
52805
  validationCoverageRule,
@@ -52591,7 +52808,10 @@ var builtinRules = [
52591
52808
  mergeIntegrityRule,
52592
52809
  artifactOwnershipRule,
52593
52810
  toolCommandsRule,
52594
- guardrailPolicyCoverageRule
52811
+ guardrailPolicyCoverageRule,
52812
+ yamlReservedKeySafetyRule,
52813
+ artifactRequiredValidationWiringRule,
52814
+ taskOutputValidationCompletenessRule
52595
52815
  ];
52596
52816
  function lint(dsl, rules = builtinRules) {
52597
52817
  const diagnostics = [];