Skip to content

Repair workflow: AssetRecord, triage, salvage-match, claim, and compatible manifests (GAPs 1–8)#179

Merged
touchthesun merged 21 commits into
mainfrom
repair
Jun 22, 2026
Merged

Repair workflow: AssetRecord, triage, salvage-match, claim, and compatible manifests (GAPs 1–8)#179
touchthesun merged 21 commits into
mainfrom
repair

Conversation

@touchthesun

Copy link
Copy Markdown
Contributor

Summary

Adds the complete repair workflow to OHM, enabling coordinators to track physical device units in the field, triage their condition against an OKH design, find salvageable parts across the fleet, and route them to technicians who need them.

This PR closes all 8 gaps documented in docs/models/repair-workflow-gaps.md.

What's new

Data model

  • AssetRecord — physical-state domain object alongside OKH (design) and OKW (capability); stored at asset/{id}.json
  • ComponentState with condition, harvest_viable, repair_feasible, source_required (auto-derived from triage + manifest flags)
  • AssetStatus lifecycle: active → under_triage → parts_pending → under_repair → restored / condemned
  • compatible_manifest_ids field on OKHManifest for cross-model salvage search
  • Repair fields on OKHManifest: repair_guides, disassembly_guides, spare_parts, repair_difficulty, etc.

API endpoints (/v1/api/asset/... and /v1/api/okh/...)

Method Path Description
POST /api/asset/ Register a device unit
GET /api/asset/{id} Get asset
GET /api/asset/ List assets (filter by manifest, status, harvest_viable)
PUT /api/asset/{id} Update lifecycle status / notes
DELETE /api/asset/{id} Remove asset
POST /api/asset/{id}/triage Record component observations (auto-derives flags)
GET /api/asset/{id}/triage-report Per-component repair recommendation
GET /api/asset/{id}/triage-checklist Component list pre-filled with current observations
GET /api/asset/{id}/resolve-sourcing Fleet-first sourcing resolution
POST /api/asset/salvage-match Fleet search for harvestable components
POST /api/asset/{id}/claim-component Reserve a component (409 if already claimed)
POST /api/okh/harvest-parts Flat component inventory with optional fleet enrichment
POST /api/okh/extract-repair-docs Extract repair fields from service manuals
POST /api/okh/import-repair-doc Merge extracted repair fields into a manifest

CLI commands (ohm asset ... and ohm okh ...)

  • ohm asset: create, get, list, update, delete, triage, triage-report, triage-checklist, resolve-sourcing, salvage-match, claim-component
  • ohm okh: harvest-parts (with --enrich-fleet), set-compatible-manifests, extract-repair-docs, import-repair-doc

Documentation

  • docs/models/repair-workflow.md — end-to-end workflow guide with API + CLI examples for every step
  • Updated docs/CLI/index.md with full Asset Commands section and OKH Repair Commands section
  • Updated docs/models/index.md with AssetRecord as a first-class domain model

