@aryacode/dsh-engineering 0.8.4 → 0.9.1

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 (64) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +7 -1
  3. package/cordis.patch.yml +7 -0
  4. package/entries/resources.js +23 -0
  5. package/package.json +6 -3
  6. package/vendor/ftview/lib/edit/tools.d.ts +1 -1
  7. package/vendor/ftview/lib/edit/tools.js +26 -6
  8. package/vendor/ftview/lib/edit.js +2 -1
  9. package/vendor/ftview/lib/session.d.ts +9 -0
  10. package/vendor/ftview/lib/session.js +11 -0
  11. package/vendor/ftview/lib/style.d.ts +60 -0
  12. package/vendor/ftview/lib/style.js +109 -0
  13. package/vendor/l5x/lib/engine/style.d.ts +67 -0
  14. package/vendor/l5x/lib/engine/style.js +119 -0
  15. package/vendor/l5x/lib/session.d.ts +9 -0
  16. package/vendor/l5x/lib/session.js +11 -0
  17. package/vendor/resources/LICENSE-CONTENT.md +17 -0
  18. package/vendor/resources/SKILL.md +27 -0
  19. package/vendor/resources/SOURCE.json +6 -0
  20. package/vendor/resources/corpus/craft/craft-comments-and-intent.md +29 -0
  21. package/vendor/resources/corpus/craft/craft-debounce-and-filtering.md +29 -0
  22. package/vendor/resources/corpus/craft/craft-fault-handling.md +32 -0
  23. package/vendor/resources/corpus/craft/craft-naming.md +30 -0
  24. package/vendor/resources/corpus/craft/craft-review-checklist.md +29 -0
  25. package/vendor/resources/corpus/craft/craft-units-and-ranges.md +32 -0
  26. package/vendor/resources/corpus/iec/iec-ld-fbd-equivalence.md +31 -0
  27. package/vendor/resources/corpus/iec/iec-sequences-and-sfc.md +32 -0
  28. package/vendor/resources/corpus/iec/iec-st-structure.md +33 -0
  29. package/vendor/resources/corpus/iec/iec-state-machines.md +33 -0
  30. package/vendor/resources/corpus/iec/iec-task-model.md +31 -0
  31. package/vendor/resources/corpus/iec/iec-types-and-scaling.md +32 -0
  32. package/vendor/resources/corpus/optix/optix-alarms-and-trends.md +30 -0
  33. package/vendor/resources/corpus/optix/optix-bindings-and-expressions.md +30 -0
  34. package/vendor/resources/corpus/optix/optix-netlogic-usage.md +29 -0
  35. package/vendor/resources/corpus/optix/optix-ui-structure.md +29 -0
  36. package/vendor/resources/corpus/rockwell/rockwell-aoi-and-udt.md +32 -0
  37. package/vendor/resources/corpus/rockwell/rockwell-fault-and-alarm-words.md +31 -0
  38. package/vendor/resources/corpus/rockwell/rockwell-hmi-handshake.md +33 -0
  39. package/vendor/resources/corpus/rockwell/rockwell-program-structure.md +32 -0
  40. package/vendor/resources/corpus/rockwell/rockwell-safety-boundaries.md +31 -0
  41. package/vendor/resources/corpus/rockwell/rockwell-tag-naming.md +33 -0
  42. package/vendor/resources/corpus/view/view-alarm-presentation.md +32 -0
  43. package/vendor/resources/corpus/view/view-colour-and-contrast.md +31 -0
  44. package/vendor/resources/corpus/view/view-faceplates.md +32 -0
  45. package/vendor/resources/corpus/view/view-hpm-01.md +33 -0
  46. package/vendor/resources/corpus/view/view-navigation.md +31 -0
  47. package/vendor/resources/corpus/view/view-popup-patterns.md +31 -0
  48. package/vendor/resources/corpus/view/view-screen-hierarchy.md +31 -0
  49. package/vendor/resources/corpus/yaskawa/yaskawa-cam-and-gear.md +31 -0
  50. package/vendor/resources/corpus/yaskawa/yaskawa-error-handling.md +31 -0
  51. package/vendor/resources/corpus/yaskawa/yaskawa-motion-function-blocks.md +32 -0
  52. package/vendor/resources/corpus/yaskawa/yaskawa-pou-organisation.md +29 -0
  53. package/vendor/resources/corpus/yaskawa/yaskawa-task-rates.md +30 -0
  54. package/vendor/resources/index.json +873 -0
  55. package/vendor/resources/lib/corpus.d.ts +80 -0
  56. package/vendor/resources/lib/corpus.js +126 -0
  57. package/vendor/resources/lib/index.d.ts +55 -0
  58. package/vendor/resources/lib/index.js +88 -0
  59. package/vendor/resources/lib/profile.d.ts +113 -0
  60. package/vendor/resources/lib/profile.js +52 -0
  61. package/vendor/resources/lib/store.d.ts +22 -0
  62. package/vendor/resources/lib/store.js +35 -0
  63. package/vendor/resources/lib/tools.d.ts +138 -0
  64. package/vendor/resources/lib/tools.js +190 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.1] - 2026-10-27
