patchground 0.1.0__py3-none-any.whl

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 (194) hide show
  1. patchground/__init__.py +10 -0
  2. patchground/__main__.py +3 -0
  3. patchground/authoring/__init__.py +5 -0
  4. patchground/authoring/create.py +179 -0
  5. patchground/authoring/templates/python-debug/TICKET.md.tmpl +11 -0
  6. patchground/authoring/templates/python-debug/challenge.yaml.tmpl +21 -0
  7. patchground/authoring/templates/python-debug/checks/hidden/tests/test_starter_hidden.py.tmpl +13 -0
  8. patchground/authoring/templates/python-debug/checks/visible/tests/test_starter.py.tmpl +9 -0
  9. patchground/authoring/templates/python-debug/environment/Dockerfile.tmpl +3 -0
  10. patchground/authoring/templates/python-debug/solution/reference.patch.tmpl +8 -0
  11. patchground/authoring/templates/python-debug/workspace/pyproject.toml.tmpl +8 -0
  12. patchground/authoring/templates/python-debug/workspace/src/starter.py.tmpl +3 -0
  13. patchground/authoring/templates/python-feature/TICKET.md.tmpl +13 -0
  14. patchground/authoring/templates/python-feature/challenge.yaml.tmpl +22 -0
  15. patchground/authoring/templates/python-feature/checks/hidden/tests/test_starter_hidden.py.tmpl +13 -0
  16. patchground/authoring/templates/python-feature/checks/visible/tests/test_starter.py.tmpl +9 -0
  17. patchground/authoring/templates/python-feature/environment/Dockerfile.tmpl +3 -0
  18. patchground/authoring/templates/python-feature/solution/reference.patch.tmpl +9 -0
  19. patchground/authoring/templates/python-feature/workspace/pyproject.toml.tmpl +8 -0
  20. patchground/authoring/templates/python-feature/workspace/src/starter.py.tmpl +8 -0
  21. patchground/authoring/templates/typescript-debug/TICKET.md.tmpl +11 -0
  22. patchground/authoring/templates/typescript-debug/challenge.yaml.tmpl +21 -0
  23. patchground/authoring/templates/typescript-debug/checks/hidden/tests/starter-hidden.test.ts.tmpl +14 -0
  24. patchground/authoring/templates/typescript-debug/checks/visible/tests/starter.test.ts.tmpl +10 -0
  25. patchground/authoring/templates/typescript-debug/solution/reference.patch.tmpl +9 -0
  26. patchground/authoring/templates/typescript-debug/workspace/package.json.tmpl +6 -0
  27. patchground/authoring/templates/typescript-debug/workspace/src/starter.ts.tmpl +4 -0
  28. patchground/authoring/templates/typescript-feature/TICKET.md.tmpl +13 -0
  29. patchground/authoring/templates/typescript-feature/challenge.yaml.tmpl +22 -0
  30. patchground/authoring/templates/typescript-feature/checks/hidden/tests/starter-hidden.test.ts.tmpl +14 -0
  31. patchground/authoring/templates/typescript-feature/checks/visible/tests/starter.test.ts.tmpl +10 -0
  32. patchground/authoring/templates/typescript-feature/solution/reference.patch.tmpl +10 -0
  33. patchground/authoring/templates/typescript-feature/workspace/package.json.tmpl +6 -0
  34. patchground/authoring/templates/typescript-feature/workspace/src/starter.ts.tmpl +9 -0
  35. patchground/automation/__init__.py +5 -0
  36. patchground/automation/__main__.py +131 -0
  37. patchground/automation/workflow.py +214 -0
  38. patchground/catalog/README.md +10 -0
  39. patchground/catalog/cache-expiry/TICKET.md +49 -0
  40. patchground/catalog/cache-expiry/challenge.yaml +32 -0
  41. patchground/catalog/cache-expiry/checks/hidden/tests/test_cache_hidden.py +164 -0
  42. patchground/catalog/cache-expiry/checks/visible/tests/test_cache.py +56 -0
  43. patchground/catalog/cache-expiry/debrief/questions.md +4 -0
  44. patchground/catalog/cache-expiry/environment/Dockerfile +3 -0
  45. patchground/catalog/cache-expiry/solution/reference.patch +41 -0
  46. patchground/catalog/cache-expiry/workspace/pyproject.toml +8 -0
  47. patchground/catalog/cache-expiry/workspace/src/profilecache/__init__.py +6 -0
  48. patchground/catalog/cache-expiry/workspace/src/profilecache/cache.py +67 -0
  49. patchground/catalog/cache-expiry/workspace/src/profilecache/client.py +38 -0
  50. patchground/catalog/csv-import-row-errors/TICKET.md +97 -0
  51. patchground/catalog/csv-import-row-errors/challenge.yaml +33 -0
  52. patchground/catalog/csv-import-row-errors/checks/hidden/tests/test_import_hidden.py +359 -0
  53. patchground/catalog/csv-import-row-errors/checks/visible/tests/test_import.py +34 -0
  54. patchground/catalog/csv-import-row-errors/debrief/questions.md +5 -0
  55. patchground/catalog/csv-import-row-errors/environment/Dockerfile +3 -0
  56. patchground/catalog/csv-import-row-errors/solution/reference.patch +74 -0
  57. patchground/catalog/csv-import-row-errors/workspace/pyproject.toml +8 -0
  58. patchground/catalog/csv-import-row-errors/workspace/src/stockimport/__init__.py +4 -0
  59. patchground/catalog/csv-import-row-errors/workspace/src/stockimport/importer.py +69 -0
  60. patchground/catalog/csv-import-row-errors/workspace/src/stockimport/models.py +32 -0
  61. patchground/catalog/duplicate-charges-on-retry/TICKET.md +28 -0
  62. patchground/catalog/duplicate-charges-on-retry/challenge.yaml +30 -0
  63. patchground/catalog/duplicate-charges-on-retry/checks/hidden/tests/test_payment_regressions.py +87 -0
  64. patchground/catalog/duplicate-charges-on-retry/checks/visible/tests/test_checkout.py +34 -0
  65. patchground/catalog/duplicate-charges-on-retry/debrief/questions.md +6 -0
  66. patchground/catalog/duplicate-charges-on-retry/environment/Dockerfile +3 -0
  67. patchground/catalog/duplicate-charges-on-retry/solution/reference.patch +11 -0
  68. patchground/catalog/duplicate-charges-on-retry/workspace/pyproject.toml +8 -0
  69. patchground/catalog/duplicate-charges-on-retry/workspace/src/payments/__init__.py +1 -0
  70. patchground/catalog/duplicate-charges-on-retry/workspace/src/payments/gateway.py +38 -0
  71. patchground/catalog/duplicate-charges-on-retry/workspace/src/payments/models.py +19 -0
  72. patchground/catalog/duplicate-charges-on-retry/workspace/src/payments/service.py +23 -0
  73. patchground/catalog/event-bus-subscriptions/TICKET.md +108 -0
  74. patchground/catalog/event-bus-subscriptions/challenge.yaml +33 -0
  75. patchground/catalog/event-bus-subscriptions/checks/hidden/tests/event-bus-contract.test.ts +566 -0
  76. patchground/catalog/event-bus-subscriptions/checks/visible/tests/event-bus.test.ts +70 -0
  77. patchground/catalog/event-bus-subscriptions/debrief/questions.md +5 -0
  78. patchground/catalog/event-bus-subscriptions/environment/Dockerfile +7 -0
  79. patchground/catalog/event-bus-subscriptions/environment/package-lock.json +1054 -0
  80. patchground/catalog/event-bus-subscriptions/environment/package.json +13 -0
  81. patchground/catalog/event-bus-subscriptions/environment/vitest-runner.mjs +124 -0
  82. patchground/catalog/event-bus-subscriptions/solution/reference.patch +82 -0
  83. patchground/catalog/event-bus-subscriptions/workspace/package.json +6 -0
  84. patchground/catalog/event-bus-subscriptions/workspace/src/bus.ts +32 -0
  85. patchground/catalog/order-notifications-boundary/TICKET.md +51 -0
  86. patchground/catalog/order-notifications-boundary/challenge.yaml +32 -0
  87. patchground/catalog/order-notifications-boundary/checks/hidden/tests/test_boundary_hidden.py +87 -0
  88. patchground/catalog/order-notifications-boundary/checks/visible/tests/test_confirmation.py +41 -0
  89. patchground/catalog/order-notifications-boundary/debrief/questions.md +5 -0
  90. patchground/catalog/order-notifications-boundary/environment/Dockerfile +3 -0
  91. patchground/catalog/order-notifications-boundary/solution/reference.patch +77 -0
  92. patchground/catalog/order-notifications-boundary/workspace/pyproject.toml +8 -0
  93. patchground/catalog/order-notifications-boundary/workspace/src/orders/__init__.py +6 -0
  94. patchground/catalog/order-notifications-boundary/workspace/src/orders/app.py +9 -0
  95. patchground/catalog/order-notifications-boundary/workspace/src/orders/models.py +25 -0
  96. patchground/catalog/order-notifications-boundary/workspace/src/orders/service.py +31 -0
  97. patchground/catalog/order-notifications-boundary/workspace/src/orders/smtp_gateway.py +19 -0
  98. patchground/catalog/order-notifications-boundary/workspace/src/orders/sqlite_store.py +44 -0
  99. patchground/catalog/pagination-last-page/TICKET.md +44 -0
  100. patchground/catalog/pagination-last-page/challenge.yaml +31 -0
  101. patchground/catalog/pagination-last-page/checks/hidden/tests/test_pagination_hidden.py +32 -0
  102. patchground/catalog/pagination-last-page/checks/visible/tests/test_pagination.py +37 -0
  103. patchground/catalog/pagination-last-page/debrief/questions.md +9 -0
  104. patchground/catalog/pagination-last-page/environment/Dockerfile +3 -0
  105. patchground/catalog/pagination-last-page/solution/reference.patch +22 -0
  106. patchground/catalog/pagination-last-page/workspace/pyproject.toml +8 -0
  107. patchground/catalog/pagination-last-page/workspace/src/catalog/__init__.py +1 -0
  108. patchground/catalog/pagination-last-page/workspace/src/catalog/pagination.py +52 -0
  109. patchground/catalog/release-version-ordering/TICKET.md +72 -0
  110. patchground/catalog/release-version-ordering/challenge.yaml +31 -0
  111. patchground/catalog/release-version-ordering/checks/hidden/tests/releasekit_contract.test.ts +92 -0
  112. patchground/catalog/release-version-ordering/checks/visible/tests/releasekit.test.ts +31 -0
  113. patchground/catalog/release-version-ordering/debrief/questions.md +9 -0
  114. patchground/catalog/release-version-ordering/environment/Dockerfile +7 -0
  115. patchground/catalog/release-version-ordering/environment/package-lock.json +1054 -0
  116. patchground/catalog/release-version-ordering/environment/package.json +13 -0
  117. patchground/catalog/release-version-ordering/environment/vitest-runner.mjs +124 -0
  118. patchground/catalog/release-version-ordering/solution/reference.patch +36 -0
  119. patchground/catalog/release-version-ordering/workspace/README.md +7 -0
  120. patchground/catalog/release-version-ordering/workspace/src/releasekit/index.ts +2 -0
  121. patchground/catalog/release-version-ordering/workspace/src/releasekit/ordering.ts +41 -0
  122. patchground/catalog/release-version-ordering/workspace/src/releasekit/parse.ts +24 -0
  123. patchground/cli/__init__.py +1 -0
  124. patchground/cli/app.py +59 -0
  125. patchground/cli/authoring.py +41 -0
  126. patchground/cli/consent.py +77 -0
  127. patchground/cli/discovery.py +189 -0
  128. patchground/cli/doctor.py +249 -0
  129. patchground/cli/packs.py +142 -0
  130. patchground/cli/practice.py +141 -0
  131. patchground/cli/render.py +58 -0
  132. patchground/cli/reports.py +53 -0
  133. patchground/cli/sessions.py +195 -0
  134. patchground/cli/validate.py +117 -0
  135. patchground/packs/__init__.py +23 -0
  136. patchground/packs/builtin.py +53 -0
  137. patchground/packs/discovery.py +285 -0
  138. patchground/packs/execution.py +420 -0
  139. patchground/packs/models.py +98 -0
  140. patchground/packs/registry.py +318 -0
  141. patchground/packs/source.py +230 -0
  142. patchground/packs/store.py +420 -0
  143. patchground/py.typed +0 -0
  144. patchground/report/__init__.py +6 -0
  145. patchground/report/analysis.py +188 -0
  146. patchground/report/models.py +59 -0
  147. patchground/report/regression.py +597 -0
  148. patchground/report/render.py +92 -0
  149. patchground/report/workflow.py +84 -0
  150. patchground/runner_assets/Dockerfile +7 -0
  151. patchground/runner_assets/__init__.py +16 -0
  152. patchground/runner_assets/package-lock.json +1054 -0
  153. patchground/runner_assets/package.json +13 -0
  154. patchground/runner_assets/vitest-runner.mjs +124 -0
  155. patchground/safeio/__init__.py +115 -0
  156. patchground/sandbox/__init__.py +39 -0
  157. patchground/sandbox/_engine.py +23 -0
  158. patchground/sandbox/build.py +202 -0
  159. patchground/sandbox/errors.py +23 -0
  160. patchground/sandbox/junit.py +76 -0
  161. patchground/sandbox/probe.py +118 -0
  162. patchground/sandbox/run.py +535 -0
  163. patchground/sessions/__init__.py +44 -0
  164. patchground/sessions/diagnostics.py +145 -0
  165. patchground/sessions/files.py +113 -0
  166. patchground/sessions/git.py +70 -0
  167. patchground/sessions/history.py +386 -0
  168. patchground/sessions/history_ignore.py +279 -0
  169. patchground/sessions/models.py +126 -0
  170. patchground/sessions/practice.py +202 -0
  171. patchground/sessions/reset.py +121 -0
  172. patchground/sessions/store.py +203 -0
  173. patchground/sessions/submission.py +344 -0
  174. patchground/sessions/workflow.py +297 -0
  175. patchground/spec/__init__.py +35 -0
  176. patchground/spec/_schema.py +53 -0
  177. patchground/spec/loader.py +713 -0
  178. patchground/spec/models.py +394 -0
  179. patchground/spec/schema.py +50 -0
  180. patchground/verify/__init__.py +29 -0
  181. patchground/verify/errors.py +10 -0
  182. patchground/verify/pytest_adapter.py +95 -0
  183. patchground/verify/record.py +94 -0
  184. patchground/verify/reference.py +158 -0
  185. patchground/verify/result.py +199 -0
  186. patchground/verify/runners.py +47 -0
  187. patchground/verify/tampering.py +106 -0
  188. patchground/verify/validate.py +686 -0
  189. patchground/verify/vitest_adapter.py +252 -0
  190. patchground-0.1.0.dist-info/METADATA +258 -0
  191. patchground-0.1.0.dist-info/RECORD +194 -0
  192. patchground-0.1.0.dist-info/WHEEL +4 -0
  193. patchground-0.1.0.dist-info/entry_points.txt +3 -0
  194. patchground-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,10 @@
