refactorai-core 3.2.2__tar.gz → 3.2.3__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.
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/PKG-INFO +1 -1
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/pyproject.toml +2 -1
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/__init__.py +1 -1
- refactorai_core-3.2.3/refactor_core/intent/__init__.py +36 -0
- refactorai_core-3.2.3/refactor_core/intent/capture.py +296 -0
- refactorai_core-3.2.3/refactor_core/intent/contract.py +154 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactorai_core.egg-info/PKG-INFO +1 -1
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactorai_core.egg-info/SOURCES.txt +3 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/README.md +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/apply.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/budgeting.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/capabilities.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/compliance/__init__.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/compliance/detectors.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/compliance/evidence.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/compliance/loader.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/compliance/models.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/compliance/packs/__init__.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/compliance/packs/hipaa-ephi.json +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/compliance/packs/iso27001-sdlc.json +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/compliance/packs/pci-payments.json +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/compliance/packs/privacy-gdpr.json +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/compliance/packs/soc2-core.json +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/compliance/projection.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/compliance_settings.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/constitution.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/gate.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/host_toolchain.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/indexing.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/merge_gate.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/model_catalog.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/models.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/op_classifier.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/op_feasibility.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/paths.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/refactor_ops.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/refactor_requests.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/rules/__init__.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/rules/injection.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/rules/loader.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/rules/matcher.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/rules/models.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/rules/resolver.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/rules/system_rules.json +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/security/__init__.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/security/finding_merge.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/security/semgrep_parser.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/security/semgrep_runner.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/security/suppression.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/store.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/testcmd.py +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactorai_core.egg-info/dependency_links.txt +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactorai_core.egg-info/requires.txt +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactorai_core.egg-info/top_level.txt +0 -0
- {refactorai_core-3.2.2 → refactorai_core-3.2.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: refactorai-core
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.3
|
|
4
4
|
Summary: Public client core for the refactor platform: indexing, constitution parsing, models, gate evaluation, rules, security prefilter, RR/compliance classification. Contains no proprietary inference engine.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "refactorai-core"
|
|
3
|
-
version = "3.2.
|
|
3
|
+
version = "3.2.3"
|
|
4
4
|
description = "Public client core for the refactor platform: indexing, constitution parsing, models, gate evaluation, rules, security prefilter, RR/compliance classification. Contains no proprietary inference engine."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -38,6 +38,7 @@ include = [
|
|
|
38
38
|
"refactor_core.security",
|
|
39
39
|
"refactor_core.compliance",
|
|
40
40
|
"refactor_core.compliance.packs",
|
|
41
|
+
"refactor_core.intent",
|
|
41
42
|
]
|
|
42
43
|
# Defensive: never ship engine subpackages in the public client wheel even if a
|
|
43
44
|
# future refactor accidentally makes one discoverable.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Shared, pure intent-capture and contract-normalization primitives (`R27`/`R28`).
|
|
2
|
+
|
|
3
|
+
Single source of truth for the deterministic intent math used by both the server
|
|
4
|
+
(`backend/app/services/intent*.py`) and the local commit-scoped review loop
|
|
5
|
+
(docs/41). Everything here is provider- and storage-independent.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from refactor_core.intent.capture import (
|
|
9
|
+
build_payload,
|
|
10
|
+
derive_contract,
|
|
11
|
+
extract_statements,
|
|
12
|
+
summarize_intent,
|
|
13
|
+
)
|
|
14
|
+
from refactor_core.intent.contract import (
|
|
15
|
+
ELEVATED_RISK_TIERS,
|
|
16
|
+
STRUCTURAL_ASSERTIONS,
|
|
17
|
+
compute_content_hash,
|
|
18
|
+
infer_verifier,
|
|
19
|
+
normalize_contract_input,
|
|
20
|
+
required_permission_for_risk_tier,
|
|
21
|
+
summarize_verdicts,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"build_payload",
|
|
26
|
+
"derive_contract",
|
|
27
|
+
"extract_statements",
|
|
28
|
+
"summarize_intent",
|
|
29
|
+
"ELEVATED_RISK_TIERS",
|
|
30
|
+
"STRUCTURAL_ASSERTIONS",
|
|
31
|
+
"compute_content_hash",
|
|
32
|
+
"infer_verifier",
|
|
33
|
+
"normalize_contract_input",
|
|
34
|
+
"required_permission_for_risk_tier",
|
|
35
|
+
"summarize_verdicts",
|
|
36
|
+
]
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Deterministic implicit intent capture (docs/39 FR-01, Part D INTENT CAPTURE).
|
|
2
|
+
|
|
3
|
+
Intent does **not** have to be authored by hand. This module derives the same
|
|
4
|
+
canonical acceptance-criteria structure an explicit contract produces from the
|
|
5
|
+
free-form context that already surrounds a change:
|
|
6
|
+
|
|
7
|
+
- a coding agent's prompt (Cursor / Claude Code) submitted at trigger time,
|
|
8
|
+
- a commit message (local commit trigger, docs/41 `R28`),
|
|
9
|
+
- a pull-request title/description (webhook path),
|
|
10
|
+
- a linked ticket, or a design/spec doc.
|
|
11
|
+
|
|
12
|
+
The derived structure is fed through :func:`contract.normalize_contract_input`,
|
|
13
|
+
so capture shares one normalization/keying/verifier-routing path with explicit
|
|
14
|
+
contracts, on both the server and the local CLI.
|
|
15
|
+
|
|
16
|
+
Design constraints:
|
|
17
|
+
|
|
18
|
+
- **Deterministic and provider-independent.** No LLM call, so every execution
|
|
19
|
+
mode behaves identically and the extractor is fully unit-testable. Richer LLM
|
|
20
|
+
extraction can layer on later behind the same ``derive_contract`` interface.
|
|
21
|
+
- **Never fabricate a verified verdict.** Extraction only *captures* criteria;
|
|
22
|
+
structural ones route to the deterministic ``code-scan`` verifier, everything
|
|
23
|
+
else stays ``behavioral`` (recorded, escalatable) rather than silently passing
|
|
24
|
+
(docs/39 Part C hard boundary).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import re
|
|
30
|
+
|
|
31
|
+
from refactor_core.intent import contract as contract_mod
|
|
32
|
+
|
|
33
|
+
# A statement looks like a requirement when it carries an imperative/modal verb.
|
|
34
|
+
_MODAL = re.compile(
|
|
35
|
+
r"\b(must|should|shall|need(?:s)? to|has to|have to|will|ensure|add|adds|"
|
|
36
|
+
r"implement|implements|introduce|create|creates|fix|fixes|support|supports|"
|
|
37
|
+
r"expose|exposes|return|returns|handle|handles|remove|removes|rename|renames|"
|
|
38
|
+
r"update|updates|allow|allows|prevent|prevents|validate|validates|require|"
|
|
39
|
+
r"requires|guarantee|guarantees|enforce|enforces)\b",
|
|
40
|
+
re.IGNORECASE,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Section headers that flip parsing into "these bullets are non-goals".
|
|
44
|
+
_NON_GOAL_HINT = re.compile(r"\b(non[-\s]?goals?|out[-\s]?of[-\s]?scope|not in scope|will not)\b", re.IGNORECASE)
|
|
45
|
+
# Section headers that mark an explicit list of acceptance criteria.
|
|
46
|
+
_CRITERIA_HINT = re.compile(
|
|
47
|
+
r"\b(acceptance criteri\w+|requirements?|criteria|tasks?|checklist|to[-\s]?do)\b", re.IGNORECASE
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
_BULLET = re.compile(r"^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ xX]?\]\s+)?(.*\S)\s*$")
|
|
51
|
+
_HEADER = re.compile(r"^\s*(?:#{1,6}\s*|\*\*)\s*(.+?)\s*(?:\*\*)?\s*:?\s*$")
|
|
52
|
+
_QUOTED = re.compile(r"`([^`]+)`|\"([^\"]+)\"|'([^']+)'")
|
|
53
|
+
_BARE_PATH = re.compile(r"(?<![\w./-])([\w./-]+\.[A-Za-z0-9]{1,6})(?![\w/])")
|
|
54
|
+
_CONTAINS_VERB = re.compile(
|
|
55
|
+
r"\b(contain|contains|mention|mentions|include|includes|reference|references|say|says|document|documents)\b",
|
|
56
|
+
re.IGNORECASE,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
_MAX_CRITERIA = 12
|
|
60
|
+
_MAX_STATEMENT = 200
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _looks_like_path(token: str) -> bool:
|
|
64
|
+
token = (token or "").strip()
|
|
65
|
+
if not token or " " in token or "\t" in token:
|
|
66
|
+
return False
|
|
67
|
+
if not re.fullmatch(r"[\w./@+-]+", token):
|
|
68
|
+
return False
|
|
69
|
+
return "/" in token or bool(re.search(r"\.[A-Za-z0-9]{1,6}$", token))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _find_path(statement: str) -> str:
|
|
73
|
+
"""Return the first path-like token mentioned in a statement, else ``""``."""
|
|
74
|
+
for match in _QUOTED.finditer(statement):
|
|
75
|
+
token = next((g for g in match.groups() if g), "")
|
|
76
|
+
if _looks_like_path(token):
|
|
77
|
+
return token.strip().lstrip("/")
|
|
78
|
+
bare = _BARE_PATH.search(statement)
|
|
79
|
+
if bare and _looks_like_path(bare.group(1)):
|
|
80
|
+
return bare.group(1).strip().lstrip("/")
|
|
81
|
+
return ""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _detect_structural(statement: str) -> tuple[str, str, str, str]:
|
|
85
|
+
"""Classify a statement into (kind, assertion, path, expected).
|
|
86
|
+
|
|
87
|
+
A statement that names a concrete file/path is treated as a deterministically
|
|
88
|
+
checkable structural criterion; if it also asks for that file to *contain* a
|
|
89
|
+
quoted term it becomes ``path_contains``. Everything else is ``behavioral``
|
|
90
|
+
(routed to the runtime verifier, escalatable, never silently passed).
|
|
91
|
+
"""
|
|
92
|
+
path = _find_path(statement)
|
|
93
|
+
if not path:
|
|
94
|
+
return "behavioral", "", "", ""
|
|
95
|
+
|
|
96
|
+
if _CONTAINS_VERB.search(statement):
|
|
97
|
+
for match in _QUOTED.finditer(statement):
|
|
98
|
+
token = next((g for g in match.groups() if g), "").strip()
|
|
99
|
+
if token and token.lstrip("/") != path and not _looks_like_path(token):
|
|
100
|
+
return "structural", "path_contains", path, token
|
|
101
|
+
return "structural", "path_exists", path, ""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _clean_statement(text: str) -> str:
|
|
105
|
+
text = re.sub(r"\s+", " ", (text or "").strip())
|
|
106
|
+
text = text.rstrip(".;")
|
|
107
|
+
return text[:_MAX_STATEMENT].strip()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _sentences(line: str) -> list[str]:
|
|
111
|
+
# Split a prose line into candidate requirement sentences.
|
|
112
|
+
parts = re.split(r"(?<=[.!?])\s+", line.strip())
|
|
113
|
+
return [p for p in (part.strip() for part in parts) if p]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def extract_statements(text: str) -> tuple[list[str], list[str]]:
|
|
117
|
+
"""Extract (criterion statements, non-goal statements) from one text blob.
|
|
118
|
+
|
|
119
|
+
Bullet/checklist lines become criteria (or non-goals under a non-goal
|
|
120
|
+
heading); free prose contributes sentences that carry a modal/imperative verb.
|
|
121
|
+
"""
|
|
122
|
+
if not text:
|
|
123
|
+
return [], []
|
|
124
|
+
|
|
125
|
+
criteria: list[str] = []
|
|
126
|
+
non_goals: list[str] = []
|
|
127
|
+
section = "body" # body | non_goal | criteria
|
|
128
|
+
|
|
129
|
+
for raw in str(text).splitlines():
|
|
130
|
+
line = raw.rstrip()
|
|
131
|
+
if not line.strip():
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
bullet = _BULLET.match(line)
|
|
135
|
+
header = None if bullet else _HEADER.match(line)
|
|
136
|
+
|
|
137
|
+
if header:
|
|
138
|
+
label = header.group(1)
|
|
139
|
+
if _NON_GOAL_HINT.search(label):
|
|
140
|
+
section = "non_goal"
|
|
141
|
+
elif _CRITERIA_HINT.search(label):
|
|
142
|
+
section = "criteria"
|
|
143
|
+
else:
|
|
144
|
+
section = "body"
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
if bullet:
|
|
148
|
+
content = _clean_statement(bullet.group(1))
|
|
149
|
+
if not content:
|
|
150
|
+
continue
|
|
151
|
+
if section == "non_goal":
|
|
152
|
+
non_goals.append(content)
|
|
153
|
+
else:
|
|
154
|
+
criteria.append(content)
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
# Non-bullet prose: harvest imperative sentences (and non-goals when the
|
|
158
|
+
# current section says so).
|
|
159
|
+
for sentence in _sentences(line):
|
|
160
|
+
if section == "non_goal":
|
|
161
|
+
non_goals.append(_clean_statement(sentence))
|
|
162
|
+
elif _MODAL.search(sentence):
|
|
163
|
+
criteria.append(_clean_statement(sentence))
|
|
164
|
+
|
|
165
|
+
return [c for c in criteria if c], [n for n in non_goals if n]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _dedupe(values: list[str]) -> list[str]:
|
|
169
|
+
seen: set[str] = set()
|
|
170
|
+
ordered: list[str] = []
|
|
171
|
+
for value in values:
|
|
172
|
+
key = value.lower()
|
|
173
|
+
if key and key not in seen:
|
|
174
|
+
seen.add(key)
|
|
175
|
+
ordered.append(value)
|
|
176
|
+
return ordered
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _source_label(sources: list[dict]) -> str:
|
|
180
|
+
kinds = {str(s.get("type", "") or "").strip().lower() for s in sources if str(s.get("type", "") or "").strip()}
|
|
181
|
+
if len(kinds) == 1:
|
|
182
|
+
return next(iter(kinds))
|
|
183
|
+
return "derived"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _title_from(sources: list[dict], statements: list[str]) -> str:
|
|
187
|
+
for source in sources:
|
|
188
|
+
title = str(source.get("title", "") or "").strip()
|
|
189
|
+
if title:
|
|
190
|
+
return title[:_MAX_STATEMENT]
|
|
191
|
+
for source in sources:
|
|
192
|
+
for line in str(source.get("text", "") or "").splitlines():
|
|
193
|
+
stripped = line.strip().lstrip("#").strip()
|
|
194
|
+
if stripped:
|
|
195
|
+
return _clean_statement(stripped)
|
|
196
|
+
return statements[0] if statements else ""
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def build_payload(sources: list[dict]) -> dict | None:
|
|
200
|
+
"""Build a raw intent-contract *input* payload from free-form sources.
|
|
201
|
+
|
|
202
|
+
Returns the payload shape consumed by
|
|
203
|
+
:func:`contract.normalize_contract_input` (or ``None`` when nothing usable
|
|
204
|
+
was found). Kept separate from :func:`derive_contract` so it is trivially
|
|
205
|
+
unit-testable without touching the normalization layer.
|
|
206
|
+
"""
|
|
207
|
+
if not sources:
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
# Keep first-seen provenance for each distinct statement so every derived
|
|
211
|
+
# criterion stays auditable even when we aggregate multiple source types.
|
|
212
|
+
all_criteria: list[tuple[str, str, str]] = []
|
|
213
|
+
all_non_goals: list[str] = []
|
|
214
|
+
for source in sources:
|
|
215
|
+
source_type = str(source.get("type", "derived") or "derived").strip().lower() or "derived"
|
|
216
|
+
source_ref = str(source.get("ref", "") or "").strip()
|
|
217
|
+
source_title = str(source.get("title", "") or "").strip()
|
|
218
|
+
source_text = str(source.get("text", "") or "").strip()
|
|
219
|
+
# Parse both title and body: webhook PR events and commit subjects often
|
|
220
|
+
# encode intent in the title while the body may be empty/minimal.
|
|
221
|
+
merged_text = "\n".join(part for part in [source_title, source_text] if part)
|
|
222
|
+
crit, non_goals = extract_statements(merged_text)
|
|
223
|
+
for statement in crit:
|
|
224
|
+
all_criteria.append((statement, source_type, source_ref))
|
|
225
|
+
all_non_goals.extend(non_goals)
|
|
226
|
+
|
|
227
|
+
statement_provenance: dict[str, tuple[str, str]] = {}
|
|
228
|
+
statements: list[str] = []
|
|
229
|
+
for statement, source_type, source_ref in all_criteria:
|
|
230
|
+
key = statement.lower()
|
|
231
|
+
if key in statement_provenance:
|
|
232
|
+
continue
|
|
233
|
+
statement_provenance[key] = (source_type, source_ref)
|
|
234
|
+
statements.append(statement)
|
|
235
|
+
if len(statements) >= _MAX_CRITERIA:
|
|
236
|
+
break
|
|
237
|
+
non_goals = _dedupe(all_non_goals)[:_MAX_CRITERIA]
|
|
238
|
+
title = _title_from(sources, statements)
|
|
239
|
+
|
|
240
|
+
if not title and not statements:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
label = _source_label(sources)
|
|
244
|
+
criteria: list[dict] = []
|
|
245
|
+
for index, statement in enumerate(statements, start=1):
|
|
246
|
+
kind, assertion, path, expected = _detect_structural(statement)
|
|
247
|
+
source_type, source_ref = statement_provenance.get(statement.lower(), (label, ""))
|
|
248
|
+
note = f"derived from {source_type}"
|
|
249
|
+
if source_ref:
|
|
250
|
+
note = f"{note} ({source_ref})"
|
|
251
|
+
criteria.append(
|
|
252
|
+
{
|
|
253
|
+
"id": f"criterion-{index}",
|
|
254
|
+
"statement": statement,
|
|
255
|
+
"kind": kind,
|
|
256
|
+
"risk_tier": "standard",
|
|
257
|
+
"assert": assertion,
|
|
258
|
+
"path": path,
|
|
259
|
+
"contains": expected,
|
|
260
|
+
"notes": note,
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return {"title": title, "source": label, "non_goals": non_goals, "criteria": criteria}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def derive_contract(sources: list[dict] | None) -> dict | None:
|
|
268
|
+
"""Derive a canonical intent contract from free-form authoring context.
|
|
269
|
+
|
|
270
|
+
``sources`` is a list of ``{type, title, text, ref}`` dicts. Returns the
|
|
271
|
+
canonical structure (same shape as an explicitly-submitted contract), or
|
|
272
|
+
``None`` when no intent could be derived (callers treat that as the
|
|
273
|
+
non-blocking "no intent" case).
|
|
274
|
+
"""
|
|
275
|
+
payload = build_payload([s for s in (sources or []) if isinstance(s, dict)])
|
|
276
|
+
if payload is None:
|
|
277
|
+
return None
|
|
278
|
+
return contract_mod.normalize_contract_input(payload)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def summarize_intent(canonical: dict | None) -> str:
|
|
282
|
+
"""One-line human summary of a derived contract for terminal display (R28).
|
|
283
|
+
|
|
284
|
+
Prefers the contract title, else the first criterion statement; returns an
|
|
285
|
+
empty string when there is no usable intent.
|
|
286
|
+
"""
|
|
287
|
+
if not isinstance(canonical, dict):
|
|
288
|
+
return ""
|
|
289
|
+
title = str(canonical.get("title", "") or "").strip()
|
|
290
|
+
if title:
|
|
291
|
+
return title[:_MAX_STATEMENT]
|
|
292
|
+
for criterion in canonical.get("criteria", []) or []:
|
|
293
|
+
statement = str((criterion or {}).get("statement", "") or "").strip()
|
|
294
|
+
if statement:
|
|
295
|
+
return statement[:_MAX_STATEMENT]
|
|
296
|
+
return ""
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Pure intent-contract normalization and verdict aggregation (docs/39 `R27`).
|
|
2
|
+
|
|
3
|
+
This is the single, provider- and storage-independent implementation of the
|
|
4
|
+
intent-contract math shared by the server (`backend/app/services/intent.py`) and
|
|
5
|
+
the local CLI (docs/41 `R28`). It contains only pure functions over plain dicts:
|
|
6
|
+
|
|
7
|
+
- ``normalize_contract_input`` coerces a raw payload into a canonical,
|
|
8
|
+
deterministically-keyed structure (shared shape for persistence and the local
|
|
9
|
+
commit-scoped review).
|
|
10
|
+
- ``_infer_verifier`` routes a criterion to exactly one verifier.
|
|
11
|
+
- ``compute_content_hash`` produces a stable hash over the canonical contract.
|
|
12
|
+
- ``summarize_verdicts`` aggregates per-criterion verdicts into a gate decision.
|
|
13
|
+
|
|
14
|
+
No database, no execution mode, no provider selection — so ``local`` /
|
|
15
|
+
``cloud_managed`` / ``cloud_byok`` behave identically wherever this is imported.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
|
|
23
|
+
# Structural assertions the deterministic ``code-scan`` verifier understands.
|
|
24
|
+
STRUCTURAL_ASSERTIONS = {"path_exists", "path_contains"}
|
|
25
|
+
|
|
26
|
+
# Criteria that carry organizational risk require elevated permission to amend
|
|
27
|
+
# (docs/39 FR-08). Kept here so the risk vocabulary has one definition.
|
|
28
|
+
ELEVATED_RISK_TIERS = {"high", "security", "compliance"}
|
|
29
|
+
|
|
30
|
+
_VALID_RISK_TIERS = {"standard", "high", "security", "compliance"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def infer_verifier(kind: str, assertion: str) -> str:
|
|
34
|
+
"""Route a criterion to exactly one verifier (docs/39 FR-03).
|
|
35
|
+
|
|
36
|
+
Only the deterministic ``code-scan`` verifier is implemented for structural
|
|
37
|
+
criteria; behavioral criteria route to ``runtime`` (recorded, escalatable,
|
|
38
|
+
never silently passed); anything else is ``pending``.
|
|
39
|
+
"""
|
|
40
|
+
if kind == "structural" and assertion in STRUCTURAL_ASSERTIONS:
|
|
41
|
+
return "code-scan"
|
|
42
|
+
if kind == "behavioral":
|
|
43
|
+
return "runtime"
|
|
44
|
+
return "pending"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Backwards-compatible private alias (backend historically referenced the
|
|
48
|
+
# underscore-prefixed name).
|
|
49
|
+
_infer_verifier = infer_verifier
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def normalize_contract_input(payload: dict | None) -> dict | None:
|
|
53
|
+
"""Coerce a raw intent payload into a canonical contract structure.
|
|
54
|
+
|
|
55
|
+
Returns ``None`` when no meaningful contract was supplied (no criteria and no
|
|
56
|
+
title), so callers can treat "no intent" as a first-class, non-blocking case.
|
|
57
|
+
Criterion keys are assigned deterministically (``criterion-1``, ...) so the
|
|
58
|
+
persisted rows and any downstream verdicts reference the same identifiers.
|
|
59
|
+
"""
|
|
60
|
+
if not isinstance(payload, dict):
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
title = str(payload.get("title", "") or "").strip()
|
|
64
|
+
source = str(payload.get("source", "api") or "api").strip().lower() or "api"
|
|
65
|
+
raw_non_goals = payload.get("non_goals")
|
|
66
|
+
non_goals = (
|
|
67
|
+
[str(item).strip() for item in raw_non_goals if str(item).strip()]
|
|
68
|
+
if isinstance(raw_non_goals, list)
|
|
69
|
+
else []
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
raw_criteria = payload.get("criteria")
|
|
73
|
+
criteria: list[dict] = []
|
|
74
|
+
if isinstance(raw_criteria, list):
|
|
75
|
+
for index, item in enumerate(raw_criteria, start=1):
|
|
76
|
+
if not isinstance(item, dict):
|
|
77
|
+
continue
|
|
78
|
+
kind = str(item.get("kind", "structural") or "structural").strip().lower()
|
|
79
|
+
if kind not in {"structural", "behavioral"}:
|
|
80
|
+
kind = "structural"
|
|
81
|
+
assertion = str(item.get("assert", item.get("assertion", "")) or "").strip().lower()
|
|
82
|
+
key = str(item.get("id", "") or f"criterion-{index}").strip() or f"criterion-{index}"
|
|
83
|
+
risk_tier = str(item.get("risk_tier", "standard") or "standard").strip().lower()
|
|
84
|
+
if risk_tier not in _VALID_RISK_TIERS:
|
|
85
|
+
risk_tier = "standard"
|
|
86
|
+
criteria.append(
|
|
87
|
+
{
|
|
88
|
+
"criterion_key": key,
|
|
89
|
+
"statement": str(item.get("statement", "") or "").strip(),
|
|
90
|
+
"kind": kind,
|
|
91
|
+
"risk_tier": risk_tier,
|
|
92
|
+
"verifier": infer_verifier(kind, assertion),
|
|
93
|
+
"assertion": assertion,
|
|
94
|
+
"path": str(item.get("path", "") or "").strip().lstrip("/"),
|
|
95
|
+
"expected": str(item.get("expected", item.get("contains", "")) or "").strip(),
|
|
96
|
+
"notes": str(item.get("notes", "") or "").strip(),
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if not title and not criteria:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
return {"title": title, "source": source, "non_goals": non_goals, "criteria": criteria}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def compute_content_hash(canonical: dict) -> str:
|
|
107
|
+
"""Stable content hash over the canonical contract (docs/39 Part E)."""
|
|
108
|
+
encoded = json.dumps(canonical, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
109
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def required_permission_for_risk_tier(risk_tier: str) -> str:
|
|
113
|
+
"""Minimum permission required to amend a criterion of this tier (FR-08)."""
|
|
114
|
+
return "admin" if str(risk_tier or "standard").strip().lower() in ELEVATED_RISK_TIERS else "write"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def summarize_verdicts(verdicts: list[str]) -> dict:
|
|
118
|
+
"""Aggregate criterion verdicts into a gate decision (pure).
|
|
119
|
+
|
|
120
|
+
Result values:
|
|
121
|
+
- ``not_provided``: no verdicts, or only ``not_applicable``.
|
|
122
|
+
- ``pending``: some criteria still unverified.
|
|
123
|
+
- ``passed``: at least one satisfied and none failed/pending.
|
|
124
|
+
- ``blocked``: at least one ``failed``/``escalated``.
|
|
125
|
+
"""
|
|
126
|
+
passed = failed = pending = not_applicable = 0
|
|
127
|
+
for verdict in verdicts:
|
|
128
|
+
if verdict == "passed":
|
|
129
|
+
passed += 1
|
|
130
|
+
elif verdict in {"failed", "escalated"}:
|
|
131
|
+
failed += 1
|
|
132
|
+
elif verdict == "pending":
|
|
133
|
+
pending += 1
|
|
134
|
+
else:
|
|
135
|
+
not_applicable += 1
|
|
136
|
+
|
|
137
|
+
if failed:
|
|
138
|
+
result, reason = "blocked", "one_or_more_criteria_failed"
|
|
139
|
+
elif pending:
|
|
140
|
+
result, reason = "pending", "verification_incomplete"
|
|
141
|
+
elif passed:
|
|
142
|
+
result, reason = "passed", "all_verifiable_criteria_satisfied"
|
|
143
|
+
else:
|
|
144
|
+
result, reason = "not_provided", "no_verifiable_criteria"
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
"result": result,
|
|
148
|
+
"criteria_total": len(verdicts),
|
|
149
|
+
"criteria_passed": passed,
|
|
150
|
+
"criteria_failed": failed,
|
|
151
|
+
"criteria_pending": pending,
|
|
152
|
+
"criteria_not_applicable": not_applicable,
|
|
153
|
+
"reason": reason,
|
|
154
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: refactorai-core
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.3
|
|
4
4
|
Summary: Public client core for the refactor platform: indexing, constitution parsing, models, gate evaluation, rules, security prefilter, RR/compliance classification. Contains no proprietary inference engine.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -31,6 +31,9 @@ refactor_core/compliance/packs/iso27001-sdlc.json
|
|
|
31
31
|
refactor_core/compliance/packs/pci-payments.json
|
|
32
32
|
refactor_core/compliance/packs/privacy-gdpr.json
|
|
33
33
|
refactor_core/compliance/packs/soc2-core.json
|
|
34
|
+
refactor_core/intent/__init__.py
|
|
35
|
+
refactor_core/intent/capture.py
|
|
36
|
+
refactor_core/intent/contract.py
|
|
34
37
|
refactor_core/rules/__init__.py
|
|
35
38
|
refactor_core/rules/injection.py
|
|
36
39
|
refactor_core/rules/loader.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/compliance/packs/hipaa-ephi.json
RENAMED
|
File without changes
|
{refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/compliance/packs/iso27001-sdlc.json
RENAMED
|
File without changes
|
{refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/compliance/packs/pci-payments.json
RENAMED
|
File without changes
|
{refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/compliance/packs/privacy-gdpr.json
RENAMED
|
File without changes
|
{refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactor_core/compliance/packs/soc2-core.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{refactorai_core-3.2.2 → refactorai_core-3.2.3}/refactorai_core.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|