@aryacode/dsh-engineering 0.4.22 → 0.6.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +10 -7
  3. package/cordis.patch.yml +1 -1
  4. package/entries/netlogic.js +266 -0
  5. package/entries/optix.js +13 -0
  6. package/package.json +3 -2
  7. package/vendor/ftview/SKILL.md +6 -0
  8. package/vendor/ftview/lib/model.d.ts +8 -3
  9. package/vendor/ftview/lib/model.js +63 -16
  10. package/vendor/l5x/lib/engine/analysis.d.ts +9 -1
  11. package/vendor/l5x/lib/engine/analysis.js +28 -3
  12. package/vendor/l5x/lib/engine/change-set.js +5 -4
  13. package/vendor/l5x/lib/engine/cip-request.d.ts +1 -0
  14. package/vendor/l5x/lib/engine/cip-request.js +71 -26
  15. package/vendor/l5x/lib/engine/direct-polling.js +12 -6
  16. package/vendor/l5x/lib/engine/review.js +12 -1
  17. package/vendor/l5x/lib/engine/studio-sdk.js +24 -6
  18. package/vendor/mwi/code/mw_code.py +4 -1
  19. package/vendor/mwi/legacy/native-variables.js +9 -1
  20. package/vendor/mwi/lib/read/service.d.ts +7 -1
  21. package/vendor/mwi/lib/read/service.js +13 -1
  22. package/vendor/mwi/lib/session/service.js +7 -1
  23. package/vendor/optix/SKILL.md +68 -0
  24. package/vendor/optix/netlogic/SOURCE.json +12 -0
  25. package/vendor/optix/netlogic/index.json +1987 -0
  26. package/vendor/optix/netlogic/pages/accessing-project-nodes.md +113 -0
  27. package/vendor/optix/netlogic/pages/advanced-svg.md +124 -0
  28. package/vendor/optix/netlogic/pages/alarming.md +489 -0
  29. package/vendor/optix/netlogic/pages/async-tasks.md +236 -0
  30. package/vendor/optix/netlogic/pages/audit-signature.md +91 -0
  31. package/vendor/optix/netlogic/pages/colors.md +122 -0
  32. package/vendor/optix/netlogic/pages/companion-specs.md +77 -0
  33. package/vendor/optix/netlogic/pages/creating-objects.md +370 -0
  34. package/vendor/optix/netlogic/pages/database-interaction.md +437 -0
  35. package/vendor/optix/netlogic/pages/datagrids.md +123 -0
  36. package/vendor/optix/netlogic/pages/debugging-tips.md +605 -0
  37. package/vendor/optix/netlogic/pages/designtime-tags-import.md +531 -0
  38. package/vendor/optix/netlogic/pages/dialog-boxes.md +150 -0
  39. package/vendor/optix/netlogic/pages/dispatch-events.md +200 -0
  40. package/vendor/optix/netlogic/pages/dynamic-links.md +825 -0
  41. package/vendor/optix/netlogic/pages/element-access.md +1208 -0
  42. package/vendor/optix/netlogic/pages/engineering-units.md +298 -0
  43. package/vendor/optix/netlogic/pages/events.md +340 -0
  44. package/vendor/optix/netlogic/pages/execute-command.md +202 -0
  45. package/vendor/optix/netlogic/pages/fto-studio-cli.md +99 -0
  46. package/vendor/optix/netlogic/pages/good-practices.md +50 -0
  47. package/vendor/optix/netlogic/pages/information-model.md +89 -0
  48. package/vendor/optix/netlogic/pages/log-output.md +22 -0
  49. package/vendor/optix/netlogic/pages/managing-aliases.md +22 -0
  50. package/vendor/optix/netlogic/pages/manipulate-nodes.md +72 -0
  51. package/vendor/optix/netlogic/pages/mqtt.md +240 -0
  52. package/vendor/optix/netlogic/pages/netlogic-lifecycle.md +270 -0
  53. package/vendor/optix/netlogic/pages/netlogic-overview.md +155 -0
  54. package/vendor/optix/netlogic/pages/opcua.md +325 -0
  55. package/vendor/optix/netlogic/pages/plc-tags.md +219 -0
  56. package/vendor/optix/netlogic/pages/queries.md +1297 -0
  57. package/vendor/optix/netlogic/pages/random-generation.md +90 -0
  58. package/vendor/optix/netlogic/pages/recipes.md +375 -0
  59. package/vendor/optix/netlogic/pages/recipex.md +2383 -0
  60. package/vendor/optix/netlogic/pages/regex.md +15 -0
  61. package/vendor/optix/netlogic/pages/register-observers.md +346 -0
  62. package/vendor/optix/netlogic/pages/reports.md +141 -0
  63. package/vendor/optix/netlogic/pages/resource-uri.md +308 -0
  64. package/vendor/optix/netlogic/pages/runtime-tags-import.md +280 -0
  65. package/vendor/optix/netlogic/pages/serial-port.md +357 -0
  66. package/vendor/optix/netlogic/pages/sessions.md +58 -0
  67. package/vendor/optix/netlogic/pages/system-node.md +153 -0
  68. package/vendor/optix/netlogic/pages/template-library.md +127 -0
  69. package/vendor/optix/netlogic/pages/threading.md +236 -0
  70. package/vendor/optix/netlogic/pages/translations.md +417 -0
  71. package/vendor/optix/netlogic/pages/trends.md +128 -0
  72. package/vendor/optix/netlogic/pages/ui-animations.md +115 -0
  73. package/vendor/optix/netlogic/pages/users-groups.md +416 -0
  74. package/vendor/optix/netlogic/pages/variables-formatting.md +16 -0
  75. package/vendor/optix/netlogic/pages/variables-interaction.md +557 -0
  76. package/vendor/optix/netlogic/pages/variables-structure.md +155 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,103 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.0] - 2026-10-27