1
+ """Patchground: real code, real problems, your tools."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("patchground")
7
+ except PackageNotFoundError: # pragma: no cover - running from a source tree without install
8
+ __version__ = "0.0.0"
9
+
10
+ __all__ = ["__version__"]
@@ -0,0 +1,3 @@
1
+ from patchground.cli.app import main
2
+
3
+ main()
@@ -0,0 +1,5 @@
1
+ """Safe local challenge scaffolding; never builds or runs authored code."""
2
+
3
+ from patchground.authoring.create import AuthoringError, CreatedChallenge, create_challenge
4
+
5
+ __all__ = ["AuthoringError", "CreatedChallenge", "create_challenge"]
@@ -0,0 +1,179 @@
1
+ """Create a runnable challenge from bundled, non-executable template resources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ from contextlib import ExitStack, suppress
8
+ from importlib.resources import files
9
+ from pathlib import Path, PurePosixPath
10
+ from typing import Literal
11
+
12
+ from pydantic import BaseModel, ConfigDict
13
+
14
+ from patchground import runner_assets
15
+ from patchground.spec import ManifestError, load_challenge
16
+ from patchground.spec.models import SLUG_PATTERN
17
+
18
+ TEMPLATE_FILES = (
19
+ "challenge.yaml",
20
+ "TICKET.md",
21
+ "workspace/pyproject.toml",
22
+ "workspace/src/starter.py",
23
+ "checks/visible/tests/test_starter.py",
24
+ "checks/hidden/tests/test_starter_hidden.py",
25
+ "environment/Dockerfile",
26
+ "solution/reference.patch",
27
+ )
28
+
29
+
30
+ _TYPESCRIPT_FILES = (
31
+ "challenge.yaml",
32
+ "TICKET.md",
33
+ "workspace/package.json",
34
+ "workspace/src/starter.ts",
35
+ "checks/visible/tests/starter.test.ts",
36
+ "checks/hidden/tests/starter-hidden.test.ts",
37
+ "solution/reference.patch",
38
+ )
39
+ # (type, language) -> bundled template folder and its template-backed relative files. TypeScript
40
+ # starters additionally receive the canonical runner environment from `runner_assets`.
41
+ _STARTERS = {
42
+ ("debug", "python"): ("python-debug", TEMPLATE_FILES),
43
+ ("feature", "python"): ("python-feature", TEMPLATE_FILES),
44
+ ("debug", "typescript"): ("typescript-debug", _TYPESCRIPT_FILES),
45
+ ("feature", "typescript"): ("typescript-feature", _TYPESCRIPT_FILES),
46
+ }
47
+
48
+
49
+ class AuthoringError(Exception):
50
+ """Invalid scaffold input, an occupied destination or unavailable local storage."""
51
+
52
+
53
+ class CreatedChallenge(BaseModel):
54
+ model_config = ConfigDict(frozen=True)
55
+
56
+ id: str
57
+ path: Path
58
+ type: Literal["debug", "feature"] = "debug"
59
+ language: Literal["python", "typescript"] = "python"
60
+ files: tuple[str, ...] = TEMPLATE_FILES
61
+
62
+
63
+ def _identity(parent: int, name: str) -> tuple[int, int]:
64
+ info = os.stat(name, dir_fd=parent, follow_symlinks=False)
65
+ return info.st_dev, info.st_ino
66
+
67
+
68
+ def _runner_environment() -> dict[str, bytes]:
69
+ """Canonical TypeScript environment files, verbatim and keyed by path under environment/."""
70
+ try:
71
+ assets = runner_assets.vitest_assets()
72
+ except Exception as exc:
73
+ raise AuthoringError("TypeScript runner assets are unavailable") from exc
74
+ if "Dockerfile" not in assets:
75
+ raise AuthoringError("TypeScript runner assets are incomplete")
76
+ names = set(assets)
77
+ for name, content in assets.items():
78
+ path = PurePosixPath(name)
79
+ if (
80
+ not isinstance(content, bytes)
81
+ or path.is_absolute()
82
+ or str(path) != name
83
+ or ".." in path.parts
84
+ or "\\" in name
85
+ or any(str(parent) in names for parent in path.parents)
86
+ ):
87
+ raise AuthoringError("TypeScript runner assets are invalid")
88
+ return {f"environment/{name}": content for name, content in assets.items()}
89
+
90
+
91
+ def create_challenge(
92
+ challenge_id: str, *, type_: str = "debug", language: str = "python"
93
+ ) -> CreatedChallenge:
94
+ """Reserve cwd/id exclusively; never overwrite, run a challenge or initialize Git."""
95
+ if re.fullmatch(SLUG_PATTERN, challenge_id) is None:
96
+ raise AuthoringError("id must use lowercase letters, digits and single hyphens")
97
+ if (type_, language) not in _STARTERS:
98
+ raise AuthoringError("supported templates: --type debug|feature --lang python|typescript")
99
+ folder, names = _STARTERS[(type_, language)]
100
+ template = files("patchground.authoring").joinpath("templates", folder)
101
+ contents = {
102
+ name: template.joinpath(name + ".tmpl")
103
+ .read_bytes()
104
+ .replace(b"__CHALLENGE_ID__", challenge_id.encode("ascii"))
105
+ for name in names
106
+ }
107
+ if language == "typescript":
108
+ contents.update(_runner_environment())
109
+ created: list[tuple[int, str, tuple[int, int], bool]] = []
110
+ with ExitStack() as handles:
111
+ try:
112
+ parent = os.open(".", os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
113
+ handles.callback(os.close, parent)
114
+ try:
115
+ os.mkdir(challenge_id, mode=0o700, dir_fd=parent)
116
+ except FileExistsError as exc:
117
+ raise AuthoringError(
118
+ "destination already exists; choose a new challenge id"
119
+ ) from exc
120
+ root_identity = _identity(parent, challenge_id)
121
+ created.append((parent, challenge_id, root_identity, True))
122
+ root = os.open(
123
+ challenge_id, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=parent
124
+ )
125
+ handles.callback(os.close, root)
126
+ directories: dict[tuple[str, ...], int] = {(): root}
127
+ for relative, content in contents.items():
128
+ parts = Path(relative).parts
129
+ for length in range(1, len(parts)):
130
+ key = parts[:length]
131
+ if key in directories:
132
+ continue
133
+ directory = directories[key[:-1]]
134
+ os.mkdir(key[-1], mode=0o700, dir_fd=directory)
135
+ created.append((directory, key[-1], _identity(directory, key[-1]), True))
136
+ descriptor = os.open(
137
+ key[-1], os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=directory
138
+ )
139
+ handles.callback(os.close, descriptor)
140
+ directories[key] = descriptor
141
+ directory = directories[parts[:-1]]
142
+ descriptor = os.open(
143
+ parts[-1],
144
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
145
+ 0o600,
146
+ dir_fd=directory,
147
+ )
148
+ with os.fdopen(descriptor, "wb") as stream:
149
+ info = os.fstat(stream.fileno())
150
+ created.append((directory, parts[-1], (info.st_dev, info.st_ino), False))
151
+ stream.write(content)
152
+ stream.flush()
153
+ os.fsync(stream.fileno())
154
+ for directory, name, identity, _is_directory in created:
155
+ if _identity(directory, name) != identity:
156
+ raise AuthoringError("scaffold destination changed during creation")
157
+ load_challenge(Path(challenge_id))
158
+ except BaseException as exc:
159
+ for directory, name, identity, is_directory in reversed(created):
160
+ with suppress(OSError):
161
+ if _identity(directory, name) == identity:
162
+ if is_directory:
163
+ os.rmdir(name, dir_fd=directory)
164
+ else:
165
+ os.unlink(name, dir_fd=directory)
166
+ if isinstance(exc, OSError | ManifestError):
167
+ raise AuthoringError(
168
+ "could not create the scaffold; check disk space and directory access"
169
+ ) from exc
170
+ raise
171
+ return CreatedChallenge.model_validate(
172
+ {
173
+ "id": challenge_id,
174
+ "path": Path(challenge_id),
175
+ "type": type_,
176
+ "language": language,
177
+ "files": tuple(contents),
178
+ }
179
+ )
@@ -0,0 +1,11 @@
1
+ # Line-item totals are sometimes too small
2
+
3
+ The total displayed for a list of integer amounts is sometimes smaller than expected.
4
+ An empty list should total zero, and every supplied amount should contribute exactly once.
5
+
6
+ Investigate `total` in `src/starter.py`. Keep its public API and add a regression test.
7
+ Run `pgnd check` from a session workspace to verify the fix in Docker.
8
+
9
+ This is a tiny teaching starter for challenge authors, not a curated realistic exercise.
10
+ Authors should replace its story, application, tests and reference together, then run
11
+ `pgnd validate` before sharing it.
@@ -0,0 +1,21 @@
1
+ spec_version: "0.1"
2
+ id: __CHALLENGE_ID__
3
+ version: 0.1.0
4
+ title: The last line item is missing from the total
5
+ type: debug
6
+ summary: Investigate incorrect totals without changing the public API.
7
+ languages: [python]
8
+ tags: [starter]
9
+ difficulty: beginner
10
+ timebox_minutes: 15
11
+ ai_policy: allowed
12
+ environment:
13
+ dockerfile: environment/Dockerfile
14
+ services: []
15
+ test_command: pytest -q -p no:cacheprovider --junitxml=$PG_RESULTS/junit.xml
16
+ checks:
17
+ visible: checks/visible
18
+ hidden: checks/hidden
19
+ solution: solution/reference.patch
20
+ author: {name: Challenge author}
21
+ content_license: CC-BY-4.0
@@ -0,0 +1,13 @@
1
+ from starter import total
2
+
3
+
4
+ def test_final_amount_contributes():
5
+ assert total([3, 2]) == 5
6
+
7
+
8
+ def test_single_amount():
9
+ assert total([7]) == 7
10
+
11
+
12
+ def test_negative_amounts():
13
+ assert total([4, -2]) == 2
@@ -0,0 +1,9 @@
1
+ from starter import total
2
+
3
+
4
+ def test_empty_total():
5
+ assert total([]) == 0
6
+
7
+
8
+ def test_zero_final_amount():
9
+ assert total([4, 5, 0]) == 9
@@ -0,0 +1,3 @@
1
+ FROM python:3.12-slim
2
+ ENV PYTHONDONTWRITEBYTECODE=1
3
+ RUN pip install --no-cache-dir pytest==8.3.5
@@ -0,0 +1,8 @@
1
+ diff --git a/src/starter.py b/src/starter.py
2
+ --- a/src/starter.py
3
+ +++ b/src/starter.py
4
+ @@ -1,3 +1,3 @@
5
+ def total(items: list[int]) -> int:
6
+ """Return the sum of the supplied integer amounts."""
7
+ - return sum(items[:-1])
8
+ + return sum(items)
@@ -0,0 +1,8 @@
1
+ [project]
2
+ name = "patchground-starter"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.12"
5
+
6
+ [tool.pytest.ini_options]
7
+ pythonpath = ["src"]
8
+ testpaths = ["tests"]
@@ -0,0 +1,3 @@
1
+ def total(items: list[int]) -> int:
2
+ """Return the sum of the supplied integer amounts."""
3
+ return sum(items[:-1])
@@ -0,0 +1,13 @@
1
+ # Add an average of the line-item amounts
2
+
3
+ Reports need the mean of a list of integer amounts. Implement `average` in `src/starter.py`:
4
+
5
+ - `average(items)` returns the arithmetic mean of the amounts as a `float`.
6
+ - An empty list averages `0.0` instead of raising an error.
7
+ - `total` and the public signatures stay as they are.
8
+
9
+ Add tests for your implementation. Run `pgnd check` from a session workspace to verify it in Docker.
10
+
11
+ This is a tiny teaching starter for challenge authors, not a curated realistic exercise.
12
+ Authors should replace its story, application, tests and reference together, then run
13
+ `pgnd validate` before sharing it.
@@ -0,0 +1,22 @@
1
+ spec_version: "0.1"
2
+ id: __CHALLENGE_ID__
3
+ version: 0.1.0
4
+ title: Add an average to the line-item helpers
5
+ type: feature
6
+ summary: Add the missing average helper without changing the existing total.
7
+ languages: [python]
8
+ tags: [starter]
9
+ difficulty: beginner
10
+ timebox_minutes: 15
11
+ ai_policy: allowed
12
+ environment:
13
+ dockerfile: environment/Dockerfile
14
+ services: []
15
+ test_command: pytest -q -p no:cacheprovider --junitxml=$PG_RESULTS/junit.xml
16
+ checks:
17
+ visible: checks/visible
18
+ hidden: checks/hidden
19
+ solution: solution/reference.patch
20
+ expected_paths: [src/, tests/]
21
+ author: {name: Challenge author}
22
+ content_license: CC-BY-4.0
@@ -0,0 +1,13 @@
1
+ from starter import average
2
+
3
+
4
+ def test_average_of_amounts():
5
+ assert average([2, 4]) == 3.0
6
+
7
+
8
+ def test_average_of_one_amount():
9
+ assert average([7]) == 7.0
10
+
11
+
12
+ def test_average_of_empty_list_is_zero():
13
+ assert average([]) == 0.0
@@ -0,0 +1,9 @@
1
+ from starter import average, total
2
+
3
+
4
+ def test_total_of_amounts():
5
+ assert total([4, 5, 0]) == 9
6
+
7
+
8
+ def test_average_is_exported():
9
+ assert callable(average)
@@ -0,0 +1,3 @@
1
+ FROM python:3.12-slim
2
+ ENV PYTHONDONTWRITEBYTECODE=1
3
+ RUN pip install --no-cache-dir pytest==8.3.5
@@ -0,0 +1,9 @@
1
+ diff --git a/src/starter.py b/src/starter.py
2
+ --- a/src/starter.py
3
+ +++ b/src/starter.py
4
+ @@ -4,4 +4,4 @@
5
+
6
+ def average(items: list[int]) -> float:
7
+ """Return the mean of the amounts, or 0.0 for an empty list."""
8
+ - raise NotImplementedError
9
+ + return sum(items) / len(items) if items else 0.0
@@ -0,0 +1,8 @@
1
+ [project]
2
+ name = "patchground-starter"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.12"
5
+
6
+ [tool.pytest.ini_options]
7
+ pythonpath = ["src"]
8
+ testpaths = ["tests"]
@@ -0,0 +1,8 @@
1
+ def total(items: list[int]) -> int:
2
+ """Return the sum of the supplied integer amounts."""
3
+ return sum(items)
4
+
5
+
6
+ def average(items: list[int]) -> float:
7
+ """Return the mean of the amounts, or 0.0 for an empty list."""
8
+ raise NotImplementedError
@@ -0,0 +1,11 @@
1
+ # Line-item totals are sometimes too small
2
+
3
+ The total displayed for a list of integer amounts is sometimes smaller than expected.
4
+ An empty list should total zero, and every supplied amount should contribute exactly once.
5
+
6
+ Investigate `total` in `src/starter.ts`. Keep its public API and add a regression test.
7
+ Run `pgnd check` from a session workspace to verify the fix in Docker.
8
+
9
+ This is a tiny teaching starter for challenge authors, not a curated realistic exercise.
10
+ Authors should replace its story, application, tests and reference together, then run
11
+ `pgnd validate` before sharing it.
@@ -0,0 +1,21 @@
1
+ spec_version: "0.1"
2
+ id: __CHALLENGE_ID__
3
+ version: 0.1.0
4
+ title: The last line item is missing from the total
5
+ type: debug
6
+ summary: Investigate incorrect totals without changing the public API.
7
+ languages: [typescript]
8
+ tags: [starter]
9
+ difficulty: beginner
10
+ timebox_minutes: 15
11
+ ai_policy: allowed
12
+ environment:
13
+ dockerfile: environment/Dockerfile
14
+ services: []
15
+ test_command: node /opt/patchground/vitest-runner.mjs
16
+ checks:
17
+ visible: checks/visible
18
+ hidden: checks/hidden
19
+ solution: solution/reference.patch
20
+ author: {name: Challenge author}
21
+ content_license: CC-BY-4.0
@@ -0,0 +1,14 @@
1
+ import { expect, test } from "vitest";
2
+ import { total } from "../src/starter";
3
+
4
+ test("total includes the final amount", () => {
5
+ expect(total([3, 2])).toBe(5);
6
+ });
7
+
8
+ test("total of a single amount", () => {
9
+ expect(total([7])).toBe(7);
10
+ });
11
+
12
+ test("total handles negative amounts", () => {
13
+ expect(total([4, -2])).toBe(2);
14
+ });
@@ -0,0 +1,10 @@
1
+ import { expect, test } from "vitest";
2
+ import { total } from "../src/starter";
3
+
4
+ test("total of an empty list is zero", () => {
5
+ expect(total([])).toBe(0);
6
+ });
7
+
8
+ test("total ignores a zero final amount", () => {
9
+ expect(total([4, 5, 0])).toBe(9);
10
+ });
@@ -0,0 +1,9 @@
1
+ diff --git a/src/starter.ts b/src/starter.ts
2
+ --- a/src/starter.ts
3
+ +++ b/src/starter.ts
4
+ @@ -1,4 +1,4 @@
5
+ /** Return the sum of the supplied integer amounts. */
6
+ export function total(items: number[]): number {
7
+ - return items.slice(0, -1).reduce((sum, amount) => sum + amount, 0);
8
+ + return items.reduce((sum, amount) => sum + amount, 0);
9
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "patchground-starter",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module"
6
+ }
@@ -0,0 +1,4 @@
1
+ /** Return the sum of the supplied integer amounts. */
2
+ export function total(items: number[]): number {
3
+ return items.slice(0, -1).reduce((sum, amount) => sum + amount, 0);
4
+ }
@@ -0,0 +1,13 @@
1
+ # Add an average of the line-item amounts
2
+
3
+ Reports need the mean of a list of integer amounts. Implement `average` in `src/starter.ts`:
4
+
5
+ - `average(items)` returns the arithmetic mean of the amounts as a `float`.
6
+ - An empty list averages `0.0` instead of raising an error.
7
+ - `total` and the public signatures stay as they are.
8
+
9
+ Add tests for your implementation. Run `pgnd check` from a session workspace to verify it in Docker.
10
+
11
+ This is a tiny teaching starter for challenge authors, not a curated realistic exercise.
12
+ Authors should replace its story, application, tests and reference together, then run
13
+ `pgnd validate` before sharing it.
@@ -0,0 +1,22 @@
1
+ spec_version: "0.1"
2
+ id: __CHALLENGE_ID__
3
+ version: 0.1.0
4
+ title: Add an average to the line-item helpers
5
+ type: feature
6
+ summary: Add the missing average helper without changing the existing total.
7
+ languages: [typescript]
8
+ tags: [starter]
9
+ difficulty: beginner
10
+ timebox_minutes: 15
11
+ ai_policy: allowed
12
+ environment:
13
+ dockerfile: environment/Dockerfile
14
+ services: []
15
+ test_command: node /opt/patchground/vitest-runner.mjs
16
+ checks:
17
+ visible: checks/visible
18
+ hidden: checks/hidden
19
+ solution: solution/reference.patch
20
+ expected_paths: [src/, tests/]
21
+ author: {name: Challenge author}
22
+ content_license: CC-BY-4.0
@@ -0,0 +1,14 @@
1
+ import { expect, test } from "vitest";
2
+ import { average } from "../src/starter";
3
+
4
+ test("average of the amounts", () => {
5
+ expect(average([2, 4])).toBe(3);
6
+ });
7
+
8
+ test("average of one amount", () => {
9
+ expect(average([7])).toBe(7);
10
+ });
11
+
12
+ test("average of an empty list is zero", () => {
13
+ expect(average([])).toBe(0);
14
+ });
@@ -0,0 +1,10 @@
1
+ import { expect, test } from "vitest";
2
+ import { average, total } from "../src/starter";
3
+
4
+ test("total of the amounts", () => {
5
+ expect(total([4, 5, 0])).toBe(9);
6
+ });
7
+
8
+ test("average is exported", () => {
9
+ expect(typeof average).toBe("function");
10
+ });
@@ -0,0 +1,10 @@
1
+ diff --git a/src/starter.ts b/src/starter.ts
2
+ --- a/src/starter.ts
3
+ +++ b/src/starter.ts
4
+ @@ -5,5 +5,5 @@
5
+
6
+ /** Return the mean of the amounts, or 0 for an empty list. */
7
+ export function average(items: number[]): number {
8
+ - throw new Error("not implemented");
9
+ + return items.length === 0 ? 0 : total(items) / items.length;
10
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "patchground-starter",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module"
6
+ }
@@ -0,0 +1,9 @@
1
+ /** Return the sum of the supplied integer amounts. */
2
+ export function total(items: number[]): number {
3
+ return items.reduce((sum, amount) => sum + amount, 0);
4
+ }
5
+
6
+ /** Return the mean of the amounts, or 0 for an empty list. */
7
+ export function average(items: number[]): number {
8
+ throw new Error("not implemented");
9
+ }
@@ -0,0 +1,5 @@
1
+ """Committed-input validation automation; never executes checkout code on the host."""
2
+
3
+ from patchground.automation.workflow import AutomationResult, validate_changes
4
+
5
+ __all__ = ["AutomationResult", "validate_changes"]