in progress (backend shipped 2026-06-24; Contract Graph migration 2026-07-14; validation-gated)

Spec: F84 — Field-Service Industry Ontology

The vocabulary contract for the beachhead verticals. F83 built the framework for industry-aware extraction (OrgPulseConfiguration.industry_id, the daily_update_pack data layer, a _SIGNAL_KEYWORD_MAP). This spec defines the content: the entities, signal taxonomy, and trade vocabulary for HVAC/MEP maintenance (PRIMARY) and interior fit-out (PARALLEL TEST) — so Lumi understands service calls, job status, SLAs, parts, deposits, and sign-off by default, with no per-org configuration.

Status: in progress (backend shipped 2026-06-24; Contract Graph migration 2026-07-14; validation-gated) Builds on: F83 (framework) · F08 (extraction engine) · consumed by F70 (the wedge)

Validation-gated, not velocity-gated. Like F70, this ontology is a draft contract tied to the field-test gates. The beachhead vocabulary below is a founder/product hypothesis. It does not advance to agreed/shipped until the trade vocabulary is validated against real Vietnam contractor chat transcripts (HVAC/MEP + fit-out) — the pilot decides the words, not the deck.


1. Problem & CEO Job

Serves: see clearly · trust the signal.

A field-service owner’s day lives in Zalo: “kẹt vật tư, board mạch chưa về”, “khách chưa cọc 50 triệu”, “phòng 3 xong rồi, chờ nghiệm thu”, “khách muốn đổi màu gạch”. Generic extraction (F08) reads this as undifferentiated text: “board mạch chưa về” becomes a vague operations note instead of a missing_parts blocker that has cost a full crew-day; “chưa cọc” becomes nothing instead of an overdue deposit putting cash at risk; “chờ nghiệm thu” becomes noise instead of a job awaiting the customer sign-off that releases the invoice.

The pitch names this the third pillar of “what turns noise into signal”: Industry ontology — service calls, job status, SLAs, parts, deposit, sign-off — understood by default. Without a defined ontology, F83’s framework is an empty shell: industry_id points at field_service_technical_maintenance but the signal IDs it lists (missing_parts, sla_risk, …) have no trade-vocabulary triggers wired into the extraction enrichment, and _SIGNAL_KEYWORD_MAP carries only ~12 generic signals with a thin, Vietnamese-only keyword list. The Brief and the F70 Boss Brief starve on signal quality precisely where the wedge needs them sharpest — the first contractor’s first paste.

This spec closes that gap by defining, as data (not new code paths and not new core models), the field-service entity map and signal taxonomy that lets Lumi understand a contractor’s words on day one, in Vietnamese first, with English alongside.


2. User Stories & Acceptance Criteria

2.1 Field technician — trade words understood by default

  • As a field technician in an HVAC/MEP org, I want Lumi to understand my normal trade phrasing without anyone configuring it, so that my manager sees the real blocker, not a vague note.
    • Given org industry_id = field_service_technical_maintenance, when a pulse says “Site B kẹt vì board mạch chưa về”, then a BLOCKER item (Risk, see §4) is produced with signal missing_parts, severity=high, an EvidenceSource quoting the line, and confidence_score ∈ [0.65, 0.80] (keyword) or [0.85, 0.90] (LLM).
    • Given the same org and “khách đã hứa quay lại trước 14h mai”, then a COMMITMENT (Commitment) is produced with signal commitment_due, evidence-linked.
    • Given “phòng 3 xong rồi, chờ nghiệm thu”, then a sign_off_pending signal is produced as a DECISION/blocker awaiting customer acceptance — never silently marked complete (sign-off is a customer act, not a technician claim).

2.2 Interior fit-out supervisor — parallel-test vocabulary

  • As a site supervisor in an interior fit-out org (specialty_contracting_construction), I want my words about drawings, approvals, and acceptance understood, so that the owner sees the approval that is blocking the build.
    • Given “chưa thi công trần vì bản vẽ MEP chưa được duyệt”, then an approval_blocker BLOCKER (Risk) is produced, evidence-linked, severity high.
    • Given “cần chủ đầu tư xác nhận phát sinh trước 11h mai”, then a DECISION (DecisionRequest) is produced with signal scope_change/customer_approval_delay.

2.3 Owner — the four-state operating view

  • As an owner, I want every extracted item to fall into one of four operating states (COMMITMENT · BLOCKER · DECISION · COMPLETED), so that my brief reads as “what’s promised, what’s stuck, what needs me, what’s done” — exactly the pitch’s HOW-IT-WORKS structure.
    • Given any field-service signal fires, when it produces an item, then it maps to exactly one of the four output types per the §4 table — never an unmapped fifth state.
    • Given a deposit phrase (“chưa cọc 50 triệu, quá 3 ngày”), then a deposit_overdue signal produces a cash BLOCKER (Risk, risk_type=cash) with the amount captured.

2.4 Org admin — per-company vocabulary override, no leakage

  • As an org admin, I want to add my company’s own terms (“con bo” for a specific part, an internal site codename) without affecting any other company, so that Lumi fits our slang.
    • Given admin adds {"con bo": "part"} to custom_company_vocab (F83 field), when a member’s pulse uses that term, then it resolves to the part entity / missing_parts signal for that org only.
    • Given two orgs with different custom_company_vocab, then one org’s custom term never influences extraction for the other (tenant isolation — §7 mandatory test).

