agent-contracts 0.11.2 → 0.11.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/README.md CHANGED
@@ -297,7 +297,7 @@ Design regressions become testable.
297
297
  * **Structured handoff definitions** with formal JSON Schema and `allOf` composition
298
298
  * **Reusable schema components** via `components.schemas` and JSON Pointer `$ref`
299
299
  * **Artifact ownership and lifecycle modeling**
300
- * **Config-driven prompt rendering**
300
+ * **Config-driven prompt rendering** with `skip_empty` support for conditional file generation
301
301
  * **Variable substitution** via `${vars.xxx}` in DSL values
302
302
  * **Inheritance with merge operators via `extends`**
303
303
  * **Guardrail definitions** for cross-cutting process constraints
@@ -865,6 +865,44 @@ This lets you generate static outputs for:
865
865
 
866
866
  all from the same resolved DSL.
867
867
 
868
+ ### Render target options
869
+
870
+ Each entry in `renders` supports these fields:
871
+
872
+ | Field | Type | Required | Description |
873
+ |-------|------|----------|-------------|
874
+ | `template` | string | yes | Path to Handlebars template |
875
+ | `context` | string | yes | Context type (see below) |
876
+ | `output` | string | yes | Output file path (supports `{<context>.id}` placeholder) |
877
+ | `include` | string[] | no | Only render these entity IDs (not with `system`) |
878
+ | `exclude` | string[] | no | Skip these entity IDs (not with `system`) |
879
+ | `skip_empty` | boolean | no | When `true`, if the rendered output is empty or whitespace-only, the file is **not written**. If the file already exists, it is **deleted**. |
880
+
881
+ #### `skip_empty` usage
882
+
883
+ `skip_empty` is useful when a single template applies to all entities of a context type, but only some entities produce meaningful output.
884
+
885
+ For example, when using `context: tool` to generate per-tool scripts, tools without an `x-script` property would produce empty files. With `skip_empty: true`, those files are simply not created:
886
+
887
+ ````yaml
888
+ renders:
889
+ - template: ./templates/tool-script.sh.hbs
890
+ context: tool
891
+ output: ./output/scripts/{tool.id}.sh
892
+ skip_empty: true
893
+ ````
894
+
895
+ ````handlebars
896
+ {{!-- tool-script.sh.hbs --}}
897
+ {{#if tool.x-script}}
898
+ {{{tool.x-script}}}
899
+ {{/if}}
900
+ ````
901
+
902
+ Tools with `x-script` get a generated script file; tools without it produce no file at all.
903
+
904
+ `skip_empty` also works with `render --check` (drift detection): when the expected output is empty, the check expects the file to **not exist** and reports drift if it does.
905
+
868
906
  ### Available context types
869
907
 
870
908
  Each `context` type provides a different rendering scope:
package/dist/cli.js CHANGED
@@ -50526,7 +50526,8 @@ var BindingOutputSchema = z3.object({
50526
50526
  inline_template: z3.string().optional(),
50527
50527
  mode: z3.enum(["write", "patch"]).default("write"),
50528
50528
  group_by: z3.string().optional(),
50529
- executable: z3.boolean().optional()
50529
+ executable: z3.boolean().optional(),
50530
+ skip_empty: z3.boolean().optional()
50530
50531
  }).passthrough().refine(
50531
50532
  (data) => !(data.template && data.inline_template),
50532
50533
  { message: "template and inline_template are mutually exclusive" }
@@ -50860,7 +50861,8 @@ var RenderTargetSchema = z14.object({
50860
50861
  context: ContextTypeSchema,
50861
50862
  output: z14.string(),
50862
50863
  include: z14.array(z14.string()).optional(),
50863
- exclude: z14.array(z14.string()).optional()
50864
+ exclude: z14.array(z14.string()).optional(),
50865
+ skip_empty: z14.boolean().optional()
50864
50866
  }).refine(
50865
50867
  (data) => !(data.include && data.exclude),
50866
50868
  { message: "include and exclude are mutually exclusive" }
@@ -53281,7 +53283,7 @@ var lintCommand = new Command3("lint").description("Run semantic lint rules on r
53281
53283
  import { Command as Command4 } from "commander";
53282
53284
 
53283
53285
  // src/renderer/renderer.ts
53284
- import { readFile as readFile4, writeFile, mkdir } from "fs/promises";
53286
+ import { readFile as readFile4, writeFile, mkdir, unlink } from "fs/promises";
53285
53287
  import { dirname as dirname5 } from "path";
53286
53288
  import Handlebars from "handlebars";
53287
53289
 
@@ -54397,6 +54399,15 @@ function buildEntityContext(dsl, context, entityId) {
54397
54399
  async function loadTemplate(templatePath) {
54398
54400
  return readFile4(templatePath, "utf8");
54399
54401
  }
54402
+ function isEffectivelyEmpty(output) {
54403
+ return output.trim().length === 0;
54404
+ }
54405
+ async function removeIfExists(filePath) {
54406
+ try {
54407
+ await unlink(filePath);
54408
+ } catch {
54409
+ }
54410
+ }
54400
54411
  async function renderFromConfig(dsl, renderTargets) {
54401
54412
  const outputFiles = [];
54402
54413
  for (const target of renderTargets) {
@@ -54405,6 +54416,10 @@ async function renderFromConfig(dsl, renderTargets) {
54405
54416
  if (target.context === "system") {
54406
54417
  const ctx = buildSystemContext(dsl);
54407
54418
  const output = compiled(ctx);
54419
+ if (target.skip_empty && isEffectivelyEmpty(output)) {
54420
+ await removeIfExists(target.output);
54421
+ continue;
54422
+ }
54408
54423
  await mkdir(dirname5(target.output), { recursive: true });
54409
54424
  await writeFile(target.output, output, "utf8");
54410
54425
  outputFiles.push(target.output);
@@ -54416,6 +54431,10 @@ async function renderFromConfig(dsl, renderTargets) {
54416
54431
  const ctx = buildEntityContext(dsl, target.context, entityId);
54417
54432
  const output = compiled(ctx);
54418
54433
  const outputPath = expandOutputPath(target.output, target.context, entityId);
54434
+ if (target.skip_empty && isEffectivelyEmpty(output)) {
54435
+ await removeIfExists(outputPath);
54436
+ continue;
54437
+ }
54419
54438
  await mkdir(dirname5(outputPath), { recursive: true });
54420
54439
  await writeFile(outputPath, output, "utf8");
54421
54440
  outputFiles.push(outputPath);
@@ -54424,6 +54443,26 @@ async function renderFromConfig(dsl, renderTargets) {
54424
54443
  }
54425
54444
  return outputFiles;
54426
54445
  }
54446
+ async function fileExists2(filePath) {
54447
+ try {
54448
+ await readFile4(filePath);
54449
+ return true;
54450
+ } catch {
54451
+ return false;
54452
+ }
54453
+ }
54454
+ function checkExpectedVsExisting(expected, outputPath, skipEmpty, diffs) {
54455
+ if (skipEmpty && isEffectivelyEmpty(expected)) {
54456
+ return fileExists2(outputPath).then((exists) => {
54457
+ if (exists) diffs.push(outputPath);
54458
+ });
54459
+ }
54460
+ return readFile4(outputPath, "utf8").then((existing) => {
54461
+ if (existing !== expected) diffs.push(outputPath);
54462
+ }).catch(() => {
54463
+ diffs.push(outputPath);
54464
+ });
54465
+ }
54427
54466
  async function checkDriftFromConfig(dsl, renderTargets) {
54428
54467
  const diffs = [];
54429
54468
  for (const target of renderTargets) {
@@ -54432,12 +54471,7 @@ async function checkDriftFromConfig(dsl, renderTargets) {
54432
54471
  if (target.context === "system") {
54433
54472
  const ctx = buildSystemContext(dsl);
54434
54473
  const expected = compiled(ctx);
54435
- try {
54436
- const existing = await readFile4(target.output, "utf8");
54437
- if (existing !== expected) diffs.push(target.output);
54438
- } catch {
54439
- diffs.push(target.output);
54440
- }
54474
+ await checkExpectedVsExisting(expected, target.output, target.skip_empty, diffs);
54441
54475
  } else {
54442
54476
  const section = getDslSection(dsl, target.context);
54443
54477
  const allIds = Object.keys(section);
@@ -54446,12 +54480,7 @@ async function checkDriftFromConfig(dsl, renderTargets) {
54446
54480
  const ctx = buildEntityContext(dsl, target.context, entityId);
54447
54481
  const expected = compiled(ctx);
54448
54482
  const outputPath = expandOutputPath(target.output, target.context, entityId);
54449
- try {
54450
- const existing = await readFile4(outputPath, "utf8");
54451
- if (existing !== expected) diffs.push(outputPath);
54452
- } catch {
54453
- diffs.push(outputPath);
54454
- }
54483
+ await checkExpectedVsExisting(expected, outputPath, target.skip_empty, diffs);
54455
54484
  }
54456
54485
  }
54457
54486
  }
@@ -54609,7 +54638,7 @@ var checkCommand = new Command5("check").description("Run full pipeline: resolve
54609
54638
  import { Command as Command6 } from "commander";
54610
54639
 
54611
54640
  // src/guardrail-generator/generator.ts
54612
- import { readFile as readFile5, writeFile as writeFile2, mkdir as mkdir2, chmod } from "fs/promises";
54641
+ import { readFile as readFile5, writeFile as writeFile2, mkdir as mkdir2, chmod, unlink as unlink2 } from "fs/promises";
54613
54642
  import { resolve as resolve6, dirname as dirname6 } from "path";
54614
54643
  import Handlebars2 from "handlebars";
54615
54644
 
@@ -54783,6 +54812,7 @@ async function generateGuardrails(options) {
54783
54812
  });
54784
54813
  continue;
54785
54814
  }
54815
+ const shouldSkipEmpty = outputDef.skip_empty === true;
54786
54816
  if (outputDef.group_by) {
54787
54817
  const groupField = outputDef.group_by;
54788
54818
  const groups = /* @__PURE__ */ new Map();
@@ -54802,6 +54832,15 @@ async function generateGuardrails(options) {
54802
54832
  const compiled = Handlebars2.compile(templateContent, { noEscape: true });
54803
54833
  const output = compiled(groupCtx);
54804
54834
  const groupTarget = resolve6(targetPath, groupKey);
54835
+ if (shouldSkipEmpty && output.trim().length === 0) {
54836
+ if (!dryRun) {
54837
+ try {
54838
+ await unlink2(groupTarget);
54839
+ } catch {
54840
+ }
54841
+ }
54842
+ continue;
54843
+ }
54805
54844
  if (!dryRun) {
54806
54845
  await mkdir2(dirname6(groupTarget), { recursive: true });
54807
54846
  await writeFile2(groupTarget, output, "utf8");
@@ -54814,14 +54853,23 @@ async function generateGuardrails(options) {
54814
54853
  } else {
54815
54854
  const compiled = Handlebars2.compile(templateContent, { noEscape: true });
54816
54855
  const output = compiled(ctx);
54817
- if (!dryRun) {
54818
- await mkdir2(dirname6(targetPath), { recursive: true });
54819
- await writeFile2(targetPath, output, "utf8");
54820
- if (outputDef.executable) {
54821
- await chmod(targetPath, 493);
54856
+ if (shouldSkipEmpty && output.trim().length === 0) {
54857
+ if (!dryRun) {
54858
+ try {
54859
+ await unlink2(targetPath);
54860
+ } catch {
54861
+ }
54862
+ }
54863
+ } else {
54864
+ if (!dryRun) {
54865
+ await mkdir2(dirname6(targetPath), { recursive: true });
54866
+ await writeFile2(targetPath, output, "utf8");
54867
+ if (outputDef.executable) {
54868
+ await chmod(targetPath, 493);
54869
+ }
54822
54870
  }
54871
+ outputFiles.push(targetPath);
54823
54872
  }
54824
- outputFiles.push(targetPath);
54825
54873
  }
54826
54874
  }
54827
54875
  }