Spec: F84 — Field-Service Industry Ontology
The vocabulary contract for the beachhead verticals. F83 built the framework for industry-aware extraction (
OrgPulseConfiguration.industry_id, thedaily_update_packdata 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/shippeduntil 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 signalmissing_parts,severity=high, anEvidenceSourcequoting the line, andconfidence_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 signalcommitment_due, evidence-linked. - Given “phòng 3 xong rồi, chờ nghiệm thu”, then a
sign_off_pendingsignal is produced as a DECISION/blocker awaiting customer acceptance — never silently marked complete (sign-off is a customer act, not a technician claim).
- Given org
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_blockerBLOCKER (Risk) is produced, evidence-linked, severityhigh. - Given “cần chủ đầu tư xác nhận phát sinh trước 11h mai”, then a DECISION
(
DecisionRequest) is produced with signalscope_change/customer_approval_delay.
- Given “chưa thi công trần vì bản vẽ MEP chưa được duyệt”, then an
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_overduesignal 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"}tocustom_company_vocab(F83 field), when a member’s pulse uses that term, then it resolves to thepartentity /missing_partssignal 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).
- Given admin adds
2.5 Fallback — no industry, no breakage
- As a member of an org with no
industry_idset, I want extraction to still work, so that the absence of configuration never produces a blank or wrong brief.- Given
industry_idis 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.
- Given
3. Scope Boundaries
In:
- The entity map and signal taxonomy for the two beachhead verticals:
field_service_technical_maintenance(HVAC/MEP maintenance — PRIMARY) andspecialty_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 inbackend/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_vocabandsignal_severity_overrides— no new override mechanism. - The vocabulary lexicon file (proposed, §4.3) that authors trade vocabulary as data
under the existing
daily_update_packlayer.
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 id | VI label | EN label | Maps to (existing) | Notes |
|---|---|---|---|---|
job / work_order | công việc / lệnh sửa | service call / job | work.Task (+ Project for fit-out) | A dispatched site visit = a Task; a fit-out package = a Project of Tasks. |
site | công trình / hiện trường | site | work.Task.location + free-text on item; or crm.Company address | No Site model — site is an attribute, not an entity table. |
part / material | vật tư / linh kiện / board mạch | part / material | (no model) — surfaced via missing_parts Risk + evidence | A missing part is a blocker signal, not a stored inventory row (inventory is out of scope here). |
customer | khách hàng / chủ đầu tư | customer | crm.Company (Relationship spine) | Reuse CRM; never a parallel customer table. |
deposit (đặt cọc) | đặt cọc / tiền cọc | deposit | Risk (risk_type=cash, money_amount) + CRM deal deposit_* fields | Overdue 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á | quote | Commitment (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ành | sign-off / acceptance | DecisionRequest (awaiting) → Task/Commitment closed-with-evidence (done) | Sign-off is a customer act that gates COMPLETED. |
warranty (bảo hành) | bảo hành | warranty | Risk (risk_type=customer) or Commitment | A warranty claim/return = customer risk + a commitment to revisit. |
sla | SLA / cam kết thời gian | SLA | Risk (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 2 | return visit / revisit | work.Task (follow-up) + Commitment | Promised 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 id | VI trigger vocabulary (examples) | EN trigger vocabulary | Output type | Default severity | Confidence (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” | BLOCKER | high | 0.65–0.75 / 0.85–0.90 | exists |
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” | BLOCKER | high | 0.65–0.75 / 0.85–0.90 | exists |
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) | high | 0.65–0.75 / 0.85–0.90 | NEW |
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” | DECISION | medium | 0.65–0.75 / 0.85–0.90 | NEW |
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” | BLOCKER | high | 0.65–0.75 / 0.85–0.90 | exists |
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 risk | medium | 0.65–0.75 / 0.85–0.90 | NEW |
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” | BLOCKER | medium | 0.65–0.75 / 0.85–0.90 | exists |
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 COMMITMENT | high | 0.65–0.75 / 0.85–0.90 | exists |
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” | BLOCKER | high | 0.65–0.75 / 0.85–0.90 | exists |
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” | DECISION | high | 0.65–0.75 / 0.85–0.90 | exists |
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” | COMMITMENT | high | 0.65–0.75 / 0.85–0.90 | exists |
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) | high | 0.65–0.75 / 0.85–0.90 | exists |
Honesty note on
sign_off_pendingvs 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 asign_off_pendingDECISION/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) — gatesdeposit_overdueand 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
Riskwith an appropriate existingrisk_type(deliveryfor parts/SLA/ job delay,cashfor deposits,operationsfor site access) andseverity ∈ {high, critical}, optionally linked to a blockedTaskviaRisk.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:
| Method | Path | Auth/Role | Role of ontology |
|---|---|---|---|
GET | /api/pulses/profile/ | member | Resolved profile (F83) carries extraction_context.signal_ids + entities + custom_vocab for the org’s industry_id. |
GET | /api/pulses/pack/signals/ | admin | Serves the signal catalog incl. the new field-service signals (read-only, F83). |
POST | /api/pulses/submit/ | member | Extraction (F08) reads PulseEntry.profile_snapshot.extraction_context to enrich keyword/LLM matching with field-service signals. |
POST | /api/chaos-import/ | member | The wedge (F70) run_chaos_import() resolves the org ontology the same way and produces the Boss Brief. |
GET/PATCH | /api/pulses/config/ | admin | Sets 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 whenindustry_idis 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.
| # | Case | Type | Where |
|---|---|---|---|
| 1 | VI: “board mạch chưa về” → missing_parts BLOCKER (Risk), evidence-linked, conf 0.65–0.80 | backend | backend/tests/test_field_service_ontology.py::TestVietnameseSignals |
| 2 | EN: “the board hasn’t arrived” → missing_parts BLOCKER, evidence-linked | backend | …::TestEnglishSignals |
| 3 | VI: “chưa cọc 50 triệu, quá 3 ngày” → deposit_overdue cash BLOCKER, money_amount captured | backend | …::TestDeposit |
| 4 | VI: “phòng 3 xong, chờ nghiệm thu” → sign_off_pending DECISION — not COMPLETED | backend | …::TestSignOff::test_sign_off_not_marked_complete |
| 5 | VI fit-out: “bản vẽ MEP chưa duyệt” → approval_blocker DECISION/blocker | backend | …::TestFitOut |
| 6 | Output-type mapping: each signal → exactly one of COMMITMENT/BLOCKER/DECISION/COMPLETED | backend | …::TestOutputTypeMapping |
| 7 | BLOCKER maps to existing risk_type (delivery/cash/operations) — never invents blocker value | backend | …::TestOutputTypeMapping::test_blocker_uses_valid_risk_type |
| 8 | Confidence range: keyword ∈ [0.65,0.80], LLM ∈ [0.85,0.90], never 1.0 | backend | …::TestConfidence |
| 9 | Cross-tenant: org A’s custom_company_vocab does not affect org B’s extraction | backend | backend/tests/test_multitenancy.py::TestOntologyVocabIsolation |
| 10 | Per-org custom_company_vocab term resolves to canonical entity for owning org | backend | …::TestCustomVocab |
| 11 | signal_severity_overrides raises a field-service signal’s severity for that org only | backend | …::TestSeverityOverride |
| 12 | materiality_threshold_vnd suppresses a sub-threshold deposit as a cash blocker | backend | …::TestMateriality |
| 13 | No-industry fallback: blank industry_id → core signals only, logged WARNING, no fabricated field-service item | backend | …::TestFallback |
| 14 | New signals present in signal_catalog.json + selected by both beachhead industries in industries.json | backend | backend/tests/test_pulse_v2_p1.py::TestPackContent (extend) |
| 15 | Every produced item links ≥1 EvidenceSource; unsourced item dropped | backend | …::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.jsondefines the reusable signals, including the additivedeposit_overdue,sign_off_pending, andwarranty_claimentries.daily_update_pack/config/industries.jsonalready definesfield_service_technical_maintenanceandspecialty_contracting_constructionwithindustry_signal_ids,entities,kpis, and a Vietnamese free-form example.backend/apps/ai/services/extraction_patterns.pycarries the F84 Vietnamese-first and English trigger map;backend/apps/ai/field_service.py::enrich_field_service_signalsmaps selected signals to the existing spine and applies F86 grounding.- F83
resolve_profile()+profile_snapshotdeliver 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/*.jsonlexicon 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
| # | Error | Behavior |
|---|---|---|
| ES1 | Unknown industry_id on org | Extraction falls back to core signals (F83 ES4); admin warned in /settings/pulse-config; no field-service signal asserted. Logged WARNING. |
| ES2 | Signal id in industries.json missing from signal_catalog.json | PulsePackLoader validation fails fast at startup (F83 ES1, ImproperlyConfigured) — CI catches the dangling reference before deploy. |
| ES3 | custom_company_vocab maps a term to a non-existent canonical entity | The mapping is ignored for extraction; logged WARNING; never crashes a submission. |
| ES4 | A field-service signal matches but no evidence span can be quoted | Item is dropped, not shown (F70/F08 hallucination guard). Never fabricate. |
| ES5 | materiality_threshold_vnd = 0 | Treated as null/no threshold (F83 ES8). |
| ES6 | LLM returns a confidence > 0.90 for a field-service item | Clamped to [0.85, 0.90] (F70 LLM path); never 1.0. |
| ES7 | Technician text claims completion without sign-off | sign_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 orrisk_typevalue. - 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
blockerrisk_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_claimadded todaily_update_pack/config/signal_catalog.json; selected by both beachhead industries inindustries.json(field-service gets all three +approval_blocker; fit-out getsdeposit_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 fromextract_pulse_entry. Each field-service signal produces the correct spine object (Risk with a valid existingrisk_type— neverblocker; 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_groundedwrapper — 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_pendingDECISION, never COMPLETED. ES1 fallback logs WARNINGfield_service_ontology_inactivewhen 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
blockerrisk_typeadded (default kept; BLOCKER maps to existing types). - OQ2 — triggers remain in
_SIGNAL_KEYWORD_MAP(Python) per the validation-phase lean; migration tovocab/*.jsondata files is post-pilot. - OQ3 —
deposit_overdueproduces 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) andOrgPulseConfiguration(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.jsonalready select the core field-service signals — present.
What this spec adds when built (all additive, no removals, no new core models):
- New signals
deposit_overdue,sign_off_pending,warranty_claiminsignal_catalog.jsonand selection by both beachhead industries inindustries.json. - 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. - The §4.1 entity→spine mapping enforced in extraction (which model/
risk_typeeach signal produces) and tested as the §7 output-type contract.
Hard constraints (rails):
- Reuse the spine.
Commitment/Risk/DecisionRequest/Task/EvidenceSource/OrganizationMember/crm.Companyonly. NoJob,Deposit,SignOff,SLA,Warrantytables. risk_typehas noblockervalue today — map BLOCKER todelivery/cash/operationsor a blocked Task. Adding ablockerchoice 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
| # | Question | Status |
|---|---|---|
| OQ1 | Should 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. |
| OQ2 | Should 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. |
| OQ3 | Does 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. |
| OQ4 | How 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. |
| OQ5 | Which 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.cuecontract 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
drafttoin 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_partsBLOCKER with evidence and conf range — confirmed by §7 row 1 (green). - 2.1 AC2:
commitment_dueCOMMITMENT produced — confirmed by §7 row 6 output-type mapping (green). - 2.1 AC3:
sign_off_pendingDECISION, never COMPLETED — confirmed by §11 honesty rule + §7 row 4 (green). - 2.2 AC1:
approval_blockerfit-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_overduecash BLOCKER with amount — confirmed by §7 row 3 (green). - 2.4 AC1:
custom_company_vocabresolves 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_idfallback with WARNING log — confirmed by §7 row 13 (green).
- 2.1 AC1:
- §2.2 AC2 left
[ ]: signalsscope_change/customer_approval_delaydo 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_PATTERNSinapps/ai/services.pywith 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 asmissing_partsrisk viaenrich_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 tooperationsrisk 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→customerrisk type (via_infer_risk_type). - Financial penalty:
bị phạt,phạt tiền,tiền phạt→cashrisk type.
- Supply in transit:
_DECISION_PATTERNSexpanded 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_typeupdated: penalty →cash, cancellation →customer.- All expansions additive — no changes to
FIELD_SERVICE_MAPorsignal_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_claimto the pack and selected them in both beachhead industries. Extended_SIGNAL_KEYWORD_MAPwith VI+EN trade vocabulary for the §4.2 signals. Addedapps/ai/field_service.py(FIELD_SERVICE_MAPsingle-source mapping +enrich_field_service_signals), invoked fromextract_pulse_entryso 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
blockerrisk_type. BLOCKER maps to existingrisk_typevalues (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_pendingDECISION, never a COMPLETED (§9 ES7). No-industry orgs logfield_service_ontology_inactiveWARNING 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 toshipped. 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, thedaily_update_packdata 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 F83daily_update_packlayer + per-orgOrgPulseConfigurationoverrides. - Adds (when built) signals
deposit_overdue,sign_off_pending,warranty_claimand 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_typehas noblockervalue — BLOCKER is a surfaced Risk/Task state, not a model or type; (2) the F08_SIGNAL_KEYWORD_MAPis 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 intest_pulse_v2_p1.py.
- cross-tenant vocab-isolation case in
Notes:
- Validation-gated: status stays
draftuntil 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.