2.5 Fallback — no industry, no breakage

  • As a member of an org with no industry_id set, I want extraction to still work, so that the absence of configuration never produces a blank or wrong brief.
    • Given industry_id is blank/unknown, then extraction falls back to the core signal set (F83 §12 resolution rules), produces honest generic items, and logs at WARNING — no field-service-only signal is asserted without the ontology being active.

3. Scope Boundaries

In:

  • The entity map and signal taxonomy for the two beachhead verticals: field_service_technical_maintenance (HVAC/MEP maintenance — PRIMARY) and specialty_contracting_construction (interior fit-out — PARALLEL TEST).
  • The trade-vocabulary triggers (Vietnamese-first, English alongside) that bind each field-service signal to phrasing a contractor actually uses — extending the existing daily_update_pack/config/signal_catalog.json (signal definitions) and _SIGNAL_KEYWORD_MAP (keyword triggers in backend/apps/ai/services.py).
  • The output-type mapping (§4): how each ontology signal lands on the existing spine (Commitment / Risk / DecisionRequest / Task) as COMMITMENT / BLOCKER / DECISION / COMPLETED.
  • The per-org override contract via the existing F83 OrgPulseConfiguration.custom_company_vocab and signal_severity_overrides — no new override mechanism.
  • The vocabulary lexicon file (proposed, §4.3) that authors trade vocabulary as data under the existing daily_update_pack layer.

Out:

  • New core models — explicitly forbidden. The ontology maps onto the existing spine (Commitment, Risk, DecisionRequest, Task, EvidenceSource, OrganizationMember, Relationship/crm.Company). A “Job”, “Deposit”, or “Sign-off” is a concept this spec names and maps — it is never a new Django table.
  • The extraction engine itself — owned by F08. This spec only supplies the vocabulary/signal content F08 consumes via the resolved profile.
  • The pulse UI, profile resolution, and org config models — owned by F83. This spec adds content into F83’s data layer.
  • Other verticals. Wholesale distribution and general construction are LATER per the pitch’s subsegment scoring. Their stubs already exist in industries.json; this spec does not flesh out their vocabulary.
  • LLM prompt design / multilingual NLU — F37. This spec defines the what (vocabulary + signals); F37 owns the LLM-mode how.
  • Auto-detection of industry from org name — F83 §12 OQ3 (Phase 2), out.

4. Data Model

No new Django models. The ontology lives as versioned JSON in the daily_update_pack layer (the F83-established pattern: editorial source at repo-root daily_update_pack/, copied to backend/apps/pulses/data/daily_update_pack/ at build time, loaded by PulsePackLoader) plus per-org overrides on the existing OrgPulseConfiguration (F83). Where keyword triggers must be in code today (mock mode), they extend the existing _SIGNAL_KEYWORD_MAP in backend/apps/ai/services.py.

4.1 Entity map — concept → existing spine model

For each beachhead vertical, the ontology names the operational entities and binds each to an existing model. None of these is a new table.

Canonical idVI labelEN labelMaps to (existing)Notes
job / work_ordercông việc / lệnh sửaservice call / jobwork.Task (+ Project for fit-out)A dispatched site visit = a Task; a fit-out package = a Project of Tasks.
sitecông trình / hiện trườngsitework.Task.location + free-text on item; or crm.Company addressNo Site model — site is an attribute, not an entity table.
part / materialvật tư / linh kiện / board mạchpart / material(no model) — surfaced via missing_parts Risk + evidenceA missing part is a blocker signal, not a stored inventory row (inventory is out of scope here).
customerkhách hàng / chủ đầu tưcustomercrm.Company (Relationship spine)Reuse CRM; never a parallel customer table.
deposit (đặt cọc)đặt cọc / tiền cọcdepositRisk (risk_type=cash, money_amount) + CRM deal deposit_* fieldsOverdue deposit = cash blocker; existing F50 deal deposit_paid/deposit_due_date fields back the CRM path.
quote (báo giá)báo giá / chào giáquoteCommitment (to send) or DecisionRequest (to approve)“Send the quote Thursday” = Commitment; “approve the quote” = Decision.
sign_off / acceptance (nghiệm thu)nghiệm thu / xác nhận hoàn thànhsign-off / acceptanceDecisionRequest (awaiting) → Task/Commitment closed-with-evidence (done)Sign-off is a customer act that gates COMPLETED.
warranty (bảo hành)bảo hànhwarrantyRisk (risk_type=customer) or CommitmentA warranty claim/return = customer risk + a commitment to revisit.
slaSLA / cam kết thời gianSLARisk (risk_type=delivery/customer)SLA breach/at-risk = delivery/customer risk; never a new SLA table.
revisit (quay lại)quay lại / đi lại lần 2return visit / revisitwork.Task (follow-up) + CommitmentPromised revisit = Commitment; the work itself = a Task.

4.2 Signal taxonomy — field-service signals

These signal IDs already exist in daily_update_pack/config/signal_catalog.json and are already selected by the two beachhead industries in industries.json. This spec defines their trade-vocabulary triggers (VI + EN), output type, default severity, and honest confidence range. New IDs introduced by this spec (deposit_overdue, sign_off_pending, warranty_claim) must be added to the signal catalog (additive, no removals) before activation — flagged in §11.

