Rewrite frontend types and HTTP client in src.
Export names match the leftover pages. Billing HTTP wrappers stay so gateway seams are not restyled here.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,533 @@
|
|||||||
|
/** Frontend mirrors of Periscope backend contracts (`periscopex.models` + HTTP). */
|
||||||
|
|
||||||
|
export type FindingStatus = "ERROR" | "WARNING" | "INFO";
|
||||||
|
export type FindingClass = "RULE" | "RISK" | "REVIEW" | "INFO";
|
||||||
|
export type DatasheetProvenance = "MANDATORY" | "RECOMMENDED" | "TYPICAL" | "EXAMPLE";
|
||||||
|
export type EvidenceStatus = "SUFFICIENT" | "INSUFFICIENT";
|
||||||
|
export type FindingReviewState = "open" | "false_positive" | "accepted" | "wontfix";
|
||||||
|
|
||||||
|
export interface Finding {
|
||||||
|
finding_id: string | null;
|
||||||
|
designator: string;
|
||||||
|
mpn: string;
|
||||||
|
aspect: string | null;
|
||||||
|
finding: string;
|
||||||
|
why: string;
|
||||||
|
source_page: number | null;
|
||||||
|
source_quote?: string;
|
||||||
|
source_designator?: string | null;
|
||||||
|
status: FindingStatus;
|
||||||
|
recommendation?: string;
|
||||||
|
reference: string;
|
||||||
|
source?: string | null;
|
||||||
|
net?: string | null;
|
||||||
|
pins?: string[];
|
||||||
|
rule_id?: string | null;
|
||||||
|
cad_sheet?: string | null;
|
||||||
|
cad_uuid?: string | null;
|
||||||
|
variant?: string | null;
|
||||||
|
facts?: string;
|
||||||
|
requirement?: string;
|
||||||
|
inference?: string;
|
||||||
|
provenance?: DatasheetProvenance | null;
|
||||||
|
finding_class?: FindingClass | null;
|
||||||
|
confidence?: number | null;
|
||||||
|
evidence_status?: EvidenceStatus | null;
|
||||||
|
calculation?: string;
|
||||||
|
assumptions?: string[];
|
||||||
|
action?: string;
|
||||||
|
decision_id?: string | null;
|
||||||
|
suppressed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FindingComment {
|
||||||
|
comment_id: string;
|
||||||
|
finding_id: string;
|
||||||
|
user_id: string;
|
||||||
|
user_name: string;
|
||||||
|
text: string;
|
||||||
|
mentions: string[];
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FindingReview {
|
||||||
|
state: FindingReviewState;
|
||||||
|
reason: string;
|
||||||
|
user_id: string;
|
||||||
|
user_name: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportRelease {
|
||||||
|
sha256: string;
|
||||||
|
user_id: string;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationReport {
|
||||||
|
project: string;
|
||||||
|
timestamp: string;
|
||||||
|
findings: Finding[];
|
||||||
|
summary: Record<string, number>;
|
||||||
|
coverage: Record<string, string[]>;
|
||||||
|
review_errors?: Record<string, string>;
|
||||||
|
not_reviewed?: { designator: string; reason: string }[];
|
||||||
|
comments?: Record<string, FindingComment[]>;
|
||||||
|
review_states?: Record<string, FindingReview>;
|
||||||
|
release?: ReportRelease;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NetType = "power" | "ground" | "signal" | "unknown";
|
||||||
|
export type ComponentType =
|
||||||
|
| "resistor"
|
||||||
|
| "capacitor"
|
||||||
|
| "inductor"
|
||||||
|
| "ic"
|
||||||
|
| "connector"
|
||||||
|
| "crystal"
|
||||||
|
| "discrete"
|
||||||
|
| "transformer"
|
||||||
|
| "fuse"
|
||||||
|
| "switch"
|
||||||
|
| "test_point"
|
||||||
|
| "fiducial"
|
||||||
|
| "mechanical"
|
||||||
|
| "unknown";
|
||||||
|
|
||||||
|
export interface ComponentSpecs {
|
||||||
|
specs_type: string;
|
||||||
|
value_formatted?: string;
|
||||||
|
values?: Record<string, string | number | null>;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Component {
|
||||||
|
reference: string;
|
||||||
|
value: string;
|
||||||
|
footprint: string;
|
||||||
|
component_type: ComponentType;
|
||||||
|
component_subtype: string | null;
|
||||||
|
mpn: string | null;
|
||||||
|
pins: Record<string, string>;
|
||||||
|
specs: ComponentSpecs | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PinConnection {
|
||||||
|
component_ref: string;
|
||||||
|
pin_number: string;
|
||||||
|
pin_name: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Net {
|
||||||
|
name: string;
|
||||||
|
net_type: NetType;
|
||||||
|
voltage: number | null;
|
||||||
|
pins: PinConnection[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DesignGraph {
|
||||||
|
components: Record<string, Component>;
|
||||||
|
nets: Record<string, Net>;
|
||||||
|
bom_fields?: Record<string, { mpn?: string | null; value?: string; dnp?: boolean; variant?: string | null }>;
|
||||||
|
schematic_fields?: Record<string, { mpn?: string | null; value?: string; cad_uuid?: string; cad_sheet?: string }>;
|
||||||
|
cad_index?: Record<string, { uuid?: string; sheet?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BomSummaryRow {
|
||||||
|
mpn: string | null;
|
||||||
|
designators: string[];
|
||||||
|
value: string;
|
||||||
|
category: string | null;
|
||||||
|
specs: Record<string, string | number> | null;
|
||||||
|
description: string | null;
|
||||||
|
hasDatasheet: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProjectStatus =
|
||||||
|
| "draft"
|
||||||
|
| "queued"
|
||||||
|
| "running"
|
||||||
|
| "complete"
|
||||||
|
| "error"
|
||||||
|
| "cancelled"
|
||||||
|
| "paused_insufficient_credits"
|
||||||
|
| "paused_by_user";
|
||||||
|
|
||||||
|
export interface PauseCheckpoint {
|
||||||
|
paused_at?: string | null;
|
||||||
|
paused_stage?: string | null;
|
||||||
|
last_completed_label?: string | null;
|
||||||
|
completed_review_refs?: string[];
|
||||||
|
pending_review_refs?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkippedComponent {
|
||||||
|
identifier: string;
|
||||||
|
stage: string;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LcscPayload {
|
||||||
|
mpn?: string | null;
|
||||||
|
manufacturer?: string | null;
|
||||||
|
package?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
category?: string | null;
|
||||||
|
subcategory?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComponentMpnBuckets {
|
||||||
|
ic: string[];
|
||||||
|
passive: string[];
|
||||||
|
simple: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Project {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
created: string;
|
||||||
|
status: ProjectStatus;
|
||||||
|
summary?: Record<string, number>;
|
||||||
|
hasNetlist: boolean;
|
||||||
|
hasBom: boolean;
|
||||||
|
hasPcb?: boolean;
|
||||||
|
datasheetCount: number;
|
||||||
|
skippedComponents?: SkippedComponent[];
|
||||||
|
completedReviewRefs?: string[];
|
||||||
|
userId?: string;
|
||||||
|
collaborators?: string[];
|
||||||
|
creditsSpent?: number;
|
||||||
|
totalCostUsd?: number | null;
|
||||||
|
pauseCheckpoint?: PauseCheckpoint | null;
|
||||||
|
pauseReason?: string | null;
|
||||||
|
bomColumns?: { reference: string; mpn: string } | null;
|
||||||
|
pipelineError?: string | null;
|
||||||
|
lcscToMpn?: Record<string, string> | null;
|
||||||
|
lcscPayloads?: Record<string, LcscPayload> | null;
|
||||||
|
componentMpns?: ComponentMpnBuckets | null;
|
||||||
|
periscopeVersion?: string | null;
|
||||||
|
netlistFormat?: "pads" | "edif" | "kicad_xml" | "kicad_sexp" | "kicad_sch" | null;
|
||||||
|
netlistSubdesigns?: string[] | null;
|
||||||
|
placementStatus?: "draft" | "queued" | "running" | "complete" | "error" | "cancelled";
|
||||||
|
placementState?: Record<string, unknown> | null;
|
||||||
|
pcbStatus?: "draft" | "queued" | "running" | "complete" | "error" | "cancelled";
|
||||||
|
pcbState?: Record<string, unknown> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RoleHint =
|
||||||
|
| "decoupling"
|
||||||
|
| "bulk"
|
||||||
|
| "load_cap"
|
||||||
|
| "filter"
|
||||||
|
| "pullup"
|
||||||
|
| "series"
|
||||||
|
| "divider"
|
||||||
|
| "bridge"
|
||||||
|
| "crystal"
|
||||||
|
| "other";
|
||||||
|
|
||||||
|
export interface PlacementSatellite {
|
||||||
|
ref: string;
|
||||||
|
component_type?: string;
|
||||||
|
component_subtype?: string | null;
|
||||||
|
nets?: string[];
|
||||||
|
hop?: number;
|
||||||
|
role_hint?: RoleHint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlacementIcGroup {
|
||||||
|
ref: string;
|
||||||
|
mpn?: string | null;
|
||||||
|
component_subtype?: string | null;
|
||||||
|
rank?: number;
|
||||||
|
nets?: string[];
|
||||||
|
satellites: PlacementSatellite[];
|
||||||
|
layout_rules?: unknown[];
|
||||||
|
assemble_order?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlacementDomain {
|
||||||
|
domain_id: string;
|
||||||
|
power_nets: string[];
|
||||||
|
ic_refs: string[];
|
||||||
|
assemble_order: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlacementPlan {
|
||||||
|
objective?: string;
|
||||||
|
domains: PlacementDomain[];
|
||||||
|
groups: PlacementIcGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlacementProposal {
|
||||||
|
ref: string;
|
||||||
|
anchor_ref: string;
|
||||||
|
rule_kind: string;
|
||||||
|
max_distance_mm: number;
|
||||||
|
proposed_x: number;
|
||||||
|
proposed_y: number;
|
||||||
|
layer?: string;
|
||||||
|
basis?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlacementPack {
|
||||||
|
objective?: string;
|
||||||
|
status: "packed" | "skipped";
|
||||||
|
skip_reason?: string | null;
|
||||||
|
placements: PlacementProposal[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EdifSubDesign {
|
||||||
|
id: string | null;
|
||||||
|
instance_count: number;
|
||||||
|
designators: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Collaborator {
|
||||||
|
user_id: string;
|
||||||
|
name: string | null;
|
||||||
|
email: string | null;
|
||||||
|
image_url: string | null;
|
||||||
|
role: "owner" | "collaborator";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiLogEntry {
|
||||||
|
timestamp: string;
|
||||||
|
stage: string;
|
||||||
|
identifier: string;
|
||||||
|
model: string;
|
||||||
|
input_tokens: number;
|
||||||
|
output_tokens: number;
|
||||||
|
duration_ms: number;
|
||||||
|
stop_reason: string;
|
||||||
|
skill_id?: string | null;
|
||||||
|
turns?: number | null;
|
||||||
|
error?: string | null;
|
||||||
|
cost_usd?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeratingRow {
|
||||||
|
designator: string;
|
||||||
|
mpn: string | null;
|
||||||
|
value_formatted: string | null;
|
||||||
|
rated_voltage_v: number | null;
|
||||||
|
operating_voltage_v: number | null;
|
||||||
|
operating_voltage_source: string | null;
|
||||||
|
net_plus: string | null;
|
||||||
|
net_minus: string | null;
|
||||||
|
dielectric_category: "ceramic" | "tantalum" | "electrolytic" | null;
|
||||||
|
dielectric?: string | null;
|
||||||
|
c_nominal_f?: number | null;
|
||||||
|
dc_bias_factor?: number | null;
|
||||||
|
c_eff_f?: number | null;
|
||||||
|
c_eff_formatted?: string | null;
|
||||||
|
dc_bias_model?: "stima" | null;
|
||||||
|
stress?: "PASS" | "MARGIN" | "RISK" | "UNKNOWN";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeratingSettings {
|
||||||
|
ceramic: number;
|
||||||
|
tantalum: number;
|
||||||
|
electrolytic: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ImpedanceKind = "microstrip" | "stripline" | "cpw" | "diff";
|
||||||
|
|
||||||
|
export interface ImpedanceTraceResult {
|
||||||
|
kind: ImpedanceKind;
|
||||||
|
z0?: number | null;
|
||||||
|
zodd?: number | null;
|
||||||
|
zeven?: number | null;
|
||||||
|
zdiff?: number | null;
|
||||||
|
w_mm?: number | null;
|
||||||
|
s_mm?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImpedanceTarget {
|
||||||
|
kind: string;
|
||||||
|
w_mm: number | null;
|
||||||
|
s_mm: number | null;
|
||||||
|
z0: number | null;
|
||||||
|
zdiff: number | null;
|
||||||
|
formula: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImpedanceStackupResult {
|
||||||
|
targets: Record<string, ImpedanceTarget>;
|
||||||
|
kicad_dru: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImpedanceNetRow {
|
||||||
|
net_name: string;
|
||||||
|
length_mm?: number;
|
||||||
|
branch_count?: number;
|
||||||
|
is_differential?: boolean;
|
||||||
|
partner_net_name?: string | null;
|
||||||
|
topologies?: string[];
|
||||||
|
z0_min_ohms?: number | null;
|
||||||
|
z0_max_ohms?: number | null;
|
||||||
|
z0_avg_ohms?: number | null;
|
||||||
|
flags?: string[];
|
||||||
|
sample_count?: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImpedanceNetsReport {
|
||||||
|
pitch_mm: number;
|
||||||
|
nets: ImpedanceNetRow[];
|
||||||
|
skipped: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AntennaVerifyRow {
|
||||||
|
ic_ref: string;
|
||||||
|
pin: string;
|
||||||
|
net: string;
|
||||||
|
topology: string;
|
||||||
|
parts: string[];
|
||||||
|
target_z_ohm: number;
|
||||||
|
status: "ok" | "warning" | "info";
|
||||||
|
detail: string;
|
||||||
|
feed_z0?: number | null;
|
||||||
|
feed_length_mm?: number | null;
|
||||||
|
marker_ref?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AntennaGeometry {
|
||||||
|
template: "ifa" | "meander" | "stub";
|
||||||
|
fit: "ok" | "scaled" | "overflow" | "need_f0";
|
||||||
|
segments: { points: number[][]; width_mm: number }[];
|
||||||
|
total_length_mm?: number | null;
|
||||||
|
length_ideal_mm?: number | null;
|
||||||
|
scale?: number;
|
||||||
|
svg?: string | null;
|
||||||
|
kicad_mod?: string | null;
|
||||||
|
footprint_name?: string | null;
|
||||||
|
note?: string;
|
||||||
|
detail?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AntennaDesignRecipe {
|
||||||
|
status: "ready" | "need_pcb" | "need_stackup" | "need_marker";
|
||||||
|
feed_point?: Record<string, unknown> | null;
|
||||||
|
feed_line?: {
|
||||||
|
kind: string;
|
||||||
|
target_z_ohm: number;
|
||||||
|
w_mm?: number | null;
|
||||||
|
h_mm?: number | null;
|
||||||
|
er?: number | null;
|
||||||
|
t_mm?: number | null;
|
||||||
|
} | null;
|
||||||
|
radiator?: {
|
||||||
|
length_mm_suggest?: number | null;
|
||||||
|
f0_mhz?: number | null;
|
||||||
|
note?: string;
|
||||||
|
} | null;
|
||||||
|
geometry?: AntennaGeometry | null;
|
||||||
|
zone?: {
|
||||||
|
net: string;
|
||||||
|
layer: string;
|
||||||
|
bbox_mm?: number[] | null;
|
||||||
|
area_mm2?: number | null;
|
||||||
|
} | null;
|
||||||
|
keepout_checklist: string[];
|
||||||
|
detail: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AntennaReport {
|
||||||
|
verify: AntennaVerifyRow[];
|
||||||
|
design: AntennaDesignRecipe | null;
|
||||||
|
marker_help: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetlistPreviewDesignator {
|
||||||
|
ref: string;
|
||||||
|
pins: { number: string; net_name: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PipelineSubstep {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
status: "pending" | "running" | "complete";
|
||||||
|
cached?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PipelineStep {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
substeps: PipelineSubstep[];
|
||||||
|
status: "pending" | "running" | "complete";
|
||||||
|
totalNew?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreditSnapshot {
|
||||||
|
user_id: string;
|
||||||
|
balance: number;
|
||||||
|
plan: string;
|
||||||
|
last_entry_ts?: string | null;
|
||||||
|
next_expiry?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreditGrant {
|
||||||
|
grant_id: string;
|
||||||
|
user_id: string;
|
||||||
|
amount_usd: number;
|
||||||
|
remaining: number;
|
||||||
|
granted_at: string;
|
||||||
|
expires_at: string | null;
|
||||||
|
source:
|
||||||
|
| "top_up"
|
||||||
|
| "trial_grant"
|
||||||
|
| "admin_adjust"
|
||||||
|
| "refund_system_error"
|
||||||
|
| "pre_migration";
|
||||||
|
stripe_event_id?: string | null;
|
||||||
|
expired?: boolean;
|
||||||
|
note?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutoTopupConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
threshold_credits: number;
|
||||||
|
amount_usd: number;
|
||||||
|
has_payment_method: boolean;
|
||||||
|
last_attempt_ts?: string | null;
|
||||||
|
last_attempt_status?: "ok" | "failed" | "pending" | "";
|
||||||
|
last_failure_reason?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreditLedgerEntry {
|
||||||
|
user_id: string;
|
||||||
|
timestamp: string;
|
||||||
|
delta: number;
|
||||||
|
balance_after: number;
|
||||||
|
reason: string;
|
||||||
|
run_id?: string | null;
|
||||||
|
unit_id?: string | null;
|
||||||
|
stripe_event_id?: string | null;
|
||||||
|
note?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CostItem {
|
||||||
|
identifier: string;
|
||||||
|
kind: string;
|
||||||
|
api_cost_usd: number;
|
||||||
|
source: "cache_hit" | "api_call" | "api_call_estimated";
|
||||||
|
note?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CostEstimate {
|
||||||
|
api_cost_low: number;
|
||||||
|
api_cost_high: number;
|
||||||
|
api_cost_mid: number;
|
||||||
|
credits_low: number;
|
||||||
|
credits_high: number;
|
||||||
|
credits_mid: number;
|
||||||
|
breakdown: CostItem[];
|
||||||
|
ic_count: number;
|
||||||
|
simple_count: number;
|
||||||
|
passive_count: number;
|
||||||
|
cached_ic_count: number;
|
||||||
|
cached_simple_count: number;
|
||||||
|
cached_passive_count: number;
|
||||||
|
review_ic_count: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import type { Finding, FindingClass, FindingStatus } from "./types";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function groupBy<T>(items: T[], key: (item: T) => string): Record<string, T[]> {
|
||||||
|
const out: Record<string, T[]> = {};
|
||||||
|
for (const item of items) {
|
||||||
|
const k = key(item);
|
||||||
|
(out[k] ??= []).push(item);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BY_STATUS: Record<FindingStatus, number> = { ERROR: 0, WARNING: 1, INFO: 2 };
|
||||||
|
const BY_CLASS: Record<FindingClass, number> = { RULE: 0, RISK: 1, REVIEW: 2, INFO: 3 };
|
||||||
|
|
||||||
|
export function sortFindings(findings: Finding[]): Finding[] {
|
||||||
|
return [...findings].sort((a, b) => {
|
||||||
|
const status = BY_STATUS[a.status] - BY_STATUS[b.status];
|
||||||
|
if (status) return status;
|
||||||
|
const ac = BY_CLASS[a.finding_class ?? "INFO"] ?? 9;
|
||||||
|
const bc = BY_CLASS[b.finding_class ?? "INFO"] ?? 9;
|
||||||
|
if (ac !== bc) return ac - bc;
|
||||||
|
return (
|
||||||
|
(a.designator || "").localeCompare(b.designator || "") ||
|
||||||
|
(a.rule_id || "").localeCompare(b.rule_id || "") ||
|
||||||
|
(a.finding || "").localeCompare(b.finding || "")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFindingKey(finding: Finding, index: number): string {
|
||||||
|
return finding.finding_id ?? `${finding.designator}-idx-${index}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subtypeLabel(subtype: string | null): string {
|
||||||
|
if (!subtype) return "";
|
||||||
|
const last = subtype.split(".").pop() ?? subtype;
|
||||||
|
return last.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""Native frontend types and API client live under periscope/src."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SRC = ROOT / "periscope" / "src" / "frontend" / "src" / "lib"
|
||||||
|
DEP = ROOT / "periscope" / "dependency" / "frontend" / "src" / "lib"
|
||||||
|
|
||||||
|
|
||||||
|
def _exports(path: Path) -> set[str]:
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
names = set(re.findall(r"^export (?:async )?function (\w+)", text, re.M))
|
||||||
|
names |= set(re.findall(r"^export (?:interface|type|class|const) (\w+)", text, re.M))
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
def test_frontend_lib_is_src():
|
||||||
|
for name in ("api.ts", "types.ts", "utils.ts"):
|
||||||
|
path = SRC / name
|
||||||
|
assert path.is_file(), name
|
||||||
|
head = path.read_text(encoding="utf-8")[:400]
|
||||||
|
assert "Native Periscope overlay" not in head
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_keeps_page_exports():
|
||||||
|
src = _exports(SRC / "api.ts")
|
||||||
|
dep = _exports(DEP / "api.ts")
|
||||||
|
missing = dep - src
|
||||||
|
assert not missing, sorted(missing)
|
||||||
|
|
||||||
|
|
||||||
|
def test_types_keep_page_exports():
|
||||||
|
src = _exports(SRC / "types.ts")
|
||||||
|
dep = _exports(DEP / "types.ts")
|
||||||
|
missing = dep - src
|
||||||
|
assert not missing, sorted(missing)
|
||||||
Reference in New Issue
Block a user