Skip to content

Этот раздел генерируется из исходников SDK (на английском языке) и является эталоном. Перевод приводится по мере обновлений.

All notable changes to @tv/extension-sdk.

The format follows Keep a Changelog; the SDK uses Semantic Versioning from 1.0 onward.

[Unreleased]

Fixed

  • publishConfig.registry pointed at public npmjs, where @tv/* has never been published and the scope isn't even ours (curl https://registry.npmjs.org/@tv%2fextension-sdk → 404; the @tv org page → 403). Every real publish has always gone to the private Verdaccio (https://npm.k8s.tangovision.dev/) — manual publishes because the @tv:registry scope mapping in ~/.npmrc overrides both publishConfig and an explicit --registry flag for scoped packages (verified during the 1.11.0 release: npm publish --registry=https://registry.npmjs.org/ still PUT to Verdaccio), and CI publishes because publish-extension-sdk.yml in tv-platform gets its registry from actions/setup-node's registry-url + scope: '@tv' inputs, which write the same kind of scope-specific mapping — not from publishConfig. So the stale value was never actually reachable, only misleading: a reader (or an agent) who trusted it would conclude the package is public, and an environment without the scope mapping would have sent a real publish attempt at a registry where we don't control the scope. publishConfig.registry now reads https://npm.k8s.tangovision.dev/, matching every other reference to the registry in this repo (README, AGENTS.md). Checked and unaffected: the generated .npmrc template (scaffoldNpmrc() in src/manifest/scaffold.ts) intentionally keeps a bare registry=https://registry.npmjs.org/ alongside the @tv:registry= override — that's the correct shape (unscoped deps from public npm, @tv/* from Verdaccio), not the same bug. No other publishConfig block exists in this repository.

Sales Impact

  • Internal correctness fix to a package manifest field; no customer-facing behavior changed. Omitted per AGENTS.md §3.3.

[1.11.0] — 2026-08-29

Changed

  • sandbox create now polls the provisioner instead of holding one HTTP request open for the whole provision. The provisioner answers POST /api/v1/sandboxes with 202 Accepted and a record in provisioning status (tv-platform#658); the CLI polls GET /api/v1/sandboxes/:id every 5s (progress line every ~30s in human mode, silent in --format=json) until the status turns ready or failed, giving up with exit 2 after 15 minutes — provisioning continues server-side and sandbox list / sandbox connect <id> pick it up later. Old synchronous provisioners still work: a ready response skips the polling loop, and when the create call is cut off mid-flight (the pre-202 failure mode — the ingress used to 504 a create that then succeeded server-side) the CLI reconciles by display name via GET /api/v1/sandboxes and resumes watching the sandbox it finds. New exports for tooling that drives creates itself: waitForSandboxTerminal, findLatestSandboxByName, SandboxWaitTimeoutError.

Sales Impact

  • Persona: external module developer on a slow or proxied network.
  • Problem Solved: sandbox create no longer reports a scary error for a create that actually succeeded — the command now reliably ends with the sandbox's real outcome, however long seeding takes.
  • Pitch Hook: kick off a sandbox and watch it come up — the CLI tracks provisioning to completion instead of gambling on one long HTTP request.

[1.10.0] — 2026-08-29

Added

  • login / logout / whoami — the interactive credential path (Phase 3.5 of the Module Developer Platform initiative). npx @tv/extension-sdk login completes the OAuth 2.0 Device Authorization Grant (RFC 8628) against the developers Keycloak realm: it opens the browser to the verification page (printing the URL and user code as the fallback — --no-browser skips the launch), polls the token endpoint with the RFC's pacing rules, and stores the resulting tokens in the OS keyring via @napi-rs/keyring — macOS Keychain, Linux Secret Service, Windows Credential Manager. Where no keyring is usable (headless Linux without a D-Bus secret service, unsupported platforms) the tokens fall back to ~/.config/tv-sdk/credentials.json with 0600 permissions and a stderr warning; TV_SDK_TOKEN_STORE=file|keyring forces the backend — file is the right setting for CI, keyring turns a silent fallback into a hard error. The flow requests offline_access deliberately: without it the refresh token dies with the realm's 30-minute SSO idle timeout, with it the session survives until 30 days unused (the realm's offline idle limit). logout revokes the refresh token server-side (RFC 7009) and clears local storage even when the realm is unreachable; whoami proves the whole chain by round-tripping storage → refresh → userinfo. Tokens are never printed. The Keycloak side is a dedicated public client tv-sdk-cli with ONLY the device grant enabled (standard flow and direct grants off) and the same developer-api audience mapper as developer-portal, so provisioner-side JWT verification needed no changes. Limits worth knowing: login is the interactive path — tvk_… API keys minted in the developer portal remain the non-interactive credential for CI, and the account must exist in the invite-only developers realm first.

  • sandbox live subcommands now actually call the deployed provisioner. list, connect, extend, delete, reset, and live create were "service is not yet deployed" stubs from Phase 1a.4; they now call https://sandbox-api.k8s.tangovision.dev (override with --api, TV_SANDBOX_API_URL, or TV_API_URL, in that order — the localhost default is gone now that the service is real). Credential resolution, in order: --token flag, TV_API_TOKEN env, then the stored login session with automatic token refresh — so a logged-in developer runs npx @tv/extension-sdk sandbox create … with no env setup at all, while existing API-key workflows keep working unchanged. New --ttl-days=N flag on create (1–30, service default 14). Distinct failure modes are distinguished: 401 tells you which credential path was rejected and what to do, quota refusals surface the provisioner's structured message, and an unreachable service still exits 2 where a refused request exits 1.

  • init-module scaffolds the ADR 103 federation-safe i18n setup, and the module it emits now builds. Three things every generated frontend is born with, ported from the canonical tv-module-example: src/shims/use-sync-external-store-shim.ts (an import-then-re-export of useSyncExternalStore from react — a plain export { x } from 'react' re-export is NOT rewritten by the federation plugin and reintroduces the second-React crash), the matching resolve.alias for use-sync-external-store/shim in the generated vite.config.ts, and a generated .github/workflows/ci.yml whose frontend checks job builds the real federation remote and mounts it in the shell-integration smoke (tangovision/infrastructure/.github/actions/shell-smoke@master, report-only). Without the alias, every scaffold carrying react-i18next ≥ 16 shipped the 2026-08-27 incident by construction; the smoke is the only CI signal that can see that failure class.

    For that CI to be green on the first push, the scaffold also gains the files a generated module was missing to build at all: tsconfig.json (the generated tsc -b && vite build / tsc --noEmit scripts had nothing to run against), index.html + src/main.tsx (the standalone dev entry vite build needs), .npmrc (without it the first pnpm install resolves @tv/extension-sdk against npmjs and 404s), and src/locales/catalogues.test.ts — the key-parity test the generated src/i18n.ts docblock was already promising ("the parity check in CI will hold you to it") without shipping it. New exports: scaffoldShim(), scaffoldCiWorkflow(), scaffoldTsconfig(), scaffoldIndexHtml(), scaffoldMainEntry(), scaffoldNpmrc(), scaffoldCatalogueTest().

  • init-module emits a .gitignore. A freshly scaffolded module had none, so its very first commit could take node_modules/, dist/ and coverage output with it — and because ignoring an already-tracked path does not untrack it (platform rule 3.1), that mistake is permanent until someone runs git rm --cached, which is exactly the cleanup the 2026-08 fleet sweep spent on repos born this way. The generated rules cover install output, build output (dist/, *.tsbuildinfo), coverage, logs and local .env files, and deliberately do NOT ignore pnpm-lock.yaml: the generated CI installs with --frozen-lockfile, which fails on a repo without a committed lockfile. Exported as scaffoldGitignore(). (Extracted from the still-useful part of #82; that PR's i18n half predates ADR 103 and was superseded by the shim + alias work above.)

Changed

  • The shipped platform rules state that pnpm.overrides replaces, rather than merges. Re-synced PLATFORM_RULES from tangovision/templates/PLATFORM-RULES.md (templates#32). The override-floor bullet previously stopped at "pnpm-workspace.yaml, not package.json", which is correct advice and still misses the way the floors actually came off: a non-empty pnpm.overrides in an install root's package.json replaces that root's pnpm-workspace.yaml overrides: block wholesale, not key by key, so a floor written in only one of the two files is not a floor at all. Both files may legitimately carry the block — pnpm install --ignore-workspace does not read pnpm-workspace.yaml — but then they must be kept byte-identical, and ERR_PNPM_LOCKFILE_CONFIG_MISMATCH must never be answered with --no-frozen-lockfile, which turns the loud failure green and the silent downgrade permanent. tv-platform#598 pasted a generated floor list into 13 package.json files and made six workspace blocks inert this way; in tv-module-notifications a deepmerge-ts CVE floor silently weakened from ^8.0.1 to >=7.1.5 (GHSA-ggr8-5vv4-36mx) while the comment above it still claimed the two files were in sync (tv-platform#615). Full rationale and the override-source.yml gate that detects it: tangovision/infrastructure/docs/ci-gates.md#the-pnpm-override-source-invariant (infrastructure#414).

  • Scaffolded modules pin react-i18next at ^17.0.0 (was ^16.0.0) — the major ADR 103 verified as safe in both sanctioned i18n models and what tv-module-example runs. Safe only together with the shim alias above; neither half may be dropped without the other.

  • buildingTypes documents where its vocabulary is decided. The field's slugs are validated at ingest, against tv-api's building_types registry — the platform's single source of truth for building types (ADR 097 layer 2, seeded in tv-api#162). POST /api/v1/registry/modules/ingest now rejects a manifest naming a type that does not exist, listing the valid values; before, a typo ingested happily and gated the module to nothing, silently and permanently.

    The Zod schema deliberately does not enumerate the slugs. This package ships on its own release cadence, so an enum here would be a fourth copy of a list that already disagreed with itself in three places, and a module could not adopt a newly added building type until the SDK caught up. The schema keeps validating slug shape; the registry decides slug membership.

Security

  • The copilot's init_module tool and the RU contracts doc invoke the CLI by its full package name. Both used bare npx tv-sdk — the doc as an instruction, the copilot as a string passed to execSync. The package name tv-sdk is unclaimed on public npm (npm view tv-sdk → 404), and outside a directory with @tv/extension-sdk installed locally, npx resolves a bare name against registry.npmjs.org — so a squatter publishing tv-sdk would have had their code executed by every developer following the checklist, and by the copilot itself when run outside such a directory. Both sites now spell npx @tv/extension-sdk <cmd>, which resolves through the authenticated @tv registry wherever it runs; since the package declares a single bin, the two forms are otherwise identical. The developer-docs guides carried the same instruction in 34 places (fixed in tv-platform#647), and infrastructure/docs/ci-gates.md in one.

Sales Impact

  • Persona: external module developers and ISV partners in the developer-platform preview.
  • Problem Solved: getting a credential into the CLI used to mean signing in to the portal, minting an API key, and pasting it into environment variables before the first sandbox command could run — and that key then lived in shell history or a dotfile. Now it is one browser sign-in: npx @tv/extension-sdk login, and every sandbox command authenticates itself from the OS keyring.
  • Pitch Hook: "Sign in once in your browser and the SDK remembers you — securely, in your OS keyring, for up to 30 days of inactivity. No tokens in dotfiles, and logout revokes the session server-side, not just locally." (Scope of the claim: the interactive CLI path in the invite-only preview; CI and other non-interactive callers still use portal-minted API keys.)

[1.9.0] — 2026-08-25

Added

  • init-module emits AGENTS.md and CLAUDE.md. The AGENTS.md opens with the platform rules from tangovision/templates/PLATFORM-RULES.md (English only, call build-push.yml, setup-pnpm / setup-tv-registry, org secrets, override floors, shared-runner constraints), so an agent working in a scaffolded module repo can see rules that otherwise live only in the ceo monorepo. Exported as scaffoldAgentsMd(), scaffoldClaudeMd() and the PLATFORM_RULES constant.

[1.8.0] — 2026-08-20

Added

  • building.fault.created joins the event catalog. tv-api's FDD engine has emitted this event since the fault/work-order bridge landed — src/fdd/fdd.service.ts publishes it immediately after building.alarm.triggered whenever a diagnostic rule opens a fault — but it was never declared in EVENT_CATALOG, so it was absent from tv-events.snapshot.json and invisible to every piece of tooling built on that snapshot.

    The visible symptom was in check-pact: @tv/module-cafm declares a full payloadContract on it (Auto-create a work order from an FDD fault) and was told EVENT_NOT_PUBLISHED — rule 1, the diagnosis reserved for a consumer subscribing to an event nothing emits. That was wrong on the facts. The producer exists, is wired end to end (EventBusService.publish routes it to tv.building.<buildingId>.building.fault.created, which is exactly the subject CAFM's TicketBridgeService subscribes to), and CAFM's handler, dedup path, faultId column and unit tests are all in place. Only the declaration was missing. tv-platform's manifest-fleet-gate carried the gap on its allowlist; that line comes off with this release.

    The payload is transcribed from the emit site rather than from the consumer's expectations: faultId, buildingId, severity, title and detectedAt are required (all non-null in the faults table, and detectedAt defaults to now()), while elementId and pointId are optional (both nullable columns, emitted as ?? undefined).

    severity is deliberately an open z.string().min(1) and not an enum like building.alarm.triggered's. The faults.severity column is a free String written by rule definitions, and tv-api's own summary code guards with if (f.severity in bySeverity) instead of assuming the set — so an enum here would advertise a guarantee the producer does not make, and would hand consumers a false PRODUCER_EMITS_UNHANDLED_ENUM signal. Common values are critical/high/medium/low; CAFM's severity mapping already falls back for anything else.

[1.7.0] — 2026-08-20

Added

  • init-module emits a pnpm-workspace.yaml with the fleet's build-script approval lists. A freshly scaffolded module was born unable to run the very pnpm install the CLI prints as its next step: pnpm 11 hard-fails install with ERR_PNPM_IGNORED_BUILDS on any dependency whose build script is not approved (pnpm 10, the platform CI pin, only warns), and the scaffold's own tree contains one such package — esbuild, via vite. This is the same failure that had to be fixed one repo at a time across all eleven existing tv-module-* repositories (e.g. tv-module-cafm#79, tv-module-leases#89/#90).

    The generated file carries the approvals in the fleet's dual-list convention — allowBuilds (a map, read by pnpm 11) mirrored by onlyBuiltDependencies (a list, read by pnpm 10), alphabetical — both rendered from a single exported constant, APPROVED_BUILD_PACKAGES, so the generated lists cannot drift apart the way hand-mirrored ones have. The set is the platform's canonical one (what tv-module-cafm and tv-module-example carry), not the scaffold's minimal need: beyond esbuild it pre-approves what the backend every module grows next actually pulls in — @prisma/engines (prisma postinstall) and unrs-resolver (jest ≥ 30.4's native resolver) above all. @scarf/scarf is deliberately not listed: the scaffold has no @nestjs/swagger, and the file's comment instructs the author to add the fleet's explicit block ('@scarf/scarf': false + ignoredBuiltDependencies) rather than an approval if swagger ever brings it in.

    The scaffold-canary workflow gains an install of the generated module under a pinned pnpm 11.18.0. The existing pnpm 10 install cannot regress this guarantee — 10 only warns — so without it the failure would quietly return the first time the scaffold's dependency tree grows a new build-script package.

[1.6.1] — 2026-08-20

Fixed

  • tv-sdk check-pact resolves its default snapshot without assuming cwd. The default producer snapshot was a single cwd-relative path, packages/tv-extension-sdk/tv-events.snapshot.json, which only exists when the command runs from the tv-platform meta-repo root. Running check-pact from inside the SDK repo — where tv-events.snapshot.json sits in the repo root — failed with ENOENT, and the standing workaround was to pass --snapshot=tv-events.snapshot.json by hand. Pre-existing; 1.6.0 left it alone to keep the --producer change scoped.

    The default is now a search over candidates, most-authoritative first: the meta-repo layout relative to cwd (unchanged, still the common case in CI), then the snapshot bundled with the running SDK install, then an upward walk from cwd checking both the bare filename and the meta-repo layout at each ancestor. The bundled candidate is resolved from the module's own URL rather than from cwd — cwd is the thing that cannot be trusted here — which is what makes the SDK repo, an npx @tv/extension-sdk install, and vitest running the TypeScript source all land on the right file. tv-events.snapshot.json is listed in package.json files, so it is present at that root in a published tarball too.

    An explicit --snapshot=<path> still wins outright and is still what the error tells you to reach for, but the failure now lists every candidate that was tried instead of naming one path that was never going to exist.

    check-events and snapshot-events were checked for the same defect and do not share it: both take a required positional path, and snapshot-events' path is a write target, where an upward search would be wrong. They are unchanged.

    Resolution moved out of cli.ts into src/events/snapshotPath.ts (findDefaultSnapshotPath(), defaultSnapshotCandidates()) so it is unit-testable — cli.ts runs process.exit() on import. It is deliberately not re-exported from @tv/extension-sdk/events, which module frontends import and which must stay bundler-safe; this file reaches for node:fs.

[1.6.0] — 2026-08-20

Added

  • Modules can act as event producers in check-pact. The pact checker built its producer view solely from EVENT_CATALOG — tv-api's first-party events. Module-published events are declared in module-manifest.json under events.publishes and never appear in that catalog by design, so a consumer declaring a payloadContract on one tripped rule 1 (EVENT_NOT_PUBLISHED) and failed CI with no correct way out.

    tv-sdk check-pact gains a repeatable --producer=<manifest> flag that federates a producing module's events.publishes into the producer view. The first-party snapshot is merged first, so tv-api keeps ownership if a module ever collides with a catalog event; a second claim on the same event name is reported as a conflict rather than silently overwriting.

    The alternative — grafting module events into EVENT_CATALOG — was rejected: it misattributes them to tv-api, couples a module's contract to the SDK release cycle, and enforces nothing at runtime, since the catalog has no runtime consumer and feeds snapshot/CI tooling only.

    New exports on @tv/extension-sdk (src/events/manifestProducer.ts): buildModuleProducerSnapshot() converts a manifest's events.publishes[*].payloadContract into the snapshot shape buildEventSchemaSnapshot() emits, and mergeProducerSnapshots() unions producer snapshots. Both are pure and take generatedAt from the caller so regenerated output stays byte-identical.

  • payloadContract is now symmetric. On events.subscribes[*] it remains the subset of a producer's payload the consumer reads; on events.publishes[*] it describes what the module guarantees to emit. Same wire format and no schema change — manifest.schema.json and tv-events.snapshot.json are byte-identical to 1.5.0.

    A publishes entry without a payloadContract still registers the event, deliberately: the module does publish it, so a subscriber must never be told EVENT_NOT_PUBLISHED. Such an entry carries an explicit x-tv-opaque marker (OPAQUE_PAYLOAD_MARKER) and surfaces as a new info-level PRODUCER_PAYLOAD_OPAQUE diff, making the coverage gap visible without turning CI red. The marker is explicit rather than inferred from a missing properties key, because that key is also absent on a corrupt or truncated snapshot — which must keep reporting breaking diffs as it always has.

[1.5.0] — 2026-08-19

Added

  • Shared <LocationPicker> on @tv/extension-sdk/react. The storey → space → optional element cascade that CAFM and service-desk each hand-rolled over the same three graph endpoints is now one component (ADR 094 phase 5). It runs over any BuildingGraphClient passed as a prop, queries through buildingGraphKeys with the same query shapes as the modules' useBuildingGraph hooks (so both observe the same react-query cache entries), and emits ids only{}, {storeyId}, {storeyId, spaceId} or all three, spread-safe into create payloads. The denormalized locationName label is a write-once historical value by decision; modules that persist one compose it at submit time from data the picker already fetched.

    What each donor contributed: CAFM's client-side space search above a threshold (spaceSearchThreshold, default 8) and its element formatting hook (formatElement); service-desk's graceful error branch, ids-only emission semantics and data-testid contract (location-picker, -no-building, -unavailable, plus new per-select ids).

    Integration stays on the module's side of the boundary, which is why the three new peer dependencies are optional (peerDependenciesMeta): @tanstack/react-query is imported only by this component; @tv/ui is never imported at all — it resolves only through the authenticated registry and this repo's CI installs without credentials, so the picker takes Input/Label through a structurally-typed components slot and renders native elements otherwise; react-i18next stays entirely in the host — every user-facing string arrives via the required labels prop, no namespace is baked in.

[1.4.1] — 2026-08-19

Changed

  • Vendored graph types re-synced with upstream. tv-building-graph#1 aligned that package with schema.prisma — nullable BuildingChangeset.buildingId, the new siteId, and optional provenance columns (externalId, sourceSystem, classifications) on Storey/Space/Element — landing upstream what this SDK's wire layer had been declaring on its own since 1.4.0. The vendored copy now matches upstream main byte-for-byte below its provenance header, and the wire layer derives instead of overriding: the buildingId/siteId overrides on Changeset are gone, and ProvenanceFields is Required<Pick<…>> of the vendored Storey rather than a hand-maintained parallel declaration. Provenance stays non-optional on the wire — upstream made the columns optional for construction-side ergonomics, but API responses always include them, and the SDK keeps that guarantee. No public type changes: every exported wire shape is structurally identical to 1.4.0.

[1.4.0] — 2026-08-19

Added

  • Typed building-graph client. New subpath @tv/extension-sdk/graph exports createBuildingGraphClient(): read + write for the core graph (storeys, spaces, elements) and its versioning surface (changesets with the full draft → submit → review → apply → revert lifecycle, point-in-time history and diffs, named snapshots), plus both catalogs. Every path, query parameter, request body and response shape mirrors tv-api's controllers and DTOs, so a consumer that typechecks is calling endpoints that exist — this is ADR 094 phase 2.

    The wire types derive from the @tv/building-graph package (vendored, the way the events catalog and the OpenAPI/permissions snapshots already ship in this package — @tv/* resolves only through the authenticated registry, and this repo's CI must keep installing without credentials) through a Serialized<T> transform: over JSON, every Date is an ISO-8601 string. Response-only realities the package does not model — nested includes, pagination envelopes, provenance columns (externalId, sourceSystem, classifications), the nullable buildingId + siteId on site-level changesets — are layered on top, verified against schema.prisma.

    Why this exists: the fleet survey behind ADR 094 counted six hand-rolled Space types and two copy-pasted useBuildingGraph.ts hooks (service-desk, CAFM) drifting against the same six endpoints, and the shell's Structure page added a third client the same week. One typed client, three deletions.

    The client is transport-agnostic — it runs over any PlatformApiClient (the context-provided client inside a federated module, or the shell's own). For the axios instances every current consumer actually has, platformApiFromAxios() wraps one in five lines, and apiPrefix: '' accommodates base URLs that already end in /api/v1. Also exported: buildingGraphKeys, a canonical react-query key factory (plain arrays, no react-query dependency) whose root segment matches the module hooks it replaces, so their caches survive the migration.

[1.3.1] — 2026-08-18

Fixed

  • Every subpath is now loadable from CommonJS. The exports map declared only import and types on eleven of its twelve subpaths — ., ./manifest, ./context, ./events, ./api, ./heartbeat, ./react, ./testing, ./version-check, ./permissions and ./i18n. Only ./nestjs declared require. Each of the eleven now declares it too, resolving to the same file as import.

    This failed in the one way that survives every gate. TypeScript resolves a CommonJS import of ./i18n by walking the conditions, finding no require, and matching types instead — so tsc reports success and the image builds clean. Node has no concept of a types condition, finds nothing it can load, and throws ERR_PACKAGE_PATH_NOT_EXPORTED the first time the code actually runs. Compile-time green, runtime dead.

    It matters now because of who consumes this package. All twenty-five module backends are CommonJS — nineteen on module: commonjs, six on nodenext with no "type": "module" — so every one of them resolves this package under the require condition. None imports ./i18n yet, which is the only reason 1.3.0 shipped without anyone noticing; the i18n migration currently moving through the fleet is what would have found it, one module at a time, in whatever environment ran the code first.

    The package stays "type": "module" and ships one build: require and import point at the same ESM file, which Node loads through require(esm). That is what ./nestjs has always done, and the module images run Node 22, where it is unflagged. Nothing about the ESM entry points changes, so bundled frontend consumers resolving under import are unaffected.

    A contract test now asserts that every subpath declares require and types, and that require and import agree — the check that would have caught this when ./i18n was added in 1.3.0.

[1.3.0] — 2026-08-09

Added

  • Locale resolution is now a supported API. New subpath @tv/extension-sdk/i18n exports resolveLocale() plus the storage keys the platform agrees on (LOCALE_STORAGE_KEY, RESOLVED_LOCALE_STORAGE_KEY, LEGACY_LOCALE_KEYS, LOCALE_LOOKUP_ORDER, FALLBACK_LOCALE). A federated module can follow the shell's language without writing its own detector.

    This closes the same class of gap the building-selection API closed in 1.2.0. Five modules — service-desk, leases, comfort, parking, room-booking — had each hand-written a detectLanguage() that reads the shell's localStorage key, copied from one another and already drifting. A module reading the wrong key does not fail loudly; it simply stays in the old language while the shell switches, which is how service-desk sat in Russian inside an English shell without anyone noticing. Thirty-three copies of a policy is thirty-three pull requests per change, so the policy moves here and a change becomes a version bump.

    Modules never ask the browser. resolveLocale() reads storage and nothing else. A remote that called navigator.language itself could disagree with the shell it renders inside — an English panel on a Russian page — and neither half would look wrong in isolation. One detector, in the shell; everyone else follows it.

    Two keys, deliberately. tv-platform-locale means the user explicitly picked a language; tv-platform-locale-resolved means this is what the shell is currently rendering, detection included. i18next's default caches: ['localStorage'] collapses the two — the first visit persists a detected language as though it had been chosen, and the browser's language is then ignored forever after. Keeping them apart is what lets an explicit choice outrank detection permanently while detection still reaches modules.

    Region tags fold only when they have to: en-US resolves to an en catalogue, while pt-BR stays pt-BR rather than collapsing into a pt catalogue nobody ships. A stored language the module has no catalogue for is rejected rather than rendered, because fallbackLng would otherwise fill every key from the fallback catalogue and produce a fluent-looking page in the wrong language. Storage that throws — private mode, embedded webviews, locked-down kiosks — yields the fallback instead of taking down the federated route over a rendering preference.

[1.2.0] — 2026-08-04

Added

  • Building selection is now a supported API. @tv/extension-sdk/context gains readBuildingSelection(), subscribeBuildingSelection() and isSameBuildingSelection(); @tv/extension-sdk/react gains useSelectedBuildingId() and useBuildingSelection(). A federated module can now follow the Building OS header selector — including reacting to changes while mounted — without parsing the host's storage itself.

    This closes a gap that had every module solving it privately. PlatformContext.building was the documented answer, but it travels through React Context, and React Context does not cross a Module Federation boundary unless @tv/extension-sdk is a shared singleton in both the host and every remote — which it is not. usePlatformContext() therefore throws inside a federated module even though the shell renders <PlatformProvider>, so it had zero module consumers, while five modules (cafm, equipment, leases, service-desk, bim) had each hand-written a parser for the shell's building-storage key and its tv:building-selection-changed event. The SDK now owns that read: the storage key and event name become the SDK's problem rather than each module's, and the same call keeps working when the shell later moves to a shared-singleton context.

    BuildingSelectionEntry.organization is optional and parsed all-or-nothing — a half-populated value is dropped rather than rendered as a confident owner label. Older shells persist only {id, name}; the field is here from the start so consumers can render the owning organization as soon as a host publishes it, without waiting on another SDK release. It matters wherever building names are not unique: a platform admin sees several organizations' buildings at once, and production has more than one set sharing a name.

    The new functions do what the hand-written copies did not: they de-duplicate notifications so a 2-second fallback poll no longer re-renders consumers on every tick, they recover the primary building when only the legacy scalar id was persisted, and they reject the literal string "undefined" that reached the store in the wild.

Changed

  • README no longer teaches useBuilding() as the way to scope a federated module to a building — that example did not work in the environment it was written for. It now leads with useSelectedBuildingId(), states that Building OS has exactly one building selector and modules must not ship a second, and keeps the PlatformContext example scoped to code running inside the shell bundle.

[1.1.0] — 2026-08-01

Minor rather than patch: the fixes below are patch-level, but --scope adds a CLI flag and a ScaffoldInput field. Additive surface bumps the minor.

The headline is that manifest.schema.json is usable for the first time. Until now it was emitted in output mode and rejected 30 of the 31 manifests actually shipping, so $schema had to be left out of module manifests entirely. On this version you can point $schema at it and get editor validation that agrees with tv-sdk validate.

Fixed

  • check-exposes no longer reports a false "Missing canonical expose ./Shell" on modules that are correct at runtime (#31). It reads vite.config.ts as text, and two static-analysis faults compounded: the entry regex matched only quoted keys, so a computed [`./${EXPOSE_COMPONENT}`] never registered; and the exposes: { … } block was sliced with a non-greedy /\{[\s\S]*?\}/ that stopped inside ${EXPOSE_COMPONENT}, dropping every later entry once one used a placeholder. Simple single-file const NAME = 'literal' declarations are now folded in before matching, the entry pattern accepts template literals and the bracketed computed form, and a brace-balanced scanner (skipping strings, template literals including nested ${}, and comments) replaces the regex. Anything still unresolvable is reported rather than silently dropped, and a missing ./Shell alongside unresolved keys is a warning, not an error.

  • manifest.schema.json is now emitted in input mode. It was generated with Zod's default io: 'output', which describes a manifest after parsing — so every field carrying a .default() (sdkVersion, buildingTypes, capabilities, mcpTools, lifecycle, plus ui.remoteEntry, ui.dashboardWidgets, ui.defaultLandingByRole, navigation[].roles, mcpTools[].permissions) was published as required. The artifact exists so consumers can point $schema at it for editor validation, where that is exactly backwards: it demanded the fields the validator exists to supply. Measured against tv-platform, the published schema rejected 30 of 31 shipping manifests — including tv-module-example, the reference implementation — while tv-sdk validate accepted all of them. The mismatch went unnoticed because the $schema URL the docs advertised 404'd. The schema and the Zod validator now agree exactly; the 5 manifests that still fail do so under both, and are genuine manifest bugs. Regenerated. New src/manifest/generate-schema.test.ts locks the invariant so a future emitter change can't silently reintroduce it.

  • init-module / init-manifest scaffolder defaults corrected. All three were wrong for the external module authors the developer portal now routes to this command:

    • sdkVersion was the hardcoded literal "0.1.0", stale since before the 1.0 contract. Now stamped from the package's own version at scaffold time. The platform gates on this field when deciding whether it can run a module, so a stale value is not cosmetic.
    • $schema was "../../packages/tv-extension-sdk/manifest.schema.json", which only resolves from inside the tv-platform monorepo and dangled for everyone else. Now "./node_modules/@tv/extension-sdk/manifest.schema.json", which resolves for anyone who installed the SDK (pnpm symlinks the workspace package into the same path). Useful again now that the schema itself is input-mode.
    • The module id hardcoded Tango Vision's own @tv scope, so a third-party module was scaffolded into a scope it must not publish under.

Added

  • --scope=<scope> on init-manifest and init-module (and ScaffoldInput.scope), with or without the leading @: --scope=acme produces @acme/module-<slug> in both the manifest id and the generated package.json name. Defaults to @tv for first-party use — unchanged behaviour for existing callers.

Changed

  • init-module's "Next steps" output now says pnpm install / pnpm dev. pnpm is the platform standard; the scaffolder was the one place still telling authors to use npm.

[1.0.6] — 2026-07-24

Fixed

  • Manifest schema: event names may now contain underscores within dot-separated segments ([a-z0-9_-], segment must still start with a letter or digit). The old pattern rejected the platform's own first-party catalog events — building.element.status_changed, and the new service-desk.ticket.sla_warning / sla_breached — so tv-sdk validate failed on any manifest that declared them (hit while updating tv-module-cafm's manifest). Module/capability ids and permission subjects keep the stricter kebab-case pattern; only events.publishes[].name / events.subscribes[].name are affected. manifest.schema.json regenerated.
  • buildSubject() had the same gap: it threw Invalid eventType for snake_case event types, including the catalog's own building.element.status_changed. NATS subject tokens permit _, so the guard now allows it (wildcard/injection characters are still rejected).

[1.0.5] — 2026-07-19

Changed

  • permissions.snapshot.json regenerated from tv-api's PermissionRegistry (#11) — adds the building.models and building.payments subjects. Additive only: no existing subject or grant was removed. This version bump is what actually ships the regenerated catalog to consumers — the snapshot was regenerated in place in 1.0.4 without a version bump, so the published 1.0.4 tarball still carried the pre-regen catalog and installs never saw the new subjects.

[1.0.4] — 2026-07-17

Fixed

  • tv-sdk write-version now reads the TV_APP_COMMIT env var when --commit is not given, before falling back to git rev-parse HEAD. The standard module Dockerfile declares ARG TV_APP_COMMIT + ENV TV_APP_COMMIT=$TV_APP_COMMIT expecting exactly this, but .git is dockerignored, so every containerized build stamped "commit": "unknown" into version.json. Resolution order is now --commit flag → TV_APP_COMMIT env (non-empty) → git rev-parse HEAD"unknown" — the same precedence building-os uses for its __TV_APP_COMMIT__ build constant.

[1.0.0] — 2026-05-30

First stable release. The surface tagged in Phase 2.1 (see STABILITY.md) is now binding under semver: @stable exports carry a 6-month deprecation guarantee; @experimental exports may still change in any release.

No breaking API changes from 0.2.0 — the move to 1.0 is the commitment to the surface, not a reshaping of it. Existing consumers upgrade by bumping the version; review the @experimental list below and decide which symbols to track changes on.

Added

  • Stability tiers documented: every subpath barrel now carries a @stable or @experimental JSDoc tag. See STABILITY.md.
  • @tv/extension-sdk/permissions — permission catalog extractor + types (extractCatalog, extractCatalogFromFile, PermissionCatalog, SubjectEntry). Phase 2.4.
  • tv-sdk snapshot-permissions CLI command — extracts the catalog from tv-api's PERMISSION_ROLE_MAP source. Phase 2.4.
  • permissions.snapshot.json artifact ships with the package (17 subjects, 33 actions as of 2026-05-25).
  • @tv/extension-sdk/sandboxtv-sdk sandbox CLI subcommand family (create / list / connect / extend / delete / reset). Phase 1a.4. @experimental — surface may grow during Phase 1a rollout.

Changed

  • Build moved off zod-to-json-schema (Zod 3-only) onto Zod 4's built-in z.toJSONSchema(). Affects manifest.schema.json + tv-events.snapshot.json outputs — same contract, normalized representation. CI's check-events self-consistency passes against the regenerated snapshot.
  • tsconfig.json adds "types": ["node"] so node:fs / process / etc. resolve. Closes silent SDK build break that started 2026-05-01.
  • z.record() call sites updated to Zod 4's two-arg form (z.record(z.string(), …)).
  • manifest.schema.json regenerated under Zod 4's emitter — more strict (more required fields surfaced). Downstream consumers using $schema for editor validation should re-validate their manifests.

Removed

  • zod-to-json-schema dependency (no longer used).

Stability tiers in this release

Already @stable (no churn expected within a major):

  • @tv/extension-sdk/manifest — schema + validator + scaffolders + checkExposes
  • @tv/extension-sdk/contextPlatformContext types
  • @tv/extension-sdk/events — catalog, envelope, subjects
  • @tv/extension-sdk/apiPlatformApiClient types
  • @tv/extension-sdk/react — provider + core hooks
  • @tv/extension-sdk/nestjs@ModuleCapability(), @RequiresLicense() decorators
  • @tv/extension-sdk/testingcreateMockPlatformContext
  • @tv/extension-sdk/permissions — extractor + catalog types
  • CLI: validate, check-exposes, init-manifest, init-module, write-version, snapshot-permissions
  • JSON artifacts: manifest.schema.json, permissions.snapshot.json

@experimental (may change in any release until graduation; see STABILITY.md):

  • @tv/extension-sdk/heartbeat
  • @tv/extension-sdk/version-check
  • @tv/extension-sdk/sandbox (CLI subcommands)
  • Manifest fields: PayloadContract, McpToolDeclaration.permissions
  • CLI: check-events, check-api, check-pact, snapshot-events, snapshot-api
  • React: useVersionCheck, <UpdatePrompt>
  • NestJS: TvHeartbeatModule.register()

[0.2.0] — 2026-04 (historical reference)

Initial SDK consolidation. ModuleManifest schema + PlatformContext + React/NestJS integration. See git history for details.

Migration guide: 0.x → 1.0

When 1.0.0 ships (Phase 2.2), the surface above becomes binding under semver. There are no breaking API changes between current pre-1.0 and the planned 1.0 — only the addition of the stability tiers. Existing internal consumers should not need code changes; they should review the @experimental list above and decide which symbols to track changes on more carefully.

Создано на платформе Tango Vision. Вопросы? developers@tango.vision