Confidence policy (F08 §2, Rule 2): keyword 0.65–0.80, LLM 0.85–0.90, never 1.0, never hardcoded. Severity values are low | medium | high | critical (Risk.SEVERITY_CHOICES).

Signal idVI trigger vocabulary (examples)EN trigger vocabularyOutput typeDefault severityConfidence (kw / LLM)Catalog status
missing_parts”kẹt vật tư”, “board mạch chưa về”, “thiếu linh kiện”, “chưa có phụ tùng”, “thiếu vật tư""missing part”, “board hasn’t arrived”, “out of materials”, “waiting on the part”BLOCKERhigh0.65–0.75 / 0.85–0.90exists
sla_risk”trễ SLA”, “quá thời gian dịch vụ”, “trễ hẹn khách”, “không kịp cam kết""SLA at risk”, “missing the deadline”, “past the service window”BLOCKERhigh0.65–0.75 / 0.85–0.90exists
deposit_overdue (đặt cọc)“chưa cọc”, “chưa đặt cọc”, “tiền cọc quá hạn”, “khách chưa chuyển cọc 50 triệu""deposit overdue”, “hasn’t paid the deposit”, “deposit not received”BLOCKER (cash)high0.65–0.75 / 0.85–0.90NEW
sign_off_pending (nghiệm thu)“chờ nghiệm thu”, “đợi khách xác nhận”, “chưa nghiệm thu”, “xong rồi chờ duyệt""awaiting sign-off”, “pending acceptance”, “waiting for customer confirmation”DECISIONmedium0.65–0.75 / 0.85–0.90NEW
repeat_failure”lại hỏng”, “hỏng lần 2”, “vẫn chưa hết lỗi”, “tái diễn sự cố”, “máy lại đứng""failed again”, “same issue recurred”, “broke down again”BLOCKERhigh0.65–0.75 / 0.85–0.90exists
warranty_claim (bảo hành)“yêu cầu bảo hành”, “đòi bảo hành”, “trong thời gian bảo hành”, “lỗi bảo hành""warranty claim”, “under warranty”, “warranty issue”COMMITMENT (revisit) + customer riskmedium0.65–0.75 / 0.85–0.90NEW
site_access_problem (site_blocked)“không vào được công trình”, “bị chặn”, “chưa có giấy phép vào”, “bảo vệ không cho vào""site blocked”, “no access”, “permit missing”, “can’t get on site”BLOCKERmedium0.65–0.75 / 0.85–0.90exists
rework_required (revisit_required)“phải làm lại”, “quay lại sửa”, “đi lại lần 2”, “làm lại từ đầu""rework needed”, “have to redo”, “return visit required”BLOCKER + revisit COMMITMENThigh0.65–0.75 / 0.85–0.90exists
job_delay”job chưa xong”, “trễ tiến độ”, “không kịp hôm nay”, “dời sang mai""job not finished”, “running late”, “pushed to tomorrow”BLOCKERhigh0.65–0.75 / 0.85–0.90exists
approval_blocker”chưa được duyệt”, “bản vẽ chưa duyệt”, “chờ phê duyệt”, “đợi chủ đầu tư duyệt""not approved”, “waiting on approval”, “drawing not signed off”DECISIONhigh0.65–0.75 / 0.85–0.90exists
commitment_due”đã hứa”, “sẽ gửi”, “cam kết”, “trước 14h mai”, “hẹn khách""promised”, “will send”, “committed to”, “told the customer by”COMMITMENThigh0.65–0.75 / 0.85–0.90exists
completed_not_invoiced”xong nhưng chưa xuất hóa đơn”, “đã hoàn thành chưa nghiệm thu”, “chờ nghiệm thu để xuất""done but not invoiced”, “completed pending billing”COMPLETED (with revenue-leakage risk)high0.65–0.75 / 0.85–0.90exists

Honesty note on sign_off_pending vs COMPLETED. A technician saying “xong rồi” (done) is not sufficient to mark a job COMPLETED. Field-service COMPLETED requires the customer sign-off (nghiệm thu). So “phòng 3 xong, chờ nghiệm thu” produces a sign_off_pending DECISION/blocker, not a COMPLETED — Lumi never fabricates an acceptance the customer did not give (the pitch’s “Zero invented … sign-offs” gate).

4.3 Proposed vocabulary lexicon file (data layer)

Trade vocabulary is authored as data under the existing pack, not hardcoded long-term. Proposed new file (additive to the F83 layer; mark clearly as proposed until built):

daily_update_pack/config/
  signal_catalog.json        # exists — add deposit_overdue, sign_off_pending, warranty_claim
  industries.json            # exists — field_service_* + specialty_contracting_* already select signals
  vocab/                     # PROPOSED (new subdirectory, data only)
    field_service_vi.json    # VI trade-vocabulary triggers per signal + entity aliases
    field_service_en.json    # EN trade-vocabulary triggers
    fit_out_vi.json
    fit_out_en.json

Proposed schema for one vocabulary entry (field_service_vi.json):

