PLC Fleet Archive

Git-backed Product Data Management (PDM) for PSI’s fleet of Allen-Bradley PLC programs. Automated version control, change tracking, and attribution for 489+ Logix 5000 projects — essentially “Git for PLC code.”

Single fleet analyzer as of 2026-06-23 (ADR-001)

This repo is now the single analyzer for the whole PLC fleet. The analysis toolkit (l5x_parser.js, acd_analyzer.py, compute-scorecards.js, generate-fleet-summaries.js) is vendored in-repo at scripts/analysis/, and the nightly job produces analysis.json + scorecard.json + summary.json for every project. psi-plc-runner is conversion-only and no longer analyzes. The MCP-dir sync uses robocopy /E (never deletes). See docs/ADR-001-single-analyzer.md in the repo.


Overview

Rockwell Automation’s ACD files have no built-in version control, no change history, and no author attribution. Engineers save ACD files to the K: drive and there’s no record of who changed what or when.

The PLC Fleet Archive solves this by creating a git-backed repository that automatically:

  1. Scans K: drive nightly for new or modified ACD files (via MD5 hash comparison)
  2. Analyzes each changed program (modules, tags, programs, routines, I/O mapping)
  3. Diffs against the previous version to detect specific structural changes
  4. Attributes changes (NTFS file owner, assigned controls engineer, timestamps)
  5. Commits everything with structured git history
PropertyValue
RepositoryProgressiveSurface/plc-fleet-archive
Projects archived88 (from L5X analysis), expanding to 489+ via ACD analyzer
Total rungs tracked100,226
Total I/O modules1,637
Nightly scan11pm ET via GitHub Actions on PS-PROXY
Analysis engineacd_analyzer.py (100% coverage) + l5x_parser.js (richer L5X data)

How It Works

K: Drive (engineers save ACD files as usual)
  │
  ├─ scan-fleet.ps1 (nightly, detect changes via MD5)
  │     ↓
  ├─ compute-attribution.ps1 (NTFS owner + AFTEC engineer)
  │     ↓
  ├─ analyze-project.ps1 (acd_analyzer.py or l5x_parser.js)
  │     ↓
  ├─ diff-analysis.js (structural diff: tags, modules, rungs)
  │     ↓
  ├─ commit-changes.ps1 (git commit with attribution)
  │     ↓
  └─ MCP Server → "Ask the Fleet" chatbot + PSI Explorer

Engineers don’t change their workflow. They save ACD files to K: drive exactly as they always have. The archive detects and tracks changes automatically.


Attribution: “Who Changed It?”

ACD files do NOT store editor identity. The archive layers multiple sources to determine attribution:

LayerSourceConfidenceAlways Available?
1NTFS file owner (Get-Acl)MediumYes
2ACD header save timestampsLow (no user)Yes
3AFTEC CONT.ENGR (assigned controls engineer)MediumYes — via UniData API
4Windows Security Event Log (Event ID 4663)HighIf NTFS auditing enabled
5Manual override (git notes)ConfirmedOn-demand

Each change record stores:

{
  "date": "2026-02-28T23:15:00-05:00",
  "acdHash": "a1b2c3d4...",
  "attribution": {
    "confidence": "assigned",
    "ntfsOwner": "PTI_DOMAIN\\jsmith",
    "controlsEngineer": "John Smith"
  },
  "changes": {
    "summary": "3 tags added, +5 rungs in Pressure_Control"
  }
}

Structural Diffing

The archive doesn’t just detect that a file changed — it tells you what changed inside the PLC program:

  • Tags: Added, removed, data type changed, description changed
  • Rungs: Count changes per routine (e.g., “+5 rungs in Pressure_Control”)
  • Modules: I/O modules added, removed, slot changed, RPI changed
  • Programs: Programs/routines added or removed
  • Controller: Firmware version, processor type, security code changes
  • Data Types: UDTs added or removed

Example diff output:

1 tag(s) added, 1 tag(s) removed, +5 rungs in Pressure_Control, firmware 34.11 → 34.12

Repository Structure

plc-fleet-archive/
  manifest.json                      # Fleet index: all projects + latest state
  config/fleet-config.json           # Paths, schedules, thresholds
  projects/
    {job}/
      analysis.json                  # Latest PLC analysis (unified schema)
      metadata.json                  # ACD file metadata + attribution
      history.json                   # Array of change records with diffs
  scripts/
    scan-fleet.ps1                   # K: drive change detector
    compute-attribution.ps1          # NTFS + AFTEC attribution engine
    analyze-project.ps1              # Analysis wrapper (ACD + L5X)
    diff-analysis.js                 # Structural JSON differ
    commit-changes.ps1               # Git commit with structured messages
    generate-report.js               # Nightly change report
    seed-archive.js                  # One-time: populate from existing data
  reports/
    nightly/{YYYY-MM-DD}.json        # Daily change reports
    fleet-summary.json               # 90-day scan history
  .github/workflows/
    nightly-scan.yml                 # Scheduled GitHub Actions pipeline

Nightly Pipeline

The GitHub Actions workflow runs on the [self-hosted, ps-proxy] runner at 11pm ET:

  1. Scan — Compare K: drive ACD file MD5 hashes against manifest.json
  2. Attribute — Extract NTFS metadata, query AFTEC for assigned controls engineer
  3. Analyze — Run acd_analyzer.py (always) + l5x_parser.js (if L5X exists)
  4. Diff — Compare new analysis against previous version
  5. Commit — Update project files, history, manifest; git commit with structured message
  6. Report — Generate nightly change report + update fleet summary
  7. Push — Push to GHE

