{"id":45639091,"url":"https://github.com/zot/change-tracker","last_synced_at":"2026-02-24T02:01:21.804Z","repository":{"id":335788748,"uuid":"1122808381","full_name":"zot/change-tracker","owner":"zot","description":"Manage a tree of variables connected to code using a Resolver interface to implement the connection","archived":false,"fork":false,"pushed_at":"2026-02-13T21:59:00.000Z","size":3963,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-14T04:23:31.674Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zot.png","metadata":{"files":{"readme":"README.md","changelog":"change-tracker.test","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-25T14:58:21.000Z","updated_at":"2026-02-13T21:59:03.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/zot/change-tracker","commit_stats":null,"previous_names":["zot/change-tracker"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/zot/change-tracker","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zot%2Fchange-tracker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zot%2Fchange-tracker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zot%2Fchange-tracker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zot%2Fchange-tracker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zot","download_url":"https://codeload.github.com/zot/change-tracker/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zot%2Fchange-tracker/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29768307,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-24T01:40:24.820Z","status":"online","status_checked_at":"2026-02-24T02:00:07.497Z","response_time":75,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-02-24T02:01:20.018Z","updated_at":"2026-02-24T02:01:21.794Z","avatar_url":"https://github.com/zot.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Change Tracker\n\nA Go package for variable management with automatic change detection. Track values in nested data structures and detect changes with priority-based sorting.\n\n**No observer pattern, no event emitters, no interfaces to implement.** Just point the tracker at your existing data structures and detect what changed.\n\n## Features\n\n- **Variable Tracking** - Track values in nested object hierarchies with parent-child relationships\n- **Change Detection** - Automatic detection via value comparison with `DetectChanges()`\n- **Priority Sorting** - Changes returned sorted by priority (high → medium → low)\n- **Object Registry** - Consistent identity for objects via weak references (Go 1.24+)\n- **Path Navigation** - Navigate nested structures: `\"Address.City\"`, `\"items.0\"`, `\"GetName()\"`\n- **Pluggable Resolvers** - Custom navigation strategies for complex domains\n- **Wrappers** - Provide alternative objects for child navigation via custom resolvers\n- **Recomputation Timing** - Per-variable `ComputeTime` and `MaxComputeTime` for profiling\n- **Diagnostics** - Per-variable diagnostic collection via `Diag()` for debugging resolvers\n- **Structured Errors** - Typed `VariableError` with categorized error types\n- **Zero Coupling** - Domain objects require no modification\n\n## Installation\n\n```bash\ngo get github.com/zot/change-tracker\n```\n\nRequires Go 1.24+ (uses `weak` package).\n\n## Quick Start\n\n```go\ntracker := changetracker.NewTracker()\n\n// Create a root variable holding your data\ndata := \u0026MyData{Name: \"Alice\", Count: 42}\nroot := tracker.CreateVariable(data, 0, \"\", nil)\n\n// Create child variables to track nested values\nname := tracker.CreateVariable(nil, root.ID, \"Name\", nil)\ncount := tracker.CreateVariable(nil, root.ID, \"Count?priority=high\", nil)\n\n// Make changes to your data...\ndata.Name = \"Bob\"\ndata.Count = 100\n\n// Detect what changed (sorted by priority: high → medium → low)\nchanges := tracker.DetectChanges()\nfor _, change := range changes {\n    fmt.Printf(\"Variable %d: value=%v props=%v\\n\",\n        change.VariableID, change.ValueChanged, change.PropertiesChanged)\n}\n```\n\n## Access Modes\n\n| Mode | Read | Write | Change Detection | Initial Value |\n|------|------|-------|------------------|---------------|\n| `rw` (default) | ✓ | ✓ | ✓ | computed |\n| `r` | ✓ | ✗ | ✓ | computed |\n| `w` | ✗ | ✓ | ✗ | computed |\n| `action` | ✗ | ✓ | ✗ | skipped |\n\nSet via path query: `\"field?access=r\"` or properties map.\n\nThe `action` mode is for variables that trigger side effects (like `AddContact(_)`) where computing the initial value would invoke the action prematurely.\n\n## Path Navigation\n\nPaths navigate from a parent variable's value to a nested value:\n\n```go\n// Struct fields\ntracker.CreateVariable(nil, root.ID, \"Address.City\", nil)\n\n// Slice indices\ntracker.CreateVariable(nil, root.ID, \"Items.0.Name\", nil)\n\n// Zero-arg method calls (getters)\ntracker.CreateVariable(nil, root.ID, \"GetName()\", map[string]string{\"access\": \"r\"})\n\n// One-arg method calls (setters)\ntracker.CreateVariable(nil, root.ID, \"SetName(_)\", map[string]string{\"access\": \"action\"})\n\n// URL-style query parameters set properties\ntracker.CreateVariable(nil, root.ID, \"Count?priority=high\u0026label=counter\", nil)\n```\n\n## Priority Levels\n\n| Priority | Value | Use Case |\n|----------|-------|----------|\n| High | 1 | Critical changes to process first |\n| Medium | 0 | Default priority |\n| Low | -1 | Background/deferred changes |\n\nSet via path properties: `\"field?priority=high\"`\n\nProperties can also have independent priorities via suffix: `v.SetProperty(\"label:high\", \"Important\")`\n\n## Active Flag\n\nVariables can be deactivated to skip change detection for an entire subtree:\n\n```go\nv.SetActive(false)  // v and all descendants skipped during DetectChanges\nv.SetActive(true)   // re-enable\n```\n\n## Wrappers\n\nCustom resolvers can provide alternative objects for child navigation:\n\n```go\ntype myResolver struct { *changetracker.Tracker }\n\nfunc (r *myResolver) CreateWrapper(v *changetracker.Variable) any {\n    if p, ok := v.Value.(*Person); ok {\n        return \u0026PersonView{DisplayName: p.First + \" \" + p.Last}\n    }\n    return nil\n}\n\ntr := changetracker.NewTracker()\ntr.Resolver = \u0026myResolver{tr}\nparent := tr.CreateVariable(person, 0, \"?wrapper=true\", nil)\n// Children now navigate through PersonView, not Person\nchild := tr.CreateVariable(nil, parent.ID, \"DisplayName\", nil)\n```\n\nWrappers support state preservation — returning the same pointer from `CreateWrapper` keeps the wrapper's internal state intact across value changes.\n\n## Recomputation Timing\n\nEach variable tracks how long its own path navigation takes:\n\n```go\nchild := tracker.CreateVariable(nil, root.ID, \"Address.City\", nil)\nval, _ := child.Get()\n\nfmt.Println(child.ComputeTime)    // duration of most recent recompute\nfmt.Println(child.MaxComputeTime) // peak duration across all recomputes\n```\n\nTiming measures only the variable's own path navigation, excluding parent value retrieval.\n\n## Diagnostics\n\nCustom resolvers can emit per-variable diagnostics during path navigation:\n\n```go\ntracker.DiagLevel = 1  // enable level-1 diagnostics\n\n// In your custom resolver's Get method:\nfunc (r *myResolver) Get(obj any, pathElement any) (any, error) {\n    r.Tracker.Diag(1, \"resolving %v on %T\", pathElement, obj)\n    return r.Tracker.Get(obj, pathElement)\n}\n\n// After Get() or DetectChanges(), check variable.Diags\nval, _ := child.Get()\nfor _, msg := range child.Diags {\n    fmt.Println(msg)\n}\n```\n\nDiagnostics are cleared at the start of each recompute. `DiagLevel = 0` (default) disables collection.\n\n## Structured Errors\n\nOperations return `*VariableError` with typed error categories:\n\n```go\n_, err := variable.Get()\nif ve, ok := variable.Error.(*changetracker.VariableError); ok {\n    switch ve.ErrorType {\n    case changetracker.PathError:   // path navigation failed\n    case changetracker.NotFound:    // variable or parent not found\n    case changetracker.BadAccess:   // access mode violation\n    case changetracker.NilPath:     // nil value in path\n    case changetracker.BadCall:     // method call failed\n    }\n}\n```\n\n## Object Registry\n\nThe tracker maintains a weak map from Go objects (pointers and maps) to IDs, providing consistent identity without modifying domain types:\n\n```go\nalice := \u0026Person{Name: \"Alice\"}\nroot := tracker.CreateVariable(alice, 0, \"\", nil)\n\n// Same object always serializes to the same reference\njson := tracker.ToValueJSON(alice)  // {\"obj\": 1}\n\n// Weak references — objects can be garbage collected normally\nid, ok := tracker.LookupObject(alice)\nobj := tracker.GetObject(id)\n```\n\n## Concurrency\n\nChange detection happens in a single thread. If your data structures are accessed from multiple goroutines, provide a custom Resolver implementation that implements safe access.\n\n## Documentation\n\n- [Specifications](specs/main.md) - Design principles and concepts\n- [API Reference](specs/api.md) - Complete API documentation\n- [Resolver Spec](specs/resolver.md) - Value resolver interface\n- [Wrapper Spec](specs/wrapper.md) - Wrapper support\n- [Value JSON Spec](specs/value-json.md) - Value JSON serialization format\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzot%2Fchange-tracker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzot%2Fchange-tracker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzot%2Fchange-tracker/lists"}