@workingdevshero/olympia-ait-adapter 0.1.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.
Files changed (51) hide show
  1. package/README.md +208 -0
  2. package/dist/index.d.ts +5 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +8 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/reader/parse-day-sheet.d.ts +10 -0
  7. package/dist/reader/parse-day-sheet.d.ts.map +1 -0
  8. package/dist/reader/parse-day-sheet.js +199 -0
  9. package/dist/reader/parse-day-sheet.js.map +1 -0
  10. package/dist/reader/parse-workbook.d.ts +3 -0
  11. package/dist/reader/parse-workbook.d.ts.map +1 -0
  12. package/dist/reader/parse-workbook.js +55 -0
  13. package/dist/reader/parse-workbook.js.map +1 -0
  14. package/dist/schema/program.d.ts +692 -0
  15. package/dist/schema/program.d.ts.map +1 -0
  16. package/dist/schema/program.js +125 -0
  17. package/dist/schema/program.js.map +1 -0
  18. package/dist/server.d.ts +4 -0
  19. package/dist/server.d.ts.map +1 -0
  20. package/dist/server.js +58 -0
  21. package/dist/server.js.map +1 -0
  22. package/dist/tools/json-to-xlsx.d.ts +3 -0
  23. package/dist/tools/json-to-xlsx.d.ts.map +1 -0
  24. package/dist/tools/json-to-xlsx.js +5 -0
  25. package/dist/tools/json-to-xlsx.js.map +1 -0
  26. package/dist/tools/xlsx-to-json.d.ts +3 -0
  27. package/dist/tools/xlsx-to-json.d.ts.map +1 -0
  28. package/dist/tools/xlsx-to-json.js +5 -0
  29. package/dist/tools/xlsx-to-json.js.map +1 -0
  30. package/dist/writer/build-workbook.d.ts +3 -0
  31. package/dist/writer/build-workbook.d.ts.map +1 -0
  32. package/dist/writer/build-workbook.js +166 -0
  33. package/dist/writer/build-workbook.js.map +1 -0
  34. package/dist/writer/layout.d.ts +21 -0
  35. package/dist/writer/layout.d.ts.map +1 -0
  36. package/dist/writer/layout.js +40 -0
  37. package/dist/writer/layout.js.map +1 -0
  38. package/dist/writer/stamp-exercise.d.ts +31 -0
  39. package/dist/writer/stamp-exercise.d.ts.map +1 -0
  40. package/dist/writer/stamp-exercise.js +80 -0
  41. package/dist/writer/stamp-exercise.js.map +1 -0
  42. package/dist/writer/styles.d.ts +59 -0
  43. package/dist/writer/styles.d.ts.map +1 -0
  44. package/dist/writer/styles.js +266 -0
  45. package/dist/writer/styles.js.map +1 -0
  46. package/dist/writer/supplemental-template.d.ts +23 -0
  47. package/dist/writer/supplemental-template.d.ts.map +1 -0
  48. package/dist/writer/supplemental-template.js +112 -0
  49. package/dist/writer/supplemental-template.js.map +1 -0
  50. package/package.json +42 -0
  51. package/src/assets/olympia-logo.png +0 -0
