Skip to content

Commit cb645f9

Browse files
committed
Merged steps-history: every step gets the person history rows Rock's step save hook writes, in Rock's Steps history category, with a new invariant that checks them (decision 134). All three packages pass verify on Rock 21.0.2
2 parents 6da4606 + 4197f3c commit cb645f9

7 files changed

Lines changed: 111 additions & 7 deletions

File tree

‎docs/decisions.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ The design is in [Steps](steps.md). The adult Discipleship Path and the Student
175175
| 117 | 2026-10-22 | A student's baptism (decision 87) is planned with the Student Path. 60% of the students with a Follow Jesus decision are baptized here on the first baptism Sunday at least two weeks after it, when that Sunday has come by the install day. 10% of the other students are baptized here on a baptism Sunday after they started coming to the student service. A student is 12 or older when baptized, and a Baptism Date in a Cast file is kept. Both rates are shares, counted as in decision 116. The adult rates in decision 107 are unchanged and count only adults. | A decision and a baptism tell one story, with time between them to talk with a leader. A recent decision waits for the next baptism Sunday, as it does in a real church. Students are not adults, so the adults' 60% stays where it was. |
176176
| 118 | 2026-10-22 | A child whose household file sets a graduation keeps that graduation year in the loader past age 17, so attendance checks them in by grade until they graduate. Jasmine Washington's file now says she graduated this past May, as a relative date, so she checks in to the student service through her senior year and has completed the Student Path. | The loader derived a grade only for ages 5 to 17, so Jasmine had no graduation year in the loader and no check-ins, though her notes say she sang on the student worship team. A relative date keeps "this past May" true on any install day. Only a file that sets a graduation keeps it, so no one else changes. |
177177
| 119 | 2026-10-22 | A Cast story can name the student events a person went to, by season and by how many years back from the latest one held by the install day, or say they took a step and let the usual rules date it. The load fails when a story cannot hold. A Cast person's step expectations name the programs they must have completed, by key (`programCompletions`), in place of a yes or no for one program. | "Last summer's camp" must stay true on any install day. With two programs, a yes or no cannot say which program a person completed. |
178+
| 134 | 2026-10-23 | Every step has the person history Rock's step save hook writes. One "Added" row with the step's name and campus is dated the day the step started. A step that started In Progress and was completed later also has its status change and completion date on the day it was completed, and a step that ended has its end date on the day it ended. | The person's History tab shows each step the way a church that recorded it in Rock sees it, so person pages look real in demos. |
178179

179180
## Communications
180181

‎docs/roadmap.md‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,10 @@ Ideas we agreed on but set aside for later. Move an item into "Next" when we pic
7878

7979
Both step programs are built (see [Steps](steps.md)). These ideas from its "Later" section wait for other work or a reason to pick them up. Keep the two lists in step.
8080

81-
1. **Person history.** Rock's step save hook adds a line to the person's History tab for each new step. The loader does not write those yet, as it does not for group memberships.
82-
2. **eRA steps** in Rock's Core Steps program, from the attendance the data already has.
83-
3. **Links to learning and events.** Foundations links to a learning class, and Student Camp and Mission Trip to a registration, once learning (LMS) and event registrations are built.
84-
4. **Student small groups,** which would add a group step to the Student Path.
85-
5. **Step workflows,** such as a follow-up after a baptism.
81+
1. **eRA steps** in Rock's Core Steps program, from the attendance the data already has.
82+
2. **Links to learning and events.** Foundations links to a learning class, and Student Camp and Mission Trip to a registration, once learning (LMS) and event registrations are built.
83+
3. **Student small groups,** which would add a group step to the Student Path.
84+
4. **Step workflows,** such as a follow-up after a baptism.
8685

8786
### Packages and loaders
8887

‎docs/steps.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,10 @@ A student's baptism goes on the Discipleship Path's Baptism step, with the Bapti
139139
- **Other students.** 10% of the other students are baptized here on a baptism Sunday after they started coming to the student service.
140140
- **Rules.** A student is 12 or older when baptized. A Baptism Date in a Cast file is kept as it is. The adult rates above are unchanged, because students are not adults.
141141