4
+
5
+ ### Added
6
+
7
+ - **FactoryTalk Optix NetLogic reference.** The bundle vendors a pinned snapshot of Rockwell's
8
+ [NetLogic cheat sheet](https://github.com/FactoryTalk-Optix/NetLogic_CheatSheet) (51 pages, commit
9
+ `5e652541`) and exposes it as one tool instead of dumping C# into the prompt:
10
+ `optix_netlogic` with no arguments returns the contents, a `query` ranks pages by title, topic and
11
+ symbol before body text, and a `slug` returns that page verbatim. Every page result carries its
12
+ `risk` (`normal`, `advanced` or `unsupported`, with the repository's own Advanced section marked
13
+ unsupported), the upstream commit, a source URL and the upstream disclaimer that the snippets may use
14
+ non-public APIs and must not be used in production as-is.
15
+ - **A FactoryTalk Optix skill.** The row previously had tools and no guidance; `factorytalk-optix` now
16
+ names the surface, the NetLogic reader and its risk rule, and the report-before-claiming boundary.
17
+ - The `optix` row gains a `netlogic` switch (on by default). Off removes one tool and the guidance
18
+ without touching the prebuilt engine.
19
+ - `scripts/sync-netlogic.mjs` is the only writer of the snapshot: it pins a commit, copies
20
+ `pages/*.md`, and generates `index.json` plus `SOURCE.json` (commit, page count, risk counts).
21
+ `pnpm netlogic:sync` refreshes it; `test/netlogic.spec.mjs` fails when the index and the pages drift.
22
+
23
+ ## [0.5.0] - 2026-10-27
24
+
25
+ Commissioned against the Laminator fixtures: a real Studio 5000 ACD exported to a 5.4 MB L5X, a
26
+ real FactoryTalk View ME export, and two MotionWorks projects.
27
+
28
+ ### Fixed — FactoryTalk View could not read a real Studio export
29
+
30
+ `ftview_open` reported **0 displays and 0 tags** for a genuine ME export. Five independent causes:
31
+
32
+ - **The tag CSV delimiter was hard-coded to `;`** while Studio exports are comma-separated and use a
33
+ leading `;` only as a comment prefix, so every data row parsed as one cell. The separator is now
34
+ detected per line with quote awareness.
35
+ - **The header was assumed to follow the `###002` line.** A real export writes the header first, then
36
+ `###002`, then `;Folders Section` / `;Tag Section` comments, and the parser took the first comment
37
+ for the header. It now finds a `Tag Name` header on either side of the version line.
38
+ - **Short rows were rejected.** Studio omits trailing empty columns, so 29-column headers come with
39
+ 23-column rows. Only over-long rows are errors now.
40
+ - **`<gfx>` displays were refused.** ME display XML is `<gfx>` (Gfx-ME17.xsd), not `<display>`.
41
+ - **UTF-16 files were decoded as UTF-8**, producing a root element of `\0g\0f\0x`; BOM sniffing now
42
+ selects UTF-16LE.
43
+
44
+ Also normalised the host contract: `scanApplication` accepts the `FsTarget` that `fs.resolve`
45
+ returns instead of calling string methods on it.
46
+
47
+ Verified on `Laminator/HMI/Main`: **18 displays, 361 tags, 0 parse errors** (was 0 and 0). Reading a
48
+ `<gfx>` export is supported; the staged edit tools were built for the `<display>` shape and are not
49
+ verified to rewrite `<gfx>` objects.
50
+
51
+ ### Fixed — Studio 5000 checks drowned real findings in false positives
52
+
53
+ `run_checks` on the Laminator export produced 562 findings, 315 of them "unresolved reference", on a
54
+ project that compiles. Every one was an alias to module I/O (`In_FF_ModulePresent` →
55
+ `FanFold_StraightCut_AENTR:2:I.7`): aliases carry no `DataType`, their targets are not declared
56
+ `<Tag>`s, and the fast resolver stopped instead of falling back to the canonical one.
57
+
58
+ - `resolveSymbol` now falls back to `resolveTag`, and an empty type with a trailing numeric part
59
+ resolves as a bit.
60
+ - A member of a module-defined or opaque type (`Drive_MCFG.CommFLT`, `Axis_MAH.PC`) and `S:`/`R:`
61
+ system flags are reported as coverage limits, not unresolved references.
62
+
63
+ **562 → 264 findings; unresolved 315 → 16**, with the unused-tag and multiple-write findings intact.
64
+
65
+ ### Fixed — Studio 5000 edits
66
+
67
+ - **`add_tag` required a value for a scalar tag.** Studio defaults scalars to zero, so declaring a
68
+ plain REAL was impossible without inventing a value; `value` is now optional and still validated
69
+ when supplied.
70
+ - **Live reads refused aliases.** `read_live_tags` and `poll_live_tags` ran the write rule, so
71
+ ordinary module-point BOOLs were rejected as "not a writable declared scalar". Reads and polls now
72
+ take an alias's type from the canonical resolver; writes still refuse an alias and name the tag to
73
+ write instead.
74
+ - **`export_acd_to_l5x` failed when Studio had the ACD open.** The SDK opens the file itself, so the
75
+ normal case — the project the user is working on — failed with "being used by another process". The
76
+ export now copies the ACD and opens the copy, with the source hash re-checked before and after.
77
+
78
+ Verified end to end on the Laminator export: `draft_tag` (REAL, no value) plus a rung replacement
79
+ validated and applied to a 5,391,840-byte L5X.
80
+
81
+ ### Fixed — MotionWorks IEC
82
+
83
+ - **`mw_get kind="block"` crashed with `KeyError: 'native_libraries'`.** The block interface is decoded
84
+ against the installed libraries as well as the saved project; the read service now supplies the live
85
+ inventory and falls back to the project's own registry when no IDE answers.
86
+ - **`mw_edit_variables` refused with "Live declarations disagree with saved baseline"** and nothing
87
+ else. The refusal now names the missing, unexpected and changed declarations with their live and
88
+ saved values.
89
+
90
+ Verified on both Laminator projects through the shipped engine: attach into
91
+ `backups/motionworks/<project>/<stamp>/`, then `workflow_check`, `pous`, `read_text`, `validate`,
92
+ `tasks`, `types`, `globals` and `library` all succeed (8 POUs, 5 tasks, ~170 globals, 297 types,
93
+ 23 blocks each).
94
+
95
+ ### Documentation
96
+
97
+ - `docs/optix-netlogic.md`: how the FactoryTalk Optix NetLogic cheat sheet should be carried — a
98
+ pinned snapshot, a generated index, and one `optix_netlogic` retrieval tool, with the risk flags and
99
+ disclaimers the repository itself requires.
100
+
3
101
  ## [0.4.22] - 2026-10-27
