research-document
Time Entry System — State Model v0.1
Time Entry System — State Model v0.1
Date: 2026-08-14 Status: Initial semantic model Purpose: Define the first application-specific state machines, transitions, requirements, invariants, and derived capabilities for the time-entry system.
This document is intentionally separate from the business requirements.
Business requirements define what the product must do. This document defines the current semantic/state model used to implement those requirements.
Core rule:
States describe durable semantic conditions. Transitions describe legal changes. Requirements describe conditions for a specific transition. Invariants describe truths that must always hold. Capabilities are derived legal actions, not additional state.
Do not add states merely because a value can be represented as an enum. A state should exist because it changes legitimate future behavior.
1. TimeEntry State Family
TimeEntryStatus
Recorded
Voided
Superseded
Recorded
Meaning:
The entry is part of the presently authoritative allocation of time.
Consequences:
- contributes to totals;
- may be corrected;
- may be split when splittable;
- may be voided.
Voided
Meaning:
The entry was intentionally withdrawn from current reported time.
Consequences:
- does not contribute to totals;
- remains historically recoverable;
- has no ordinary v0.1 transitions.
Superseded
Meaning:
The entry was replaced by one or more new entries that now represent the same underlying time allocation more accurately.
Typical cause:
- SplitTimeEntry
Consequences:
- does not contribute to totals;
- replacement entries are authoritative;
- remains historically recoverable;
- has no ordinary v0.1 transitions.
Voided and Superseded are intentionally different.
Voided means:
this entry should no longer count
Superseded means:
this time still exists, but this representation has been replaced
2. TimeEntry Transition Table
| Source | Operation | Result |
|---|---|---|
| none | CreateTimeEntry | Recorded |
| Recorded | CorrectTimeEntry | Recorded |
| Recorded | SplitTimeEntry | source Superseded + two Recorded children |
| Recorded | VoidTimeEntry | Voided |
| Voided | none in v0.1 | terminal |
| Superseded | none in v0.1 | terminal |
3. CreateTimeEntry
Transition:
none -> Recorded
Inputs:
PersonId
Date
Start
DurationUnits
ProjectId
Labels[]
Description
Requirements:
- person exists;
- person is active;
- project exists;
- project is Active;
- selected labels exist;
- selected labels are active;
- durationUnits > 0;
- duration is integral six-minute units;
- start is on a six-minute boundary;
- interval does not cross midnight;
- description is non-blank;
- interval does not overlap another Recorded entry for the same person.
Produces:
- new stable TimeEntryId;
- status = Recorded;
- initial revision/version;
- creation provenance.
4. CorrectTimeEntry
Transition:
Recorded -> Recorded
A semantic transition does not have to change an enum value.
Correctable values:
- date;
- start;
- durationUnits;
- project;
- labels;
- description.
Requirements:
- source entry is Recorded;
- resulting durationUnits > 0;
- resulting start is on a six-minute boundary;
- resulting interval does not cross midnight;
- resulting interval does not overlap another Recorded entry for the same person;
- resulting description is non-blank;
- selected labels exist;
- newly assigned labels are active;
- project exists.
Project rule:
An archived project cannot be newly assigned.
However, an existing entry that already references a project later archived may retain that historical project association while correcting unrelated fields.
If the correction changes ProjectId, the destination project must be Active.
Produces:
- same TimeEntryId;
- status remains Recorded;
- revised values;
- revision/version increment;
- historical change remains recoverable.
5. SplitTimeEntry
Transition:
Recorded source
->
Superseded source
+ Recorded left child
+ Recorded right child
Inputs:
SourceEntryId
SplitPoint
Requirements:
- source.status = Recorded;
- split point > source start;
- split point < source end;
- split point lies on six-minute boundary;
- both resulting entries have duration >= 1 unit.
Derived values:
left.start = source.start
left.end = splitPoint
right.start = splitPoint
right.end = source.end
Strong postconditions:
left.durationUnits + right.durationUnits
=
source.durationUnits
Children must exactly cover the original interval.
No gap. No overlap.
Both children preserve:
- personId;
- date.
Both initially inherit:
- projectId;
- labels;
- description.
Provenance:
left.splitFrom = source.id
right.splitFrom = source.id
Recommended reverse relationship:
source.supersededBy = [left.id, right.id]
Produces:
source.status = Superseded
left.status = Recorded
right.status = Recorded
The source no longer contributes to totals.
6. VoidTimeEntry
Transition:
Recorded -> Voided
Inputs:
EntryId
Reason? # optional initially
Requirements:
- entry.status = Recorded.
Produces:
- status = Voided;
- void timestamp;
- optional reason;
- version/revision increment.
Consequences:
The entry stops contributing to:
- day totals;
- week totals;
- project totals;
- label totals.
Voided is terminal in v0.1.
Do not add RestoreTimeEntry until a real product requirement justifies it.
7. Derived TimeEntry Capabilities
Capabilities are not states.
They are derived from state plus other requirements.
Recorded entry
Generally available:
CanCorrect
CanVoid
Recorded entry with durationUnits >= 2
Additionally:
CanSplit
Recorded entry with durationUnits = 1
CanSplit = false
because no legal interior six-minute boundary exists.
Voided
No ordinary v0.1 capabilities.
Superseded
No ordinary v0.1 capabilities.
Do not create states such as:
RecordedSplittable
RecordedNotSplittable
Splittable is a derived capability, not lifecycle state.
8. TimeEntry Invariants
These must hold regardless of which transition produced current state.
TIME-001
durationUnits > 0
for every Recorded entry.
TIME-002
Duration is an integer number of six-minute units.
TIME-003
Start lies on a six-minute boundary.
TIME-004
A Recorded entry does not cross midnight.
TIME-005
Recorded entries for the same person do not overlap.
TIME-006
Every Recorded entry references an existing project.
TIME-007
Every label reference resolves to a known label.
TIME-008
Description is non-blank.
TIME-009
Voided and Superseded entries do not contribute to current totals.
9. State vs Requirement vs Invariant
Example state:
entry.status = Recorded
Example transition requirement:
SplitPoint lies strictly inside the entry interval
Example invariant:
Recorded entries for the same person do not overlap
Do not create lifecycle states for:
NonOverlapping
ValidProject
HasDescription
Splittable
Those are invariants or derived conditions.
10. Project State Family
ProjectStatus
Active
Archived
Active
- accepts new ordinary time entries;
- may be corrected;
- may be archived.
Archived
- remains historically resolvable;
- remains reportable;
- may not receive newly assigned ordinary time entries.
Potential Reactivate behavior must be an explicit product decision.
11. Project Transitions
CreateProject
none -> Active
CorrectProject
Active -> Active
Potentially allow limited corrections to Archived projects if needed for historical metadata, but do not assume this without a requirement.
ArchiveProject
Active -> Archived
ReactivateProject
Not required in v0.1 unless explicitly approved.
12. Cross-State Semantic Dependencies
TimeEntry and Project remain separate state families.
Do not combine them into a single mega-state.
Example:
CreateTimeEntry
requires:
Project(projectId).status = Active
This is a semantic dependency:
TimeEntry.Create
depends on
Project.Active
CorrectTimeEntry has a related dependency:
If ProjectId changes:
destination Project.status must be Active
An existing historical reference to an Archived project remains legitimate.
13. Synchronization State Family
Synchronization state is not TimeEntry business state.
SyncState
Clean
ModifiedLocally
Saving
SaveFailed
SaveOutcomeUnknown
Conflict
This state family describes persistence/execution condition.
A TimeEntry may remain:
Recorded
while synchronization is:
SaveOutcomeUnknown
Do not merge these dimensions.
14. Synchronization Transitions
LocalSemanticChange
Clean -> ModifiedLocally
A valid local semantic transition has changed the current weekly document but has not yet been durably confirmed remotely.
BeginSave
ModifiedLocally -> Saving
SaveConfirmed
Saving -> Clean
Remote GitHub state conclusively contains intended change.
SaveKnownFailed
Saving -> SaveFailed
The system has authoritative evidence that the intended write did not occur.
SaveIndeterminate
Saving -> SaveOutcomeUnknown
The caller cannot establish whether the intended GitHub write committed.
Examples:
- connection lost after request dispatch;
- timeout before conclusive response;
- ambiguous transport failure.
SaveConflict
Saving -> Conflict
Expected Git SHA/version is stale.
Remote state changed concurrently.
RetryKnownFailedSave
SaveFailed -> Saving
Legal only when prior effect is known not to have occurred.
ReconcileUnknownSuccess
SaveOutcomeUnknown -> Clean
Reconciliation proves intended write already exists remotely.
ReconcileUnknownAbsent
SaveOutcomeUnknown -> ModifiedLocally
Reconciliation proves intended write did not occur.
Only after this transition may normal save logic create a new write attempt.
ResolveConflict
Conflict -> ModifiedLocally
Requires deliberate semantic resolution of local vs remote state.
15. Synchronization Capability Principles
Saving
Do not start another equivalent semantic save while an existing save attempt is unresolved.
SaveOutcomeUnknown
Available:
ReconcileRemoteState
Unavailable:
blindly create another equivalent save operation
Unknown must not collapse into failure.
Conflict
Available:
InspectRemote
ResolveConflict
Do not apply generic last-write-wins.
16. Semantic Operation Identity for Persistence
A future implementation should associate a stable semantic operation ID with a meaningful save operation.
Example:
SaveWeek:kevin:2026-08-10:<operation-id>
Transport retries are attempts of the same semantic operation.
Do not confuse:
retry same semantic operation
with:
create another new semantic operation
This distinction is especially important during SaveOutcomeUnknown.
17. State Families in v0.1
The current intended state families are:
TimeEntryStatus
Recorded
Voided
Superseded
ProjectStatus
Active
Archived
SyncState
Clean
ModifiedLocally
Saving
SaveFailed
SaveOutcomeUnknown
Conflict
These state families are intentionally independent.
Do not construct a Cartesian-product mega-state such as:
RecordedActiveProjectSaving
Compose dimensions through requirements and dependencies instead.
18. States We Are Deliberately NOT Adding Yet
Do not currently introduce:
Draft
Submitted
Approved
Rejected
Locked
Billed
Invoiced
PendingApproval
ReadyForBilling
These may become legitimate later if they change what actions are legally available.
Rule:
Earn your states.
19. Future State-Migration Experiment
A later version may introduce approval state.
Example:
Approved
may later split into:
ManagerApproved
ClientApproved
or:
InternallyApproved
ReadyForBilling
When this occurs, consequential interpretations must not silently inherit old Approved behavior.
Dependent meanings such as:
CanEdit
CanInvoice
CanExport
CanClosePeriod
must be explicitly reconsidered.
This is intended as a test of semantic dependency and migration handling.
20. Implementation Directive
Implement these state families as application-specific semantic types.
Do not create a generic metadata-driven runtime representation of business state.
The reusable architecture may understand:
- how to validate transitions;
- how to derive capabilities;
- how to version state;
- how to track semantic dependencies;
- how to produce structured diagnostics.
It should not define what Recorded, Voided, Superseded, Active, or Archived mean.
Those meanings belong to this application.