<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Reliability on Boris Dupoux</title><link>https://borisdupoux.com/tags/reliability/</link><description>Recent content in Reliability on Boris Dupoux</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Mon, 01 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://borisdupoux.com/tags/reliability/index.xml" rel="self" type="application/rss+xml"/><item><title>Deterministic Process Automation Under Strong Security Constraints</title><link>https://borisdupoux.com/research/deterministic-rpa/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://borisdupoux.com/research/deterministic-rpa/</guid><description>&lt;h2 id="context"&gt;&lt;a href="#context" class="header-anchor"&gt;&lt;/a&gt;Context
&lt;/h2&gt;&lt;p&gt;A client operating under strong security constraints manages several hundred
sites and runs four recurring back-office processes every day: maintaining field
equipment, handling recurring spurious equipment events, writing intervention
reports for its customers, and checking that each site&amp;rsquo;s periodic health check
has arrived. The inputs are scattered across three systems with very different
access paths: PDF reports delivered by e-mail, a business web portal with no
public API, and Excel workbooks maintained by hand over many years.&lt;/p&gt;
&lt;p&gt;The goal: automate these processes on an office workstation while keeping a
human operator as the final authority on every customer message.&lt;/p&gt;
&lt;h2 id="design-constraints"&gt;&lt;a href="#design-constraints" class="header-anchor"&gt;&lt;/a&gt;Design constraints
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No language model at run time.&lt;/strong&gt; Every decision comes from explicit rules
and every sentence from a template. This makes the robot&amp;rsquo;s output
reproducible: the same inputs always produce the same drafts, a design meant to
allow word-for-word comparison of a new version against production; any change traces back
to a rule or a data source. No customer data goes to an external model
service. A vision-language model was used once, offline, during design, to map
recorded operator screen sessions; it is not part of the deployed system.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drafts, not sends.&lt;/strong&gt; The robot only creates Outlook drafts; the operator
reviews and sends them. A global kill switch forces draft mode regardless of
per-process settings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local execution, no new infrastructure.&lt;/strong&gt; Python on a Windows workstation,
scheduled tasks, a shared NAS folder for state and configuration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No plaintext secrets.&lt;/strong&gt; Credentials and tokens live in the Windows
Credential Manager (DPAPI); configuration is YAML the operator can edit
without a rebuild.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="architecture"&gt;&lt;a href="#architecture" class="header-anchor"&gt;&lt;/a&gt;Architecture
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TB
 SCHED["Scheduled passes (morning / afternoon)"] --&gt; ING
 MAIL["Mailbox (Microsoft Graph, read)"] --&gt; ING["Ingestion PDF reports grouped by day"]
 ING --&gt; PARSE["PDF parser (pypdf + regular expressions)"]
 PORTAL["Business web portal Playwright login → session token → JSON endpoints replayed"] --&gt; RULES
 XLS["Reference &amp; history workbooks (Excel)"] --&gt; RULES
 PARSE --&gt; RULES["Rule engine 4 processes, ordered rules &amp; exclusions"]
 RULES --&gt; TPL["Template composer French agreement, names, dates"]
 TPL --&gt; DRAFTS["Outlook drafts (Graph, never sent)"]
 RULES --&gt; TRACK["Tracking workbooks (atomic append)"]
 DRAFTS --&gt; OP(["Operator reviews &amp; sends"])
 OP -. "sent items: corrected recipients" .-&gt; RULES
 NAS[("NAS: locks, heartbeats, journal, idempotence ledger")] --- RULES
 RULES --&gt; HEALTH["Health alerts webhook + dead-man check"]&lt;/pre&gt;&lt;h2 id="the-four-processes"&gt;&lt;a href="#the-four-processes" class="header-anchor"&gt;&lt;/a&gt;The four processes
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Equipment maintenance.&lt;/strong&gt; Fault lines parsed from the daily PDF reports are
matched against each device&amp;rsquo;s battery and repair history. An ordered decision
tree (first match wins) chooses between ignoring, waiting, battery replacement,
device replacement or radio-range testing, and drafts a technician work order;
each device on a multi-device order keeps its own decision.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Recurring spurious events.&lt;/strong&gt; Event counts per site and per device over 24
hours are compared with thresholds to detect recurring spurious events. Portal
data (an incident confirmed as real, the system being switched off minutes
after a burst) overrides the counts; the outcome is an advisory e-mail to the
customer, a work order, or nothing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Intervention reports.&lt;/strong&gt; For each on-site intervention, the robot rebuilds a
single timeline from the portal&amp;rsquo;s event log, call log and field-staff records,
handles events that straddle midnight, and drafts a customer report with the
media files attached.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Periodic health checks.&lt;/strong&gt; Sites whose periodic health check is missing are
identified, the status of each connectivity channel is checked on the portal,
and a customer e-mail, a technician work order or an internal note is drafted.&lt;/p&gt;
&lt;h2 id="composing-an-e-mail-from-parameters-to-a-draft"&gt;&lt;a href="#composing-an-e-mail-from-parameters-to-a-draft" class="header-anchor"&gt;&lt;/a&gt;Composing an e-mail: from parameters to a draft
&lt;/h2&gt;&lt;p&gt;Each draft is the end of a long chain of explicit decisions. Counted in the
source code (October 2026), the four rule engines have &lt;strong&gt;53 terminal outcomes&lt;/strong&gt;
(each place where a rule returns a decision), mapped onto &lt;strong&gt;37 decision types&lt;/strong&gt;
and &lt;strong&gt;13 draft builders&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The customer advisory e-mail of the spurious-events process shows the scale. A
site reaches it only after up to &lt;strong&gt;14 ordered tests&lt;/strong&gt; in the rule engine (16
terminal outcomes for this process alone). The composer then reads &lt;strong&gt;about twenty
inputs&lt;/strong&gt; — 17 fields of the site&amp;rsquo;s decision record and customer record, the
recipient list, the attachments, and two learned resources (zone names from the
portal and a spelling dictionary learned from the operator&amp;rsquo;s own history
workbook). Between the decision and the final text there are &lt;strong&gt;about 100
conditional branch points&lt;/strong&gt; (&lt;code&gt;if&lt;/code&gt; statements, conditional expressions and boolean operands,
counted on the syntax tree of the composer and the ten helpers it calls, before
the HTML pass).&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TB
 IN(["1 · Rule engine — 14 ordered tests, first match wins. Input: daily event reports · portal journal · customer record · learned site profile"]) --&gt; T0

 T0["Blocking flag on the site record?"] -- yes --&gt; X0["No e-mail"]
 T0 -- no --&gt; T1["R6 · incident confirmed as real today?"]
 T1 -- yes --&gt; X1["No e-mail"]
 T1 -- no --&gt; T2["R17 · media events &amp;lt; 30 s apart across zones?"]
 T2 -- yes --&gt; X2["Manual review"]
 T2 -- no --&gt; T3["R12 · external cause confirmed on media?"]
 T3 -- no --&gt; T4["R8 · ≥ 8 integrity events?"]
 T4 -- yes --&gt; T4b["persisting ≥ 3 days?"]
 T4b -- yes --&gt; X3["Technician work order"]
 T4b -- no --&gt; X4["Keep watching"]
 T4 -- no --&gt; T5["≥ 2 integrity events, ≥ half of the burst?"]
 T5 -- yes --&gt; X5["No e-mail"]
 T5 -- no --&gt; T6["R16 · ≥ 140 events, advisory ignored ≥ 4 weeks? (off by default)"]
 T6 -- yes --&gt; X6["Billing notice"]
 T6 -- no --&gt; T7["R15 · shared-building flag?"]
 T7 -- yes --&gt; X7["Coordination e-mail"]
 T7 -- no --&gt; T8["R5 · weekend + chronic-device flag?"]
 T8 -- yes --&gt; X8["Temporary masking request"]
 T8 -- no --&gt; T9["R1 · same device ≥ 7 events?"]
 T9 -- no --&gt; T10["R2 · ≥ 5 devices and ≥ 8 events?"]
 T10 -- no --&gt; T11["R4 · ≥ 8 events, ≥ 50 % 05:00–07:00?"]
 T11 -- no --&gt; T12["R3 · ≥ 8 events, ≥ 60 % 12:00–18:00, ≥ 70 % in 4 h?"]
 T12 -- no --&gt; T13["R1bis · site total ≥ 7 events?"]
 T13 -- no --&gt; X9["No e-mail"]

 T3 &amp; T9 &amp; T10 &amp; T11 &amp; T12 &amp; T13 -- yes --&gt; ADV

 ADV(["2 · Composer — advisory e-mail, rule id carried along. Customer name: legal form removed, re-cased, acronyms kept"])
 ADV --&gt; SUBJ["≥ 30 events?"]
 SUBJ -- yes --&gt; S1["Subject with urgent prefix"]
 SUBJ -- no --&gt; S2["Plain subject"]
 S1 &amp; S2 --&gt; GR["Contact cell"]
 GR -- "several people, or a role / organisation word" --&gt; G1["Generic greeting"]
 GR -- "title + surname" --&gt; G2["Greeting + normalised title (7 abbreviations) + SURNAME"]
 GR -- "surname only" --&gt; G3["Greeting + SURNAME"]
 G1 &amp; G2 &amp; G3 --&gt; DEV["Devices involved"]
 DEV -- one --&gt; D1["Portal zone name if known, else cleaned label + jargon map + learned accents"]
 DEV -- several --&gt; D2["Generic phrase + list of ≤ 6 by count, each with its device code if known"]
 D1 &amp; D2 --&gt; HND["System switched on/off within minutes of the burst? (R1–R4 only)"]
 HND -- "off just after" --&gt; C1["Handling-error wording A"]
 HND -- "on just before" --&gt; C2["Handling-error wording B"]
 HND -- no --&gt; ACT["Site in activity? — all of: R1–R4 or R1bis · ≥ 2 devices · ≥ 10 events · no intervention in the window · professional site · ≥ 90 % in learned hours · media sensors only: ≥ 90 % while on"]
 ACT -- yes --&gt; C3["Site-in-activity wording"]
 ACT -- no --&gt; RUL["Rule that fired"]
 RUL -- "R1, R2" --&gt; C4["Generic environmental cause"]
 RUL -- "R3, R4" --&gt; C5["Staff-passage cause"]
 RUL -- R12 --&gt; C6["External cause confirmed on media"]
 RUL -- R1bis --&gt; C7["Cause to investigate on site"]
 C4 &amp; C5 --&gt; ATT["Media actually attached?"]
 ATT -- yes --&gt; A1["+ reference to the attachments"]
 ATT -- no --&gt; FLG
 A1 &amp; C1 &amp; C2 &amp; C3 &amp; C6 &amp; C7 --&gt; FLG["Site flag: frequent contractor works?"]
 FLG -- yes --&gt; F1["Request: switch off before works"]
 FLG -- no --&gt; F2["Request: treat the cause"]
 F1 &amp; F2 --&gt; REC["Addresses in the customer record?"]
 REC -- yes --&gt; RC1["All addresses found"]
 REC -- no --&gt; RC2["Internal fallback mailbox"]
 RC1 &amp; RC2 --&gt; DRAFT(["Plain text → Outlook HTML (unfolding, lists, fixed typography, signature) → drafts folder, never sent"])
 classDef q stroke-dasharray:4 3,stroke-width:2px;
 class T0,T1,T2,T3,T4,T4b,T5,T6,T7,T8,T9,T10,T11,T12,T13,SUBJ,GR,DEV,HND,ACT,RUL,ATT,FLG,REC q;&lt;/pre&gt;&lt;p&gt;The wording space of this single e-mail: 2 subject forms, 3 greeting forms, 2