4
+
5
+ ### Fixed
6
+
7
+ - **0.9.0 could stop AryaCode from booting.** The new row was inserted with `id: resources`, and `resources`
8
+ is the name of a harness service other entries wait for; inserting a row under that id displaced the
9
+ provider, so the boot reported `44 entries did not activate` and the application had to start with plugins
10
+ disabled. The row is now `engineering-resources` (namespaced like every other non-product row), and a test
11
+ rejects any future row id that matches a harness service name.
12
+ - **The resources row can no longer break a boot even if it fails.** Its `apply()` wraps registration and
13
+ skill mounting in a guard that reports and continues; a reference surface must never be load-bearing.
14
+ - **The corpus is now actually shipped.** The assembler copied only `lib/` and `SKILL.md`, so the vendored
15
+ package had no `corpus/`, `index.json` or `SOURCE.json` and `engineering_reference` would have failed at
16
+ the first call. `scripts/assemble.mjs` now copies those files, and the packed artifact is checked for them.
17
+ ## [0.9.0] - 2026-10-27
18
+
19
+ ### Added
20
+
21
+ - **Model-agnostic engineering resources: a local standards corpus.** 34 authored pages across six domains
22
+ (IEC 61131-3, Rockwell, Yaskawa, FactoryTalk View/HMI, Optix, craft) with front-matter provenance and 100+
23
+ rule IDs (`CRAFT-NAME-01`, `VIEW-HPM-02`, …). `engineering_reference` lists the contents, ranks pages for a
24
+ query and returns one page with its rules; search is deterministic and needs no network, embedding model or
25
+ tokenizer. `scripts/gen-resources.mjs` validates every page (fields, domains, id/file agreement, rule
26
+ uniqueness, size and word budgets) and generates `index.json`/`SOURCE.json`; `pnpm resources:check` fails on
27
+ a stale index.
28
+ - **House-style profiles.** `engineering_profile` reads the conventions of the open project — L5X tag prefixes
29
+ and case, seal-in and latch forms, comment density; FactoryTalk View palette, fonts, button geometry,
30
+ navigation and popups — with counts, evidence and a `low`/`medium`/`high` confidence. A profile is evidence,
31
+ not authority, and the tool says which project and SHA-256 it came from.
32
+ - **Generation follows the profile.** `ftview_draft_display_create` applies the profile's background colour and
33
+ `ftview_draft_object_add` its button geometry and font when the caller omits them, reporting
34
+ `profile_applied`.
35
+ - **New `resources` row and the `engineering-standards` skill.** The row mounts the two tools and the skill
36
+ that tells a model to read the relevant page and the project profile before authoring. Tools 97 → 99, rows
37
+ 5 → 6.
38
+ - `docs/resources.md` documents the corpus, its provenance, the index commands and the profile schema.
39
+
40
+ ### Known limits
41
+
42
+ - `engineering_review` (rule-based draft review) and MWI/Optix profile extraction are planned for 0.9.1; the
43
+ corpus already applies to those products and the product validation still judges the result.
3
44
  ## [0.8.4] - 2026-10-27
4
45
 
