r2s 0.2.1__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.
r2s/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ """r2s — reference runtime for the research-to-ship schema contract."""
2
+
3
+ from r2s.errors import KillError, ScoreError, ValidationError
4
+ from r2s.handoff import handoff
5
+ from r2s.io import load_board, load_card, load_json
6
+ from r2s.rank import rank
7
+ from r2s.score import score, should_kill
8
+
9
+ __all__ = [
10
+ "KillError",
11
+ "ScoreError",
12
+ "ValidationError",
13
+ "handoff",
14
+ "load_board",
15
+ "load_card",
16
+ "load_json",
17
+ "rank",
18
+ "score",
19
+ "should_kill",
20
+ ]
21
+
22
+ __version__ = "0.2.1"
r2s/cli.py ADDED
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+
7
+ from r2s.handoff import handoff
8
+ from r2s.io import load_board, load_card
9
+ from r2s.rank import rank
10
+ from r2s.score import score, should_kill
11
+
12
+
13
+ def main(argv: list[str] | None = None) -> int:
14
+ parser = argparse.ArgumentParser(prog="r2s", description="r2s reference runtime CLI")
15
+ sub = parser.add_subparsers(dest="cmd", required=True)
16
+
17
+ p_rank = sub.add_parser("rank", help="Rank cards on a board")
18
+ p_rank.add_argument("board", help="Path to board JSON")
19
+
20
+ p_score = sub.add_parser("score", help="Score a single card")
21
+ p_score.add_argument("card", help="Path to card JSON")
22
+
23
+ p_hand = sub.add_parser("handoff", help="Emit handoff JSON for a card")
24
+ p_hand.add_argument("card", help="Path to card JSON")
25
+ p_hand.add_argument("--draft", help="Optional draft JSON with handoff fields")
26
+
27
+ args = parser.parse_args(argv)
28
+
29
+ if args.cmd == "rank":
30
+ board = load_board(args.board)
31
+ ranked = rank(board)
32
+ out = []
33
+ for card in ranked:
34
+ killed, reason = should_kill(card)
35
+ item = {"id": card["id"], "score": score(card), "status": card["status"]}
36
+ if killed:
37
+ item["kill_reason"] = reason
38
+ out.append(item)
39
+ json.dump(out, sys.stdout, indent=2)
40
+ sys.stdout.write("\n")
41
+ return 0
42
+
43
+ if args.cmd == "score":
44
+ card = load_card(args.card)
45
+ killed, reason = should_kill(card)
46
+ payload = {"id": card["id"], "score": score(card), "killed": killed, "kill_reason": reason}
47
+ json.dump(payload, sys.stdout, indent=2)
48
+ sys.stdout.write("\n")
49
+ return 0
50
+
51
+ if args.cmd == "handoff":
52
+ card = load_card(args.card)
53
+ draft = None
54
+ if args.draft:
55
+ draft = json.loads(open(args.draft, encoding="utf-8").read())
56
+ packet = handoff(card, draft=draft)
57
+ json.dump(packet, sys.stdout, indent=2)
58
+ sys.stdout.write("\n")
59
+ return 0
60
+
61
+ return 2
62
+
63
+
64
+ if __name__ == "__main__":
65
+ raise SystemExit(main())
r2s/errors.py ADDED
@@ -0,0 +1,14 @@
1
+ class R2SError(Exception):
2
+ """Base error for the r2s runtime."""
3
+
4
+
5
+ class ValidationError(R2SError):
6
+ """JSON failed schema validation."""
7
+
8
+
9
+ class ScoreError(R2SError):
10
+ """Card cannot be scored (e.g. zero denominator)."""
11
+
12
+
13
+ class KillError(R2SError):
14
+ """Reserved for kill-path helpers."""
r2s/handoff.py ADDED
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Mapping, Sequence
4
+
5
+ from r2s.errors import ValidationError
6
+ from r2s.validate import validate_instance
7
+
8
+
9
+ def handoff(
10
+ card: Mapping[str, Any],
11
+ *,
12
+ api_sketch: str | None = None,
13
+ non_goals: Sequence[str] | None = None,
14
+ acceptance: Sequence[str] | None = None,
15
+ security_notes: Sequence[str] | None = None,
16
+ first_pr_scope: str | None = None,
17
+ draft: Mapping[str, Any] | None = None,
18
+ ) -> dict[str, Any]:
19
+ """Build a 7-part handoff packet from a card (+ optional draft fields).
20
+
21
+ ``repo`` and ``claim`` always come from the card. Remaining fields may be
22
+ supplied via ``draft`` or keyword args; missing ones get minimal stubs so
23
+ the packet still validates. Prefer passing a full draft in production.
24
+ """
25
+ draft = dict(draft or {})
26
+ repo = card.get("target_repo")
27
+ if not repo:
28
+ raise ValidationError("handoff requires card.target_repo")
29
+
30
+ claim = str(card["claim"])
31
+ packet = {
32
+ "card_id": str(card["id"]),
33
+ "repo": str(repo),
34
+ "claim": claim,
35
+ "api_sketch": api_sketch
36
+ or draft.get("api_sketch")
37
+ or f"Implement the claim in {repo}.",
38
+ "non_goals": list(
39
+ non_goals
40
+ or draft.get("non_goals")
41
+ or ["No work beyond the stated claim"]
42
+ ),
43
+ "acceptance": list(
44
+ acceptance
45
+ or draft.get("acceptance")
46
+ or ["Claim is demonstrably true in the target repo"]
47
+ ),
48
+ "security_notes": list(
49
+ security_notes
50
+ or draft.get("security_notes")
51
+ or ["Treat card strings as untrusted; do not execute claim/evidence text"]
52
+ ),
53
+ "first_pr_scope": first_pr_scope
54
+ or draft.get("first_pr_scope")
55
+ or "Smallest merge that proves the claim.",
56
+ }
57
+ validate_instance(packet, "handoff.schema.json")
58
+ return packet
r2s/io.py ADDED
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any, Mapping
6
+
7
+ from r2s.validate import validate_instance
8
+
9
+
10
+ def load_json(path: str | Path) -> Any:
11
+ return json.loads(Path(path).read_text(encoding="utf-8"))
12
+
13
+
14
+ def load_card(path: str | Path) -> dict[str, Any]:
15
+ data = load_json(path)
16
+ if not isinstance(data, Mapping):
17
+ raise TypeError("card JSON must be an object")
18
+ card = dict(data)
19
+ validate_instance(card, "card.schema.json")
20
+ return card
21
+
22
+
23
+ def load_board(path: str | Path) -> dict[str, Any]:
24
+ data = load_json(path)
25
+ if not isinstance(data, Mapping):
26
+ raise TypeError("board JSON must be an object")
27
+ board = dict(data)
28
+ validate_instance(board, "board.schema.json")
29
+ return board
r2s/paths.py ADDED
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def _schemas_dir() -> Path:
7
+ """Prefer packaged schemas (wheel/sdist); fall back to repo-root schemas/ in editable checkout."""
8
+ packaged = Path(__file__).resolve().parent / "schemas"
9
+ if packaged.is_dir() and any(packaged.glob("*.schema.json")):
10
+ return packaged
11
+ repo = Path(__file__).resolve().parents[2] / "schemas"
12
+ if repo.is_dir() and any(repo.glob("*.schema.json")):
13
+ return repo
14
+ raise FileNotFoundError(
15
+ "r2s schemas not found (expected package data src/r2s/schemas or repo schemas/)"
16
+ )
17
+
18
+
19
+ SCHEMAS_DIR = _schemas_dir()
20
+
21
+
22
+ def schema_path(name: str) -> Path:
23
+ return SCHEMAS_DIR / name
r2s/rank.py ADDED
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Mapping
4
+
5
+ from r2s.score import score, should_kill
6
+
7
+
8
+ def rank(board: Mapping[str, Any]) -> list[dict[str, Any]]:
9
+ """Rank cards by descending S, tie-break ascending id.
10
+
11
+ Cards that fail kill rules or are unscorable are omitted from the ranked
12
+ list. Among remaining cards, at most ``max_active_builds`` cards with
13
+ status ``build`` are kept (highest S first); other statuses are uncapped
14
+ except by overall sort order for presentation.
15
+ """
16
+ max_builds = int(board.get("max_active_builds") or 2)
17
+ cards = [dict(c) for c in board.get("cards", [])]
18
+
19
+ scored: list[tuple[float, str, dict[str, Any]]] = []
20
+ for card in cards:
21
+ killed, _ = should_kill(card)
22
+ if killed:
23
+ continue
24
+ try:
25
+ s = score(card)
26
+ except Exception:
27
+ continue
28
+ scored.append((s, str(card["id"]), card))
29
+
30
+ scored.sort(key=lambda t: (-t[0], t[1]))
31
+
32
+ result: list[dict[str, Any]] = []
33
+ builds = 0
34
+ for s, _id, card in scored:
35
+ if card.get("status") == "build":
36
+ if builds >= max_builds:
37
+ continue
38
+ builds += 1
39
+ result.append(card)
40
+ return result
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ao3575911.github.io/r2s/schemas/board.schema.json",
4
+ "title": "R2S Board",
5
+ "description": "Hypothesis board: a set of cards plus ranking policy.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "version",
10
+ "cards"
11
+ ],
12
+ "properties": {
13
+ "version": {
14
+ "type": "string",
15
+ "const": "0.1.0"
16
+ },
17
+ "max_active_builds": {
18
+ "type": "integer",
19
+ "minimum": 1,
20
+ "default": 2
21
+ },
22
+ "cards": {
23
+ "type": "array",
24
+ "items": {
25
+ "$ref": "card.schema.json"
26
+ }
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,174 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ao3575911.github.io/r2s/schemas/card.schema.json",
4
+ "title": "R2S Card",
5
+ "description": "Normalized research signal for the research-to-ship loop.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "id",
10
+ "claim",
11
+ "evidence",
12
+ "novelty",
13
+ "evidence_strength",
14
+ "usability",
15
+ "dependency_fit",
16
+ "hours",
17
+ "compute_cost",
18
+ "blast_radius",
19
+ "status"
20
+ ],
21
+ "properties": {
22
+ "id": {
23
+ "type": "string",
24
+ "minLength": 1,
25
+ "description": "Stable card id (slug)."
26
+ },
27
+ "claim": {
28
+ "type": "string",
29
+ "minLength": 1,
30
+ "description": "One sentence the eventual code must make true."
31
+ },
32
+ "evidence": {
33
+ "type": "array",
34
+ "minItems": 0,
35
+ "items": {
36
+ "type": "object",
37
+ "additionalProperties": false,
38
+ "required": [
39
+ "kind",
40
+ "ref"
41
+ ],
42
+ "properties": {
43
+ "kind": {
44
+ "type": "string",
45
+ "enum": [
46
+ "paper",
47
+ "repo",
48
+ "doc",
49
+ "spike",
50
+ "outcome",
51
+ "other"
52
+ ]
53
+ },
54
+ "ref": {
55
+ "type": "string",
56
+ "minLength": 1
57
+ },
58
+ "note": {
59
+ "type": "string"
60
+ }
61
+ }
62
+ }
63
+ },
64
+ "earn_path": {
65
+ "type": "object",
66
+ "additionalProperties": false,
67
+ "properties": {
68
+ "who_pays": {
69
+ "type": "string"
70
+ },
71
+ "wedge": {
72
+ "type": "string"
73
+ },
74
+ "cheapest_test": {
75
+ "type": "string"
76
+ }
77
+ }
78
+ },
79
+ "target_repo": {
80
+ "type": "string",
81
+ "pattern": "^[^/\\s]+/[^/\\s]+$",
82
+ "description": "owner/name when status is design or later."
83
+ },
84
+ "novelty": {
85
+ "type": "number",
86
+ "minimum": 0,
87
+ "maximum": 1
88
+ },
89
+ "evidence_strength": {
90
+ "type": "number",
91
+ "minimum": 0,
92
+ "maximum": 1
93
+ },
94
+ "usability": {
95
+ "type": "number",
96
+ "minimum": 0,
97
+ "maximum": 1
98
+ },
99
+ "dependency_fit": {
100
+ "type": "number",
101
+ "minimum": 0,
102
+ "maximum": 1
103
+ },
104
+ "hours": {
105
+ "type": "number",
106
+ "exclusiveMinimum": 0
107
+ },
108
+ "compute_cost": {
109
+ "type": "number",
110
+ "minimum": 0
111
+ },
112
+ "blast_radius": {
113
+ "type": "number",
114
+ "minimum": 0
115
+ },
116
+ "status": {
117
+ "type": "string",
118
+ "enum": [
119
+ "sense",
120
+ "design",
121
+ "build",
122
+ "ship",
123
+ "learn",
124
+ "killed"
125
+ ]
126
+ },
127
+ "kill_reason": {
128
+ "type": "string"
129
+ },
130
+ "notes": {
131
+ "type": "string"
132
+ }
133
+ },
134
+ "allOf": [
135
+ {
136
+ "if": {
137
+ "properties": {
138
+ "status": {
139
+ "enum": [
140
+ "design",
141
+ "build",
142
+ "ship"
143
+ ]
144
+ }
145
+ },
146
+ "required": [
147
+ "status"
148
+ ]
149
+ },
150
+ "then": {
151
+ "required": [
152
+ "target_repo"
153
+ ]
154
+ }
155
+ },
156
+ {
157
+ "if": {
158
+ "properties": {
159
+ "status": {
160
+ "const": "killed"
161
+ }
162
+ },
163
+ "required": [
164
+ "status"
165
+ ]
166
+ },
167
+ "then": {
168
+ "required": [
169
+ "kill_reason"
170
+ ]
171
+ }
172
+ }
173
+ ]
174
+ }
@@ -0,0 +1,60 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ao3575911.github.io/r2s/schemas/handoff.schema.json",
4
+ "title": "R2S Handoff Packet",
5
+ "description": "Fixed 7-part ship handoff for Builder (or any implementer).",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "repo",
10
+ "claim",
11
+ "api_sketch",
12
+ "non_goals",
13
+ "acceptance",
14
+ "security_notes",
15
+ "first_pr_scope"
16
+ ],
17
+ "properties": {
18
+ "card_id": {
19
+ "type": "string"
20
+ },
21
+ "repo": {
22
+ "type": "string",
23
+ "pattern": "^[^/\\s]+/[^/\\s]+$"
24
+ },
25
+ "claim": {
26
+ "type": "string",
27
+ "minLength": 1
28
+ },
29
+ "api_sketch": {
30
+ "type": "string",
31
+ "minLength": 1,
32
+ "description": "Public surface the outside world sees (for a spec repo: schemas and docs paths)."
33
+ },
34
+ "non_goals": {
35
+ "type": "array",
36
+ "items": {
37
+ "type": "string"
38
+ },
39
+ "minItems": 1
40
+ },
41
+ "acceptance": {
42
+ "type": "array",
43
+ "items": {
44
+ "type": "string"
45
+ },
46
+ "minItems": 1
47
+ },
48
+ "security_notes": {
49
+ "type": "array",
50
+ "items": {
51
+ "type": "string"
52
+ },
53
+ "minItems": 1
54
+ },
55
+ "first_pr_scope": {
56
+ "type": "string",
57
+ "minLength": 1
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,82 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ao3575911.github.io/r2s/schemas/outcome.schema.json",
4
+ "title": "R2S Outcome",
5
+ "description": "Closed-loop result used to update ranking judgment (human-applied in v0).",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "card_id",
10
+ "result",
11
+ "recorded_at"
12
+ ],
13
+ "properties": {
14
+ "card_id": {
15
+ "type": "string",
16
+ "minLength": 1
17
+ },
18
+ "result": {
19
+ "type": "string",
20
+ "enum": [
21
+ "ship",
22
+ "kill",
23
+ "learn"
24
+ ]
25
+ },
26
+ "recorded_at": {
27
+ "type": "string",
28
+ "format": "date-time"
29
+ },
30
+ "time_to_green_hours": {
31
+ "type": "number",
32
+ "minimum": 0
33
+ },
34
+ "second_consumer": {
35
+ "type": "boolean",
36
+ "description": "Did a second module/repo consume the API?"
37
+ },
38
+ "earn_hypothesis_held": {
39
+ "type": "boolean"
40
+ },
41
+ "notes": {
42
+ "type": "string"
43
+ },
44
+ "kill_reason": {
45
+ "type": "string"
46
+ },
47
+ "commit_sha": {
48
+ "type": "string",
49
+ "pattern": "^[0-9a-f]{40}$",
50
+ "description": "Git commit SHA that realizes this outcome (usually the merge or tip commit)."
51
+ },
52
+ "merge_commit_sha": {
53
+ "type": "string",
54
+ "pattern": "^[0-9a-f]{40}$",
55
+ "description": "Merge commit SHA when the ship landed via PR."
56
+ },
57
+ "pr": {
58
+ "type": "integer",
59
+ "minimum": 1,
60
+ "description": "Pull request number when applicable."
61
+ }
62
+ },
63
+ "allOf": [
64
+ {
65
+ "if": {
66
+ "properties": {
67
+ "result": {
68
+ "const": "kill"
69
+ }
70
+ },
71
+ "required": [
72
+ "result"
73
+ ]
74
+ },
75
+ "then": {
76
+ "required": [
77
+ "kill_reason"
78
+ ]
79
+ }
80
+ }
81
+ ]
82
+ }
@@ -0,0 +1,52 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ao3575911.github.io/r2s/schemas/scoring.schema.json",
4
+ "title": "R2S Scoring Rules",
5
+ "description": "Normative scoring and kill rules for implementers.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "formula",
10
+ "kill_rules",
11
+ "rank_policy"
12
+ ],
13
+ "properties": {
14
+ "formula": {
15
+ "type": "string",
16
+ "const": "S = (novelty * evidence_strength * usability * dependency_fit) / (hours + compute_cost + blast_radius)"
17
+ },
18
+ "denominator_guard": {
19
+ "type": "string",
20
+ "description": "If hours + compute_cost + blast_radius == 0, treat as invalid card (do not score)."
21
+ },
22
+ "kill_rules": {
23
+ "type": "array",
24
+ "items": {
25
+ "type": "string"
26
+ },
27
+ "minItems": 1
28
+ },
29
+ "rank_policy": {
30
+ "type": "object",
31
+ "additionalProperties": false,
32
+ "required": [
33
+ "order",
34
+ "tie_break"
35
+ ],
36
+ "properties": {
37
+ "order": {
38
+ "type": "string",
39
+ "const": "descending_S"
40
+ },
41
+ "tie_break": {
42
+ "type": "string",
43
+ "const": "ascending_id"
44
+ },
45
+ "max_active_builds": {
46
+ "type": "integer",
47
+ "minimum": 1
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
r2s/score.py ADDED
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Mapping
4
+
5
+ from r2s.errors import ScoreError
6
+
7
+ # Kill threshold: "near 0" in SPEC — treat values <= epsilon as near zero.
8
+ _NEAR_ZERO = 1e-9
9
+ _DESIGN_PLUS = frozenset({"design", "build", "ship"})
10
+
11
+
12
+ def score(card: Mapping[str, Any]) -> float:
13
+ """Return S = (N*E*U*D) / (H+C+B). Raises ScoreError if denominator is 0."""
14
+ n = float(card["novelty"])
15
+ e = float(card["evidence_strength"])
16
+ u = float(card["usability"])
17
+ d = float(card["dependency_fit"])
18
+ h = float(card["hours"])
19
+ c = float(card["compute_cost"])
20
+ b = float(card["blast_radius"])
21
+ denom = h + c + b
22
+ if denom == 0:
23
+ raise ScoreError("hours + compute_cost + blast_radius == 0; card is unscorable")
24
+ return (n * e * u * d) / denom
25
+
26
+
27
+ def should_kill(card: Mapping[str, Any]) -> tuple[bool, str | None]:
28
+ """Apply SPEC kill rules. Returns (killed?, reason)."""
29
+ if float(card["usability"]) <= _NEAR_ZERO:
30
+ return True, "usability near 0"
31
+ if float(card["dependency_fit"]) <= _NEAR_ZERO:
32
+ return True, "dependency_fit near 0"
33
+ status = card["status"]
34
+ if status in _DESIGN_PLUS and not card.get("target_repo"):
35
+ return True, "missing target_repo at design+"
36
+ return False, None
r2s/validate.py ADDED
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from functools import lru_cache
5
+ from pathlib import Path
6
+ from typing import Any, Mapping
7
+
8
+ from jsonschema import Draft202012Validator
9
+ from referencing import Registry, Resource
10
+
11
+ from r2s.errors import ValidationError
12
+ from r2s.paths import SCHEMAS_DIR
13
+
14
+
15
+ @lru_cache(maxsize=1)
16
+ def _registry() -> Registry:
17
+ registry: Registry = Registry()
18
+ for path in sorted(SCHEMAS_DIR.glob("*.schema.json")):
19
+ data = json.loads(path.read_text(encoding="utf-8"))
20
+ resource = Resource.from_contents(data)
21
+ registry = registry.with_resource(str(data["$id"]), resource)
22
+ registry = registry.with_resource(path.name, resource)
23
+ return registry
24
+
25
+
26
+ def validate_instance(instance: Mapping[str, Any], schema_filename: str) -> None:
27
+ schema_file = SCHEMAS_DIR / schema_filename
28
+ if not schema_file.is_file():
29
+ raise ValidationError(f"missing schema: {schema_filename}")
30
+ schema = json.loads(schema_file.read_text(encoding="utf-8"))
31
+ validator = Draft202012Validator(schema, registry=_registry())
32
+ errors = sorted(validator.iter_errors(instance), key=lambda e: list(e.path))
33
+ if errors:
34
+ first = errors[0]
35
+ path = "/".join(str(p) for p in first.path) or "(root)"
36
+ raise ValidationError(f"{schema_filename} @ {path}: {first.message}")
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: r2s
3
+ Version: 0.2.1
4
+ Summary: Reference runtime for the r2s research-to-ship JSON Schema contract
5
+ Author: ao3575911
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ao3575911/r2s
8
+ Project-URL: Repository, https://github.com/ao3575911/r2s
9
+ Project-URL: Changelog, https://github.com/ao3575911/r2s/blob/main/CHANGELOG.md
10
+ Project-URL: Issues, https://github.com/ao3575911/r2s/issues
11
+ Project-URL: Documentation, https://github.com/ao3575911/r2s/blob/main/docs/SPEC.md
12
+ Keywords: json-schema,research,handoff,ranking,specification
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: jsonschema<5,>=4.22
25
+ Requires-Dist: referencing<1,>=0.35
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest<9,>=8; extra == "dev"
28
+ Requires-Dist: build<2,>=1.2; extra == "dev"
29
+ Requires-Dist: twine<7,>=5; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ <p align="center">
33
+ <img src="docs/brand/icon.png" alt="r2s" width="96" height="96" />
34
+ </p>
35
+
36
+ <h1 align="center">r2s</h1>
37
+
38
+ <p align="center">
39
+ <strong>↻ research → rank → ship</strong><br />
40
+ JSON Schema contract for turning research signals into repo and library work.
41
+ </p>
42
+
43
+ <p align="center">
44
+ <img src="docs/brand/loop.gif" alt="sense → rank → handoff → ship" width="640" />
45
+ </p>
46
+
47
+ <p align="center">
48
+ <a href="docs/SPEC.md"><img src="https://img.shields.io/badge/spec-v0.1.0-5eead4?style=flat-square" alt="spec v0.1.0" /></a>
49
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-0f1115?style=flat-square" alt="MIT" /></a>
50
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-SHA%20verified-5eead4?style=flat-square" alt="changelog" /></a>
51
+ <img src="https://img.shields.io/badge/runtime-Python%203.11%2B-8b949e?style=flat-square" alt="Python runtime" />
52
+ </p>
53
+
54
+ ---
55
+
56
+ ## What this is
57
+
58
+ **r2s** is a small, versioned contract — not an agent and not a framework.
59
+
60
+ You normalize research into **cards**, **rank** them with an explicit score, emit a fixed **handoff** for whoever builds, then record an **outcome** so the next cycle is smarter.
61
+
62
+ **v0.1.0** is the schema contract; **v0.2.0** adds a thin Python reference runtime. Both obey `docs/SPEC.md`. Public history with full commit SHAs: [`CHANGELOG.md`](CHANGELOG.md).
63
+
64
+ ## Quick start
65
+
66
+ 1. Read the normative rules: [`docs/SPEC.md`](docs/SPEC.md)
67
+ 2. Copy [`examples/board.example.json`](examples/board.example.json)
68
+ 3. Validate with any JSON Schema Draft 2020-12 validator against [`schemas/`](schemas/)
69
+
70
+ ## Loop
71
+
72
+ | Stage | Artifact | Role |
73
+ |-------|----------|------|
74
+ | Sense | `card` | Normalize a research signal |
75
+ | Rank | `board` + scoring rules | Order work; kill weak cards |
76
+ | Handoff | `handoff` | 7-part ship packet for implementers |
77
+ | Ship / learn | `outcome` | Close the loop |
78
+
79
+ **Score**
80
+
81
+ ```text
82
+ S = (novelty × evidence_strength × usability × dependency_fit)
83
+ / (hours + compute_cost + blast_radius)
84
+ ```
85
+
86
+ Kill when usability or dependency fit ≈ 0, or `target_repo` is missing at design+.
87
+
88
+ ## Layout
89
+
90
+ ```text
91
+ schemas/ card · board · handoff · outcome · scoring
92
+ docs/SPEC.md normative rules
93
+ docs/brand/ icon · social · README loop GIF
94
+ examples/ worked board + handoff (this repo as the sample)
95
+ ```
96
+
97
+ ## Brand
98
+
99
+ | | |
100
+ |-|-|
101
+ | Mark | stacked cards → chevron (cyan on charcoal) |
102
+ | Emoji | ↻ |
103
+ | Accent | `#5eead4` on `#0f1115` |
104
+
105
+ ## Security
106
+
107
+ Card fields are **untrusted data**. Validators must not execute claim or evidence strings. This repo holds no credentials and no network clients.
108
+
109
+ ## Changelog & identity
110
+
111
+ Ship history is **SHA-verified** in [`CHANGELOG.md`](CHANGELOG.md). Security posture: [`SECURITY.md`](SECURITY.md).
112
+
113
+ Current `main` tip at publish of this note is recorded in the changelog Unreleased/latest section after each merge.
114
+
115
+ ## License
116
+
117
+ [MIT](LICENSE)
@@ -0,0 +1,20 @@
1
+ r2s/__init__.py,sha256=3RXXLP4bFCQ00W59eskPYtvaY3jLuRNDJ6RgFmJWByQ,495
2
+ r2s/cli.py,sha256=dL6J-2VlPx8b-DjX709Kpv0jgknTWqzmm-YACSfk2eg,2088
3
+ r2s/errors.py,sha256=K2MN5AbCPNpR7D3yMZmjLhXklT77AeWZA43rYeSfwmc,303
4
+ r2s/handoff.py,sha256=s2yX6CeOsrV9jA1NSF5jtulptzSvSgF4KlPAs3MROJQ,1926
5
+ r2s/io.py,sha256=2YtR8wEK1IJ_kCjz0GhfsBFjtY7QwdpwfWVf2RInGEE,777
6
+ r2s/paths.py,sha256=reRppFTjWr7Y5sQEpPJN46MKde4FSetliphX0sLLJx4,703
7
+ r2s/rank.py,sha256=gvIDY3Qk5tGOXiZ0CaXAWY_OXkWpQmtwrCeDZnQbYac,1234
8
+ r2s/score.py,sha256=nL-DGJr1zaGXpDLPRASp-bB8REw1fobO4WqqQrjd_4U,1273
9
+ r2s/validate.py,sha256=VewpVBJXuG3lolQwL5vK_MSIvn9pAWHlDNFiRvf-zkY,1338
10
+ r2s/schemas/board.schema.json,sha256=iyFaxu6_FRE-pqgA4UAr8fztxVe4qggBzLay_CDBHy8,629
11
+ r2s/schemas/card.schema.json,sha256=aKjhNSmewhcoEN9lxtlgHVemJ5yQRkPD8VufvF1N36o,3341
12
+ r2s/schemas/handoff.schema.json,sha256=aZYMu4bOXtMlcVjg-dCeYkCU_kEZBpNpVatuw59Oi5g,1283
13
+ r2s/schemas/outcome.schema.json,sha256=X2rnK7Vkp1cLU-uikiwO8ruSZsm8slaxiQJvFs31EWg,1777
14
+ r2s/schemas/scoring.schema.json,sha256=LkN1otuMILbkf1mUNrxGDhiz5o63P23PL_9Qb96PsWA,1305
15
+ r2s-0.2.1.dist-info/licenses/LICENSE,sha256=-HiXdRZIQBhQbwFgC-rY5_tZ_2ej1kQI58ZN2vLK6xk,1082
16
+ r2s-0.2.1.dist-info/METADATA,sha256=ZWCQPxo0OXpQ-Mf56ZWxufXiFM44jsJwgUlPpVKeVdA,4208
17
+ r2s-0.2.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
18
+ r2s-0.2.1.dist-info/entry_points.txt,sha256=qTXgt6vIHv-lA7nqNVv7mOYwRDsheqjiP6PVuaLvBXQ,37
19
+ r2s-0.2.1.dist-info/top_level.txt,sha256=6ccEVxLvLdFZoJVGVXSdLoLCOLHmjJtbUK188Q9VUFA,4
20
+ r2s-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ r2s = r2s.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adam O'Connor (ao3575911)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ r2s