opening-sentence forms, 7 reachable cause wordings (two of them with or without
a reference to attached media), 2 requested actions, and an optional device list
of 2 to 6 lines. Most thresholds above live in the YAML configuration; a few
(urgency, site-in-activity, 4-hour concentration) are still in code.&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Parameter&lt;/th&gt;
					&lt;th&gt;Source&lt;/th&gt;
					&lt;th&gt;Effect on the e-mail&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Event count over 24 h&lt;/td&gt;
					&lt;td&gt;daily reports&lt;/td&gt;
					&lt;td&gt;figure in the opening sentence; ≥ 30 adds an urgent prefix to the subject&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Rule that fired&lt;/td&gt;
					&lt;td&gt;rule engine&lt;/td&gt;
					&lt;td&gt;selects the cause wording; gates the handling-error and site-in-activity checks&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Devices involved (cleaned, de-duplicated)&lt;/td&gt;
					&lt;td&gt;daily reports&lt;/td&gt;
					&lt;td&gt;one named device, or a generic phrase plus a list of up to 6 lines ordered by count&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Device code per report line&lt;/td&gt;
					&lt;td&gt;daily reports&lt;/td&gt;
					&lt;td&gt;appended in parentheses to each listed device, only when known&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Zone name of the main device&lt;/td&gt;
					&lt;td&gt;portal&lt;/td&gt;
					&lt;td&gt;replaces the raw report label when available&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Learned spellings&lt;/td&gt;
					&lt;td&gt;operator&amp;rsquo;s history workbook&lt;/td&gt;
					&lt;td&gt;restores accents and casing of device names&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Title and contact-name cell&lt;/td&gt;
					&lt;td&gt;customer record&lt;/td&gt;
					&lt;td&gt;one of 3 greeting forms; several people, a role or an organisation name fall back to the generic form&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Company name&lt;/td&gt;
					&lt;td&gt;customer record&lt;/td&gt;
					&lt;td&gt;legal form removed, re-cased with acronyms kept, used in subject and body&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Site type, learned opening hours, interventions in the window&lt;/td&gt;
					&lt;td&gt;tracking workbook, portal journal&lt;/td&gt;
					&lt;td&gt;enable or suppress the site-in-activity wording&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;System switched on/off around the burst&lt;/td&gt;
					&lt;td&gt;portal journal&lt;/td&gt;
					&lt;td&gt;handling-error wording (2 variants), overriding every other cause&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Site flags&lt;/td&gt;
					&lt;td&gt;customer record&lt;/td&gt;
					&lt;td&gt;choice between the 2 requested actions&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Attachments present&lt;/td&gt;
					&lt;td&gt;portal media&lt;/td&gt;
					&lt;td&gt;reference to the attachments, or nothing&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Recipient addresses&lt;/td&gt;
					&lt;td&gt;customer record&lt;/td&gt;
					&lt;td&gt;every address found; otherwise an internal fallback mailbox&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The other drafts draw on the same shared French-language helpers: the
