devagent-ignition 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. devagent_hmi.py +471 -0
  2. devagent_ignition-0.1.0.dist-info/METADATA +204 -0
  3. devagent_ignition-0.1.0.dist-info/RECORD +133 -0
  4. devagent_ignition-0.1.0.dist-info/WHEEL +4 -0
  5. devagent_ignition-0.1.0.dist-info/entry_points.txt +2 -0
  6. devagent_spatial/__init__.py +1 -0
  7. devagent_spatial/_resources/README.md +187 -0
  8. devagent_spatial/_resources/RELEASE_MANIFEST.json +312 -0
  9. devagent_spatial/_resources/SHA256SUMS.txt +270 -0
  10. devagent_spatial/_resources/SOURCE_PROVENANCE.json +186 -0
  11. devagent_spatial/_resources/assets/agent.js +102 -0
  12. devagent_spatial/_resources/assets/creator.css +247 -0
  13. devagent_spatial/_resources/assets/creator_2d.mjs +16 -0
  14. devagent_spatial/_resources/assets/creator_original_r9.mjs +58 -0
  15. devagent_spatial/_resources/assets/creator_original_v8.mjs +120 -0
  16. devagent_spatial/_resources/assets/creator_r9_diagnostics.mjs +53 -0
  17. devagent_spatial/_resources/assets/creator_scene.mjs +128 -0
  18. devagent_spatial/_resources/assets/creator_ui.mjs +60 -0
  19. devagent_spatial/_resources/assets/customer_documents.js +110 -0
  20. devagent_spatial/_resources/assets/equipment_controls.mjs +27 -0
  21. devagent_spatial/_resources/assets/facility2d.css +24 -0
  22. devagent_spatial/_resources/assets/facility2d_app.mjs +45 -0
  23. devagent_spatial/_resources/assets/facility2d_core.mjs +114 -0
  24. devagent_spatial/_resources/assets/facility2d_simulation.mjs +167 -0
  25. devagent_spatial/_resources/assets/facility2d_viewer.html +7 -0
  26. devagent_spatial/_resources/assets/fallback_scene.js +17 -0
  27. devagent_spatial/_resources/assets/flow_geometry.mjs +218 -0
  28. devagent_spatial/_resources/assets/harness.js +78 -0
  29. devagent_spatial/_resources/assets/live_design_review.js +129 -0
  30. devagent_spatial/_resources/assets/operations_ui.js +305 -0
  31. devagent_spatial/_resources/assets/operator_viewer.html +64 -0
  32. devagent_spatial/_resources/assets/project_package.js +52 -0
  33. devagent_spatial/_resources/assets/r9_transport.mjs +70 -0
  34. devagent_spatial/_resources/assets/runtime_feed.js +30 -0
  35. devagent_spatial/_resources/assets/studio.html +139 -0
  36. devagent_spatial/_resources/assets/style.css +191 -0
  37. devagent_spatial/_resources/assets/test_workspace.html +195 -0
  38. devagent_spatial/_resources/assets/three_scene.js +442 -0
  39. devagent_spatial/_resources/assets/unified_engineer.js +101 -0
  40. devagent_spatial/_resources/assets/v076_fat_ui.js +40 -0
  41. devagent_spatial/_resources/assets/v078_command_ui.js +44 -0
  42. devagent_spatial/_resources/assets/viewer.html +45 -0
  43. devagent_spatial/_resources/assets/viewer.js +112 -0
  44. devagent_spatial/_resources/design-system.json +130 -0
  45. devagent_spatial/_resources/examples/CV101-command-contract-PENDING.json +30 -0
  46. devagent_spatial/_resources/examples/cv101_guided.json +272 -0
  47. devagent_spatial/_resources/examples/make_examples.py +36 -0
  48. devagent_spatial/_resources/examples/single_floor.json +444 -0
  49. devagent_spatial/_resources/examples/v0771_training_wizard.json +11 -0
  50. devagent_spatial/_resources/examples/warehouse-3floor-standard-2d.json +708 -0
  51. devagent_spatial/_resources/examples/warehouse-operations-synthetic.json +1186 -0
  52. devagent_spatial/_resources/examples/warehouse-v07-lab-bindings-PENDING.json +372 -0
  53. devagent_spatial/_resources/examples/warehouse.json +708 -0
  54. devagent_spatial/_resources/examples/water.json +288 -0
  55. devagent_spatial/_resources/reference/r9-golden/REFERENCE_SHA256.json +13 -0
  56. devagent_spatial/_resources/reference/r9-golden/web/app.mjs +745 -0
  57. devagent_spatial/_resources/reference/r9-golden/web/index.html +213 -0
  58. devagent_spatial/_resources/reference/r9-golden/web/lab_panel.mjs +53 -0
  59. devagent_spatial/_resources/reference/r9-golden/web/model.mjs +367 -0
  60. devagent_spatial/_resources/reference/r9-golden/web/state_adapter.mjs +78 -0
  61. devagent_spatial/_resources/reference/r9-golden/web/styles.css +76 -0
  62. devagent_spatial/_resources/reference/r9-original/README_R9.md +27 -0
  63. devagent_spatial/_resources/reference/r9-original/SHA256SUMS.txt +63 -0
  64. devagent_spatial/_resources/reference/r9-original/SNAPSHOT_SHA256SUMS.txt +16 -0
  65. devagent_spatial/_resources/reference/r9-original/flow_routes.json +527 -0
  66. devagent_spatial/_resources/reference/r9-original/flow_sim.py +117 -0
  67. devagent_spatial/_resources/reference/r9-original/lab_control.py +73 -0
  68. devagent_spatial/_resources/reference/r9-original/web/app.mjs +745 -0
  69. devagent_spatial/_resources/reference/r9-original/web/index.html +213 -0
  70. devagent_spatial/_resources/reference/r9-original/web/lab_panel.mjs +53 -0
  71. devagent_spatial/_resources/reference/r9-original/web/model.mjs +367 -0
  72. devagent_spatial/_resources/reference/r9-original/web/state_adapter.mjs +78 -0
  73. devagent_spatial/_resources/reference/r9-original/web/styles.css +76 -0
  74. devagent_spatial/_resources/reference/r9-original/web/vendor/LICENSE-three.txt +21 -0
  75. devagent_spatial/_resources/reference/r9-original/web/vendor/SHA256SUMS.txt +4 -0
  76. devagent_spatial/_resources/reference/r9-original/web/vendor/addons/controls/OrbitControls.js +1860 -0
  77. devagent_spatial/_resources/reference/r9-original/web/vendor/three.core.js +58773 -0
  78. devagent_spatial/_resources/reference/r9-original/web/vendor/three.module.js +18251 -0
  79. devagent_spatial/_resources/reference/v8-original/README.md +36 -0
  80. devagent_spatial/_resources/reference/v8-original/SHA256SUMS.txt +12 -0
  81. devagent_spatial/_resources/reference/v8-original/model.py +124 -0
  82. devagent_spatial/_resources/reference/v8-original/run_demo.sh +4 -0
  83. devagent_spatial/_resources/reference/v8-original/server.py +100 -0
  84. devagent_spatial/_resources/reference/v8-original/tests/browser_smoke.py +50 -0
  85. devagent_spatial/_resources/reference/v8-original/tests/test_v8.py +144 -0
  86. devagent_spatial/_resources/reference/v8-original/topology.json +24 -0
  87. devagent_spatial/_resources/reference/v8-original/web/app.js +114 -0
  88. devagent_spatial/_resources/reference/v8-original/web/index.html +22 -0
  89. devagent_spatial/_resources/reference/v8-original/web/simulator.html +1 -0
  90. devagent_spatial/_resources/reference/v8-original/web/simulator.js +6 -0
  91. devagent_spatial/_resources/reference/v8-original/web/style.css +16 -0
  92. devagent_spatial/_resources/sample_customer_inputs/Plant_Equipment_Register.csv +4 -0
  93. devagent_spatial/_resources/sample_customer_inputs/Plant_Flow_Requirements.txt +5 -0
  94. devagent_spatial/_resources/sample_customer_inputs/Raw_PLC_Tag_Export.csv +4 -0
  95. devagent_spatial/_resources/templates/warehouse_connections.csv +12 -0
  96. devagent_spatial/_resources/templates/warehouse_equipment.csv +21 -0
  97. devagent_spatial/_resources/templates/warehouse_signals.csv +34 -0
  98. devagent_spatial/_resources/templates/water_connections.csv +7 -0
  99. devagent_spatial/_resources/templates/water_equipment.csv +10 -0
  100. devagent_spatial/_resources/templates/water_signals.csv +12 -0
  101. devagent_spatial/agent_harness.py +299 -0
  102. devagent_spatial/agent_runtime.py +314 -0
  103. devagent_spatial/contract_opc_fat.py +216 -0
  104. devagent_spatial/contract_opc_source.py +265 -0
  105. devagent_spatial/control_intents.py +38 -0
  106. devagent_spatial/customer_intake.py +235 -0
  107. devagent_spatial/engineering_expertise.py +114 -0
  108. devagent_spatial/generator.py +201 -0
  109. devagent_spatial/ignition_native.py +234 -0
  110. devagent_spatial/ignition_project.py +144 -0
  111. devagent_spatial/live_lab_binding.py +128 -0
  112. devagent_spatial/model.py +297 -0
  113. devagent_spatial/native_reference.py +69 -0
  114. devagent_spatial/opc_snapshot.py +87 -0
  115. devagent_spatial/operations.py +284 -0
  116. devagent_spatial/operations_capture.py +47 -0
  117. devagent_spatial/operations_lab.py +64 -0
  118. devagent_spatial/original_r9.py +169 -0
  119. devagent_spatial/original_v8.py +81 -0
  120. devagent_spatial/preflight.py +140 -0
  121. devagent_spatial/preview_simulation.py +189 -0
  122. devagent_spatial/project_package.py +101 -0
  123. devagent_spatial/qualification.py +152 -0
  124. devagent_spatial/strict_json.py +29 -0
  125. devagent_spatial/studio.py +894 -0
  126. devagent_spatial/studio_workflow.py +277 -0
  127. devagent_spatial/test_reporting.py +87 -0
  128. devagent_spatial/v076_opc_fat.py +180 -0
  129. devagent_spatial/v078_commands.py +277 -0
  130. devagent_spatial/v078_native_commands.py +161 -0
  131. devagent_spatial/v07_fat.py +110 -0
  132. devagent_spatial/v07_native.py +318 -0
  133. devagent_spatial/v083_native.py +349 -0
