chronocratic-models 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.
Files changed (158) hide show
  1. chronocratic_models-0.1.0a1/.github/workflows/auto-changelog-fragment.yml +128 -0
  2. chronocratic_models-0.1.0a1/.github/workflows/build-and-test.yml +80 -0
  3. chronocratic_models-0.1.0a1/.github/workflows/ff-merge-check.yml +46 -0
  4. chronocratic_models-0.1.0a1/.github/workflows/ff-merge-do.yml +98 -0
  5. chronocratic_models-0.1.0a1/.github/workflows/main-pre-merge-gate.yml +61 -0
  6. chronocratic_models-0.1.0a1/.github/workflows/pypi-publish.yml +30 -0
  7. chronocratic_models-0.1.0a1/.github/workflows/release-notes.yml +44 -0
  8. chronocratic_models-0.1.0a1/.github/workflows/release-prep.yml +83 -0
  9. chronocratic_models-0.1.0a1/.gitignore +232 -0
  10. chronocratic_models-0.1.0a1/.readthedocs.yaml +16 -0
  11. chronocratic_models-0.1.0a1/.vscode/settings.json +22 -0
  12. chronocratic_models-0.1.0a1/CHANGELOG.md +47 -0
  13. chronocratic_models-0.1.0a1/LICENSE +29 -0
  14. chronocratic_models-0.1.0a1/PKG-INFO +125 -0
  15. chronocratic_models-0.1.0a1/README.md +90 -0
  16. chronocratic_models-0.1.0a1/changelog.d/21.changed.md +1 -0
  17. chronocratic_models-0.1.0a1/changelog.d/README.md +43 -0
  18. chronocratic_models-0.1.0a1/docs/_static/custom.css +1 -0
  19. chronocratic_models-0.1.0a1/docs/api/augmentation.md +53 -0
  20. chronocratic_models-0.1.0a1/docs/api/conv_dilated.md +46 -0
  21. chronocratic_models-0.1.0a1/docs/api/conv_standard.md +45 -0
  22. chronocratic_models-0.1.0a1/docs/api/distances.md +13 -0
  23. chronocratic_models-0.1.0a1/docs/api/generative.md +21 -0
  24. chronocratic_models-0.1.0a1/docs/api/index.md +27 -0
  25. chronocratic_models-0.1.0a1/docs/api/layers.md +9 -0
  26. chronocratic_models-0.1.0a1/docs/api/mixins.md +23 -0
  27. chronocratic_models-0.1.0a1/docs/api/recurrent.md +17 -0
  28. chronocratic_models-0.1.0a1/docs/api/supervised.md +23 -0
  29. chronocratic_models-0.1.0a1/docs/api/transformer.md +17 -0
  30. chronocratic_models-0.1.0a1/docs/changelog.md +4 -0
  31. chronocratic_models-0.1.0a1/docs/conf.py +71 -0
  32. chronocratic_models-0.1.0a1/docs/contributing.md +86 -0
  33. chronocratic_models-0.1.0a1/docs/index.md +52 -0
  34. chronocratic_models-0.1.0a1/docs/quickstart.md +56 -0
  35. chronocratic_models-0.1.0a1/pyproject.toml +122 -0
  36. chronocratic_models-0.1.0a1/ruff.toml +53 -0
  37. chronocratic_models-0.1.0a1/setup.cfg +4 -0
  38. chronocratic_models-0.1.0a1/src/chronocratic/models/__init__.py +48 -0
  39. chronocratic_models-0.1.0a1/src/chronocratic/models/_mixin/__init__.py +3 -0
  40. chronocratic_models-0.1.0a1/src/chronocratic/models/_mixin/encoding.py +147 -0
  41. chronocratic_models-0.1.0a1/src/chronocratic/models/_version.py +24 -0
  42. chronocratic_models-0.1.0a1/src/chronocratic/models/augmentation/__init__.py +70 -0
  43. chronocratic_models-0.1.0a1/src/chronocratic/models/augmentation/base.py +328 -0
  44. chronocratic_models-0.1.0a1/src/chronocratic/models/augmentation/decorators.py +65 -0
  45. chronocratic_models-0.1.0a1/src/chronocratic/models/augmentation/primitives.py +251 -0
  46. chronocratic_models-0.1.0a1/src/chronocratic/models/augmentation/producers.py +164 -0
  47. chronocratic_models-0.1.0a1/src/chronocratic/models/augmentation/trainable_support.py +99 -0
  48. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/__init__.py +35 -0
  49. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/__init__.py +16 -0
  50. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/_mixin/__init__.py +7 -0
  51. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/_mixin/encoding.py +379 -0
  52. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/autotcl/__init__.py +4 -0
  53. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/autotcl/augmentation/__init__.py +20 -0
  54. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/autotcl/augmentation/methods.py +196 -0
  55. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/autotcl/augmentation/training.py +128 -0
  56. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/autotcl/config.py +51 -0
  57. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/autotcl/losses.py +332 -0
  58. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/autotcl/model.py +247 -0
  59. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/autotcl/utils.py +93 -0
  60. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/cost/__init__.py +4 -0
  61. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/cost/augmentation.py +148 -0
  62. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/cost/config.py +52 -0
  63. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/cost/model.py +331 -0
  64. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/cost/utils.py +50 -0
  65. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/encoders/__init__.py +13 -0
  66. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/encoders/encoders.py +639 -0
  67. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/encoders/masking.py +192 -0
  68. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/layers/__init__.py +7 -0
  69. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/layers/dilated.py +50 -0
  70. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/layers/same_pad.py +131 -0
  71. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/ts2vec/__init__.py +4 -0
  72. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/ts2vec/augmentation.py +126 -0
  73. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/ts2vec/config.py +45 -0
  74. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/ts2vec/model.py +163 -0
  75. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/dilated/ts2vec/utils.py +23 -0
  76. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/__init__.py +16 -0
  77. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/mcl/__init__.py +4 -0
  78. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/mcl/config.py +27 -0
  79. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/mcl/encoder.py +34 -0
  80. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/mcl/losses.py +40 -0
  81. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/mcl/model.py +85 -0
  82. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/series2vec/__init__.py +4 -0
  83. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/series2vec/config.py +54 -0
  84. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/series2vec/encoder.py +44 -0
  85. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/series2vec/filters.py +63 -0
  86. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/series2vec/losses.py +79 -0
  87. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/series2vec/model.py +162 -0
  88. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/series2vec/network.py +134 -0
  89. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/tstcc/__init__.py +4 -0
  90. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/tstcc/augmentations.py +61 -0
  91. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/tstcc/config.py +59 -0
  92. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/tstcc/encoder.py +68 -0
  93. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/tstcc/losses.py +58 -0
  94. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/tstcc/model.py +205 -0
  95. chronocratic_models-0.1.0a1/src/chronocratic/models/convolutional/standard/tstcc/temporal_contrast.py +188 -0
  96. chronocratic_models-0.1.0a1/src/chronocratic/models/distances/__init__.py +3 -0
  97. chronocratic_models-0.1.0a1/src/chronocratic/models/distances/soft_dtw/__init__.py +3 -0
  98. chronocratic_models-0.1.0a1/src/chronocratic/models/distances/soft_dtw/soft_dtw_cuda.py +453 -0
  99. chronocratic_models-0.1.0a1/src/chronocratic/models/generative/__init__.py +7 -0
  100. chronocratic_models-0.1.0a1/src/chronocratic/models/generative/timevae/__init__.py +4 -0
  101. chronocratic_models-0.1.0a1/src/chronocratic/models/generative/timevae/config.py +44 -0
  102. chronocratic_models-0.1.0a1/src/chronocratic/models/generative/timevae/model.py +191 -0
  103. chronocratic_models-0.1.0a1/src/chronocratic/models/generative/timevae/vae_base.py +137 -0
  104. chronocratic_models-0.1.0a1/src/chronocratic/models/layers/__init__.py +3 -0
  105. chronocratic_models-0.1.0a1/src/chronocratic/models/layers/general.py +249 -0
  106. chronocratic_models-0.1.0a1/src/chronocratic/models/losses/__init__.py +11 -0
  107. chronocratic_models-0.1.0a1/src/chronocratic/models/losses/contrastive.py +144 -0
  108. chronocratic_models-0.1.0a1/src/chronocratic/models/recurrent/__init__.py +7 -0
  109. chronocratic_models-0.1.0a1/src/chronocratic/models/recurrent/timenet/__init__.py +4 -0
  110. chronocratic_models-0.1.0a1/src/chronocratic/models/recurrent/timenet/config.py +31 -0
  111. chronocratic_models-0.1.0a1/src/chronocratic/models/recurrent/timenet/model.py +115 -0
  112. chronocratic_models-0.1.0a1/src/chronocratic/models/supervised/__init__.py +62 -0
  113. chronocratic_models-0.1.0a1/src/chronocratic/models/supervised/_adapters.py +102 -0
  114. chronocratic_models-0.1.0a1/src/chronocratic/models/supervised/_callbacks.py +78 -0
  115. chronocratic_models-0.1.0a1/src/chronocratic/models/supervised/_utils.py +43 -0
  116. chronocratic_models-0.1.0a1/src/chronocratic/models/supervised/factory.py +188 -0
  117. chronocratic_models-0.1.0a1/src/chronocratic/models/supervised/supervised.py +214 -0
  118. chronocratic_models-0.1.0a1/src/chronocratic/models/transformer/__init__.py +7 -0
  119. chronocratic_models-0.1.0a1/src/chronocratic/models/transformer/tst/__init__.py +4 -0
  120. chronocratic_models-0.1.0a1/src/chronocratic/models/transformer/tst/config.py +67 -0
  121. chronocratic_models-0.1.0a1/src/chronocratic/models/transformer/tst/loss.py +40 -0
  122. chronocratic_models-0.1.0a1/src/chronocratic/models/transformer/tst/model.py +223 -0
  123. chronocratic_models-0.1.0a1/src/chronocratic/models/transformer/tst/ts_transformer.py +303 -0
  124. chronocratic_models-0.1.0a1/src/chronocratic/models/utils.py +219 -0
  125. chronocratic_models-0.1.0a1/src/chronocratic_models.egg-info/PKG-INFO +125 -0
  126. chronocratic_models-0.1.0a1/src/chronocratic_models.egg-info/SOURCES.txt +156 -0
  127. chronocratic_models-0.1.0a1/src/chronocratic_models.egg-info/dependency_links.txt +1 -0
  128. chronocratic_models-0.1.0a1/src/chronocratic_models.egg-info/requires.txt +12 -0
  129. chronocratic_models-0.1.0a1/src/chronocratic_models.egg-info/top_level.txt +1 -0
  130. chronocratic_models-0.1.0a1/tests/conftest.py +132 -0
  131. chronocratic_models-0.1.0a1/tests/integration/__init__.py +0 -0
  132. chronocratic_models-0.1.0a1/tests/integration/test_supervised_integration.py +317 -0
  133. chronocratic_models-0.1.0a1/tests/test_aug_config.py +203 -0
  134. chronocratic_models-0.1.0a1/tests/test_aug_contract.py +277 -0
  135. chronocratic_models-0.1.0a1/tests/test_aug_covariance.py +86 -0
  136. chronocratic_models-0.1.0a1/tests/test_aug_cross_model.py +228 -0
  137. chronocratic_models-0.1.0a1/tests/test_aug_decorators.py +169 -0
  138. chronocratic_models-0.1.0a1/tests/test_aug_primitives.py +158 -0
  139. chronocratic_models-0.1.0a1/tests/test_aug_producers.py +187 -0
  140. chronocratic_models-0.1.0a1/tests/test_aug_trainable_support.py +204 -0
  141. chronocratic_models-0.1.0a1/tests/test_augmentation.py +331 -0
  142. chronocratic_models-0.1.0a1/tests/test_augmentation_base.py +142 -0
  143. chronocratic_models-0.1.0a1/tests/test_augmentation_per_model.py +417 -0
  144. chronocratic_models-0.1.0a1/tests/test_autotcl_producer.py +200 -0
  145. chronocratic_models-0.1.0a1/tests/test_config.py +261 -0
  146. chronocratic_models-0.1.0a1/tests/test_config_hierarchy.py +169 -0
  147. chronocratic_models-0.1.0a1/tests/test_cost_producer.py +119 -0
  148. chronocratic_models-0.1.0a1/tests/test_from_config.py +258 -0
  149. chronocratic_models-0.1.0a1/tests/test_mixin.py +322 -0
  150. chronocratic_models-0.1.0a1/tests/test_smoke.py +240 -0
  151. chronocratic_models-0.1.0a1/tests/test_ts2vec_producer.py +165 -0
  152. chronocratic_models-0.1.0a1/tests/test_tstcc_producer.py +167 -0
  153. chronocratic_models-0.1.0a1/tests/unit/test_backbone_representation_dim.py +154 -0
  154. chronocratic_models-0.1.0a1/tests/unit/test_series2vec_supervised.py +91 -0
  155. chronocratic_models-0.1.0a1/tests/unit/test_supervised_package.py +454 -0
  156. chronocratic_models-0.1.0a1/tests/unit/test_tst_supervised.py +85 -0
  157. chronocratic_models-0.1.0a1/tests/unit/test_tstcc_supervised.py +149 -0
  158. chronocratic_models-0.1.0a1/uv.lock +3050 -0