Bug fixes (found during integration testing)

  • FederationService: renamed self.metricsself.federation_metrics to fix AttributeError on startup (collision with BaseService.ServiceMetrics.errors)
  • AssetService.initialize: removed stray _ensure_domains_registered() call (copy-paste from OKWService; method doesn't exist on AssetService)
  • SmartFileDiscovery: added "asset": "asset/" to directory_prefixes — without this, all AssetService.get() calls returned None even after a successful write
  • OKHUpdateRequest.repo: changed from required str to Optional[str] — manifests created without a repo URL returned null on GET, causing 422 on PUT-back
  • update_okh route: was returning bare OKHManifest instead of OKHResponse, causing 500 on serialization

API contract improvements

  • Moved inline request models from the route file to src/core/api/models/asset/request.py with Field() descriptions — proper OpenAPI schema names and documentation
  • Added message: str to all 7 asset response models — every endpoint returns a human-readable display string
  • Documented the AssetResponse.status naming distinction (lifecycle status, not API status) in model docstrings and the workflow guide

Test plan

  • 19 integration tests across 3 files, all passing (RUN_LIVE_API_TESTS=1 STORAGE_PROVIDER=local .venv/bin/python -m pytest tests/integration/test_api_claim_component.py tests/integration/test_api_harvest_enrich.py tests/integration/test_api_compatible_manifests.py)
  • 395 unit tests passing
  • Tests use FastAPI TestClient (no live server required)

🤖 Generated with Claude Code

MakerNetwork and others added 21 commits June 20, 2026 17:21
Add structured repair support to OKHManifest following a research spike
across 9 technical documents (medical devices, lab equipment, consumer
appliances, iFixit guides):

Component:
  - part_number: manufacturer/supplier P/N for parts matching
  - consumable: flag for periodic-replacement parts (filters, catalysts,
    seals) — distinct from replaceable, affects salvage value assessment
  - diagnostic_codes: machine-readable alarm/error codes that identify
    component failures (e.g. FLWERR on Fresenius, F1/F3 on appliances)

RepairGuide:
  - author: attribution for community guides (iFixit, wiki)
  - safety_prerequisites: hard safety gates before beginning work,
    prominently distinct from repair_notes
  - applies_to_models: model variants a guide covers (common when one
    document spans a product family)

DocumentationType enum:
  - PARTS_CATALOG: exploded-diagram parts references
  - SERVICE_MANUAL: professional technician service docs
  - TROUBLESHOOTING_GUIDE: flowchart/decision-tree diagnostic guides
  - OPERATIONS_MANUAL: operator-facing docs with basic maintenance

Validation metadata adds consumable_count alongside repair_guide_count.
Integration tests cover all new fields via API roundtrip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two-pass extractor (RepairDocExtractor) pulls structured OKH repair fields
from uploaded PDFs and text documents. Programmatic pass runs offline and
covers document-type classification, parts-table parsing (POS.NO / PART.NO),
diagnostic-code extraction (FLWERR, !WATER, F1-style codes), tools, safety
prerequisites, author attribution, and model applicability. Optional LLM pass
validates and enriches the programmatic output via LLMService.generate().

Wires in: POST /extract-repair-docs (multipart, optional manifest_id + use_llm),
OKHRepairExtractResponse, `ohm okh extract-repair-docs` CLI command with
execute_with_fallback, docs section in okh-docs.md, and integration tests
gated behind RUN_LIVE_API_TESTS=1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reduces false positives from 54 to 30 for Fresenius Technical,
24 to 3 for Buffalo Ops Manual, and fixes Frigidaire doc-type
classification (None → PARTS_CATALOG).

_PN_IGNORE: extend to catch multi-level section numbers (2.4.2, 2.1A)
using \d+(?:\.\d+)+[A-Z]? instead of the single-dot \d+\.\d+ pattern.

_looks_like_pn: require at least one digit (rejects "U.S.", "P/N",
"SPECIFICATIONS."); reject US phone numbers (NNN-NNN-NNNN); reject
tokens whose first slash-segment is all-alpha ("QUARTERLY/1000", "A/C");
reject voltage/frequency specs ("100/120/220/240V", "50/60Hz").

_PN_EXPLICIT context: strip trailing ")].,;:" from extracted names
so "12 VDC Power Supply)." becomes "12 VDC Power Supply".

_DOCTYPE_KEYWORDS: add "pos. no" and "part no" variants so Frigidaire's
"POS. NO  PART NO." table header scores as PARTS_CATALOG.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Corrects parts-catalog key signals table to include space-variant
column header formats (POS. NO, PART NO.) validated against the
Frigidaire parts catalog.

Adds 'Extraction quality' section explaining when --use-llm is
needed: non-standard table layouts (Bactron-style), dense technical
manuals with inline P/N prose, and operations manuals.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents the architecture decision (Option C over A/B), data model
for ComponentCondition/ComponentState/AssetRecord, service methods,
API endpoints, CLI commands, integration test plan, and the four-PR
implementation sequence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tests

Harvest a flat component inventory from one or more OKH manifests.
Each component is annotated with source_manifest_id and source_manifest_title.
Four filter flags (AND-combined): replaceable_only, salvageable_only,
consumable_only, has_part_number. Response includes per-category counts.

- OKHHarvestRequest / OKHHarvestResponse in request/response models
- POST /api/okh/harvest-parts route with 404 on unknown manifest
- ohm okh harvest-parts CLI with execute_with_fallback and --output flag
- Integration tests: shape, per-filter correctness, combined-filter AND,
  multi-manifest merging, 404/400 on bad IDs
- okh-docs.md section with API reference, CLI examples, AssetRecord callout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces the AssetRecord model (ComponentCondition enum, ComponentState,
AssetRecord dataclass), AssetService (CRUD + record_triage upsert-by-name),
REST API (/api/asset/ + /{id}/triage), CLI (ohm asset create/get/list/
update/delete/triage with execute_with_fallback), and integration/unit
test suites. Closes the physical-state modeling gap identified in the
repair epic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Joins AssetRecord (observed condition) with OKHManifest (design flags) to
produce a TriageReport with one TriageAction per component: assess /
no_action / repair_in_place / harvest / source_new / decommission. Derived
from condition + replaceable/salvageable flags; stored nowhere, generated on
demand. Adds GET /api/asset/{id}/triage-report, ohm asset triage-report CLI
command, 14 unit tests, and 10 integration tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fleet-level inventory query: given a component name (substring) and/or
part number (exact), scans all AssetRecord component_states where
harvest_viable=True and returns matching asset context + manifest flags.
Adds _is_salvage_match pure predicate, AssetService.salvage_match with
per-manifest OKH cache, POST /api/asset/salvage-match endpoint, ohm asset
salvage-match CLI command, 16 unit tests, and 14 integration tests. Closes
the salvage matching item in the repair epic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Records the gaps between current implementation and a complete real-world
repair workflow: write-back from triage recommendations (GAP-1, critical),
lifecycle state (GAP-3), checklist entry point (GAP-2), sourcing resolution
(GAP-5), RepairDoc ingestion (GAP-4), fleet enrichment (GAP-6), reservation
(GAP-7), and cross-manifest compatibility (GAP-8). Includes ordered fix
sequence with dependency rationale.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e_required on record_triage

When a technician records component conditions without explicitly setting
harvest_viable, repair_feasible, or source_required, record_triage() now
derives those flags from _derive_action() (condition + manifest design flags)
and fills any None fields before the upsert. Caller-supplied non-None values
always win. Closes the loop between triage observations and salvage matching:
a component marked salvageable in the manifest is now findable in salvage-
match after a plain condition-only triage call.

Adds _derive_flags() pure function, 8 unit tests, and 4 integration tests
covering the GAP-1 scenario and caller-override semantics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces AssetStatus enum (active | under_triage | parts_pending |
under_repair | restored | condemned) on AssetRecord. Migration-safe:
from_dict defaults None/missing to ACTIVE so stored records without
the field load cleanly.

API: status surfaced in AssetResponse; PUT /api/asset/{id} accepts
status updates with validation; GET /api/asset/ supports ?status=
filter with alias to avoid FastAPI naming conflict.

CLI: ohm asset update --status, ohm asset list --status, and the
detail display all reflect the new field.

Unit tests cover enum values, round-trip, and migration safety.
Integration tests cover create default, update all values, invalid
value rejection, and the ?status= filter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GET /api/asset/{id}/triage-checklist returns the manifest's full
component list pre-filled with any existing ComponentState observations.
Each item carries assessed=True/False, current_condition, current_state,
and manifest design flags (replaceable/salvageable/consumable/part_number)
so a technician opening a device knows exactly what to assess and what
has already been recorded.

Components observed but absent from the manifest appear at the end of
the list so unlisted parts are not silently dropped.

Summary fields assessed_count and pending_count let callers render
progress without iterating items.

CLI: ohm asset triage-checklist <asset-id> [--output file.json]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GET /api/asset/{id}/resolve-sourcing reads the triage report and, for
each source_new component, checks whether a harvestable match exists
elsewhere in the fleet before escalating to procurement.

The requesting asset is excluded from its own salvage search.

Per-component verdicts:
- fleet_available — match found in fleet; matches list included
- procure_new     — nothing in fleet; must order externally

Summary fields fleet_available_count and procure_new_count give a
quick breakdown without iterating items.

salvage_match() gains an exclude_asset_id parameter to support this
self-exclusion requirement.

CLI: ohm asset resolve-sourcing <asset-id> [--output file.json]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /api/okh/import-repair-doc and ohm okh import-repair-doc connect
RepairDocExtractor output to OKH manifests with correct merge behavior:

- Components already in the manifest keep all existing flags (replaceable,
  salvageable, consumable) — human annotations are never overwritten.
- Newly-extracted components default to replaceable=False, salvageable=False
  regardless of extractor heuristics, requiring human annotation before the
  manifest can drive triage or salvage matching.
- Deduplication by component name (case-insensitive); part_number and
  diagnostic_codes are enriched on existing components when previously unset.
- Repair guides are deduplicated by title; new ones are appended.

This differs from extract-repair-docs, which appends components without
deduplication and does not enforce conservative flag defaults.

An annotation_reminder field in the response makes the required human
review step explicit.

CLI: ohm okh import-repair-doc <files> --title <name>
     ohm okh import-repair-doc <files> --manifest-id <uuid>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add optional enrich_fleet flag to POST /api/okh/harvest-parts.
When set, runs salvage_match per component and attaches
fleet_available_count and fleet_asset_ids to each component in
the response. Closes the gap between design-level inventory and
actual fleet availability in one request.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ComponentState gains claimed_by + claimed_at with a 48h lazy-expiry
check (is_claimed). AssetService.claim_component() sets the claim and
raises ValueError on conflict (mapped to 409 at the API layer).
POST /api/asset/{id}/claim-component is the new endpoint; salvage-match
gains exclude_claimed (default true) so claimed components are hidden by
default. ohm asset claim-component command added; salvage-match gets
--include-claimed to override the default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OKHManifest gains compatible_manifest_ids: List[str] — UUIDs of
manifests whose components are physically interchangeable with this
one. salvage_match expands the fleet search to include assets from
all listed manifests when manifest_id is scoped. OKHResponse and
OKHUpdateRequest expose the field so it can be read and set via
the existing GET/PUT /api/okh/{id} endpoints.

Part-number search (no manifest_id) already crossed manifest
boundaries and remains the correct path for ad-hoc cross-manifest
queries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Five bugs found and fixed while getting 19 integration tests green:

- FederationService: renamed self.metrics → self.federation_metrics to avoid
  collision with BaseService's ServiceMetrics which has an .errors attribute
  (AttributeError on startup when federation was enabled)
- AssetService.initialize: removed stray _ensure_domains_registered() call
  copied from OKWService — AssetService has no such method and doesn't need it
- SmartFileDiscovery: added "asset" → "asset/" to directory_prefixes so
  discover_files("asset") finds files instead of returning [] (caused all
  AssetService.get() calls to return None after successful writes)
- OKHUpdateRequest: repo field changed from required str to Optional[str] —
  manifests created without a repo URL return null on GET, and PUT-back failed
  422 validation
- update_okh route: returned bare OKHManifest instead of OKHResponse, causing
  500 on serialization; fixed to wrap in OKHResponse.model_validate like other
  routes do

Integration test infrastructure:
- Added POST /manifests/ and DELETE /manifests/{id} routes to OKH router for
  test fixture setup/teardown
- Converted all three integration test files from httpx + live server to
  FastAPI TestClient (tests/integration/conftest.py session fixture)
- Added AssetGenerator to generate_synthetic_data.py and seed_repair_scenario.py
  for local dev seeding of repair-workflow data

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CLI parity (GAP-8):
- Add `ohm okh set-compatible-manifests` command — set, add, remove, or clear
  compatible_manifest_ids on a stored manifest (HTTP API + direct fallback)

API contract consistency:
- Move inline request models (AssetTriageRequest, SalvageMatchRequest,
  ClaimComponentRequest, AssetCreateRequest, AssetUpdateRequest) from route
  file to src/core/api/models/asset/request.py with Field() descriptions —
  these now appear in OpenAPI schema with proper names and doc strings
- Add `message: str` to all asset response models (AssetResponse,
  AssetListResponse, TriageReportResponse, ChecklistResponse,
  SourcingResolutionResponse, SalvageMatchResponse, ClaimComponentResponse)
  with human-readable defaults so frontends always get a display-ready string
- Document the AssetResponse.status naming convention (lifecycle status, not
  API status) in both the response model and the repair-workflow doc

Documentation:
- docs/models/repair-workflow.md — full end-to-end repair workflow guide:
  AssetRecord model, lifecycle status table, 7-step workflow with API + CLI
  examples for each step, compatible manifests, harvest-parts, repair-doc
  extraction, and API response convention note
- docs/models/index.md — add AssetRecord as first core model entry linking
  to asset-docs.md and repair-workflow.md
- docs/CLI/index.md — add `asset` to command group table; add Asset Commands
  section covering all 10 subcommands; add OKH Repair Commands section
  covering harvest-parts, set-compatible-manifests, extract-repair-docs,
  import-repair-doc

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@touchthesun touchthesun merged commit 39ea413 into main Jun 22, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants