Important: what I describe here is an anonymized and adapted version of a conversation, to avoid exposing private details of third-party companies.

The other day I was chatting with a former colleague, Rafa, about all sorts of things, and he dropped this line on me:

“Since I’ve got you here, where would you start these days to discover the functionality in a legacy system? xD”

Not much to it, right? One of those cases where a short sentence drags a ton of implications along with it and depends enormously on the context.

And this was my response:

If we’re talking about a legacy like that desktop app of yours, I’d set up a “discovery lab” with Claude Code connected to it, and run the use cases one by one. In that lab I’d do the equivalent of what you do when you train users on a new rollout (or something like that), and I’d record the desktop video and the session itself (audio and video). Claude would be watching the telemetry, dumped somewhere “isolated” so it has no noise, plus the SQL profiler and the whole kit. All of it timestamped, so you can later ask Claude Code to analyze all that material and do the discovery for you.

All of that has a lot of nuances, but it’s the shortest version I can give you. From there, talk to Claude Code a lot, and even ask it to find inconsistencies between what was explained and what the code says, etc.

When I say record the audio and video of the session, it’s because during the analysis it’ll then be able to tell who speaks with authority (the trainer), who asks the questions, attribute things to them, etc.

The important thing is for the agent to have as much information as possible. Because with that it works miracles.

The thing is, the amount of information in a training session or a tutorial is insane, and AI can now correlate it. What’s more, if you do it right (no keyboard shortcuts, good video quality, maybe a bigger mouse cursor or making sure it has good contrast…) it can even build equivalent web interfaces for you, automate (expensive) tests, etc. Some of those things it would do in a rough way, but they’d be the precursors of more refined ones.

The good part is that if you keep everything stored, you can try several times with different prompts, models, etc.

Two weeks later, he wrote back:

I tried this. In a couple of hours, without knowing anything about the business, I got what used to take a week.

It also leaves you scenarios with questions and how to resolve them. Things like “send me a recording doing the flow this way”, or “also turn on the database response in the telemetry”.

The PoC was very rough. I wanted to put it to the test right away, and there are things to improve in the telemetry, but even so it went pretty well.

Input:

  • A screen recording of the app, with someone explaining as they go through a workflow.
  • Telemetry: here we only gave it a SQL Profiler trace that keeps just the calls the app made, both SQL and stored procedure calls. Calls inside the stored procedures don’t show up. They’re out of the migration scope and would add noise.
  • Access to the app’s repo.

I started by giving it context: what we have, what we’re looking for, not to dig into the stored procedures, etc.

It started analyzing the videos and looking at the code, and when we got to a point where I liked what it had produced, I told it that since this would be run many times, we had to standardize the process and the output documents.

I set up a repo with some skills and templates for the output files, and now you just drop the video and the telemetry in a folder and run that skill.

The output

The case below is a repair shop’s back-office app: the screen technicians use to look up and update the record of a customer’s computer.

Claude Code generated 2 relevant files, among others that are not important for this example.

functional.md

In the functional spec it marks with a green check what it got from the video, and in yellow what it deduced from the code.

Flow: Device sheet — viewing and editing header data

Status: analysis v1. Recorded variants: V1–V7 (longScenario). Deduced from the code: V8–V20.

Goal and actor

A repair shop user (or a sales user) opens a device sheet from the device search to:

  • View all the device’s information, split across tabs: contacts, components, specs, documents, diagnostics, shipping and RMA, manufacturer datasheet, stock management, warranties, repair order history, contracts, attachments, data backups, and specifications.
  • Edit header data on the spot, each field with its own pencil and dialog, and one write per dialog: service tier, battery cycles, power-on hours, and storage location.
  • Enter the editing of asset tag, dates, and owner. That is the retag-asset-tag flow, only referenced here.

The sheet is also the entry point for many other edits (contacts, specifications, warranties, contracts…). This flow only inventories those edits (V14–V18) and leaves their analysis to their own flows.

Legacy screens

Paths are relative to the repo root.

