@prorigo/protrak-forge 0.3.3 → 0.3.4

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.
@@ -7,23 +7,80 @@ that implement Protrak interfaces (PreCreate, PostCreate, PreUpdate, PromoteActi
7
7
 
8
8
  ## How to write Protrak programs
9
9
 
10
- **ALWAYS use the Protrak Forge MCP tools when working with Protrak programs.**
10
+ Default to the Protrak Forge MCP tools, not the filesystem. The cost-of-skipping table at the end
11
+ of this section makes the trade-off concrete; the workflows below tell you which tools to call.
11
12
 
12
- ### Writing a new program:
13
- 1. Call `get_protrak_program_context` with the type name, program type, and description —
14
- it returns the type schema, interface contract, relevant patterns, and similar programs
15
- in ONE call.
16
- 2. Write the C# program following the interface and patterns returned.
17
- 3. Call `validate_protrak_program` to check for errors before committing.
13
+ ### Writing a NEW program:
14
+ 1. Call `get_protrak_program_context(type_name, program_type, description)` returns type
15
+ schema, C# interface contract, coding patterns, similar programs, and available query
16
+ definitions in ONE call.
17
+ 2. Write the C# source.
18
+ 3. Call `validate_protrak_program(code, program_type)` see "What this catches" below.
19
+ 4. Call `generate_protrak_program(program_name, program_type, source)` — writes BOTH
20
+ `Programs/<name>.cs` AND `Programs/<name>.json` (companion metadata required by Protrak's
21
+ import flow). **Do NOT use the Write tool here** — you will skip the .json companion and
22
+ the program will not import.
23
+
24
+ ```
25
+ get_protrak_program_context(type_name='Project', program_type='PreUpdate', description='block status change when invoice unpaid')
26
+ # ... agent writes the C# source ...
27
+ validate_protrak_program(code='<source>', program_type='PreUpdate')
28
+ generate_protrak_program(program_name='ProjectPreUpdate', program_type='PreUpdateTrigger', source='<source>')
29
+ ```
30
+
31
+ ### Editing an EXISTING program:
32
+ 1. Call `get_protrak_program_context(type_name, program_type, program_name='<ExistingName>')`.
33
+ Passing `program_name` switches the tool to refactor mode — response includes
34
+ `existing_source`, `existing_services_used`, `existing_patterns_detected`, and
35
+ `existing_class_name` alongside the usual schema/interface/patterns context.
36
+ **One call** — no need to chain `read_protrak_program` separately.
37
+ 2. Edit the C# source.
38
+ 3. Call `validate_protrak_program(code, program_type)` on the edited source.
39
+ 4. Write the edited source back to the existing `Programs/<name>.cs` file. Do NOT call
40
+ `generate_protrak_program` for refactors — the companion .json already exists.
41
+
42
+ ```
43
+ get_protrak_program_context(type_name='Project', program_type='PreUpdate', program_name='CalculateNextInvoiceDate')
44
+ # ... agent edits existing_source ...
45
+ validate_protrak_program(code='<edited source>', program_type='PreUpdate')
46
+ # Write edited source to existing_file_path
47
+ ```
48
+
49
+ ### What `validate_protrak_program` catches (and what you skip if you don't run it):
50
+
51
+ | Check | What breaks if you ship it |
52
+ | ----------------------------- | ------------------------------------------------ |
53
+ | `.Result` / `.Wait()` | Deadlock under Protrak's async context |
54
+ | `async void` | Exceptions escape silently |
55
+ | Service as private/readonly | Protrak DI requires PUBLIC PROPERTIES — won't inject |
56
+ | Missing interface | PreCreate without `IPreCreateTriggerProgramAsync<T>` — Protrak can't bind it |
57
+ | `*Async()` without `await` | Silent fire-and-forget |
58
+ | Service not declared as prop | Runtime null-ref on first use |
59
+ | Missing namespace | Protrak import rejects the file |
60
+
61
+ These are **Protrak-specific gotchas you cannot catch by reading the code**. Always run
62
+ `validate_protrak_program` after writing or editing.
63
+
64
+ ### Cost of skipping the MCP tools:
65
+
66
+ | Skip | What you miss |
67
+ | ----------------------------- | -------------------------------------------------------------- |
68
+ | `get_protrak_program_context` | Wrong interface, wrong service injection, wrong query patterns |
69
+ | Refactor mode (`program_name`)| Re-derive existing source from disk; miss `services_used` and `patterns_detected` |
70
+ | `validate_protrak_program` | Deadlocks, async-void bugs, DI shape mismatches in production |
71
+ | `generate_protrak_program` | No companion `.json` — Protrak won't import the program |
18
72
 
19
73
  ### Understanding the workspace:
20
- - Call `list_protrak_schema_elements` (optionally with `kind`: `type`, `attribute`,
21
- `lifecycle`, or `relation_type`) to catalog what's defined.
22
- - Call `read_protrak_schema_element` (with `kind` and `name`) for the full JSON of any one
23
- element — for `kind='type'` you get the resolved schema with attributes, lifecycle and
74
+ - `list_protrak_schema_elements` (optionally with `kind`: `type`, `attribute`, `lifecycle`,
75
+ `relation_type`) catalogs what's defined.
76
+ - `read_protrak_schema_element` (with `kind` and `name`) returns the full JSON of any one
77
+ element — for `kind='type'` you get the resolved schema with attributes, lifecycle, and
24
78
  relations hydrated.
25
- - Call `list_protrak_programs` for the program catalog; call `search_protrak_knowledge`
26
- with `scope='programs'` for relevance-ranked search by description.
79
+ - `list_protrak_programs` lists all programs; `read_protrak_program(program_name)` returns
80
+ source + metadata for one (but for refactor work, prefer `get_protrak_program_context`
81
+ with `program_name` — it returns source AND full context together).
82
+ - `search_protrak_knowledge(query, scope)` runs relevance-ranked search across patterns,
83
+ C# API docs, and workspace programs.
27
84
 
28
85
  ### Scaffolding schema elements (Types, Lifecycles, Attributes, RelationTypes):
29
86
  - For a brand-new entity, call `scaffold_protrak_entity` once with the type name, the
@@ -49,17 +106,28 @@ that implement Protrak interfaces (PreCreate, PostCreate, PreUpdate, PromoteActi
49
106
  - All schema names must be PascalCase. Aliases for `attribute_type`: `Number`→`Numeric`,
50
107
  `File`→`Attachment`.
51
108
 
52
- ### Key Protrak rules:
53
- - Services are **public properties**, never constructor parameters
54
- - Always null-check attributes: `instance.Attributes?["Name"]?.Value`
55
- - Use typed accessors: `GetTextAttributeValue`, `GetDateAttributeValue`, `GetPicklistAttributeValue`
56
- - Use `SetAttributeValue(name, AttributeType, value)` to write attributes
57
- - Prefer async interface variants (`RunAsync` not `Run`)
58
- - Return `ProgramResult { IsSuccess = false, Message = "..." }` to block operations in Pre-triggers
109
+ ### Key Protrak rules (each cross-references a `validate_protrak_program` check):
110
+ - Services are **public properties**, never constructor parameters — checked by
111
+ `service_as_public_property`.
112
+ - Implement the exact program-type interface — checked by `interface_compliance`.
113
+ - Prefer async interface variants (`RunAsync` not `Run`); never call `.Result` / `.Wait()`
114
+ checked by `no_dot_result`.
115
+ - Never write `async void` checked by `no_async_void`.
116
+ - `await` or `return` every `*Async()` call — checked by `missing_await`.
117
+ - Declare every service used as a public property — checked by `undeclared_service`.
118
+ - Declare a `namespace` — checked by `missing_namespace`.
119
+ - Always null-check attributes: `instance.Attributes?["Name"]?.Value` (style rule, not
120
+ validated by static analysis).
121
+ - Use typed accessors: `GetTextAttributeValue`, `GetDateAttributeValue`,
122
+ `GetPicklistAttributeValue` (style rule).
123
+ - Use `SetAttributeValue(name, AttributeType, value)` to write attributes (style rule).
124
+ - Return `ProgramResult { IsSuccess = false, Message = "..." }` to block operations in
125
+ Pre-triggers (style rule).
59
126
 
60
127
  ## Project structure
61
128
  - `Types/` — entity type JSON definitions
62
129
  - `Attributes/` — attribute JSON definitions
63
130
  - `Lifecycles/` — lifecycle state machine definitions
64
- - `Programs/` — C# program source + JSON metadata
131
+ - `Programs/` — C# program source + JSON metadata (paired files; use
132
+ `generate_protrak_program` to keep them in sync)
65
133
  - `RelationTypes/` — relation definitions between types
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prorigo/protrak-forge",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
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"
@@ -1,140 +0,0 @@
1
-
2
- # Aggregate Related Instances Pattern
3
-
4
- ## When to Use
5
- Use this pattern whenever a parent instance needs a value computed from its related child instances. Common uses:
6
- - Sum/total an attribute across related records (consumed effort, total cost, hours logged)
7
- - Count related instances by state (open subtasks, overdue items)
8
- - Roll up a max/min/latest date from related records
9
- - Calculate completion percentages from related deliverables
10
-
11
- This pattern is the canonical replacement for the **N+1 fan-out anti-pattern** (calling `GetInstanceAsync` once per related id just to read attributes — see "Anti-Pattern" below).
12
-
13
- ## Program Types
14
- Applies to: `Scheduler`, `PostCreate`, `PromoteActionCommand`, `Common`
15
-
16
- ## Services Used
17
- - `IInstanceService` — query parents, fetch related instances, write the aggregate back
18
- - `IQueryBuilderService` — filter the parent set by state/attribute (optional)
19
-
20
- ## Key Idiom: `RelatedQueryBuilder.Select(...)`
21
-
22
- The single most important rule for this pattern:
23
-
24
- > **Always use `RelatedQueryBuilder.Select(...)` to fetch the attributes you need from related instances in the same call — do not call `GetInstanceAsync` per related id.**
25
-
26
- `GetRelatedInstancesAsync` returns `RelatedInstance` objects that already carry the attributes you `.Select()`-ed. Calling `GetInstanceAsync` afterwards is an N+1 round-trip per parent and is strictly worse.
27
-
28
- ## Code Example
29
-
30
- Scheduler that, for each Project in `Active` state, sums "consumed effort" across all related Allocations (weekdays between AllocationStartDate and AllocationEndDate, today if end is blank/future) and writes back to `ConsumedEffort` and `ConsumedEffortCalculatedOn` on the Project.
31
-
32
- ```csharp
33
- using Prorigo.Protrak.API.Contracts;
34
- using Prorigo.Protrak.API.Contracts.Builders;
35
- using Prorigo.Protrak.API.Contracts.Extensions;
36
- using System;
37
- using System.Linq;
38
- using System.Threading.Tasks;
39
-
40
- namespace MyProject.Customization
41
- {
42
- public class CalculateProjectConsumedEffort : ISchedulerProgramAsync
43
- {
44
- public IInstanceService InstanceService { get; set; }
45
- public IQueryBuilderService QueryBuilderService { get; set; }
46
-
47
- public async Task RunAsync()
48
- {
49
- var projects = await InstanceService.GetInstancesAsync(new InstanceQuery
50
- {
51
- TypeName = "Project",
52
- StateName = "Active",
53
- Take = int.MaxValue,
54
- });
55
- if (projects?.Data == null) return;
56
-
57
- var today = DateTime.UtcNow.Date;
58
-
59
- foreach (var project in projects.Data)
60
- {
61
- // ✅ Fetch only the attributes we need from related Allocations,
62
- // in a SINGLE call. Do NOT loop GetInstanceAsync per id.
63
- var allocationQuery = RelatedQueryBuilder.Create()
64
- .ForRelation("ProjectToAllocation", "Allocation", RelationDirection.From)
65
- .Select("AllocationStartDate", "AllocationEndDate")
66
- .TakeAll()
67
- .Build();
68
-
69
- var allocations = await InstanceService.GetRelatedInstancesAsync(
70
- project.Id, allocationQuery);
71
-
72
- double totalWeekdays = 0;
73
- foreach (var allocation in allocations.SafeItems())
74
- {
75
- var start = allocation.GetDateAttributeValue("AllocationStartDate");
76
- var end = allocation.GetDateAttributeValue("AllocationEndDate");
77
- if (start == null) continue;
78
-
79
- var effectiveEnd = (end == null || end.Value.Date > today)
80
- ? today
81
- : end.Value.Date;
82
-
83
- totalWeekdays += CountWeekdays(start.Value.Date, effectiveEnd);
84
- }
85
-
86
- var update = InstanceBuilder.ForUpdate(project.Id, "Project")
87
- .SetNumericAttributeValue("ConsumedEffort", totalWeekdays)
88
- .SetDateAttributeValue("ConsumedEffortCalculatedOn", today)
89
- .Build();
90
-
91
- await InstanceService.UpdateInstanceAsync(update, null);
92
- }
93
- }
94
-
95
- private static int CountWeekdays(DateTime fromInclusive, DateTime toInclusive)
96
- {
97
- if (toInclusive < fromInclusive) return 0;
98
- int count = 0;
99
- for (var d = fromInclusive; d <= toInclusive; d = d.AddDays(1))
100
- {
101
- if (d.DayOfWeek != DayOfWeek.Saturday && d.DayOfWeek != DayOfWeek.Sunday)
102
- count++;
103
- }
104
- return count;
105
- }
106
- }
107
- }
108
- ```
109
-
110
- ## Anti-Pattern (DO NOT DO THIS)
111
-
112
- ```csharp
113
- // ❌ Wrong: N+1 round-trips. One DB hit per related allocation.
114
- var allocationsRef = await InstanceService.GetRelatedInstancesAsync(
115
- project.Id, RelatedQueryBuilder.Create()
116
- .ForRelation("ProjectToAllocation", "Allocation", RelationDirection.From)
117
- .TakeAll().Build());
118
-
119
- foreach (var ref in allocationsRef.SafeItems())
120
- {
121
- var full = await InstanceService.GetInstanceAsync(
122
- ref.Id, new[] { "AllocationStartDate", "AllocationEndDate" }); // ← extra call per id
123
- var start = full.GetDateAttributeValue("AllocationStartDate");
124
- // ...
125
- }
126
- ```
127
-
128
- The fix is to add `.Select("AllocationStartDate", "AllocationEndDate")` to the builder and read the attributes off the `RelatedInstance` directly. `RelatedInstance` exposes the same `GetTextAttributeValue` / `GetDateAttributeValue` / `GetNumericAttributeValue` accessors as `Instance` — see [Instance Accessor Methods](./instance-accessor-methods.md#on-relatedinstance).
129
-
130
- ## Key Rules
131
-
132
- - Always pre-select the attributes you need with `.Select(name1, name2, ...)`. Omitting `.Select()` returns instances without their attribute values populated; adding `GetInstanceAsync` calls afterwards is the N+1 anti-pattern.
133
- - Use `.TakeAll()` (or `Skip`/`Take` for pagination) — the default page size truncates large parents.
134
- - Use `RelatedInstance` accessors directly (`GetDateAttributeValue`, etc.) — same syntax as `Instance`.
135
- - Iterate with `.SafeItems()` to avoid manual null checks.
136
- - Write the rolled-up value back with `InstanceBuilder.ForUpdate(...)` + `UpdateInstanceAsync`. Do not mutate the parent in-place inside a Scheduler — Scheduler programs receive no incoming Instance.
137
- - For schedulers, prefer `Take = int.MaxValue` on the parent query to ensure all parents are processed; for *very* large datasets, paginate explicitly.
138
-
139
- ## Related Patterns
140
- - [Query and Filter](./query-filter-pattern.md) — filtering the parent set