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.
- {scry_parse-1.2.0 → scry_parse-1.2.1}/CHANGELOG.md +23 -0
- {scry_parse-1.2.0 → scry_parse-1.2.1}/PKG-INFO +1 -1
- {scry_parse-1.2.0 → scry_parse-1.2.1}/pyproject.toml +8 -1
- {scry_parse-1.2.0 → scry_parse-1.2.1}/src/scry_parse/__init__.py +1 -1
- {scry_parse-1.2.0 → scry_parse-1.2.1}/src/scry_parse/consts.py +2 -1
- {scry_parse-1.2.0 → scry_parse-1.2.1}/src/scry_parse/validate.py +19 -2
- {scry_parse-1.2.0 → scry_parse-1.2.1}/tests/test_parse.py +108 -2
- {scry_parse-1.2.0 → scry_parse-1.2.1}/uv.lock +59 -1
- {scry_parse-1.2.0 → scry_parse-1.2.1}/.github/workflows/ci.yml +0 -0
- {scry_parse-1.2.0 → scry_parse-1.2.1}/.github/workflows/publish.yml +0 -0
- {scry_parse-1.2.0 → scry_parse-1.2.1}/.gitignore +0 -0
- {scry_parse-1.2.0 → scry_parse-1.2.1}/LICENSE +0 -0
- {scry_parse-1.2.0 → scry_parse-1.2.1}/README.md +0 -0
- {scry_parse-1.2.0 → scry_parse-1.2.1}/docs/design.scry-spec-fr15-code-block-exclusion~35c40589.md +0 -0
- {scry_parse-1.2.0 → scry_parse-1.2.1}/src/scry_parse/markers.py +0 -0
- {scry_parse-1.2.0 → scry_parse-1.2.1}/src/scry_parse/mint.py +0 -0
- {scry_parse-1.2.0 → scry_parse-1.2.1}/tests/__init__.py +0 -0
|
@@ -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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scry-parse"
|
|
7
|
-
version = "1.2.
|
|
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
|
+
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Constants for scry-spec v1.
|
|
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
|
+
"""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
|
|
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.
|
|
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
|
|
File without changes
|
{scry_parse-1.2.0 → scry_parse-1.2.1}/docs/design.scry-spec-fr15-code-block-exclusion~35c40589.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|