Screen (title) Java class Path (from repo root) Role in the flow
Device tracking DeviceTrackingForm repairs/devices/DeviceTrackingForm.java (context menu, l. 212-240) Entry: View sheet (or double click) opens DeviceSheetForm. View in Repairs Web opens an external URL
Device sheet → “DeviceSheetForm / Devices” DeviceSheetForm repairs/devices/DeviceSheetForm.java (onLoad l. 1410-1585, onAuxTabSelectionChanged l. 1602-1690, configureEditButtonsVisibility l. 2304-2351) Main screen: header, tabs, and pencils
— (business class) Device repairs/devices/Device.java Data loading (initializeDevice l. 388) and service tier write (changeServiceTier l. 902-948)
— (data access) DeviceDb / StoredProcedures common/device/DeviceDb.java, StoredProcedures.java (l. 144) Wrapper for dev_Device_Get and the Op → data map
Maintenance (tier picker) SimpleMaintenanceForm + ServiceTierMaintenance common/maintenance/SimpleMaintenanceForm.java, ServiceTierMaintenance.java (l. 97-151) Generic picker in selection mode: lists the active tiers; one is picked with a double click

…and 5 more screens, trimmed for brevity.

Preconditions
  • The device exists (serial number) and shows up in DeviceTrackingForm.
  • The user has access to DeviceSheetForm and to each edit dialog (checkAccess, R6).
  • For power-on hours: the DeviceWorkingHoursControl parameter > 0 and the device model reports power-on hours through SMART (R9).
  • For the storage location: the device is not a loaner unit or part of the shop’s resale stock (R11).
Steps

Recorded path (longScenario): full viewing and editing of the four header fields and the asset tag.

  1. In Device tracking, right click on the device → View sheet (R1).
  2. Storage location: pencil → “Storage location maintenance” → edits the text → Save. Saved, truncated to 11 characters (R6, R11, R12).
  3. Goes through the lower tabs: Components, Specs, Documents, Diagnostics, Shipping and RMA, Manufacturer datasheet (the API fails and the tab stays empty), Stock management, Warranties, Repair order history, Contracts, Attachments, and Data backups. Each one loads the first time it’s selected (R3, R16, R18).
  4. Opens the top Specifications tab: every field and pencil is disabled (R15).

…and 6 more steps, trimmed for brevity.

Business rules
# Rule Source
R1 The sheet opens from DeviceTrackingForm with View sheet in the context menu or with a double click on the row. View in Repairs Web only shows up with the ShowDeviceSheetLinkToRepairsWeb feature toggle and opens RepairsWebBaseUrl + "/device/{serial}" in the browser (it doesn’t open DeviceSheetForm). Select only with Repairs entry; Copy serial number always. V, C
R2 Initial load: dev_Device_Get HDR (header + 2nd resultset with per-model field visibility: ReportsPowerOnHours, InternalId, UpgradesControl, LiquidDamageHistory) and CMP (components). Then the device is classified: loaner (dev_Loaners_Get), resale stock (stk_Resale_Search), and other assets (stk_OtherAssets_Get). That classification decides what can be edited (R11 and, in retag-asset-tag, R2). With Repairs entry, Contacts is loaded (CUR = current contacts, HIC = contact history). T, C
R3 Lazy loading per tab: each tab runs its read the first time it’s selected (with a flag or by checking whether the data is null), and first requires a serial number. Map: Components → CMP already loaded (+ PARTSDB provider); Specs → SPC; Documents → DOC; Diagnostics → DIA; Shipping and RMA → SHP + RMA; Manufacturer datasheet → API (R16); Stock management → STK (the other side groups, FIN/RSV/TAX/LOC, on demand); Warranties → WAR + dev_ExtraWarranty_Get; Repair order history → ROH; Contracts → dev_ServiceContracts_Get (+ extended support plans, D7); Attachments → dev_Files_Linked; Data backups → embedded SQL. Documents, Shipping and RMA, and Data backups are queried again if they returned no rows the first time; Components is reformatted on every selection. T, C

…and 16 more rules, trimmed for brevity.

Dependencies outside the stored procedures
# Dependency Where What it does / pending
X1 Datasheet REST API (OAuth2) common/apiclient/DatasheetHttpClient.java via VendorDatasheetClient GET api/v1/companies/{companyId}/datasheets/devices/{serial}/sheet and /extended. There’s a SOAP alternative (DatasheetSoapClient). It returned Internal Server Error in the recording
X2 Components SOAP service sales/webservice/DeviceDataWebService.java (getComponents) http://<web-service-host>/device-data/DeviceDataService; only if CMP returns no data
X3 DoC service WebServiceRegistry.listWithCompany(12) + DocFilesClient Downloads the declaration of conformity (outside the recorded path)

