feat(06-05): capability guards — haptics + close protection + optional webapp members + RS10/BA9 gate bumps (GREEN)
- fireHaptic: the three D-08 cues (selectionChanged / impactOccurred medium / notificationOccurred success) via ?. chains; silent no-op without the API; header carries the no-haptic-on-reroute law
- engage/releaseCloseProtection: PX-03 layers gated BEFORE each call — isVersionAtLeast("7.7") body-swipe, ("6.2") native-close net; no-throw on capability-less clients (T-06-06)
- webapp.ts: HapticFeedback / vertical-swipes / closing-confirmation / isActive join as OPTIONAL members only — existing fakes untouched
- RS10 floor 16→24; BA9 input list += web/src/guidance (69507 / 204800 gz, printed)
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* close-protection.ts — PX-03's layered close protection: engaged on
|
||||||
|
* guidance start, released at trip end (UI-SPEC §Close Protection).
|
||||||
|
* ----------------------------------------------------------------------------
|
||||||
|
* Layer 1: disableVerticalSwipes() (Bot API 7.7+) — the body-swipe minimize
|
||||||
|
* path closes while guidance is active; re-enabled at trip end.
|
||||||
|
* Layer 2: enableClosingConfirmation() (Bot API 6.2+) — the native close
|
||||||
|
* dialog is the defense-in-depth NET behind the Rams confirm card (the
|
||||||
|
* designed surface, D-09). The header swipe itself CANNOT be disabled —
|
||||||
|
* official Telegram docs — which is exactly why the net exists.
|
||||||
|
* Layer 3: the in-app abandon confirm card (banner.ts buildConfirmCard) —
|
||||||
|
* every exit path lands there; BackButton-from-guidance means the same card.
|
||||||
|
*
|
||||||
|
* T-06-06 mitigation, verbatim: version gates BEFORE every call + `?.`
|
||||||
|
* optional-member chains + no-throw behavior on capability-less clients
|
||||||
|
* (RESEARCH Pitfall 5). A client without the capability is a silent no-op —
|
||||||
|
* graceful degradation is the D-08 law applied to close protection.
|
||||||
|
*/
|
||||||
|
import type { TelegramWebAppLike } from "../telegram/webapp.ts";
|
||||||
|
|
||||||
|
export function engageCloseProtection(wa: TelegramWebAppLike | null): void {
|
||||||
|
// layer 1 — Bot API 7.7+: gate BEFORE the call, then the ?. chain
|
||||||
|
if (wa !== null && typeof wa.isVersionAtLeast === "function" && wa.isVersionAtLeast("7.7")) wa?.disableVerticalSwipes?.();
|
||||||
|
// layer 2 — Bot API 6.2+: the native-close net, gated the same way
|
||||||
|
if (wa !== null && typeof wa.isVersionAtLeast === "function" && wa.isVersionAtLeast("6.2")) wa?.enableClosingConfirmation?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function releaseCloseProtection(wa: TelegramWebAppLike | null): void {
|
||||||
|
if (wa !== null && typeof wa.isVersionAtLeast === "function" && wa.isVersionAtLeast("7.7")) wa?.enableVerticalSwipes?.();
|
||||||
|
if (wa !== null && typeof wa.isVersionAtLeast === "function" && wa.isVersionAtLeast("6.2")) wa?.disableClosingConfirmation?.();
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* haptics.ts — the eyes-free channel (D-08 / UI-SPEC §Haptics).
|
||||||
|
* ----------------------------------------------------------------------------
|
||||||
|
* fireHaptic dispatches the three guidance cues through the capability-
|
||||||
|
* guarded Telegram HapticFeedback API — the UI-SPEC's exact table:
|
||||||
|
*
|
||||||
|
* approach → HapticFeedback.selectionChanged() (within 300 m of the maneuver)
|
||||||
|
* maneuver → HapticFeedback.impactOccurred("medium") (the step boundary)
|
||||||
|
* arrive → HapticFeedback.notificationOccurred("success") (the trip-end
|
||||||
|
* card's tactile twin, eyes-on-road)
|
||||||
|
*
|
||||||
|
* D-08 graceful degradation is the law: a WebApp without the API (older
|
||||||
|
* client, plain browser, null) is a SILENT no-op — haptics never throw
|
||||||
|
* (T-06-06: version variance across Bot API 6.1–8.0+ is the trust boundary;
|
||||||
|
* the `?.` chains ARE the mitigation). Every cue is once-per-edge, armed by
|
||||||
|
* the guidance state machine (06-02) and dispatched here.
|
||||||
|
*
|
||||||
|
* The no-haptic law (UI-SPEC): NO cue on reroute — a haptic would announce
|
||||||
|
* blame, and the banner stays calm (D-04) — and none on confirm dialogs
|
||||||
|
* (visual + thumb is enough). Firing is the caller's discipline; the module
|
||||||
|
* exposes exactly the three legal cues so an illegal one cannot be typed.
|
||||||
|
*/
|
||||||
|
import type { TelegramWebAppLike } from "../telegram/webapp.ts";
|
||||||
|
|
||||||
|
export type HapticCue = "approach" | "maneuver" | "arrive";
|
||||||
|
|
||||||
|
export function fireHaptic(wa: TelegramWebAppLike | null, cue: HapticCue): void {
|
||||||
|
if (cue === "approach") wa?.HapticFeedback?.selectionChanged();
|
||||||
|
else if (cue === "maneuver") wa?.HapticFeedback?.impactOccurred("medium");
|
||||||
|
else wa?.HapticFeedback?.notificationOccurred("success");
|
||||||
|
}
|
||||||
@@ -17,10 +17,14 @@
|
|||||||
* members: `startParam` (the documented direct-link start parameter — NOT
|
* members: `startParam` (the documented direct-link start parameter — NOT
|
||||||
* populated by an inline web_app button, whose WebAppInfo carries only a
|
* populated by an inline web_app button, whose WebAppInfo carries only a
|
||||||
* `url`), `addToHomeScreen` and `checkHomeScreenStatus` (both Bot API 8.0+
|
* `url`), `addToHomeScreen` and `checkHomeScreenStatus` (both Bot API 8.0+
|
||||||
* only; on iOS the status is ALWAYS "unknown"). The hand-rolled contract
|
* only; on iOS the status is ALWAYS "unknown"). Phase 6 (PX-03/D-08) adds
|
||||||
* stays the contract — `@types/telegram-web-app` remains deliberately
|
* the guidance guards as OPTIONAL members only: `HapticFeedback`,
|
||||||
* NOT a dependency; every test fake and the harness mock satisfy exactly
|
* `disableVerticalSwipes`/`enableVerticalSwipes` (7.7+),
|
||||||
* these members.
|
* `enableClosingConfirmation`/`disableClosingConfirmation` (6.2+) and
|
||||||
|
* `isActive` (8.0+) — the contract stays the contract, no existing member
|
||||||
|
* changes shape, every test fake and the harness mock satisfy exactly
|
||||||
|
* these members. `@types/telegram-web-app` remains deliberately
|
||||||
|
* NOT a dependency.
|
||||||
*/
|
*/
|
||||||
export type HomeScreenStatus =
|
export type HomeScreenStatus =
|
||||||
| "unsupported" // the feature is not supported here
|
| "unsupported" // the feature is not supported here
|
||||||
@@ -45,6 +49,28 @@ export interface TelegramWebAppLike {
|
|||||||
addToHomeScreen?(): void;
|
addToHomeScreen?(): void;
|
||||||
/** Bot API 8.0+ only. On iOS this ALWAYS reports "unknown". */
|
/** Bot API 8.0+ only. On iOS this ALWAYS reports "unknown". */
|
||||||
checkHomeScreenStatus?(cb: (status: HomeScreenStatus) => void): void;
|
checkHomeScreenStatus?(cb: (status: HomeScreenStatus) => void): void;
|
||||||
|
/** Bot API 8.0+ only — whether the Mini App is the active view (false while
|
||||||
|
* minimized; the cron turn-card gate reads it via the "activated"/
|
||||||
|
* "deactivated" events). */
|
||||||
|
isActive?: boolean;
|
||||||
|
/** Bot API 6.1+ — the eyes-free haptic channel (D-08). Optional: absent on
|
||||||
|
* older clients; every call rides a `?.` chain and degrades to a silent
|
||||||
|
* no-op (haptics.ts — T-06-06). */
|
||||||
|
HapticFeedback?: {
|
||||||
|
impactOccurred(style: "light" | "medium" | "heavy" | "rigid" | "soft"): unknown;
|
||||||
|
notificationOccurred(type: "error" | "success" | "warning"): unknown;
|
||||||
|
selectionChanged(): unknown;
|
||||||
|
};
|
||||||
|
/** Bot API 7.7+ — close the body-swipe minimize path (PX-03 layer 1,
|
||||||
|
* engaged on guidance start). Optional — 7.7-gated before every call. */
|
||||||
|
disableVerticalSwipes?: () => void;
|
||||||
|
/** Bot API 7.7+ — re-open the body-swipe path (released at trip end). */
|
||||||
|
enableVerticalSwipes?: () => void;
|
||||||
|
/** Bot API 6.2+ — the native close confirmation: the defense-in-depth net
|
||||||
|
* behind the Rams confirm card (PX-03 layer 2). Optional — 6.2-gated. */
|
||||||
|
enableClosingConfirmation?: () => void;
|
||||||
|
/** Bot API 6.2+ — release the native close net (at trip end). */
|
||||||
|
disableClosingConfirmation?: () => void;
|
||||||
onEvent?(event: string, cb: () => void): void;
|
onEvent?(event: string, cb: () => void): void;
|
||||||
offEvent?(event: string, cb: () => void): void;
|
offEvent?(event: string, cb: () => void): void;
|
||||||
BackButton?: { show(): void; hide(): void; onClick(cb: () => void): void; offClick(cb: () => void): void };
|
BackButton?: { show(): void; hide(): void; onClick(cb: () => void): void; offClick(cb: () => void): void };
|
||||||
|
|||||||
@@ -37,9 +37,10 @@
|
|||||||
* its own.
|
* its own.
|
||||||
* BA9 the route payload fits the D-11 bundle budget: the summed gzipped
|
* BA9 the route payload fits the D-11 bundle budget: the summed gzipped
|
||||||
* size of the fixture dataset (routing/src/data/route-fixtures.json),
|
* size of the fixture dataset (routing/src/data/route-fixtures.json),
|
||||||
* every routing/src *.ts, and every web/src/route *.ts is ≤ 204800
|
* every routing/src *.ts, every web/src/route *.ts, and every
|
||||||
* bytes (200 KiB) — and the measurement is PRINTED on every run (the
|
* web/src/guidance *.ts is ≤ 204800 bytes (200 KiB) — and the
|
||||||
* budget is visible, never discovered). T-05-12.
|
* measurement is PRINTED on every run (the budget is visible, never
|
||||||
|
* discovered). T-05-12.
|
||||||
* BA10 the shipped JS carries EVERY pairId from the committed
|
* BA10 the shipped JS carries EVERY pairId from the committed
|
||||||
* route-fixtures.json (the BA7 analog, T-05-12/D-11): a stale bundle
|
* route-fixtures.json (the BA7 analog, T-05-12/D-11): a stale bundle
|
||||||
* that lost the fixtures could never serve a route.
|
* that lost the fixtures could never serve a route.
|
||||||
@@ -187,9 +188,12 @@ test("BA8 the shipped bundle carries NO deep-link crypto (D-11, T-04-40)", () =>
|
|||||||
|
|
||||||
/* ————— the route family's production-build gates (05-05, D-11, T-05-12) ————— */
|
/* ————— the route family's production-build gates (05-05, D-11, T-05-12) ————— */
|
||||||
|
|
||||||
/* BA9's inputs: every committed file the route experience ADDS to the web
|
/* BA9's inputs: every committed file the route + guidance experience ADDS to
|
||||||
* bundle — the fixture dataset, the routing workspace sources, and the web
|
* the web bundle — the fixture dataset, the routing workspace sources, the
|
||||||
* route modules. All are committed files, production-shaped by construction.
|
* web route modules, and (Phase 6) the web guidance modules (glyphs, banner,
|
||||||
|
* haptics, close-protection, guidance, eta, watch — the camera lands in plan
|
||||||
|
* 06 and rides the same list then). All are committed files,
|
||||||
|
* production-shaped by construction.
|
||||||
* The minified route code's gz size is approximated by its SOURCE gz size —
|
* The minified route code's gz size is approximated by its SOURCE gz size —
|
||||||
* the approximation is conservative in the gate's favor at these sizes
|
* the approximation is conservative in the gate's favor at these sizes
|
||||||
* (minified+gzipped code is smaller than source gzipped), and the 200 KiB
|
* (minified+gzipped code is smaller than source gzipped), and the 200 KiB
|
||||||
@@ -200,6 +204,7 @@ test("BA8 the shipped bundle carries NO deep-link crypto (D-11, T-04-40)", () =>
|
|||||||
const ROUTE_INPUT_DIRS = [
|
const ROUTE_INPUT_DIRS = [
|
||||||
join(WEB, "..", "routing", "src"),
|
join(WEB, "..", "routing", "src"),
|
||||||
join(WEB, "src", "route"),
|
join(WEB, "src", "route"),
|
||||||
|
join(WEB, "src", "guidance"),
|
||||||
];
|
];
|
||||||
const ROUTE_FIXTURES_FILE = join(WEB, "..", "routing", "src", "data", "route-fixtures.json");
|
const ROUTE_FIXTURES_FILE = join(WEB, "..", "routing", "src", "data", "route-fixtures.json");
|
||||||
|
|
||||||
|
|||||||
@@ -543,10 +543,10 @@ test("RS9 strings-only copy — no pending-card English in web/src/route; the ca
|
|||||||
|
|
||||||
/* ————— RS10 — the route family exists under the storage gate ————— */
|
/* ————— RS10 — the route family exists under the storage gate ————— */
|
||||||
|
|
||||||
test("RS10 no-storage floor bump — web/src/route exists and joins the GEO3 scan (>= 16 files)", () => {
|
test("RS10 no-storage floor bump — web/src/route exists and joins the GEO3 scan (>= 24 files, the guidance family landed)", () => {
|
||||||
assert.ok(existsSync(ROUTE_DIR), "web/src/route exists (the Phase 5 route module family)");
|
assert.ok(existsSync(ROUTE_DIR), "web/src/route exists (the Phase 5 route module family)");
|
||||||
const all = listFiles(SRC, [".ts", ".html"]);
|
const all = listFiles(SRC, [".ts", ".html"]);
|
||||||
assert.ok(all.length >= 16, `the src scan totals >= 16 files now the route family landed (found ${all.length})`);
|
assert.ok(all.length >= 24, `the src scan totals >= 24 files now the Phase 6 guidance family landed (found ${all.length})`);
|
||||||
const routeFiles = listFiles(ROUTE_DIR, [".ts"]);
|
const routeFiles = listFiles(ROUTE_DIR, [".ts"]);
|
||||||
assert.ok(routeFiles.length >= 4, `the route family carries its four modules (found ${routeFiles.length})`);
|
assert.ok(routeFiles.length >= 4, `the route family carries its four modules (found ${routeFiles.length})`);
|
||||||
for (const file of routeFiles) {
|
for (const file of routeFiles) {
|
||||||
|
|||||||
Reference in New Issue
Block a user