{
  "version": "1.0.0",
  "industry_id": "field_service_technical_maintenance",
  "locale": "vi",
  "signal_vocab": {
    "missing_parts": {
      "triggers": ["kẹt vật tư", "board mạch chưa về", "thiếu linh kiện", "chưa có phụ tùng"],
      "output_type": "blocker",
      "maps_to": {"model": "Risk", "risk_type": "delivery", "severity": "high"},
      "entities": ["part", "job"]
    },
    "deposit_overdue": {
      "triggers": ["chưa cọc", "tiền cọc quá hạn", "khách chưa chuyển cọc"],
      "output_type": "blocker",
      "maps_to": {"model": "Risk", "risk_type": "cash", "severity": "high"},
      "entities": ["deposit", "customer"]
    }
  },
  "entity_aliases": {
    "part": ["vật tư", "linh kiện", "board mạch", "phụ tùng"],
    "site": ["công trình", "hiện trường", "site"],
    "deposit": ["cọc", "đặt cọc", "tiền cọc"],
    "sign_off": ["nghiệm thu", "xác nhận hoàn thành"]
  }
}

The existing industry pack entry (industries.json → field_service_technical_maintenance) already declares industry_signal_ids, entities, kpis, and a free_form_example_vi. This spec’s lexicon adds the trigger phrasing that the F08 enrichment (_build_industry_risk_patterns → _SIGNAL_KEYWORD_MAP) consumes. The OrgPulseConfiguration.profile_snapshot.extraction_context (F83) is the carrier that delivers the resolved signal set + custom_company_vocab to the extractor.

4.4 Per-org overrides (existing F83 fields — no new fields)

  • OrgPulseConfiguration.custom_company_vocab (JSONField, F83) — {local_term: canonical_entity}, tenant-scoped. A company’s slang resolves to a canonical entity/signal for that org only.
  • OrgPulseConfiguration.signal_severity_overrides (JSONField, F83) — {signal_id: severity}, lets an org raise/lower a field-service signal’s severity.
  • OrgPulseConfiguration.materiality_threshold_vnd (F83) — gates deposit_overdue and other cash signals: a deposit below the threshold is not flagged a cash blocker.

No migration is required for the ontology content; the only DB surface is the existing F83 config, already shipped.

4.5 BLOCKER is not a new model or a new risk_type

Critical constraint (from codebase inspection). Risk.risk_type choices are customer | revenue | cash | delivery | people | hiring | legal | operations | other — there is no blocker value, and this spec does not add one as a primary path. A BLOCKER is a surfaced state, not a new entity:

  • A BLOCKER is a Risk with an appropriate existing risk_type (delivery for parts/SLA/ job delay, cash for deposits, operations for site access) and severity ∈ {high, critical}, optionally linked to a blocked Task via Risk.related_task.
  • Alternatively (when the blocked work is the primary object) a BLOCKER surfaces as a Task dependency / blocked Task rather than a Risk.

The Brief/Today queue presents these high-severity, work-blocking Risks (and blocked Tasks) under the owner-facing “Đang bị kẹt / Blocked” group (F70 Rủi ro / Việc cần xử lý sections) — the four-state operating view is a rendering, not four tables. See §12 OQ1 on whether an additive blocker risk_type is worth proposing.


5. API Contract

No new endpoints. The ontology is consumed through existing surfaces:

MethodPathAuth/RoleRole of ontology
GET/api/pulses/profile/memberResolved profile (F83) carries extraction_context.signal_ids + entities + custom_vocab for the org’s industry_id.
GET/api/pulses/pack/signals/adminServes the signal catalog incl. the new field-service signals (read-only, F83).
POST/api/pulses/submit/memberExtraction (F08) reads PulseEntry.profile_snapshot.extraction_context to enrich keyword/LLM matching with field-service signals.
POST/api/chaos-import/memberThe wedge (F70) run_chaos_import() resolves the org ontology the same way and produces the Boss Brief.
GET/PATCH/api/pulses/config/adminSets industry_id, custom_company_vocab, signal_severity_overrides, materiality_threshold_vnd (F83).

Resolution path. F83 resolve_profile(user, org, pack) reads org.pulse_config.industry_id, selects the industry’s industry_signal_ids + entities from industries.json, layers custom_company_vocab + signal_severity_overrides, and writes the result into profile_snapshot at submit time (immutable audit trail, F83 OQ2). F08/F70 extraction reads that snapshot — they never re-resolve and never widen tenant scope.

Tenant scoping. Every query is scoped by organization_id=request.organization_id (F83/F08/F70 already enforce this). custom_company_vocab lives on the org’s own OrgPulseConfiguration (OneToOne with Organization); it is structurally impossible for one org’s vocab to be read while resolving another org’s profile — verified by the mandatory cross-tenant test (§7).

EvidenceSource + confidence. Every produced item links ≥1 EvidenceSource quoting the source line (Rule 2, F08 §2). Confidence is honest: 0.65–0.80 keyword, 0.85–0.90 LLM, never 1.0, never hardcoded. An ontology signal that produces an item with no evidence span is dropped, not shown (F70 hallucination guard).


6. UI / UX