…and 9 more dependencies, trimmed for brevity.

Variants
# Variant Status Scenario
V1 Viewing the sheet going through every tab ✅ longScenario
V2 Service tier change ✅ longScenario
V3 Battery cycles change (upward, with control) ✅ longScenario
V4 Power-on hours change (upward, with control) ✅ longScenario
V5 Storage location change (truncated, R12) ✅ longScenario
V6 Asset tag correction from the sheet (details in retag-asset-tag) ✅ longScenario
V7 Datasheet API failure (empty tab and logged error) ✅ longScenario
V8 Cycles or hours lower than or equal to the current ones: blocked with the control checked, or forced by unchecking it (R7, R9) 🟡 —
V9 Cycles with Battery replaced (@BatteryReplaced=1) and error -4 when the value exceeds the battery’s rated limit (R8) 🟡 —

…and 11 more variants, trimmed for brevity.

Web UX proposal

A Device sheet page (/devices/{serial}), with a sticky header and sections:

  1. Header (R2, R19): serial number, description, asset tag and dates, service tier, battery cycles, power-on hours (only when applicable, R9), storage location, and indicators (notes, published, DoC…). Each editable field has an Edit button that opens a popover or small modal with its form:
    • Service tier: a searchable combo with the active tiers, instead of the double-click grid; saves on confirm (R5).
    • Battery cycles / hours: current value, new value, Allow lower or equal value (same as unchecking the control; R7, R9), and Battery replaced for cycles (R7). Live validation with the same messages, and SP errors in the form itself (R8, R10).
    • Storage location: free text with the column’s real length, no truncation (R11, R12, D2).
    • Asset tag and owner: the panel defined in retag-asset-tag (R14).
    • After saving, the value is reread from the DB (an improvement over R13) and a confirmation is shown.
    • Unavailable buttons are shown disabled with a tooltip explaining why (stock, license, permission, device from sales), unlike the legacy (R11, R15).
  2. Tabs (R3, R4) with lazy loading and a per-session cache: Contacts, Components, Specs, Documents, Diagnostics, Shipping and RMA, Manufacturer datasheet, Stock management, Warranties, Repair order history, Contracts, Attachments, and Data backups. Visibility is computed in the backend with the same parameters and permissions. An empty tab shows “No data”.
  3. Specifications as one more section of the sheet (R15), read-only unless the edit conditions are met.
  4. Manufacturer datasheet (R16): if the API fails, the tab shows “Couldn’t retrieve the manufacturer datasheet” and a Retry button, instead of the technical error. The error is logged in the backend.
  5. Edits V14–V17 are linked from the sheet, but each one is its own flow.
Open questions
# Question How to resolve it Status
D1 What does @BatteryReplaced=1 do in …_SaveCycles (does it log a battery swap in the device history?) and what condition returns -4? What do -1/-2/-3 mean (no message in the code)? Record V9; if that’s not enough, read the SPs (justified: the code has no messages for -1/-2/-3). open
D2 How long is the location in the DB? Should the web save the full text (fixing the 11-character truncation)? Check the column schema or the SP parameter type (metadata, without reading the code) and ask the business. open
D3 What RETURN_VALUE does dev_AssignServiceTier return on success (the code requires > 0)? Is it the TierId? Trace with RPC:Completed / output parameters. open
D4 What message does checkAccess show when it denies access, and which permission is behind each form (BatteryCyclesForm, PowerOnHoursForm, StorageLocationForm)? The code of Session.checkAccess + record V19 with a user without access. open
D5 Is dev_Specs_CanEdit just a side-effect-free check? According to the code it is (executeUpdate, only used for visibility). Confirm with RPC:Completed (affected rows) or by reading the SP if spec editing is migrated. open
D6 The code says the datasheet error shows a modal dialog (ErrorDialog), but it doesn’t appear in any frame (04:41–04:48). Is the message suppressed, or was the dialog very brief? Review the video at normal speed at 04:41–04:48 and the handleError signature used by VendorDatasheetClient. open

…and 6 more open questions, trimmed for brevity.

acceptance.md

Acceptance criteria — Device sheet: viewing and editing header data

SQL oracle: the writes in each scenario must match the ones listed here in order, SP, Op, and parameters. To check it, the web’s trace is captured with the same filter as in legacy. Reads aren’t part of the oracle: they can be grouped, cached, or reordered.

