@prorigo/protrak-forge 0.4.0 → 0.4.2

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.
@@ -15672,6 +15672,14 @@ var LocalSchemaProvider = class {
15672
15672
  invalidateRules() {
15673
15673
  this._rules = null;
15674
15674
  }
15675
+ /** Force a re-read of types on next access. */
15676
+ invalidateTypes() {
15677
+ this._types = null;
15678
+ }
15679
+ /** Force a re-read of attributes on next access. */
15680
+ invalidateAttributes() {
15681
+ this._attributes = null;
15682
+ }
15675
15683
  // ── Public API ────────────────────────────────────────────────────────────
15676
15684
  listTypes() {
15677
15685
  return Object.entries(this.types).map(([name, typeData]) => ({
@@ -17948,7 +17956,7 @@ function getClient() {
17948
17956
  }
17949
17957
 
17950
17958
  // src/version.ts
17951
- var PACKAGE_VERSION = "0.4.0";
17959
+ var PACKAGE_VERSION = "0.4.2";
17952
17960
 
17953
17961
  // src/tools/_schema-write-utils.ts
17954
17962
  var fs4 = __toESM(require("fs"));
@@ -22486,9 +22494,6 @@ function serializeStyle(style) {
22486
22494
  if (!FONT_STYLE_OPTIONS.includes(v)) {
22487
22495
  return { error: `font_style entry '${v}' must be one of: ${FONT_STYLE_OPTIONS.join(", ")}.` };
22488
22496
  }
22489
- if (seen.has(v)) {
22490
- return { error: `font_style contains duplicate entry '${v}'.` };
22491
- }
22492
22497
  seen.add(v);
22493
22498
  }
22494
22499
  if (seen.has("Bold")) tokens.push("fontWeight:bold");
@@ -22599,6 +22604,50 @@ var EXAMPLES = [
22599
22604
  }
22600
22605
  ]
22601
22606
  }
22607
+ },
22608
+ {
22609
+ intent: "Current user's roles include 'Manager'. UserRoles context: attributeName is omitted entirely; Contains takes only firstValue.",
22610
+ rule: {
22611
+ name: "CurrentUserIsManager",
22612
+ attributeFilterGroup: [
22613
+ {
22614
+ operator: "None",
22615
+ attributeFilterConditions: [
22616
+ {
22617
+ attributeContext: "UserRoles",
22618
+ condition: "Contains",
22619
+ firstValue: { valueType: "Static", value: "Manager" },
22620
+ operator: "None"
22621
+ }
22622
+ ]
22623
+ }
22624
+ ]
22625
+ }
22626
+ },
22627
+ {
22628
+ intent: "Visible when current user is the Creator OR has the 'Manager' role. Mixes UserRoles + Instance contexts in a single group joined by Or.",
22629
+ rule: {
22630
+ name: "VisibleToManagerOrCreator",
22631
+ attributeFilterGroup: [
22632
+ {
22633
+ operator: "None",
22634
+ attributeFilterConditions: [
22635
+ {
22636
+ attributeContext: "UserRoles",
22637
+ condition: "Contains",
22638
+ firstValue: { valueType: "Static", value: "Manager" },
22639
+ operator: "Or"
22640
+ },
22641
+ {
22642
+ attributeName: "Creator",
22643
+ attributeContext: "Instance",
22644
+ condition: "ContextUser",
22645
+ operator: "None"
22646
+ }
22647
+ ]
22648
+ }
22649
+ ]
22650
+ }
22602
22651
  }
22603
22652
  ];
22604
22653
  var DYNAMIC_VALUE_RULES = {
@@ -22712,7 +22761,7 @@ var CONDITION_INPUT_SCHEMA = {
22712
22761
  },
22713
22762
  attribute_name: {
22714
22763
  type: "string",
22715
- description: "PascalCase attribute name. Required for Instance and User contexts. Omit for UserRoles context (it evaluates the current user's roles)."
22764
+ description: "PascalCase attribute name. Required for Instance and User contexts. OMIT this key entirely for UserRoles context (it evaluates the current user's roles). Do NOT send empty string '' \u2014 leave the key out."
22716
22765
  },
22717
22766
  condition: {
22718
22767
  type: "string",
@@ -22720,11 +22769,11 @@ var CONDITION_INPUT_SCHEMA = {
22720
22769
  },
22721
22770
  first_value: {
22722
22771
  ...VALUE_INPUT_SCHEMA,
22723
- description: "First value. Required when the condition takes a value; omit for IsEmpty/Today/ContextUser/etc."
22772
+ description: "First value of the condition. Required for value-bearing conditions (Equals, Contains, GreaterThan, Between, In, etc.). OMIT this key entirely for valueless conditions: IsEmpty, IsNotEmpty, Today, LastOneWeek, LastOneMonth, LastOneYear, NextWeek, NextOneMonth, NextOneYear, ContextUser. Do NOT send `null` or `{value: ''}` \u2014 leave the key out of the JSON object."
22724
22773
  },
22725
22774
  second_value: {
22726
22775
  ...VALUE_INPUT_SCHEMA,
22727
- description: "Second value. Required only for 'Between' conditions."
22776
+ description: "Second value. Only valid for 'Between'. OMIT this key entirely for every other condition (including Contains, Equals, In, IsEmpty, ContextUser, etc.). Do NOT send `null` or `{value: ''}` \u2014 leave the key out of the JSON object."
22728
22777
  },
22729
22778
  operator: {
22730
22779
  type: "string",
@@ -22753,7 +22802,7 @@ var GROUP_INPUT_SCHEMA = {
22753
22802
  };
22754
22803
  var TOOL_DEF29 = {
22755
22804
  name: "generate_protrak_rule",
22756
- description: "Generate and write a Rule JSON file to <workspace>/Rules/<name>.json. Rules express attribute-filter logic used by display conditions, conditional formatting, dependent picklists, and Access Policy. Inputs are strictly structured: the agent should translate natural-language intent into attribute_filter_groups using the schema returned by get_protrak_rule_context. Validates: PascalCase name; Instance-context attribute_name exists in the workspace (or is a recognised basic attribute); the condition is valid for the attribute's type; value presence matches the condition (e.g. Between requires both, IsEmpty rejects values); dynamic values match @Instance.X, @User.X, or $StartOfToday()/$StartOfMonth(\xB1Nd) patterns; @Instance dynamic references resolve to known attributes. Pass error_message only when the rule will be used as an Access Policy. Refuses to overwrite an existing file unless overwrite:true.",
22805
+ description: "Generate and write a Rule JSON file to <workspace>/Rules/<name>.json. Rules express attribute-filter logic used by display conditions, conditional formatting, dependent picklists, and Access Policy. Inputs are strictly structured: the agent should translate natural-language intent into attribute_filter_groups using the schema returned by get_protrak_rule_context. Validates: PascalCase name; Instance-context attribute_name exists in the workspace (or is a recognised basic attribute); the condition is valid for the attribute's type; value presence matches the condition (e.g. Between requires both, IsEmpty rejects values); dynamic values match @Instance.X, @User.X, or $StartOfToday()/$StartOfMonth(\xB1N[d|w|m|y]) patterns; @Instance dynamic references resolve to known attributes. Pass error_message only when the rule will be used as an Access Policy. Refuses to overwrite an existing file unless overwrite:true.",
22757
22806
  inputSchema: {
22758
22807
  type: "object",
22759
22808
  properties: {
@@ -22780,6 +22829,49 @@ var TOOL_DEF29 = {
22780
22829
  required: ["name", "attribute_filter_groups"]
22781
22830
  }
22782
22831
  };
22832
+ function normalizeValue(v) {
22833
+ if (v === void 0 || v === null) return void 0;
22834
+ if (typeof v !== "object") return v;
22835
+ const obj = v;
22836
+ if (typeof obj["value"] === "string" && obj["value"].length === 0) return void 0;
22837
+ return obj;
22838
+ }
22839
+ function normalizeCondition(rc) {
22840
+ const attrName = rc["attribute_name"];
22841
+ return {
22842
+ attribute_context: rc["attribute_context"],
22843
+ attribute_name: typeof attrName === "string" && attrName.length > 0 ? attrName : void 0,
22844
+ condition: rc["condition"],
22845
+ first_value: normalizeValue(rc["first_value"]),
22846
+ second_value: normalizeValue(rc["second_value"]),
22847
+ operator: rc["operator"]
22848
+ };
22849
+ }
22850
+ function expectedConditionShape(cond) {
22851
+ const shape = {
22852
+ attribute_context: cond.attribute_context
22853
+ };
22854
+ if (cond.attribute_context !== "UserRoles") {
22855
+ shape["attribute_name"] = cond.attribute_name ?? "<AttributeName>";
22856
+ }
22857
+ shape["condition"] = cond.condition;
22858
+ if (RANGE_CONDITIONS.has(cond.condition)) {
22859
+ shape["first_value"] = { value_type: "Static", value: "<from>" };
22860
+ shape["second_value"] = { value_type: "Static", value: "<to>" };
22861
+ } else if (!VALUE_LESS_CONDITIONS.has(cond.condition)) {
22862
+ shape["first_value"] = { value_type: "Static", value: "<value>" };
22863
+ }
22864
+ shape["operator"] = cond.operator;
22865
+ return shape;
22866
+ }
22867
+ function conditionShapeHints(cond) {
22868
+ return {
22869
+ expected_shape: expectedConditionShape(cond),
22870
+ valueless_conditions: [...VALUE_LESS_CONDITIONS].sort(),
22871
+ range_conditions: [...RANGE_CONDITIONS].sort(),
22872
+ hint: 'Send the value-bearing keys ONLY when needed. To "omit" a key, leave it out of the JSON entirely \u2014 do not send null or { value: "" }.'
22873
+ };
22874
+ }
22783
22875
  function validateValueShape(value, label) {
22784
22876
  if (value === void 0) return null;
22785
22877
  if (typeof value !== "object" || value === null) {
@@ -22808,35 +22900,42 @@ function validateDynamicValue(provider, value, label) {
22808
22900
  }
22809
22901
  function validateCondition(provider, cond, prefix) {
22810
22902
  if (!ATTRIBUTE_CONTEXTS.includes(cond.attribute_context)) {
22811
- return `${prefix}.attribute_context must be one of: ${ATTRIBUTE_CONTEXTS.join(", ")}.`;
22903
+ return { error: `${prefix}.attribute_context must be one of: ${ATTRIBUTE_CONTEXTS.join(", ")}.` };
22812
22904
  }
22813
22905
  if (!GROUP_OPERATORS.includes(cond.operator)) {
22814
- return `${prefix}.operator must be one of: ${GROUP_OPERATORS.join(", ")}.`;
22906
+ return { error: `${prefix}.operator must be one of: ${GROUP_OPERATORS.join(", ")}.` };
22815
22907
  }
22816
22908
  if (typeof cond.condition !== "string" || !cond.condition) {
22817
- return `${prefix}.condition is required.`;
22909
+ return { error: `${prefix}.condition is required.` };
22818
22910
  }
22819
- let validConditions;
22820
22911
  if (cond.attribute_context === "UserRoles") {
22821
22912
  if (cond.attribute_name) {
22822
- return `${prefix}.attribute_name must be omitted when attribute_context is 'UserRoles'.`;
22913
+ return { error: `${prefix}.attribute_name must be omitted when attribute_context is 'UserRoles'.` };
22914
+ }
22915
+ if (!USER_ROLES_CONDITIONS.includes(cond.condition)) {
22916
+ return {
22917
+ error: `${prefix}.condition '${cond.condition}' is not valid for UserRoles context. Valid: ${USER_ROLES_CONDITIONS.join(", ")}.`
22918
+ };
22823
22919
  }
22824
- validConditions = USER_ROLES_CONDITIONS;
22825
22920
  } else {
22826
22921
  if (!cond.attribute_name) {
22827
- return `${prefix}.attribute_name is required for attribute_context='${cond.attribute_context}'.`;
22922
+ return { error: `${prefix}.attribute_name is required for attribute_context='${cond.attribute_context}'.` };
22828
22923
  }
22829
22924
  if (!isPascalCase(cond.attribute_name)) {
22830
- return `${prefix}.${pascalCaseError("attribute_name", cond.attribute_name)}`;
22925
+ return { error: `${prefix}.${pascalCaseError("attribute_name", cond.attribute_name)}` };
22831
22926
  }
22832
22927
  if (cond.attribute_context === "Instance") {
22833
22928
  const attrType = resolveInstanceAttributeType(provider, cond.attribute_name);
22834
22929
  if (!attrType) {
22835
- return `${prefix} references Instance attribute '${cond.attribute_name}' which does not exist in the workspace. Add the attribute (or use a basic attribute name like Name/State/Created/Creator) before generating this rule.`;
22930
+ return {
22931
+ error: `${prefix} references Instance attribute '${cond.attribute_name}' which does not exist in the workspace. Add the attribute (or use a basic attribute name like Name/State/Created/Creator) before generating this rule.`
22932
+ };
22836
22933
  }
22837
- validConditions = CONDITIONS_BY_ATTR_TYPE[attrType] ?? CONDITIONS_BY_ATTR_TYPE["Text"];
22934
+ const validConditions = CONDITIONS_BY_ATTR_TYPE[attrType] ?? CONDITIONS_BY_ATTR_TYPE["Text"];
22838
22935
  if (!validConditions.includes(cond.condition)) {
22839
- return `${prefix}.condition '${cond.condition}' is not valid for Instance attribute '${cond.attribute_name}' (type=${attrType}). Valid conditions: ${validConditions.join(", ")}.`;
22936
+ return {
22937
+ error: `${prefix}.condition '${cond.condition}' is not valid for Instance attribute '${cond.attribute_name}' (type=${attrType}). Valid conditions: ${validConditions.join(", ")}.`
22938
+ };
22840
22939
  }
22841
22940
  } else {
22842
22941
  const acceptable = /* @__PURE__ */ new Set([
@@ -22847,32 +22946,42 @@ function validateCondition(provider, cond, prefix) {
22847
22946
  ...CONDITIONS_BY_ATTR_TYPE["Boolean"]
22848
22947
  ]);
22849
22948
  if (!acceptable.has(cond.condition)) {
22850
- return `${prefix}.condition '${cond.condition}' is not recognised for User-context conditions.`;
22949
+ return {
22950
+ error: `${prefix}.condition '${cond.condition}' is not recognised for User-context conditions.`
22951
+ };
22851
22952
  }
22852
- validConditions = Array.from(acceptable);
22853
22953
  }
22854
22954
  }
22855
- if (cond.attribute_context === "UserRoles" && !USER_ROLES_CONDITIONS.includes(cond.condition)) {
22856
- return `${prefix}.condition '${cond.condition}' is not valid for UserRoles context. Valid: ${USER_ROLES_CONDITIONS.join(", ")}.`;
22857
- }
22858
22955
  const valueless = VALUE_LESS_CONDITIONS.has(cond.condition);
22859
22956
  const ranged = RANGE_CONDITIONS.has(cond.condition);
22860
22957
  const hasFirst = cond.first_value !== void 0;
22861
22958
  const hasSecond = cond.second_value !== void 0;
22862
22959
  if (valueless) {
22863
22960
  if (hasFirst || hasSecond) {
22864
- return `${prefix}.condition '${cond.condition}' takes no value(s); remove first_value/second_value.`;
22961
+ return {
22962
+ error: `${prefix}.condition '${cond.condition}' takes no value(s); omit first_value and second_value entirely.`,
22963
+ extras: conditionShapeHints(cond)
22964
+ };
22865
22965
  }
22866
22966
  } else if (ranged) {
22867
22967
  if (!hasFirst || !hasSecond) {
22868
- return `${prefix}.condition '${cond.condition}' requires BOTH first_value and second_value.`;
22968
+ return {
22969
+ error: `${prefix}.condition '${cond.condition}' requires BOTH first_value and second_value.`,
22970
+ extras: conditionShapeHints(cond)
22971
+ };
22869
22972
  }
22870
22973
  } else {
22871
22974
  if (!hasFirst) {
22872
- return `${prefix}.condition '${cond.condition}' requires first_value.`;
22975
+ return {
22976
+ error: `${prefix}.condition '${cond.condition}' requires first_value.`,
22977
+ extras: conditionShapeHints(cond)
22978
+ };
22873
22979
  }
22874
22980
  if (hasSecond) {
22875
- return `${prefix}.condition '${cond.condition}' takes only first_value; remove second_value.`;
22981
+ return {
22982
+ error: `${prefix}.condition '${cond.condition}' takes only first_value; omit second_value entirely (do not send null or empty value).`,
22983
+ extras: conditionShapeHints(cond)
22984
+ };
22876
22985
  }
22877
22986
  }
22878
22987
  for (const [v, lbl] of [
@@ -22880,10 +22989,10 @@ function validateCondition(provider, cond, prefix) {
22880
22989
  [cond.second_value, `${prefix}.second_value`]
22881
22990
  ]) {
22882
22991
  const shapeErr = validateValueShape(v, lbl);
22883
- if (shapeErr) return shapeErr;
22992
+ if (shapeErr) return { error: shapeErr };
22884
22993
  if (v) {
22885
22994
  const dynErr = validateDynamicValue(provider, v, lbl);
22886
- if (dynErr) return dynErr;
22995
+ if (dynErr) return { error: dynErr };
22887
22996
  }
22888
22997
  }
22889
22998
  return null;
@@ -22942,16 +23051,9 @@ function handle29(provider, args) {
22942
23051
  if (typeof rc !== "object" || rc === null) {
22943
23052
  return errorResponse(`attribute_filter_groups[${gi}].conditions[${ci}] must be an object.`);
22944
23053
  }
22945
- const cond = {
22946
- attribute_context: rc["attribute_context"],
22947
- attribute_name: rc["attribute_name"],
22948
- condition: rc["condition"],
22949
- first_value: rc["first_value"],
22950
- second_value: rc["second_value"],
22951
- operator: rc["operator"]
22952
- };
23054
+ const cond = normalizeCondition(rc);
22953
23055
  const err = validateCondition(provider, cond, `attribute_filter_groups[${gi}].conditions[${ci}]`);
22954
- if (err) return errorResponse(err);
23056
+ if (err) return errorResponse(err.error, err.extras);
22955
23057
  conditions.push(cond);
22956
23058
  }
22957
23059
  groups.push({ operator: raw["operator"], conditions });
@@ -22995,7 +23097,7 @@ var DISPLAY_TARGET_KINDS = ["layout_template", "form"];
22995
23097
  var FORMAT_TARGET_KINDS = ["type_widget", "form"];
22996
23098
  var TOOL_DEF30 = {
22997
23099
  name: "attach_protrak_rule",
22998
- description: "Attach an existing Rule to a Protrak schema artifact. One polymorphic tool covering five consumer shapes selected by `usage`: 'display_condition' (LayoutTemplate widget displayConditions[] or Form field displayCondition); 'format_condition' (ViewLayout field formatConditions); 'conditional_formatting' (TypeWidget column or Form field conditionalFormatting); 'access_policy' (Type.accessPolicyRule); 'dependent_picklist' (Picklist option.rule). Rule must already exist in Rules/. Target file/widget/attribute/option must exist; the tool refuses to silently create them. Style is structured ({ background_color?, fore_color?, font_style? }) and serialized to the comma-terminated CSS-string the SPA evaluator expects.",
23100
+ description: "Attach an existing Rule to a Protrak schema artifact. One polymorphic tool covering five consumer shapes selected by `usage`: 'display_condition' (LayoutTemplate widget displayConditions[], Form container displayConditions[], or Form field displayCondition); 'format_condition' (ViewLayout field formatConditions); 'conditional_formatting' (TypeWidget column or Form field conditionalFormatting); 'access_policy' (Type.accessPolicyRule); 'dependent_picklist' (Picklist option.rule). Rule must already exist in Rules/. Target file/widget/attribute/option must exist; the tool refuses to silently create them. Style is structured ({ background_color?, fore_color?, font_style? }) and serialized to the comma-terminated CSS-string the SPA evaluator expects.",
22999
23101
  inputSchema: {
23000
23102
  type: "object",
23001
23103
  properties: {
@@ -23028,7 +23130,7 @@ var TOOL_DEF30 = {
23028
23130
  },
23029
23131
  container_id: {
23030
23132
  type: "string",
23031
- description: "Form target only: formContainerKey to disambiguate when multiple containers exist."
23133
+ description: "Form target only: formContainerKey. For usage='display_condition': when provided WITHOUT attribute_name, attaches the rule to the container itself (plural displayConditions[]). When attribute_name is also provided, disambiguates which container to search for that field. For usage='conditional_formatting': disambiguates which container holds the field. When omitted on a field lookup and the same attribute exists in multiple containers, the first match wins \u2014 pass container_id to disambiguate."
23032
23134
  },
23033
23135
  section_name: {
23034
23136
  type: "string",
@@ -23064,6 +23166,8 @@ var TOOL_DEF30 = {
23064
23166
  }
23065
23167
  };
23066
23168
  function ruleExists(provider, ruleName) {
23169
+ if (provider.rules[ruleName]) return true;
23170
+ provider.invalidateRules();
23067
23171
  return !!provider.rules[ruleName];
23068
23172
  }
23069
23173
  function findFormField(formData, attrName, containerId) {
@@ -23084,7 +23188,6 @@ function findFormField(formData, attrName, containerId) {
23084
23188
  return {
23085
23189
  result: {
23086
23190
  field: fields[idx],
23087
- index: idx,
23088
23191
  parent: fields,
23089
23192
  containerKey: container["formContainerKey"] ?? ""
23090
23193
  }
@@ -23158,16 +23261,50 @@ function attachDisplayCondition(provider, args) {
23158
23261
  );
23159
23262
  }
23160
23263
  const attrName = args["attribute_name"];
23161
- if (!attrName) {
23162
- return errorResponse("attribute_name is required for usage='display_condition' on a form.");
23264
+ const containerId = args["container_id"] ?? void 0;
23265
+ if (!attrName && !containerId) {
23266
+ return errorResponse(
23267
+ "Either attribute_name (to attach to a field) or container_id (to attach to a whole container) is required for usage='display_condition' on a form."
23268
+ );
23163
23269
  }
23164
- if (!isPascalCase(attrName)) return errorResponse(pascalCaseError("attribute_name", attrName));
23165
23270
  const filePath = path17.join(provider.ws.formsDir, `${targetName}.json`);
23166
23271
  if (!fs21.existsSync(filePath)) {
23167
23272
  return errorResponse(`Form '${targetName}' not found at ${filePath}.`);
23168
23273
  }
23169
23274
  const data = readJsonFile(filePath);
23170
- const containerId = args["container_id"] ?? void 0;
23275
+ if (!attrName) {
23276
+ const config2 = data["formConfiguration"] ?? {};
23277
+ const containers = Array.isArray(config2["containers"]) ? config2["containers"] : [];
23278
+ const container = containers.find((c) => c["formContainerKey"] === containerId);
23279
+ if (!container) {
23280
+ return errorResponse(`Container '${containerId}' not found in form '${targetName}'.`, {
23281
+ available_containers: containers.map((c) => c["formContainerKey"] ?? "")
23282
+ });
23283
+ }
23284
+ const existing2 = Array.isArray(container["displayConditions"]) ? container["displayConditions"] : [];
23285
+ if (existing2.includes(ruleName)) {
23286
+ if (!replace) {
23287
+ return errorResponse(
23288
+ `Rule '${ruleName}' is already in displayConditions for container '${containerId}'. Pass replace:true to no-op silently.`
23289
+ );
23290
+ }
23291
+ } else {
23292
+ existing2.push(ruleName);
23293
+ }
23294
+ container["displayConditions"] = existing2;
23295
+ writeJsonFilePath(filePath, data);
23296
+ return successResponse(
23297
+ {
23298
+ usage: "display_condition",
23299
+ target_kind: "form",
23300
+ target_path: filePath,
23301
+ container_id: containerId,
23302
+ display_conditions: existing2
23303
+ },
23304
+ { nextSteps: [`Verify by reading Forms/${targetName}.json.`] }
23305
+ );
23306
+ }
23307
+ if (!isPascalCase(attrName)) return errorResponse(pascalCaseError("attribute_name", attrName));
23171
23308
  const lookup2 = findFormField(data, attrName, containerId);
23172
23309
  if (lookup2.error) return errorResponse(lookup2.error, lookup2.available_containers ? { available_containers: lookup2.available_containers } : void 0);
23173
23310
  const field = lookup2.result.field;
@@ -23279,7 +23416,8 @@ function attachFormatCondition(provider, args) {
23279
23416
  usage: "format_condition",
23280
23417
  target_path: filePath,
23281
23418
  attribute_name: attrName,
23282
- format_conditions: fc
23419
+ entry,
23420
+ total_format_conditions: fc.length
23283
23421
  },
23284
23422
  { nextSteps: [`Verify by reading ViewLayouts/${targetName}.json.`] }
23285
23423
  );
@@ -23371,7 +23509,8 @@ function attachConditionalFormatting(provider, args) {
23371
23509
  target_kind: targetKind,
23372
23510
  target_path: filePath,
23373
23511
  attribute_name: attrName,
23374
- conditional_formatting: cf
23512
+ entry,
23513
+ total_conditional_formatting: cf.length
23375
23514
  },
23376
23515
  { nextSteps: [`Verify by reading ${path17.basename(filePath)}.`] }
23377
23516
  );
@@ -178,6 +178,9 @@ Call `attach_protrak_rule(usage, rule_name, ...)`. One polymorphic tool covers f
178
178
  — appends to the widget's `displayConditions: string[]`.
179
179
  - `usage='display_condition'`, `target_kind='form'`, `target_name`, `attribute_name`,
180
180
  `container_id?` — sets the **singular** `displayCondition` on the form field.
181
+ - `usage='display_condition'`, `target_kind='form'`, `target_name`, `container_id`
182
+ (no `attribute_name`) — appends to the container's **plural** `displayConditions: string[]`
183
+ (same shape as a LayoutTemplate widget).
181
184
  - `usage='format_condition'`, `target_name=<ViewLayout>`, `attribute_name`, `section_name?`,
182
185
  `widget_name?`, `style`, `order_index?` — appends `{ rule:{name}, orderIndex, style }`
183
186
  to the ViewLayout field's `formatConditions`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prorigo/protrak-forge",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Protrak domain context for coding agents — MCP server for GitHub Copilot, Claude, and Cursor",
5
5
  "bin": {
6
6
  "protrak-forge": "./bin/protrak-forge.js"