142-
## Campus and program completions
142+
## Campus, history, and program completions
143143

144144
- **Campus.** Every step's campus is the person's campus, and every step is on their primary alias.
145+
- **Person history.** Every step has the History tab line Rock's step save hook writes: "Added" with the step's name and campus on the day it started (decision 134). A step that started In Progress and was completed later (a class, or a Pending volunteer who became Active) also has its status change and completion date on the day it was completed. A step that ended has its end date on the day it ended.
145146
- **Program completions.** A person whose Complete steps cover every step type in a program gets a completion for that program, the way Rock's Update Step Program Completions job records it: from their earliest Complete step's start to their latest Complete step's completion. Every step of theirs in the program, In Progress ones too, points at it. Rock's job finds nothing to add after install, in either program.
146147

147148
## How many
@@ -222,4 +223,3 @@ Jasmine's household file now sets her graduation as a relative date, this past M
222223
- **Links to learning and events.** When learning (LMS) and event registrations are built, Foundations can link to a learning class, and Student Camp and Mission Trip to a registration.
223224
- **Student small groups,** which would add a group step to the Student Path.
224225
- **Step workflows,** such as a follow-up after a baptism.
225-
- **Person history.** Rock's step save hook adds a line to the person's History tab for each new step. The loader does not write those yet, as it does not for group memberships.

‎docs/verification.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ It runs the checks below for every dataset. A full run takes about 5 minutes.
137137
| `baptism-step-agrees-with-baptism-date` | The Baptism step and the Baptism Date attribute always agree, and Baptized Here matches on the step and the person. |
138138
| `serve-step-follows-starting-point` | Rock's prerequisite: every Serve step has a Starting Point Class completed before it. |
139139
| `give-step-agrees-with-journey-stage` | A giver Rock shows as Consistent has an open Give step, and an open Give step belongs to a Consistent or Occasional giver. |
140+
| `step-has-its-person-history` | Every step has one "Added" line on its person's History tab, dated the day it started, and a step completed after it started has its status change. Every step history line points at a step of that person. |
140141
| `program-completions-follow-rocks-rule` | Rock's Update Step Program Completions job finds nothing to add or fix after install, in any program the dataset has steps in. |
141142
| `student-step-belongs-to-a-student` | Every Student Path step belongs to someone in grades 7 to 12 on its start who had checked in to the student service by then. Mission Trip is high school only, and Student Leader is juniors and seniors only. |
142143
| `student-path-decisions-and-leaders` | Follow Jesus and Student Leader happen at most once per person. Student Leader ends at graduation in May, or stays open while the student is still in school. |