Base data: device SN-7K2Q9-48120 (14” business laptop), asset tag RS-004417, service tier Bronze, battery cycles 412, power-on hours 0.0, no storage location (“No location defined for this serial number” is shown), customer device (neither a loaner unit nor resale stock), environment with power-on hours enabled, user with a license other than Technician and access to the dialogs.

Legend: ✅ recorded and verified in legacy · 🟡 deduced from the code, pending recording.


AC1 — View the sheet and its tabs ✅

Covers: V1, R1, R2, R3, R4, R19

Scenario: Open the sheet and go through the tabs
  Given the device "SN-7K2Q9-48120" in the device search
  When the user opens its sheet
  Then they see the header (serial number, description, asset tag and dates, service tier, battery cycles, power-on hours, storage location) and the current contacts with their history
  When they select each tab (Components, Specs, Documents, Diagnostics, Shipping and RMA, Stock management, Warranties, Repair order history, Contracts, Attachments, Data backups)
  Then each tab shows the data from its read (or "No data")
  And each tab only loads the first time it's opened
  And only the tabs allowed by the parameters and permissions show up (R4)

Expected writes: none.

State check: each tab shows what its read returns (dev_Device_Get with SPC, DOC, DIA, SHP/RMA, STK, WAR, ROH; dev_ExtraWarranty_Get, dev_ServiceContracts_Get, dev_Files_Linked, and the data backups query).

Cleanup: not applicable (read-only).

AC2 — Change the service tier ✅

Covers: V2, R5, R13

Scenario: Assign another service tier to the device
  Given the sheet open with service tier "Bronze"
  When the user clicks "Change service tier"
  Then the active tiers are listed in the user's language
  When they pick the tier "Silver"
  Then the sheet shows the service tier "Silver"

Expected writes:

dev_AssignServiceTier @SerialNumber='SN-7K2Q9-48120', @TierId=5, @TierName=<OUTPUT>, @LanguageId=1
-- RETURN_VALUE > 0 (D3); @TierName = 'Silver'

State check: dev_Device_Get @Op='HDR' returns the tier Silver (id 5).

Cleanup: repeat with the id of the Bronze tier (from cat_ServiceTier_Maintenance @OP='R').

AC3 — Change the battery cycles ✅

Covers: V3, R6, R7, R8, R13

Scenario: Raise the device's battery cycles
  Given the sheet open with 412 cycles
  When the user edits the battery cycles, enters 413 with the higher cycles control on and without marking a battery replacement
  And confirms
  Then the sheet shows 413 cycles

Expected writes:

dev_Device_SaveCycles @SerialNumber='SN-7K2Q9-48120', @Cycles=413, @BatteryReplaced=0
-- RETURN_VALUE = 0

State check: dev_Device_Get @Op='HDR' returns 413 cycles.

Cleanup: go back to 412 without the higher cycles control (V8): …_SaveCycles @Cycles=412, @BatteryReplaced=0.

…and 14 more acceptance criteria (AC4 to AC17), trimmed for brevity.

Non-functional
  • Opening the sheet must not wait for the tabs: the header renders first and each tab loads when opened (R3). In legacy, the initial load takes about 8 s (10:14:05 → 10:14:13).
  • Header writes are immediate (< 3 s observed). While saving, the form blocks a second submission. After saving, the sheet rereads the value from the DB (an improvement over R13).
  • SP errors are shown with an understandable message: no empty error dialogs (-1/-2/-3) or silent failures (service tier ≤ 0).
  • External service failures (X1–X5) don’t block the sheet: a message in the affected section, with retry.
  • Its own retry and timeout policy for database locks, without replicating the legacy’s broken retry (R17).
  • No field is silently truncated (R12): the web validates the length before sending.

These two files, along with other supporting ones, could be used to improve the legacy system, refactor it, write documentation, explain it to other people on the team, or for many other activities that depend on knowledge that, until then, lived in the head of someone who knew the application from a functional point of view and didn’t even connect that knowledge to the actual implementation in the code.

Keep in mind that this is just a simplified example. From these recordings, you could ask the AI to take screenshots of every form and every process, to create alternative versions following other design guidelines, and so on. It’s also far from perfect, but it’s a more than valid starting point, for a very small fraction of the human effort this work would otherwise have required.

In short, we can drastically cut the time it takes to acquire knowledge that would otherwise have taken us days or even weeks, using tools we already have at hand today, and without building very sophisticated or complex systems.