workfold 0.1.0a1__tar.gz
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.
- workfold-0.1.0a1/LICENSE +21 -0
- workfold-0.1.0a1/MANIFEST.in +2 -0
- workfold-0.1.0a1/PKG-INFO +204 -0
- workfold-0.1.0a1/README.md +165 -0
- workfold-0.1.0a1/THIRD_PARTY_NOTICES.md +19 -0
- workfold-0.1.0a1/pyproject.toml +107 -0
- workfold-0.1.0a1/setup.cfg +4 -0
- workfold-0.1.0a1/tests/support/__init__.py +1 -0
- workfold-0.1.0a1/tests/support/git_repo.py +130 -0
- workfold-0.1.0a1/tests/test_aggregation.py +274 -0
- workfold-0.1.0a1/tests/test_application.py +409 -0
- workfold-0.1.0a1/tests/test_cli.py +210 -0
- workfold-0.1.0a1/tests/test_config.py +323 -0
- workfold-0.1.0a1/tests/test_coverage.py +195 -0
- workfold-0.1.0a1/tests/test_end_to_end.py +439 -0
- workfold-0.1.0a1/tests/test_filesystem_collector.py +719 -0
- workfold-0.1.0a1/tests/test_filesystem_ignores.py +384 -0
- workfold-0.1.0a1/tests/test_filesystem_times.py +275 -0
- workfold-0.1.0a1/tests/test_git_changes.py +415 -0
- workfold-0.1.0a1/tests/test_git_collector.py +633 -0
- workfold-0.1.0a1/tests/test_git_objects.py +142 -0
- workfold-0.1.0a1/tests/test_git_reflogs.py +629 -0
- workfold-0.1.0a1/tests/test_git_tags.py +437 -0
- workfold-0.1.0a1/tests/test_linux_statx.py +70 -0
- workfold-0.1.0a1/tests/test_models.py +241 -0
- workfold-0.1.0a1/tests/test_package.py +14 -0
- workfold-0.1.0a1/tests/test_schedule.py +152 -0
- workfold-0.1.0a1/tests/test_terminal_renderer.py +382 -0
- workfold-0.1.0a1/tests/test_time_ranges.py +251 -0
- workfold-0.1.0a1/workfold/__init__.py +10 -0
- workfold-0.1.0a1/workfold/__main__.py +5 -0
- workfold-0.1.0a1/workfold/aggregation.py +394 -0
- workfold-0.1.0a1/workfold/application.py +1022 -0
- workfold-0.1.0a1/workfold/cli.py +204 -0
- workfold-0.1.0a1/workfold/collectors/__init__.py +54 -0
- workfold-0.1.0a1/workfold/collectors/base.py +41 -0
- workfold-0.1.0a1/workfold/collectors/filesystem.py +858 -0
- workfold-0.1.0a1/workfold/collectors/filesystem_times.py +379 -0
- workfold-0.1.0a1/workfold/collectors/git.py +788 -0
- workfold-0.1.0a1/workfold/collectors/git_changes.py +495 -0
- workfold-0.1.0a1/workfold/collectors/git_objects.py +370 -0
- workfold-0.1.0a1/workfold/collectors/git_reflogs.py +649 -0
- workfold-0.1.0a1/workfold/collectors/git_tags.py +549 -0
- workfold-0.1.0a1/workfold/collectors/ignores.py +621 -0
- workfold-0.1.0a1/workfold/collectors/linux_statx.py +145 -0
- workfold-0.1.0a1/workfold/config.py +505 -0
- workfold-0.1.0a1/workfold/coverage.py +553 -0
- workfold-0.1.0a1/workfold/models.py +363 -0
- workfold-0.1.0a1/workfold/provenance.py +187 -0
- workfold-0.1.0a1/workfold/py.typed +1 -0
- workfold-0.1.0a1/workfold/renderers/__init__.py +5 -0
- workfold-0.1.0a1/workfold/renderers/terminal.py +621 -0
- workfold-0.1.0a1/workfold/reports.py +101 -0
- workfold-0.1.0a1/workfold/sanitization.py +143 -0
- workfold-0.1.0a1/workfold/schedule.py +211 -0
- workfold-0.1.0a1/workfold/time_ranges.py +316 -0
- workfold-0.1.0a1/workfold.egg-info/PKG-INFO +204 -0
- workfold-0.1.0a1/workfold.egg-info/SOURCES.txt +60 -0
- workfold-0.1.0a1/workfold.egg-info/dependency_links.txt +1 -0
- workfold-0.1.0a1/workfold.egg-info/entry_points.txt +2 -0
- workfold-0.1.0a1/workfold.egg-info/requires.txt +18 -0
- workfold-0.1.0a1/workfold.egg-info/top_level.txt +1 -0
workfold-0.1.0a1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jan T. Müller
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: workfold
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Fold local Git and filesystem timestamp activity onto a representative week.
|
|
5
|
+
Author-email: "Jan T. Müller" <mail@jantmueller.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: homepage, https://github.com/janthmueller/workfold
|
|
8
|
+
Project-URL: documentation, https://janthmueller.github.io/workfold/
|
|
9
|
+
Project-URL: source, https://github.com/janthmueller/workfold
|
|
10
|
+
Project-URL: tracker, https://github.com/janthmueller/workfold/issues
|
|
11
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: pathspec<2,>=1
|
|
25
|
+
Requires-Dist: rich<15,>=14
|
|
26
|
+
Requires-Dist: tzdata>=2025.2; sys_platform == "win32"
|
|
27
|
+
Requires-Dist: tzlocal<6,>=5
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pyright>=1.1; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov>=6; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
33
|
+
Provides-Extra: release
|
|
34
|
+
Requires-Dist: build>=1; extra == "release"
|
|
35
|
+
Requires-Dist: pyinstaller>=6; extra == "release"
|
|
36
|
+
Requires-Dist: python-semantic-release<11,>=10; extra == "release"
|
|
37
|
+
Requires-Dist: twine>=5; extra == "release"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# Workfold
|
|
41
|
+
|
|
42
|
+
Workfold is a private, local CLI that folds timestamped activity from Git and
|
|
43
|
+
filesystem metadata onto one representative Monday-to-Sunday week. It makes the
|
|
44
|
+
shape of recorded activity easy to scan and marks events outside a configured
|
|
45
|
+
working schedule without pretending to measure hours worked.
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
Time Mon Tue Wed Thu Fri Sat Sun
|
|
49
|
+
07:48–08:12 ○●■ ●
|
|
50
|
+
⋮ 1h 8m
|
|
51
|
+
09:20–09:29 ■●
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Terminal output starts directly with this matrix, followed by a content-aware
|
|
55
|
+
symbol key and three direct statistic rows: total events, the inside/outside
|
|
56
|
+
schedule split, and the weekday/weekend calendar split. There is no summary
|
|
57
|
+
heading. The key has no heading and names only categories actually
|
|
58
|
+
visible in the matrix; its `×N` explanation appears only when a cell uses exact
|
|
59
|
+
count notation. The configured working hours follow on their own left-aligned
|
|
60
|
+
line. Schedule and calendar percentages each use the total event count as their
|
|
61
|
+
denominator; weekend activity may also be outside the configured schedule. A
|
|
62
|
+
normal complete run stops there.
|
|
63
|
+
`--verbose` adds scope, period, the full successful coverage status, operational
|
|
64
|
+
metadata, and the detailed coverage ledger. Use `--coverage` when the ledger is
|
|
65
|
+
wanted without those verbose details. Partial collection, explicit narrowing,
|
|
66
|
+
unsupported capabilities, and nonzero display cropping remain visible by
|
|
67
|
+
default as exception notices; compact output never hides those limitations.
|
|
68
|
+
|
|
69
|
+
Each ordinary symbol is one activity marker. Circles are Git and squares are
|
|
70
|
+
filesystem evidence; filled shapes are inside working hours and hollow red
|
|
71
|
+
shapes are outside. Git-inside markers are green and filesystem-inside markers
|
|
72
|
+
are blue. Shapes preserve the same meaning with `--no-color`. Rows are greedy,
|
|
73
|
+
globally aligned time clusters (one hour by default), so empty time disappears
|
|
74
|
+
without losing the order expressed by the labels.
|
|
75
|
+
|
|
76
|
+
## Status
|
|
77
|
+
|
|
78
|
+
Workfold is an alpha, terminal-only CLI with its sparse terminal renderer and
|
|
79
|
+
local collection pipeline implemented end to end. Git commits, per-file
|
|
80
|
+
changes, annotated tags, local
|
|
81
|
+
reflogs, filesystem metadata, combined-source views, the `full` profile, strict
|
|
82
|
+
coverage accounting, and outside-event listing share the same normalized
|
|
83
|
+
pipeline.
|
|
84
|
+
|
|
85
|
+
The [documentation](https://janthmueller.github.io/workfold/) covers CLI usage,
|
|
86
|
+
accuracy, and development. The Astro site in `docs/` documents the terminal
|
|
87
|
+
product; it is not an HTML reporting feature.
|
|
88
|
+
|
|
89
|
+
## Install
|
|
90
|
+
|
|
91
|
+
Workfold requires Python 3.10 or newer. Git collection uses the installed
|
|
92
|
+
`git` executable and never contacts a remote.
|
|
93
|
+
|
|
94
|
+
Install Workfold from PyPI:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pip install workfold
|
|
98
|
+
workfold --help
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
For an isolated CLI environment, `uv tool install workfold` and
|
|
102
|
+
`pipx install workfold` are supported alternatives. Standalone archives for
|
|
103
|
+
supported platforms are attached to
|
|
104
|
+
[GitHub releases](https://github.com/janthmueller/workfold/releases).
|
|
105
|
+
|
|
106
|
+
## Quick Git view
|
|
107
|
+
|
|
108
|
+
Run inside a repository to fold the current ISO week's author timestamps from
|
|
109
|
+
commits reachable through all local refs:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
workfold
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This uses the default selectors `-t this-week -m git -p standard`.
|
|
116
|
+
`-t/--time` also accepts an ISO week, repeated ISO weeks, an inclusive
|
|
117
|
+
`DATE..DATE` range with either endpoint open, or `all`. `-m/--mode` accepts
|
|
118
|
+
`git`, `fs`, or `all`; changing time never changes mode and vice versa.
|
|
119
|
+
|
|
120
|
+
Common selections include:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
workfold . -t 2026-W31
|
|
124
|
+
workfold . -t 2026-W30 -t 2026-W31
|
|
125
|
+
workfold . -t 2026-07-01..2026-07-31
|
|
126
|
+
workfold . -t 2026-01-01..
|
|
127
|
+
workfold . -t all -m git --git-commit-times author,committer --coverage
|
|
128
|
+
workfold . --hours 'Mo-Thu 08:00-16:30; Fr 08:00-14:00'
|
|
129
|
+
workfold . --cluster-window 30s
|
|
130
|
+
workfold . --cluster-window 10m
|
|
131
|
+
workfold . --cluster-window 1h5m
|
|
132
|
+
workfold . --cluster-window '1h 5m'
|
|
133
|
+
workfold . --timezone Europe/Berlin --list-outside --limit 50
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Chart band labels always use `HH:MM`, even for second-level cluster windows and
|
|
137
|
+
events. Exact seconds and nanoseconds remain in normalized provenance and the
|
|
138
|
+
outside-hours event list.
|
|
139
|
+
|
|
140
|
+
For portable Git-object evidence across all available history, use:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
workfold . -t all -m git -p portable
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The portable profile includes commit author and committer timestamps plus
|
|
147
|
+
annotated-tag tagger timestamps. It does not use filesystem metadata or local
|
|
148
|
+
reflogs, contact a remote, or claim to show when an object was published
|
|
149
|
+
remotely.
|
|
150
|
+
|
|
151
|
+
The exhaustive local command is:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
workfold . -t all -m all -p full
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`-p/--profile` defaults to `standard`. The `full` profile broadens timestamp
|
|
158
|
+
and record collection only inside the selected time and mode. It never changes
|
|
159
|
+
either selector. With `-m all`, it requests all
|
|
160
|
+
supported Git and filesystem evidence, ignored entries, and all local refs. It
|
|
161
|
+
changes collection scope only: detailed ledger output still requires
|
|
162
|
+
`--coverage` or `--verbose`. It does not parse date-like text inside files.
|
|
163
|
+
|
|
164
|
+
## Accuracy and privacy
|
|
165
|
+
|
|
166
|
+
A Workfold event is a discrete timestamp observation, not a work session. Git
|
|
167
|
+
file changes are derived from first-parent tree differences, not stored human
|
|
168
|
+
actions. Commits can be rewritten; annotated tags have independent tagger dates
|
|
169
|
+
while lightweight tags do not; and reflogs can expire. Filesystem metadata is a
|
|
170
|
+
mutable current snapshot: Linux birth time is collected through `statx` when
|
|
171
|
+
the filesystem returns it, birth time remains platform-dependent elsewhere,
|
|
172
|
+
ctime is metadata-change time, and atime may be unreliable. Workfold cannot
|
|
173
|
+
recover unsaved edit sessions, deleted untracked files, or earlier metadata
|
|
174
|
+
values.
|
|
175
|
+
|
|
176
|
+
Collection remains local. No Git hosting API, telemetry service, web server, or
|
|
177
|
+
network collector is part of the MVP. Coverage reports account for captured,
|
|
178
|
+
filtered, unavailable, unsupported, and unreadable timestamps within the exact
|
|
179
|
+
requested scope instead of claiming unqualified completeness.
|
|
180
|
+
|
|
181
|
+
## Development
|
|
182
|
+
|
|
183
|
+
The project intentionally uses a flat Python package: the implementation is in
|
|
184
|
+
`workfold/`, with no `src/` wrapper.
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
nix develop
|
|
188
|
+
uv sync --extra dev
|
|
189
|
+
uv run pytest
|
|
190
|
+
ruff check .
|
|
191
|
+
ruff format --check .
|
|
192
|
+
uv run pyright
|
|
193
|
+
uv run workfold --help
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Build the Python distributions and validate the Astro documentation with:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
uv build
|
|
200
|
+
nix run .#docs-check
|
|
201
|
+
nix run .#docs-build
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Start the documentation site locally with `nix run .#docs-dev`.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Workfold
|
|
2
|
+
|
|
3
|
+
Workfold is a private, local CLI that folds timestamped activity from Git and
|
|
4
|
+
filesystem metadata onto one representative Monday-to-Sunday week. It makes the
|
|
5
|
+
shape of recorded activity easy to scan and marks events outside a configured
|
|
6
|
+
working schedule without pretending to measure hours worked.
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
Time Mon Tue Wed Thu Fri Sat Sun
|
|
10
|
+
07:48–08:12 ○●■ ●
|
|
11
|
+
⋮ 1h 8m
|
|
12
|
+
09:20–09:29 ■●
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Terminal output starts directly with this matrix, followed by a content-aware
|
|
16
|
+
symbol key and three direct statistic rows: total events, the inside/outside
|
|
17
|
+
schedule split, and the weekday/weekend calendar split. There is no summary
|
|
18
|
+
heading. The key has no heading and names only categories actually
|
|
19
|
+
visible in the matrix; its `×N` explanation appears only when a cell uses exact
|
|
20
|
+
count notation. The configured working hours follow on their own left-aligned
|
|
21
|
+
line. Schedule and calendar percentages each use the total event count as their
|
|
22
|
+
denominator; weekend activity may also be outside the configured schedule. A
|
|
23
|
+
normal complete run stops there.
|
|
24
|
+
`--verbose` adds scope, period, the full successful coverage status, operational
|
|
25
|
+
metadata, and the detailed coverage ledger. Use `--coverage` when the ledger is
|
|
26
|
+
wanted without those verbose details. Partial collection, explicit narrowing,
|
|
27
|
+
unsupported capabilities, and nonzero display cropping remain visible by
|
|
28
|
+
default as exception notices; compact output never hides those limitations.
|
|
29
|
+
|
|
30
|
+
Each ordinary symbol is one activity marker. Circles are Git and squares are
|
|
31
|
+
filesystem evidence; filled shapes are inside working hours and hollow red
|
|
32
|
+
shapes are outside. Git-inside markers are green and filesystem-inside markers
|
|
33
|
+
are blue. Shapes preserve the same meaning with `--no-color`. Rows are greedy,
|
|
34
|
+
globally aligned time clusters (one hour by default), so empty time disappears
|
|
35
|
+
without losing the order expressed by the labels.
|
|
36
|
+
|
|
37
|
+
## Status
|
|
38
|
+
|
|
39
|
+
Workfold is an alpha, terminal-only CLI with its sparse terminal renderer and
|
|
40
|
+
local collection pipeline implemented end to end. Git commits, per-file
|
|
41
|
+
changes, annotated tags, local
|
|
42
|
+
reflogs, filesystem metadata, combined-source views, the `full` profile, strict
|
|
43
|
+
coverage accounting, and outside-event listing share the same normalized
|
|
44
|
+
pipeline.
|
|
45
|
+
|
|
46
|
+
The [documentation](https://janthmueller.github.io/workfold/) covers CLI usage,
|
|
47
|
+
accuracy, and development. The Astro site in `docs/` documents the terminal
|
|
48
|
+
product; it is not an HTML reporting feature.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
Workfold requires Python 3.10 or newer. Git collection uses the installed
|
|
53
|
+
`git` executable and never contacts a remote.
|
|
54
|
+
|
|
55
|
+
Install Workfold from PyPI:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install workfold
|
|
59
|
+
workfold --help
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For an isolated CLI environment, `uv tool install workfold` and
|
|
63
|
+
`pipx install workfold` are supported alternatives. Standalone archives for
|
|
64
|
+
supported platforms are attached to
|
|
65
|
+
[GitHub releases](https://github.com/janthmueller/workfold/releases).
|
|
66
|
+
|
|
67
|
+
## Quick Git view
|
|
68
|
+
|
|
69
|
+
Run inside a repository to fold the current ISO week's author timestamps from
|
|
70
|
+
commits reachable through all local refs:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
workfold
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This uses the default selectors `-t this-week -m git -p standard`.
|
|
77
|
+
`-t/--time` also accepts an ISO week, repeated ISO weeks, an inclusive
|
|
78
|
+
`DATE..DATE` range with either endpoint open, or `all`. `-m/--mode` accepts
|
|
79
|
+
`git`, `fs`, or `all`; changing time never changes mode and vice versa.
|
|
80
|
+
|
|
81
|
+
Common selections include:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
workfold . -t 2026-W31
|
|
85
|
+
workfold . -t 2026-W30 -t 2026-W31
|
|
86
|
+
workfold . -t 2026-07-01..2026-07-31
|
|
87
|
+
workfold . -t 2026-01-01..
|
|
88
|
+
workfold . -t all -m git --git-commit-times author,committer --coverage
|
|
89
|
+
workfold . --hours 'Mo-Thu 08:00-16:30; Fr 08:00-14:00'
|
|
90
|
+
workfold . --cluster-window 30s
|
|
91
|
+
workfold . --cluster-window 10m
|
|
92
|
+
workfold . --cluster-window 1h5m
|
|
93
|
+
workfold . --cluster-window '1h 5m'
|
|
94
|
+
workfold . --timezone Europe/Berlin --list-outside --limit 50
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Chart band labels always use `HH:MM`, even for second-level cluster windows and
|
|
98
|
+
events. Exact seconds and nanoseconds remain in normalized provenance and the
|
|
99
|
+
outside-hours event list.
|
|
100
|
+
|
|
101
|
+
For portable Git-object evidence across all available history, use:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
workfold . -t all -m git -p portable
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The portable profile includes commit author and committer timestamps plus
|
|
108
|
+
annotated-tag tagger timestamps. It does not use filesystem metadata or local
|
|
109
|
+
reflogs, contact a remote, or claim to show when an object was published
|
|
110
|
+
remotely.
|
|
111
|
+
|
|
112
|
+
The exhaustive local command is:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
workfold . -t all -m all -p full
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`-p/--profile` defaults to `standard`. The `full` profile broadens timestamp
|
|
119
|
+
and record collection only inside the selected time and mode. It never changes
|
|
120
|
+
either selector. With `-m all`, it requests all
|
|
121
|
+
supported Git and filesystem evidence, ignored entries, and all local refs. It
|
|
122
|
+
changes collection scope only: detailed ledger output still requires
|
|
123
|
+
`--coverage` or `--verbose`. It does not parse date-like text inside files.
|
|
124
|
+
|
|
125
|
+
## Accuracy and privacy
|
|
126
|
+
|
|
127
|
+
A Workfold event is a discrete timestamp observation, not a work session. Git
|
|
128
|
+
file changes are derived from first-parent tree differences, not stored human
|
|
129
|
+
actions. Commits can be rewritten; annotated tags have independent tagger dates
|
|
130
|
+
while lightweight tags do not; and reflogs can expire. Filesystem metadata is a
|
|
131
|
+
mutable current snapshot: Linux birth time is collected through `statx` when
|
|
132
|
+
the filesystem returns it, birth time remains platform-dependent elsewhere,
|
|
133
|
+
ctime is metadata-change time, and atime may be unreliable. Workfold cannot
|
|
134
|
+
recover unsaved edit sessions, deleted untracked files, or earlier metadata
|
|
135
|
+
values.
|
|
136
|
+
|
|
137
|
+
Collection remains local. No Git hosting API, telemetry service, web server, or
|
|
138
|
+
network collector is part of the MVP. Coverage reports account for captured,
|
|
139
|
+
filtered, unavailable, unsupported, and unreadable timestamps within the exact
|
|
140
|
+
requested scope instead of claiming unqualified completeness.
|
|
141
|
+
|
|
142
|
+
## Development
|
|
143
|
+
|
|
144
|
+
The project intentionally uses a flat Python package: the implementation is in
|
|
145
|
+
`workfold/`, with no `src/` wrapper.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
nix develop
|
|
149
|
+
uv sync --extra dev
|
|
150
|
+
uv run pytest
|
|
151
|
+
ruff check .
|
|
152
|
+
ruff format --check .
|
|
153
|
+
uv run pyright
|
|
154
|
+
uv run workfold --help
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Build the Python distributions and validate the Astro documentation with:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
uv build
|
|
161
|
+
nix run .#docs-check
|
|
162
|
+
nix run .#docs-build
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Start the documentation site locally with `nix run .#docs-dev`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Third-party notices for standalone Workfold bundles
|
|
2
|
+
|
|
3
|
+
The standalone archives are produced with PyInstaller and include portions of
|
|
4
|
+
the Python runtime plus Workfold's runtime dependencies. Their exact license
|
|
5
|
+
files are copied into the archive's `licenses/` directory at build time.
|
|
6
|
+
|
|
7
|
+
- Python and its standard library: Python Software Foundation License.
|
|
8
|
+
- pathspec: Mozilla Public License 2.0.
|
|
9
|
+
- Rich: MIT License.
|
|
10
|
+
- markdown-it-py and mdurl: MIT License.
|
|
11
|
+
- Pygments: BSD 2-Clause License.
|
|
12
|
+
- tzlocal: MIT License.
|
|
13
|
+
- tzdata, when bundled on Windows: licenses shipped by the Python tzdata
|
|
14
|
+
distribution and the underlying IANA time-zone database.
|
|
15
|
+
- PyInstaller bootloader: GNU General Public License with PyInstaller's
|
|
16
|
+
bootloader exception; its shipped notices are included.
|
|
17
|
+
|
|
18
|
+
This notice is informational. The complete license texts included beside the
|
|
19
|
+
binary control the corresponding components.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "workfold"
|
|
7
|
+
version = "0.1.0-alpha.1"
|
|
8
|
+
description = "Fold local Git and filesystem timestamp activity onto a representative week."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"pathspec>=1,<2",
|
|
14
|
+
"rich>=14,<15",
|
|
15
|
+
"tzdata>=2025.2; sys_platform == 'win32'",
|
|
16
|
+
"tzlocal>=5,<6",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
20
|
+
"Environment :: Console",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Programming Language :: Python :: 3.14",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[[project.authors]]
|
|
32
|
+
name = "Jan T. Müller"
|
|
33
|
+
email = "mail@jantmueller.com"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
homepage = "https://github.com/janthmueller/workfold"
|
|
37
|
+
documentation = "https://janthmueller.github.io/workfold/"
|
|
38
|
+
source = "https://github.com/janthmueller/workfold"
|
|
39
|
+
tracker = "https://github.com/janthmueller/workfold/issues"
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
workfold = "workfold.cli:main"
|
|
43
|
+
|
|
44
|
+
[project.optional-dependencies]
|
|
45
|
+
dev = [
|
|
46
|
+
"pyright>=1.1",
|
|
47
|
+
"pytest>=8",
|
|
48
|
+
"pytest-cov>=6",
|
|
49
|
+
"ruff>=0.8",
|
|
50
|
+
]
|
|
51
|
+
release = [
|
|
52
|
+
"build>=1",
|
|
53
|
+
"pyinstaller>=6",
|
|
54
|
+
"python-semantic-release>=10,<11",
|
|
55
|
+
"twine>=5",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.setuptools.packages.find]
|
|
59
|
+
where = ["."]
|
|
60
|
+
include = ["workfold*"]
|
|
61
|
+
exclude = ["tests*"]
|
|
62
|
+
|
|
63
|
+
[tool.setuptools.package-data]
|
|
64
|
+
workfold = ["py.typed"]
|
|
65
|
+
|
|
66
|
+
[tool.ruff]
|
|
67
|
+
line-length = 120
|
|
68
|
+
target-version = "py310"
|
|
69
|
+
src = ["workfold", "tests"]
|
|
70
|
+
|
|
71
|
+
[tool.ruff.lint]
|
|
72
|
+
select = ["E4", "E7", "E9", "F", "I"]
|
|
73
|
+
|
|
74
|
+
[tool.pyright]
|
|
75
|
+
pythonVersion = "3.10"
|
|
76
|
+
typeCheckingMode = "strict"
|
|
77
|
+
include = ["workfold", "tests"]
|
|
78
|
+
venvPath = "."
|
|
79
|
+
venv = ".venv"
|
|
80
|
+
|
|
81
|
+
[tool.pytest.ini_options]
|
|
82
|
+
addopts = ["-ra", "--strict-config", "--strict-markers"]
|
|
83
|
+
testpaths = ["tests"]
|
|
84
|
+
|
|
85
|
+
[tool.coverage.run]
|
|
86
|
+
branch = true
|
|
87
|
+
source = ["workfold"]
|
|
88
|
+
omit = ["workfold/__main__.py"]
|
|
89
|
+
|
|
90
|
+
[tool.coverage.report]
|
|
91
|
+
fail_under = 90
|
|
92
|
+
show_missing = true
|
|
93
|
+
|
|
94
|
+
[tool.semantic_release]
|
|
95
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
96
|
+
commit_parser = "conventional"
|
|
97
|
+
allow_zero_version = true
|
|
98
|
+
major_on_zero = false
|
|
99
|
+
commit_message = "chore(release): {version} [skip ci]\n\nAutomatically generated by python-semantic-release"
|
|
100
|
+
build_command = "python -m build"
|
|
101
|
+
|
|
102
|
+
[tool.semantic_release.branches.main]
|
|
103
|
+
match = "main"
|
|
104
|
+
prerelease = false
|
|
105
|
+
|
|
106
|
+
[tool.semantic_release.publish]
|
|
107
|
+
upload_to_vcs_release = true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Deterministic test support for Workfold."""
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Isolated local Git repositories with exact timestamp fixtures."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Mapping
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True)
|
|
13
|
+
class GitRepo:
|
|
14
|
+
"""Small plumbing-based repository fixture independent of user config."""
|
|
15
|
+
|
|
16
|
+
path: Path
|
|
17
|
+
branch: str = "main"
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def create(cls, path: Path) -> GitRepo:
|
|
21
|
+
path.mkdir(parents=True)
|
|
22
|
+
repo = cls(path=path)
|
|
23
|
+
repo.run("init", "--initial-branch=main")
|
|
24
|
+
return repo
|
|
25
|
+
|
|
26
|
+
def _environment(self, additions: Mapping[str, str] | None = None) -> dict[str, str]:
|
|
27
|
+
environment = dict(os.environ)
|
|
28
|
+
environment.update(
|
|
29
|
+
{
|
|
30
|
+
"GIT_CONFIG_GLOBAL": os.devnull,
|
|
31
|
+
"GIT_CONFIG_NOSYSTEM": "1",
|
|
32
|
+
"GIT_TERMINAL_PROMPT": "0",
|
|
33
|
+
"LANG": "C",
|
|
34
|
+
"LC_ALL": "C",
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
if additions is not None:
|
|
38
|
+
environment.update(additions)
|
|
39
|
+
return environment
|
|
40
|
+
|
|
41
|
+
def run(
|
|
42
|
+
self,
|
|
43
|
+
*arguments: str,
|
|
44
|
+
input_data: bytes | None = None,
|
|
45
|
+
environment: Mapping[str, str] | None = None,
|
|
46
|
+
) -> bytes:
|
|
47
|
+
completed = subprocess.run(
|
|
48
|
+
(
|
|
49
|
+
"git",
|
|
50
|
+
"--no-pager",
|
|
51
|
+
"-c",
|
|
52
|
+
"commit.gpgSign=false",
|
|
53
|
+
"-c",
|
|
54
|
+
"core.hooksPath=/dev/null",
|
|
55
|
+
*arguments,
|
|
56
|
+
),
|
|
57
|
+
cwd=self.path,
|
|
58
|
+
env=self._environment(environment),
|
|
59
|
+
input=input_data,
|
|
60
|
+
stdout=subprocess.PIPE,
|
|
61
|
+
stderr=subprocess.PIPE,
|
|
62
|
+
shell=False,
|
|
63
|
+
check=False,
|
|
64
|
+
)
|
|
65
|
+
if completed.returncode != 0:
|
|
66
|
+
stderr = completed.stderr.decode("utf-8", errors="replace")
|
|
67
|
+
raise AssertionError(f"fixture Git command failed ({completed.returncode}): {stderr}")
|
|
68
|
+
return completed.stdout
|
|
69
|
+
|
|
70
|
+
def commit(
|
|
71
|
+
self,
|
|
72
|
+
filename: str,
|
|
73
|
+
content: str,
|
|
74
|
+
subject: str,
|
|
75
|
+
*,
|
|
76
|
+
author_date: str,
|
|
77
|
+
committer_date: str,
|
|
78
|
+
author_name: str = "Fixture Author",
|
|
79
|
+
author_email: str = "author@example.test",
|
|
80
|
+
committer_name: str = "Fixture Committer",
|
|
81
|
+
committer_email: str = "committer@example.test",
|
|
82
|
+
parent: str | None = None,
|
|
83
|
+
update_ref: str | None = None,
|
|
84
|
+
) -> str:
|
|
85
|
+
target = self.path / filename
|
|
86
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
target.write_text(content, encoding="utf-8")
|
|
88
|
+
self.run("add", "--", filename)
|
|
89
|
+
tree = self.run("write-tree").decode("ascii").strip()
|
|
90
|
+
if parent is None:
|
|
91
|
+
parent_result = subprocess.run(
|
|
92
|
+
("git", "rev-parse", "--verify", "HEAD^{commit}"),
|
|
93
|
+
cwd=self.path,
|
|
94
|
+
env=self._environment(),
|
|
95
|
+
stdout=subprocess.PIPE,
|
|
96
|
+
stderr=subprocess.DEVNULL,
|
|
97
|
+
shell=False,
|
|
98
|
+
check=False,
|
|
99
|
+
)
|
|
100
|
+
parent = parent_result.stdout.decode("ascii").strip() if parent_result.returncode == 0 else None
|
|
101
|
+
|
|
102
|
+
arguments = ["commit-tree", tree]
|
|
103
|
+
if parent is not None:
|
|
104
|
+
arguments.extend(("-p", parent))
|
|
105
|
+
identity_environment = {
|
|
106
|
+
"GIT_AUTHOR_DATE": author_date,
|
|
107
|
+
"GIT_AUTHOR_EMAIL": author_email,
|
|
108
|
+
"GIT_AUTHOR_NAME": author_name,
|
|
109
|
+
"GIT_COMMITTER_DATE": committer_date,
|
|
110
|
+
"GIT_COMMITTER_EMAIL": committer_email,
|
|
111
|
+
"GIT_COMMITTER_NAME": committer_name,
|
|
112
|
+
}
|
|
113
|
+
commit_id = (
|
|
114
|
+
self.run(
|
|
115
|
+
*arguments,
|
|
116
|
+
input_data=subject.encode("utf-8") + b"\n",
|
|
117
|
+
environment=identity_environment,
|
|
118
|
+
)
|
|
119
|
+
.decode("ascii")
|
|
120
|
+
.strip()
|
|
121
|
+
)
|
|
122
|
+
ref_name = update_ref or f"refs/heads/{self.branch}"
|
|
123
|
+
self.run("update-ref", ref_name, commit_id)
|
|
124
|
+
return commit_id
|
|
125
|
+
|
|
126
|
+
def point_ref(self, ref_name: str, object_id: str) -> None:
|
|
127
|
+
self.run("update-ref", ref_name, object_id)
|
|
128
|
+
|
|
129
|
+
def detach(self, object_id: str) -> None:
|
|
130
|
+
self.run("update-ref", "--no-deref", "HEAD", object_id)
|