5
46
  ### Added
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` · 97 tools · 5 rows · 4 products · MIT
7
+ `@aryacode/dsh-engineering` · 99 tools · 6 rows · 4 products · MIT
8
8
 
9
9
  **New here? Start with [Getting started](docs/getting-started.md)** — what each row needs, the loop every
10
10
  product follows, and the first prompt to paste for each of the four.
@@ -43,6 +43,7 @@ bundle.
43
43
  | **FactoryTalk View ME** | 22 | all on | Node for an export folder; FactoryTalk View Studio ME for discovery and the handoff |
44
44
  | **MotionWorks IEC** | 24 | all on | MotionWorks IEC 3 Pro installed; verified against 1.19 |
45
45
  | **FactoryTalk Optix** | 10 (3 need preview) | all on | Optix Studio installed; Tesseract for OCR |
46
+ | **Engineering resources** | 2 | all on | none; a profile needs an L5X or FactoryTalk View project open |
46
47
 
47
48
  Tool groups, by product:
48
49
 
@@ -51,6 +52,9 @@ Tool groups, by product:
51
52
  - **FactoryTalk View** — 6 read, 5 staged edits, 3 Studio handoff
52
53
  - **MotionWorks IEC** — 7 session, 7 saved-project reads, 3 verify, 7 direct edits
53
54
  - **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
55
+ - **Engineering resources** — 2 tools and the `engineering-standards` skill: `engineering_reference` reads the
56
+ local 34-page corpus (IEC 61131-3, Rockwell, Yaskawa, HMI, Optix, craft) and `engineering_profile` infers the
57
+ open project's own naming, idioms and HMI palette, with evidence and a confidence level
54
58
 
55
59
  ## Switches
56
60
 
@@ -65,6 +69,7 @@ down:
65
69
  | `ftview` | FactoryTalk View ME read, guidance and edits; optionally the Studio handoff | `edit` (on), `studio` (on) |
66
70
  | `mwi` | MotionWorks IEC session, reads, verify, guidance and the edits | `workspace` (empty), `edit` (on) |
67
71
  | `viewers` | ladder + ME display previews | — |
72
+ | `engineering-resources` | the engineering corpus, the house-style profile and the `engineering-standards` skill; no project of its own | `reference` (on) |
68
73
  | `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) |
69
74
 
70
75
  **Why the row list collapsed.** The Plugins page lists a bundle's rows flat and skips `cordis:group`
@@ -238,6 +243,7 @@ even when the implementation behind a row is replaced.
238
243
  | Document | Owns |
239
244
  |---|---|
240
245
  | [Getting started](docs/getting-started.md) | The first session: what each row needs, the shared loop, and a paste-ready prompt per product |
246
+ | [Engineering resources](docs/resources.md) | The 34-page standards corpus, its rule IDs, the generated index, and the house-style profile |
241
247
  | [Workflow handbook (PDF)](docs/handbook/Engineering-Handbook.pdf) | The whole surface as a printable handbook with the four workflow diagrams; [editable DOCX](docs/handbook/Engineering-Handbook.docx) and [HTML](docs/handbook/Engineering-Handbook.html) beside it |
242
248
  | [Tool reference](docs/reference/tools.md) | Every tool, parameter and description (generated) |
243
249
  | [Examples](docs/examples.md) | End-to-end sessions per product |
package/cordis.patch.yml CHANGED
@@ -38,6 +38,13 @@
38
38
  # context, so this row is independent of every product row above.
39
39
  - id: viewers
40
40
  name: '@aryacode/dsh-engineering'
41
+ # Cross-product engineering resources: a local standards corpus (IEC 61131-3,
42
+ # Rockwell, Yaskawa, HMI and craft rules), a house-style profile read from the
43
+ # open project, and the engineering-standards skill that says to read both
44
+ # before authoring. Disable it in a lean profile; it adds no product row.
45
+ - id: engineering-resources
46
+ name: '@aryacode/dsh-engineering/resources'
47
+
41
48
  # FactoryTalk Optix: vendored like the other three products, so one install
42
49
  # carries every surface and no component package is a dependency.
