@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.
- package/bin/protrak-forge.js +403 -219
- package/data/CLAUDE.md.template +89 -21
- package/package.json +1 -1
- package/data/patterns/aggregate-related-instances.md +0 -140
package/data/CLAUDE.md.template
CHANGED
|
@@ -7,23 +7,80 @@ that implement Protrak interfaces (PreCreate, PostCreate, PreUpdate, PromoteActi
|
|
|
7
7
|
|
|
8
8
|
## How to write Protrak programs
|
|
9
9
|
|
|
10
|
-
|
|
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
|
|
13
|
-
1. Call `get_protrak_program_context
|
|
14
|
-
|
|
15
|
-
in ONE call.
|
|
16
|
-
2. Write the C#
|
|
17
|
-
3. Call `validate_protrak_program`
|
|
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
|
-
-
|
|
21
|
-
`
|
|
22
|
-
-
|
|
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
|
-
-
|
|
26
|
-
|
|
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
|
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
|
|
58
|
-
-
|
|
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,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
|