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.
- devagent_hmi.py +471 -0
- devagent_ignition-0.1.0.dist-info/METADATA +204 -0
- devagent_ignition-0.1.0.dist-info/RECORD +133 -0
- devagent_ignition-0.1.0.dist-info/WHEEL +4 -0
- devagent_ignition-0.1.0.dist-info/entry_points.txt +2 -0
- devagent_spatial/__init__.py +1 -0
- devagent_spatial/_resources/README.md +187 -0
- devagent_spatial/_resources/RELEASE_MANIFEST.json +312 -0
- devagent_spatial/_resources/SHA256SUMS.txt +270 -0
- devagent_spatial/_resources/SOURCE_PROVENANCE.json +186 -0
- devagent_spatial/_resources/assets/agent.js +102 -0
- devagent_spatial/_resources/assets/creator.css +247 -0
- devagent_spatial/_resources/assets/creator_2d.mjs +16 -0
- devagent_spatial/_resources/assets/creator_original_r9.mjs +58 -0
- devagent_spatial/_resources/assets/creator_original_v8.mjs +120 -0
- devagent_spatial/_resources/assets/creator_r9_diagnostics.mjs +53 -0
- devagent_spatial/_resources/assets/creator_scene.mjs +128 -0
- devagent_spatial/_resources/assets/creator_ui.mjs +60 -0
- devagent_spatial/_resources/assets/customer_documents.js +110 -0
- devagent_spatial/_resources/assets/equipment_controls.mjs +27 -0
- devagent_spatial/_resources/assets/facility2d.css +24 -0
- devagent_spatial/_resources/assets/facility2d_app.mjs +45 -0
- devagent_spatial/_resources/assets/facility2d_core.mjs +114 -0
- devagent_spatial/_resources/assets/facility2d_simulation.mjs +167 -0
- devagent_spatial/_resources/assets/facility2d_viewer.html +7 -0
- devagent_spatial/_resources/assets/fallback_scene.js +17 -0
- devagent_spatial/_resources/assets/flow_geometry.mjs +218 -0
- devagent_spatial/_resources/assets/harness.js +78 -0
- devagent_spatial/_resources/assets/live_design_review.js +129 -0
- devagent_spatial/_resources/assets/operations_ui.js +305 -0
- devagent_spatial/_resources/assets/operator_viewer.html +64 -0
- devagent_spatial/_resources/assets/project_package.js +52 -0
- devagent_spatial/_resources/assets/r9_transport.mjs +70 -0
- devagent_spatial/_resources/assets/runtime_feed.js +30 -0
- devagent_spatial/_resources/assets/studio.html +139 -0
- devagent_spatial/_resources/assets/style.css +191 -0
- devagent_spatial/_resources/assets/test_workspace.html +195 -0
- devagent_spatial/_resources/assets/three_scene.js +442 -0
- devagent_spatial/_resources/assets/unified_engineer.js +101 -0
- devagent_spatial/_resources/assets/v076_fat_ui.js +40 -0
- devagent_spatial/_resources/assets/v078_command_ui.js +44 -0
- devagent_spatial/_resources/assets/viewer.html +45 -0
- devagent_spatial/_resources/assets/viewer.js +112 -0
- devagent_spatial/_resources/design-system.json +130 -0
- devagent_spatial/_resources/examples/CV101-command-contract-PENDING.json +30 -0
- devagent_spatial/_resources/examples/cv101_guided.json +272 -0
- devagent_spatial/_resources/examples/make_examples.py +36 -0
- devagent_spatial/_resources/examples/single_floor.json +444 -0
- devagent_spatial/_resources/examples/v0771_training_wizard.json +11 -0
- devagent_spatial/_resources/examples/warehouse-3floor-standard-2d.json +708 -0
- devagent_spatial/_resources/examples/warehouse-operations-synthetic.json +1186 -0
- devagent_spatial/_resources/examples/warehouse-v07-lab-bindings-PENDING.json +372 -0
- devagent_spatial/_resources/examples/warehouse.json +708 -0
- devagent_spatial/_resources/examples/water.json +288 -0
- devagent_spatial/_resources/reference/r9-golden/REFERENCE_SHA256.json +13 -0
- devagent_spatial/_resources/reference/r9-golden/web/app.mjs +745 -0
- devagent_spatial/_resources/reference/r9-golden/web/index.html +213 -0
- devagent_spatial/_resources/reference/r9-golden/web/lab_panel.mjs +53 -0
- devagent_spatial/_resources/reference/r9-golden/web/model.mjs +367 -0
- devagent_spatial/_resources/reference/r9-golden/web/state_adapter.mjs +78 -0
- devagent_spatial/_resources/reference/r9-golden/web/styles.css +76 -0
- devagent_spatial/_resources/reference/r9-original/README_R9.md +27 -0
- devagent_spatial/_resources/reference/r9-original/SHA256SUMS.txt +63 -0
- devagent_spatial/_resources/reference/r9-original/SNAPSHOT_SHA256SUMS.txt +16 -0
- devagent_spatial/_resources/reference/r9-original/flow_routes.json +527 -0
- devagent_spatial/_resources/reference/r9-original/flow_sim.py +117 -0
- devagent_spatial/_resources/reference/r9-original/lab_control.py +73 -0
- devagent_spatial/_resources/reference/r9-original/web/app.mjs +745 -0
- devagent_spatial/_resources/reference/r9-original/web/index.html +213 -0
- devagent_spatial/_resources/reference/r9-original/web/lab_panel.mjs +53 -0
- devagent_spatial/_resources/reference/r9-original/web/model.mjs +367 -0
- devagent_spatial/_resources/reference/r9-original/web/state_adapter.mjs +78 -0
- devagent_spatial/_resources/reference/r9-original/web/styles.css +76 -0
- devagent_spatial/_resources/reference/r9-original/web/vendor/LICENSE-three.txt +21 -0
- devagent_spatial/_resources/reference/r9-original/web/vendor/SHA256SUMS.txt +4 -0
- devagent_spatial/_resources/reference/r9-original/web/vendor/addons/controls/OrbitControls.js +1860 -0
- devagent_spatial/_resources/reference/r9-original/web/vendor/three.core.js +58773 -0
- devagent_spatial/_resources/reference/r9-original/web/vendor/three.module.js +18251 -0
- devagent_spatial/_resources/reference/v8-original/README.md +36 -0
- devagent_spatial/_resources/reference/v8-original/SHA256SUMS.txt +12 -0
- devagent_spatial/_resources/reference/v8-original/model.py +124 -0
- devagent_spatial/_resources/reference/v8-original/run_demo.sh +4 -0
- devagent_spatial/_resources/reference/v8-original/server.py +100 -0
- devagent_spatial/_resources/reference/v8-original/tests/browser_smoke.py +50 -0
- devagent_spatial/_resources/reference/v8-original/tests/test_v8.py +144 -0
- devagent_spatial/_resources/reference/v8-original/topology.json +24 -0
- devagent_spatial/_resources/reference/v8-original/web/app.js +114 -0
- devagent_spatial/_resources/reference/v8-original/web/index.html +22 -0
- devagent_spatial/_resources/reference/v8-original/web/simulator.html +1 -0
- devagent_spatial/_resources/reference/v8-original/web/simulator.js +6 -0
- devagent_spatial/_resources/reference/v8-original/web/style.css +16 -0
- devagent_spatial/_resources/sample_customer_inputs/Plant_Equipment_Register.csv +4 -0
- devagent_spatial/_resources/sample_customer_inputs/Plant_Flow_Requirements.txt +5 -0
- devagent_spatial/_resources/sample_customer_inputs/Raw_PLC_Tag_Export.csv +4 -0
- devagent_spatial/_resources/templates/warehouse_connections.csv +12 -0
- devagent_spatial/_resources/templates/warehouse_equipment.csv +21 -0
- devagent_spatial/_resources/templates/warehouse_signals.csv +34 -0
- devagent_spatial/_resources/templates/water_connections.csv +7 -0
- devagent_spatial/_resources/templates/water_equipment.csv +10 -0
- devagent_spatial/_resources/templates/water_signals.csv +12 -0
- devagent_spatial/agent_harness.py +299 -0
- devagent_spatial/agent_runtime.py +314 -0
- devagent_spatial/contract_opc_fat.py +216 -0
- devagent_spatial/contract_opc_source.py +265 -0
- devagent_spatial/control_intents.py +38 -0
- devagent_spatial/customer_intake.py +235 -0
- devagent_spatial/engineering_expertise.py +114 -0
- devagent_spatial/generator.py +201 -0
- devagent_spatial/ignition_native.py +234 -0
- devagent_spatial/ignition_project.py +144 -0
- devagent_spatial/live_lab_binding.py +128 -0
- devagent_spatial/model.py +297 -0
- devagent_spatial/native_reference.py +69 -0
- devagent_spatial/opc_snapshot.py +87 -0
- devagent_spatial/operations.py +284 -0
- devagent_spatial/operations_capture.py +47 -0
- devagent_spatial/operations_lab.py +64 -0
- devagent_spatial/original_r9.py +169 -0
- devagent_spatial/original_v8.py +81 -0
- devagent_spatial/preflight.py +140 -0
- devagent_spatial/preview_simulation.py +189 -0
- devagent_spatial/project_package.py +101 -0
- devagent_spatial/qualification.py +152 -0
- devagent_spatial/strict_json.py +29 -0
- devagent_spatial/studio.py +894 -0
- devagent_spatial/studio_workflow.py +277 -0
- devagent_spatial/test_reporting.py +87 -0
- devagent_spatial/v076_opc_fat.py +180 -0
- devagent_spatial/v078_commands.py +277 -0
- devagent_spatial/v078_native_commands.py +161 -0
- devagent_spatial/v07_fat.py +110 -0
- devagent_spatial/v07_native.py +318 -0
- devagent_spatial/v083_native.py +349 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""V0.7: create-only, offline, read-only Perspective candidate + route.
|
|
2
|
+
|
|
3
|
+
DOES NOT import to Gateway, connect to PLC, certify physical mappings or execute FAT.
|
|
4
|
+
Binding source is the explicit engineer-owned lab manifest; no NodeId -> tag path inference.
|
|
5
|
+
All generated resources are sanitized: no scripts, events, outputs, commands, or writes.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import stat
|
|
14
|
+
import tempfile
|
|
15
|
+
import zipfile
|
|
16
|
+
from copy import deepcopy
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from .generator import fat_inventory, slugify, ROOT
|
|
20
|
+
from .ignition_native import NativeHold, _j, _label, _flex, build_view, _template_reference, native_member_names, sha, VIEW_PATH
|
|
21
|
+
from .ignition_project import _project_blob, _zip_blob
|
|
22
|
+
from .model import qualify
|
|
23
|
+
from .strict_json import loads as strict_loads
|
|
24
|
+
|
|
25
|
+
SCHEMA = 'devagent.ignition.v07.lab_compiler.v1'
|
|
26
|
+
MANIFEST_SCHEMA = 'devagent.ignition.v07.lab_bindings.v1'
|
|
27
|
+
CONFIRM = 'ISOLATED_OFFLINE_COMPILER_ONLY'
|
|
28
|
+
PAGE_CONFIG = 'com.inductiveautomation.perspective/page-config/config.json'
|
|
29
|
+
PAGE_RESOURCE = 'com.inductiveautomation.perspective/page-config/resource.json'
|
|
30
|
+
LAB_PATH = re.compile(r'^\[DevAgentLab[A-Za-z0-9_]{0,32}\](?:[A-Za-z0-9_][A-Za-z0-9_ -]{0,59}/)*[A-Za-z0-9_][A-Za-z0-9_ -]{0,59}$')
|
|
31
|
+
H64 = re.compile(r'^[0-9a-f]{64}$')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def canonical(obj: dict) -> bytes:
|
|
35
|
+
return (json.dumps(obj, sort_keys=True, ensure_ascii=False, separators=(',', ':'))+'\n').encode('utf-8')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def model_sha(model: dict) -> str:
|
|
39
|
+
return hashlib.sha256(canonical(model)).hexdigest()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def manifest_skeleton(model: dict) -> dict:
|
|
43
|
+
if qualify(model)['status'] != 'PASS_STATIC_CONTRACT_ONLY':
|
|
44
|
+
raise NativeHold('CUSTOMER_MODEL_HOLD')
|
|
45
|
+
return {'schema': MANIFEST_SCHEMA, 'model_sha256': model_sha(model),
|
|
46
|
+
'scope': 'ISOLATED_TEST_GATEWAY_ONLY', 'engineer': '',
|
|
47
|
+
'bindings': [{'signal_id':s['id'],'asset_id':s['asset_id'],'role':s['role'],
|
|
48
|
+
'source':deepcopy(s['source']),'tag_path':'',
|
|
49
|
+
'approval':'PENDING_ENGINEER_REVIEW'} for s in model.get('signals',[])],
|
|
50
|
+
'note':'No tag paths are inferred. Provide only isolated DevAgentLab provider paths from reviewed test Gateway.'}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def validate_manifest(model: dict, manifest: dict) -> dict:
|
|
54
|
+
if qualify(model)['status']!='PASS_STATIC_CONTRACT_ONLY':raise NativeHold('CUSTOMER_MODEL_HOLD')
|
|
55
|
+
if not isinstance(manifest,dict) or set(manifest)!={'schema','model_sha256','scope','engineer','bindings','note'}:
|
|
56
|
+
raise NativeHold('BINDING_MANIFEST_SCHEMA_INVALID')
|
|
57
|
+
if manifest['schema']!=MANIFEST_SCHEMA or manifest['model_sha256']!=model_sha(model):
|
|
58
|
+
raise NativeHold('BINDING_MANIFEST_MODEL_IDENTITY_MISMATCH')
|
|
59
|
+
if manifest['scope']!='ISOLATED_TEST_GATEWAY_ONLY':raise NativeHold('BINDING_SCOPE_NOT_ISOLATED')
|
|
60
|
+
if not isinstance(manifest['engineer'],str) or not 3<=len(manifest['engineer'].strip())<=120:
|
|
61
|
+
raise NativeHold('ENGINEER_ID_REQUIRED')
|
|
62
|
+
if not isinstance(manifest['note'],str) or len(manifest['note'])>600:
|
|
63
|
+
raise NativeHold('BINDING_MANIFEST_NOTE_INVALID')
|
|
64
|
+
bindings=manifest['bindings']
|
|
65
|
+
signals={s['id']:s for s in model['signals']}
|
|
66
|
+
if not signals or not isinstance(bindings,list) or len(bindings)!=len(signals):
|
|
67
|
+
raise NativeHold('ALL_DECLARED_SIGNALS_REQUIRE_EXPLICIT_DISPOSITION')
|
|
68
|
+
seen,paths,approved=set(),set(),{}
|
|
69
|
+
for entry in bindings:
|
|
70
|
+
if not isinstance(entry,dict) or set(entry)!={'signal_id','asset_id','role','source','tag_path','approval'}:
|
|
71
|
+
raise NativeHold('BINDING_ENTRY_SCHEMA_INVALID')
|
|
72
|
+
sid=entry['signal_id']
|
|
73
|
+
if not isinstance(sid,str) or sid not in signals or sid in seen:raise NativeHold('BINDING_SIGNAL_ID_UNKNOWN_OR_DUPLICATE')
|
|
74
|
+
seen.add(sid);s=signals[sid]
|
|
75
|
+
if any(entry[f]!=s[f] for f in ('asset_id','role','source')):
|
|
76
|
+
raise NativeHold('BINDING_SOURCE_OR_ASSET_MISMATCH: '+sid)
|
|
77
|
+
path=entry['tag_path']; decision=entry['approval']
|
|
78
|
+
if decision=='PENDING_ENGINEER_REVIEW':
|
|
79
|
+
if path!='':raise NativeHold('PENDING_BINDING_CANNOT_HAVE_PATH: '+sid)
|
|
80
|
+
elif decision=='APPROVED_FOR_ISOLATED_FAT_ONLY':
|
|
81
|
+
if not isinstance(path,str) or len(path)>220 or not LAB_PATH.fullmatch(path):
|
|
82
|
+
raise NativeHold('UNSAFE_OR_NON_LAB_TAG_PATH: '+sid)
|
|
83
|
+
if path in paths:raise NativeHold('DUPLICATE_LAB_TAG_PATH: '+sid)
|
|
84
|
+
paths.add(path);approved[sid]=path
|
|
85
|
+
else:
|
|
86
|
+
raise NativeHold('BINDING_APPROVAL_INVALID: '+sid)
|
|
87
|
+
if seen!=set(signals):raise NativeHold('BINDING_SIGNAL_COVERAGE_MISMATCH')
|
|
88
|
+
# Zero independently approved paths is a valid commissioning state. The
|
|
89
|
+
# generated HMI must show NOT MAPPED rather than invent a Gateway binding.
|
|
90
|
+
return approved
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _walk(component):
|
|
94
|
+
yield component
|
|
95
|
+
for child in component.get('children',[]):yield from _walk(child)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _bound_display(signal:dict, path: str | None):
|
|
99
|
+
label=_label('UNKNOWN' if path else 'NOT MAPPED',13,True,
|
|
100
|
+
'#e6bd70' if path is None else '#ecf5ff',name='LiveValue')
|
|
101
|
+
if path:
|
|
102
|
+
# Perspective defaults must retain BAD/STALE quality overlays (no opt-out).
|
|
103
|
+
label['propConfig']={'props.text':{'binding':{'type':'tag',
|
|
104
|
+
'config':{'mode':'direct','tagPath':path}}}}
|
|
105
|
+
return _flex([
|
|
106
|
+
_label('RAW '+signal['role'].replace('_',' ').upper(),11,False,'#82a6b9',name='SignalRole'),
|
|
107
|
+
label,
|
|
108
|
+
_label(signal.get('unit','') or ' ',10,False,'#82a6b9',name='EngineeringUnit')
|
|
109
|
+
],name='SignalRow',direction='row',style={'alignItems':'center','justifyContent':'space-between',
|
|
110
|
+
'gap':12,'borderTop':'1px solid #294b61','paddingTop':7,'paddingBottom':3})
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def bound_view(model:dict,manifest:dict):
|
|
114
|
+
approved=validate_manifest(model,manifest)
|
|
115
|
+
view=build_view(model)
|
|
116
|
+
# Preserve R9 geometry and safety semantics while applying approved brand palette.
|
|
117
|
+
design=strict_loads((ROOT/'design-system.json').read_text(encoding='utf8'))
|
|
118
|
+
brand=design['palettes'][model.get('palette','r9_cyan')]
|
|
119
|
+
token_map={'#07131f':brand['background'], '#0e2638':brand['panel'],
|
|
120
|
+
'#294b61':brand['border'], '#58c1dc':brand['accent'],
|
|
121
|
+
'#82a6b9':brand['muted']}
|
|
122
|
+
# Replace the conceptual rows, never reuse untrusted reference scripts.
|
|
123
|
+
signals={s['id']:s for s in model['signals']}
|
|
124
|
+
by_asset={e['id']:[] for e in model['equipment']}
|
|
125
|
+
for s in model['signals']:by_asset[s['asset_id']].append(s)
|
|
126
|
+
for comp in _walk(view['root']):
|
|
127
|
+
if comp.get('meta',{}).get('name')!='AssetCard':continue
|
|
128
|
+
idlabels=[c for c in comp['children'] if c.get('meta',{}).get('name')=='AssetId']
|
|
129
|
+
if len(idlabels)!=1:raise NativeHold('NATIVE_ASSET_CARD_ID_INVALID')
|
|
130
|
+
eid=idlabels[0]['props']['text']
|
|
131
|
+
static=[c for c in comp['children'] if c.get('meta',{}).get('name') in ('AssetId','AssetName','AssetDetail')]
|
|
132
|
+
comp['children']=static+[_bound_display(s,approved.get(s['id']))
|
|
133
|
+
for s in sorted(by_asset[eid],key=lambda item:item['role'])]
|
|
134
|
+
if not by_asset[eid]:comp['children'].append(_label('NOT MAPPED',12,True,'#e6bd70',name='UnmappedAsset'))
|
|
135
|
+
# VIEW BUILD: Never present unverified values as real PLC telemetry.
|
|
136
|
+
for comp in _walk(view['root']):
|
|
137
|
+
if comp.get('meta',{}).get('name')=='ReleaseNotice':
|
|
138
|
+
comp['props']['text']='ISOLATED LAB CANDIDATE • BINDINGS NOT PHYSICALLY VERIFIED • READ ONLY'
|
|
139
|
+
def repaint(value):
|
|
140
|
+
if isinstance(value,dict):return {k:repaint(v) for k,v in value.items()}
|
|
141
|
+
if isinstance(value,list):return [repaint(x) for x in value]
|
|
142
|
+
if isinstance(value,str):
|
|
143
|
+
for old,new in token_map.items():value=value.replace(old,new)
|
|
144
|
+
return value
|
|
145
|
+
view=repaint(view)
|
|
146
|
+
if sum(1 for _ in _walk(view['root']))>1600:raise NativeHold('NATIVE_VIEW_COMPLEXITY_LIMIT')
|
|
147
|
+
return view
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _page_resource(reference:Path,template_view:str):
|
|
151
|
+
match=VIEW_PATH.fullmatch(template_view)
|
|
152
|
+
if not match:raise NativeHold('TEMPLATE_VIEW_PATH_UNSUPPORTED')
|
|
153
|
+
prefix=match.group('prefix')
|
|
154
|
+
pc,pr=prefix+PAGE_CONFIG,prefix+PAGE_RESOURCE
|
|
155
|
+
with zipfile.ZipFile(reference) as z:
|
|
156
|
+
names=set(z.namelist())
|
|
157
|
+
if pc not in names or pr not in names:
|
|
158
|
+
raise NativeHold('REAL_REFERENCE_PAGE_CONFIG_REQUIRED')
|
|
159
|
+
config=_j(z.read(pc),'REFERENCE_PAGE_CONFIG')
|
|
160
|
+
resource=_j(z.read(pr),'REFERENCE_PAGE_RESOURCE')
|
|
161
|
+
if not isinstance(config,dict) or not isinstance(config.get('pages'),dict):
|
|
162
|
+
raise NativeHold('REFERENCE_PAGE_CONFIG_SHAPE_UNSUPPORTED')
|
|
163
|
+
if not isinstance(resource,dict) or 'config.json' not in resource.get('files',[]):
|
|
164
|
+
raise NativeHold('REFERENCE_PAGE_RESOURCE_SHAPE_UNSUPPORTED')
|
|
165
|
+
return pc,pr,resource
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def expected_members(model:dict,manifest:dict,reference:Path,template_view:str,
|
|
169
|
+
reference_sha256:str,new_project_name:str,*,project_enabled:bool=True):
|
|
170
|
+
approved=validate_manifest(model,manifest)
|
|
171
|
+
if not H64.fullmatch(reference_sha256):raise NativeHold('EXACT_REFERENCE_SHA256_REQUIRED')
|
|
172
|
+
reference=Path(reference)
|
|
173
|
+
index,base_resource=_template_reference(reference,template_view,reference_sha256)
|
|
174
|
+
# This routed lab artifact is executable only as a new isolated project.
|
|
175
|
+
# It remains read-only and production-held. The older generic project
|
|
176
|
+
# candidate API stays disabled by default.
|
|
177
|
+
project_path,project_blob,project_info=_project_blob(
|
|
178
|
+
reference,template_view,new_project_name,enabled=project_enabled)
|
|
179
|
+
pc,pr,page_res=_page_resource(reference,template_view)
|
|
180
|
+
name=slugify(model['name']).replace('-','_')
|
|
181
|
+
view_path,res_path=native_member_names(template_view,name)
|
|
182
|
+
if view_path in index['views']:raise NativeHold('TARGET_VIEW_ALREADY_EXISTS_IN_REFERENCE')
|
|
183
|
+
if len(model.get('equipment',[]))>150:raise NativeHold('EQUIPMENT_LIMIT')
|
|
184
|
+
def resource_from(template,filename):
|
|
185
|
+
metadata={k:deepcopy(template[k]) for k in ('scope','version','restricted','overridable') if k in template}
|
|
186
|
+
metadata['attributes']={}
|
|
187
|
+
metadata['files']=[filename]
|
|
188
|
+
return metadata
|
|
189
|
+
view=bound_view(model,manifest)
|
|
190
|
+
route={'pages':{'/':{'viewPath':'DevAgentGenerated/'+name,
|
|
191
|
+
'title':'DevAgent • '+model['name'][:85]}},'sharedDocks':{}}
|
|
192
|
+
members={project_path:project_blob,
|
|
193
|
+
view_path:canonical(view),res_path:canonical(resource_from(base_resource,'view.json')),
|
|
194
|
+
pc:canonical(route),pr:canonical(resource_from(page_res,'config.json'))}
|
|
195
|
+
if len(members)!=5:raise NativeHold('NATIVE_MEMBER_COLLISION')
|
|
196
|
+
return members,{'approved_bindings':len(approved),'signals':len(model['signals']),
|
|
197
|
+
'unmapped_signals':len(model['signals'])-len(approved),
|
|
198
|
+
'project_source_info':project_info,'page_route':'/',
|
|
199
|
+
'fat_signal_coverage':len([c for c in fat_inventory(model) if c.get('signal_id')])}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _verify_resource_safety(members:dict,signal_count:int,approved_count:int):
|
|
203
|
+
if len(members)!=5:raise NativeHold('PROJECT_RESOURCE_COUNT_INVALID')
|
|
204
|
+
views=[k for k in members if k.endswith('/view.json')]
|
|
205
|
+
if len(views)!=1:raise NativeHold('VIEW_COUNT_INVALID')
|
|
206
|
+
v=strict_loads(members[views[0]].decode('utf8'))
|
|
207
|
+
count=0;names=[]
|
|
208
|
+
def walk(c):
|
|
209
|
+
nonlocal count
|
|
210
|
+
count+=1
|
|
211
|
+
if not isinstance(c,dict) or c.get('type') not in ('ia.container.flex','ia.display.label'):
|
|
212
|
+
raise NativeHold('NON_ALLOWLISTED_COMPONENT')
|
|
213
|
+
if any(k in c for k in ('events','customMethods','scripts','onChange')):
|
|
214
|
+
raise NativeHold('ACTIVE_COMPONENT_FEATURE_PROHIBITED')
|
|
215
|
+
for binding in c.get('propConfig',{}).values():
|
|
216
|
+
if set(binding)!={'binding'} or set(binding['binding'])!={'type','config'}:
|
|
217
|
+
raise NativeHold('BINDING_FEATURE_NOT_ALLOWLISTED')
|
|
218
|
+
if binding['binding']['type']!='tag':raise NativeHold('NON_TAG_BINDING_DISALLOWED')
|
|
219
|
+
cfg=binding['binding']['config']
|
|
220
|
+
if set(cfg)!={'mode','tagPath'} or cfg['mode']!='direct' or not LAB_PATH.fullmatch(cfg['tagPath']):
|
|
221
|
+
raise NativeHold('NON_LAB_BINDING_REJECTED')
|
|
222
|
+
names.append(cfg['tagPath'])
|
|
223
|
+
for child in c.get('children',[]):walk(child)
|
|
224
|
+
walk(v['root'])
|
|
225
|
+
if count>1600 or len(names)!=approved_count or len(names)!=len(set(names)) or len(names)>signal_count:
|
|
226
|
+
raise NativeHold('COMPILER_BINDING_COUNT_OR_COMPLEXITY_MISMATCH')
|
|
227
|
+
# Sanity guard for serialized write/script tokens, even if injected in innocuous attributes.
|
|
228
|
+
serialized=json.dumps(v).lower()
|
|
229
|
+
for token in ('writeblocking','writeasync','system.opc.write','onactionperformed','"bidirectional": true',
|
|
230
|
+
'"overlayoptout": true','"script"'):
|
|
231
|
+
if token in serialized:raise NativeHold('FORBIDDEN_RUNTIME_FEATURE: '+token)
|
|
232
|
+
return names
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def compile_lab(model,manifest,reference,template_view,reference_sha256,new_project_name,out,confirm):
|
|
236
|
+
if confirm!=CONFIRM:raise NativeHold('EXPLICIT_ISOLATED_COMPILER_CONFIRMATION_REQUIRED')
|
|
237
|
+
target=Path(out).expanduser().absolute()
|
|
238
|
+
if target.suffix.lower()!='.zip' or not target.parent.is_dir() or target.exists() or target.is_symlink():
|
|
239
|
+
raise NativeHold('NEW_ZIP_OUTPUT_REQUIRED_CREATE_ONLY')
|
|
240
|
+
reference=Path(reference).expanduser().absolute()
|
|
241
|
+
before=sha(reference)
|
|
242
|
+
members,meta=expected_members(model,manifest,reference,template_view,reference_sha256,new_project_name)
|
|
243
|
+
if before!=sha(reference):raise NativeHold('REFERENCE_CHANGED_DURING_COMPILATION')
|
|
244
|
+
_verify_resource_safety(members,meta['signals'],meta['approved_bindings'])
|
|
245
|
+
payload=_zip_blob(members)
|
|
246
|
+
fd,temp=tempfile.mkstemp(prefix='.devagent-v07-',suffix='.zip',dir=str(target.parent))
|
|
247
|
+
try:
|
|
248
|
+
with os.fdopen(fd,'wb') as f:
|
|
249
|
+
f.write(payload);f.flush();os.fsync(f.fileno())
|
|
250
|
+
if Path(temp).read_bytes()!=payload:raise NativeHold('STAGED_PROJECT_BYTES_CHANGED')
|
|
251
|
+
try:os.link(temp,target)
|
|
252
|
+
except FileExistsError:raise NativeHold('OUTPUT_ALREADY_EXISTS_AT_PUBLISH') from None
|
|
253
|
+
try:os.chmod(target,0o600)
|
|
254
|
+
except OSError:pass
|
|
255
|
+
finally:Path(temp).unlink(missing_ok=True)
|
|
256
|
+
return {'schema':SCHEMA,'status':'PASS_OFFLINE_BOUND_PROJECT_CANDIDATE_ONLY',
|
|
257
|
+
'project_zip':str(target),'project_sha256':sha(target),
|
|
258
|
+
'reference_sha256':before,'model_sha256':model_sha(model),
|
|
259
|
+
'manifest_sha256':hashlib.sha256(canonical(manifest)).hexdigest(),
|
|
260
|
+
'native_view':'GENERATED','native_page_routing':'GENERATED_UNTESTED',
|
|
261
|
+
'tag_bindings':'GENERATED_UNTESTED',**meta,
|
|
262
|
+
'project_enabled_for_isolated_test':True,
|
|
263
|
+
'unmapped_display_policy':'NOT MAPPED',
|
|
264
|
+
'runtime_import':'NOT_TESTED','gateway_fat':'NOT_EXECUTED',
|
|
265
|
+
'physical_mapping':'NOT_VERIFIED','production_release':'HOLD'}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def verify_lab(model,manifest,reference,template_view,reference_sha256,new_project_name,candidate):
|
|
269
|
+
target=Path(candidate)
|
|
270
|
+
if not target.is_file() or target.is_symlink():raise NativeHold('CANDIDATE_NOT_SAFE_FILE')
|
|
271
|
+
members,meta=expected_members(model,manifest,Path(reference),template_view,reference_sha256,new_project_name)
|
|
272
|
+
if target.stat().st_size>4_000_000:raise NativeHold('CANDIDATE_TOO_LARGE')
|
|
273
|
+
try:
|
|
274
|
+
with zipfile.ZipFile(target) as z:
|
|
275
|
+
actual=z.infolist()
|
|
276
|
+
if len(actual)!=5 or set(z.namelist())!=set(members) or z.testzip():
|
|
277
|
+
raise NativeHold('CANDIDATE_RESOURCE_ALLOWLIST_HOLD')
|
|
278
|
+
for item in actual:
|
|
279
|
+
if item.flag_bits&1 or stat.S_IFMT((item.external_attr>>16)&0xffff)==stat.S_IFLNK:
|
|
280
|
+
raise NativeHold('CANDIDATE_SYMLINK_OR_ENCRYPTED')
|
|
281
|
+
if z.read(item)!=members[item.filename]:raise NativeHold('CANDIDATE_REPRODUCIBILITY_MISMATCH')
|
|
282
|
+
except zipfile.BadZipFile:raise NativeHold('CANDIDATE_ZIP_INVALID') from None
|
|
283
|
+
if target.read_bytes()!=_zip_blob(members):raise NativeHold('CANDIDATE_NOT_BYTE_REPRODUCIBLE')
|
|
284
|
+
_verify_resource_safety(members,meta['signals'],meta['approved_bindings'])
|
|
285
|
+
return {'schema':SCHEMA,'status':'PASS_OFFLINE_RESOURCE_REPRODUCIBILITY_ONLY',
|
|
286
|
+
'project_sha256':sha(target),**meta,'gateway_fat':'NOT_EXECUTED',
|
|
287
|
+
'project_enabled_for_isolated_test':True,
|
|
288
|
+
'unmapped_display_policy':'NOT MAPPED',
|
|
289
|
+
'runtime_import':'NOT_TESTED','production_release':'HOLD'}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def fat_plan(model:dict,manifest:dict) -> dict:
|
|
293
|
+
approved=validate_manifest(model,manifest)
|
|
294
|
+
inventory=fat_inventory(model)
|
|
295
|
+
per_signal={s['id']:[] for s in model['signals']}
|
|
296
|
+
for case in inventory:
|
|
297
|
+
if case.get('signal_id') in per_signal:per_signal[case['signal_id']].append(case['id'])
|
|
298
|
+
if any(len(cases)!=1 for cases in per_signal.values()):
|
|
299
|
+
raise NativeHold('FAT_INVENTORY_LACKS_EXACTLY_ONE_CASE_PER_SIGNAL')
|
|
300
|
+
cases=[]
|
|
301
|
+
for s in model['signals']:
|
|
302
|
+
cases.append({'id':per_signal[s['id']][0], 'signal_id':s['id'],
|
|
303
|
+
'equipment_id':s['asset_id'],'role':s['role'],
|
|
304
|
+
'tag_binding':'LAB_DECLARED_NOT_GATEWAY_VERIFIED' if s['id'] in approved else 'NOT_MAPPED',
|
|
305
|
+
'phases':['VALID_SOURCE_IDENTITY','OPC_READBACK','IGNITION_QUALIFIED_VALUE',
|
|
306
|
+
'PERSPECTIVE_RENDER','GOOD_TO_BAD_QUALITY','STALE_OR_COMMS_LOSS'],
|
|
307
|
+
'result':'HOLD_NOT_EXECUTED','evidence':[], 'plc_write_allowed':False})
|
|
308
|
+
requirements=[{'id':'REQ_'+r['id'],'requirement_id':r['id'],
|
|
309
|
+
'text':r['text'],'result':'HOLD_NOT_EXECUTED',
|
|
310
|
+
'reason':'ACCEPTANCE_CRITERIA_AND_SIGNAL_TRACEABILITY_REQUIRE_ENGINEER_APPROVAL',
|
|
311
|
+
'required_evidence':['APPROVED_ACCEPTANCE_CRITERIA','IGNITION_HMI_FAT','ENGINEER_SIGNOFF']}
|
|
312
|
+
for r in model.get('requirements',[])]
|
|
313
|
+
return {'schema':'devagent.fat.v07.execution_plan.v1',
|
|
314
|
+
'model_sha256':model_sha(model),'signal_count':len(model['signals']),
|
|
315
|
+
'individual_signal_coverage':len(cases),
|
|
316
|
+
'mapping_coverage':len(approved),'requirements_count':len(model.get('requirements',[])),
|
|
317
|
+
'cases':cases,'requirement_cases':requirements,'gateway_fat':'NOT_EXECUTED','physical_mapping':'NOT_VERIFIED',
|
|
318
|
+
'production_release':'HOLD'}
|