preposition before a site name agrees with its first noun (6 forms, from &lt;em&gt;chez&lt;/em&gt;
to &lt;em&gt;à l&amp;rsquo;&lt;/em&gt; and &lt;em&gt;aux&lt;/em&gt;); report titles agree in gender and number; dates are
written in full French form; a &amp;ldquo;Contact&amp;rdquo; heading becomes plural when several
contacts are listed; and an 8-entry map turns technical equipment jargon into
customer vocabulary.&lt;/p&gt;
&lt;h2 id="engineering-for-reliability"&gt;&lt;a href="#engineering-for-reliability" class="header-anchor"&gt;&lt;/a&gt;Engineering for reliability
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Idempotence.&lt;/strong&gt; A daily ledger keyed on site, message type and subject hash
prevents duplicate drafts; a cross-day register prevents re-issuing a work
order before the previous intervention has happened. Missed days are caught up
automatically.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Concurrency on a shared NAS.&lt;/strong&gt; Exclusive lock files (&lt;code&gt;O_CREAT|O_EXCL&lt;/code&gt;) with
a time-to-live for orphans; two workstations publish heartbeats, and the
secondary takes over only when the primary has been silent for two minutes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safe spreadsheet writes.&lt;/strong&gt; Writes go through a temporary file and an atomic
replace, with retries when the workbook is open by a user; inserted rows shift
footer formulas so totals stay correct.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network robustness.&lt;/strong&gt; Graph calls handle token refresh, HTTP 429
&lt;code&gt;Retry-After&lt;/code&gt; and 5xx back-off.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Explicit failure.&lt;/strong&gt; A crash wrapper with no dependency on the main package
sends a redacted alert and a dead-man ping even when imports fail; a sentinel
raises an alert when the portal returns no data at all instead of reporting
success.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shadow benchmarking.&lt;/strong&gt; A sandbox build replays a day in read-only mode and
diffs its drafts against production. The acceptance metric will be the share of
drafts the operator sends without changing a single word; it has not been measured
yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="results"&gt;&lt;a href="#results" class="header-anchor"&gt;&lt;/a&gt;Results
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;1,766&lt;/strong&gt; automated tests collected in the repository (October 2026; pass
status not logged), built on mock mail and portal adapters; tests that need
real fixtures are skipped when those are absent.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;9.5 %&lt;/strong&gt; (132/1,383) of event lines over 12 days of real reports were
silently dropped, found after a site was missing from a run: event codes and
labels are concatenated in the extracted PDF text. The parser was rewritten
to recognise the code forms actually emitted; after the rewrite, 0 lines are
lost on 11 real reports.&lt;/li&gt;
&lt;li&gt;Caching the fault-history workbook (1.49 MB, 15,533 rows) cut the equipment
maintenance process from &lt;strong&gt;161.1 s to 23.3 s&lt;/strong&gt; on one replay of one day&amp;rsquo;s real
reports (single run); workbook opens fell from 26 to 3.&lt;/li&gt;
&lt;li&gt;First clean shadow day against production: &lt;strong&gt;14 drafts on each side, same
decisions and subjects, 0 missed, 0 added&lt;/strong&gt;. Bodies have not yet been
compared, and typical days have about 90 drafts.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="stack"&gt;&lt;a href="#stack" class="header-anchor"&gt;&lt;/a&gt;Stack
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Python 3.11&lt;/code&gt; · &lt;code&gt;Microsoft Graph&lt;/code&gt; · &lt;code&gt;MSAL&lt;/code&gt; · &lt;code&gt;httpx&lt;/code&gt; · &lt;code&gt;Playwright&lt;/code&gt; · &lt;code&gt;pypdf&lt;/code&gt; ·
&lt;code&gt;openpyxl&lt;/code&gt; · &lt;code&gt;pydantic&lt;/code&gt; · &lt;code&gt;keyring&lt;/code&gt; · &lt;code&gt;pytest&lt;/code&gt; · &lt;code&gt;PyInstaller&lt;/code&gt;&lt;/p&gt;</description></item></channel></rss>