@@ -0,0 +1,128 @@
1
+ name: Changelog fragment
2
+
3
+ # Single source of truth for changelog fragments on PRs targeting `dev`:
4
+ # 1. Same-repo PR with no fragment -> create one from the PR title and push it.
5
+ # 2. Always (incl. fork PRs) -> `towncrier check` so every PR has a fragment.
6
+ #
7
+ # Both happen in one job so the check sees the freshly-created fragment in the
8
+ # same run. A push made with GITHUB_TOKEN does NOT re-trigger workflows, so the
9
+ # check must not live in a separate workflow that never re-runs after the push.
10
+ #
11
+ # Fork PRs cannot be pushed to, so they are only checked — the contributor must
12
+ # add a fragment by hand (or a maintainer pushes one to the PR branch).
13
+ #
14
+ # Label convention for fragment type (defaults to `changed`):
15
+ # changelog:added | changelog:fixed | changelog:changed
16
+ # changelog:removed | changelog:deprecated | changelog:security
17
+ # `skip-changelog` opts the PR out entirely.
18
+
19
+ on:
20
+ pull_request:
21
+ branches: [dev]
22
+ types: [opened, edited, synchronize, labeled, unlabeled]
23
+
24
+ permissions:
25
+ contents: write
26
+
27
+ jobs:
28
+ fragment:
29
+ name: Changelog fragment
30
+ runs-on: ubuntu-latest
31
+ # Skip release-prep PRs two ways: the `skip-changelog` label (may be missing
32
+ # if the PR was opened by hand) AND the `release-prep/*` head branch, which is
33
+ # always present in the event payload regardless of labelling.
34
+ if: >-
35
+ !contains(github.event.pull_request.labels.*.name, 'skip-changelog') &&
36
+ !startsWith(github.event.pull_request.head.ref, 'release-prep/')
37
+ steps:
38
+ - uses: actions/checkout@v6
39
+ with:
40
+ # For same-repo PRs check out the head branch so we can commit + push
41
+ # a fragment. For forks this ref is still readable for the check step.
42
+ ref: ${{ github.event.pull_request.head.ref }}
43
+ repository: ${{ github.event.pull_request.head.repo.full_name }}
44
+ token: ${{ secrets.GITHUB_TOKEN }}
45
+ fetch-depth: 0
46
+
47
+ - name: Set up Python
48
+ uses: actions/setup-python@v6
49
+ with:
50
+ python-version: "3.12"
51
+
52
+ - name: Install towncrier
53
+ run: pip install towncrier
54
+
55
+ - name: Check if a fragment already exists
56
+ id: check
57
+ env:
58
+ PR: ${{ github.event.pull_request.number }}
59
+ run: |
60
+ if compgen -G "changelog.d/${PR}.*.md" > /dev/null 2>&1; then
61
+ echo "exists=true" >> "$GITHUB_OUTPUT"
62
+ else
63
+ echo "exists=false" >> "$GITHUB_OUTPUT"
64
+ fi
65
+
66
+ # A `changelog:<type>` label wins; otherwise infer from the Conventional
67
+ # Commits prefix of the PR title; otherwise fall back to `changed`.
68
+ - name: Resolve fragment type
69
+ if: steps.check.outputs.exists == 'false'
70
+ id: type
71
+ env:
72
+ LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
73
+ PR_TITLE: ${{ github.event.pull_request.title }}
74
+ run: |
75
+ TYPE=""
76
+ if echo "$LABELS" | grep -q '"changelog:added"'; then TYPE="added"
77
+ elif echo "$LABELS" | grep -q '"changelog:fixed"'; then TYPE="fixed"
78
+ elif echo "$LABELS" | grep -q '"changelog:changed"'; then TYPE="changed"
79
+ elif echo "$LABELS" | grep -q '"changelog:removed"'; then TYPE="removed"
80
+ elif echo "$LABELS" | grep -q '"changelog:deprecated"'; then TYPE="deprecated"
81
+ elif echo "$LABELS" | grep -q '"changelog:security"'; then TYPE="security"
82
+ fi
83
+
84
+ if [ -z "$TYPE" ]; then
85
+ # Lowercase Conventional Commits verb before the first ( or : or !
86
+ PREFIX="$(printf '%s' "$PR_TITLE" | sed -E 's/^([a-zA-Z]+).*/\1/' | tr '[:upper:]' '[:lower:]')"
87
+ case "$PREFIX" in
88
+ fix) TYPE="fixed" ;;
89
+ feat) TYPE="added" ;;
90
+ revert|remove) TYPE="removed" ;;
91
+ deprecate) TYPE="deprecated" ;;
92
+ security) TYPE="security" ;;
93
+ refactor|perf|change) TYPE="changed" ;;
94
+ *) TYPE="changed" ;;
95
+ esac
96
+ fi
97
+
98
+ echo "type=$TYPE" >> "$GITHUB_OUTPUT"
99
+
100
+ # Only same-repo PRs can be pushed to. Forks fall through to the check
101
+ # step, which fails until the contributor adds a fragment by hand.
102
+ - name: Create and commit fragment
103
+ if: >-
104
+ steps.check.outputs.exists == 'false' &&
105
+ github.event.pull_request.head.repo.full_name == github.repository
106
+ env:
107
+ PR_TITLE: ${{ github.event.pull_request.title }}
108
+ PR_NUMBER: ${{ github.event.pull_request.number }}
109
+ FRAGMENT_TYPE: ${{ steps.type.outputs.type }}
110
+ run: |
111
+ # Strip a leading Conventional Commits prefix (e.g. "ci(changelog): ")
112
+ # so the changelog reads as user-facing prose, then capitalise and
113
+ # ensure a single trailing period.
114
+ BODY="$(printf '%s' "${PR_TITLE}" | sed -E 's/^[a-z]+(\([^)]*\))?!?:[[:space:]]*//')"
115
+ BODY="$(printf '%s' "${BODY}" | awk '{ print toupper(substr($0,1,1)) substr($0,2) }')"
116
+ BODY="${BODY%.}."
117
+
118
+ FRAGMENT="changelog.d/${PR_NUMBER}.${FRAGMENT_TYPE}.md"
119
+ printf '%s\n' "${BODY}" > "${FRAGMENT}"
120
+
121
+ git config user.name "github-actions[bot]"
122
+ git config user.email "github-actions[bot]@users.noreply.github.com"
123
+ git add "${FRAGMENT}"
124
+ git commit -m "ci: add towncrier fragment for PR #${PR_NUMBER}"
125
+ git push
126
+
127
+ - name: Verify a fragment is present
128
+ run: towncrier check --compare-with origin/${{ github.base_ref }}
@@ -0,0 +1,80 @@
1
+ name: Build and Test
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main, dev]
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v6
12
+ with:
13
+ fetch-depth: 0
14
+ fetch-tags: true
15
+ - name: Set up Python
16
+ uses: actions/setup-python@v6
17
+ with:
18
+ python-version: "3.12"
19
+ - name: Install uv
20
+ run: pip install uv
21
+ - name: Install dependencies
22
+ run: uv sync --all-extras
23
+ - name: Run tests
24
+ run: uv run pytest tests/ --cov=src/chronocratic/models --cov-report=xml
25
+
26
+ lint:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v6
30
+ with:
31
+ fetch-depth: 0
32
+ fetch-tags: true
33
+ - name: Set up Python
34
+ uses: actions/setup-python@v6
35
+ with:
36
+ python-version: "3.12"
37
+ - name: Install ruff
38
+ run: pip install ruff
39
+ - name: Run ruff check
40
+ run: ruff check src/
41
+ - name: Run ruff format check
42
+ run: ruff format --check src/ tests/
43
+
44
+ build:
45
+ runs-on: ubuntu-latest
46
+ needs: [test, lint]
47
+ steps:
48
+ - uses: actions/checkout@v6
49
+ with:
50
+ fetch-depth: 0
51
+ fetch-tags: true
52
+ - name: Set up Python
53
+ uses: actions/setup-python@v6
54
+ with:
55
+ python-version: "3.12"
56
+ - name: Install build
57
+ run: pip install build
58
+ - name: Build package
59
+ run: python -m build
60
+ - name: Validate with twine
61
+ run: pip install twine && twine check dist/*
62
+
63
+ docs:
64
+ runs-on: ubuntu-latest
65
+ needs: [test, lint]
66
+ steps:
67
+ - uses: actions/checkout@v6
68
+ with:
69
+ fetch-depth: 0
70
+ fetch-tags: true
71
+ - name: Set up Python
72
+ uses: actions/setup-python@v6
73
+ with:
74
+ python-version: "3.12"
75
+ - name: Install uv
76
+ run: pip install uv
77
+ - name: Install dependencies
78
+ run: uv sync --extra docs
79
+ - name: Build docs
80
+ run: uv run sphinx-build -b html docs/ docs/_build/
@@ -0,0 +1,46 @@
1
+ name: Check Fast-Forward
2
+
3
+ on:
4
+ issue_comment:
5
+ types: [created]
6
+
7
+ jobs:
8
+ check-fast-forward:
9
+ if: ${{ contains(github.event.comment.body, '/check-fast-forward')
10
+ && github.event.issue.pull_request }}
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: read
14
+ pull-requests: write
15
+ issues: write
16
+ steps:
17
+ - name: Verify target branch
18
+ uses: actions/github-script@v9
19
+ with:
20
+ script: |
21
+ const { data: pr } = await github.rest.pulls.get({
22
+ owner: context.repo.owner,
23
+ repo: context.repo.repo,
24
+ pull_number: context.issue.number
25
+ });
26
+ if (pr.base.ref !== 'main' || pr.head.ref !== 'dev') {
27
+ await github.rest.issues.createComment({
28
+ owner: context.repo.owner,
29
+ repo: context.repo.repo,
30
+ issue_number: context.issue.number,
31
+ body: `❌ \`/check-fast-forward\` only works on \`dev → main\` PRs `
32
+ + `(this PR: \`${pr.head.ref} → ${pr.base.ref}\`).`
33
+ });
34
+ core.setFailed('Fast-forward check is only allowed for dev → main PRs.');
35
+ }
36
+
37
+ - name: Checkout repo
38
+ uses: actions/checkout@v6
39
+ with:
40
+ fetch-depth: 0
41
+
42
+ - name: Check if fast-forward is possible
43
+ uses: sequoia-pgp/fast-forward@v1
44
+ with:
45
+ merge: false
46
+ comment: always
@@ -0,0 +1,98 @@
1
+ name: Fast Forward Merge
2
+
3
+ on:
4
+ issue_comment:
5
+ types: [created]
6
+
7
+ concurrency:
8
+ group: fast-forward-${{ github.event.issue.number }}
9
+ cancel-in-progress: false
10
+
11
+ jobs:
12
+ fast-forward:
13
+ if: ${{ contains(github.event.comment.body, '/fast-forward')
14
+ && github.event.issue.pull_request }}
15
+ runs-on: ubuntu-latest
16
+ # The FF_MERGE_TOKEN secret lives in the `ff_merge` environment. It is a PAT
17
+ # of a ruleset bypass actor — the GITHUB_TOKEN (github-actions[bot]) is not on
18
+ # the main ruleset bypass list, so its push to main is rejected.
19
+ environment: ff_merge
20
+ permissions:
21
+ contents: write
22
+ pull-requests: write
23
+ issues: write
24
+ steps:
25
+ - name: Checkout repo
26
+ uses: actions/checkout@v6
27
+ with:
28
+ fetch-depth: 0
29
+ token: ${{ secrets.FF_MERGE_TOKEN }}
30
+
31
+ - name: Configure git
32
+ run: |
33
+ git config user.name "github-actions[bot]"
34
+ git config user.email "github-actions[bot]@users.noreply.github.com"
35
+
36
+ - name: Verify PR and actor permissions
37
+ uses: actions/github-script@v9
38
+ with:
39
+ script: |
40
+ const { data: pr } = await github.rest.pulls.get({
41
+ owner: context.repo.owner,
42
+ repo: context.repo.repo,
43
+ pull_number: context.issue.number
44
+ });
45
+ if (pr.base.ref !== 'main') {
46
+ core.setFailed('Fast-forward is only allowed for PRs targeting main.');
47
+ }
48
+ if (pr.head.ref !== 'dev') {
49
+ core.setFailed('Fast-forward is only allowed for PRs from dev to main.');
50
+ }
51
+ const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
52
+ owner: context.repo.owner,
53
+ repo: context.repo.repo,
54
+ username: context.actor
55
+ });
56
+ if (!['admin', 'maintain'].includes(perm.permission)) {
57
+ core.setFailed(`@${context.actor} (role: ${perm.permission}) — only admin/maintainer can fast-forward.`);
58
+ }
59
+
60
+ # Disabled while repo is a personal fork with no org reviewers — a
61
+ # solo maintainer cannot approve their own PR. Re-enable once moved
62
+ # to an org with a second reviewer (or branch-protection approval rule).
63
+ # - name: Check PR is approved
64
+ # uses: actions/github-script@v9
65
+ # with:
66
+ # script: |
67
+ # const { data: reviews } = await github.rest.pulls.listReviews({
68
+ # owner: context.repo.owner,
69
+ # repo: context.repo.repo,
70
+ # pull_number: context.issue.number
71
+ # });
72
+ # const approved = reviews.filter(r => r.state === 'APPROVED');
73
+ # if (approved.length === 0) {
74
+ # core.setFailed('PR must have at least one approval before fast-forward merge.');
75
+ # }
76
+
77
+ - name: Backup main before merge
78
+ run: |
79
+ BACKUP="main.backup.${{ github.event.issue.number }}.$(date +%Y%m%d-%H%M%S)"
80
+ git branch "$BACKUP" origin/main
81
+ git push origin "$BACKUP"
82
+ echo "Created backup: $BACKUP"
83
+
84
+ - name: Fast-forward merge
85
+ uses: sequoia-pgp/fast-forward@v1
86
+ with:
87
+ merge: true
88
+ comment: always
89
+ github_token: ${{ secrets.FF_MERGE_TOKEN }}
90
+
91
+ - name: Clean up old backups
92
+ run: |
93
+ git fetch --prune origin
94
+ # Sort by the trailing YYYYMMDD-HHMMSS timestamp (4th dot field) so
95
+ # retention is chronological regardless of PR-number digit width.
96
+ git branch -r | grep 'origin/main.backup.' | sed 's| *origin/||' | \
97
+ sort -t. -k4 | head -n -5 | \
98
+ xargs -I {} git push origin --delete {} || true
@@ -0,0 +1,61 @@
1
+ name: Main Pre-merge Gate
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+ types: [opened, synchronize, reopened]
7
+
8
+ concurrency:
9
+ group: pre-merge-gate-${{ github.event.pull_request.number }}
10
+ cancel-in-progress: true
11
+
12
+ jobs:
13
+ forbidden-files:
14
+ name: Check forbidden paths
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - name: Reject non-dev PRs
18
+ if: ${{ github.head_ref != 'dev' }}
19
+ run: |
20
+ echo "::error::PRs from '${{ github.head_ref }}' to main are not allowed. Only dev→main merges are permitted."
21
+ exit 1
22
+
23
+ - name: Checkout
24
+ uses: actions/checkout@v6
25
+ with:
26
+ fetch-depth: 0
27
+
28
+ - name: Scan PR for forbidden files
29
+ run: |
30
+ FORBIDDEN=(
31
+ ".claude/"
32
+ ".remember/"
33
+ ".planning/"
34
+ "MEMORY.md"
35
+ "CLAUDE.md"
36
+ "CONTEXT.md"
37
+ "STATE.md"
38
+ "ROADMAP.md"
39
+ ".continue-here.md"
40
+ "HANDOFF.json"
41
+ )
42
+
43
+ CHANGED=$(git diff --diff-filter=ACMR --name-only ${{ github.event.pull_request.base.sha }}..HEAD)
44
+ MATCHED=()
45
+
46
+ for path in "${FORBIDDEN[@]}"; do
47
+ # Anchor to a path boundary (start of path or after a '/') so e.g.
48
+ # CLAUDE.md does not match NOTCLAUDE.md.
49
+ while IFS= read -r f; do
50
+ [ -n "$f" ] && MATCHED+=("$f")
51
+ done < <(printf '%s\n' "$CHANGED" | grep -E "(^|/)$path" || true)
52
+ done
53
+
54
+ if [ "${#MATCHED[@]}" -gt 0 ]; then
55
+ echo "::error::Forbidden files detected:"
56
+ for f in "${MATCHED[@]}"; do
57
+ echo "::error:: - $f"
58
+ done
59
+ exit 1
60
+ fi
61
+ echo "All clear."
@@ -0,0 +1,30 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ pypi-publish:
9
+ name: Upload release to PyPI
10
+ runs-on: ubuntu-latest
11
+ environment:
12
+ name: pypi
13
+ url: https://pypi.org/p/chronocratic-models
14
+ permissions:
15
+ id-token: write
16
+ steps:
17
+ - uses: actions/checkout@v6
18
+ with:
19
+ fetch-depth: 0
20
+ fetch-tags: true
21
+ - name: Set up Python
22
+ uses: actions/setup-python@v6
23
+ with:
24
+ python-version: "3.12"
25
+ - name: Install build
26
+ run: pip install build
27
+ - name: Build package
28
+ run: python -m build
29
+ - name: Publish to PyPI
30
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,44 @@
1
+ name: Sync release notes
2
+
3
+ # When a GitHub release is published, replace its body with the matching
4
+ # section from CHANGELOG.md so the repo, GitHub, and Read the Docs all show
5
+ # identical notes. CHANGELOG.md is assembled from towncrier fragments on `dev`
6
+ # before the release, so the section already exists at tag time.
7
+
8
+ on:
9
+ release:
10
+ types: [published]
11
+
12
+ permissions:
13
+ contents: write
14
+
15
+ jobs:
16
+ sync:
17
+ name: Sync notes from CHANGELOG.md
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v6
21
+ with:
22
+ ref: main
23
+ fetch-depth: 0
24
+
25
+ - name: Extract changelog section
26
+ run: |
27
+ TAG="${{ github.event.release.tag_name }}"
28
+ VERSION="${TAG#v}"
29
+ awk -v v="$VERSION" '
30
+ index($0, "## v" v " ") == 1 { f = 1; print; next }
31
+ f && /^## v/ { exit }
32
+ f { print }
33
+ ' CHANGELOG.md > release-notes.md
34
+ if [ ! -s release-notes.md ]; then
35
+ echo "::error::No CHANGELOG.md section found for $TAG (expected a '## v$VERSION ' heading)."
36
+ exit 1
37
+ fi
38
+ echo "Extracted notes for $TAG:"
39
+ cat release-notes.md
40
+
41
+ - name: Update release body
42
+ env:
43
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44
+ run: gh release edit "${{ github.event.release.tag_name }}" --notes-file release-notes.md
@@ -0,0 +1,83 @@
1
+ name: Release prep
2
+
3
+ # Assembles CHANGELOG.md from the accumulated towncrier fragments on `dev`,
4
+ # BEFORE the release. Run this, merge the resulting PR into `dev`, then open
5
+ # the dev -> main PR and fast-forward it (`/fast-forward`). The dev -> main
6
+ # merge cannot build the changelog itself: that merge is fast-forward only,
7
+ # so main must equal dev's exact HEAD — every commit has to already be on dev.
8
+ #
9
+ # Versioning is dynamic (setuptools_scm), so the version is supplied here and
10
+ # only stamps the changelog heading; the real release version is the git tag
11
+ # created when the GitHub release is published.
12
+
13
+ on:
14
+ workflow_dispatch:
15
+ inputs:
16
+ version:
17
+ description: "Release version, no leading v (e.g. 0.1.0a2)"
18
+ required: true
19
+ type: string
20
+
21
+ permissions:
22
+ contents: write
23
+ pull-requests: write
24
+
25
+ jobs:
26
+ prep:
27
+ name: Build changelog and open PR
28
+ runs-on: ubuntu-latest
29
+ # FF_MERGE_TOKEN (PAT, in the `ff_merge` environment) acts as a real user, so
30
+ # it can open the PR — the GITHUB_TOKEN is blocked by the repo's "Allow GitHub
31
+ # Actions to create and approve pull requests" policy being off.
32
+ environment: ff_merge
33
+ steps:
34
+ - uses: actions/checkout@v6
35
+ with:
36
+ ref: dev
37
+ fetch-depth: 0
38
+ token: ${{ secrets.FF_MERGE_TOKEN }}
39
+
40
+ - name: Set up Python
41
+ uses: actions/setup-python@v6
42
+ with:
43
+ python-version: "3.12"
44
+
45
+ - name: Install towncrier
46
+ run: pip install towncrier
47
+
48
+ - name: Validate version
49
+ env:
50
+ VERSION: ${{ inputs.version }}
51
+ run: |
52
+ if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([ab]|rc)?[0-9]*$'; then
53
+ echo "::error::'$VERSION' is not a valid PEP 440 version (expected e.g. 0.1.0 or 0.1.0a2)."
54
+ exit 1
55
+ fi
56
+ if [ -z "$(ls -A changelog.d/*.md 2>/dev/null | grep -v '/README.md')" ]; then
57
+ echo "::error::No towncrier fragments in changelog.d/ — nothing to release."
58
+ exit 1
59
+ fi
60
+
61
+ - name: Build changelog
62
+ env:
63
+ VERSION: ${{ inputs.version }}
64
+ run: towncrier build --yes --version "$VERSION"
65
+
66
+ - name: Open release-prep PR
67
+ env:
68
+ VERSION: ${{ inputs.version }}
69
+ GH_TOKEN: ${{ secrets.FF_MERGE_TOKEN }}
70
+ run: |
71
+ BRANCH="release-prep/v${VERSION}"
72
+ git config user.name "github-actions[bot]"
73
+ git config user.email "github-actions[bot]@users.noreply.github.com"
74
+ git checkout -b "$BRANCH"
75
+ git add -A
76
+ git commit -m "docs(changelog): assemble notes for v${VERSION}"
77
+ git push -u origin "$BRANCH"
78
+ gh pr create \
79
+ --base dev \
80
+ --head "$BRANCH" \
81
+ --title "docs(changelog): release v${VERSION}" \
82
+ --label skip-changelog \
83
+ --body "Auto-generated by the release-prep workflow. Builds \`CHANGELOG.md\` from the towncrier fragments and removes them. Merge into \`dev\`, then open the dev -> main PR."