package/README.md ADDED
@@ -0,0 +1,208 @@
1
+ <p align="center">
2
+ <img src="src/assets/olympia-logo.png" alt="Olympia Fitness & Performance RI" width="400">
3
+ </p>
4
+
5
+ # olympia-ait-adapter
6
+
7
+ MCP adapter that converts Olympia Fitness & Performance program XLSX workbooks
8
+ to and from a strict JSON schema. Built for the
9
+ [Automate It](https://automate.it.com) framework so AI agents can author new
10
+ strength-and-conditioning programs from prior client data without ever
11
+ touching XLSX directly.
12
+
13
+ ## Why this exists
14
+
15
+ Olympia's coaches build client programs in XLSX workbooks — one client per
16
+ file, one sheet per training day, with merged cells, custom borders, an
17
+ embedded logo, and a domain-specific notation (`12E`, `:20E`, `12EL`,
18
+ `x3 Rest :60`, etc.). Asking an LLM to edit XLSX directly is fragile: it
19
+ corrupts merges, loses the logo, and can't reason about the implicit
20
+ 6-week / 1A-1B / set-by-set structure encoded in the visual layout.
21
+
22
+ This package gives the agent a clean structured view of an Olympia program
23
+ and guarantees well-formed output: the LLM reads JSON, reasons about it,
24
+ writes JSON, and the adapter handles all the visual fidelity on the way
25
+ back to XLSX.
26
+
27
+ ## What it does
28
+
29
+ Two MCP tools, exposed over stdio:
30
+
31
+ | Tool | Direction | Purpose |
32
+ | ------------------------ | --------------- | ------------------------------------------------------------------ |
33
+ | `olympia_xlsx_to_json` | XLSX → JSON | Parse an Olympia workbook into a typed `Program` document. |
34
+ | `olympia_json_to_xlsx` | JSON → XLSX | Render a `Program` document back to a styled .xlsx on disk. |
35
+
36
+ The JSON schema is defined once in `src/schema/program.ts` using Zod and
37
+ surfaces as:
38
+
39
+ 1. The TypeScript type used internally
40
+ 2. The runtime validator at each tool boundary
41
+ 3. The JSON Schema published as each tool's `inputSchema` so the LLM sees
42
+ the full typed contract at discovery time
43
+
44
+ ## JSON shape
45
+
46
+ ```ts
47
+ type Program = {
48
+ client: string; // "Bobby Galli"
49
+ programDate: string; // "4.26"
50
+ weekDates?: (string | null)[]; // optional, length 6
51
+ days: Day[];
52
+ };
53
+
54
+ type Day = {
55
+ name: string; // "Day 1"
56
+ sections: Section[];
57
+ supplementalNotes?: SupplementalNote[];
58
+ };
59
+
60
+ type Section = {
61
+ kind: "warmup" | "main" | "sportSpecific" | "supplemental" | "conditioning";
62
+ exercises: Exercise[];
63
+ };
64
+
65
+ type Exercise = {
66
+ name: string; // "2DB Alternating Bench Press"
67
+ groupLabel?: string; // "1A", "1B", "2A"… matches /^\d+[A-Z]$/
68
+ notes?: string;
69
+ sets: (string | null)[][]; // [setIdx][weekIdx]; inner length 6
70
+ };
71
+
72
+ type SupplementalNote = {
73
+ row: number; // offset from "Sport Specific/" label row
74
+ col: string; // "C" | "D" | … | "P"
75
+ value: string;
76
+ };
77
+ ```
78
+
79
+ ### Prescription cell grammar
80
+
81
+ Olympia does **not** prescribe weights. Each `sets[setIdx][weekIdx]` is a
82
+ single opaque string encoding **reps**, **time**, or **breaths**:
83
+
84
+ | Format | Meaning |
85
+ | --------------------- | ------------------------------------ |
86
+ | `"12"` / `"10E"` | reps (`E` = each side) |
87
+ | `":20"` / `":20E"` | seconds (`E` = each side) |
88
+ | `"5 breaths"` | breath count |
89
+ | `"12EL"` / `"10ER"` | reps each side, left or right lead |
90
+ | free-form strings | circuit-style instructions, OK too |
91
+ | `null` | empty cell |
92
+
93
+ The schema enforces shape (6 weeks per set, group label regex, allowed
94
+ section kinds) but does **not** validate the grammar of each prescription
95
+ string — Olympia's own files include free-form entries in some spots.
96
+
97
+ ### Supplemental area
98
+
99
+ The bottom of every day sheet is a trainer-managed "Sport Specific /
100
+ Supplemental" area where coaches write athlete-specific instructions that
101
+ don't fit the structured exercise table (conditioning circuits, sport
102
+ drills, recovery work). The adapter:
103
+
104
+ - **Reads** that area as a sparse list of `{ row, col, value }` entries
105
+ (master cells only) so the signal is preserved for downstream agents.
106
+ - **Writes** the fixed 10-row template (with "Sport Specific/" and
107
+ "Supplemental" labels) at the bottom of every day, then layers any
108
+ supplied `supplementalNotes` on top.
109
+
110
+ ## Installation
111
+
112
+ ```bash
113
+ npm install
114
+ npm run build
115
+ ```
116
+
117
+ The built CLI lives at `dist/server.js`; the `bin` entry registers it as
118
+ `olympia-ait-adapter`.
119
+
120
+ ## Running the MCP server
121
+
122
+ ```bash
123
+ # Dev (TypeScript via tsx)
124
+ npm run dev
125
+
126
+ # Built (after `npm run build`)
127
+ npm start
128
+ # or
129
+ npx olympia-ait-adapter
130
+ ```
131
+
132
+ The server speaks the Model Context Protocol over stdio. To wire it into
133
+ Claude or another MCP client, add an entry like:
134
+
135
+ ```json
136
+ {
137
+ "mcpServers": {
138
+ "olympia-ait-adapter": {
139
+ "command": "npx",
140
+ "args": ["olympia-ait-adapter"]
141
+ }
142
+ }
143
+ }
144
+ ```
145
+
146
+ Both tools take absolute paths to .xlsx files on the local filesystem.
147
+
148
+ ## Repo layout
149
+
150
+ ```
151
+ src/
152
+ schema/program.ts # Zod schema — single source of truth
153
+ reader/
154
+ parse-workbook.ts # Workbook → Program JSON
155
+ parse-day-sheet.ts # One sheet, section detection, supplemental capture
156
+ writer/
157
+ build-workbook.ts # Program → ExcelJS workbook
158
+ layout.ts # Column widths, row positions, logo anchor
159
+ styles.ts # All borders/fonts/fills as constants
160
+ stamp-exercise.ts # One exercise block (variable sets, group borders)
161
+ supplemental-template.ts # Fixed bottom template + supplementalNotes overlay
162
+ tools/
163
+ xlsx-to-json.ts # MCP handler
164
+ json-to-xlsx.ts # MCP handler
165
+ server.ts # Bootstrap, registers both tools
166
+ assets/
167
+ olympia-logo.png # Extracted from sample workbooks; embedded in output
168
+
169
+ samples/ # Real Olympia .xlsx files used as round-trip fixtures
170
+ scripts/ # Inspection helpers used during development
171
+ tests/ # vitest — schema, reader, round-trip, MCP smoke
172
+ ```
173
+
174
+ ## Tests
175
+
176
+ ```bash
177
+ npm test # one-shot
178
+ npm run test:watch # vitest watch mode
179
+ npm run typecheck # tsc --noEmit
180
+ ```
181
+
182
+ Coverage:
183
+
184
+ - **`schema.test.ts`** — Zod rejects malformed input (week count, group
185
+ label regex, section kinds, missing fields).
186
+ - **`reader.test.ts`** — All three sample workbooks parse into
187
+ schema-valid `Program` documents; spot checks on Bobby's first exercise,
188
+ Giulianna's variable set counts, exercise notes, supplemental notes.
189
+ - **`round-trip.test.ts`** — The primary determinism guarantee: for each
190
+ sample, `parse → write → parse` produces JSON equal to the original.
191
+ - **`mcp-smoke.test.ts`** — End-to-end MCP server check via the in-memory
192
+ transport: tool discovery, round-trip via `client.callTool`, schema
193
+ rejection of malformed input at the SDK boundary.
194
+
195
+ ## Determinism
196
+
197
+ The writer is fully programmatic — no template `.xlsx` file. Every byte of
198
+ the output is determined by code plus the input JSON, so the same JSON
199
+ produces the same workbook. The Olympia logo is the only binary asset
200
+ (`src/assets/olympia-logo.png`).
201
+
202
+ This trade-off is intentional: templates introduce hidden state that
203
+ ExcelJS occasionally drops (drawings, theme colors), whereas pure code →
204
+ output gives byte-stable, snapshot-testable results.
205
+
206
+ ## License
207
+
208
+ Internal — Automate It / Olympia Fitness & Performance RI.
@@ -0,0 +1,5 @@
1
+ export { Program, Day, Section, SectionKind, Exercise, SetRow, Cell, SupplementalNote, XlsxToJsonInput, XlsxToJsonOutput, JsonToXlsxInput, JsonToXlsxOutput, } from "./schema/program.js";
2
+ export { createServer } from "./server.js";
3
+ export { parseWorkbook } from "./reader/parse-workbook.js";
4
+ export { buildWorkbook } from "./writer/build-workbook.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,OAAO,EACP,GAAG,EACH,OAAO,EACP,WAAW,EACX,QAAQ,EACR,MAAM,EACN,IAAI,EACJ,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // Programmatic entry point. The package's primary use is the MCP CLI
2
+ // (registered under the `bin` field), but the schema and server factory
3
+ // are also exported for callers that want to embed or compose the adapter.
4
+ export { Program, Day, Section, SectionKind, Exercise, SetRow, Cell, SupplementalNote, XlsxToJsonInput, XlsxToJsonOutput, JsonToXlsxInput, JsonToXlsxOutput, } from "./schema/program.js";
5
+ export { createServer } from "./server.js";
6
+ export { parseWorkbook } from "./reader/parse-workbook.js";
7
+ export { buildWorkbook } from "./writer/build-workbook.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,wEAAwE;AACxE,2EAA2E;AAE3E,OAAO,EACL,OAAO,EACP,GAAG,EACH,OAAO,EACP,WAAW,EACX,QAAQ,EACR,MAAM,EACN,IAAI,EACJ,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC"}
@@ -0,0 +1,10 @@
1
+ import ExcelJS from "exceljs";
2
+ import type { SupplementalNote, Day } from "../schema/program.js";
3
+ export interface ParsedDay {
4
+ sections: Day["sections"];
5
+ weekDates?: (string | null)[];
6
+ supplementalNotes?: SupplementalNote[];
7
+ warnings: string[];
8
+ }
9
+ export declare function parseDaySheet(ws: ExcelJS.Worksheet): ParsedDay;
10
+ //# sourceMappingURL=parse-day-sheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-day-sheet.d.ts","sourceRoot":"","sources":["../../src/reader/parse-day-sheet.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,EACV,gBAAgB,EAEhB,GAAG,EAIJ,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;IAC1B,SAAS,CAAC,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAC9B,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACvC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAmFD,wBAAgB,aAAa,CAAC,EAAE,EAAE,OAAO,CAAC,SAAS,GAAG,SAAS,CAsG9D"}
@@ -0,0 +1,199 @@
1
+ const FIRST_BODY_ROW = 7;
2
+ const WEEK_COLS = ["D", "F", "H", "J", "L", "N"];
3
+ const SECTION_LABELS = new Map([
4
+ ["warmup", "warmup"],
5
+ ["sport specific", "sportSpecific"],
6
+ ["sport specific/", "sportSpecific"],
7
+ ["supplemental", "supplemental"],
8
+ ["conditioning", "conditioning"],
9
+ ]);
10
+ const SUPPLEMENTAL_TRIGGER_LABELS = new Set([
11
+ "sport specific",
12
+ "sport specific/",
13
+ "supplemental",
14
+ "conditioning",
15
+ "circuit",
16
+ ]);
17
+ // Columns scanned for supplementalNotes. Excludes A/B (cosmetic narrow margins)
18
+ // and Q (rarely used).
19
+ const SUPPLEMENTAL_COLS = ["C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P"];
20
+ function isGroupLabel(s) {
21
+ return /^\d+[A-Z]$/.test(s);
22
+ }
23
+ function cellText(value) {
24
+ if (value === null || value === undefined)
25
+ return "";
26
+ if (typeof value === "string")
27
+ return value;
28
+ if (typeof value === "number" || typeof value === "boolean")
29
+ return String(value);
30
+ if (value instanceof Date)
31
+ return value.toISOString();
32
+ if (typeof value === "object") {
33
+ if ("richText" in value && Array.isArray(value.richText)) {
34
+ return value.richText
35
+ .map((r) => r.text)
36
+ .join("");
37
+ }
38
+ if ("text" in value && typeof value.text === "string") {
39
+ return value.text;
40
+ }
41
+ if ("result" in value) {
42
+ return cellText(value.result);
43
+ }
44
+ }
45
+ return "";
46
+ }
47
+ function cellOrNull(value) {
48
+ const t = cellText(value).trim();
49
+ return t === "" ? null : t;
50
+ }
51
+ function isMergeContinuation(cell) {
52
+ if (!cell.isMerged)
53
+ return false;
54
+ const master = cell.master;
55
+ return master !== undefined && master.address !== cell.address;
56
+ }
57
+ function readWeekRow(row) {
58
+ return WEEK_COLS.map((col) => cellOrNull(row.getCell(col).value));
59
+ }
60
+ function readWeekDates(row) {
61
+ return WEEK_COLS.map((col) => {
62
+ const t = cellText(row.getCell(col).value).trim();
63
+ if (t === "" || t.toLowerCase() === "date:")
64
+ return null;
65
+ return t;
66
+ });
67
+ }
68
+ export function parseDaySheet(ws) {
69
+ const warnings = [];
70
+ const sections = [];
71
+ let current = { kind: "main", exercises: [] };
72
+ let exercise = null;
73
+ let weekDates;
74
+ const dateRow = ws.getRow(2);
75
+ if (dateRow) {
76
+ const dates = readWeekDates(dateRow);
77
+ if (dates.some((d) => d !== null))
78
+ weekDates = dates;
79
+ }
80
+ const flushExercise = (groupLabel) => {
81
+ if (!exercise)
82
+ return;
83
+ const ex = {
84
+ name: exercise.name.trim(),
85
+ sets: exercise.sets,
86
+ };
87
+ if (groupLabel)
88
+ ex.groupLabel = groupLabel;
89
+ if (exercise.notes)
90
+ ex.notes = exercise.notes;
91
+ current.exercises.push(ex);
92
+ exercise = null;
93
+ };
94
+ const flushSection = () => {
95
+ flushExercise();
96
+ sections.push({ kind: current.kind, exercises: current.exercises });
97
+ };
98
+ const lastRow = ws.actualRowCount > 0 ? ws.rowCount : 0;
99
+ let supplementalStartRow;
100
+ let supplementalNotes;
101
+ for (let r = FIRST_BODY_ROW; r <= lastRow; r++) {
102
+ const row = ws.getRow(r);
103
+ const cCell = row.getCell("C");
104
+ const cText = cellText(cCell.value).trim();
105
+ const cLower = cText.toLowerCase();
106
+ // Trigger label → switch from exercise parsing to supplemental capture.
107
+ if (SUPPLEMENTAL_TRIGGER_LABELS.has(cLower)) {
108
+ flushSection();
109
+ supplementalStartRow = r;
110
+ supplementalNotes = captureSupplementalNotes(ws, r, lastRow);
111
+ break;
112
+ }
113
+ // Group label terminates an exercise block.
114
+ if (cText !== "" && isGroupLabel(cText) && !isMergeContinuation(cCell)) {
115
+ if (!exercise) {
116
+ warnings.push(`row ${r}: group label "${cText}" without a preceding exercise name`);
117
+ continue;
118
+ }
119
+ exercise.sets.push(readWeekRow(row));
120
+ const pNote = cellText(row.getCell("P").value).trim();
121
+ if (pNote && !exercise.notes)
122
+ exercise.notes = pNote;
123
+ flushExercise(cText);
124
+ continue;
125
+ }
126
+ // Continuation of a merged exercise name?
127
+ if (isMergeContinuation(cCell) && exercise) {
128
+ exercise.sets.push(readWeekRow(row));
129
+ const pNote = cellText(row.getCell("P").value).trim();
130
+ if (pNote && !exercise.notes)
131
+ exercise.notes = pNote;
132
+ continue;
133
+ }
134
+ // New exercise block: non-empty C, not a section/group label, master cell.
135
+ if (cText !== "") {
136
+ flushExercise();
137
+ exercise = {
138
+ name: cText,
139
+ sets: [readWeekRow(row)],
140
+ };
141
+ const pNote = cellText(row.getCell("P").value).trim();
142
+ if (pNote)
143
+ exercise.notes = pNote;
144
+ continue;
145
+ }
146
+ }
147
+ if (supplementalStartRow === undefined) {
148
+ flushSection();
149
+ }
150
+ const nonEmpty = sections.filter((s) => s.exercises.length > 0);
151
+ if (nonEmpty.length === 0) {
152
+ nonEmpty.push({ kind: "main", exercises: [] });
153
+ }
154
+ return {
155
+ sections: nonEmpty,
156
+ ...(weekDates ? { weekDates } : {}),
157
+ ...(supplementalNotes && supplementalNotes.length > 0
158
+ ? { supplementalNotes }
159
+ : {}),
160
+ warnings,
161
+ };
162
+ }
163
+ /**
164
+ * Capture freeform cells in the supplemental area of a day sheet — the
165
+ * trainer-managed Sport Specific / Supplemental block below the main
166
+ * programming. Returns an array of `{ row, col, value }` entries where `row`
167
+ * is the offset from `supplementalStartRow` (so row 0 = the row containing
168
+ * the first trigger label).
169
+ *
170
+ * Only MASTER cells of merge ranges are captured (so a value that spans
171
+ * multiple cells appears once). The trigger labels themselves are skipped
172
+ * since they're added back by the writer's fixed template.
173
+ */
174
+ function captureSupplementalNotes(ws, supplementalStartRow, lastRow) {
175
+ const out = [];
176
+ for (let r = supplementalStartRow; r <= lastRow; r++) {
177
+ const row = ws.getRow(r);
178
+ for (const col of SUPPLEMENTAL_COLS) {
179
+ const cell = row.getCell(col);
180
+ if (isMergeContinuation(cell))
181
+ continue;
182
+ const value = cellText(cell.value).trim();
183
+ if (value === "")
184
+ continue;
185
+ // Skip the static template labels — the writer adds them back.
186
+ if (col === "C" &&
187
+ SUPPLEMENTAL_TRIGGER_LABELS.has(value.toLowerCase())) {
188
+ continue;
189
+ }
190
+ out.push({
191
+ row: r - supplementalStartRow,
192
+ col,
193
+ value,
194
+ });
195
+ }
196
+ }
197
+ return out;
198
+ }
199
+ //# sourceMappingURL=parse-day-sheet.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-day-sheet.js","sourceRoot":"","sources":["../../src/reader/parse-day-sheet.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { XlsxToJsonOutput } from "../schema/program.js";
2
+ export declare function parseWorkbook(xlsxPath: string): Promise<XlsxToJsonOutput>;
3
+ //# sourceMappingURL=parse-workbook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-workbook.d.ts","sourceRoot":"","sources":["../../src/reader/parse-workbook.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAG7D,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,gBAAgB,CAAC,CAyD3B"}
@@ -0,0 +1,55 @@
1
+ import ExcelJS from "exceljs";
2
+ import { parseDaySheet } from "./parse-day-sheet.js";
3
+ export async function parseWorkbook(xlsxPath) {
4
+ const workbook = new ExcelJS.Workbook();
5
+ await workbook.xlsx.readFile(xlsxPath);
6
+ const warnings = [];
7
+ const days = [];
8
+ let weekDates;
9
+ for (const ws of workbook.worksheets) {
10
+ const parsed = parseDaySheet(ws);
11
+ parsed.warnings.forEach((w) => warnings.push(`[${ws.name}] ${w}`));
12
+ if (weekDates === undefined && parsed.weekDates !== undefined) {
13
+ weekDates = parsed.weekDates;
14
+ }
15
+ days.push({
16
+ name: ws.name,
17
+ sections: parsed.sections,
18
+ ...(parsed.supplementalNotes
19
+ ? { supplementalNotes: parsed.supplementalNotes }
20
+ : {}),
21
+ });
22
+ }
23
+ // Prefer workbook custom properties (set by our own writer); fall back to
24
+ // filename parsing for source files coming straight from Olympia.
25
+ let clientName;
26
+ let programDate;
27
+ const subject = typeof workbook.subject === "string" ? workbook.subject.trim() : "";
28
+ const keywords = typeof workbook.keywords === "string" ? workbook.keywords.trim() : "";
29
+ if (subject && keywords) {
30
+ clientName = subject;
31
+ programDate = keywords;
32
+ }
33
+ else {
34
+ const match = xlsxPath.match(/([^/\\]+?)\s+Programs?\s+([\d.]+)\.xlsx$/i);
35
+ if (match && match[1] !== undefined && match[2] !== undefined) {
36
+ clientName = match[1];
37
+ programDate = match[2];
38
+ }
39
+ else {
40
+ clientName = "Unknown";
41
+ programDate = "";
42
+ warnings.push(`Could not derive client name or program date from filename or workbook properties: ${xlsxPath}`);
43
+ }
44
+ }
45
+ return {
46
+ program: {
47
+ client: clientName,
48
+ programDate,
49
+ ...(weekDates ? { weekDates } : {}),
50
+ days,
51
+ },
52
+ warnings,
53
+ };
54
+ }
55
+ //# sourceMappingURL=parse-workbook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-workbook.js","sourceRoot":"","sources":["../../src/reader/parse-workbook.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAE9B,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,QAAgB;IAEhB,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;IACxC,MAAM,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAEvC,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,IAAI,GAAwC,EAAE,CAAC;IACrD,IAAI,SAAwC,CAAC;IAE7C,KAAK,MAAM,EAAE,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC;QACrC,MAAM,MAAM,GAAG,aAAa,CAAC,EAAE,CAAC,CAAC;QACjC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;QACnE,IAAI,SAAS,KAAK,SAAS,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;YAC9D,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;QAC/B,CAAC;QACD,IAAI,CAAC,IAAI,CAAC;YACR,IAAI,EAAE,EAAE,CAAC,IAAI;YACb,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,GAAG,CAAC,MAAM,CAAC,iBAAiB;gBAC1B,CAAC,CAAC,EAAE,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,EAAE;gBACjD,CAAC,CAAC,EAAE,CAAC;SACR,CAAC,CAAC;IACL,CAAC;IAED,0EAA0E;IAC1E,kEAAkE;IAClE,IAAI,UAA8B,CAAC;IACnC,IAAI,WAA+B,CAAC;IACpC,MAAM,OAAO,GACX,OAAO,QAAQ,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACtE,MAAM,QAAQ,GACZ,OAAO,QAAQ,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACxE,IAAI,OAAO,IAAI,QAAQ,EAAE,CAAC;QACxB,UAAU,GAAG,OAAO,CAAC;QACrB,WAAW,GAAG,QAAQ,CAAC;IACzB,CAAC;SAAM,CAAC;QACN,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,2CAA2C,CAAC,CAAC;QAC1E,IAAI,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;YAC9D,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACtB,WAAW,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACzB,CAAC;aAAM,CAAC;YACN,UAAU,GAAG,SAAS,CAAC;YACvB,WAAW,GAAG,EAAE,CAAC;YACjB,QAAQ,CAAC,IAAI,CACX,sFAAsF,QAAQ,EAAE,CACjG,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO;QACL,OAAO,EAAE;YACP,MAAM,EAAE,UAAU;YAClB,WAAW;YACX,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACnC,IAAI;SACL;QACD,QAAQ;KACT,CAAC;AACJ,CAAC"}