From 7afd994548f1d1e5cadd53213d0e67a16071c5cf Mon Sep 17 00:00:00 2001 From: andrewmuratov Date: Wed, 30 Sep 2026 18:18:46 -0400 Subject: [PATCH] fix: sync public campus coverage --- .github/workflows/ci.yml | 10 +++++++++ CAMPUS_DATA_OWNERSHIP.md | 2 +- ECOSYSTEM.md | 4 ++-- README.md | 4 ++-- app/layout.tsx | 6 +++--- contracts/supported-universities.json | 15 +++++++++++++ docs/CURRENT_LIMITATIONS.md | 2 +- docs/TOOL_CONTRACT.md | 2 +- package.json | 3 ++- scripts/check-university-registry.mjs | 29 +++++++++++++++++++++++++ src/domain/public-campus.ts | 31 +++++---------------------- tests/public-campus.test.ts | 4 ++-- 12 files changed, 73 insertions(+), 39 deletions(-) create mode 100644 contracts/supported-universities.json create mode 100644 scripts/check-university-registry.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33ffa54..d39fbbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,16 @@ jobs: run: npm run check:cost-model - name: Install dependencies run: npm ci --ignore-scripts --no-audit --no-fund + - name: Check out the canonical university registry + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: GapwiseHQ/gapwise + ref: main + path: .canonical-gapwise + sparse-checkout: universities.json + persist-credentials: false + - name: Verify supported university parity + run: npm run check:universities - name: Verify generated MCP contract run: npm run contract:check - name: Typecheck diff --git a/CAMPUS_DATA_OWNERSHIP.md b/CAMPUS_DATA_OWNERSHIP.md index c23e605..9df6644 100644 --- a/CAMPUS_DATA_OWNERSHIP.md +++ b/CAMPUS_DATA_OWNERSHIP.md @@ -1,6 +1,6 @@ # Campus data ownership -Canonical public University of Toronto campus facts and geometry live in `GapwiseHQ/data`. UTM's reviewed entrance/routing dataset lives under `data/utm`; UTSG and UTSC identities and geometry live in their own campus directories. +Canonical public multi-university campus facts and geometry live in `GapwiseHQ/data`. University datasets retain source provenance and uncertainty; UTM's specialized reviewed entrance/routing artifacts live under `data/utm`. `gapwise` consumes a validated build-time snapshot of that dataset and remains responsible for deterministic routing, gap planning, public API behavior, and SDK contracts. `ai` should continue exposing those deterministic Gapwise semantics through MCP rather than becoming a second campus-data or routing engine. diff --git a/ECOSYSTEM.md b/ECOSYSTEM.md index 2573940..06b7480 100644 --- a/ECOSYSTEM.md +++ b/ECOSYSTEM.md @@ -2,7 +2,7 @@ `ai` is the OAuth/MCP trust boundary of the seven-repository Gapwise product ecosystem. It exposes stateless public multi-university campus intelligence plus explicitly delegated, minimized student context and bounded actions to compatible AI clients. It does not replace deterministic Gapwise product logic, the public campus API, or the public SDKs. -All seven first-party product repositories are owned by the **Gapwise** GitHub organization (`GapwiseHQ`). Organization-wide GitHub defaults live in the separate `.github` repository. Andrew Muratov remains the creator and primary maintainer. +All first-party product repositories are owned by the **Gapwise** GitHub organization (`GapwiseHQ`). Organization-wide GitHub defaults live in the separate `.github` repository. Andrew Muratov remains the creator and primary maintainer. ## Connected surfaces @@ -23,7 +23,7 @@ All seven first-party product repositories are owned by the **Gapwise** GitHub o ## Product scope -Gapwise delegated timetable context supports all supported institutions and campuses. The stateless public tools expose multi-university discovery, building, and routing queries across all 11 supported universities (13 campus models), with legacy UTM-specific tools providing dedicated UTM place catalog and gap-window planning tools. AI must preserve institution and campus identity and must not extrapolate campus-specific place or routing tools beyond their supported coverage. +Gapwise delegated timetable context supports all supported institutions and campuses. The stateless public tools expose multi-university discovery, building, and routing queries across all 13 supported universities (15 campus models), with legacy UTM-specific tools providing dedicated UTM place catalog and gap-window planning tools. AI must preserve institution and campus identity and must not extrapolate campus-specific place or routing tools beyond their supported coverage. ## Public SDK state diff --git a/README.md b/README.md index d937a54..da91fc2 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ ## What Gapwise AI is -Gapwise AI is the provider-neutral AI integration layer of **Gapwise**, a free and open-source timetable and campus-intelligence platform for university students across 11 Canadian universities (University of Toronto, Carleton University, Toronto Metropolitan University, Queen's University, Wilfrid Laurier University, York University, McMaster University, Western University, University of Guelph, University of Ottawa, and Brock University). +Gapwise AI is the provider-neutral AI integration layer of **Gapwise**, a free and open-source timetable and campus-intelligence platform for university students across 13 Canadian universities, including the University of British Columbia and University of Waterloo editions. The main [`gapwise`](https://github.com/GapwiseHQ/gapwise) platform owns canonical student state and deterministic timetable/campus calculations. [`android`](https://github.com/GapwiseHQ/android) and [`ios`](https://github.com/GapwiseHQ/ios) provide native clients. Gapwise AI exposes a narrow remote MCP interface to bounded context rather than becoming a second timetable, routing, or planning engine. @@ -187,7 +187,7 @@ The JavaScript/TypeScript SDK is published on npm and JSR; the Python SDK is pub | **[`docs`](https://github.com/GapwiseHQ/docs)** | Canonical public developer documentation | [docs.gapwise.ca](https://docs.gapwise.ca) | | **[`status`](https://github.com/GapwiseHQ/status)** | Independent service-health monitoring and incident communication | [status.gapwise.ca](https://status.gapwise.ca) | -All seven first-party product repositories are owned by the **Gapwise** GitHub organization (`GapwiseHQ`). Organization-wide GitHub defaults live in [`.github`](https://github.com/GapwiseHQ/.github). Andrew Muratov remains the creator and primary maintainer. +All first-party product repositories are owned by the **Gapwise** GitHub organization (`GapwiseHQ`). Organization-wide GitHub defaults live in [`.github`](https://github.com/GapwiseHQ/.github). Andrew Muratov remains the creator and primary maintainer. --- diff --git a/app/layout.tsx b/app/layout.tsx index 3b58d31..77718d1 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,12 +4,12 @@ import "./globals.css"; export const metadata: Metadata = { title: "Gapwise AI", - description: "Permissioned, provider-neutral MCP access to Gapwise campus intelligence and explicitly delegated student context across 11 Canadian universities.", + description: "Permissioned, provider-neutral MCP access to Gapwise campus intelligence and explicitly delegated student context across 13 Canadian universities.", icons: { icon: "/favicon.svg" }, robots: { index: false, follow: false }, openGraph: { title: "Gapwise AI", - description: "Permissioned, provider-neutral MCP access to Gapwise campus intelligence and explicitly delegated student context across 11 Canadian universities.", + description: "Permissioned, provider-neutral MCP access to Gapwise campus intelligence and explicitly delegated student context across 13 Canadian universities.", url: "https://ai.gapwise.ca", siteName: "Gapwise AI", type: "website", @@ -25,7 +25,7 @@ export const metadata: Metadata = { twitter: { card: "summary_large_image", title: "Gapwise AI", - description: "Permissioned, provider-neutral MCP access to Gapwise campus intelligence and explicitly delegated student context across 11 Canadian universities.", + description: "Permissioned, provider-neutral MCP access to Gapwise campus intelligence and explicitly delegated student context across 13 Canadian universities.", images: ["https://ai.gapwise.ca/og-card.png"], }, }; diff --git a/contracts/supported-universities.json b/contracts/supported-universities.json new file mode 100644 index 0000000..753aa7c --- /dev/null +++ b/contracts/supported-universities.json @@ -0,0 +1,15 @@ +{ + "uoft": { "defaultCampus": "utm", "campuses": ["utm", "utsg", "utsc"] }, + "carleton": { "defaultCampus": "carleton", "campuses": ["carleton"] }, + "tmu": { "defaultCampus": "tmu", "campuses": ["tmu"] }, + "queens": { "defaultCampus": "queens", "campuses": ["queens"] }, + "laurier": { "defaultCampus": "waterloo", "campuses": ["waterloo"] }, + "york": { "defaultCampus": "keele", "campuses": ["keele"] }, + "mcmaster": { "defaultCampus": "mcmaster", "campuses": ["mcmaster"] }, + "western": { "defaultCampus": "western", "campuses": ["western"] }, + "guelph": { "defaultCampus": "guelph", "campuses": ["guelph"] }, + "uottawa": { "defaultCampus": "uottawa", "campuses": ["uottawa"] }, + "brock": { "defaultCampus": "brock", "campuses": ["brock"] }, + "ubc": { "defaultCampus": "ubc-vancouver", "campuses": ["ubc-vancouver"] }, + "waterloo": { "defaultCampus": "waterloo-main", "campuses": ["waterloo-main"] } +} diff --git a/docs/CURRENT_LIMITATIONS.md b/docs/CURRENT_LIMITATIONS.md index 34cc063..615b68f 100644 --- a/docs/CURRENT_LIMITATIONS.md +++ b/docs/CURRENT_LIMITATIONS.md @@ -1,5 +1,5 @@ # Current integration limitations -- The MCP surface now includes 12 stateless public campus-intelligence tools (supporting multi-university university/campus discovery, building search, and routing across 11 universities, plus dedicated UTM place and gap-planning tools) alongside the permissioned private student-context tools. Public tools do not authenticate a Gapwise account and must not be treated as access to a student's private timetable, friends, location, or sync state. +- The MCP surface includes 17 stateless public campus-intelligence tools (10 canonical multi-university tools supporting discovery, building search, places, routing, and gap planning across 13 universities, plus 7 deprecated UTM compatibility aliases) alongside the permissioned private student-context tools. Public tools do not authenticate a Gapwise account and must not be treated as access to a student's private timetable, friends, location, or sync state. - MCP write actions are applied by Gapwise through the queued-action channel; they do not remotely rewrite the primary encrypted private payload. - Broad ChatGPT/Claude support is still a release-validation target rather than a universal support claim. Real OAuth/read/write/revoke behavior depends on the current client/product capabilities and must pass the repository's compatibility matrix before a client is represented as supported. diff --git a/docs/TOOL_CONTRACT.md b/docs/TOOL_CONTRACT.md index 7f6174c..e568d94 100644 --- a/docs/TOOL_CONTRACT.md +++ b/docs/TOOL_CONTRACT.md @@ -17,7 +17,7 @@ MCP text `content` is intentionally compact. Exact recurrence, exclusions, rich ## Public campus tools -These tools use public deterministic Gapwise campus data across all 11 supported universities. They do not authenticate a Gapwise account and do not read a student's private timetable, friends, precise location, or private sync state. +These tools use public deterministic Gapwise campus data across all 13 supported universities and 15 campus models. They do not authenticate a Gapwise account and do not read a student's private timetable, friends, precise location, or private sync state. ### `list_supported_universities` Lists all supported universities and campus editions across the Gapwise platform with capabilities and status. diff --git a/package.json b/package.json index 0190d88..5f3fc74 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,9 @@ "test": "vitest run", "scan:secrets": "node scripts/scan-secrets.mjs", "check:cost-model": "node scripts/check-cost-model.mjs", + "check:universities": "node scripts/check-university-registry.mjs", "audit:production": "node scripts/audit-production-dependencies.mjs", - "check": "npm run scan:secrets && npm run check:cost-model && npm run contract:check && npm run typecheck && npm test && npm run build", + "check": "npm run scan:secrets && npm run check:cost-model && npm run check:universities && npm run contract:check && npm run typecheck && npm test && npm run build", "contract:generate": "node scripts/mcp-surface-contract.mjs --write", "contract:check": "node scripts/mcp-surface-contract.mjs --check" }, diff --git a/scripts/check-university-registry.mjs b/scripts/check-university-registry.mjs new file mode 100644 index 0000000..3f87fc8 --- /dev/null +++ b/scripts/check-university-registry.mjs @@ -0,0 +1,29 @@ +import { readFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; + +const sourceArg = process.argv.find((argument) => argument.startsWith("--source=")); +const source = + sourceArg?.slice("--source=".length) ?? + (existsSync(".canonical-gapwise/universities.json") + ? ".canonical-gapwise/universities.json" + : "../gapwise/universities.json"); +const canonical = JSON.parse(await readFile(source, "utf8")); +const local = JSON.parse(await readFile("contracts/supported-universities.json", "utf8")); +const expected = Object.fromEntries( + canonical.universities + .filter((university) => university.status === "supported") + .map((university) => [ + university.id, + { defaultCampus: university.defaultCampus, campuses: university.campuses }, + ]), +); + +if (JSON.stringify(local) !== JSON.stringify(expected)) { + throw new Error( + "contracts/supported-universities.json is out of sync with GapwiseHQ/gapwise universities.json", + ); +} + +console.log( + `Verified AI university registry for ${Object.keys(local).length} supported universities.`, +); diff --git a/src/domain/public-campus.ts b/src/domain/public-campus.ts index fd82aaa..4c58715 100644 --- a/src/domain/public-campus.ts +++ b/src/domain/public-campus.ts @@ -1,39 +1,18 @@ import { z } from "zod"; import { getRuntimeConfig } from "@/src/config"; import { GapPreferencesSchema, TermSchema, WeekdaySchema } from "@/src/domain/schemas"; +import universityRegistry from "@/contracts/supported-universities.json" with { type: "json" }; -export const CANONICAL_UNIVERSITIES = [ - "uoft", - "carleton", - "tmu", - "queens", - "laurier", - "york", - "mcmaster", - "western", - "guelph", - "uottawa", - "brock", -] as const; +export const CANONICAL_UNIVERSITIES = Object.keys(universityRegistry) as Array< + keyof typeof universityRegistry +>; export type CanonicalUniversity = (typeof CANONICAL_UNIVERSITIES)[number]; export const CANONICAL_UNIVERSITY_CAMPUSES: Record< string, { defaultCampus: string; campuses: readonly string[] } -> = { - uoft: { defaultCampus: "utm", campuses: ["utm", "utsg", "utsc"] }, - carleton: { defaultCampus: "carleton", campuses: ["carleton"] }, - tmu: { defaultCampus: "tmu", campuses: ["tmu"] }, - queens: { defaultCampus: "queens", campuses: ["queens"] }, - laurier: { defaultCampus: "waterloo", campuses: ["waterloo"] }, - york: { defaultCampus: "keele", campuses: ["keele"] }, - mcmaster: { defaultCampus: "mcmaster", campuses: ["mcmaster"] }, - western: { defaultCampus: "western", campuses: ["western"] }, - guelph: { defaultCampus: "guelph", campuses: ["guelph"] }, - uottawa: { defaultCampus: "uottawa", campuses: ["uottawa"] }, - brock: { defaultCampus: "brock", campuses: ["brock"] }, -}; +> = universityRegistry; const VerificationStatusSchema = z.enum(["verified", "inferred", "unknown"]); const AccessibilitySchema = z.enum(["accessible", "not_accessible", "unknown"]); diff --git a/tests/public-campus.test.ts b/tests/public-campus.test.ts index 303cd78..31b0a57 100644 --- a/tests/public-campus.test.ts +++ b/tests/public-campus.test.ts @@ -210,8 +210,8 @@ describe("public campus intelligence adapter", () => { }); describe("canonical multi-university programmatic matrix", () => { - it("covers all 11 supported universities in the canonical registry", () => { - expect(CANONICAL_UNIVERSITIES).toHaveLength(11); + it("covers all 13 supported universities in the canonical registry", () => { + expect(CANONICAL_UNIVERSITIES).toHaveLength(13); for (const u of CANONICAL_UNIVERSITIES) { const config = CANONICAL_UNIVERSITY_CAMPUSES[u]; expect(config).toBeDefined();