@ak--47/dungeon-master 1.5.2 → 1.5.3
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/.claude/skills/analyze-soup/SKILL.md +6 -1
- package/.claude/skills/create-dungeon/SKILL.md +20 -8
- package/.claude/skills/verify-dungeon/SKILL.md +28 -8
- package/.claude/skills/verify-dungeon/references/report-format.md +5 -7
- package/.claude/skills/write-hooks/SKILL.md +2 -2
- package/CHANGELOG.md +51 -0
- package/README.md +26 -0
- package/index.js +3 -1
- package/lib/core/dungeon-to-json.js +220 -0
- package/lib/core/extract-comments.js +120 -0
- package/package.json +2 -2
- package/scripts/dungeon-to-json.mjs +5 -124
- package/types.d.ts +73 -0
|
@@ -134,7 +134,12 @@ FROM (
|
|
|
134
134
|
|
|
135
135
|
## Step 3: Write Report
|
|
136
136
|
|
|
137
|
-
Create `soup-analysis.md`
|
|
137
|
+
Create `soup-analysis.md` with the structure below. For a user dungeon
|
|
138
|
+
(`dungeons/user/<name>/<name>.js`) write it into the dungeon's folder
|
|
139
|
+
(`dungeons/user/<name>/soup-analysis.md`) — everything about a dungeon lives in
|
|
140
|
+
its folder. Otherwise write it to the project root. (The generated
|
|
141
|
+
`./data/soup-analysis-EVENTS.json` is throwaway verification data — leave it in
|
|
142
|
+
`./data/`.) Contents:
|
|
138
143
|
|
|
139
144
|
1. **Config**: The soup parameters used (peaks, deviation, mean, numDays)
|
|
140
145
|
2. **Summary stats**: Total events, event count, avg EPS
|
|
@@ -18,7 +18,7 @@ box. It does **NOT** engineer story trends or magic numbers — those are the
|
|
|
18
18
|
should be:
|
|
19
19
|
|
|
20
20
|
```
|
|
21
|
-
/write-hooks dungeons/user/<
|
|
21
|
+
/write-hooks dungeons/user/<name>/<name>.js "describe the trends to engineer"
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
In scope here:
|
|
@@ -63,7 +63,7 @@ Before writing any code, scan:
|
|
|
63
63
|
- `lib/utils/utils.js` — `pickAWinner`, `weighNumRange`, `initChance`, `exhaust`,
|
|
64
64
|
`takeSome` for property value distributions
|
|
65
65
|
- `dungeons/vertical/sass.js` — B2B reference dungeon with full identity model
|
|
66
|
-
- `dungeons/user/my-buddy.js` — consumer-app reference (gitignored)
|
|
66
|
+
- `dungeons/user/my-buddy/my-buddy.js` — consumer-app reference (gitignored)
|
|
67
67
|
- `dungeons/technical/identity-model-verify.js` — minimal identity-model fixture
|
|
68
68
|
|
|
69
69
|
## File structure
|
|
@@ -524,9 +524,9 @@ userProps: { Plan: PLANS, Region: REGIONS, Role: ROLES, ... },
|
|
|
524
524
|
|
|
525
525
|
After writing the file:
|
|
526
526
|
|
|
527
|
-
1. Smoke-test: `node scripts/verify-runner.mjs dungeons/user/<
|
|
528
|
-
2. Hand to the next skill: `/write-hooks dungeons/user/<
|
|
529
|
-
3. After hooks land: `/verify-dungeon dungeons/user/<
|
|
527
|
+
1. Smoke-test: `node scripts/verify-runner.mjs dungeons/user/<name>/<name>.js verify-<name> --small`. Confirm zero errors.
|
|
528
|
+
2. Hand to the next skill: `/write-hooks dungeons/user/<name>/<name>.js "describe trends"`.
|
|
529
|
+
3. After hooks land: `/verify-dungeon dungeons/user/<name>/<name>.js`.
|
|
530
530
|
|
|
531
531
|
## Property Type Reference
|
|
532
532
|
|
|
@@ -550,8 +550,20 @@ When designing event properties, always consider which Mixpanel type best repres
|
|
|
550
550
|
|
|
551
551
|
## Output
|
|
552
552
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
(
|
|
553
|
+
**One folder per customer/dungeon.** Pick a short, kebab-case `<name>` from the
|
|
554
|
+
app/customer, then **create `dungeons/user/<name>/` if it doesn't already exist**
|
|
555
|
+
(`mkdir -p dungeons/user/<name>`) and write the dungeon to
|
|
556
|
+
`dungeons/user/<name>/<name>.js` (e.g. `dungeons/user/acme/acme.js`). Folder and
|
|
557
|
+
file share the name, matching `kodiak/kodiak.js`, `my-buddy/my-buddy.js`.
|
|
558
|
+
|
|
559
|
+
This keeps `dungeons/user/` organized — EVERYTHING about this dungeon lives in
|
|
560
|
+
the same folder: `hook-results.md` + `hook-query-log.txt` +
|
|
561
|
+
`<name>-verifications.sql` (from `verify-dungeon`), `soup-analysis.md` (from
|
|
562
|
+
`analyze-soup`), briefs, schema CSV/JSON, example data. The only thing kept
|
|
563
|
+
outside is the throwaway verification data the runs write to `./data/` (cleaned
|
|
564
|
+
after).
|
|
565
|
+
|
|
566
|
+
Do NOT inject hooks. Do NOT use `subscription`, `attribution`, `geo`,
|
|
567
|
+
`features`, or `anomalies` (the engine will silently strip them and warn).
|
|
556
568
|
|
|
557
569
|
When done, tell the user the next skill to run.
|
|
@@ -102,13 +102,33 @@ For emulator details, identity-model dungeons (must pass `profiles`), and time-s
|
|
|
102
102
|
- Experiment invariants (variant distribution, exposure timing, deterministic assignment) when any funnel uses `experiment:`
|
|
103
103
|
- SuperProp consistency, SuperProp/UserProp mirror, Mixpanel default-property casing, funnel-pre dilution
|
|
104
104
|
|
|
105
|
-
###
|
|
105
|
+
### Artifact location
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
**Everything about a dungeon lives in its folder.** When the dungeon being
|
|
108
|
+
verified is a user dungeon at `dungeons/user/<name>/<name>.js`, write ALL
|
|
109
|
+
generated artifacts into `dungeons/user/<name>/`:
|
|
110
|
+
- `hook-results.md` (Step 6)
|
|
111
|
+
- `hook-query-log.txt` (Step 5)
|
|
112
|
+
- `<name>-verifications.sql` (Step 6b)
|
|
108
113
|
|
|
109
|
-
|
|
114
|
+
The ONLY exception is the throwaway verification data the run writes to
|
|
115
|
+
`./data/` (`verify-*` event/user files) — that stays in `./data/` and is
|
|
116
|
+
deleted in Step 7.
|
|
110
117
|
|
|
111
|
-
|
|
118
|
+
For non-user dungeons (technical/vertical) or batch runs across many dungeons,
|
|
119
|
+
fall back to `./research/` for `hook-results.md` / `hook-query-log.txt`.
|
|
120
|
+
|
|
121
|
+
### Step 5: Stash query log
|
|
122
|
+
|
|
123
|
+
Write every DuckDB query execution to `hook-query-log.txt`:
|
|
124
|
+
- **User dungeon:** always write to `dungeons/user/<name>/hook-query-log.txt`.
|
|
125
|
+
- **Otherwise:** if `./research/` exists locally, write to `./research/hook-query-log.txt`; if it doesn't exist, skip — do not create the directory.
|
|
126
|
+
|
|
127
|
+
Format and conventions: see [report-format.md "Query log format"](references/report-format.md#query-log-format).
|
|
128
|
+
|
|
129
|
+
### Step 6: Write `hook-results.md`
|
|
130
|
+
|
|
131
|
+
Write to `dungeons/user/<name>/hook-results.md` for a user dungeon, else `./research/hook-results.md`. Use the templates in [report-format.md](references/report-format.md):
|
|
112
132
|
- Single-dungeon report structure
|
|
113
133
|
- Multi-dungeon report structure (when batch mode)
|
|
114
134
|
- Per-hook detail block
|
|
@@ -118,7 +138,7 @@ Use the templates in [report-format.md](references/report-format.md):
|
|
|
118
138
|
|
|
119
139
|
### Step 6b: Write verification SQL (mandatory for user dungeons)
|
|
120
140
|
|
|
121
|
-
When verifying a dungeon in `dungeons/user/`, also write a standalone DuckDB SQL file at `dungeons/user/<name>-verifications.sql`. Vertical dungeons already have their SQL in `verification/verticals/`. Format: see [report-format.md "Verification SQL file"](references/report-format.md#verification-sql-file-mandatory-for-user-dungeons).
|
|
141
|
+
When verifying a dungeon in `dungeons/user/`, also write a standalone DuckDB SQL file alongside the dungeon in its folder at `dungeons/user/<name>/<name>-verifications.sql`. Vertical dungeons already have their SQL in `verification/verticals/`. Format: see [report-format.md "Verification SQL file"](references/report-format.md#verification-sql-file-mandatory-for-user-dungeons).
|
|
122
142
|
|
|
123
143
|
### Step 7: Cleanup
|
|
124
144
|
|
|
@@ -148,9 +168,9 @@ Return-value behavior:
|
|
|
148
168
|
## Final output
|
|
149
169
|
|
|
150
170
|
Tell the user:
|
|
151
|
-
1. Report path: `./research/hook-results.md`
|
|
152
|
-
2. Verification SQL path (for user dungeons): `dungeons/user/<name>-verifications.sql`
|
|
153
|
-
3. Query log path (if written): `./research/hook-query-log.txt`
|
|
171
|
+
1. Report path: `dungeons/user/<name>/hook-results.md` (user dungeon) or `./research/hook-results.md`
|
|
172
|
+
2. Verification SQL path (for user dungeons): `dungeons/user/<name>/<name>-verifications.sql`
|
|
173
|
+
3. Query log path (if written): alongside the report (`dungeons/user/<name>/hook-query-log.txt`, else `./research/hook-query-log.txt`)
|
|
154
174
|
4. Pass/weak/fail counts (per dungeon if batch mode)
|
|
155
175
|
5. One-line summary of the most interesting finding
|
|
156
176
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Report Format
|
|
2
2
|
|
|
3
|
-
Templates and conventions for writing
|
|
3
|
+
Templates and conventions for writing `hook-results.md` and per-dungeon verification SQL. For user dungeons these live in the dungeon's folder (`dungeons/user/<name>/`); otherwise in `./research/`. See [SKILL.md "Artifact location"](../SKILL.md).
|
|
4
4
|
|
|
5
5
|
## Verdict criteria (5-tier)
|
|
6
6
|
|
|
@@ -147,9 +147,7 @@ Each hook's detailed section follows this template (same for single and multi-du
|
|
|
147
147
|
|
|
148
148
|
## Query log format
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
Check with: `ls -d ./research/ 2>/dev/null`
|
|
150
|
+
Write a plain-text log of every DuckDB query execution to `hook-query-log.txt`. For a user dungeon, write it to the dungeon's folder (`dungeons/user/<name>/hook-query-log.txt`). Otherwise, only if `./research/` exists locally write `./research/hook-query-log.txt` — if it doesn't exist, skip this step entirely (don't create the directory; check with `ls -d ./research/ 2>/dev/null`).
|
|
153
151
|
|
|
154
152
|
Use a consistent delimited format — one block per query, separated by a ruler line. DuckDB table output is preserved verbatim:
|
|
155
153
|
|
|
@@ -191,7 +189,7 @@ grep "^DUNGEON:" research/hook-query-log.txt # list of dungeons querie
|
|
|
191
189
|
|
|
192
190
|
## Verification SQL file (mandatory for user dungeons)
|
|
193
191
|
|
|
194
|
-
When verifying a dungeon in `dungeons/user/`, write a standalone DuckDB SQL file alongside the dungeon at `dungeons/user/<name>-verifications.sql`. This file is the reproducible verification artifact — anyone can re-run it against fresh data.
|
|
192
|
+
When verifying a dungeon in `dungeons/user/`, write a standalone DuckDB SQL file alongside the dungeon in its folder at `dungeons/user/<name>/<name>-verifications.sql`. This file is the reproducible verification artifact — anyone can re-run it against fresh data.
|
|
195
193
|
|
|
196
194
|
Follow the format in `verification/verticals/`:
|
|
197
195
|
|
|
@@ -200,8 +198,8 @@ Follow the format in `verification/verticals/`:
|
|
|
200
198
|
-- <name>.js — Hook Verification SQL (N hooks)
|
|
201
199
|
-- ============================================================================
|
|
202
200
|
-- USAGE:
|
|
203
|
-
-- 1. node scripts/verify-runner.mjs dungeons/user/<name>.js verify-<name>
|
|
204
|
-
-- 2. duckdb < dungeons/user/<name>-verifications.sql
|
|
201
|
+
-- 1. node scripts/verify-runner.mjs dungeons/user/<name>/<name>.js verify-<name>
|
|
202
|
+
-- 2. duckdb < dungeons/user/<name>/<name>-verifications.sql
|
|
205
203
|
-- 3. rm -f verify-<name>-*
|
|
206
204
|
-- ============================================================================
|
|
207
205
|
|
|
@@ -36,8 +36,8 @@ Out of scope:
|
|
|
36
36
|
- `lib/hook-patterns/index.js` — high-level recipes (one per Mixpanel
|
|
37
37
|
analysis type).
|
|
38
38
|
- `lib/verify/emulate-breakdown.js` — what `verify-dungeon` will check.
|
|
39
|
-
- `dungeons/user/my-buddy.js` — reference dungeon using a mix of atoms
|
|
40
|
-
hand-rolled logic.
|
|
39
|
+
- `dungeons/user/my-buddy/my-buddy.js` — reference dungeon using a mix of atoms
|
|
40
|
+
and hand-rolled logic.
|
|
41
41
|
- `dungeons/technical/pattern-*.js` — five minimal pattern fixtures, one per
|
|
42
42
|
recipe.
|
|
43
43
|
- `HOOKS.md` — encyclopedia of hook recipes organized by story pattern. Contains
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@ak--47/dungeon-master`.
|
|
4
4
|
|
|
5
|
+
## 1.5.3 — 2026-06-04
|
|
6
|
+
|
|
7
|
+
Adds two JSON/source interop helpers to the public API. No breaking changes —
|
|
8
|
+
existing exports and behavior are untouched.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`dungeonToJSON(input, options?)`** export. The inverse of `parseJSONDungeon`:
|
|
13
|
+
turns a dungeon into the `{ schema, hooks, timestamp, version }` JSON/UI wrapper
|
|
14
|
+
format. Accepts the same input flavors as the default export — a config object,
|
|
15
|
+
a `.js`/`.mjs`/`.json` file path, a raw JS source string, or an array of file
|
|
16
|
+
paths (returns an array). Output round-trips: `parseJSONDungeon(await
|
|
17
|
+
dungeonToJSON(x))` yields a runnable config. Best effort — arrow functions and
|
|
18
|
+
bound `chance.*` methods survive the round trip; detected utility calls
|
|
19
|
+
(`weighArray`, `weighNumRange`, …) are serialized by name without their
|
|
20
|
+
arguments and revive to `null` (handled gracefully by the validator). To keep
|
|
21
|
+
the field's **type** even when the generator can't be revived, every function
|
|
22
|
+
is sampled at serialization time (closures are still live) and its inferred
|
|
23
|
+
output type is recorded as `dataType` on the serialized object (e.g.
|
|
24
|
+
`{ functionName: "weighNumRange", args: [], dataType: "number" }`).
|
|
25
|
+
**Credentials (`token`, `serviceAccount`, `serviceSecret`, `projectId`,
|
|
26
|
+
`secret`) are stripped by default** so tokens never leak into JSON — pass
|
|
27
|
+
`{ includeCredentials: true }` to keep them.
|
|
28
|
+
- **`DungeonJSON`, `DungeonComments`, and `SerializedFunction` types** in
|
|
29
|
+
`types.d.ts` — the JSON-representation shapes are now formally specced.
|
|
30
|
+
- **`extractComments(input)`** export. Pulls the human-readable doc blocks out of
|
|
31
|
+
a dungeon's **source** — the `// ── OVERVIEW ──` and `// ── HOOK STORIES ──`
|
|
32
|
+
blocks plus every other `// ── LABEL ──` header that is immediately followed by
|
|
33
|
+
a block comment. Returns `{ overview, hookStories, sections }` with the comment
|
|
34
|
+
scaffolding (`// ──`, `/* */`, leading ` * `) stripped to readable prose.
|
|
35
|
+
Operates on a file path or raw source string — it never imports the dungeon,
|
|
36
|
+
since importing discards comments. Best effort: relies on the canonical
|
|
37
|
+
header + block-comment convention emitted by the `create-dungeon` /
|
|
38
|
+
`write-hooks` skills.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- **`scripts/dungeon-to-json.mjs`** is now a thin CLI wrapper over the exported
|
|
43
|
+
`dungeonToJSON` (passing `includeCredentials: true` to preserve its legacy
|
|
44
|
+
full-config UI round-trip output). The inline `convertToJSON` /
|
|
45
|
+
`convertFunctionToObject` logic moved into `lib/core/dungeon-to-json.js`.
|
|
46
|
+
|
|
47
|
+
### Why
|
|
48
|
+
|
|
49
|
+
The package could ingest JSON dungeons (`parseJSONDungeon`, `loadFromFile`,
|
|
50
|
+
`loadFromText`) but had no exported way to go the other direction, and no way to
|
|
51
|
+
programmatically read a dungeon's OVERVIEW / HOOK STORIES documentation. Both
|
|
52
|
+
existed only as un-importable script internals. Exporting them completes
|
|
53
|
+
best-effort JSON interop and lets tools (UIs, LLM pipelines) read dungeon docs
|
|
54
|
+
directly.
|
|
55
|
+
|
|
5
56
|
## 1.5.2 — 2026-05-21
|
|
6
57
|
|
|
7
58
|
Docs-only patch. Aligns the `.claude/skills/` authoring + verification
|
package/README.md
CHANGED
|
@@ -155,6 +155,32 @@ import { createTextGenerator, generateBatch } from '@ak--47/dungeon-master/text'
|
|
|
155
155
|
|
|
156
156
|
these are the same functions used internally. `pickAWinner` creates weighted distributions, `weighNumRange` generates realistic numeric ranges with configurable skew, and the text generators produce organic-looking strings with sentiment analysis and keyword injection.
|
|
157
157
|
|
|
158
|
+
### named exports
|
|
159
|
+
|
|
160
|
+
alongside the default `DUNGEON_MASTER` export, the package root exports loader + interop helpers:
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
import DUNGEON_MASTER, {
|
|
164
|
+
loadFromFile, // (path) → Promise<Dungeon> load+validate a .js/.mjs/.json dungeon
|
|
165
|
+
loadFromText, // (code) → Promise<Dungeon> load+validate a raw JS source string
|
|
166
|
+
parseJSONDungeon, // (json) → Dungeon revive a JSON dungeon into a runnable config
|
|
167
|
+
validateDungeonShape, // (config) → void throw if config isn't dungeon-shaped
|
|
168
|
+
dungeonToJSON, // (input, options?) → Promise<DungeonJSON> serialize a dungeon → JSON (inverse of parseJSONDungeon)
|
|
169
|
+
extractComments, // (input) → DungeonComments pull OVERVIEW / HOOK STORIES doc blocks from source
|
|
170
|
+
} from '@ak--47/dungeon-master';
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`dungeonToJSON` accepts a config object, a file path, raw JS source, or an array of paths, and returns the `{ schema, hooks, timestamp, version }` wrapper format. it round-trips with `parseJSONDungeon`:
|
|
174
|
+
|
|
175
|
+
```javascript
|
|
176
|
+
const json = await dungeonToJSON('./dungeons/vertical/ecommerce.js'); // creds stripped by default
|
|
177
|
+
const config = parseJSONDungeon(json); // back to a runnable dungeon
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
it's best effort — arrow functions and `chance.*` methods survive the round trip; detected utility calls (`weighArray`, `weighNumRange`, …) serialize by name without their args. pass `{ includeCredentials: true }` to keep `token` / `serviceAccount` / etc. in the output (stripped by default).
|
|
181
|
+
|
|
182
|
+
`extractComments` reads a dungeon's **source** (file path or raw text — never the imported module, since importing discards comments) and returns `{ overview, hookStories, sections }` with the comment scaffolding stripped to readable prose.
|
|
183
|
+
|
|
158
184
|
## how it works
|
|
159
185
|
|
|
160
186
|
one call to `DUNGEON_MASTER(config)` runs through these phases in order:
|
package/index.js
CHANGED
|
@@ -15,6 +15,8 @@ import { createContext, updateContextWithStorage } from './lib/core/context.js';
|
|
|
15
15
|
import { validateDungeonConfig } from './lib/core/config-validator.js';
|
|
16
16
|
import { StorageManager } from './lib/core/storage.js';
|
|
17
17
|
import { detectInputType, loadFromFile, loadFromText, parseJSONDungeon, validateDungeonShape } from './lib/core/dungeon-loader.js';
|
|
18
|
+
import { dungeonToJSON } from './lib/core/dungeon-to-json.js';
|
|
19
|
+
import { extractComments } from './lib/core/extract-comments.js';
|
|
18
20
|
|
|
19
21
|
// Orchestrators
|
|
20
22
|
import { userLoop } from './lib/orchestrators/user-loop.js';
|
|
@@ -597,5 +599,5 @@ function extractStorageData(storage) {
|
|
|
597
599
|
|
|
598
600
|
// ES Module exports
|
|
599
601
|
export default DUNGEON_MASTER;
|
|
600
|
-
export { parseJSONDungeon, validateDungeonShape, loadFromFile, loadFromText };
|
|
602
|
+
export { parseJSONDungeon, validateDungeonShape, loadFromFile, loadFromText, dungeonToJSON, extractComments };
|
|
601
603
|
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dungeon → JSON serialization.
|
|
3
|
+
*
|
|
4
|
+
* The inverse of `parseJSONDungeon` (lib/core/dungeon-loader.js): turns a runnable
|
|
5
|
+
* dungeon into the UI/JSON wrapper format `{ schema, hooks, timestamp, version }`.
|
|
6
|
+
* Functions in the schema are serialized to `{ functionName, body }` / `{ functionName, args }`
|
|
7
|
+
* objects that `reviveJSONConfig` knows how to revive — so the output round-trips:
|
|
8
|
+
*
|
|
9
|
+
* parseJSONDungeon(await dungeonToJSON(cfg)) → runnable config
|
|
10
|
+
*
|
|
11
|
+
* This is BEST EFFORT. Arrow functions and bound `chance.*` methods round-trip cleanly;
|
|
12
|
+
* detected utility calls (weighArray, weighNumRange, …) are serialized without their
|
|
13
|
+
* arguments and revive to null, which the config validator handles gracefully.
|
|
14
|
+
*
|
|
15
|
+
* To preserve the field's TYPE even when the generator can't be revived, every function
|
|
16
|
+
* is sampled at serialization time (when its closure is still live) and the inferred
|
|
17
|
+
* output type is recorded as `dataType` (e.g. "number", "string", "boolean", "number[]").
|
|
18
|
+
* So a field that loses its `weighNumRange(1,10)` generator still records `dataType: "number"`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { detectInputType, loadFromFile, loadFromText } from './dungeon-loader.js';
|
|
22
|
+
|
|
23
|
+
/** Credential keys stripped from JSON output unless `includeCredentials` is set. */
|
|
24
|
+
const CREDENTIAL_KEYS = ['token', 'serviceAccount', 'serviceSecret', 'projectId', 'secret'];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Convert a dungeon into its JSON representation.
|
|
28
|
+
*
|
|
29
|
+
* Accepts the same input flavors as the default export: a config object, a path to a
|
|
30
|
+
* `.js`/`.mjs`/`.json` dungeon file, a raw JS source string (must `export default`), or
|
|
31
|
+
* an array of file paths (returns an array of results).
|
|
32
|
+
*
|
|
33
|
+
* @param {import('../../types').Dungeon | string | string[]} input
|
|
34
|
+
* @param {{ includeCredentials?: boolean }} [options]
|
|
35
|
+
* @returns {Promise<import('../../types').DungeonJSON | import('../../types').DungeonJSON[]>}
|
|
36
|
+
*/
|
|
37
|
+
export async function dungeonToJSON(input, options = {}) {
|
|
38
|
+
const { includeCredentials = false } = options;
|
|
39
|
+
const { type, value } = detectInputType(input);
|
|
40
|
+
|
|
41
|
+
switch (type) {
|
|
42
|
+
case 'object':
|
|
43
|
+
return serializeConfig(value, includeCredentials);
|
|
44
|
+
case 'file':
|
|
45
|
+
return serializeConfig(await loadFromFile(value), includeCredentials);
|
|
46
|
+
case 'text':
|
|
47
|
+
return serializeConfig(await loadFromText(value), includeCredentials);
|
|
48
|
+
case 'files':
|
|
49
|
+
return Promise.all(
|
|
50
|
+
value.map(async (p) => serializeConfig(await loadFromFile(p), includeCredentials))
|
|
51
|
+
);
|
|
52
|
+
default:
|
|
53
|
+
throw new Error(`dungeon-master: dungeonToJSON cannot handle input type "${type}".`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build the `{ schema, hooks, timestamp, version }` wrapper from a runnable config.
|
|
59
|
+
* @param {import('../../types').Dungeon} config
|
|
60
|
+
* @param {boolean} includeCredentials
|
|
61
|
+
* @returns {import('../../types').DungeonJSON}
|
|
62
|
+
*/
|
|
63
|
+
function serializeConfig(config, includeCredentials) {
|
|
64
|
+
// Hook may be a live function (from .js/text/object) or already a string (from .json).
|
|
65
|
+
const hook = config.hook;
|
|
66
|
+
const hooks = typeof hook === 'function'
|
|
67
|
+
? hook.toString()
|
|
68
|
+
: (typeof hook === 'string' ? hook : null);
|
|
69
|
+
|
|
70
|
+
// Strip the hook (serialized separately) and, by default, credentials (don't leak tokens).
|
|
71
|
+
const cleanConfig = { ...config };
|
|
72
|
+
delete cleanConfig.hook;
|
|
73
|
+
if (!includeCredentials) {
|
|
74
|
+
for (const key of CREDENTIAL_KEYS) delete cleanConfig[key];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
schema: convertToJSON(cleanConfig),
|
|
79
|
+
hooks,
|
|
80
|
+
timestamp: new Date().toISOString(),
|
|
81
|
+
version: '4.0'
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Convert a JavaScript value to a JSON-serializable form, turning functions into
|
|
87
|
+
* `{ functionName, body | args }` objects that `reviveJSONConfig` can revive.
|
|
88
|
+
* @param {any} value
|
|
89
|
+
* @returns {any}
|
|
90
|
+
*/
|
|
91
|
+
export function convertToJSON(value) {
|
|
92
|
+
// Null/undefined
|
|
93
|
+
if (value === null || value === undefined) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Primitives
|
|
98
|
+
if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string') {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Functions - convert to object representation
|
|
103
|
+
if (typeof value === 'function') {
|
|
104
|
+
return convertFunctionToObject(value);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Arrays
|
|
108
|
+
if (Array.isArray(value)) {
|
|
109
|
+
return value.map(item => convertToJSON(item));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Objects
|
|
113
|
+
if (typeof value === 'object') {
|
|
114
|
+
const result = {};
|
|
115
|
+
for (const [key, val] of Object.entries(value)) {
|
|
116
|
+
result[key] = convertToJSON(val);
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Fallback
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Convert a function to its object representation.
|
|
127
|
+
* Arrow functions and bound `chance.*` methods round-trip cleanly; detected utility
|
|
128
|
+
* functions are stored by name without args (best effort). Every form also carries a
|
|
129
|
+
* sampled `dataType` so the field's output type survives even when the generator can't.
|
|
130
|
+
* @param {Function} func
|
|
131
|
+
* @returns {import('../../types').SerializedFunction}
|
|
132
|
+
*/
|
|
133
|
+
function convertFunctionToObject(func) {
|
|
134
|
+
const funcString = func.toString();
|
|
135
|
+
// Sample now, while the closure is still live — the only reliable time to learn the type.
|
|
136
|
+
const dataType = inferDataType(func);
|
|
137
|
+
|
|
138
|
+
// Arrow function
|
|
139
|
+
if (funcString.startsWith('(') || funcString.startsWith('_') || funcString.includes('=>')) {
|
|
140
|
+
return withDataType({ functionName: 'arrow', body: funcString }, dataType);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Bound chance methods (e.g., chance.name.bind(chance))
|
|
144
|
+
if (funcString.includes('.bind(')) {
|
|
145
|
+
const match = funcString.match(/chance\.(\w+)\.bind/);
|
|
146
|
+
if (match) {
|
|
147
|
+
return withDataType({ functionName: `chance.${match[1]}`, args: [] }, dataType);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Try to detect common utility functions
|
|
152
|
+
// This is a best-effort approach - some complex functions might not be detected
|
|
153
|
+
const commonFunctions = [
|
|
154
|
+
'weighNumRange',
|
|
155
|
+
'weighArray',
|
|
156
|
+
'weighChoices',
|
|
157
|
+
'pickAWinner',
|
|
158
|
+
'date',
|
|
159
|
+
'integer',
|
|
160
|
+
'uid',
|
|
161
|
+
'comma'
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
for (const fnName of commonFunctions) {
|
|
165
|
+
if (funcString.includes(fnName)) {
|
|
166
|
+
// Args can't be recovered from a stringified function, but dataType is captured.
|
|
167
|
+
return withDataType({ functionName: fnName, args: [] }, dataType);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Generic function - just store as arrow function
|
|
172
|
+
return withDataType({ functionName: 'arrow', body: funcString }, dataType);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Attach a `dataType` field if one was inferred (omitted otherwise).
|
|
177
|
+
* @param {import('../../types').SerializedFunction} obj
|
|
178
|
+
* @param {string | undefined} dataType
|
|
179
|
+
* @returns {import('../../types').SerializedFunction}
|
|
180
|
+
*/
|
|
181
|
+
function withDataType(obj, dataType) {
|
|
182
|
+
if (dataType) obj.dataType = dataType;
|
|
183
|
+
return obj;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Sample a function (no args) and classify its return type. Best effort — returns
|
|
188
|
+
* undefined if the call throws or yields an indeterminate value.
|
|
189
|
+
* @param {Function} func
|
|
190
|
+
* @returns {string | undefined}
|
|
191
|
+
*/
|
|
192
|
+
function inferDataType(func) {
|
|
193
|
+
let value;
|
|
194
|
+
try {
|
|
195
|
+
value = func();
|
|
196
|
+
} catch {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
return classifyValue(value);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Map a sampled value to a type label: "number" | "string" | "boolean" | "date" |
|
|
204
|
+
* "object" | "<elementType>[]" | "array". Returns undefined for null/undefined/functions.
|
|
205
|
+
* @param {any} value
|
|
206
|
+
* @returns {string | undefined}
|
|
207
|
+
*/
|
|
208
|
+
function classifyValue(value) {
|
|
209
|
+
if (value === null || value === undefined) return undefined;
|
|
210
|
+
if (Array.isArray(value)) {
|
|
211
|
+
const el = value.find((v) => v !== null && v !== undefined);
|
|
212
|
+
const elType = el === undefined ? undefined : classifyValue(el);
|
|
213
|
+
return elType ? `${elType}[]` : 'array';
|
|
214
|
+
}
|
|
215
|
+
if (value instanceof Date) return 'date';
|
|
216
|
+
const t = typeof value;
|
|
217
|
+
if (t === 'number' || t === 'string' || t === 'boolean') return t;
|
|
218
|
+
if (t === 'object') return 'object';
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort extraction of the human-readable doc blocks from a dungeon's SOURCE.
|
|
3
|
+
*
|
|
4
|
+
* Generated/authored dungeons separate sections with box-drawing headers followed by a
|
|
5
|
+
* block comment, e.g.:
|
|
6
|
+
*
|
|
7
|
+
* // ── OVERVIEW ──
|
|
8
|
+
* /* ... *\/
|
|
9
|
+
* // ── HOOK STORIES ──
|
|
10
|
+
* /* ... *\/
|
|
11
|
+
*
|
|
12
|
+
* This pulls those blocks out as cleaned prose. It operates on RAW SOURCE TEXT — it never
|
|
13
|
+
* imports the dungeon, because importing a module discards its comments.
|
|
14
|
+
*
|
|
15
|
+
* Returns `{ overview, hookStories, sections }`:
|
|
16
|
+
* - `overview` — cleaned text of the OVERVIEW block (or null)
|
|
17
|
+
* - `hookStories` — cleaned text of the HOOK STORIES block (or null)
|
|
18
|
+
* - `sections` — every `// ── LABEL ──` header that is immediately followed by a
|
|
19
|
+
* block comment, keyed by the exact label as written.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readFileSync } from 'fs';
|
|
23
|
+
import { detectInputType } from './dungeon-loader.js';
|
|
24
|
+
|
|
25
|
+
// A `// ── LABEL ──` header line. Anchored to `//` at line start so that the inner
|
|
26
|
+
// ` * ───────` dividers inside a block comment are NOT mistaken for section headers.
|
|
27
|
+
const HEADER_RE = /^[ \t]*\/\/[ \t]*─+[ \t]*(.+?)[ \t]*─+[ \t]*$/gm;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string | string[]} input - A dungeon file path, raw dungeon source, or array of paths.
|
|
31
|
+
* @returns {import('../../types').DungeonComments | import('../../types').DungeonComments[]}
|
|
32
|
+
*/
|
|
33
|
+
export function extractComments(input) {
|
|
34
|
+
const { type, value } = detectInputType(input);
|
|
35
|
+
|
|
36
|
+
switch (type) {
|
|
37
|
+
case 'file':
|
|
38
|
+
return parseSource(readFileSync(value, 'utf-8'));
|
|
39
|
+
case 'text':
|
|
40
|
+
return parseSource(value);
|
|
41
|
+
case 'files':
|
|
42
|
+
return value.map((p) => parseSource(readFileSync(p, 'utf-8')));
|
|
43
|
+
case 'object':
|
|
44
|
+
throw new Error(
|
|
45
|
+
'dungeon-master: extractComments needs source text or a file path, not a config object (comments are lost once a dungeon is imported).'
|
|
46
|
+
);
|
|
47
|
+
default:
|
|
48
|
+
throw new Error(`dungeon-master: extractComments cannot handle input type "${type}".`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse a single source string into `{ overview, hookStories, sections }`.
|
|
54
|
+
* @param {string} source
|
|
55
|
+
* @returns {import('../../types').DungeonComments}
|
|
56
|
+
*/
|
|
57
|
+
function parseSource(source) {
|
|
58
|
+
/** @type {Record<string, string>} */
|
|
59
|
+
const sections = {};
|
|
60
|
+
|
|
61
|
+
// Collect every header with its label and the position right after its line.
|
|
62
|
+
const headers = [];
|
|
63
|
+
HEADER_RE.lastIndex = 0;
|
|
64
|
+
let m;
|
|
65
|
+
while ((m = HEADER_RE.exec(source)) !== null) {
|
|
66
|
+
headers.push({ label: m[1].trim(), end: HEADER_RE.lastIndex });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < headers.length; i++) {
|
|
70
|
+
const { label, end } = headers[i];
|
|
71
|
+
// Only look between this header and the next one.
|
|
72
|
+
const sliceEnd = i + 1 < headers.length ? headers[i + 1].end : source.length;
|
|
73
|
+
const slice = source.slice(end, sliceEnd);
|
|
74
|
+
|
|
75
|
+
// Accept the block only if a `/* ... */` comment is the first non-whitespace content
|
|
76
|
+
// after the header (so headers followed by code — IMPORTS, SCALE — are skipped).
|
|
77
|
+
const block = slice.match(/^\s*\/\*([\s\S]*?)\*\//);
|
|
78
|
+
if (block) {
|
|
79
|
+
sections[label] = cleanBlock(block[1]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
overview: findSection(sections, ['OVERVIEW']),
|
|
85
|
+
hookStories: findSection(sections, ['HOOK STORIES', 'HOOK STORY']),
|
|
86
|
+
sections
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Strip block-comment scaffolding (` * `) and trim blank edges, leaving readable prose.
|
|
92
|
+
* @param {string} inner - The text between `/*` and `*\/`.
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
function cleanBlock(inner) {
|
|
96
|
+
const lines = inner
|
|
97
|
+
.split('\n')
|
|
98
|
+
// Drop a leading ` * ` (or bare ` *`) from each line; preserve content indentation.
|
|
99
|
+
.map((line) => line.replace(/^[ \t]*\*[ \t]?/, '').replace(/\s+$/, ''));
|
|
100
|
+
|
|
101
|
+
// Trim leading/trailing blank lines.
|
|
102
|
+
while (lines.length && lines[0] === '') lines.shift();
|
|
103
|
+
while (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
104
|
+
|
|
105
|
+
return lines.join('\n');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Case-insensitive lookup of the first matching label.
|
|
110
|
+
* @param {Record<string, string>} sections
|
|
111
|
+
* @param {string[]} candidates
|
|
112
|
+
* @returns {string | null}
|
|
113
|
+
*/
|
|
114
|
+
function findSection(sections, candidates) {
|
|
115
|
+
const wanted = candidates.map((c) => c.toUpperCase());
|
|
116
|
+
for (const [label, text] of Object.entries(sections)) {
|
|
117
|
+
if (wanted.includes(label.toUpperCase())) return text;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Converts a JavaScript dungeon file to JSON that can be loaded into the UI
|
|
4
|
+
* Converts a JavaScript dungeon file to JSON that can be loaded into the UI.
|
|
5
|
+
* Thin CLI wrapper around the exported `dungeonToJSON` (lib/core/dungeon-to-json.js).
|
|
5
6
|
* Usage: node scripts/dungeon-to-json.js <input.js> [output-name]
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { writeFileSync } from 'fs';
|
|
9
10
|
import path from 'path';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
import { dungeonToJSON } from '../lib/core/dungeon-to-json.js';
|
|
13
12
|
|
|
14
13
|
// Get command line arguments
|
|
15
14
|
const args = process.argv.slice(2);
|
|
@@ -25,34 +24,10 @@ const inputPath = path.resolve(args[0]);
|
|
|
25
24
|
const outputName = args[1] || path.basename(inputPath, '.js');
|
|
26
25
|
|
|
27
26
|
try {
|
|
28
|
-
// Import the JavaScript module
|
|
29
27
|
console.log(`📖 Loading ${inputPath}...`);
|
|
30
|
-
const module = await import(`file://${inputPath}`);
|
|
31
|
-
const config = module.default;
|
|
32
|
-
|
|
33
|
-
if (!config) {
|
|
34
|
-
throw new Error('No default export found in the module');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Extract hooks if they exist
|
|
38
|
-
const hooksFunction = config.hook;
|
|
39
|
-
const hooksString = hooksFunction ? hooksFunction.toString() : null;
|
|
40
|
-
|
|
41
|
-
// Create a clean config without the hook function
|
|
42
|
-
const cleanConfig = { ...config };
|
|
43
|
-
delete cleanConfig.hook;
|
|
44
|
-
|
|
45
|
-
// Convert to JSON-serializable format
|
|
46
28
|
console.log('🔄 Converting to JSON format...');
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// Create the dungeon state object (same format as UI saves)
|
|
50
|
-
const dungeonState = {
|
|
51
|
-
schema,
|
|
52
|
-
hooks: hooksString,
|
|
53
|
-
timestamp: new Date().toISOString(),
|
|
54
|
-
version: '4.0'
|
|
55
|
-
};
|
|
29
|
+
// includeCredentials: true preserves the legacy UI round-trip behavior (full config).
|
|
30
|
+
const dungeonState = await dungeonToJSON(inputPath, { includeCredentials: true });
|
|
56
31
|
|
|
57
32
|
// Write to JSON file
|
|
58
33
|
const outputPath = path.join(path.dirname(inputPath), `${outputName}.json`);
|
|
@@ -67,97 +42,3 @@ try {
|
|
|
67
42
|
console.error(error.stack);
|
|
68
43
|
process.exit(1);
|
|
69
44
|
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Convert JavaScript config to JSON-serializable format
|
|
73
|
-
* Detects functions and converts them to object representation
|
|
74
|
-
*/
|
|
75
|
-
function convertToJSON(value) {
|
|
76
|
-
// Null/undefined
|
|
77
|
-
if (value === null || value === undefined) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Primitives
|
|
82
|
-
if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string') {
|
|
83
|
-
return value;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Functions - convert to object representation
|
|
87
|
-
if (typeof value === 'function') {
|
|
88
|
-
return convertFunctionToObject(value);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Arrays
|
|
92
|
-
if (Array.isArray(value)) {
|
|
93
|
-
return value.map(item => convertToJSON(item));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Objects
|
|
97
|
-
if (typeof value === 'object') {
|
|
98
|
-
const result = {};
|
|
99
|
-
for (const [key, val] of Object.entries(value)) {
|
|
100
|
-
result[key] = convertToJSON(val);
|
|
101
|
-
}
|
|
102
|
-
return result;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Fallback
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Convert a function to its object representation
|
|
111
|
-
* Tries to detect the function type and extract parameters
|
|
112
|
-
*/
|
|
113
|
-
function convertFunctionToObject(func) {
|
|
114
|
-
const funcString = func.toString();
|
|
115
|
-
|
|
116
|
-
// Arrow function
|
|
117
|
-
if (funcString.startsWith('(') || funcString.startsWith('_') || funcString.includes('=>')) {
|
|
118
|
-
return {
|
|
119
|
-
functionName: 'arrow',
|
|
120
|
-
body: funcString
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Bound chance methods (e.g., chance.name.bind(chance))
|
|
125
|
-
if (funcString.includes('.bind(')) {
|
|
126
|
-
const match = funcString.match(/chance\.(\w+)\.bind/);
|
|
127
|
-
if (match) {
|
|
128
|
-
return {
|
|
129
|
-
functionName: `chance.${match[1]}`,
|
|
130
|
-
args: []
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Try to detect common utility functions
|
|
136
|
-
// This is a best-effort approach - some complex functions might not be detected
|
|
137
|
-
const commonFunctions = [
|
|
138
|
-
'weighNumRange',
|
|
139
|
-
'weighArray',
|
|
140
|
-
'weighChoices',
|
|
141
|
-
'pickAWinner',
|
|
142
|
-
'date',
|
|
143
|
-
'integer',
|
|
144
|
-
'uid',
|
|
145
|
-
'comma'
|
|
146
|
-
];
|
|
147
|
-
|
|
148
|
-
for (const fnName of commonFunctions) {
|
|
149
|
-
if (funcString.includes(fnName)) {
|
|
150
|
-
// Extract args (this is simplified - real parsing would be more complex)
|
|
151
|
-
return {
|
|
152
|
-
functionName: fnName,
|
|
153
|
-
args: [] // Args would need to be extracted, but that's complex
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Generic function - just store as arrow function
|
|
159
|
-
return {
|
|
160
|
-
functionName: 'arrow',
|
|
161
|
-
body: funcString
|
|
162
|
-
};
|
|
163
|
-
}
|
package/types.d.ts
CHANGED
|
@@ -1508,6 +1508,79 @@ export declare function parseJSONDungeon(json: object): Dungeon;
|
|
|
1508
1508
|
/** Validate that an object has the minimum shape of a dungeon config. Throws on shape violations. */
|
|
1509
1509
|
export declare function validateDungeonShape(config: unknown): void;
|
|
1510
1510
|
|
|
1511
|
+
/**
|
|
1512
|
+
* The serialized form of a function found in a dungeon schema. Produced by `dungeonToJSON`
|
|
1513
|
+
* and revived by `parseJSONDungeon`.
|
|
1514
|
+
*
|
|
1515
|
+
* - `functionName: "arrow"` with a `body` — an inline/closure function, re-eval'd on revive.
|
|
1516
|
+
* - `functionName: "chance.<method>"` — a bound chance method, e.g. `chance.name.bind(chance)`.
|
|
1517
|
+
* - `functionName: "<utility>"` (weighNumRange, weighArray, …) — a detected utility; args are
|
|
1518
|
+
* not recoverable from the stringified source, so it revives to null (best effort).
|
|
1519
|
+
*
|
|
1520
|
+
* `dataType` records the function's sampled output type so the field's type is preserved even
|
|
1521
|
+
* when the generator itself can't be revived.
|
|
1522
|
+
*/
|
|
1523
|
+
export interface SerializedFunction {
|
|
1524
|
+
/** "arrow", "chance.<method>", or a known utility name. */
|
|
1525
|
+
functionName: string;
|
|
1526
|
+
/** Stringified function source (present for arrow/closure forms). */
|
|
1527
|
+
body?: string;
|
|
1528
|
+
/** Captured call arguments (best effort; usually empty for detected utilities). */
|
|
1529
|
+
args?: unknown[];
|
|
1530
|
+
/**
|
|
1531
|
+
* Inferred output type, sampled at serialization time:
|
|
1532
|
+
* "number" | "string" | "boolean" | "date" | "object" | "<elementType>[]" | "array".
|
|
1533
|
+
* Omitted when sampling failed (e.g. the function threw).
|
|
1534
|
+
*/
|
|
1535
|
+
dataType?: string;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
/**
|
|
1539
|
+
* The JSON/UI wrapper representation of a dungeon, as produced by `dungeonToJSON` and
|
|
1540
|
+
* consumed by `parseJSONDungeon`. Functions in `schema` are serialized to
|
|
1541
|
+
* {@link SerializedFunction} objects; everything else is plain JSON.
|
|
1542
|
+
*/
|
|
1543
|
+
export interface DungeonJSON {
|
|
1544
|
+
/** The dungeon config with functions converted to {@link SerializedFunction} objects (hook excluded). */
|
|
1545
|
+
schema: Record<string, unknown>;
|
|
1546
|
+
/** The `hook` function stringified, or null if the dungeon has no hook. */
|
|
1547
|
+
hooks: string | null;
|
|
1548
|
+
/** ISO timestamp of when the JSON was produced. */
|
|
1549
|
+
timestamp: string;
|
|
1550
|
+
/** UI schema format version. */
|
|
1551
|
+
version: string;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/** The doc blocks extracted from a dungeon's source by `extractComments`. */
|
|
1555
|
+
export interface DungeonComments {
|
|
1556
|
+
/** Cleaned text of the `// ── OVERVIEW ──` block, or null if absent. */
|
|
1557
|
+
overview: string | null;
|
|
1558
|
+
/** Cleaned text of the `// ── HOOK STORIES ──` block, or null if absent. */
|
|
1559
|
+
hookStories: string | null;
|
|
1560
|
+
/** Every `// ── LABEL ──` header followed by a block comment, keyed by exact label. */
|
|
1561
|
+
sections: Record<string, string>;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
/**
|
|
1565
|
+
* Convert a dungeon into its JSON representation (the inverse of `parseJSONDungeon`).
|
|
1566
|
+
* Accepts a config object, a file path, raw JS source, or an array of file paths
|
|
1567
|
+
* (returns an array). Credentials are stripped unless `includeCredentials` is set.
|
|
1568
|
+
* Best effort: arrow functions and `chance.*` methods round-trip; detected utility
|
|
1569
|
+
* calls lose their arguments. Always async.
|
|
1570
|
+
*/
|
|
1571
|
+
export declare function dungeonToJSON(input: Dungeon, options?: { includeCredentials?: boolean }): Promise<DungeonJSON>;
|
|
1572
|
+
export declare function dungeonToJSON(input: string, options?: { includeCredentials?: boolean }): Promise<DungeonJSON>;
|
|
1573
|
+
export declare function dungeonToJSON(input: string[], options?: { includeCredentials?: boolean }): Promise<DungeonJSON[]>;
|
|
1574
|
+
|
|
1575
|
+
/**
|
|
1576
|
+
* Extract the human-readable doc blocks (OVERVIEW, HOOK STORIES, …) from a dungeon's
|
|
1577
|
+
* SOURCE. Operates on a file path or raw source string — never imports the dungeon
|
|
1578
|
+
* (importing discards comments). Best effort: relies on the canonical `// ── LABEL ──`
|
|
1579
|
+
* header + block-comment convention.
|
|
1580
|
+
*/
|
|
1581
|
+
export declare function extractComments(input: string): DungeonComments;
|
|
1582
|
+
export declare function extractComments(input: string[]): DungeonComments[];
|
|
1583
|
+
|
|
1511
1584
|
// ============= Text Generator Types =============
|
|
1512
1585
|
|
|
1513
1586
|
/**
|