portlearn 0.0.1.dev0__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 (28) hide show
  1. portlearn-0.0.1.dev0/.github/workflows/ci.yml +50 -0
  2. portlearn-0.0.1.dev0/.github/workflows/release.yml +425 -0
  3. portlearn-0.0.1.dev0/.gitignore +20 -0
  4. portlearn-0.0.1.dev0/CHANGELOG.md +23 -0
  5. portlearn-0.0.1.dev0/LICENSE +202 -0
  6. portlearn-0.0.1.dev0/PKG-INFO +70 -0
  7. portlearn-0.0.1.dev0/README.md +59 -0
  8. portlearn-0.0.1.dev0/examples/foundation_contract_wiring.py +136 -0
  9. portlearn-0.0.1.dev0/pyproject.toml +39 -0
  10. portlearn-0.0.1.dev0/scripts/verify_built_wheel.py +284 -0
  11. portlearn-0.0.1.dev0/src/portlearn/__init__.py +18 -0
  12. portlearn-0.0.1.dev0/src/portlearn/interfaces.py +539 -0
  13. portlearn-0.0.1.dev0/src/portlearn/leakage.py +290 -0
  14. portlearn-0.0.1.dev0/src/portlearn/manifest.py +298 -0
  15. portlearn-0.0.1.dev0/src/portlearn/observations.py +288 -0
  16. portlearn-0.0.1.dev0/src/portlearn/py.typed +0 -0
  17. portlearn-0.0.1.dev0/src/portlearn/timing.py +328 -0
  18. portlearn-0.0.1.dev0/tests/test_aware_validator_dedup.py +277 -0
  19. portlearn-0.0.1.dev0/tests/test_fold_semantics.py +439 -0
  20. portlearn-0.0.1.dev0/tests/test_information_contracts.py +588 -0
  21. portlearn-0.0.1.dev0/tests/test_interface_contracts.py +1109 -0
  22. portlearn-0.0.1.dev0/tests/test_invariant_battery.py +689 -0
  23. portlearn-0.0.1.dev0/tests/test_package_contract.py +388 -0
  24. portlearn-0.0.1.dev0/tests/test_public_release_mechanism.py +1218 -0
  25. portlearn-0.0.1.dev0/tests/test_public_release_surface.py +420 -0
  26. portlearn-0.0.1.dev0/tests/test_timing_contracts.py +567 -0
  27. portlearn-0.0.1.dev0/tests/test_wiring_contracts.py +605 -0
  28. portlearn-0.0.1.dev0/uv.lock +108 -0
