scry-parse 1.2.0__tar.gz → 1.2.1__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.
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.1] - 2026-09-21
9
+
10
+ ### Fixed
11
+
12
+ - **`kind: goal` added to `BASELINE_KINDS`** (scry-spec v1.2.0 FR8).
13
+ v1.2.0 added `goal` to the spec's baseline kind enumeration but
14
+ `consts.py` was not updated. `validate_marker` now correctly
15
+ recognizes `kind: goal` without emitting a "not in baseline kinds"
16
+ warning.
17
+
18
+ - **INV-GOAL-COMPLETION enforced in `validate_marker`** (scry-spec
19
+ v1.2.0). `status: met` on a `kind: goal` marker now produces
20
+ `valid=False` with an explicit `INV-GOAL-COMPLETION` error.
21
+ Met-ness is computed from the deliverable set, never declared.
22
+ The redundant "status 'met' not in baseline statuses" warning is
23
+ suppressed when the INV error fires.
24
+
25
+ - **Module docstrings** updated from v1.0/v1.1 to v1.2 in `consts.py`
26
+ and `validate.py`.
27
+
28
+ - **122/122 tests pass** (4 new tests for goal kind and
29
+ INV-GOAL-COMPLETION).
30
+
8
31
  ## [1.2.0] - 2026-09-21
9
32
 
10
33
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: scry-parse
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Python parser for scry-spec v1.2 markers
5
5
  Project-URL: Repository, https://github.com/prmichaelsen/scry-parse-py
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "scry-parse"
7
- version = "1.2.0"
7
+ version = "1.2.1"
8
8
  description = "Python parser for scry-spec v1.2 markers"
9
9
  readme = "README.md"
10
10
  license = {file = "LICENSE"}
@@ -22,3 +22,10 @@ packages = ["src/scry_parse"]
22
22
 
23
23
  [tool.pytest.ini_options]
24
24
  testpaths = ["tests"]
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "anyio>=4.15.1",
29
+ "pytest>=9.0.3",
30
+ "pytest-asyncio>=1.4.0",
31
+ ]
@@ -47,4 +47,4 @@ __all__ = [
47
47
  "BASELINE_STATUSES",
48
48
  ]
49
49
 
50
- __version__ = "1.2.0"
50
+ __version__ = "1.2.1"
@@ -1,4 +1,4 @@
1
- """Constants for scry-spec v1.0: baseline kinds, statuses, and ID regexes."""
1
+ """Constants for scry-spec v1.2: baseline kinds, statuses, and ID regexes."""
2
2
  import re
3
3
 