43
50
  - id: optix
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The engineering resources row: the standards corpus, the house-style profile
3
+ * and the `engineering-standards` skill.
4
+ *
5
+ * The corpus and the index are generated data shipped inside the vendored
6
+ * package; this entry only mounts them. A profile needs a product row to have a
7
+ * project open, and says so when none is.
8
+ */
9
+ import * as resources from '../vendor/resources/lib/index.js'
10
+
11
+ export const name = 'engineering-resources'
12
+
13
+ /** The tools need the registry; the skill is registered when the host offers it. */
14
+ export const inject = ['tools']
15
+
16
+ /**
17
+ * Mount the resources surface.
18
+ * @param ctx - plugin context.
19
+ * @param config - `reference` (default true) controls the two read tools.
20
+ */
21
+ export function apply(ctx, config = {}) {
22
+ resources.apply(ctx, config)
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aryacode/dsh-engineering",
3
- "version": "0.8.4",
3
+ "version": "0.9.1",
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",
@@ -93,13 +93,16 @@
93
93
  "./mwi/read": "./vendor/mwi/lib/read.js",
94
94
  "./mwi/skill": "./vendor/mwi/lib/skill.js",
95
95
  "./mwi/verify": "./vendor/mwi/lib/verify.js",
96
- "./mwi/edit": "./vendor/mwi/lib/edit.js"
96
+ "./mwi/edit": "./vendor/mwi/lib/edit.js",
97
+ "./resources": "./entries/resources.js"
97
98
  },
98
99
  "scripts": {
99
- "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",
100
+ "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 test/resources.spec.mjs",
100
101
  "docs:tools": "tsx scripts/gen-tool-reference.ts",
101
102
  "docs:check": "tsx scripts/gen-tool-reference.ts --check",
102
103
  "netlogic:sync": "node scripts/sync-netlogic.mjs",
104
+ "resources:build": "node scripts/resources.mjs",
105
+ "resources:check": "node scripts/resources.mjs --check",
103
106
  "build:client": "node scripts/build-client.mjs",
104
107
  "assemble": "node scripts/assemble.mjs"
105
108
  }
@@ -20,6 +20,6 @@ import type { EditOperation } from './operations.ts';
20
20
  * @param maxOperations - Inclusive operations per draft.
21
21
  * @returns Tool definitions in registration order.
22
22
  */
23
- export declare function editTools(session: ViewSession, drafts: EditDrafts, fs: FsService, maxOperations: number): readonly ToolDefinition[];
23
+ export declare function editTools(session: ViewSession, drafts: EditDrafts, fs: FsService, maxOperations: number, profileOf?: (id: string) => unknown): readonly ToolDefinition[];
24
24
  /** A helper kept for callers that want the operation count type. */
25
25
  export type { EditOperation };