@@ -0,0 +1,349 @@
1
+ """Native operator candidate, versioned separately so R2 remains reproducible.
2
+
3
+ Reuses the V07 model/manifest/reference validators and deterministic ZIP envelope.
4
+ Only generated UI-local selection scripts are allowed. No tag/OPC writes or
5
+ Gateway deployment. Mapped telemetry requires explicit freshness policy.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import os
12
+ import tempfile
13
+ from pathlib import Path
14
+
15
+ from .engineering_expertise import review
16
+ from .generator import ROOT
17
+ from .ignition_native import NativeHold, _flex, _label, sha
18
+ from .ignition_project import _zip_blob
19
+ from .strict_json import loads
20
+ from .v07_native import canonical, expected_members as base_members, model_sha, validate_manifest, _walk
21
+
22
+ SCHEMA = 'devagent.ignition.v083.operator_policy.v1'
23
+ CONFIRM = 'ISOLATED_OFFLINE_OPERATOR_CANDIDATE_ONLY'
24
+
25
+
26
+ def policy_skeleton(model):
27
+ return {'schema': SCHEMA, 'model_sha256': model_sha(model), 'engineer': '',
28
+ 'approval': 'PENDING_ENGINEER_REVIEW',
29
+ 'signals': {s['id']: {'max_age_ms': None} for s in model['signals']},
30
+ 'equipment': {e['id']: {'pe_in': None, 'pe_out': None} for e in model['equipment']}}
31
+
32
+
33
+ def validate_policy(model, policy):
34
+ expected = policy_skeleton(model)
35
+ if not isinstance(policy, dict) or set(policy) != set(expected):
36
+ raise NativeHold('OPERATOR_POLICY_SCHEMA_INVALID')
37
+ if policy['schema'] != SCHEMA or policy['model_sha256'] != model_sha(model):
38
+ raise NativeHold('OPERATOR_POLICY_MODEL_MISMATCH')
39
+ if policy['approval'] not in ('PENDING_ENGINEER_REVIEW', 'APPROVED_FOR_ISOLATED_FAT_ONLY'):
40
+ raise NativeHold('OPERATOR_POLICY_APPROVAL_INVALID')
41
+ if not isinstance(policy['engineer'], str) or len(policy['engineer']) > 120:
42
+ raise NativeHold('OPERATOR_POLICY_ENGINEER_INVALID')
43
+ approved = policy['approval'] == 'APPROVED_FOR_ISOLATED_FAT_ONLY'
44
+ if approved and len(policy['engineer'].strip()) < 3:
45
+ raise NativeHold('OPERATOR_POLICY_ENGINEER_REQUIRED')
46
+ signals = {s['id']: s for s in model['signals']}
47
+ equipment = {e['id']: e for e in model['equipment']}
48
+ for key in ('signals', 'equipment'):
49
+ if not isinstance(policy[key], dict) or set(policy[key]) != set(expected[key]):
50
+ raise NativeHold('OPERATOR_POLICY_COVERAGE_MISMATCH')
51
+ for item in policy['signals'].values():
52
+ if not isinstance(item, dict) or set(item) != {'max_age_ms'}:
53
+ raise NativeHold('OPERATOR_FRESHNESS_SCHEMA_INVALID')
54
+ age = item['max_age_ms']
55
+ if age is not None and (not approved or type(age) is not int or not 100 <= age <= 86400000):
56
+ raise NativeHold('OPERATOR_FRESHNESS_REVIEW_REQUIRED')
57
+ associations = set()
58
+ for eid, item in policy['equipment'].items():
59
+ if not isinstance(item, dict) or set(item) != {'pe_in', 'pe_out'}:
60
+ raise NativeHold('OPERATOR_PE_SCHEMA_INVALID')
61
+ for sid in item.values():
62
+ if sid is None:
63
+ continue
64
+ if not isinstance(sid, str) or not approved or sid not in signals or signals[sid]['role'] != 'photoeye_blocked':
65
+ raise NativeHold('OPERATOR_PE_REVIEW_REQUIRED')
66
+ sensor = equipment.get(signals[sid]['asset_id'])
67
+ if sensor is None or sensor.get('floor', 1) != equipment[eid].get('floor', 1):
68
+ raise NativeHold('OPERATOR_PE_CROSS_FLOOR_RELATIONSHIP_UNVERIFIED')
69
+ if sid in associations:
70
+ raise NativeHold('OPERATOR_PE_ASSOCIATION_DUPLICATE')
71
+ associations.add(sid)
72
+ return policy
73
+
74
+
75
+ def _expr(node, target, expression):
76
+ node.setdefault('propConfig', {})[target] = {
77
+ 'binding': {'type': 'expr', 'config': {'expression': expression}}}
78
+
79
+
80
+ def _button(name, text, palette, script=None):
81
+ node = {'type': 'ia.input.button', 'version': 0, 'meta': {'name': name},
82
+ 'props': {'text': text, 'enabled': script is not None,
83
+ 'style': {'backgroundColor': palette['deep'], 'color': '#ecf5ff',
84
+ 'border': '1px solid '+palette['border'], 'borderRadius': '6px',
85
+ 'fontSize': '14px', 'minHeight': '44px', 'padding': '10px'}},
86
+ 'position': {'basis': 'auto', 'grow': 0, 'shrink': 0}}
87
+ if script is not None:
88
+ node['events'] = {'component': {'onActionPerformed': {
89
+ 'type': 'script', 'scope': 'G', 'config': {'script': script}}}}
90
+ return node
91
+
92
+
93
+ def _floor_script(floor):
94
+ return '\tself.view.custom.selectedFloor = '+str(floor)+'\n\tself.view.custom.selectedAsset = ""\n'
95
+
96
+
97
+ def _asset_script(asset):
98
+ return '\tself.view.custom.selectedAsset = '+json.dumps(asset)+'\n'
99
+
100
+
101
+ def signal_expressions(signal, mapped, max_age_ms):
102
+ """Generate fixed expressions; bad/old/missing data never becomes a state.
103
+
104
+ Timestamp-only changes and source-time propagation must be checked in FAT.
105
+ No freshness interval is guessed by this compiler.
106
+ """
107
+ if not mapped:
108
+ return {'value': '"NOT MAPPED"', 'quality': '"NOT MAPPED"', 'timestamp': '"UNKNOWN"'}
109
+ raw = '{view.custom.raw.'+signal['id']+'}'
110
+ quality = 'toStr(qualityOf('+raw+'))'
111
+ timestamp = 'toStr(timestampOf('+raw+'))'
112
+ if max_age_ms is None:
113
+ return {'value': '"UNKNOWN - STALE LIMIT NOT CONFIGURED"',
114
+ 'quality': quality, 'timestamp': timestamp}
115
+ age = '(toMillis(now(1000)) - toMillis(timestampOf('+raw+')))'
116
+ fresh = '('+age+' >= 0 && '+age+' <= '+str(max_age_ms)+')'
117
+ value = 'toStr('+raw+')'
118
+ role = signal['role']
119
+ if role in ('run_feedback', 'fault', 'photoeye_blocked'):
120
+ active = raw if signal['polarity'] == 'active_high' else '!'+raw
121
+ active_text, inactive_text = {
122
+ 'run_feedback': ('RUNNING', 'STOPPED'),
123
+ 'fault': ('FAULT ACTIVE', 'FAULT INACTIVE'),
124
+ 'photoeye_blocked': ('BLOCKED', 'CLEAR')}[role]
125
+ value = 'if('+active+', '+json.dumps(active_text)+', '+json.dumps(inactive_text)+')'
126
+ elif role == 'run_command':
127
+ value = 'if('+raw+', "COMMAND ON", "COMMAND OFF")'
128
+ return {
129
+ 'value': 'if(isNull('+raw+'), "UNKNOWN", if(!isGood('+raw+'), "UNKNOWN", if(!'+fresh+', "UNKNOWN - STALE", '+value+')))',
130
+ 'quality': 'if(!isGood('+raw+'), '+quality+', if(!'+fresh+', "STALE", '+quality+'))',
131
+ 'timestamp': timestamp}
132
+
133
+
134
+ def operator_view(model, manifest, policy, *, _skip_audit=False):
135
+ approved = validate_manifest(model, manifest)
136
+ validate_policy(model, policy)
137
+ if review(model)['role_asset_mismatches']:
138
+ raise NativeHold('ENGINEERING_ROLE_ASSET_KIND_MISMATCH')
139
+ design = loads((ROOT/'design-system.json').read_text())
140
+ palette = design['palettes'][model.get('palette', 'r9_cyan')]
141
+ by_id = {s['id']: s for s in model['signals']}
142
+ by_asset = {e['id']: {} for e in model['equipment']}
143
+ for signal in model['signals']:
144
+ by_asset[signal['asset_id']][signal['role']] = signal
145
+ floors = sorted({e.get('floor', 1) for e in model['equipment']})
146
+ view = {'custom': {'selectedFloor': 0, 'selectedAsset': '',
147
+ 'raw': {sid: None for sid in approved}},
148
+ 'params': {}, 'propConfig': {},
149
+ 'props': {'defaultSize': {'width': 1600, 'height': 1000}}}
150
+ for sid, path in approved.items():
151
+ view['propConfig']['custom.raw.'+sid] = {
152
+ 'binding': {'type': 'tag', 'config': {'mode': 'direct', 'tagPath': path}}}
153
+
154
+ def label(text, name, size=14, color='#ecf5ff'):
155
+ return _label(text, size, False, color, name=name)
156
+
157
+ def diagnostic(title, signal, name):
158
+ value = label('NOT MAPPED', name+'Value', color='#e6bd70')
159
+ quality = label('NOT MAPPED', name+'Quality', 11, palette['muted'])
160
+ timestamp = label('UNKNOWN', name+'Timestamp', 10, palette['muted'])
161
+ if signal:
162
+ sid = signal['id']
163
+ exprs = signal_expressions(signal, sid in approved, policy['signals'][sid]['max_age_ms'])
164
+ for node, key in ((value, 'value'), (quality, 'quality'), (timestamp, 'timestamp')):
165
+ _expr(node, 'props.text', exprs[key])
166
+ title += ' | '+sid+(' ['+signal['unit']+']' if signal.get('unit') else '')
167
+ return _flex([label(title, name+'Title', 12), value, quality, timestamp], name=name,
168
+ style={'borderBottom': '1px solid '+palette['border'], 'padding': '8px', 'gap': '3px'})
169
+
170
+ nav = [_button('FloorAll', 'WAREHOUSE OVERVIEW', palette, _floor_script(0))]
171
+ nav += [_button('Floor'+str(f), 'FLOOR '+str(f), palette, _floor_script(f)) for f in floors]
172
+ groups = []
173
+ for floor in floors:
174
+ cards = []
175
+ for equipment in model['equipment']:
176
+ if equipment.get('floor', 1) != floor:
177
+ continue
178
+ eid = equipment['id']
179
+ select = _button('Select_'+eid, eid+' | '+equipment['name'], palette, _asset_script(eid))
180
+ _expr(select, 'props.style.borderColor', 'if({view.custom.selectedAsset} = '+json.dumps(eid)+', '+json.dumps(palette['accent'])+', '+json.dumps(palette['border'])+')')
181
+ rows = [select, label(equipment['kind'].upper(), 'Kind_'+eid, 11, palette['muted'])]
182
+ for role in ('run_feedback', 'fault'):
183
+ signal = by_asset[eid].get(role)
184
+ if signal:
185
+ status = label('NOT MAPPED', role+'_'+eid)
186
+ _expr(status, 'props.text', signal_expressions(signal, signal['id'] in approved,
187
+ policy['signals'][signal['id']]['max_age_ms'])['value'])
188
+ rows.append(status)
189
+ destinations = [c['to'] for c in model.get('connections', []) if c['from'] == eid]
190
+ if destinations:
191
+ rows.append(label('TO: '+', '.join(destinations), 'Flow_'+eid, 11, palette['muted']))
192
+ card = _flex(rows, name='Equipment_'+eid, style={'backgroundColor': palette['panel'],
193
+ 'padding': '10px', 'gap': '6px', 'borderRadius': '8px'})
194
+ card['position'] = {'basis': '260px', 'grow': 1, 'shrink': 0}
195
+ cards.append(card)
196
+ group = _flex([label('FLOOR '+str(floor), 'FloorTitle'+str(floor), 20),
197
+ _flex(cards, name='EquipmentGrid'+str(floor), direction='row', style={'gap': '12px'})],
198
+ name='FloorGroup'+str(floor), style={'padding': '12px', 'gap': '12px'})
199
+ _expr(group, 'meta.visible', '{view.custom.selectedFloor} = 0 || {view.custom.selectedFloor} = '+str(floor))
200
+ groups.append(group)
201
+ overview = _flex(groups, name='WarehouseOverview', style={'overflow': 'auto', 'minWidth': '400px'}, grow=1)
202
+ overview['position']['basis'] = '0px'
203
+
204
+ empty = label('SELECT EQUIPMENT', 'InspectorEmpty', 20)
205
+ _expr(empty, 'meta.visible', '{view.custom.selectedAsset} = ""')
206
+ inspectors = [empty]
207
+ for equipment in model['equipment']:
208
+ eid = equipment['id']
209
+ roles = by_asset[eid]
210
+ rows = [label(eid+' | '+equipment['name'], 'InspectorTitle_'+eid, 20),
211
+ label('FLOOR '+str(equipment.get('floor', 1))+' | '+equipment.get('zone', ''), 'Location_'+eid, 12)]
212
+ for role, title in (('run_command', 'MOTOR COMMAND (READBACK)'), ('run_feedback', 'RUN FEEDBACK'),
213
+ ('motor_speed', 'MOTOR SPEED'), ('vfd_frequency', 'VFD FREQUENCY'),
214
+ ('fault', 'FAULT'), ('fault_code', 'FAULT CODE')):
215
+ rows.append(diagnostic(title, roles.get(role), 'Diag_'+eid+'_'+role))
216
+ consumed = {'run_command', 'run_feedback', 'motor_speed', 'vfd_frequency', 'fault', 'fault_code'}
217
+ for direction in ('pe_in', 'pe_out'):
218
+ sid = policy['equipment'][eid][direction]
219
+ rows.append(diagnostic(direction.upper().replace('_', ' '), by_id.get(sid), 'Diag_'+eid+'_'+direction))
220
+ for role, signal in sorted(roles.items()):
221
+ if role not in consumed:
222
+ rows.append(diagnostic('DECLARED '+role.upper().replace('_', ' '), signal, 'Diag_'+eid+'_'+role))
223
+ rows += [label('ALARM: NOT MAPPED | lifecycle not commissioned', 'Alarm_'+eid, 12, '#e6bd70'),
224
+ label('QUALITY / SOURCE TIME shown per signal', 'QualityNotice_'+eid, 11, palette['muted']),
225
+ _flex([_button('Command_'+eid+'_'+action, action, palette) for action in ('START', 'STOP', 'RESET')],
226
+ name='MotorCommands_'+eid, direction='row', style={'gap': '8px'}),
227
+ label('CONTROL LOCKED | no command authority', 'ControlLock_'+eid, 12, '#e6bd70')]
228
+ panel = _flex(rows, name='Inspector_'+eid, style={'gap': '5px'})
229
+ _expr(panel, 'meta.visible', '{view.custom.selectedAsset} = '+json.dumps(eid))
230
+ inspectors.append(panel)
231
+ inspector = _flex(inspectors, name='EquipmentInspector', style={
232
+ 'backgroundColor': palette['panel'], 'padding': '16px', 'overflow': 'auto', 'minWidth': '340px'})
233
+ inspector['position'] = {'basis': '400px', 'grow': 0, 'shrink': 0}
234
+ body = _flex([overview, inspector], name='OperationsBody', direction='row', grow=1,
235
+ style={'gap': '14px', 'minHeight': '600px', 'alignItems': 'stretch'})
236
+ body['props']['wrap'] = 'nowrap'
237
+ root = _flex([label(model['name'], 'FacilityTitle', 26),
238
+ label('ISOLATED LAB | BINDINGS NOT PHYSICALLY VERIFIED | PRODUCTION HOLD', 'Qualification', 12, '#e6bd70'),
239
+ _flex(nav, name='FloorNavigation', direction='row', style={'gap': '10px'}), body],
240
+ name='DevAgentOperatorRoot', style={'backgroundColor': palette['background'],
241
+ 'color': '#ecf5ff', 'padding': '16px', 'gap': '14px', 'overflow': 'auto'})
242
+ root.pop('position', None)
243
+ view['root'] = root
244
+ if not _skip_audit:
245
+ audit_view(model, manifest, view, policy=policy)
246
+ return view
247
+
248
+
249
+ def audit_view(model, manifest, view, *, policy=None):
250
+ approved = validate_manifest(model, manifest)
251
+ floor_scripts = {'FloorAll': _floor_script(0)}
252
+ floor_scripts.update({'Floor'+str(f): _floor_script(f)
253
+ for f in {e.get('floor', 1) for e in model['equipment']}})
254
+ asset_scripts = {'Select_'+e['id']: _asset_script(e['id']) for e in model['equipment']}
255
+ allowed_script_by_name = {**floor_scripts, **asset_scripts}
256
+ nodes = list(_walk(view['root']))
257
+ if len(nodes) > 12000:
258
+ raise NativeHold('OPERATOR_COMPONENT_LIMIT')
259
+ actual_tags = []
260
+ for node in [view, *nodes]:
261
+ if node is not view and node.get('type') not in ('ia.container.flex', 'ia.display.label', 'ia.input.button'):
262
+ raise NativeHold('OPERATOR_COMPONENT_NOT_ALLOWED')
263
+ if any(k in node for k in ('scripts', 'customMethods', 'onChange')):
264
+ raise NativeHold('OPERATOR_ACTIVE_FEATURE_REJECTED')
265
+ if 'events' in node:
266
+ event = node['events']
267
+ action = event.get('component', {}).get('onActionPerformed', {})
268
+ script = action.get('config', {}).get('script', '')
269
+ expected = {'component': {'onActionPerformed': {'type': 'script', 'scope': 'G', 'config': {'script': script}}}}
270
+ if (event != expected or node.get('type') != 'ia.input.button' or
271
+ script != allowed_script_by_name.get(node.get('meta', {}).get('name'))):
272
+ raise NativeHold('OPERATOR_EVENT_NOT_UI_LOCAL')
273
+ if node.get('meta', {}).get('name', '').startswith('Command_'):
274
+ if node['props']['enabled'] is not False or 'events' in node or 'propConfig' in node:
275
+ raise NativeHold('OPERATOR_COMMAND_AUTHORITY_REJECTED')
276
+ for target, cfg in node.get('propConfig', {}).items():
277
+ if set(cfg) != {'binding'}:
278
+ raise NativeHold('OPERATOR_BINDING_FEATURE_REJECTED')
279
+ binding = cfg['binding']
280
+ if set(binding) != {'type', 'config'}:
281
+ raise NativeHold('OPERATOR_BINDING_FEATURE_REJECTED')
282
+ if binding['type'] == 'tag':
283
+ sid = target.removeprefix('custom.raw.')
284
+ if node is not view or sid not in approved or binding['config'] != {'mode': 'direct', 'tagPath': approved[sid]}:
285
+ raise NativeHold('OPERATOR_TAG_NOT_APPROVED')
286
+ actual_tags.append(sid)
287
+ elif binding['type'] == 'expr':
288
+ if set(binding['config']) != {'expression'}:
289
+ raise NativeHold('OPERATOR_EXPRESSION_CONFIG_INVALID')
290
+ expression = binding['config']['expression']
291
+ # All telemetry reads must flow through the approved raw bindings.
292
+ if any(token in expression.lower() for token in ('runscript', 'tag(', '{[', 'system.', 'http', 'forcequality', 'qualifiedvalue')):
293
+ raise NativeHold('OPERATOR_EXPRESSION_ACCESS_REJECTED')
294
+ else:
295
+ raise NativeHold('OPERATOR_BINDING_TYPE_REJECTED')
296
+ if sorted(actual_tags) != sorted(approved):
297
+ raise NativeHold('OPERATOR_BINDING_COVERAGE_INVALID')
298
+ if policy is None:
299
+ policy = policy_skeleton(model)
300
+ expected_view = operator_view(model, manifest, policy, _skip_audit=True)
301
+ if canonical(view) != canonical(expected_view):
302
+ raise NativeHold('OPERATOR_VIEW_SEMANTIC_DRIFT')
303
+
304
+
305
+ def expected_members(model, manifest, policy, reference, template_view, reference_sha, name):
306
+ members, meta = base_members(model, manifest, reference, template_view, reference_sha, name)
307
+ view_path = next(path for path in members if path.endswith('/view.json'))
308
+ members[view_path] = canonical(operator_view(model, manifest, policy))
309
+ return members, meta
310
+
311
+
312
+ def candidate(model, manifest, policy, reference, template_view, reference_sha, name, target, *, verify=False, confirm=''):
313
+ if not verify and confirm != CONFIRM:
314
+ raise NativeHold('OPERATOR_COMPILER_CONFIRMATION_REQUIRED')
315
+ target = Path(target).absolute()
316
+ if target.is_symlink() or target.suffix != '.zip' or not target.parent.is_dir():
317
+ raise NativeHold('OPERATOR_TARGET_INVALID')
318
+ members, meta = expected_members(model, manifest, policy, reference, template_view, reference_sha, name)
319
+ payload = _zip_blob(members)
320
+ if len(payload) > 4_000_000:
321
+ raise NativeHold('OPERATOR_CANDIDATE_TOO_LARGE')
322
+ if verify:
323
+ if not target.is_file() or target.read_bytes() != payload:
324
+ raise NativeHold('OPERATOR_REPRODUCIBILITY_MISMATCH')
325
+ else:
326
+ if target.exists():
327
+ raise NativeHold('OPERATOR_CREATE_ONLY_TARGET_EXISTS')
328
+ fd, temp = tempfile.mkstemp(prefix='.v083-native-', dir=target.parent)
329
+ try:
330
+ with os.fdopen(fd, 'wb') as stream:
331
+ stream.write(payload)
332
+ stream.flush()
333
+ os.fsync(stream.fileno())
334
+ os.chmod(temp, 0o600)
335
+ try:
336
+ os.link(temp, target)
337
+ except FileExistsError:
338
+ raise NativeHold('OPERATOR_CREATE_ONLY_TARGET_EXISTS') from None
339
+ finally:
340
+ Path(temp).unlink(missing_ok=True)
341
+ return {'schema': 'devagent.ignition.v083.operator_candidate.v1',
342
+ 'status': 'PASS_OFFLINE_OPERATOR_REPRODUCIBILITY_ONLY' if verify else 'PASS_OFFLINE_OPERATOR_CANDIDATE_ONLY',
343
+ 'project_zip': str(target), 'project_sha256': sha(target), 'reference_sha256': reference_sha,
344
+ 'model_sha256': model_sha(model), 'manifest_sha256': hashlib.sha256(canonical(manifest)).hexdigest(),
345
+ 'policy_sha256': hashlib.sha256(canonical(policy)).hexdigest(), **meta,
346
+ 'OFFLINE_COMPILER': 'PASS', 'AUTHENTICATED_GATEWAY_IMPORT': 'HOLD_NOT_EXECUTED',
347
+ 'NATIVE_PERSPECTIVE_RENDER': 'HOLD_NOT_EXECUTED', 'NATIVE_EQUIPMENT_INTERACTION': 'HOLD_NOT_EXECUTED',
348
+ 'GATEWAY_OPC_FAT': 'HOLD_NOT_EXECUTED', 'REAL_OPC_SOURCE_VALIDATION': 'HOLD_NOT_VERIFIED',
349
+ 'PRODUCTION_APPROVAL': 'HOLD', 'command_authority': 'NONE', 'r9_native_parity': 'NOT_VERIFIED'}