SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
6 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
21.3 KB · 342 lines tsx
Raw Blame History
1import type { AuthorityField, ClaimScope, SourceKind } from "@dci/core";2import { AI_EVIDENCE_LEVELS, AUTHORITY_FIELDS, CAPACITY_PREDICATE_LABEL, CLAIM_SCOPES, FIELD_AUTHORITY, INVESTMENT_PREDICATE_LABEL, PROJECT_CLASSES, SOURCE_KINDS, isSiteScope } from "@dci/core";3import Link from "next/link";4import type { ReactNode } from "react";5import { AiMark, ScopeBadge, StageBadge, TierMark } from "@/components/ui/badge";6import { Section } from "@/components/ui/card";7import { TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";8import { AI_EVIDENCE_LABEL, AUTHORITY_TIER_LABEL, PROJECT_STAGE_ORDER, SOURCE_KIND_LABEL } from "@/lib/labels";9import { routes } from "@/lib/routes";1011/* ------------------------------------------------------------------------------------------12   Methodology §9–17: the claim-first architecture (docs/CLAIMS.md), rendered from the same code13   tables the pipeline uses (@dci/core claims.ts) so the page can never drift from the rules.14   ------------------------------------------------------------------------------------------ */1516const linkCls = "text-ink underline decoration-hair-2 hover:text-accent";17function P({ children }: { children: ReactNode }) {18  return <p className="mt-3 first:mt-0">{children}</p>;19}20function Code({ children }: { children: ReactNode }) {21  return <code className="figure text-[12.5px]">{children}</code>;22}2324const CLAIM_COLUMNS: Array<[string, string]> = [25  ["subject_type, subject_id", "facility / campus / project / operator / market / country the figure is about"],26  ["predicate", "what is asserted — it_capacity_mw, critical_power_mw, utility_capacity_mw, grid_connection_mw, current_power_mw, planned_power_mw, ultimate_campus_mw, phase_mw, project_investment_usd, campus_investment_usd, company_investment_usd, country_program_usd, multi_year_capex_usd, deal_value_usd, status…"],27  ["value, value_text, unit", "the figure (MW, USD) or text"],28  ["scope, scope_reason", "building · facility · campus · metro · country · portfolio · company · unknown, and the rule that decided"],29  ["evidence_text, evidence_start, evidence_end", "the supporting sentence and its offsets in the parsed text — no sentence, no numerical claim (structured spec tables record the extraction method instead)"],30  ["source_id, url, document_id, published_at, retrieved_at", "where and when"],31  ["authority_tier", "A–E for this field (see field-level authority)"],32  ["parser_name, parser_version, run_id", "who extracted it — enables reprocessing and rollback"],33  ["status", "current · superseded (same source, same page, new value) · rejected (review / rollback) · review (critical sanity flag) · unscoped (describes a portfolio, a company, a country, a metro, or its scope is unknown)"],34];3536const SCOPE_MEANING: Record<ClaimScope, string> = {37  building: "One hall or building on a site — may populate a building record; never stands for its campus.",38  facility: "A data center as one record — populates the facility's own columns.",39  campus: "The whole campus / park — populates the campus row; never written to a building.",40  metro: "A market statistic (“the Northern Virginia market adds 1 GW”) — evidence only.",41  country: "A national programme or country total — evidence only.",42  portfolio: "A company's footprint or pipeline (“across its portfolio”) — evidence only.",43  company: "Capex, guidance, multi-year plans, market-research figures — evidence only.",44  unknown: "The sentence gives no scope signal and the record has none — stored, never assigned.",45};4647const BLOCKING: Array<[string, string]> = [48  ["scope_*", "figure describes a portfolio, company, country, metro or unknown scope — kept as a claim, not assigned"],49  ["scope_campus_on_building", "campus-level figure on a building record"],50  ["mw_market_statistic", "≥ 20 GW — an industry / market statistic, not a site"],51  ["mw_invalid", "zero, negative or non-numeric MW"],52  ["inv_deal_value_usd · inv_multi_year_capex_usd · inv_company_investment_usd · inv_country_program_usd", "deal value, capex, company or country programme filed as a project investment"],53  ["inv_gt_500b", "more than $500 B on one record"],54];55const NON_BLOCKING: Array<[string, string, string]> = [56  ["mw_single_site_gt_1000", "critical", "single facility above 1 000 MW without a campus designation"],57  ["mw_building_gt_500", "warn", "one building above 500 MW"],58  ["mw_change_5x", "warn", "figure changed more than 5× against the previous value"],59  ["mw_money_collision", "critical", "a money figure with the same number sits in the same sentence — unit to verify"],60  ["inv_single_site_gt_50b", "critical", "more than $50 B on one site"],61  ["mw_semantics_default", "info", "the sentence does not say what kind of MW this is; semantics defaulted from the record status"],62  ["mw_utility_not_it", "info", "utility / grid figure — supply side, never IT load"],63];6465const CLASS_GROUPS: Array<{ title: string; note: string; classes: string[] }> = [66  { title: "Physical — may create or modify a project", note: "Creation additionally needs the evidence threshold: an explicit name or operator, a location, and a development verb. Company domiciles (“Denver-based”, “headquartered in”) are stripped before locating the project.", classes: ["NEW_BUILD", "EXPANSION", "CONSTRUCTION_START", "PERMIT", "LAND_ACQUISITION", "GRID_CONNECTION"] },67  { title: "Associated — attach an event to an identifiable project, never create one", note: "Power agreements, financing and partnerships are corporate context; on operator pages they are listed apart from physical projects.", classes: ["POWER_AGREEMENT", "FINANCING", "ACQUISITION", "PARTNERSHIP", "CUSTOMER_AGREEMENT"] },68  { title: "Never a project", note: "Executive appointments, sustainability reports, product news and general company news are recorded as events at most.", classes: PROJECT_CLASSES.filter((c) => !["NEW_BUILD", "EXPANSION", "CONSTRUCTION_START", "PERMIT", "LAND_ACQUISITION", "GRID_CONNECTION", "POWER_AGREEMENT", "FINANCING", "ACQUISITION", "PARTNERSHIP", "CUSTOMER_AGREEMENT"].includes(c)) },69];7071export const CLAIMS_NAV = [72  { id: "claims", label: "Claims" },73  { id: "scopes", label: "Scopes" },74  { id: "semantics", label: "Capacity semantics" },75  { id: "authority", label: "Authority" },76  { id: "quality", label: "Sanity & quality" },77  { id: "classes", label: "Announcement classes" },78  { id: "lifecycle", label: "Lifecycle" },79  { id: "ai-evidence", label: "AI evidence" },80  { id: "coverage-ranking", label: "Coverage-aware ranking" },81];8283export function ClaimsSections({ from }: { from: number }) {84  const n = (i: number) => `${from + i} · `;85  return (86    <>87      <Section id="claims" className="scroll-mt-28" title={`${n(0)}Claims — the claim-first architecture`}>88        <P>89          Since 2026-09-12 every numerical figure the index extracts is stored as a <strong className="text-ink">claim</strong> before it can become a displayed value. A claim is one assertion by one document about one subject, with its scope, its semantics, the sentence that supports it, the parser that produced it and the field-level authority of its source. Reconciliation then decides which claim (if any) populates a column. <strong className="text-ink">Nothing is deleted when sources disagree</strong> — every claim stays visible in the evidence drawer.90        </P>91        <div className="mt-4">92          <Table dense>93            <THead>94              <tr>95                <Th>Column</Th>96                <Th>Meaning</Th>97              </tr>98            </THead>99            <TBody>100              {CLAIM_COLUMNS.map(([c, m]) => (101                <Tr key={c}>102                  <Td primary className="align-top">103                    <Code>{c}</Code>104                  </Td>105                  <Td label="Meaning" className="text-[12.5px] text-ink-2">106                    {m}107                  </Td>108                </Tr>109              ))}110            </TBody>111          </Table>112        </div>113        <P>114          The winner behind each displayed value is marked (<Code>is_winner</Code>); <Code>run_id</Code> on claims, provenance rows, events and document versions ties every change to a connector run, which can be rolled back as a whole. Daily snapshots keep global totals, per-operator and per-country totals, rankings and stage counts for “as of” views and automated regression checks (facilities −5 %, known MW +20 %, one operator +10 GW in a day, one connector &gt; 500 records a day, &gt; 200 location changes a day).115        </P>116      </Section>117118      <Section id="scopes" className="scroll-mt-28" title={`${n(1)}Scopes`}>119        <P>120          Only <strong className="text-ink">site scopes</strong> (building, facility, campus) may populate a facility&rsquo;s or project&rsquo;s capacity and investment columns. Portfolio, company, country, metro and unknown scopes are stored and shown as evidence, never summed. Scope is classified deterministically from the sentence (<Code>classifyScope</Code>): company → portfolio → country → metro → campus → building → facility → the record&rsquo;s own scope when the sentence has no signal (structured parsers) → unknown. A campus figure is never written to a building record; a building&rsquo;s figure never stands for its campus.121        </P>122        <div className="mt-4">123          <Table dense>124            <THead>125              <tr>126                <Th>Scope</Th>127                <Th className="hidden sm:table-cell">Populates a site&rsquo;s figures?</Th>128                <Th>Meaning</Th>129              </tr>130            </THead>131            <TBody>132              {CLAIM_SCOPES.map((s) => (133                <Tr key={s}>134                  <Td primary>135                    <ScopeBadge scope={s} short={false} size="sm" />136                  </Td>137                  <Td label="Populates" className="hidden text-[12.5px] sm:table-cell">138                    {isSiteScope(s) ? <span className="text-ok">yes — site scope</span> : <span className="text-ink-3">no — evidence only</span>}139                  </Td>140                  <Td label="Meaning" className="text-[12.5px] text-ink-2">141                    {SCOPE_MEANING[s]}142                    <span className="sm:hidden"> {isSiteScope(s) ? "(site scope)" : "(evidence only)"}</span>143                  </Td>144                </Tr>145              ))}146            </TBody>147          </Table>148        </div>149      </Section>150151      <Section id="semantics" className="scroll-mt-28" title={`${n(2)}Capacity semantics`}>152        <P>153          IT load, critical power, utility capacity, grid connection, current power, planned capacity, ultimate build-out and phase capacity are <strong className="text-ink">different predicates and different columns</strong>. The displayed figure carries <Code>capacityScope</Code> and <Code>capacitySemantics</Code> so a page can say what “300 MW” means. Utility and grid figures are supply-side — never IT load and never comparable with IT capacity. When a sentence gives no cue the predicate defaults from the field / record status and the claim is flagged <Code>mw_semantics_default</Code>.154        </P>155        <div className="mt-4 grid gap-4 md:grid-cols-2">156          <Table dense>157            <THead>158              <tr>159                <Th>Capacity predicate</Th>160                <Th>Label</Th>161              </tr>162            </THead>163            <TBody>164              {Object.entries(CAPACITY_PREDICATE_LABEL).map(([k, v]) => (165                <Tr key={k}>166                  <Td primary>167                    <Code>{k}</Code>168                  </Td>169                  <Td label="Label" className="text-[12.5px] text-ink-2">170                    {v}171                  </Td>172                </Tr>173              ))}174            </TBody>175          </Table>176          <Table dense>177            <THead>178              <tr>179                <Th>Investment predicate</Th>180                <Th>Label</Th>181              </tr>182            </THead>183            <TBody>184              {Object.entries(INVESTMENT_PREDICATE_LABEL).map(([k, v]) => (185                <Tr key={k}>186                  <Td primary>187                    <Code>{k}</Code>188                  </Td>189                  <Td label="Label" className="text-[12.5px] text-ink-2">190                    {v}191                  </Td>192                </Tr>193              ))}194            </TBody>195          </Table>196        </div>197        <P>Only project and campus investment may stand for a site; deal values, capex, company investment and country programmes are stored as evidence.</P>198      </Section>199200      <Section id="authority" className="scroll-mt-28" title={`${n(3)}Field-level authority`}>201        <P>202          One universal source rank is wrong: OpenStreetMap is excellent for geometry and useless for capacity; PeeringDB is the reference for interconnection; planning filings beat press releases for approved capacity; utility filings beat everything for grid requirements. <Code>FIELD_AUTHORITY</Code> therefore gives a tier per field. Estimates and LLM-only extractions drop one tier; a human review raises one. The table below is rendered from the code the pipeline runs.203        </P>204        <div className="mt-4 -mx-3 overflow-x-auto px-3 sm:mx-0 sm:px-0">205          <table className="dci-table dense min-w-[720px] text-[12px]">206            <thead className="text-left">207              <tr>208                <Th>Field</Th>209                {SOURCE_KINDS.map((k) => (210                  <Th key={k} className="text-center">211                    {SOURCE_KIND_LABEL[k as SourceKind]}212                  </Th>213                ))}214              </tr>215            </thead>216            <TBody>217              {AUTHORITY_FIELDS.map((f) => (218                <tr key={f} className="align-middle">219                  <td className="px-2 py-1.5 pl-0 font-medium capitalize text-ink">{f as AuthorityField}</td>220                  {SOURCE_KINDS.map((k) => (221                    <td key={k} className="px-2 py-1.5 text-center">222                      <TierMark tier={FIELD_AUTHORITY[f][k as SourceKind]} />223                    </td>224                  ))}225                </tr>226              ))}227            </TBody>228          </table>229        </div>230        <ul className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-[11.5px] text-ink-3">231          {(Object.keys(AUTHORITY_TIER_LABEL) as Array<keyof typeof AUTHORITY_TIER_LABEL>).map((t) => (232            <li key={t} className="inline-flex items-center gap-1.5">233              <TierMark tier={t} /> {AUTHORITY_TIER_LABEL[t].replace(/^Tier [A-E] — /, "")}234            </li>235          ))}236        </ul>237      </Section>238239      <Section id="quality" className="scroll-mt-28" title={`${n(4)}Sanity engines & quality flags`}>240        <P>241          <Code>capacitySanity</Code> and <Code>investmentSanity</Code> run on every claim. <strong className="text-ink">Blocking</strong> flags keep the figure out of the columns (it stays a claim). <strong className="text-ink">Non-blocking</strong> critical flags assign the figure but open a quality flag prioritised by impact; the figure stays visible and marked until a reviewer resolves or dismisses the flag. Every entity page lists its open flags in the data-quality block.242        </P>243        <div className="mt-4 grid gap-4 md:grid-cols-2">244          <div>245            <h3 className="text-[13.5px] font-semibold text-ink">Blocking</h3>246            <ul className="mt-1.5 divide-y divide-hair text-[12.5px]">247              {BLOCKING.map(([c, m]) => (248                <li key={c} className="py-1.5">249                  <Code>{c}</Code>250                  <div className="text-ink-2">{m}</div>251                </li>252              ))}253            </ul>254          </div>255          <div>256            <h3 className="text-[13.5px] font-semibold text-ink">Non-blocking (flagged)</h3>257            <ul className="mt-1.5 divide-y divide-hair text-[12.5px]">258              {NON_BLOCKING.map(([c, sev, m]) => (259                <li key={c} className="py-1.5">260                  <Code>{c}</Code> <span className={sev === "critical" ? "text-danger" : sev === "warn" ? "text-warn" : "text-ink-3"}>· {sev}</span>261                  <div className="text-ink-2">{m}</div>262                </li>263              ))}264            </ul>265          </div>266        </div>267        <P>Review priority weighs the figure&rsquo;s magnitude, how many pages display it and the number of sources that disagree; the admin console works the queue from the top.</P>268      </Section>269270      <Section id="classes" className="scroll-mt-28" title={`${n(5)}Announcement classes (projects)`}>271        <P>272          Before a news article can create a project, <Code>classifyProjectEvent</Code> labels it. Only physical classes may create or modify a project; associated classes attach an event to an identifiable project; the rest never become projects. This is what keeps executive appointments, market-research articles and capex guidance out of the pipeline.273        </P>274        <div className="mt-4 flex flex-col gap-4">275          {CLASS_GROUPS.map((g) => (276            <div key={g.title}>277              <h3 className="text-[13.5px] font-semibold text-ink">{g.title}</h3>278              <div className="mt-1.5 flex flex-wrap gap-1.5">279                {g.classes.map((c) => (280                  <span key={c} className="figure inline-flex h-[22px] items-center rounded-xs border border-hair-2 px-2 text-[11px] text-ink-2">281                    {c}282                  </span>283                ))}284              </div>285              <p className="mt-1.5 text-[12.5px] text-ink-3">{g.note}</p>286            </div>287          ))}288        </div>289      </Section>290291      <Section id="lifecycle" className="scroll-mt-28" title={`${n(6)}Lifecycle state machine`}>292        <P>293          <Code>projectTransition(from, to)</Code> accepts forward moves along the funnel; <em>delayed</em> and <em>cancelled</em> are side branches; leaving <em>delayed</em> resumes where the project was. A backward move needs a source that outranks the stored one for the status field, otherwise it is flagged <Code>status_backward</Code>. Stage dates (<Code>permit_filed_on</Code>, <Code>approved_on</Code>, <Code>construction_started_on</Code>, <Code>opened_on</Code>) are set from the announcement that moved the stage — earliest evidence wins.294        </P>295        <ol className="mt-4 flex flex-wrap items-center gap-1.5" aria-label="Lifecycle stages in order">296          {PROJECT_STAGE_ORDER.filter((s) => s !== "delayed" && s !== "cancelled").map((s, i, arr) => (297            <li key={s} className="inline-flex items-center gap-1.5">298              <StageBadge stage={s} size="sm" />299              {i < arr.length - 1 && <span aria-hidden className="text-ink-4">→</span>}300            </li>301          ))}302        </ol>303        <div className="mt-2 flex flex-wrap items-center gap-1.5 text-[12.5px] text-ink-3">304          side branches: <StageBadge stage="delayed" size="sm" /> <StageBadge stage="cancelled" size="sm" />305        </div>306      </Section>307308      <Section id="ai-evidence" className="scroll-mt-28" title={`${n(7)}AI evidence levels`}>309        <P>A single keyword never confirms an AI site. Each facility and project carries an evidence level; AI counts and MW on the site cover confirmed + likely only, and figures are published, site-scoped values (containment-aware), never extrapolated.</P>310        <ul className="mt-3 divide-y divide-hair text-[12.5px]">311          {AI_EVIDENCE_LEVELS.map((lvl) => (312            <li key={lvl} className="flex flex-wrap items-start gap-x-3 gap-y-1 py-2">313              <span className="w-[150px] shrink-0">314                <AiMark evidence={lvl} size="sm" />315              </span>316              <span className="min-w-0 flex-1 text-ink-2">317                <strong className="text-ink">{AI_EVIDENCE_LABEL[lvl]}</strong>318                {lvl === "confirmed" && " — a source explicitly describes the site as AI / HPC / GPU / accelerated-computing infrastructure."}319                {lvl === "likely" && " — AI-ready, high-density, liquid- or direct-to-chip cooling or GPU signals in the source."}320                {lvl === "associated" && " — only an AI tenant, customer or company mention; reported separately, not counted as AI infrastructure."}321                {lvl === "unknown" && " — no signal in any source."}322              </span>323            </li>324          ))}325        </ul>326        <P>327          See the <Link href={routes.ai()} className={linkCls}>AI infrastructure index</Link>.328        </P>329      </Section>330331      <Section id="coverage-ranking" className="scroll-mt-28" title={`${n(8)}Coverage-aware ranking`}>332        <P>333          Every aggregate uses the containment-aware facility view: a campus whose buildings publish figures contributes no MW itself; a building without a figure under a campus with one is “covered”; a campus with buildings is not counted as an extra facility. Each MW aggregate exposes its <strong className="text-ink">coverage</strong> (facilities with any figure ÷ facilities) and rankings gate on it (≥ 5 facilities, ≥ 35 %). Market concentration (HHI, top-3 / top-5 shares) is computed on facility counts and, separately, on known MW with its coverage — the MW view is labelled partial when coverage is low. Momentum is shown as separate counters (projects announced, MW entering construction, new entrants, openings, cloud additions, grid events), never collapsed into a score.334        </P>335        <P>336          Browse the <Link href={routes.rankings()} className={linkCls}>rankings</Link> and the <Link href={routes.coverage()} className={linkCls}>coverage report</Link>.337        </P>338      </Section>339    </>340  );341}342