@sharpee/chord 3.3.0 → 3.5.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/analyzer.d.ts.map +1 -1
- package/analyzer.js +158 -0
- package/analyzer.js.map +1 -1
- package/ast.d.ts +65 -0
- package/ast.d.ts.map +1 -1
- package/index.d.ts +1 -0
- package/index.d.ts.map +1 -1
- package/index.js +3 -1
- package/index.js.map +1 -1
- package/ir.d.ts +71 -0
- package/ir.d.ts.map +1 -1
- package/manifests/hunger.d.ts +18 -0
- package/manifests/hunger.d.ts.map +1 -0
- package/manifests/hunger.js +8 -0
- package/manifests/hunger.js.map +1 -0
- package/manifests/index.d.ts +2 -0
- package/manifests/index.d.ts.map +1 -1
- package/manifests/index.js +8 -2
- package/manifests/index.js.map +1 -1
- package/manifests/scoring.d.ts +25 -0
- package/manifests/scoring.d.ts.map +1 -0
- package/manifests/scoring.js +8 -0
- package/manifests/scoring.js.map +1 -0
- package/message-alias-catalog.d.ts.map +1 -1
- package/message-alias-catalog.js +9 -13
- package/message-alias-catalog.js.map +1 -1
- package/package.json +1 -1
- package/parser.d.ts.map +1 -1
- package/parser.js +183 -3
- package/parser.js.map +1 -1
- package/version.d.ts +38 -0
- package/version.d.ts.map +1 -0
- package/version.js +41 -0
- package/version.js.map +1 -0
package/analyzer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../../../repos/sharpee_v2/packages/chord/src/analyzer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAsBL,SAAS,EAKV,MAAM,UAAU,CAAC;AAIlB,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../../../repos/sharpee_v2/packages/chord/src/analyzer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAsBL,SAAS,EAKV,MAAM,UAAU,CAAC;AAIlB,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAuBL,OAAO,EACR,MAAM,SAAS,CAAC;AAsDjB;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAInD;AA8LD;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,GAAG,OAAO,CAE3E"}
|
package/analyzer.js
CHANGED
|
@@ -5,9 +5,22 @@ exports.analyze = analyze;
|
|
|
5
5
|
const catalog_js_1 = require("./catalog.js");
|
|
6
6
|
const index_js_1 = require("./manifests/index.js");
|
|
7
7
|
const phrasebooks_js_1 = require("./phrasebooks.js");
|
|
8
|
+
const version_js_1 = require("./version.js");
|
|
8
9
|
const ir_js_1 = require("./ir.js");
|
|
9
10
|
/** Phase A stories register text in this locale (design.md §2.6). */
|
|
10
11
|
const DEFAULT_LOCALE = 'en-US';
|
|
12
|
+
/**
|
|
13
|
+
* Kebab-case a quoted author string into a story key (ADR-254).
|
|
14
|
+
*
|
|
15
|
+
* Used to derive a rank's id from its name, so a rank is addressable in
|
|
16
|
+
* diagnostics and in `if.event.band_crossed` without the author declaring one.
|
|
17
|
+
*/
|
|
18
|
+
function kebabId(name) {
|
|
19
|
+
return name
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
22
|
+
.replace(/^-+|-+$/g, '');
|
|
23
|
+
}
|
|
11
24
|
/**
|
|
12
25
|
* Exit-direction opposites (parser DIRECTIONS vocabulary) — the same
|
|
13
26
|
* inference every plain exit line performs platform-side; here it backs
|
|
@@ -293,7 +306,18 @@ class Analyzer {
|
|
|
293
306
|
// ADR-215: validate `use` lines against the manifest registry — an
|
|
294
307
|
// unknown name is a compile error (the loader's trusted registry check
|
|
295
308
|
// backstops rogue IR), a duplicate is one-`use`-per-extension.
|
|
309
|
+
const announceModes = {};
|
|
310
|
+
const VALID_ANNOUNCE_MODES = ['all', 'collapsed', 'combined', 'silent'];
|
|
296
311
|
for (const use of this.ast.header?.uses ?? []) {
|
|
312
|
+
// ADR-262 D3: validate the `, announce <mode>` suffix and record it.
|
|
313
|
+
if (use.announce !== undefined) {
|
|
314
|
+
if (!VALID_ANNOUNCE_MODES.includes(use.announce)) {
|
|
315
|
+
this.diagnostics.error('analysis.invalid-announce-mode', `\`announce ${use.announce}\` is not a mode — use one of: ${VALID_ANNOUNCE_MODES.join(', ')}.`, use.span);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
announceModes[use.name] = use.announce;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
297
321
|
const manifest = index_js_1.EXTENSION_MANIFESTS.get(use.name);
|
|
298
322
|
if (!manifest) {
|
|
299
323
|
const gated = [...index_js_1.EXTENSION_MANIFESTS.values()].filter((m) => !m.core).map((m) => m.name);
|
|
@@ -310,14 +334,27 @@ class Analyzer {
|
|
|
310
334
|
this.usedExtensions.add(use.name);
|
|
311
335
|
}
|
|
312
336
|
}
|
|
337
|
+
// ADR-261 D4: scoring's constructs sit behind `use scoring`. Gating them
|
|
338
|
+
// together is what makes D3 ("absent `use scoring` means the game has no
|
|
339
|
+
// score") a rule with no exceptions — scoring is on precisely when the
|
|
340
|
+
// header says so, and there is one place to look. Reported once per
|
|
341
|
+
// construct kind, at the first offending site, rather than once per line.
|
|
342
|
+
if (!this.usedExtensions.has('scoring') && this.scoreDecls.length > 0) {
|
|
343
|
+
this.reportScoringGate('score', this.scoreDecls[0].span);
|
|
344
|
+
}
|
|
345
|
+
// Built once (it emits diagnostics), spread in only when present so the
|
|
346
|
+
// optional `hunger` field never appears as `undefined` on a story without it.
|
|
347
|
+
const hungerDef = this.buildHunger();
|
|
313
348
|
const ir = {
|
|
314
349
|
format: ir_js_1.IR_FORMAT,
|
|
350
|
+
languageVersion: version_js_1.CHORD_LANGUAGE_VERSION, // ADR-257 D3 — the language version that compiled this story
|
|
315
351
|
meta: {
|
|
316
352
|
title: this.ast.header?.title ?? '',
|
|
317
353
|
author: this.ast.header?.author ?? '',
|
|
318
354
|
fields: this.ast.header?.fields ?? {},
|
|
319
355
|
},
|
|
320
356
|
uses: [...this.usedExtensions],
|
|
357
|
+
announceModes,
|
|
321
358
|
story: {
|
|
322
359
|
states: this.storyStates,
|
|
323
360
|
reversible: this.ast.header?.statesReversible ?? false,
|
|
@@ -339,6 +376,8 @@ class Analyzer {
|
|
|
339
376
|
traits: [],
|
|
340
377
|
actions: [],
|
|
341
378
|
scores: this.scoreDecls,
|
|
379
|
+
ranks: this.buildRanks(),
|
|
380
|
+
...(hungerDef !== undefined ? { hunger: hungerDef } : {}),
|
|
342
381
|
sequences: [],
|
|
343
382
|
machines: [],
|
|
344
383
|
channels: [],
|
|
@@ -1383,6 +1422,121 @@ class Analyzer {
|
|
|
1383
1422
|
for (const s of decl.scores)
|
|
1384
1423
|
this.collectScore(s.name, s.worth, s.span, id);
|
|
1385
1424
|
}
|
|
1425
|
+
/** Construct kinds already reported by the scoring gate (one each). */
|
|
1426
|
+
scoringGateReported = new Set();
|
|
1427
|
+
/**
|
|
1428
|
+
* Report the `use scoring` gate for one construct kind (ADR-261 D4).
|
|
1429
|
+
*
|
|
1430
|
+
* A gated construct must never be silently dead, so this is an error rather
|
|
1431
|
+
* than a warning, backstopped by a `LoadError` in the story-loader for rogue
|
|
1432
|
+
* IR — the two-layer shape ADR-215 uses for `define machine`.
|
|
1433
|
+
*
|
|
1434
|
+
* The third gated construct, a `rank` rung, cannot reach this check: the
|
|
1435
|
+
* ladder is structurally inside the `use scoring` body, so a stray rung is
|
|
1436
|
+
* `parse.rank-outside-scoring` at parse time — an earlier and more precise
|
|
1437
|
+
* diagnostic. The loader's backstop covers the rogue-IR form.
|
|
1438
|
+
*/
|
|
1439
|
+
reportScoringGate(kind, span) {
|
|
1440
|
+
if (this.scoringGateReported.has(kind))
|
|
1441
|
+
return;
|
|
1442
|
+
this.scoringGateReported.add(kind);
|
|
1443
|
+
const what = kind === 'score'
|
|
1444
|
+
? 'A `score … worth N` line needs'
|
|
1445
|
+
: 'An `award` statement needs';
|
|
1446
|
+
this.diagnostics.error('analysis.scoring-needs-use', `${what} \`use scoring\` in the story header — without it the game has no score at all, and SCORE says so.`, span);
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Build the `use scoring` rank ladder (ADR-261 D2/D5).
|
|
1450
|
+
*
|
|
1451
|
+
* Rungs may be written in any order and are sorted ascending here, so the
|
|
1452
|
+
* loader and the ledger both receive a sorted ladder. Three gates:
|
|
1453
|
+
*
|
|
1454
|
+
* - **duplicate threshold** — silently keeping one rung would make the
|
|
1455
|
+
* resolved rank depend on array order (ADR-260 D2);
|
|
1456
|
+
* - **duplicate id** — two names that kebab-case alike collide in
|
|
1457
|
+
* `if.event.band_crossed`'s payload, which is keyed on the id;
|
|
1458
|
+
* - **rung above max** — an unreachable rank. This check is sound for
|
|
1459
|
+
* Chord *specifically* because Chord has no statement that changes
|
|
1460
|
+
* maxScore at runtime, so the sum of declared `worth` is the whole
|
|
1461
|
+
* ceiling. A TypeScript story calling `setMaxScore` mid-game (as dungeo
|
|
1462
|
+
* does) has no compile step and is unaffected, by design.
|
|
1463
|
+
*/
|
|
1464
|
+
buildRanks() {
|
|
1465
|
+
const declared = this.ast.header?.ranks ?? [];
|
|
1466
|
+
if (declared.length === 0)
|
|
1467
|
+
return [];
|
|
1468
|
+
const maxScore = this.scoreDecls.reduce((sum, s) => sum + s.worth, 0);
|
|
1469
|
+
const byThreshold = new Map();
|
|
1470
|
+
const byId = new Map();
|
|
1471
|
+
const ranks = [];
|
|
1472
|
+
for (const rung of declared) {
|
|
1473
|
+
const id = kebabId(rung.name);
|
|
1474
|
+
if (!id) {
|
|
1475
|
+
this.diagnostics.error('analysis.rank-id-empty', `Rank name "${rung.name}" yields no id — a rank name needs at least one letter or digit.`, rung.span);
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
const clashingThreshold = byThreshold.get(rung.threshold);
|
|
1479
|
+
if (clashingThreshold !== undefined) {
|
|
1480
|
+
this.diagnostics.error('analysis.duplicate-rank-threshold', `Two rungs share the threshold ${rung.threshold} ("${clashingThreshold}" and "${rung.name}") — which rank applies would depend on source order.`, rung.span);
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
const clashingId = byId.get(id);
|
|
1484
|
+
if (clashingId !== undefined) {
|
|
1485
|
+
this.diagnostics.error('analysis.duplicate-rank-id', `Rank names "${clashingId}" and "${rung.name}" both reduce to the id \`${id}\` — a promotion event could not tell them apart.`, rung.span);
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
if (maxScore > 0 && rung.threshold > maxScore) {
|
|
1489
|
+
this.diagnostics.error('analysis.rank-above-max', `Rank "${rung.name}" sits at ${rung.threshold}, above the ${maxScore} points this story declares — no player could reach it.`, rung.span);
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
byThreshold.set(rung.threshold, rung.name);
|
|
1493
|
+
byId.set(id, rung.name);
|
|
1494
|
+
ranks.push({
|
|
1495
|
+
id,
|
|
1496
|
+
name: rung.name,
|
|
1497
|
+
threshold: rung.threshold,
|
|
1498
|
+
...(rung.phraseKey !== undefined ? { phraseKey: rung.phraseKey } : {}),
|
|
1499
|
+
span: rung.span,
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
return ranks.sort((a, b) => a.threshold - b.threshold);
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Lower the `use hunger` body (ADR-263 D1): dedup band thresholds, sort
|
|
1506
|
+
* ascending. `grows`/`fatal` pass through for the loader's daemon and
|
|
1507
|
+
* death-trigger lowering.
|
|
1508
|
+
*/
|
|
1509
|
+
buildHunger() {
|
|
1510
|
+
const decl = this.ast.header?.hunger;
|
|
1511
|
+
if (!decl)
|
|
1512
|
+
return undefined;
|
|
1513
|
+
const seen = new Set();
|
|
1514
|
+
const rungs = [];
|
|
1515
|
+
for (const rung of decl.rungs) {
|
|
1516
|
+
if (!rung.band) {
|
|
1517
|
+
this.diagnostics.error('analysis.meter-band-empty', 'A hunger band needs a name.', rung.span);
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
if (seen.has(rung.threshold)) {
|
|
1521
|
+
this.diagnostics.error('analysis.duplicate-hunger-threshold', `Two hunger bands share threshold ${rung.threshold} — the resolved band would depend on order.`, rung.span);
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
seen.add(rung.threshold);
|
|
1525
|
+
rungs.push({
|
|
1526
|
+
id: rung.band,
|
|
1527
|
+
threshold: rung.threshold,
|
|
1528
|
+
...(rung.phraseKey !== undefined ? { phraseKey: rung.phraseKey } : {}),
|
|
1529
|
+
span: rung.span,
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
rungs.sort((a, b) => a.threshold - b.threshold);
|
|
1533
|
+
return {
|
|
1534
|
+
...(decl.grows !== undefined ? { grows: decl.grows } : {}),
|
|
1535
|
+
...(decl.fatal !== undefined ? { fatal: decl.fatal } : {}),
|
|
1536
|
+
rungs,
|
|
1537
|
+
span: decl.span,
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1386
1540
|
/** Register an owner-attached score (ratchet D12) under its qualified id. */
|
|
1387
1541
|
collectScore(name, worth, span, ownerKey) {
|
|
1388
1542
|
const qualified = ownerKey ? `${ownerKey}.${name}` : name;
|
|
@@ -1962,6 +2116,10 @@ class Analyzer {
|
|
|
1962
2116
|
span: stmt.span,
|
|
1963
2117
|
};
|
|
1964
2118
|
case 'award': {
|
|
2119
|
+
// ADR-261 D4: `award` is gated with `score` and `ranks`.
|
|
2120
|
+
if (!this.usedExtensions.has('scoring')) {
|
|
2121
|
+
this.reportScoringGate('award', stmt.span);
|
|
2122
|
+
}
|
|
1965
2123
|
// `award <score-name>` resolves owner-first (ratchet D12): the
|
|
1966
2124
|
// enclosing owner's qualified id, then the story-level bare name.
|
|
1967
2125
|
let expression = stmt.expression;
|