@aryacode/dsh-engineering 0.0.0-stage → 0.4.0
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.
- package/CHANGELOG.md +128 -0
- package/LICENSE +21 -0
- package/README.md +230 -2
- package/cordis.patch.yml +123 -0
- package/lib/client.js +3448 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +9 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +94 -3
- package/vendor/ftview/README.md +55 -0
- package/vendor/ftview/SKILL.md +74 -0
- package/vendor/ftview/lib/checks.d.ts +27 -0
- package/vendor/ftview/lib/checks.js +39 -0
- package/vendor/ftview/lib/core.d.ts +208 -0
- package/vendor/ftview/lib/core.js +160 -0
- package/vendor/ftview/lib/edit/drafts.d.ts +49 -0
- package/vendor/ftview/lib/edit/drafts.js +84 -0
- package/vendor/ftview/lib/edit/operations.d.ts +62 -0
- package/vendor/ftview/lib/edit/operations.js +126 -0
- package/vendor/ftview/lib/edit/prepare.d.ts +38 -0
- package/vendor/ftview/lib/edit/prepare.js +133 -0
- package/vendor/ftview/lib/edit/tools.d.ts +25 -0
- package/vendor/ftview/lib/edit/tools.js +198 -0
- package/vendor/ftview/lib/edit.d.ts +30 -0
- package/vendor/ftview/lib/edit.js +38 -0
- package/vendor/ftview/lib/index.d.ts +31 -0
- package/vendor/ftview/lib/index.js +36 -0
- package/vendor/ftview/lib/model.d.ts +80 -0
- package/vendor/ftview/lib/model.js +267 -0
- package/vendor/ftview/lib/session.d.ts +48 -0
- package/vendor/ftview/lib/session.js +98 -0
- package/vendor/ftview/lib/skill.d.ts +29 -0
- package/vendor/ftview/lib/skill.js +43 -0
- package/vendor/ftview/lib/studio/discover.d.ts +36 -0
- package/vendor/ftview/lib/studio/discover.js +87 -0
- package/vendor/ftview/lib/studio/preflight.d.ts +51 -0
- package/vendor/ftview/lib/studio/preflight.js +64 -0
- package/vendor/ftview/lib/studio/tools.d.ts +15 -0
- package/vendor/ftview/lib/studio/tools.js +83 -0
- package/vendor/ftview/lib/studio.d.ts +30 -0
- package/vendor/ftview/lib/studio.js +37 -0
- package/vendor/ftview/lib/tools.d.ts +14 -0
- package/vendor/ftview/lib/tools.js +202 -0
- package/vendor/l5x/README.md +62 -0
- package/vendor/l5x/SKILL.md +81 -0
- package/vendor/l5x/lib/approvals.d.ts +50 -0
- package/vendor/l5x/lib/approvals.js +93 -0
- package/vendor/l5x/lib/client.js +412 -0
- package/vendor/l5x/lib/client.js.map +1 -0
- package/vendor/l5x/lib/core/errors.d.ts +38 -0
- package/vendor/l5x/lib/core/errors.js +48 -0
- package/vendor/l5x/lib/core/params.d.ts +49 -0
- package/vendor/l5x/lib/core/params.js +39 -0
- package/vendor/l5x/lib/core/result.d.ts +46 -0
- package/vendor/l5x/lib/core/result.js +21 -0
- package/vendor/l5x/lib/core/tool.d.ts +102 -0
- package/vendor/l5x/lib/core/tool.js +19 -0
- package/vendor/l5x/lib/drafts.d.ts +66 -0
- package/vendor/l5x/lib/drafts.js +98 -0
- package/vendor/l5x/lib/editing.d.ts +30 -0
- package/vendor/l5x/lib/editing.js +42 -0
- package/vendor/l5x/lib/engine/acd-edit.d.ts +13 -0
- package/vendor/l5x/lib/engine/acd-edit.js +115 -0
- package/vendor/l5x/lib/engine/analysis.d.ts +22 -0
- package/vendor/l5x/lib/engine/analysis.js +258 -0
- package/vendor/l5x/lib/engine/capabilities.d.ts +9 -0
- package/vendor/l5x/lib/engine/capabilities.js +40 -0
- package/vendor/l5x/lib/engine/change-set.d.ts +122 -0
- package/vendor/l5x/lib/engine/change-set.js +446 -0
- package/vendor/l5x/lib/engine/configuration.d.ts +21 -0
- package/vendor/l5x/lib/engine/configuration.js +42 -0
- package/vendor/l5x/lib/engine/details.d.ts +47 -0
- package/vendor/l5x/lib/engine/details.js +173 -0
- package/vendor/l5x/lib/engine/direct-polling.d.ts +91 -0
- package/vendor/l5x/lib/engine/direct-polling.js +193 -0
- package/vendor/l5x/lib/engine/download-review.d.ts +15 -0
- package/vendor/l5x/lib/engine/download-review.js +63 -0
- package/vendor/l5x/lib/engine/logic-syntax.d.ts +60 -0
- package/vendor/l5x/lib/engine/logic-syntax.js +237 -0
- package/vendor/l5x/lib/engine/observations.d.ts +41 -0
- package/vendor/l5x/lib/engine/observations.js +67 -0
- package/vendor/l5x/lib/engine/project-analysis.d.ts +131 -0
- package/vendor/l5x/lib/engine/project-analysis.js +314 -0
- package/vendor/l5x/lib/engine/review-evidence.d.ts +8 -0
- package/vendor/l5x/lib/engine/review-evidence.js +49 -0
- package/vendor/l5x/lib/engine/review.d.ts +18 -0
- package/vendor/l5x/lib/engine/review.js +123 -0
- package/vendor/l5x/lib/engine/studio-sdk.d.ts +101 -0
- package/vendor/l5x/lib/engine/studio-sdk.js +571 -0
- package/vendor/l5x/lib/engine/tag-resolution.d.ts +20 -0
- package/vendor/l5x/lib/engine/tag-resolution.js +169 -0
- package/vendor/l5x/lib/engine/usage.d.ts +16 -0
- package/vendor/l5x/lib/engine/usage.js +92 -0
- package/vendor/l5x/lib/engine/xml-regions.d.ts +23 -0
- package/vendor/l5x/lib/engine/xml-regions.js +77 -0
- package/vendor/l5x/lib/fs.d.ts +17 -0
- package/vendor/l5x/lib/fs.js +36 -0
- package/vendor/l5x/lib/index.d.ts +36 -0
- package/vendor/l5x/lib/index.js +42 -0
- package/vendor/l5x/lib/live.d.ts +48 -0
- package/vendor/l5x/lib/live.js +48 -0
- package/vendor/l5x/lib/model/aois.d.ts +39 -0
- package/vendor/l5x/lib/model/aois.js +112 -0
- package/vendor/l5x/lib/model/errors.d.ts +15 -0
- package/vendor/l5x/lib/model/errors.js +18 -0
- package/vendor/l5x/lib/model/index.d.ts +20 -0
- package/vendor/l5x/lib/model/index.js +19 -0
- package/vendor/l5x/lib/model/module.d.ts +37 -0
- package/vendor/l5x/lib/model/module.js +54 -0
- package/vendor/l5x/lib/model/modules.d.ts +24 -0
- package/vendor/l5x/lib/model/modules.js +55 -0
- package/vendor/l5x/lib/model/parse.d.ts +32 -0
- package/vendor/l5x/lib/model/parse.js +116 -0
- package/vendor/l5x/lib/model/project.d.ts +15 -0
- package/vendor/l5x/lib/model/project.js +31 -0
- package/vendor/l5x/lib/model/routines.d.ts +71 -0
- package/vendor/l5x/lib/model/routines.js +183 -0
- package/vendor/l5x/lib/model/tags.d.ts +51 -0
- package/vendor/l5x/lib/model/tags.js +119 -0
- package/vendor/l5x/lib/model/types.d.ts +304 -0
- package/vendor/l5x/lib/model/types.js +12 -0
- package/vendor/l5x/lib/model/udts.d.ts +34 -0
- package/vendor/l5x/lib/model/udts.js +81 -0
- package/vendor/l5x/lib/model/xml.d.ts +139 -0
- package/vendor/l5x/lib/model/xml.js +291 -0
- package/vendor/l5x/lib/model/xref.d.ts +28 -0
- package/vendor/l5x/lib/model/xref.js +154 -0
- package/vendor/l5x/lib/session.d.ts +119 -0
- package/vendor/l5x/lib/session.js +236 -0
- package/vendor/l5x/lib/skill.d.ts +30 -0
- package/vendor/l5x/lib/skill.js +44 -0
- package/vendor/l5x/lib/studio-state.d.ts +77 -0
- package/vendor/l5x/lib/studio-state.js +79 -0
- package/vendor/l5x/lib/studio.d.ts +29 -0
- package/vendor/l5x/lib/studio.js +40 -0
- package/vendor/l5x/lib/tools/checks.d.ts +19 -0
- package/vendor/l5x/lib/tools/checks.js +140 -0
- package/vendor/l5x/lib/tools/common.d.ts +41 -0
- package/vendor/l5x/lib/tools/common.js +84 -0
- package/vendor/l5x/lib/tools/controller.d.ts +21 -0
- package/vendor/l5x/lib/tools/controller.js +170 -0
- package/vendor/l5x/lib/tools/drafts.d.ts +23 -0
- package/vendor/l5x/lib/tools/drafts.js +270 -0
- package/vendor/l5x/lib/tools/live.d.ts +21 -0
- package/vendor/l5x/lib/tools/live.js +175 -0
- package/vendor/l5x/lib/tools/read.d.ts +19 -0
- package/vendor/l5x/lib/tools/read.js +245 -0
- package/vendor/l5x/lib/tools/references.d.ts +20 -0
- package/vendor/l5x/lib/tools/references.js +269 -0
- package/vendor/l5x/lib/tools/studio.d.ts +21 -0
- package/vendor/l5x/lib/tools/studio.js +193 -0
- package/vendor/l5x/poll-worker.mjs +77 -0
- package/vendor/l5x/studio-bridge/NuGet.Config +1 -0
- package/vendor/l5x/studio-bridge/Program.cs +304 -0
- package/vendor/l5x/studio-bridge/StudioBridge.csproj +11 -0
- package/vendor/mwi/README.md +60 -0
- package/vendor/mwi/SKILL.md +87 -0
- package/vendor/mwi/bridge/README.md +8 -0
- package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -0
- package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -0
- package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -0
- package/vendor/mwi/bridge/start_bridge.cmd +21 -0
- package/vendor/mwi/bridge/watch_dialogs.cmd +13 -0
- package/vendor/mwi/bridge/watch_dialogs.ps1 +153 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__init__.py +14 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/block_interfaces.py +275 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/capture.py +448 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/com.py +268 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/datatypes.py +204 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/descriptions.py +30 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/eip.py +292 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/errors.py +36 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/graphical_listing.py +208 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/grid.py +227 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +317 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/iec.py +317 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/installed_help.py +151 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/knowledge.py +218 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/libraries.py +250 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/navigation.py +44 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/pou_writer.py +1547 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/program_checks.py +360 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/project.py +704 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/server.py +1913 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/snapshot.py +138 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/st_comments.py +25 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +179 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +676 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/structure.py +98 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/task_bindings.py +68 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tasks.py +162 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/transaction.py +157 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree.py +563 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/uia.py +465 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/validation.py +41 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/variable_edit.py +183 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/variables.py +472 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/workflow.py +130 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +666 -0
- package/vendor/mwi/code/mw_code.py +1258 -0
- package/vendor/mwi/legacy/edit-session.js +50 -0
- package/vendor/mwi/legacy/fb-insertion.js +74 -0
- package/vendor/mwi/legacy/native-code.js +83 -0
- package/vendor/mwi/legacy/native-groups.js +75 -0
- package/vendor/mwi/legacy/native-structure.js +143 -0
- package/vendor/mwi/legacy/native-variables.js +77 -0
- package/vendor/mwi/legacy/pou-conversion.js +47 -0
- package/vendor/mwi/legacy/pou-package.js +66 -0
- package/vendor/mwi/lib/core/bridge.d.ts +71 -0
- package/vendor/mwi/lib/core/bridge.js +53 -0
- package/vendor/mwi/lib/core/code-engine.d.ts +47 -0
- package/vendor/mwi/lib/core/code-engine.js +109 -0
- package/vendor/mwi/lib/core/errors.d.ts +37 -0
- package/vendor/mwi/lib/core/errors.js +47 -0
- package/vendor/mwi/lib/core/params.d.ts +52 -0
- package/vendor/mwi/lib/core/params.js +46 -0
- package/vendor/mwi/lib/core/ps-bridge.d.ts +62 -0
- package/vendor/mwi/lib/core/ps-bridge.js +127 -0
- package/vendor/mwi/lib/core/result.d.ts +45 -0
- package/vendor/mwi/lib/core/result.js +20 -0
- package/vendor/mwi/lib/core/tool.d.ts +80 -0
- package/vendor/mwi/lib/core/tool.js +55 -0
- package/vendor/mwi/lib/edit/compare.d.ts +33 -0
- package/vendor/mwi/lib/edit/compare.js +70 -0
- package/vendor/mwi/lib/edit.d.ts +20 -0
- package/vendor/mwi/lib/edit.js +32 -0
- package/vendor/mwi/lib/index.d.ts +21 -0
- package/vendor/mwi/lib/index.js +33 -0
- package/vendor/mwi/lib/read/service.d.ts +117 -0
- package/vendor/mwi/lib/read/service.js +229 -0
- package/vendor/mwi/lib/read.d.ts +21 -0
- package/vendor/mwi/lib/read.js +33 -0
- package/vendor/mwi/lib/session/project.d.ts +34 -0
- package/vendor/mwi/lib/session/project.js +54 -0
- package/vendor/mwi/lib/session/service.d.ts +167 -0
- package/vendor/mwi/lib/session/service.js +566 -0
- package/vendor/mwi/lib/session/stage.d.ts +84 -0
- package/vendor/mwi/lib/session/stage.js +187 -0
- package/vendor/mwi/lib/session/workspace.d.ts +52 -0
- package/vendor/mwi/lib/session/workspace.js +90 -0
- package/vendor/mwi/lib/skill.d.ts +36 -0
- package/vendor/mwi/lib/skill.js +56 -0
- package/vendor/mwi/lib/tools/edit.d.ts +20 -0
- package/vendor/mwi/lib/tools/edit.js +286 -0
- package/vendor/mwi/lib/tools/read.d.ts +19 -0
- package/vendor/mwi/lib/tools/read.js +161 -0
- package/vendor/mwi/lib/tools/session.d.ts +20 -0
- package/vendor/mwi/lib/tools/session.js +166 -0
- package/vendor/mwi/lib/tools/verify.d.ts +19 -0
- package/vendor/mwi/lib/tools/verify.js +85 -0
- package/vendor/mwi/lib/verify.d.ts +17 -0
- package/vendor/mwi/lib/verify.js +29 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Versioned vendor references and bounded local PDF search. No IDE operations."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from urllib.parse import urlsplit
|
|
9
|
+
from urllib.request import Request, urlopen
|
|
10
|
+
from uuid import uuid4
|
|
11
|
+
|
|
12
|
+
DATA = Path(__file__).with_name('reference_data')
|
|
13
|
+
MAX_DOWNLOAD = 64 * 1024 * 1024
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def catalog():
|
|
17
|
+
return json.loads((DATA / 'catalog.json').read_text(encoding='utf-8'))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def citation(source_id, pages=None, section=None):
|
|
21
|
+
source = next(s for s in catalog()['sources'] if s['id'] == source_id)
|
|
22
|
+
return {**source, 'pdf_pages': pages or [], 'section': section,
|
|
23
|
+
'page_url': source['url'] + (f'#page={pages[0]}' if pages else ''),
|
|
24
|
+
'version_match': 'not_verified_against_installed_library'}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def reference(topic_id):
|
|
28
|
+
topic = next(t for t in catalog()['topics'] if t['id'] == topic_id)
|
|
29
|
+
return {**topic, 'citation': citation(topic['source_id'], topic['pdf_pages'], topic['section'])}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def tokens(text):
|
|
33
|
+
return re.findall(r'[a-z0-9_]+', text.casefold())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _score(query, text):
|
|
37
|
+
wanted = set(tokens(query))
|
|
38
|
+
actual = set(tokens(text))
|
|
39
|
+
# Full-phrase matching also helps PDFs whose glyph extraction omits spaces.
|
|
40
|
+
compact = re.sub(r'\s+', '', text.casefold())
|
|
41
|
+
phrase = re.sub(r'\s+', '', query.casefold())
|
|
42
|
+
return len(wanted & actual) + (4 if phrase and phrase in compact else 0)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def cache_root():
|
|
46
|
+
from .staging import workspace_root
|
|
47
|
+
workspace = workspace_root()
|
|
48
|
+
root = (workspace / '.motionworks' / 'references').resolve()
|
|
49
|
+
if not root.is_relative_to(workspace):
|
|
50
|
+
raise ValueError('Reference cache escapes the workspace')
|
|
51
|
+
return root
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _safe_child(root, name):
|
|
55
|
+
path = root / name
|
|
56
|
+
if not path.resolve().is_relative_to(root):
|
|
57
|
+
raise ValueError('Linked reference cache entry escapes its directory')
|
|
58
|
+
return path
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def search(query='', *, source_id=None, limit=5):
|
|
62
|
+
limit = max(1, min(int(limit), 10))
|
|
63
|
+
data = catalog()
|
|
64
|
+
if source_id and source_id not in {s['id'] for s in data['sources']}:
|
|
65
|
+
raise ValueError('Unknown source_id')
|
|
66
|
+
if not query.strip():
|
|
67
|
+
return {'sources': [s for s in data['sources'] if not source_id or s['id'] == source_id], 'topics': [{'id': t['id'], 'title': t['title']} for t in data['topics'] if not source_id or t['source_id'] == source_id],
|
|
68
|
+
'signatures': sorted(data['signatures']), 'note': 'Curated notes work offline; sync PDFs for page search.'}
|
|
69
|
+
if len(query) > 256: raise ValueError('Reference query must be at most 256 characters')
|
|
70
|
+
scored = []
|
|
71
|
+
for topic in data['topics']:
|
|
72
|
+
if source_id and topic['source_id'] != source_id: continue
|
|
73
|
+
score = _score(query, ' '.join([topic['title'], topic['summary'], *topic['tags']]))
|
|
74
|
+
if score: scored.append((score, topic['id']))
|
|
75
|
+
curated = [reference(id) for _, id in sorted(scored, key=lambda x: (-x[0], x[1]))[:limit]]
|
|
76
|
+
pages, unavailable = [], []
|
|
77
|
+
exact_signature = signature(query.strip())
|
|
78
|
+
root = cache_root()
|
|
79
|
+
for source in data['sources']:
|
|
80
|
+
if source['kind'] != 'pdf' or (source_id and source['id'] != source_id): continue
|
|
81
|
+
path = _safe_child(root, source['id'] + '.json')
|
|
82
|
+
if not path.exists():
|
|
83
|
+
unavailable.append(source['id']); continue
|
|
84
|
+
try:
|
|
85
|
+
cached = json.loads(path.read_text(encoding='utf-8'))
|
|
86
|
+
pdf = _safe_child(root, source['id'] + '.pdf')
|
|
87
|
+
if cached['sha256'] != source['sha256'] or hashlib.sha256(pdf.read_bytes()).hexdigest() != source['sha256']:
|
|
88
|
+
raise ValueError('Cached PDF version/hash does not match reviewed source')
|
|
89
|
+
if len(cached['pages']) != source['pages'] or not all(isinstance(p, str) for p in cached['pages']):
|
|
90
|
+
raise ValueError('Cached page index is incomplete')
|
|
91
|
+
if cached.get('text_sha256') != hashlib.sha256(json.dumps(cached['pages'], ensure_ascii=False).encode('utf-8')).hexdigest():
|
|
92
|
+
raise ValueError('Cached page index failed integrity check; synchronize again')
|
|
93
|
+
best = []
|
|
94
|
+
for i, text in enumerate(cached['pages'], 1):
|
|
95
|
+
score = _score(query, text)
|
|
96
|
+
if score:
|
|
97
|
+
score += min(3, text.casefold().count(query.strip().casefold()))
|
|
98
|
+
if _score(query, text[:180]) >= 4: score += 2
|
|
99
|
+
if any(t['source_id'] == source['id'] and i in t['pdf_pages'] for t in curated): score += 8
|
|
100
|
+
if exact_signature and exact_signature['source_id'] == source['id'] and i in exact_signature['pdf_pages']: score += 20
|
|
101
|
+
if score: best.append((score, i, text))
|
|
102
|
+
# At most one short excerpt per source; other matches remain page links.
|
|
103
|
+
for rank, (_, page, text) in enumerate(sorted(best, key=lambda x: (-x[0], x[1]))[:limit]):
|
|
104
|
+
hit = re.search(re.escape(query.strip()), text, re.I)
|
|
105
|
+
start = hit.start() if hit else 0
|
|
106
|
+
excerpt = ' '.join(text[start:].split()[:20])[:150] if rank == 0 else None
|
|
107
|
+
pages.append({'source_id': source['id'], 'pdf_page': page, 'excerpt': excerpt,
|
|
108
|
+
'citation': citation(source['id'], [page], 'PDF search result')})
|
|
109
|
+
except (OSError, ValueError, KeyError) as exc:
|
|
110
|
+
unavailable.append({'source_id': source['id'], 'reason': str(exc)})
|
|
111
|
+
return {'query': query, 'topics': curated, 'pages': pages, 'unavailable_indexes': unavailable,
|
|
112
|
+
'note': 'PDF pages are physical 1-based pages, which can differ from printed labels. Historical references do not establish installed-version compatibility.'}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def signature(name):
|
|
116
|
+
match = next((v for k, v in catalog()['signatures'].items() if k.casefold() == name.casefold()), None)
|
|
117
|
+
if match is None: return None
|
|
118
|
+
return {**match, 'citation': citation(match['source_id'], match['pdf_pages'], match['section'])}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _atomic(path, data):
|
|
122
|
+
temp = path.with_name(path.name + '.' + uuid4().hex + '.tmp')
|
|
123
|
+
try:
|
|
124
|
+
temp.write_bytes(data)
|
|
125
|
+
os.replace(temp, path)
|
|
126
|
+
finally:
|
|
127
|
+
temp.unlink(missing_ok=True)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _fetch(url):
|
|
131
|
+
# Only catalog URLs are passed here. Never send project data or credentials.
|
|
132
|
+
with urlopen(Request(url, headers={'User-Agent': 'MotionWorksReference/0.3'}), timeout=45) as response:
|
|
133
|
+
final = urlsplit(response.url)
|
|
134
|
+
if final.scheme != 'https' or final.hostname != 'www.yaskawa.com':
|
|
135
|
+
raise ValueError('Reference download redirected outside the official host')
|
|
136
|
+
raw = response.read(MAX_DOWNLOAD + 1)
|
|
137
|
+
if len(raw) > MAX_DOWNLOAD or not raw.startswith(b'%PDF'):
|
|
138
|
+
raise ValueError('Official download did not return a supported PDF; it may require login')
|
|
139
|
+
return raw
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def sync(source_ids=None):
|
|
143
|
+
data = catalog()
|
|
144
|
+
sources = {s['id']: s for s in data['sources'] if s['kind'] == 'pdf'}
|
|
145
|
+
ids = list(dict.fromkeys(sources if source_ids is None else source_ids))
|
|
146
|
+
if any(id not in sources for id in ids): raise ValueError('Only catalog PDF source IDs can be synchronized')
|
|
147
|
+
try:
|
|
148
|
+
from pypdf import PdfReader
|
|
149
|
+
except ImportError:
|
|
150
|
+
return {'ok': False, 'error': 'PDF indexing requires pypdf in the selected plugin Python runtime. Curated search remains available.', 'sources': []}
|
|
151
|
+
root = cache_root(); root.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
results = []
|
|
153
|
+
for id in ids:
|
|
154
|
+
source = sources[id]
|
|
155
|
+
try:
|
|
156
|
+
pdf = _safe_child(root, id + '.pdf')
|
|
157
|
+
index = _safe_child(root, id + '.json')
|
|
158
|
+
raw = pdf.read_bytes() if pdf.exists() else _fetch(source['url'])
|
|
159
|
+
digest = hashlib.sha256(raw).hexdigest()
|
|
160
|
+
if digest != source['sha256']:
|
|
161
|
+
raise ValueError('Vendor PDF differs from the reviewed revision. Catalog review is required; no old page citations were applied.')
|
|
162
|
+
import io
|
|
163
|
+
reader = PdfReader(io.BytesIO(raw))
|
|
164
|
+
if len(reader.pages) != source['pages']: raise ValueError('PDF page count differs from catalog')
|
|
165
|
+
pages = [page.extract_text() or '' for page in reader.pages]
|
|
166
|
+
_atomic(pdf, raw)
|
|
167
|
+
text_digest = hashlib.sha256(json.dumps(pages, ensure_ascii=False).encode('utf-8')).hexdigest()
|
|
168
|
+
_atomic(index, json.dumps({'sha256': digest, 'text_sha256': text_digest, 'pages': pages}, ensure_ascii=False).encode('utf-8'))
|
|
169
|
+
results.append({'source_id': id, 'status': 'indexed', 'pages': len(pages), 'path': str(pdf), 'revision': source['revision']})
|
|
170
|
+
except Exception as exc:
|
|
171
|
+
results.append({'source_id': id, 'status': 'unavailable', 'error': str(exc)})
|
|
172
|
+
return {'ok': all(r['status'] == 'indexed' for r in results), 'sources': results}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def diagnose(message):
|
|
176
|
+
if not message.strip(): raise ValueError('Supply the compiler/runtime diagnostic text')
|
|
177
|
+
patterns = [
|
|
178
|
+
(r'file error.*(?:\.vbc|src\.st1)|MSILv2ResManager|variable.*not found', 'diagnostics', 'compiler_or_native_format', ['Preserve the exact project, error text and transaction journal.', 'Compare paired VB/VGR declarations and source stream hashes with the last verified snapshot.', 'Do not patch generated cache files, replace a binary container as text, or restore underneath an open IDE.']),
|
|
179
|
+
(r'division by zero|divide.by.zero', 'diagnostics', 'runtime', ['Inspect denominators and cam-generation segment lengths at the faulting operation.', 'Zero compiler errors do not exclude runtime arithmetic faults. Capture runtime values before proposing a change.']),
|
|
180
|
+
(r'identity.*workspace|outside.*stag|ENOTDIR', 'diagnostics', 'workspace_state', ['Read the calling session workspace and mw_workflow_check for the explicit stage.', 'Check relocated identity paths and wrappers. Do not weaken guards or restage over unsynced source edits.']),
|
|
181
|
+
(r'error\s+in\s+native\s+code\s+generation', 'native-code-error', 'compiler', ['Read the first underlying compiler diagnostic.', 'Compare IDE, controller target and library versions before changing declarations.']),
|
|
182
|
+
(r'operand\s+not\s+implemented|area\s+exceeded', 'address-range', 'runtime_or_debug', ['Inspect IO_Configuration address ranges.', 'Compare the located variable address with the configured driver.']),
|
|
183
|
+
(r'no\s+matching\s+global|global-not-declared-external|external.*(?:missing|mismatch)', 'scope', 'compiler', ['Compare the resource global with the POU external name and type.', 'Inspect both VB and VGR consistency; do not convert an external to a local just to hide the error.']),
|
|
184
|
+
(r'type.*(?:invalid|mismatch)|incompatible.*type|cannot.*convert', 'strict-types', 'compiler', ['Read operand declarations and FB formal types.', 'Use an explicit, range-appropriate conversion where intended.']),
|
|
185
|
+
(r'\b4370\b|motionprohibited', 'stop', 'runtime', ['Inspect axis state, active commands and the MC_Stop instance.', 'Do not automatically clear interlocks or reset the axis.']),
|
|
186
|
+
(r'\b4625\b|axis.*(?:id|number).*not', 'axis-id', 'runtime', ['Compare the logical axis reference with Hardware Configuration.']),
|
|
187
|
+
(r'watchdog|task.*overrun', 'task-priority', 'runtime', ['Inspect task timing, execution load and priority.', 'Do not mask an overrun merely by extending its watchdog.']),
|
|
188
|
+
(r'completion unverified|stalled|empty.*errors', 'diagnostics', 'ide_state', ['Inspect modal dialogs and current compiler state.', 'An empty error pane is not a success verdict; never repair disk files under an open IDE.']),
|
|
189
|
+
]
|
|
190
|
+
matches = [{'category': kind, 'evidence': reference(topic), 'suggested_checks': checks,
|
|
191
|
+
'confidence': 'candidate_not_confirmed', 'mapping': 'plugin_authored_from_cited_guidance'}
|
|
192
|
+
for pattern, topic, kind, checks in patterns if re.search(pattern, message, re.I)]
|
|
193
|
+
if re.search(r'''illegal\s+IEC\s+syntax\s+at\s+or\s+before\s+['"]>['"]''',message,re.I):
|
|
194
|
+
matches.append({
|
|
195
|
+
'category':'compiler',
|
|
196
|
+
'confidence':'candidate_not_confirmed',
|
|
197
|
+
'mapping':'plugin_authored_from_live_observation_and_installed_help',
|
|
198
|
+
'evidence':{'kind':'installed_help_and_live_compile',
|
|
199
|
+
'observed_ide':'MotionWorks IEC 3 Pro, automation version 1.19',
|
|
200
|
+
'installed_help':{'tool':'mw_code_installed_help','module':'ST001','topic':'callingfunctionblocksinst.htm'}},
|
|
201
|
+
'suggested_checks':[
|
|
202
|
+
'Read the exact offending ST line before diagnosing the token.',
|
|
203
|
+
'If the line uses => for an FB output, read the installed ST FB-call topic: the tested IDE rejected that form and accepted assignments from instance output fields after the call.',
|
|
204
|
+
'Resolve actual pin directions/types with mw_code_block_interface. Keep in-out bindings in the call; capture output fields afterward.',
|
|
205
|
+
'A greater-than token can fail for other reasons. Confirm the source context and installed version, then use guarded code editing and fresh Build/Make; do not rewrite automatically.'
|
|
206
|
+
]})
|
|
207
|
+
return {'matches': matches, 'matched': bool(matches), 'automatic_changes': False,
|
|
208
|
+
'next': 'Confirm candidates against the exact diagnostic, project and installed versions.' if matches else 'No documented mapping matched. Preserve the exact message and inspect its source location; do not invent a root cause.'}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def patterns(name=None):
|
|
212
|
+
data = json.loads((DATA / 'patterns.json').read_text(encoding='utf-8'))
|
|
213
|
+
if name is None: return {'patterns': [{'id': p['id'], 'title': p['title']} for p in data]}
|
|
214
|
+
found = next((p for p in data if p['id'] == name), None)
|
|
215
|
+
if found is None: raise ValueError('Unknown pattern ID')
|
|
216
|
+
return {**found, 'references': [reference(id) for id in found['topic_ids']],
|
|
217
|
+
'provenance': 'Original illustrative plugin pattern; not copied vendor code',
|
|
218
|
+
'validation': 'Requires project-specific adaptation and native IDE compile; no hardware action is performed'}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Read the library side of a MotionWorks project: which function blocks it uses, and what
|
|
2
|
+
those blocks offer.
|
|
3
|
+
|
|
4
|
+
Two sources, and they answer different questions.
|
|
5
|
+
|
|
6
|
+
**`eCLRPouDependencies.dat` is the map.** It is plain text at the top of the resource
|
|
7
|
+
directory and names every POU and library block in the project with its kind and what it
|
|
8
|
+
depends on::
|
|
9
|
+
|
|
10
|
+
PouDep.Cache;schema 1
|
|
11
|
+
0,CalcBezier,3,FB
|
|
12
|
+
1,CalcSplineMatrix,3,FB
|
|
13
|
+
2,CalcSpline,3,FB; 1,CalcSplineMatrix
|
|
14
|
+
4,CamGenerator,3,FB; 2,CalcSpline; 0,CalcBezier; 3,MasterIndex_Lookup
|
|
15
|
+
5,TopCutterCamSetup,2,PG; 4,CamGenerator
|
|
16
|
+
|
|
17
|
+
so ``2,PG`` is a program and ``3,FB`` a function block, and the trailing entries are the
|
|
18
|
+
blocks it calls. That is enough to answer "what does this POU use" without reading any
|
|
19
|
+
binary.
|
|
20
|
+
|
|
21
|
+
**The block's own members come from its compiled assembly.** A library block in this project
|
|
22
|
+
is compiled to ``<name>.DLL`` beside the resource, and it is a real .NET assembly - the
|
|
23
|
+
signature is ``BSJB``, runtime ``v4.0.30319`` - so its identifiers sit in the metadata
|
|
24
|
+
``#Strings`` heap as plain UTF-8. Measured on CamGenerator.DLL, that heap begins with the
|
|
25
|
+
names that matter::
|
|
26
|
+
|
|
27
|
+
CamGenerator, ProConOS_eCLR, Iec61131Standard, MasterIndex_Lookup, CalcSpline,
|
|
28
|
+
CalcBezier, FUNCTION_BLOCK, CamSegmentStruct, Y_MS_CAM_STRUCT, ...
|
|
29
|
+
Execute, TableSize, Done, Busy, Error, ErrorID, iActive, CamData, CamTable
|
|
30
|
+
|
|
31
|
+
WHAT THIS CANNOT TELL YOU, and it matters: **the heap does not record input/output
|
|
32
|
+
direction.** ``Execute`` and ``Done`` are distinguishable by convention, not by evidence
|
|
33
|
+
here, and this module does not guess. It reports the names the assembly defines and says
|
|
34
|
+
plainly that direction is not among them. The block's own declarations/native parameter
|
|
35
|
+
table (``mw_code_block_interface``) establish direction; caller instance declarations
|
|
36
|
+
do not establish a block's pin directions.
|
|
37
|
+
|
|
38
|
+
The heap also carries compiler temporaries - ``__temp_1`` through ``__temp_50``, ``s1``
|
|
39
|
+
through ``s7`` - which are noise here, so they are filtered out rather than reported as if
|
|
40
|
+
they were part of the block's interface.
|
|
41
|
+
"""
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import re
|
|
45
|
+
import struct
|
|
46
|
+
from dataclasses import dataclass, field
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
|
|
49
|
+
from .errors import NotFound
|
|
50
|
+
|
|
51
|
+
DEPENDENCY_FILE = "C/Configuration/R/Resource/eCLRPouDependencies.dat"
|
|
52
|
+
RESOURCE_DIR = "C/Configuration/R/Resource"
|
|
53
|
+
|
|
54
|
+
KINDS = {"1": "FUNCTION", "2": "PROGRAM", "3": "FUNCTION_BLOCK", "4": "FUNCTION_BLOCK"}
|
|
55
|
+
|
|
56
|
+
# Names the compiler generates rather than the author writing: temporaries, scratch
|
|
57
|
+
# registers and one/two-letter locals. Reporting these as part of a block's interface would
|
|
58
|
+
# be worse than reporting nothing.
|
|
59
|
+
_NOISE = re.compile(
|
|
60
|
+
r"^(__temp_\d+|__\w+|_\w+|[a-z]\d*|[A-Z]\d*|[a-z]{2}\d*|k\d+|sPtr|amm|amp|C0|d|p)$"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class Block:
|
|
66
|
+
"""One POU or library block named in the dependency manifest."""
|
|
67
|
+
|
|
68
|
+
index: int
|
|
69
|
+
name: str
|
|
70
|
+
kind: str
|
|
71
|
+
depends_on: list[str] = field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def is_library(self) -> bool:
|
|
75
|
+
return self.kind == "FUNCTION_BLOCK" or self.kind == "FUNCTION"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class BlockMembers:
|
|
80
|
+
"""The identifiers an assembly defines, in heap order."""
|
|
81
|
+
|
|
82
|
+
name: str
|
|
83
|
+
assembly: str
|
|
84
|
+
runtime: str
|
|
85
|
+
identifiers: list[str] = field(default_factory=list)
|
|
86
|
+
filtered_out: int = 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _resource_dir(project_root: Path) -> Path:
|
|
90
|
+
candidate = Path(project_root) / RESOURCE_DIR
|
|
91
|
+
if not candidate.is_dir():
|
|
92
|
+
raise NotFound(f"no {RESOURCE_DIR} in {project_root}")
|
|
93
|
+
return candidate
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def read_dependencies(project_root: Path) -> list[Block]:
|
|
97
|
+
"""Parse ``eCLRPouDependencies.dat`` into blocks, in file order."""
|
|
98
|
+
path = Path(project_root) / DEPENDENCY_FILE
|
|
99
|
+
if not path.is_file():
|
|
100
|
+
raise NotFound(
|
|
101
|
+
f"no {DEPENDENCY_FILE} in {project_root}; the project has not been built, so it "
|
|
102
|
+
f"has no dependency manifest yet"
|
|
103
|
+
)
|
|
104
|
+
blocks: list[Block] = []
|
|
105
|
+
by_index: dict[int, Block] = {}
|
|
106
|
+
for raw in path.read_text(encoding="latin1", errors="replace").splitlines():
|
|
107
|
+
line = raw.strip()
|
|
108
|
+
if not line or line.startswith("PouDep"):
|
|
109
|
+
continue
|
|
110
|
+
parts = [p.strip() for p in line.split(";")]
|
|
111
|
+
head = parts[0].split(",")
|
|
112
|
+
if len(head) < 4:
|
|
113
|
+
continue
|
|
114
|
+
try:
|
|
115
|
+
index = int(head[0])
|
|
116
|
+
except ValueError:
|
|
117
|
+
continue
|
|
118
|
+
name, code = head[1], head[2]
|
|
119
|
+
block = Block(index=index, name=name, kind=KINDS.get(code, f"KIND_{code}"))
|
|
120
|
+
blocks.append(block)
|
|
121
|
+
by_index[index] = block
|
|
122
|
+
|
|
123
|
+
# second pass: the trailing entries are indices, resolved once every block is known
|
|
124
|
+
for raw in path.read_text(encoding="latin1", errors="replace").splitlines():
|
|
125
|
+
line = raw.strip()
|
|
126
|
+
if not line or line.startswith("PouDep"):
|
|
127
|
+
continue
|
|
128
|
+
parts = [p.strip() for p in line.split(";")]
|
|
129
|
+
head = parts[0].split(",")
|
|
130
|
+
if len(head) < 4:
|
|
131
|
+
continue
|
|
132
|
+
try:
|
|
133
|
+
index = int(head[0])
|
|
134
|
+
except ValueError:
|
|
135
|
+
continue
|
|
136
|
+
block = by_index.get(index)
|
|
137
|
+
if block is None:
|
|
138
|
+
continue
|
|
139
|
+
for dep in parts[1:]:
|
|
140
|
+
if not dep:
|
|
141
|
+
continue
|
|
142
|
+
dep_index = dep.split(",")[0].strip()
|
|
143
|
+
try:
|
|
144
|
+
target = by_index.get(int(dep_index))
|
|
145
|
+
except ValueError:
|
|
146
|
+
target = None
|
|
147
|
+
block.depends_on.append(target.name if target else dep)
|
|
148
|
+
return blocks
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ── .NET metadata ────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
def _metadata_streams(data: bytes):
|
|
154
|
+
"""Locate the .NET metadata streams, or None if this is not a managed assembly."""
|
|
155
|
+
at = data.find(b"BSJB")
|
|
156
|
+
if at < 0:
|
|
157
|
+
return None
|
|
158
|
+
off = at + 4
|
|
159
|
+
try:
|
|
160
|
+
_major, _minor, _reserved, version_len = struct.unpack_from("<HHII", data, off)
|
|
161
|
+
except struct.error:
|
|
162
|
+
return None
|
|
163
|
+
off += 12
|
|
164
|
+
version = data[off:off + version_len].split(b"\x00")[0].decode("ascii", "replace")
|
|
165
|
+
off += version_len + 2
|
|
166
|
+
try:
|
|
167
|
+
count = struct.unpack_from("<H", data, off)[0]
|
|
168
|
+
except struct.error:
|
|
169
|
+
return None
|
|
170
|
+
off += 2
|
|
171
|
+
heaps: dict[str, tuple[int, int]] = {}
|
|
172
|
+
for _ in range(count):
|
|
173
|
+
try:
|
|
174
|
+
stream_off, stream_size = struct.unpack_from("<II", data, off)
|
|
175
|
+
except struct.error:
|
|
176
|
+
return None
|
|
177
|
+
off += 8
|
|
178
|
+
end = data.find(b"\x00", off)
|
|
179
|
+
if end < 0:
|
|
180
|
+
return None
|
|
181
|
+
name = data[off:end].decode("ascii", "replace")
|
|
182
|
+
off = (end + 1 + 3) & ~3
|
|
183
|
+
heaps[name] = (at + stream_off, stream_size)
|
|
184
|
+
return version, heaps
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _strings(data: bytes, base: int, size: int) -> list[str]:
|
|
188
|
+
raw = data[base:base + size]
|
|
189
|
+
out: list[str] = []
|
|
190
|
+
current = bytearray()
|
|
191
|
+
for byte in raw:
|
|
192
|
+
if byte == 0:
|
|
193
|
+
if current:
|
|
194
|
+
out.append(current.decode("utf-8", "replace"))
|
|
195
|
+
current = bytearray()
|
|
196
|
+
else:
|
|
197
|
+
current.append(byte)
|
|
198
|
+
if current:
|
|
199
|
+
out.append(current.decode("utf-8", "replace"))
|
|
200
|
+
return out
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def read_block_members(project_root: Path, name: str) -> BlockMembers:
|
|
204
|
+
"""Identifiers defined by a block's compiled assembly.
|
|
205
|
+
|
|
206
|
+
Direction is NOT among them - see the module docstring - so this reports names and says
|
|
207
|
+
so rather than presenting a guess as an interface.
|
|
208
|
+
"""
|
|
209
|
+
directory = _resource_dir(project_root)
|
|
210
|
+
for candidate in (directory / f"{name}.DLL", directory / f"{name}.dll"):
|
|
211
|
+
if candidate.is_file():
|
|
212
|
+
assembly = candidate
|
|
213
|
+
break
|
|
214
|
+
else:
|
|
215
|
+
blocks = {b.name.lower(): b for b in read_dependencies(project_root)}
|
|
216
|
+
known = ", ".join(sorted(b.name for b in blocks.values())[:25])
|
|
217
|
+
raise NotFound(
|
|
218
|
+
f"no compiled assembly for {name!r} in {RESOURCE_DIR}. Blocks with an assembly "
|
|
219
|
+
f"here: {known}. A block with no assembly may be defined in the project's own "
|
|
220
|
+
f"source rather than a library - list those with mw_code_pous."
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
data = assembly.read_bytes()
|
|
224
|
+
streams = _metadata_streams(data)
|
|
225
|
+
if not streams:
|
|
226
|
+
raise NotFound(
|
|
227
|
+
f"{assembly.name} is not a managed assembly, so it has no readable identifier "
|
|
228
|
+
f"table. It may be legacy native code."
|
|
229
|
+
)
|
|
230
|
+
version, heaps = streams
|
|
231
|
+
if "#Strings" not in heaps:
|
|
232
|
+
raise NotFound(f"{assembly.name} has .NET metadata but no #Strings heap")
|
|
233
|
+
|
|
234
|
+
base, size = heaps["#Strings"]
|
|
235
|
+
everything = _strings(data, base, size)
|
|
236
|
+
seen: set[str] = set()
|
|
237
|
+
kept: list[str] = []
|
|
238
|
+
dropped = 0
|
|
239
|
+
for item in everything:
|
|
240
|
+
if not item or item in seen:
|
|
241
|
+
continue
|
|
242
|
+
seen.add(item)
|
|
243
|
+
if _NOISE.match(item):
|
|
244
|
+
dropped += 1
|
|
245
|
+
continue
|
|
246
|
+
kept.append(item)
|
|
247
|
+
return BlockMembers(
|
|
248
|
+
name=name, assembly=assembly.name, runtime=version,
|
|
249
|
+
identifiers=kept, filtered_out=dropped,
|
|
250
|
+
)
|