4
4
  BASELINE_KINDS = (
@@ -6,6 +6,7 @@ BASELINE_KINDS = (
6
6
  "task", "milestone",
7
7
  "report", "audit", "research",
8
8
  "code",
9
+ "goal", # scry-spec v1.2.0 FR8 — declares an objective
9
10
  )
10
11
 
11
12
  BASELINE_STATUSES = ("draft", "active", "deprecated")
@@ -1,4 +1,4 @@
1
- """Validation for parsed scry markers per scry-spec v1.1."""
1
+ """Validation for parsed scry markers per scry-spec v1.2."""
2
2
  from __future__ import annotations
3
3
 
4
4
  from dataclasses import dataclass
@@ -67,10 +67,27 @@ def _validate_entry(marker: EntryMarker) -> ValidationResult:
67
67
  if not marker.summary or not marker.summary.strip():
68
68
  errors.append("summary must be non-empty")
69
69
 
70
+ # INV-GOAL-COMPLETION (scry-spec v1.2.0): status: met MUST NOT be hand-set
71
+ # on kind: goal markers. Met-ness is computed from the deliverable set;
72
+ # "abandoned" is the only valid terminal status an author may write on a
73
+ # goal marker. Validators MUST reject any marker with kind=goal +
74
+ # status=met.
75
+ if marker.kind == "goal" and marker.status == "met":
76
+ errors.append(
77
+ "INV-GOAL-COMPLETION (scry-spec v1.2.0): status 'met' must not "
78
+ "be set on a kind=goal marker — met-ness is computed from the "
79
+ "deliverable set, never declared. Use 'abandoned' to close a goal."
80
+ )
81
+
70
82
  # status must be non-empty; warn if not in BASELINE_STATUSES
71
83
  if not marker.status or not marker.status.strip():
72
84
  errors.append("status must be a non-empty string")
73
- elif marker.status not in BASELINE_STATUSES:
85
+ elif marker.status not in BASELINE_STATUSES and not (
86
+ marker.kind == "goal" and marker.status == "met"
87
+ ):
88
+ # status:met on kind:goal is already an error (INV-GOAL-COMPLETION
89
+ # above); suppress the redundant "not in baseline statuses" warning
90
+ # to keep diagnostic output clean.
74
91
  warnings.append(
75
92
  f"status {marker.status!r} is not in the baseline statuses list; "
76
93
  f"expected one of: {', '.join(BASELINE_STATUSES)}"
@@ -717,10 +717,12 @@ def test_validate_binding_empty_ref():
717
717
  # consts tests
718
718
  # ---------------------------------------------------------------------------
719
719
 
720
- def test_baseline_kinds_contains_all_v1_kinds():
720
+ def test_baseline_kinds_contains_all_v12_kinds():
721
+ """BASELINE_KINDS must contain all scry-spec v1.2 kinds (v1.0 set + goal)."""
721
722
  expected = {
722
723
  "design", "pattern", "spec", "lesson", "internal",
723
- "task", "milestone", "report", "audit", "research", "code"
724
+ "task", "milestone", "report", "audit", "research", "code",
725
+ "goal", # scry-spec v1.2.0 FR8
724
726
  }
725
727
  assert set(BASELINE_KINDS) == expected
726
728
 
@@ -2257,3 +2259,107 @@ summary: Test
2257
2259
  assert len(result.entries) == 1
2258
2260
  e = result.entries[0]
2259
2261
  assert e.satisfies == []
2262
+
2263
+
2264
+ # ---------------------------------------------------------------------------
2265
+ # scry-spec v1.2.0 — kind=goal baseline recognition + INV-GOAL-COMPLETION
2266
+ # ---------------------------------------------------------------------------
2267
+
2268
+ def test_goal_kind_is_baseline_kind():
2269
+ """kind=goal is a scry-spec v1.2.0 baseline kind — validator MUST NOT warn."""
2270
+ content = """\
2271
+ <!-- @scry.entry
2272
+ id: goal.reduce-churn~abcd1234
2273
+ kind: goal
2274
+ summary: Reduce monthly churn to below 2% by end of Q4.
2275
+ status: active
2276
+ weight: 0.8
2277
+ @scry.entry.end -->
2278
+ """
2279
+ result = parse_markers(content)
2280
+ assert len(result.entries) == 1
2281
+ e = result.entries[0]
2282
+ assert e.kind == "goal"
2283
+ vr = validate_marker(e)
2284
+ assert vr.valid is True
2285
+ kind_warnings = [w for w in vr.warnings if "kind" in w.lower() and "goal" in w.lower()]
2286
+ assert not kind_warnings, (
2287
+ f"validate_marker emitted unexpected kind warning for kind=goal: {kind_warnings}"
2288
+ )
2289
+
2290
+
2291
+ def test_inv_goal_completion_met_is_error():
2292
+ """INV-GOAL-COMPLETION: status=met on kind=goal MUST be rejected as an error."""
2293
+ content = """\
2294
+ <!-- @scry.entry
2295
+ id: goal.reduce-churn~abcd1234
2296
+ kind: goal
2297
+ summary: Reduce monthly churn to below 2% by end of Q4.
2298
+ status: met
2299
+ weight: 0.8
2300
+ @scry.entry.end -->
2301
+ """
2302
+ result = parse_markers(content)
2303
+ assert len(result.entries) == 1, "Entry should still parse (parse != validate)"
2304
+ e = result.entries[0]
2305
+ assert e.kind == "goal"
2306
+ assert e.status == "met"
2307
+ vr = validate_marker(e)
2308
+ assert vr.valid is False, (
2309
+ "validate_marker should return valid=False for kind=goal status=met "
2310
+ "(INV-GOAL-COMPLETION)"
2311
+ )
2312
+ inv_errors = [err for err in vr.errors if "INV-GOAL-COMPLETION" in err]
2313
+ assert inv_errors, (
2314
+ f"Expected an INV-GOAL-COMPLETION error; got errors: {vr.errors}"
2315
+ )
2316
+ # The redundant "status 'met' not in baseline" warning should be suppressed
2317
+ redundant_warnings = [w for w in vr.warnings if "met" in w and "baseline" in w]
2318
+ assert not redundant_warnings, (
2319
+ f"Redundant 'met not in baseline' warning should be suppressed when "
2320
+ f"INV-GOAL-COMPLETION fires; got: {redundant_warnings}"
2321
+ )
2322
+
2323
+
2324
+ def test_inv_goal_completion_abandoned_is_ok():
2325
+ """status=abandoned on kind=goal is valid — only terminal author-writable status."""
2326
+ content = """\
2327
+ <!-- @scry.entry
2328
+ id: goal.reduce-churn~abcd1234
2329
+ kind: goal
2330
+ summary: Reduce monthly churn.
2331
+ status: active
2332
+ weight: 0.5
2333
+ @scry.entry.end -->
2334
+ """
2335
+ result = parse_markers(content)
2336
+ e = result.entries[0]
2337
+ vr = validate_marker(e)
2338
+ assert vr.valid is True
2339
+ assert not any("INV-GOAL-COMPLETION" in w for w in vr.errors)
2340
+
2341
+
2342
+ def test_inv_goal_completion_only_fires_on_goal_kind():
2343
+ """status=met on a non-goal kind is a warning (unknown status), not INV error."""
2344
+ content = """\
2345
+ <!-- @scry.entry
2346
+ id: design.foo~abcd1234
2347
+ kind: design
2348
+ summary: Some design doc.
2349
+ status: met
2350
+ weight: 0.5
2351
+ @scry.entry.end -->
2352
+ """
2353
+ result = parse_markers(content)
2354
+ e = result.entries[0]
2355
+ vr = validate_marker(e)
2356
+ # No INV-GOAL-COMPLETION error (kind != goal)
2357
+ inv_errors = [err for err in vr.errors if "INV-GOAL-COMPLETION" in err]
2358
+ assert not inv_errors, (
2359
+ f"INV-GOAL-COMPLETION should only fire on kind=goal; got: {inv_errors}"
2360
+ )
2361
+ # But status=met IS an unknown status warning for non-goal kinds
2362
+ status_warnings = [w for w in vr.warnings if "met" in w]
2363
+ assert status_warnings, (
2364
+ "Expected a 'met not in baseline statuses' warning for kind=design status=met"
2365
+ )
@@ -2,6 +2,19 @@ version = 1
2
2
  revision = 3
3
3
  requires-python = ">=3.11"
4
4
 
5
+ [[package]]
6
+ name = "anyio"
7
+ version = "4.15.1"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ dependencies = [
10
+ { name = "idna" },
11
+ { name = "typing-extensions", marker = "python_full_version < '3.15'" },
12
+ ]
13
+ sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" }
14
+ wheels = [
15
+ { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" },
16
+ ]
17
+
5
18
  [[package]]
6
19
  name = "colorama"
7
20
  version = "0.4.6"
@@ -11,6 +24,15 @@ wheels = [
11
24
  { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
12
25
  ]
13
26
 
27
+ [[package]]
28
+ name = "idna"
29
+ version = "3.20"
30
+ source = { registry = "https://pypi.org/simple" }
31
+ sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" }
32
+ wheels = [
33
+ { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" },
34
+ ]
35
+
14
36
  [[package]]
15
37
  name = "iniconfig"
16
38
  version = "2.3.0"
@@ -63,6 +85,19 @@ wheels = [
63
85
  { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
64
86
  ]
65
87
 
88
+ [[package]]
89
+ name = "pytest-asyncio"
90
+ version = "1.4.0"
91
+ source = { registry = "https://pypi.org/simple" }
92
+ dependencies = [
93
+ { name = "pytest" },
94
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
95
+ ]
96
+ sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" }
97
+ wheels = [
98
+ { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" },
99
+ ]
100
+
66
101
  [[package]]
67
102
  name = "pyyaml"
68
103
  version = "6.0.3"
@@ -120,7 +155,7 @@ wheels = [
120
155
 
121
156
  [[package]]
122
157
  name = "scry-parse"
123
- version = "1.2.0"
158
+ version = "1.2.1"
124
159
  source = { editable = "." }
125
160
  dependencies = [
126
161
  { name = "pyyaml" },
@@ -131,9 +166,32 @@ dev = [
131
166
  { name = "pytest" },
132
167
  ]
133
168
 
169
+ [package.dev-dependencies]
170
+ dev = [
171
+ { name = "anyio" },
172
+ { name = "pytest" },
173
+ { name = "pytest-asyncio" },
174
+ ]
175
+
134
176
  [package.metadata]
135
177
  requires-dist = [
136
178
  { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" },
137
179
  { name = "pyyaml", specifier = ">=6.0" },
138
180
  ]
139
181
  provides-extras = ["dev"]
182
+
183
+ [package.metadata.requires-dev]
184
+ dev = [
185
+ { name = "anyio", specifier = ">=4.15.1" },
186
+ { name = "pytest", specifier = ">=9.0.3" },
187
+ { name = "pytest-asyncio", specifier = ">=1.4.0" },
188
+ ]
189
+
190
+ [[package]]
191
+ name = "typing-extensions"
192
+ version = "4.16.0"
193
+ source = { registry = "https://pypi.org/simple" }
194
+ sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
195
+ wheels = [
196
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
197
+ ]
File without changes
File without changes
File without changes
File without changes