4
102
 
5
103
  ### Fixed
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  MotionWorks IEC and FactoryTalk Optix sit behind a single install, one row per surface. A row is the
5
5
  switch: disable it and that plugin unloads, so its tool schemas leave the request.
6
6
 
7
- `@aryacode/dsh-engineering` · 84 tools · 5 rows · 4 products · MIT
7
+ `@aryacode/dsh-engineering` · 85 tools · 5 rows · 4 products · MIT
8
8
 
9
9
  ---
10
10
 
@@ -39,7 +39,7 @@ bundle.
39
39
  | **Studio 5000 L5X** | 40 | all on | Node for offline reads; Studio 5000 + Logix Designer SDK for SDK work; a controller for live |
40
40
  | **FactoryTalk View ME** | 14 | all on | Node for an export folder; FactoryTalk View Studio ME for discovery and the handoff |
41
41
  | **MotionWorks IEC** | 24 | all on | MotionWorks IEC 3 Pro installed; verified against 1.19 |
42
- | **FactoryTalk Optix** | 9 (3 need preview) | all on | Optix Studio installed; Tesseract for OCR |
42
+ | **FactoryTalk Optix** | 10 (3 need preview) | all on | Optix Studio installed; Tesseract for OCR |
43
43
 
44
44
  Tool groups, by product:
45
45
 
@@ -47,7 +47,7 @@ Tool groups, by product:
47
47
  Designer SDK, 7 supervised controller, 7 live EtherNet/IP (2 reads, 2 identity/discovery, 1 batch read, 2 approval-gated writes)
48
48
  - **FactoryTalk View** — 6 read, 5 staged edits, 3 Studio handoff
49
49
  - **MotionWorks IEC** — 7 session, 7 saved-project reads, 3 verify, 7 direct edits
50
- - **Optix** — 6 tools, unlinked to a skill until one exists
50
+ - **Optix** — 10 tools, linked to the `factorytalk-optix` guidance; `optix_netlogic` reads the pinned NetLogic cheat-sheet snapshot, and the 9 bridge tools need Studio
51
51
 
52
52
  ## Switches
53
53
 
@@ -62,7 +62,7 @@ down:
62
62
  | `ftview` | FactoryTalk View ME read, guidance and edits; optionally the Studio handoff | `edit` (on), `studio` (on) |
63
63
  | `mwi` | MotionWorks IEC session, reads, verify, guidance and the edits | `workspace` (empty), `edit` (on) |
64
64
  | `viewers` | ladder + ME display previews | — |