No new routes or components. The ontology is invisible infrastructure; its UX is the quality of items appearing in existing surfaces:

  • Pulse (/intel/pulse, F83) — the industry-specific placeholder + hint chips already reference jobs/parts/visits when industry_id is field-service; this spec ensures the extracted result matches that vocabulary.
  • Boss Brief (/intel/import, F70) — extracted items render in the owner-facing groups Việc cần xử lý / Cần chốt với bên ngoài / Rủi ro / Tin nhắn nên gửi, mapping the four operating states (COMMITMENT/BLOCKER/DECISION/COMPLETED) onto the contractor’s view.
  • Brief / Today (F12) — the same items flow into the daily brief.

i18n. No new namespace. Trade vocabulary is data (the §4.3 lexicon files), not UI strings; it is not subject to the en/+vi/ locale-file rule (Rule 3) because it is not rendered chrome — it is matched against user text. Vietnamese-first per positioning doctrine, English alongside, with the pack’s per-locale fallback (F83 §11.5). Any owner-facing labels for the four operating states reuse existing F70/F12 keys.

DESIGN.md / TASTE.md. No component work; existing surfaces already comply.


7. Test Plan

All backend; the ontology is data + extraction behavior, exercised through F08/F70/F83. Backend coverage must stay ≥97% (CLAUDE.md) / ≥99% where the touched apps already sit.

#CaseTypeWhere
1VI: “board mạch chưa về” → missing_parts BLOCKER (Risk), evidence-linked, conf 0.65–0.80backendbackend/tests/test_field_service_ontology.py::TestVietnameseSignals
2EN: “the board hasn’t arrived” → missing_parts BLOCKER, evidence-linkedbackend…::TestEnglishSignals
3VI: “chưa cọc 50 triệu, quá 3 ngày” → deposit_overdue cash BLOCKER, money_amount capturedbackend…::TestDeposit
4VI: “phòng 3 xong, chờ nghiệm thu” → sign_off_pending DECISION — not COMPLETEDbackend…::TestSignOff::test_sign_off_not_marked_complete
5VI fit-out: “bản vẽ MEP chưa duyệt” → approval_blocker DECISION/blockerbackend…::TestFitOut
6Output-type mapping: each signal → exactly one of COMMITMENT/BLOCKER/DECISION/COMPLETEDbackend…::TestOutputTypeMapping
7BLOCKER maps to existing risk_type (delivery/cash/operations) — never invents blocker valuebackend…::TestOutputTypeMapping::test_blocker_uses_valid_risk_type
8Confidence range: keyword ∈ [0.65,0.80], LLM ∈ [0.85,0.90], never 1.0backend…::TestConfidence
9Cross-tenant: org A’s custom_company_vocab does not affect org B’s extractionbackendbackend/tests/test_multitenancy.py::TestOntologyVocabIsolation
10Per-org custom_company_vocab term resolves to canonical entity for owning orgbackend…::TestCustomVocab
11signal_severity_overrides raises a field-service signal’s severity for that org onlybackend…::TestSeverityOverride
12materiality_threshold_vnd suppresses a sub-threshold deposit as a cash blockerbackend…::TestMateriality
13No-industry fallback: blank industry_id → core signals only, logged WARNING, no fabricated field-service itembackend…::TestFallback
14New signals present in signal_catalog.json + selected by both beachhead industries in industries.jsonbackendbackend/tests/test_pulse_v2_p1.py::TestPackContent (extend)
15Every produced item links ≥1 EvidenceSource; unsourced item droppedbackend…::TestEvidence

No E2E describe block is added by this spec alone — the user-facing flows are already covered by F70 (daily-pulse-v2.spec.ts and the Chaos Import wedge E2E); when the new signals activate, those suites assert the field-service vocabulary produces the right brief sections.


8. Current Behavior

The framework and the first typed field-service content slice are shipped; the feature remains validation-gated until real contractor language proves the vocabulary.

  • daily_update_pack/config/signal_catalog.json defines the reusable signals, including the additive deposit_overdue, sign_off_pending, and warranty_claim entries.
  • daily_update_pack/config/industries.json already defines field_service_technical_maintenance and specialty_contracting_construction with industry_signal_ids, entities, kpis, and a Vietnamese free-form example.
  • backend/apps/ai/services/extraction_patterns.py carries the F84 Vietnamese-first and English trigger map; backend/apps/ai/field_service.py::enrich_field_service_signals maps selected signals to the existing spine and applies F86 grounding.
  • F83 resolve_profile() + profile_snapshot deliver the resolved signal set to F08/F70.

