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 +22 -0
- r2s/cli.py +65 -0
- r2s/errors.py +14 -0
- r2s/handoff.py +58 -0
- r2s/io.py +29 -0
- r2s/paths.py +23 -0
- r2s/rank.py +40 -0
- r2s/schemas/board.schema.json +29 -0
- r2s/schemas/card.schema.json +174 -0
- r2s/schemas/handoff.schema.json +60 -0
- r2s/schemas/outcome.schema.json +82 -0
- r2s/schemas/scoring.schema.json +52 -0
- r2s/score.py +36 -0
- r2s/validate.py +36 -0
- r2s-0.2.1.dist-info/METADATA +117 -0
- r2s-0.2.1.dist-info/RECORD +20 -0
- r2s-0.2.1.dist-info/WHEEL +5 -0
- r2s-0.2.1.dist-info/entry_points.txt +2 -0
- r2s-0.2.1.dist-info/licenses/LICENSE +21 -0
- r2s-0.2.1.dist-info/top_level.txt +1 -0
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,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
|