65
- | `optix` | FactoryTalk Optix Studio authoring | `bridgeUrls` (8768), `bridgeToken` (empty), `allowWrites` (on), `allowDestructive` (on), `allowInvoke` (on), `preview` (on), `allowPreviewInput` (on), `cdpUrl`, `previewUrl`, `ocrExecutable` (empty), `ocrLanguage` (`eng`), `ocrTessdataDirectory` (empty) |
65
+ | `optix` | FactoryTalk Optix Studio authoring and the NetLogic reader | `bridgeUrls` (8768), `bridgeToken` (empty), `allowWrites` (on), `allowDestructive` (on), `allowInvoke` (on), `preview` (on), `allowPreviewInput` (on), `cdpUrl`, `previewUrl`, `ocrExecutable` (empty), `ocrLanguage` (`eng`), `ocrTessdataDirectory` (empty), `netlogic` (on) |
66
66
 
67
67
  **Why the row list collapsed.** The Plugins page lists a bundle's rows flat and skips `cordis:group`
68
68
  containers (`plugin-manager/src/index.ts`: *"Group rows are containers … their children carry the
@@ -91,7 +91,9 @@ Where each surface actually stands, based on what has been exercised.
91
91
  downloads accepting only a verification id produced by a passed build
92
92
  - Live EtherNet/IP: polling, one-request batch reads of up to 64 tags, controller identity and clock, LAN discovery, and a bounded trend
93
93
  - Supervised live writes: `write_live_tag` and `write_controller_clock` need the row's `allowWrites` switch **and** per-call human approval, and report the value the controller reads back
94
- - FactoryTalk View ME: export reads, structural checks, comparison, staged display and HMI tag edits
94
+ - FactoryTalk View ME: export reads, structural checks, comparison, staged display and HMI tag edits;
95
+ reads verified on a real Studio export (`<gfx>` displays and the comma-delimited tag CSV —
96
+ 18 displays, 361 tags, no parse errors)
95
97
  - MotionWorks IEC: verified live against 3 Pro 1.19 — direct open with a verified pre-edit backup
96
98
  under `.motionworks/attached`, attach-without-reload to a project the IDE already has open, start,
97
99
  worksheet, close, saved-project reads, compile diagnostics, code replacement, declaration add, POU
@@ -110,7 +112,7 @@ the lean profile is the one that turns them off.
110
112
  - FBD/SFC semantics are not exercised
111
113
  - FactoryTalk View Studio work is a **manual handoff**: the bundle plans the import and preflights
112
114
  changed files, it does not import
113
- - Optix has no skill yet; its tools follow the `optix` row config and the Studio bridge
115
+ - Optix is linked to a skill now: the `factorytalk-optix` guidance plus the NetLogic reader; the bridge tools still follow the `optix` row config and the Studio bridge
114
116
 
115
117
  **Not in this repository** — CAD mounts as an independent plugin.
116
118
 
@@ -118,6 +120,7 @@ the lean profile is the one that turns them off.
118
120
 
119
121
  | Version | What landed |
120
122
  |---|---|
123
+ | **0.5.0** | Commissioned on real files: FactoryTalk View reads genuine Studio exports (`<gfx>` displays, comma CSVs, UTF-16) instead of reporting zero; L5X alias resolution cuts 315 false "unresolved reference" findings to 16 on a 1279-tag project; `add_tag` no longer demands a value; a locked ACD exports from a copy |
121
124
  | **0.4.17** | MotionWorks edits the real project again: the pre-edit backup is written where the bridge and engine verify it, an already-open project is attached instead of reloaded, and `mw_get` returns the body hash the edit takes |
122
125
  | **0.4.15** | Every tool surface ships on by default; the row Configure pages switch them off |
123
126
  | **0.4.4** | One row per product: five rows, optional surfaces moved into each row's config |
@@ -172,7 +175,7 @@ even when the implementation behind a row is replaced.
172
175
  | `ftview` | `dsh-engineering-ftview` | `edit` on · `studio` on |
173
176
  | `mwi` | `dsh-engineering-mwi` | `edit` on |
174
177
  | `viewers` | `@aryacode/dsh-engineering` (its own client face: ladder + ME display previews) | — |
175
- | `optix` | `@aryacode/dsh-engineering/optix` (wraps the prebuilt `@aryacode/dsh-tool-ft-optix`) | `allowWrites` on · `allowDestructive` on · `allowInvoke` on · `preview` on |
178
+ | `optix` | `@aryacode/dsh-engineering/optix` (wraps the prebuilt `@aryacode/dsh-tool-ft-optix`) | `allowWrites` on · `allowDestructive` on · `allowInvoke` on · `preview` on · `netlogic` on |
176
179
 
177
180
  ## Safety
178
181
 
package/cordis.patch.yml CHANGED
@@ -9,7 +9,7 @@
9
9
  # l5x → editing, studio, live, allowWrites (all on by default)
10
10
  # ftview → edit, studio (both on by default)
11
11
  # mwi → edit (on by default)
12
- # optix → authoring, destructive, invoke, preview (all on by default)
12
+ # optix → authoring, destructive, invoke, preview, netlogic (all on by default)
13
13
  #
14
14
  # Every surface ships on so a fresh install can use the full plugin. Turn one
15
15
  # off from the row's Configure page when the profile is read-only or the request
@@ -0,0 +1,266 @@
1
+ /**
2
+ * FactoryTalk Optix NetLogic retrieval.
3
+ *
4
+ * The vendor repository (FactoryTalk-Optix/NetLogic_CheatSheet) is vendored as a
5
+ * pinned snapshot by `scripts/sync-netlogic.mjs`. This entry exposes it as one tool
6
+ * so a model can pull the page it needs instead of every page at once: the table of
7
+ * contents costs a few hundred tokens, a page a few thousand.
8
+ *
9
+ * Every answer carries the page's risk flag and the upstream disclaimer. The
10
+ * repository itself says the snippets may use non-public APIs, may break a project
11
+ * and are not for production as-is; this module must not outrank that.
12
+ *
13
+ * @module dsh-engineering/entries/netlogic
14
+ */
15
+
16
+ import { existsSync, readFileSync } from 'node:fs'
17
+ import { dirname, join } from 'node:path'
18
+ import { fileURLToPath } from 'node:url'
19
+
20
+ /** Plugin name in a composition. */
21
+ export const name = 'engineering-optix-netlogic'
22
+
23
+ /** The tool needs the registry; the skill is registered when the host offers it. */
24
+ export const inject = ['tools']
25
+
26
+ /** Registration metadata, kept here so a test can assert it without the file. */
27
+ export const SKILL = {
28
+ name: 'factorytalk-optix',
29
+ description: 'FactoryTalk Optix Studio engineering: inspect and author the loaded project through the Studio bridge, validate expressions, take preview screenshots, read NetLogic C# snippets from the pinned cheat-sheet snapshot, and start from a template.',
30
+ whenToUse: 'The user asks about FactoryTalk Optix — project structure, nodes, variables, aliases, expressions, NetLogic C#, alarms, trends, recipes, OPC UA, or the emulator preview. Route FactoryTalk View ME elsewhere.',
31
+ }
32
+
33
+ /** The upstream statement every page result repeats. */
34
+ export const DISCLAIMER = 'Reference only. The upstream repository states these snippets may use non-public APIs, may break a project, and must not be used in production as-is; adapt and test them in your own project.'
35
+
36
+ const HERE = dirname(fileURLToPath(import.meta.url))
37
+ const DATA_DIRS = [
38
+ join(HERE, '..', 'vendor', 'optix', 'netlogic'),
39
+ join(HERE, '..', 'packages', 'optix', 'netlogic'),
40
+ ]
41
+ const SKILL_PATHS = [
42
+ join(HERE, '..', 'vendor', 'optix', 'SKILL.md'),
43
+ join(HERE, '..', 'packages', 'optix', 'SKILL.md'),
44
+ ]
45
+
46
+ const RISKS = new Set(['normal', 'advanced', 'unsupported'])
47
+
48
+ /** The first existing path from a candidate list. */
49
+ function first(candidates) {
50
+ for (const candidate of candidates) if (existsSync(candidate)) return candidate
51
+ return undefined
52
+ }
53
+
54
+ /** The vendored snapshot directory, or undefined when it was not shipped. */
55
+ export function dataDirectory() {
56
+ return first(DATA_DIRS)
57
+ }
58
+
59
+ /** Cached index and source pin; the snapshot is immutable for a process. */
60
+ let cachedSnapshot
61
+ function snapshot() {
62
+ if (cachedSnapshot === undefined) cachedSnapshot = loadSnapshot()
63
+ return cachedSnapshot
64
+ }
65
+
66
+ /** Cached page text, keyed by slug. */
67
+ const pageCache = new Map()
68
+
69
+ /** Read the generated index and source pin. */
70
+ export function loadSnapshot() {
71
+ const directory = dataDirectory()
72
+ if (directory === undefined) throw new Error('The NetLogic snapshot is missing; run `pnpm netlogic:sync` before packing.')
73
+ const index = JSON.parse(readFileSync(join(directory, 'index.json'), 'utf8'))
74
+ const source = JSON.parse(readFileSync(join(directory, 'SOURCE.json'), 'utf8'))
75
+ if (!Array.isArray(index) || index.length === 0) throw new Error('The NetLogic index is empty; run `pnpm netlogic:sync`.')
76
+ for (const row of index) {
77
+ if (typeof row?.slug !== 'string' || !/^[a-z0-9][a-z0-9-]*$/u.test(row.slug)) throw new Error(`The NetLogic index has an invalid slug: ${String(row?.slug)}`)
78
+ if (!RISKS.has(row.risk)) throw new Error(`The NetLogic index has an invalid risk for ${row.slug}: ${String(row.risk)}`)
79
+ }
80
+ return { directory, index, source }
81
+ }
82
+
83
+ /** Read one page's markdown, refusing anything outside the snapshot. */
84
+ export function pageOf(slug) {
85
+ if (typeof slug !== 'string' || !/^[a-z0-9][a-z0-9-]*$/u.test(slug)) {
86
+ throw Object.assign(new Error(`"${String(slug)}" is not a page slug.`), { code: 'INVALID_ARGUMENT' })
87
+ }
88
+ const cached = pageCache.get(slug)
89
+ if (cached !== undefined) return cached
90
+ const { directory } = snapshot()
91
+ const path = join(directory, 'pages', `${slug}.md`)
92
+ if (!existsSync(path)) throw Object.assign(new Error(`No page named "${slug}".`), { code: 'COMPONENT_NOT_FOUND' })
93
+ const markdown = readFileSync(path, 'utf8')
94
+ pageCache.set(slug, markdown)
95
+ return markdown
96
+ }
97
+
98
+ /** Score one page against a query; higher is better, zero means no hit. */
99
+ function scoreOf(row, query) {
100
+ const needle = query.toLowerCase()
101
+ if (row.slug === needle) return 100
102
+ if (row.title.toLowerCase() === needle) return 90
103
+ if (row.symbols.some(symbol => symbol.toLowerCase() === needle)) return 80
104
+ if (row.slug.includes(needle)) return 60
105
+ if (row.title.toLowerCase().includes(needle)) return 55
106
+ if (row.topics.some(topic => topic === needle)) return 45
107
+ if (row.symbols.some(symbol => symbol.toLowerCase().startsWith(needle))) return 35
108
+ return 0
109
+ }
110
+
111
+ /** Rank pages by metadata, then by body text for the remainder. */
112
+ export function search(index, query, limit = 8) {
113
+ const scored = index.map(row => ({ row, score: scoreOf(row, query) })).filter(hit => hit.score > 0)
114
+ if (scored.length < limit) {
115
+ const seen = new Set(scored.map(hit => hit.row.slug))
116
+ for (const row of index) {
117
+ if (seen.has(row.slug)) continue
118
+ const body = pageOf(row.slug)
119
+ if (body.toLowerCase().includes(query.toLowerCase())) scored.push({ row, score: 20 })
120
+ }
121
+ }
122
+ return scored
123
+ .sort((left, right) => right.score - left.score || left.row.title.localeCompare(right.row.title))
124
+ .slice(0, limit)
125
+ .map(hit => ({ slug: hit.row.slug, title: hit.row.title, section: hit.row.section, risk: hit.row.risk, score: hit.score }))
126
+ }
127
+
128
+ /** The coverage every answer carries. */
129
+ const COVERAGE = {
130
+ complete: false,
131
+ unverified: [
132
+ 'C# compiles against the installed Optix version',
133
+ 'the snippet is safe for this project',
134
+ 'runtime behaviour in the emulator or on the panel',
135
+ ],
136
+ }
137
+
138
+ /**
139
+ * Answer one call.
140
+ *
141
+ * @param args - `slug` for one page, `query` to search, or neither for the contents.
142
+ * @returns The tool envelope.
143
+ */
144
+ export function answer(args = {}) {
145
+ const { index, source } = snapshot()
146
+ const slug = typeof args.slug === 'string' && args.slug.trim() !== '' ? args.slug.trim() : undefined
147
+ if (slug !== undefined) {
148
+ const markdown = pageOf(slug)
149
+ const row = index.find(entry => entry.slug === slug)
150
+ return {
151
+ summary: `Read ${row.title} (${row.risk}) from ${source.repository}@${source.commit.slice(0, 8)}.`,
152
+ data: {
153
+ slug,
154
+ title: row.title,
155
+ section: row.section,
156
+ risk: row.risk,
157
+ commit: source.commit,
158
+ source_url: `https://github.com/${source.repository}/blob/${source.commit}/pages/${slug}.md`,
159
+ code_blocks: Math.floor((markdown.match(/^```/gmu) ?? []).length / 2),
160
+ disclaimer: DISCLAIMER,
161
+ markdown,
162
+ },
163
+ next: [{ tool: 'optix_edit', why: 'Apply the adapted snippet through the Studio bridge, then validate.' }],
164
+ coverage: COVERAGE,
165
+ }
166
+ }
167
+ const query = typeof args.query === 'string' && args.query.trim() !== '' ? args.query.trim() : undefined
168
+ if (query === undefined) {
169
+ return {
170
+ summary: `${index.length} NetLogic pages from ${source.repository}@${source.commit.slice(0, 8)}.`,
171
+ data: {
172
+ commit: source.commit,
173
+ disclaimer: DISCLAIMER,
174
+ pages: index.map(row => ({ slug: row.slug, title: row.title, section: row.section, risk: row.risk })),
175
+ },
176
+ next: [{ tool: 'optix_netlogic', args: { query: 'sessions' }, why: 'Read the page for a topic before writing NetLogic.' }],
177
+ coverage: COVERAGE,
178
+ }
179
+ }
180
+ const requested = typeof args.limit === 'number' && Number.isInteger(args.limit) ? args.limit : 8
181
+ const limit = Math.min(Math.max(requested, 1), 20)
182
+ const hits = search(index, query, limit)
183
+ return {
184
+ summary: hits.length === 0
185
+ ? `No NetLogic page matches "${query}".`
186
+ : `${hits.length} NetLogic page(s) match "${query}".`,
187
+ data: { commit: source.commit, query, hits, ...(hits.length === 0 ? { suggestion: 'Call optix_netlogic with no arguments for the contents, then pass one slug.' } : {}) },
188
+ next: hits.length === 0
189
+ ? [{ tool: 'optix_netlogic', why: 'List the contents and pick a slug.' }]
190
+ : [{ tool: 'optix_netlogic', args: { slug: hits[0].slug }, why: 'Read the best-matching page.' }],
191
+ coverage: COVERAGE,
192
+ }
193
+ }
194
+
195
+ /** The failure envelope every engineering tool returns. */
196
+ function failure(error) {
197
+ const code = typeof error?.code === 'string' ? error.code : 'INTERNAL'
198
+ return {
199
+ error: {
200
+ code,
201
+ message: error instanceof Error ? error.message : String(error),
202
+ fix: code === 'COMPONENT_NOT_FOUND'
203
+ ? 'Call optix_netlogic with no arguments for the available slugs.'
204
+ : 'Correct the argument and retry; the snapshot is local and needs no network.',
205
+ candidates: [],
206
+ },
207
+ }
208
+ }
209
+
210
+ /** The tool definition this entry registers. */
211
+ export const definition = {
212
+ name: 'optix_netlogic',
213
+ description: 'Read the pinned FactoryTalk Optix NetLogic cheat-sheet snapshot: return the contents with no arguments, ranked pages for a query, or one page in full for a slug. Every page result carries its risk flag, the upstream commit and the upstream disclaimer.',
214
+ parameters: {
215
+ type: 'object',
216
+ properties: {
217
+ query: { type: 'string', description: 'Topic, method or symbol to search for, for example "sessions", "PeriodicTask" or "alarm".' },
218
+ slug: { type: 'string', description: 'Exact page slug from the contents, for example "netlogic-lifecycle".' },
219
+ limit: { type: 'integer', description: 'Most search hits to return; defaults to 8, maximum 20.', minimum: 1, maximum: 20 },
220
+ },
221
+ additionalProperties: false,
222
+ required: [],
223
+ },
224
+ output: {
225
+ schema: { type: 'object', additionalProperties: true },
226
+ render(_args, value) {
227
+ return [{ type: 'text', text: JSON.stringify(value, undefined, 2) }]
228
+ },
229
+ },
230
+ async execute(args) {
231
+ try {
232
+ return answer(args ?? {})
233
+ } catch (error) {
234
+ return failure(error)
235
+ }
236
+ },
237
+ }
238
+
239
+ /**
240
+ * Register the retrieval tool and, when the host offers it, the Optix skill.
241
+ *
242
+ * @param ctx - Plugin context with the tool registry and an optional skills service.
243
+ */
244
+ export function apply(ctx) {
245
+ ctx.tools.register(definition)
246
+ const skillPath = first(SKILL_PATHS)
247
+ const skills = typeof ctx.get === 'function' ? ctx.get('skills') : undefined
248
+ if (skillPath === undefined || skills === undefined || typeof skills.register !== 'function') {
249
+ try {
250
+ ctx.logger?.warn?.('optix-netlogic: the skill was not registered; the tool is available.')
251
+ } catch {
252
+ // A logger that refuses must not turn a missing skill into a crash.
253
+ }
254
+ return
255
+ }
256
+ const raw = readFileSync(skillPath, 'utf8')
257
+ const content = raw.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/u, '')
258
+ skills.register({
259
+ ...SKILL,
260
+ content,
261
+ path: skillPath,
262
+ source: 'custom',
263
+ provider: '@aryacode/dsh-engineering',
264
+ invocation: { modelInvocable: true, userInvocable: true },
265
+ })
266
+ }
package/entries/optix.js CHANGED
@@ -11,6 +11,7 @@
11
11
  import z from '@deepseek-ai/schemastery'
12
12
  import { dirname, join } from 'node:path'
13
13
  import * as optix from '../vendor/optix/lib/index.js'
14
+ import * as netlogic from './netlogic.js'
14
15
  import { flag, list, text } from './config.js'
15
16
 
16
17
  export const name = 'engineering-optix'
@@ -53,6 +54,9 @@ export const Config = z.object({
53
54
  ocrTessdataDirectory: z.string().default('').volatile().description(
54
55
  'Folder holding the .traineddata files. Empty derives tessdata beside tesseract.exe.',
55
56
  ),
57
+ netlogic: z.boolean().default(true).volatile().description(
58
+ 'Register the pinned NetLogic cheat-sheet reader (optix_netlogic) and the FactoryTalk Optix skill. Off removes one tool and its guidance.',
59
+ ),
56
60
  requestTimeoutMs: z.natural().min(1).max(2147483647).default(20000),
57
61
  maxResponseBytes: z.natural().min(1).default(2000000),
58
62
  maxBatchOps: z.natural().min(1).default(40),
@@ -93,12 +97,21 @@ export function apply(ctx, config = {}) {
93
97
  }
94
98
  }
95
99
  let mounted
100
+ let netlogicMounted
96
101
  // Volatile-only config changes are committed into the live references and
97
102
  // emit `loader/volatile-update` instead of re-running apply(). The prebuilt
98
103
  // plugin reads its options once, so remount it to pick up a switch change.
99
104
  const sync = () => {
100
105
  if (mounted !== undefined) void mounted.dispose()
101
106
  mounted = ctx.plugin(optix, options())
107
+ // The NetLogic reader is this repository's own entry: it is a separate plugin
108
+ // so the row's `netlogic` switch can drop one tool and its guidance without
109
+ // touching the prebuilt engine.
110
+ if (netlogicMounted !== undefined) {
111
+ void netlogicMounted.dispose()
112
+ netlogicMounted = undefined
113
+ }
114
+ if (flag(config.netlogic)) netlogicMounted = ctx.plugin(netlogic)
102
115
  }
103
116
  sync()
104
117
  ctx.on('loader/volatile-update', sync)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aryacode/dsh-engineering",
3
- "version": "0.4.22",
3
+ "version": "0.6.0",
4
4
  "description": "AryaCode engineering bundle: one install surface for the Studio 5000 L5X, FactoryTalk View ME, MotionWorks IEC and FactoryTalk Optix surfaces, one row per product, with the ladder and display previews.",
5
5
  "keywords": [
6
6
  "dsh",
@@ -56,9 +56,10 @@
56
56
  }
57
57
  },
58
58
  "scripts": {
59
- "test": "node --import tsx --test test/patch.spec.mjs test/docs.spec.mjs test/tool-reference.spec.mjs test/manifests.spec.mjs test/client-face.spec.mjs test/config.spec.mjs",
59
+ "test": "node --import tsx --test test/patch.spec.mjs test/docs.spec.mjs test/tool-reference.spec.mjs test/manifests.spec.mjs test/client-face.spec.mjs test/config.spec.mjs test/netlogic.spec.mjs",
60
60
  "docs:tools": "tsx scripts/gen-tool-reference.ts",
61
61
  "docs:check": "tsx scripts/gen-tool-reference.ts --check",
62
+ "netlogic:sync": "node scripts/sync-netlogic.mjs",
62
63
  "build:client": "node scripts/build-client.mjs",
63
64
  "assemble": "node scripts/assemble.mjs",
64
65
  "prepack": "node scripts/build-client.mjs && node scripts/assemble.mjs"
@@ -13,6 +13,12 @@ A staged edit is written to a NEW export folder; the source is never modified. T
13
13
  a manual import for a human; it does not drive Studio, contact a terminal, or decode MER or MED
14
14
  databases.
15
15
 
16
+ Both export shapes are read: the Studio ME export (`<gfx>` displays, `Gfx-ME17.xsd`, a comma-delimited
17
+ `Tags/*.csv` with its header before the `###002` line and `;` comment rows) and the older
18
+ `<display>`-root files. Staged attribute edits were built for the `<display>` shape; on a `<gfx>`
19
+ export, read and verify with this surface and make the change in Studio, or confirm the edit applied
20
+ before relying on it.
21
+
16
22
  ## Backups
17
23
 
18
24
  Every engineering product backs up into the same tree, so a backup means one place whichever surface
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * @module dsh-engineering-ftview/model
10
10
  */
11
- import { type FsService } from './core.ts';
11
+ import { type FsService, type FsTarget } from './core.ts';
12
12
  /** One row of the HMI tag CSV, keyed by its canonical columns. */
13
13
  export interface TagRow {
14
14
  readonly name: string;
@@ -51,11 +51,16 @@ export interface TagCsv {
51
51
  readonly rows: readonly TagRow[];
52
52
  readonly errors: readonly string[];
53
53
  }
54
- /** Split one semicolon-separated CSV line, honouring double quotes. */
54
+ /** Split one CSV line, honouring double quotes and the line's own separator. */
55
55
  export declare function splitCsv(line: string): string[];
56
56
  /**
57
57
  * Parse the HMI tag CSV.
58
58
  *
59
+ * The ME export writes an optional header first, then a `###002` version line, then
60
+ * `;` comment/section lines and the data rows. Earlier builds assumed the header
61
+ * followed the version line and treated `;Folders Section` as the header, so a real
62
+ * export parsed as zero tags.
63
+ *
59
64
  * @param text - Whole CSV file.
60
65
  * @returns Rows plus structural errors; a file with no `###002` marker yields no rows.
61
66
  */
@@ -77,4 +82,4 @@ export declare function parseDisplay(file: string, text: string): DisplayFile;
77
82
  * @param maxFiles - Largest number of display/tag files accepted.
78
83
  * @returns The scanned application.
79
84
  */
80
- export declare function scanApplication(fs: FsService, root: string, signal: AbortSignal | undefined, maxFiles: number): Promise<Application>;
85
+ export declare function scanApplication(fs: FsService, root: string | FsTarget, signal: AbortSignal | undefined, maxFiles: number): Promise<Application>;