standardgraph 1.0.0__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.
- common_core/__init__.py +0 -0
- common_core/config.py +11 -0
- common_core/server.py +698 -0
- standardgraph-1.0.0.dist-info/METADATA +67 -0
- standardgraph-1.0.0.dist-info/RECORD +8 -0
- standardgraph-1.0.0.dist-info/WHEEL +5 -0
- standardgraph-1.0.0.dist-info/entry_points.txt +2 -0
- standardgraph-1.0.0.dist-info/top_level.txt +1 -0
common_core/__init__.py
ADDED
|
File without changes
|
common_core/config.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
# For distributed installs (uvx / PyPI), the database lives in ~/.standardgraph/.
|
|
5
|
+
# For local dev, override via the DB_PATH env var.
|
|
6
|
+
DB_PATH = Path(os.getenv("DB_PATH", str(Path.home() / ".standardgraph" / "common_core.db")))
|
|
7
|
+
|
|
8
|
+
# Distributed users run Ollama locally; the 169.254.1.1 Thunderbolt Bridge address
|
|
9
|
+
# is overridden by OLLAMA_BASE_URL in the local dev / overnight pipeline environment.
|
|
10
|
+
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
|
|
11
|
+
EMBED_MODEL = "nomic-embed-text"
|
common_core/server.py
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
"""International Math Standards MCP server."""
|
|
2
|
+
import json
|
|
3
|
+
import sqlite3
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from fastmcp import FastMCP
|
|
7
|
+
|
|
8
|
+
from common_core.config import DB_PATH, OLLAMA_BASE_URL, EMBED_MODEL
|
|
9
|
+
|
|
10
|
+
def _build_instructions() -> str:
|
|
11
|
+
try:
|
|
12
|
+
_c = sqlite3.connect(DB_PATH)
|
|
13
|
+
_std_count = _c.execute("SELECT COUNT(*) FROM standards").fetchone()[0]
|
|
14
|
+
_sys_count = _c.execute("SELECT COUNT(DISTINCT system) FROM standards").fetchone()[0]
|
|
15
|
+
_c.close()
|
|
16
|
+
except Exception:
|
|
17
|
+
_std_count, _sys_count = 0, 0
|
|
18
|
+
return f"""\
|
|
19
|
+
You have access to a database of {_std_count:,} math standards across {_sys_count} curriculum \
|
|
20
|
+
systems, all cross-referenced to the US Common Core State Standards (CCSS) as the hub.
|
|
21
|
+
|
|
22
|
+
## Available systems
|
|
23
|
+
|
|
24
|
+
**North America — US:** ccss (hub), plus all 50 states + DC by two-letter code
|
|
25
|
+
(al ak az ar ca co ct dc de fl ga hi id il in ia ks ky la me md ma mi mn ms mo
|
|
26
|
+
mt ne nv nh nj nm ny nc nd oh ok or pa ri sc sd tn tx ut vt va wa wv wi wy)
|
|
27
|
+
|
|
28
|
+
**North America — Canada:** ca-ab (Alberta) ca-bc (British Columbia) ca-mb (Manitoba)
|
|
29
|
+
ca-nb (New Brunswick) ca-on (Ontario) ca-qc (Quebec, French) ca-sk (Saskatchewan)
|
|
30
|
+
|
|
31
|
+
**Asia-Pacific:** sg-moe (Singapore) jp-mext (Japan, Gr 1–6) nz-moe (New Zealand Yr 0–10)
|
|
32
|
+
au-acara (Australian Curriculum) au-vic (Victoria) hk-edb (Hong Kong KS1–3)
|
|
33
|
+
ph-deped (Philippines K–10)
|
|
34
|
+
|
|
35
|
+
**Europe:** uk-nc (England Yr 1–6) uk-aqa (AQA GCSE) gb-sco (Scotland CfE)
|
|
36
|
+
ie-ncca (Ireland Junior Cycle)
|
|
37
|
+
|
|
38
|
+
**South Asia:** in-ncert (India NCERT)
|
|
39
|
+
|
|
40
|
+
**Sub-Saharan Africa:** gh-nacca (Ghana B1–12) za-caps (South Africa Gr R–12)
|
|
41
|
+
rw-reb (Rwanda P4–P6)
|
|
42
|
+
|
|
43
|
+
**International:** cambridge (Cambridge International) ib-myp (IB Middle Years)
|
|
44
|
+
ib-dp (IB Diploma)
|
|
45
|
+
|
|
46
|
+
## Grade codes
|
|
47
|
+
K, 1, 2, 3, 4, 5, 6, 7, 8, HS
|
|
48
|
+
|
|
49
|
+
## When to use each tool
|
|
50
|
+
|
|
51
|
+
- **lookup_standard**: user provides a specific standard ID they want to read
|
|
52
|
+
- **search_standards**: user describes a concept and wants to find matching standards
|
|
53
|
+
- **get_progression**: user asks how a topic develops across grade levels
|
|
54
|
+
- **map_standard**: user wants to compare a standard across systems (e.g. "how does Texas
|
|
55
|
+
cover this vs CCSS?" or "what does the IB equivalent look like?")
|
|
56
|
+
|
|
57
|
+
## Tips
|
|
58
|
+
- Crosswalk mappings are NLP-generated (cosine similarity), not human-verified.
|
|
59
|
+
A confidence ≥ 0.85 is a strong match; 0.70–0.80 is plausible but worth checking.
|
|
60
|
+
A grade_delta ≠ 0 means the systems introduce the concept at different grade levels.
|
|
61
|
+
- map_standard tries three strategies in order: (1) precomputed crosswalk above
|
|
62
|
+
threshold; (2) two-hop CCSS bridge (source→CCSS→target for any-to-any mapping);
|
|
63
|
+
(3) semantic embedding fallback. Below-threshold precomputed results are always
|
|
64
|
+
included, flagged with "below_threshold": true.
|
|
65
|
+
- search_standards queries one system at a time; call it multiple times to compare
|
|
66
|
+
how different curricula cover the same concept.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
GRADE_ORDER = ["K", "1", "2", "3", "4", "5", "6", "7", "8", "HS"]
|
|
71
|
+
|
|
72
|
+
# ── System metadata ────────────────────────────────────────────────────────────
|
|
73
|
+
# country_code: ISO 3166-1 alpha-2
|
|
74
|
+
# region: broad geographic grouping used for display and filtering
|
|
75
|
+
# language: primary language of instruction
|
|
76
|
+
# level: national | state | provincial | international | exam_board
|
|
77
|
+
|
|
78
|
+
SYSTEM_META: dict[str, dict] = {
|
|
79
|
+
# ── Hub ──────────────────────────────────────────────────────────────────
|
|
80
|
+
"ccss": {"country": "United States", "country_code": "US", "region": "North America", "language": "English", "level": "national"},
|
|
81
|
+
# ── Canada ───────────────────────────────────────────────────────────────
|
|
82
|
+
"ca-ab": {"country": "Canada", "country_code": "CA", "region": "North America", "language": "English", "level": "provincial"},
|
|
83
|
+
"ca-bc": {"country": "Canada", "country_code": "CA", "region": "North America", "language": "English", "level": "provincial"},
|
|
84
|
+
"ca-mb": {"country": "Canada", "country_code": "CA", "region": "North America", "language": "English", "level": "provincial"},
|
|
85
|
+
"ca-nb": {"country": "Canada", "country_code": "CA", "region": "North America", "language": "English/French", "level": "provincial"},
|
|
86
|
+
"ca-on": {"country": "Canada", "country_code": "CA", "region": "North America", "language": "English", "level": "provincial"},
|
|
87
|
+
"ca-qc": {"country": "Canada", "country_code": "CA", "region": "North America", "language": "French", "level": "provincial"},
|
|
88
|
+
"ca-sk": {"country": "Canada", "country_code": "CA", "region": "North America", "language": "English", "level": "provincial"},
|
|
89
|
+
# ── Asia-Pacific ─────────────────────────────────────────────────────────
|
|
90
|
+
"au-acara": {"country": "Australia", "country_code": "AU", "region": "Asia-Pacific", "language": "English", "level": "national"},
|
|
91
|
+
"au-vic": {"country": "Australia", "country_code": "AU", "region": "Asia-Pacific", "language": "English", "level": "state"},
|
|
92
|
+
"hk-edb": {"country": "Hong Kong", "country_code": "HK", "region": "Asia-Pacific", "language": "English/Chinese", "level": "national"},
|
|
93
|
+
"jp-mext": {"country": "Japan", "country_code": "JP", "region": "Asia-Pacific", "language": "Japanese", "level": "national"},
|
|
94
|
+
"nz-moe": {"country": "New Zealand", "country_code": "NZ", "region": "Asia-Pacific", "language": "English", "level": "national"},
|
|
95
|
+
"ph-deped": {"country": "Philippines", "country_code": "PH", "region": "Asia-Pacific", "language": "English/Filipino", "level": "national"},
|
|
96
|
+
"sg-moe": {"country": "Singapore", "country_code": "SG", "region": "Asia-Pacific", "language": "English", "level": "national"},
|
|
97
|
+
# ── Europe ───────────────────────────────────────────────────────────────
|
|
98
|
+
"gb-sco": {"country": "Scotland", "country_code": "GB", "region": "Europe", "language": "English", "level": "national"},
|
|
99
|
+
"ie-ncca": {"country": "Ireland", "country_code": "IE", "region": "Europe", "language": "English/Irish", "level": "national"},
|
|
100
|
+
"uk-aqa": {"country": "England", "country_code": "GB", "region": "Europe", "language": "English", "level": "exam_board"},
|
|
101
|
+
"uk-nc": {"country": "England", "country_code": "GB", "region": "Europe", "language": "English", "level": "national"},
|
|
102
|
+
# ── South Asia ───────────────────────────────────────────────────────────
|
|
103
|
+
"in-ncert": {"country": "India", "country_code": "IN", "region": "South Asia", "language": "English", "level": "national"},
|
|
104
|
+
# ── Sub-Saharan Africa ───────────────────────────────────────────────────
|
|
105
|
+
"gh-nacca": {"country": "Ghana", "country_code": "GH", "region": "Sub-Saharan Africa", "language": "English", "level": "national"},
|
|
106
|
+
"rw-reb": {"country": "Rwanda", "country_code": "RW", "region": "Sub-Saharan Africa", "language": "English/French", "level": "national"},
|
|
107
|
+
"za-caps": {"country": "South Africa", "country_code": "ZA", "region": "Sub-Saharan Africa", "language": "English/Afrikaans", "level": "national"},
|
|
108
|
+
# ── International ────────────────────────────────────────────────────────
|
|
109
|
+
"cambridge": {"country": "International", "country_code": None, "region": "International", "language": "English", "level": "international"},
|
|
110
|
+
"ib-dp": {"country": "International", "country_code": None, "region": "International", "language": "English", "level": "international"},
|
|
111
|
+
"ib-myp": {"country": "International", "country_code": None, "region": "International", "language": "English", "level": "international"},
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_US_STATE_CODES = {
|
|
115
|
+
"ak","al","ar","az","ca","co","ct","dc","de","fl","ga","hi","ia","id","il",
|
|
116
|
+
"in","ks","ky","la","ma","md","me","mi","mn","mo","ms","mt","nc","nd","ne",
|
|
117
|
+
"nh","nj","nm","nv","ny","oh","ok","or","pa","ri","sc","sd","tn","tx","ut",
|
|
118
|
+
"va","vt","wa","wi","wv","wy",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_US_STATE_META = {"country": "United States", "country_code": "US", "region": "North America", "language": "English", "level": "state"}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _meta(system: str) -> dict:
|
|
125
|
+
if system in SYSTEM_META:
|
|
126
|
+
return SYSTEM_META[system]
|
|
127
|
+
if system in _US_STATE_CODES:
|
|
128
|
+
return _US_STATE_META
|
|
129
|
+
return {}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
mcp = FastMCP(
|
|
133
|
+
"intl-math-standards",
|
|
134
|
+
instructions=_build_instructions(),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ── DB helpers ────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
def _db() -> sqlite3.Connection:
|
|
141
|
+
conn = sqlite3.connect(DB_PATH)
|
|
142
|
+
conn.row_factory = sqlite3.Row
|
|
143
|
+
conn.execute("PRAGMA foreign_keys = ON")
|
|
144
|
+
return conn
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _expand_id(standard_id: str, system: str = "ccss") -> str:
|
|
148
|
+
"""Accept shortform '6.RP.A.3' and expand to 'CCSS.MATH.6.RP.A.3'.
|
|
149
|
+
|
|
150
|
+
Already-qualified IDs (containing '.MATH.' or starting with a
|
|
151
|
+
two-letter state/system prefix like 'TX.') are returned unchanged.
|
|
152
|
+
"""
|
|
153
|
+
sid = standard_id.strip()
|
|
154
|
+
upper = sid.upper()
|
|
155
|
+
if upper.startswith("CCSS."):
|
|
156
|
+
return sid
|
|
157
|
+
# Already a qualified non-CCSS ID (e.g. 'TX.MATH.5.3.K', 'FL.MATH.MA.5.NSO.2.5')
|
|
158
|
+
if ".MATH." in upper:
|
|
159
|
+
return sid
|
|
160
|
+
if system == "ccss":
|
|
161
|
+
return f"CCSS.MATH.{sid}"
|
|
162
|
+
return sid
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _grade_key(g: str) -> int:
|
|
166
|
+
try:
|
|
167
|
+
return GRADE_ORDER.index(g)
|
|
168
|
+
except ValueError:
|
|
169
|
+
return 99
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ── Embedding ─────────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
def _embed_query(text: str) -> np.ndarray:
|
|
175
|
+
import httpx
|
|
176
|
+
resp = httpx.post(
|
|
177
|
+
f"{OLLAMA_BASE_URL}/api/embed",
|
|
178
|
+
json={"model": EMBED_MODEL, "input": [text]},
|
|
179
|
+
timeout=30,
|
|
180
|
+
)
|
|
181
|
+
resp.raise_for_status()
|
|
182
|
+
return np.array(resp.json()["embeddings"][0], dtype=np.float32)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _cosine_scores(query_vec: np.ndarray, conn: sqlite3.Connection) -> list[tuple[float, str]]:
|
|
186
|
+
rows = conn.execute("SELECT standard_id, vector, dimensions FROM embeddings").fetchall()
|
|
187
|
+
if not rows:
|
|
188
|
+
return []
|
|
189
|
+
dim = rows[0]["dimensions"]
|
|
190
|
+
matrix = np.frombuffer(b"".join(r["vector"] for r in rows), dtype=np.float32).reshape(len(rows), dim)
|
|
191
|
+
ids = [r["standard_id"] for r in rows]
|
|
192
|
+
|
|
193
|
+
q = query_vec / (np.linalg.norm(query_vec) + 1e-9)
|
|
194
|
+
norms = np.linalg.norm(matrix, axis=1, keepdims=True) + 1e-9
|
|
195
|
+
scores = (matrix / norms) @ q
|
|
196
|
+
|
|
197
|
+
return sorted(zip(scores.tolist(), ids), reverse=True)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ── Tool 1: lookup_standard ───────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
@mcp.tool()
|
|
203
|
+
def lookup_standard(
|
|
204
|
+
standard_id: str,
|
|
205
|
+
system: str = "ccss",
|
|
206
|
+
include_elaborations: bool = False,
|
|
207
|
+
) -> str:
|
|
208
|
+
"""Fetch the full text, domain, cluster, prerequisites, and successors for a single standard.
|
|
209
|
+
|
|
210
|
+
Use this when the user provides a specific standard ID they want to read or understand.
|
|
211
|
+
|
|
212
|
+
standard_id: full ID like 'CCSS.MATH.6.RP.A.3' or shortform '6.RP.A.3' (for CCSS).
|
|
213
|
+
For other systems use the full ID, e.g. 'TX.MATH.5.3.K' or 'CA_BC.MATH.3.a'.
|
|
214
|
+
system: curriculum system code (default 'ccss'). See server instructions for all codes.
|
|
215
|
+
|
|
216
|
+
Returns the standard text, grade, domain, cluster, sub-standards (if any),
|
|
217
|
+
prerequisite standard IDs from the prior grade, and successor IDs for the next grade.
|
|
218
|
+
"""
|
|
219
|
+
sid = _expand_id(standard_id, system)
|
|
220
|
+
conn = _db()
|
|
221
|
+
|
|
222
|
+
row = conn.execute("SELECT * FROM standards WHERE id = ?", (sid,)).fetchone()
|
|
223
|
+
if not row:
|
|
224
|
+
# Suggest nearby IDs
|
|
225
|
+
suggestions = [
|
|
226
|
+
r[0] for r in conn.execute(
|
|
227
|
+
"SELECT id FROM standards WHERE system=? AND grade=? LIMIT 5",
|
|
228
|
+
(system, sid.split(".")[2] if "." in sid else ""),
|
|
229
|
+
).fetchall()
|
|
230
|
+
]
|
|
231
|
+
conn.close()
|
|
232
|
+
return json.dumps({"error": "standard_not_found", "queried_id": sid, "suggestions": suggestions})
|
|
233
|
+
|
|
234
|
+
std = dict(row)
|
|
235
|
+
|
|
236
|
+
sub_stds = conn.execute(
|
|
237
|
+
"SELECT id, text FROM sub_standards WHERE parent_id=? ORDER BY position",
|
|
238
|
+
(sid,),
|
|
239
|
+
).fetchall()
|
|
240
|
+
|
|
241
|
+
prerequisites = [
|
|
242
|
+
r[0] for r in conn.execute(
|
|
243
|
+
"SELECT target_id FROM standard_relationships WHERE source_id=? AND relationship='prerequisite'",
|
|
244
|
+
(sid,),
|
|
245
|
+
).fetchall()
|
|
246
|
+
]
|
|
247
|
+
successors = [
|
|
248
|
+
r[0] for r in conn.execute(
|
|
249
|
+
"SELECT target_id FROM standard_relationships WHERE source_id=? AND relationship='successor'",
|
|
250
|
+
(sid,),
|
|
251
|
+
).fetchall()
|
|
252
|
+
]
|
|
253
|
+
conn.close()
|
|
254
|
+
|
|
255
|
+
return json.dumps({
|
|
256
|
+
"id": std["id"],
|
|
257
|
+
"system": std["system"],
|
|
258
|
+
"grade": std["grade"],
|
|
259
|
+
"domain": std["domain"],
|
|
260
|
+
"cluster": std["cluster"],
|
|
261
|
+
"standard_text": std["standard_text"],
|
|
262
|
+
"sub_standards": [f"{r['id']} — {r['text']}" for r in sub_stds],
|
|
263
|
+
"prerequisites": prerequisites,
|
|
264
|
+
"successors": successors,
|
|
265
|
+
"source_url": std["source_url"],
|
|
266
|
+
"elaborations": None,
|
|
267
|
+
}, indent=2)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ── Tool 2: search_standards ──────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
@mcp.tool()
|
|
273
|
+
def search_standards(
|
|
274
|
+
query: str,
|
|
275
|
+
system: str = "ccss",
|
|
276
|
+
grade: str | None = None,
|
|
277
|
+
domain: str | None = None,
|
|
278
|
+
limit: int = 5,
|
|
279
|
+
) -> str:
|
|
280
|
+
"""Find math standards that match a natural language description of a concept or skill.
|
|
281
|
+
|
|
282
|
+
Use this when the user describes what they're looking for rather than citing a standard ID.
|
|
283
|
+
Examples: "adding fractions with unlike denominators", "geometric transformations grade 8",
|
|
284
|
+
"solving quadratic equations".
|
|
285
|
+
|
|
286
|
+
query: plain English description of the math concept or skill.
|
|
287
|
+
system: which curriculum to search (default 'ccss'). Call multiple times to compare systems.
|
|
288
|
+
grade: optional filter — single grade '5', range '6-8', or 'HS'. Grade codes: K 1 2 3 4 5 6 7 8 HS.
|
|
289
|
+
domain: optional keyword to restrict by domain name (e.g. 'geometry', 'algebra').
|
|
290
|
+
limit: number of results (default 5, max sensible ~10).
|
|
291
|
+
|
|
292
|
+
Returns standards ranked by semantic similarity with relevance scores (0–1).
|
|
293
|
+
"""
|
|
294
|
+
query_vec = _embed_query(query)
|
|
295
|
+
conn = _db()
|
|
296
|
+
scored = _cosine_scores(query_vec, conn)
|
|
297
|
+
|
|
298
|
+
results = []
|
|
299
|
+
for score, sid in scored:
|
|
300
|
+
if len(results) >= limit:
|
|
301
|
+
break
|
|
302
|
+
row = conn.execute(
|
|
303
|
+
"SELECT * FROM standards WHERE id=? AND system=?", (sid, system)
|
|
304
|
+
).fetchone()
|
|
305
|
+
if not row:
|
|
306
|
+
continue
|
|
307
|
+
std = dict(row)
|
|
308
|
+
|
|
309
|
+
if grade is not None:
|
|
310
|
+
# Accept "6", "6-8", or ["5","6","7"]
|
|
311
|
+
grades_wanted = _parse_grade_filter(grade)
|
|
312
|
+
if std["grade"] not in grades_wanted:
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
if domain is not None and domain.lower() not in std["domain"].lower():
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
results.append({
|
|
319
|
+
"id": std["id"],
|
|
320
|
+
"grade": std["grade"],
|
|
321
|
+
"domain": std["domain"],
|
|
322
|
+
"standard_text": std["standard_text"],
|
|
323
|
+
"relevance_score": round(score, 4),
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
conn.close()
|
|
327
|
+
return json.dumps(results, indent=2)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _parse_grade_filter(grade: str | list) -> set[str]:
|
|
331
|
+
if isinstance(grade, list):
|
|
332
|
+
return set(grade)
|
|
333
|
+
if "-" in grade and not grade.startswith("K"):
|
|
334
|
+
# range like "6-8"
|
|
335
|
+
parts = grade.split("-")
|
|
336
|
+
try:
|
|
337
|
+
lo, hi = int(parts[0]), int(parts[1])
|
|
338
|
+
return {str(g) for g in range(lo, hi + 1)}
|
|
339
|
+
except ValueError:
|
|
340
|
+
pass
|
|
341
|
+
return {grade}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# ── Tool 3: get_progression ───────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
@mcp.tool()
|
|
347
|
+
def get_progression(
|
|
348
|
+
concept: str,
|
|
349
|
+
system: str = "ccss",
|
|
350
|
+
grade_start: int | None = None,
|
|
351
|
+
grade_end: int | None = None,
|
|
352
|
+
) -> str:
|
|
353
|
+
"""Show how a math concept is introduced and built upon across grade levels.
|
|
354
|
+
|
|
355
|
+
Use this when the user asks questions like "how does fractions develop from grade 3 to 6?"
|
|
356
|
+
or "what's the full progression for proportional reasoning?" or "when is X introduced?"
|
|
357
|
+
|
|
358
|
+
concept: plain English name of the math concept (e.g. 'fractions', 'linear equations',
|
|
359
|
+
'place value', 'geometric transformations').
|
|
360
|
+
system: curriculum to trace (default 'ccss'). Try 'cambridge' or 'ib-myp' for comparison.
|
|
361
|
+
grade_start / grade_end: optional integer bounds to narrow the range (e.g. 3 and 8).
|
|
362
|
+
|
|
363
|
+
Returns the top matching standards per grade, ordered K through HS, showing how the
|
|
364
|
+
concept deepens over time.
|
|
365
|
+
"""
|
|
366
|
+
query_vec = _embed_query(concept)
|
|
367
|
+
conn = _db()
|
|
368
|
+
scored = _cosine_scores(query_vec, conn)
|
|
369
|
+
|
|
370
|
+
# Collect top standards per grade, filtered by grade range
|
|
371
|
+
by_grade: dict[str, list[dict]] = {}
|
|
372
|
+
for score, sid in scored:
|
|
373
|
+
if score < 0.5:
|
|
374
|
+
break
|
|
375
|
+
row = conn.execute(
|
|
376
|
+
"SELECT * FROM standards WHERE id=? AND system=?", (sid, system)
|
|
377
|
+
).fetchone()
|
|
378
|
+
if not row:
|
|
379
|
+
continue
|
|
380
|
+
std = dict(row)
|
|
381
|
+
g = std["grade"]
|
|
382
|
+
|
|
383
|
+
if grade_start is not None and _grade_key(g) < _grade_key(str(grade_start)):
|
|
384
|
+
continue
|
|
385
|
+
if grade_end is not None and _grade_key(g) > _grade_key(str(grade_end)):
|
|
386
|
+
continue
|
|
387
|
+
|
|
388
|
+
by_grade.setdefault(g, []).append({
|
|
389
|
+
"id": std["id"],
|
|
390
|
+
"text": std["standard_text"],
|
|
391
|
+
"score": round(score, 4),
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
conn.close()
|
|
395
|
+
|
|
396
|
+
gr_start = str(grade_start) if grade_start is not None else "K"
|
|
397
|
+
gr_end = str(grade_end) if grade_end is not None else "HS"
|
|
398
|
+
|
|
399
|
+
stages = []
|
|
400
|
+
for g in sorted(by_grade.keys(), key=_grade_key):
|
|
401
|
+
stds = sorted(by_grade[g], key=lambda x: -x["score"])[:3]
|
|
402
|
+
stages.append({
|
|
403
|
+
"grade": g,
|
|
404
|
+
"standards": [{"id": s["id"], "text": s["text"]} for s in stds],
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
return json.dumps({
|
|
408
|
+
"concept": concept,
|
|
409
|
+
"system": system,
|
|
410
|
+
"grade_range": f"{gr_start}–{gr_end}",
|
|
411
|
+
"stages": stages,
|
|
412
|
+
}, indent=2)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# ── Tool 4: map_standard ──────────────────────────────────────────────────────
|
|
416
|
+
|
|
417
|
+
@mcp.tool()
|
|
418
|
+
def map_standard(
|
|
419
|
+
standard_id: str,
|
|
420
|
+
from_system: str,
|
|
421
|
+
to_system: str,
|
|
422
|
+
confidence_threshold: float = 0.7,
|
|
423
|
+
) -> str:
|
|
424
|
+
"""Find the closest equivalent to a standard in a different curriculum system.
|
|
425
|
+
|
|
426
|
+
Use this when the user wants to compare curricula — e.g. "what is the CCSS equivalent
|
|
427
|
+
of this Texas standard?", "how does Singapore cover this?", or
|
|
428
|
+
"I'm moving from Ontario to the UK — what's the equivalent?"
|
|
429
|
+
|
|
430
|
+
Three strategies are tried in order:
|
|
431
|
+
1. Precomputed NLP crosswalk (direct or reverse through CCSS hub).
|
|
432
|
+
2. Two-hop CCSS bridge: source→CCSS→target (enables any-to-any comparison).
|
|
433
|
+
3. Semantic embedding fallback: embed source text, find nearest in target system.
|
|
434
|
+
Below-threshold precomputed results are always returned, flagged with below_threshold.
|
|
435
|
+
|
|
436
|
+
standard_id: the source standard ID (full form, e.g. 'TX.MATH.5.3.K').
|
|
437
|
+
from_system: system code of the source standard (e.g. 'tx', 'ca-on', 'sg-moe').
|
|
438
|
+
to_system: target system code (any indexed system).
|
|
439
|
+
confidence_threshold: minimum cosine similarity for primary results (default 0.7).
|
|
440
|
+
|
|
441
|
+
Returns matched standards with confidence score, grade alignment, and mapping method.
|
|
442
|
+
"""
|
|
443
|
+
sid = _expand_id(standard_id, from_system)
|
|
444
|
+
conn = _db()
|
|
445
|
+
|
|
446
|
+
src = conn.execute("SELECT * FROM standards WHERE id=?", (sid,)).fetchone()
|
|
447
|
+
if not src:
|
|
448
|
+
conn.close()
|
|
449
|
+
return json.dumps({"error": "standard_not_found", "queried_id": sid})
|
|
450
|
+
|
|
451
|
+
src_dict = dict(src)
|
|
452
|
+
|
|
453
|
+
# ── 1. Precomputed crosswalk above threshold ───────────────────────────────
|
|
454
|
+
mappings = conn.execute(
|
|
455
|
+
"""SELECT cm.*, s.standard_text AS target_text, s.grade AS target_grade,
|
|
456
|
+
s.domain AS target_domain
|
|
457
|
+
FROM crosswalk_mappings cm
|
|
458
|
+
JOIN standards s ON s.id = cm.target_id
|
|
459
|
+
WHERE cm.source_id = ?
|
|
460
|
+
AND cm.target_system = ?
|
|
461
|
+
AND cm.confidence_score >= ?
|
|
462
|
+
ORDER BY cm.confidence_score DESC""",
|
|
463
|
+
(sid, to_system, confidence_threshold),
|
|
464
|
+
).fetchall()
|
|
465
|
+
|
|
466
|
+
if mappings:
|
|
467
|
+
conn.close()
|
|
468
|
+
return json.dumps({
|
|
469
|
+
"source_id": src_dict["id"],
|
|
470
|
+
"target_curriculum": to_system,
|
|
471
|
+
"mapping_method": "precomputed_crosswalk",
|
|
472
|
+
"mappings": [
|
|
473
|
+
{
|
|
474
|
+
"target_id": m["target_id"],
|
|
475
|
+
"target_standard_text": m["target_text"],
|
|
476
|
+
"relationship": m["relationship"],
|
|
477
|
+
"confidence": m["confidence_score"],
|
|
478
|
+
"grade_delta": m["grade_delta"],
|
|
479
|
+
"grade_alignment": "exact" if m["grade_delta"] == 0 else (
|
|
480
|
+
f"{abs(m['grade_delta'])} year{'s' if abs(m['grade_delta']) > 1 else ''} "
|
|
481
|
+
f"{'later' if m['grade_delta'] > 0 else 'earlier'} in target"
|
|
482
|
+
),
|
|
483
|
+
"verified_by_human": bool(m["verified_by_human"]),
|
|
484
|
+
"notes": m["notes"],
|
|
485
|
+
}
|
|
486
|
+
for m in mappings
|
|
487
|
+
],
|
|
488
|
+
}, indent=2)
|
|
489
|
+
|
|
490
|
+
# ── 2. Best precomputed result below threshold ─────────────────────────────
|
|
491
|
+
best_below = conn.execute(
|
|
492
|
+
"""SELECT cm.*, s.standard_text AS target_text, s.grade AS target_grade,
|
|
493
|
+
s.domain AS target_domain
|
|
494
|
+
FROM crosswalk_mappings cm
|
|
495
|
+
JOIN standards s ON s.id = cm.target_id
|
|
496
|
+
WHERE cm.source_id = ?
|
|
497
|
+
AND cm.target_system = ?
|
|
498
|
+
ORDER BY cm.confidence_score DESC
|
|
499
|
+
LIMIT 1""",
|
|
500
|
+
(sid, to_system),
|
|
501
|
+
).fetchone()
|
|
502
|
+
|
|
503
|
+
# ── 3. Two-hop: source → CCSS → target (or reverse for CCSS sources) ──────
|
|
504
|
+
two_hop: list[dict] = []
|
|
505
|
+
if from_system == "ccss":
|
|
506
|
+
# Source IS the CCSS hub — find target-system standards pointing to it
|
|
507
|
+
rows = conn.execute(
|
|
508
|
+
"""SELECT cm.source_id, cm.confidence_score,
|
|
509
|
+
s.standard_text, s.grade, s.domain
|
|
510
|
+
FROM crosswalk_mappings cm
|
|
511
|
+
JOIN standards s ON s.id = cm.source_id
|
|
512
|
+
WHERE cm.target_id = ?
|
|
513
|
+
AND s.system = ?
|
|
514
|
+
ORDER BY cm.confidence_score DESC
|
|
515
|
+
LIMIT 5""",
|
|
516
|
+
(sid, to_system),
|
|
517
|
+
).fetchall()
|
|
518
|
+
for r in rows:
|
|
519
|
+
two_hop.append({
|
|
520
|
+
"target_id": r["source_id"],
|
|
521
|
+
"target_standard_text": r["standard_text"],
|
|
522
|
+
"via_ccss": sid,
|
|
523
|
+
"hop1_confidence": 1.0,
|
|
524
|
+
"hop2_confidence": round(r["confidence_score"], 4),
|
|
525
|
+
"combined_confidence": round(r["confidence_score"], 4),
|
|
526
|
+
"grade": r["grade"],
|
|
527
|
+
})
|
|
528
|
+
elif to_system != "ccss":
|
|
529
|
+
# Forward two-hop: source → CCSS intermediary → target
|
|
530
|
+
ccss_rows = conn.execute(
|
|
531
|
+
"""SELECT target_id, confidence_score
|
|
532
|
+
FROM crosswalk_mappings
|
|
533
|
+
WHERE source_id = ? AND target_system = 'ccss'
|
|
534
|
+
ORDER BY confidence_score DESC
|
|
535
|
+
LIMIT 3""",
|
|
536
|
+
(sid,),
|
|
537
|
+
).fetchall()
|
|
538
|
+
raw: list[dict] = []
|
|
539
|
+
for cr in ccss_rows:
|
|
540
|
+
ccss_id, ccss_conf = cr["target_id"], cr["confidence_score"]
|
|
541
|
+
target_rows = conn.execute(
|
|
542
|
+
"""SELECT cm.source_id, cm.confidence_score,
|
|
543
|
+
s.standard_text, s.grade, s.domain
|
|
544
|
+
FROM crosswalk_mappings cm
|
|
545
|
+
JOIN standards s ON s.id = cm.source_id
|
|
546
|
+
WHERE cm.target_id = ?
|
|
547
|
+
AND s.system = ?
|
|
548
|
+
ORDER BY cm.confidence_score DESC
|
|
549
|
+
LIMIT 3""",
|
|
550
|
+
(ccss_id, to_system),
|
|
551
|
+
).fetchall()
|
|
552
|
+
for tr in target_rows:
|
|
553
|
+
raw.append({
|
|
554
|
+
"target_id": tr["source_id"],
|
|
555
|
+
"target_standard_text": tr["standard_text"],
|
|
556
|
+
"via_ccss": ccss_id,
|
|
557
|
+
"hop1_confidence": round(ccss_conf, 4),
|
|
558
|
+
"hop2_confidence": round(tr["confidence_score"], 4),
|
|
559
|
+
"combined_confidence": round(ccss_conf * tr["confidence_score"], 4),
|
|
560
|
+
"grade": tr["grade"],
|
|
561
|
+
})
|
|
562
|
+
seen: set[str] = set()
|
|
563
|
+
for r in sorted(raw, key=lambda x: -x["combined_confidence"]):
|
|
564
|
+
if r["target_id"] not in seen:
|
|
565
|
+
seen.add(r["target_id"])
|
|
566
|
+
two_hop.append(r)
|
|
567
|
+
if len(two_hop) >= 5:
|
|
568
|
+
break
|
|
569
|
+
|
|
570
|
+
# ── 4. Semantic embedding fallback ────────────────────────────────────────
|
|
571
|
+
nearest_by_concept: list[dict] = []
|
|
572
|
+
try:
|
|
573
|
+
qvec = _embed_query(src_dict["standard_text"])
|
|
574
|
+
scored = _cosine_scores(qvec, conn)
|
|
575
|
+
for score, candidate_id in scored:
|
|
576
|
+
if len(nearest_by_concept) >= 3:
|
|
577
|
+
break
|
|
578
|
+
if score < 0.35:
|
|
579
|
+
break
|
|
580
|
+
row = conn.execute(
|
|
581
|
+
"SELECT * FROM standards WHERE id=? AND system=?", (candidate_id, to_system)
|
|
582
|
+
).fetchone()
|
|
583
|
+
if row:
|
|
584
|
+
nearest_by_concept.append({
|
|
585
|
+
"target_id": row["id"],
|
|
586
|
+
"target_standard_text": row["standard_text"],
|
|
587
|
+
"grade": row["grade"],
|
|
588
|
+
"semantic_similarity": round(score, 4),
|
|
589
|
+
})
|
|
590
|
+
except Exception:
|
|
591
|
+
pass
|
|
592
|
+
|
|
593
|
+
conn.close()
|
|
594
|
+
|
|
595
|
+
# ── Build no-match response ────────────────────────────────────────────────
|
|
596
|
+
response: dict = {
|
|
597
|
+
"source_id": src_dict["id"],
|
|
598
|
+
"source_text": src_dict["standard_text"],
|
|
599
|
+
"target_curriculum": to_system,
|
|
600
|
+
"result": "no_precomputed_mapping_above_threshold",
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if best_below:
|
|
604
|
+
response["best_precomputed_below_threshold"] = {
|
|
605
|
+
"target_id": best_below["target_id"],
|
|
606
|
+
"target_standard_text": best_below["target_text"],
|
|
607
|
+
"confidence": round(best_below["confidence_score"], 4),
|
|
608
|
+
"below_threshold": True,
|
|
609
|
+
"threshold_used": confidence_threshold,
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if two_hop:
|
|
613
|
+
response["two_hop_via_ccss"] = two_hop
|
|
614
|
+
|
|
615
|
+
if nearest_by_concept:
|
|
616
|
+
response["nearest_by_concept"] = nearest_by_concept
|
|
617
|
+
|
|
618
|
+
if not best_below and not two_hop and not nearest_by_concept:
|
|
619
|
+
response["result"] = "no_mapping_found"
|
|
620
|
+
try:
|
|
621
|
+
_c = sqlite3.connect(DB_PATH)
|
|
622
|
+
response["available_systems"] = [
|
|
623
|
+
r[0] for r in _c.execute(
|
|
624
|
+
"SELECT DISTINCT system FROM standards ORDER BY system"
|
|
625
|
+
).fetchall()
|
|
626
|
+
]
|
|
627
|
+
_c.close()
|
|
628
|
+
except Exception:
|
|
629
|
+
pass
|
|
630
|
+
|
|
631
|
+
return json.dumps(response, indent=2)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
# ── Tool 5: list_systems ──────────────────────────────────────────────────────
|
|
635
|
+
|
|
636
|
+
@mcp.tool()
|
|
637
|
+
def list_systems() -> str:
|
|
638
|
+
"""Return a live count of every curriculum system currently in the database.
|
|
639
|
+
|
|
640
|
+
Use this to see exactly which systems are available, how many standards each has,
|
|
641
|
+
and overall DB stats. Unlike the server instructions (which are set at startup),
|
|
642
|
+
this always reflects the current state of the database.
|
|
643
|
+
|
|
644
|
+
Returns system codes, standard counts, embedding coverage, and crosswalk coverage.
|
|
645
|
+
"""
|
|
646
|
+
conn = _db()
|
|
647
|
+
|
|
648
|
+
systems = conn.execute(
|
|
649
|
+
"""SELECT s.system,
|
|
650
|
+
COUNT(s.id) AS standards,
|
|
651
|
+
COUNT(e.standard_id) AS embedded,
|
|
652
|
+
COUNT(cm.source_id) AS crosswalked
|
|
653
|
+
FROM standards s
|
|
654
|
+
LEFT JOIN embeddings e ON e.standard_id = s.id
|
|
655
|
+
LEFT JOIN crosswalk_mappings cm ON cm.source_id = s.id
|
|
656
|
+
GROUP BY s.system
|
|
657
|
+
ORDER BY s.system"""
|
|
658
|
+
).fetchall()
|
|
659
|
+
|
|
660
|
+
total_std = conn.execute("SELECT COUNT(*) FROM standards").fetchone()[0]
|
|
661
|
+
total_emb = conn.execute("SELECT COUNT(*) FROM embeddings").fetchone()[0]
|
|
662
|
+
total_xwalk = conn.execute("SELECT COUNT(*) FROM crosswalk_mappings").fetchone()[0]
|
|
663
|
+
total_rel = conn.execute("SELECT COUNT(*) FROM standard_relationships").fetchone()[0]
|
|
664
|
+
conn.close()
|
|
665
|
+
|
|
666
|
+
system_rows = []
|
|
667
|
+
for r in systems:
|
|
668
|
+
m = _meta(r["system"])
|
|
669
|
+
system_rows.append({
|
|
670
|
+
"system": r["system"],
|
|
671
|
+
"standards": r["standards"],
|
|
672
|
+
"embedded": r["embedded"],
|
|
673
|
+
"crosswalked": r["crosswalked"],
|
|
674
|
+
"country": m.get("country"),
|
|
675
|
+
"country_code": m.get("country_code"),
|
|
676
|
+
"region": m.get("region"),
|
|
677
|
+
"language": m.get("language"),
|
|
678
|
+
"level": m.get("level"),
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
return json.dumps({
|
|
682
|
+
"totals": {
|
|
683
|
+
"systems": len(systems),
|
|
684
|
+
"standards": total_std,
|
|
685
|
+
"embeddings": total_emb,
|
|
686
|
+
"crosswalk_mappings": total_xwalk,
|
|
687
|
+
"relationships": total_rel,
|
|
688
|
+
},
|
|
689
|
+
"systems": system_rows,
|
|
690
|
+
}, indent=2)
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def main() -> None:
|
|
694
|
+
mcp.run()
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
if __name__ == "__main__":
|
|
698
|
+
main()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: standardgraph
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: 20,000+ math standards across 75+ curriculum systems, cross-referenced to CCSS via NLP — accessible as a Claude MCP server
|
|
5
|
+
Author: StandardGraph
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/swoopeagle/standardgraph
|
|
8
|
+
Project-URL: Repository, https://github.com/swoopeagle/standardgraph
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/swoopeagle/standardgraph/issues
|
|
10
|
+
Keywords: mcp,math,education,curriculum,standards,ccss,claude
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Education
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Education
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: fastmcp>=0.3.0
|
|
20
|
+
Requires-Dist: httpx>=0.27.0
|
|
21
|
+
Requires-Dist: numpy>=1.26.0
|
|
22
|
+
|
|
23
|
+
# standardgraph
|
|
24
|
+
|
|
25
|
+
**20,000+ math standards across 75+ curriculum systems, accessible as a Claude MCP server.**
|
|
26
|
+
|
|
27
|
+
Covers the US (CCSS + all 50 states), Canada, Australia, UK, Singapore, Japan, New Zealand, Ireland, Hong Kong, India, Ghana, South Africa, Rwanda, Cambridge International, IB MYP/DP, and more — all cross-referenced to CCSS via NLP semantic similarity.
|
|
28
|
+
|
|
29
|
+
## Install (macOS)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
curl -fsSL https://raw.githubusercontent.com/swoopeagle/standardgraph/main/install.sh | bash
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Restart Claude Desktop and look for the hammer 🔨 icon.
|
|
36
|
+
|
|
37
|
+
## Manual setup
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
mkdir -p ~/.standardgraph
|
|
41
|
+
curl -L https://huggingface.co/datasets/swoopeagle/standardgraph/resolve/main/common_core.db \
|
|
42
|
+
-o ~/.standardgraph/common_core.db
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"standardgraph": {
|
|
51
|
+
"command": "uvx",
|
|
52
|
+
"args": ["standardgraph"],
|
|
53
|
+
"env": { "DB_PATH": "/Users/YOUR_USERNAME/.standardgraph/common_core.db" }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Tools
|
|
60
|
+
|
|
61
|
+
- `search_standards` — find standards by concept description
|
|
62
|
+
- `lookup_standard` — fetch a standard by ID with prerequisites/successors
|
|
63
|
+
- `get_progression` — trace how a concept develops across grade levels
|
|
64
|
+
- `map_standard` — find the equivalent standard in another curriculum
|
|
65
|
+
- `list_systems` — see all indexed systems with live counts
|
|
66
|
+
|
|
67
|
+
Full documentation: https://github.com/swoopeagle/standardgraph
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
common_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
common_core/config.py,sha256=E5hA9JdyQlWJmIlPYVtkJa9tOmDusOCiPnzqkCZhYyI,538
|
|
3
|
+
common_core/server.py,sha256=MKjEdsLv79KVmrx9TOuaW5pPXloVHGWCDh5DSM2XssI,31503
|
|
4
|
+
standardgraph-1.0.0.dist-info/METADATA,sha256=tTKT_XtmhIGaC0LAQYKM4peQBaz5jo8OuHyZpZieIwE,2378
|
|
5
|
+
standardgraph-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
standardgraph-1.0.0.dist-info/entry_points.txt,sha256=lNVfHt1e4xgmtHTlIvdZkgLv_xl9X-WUJdlrML03rRY,58
|
|
7
|
+
standardgraph-1.0.0.dist-info/top_level.txt,sha256=2s3t9_qaNwV4JABX3a61rRz6aVT9aEnsYdkxBXaWhMU,12
|
|
8
|
+
standardgraph-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
common_core
|