Typical runtime: ~5 min for change detection, ~30-60 sec per changed project.


MCP Server Integration

The archive data is exposed through 5 MCP tools deployed on PS-PROXY:3100, alongside the existing 37 machine intelligence tools. The get_plc_analysis tool now checks the archive first (preferred source), falling back to legacy flat files and on-demand ACD analysis.

ToolPurposeExample
get_plc_changesChange history for a project (what changed and when)“What changed in 2399’s PLC code this month?”
get_plc_historyVersion timeline with attribution (who changed it)“Who last modified the PLC program for project 2386?”
search_fleet_tagsCross-fleet tag/module/program search”Which projects use module 1769-IQ16/A?” (found in 55 projects)
compare_plc_versionsSide-by-side structural diff between projects”Compare the PLC programs of 2326 and 2327”
get_fleet_summaryAggregate fleet statistics (overview, modules, changes, processors)“How many projects are in the archive?”

The archive data lives at C:\Services\plc-fleet-archive on PS-PROXY, synced automatically after each nightly scan.


Complexity Analysis Pipeline

A batch complexity scorer runs against the entire ACD fleet to produce per-project scores and a historical trend.

ScriptPurpose
batch-complexity-scan.pyScans all 489 ACDs, extracts 7 sub-metrics via acd-tools, outputs fleet_complexity_scores.csv
build-complexity-trend.jsJoins scores to comprehensive_dataset.csv, outputs yearly trend and per-project joined CSV

Run: C:/Python311/python.exe batch-complexity-scan.py --resume (supports incremental resume; ~4–8 hrs full fleet)

Output files (C:\git\PLC\):

FileRowsContents
fleet_complexity_scores.csv487Job, Complexity (0–100), TotalRungs, TagCount, UdtDepth, RoutineCount, AvgRungs, JsrDepth, TimerDensity
fleet_complexity_trend.csv26 yearsShipYear, N, Mean, Median, Min, Max, P25, P75
fleet_complexity_joined.csv451Per-project scores joined to lead time dataset (ShipYear, Customer, MachineType)

ACD version handling:

  • v29+ (109 projects): Full extraction — all 7 sub-metrics including tags/UDTs via TagInfo.XML
  • v12–v20 (378 projects): Fallback path via partial sqlite DB — 5 sub-metrics (tagCount and udtDepth unavailable)

Key finding (2026-03-03): Median complexity was stable at 32–35 from 2010–2022, then surged: 37 (2022) → 41 (2024) → 48 (2025). P75 jumped from 37 to 55. The top quarter of current projects are 50% more complex than anything built pre-2022.

See Data Brain §1.7 for full schema and analysis record.


Interactive Workbench (render-ladder.py)

Added 2026-05-28. Renders any archived project (or an L5X file directly) as a self-contained HTML ladder-logic workbench:

  • Every tag is clickable → side panel shows description, data type, scope, alias chain, and every rung that uses it (split into input/output)
  • “Trace this output” walks back recursively through driving rungs to answer “why isn’t this on?”
  • JSR routine refs are clickable jumps; each routine shows “Called from” backlinks
  • Full-text search across tags, rung comments, and routine names (/ to focus)
  • Diff mode: --compare-with <previous-analysis.json> highlights added/modified/removed rungs and surfaces the prior text on each modified rung — the natural PR-review surface for PLC changes
# From a refreshed archive entry
python scripts/render-ladder.py --analysis projects/2353/analysis.json --out 2353.html
 
# Diff against the previous git revision
git show HEAD~1:projects/2353/analysis.json > /tmp/prev.json
python scripts/render-ladder.py --analysis projects/2353/analysis.json \
                                --compare-with /tmp/prev.json \
                                --out 2353-diff.html

Schema 1.1 — descriptions and comments restored (2026-05-28)

The 2026-02-28 seed used an older l5x_parser.js whose UDT member extractor missed ~25% of visible bit members (regex didn’t match across nested attribute orderings) and never emitted AliasFor. The downstream effect was that archived analysis.json files were structurally complete but stripped of every human-readable description and rung comment — the most valuable engineering knowledge in the L5X.

Fixed in this round:

  1. Rewrote the UDT member extractor in l5x_parser.js (two-pass walk over <Member …> start tags + their matching </Member> closes; the regex-only approach was inherently fragile).
  2. Added AliasFor capture on tags.
  3. Re-ran refresh-analysis.py across all 83 archived projects with an L5X on K:.

Result for proj2353 alone: 22 → 30 visible FLT members captured, 438 → 518 member descriptions, 54 rung comments restored. Across the fleet: tens of thousands of recovered descriptions/comments.

analysis.json now carries:

FieldSource
controllerTags[].description, …aliasForparser v2
programs[].tags[].description, …aliasForparser v2
dataTypes[].descriptionparser v2
dataTypes[].members[].descriptionparser v2 (fixed)
programs[].routines[].rungs[].commentparser v2
  • plc-fleet-catalog — Fleet inventory (all 1,391 projects, all platforms)
  • l5x-tools — ACD/L5X conversion toolchain
  • mcp-server — MCP Server (42 tools, including PLC analysis and fleet archive)
  • psi-explorer — PSI Explorer with “Ask the Fleet” chatbot