PSA files

# Read and convert ProSpace PSA planogram files

The free @vazen/toml package reads ProSpace .psa planogram files from JDA and Blue Yonder Space Planning. It converts them to JSON or Vazen TOML, and writes them back as PSA. It runs without Space Planning.

On this page

- [Is this your file?](#identify)
- [Installation](#installation)
- [Convert PSA to JSON](#psa-to-json)
- [Convert PSA to Vazen TOML](#psa-to-toml)
- [Convert from the command line](#command-line)
- [Write a PSA file](#write)
- [What a conversion keeps](#conversion)
- [Edit PSA records](#records)

## Is this your file?

A ProSpace PSA file is text. It starts with these lines:

```
PROSPACE SCHEMATIC FILE
; Version 2024.4.0
; Codepage=1252
```

The version changes from file to file. Unreal Engine animation files also use the .psa extension. This package does not read them.

## Installation

Install the package and Effect from npm:

```
npm install @vazen/toml effect
```

The package needs Node.js `>=24` and ES modules. Effect `^4.0.0` is a peer dependency. A Python package is planned.

The examples on this page use @vazen/toml 0.4.0 and Node.js 24.

## Convert PSA to JSON

Download the made-up file [v2024.4.0.psa (opens in a new tab)](https://github.com/vazen/toml/blob/main/typescript/src/psa/.test-data/v2024.4.0.psa) and save it as `example.psa`, or run:

```
curl -o example.psa https://raw.githubusercontent.com/vazen/toml/main/typescript/src/psa/.test-data/v2024.4.0.psa
```

Save this script as `convert-to-json.mjs` beside the file. Then run `node convert-to-json.mjs`.

```
import { readFileSync, writeFileSync } from 'node:fs';
import {
  decodeVazenProjectFromPsaFile,
  encodeJsonFileFromVazenProject,
} from '@vazen/toml';

const { project } = decodeVazenProjectFromPsaFile(readFileSync('example.psa'));
writeFileSync(
  'example.vazen.json',
  encodeJsonFileFromVazenProject({ project }),
);
```

The start of `example.vazen.json` is:

```
{
  "schema": "vazen/layout",
  "schema_version": "0.3.0",
  "name": "Fictional Project 5",
  "products": [
    {
      "ref": "p_18jm0l02ylwz1",
      "name": "Product 01",
      "gtin": "5000000000001",
      "dimensions": {
        "width": 65,
        "height": 120,
        "depth": 60
      },
      "_psa__id": "7000001",
```

## Convert PSA to Vazen TOML

Save this script as `convert-to-toml.mjs` beside the file. Then run `node convert-to-toml.mjs`.

```
import { readFileSync, writeFileSync } from 'node:fs';
import {
  decodeVazenProjectFromPsaFile,
  encodeTomlFileFromVazenProject,
} from '@vazen/toml';

const { project, source, messages } = decodeVazenProjectFromPsaFile(
  readFileSync('example.psa'),
);
writeFileSync(
  'example.vazen.toml',
  encodeTomlFileFromVazenProject({ project }),
);
console.log(source);
// Output: { declaredVersion: '2024.4.0', format: 'psa' }
console.log(messages);
// Output: []
```

The start of `example.vazen.toml` is:

```
schema = "vazen/layout"
schema_version = "0.3.0"
name = "Fictional Project 5"
_psa__key = "PRJ-5"
_psa__primary_key = "id"
_psa__case_multiple = 1
_psa__days_supply = 7
_psa__number_of_stores = 12
```

The JSON and the TOML file hold the same tables:

- Each planogram becomes a fixture.
- Each fixture row becomes equipment in that fixture.
- Each position becomes a site.
- Every length is in millimetres.
- A value with a standard key uses that key. Every other value becomes a `_psa__` key on its table, unless the value is the column’s default.

The [specification](/spec/0.3.0) defines each table.

## Convert from the command line

The package's `vazen-toml` command converts a file without a script, and never changes the file. This writes the same `example.vazen.toml`:

```
npx @vazen/toml@0.4.0 convert example.psa --to toml > example.vazen.toml
```

`inspect` lists the plans in a file, and `--plan` converts one of them. The [package README (opens in a new tab)](https://github.com/vazen/toml/blob/main/typescript/README.md#command-line) lists the command's diagnostics and exit codes.

## Write a PSA file

`encodePsaFileFromVazenProject` writes a layout as a PSA file. This script reads `example.vazen.toml` and writes it as PSA:

```
import { readFileSync, writeFileSync } from 'node:fs';
import {
  decodeVazenProjectFromTomlFile,
  encodePsaFileFromVazenProject,
} from '@vazen/toml';

const { project } = decodeVazenProjectFromTomlFile(
  readFileSync('example.vazen.toml', 'utf8'),
);
writeFileSync('example-copy.psa', encodePsaFileFromVazenProject({ project }));
```

The writer writes PSA 2024.4.0. The `version` option names another version that `PsaVersion` lists: 2017.1.0, 2017.2.0, 2019.1.0, 2019.1.1, 2020.1.0, 2020.1.1, 2021.1.2, 2023.1.0, 2024.1.0, 2024.4.0.

```
const bytes = encodePsaFileFromVazenProject({ project, version: '2017.2.0' });
```

The writer refuses a value that a column of that version cannot hold. It writes every file in Windows-1252.

## What a conversion keeps

The reader reads a file as UTF-8 if it is valid UTF-8, and as Windows-1252 if not. Reading makes a best effort and reports any lost values, uncertain interpretations or generated ids in `messages`, beside the project. An empty list means no messages were reported. Each entry identifies the affected record type, rows and column where known:

| Message         | Meaning                                                                                                                             |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `UnlistedWidth` | Rows with a cell count that no listed version has for their type. They are read by position, so a value can be in the wrong column. |
| `DroppedCells`  | Values in cells after the last column of a row. They are dropped.                                                                   |
| `UnreadCells`   | Cells that their column cannot read. They are read as empty.                                                                        |
| `IgnoredRows`   | Rows with no place in `PsaData`, such as a position before the first fixture of its planogram. They are ignored.                    |
| `GeneratedIds`  | Planograms without a key. Each gets an id from its place in the file, such as `planogram-1`, which writing puts in its key.         |

Read a PSA file, then write the project as PSA again. The project can pass through TOML or JSON between the two steps. For a file read with no messages, the new file holds the same values, with these differences:

- A cell that holds its column’s default can become empty, and an empty cell can get the default. The value does not change.
- Text is written in Unicode’s composed form, and U+FFFD is written as `?`.
- A length is rounded to nine decimal places of a millimetre.
- The number format, line endings and row order can change.

So a round trip does not give the same bytes. The [PSA README (opens in a new tab)](https://github.com/vazen/toml/blob/main/typescript/src/psa/README.md) gives every rule for reading and writing.

## Edit PSA records

To edit PSA columns directly, read the records, not a project. `decodePsaDataFromPsaFile` returns `PsaData`, the rows of the file as records, and its `messages`. `encodePsaFileFromPsaData` writes the records as a file. The [API overview](/docs#api) lists every function.

Blue Yonder and .psa are trademarks of their respective owners. Vazen is not affiliated with or endorsed by Blue Yonder. PSA support was built from PSA files that customers supplied or that were published online; column and value names follow ProSpace’s field definitions. No Blue Yonder software was used.