@@ -0,0 +1,50 @@
1
+ # Public CI for PortLearn.
2
+ # Runs on every push and pull request, guarded to the public fmasoudy/
3
+ # PortLearn repository, least-privilege permissions, frozen
4
+ # per-interpreter sequence, single dependent build-and-verify-wheel job,
5
+ # immutable action pins.
6
+ name: ci
7
+
8
+ on:
9
+ push:
10
+ branches: [main]
11
+ pull_request:
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ checks:
18
+ if: github.repository == 'fmasoudy/PortLearn'
19
+ runs-on: ubuntu-latest
20
+ strategy:
21
+ fail-fast: false
22
+ matrix:
23
+ python-version: ["3.11", "3.12", "3.13"]
24
+ steps:
25
+ - name: Check out the repository
26
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
27
+ - name: Set up uv
28
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
29
+ with:
30
+ version: "0.12.10"
31
+ - name: Run the frozen per-interpreter sequence
32
+ run: |
33
+ uv sync --locked --python ${{ matrix.python-version }}
34
+ uv run --python ${{ matrix.python-version }} ruff check .
35
+ uv run --python ${{ matrix.python-version }} pytest
36
+ build-and-verify-wheel:
37
+ if: github.repository == 'fmasoudy/PortLearn'
38
+ needs: checks
39
+ runs-on: ubuntu-latest
40
+ steps:
41
+ - name: Check out the repository
42
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
43
+ - name: Set up uv
44
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
45
+ with:
46
+ version: "0.12.10"
47
+ - name: Build the distribution
48
+ run: |
49
+ uv build
50
+ uv run python scripts/verify_built_wheel.py
@@ -0,0 +1,425 @@
1
+ # Controlled public release mechanism.
2
+ #
3
+ # This workflow is the intended path for creating a public version tag
4
+ # and its GitHub Release. It never runs automatically: workflow_dispatch
5
+ # is the sole trigger, so every run is a deliberate, manually dispatched
6
+ # release act.
7
+ #
8
+ # It validates every consistency rule FIRST and fails closed on any
9
+ # mismatch, leaving no partial state beyond the failed run log. It never
10
+ # deletes, moves, or overwrites an existing tag or release. Existence
11
+ # queries fail closed the same way: a query whose answer cannot be
12
+ # confirmed (transport error, auth error, unexpected HTTP status)
13
+ # aborts the run rather than being read as "absent". Runs are also
14
+ # serialized in a 'public-release' concurrency group (no cancel), so
15
+ # two dispatches cannot race between validation and creation.
16
+ #
17
+ # Development releases are rejected: a version carrying a PEP 440
18
+ # development segment (for example 0.0.1.dev0) describes an
19
+ # unreleased development state and must never receive a public release
20
+ # tag or GitHub Release. The validation module fails closed on it.
21
+ # Pre-release versions (a/b/rc) are likewise rejected: this mechanism
22
+ # does not mark the GitHub Release as a prerelease, so RC support is
23
+ # deferred until the creation step can label them correctly.
24
+ #
25
+ # Tag-object design decision: creation is a single release-creation
26
+ # command with --target, which creates the release's own lightweight tag
27
+ # at the exact target commit together with the Release. A separate
28
+ # annotated tag created and pushed on its own would split the operation
29
+ # in two and risk partial state (an annotated tag with no Release).
30
+ # Keeping creation to one command reduces partial-state risk; it is not
31
+ # a transaction spanning the underlying API calls. The release-created
32
+ # lightweight tag is therefore selected deliberately to minimize
33
+ # partial-state risk; release provenance is carried by the Release
34
+ # record, the pinned target commit, and the manually dispatched run,
35
+ # not by tag-object metadata.
36
+ name: public-release
37
+
38
+ on:
39
+ workflow_dispatch:
40
+ inputs:
41
+ version:
42
+ description: "Requested release version; must exactly equal [project].version in pyproject.toml at the target commit (the single version authority) and must be a final release: development (dev) and pre-release (a/b/rc) versions are rejected."
43
+ required: true
44
+ type: string
45
+ tag_name:
46
+ description: "Release tag name; must equal v plus the pyproject.toml version (for example v0.0.1)."
47
+ required: true
48
+ type: string
49
+ target_sha:
50
+ description: "Full 40-hex commit SHA of public main to tag; it must exist and be the current origin/main head."
51
+ required: true
52
+ type: string
53
+
54
+ permissions:
55
+ contents: write
56
+
57
+ concurrency:
58
+ group: public-release
59
+ cancel-in-progress: false
60
+
61
+ jobs:
62
+ release:
63
+ if: github.repository == 'fmasoudy/PortLearn'
64
+ runs-on: ubuntu-latest
65
+ env:
66
+ PL_VERSION: ${{ inputs.version }}
67
+ PL_TAG_NAME: ${{ inputs.tag_name }}
68
+ PL_TARGET_SHA: ${{ inputs.target_sha }}
69
+ steps:
70
+ - name: Check out the exact target commit
71
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
72
+ with:
73
+ ref: ${{ inputs.target_sha }}
74
+ fetch-depth: 0
75
+ # Least authority: no later step may use a persisted token;
76
+ # every repo operation after this is read-only except the
77
+ # final release creation, which names its token and
78
+ # repository explicitly.
79
+ persist-credentials: false
80
+ - name: Fail closed unless the target commit is the current origin main head
81
+ run: |
82
+ set -euo pipefail
83
+ git fetch --no-tags origin "+refs/heads/main:refs/remotes/origin/main"
84
+ git fetch --tags origin
85
+ git cat-file -e "${PL_TARGET_SHA}^{commit}"
86
+ main_head="$(git rev-parse refs/remotes/origin/main)"
87
+ if [ "$main_head" != "$PL_TARGET_SHA" ]; then
88
+ echo "::error::target ${PL_TARGET_SHA} is not the current origin/main head (${main_head}); a stale or off-main commit is never tagged"
89
+ exit 1
90
+ fi
91
+ - name: Fail closed if the tag already exists locally or on the remote
92
+ run: |
93
+ set -euo pipefail
94
+ if [ -n "$(git tag --list "$PL_TAG_NAME")" ]; then
95
+ echo "::error::local tag ${PL_TAG_NAME} already exists; existing tags are never overwritten"
96
+ exit 1
97
+ fi
98
+ # A failed remote query must never be read as tag-absence:
99
+ # capture the query status separately and abort on any nonzero
100
+ # (set -e does not fire inside if-conditions, so only the
101
+ # explicit status check below makes failure observable).
102
+ remote_tag_query="$(git ls-remote --tags origin "refs/tags/$PL_TAG_NAME")"
103
+ remote_tag_query_status="$?"
104
+ if [ "$remote_tag_query_status" -ne 0 ]; then
105
+ echo "::error::remote tag query for ${PL_TAG_NAME} failed (exit ${remote_tag_query_status}); absence cannot be confirmed"
106
+ exit 1
107
+ fi
108
+ if [ -n "$remote_tag_query" ]; then
109
+ echo "::error::remote tag ${PL_TAG_NAME} already exists; existing tags are never overwritten"
110
+ exit 1
111
+ fi
112
+ - name: Fail closed if the GitHub Release already exists
113
+ env:
114
+ GH_TOKEN: ${{ github.token }}
115
+ GH_REPO: ${{ github.repository }}
116
+ run: |
117
+ set -euo pipefail
118
+ # Only an explicit answer permits the run to proceed: 200 means
119
+ # the release exists (abort), 404 means absent (proceed), and
120
+ # every other status — auth failure, rate limit, server or
121
+ # transport error — aborts. The CLI release-view helper cannot
122
+ # express this: its exit 1 conflates 404 with every failure
123
+ # mode, so the guard probes the REST endpoint directly.
124
+ release_status="$(curl --silent --show-error \
125
+ --output /dev/null \
126
+ --write-out "%{http_code}" \
127
+ -H "Authorization: Bearer ${GH_TOKEN}" \
128
+ -H "Accept: application/vnd.github+json" \
129
+ "$GITHUB_API_URL/repos/$GH_REPO/releases/tags/$PL_TAG_NAME")"
130
+ if [ "$release_status" = "200" ]; then
131
+ echo "::error::a GitHub Release for ${PL_TAG_NAME} already exists; an existing release is never overwritten"
132
+ exit 1
133
+ elif [ "$release_status" = "404" ]; then
134
+ echo "no existing GitHub Release for ${PL_TAG_NAME} (HTTP 404)"
135
+ else
136
+ echo "::error::release-existence query for ${PL_TAG_NAME} returned HTTP ${release_status}; absence cannot be confirmed"
137
+ exit 1
138
+ fi
139
+ - name: Set up uv
140
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
141
+ with:
142
+ version: "0.12.10"
143
+ - name: Write the release validation module
144
+ run: |
145
+ cat > "${RUNNER_TEMP}/release_validation.py" <<'PY'
146
+ # --- portlearn-release-validation: begin (pure, offline) ---
147
+ """Pure, offline release-consistency validation.
148
+
149
+ This module is deliberately standalone: it reads only the
150
+ pyproject.toml and CHANGELOG.md of the checked-out target commit
151
+ plus a caller-supplied list of existing version tags. It performs
152
+ no network or repository access and it creates nothing. Every rule
153
+ fails closed by raising ReleaseValidationError with the reason;
154
+ the caller reports the reason and stops before anything is created.
155
+ """
156
+
157
+ from __future__ import annotations
158
+
159
+ import argparse
160
+ import re
161
+ import sys
162
+ import tomllib
163
+ from pathlib import Path
164
+
165
+ from packaging.version import InvalidVersion, Version
166
+
167
+ FULL_SHA_PATTERN = re.compile(r"[0-9a-f]{40}")
168
+
169
+
170
+ class ReleaseValidationError(Exception):
171
+ """A release-consistency rule failed; nothing may be created."""
172
+
173
+
174
+ def heading_pattern(version: str) -> re.Pattern[str]:
175
+ """The Keep-a-Changelog release heading pattern for one version."""
176
+ return re.compile(
177
+ rf"^## \[{re.escape(version)}\](?:\s.*)?$", re.MULTILINE
178
+ )
179
+
180
+
181
+ def changelog_section(changelog_text: str, version: str) -> str | None:
182
+ """The body of one versioned release heading, or None if absent."""
183
+ match = heading_pattern(version).search(changelog_text)
184
+ if match is None:
185
+ return None
186
+ rest = changelog_text[match.end() :]
187
+ following = re.search(r"^## ", rest, re.MULTILINE)
188
+ return rest[: following.start()] if following else rest
189
+
190
+
191
+ def meaningful_content(section_text: str) -> list[str]:
192
+ """The content lines of one section body.
193
+
194
+ Headings (the Keep-a-Changelog ``###`` subsections and the
195
+ ``##`` version/Unreleased headings) are structure, not
196
+ content: an otherwise-empty scaffold of headings is an
197
+ empty section. Everything else — bullets, prose, any
198
+ non-empty non-heading line — is content that must be
199
+ released or moved before a release is cut.
200
+ """
201
+ return [
202
+ line.strip()
203
+ for line in section_text.splitlines()
204
+ if line.strip() and not line.lstrip().startswith("#")
205
+ ]
206
+
207
+
208
+ def declared_version(pyproject_path: Path | str) -> str:
209
+ """The version declared by pyproject.toml at the target commit."""
210
+ data = tomllib.loads(Path(pyproject_path).read_text(encoding="utf-8"))
211
+ try:
212
+ return str(data["project"]["version"])
213
+ except KeyError as error:
214
+ message = (
215
+ "pyproject.toml at the target commit declares no "
216
+ "[project] version"
217
+ )
218
+ raise ReleaseValidationError(message) from error
219
+
220
+
221
+ def check_monotone_forward(
222
+ version: str, existing_version_tags: list[str]
223
+ ) -> None:
224
+ """The candidate version must be strictly after every existing tag."""
225
+ candidate = Version(version)
226
+ for tag in existing_version_tags:
227
+ name = tag[1:] if tag.startswith("v") else tag
228
+ try:
229
+ existing = Version(name)
230
+ except InvalidVersion:
231
+ continue # not a version tag; it does not constrain ordering
232
+ if candidate <= existing:
233
+ message = (
234
+ f"version {version} is not monotone-forward: the "
235
+ f"existing public version tag {tag} is at or after it"
236
+ )
237
+ raise ReleaseValidationError(message)
238
+
239
+
240
+ def reject_development_release(candidate: Version) -> None:
241
+ """A PEP 440 development release must never be released publicly.
242
+
243
+ A version carrying a development segment (``.devN``, as in
244
+ ``0.0.1.dev0``) describes an unreleased development state,
245
+ not a releasable version: tagging it would publish a moving
246
+ target. Pre-release segments (``a``, ``b``, ``rc``) are
247
+ rejected separately by ``reject_prerelease_version``.
248
+ """
249
+ if candidate.dev is not None:
250
+ message = (
251
+ f"version {str(candidate)!r} is a PEP 440 development "
252
+ "release (dev segment present): development releases "
253
+ "are never tagged or released publicly"
254
+ )
255
+ raise ReleaseValidationError(message)
256
+
257
+
258
+ def reject_prerelease_version(candidate: Version) -> None:
259
+ """Pre-release versions are deferred, not released.
260
+
261
+ This mechanism creates GitHub Releases without the
262
+ prerelease flag, so publishing ``a``/``b``/``rc`` versions
263
+ would mislabel them as full releases. Pre-release support
264
+ must be added to the creation step before any pre-release
265
+ may validate.
266
+ """
267
+ if candidate.pre is not None:
268
+ message = (
269
+ f"version {str(candidate)!r} is a PEP 440 pre-release "
270
+ f"({candidate.pre[0]}{candidate.pre[1]} segment "
271
+ "present): prerelease (RC) support is deferred, so "
272
+ "pre-release versions are never tagged or released "
273
+ "through this mechanism"
274
+ )
275
+ raise ReleaseValidationError(message)
276
+
277
+
278
+ def validate_release(
279
+ version: str,
280
+ tag_name: str,
281
+ target_sha: str,
282
+ pyproject_path: Path | str,
283
+ changelog_path: Path | str,
284
+ existing_version_tags: list[str] | None = None,
285
+ ) -> str:
286
+ """Validate every release-consistency rule; return the release notes.
287
+
288
+ Any violation raises ReleaseValidationError describing the broken
289
+ rule; the caller must abort before creating anything.
290
+ """
291
+ if not FULL_SHA_PATTERN.fullmatch(target_sha):
292
+ message = (
293
+ f"target {target_sha!r} is not a 40-character full "
294
+ "commit SHA; a shortened or malformed target is never "
295
+ "tagged"
296
+ )
297
+ raise ReleaseValidationError(message)
298
+
299
+ declared = declared_version(pyproject_path)
300
+ if version != declared:
301
+ message = (
302
+ f"requested version {version!r} does not equal the "
303
+ "version declared by pyproject.toml at the target "
304
+ f"commit ({declared!r})"
305
+ )
306
+ raise ReleaseValidationError(message)
307
+
308
+ try:
309
+ candidate = Version(version)
310
+ except InvalidVersion as error:
311
+ message = (
312
+ f"requested version {version!r} is not a valid "
313
+ "PEP 440 version"
314
+ )
315
+ raise ReleaseValidationError(message) from error
316
+
317
+ reject_development_release(candidate)
318
+ reject_prerelease_version(candidate)
319
+
320
+ expected_tag = f"v{version}"
321
+ if tag_name != expected_tag:
322
+ message = (
323
+ f"tag name {tag_name!r} must equal 'v' plus the "
324
+ f"declared version ({expected_tag!r})"
325
+ )
326
+ raise ReleaseValidationError(message)
327
+
328
+ changelog_text = Path(changelog_path).read_text(encoding="utf-8")
329
+ section = changelog_section(changelog_text, version)
330
+ if section is None:
331
+ message = (
332
+ f"CHANGELOG.md has no release section for {version}: "
333
+ f"the '## [{version}]' heading is required before a "
334
+ "release"
335
+ )
336
+ raise ReleaseValidationError(message)
337
+ if not meaningful_content(section):
338
+ message = (
339
+ f"the CHANGELOG.md [{version}] release section must "
340
+ "not be empty: the release notes are drawn from it"
341
+ )
342
+ raise ReleaseValidationError(message)
343
+
344
+ unreleased = changelog_section(changelog_text, "Unreleased")
345
+ pending = meaningful_content(unreleased) if unreleased is not None else []
346
+ if pending:
347
+ message = (
348
+ "the CHANGELOG.md [Unreleased] section is not empty "
349
+ f"or absent: {len(pending)} pending entries must move "
350
+ "into release sections first"
351
+ )
352
+ raise ReleaseValidationError(message)
353
+
354
+ check_monotone_forward(version, existing_version_tags or [])
355
+ return section.strip()
356
+
357
+
358
+ def main(argv: list[str] | None = None) -> int:
359
+ """Run validation from command-line arguments; 0 on success."""
360
+ parser = argparse.ArgumentParser(
361
+ description="Validate a public release request (fail-closed)."
362
+ )
363
+ parser.add_argument("--version", required=True)
364
+ parser.add_argument("--tag-name", required=True)
365
+ parser.add_argument("--target-sha", required=True)
366
+ parser.add_argument("--pyproject", required=True)
367
+ parser.add_argument("--changelog", required=True)
368
+ parser.add_argument("--existing-tags-file", required=True)
369
+ parser.add_argument("--notes-out", required=True)
370
+ args = parser.parse_args(argv)
371
+
372
+ tags_path = Path(args.existing_tags_file)
373
+ existing: list[str] = []
374
+ if tags_path.is_file():
375
+ existing = [
376
+ line.strip()
377
+ for line in tags_path.read_text(encoding="utf-8").splitlines()
378
+ if line.strip()
379
+ ]
380
+
381
+ try:
382
+ notes = validate_release(
383
+ version=args.version,
384
+ tag_name=args.tag_name,
385
+ target_sha=args.target_sha,
386
+ pyproject_path=args.pyproject,
387
+ changelog_path=args.changelog,
388
+ existing_version_tags=existing,
389
+ )
390
+ except ReleaseValidationError as error:
391
+ print(f"release validation failed: {error}", file=sys.stderr)
392
+ return 1
393
+
394
+ Path(args.notes_out).write_text(notes + "\n", encoding="utf-8")
395
+ print(f"release validation passed; notes written to {args.notes_out}")
396
+ return 0
397
+
398
+
399
+ if __name__ == "__main__":
400
+ raise SystemExit(main())
401
+ # --- portlearn-release-validation: end ---
402
+ PY
403
+ - name: Collect existing public version tags
404
+ run: |
405
+ set -euo pipefail
406
+ git tag --list "v*" | sort > "${RUNNER_TEMP}/existing_tags.txt"
407
+ - name: Fail closed on any release-consistency mismatch
408
+ run: |
409
+ set -euo pipefail
410
+ uv sync --locked
411
+ uv run python "${RUNNER_TEMP}/release_validation.py" \
412
+ --version "$PL_VERSION" \
413
+ --tag-name "$PL_TAG_NAME" \
414
+ --target-sha "$PL_TARGET_SHA" \
415
+ --pyproject pyproject.toml \
416
+ --changelog CHANGELOG.md \
417
+ --existing-tags-file "${RUNNER_TEMP}/existing_tags.txt" \
418
+ --notes-out "${RUNNER_TEMP}/release-notes.md"
419
+ - name: Create the GitHub Release and its tag in one action
420
+ env:
421
+ GH_TOKEN: ${{ github.token }}
422
+ GH_REPO: ${{ github.repository }}
423
+ run: |
424
+ set -euo pipefail
425
+ gh release create "$PL_TAG_NAME" --target "$PL_TARGET_SHA" --title "$PL_TAG_NAME" --notes-file "${RUNNER_TEMP}/release-notes.md"
@@ -0,0 +1,20 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .mypy_cache/
10
+
11
+ # Environments
12
+ .venv/
13
+ venv/
14
+ .env
15
+
16
+ # OS
17
+ .DS_Store
18
+
19
+ # Local scratch (never commit run artifacts without an explicit milestone decision)
20
+ scratch/
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All notable public changes to PortLearn are documented in this file.
4
+
5
+ Git history remains the detailed development record. This changelog is the curated user/researcher-facing history: it tracks meaningful public changes and public software releases, with release sections recording releases and the entries within them recording user-visible changes.
6
+
7
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows the project's PEP 440 policy: early-development `0.0.x` public releases.
8
+
9
+ ## [Unreleased]
10
+
11
+ ## [0.0.1.dev0]
12
+
13
+ ### Added
14
+
15
+ - Foundation research contracts: time/chronology contracts, observation handling, information sets, core research interfaces, information-leakage validation, and run manifests, with a synthetic contract-wiring example, public test suite, and public CI.
16
+
17
+ ### Changed
18
+
19
+ ### Fixed
20
+
21
+ ### Deprecated
22
+
23
+ ### Removed