--- name: ignition-exchange-convert description: Convert an existing Ignition project into a publishable Exchange resource — re-namespacing views/styles/scripts, migrating Perspective session properties to view properties, renaming Python to the guide's conventions — and prove the conversion did not break anything. Use when asked to make a project Exchange-ready, de-brand a project for publication, or refactor one out of session-scoped state. Bundles verify_refs.py and parity.py, which catch the two failure modes a conformance checker cannot see: dangling references and silently dropped features. Not for merely checking conformance (use ignition-exchange-conformance) or uploading (use ignition-exchange-publish). --- # Converting a project into an Exchange resource A conversion is a large mechanical rename plus one genuine refactor. It is **very easy to produce a project that is perfectly named and completely broken** — so the verification gates below are not optional garnish, they are the point. Check the target shape first with [`ignition-exchange-conformance`](../ignition-exchange-conformance/SKILL.md); this skill is what to do about the findings. ## Work on a copy Never convert in place. Copy the project to its new name, leave the original untouched, and diff against it at the end. The original is your parity baseline — without it you cannot prove you did not drop a feature. Ignition project folder name **is** the project name, so the copy's directory must already be the new lowercase-dashed name. ## Order matters Do it in this order. Step 2 invalidates bindings that step 3 would otherwise have to rewrite twice. 1. **Delete what must not ship.** Vision `client-tags/`, scratch views, dev-only resources. 2. **Session properties → view properties**, while paths are still the familiar old ones. Verify the app still works before touching namespaces. 3. **Re-namespace** views, style classes, scripts, page config — one mechanical sweep. 4. **Python pass** — tabs, camelCase, docstrings, loggers. 5. **Content sweep** — branding, internal names, placeholder text. 6. **Verify** (gates below), then package. ## Step 2 is the real refactor Everything else is search-and-replace. This one changes how the app works. The guide forbids session custom properties in an Exchange resource because importing merges them into the host project's session props. Replace with: - **State lives on the root view** as `custom` properties. - **Flows down** to child views as `params` (the guide is explicit: `params` = public/configuration, `custom` = internal). - **Flows up** as page-scoped messages handled on the root view, named `exchange.resourceName.handlerName`. **Bindings must stay reactive.** Do not park binding-read state in `system.util.getGlobals()` — globals do not trigger Perspective binding updates. Globals are only for non-reactive cross-session data, and most resources need none. Before rewriting a binding, check **what it actually depended on**. A bidirectional binding that writes a session prop does not necessarily re-trigger the data fetch; if the fetch keyed off a `refreshToken`, then an explicit Apply button was already the commit point and your message-based version preserves behaviour exactly. Read the dependency graph rather than assuming. ## Jython 2.7 constraints Gateway-scoped code is Jython 2.7. Violations are silent until they reach the gateway. - **No f-strings.** `%` formatting only. - No `typing`, `statistics`, `zoneinfo`. - Java exceptions bypass `except Exception:` — use bare `except:` with `sys.exc_info()` in gateway-facing defensive code. - **Scripts embedded in `view.json`** are JSON strings that must be tab-indented: the first character of every line in the block is a tab. ## Python renames The guide wants camelCase functions and variables (deliberately not PEP-8) and tab indentation. Two traps: - **Rename through the tokenizer, not regex.** A blind replace will corrupt string literals and attribute names that happen to match (`lt.tm_wday`). - **Do not rename data-dictionary keys.** Keys inside dicts returned to view bindings are a wire format between the scripts and every binding that reads them. They are not covered by any rule and renaming them is a large, risky diff for no conformance gain. Tab conversion applies to **indentation** only — bracket continuation lines starting with spaces are fine and normal. Verify with the tokenizer's INDENT tokens, not by grepping for leading spaces. ## Verification gates Run all four. Each catches something the others cannot. ### 1 · Conformance ```bash python3 skills/ignition-exchange-conformance/exchange_lint.py ``` Target: **0 failures**. Warnings are a judgement call. ### 2 · Referential integrity ```bash python3 skills/ignition-exchange-convert/verify_refs.py --old ``` Resolves every embedded view path, style-class reference, `view.params.X` / `view.custom.X`, and every call into the project library against the functions that actually exist. Also flags leftover old-namespace strings, surviving `session.custom` reads, untabbed script transforms, f-strings, and `resource.json` drift. Target: **0 breakages**. With ~40 function renames, a missed call site is the single most likely defect, and nothing else finds it. ### 3 · Feature parity ```bash python3 skills/ignition-exchange-convert/parity.py ``` Fingerprints every view — component types and names, bindings by target property and type, script transforms, event and message handlers, embedded views, declared params — with names normalised for case, punctuation and snake/camel. **A pure rename cancels out; a dropped feature does not.** Target: **zero `LOST` or `MISSING` lines.** `ADDED` lines are expected — the session-props refactor legitimately adds params and handlers. Read the diff, do not just count it. ### 4 · Behaviour If the project has a test suite over its pure modules, run it against the converted library. Point the package name at the new location and alias camelCase functions **and their keyword arguments** back to the original names — a renamed kwarg (`higher_is_worse` → `higherIsWorse`) breaks callers that pass it by name, which no static check catches. Compare against the **original project's** result, not against zero failures. A pre-existing failure staying pre-existing is a pass. ### Then: load it on a gateway Static analysis cannot prove a screen renders. Load the project and open it. A clean project load proves resources parse; only a browser proves components render. ## De-branding Conformance checkers read names, never label text. Sweep user-visible strings explicitly — `props.text`, `title`, `placeholder`, `tooltip` — for company names, project codenames, contest or demo references, internal hostnames, ticket numbers. Strip the text rather than deleting the component, so layout and parity are unchanged. ## Testing a minimum-version claim To claim a floor lower than the gateway you built on, load the project on that version — **against a copy of the files**. An older gateway rewrites every `resource.json` with `lastModification` metadata and leaves root-owned `.resources/` artifacts behind that your user account cannot delete (clean up with a throwaway container rather than sudo). A clean load proves resources parse. It does **not** prove screens render — a component property introduced in a later version fails at render time, not load time. Open the client. ## Reporting Report what the gates actually said, with numbers. A conversion reported accurately with two known gaps is worth far more than one claimed clean. If a gate is a false positive, say which and why — the tooling is not infallible and both bundled scripts have known blind spots documented in their docstrings. ## Provenance Procedure and both scripts derived from a full conversion of a 20-view / 32-style / 3-module Perspective project (2026-09-15), verified on Ignition 8.1.20 and 8.3.7. The gate thresholds are the ones that actually caught defects during it.