‎loader/loading/StepsLoader.cs‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ internal sealed class StepsLoader
4646
{
4747
private static readonly Guid GroupEntityTypeGuid = new Guid( "9BBFDA11-0D22-40D5-902F-60ADFBC88987" );
4848

49+
private static readonly Guid PersonStepHistoryCategoryGuid = new Guid( "517BB2D3-0A50-4132-818E-63BB3C81EAE9" );
50+
4951
private readonly RockDatabase _db;
5052
private readonly SampleDataSource _source;
5153
private readonly AnchorDates _dates;
@@ -406,10 +408,12 @@ private void Write( StepsConfiguration config, StepsSetup setup, StepPlan plan )
406408
var values = TableFactory.Create( "IsSystem:bool", "AttributeId:int", "EntityId:int", "Value:string", "ValueAsDateTime:date", "ValueAsBoolean:bool", "IsPersistedValueDirty:bool",
407409
"Guid:guid", "ForeignKey:string", "CreatedDateTime:date", "ModifiedDateTime:date" );
408410
var baptisms = plan.Baptisms.ToDictionary( b => b.PersonId );
411+
var history = new StepHistory( _db, _foreignKey, config );
409412

410413
foreach ( var step in plan.Steps.OrderBy( s => s.AliasId ).ThenBy( s => setup.TypeIds[s.StepType] ).ThenBy( s => s.Start ) )
411414
{
412415
var id = nextStepId++;
416+
history.Add( step, id );
413417
var modified = new[] { step.Start, step.Completed ?? step.Start, step.End ?? step.Start }.Max();
414418
var programKey = setup.ProgramKeyByType[step.StepType];
415419
var program = setup.Programs[programKey];
@@ -436,6 +440,72 @@ private void Write( StepsConfiguration config, StepsSetup setup, StepPlan plan )
436440
_db.BulkInsert( "StepProgramCompletion", completions, keepIdentity: true );
437441
_db.BulkInsert( "Step", steps, keepIdentity: true );
438442
_db.BulkInsert( "AttributeValue", values, keepIdentity: false );
443+
_db.BulkInsert( "History", history.Rows, keepIdentity: false );
444+
}
445+
446+
/*
447+
10/23/26 - CLAUDE
448+
449+
Rock's step save hook (Rock/Model/Engagement/Step/Step.SaveHook.cs) writes person history: one
450+
"Added" row when a step is made, with the campus as related data, and "Modify" rows when its status,
451+
end, or completion date changes later. A step the plan completes after its start (a class, or a
452+
Pending volunteer who became Active) was added In Progress and completed on that day. A step with an
453+
end date was ended on that day. Dates use Rock's US short date format, as the hook writes them.
454+
455+
Reason: The person's History tab shows each step the way a church that recorded it in Rock sees it.
456+
*/
457+
458+
/// <summary>
459+
/// The person history rows Rock's step save hook writes for each step.
460+
/// </summary>
461+
private sealed class StepHistory
462+
{
463+
private readonly string _foreignKey;
464+
private readonly int _categoryId;
465+
private readonly int _personEntityTypeId;
466+
private readonly int _stepEntityTypeId;
467+
private readonly Dictionary<int, string> _campusNames;
468+
private readonly Dictionary<string, string> _typeNames;
469+
470+
public DataTable Rows { get; } = TableFactory.Create( "IsSystem:bool", "CategoryId:int", "EntityTypeId:int", "EntityId:int", "Caption:string", "RelatedEntityTypeId:int", "RelatedEntityId:int",
471+
"Verb:string", "ChangeType:string", "ValueName:string", "OldValue:string", "NewValue:string", "RelatedData:string", "IsSensitive:bool",
472+
"Guid:guid", "ForeignKey:string", "CreatedDateTime:date", "ModifiedDateTime:date" );
473+
474+
public StepHistory( RockDatabase db, string foreignKey, StepsConfiguration config )
475+
{
476+
_foreignKey = foreignKey;
477+
_categoryId = db.RequireIdByGuid( "Category", PersonStepHistoryCategoryGuid );
478+
_personEntityTypeId = Convert.ToInt32( db.Scalar( "SELECT [Id] FROM [EntityType] WHERE [Name] = 'Rock.Model.Person'" ) );
479+
_stepEntityTypeId = Convert.ToInt32( db.Scalar( "SELECT [Id] FROM [EntityType] WHERE [Name] = 'Rock.Model.Step'" ) );
480+
_campusNames = db.Query( "SELECT [Id], [Name] FROM [Campus]" ).ToDictionary( r => Convert.ToInt32( r["Id"] ), r => ( string ) r["Name"] );
481+
_typeNames = config.StepTypes.ToDictionary( t => t.Key, t => t.Name );
482+
}
483+
484+
public void Add( PlannedStep step, int stepId )
485+
{
486+
var name = _typeNames[step.StepType];
487+
Row( step, stepId, name, "STEPADDED", name, null, null, _campusNames.GetValueOrDefault( step.CampusId ), step.Start );
488+
489+
if ( step.Completed.HasValue && step.Completed.Value > step.Start )
490+
{
491+
Row( step, stepId, name, "MODIFY", "Step Status", "In Progress", "Complete", null, step.Completed.Value );
492+
Row( step, stepId, name, "MODIFY", "Completed", string.Empty, ShortDate( step.Completed.Value ), null, step.Completed.Value );
493+
}
494+
495+
if ( step.End.HasValue && step.End.Value > step.Start )
496+
{
497+
Row( step, stepId, name, "MODIFY", "Step End", string.Empty, ShortDate( step.End.Value ), null, step.End.Value );
498+
}
499+
}
500+
501+
private void Row( PlannedStep step, int stepId, string caption, string verb, string valueName, string oldValue, string newValue, string relatedData, DateTime when )
502+
{
503+
Rows.Rows.Add( false, _categoryId, _personEntityTypeId, step.PersonId, caption, _stepEntityTypeId, stepId, verb, "Record", valueName,
504+
( object ) oldValue ?? DBNull.Value, ( object ) newValue ?? DBNull.Value, ( object ) relatedData ?? DBNull.Value, false,
505+
Guid.NewGuid(), _foreignKey, when, when );
506+
}
507+
508+
private static string ShortDate( DateTime date ) => date.ToString( "M/d/yyyy", System.Globalization.CultureInfo.InvariantCulture );
439509
}
440510

441511
private static int? DateKey( DateTime? date ) => date.HasValue ? int.Parse( date.Value.ToString( "yyyyMMdd" ) ) : null;

‎validation/invariants.sql‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,35 @@ WHERE [s].[ForeignKey] = @ForeignKey
275275
OR ISNULL( [s].[CampusId], 0 ) <> ISNULL( [p].[PrimaryCampusId], 0 )
276276
OR [s].[PersonAliasId] <> [p].[PrimaryAliasId] );
277277

