Canonical vocabulary for runtime modules. Prefer these names over file names.
Registered on ServiceContainer. Hosts bootstrap them once; runtime code reads the container (or engine.storage / ctx.service(...)), not ad-hoc env fallbacks.
| Port | Role |
|---|---|
IHttpService |
Network + optional session |
IStorageService |
Timetables, preferences, wallpaper, plugin KV |
IVaultService |
Encrypted secret store (native hosts: Keychain / Keystore). Not a general KV. |
IRuntimeService |
Platform id + SHA-256 (sha256 only; timers/UTF-8 helpers removed Round 6) |
IAnalyticsService |
Optional product analytics (registered via ChronosEnv.analytics → container; screens may still call $lib/client/analytics during migration) |
IHostNavigation |
Optional host routes (openCourseEditor); plugins use ctx.tryService(IHostNavigation) — never hardcode host paths |
ChronosEnv is only a host bootstrap adapter (web + native). After construction, registerEnvProviders copies ports into the container. All hosts must pass env at construction (no container-only facade).
Core owns the shapes. Web Dexie / Share codecs are strict Zod adapters (schemaVersion 1).
- Timetable: courses,
academicConfig(includingperiodTimes),viewPrefs, optionalimportMetadata, optionalcustomMetadata.academicConfig.holidayCalendaris plugin-managed (tool-calendar-holidayssyncs public holidays); core only renders holidays already on the timetable (buildHolidayLookup, grid column headers, muted courses). - ImportMetadata:
{ source: string; campusId?: string }. Campus period tables live incustomMetadata['source-cqut'], not onimportMetadata. - Weekend columns: initial
showSaturday/showSundayderive from course occupancy via corederiveWeekendViewPrefs— import-constructing plugins must use it; users override afterwards in details editing. - UserPreferences tokens: theme
light|dark|auto; palettevibrant|wallpaper; layoutfixed|compact; cornersrounded|sharp|pill;visualThemeId; optionallocale(zh-cn|en). Active icon theme is derived, never stored: engine resolves it from the active theme'srecommendedIconTheme(fallbackhost-default) — see ADR 0026.
One lookup module (packages/core/src/engine/period-clock.ts), two fallbacks:
'none'— EngineupdateTime/currentPeriodIndex(period only while in progress).'upcomingOrLast'— grid highlight (host/plugin screens derive fromclockNow).
Scheduler (single): ChronosEngine owns the only createDayClock instance (midnight + period-boundary timers with reschedule/dispose). time:tick emits { currentWeek, currentPeriod, now, todayIso }; ReactiveChronosController mirrors clockNow / clockTodayIso. Host timetable screen and tool-today must not instantiate their own clocks.
Also exports period parsing helpers and delay utilities. ISO local weekday (dayOfWeekFromIso, 1 = Monday … 7 = Sunday) lives in packages/core/src/engine/date.ts.
CQUT campus tables (花溪 1 节 08:20, 两江下午 14:20, 10 节) live only in @chronos/plugin-source-cqut.
Single event + hook runtime on ChronosEngine.events (emit / on, serial guards, waterfall).
Removed: EventBus, DataPipeline, engine.pipeline aliases, the plugin-facing ctx.registerWaterfallHook / ctx.registerSerialHook registration face, and the never-emitted import:before/after / export:before/after events plus ExportTransformHook types. Do not reintroduce them.
FROZEN BASELINE: engine-internal serial/waterfall machinery (and the guard/waterfall wrappers inside every engine action) has zero hook registrants. Like hosts/native-protocol.ts: no new public API; if no real consumer appears within two release cycles, remove the machinery and the action wrappers wholesale.
IStorageService.queryCourses (cross-timetable course lookup) is a reserved capability: implemented by Dexie, threaded through env/engine facades. First production consumer: official plugin tool-today (queryTodayCourses). Kept deliberately (Round 4 decision); do not extend query parameters without revisiting that decision.
Import UI executes import.source.tab slots directly. Host transfer-state is the sole flow owner: preview persistence, previewAndPersist / previewDeepLinkImport (for /s), setImportMode / confirmImport with overwrite guards, and engine.importTimetable on confirm. All import plugins throw ImportSlotError + kind (no-data / invalid-data / network / unsupported / unknown); rich import tabs notify via controller.notify, not alert. Share-link codec lives in @chronos/plugin-codec-share only (no web copy). Export uses export.action slots; clipboard/download helpers live in apps/web/src/lib/platform/transfer.ts.
ImportTabSlotContribution.importKind (online | file | link) drives host onboarding/import copy without plugin-id hardcoding.
- Profile builtin plugins:
ProfileManager.loadPlugins/applyProfileis the only assembly surface. Host suppliesresolveBuiltinPlugin; phase 1/2 filters run throughloadPlugins. Plugin-center listing prefers a display cache fromresolveProfileBuiltinPlugins(metadata import, noloadPlugin), elselistLoadedPlugins(). - Official online plugins:
OfficialPluginServicefacade orchestrates four deep modules (OfficialPluginCatalogClient,OfficialPluginAssetPipeline,OfficialPluginInstalledStore,OfficialPluginRuntimeActivator) → fetch manifest + assets (SHA-256) →loadEsmPluginFromCode(when bundle present) →engine.loadPlugin.init()order:load → dedupeBuiltinOverlap → activate cache → syncInstalledWithHost.
Both paths share the same ChronosEngine lifecycle and slot owner tracking. No plugin.inject dependency topology — optional services use ctx.service(...) inside apply. Catalog: apps/web/static/official-plugins/catalog.json.
Server-side plugin handlers expose HTTP actions via /api/plugins/{pluginId}/{action}. Wire envelope is PluginServerResponse<T> in @chronos/core (pluginServerSuccess / pluginServerError / parsePluginServerResponse). IHttpService.proxy posts to this route from the browser; handler implementation errors use plugin-local AppResult, mapped at the handler boundary.
- Host shell UI:
host-uimessage catalog (apps/web/src/lib/i18n/host-messages.ts), registered on engine bootstrap; screens use reactivehostT()fromhost-i18n.svelte.ts. Paraglide handles cookie,document.lang, and URL de-localization only — locale switches do not reload the page. - Host navigation slots:
core-shellplugin registers shell/mine keys from the same catalog subset. - Plugins:
defineChronosPluginorctx.i18n.registerMessages(catalog)inapply; slots/schemas use() => ctx.i18n.t('key'); rich UI uses ui-kitpluginText(controller, pluginId, messages, key, params?). - Locale hub:
ChronosEngine.setLocaleemitsi18n:localeChanged;ReactiveChronosController.slotVersionincrements so slot UI re-resolvesLocalizedText;host-i18nsubscribes viaconfigureHostI18n. - See ADR 0024 (revised §D4 in ADR 0027).
ChronosPlugin(ESM bundle) — fullapply()+ slots (e.g.tool-wallpaper).ThemeManifest(JSON-only) —colorsUrl/iconThemeUrlwithout JS; assets register through a headlessScopedContextowned byOfficialPluginService(e.g.theme-yumemita). Manifests declarethemeIdexplicitly; the host never guesses id prefixes.
Slot field component?: ChronosMountable is the only rich-UI protocol. In-process Svelte components are wrapped via ui-kit mountableSvelteComponent(); ESM bundles ship their own mountable wrapper. Hosts render through MountableSlotOutlet (+ SchemaForm fallback) and never branch on component shape.
No global conflict arbitrator. Behavior by resource type:
| Resource | Strategy |
|---|---|
Multi-contribution slots (import.source.tab, mine.*, export.action) |
Coexist; sorted by order |
timetable.cell.badge |
Aggregate all contributors (RESERVED — pipeline live, zero producers; BadgeManager early-exits when empty) |
| Color / icon themes | Register many; user picks the color scheme, its recommendedIconTheme supplies icons (ADR 0026) |
Same contribution.id under one slot |
Last registration wins (warned in dev) |
Same plugin.id reload |
Unload then load |
| Profile builtin vs official install overlap | Builtin wins; official record deduped |
| Plugin uninstall with active theme | revertThemeIfNeeded → defaults |
dynamicColor:* events |
Broadcast; host keeps single dynamicColorUri (last emit wins) |
Builtin plugin (defineChronosPlugin) registering shell.bottom-bar.tab and mine.* slots. Loaded first in every profile. Host tabs declare hostPanel: 'timetable' | 'mine'; the host switches views via activeTabId on / and branches on hostPanel, never on tab id literals (ADR 0029 / 0032). defaultLaunch: true sets initial tab via resolveDefaultLaunchTab (first defaultLaunch in registry order); fallback is resolveHostPanelTab(tabs, 'timetable'). Plugin tabs omit hostPanel and render through resolveSlotOwner + PluginScreenContainer. Secondary tools still use /plugins/[pluginId]/... or IHostNavigation for host-owned editors. Mine items without sectionId use DEFAULT_MINE_SECTION_ID (app-support). Search keywords come from the host catalog (item.*.keywords).
Kernel events: dynamicColor:set, dynamicColor:changed, dynamicColor:hydrate. Host app-shell bridges to dynamicColorUri; ThemeContribution.dynamicColorAdapter (DynamicColorAdapter) paints course palette from image URI. Scheme id wallpaper in preferences is legacy-compatible naming.
@chronos/codec-kit: shared build-time byte-codec primitives consumed as a normal dependency by codec plugins (not a plugin, not in profiles). Exports deflateRaw/inflateRaw (deflate-raw; throws on failure — fallback policy belongs to callers), base64/base64url, crc32/appendCrc32/verifyAndStripCrc32, varint, canonical week bitmask (bit(w-1), MAX_TIMETABLE_WEEK=32, assertValidWeeks), and StringInterner. No wire-format envelopes live here. See ADR 0020.
Canonical implementation: @chronos/plugin-codec-share/share-link. Slots: import.source.tab (share-link), export.action (share-link).