Vertex Macro | Financial Cloud Cloud · Builder Articles
Kiro: Hands-On Lab — Build a Typed Factory Risk Portal from Scratch
Educational engineering purpose only. This is a software architecture exercise and not process-release advice.
Lab overview
In this lab, developers use Kiro to build a TypeScript React single-page application from an empty folder. The app visualizes factory machinery risk using deterministic local fixtures, typed domain logic, live simulated updates, search, sort, and SVG charts. Kiro is used as the single AI engineering service throughout the lab: steering, specs, chat, hooks, code generation, refactoring, and documentation.
Prerequisites
● Kiro installed and opened to an empty workspace.
● Node.js 20+ recommended.
● npm available.
● Familiarity with React and TypeScript.
Step 1 — Create the Vite TypeScript project
Run this in the terminal:
npm create vite@latest factory-risk-portal -- --template react-ts
cd factory-risk-portal
npm install
npm run dev
Expected result: a default React + TypeScript application runs locally in the browser.
Kiro prompt sample
Inspect this new React TypeScript workspace. Summarize the current structure and propose the minimal files we should add for a typed factory machinery risk portal. Do not write implementation code yet. Focus on architecture and type boundaries.
System design rationale
● Start with a known build system. Vite with React TypeScript gives the workshop a small, predictable baseline. This lets developers spend the session on Kiro workflows and engineering design instead of bundler configuration. A stable project scaffold also makes hooks easier because npm run build, tsc, and local dev commands have predictable names.
● Separate inspection from implementation. The first Kiro interaction asks for a structure proposal, not code. This is important because agentic coding quality improves when the assistant first reads the workspace and forms a plan. For factory engineering software, rushing from blank folder to UI tends to bury domain assumptions in JSX, which is exactly what causes typed React false-starts.
● Create a repeatable classroom baseline. Everyone begins with the same project template. That makes troubleshooting easier and ensures the Kiro behavior being taught relates to specs, steering, and implementation tasks rather than differences in local frameworks. It also mirrors field work: establish a stable control baseline before adjusting process parameters.
Step 2 — Add Kiro steering files
Create .kiro/steering/product.md:
# Product overview
Build a local-only factory machinery risk portal for professional engineers. The portal helps review machinery drift, sensor-health symptoms, and risk recommendations. The application is advisory-only and must not imply autonomous equipment control, machine state changes, or bypassing human approval.
Create .kiro/steering/tech.md:
# Technology stack
- React with TypeScript.
- Strict TypeScript; no `any`.
- Vite build system.
- CSS modules are not required; use plain CSS with clear class names.
- Inline SVG is allowed for sparklines and bar charts.
- No charting dependencies unless explicitly approved.
- No backend calls in the lab.
- Use local deterministic fixture data.
Create .kiro/steering/structure.md:
# Project structure
- `src/types.ts` contains shared domain types.
- `src/data/fixtures.ts` contains deterministic local data.
- `src/domain/risk.ts` contains pure risk and formatting functions.
- `src/components/` contains presentational React components.
- `src/App.tsx` owns top-level state and tab switching.
- Event handlers and props must be typed.
- Action labels, tab names, and sort keys must be union types.
Kiro prompt sample
Read the steering files and confirm the constraints you will follow. Then identify the first three implementation tasks for the factory risk portal. Do not create files yet.
System design rationale
● Steering converts tribal standards into persistent context. Field teams often have standards that live in senior engineers' heads: never imply autonomous control, do not use untyped state, keep risk logic reviewable, and avoid network assumptions in demos. Steering files make those standards available to Kiro on every interaction, reducing the need to repeat instructions.
● Type policy is placed before code generation. TypeScript failures are cheaper to prevent than to fix after a component tree exists. By requiring union types for actions, tabs, and sort keys upfront, Kiro is less likely to create permissive string props or unsafe indexing logic. This is the main control against React projects drifting into untyped code.
● Structure files create maintainability pressure. If Kiro knows that domain logic belongs in risk.ts, fixtures in fixtures.ts, and UI in components, it is less likely to produce a monolithic App.tsx. That matters for professional developers because maintainability is a system property, not a formatting preference.
Step 3 — Generate a Kiro spec
Ask Kiro to create a spec with requirements, design, and tasks.
Kiro prompt sample
Create a Kiro spec for a TypeScript React factory machinery risk portal.
The app must include:
- typed machinery fixtures
- top KPI strip
- tabs for Overview, Live Risk Board, Health Matrix, Dynamic Matching, Triage, and Runbook
- search by machine, layer, or symptom
- sorting by risk, blind-window age, matching gap, slope, fleet sigma, or residual
- expandable evidence panels
- SVG sparklines and a larger SVG trajectory chart
- simulated 5-second live updates
- advisory-only wording
Use strict TypeScript and include acceptance criteria that can be tested.
Expected result: Kiro creates or proposes a spec with a requirements section, design section, and task list.
System design rationale
● The spec is the engineering contract. A professional developer should not let an AI assistant infer control behavior from UI labels alone. The spec states which behavior is available, what is local-only, what must remain advisory, and which acceptance criteria prove correctness. This reduces ambiguity and provides a review artifact.
● Acceptance criteria control scope. Without acceptance criteria, Kiro may overbuild or underbuild. For example, “make a live dashboard” could become a backend architecture, a WebSocket client, or a static table. The acceptance criteria narrow the system to deterministic fixtures, explicit search, explicit sort keys, accessible controls, and simulated updates.
● The spec preserves traceability. Factory machinery applications often need review by software, equipment, process, and safety owners. A spec-driven workflow creates artifacts that show why a feature exists and what it is allowed to do. That is more reliable than a chat transcript containing scattered decisions.
Step 4 — Create the domain types
Create src/types.ts:
export type Recommendation =
| 'RELEASE'
| 'WATCH'
| 'RUN SPC'
| 'GOLDEN WAFER'
| 'ROUTE LIMIT'
| 'HOLD REVIEW'
| 'APC GUARD';
export type RiskTone = 'pass' | 'warn' | 'danger';
export type SortKey = 'risk' | 'blindHours' | 'matchingGapNm' | 'slope' | 'fleetSigma' | 'residual3SigmaNm';
export type TabKey =
| 'OVERVIEW'
| 'LIVE RISK BOARD'
| 'HEALTH MATRIX'
| 'DYNAMIC MATCHING'
| 'TRIAGE'
| 'RUNBOOK';
export type MachineRecord = {
id: string;
layer: string;
symptom: string;
risk: number;
blindHours: number;
matchingGapNm: number;
slope: number;
fleetSigma: number;
residual3SigmaNm: number;
recommendation: Recommendation;
deltaMeanSeries: number[];
};
export type HealthLink = {
name: string;
sensor: string;
metrologyImpact: string;
yieldRisk: string;
advisoryRule: string;
};
Business logic explanation
The domain types define the vocabulary of the risk portal. MachineRecord represents one machinery or metrology tool state. Recommendation is a closed set of review actions so the UI cannot accidentally present an unsupported command. SortKey protects sorting from arbitrary strings. RiskTone lets the UI show color plus text status. HealthLink maps sensor symptoms to engineering review logic.
Code logic explanation
Each type is exported from a central file so components and domain functions share the same contract. Union types are deliberately used for recommendations, tabs, and sorting. That allows TypeScript to detect invalid labels, unsafe sort keys, and misspelled tabs at build time.
Expected result
Kiro and the TypeScript compiler should now understand the core domain vocabulary. Later code can import these types instead of inventing implicit object shapes.
System design rationale
● Closed sets prevent unsafe language drift. In a factory context, a label can be interpreted as an operation. A free-form action string might accidentally become “STOP TOOL” or “AUTO ROUTE.” A union type restricts the application to advisory recommendations that are known, reviewed, and intentionally worded.
● Typed sort keys remove runtime guesswork. Sorting user interfaces often start with generic strings and dynamic object indexing. That is fragile. A SortKey union creates a contract between controls, data fields, and sort logic. If a field changes, TypeScript flags every affected area before the app reaches users.
● Domain-first coding guides Kiro. Kiro can generate better components once it sees the data contracts. This avoids the common false-start where an AI assistant generates attractive JSX with implicit shapes, then struggles to retrofit type declarations around already-written state and props.
Step 5 — Add deterministic fixtures
Create src/data/fixtures.ts:
import type { HealthLink, MachineRecord } from '../types';
export const initialMachines: MachineRecord[] = [
{
id: 'CDSEM-01',
layer: 'Gate ADI',
symptom: 'Stable master, production anchor',
risk: 18,
blindHours: 1.2,
matchingGapNm: 0.12,
slope: 1.0,
fleetSigma: 0.4,
residual3SigmaNm: 0.08,
recommendation: 'RELEASE',
deltaMeanSeries: [0.02, 0.01, 0.03, 0.01, 0.02, 0, 0.01, 0.02, 0.01, 0.02]
},
{
id: 'CDSEM-03',
layer: 'Fin dense CD',
symptom: 'Deflector DAC wobble, slope mismatch',
risk: 91,
blindHours: 7.4,
matchingGapNm: 0.23,
slope: 1.031,
fleetSigma: 3.2,
residual3SigmaNm: 0.16,
recommendation: 'HOLD REVIEW',
deltaMeanSeries: [0.02, 0.03, 0.06, 0.05, 0.08, 0.12, 0.16, 0.2, 0.22, 0.24]
}
];
export const healthLinks: HealthLink[] = [
{
name: 'Deflector DAC',
sensor: 'Deflector linearity / DAC stability',
metrologyImpact: 'Mandel Slope moves away from 1',
yieldRisk: 'Dense/isolated feature bias and process window narrowing',
advisoryRule: 'If slope <0.98 or >1.02, request engineering review before critical-layer routing.'
}
];
Business logic explanation
Fixtures represent a realistic but safe learning dataset. One record is stable and one is high risk. This lets developers immediately validate pass/warn/danger behavior, sorting, chart geometry, and recommendation display without connecting to factory systems.
Code logic explanation
The arrays are explicitly typed as MachineRecord[] and HealthLink[]. TypeScript now validates every field name, numeric metric, and recommendation label. If a developer writes HOLD instead of HOLD REVIEW, the compiler catches it.
Expected result
The app has deterministic data that can be rendered, sorted, searched, simulated, and tested.
System design rationale
● Fixtures are safer than live machinery data. The workshop is about software engineering with Kiro, not operational integration. Deterministic local data avoids credentials, network failures, sensitive production values, and accidental control implications. It also allows every participant to see the same expected result.
● Representative extremes support validation. Including both a low-risk stable machine and a high-risk drift case helps validate color thresholds, recommendation wording, expanded detail panels, and chart scaling. A dataset with only normal values would hide risk-path defects until later.
● Typed fixtures act as contract tests. The fixture file is not just mock data; it is a compile-time check that the domain model is usable. If the model is overcomplicated or missing fields, Kiro and TypeScript surface the mismatch immediately when the fixtures are written.
Step 6 — Add pure risk logic
Create src/domain/risk.ts:
import type { MachineRecord, RiskTone, SortKey } from '../types';
export function getRiskTone(risk: number): RiskTone {
if (risk >= 80) return 'danger';
if (risk >= 55) return 'warn';
return 'pass';
}
export function getStatusLabel(machine: MachineRecord): string {
if (machine.risk >= 80) return 'HOLD CANDIDATE';
if (machine.risk >= 55) return 'WATCH / REVIEW';
return 'NORMAL RELEASE';
}
export function filterMachines(machines: MachineRecord[], query: string): MachineRecord[] {
const q = query.trim().toLowerCase();
if (!q) return machines;
return machines.filter(machine =>
`${machine.id} ${machine.layer} ${machine.symptom}`.toLowerCase().includes(q)
);
}
export function sortMachines(machines: MachineRecord[], sortKey: SortKey): MachineRecord[] {
return [...machines].sort((a, b) => Math.abs(b[sortKey]) - Math.abs(a[sortKey]));
}
Business logic explanation
Risk thresholds are centralized. Green/pass is below 55, yellow/watch is 55-79, and red/danger is 80+. Search supports machine ID, process layer, or symptom. Sorting ranks the most severe values first for whichever metric the engineer selects.
Code logic explanation
The functions are pure: the same inputs always produce the same outputs. sortMachines copies the array before sorting to avoid mutating React state. SortKey ensures that dynamic indexing only uses valid numeric fields.
Expected result
Kiro can now build UI components on top of testable business logic. Developers can add unit tests later without rendering the whole application.
System design rationale
● Pure functions make AI-generated code reviewable. When business logic is mixed directly inside JSX, reviewers must mentally separate rendering, state, thresholds, and formatting. Pure functions isolate the decisions that matter: risk thresholds, filtering, sorting, and status language.
● Immutability protects React state. Sorting arrays in place is a common bug in dashboards. Copying before sorting avoids subtle UI issues and makes live update behavior easier to reason about. Kiro should be guided to preserve immutability whenever it transforms state.
● Centralized thresholds support governance. Risk thresholds are engineering policy. Keeping them in one domain file makes future review and change control easier. If a process owner changes the warning threshold, the implementation has one obvious location and fewer hidden UI dependencies.
Step 7 — Build the SVG sparkline component
Create src/components/Sparkline.tsx:
type SparklineProps = {
data: number[];
width?: number;
height?: number;
color?: string;
};
export function Sparkline({ data, width = 130, height = 38, color = '#35d5ff' }: SparklineProps) {
const min = Math.min(...data, 0);
const max = Math.max(...data, 0.24);
const points = data.map((value, index) => {
const x = (index / Math.max(1, data.length - 1)) * width;
const y = height - ((value - min) / (max - min || 1)) * (height - 6) - 3;
return `${x.toFixed(1)},${y.toFixed(1)}`;
}).join(' ');
return (
<svg className="spark" viewBox={`0 0 ${width} ${height}`} role="img" aria-label="DeltaMean sparkline">
<polyline points={points} fill="none" stroke={color} strokeWidth="2" />
<line x1="0" y1={height / 2} x2={width} y2={height / 2} stroke="#49657c" strokeDasharray="4 4" />
</svg>
);
}
Business logic explanation
The sparkline gives fast visual context for recent drift without adding a third-party chart package. Engineers can spot steady, rising, or noisy behavior directly in the risk table.
Code logic explanation
The component maps numeric values to SVG coordinates. It normalizes values between a minimum and maximum, builds a polyline string, and draws a center reference line. Props are explicitly typed and optional dimensions have defaults.
Expected result
Each machine row can display a compact trend line with accessible image metadata.
System design rationale
● Inline SVG keeps the dependency surface small. For the workshop, a charting library would add API learning overhead and version risk. SVG is native to the browser, typed well enough for React, and sufficient for sparklines, trajectory charts, and sigma bars.
● A chart component isolates coordinate math. Coordinate transformation is not UI decoration; it is logic. Putting it in a component with typed props keeps the table simpler and makes the rendering decision reusable across compact and expanded views.
● Accessibility remains explicit. The sparkline uses role="img" and an aria-label. This prevents visual-only status communication. In engineering tools, users should be able to interpret the status through text and labels even if color or visual detail is unavailable.
Step 8 — Ask Kiro to implement the app shell
Kiro prompt sample
Implement the app shell using the existing types, fixtures, risk functions, and Sparkline component.
Requirements:
- top header with title and live local clock
- KPI strip
- tab navigation controlled by typed TabKey state
- Live Risk Board with search, sort buttons, and expandable details
- simulated live updates every 5 seconds
- advisory-only footer
Keep App.tsx readable. If it becomes too large, propose component extraction before writing more code.
Expected result: Kiro creates a working top-level app with typed state and renders the core board.
System design rationale
● Kiro receives constraints and assets together. At this point, the domain model, fixtures, pure functions, and sparkline component exist. The prompt tells Kiro to compose existing pieces, not invent new ones. That dramatically improves type consistency and reduces duplicate logic.
● State ownership is intentional. The top-level app owns active tab, query, sort key, expanded row, live clock, and machine records. This is appropriate for a small single-page lab. If the app grows, Kiro should extract state or components rather than bury everything in deeply nested children.
● Simulated updates model real telemetry safely. Field software often needs to demonstrate time-varying behavior. A deterministic local update loop is safer than connecting to equipment. It validates re-rendering, chart updates, and risk changes while preserving the advisory-only boundary.
Step 9 — Add hooks for type discipline
Create .kiro/hooks/typecheck-on-save.json:
{
"version": "v1",
"hooks": [
{
"name": "typecheck-on-save",
"description": "Run TypeScript build checks when TS or TSX files are saved.",
"trigger": "PostFileSave",
"matcher": "\\.(ts|tsx)$",
"action": {
"type": "command",
"command": "npm run build"
},
"timeout": 60,
"enabled": true
}
]
}
Create .kiro/hooks/review-react-change.json:
{
"version": "v1",
"hooks": [
{
"name": "review-react-change",
"description": "Ask Kiro to review React changes for typing, state mutation, accessibility, and advisory-only wording.",
"trigger": "PostFileSave",
"matcher": "src/.*\\.(ts|tsx)$",
"action": {
"type": "agent",
"prompt": "Review the changed TypeScript React file. Check for any, implicit object shapes, unsafe state mutation, untyped props, inaccessible controls, color-only status, and wording that implies autonomous machinery control. Suggest minimal fixes."
},
"timeout": 60,
"enabled": true
}
]
}
Business logic explanation
The hooks act like an engineering quality gate. They do not control machinery and they do not approve production behavior. They help developers catch unsafe implementation patterns early: type drift, mutation, UI-only status, and language that could be misread as an operational command.
Code logic explanation
The first hook runs a shell command after TypeScript or TSX files are saved. The second hook invokes Kiro’s agent review after TypeScript React source changes. The matcher limits automation to relevant files.
Expected result
Saving a TypeScript file triggers checks or review automation, depending on the Kiro hook configuration in the local environment.
System design rationale
● Hooks convert standards into workflow. Steering tells Kiro what standards exist; hooks make those standards active during development. This is critical when developers iterate quickly and may not manually ask for reviews every time.
● Type checking is automated at the point of change. The moment a TS or TSX file is saved, the build can catch invalid labels, missing props, and broken imports. That reduces the chance of discovering type errors only at the end of the lab.
● Agent review catches semantic issues. TypeScript cannot detect whether a button label implies autonomous equipment control. An agent hook can review language, accessibility, and engineering intent. This complements compiler checks instead of replacing them.
Step 10 — Final hardening prompt
Kiro prompt sample
Perform a final technical review of the project.
Check:
- strict typing and no `any`
- no mutation of React state arrays
- all controls keyboard accessible
- search and sorting use typed contracts
- risk status includes text, not only color
- app wording remains advisory-only
- README explains how to run the app and states that fixture data must be replaced with validated data before operational use
Return a prioritized fix list, then implement only the safe low-risk fixes.
System design rationale
● Final review separates detection from action. Asking Kiro for a prioritized list before implementation avoids unreviewed sweeping changes. This is important in engineering environments where a broad refactor can introduce regressions late in a workshop or sprint.
● Safety language is reviewed as part of technical quality. In factory applications, wording is part of the system. “Hold candidate” and “recommend route limit” are different from “hold tool” or “reroute lot.” The review makes sure the software remains within advisory scope.
● Low-risk fixes preserve workshop completion. The last minutes should stabilize the project, not open architectural churn. Kiro is useful for identifying many improvements, but professional developers must decide what to implement now versus backlog.
Completion checklist
● [ ] Project runs locally.
● [ ] Kiro steering files exist.
● [ ] Kiro spec exists or has been reviewed.
● [ ] Domain types exist before UI logic.
● [ ] No any in source files.
● [ ] Search works for machine, layer, and symptom.
● [ ] Sorting works for all configured keys.
● [ ] Expand/collapse evidence panel works.
● [ ] Live update loop changes risk and trend values.
● [ ] SVG charts render without chart libraries.
● [ ] Advisory-only disclaimer is visible.
● [ ] Hooks exist for type checks and AI review.
lab section: create complete fixture coverage
The short fixture in the main lab is intentionally small for teaching. For a richer demo, replace it with a broader set of records that covers low, medium, and high risk. This gives Kiro and participants more cases for sorting, filtering, display color, and expanded evidence.
import type { HealthLink, MachineRecord } from '../types';
export const initialMachines: MachineRecord[] = [
{
id: 'CDSEM-01',
layer: 'Gate ADI',
symptom: 'Stable master, production anchor',
risk: 18,
blindHours: 1.2,
matchingGapNm: 0.12,
slope: 1.0,
fleetSigma: 0.4,
residual3SigmaNm: 0.08,
recommendation: 'RELEASE',
deltaMeanSeries: [0.02, 0.01, 0.03, 0.01, 0.02, 0.0, 0.01, 0.02, 0.01, 0.02]
},
{
id: 'CDSEM-02',
layer: 'Gate AEI',
symptom: 'Emission current ramp, small mean offset',
risk: 64,
blindHours: 5.8,
matchingGapNm: 0.18,
slope: 1.012,
fleetSigma: 1.8,
residual3SigmaNm: 0.14,
recommendation: 'WATCH',
deltaMeanSeries: [0.02, 0.04, 0.05, 0.08, 0.10, 0.12, 0.14, 0.16, 0.17, 0.18]
},
{
id: 'CDSEM-03',
layer: 'Fin dense CD',
symptom: 'Deflector DAC wobble, slope mismatch',
risk: 91,
blindHours: 7.4,
matchingGapNm: 0.23,
slope: 1.031,
fleetSigma: 3.2,
residual3SigmaNm: 0.16,
recommendation: 'HOLD REVIEW',
deltaMeanSeries: [0.02, 0.03, 0.06, 0.05, 0.08, 0.12, 0.16, 0.20, 0.22, 0.24]
},
{
id: 'CDSEM-05',
layer: 'Contact/Via',
symptom: 'Vacuum slow degradation, residual rising',
risk: 76,
blindHours: 6.9,
matchingGapNm: 0.19,
slope: 1.006,
fleetSigma: 2.4,
residual3SigmaNm: 0.21,
recommendation: 'GOLDEN WAFER',
deltaMeanSeries: [0.02, 0.01, 0.04, 0.06, 0.08, 0.10, 0.12, 0.13, 0.16, 0.18]
},
{
id: 'CDSEM-10',
layer: 'Risk ramp product',
symptom: 'Yield-loss watch lots routed here',
risk: 82,
blindHours: 6.2,
matchingGapNm: 0.21,
slope: 0.976,
fleetSigma: 2.9,
residual3SigmaNm: 0.17,
recommendation: 'APC GUARD',
deltaMeanSeries: [0.02, 0.04, 0.07, 0.09, 0.11, 0.13, 0.15, 0.18, 0.20, 0.23]
}
];
export const healthLinks: HealthLink[] = [
{
name: 'Gun Vacuum',
sensor: 'Gun chamber vacuum / Torr',
metrologyImpact: 'Residual 3 sigma and TMP rise',
yieldRisk: 'False alarms and random CD noise',
advisoryRule: 'If vacuum degrades while residual 3 sigma rises, recommend golden-wafer review.'
},
{
name: 'Emission Current',
sensor: 'Extraction voltage / emission current',
metrologyImpact: 'Mean offset step jump',
yieldRisk: 'APC receives biased CD and may correct in the wrong direction',
advisoryRule: 'If emission slope changes for three lots, recommend virtual OOC review before next routine SPC.'
},
{
name: 'Deflector DAC',
sensor: 'Deflector linearity / DAC stability',
metrologyImpact: 'Mandel Slope moves away from 1',
yieldRisk: 'Dense/isolated feature bias and process-window narrowing',
advisoryRule: 'If slope <0.98 or >1.02, request engineering review before critical-layer routing.'
},
{
name: 'Stage Vibration',
sensor: 'Stage vibration / facility vibration mG',
metrologyImpact: 'Site-to-site Delta Max-Min expands',
yieldRisk: 'False local variation signatures can hide real yield patterns',
advisoryRule: 'If residual and site range rise after a vibration burst, recommend routing review for tight CD layers.'
}
];
Business logic explanation
This expanded fixture set includes multiple risk profiles: stable release, watch, high fleet deviation, residual rise, slope drift, and yield-watch exposure. That variety is important because engineering dashboards must make abnormal conditions obvious without losing normal baseline context.
Code logic explanation
The fields match MachineRecord exactly. The fixture names use engineering-friendly labels while preserving type-safe property names. Each series has the same length so SVG scaling and live update logic remain predictable.
Expected result
The risk board can demonstrate all major user paths: search for a tool, sort by different metrics, expand a high-risk row, inspect the trend chart, and confirm advisory recommendation language.
System design rationale
● Coverage prevents misleading demos. A dashboard with only one or two rows can appear correct even when sorting, threshold coloring, and row expansion contain defects. A broader fixture set creates enough variation for participants to verify behavior across multiple paths.
● Normal cases remain visible. Field engineering tools should not show only alarms. Including stable baseline tools helps users compare drift candidates against a healthy anchor. That context is valuable for both visual interpretation and software validation.
● Fixture consistency protects chart logic. Keeping series length consistent avoids distracting edge cases during the workshop. The facilitator can later introduce missing or uneven data as an advanced extension after the core typed design is understood.
Expanded lab section: implement top-level state in App.tsx
import { useEffect, useMemo, useState } from 'react';
import { initialMachines } from './data/fixtures';
import { filterMachines, sortMachines } from './domain/risk';
import type { MachineRecord, SortKey, TabKey } from './types';
const tabs: TabKey[] = ['OVERVIEW', 'LIVE RISK BOARD', 'HEALTH MATRIX', 'DYNAMIC MATCHING', 'TRIAGE', 'RUNBOOK'];
export function App() {
const [activeTab, setActiveTab] = useState<TabKey>('OVERVIEW');
const [query, setQuery] = useState('');
const [sortKey, setSortKey] = useState<SortKey>('risk');
const [expandedId, setExpandedId] = useState<string | null>(null);
const [machines, setMachines] = useState<MachineRecord[]>(initialMachines);
const visibleMachines = useMemo(() => {
return sortMachines(filterMachines(machines, query), sortKey);
}, [machines, query, sortKey]);
useEffect(() => {
let tick = 0;
const timer = window.setInterval(() => {
tick += 1;
setMachines(previous => previous.map((machine, index) => {
const wave = Math.sin((tick + index) * 0.7);
const risk = Math.max(5, Math.min(99, machine.risk + wave * 1.7));
const blindHours = Math.max(0.2, machine.blindHours + 0.05 + wave * 0.03);
const latest = machine.deltaMeanSeries[machine.deltaMeanSeries.length - 1] ?? 0;
const nextPoint = Math.max(0, Math.min(0.28, latest + wave * 0.006));
return {
...machine,
risk,
blindHours,
deltaMeanSeries: [...machine.deltaMeanSeries.slice(1), Number(nextPoint.toFixed(3))]
};
}));
}, 5000);
return () => window.clearInterval(timer);
}, []);
return (
<div className="app-shell">
<nav className="tabs" aria-label="Portal panels">
{tabs.map(tab => (
<button key={tab} className={activeTab === tab ? 'active' : ''} onClick={() => setActiveTab(tab)}>
{tab}
</button>
))}
</nav>
<pre>{JSON.stringify({ activeTab, query, sortKey, expandedId, visibleCount: visibleMachines.length }, null, 2)}</pre>
</div>
);
}
Business logic explanation
Top-level state captures the primary dashboard interactions: active panel, text search, selected sorting metric, expanded evidence row, and current machine records. Simulated updates make the portal feel live while remaining disconnected from real systems.
Code logic explanation
visibleMachines is derived with useMemo from machines, query, and sortKey. The update loop creates new machine objects instead of mutating the previous state. The tab list is typed as TabKey[], which prevents unsupported panels.
Expected result
The app should display changing derived state every five seconds. This is a checkpoint before building the full UI.
System design rationale
● State is explicit and minimal. The app stores only values that users control or that change over time. Filtered and sorted data is derived. This prevents duplicated state, which is a common source of dashboard bugs.
● Derived views are memoized. Search and sorting can be recalculated whenever inputs change. useMemo keeps that logic visible and prevents unnecessary recomputation as the app grows.
● The update loop is bounded and reversible. Simulated data changes are small, clamped, and local. The design demonstrates live behavior without pretending to be a production telemetry pipeline.
Expanded lab section: build a typed metric card
Create src/components/MetricCard.tsx:
type MetricCardProps = {
label: string;
value: string | number;
detail: string;
tone?: 'neutral' | 'pass' | 'warn' | 'danger';
};
export function MetricCard({ label, value, detail, tone = 'neutral' }: MetricCardProps) {
return (
<article className={`metric-card ${tone}`}>
<span>{label}</span>
<strong>{value}</strong>
<em>{detail}</em>
</article>
);
}
Business logic explanation
Metric cards summarize the operational state before users inspect detail rows. Examples include tools watched, blind-window average, fleet out-of-control count, and yield-watch lots.
Code logic explanation
The component accepts a small typed prop set. tone is optional and constrained to known display styles. The component does not calculate metrics; it only renders them.
Expected result
KPI cards can be reused in the header or summary rows without duplicating markup.
System design rationale
● Presentational components should stay simple. A metric card should render a metric, not decide how fleet risk is calculated. Keeping it presentation-only reduces hidden logic and makes Kiro-generated UI easier to review.
● Typed visual tone prevents CSS drift. Instead of arbitrary class strings, the component supports a closed set of tones. This mirrors the risk-tone pattern and makes future styling safer.
● Reusable layout reduces duplication. Dashboards often repeat cards across panels. A small typed component lets Kiro build consistent UI faster without creating inconsistent markup each time.
Expanded lab section: create the risk board component
Create src/components/RiskBoard.tsx:
import { getRiskTone, getStatusLabel } from '../domain/risk';
import type { MachineRecord, SortKey } from '../types';
import { Sparkline } from './Sparkline';
type RiskBoardProps = {
machines: MachineRecord[];
query: string;
sortKey: SortKey;
expandedId: string | null;
onQueryChange: (query: string) => void;
onSortChange: (key: SortKey) => void;
onExpandedChange: (id: string | null) => void;
};
const sortOptions: Array<[SortKey, string]> = [
['risk', 'Risk'],
['blindHours', 'Blind Window'],
['matchingGapNm', 'TMG'],
['slope', 'Slope'],
['fleetSigma', 'Fleet sigma'],
['residual3SigmaNm', 'Residual']
];
export function RiskBoard({ machines, query, sortKey, expandedId, onQueryChange, onSortChange, onExpandedChange }: RiskBoardProps) {
return (
<section className="risk-board">
<div className="toolbar">
<input
value={query}
onChange={event => onQueryChange(event.target.value)}
placeholder="Search by machine, layer, or symptom"
aria-label="Search by machine, layer, or symptom"
/>
<div className="sorts">
{sortOptions.map(([key, label]) => (
<button key={key} className={sortKey === key ? 'active' : ''} onClick={() => onSortChange(key)}>
Sort: {label}
</button>
))}
</div>
</div>
{machines.map(machine => {
const tone = getRiskTone(machine.risk);
const isExpanded = expandedId === machine.id;
return (
<article className="machine-row" key={machine.id}>
<div className="row-main">
<strong>{machine.id}</strong>
<span>{machine.layer}</span>
<span className={`risk ${tone}`}>{Math.round(machine.risk)} {tone.toUpperCase()}</span>
<Sparkline data={machine.deltaMeanSeries} />
<button onClick={() => onExpandedChange(isExpanded ? null : machine.id)} aria-expanded={isExpanded}>
{isExpanded ? 'Hide evidence' : machine.recommendation}
</button>
</div>
{isExpanded && (
<div className="evidence-panel">
<p><b>Status:</b> {getStatusLabel(machine)}</p>
<p><b>Evidence:</b> {machine.symptom}</p>
<p><b>Blind Window:</b> {machine.blindHours.toFixed(1)}h</p>
<p><b>Fleet Sigma:</b> {machine.fleetSigma.toFixed(1)}σ</p>
</div>
)}
</article>
);
})}
</section>
);
}
Business logic explanation
The Risk Board is where engineers search, rank, and inspect machine evidence. It shows risk status text, layer context, trend evidence, and advisory recommendations.
Code logic explanation
The component receives all state from its parent and emits changes through typed callbacks. It does not own the machine list. This makes the component easier to test and keeps state ownership in App.tsx.
Expected result
Users can search, sort, and expand rows. Each row shows a machine ID, layer, risk text, sparkline, and recommendation button.
System design rationale
● Controlled inputs preserve state traceability. Search and sort state live in the parent, so the current board state can be logged, tested, or passed to other panels later. This is more maintainable than burying local state inside a large table component.
● Callbacks are typed contracts. onSortChange accepts only SortKey, so the component cannot emit an unsupported field. This is another guard against Kiro creating stringly typed UI controls.
● Evidence is exposed on demand. Expanded panels keep the default board dense while still making the reasoning visible. Factory engineers need both fast triage and drill-down evidence.
Expanded lab section: add a larger SVG trajectory chart
Create src/components/TrajectoryChart.tsx:
type TrajectoryChartProps = {
data: number[];
advisoryLimit?: number;
};
export function TrajectoryChart({ data, advisoryLimit = 0.18 }: TrajectoryChartProps) {
const width = 620;
const height = 190;
const pad = 24;
const min = Math.min(...data, -0.02);
const max = Math.max(...data, 0.26, advisoryLimit);
const xFor = (index: number) => pad + (index / Math.max(1, data.length - 1)) * (width - pad * 2);
const yFor = (value: number) => height - pad - ((value - min) / (max - min || 1)) * (height - pad * 2);
const points = data.map((value, index) => `${xFor(index)},${yFor(value)}`).join(' ');
return (
<svg className="trajectory-chart" viewBox={`0 0 ${width} ${height}`} role="img" aria-label="DeltaMean trajectory chart">
<line x1={pad} x2={width - pad} y1={yFor(advisoryLimit)} y2={yFor(advisoryLimit)} stroke="#ff5b6e" strokeDasharray="6 6" />
<polyline points={points} fill="none" stroke="#35d5ff" strokeWidth="4" strokeLinecap="round" strokeLinejoin="round" />
{data.map((value, index) => (
<circle key={index} cx={xFor(index)} cy={yFor(value)} r="4" fill={value > advisoryLimit ? '#ff5b6e' : '#35d5ff'} />
))}
</svg>
);
}
Business logic explanation
The expanded chart shows whether recent DeltaMean values are approaching or crossing an advisory threshold. It supports engineering review by making trend direction and threshold crossings visible.
Code logic explanation
The component converts a numeric series into SVG coordinates. The advisory limit is a prop with a default value, which makes the chart reusable for different layers or process windows later.
Expected result
The evidence panel can show a larger trajectory chart with a dashed advisory limit line and colored points.
System design rationale
● Thresholds are visible in context. A number alone does not show trend velocity. A chart with an advisory line helps engineers distinguish a stable high value from a fast-rising risk signal.
● The chart remains deterministic. No external rendering library or asynchronous data source is required. The math is transparent and easy for participants to inspect.
● The limit is configurable but bounded. Making advisoryLimit a typed prop avoids hard-coding every threshold while still preventing hidden global behavior.
Expanded lab section: add unit-testable update logic
Move the update function into src/domain/risk.ts:
export function simulateMachineUpdate(machine: MachineRecord, tick: number, index: number): MachineRecord {
const wave = Math.sin((tick + index) * 0.7);
const risk = Math.max(5, Math.min(99, machine.risk + wave * 1.7));
const blindHours = Math.max(0.2, machine.blindHours + 0.05 + wave * 0.03);
const latest = machine.deltaMeanSeries[machine.deltaMeanSeries.length - 1] ?? 0;
const nextPoint = Math.max(0, Math.min(0.28, latest + wave * 0.006));
return {
...machine,
risk,
blindHours,
deltaMeanSeries: [...machine.deltaMeanSeries.slice(1), Number(nextPoint.toFixed(3))]
};
}
Then simplify the effect:
useEffect(() => {
let tick = 0;
const timer = window.setInterval(() => {
tick += 1;
setMachines(previous => previous.map((machine, index) => simulateMachineUpdate(machine, tick, index)));
}, 5000);
return () => window.clearInterval(timer);
}, []);
Business logic explanation
The update function simulates minor risk and trend changes to keep the dashboard active. It does not represent actual equipment telemetry.
Code logic explanation
Moving the function out of App.tsx makes it testable. The React effect becomes responsible only for scheduling updates.
Expected result
Behavior remains the same, but the code is easier to validate and maintain.
System design rationale
● Scheduling and calculation are separate concerns. React effects should manage time and lifecycle. Domain functions should calculate new values. This separation makes both parts easier to reason about.
● Pure simulation supports testing. A pure function can be tested with known inputs. That is useful because live update bugs often appear as gradual drift or state mutation, which are hard to diagnose in UI-only tests.
● The model remains honest. Naming the function simulateMachineUpdate makes clear that values are synthetic. This avoids confusing demo behavior with validated factory data.
Expanded lab section: README content
Ask Kiro to generate a README with this prompt:
Create a README for this local-only TypeScript React factory risk portal. Include install commands, run commands, architecture summary, Kiro steering and hook notes, advisory-only disclaimer, and a warning that fixture data must be replaced with validated engineering data before operational use.
Minimum README content:
# Factory Risk Portal
Local-only TypeScript React workshop project built with Kiro.
## Run
npm install
npm run dev
## Build
npm run build
## Safety boundary
This application is advisory-only. It does not command equipment, change route state, write to dispatch systems, modify APC feedback, or bypass human approval. Fixture data is for learning only and must be replaced with validated engineering data before operational use.
System design rationale
● Documentation is part of the deliverable. Engineering software should explain how to run it and what it does not do. This is especially important when the UI resembles a factory control dashboard.
● The safety boundary travels with the code. A README can be read by future developers who did not attend the workshop. It preserves the advisory-only constraint outside the live Kiro session.
● Kiro can generate useful maintenance artifacts. Documentation generation is a practical use of Kiro after implementation, but it should be based on the actual project structure and constraints rather than generic boilerplate.
Debugging guide for participants
Problem: TypeScript says sort key cannot index machine record
Likely cause: SortKey includes a field that is not numeric or does not exist on MachineRecord.
Fix prompt:
Inspect SortKey and MachineRecord. Make the sort key union include only numeric MachineRecord fields. Update sort buttons to match. Do not use type assertions to hide the problem.
Problem: Risk colors render but screen readers have no status
Likely cause: status is communicated only by CSS.
Fix prompt:
Update risk display so it includes visible text and an aria-label with the numeric risk and risk tone. Keep the existing colors.
Problem: Kiro generated a new action label
Likely cause: the model treated recommendations as ordinary strings.
Fix prompt:
The recommendation field must use the existing Recommendation union. Replace unsupported labels with the closest existing advisory recommendation or propose a union update before changing fixtures.
Problem: The final code is all in one file
Likely cause: the implementation prompt did not constrain structure enough.
Fix prompt:
Refactor the existing behavior into the agreed structure. Preserve runtime behavior. Move types, fixtures, pure domain functions, and reusable components into separate files. Explain each movement before editing.
Additional lab section — Build fuller fixture coverage
The earlier lab uses a minimal fixture to teach the concept quickly. For a richer workshop demo, expand src/data/fixtures.ts with additional records. This gives sorting, searching, color thresholds, charting, and KPI calculations more realistic behavior.
export const additionalMachines: MachineRecord[] = [
{
id: 'CDSEM-05',
layer: 'Contact/Via',
symptom: 'Vacuum slow degradation, residual rising',
risk: 76,
blindHours: 6.9,
matchingGapNm: 0.19,
slope: 1.006,
fleetSigma: 2.4,
residual3SigmaNm: 0.21,
recommendation: 'GOLDEN WAFER',
deltaMeanSeries: [0.02, 0.01, 0.04, 0.06, 0.08, 0.10, 0.12, 0.13, 0.16, 0.18]
},
{
id: 'CDSEM-06',
layer: 'Overlay support',
symptom: 'Vibration burst after facility event',
risk: 72,
blindHours: 4.3,
matchingGapNm: 0.17,
slope: 0.989,
fleetSigma: 2.2,
residual3SigmaNm: 0.19,
recommendation: 'ROUTE LIMIT',
deltaMeanSeries: [0.01, 0.00, 0.03, 0.02, 0.12, 0.03, 0.13, 0.05, 0.15, 0.06]
},
{
id: 'CDSEM-10',
layer: 'Risk ramp product',
symptom: 'Yield-loss watch lots routed here',
risk: 82,
blindHours: 6.2,
matchingGapNm: 0.21,
slope: 0.976,
fleetSigma: 2.9,
residual3SigmaNm: 0.17,
recommendation: 'APC GUARD',
deltaMeanSeries: [0.02, 0.04, 0.07, 0.09, 0.11, 0.13, 0.15, 0.18, 0.20, 0.23]
}
];
Business logic explanation
The expanded records introduce three different risk patterns: residual-driven review, vibration-related route advisory, and APC guard evidence. This gives developers examples of different machine symptoms without connecting to real telemetry.
Code logic explanation
The records use the same MachineRecord type. They can be merged into initialMachines with the spread operator. TypeScript validates every recommendation and metric field.
Expected result
The Live Risk Board becomes more useful for demonstration. Sorting by risk, fleet sigma, residual, or blind-window age now changes the order in visible ways.
System design rationale
● A richer dataset exposes UI edge cases. With only two records, developers cannot fully validate sort behavior, row wrapping, warning colors, or multiple recommendation labels. Additional records create realistic layout pressure while still keeping the lab safe and deterministic.
● Different symptoms test domain language. Vacuum, vibration, and APC guard examples force the UI to show evidence text, not just a risk number. This is important because engineering reviewers need the reason behind a recommendation before acting on it.
● Typed fixture expansion proves scalability. Adding records should not require component changes. If the app breaks when new records are added, the design is too tightly coupled. This teaches developers that data-driven rendering is a system design goal.
Additional lab section — Add KPI calculations as pure functions
Create src/domain/kpi.ts:
import type { MachineRecord } from '../types';
export type KpiSummary = {
toolsWatched: number;
averageBlindHours: number;
fleetOocRiskCount: number;
yieldWatchCount: number;
fdcHealthLinks: number;
dynamicLimitsEnabled: boolean;
};
export function calculateKpis(machines: MachineRecord[], healthLinkCount: number): KpiSummary {
const averageBlindHours = machines.reduce((sum, machine) => sum + machine.blindHours, 0) / machines.length;
const fleetOocRiskCount = machines.filter(machine => machine.fleetSigma > 2).length;
const yieldWatchCount = machines.filter(machine =>
machine.recommendation === 'HOLD REVIEW' ||
machine.recommendation === 'GOLDEN WAFER' ||
machine.recommendation === 'APC GUARD'
).length;
return {
toolsWatched: machines.length,
averageBlindHours,
fleetOocRiskCount,
yieldWatchCount,
fdcHealthLinks: healthLinkCount,
dynamicLimitsEnabled: true
};
}
Business logic explanation
KPI cards summarize operational review pressure: how many tools are watched, average blind-window age, fleet out-of-control exposure, health-link coverage, dynamic limit status, and yield-watch load.
Code logic explanation
The function accepts machine records and a health-link count, then returns a typed KpiSummary. It centralizes calculations outside React rendering so the same values can be tested and reused.
Expected result
App.tsx can calculate KPI values with one pure function call and pass the summary to a KpiStrip component.
System design rationale
● KPI calculation belongs outside JSX. Dashboards often bury KPI math inside render blocks. That makes review and testing harder. A pure KPI function makes the calculation visible and reusable.
● The summary type documents business meaning. KpiSummary gives names to the values shown in the UI. This is easier to review than an array of anonymous strings and numbers.
● Kpi logic supports future replacement of fixture data. When live validated data is introduced later, the KPI function can remain stable. Only the data source changes.
Additional lab section — Add a trajectory chart component
Create src/components/TrajectoryChart.tsx:
type TrajectoryChartProps = {
data: number[];
threshold?: number;
};
export function TrajectoryChart({ data, threshold = 0.18 }: TrajectoryChartProps) {
const width = 620;
const height = 188;
const pad = 22;
const min = Math.min(...data, -0.02);
const max = Math.max(...data, 0.26);
const xFor = (index: number) => pad + (index / Math.max(1, data.length - 1)) * (width - pad * 2);
const yFor = (value: number) => height - pad - ((value - min) / (max - min || 1)) * (height - pad * 2);
const points = data.map((value, index) => `${xFor(index)},${yFor(value)}`).join(' ');
return (
<svg className="big-chart" viewBox={`0 0 ${width} ${height}`} role="img" aria-label="DeltaMean trajectory chart">
<line x1={pad} x2={width - pad} y1={yFor(threshold)} y2={yFor(threshold)} stroke="#ff5b6e" strokeDasharray="6 6" />
<polyline points={points} fill="none" stroke="#35d5ff" strokeWidth="4" strokeLinejoin="round" strokeLinecap="round" />
{data.map((value, index) => (
<circle key={index} cx={xFor(index)} cy={yFor(value)} r="4" fill={value > threshold ? '#ff5b6e' : '#35d5ff'} />
))}
</svg>
);
}
Business logic explanation
The trajectory chart shows whether recent drift is approaching or crossing a review threshold. It gives an expanded evidence view for a selected machine row.
Code logic explanation
The component converts data points into SVG coordinates, draws a threshold line, plots the series, and colors points above threshold differently while still relying on text metrics elsewhere for status.
Expected result
Expanded rows can show a larger chart that supports engineering review.
System design rationale
● Expanded evidence separates summary from detail. A table row should stay compact. Detailed drift evidence belongs in an expandable panel where the user intentionally asks for more context.
● Threshold rendering supports traceability. Showing the threshold visually makes it easier to understand why a recommendation exists. However, the status must still be shown as text to avoid color-only interpretation.
● Typed chart props reduce accidental misuse. The chart accepts only numeric data and an optional numeric threshold. This keeps the component reusable and prevents Kiro from passing entire machine objects where a number array is expected.
Additional lab section — Add unit tests for pure logic
If your workshop environment allows adding Vitest, ask Kiro to add tests. If package installation is not desired, treat this as an optional read-through exercise.
Kiro prompt sample
Add Vitest tests for the pure domain functions only. Test risk tone thresholds, search filtering, sorting by risk, sorting by fleet sigma, KPI calculation, and simulated update bounds. Do not test CSS or implementation details of React components.
Example test file src/domain/risk.test.ts:
import { describe, expect, it } from 'vitest';
import { getRiskTone, sortMachines } from './risk';
import { initialMachines } from '../data/fixtures';
describe('risk domain logic', () => {
it('classifies risk thresholds', () => {
expect(getRiskTone(20)).toBe('pass');
expect(getRiskTone(55)).toBe('warn');
expect(getRiskTone(80)).toBe('danger');
});
it('sorts machines by highest risk first', () => {
const sorted = sortMachines(initialMachines, 'risk');
expect(sorted[0].risk).toBeGreaterThanOrEqual(sorted[1].risk);
});
});
Business logic explanation
Tests capture the engineering rules behind the dashboard. A future refactor should not accidentally change risk thresholds or sort order.
Code logic explanation
The tests call pure functions directly. They do not depend on the browser or rendered components.
Expected result
Developers gain a fast safety net for the most important logic.
System design rationale
● Test pure logic before UI. Risk thresholds and sort order are more critical than whether a CSS class is applied. Unit tests should protect the engineering decisions first.
● Avoid brittle visual tests in a short workshop. Snapshot or pixel-style testing can distract participants. Pure function tests are easier to understand and maintain.
● Kiro can generate tests after contracts exist. AI-generated tests are better when types and functions are already stable. Asking for tests too early often creates tests around implementation details that later change.
Additional lab section — Add README content with operational boundaries
Ask Kiro to create or update README.md:
Update README.md with setup commands, project structure, Kiro workflow, type standards, hook descriptions, and advisory-only limitations. State clearly that fixture data is synthetic learning data and must be replaced with validated engineering data before any operational use.
Suggested README section:
## Operational boundary
This project is a learning application. It does not connect to factory equipment, dispatch systems, APC systems, or production databases. All records are local fixtures. Recommendations are advisory labels for review workflows only and must not be interpreted as autonomous machine commands.
System design rationale
● Documentation is part of the safety boundary. A developer may understand that an app is local-only, but a future reader may not. README text preserves the intended use.
● Run instructions reduce workshop friction. Participants often return to the project later. Clear setup commands let them restart without depending on memory.
● Kiro workflow notes make the lesson portable. Documenting steering, specs, and hooks helps participants reuse the pattern in their own repositories.
Advanced challenge — Make Kiro refactor without changing behavior
Kiro prompt sample
Refactor the Live Risk Board into `RiskBoard.tsx`, `MachineRow.tsx`, and `EvidencePanel.tsx`. Preserve behavior exactly. Keep all props typed. Do not change CSS class names. Show a summary of files changed before implementation.
System design rationale
● Refactoring is safer with explicit invariants. “Preserve behavior exactly” and “do not change CSS class names” are important constraints. Without them, Kiro may improve structure but unintentionally break styling or interactions.
● Typed props reveal component boundaries. Refactoring forces developers to decide what each component owns. This is a good test of whether the domain model is clean.
● A change summary improves review. Before implementation, Kiro should explain what files it plans to touch. This reduces surprise and models professional pull-request behavior.