@assemblyline/al-control 0.0.0-stage → 0.3.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.
- package/CHANGELOG.md +61 -0
- package/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +102 -2
- package/bin/al-control.js +6 -0
- package/package.json +56 -4
- package/src/cli/_helpers.js +14 -0
- package/src/cli/factories/list.js +56 -0
- package/src/cli/hosts.js +59 -0
- package/src/cli/index.js +288 -0
- package/src/cli/lines/list.js +72 -0
- package/src/cli/lines/show.js +76 -0
- package/src/cli/lines/status.js +124 -0
- package/src/cli/procs/actions.js +95 -0
- package/src/cli/procs/list.js +25 -0
- package/src/cli/stations/status.js +79 -0
- package/src/cli/workpieces/_resolve.js +25 -0
- package/src/cli/workpieces/cat.js +20 -0
- package/src/cli/workpieces/clear.js +121 -0
- package/src/cli/workpieces/count.js +73 -0
- package/src/cli/workpieces/errors.js +79 -0
- package/src/cli/workpieces/list.js +140 -0
- package/src/cli/workpieces/open.js +25 -0
- package/src/cli/workpieces/requeue.js +98 -0
- package/src/cli/workpieces/show.js +126 -0
- package/src/cli/workpieces/watch.js +159 -0
- package/src/config.js +259 -0
- package/src/probe.js +806 -0
- package/src/transport.js +184 -0
- package/src/utils/conveyors.js +50 -0
- package/src/utils/factory-data.js +123 -0
- package/src/utils/failures.js +26 -0
- package/src/utils/format.js +160 -0
- package/src/utils/line-state.js +225 -0
- package/src/utils/watch-render.js +40 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line state — client-side model over what the probe read from a factory.
|
|
3
|
+
*
|
|
4
|
+
* The probe returns raw station defs and bin listings (plain JSON). This
|
|
5
|
+
* module turns them into per-line state and answers:
|
|
6
|
+
* - which lines exist and their station order
|
|
7
|
+
* - how many workpieces sit in each bin of each station
|
|
8
|
+
* - where a given workpiece is right now (most-advanced live bin)
|
|
9
|
+
*
|
|
10
|
+
* Mental model (see fde-handbook/patterns/structural/assembly-line-processing.md):
|
|
11
|
+
* - Live bins (workpiece's current location): input | doing | output | failed
|
|
12
|
+
* - Archive bin (receipt of a successful forward move): done — NOT a position
|
|
13
|
+
* - Position rule: walk stations terminal → source; at each, check
|
|
14
|
+
* output > doing > input > failed; first match wins. done ignored.
|
|
15
|
+
*
|
|
16
|
+
* Station order, terminal and summaries come from ../probe.js (one copy, shared
|
|
17
|
+
* with control-room); this module adds the Map view and workpiece positions.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
ALL_BINS,
|
|
22
|
+
LIVE_BINS,
|
|
23
|
+
codeOf,
|
|
24
|
+
indexStationsByRef,
|
|
25
|
+
dependencyRef,
|
|
26
|
+
resolveStationRef,
|
|
27
|
+
topoSortStations,
|
|
28
|
+
computeTerminal,
|
|
29
|
+
groupLines,
|
|
30
|
+
summarizeCounts,
|
|
31
|
+
} from '../probe.js';
|
|
32
|
+
|
|
33
|
+
export { ALL_BINS, LIVE_BINS, codeOf, indexStationsByRef, dependencyRef, resolveStationRef, topoSortStations, computeTerminal };
|
|
34
|
+
|
|
35
|
+
const LIVE_PRIORITY = ['output', 'doing', 'input', 'failed']; // most-advanced first
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {import('../probe.js').StationDef} StationDef
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {object} LineState
|
|
43
|
+
* @property {string} code - line code (e.g. 'VM')
|
|
44
|
+
* @property {string[]} stations - Station codes in dependency order
|
|
45
|
+
* @property {string} terminal - Sink station of the line
|
|
46
|
+
* @property {Object<string, Object<string, Map<string, string>|null>>} bins -
|
|
47
|
+
* bins[station][bin] is a Map of workpiece_id → path relative to the bin
|
|
48
|
+
* (e.g. 'hi-1', or 'invoices/hi-1__NT-...' under a sub-bin), or null if the
|
|
49
|
+
* bin directory does not exist.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @typedef {object} Position
|
|
54
|
+
* @property {string} line
|
|
55
|
+
* @property {string} station
|
|
56
|
+
* @property {string} bin
|
|
57
|
+
* @property {string} subpath - path relative to the bin
|
|
58
|
+
* @property {boolean} terminal - true if station is the line's terminal
|
|
59
|
+
* @property {boolean} [anomaly] - true when only present in done (not live)
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build per-line state from a probe `state` result.
|
|
64
|
+
* @param {{stations: StationDef[], bins: Object<string, Object<string, Object<string, string>|null>>}} state
|
|
65
|
+
* @returns {Object<string, LineState>} keyed by line code
|
|
66
|
+
*/
|
|
67
|
+
export function buildLineState(state) {
|
|
68
|
+
const lines = {};
|
|
69
|
+
for (const [line_code, line] of Object.entries(groupLines(state.stations))) {
|
|
70
|
+
const bins = {};
|
|
71
|
+
for (const station of line.stations) {
|
|
72
|
+
bins[station] = {};
|
|
73
|
+
for (const bin of ALL_BINS) {
|
|
74
|
+
const listing = state.bins[station]?.[bin];
|
|
75
|
+
bins[station][bin] = listing ? new Map(Object.entries(listing)) : null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
lines[line_code] = { code: line_code, stations: line.stations, terminal: line.terminal, bins };
|
|
79
|
+
}
|
|
80
|
+
return lines;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A workpiece's live position: every line terminal → source, output → doing →
|
|
85
|
+
* input → failed; first match wins. Falls back to a done-only anomaly.
|
|
86
|
+
* @param {string} workpiece_id
|
|
87
|
+
* @param {Object<string, LineState>} lines
|
|
88
|
+
* @returns {Position|null}
|
|
89
|
+
*/
|
|
90
|
+
export function resolvePosition(workpiece_id, lines) {
|
|
91
|
+
for (const line of Object.values(lines)) {
|
|
92
|
+
for (let i = line.stations.length - 1; i >= 0; i--) {
|
|
93
|
+
const station = line.stations[i];
|
|
94
|
+
for (const bin of LIVE_PRIORITY) {
|
|
95
|
+
const map = line.bins[station][bin];
|
|
96
|
+
if (map?.has(workpiece_id)) {
|
|
97
|
+
return { line: line.code, station, bin, subpath: map.get(workpiece_id), terminal: station === line.terminal };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const line of Object.values(lines)) {
|
|
103
|
+
for (let i = line.stations.length - 1; i >= 0; i--) {
|
|
104
|
+
const station = line.stations[i];
|
|
105
|
+
const map = line.bins[station].done;
|
|
106
|
+
if (map?.has(workpiece_id)) {
|
|
107
|
+
return { line: line.code, station, bin: 'done', subpath: map.get(workpiece_id), terminal: station === line.terminal, anomaly: true };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Every workpiece id containing `query`, each resolved to its position.
|
|
116
|
+
* @param {string} query
|
|
117
|
+
* @param {Object<string, LineState>} lines
|
|
118
|
+
* @returns {Map<string, Position>}
|
|
119
|
+
*/
|
|
120
|
+
export function findWorkpieceMatches(query, lines) {
|
|
121
|
+
const ids = new Set();
|
|
122
|
+
for (const line of Object.values(lines)) {
|
|
123
|
+
for (const station of line.stations) {
|
|
124
|
+
for (const bin of ALL_BINS) {
|
|
125
|
+
const map = line.bins[station][bin];
|
|
126
|
+
if (!map) continue;
|
|
127
|
+
for (const id of map.keys()) if (id.includes(query)) ids.add(id);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const matches = new Map();
|
|
132
|
+
for (const id of ids) {
|
|
133
|
+
const pos = resolvePosition(id, lines);
|
|
134
|
+
if (pos) matches.set(id, pos);
|
|
135
|
+
}
|
|
136
|
+
return matches;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Resolve a user-typed id: an exact id wins, else a substring matching
|
|
141
|
+
* exactly one workpiece.
|
|
142
|
+
* @param {string} query
|
|
143
|
+
* @param {Object<string, LineState>} lines
|
|
144
|
+
* @returns {{id: string, pos: Position} | {ambiguous: string[]} | null}
|
|
145
|
+
*/
|
|
146
|
+
export function resolveWorkpieceQuery(query, lines) {
|
|
147
|
+
const matches = findWorkpieceMatches(query, lines);
|
|
148
|
+
if (matches.has(query)) return { id: query, pos: matches.get(query) };
|
|
149
|
+
if (matches.size > 1) return { ambiguous: [...matches.keys()].sort() };
|
|
150
|
+
const [only] = matches;
|
|
151
|
+
return only ? { id: only[0], pos: only[1] } : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Every workpiece id in a bin spec: `STATION/BIN` or `STATION/BIN/SUBPATH`.
|
|
156
|
+
* @param {string} bin_spec
|
|
157
|
+
* @param {Object<string, LineState>} lines
|
|
158
|
+
* @returns {{ok: true, ids: string[]} | {ok: false, error: string}}
|
|
159
|
+
*/
|
|
160
|
+
export function collectIdsForBin(bin_spec, lines) {
|
|
161
|
+
const m = bin_spec.match(/^([^/]+)\/(input|doing|output|done|failed)(?:\/(.+))?$/);
|
|
162
|
+
if (!m) {
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
error: `Invalid bin spec "${bin_spec}". Use STATION/BIN or STATION/BIN/SUBPATH (e.g. AP3b/failed, HI3/output/invoices).`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const [, station, bin, subpath_filter] = m;
|
|
169
|
+
const line_found = Object.values(lines).find((line) => line.stations.includes(station));
|
|
170
|
+
if (!line_found) return { ok: false, error: `Station "${station}" not found.` };
|
|
171
|
+
const map = line_found.bins[station][bin];
|
|
172
|
+
if (!map) return { ok: true, ids: [] };
|
|
173
|
+
if (!subpath_filter) return { ok: true, ids: [...map.keys()] };
|
|
174
|
+
const filter_prefix = subpath_filter.replace(/\/+$/, '') + '/';
|
|
175
|
+
const ids = [];
|
|
176
|
+
for (const [id, sub] of map) {
|
|
177
|
+
if (sub === subpath_filter || sub.startsWith(filter_prefix)) ids.push(id);
|
|
178
|
+
}
|
|
179
|
+
return { ok: true, ids };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Every workpiece id on a line, across all bins.
|
|
184
|
+
* @param {LineState} line
|
|
185
|
+
* @returns {string[]}
|
|
186
|
+
*/
|
|
187
|
+
export function idsOnLine(line) {
|
|
188
|
+
const set = new Set();
|
|
189
|
+
for (const station of line.stations) {
|
|
190
|
+
for (const bin of ALL_BINS) {
|
|
191
|
+
const map = line.bins[station][bin];
|
|
192
|
+
if (map) for (const id of map.keys()) set.add(id);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return [...set];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Workpiece dir relative to temp/stations/ — the form the probe's `logs`
|
|
200
|
+
* query takes.
|
|
201
|
+
* @param {Position} pos
|
|
202
|
+
* @param {string} workpiece_id
|
|
203
|
+
*/
|
|
204
|
+
export function workpieceRelDir(pos, workpiece_id) {
|
|
205
|
+
return `${pos.station}/${pos.bin}/${pos.subpath || workpiece_id}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Per-line totals from live bins only (`done` ignored).
|
|
210
|
+
* - in_flight = input + doing + output, excluding terminal output
|
|
211
|
+
* - stuck = failed across the line
|
|
212
|
+
* - finished = terminal-station output
|
|
213
|
+
* @param {LineState} line
|
|
214
|
+
*/
|
|
215
|
+
export function summarizeLine(line) {
|
|
216
|
+
const counts = {};
|
|
217
|
+
for (const station of line.stations) {
|
|
218
|
+
counts[station] = {};
|
|
219
|
+
for (const bin of LIVE_BINS) {
|
|
220
|
+
const map = line.bins[station][bin];
|
|
221
|
+
counts[station][bin] = map ? map.size : null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return summarizeCounts(line, counts);
|
|
225
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch-mode renderer for snapshot commands (e.g. `al-control lines status --watch`).
|
|
3
|
+
*
|
|
4
|
+
* Renders in place on the main screen. Each tick sends:
|
|
5
|
+
* `\x1b[2J` — erase the visible display
|
|
6
|
+
* `\x1b[3J` — erase the scrollback / "saved lines" (xterm DECSED 3)
|
|
7
|
+
* `\x1b[H` — home cursor to (1,1)
|
|
8
|
+
*
|
|
9
|
+
* The scrollback erase is the key bit: when a frame is taller than the
|
|
10
|
+
* terminal viewport, the part that overflows scrolls into scrollback. Without
|
|
11
|
+
* `\x1b[3J`, tops of past frames pile up across ticks. With it, scrollback is
|
|
12
|
+
* purged each tick so only the current frame is ever visible.
|
|
13
|
+
*
|
|
14
|
+
* Side effect: any pre-existing scrollback above the command is also wiped
|
|
15
|
+
* each tick. That's the trade-off for staying on the main screen instead of
|
|
16
|
+
* using the alternate-screen buffer (which would hide the last frame on exit).
|
|
17
|
+
*
|
|
18
|
+
* If stdout isn't a TTY (e.g. piped to a file), falls back to plain append.
|
|
19
|
+
*
|
|
20
|
+
* `render` is invoked once per tick and may be async. Errors are not caught —
|
|
21
|
+
* the loop is meant to halt on programmer mistakes, not silently spin.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_WATCH_INTERVAL_SECS = 1;
|
|
25
|
+
|
|
26
|
+
const CLEAR_AND_HOME = '\x1b[2J\x1b[3J\x1b[H';
|
|
27
|
+
|
|
28
|
+
export async function watchRender({ render, interval_secs = DEFAULT_WATCH_INTERVAL_SECS }) {
|
|
29
|
+
process.on('SIGINT', () => { process.stdout.write('\n'); process.exit(0); });
|
|
30
|
+
let first = true;
|
|
31
|
+
while (true) {
|
|
32
|
+
if (process.stdout.isTTY && !first) {
|
|
33
|
+
process.stdout.write(CLEAR_AND_HOME);
|
|
34
|
+
}
|
|
35
|
+
first = false;
|
|
36
|
+
await render();
|
|
37
|
+
console.log(`\nupdated ${new Date().toLocaleTimeString()} — refresh every ${interval_secs}s (Ctrl-C to stop)`);
|
|
38
|
+
await new Promise((r) => setTimeout(r, interval_secs * 1000));
|
|
39
|
+
}
|
|
40
|
+
}
|