Built via parallel Workflow (5 builders + adversarial verifiers), then live-rendered and fixed in a headless browser against the running gateway. Tabs (PrimeControls/Tabs/*): - Overview: priority cards, KPI status row, alarm-rate timeline, insights, top-5 - Analysis: hour x day heatmap, priority donut vs ISA 80/15/5, MTTA/MTTR, floods - BadActors: chattering/standing/fleeting tables, focus chip, AlarmDetail wiring - Journal: paginated getJournalPage table, CSV export, EventDetail wiring Popups (PrimeControls/Popups/*): HealthScore, ShiftReport, AlarmDetail, EventDetail Chart fixes (verified rendering in-browser): - Overview timeline: seeded dataSources.rate, switched to static fills, split flood bins into their own red stacked series (per-point deriveFieldsFromData fill was crashing render with React.cloneElement null). - BadActors: removed the dual-axis Pareto XY chart (scaling was unusable); replaced with a ranked "Top Sources" table (share-of-total progress bar + cumulative %). Same insight, no axis scaling to break. Structural / hygiene: - Relocated Header + FilterBar from Dashboard/ to Shell/ and updated the two embed paths. A Perspective view cannot resolve sub-views nested inside another view's folder, so both were rendering as "view does not exist" placeholders. - Set persistent:false on all view input params. persistent:true caused the gateway to bake the live bundle (with sim tag paths) into Tabs/Analysis on render; nulled that baked data and reset the session query range to 0. lint_project.py: 0 failures / 0 warnings. Verified in-browser: Overview, BadActors. NOT yet live-verified: Analysis, Journal, and the four popups (schema-verified only). Header content is next phase (renders now, but a benign cloneElement console warning remains). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
227 lines
14 KiB
Markdown
227 lines
14 KiB
Markdown
# CONTRACT.md — PrimeBAT build contract (FROZEN after Gate G1)
|
||
|
||
Every builder/verifier agent works from this file. Do not deviate; changes require the
|
||
integrator to edit this file first. Repo: `/home/bpeck/git/buildathon`. Project root:
|
||
`ignition/gateway/projects/PrimeBAT/`. Reload: `python3 tools/provision.py scan-projects`.
|
||
Lint: `python3 tools/lint_project.py` (must exit 0). Component prop schemas (ground truth
|
||
for every prop name): `/tmp/claude-1000/-home-bpeck-git-buildathon/a7f89b44-360c-4f23-ab63-503f3c25b53b/scratchpad/schemas/components/<component-id>.schema.json`.
|
||
Format exemplar (READ IT before writing any view.json):
|
||
`/home/bpeck/git/primebench/ignition/gateway/projects/PrimeBench/com.inductiveautomation.perspective/views/ForceRow/view.json`.
|
||
|
||
## Hard rules (contest + platform)
|
||
|
||
1. Jython 2.7 everywhere (project scripts, transforms, event scripts): **NO f-strings**
|
||
(`%` formatting only), no `typing`, no `statistics`. Scripts in view.json are inline
|
||
JSON strings, tab-indented (`\t`), first char of every script/transform line block is a tab.
|
||
2. **Java exceptions bypass `except Exception`** in Jython. Gateway-facing defensive code
|
||
uses bare `except:` + `sys.exc_info()` (probe-verified).
|
||
3. NO tag bindings in PrimeBAT. NO SQL / `system.db.*`. NO `journalName=` literals.
|
||
Banned strings (lint-enforced): `[default]`, `BuildathonSim`, `Buildathon_DB`,
|
||
`alarmsim`, `SELECT`, f-string prefixes.
|
||
4. All views/styles/scripts under `PrimeControls/` namespaces. Single page `/` →
|
||
`PrimeControls/Dashboard`. `sharedDocks` stays `{}` (docked views prohibited).
|
||
5. Every view tolerates `bundle: null` / empty arrays — no binding may throw on empty.
|
||
Empty-window UX = `PrimeControls/Components/EmptyState` embed, not a broken chart.
|
||
6. resource.json boilerplate for every view:
|
||
`{"scope":"G","version":1,"restricted":false,"overridable":true,"files":["view.json"],"attributes":{}}`
|
||
(style classes: `files:["style.json"]`; script modules: scope `"A"`, `attributes:{"hintScope":2}`).
|
||
7. Numbers/durations/timestamps rendered through `PrimeControls.fmt` helpers (consistency).
|
||
|
||
## Probe-verified journal API facts (do not re-derive)
|
||
|
||
- `system.alarm.queryJournal(startDate=<ms|Date>, endDate=<ms|Date>)` — epoch millis
|
||
accepted directly; omit `journalName` (uses the single configured journal); nonexistent
|
||
name raises java `IllegalArgumentException` (catch with bare except).
|
||
- Result `AlarmQueryResultImpl`: iterable, `len()` works. One entry per **transition**;
|
||
entries of one alarm instance share `str(evt.getId())` (uuid).
|
||
- `str(evt.getState())` is compound: `"Active, Unacknowledged"`, `"Cleared, Acknowledged"`, etc.
|
||
Transition kind per row: activation row state starts `"Active"` and row's
|
||
`getActiveData()` non-null; clear row starts `"Cleared"` (`getClearedData()` non-null);
|
||
ack row has `getAckData()` non-null / state contains `", Acknowledged"`.
|
||
Per-instance: `active_ms` = min ts of Active rows; `clear_ms` = min ts of Cleared rows;
|
||
`ack_ms` = min ts of rows with ack marker.
|
||
- `evt.get("eventTime")` → java Date; `.getTime()` → ms. `evt.get("eventType")` is None (unused).
|
||
- `evt.getDisplayPath()` = `""` when unconfigured → fall back to parsing `getSource()`
|
||
(`prov:default:/tag:FOLDER/SUB/TAG:/alm:ALARMNAME`).
|
||
- `evt.getPriority()` → AlarmPriority enum: `.ordinal()` → 0..4, `str()` → name.
|
||
- System rows: `source == "evt:System Startup"` etc., `evt.get("isSystemEvent")` True.
|
||
`includeSystem`-style kwargs are ACCEPTED BUT IGNORED — always filter client-side.
|
||
- Ack user field: `evt.get("ackUserName")` (unicode, may be `u''`). `ackUser` is None.
|
||
- `system.alarm.queryStatus(state=["ActiveUnacked","ActiveAcked"])` works; entries have
|
||
`getActiveData()` with eventTime. `system.alarm.getShelvedPaths()` → list.
|
||
|
||
## Session props (only these exist; NO agent may add any)
|
||
|
||
`session.custom.PrimeControls`:
|
||
```json
|
||
{"query": {"startMs": 0, "endMs": 0, "rangePreset": "8h", "priorities": [], "areas": [],
|
||
"states": [], "search": "", "refreshToken": 0},
|
||
"ui": {"selectedTab": 0, "badActorFocus": ""}}
|
||
```
|
||
- `rangePreset` ∈ `4h|8h|24h|7d|custom`. Header preset dropdown writes startMs/endMs/rangePreset.
|
||
- Filters (`priorities` = int levels 0–4, `areas` = strings, `states` ∈
|
||
`["active","cleared","acked","unacked"]`, `search` substring) are written by FilterBar;
|
||
**Apply button bumps `refreshToken`** (filters do NOT auto-refetch).
|
||
- `ui.selectedTab`: 0 Overview · 1 Analysis · 2 Bad Actors · 3 Journal.
|
||
- `ui.badActorFocus`: source string; set by Overview top-5 click; BadActors highlights + clears.
|
||
|
||
## Data layer (script package `PrimeControls`, resource paths `ignition/script-python/PrimeControls/<mod>/code.py`)
|
||
|
||
Modules: `calc` (pure), `fmt` (pure), `alarms` (gateway adapter). Pure modules run under
|
||
CPython 3 for pytest AND Jython 2.7 (`from __future__ import division, print_function`).
|
||
|
||
### Perspective entry points (exact signatures)
|
||
|
||
```python
|
||
PrimeControls.alarms.getDashboardBundle(startMs, endMs, options=None) # -> bundle dict (below)
|
||
PrimeControls.alarms.getJournalPage(startMs, endMs, filters=None, page=0, pageSize=50)
|
||
# filters: {"priorities":[int], "areas":[str], "states":[str], "search":str}
|
||
# -> {"rows":[{"id","time_ms","time_label","source","label","area","state","state_label",
|
||
# "priority","priority_name","ack_user"}], "total":int, "page":int,
|
||
# "page_size":int, "truncated":bool, "error":None|str}
|
||
PrimeControls.alarms.journalCsv(startMs, endMs, filters=None, maxRows=10000) # -> CSV string
|
||
PrimeControls.alarms.getSourceDetail(source, startMs, endMs)
|
||
# -> {"label","area","events":[journal-page rows],"daily":[{"t0","count"}],
|
||
# "stats":{"count","avg_tta_ms","avg_active_ms","fleeting_count",
|
||
# "top_ack_users":[{"user","count"}]},"error":None|str}
|
||
PrimeControls.alarms.getEventData(eventId, aroundMs, source) # -> {"props":[{"name","value"}],"error":None|str}
|
||
PrimeControls.alarms.getShiftReportText(shiftHours=8, anchorHour=0, options=None)
|
||
# -> {"text":str, "start_ms":int, "end_ms":int, "label":str}
|
||
PrimeControls.fmt.dur(ms) / num(v, dec=0) / pct(v, dec=0, signed=False) / clock(ms) / day_clock(ms)
|
||
PrimeControls.fmt.delta_chip(d) # d = a kpi delta dict -> {"arrow","text","good"}
|
||
```
|
||
|
||
### Bundle schema (`getDashboardBundle` return — every key ALWAYS present)
|
||
|
||
```
|
||
meta: {start_ms, end_ms, prior_start_ms, now_ms, window_hours, event_count,
|
||
activation_count, dropped_rows, truncated, correlation_mode,
|
||
standing_mode, areas:[str], priorities_seen:[{"value":int,"label":str}],
|
||
source_count, error:None|str, opts:{...effective thresholds...}}
|
||
kpis: {activations, rate_per_hr, active_now, unacked_now, shelved_now, mtta_ms,
|
||
mttr_ms, flood_pct, flood_count, chatter_count, standing_count, fleeting_count}
|
||
# each value = {"value", "prior", "delta_pct", "dir": "up|down|flat|new|none", "good": bool|None}
|
||
health: {grade:"A".."F"|None, score:float|None,
|
||
subs:[{key,label,score:float|None,weight,detail}]} # keys: rate,flood,chatter,standing,priority
|
||
insights: [{severity:int(0 crit..3 info), icon:str, text:str, tab:str}]
|
||
rate: {bins:[{t0,t1,count,flood:bool}], bin_ms, target_per_bin, flood_threshold, max_count}
|
||
floods: {episodes:[{start_ms,end_ms,duration_ms,event_count,peak_bin_count,
|
||
top_source_label,top_source_count,top_area}], pct_time_in_flood}
|
||
priority: {raw:[{name,level,count}], buckets:{low,medium,high,other},
|
||
pct:{low,medium,high}, target:{low:80.0,medium:15.0,high:5.0}, sum_abs_dev}
|
||
heatmap: {rows:[[int]*24]*7, row_labels:["Mon".."Sun"], max_count, total}
|
||
mtta_mttr: {mtta:{mean_ms,median_ms,count}, mttr:{mean_ms,median_ms,count},
|
||
trend:[{t0,mtta_ms|None,mttr_ms|None,ack_n,clear_n}]}
|
||
pareto: {total_activations, rows:[{rank,source,label,area,priority_name,count,pct,cum_pct}]}
|
||
top_sources: [first 5 pareto rows]
|
||
chattering: [{source,label,area,priority_name,count,per_hour,median_gap_s}]
|
||
fleeting: {total, sources:[{source,label,area,priority_name,count,median_s}]}
|
||
standing: {mode, count, rows:[{source,label,area,priority_name,active_ms,age_ms,age_h,unacked}]}
|
||
active_now: [{source,label,area,priority_name,active_ms,age_ms,unacked}] # capped 200
|
||
```
|
||
|
||
Thresholds (surfaced in `meta.opts`): bins 10 min; flood >10/10min (end hysteresis 5);
|
||
ISA target 6/hr; chatter >10/hr AND median gap ≤120 s (min 5); fleeting <10 s;
|
||
standing >24 h; deltas flat band ±5%; caps: 50 000 events, 5000 bins, lists ≤50 rows.
|
||
|
||
## Binding patterns (copy these shapes exactly)
|
||
|
||
**P1 — bundle binding (Dashboard shell ONLY):** `custom.bundle` ← expr binding
|
||
```json
|
||
{"type": "expr",
|
||
"config": {"expression": "{session.custom.PrimeControls.query.refreshToken} + '|' + {session.custom.PrimeControls.query.startMs} + '|' + {session.custom.PrimeControls.query.endMs}"},
|
||
"transforms": [{"type": "script", "code": "\tq = self.session.custom.PrimeControls.query\n\treturn PrimeControls.alarms.getDashboardBundle(q.startMs, q.endMs, {'priorities': list(q.priorities), 'areas': list(q.areas), 'states': list(q.states), 'search': q.search})"}]}
|
||
```
|
||
|
||
**P2 — embed a sub-view with bundle + tab visibility:**
|
||
```json
|
||
{"meta": {"name": "tabOverview"}, "position": {"grow": 1},
|
||
"propConfig": {
|
||
"props.params.bundle": {"binding": {"type": "property", "config": {"path": "view.custom.bundle"}}},
|
||
"position.display": {"binding": {"type": "expr", "config": {"expression": "{session.custom.PrimeControls.ui.selectedTab} = 0"}}}},
|
||
"props": {"path": "PrimeControls/Tabs/Overview", "params": {}},
|
||
"type": "ia.display.view"}
|
||
```
|
||
|
||
**P3 — bidirectional session binding (FilterBar widgets):**
|
||
```json
|
||
{"binding": {"bidirectional": true, "type": "property",
|
||
"config": {"path": "session.custom.PrimeControls.query.search"}}}
|
||
```
|
||
|
||
**P4 — event scripts:** buttons/dropdowns fire `events.component.onActionPerformed`;
|
||
labels/icons/containers fire `events.dom.onClick` (Designer-verified shape). Action object
|
||
is identical for both: `{"config": {"script": "\t..."}, "scope": "G", "type": "script"}`.
|
||
```json
|
||
"events": {"dom": {"onClick": {"config": {"script": "\tsystem.perspective.openPopup('PC_HealthScore', 'PrimeControls/Popups/HealthScore', params = {'health': self.view.params.health}, showCloseIcon = True, draggable = True)"}, "scope": "G", "type": "script"}}}
|
||
```
|
||
|
||
**P5 — flex repeater from bundle slice:**
|
||
```json
|
||
{"type": "ia.display.flex-repeater",
|
||
"propConfig": {"props.instances": {"binding": {"type": "property", "config": {"path": "view.params.bundle.top_sources"}}}},
|
||
"props": {"path": "PrimeControls/Components/SourceRow", "direction": "column"}}
|
||
```
|
||
Repeater instance keys become the child view's params → **bundle array element keys ==
|
||
component view param names** (snake_case).
|
||
|
||
View param declaration: `"params": {"bundle": null}` + propConfig
|
||
`"params.bundle": {"paramDirection": "input", "persistent": true}`.
|
||
|
||
## Popups (IDs are constants)
|
||
|
||
| ID | View | Params |
|
||
|---|---|---|
|
||
| `PC_HealthScore` | `PrimeControls/Popups/HealthScore` | `{health: bundle.health}` |
|
||
| `PC_AlarmDetail` | `PrimeControls/Popups/AlarmDetail` | `{source, label}` |
|
||
| `PC_EventDetail` | `PrimeControls/Popups/EventDetail` | `{event: <journal row dict>}` |
|
||
| `PC_ShiftReport` | `PrimeControls/Popups/ShiftReport` | `{}` |
|
||
|
||
Close: `system.perspective.closePopup('<ID>')`.
|
||
|
||
## Style classes (foundation-owned; reference as `"style": {"classes": "PrimeControls/Card"}`)
|
||
|
||
`PrimeControls/Priority/{Diagnostic,Low,Medium,High,Critical}` (solid chip bg+fg) ·
|
||
`PrimeControls/PrioritySoft/{...same 5}` (tinted bg) ·
|
||
`PrimeControls/Grade/{A,B,C,D,F,NA}` · `PrimeControls/Card` · `PrimeControls/CardTitle` ·
|
||
`PrimeControls/Toolbar` · `PrimeControls/PageBg` · `PrimeControls/Chip/{Good,Bad,Flat}` ·
|
||
`PrimeControls/Tab/{Active,Inactive}` · `PrimeControls/State/{Active,Acked,Cleared}` ·
|
||
`PrimeControls/Text/{Big,Kpi,Muted}` · `PrimeControls/Empty`.
|
||
Priority scale: Diagnostic `#8A94A6` · Low `#5B9BD5` · Medium `#E5C453` · High `#E8883A` ·
|
||
Critical `#D64550`. Accent: `#3B7DD8`. Agents may ADD classes only under
|
||
`style-classes/PrimeControls/<TheirArea>/...`, never modify shared ones.
|
||
|
||
## Shared components (foundation-owned, under `views/PrimeControls/Components/`)
|
||
|
||
| View | Params (all input) |
|
||
|---|---|
|
||
| `Components/TabButton` | `title:str, index:int` (writes `ui.selectedTab` onClick; active style expr) |
|
||
| `Components/KpiCard` | `label:str, value:str, unit:str, delta:{...kpi delta}|null` (polarity already encoded in `delta.good`) |
|
||
| `Components/PriorityCard` | `priority_name:str, count:int, pct:float` |
|
||
| `Components/InsightRow` | `severity:int, icon:str, text:str, tab:str` |
|
||
| `Components/SourceRow` | `rank:int, source:str, label:str, count:int, pct:float` (click → focus+tab 2) |
|
||
| `Components/HeatmapCell` | `count:int, max:int` (bg alpha-scaled accent) |
|
||
| `Components/ScoreBar` | `key:str, label:str, score:float|null, weight:float, detail:str` |
|
||
| `Components/EmptyState` | `message:str, icon:str` |
|
||
| `Components/HealthGauge` | `health:{grade,score,subs}|null` (own dom.onClick opens PC_HealthScore with `{'health': ...}`) |
|
||
|
||
## View ownership (Phase 2/3)
|
||
|
||
A: `Shell/Header` + `Shell/FilterBar` (relocated out of `Dashboard/` — a view cannot
|
||
nest sub-views under another view's folder) · B: `Tabs/Overview` · C: `Tabs/Analysis` ·
|
||
D: `Tabs/BadActors` · E: `Tabs/Journal` · F: `Popups/HealthScore`+`Popups/ShiftReport` ·
|
||
G: `Popups/AlarmDetail`+`Popups/EventDetail` · H: `Components/HealthGauge` upgrade.
|
||
Integrator-only: `Dashboard/view.json`, session-props, page-config, shared Components,
|
||
root style classes, `script-python/*`, SimHarness.
|
||
|
||
## Verify loop (every agent, before declaring done)
|
||
|
||
```bash
|
||
python3 tools/lint_project.py
|
||
python3 tools/provision.py scan-projects
|
||
sleep 5; docker logs buildathon-ignition --since 1m 2>&1 | grep -iE "error|exception|traceback"
|
||
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8088/data/perspective/client/PrimeBAT
|
||
```
|
||
Schema uncertainty → read the harvested schema file; still unsure → minimal probe view in
|
||
`SimHarness/views/Scratch/Current` (coordinate via integrator), never guess deep chart JSON.
|