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
devagent_hmi.py ADDED
@@ -0,0 +1,471 @@
1
+ #!/usr/bin/env python3
2
+ """DevAgent Spatial HMI Engine — independent local design and validation tool.
3
+
4
+ This program NEVER imports into Ignition, modifies PLC tags, or writes OPC UA.
5
+ """
6
+ from __future__ import annotations
7
+ import argparse
8
+ import json
9
+ import zipfile
10
+ from functools import partial
11
+ from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
12
+ from pathlib import Path
13
+ from devagent_spatial.generator import build, BuildHold, ROOT
14
+ from devagent_spatial.model import qualify
15
+ from devagent_spatial.strict_json import loads as strict_loads
16
+
17
+
18
+ def main(argv=None):
19
+ p=argparse.ArgumentParser(description='DevAgent Spatial HMI — model-driven preview and static FAT contract generator (read-only)')
20
+ sub=p.add_subparsers(dest='action',required=True)
21
+ a=sub.add_parser('validate',help='Validate JSON project contract without writing files')
22
+ a.add_argument('--model',type=Path,required=True)
23
+ a=sub.add_parser('build',help='Create a NEW preview artifact directory (no overwrite)')
24
+ a.add_argument('--model',type=Path,required=True)
25
+ a.add_argument('--out',type=Path,required=True)
26
+ a.add_argument('--vendor-three',type=Path,default=None,help='Existing LOCAL Three.js r180 vendor directory with license; source is not changed')
27
+ a=sub.add_parser('studio',help='Start localhost customer project creator, no remote bind')
28
+ a.add_argument('--port',type=int,default=8895)
29
+ a.add_argument('--workspace',type=Path,default=Path.home()/'.local/share/devagent/spatial-projects')
30
+ a.add_argument('--vendor-three',type=Path,default=None,help='Optional local r180 vendor folder, e.g. R9 viewer web/vendor')
31
+ a=sub.add_parser('preview',help='Serve an existing generated preview over localhost HTTP')
32
+ a.add_argument('--project-dir',type=Path,required=True)
33
+ a.add_argument('--port',type=int,default=8896)
34
+ a=sub.add_parser('opc-snapshot',help='READ-ONLY isolated-lab OPC UA node/type/quality verification; no trigger or HMI claims')
35
+ a.add_argument('--model',type=Path,required=True)
36
+ a.add_argument('--endpoint',required=True)
37
+ a.add_argument('--lab-confirm',required=True)
38
+ a=sub.add_parser('qualify',help='Independent artifact integrity/traceability audit, preview or production gate')
39
+ a.add_argument('--project-dir',type=Path,required=True)
40
+ a.add_argument('--purpose',choices=['preview','production'],default='preview')
41
+ a=sub.add_parser('audit-ignition',help='Read-only safety audit of a native Ignition Perspective ZIP (not import qualification)')
42
+ a.add_argument('--project-zip',type=Path,required=True)
43
+ a=sub.add_parser('ignition-list',help='List views in a customer-provided native Ignition project export, without mutation')
44
+ a.add_argument('--reference-zip',type=Path,required=True)
45
+ a=sub.add_parser('ignition-candidate',help='Build read-only native Perspective view RESOURCE overlay (not a verified project export)')
46
+ a.add_argument('--model',type=Path,required=True)
47
+ a.add_argument('--reference-zip',type=Path,required=True)
48
+ a.add_argument('--reference-sha256',required=True)
49
+ a.add_argument('--template-view',required=True)
50
+ a.add_argument('--out',type=Path,required=True)
51
+ a=sub.add_parser('ignition-verify-candidate',help='Independently compare view candidate against original customer model and reference')
52
+ a.add_argument('--model',type=Path,required=True)
53
+ a.add_argument('--reference-zip',type=Path,required=True)
54
+ a.add_argument('--reference-sha256',required=True)
55
+ a.add_argument('--template-view',required=True)
56
+ a.add_argument('--candidate',type=Path,required=True)
57
+ a=sub.add_parser('ignition-project-candidate',help='Build a NEW minimal read-only isolated Ignition project ZIP candidate; never imports')
58
+ a.add_argument('--model',type=Path,required=True)
59
+ a.add_argument('--reference-zip',type=Path,required=True)
60
+ a.add_argument('--reference-sha256',required=True)
61
+ a.add_argument('--template-view',required=True)
62
+ a.add_argument('--new-project-name',required=True)
63
+ a.add_argument('--confirm',required=True)
64
+ a.add_argument('--out',type=Path,required=True)
65
+ a=sub.add_parser('ignition-verify-project',help='Independently verify a new minimal project candidate')
66
+ a.add_argument('--model',type=Path,required=True)
67
+ a.add_argument('--reference-zip',type=Path,required=True)
68
+ a.add_argument('--reference-sha256',required=True)
69
+ a.add_argument('--template-view',required=True)
70
+ a.add_argument('--new-project-name',required=True)
71
+ a.add_argument('--candidate',type=Path,required=True)
72
+ a=sub.add_parser('ops-studio-lab',help='Create copy-only Studio import CSV + lab-only remapped model, original unchanged')
73
+ a.add_argument('--model',type=Path,required=True)
74
+ a.add_argument('--out-dir',type=Path,required=True)
75
+ a.add_argument('--confirm',required=True)
76
+ a=sub.add_parser('ops-demo',help='Create synthetic LAB ONLY alarm/KPI replay JSON, never live proof')
77
+ a.add_argument('--model',type=Path,required=True)
78
+ a.add_argument('--out',type=Path,required=True)
79
+ a=sub.add_parser('ops-evaluate',help='Validate external isolated-lab observation frames by exact customer tag identity')
80
+ a.add_argument('--model',type=Path,required=True)
81
+ a.add_argument('--frames',type=Path,required=True)
82
+ a.add_argument('--out',type=Path,required=True)
83
+ a=sub.add_parser('ops-capture',help='Bounded READ ONLY OPC UA capture on approved isolated lab endpoint')
84
+ a.add_argument('--model',type=Path,required=True)
85
+ a.add_argument('--endpoint',required=True)
86
+ a.add_argument('--lab-confirm',required=True)
87
+ a.add_argument('--samples',type=int,default=3)
88
+ a.add_argument('--out',type=Path,required=True)
89
+ a=sub.add_parser('v07-binding-skeleton',help='Create NEW customer lab-tag manifest template, with NO guessed tag paths')
90
+ a.add_argument('--model',type=Path,required=True)
91
+ a.add_argument('--out',type=Path,required=True)
92
+ a=sub.add_parser('v07-native-compile',help='Compile isolated native Perspective project candidate with read-only lab bindings + route')
93
+ a.add_argument('--model',type=Path,required=True)
94
+ a.add_argument('--binding-manifest',type=Path,required=True)
95
+ a.add_argument('--reference-zip',type=Path,required=True)
96
+ a.add_argument('--reference-sha256',required=True)
97
+ a.add_argument('--template-view',required=True)
98
+ a.add_argument('--new-project-name',required=True)
99
+ a.add_argument('--confirm',required=True)
100
+ a.add_argument('--out',type=Path,required=True)
101
+ a=sub.add_parser('v07-native-verify',help='Byte-for-byte independent verifier for V0.7 offline project candidate')
102
+ a.add_argument('--model',type=Path,required=True)
103
+ a.add_argument('--binding-manifest',type=Path,required=True)
104
+ a.add_argument('--reference-zip',type=Path,required=True)
105
+ a.add_argument('--reference-sha256',required=True)
106
+ a.add_argument('--template-view',required=True)
107
+ a.add_argument('--new-project-name',required=True)
108
+ a.add_argument('--candidate',type=Path,required=True)
109
+ a=sub.add_parser('v07-fat-plan',help='Create unexecuted per-signal FAT plan: signal coverage and quality transitions')
110
+ a.add_argument('--model',type=Path,required=True)
111
+ a.add_argument('--binding-manifest',type=Path,required=True)
112
+ a.add_argument('--out',type=Path,required=True)
113
+ for action in ('v083-native-compile', 'v083-native-verify'):
114
+ a=sub.add_parser(action,help='Create/verify native operator candidate; UI selection only, no command authority')
115
+ a.add_argument('--model',type=Path,required=True)
116
+ a.add_argument('--binding-manifest',type=Path,required=True)
117
+ a.add_argument('--operator-policy',type=Path,help='Reviewed PE association and freshness policy; omitted means all pending')
118
+ a.add_argument('--reference-zip',type=Path,required=True)
119
+ a.add_argument('--reference-sha256',required=True)
120
+ a.add_argument('--template-view',required=True)
121
+ a.add_argument('--new-project-name',required=True)
122
+ if action=='v083-native-compile':
123
+ a.add_argument('--confirm',required=True)
124
+ a.add_argument('--out',type=Path,required=True)
125
+ else:
126
+ a.add_argument('--candidate',type=Path,required=True)
127
+ a=sub.add_parser('v07-fat-assess',help='Evaluate supplied OPC/Tag/HMI phase consistency; never asserts live FAT PASS')
128
+ a.add_argument('--candidate',type=Path,required=True)
129
+ a.add_argument('--model',type=Path,required=True)
130
+ a.add_argument('--binding-manifest',type=Path,required=True)
131
+ a.add_argument('--observations',type=Path,required=True)
132
+ a.add_argument('--out',type=Path,required=True)
133
+ a=sub.add_parser('v076-opc-plan',help='Generate synthetic OPC UA lab plan only; no protocol execution')
134
+ a.add_argument('--model',type=Path,required=True)
135
+ a.add_argument('--out',type=Path,required=True)
136
+ a=sub.add_parser('v076-opc-fat',help='Execute READ-ONLY-TO-CLIENTS fake OPC UA loopback FAT with independent client; NO IGNITION CLAIM')
137
+ a.add_argument('--model',type=Path,required=True)
138
+ a.add_argument('--out',type=Path,required=True)
139
+ a.add_argument('--port',type=int,default=4898)
140
+ a.add_argument('--confirm',required=True)
141
+ a=sub.add_parser('v078-command-template',help='Create UNAPPROVED lab-only Device Command Contract for a declared equipment ID')
142
+ a.add_argument('--model',type=Path,required=True)
143
+ a.add_argument('--asset-id',required=True)
144
+ a.add_argument('--out',type=Path,required=True)
145
+ a=sub.add_parser('v078-virtual-plc-serve',help='Serve bounded live fake OPC UA Virtual PLC on localhost for manual Ignition FAT')
146
+ a.add_argument('--model',type=Path,required=True)
147
+ a.add_argument('--contract',type=Path,required=True)
148
+ a.add_argument('--port',type=int,default=4899)
149
+ a.add_argument('--seconds',type=int,default=300)
150
+ a.add_argument('--confirm',required=True)
151
+ a=sub.add_parser('v078-virtual-plc-fat',help='Execute fake OPC UA START/STOP/RESET interlock/feedback FAT on loopback only')
152
+ a.add_argument('--model',type=Path,required=True)
153
+ a.add_argument('--contract',type=Path,required=True)
154
+ a.add_argument('--out',type=Path,required=True)
155
+ a.add_argument('--port',type=int,default=4899)
156
+ a.add_argument('--confirm',required=True)
157
+ for action in ('v078-native-compile','v078-native-verify'):
158
+ a=sub.add_parser(action,help='Create/verify disabled-by-default LAB ONLY Perspective device command project candidate')
159
+ a.add_argument('--model',type=Path,required=True)
160
+ a.add_argument('--contract',type=Path,required=True)
161
+ a.add_argument('--binding-manifest',type=Path,required=True)
162
+ a.add_argument('--reference-zip',type=Path,required=True)
163
+ a.add_argument('--reference-sha256',required=True)
164
+ a.add_argument('--template-view',required=True)
165
+ a.add_argument('--new-project-name',required=True)
166
+ a.add_argument('--confirm',default='',required=action=='v078-native-compile')
167
+ if action=='v078-native-compile':a.add_argument('--out',type=Path,required=True)
168
+ else:a.add_argument('--candidate',type=Path,required=True)
169
+ a=sub.add_parser('preflight',help='Offline cross-project release checks; does NOT certify Ignition runtime')
170
+ a.add_argument('--reference-zip',type=Path,default=None)
171
+ a.add_argument('--reference-sha256',default=None)
172
+ a.add_argument('--template-view',default=None)
173
+ a.add_argument('--report',type=Path,default=None,help='Create a new JSON report; refuses overwrite')
174
+ a=sub.add_parser('examples',help='Show bundled customer model examples')
175
+ args=p.parse_args(argv)
176
+ if args.action.startswith('v078-'):
177
+ from devagent_spatial.v078_commands import (contract_template,validate_contract,run_virtual_plc_fat,
178
+ CommandHold,LAB_CONFIRM)
179
+ try:
180
+ if args.model.stat().st_size>2_000_000:raise CommandHold('MODEL_TOO_LARGE')
181
+ model=strict_loads(args.model.read_text(encoding='utf8'))
182
+ if args.action=='v078-command-template':
183
+ result=contract_template(model,args.asset_id)
184
+ with args.out.open('x',encoding='utf8') as f:
185
+ json.dump(result,f,indent=2);f.write('\n')
186
+ print(json.dumps({'status':'PENDING_LAB_REVIEW','file':str(args.out),'production':'HOLD'}))
187
+ return 0
188
+ if args.contract.stat().st_size>100_000:raise CommandHold('CONTRACT_TOO_LARGE')
189
+ contract=strict_loads(args.contract.read_text(encoding='utf8'))
190
+ validate_contract(model,contract,require_lab_approval=True)
191
+ if args.action=='v078-virtual-plc-serve':
192
+ import asyncio
193
+ from devagent_spatial.v078_commands import serve_virtual_plc
194
+ print('LAB_VPLC_ENDPOINT=opc.tcp://127.0.0.1:'+str(args.port)+'/devagent-virtual-plc/')
195
+ print('LAB_VPLC_ONLY=YES | IGNITION_GATEWAY=NOT_VERIFIED | PRODUCTION=HOLD',flush=True)
196
+ result=asyncio.run(serve_virtual_plc(model,contract,port=args.port,
197
+ seconds=args.seconds,confirm=args.confirm))
198
+ print(json.dumps(result,indent=2));return 0
199
+ if args.action=='v078-virtual-plc-fat':
200
+ import asyncio
201
+ result=asyncio.run(asyncio.wait_for(run_virtual_plc_fat(
202
+ model,contract,port=args.port,confirm=args.confirm),timeout=45))
203
+ with args.out.open('x',encoding='utf8') as f:
204
+ json.dump(result,f,indent=2);f.write('\n')
205
+ print(json.dumps({'status':result['status'],'file':str(args.out),'production':'HOLD'}))
206
+ return 0 if result['status'].startswith('PASS_') else 2
207
+ from devagent_spatial.v078_native_commands import compile_lab_native,verify_lab_native
208
+ from devagent_spatial.ignition_native import NativeHold
209
+ if args.binding_manifest.stat().st_size>2_000_000:raise CommandHold('BINDING_MANIFEST_TOO_LARGE')
210
+ binding_manifest=strict_loads(args.binding_manifest.read_text(encoding='utf8'))
211
+ if args.action=='v078-native-compile':
212
+ result=compile_lab_native(model,binding_manifest,contract,args.reference_zip,
213
+ args.template_view,args.reference_sha256,args.new_project_name,args.out,args.confirm)
214
+ else:
215
+ result=verify_lab_native(model,binding_manifest,contract,args.reference_zip,
216
+ args.template_view,args.reference_sha256,args.new_project_name,args.candidate)
217
+ print(json.dumps(result,indent=2));return 0
218
+ except (OSError,CommandHold,ValueError,TypeError,KeyError,RecursionError,Exception) as ex:
219
+ # Only known stable error codes leave the CLI; never leak model credentials/hostnames.
220
+ from devagent_spatial.ignition_native import NativeHold
221
+ if not isinstance(ex,(OSError,CommandHold,ValueError,TypeError,KeyError,RecursionError,NativeHold)):
222
+ raise
223
+ code=str(ex)
224
+ import re
225
+ if not re.fullmatch('[A-Z0-9_:]{3,170}',code):code='LAB_COMMAND_OR_NATIVE_COMPILER_HOLD'
226
+ print('HOLD=V078: '+code);return 2
227
+ if args.action in ('v076-opc-plan','v076-opc-fat'):
228
+ from devagent_spatial.v076_opc_fat import plan,run_loopback_fat,save_new,LabHold
229
+ import asyncio
230
+ try:
231
+ if args.action=='v076-opc-fat' and args.confirm!='ISOLATED_FAKE_OPC_LOOPBACK_ONLY':
232
+ raise LabHold('EXPLICIT_LAB_CONFIRMATION_REQUIRED')
233
+ if args.model.stat().st_size>2_000_000:raise LabHold('MODEL_FILE_TOO_LARGE')
234
+ model=strict_loads(args.model.read_text(encoding='utf8'))
235
+ result=(plan(model) if args.action=='v076-opc-plan' else
236
+ asyncio.run(asyncio.wait_for(run_loopback_fat(model,args.port),timeout=50)))
237
+ save_new(args.out,result)
238
+ print(json.dumps({'status':result['status'],'report':str(args.out),
239
+ 'production':'HOLD','ignition':'NOT_VERIFIED'},indent=2))
240
+ return 0 if result['status'] in ('NOT_EXECUTED','PASS_LAB_OPC_PROTOCOL_ONLY') else 2
241
+ except (OSError,LabHold,ValueError,TypeError,KeyError,RecursionError) as e:
242
+ print('HOLD=V076_FAKE_OPC_FAT: '+str(e)[:160]);return 2
243
+ if args.action in ('v083-native-compile','v083-native-verify'):
244
+ from devagent_spatial.v083_native import candidate, policy_skeleton
245
+ from devagent_spatial.ignition_native import NativeHold
246
+ try:
247
+ for path in (args.model,args.binding_manifest,args.operator_policy):
248
+ if path and path.stat().st_size>2_000_000:
249
+ raise NativeHold('OPERATOR_INPUT_TOO_LARGE')
250
+ model=strict_loads(args.model.read_text(encoding='utf8'))
251
+ manifest=strict_loads(args.binding_manifest.read_text(encoding='utf8'))
252
+ policy=(strict_loads(args.operator_policy.read_text(encoding='utf8'))
253
+ if args.operator_policy else policy_skeleton(model))
254
+ verify=args.action=='v083-native-verify'
255
+ result=candidate(model,manifest,policy,args.reference_zip,args.template_view,
256
+ args.reference_sha256,args.new_project_name,
257
+ args.candidate if verify else args.out,verify=verify,
258
+ confirm='' if verify else args.confirm)
259
+ print(json.dumps(result,indent=2,ensure_ascii=False));return 0
260
+ except (NativeHold,OSError,ValueError,TypeError,KeyError,zipfile.BadZipFile,RecursionError) as ex:
261
+ print('HOLD=V083: '+str(ex)[:300]);return 2
262
+ if args.action.startswith('v07-'):
263
+ from devagent_spatial.v07_native import (manifest_skeleton, compile_lab, verify_lab,
264
+ fat_plan, NativeHold)
265
+ try:
266
+ if args.model.stat().st_size>2_000_000:raise NativeHold('MODEL_FILE_TOO_LARGE')
267
+ model=strict_loads(args.model.read_text(encoding='utf8'))
268
+ if args.action=='v07-binding-skeleton':
269
+ result=manifest_skeleton(model)
270
+ else:
271
+ if args.binding_manifest.stat().st_size>2_000_000:raise NativeHold('MANIFEST_FILE_TOO_LARGE')
272
+ manifest=strict_loads(args.binding_manifest.read_text(encoding='utf8'))
273
+ if args.action=='v07-fat-assess':
274
+ from devagent_spatial.v07_fat import assess
275
+ if args.observations.stat().st_size>2_000_000:raise NativeHold('OBSERVATIONS_TOO_LARGE')
276
+ observations=strict_loads(args.observations.read_text(encoding='utf8'))
277
+ result=assess(model,manifest,observations,args.candidate)
278
+ elif args.action=='v07-native-compile':
279
+ result=compile_lab(model,manifest,args.reference_zip,args.template_view,
280
+ args.reference_sha256,args.new_project_name,args.out,args.confirm)
281
+ elif args.action=='v07-native-verify':
282
+ result=verify_lab(model,manifest,args.reference_zip,args.template_view,
283
+ args.reference_sha256,args.new_project_name,args.candidate)
284
+ else:result=fat_plan(model,manifest)
285
+ if args.action in ('v07-binding-skeleton','v07-fat-plan','v07-fat-assess'):
286
+ with args.out.expanduser().open('x',encoding='utf8') as f:
287
+ json.dump(result,f,indent=2,ensure_ascii=False);f.write('\n')
288
+ print(json.dumps({'status':result.get('status','PASS_UNEXECUTED_PLAN_ONLY'),
289
+ 'file':str(args.out), 'production_release':'HOLD',
290
+ 'gateway_fat':'NOT_VERIFIED'},indent=2))
291
+ else:print(json.dumps(result,indent=2,ensure_ascii=False))
292
+ return 0
293
+ except (NativeHold,OSError,ValueError,TypeError,KeyError,zipfile.BadZipFile,RecursionError) as e:
294
+ print('HOLD=V07: '+str(e)[:400]);return 2
295
+ if args.action=='ops-studio-lab':
296
+ from devagent_spatial.operations_lab import create_lab_derivative
297
+ from devagent_spatial.operations import OperationsHold
298
+ try:
299
+ if args.model.stat().st_size>2_000_000:raise OperationsHold('MODEL_TOO_LARGE')
300
+ original=strict_loads(args.model.read_text(encoding='utf-8'))
301
+ result=create_lab_derivative(original,args.out_dir,args.confirm)
302
+ print(json.dumps(result,indent=2,ensure_ascii=False))
303
+ return 0
304
+ except (OSError,ValueError,KeyError,OperationsHold) as exc:
305
+ print('HOLD=STUDIO_LAB_ALIAS: '+str(exc)[:300]);return 2
306
+ if args.action in ('ops-demo','ops-evaluate','ops-capture'):
307
+ from devagent_spatial.operations import (evaluate,make_synthetic_frames,OperationsHold)
308
+ try:
309
+ if args.model.stat().st_size>2_000_000:raise OperationsHold('MODEL_TOO_LARGE')
310
+ model=strict_loads(args.model.read_text(encoding='utf-8'))
311
+ if args.action=='ops-demo':
312
+ frames=make_synthetic_frames(model)
313
+ origin='SYNTHETIC_OFFLINE_FIXTURE'
314
+ elif args.action=='ops-evaluate':
315
+ if args.frames.stat().st_size>2_000_000:raise OperationsHold('FRAMES_TOO_LARGE')
316
+ frames=strict_loads(args.frames.read_text(encoding='utf-8'))
317
+ origin='SYNTHETIC_OFFLINE_FIXTURE' # Unproven supplied frames cannot claim verified Studio readings.
318
+ else:
319
+ if args.lab_confirm!='ISOLATED_TEST_ONLY':raise OperationsHold('EXPLICIT_LAB_CONFIRMATION_REQUIRED')
320
+ if not 1<=args.samples<=12:raise OperationsHold('UNSAFE_CAPTURE_SAMPLE_COUNT')
321
+ import asyncio
322
+ from devagent_spatial.operations_capture import capture_lab
323
+ frames=asyncio.run(capture_lab(model,args.endpoint,args.samples))
324
+ origin='ISOLATED_LAB_OPC_READ_ONLY'
325
+ receipt=evaluate(model,frames,origin=origin)
326
+ with args.out.expanduser().open('x',encoding='utf-8') as f:
327
+ json.dump(receipt,f,indent=2,ensure_ascii=False)
328
+ f.write('\n')
329
+ print(json.dumps({'status':receipt['status'],'file':str(args.out),'frames':len(receipt['frames']),
330
+ 'events':len(receipt['events']),'mapping_digest':receipt['mapping_digest'],
331
+ 'ignition_hmi':'NOT_VERIFIED','plc_writes':'DISABLED'},indent=2))
332
+ return 0
333
+ except (OSError,ValueError,TypeError,KeyError,OperationsHold) as exc:
334
+ print('HOLD=OPERATIONS: '+str(exc)[:300]);return 2
335
+ if args.action=='preflight':
336
+ from devagent_spatial.preflight import preflight
337
+ result=preflight(reference_zip=args.reference_zip,
338
+ reference_sha256=args.reference_sha256,template_view=args.template_view)
339
+ blob=json.dumps(result,indent=2,ensure_ascii=False)+'\n'
340
+ if args.report:
341
+ try:
342
+ with args.report.open('x',encoding='utf-8') as f:f.write(blob)
343
+ except (OSError,ValueError) as exc:
344
+ print('HOLD=REPORT_CREATE_ONLY: '+str(exc));return 2
345
+ print(blob)
346
+ return 0 if result['status']=='PASS_OFFLINE_PRETEST_ONLY' else 2
347
+ if args.action in ('ignition-project-candidate','ignition-verify-project'):
348
+ from devagent_spatial.ignition_project import compile_project, verify_project
349
+ from devagent_spatial.ignition_native import NativeHold
350
+ try:
351
+ if args.model.stat().st_size>2_000_000:raise NativeHold('MODEL_FILE_TOO_LARGE')
352
+ model=strict_loads(args.model.read_text(encoding='utf-8'))
353
+ if args.action=='ignition-project-candidate':
354
+ result=compile_project(model,args.reference_zip,args.template_view,
355
+ args.reference_sha256,args.new_project_name,args.out,args.confirm)
356
+ else:
357
+ result=verify_project(model,args.reference_zip,args.template_view,
358
+ args.reference_sha256,args.new_project_name,args.candidate)
359
+ print(json.dumps(result,indent=2,ensure_ascii=False))
360
+ return 0
361
+ except (NativeHold,OSError,ValueError,KeyError,TypeError,zipfile.BadZipFile,RecursionError) as exc:
362
+ print('HOLD=IGNITION_PROJECT: '+str(exc)[:350])
363
+ return 2
364
+ if args.action in ('ignition-list','ignition-candidate','ignition-verify-candidate'):
365
+ from devagent_spatial.ignition_native import (find_views, compile_candidate,
366
+ verify_candidate,NativeHold)
367
+ try:
368
+ if args.action=='ignition-list':
369
+ result=find_views(args.reference_zip)
370
+ else:
371
+ if args.model.stat().st_size>2_000_000:raise NativeHold('MODEL_FILE_TOO_LARGE')
372
+ data=strict_loads(args.model.read_text(encoding='utf-8'))
373
+ if args.action=='ignition-candidate':
374
+ result=compile_candidate(data,args.reference_zip,args.template_view,
375
+ args.reference_sha256,args.out)
376
+ else:
377
+ result=verify_candidate(data,args.reference_zip,args.template_view,
378
+ args.reference_sha256,args.candidate)
379
+ print(json.dumps(result,indent=2,ensure_ascii=False))
380
+ return 0
381
+ except (NativeHold,OSError,ValueError,KeyError,TypeError,zipfile.BadZipFile) as e:
382
+ print('HOLD=IGNITION_NATIVE: '+str(e))
383
+ return 2
384
+ if args.action=='opc-snapshot':
385
+ from devagent_spatial.opc_snapshot import probe_live, SnapshotHold
386
+ import asyncio
387
+ if args.lab_confirm!='ISOLATED_TEST_ONLY':
388
+ print('HOLD=EXPLICIT_ISOLATED_LAB_CONFIRMATION_REQUIRED');return 2
389
+ try:
390
+ if args.model.stat().st_size>2_000_000:raise ValueError('MODEL_FILE_TOO_LARGE')
391
+ model=strict_loads(args.model.read_text(encoding='utf8'))
392
+ result=asyncio.run(probe_live(model,args.endpoint))
393
+ print(json.dumps(result,indent=2,ensure_ascii=False))
394
+ return 0 if result['status']=='PASS_LAB_PROTOCOL_SNAPSHOT_ONLY' else 2
395
+ except (OSError,ValueError,SnapshotHold) as e:
396
+ print('HOLD=OPC_SNAPSHOT: '+str(e));return 2
397
+ if args.action=='qualify':
398
+ from devagent_spatial.qualification import audit
399
+ report=audit(args.project_dir,purpose=args.purpose)
400
+ print(json.dumps(report,indent=2,ensure_ascii=False))
401
+ return 0 if report['status']=='PASS_CONCEPT_PREVIEW_ONLY' else 2
402
+ if args.action=='audit-ignition':
403
+ from devagent_spatial.native_reference import audit_ignition_archive
404
+ report=audit_ignition_archive(args.project_zip)
405
+ print(json.dumps(report,indent=2,ensure_ascii=False))
406
+ return 0 if report['status']=='PASS_ZIP_SAFETY_ONLY' else 2
407
+ if args.action=='examples':
408
+ print('WAREHOUSE_EXAMPLE='+str(ROOT/'examples/warehouse.json'))
409
+ print('WATER_TREATMENT_EXAMPLE='+str(ROOT/'examples/water.json'))
410
+ print('SINGLE_FLOOR_EXAMPLE='+str(ROOT/'examples/single_floor.json'))
411
+ return 0
412
+ if args.action in ('validate','build'):
413
+ try:
414
+ raw_path=args.model.expanduser()
415
+ if raw_path.stat().st_size > 2_000_000:raise ValueError('INPUT_FILE_TOO_LARGE')
416
+ data=strict_loads(raw_path.read_text(encoding='utf-8'))
417
+ except (OSError,ValueError,RecursionError,UnicodeError) as e:
418
+ print('HOLD=INPUT_UNREADABLE_OR_INVALID_JSON: '+str(e))
419
+ return 2
420
+ if args.action=='validate':
421
+ report=qualify(data)
422
+ print(json.dumps(report,indent=2,ensure_ascii=False))
423
+ return 0 if report['status']!='HOLD' else 2
424
+ try:
425
+ result=build(data,args.out,vendor_three=args.vendor_three)
426
+ print(json.dumps(result,indent=2,ensure_ascii=False))
427
+ return 0
428
+ except (BuildHold,OSError,ValueError) as e:
429
+ print('HOLD='+str(e))
430
+ return 2
431
+ if not 1024<=args.port<=65535:
432
+ print('HOLD=INVALID_PORT')
433
+ return 2
434
+ if args.action=='studio':
435
+ from devagent_spatial.studio import StudioServer
436
+ try:
437
+ server=StudioServer(('127.0.0.1',args.port),args.workspace,args.vendor_three)
438
+ except (OSError,ValueError,RecursionError,UnicodeError) as e:
439
+ print('HOLD=STUDIO_START: '+str(e))
440
+ return 2
441
+ print('DEVAGENT_SPATIAL_STUDIO=READY',flush=True)
442
+ print(f'LOCAL_UI=http://127.0.0.1:{args.port}/',flush=True)
443
+ print(f'CUSTOMER_PROJECTS={server.workspace}',flush=True)
444
+ print('WRITE_AUTHORITY=DISABLED; IGNITION_MODIFICATIONS=NONE',flush=True)
445
+ try:server.serve_forever(poll_interval=.2)
446
+ except KeyboardInterrupt:pass
447
+ finally:server.server_close()
448
+ return 0
449
+ if args.action=='preview':
450
+ project=args.project_dir.expanduser().resolve()
451
+ viewer=project/'viewer'
452
+ if not (viewer/'index.html').is_file() or not (project/'BUILD.json').is_file():
453
+ print('HOLD=NOT_A_GENERATED_DEVAGENT_PREVIEW')
454
+ return 2
455
+ class Handler(SimpleHTTPRequestHandler):
456
+ def end_headers(self):
457
+ self.send_header('X-Content-Type-Options','nosniff');self.send_header('Cache-Control','no-store')
458
+ super().end_headers()
459
+ try:
460
+ server=ThreadingHTTPServer(('127.0.0.1',args.port),partial(Handler,directory=str(viewer)))
461
+ except OSError as e:
462
+ print('HOLD=PREVIEW_PORT: '+str(e))
463
+ return 2
464
+ print(f'PREVIEW=http://127.0.0.1:{args.port}/',flush=True)
465
+ try:server.serve_forever(poll_interval=.2)
466
+ except KeyboardInterrupt:pass
467
+ finally:server.server_close()
468
+ return 0
469
+ return 2
470
+
471
+ if __name__=='__main__':raise SystemExit(main())