@@ -75,13 +75,13 @@ function jsonObject(value, name) {
75
75
  * @param maxOperations - Inclusive operations per draft.
76
76
  * @returns Tool definitions in registration order.
77
77
  */
78
- export function editTools(session, drafts, fs, maxOperations) {
78
+ export function editTools(session, drafts, fs, maxOperations, profileOf) {
79
79
  /** Resolve the view and append one operation. */
80
80
  const stage = (exec, viewId, operation) => {
81
81
  const view = session.require(viewId, exec);
82
82
  return { view, draft: drafts.append(exec, view.id, operation, maxOperations) };
83
83
  };
84
- const staged = (draft, operation) => success(`Draft ${draft.id}: ${draft.operations.length} ${draft.operations.length === 1 ? 'operation' : 'operations'}.`, { draft_id: draft.id, operation_count: draft.operations.length, last_operation: operation }, nextValidate(draft.id), EDIT_COVERAGE);
84
+ const staged = (draft, operation, profileId) => success(`Draft ${draft.id}: ${draft.operations.length} ${draft.operations.length === 1 ? 'operation' : 'operations'}.`, { draft_id: draft.id, operation_count: draft.operations.length, last_operation: operation, ...(profileId === undefined ? {} : { profile_applied: profileId }) }, nextValidate(draft.id), EDIT_COVERAGE);
85
85
  return [
86
86
  tool({
87
87
  name: 'ftview_draft_display_set',
@@ -182,6 +182,7 @@ export function editTools(session, drafts, fs, maxOperations) {
182
182
  back_color: string('Optional background colour. Example: "#C6C6C6".'),
183
183
  display_number: integer('Optional display number.', { minimum: 0 }),
184
184
  file: string('Optional export-relative file path. Defaults to "Displays/<name>.xml".'),
185
+ profile_id: string('Optional house-style profile from engineering_profile; its background colour is used when back_color is absent.'),
185
186
  }, ['name', 'width', 'height']),
186
187
  async run(args, exec) {
187
188
  const view = session.require(optionalString(args, 'view_id'), exec);
@@ -190,7 +191,9 @@ export function editTools(session, drafts, fs, maxOperations) {
190
191
  throw new EngineeringError('COMPONENT_EXISTS', `A display named ${JSON.stringify(name)} already exists in this export.`, 'Pass a new display name.');
191
192
  }
192
193
  const displayType = optionalString(args, 'display_type');
193
- const backColor = optionalString(args, 'back_color');
194
+ const profileId = optionalString(args, 'profile_id');
195
+ const profile = profileId === undefined ? undefined : profileOf?.(profileId);
196
+ const backColor = optionalString(args, 'back_color') ?? profile?.hmi?.palette?.[0]?.value;
194
197
  const displayNumber = optionalInteger(args, 'display_number');
195
198
  const { draft } = stage(exec, optionalString(args, 'view_id'), {
196
199
  kind: 'display_create',
@@ -202,7 +205,7 @@ export function editTools(session, drafts, fs, maxOperations) {
202
205
  ...(backColor === undefined ? {} : { backColor }),
203
206
  ...(displayNumber === undefined ? {} : { displayNumber }),
204
207
  });
205
- return staged(draft, draft.operations[draft.operations.length - 1]);
208
+ return staged(draft, draft.operations[draft.operations.length - 1], profileId);
206
209
  },
207
210
  }),
208
211
  tool({
@@ -218,11 +221,28 @@ export function editTools(session, drafts, fs, maxOperations) {
218
221
  connections_json: string('Optional connections as a JSON object, e.g. {"Value":"SystemRunning"}.'),
219
222
  parent: string('Optional group name to add inside. Omit to add at the display root.'),
220
223
  index: integer('Optional position among siblings, for z-order.', { minimum: 0 }),
224
+ profile_id: string('Optional house-style profile from engineering_profile; its button geometry and font are used when the attributes omit them.'),
221
225
  }, ['display', 'type', 'name']),
222
226
  async run(args, exec) {
223
227
  const view = session.require(optionalString(args, 'view_id'), exec);
224
228
  const display = displayOf(view, requiredString(args, 'display'));
229
+ const type = requiredString(args, 'type');
225
230
  const attributes = { ...jsonObject(args.attributes_json, 'attributes_json') };
231
+ const profileId = optionalString(args, 'profile_id');
232
+ const profile = profileId === undefined ? undefined : profileOf?.(profileId);
233
+ if (profile !== undefined) {
234
+ const cluster = profile.hmi?.button?.[0]?.value;
235
+ if (/button$/iu.test(type) && cluster !== undefined && attributes.width === undefined && attributes.height === undefined) {
236
+ const [width, height] = cluster.split('x');
237
+ if (width !== undefined && height !== undefined) {
238
+ attributes.width = width;
239
+ attributes.height = height;
240
+ }
241
+ }
242
+ const font = profile.hmi?.fonts?.[0]?.value;
243
+ if (font !== undefined && attributes.fontFamily === undefined && (/text$/iu.test(type) || /button$/iu.test(type)))
244
+ attributes.fontFamily = font;
245
+ }
226
246
  const caption = optionalString(args, 'caption');
227
247
  if (caption !== undefined)
228
248
  attributes.caption = caption;
@@ -232,14 +252,14 @@ export function editTools(session, drafts, fs, maxOperations) {
232
252
  const { draft } = stage(exec, optionalString(args, 'view_id'), {
233
253
  kind: 'object_add',
234
254
  file: display.file,
235
- type: requiredString(args, 'type'),
255
+ type,
236
256
  name: requiredString(args, 'name'),
237
257
  attributes,
238
258
  ...(parent === undefined ? {} : { parent }),
239
259
  ...(index === undefined ? {} : { index }),
240
260
  ...(Object.keys(connections).length === 0 ? {} : { connections }),
241
261
  });
242
- return staged(draft, draft.operations[draft.operations.length - 1]);
262
+ return staged(draft, draft.operations[draft.operations.length - 1], profileId);
243
263
  },
244
264
  }),
245
265
  tool({
@@ -30,7 +30,8 @@ export function apply(ctx, config = {}) {
30
30
  if (!Number.isInteger(maxDraftOperations) || maxDraftOperations <= 0) {
31
31
  throw new Error('ftview-edit: maxDraftOperations must be a positive integer');
32
32
  }
33
- for (const definition of editTools(session, new EditDrafts(), stringFs(ctx.fs), maxDraftOperations))
33
+ const profiles = ctx.get?.('engineeringProfiles');
34
+ for (const definition of editTools(session, new EditDrafts(), stringFs(ctx.fs), maxDraftOperations, profiles?.get.bind(profiles)))
34
35
  ctx.tools.register(definition);
35
36
  }
36
37
  export { EditDrafts } from "./edit/drafts.js";
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { type ExecContext, type FsService } from './core.ts';
11
11
  import { type Application } from './model.ts';
12
+ import { type FtviewStyleProfile } from './style.ts';
12
13
  /** One opened export folder. */
13
14
  export interface OpenView {
14
15
  readonly id: string;
@@ -35,6 +36,14 @@ export declare class ViewSession {
35
36
  * @returns The retained view.
36
37
  */
37
38
  open(requestedRoot: string, exec?: ExecContext): Promise<OpenView>;
39
+ /**
40
+ * Read the house style of one open export.
41
+ *
42
+ * @param viewId - Explicit handle, or undefined to use the chat's only view.
43
+ * @param exec - Calling context.
44
+ * @returns The visual profile of the export.
45
+ */
46
+ styleProfile(viewId: string | undefined, exec?: ExecContext): FtviewStyleProfile;
38
47
  /**
39
48
  * Resolve the view a call applies to.
40
49
  *
@@ -10,6 +10,7 @@
10
10
  import { randomUUID } from 'node:crypto';
11
11
  import { EngineeringError } from "./core.js";
12
12
  import { scanApplication } from "./model.js";
13
+ import { styleProfileOf } from "./style.js";
13
14
  /** The scanned export store, keyed by chat. */
14
15
  export class ViewSession {
15
16
  fs;
@@ -57,6 +58,16 @@ export class ViewSession {
57
58
  }
58
59
  return view;
59
60
  }
61
+ /**
62
+ * Read the house style of one open export.
63
+ *
64
+ * @param viewId - Explicit handle, or undefined to use the chat's only view.
65
+ * @param exec - Calling context.
66
+ * @returns The visual profile of the export.
67
+ */
68
+ styleProfile(viewId, exec) {
69
+ return styleProfileOf(this.require(viewId, exec).app);
70
+ }
60
71
  /**
61
72
  * Resolve the view a call applies to.
62
73
  *
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Infer the visual style of one FactoryTalk View ME export.
3
+ *
4
+ * Counts only what the export states: colours, fonts, button geometry,
5
+ * navigation and popup usage. Group screens inherited from another project and
6
+ * a two-display export are reported as low confidence rather than dressed up as
7
+ * a standard.
8
+ *
9
+ * @module dsh-engineering-ftview/style
10
+ */
11
+ import type { Application } from './model.ts';
12
+ /** One counted value. */
13
+ interface Counted {
14
+ readonly value: string;
15
+ readonly count: number;
16
+ }
17
+ /** Evidence for one claim. */
18
+ interface Evidence {
19
+ readonly file?: string;
20
+ readonly count?: number;
21
+ readonly note: string;
22
+ }
23
+ /** The profile, structurally compatible with the resources schema. */
24
+ export interface FtviewStyleProfile {
25
+ readonly schema: 1;
26
+ readonly source: {
27
+ readonly kind: 'ftview';
28
+ readonly path: string;
29
+ readonly sha256: string;
30
+ };
31
+ readonly naming: {
32
+ readonly displays: {
33
+ readonly case: string;
34
+ readonly examples: readonly string[];
35
+ };
36
+ };
37
+ readonly hmi: {
38
+ readonly palette: readonly Counted[];
39
+ readonly fonts: readonly Counted[];
40
+ readonly button: readonly Counted[];
41
+ readonly navigation: {
42
+ readonly displaysWithNav: number;
43
+ readonly displays: number;
44
+ };
45
+ readonly popups: {
46
+ readonly onTop: number;
47
+ readonly replace: number;
48
+ };
49
+ };
50
+ readonly evidence: readonly Evidence[];
51
+ readonly confidence: 'low' | 'medium' | 'high';
52
+ }
53
+ /**
54
+ * Read the style of one export.
55
+ *
56
+ * @param app - The scanned export.
57
+ * @returns The profile.
58
+ */
59
+ export declare function styleProfileOf(app: Application): FtviewStyleProfile;
60
+ export {};
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Infer the visual style of one FactoryTalk View ME export.
3
+ *
4
+ * Counts only what the export states: colours, fonts, button geometry,
5
+ * navigation and popup usage. Group screens inherited from another project and
6
+ * a two-display export are reported as low confidence rather than dressed up as
7
+ * a standard.
8
+ *
9
+ * @module dsh-engineering-ftview/style
10
+ */
11
+ /** The most common value in a counter, with its count. */
12
+ function top(counts) {
13
+ let best;
14
+ for (const [value, count] of counts) {
15
+ if (best === undefined || count > best.count)
16
+ best = { value, count };
17
+ }
18
+ return best;
19
+ }
20
+ /** Normalise a hex colour to lower case with a leading hash. */
21
+ function colour(value) {
22
+ if (value === undefined)
23
+ return undefined;
24
+ const trimmed = value.trim().toLowerCase();
25
+ return /^#[0-9a-f]{3,8}$/u.test(trimmed) ? trimmed : undefined;
26
+ }
27
+ /** Classify one display name's case convention. */
28
+ function nameCase(name) {
29
+ if (/^[A-Z][a-z]+(?: [A-Z][a-z]+)+$/u.test(name))
30
+ return 'Title Case';
31
+ if (/^[A-Z][A-Za-z0-9]*$/u.test(name))
32
+ return 'PascalCase';
33
+ if (/^[a-z0-9]+(?:_[a-z0-9]+)+$/u.test(name))
34
+ return 'snake_case';
35
+ return 'other';
36
+ }
37
+ /**
38
+ * Read the style of one export.
39
+ *
40
+ * @param app - The scanned export.
41
+ * @returns The profile.
42
+ */
43
+ export function styleProfileOf(app) {
44
+ const palette = new Map();
45
+ const fonts = new Map();
46
+ const buttons = new Map();
47
+ const displayCases = new Map();
48
+ let displaysWithNav = 0;
49
+ let onTop = 0;
50
+ let replace = 0;
51
+ const evidence = [];
52
+ for (const display of app.displays) {
53
+ displayCases.set(nameCase(display.name), (displayCases.get(nameCase(display.name)) ?? 0) + 1);
54
+ let gotoButtons = 0;
55
+ for (const object of display.objects) {
56
+ const back = colour(object.attributes.backColor);
57
+ if (back !== undefined)
58
+ palette.set(back, (palette.get(back) ?? 0) + 1);
59
+ const family = object.attributes.fontFamily;
60
+ if (family !== undefined && family.trim() !== '')
61
+ fonts.set(family, (fonts.get(family) ?? 0) + 1);
62
+ if (/button$/iu.test(object.type)) {
63
+ const width = object.attributes.width;
64
+ const height = object.attributes.height;
65
+ if (width !== undefined && height !== undefined) {
66
+ const key = `${width}x${height}`;
67
+ buttons.set(key, (buttons.get(key) ?? 0) + 1);
68
+ }
69
+ }
70
+ if (object.type === 'gotoButton')
71
+ gotoButtons += 1;
72
+ if (object.type === 'displaySettings') {
73
+ const type = object.attributes.displayType;
74
+ if (type === 'onTop')
75
+ onTop += 1;
76
+ if (type === 'replace')
77
+ replace += 1;
78
+ }
79
+ }
80
+ if (gotoButtons >= 3)
81
+ displaysWithNav += 1;
82
+ }
83
+ const back = top(palette);
84
+ const family = top(fonts);
85
+ const button = top(buttons);
86
+ const displayCase = top(displayCases);
87
+ if (back !== undefined)
88
+ evidence.push({ count: back.count, note: `background "${back.value}" used ${back.count} time(s)` });
89
+ if (family !== undefined)
90
+ evidence.push({ count: family.count, note: `font "${family.value}" used ${family.count} time(s)` });
91
+ if (button !== undefined)
92
+ evidence.push({ count: button.count, note: `button geometry ${button.value} used ${button.count} time(s)` });
93
+ evidence.push({ count: app.displays.length, note: `${app.displays.length} display(s) read` });
94
+ const confidence = app.displays.length >= 8 && (back?.count ?? 0) >= 5 ? 'high' : app.displays.length >= 3 ? 'medium' : 'low';
95
+ return {
96
+ schema: 1,
97
+ source: { kind: 'ftview', path: app.root, sha256: '' },
98
+ naming: { displays: { case: displayCase?.value ?? 'Title Case', examples: app.displays.slice(0, 5).map(display => display.name) } },
99
+ hmi: {
100
+ palette: [...palette].sort((left, right) => right[1] - left[1]).slice(0, 6).map(([value, count]) => ({ value, count })),
101
+ fonts: [...fonts].sort((left, right) => right[1] - left[1]).slice(0, 4).map(([value, count]) => ({ value, count })),
102
+ button: [...buttons].sort((left, right) => right[1] - left[1]).slice(0, 4).map(([value, count]) => ({ value, count })),
103
+ navigation: { displaysWithNav, displays: app.displays.length },
104
+ popups: { onTop, replace },
105
+ },
106
+ evidence,
107
+ confidence,
108
+ };
109
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Infer the house style of one L5X project.
3
+ *
4
+ * Every number here is counted from the export, and the shape is deliberately
5
+ * the bundle-wide profile schema (`packages/resources/src/profile.ts`) rather
6
+ * than an L5X-specific one, so the resources row can validate and store it.
7
+ * Nothing is inferred from fewer than a handful of samples, and the confidence
8
+ * field says how much of the project the inference actually saw.
9
+ *
10
+ * @module dsh-engineering-l5x/engine/style
11
+ */
12
+ import type { ProjectSnapshot } from '../session.ts';
13
+ /** One counted value. */
14
+ interface Counted {
15
+ readonly value: string;
16
+ readonly count: number;
17
+ }
18
+ /** Evidence for one claim. */
19
+ interface Evidence {
20
+ readonly file?: string;
21
+ readonly line?: number;
22
+ readonly count?: number;
23
+ readonly note: string;
24
+ }
25
+ /** The profile, structurally compatible with the resources schema. */
26
+ export interface L5xStyleProfile {
27
+ readonly schema: 1;
28
+ readonly source: {
29
+ readonly kind: 'l5x';
30
+ readonly path: string;
31
+ readonly sha256: string;
32
+ };
33
+ readonly naming: {
34
+ readonly tags: {
35
+ readonly prefixes: readonly Counted[];
36
+ readonly case: string;
37
+ };
38
+ readonly routines: {
39
+ readonly case: string;
40
+ readonly examples: readonly string[];
41
+ };
42
+ };
43
+ readonly idioms: {
44
+ readonly sealIn: {
45
+ readonly count: number;
46
+ readonly form: string;
47
+ };
48
+ readonly latch: {
49
+ readonly count: number;
50
+ };
51
+ };
52
+ readonly comments: {
53
+ readonly total: number;
54
+ readonly commented: number;
55
+ readonly ratio: number;
56
+ };
57
+ readonly evidence: readonly Evidence[];
58
+ readonly confidence: 'low' | 'medium' | 'high';
59
+ }
60
+ /**
61
+ * Read the style of one project.
62
+ *
63
+ * @param project - Snapshot the session opened.
64
+ * @returns The profile.
65
+ */
66
+ export declare function styleProfileOf(project: ProjectSnapshot): L5xStyleProfile;
67
+ export {};