278+
-- check: step-has-its-person-history
279+
-- why: Rock's step save hook writes person history: one "Added" row per step on the day it was made, and a status change on the day a step that started In Progress was completed. The person's History tab shows each step.
280+
WITH [StepHistory] AS (
281+
SELECT [h].*
282+
FROM [History] AS [h]
283+
INNER JOIN [Category] AS [c] ON [c].[Id] = [h].[CategoryId]
284+
WHERE [h].[ForeignKey] = @ForeignKey AND [c].[Guid] = '517BB2D3-0A50-4132-818E-63BB3C81EAE9'
285+
)
286+
SELECT ( SELECT COUNT(*)
287+
FROM [Step] AS [s]
288+
INNER JOIN [StepType] AS [st] ON [st].[Id] = [s].[StepTypeId]
289+
INNER JOIN [PersonAlias] AS [pa] ON [pa].[Id] = [s].[PersonAliasId]
290+
WHERE [s].[ForeignKey] = @ForeignKey
291+
AND ( SELECT COUNT(*) FROM [StepHistory] AS [h]
292+
WHERE [h].[RelatedEntityId] = [s].[Id] AND [h].[EntityId] = [pa].[PersonId] AND [h].[Verb] = 'STEPADDED'
293+
AND [h].[ValueName] = [st].[Name] AND [h].[CreatedDateTime] = [s].[StartDateTime] ) <> 1 )
294+
+ ( SELECT COUNT(*)
295+
FROM [Step] AS [s]
296+
INNER JOIN [PersonAlias] AS [pa] ON [pa].[Id] = [s].[PersonAliasId]
297+
WHERE [s].[ForeignKey] = @ForeignKey AND [s].[CompletedDateTime] > [s].[StartDateTime]
298+
AND NOT EXISTS ( SELECT 1 FROM [StepHistory] AS [h]
299+
WHERE [h].[RelatedEntityId] = [s].[Id] AND [h].[EntityId] = [pa].[PersonId] AND [h].[Verb] = 'MODIFY'
300+
AND [h].[ValueName] = 'Step Status' AND [h].[CreatedDateTime] = [s].[CompletedDateTime] ) )
301+
+ ( SELECT COUNT(*)
302+
FROM [StepHistory] AS [h]
303+
WHERE NOT EXISTS ( SELECT 1 FROM [Step] AS [s]
304+
INNER JOIN [PersonAlias] AS [pa] ON [pa].[Id] = [s].[PersonAliasId]
305+
WHERE [s].[Id] = [h].[RelatedEntityId] AND [pa].[PersonId] = [h].[EntityId] ) );
306+
278307
-- check: baptism-step-agrees-with-baptism-date
279308
-- why: The Baptism step and Rock's Baptism Date person attribute always agree, and the step's Baptized Here matches the person's.
280309
WITH [Dates] AS (

‎validation/rock-contract.json‎

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)