Remaining contract gaps:

  • The vocabulary has controlled fixture coverage but no consented, scrubbed real Vietnam contractor transcript validation yet.
  • The proposed vocab/*.json lexicon remains deliberately deferred while the pilot vocabulary is still changing; triggers remain in Python during this validation phase.
  • CRM deposit reconciliation and the sign-off-to-completed lifecycle remain owned/open elsewhere.

9. Error States

#ErrorBehavior
ES1Unknown industry_id on orgExtraction falls back to core signals (F83 ES4); admin warned in /settings/pulse-config; no field-service signal asserted. Logged WARNING.
ES2Signal id in industries.json missing from signal_catalog.jsonPulsePackLoader validation fails fast at startup (F83 ES1, ImproperlyConfigured) — CI catches the dangling reference before deploy.
ES3custom_company_vocab maps a term to a non-existent canonical entityThe mapping is ignored for extraction; logged WARNING; never crashes a submission.
ES4A field-service signal matches but no evidence span can be quotedItem is dropped, not shown (F70/F08 hallucination guard). Never fabricate.
ES5materiality_threshold_vnd = 0Treated as null/no threshold (F83 ES8).
ES6LLM returns a confidence > 0.90 for a field-service itemClamped to [0.85, 0.90] (F70 LLM path); never 1.0.
ES7Technician text claims completion without sign-offsign_off_pending DECISION produced; never a COMPLETED item — protects the “no invented sign-offs” gate.

10. Diagrams & Docs Touched

  • GLOSSARY.md — add: deposit (đặt cọc), sign-off / acceptance (nghiệm thu), warranty (bảo hành), SLA, báo giá (quote), revisit (quay lại), and the field-service signal set (missing_parts, sla_risk, deposit_overdue, sign_off_pending, repeat_failure, warranty_claim, site_access_problem, rework_required, job_delay, approval_blocker). Add an Industry Ontology entry (concept → spine mapping), and a note clarifying BLOCKER is a surfaced Risk/Task state, not a model or risk_type value.
  • DIAGRAMS.md — AI pipeline: annotate the F83 “Profile Resolution” step to note the field-service signal set + custom vocab feeding extraction.
  • backend/DIAGRAMS.md — only if §12 OQ1 is resolved to add an additive blocker risk_type; otherwise no state-machine change (the ontology adds no fields). The four-state view is a rendering, not a new FSM.
  • No new ADR required unless OQ1 (additive risk_type) or a new vocabulary-file loader is adopted — both are flagged in §12.

Depends on: F83 (data layer + resolver), F08 (extraction + confidence/evidence), F70 (the consuming wedge), F50 (deal deposit_* fields), F37 (LLM-mode multilingual NLU — closes the Vietnamese-only keyword gap).

11. Implementation Audit

Backend built (2026-06-24). The additive ontology content + the typed output-state extraction pass are implemented and test-green; status 🚧 (held out of shipped per the validation gate — vocabulary still awaits real-transcript validation, §intro).

Built (all additive, no removals, no new core models):

  • ✅ New signals deposit_overdue, sign_off_pending, warranty_claim added to daily_update_pack/config/signal_catalog.json; selected by both beachhead industries in industries.json (field-service gets all three + approval_blocker; fit-out gets deposit_overdue + sign_off_pending). Pack loader validation passes (no dangling references). Test: tests/test_pulse_v2_p1.py::TestPackContent.
  • ✅ Trade-vocabulary triggers (VI + EN) for the §4.2 signals extended in apps/ai/services.py::_SIGNAL_KEYWORD_MAP — closes the F08 §12 Vietnamese-only gap for the beachhead.
  • ✅ The §4.1/§4.2/§4.5 output-type mapping implemented as the single source of truth in apps/ai/field_service.py::FIELD_SERVICE_MAP + enrich_field_service_signals(), invoked from extract_pulse_entry. Each field-service signal produces the correct spine object (Risk with a valid existing risk_type — never blocker; DecisionRequest; Commitment; closed-with-sign-off Commitment + revenue-leakage Risk).
  • ✅ Per-org overrides reuse F83 fields (no new fields): custom_company_vocab (tenant-scoped slang→entity), signal_severity_overrides, materiality_threshold_vnd.
  • ✅ Faithfulness reuses the F86 guard (faithfulness.check) via the caller’s _grounded wrapper — evidence on every item, honest confidence (0.7 keyword, clamped to [0.60,0.80]), never 1.0; unsourced items dropped.
  • ✅ Honesty rule (§9 ES7): “xong rồi, chờ nghiệm thu” → sign_off_pending DECISION, never COMPLETED. ES1 fallback logs WARNING field_service_ontology_inactive when no field-service signals are active.

Tests (§7): all rows implemented and green — tests/test_field_service_ontology.py (rows 1–8, 10–13, 15 + secondary-item, custom-vocab edge, company-link, drop-path coverage, 31 tests, 98% module coverage), tests/test_multitenancy.py::TestOntologyVocabIsolation (row 9, mandatory cross-tenant), tests/test_pulse_v2_p1.py::TestPackContent (row 14).

Deferred (open questions, not regressions):

  • OQ1 — no blocker risk_type added (default kept; BLOCKER maps to existing types).
  • OQ2 — triggers remain in _SIGNAL_KEYWORD_MAP (Python) per the validation-phase lean; migration to vocab/*.json data files is post-pilot.
  • OQ3 — deposit_overdue produces a pulse-text cash Risk; CRM deal reconciliation (F50) not yet linked.
  • OQ4 — sign_off_pending → COMPLETED transition is steady-state, out of this slice.

What exists and constrains the design (must reuse, no parallel system):

  • ✅ F83 data layer (daily_update_pack/, PulsePackLoader, signal_catalog.json, industries.json) and OrgPulseConfiguration (incl. custom_company_vocab, signal_severity_overrides, materiality_threshold_vnd) — shipped. Use these; do not add config models.
  • ✅ F08 extraction engine + evidence/confidence policy + _SIGNAL_KEYWORD_MAP / _build_industry_risk_patterns — shipped (mock) / LLM in F70. Extend the keyword map and consume the resolved profile; do not fork the engine.
  • ✅ The two beachhead industry entries in industries.json already select the core field-service signals — present.

What this spec adds when built (all additive, no removals, no new core models):

  1. New signals deposit_overdue, sign_off_pending, warranty_claim in signal_catalog.json and selection by both beachhead industries in industries.json.
  2. Trade-vocabulary triggers (VI + EN) for the §4.2 signals — initially extending _SIGNAL_KEYWORD_MAP (mock mode), ideally migrated to the §4.3 lexicon data files.
  3. The §4.1 entity→spine mapping enforced in extraction (which model/risk_type each signal produces) and tested as the §7 output-type contract.

Hard constraints (rails):

  • Reuse the spine. Commitment / Risk / DecisionRequest / Task / EvidenceSource / OrganizationMember / crm.Company only. No Job, Deposit, SignOff, SLA, Warranty tables.
  • risk_type has no blocker value today — map BLOCKER to delivery/cash/operations or a blocked Task. Adding a blocker choice is an open question, not a default (§12 OQ1).
  • Confidence honest (0.65–0.80 kw / 0.85–0.90 LLM, never 1.0), evidence on every item, tenant scope never widened.
  • English triggers required for the beachhead — the F08 §12 Vietnamese-only keyword gap must not be inherited; LLM mode (F37) is the durable fix, keyword EN is the interim floor.

12. Open Questions / Decisions

#QuestionStatus
OQ1Should an additive blocker value be added to Risk.risk_type, or is mapping to delivery/cash/operations + high severity sufficient?Open. Default: reuse existing types (no migration). An additive choice would touch backend/DIAGRAMS.md + GLOSSARY and needs a small ADR — only if the four-state rendering proves it needs a first-class type. Do not add silently.
OQ2Should trade vocabulary move from _SIGNAL_KEYWORD_MAP (Python) to the §4.3 vocab/*.json data files now, or after pilot validation?Open. Lean: keep in Python for the validation phase (fast iteration), migrate to data once the lexicon stabilizes against real transcripts. A new loader is a small ADR if adopted.
OQ3Does deposit_overdue extraction reconcile with the existing F50 CRM deal deposit_paid/deposit_due_date logic, or run independently from pulse text?Open. Prefer: pulse-text deposit_overdue produces a cash Risk; if a matching CRM deal exists, link via Risk.company. Avoid double-counting with F50’s brief deal-surfacing.
OQ4How is sign_off_pending → COMPLETED transition triggered — does a later pulse (“khách đã nghiệm thu”) close the prior DECISION/Task?Open. Likely a steady-state (F83 follow-up) concern; out of this spec’s first slice.
OQ5Which subsegment (HVAC/MEP vs fit-out) becomes PRIMARY — affects which vocabulary set is hardened first.Pilot decides (pitch EVIDENCE slide). Both authored in parallel; the data, not the deck, picks the wedge.

13. Bug / Change Log

2026-07-14 — Contract Graph migration and implementation-truth correction

Type: Contract Graph / Documentation Truth Fix

Summary:

  • Added the canonical contracts/features/F84.cue contract for the typed mapping, bilingual vocabulary, pack integrity, tenant-scoped overrides, and safe fallback behavior.
  • Bound the contract to collected pytest proof IDs for signal mapping, grounding, custom vocabulary, tenant isolation, and pack consistency.
  • Replaced stale pre-implementation wording with the shipped backend boundary and kept validation, lexicon, CRM reconciliation, and sign-off lifecycle gaps explicit.

2026-07-02 — Spec drift corrected (batch 5)

Type: Docs

Summary:

  • Status updated from draft to in progress (backend shipped 2026-06-24, validation-gated) to match §11 (backend implemented 2026-06-24; held at 🚧 per the pilot-validation gate).
  • §2 ACs marked [x] per §11 audit:
    • 2.1 AC1: missing_parts BLOCKER with evidence and conf range — confirmed by §7 row 1 (green).
    • 2.1 AC2: commitment_due COMMITMENT produced — confirmed by §7 row 6 output-type mapping (green).
    • 2.1 AC3: sign_off_pending DECISION, never COMPLETED — confirmed by §11 honesty rule + §7 row 4 (green).
    • 2.2 AC1: approval_blocker fit-out DECISION/blocker — confirmed by §7 row 5 (green).
    • 2.3 AC1: four-state output-type mapping, no unmapped fifth state — confirmed by §7 row 6 (green).
    • 2.3 AC2: deposit_overdue cash BLOCKER with amount — confirmed by §7 row 3 (green).
    • 2.4 AC1: custom_company_vocab resolves for owning org only — confirmed by §7 row 10 (green).
    • 2.4 AC2: cross-tenant vocab isolation — confirmed by TestOntologyVocabIsolation §7 row 9 (green).
    • 2.5 AC1: blank industry_id fallback with WARNING log — confirmed by §7 row 13 (green).
  • §2.2 AC2 left [ ]: signals scope_change/customer_approval_delay do not appear in the §4.2 signal taxonomy; no matching §7 test row confirms them implemented.

2026-06-26 — Risk pattern expansion aligned with F08 vocabulary pass

Type: Feature (extraction quality)

Summary:

  • Extended _RISK_PATTERNS in apps/ai/services.py with field-service–specific patterns:
    • Supply in transit: chờ hàng, chờ vật tư, chờ linh kiện, chờ giao, chờ về — parts ordered but awaiting delivery, previously classified as generic delay; now surfaces as missing_parts risk via enrich_field_service_signals.
    • Workforce unavailability: cách ly, bị cách ly, bị thương, nhập viện — technician quarantine/injury blocks site visits; maps to operations risk type.
    • Workload capacity: quá tải, không đủ nhân sự, thiếu người — site demand exceeds headcount.
    • Order cancellation: hủy, hủy đơn, khách hủy → customer risk type (via _infer_risk_type).
    • Financial penalty: bị phạt, phạt tiền, tiền phạt → cash risk type.
  • _DECISION_PATTERNS expanded with authority-gap phrases (không có quyền, vượt thẩm quyền) — captures field-team blocker that requires CEO/manager decision.
  • _infer_risk_type updated: penalty → cash, cancellation → customer.
  • All expansions additive — no changes to FIELD_SERVICE_MAP or signal_catalog.json; the vocabulary augments the baseline keyword layer that F84 built on.

Tests: test_pulse_v2_p3.py (new test classes for each pattern group); test_field_service_ontology.py existing tests unchanged and green.

2026-06-24 — Implemented F84 (backend + docs)

Type: Feature

Summary:

  • Shipped the additive field-service ontology content and the typed four-state extraction pass. Added signals deposit_overdue, sign_off_pending, warranty_claim to the pack and selected them in both beachhead industries. Extended _SIGNAL_KEYWORD_MAP with VI+EN trade vocabulary for the §4.2 signals. Added apps/ai/field_service.py (FIELD_SERVICE_MAP single-source mapping + enrich_field_service_signals), invoked from extract_pulse_entry so each field-service signal lands on the correct spine object.

Reason:

  • F83 built the framework but the beachhead vocabulary, entities, and typed output-state mapping were undefined — the Brief/F70 wedge starved on signal quality. F84 is that content contract, now implemented.

Impact:

  • No new core models. No blocker risk_type. BLOCKER maps to existing risk_type values (delivery/cash/operations/revenue + high/critical severity). COMMITMENT→Commitment, DECISION→DecisionRequest, COMPLETED→closed Commitment + revenue-leakage Risk. Per-org overrides reuse F83 fields. Every item is grounded via the F86 guard with honest confidence (never 1.0).
  • Behavior change: field-service-mapped signals are now handled exclusively by the typed F84 pass and EXCLUDED from the generic risk-pattern enrichment, so a single field-service blocker no longer also creates a generic medium-severity risk.
  • Honesty: a technician “xong rồi / done” without sign-off yields sign_off_pending DECISION, never a COMPLETED (§9 ES7). No-industry orgs log field_service_ontology_inactive WARNING and fall back to core signals (ES1).

Tests:

  • tests/test_field_service_ontology.py (31 tests, 98% module coverage), tests/test_multitenancy.py::TestOntologyVocabIsolation (mandatory cross-tenant), tests/test_pulse_v2_p1.py::TestPackContent. Full backend suite green; ruff clean.

Notes:

  • Status held at 🚧 per the validation gate: vocabulary still awaits real Vietnam contractor transcript validation before advancing to shipped. Open questions OQ1–OQ4 deferred (see §11/§12).

2026-06-24 — Spec created

Type: Feature

Summary:

  • New spec F84 defining the field-service industry ontology: the entity→spine mapping and signal taxonomy (with VI+EN trade vocabulary, output-type mapping, default severity, honest confidence) for the beachhead verticals HVAC/MEP maintenance (PRIMARY) and interior fit-out (PARALLEL TEST).

Reason:

  • The pitch names “Industry ontology — service calls, job status, SLAs, parts, deposit, sign-off, understood by default” as a core pillar of turning noise into signal. F83 built the framework (OrgPulseConfiguration.industry_id, the daily_update_pack data layer, _SIGNAL_KEYWORD_MAP) but no spec defined the actual field-service vocabulary, entities, and signal taxonomy. F84 is that content contract.

Impact:

  • No new core models — the ontology maps onto the existing spine (Commitment/Risk/ DecisionRequest/Task/EvidenceSource/crm.Company) and lives as data in the F83 daily_update_pack layer + per-org OrgPulseConfiguration overrides.
  • Adds (when built) signals deposit_overdue, sign_off_pending, warranty_claim and their trade-vocabulary triggers; defines the four-state (COMMITMENT/BLOCKER/DECISION/COMPLETED) output mapping as a tested contract.
  • Flags two codebase constraints discovered during authoring: (1) Risk.risk_type has no blocker value — BLOCKER is a surfaced Risk/Task state, not a model or type; (2) the F08 _SIGNAL_KEYWORD_MAP is Vietnamese-only and must gain English triggers for the beachhead.

Tests:

  • Test plan defined (§7); not yet implemented. New backend/tests/test_field_service_ontology.py
    • cross-tenant vocab-isolation case in test_multitenancy.py + pack-content assertions in test_pulse_v2_p1.py.

Notes:

  • Validation-gated: status stays draft until the beachhead vocabulary validates against real Vietnam contractor chat transcripts (HVAC/MEP + fit-out). The pilot picks the words and the PRIMARY subsegment — not the deck. Build the two vocabulary sets in parallel.