@oga-mcp/server 1.0.0
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.
- package/README.md +137 -0
- package/dist/api-client.d.ts +255 -0
- package/dist/api-client.js +260 -0
- package/dist/api-client.js.map +1 -0
- package/dist/methodology-data.d.ts +40 -0
- package/dist/methodology-data.js +179 -0
- package/dist/methodology-data.js.map +1 -0
- package/dist/server.d.ts +28 -0
- package/dist/server.js +173 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/area-brief-audiences.d.ts +40 -0
- package/dist/tools/area-brief-audiences.js +131 -0
- package/dist/tools/area-brief-audiences.js.map +1 -0
- package/dist/tools/area-brief-format.d.ts +15 -0
- package/dist/tools/area-brief-format.js +142 -0
- package/dist/tools/area-brief-format.js.map +1 -0
- package/dist/tools/area-brief.d.ts +48 -0
- package/dist/tools/area-brief.js +82 -0
- package/dist/tools/area-brief.js.map +1 -0
- package/dist/tools/compare-postcodes.d.ts +57 -0
- package/dist/tools/compare-postcodes.js +148 -0
- package/dist/tools/compare-postcodes.js.map +1 -0
- package/dist/tools/engine-version.d.ts +23 -0
- package/dist/tools/engine-version.js +35 -0
- package/dist/tools/engine-version.js.map +1 -0
- package/dist/tools/find-areas.d.ts +38 -0
- package/dist/tools/find-areas.js +58 -0
- package/dist/tools/find-areas.js.map +1 -0
- package/dist/tools/find-peers.d.ts +44 -0
- package/dist/tools/find-peers.js +77 -0
- package/dist/tools/find-peers.js.map +1 -0
- package/dist/tools/get-area-signals.d.ts +39 -0
- package/dist/tools/get-area-signals.js +60 -0
- package/dist/tools/get-area-signals.js.map +1 -0
- package/dist/tools/get-portfolio-changes.d.ts +58 -0
- package/dist/tools/get-portfolio-changes.js +121 -0
- package/dist/tools/get-portfolio-changes.js.map +1 -0
- package/dist/tools/get-signals-by-category.d.ts +44 -0
- package/dist/tools/get-signals-by-category.js +67 -0
- package/dist/tools/get-signals-by-category.js.map +1 -0
- package/dist/tools/intelligence-format.d.ts +28 -0
- package/dist/tools/intelligence-format.js +197 -0
- package/dist/tools/intelligence-format.js.map +1 -0
- package/dist/tools/methodology-for.d.ts +34 -0
- package/dist/tools/methodology-for.js +71 -0
- package/dist/tools/methodology-for.js.map +1 -0
- package/dist/tools/score-postcode.d.ts +48 -0
- package/dist/tools/score-postcode.js +108 -0
- package/dist/tools/score-postcode.js.map +1 -0
- package/dist/tools/signals-format.d.ts +11 -0
- package/dist/tools/signals-format.js +116 -0
- package/dist/tools/signals-format.js.map +1 -0
- package/dist/tools/watch-portfolio.d.ts +50 -0
- package/dist/tools/watch-portfolio.js +126 -0
- package/dist/tools/watch-portfolio.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Methodology snippets per dimension. Baked into the MCP server so
|
|
3
|
+
* `methodology_for` returns instant explanations without a network round-trip.
|
|
4
|
+
*
|
|
5
|
+
* Source of truth: https://www.onegoodarea.com/methodology
|
|
6
|
+
* Update this file when the methodology page changes — the engine version
|
|
7
|
+
* is stamped on every API response so a stale snippet here just means the
|
|
8
|
+
* MCP description lags real scoring by a release.
|
|
9
|
+
*/
|
|
10
|
+
export interface DimensionMethodology {
|
|
11
|
+
dimension: string;
|
|
12
|
+
intents: string[];
|
|
13
|
+
source: string;
|
|
14
|
+
summary: string;
|
|
15
|
+
weights: Record<string, number>;
|
|
16
|
+
}
|
|
17
|
+
export declare const METHODOLOGY: DimensionMethodology[];
|
|
18
|
+
export declare const ENGINE: {
|
|
19
|
+
readonly version: "2.0.2";
|
|
20
|
+
readonly released: "2026-05-14";
|
|
21
|
+
readonly changelog: readonly [{
|
|
22
|
+
readonly version: "2.0.2";
|
|
23
|
+
readonly date: "2026-05-14";
|
|
24
|
+
readonly summary: "OpenStreetMap reliability hardening — Overpass timeout doubled, retry-once-with-jitter, errors now logged via logger.warn(). Resolves city-centre Transport NONE-confidence on Manchester/Birmingham/Edinburgh/Cardiff/York. Scoring formulas byte-identical to v2.0.1.";
|
|
25
|
+
}, {
|
|
26
|
+
readonly version: "2.0.1";
|
|
27
|
+
readonly date: "2026-05-14";
|
|
28
|
+
readonly summary: "Variance-aware property confidence rubric: HIGH now requires both >=50 transactions AND <=15% absolute YoY change. Resolves over-confident scores in central York and similar low-volume postcodes. No scoring formula changes.";
|
|
29
|
+
}, {
|
|
30
|
+
readonly version: "2.0.0";
|
|
31
|
+
readonly date: "2026-04-26";
|
|
32
|
+
readonly summary: "Confidence scoring per dimension. Engine version stamp on every response. Source attribution per signal. Confidence reasons explain WHY each dimension's data quality is what it is.";
|
|
33
|
+
}, {
|
|
34
|
+
readonly version: "1.x";
|
|
35
|
+
readonly date: "pre 2026-04-26";
|
|
36
|
+
readonly summary: "Initial deterministic scoring engine. 7 public datasets integrated. 4 preset compositions (formerly intents). LSOA-level scoring. JSON API.";
|
|
37
|
+
}];
|
|
38
|
+
};
|
|
39
|
+
/** Match a user's dimension query against the canonical names (case-insensitive, partial match). */
|
|
40
|
+
export declare function findDimension(query: string): DimensionMethodology | null;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Methodology snippets per dimension. Baked into the MCP server so
|
|
3
|
+
* `methodology_for` returns instant explanations without a network round-trip.
|
|
4
|
+
*
|
|
5
|
+
* Source of truth: https://www.onegoodarea.com/methodology
|
|
6
|
+
* Update this file when the methodology page changes — the engine version
|
|
7
|
+
* is stamped on every API response so a stale snippet here just means the
|
|
8
|
+
* MCP description lags real scoring by a release.
|
|
9
|
+
*/
|
|
10
|
+
export const METHODOLOGY = [
|
|
11
|
+
{
|
|
12
|
+
dimension: "Safety & Crime",
|
|
13
|
+
intents: ["moving", "research"],
|
|
14
|
+
source: "Police.uk (last 3 months street-level crime data)",
|
|
15
|
+
summary: "Penalises rising crime, rewards falling crime, weights violent crime concentration. Benchmarked against the area's urban/suburban/rural classification so rural postcodes are not unfairly penalised against city-centre baselines.",
|
|
16
|
+
weights: { moving: 25, business: 15, investing: 15, research: 20 },
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
dimension: "Schools & Education",
|
|
20
|
+
intents: ["moving"],
|
|
21
|
+
source: "Ofsted inspection ratings (England), Estyn (Wales), Education Scotland (planned)",
|
|
22
|
+
summary: "School and educational facility density nearby with diminishing-returns curve. One Outstanding-rated school within 1.5km matters more than many middling ones further away.",
|
|
23
|
+
weights: { moving: 20, business: 0, investing: 10, research: 20 },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
dimension: "Transport & Commute",
|
|
27
|
+
intents: ["moving", "business", "research"],
|
|
28
|
+
source: "OpenStreetMap (rail stations, bus stops, road network)",
|
|
29
|
+
summary: "Rail and bus connectivity combined into a single accessibility score. Benchmarked against area type so a rural postcode with 1 train station ranks higher than a city-centre postcode with 1 train station.",
|
|
30
|
+
weights: { moving: 20, business: 15, investing: 15, research: 20 },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
dimension: "Daily Amenities",
|
|
34
|
+
intents: ["moving", "research"],
|
|
35
|
+
source: "OpenStreetMap (food/drink, healthcare, shops, parks/leisure, retail)",
|
|
36
|
+
summary: "Weighted composite across education, food and drink, healthcare, retail, and green spaces. Each category normalised against area-type benchmarks to avoid penalising rural areas with fewer amenities by absolute count.",
|
|
37
|
+
weights: { moving: 15, business: 0, investing: 10, research: 20 },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
dimension: "Cost of Living",
|
|
41
|
+
intents: ["moving"],
|
|
42
|
+
source: "HM Land Registry sold prices (primary), IMD 2025 deprivation data (fallback)",
|
|
43
|
+
summary: "Uses Land Registry sold prices as the primary input. Scored as a ratio of local median to national median. Falls back to IMD deprivation deciles when Land Registry data is unavailable for the postcode area.",
|
|
44
|
+
weights: { moving: 20, business: 0, investing: 0, research: 0 },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
dimension: "Foot Traffic & Demand",
|
|
48
|
+
intents: ["business"],
|
|
49
|
+
source: "OpenStreetMap (commercial activity density), transport connectivity",
|
|
50
|
+
summary: "Transport connectivity combined with commercial activity density. Strong rail, bus, and retail presence indicates higher natural footfall. Used by retail, F&B, and hospitality site-selection teams.",
|
|
51
|
+
weights: { business: 30 },
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
dimension: "Competition Density",
|
|
55
|
+
intents: ["business"],
|
|
56
|
+
source: "OpenStreetMap (existing commercial venues by category)",
|
|
57
|
+
summary: "Counts existing similar venues nearby. Lower density = less competitive saturation. Doesn't differentiate by venue size or quality — purely a density metric.",
|
|
58
|
+
weights: { business: 20 },
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
dimension: "Local Spending Power",
|
|
62
|
+
intents: ["business"],
|
|
63
|
+
source: "HM Land Registry, IMD 2025",
|
|
64
|
+
summary: "Combines property values with deprivation scores to estimate disposable income in the area. Higher spending power supports premium retail and F&B.",
|
|
65
|
+
weights: { business: 20 },
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
dimension: "Commercial Costs",
|
|
69
|
+
intents: ["business"],
|
|
70
|
+
source: "HM Land Registry (residential as proxy until commercial data lands)",
|
|
71
|
+
summary: "Estimates rent and leasehold cost using residential prices as a proxy. Inverted scoring: high cost = low score. Replace with commercial rent data when AR-134 (address-level) ships.",
|
|
72
|
+
weights: { business: 15 },
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
dimension: "Price Growth",
|
|
76
|
+
intents: ["investing"],
|
|
77
|
+
source: "HM Land Registry (5-year price history)",
|
|
78
|
+
summary: "Year-on-year median price growth over 5 years. Smoothed to avoid single-quarter spikes. Used by buy-to-let and build-to-rent operators to forecast capital appreciation.",
|
|
79
|
+
weights: { investing: 25 },
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
dimension: "Rental Yield",
|
|
83
|
+
intents: ["investing"],
|
|
84
|
+
source: "HM Land Registry (sold prices), VOA/Rightmove proxies (rent estimates)",
|
|
85
|
+
summary: "Annual gross rental income as a percentage of capital value. London-weighted benchmarks vs national. Below 4% = soft, 4-6% = market, above 6% = strong yield play.",
|
|
86
|
+
weights: { investing: 25 },
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
dimension: "Regeneration & Infrastructure",
|
|
90
|
+
intents: ["investing"],
|
|
91
|
+
source: "OpenStreetMap (construction/development markers), IMD 2025 (infrastructure indicators)",
|
|
92
|
+
summary: "Signals of imminent regeneration: new construction nearby, transport upgrades, low IMD score with rising property prices. Forward-looking indicator for capital appreciation.",
|
|
93
|
+
weights: { investing: 20 },
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
dimension: "Tenant Demand",
|
|
97
|
+
intents: ["investing"],
|
|
98
|
+
source: "IMD 2025, Police.uk (proxy via local density and stability)",
|
|
99
|
+
summary: "Estimates rental demand from population density, employment proxies via IMD, and historic crime stability. Weak proxy until address-level demographic data lands.",
|
|
100
|
+
weights: { investing: 15 },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
dimension: "Risk Factors",
|
|
104
|
+
intents: ["investing"],
|
|
105
|
+
source: "Environment Agency (flood zones), Police.uk (crime stability)",
|
|
106
|
+
summary: "Flood risk zones, active flood warnings, crime trend volatility. Inverted scoring: more risk = lower score.",
|
|
107
|
+
weights: { investing: 15 },
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
dimension: "Transport Links",
|
|
111
|
+
intents: ["research"],
|
|
112
|
+
source: "OpenStreetMap",
|
|
113
|
+
summary: "Same as Transport & Commute but presented as a neutral baseline metric for the research intent (no decision-side weighting).",
|
|
114
|
+
weights: { research: 20 },
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
dimension: "Amenities & Services",
|
|
118
|
+
intents: ["research"],
|
|
119
|
+
source: "OpenStreetMap",
|
|
120
|
+
summary: "Same composite as Daily Amenities but presented neutrally for the research intent.",
|
|
121
|
+
weights: { research: 20 },
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
dimension: "Demographics & Economy",
|
|
125
|
+
intents: ["research"],
|
|
126
|
+
source: "IMD 2025 (England), WIMD 2019 (Wales), SIMD 2020 (Scotland)",
|
|
127
|
+
summary: "Official deprivation indices. Maps decile ranking to a 0-100 score that reflects the socioeconomic profile of the neighbourhood. Decile 1 = most deprived, decile 10 = least deprived.",
|
|
128
|
+
weights: { research: 20 },
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
dimension: "Environment & Quality",
|
|
132
|
+
intents: ["research"],
|
|
133
|
+
source: "Environment Agency, OpenStreetMap (green space)",
|
|
134
|
+
summary: "Combines flood risk zones, active flood warnings, and green space availability. Areas with no flood risk and good park access score highest.",
|
|
135
|
+
weights: { research: 20 },
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
/* AR-364: bumped to v2.0.2 (the live engine version). The MCP's static
|
|
139
|
+
snapshot is intentionally a fallback — the score_postcode tool already
|
|
140
|
+
echoes the live engine_version from each response. A future story
|
|
141
|
+
(S3) should make engine_version read the live value from the /me
|
|
142
|
+
startup call rather than this constant. */
|
|
143
|
+
export const ENGINE = {
|
|
144
|
+
version: "2.0.2",
|
|
145
|
+
released: "2026-05-14",
|
|
146
|
+
changelog: [
|
|
147
|
+
{
|
|
148
|
+
version: "2.0.2",
|
|
149
|
+
date: "2026-05-14",
|
|
150
|
+
summary: "OpenStreetMap reliability hardening — Overpass timeout doubled, retry-once-with-jitter, errors now logged via logger.warn(). Resolves city-centre Transport NONE-confidence on Manchester/Birmingham/Edinburgh/Cardiff/York. Scoring formulas byte-identical to v2.0.1.",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
version: "2.0.1",
|
|
154
|
+
date: "2026-05-14",
|
|
155
|
+
summary: "Variance-aware property confidence rubric: HIGH now requires both >=50 transactions AND <=15% absolute YoY change. Resolves over-confident scores in central York and similar low-volume postcodes. No scoring formula changes.",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
version: "2.0.0",
|
|
159
|
+
date: "2026-04-26",
|
|
160
|
+
summary: "Confidence scoring per dimension. Engine version stamp on every response. Source attribution per signal. Confidence reasons explain WHY each dimension's data quality is what it is.",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
version: "1.x",
|
|
164
|
+
date: "pre 2026-04-26",
|
|
165
|
+
summary: "Initial deterministic scoring engine. 7 public datasets integrated. 4 preset compositions (formerly intents). LSOA-level scoring. JSON API.",
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
/** Match a user's dimension query against the canonical names (case-insensitive, partial match). */
|
|
170
|
+
export function findDimension(query) {
|
|
171
|
+
const q = query.toLowerCase().trim();
|
|
172
|
+
// Exact match first
|
|
173
|
+
const exact = METHODOLOGY.find((d) => d.dimension.toLowerCase() === q);
|
|
174
|
+
if (exact)
|
|
175
|
+
return exact;
|
|
176
|
+
// Substring match (e.g. "safety" matches "Safety & Crime")
|
|
177
|
+
return METHODOLOGY.find((d) => d.dimension.toLowerCase().includes(q)) ?? null;
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=methodology-data.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"methodology-data.js","sourceRoot":"","sources":["../src/methodology-data.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAUH,MAAM,CAAC,MAAM,WAAW,GAA2B;IACjD;QACE,SAAS,EAAE,gBAAgB;QAC3B,OAAO,EAAE,CAAC,QAAQ,EAAE,UAAU,CAAC;QAC/B,MAAM,EAAE,mDAAmD;QAC3D,OAAO,EACL,qOAAqO;QACvO,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;KACnE;IACD;QACE,SAAS,EAAE,qBAAqB;QAChC,OAAO,EAAE,CAAC,QAAQ,CAAC;QACnB,MAAM,EAAE,kFAAkF;QAC1F,OAAO,EACL,6KAA6K;QAC/K,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;KAClE;IACD;QACE,SAAS,EAAE,qBAAqB;QAChC,OAAO,EAAE,CAAC,QAAQ,EAAE,UAAU,EAAE,UAAU,CAAC;QAC3C,MAAM,EAAE,wDAAwD;QAChE,OAAO,EACL,6MAA6M;QAC/M,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;KACnE;IACD;QACE,SAAS,EAAE,iBAAiB;QAC5B,OAAO,EAAE,CAAC,QAAQ,EAAE,UAAU,CAAC;QAC/B,MAAM,EAAE,sEAAsE;QAC9E,OAAO,EACL,0NAA0N;QAC5N,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;KAClE;IACD;QACE,SAAS,EAAE,gBAAgB;QAC3B,OAAO,EAAE,CAAC,QAAQ,CAAC;QACnB,MAAM,EAAE,8EAA8E;QACtF,OAAO,EACL,gNAAgN;QAClN,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE;KAChE;IACD;QACE,SAAS,EAAE,uBAAuB;QAClC,OAAO,EAAE,CAAC,UAAU,CAAC;QACrB,MAAM,EAAE,qEAAqE;QAC7E,OAAO,EACL,uMAAuM;QACzM,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;KAC1B;IACD;QACE,SAAS,EAAE,qBAAqB;QAChC,OAAO,EAAE,CAAC,UAAU,CAAC;QACrB,MAAM,EAAE,wDAAwD;QAChE,OAAO,EACL,+JAA+J;QACjK,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;KAC1B;IACD;QACE,SAAS,EAAE,sBAAsB;QACjC,OAAO,EAAE,CAAC,UAAU,CAAC;QACrB,MAAM,EAAE,4BAA4B;QACpC,OAAO,EACL,oJAAoJ;QACtJ,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;KAC1B;IACD;QACE,SAAS,EAAE,kBAAkB;QAC7B,OAAO,EAAE,CAAC,UAAU,CAAC;QACrB,MAAM,EAAE,qEAAqE;QAC7E,OAAO,EACL,sLAAsL;QACxL,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;KAC1B;IACD;QACE,SAAS,EAAE,cAAc;QACzB,OAAO,EAAE,CAAC,WAAW,CAAC;QACtB,MAAM,EAAE,yCAAyC;QACjD,OAAO,EACL,0KAA0K;QAC5K,OAAO,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE;KAC3B;IACD;QACE,SAAS,EAAE,cAAc;QACzB,OAAO,EAAE,CAAC,WAAW,CAAC;QACtB,MAAM,EAAE,wEAAwE;QAChF,OAAO,EACL,oKAAoK;QACtK,OAAO,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE;KAC3B;IACD;QACE,SAAS,EAAE,+BAA+B;QAC1C,OAAO,EAAE,CAAC,WAAW,CAAC;QACtB,MAAM,EAAE,wFAAwF;QAChG,OAAO,EACL,+KAA+K;QACjL,OAAO,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE;KAC3B;IACD;QACE,SAAS,EAAE,eAAe;QAC1B,OAAO,EAAE,CAAC,WAAW,CAAC;QACtB,MAAM,EAAE,6DAA6D;QACrE,OAAO,EACL,mKAAmK;QACrK,OAAO,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE;KAC3B;IACD;QACE,SAAS,EAAE,cAAc;QACzB,OAAO,EAAE,CAAC,WAAW,CAAC;QACtB,MAAM,EAAE,+DAA+D;QACvE,OAAO,EACL,6GAA6G;QAC/G,OAAO,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE;KAC3B;IACD;QACE,SAAS,EAAE,iBAAiB;QAC5B,OAAO,EAAE,CAAC,UAAU,CAAC;QACrB,MAAM,EAAE,eAAe;QACvB,OAAO,EACL,8HAA8H;QAChI,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;KAC1B;IACD;QACE,SAAS,EAAE,sBAAsB;QACjC,OAAO,EAAE,CAAC,UAAU,CAAC;QACrB,MAAM,EAAE,eAAe;QACvB,OAAO,EACL,oFAAoF;QACtF,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;KAC1B;IACD;QACE,SAAS,EAAE,wBAAwB;QACnC,OAAO,EAAE,CAAC,UAAU,CAAC;QACrB,MAAM,EAAE,6DAA6D;QACrE,OAAO,EACL,wLAAwL;QAC1L,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;KAC1B;IACD;QACE,SAAS,EAAE,uBAAuB;QAClC,OAAO,EAAE,CAAC,UAAU,CAAC;QACrB,MAAM,EAAE,iDAAiD;QACzD,OAAO,EACL,8IAA8I;QAChJ,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;KAC1B;CACF,CAAC;AAEF;;;;6CAI6C;AAC7C,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,OAAO,EAAE,OAAO;IAChB,QAAQ,EAAE,YAAY;IACtB,SAAS,EAAE;QACT;YACE,OAAO,EAAE,OAAO;YAChB,IAAI,EAAE,YAAY;YAClB,OAAO,EACL,yQAAyQ;SAC5Q;QACD;YACE,OAAO,EAAE,OAAO;YAChB,IAAI,EAAE,YAAY;YAClB,OAAO,EACL,iOAAiO;SACpO;QACD;YACE,OAAO,EAAE,OAAO;YAChB,IAAI,EAAE,YAAY;YAClB,OAAO,EACL,sLAAsL;SACzL;QACD;YACE,OAAO,EAAE,KAAK;YACd,IAAI,EAAE,gBAAgB;YACtB,OAAO,EACL,6IAA6I;SAChJ;KACF;CACO,CAAC;AAEX,oGAAoG;AACpG,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,MAAM,CAAC,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;IACrC,oBAAoB;IACpB,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC;IACvE,IAAI,KAAK;QAAE,OAAO,KAAK,CAAC;IACxB,2DAA2D;IAC3D,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AAChF,CAAC"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OneGoodArea MCP server — entry point.
|
|
4
|
+
*
|
|
5
|
+
* Exposes the OneGoodArea engine as MCP tools so Claude Desktop / Cursor /
|
|
6
|
+
* any MCP-compatible client can score UK areas inline.
|
|
7
|
+
*
|
|
8
|
+
* Auth: reads OOGA_API_KEY from env (the customer's API key from
|
|
9
|
+
* https://www.onegoodarea.com/dashboard). Keys start with `oga_`.
|
|
10
|
+
*
|
|
11
|
+
* Base URL: defaults to https://onegoodarea.onrender.com (the live API).
|
|
12
|
+
* Override via OOGA_API_BASE env (useful for local dev against
|
|
13
|
+
* `cd apps/api && npm run dev`).
|
|
14
|
+
*
|
|
15
|
+
* Run via npx: `npx @oga-mcp/server`
|
|
16
|
+
*
|
|
17
|
+
* Claude Desktop config example:
|
|
18
|
+
* {
|
|
19
|
+
* "mcpServers": {
|
|
20
|
+
* "onegoodarea": {
|
|
21
|
+
* "command": "npx",
|
|
22
|
+
* "args": ["-y", "@oga-mcp/server"],
|
|
23
|
+
* "env": { "OOGA_API_KEY": "oga_..." }
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
*/
|
|
28
|
+
export {};
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OneGoodArea MCP server — entry point.
|
|
4
|
+
*
|
|
5
|
+
* Exposes the OneGoodArea engine as MCP tools so Claude Desktop / Cursor /
|
|
6
|
+
* any MCP-compatible client can score UK areas inline.
|
|
7
|
+
*
|
|
8
|
+
* Auth: reads OOGA_API_KEY from env (the customer's API key from
|
|
9
|
+
* https://www.onegoodarea.com/dashboard). Keys start with `oga_`.
|
|
10
|
+
*
|
|
11
|
+
* Base URL: defaults to https://onegoodarea.onrender.com (the live API).
|
|
12
|
+
* Override via OOGA_API_BASE env (useful for local dev against
|
|
13
|
+
* `cd apps/api && npm run dev`).
|
|
14
|
+
*
|
|
15
|
+
* Run via npx: `npx @oga-mcp/server`
|
|
16
|
+
*
|
|
17
|
+
* Claude Desktop config example:
|
|
18
|
+
* {
|
|
19
|
+
* "mcpServers": {
|
|
20
|
+
* "onegoodarea": {
|
|
21
|
+
* "command": "npx",
|
|
22
|
+
* "args": ["-y", "@oga-mcp/server"],
|
|
23
|
+
* "env": { "OOGA_API_KEY": "oga_..." }
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
*/
|
|
28
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
29
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
30
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
31
|
+
import { OogaApiClient } from "./api-client.js";
|
|
32
|
+
import { scorePostcodeToolDef, scorePostcodeToolName, parseScorePostcodeArgs, executeScorePostcode, } from "./tools/score-postcode.js";
|
|
33
|
+
import { comparePostcodesToolDef, comparePostcodesToolName, parseComparePostcodesArgs, executeComparePostcodes, } from "./tools/compare-postcodes.js";
|
|
34
|
+
import { methodologyForToolDef, methodologyForToolName, parseMethodologyForArgs, executeMethodologyFor, } from "./tools/methodology-for.js";
|
|
35
|
+
import { engineVersionToolDef, engineVersionToolName, executeEngineVersion, } from "./tools/engine-version.js";
|
|
36
|
+
import { getAreaSignalsToolDef, getAreaSignalsToolName, parseGetAreaSignalsArgs, executeGetAreaSignals, } from "./tools/get-area-signals.js";
|
|
37
|
+
import { getSignalsByCategoryToolDef, getSignalsByCategoryToolName, parseGetSignalsByCategoryArgs, executeGetSignalsByCategory, } from "./tools/get-signals-by-category.js";
|
|
38
|
+
import { findAreasToolDef, findAreasToolName, parseFindAreasArgs, executeFindAreas, } from "./tools/find-areas.js";
|
|
39
|
+
import { findPeersToolDef, findPeersToolName, parseFindPeersArgs, executeFindPeers, } from "./tools/find-peers.js";
|
|
40
|
+
import { watchPortfolioToolDef, watchPortfolioToolName, parseWatchPortfolioArgs, executeWatchPortfolio, } from "./tools/watch-portfolio.js";
|
|
41
|
+
import { getPortfolioChangesToolDef, getPortfolioChangesToolName, parseGetPortfolioChangesArgs, executeGetPortfolioChanges, } from "./tools/get-portfolio-changes.js";
|
|
42
|
+
import { areaBriefToolDef, areaBriefToolName, parseAreaBriefArgs, executeAreaBrief, } from "./tools/area-brief.js";
|
|
43
|
+
const SERVER_VERSION = "1.0.0";
|
|
44
|
+
function readApiKey() {
|
|
45
|
+
const key = process.env.OOGA_API_KEY;
|
|
46
|
+
if (!key) {
|
|
47
|
+
process.stderr.write("[oga-mcp] Missing OOGA_API_KEY env var. Get one at https://www.onegoodarea.com/dashboard\n");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
if (!key.startsWith("oga_")) {
|
|
51
|
+
process.stderr.write(`[oga-mcp] OOGA_API_KEY looks malformed (expected to start with 'oga_'). Got prefix: ${key.slice(0, 4)}\n`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
return key;
|
|
55
|
+
}
|
|
56
|
+
async function checkMcpAccess(client) {
|
|
57
|
+
// Skip the entitlement check if explicitly disabled (e.g. for local dev
|
|
58
|
+
// before the /v1/me endpoint is deployed). Production should never
|
|
59
|
+
// set this — it's a developer escape hatch.
|
|
60
|
+
if (process.env.OOGA_SKIP_ENTITLEMENT_CHECK === "1") {
|
|
61
|
+
process.stderr.write("[oga-mcp] OOGA_SKIP_ENTITLEMENT_CHECK=1 — skipping /me check\n");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
let me;
|
|
65
|
+
try {
|
|
66
|
+
me = await client.me();
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
70
|
+
process.stderr.write(`[oga-mcp] Could not verify entitlement at /v1/me: ${msg}\n`);
|
|
71
|
+
process.stderr.write(`[oga-mcp] Check OOGA_API_KEY is valid and OOGA_API_BASE is reachable.\n`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
if (!me.mcp_access) {
|
|
75
|
+
process.stderr.write(`[oga-mcp] Your plan (${me.plan_name}) does not include MCP access.\n` +
|
|
76
|
+
`[oga-mcp] MCP is included free on Growth (£1,499/mo) and Enterprise tiers.\n` +
|
|
77
|
+
`[oga-mcp] On Sandbox / Starter / Build / Scale you can purchase the £29/mo MCP add-on.\n` +
|
|
78
|
+
`[oga-mcp] Upgrade or add MCP at https://www.onegoodarea.com/pricing\n`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
process.stderr.write(`[oga-mcp] Entitlement OK · plan: ${me.plan_name} · engine: ${me.engine_version}\n`);
|
|
82
|
+
}
|
|
83
|
+
async function main() {
|
|
84
|
+
const apiKey = readApiKey();
|
|
85
|
+
const baseUrl = process.env.OOGA_API_BASE;
|
|
86
|
+
const client = new OogaApiClient({ apiKey, baseUrl });
|
|
87
|
+
// Fail fast if customer's plan doesn't include MCP access.
|
|
88
|
+
await checkMcpAccess(client);
|
|
89
|
+
const server = new Server({
|
|
90
|
+
name: "onegoodarea",
|
|
91
|
+
version: SERVER_VERSION,
|
|
92
|
+
}, {
|
|
93
|
+
capabilities: { tools: {} },
|
|
94
|
+
});
|
|
95
|
+
// 11 tools: score_postcode, compare_postcodes, get_area_signals,
|
|
96
|
+
// get_signals_by_category, find_areas, find_peers, watch_portfolio,
|
|
97
|
+
// get_portfolio_changes, area_brief (network); methodology_for,
|
|
98
|
+
// engine_version (static lookup, no network).
|
|
99
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
100
|
+
tools: [
|
|
101
|
+
scorePostcodeToolDef,
|
|
102
|
+
comparePostcodesToolDef,
|
|
103
|
+
getAreaSignalsToolDef,
|
|
104
|
+
getSignalsByCategoryToolDef,
|
|
105
|
+
findAreasToolDef,
|
|
106
|
+
findPeersToolDef,
|
|
107
|
+
watchPortfolioToolDef,
|
|
108
|
+
getPortfolioChangesToolDef,
|
|
109
|
+
areaBriefToolDef,
|
|
110
|
+
methodologyForToolDef,
|
|
111
|
+
engineVersionToolDef,
|
|
112
|
+
],
|
|
113
|
+
}));
|
|
114
|
+
// Dispatch on tool name. Each tool owns its own arg parsing + execution.
|
|
115
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
116
|
+
const { name, arguments: args } = req.params;
|
|
117
|
+
if (name === scorePostcodeToolName) {
|
|
118
|
+
const parsed = parseScorePostcodeArgs(args);
|
|
119
|
+
return executeScorePostcode(client, parsed);
|
|
120
|
+
}
|
|
121
|
+
if (name === comparePostcodesToolName) {
|
|
122
|
+
const parsed = parseComparePostcodesArgs(args);
|
|
123
|
+
return executeComparePostcodes(client, parsed);
|
|
124
|
+
}
|
|
125
|
+
if (name === getAreaSignalsToolName) {
|
|
126
|
+
const parsed = parseGetAreaSignalsArgs(args);
|
|
127
|
+
return executeGetAreaSignals(client, parsed);
|
|
128
|
+
}
|
|
129
|
+
if (name === getSignalsByCategoryToolName) {
|
|
130
|
+
const parsed = parseGetSignalsByCategoryArgs(args);
|
|
131
|
+
return executeGetSignalsByCategory(client, parsed);
|
|
132
|
+
}
|
|
133
|
+
if (name === findAreasToolName) {
|
|
134
|
+
const parsed = parseFindAreasArgs(args);
|
|
135
|
+
return executeFindAreas(client, parsed);
|
|
136
|
+
}
|
|
137
|
+
if (name === findPeersToolName) {
|
|
138
|
+
const parsed = parseFindPeersArgs(args);
|
|
139
|
+
return executeFindPeers(client, parsed);
|
|
140
|
+
}
|
|
141
|
+
if (name === watchPortfolioToolName) {
|
|
142
|
+
const parsed = parseWatchPortfolioArgs(args);
|
|
143
|
+
return executeWatchPortfolio(client, parsed);
|
|
144
|
+
}
|
|
145
|
+
if (name === getPortfolioChangesToolName) {
|
|
146
|
+
const parsed = parseGetPortfolioChangesArgs(args);
|
|
147
|
+
return executeGetPortfolioChanges(client, parsed);
|
|
148
|
+
}
|
|
149
|
+
if (name === areaBriefToolName) {
|
|
150
|
+
const parsed = parseAreaBriefArgs(args);
|
|
151
|
+
return executeAreaBrief(client, parsed);
|
|
152
|
+
}
|
|
153
|
+
if (name === methodologyForToolName) {
|
|
154
|
+
const parsed = parseMethodologyForArgs(args);
|
|
155
|
+
return executeMethodologyFor(parsed);
|
|
156
|
+
}
|
|
157
|
+
if (name === engineVersionToolName) {
|
|
158
|
+
return executeEngineVersion();
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
162
|
+
isError: true,
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
const transport = new StdioServerTransport();
|
|
166
|
+
await server.connect(transport);
|
|
167
|
+
process.stderr.write(`[oga-mcp] v${SERVER_VERSION} listening on stdio (api: ${baseUrl ?? "https://onegoodarea.onrender.com"})\n`);
|
|
168
|
+
}
|
|
169
|
+
main().catch((err) => {
|
|
170
|
+
process.stderr.write(`[oga-mcp] Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
});
|
|
173
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AR-369: AUDIENCES config for `area_brief`. The marquee composite tool
|
|
3
|
+
* shapes its output by audience: lender, insurer, retailer, investor.
|
|
4
|
+
*
|
|
5
|
+
* Each entry says:
|
|
6
|
+
* - which scoring preset to call /v1/score?explain=true with
|
|
7
|
+
* - which dimension labels to emphasise in the brief
|
|
8
|
+
* - which raw signal keys to highlight, grouped by section
|
|
9
|
+
*
|
|
10
|
+
* Server-side narrative policy: nothing here invents prose. Every
|
|
11
|
+
* "highlight" is just selection — which real field from the response
|
|
12
|
+
* renders in which section. The score's `summary`, `recommendations`,
|
|
13
|
+
* per-dimension `reasoning` + `confidence_reason` all come from the
|
|
14
|
+
* engine; the audience config decides *which* of them surface.
|
|
15
|
+
*/
|
|
16
|
+
import type { Preset } from "../api-client.js";
|
|
17
|
+
export type Audience = "lender" | "insurer" | "retailer" | "investor";
|
|
18
|
+
export declare const AUDIENCES: Audience[];
|
|
19
|
+
export interface BriefSection {
|
|
20
|
+
/** Section heading shown in the markdown output. */
|
|
21
|
+
title: string;
|
|
22
|
+
/** Dimension labels (from the score response) to render in this section,
|
|
23
|
+
in order. Labels match what the engine emits in `dimensions[].label`. */
|
|
24
|
+
dimensions: string[];
|
|
25
|
+
/** Raw signal keys (from the /v1/area catalog) to render in this section,
|
|
26
|
+
in order. */
|
|
27
|
+
signals: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface AudienceConfig {
|
|
30
|
+
audience: Audience;
|
|
31
|
+
/** Display label for the brief header. */
|
|
32
|
+
label: string;
|
|
33
|
+
/** One-line description of who this brief is for. */
|
|
34
|
+
framing: string;
|
|
35
|
+
/** Which scoring preset feeds the overall verdict. */
|
|
36
|
+
preset: Preset;
|
|
37
|
+
/** Ordered sections after the verdict block. */
|
|
38
|
+
sections: BriefSection[];
|
|
39
|
+
}
|
|
40
|
+
export declare function getAudienceConfig(audience: Audience): AudienceConfig;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AR-369: AUDIENCES config for `area_brief`. The marquee composite tool
|
|
3
|
+
* shapes its output by audience: lender, insurer, retailer, investor.
|
|
4
|
+
*
|
|
5
|
+
* Each entry says:
|
|
6
|
+
* - which scoring preset to call /v1/score?explain=true with
|
|
7
|
+
* - which dimension labels to emphasise in the brief
|
|
8
|
+
* - which raw signal keys to highlight, grouped by section
|
|
9
|
+
*
|
|
10
|
+
* Server-side narrative policy: nothing here invents prose. Every
|
|
11
|
+
* "highlight" is just selection — which real field from the response
|
|
12
|
+
* renders in which section. The score's `summary`, `recommendations`,
|
|
13
|
+
* per-dimension `reasoning` + `confidence_reason` all come from the
|
|
14
|
+
* engine; the audience config decides *which* of them surface.
|
|
15
|
+
*/
|
|
16
|
+
export const AUDIENCES = ["lender", "insurer", "retailer", "investor"];
|
|
17
|
+
/** Lender (residential mortgage origination): preset=moving. Focused on
|
|
18
|
+
affordability, borrower-side risk, and long-term value retention. */
|
|
19
|
+
const LENDER = {
|
|
20
|
+
audience: "lender",
|
|
21
|
+
label: "Lender brief",
|
|
22
|
+
framing: "Residential mortgage origination — affordability, borrower-side risk, and long-term value retention.",
|
|
23
|
+
preset: "moving",
|
|
24
|
+
sections: [
|
|
25
|
+
{
|
|
26
|
+
title: "Affordability & cost",
|
|
27
|
+
dimensions: ["Cost of Living"],
|
|
28
|
+
signals: ["property.median_price", "property.price_change_pct", "property.transaction_count"],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
title: "Borrower-side risk",
|
|
32
|
+
dimensions: ["Safety", "Safety & Crime"],
|
|
33
|
+
signals: ["crime.total_12m", "crime.monthly_rate", "environment.flood_areas_nearby", "environment.active_flood_warnings"],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
title: "Long-term value drivers",
|
|
37
|
+
dimensions: ["Transport", "Transport & Commute", "Schools", "Schools & Education"],
|
|
38
|
+
signals: ["schools.rated_count", "schools.good_or_outstanding_pct", "transport.stations", "transport.bus_stops"],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
/** Insurer (property risk underwriting): preset=investing (still risk-aware).
|
|
43
|
+
Focused on physical hazards + crime + building stock signals. */
|
|
44
|
+
const INSURER = {
|
|
45
|
+
audience: "insurer",
|
|
46
|
+
label: "Insurer brief",
|
|
47
|
+
framing: "Property risk underwriting — physical hazards, crime profile, and replacement-cost signals.",
|
|
48
|
+
preset: "investing",
|
|
49
|
+
sections: [
|
|
50
|
+
{
|
|
51
|
+
title: "Physical hazard",
|
|
52
|
+
dimensions: ["Risk Factors"],
|
|
53
|
+
signals: ["environment.flood_areas_nearby", "environment.active_flood_warnings"],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
title: "Crime profile",
|
|
57
|
+
dimensions: ["Safety", "Safety & Crime"],
|
|
58
|
+
signals: ["crime.total_12m", "crime.monthly_rate"],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
title: "Building stock & market signals",
|
|
62
|
+
dimensions: ["Price Growth"],
|
|
63
|
+
signals: ["property.median_price", "property.transaction_count", "property.price_change_pct"],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
/** Retailer (commercial site selection): preset=business. Focused on
|
|
68
|
+
catchment demand, competition, access, and commercial costs. */
|
|
69
|
+
const RETAILER = {
|
|
70
|
+
audience: "retailer",
|
|
71
|
+
label: "Retailer brief",
|
|
72
|
+
framing: "Commercial site selection — catchment demand, competition, access, and commercial costs.",
|
|
73
|
+
preset: "business",
|
|
74
|
+
sections: [
|
|
75
|
+
{
|
|
76
|
+
title: "Footfall & spending power",
|
|
77
|
+
dimensions: ["Foot Traffic", "Local Spending Power", "Spending Power"],
|
|
78
|
+
signals: ["amenities.total", "amenities.restaurants_cafes", "amenities.shops"],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
title: "Competition",
|
|
82
|
+
dimensions: ["Competition", "Competition Density"],
|
|
83
|
+
signals: ["amenities.shops", "amenities.pubs_bars", "amenities.restaurants_cafes"],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
title: "Access",
|
|
87
|
+
dimensions: ["Transport", "Transport Access"],
|
|
88
|
+
signals: ["transport.stations", "transport.bus_stops"],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
title: "Commercial costs",
|
|
92
|
+
dimensions: ["Commercial Costs"],
|
|
93
|
+
signals: ["property.median_price"],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
/** Investor (residential investment): preset=investing. Focused on yield,
|
|
98
|
+
growth, demand pressure, and risk discount. */
|
|
99
|
+
const INVESTOR = {
|
|
100
|
+
audience: "investor",
|
|
101
|
+
label: "Investor brief",
|
|
102
|
+
framing: "Residential investment — yield, growth, demand pressure, and risk discount.",
|
|
103
|
+
preset: "investing",
|
|
104
|
+
sections: [
|
|
105
|
+
{
|
|
106
|
+
title: "Yield & growth",
|
|
107
|
+
dimensions: ["Price Growth", "Rental Yield"],
|
|
108
|
+
signals: ["property.median_price", "property.price_change_pct", "property.transaction_count"],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
title: "Demand pressure",
|
|
112
|
+
dimensions: ["Tenant Demand", "Regeneration", "Regeneration & Infrastructure"],
|
|
113
|
+
signals: ["amenities.total", "transport.stations", "transport.bus_stops"],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
title: "Risk discount",
|
|
117
|
+
dimensions: ["Risk Factors"],
|
|
118
|
+
signals: ["crime.total_12m", "environment.flood_areas_nearby", "environment.active_flood_warnings"],
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
const BY_AUDIENCE = {
|
|
123
|
+
lender: LENDER,
|
|
124
|
+
insurer: INSURER,
|
|
125
|
+
retailer: RETAILER,
|
|
126
|
+
investor: INVESTOR,
|
|
127
|
+
};
|
|
128
|
+
export function getAudienceConfig(audience) {
|
|
129
|
+
return BY_AUDIENCE[audience];
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=area-brief-audiences.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"area-brief-audiences.js","sourceRoot":"","sources":["../../src/tools/area-brief-audiences.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AR-369: Render an audience-shaped brief from a real AreaProfile +
|
|
3
|
+
* ScoreResponse (?explain=true). Server-side-narrative policy:
|
|
4
|
+
*
|
|
5
|
+
* - The `summary`, `dimensions[].reasoning`, `dimensions[].confidence_reason`,
|
|
6
|
+
* `recommendations[]`, and `data_sources[]` come straight from
|
|
7
|
+
* /v1/score?explain=true (server-composed in AR-363).
|
|
8
|
+
* - The per-signal `confidence_reason`, `value`, `unit`, `percentile`,
|
|
9
|
+
* `source`, `observed_period` come straight from /v1/area (engine output).
|
|
10
|
+
* - This module SELECTS which of those fields render in which section
|
|
11
|
+
* based on the AudienceConfig. It does NOT invent prose.
|
|
12
|
+
*/
|
|
13
|
+
import type { OogaAreaProfile, OogaScoreResponse } from "../api-client.js";
|
|
14
|
+
import type { AudienceConfig } from "./area-brief-audiences.js";
|
|
15
|
+
export declare function formatAreaBriefAsText(audience: AudienceConfig, profile: OogaAreaProfile, score: OogaScoreResponse): string;
|